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 */