chiark / gitweb /
never set an expired cookie; on logout, re-login as guest
[disorder] / server / dcgi.c
1 /*
2  * This file is part of DisOrder.
3  * Copyright (C) 2004, 2005, 2006, 2007 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 "cgi.h"
42 #include "dcgi.h"
43 #include "log.h"
44 #include "configuration.h"
45 #include "table.h"
46 #include "queue.h"
47 #include "plugin.h"
48 #include "split.h"
49 #include "wstat.h"
50 #include "kvp.h"
51 #include "syscalls.h"
52 #include "printf.h"
53 #include "regsub.h"
54 #include "defs.h"
55 #include "trackname.h"
56 #include "charset.h"
57
58 char *login_cookie;
59
60 static void expand(cgi_sink *output,
61                    const char *template,
62                    dcgi_state *ds);
63 static void expandstring(cgi_sink *output,
64                          const char *string,
65                          dcgi_state *ds);
66
67 struct entry {
68   const char *path;
69   const char *sort;
70   const char *display;
71 };
72
73 static const char *nonce(void) {
74   static unsigned long count;
75   char *s;
76
77   byte_xasprintf(&s, "%lx%lx%lx",
78            (unsigned long)time(0),
79            (unsigned long)getpid(),
80            count++);
81   return s;
82 }
83
84 static int compare_entry(const void *a, const void *b) {
85   const struct entry *ea = a, *eb = b;
86
87   return compare_tracks(ea->sort, eb->sort,
88                         ea->display, eb->display,
89                         ea->path, eb->path);
90 }
91
92 static const char *front_url(void) {
93   char *url;
94   const char *mgmt;
95
96   /* preserve management interface visibility */
97   if((mgmt = cgi_get("mgmt")) && !strcmp(mgmt, "true")) {
98     byte_xasprintf(&url, "%s?mgmt=true", config->url);
99     return url;
100   }
101   return config->url;
102 }
103
104 static void header_cookie(struct sink *output) {
105   struct dynstr d[1];
106   char *s;
107
108   if(login_cookie) {
109     dynstr_init(d);
110     for(s = login_cookie; *s; ++s) {
111       if(*s == '"')
112         dynstr_append(d, '\\');
113       dynstr_append(d, *s);
114     }
115     dynstr_terminate(d);
116     byte_xasprintf(&s, "disorder=\"%s\"", d->vec); /* TODO domain, path, expiry */
117     cgi_header(output, "Set-Cookie", s);
118   } else
119     /* Force browser to discard cookie */
120     cgi_header(output, "Set-Cookie", "disorder=none;Max-Age=0");
121 }
122
123 static void redirect(struct sink *output) {
124   const char *back;
125
126   back = cgi_get("back");
127   cgi_header(output, "Location", back && *back ? back : front_url());
128   header_cookie(output);
129   cgi_body(output);
130 }
131
132 static void expand_template(dcgi_state *ds, cgi_sink *output,
133                             const char *action) {
134   cgi_header(output->sink, "Content-Type", "text/html");
135   header_cookie(output->sink);
136   cgi_body(output->sink);
137   expand(output, action, ds);
138 }
139
140 static void lookups(dcgi_state *ds, unsigned want) {
141   unsigned need;
142   struct queue_entry *r, *rnext;
143   const char *dir, *re;
144
145   if(ds->g->client && (need = want ^ (ds->g->flags & want)) != 0) {
146     if(need & DC_QUEUE)
147       disorder_queue(ds->g->client, &ds->g->queue);
148     if(need & DC_PLAYING)
149       disorder_playing(ds->g->client, &ds->g->playing);
150     if(need & DC_NEW)
151       disorder_new_tracks(ds->g->client, &ds->g->new, &ds->g->nnew, 0);
152     if(need & DC_RECENT) {
153       /* we need to reverse the order of the list */
154       disorder_recent(ds->g->client, &r);
155       while(r) {
156         rnext = r->next;
157         r->next = ds->g->recent;
158         ds->g->recent = r;
159         r = rnext;
160       }
161     }
162     if(need & DC_VOLUME)
163       disorder_get_volume(ds->g->client,
164                           &ds->g->volume_left, &ds->g->volume_right);
165     if(need & (DC_FILES|DC_DIRS)) {
166       if(!(dir = cgi_get("directory")))
167         dir = "";
168       re = cgi_get("regexp");
169       if(need & DC_DIRS)
170         if(disorder_directories(ds->g->client, dir, re,
171                                 &ds->g->dirs, &ds->g->ndirs))
172           ds->g->ndirs = 0;
173       if(need & DC_FILES)
174         if(disorder_files(ds->g->client, dir, re,
175                           &ds->g->files, &ds->g->nfiles))
176           ds->g->nfiles = 0;
177     }
178     ds->g->flags |= need;
179   }
180 }
181
182 /* actions ********************************************************************/
183
184 static void act_disable(cgi_sink *output,
185                         dcgi_state *ds) {
186   if(ds->g->client)
187     disorder_disable(ds->g->client);
188   redirect(output->sink);
189 }
190
191 static void act_enable(cgi_sink *output,
192                               dcgi_state *ds) {
193   if(ds->g->client)
194     disorder_enable(ds->g->client);
195   redirect(output->sink);
196 }
197
198 static void act_random_disable(cgi_sink *output,
199                                dcgi_state *ds) {
200   if(ds->g->client)
201     disorder_random_disable(ds->g->client);
202   redirect(output->sink);
203 }
204
205 static void act_random_enable(cgi_sink *output,
206                               dcgi_state *ds) {
207   if(ds->g->client)
208     disorder_random_enable(ds->g->client);
209   redirect(output->sink);
210 }
211
212 static void act_remove(cgi_sink *output,
213                        dcgi_state *ds) {
214   const char *id;
215
216   if(!(id = cgi_get("id"))) fatal(0, "missing id argument");
217   if(ds->g->client)
218     disorder_remove(ds->g->client, id);
219   redirect(output->sink);
220 }
221
222 static void act_move(cgi_sink *output,
223                      dcgi_state *ds) {
224   const char *id, *delta;
225
226   if(!(id = cgi_get("id"))) fatal(0, "missing id argument");
227   if(!(delta = cgi_get("delta"))) fatal(0, "missing delta argument");
228   if(ds->g->client)
229     disorder_move(ds->g->client, id, atoi(delta));
230   redirect(output->sink);
231 }
232
233 static void act_scratch(cgi_sink *output,
234                         dcgi_state *ds) {
235   if(ds->g->client)
236     disorder_scratch(ds->g->client, cgi_get("id"));
237   redirect(output->sink);
238 }
239
240 static void act_playing(cgi_sink *output, dcgi_state *ds) {
241   char r[1024];
242   long refresh = config->refresh, length;
243   time_t now, fin;
244   int random_enabled = 0;
245   int enabled = 0;
246
247   lookups(ds, DC_PLAYING|DC_QUEUE);
248   cgi_header(output->sink, "Content-Type", "text/html");
249   disorder_random_enabled(ds->g->client, &random_enabled);
250   disorder_enabled(ds->g->client, &enabled);
251   if(ds->g->playing
252      && ds->g->playing->state == playing_started /* i.e. not paused */
253      && !disorder_length(ds->g->client, ds->g->playing->track, &length)
254      && length
255      && ds->g->playing->sofar >= 0) {
256     /* Try to put the next refresh at the start of the next track. */
257     time(&now);
258     fin = now + length - ds->g->playing->sofar + config->gap;
259     if(now + refresh > fin)
260       refresh = fin - now;
261   }
262   if(ds->g->queue && ds->g->queue->state == playing_isscratch) {
263     /* next track is a scratch, don't leave more than the inter-track gap */
264     if(refresh > config->gap)
265       refresh = config->gap;
266   }
267   if(!ds->g->playing && ((ds->g->queue
268                           && ds->g->queue->state != playing_random)
269                          || random_enabled) && enabled) {
270     /* no track playing but playing is enabled and there is something coming
271      * up, must be in a gap */
272     if(refresh > config->gap)
273       refresh = config->gap;
274   }
275   byte_snprintf(r, sizeof r, "%ld;url=%s", refresh > 0 ? refresh : 1,
276                 front_url());
277   cgi_header(output->sink, "Refresh", r);
278   header_cookie(output->sink);
279   cgi_body(output->sink);
280   expand(output, "playing", ds);
281 }
282
283 static void act_play(cgi_sink *output,
284                      dcgi_state *ds) {
285   const char *track, *dir;
286   char **tracks;
287   int ntracks, n;
288   struct entry *e;
289
290   if((track = cgi_get("file"))) {
291     disorder_play(ds->g->client, track);
292   } else if((dir = cgi_get("directory"))) {
293     if(disorder_files(ds->g->client, dir, 0, &tracks, &ntracks)) ntracks = 0;
294     if(ntracks) {
295       e = xmalloc(ntracks * sizeof (struct entry));
296       for(n = 0; n < ntracks; ++n) {
297         e[n].path = tracks[n];
298         e[n].sort = trackname_transform("track", tracks[n], "sort");
299         e[n].display = trackname_transform("track", tracks[n], "display");
300       }
301       qsort(e, ntracks, sizeof (struct entry), compare_entry);
302       for(n = 0; n < ntracks; ++n)
303         disorder_play(ds->g->client, e[n].path);
304     }
305   }
306   /* XXX error handling */
307   redirect(output->sink);
308 }
309
310 static int clamp(int n, int min, int max) {
311   if(n < min)
312     return min;
313   if(n > max)
314     return max;
315   return n;
316 }
317
318 static const char *volume_url(void) {
319   char *url;
320   
321   byte_xasprintf(&url, "%s?action=volume", config->url);
322   return url;
323 }
324
325 static void act_volume(cgi_sink *output, dcgi_state *ds) {
326   const char *l, *r, *d, *back;
327   int nd, changed = 0;;
328
329   if((d = cgi_get("delta"))) {
330     lookups(ds, DC_VOLUME);
331     nd = clamp(atoi(d), -255, 255);
332     disorder_set_volume(ds->g->client,
333                         clamp(ds->g->volume_left + nd, 0, 255),
334                         clamp(ds->g->volume_right + nd, 0, 255));
335     changed = 1;
336   } else if((l = cgi_get("left")) && (r = cgi_get("right"))) {
337     disorder_set_volume(ds->g->client, atoi(l), atoi(r));
338     changed = 1;
339   }
340   if(changed) {
341     /* redirect back to ourselves (but without the volume-changing bits in the
342      * URL) */
343     cgi_header(output->sink, "Location",
344                (back = cgi_get("back")) ? back : volume_url());
345     header_cookie(output->sink);
346     cgi_body(output->sink);
347   } else {
348     cgi_header(output->sink, "Content-Type", "text/html");
349     header_cookie(output->sink);
350     cgi_body(output->sink);
351     expand(output, "volume", ds);
352   }
353 }
354
355 static void act_prefs_errors(const char *msg,
356                              void attribute((unused)) *u) {
357   fatal(0, "error splitting parts list: %s", msg);
358 }
359
360 static const char *numbered_arg(const char *argname, int numfile) {
361   char *fullname;
362
363   byte_xasprintf(&fullname, "%d_%s", numfile, argname);
364   return cgi_get(fullname);
365 }
366
367 static void process_prefs(dcgi_state *ds, int numfile) {
368   const char *file, *name, *value, *part, *parts, *current, *context;
369   char **partslist;
370
371   if(!(file = numbered_arg("file", numfile)))
372     /* The first file doesn't need numbering. */
373     if(numfile > 0 || !(file = cgi_get("file")))
374       return;
375   if((parts = numbered_arg("parts", numfile))
376      || (parts = cgi_get("parts"))) {
377     /* Default context is display.  Other contexts not actually tested. */
378     if(!(context = numbered_arg("context", numfile))) context = "display";
379     partslist = split(parts, 0, 0, act_prefs_errors, 0);
380     while((part = *partslist++)) {
381       if(!(value = numbered_arg(part, numfile)))
382         continue;
383       /* If it's already right (whether regexps or db) don't change anything,
384        * so we don't fill the database up with rubbish. */
385       if(disorder_part(ds->g->client, (char **)&current,
386                        file, context, part))
387         fatal(0, "disorder_part() failed");
388       if(!strcmp(current, value))
389         continue;
390       byte_xasprintf((char **)&name, "trackname_%s_%s", context, part);
391       disorder_set(ds->g->client, file, name, value);
392     }
393     if((value = numbered_arg("random", numfile)))
394       disorder_unset(ds->g->client, file, "pick_at_random");
395     else
396       disorder_set(ds->g->client, file, "pick_at_random", "0");
397     if((value = numbered_arg("tags", numfile)))
398       disorder_set(ds->g->client, file, "tags", value);
399   } else if((name = cgi_get("name"))) {
400     /* Raw preferences.  Not well supported in the templates at the moment. */
401     value = cgi_get("value");
402     if(value)
403       disorder_set(ds->g->client, file, name, value);
404     else
405       disorder_unset(ds->g->client, file, name);
406   }
407 }
408
409 static void act_prefs(cgi_sink *output, dcgi_state *ds) {
410   const char *files;
411   int nfiles, numfile;
412
413   if((files = cgi_get("files"))) nfiles = atoi(files);
414   else nfiles = 1;
415   for(numfile = 0; numfile < nfiles; ++numfile)
416     process_prefs(ds, numfile);
417   cgi_header(output->sink, "Content-Type", "text/html");
418   header_cookie(output->sink);
419   cgi_body(output->sink);
420   expand(output, "prefs", ds);
421 }
422
423 static void act_pause(cgi_sink *output,
424                       dcgi_state *ds) {
425   if(ds->g->client)
426     disorder_pause(ds->g->client);
427   redirect(output->sink);
428 }
429
430 static void act_resume(cgi_sink *output,
431                        dcgi_state *ds) {
432   if(ds->g->client)
433     disorder_resume(ds->g->client);
434   redirect(output->sink);
435 }
436
437 static void act_login(cgi_sink *output,
438                       dcgi_state *ds) {
439   const char *username, *password, *back;
440   disorder_client *c;
441
442   username = cgi_get("username");
443   password = cgi_get("password");
444   if(!username || !password
445      || !strcmp(username, "guest")/*bodge to avoid guest cookies*/) {
446     /* We're just visiting the login page */
447     expand_template(ds, output, "login");
448     return;
449   }
450   c = disorder_new(1);
451   if(disorder_connect_user(c, username, password)) {
452     cgi_set_option("error", "loginfailed");
453     expand_template(ds, output, "login");
454     return;
455   }
456   if(disorder_make_cookie(c, &login_cookie)) {
457     cgi_set_option("error", "cookiefailed");
458     expand_template(ds, output, "login");
459     return;
460   }
461   /* We have a new cookie */
462   header_cookie(output->sink);
463   if((back = cgi_get("back")) && back)
464     /* Redirect back to somewhere or other */
465     redirect(output->sink);
466   else
467     /* Stick to the login page */
468     expand_template(ds, output, "login");
469 }
470
471 static void act_logout(cgi_sink *output,
472                        dcgi_state *ds) {
473   disorder_revoke(ds->g->client);
474   login_cookie = 0;
475   /* Reconnect as guest */
476   ds->g->client = disorder_new(0);
477   if(disorder_connect_cookie(ds->g->client, 0)) {
478     disorder_cgi_error(output, ds, "connect");
479     exit(0);
480   }
481   /* Back to the login page */
482   expand_template(ds, output, "login");
483 }
484
485 static void act_register(cgi_sink *output,
486                          dcgi_state *ds) {
487   const char *username, *password, *email;
488   char *confirm;
489
490   username = cgi_get("username");
491   password = cgi_get("password");
492   email = cgi_get("email");
493
494   if(!username || !*username) {
495     cgi_set_option("error", "nousername");
496     expand_template(ds, output, "login");
497     return;
498   }
499   if(!password || !*password) {
500     cgi_set_option("error", "nopassword");
501     expand_template(ds, output, "login");
502     return;
503   }
504   if(!email || !*email) {
505     cgi_set_option("error", "noemail");
506     expand_template(ds, output, "login");
507     return;
508   }
509   /* We could well do better address validation but for now we'll just do the
510    * minimum */
511   if(!strchr(email, '@')) {
512     cgi_set_option("error", "bademail");
513     expand_template(ds, output, "login");
514     return;
515   }
516   if(disorder_register(ds->g->client, username, password, email, &confirm)) {
517     cgi_set_option("error", "cannotregister");
518     expand_template(ds, output, "login");
519     return;
520   }
521   /* We'll go back to the login page with a suitable message */
522   cgi_set_option("registered", "registeredok");
523   expand_template(ds, output, "login");
524 }
525
526 static const struct action {
527   const char *name;
528   void (*handler)(cgi_sink *output, dcgi_state *ds);
529 } actions[] = {
530   { "disable", act_disable },
531   { "enable", act_enable },
532   { "login", act_login },
533   { "logout", act_logout },
534   { "move", act_move },
535   { "pause", act_pause },
536   { "play", act_play },
537   { "playing", act_playing },
538   { "prefs", act_prefs },
539   { "random-disable", act_random_disable },
540   { "random-enable", act_random_enable },
541   { "register", act_register },
542   { "remove", act_remove },
543   { "resume", act_resume },
544   { "scratch", act_scratch },
545   { "volume", act_volume },
546 };
547
548 /* expansions *****************************************************************/
549
550 static void exp_include(int attribute((unused)) nargs,
551                         char **args,
552                         cgi_sink *output,
553                         void *u) {
554   expand(output, args[0], u);
555 }
556
557 static void exp_server_version(int attribute((unused)) nargs,
558                                char attribute((unused)) **args,
559                                cgi_sink *output,
560                                void *u) {
561   dcgi_state *ds = u;
562   const char *v;
563
564   if(ds->g->client) {
565     if(disorder_version(ds->g->client, (char **)&v)) v = "(cannot get version)";
566   } else
567     v = "(server not running)";
568   cgi_output(output, "%s", v);
569 }
570
571 static void exp_version(int attribute((unused)) nargs,
572                         char attribute((unused)) **args,
573                         cgi_sink *output,
574                         void attribute((unused)) *u) {
575   cgi_output(output, "%s", disorder_short_version_string);
576 }
577
578 static void exp_nonce(int attribute((unused)) nargs,
579                       char attribute((unused)) **args,
580                       cgi_sink *output,
581                       void attribute((unused)) *u) {
582   cgi_output(output, "%s", nonce());
583 }
584
585 static void exp_label(int attribute((unused)) nargs,
586                       char **args,
587                       cgi_sink *output,
588                       void attribute((unused)) *u) {
589   cgi_output(output, "%s", cgi_label(args[0]));
590 }
591
592 struct trackinfo_state {
593   dcgi_state *ds;
594   const struct queue_entry *q;
595   long length;
596   time_t when;
597 };
598
599 static void exp_who(int attribute((unused)) nargs,
600                     char attribute((unused)) **args,
601                     cgi_sink *output,
602                     void *u) {
603   dcgi_state *ds = u;
604   
605   if(ds->track && ds->track->submitter)
606     cgi_output(output, "%s", ds->track->submitter);
607 }
608
609 static void exp_length(int attribute((unused)) nargs,
610                        char attribute((unused)) **args,
611                        cgi_sink *output,
612                        void *u) {
613   dcgi_state *ds = u;
614   long length = 0;
615
616   if(ds->track
617      && (ds->track->state == playing_started
618          || ds->track->state == playing_paused)
619      && ds->track->sofar >= 0)
620     cgi_output(output, "%ld:%02ld/",
621                ds->track->sofar / 60, ds->track->sofar % 60);
622   length = 0;
623   if(ds->track)
624     disorder_length(ds->g->client, ds->track->track, &length);
625   else if(ds->tracks)
626     disorder_length(ds->g->client, ds->tracks[0], &length);
627   if(length)
628     cgi_output(output, "%ld:%02ld", length / 60, length % 60);
629   else
630     sink_printf(output->sink, "%s", "&nbsp;");
631 }
632
633 static void exp_when(int attribute((unused)) nargs,
634                      char attribute((unused)) **args,
635                      cgi_sink *output,
636                      void *u) {
637   dcgi_state *ds = u;
638   const struct tm *w = 0;
639
640   if(ds->track)
641     switch(ds->track->state) {
642     case playing_isscratch:
643     case playing_unplayed:
644     case playing_random:
645       if(ds->track->expected)
646         w = localtime(&ds->track->expected);
647       break;
648     case playing_failed:
649     case playing_no_player:
650     case playing_ok:
651     case playing_scratched:
652     case playing_started:
653     case playing_paused:
654     case playing_quitting:
655       if(ds->track->played)
656         w = localtime(&ds->track->played);
657       break;
658     }
659   if(w)
660     cgi_output(output, "%d:%02d", w->tm_hour, w->tm_min);
661   else
662     sink_printf(output->sink, "&nbsp;");
663 }
664
665 static void exp_part(int nargs,
666                      char **args,
667                      cgi_sink *output,
668                      void *u) {
669   dcgi_state *ds = u;
670   const char *s, *track, *part, *context;
671
672   if(nargs == 3)
673     track = args[2];
674   else {
675     if(ds->track)
676       track = ds->track->track;
677     else if(ds->tracks)
678       track = ds->tracks[0];
679     else
680       track = 0;
681   }
682   if(track) {
683     switch(nargs) {
684     case 1:
685       context = "display";
686       part = args[0];
687       break;
688     case 2:
689     case 3:
690       context = args[0];
691       part = args[1];
692       break;
693     default:
694       abort();
695     }
696     if(disorder_part(ds->g->client, (char **)&s, track,
697                      !strcmp(context, "short") ? "display" : context, part))
698       fatal(0, "disorder_part() failed");
699     if(!strcmp(context, "short"))
700       s = truncate_for_display(s, config->short_display);
701     cgi_output(output, "%s", s);
702   } else
703     sink_printf(output->sink, "&nbsp;");
704 }
705
706 static void exp_playing(int attribute((unused)) nargs,
707                         char **args,
708                         cgi_sink *output,
709                         void  *u) {
710   dcgi_state *ds = u;
711   dcgi_state s;
712
713   lookups(ds, DC_PLAYING);
714   memset(&s, 0, sizeof s);
715   s.g = ds->g;
716   if(ds->g->playing) {
717     s.track = ds->g->playing;
718     expandstring(output, args[0], &s);
719   }
720 }
721
722 static void exp_queue(int attribute((unused)) nargs,
723                       char **args,
724                       cgi_sink *output,
725                       void  *u) {
726   dcgi_state *ds = u;
727   dcgi_state s;
728   struct queue_entry *q;
729
730   lookups(ds, DC_QUEUE);
731   memset(&s, 0, sizeof s);
732   s.g = ds->g;
733   s.first = 1;
734   for(q = ds->g->queue; q; q = q->next) {
735     s.last = !q->next;
736     s.track = q;
737     expandstring(output, args[0], &s);
738     s.index++;
739     s.first = 0;
740   }
741 }
742
743 static void exp_recent(int attribute((unused)) nargs,
744                        char **args,
745                        cgi_sink *output,
746                        void  *u) {
747   dcgi_state *ds = u;
748   dcgi_state s;
749   struct queue_entry *q;
750
751   lookups(ds, DC_RECENT);
752   memset(&s, 0, sizeof s);
753   s.g = ds->g;
754   s.first = 1;
755   for(q = ds->g->recent; q; q = q->next) {
756     s.last = !q;
757     s.track = q;
758     expandstring(output, args[0], &s);
759     s.index++;
760     s.first = 0;
761   }
762 }
763
764 static void exp_new(int attribute((unused)) nargs,
765                     char **args,
766                     cgi_sink *output,
767                     void  *u) {
768   dcgi_state *ds = u;
769   dcgi_state s;
770
771   lookups(ds, DC_NEW);
772   memset(&s, 0, sizeof s);
773   s.g = ds->g;
774   s.first = 1;
775   for(s.index = 0; s.index < ds->g->nnew; ++s.index) {
776     s.last = s.index + 1 < ds->g->nnew;
777     s.tracks = &ds->g->new[s.index];
778     expandstring(output, args[0], &s);
779     s.first = 0;
780   }
781 }
782
783 static void exp_url(int attribute((unused)) nargs,
784                     char attribute((unused)) **args,
785                     cgi_sink *output,
786                     void attribute((unused)) *u) {
787   cgi_output(output, "%s", config->url);
788 }
789
790 struct result {
791   char *track;
792   const char *sort;
793 };
794
795 static int compare_result(const void *a, const void *b) {
796   const struct result *ra = a, *rb = b;
797   int c;
798
799   if(!(c = strcmp(ra->sort, rb->sort)))
800     c = strcmp(ra->track, rb->track);
801   return c;
802 }
803
804 static void exp_search(int nargs,
805                        char **args,
806                        cgi_sink *output,
807                        void *u) {
808   dcgi_state *ds = u, substate;
809   char **tracks;
810   const char *q, *context, *part, *template;
811   int ntracks, n, m;
812   struct result *r;
813
814   switch(nargs) {
815   case 2:
816     part = args[0];
817     context = "sort";
818     template = args[1];
819     break;
820   case 3:
821     part = args[0];
822     context = args[1];
823     template = args[2];
824     break;
825   default:
826     assert(!"should never happen");
827     part = context = template = 0;      /* quieten compiler */
828   }
829   if(ds->tracks == 0) {
830     /* we are the top level, let's get some search results */
831     if(!(q = cgi_get("query"))) return; /* no results yet */
832     if(disorder_search(ds->g->client, q, &tracks, &ntracks)) return;
833     if(!ntracks) return;
834   } else {
835     tracks = ds->tracks;
836     ntracks = ds->ntracks;
837   }
838   assert(ntracks != 0);
839   /* sort tracks by the appropriate part */
840   r = xmalloc(ntracks * sizeof *r);
841   for(n = 0; n < ntracks; ++n) {
842     r[n].track = tracks[n];
843     if(disorder_part(ds->g->client, (char **)&r[n].sort,
844                      tracks[n], context, part))
845       fatal(0, "disorder_part() failed");
846   }
847   qsort(r, ntracks, sizeof (struct result), compare_result);
848   /* expand the 2nd arg once for each group.  We re-use the passed-in tracks
849    * array as we know it's guaranteed to be big enough and isn't going to be
850    * used for anything else any more. */
851   memset(&substate, 0, sizeof substate);
852   substate.g = ds->g;
853   substate.first = 1;
854   n = 0;
855   while(n < ntracks) {
856     substate.tracks = tracks;
857     substate.ntracks = 0;
858     m = n;
859     while(m < ntracks
860           && !strcmp(r[m].sort, r[n].sort))
861       tracks[substate.ntracks++] = r[m++].track;
862     substate.last = (m == ntracks);
863     expandstring(output, template, &substate);
864     substate.index++;
865     substate.first = 0;
866     n = m;
867   }
868   assert(substate.last != 0);
869 }
870
871 static void exp_arg(int attribute((unused)) nargs,
872                     char **args,
873                     cgi_sink *output,
874                     void attribute((unused)) *u) {
875   const char *v;
876
877   if((v = cgi_get(args[0])))
878     cgi_output(output, "%s", v);
879 }
880
881 static void exp_stats(int attribute((unused)) nargs,
882                       char attribute((unused)) **args,
883                       cgi_sink *output,
884                       void *u) {
885   dcgi_state *ds = u;
886   char **v;
887
888   cgi_opentag(output->sink, "pre", "class", "stats", (char *)0);
889   if(!disorder_stats(ds->g->client, &v, 0)) {
890     while(*v)
891       cgi_output(output, "%s\n", *v++);
892   }
893   cgi_closetag(output->sink, "pre");
894 }
895
896 static void exp_volume(int attribute((unused)) nargs,
897                        char **args,
898                        cgi_sink *output,
899                        void *u) {
900   dcgi_state *ds = u;
901
902   lookups(ds, DC_VOLUME);
903   if(!strcmp(args[0], "left"))
904     cgi_output(output, "%d", ds->g->volume_left);
905   else
906     cgi_output(output, "%d", ds->g->volume_right);
907 }
908
909 static void exp_shell(int attribute((unused)) nargs,
910                       char **args,
911                       cgi_sink *output,
912                       void attribute((unused)) *u) {
913   int w, p[2], n;
914   char buffer[4096];
915   pid_t pid;
916   
917   xpipe(p);
918   if(!(pid = xfork())) {
919     exitfn = _exit;
920     xclose(p[0]);
921     xdup2(p[1], 1);
922     xclose(p[1]);
923     execlp("sh", "sh", "-c", args[0], (char *)0);
924     fatal(errno, "error executing sh");
925   }
926   xclose(p[1]);
927   while((n = read(p[0], buffer, sizeof buffer))) {
928     if(n < 0) {
929       if(errno == EINTR) continue;
930       else fatal(errno, "error reading from pipe");
931     }
932     output->sink->write(output->sink, buffer, n);
933   }
934   xclose(p[0]);
935   while((n = waitpid(pid, &w, 0)) < 0 && errno == EINTR)
936     ;
937   if(n < 0) fatal(errno, "error calling waitpid");
938   if(w)
939     error(0, "shell command '%s' %s", args[0], wstat(w));
940 }
941
942 static inline int str2bool(const char *s) {
943   return !strcmp(s, "true");
944 }
945
946 static inline const char *bool2str(int n) {
947   return n ? "true" : "false";
948 }
949
950 static char *expandarg(const char *arg, dcgi_state *ds) {
951   struct dynstr d;
952   cgi_sink output;
953
954   dynstr_init(&d);
955   output.quote = 0;
956   output.sink = sink_dynstr(&d);
957   expandstring(&output, arg, ds);
958   dynstr_terminate(&d);
959   return d.vec;
960 }
961
962 static void exp_prefs(int attribute((unused)) nargs,
963                       char **args,
964                       cgi_sink *output,
965                       void *u) {
966   dcgi_state *ds = u;
967   dcgi_state substate;
968   struct kvp *k;
969   const char *file = expandarg(args[0], ds);
970   
971   memset(&substate, 0, sizeof substate);
972   substate.g = ds->g;
973   substate.first = 1;
974   if(disorder_prefs(ds->g->client, file, &k)) return;
975   while(k) {
976     substate.last = !k->next;
977     substate.pref = k;
978     expandstring(output, args[1], &substate);
979     ++substate.index;
980     k = k->next;
981     substate.first = 0;
982   }
983 }
984
985 static void exp_pref(int attribute((unused)) nargs,
986                      char **args,
987                      cgi_sink *output,
988                      void *u) {
989   char *value;
990   dcgi_state *ds = u;
991
992   if(!disorder_get(ds->g->client, args[0], args[1], &value))
993     cgi_output(output, "%s", value);
994 }
995
996 static void exp_if(int nargs,
997                    char **args,
998                    cgi_sink *output,
999                    void *u) {
1000   dcgi_state *ds = u;
1001   int n = str2bool(expandarg(args[0], ds)) ? 1 : 2;
1002   
1003   if(n < nargs)
1004     expandstring(output, args[n], ds);
1005 }
1006
1007 static void exp_and(int nargs,
1008                     char **args,
1009                     cgi_sink *output,
1010                     void *u) {
1011   dcgi_state *ds = u;
1012   int n, result = 1;
1013
1014   for(n = 0; n < nargs; ++n)
1015     if(!str2bool(expandarg(args[n], ds))) {
1016       result = 0;
1017       break;
1018     }
1019   sink_printf(output->sink, "%s", bool2str(result));
1020 }
1021
1022 static void exp_or(int nargs,
1023                    char **args,
1024                    cgi_sink *output,
1025                    void *u) {
1026   dcgi_state *ds = u;
1027   int n, result = 0;
1028
1029   for(n = 0; n < nargs; ++n)
1030     if(str2bool(expandarg(args[n], ds))) {
1031       result = 1;
1032       break;
1033     }
1034   sink_printf(output->sink, "%s", bool2str(result));
1035 }
1036
1037 static void exp_not(int attribute((unused)) nargs,
1038                     char **args,
1039                     cgi_sink *output,
1040                     void attribute((unused)) *u) {
1041   sink_printf(output->sink, "%s", bool2str(!str2bool(args[0])));
1042 }
1043
1044 static void exp_isplaying(int attribute((unused)) nargs,
1045                           char attribute((unused)) **args,
1046                           cgi_sink *output,
1047                           void *u) {
1048   dcgi_state *ds = u;
1049
1050   lookups(ds, DC_PLAYING);
1051   sink_printf(output->sink, "%s", bool2str(!!ds->g->playing));
1052 }
1053
1054 static void exp_isqueue(int attribute((unused)) nargs,
1055                         char attribute((unused)) **args,
1056                         cgi_sink *output,
1057                         void *u) {
1058   dcgi_state *ds = u;
1059
1060   lookups(ds, DC_QUEUE);
1061   sink_printf(output->sink, "%s", bool2str(!!ds->g->queue));
1062 }
1063
1064 static void exp_isrecent(int attribute((unused)) nargs,
1065                          char attribute((unused)) **args,
1066                          cgi_sink *output,
1067                          void *u) {
1068   dcgi_state *ds = u;
1069
1070   lookups(ds, DC_RECENT);
1071   sink_printf(output->sink, "%s", bool2str(!!ds->g->recent));
1072 }
1073
1074 static void exp_isnew(int attribute((unused)) nargs,
1075                       char attribute((unused)) **args,
1076                       cgi_sink *output,
1077                       void *u) {
1078   dcgi_state *ds = u;
1079
1080   lookups(ds, DC_NEW);
1081   sink_printf(output->sink, "%s", bool2str(!!ds->g->nnew));
1082 }
1083
1084 static void exp_id(int attribute((unused)) nargs,
1085                    char attribute((unused)) **args,
1086                    cgi_sink *output,
1087                    void *u) {
1088   dcgi_state *ds = u;
1089
1090   if(ds->track)
1091     cgi_output(output, "%s", ds->track->id);
1092 }
1093
1094 static void exp_track(int attribute((unused)) nargs,
1095                       char attribute((unused)) **args,
1096                       cgi_sink *output,
1097                       void *u) {
1098   dcgi_state *ds = u;
1099
1100   if(ds->track)
1101     cgi_output(output, "%s", ds->track->track);
1102 }
1103
1104 static void exp_parity(int attribute((unused)) nargs,
1105                        char attribute((unused)) **args,
1106                        cgi_sink *output,
1107                        void *u) {
1108   dcgi_state *ds = u;
1109
1110   cgi_output(output, "%s", ds->index % 2 ? "odd" : "even");
1111 }
1112
1113 static void exp_comment(int attribute((unused)) nargs,
1114                         char attribute((unused)) **args,
1115                         cgi_sink attribute((unused)) *output,
1116                         void attribute((unused)) *u) {
1117   /* do nothing */
1118 }
1119
1120 static void exp_prefname(int attribute((unused)) nargs,
1121                          char attribute((unused)) **args,
1122                          cgi_sink *output,
1123                          void *u) {
1124   dcgi_state *ds = u;
1125
1126   if(ds->pref && ds->pref->name)
1127     cgi_output(output, "%s", ds->pref->name);
1128 }
1129
1130 static void exp_prefvalue(int attribute((unused)) nargs,
1131                           char attribute((unused)) **args,
1132                           cgi_sink *output,
1133                           void *u) {
1134   dcgi_state *ds = u;
1135
1136   if(ds->pref && ds->pref->value)
1137     cgi_output(output, "%s", ds->pref->value);
1138 }
1139
1140 static void exp_isfiles(int attribute((unused)) nargs,
1141                         char attribute((unused)) **args,
1142                         cgi_sink *output,
1143                         void *u) {
1144   dcgi_state *ds = u;
1145
1146   lookups(ds, DC_FILES);
1147   sink_printf(output->sink, "%s", bool2str(!!ds->g->nfiles));
1148 }
1149
1150 static void exp_isdirectories(int attribute((unused)) nargs,
1151                               char attribute((unused)) **args,
1152                               cgi_sink *output,
1153                               void *u) {
1154   dcgi_state *ds = u;
1155
1156   lookups(ds, DC_DIRS);
1157   sink_printf(output->sink, "%s", bool2str(!!ds->g->ndirs));
1158 }
1159
1160 static void exp_choose(int attribute((unused)) nargs,
1161                        char **args,
1162                        cgi_sink *output,
1163                        void *u) {
1164   dcgi_state *ds = u;
1165   dcgi_state substate;
1166   int nfiles, n;
1167   char **files;
1168   struct entry *e;
1169   const char *type, *what = expandarg(args[0], ds);
1170
1171   if(!strcmp(what, "files")) {
1172     lookups(ds, DC_FILES);
1173     files = ds->g->files;
1174     nfiles = ds->g->nfiles;
1175     type = "track";
1176   } else if(!strcmp(what, "directories")) {
1177     lookups(ds, DC_DIRS);
1178     files = ds->g->dirs;
1179     nfiles = ds->g->ndirs;
1180     type = "dir";
1181   } else {
1182     error(0, "unknown @choose@ argument '%s'", what);
1183     return;
1184   }
1185   e = xmalloc(nfiles * sizeof (struct entry));
1186   for(n = 0; n < nfiles; ++n) {
1187     e[n].path = files[n];
1188     e[n].sort = trackname_transform(type, files[n], "sort");
1189     e[n].display = trackname_transform(type, files[n], "display");
1190   }
1191   qsort(e, nfiles, sizeof (struct entry), compare_entry);
1192   memset(&substate, 0, sizeof substate);
1193   substate.g = ds->g;
1194   substate.first = 1;
1195   for(n = 0; n < nfiles; ++n) {
1196     substate.last = (n == nfiles - 1);
1197     substate.index = n;
1198     substate.entry = &e[n];
1199     expandstring(output, args[1], &substate);
1200     substate.first = 0;
1201   }
1202 }
1203
1204 static void exp_file(int attribute((unused)) nargs,
1205                      char attribute((unused)) **args,
1206                      cgi_sink *output,
1207                      void *u) {
1208   dcgi_state *ds = u;
1209
1210   if(ds->entry)
1211     cgi_output(output, "%s", ds->entry->path);
1212   else if(ds->track)
1213     cgi_output(output, "%s", ds->track->track);
1214   else if(ds->tracks)
1215     cgi_output(output, "%s", ds->tracks[0]);
1216 }
1217
1218 static void exp_transform(int nargs,
1219                           char **args,
1220                           cgi_sink *output,
1221                           void attribute((unused)) *u) {
1222   const char *context = nargs > 2 ? args[2] : "display";
1223
1224   cgi_output(output, "%s", trackname_transform(args[1], args[0], context));
1225 }
1226
1227 static void exp_urlquote(int attribute((unused)) nargs,
1228                          char **args,
1229                          cgi_sink *output,
1230                          void attribute((unused)) *u) {
1231   cgi_output(output, "%s", urlencodestring(args[0]));
1232 }
1233
1234 static void exp_scratchable(int attribute((unused)) nargs,
1235                             char attribute((unused)) **args,
1236                             cgi_sink *output,
1237                             void attribute((unused)) *u) {
1238   dcgi_state *ds = u;
1239   int result;
1240
1241   if(config->restrictions & RESTRICT_SCRATCH) {
1242     lookups(ds, DC_PLAYING);
1243     result = (ds->g->playing
1244               && (!ds->g->playing->submitter
1245                   || !strcmp(ds->g->playing->submitter,
1246                              disorder_user(ds->g->client))));
1247   } else
1248     result = 1;
1249   sink_printf(output->sink, "%s", bool2str(result));
1250 }
1251
1252 static void exp_removable(int attribute((unused)) nargs,
1253                           char attribute((unused)) **args,
1254                           cgi_sink *output,
1255                           void attribute((unused)) *u) {
1256   dcgi_state *ds = u;
1257   int result;
1258
1259   if(config->restrictions & RESTRICT_REMOVE)
1260     result = (ds->track
1261               && ds->track->submitter
1262               && !strcmp(ds->track->submitter,
1263                          disorder_user(ds->g->client)));
1264   else
1265     result = 1;
1266   sink_printf(output->sink, "%s", bool2str(result));
1267 }
1268
1269 static void exp_navigate(int attribute((unused)) nargs,
1270                          char **args,
1271                          cgi_sink *output,
1272                          void *u) {
1273   dcgi_state *ds = u;
1274   dcgi_state substate;
1275   const char *path = expandarg(args[0], ds);
1276   const char *ptr;
1277   int dirlen;
1278
1279   if(*path) {
1280     memset(&substate, 0, sizeof substate);
1281     substate.g = ds->g;
1282     ptr = path + 1;                     /* skip root */
1283     dirlen = 0;
1284     substate.nav_path = path;
1285     substate.first = 1;
1286     while(*ptr) {
1287       while(*ptr && *ptr != '/')
1288         ++ptr;
1289       substate.last = !*ptr;
1290       substate.nav_len = ptr - path;
1291       substate.nav_dirlen = dirlen;
1292       expandstring(output, args[1], &substate);
1293       dirlen = substate.nav_len;
1294       if(*ptr) ++ptr;
1295       substate.first = 0;
1296     }
1297   }
1298 }
1299
1300 static void exp_fullname(int attribute((unused)) nargs,
1301                          char attribute((unused)) **args,
1302                          cgi_sink *output,
1303                          void *u) {
1304   dcgi_state *ds = u;
1305   cgi_output(output, "%.*s", ds->nav_len, ds->nav_path);
1306 }
1307
1308 static void exp_basename(int nargs,
1309                          char **args,
1310                          cgi_sink *output,
1311                          void *u) {
1312   dcgi_state *ds = u;
1313   const char *s;
1314   
1315   if(nargs) {
1316     if((s = strrchr(args[0], '/'))) ++s;
1317     else s = args[0];
1318     cgi_output(output, "%s", s);
1319   } else
1320     cgi_output(output, "%.*s", ds->nav_len - ds->nav_dirlen - 1,
1321                ds->nav_path + ds->nav_dirlen + 1);
1322 }
1323
1324 static void exp_dirname(int nargs,
1325                         char **args,
1326                         cgi_sink *output,
1327                         void *u) {
1328   dcgi_state *ds = u;
1329   const char *s;
1330   
1331   if(nargs) {
1332     if((s = strrchr(args[0], '/')))
1333       cgi_output(output, "%.*s", (int)(s - args[0]), args[0]);
1334   } else
1335     cgi_output(output, "%.*s", ds->nav_dirlen, ds->nav_path);
1336 }
1337
1338 static void exp_eq(int attribute((unused)) nargs,
1339                    char **args,
1340                    cgi_sink *output,
1341                    void attribute((unused)) *u) {
1342   cgi_output(output, "%s", bool2str(!strcmp(args[0], args[1])));
1343 }
1344
1345 static void exp_ne(int attribute((unused)) nargs,
1346                    char **args,
1347                    cgi_sink *output,
1348                    void attribute((unused)) *u) {
1349   cgi_output(output, "%s", bool2str(strcmp(args[0], args[1])));
1350 }
1351
1352 static void exp_enabled(int attribute((unused)) nargs,
1353                                char attribute((unused)) **args,
1354                                cgi_sink *output,
1355                                void *u) {
1356   dcgi_state *ds = u;
1357   int enabled = 0;
1358
1359   if(ds->g->client)
1360     disorder_enabled(ds->g->client, &enabled);
1361   cgi_output(output, "%s", bool2str(enabled));
1362 }
1363
1364 static void exp_random_enabled(int attribute((unused)) nargs,
1365                                char attribute((unused)) **args,
1366                                cgi_sink *output,
1367                                void *u) {
1368   dcgi_state *ds = u;
1369   int enabled = 0;
1370
1371   if(ds->g->client)
1372     disorder_random_enabled(ds->g->client, &enabled);
1373   cgi_output(output, "%s", bool2str(enabled));
1374 }
1375
1376 static void exp_trackstate(int attribute((unused)) nargs,
1377                            char **args,
1378                            cgi_sink *output,
1379                            void *u) {
1380   dcgi_state *ds = u;
1381   struct queue_entry *q;
1382   char *track;
1383
1384   if(disorder_resolve(ds->g->client, &track, args[0])) return;
1385   lookups(ds, DC_QUEUE|DC_PLAYING);
1386   if(ds->g->playing && !strcmp(ds->g->playing->track, track))
1387     cgi_output(output, "playing");
1388   else {
1389     for(q = ds->g->queue; q && strcmp(q->track, track); q = q->next)
1390       ;
1391     if(q)
1392       cgi_output(output, "queued");
1393   }
1394 }
1395
1396 static void exp_thisurl(int attribute((unused)) nargs,
1397                         char attribute((unused)) **args,
1398                         cgi_sink *output,
1399                         void attribute((unused)) *u) {
1400   kvp_set(&cgi_args, "nonce", nonce()); /* nonces had better differ! */
1401   cgi_output(output, "%s?%s", config->url, kvp_urlencode(cgi_args, 0));
1402 }
1403
1404 static void exp_isfirst(int attribute((unused)) nargs,
1405                         char attribute((unused)) **args,
1406                         cgi_sink *output,
1407                         void *u) {
1408   dcgi_state *ds = u;
1409
1410   sink_printf(output->sink, "%s", bool2str(!!ds->first));
1411 }
1412
1413 static void exp_islast(int attribute((unused)) nargs,
1414                         char attribute((unused)) **args,
1415                         cgi_sink *output,
1416                         void *u) {
1417   dcgi_state *ds = u;
1418
1419   sink_printf(output->sink, "%s", bool2str(!!ds->last));
1420 }
1421
1422 static void exp_action(int attribute((unused)) nargs,
1423                        char attribute((unused)) **args,
1424                        cgi_sink *output,
1425                        void attribute((unused)) *u) {
1426   const char *action = cgi_get("action"), *mgmt;
1427
1428   if(!action) action = "playing";
1429   if(!strcmp(action, "playing")
1430      && (mgmt = cgi_get("mgmt"))
1431      && !strcmp(mgmt, "true"))
1432     action = "manage";
1433   sink_printf(output->sink, "%s", action);
1434 }
1435
1436 static void exp_resolve(int attribute((unused)) nargs,
1437                       char  **args,
1438                       cgi_sink *output,
1439                       void attribute((unused)) *u) {
1440   dcgi_state *ds = u;
1441   char *track;
1442   
1443   if(!disorder_resolve(ds->g->client, &track, args[0]))
1444     sink_printf(output->sink, "%s", track);
1445 }
1446  
1447 static void exp_paused(int attribute((unused)) nargs,
1448                        char attribute((unused)) **args,
1449                        cgi_sink *output,
1450                        void *u) {
1451   dcgi_state *ds = u;
1452   int paused = 0;
1453
1454   lookups(ds, DC_PLAYING);
1455   if(ds->g->playing && ds->g->playing->state == playing_paused)
1456     paused = 1;
1457   cgi_output(output, "%s", bool2str(paused));
1458 }
1459
1460 static void exp_state(int attribute((unused)) nargs,
1461                       char attribute((unused)) **args,
1462                       cgi_sink *output,
1463                       void *u) {
1464   dcgi_state *ds = u;
1465
1466   if(ds->track)
1467     cgi_output(output, "%s", playing_states[ds->track->state]);
1468 }
1469
1470 static void exp_files(int attribute((unused)) nargs,
1471                       char **args,
1472                       cgi_sink *output,
1473                       void *u) {
1474   dcgi_state *ds = u;
1475   dcgi_state substate;
1476   const char *nfiles_arg, *directory;
1477   int nfiles, numfile;
1478   struct kvp *k;
1479
1480   memset(&substate, 0, sizeof substate);
1481   substate.g = ds->g;
1482   if((directory = cgi_get("directory"))) {
1483     /* Prefs for whole directory. */
1484     lookups(ds, DC_FILES);
1485     /* Synthesize args for the file list. */
1486     nfiles = ds->g->nfiles;
1487     for(numfile = 0; numfile < nfiles; ++numfile) {
1488       k = xmalloc(sizeof *k);
1489       byte_xasprintf((char **)&k->name, "%d_file", numfile);
1490       k->value = ds->g->files[numfile];
1491       k->next = cgi_args;
1492       cgi_args = k;
1493     }
1494   } else {
1495     /* Args already present. */
1496     if((nfiles_arg = cgi_get("files"))) nfiles = atoi(nfiles_arg);
1497     else nfiles = 1;
1498   }
1499   for(numfile = 0; numfile < nfiles; ++numfile) {
1500     substate.index = numfile;
1501     expandstring(output, args[0], &substate);
1502   }
1503 }
1504
1505 static void exp_index(int attribute((unused)) nargs,
1506                       char attribute((unused)) **args,
1507                       cgi_sink *output,
1508                       void *u) {
1509   dcgi_state *ds = u;
1510
1511   cgi_output(output, "%d", ds->index);
1512 }
1513
1514 static void exp_nfiles(int attribute((unused)) nargs,
1515                        char attribute((unused)) **args,
1516                        cgi_sink *output,
1517                        void *u) {
1518   dcgi_state *ds = u;
1519   const char *files_arg;
1520
1521   if(cgi_get("directory")) {
1522     lookups(ds, DC_FILES);
1523     cgi_output(output, "%d", ds->g->nfiles);
1524   } else if((files_arg = cgi_get("files")))
1525     cgi_output(output, "%s", files_arg);
1526   else
1527     cgi_output(output, "1");
1528 }
1529
1530 static void exp_user(int attribute((unused)) nargs,
1531                      char attribute((unused)) **args,
1532                      cgi_sink *output,
1533                      void *u) {
1534   dcgi_state *const ds = u;
1535
1536   cgi_output(output, "%s", disorder_user(ds->g->client));
1537 }
1538
1539 static const struct cgi_expansion expansions[] = {
1540   { "#", 0, INT_MAX, EXP_MAGIC, exp_comment },
1541   { "action", 0, 0, 0, exp_action },
1542   { "and", 0, INT_MAX, EXP_MAGIC, exp_and },
1543   { "arg", 1, 1, 0, exp_arg },
1544   { "basename", 0, 1, 0, exp_basename },
1545   { "choose", 2, 2, EXP_MAGIC, exp_choose },
1546   { "dirname", 0, 1, 0, exp_dirname },
1547   { "enabled", 0, 0, 0, exp_enabled },
1548   { "eq", 2, 2, 0, exp_eq },
1549   { "file", 0, 0, 0, exp_file },
1550   { "files", 1, 1, EXP_MAGIC, exp_files },
1551   { "fullname", 0, 0, 0, exp_fullname },
1552   { "id", 0, 0, 0, exp_id },
1553   { "if", 2, 3, EXP_MAGIC, exp_if },
1554   { "include", 1, 1, 0, exp_include },
1555   { "index", 0, 0, 0, exp_index },
1556   { "isdirectories", 0, 0, 0, exp_isdirectories },
1557   { "isfiles", 0, 0, 0, exp_isfiles },
1558   { "isfirst", 0, 0, 0, exp_isfirst },
1559   { "islast", 0, 0, 0, exp_islast },
1560   { "isnew", 0, 0, 0, exp_isnew },
1561   { "isplaying", 0, 0, 0, exp_isplaying },
1562   { "isqueue", 0, 0, 0, exp_isqueue },
1563   { "isrecent", 0, 0, 0, exp_isrecent },
1564   { "label", 1, 1, 0, exp_label },
1565   { "length", 0, 0, 0, exp_length },
1566   { "navigate", 2, 2, EXP_MAGIC, exp_navigate },
1567   { "ne", 2, 2, 0, exp_ne },
1568   { "new", 1, 1, EXP_MAGIC, exp_new },
1569   { "nfiles", 0, 0, 0, exp_nfiles },
1570   { "nonce", 0, 0, 0, exp_nonce },
1571   { "not", 1, 1, 0, exp_not },
1572   { "or", 0, INT_MAX, EXP_MAGIC, exp_or },
1573   { "parity", 0, 0, 0, exp_parity },
1574   { "part", 1, 3, 0, exp_part },
1575   { "paused", 0, 0, 0, exp_paused },
1576   { "playing", 1, 1, EXP_MAGIC, exp_playing },
1577   { "pref", 2, 2, 0, exp_pref },
1578   { "prefname", 0, 0, 0, exp_prefname },
1579   { "prefs", 2, 2, EXP_MAGIC, exp_prefs },
1580   { "prefvalue", 0, 0, 0, exp_prefvalue },
1581   { "queue", 1, 1, EXP_MAGIC, exp_queue },
1582   { "random-enabled", 0, 0, 0, exp_random_enabled },
1583   { "recent", 1, 1, EXP_MAGIC, exp_recent },
1584   { "removable", 0, 0, 0, exp_removable },
1585   { "resolve", 1, 1, 0, exp_resolve },
1586   { "scratchable", 0, 0, 0, exp_scratchable },
1587   { "search", 2, 3, EXP_MAGIC, exp_search },
1588   { "server-version", 0, 0, 0, exp_server_version },
1589   { "shell", 1, 1, 0, exp_shell },
1590   { "state", 0, 0, 0, exp_state },
1591   { "stats", 0, 0, 0, exp_stats },
1592   { "thisurl", 0, 0, 0, exp_thisurl },
1593   { "track", 0, 0, 0, exp_track },
1594   { "trackstate", 1, 1, 0, exp_trackstate },
1595   { "transform", 2, 3, 0, exp_transform },
1596   { "url", 0, 0, 0, exp_url },
1597   { "urlquote", 1, 1, 0, exp_urlquote },
1598   { "user", 0, 0, 0, exp_user },
1599   { "version", 0, 0, 0, exp_version },
1600   { "volume", 1, 1, 0, exp_volume },
1601   { "when", 0, 0, 0, exp_when },
1602   { "who", 0, 0, 0, exp_who }
1603 };
1604
1605 static void expand(cgi_sink *output,
1606                    const char *template,
1607                    dcgi_state *ds) {
1608   cgi_expand(template,
1609              expansions, sizeof expansions / sizeof *expansions,
1610              output,
1611              ds);
1612 }
1613
1614 static void expandstring(cgi_sink *output,
1615                          const char *string,
1616                          dcgi_state *ds) {
1617   cgi_expand_string("",
1618                     string,
1619                     expansions, sizeof expansions / sizeof *expansions,
1620                     output,
1621                     ds);
1622 }
1623
1624 static void perform_action(cgi_sink *output, dcgi_state *ds,
1625                            const char *action) {
1626   int n;
1627
1628   /* We don't ever want anything to be cached */
1629   cgi_header(output->sink, "Cache-Control", "no-cache");
1630   if((n = TABLE_FIND(actions, struct action, name, action)) >= 0)
1631     actions[n].handler(output, ds);
1632   else
1633     expand_template(ds, output, action);
1634 }
1635
1636 void disorder_cgi(cgi_sink *output, dcgi_state *ds) {
1637   const char *action = cgi_get("action");
1638
1639   if(!action) action = "playing";
1640   perform_action(output, ds, action);
1641 }
1642
1643 void disorder_cgi_error(cgi_sink *output, dcgi_state *ds,
1644                         const char *msg) {
1645   cgi_set_option("error", msg);
1646   perform_action(output, ds, "error");
1647 }
1648
1649 /*
1650 Local Variables:
1651 c-basic-offset:2
1652 comment-column:40
1653 fill-column:79
1654 End:
1655 */