chiark / gitweb /
Merge disorder.macros branch.
authorRichard Kettlewell <rjk@greenend.org.uk>
Sun, 18 May 2008 21:29:17 +0000 (22:29 +0100)
committerRichard Kettlewell <rjk@greenend.org.uk>
Sun, 18 May 2008 21:29:17 +0000 (22:29 +0100)
This is a major rewrite of the web interface.  The template language
has been changed and is hopefuly easier to use.  Much of the
implementation has moved to lib/, along with some of the CGI support.
The CGI can now figure out its own URL, including HTTPS URLs.

The web interface documentation is no longer mixed into
disorder_config(5).  The top level is disorder.cgi(8) but there are
several related pages, much of the content generated from source code
comments.

The server now unsets track preferences if you try to set them to
their default value.  This resolves a long-standing TODO.  The server
is otherwise largely unchanged.

This changes fixes defects 2, 12 and 18 (the first and last of these
being the payoff for casual users).

1  2 
lib/trackdb.c
server/cgi.c
server/dcgi.c
server/dcgi.h

diff --cc lib/trackdb.c
Simple merge
diff --cc server/cgi.c
index 2017d1b,5f9e17f..0000000
deleted file mode 100644,100644
+++ /dev/null
@@@ -1,633 -1,71 +1,0 @@@
--/*
-- * This file is part of DisOrder.
-- * Copyright (C) 2004-2008 Richard Kettlewell
-- *
-- * This program is free software; you can redistribute it and/or modify
-- * it under the terms of the GNU General Public License as published by
-- * the Free Software Foundation; either version 2 of the License, or
-- * (at your option) any later version.
-- *
-- * This program is distributed in the hope that it will be useful, but
-- * WITHOUT ANY WARRANTY; without even the implied warranty of
-- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
-- * General Public License for more details.
-- *
-- * You should have received a copy of the GNU General Public License
-- * along with this program; if not, write to the Free Software
-- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
-- * USA
-- */
--
--#include <config.h>
--#include "types.h"
--
--#include <string.h>
--#include <stdio.h>
--#include <unistd.h>
--#include <stdlib.h>
--#include <errno.h>
--#include <sys/stat.h>
--#include <stddef.h>
--#include <fcntl.h>
--#include <unistd.h>
--#include <pcre.h>
--#include <limits.h>
--#include <fnmatch.h>
--#include <ctype.h>
--
--#include "mem.h"
--#include "log.h"
--#include "hex.h"
--#include "charset.h"
--#include "configuration.h"
--#include "table.h"
--#include "syscalls.h"
--#include "kvp.h"
--#include "vector.h"
--#include "split.h"
--#include "inputline.h"
--#include "regsub.h"
--#include "defs.h"
--#include "sink.h"
- #include "cgi.h"
 -#include "server-cgi.h"
--#include "printf.h"
--#include "mime.h"
--#include "unicode.h"
- struct kvp *cgi_args;
- /* options */
- struct column {
-   struct column *next;
-   char *name;
-   int ncolumns;
-   char **columns;
- };
- #define RELIST(x) struct re *x, **x##_tail = &x
- static int have_read_options;
- static struct kvp *labels;
- static struct column *columns;
- static void include_options(const char *name);
- static void cgi_parse_get(void) {
-   const char *q;
-   if(!(q = getenv("QUERY_STRING"))) fatal(0, "QUERY_STRING not set");
-   cgi_args = kvp_urldecode(q, strlen(q));
- }
- static void cgi_input(char **ptrp, size_t *np) {
-   const char *cl;
-   char *q;
-   size_t n, m = 0;
-   int r;
-   if(!(cl = getenv("CONTENT_LENGTH"))) fatal(0, "CONTENT_LENGTH not set");
-   n = atol(cl);
-   q = xmalloc_noptr(n + 1);
-   while(m < n) {
-     r = read(0, q + m, n - m);
-     if(r > 0)
-       m += r;
-     else if(r == 0)
-       fatal(0, "unexpected end of file reading request body");
-     else switch(errno) {
-     case EINTR: break;
-     default: fatal(errno, "error reading request body");
-     }
-   }
-   if(memchr(q, 0, n)) fatal(0, "null character in request body");
-   q[n + 1] = 0;
-   *ptrp = q;
-   if(np) *np = n;
- }
- static int cgi_field_callback(const char *name, const char *value,
-                             void *u) {
-   char *disposition, *pname, *pvalue;
-   char **namep = u;
-   if(!strcmp(name, "content-disposition")) {
-     if(mime_rfc2388_content_disposition(value,
-                                       &disposition,
-                                       &pname,
-                                       &pvalue))
-       fatal(0, "error parsing Content-Disposition field");
-     if(!strcmp(disposition, "form-data")
-        && pname
-        && !strcmp(pname, "name")) {
-       if(*namep)
-       fatal(0, "duplicate Content-Disposition field");
-       *namep = pvalue;
-     }
-   }
-   return 0;
- }
- static int cgi_part_callback(const char *s,
-                            void attribute((unused)) *u) {
-   char *name = 0;
-   struct kvp *k;
-   
-   if(!(s = mime_parse(s, cgi_field_callback, &name)))
-     fatal(0, "error parsing part header");
-   if(!name) fatal(0, "no name found");
-   k = xmalloc(sizeof *k);
-   k->next = cgi_args;
-   k->name = name;
-   k->value = s;
-   cgi_args = k;
-   return 0;
- }
- static void cgi_parse_multipart(const char *boundary) {
-   char *q;
-   
-   cgi_input(&q, 0);
-   if(mime_multipart(q, cgi_part_callback, boundary, 0))
-     fatal(0, "invalid multipart object");
- }
- static void cgi_parse_post(void) {
-   const char *ct, *boundary;
-   char *q, *type;
-   size_t n;
-   struct kvp *k;
-   if(!(ct = getenv("CONTENT_TYPE")))
-     ct = "application/x-www-form-urlencoded";
-   if(mime_content_type(ct, &type, &k))
-     fatal(0, "invalid content type '%s'", ct);
-   if(!strcmp(type, "application/x-www-form-urlencoded")) {
-     cgi_input(&q, &n);
-     cgi_args = kvp_urldecode(q, n);
-     return;
-   }
-   if(!strcmp(type, "multipart/form-data")) {
-     if(!(boundary = kvp_get(k, "boundary")))
-       fatal(0, "no boundary parameter found");
-     cgi_parse_multipart(boundary);
-     return;
-   }
-   fatal(0, "unrecognized content type '%s'", type);
- }
- void cgi_parse(void) {
-   const char *p;
-   struct kvp *k;
-   if(!(p = getenv("REQUEST_METHOD"))) fatal(0, "REQUEST_METHOD not set");
-   if(!strcmp(p, "GET"))
-     cgi_parse_get();
-   else if(!strcmp(p, "POST"))
-     cgi_parse_post();
-   else
-     fatal(0, "unknown request method %s", p);
-   for(k = cgi_args; k; k = k->next)
-     if(!utf8_valid(k->name, strlen(k->name))
-        || !utf8_valid(k->value, strlen(k->value)))
-       fatal(0, "invalid UTF-8 sequence in cgi argument");
- }
- const char *cgi_get(const char *name) {
-   return kvp_get(cgi_args, name);
- }
- void cgi_output(cgi_sink *output, const char *fmt, ...) {
-   va_list ap;
-   int n;
-   char *r;
-   va_start(ap, fmt);
-   n = byte_vasprintf(&r, fmt, ap);
-   if(n < 0)
-     fatal(errno, "error calling byte_vasprintf");
-   if(output->quote)
-     r = cgi_sgmlquote(r, 0);
-   output->sink->write(output->sink, r, strlen(r));
-   va_end(ap);
- }
 -#include "hash.h"
