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