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