--
--void cgi_header(struct sink *output, const char *name, const char *value) {
--  sink_printf(output, "%s: %s\r\n", name, value);
--}
--
--void cgi_body(struct sink *output) {
--  sink_printf(output, "\r\n");
- }
- char *cgi_sgmlquote(const char *s, int raw) {
-   uint32_t *ucs, *p, c;
-   char *b, *bp;
-   int n;
-   if(!raw) {
-     if(!(ucs = utf8_to_utf32(s, strlen(s), 0))) exit(EXIT_FAILURE);
-   } else {
-     ucs = xmalloc_noptr((strlen(s) + 1) * sizeof(uint32_t));
-     for(n = 0; s[n]; ++n)
-       ucs[n] = (unsigned char)s[n];
-     ucs[n] = 0;
-   }
-   n = 1;
-   /* estimate the length we'll need */
-   for(p = ucs; (c = *p); ++p) {
-     switch(c) {
-     default:
-       if(c > 127 || c < 32) {
-       case '"':
-       case '&':
-       case '<':
-       case '>':
-       n += 12;
-       break;
-       } else
-       n++;
-     }
-   }
-   /* format the string */
-   b = bp = xmalloc_noptr(n);
-   for(p = ucs; (c = *p); ++p) {
-     switch(c) {
-     default:
-       if(*p > 127 || *p < 32) {
-       case '"':
-       case '&':
-       case '<':
-       case '>':
-       bp += sprintf(bp, "&#%lu;", (unsigned long)c);
-       break;
-       } else
-       *bp++ = c;
-     }
-   }
-   *bp = 0;
-   return b;
- }
- void cgi_attr(struct sink *output, const char *name, const char *value) {
-   if(!value[strspn(value, "abcdefghijklmnopqrstuvwxyz"
-                  "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
-                  "0123456789")])
-     sink_printf(output, "%s=%s", name, value);
-   else
-     sink_printf(output, "%s=\"%s\"", name, cgi_sgmlquote(value, 0));
- }
- void cgi_opentag(struct sink *output, const char *name, ...) {
-   va_list ap;
-   const char *n, *v;
-    
-   sink_printf(output, "<%s", name);
-   va_start(ap, name);
-   while((n = va_arg(ap, const char *))) {
-     sink_printf(output, " ");
-     v = va_arg(ap, const char *);
-     if(v)
-       cgi_attr(output, n, v);
-     else
-       sink_printf(output, n);
-   }
-   sink_printf(output, ">");
- }
- void cgi_closetag(struct sink *output, const char *name) {
-   sink_printf(output, "</%s>", name);
- }
- static int template_open(const char *name,
-                        const char *ext,
-                        const char **filenamep) {
-   const char *dirs[2];
-   int fd = -1, n;
-   char *fullpath;
-   dirs[0] = pkgconfdir;
-   dirs[1] = pkgdatadir;
-   if(name[0] == '/') {
-     if((fd = open(name, O_RDONLY)) < 0) fatal(0, "cannot open %s", name);
-     *filenamep = name;
-   } else {
-     for(n = 0; n < config->templates.n + (int)(sizeof dirs / sizeof *dirs); ++n) {
-       byte_xasprintf(&fullpath, "%s/%s%s",
-                    n < config->templates.n ? config->templates.s[n]
-                                            : dirs[n - config->templates.n],
-                    name, ext);
-       if((fd = open(fullpath, O_RDONLY)) >= 0) break;
-     }
-     if(fd < 0) error(0, "cannot find %s%s in template path", name, ext);
-     *filenamep = fullpath;
-   }
-   return fd;
- }
- static int valid_template_name(const char *name) {
-   if(strchr(name, '/') || name[0] == '.')
-     return 0;
-   return 1;
- }
- void cgi_expand(const char *template,
-               const struct cgi_expansion *expansions,
-               size_t nexpansions,
-               cgi_sink *output,
-               void *u) {
-   int fd = -1;
-   int n;
-   off_t m;
-   char *b;
-   struct stat sb;
-   if(!valid_template_name(template))
-     fatal(0, "invalid template name '%s'", template);
-   if((fd = template_open(template, ".html", &template)) < 0)
-     exitfn(EXIT_FAILURE);
-   if(fstat(fd, &sb) < 0) fatal(errno, "cannot stat %s", template);
-   m = 0;
-   b = xmalloc_noptr(sb.st_size + 1);
-   while(m < sb.st_size) {
-     n = read(fd, b + m, sb.st_size - m);
-     if(n > 0) m += n;
-     else if(n == 0) fatal(0, "unexpected EOF reading %s", template);
-     else if(errno != EINTR) fatal(errno, "error reading %s", template);
-   }
-   b[sb.st_size] = 0;
-   xclose(fd);
-   cgi_expand_string(template, b, expansions, nexpansions, output, u);
- }
- void cgi_expand_string(const char *name,
-                      const char *template,
-                      const struct cgi_expansion *expansions,
-                      size_t nexpansions,
-                      cgi_sink *output,
-                      void *u) {
-   int braces, n, m, line = 1, sline;
-   char *argname;
-   const char *p;
-   struct vector v;
-   struct dynstr d;
-   cgi_sink parameter_output;
-   
-   while(*template) {
-     if(*template != '@') {
-       p = template;
-       while(*p && *p != '@') {
-       if(*p == '\n') ++line;
-       ++p;
-       }
-       output->sink->write(output->sink, template, p - template);
-       template = p;
-       continue;
-     }
-     vector_init(&v);
-     braces = 0;
-     ++template;
-     sline = line;
-     while(*template != '@') {
-       dynstr_init(&d);
-       if(*template == '{') {
-       /* bracketed arg */
-       ++template;
-       while(*template && (*template != '}' || braces > 0)) {
-         switch(*template) {
-         case '{': ++braces; break;
-         case '}': --braces; break;
-         case '\n': ++line; break;
-         }
-         dynstr_append(&d, *template++);
-       }
-       if(!*template) fatal(0, "%s:%d: unterminated expansion", name, sline);
-       ++template;
-       /* skip whitespace after closing bracket */
-       while(isspace((unsigned char)*template))
-         ++template;
-       } else {
-       /* unbracketed arg */
-       /* leading whitespace is not significant in unquoted args */
-       while(isspace((unsigned char)*template))
-         ++template;
-       while(*template
-             && *template != '@' && *template != '{' && *template != ':') {
-         if(*template == '\n') ++line;
-         dynstr_append(&d, *template++);
-       }
-       if(*template == ':')
-         ++template;
-       if(!*template) fatal(0, "%s:%d: unterminated expansion", name, sline);
-       /* trailing whitespace is not significant in unquoted args */
-       while(d.nvec && (isspace((unsigned char)d.vec[d.nvec - 1])))
-         --d.nvec;
-       }
-       dynstr_terminate(&d);
-       vector_append(&v, d.vec);
-     }
-     ++template;
-     vector_terminate(&v);
-     /* @@ terminates this file */
-     if(v.nvec == 0)
-       break;
-     if((n = table_find(expansions,
-                      offsetof(struct cgi_expansion, name),
-                      sizeof (struct cgi_expansion),
-                      nexpansions,
-                      v.vec[0])) < 0)
-       fatal(0, "%s:%d: unknown expansion '%s'", name, line, v.vec[0]);
-     if(v.nvec - 1 < expansions[n].minargs)
-       fatal(0, "%s:%d: insufficient arguments to @%s@ (min %d, got %d)",
-           name, line, v.vec[0], expansions[n].minargs, v.nvec - 1);
-     if(v.nvec - 1 > expansions[n].maxargs)
-       fatal(0, "%s:%d: too many arguments to @%s@ (max %d, got %d)",
-           name, line, v.vec[0], expansions[n].maxargs, v.nvec - 1);
-     /* for ordinary expansions, recursively expand the arguments */
-     if(!(expansions[n].flags & EXP_MAGIC)) {
-       for(m = 1; m < v.nvec; ++m) {
-       dynstr_init(&d);
-       byte_xasprintf(&argname, "<%s:%d arg #%d>", name, sline, m);
-       parameter_output.quote = 0;
-       parameter_output.sink = sink_dynstr(&d);
-       cgi_expand_string(argname, v.vec[m],
-                         expansions, nexpansions,
-                         &parameter_output, u);
-       dynstr_terminate(&d);
-       v.vec[m] = d.vec;
-       }
-     }
-     expansions[n].handler(v.nvec - 1, v.vec + 1, output, u);
-   }
- }
- char *cgi_makeurl(const char *url, ...) {
-   va_list ap;
-   struct kvp *kvp, *k, **kk = &kvp;
-   struct dynstr d;
-   const char *n, *v;
-   
-   dynstr_init(&d);
-   dynstr_append_string(&d, url);
-   va_start(ap, url);
-   while((n = va_arg(ap, const char *))) {
-     v = va_arg(ap, const char *);
-     *kk = k = xmalloc(sizeof *k);
-     kk = &k->next;
-     k->name = n;
-     k->value = v;
-   }
-   *kk = 0;
-   if(kvp) {
-     dynstr_append(&d, '?');
-     dynstr_append_string(&d, kvp_urlencode(kvp, 0));
-   }
-   dynstr_terminate(&d);
-   return d.vec;
- }
- void cgi_set_option(const char *name, const char *value) {
-   struct kvp *k = xmalloc(sizeof *k);
-   k->next = labels;
-   k->name = name;
-   k->value = value;
-   labels = k;
- }
- static void option_label(int attribute((unused)) nvec,
-                        char **vec) {
-   cgi_set_option(vec[0], vec[1]);
- }
- static void option_include(int attribute((unused)) nvec,
-                          char **vec) {
-   include_options(vec[0]);
- }
- static void option_columns(int nvec,
-                           char **vec) {
-   struct column *c = xmalloc(sizeof *c);
-   
-   c->next = columns;
-   c->name = vec[0];
-   c->ncolumns = nvec - 1;
-   c->columns = &vec[1];
-   columns = c;
- }
- static struct option {
-   const char *name;
-   int minargs, maxargs;
-   void (*handler)(int nvec, char **vec);
- } options[] = {
-   { "columns", 1, INT_MAX, option_columns },
-   { "include", 1, 1, option_include },
-   { "label", 2, 2, option_label },
- };
- struct read_options_state {
-   const char *name;
-   int line;
- };
- static void read_options_error(const char *msg,
-                              void *u) {
-   struct read_options_state *cs = u;
-   
-   error(0, "%s:%d: %s", cs->name, cs->line, msg);
- }
- static void include_options(const char *name) {
-   int n, i;
-   int fd;
-   FILE *fp;
-   char **vec, *buffer;
-   struct read_options_state cs;
-   if((fd = template_open(name, "", &cs.name)) < 0) return;
-   if(!(fp = fdopen(fd, "r"))) fatal(errno, "error calling fdopen");
-   cs.line = 0;
-   while(!inputline(cs.name, fp, &buffer, '\n')) {
-     ++cs.line;
-     if(!(vec = split(buffer, &n, SPLIT_COMMENTS|SPLIT_QUOTES,
-                    read_options_error, &cs)))
-       continue;
-     if(!n) continue;
-     if((i = TABLE_FIND(options, struct option, name, vec[0])) == -1) {
-       error(0, "%s:%d: unknown option '%s'", cs.name, cs.line, vec[0]);
-       continue;
-     }
-     ++vec;
-     --n;
-     if(n < options[i].minargs) {
-       error(0, "%s:%d: too few arguments to '%s'", cs.name, cs.line, vec[-1]);
-       continue;
-     }
-     if(n > options[i].maxargs) {
-       error(0, "%s:%d: too many arguments to '%s'", cs.name, cs.line, vec[-1]);
-       continue;
-     }
-     options[i].handler(n, vec);
-   }
-   fclose(fp);
- }
- static void read_options(void) {
-   if(!have_read_options) {
-     have_read_options = 1;
-     include_options("options");
-   }
- }
- const char *cgi_label(const char *key) {
-   const char *label;
-   read_options();
-   if(!(label = kvp_get(labels, key))) {
-     /* No label found */
-     if(!strncmp(key, "images.", 7)) {
-       static const char *url_static;
-       /* images.X defaults to <url.static>X.png */
-       if(!url_static)
-       url_static = cgi_label("url.static");
-       byte_xasprintf((char **)&label, "%s%s.png", url_static, key + 7);
-     } else if((label = strchr(key, '.')))
-       /* X.Y defaults to Y */
-       ++label;
-     else
-       /* otherwise default to label name */
-       label = key;
-   }
-   return label;
- }
- int cgi_label_exists(const char *key) {
-   read_options();
-   return kvp_get(labels, key) ? 1 : 0;
- }
- char **cgi_columns(const char *name, int *ncolumns) {
-   struct column *c;
-   read_options();
-   for(c = columns; c && strcmp(name, c->name); c = c->next)
-     ;
-   if(c) {
-     if(ncolumns)
-       *ncolumns = c->ncolumns;
-     return c->columns;
-   } else {
-     if(ncolumns)
-       *ncolumns = 0;
-     return 0;
-   }
--}
--
--/*
--Local Variables:
--c-basic-offset:2
--comment-column:40
--End:
--*/
diff --cc server/dcgi.c
index 33630fa,bf6fbe5..0000000
deleted file mode 100644,100644
+++ /dev/null
@@@ -1,1906 -1,178 +1,0 @@@
- /*
-  * This file is part of DisOrder.
-  * Copyright (C) 2004-2008 Richard Kettlewell
-  *
-  * This program is free software; you can redistribute it and/or modify
-  * it under the terms of the GNU General Public License as published by
-  * the Free Software Foundation; either version 2 of the License, or
-  * (at your option) any later version.
-  *
-  * This program is distributed in the hope that it will be useful, but
-  * WITHOUT ANY WARRANTY; without even the implied warranty of
-  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
-  * General Public License for more details.
-  *
-  * You should have received a copy of the GNU General Public License
-  * along with this program; if not, write to the Free Software
-  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
-  * USA
-  */
- #include <config.h>
- #include "types.h"
--
--#include <stdio.h>
--#include <errno.h>
--#include <sys/types.h>
--#include <sys/socket.h>
--#include <stddef.h>
--#include <stdlib.h>
--#include <time.h>
--#include <unistd.h>
--#include <string.h>
--#include <sys/wait.h>
--#include <pcre.h>
--#include <assert.h>
--
--#include "client.h"
--#include "mem.h"
--#include "vector.h"
--#include "sink.h"
- #include "cgi.h"
 -#include "server-cgi.h"
