chiark / gitweb /
15faf2fba1ff792a4ca08f8bc107feb8846b22cd
[disorder] / server / dcgi.c
1 /*
2  * This file is part of DisOrder.
3  * Copyright (C) 2004-2008 Richard Kettlewell
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 2 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful, but
11  * WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  * General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program; if not, write to the Free Software
17  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
18  * USA
19  */
20
21 #include <config.h>
22 #include "types.h"
23
24 #include <stdio.h>
25 #include <errno.h>
26 #include <sys/types.h>
27 #include <sys/socket.h>
28 #include <stddef.h>
29 #include <stdlib.h>
30 #include <time.h>
31 #include <unistd.h>
32 #include <string.h>
33 #include <sys/wait.h>
34 #include <pcre.h>
35 #include <assert.h>
36
37 #include "client.h"
38 #include "mem.h"
39 #include "vector.h"
40 #include "sink.h"
41 #include "server-cgi.h"
42 #include "log.h"
43 #include "configuration.h"
44 #include "table.h"
45 #include "queue.h"
46 #include "plugin.h"
47 #include "split.h"
48 #include "wstat.h"
49 #include "kvp.h"
50 #include "syscalls.h"
51 #include "printf.h"
52 #include "regsub.h"
53 #include "defs.h"
54 #include "trackname.h"
55 #include "charset.h"
56 #include "dcgi.h"
57 #include "url.h"
58 #include "mime.h"
59 #include "sendmail.h"
60 #include "base64.h"
61
62 char *login_cookie;
63
64 static void expand(cgi_sink *output,
65                    const char *template,
66                    dcgi_state *ds);
67 static void expandstring(cgi_sink *output,
68                          const char *string,
69                          dcgi_state *ds);
70
71 struct entry {
72   const char *path;
73   const char *sort;
74   const char *display;
75 };
76
77 static const char nonce_base64_table[] =
78   "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-/*";
79
80 static const char *nonce(void) {
81   static uint32_t count;
82
83   struct ndata {
84     uint16_t count;
85     uint16_t pid;
86     uint32_t when;
87   } nd;
88
89   nd.count = count++;
90   nd.pid = (uint32_t)getpid();
91   nd.when = (uint32_t)time(0);
92   return generic_to_base64((void *)&nd, sizeof nd,
93                            nonce_base64_table);
94 }
95
96 static int compare_entry(const void *a, const void *b) {
97   const struct entry *ea = a, *eb = b;
98
99   return compare_tracks(ea->sort, eb->sort,
100                         ea->display, eb->display,
101                         ea->path, eb->path);
102 }
103
104 static const char *front_url(void) {
105   char *url;
106   const char *mgmt;
107
108   /* preserve management interface visibility */
109   if((mgmt = cgi_get("mgmt")) && !strcmp(mgmt, "true")) {
110     byte_xasprintf(&url, "%s?mgmt=true", config->url);
111     return url;
112   }
113   return config->url;
114 }
115
116 static void header_cookie(struct sink *output) {
117   struct dynstr d[1];
118   struct url u;
119
120   memset(&u, 0, sizeof u);
121   dynstr_init(d);
122   parse_url(config->url, &u);
123   if(login_cookie) {
124     dynstr_append_string(d, "disorder=");
125     dynstr_append_string(d, login_cookie);
126   } else {
127     /* Force browser to discard cookie */
128     dynstr_append_string(d, "disorder=none;Max-Age=0");
129   }
130   if(u.path) {
131     /* The default domain matches the request host, so we need not override
132      * that.  But the default path only goes up to the rightmost /, which would
133      * cause the browser to expose the cookie to other CGI programs on the same
134      * web server. */
135     dynstr_append_string(d, ";Version=1;Path=");
136     /* Formally we are supposed to quote the path, since it invariably has a
137      * slash in it.  However Safari does not parse quoted paths correctly, so
138      * this won't work.  Fortunately nothing else seems to care about proper
139      * quoting of paths, so in practice we get with it.  (See also
140      * parse_cookie() where we are liberal about cookie paths on the way back
141      * in.) */
142     dynstr_append_string(d, u.path);
143   }
144   dynstr_terminate(d);
145   cgi_header(output, "Set-Cookie", d->vec);
146 }
147
148 static void redirect(struct sink *output) {
149   const char *back;
150
151   back = cgi_get("back");
152   cgi_header(output, "Location", back && *back ? back : front_url());
153   header_cookie(output);
154   cgi_body(output);
155 }
156
157 static void expand_template(dcgi_state *ds, cgi_sink *output,
158                             const char *action) {
159   cgi_header(output->sink, "Content-Type", "text/html");
160   header_cookie(output->sink);
161   cgi_body(output->sink);
162   expand(output, action, ds);
163 }
164
165 /* actions ********************************************************************/
166
167 static void act_disable(cgi_sink *output,
168                         dcgi_state *ds) {
169   if(ds->g->client)
170     disorder_disable(ds->g->client);
171   redirect(output->sink);
172 }
173
174 static void act_enable(cgi_sink *output,
175                               dcgi_state *ds) {
176   if(ds->g->client)
177     disorder_enable(ds->g->client);
178   redirect(output->sink);
179 }
180
181 static void act_random_disable(cgi_sink *output,
182                                dcgi_state *ds) {
183   if(ds->g->client)
184     disorder_random_disable(ds->g->client);
185   redirect(output->sink);
186 }
187
188 static void act_random_enable(cgi_sink *output,
189                               dcgi_state *ds) {
190   if(ds->g->client)
191     disorder_random_enable(ds->g->client);
192   redirect(output->sink);
193 }
194
195 static void act_remove(cgi_sink *output,
196                        dcgi_state *ds) {
197   const char *id;
198
199   if(!(id = cgi_get("id"))) fatal(0, "missing id argument");
200   if(ds->g->client)
201     disorder_remove(ds->g->client, id);
202   redirect(output->sink);
203 }
204
205 static void act_move(cgi_sink *output,
206                      dcgi_state *ds) {
207   const char *id, *delta;
208
209   if(!(id = cgi_get("id"))) fatal(0, "missing id argument");
210   if(!(delta = cgi_get("delta"))) fatal(0, "missing delta argument");
211   if(ds->g->client)
212     disorder_move(ds->g->client, id, atoi(delta));
213   redirect(output->sink);
214 }
215
216 static void act_scratch(cgi_sink *output,
217                         dcgi_state *ds) {
218   if(ds->g->client)
219     disorder_scratch(ds->g->client, cgi_get("id"));
220   redirect(output->sink);
221 }
222
223 static void act_playing(cgi_sink *output, dcgi_state *ds) {
224   char r[1024];
225   long refresh = config->refresh, length;
226   time_t now, fin;
227   int random_enabled = 0;
228   int enabled = 0;
229
230   lookups(ds, DC_PLAYING|DC_QUEUE);
231   cgi_header(output->sink, "Content-Type", "text/html");
232   disorder_random_enabled(ds->g->client, &random_enabled);
233   disorder_enabled(ds->g->client, &enabled);
234   if(ds->g->playing
235      && ds->g->playing->state == playing_started /* i.e. not paused */
236      && !disorder_length(ds->g->client, ds->g->playing->track, &length)
237      && length
238      && ds->g->playing->sofar >= 0) {
239     /* Try to put the next refresh at the start of the next track. */
240     time(&now);
241     fin = now + length - ds->g->playing->sofar + config->gap;
242     if(now + refresh > fin)
243       refresh = fin - now;
244   }
245   if(ds->g->queue && ds->g->queue->state == playing_isscratch) {
246     /* next track is a scratch, don't leave more than the inter-track gap */
247     if(refresh > config->gap)
248       refresh = config->gap;
249   }
250   if(!ds->g->playing && ((ds->g->queue
251                           && ds->g->queue->state != playing_random)
252                          || random_enabled) && enabled) {
253     /* no track playing but playing is enabled and there is something coming
254      * up, must be in a gap */
255     if(refresh > config->gap)
256       refresh = config->gap;
257   }
258   byte_snprintf(r, sizeof r, "%ld;url=%s", refresh > 0 ? refresh : 1,
259                 front_url());
260   cgi_header(output->sink, "Refresh", r);
261   header_cookie(output->sink);
262   cgi_body(output->sink);
263   expand(output, "playing", ds);
264 }
265
266 static void act_play(cgi_sink *output,
267                      dcgi_state *ds) {
268   const char *track, *dir;
269   char **tracks;
270   int ntracks, n;
271   struct entry *e;
272
273   if((track = cgi_get("file"))) {
274     disorder_play(ds->g->client, track);
275   } else if((dir = cgi_get("directory"))) {
276     if(disorder_files(ds->g->client, dir, 0, &tracks, &ntracks)) ntracks = 0;
277     if(ntracks) {
278       e = xmalloc(ntracks * sizeof (struct entry));
279       for(n = 0; n < ntracks; ++n) {
280         e[n].path = tracks[n];
281         e[n].sort = trackname_transform("track", tracks[n], "sort");
282         e[n].display = trackname_transform("track", tracks[n], "display");
283       }
284       qsort(e, ntracks, sizeof (struct entry), compare_entry);
285       for(n = 0; n < ntracks; ++n)
286         disorder_play(ds->g->client, e[n].path);
287     }
288   }
289   /* XXX error handling */
290   redirect(output->sink);
291 }
292
293 static int clamp(int n, int min, int max) {
294   if(n < min)
295     return min;
296   if(n > max)
297     return max;
298   return n;
299 }
300
301 static const char *volume_url(void) {
302   char *url;
303   
304   byte_xasprintf(&url, "%s?action=volume", config->url);
305   return url;
306 }
307
308 static void act_volume(cgi_sink *output, dcgi_state *ds) {
309   const char *l, *r, *d, *back;
310   int nd, changed = 0;;
311
312   if((d = cgi_get("delta"))) {
313     lookups(ds, DC_VOLUME);
314     nd = clamp(atoi(d), -255, 255);
315     disorder_set_volume(ds->g->client,
316                         clamp(ds->g->volume_left + nd, 0, 255),
317                         clamp(ds->g->volume_right + nd, 0, 255));
318     changed = 1;
319   } else if((l = cgi_get("left")) && (r = cgi_get("right"))) {
320     disorder_set_volume(ds->g->client, atoi(l), atoi(r));
321     changed = 1;
322   }
323   if(changed) {
324     /* redirect back to ourselves (but without the volume-changing bits in the
325      * URL) */
326     cgi_header(output->sink, "Location",
327                (back = cgi_get("back")) ? back : volume_url());
328     header_cookie(output->sink);
329     cgi_body(output->sink);
330   } else {
331     cgi_header(output->sink, "Content-Type", "text/html");
332     header_cookie(output->sink);
333     cgi_body(output->sink);
334     expand(output, "volume", ds);
335   }
336 }
337
338 static void act_prefs_errors(const char *msg,
339                              void attribute((unused)) *u) {
340   fatal(0, "error splitting parts list: %s", msg);
341 }
342
343 static const char *numbered_arg(const char *argname, int numfile) {
344   char *fullname;
345
346   byte_xasprintf(&fullname, "%d_%s", numfile, argname);
347   return cgi_get(fullname);
348 }
349
350 static void process_prefs(dcgi_state *ds, int numfile) {
351   const char *file, *name, *value, *part, *parts, *current, *context;
352   char **partslist;
353
354   if(!(file = numbered_arg("file", numfile)))
355     /* The first file doesn't need numbering. */
356     if(numfile > 0 || !(file = cgi_get("file")))
357       return;
358   if((parts = numbered_arg("parts", numfile))
359      || (parts = cgi_get("parts"))) {
360     /* Default context is display.  Other contexts not actually tested. */
361     if(!(context = numbered_arg("context", numfile))) context = "display";
362     partslist = split(parts, 0, 0, act_prefs_errors, 0);
363     while((part = *partslist++)) {
364       if(!(value = numbered_arg(part, numfile)))
365         continue;
366       /* If it's already right (whether regexps or db) don't change anything,
367        * so we don't fill the database up with rubbish. */
368       if(disorder_part(ds->g->client, (char **)&current,
369                        file, context, part))
370         fatal(0, "disorder_part() failed");
371       if(!strcmp(current, value))
372         continue;
373       byte_xasprintf((char **)&name, "trackname_%s_%s", context, part);
374       disorder_set(ds->g->client, file, name, value);
375     }
376     if((value = numbered_arg("random", numfile)))
377       disorder_unset(ds->g->client, file, "pick_at_random");
378     else
379       disorder_set(ds->g->client, file, "pick_at_random", "0");
380     if((value = numbered_arg("tags", numfile))) {
381       if(!*value)
382         disorder_unset(ds->g->client, file, "tags");
383       else
384         disorder_set(ds->g->client, file, "tags", value);
385     }
386     if((value = numbered_arg("weight", numfile))) {
387       if(!*value || !strcmp(value, "90000"))
388         disorder_unset(ds->g->client, file, "weight");
389       else
390         disorder_set(ds->g->client, file, "weight", value);
391     }
392   } else if((name = cgi_get("name"))) {
393     /* Raw preferences.  Not well supported in the templates at the moment. */
394     value = cgi_get("value");
395     if(value)
396       disorder_set(ds->g->client, file, name, value);
397     else
398       disorder_unset(ds->g->client, file, name);
399   }
400 }
401
402 static void act_prefs(cgi_sink *output, dcgi_state *ds) {
403   const char *files;
404   int nfiles, numfile;
405
406   if((files = cgi_get("files"))) nfiles = atoi(files);
407   else nfiles = 1;
408   for(numfile = 0; numfile < nfiles; ++numfile)
409     process_prefs(ds, numfile);
410   cgi_header(output->sink, "Content-Type", "text/html");
411   header_cookie(output->sink);
412   cgi_body(output->sink);
413   expand(output, "prefs", ds);
414 }
415
416 static void act_pause(cgi_sink *output,
417                       dcgi_state *ds) {
418   if(ds->g->client)
419     disorder_pause(ds->g->client);
420   redirect(output->sink);
421 }
422
423 static void act_resume(cgi_sink *output,
424                        dcgi_state *ds) {
425   if(ds->g->client)
426     disorder_resume(ds->g->client);
427   redirect(output->sink);
428 }
429
430 static void act_login(cgi_sink *output,
431                       dcgi_state *ds) {
432   const char *username, *password, *back;
433   disorder_client *c;
434
435   username = cgi_get("username");
436   password = cgi_get("password");
437   if(!username || !password
438      || !strcmp(username, "guest")/*bodge to avoid guest cookies*/) {
439     /* We're just visiting the login page */
440     expand_template(ds, output, "login");
441     return;
442   }
443   /* We'll need a new connection as we are going to stop being guest */
444   c = disorder_new(0);
445   if(disorder_connect_user(c, username, password)) {
446     cgi_set_option("error", "loginfailed");
447     expand_template(ds, output, "login");
448     return;
449   }
450   if(disorder_make_cookie(c, &login_cookie)) {
451     cgi_set_option("error", "cookiefailed");
452     expand_template(ds, output, "login");
453     return;
454   }
455   /* Use the new connection henceforth */
456   ds->g->client = c;
457   ds->g->flags = 0;
458   /* We have a new cookie */
459   header_cookie(output->sink);
460   cgi_set_option("status", "loginok");
461   if((back = cgi_get("back")) && *back)
462     /* Redirect back to somewhere or other */
463     redirect(output->sink);
464   else
465     /* Stick to the login page */
466     expand_template(ds, output, "login");
467 }
468
469 static void act_logout(cgi_sink *output,
470                        dcgi_state *ds) {
471   disorder_revoke(ds->g->client);
472   login_cookie = 0;
473   /* Reconnect as guest */
474   disorder_cgi_login(ds, output);
475   /* Back to the login page */
476   cgi_set_option("status", "logoutok");
477   expand_template(ds, output, "login");
478 }
479
480 static void act_register(cgi_sink *output,
481                          dcgi_state *ds) {
482   const char *username, *password, *password2, *email;
483   char *confirm, *content_type;
484   const char *text, *encoding, *charset;
485
486   username = cgi_get("username");
487   password = cgi_get("password1");
488   password2 = cgi_get("password2");
489   email = cgi_get("email");
490
491   if(!username || !*username) {
492     cgi_set_option("error", "nousername");
493     expand_template(ds, output, "login");
494     return;
495   }
496   if(!password || !*password) {
497     cgi_set_option("error", "nopassword");
498     expand_template(ds, output, "login");
499     return;
500   }
501   if(!password2 || !*password2 || strcmp(password, password2)) {
502     cgi_set_option("error", "passwordmismatch");
503     expand_template(ds, output, "login");
504     return;
505   }
506   if(!email || !*email) {
507     cgi_set_option("error", "noemail");
508     expand_template(ds, output, "login");
509     return;
510   }
511   /* We could well do better address validation but for now we'll just do the
512    * minimum */
513   if(!strchr(email, '@')) {
514     cgi_set_option("error", "bademail");
515     expand_template(ds, output, "login");
516     return;
517   }
518   if(disorder_register(ds->g->client, username, password, email, &confirm)) {
519     cgi_set_option("error", "cannotregister");
520     expand_template(ds, output, "login");
521     return;
522   }
523   /* Send the user a mail */
524   /* TODO templatize this */
525   byte_xasprintf((char **)&text,
526                  "Welcome to DisOrder.  To active your login, please visit this URL:\n"
527                  "\n"
528                  "%s?c=%s\n", config->url, urlencodestring(confirm));
529   if(!(text = mime_encode_text(text, &charset, &encoding)))
530     fatal(0, "cannot encode email");
531   byte_xasprintf(&content_type, "text/plain;charset=%s",
532                  quote822(charset, 0));
533   sendmail("", config->mail_sender, email, "Welcome to DisOrder",
534            encoding, content_type, text); /* TODO error checking  */
535   /* We'll go back to the login page with a suitable message */
536   cgi_set_option("status", "registered");
537   expand_template(ds, output, "login");
538 }
539
540 static void act_confirm(cgi_sink *output,
541                         dcgi_state *ds) {
542   const char *confirmation;
543
544   if(!(confirmation = cgi_get("c"))) {
545     cgi_set_option("error", "noconfirm");
546     expand_template(ds, output, "login");
547   }
548   /* Confirm our registration */
549   if(disorder_confirm(ds->g->client, confirmation)) {
550     cgi_set_option("error", "badconfirm");
551     expand_template(ds, output, "login");
552   }
553   /* Get a cookie */
554   if(disorder_make_cookie(ds->g->client, &login_cookie)) {
555     cgi_set_option("error", "cookiefailed");
556     expand_template(ds, output, "login");
557     return;
558   }
559   /* Discard any cached data JIC */
560   ds->g->flags = 0;
561   /* We have a new cookie */
562   header_cookie(output->sink);
563   cgi_set_option("status", "confirmed");
564   expand_template(ds, output, "login");
565 }
566
567 static void act_edituser(cgi_sink *output,
568                          dcgi_state *ds) {
569   const char *email = cgi_get("email"), *password = cgi_get("changepassword1");
570   const char *password2 = cgi_get("changepassword2");
571   int newpassword = 0;
572   disorder_client *c;
573
574   if((password && *password) || (password && *password2)) {
575     if(!password || !password2 || strcmp(password, password2)) {
576       cgi_set_option("error", "passwordmismatch");
577       expand_template(ds, output, "login");
578       return;
579     }
580   } else
581     password = password2 = 0;
582   
583   if(email) {
584     if(disorder_edituser(ds->g->client, disorder_user(ds->g->client),
585                          "email", email)) {
586       cgi_set_option("error", "badedit");
587       expand_template(ds, output, "login");
588       return;
589     }
590   }
591   if(password) {
592     if(disorder_edituser(ds->g->client, disorder_user(ds->g->client),
593                          "password", password)) {
594       cgi_set_option("error", "badedit");
595       expand_template(ds, output, "login");
596       return;
597     }
598     newpassword = 1;
599   }
600   if(newpassword) {
601     login_cookie = 0;                   /* it'll be invalid now */
602     /* This is a bit duplicative of act_login() */
603     c = disorder_new(0);
604     if(disorder_connect_user(c, disorder_user(ds->g->client), password)) {
605       cgi_set_option("error", "loginfailed");
606       expand_template(ds, output, "login");
607       return;
608     }
609     if(disorder_make_cookie(c, &login_cookie)) {
610       cgi_set_option("error", "cookiefailed");
611       expand_template(ds, output, "login");
612       return;
613     }
614     /* Use the new connection henceforth */
615     ds->g->client = c;
616     ds->g->flags = 0;
617     /* We have a new cookie */
618     header_cookie(output->sink);
619   }
620   cgi_set_option("status", "edited");
621   expand_template(ds, output, "login");  
622 }
623
624 static void act_reminder(cgi_sink *output,
625                          dcgi_state *ds) {
626   const char *const username = cgi_get("username");
627
628   if(!username || !*username) {
629     cgi_set_option("error", "nousername");
630     expand_template(ds, output, "login");
631     return;
632   }
633   if(disorder_reminder(ds->g->client, username)) {
634     cgi_set_option("error", "reminderfailed");
635     expand_template(ds, output, "login");
636     return;
637   }
638   cgi_set_option("status", "reminded");
639   expand_template(ds, output, "login");  
640 }
641
642 static const struct action {
643   const char *name;
644   void (*handler)(cgi_sink *output, dcgi_state *ds);
645 } actions[] = {
646   { "confirm", act_confirm },
647   { "disable", act_disable },
648   { "edituser", act_edituser },
649   { "enable", act_enable },
650   { "login", act_login },
651   { "logout", act_logout },
652   { "move", act_move },
653   { "pause", act_pause },
654   { "play", act_play },
655   { "playing", act_playing },
656   { "prefs", act_prefs },
657   { "random-disable", act_random_disable },
658   { "random-enable", act_random_enable },
659   { "register", act_register },
660   { "reminder", act_reminder },
661   { "remove", act_remove },
662   { "resume", act_resume },
663   { "scratch", act_scratch },
664   { "volume", act_volume },
665 };
666
667 /* expansions *****************************************************************/
668
669 static void exp_label(int attribute((unused)) nargs,
670                       char **args,
671                       cgi_sink *output,
672                       void attribute((unused)) *u) {
673   cgi_output(output, "%s", cgi_label(args[0]));
674 }
675
676 struct trackinfo_state {
677   dcgi_state *ds;
678   const struct queue_entry *q;
679   long length;
680   time_t when;
681 };
682
683 struct result {
684   char *track;
685   const char *sort;
686 };
687
688 static int compare_result(const void *a, const void *b) {
689   const struct result *ra = a, *rb = b;
690   int c;
691
692   if(!(c = strcmp(ra->sort, rb->sort)))
693     c = strcmp(ra->track, rb->track);
694   return c;
695 }
696
697 static void exp_search(int nargs,
698                        char **args,
699                        cgi_sink *output,
700                        void *u) {
701   dcgi_state *ds = u, substate;
702   char **tracks;
703   const char *q, *context, *part, *template;
704   int ntracks, n, m;
705   struct result *r;
706
707   switch(nargs) {
708   case 2:
709     part = args[0];
710     context = "sort";
711     template = args[1];
712     break;
713   case 3:
714     part = args[0];
715     context = args[1];
716     template = args[2];
717     break;
718   default:
719     assert(!"should never happen");
720     part = context = template = 0;      /* quieten compiler */
721   }
722   if(ds->tracks == 0) {
723     /* we are the top level, let's get some search results */
724     if(!(q = cgi_get("query"))) return; /* no results yet */
725     if(disorder_search(ds->g->client, q, &tracks, &ntracks)) return;
726     if(!ntracks) return;
727   } else {
728     tracks = ds->tracks;
729     ntracks = ds->ntracks;
730   }
731   assert(ntracks != 0);
732   /* sort tracks by the appropriate part */
733   r = xmalloc(ntracks * sizeof *r);
734   for(n = 0; n < ntracks; ++n) {
735     r[n].track = tracks[n];
736     if(disorder_part(ds->g->client, (char **)&r[n].sort,
737                      tracks[n], context, part))
738       fatal(0, "disorder_part() failed");
739   }
740   qsort(r, ntracks, sizeof (struct result), compare_result);
741   /* expand the 2nd arg once for each group.  We re-use the passed-in tracks
742    * array as we know it's guaranteed to be big enough and isn't going to be
743    * used for anything else any more. */
744   memset(&substate, 0, sizeof substate);
745   substate.g = ds->g;
746   substate.first = 1;
747   n = 0;
748   while(n < ntracks) {
749     substate.tracks = tracks;
750     substate.ntracks = 0;
751     m = n;
752     while(m < ntracks
753           && !strcmp(r[m].sort, r[n].sort))
754       tracks[substate.ntracks++] = r[m++].track;
755     substate.last = (m == ntracks);
756     expandstring(output, template, &substate);
757     substate.index++;
758     substate.first = 0;
759     n = m;
760   }
761   assert(substate.last != 0);
762 }
763
764 static void exp_stats(int attribute((unused)) nargs,
765                       char attribute((unused)) **args,
766                       cgi_sink *output,
767                       void *u) {
768   dcgi_state *ds = u;
769   char **v;
770
771   cgi_opentag(output->sink, "pre", "class", "stats", (char *)0);
772   if(!disorder_stats(ds->g->client, &v, 0)) {
773     while(*v)
774       cgi_output(output, "%s\n", *v++);
775   }
776   cgi_closetag(output->sink, "pre");
777 }
778
779 static char *expandarg(const char *arg, dcgi_state *ds) {
780   struct dynstr d;
781   cgi_sink output;
782
783   dynstr_init(&d);
784   output.quote = 0;
785   output.sink = sink_dynstr(&d);
786   expandstring(&output, arg, ds);
787   dynstr_terminate(&d);
788   return d.vec;
789 }
790
791 static void exp_isfiles(int attribute((unused)) nargs,
792                         char attribute((unused)) **args,
793                         cgi_sink *output,
794                         void *u) {
795   dcgi_state *ds = u;
796
797   lookups(ds, DC_FILES);
798   sink_printf(output->sink, "%s", bool2str(!!ds->g->nfiles));
799 }
800
801 static void exp_isdirectories(int attribute((unused)) nargs,
802                               char attribute((unused)) **args,
803                               cgi_sink *output,
804                               void *u) {
805   dcgi_state *ds = u;
806
807   lookups(ds, DC_DIRS);
808   sink_printf(output->sink, "%s", bool2str(!!ds->g->ndirs));
809 }
810
811 static void exp_choose(int attribute((unused)) nargs,
812                        char **args,
813                        cgi_sink *output,
814                        void *u) {
815   dcgi_state *ds = u;
816   dcgi_state substate;
817   int nfiles, n;
818   char **files;
819   struct entry *e;
820   const char *type, *what = expandarg(args[0], ds);
821
822   if(!strcmp(what, "files")) {
823     lookups(ds, DC_FILES);
824     files = ds->g->files;
825     nfiles = ds->g->nfiles;
826     type = "track";
827   } else if(!strcmp(what, "directories")) {
828     lookups(ds, DC_DIRS);
829     files = ds->g->dirs;
830     nfiles = ds->g->ndirs;
831     type = "dir";
832   } else {
833     error(0, "unknown @choose@ argument '%s'", what);
834     return;
835   }
836   e = xmalloc(nfiles * sizeof (struct entry));
837   for(n = 0; n < nfiles; ++n) {
838     e[n].path = files[n];
839     e[n].sort = trackname_transform(type, files[n], "sort");
840     e[n].display = trackname_transform(type, files[n], "display");
841   }
842   qsort(e, nfiles, sizeof (struct entry), compare_entry);
843   memset(&substate, 0, sizeof substate);
844   substate.g = ds->g;
845   substate.first = 1;
846   for(n = 0; n < nfiles; ++n) {
847     substate.last = (n == nfiles - 1);
848     substate.index = n;
849     substate.entry = &e[n];
850     expandstring(output, args[1], &substate);
851     substate.first = 0;
852   }
853 }
854
855 static void exp_file(int attribute((unused)) nargs,
856                      char attribute((unused)) **args,
857                      cgi_sink *output,
858                      void *u) {
859   dcgi_state *ds = u;
860
861   if(ds->entry)
862     cgi_output(output, "%s", ds->entry->path);
863   else if(ds->track)
864     cgi_output(output, "%s", ds->track->track);
865   else if(ds->tracks)
866     cgi_output(output, "%s", ds->tracks[0]);
867 }
868
869 static void exp_navigate(int attribute((unused)) nargs,
870                          char **args,
871                          cgi_sink *output,
872                          void *u) {
873   dcgi_state *ds = u;
874   dcgi_state substate;
875   const char *path = expandarg(args[0], ds);
876   const char *ptr;
877   int dirlen;
878
879   if(*path) {
880     memset(&substate, 0, sizeof substate);
881     substate.g = ds->g;
882     ptr = path + 1;                     /* skip root */
883     dirlen = 0;
884     substate.nav_path = path;
885     substate.first = 1;
886     while(*ptr) {
887       while(*ptr && *ptr != '/')
888         ++ptr;
889       substate.last = !*ptr;
890       substate.nav_len = ptr - path;
891       substate.nav_dirlen = dirlen;
892       expandstring(output, args[1], &substate);
893       dirlen = substate.nav_len;
894       if(*ptr) ++ptr;
895       substate.first = 0;
896     }
897   }
898 }
899
900 static void exp_fullname(int attribute((unused)) nargs,
901                          char attribute((unused)) **args,
902                          cgi_sink *output,
903                          void *u) {
904   dcgi_state *ds = u;
905   cgi_output(output, "%.*s", ds->nav_len, ds->nav_path);
906 }
907
908 static void exp_basename(int nargs,
909                          char **args,
910                          cgi_sink *output,
911                          void *u) {
912   dcgi_state *ds = u;
913   const char *s;
914   
915   if(nargs) {
916     if((s = strrchr(args[0], '/'))) ++s;
917     else s = args[0];
918     cgi_output(output, "%s", s);
919   } else
920     cgi_output(output, "%.*s", ds->nav_len - ds->nav_dirlen - 1,
921                ds->nav_path + ds->nav_dirlen + 1);
922 }
923
924 static void exp_dirname(int nargs,
925                         char **args,
926                         cgi_sink *output,
927                         void *u) {
928   dcgi_state *ds = u;
929   const char *s;
930   
931   if(nargs) {
932     if((s = strrchr(args[0], '/')))
933       cgi_output(output, "%.*s", (int)(s - args[0]), args[0]);
934   } else
935     cgi_output(output, "%.*s", ds->nav_dirlen, ds->nav_path);
936 }
937
938 static void exp_files(int attribute((unused)) nargs,
939                       char **args,
940                       cgi_sink *output,
941                       void *u) {
942   dcgi_state *ds = u;
943   dcgi_state substate;
944   const char *nfiles_arg, *directory;
945   int nfiles, numfile;
946   struct kvp *k;
947
948   memset(&substate, 0, sizeof substate);
949   substate.g = ds->g;
950   if((directory = cgi_get("directory"))) {
951     /* Prefs for whole directory. */
952     lookups(ds, DC_FILES);
953     /* Synthesize args for the file list. */
954     nfiles = ds->g->nfiles;
955     for(numfile = 0; numfile < nfiles; ++numfile) {
956       k = xmalloc(sizeof *k);
957       byte_xasprintf((char **)&k->name, "%d_file", numfile);
958       k->value = ds->g->files[numfile];
959       k->next = cgi_args;
960       cgi_args = k;
961     }
962   } else {
963     /* Args already present. */
964     if((nfiles_arg = cgi_get("files"))) nfiles = atoi(nfiles_arg);
965     else nfiles = 1;
966   }
967   for(numfile = 0; numfile < nfiles; ++numfile) {
968     substate.index = numfile;
969     expandstring(output, args[0], &substate);
970   }
971 }
972
973 static void exp_nfiles(int attribute((unused)) nargs,
974                        char attribute((unused)) **args,
975                        cgi_sink *output,
976                        void *u) {
977   dcgi_state *ds = u;
978   const char *files_arg;
979
980   if(cgi_get("directory")) {
981     lookups(ds, DC_FILES);
982     cgi_output(output, "%d", ds->g->nfiles);
983   } else if((files_arg = cgi_get("files")))
984     cgi_output(output, "%s", files_arg);
985   else
986     cgi_output(output, "1");
987 }
988
989 static void exp_image(int attribute((unused)) nargs,
990                       char **args,
991                       cgi_sink *output,
992                       void attribute((unused)) *u) {
993   char *labelname;
994   const char *imagestem;
995
996   byte_xasprintf(&labelname, "images.%s", args[0]);
997   if(cgi_label_exists(labelname))
998     imagestem = cgi_label(labelname);
999   else if(strchr(args[0], '.'))
1000     imagestem = args[0];
1001   else
1002     byte_xasprintf((char **)&imagestem, "%s.png", args[0]);
1003   if(cgi_label_exists("url.static"))
1004     cgi_output(output, "%s/%s", cgi_label("url.static"), imagestem);
1005   else
1006     cgi_output(output, "/disorder/%s", imagestem);
1007 }
1008
1009 static const struct cgi_expansion expansions[] = {
1010   { "#", 0, INT_MAX, EXP_MAGIC, exp_comment },
1011   { "action", 0, 0, 0, exp_action },
1012   { "and", 0, INT_MAX, EXP_MAGIC, exp_and },
1013   { "arg", 1, 1, 0, exp_arg },
1014   { "basename", 0, 1, 0, exp_basename },
1015   { "choose", 2, 2, EXP_MAGIC, exp_choose },
1016   { "define", 3, 3, EXP_MAGIC, exp_define },
1017   { "dirname", 0, 1, 0, exp_dirname },
1018   { "enabled", 0, 0, 0, exp_enabled },
1019   { "eq", 2, 2, 0, exp_eq },
1020   { "file", 0, 0, 0, exp_file },
1021   { "files", 1, 1, EXP_MAGIC, exp_files },
1022   { "fullname", 0, 0, 0, exp_fullname },
1023   { "id", 0, 0, 0, exp_id },
1024   { "if", 2, 3, EXP_MAGIC, exp_if },
1025   { "image", 1, 1, 0, exp_image },
1026   { "include", 1, 1, 0, exp_include },
1027   { "index", 0, 0, 0, exp_index },
1028   { "isdirectories", 0, 0, 0, exp_isdirectories },
1029   { "isfiles", 0, 0, 0, exp_isfiles },
1030   { "isfirst", 0, 0, 0, exp_isfirst },
1031   { "islast", 0, 0, 0, exp_islast },
1032   { "isnew", 0, 0, 0, exp_isnew },
1033   { "isplaying", 0, 0, 0, exp_isplaying },
1034   { "isqueue", 0, 0, 0, exp_isqueue },
1035   { "isrecent", 0, 0, 0, exp_isrecent },
1036   { "label", 1, 1, 0, exp_label },
1037   { "length", 0, 0, 0, exp_length },
1038   { "movable", 0, 0, 0, exp_movable },
1039   { "navigate", 2, 2, EXP_MAGIC, exp_navigate },
1040   { "ne", 2, 2, 0, exp_ne },
1041   { "new", 1, 1, EXP_MAGIC, exp_new },
1042   { "nfiles", 0, 0, 0, exp_nfiles },
1043   { "nonce", 0, 0, 0, exp_nonce },
1044   { "not", 1, 1, 0, exp_not },
1045   { "or", 0, INT_MAX, EXP_MAGIC, exp_or },
1046   { "parity", 0, 0, 0, exp_parity },
1047   { "part", 1, 3, 0, exp_part },
1048   { "paused", 0, 0, 0, exp_paused },
1049   { "playing", 1, 1, EXP_MAGIC, exp_playing },
1050   { "pref", 2, 2, 0, exp_pref },
1051   { "prefname", 0, 0, 0, exp_prefname },
1052   { "prefs", 2, 2, EXP_MAGIC, exp_prefs },
1053   { "prefvalue", 0, 0, 0, exp_prefvalue },
1054   { "queue", 1, 1, EXP_MAGIC, exp_queue },
1055   { "random-enabled", 0, 0, 0, exp_random_enabled },
1056   { "recent", 1, 1, EXP_MAGIC, exp_recent },
1057   { "removable", 0, 0, 0, exp_removable },
1058   { "resolve", 1, 1, 0, exp_resolve },
1059   { "right", 1, 3, EXP_MAGIC, exp_right },
1060   { "scratchable", 0, 0, 0, exp_scratchable },
1061   { "search", 2, 3, EXP_MAGIC, exp_search },
1062   { "server-version", 0, 0, 0, exp_server_version },
1063   { "shell", 1, 1, 0, exp_shell },
1064   { "state", 0, 0, 0, exp_state },
1065   { "stats", 0, 0, 0, exp_stats },
1066   { "thisurl", 0, 0, 0, exp_thisurl },
1067   { "track", 0, 0, 0, exp_track },
1068   { "trackstate", 1, 1, 0, exp_trackstate },
1069   { "transform", 2, 3, 0, exp_transform },
1070   { "url", 0, 0, 0, exp_url },
1071   { "urlquote", 1, 1, 0, exp_urlquote },
1072   { "user", 0, 0, 0, exp_user },
1073   { "userinfo", 1, 1, 0, exp_userinfo },
1074   { "version", 0, 0, 0, exp_version },
1075   { "volume", 1, 1, 0, exp_volume },
1076   { "when", 0, 0, 0, exp_when },
1077   { "who", 0, 0, 0, exp_who }
1078 };
1079
1080 static void expand(cgi_sink *output,
1081                    const char *template,
1082                    dcgi_state *ds) {
1083   cgi_expand(template,
1084              expansions, sizeof expansions / sizeof *expansions,
1085              output,
1086              ds);
1087 }
1088
1089 static void expandstring(cgi_sink *output,
1090                          const char *string,
1091                          dcgi_state *ds) {
1092   cgi_expand_string("",
1093                     string,
1094                     expansions, sizeof expansions / sizeof *expansions,
1095                     output,
1096                     ds);
1097 }
1098
1099 static void perform_action(cgi_sink *output, dcgi_state *ds,
1100                            const char *action) {
1101   int n;
1102
1103   /* We don't ever want anything to be cached */
1104   cgi_header(output->sink, "Cache-Control", "no-cache");
1105   if((n = TABLE_FIND(actions, struct action, name, action)) >= 0)
1106     actions[n].handler(output, ds);
1107   else
1108     expand_template(ds, output, action);
1109 }
1110
1111 void disorder_cgi(cgi_sink *output, dcgi_state *ds) {
1112   const char *action = cgi_get("action");
1113
1114   if(!action) {
1115     /* We allow URLs which are just confirm=... in order to keep confirmation
1116      * URLs, which are user-facing, as short as possible. */
1117     if(cgi_get("c"))
1118       action = "confirm";
1119     else
1120       action = "playing";
1121   }
1122   perform_action(output, ds, action);
1123 }
1124
1125 void disorder_cgi_error(cgi_sink *output, dcgi_state *ds,
1126                         const char *msg) {
1127   cgi_set_option("error", msg);
1128   perform_action(output, ds, "error");
1129 }
1130
1131 /** @brief Log in as the current user or guest if none */
1132 void disorder_cgi_login(dcgi_state *ds, cgi_sink *output) {
1133   /* Create a new connection */
1134   ds->g->client = disorder_new(0);
1135   /* Forget everything we knew */
1136   ds->g->flags = 0;
1137   /* Reconnect */
1138   if(disorder_connect_cookie(ds->g->client, login_cookie)) {
1139     disorder_cgi_error(output, ds, "connect");
1140     exit(0);
1141   }
1142   /* If there was a cookie but it went bad, we forget it */
1143   if(login_cookie && !strcmp(disorder_user(ds->g->client), "guest"))
1144     login_cookie = 0;
1145 }
1146
1147 /*
1148 Local Variables:
1149 c-basic-offset:2
1150 comment-column:40
1151 fill-column:79
1152 End:
1153 */