chiark / gitweb /
Merge from dmanual branch
[disorder] / cgi / actions.c
CommitLineData
bca4e2b7
RK
1/*
2 * This file is part of DisOrder.
3 * Copyright (C) 2004-2008 Richard Kettlewell
4 *
e7eb3a27 5 * This program is free software: you can redistribute it and/or modify
bca4e2b7 6 * it under the terms of the GNU General Public License as published by
e7eb3a27 7 * the Free Software Foundation, either version 3 of the License, or
bca4e2b7
RK
8 * (at your option) any later version.
9 *
e7eb3a27
RK
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
bca4e2b7 15 * You should have received a copy of the GNU General Public License
e7eb3a27 16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
bca4e2b7 17 */
59cf25c4 18/** @file cgi/actions.c
1e97629d
RK
19 * @brief DisOrder web actions
20 *
21 * Actions are anything that the web interface does beyond passive template
22 * expansion and inspection of state recieved from the server. This means
23 * playing tracks, editing prefs etc but also setting extra headers e.g. to
24 * auto-refresh the playing list.
59cf25c4
RK
25 *
26 * See @ref lib/macros-builtin.c for docstring syntax.
1e97629d 27 */
bca4e2b7 28
1e97629d 29#include "disorder-cgi.h"
5a7df048
RK
30
31/** @brief Redirect to some other action or URL */
32static void redirect(const char *url) {
33 /* By default use the 'back' argument */
34 if(!url)
71634563 35 url = cgi_get("back");
04aa2c4f 36 if(url && *url) {
e7ce7665 37 if(strncmp(url, "http", 4))
5a7df048
RK
38 /* If the target is not a full URL assume it's the action */
39 url = cgi_makeurl(config->url, "action", url, (char *)0);
40 } else {
41 /* If back= is not set just go back to the front page */
42 url = config->url;
43 }
44 if(printf("Location: %s\n"
45 "%s\n"
1e97629d 46 "\n", url, dcgi_cookie_header()) < 0)
2e9ba080 47 disorder_fatal(errno, "error writing to stdout");
5a7df048
RK
48}
49
59cf25c4 50/*$ playing
5c1ae3bc
RK
51 *
52 * Expands \fIplaying.tmpl\fR as if there was no special 'playing' action, but
53 * adds a Refresh: field to the HTTP header. The maximum refresh interval is
54 * defined by \fBrefresh\fR (see \fBdisorder_config\fR(5)) but may be less if
55 * the end of the track is near.
56 */
59cf25c4 57/*$ manage
5c1ae3bc
RK
58 *
59 * Expands \fIplaying.tmpl\fR (NB not \fImanage.tmpl\fR) as if there was no
60 * special 'playing' action, and adds a Refresh: field to the HTTP header. The
61 * maximum refresh interval is defined by \Bfrefresh\fR (see
62 * \fBdisorder_config\fR(5)) but may be less if the end of the track is near.
63 */
448d3570
RK
64static void act_playing(void) {
65 long refresh = config->refresh;
66 long length;
67 time_t now, fin;
68 char *url;
71634563 69 const char *action;
448d3570 70
1e97629d
RK
71 dcgi_lookup(DCGI_PLAYING|DCGI_QUEUE|DCGI_ENABLED|DCGI_RANDOM_ENABLED);
72 if(dcgi_playing
73 && dcgi_playing->state == playing_started /* i.e. not paused */
74 && !disorder_length(dcgi_client, dcgi_playing->track, &length)
448d3570 75 && length
1e97629d 76 && dcgi_playing->sofar >= 0) {
448d3570 77 /* Try to put the next refresh at the start of the next track. */
4265e5d3 78 xtime(&now);
1e97629d 79 fin = now + length - dcgi_playing->sofar + config->gap;
448d3570
RK
80 if(now + refresh > fin)
81 refresh = fin - now;
82 }
6a5964b7 83 if(dcgi_queue && dcgi_queue->origin == origin_scratch) {
448d3570
RK
84 /* next track is a scratch, don't leave more than the inter-track gap */
85 if(refresh > config->gap)
86 refresh = config->gap;
87 }
1e97629d
RK
88 if(!dcgi_playing
89 && ((dcgi_queue
2dc2f478 90 && dcgi_queue->origin != origin_random)
1e97629d
RK
91 || dcgi_random_enabled)
92 && dcgi_enabled) {
448d3570
RK
93 /* no track playing but playing is enabled and there is something coming
94 * up, must be in a gap */
95 if(refresh > config->gap)
96 refresh = config->gap;
97 }
98 if((action = cgi_get("action")))
99 url = cgi_makeurl(config->url, "action", action, (char *)0);
100 else
101 url = config->url;
e7ce7665
RK
102 if(printf("Refresh: %ld;url=%s\n",
103 refresh, url) < 0)
2e9ba080 104 disorder_fatal(errno, "error writing to stdout");
e7ce7665 105 dcgi_expand("playing", 1);
1e97629d
RK
106}
107
59cf25c4 108/*$ disable
5c1ae3bc
RK
109 *
110 * Disables play.
111 */
1e97629d
RK
112static void act_disable(void) {
113 if(dcgi_client)
114 disorder_disable(dcgi_client);
115 redirect(0);
116}
117
59cf25c4 118/*$ enable
5c1ae3bc
RK
119 *
120 * Enables play.
121 */
1e97629d
RK
122static void act_enable(void) {
123 if(dcgi_client)
124 disorder_enable(dcgi_client);
125 redirect(0);
126}
127
59cf25c4 128/*$ random-disable
5c1ae3bc
RK
129 *
130 * Disables random play.
131 */
1e97629d
RK
132static void act_random_disable(void) {
133 if(dcgi_client)
134 disorder_random_disable(dcgi_client);
135 redirect(0);
136}
137
59cf25c4 138/*$ random-enable
5c1ae3bc
RK
139 *
140 * Enables random play.
141 */
1e97629d
RK
142static void act_random_enable(void) {
143 if(dcgi_client)
144 disorder_random_enable(dcgi_client);
145 redirect(0);
448d3570
RK
146}
147
59cf25c4 148/*$ pause
5c1ae3bc
RK
149 *
150 * Pauses the current track (if there is one and it's not paused already).
151 */
6d9dd8d9
RK
152static void act_pause(void) {
153 if(dcgi_client)
154 disorder_pause(dcgi_client);
155 redirect(0);
156}
157
59cf25c4 158/*$ resume
5c1ae3bc
RK
159 *
160 * Resumes the current track (if there is one and it's paused).
161 */
6d9dd8d9
RK
162static void act_resume(void) {
163 if(dcgi_client)
164 disorder_resume(dcgi_client);
165 redirect(0);
166}
167
59cf25c4 168/*$ remove
5c1ae3bc
RK
169 *
170 * Removes the track given by the \fBid\fR argument. If this is the currently
171 * playing track then it is scratched.
172 */
a2c4ad5f
RK
173static void act_remove(void) {
174 const char *id;
175 struct queue_entry *q;
176
177 if(dcgi_client) {
178 if(!(id = cgi_get("id")))
2e9ba080 179 disorder_error(0, "missing 'id' argument");
a2c4ad5f 180 else if(!(q = dcgi_findtrack(id)))
2e9ba080 181 disorder_error(0, "unknown queue id %s", id);
6a5964b7
RK
182 else if(q->origin == origin_scratch)
183 /* can't scratch scratches */
2e9ba080 184 disorder_error(0, "does not make sense to scratch or remove %s", id);
6a5964b7
RK
185 else if(q->state == playing_paused
186 || q->state == playing_started)
187 /* removing the playing track = scratching */
a2c4ad5f 188 disorder_scratch(dcgi_client, id);
6a5964b7
RK
189 else if(q->state == playing_unplayed)
190 /* otherwise it must be in the queue */
a2c4ad5f 191 disorder_remove(dcgi_client, id);
6a5964b7
RK
192 else
193 /* various error states */
2e9ba080 194 disorder_error(0, "does not make sense to scratch or remove %s", id);
a2c4ad5f
RK
195 }
196 redirect(0);
197}
198
59cf25c4 199/*$ move
5c1ae3bc
RK
200 *
201 * Moves the track given by the \fBid\fR argument the distance given by the
202 * \fBdelta\fR argument. If this is positive the track is moved earlier in the
203 * queue and if negative, later.
204 */
6d9dd8d9
RK
205static void act_move(void) {
206 const char *id, *delta;
207 struct queue_entry *q;
208
209 if(dcgi_client) {
210 if(!(id = cgi_get("id")))
2e9ba080 211 disorder_error(0, "missing 'id' argument");
6d9dd8d9 212 else if(!(delta = cgi_get("delta")))
2e9ba080 213 disorder_error(0, "missing 'delta' argument");
6d9dd8d9 214 else if(!(q = dcgi_findtrack(id)))
2e9ba080 215 disorder_error(0, "unknown queue id %s", id);
6d9dd8d9
RK
216 else switch(q->state) {
217 case playing_random: /* unplayed randomly chosen track */
218 case playing_unplayed: /* haven't played this track yet */
219 disorder_move(dcgi_client, id, atol(delta));
220 break;
221 default:
2e9ba080 222 disorder_error(0, "does not make sense to scratch %s", id);
6d9dd8d9
RK
223 break;
224 }
225 }
226 redirect(0);
227}
228
59cf25c4 229/*$ play
5c1ae3bc
RK
230 *
231 * Play the track given by the \fBtrack\fR argument, or if that is not set all
232 * the tracks in the directory given by the \fBdir\fR argument.
233 */
6d9dd8d9
RK
234static void act_play(void) {
235 const char *track, *dir;
236 char **tracks;
237 int ntracks, n;
e13809e4 238 struct tracksort_data *tsd;
6d9dd8d9
RK
239
240 if(dcgi_client) {
02eaa49d 241 if((track = cgi_get("track"))) {
6d9dd8d9
RK
242 disorder_play(dcgi_client, track);
243 } else if((dir = cgi_get("dir"))) {
244 if(disorder_files(dcgi_client, dir, 0, &tracks, &ntracks))
245 ntracks = 0;
e13809e4 246 tsd = tracksort_init(ntracks, tracks, "track");
6d9dd8d9 247 for(n = 0; n < ntracks; ++n)
e13809e4 248 disorder_play(dcgi_client, tsd[n].track);
6d9dd8d9
RK
249 }
250 }
251 redirect(0);
252}
253
254static int clamp(int n, int min, int max) {
255 if(n < min)
256 return min;
257 if(n > max)
258 return max;
259 return n;
260}
261
59cf25c4 262/*$ volume
5c1ae3bc
RK
263 *
264 * If the \fBdelta\fR argument is set: adjust both channels by that amount (up
265 * if positive, down if negative).
266 *
267 * Otherwise if \fBleft\fR and \fBright\fR are set, set the channels
268 * independently to those values.
269 */
6d9dd8d9
RK
270static void act_volume(void) {
271 const char *l, *r, *d;
272 int nd;
273
274 if(dcgi_client) {
275 if((d = cgi_get("delta"))) {
276 dcgi_lookup(DCGI_VOLUME);
277 nd = clamp(atoi(d), -255, 255);
278 disorder_set_volume(dcgi_client,
279 clamp(dcgi_volume_left + nd, 0, 255),
280 clamp(dcgi_volume_right + nd, 0, 255));
281 } else if((l = cgi_get("left")) && (r = cgi_get("right")))
282 disorder_set_volume(dcgi_client, atoi(l), atoi(r));
283 }
284 redirect(0);
285}
286
e7ce7665 287/** @brief Expand the login template with @b @@error set to @p error
b7e452c7 288 * @param e Error keyword
e7ce7665 289 */
b7e452c7
RK
290static void login_error(const char *e) {
291 dcgi_error_string = e;
e7ce7665
RK
292 dcgi_expand("login", 1);
293}
294
295/** @brief Log in
296 * @param username Login name
297 * @param password Password
298 * @return 0 on success, non-0 on error
299 *
300 * On error, calls login_error() to expand the login template.
301 */
302static int login_as(const char *username, const char *password) {
303 disorder_client *c;
304
305 if(dcgi_cookie && dcgi_client)
306 disorder_revoke(dcgi_client);
307 /* We'll need a new connection as we are going to stop being guest */
308 c = disorder_new(0);
309 if(disorder_connect_user(c, username, password)) {
310 login_error("loginfailed");
311 return -1;
312 }
313 /* Generate a cookie so we can log in again later */
314 if(disorder_make_cookie(c, &dcgi_cookie)) {
315 login_error("cookiefailed");
316 return -1;
317 }
318 /* Use the new connection henceforth */
319 dcgi_client = c;
320 dcgi_lookup_reset();
321 return 0; /* OK */
322}
323
59cf25c4 324/*$ login
5c1ae3bc
RK
325 *
326 * If \fBusername\fR and \fBpassword\fR are set (and the username isn't
327 * "guest") then attempt to log in using those credentials. On success,
328 * redirects to the \fBback\fR argument if that is set, or just expands
329 * \fIlogin.tmpl\fI otherwise, with \fB@status\fR set to \fBloginok\fR.
330 *
331 * If they aren't set then just expands \fIlogin.tmpl\fI.
332 */
e7ce7665
RK
333static void act_login(void) {
334 const char *username, *password;
335
336 /* We try all this even if not connected since the subsequent connection may
337 * succeed. */
338
339 username = cgi_get("username");
340 password = cgi_get("password");
341 if(!username
342 || !password
343 || !strcmp(username, "guest")/*bodge to avoid guest cookies*/) {
344 /* We're just visiting the login page, not performing an action at all. */
345 dcgi_expand("login", 1);
346 return;
347 }
348 if(!login_as(username, password)) {
349 /* Report the succesful login */
350 dcgi_status_string = "loginok";
2cc4c0ef
RK
351 /* Redirect back to where we came from, if necessary */
352 if(cgi_get("back"))
353 redirect(0);
354 else
355 dcgi_expand("login", 1);
e7ce7665
RK
356 }
357}
358
59cf25c4 359/*$ logout
5c1ae3bc
RK
360 *
361 * Logs out the current user and expands \fIlogin.tmpl\fR with \fBstatus\fR or
362 * \fB@error\fR set according to the result.
363 */
e7ce7665
RK
364static void act_logout(void) {
365 if(dcgi_client) {
366 /* Ask the server to revoke the cookie */
367 if(!disorder_revoke(dcgi_client))
368 dcgi_status_string = "logoutok";
369 else
370 dcgi_error_string = "revokefailed";
371 } else {
372 /* We can't guarantee a logout if we can't connect to the server to revoke
373 * the cookie, so we report an error. We'll still ask the browser to
374 * forget the cookie though. */
375 dcgi_error_string = "connect";
376 }
377 /* Attempt to reconnect without the cookie */
378 dcgi_cookie = 0;
379 dcgi_login();
380 /* Back to login page, hopefuly forcing the browser to forget the cookie. */
381 dcgi_expand("login", 1);
382}
383
59cf25c4 384/*$ register
5c1ae3bc
RK
385 *
386 * Register a new user using \fBusername\fR, \fBpassword1\fR, \fBpassword2\fR
387 * and \fBemail\fR and expands \fIlogin.tmpl\fR with \fBstatus\fR or
388 * \fB@error\fR set according to the result.
389 */
e7ce7665
RK
390static void act_register(void) {
391 const char *username, *password, *password2, *email;
392 char *confirm, *content_type;
393 const char *text, *encoding, *charset;
394
395 /* If we're not connected then this is a hopeless exercise */
396 if(!dcgi_client) {
397 login_error("connect");
398 return;
399 }
400
401 /* Collect arguments */
402 username = cgi_get("username");
403 password = cgi_get("password1");
404 password2 = cgi_get("password2");
405 email = cgi_get("email");
406
407 /* Verify arguments */
408 if(!username || !*username) {
409 login_error("nousername");
410 return;
411 }
412 if(!password || !*password) {
413 login_error("nopassword");
414 return;
415 }
416 if(!password2 || !*password2 || strcmp(password, password2)) {
417 login_error("passwordmismatch");
418 return;
419 }
420 if(!email || !*email) {
421 login_error("noemail");
422 return;
423 }
424 /* We could well do better address validation but for now we'll just do the
5f2d537c 425 * minimum */
33e95f03 426 if(!email_valid(email)) {
e7ce7665
RK
427 login_error("bademail");
428 return;
429 }
430 if(disorder_register(dcgi_client, username, password, email, &confirm)) {
431 login_error("cannotregister");
432 return;
433 }
434 /* Send the user a mail */
435 /* TODO templatize this */
436 byte_xasprintf((char **)&text,
437 "Welcome to DisOrder. To active your login, please visit this URL:\n"
438 "\n"
439 "%s?c=%s\n", config->url, urlencodestring(confirm));
440 if(!(text = mime_encode_text(text, &charset, &encoding)))
2e9ba080 441 disorder_fatal(0, "cannot encode email");
e7ce7665
RK
442 byte_xasprintf(&content_type, "text/plain;charset=%s",
443 quote822(charset, 0));
444 sendmail("", config->mail_sender, email, "Welcome to DisOrder",
445 encoding, content_type, text); /* TODO error checking */
446 /* We'll go back to the login page with a suitable message */
447 dcgi_status_string = "registered";
448 dcgi_expand("login", 1);
449}
450
59cf25c4 451/*$ confirm
5c1ae3bc
RK
452 *
453 * Confirm a user registration using the nonce supplied in \fBc\fR and expands
454 * \fIlogin.tmpl\fR with \fBstatus\fR or \fB@error\fR set according to the
455 * result.
456 */
e7ce7665
RK
457static void act_confirm(void) {
458 const char *confirmation;
459
460 /* If we're not connected then this is a hopeless exercise */
461 if(!dcgi_client) {
462 login_error("connect");
463 return;
464 }
465
466 if(!(confirmation = cgi_get("c"))) {
467 login_error("noconfirm");
468 return;
469 }
470 /* Confirm our registration */
471 if(disorder_confirm(dcgi_client, confirmation)) {
472 login_error("badconfirm");
473 return;
474 }
475 /* Get a cookie */
476 if(disorder_make_cookie(dcgi_client, &dcgi_cookie)) {
477 login_error("cookiefailed");
478 return;
479 }
480 /* Junk cached data */
481 dcgi_lookup_reset();
482 /* Report success */
483 dcgi_status_string = "confirmed";
484 dcgi_expand("login", 1);
485}
486
59cf25c4 487/*$ edituser
5c1ae3bc
RK
488 *
489 * Edit user details using \fBusername\fR, \fBchangepassword1\fR,
490 * \fBchangepassword2\fR and \fBemail\fR and expands \fIlogin.tmpl\fR with
491 * \fBstatus\fR or \fB@error\fR set according to the result.
492 */
e7ce7665
RK
493static void act_edituser(void) {
494 const char *email = cgi_get("email"), *password = cgi_get("changepassword1");
495 const char *password2 = cgi_get("changepassword2");
496 int newpassword = 0;
497
498 /* If we're not connected then this is a hopeless exercise */
499 if(!dcgi_client) {
500 login_error("connect");
501 return;
502 }
503
504 /* Verify input */
505
506 /* If either password or password2 is set we insist they match. If they
507 * don't we report an error. */
508 if((password && *password) || (password2 && *password2)) {
509 if(!password || !password2 || strcmp(password, password2)) {
510 login_error("passwordmismatch");
511 return;
512 }
513 } else
514 password = password2 = 0;
33e95f03 515 if(email && !email_valid(email)) {
e7ce7665
RK
516 login_error("bademail");
517 return;
518 }
519
520 /* Commit changes */
521 if(email) {
522 if(disorder_edituser(dcgi_client, disorder_user(dcgi_client),
523 "email", email)) {
524 login_error("badedit");
525 return;
526 }
527 }
528 if(password) {
529 if(disorder_edituser(dcgi_client, disorder_user(dcgi_client),
530 "password", password)) {
531 login_error("badedit");
532 return;
533 }
534 newpassword = 1;
535 }
536
537 if(newpassword) {
538 /* If we changed the password, the cookie is now invalid, so we must log
539 * back in. */
540 if(login_as(disorder_user(dcgi_client), password))
541 return;
542 }
543 /* Report success */
544 dcgi_status_string = "edited";
545 dcgi_expand("login", 1);
546}
547
59cf25c4 548/*$ reminder
5c1ae3bc
RK
549 *
550 * Issue an email password reminder to \fBusername\fR and expands
551 * \fIlogin.tmpl\fR with \fBstatus\fR or \fB@error\fR set according to the
552 * result.
553 */
e7ce7665
RK
554static void act_reminder(void) {
555 const char *const username = cgi_get("username");
556
557 /* If we're not connected then this is a hopeless exercise */
558 if(!dcgi_client) {
559 login_error("connect");
560 return;
561 }
562
563 if(!username || !*username) {
564 login_error("nousername");
565 return;
566 }
567 if(disorder_reminder(dcgi_client, username)) {
568 login_error("reminderfailed");
569 return;
570 }
571 /* Report success */
572 dcgi_status_string = "reminded";
573 dcgi_expand("login", 1);
574}
575
02eaa49d
RK
576/** @brief Get the numbered version of an argument
577 * @param argname Base argument name
578 * @param numfile File number
579 * @return cgi_get(NUMFILE_ARGNAME)
580 */
581static const char *numbered_arg(const char *argname, int numfile) {
582 char *fullname;
583
584 byte_xasprintf(&fullname, "%d_%s", numfile, argname);
585 return cgi_get(fullname);
586}
587
588/** @brief Set preferences for file @p numfile
589 * @return 0 on success, -1 if there is no such track number
590 *
591 * The old @b nfiles parameter has been abolished, we just keep look for more
592 * files until we run out.
593 */
594static int process_prefs(int numfile) {
595 const char *file, *name, *value, *part, *parts, *context;
596 char **partslist;
597
598 if(!(file = numbered_arg("track", numfile)))
599 return -1;
600 if(!(parts = cgi_get("parts")))
601 parts = "artist album title";
602 if(!(context = cgi_get("context")))
603 context = "display";
604 partslist = split(parts, 0, 0, 0, 0);
605 while((part = *partslist++)) {
606 if(!(value = numbered_arg(part, numfile)))
607 continue;
608 byte_xasprintf((char **)&name, "trackname_%s_%s", context, part);
609 disorder_set(dcgi_client, file, name, value);
610 }
611 if((value = numbered_arg("random", numfile)))
612 disorder_unset(dcgi_client, file, "pick_at_random");
613 else
614 disorder_set(dcgi_client, file, "pick_at_random", "0");
615 if((value = numbered_arg("tags", numfile))) {
616 if(!*value)
617 disorder_unset(dcgi_client, file, "tags");
618 else
619 disorder_set(dcgi_client, file, "tags", value);
620 }
621 if((value = numbered_arg("weight", numfile))) {
622 if(!*value)
623 disorder_unset(dcgi_client, file, "weight");
624 else
625 disorder_set(dcgi_client, file, "weight", value);
626 }
627 return 0;
628}
629
59cf25c4 630/*$ prefs
5c1ae3bc
RK
631 *
632 * Set preferences on a number of tracks.
633 *
634 * The tracks to modify are specified in arguments \fB0_track\fR, \fB1_track\fR
635 * etc. The number sequence must be contiguous and start from 0.
636 *
637 * For each track \fIINDEX\fB_track\fR:
638 * - \fIINDEX\fB_\fIPART\fR is used to set the trackname preference for
639 * that part. (See \fBparts\fR below.)
640 * - \fIINDEX\fB_\fIrandom\fR if present enables random play for this track
641 * or disables it if absent.
642 * - \fIINDEX\fB_\fItags\fR sets the list of tags for this track.
643 * - \fIINDEX\fB_\fIweight\fR sets the weight for this track.
644 *
645 * \fBparts\fR can be set to the track name parts to modify. The default is
646 * "artist album title".
647 *
648 * \fBcontext\fR can be set to the context to modify. The default is
649 * "display".
650 *
651 * If the server detects a preference being set to its default, it removes the
652 * preference, thus keeping the database tidy.
653 */
02eaa49d
RK
654static void act_set(void) {
655 int numfile;
656
657 if(dcgi_client) {
658 for(numfile = 0; !process_prefs(numfile); ++numfile)
659 ;
660 }
661 redirect(0);
662}
663
bca4e2b7
RK
664/** @brief Table of actions */
665static const struct action {
666 /** @brief Action name */
667 const char *name;
668 /** @brief Action handler */
669 void (*handler)(void);
02eaa49d
RK
670 /** @brief Union of suitable rights */
671 rights_type rights;
bca4e2b7 672} actions[] = {
02eaa49d
RK
673 { "confirm", act_confirm, 0 },
674 { "disable", act_disable, RIGHT_GLOBAL_PREFS },
675 { "edituser", act_edituser, 0 },
676 { "enable", act_enable, RIGHT_GLOBAL_PREFS },
677 { "login", act_login, 0 },
678 { "logout", act_logout, 0 },
679 { "manage", act_playing, 0 },
680 { "move", act_move, RIGHT_MOVE__MASK },
681 { "pause", act_pause, RIGHT_PAUSE },
682 { "play", act_play, RIGHT_PLAY },
683 { "playing", act_playing, 0 },
684 { "randomdisable", act_random_disable, RIGHT_GLOBAL_PREFS },
685 { "randomenable", act_random_enable, RIGHT_GLOBAL_PREFS },
686 { "register", act_register, 0 },
687 { "reminder", act_reminder, 0 },
688 { "remove", act_remove, RIGHT_MOVE__MASK|RIGHT_SCRATCH__MASK },
689 { "resume", act_resume, RIGHT_PAUSE },
690 { "set", act_set, RIGHT_PREFS },
691 { "volume", act_volume, RIGHT_VOLUME },
bca4e2b7
RK
692};
693
40dcd866
RK
694/** @brief Check that an action name is valid
695 * @param name Action
696 * @return 1 if valid, 0 if not
697 */
698static int dcgi_valid_action(const char *name) {
699 int c;
700
701 /* First character must be letter or digit (this also requires there to _be_
702 * a first character) */
703 if(!isalnum((unsigned char)*name))
704 return 0;
705 /* Only letters, digits, '.' and '-' allowed */
706 while((c = (unsigned char)*name++)) {
707 if(!(isalnum(c)
708 || c == '.'
709 || c == '_'))
710 return 0;
711 }
712 return 1;
713}
714
bca4e2b7
RK
715/** @brief Expand a template
716 * @param name Base name of template, or NULL to consult CGI args
e7ce7665 717 * @param header True to write header
bca4e2b7 718 */
e7ce7665 719void dcgi_expand(const char *name, int header) {
99955407 720 const char *p, *found;
0d0253c9
RK
721
722 /* Parse macros first */
f2d306b4
RK
723 if((found = mx_find("macros.tmpl", 1/*report*/)))
724 mx_expand_file(found, sink_discard(), 0);
725 if((found = mx_find("user.tmpl", 0/*report*/)))
99955407 726 mx_expand_file(found, sink_discard(), 0);
bca4e2b7 727 /* For unknown actions check that they aren't evil */
40dcd866 728 if(!dcgi_valid_action(name))
2e9ba080 729 disorder_fatal(0, "invalid action name '%s'", name);
71634563 730 byte_xasprintf((char **)&p, "%s.tmpl", name);
f2d306b4 731 if(!(found = mx_find(p, 0/*report*/)))
2e9ba080 732 disorder_fatal(errno, "cannot find %s", p);
e7ce7665 733 if(header) {
10921eba 734 if(printf("Content-Type: text/html; charset=UTF-8\n"
e7ce7665
RK
735 "%s\n"
736 "\n", dcgi_cookie_header()) < 0)
2e9ba080 737 disorder_fatal(errno, "error writing to stdout");
e7ce7665 738 }
99955407 739 if(mx_expand_file(found, sink_stdio("stdout", stdout), 0) == -1
bca4e2b7 740 || fflush(stdout) < 0)
2e9ba080 741 disorder_fatal(errno, "error writing to stdout");
bca4e2b7
RK
742}
743
744/** @brief Execute a web action
745 * @param action Action to perform, or NULL to consult CGI args
746 *
747 * If no recognized action is specified then 'playing' is assumed.
748 */
1e97629d 749void dcgi_action(const char *action) {
bca4e2b7 750 int n;
bca4e2b7
RK
751
752 /* Consult CGI args if caller had no view */
753 if(!action)
754 action = cgi_get("action");
755 /* Pick a default if nobody cares at all */
756 if(!action) {
757 /* We allow URLs which are just c=... in order to keep confirmation URLs,
758 * which are user-facing, as short as possible. Actually we could lose the
759 * c= for this... */
760 if(cgi_get("c"))
761 action = "confirm";
762 else
763 action = "playing";
5a7df048
RK
764 /* Make sure 'action' is always set */
765 cgi_set("action", action);
bca4e2b7 766 }
ba937f01 767 if((n = TABLE_FIND(actions, name, action)) >= 0) {
02eaa49d
RK
768 if(actions[n].rights) {
769 /* Some right or other is required */
770 dcgi_lookup(DCGI_RIGHTS);
771 if(!(actions[n].rights & dcgi_rights)) {
2cc4c0ef 772 const char *back = cgi_thisurl(config->url);
02eaa49d 773 /* Failed operations jump you to the login screen with an error
2cc4c0ef
RK
774 * message. On success, the user comes back to the page they were
775 * after. */
776 cgi_clear();
777 cgi_set("back", back);
02eaa49d
RK
778 login_error("noright");
779 return;
780 }
781 }
782 /* It's a known action */
bca4e2b7 783 actions[n].handler();
02eaa49d 784 } else {
bca4e2b7 785 /* Just expand the template */
e7ce7665 786 dcgi_expand(action, 1/*header*/);
448d3570 787 }
bca4e2b7
RK
788}
789
790/** @brief Generate an error page */
0d0253c9
RK
791void dcgi_error(const char *key) {
792 dcgi_error_string = xstrdup(key);
e7ce7665 793 dcgi_expand("error", 1);
bca4e2b7
RK
794}
795
796/*
797Local Variables:
798c-basic-offset:2
799comment-column:40
800fill-column:79
801indent-tabs-mode:nil
802End:
803*/