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