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