--#include "log.h"
--#include "configuration.h"
--#include "table.h"
--#include "queue.h"
--#include "plugin.h"
--#include "split.h"
--#include "wstat.h"
--#include "kvp.h"
--#include "syscalls.h"
--#include "printf.h"
--#include "regsub.h"
--#include "defs.h"
--#include "trackname.h"
--#include "charset.h"
--#include "dcgi.h"
--#include "url.h"
--#include "mime.h"
--#include "sendmail.h"
--#include "base64.h"
- char *login_cookie;
- static void expand(cgi_sink *output,
-                  const char *template,
-                  dcgi_state *ds);
- static void expandstring(cgi_sink *output,
-                        const char *string,
-                        dcgi_state *ds);
--
--struct entry {
--  const char *path;
--  const char *sort;
--  const char *display;
--};
- static const char nonce_base64_table[] =
-   "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-/*";
- static const char *nonce(void) {
-   static uint32_t count;
-   struct ndata {
-     uint16_t count;
-     uint16_t pid;
-     uint32_t when;
-   } nd;
-   nd.count = count++;
-   nd.pid = (uint32_t)getpid();
-   nd.when = (uint32_t)time(0);
-   return generic_to_base64((void *)&nd, sizeof nd,
-                          nonce_base64_table);
- }
--
--static int compare_entry(const void *a, const void *b) {
--  const struct entry *ea = a, *eb = b;
--
--  return compare_tracks(ea->sort, eb->sort,
--                      ea->display, eb->display,
--                      ea->path, eb->path);
--}
--
--static const char *front_url(void) {
--  char *url;
--  const char *mgmt;
--
--  /* preserve management interface visibility */
--  if((mgmt = cgi_get("mgmt")) && !strcmp(mgmt, "true")) {
--    byte_xasprintf(&url, "%s?mgmt=true", config->url);
--    return url;
--  }
--  return config->url;
- }
- static void header_cookie(struct sink *output) {
-   struct dynstr d[1];
-   struct url u;
-   memset(&u, 0, sizeof u);
-   dynstr_init(d);
-   parse_url(config->url, &u);
-   if(login_cookie) {
-     dynstr_append_string(d, "disorder=");
-     dynstr_append_string(d, login_cookie);
-   } else {
-     /* Force browser to discard cookie */
-     dynstr_append_string(d, "disorder=none;Max-Age=0");
-   }
-   if(u.path) {
-     /* The default domain matches the request host, so we need not override
-      * that.  But the default path only goes up to the rightmost /, which would
-      * cause the browser to expose the cookie to other CGI programs on the same
-      * web server. */
-     dynstr_append_string(d, ";Version=1;Path=");
-     /* Formally we are supposed to quote the path, since it invariably has a
-      * slash in it.  However Safari does not parse quoted paths correctly, so
-      * this won't work.  Fortunately nothing else seems to care about proper
-      * quoting of paths, so in practice we get with it.  (See also
-      * parse_cookie() where we are liberal about cookie paths on the way back
-      * in.) */
-     dynstr_append_string(d, u.path);
-   }
-   dynstr_terminate(d);
-   cgi_header(output, "Set-Cookie", d->vec);
--}
--
--static void redirect(struct sink *output) {
--  const char *back;
--
--  back = cgi_get("back");
--  cgi_header(output, "Location", back && *back ? back : front_url());
--  header_cookie(output);
--  cgi_body(output);
--}
--
--static void expand_template(dcgi_state *ds, cgi_sink *output,
--                          const char *action) {
--  cgi_header(output->sink, "Content-Type", "text/html");
--  header_cookie(output->sink);
--  cgi_body(output->sink);
--  expand(output, action, ds);
- }
- static void lookups(dcgi_state *ds, unsigned want) {
-   unsigned need;
-   struct queue_entry *r, *rnext;
-   const char *dir, *re;
-   char *rights;
-   if(ds->g->client && (need = want ^ (ds->g->flags & want)) != 0) {
-     if(need & DC_QUEUE)
-       disorder_queue(ds->g->client, &ds->g->queue);
-     if(need & DC_PLAYING)
-       disorder_playing(ds->g->client, &ds->g->playing);
-     if(need & DC_NEW)
-       disorder_new_tracks(ds->g->client, &ds->g->new, &ds->g->nnew, 0);
-     if(need & DC_RECENT) {
-       /* we need to reverse the order of the list */
-       disorder_recent(ds->g->client, &r);
-       while(r) {
-       rnext = r->next;
-       r->next = ds->g->recent;
-       ds->g->recent = r;
-       r = rnext;
-       }
-     }
-     if(need & DC_VOLUME)
-       disorder_get_volume(ds->g->client,
-                         &ds->g->volume_left, &ds->g->volume_right);
-     if(need & (DC_FILES|DC_DIRS)) {
-       if(!(dir = cgi_get("directory")))
-       dir = "";
-       re = cgi_get("regexp");
-       if(need & DC_DIRS)
-       if(disorder_directories(ds->g->client, dir, re,
-                               &ds->g->dirs, &ds->g->ndirs))
-         ds->g->ndirs = 0;
-       if(need & DC_FILES)
-       if(disorder_files(ds->g->client, dir, re,
-                         &ds->g->files, &ds->g->nfiles))
-         ds->g->nfiles = 0;
-     }
-     if(need & DC_RIGHTS) {
-       ds->g->rights = RIGHT_READ;     /* fail-safe */
-       if(!disorder_userinfo(ds->g->client, disorder_user(ds->g->client),
-                           "rights", &rights))
-       parse_rights(rights, &ds->g->rights, 1);
-     }
-     ds->g->flags |= need;
-   }
- }
- /* actions ********************************************************************/
- static void act_disable(cgi_sink *output,
-                       dcgi_state *ds) {
-   if(ds->g->client)
-     disorder_disable(ds->g->client);
-   redirect(output->sink);
- }
- static void act_enable(cgi_sink *output,
-                             dcgi_state *ds) {
-   if(ds->g->client)
-     disorder_enable(ds->g->client);
-   redirect(output->sink);
- }
- static void act_random_disable(cgi_sink *output,
-                              dcgi_state *ds) {
-   if(ds->g->client)
-     disorder_random_disable(ds->g->client);
-   redirect(output->sink);
- }
- static void act_random_enable(cgi_sink *output,
-                             dcgi_state *ds) {
-   if(ds->g->client)
-     disorder_random_enable(ds->g->client);
-   redirect(output->sink);
- }
- static void act_remove(cgi_sink *output,
-                      dcgi_state *ds) {
-   const char *id;
-   if(!(id = cgi_get("id"))) fatal(0, "missing id argument");
-   if(ds->g->client)
-     disorder_remove(ds->g->client, id);
-   redirect(output->sink);
- }
- static void act_move(cgi_sink *output,
-                    dcgi_state *ds) {
-   const char *id, *delta;
-   if(!(id = cgi_get("id"))) fatal(0, "missing id argument");
-   if(!(delta = cgi_get("delta"))) fatal(0, "missing delta argument");
-   if(ds->g->client)
-     disorder_move(ds->g->client, id, atoi(delta));
-   redirect(output->sink);
- }
- static void act_scratch(cgi_sink *output,
-                       dcgi_state *ds) {
-   if(ds->g->client)
-     disorder_scratch(ds->g->client, cgi_get("id"));
-   redirect(output->sink);
- }
- static void act_playing(cgi_sink *output, dcgi_state *ds) {
-   char r[1024];
-   long refresh = config->refresh, length;
-   time_t now, fin;
-   int random_enabled = 0;
-   int enabled = 0;
-   lookups(ds, DC_PLAYING|DC_QUEUE);
-   cgi_header(output->sink, "Content-Type", "text/html");
-   disorder_random_enabled(ds->g->client, &random_enabled);
-   disorder_enabled(ds->g->client, &enabled);
-   if(ds->g->playing
-      && ds->g->playing->state == playing_started /* i.e. not paused */
-      && !disorder_length(ds->g->client, ds->g->playing->track, &length)
-      && length
-      && ds->g->playing->sofar >= 0) {
-     /* Try to put the next refresh at the start of the next track. */
-     time(&now);
-     fin = now + length - ds->g->playing->sofar + config->gap;
-     if(now + refresh > fin)
-       refresh = fin - now;
-   }
-   if(ds->g->queue && ds->g->queue->state == playing_isscratch) {
-     /* next track is a scratch, don't leave more than the inter-track gap */
-     if(refresh > config->gap)
-       refresh = config->gap;
-   }
-   if(!ds->g->playing && ((ds->g->queue
-                         && ds->g->queue->state != playing_random)
-                        || random_enabled) && enabled) {
-     /* no track playing but playing is enabled and there is something coming
-      * up, must be in a gap */
-     if(refresh > config->gap)
-       refresh = config->gap;
-   }
-   byte_snprintf(r, sizeof r, "%ld;url=%s", refresh > 0 ? refresh : 1,
-               front_url());
-   cgi_header(output->sink, "Refresh", r);
-   header_cookie(output->sink);
-   cgi_body(output->sink);
-   expand(output, "playing", ds);
- }
- static void act_play(cgi_sink *output,
-                    dcgi_state *ds) {
-   const char *track, *dir;
-   char **tracks;
-   int ntracks, n;
-   struct entry *e;
-   if((track = cgi_get("file"))) {
-     disorder_play(ds->g->client, track);
-   } else if((dir = cgi_get("directory"))) {
-     if(disorder_files(ds->g->client, dir, 0, &tracks, &ntracks)) ntracks = 0;
-     if(ntracks) {
-       e = xmalloc(ntracks * sizeof (struct entry));
-       for(n = 0; n < ntracks; ++n) {
-       e[n].path = tracks[n];
-       e[n].sort = trackname_transform("track", tracks[n], "sort");
-       e[n].display = trackname_transform("track", tracks[n], "display");
-       }
-       qsort(e, ntracks, sizeof (struct entry), compare_entry);
-       for(n = 0; n < ntracks; ++n)
-       disorder_play(ds->g->client, e[n].path);
-     }
-   }
-   /* XXX error handling */
-   redirect(output->sink);
- }
- static int clamp(int n, int min, int max) {
-   if(n < min)
-     return min;
-   if(n > max)
-     return max;
-   return n;
- }
- static const char *volume_url(void) {
-   char *url;
-   
-   byte_xasprintf(&url, "%s?action=volume", config->url);
-   return url;
- }
- static void act_volume(cgi_sink *output, dcgi_state *ds) {
-   const char *l, *r, *d, *back;
-   int nd, changed = 0;;
-   if((d = cgi_get("delta"))) {
-     lookups(ds, DC_VOLUME);
-     nd = clamp(atoi(d), -255, 255);
-     disorder_set_volume(ds->g->client,
-                       clamp(ds->g->volume_left + nd, 0, 255),
-                       clamp(ds->g->volume_right + nd, 0, 255));
-     changed = 1;
-   } else if((l = cgi_get("left")) && (r = cgi_get("right"))) {
-     disorder_set_volume(ds->g->client, atoi(l), atoi(r));
-     changed = 1;
-   }
-   if(changed) {
-     /* redirect back to ourselves (but without the volume-changing bits in the
-      * URL) */
-     cgi_header(output->sink, "Location",
-              (back = cgi_get("back")) ? back : volume_url());
-     header_cookie(output->sink);
-     cgi_body(output->sink);
-   } else {
-     cgi_header(output->sink, "Content-Type", "text/html");
-     header_cookie(output->sink);
-     cgi_body(output->sink);
-     expand(output, "volume", ds);
-   }
- }
- static void act_prefs_errors(const char *msg,
-                            void attribute((unused)) *u) {
-   fatal(0, "error splitting parts list: %s", msg);
- }
- static const char *numbered_arg(const char *argname, int numfile) {
-   char *fullname;
-   byte_xasprintf(&fullname, "%d_%s", numfile, argname);
-   return cgi_get(fullname);
- }
- static void process_prefs(dcgi_state *ds, int numfile) {
-   const char *file, *name, *value, *part, *parts, *current, *context;
-   char **partslist;
-   if(!(file = numbered_arg("file", numfile)))
-     /* The first file doesn't need numbering. */
-     if(numfile > 0 || !(file = cgi_get("file")))
-       return;
-   if((parts = numbered_arg("parts", numfile))
-      || (parts = cgi_get("parts"))) {
-     /* Default context is display.  Other contexts not actually tested. */
-     if(!(context = numbered_arg("context", numfile))) context = "display";
-     partslist = split(parts, 0, 0, act_prefs_errors, 0);
-     while((part = *partslist++)) {
-       if(!(value = numbered_arg(part, numfile)))
-       continue;
-       /* If it's already right (whether regexps or db) don't change anything,
-        * so we don't fill the database up with rubbish. */
-       if(disorder_part(ds->g->client, (char **)&current,
-                      file, context, part))
-       fatal(0, "disorder_part() failed");
-       if(!strcmp(current, value))
-       continue;
-       byte_xasprintf((char **)&name, "trackname_%s_%s", context, part);
-       disorder_set(ds->g->client, file, name, value);
-     }
-     if((value = numbered_arg("random", numfile)))
-       disorder_unset(ds->g->client, file, "pick_at_random");
-     else
-       disorder_set(ds->g->client, file, "pick_at_random", "0");
-     if((value = numbered_arg("tags", numfile))) {
-       if(!*value)
-       disorder_unset(ds->g->client, file, "tags");
-       else
-       disorder_set(ds->g->client, file, "tags", value);
-     }
-     if((value = numbered_arg("weight", numfile))) {
-       if(!*value || !strcmp(value, "90000"))
-       disorder_unset(ds->g->client, file, "weight");
-       else
-       disorder_set(ds->g->client, file, "weight", value);
-     }
-   } else if((name = cgi_get("name"))) {
-     /* Raw preferences.  Not well supported in the templates at the moment. */
-     value = cgi_get("value");
-     if(value)
-       disorder_set(ds->g->client, file, name, value);
-     else
-       disorder_unset(ds->g->client, file, name);
-   }
- }
- static void act_prefs(cgi_sink *output, dcgi_state *ds) {
-   const char *files;
-   int nfiles, numfile;
-   if((files = cgi_get("files"))) nfiles = atoi(files);
-   else nfiles = 1;
-   for(numfile = 0; numfile < nfiles; ++numfile)
-     process_prefs(ds, numfile);
-   cgi_header(output->sink, "Content-Type", "text/html");
-   header_cookie(output->sink);
-   cgi_body(output->sink);
-   expand(output, "prefs", ds);
- }
- static void act_pause(cgi_sink *output,
-                     dcgi_state *ds) {
-   if(ds->g->client)
-     disorder_pause(ds->g->client);
-   redirect(output->sink);
- }
- static void act_resume(cgi_sink *output,
-                      dcgi_state *ds) {
-   if(ds->g->client)
-     disorder_resume(ds->g->client);
-   redirect(output->sink);
- }
- static void act_login(cgi_sink *output,
-                     dcgi_state *ds) {
-   const char *username, *password, *back;
-   disorder_client *c;
-   username = cgi_get("username");
-   password = cgi_get("password");
-   if(!username || !password
-      || !strcmp(username, "guest")/*bodge to avoid guest cookies*/) {
-     /* We're just visiting the login page */
-     expand_template(ds, output, "login");
-     return;
-   }
-   /* We'll need a new connection as we are going to stop being guest */
-   c = disorder_new(0);
-   if(disorder_connect_user(c, username, password)) {
-     cgi_set_option("error", "loginfailed");
-     expand_template(ds, output, "login");
-     return;
-   }
-   if(disorder_make_cookie(c, &login_cookie)) {
-     cgi_set_option("error", "cookiefailed");
-     expand_template(ds, output, "login");
-     return;
-   }
-   /* Use the new connection henceforth */
-   ds->g->client = c;
-   ds->g->flags = 0;
-   /* We have a new cookie */
-   header_cookie(output->sink);
-   cgi_set_option("status", "loginok");
-   if((back = cgi_get("back")) && *back)
-     /* Redirect back to somewhere or other */
-     redirect(output->sink);
-   else
-     /* Stick to the login page */
-     expand_template(ds, output, "login");
- }
- static void act_logout(cgi_sink *output,
-                      dcgi_state *ds) {
-   disorder_revoke(ds->g->client);
-   login_cookie = 0;
-   /* Reconnect as guest */
-   disorder_cgi_login(ds, output);
-   /* Back to the login page */
-   cgi_set_option("status", "logoutok");
-   expand_template(ds, output, "login");
- }
- static void act_register(cgi_sink *output,
-                        dcgi_state *ds) {
-   const char *username, *password, *password2, *email;
-   char *confirm, *content_type;
-   const char *text, *encoding, *charset;
-   username = cgi_get("username");
-   password = cgi_get("password1");
-   password2 = cgi_get("password2");
-   email = cgi_get("email");
-   if(!username || !*username) {
-     cgi_set_option("error", "nousername");
-     expand_template(ds, output, "login");
-     return;
-   }
-   if(!password || !*password) {
-     cgi_set_option("error", "nopassword");
-     expand_template(ds, output, "login");
-     return;
-   }
-   if(!password2 || !*password2 || strcmp(password, password2)) {
-     cgi_set_option("error", "passwordmismatch");
-     expand_template(ds, output, "login");
-     return;
-   }
-   if(!email || !*email) {
-     cgi_set_option("error", "noemail");
-     expand_template(ds, output, "login");
-     return;
-   }
-   /* We could well do better address validation but for now we'll just do the
-    * minimum */
-   if(!strchr(email, '@')) {
-     cgi_set_option("error", "bademail");
-     expand_template(ds, output, "login");
-     return;
-   }
-   if(disorder_register(ds->g->client, username, password, email, &confirm)) {
-     cgi_set_option("error", "cannotregister");
-     expand_template(ds, output, "login");
-     return;
-   }
-   /* Send the user a mail */
-   /* TODO templatize this */
-   byte_xasprintf((char **)&text,
-                "Welcome to DisOrder.  To active your login, please visit this URL:\n"
-                "\n"
-                "%s?c=%s\n", config->url, urlencodestring(confirm));
-   if(!(text = mime_encode_text(text, &charset, &encoding)))
-     fatal(0, "cannot encode email");
-   byte_xasprintf(&content_type, "text/plain;charset=%s",
-                quote822(charset, 0));
-   sendmail("", config->mail_sender, email, "Welcome to DisOrder",
-          encoding, content_type, text); /* TODO error checking  */
-   /* We'll go back to the login page with a suitable message */
-   cgi_set_option("status", "registered");
-   expand_template(ds, output, "login");
- }
- static void act_confirm(cgi_sink *output,
-                       dcgi_state *ds) {
-   const char *confirmation;
-   if(!(confirmation = cgi_get("c"))) {
-     cgi_set_option("error", "noconfirm");
-     expand_template(ds, output, "login");
-   }
-   /* Confirm our registration */
-   if(disorder_confirm(ds->g->client, confirmation)) {
-     cgi_set_option("error", "badconfirm");
-     expand_template(ds, output, "login");
-   }
-   /* Get a cookie */
-   if(disorder_make_cookie(ds->g->client, &login_cookie)) {
-     cgi_set_option("error", "cookiefailed");
-     expand_template(ds, output, "login");
-     return;
-   }
-   /* Discard any cached data JIC */
-   ds->g->flags = 0;
-   /* We have a new cookie */
-   header_cookie(output->sink);
-   cgi_set_option("status", "confirmed");
-   expand_template(ds, output, "login");
- }
- static void act_edituser(cgi_sink *output,
-                        dcgi_state *ds) {
-   const char *email = cgi_get("email"), *password = cgi_get("changepassword1");
-   const char *password2 = cgi_get("changepassword2");
-   int newpassword = 0;
-   disorder_client *c;
-   if((password && *password) || (password && *password2)) {
-     if(!password || !password2 || strcmp(password, password2)) {
-       cgi_set_option("error", "passwordmismatch");
-       expand_template(ds, output, "login");
-       return;
-     }
-   } else
-     password = password2 = 0;
-   
-   if(email) {
-     if(disorder_edituser(ds->g->client, disorder_user(ds->g->client),
-                        "email", email)) {
-       cgi_set_option("error", "badedit");
-       expand_template(ds, output, "login");
-       return;
-     }
-   }
-   if(password) {
-     if(disorder_edituser(ds->g->client, disorder_user(ds->g->client),
-                        "password", password)) {
-       cgi_set_option("error", "badedit");
-       expand_template(ds, output, "login");
-       return;
-     }
-     newpassword = 1;
-   }
-   if(newpassword) {
-     login_cookie = 0;                 /* it'll be invalid now */
-     /* This is a bit duplicative of act_login() */
-     c = disorder_new(0);
-     if(disorder_connect_user(c, disorder_user(ds->g->client), password)) {
-       cgi_set_option("error", "loginfailed");
-       expand_template(ds, output, "login");
-       return;
-     }
-     if(disorder_make_cookie(c, &login_cookie)) {
-       cgi_set_option("error", "cookiefailed");
-       expand_template(ds, output, "login");
-       return;
-     }
-     /* Use the new connection henceforth */
-     ds->g->client = c;
-     ds->g->flags = 0;
-     /* We have a new cookie */
-     header_cookie(output->sink);
-   }
-   cgi_set_option("status", "edited");
-   expand_template(ds, output, "login");  
- }
- static void act_reminder(cgi_sink *output,
-                        dcgi_state *ds) {
-   const char *const username = cgi_get("username");
-   if(!username || !*username) {
-     cgi_set_option("error", "nousername");
-     expand_template(ds, output, "login");
-     return;
-   }
-   if(disorder_reminder(ds->g->client, username)) {
-     cgi_set_option("error", "reminderfailed");
-     expand_template(ds, output, "login");
-     return;
-   }
-   cgi_set_option("status", "reminded");
-   expand_template(ds, output, "login");  
--}
- static const struct action {
-   const char *name;
-   void (*handler)(cgi_sink *output, dcgi_state *ds);
- } actions[] = {
-   { "confirm", act_confirm },
-   { "disable", act_disable },
-   { "edituser", act_edituser },
-   { "enable", act_enable },
-   { "login", act_login },
-   { "logout", act_logout },
-   { "move", act_move },
-   { "pause", act_pause },
-   { "play", act_play },
-   { "playing", act_playing },
-   { "prefs", act_prefs },
-   { "random-disable", act_random_disable },
-   { "random-enable", act_random_enable },
-   { "register", act_register },
-   { "reminder", act_reminder },
-   { "remove", act_remove },
-   { "resume", act_resume },
-   { "scratch", act_scratch },
-   { "volume", act_volume },
- };
--
--/* expansions *****************************************************************/
- static void exp_include(int attribute((unused)) nargs,
-                       char **args,
-                       cgi_sink *output,
-                       void *u) {
-   expand(output, args[0], u);
- }
- static void exp_server_version(int attribute((unused)) nargs,
-                              char attribute((unused)) **args,
-                              cgi_sink *output,
-                              void *u) {
-   dcgi_state *ds = u;
-   const char *v;
-   if(ds->g->client) {
-     if(disorder_version(ds->g->client, (char **)&v)) v = "(cannot get version)";
-   } else
-     v = "(server not running)";
-   cgi_output(output, "%s", v);
- }
- static void exp_version(int attribute((unused)) nargs,
-                       char attribute((unused)) **args,
-                       cgi_sink *output,
-                       void attribute((unused)) *u) {
-   cgi_output(output, "%s", disorder_short_version_string);
- }
- static void exp_nonce(int attribute((unused)) nargs,
-                     char attribute((unused)) **args,
-                     cgi_sink *output,
-                     void attribute((unused)) *u) {
-   cgi_output(output, "%s", nonce());
- }
- static void exp_label(int attribute((unused)) nargs,
-                     char **args,
-                     cgi_sink *output,
-                     void attribute((unused)) *u) {
-   cgi_output(output, "%s", cgi_label(args[0]));
- }
--
--struct trackinfo_state {
--  dcgi_state *ds;
--  const struct queue_entry *q;
--  long length;
--  time_t when;
--};
- static void exp_who(int attribute((unused)) nargs,
-                   char attribute((unused)) **args,
-                   cgi_sink *output,
-                   void *u) {
-   dcgi_state *ds = u;
-   
-   if(ds->track && ds->track->submitter)
-     cgi_output(output, "%s", ds->track->submitter);
- }
- static void exp_length(int attribute((unused)) nargs,
-                      char attribute((unused)) **args,
-                      cgi_sink *output,
-                      void *u) {
-   dcgi_state *ds = u;
-   long length = 0;
-   if(ds->track
-      && (ds->track->state == playing_started
-        || ds->track->state == playing_paused)
-      && ds->track->sofar >= 0)
-     cgi_output(output, "%ld:%02ld/",
-              ds->track->sofar / 60, ds->track->sofar % 60);
-   length = 0;
-   if(ds->track)
-     disorder_length(ds->g->client, ds->track->track, &length);
-   else if(ds->tracks)
-     disorder_length(ds->g->client, ds->tracks[0], &length);
-   if(length)
-     cgi_output(output, "%ld:%02ld", length / 60, length % 60);
-   else
-     sink_printf(output->sink, "%s", "&nbsp;");
- }
- static void exp_when(int attribute((unused)) nargs,
-                    char attribute((unused)) **args,
-                    cgi_sink *output,
-                    void *u) {
-   dcgi_state *ds = u;
-   const struct tm *w = 0;
-   if(ds->track)
-     switch(ds->track->state) {
-     case playing_isscratch:
-     case playing_unplayed:
-     case playing_random:
-       if(ds->track->expected)
-       w = localtime(&ds->track->expected);
-       break;
-     case playing_failed:
-     case playing_no_player:
-     case playing_ok:
-     case playing_scratched:
-     case playing_started:
-     case playing_paused:
-     case playing_quitting:
-       if(ds->track->played)
-       w = localtime(&ds->track->played);
-       break;
-     }
-   if(w)
-     cgi_output(output, "%d:%02d", w->tm_hour, w->tm_min);
-   else
-     sink_printf(output->sink, "&nbsp;");
- }
- static void exp_part(int nargs,
-                    char **args,
-                    cgi_sink *output,
-                    void *u) {
-   dcgi_state *ds = u;
-   const char *s, *track, *part, *context;
-   if(nargs == 3)
-     track = args[2];
-   else {
-     if(ds->track)
-       track = ds->track->track;
-     else if(ds->tracks)
-       track = ds->tracks[0];
-     else
-       track = 0;
-   }
-   if(track) {
-     switch(nargs) {
-     case 1:
-       context = "display";
-       part = args[0];
-       break;
-     case 2:
-     case 3:
-       context = args[0];
-       part = args[1];
-       break;
-     default:
-       abort();
-     }
-     if(disorder_part(ds->g->client, (char **)&s, track,
-                    !strcmp(context, "short") ? "display" : context, part))
-       fatal(0, "disorder_part() failed");
-     if(!strcmp(context, "short"))
-       s = truncate_for_display(s, config->short_display);
-     cgi_output(output, "%s", s);
-   } else
-     sink_printf(output->sink, "&nbsp;");
- }
- static void exp_playing(int attribute((unused)) nargs,
-                       char **args,
-                       cgi_sink *output,
-                       void  *u) {
-   dcgi_state *ds = u;
-   dcgi_state s;
-   lookups(ds, DC_PLAYING);
-   memset(&s, 0, sizeof s);
-   s.g = ds->g;
-   if(ds->g->playing) {
-     s.track = ds->g->playing;
-     expandstring(output, args[0], &s);
-   }
- }
- static void exp_queue(int attribute((unused)) nargs,
-                     char **args,
-                     cgi_sink *output,
-                     void  *u) {
-   dcgi_state *ds = u;
-   dcgi_state s;
-   struct queue_entry *q;
-   lookups(ds, DC_QUEUE);
-   memset(&s, 0, sizeof s);
-   s.g = ds->g;
-   s.first = 1;
-   for(q = ds->g->queue; q; q = q->next) {
-     s.last = !q->next;
-     s.track = q;
-     expandstring(output, args[0], &s);
-     s.index++;
-     s.first = 0;
-   }
- }
- static void exp_recent(int attribute((unused)) nargs,
-                      char **args,
-                      cgi_sink *output,
-                      void  *u) {
-   dcgi_state *ds = u;
-   dcgi_state s;
-   struct queue_entry *q;
-   lookups(ds, DC_RECENT);
-   memset(&s, 0, sizeof s);
-   s.g = ds->g;
-   s.first = 1;
-   for(q = ds->g->recent; q; q = q->next) {
-     s.last = !q;
-     s.track = q;
-     expandstring(output, args[0], &s);
-     s.index++;
-     s.first = 0;
-   }
- }
- static void exp_new(int attribute((unused)) nargs,
-                   char **args,
-                   cgi_sink *output,
-                   void  *u) {
-   dcgi_state *ds = u;
-   dcgi_state s;
-   lookups(ds, DC_NEW);
-   memset(&s, 0, sizeof s);
-   s.g = ds->g;
-   s.first = 1;
-   for(s.index = 0; s.index < ds->g->nnew; ++s.index) {
-     s.last = s.index + 1 < ds->g->nnew;
-     s.tracks = &ds->g->new[s.index];
-     expandstring(output, args[0], &s);
-     s.first = 0;
-   }
- }
- static void exp_url(int attribute((unused)) nargs,
-                   char attribute((unused)) **args,
-                   cgi_sink *output,
-                   void attribute((unused)) *u) {
-   cgi_output(output, "%s", config->url);
- }
--
--struct result {
--  char *track;
--  const char *sort;
--};
--
--static int compare_result(const void *a, const void *b) {
--  const struct result *ra = a, *rb = b;
--  int c;
--
--  if(!(c = strcmp(ra->sort, rb->sort)))
--    c = strcmp(ra->track, rb->track);
--  return c;
- }
- static void exp_search(int nargs,
-                      char **args,
-                      cgi_sink *output,
-                      void *u) {
-   dcgi_state *ds = u, substate;
-   char **tracks;
-   const char *q, *context, *part, *template;
-   int ntracks, n, m;
-   struct result *r;
-   switch(nargs) {
-   case 2:
-     part = args[0];
-     context = "sort";
-     template = args[1];
-     break;
-   case 3:
-     part = args[0];
-     context = args[1];
-     template = args[2];
-     break;
-   default:
-     assert(!"should never happen");
-     part = context = template = 0;    /* quieten compiler */
-   }
-   if(ds->tracks == 0) {
-     /* we are the top level, let's get some search results */
-     if(!(q = cgi_get("query"))) return;       /* no results yet */
-     if(disorder_search(ds->g->client, q, &tracks, &ntracks)) return;
-     if(!ntracks) return;
-   } else {
-     tracks = ds->tracks;
-     ntracks = ds->ntracks;
-   }
-   assert(ntracks != 0);
-   /* sort tracks by the appropriate part */
-   r = xmalloc(ntracks * sizeof *r);
-   for(n = 0; n < ntracks; ++n) {
-     r[n].track = tracks[n];
-     if(disorder_part(ds->g->client, (char **)&r[n].sort,
-                    tracks[n], context, part))
-       fatal(0, "disorder_part() failed");
-   }
-   qsort(r, ntracks, sizeof (struct result), compare_result);
-   /* expand the 2nd arg once for each group.  We re-use the passed-in tracks
-    * array as we know it's guaranteed to be big enough and isn't going to be
-    * used for anything else any more. */
-   memset(&substate, 0, sizeof substate);
-   substate.g = ds->g;
-   substate.first = 1;
-   n = 0;
-   while(n < ntracks) {
-     substate.tracks = tracks;
-     substate.ntracks = 0;
-     m = n;
-     while(m < ntracks
-         && !strcmp(r[m].sort, r[n].sort))
-       tracks[substate.ntracks++] = r[m++].track;
-     substate.last = (m == ntracks);
-     expandstring(output, template, &substate);
-     substate.index++;
-     substate.first = 0;
-     n = m;
-   }
-   assert(substate.last != 0);
- }
- static void exp_arg(int attribute((unused)) nargs,
-                   char **args,
-                   cgi_sink *output,
-                   void attribute((unused)) *u) {
-   const char *v;
-   if((v = cgi_get(args[0])))
-     cgi_output(output, "%s", v);
--}
--
--static void exp_stats(int attribute((unused)) nargs,
--                    char attribute((unused)) **args,
--                    cgi_sink *output,
--                    void *u) {
--  dcgi_state *ds = u;
--  char **v;
--
--  cgi_opentag(output->sink, "pre", "class", "stats", (char *)0);
--  if(!disorder_stats(ds->g->client, &v, 0)) {
--    while(*v)
--      cgi_output(output, "%s\n", *v++);
--  }
--  cgi_closetag(output->sink, "pre");
- }
- static void exp_volume(int attribute((unused)) nargs,
-                      char **args,
-                      cgi_sink *output,
-                      void *u) {
-   dcgi_state *ds = u;
-   lookups(ds, DC_VOLUME);
-   if(!strcmp(args[0], "left"))
-     cgi_output(output, "%d", ds->g->volume_left);
-   else
-     cgi_output(output, "%d", ds->g->volume_right);
- }
- static void exp_shell(int attribute((unused)) nargs,
-                     char **args,
-                     cgi_sink *output,
-                     void attribute((unused)) *u) {
-   int w, p[2], n;
-   char buffer[4096];
-   pid_t pid;
-   
-   xpipe(p);
-   if(!(pid = xfork())) {
-     exitfn = _exit;
-     xclose(p[0]);
-     xdup2(p[1], 1);
-     xclose(p[1]);
-     execlp("sh", "sh", "-c", args[0], (char *)0);
-     fatal(errno, "error executing sh");
-   }
-   xclose(p[1]);
-   while((n = read(p[0], buffer, sizeof buffer))) {
-     if(n < 0) {
-       if(errno == EINTR) continue;
-       else fatal(errno, "error reading from pipe");
-     }
-     output->sink->write(output->sink, buffer, n);
-   }
-   xclose(p[0]);
-   while((n = waitpid(pid, &w, 0)) < 0 && errno == EINTR)
-     ;
-   if(n < 0) fatal(errno, "error calling waitpid");
-   if(w)
-     error(0, "shell command '%s' %s", args[0], wstat(w));
- }
- static inline int str2bool(const char *s) {
-   return !strcmp(s, "true");
- }
- static inline const char *bool2str(int n) {
-   return n ? "true" : "false";
--}
--
--static char *expandarg(const char *arg, dcgi_state *ds) {
--  struct dynstr d;
--  cgi_sink output;
--
--  dynstr_init(&d);
--  output.quote = 0;
--  output.sink = sink_dynstr(&d);
--  expandstring(&output, arg, ds);
--  dynstr_terminate(&d);
--  return d.vec;
- }
- static void exp_prefs(int attribute((unused)) nargs,
-                     char **args,
-                     cgi_sink *output,
-                     void *u) {
-   dcgi_state *ds = u;
-   dcgi_state substate;
-   struct kvp *k;
-   const char *file = expandarg(args[0], ds);
-   
-   memset(&substate, 0, sizeof substate);
-   substate.g = ds->g;
-   substate.first = 1;
-   if(disorder_prefs(ds->g->client, file, &k)) return;
-   while(k) {
-     substate.last = !k->next;
-     substate.pref = k;
-     expandstring(output, args[1], &substate);
-     ++substate.index;
-     k = k->next;
-     substate.first = 0;
-   }
- }
- static void exp_pref(int attribute((unused)) nargs,
-                    char **args,
-                    cgi_sink *output,
-                    void *u) {
-   char *value;
-   dcgi_state *ds = u;
-   if(!disorder_get(ds->g->client, args[0], args[1], &value))
-     cgi_output(output, "%s", value);
- }
- static void exp_if(int nargs,
-                  char **args,
-                  cgi_sink *output,
-                  void *u) {
-   dcgi_state *ds = u;
-   int n = str2bool(expandarg(args[0], ds)) ? 1 : 2;
-   
-   if(n < nargs)
-     expandstring(output, args[n], ds);
- }
- static void exp_and(int nargs,
-                   char **args,
-                   cgi_sink *output,
-                   void *u) {
-   dcgi_state *ds = u;
-   int n, result = 1;
-   for(n = 0; n < nargs; ++n)
-     if(!str2bool(expandarg(args[n], ds))) {
-       result = 0;
-       break;
-     }
-   sink_printf(output->sink, "%s", bool2str(result));
- }
- static void exp_or(int nargs,
-                  char **args,
-                  cgi_sink *output,
-                  void *u) {
-   dcgi_state *ds = u;
-   int n, result = 0;
-   for(n = 0; n < nargs; ++n)
-     if(str2bool(expandarg(args[n], ds))) {
-       result = 1;
-       break;
-     }
-   sink_printf(output->sink, "%s", bool2str(result));
- }
- static void exp_not(int attribute((unused)) nargs,
-                   char **args,
-                   cgi_sink *output,
-                   void attribute((unused)) *u) {
-   sink_printf(output->sink, "%s", bool2str(!str2bool(args[0])));
- }
- static void exp_isplaying(int attribute((unused)) nargs,
-                         char attribute((unused)) **args,
-                         cgi_sink *output,
-                         void *u) {
-   dcgi_state *ds = u;
-   lookups(ds, DC_PLAYING);
-   sink_printf(output->sink, "%s", bool2str(!!ds->g->playing));
- }
- static void exp_isqueue(int attribute((unused)) nargs,
-                       char attribute((unused)) **args,
-                       cgi_sink *output,
-                       void *u) {
-   dcgi_state *ds = u;
-   lookups(ds, DC_QUEUE);
-   sink_printf(output->sink, "%s", bool2str(!!ds->g->queue));
- }
- static void exp_isrecent(int attribute((unused)) nargs,
-                        char attribute((unused)) **args,
-                        cgi_sink *output,
-                        void *u) {
-   dcgi_state *ds = u;
-   lookups(ds, DC_RECENT);
-   sink_printf(output->sink, "%s", bool2str(!!ds->g->recent));
- }
- static void exp_isnew(int attribute((unused)) nargs,
-                     char attribute((unused)) **args,
-                     cgi_sink *output,
-                     void *u) {
-   dcgi_state *ds = u;
-   lookups(ds, DC_NEW);
-   sink_printf(output->sink, "%s", bool2str(!!ds->g->nnew));
- }
- static void exp_id(int attribute((unused)) nargs,
-                  char attribute((unused)) **args,
-                  cgi_sink *output,
-                  void *u) {
-   dcgi_state *ds = u;
-   if(ds->track)
-     cgi_output(output, "%s", ds->track->id);
- }
- static void exp_track(int attribute((unused)) nargs,
-                     char attribute((unused)) **args,
-                     cgi_sink *output,
-                     void *u) {
-   dcgi_state *ds = u;
-   if(ds->track)
-     cgi_output(output, "%s", ds->track->track);
- }
- static void exp_parity(int attribute((unused)) nargs,
-                      char attribute((unused)) **args,
-                      cgi_sink *output,
-                      void *u) {
-   dcgi_state *ds = u;
-   cgi_output(output, "%s", ds->index % 2 ? "odd" : "even");
- }
- static void exp_comment(int attribute((unused)) nargs,
-                       char attribute((unused)) **args,
-                       cgi_sink attribute((unused)) *output,
-                       void attribute((unused)) *u) {
-   /* do nothing */
- }
- static void exp_prefname(int attribute((unused)) nargs,
-                        char attribute((unused)) **args,
-                        cgi_sink *output,
-                        void *u) {
-   dcgi_state *ds = u;
-   if(ds->pref && ds->pref->name)
-     cgi_output(output, "%s", ds->pref->name);
- }
- static void exp_prefvalue(int attribute((unused)) nargs,
-                         char attribute((unused)) **args,
-                         cgi_sink *output,
-                         void *u) {
-   dcgi_state *ds = u;
-   if(ds->pref && ds->pref->value)
-     cgi_output(output, "%s", ds->pref->value);
- }
- static void exp_isfiles(int attribute((unused)) nargs,
-                       char attribute((unused)) **args,
-                       cgi_sink *output,
-                       void *u) {
-   dcgi_state *ds = u;
-   lookups(ds, DC_FILES);
-   sink_printf(output->sink, "%s", bool2str(!!ds->g->nfiles));
- }
- static void exp_isdirectories(int attribute((unused)) nargs,
-                             char attribute((unused)) **args,
-                             cgi_sink *output,
-                             void *u) {
-   dcgi_state *ds = u;
-   lookups(ds, DC_DIRS);
-   sink_printf(output->sink, "%s", bool2str(!!ds->g->ndirs));
- }
- static void exp_choose(int attribute((unused)) nargs,
-                      char **args,
-                      cgi_sink *output,
-                      void *u) {
-   dcgi_state *ds = u;
-   dcgi_state substate;
-   int nfiles, n;
-   char **files;
-   struct entry *e;
-   const char *type, *what = expandarg(args[0], ds);
-   if(!strcmp(what, "files")) {
-     lookups(ds, DC_FILES);
-     files = ds->g->files;
-     nfiles = ds->g->nfiles;
-     type = "track";
-   } else if(!strcmp(what, "directories")) {
-     lookups(ds, DC_DIRS);
-     files = ds->g->dirs;
-     nfiles = ds->g->ndirs;
-     type = "dir";
-   } else {
-     error(0, "unknown @choose@ argument '%s'", what);
-     return;
-   }
-   e = xmalloc(nfiles * sizeof (struct entry));
-   for(n = 0; n < nfiles; ++n) {
-     e[n].path = files[n];
-     e[n].sort = trackname_transform(type, files[n], "sort");
-     e[n].display = trackname_transform(type, files[n], "display");
-   }
-   qsort(e, nfiles, sizeof (struct entry), compare_entry);
-   memset(&substate, 0, sizeof substate);
-   substate.g = ds->g;
-   substate.first = 1;
-   for(n = 0; n < nfiles; ++n) {
-     substate.last = (n == nfiles - 1);
-     substate.index = n;
-     substate.entry = &e[n];
-     expandstring(output, args[1], &substate);
-     substate.first = 0;
-   }
- }
- static void exp_file(int attribute((unused)) nargs,
-                    char attribute((unused)) **args,
-                    cgi_sink *output,
-                    void *u) {
-   dcgi_state *ds = u;
-   if(ds->entry)
-     cgi_output(output, "%s", ds->entry->path);
-   else if(ds->track)
-     cgi_output(output, "%s", ds->track->track);
-   else if(ds->tracks)
-     cgi_output(output, "%s", ds->tracks[0]);
- }
- static void exp_transform(int nargs,
-                         char **args,
-                         cgi_sink *output,
-                         void attribute((unused)) *u) {
-   const char *context = nargs > 2 ? args[2] : "display";
-   cgi_output(output, "%s", trackname_transform(args[1], args[0], context));
- }
- static void exp_urlquote(int attribute((unused)) nargs,
-                        char **args,
-                        cgi_sink *output,
-                        void attribute((unused)) *u) {
-   cgi_output(output, "%s", urlencodestring(args[0]));
- }
- static void exp_scratchable(int attribute((unused)) nargs,
-                           char attribute((unused)) **args,
-                           cgi_sink *output,
-                           void attribute((unused)) *u) {
-   dcgi_state *ds = u;
-   lookups(ds, DC_PLAYING|DC_RIGHTS);
-   sink_printf(output->sink, "%s",
-             bool2str(right_scratchable(ds->g->rights,
-                                        disorder_user(ds->g->client),
-                                        ds->g->playing)));
- }
- static void exp_removable(int attribute((unused)) nargs,
-                         char attribute((unused)) **args,
-                         cgi_sink *output,
-                         void attribute((unused)) *u) {
-   dcgi_state *ds = u;
-   lookups(ds, DC_RIGHTS);
-   sink_printf(output->sink, "%s",
-             bool2str(right_removable(ds->g->rights,
-                                      disorder_user(ds->g->client),
-                                      ds->track)));
- }
- static void exp_movable(int attribute((unused)) nargs,
-                       char attribute((unused)) **args,
-                       cgi_sink *output,
-                       void attribute((unused)) *u) {
-   dcgi_state *ds = u;
-   lookups(ds, DC_RIGHTS);
-   sink_printf(output->sink, "%s",
-             bool2str(right_movable(ds->g->rights,
-                                    disorder_user(ds->g->client),
-                                    ds->track)));
--}
--
--static void exp_navigate(int attribute((unused)) nargs,
--                       char **args,
--                       cgi_sink *output,
--                       void *u) {
--  dcgi_state *ds = u;
--  dcgi_state substate;
--  const char *path = expandarg(args[0], ds);
--  const char *ptr;
--  int dirlen;
--
--  if(*path) {
--    memset(&substate, 0, sizeof substate);
--    substate.g = ds->g;
--    ptr = path + 1;                   /* skip root */
--    dirlen = 0;
--    substate.nav_path = path;
--    substate.first = 1;
--    while(*ptr) {
--      while(*ptr && *ptr != '/')
--      ++ptr;
--      substate.last = !*ptr;
--      substate.nav_len = ptr - path;
--      substate.nav_dirlen = dirlen;
--      expandstring(output, args[1], &substate);
--      dirlen = substate.nav_len;
--      if(*ptr) ++ptr;
--      substate.first = 0;
--    }
--  }
--}
--
--static void exp_fullname(int attribute((unused)) nargs,
--                       char attribute((unused)) **args,
--                       cgi_sink *output,
--                       void *u) {
--  dcgi_state *ds = u;
--  cgi_output(output, "%.*s", ds->nav_len, ds->nav_path);
- }
- static void exp_basename(int nargs,
-                        char **args,
-                        cgi_sink *output,
-                        void *u) {
-   dcgi_state *ds = u;
-   const char *s;
-   
-   if(nargs) {
-     if((s = strrchr(args[0], '/'))) ++s;
-     else s = args[0];
-     cgi_output(output, "%s", s);
-   } else
-     cgi_output(output, "%.*s", ds->nav_len - ds->nav_dirlen - 1,
-              ds->nav_path + ds->nav_dirlen + 1);
- }
- static void exp_dirname(int nargs,
-                       char **args,
-                       cgi_sink *output,
-                       void *u) {
-   dcgi_state *ds = u;
-   const char *s;
-   
-   if(nargs) {
-     if((s = strrchr(args[0], '/')))
-       cgi_output(output, "%.*s", (int)(s - args[0]), args[0]);
-   } else
-     cgi_output(output, "%.*s", ds->nav_dirlen, ds->nav_path);
- }
- static void exp_eq(int attribute((unused)) nargs,
-                  char **args,
-                  cgi_sink *output,
-                  void attribute((unused)) *u) {
-   cgi_output(output, "%s", bool2str(!strcmp(args[0], args[1])));
- }
- static void exp_ne(int attribute((unused)) nargs,
-                  char **args,
-                  cgi_sink *output,
-                  void attribute((unused)) *u) {
-   cgi_output(output, "%s", bool2str(strcmp(args[0], args[1])));
- }
- static void exp_enabled(int attribute((unused)) nargs,
-                              char attribute((unused)) **args,
-                              cgi_sink *output,
-                              void *u) {
-   dcgi_state *ds = u;
-   int enabled = 0;
-   if(ds->g->client)
-     disorder_enabled(ds->g->client, &enabled);
-   cgi_output(output, "%s", bool2str(enabled));
- }
- static void exp_random_enabled(int attribute((unused)) nargs,
-                              char attribute((unused)) **args,
-                              cgi_sink *output,
-                              void *u) {
-   dcgi_state *ds = u;
-   int enabled = 0;
-   if(ds->g->client)
-     disorder_random_enabled(ds->g->client, &enabled);
-   cgi_output(output, "%s", bool2str(enabled));
- }
- static void exp_trackstate(int attribute((unused)) nargs,
-                          char **args,
-                          cgi_sink *output,
-                          void *u) {
-   dcgi_state *ds = u;
-   struct queue_entry *q;
-   char *track;
-   if(disorder_resolve(ds->g->client, &track, args[0])) return;
-   lookups(ds, DC_QUEUE|DC_PLAYING);
-   if(ds->g->playing && !strcmp(ds->g->playing->track, track))
-     cgi_output(output, "playing");
-   else {
-     for(q = ds->g->queue; q && strcmp(q->track, track); q = q->next)
-       ;
-     if(q)
-       cgi_output(output, "queued");
-   }
- }
- static void exp_thisurl(int attribute((unused)) nargs,
-                       char attribute((unused)) **args,
-                       cgi_sink *output,
-                       void attribute((unused)) *u) {
-   kvp_set(&cgi_args, "nonce", nonce());       /* nonces had better differ! */
-   cgi_output(output, "%s?%s", config->url, kvp_urlencode(cgi_args, 0));
- }
- static void exp_isfirst(int attribute((unused)) nargs,
-                       char attribute((unused)) **args,
-                       cgi_sink *output,
-                       void *u) {
-   dcgi_state *ds = u;
-   sink_printf(output->sink, "%s", bool2str(!!ds->first));
- }
- static void exp_islast(int attribute((unused)) nargs,
-                       char attribute((unused)) **args,
-                       cgi_sink *output,
-                       void *u) {
-   dcgi_state *ds = u;
-   sink_printf(output->sink, "%s", bool2str(!!ds->last));
- }
- static void exp_action(int attribute((unused)) nargs,
-                      char attribute((unused)) **args,
-                      cgi_sink *output,
-                      void attribute((unused)) *u) {
-   const char *action = cgi_get("action"), *mgmt;
-   if(!action) action = "playing";
-   if(!strcmp(action, "playing")
-      && (mgmt = cgi_get("mgmt"))
-      && !strcmp(mgmt, "true"))
-     action = "manage";
-   sink_printf(output->sink, "%s", action);
- }
- static void exp_resolve(int attribute((unused)) nargs,
-                       char  **args,
-                       cgi_sink *output,
-                       void attribute((unused)) *u) {
-   dcgi_state *ds = u;
-   char *track;
-   
-   if(!disorder_resolve(ds->g->client, &track, args[0]))
-     sink_printf(output->sink, "%s", track);
- }
-  
- static void exp_paused(int attribute((unused)) nargs,
-                      char attribute((unused)) **args,
-                      cgi_sink *output,
-                      void *u) {
-   dcgi_state *ds = u;
-   int paused = 0;
-   lookups(ds, DC_PLAYING);
-   if(ds->g->playing && ds->g->playing->state == playing_paused)
-     paused = 1;
-   cgi_output(output, "%s", bool2str(paused));
- }
- static void exp_state(int attribute((unused)) nargs,
-                     char attribute((unused)) **args,
-                     cgi_sink *output,
-                     void *u) {
-   dcgi_state *ds = u;
-   if(ds->track)
-     cgi_output(output, "%s", playing_states[ds->track->state]);
- }
- static void exp_files(int attribute((unused)) nargs,
-                     char **args,
-                     cgi_sink *output,
-                     void *u) {
-   dcgi_state *ds = u;
-   dcgi_state substate;
-   const char *nfiles_arg, *directory;
-   int nfiles, numfile;
-   struct kvp *k;
-   memset(&substate, 0, sizeof substate);
-   substate.g = ds->g;
-   if((directory = cgi_get("directory"))) {
-     /* Prefs for whole directory. */
-     lookups(ds, DC_FILES);
-     /* Synthesize args for the file list. */
-     nfiles = ds->g->nfiles;
-     for(numfile = 0; numfile < nfiles; ++numfile) {
-       k = xmalloc(sizeof *k);
-       byte_xasprintf((char **)&k->name, "%d_file", numfile);
-       k->value = ds->g->files[numfile];
-       k->next = cgi_args;
-       cgi_args = k;
-     }
-   } else {
-     /* Args already present. */
-     if((nfiles_arg = cgi_get("files"))) nfiles = atoi(nfiles_arg);
-     else nfiles = 1;
-   }
-   for(numfile = 0; numfile < nfiles; ++numfile) {
-     substate.index = numfile;
-     expandstring(output, args[0], &substate);
-   }
- }
- static void exp_index(int attribute((unused)) nargs,
-                     char attribute((unused)) **args,
-                     cgi_sink *output,
-                     void *u) {
-   dcgi_state *ds = u;
-   cgi_output(output, "%d", ds->index);
- }
- static void exp_nfiles(int attribute((unused)) nargs,
-                      char attribute((unused)) **args,
-                      cgi_sink *output,
-                      void *u) {
-   dcgi_state *ds = u;
-   const char *files_arg;
-   if(cgi_get("directory")) {
-     lookups(ds, DC_FILES);
-     cgi_output(output, "%d", ds->g->nfiles);
-   } else if((files_arg = cgi_get("files")))
-     cgi_output(output, "%s", files_arg);
-   else
-     cgi_output(output, "1");
- }
- static void exp_user(int attribute((unused)) nargs,
-                    char attribute((unused)) **args,
-                    cgi_sink *output,
-                    void *u) {
-   dcgi_state *const ds = u;
-   cgi_output(output, "%s", disorder_user(ds->g->client));
- }
- static void exp_right(int attribute((unused)) nargs,
-                     char **args,
-                     cgi_sink *output,
-                     void *u) {
-   dcgi_state *const ds = u;
-   const char *right = expandarg(args[0], ds);
-   rights_type r;
-   lookups(ds, DC_RIGHTS);
-   if(parse_rights(right, &r, 1/*report*/))
-     r = 0;
-   if(args[1] == 0)
-     cgi_output(output, "%s", bool2str(!!(r & ds->g->rights)));
-   else if(r & ds->g->rights)
-     expandstring(output, args[1], ds);
-   else if(args[2])
-     expandstring(output, args[2], ds);
- }
- static void exp_userinfo(int attribute((unused)) nargs,
-                        char **args,
-                        cgi_sink *output,
-                        void *u) {
-   dcgi_state *const ds = u;
-   const char *value;
-   if(disorder_userinfo(ds->g->client, disorder_user(ds->g->client), args[0],
-                      (char **)&value))
-     value = "";
-   cgi_output(output, "%s", value);
- }
- static void exp_image(int attribute((unused)) nargs,
-                     char **args,
-                     cgi_sink *output,
-                     void attribute((unused)) *u) {
-   char *labelname;
-   const char *imagestem;
-   byte_xasprintf(&labelname, "images.%s", args[0]);
-   if(cgi_label_exists(labelname))
-     imagestem = cgi_label(labelname);
-   else if(strchr(args[0], '.'))
-     imagestem = args[0];
-   else
-     byte_xasprintf((char **)&imagestem, "%s.png", args[0]);
-   if(cgi_label_exists("url.static"))
-     cgi_output(output, "%s/%s", cgi_label("url.static"), imagestem);
-   else
-     cgi_output(output, "/disorder/%s", imagestem);
- }
- static const struct cgi_expansion expansions[] = {
-   { "#", 0, INT_MAX, EXP_MAGIC, exp_comment },
-   { "action", 0, 0, 0, exp_action },
-   { "and", 0, INT_MAX, EXP_MAGIC, exp_and },
-   { "arg", 1, 1, 0, exp_arg },
-   { "basename", 0, 1, 0, exp_basename },
-   { "choose", 2, 2, EXP_MAGIC, exp_choose },
-   { "dirname", 0, 1, 0, exp_dirname },
-   { "enabled", 0, 0, 0, exp_enabled },
-   { "eq", 2, 2, 0, exp_eq },
-   { "file", 0, 0, 0, exp_file },
-   { "files", 1, 1, EXP_MAGIC, exp_files },
-   { "fullname", 0, 0, 0, exp_fullname },
-   { "id", 0, 0, 0, exp_id },
-   { "if", 2, 3, EXP_MAGIC, exp_if },
-   { "image", 1, 1, 0, exp_image },
-   { "include", 1, 1, 0, exp_include },
-   { "index", 0, 0, 0, exp_index },
-   { "isdirectories", 0, 0, 0, exp_isdirectories },
-   { "isfiles", 0, 0, 0, exp_isfiles },
-   { "isfirst", 0, 0, 0, exp_isfirst },
-   { "islast", 0, 0, 0, exp_islast },
-   { "isnew", 0, 0, 0, exp_isnew },
-   { "isplaying", 0, 0, 0, exp_isplaying },
-   { "isqueue", 0, 0, 0, exp_isqueue },
-   { "isrecent", 0, 0, 0, exp_isrecent },
-   { "label", 1, 1, 0, exp_label },
-   { "length", 0, 0, 0, exp_length },
-   { "movable", 0, 0, 0, exp_movable },
-   { "navigate", 2, 2, EXP_MAGIC, exp_navigate },
-   { "ne", 2, 2, 0, exp_ne },
-   { "new", 1, 1, EXP_MAGIC, exp_new },
-   { "nfiles", 0, 0, 0, exp_nfiles },
-   { "nonce", 0, 0, 0, exp_nonce },
-   { "not", 1, 1, 0, exp_not },
-   { "or", 0, INT_MAX, EXP_MAGIC, exp_or },
-   { "parity", 0, 0, 0, exp_parity },
-   { "part", 1, 3, 0, exp_part },
-   { "paused", 0, 0, 0, exp_paused },
-   { "playing", 1, 1, EXP_MAGIC, exp_playing },
-   { "pref", 2, 2, 0, exp_pref },
-   { "prefname", 0, 0, 0, exp_prefname },
-   { "prefs", 2, 2, EXP_MAGIC, exp_prefs },
-   { "prefvalue", 0, 0, 0, exp_prefvalue },
-   { "queue", 1, 1, EXP_MAGIC, exp_queue },
-   { "random-enabled", 0, 0, 0, exp_random_enabled },
-   { "recent", 1, 1, EXP_MAGIC, exp_recent },
-   { "removable", 0, 0, 0, exp_removable },
-   { "resolve", 1, 1, 0, exp_resolve },
-   { "right", 1, 3, EXP_MAGIC, exp_right },
-   { "scratchable", 0, 0, 0, exp_scratchable },
-   { "search", 2, 3, EXP_MAGIC, exp_search },
-   { "server-version", 0, 0, 0, exp_server_version },
-   { "shell", 1, 1, 0, exp_shell },
-   { "state", 0, 0, 0, exp_state },
-   { "stats", 0, 0, 0, exp_stats },
-   { "thisurl", 0, 0, 0, exp_thisurl },
-   { "track", 0, 0, 0, exp_track },
-   { "trackstate", 1, 1, 0, exp_trackstate },
-   { "transform", 2, 3, 0, exp_transform },
-   { "url", 0, 0, 0, exp_url },
-   { "urlquote", 1, 1, 0, exp_urlquote },
-   { "user", 0, 0, 0, exp_user },
-   { "userinfo", 1, 1, 0, exp_userinfo },
-   { "version", 0, 0, 0, exp_version },
-   { "volume", 1, 1, 0, exp_volume },
-   { "when", 0, 0, 0, exp_when },
-   { "who", 0, 0, 0, exp_who }
- };
- static void expand(cgi_sink *output,
-                  const char *template,
-                  dcgi_state *ds) {
-   cgi_expand(template,
-            expansions, sizeof expansions / sizeof *expansions,
-            output,
-            ds);
- }
- static void expandstring(cgi_sink *output,
-                        const char *string,
-                        dcgi_state *ds) {
-   cgi_expand_string("",
-                   string,
-                   expansions, sizeof expansions / sizeof *expansions,
-                   output,
-                   ds);
- }
- static void perform_action(cgi_sink *output, dcgi_state *ds,
-                          const char *action) {
-   int n;
-   /* We don't ever want anything to be cached */
-   cgi_header(output->sink, "Cache-Control", "no-cache");
-   if((n = TABLE_FIND(actions, struct action, name, action)) >= 0)
-     actions[n].handler(output, ds);
-   else
-     expand_template(ds, output, action);
- }
- void disorder_cgi(cgi_sink *output, dcgi_state *ds) {
-   const char *action = cgi_get("action");
-   if(!action) {
-     /* We allow URLs which are just confirm=... in order to keep confirmation
-      * URLs, which are user-facing, as short as possible. */
-     if(cgi_get("c"))
-       action = "confirm";
-     else
-       action = "playing";
-   }
-   perform_action(output, ds, action);
- }
- void disorder_cgi_error(cgi_sink *output, dcgi_state *ds,
-                       const char *msg) {
-   cgi_set_option("error", msg);
-   perform_action(output, ds, "error");
- }
- /** @brief Log in as the current user or guest if none */
- void disorder_cgi_login(dcgi_state *ds, cgi_sink *output) {
-   /* Create a new connection */
-   ds->g->client = disorder_new(0);
-   /* Forget everything we knew */
-   ds->g->flags = 0;
-   /* Reconnect */
-   if(disorder_connect_cookie(ds->g->client, login_cookie)) {
-     disorder_cgi_error(output, ds, "connect");
-     exit(0);
-   }
-   /* If there was a cookie but it went bad, we forget it */
-   if(login_cookie && !strcmp(disorder_user(ds->g->client), "guest"))
-     login_cookie = 0;
--}
--
--/*
--Local Variables:
--c-basic-offset:2
--comment-column:40
--fill-column:79
--End:
--*/
diff --cc server/dcgi.h
index 09e74da,09e74da..0000000
deleted file mode 100644,100644
+++ /dev/null
@@@ -1,73 -1,73 +1,0 @@@
--/*
-- * This file is part of DisOrder.
-- * Copyright (C) 2004, 2005, 2007, 2008 Richard Kettlewell
-- *
-- * This program is free software; you can redistribute it and/or modify
-- * it under the terms of the GNU General Public License as published by
-- * the Free Software Foundation; either version 2 of the License, or
-- * (at your option) any later version.
-- *
-- * This program is distributed in the hope that it will be useful, but
-- * WITHOUT ANY WARRANTY; without even the implied warranty of
-- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
-- * General Public License for more details.
-- *
-- * You should have received a copy of the GNU General Public License
-- * along with this program; if not, write to the Free Software
-- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
-- * USA
-- */
--
--#ifndef DCGI_H
--#define DCGI_H
--
--typedef struct dcgi_global {
--  disorder_client *client;
--  unsigned flags;
--#define DC_QUEUE 0x0001
--#define DC_PLAYING 0x0002
--#define DC_RECENT 0x0004
--#define DC_VOLUME 0x0008
--#define DC_DIRS 0x0010
--#define DC_FILES 0x0020
--#define DC_NEW 0x0040
--#define DC_RIGHTS 0x0080
--  struct queue_entry *queue, *playing, *recent;
--  int volume_left, volume_right;
--  char **files, **dirs;
--  int nfiles, ndirs;
--  char **new;
--  int nnew;
--  rights_type rights;
--} dcgi_global;
--
--typedef struct dcgi_state {
--  dcgi_global *g;
--  struct queue_entry *track;
--  struct kvp *pref;
--  int index;
--  int first, last;
--  struct entry *entry;
--  /* for searching */
--  int ntracks;
--  char **tracks;
--  /* for @navigate@ */
--  const char *nav_path;
--  int nav_len, nav_dirlen;
--} dcgi_state;
--
--void disorder_cgi(cgi_sink *output, dcgi_state *ds);
--void disorder_cgi_error(cgi_sink *output, dcgi_state *ds,
--                      const char *msg);
--void disorder_cgi_login(dcgi_state *ds, cgi_sink *output);
--
--extern char *login_cookie;
--
--#endif /* DCGI_H */
--
--/*
--Local Variables:
--c-basic-offset:2
--comment-column:40
--End:
--*/