chiark / gitweb /
Start conversion of CGI actions.
[disorder] / server / dcgi.c
1
2 #include <stdio.h>
3 #include <errno.h>
4 #include <sys/types.h>
5 #include <sys/socket.h>
6 #include <stddef.h>
7 #include <stdlib.h>
8 #include <time.h>
9 #include <unistd.h>
10 #include <string.h>
11 #include <sys/wait.h>
12 #include <pcre.h>
13 #include <assert.h>
14
15 #include "client.h"
16 #include "mem.h"
17 #include "vector.h"
18 #include "sink.h"
19 #include "server-cgi.h"
20 #include "log.h"
21 #include "configuration.h"
22 #include "table.h"
23 #include "queue.h"
24 #include "plugin.h"
25 #include "split.h"
26 #include "wstat.h"
27 #include "kvp.h"
28 #include "syscalls.h"
29 #include "printf.h"
30 #include "regsub.h"
31 #include "defs.h"
32 #include "trackname.h"
33 #include "charset.h"
34 #include "dcgi.h"
35 #include "url.h"
36 #include "mime.h"
37 #include "sendmail.h"
38 #include "base64.h"
39
40 struct entry {
41   const char *path;
42   const char *sort;
43   const char *display;
44 };
45
46 static int compare_entry(const void *a, const void *b) {
47   const struct entry *ea = a, *eb = b;
48
49   return compare_tracks(ea->sort, eb->sort,
50                         ea->display, eb->display,
51                         ea->path, eb->path);
52 }
53
54 static const char *front_url(void) {
55   char *url;
56   const char *mgmt;
57
58   /* preserve management interface visibility */
59   if((mgmt = cgi_get("mgmt")) && !strcmp(mgmt, "true")) {
60     byte_xasprintf(&url, "%s?mgmt=true", config->url);
61     return url;
62   }
63   return config->url;
64 }
65
66 static void header_cookie(struct sink *output) {
67   struct dynstr d[1];
68   struct url u;
69
70   memset(&u, 0, sizeof u);
71   dynstr_init(d);
72   parse_url(config->url, &u);
73   if(login_cookie) {
74     dynstr_append_string(d, "disorder=");
75     dynstr_append_string(d, login_cookie);
76   } else {
77     /* Force browser to discard cookie */
78     dynstr_append_string(d, "disorder=none;Max-Age=0");
79   }
80   if(u.path) {
81     /* The default domain matches the request host, so we need not override
82      * that.  But the default path only goes up to the rightmost /, which would
83      * cause the browser to expose the cookie to other CGI programs on the same
84      * web server. */
85     dynstr_append_string(d, ";Version=1;Path=");
86     /* Formally we are supposed to quote the path, since it invariably has a
87      * slash in it.  However Safari does not parse quoted paths correctly, so
88      * this won't work.  Fortunately nothing else seems to care about proper
89      * quoting of paths, so in practice we get with it.  (See also
90      * parse_cookie() where we are liberal about cookie paths on the way back
91      * in.) */
92     dynstr_append_string(d, u.path);
93   }
94   dynstr_terminate(d);
95   cgi_header(output, "Set-Cookie", d->vec);
96 }
97
98 static void redirect(struct sink *output) {
99   const char *back;
100
101   back = cgi_get("back");
102   cgi_header(output, "Location", back && *back ? back : front_url());
103   header_cookie(output);
104   cgi_body(output);
105 }
106
107 static void expand_template(dcgi_state *ds, cgi_sink *output,
108                             const char *action) {
109   cgi_header(output->sink, "Content-Type", "text/html");
110   header_cookie(output->sink);
111   cgi_body(output->sink);
112   expand(output, action, ds);
113 }
114
115 /* actions ********************************************************************/
116
117 static void act_disable(cgi_sink *output,
118                         dcgi_state *ds) {
119   if(ds->g->client)
120     disorder_disable(ds->g->client);
121   redirect(output->sink);
122 }
123
124 static void act_enable(cgi_sink *output,
125                               dcgi_state *ds) {
126   if(ds->g->client)
127     disorder_enable(ds->g->client);
128   redirect(output->sink);
129 }
130
131 static void act_random_disable(cgi_sink *output,
132                                dcgi_state *ds) {
133   if(ds->g->client)
134     disorder_random_disable(ds->g->client);
135   redirect(output->sink);
136 }
137
138 static void act_random_enable(cgi_sink *output,
139                               dcgi_state *ds) {
140   if(ds->g->client)
141     disorder_random_enable(ds->g->client);
142   redirect(output->sink);
143 }
144
145 static void act_remove(cgi_sink *output,
146                        dcgi_state *ds) {
147   const char *id;
148
149   if(!(id = cgi_get("id"))) fatal(0, "missing id argument");
150   if(ds->g->client)
151     disorder_remove(ds->g->client, id);
152   redirect(output->sink);
153 }
154
155 static void act_move(cgi_sink *output,
156                      dcgi_state *ds) {
157   const char *id, *delta;
158
159   if(!(id = cgi_get("id"))) fatal(0, "missing id argument");
160   if(!(delta = cgi_get("delta"))) fatal(0, "missing delta argument");
161   if(ds->g->client)
162     disorder_move(ds->g->client, id, atoi(delta));
163   redirect(output->sink);
164 }
165
166 static void act_scratch(cgi_sink *output,
167                         dcgi_state *ds) {
168   if(ds->g->client)
169     disorder_scratch(ds->g->client, cgi_get("id"));
170   redirect(output->sink);
171 }
172
173 static void act_playing(cgi_sink *output, dcgi_state *ds) {
174   char r[1024];
175   long refresh = config->refresh, length;
176   time_t now, fin;
177   int random_enabled = 0;
178   int enabled = 0;
179
180   lookups(ds, DC_PLAYING|DC_QUEUE);
181   cgi_header(output->sink, "Content-Type", "text/html");
182   disorder_random_enabled(ds->g->client, &random_enabled);
183   disorder_enabled(ds->g->client, &enabled);
184   if(ds->g->playing
185      && ds->g->playing->state == playing_started /* i.e. not paused */
186      && !disorder_length(ds->g->client, ds->g->playing->track, &length)
187      && length
188      && ds->g->playing->sofar >= 0) {
189     /* Try to put the next refresh at the start of the next track. */
190     time(&now);
191     fin = now + length - ds->g->playing->sofar + config->gap;
192     if(now + refresh > fin)
193       refresh = fin - now;
194   }
195   if(ds->g->queue && ds->g->queue->state == playing_isscratch) {
196     /* next track is a scratch, don't leave more than the inter-track gap */
197     if(refresh > config->gap)
198       refresh = config->gap;
199   }
200   if(!ds->g->playing && ((ds->g->queue
201                           && ds->g->queue->state != playing_random)
202                          || random_enabled) && enabled) {
203     /* no track playing but playing is enabled and there is something coming
204      * up, must be in a gap */
205     if(refresh > config->gap)
206       refresh = config->gap;
207   }
208   byte_snprintf(r, sizeof r, "%ld;url=%s", refresh > 0 ? refresh : 1,
209                 front_url());
210   cgi_header(output->sink, "Refresh", r);
211   header_cookie(output->sink);
212   cgi_body(output->sink);
213   expand(output, "playing", ds);
214 }
215
216 static void act_play(cgi_sink *output,
217                      dcgi_state *ds) {
218   const char *track, *dir;
219   char **tracks;
220   int ntracks, n;
221   struct entry *e;
222
223   if((track = cgi_get("file"))) {
224     disorder_play(ds->g->client, track);
225   } else if((dir = cgi_get("directory"))) {
226     if(disorder_files(ds->g->client, dir, 0, &tracks, &ntracks)) ntracks = 0;
227     if(ntracks) {
228       e = xmalloc(ntracks * sizeof (struct entry));
229       for(n = 0; n < ntracks; ++n) {
230         e[n].path = tracks[n];
231         e[n].sort = trackname_transform("track", tracks[n], "sort");
232         e[n].display = trackname_transform("track", tracks[n], "display");
233       }
234       qsort(e, ntracks, sizeof (struct entry), compare_entry);
235       for(n = 0; n < ntracks; ++n)
236         disorder_play(ds->g->client, e[n].path);
237     }
238   }
239   /* XXX error handling */
240   redirect(output->sink);
241 }
242
243 static int clamp(int n, int min, int max) {
244   if(n < min)
245     return min;
246   if(n > max)
247     return max;
248   return n;
249 }
250
251 static const char *volume_url(void) {
252   char *url;
253   
254   byte_xasprintf(&url, "%s?action=volume", config->url);
255   return url;
256 }
257
258 static void act_volume(cgi_sink *output, dcgi_state *ds) {
259   const char *l, *r, *d, *back;
260   int nd, changed = 0;;
261
262   if((d = cgi_get("delta"))) {
263     lookups(ds, DC_VOLUME);
264     nd = clamp(atoi(d), -255, 255);
265     disorder_set_volume(ds->g->client,
266                         clamp(ds->g->volume_left + nd, 0, 255),
267                         clamp(ds->g->volume_right + nd, 0, 255));
268     changed = 1;
269   } else if((l = cgi_get("left")) && (r = cgi_get("right"))) {
270     disorder_set_volume(ds->g->client, atoi(l), atoi(r));
271     changed = 1;
272   }
273   if(changed) {
274     /* redirect back to ourselves (but without the volume-changing bits in the
275      * URL) */
276     cgi_header(output->sink, "Location",
277                (back = cgi_get("back")) ? back : volume_url());
278     header_cookie(output->sink);
279     cgi_body(output->sink);
280   } else {
281     cgi_header(output->sink, "Content-Type", "text/html");
282     header_cookie(output->sink);
283     cgi_body(output->sink);
284     expand(output, "volume", ds);
285   }
286 }
287
288 static void act_prefs_errors(const char *msg,
289                              void attribute((unused)) *u) {
290   fatal(0, "error splitting parts list: %s", msg);
291 }
292
293 static const char *numbered_arg(const char *argname, int numfile) {
294   char *fullname;
295
296   byte_xasprintf(&fullname, "%d_%s", numfile, argname);
297   return cgi_get(fullname);
298 }
299
300 static void process_prefs(dcgi_state *ds, int numfile) {
301   const char *file, *name, *value, *part, *parts, *current, *context;
302   char **partslist;
303
304   if(!(file = numbered_arg("file", numfile)))
305     /* The first file doesn't need numbering. */
306     if(numfile > 0 || !(file = cgi_get("file")))
307       return;
308   if((parts = numbered_arg("parts", numfile))
309      || (parts = cgi_get("parts"))) {
310     /* Default context is display.  Other contexts not actually tested. */
311     if(!(context = numbered_arg("context", numfile))) context = "display";
312     partslist = split(parts, 0, 0, act_prefs_errors, 0);
313     while((part = *partslist++)) {
314       if(!(value = numbered_arg(part, numfile)))
315         continue;
316       /* If it's already right (whether regexps or db) don't change anything,
317        * so we don't fill the database up with rubbish. */
318       if(disorder_part(ds->g->client, (char **)&current,
319                        file, context, part))
320         fatal(0, "disorder_part() failed");
321       if(!strcmp(current, value))
322         continue;
323       byte_xasprintf((char **)&name, "trackname_%s_%s", context, part);
324       disorder_set(ds->g->client, file, name, value);
325     }
326     if((value = numbered_arg("random", numfile)))
327       disorder_unset(ds->g->client, file, "pick_at_random");
328     else
329       disorder_set(ds->g->client, file, "pick_at_random", "0");
330     if((value = numbered_arg("tags", numfile))) {
331       if(!*value)
332         disorder_unset(ds->g->client, file, "tags");
333       else
334         disorder_set(ds->g->client, file, "tags", value);
335     }
336     if((value = numbered_arg("weight", numfile))) {
337       if(!*value || !strcmp(value, "90000"))
338         disorder_unset(ds->g->client, file, "weight");
339       else
340         disorder_set(ds->g->client, file, "weight", value);
341     }
342   } else if((name = cgi_get("name"))) {
343     /* Raw preferences.  Not well supported in the templates at the moment. */
344     value = cgi_get("value");
345     if(value)
346       disorder_set(ds->g->client, file, name, value);
347     else
348       disorder_unset(ds->g->client, file, name);
349   }
350 }
351
352 static void act_prefs(cgi_sink *output, dcgi_state *ds) {
353   const char *files;
354   int nfiles, numfile;
355
356   if((files = cgi_get("files"))) nfiles = atoi(files);
357   else nfiles = 1;
358   for(numfile = 0; numfile < nfiles; ++numfile)
359     process_prefs(ds, numfile);
360   cgi_header(output->sink, "Content-Type", "text/html");
361   header_cookie(output->sink);
362   cgi_body(output->sink);
363   expand(output, "prefs", ds);
364 }
365
366 static void act_pause(cgi_sink *output,
367                       dcgi_state *ds) {
368   if(ds->g->client)
369     disorder_pause(ds->g->client);
370   redirect(output->sink);
371 }
372
373 static void act_resume(cgi_sink *output,
374                        dcgi_state *ds) {
375   if(ds->g->client)
376     disorder_resume(ds->g->client);
377   redirect(output->sink);
378 }
379
380 static void act_login(cgi_sink *output,
381                       dcgi_state *ds) {
382   const char *username, *password, *back;
383   disorder_client *c;
384
385   username = cgi_get("username");
386   password = cgi_get("password");
387   if(!username || !password
388      || !strcmp(username, "guest")/*bodge to avoid guest cookies*/) {
389     /* We're just visiting the login page */
390     expand_template(ds, output, "login");
391     return;
392   }
393   /* We'll need a new connection as we are going to stop being guest */
394   c = disorder_new(0);
395   if(disorder_connect_user(c, username, password)) {
396     cgi_set_option("error", "loginfailed");
397     expand_template(ds, output, "login");
398     return;
399   }
400   if(disorder_make_cookie(c, &login_cookie)) {
401     cgi_set_option("error", "cookiefailed");
402     expand_template(ds, output, "login");
403     return;
404   }
405   /* Use the new connection henceforth */
406   ds->g->client = c;
407   ds->g->flags = 0;
408   /* We have a new cookie */
409   header_cookie(output->sink);
410   cgi_set_option("status", "loginok");
411   if((back = cgi_get("back")) && *back)
412     /* Redirect back to somewhere or other */
413     redirect(output->sink);
414   else
415     /* Stick to the login page */
416     expand_template(ds, output, "login");
417 }
418
419 static void act_logout(cgi_sink *output,
420                        dcgi_state *ds) {
421   disorder_revoke(ds->g->client);
422   login_cookie = 0;
423   /* Reconnect as guest */
424   disorder_cgi_login(ds, output);
425   /* Back to the login page */
426   cgi_set_option("status", "logoutok");
427   expand_template(ds, output, "login");
428 }
429
430 static void act_register(cgi_sink *output,
431                          dcgi_state *ds) {
432   const char *username, *password, *password2, *email;
433   char *confirm, *content_type;
434   const char *text, *encoding, *charset;
435
436   username = cgi_get("username");
437   password = cgi_get("password1");
438   password2 = cgi_get("password2");
439   email = cgi_get("email");
440
441   if(!username || !*username) {
442     cgi_set_option("error", "nousername");
443     expand_template(ds, output, "login");
444     return;
445   }
446   if(!password || !*password) {
447     cgi_set_option("error", "nopassword");
448     expand_template(ds, output, "login");
449     return;
450   }
451   if(!password2 || !*password2 || strcmp(password, password2)) {
452     cgi_set_option("error", "passwordmismatch");
453     expand_template(ds, output, "login");
454     return;
455   }
456   if(!email || !*email) {
457     cgi_set_option("error", "noemail");
458     expand_template(ds, output, "login");
459     return;
460   }
461   /* We could well do better address validation but for now we'll just do the
462    * minimum */
463   if(!strchr(email, '@')) {
464     cgi_set_option("error", "bademail");
465     expand_template(ds, output, "login");
466     return;
467   }
468   if(disorder_register(ds->g->client, username, password, email, &confirm)) {
469     cgi_set_option("error", "cannotregister");
470     expand_template(ds, output, "login");
471     return;
472   }
473   /* Send the user a mail */
474   /* TODO templatize this */
475   byte_xasprintf((char **)&text,
476                  "Welcome to DisOrder.  To active your login, please visit this URL:\n"
477                  "\n"
478                  "%s?c=%s\n", config->url, urlencodestring(confirm));
479   if(!(text = mime_encode_text(text, &charset, &encoding)))
480     fatal(0, "cannot encode email");
481   byte_xasprintf(&content_type, "text/plain;charset=%s",
482                  quote822(charset, 0));
483   sendmail("", config->mail_sender, email, "Welcome to DisOrder",
484            encoding, content_type, text); /* TODO error checking  */
485   /* We'll go back to the login page with a suitable message */
486   cgi_set_option("status", "registered");
487   expand_template(ds, output, "login");
488 }
489
490 static void act_confirm(cgi_sink *output,
491                         dcgi_state *ds) {
492   const char *confirmation;
493
494   if(!(confirmation = cgi_get("c"))) {
495     cgi_set_option("error", "noconfirm");
496     expand_template(ds, output, "login");
497   }
498   /* Confirm our registration */
499   if(disorder_confirm(ds->g->client, confirmation)) {
500     cgi_set_option("error", "badconfirm");
501     expand_template(ds, output, "login");
502   }
503   /* Get a cookie */
504   if(disorder_make_cookie(ds->g->client, &login_cookie)) {
505     cgi_set_option("error", "cookiefailed");
506     expand_template(ds, output, "login");
507     return;
508   }
509   /* Discard any cached data JIC */
510   ds->g->flags = 0;
511   /* We have a new cookie */
512   header_cookie(output->sink);
513   cgi_set_option("status", "confirmed");
514   expand_template(ds, output, "login");
515 }
516
517 static void act_edituser(cgi_sink *output,
518                          dcgi_state *ds) {
519   const char *email = cgi_get("email"), *password = cgi_get("changepassword1");
520   const char *password2 = cgi_get("changepassword2");
521   int newpassword = 0;
522   disorder_client *c;
523
524   if((password && *password) || (password && *password2)) {
525     if(!password || !password2 || strcmp(password, password2)) {
526       cgi_set_option("error", "passwordmismatch");
527       expand_template(ds, output, "login");
528       return;
529     }
530   } else
531     password = password2 = 0;
532   
533   if(email) {
534     if(disorder_edituser(ds->g->client, disorder_user(ds->g->client),
535                          "email", email)) {
536       cgi_set_option("error", "badedit");
537       expand_template(ds, output, "login");
538       return;
539     }
540   }
541   if(password) {
542     if(disorder_edituser(ds->g->client, disorder_user(ds->g->client),
543                          "password", password)) {
544       cgi_set_option("error", "badedit");
545       expand_template(ds, output, "login");
546       return;
547     }
548     newpassword = 1;
549   }
550   if(newpassword) {
551     login_cookie = 0;                   /* it'll be invalid now */
552     /* This is a bit duplicative of act_login() */
553     c = disorder_new(0);
554     if(disorder_connect_user(c, disorder_user(ds->g->client), password)) {
555       cgi_set_option("error", "loginfailed");
556       expand_template(ds, output, "login");
557       return;
558     }
559     if(disorder_make_cookie(c, &login_cookie)) {
560       cgi_set_option("error", "cookiefailed");
561       expand_template(ds, output, "login");
562       return;
563     }
564     /* Use the new connection henceforth */
565     ds->g->client = c;
566     ds->g->flags = 0;
567     /* We have a new cookie */
568     header_cookie(output->sink);
569   }
570   cgi_set_option("status", "edited");
571   expand_template(ds, output, "login");  
572 }
573
574 static void act_reminder(cgi_sink *output,
575                          dcgi_state *ds) {
576   const char *const username = cgi_get("username");
577
578   if(!username || !*username) {
579     cgi_set_option("error", "nousername");
580     expand_template(ds, output, "login");
581     return;
582   }
583   if(disorder_reminder(ds->g->client, username)) {
584     cgi_set_option("error", "reminderfailed");
585     expand_template(ds, output, "login");
586     return;
587   }
588   cgi_set_option("status", "reminded");
589   expand_template(ds, output, "login");  
590 }
591
592 /* expansions *****************************************************************/
593
594 static void exp_label(int attribute((unused)) nargs,
595                       char **args,
596                       cgi_sink *output,
597                       void attribute((unused)) *u) {
598   cgi_output(output, "%s", cgi_label(args[0]));
599 }
600
601 struct trackinfo_state {
602   dcgi_state *ds;
603   const struct queue_entry *q;
604   long length;
605   time_t when;
606 };
607
608 struct result {
609   char *track;
610   const char *sort;
611 };
612
613 static int compare_result(const void *a, const void *b) {
614   const struct result *ra = a, *rb = b;
615   int c;
616
617   if(!(c = strcmp(ra->sort, rb->sort)))
618     c = strcmp(ra->track, rb->track);
619   return c;
620 }
621
622 static void exp_search(int nargs,
623                        char **args,
624                        cgi_sink *output,
625                        void *u) {
626   dcgi_state *ds = u, substate;
627   char **tracks;
628   const char *q, *context, *part, *template;
629   int ntracks, n, m;
630   struct result *r;
631
632   switch(nargs) {
633   case 2:
634     part = args[0];
635     context = "sort";
636     template = args[1];
637     break;
638   case 3:
639     part = args[0];
640     context = args[1];
641     template = args[2];
642     break;
643   default:
644     assert(!"should never happen");
645     part = context = template = 0;      /* quieten compiler */
646   }
647   if(ds->tracks == 0) {
648     /* we are the top level, let's get some search results */
649     if(!(q = cgi_get("query"))) return; /* no results yet */
650     if(disorder_search(ds->g->client, q, &tracks, &ntracks)) return;
651     if(!ntracks) return;
652   } else {
653     tracks = ds->tracks;
654     ntracks = ds->ntracks;
655   }
656   assert(ntracks != 0);
657   /* sort tracks by the appropriate part */
658   r = xmalloc(ntracks * sizeof *r);
659   for(n = 0; n < ntracks; ++n) {
660     r[n].track = tracks[n];
661     if(disorder_part(ds->g->client, (char **)&r[n].sort,
662                      tracks[n], context, part))
663       fatal(0, "disorder_part() failed");
664   }
665   qsort(r, ntracks, sizeof (struct result), compare_result);
666   /* expand the 2nd arg once for each group.  We re-use the passed-in tracks
667    * array as we know it's guaranteed to be big enough and isn't going to be
668    * used for anything else any more. */
669   memset(&substate, 0, sizeof substate);
670   substate.g = ds->g;
671   substate.first = 1;
672   n = 0;
673   while(n < ntracks) {
674     substate.tracks = tracks;
675     substate.ntracks = 0;
676     m = n;
677     while(m < ntracks
678           && !strcmp(r[m].sort, r[n].sort))
679       tracks[substate.ntracks++] = r[m++].track;
680     substate.last = (m == ntracks);
681     expandstring(output, template, &substate);
682     substate.index++;
683     substate.first = 0;
684     n = m;
685   }
686   assert(substate.last != 0);
687 }
688
689 static void exp_stats(int attribute((unused)) nargs,
690                       char attribute((unused)) **args,
691                       cgi_sink *output,
692                       void *u) {
693   dcgi_state *ds = u;
694   char **v;
695
696   cgi_opentag(output->sink, "pre", "class", "stats", (char *)0);
697   if(!disorder_stats(ds->g->client, &v, 0)) {
698     while(*v)
699       cgi_output(output, "%s\n", *v++);
700   }
701   cgi_closetag(output->sink, "pre");
702 }
703
704 static char *expandarg(const char *arg, dcgi_state *ds) {
705   struct dynstr d;
706   cgi_sink output;
707
708   dynstr_init(&d);
709   output.quote = 0;
710   output.sink = sink_dynstr(&d);
711   expandstring(&output, arg, ds);
712   dynstr_terminate(&d);
713   return d.vec;
714 }
715
716 static void exp_isfiles(int attribute((unused)) nargs,
717                         char attribute((unused)) **args,
718                         cgi_sink *output,
719                         void *u) {
720   dcgi_state *ds = u;
721
722   lookups(ds, DC_FILES);
723   sink_printf(output->sink, "%s", bool2str(!!ds->g->nfiles));
724 }
725
726 static void exp_isdirectories(int attribute((unused)) nargs,
727                               char attribute((unused)) **args,
728                               cgi_sink *output,
729                               void *u) {
730   dcgi_state *ds = u;
731
732   lookups(ds, DC_DIRS);
733   sink_printf(output->sink, "%s", bool2str(!!ds->g->ndirs));
734 }
735
736 static void exp_choose(int attribute((unused)) nargs,
737                        char **args,
738                        cgi_sink *output,
739                        void *u) {
740   dcgi_state *ds = u;
741   dcgi_state substate;
742   int nfiles, n;
743   char **files;
744   struct entry *e;
745   const char *type, *what = expandarg(args[0], ds);
746
747   if(!strcmp(what, "files")) {
748     lookups(ds, DC_FILES);
749     files = ds->g->files;
750     nfiles = ds->g->nfiles;
751     type = "track";
752   } else if(!strcmp(what, "directories")) {
753     lookups(ds, DC_DIRS);
754     files = ds->g->dirs;
755     nfiles = ds->g->ndirs;
756     type = "dir";
757   } else {
758     error(0, "unknown @choose@ argument '%s'", what);
759     return;
760   }
761   e = xmalloc(nfiles * sizeof (struct entry));
762   for(n = 0; n < nfiles; ++n) {
763     e[n].path = files[n];
764     e[n].sort = trackname_transform(type, files[n], "sort");
765     e[n].display = trackname_transform(type, files[n], "display");
766   }
767   qsort(e, nfiles, sizeof (struct entry), compare_entry);
768   memset(&substate, 0, sizeof substate);
769   substate.g = ds->g;
770   substate.first = 1;
771   for(n = 0; n < nfiles; ++n) {
772     substate.last = (n == nfiles - 1);
773     substate.index = n;
774     substate.entry = &e[n];
775     expandstring(output, args[1], &substate);
776     substate.first = 0;
777   }
778 }
779
780 static void exp_file(int attribute((unused)) nargs,
781                      char attribute((unused)) **args,
782                      cgi_sink *output,
783                      void *u) {
784   dcgi_state *ds = u;
785
786   if(ds->entry)
787     cgi_output(output, "%s", ds->entry->path);
788   else if(ds->track)
789     cgi_output(output, "%s", ds->track->track);
790   else if(ds->tracks)
791     cgi_output(output, "%s", ds->tracks[0]);
792 }
793
794 static void exp_navigate(int attribute((unused)) nargs,
795                          char **args,
796                          cgi_sink *output,
797                          void *u) {
798   dcgi_state *ds = u;
799   dcgi_state substate;
800   const char *path = expandarg(args[0], ds);
801   const char *ptr;
802   int dirlen;
803
804   if(*path) {
805     memset(&substate, 0, sizeof substate);
806     substate.g = ds->g;
807     ptr = path + 1;                     /* skip root */
808     dirlen = 0;
809     substate.nav_path = path;
810     substate.first = 1;
811     while(*ptr) {
812       while(*ptr && *ptr != '/')
813         ++ptr;
814       substate.last = !*ptr;
815       substate.nav_len = ptr - path;
816       substate.nav_dirlen = dirlen;
817       expandstring(output, args[1], &substate);
818       dirlen = substate.nav_len;
819       if(*ptr) ++ptr;
820       substate.first = 0;
821     }
822   }
823 }
824
825 static void exp_fullname(int attribute((unused)) nargs,
826                          char attribute((unused)) **args,
827                          cgi_sink *output,
828                          void *u) {
829   dcgi_state *ds = u;
830   cgi_output(output, "%.*s", ds->nav_len, ds->nav_path);
831 }
832
833 static void exp_basename(int nargs,
834                          char **args,
835                          cgi_sink *output,
836                          void *u) {
837   dcgi_state *ds = u;
838   const char *s;
839   
840   if(nargs) {
841     if((s = strrchr(args[0], '/'))) ++s;
842     else s = args[0];
843     cgi_output(output, "%s", s);
844   } else
845     cgi_output(output, "%.*s", ds->nav_len - ds->nav_dirlen - 1,
846                ds->nav_path + ds->nav_dirlen + 1);
847 }
848
849 static void exp_dirname(int nargs,
850                         char **args,
851                         cgi_sink *output,
852                         void *u) {
853   dcgi_state *ds = u;
854   const char *s;
855   
856   if(nargs) {
857     if((s = strrchr(args[0], '/')))
858       cgi_output(output, "%.*s", (int)(s - args[0]), args[0]);
859   } else
860     cgi_output(output, "%.*s", ds->nav_dirlen, ds->nav_path);
861 }
862
863 static void exp_files(int attribute((unused)) nargs,
864                       char **args,
865                       cgi_sink *output,
866                       void *u) {
867   dcgi_state *ds = u;
868   dcgi_state substate;
869   const char *nfiles_arg, *directory;
870   int nfiles, numfile;
871   struct kvp *k;
872
873   memset(&substate, 0, sizeof substate);
874   substate.g = ds->g;
875   if((directory = cgi_get("directory"))) {
876     /* Prefs for whole directory. */
877     lookups(ds, DC_FILES);
878     /* Synthesize args for the file list. */
879     nfiles = ds->g->nfiles;
880     for(numfile = 0; numfile < nfiles; ++numfile) {
881       k = xmalloc(sizeof *k);
882       byte_xasprintf((char **)&k->name, "%d_file", numfile);
883       k->value = ds->g->files[numfile];
884       k->next = cgi_args;
885       cgi_args = k;
886     }
887   } else {
888     /* Args already present. */
889     if((nfiles_arg = cgi_get("files"))) nfiles = atoi(nfiles_arg);
890     else nfiles = 1;
891   }
892   for(numfile = 0; numfile < nfiles; ++numfile) {
893     substate.index = numfile;
894     expandstring(output, args[0], &substate);
895   }
896 }
897
898 static void exp_nfiles(int attribute((unused)) nargs,
899                        char attribute((unused)) **args,
900                        cgi_sink *output,
901                        void *u) {
902   dcgi_state *ds = u;
903   const char *files_arg;
904
905   if(cgi_get("directory")) {
906     lookups(ds, DC_FILES);
907     cgi_output(output, "%d", ds->g->nfiles);
908   } else if((files_arg = cgi_get("files")))
909     cgi_output(output, "%s", files_arg);
910   else
911     cgi_output(output, "1");
912 }
913
914 static void exp_image(int attribute((unused)) nargs,
915                       char **args,
916                       cgi_sink *output,
917                       void attribute((unused)) *u) {
918   char *labelname;
919   const char *imagestem;
920
921   byte_xasprintf(&labelname, "images.%s", args[0]);
922   if(cgi_label_exists(labelname))
923     imagestem = cgi_label(labelname);
924   else if(strchr(args[0], '.'))
925     imagestem = args[0];
926   else
927     byte_xasprintf((char **)&imagestem, "%s.png", args[0]);
928   if(cgi_label_exists("url.static"))
929     cgi_output(output, "%s/%s", cgi_label("url.static"), imagestem);
930   else
931     cgi_output(output, "/disorder/%s", imagestem);
932 }
933
934 /*
935 Local Variables:
936 c-basic-offset:2
937 comment-column:40
938 fill-column:79
939 End:
940 */