5 #include <sys/socket.h>
19 #include "server-cgi.h"
21 #include "configuration.h"
32 #include "trackname.h"
46 static int compare_entry(const void *a, const void *b) {
47 const struct entry *ea = a, *eb = b;
49 return compare_tracks(ea->sort, eb->sort,
50 ea->display, eb->display,
54 static const char *front_url(void) {
58 /* preserve management interface visibility */
59 if((mgmt = cgi_get("mgmt")) && !strcmp(mgmt, "true")) {
60 byte_xasprintf(&url, "%s?mgmt=true", config->url);
66 static void header_cookie(struct sink *output) {
70 memset(&u, 0, sizeof u);
72 parse_url(config->url, &u);
74 dynstr_append_string(d, "disorder=");
75 dynstr_append_string(d, login_cookie);
77 /* Force browser to discard cookie */
78 dynstr_append_string(d, "disorder=none;Max-Age=0");
81 /* The default domain matches the request host, so we need not override
82 * that. But the default path only goes up to the rightmost /, which would
83 * cause the browser to expose the cookie to other CGI programs on the same
85 dynstr_append_string(d, ";Version=1;Path=");
86 /* Formally we are supposed to quote the path, since it invariably has a
87 * slash in it. However Safari does not parse quoted paths correctly, so
88 * this won't work. Fortunately nothing else seems to care about proper
89 * quoting of paths, so in practice we get with it. (See also
90 * parse_cookie() where we are liberal about cookie paths on the way back
92 dynstr_append_string(d, u.path);
95 cgi_header(output, "Set-Cookie", d->vec);
98 static void redirect(struct sink *output) {
101 back = cgi_get("back");
102 cgi_header(output, "Location", back && *back ? back : front_url());
103 header_cookie(output);
107 static void expand_template(dcgi_state *ds, cgi_sink *output,
108 const char *action) {
109 cgi_header(output->sink, "Content-Type", "text/html");
110 header_cookie(output->sink);
111 cgi_body(output->sink);
112 expand(output, action, ds);
115 /* actions ********************************************************************/
117 static void act_disable(cgi_sink *output,
120 disorder_disable(ds->g->client);
121 redirect(output->sink);
124 static void act_enable(cgi_sink *output,
127 disorder_enable(ds->g->client);
128 redirect(output->sink);
131 static void act_random_disable(cgi_sink *output,
134 disorder_random_disable(ds->g->client);
135 redirect(output->sink);
138 static void act_random_enable(cgi_sink *output,
141 disorder_random_enable(ds->g->client);
142 redirect(output->sink);
145 static void act_remove(cgi_sink *output,
149 if(!(id = cgi_get("id"))) fatal(0, "missing id argument");
151 disorder_remove(ds->g->client, id);
152 redirect(output->sink);
155 static void act_move(cgi_sink *output,
157 const char *id, *delta;
159 if(!(id = cgi_get("id"))) fatal(0, "missing id argument");
160 if(!(delta = cgi_get("delta"))) fatal(0, "missing delta argument");
162 disorder_move(ds->g->client, id, atoi(delta));
163 redirect(output->sink);
166 static void act_scratch(cgi_sink *output,
169 disorder_scratch(ds->g->client, cgi_get("id"));
170 redirect(output->sink);
173 static void act_playing(cgi_sink *output, dcgi_state *ds) {
175 long refresh = config->refresh, length;
177 int random_enabled = 0;
180 lookups(ds, DC_PLAYING|DC_QUEUE);
181 cgi_header(output->sink, "Content-Type", "text/html");
182 disorder_random_enabled(ds->g->client, &random_enabled);
183 disorder_enabled(ds->g->client, &enabled);
185 && ds->g->playing->state == playing_started /* i.e. not paused */
186 && !disorder_length(ds->g->client, ds->g->playing->track, &length)
188 && ds->g->playing->sofar >= 0) {
189 /* Try to put the next refresh at the start of the next track. */
191 fin = now + length - ds->g->playing->sofar + config->gap;
192 if(now + refresh > fin)
195 if(ds->g->queue && ds->g->queue->state == playing_isscratch) {
196 /* next track is a scratch, don't leave more than the inter-track gap */
197 if(refresh > config->gap)
198 refresh = config->gap;
200 if(!ds->g->playing && ((ds->g->queue
201 && ds->g->queue->state != playing_random)
202 || random_enabled) && enabled) {
203 /* no track playing but playing is enabled and there is something coming
204 * up, must be in a gap */
205 if(refresh > config->gap)
206 refresh = config->gap;
208 byte_snprintf(r, sizeof r, "%ld;url=%s", refresh > 0 ? refresh : 1,
210 cgi_header(output->sink, "Refresh", r);
211 header_cookie(output->sink);
212 cgi_body(output->sink);
213 expand(output, "playing", ds);
216 static void act_play(cgi_sink *output,
218 const char *track, *dir;
223 if((track = cgi_get("file"))) {
224 disorder_play(ds->g->client, track);
225 } else if((dir = cgi_get("directory"))) {
226 if(disorder_files(ds->g->client, dir, 0, &tracks, &ntracks)) ntracks = 0;
228 e = xmalloc(ntracks * sizeof (struct entry));
229 for(n = 0; n < ntracks; ++n) {
230 e[n].path = tracks[n];
231 e[n].sort = trackname_transform("track", tracks[n], "sort");
232 e[n].display = trackname_transform("track", tracks[n], "display");
234 qsort(e, ntracks, sizeof (struct entry), compare_entry);
235 for(n = 0; n < ntracks; ++n)
236 disorder_play(ds->g->client, e[n].path);
239 /* XXX error handling */
240 redirect(output->sink);
243 static int clamp(int n, int min, int max) {
251 static const char *volume_url(void) {
254 byte_xasprintf(&url, "%s?action=volume", config->url);
258 static void act_volume(cgi_sink *output, dcgi_state *ds) {
259 const char *l, *r, *d, *back;
260 int nd, changed = 0;;
262 if((d = cgi_get("delta"))) {
263 lookups(ds, DC_VOLUME);
264 nd = clamp(atoi(d), -255, 255);
265 disorder_set_volume(ds->g->client,
266 clamp(ds->g->volume_left + nd, 0, 255),
267 clamp(ds->g->volume_right + nd, 0, 255));
269 } else if((l = cgi_get("left")) && (r = cgi_get("right"))) {
270 disorder_set_volume(ds->g->client, atoi(l), atoi(r));
274 /* redirect back to ourselves (but without the volume-changing bits in the
276 cgi_header(output->sink, "Location",
277 (back = cgi_get("back")) ? back : volume_url());
278 header_cookie(output->sink);
279 cgi_body(output->sink);
281 cgi_header(output->sink, "Content-Type", "text/html");
282 header_cookie(output->sink);
283 cgi_body(output->sink);
284 expand(output, "volume", ds);
288 static void act_prefs_errors(const char *msg,
289 void attribute((unused)) *u) {
290 fatal(0, "error splitting parts list: %s", msg);
293 static const char *numbered_arg(const char *argname, int numfile) {
296 byte_xasprintf(&fullname, "%d_%s", numfile, argname);
297 return cgi_get(fullname);
300 static void process_prefs(dcgi_state *ds, int numfile) {
301 const char *file, *name, *value, *part, *parts, *current, *context;
304 if(!(file = numbered_arg("file", numfile)))
305 /* The first file doesn't need numbering. */
306 if(numfile > 0 || !(file = cgi_get("file")))
308 if((parts = numbered_arg("parts", numfile))
309 || (parts = cgi_get("parts"))) {
310 /* Default context is display. Other contexts not actually tested. */
311 if(!(context = numbered_arg("context", numfile))) context = "display";
312 partslist = split(parts, 0, 0, act_prefs_errors, 0);
313 while((part = *partslist++)) {
314 if(!(value = numbered_arg(part, numfile)))
316 /* If it's already right (whether regexps or db) don't change anything,
317 * so we don't fill the database up with rubbish. */
318 if(disorder_part(ds->g->client, (char **)¤t,
319 file, context, part))
320 fatal(0, "disorder_part() failed");
321 if(!strcmp(current, value))
323 byte_xasprintf((char **)&name, "trackname_%s_%s", context, part);
324 disorder_set(ds->g->client, file, name, value);
326 if((value = numbered_arg("random", numfile)))
327 disorder_unset(ds->g->client, file, "pick_at_random");
329 disorder_set(ds->g->client, file, "pick_at_random", "0");
330 if((value = numbered_arg("tags", numfile))) {
332 disorder_unset(ds->g->client, file, "tags");
334 disorder_set(ds->g->client, file, "tags", value);
336 if((value = numbered_arg("weight", numfile))) {
337 if(!*value || !strcmp(value, "90000"))
338 disorder_unset(ds->g->client, file, "weight");
340 disorder_set(ds->g->client, file, "weight", value);
342 } else if((name = cgi_get("name"))) {
343 /* Raw preferences. Not well supported in the templates at the moment. */
344 value = cgi_get("value");
346 disorder_set(ds->g->client, file, name, value);
348 disorder_unset(ds->g->client, file, name);
352 static void act_prefs(cgi_sink *output, dcgi_state *ds) {
356 if((files = cgi_get("files"))) nfiles = atoi(files);
358 for(numfile = 0; numfile < nfiles; ++numfile)
359 process_prefs(ds, numfile);
360 cgi_header(output->sink, "Content-Type", "text/html");
361 header_cookie(output->sink);
362 cgi_body(output->sink);
363 expand(output, "prefs", ds);
366 static void act_pause(cgi_sink *output,
369 disorder_pause(ds->g->client);
370 redirect(output->sink);
373 static void act_resume(cgi_sink *output,
376 disorder_resume(ds->g->client);
377 redirect(output->sink);
380 static void act_login(cgi_sink *output,
382 const char *username, *password, *back;
385 username = cgi_get("username");
386 password = cgi_get("password");
387 if(!username || !password
388 || !strcmp(username, "guest")/*bodge to avoid guest cookies*/) {
389 /* We're just visiting the login page */
390 expand_template(ds, output, "login");
393 /* We'll need a new connection as we are going to stop being guest */
395 if(disorder_connect_user(c, username, password)) {
396 cgi_set_option("error", "loginfailed");
397 expand_template(ds, output, "login");
400 if(disorder_make_cookie(c, &login_cookie)) {
401 cgi_set_option("error", "cookiefailed");
402 expand_template(ds, output, "login");
405 /* Use the new connection henceforth */
408 /* We have a new cookie */
409 header_cookie(output->sink);
410 cgi_set_option("status", "loginok");
411 if((back = cgi_get("back")) && *back)
412 /* Redirect back to somewhere or other */
413 redirect(output->sink);
415 /* Stick to the login page */
416 expand_template(ds, output, "login");
419 static void act_logout(cgi_sink *output,
421 disorder_revoke(ds->g->client);
423 /* Reconnect as guest */
424 disorder_cgi_login(ds, output);
425 /* Back to the login page */
426 cgi_set_option("status", "logoutok");
427 expand_template(ds, output, "login");
430 static void act_register(cgi_sink *output,
432 const char *username, *password, *password2, *email;
433 char *confirm, *content_type;
434 const char *text, *encoding, *charset;
436 username = cgi_get("username");
437 password = cgi_get("password1");
438 password2 = cgi_get("password2");
439 email = cgi_get("email");
441 if(!username || !*username) {
442 cgi_set_option("error", "nousername");
443 expand_template(ds, output, "login");
446 if(!password || !*password) {
447 cgi_set_option("error", "nopassword");
448 expand_template(ds, output, "login");
451 if(!password2 || !*password2 || strcmp(password, password2)) {
452 cgi_set_option("error", "passwordmismatch");
453 expand_template(ds, output, "login");
456 if(!email || !*email) {
457 cgi_set_option("error", "noemail");
458 expand_template(ds, output, "login");
461 /* We could well do better address validation but for now we'll just do the
463 if(!strchr(email, '@')) {
464 cgi_set_option("error", "bademail");
465 expand_template(ds, output, "login");
468 if(disorder_register(ds->g->client, username, password, email, &confirm)) {
469 cgi_set_option("error", "cannotregister");
470 expand_template(ds, output, "login");
473 /* Send the user a mail */
474 /* TODO templatize this */
475 byte_xasprintf((char **)&text,
476 "Welcome to DisOrder. To active your login, please visit this URL:\n"
478 "%s?c=%s\n", config->url, urlencodestring(confirm));
479 if(!(text = mime_encode_text(text, &charset, &encoding)))
480 fatal(0, "cannot encode email");
481 byte_xasprintf(&content_type, "text/plain;charset=%s",
482 quote822(charset, 0));
483 sendmail("", config->mail_sender, email, "Welcome to DisOrder",
484 encoding, content_type, text); /* TODO error checking */
485 /* We'll go back to the login page with a suitable message */
486 cgi_set_option("status", "registered");
487 expand_template(ds, output, "login");
490 static void act_confirm(cgi_sink *output,
492 const char *confirmation;
494 if(!(confirmation = cgi_get("c"))) {
495 cgi_set_option("error", "noconfirm");
496 expand_template(ds, output, "login");
498 /* Confirm our registration */
499 if(disorder_confirm(ds->g->client, confirmation)) {
500 cgi_set_option("error", "badconfirm");
501 expand_template(ds, output, "login");
504 if(disorder_make_cookie(ds->g->client, &login_cookie)) {
505 cgi_set_option("error", "cookiefailed");
506 expand_template(ds, output, "login");
509 /* Discard any cached data JIC */
511 /* We have a new cookie */
512 header_cookie(output->sink);
513 cgi_set_option("status", "confirmed");
514 expand_template(ds, output, "login");
517 static void act_edituser(cgi_sink *output,
519 const char *email = cgi_get("email"), *password = cgi_get("changepassword1");
520 const char *password2 = cgi_get("changepassword2");
524 if((password && *password) || (password && *password2)) {
525 if(!password || !password2 || strcmp(password, password2)) {
526 cgi_set_option("error", "passwordmismatch");
527 expand_template(ds, output, "login");
531 password = password2 = 0;
534 if(disorder_edituser(ds->g->client, disorder_user(ds->g->client),
536 cgi_set_option("error", "badedit");
537 expand_template(ds, output, "login");
542 if(disorder_edituser(ds->g->client, disorder_user(ds->g->client),
543 "password", password)) {
544 cgi_set_option("error", "badedit");
545 expand_template(ds, output, "login");
551 login_cookie = 0; /* it'll be invalid now */
552 /* This is a bit duplicative of act_login() */
554 if(disorder_connect_user(c, disorder_user(ds->g->client), password)) {
555 cgi_set_option("error", "loginfailed");
556 expand_template(ds, output, "login");
559 if(disorder_make_cookie(c, &login_cookie)) {
560 cgi_set_option("error", "cookiefailed");
561 expand_template(ds, output, "login");
564 /* Use the new connection henceforth */
567 /* We have a new cookie */
568 header_cookie(output->sink);
570 cgi_set_option("status", "edited");
571 expand_template(ds, output, "login");
574 static void act_reminder(cgi_sink *output,
576 const char *const username = cgi_get("username");
578 if(!username || !*username) {
579 cgi_set_option("error", "nousername");
580 expand_template(ds, output, "login");
583 if(disorder_reminder(ds->g->client, username)) {
584 cgi_set_option("error", "reminderfailed");
585 expand_template(ds, output, "login");
588 cgi_set_option("status", "reminded");
589 expand_template(ds, output, "login");
592 /* expansions *****************************************************************/
594 static void exp_label(int attribute((unused)) nargs,
597 void attribute((unused)) *u) {
598 cgi_output(output, "%s", cgi_label(args[0]));
601 struct trackinfo_state {
603 const struct queue_entry *q;
613 static int compare_result(const void *a, const void *b) {
614 const struct result *ra = a, *rb = b;
617 if(!(c = strcmp(ra->sort, rb->sort)))
618 c = strcmp(ra->track, rb->track);
622 static void exp_search(int nargs,
626 dcgi_state *ds = u, substate;
628 const char *q, *context, *part, *template;
644 assert(!"should never happen");
645 part = context = template = 0; /* quieten compiler */
647 if(ds->tracks == 0) {
648 /* we are the top level, let's get some search results */
649 if(!(q = cgi_get("query"))) return; /* no results yet */
650 if(disorder_search(ds->g->client, q, &tracks, &ntracks)) return;
654 ntracks = ds->ntracks;
656 assert(ntracks != 0);
657 /* sort tracks by the appropriate part */
658 r = xmalloc(ntracks * sizeof *r);
659 for(n = 0; n < ntracks; ++n) {
660 r[n].track = tracks[n];
661 if(disorder_part(ds->g->client, (char **)&r[n].sort,
662 tracks[n], context, part))
663 fatal(0, "disorder_part() failed");
665 qsort(r, ntracks, sizeof (struct result), compare_result);
666 /* expand the 2nd arg once for each group. We re-use the passed-in tracks
667 * array as we know it's guaranteed to be big enough and isn't going to be
668 * used for anything else any more. */
669 memset(&substate, 0, sizeof substate);
674 substate.tracks = tracks;
675 substate.ntracks = 0;
678 && !strcmp(r[m].sort, r[n].sort))
679 tracks[substate.ntracks++] = r[m++].track;
680 substate.last = (m == ntracks);
681 expandstring(output, template, &substate);
686 assert(substate.last != 0);
689 static void exp_stats(int attribute((unused)) nargs,
690 char attribute((unused)) **args,
696 cgi_opentag(output->sink, "pre", "class", "stats", (char *)0);
697 if(!disorder_stats(ds->g->client, &v, 0)) {
699 cgi_output(output, "%s\n", *v++);
701 cgi_closetag(output->sink, "pre");
704 static char *expandarg(const char *arg, dcgi_state *ds) {
710 output.sink = sink_dynstr(&d);
711 expandstring(&output, arg, ds);
712 dynstr_terminate(&d);
716 static void exp_isfiles(int attribute((unused)) nargs,
717 char attribute((unused)) **args,
722 lookups(ds, DC_FILES);
723 sink_printf(output->sink, "%s", bool2str(!!ds->g->nfiles));
726 static void exp_isdirectories(int attribute((unused)) nargs,
727 char attribute((unused)) **args,
732 lookups(ds, DC_DIRS);
733 sink_printf(output->sink, "%s", bool2str(!!ds->g->ndirs));
736 static void exp_choose(int attribute((unused)) nargs,
745 const char *type, *what = expandarg(args[0], ds);
747 if(!strcmp(what, "files")) {
748 lookups(ds, DC_FILES);
749 files = ds->g->files;
750 nfiles = ds->g->nfiles;
752 } else if(!strcmp(what, "directories")) {
753 lookups(ds, DC_DIRS);
755 nfiles = ds->g->ndirs;
758 error(0, "unknown @choose@ argument '%s'", what);
761 e = xmalloc(nfiles * sizeof (struct entry));
762 for(n = 0; n < nfiles; ++n) {
763 e[n].path = files[n];
764 e[n].sort = trackname_transform(type, files[n], "sort");
765 e[n].display = trackname_transform(type, files[n], "display");
767 qsort(e, nfiles, sizeof (struct entry), compare_entry);
768 memset(&substate, 0, sizeof substate);
771 for(n = 0; n < nfiles; ++n) {
772 substate.last = (n == nfiles - 1);
774 substate.entry = &e[n];
775 expandstring(output, args[1], &substate);
780 static void exp_file(int attribute((unused)) nargs,
781 char attribute((unused)) **args,
787 cgi_output(output, "%s", ds->entry->path);
789 cgi_output(output, "%s", ds->track->track);
791 cgi_output(output, "%s", ds->tracks[0]);
794 static void exp_navigate(int attribute((unused)) nargs,
800 const char *path = expandarg(args[0], ds);
805 memset(&substate, 0, sizeof substate);
807 ptr = path + 1; /* skip root */
809 substate.nav_path = path;
812 while(*ptr && *ptr != '/')
814 substate.last = !*ptr;
815 substate.nav_len = ptr - path;
816 substate.nav_dirlen = dirlen;
817 expandstring(output, args[1], &substate);
818 dirlen = substate.nav_len;
825 static void exp_fullname(int attribute((unused)) nargs,
826 char attribute((unused)) **args,
830 cgi_output(output, "%.*s", ds->nav_len, ds->nav_path);
833 static void exp_basename(int nargs,
841 if((s = strrchr(args[0], '/'))) ++s;
843 cgi_output(output, "%s", s);
845 cgi_output(output, "%.*s", ds->nav_len - ds->nav_dirlen - 1,
846 ds->nav_path + ds->nav_dirlen + 1);
849 static void exp_dirname(int nargs,
857 if((s = strrchr(args[0], '/')))
858 cgi_output(output, "%.*s", (int)(s - args[0]), args[0]);
860 cgi_output(output, "%.*s", ds->nav_dirlen, ds->nav_path);
863 static void exp_files(int attribute((unused)) nargs,
869 const char *nfiles_arg, *directory;
873 memset(&substate, 0, sizeof substate);
875 if((directory = cgi_get("directory"))) {
876 /* Prefs for whole directory. */
877 lookups(ds, DC_FILES);
878 /* Synthesize args for the file list. */
879 nfiles = ds->g->nfiles;
880 for(numfile = 0; numfile < nfiles; ++numfile) {
881 k = xmalloc(sizeof *k);
882 byte_xasprintf((char **)&k->name, "%d_file", numfile);
883 k->value = ds->g->files[numfile];
888 /* Args already present. */
889 if((nfiles_arg = cgi_get("files"))) nfiles = atoi(nfiles_arg);
892 for(numfile = 0; numfile < nfiles; ++numfile) {
893 substate.index = numfile;
894 expandstring(output, args[0], &substate);
898 static void exp_nfiles(int attribute((unused)) nargs,
899 char attribute((unused)) **args,
903 const char *files_arg;
905 if(cgi_get("directory")) {
906 lookups(ds, DC_FILES);
907 cgi_output(output, "%d", ds->g->nfiles);
908 } else if((files_arg = cgi_get("files")))
909 cgi_output(output, "%s", files_arg);
911 cgi_output(output, "1");
914 static void exp_image(int attribute((unused)) nargs,
917 void attribute((unused)) *u) {
919 const char *imagestem;
921 byte_xasprintf(&labelname, "images.%s", args[0]);
922 if(cgi_label_exists(labelname))
923 imagestem = cgi_label(labelname);
924 else if(strchr(args[0], '.'))
927 byte_xasprintf((char **)&imagestem, "%s.png", args[0]);
928 if(cgi_label_exists("url.static"))
929 cgi_output(output, "%s/%s", cgi_label("url.static"), imagestem);
931 cgi_output(output, "/disorder/%s", imagestem);