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).

78 files changed:
.bzrignore
configure.ac
debian/rules
doc/Makefile.am
doc/disorder.1.in
doc/disorder.cgi.8.in [new file with mode: 0644]
doc/disorder_actions.5.head [new file with mode: 0644]
doc/disorder_actions.5.tail [new file with mode: 0644]
doc/disorder_config.5.in
doc/disorder_options.5.in [new file with mode: 0644]
doc/disorder_templates.5.head [new file with mode: 0644]
doc/disorder_templates.5.tail [new file with mode: 0644]
doc/disorderd.8.in
lib/Makefile.am
lib/cgi.c [new file with mode: 0644]
lib/cgi.h [new file with mode: 0644]
lib/filepart.c
lib/filepart.h
lib/hash.c
lib/hash.h
lib/macros-builtin.c [new file with mode: 0644]
lib/macros.c [new file with mode: 0644]
lib/macros.h [new file with mode: 0644]
lib/mime.c
lib/sink.c
lib/sink.h
lib/t-cgi.c [new file with mode: 0644]
lib/t-filepart.c
lib/t-macros-1.tmpl [new file with mode: 0644]
lib/t-macros-2 [new file with mode: 0644]
lib/t-macros.c [new file with mode: 0644]
lib/t-mime.c
lib/test.c
lib/test.h
lib/trackdb.c
lib/url.c
scripts/Makefile.am
scripts/htmlman
scripts/macro-docs [new file with mode: 0755]
server/Makefile.am
server/actions.c [new file with mode: 0644]
server/cgi.c [deleted file]
server/cgi.h [deleted file]
server/cgimain.c
server/dcgi.c [deleted file]
server/dcgi.h [deleted file]
server/disorder-cgi.h [new file with mode: 0644]
server/login.c [new file with mode: 0644]
server/lookup.c [new file with mode: 0644]
server/macros-disorder.c [new file with mode: 0644]
server/options.c [new file with mode: 0644]
templates/Makefile.am
templates/about.html [deleted file]
templates/about.tmpl [new file with mode: 0644]
templates/choose.html [deleted file]
templates/choose.tmpl [new file with mode: 0644]
templates/choosealpha.html [deleted file]
templates/credits.html [deleted file]
templates/disorder.css
templates/error.tmpl [moved from templates/error.html with 80% similarity]
templates/help.tmpl [moved from templates/help.html with 66% similarity]
templates/login.tmpl [moved from templates/login.html with 67% similarity]
templates/macros.tmpl [new file with mode: 0644]
templates/new.html [deleted file]
templates/new.tmpl [new file with mode: 0644]
templates/options.labels
templates/playing.html [deleted file]
templates/playing.tmpl [new file with mode: 0644]
templates/prefs.html [deleted file]
templates/prefs.tmpl [new file with mode: 0644]
templates/recent.html [deleted file]
templates/recent.tmpl [new file with mode: 0644]
templates/search.html [deleted file]
templates/stdhead.html [deleted file]
templates/stylesheet.html [deleted file]
templates/topbar.html [deleted file]
templates/topbarend.html [deleted file]
templates/volume.html [deleted file]

index 420efc6..b3032ad 100644 (file)
@@ -172,3 +172,18 @@ lib/t-utf8
 lib/t-vector
 lib/t-words
 lib/t-wstat
+lib/t-macros
+lib/t-cgi
+doc/*.tmpl
+doc/disorder_templates.5
+oc/disorder_templates.5.html
+doc/disorder_templates.5
+doc/disorder_templates.5.html
+doc/disorder.cgi.8
+doc/disorder.cgi.8.html
+doc/disorder_actions.5
+doc/disorder_actions.5.html
+doc/disorder_options.5
+doc/disorder_options.5.html
+doc/disorder_actions.5.in
+doc/disorder_templates.5.in
index 1dabfe5..3c13dac 100644 (file)
@@ -417,7 +417,7 @@ if test $ac_cv_type_long_long = yes; then
     AC_DEFINE([DECLARES_ATOLL],[1],[define if <stdlib.h> declares atoll])
   fi
 fi
-AC_CHECK_FUNCS([ioctl nl_langinfo strsignal],[:],[
+AC_CHECK_FUNCS([ioctl nl_langinfo strsignal setenv unsetenv],[:],[
   missing_functions="$missing_functions $ac_func"
 ])
 # fsync will do if fdatasync not available
@@ -576,7 +576,6 @@ AH_BOTTOM([#ifdef __GNUC__
 #endif])
 
 AC_CONFIG_FILES([Makefile
-                templates/Makefile
                 images/Makefile
                 scripts/Makefile
                 lib/Makefile
@@ -584,6 +583,7 @@ AC_CONFIG_FILES([Makefile
                 clients/Makefile
                 disobedience/Makefile
                 doc/Makefile
+                templates/Makefile
                 plugins/Makefile
                 driver/Makefile
                 debian/Makefile
index ce385d7..20c38ca 100755 (executable)
@@ -88,11 +88,15 @@ pkg-disorder: build
                debian/disorder/etc/bash_completion.d/disorder
        rm -rf debian/disorder/usr/share/man/man8
        rm -rf debian/disorder/usr/share/disorder/*.html
+       rm -rf debian/disorder/usr/share/disorder/*.tmpl
        rmdir debian/disorder/usr/share/disorder
        rm -f debian/disorder/usr/bin/disorder-playrtp
        rm -f debian/disorder/usr/bin/disobedience
        rm -f debian/disorder/usr/share/man/man1/disorder-playrtp.1
        rm -f debian/disorder/usr/share/man/man1/disobedience.1
+       rm -f debian/disorder/usr/share/man/man5/disorder_templates.5
+       rm -f debian/disorder/usr/share/man/man5/disorder_actions.5
+       rm -f debian/disorder/usr/share/man/man5/disorder_options.5
        $(MKDIR) debian/disorder/etc/disorder
        dpkg-shlibdeps -Tdebian/substvars.disorder \
                debian/disorder/usr/bin/*
@@ -134,7 +138,8 @@ pkg-disorder-server: build
        $(MAKE) DESTDIR=`pwd`/debian/disorder-server staticdir=/var/www/disorder installdirs install -C doc
        rm -rf debian/disorder-server/usr/share/man/man1
        rm -rf debian/disorder-server/usr/share/man/man3
-       rm -rf debian/disorder-server/usr/share/man/man5
+       rm -f debian/disorder-server/usr/share/man/man5/disorder_config.5
+       rm -f debian/disorder-server/usr/share/man/man5/disorder_protocol.5
        $(MKDIR) debian/disorder-server/etc/disorder
        $(MKDIR) debian/disorder-server/etc/init.d
        $(MKDIR) debian/disorder-server/usr/lib/cgi-bin
index 0e79171..4b01791 100644 (file)
 # USA
 #
 
-SEDFILES=disorder.1 disorderd.8 disorder_config.5 \
-       disorder-dump.8 disorder_protocol.5 disorder-deadlock.8 \
-       disorder-rescan.8 disobedience.1 disorderfm.1 disorder-playrtp.1 \
-       disorder-decode.8 disorder-stats.8 disorder-dbupgrade.8
-
-include ${top_srcdir}/scripts/sedfiles.make
-
+noinst_DATA=$(HTMLMAN)
+pkgdata_DATA=$(TMPLMAN)
 man_MANS=disorderd.8 disorder.1 disorder.3 disorder_config.5 disorder-dump.8 \
        disorder_protocol.5 disorder-deadlock.8 \
        disorder-rescan.8 disobedience.1 disorderfm.1 disorder-speaker.8 \
        disorder-playrtp.1 disorder-normalize.8 disorder-decode.8 \
-       disorder-stats.8 disorder-dbupgrade.8
-
+       disorder-stats.8 disorder-dbupgrade.8 disorder_templates.5 \
+       disorder_actions.5 disorder_options.5 disorder.cgi.8
 noinst_MANS=tkdisorder.1
 
-HTMLMAN=$(foreach man,$(man_MANS),$(man).html)
+SEDFILES=disorder.1 disorderd.8 disorder_config.5 \
+       disorder-dump.8 disorder_protocol.5 disorder-deadlock.8 \
+       disorder-rescan.8 disobedience.1 disorderfm.1 disorder-playrtp.1 \
+       disorder-decode.8 disorder-stats.8 disorder-dbupgrade.8 \
+       disorder_options.5 disorder.cgi.8 disorder_templates.5 \
+       disorder_actions.5
+
+include ${top_srcdir}/scripts/sedfiles.make
 
+HTMLMAN=$(foreach man,$(man_MANS),$(man).html)
 $(HTMLMAN) : %.html : % $(top_srcdir)/scripts/htmlman
        rm -f $@.new
+       $(top_srcdir)/scripts/htmlman $< >$@.new
+       chmod 444 $@.new
+       mv -f $@.new $@
+
+TMPLMAN=$(foreach man,$(man_MANS),$(man).tmpl)
+$(TMPLMAN) : %.tmpl : % $(top_srcdir)/scripts/htmlman
+       rm -f $@.new
        $(top_srcdir)/scripts/htmlman -stdhead $< >$@.new
        chmod 444 $@.new
        mv -f $@.new $@
 
-pkgdata_DATA=$(HTMLMAN)
+disorder_templates.5.in: disorder_templates.5.head disorder_templates.5.tail \
+               $(top_srcdir)/lib/macros-builtin.c \
+               $(top_srcdir)/server/macros-disorder.c \
+               $(top_srcdir)/scripts/macro-docs
+       rm -f disorder_templates.5.new
+       cat ${srcdir}/disorder_templates.5.head >> disorder_templates.5.new
+       $(top_srcdir)/scripts/macro-docs >> disorder_templates.5.new \
+               $(top_srcdir)/lib/macros-builtin.c \
+               $(top_srcdir)/server/macros-disorder.c 
+       cat ${srcdir}/disorder_templates.5.tail >> disorder_templates.5.new
+       mv disorder_templates.5.new disorder_templates.5.in
+
+disorder_actions.5.in: disorder_actions.5.head disorder_actions.5.tail \
+               $(top_srcdir)/lib/macros-builtin.c \
+               $(top_srcdir)/server/actions.c \
+               $(top_srcdir)/scripts/macro-docs
+       rm -f disorder_actions.5.new
+       cat ${srcdir}/disorder_actions.5.head >> disorder_actions.5.new
+       $(top_srcdir)/scripts/macro-docs >> disorder_actions.5.new \
+               $(top_srcdir)/server/actions.c 
+       cat ${srcdir}/disorder_actions.5.tail >> disorder_actions.5.new
+       mv disorder_actions.5.new disorder_actions.5.in
 
 EXTRA_DIST=disorderd.8.in disorder.1.in disorder_config.5.in \
           disorder.3 disorder-dump.8.in disorder_protocol.5.in \
           tkdisorder.1 disorder-deadlock.8.in disorder-rescan.8.in \
           disobedience.1.in disorderfm.1.in disorder-speaker.8 \
           disorder-playrtp.1.in disorder-decode.8.in disorder-normalize.8 \
-          disorder-stats.8.in disorder-dbupgrade.8.in
+          disorder-stats.8.in disorder-dbupgrade.8.in \
+          disorder_actions.5.head disorder_templates.5.head \
+          disorder_actions.5.tail disorder_templates.5.tail \
+          disorder_options.5.in disorder.cgi.8.in
 
-CLEANFILES=$(SEDFILES) $(HTMLMAN)
+CLEANFILES=$(SEDFILES) $(HTMLMAN) $(TMPLMAN)
 
 export GNUSED
index e1981f6..bd22e56 100644 (file)
@@ -393,7 +393,7 @@ Per-user password file
 Communication socket for \fBdisorder\fR(1).
 .SH "SEE ALSO"
 \fBdisorderd\fR(8), \fBdisorder_config\fR(5), \fBsyslog\fR(3), \fBtime\fR(2),
-\fBpcrepattern\fR(3), \fBdisobedience\fR(1)
+\fBpcrepattern\fR(3), \fBdisobedience\fR(1), \fBdisorder.cgi\fR(8)
 .PP
 "\fBpydoc disorder\fR" for the Python API documentation.
 .\" Local Variables:
diff --git a/doc/disorder.cgi.8.in b/doc/disorder.cgi.8.in
new file mode 100644 (file)
index 0000000..833e114
--- /dev/null
@@ -0,0 +1,51 @@
+.\"
+.\" Copyright (C) 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
+.\"
+.TH disorder.cgi 8
+.SH NAME
+disorder.cgi - DisOrder web interface
+.SH DESCRIPTION
+.B disorder.cgi
+is the web interface for DisOrder.  It runs as a CGI executable under
+(for example) Apache.
+.PP
+By default it will connect as the "guest" user.  This implies that
+\fBdisorder setup-guest\fR must have been run on the server for the
+CGI to work effectively, though it should be possible to manage
+without.
+.PP
+See \fBdisorder_actions\fR(5) for a description of what the CGI does.
+.PP
+See \fBdisorder_options\fR(5) for CGI-specific configuration and
+\dBdisorder_config\fR(5) for general configuration.
+.PP
+See \fBdisorder_templates\fR(5) for the template language used.
+.SH "WHERE IS IT?"
+The DisOrder makefiles don't know where to install the CGI, or indeed
+whether you wanted it installed, so they do not install it at all.
+You must copy it into the right location according to your web
+server's configuration.
+.SH "SEE ALSO"
+.BR disorder_config (5),
+.BR disorder_options (5),
+.BR disorder_templates (5),
+.BR disorder_actions (5)
+.\" Local Variables:
+.\" mode:nroff
+.\" fill-column:79
+.\" End:
diff --git a/doc/disorder_actions.5.head b/doc/disorder_actions.5.head
new file mode 100644 (file)
index 0000000..c093a2d
--- /dev/null
@@ -0,0 +1,57 @@
+.\"
+.\" Copyright (C) 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
+.\"
+.TH disorder_actions 5
+.SH NAME
+disorder_actions - DisOrder CGI actions
+.SH DESCRIPTION
+The primary CGI parameter to the DisOrder web interface is \fBaction\fR.
+This determines which of a set of actions from the list below it carries out.
+.PP
+For any action \fIACTION\fR not in the list, the CGI expands the template
+\fIACTION\fB.tmpl\fR.
+.PP
+If no action is set, then the default is \fBplaying\fR, unless the argument
+\fBc\fR is present, in which case it is \fBconfirm\fR.
+This is a hack to keep confirmation URLs short.
+.SS Redirection
+Actions in the list below that do not documented what template they expand
+issue an HTTP redirect according to the value of the \fBback\fR argument.
+There are three possibilities:
+.TP
+.BR 1 )
+\fBback\fR is a URL.
+The browser is redirected to that URL.
+.TP
+.BR 2 )
+\fBback\fR is an action name.
+The browser is redirected to a URL which uses that action.
+.TP
+.BR 3 )
+\fBback\fR is not set.
+The browser is redirected to the front page.
+.PP
+If an action needs more rights than the logged-in user has then they are
+redirected to \fBlogin\fR with \fBback\fR set to retry the action they wanted.
+.PP
+Certain errors cause a redirection to \fBerror\fR with \fB@error\fR set.
+.SH ACTIONS
+.\" Local Variables:
+.\" mode:nroff
+.\" fill-column:79
+.\" End:
diff --git a/doc/disorder_actions.5.tail b/doc/disorder_actions.5.tail
new file mode 100644 (file)
index 0000000..9c95521
--- /dev/null
@@ -0,0 +1,26 @@
+.\"
+.\" Copyright (C) 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
+.\"
+.SH "SEE ALSO"
+.BR disorder_templates (5),
+.BR disorder_config (5),
+.BR disorder.cgi (8)
+.\" Local Variables:
+.\" mode:nroff
+.\" fill-column:79
+.\" End:
index e714a22..a774cc6 100644 (file)
@@ -145,6 +145,8 @@ This model will be changed in a future version.)
 Access control to the web interface is (currently) separate from DisOrder's own
 access control (HTTP authentication is required) but uses the same user
 namespace.
+.PP
+See \fBdisorder.cgi\fR(8) for more information.
 .SS "Searching And Tags"
 Search strings contain a list of search terms separated by spaces.
 A search term can either be a single word or a tag, prefixed with "tag:".
@@ -830,525 +832,6 @@ If this is set to a nonzero value then the driver will call \fB_exit\fR(2) if a
 write to the output file descriptor fails.
 This is a workaround for buggy players such as \fBogg123\fR that ignore
 write errors.
-.SH "WEB TEMPLATES"
-When \fBdisorder.cgi\fR wants to generate a page for an action it searches the
-directories specified with \fBtemplates\fR for a matching file.
-It is suggested that you leave the distributed templates unchanged and put
-any customisations in an earlier entry in the template path.
-.PP
-The supplied templates are:
-.TP
-.B about.html
-Display information about DisOrder.
-.TP
-.B choose.html
-Navigates through the track database to choose a track to play.
-The \fBdir\fR argument gives the directory to look in; if it is missing
-then the root directory is used.
-.TP
-.B choosealpha.html
-Provides a front end to \fBchoose.html\fR which allows subsets of the top level
-directories to be selected by initial letter.
-.TP
-.B new.html
-Lists newly added tracks.
-.TP
-.B playing.html
-The "front page", which usually shows the currently playing tracks and
-the queue.
-Gets an HTTP \fBRefresh\fR header.
-.IP
-If the \fBmgmt\fR CGI argument is set to \fBtrue\fR then we include extra
-buttons for moving tracks up and down in the queue.
-There is some logic in \fBdisorder.cgi\fR to ensure that \fBmgmt=true\fR
-is preserved across refreshes and redirects back into itself, but
-URLs embedded in web pages must include it explicitly.
-.TP
-.B prefs.html
-Views preferences.
-If the \fBfile\fR, \fBname\fR and \fBvalue\fR arguments are
-all set then that preference is modified; if \fBfile\fR and \fBname\fR are set
-but not \fBvalue\fR then the preference is deleted.
-.TP
-.B recent.html
-Lists recently played tracks.
-.TP
-.B search.html
-Presents search results.
-.TP
-.B volume.html
-Primitive volume control.
-.PP
-Additionally, other standard files are included by these:
-.TP
-.B credits.html
-Included at the end of the main content \fB<DIV>\fR element.
-.TP
-.B topbar.html
-Included at the start of the \fB<BODY>\fR element.
-.TP
-.B topbarend.html
-Included at the end of the \fB<BODY>\fR element.
-.TP
-.B stdhead.html
-Included in the \fB<HEAD>\fR element.
-.TP
-.B stylesheet.html
-Contains the default DisOrder stylesheet.
-You can override this by editing the CSS or by replacing it all with
-a \fB<LINK>\fR to an external stylesheet.
-.PP
-Templates are ASCII files containing HTML documents, with an expansion
-syntax to enable data supplied by the implementation to be inserted.
-.PP
-If you want to use characters outside the ASCII range, use either the
-appropriate HTML entity, e.g. \fB&eacute;\fR, or an SGML numeric
-character reference, e.g. \fB&#253;\fR.
-Use \fB&#64;\fR to insert a literal \fB@\fR without falling foul of
-the expansion syntax.
-.SS "Expansion Syntax"
-Expansions are surrounded by at ("@") symbols take the form of a keyword
-followed by zero or more arguments.
-Arguments may either be quoted by curly brackets ("{" and "}") or separated
-by colons (":").
-Both kinds may be mixed in a single expansion, though doing so seems
-likely to cause confusion.
-The descriptions below contain suggested forms for each expansion.
-.PP
-Leading and trailing whitespace in unquoted arguments is ignored, as is
-whitespace (including newlines) following a close bracket ("}").
-.PP
-Arguments are recursively expanded before being interpreted, except for
-\fITEMPLATE\fR arguments.
-These are expanded (possibly more than once) to produce the final expansion.
-(More than once means the same argument being expanded more than once
-for different tracks or whatever, not the result of the first
-expansion itself being re-expanded.)
-.PP
-Strings constructed by expansions (i.e. not literally copied from the template
-text) are SGML-quoted: any character which does not stand for itself in #PCDATA
-or a quoted attribute value is replaced by the appropriate numeric character
-reference.
-.PP
-The exception to this is that such strings are \fInot\fR quoted when they are
-generated in the expansion of a parameter.
-.PP
-In the descriptions below, the current track means the one set by
-\fB@playing@\fR, \fB@recent@\fR or \fB@queue@\fR, not the one that is playing.
-If none of these expansions are in force then there is no current track.
-\fIBOOL\fR should always be either \fBtrue\fR or \fBfalse\fR.
-.SS "Expansions"
-The following expansion keywords are defined:
-.TP
-.B @#{\fICOMMENT\fB}@
-Ignored.
-.TP
-.B @action@
-The current action.
-This reports
-.B manage
-if the action is really
-.B playing
-but
-.B mgmt=true
-was set.
-.TP
-.B @and{\fIBOOL\fB}{\fIBOOL\fB}\fR...\fB@
-If there are no arguments, or all the arguments are \fBtrue\fB, then expands to
-\fBtrue\fR, otherwise to \fBfalse\fR.
-.TP
-.B @arg:\fINAME\fB@
-Expands to the value of CGI argument \fINAME\fR.
-.TP
-.B @basename@
-The basename of the current directory component, in \fB@navigate@\fR.
-.TP
-.B @basename{\fIPATH\fB}@
-The base name part of \fIPATH\fR.
-.TP
-.B @choose{\fIWHAT\fB}{\fITEMPLATE\fB}@
-Expands \fITEMPLATE\fR repeatedly for each file or directory under
-\fB@arg:directory@\fR.
-\fIWHAT\fR should be either \fBfile\fR or \fBdirectory\fR.
-Use \fB@file@\fR to get the display name or filename of the file or
-directory.
-Usually used in \fBchoose.html\fR.
-.TP
-.B @dirname@
-The directory of the current directory component, in \fB@navigate@\fR.
-.TP
-.B @dirname{\fIPATH\fB}@
-The directory part of \fIPATH\fR.
-.TP
-.B @enabled@
-Expands to \fBtrue\fR if play is currently enabled, otherwise to \fBfalse\fR.
-.TP
-.B @eq{\fIA\fB}{\fIB\fB}
-Expands to \fBtrue\fR if \fIA\fR and \fIB\fR are identical, otherwise to
-\fBfalse\fR.
-.TP
-.B @file@
-Expands to the filename of the current file or directory, inside the template
-argument to \fBchoose\fR.
-.TP
-.B @files{\fITEMPLATE\fB}
-Expands \fITEMPLATE\fR once for each file indicated by the \fBdirectory\fR CGI
-arg if it is present, or otherwise for the list of files counted by \fBfiles\fR
-with names \fB0_file\fR, \fB1_file\fR etc.
-.TP
-.B @fullname@
-The full path of the current directory component, in \fB@navigate@\fR.
-.TP
-.B @id@
-The ID of the current track.
-.TP
-.B @if{\fIBOOL\fB}{\fITRUEPART\fB}{\fIFALSEPART\fB}@
-If \fIBOOL\fR expands to \fBtrue\fR then expands to \fITRUEPART\fR, otherwise
-to \fIFALSEPART\fR (which may be omitted).
-.TP
-.B @image:\fINAME\fB@
-Expands to the (possibly relative) URL for image \fINAME\fR.
-.IP
-If there is a label \fBimages.\fINAME\fR then that will be the image base name.
-Otherwise the image base name is \fINAME\fB.png\fR or just \fINAME\fR if it
-alraedy has an extension.
-Thus labels may be defined to give images role names.
-.IP
-If there is a label \fBurl.static\fR then that is the base URL for images.
-If it is not defined then \fB/disorder\fR is used as a default.
-.TP
-.B @include:\fIPATH\fB@
-Include the named file as if it were a template file.
-If \fIPATH\fR starts with a \fB/\fR then it is used as-is;
-otherwise, ".html" is appended and the template path is searched.
-.TP
-.B @index@
-Expands to the index of the current file in \fB@queue@\fR, \fB@recent@\fR or
-\fB@files@\fR.
-.TP
-.B @isdirectories@
-Expands to \fBtrue\fR if there are any directories in \fB@arg:directory@\fR,
-otherwise to \fBfalse\fR.
-.TP
-.B @isfiles@
-Expands to \fBtrue\fR if there are any files in \fB@arg:directory@\fR,
-otherwise to \fBfalse\fR.
-.TP
-.B @isfirst@
-Expands to \fBtrue\fR if this is the first repetition of a \fITEMPLATE\fR
-argument in a loop (\fB@queue\fR or similar), otherwise to \fBfalse\fR.
-.TP
-.B @islast@
-Expands to \fBtrue\fR if this is the last repetition of a \fITEMPLATE\fR in a
-loop, otherwise to \fBfalse\fR.
-.TP
-.B @isnew@
-Expands to \fBtrue\fR if the newly added tracks list has any tracks in it,
-otherwise to \fBfalse\fR.
-.TP
-.B @isplaying@
-Expands to \fBtrue\fR if a track is playing, otherwise to \fBfalse\fR.
-.TP
-.B @isqueue@
-Expands to \fBtrue\fR if there are any tracks in the queue, otherwise to
-\fBfalse\fR.
-.TP
-.B @isrecent@
-Expands to \fBtrue\fR if the recently played list has any tracks in it,
-otherwise to \fBfalse\fR.
-.TP
-.B @label:\fINAME\fR\fB@
-Expands to the value of label \fINAME\fR.
-See the shipped \fIoptions.labels\fR file for full documentation of the
-labels used by the standard templates.
-.TP
-.B @length@
-Expands to the length of the current track.
-.TP
-.B @movable@
-Expands to \fBtrue\fR if the current track is movable, otherwise to
-\fBfalse\fR.
-.TP
-.B @navigate{\fIDIRECTORY\fB}{\fITEMPLATE\fB}
-Expands \fITEMPLATE\fR for each component of \fIDIRECTORY\fR in turn.
-Use \fB@dirname\fR and \fB@basename@\fR to get the components of the path to
-each component.
-Usually used in \fBchoose.html\fR.
-.TP
-.B @ne{\fIA\fB}{\fIB\fB}
-Expands to \fBtrue\fR if \fIA\fR and \fIB\fR differ, otherwise to \fBfalse\fR.
-.TP
-.B @new{\fITEMPLATE\fB}
-Expands \fITEMPLATE\fR for each track in the newly added tracks list, starting
-with the most recent.
-Used in \fBnew.html\fR.
-.TP
-.B @nfiles@
-Expands to the number of files from \fB@files\fR (above).
-.TP
-.B @nonce@
-Expands to a string including the time and process ID, intended to be
-unique across invocations.
-.TP
-.B @not{\fIBOOL\fB}@
-Expands to \fBfalse\fR if \fIBOOL\fR is \fBtrue\fR, otherwise to
-\fBfalse\fR.
-.TP
-.B @or{\fIBOOL\fB}{\fIBOOL\fB}\fR...\fB@
-If at least one argument is \fBtrue\fB, then expands to \fBtrue\fR, otherwise
-to \fBfalse\fR.
-.TP
-.B @parity@
-Expands to \fBeven\fR or \fBodd\fR depending on whether the current track is at
-an even or odd position in \fB@queue@\fR, \fB@recent@\fR or \fB@files@\fR.
-.TP
-.B @part{\fICONTEXT\fB}{\fIPART\fB}@
-Expands to track name part \fIPART\fR using context \fICONTEXT\fR for the
-current track.
-The context may be omitted and defaults to \fBdisplay\fR.
-.IP
-The special context \fBshort\fR is equivalent to \fBdisplay\fR but limited to
-the \fBshort_display\fR limit.
-.TP
-.B @part{\fICONTEXT\fB}{\fIPART\fB}{\fITRACK\fB}@
-Expands to track name part \fIPART\fR using context \fICONTEXT\fR for
-\fITRACK\fR.
-In this usage the context may not be omitted.
-.IP
-The special context \fBshort\fR is equivalent to \fBdisplay\fR but limited to
-the \fBshort_display\fR limit.
-.TP
-.B @paused@
-Expands to \fBtrue\fR if the current track is paused, else \fBfalse\fR.
-.TP
-.B @playing{\fITEMPLATE\fB}@
-Expands \fITEMPLATE\fR using the playing track as the current track.
-.TP
-.B @pref{\fITRACK\fB}{\fIKEY\fB}@
-Expand to the track preference, or the empty string if it is not set.
-.TP
-.B @prefname@
-Expands to the name of the current preference, in the template
-argument of \fB@prefs@\fR.
-.TP
-.B @prefs{\fIFILE\fB}{\fITEMPLATE\fB}@
-Expands \fITEMPLATE\fR repeatedly, for each preference of track
-\fIFILE\fR.
-Use \fB@prefname@\fR and \fB@prefvalue@\fR to get the name and value.
-.TP
-.B @prefvalue@
-Expands to the value of the current preference, in the template
-argument of \fB@prefs@\fR.
-.TP
-.B @queue{\fITEMPLATE\fB}@
-Expands \fITEMPLATE\fR repeatedly using the each track on the queue in turn as
-the current track.
-The track at the head of the queue comes first.
-.TP
-.B @random\-enabled@
-Expands to \fBtrue\fR if random play is currently enabled, otherwise to
-\fBfalse\fR.
-.TP
-.B @recent{\fITEMPLATE\fB}@
-Expands \fITEMPLATE\fR repeatedly using the each recently played track in turn
-as the current track.
-The most recently played track comes first.
-.TP
-.B @removable@
-Expands to \fBtrue\fR if the current track is removable, otherwise to
-\fBfalse\fR.
-.TP
-.B @resolve{\fITRACK\fB}@
-Resolve aliases for \fITRACK\fR and expands to the result.
-.TP
-.B @right{\fIRIGHT\fB}@
-Exapnds to \fBtrue\fR if the user has right \fIRIGHT\fR, otherwise to
-\fBfalse\fR.
-.TP
-.B @right{\fIRIGHT\fB}{\fITRUEPART\fB}{\fIFALSEPART\fB}@
-Expands to \fITRUEPART\fR if the user right \fIRIGHT\fR, otherwise to
-\fIFALSEPART\fR (which may be omitted).
-.TP
-.B @scratchable@
-Expands to \fBtrue\fR if the currently playing track is scratchable, otherwise
-to \fBfalse\fR.
-.TP
-.B @search{\fIPART\fB}\fR[\fB{\fICONTEXT\fB}\fR]\fB{\fITEMPLATE\fB}@
-Expands \fITEMPLATE\fR once for each group of search results that have
-a common value of track part \fIPART\fR.
-The groups are sorted by the value of the part.
-.IP
-.B @part@
-and
-.B @file@
-within the template will apply to one of the tracks in the group.
-.IP
-If \fICONTEXT\fR is specified it should be either \fBsort\fR or \fBdisplay\fR,
-and determines the context for \fIPART\fR.
-The default is \fBsort\fR.
-Usually you want \fBdisplay\fR for everything except the title and
-\fBsort\fR for the title.
-If you use \fBsort\fR for artist and album then you are likely to get
-strange effects.
-.TP
-.B @server\-version@
-Expands to the server's version string.
-.TP
-.B @shell{\fICOMMAND\fB}@
-Expands to the output of \fICOMMAND\fR executed via the shell.
-\fBsh\fR is searched for using \fBPATH\fR.
-If the command fails then this is logged but otherwise ignored.
-.TP
-.B @state@
-In \fB@queue@\fR and \fB@recent@\fR, expands to the state of the current
-track.
-Otherwise the empty string.
-Known states are:
-.RS
-.TP 12
-.B failed
-The player terminated with nonzero status, but not because the track was
-scratched.
-.TP
-.B isscratch
-A scratch, in the queue.
-.TP
-.B no_player
-No player could be found.
-.TP
-.B ok
-Played successfully.
-.TP
-.B random
-A randomly chosen track, in the queue.
-.TP
-.B scratched
-This track was scratched.
-.TP
-.B unplayed
-An explicitly queued track, in the queue.
-.RE
-.IP
-Some additional states only apply to playing tracks, so will never be seen in
-the queue or recently-played list:
-.RS
-.TP 12
-.B paused
-The track has been paused.
-.TP
-.B quitting
-Interrupted because the server is shutting down.
-.TP
-.B started
-This track is currently playing.
-.RE
-.TP
-.B @stats@
-Expands to the server statistics.
-.TP
-.B @thisurl@
-Expands to the URL of the current page.
-Typically used in
-.B back
-arguments.
-If there is a
-.B nonce
-argument then it is changed to a fresh value.
-.TP
-.B @track@
-The current track.
-.TP
-.B @trackstate{\fIPATH\fB}@
-Expands to the current track state: \fBplaying\fR if the track is actually
-playing now, \fBqueued\fR if it is queued or the empty string otherwise.
-.TP
-.B @transform{\fIPATH\fB}{\fITYPE\fB}{\fICONTEXT\fB}@
-Transform a path according to \fBtransform\fR (see above).
-\fIPATH\fR should be a raw filename (of a track or directory).
-\fITYPE\fR should be the transform type (e.g. \fItrack\fR or \fIdir\fR).
-\fICONTEXT\fR should be the context, and can be omitted (the default
-is \fBdisplay\fR).
-.TP
-.B @url@
-Expands to the canonical URL as defined in \fIpkgconfdir/config\fR.
-.TP
-.B @urlquote{\fISTRING\fB}@
-URL-quote \fISTRING\fR.
-.TP
-.B @user@
-The current username.
-This will be "guest" if nobody is logged in.
-.TP
-.B @userinfo{\fIPROPERTY\fB}@
-Look up a property of the logged-in user.
-.TP
-.B @version@
-Expands to \fBdisorder.cgi\fR's version string.
-.TP
-.B @volume:\fISPEAKER\fB@
-The volume on the left or right speaker.
-\fISPEAKER\fR must be \fBleft\fR or \fBright\fR.
-.TP
-.B @when@
-When the current track was played (or when it is expected to be played, if it
-has not been played yet)
-.TP
-.B @who@
-Who submitted the current track.
-.SH "WEB OPTIONS"
-This is a file called \fIoptions\fR, searched for in the same manner
-as templates.
-It includes numerous options for the control of the web interface.
-The general syntax is the same as the main configuration
-file, except that it should be encoded using UTF-8 (though this might
-change to the current locale's character encoding; stick to ASCII to
-be safe).
-.PP
-The shipped \fIoptions\fR file includes four standard options files.
-In order, they are:
-.TP
-.I options.labels
-The default labels file.
-You wouldn't normally edit this directly - instead supply your own commands
-in \fIoptions.user\fR.
-Have a look at the shipped version of the file for documentation of
-labels used by the standard templates.
-.TP
-.I options.user
-A user options file.
-Here you should put any overrides for the default labels and any
-extra labels required by your modified templates.
-.PP
-Valid directives are:
-.TP
-.B columns \fINAME\fR \fIHEADING\fR...
-Defines the columns used in \fB@playing@\fR and \fB@recent@\fB.
-\fINAME\fR must be either \fBplaying\fR, \fBrecent\fR or \fBsearch\fR.
-\fIHEADING\fR...  is a list of heading names.
-If a column is defined more than once then the last definitions is used.
-.IP
-The heading names \fBbutton\fR, \fBlength\fR, \fBwhen\fR and \fBwho\fR
-are built in.
-.TP
-.B include \fIPATH\fR
-Includes another file.
-If \fIPATH\fR starts with a \fB/\fR then it is taken as is, otherwise
-it is searched for in the template path.
-.TP
-.B label \fINAME\fR \fIVALUE\fR
-Define a label.
-If a label is defined more than once then the last definition is used.
-.SS Labels
-Some labels are defined inside \fBdisorder.cgi\fR and others by the
-default templates.
-You can define your own labels and use them inside a template.
-.PP
-When an undefined label is expanded, if it has a dot in its name then
-the part after the final dot is used as its value.
-Otherwise the whole name is used as the value.
-.PP
-Labels are no longer documented here, see the shipped \fIoptions.labels\fR file
-instead.
 .SH "REGEXP SUBSTITUTION RULES"
 Regexps are PCRE regexps, as defined in \fBpcrepattern\fR(3).
 The only option used is \fBPCRE_UTF8\fR.
@@ -1374,100 +857,6 @@ behaviour.)
 If \fBi\fR is present in \fIREFLAGS\fR then the match is case-independent.
 If \fBg\fR is present then all matches are replaced, otherwise only the first
 match is replaced.
-.SH "ACTIONS"
-What the web interface actually does is terminated by the \fBaction\fR CGI
-argument.
-The values listed below are supported.
-.PP
-Except as specified, all actions redirect back to the \fBplaying.html\fR
-template unless the \fBback\fR argument is present, in which case the URL it
-gives is used instead.
-.PP
-Redirection to \fBplaying.html\fR preserves \fBmgmt=true\fR if it is present.
-.TP 8
-.B "move"
-Move track \fBid\fR by offset \fBdelta\fR.
-.TP
-.B "play"
-Play track \fBfile\fR, or if that is missing then play all the tracks in
-\fBdirectory\fR.
-.TP
-.B "playing"
-Don't change any state, but instead compute a suitable refresh time and include
-that in an HTTP header.
-Expands the \fBplaying.html\fR template rather than redirecting.
-.IP
-This is the default if \fBaction\fR is missing.
-.TP
-.B "random\-disable"
-Disables random play.
-.TP
-.B "random\-enable"
-Enables random play.
-.TP
-.B "disable"
-Disables play completely.
-.TP
-.B "enable"
-Enables play.
-.TP
-.B "pause"
-Pauses the current track.
-.TP
-.B "remove"
-Remove track \fBid\fR.
-.TP
-.B "resume"
-Resumes play after a pause.
-.TP
-.B "scratch"
-Scratch the playing track.
-If \fBid\fR is present it must match the playing track.
-.TP
-.B "volume"
-Change the volume by \fBdelta\fR, or if that is missing then set it to the
-values of \fBleft\fR and \fBright\fR.
-Expands to the \fBvolume.html\fR template rather than redirecting.
-.TP
-.B "prefs"
-Adjust preferences from the \fBprefs.html\fR template (which it then expands
-rather than redirecting).
-.IP
-If
-.B parts
-is set then the cooked interface is assumed.
-The value of
-.B parts
-is used to determine which trackname preferences are set.
-By default the
-.B display
-context is adjusted but this can be overridden with the
-.B context
-argument.
-Also the
-.B random
-argument is checked; if it is set then random play is enabled for that track,
-otherwise it is disabled.
-.IP
-Otherwise if the
-.B name
-and
-.B value
-arguments are set then they are used to set a single preference.
-.IP
-Otherwise if just the
-.B name
-argument is set then that preference is deleted.
-.IP
-It is recommended that links to the \fBprefs\fR action use \fB@resolve@\fR to
-enure that the real track name is always used.
-Otherwise if the preferences page is used to adjust a trackname_ preference,
-the alias may change, leading to the URL going stale.
-.TP
-.B "error"
-This action is generated automatically when an error occurs connecting to the
-server.
-The \fBerror\fR label is set to an indication of what the error is.
 .SH "TRACK NAME PARTS"
 The traditional track name parts are \fBartist\fR, \fBalbum\fR and \fBtitle\fR,
 with the obvious intended meaning.
@@ -1478,7 +867,8 @@ name and \fBext\fR which is the filename extension, including the initial dot
 (or the empty string if there is not extension).
 .SH "SEE ALSO"
 \fBdisorder\fR(1), \fBsox\fR(1), \fBdisorderd\fR(8), \fBdisorder\-dump\fR(8),
-\fBpcrepattern\fR(3)
+\fBpcrepattern\fR(3), \fBdisorder_templates\fR(5), \fBdisorder_actions\fR(5),
+\fBdisorder.cgi\fR(8)
 .\" Local Variables:
 .\" mode:nroff
 .\" fill-column:79
diff --git a/doc/disorder_options.5.in b/doc/disorder_options.5.in
new file mode 100644 (file)
index 0000000..62dd9c2
--- /dev/null
@@ -0,0 +1,81 @@
+.\"
+.\" Copyright (C) 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
+.\"
+.TH disorder_options 5
+.SH NAME
+pkgconfdir/options - DisOrder CGI actions
+.SH DESCRIPTION
+The DisOrder CGI reads much extra configuration information from
+\fIpkgconfdir/options\fR.
+The general syntax is the same as the main configuration file (see
+\fBdisorder_config\fR(5)).
+.SH DIRECTIVES
+Valid directives are:
+.TP
+.B columns \fINAME\fR \fIHEADING\fR...
+Defines the columns used in \fB@playing@\fR and \fB@recent@\fB.
+\fINAME\fR must be either \fBplaying\fR, \fBrecent\fR or \fBsearch\fR.
+\fIHEADING\fR...  is a list of heading names.
+If a column is defined more than once then the last definitions is used.
+.IP
+The heading names \fBbutton\fR, \fBlength\fR, \fBwhen\fR and \fBwho\fR
+are built in.
+.TP
+.B include \fIPATH\fR
+Includes another file.
+If \fIPATH\fR starts with a \fB/\fR then it is taken as is, otherwise
+it is searched for in \fIpkgconfdir\fR and \fIpkgdatadir\fR.
+.TP
+.B label \fINAME\fR \fIVALUE\fR
+Define a label.
+If a label is defined more than once then the last definition is used.
+.SH LABELS
+Some labels are defined inside \fBdisorder.cgi\fR and others by the
+default templates.
+You can define your own labels and use them inside a template.
+.PP
+When an undefined label is expanded, if it has a dot in its name then
+the part after the final dot is used as its value.
+Otherwise the whole name is used as the value.
+.PP
+Labels are not individually documented here, see the shipped
+\fIoptions.labels\fR file instead.
+.SH "OPTION FILES"
+The shipped \fIoptions\fR file includes four standard options files.
+In order, they are:
+.TP
+.I options.labels
+The default labels file.
+You wouldn't normally edit this directly - instead supply your own commands
+in \fIoptions.user\fR.
+Have a look at the shipped version of the file for documentation of
+labels used by the standard templates.
+.TP
+.I options.user
+A user options file.
+Here you should put any overrides for the default labels and any
+extra labels required by your modified templates.
+.SH "SEE ALSO"
+.BR disorder_config (5),
+.BR disorder_templates (5),
+.BR disorder_actions (5),
+.BR disorder.cgi (8)
+.\" Local Variables:
+.\" mode:nroff
+.\" fill-column:79
+.\" End:
diff --git a/doc/disorder_templates.5.head b/doc/disorder_templates.5.head
new file mode 100644 (file)
index 0000000..5efeef0
--- /dev/null
@@ -0,0 +1,116 @@
+.\"
+.\" Copyright (C) 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
+.\"
+.TH disorder_templates 5
+.SH NAME
+disorder_templates - DisOrder template file syntax
+.SH DESCRIPTION
+DisOrder template files are text files containing HTML documents, with an
+expansion syntax to enable data supplied by the implementation to be inserted.
+.SS "Expansion Syntax"
+An expansion starts with an at ("@") symbol and takes the form of an expansion
+name followed by zero or more arguments.
+.PP
+Expansion names may contain letters, digits or "-" (and must start with a
+letter or digit).
+No spacing is allowed between the "@" and the expansion name.
+.PP
+Each argument is bracketed
+Any of "(" and ")", "[" and "]" or "{" and "}" may be used but all arguments
+for a given expansion must use the same bracket pair.
+.PP
+Arguments may be separated from one another and the expansion name by
+whitespace (including newlines and even completely blank lines).
+The parser always reads as many arguments as are available, even if that is
+more than the expansion name can accept (so if an expansion is to be followed
+by an open bracket of the same kind it uses, you must use the \fB@_\fR
+separator; see below).
+.PP
+Arguments are expanded within themselves following the same rules, with a few
+exceptions discussed below.
+.SS "Special Symbols"
+A few sequences are special:
+.TP
+.B @@
+This expands to a single "@" sign.
+.TP
+.B @#
+This expands to nothing, and moreover removes the rest of the line it appears
+on and its trailing newline.
+It is intended to be used as a comment market but can also be used to eliminate
+newlines introduced merely to keep lines short.
+.TP
+.B @_
+This expands to nothing (but does not have the line-eating behaviour of
+\fB@#\fR).
+It is intended to be used to mark the end of an expansion where that would
+otherwise be ambiguous.
+.SS "Macros"
+It is possible to define new expansions using the \fB@define\fR expansion.  For
+example,
+.PP
+.nf
+@define{reverse}{a b}{@b @a}
+.fi
+.PP
+defines an expansion called \fB@reverse\fR which expands to its two arguments
+in reversed order.
+The input \fB@reverse{this}{that}\fR would therefore expand to "that this".
+.SS "Sub-Expansions"
+Many expansions expand their argument with additional expansions defined.
+For example, the \fB@playing\fR expansion expands its argument with the extra
+expansion \fB@id\fR defined as the ID of the playing track.
+.PP
+The scope of these sub-expansions is purely lexical.
+Therefore if you invoke a macro or include another template file, if the
+sub-expansions appear within it they will not be expanded.
+.PP
+In the case of a macro you can work around this by passing the value as an
+argument.
+Included files do not have arguments, so in this case you must rewrite the
+inclusion as a macro.
+.SS "Search Path"
+All template files are first searched for in \fIpkgconfdir\fR and then in
+\fIpkgdatadir\fR.
+.SS "macros.tmpl and user.tmpl"
+Before any template is expanded, the CGI will process \fBmacros.tmpl\fR and
+discard any output.
+This defines a collection of commonly used macros.
+.PP
+Following this the CGI will process \fBuser.tmpl\fR, again discarding output.
+This can be used to override the common macros without editing the installed
+version of \fBmacros.tmpl\fR, or to define new ones.
+.PP
+It is not an error if \fBuser.tmpl\fR does not exist.
+.SS "Character Encoding"
+The CGI does not (currently) declare any character encoding.
+This could be changed quite easily but in practice is not a pressing necessity.
+.PP
+The recommended approach is to treat the templates as ASCII files and if
+non-ASCII characters are required, use HTML entities to represent them.
+.PP
+For example, to represent the copyright sign, use \fB&copy;\fR or \fB&#xA9;\fR.
+.PP
+If you know the decimal or hex unicode value for a character then you can use
+\fB&#NNN;\fR or \fB&#xHHHH;\fR respectively.
+.SH EXPANSIONS
+.\" Local Variables:
+.\" mode:nroff
+.\" fill-column:79
+.\" End:
+
diff --git a/doc/disorder_templates.5.tail b/doc/disorder_templates.5.tail
new file mode 100644 (file)
index 0000000..e43e86f
--- /dev/null
@@ -0,0 +1,27 @@
+.\"
+.\" Copyright (C) 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
+.\"
+.SH "SEE ALSO"
+.BR disorder_actions (5),
+.BR disorder_options (5),
+.BR disorder_config (5),
+.BR disorder.cgi (8)
+.\" Local Variables:
+.\" mode:nroff
+.\" fill-column:79
+.\" End:
index ed488e9..b304e5a 100644 (file)
@@ -165,7 +165,8 @@ This prevents multiple instances of DisOrder running simultaneously.
 Current locale.
 See \fBlocale\fR(7).
 .SH "SEE ALSO"
-\fBdisorder\fR(1), \fBdisorder_config\fR(5), \fBdisorder\-dump\fR(8)
+\fBdisorder\fR(1), \fBdisorder_config\fR(5), \fBdisorder\-dump\fR(8),
+\fBdisorder.cgi\fR(8)
 .\" Local Variables:
 .\" mode:nroff
 .\" End:
index 5320060..9c9826f 100644 (file)
@@ -21,7 +21,8 @@
 TESTS=t-addr t-basen t-bits t-cache t-casefold t-cookies \
                t-filepart t-hash t-heap t-hex t-kvp t-mime t-printf \
                t-regsub t-selection t-signame t-sink t-split t-syscalls \
-               t-trackname t-unicode t-url t-utf8 t-vector t-words t-wstat
+               t-trackname t-unicode t-url t-utf8 t-vector t-words t-wstat \
+               t-macros t-cgi
 
 noinst_LIBRARIES=libdisorder.a
 include_HEADERS=disorder.h
@@ -41,6 +42,7 @@ libdisorder_a_SOURCES=charset.c charset.h             \
        base64.c base64.h                               \
        bits.c bits.h                                   \
        cache.c cache.h                                 \
+       cgi.c cgi.h                                     \
        client.c client.h                               \
        client-common.c client-common.h                 \
        configuration.c configuration.h                 \
@@ -59,6 +61,7 @@ libdisorder_a_SOURCES=charset.c charset.h             \
        kvp.c kvp.h                                     \
        log.c log.h log-impl.h                          \
        logfd.c logfd.h                                 \
+       macros.c macros-builtin.c macros.h              \
        mem.c mem.h mem-impl.h                          \
        mime.h mime.c                                   \
        mixer.c mixer.h mixer-oss.c mixer-alsa.c        \
@@ -140,6 +143,10 @@ t_casefold_SOURCES=t-casefold.c test.c test.h
 t_casefold_LDADD=libdisorder.a $(LIBPCRE) $(LIBICONV) $(LIBGC)
 t_casefold_DEPENDENCIES=libdisorder.a
 
+t_cgi_SOURCES=t-cgi.c test.c test.h
+t_cgi_LDADD=libdisorder.a $(LIBPCRE) $(LIBICONV) $(LIBGC)
+t_cgi_DEPENDENCIES=libdisorder.a
+
 t_cookies_SOURCES=t-cookies.c test.c test.h
 t_cookies_LDADD=libdisorder.a $(LIBPCRE) $(LIBICONV) $(LIBGC)
 t_cookies_DEPENDENCIES=libdisorder.a
@@ -164,6 +171,10 @@ t_kvp_SOURCES=t-kvp.c test.c test.h
 t_kvp_LDADD=libdisorder.a $(LIBPCRE) $(LIBICONV) $(LIBGC)
 t_kvp_DEPENDENCIES=libdisorder.a
 
+t_macros_SOURCES=t-macros.c test.c test.h
+t_macros_LDADD=libdisorder.a $(LIBPCRE) $(LIBICONV) $(LIBGC)
+t_macros_DEPENDENCIES=libdisorder.a
+
 t_mime_SOURCES=t-mime.c test.c test.h
 t_mime_LDADD=libdisorder.a $(LIBPCRE) $(LIBICONV) $(LIBGC)
 t_mime_DEPENDENCIES=libdisorder.a
@@ -239,4 +250,4 @@ rebuild-unicode:
 
 CLEANFILES=definitions.h definitions.h.new
 
-EXTRA_DIST=trackdb.c trackdb-stub.c
+EXTRA_DIST=trackdb.c trackdb-stub.c t-macros-1.tmpl t-macros-2
diff --git a/lib/cgi.c b/lib/cgi.c
new file mode 100644 (file)
index 0000000..c3a7680
--- /dev/null
+++ b/lib/cgi.c
@@ -0,0 +1,375 @@
+/*
+ * 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
+ */
+/** @file lib/cgi.c
+ * @brief CGI tools
+ */
+
+#include <config.h>
+#include "types.h"
+
+#include <stdlib.h>
+#include <string.h>
+#include <assert.h>
+#include <unistd.h>
+#include <errno.h>
+#include <stdio.h>
+
+#include "cgi.h"
+#include "mem.h"
+#include "log.h"
+#include "vector.h"
+#include "hash.h"
+#include "kvp.h"
+#include "mime.h"
+#include "unicode.h"
+#include "sink.h"
+
+/** @brief Hash of arguments */
+static hash *cgi_args;
+
+/** @brief Get CGI arguments from a GET request's query string */
+static struct kvp *cgi__init_get(void) {
+  const char *q;
+
+  if((q = getenv("QUERY_STRING")))
+    return kvp_urldecode(q, strlen(q));
+  error(0, "QUERY_STRING not set, assuming empty");
+  return NULL;
+}
+
+/** @brief Read the HTTP request body */
+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);
+  /* We check for overflow and also limit the input to 16MB. Lower
+   * would probably do.  */
+  if(!(n+1) || n > 16 * 1024 * 1024)
+    fatal(0, "input is much too large");
+  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;
+}
+
+/** @brief Called for each part header field (see cgi__part_callback()) */
+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;
+}
+
+/** @brief Called for each part (see cgi__init_multipart()) */
+static int cgi__part_callback(const char *s,
+                             void *u) {
+  char *name = 0;
+  struct kvp *k, **head = u;
+  
+  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 = *head;
+  k->name = name;
+  k->value = s;
+  *head = k;
+  return 0;
+}
+
+/** @brief Initialize CGI arguments from a multipart/form-data request body */
+static struct kvp *cgi__init_multipart(const char *boundary) {
+  char *q;
+  struct kvp *head = 0;
+  
+  cgi__input(&q, 0);
+  if(mime_multipart(q, cgi__part_callback, boundary, &head))
+    fatal(0, "invalid multipart object");
+  return head;
+}
+
+/** @brief Initialize CGI arguments from a POST request */
+static struct kvp *cgi__init_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);
+    return kvp_urldecode(q, n);
+  }
+  if(!strcmp(type, "multipart/form-data")) {
+    if(!(boundary = kvp_get(k, "boundary")))
+      fatal(0, "no boundary parameter found");
+    return cgi__init_multipart(boundary);
+  }
+  fatal(0, "unrecognized content type '%s'", type);
+}
+
+/** @brief Initialize CGI arguments
+ *
+ * Must be called before other cgi_ functions are used.
+ *
+ * This function can be called more than once, in which case it
+ * revisits the environment and (perhaps) standard input.  This is
+ * only intended to be used for testing, actual CGI applications
+ * should call it exactly once.
+ */
+void cgi_init(void) {
+  const char *p;
+  struct kvp *k;
+
+  cgi_args = hash_new(sizeof (char *));
+  if(!(p = getenv("REQUEST_METHOD")))
+    error(0, "REQUEST_METHOD not set, assuming GET");
+  if(!p || !strcmp(p, "GET"))
+    k = cgi__init_get();
+  else if(!strcmp(p, "POST"))
+    k = cgi__init_post();
+  else
+    fatal(0, "unknown request method %s", p);
+  /* Validate the arguments and put them in a hash */
+  for(; k; k = k->next) {
+    if(!utf8_valid(k->name, strlen(k->name))
+       || !utf8_valid(k->value, strlen(k->value)))
+      error(0, "invalid UTF-8 sequence in cgi argument %s", k->name);
+    else
+      hash_add(cgi_args, k->name, &k->value, HASH_INSERT_OR_REPLACE);
+    /* We just drop bogus arguments. */
+  }
+}
+
+/** @brief Get a CGI argument by name
+ *
+ * cgi_init() must be called first.  Names and values are all valid
+ * UTF-8 strings (and this is enforced at initialization time).
+ */
+const char *cgi_get(const char *name) {
+  const char **v = hash_find(cgi_args, name);
+
+  return v ? *v : NULL;
+}
+
+/** @brief Set a CGI argument */
+void cgi_set(const char *name, const char *value) {
+  value = xstrdup(value);
+  hash_add(cgi_args, name, &value, HASH_INSERT_OR_REPLACE);
+}
+
+/** @brief Clear CGI arguments */
+void cgi_clear(void) {
+  cgi_args = hash_new(sizeof (char *));
+}
+
+/** @brief Add SGML-style quoting
+ * @param src String to quote (UTF-8)
+ * @return Quoted string
+ *
+ * Quotes characters for insertion into HTML output.  Anything that is
+ * not a printable ASCII character will be converted to a numeric
+ * character references, as will '"', '&', '<' and '>' (since those
+ * have special meanings).
+ *
+ * Quoting everything down to ASCII means we don't care what the
+ * content encoding really is (as long as it's not anything insane
+ * like EBCDIC).
+ */
+char *cgi_sgmlquote(const char *src) {
+  uint32_t *ucs, c;
+  int n;
+  struct dynstr d[1];
+  struct sink *s;
+
+  if(!(ucs = utf8_to_utf32(src, strlen(src), 0)))
+    exit(1);
+  dynstr_init(d);
+  s = sink_dynstr(d);
+  n = 1;
+  /* format the string */
+  while((c = *ucs++)) {
+    switch(c) {
+    default:
+      if(c > 126 || c < 32) {
+      case '"':
+      case '&':
+      case '<':
+      case '>':
+       /* For simplicity we always use numeric character references
+        * even if a named reference is available. */
+       sink_printf(s, "&#%"PRIu32";", c);
+       break;
+      } else
+       sink_writec(s, (char)c);
+    }
+  }
+  dynstr_terminate(d);
+  return d->vec;
+}
+
+/** @brief Write a CGI attribute
+ * @param output Where to send output
+ * @param name Attribute name
+ * @param value Attribute value
+ */
+void cgi_attr(struct sink *output, const char *name, const char *value) {
+  /* Try to avoid needless quoting */
+  if(!value[strspn(value, "abcdefghijklmnopqrstuvwxyz"
+                  "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+                  "0123456789")])
+    sink_printf(output, "%s=%s", name, value);
+  else
+    sink_printf(output, "%s=\"%s\"", name, cgi_sgmlquote(value));
+}
+
+/** @brief Write an open tag
+ * @param output Where to send output
+ * @param name Element name
+ * @param ... Attribute name/value pairs
+ *
+ * The name/value pair list is terminated by a single (char *)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);
+  }
+  va_end(ap);
+  sink_printf(output, ">");
+}
+
+/** @brief Write a close tag
+ * @param output Where to send output
+ * @param name Element name
+ */
+void cgi_closetag(struct sink *output, const char *name) {
+  sink_printf(output, "</%s>", name);
+}
+
+/** @brief Construct a URL
+ * @param url Base URL
+ * @param ... Name/value pairs for constructed query string
+ * @return Constructed URL
+ *
+ * The name/value pair list is terminated by a single (char *)0.
+ */
+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;
+  }
+  va_end(ap);
+  *kk = 0;
+  if(kvp) {
+    dynstr_append(&d, '?');
+    dynstr_append_string(&d, kvp_urlencode(kvp, 0));
+  }
+  dynstr_terminate(&d);
+  return d.vec;
+}
+
+/** @brief Construct a URL from current parameters
+ * @param url Base URL
+ * @return Constructed URL
+ */
+char *cgi_thisurl(const char *url) {
+  struct dynstr d[1];
+  char **keys = hash_keys(cgi_args);
+  int n;
+
+  dynstr_init(d);
+  dynstr_append_string(d, url);
+  for(n = 0; keys[n]; ++n) {
+    dynstr_append(d, n ? '&' : '?');
+    dynstr_append_string(d, urlencodestring(keys[n]));
+    dynstr_append(d, '=');
+    dynstr_append_string(d, urlencodestring(cgi_get(keys[n])));
+  }
+  dynstr_terminate(d);
+  return d->vec;
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
diff --git a/lib/cgi.h b/lib/cgi.h
new file mode 100644 (file)
index 0000000..f8f5bf2
--- /dev/null
+++ b/lib/cgi.h
@@ -0,0 +1,49 @@
+/*
+ * 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
+ */
+/** @file lib/cgi.h
+ * @brief CGI tools
+ */
+
+#ifndef CGI_H
+#define CGI_H
+
+struct sink;
+
+void cgi_init(void);
+const char *cgi_get(const char *name);
+void cgi_set(const char *name, const char *value);
+char *cgi_sgmlquote(const char *src);
+void cgi_attr(struct sink *output, const char *name, const char *value);
+void cgi_opentag(struct sink *output, const char *name, ...);
+void cgi_closetag(struct sink *output, const char *name);
+char *cgi_makeurl(const char *url, ...);
+char *cgi_thisurl(const char *url);
+void cgi_clear(void);
+
+#endif
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
index c080b4d..7af7de0 100644 (file)
 #include "types.h"
 
 #include <string.h>
+#include <assert.h>
+#include <stdio.h>
 
 #include "filepart.h"
 #include "mem.h"
 
+/** @brief Parse a filename
+ * @param path Filename to parse
+ * @param Where to put directory name, or NULL
+ * @param Where to put basename, or NULL
+ */
+static void parse_filename(const char *path,
+                           char **dirnamep,
+                           char **basenamep) {
+  const char *s, *e = path + strlen(path);
+
+  /* Strip trailing slashes.  We never take these into account. */
+  while(e > path && e[-1] == '/')
+    --e;
+  if(e == path) {
+    /* The path is empty or contains only slashes */
+    if(*path) {
+      if(dirnamep)
+        *dirnamep = xstrdup("/");
+      if(basenamep)
+        *basenamep = xstrdup("/");
+    } else {
+      if(dirnamep)
+        *dirnamep = xstrdup("");
+      if(basenamep)
+        *basenamep = xstrdup("");
+    }
+  } else {
+    /* The path isn't empty and has more than just slashes.  e therefore now
+     * points at the end of the basename. */
+    s = e;
+    while(s > path && s[-1] != '/')
+      --s;
+    /* Now s points at the start of the basename */
+    if(basenamep)
+      *basenamep = xstrndup(s, e - s);
+    if(s > path) {
+      --s;
+      /* s must now be pointing at a '/' before the basename */
+      assert(*s == '/');
+      while(s > path && s[-1] == '/')
+        --s;
+      /* Now s must be pointing at the last '/' after the dirname */
+      assert(*s == '/');
+      if(s == path) {
+        /* If we reached the start we must be at the root */
+        if(dirnamep)
+          *dirnamep = xstrdup("/");
+      } else {
+        /* There's more than just the root here */
+        if(dirnamep)
+          *dirnamep = xstrndup(path, s - path);
+      }
+    } else {
+      /* There wasn't a slash */
+      if(dirnamep)
+        *dirnamep = xstrdup(".");
+    }
+  }
+}
+
 /** @brief Return the directory part of @p path
  * @param path Path to parse
  * @return Directory part of @p path
  *
  * Extracts the directory part of @p path.  This is a simple lexical
  * transformation and no canonicalization is performed.  The result will only
- * ever end "/" if it is the root directory.
+ * ever end "/" if it is the root directory.  The result will be "." if there
+ * is no directory part.
  */
 char *d_dirname(const char *path) {
-  const char *s;
+  char *d = 0;
 
-  if((s = strrchr(path, '/'))) {
-    while(s > path && s[-1] == '/')
-      --s;
-    if(s == path)
-      return xstrdup("/");
-    else
-      return xstrndup(path, s - path);
-  } else
-    return xstrdup(".");
+  parse_filename(path, &d, 0);
+  assert(d != 0);
+  return d;
+}
+
+/** @brief Return the basename part of @p path
+ * @param Path to parse
+ * @return Base part of @p path
+ *
+ * Extracts the base part of @p path.  This is a simple lexical transformation
+ * and no canonicalization is performed.  The result is always newly allocated
+ * even if compares equal to @p path.
+ */
+char *d_basename(const char *path) {
+  char *b = 0;
+
+  parse_filename(path, 0, &b);
+  assert(b != 0);
+  return b;
 }
 
 /** @brief Find the extension part of @p path
index fdc9074..9b6cf62 100644 (file)
@@ -24,6 +24,8 @@
 #ifndef FILEPART_H
 #define FILEPART_H
 
+char *d_basename(const char *path);
+
 char *d_dirname(const char *path);
 /* return the directory name part of @path@ */
 
index a77bd3a..f38e1d9 100644 (file)
@@ -26,6 +26,7 @@
 #include "hash.h"
 #include "mem.h"
 #include "log.h"
+#include "kvp.h"
 
 struct entry {
   struct entry *next;                   /* next entry same key */
index 484fda2..3dfb0ed 100644 (file)
@@ -22,6 +22,7 @@
 #define HASH_H
 
 typedef struct hash hash;
+struct kvp;
 
 hash *hash_new(size_t valuesize);
 /* Create a new hash */
diff --git a/lib/macros-builtin.c b/lib/macros-builtin.c
new file mode 100644 (file)
index 0000000..04ea490
--- /dev/null
@@ -0,0 +1,467 @@
+/*
+ * This file is part of DisOrder
+ * Copyright (C) 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
+ */
+
+/** @file lib/macros-builtin.c
+ * @brief Built-in expansions
+ *
+ * This is a grab-bag of non-domain-specific expansions.  Documentation will be
+ * generated from the comments at the head of each function.
+ */
+
+#include <config.h>
+#include "types.h"
+
+#include <stdio.h>
+#include <string.h>
+#include <errno.h>
+#include <assert.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
+
+#include "hash.h"
+#include "mem.h"
+#include "macros.h"
+#include "sink.h"
+#include "syscalls.h"
+#include "log.h"
+#include "wstat.h"
+#include "kvp.h"
+#include "split.h"
+#include "printf.h"
+#include "vector.h"
+#include "filepart.h"
+
+static struct vector include_path;
+
+/** @brief Return 1 if @p s is 'true' else 0 */
+int mx_str2bool(const char *s) {
+  return !strcmp(s, "true");
+}
+
+/** @brief Return "true" if @p n is nonzero else "false" */
+const char *mx_bool2str(int n) {
+  return n ? "true" : "false";
+}
+
+/** @brief Write a boolean result */
+int mx_bool_result(struct sink *output, int result) {
+  if(sink_writes(output, mx_bool2str(result)) < 0)
+    return -1;
+  else
+    return 0;
+}
+
+/** @brief Search the include path */
+char *mx_find(const char *name, int report) {
+  char *path;
+  int n;
+  
+  if(name[0] == '/') {
+    if(access(name, O_RDONLY) < 0) {
+      if(report)
+        error(errno, "cannot read %s", name);
+      return 0;
+    }
+    path = xstrdup(name);
+  } else {
+    /* Search the include path */
+    for(n = 0; n < include_path.nvec; ++n) {
+      byte_xasprintf(&path, "%s/%s", include_path.vec[n], name);
+      if(access(path, O_RDONLY) == 0)
+        break;
+    }
+    if(n >= include_path.nvec) {
+      if(report)
+        error(0, "cannot find '%s' in search path", name);
+      return 0;
+    }
+  }
+  return path;
+}
+
+/*! @include{TEMPLATE}@
+ *
+ * Includes TEMPLATE.
+ *
+ * TEMPLATE can be an absolute filename starting with a '/'; only the file with
+ * exactly this name will be included.
+ *
+ * Alternatively it can be a relative filename, not starting with a '/'.  In
+ * this case the file will be searched for in the include path.  When searching
+ * paths, unreadable files are treated as if they do not exist (rather than
+ * matching then producing an error).
+ *
+ * If the name chosen ends ".tmpl" then the file will be expanded as a
+ * template.  Anything else is included byte-for-byte without further
+ * modification.
+ *
+ * Only regular files are allowed (no devices, sockets or name pipes).
+ */
+static int exp_include(int attribute((unused)) nargs,
+                      char **args,
+                      struct sink *output,
+                      void *u) {
+  const char *path;
+  int fd, n;
+  char buffer[4096];
+  struct stat sb;
+
+  if(!(path = mx_find(args[0], 1/*report*/))) {
+    if(sink_printf(output, "[[cannot find '%s']]", args[0]) < 0)
+      return 0;
+    return 0;
+  }
+  /* If it's a template expand it */
+  if(strlen(path) >= 5 && !strncmp(path + strlen(path) - 5, ".tmpl", 5))
+    return mx_expand_file(path, output, u);
+  /* Read the raw file.  As with mx_expand_file() we insist that the file is a
+   * regular file. */
+  if((fd = open(path, O_RDONLY)) < 0)
+    fatal(errno, "error opening %s", path);
+  if(fstat(fd, &sb) < 0)
+    fatal(errno, "error statting %s", path);
+  if(!S_ISREG(sb.st_mode))
+    fatal(0, "%s: not a regular file", path);
+  while((n = read(fd, buffer, sizeof buffer)) > 0) {
+    if(sink_write(output, buffer, n) < 0) {
+      xclose(fd);
+      return -1;
+    }
+  }
+  if(n < 0)
+    fatal(errno, "error reading %s", path);
+  xclose(fd);
+  return 0;
+}
+
+/*! @include{COMMAND}@
+ *
+ * Executes COMMAND via the shell (using "sh -c") and copies its
+ * standard output to the template output.  The shell command output
+ * is not expanded or modified in any other way.
+ *
+ * The shell command's standard error is copied to the error log.
+ *
+ * If the shell exits nonzero then this is reported to the error log
+ * but otherwise no special action is taken.
+ */
+static int exp_shell(int attribute((unused)) nargs,
+                    char **args,
+                    struct 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");
+    }
+    if(output->write(output, buffer, n) < 0)
+      return -1;
+  }
+  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));
+  return 0;
+}
+
+/*! @if{CONDITION}{IF-TRUE}{IF-FALSE}@
+ *
+ * If CONDITION is "true" then evaluates to IF-TRUE.  Otherwise
+ * evaluates to IF-FALSE.  The IF-FALSE part is optional.
+ */
+static int exp_if(int nargs,
+                 const struct mx_node **args,
+                 struct sink *output,
+                 void *u) {
+  char *s;
+  int rc;
+
+  if((rc = mx_expandstr(args[0], &s, u, "argument #0 (CONDITION)")))
+    return rc;
+  if(mx_str2bool(s))
+    return mx_expand(args[1], output, u);
+  else if(nargs > 2)
+    return mx_expand(args[2], output, u);
+  else
+    return 0;
+}
+
+/*! @and{BRANCH}{BRANCH}...@
+ *
+ * Expands to "true" if all the branches are "true" otherwise to "false".  If
+ * there are no brances then the result is "true".  Only as many branches as
+ * necessary to compute the answer are evaluated (starting from the first one),
+ * so if later branches have side effects they may not take place.
+ */
+static int exp_and(int nargs,
+                  const struct mx_node **args,
+                  struct sink *output,
+                  void *u) {
+  int n, result = 1, rc;
+  char *s, *argname;
+
+  for(n = 0; n < nargs; ++n) {
+    byte_xasprintf(&argname, "argument #%d", n);
+    if((rc = mx_expandstr(args[n], &s, u, argname)))
+      return rc;
+    if(!mx_str2bool(s)) {
+      result = 0;
+      break;
+    }
+  }
+  return mx_bool_result(output, result);
+}
+
+/*! @or{BRANCH}{BRANCH}...@
+ *
+ * Expands to "true" if any of the branches are "true" otherwise to "false".
+ * If there are no brances then the result is "false".  Only as many branches
+ * as necessary to compute the answer are evaluated (starting from the first
+ * one), so if later branches have side effects they may not take place.
+ */
+static int exp_or(int nargs,
+                 const struct mx_node **args,
+                 struct sink *output,
+                 void *u) {
+  int n, result = 0, rc;
+  char *s, *argname;
+
+  for(n = 0; n < nargs; ++n) {
+    byte_xasprintf(&argname, "argument #%d", n);
+    if((rc = mx_expandstr(args[n], &s, u, argname)))
+      return rc;
+    if(mx_str2bool(s)) {
+      result = 1;
+      break;
+    }
+  }
+  return mx_bool_result(output, result);
+}
+
+/*! @not{CONDITION}@
+ *
+ * Expands to "true" unless CONDITION is "true" in which case "false".
+ */
+static int exp_not(int attribute((unused)) nargs,
+                  char **args,
+                  struct sink *output,
+                  void attribute((unused)) *u) {
+  return mx_bool_result(output, !mx_str2bool(args[0]));
+}
+
+/*! @#{...}@
+ *
+ * Expands to nothing.  The argument(s) are not fully evaluated, and no side
+ * effects occur.
+ */
+static int exp_comment(int attribute((unused)) nargs,
+                       const struct mx_node attribute((unused)) **args,
+                       struct sink attribute((unused)) *output,
+                       void attribute((unused)) *u) {
+  return 0;
+}
+
+/*! @urlquote{STRING}@
+ *
+ * URL-quotes a string, i.e. replaces any characters not safe to use unquoted
+ * in a URL with %-encoded form.
+ */
+static int exp_urlquote(int attribute((unused)) nargs,
+                        char **args,
+                        struct sink *output,
+                        void attribute((unused)) *u) {
+  if(sink_writes(output, urlencodestring(args[0])) < 0)
+    return -1;
+  else
+    return 0;
+}
+
+/*! @eq{S1}{S2}...@
+ *
+ * Expands to "true" if all the arguments are identical, otherwise to "false"
+ * (i.e. if any pair of arguments differs).
+ *
+ * If there are no arguments then expands to "true".  Evaluates all arguments
+ * (with their side effects) even if that's not strictly necessary to discover
+ * the result.
+ */
+static int exp_eq(int nargs,
+                 char **args,
+                 struct sink *output,
+                 void attribute((unused)) *u) {
+  int n, result = 1;
+  
+  for(n = 1; n < nargs; ++n) {
+    if(strcmp(args[n], args[0])) {
+      result = 0;
+      break;
+    }
+  }
+  return mx_bool_result(output, result);
+}
+
+/*! @ne{S1}{S2}...@
+ *
+ * Expands to "true" if all of the arguments differ from one another, otherwise
+ * to "false" (i.e. if any value appears more than once).
+ *
+ * If there are no arguments then expands to "true".  Evaluates all arguments
+ * (with their side effects) even if that's not strictly necessary to discover
+ * the result.
+ */
+static int exp_ne(int nargs,
+                 char **args,
+                 struct sink *output,
+                 void  attribute((unused))*u) {
+  hash *h = hash_new(sizeof (char *));
+  int n, result = 1;
+
+  for(n = 0; n < nargs; ++n)
+    if(hash_add(h, args[n], "", HASH_INSERT)) {
+      result = 0;
+      break;
+    }
+  return mx_bool_result(output, result);
+}
+
+/*! @discard{...}@
+ *
+ * Expands to nothing.  Unlike the comment expansion @#{...}, side effects of
+ * arguments are not suppressed.  So this can be used to surround a collection
+ * of macro definitions with whitespace, free text commentary, etc.
+ */
+static int exp_discard(int attribute((unused)) nargs,
+                       char attribute((unused)) **args,
+                       struct sink attribute((unused)) *output,
+                       void attribute((unused)) *u) {
+  return 0;
+}
+
+/*! @define{NAME}{ARG1 ARG2...}{DEFINITION}@
+ *
+ * Define a macro.  The macro will be called NAME and will act like an
+ * expansion.  When it is expanded, the expansion is replaced by DEFINITION,
+ * with each occurence of @ARG1@ etc replaced by the parameters to the
+ * expansion.
+ */
+static int exp_define(int attribute((unused)) nargs,
+                      const struct mx_node **args,
+                      struct sink attribute((unused)) *output,
+                      void attribute((unused)) *u) {
+  char **as, *name, *argnames;
+  int rc, nas;
+  
+  if((rc = mx_expandstr(args[0], &name, u, "argument #0 (NAME)")))
+    return rc;
+  if((rc = mx_expandstr(args[1], &argnames, u, "argument #1 (ARGS)")))
+    return rc;
+  as = split(argnames, &nas, 0, 0, 0);
+  mx_register_macro(name, nas, as, args[2]);
+  return 0;
+}
+
+/*! @basename{PATH}
+ *
+ * Expands to the UNQUOTED basename of PATH.
+ */
+static int exp_basename(int attribute((unused)) nargs,
+                        char **args,
+                        struct sink attribute((unused)) *output,
+                        void attribute((unused)) *u) {
+  return sink_writes(output, d_basename(args[0])) < 0 ? -1 : 0;
+}
+
+/*! @dirname{PATH}
+ *
+ * Expands to the UNQUOTED directory name of PATH.
+ */
+static int exp_dirname(int attribute((unused)) nargs,
+                       char **args,
+                       struct sink attribute((unused)) *output,
+                       void attribute((unused)) *u) {
+  return sink_writes(output, d_dirname(args[0])) < 0 ? -1 : 0;
+}
+
+/*! @q{STRING}
+ *
+ * Expands to STRING.
+ */
+static int exp_q(int attribute((unused)) nargs,
+                 char **args,
+                 struct sink attribute((unused)) *output,
+                 void attribute((unused)) *u) {
+  return sink_writes(output, args[0]) < 0 ? -1 : 0;
+}
+
+/** @brief Register built-in expansions */
+void mx_register_builtin(void) {
+  mx_register("basename", 1, 1, exp_basename);
+  mx_register("dirname", 1, 1, exp_dirname);
+  mx_register("discard", 0, INT_MAX, exp_discard);
+  mx_register("eq", 0, INT_MAX, exp_eq);
+  mx_register("include", 1, 1, exp_include);
+  mx_register("ne", 0, INT_MAX, exp_ne);
+  mx_register("not", 1, 1, exp_not);
+  mx_register("shell", 1, 1, exp_shell);
+  mx_register("urlquote", 1, 1, exp_urlquote);
+  mx_register("q", 1, 1, exp_q);
+  mx_register_magic("#", 0, INT_MAX, exp_comment);
+  mx_register_magic("and", 0, INT_MAX, exp_and);
+  mx_register_magic("define", 3, 3, exp_define);
+  mx_register_magic("if", 2, 3, exp_if);
+  mx_register_magic("or", 0, INT_MAX, exp_or);
+}
+
+/** @brief Add a directory to the search path
+ * @param s Directory to add
+ */
+void mx_search_path(const char *s) {
+  vector_append(&include_path, xstrdup(s));
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
diff --git a/lib/macros.c b/lib/macros.c
new file mode 100644 (file)
index 0000000..4d3b9a0
--- /dev/null
@@ -0,0 +1,705 @@
+/*
+ * This file is part of DisOrder
+ * Copyright (C) 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
+ */
+
+/** @file lib/macros.c
+ * @brief Macro expansion
+ */
+
+#include <config.h>
+#include "types.h"
+
+#include <string.h>
+#include <ctype.h>
+#include <assert.h>
+#include <stdio.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <errno.h>
+
+#include "hash.h"
+#include "macros.h"
+#include "mem.h"
+#include "vector.h"
+#include "log.h"
+#include "sink.h"
+#include "syscalls.h"
+#include "printf.h"
+
+VECTOR_TYPE(mx_node_vector, const struct mx_node *, xrealloc);
+
+/** @brief Definition of an expansion */
+struct expansion {
+  /** @brief Minimum permitted arguments */
+  int min;
+
+  /** @brief Maximum permitted arguments */
+  int max;
+
+  /** @brief Flags
+   *
+   * See:
+   * - @ref EXP_SIMPLE
+   * - @ref EXP_MAGIC
+   * - @ref EXP_MACRO
+   * - @ref EXP_TYPE_MASK
+   */
+  unsigned flags;
+
+  /** @brief Macro argument names */
+  char **args;
+
+  /** @brief Callback (cast to appropriate type)
+   *
+   * Cast to @ref mx_simple_callback or @ref mx_magic_callback as required. */
+  void (*callback)();
+
+  /** @brief Macro definition
+   *
+   * Only for @ref EXP_MACRO expansions. */
+  const struct mx_node *definition;
+};
+
+/** @brief Expansion takes pre-expanded strings
+ *
+ * @p callback is cast to @ref mx_simple_callback. */
+#define EXP_SIMPLE 0x0000
+
+/** @brief Expansion takes parsed templates, not strings
+ *
+ * @p callback is cast to @ref mx_magic_callback.  The callback must do its own
+ * expansion e.g. via mx_expandstr() where necessary. */
+#define EXP_MAGIC 0x0001
+
+/** @brief Expansion is a macro */
+#define EXP_MACRO 0x0002
+
+/** @brief Mask of types */
+#define EXP_TYPE_MASK 0x0003
+
+/** @brief Hash of all expansions
+ *
+ * Created by mx_register(), mx_register_macro() or mx_register_magic().
+ */
+static hash *expansions;
+
+static int mx__expand_macro(const struct expansion *e,
+                            const struct mx_node *m,
+                            struct sink *output,
+                            void *u);
+
+/* Parsing ------------------------------------------------------------------ */
+
+static int next_non_whitespace(const char *input,
+                               const char *end) {
+  while(input < end && isspace((unsigned char)*input))
+    ++input;
+  return input < end ? *input : -1;
+}
+
+/** @brief Parse a template
+ * @param filename Input filename (for diagnostics)
+ * @param line Line number (use 1 on initial call)
+ * @param input Start of text to parse
+ * @param end End of text to parse or NULL
+ * @return Pointer to parse tree root node
+ *
+ * Parses the text in [start, end) and returns an (immutable) parse
+ * tree representing it.
+ *
+ * If @p end is NULL then the whole string is parsed.
+ *
+ * Note that the @p filename value stored in the parse tree is @p filename,
+ * i.e. it is not copied.
+ */
+const struct mx_node *mx_parse(const char *filename,
+                              int line,
+                              const char *input,
+                              const char *end) {
+  int braces, argument_start_line, obracket, cbracket;
+  const char *argument_start, *argument_end;
+  struct mx_node_vector v[1];
+  struct dynstr d[1];
+  struct mx_node *head = 0, **tailp = &head, *e;
+
+  if(!end)
+    end = input + strlen(input);
+  while(input < end) {
+    if(*input != '@') {
+      e = xmalloc(sizeof *e);
+      e->next = 0;
+      e->filename = filename;
+      e->line = line;
+      e->type = MX_TEXT;
+      dynstr_init(d);
+      /* Gather up text without any expansions in. */
+      while(input < end && *input != '@') {
+       if(*input == '\n')
+         ++line;
+       dynstr_append(d, *input++);
+      }
+      dynstr_terminate(d);
+      e->text = d->vec;
+      *tailp = e;
+      tailp = &e->next;
+      continue;
+    }
+    if(input + 1 < end)
+      switch(input[1]) {
+      case '@':
+        /* '@@' expands to '@' */
+        e = xmalloc(sizeof *e);
+        e->next = 0;
+        e->filename = filename;
+        e->line = line;
+        e->type = MX_TEXT;
+        e->text = "@";
+        *tailp = e;
+        tailp = &e->next;
+        input += 2;
+        continue;
+      case '#':
+        /* '@#' starts a (newline-eating comment), like dnl */
+        input += 2;
+        while(input < end && *input != '\n')
+          ++input;
+        if(*input == '\n') {
+          ++line;
+          ++input;
+        }
+        continue;
+      case '_':
+        /* '@_' expands to nothing.  It's there to allow dump to terminate
+         * expansions without having to know what follows. */
+        input += 2;
+        continue;
+      }
+    /* It's a full expansion */
+    ++input;
+    e = xmalloc(sizeof *e);
+    e->next = 0;
+    e->filename = filename;
+    e->line = line;
+    e->type = MX_EXPANSION;
+    /* Collect the expansion name.  Expansion names start with an alnum and
+     * consist of alnums and '-'.  We don't permit whitespace between the '@'
+     * and the name. */
+    dynstr_init(d);
+    if(input == end)
+      fatal(0, "%s:%d: invalid expansion syntax (truncated)",
+            filename, e->line);
+    if(!isalnum((unsigned char)*input))
+      fatal(0, "%s:%d: invalid expansion syntax (unexpected %#x)",
+            filename, e->line, (unsigned char)*input);
+    while(input < end && (isalnum((unsigned char)*input) || *input == '-'))
+      dynstr_append(d, *input++);
+    dynstr_terminate(d);
+    e->name = d->vec;
+    /* See what the bracket character is */
+    obracket = next_non_whitespace(input, end);
+    switch(obracket) {
+    case '(': cbracket = ')'; break;
+    case '[': cbracket = ']'; break;
+    case '{': cbracket = '}'; break;
+    default: cbracket = obracket = -1; break;      /* no arguments */
+    }
+    mx_node_vector_init(v);
+    if(obracket >= 0) {
+      /* Gather up arguments */
+      while(next_non_whitespace(input, end) == obracket) {
+        while(isspace((unsigned char)*input)) {
+          if(*input == '\n')
+            ++line;
+          ++input;
+        }
+        ++input;                        /* the bracket */
+        braces = 0;
+        /* Find the end of the argument */
+        argument_start = input;
+        argument_start_line = line;
+        while(input < end && (*input != cbracket || braces > 0)) {
+          const int c = *input++;
+
+          if(c == obracket)
+            ++braces;
+          else if(c == cbracket)
+            --braces;
+          else if(c == '\n')
+            ++line;
+        }
+        if(input >= end) {
+          /* We ran out of input without encountering a balanced cbracket */
+         fatal(0, "%s:%d: unterminated expansion argument '%.*s'",
+               filename, argument_start_line,
+               (int)(input - argument_start), argument_start);
+        }
+        /* Consistency check */
+        assert(*input == cbracket);
+        /* Record the end of the argument */
+        argument_end = input;
+        /* Step over the cbracket */
+       ++input;
+        /* Now we have an argument in [argument_start, argument_end), and we
+         * know its filename and initial line number.  This is sufficient to
+         * parse it. */
+        mx_node_vector_append(v, mx_parse(filename, argument_start_line,
+                                          argument_start, argument_end));
+      }
+    }
+    /* Guarantee a NULL terminator (for the case where there's more than one
+     * argument) */
+    mx_node_vector_terminate(v);
+    /* Fill in the remains of the node */
+    e->nargs = v->nvec;
+    e->args = v->vec;
+    *tailp = e;
+    tailp = &e->next;
+  }
+  return head;
+}
+
+static void mx__dump(struct dynstr *d, const struct mx_node *m) {
+  int n;
+  const struct mx_node *mm;
+
+  if(!m)
+    return;
+  switch(m->type) {
+  case MX_TEXT:
+    if(m->text[0] == '@')
+      dynstr_append(d, '@');
+    dynstr_append_string(d, m->text);
+    break;
+  case MX_EXPANSION:
+    dynstr_append(d, '@');
+    dynstr_append_string(d, m->name);
+    for(n = 0; n < m->nargs; ++n) {
+      dynstr_append(d, '{');
+      mx__dump(d, m->args[n]);
+      dynstr_append(d, '}');
+    }
+    /* If the next non-whitespace is '{', add @_ to stop it being
+     * misinterpreted */
+    mm = m->next;
+    while(mm && mm->type == MX_TEXT) {
+      switch(next_non_whitespace(mm->text, mm->text + strlen(mm->text))) {
+      case -1:
+        mm = mm->next;
+        continue;
+      case '{':
+        dynstr_append_string(d, "@_");
+        break;
+      default:
+        break;
+      }
+      break;
+    }
+    break;
+  default:
+    assert(!"invalid m->type");
+  }
+  mx__dump(d, m->next);
+}
+
+/** @brief Dump a parse macro expansion to a string
+ *
+ * Not of production quality!  Only intended for testing!
+ */
+char *mx_dump(const struct mx_node *m) {
+  struct dynstr d[1];
+
+  dynstr_init(d);
+  mx__dump(d, m);
+  dynstr_terminate(d);
+  return d->vec;
+}
+
+/* Expansion registration --------------------------------------------------- */
+
+static int mx__register(unsigned flags,
+                        const char *name,
+                        int min,
+                        int max,
+                        char **args,
+                        void (*callback)(),
+                        const struct mx_node *definition) {
+  struct expansion e[1];
+
+  if(!expansions)
+    expansions = hash_new(sizeof(struct expansion));
+  e->min = min;
+  e->max = max;
+  e->flags = flags;
+  e->args = args;
+  e->callback = callback;
+  e->definition = definition;
+  return hash_add(expansions, name, &e, HASH_INSERT_OR_REPLACE);
+}
+
+/** @brief Register a simple expansion rule
+ * @param name Name
+ * @param min Minimum number of arguments
+ * @param max Maximum number of arguments
+ * @param callback Callback to write output
+ */
+void mx_register(const char *name,
+                 int min,
+                 int max,
+                 mx_simple_callback *callback) {
+  mx__register(EXP_SIMPLE,  name, min, max, 0, (void (*)())callback, 0);
+}
+
+/** @brief Register a magic expansion rule
+ * @param name Name
+ * @param min Minimum number of arguments
+ * @param max Maximum number of arguments
+ * @param callback Callback to write output
+ */
+void mx_register_magic(const char *name,
+                       int min,
+                       int max,
+                       mx_magic_callback *callback) {
+  mx__register(EXP_MAGIC, name, min, max, 0, (void (*)())callback, 0);
+}
+
+/** @brief Register a macro
+ * @param name Name
+ * @param nargs Number of arguments
+ * @param args Argument names
+ * @param definition Macro definition
+ * @return 0 on success, negative on error
+ */
+int mx_register_macro(const char *name,
+                      int nargs,
+                      char **args,
+                      const struct mx_node *definition) {
+  if(mx__register(EXP_MACRO, name, nargs, nargs, args,  0/*callback*/,
+                  definition)) {
+#if 0
+    /* This locates the error to the definition, which may be a line or two
+     * beyond the @define command itself.  The backtrace generated by
+     * mx_expand() may help more. */
+    error(0, "%s:%d: duplicate definition of '%s'",
+          definition->filename, definition->line, name);
+#endif
+    return -2;
+  }
+  return 0;
+}
+
+/* Expansion ---------------------------------------------------------------- */
+
+/** @brief Expand a template
+ * @param m Where to start
+ * @param output Where to send output
+ * @param u User data
+ * @return 0 on success, non-0 on error
+ *
+ * Interpretation of return values:
+ * - 0 means success
+ * - -1 means an error writing to the sink.
+ * - other negative values mean errors generated from with the macro
+ *   expansion system
+ * - positive values are reserved for the application
+ *
+ * If any callback returns non-zero then that value is returned, abandoning
+ * further expansion.
+ */
+int mx_expand(const struct mx_node *m,
+              struct sink *output,
+              void *u) {
+  const struct expansion *e;
+  int rc;
+
+  if(!m)
+    return 0;
+  switch(m->type) {
+  case MX_TEXT:
+    if(sink_writes(output, m->text) < 0)
+      return -1;
+    break;
+  case MX_EXPANSION:
+    rc = 0;
+    if(!(e = hash_find(expansions, m->name))) {
+      error(0, "%s:%d: unknown expansion name '%s'",
+            m->filename, m->line, m->name);
+      if(sink_printf(output, "[['%s' unknown]]", m->name) < 0)
+        return -1;
+    } else if(m->nargs < e->min) {
+      error(0, "%s:%d: expansion '%s' requires %d args, only %d given",
+            m->filename, m->line, m->name, e->min, m->nargs);
+      if(sink_printf(output, "[['%s' too few args]]", m->name) < 0)
+        return -1;
+    } else if(m->nargs > e->max) {
+      error(0, "%s:%d: expansion '%s' takes at most %d args, but %d given",
+            m->filename, m->line, m->name, e->max, m->nargs);
+      if(sink_printf(output, "[['%s' too many args]]", m->name) < 0)
+        return -1;
+    } else switch(e->flags & EXP_TYPE_MASK) {
+      case EXP_MAGIC: {
+        /* Magic callbacks we can call directly */
+        rc = ((mx_magic_callback *)e->callback)(m->nargs,
+                                                m->args,
+                                                output,
+                                                u);
+        break;
+      }
+      case EXP_SIMPLE: {
+        /* For simple callbacks we expand their arguments for them. */
+        char **args = xcalloc(1 + m->nargs, sizeof (char *)), *argname;
+        int n;
+        
+        for(n = 0; n < m->nargs; ++n) {
+          /* Argument numbers are at least clear from looking at the text;
+           * adding names as well would be nice.  TODO */
+          byte_xasprintf(&argname, "argument #%d", n);
+          if((rc = mx_expandstr(m->args[n], &args[n], u, argname)))
+            break;
+        }
+        if(!rc) {
+          args[n] = NULL;
+          rc = ((mx_simple_callback *)e->callback)(m->nargs,
+                                                   args,
+                                                   output,
+                                                   u);
+        }
+        break;
+      }
+      case EXP_MACRO: {
+        /* Macros we expand by rewriting their definition with argument values
+         * substituted and then expanding that. */
+        rc = mx__expand_macro(e, m, output, u);
+        break;
+      }
+      default:
+        assert(!"impossible EXP_TYPE_MASK value");
+    }
+    if(rc) {
+      /* For non-IO errors we generate some backtrace */
+      if(rc != -1)
+        error(0,  "  ...in @%s at %s:%d",
+              m->name, m->filename, m->line);
+      return rc;
+    }
+    break;
+  default:
+    assert(!"invalid m->type");
+  }
+  return mx_expand(m->next, output, u);
+}
+
+/** @brief Expand a template storing the result in a string
+ * @param m Where to start
+ * @param sp Where to store string
+ * @param u User data
+ * @param what Token for backtrace, or NULL
+ * @return 0 on success, non-0 on error
+ *
+ * Same return conventions as mx_expand().  This wrapper is slightly more
+ * convenient to use from 'magic' expansions.
+ */
+int mx_expandstr(const struct mx_node *m,
+                 char **sp,
+                 void *u,
+                 const char *what) {
+  struct dynstr d[1];
+  int rc;
+
+  dynstr_init(d);
+  if(!(rc = mx_expand(m, sink_dynstr(d), u))) {
+    dynstr_terminate(d);
+    *sp = d->vec;
+  } else
+    *sp = 0;
+  if(rc && rc != -1 && what)
+    error(0, "  ...in %s at %s:%d", what, m->filename, m->line);
+  return rc;
+}
+
+/** @brief Expand a template file
+ * @param path Filename
+ * @param output Where to send output
+ * @param u User data
+ * @return 0 on success, non-0 on error
+ *
+ * Same return conventions as mx_expand().
+ */
+int mx_expand_file(const char *path,
+                   struct sink *output,
+                   void *u) {
+  int fd, n, rc;
+  struct stat sb;
+  char *b;
+  off_t sofar;
+  const struct mx_node *m;
+
+  if((fd = open(path, O_RDONLY)) < 0)
+    fatal(errno, "error opening %s", path);
+  if(fstat(fd, &sb) < 0)
+    fatal(errno, "error statting %s", path);
+  if(!S_ISREG(sb.st_mode))
+    fatal(0, "%s: not a regular file", path);
+  sofar = 0;
+  b = xmalloc_noptr(sb.st_size);
+  while(sofar < sb.st_size) {
+    n = read(fd, b + sofar, sb.st_size - sofar);
+    if(n > 0)
+      sofar += n;
+    else if(n == 0)
+      fatal(0, "unexpected EOF reading %s", path);
+    else if(errno != EINTR)
+      fatal(errno, "error reading %s", path);
+  }
+  xclose(fd);
+  m = mx_parse(path, 1, b, b + sb.st_size);
+  rc = mx_expand(m, output, u);
+  if(rc && rc != -1)
+    /* Mention inclusion in backtrace */
+    error(0, "  ...in inclusion of file '%s'", path);
+  return rc;
+}
+
+/* Macros ------------------------------------------------------------------- */
+
+/** @brief Rewrite a parse tree substituting sub-expansions
+ * @param m Parse tree to rewrite (from macro definition)
+ * @param ... Name/value pairs to rewrite
+ * @return Rewritten parse tree
+ *
+ * The name/value pair list consists of pairs of strings and is terminated by
+ * (char *)0.  Names and values are both copied so need not survive the call.
+ */
+const struct mx_node *mx_rewritel(const struct mx_node *m,
+                                  ...) {
+  va_list ap;
+  hash *h = hash_new(sizeof (struct mx_node *));
+  const char *n, *v;
+  struct mx_node *e;
+
+  va_start(ap, m);
+  while((n = va_arg(ap, const char *))) {
+    v = va_arg(ap, const char *);
+    e = xmalloc(sizeof *e);
+    e->next = 0;
+    e->filename = m->filename;
+    e->line = m->line;
+    e->type = MX_TEXT;
+    e->text = xstrdup(v);
+    hash_add(h, n, &e, HASH_INSERT);
+    /* hash_add() copies n */
+  }
+  return mx_rewrite(m, h);
+}
+
+/** @brief Rewrite a parse tree substituting in macro arguments
+ * @param definition Parse tree to rewrite (from macro definition)
+ * @param h Hash mapping argument names to argument values
+ * @return Rewritten parse tree
+ */
+const struct mx_node *mx_rewrite(const struct mx_node *definition,
+                                 hash *h) {
+  const struct mx_node *head = 0, **tailp = &head, *argvalue, *m, *mm, **ap;
+  struct mx_node *nm;
+  int n;
+  
+  for(m = definition; m; m = m->next) {
+    switch(m->type) {
+    case MX_TEXT:
+      nm = xmalloc(sizeof *nm);
+      *nm = *m;                          /* Dumb copy of text node fields */
+      nm->next = 0;                      /* Maintain list structure */
+      *tailp = nm;
+      tailp = (const struct mx_node **)&nm->next;
+      break;
+    case MX_EXPANSION:
+      if(m->nargs == 0
+         && (ap = hash_find(h, m->name))) {
+        /* This expansion has no arguments and its name matches one of the
+         * macro arguments.  (Even if it's a valid expansion name we override
+         * it.)  We insert its value at this point.  We do NOT recursively
+         * rewrite the argument's value - it is outside the lexical scope of
+         * the argument name.
+         *
+         * We need to recreate the list structure but a shallow copy will
+         * suffice here.
+         */
+        argvalue = *ap;
+        for(mm = argvalue; mm; mm = mm->next) {
+          nm = xmalloc(sizeof *nm);
+          *nm = *mm;
+          nm->next = 0;
+          *tailp = nm;
+          tailp = (const struct mx_node **)&nm->next;
+        }
+      } else {
+        /* This is some other expansion.  We recursively rewrite its argument
+         * values according to h. */
+        nm = xmalloc(sizeof *nm);
+        *nm = *m;
+        nm->args = xcalloc(nm->nargs, sizeof (struct mx_node *));
+        for(n = 0; n < nm->nargs; ++n)
+          nm->args[n] = mx_rewrite(m->args[n], h);
+        nm->next = 0;
+        *tailp = nm;
+        tailp = (const struct mx_node **)&nm->next;
+      }
+      break;
+    default:
+      assert(!"invalid m->type");
+    }
+  }
+  *tailp = 0;                           /* Mark end of list */
+  return head;
+}
+
+/** @brief Expand a macro
+ * @param e Macro definition
+ * @param m Macro expansion
+ * @param output Where to send output
+ * @param u User data
+ * @return 0 on success, non-0 on error
+ */
+static int mx__expand_macro(const struct expansion *e,
+                            const struct mx_node *m,
+                            struct sink *output,
+                            void *u) {
+  hash *h = hash_new(sizeof (struct mx_node *));
+  int n;
+
+  /* We store the macro arguments in a hash.  Currently there is no check for
+   * duplicate argument names (and this would be the wrong place for it
+   * anyway); if you do that you just lose in some undefined way. */
+  for(n = 0; n < m->nargs; ++n)
+    hash_add(h, e->args[n], &m->args[n], HASH_INSERT);
+  /* Generate a rewritten parse tree */
+  m = mx_rewrite(e->definition, h);
+  /* Expand the result */
+  return mx_expand(m, output, u);
+  /* mx_expand() will update the backtrace */
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
diff --git a/lib/macros.h b/lib/macros.h
new file mode 100644 (file)
index 0000000..e9b0af5
--- /dev/null
@@ -0,0 +1,140 @@
+/*
+ * This file is part of DisOrder
+ * Copyright (C) 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
+ */
+
+/** @file lib/macros.h
+ * @brief Macro expansion
+ */
+
+#ifndef MACROS_H
+#define MACROS_H
+
+struct sink;
+
+/** @brief One node in a macro expansion parse tree */
+struct mx_node {
+  /** @brief Next element or NULL at end of list */
+  struct mx_node *next;
+
+  /** @brief Node type, @ref MX_TEXT or @ref MX_EXPANSION. */
+  int type;
+
+  /** @brief Filename containing this node */
+  const char *filename;
+  
+  /** @brief Line number at start of this node */
+  int line;
+  
+  /** @brief Plain text (if @p type is @ref MX_TEXT) */
+  const char *text;
+
+  /** @brief Expansion name (if @p type is @ref MX_EXPANSION) */
+  const char *name;
+
+  /** @brief Argument count (if @p type is @ref MX_EXPANSION) */
+  int nargs;
+
+  /** @brief Argument values, parsed recursively (or NULL if @p nargs is 0) */
+  const struct mx_node **args;
+};
+
+/** @brief Text node */
+#define MX_TEXT 0
+
+/** @brief Expansion node */
+#define MX_EXPANSION 1
+
+const struct mx_node *mx_parse(const char *filename,
+                              int line,
+                              const char *input,
+                              const char *end);
+char *mx_dump(const struct mx_node *m);
+
+
+/** @brief Callback for simple expansions
+ * @param nargs Number of arguments
+ * @param args Pointer to array of arguments
+ * @param output Where to send output
+ * @param u User data
+ * @return 0 on success, non-zero on error
+ */
+typedef int mx_simple_callback(int nargs,
+                               char **args,
+                               struct sink *output,
+                               void *u);
+
+/** @brief Callback for magic expansions
+ * @param nargs Number of arguments
+ * @param args Pointer to array of arguments
+ * @param output Where to send output
+ * @param u User data
+ * @return 0 on success, non-zero on error
+ */
+typedef int mx_magic_callback(int nargs,
+                              const struct mx_node **args,
+                              struct sink *output,
+                              void *u);
+
+void mx_register(const char *name,
+                 int min,
+                 int max,
+                 mx_simple_callback *callback);
+void mx_register_magic(const char *name,
+                       int min,
+                       int max,
+                       mx_magic_callback *callback);
+int mx_register_macro(const char *name,
+                      int nargs,
+                      char **args,
+                      const struct mx_node *definition);
+
+void mx_register_builtin(void);
+void mx_search_path(const char *s);
+char *mx_find(const char *name, int report);
+
+int mx_expand_file(const char *path,
+                   struct sink *output,
+                   void *u);
+int mx_expand(const struct mx_node *m,
+              struct sink *output,
+              void *u);
+int mx_expandstr(const struct mx_node *m,
+                 char **sp,
+                 void *u,
+                 const char *what);
+const struct mx_node *mx_rewrite(const struct mx_node *definition,
+                                 hash *h);
+const struct mx_node *mx_rewritel(const struct mx_node *m,
+                                  ...);
+
+int mx_str2bool(const char *s);
+const char *mx_bool2str(int n);
+int mx_bool_result(struct sink *output, int result);
+
+#endif /* MACROS_H */
+
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
index da41426..5db7585 100644 (file)
@@ -381,15 +381,19 @@ int mime_multipart(const char *s,
   int ret;
 
   /* We must start with a boundary string */
-  if(!isboundary(s, boundary, bl))
+  if(!isboundary(s, boundary, bl)) {
+    error(0, "mime_multipart: first line is not the boundary string");
     return -1;
+  }
   /* Keep going until we hit a final boundary */
   while(!isfinal(s, boundary, bl)) {
     s = strstr(s, "\r\n") + 2;
     start = s;
     while(!isboundary(s, boundary, bl)) {
-      if(!(e = strstr(s, "\r\n")))
+      if(!(e = strstr(s, "\r\n"))) {
+       error(0, "mime_multipart: line does not end CRLF");
        return -1;
+      }
       s = e + 2;
     }
     if((ret = callback(xstrndup(start,
index a3e9257..e904005 100644 (file)
@@ -131,6 +131,22 @@ struct sink *sink_dynstr(struct dynstr *output) {
   return (struct sink *)s;
 }
 
+/* discard sink **************************************************************/
+
+static int sink_discard_write(struct sink attribute((unused)) *s,
+                             const void attribute((unused)) *buffer,
+                             int nbytes) {
+  return nbytes;
+}
+
+/** @brief Return a sink which discards all output */
+struct sink *sink_discard(void) {
+  struct sink *s = xmalloc(sizeof *s);
+
+  s->write = sink_discard_write;
+  return s;
+}
+
 /*
 Local Variables:
 c-basic-offset:2
index cde7161..15462ae 100644 (file)
@@ -50,6 +50,9 @@ struct sink *sink_stdio(const char *name, FILE *fp);
 struct sink *sink_dynstr(struct dynstr *output);
 /* return a sink which appends to @output@. */
 
+struct sink *sink_discard(void);
+/* reutrn a sink which junks everything */
+
 int sink_vprintf(struct sink *s, const char *fmt, va_list ap);
 int sink_printf(struct sink *s, const char *fmt, ...)
   attribute((format (printf, 2, 3)));
diff --git a/lib/t-cgi.c b/lib/t-cgi.c
new file mode 100644 (file)
index 0000000..de44fcb
--- /dev/null
@@ -0,0 +1,158 @@
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 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 "test.h"
+#include "cgi.h"
+
+static void input_from(const char *s) {
+  FILE *fp = tmpfile();
+  char buffer[64];
+
+  if(fputs(s, fp) < 0
+     || fputs("wibble wibble\r\nspong", fp) < 0 /* ensure CONTENT_LENGTH
+                                                 * honored */
+     || fflush(fp) < 0)
+    fatal(errno, "writing to temporary file");
+  rewind(fp);
+  xdup2(fileno(fp), 0);
+  lseek(0, 0/*offset*/, SEEK_SET);
+  snprintf(buffer, sizeof buffer, "%zu", strlen(s));
+  setenv("CONTENT_LENGTH", buffer, 1);
+}
+
+static void test_cgi(void) {
+  struct dynstr d[1];
+
+  setenv("REQUEST_METHOD", "GET", 1);
+  setenv("QUERY_STRING", "foo=bar&a=b+c&c=x%7ey", 1);
+  cgi_init();
+  check_string(cgi_get("foo"), "bar");
+  check_string(cgi_get("a"), "b c");
+  check_string(cgi_get("c"), "x~y");
+
+  setenv("REQUEST_METHOD", "POST", 1);
+  setenv("CONTENT_TYPE", "application/x-www-form-urlencoded", 1);
+  unsetenv("QUERY_STRING");
+  input_from("foo=xbar&a=xb+c&c=xx%7ey");
+  cgi_init();
+  check_string(cgi_get("foo"), "xbar");
+  check_string(cgi_get("a"), "xb c");
+  check_string(cgi_get("c"), "xx~y");
+
+  /* This input string generated by Firefox 2.0.0.14 */
+  input_from("-----------------------------16128946562344073111198667379\r\n"
+             "Content-Disposition: form-data; name=\"input1\"\r\n"
+             "\r\n"
+             "normal input\r\n"
+             "-----------------------------16128946562344073111198667379\r\n"
+             "Content-Disposition: form-data; name=\"input2\"\r\n"
+             "\r\n"
+             "with pound sign: \xC2\xA3\r\n"
+             "-----------------------------16128946562344073111198667379\r\n"
+             "Content-Disposition: form-data; name=\"input3\"\r\n"
+             "\r\n"
+             "hidden input\r\n"
+             "-----------------------------16128946562344073111198667379\r\n"
+             "Content-Disposition: form-data; name=\"submit\"\r\n"
+             "\r\n"
+             "Submit Query\r\n"
+             "-----------------------------16128946562344073111198667379--\r\n");
+  setenv("CONTENT_TYPE", "multipart/form-data; boundary=---------------------------16128946562344073111198667379", 1);
+  unsetenv("QUERY_STRING");
+  cgi_init();
+  check_string(cgi_get("input1"), "normal input");
+  check_string(cgi_get("input2"), "with pound sign: \xC2\xA3");
+  check_string(cgi_get("input3"), "hidden input");
+  check_string(cgi_get("submit"), "Submit Query");
+
+  input_from("-----------------------------33919340914020259251659146591\r\n"
+             "Content-Disposition: form-data; name=\"text\"\r\n"
+             "\r\n"
+             "Text with\r\n"
+             "multiple lines\r\n"
+             "and trailing spaces \r\n"
+             "---and leading dashes\r\n"
+             "and pound sign \xC2\xA3\r\n"
+             "\r\n"
+             "-----------------------------33919340914020259251659146591\r\n"
+             "Content-Disposition: form-data; name=\"empty\"\r\n"
+             "\r\n"
+             "\r\n"
+             "-----------------------------33919340914020259251659146591\r\n"
+             "Content-Disposition: form-data; name=\"submit\"\r\n"
+             "\r\n"
+             "Submit Query\r\n"
+             "-----------------------------33919340914020259251659146591--\r\n");
+  setenv("CONTENT_TYPE", "multipart/form-data; boundary=---------------------------33919340914020259251659146591", 1);
+  cgi_init();
+  check_string(cgi_get("text"), ("Text with\r\n"
+                                 "multiple lines\r\n"
+                                 "and trailing spaces \r\n"
+                                 "---and leading dashes\r\n"
+                                 "and pound sign \xC2\xA3\r\n"
+                                 ""));
+  check_string(cgi_get("empty"), "");
+  
+  check_string(cgi_sgmlquote("foobar"), "foobar");
+  check_string(cgi_sgmlquote("<wibble>"), "&#60;wibble&#62;");
+  check_string(cgi_sgmlquote("\"&\""), "&#34;&#38;&#34;");
+  check_string(cgi_sgmlquote("\xC2\xA3"), "&#163;");
+
+  dynstr_init(d);
+  cgi_opentag(sink_dynstr(d), "element",
+             "foo", "bar",
+             "foo", "has space",
+             "foo", "has \"quotes\"",
+             (char *)NULL);
+  dynstr_terminate(d);
+  check_string(d->vec, "<element foo=bar foo=\"has space\" foo=\"has &#34;quotes&#34;\">");
+  
+  dynstr_init(d);
+  cgi_opentag(sink_dynstr(d), "element",
+             "foo", (char *)NULL,
+             (char *)NULL);
+  dynstr_terminate(d);
+  check_string(d->vec, "<element foo>");
+  
+  dynstr_init(d);
+  cgi_closetag(sink_dynstr(d), "element");
+  dynstr_terminate(d);
+  check_string(d->vec, "</element>");
+
+  check_string(cgi_makeurl("http://example.com/", (char *)NULL),
+              "http://example.com/");
+  check_string(cgi_makeurl("http://example.com/",
+                          "foo", "bar",
+                          "a", "b c",
+                          "d", "f=g+h",
+                          (char *)NULL),
+              "http://example.com/?foo=bar&a=b%20c&d=f%3dg%2bh");
+  
+}
+
+TEST(cgi);
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
index d2e741b..3eff101 100644 (file)
  */
 #include "test.h"
 
+#define check_filepart(PATH, DIR, BASE) do {            \
+  char *d = d_dirname(PATH), *b = d_basename(PATH);     \
+                                                        \
+  if(strcmp(d, DIR)) {                                  \
+    fprintf(stderr, "%s:%d: d_dirname failed:\n"        \
+            "    path: %s\n"                            \
+            "     got: %s\n"                            \
+            "expected: %s\n",                           \
+            __FILE__, __LINE__,                         \
+            PATH, d, DIR);                              \
+    count_error();                                      \
+  }                                                     \
+  if(strcmp(b, BASE)) {                                 \
+    fprintf(stderr, "%s:%d: d_basename failed:\n"       \
+            "    path: %s\n"                            \
+            "     got: %s\n"                            \
+            "expected: %s\n",                           \
+            __FILE__, __LINE__,                         \
+            PATH, d, DIR);                              \
+    count_error();                                      \
+  }                                                     \
+} while(0)
+
 static void test_filepart(void) {
-  check_string(d_dirname("/"), "/");
-  check_string(d_dirname("////"), "/");
-  check_string(d_dirname("/spong"), "/");
-  check_string(d_dirname("////spong"), "/");
-  check_string(d_dirname("/foo/bar"), "/foo");
-  check_string(d_dirname("////foo/////bar"), "////foo");
-  check_string(d_dirname("./bar"), ".");
-  check_string(d_dirname(".//bar"), ".");
-  check_string(d_dirname("."), ".");
-  check_string(d_dirname(".."), ".");
-  check_string(d_dirname("../blat"), "..");
-  check_string(d_dirname("..//blat"), "..");
-  check_string(d_dirname("wibble"), ".");
+  check_filepart("", "", "");
+  check_filepart("/", "/", "/");
+  check_filepart("////", "/", "/");
+  check_filepart("/spong", "/", "spong");
+  check_filepart("/spong/", "/", "spong");
+  check_filepart("/spong//", "/", "spong");
+  check_filepart("////spong", "/", "spong");
+  check_filepart("/foo/bar", "/foo", "bar");
+  check_filepart("/foo/bar/", "/foo", "bar");
+  check_filepart("////foo/////bar", "////foo", "bar");
+  check_filepart("./bar", ".", "bar");
+  check_filepart(".//bar", ".", "bar");
+  check_filepart(".", ".", ".");
+  check_filepart("..", ".", "..");
+  check_filepart("../blat", "..", "blat");
+  check_filepart("..//blat", "..", "blat");
+  check_filepart("wibble", ".", "wibble");
   check_string(extension("foo.c"), ".c");
   check_string(extension(".c"), ".c");
   check_string(extension("."), ".");
diff --git a/lib/t-macros-1.tmpl b/lib/t-macros-1.tmpl
new file mode 100644 (file)
index 0000000..9a3d4e8
--- /dev/null
@@ -0,0 +1 @@
+@if{true}{yes}{no}
diff --git a/lib/t-macros-2 b/lib/t-macros-2
new file mode 100644 (file)
index 0000000..15363c9
--- /dev/null
@@ -0,0 +1 @@
+wibble
diff --git a/lib/t-macros.c b/lib/t-macros.c
new file mode 100644 (file)
index 0000000..a1dc0e0
--- /dev/null
@@ -0,0 +1,257 @@
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 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 "test.h"
+#include "macros.h"
+
+static void test_macros(void) {
+  const struct mx_node *m;
+#define L1 "this is just some\n"
+#define L2 "plain text\n"
+  static const char plain[] = L1 L2;
+  char *s;
+  const char *cs;
+
+  /* Plain text ------------------------------------------------------------- */
+  
+  /* As simple as it gets */
+  m = mx_parse("plaintext1", 1, "", NULL);
+  insist(m == 0);
+
+  /* Almost as simple as that */
+  m = mx_parse("plaintext1", 1, plain, NULL);
+  check_integer(m->type, MX_TEXT);
+  check_string(m->filename, "plaintext1");
+  check_integer(m->line, 1);
+  check_string(m->text, L1 L2);
+  insist(m->next == 0);
+  check_string(mx_dump(m), plain);
+
+  /* Check that partial parses stop in the right place */
+  m = mx_parse("plaintext2", 5, plain, plain + strlen(L1));
+  check_integer(m->type, MX_TEXT);
+  check_string(m->filename, "plaintext2");
+  check_integer(m->line, 5);
+  check_string(m->text, L1);
+  insist(m->next == 0);
+  check_string(mx_dump(m), L1);
+
+  /* Simple macro parsing --------------------------------------------------- */
+
+  /* The simplest possible expansion */
+  m = mx_parse("macro1", 1, "@macro", NULL);
+  check_integer(m->type, MX_EXPANSION);
+  check_string(m->filename, "macro1");
+  check_integer(m->line, 1);
+  check_string(m->name, "macro");
+  check_integer(m->nargs, 0);
+  insist(m->next == 0);
+  check_string(mx_dump(m), "@macro");
+
+  m = mx_parse("macro2", 1, "@macro    ", NULL);
+  check_integer(m->type, MX_EXPANSION);
+  check_string(m->filename, "macro2");
+  check_integer(m->line, 1);
+  check_string(m->name, "macro");
+  check_integer(m->nargs, 0);
+  insist(m->next != 0);
+  check_integer(m->next->type, MX_TEXT);
+  check_string(mx_dump(m), "@macro    ");
+
+  /* Multiple bracketed arguments */
+  m = mx_parse("macro7", 1, "@macro{arg1}{arg2}", NULL);
+  check_string(mx_dump(m), "@macro{arg1}{arg2}");
+
+  m = mx_parse("macro8", 1, "@macro{\narg1}{\narg2}", NULL);
+  check_string(mx_dump(m), "@macro{\narg1}{\narg2}");
+  check_integer(m->args[0]->line, 1);
+  check_integer(m->args[1]->line, 2);
+  /* ...yes, lines 1 and 2: the first character of the first arg is
+   * the \n at the end of line 1.  Compare with macro9: */
+
+  m = mx_parse("macro9", 1, "@macro\n{arg1}\n{arg2}", NULL);
+  check_string(mx_dump(m), "@macro{arg1}{arg2}");
+  check_integer(m->args[0]->line, 2);
+  check_integer(m->args[1]->line, 3);
+
+  /* Arguments that themselves contain expansions */
+  m = mx_parse("macro10", 1, "@macro{@macro2{arg1}{arg2}}", NULL);
+  check_string(mx_dump(m), "@macro{@macro2{arg1}{arg2}}");
+
+  /* ...and with omitted trailing @ */
+  m = mx_parse("macro11", 1, "@macro{@macro2{arg1}{arg2}}", NULL);
+  check_string(mx_dump(m), "@macro{@macro2{arg1}{arg2}}");
+
+  /* Similarly but with more whitespace; NB that the whitespace is
+   * preserved. */
+  m = mx_parse("macro12", 1, "@macro {@macro2 {arg1} {arg2}  }\n", NULL);
+  check_string(mx_dump(m), "@macro{@macro2{arg1}{arg2}  }\n");
+
+  /* Simple expansions ------------------------------------------------------ */
+
+  mx_register_builtin();
+  mx_search_path(".");
+  mx_search_path("lib");
+  if((cs = getenv("srcdir")))
+    mx_search_path(cs);
+  
+#define check_macro(NAME, INPUT, OUTPUT, RET) do {              \
+  m = mx_parse(NAME, 1, INPUT, NULL);                           \
+  check_integer(mx_expandstr(m, &s, 0/*u*/, NAME), (RET));      \
+  if(s && strcmp(s, OUTPUT)) {                                  \
+    fprintf(stderr, "%s:%d: test %s\n"                          \
+            "     INPUT:\n%s\n"                                 \
+            "  EXPECTED: '%s'\n"                                \
+            "       GOT: '%s'\n",                               \
+            __FILE__, __LINE__, NAME, INPUT, OUTPUT, s);        \
+    count_error();                                              \
+  }                                                             \
+} while(0)
+
+  check_macro("empty", "", "", 0);
+  check_macro("plain", plain, plain, 0);
+  check_macro("quote1", "@@", "@", 0);
+  check_macro("quote2", "@@@@", "@@", 0);
+  check_macro("nothing1", "@_", "", 0);
+  check_macro("nothing2", "<@_>", "<>", 0);
+
+  check_macro("if1", "@if{true}{yes}{no}", "yes", 0);
+  check_macro("if2", "@if{true}{yes}", "yes", 0);
+  check_macro("if3", "@if{false}{yes}{no}", "no", 0);
+  check_macro("if4", "@if{false}{yes}", "", 0);
+  check_macro("if5", "@if{ true}{yes}", "", 0);
+  check_macro("if6", "@if{true}{yes}@_{wible}t", "yes{wible}t", 0);
+
+  check_macro("br1", "@if(true)(yes)(no)", "yes", 0);
+  check_macro("br1", "@if[true][yes]{no}", "yes{no}", 0);
+  
+  check_macro("and1", "@and", "true", 0);
+  check_macro("and2", "@and{true}", "true", 0);
+  check_macro("and3", "@and{false}", "false", 0);
+  check_macro("and4", "@and{true}{true}", "true", 0);
+  check_macro("and5", "@and{false}{true}", "false", 0);
+  check_macro("and6", "@and{true}{false}", "false", 0);
+  check_macro("and7", "@and{false}{false}", "false", 0);
+
+  check_macro("or1", "@or", "false", 0);
+  check_macro("or2", "@or{true}", "true", 0);
+  check_macro("or2", "@or{false}", "false", 0);
+  check_macro("or3", "@or{true}{true}", "true", 0);
+  check_macro("or4", "@or{false}{true}", "true", 0);
+  check_macro("or5", "@or{true}{false}", "true", 0);
+  check_macro("or7", "@or{false}{false}", "false", 0);
+
+  check_macro("not1", "@not{true}", "false", 0);
+  check_macro("not2", "@not{false}", "true", 0);
+  check_macro("not3", "@not{wibble}", "true", 0);
+
+  check_macro("comment1", "@# wibble\n", "", 0);
+  check_macro("comment2", "@# comment\nplus a line", "plus a line", 0);
+
+  check_macro("discard1", "@discard{wibble}", "", 0);
+  check_macro("discard2", "@discard{comment with a\nnewline in}", "", 0);
+
+  check_macro("eq1", "@eq", "true", 0);
+  check_macro("eq2", "@eq{}", "true", 0);
+  check_macro("eq3", "@eq{a}", "true", 0);
+  check_macro("eq4", "@eq{a}{a}", "true", 0);
+  check_macro("eq5", "@eq{a}{a}{a}", "true", 0);
+  check_macro("eq7", "@eq{a}{b}", "false", 0);
+  check_macro("eq8", "@eq{a}{b}{a}", "false", 0);
+  check_macro("eq9", "@eq{a}{a}{b}", "false", 0);
+  check_macro("eq10", "@eq{b}{a}{a}", "false", 0);
+
+  check_macro("ne1", "@ne", "true", 0);
+  check_macro("ne2", "@ne{}", "true", 0);
+  check_macro("ne3", "@ne{a}", "true", 0);
+  check_macro("ne4", "@ne{a}{a}", "false", 0);
+  check_macro("ne5", "@ne{a}{a}{a}", "false", 0);
+  check_macro("ne7", "@ne{a}{b}", "true", 0);
+  check_macro("ne8", "@ne{a}{b}{a}", "false", 0);
+  check_macro("ne9", "@ne{a}{a}{b}", "false", 0);
+  check_macro("ne10", "@ne{b}{a}{a}", "false", 0);
+  check_macro("ne11", "@ne{a}{b}{c}", "true", 0);
+
+  check_macro("sh1", "@shell{true}", "", 0);
+  check_macro("sh2", "@shell{echo spong}", "spong\n", 0);
+  fprintf(stderr, ">>> expect error message about shell command:\n");
+  check_macro("sh3", "@shell{echo spong;exit 3}", "spong\n", 0);
+
+  check_macro("url1", "@urlquote{unreserved}", "unreserved", 0);
+  check_macro("url2", "@urlquote{has space}", "has%20space", 0);
+  check_macro("url3", "@urlquote{\xc0\xc1}", "%c0%c1", 0);
+
+  check_macro("include1", "@include{t-macros-1.tmpl}",
+              "yes\n", 0);
+  check_macro("include2", "@include{t-macros-2}",
+              "wibble\n", 0);
+  fprintf(stderr, ">>> expect error message about t-macros-nonesuch:\n");
+  check_macro("include3", "<@include{t-macros-nonesuch}>",
+              "<[[cannot find 't-macros-nonesuch']]>", 0);
+  fprintf(stderr, ">>> expect error message about 'wibble':\n");
+  check_macro("badex1", "<@wibble>",
+              "<[['wibble' unknown]]>", 0);
+  fprintf(stderr, ">>> expect error message about 'if':\n");
+  check_macro("badex2", "<@if>",
+              "<[['if' too few args]]>", 0);
+  fprintf(stderr, ">>> expect error message about 'if':\n");
+  check_macro("badex3", "<@if{1}{2}{3}{4}{5}>",
+              "<[['if' too many args]]>", 0);
+
+  check_macro("dirname1", "@dirname{foo/bar}", "foo", 0);
+  check_macro("dirname2", "@dirname{foo & something/bar}",
+              "foo & something", 0);
+  check_macro("basename1", "@basename{xyzzy/plugh}", "plugh", 0);
+  check_macro("basename2", "@basename{xyzzy/a<b}", "a<b", 0);
+
+  check_macro("q1", "@q{wibble}", "wibble", 0);
+  check_macro("q2", "@q{wibble}wobble", "wibblewobble", 0);
+  
+  /* Macro definitions ------------------------------------------------------ */
+
+  check_macro("macro1", "@define{m}{a b c}{@c @b @a}@#\n"
+              "@m{1}{2}{3}",
+              "3 2 1", 0);
+  check_macro("macro2", "@m{b}{c}{a}",
+              "a c b", 0);
+  check_macro("macro3", "@m{@eq{z}{z}}{p}{q}",
+              "q p true", 0);
+  check_macro("macro4",
+              "@discard{\n"
+              "  @define{n}{a b c}\n"
+              "    {@if{@eq{@a}{@b}} {@c} {no}}\n"
+              "}@#\n"
+              "@n{x}{y}{z}",
+              "no", 0);
+  check_macro("macro5",
+              "@n{x}{x}{z}",
+              "z", 0);
+
+}
+
+TEST(macros);
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
index 65000c7..7f56c5d 100644 (file)
@@ -189,7 +189,37 @@ static void test_mime(void) {
                "Content-Description: jpeg-1\r\n"
                "\r\n"
                "<jpeg data>");
+
+  /* Bogus inputs to mime_multipart() */
+  fprintf(stderr, "expect two mime_multipart errors:\n");
+  insist(mime_multipart("--inner\r\n"
+                        "Content-Type: text/plain\r\n"
+                        "Content-Disposition: inline\r\n"
+                        "Content-Description: text-part-2\r\n"
+                        "\r\n"
+                        "Some more text here.\r\n"
+                        "\r\n"
+                        "--inner\r\n"
+                        "Content-Type: image/jpeg\r\n"
+                        "Content-Disposition: attachment\r\n"
+                        "Content-Description: jpeg-1\r\n"
+                        "\r\n"
+                        "<jpeg data>\r\n",
+                        test_multipart_callback,
+                        "inner",
+                        parts) == -1);
+  insist(mime_multipart("--wrong\r\n"
+                        "Content-Type: text/plain\r\n"
+                        "Content-Disposition: inline\r\n"
+                        "Content-Description: text-part-2\r\n"
+                        "\r\n"
+                        "Some more text here.\r\n"
+                        "\r\n"
+                        "--inner--\r\n",
+                        test_multipart_callback,
+                        "inner",
+                        parts) == -1);
+    
   /* XXX mime_parse */
 
   check_string(mime_qp(""), "");
index 63d7260..f5dd036 100644 (file)
 /** @file lib/test.c @brief Library tests */
 
 #include "test.h"
+#include "version.h"
+#include <getopt.h>
 
 long long tests, errors;
 int fail_first;
+int verbose;
 
 void count_error(void) {
   ++errors;
@@ -34,7 +37,7 @@ const char *format(const char *s) {
   struct dynstr d;
   int c;
   char buf[10];
-  
+
   dynstr_init(&d);
   while((c = (unsigned char)*s++)) {
     if(c >= ' ' && c <= '~')
@@ -52,7 +55,7 @@ const char *format_utf32(const uint32_t *s) {
   struct dynstr d;
   uint32_t c;
   char buf[64];
-  
+
   dynstr_init(&d);
   while((c = *s++)) {
     sprintf(buf, " %04lX", (long)c);
@@ -90,6 +93,46 @@ const char *do_printf(const char *fmt, ...) {
   return s;
 }
 
+static const struct option options[] = {
+  { "verbose", no_argument, 0, 'v' },
+  { "fail-first", no_argument, 0, 'F' },
+  { "help", no_argument, 0, 'h' },
+  { "version", no_argument, 0, 'V' },
+};
+
+/* display usage message and terminate */
+static void help(void) {
+  xprintf("Usage:\n"
+         "  %s [OPTIONS]\n"
+         "Options:\n"
+         "  --help, -h               Display usage message\n"
+         "  --version, -V            Display version number\n"
+          "  --verbose, -v            Verbose output\n"
+          "  --fail-first, -F         Stop on first failure\n",
+          progname);
+  xfclose(stdout);
+  exit(0);
+}
+
+void test_init(int argc, char **argv) {
+  int n;
+
+  set_progname(argv);
+  mem_init();
+  while((n = getopt_long(argc, argv, "vFhV", options, 0)) >= 0) {
+    switch(n) {
+    case 'v': verbose = 1; break;
+    case 'F': fail_first = 1; break;
+    case 'h': help();
+    case 'V': version(progname);
+    default: exit(1);
+    }
+  }
+  if(getenv("FAIL_FIRST"))
+    fail_first = 1;
+}
+
+
 /*
 Local Variables:
 c-basic-offset:2
index 3c43da0..7a0ef5d 100644 (file)
@@ -72,6 +72,7 @@
 
 extern long long tests, errors;
 extern int fail_first;
+extern int verbose;
 
 /** @brief Checks that @p expr is nonzero */
 #define insist(expr) do {                              \
@@ -130,13 +131,13 @@ const char *format(const char *s);
 const char *format_utf32(const uint32_t *s);
 uint32_t *ucs4parse(const char *s);
 const char *do_printf(const char *fmt, ...);
+void test_init(int argc, char **argv);
 
 #define TEST(name)                                                      \
-   int main(void) {                                                     \
-    mem_init();                                                         \
-    fail_first = !!getenv("FAIL_FIRST");                                \
+  int main(int argc, char **argv) {                                     \
+    test_init(argc, argv);                                              \
     test_##name();                                                      \
-    if(errors)                                                          \
+    if(errors || verbose)                                               \
       fprintf(stderr, "test_"#name": %lld errors out of %lld tests\n",  \
               errors, tests);                                           \
     return !!errors;                                                    \
index 2080e63..9125c0c 100644 (file)
@@ -1387,6 +1387,60 @@ void trackdb_stats_subprocess(ev_source *ev,
   ev_reader_new(ev, p[0], stats_read, stats_error, d, "disorder-stats reader");
 }
 
+/** @brief Parse a track name part preference
+ * @param name Preference name
+ * @param partp Where to store part name
+ * @param contextp Where to store context name
+ * @return 0 on success, non-0 if parse fails
+ */
+static int trackdb__parse_namepref(const char *name,
+                                   char **partp,
+                                   char **contextp) {
+  char *c;
+  static const char prefix[] = "trackname_";
+  
+  if(strncmp(name, prefix, strlen(prefix)))
+    return -1;                          /* not trackname_* at all */
+  name += strlen(prefix);
+  /* There had better be a _ between context and part */
+  c = strchr(name, '_');
+  if(!c)
+    return -1;
+  /* Context is first in the pref name even though most APIs have the part
+   * first.  Confusing; sorry. */
+  *contextp = xstrndup(name, c - name);
+  ++c;
+  /* There had better NOT be a second _ */
+  if(strchr(c, '_'))
+    return -1;
+  *partp = xstrdup(c);
+  return 0;
+}
+
+/** @brief Compute the default value for a track preference
+ * @param track Track name
+ * @param name Preference name
+ * @return Default value or 0 if none/not known
+ */
+static const char *trackdb__default(const char *track, const char *name) {
+  char *context, *part;
+  
+  if(!trackdb__parse_namepref(name, &part, &context)) {
+    /* We can work out the default for a trackname_ pref */
+    return trackname_part(track, context, part);
+  } else if(!strcmp(name, "weight")) {
+    /* We know the default weight */
+    return "90000";
+  } else if(!strcmp(name, "pick_at_random")) {
+    /* By default everything is eligible for picking at random */
+    return "1";
+  } else if(!strcmp(name, "tags")) {
+    /* By default everything no track has any tags */
+    return "";
+  }
+  return 0;
+}
+
 /* set a pref (remove if value=0) */
 int trackdb_set(const char *track,
                 const char *name,
@@ -1395,9 +1449,15 @@ int trackdb_set(const char *track,
   DB_TXN *tid;
   int err, cmp;
   char *oldalias, *newalias, **oldtags = 0, **newtags;
+  const char *def;
 
+  /* If the value matches the default then unset instead, to keep the database
+   * tidy.  Older versions did not have this feature so your database may yet
+   * have some default values stored in it. */
   if(value) {
-    /* TODO: if value matches default then set value=0 */
+    def = trackdb__default(track, name);
+    if(def && !strcmp(value, def))
+      value = 0;
   }
 
   for(;;) {
index 6d19911..16f3257 100644 (file)
--- a/lib/url.c
+++ b/lib/url.c
@@ -44,6 +44,10 @@ char *infer_url(void) {
   const char *scheme = "http", *server, *script, *e, *request_uri;
   char *url;
   int port;
+
+  /* mod_ssl sets HTTPS=on if the scheme is https */
+  if((e = getenv("HTTPS")) && !strcmp(e, "on"))
+    scheme = "https";
   
   /* Figure out the server.  'MUST' be set and we don't cope if it
    * is not. */
index 620c8f4..81574db 100644 (file)
@@ -26,6 +26,6 @@ SEDFILES=setup teardown
 include ${top_srcdir}/scripts/sedfiles.make
 
 EXTRA_DIST=htmlman sedfiles.make text2c oggrename make-unidata \
-       format-gcov-report make-version-string setup.in teardown.in
+       format-gcov-report make-version-string setup.in teardown.in macro-docs
 
 CLEANFILES=$(SEDFILES)
index fa966dd..17fc7f3 100755 (executable)
@@ -43,13 +43,13 @@ title=$(basename $1)
 echo "<html>"
 echo " <head>"
 if $stdhead; then
-  echo "@include{stdhead}@"
+  echo "@quiethead@#"
 fi
 echo "  <title>$title</title>"
 echo " </head>"
 echo " <body>"
 if $stdhead; then
-  echo "@include{topbar}@"
+  echo "@stdmenu{}@#"
 fi
 printf "   <pre class=manpage>"
 # this is kind of painful using only BREs
@@ -67,7 +67,7 @@ nroff -Tascii -man "$1" | ${GNUSED} \
                        s!</\([bi]\)><\1>!!g'
 echo "</pre>"
 if $stdhead; then
-  echo "@include{topbarend}@"
+  echo "@credits"
 fi
 echo " </body>"
 echo "</html>"
diff --git a/scripts/macro-docs b/scripts/macro-docs
new file mode 100755 (executable)
index 0000000..51586ea
--- /dev/null
@@ -0,0 +1,78 @@
+#! /usr/bin/perl -w
+use strict;
+
+my %macros = ();
+my $name;
+my $docs;
+while(defined($_ = <>)) {
+  chomp;
+  if(!defined $name and m,^/\*! (\@?([a-z\-]+).*),) {
+    $name = $2;
+    my $heading = $1;
+    $docs = [$heading];
+    $macros{$name} = $docs;
+    next;
+  }
+  if(defined $name) {
+    # Identify and strip trailing */
+    my $last = m,\*/ *$,;
+    s,\*/ *$,,;
+    # Strip trailing spaces
+    s,\s*$,,;
+    # Strip leading comment indicator and spaces
+    s,^ *\* *,,;
+    push(@$docs, $_);
+    undef $name if $last;
+  }
+}
+
+# Generate docs in name order
+my $indented = 0;
+for my $m (sort keys %macros) {
+  my @docs = @{$macros{$m}};
+  my $heading = shift @docs;
+  # Strip leading and trailing blanks
+  while(@docs > 0 and $docs[0] eq '') {
+    shift @docs;
+  }
+  while(@docs > 0 and $docs[$#docs] eq '') {
+    pop @docs;
+  }
+  print ".TP\n";
+  print ".B $heading\n";
+  for my $d (@docs) {
+    if($d =~ /^-\s*([^:]+):\s+(.*)/) {
+      if(!$indented) {
+       print ".RS\n";
+       $indented = 1;
+      }
+      print ".TP 8\n";
+      print ".B $1\n";
+      $d = $2;
+    }
+    if($d =~ /^- /) {
+      $d = $';
+      if(!$indented) {
+       print ".RS\n";
+       $indented = 1;
+      }
+      print ".TP\n";
+      print ".B .\n";
+    }
+    if($d eq '') {
+      if($indented) {
+       print ".RE\n";
+       $indented = 0;
+      }
+      print ".IP\n";
+    } else {
+      # Keep sentence-ending full stops at end of line
+      $d =~ s/\.  /\.\n/g;
+      print "$d\n";
+    }
+  }
+  if($indented) {
+    print ".RE\n";
+    $indented = 0;
+  }
+}
index 78497a5..9ad35b2 100644 (file)
@@ -100,9 +100,10 @@ disorder_dbupgrade_LDADD=$(LIBOBJS) ../lib/libdisorder.a \
        $(LIBDB) $(LIBGC) $(LIBPCRE) $(LIBICONV) $(LIBGCRYPT)
 disorder_dbupgrade_DEPENDENCIES=../lib/libdisorder.a
 
-disorder_cgi_SOURCES=dcgi.c dcgi.h                     \
-       api.c api-client.c api-client.h                 \
-       cgi.c cgi.h cgimain.c exports.c
+disorder_cgi_SOURCES=macros-disorder.c macros-disorder.h lookup.c      \
+       lookup.h options.c options.h actions.c actions.h login.c        \
+       login.h api.c                                                   \
+       api-client.c api-client.h cgimain.c exports.c
 disorder_cgi_LDADD=../lib/libdisorder.a \
        $(LIBPCRE) $(LIBGCRYPT) $(LIBDL) $(LIBDB)
 disorder_cgi_LDFLAGS=-export-dynamic
diff --git a/server/actions.c b/server/actions.c
new file mode 100644 (file)
index 0000000..2468b77
--- /dev/null
@@ -0,0 +1,814 @@
+/*
+ * 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
+ */
+/** @file server/actions.c
+ * @brief DisOrder web actions
+ *
+ * Actions are anything that the web interface does beyond passive template
+ * expansion and inspection of state recieved from the server.  This means
+ * playing tracks, editing prefs etc but also setting extra headers e.g. to
+ * auto-refresh the playing list.
+ */
+
+#include "disorder-cgi.h"
+
+/** @brief Redirect to some other action or URL */
+static void redirect(const char *url) {
+  /* By default use the 'back' argument */
+  if(!url)
+    url = cgi_get("back");
+  if(url && *url) {
+    if(strncmp(url, "http", 4))
+      /* If the target is not a full URL assume it's the action */
+      url = cgi_makeurl(config->url, "action", url, (char *)0);
+  } else {
+    /* If back= is not set just go back to the front page */
+    url = config->url;
+  }
+  if(printf("Location: %s\n"
+            "%s\n"
+            "\n", url, dcgi_cookie_header()) < 0)
+    fatal(errno, "error writing to stdout");
+}
+
+/*! playing
+ *
+ * Expands \fIplaying.tmpl\fR as if there was no special 'playing' action, but
+ * adds a Refresh: field to the HTTP header.  The maximum refresh interval is
+ * defined by \fBrefresh\fR (see \fBdisorder_config\fR(5)) but may be less if
+ * the end of the track is near.
+ */
+/*! manage
+ *
+ * Expands \fIplaying.tmpl\fR (NB not \fImanage.tmpl\fR) as if there was no
+ * special 'playing' action, and adds a Refresh: field to the HTTP header.  The
+ * maximum refresh interval is defined by \Bfrefresh\fR (see
+ * \fBdisorder_config\fR(5)) but may be less if the end of the track is near.
+ */
+static void act_playing(void) {
+  long refresh = config->refresh;
+  long length;
+  time_t now, fin;
+  char *url;
+  const char *action;
+
+  dcgi_lookup(DCGI_PLAYING|DCGI_QUEUE|DCGI_ENABLED|DCGI_RANDOM_ENABLED);
+  if(dcgi_playing
+     && dcgi_playing->state == playing_started /* i.e. not paused */
+     && !disorder_length(dcgi_client, dcgi_playing->track, &length)
+     && length
+     && dcgi_playing->sofar >= 0) {
+    /* Try to put the next refresh at the start of the next track. */
+    time(&now);
+    fin = now + length - dcgi_playing->sofar + config->gap;
+    if(now + refresh > fin)
+      refresh = fin - now;
+  }
+  if(dcgi_queue && dcgi_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(!dcgi_playing
+     && ((dcgi_queue
+          && dcgi_queue->state != playing_random)
+         || dcgi_random_enabled)
+     && dcgi_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;
+  }
+  if((action = cgi_get("action")))
+    url = cgi_makeurl(config->url, "action", action, (char *)0);
+  else
+    url = config->url;
+  if(printf("Refresh: %ld;url=%s\n",
+            refresh, url) < 0)
+    fatal(errno, "error writing to stdout");
+  dcgi_expand("playing", 1);
+}
+
+/*! disable
+ *
+ * Disables play.
+ */
+static void act_disable(void) {
+  if(dcgi_client)
+    disorder_disable(dcgi_client);
+  redirect(0);
+}
+
+/*! enable
+ *
+ * Enables play.
+ */
+static void act_enable(void) {
+  if(dcgi_client)
+    disorder_enable(dcgi_client);
+  redirect(0);
+}
+
+/*! random-disable
+ *
+ * Disables random play.
+ */
+static void act_random_disable(void) {
+  if(dcgi_client)
+    disorder_random_disable(dcgi_client);
+  redirect(0);
+}
+
+/*! random-enable
+ *
+ * Enables random play.
+ */
+static void act_random_enable(void) {
+  if(dcgi_client)
+    disorder_random_enable(dcgi_client);
+  redirect(0);
+}
+
+/*! pause
+ *
+ * Pauses the current track (if there is one and it's not paused already).
+ */
+static void act_pause(void) {
+  if(dcgi_client)
+    disorder_pause(dcgi_client);
+  redirect(0);
+}
+
+/*! resume
+ *
+ * Resumes the current track (if there is one and it's paused).
+ */
+static void act_resume(void) {
+  if(dcgi_client)
+    disorder_resume(dcgi_client);
+  redirect(0);
+}
+
+/*! remove
+ *
+ * Removes the track given by the \fBid\fR argument.  If this is the currently
+ * playing track then it is scratched.
+ */
+static void act_remove(void) {
+  const char *id;
+  struct queue_entry *q;
+
+  if(dcgi_client) {
+    if(!(id = cgi_get("id")))
+      error(0, "missing 'id' argument");
+    else if(!(q = dcgi_findtrack(id)))
+      error(0, "unknown queue id %s", id);
+    else switch(q->state) {
+    case playing_isscratch:
+    case playing_failed:
+    case playing_no_player:
+    case playing_ok:
+    case playing_quitting:
+    case playing_scratched:
+      error(0, "does not make sense to scratch %s", id);
+      break;
+    case playing_paused:                /* started but paused */
+    case playing_started:               /* started to play */
+      disorder_scratch(dcgi_client, id);
+      break;
+    case playing_random:                /* unplayed randomly chosen track */
+    case playing_unplayed:              /* haven't played this track yet */
+      disorder_remove(dcgi_client, id);
+      break;
+    }
+  }
+  redirect(0);
+}
+
+/*! move
+ *
+ * Moves the track given by the \fBid\fR argument the distance given by the
+ * \fBdelta\fR argument.  If this is positive the track is moved earlier in the
+ * queue and if negative, later.
+ */
+static void act_move(void) {
+  const char *id, *delta;
+  struct queue_entry *q;
+
+  if(dcgi_client) {
+    if(!(id = cgi_get("id")))
+      error(0, "missing 'id' argument");
+    else if(!(delta = cgi_get("delta")))
+      error(0, "missing 'delta' argument");
+    else if(!(q = dcgi_findtrack(id)))
+      error(0, "unknown queue id %s", id);
+    else switch(q->state) {
+    case playing_random:                /* unplayed randomly chosen track */
+    case playing_unplayed:              /* haven't played this track yet */
+      disorder_move(dcgi_client, id, atol(delta));
+      break;
+    default:
+      error(0, "does not make sense to scratch %s", id);
+      break;
+    }
+  }
+  redirect(0);
+}
+
+/*! play
+ *
+ * Play the track given by the \fBtrack\fR argument, or if that is not set all
+ * the tracks in the directory given by the \fBdir\fR argument.
+ */
+static void act_play(void) {
+  const char *track, *dir;
+  char **tracks;
+  int ntracks, n;
+  struct dcgi_entry *e;
+  
+  if(dcgi_client) {
+    if((track = cgi_get("track"))) {
+      disorder_play(dcgi_client, track);
+    } else if((dir = cgi_get("dir"))) {
+      if(disorder_files(dcgi_client, dir, 0, &tracks, &ntracks))
+        ntracks = 0;
+      e = xmalloc(ntracks * sizeof (struct dcgi_entry));
+      for(n = 0; n < ntracks; ++n) {
+        e[n].track = 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 dcgi_entry), dcgi_compare_entry);
+      for(n = 0; n < ntracks; ++n)
+        disorder_play(dcgi_client, e[n].track);
+    }
+  }
+  redirect(0);
+}
+
+static int clamp(int n, int min, int max) {
+  if(n < min)
+    return min;
+  if(n > max)
+    return max;
+  return n;
+}
+
+/*! volume
+ *
+ * If the \fBdelta\fR argument is set: adjust both channels by that amount (up
+ * if positive, down if negative).
+ *
+ * Otherwise if \fBleft\fR and \fBright\fR are set, set the channels
+ * independently to those values.
+ */
+static void act_volume(void) {
+  const char *l, *r, *d;
+  int nd;
+
+  if(dcgi_client) {
+    if((d = cgi_get("delta"))) {
+      dcgi_lookup(DCGI_VOLUME);
+      nd = clamp(atoi(d), -255, 255);
+      disorder_set_volume(dcgi_client,
+                          clamp(dcgi_volume_left + nd, 0, 255),
+                          clamp(dcgi_volume_right + nd, 0, 255));
+    } else if((l = cgi_get("left")) && (r = cgi_get("right")))
+      disorder_set_volume(dcgi_client, atoi(l), atoi(r));
+  }
+  redirect(0);
+}
+
+/** @brief Expand the login template with @b @@error set to @p error
+ * @param e Error keyword
+ */
+static void login_error(const char *e) {
+  dcgi_error_string = e;
+  dcgi_expand("login", 1);
+}
+
+/** @brief Log in
+ * @param username Login name
+ * @param password Password
+ * @return 0 on success, non-0 on error
+ *
+ * On error, calls login_error() to expand the login template.
+ */
+static int login_as(const char *username, const char *password) {
+  disorder_client *c;
+
+  if(dcgi_cookie && dcgi_client)
+    disorder_revoke(dcgi_client);
+  /* 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)) {
+    login_error("loginfailed");
+    return -1;
+  }
+  /* Generate a cookie so we can log in again later */
+  if(disorder_make_cookie(c, &dcgi_cookie)) {
+    login_error("cookiefailed");
+    return -1;
+  }
+  /* Use the new connection henceforth */
+  dcgi_client = c;
+  dcgi_lookup_reset();
+  return 0;                             /* OK */
+}
+
+/*! login
+ *
+ * If \fBusername\fR and \fBpassword\fR are set (and the username isn't
+ * "guest") then attempt to log in using those credentials.  On success,
+ * redirects to the \fBback\fR argument if that is set, or just expands
+ * \fIlogin.tmpl\fI otherwise, with \fB@status\fR set to \fBloginok\fR.
+ *
+ * If they aren't set then just expands \fIlogin.tmpl\fI.
+ */
+static void act_login(void) {
+  const char *username, *password;
+
+  /* We try all this even if not connected since the subsequent connection may
+   * succeed. */
+  
+  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, not performing an action at all. */
+    dcgi_expand("login", 1);
+    return;
+  }
+  if(!login_as(username, password)) {
+    /* Report the succesful login */
+    dcgi_status_string = "loginok";
+    /* Redirect back to where we came from, if necessary */
+    if(cgi_get("back"))
+      redirect(0);
+    else
+      dcgi_expand("login", 1);
+  }
+}
+
+/*! logout
+ *
+ * Logs out the current user and expands \fIlogin.tmpl\fR with \fBstatus\fR or
+ * \fB@error\fR set according to the result.
+ */
+static void act_logout(void) {
+  if(dcgi_client) {
+    /* Ask the server to revoke the cookie */
+    if(!disorder_revoke(dcgi_client))
+      dcgi_status_string = "logoutok";
+    else
+      dcgi_error_string = "revokefailed";
+  } else {
+    /* We can't guarantee a logout if we can't connect to the server to revoke
+     * the cookie, so we report an error.  We'll still ask the browser to
+     * forget the cookie though. */
+    dcgi_error_string = "connect";
+  }
+  /* Attempt to reconnect without the cookie */
+  dcgi_cookie = 0;
+  dcgi_login();
+  /* Back to login page, hopefuly forcing the browser to forget the cookie. */
+  dcgi_expand("login", 1);
+}
+
+/*! register
+ *
+ * Register a new user using \fBusername\fR, \fBpassword1\fR, \fBpassword2\fR
+ * and \fBemail\fR and expands \fIlogin.tmpl\fR with \fBstatus\fR or
+ * \fB@error\fR set according to the result.
+ */
+static void act_register(void) {
+  const char *username, *password, *password2, *email;
+  char *confirm, *content_type;
+  const char *text, *encoding, *charset;
+
+  /* If we're not connected then this is a hopeless exercise */
+  if(!dcgi_client) {
+    login_error("connect");
+    return;
+  }
+
+  /* Collect arguments */
+  username = cgi_get("username");
+  password = cgi_get("password1");
+  password2 = cgi_get("password2");
+  email = cgi_get("email");
+
+  /* Verify arguments */
+  if(!username || !*username) {
+    login_error("nousername");
+    return;
+  }
+  if(!password || !*password) {
+    login_error("nopassword");
+    return;
+  }
+  if(!password2 || !*password2 || strcmp(password, password2)) {
+    login_error("passwordmismatch");
+    return;
+  }
+  if(!email || !*email) {
+    login_error("noemail");
+    return;
+  }
+  /* We could well do better address validation but for now we'll just do the
+   * minimum */
+  if(!strchr(email, '@')) {
+    login_error("bademail");
+    return;
+  }
+  if(disorder_register(dcgi_client, username, password, email, &confirm)) {
+    login_error("cannotregister");
+    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 */
+  dcgi_status_string = "registered";
+  dcgi_expand("login", 1);
+}
+
+/*! confirm
+ *
+ * Confirm a user registration using the nonce supplied in \fBc\fR and expands
+ * \fIlogin.tmpl\fR with \fBstatus\fR or \fB@error\fR set according to the
+ * result.
+ */
+static void act_confirm(void) {
+  const char *confirmation;
+
+  /* If we're not connected then this is a hopeless exercise */
+  if(!dcgi_client) {
+    login_error("connect");
+    return;
+  }
+
+  if(!(confirmation = cgi_get("c"))) {
+    login_error("noconfirm");
+    return;
+  }
+  /* Confirm our registration */
+  if(disorder_confirm(dcgi_client, confirmation)) {
+    login_error("badconfirm");
+    return;
+  }
+  /* Get a cookie */
+  if(disorder_make_cookie(dcgi_client, &dcgi_cookie)) {
+    login_error("cookiefailed");
+    return;
+  }
+  /* Junk cached data */
+  dcgi_lookup_reset();
+  /* Report success */
+  dcgi_status_string = "confirmed";
+  dcgi_expand("login", 1);
+}
+
+/*! edituser
+ *
+ * Edit user details using \fBusername\fR, \fBchangepassword1\fR,
+ * \fBchangepassword2\fR and \fBemail\fR and expands \fIlogin.tmpl\fR with
+ * \fBstatus\fR or \fB@error\fR set according to the result.
+ */
+static void act_edituser(void) {
+  const char *email = cgi_get("email"), *password = cgi_get("changepassword1");
+  const char *password2 = cgi_get("changepassword2");
+  int newpassword = 0;
+
+  /* If we're not connected then this is a hopeless exercise */
+  if(!dcgi_client) {
+    login_error("connect");
+    return;
+  }
+
+  /* Verify input */
+
+  /* If either password or password2 is set we insist they match.  If they
+   * don't we report an error. */
+  if((password && *password) || (password2 && *password2)) {
+    if(!password || !password2 || strcmp(password, password2)) {
+      login_error("passwordmismatch");
+      return;
+    }
+  } else
+    password = password2 = 0;
+  if(email && !strchr(email, '@')) {
+    login_error("bademail");
+    return;
+  }
+
+  /* Commit changes */
+  if(email) {
+    if(disorder_edituser(dcgi_client, disorder_user(dcgi_client),
+                        "email", email)) {
+      login_error("badedit");
+      return;
+    }
+  }
+  if(password) {
+    if(disorder_edituser(dcgi_client, disorder_user(dcgi_client),
+                        "password", password)) {
+      login_error("badedit");
+      return;
+    }
+    newpassword = 1;
+  }
+
+  if(newpassword) {
+    /* If we changed the password, the cookie is now invalid, so we must log
+     * back in. */
+    if(login_as(disorder_user(dcgi_client), password))
+      return;
+  }
+  /* Report success */
+  dcgi_status_string = "edited";
+  dcgi_expand("login", 1);
+}
+
+/*! reminder
+ *
+ * Issue an email password reminder to \fBusername\fR and expands
+ * \fIlogin.tmpl\fR with \fBstatus\fR or \fB@error\fR set according to the
+ * result.
+ */
+static void act_reminder(void) {
+  const char *const username = cgi_get("username");
+
+  /* If we're not connected then this is a hopeless exercise */
+  if(!dcgi_client) {
+    login_error("connect");
+    return;
+  }
+
+  if(!username || !*username) {
+    login_error("nousername");
+    return;
+  }
+  if(disorder_reminder(dcgi_client, username)) {
+    login_error("reminderfailed");
+    return;
+  }
+  /* Report success */
+  dcgi_status_string = "reminded";
+  dcgi_expand("login", 1);
+}
+
+/** @brief Get the numbered version of an argument
+ * @param argname Base argument name
+ * @param numfile File number
+ * @return cgi_get(NUMFILE_ARGNAME)
+ */
+static const char *numbered_arg(const char *argname, int numfile) {
+  char *fullname;
+
+  byte_xasprintf(&fullname, "%d_%s", numfile, argname);
+  return cgi_get(fullname);
+}
+
+/** @brief Set preferences for file @p numfile
+ * @return 0 on success, -1 if there is no such track number
+ *
+ * The old @b nfiles parameter has been abolished, we just keep look for more
+ * files until we run out.
+ */
+static int process_prefs(int numfile) {
+  const char *file, *name, *value, *part, *parts, *context;
+  char **partslist;
+
+  if(!(file = numbered_arg("track", numfile)))
+    return -1;
+  if(!(parts = cgi_get("parts")))
+    parts = "artist album title";
+  if(!(context = cgi_get("context")))
+    context = "display";
+  partslist = split(parts, 0, 0, 0, 0);
+  while((part = *partslist++)) {
+    if(!(value = numbered_arg(part, numfile)))
+      continue;
+    byte_xasprintf((char **)&name, "trackname_%s_%s", context, part);
+    disorder_set(dcgi_client, file, name, value);
+  }
+  if((value = numbered_arg("random", numfile)))
+    disorder_unset(dcgi_client, file, "pick_at_random");
+  else
+    disorder_set(dcgi_client, file, "pick_at_random", "0");
+  if((value = numbered_arg("tags", numfile))) {
+    if(!*value)
+      disorder_unset(dcgi_client, file, "tags");
+    else
+      disorder_set(dcgi_client, file, "tags", value);
+  }
+  if((value = numbered_arg("weight", numfile))) {
+    if(!*value)
+      disorder_unset(dcgi_client, file, "weight");
+    else
+      disorder_set(dcgi_client, file, "weight", value);
+  }
+  return 0;
+}
+
+/*! prefs
+ *
+ * Set preferences on a number of tracks.
+ *
+ * The tracks to modify are specified in arguments \fB0_track\fR, \fB1_track\fR
+ * etc.  The number sequence must be contiguous and start from 0.
+ *
+ * For each track \fIINDEX\fB_track\fR:
+ * - \fIINDEX\fB_\fIPART\fR is used to set the trackname preference for
+ * that part.  (See \fBparts\fR below.)
+ * - \fIINDEX\fB_\fIrandom\fR if present enables random play for this track
+ * or disables it if absent.
+ * - \fIINDEX\fB_\fItags\fR sets the list of tags for this track.
+ * - \fIINDEX\fB_\fIweight\fR sets the weight for this track.
+ *
+ * \fBparts\fR can be set to the track name parts to modify.  The default is
+ * "artist album title".
+ *
+ * \fBcontext\fR can be set to the context to modify.  The default is
+ * "display".
+ *
+ * If the server detects a preference being set to its default, it removes the
+ * preference, thus keeping the database tidy.
+ */
+static void act_set(void) {
+  int numfile;
+
+  if(dcgi_client) {
+    for(numfile = 0; !process_prefs(numfile); ++numfile)
+      ;
+  }
+  redirect(0);
+}
+
+/** @brief Table of actions */
+static const struct action {
+  /** @brief Action name */
+  const char *name;
+  /** @brief Action handler */
+  void (*handler)(void);
+  /** @brief Union of suitable rights */
+  rights_type rights;
+} actions[] = {
+  { "confirm", act_confirm, 0 },
+  { "disable", act_disable, RIGHT_GLOBAL_PREFS },
+  { "edituser", act_edituser, 0 },
+  { "enable", act_enable, RIGHT_GLOBAL_PREFS },
+  { "login", act_login, 0 },
+  { "logout", act_logout, 0 },
+  { "manage", act_playing, 0 },
+  { "move", act_move, RIGHT_MOVE__MASK },
+  { "pause", act_pause, RIGHT_PAUSE },
+  { "play", act_play, RIGHT_PLAY },
+  { "playing", act_playing, 0 },
+  { "randomdisable", act_random_disable, RIGHT_GLOBAL_PREFS },
+  { "randomenable", act_random_enable, RIGHT_GLOBAL_PREFS },
+  { "register", act_register, 0 },
+  { "reminder", act_reminder, 0 },
+  { "remove", act_remove, RIGHT_MOVE__MASK|RIGHT_SCRATCH__MASK },
+  { "resume", act_resume, RIGHT_PAUSE },
+  { "set", act_set, RIGHT_PREFS },
+  { "volume", act_volume, RIGHT_VOLUME },
+};
+
+/** @brief Check that an action name is valid
+ * @param name Action
+ * @return 1 if valid, 0 if not
+ */
+static int dcgi_valid_action(const char *name) {
+  int c;
+
+  /* First character must be letter or digit (this also requires there to _be_
+   * a first character) */
+  if(!isalnum((unsigned char)*name))
+    return 0;
+  /* Only letters, digits, '.' and '-' allowed */
+  while((c = (unsigned char)*name++)) {
+    if(!(isalnum(c)
+         || c == '.'
+         || c == '_'))
+      return 0;
+  }
+  return 1;
+}
+
+/** @brief Expand a template
+ * @param name Base name of template, or NULL to consult CGI args
+ * @param header True to write header
+ */
+void dcgi_expand(const char *name, int header) {
+  const char *p, *found;
+
+  /* Parse macros first */
+  if((found = mx_find("macros.tmpl", 1/*report*/)))
+    mx_expand_file(found, sink_discard(), 0);
+  if((found = mx_find("user.tmpl", 0/*report*/)))
+    mx_expand_file(found, sink_discard(), 0);
+  /* For unknown actions check that they aren't evil */
+  if(!dcgi_valid_action(name))
+    fatal(0, "invalid action name '%s'", name);
+  byte_xasprintf((char **)&p, "%s.tmpl", name);
+  if(!(found = mx_find(p, 0/*report*/)))
+    fatal(errno, "cannot find %s", p);
+  if(header) {
+    if(printf("Content-Type: text/html\n"
+              "%s\n"
+              "\n", dcgi_cookie_header()) < 0)
+      fatal(errno, "error writing to stdout");
+  }
+  if(mx_expand_file(found, sink_stdio("stdout", stdout), 0) == -1
+     || fflush(stdout) < 0)
+    fatal(errno, "error writing to stdout");
+}
+
+/** @brief Execute a web action
+ * @param action Action to perform, or NULL to consult CGI args
+ *
+ * If no recognized action is specified then 'playing' is assumed.
+ */
+void dcgi_action(const char *action) {
+  int n;
+
+  /* Consult CGI args if caller had no view */
+  if(!action)
+    action = cgi_get("action");
+  /* Pick a default if nobody cares at all */
+  if(!action) {
+    /* We allow URLs which are just c=... in order to keep confirmation URLs,
+     * which are user-facing, as short as possible.  Actually we could lose the
+     * c= for this... */
+    if(cgi_get("c"))
+      action = "confirm";
+    else
+      action = "playing";
+    /* Make sure 'action' is always set */
+    cgi_set("action", action);
+  }
+  if((n = TABLE_FIND(actions, struct action, name, action)) >= 0) {
+    if(actions[n].rights) {
+      /* Some right or other is required */
+      dcgi_lookup(DCGI_RIGHTS);
+      if(!(actions[n].rights & dcgi_rights)) {
+        const char *back = cgi_thisurl(config->url);
+        /* Failed operations jump you to the login screen with an error
+         * message.  On success, the user comes back to the page they were
+         * after. */
+        cgi_clear();
+        cgi_set("back", back);
+        login_error("noright");
+        return;
+      }
+    }
+    /* It's a known action */
+    actions[n].handler();
+  } else {
+    /* Just expand the template */
+    dcgi_expand(action, 1/*header*/);
+  }
+}
+
+/** @brief Generate an error page */
+void dcgi_error(const char *key) {
+  dcgi_error_string = xstrdup(key);
+  dcgi_expand("error", 1);
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
diff --git a/server/cgi.c b/server/cgi.c
deleted file mode 100644 (file)
index 2017d1b..0000000
+++ /dev/null
@@ -1,633 +0,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 "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);
-}
-
-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 --git a/server/cgi.h b/server/cgi.h
deleted file mode 100644 (file)
index 2381c03..0000000
+++ /dev/null
@@ -1,109 +0,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 CGI_H
-#define CGI_H
-
-extern struct kvp *cgi_args;
-
-typedef struct {
-  int quote;
-  struct sink *sink;
-} cgi_sink;
-
-void cgi_parse(void);
-/* parse CGI args */
-
-const char *cgi_get(const char *name);
-/* get an argument */
-
-void cgi_header(struct sink *output, const char *name, const char *value);
-/* output a header.  @name@ and @value@ are ASCII. */
-
-void cgi_body(struct sink *output);
-/* indicate the start of the body */
-
-void cgi_output(cgi_sink *output, const char *fmt, ...)
-  attribute((format (printf, 2, 3)));
-/* SGML-quote formatted UTF-8 data and write it.  Checks errors. */
-
-char *cgi_sgmlquote(const char *s, int raw);
-/* SGML-quote multibyte string @s@ */
-
-void cgi_attr(struct sink *output, const char *name, const char *value);
-/* write an attribute */
-
-void cgi_opentag(struct sink *output, const char *name, ...);
-/* write an open tag, including attribute name-value pairs terminate
- * by (char *)0 */
-
-void cgi_closetag(struct sink *output, const char *name);
-/* write a close tag */
-
-struct cgi_expansion {
-  const char *name;
-  int minargs, maxargs;
-  unsigned flags;
-#define EXP_MAGIC 0x0001
-  void (*handler)(int nargs, char **args, cgi_sink *output, void *u);
-};
-
-void cgi_expand(const char *name,
-               const struct cgi_expansion *expansions,
-               size_t nexpansions,
-               cgi_sink *output,
-               void *u);
-/* find @name@ and substitute for expansions */
-
-void cgi_expand_string(const char *name,
-                      const char *template,
-                      const struct cgi_expansion *expansions,
-                      size_t nexpansions,
-                      cgi_sink *output,
-                      void *u);
-/* same but @template@ is text of template */
-
-char *cgi_makeurl(const char *url, ...);
-/* make up a URL */
-
-const char *cgi_label(const char *key);
-/* look up the translated label @key@ */
-
-int cgi_label_exists(const char *key);
-
-char **cgi_columns(const char *name, int *nheadings);
-/* return the list of columns for @name@ */
-
-const char *cgi_transform(const char *type,
-                         const char *track,
-                         const char *context);
-/* transform a track or directory name for display */
-
-void cgi_set_option(const char *name, const char *value);
-/* set an option */
-
-#endif /* CGI_H */
-
-/*
-Local Variables:
-c-basic-offset:2
-comment-column:40
-End:
-*/
index 5a79bb5..253f82e 100644 (file)
  * 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 <stdlib.h>
-#include <sys/types.h>
-#include <sys/socket.h>
-#include <locale.h>
-#include <string.h>
-#include <stdarg.h>
-
-#include "client.h"
-#include "sink.h"
-#include "cgi.h"
-#include "mem.h"
-#include "log.h"
-#include "configuration.h"
-#include "disorder.h"
-#include "api-client.h"
-#include "mime.h"
-#include "printf.h"
-#include "dcgi.h"
-#include "url.h"
-
-/** @brief Return true if @p a is better than @p b
- *
- * NB. We don't bother checking if the path is right, we merely check for the
- * longest path.  This isn't a security hole: if the browser wants to send us
- * bad cookies it's quite capable of sending just the right path anyway.  The
- * point of choosing the longest path is to avoid using a cookie set by another
- * CGI script which shares a path prefix with us, which would allow it to
- * maliciously log users out.
- *
- * Such a script could still "maliciously" log someone in, if it had acquired a
- * suitable cookie.  But it could just log in directly if it had that, so there
- * is no obvious vulnerability here either.
+/** @file server/cgimain.c
+ * @brief DisOrder CGI
  */
-static int better_cookie(const struct cookie *a, const struct cookie *b) {
-  if(a->path && b->path)
-    /* If both have a path then the one with the longest path is best */
-    return strlen(a->path) > strlen(b->path);
-  else if(a->path)
-    /* If only @p a has a path then it is better */
-    return 1;
-  else
-    /* If neither have a path, or if only @p b has a path, then @p b is
-     * better */
-    return 0;
-}
+
+#include "disorder-cgi.h"
 
 int main(int argc, char **argv) {
-  const char *cookie_env, *conf;
-  dcgi_global g;
-  dcgi_state s;
-  cgi_sink output;
-  int n, best_cookie;
-  struct cookiedata cd;
+  const char *conf;
 
-  if(argc > 0) progname = argv[0];
+  if(argc > 0)
+    progname = argv[0];
   /* RFC 3875 s8.2 recommends rejecting PATH_INFO if we don't make use of
    * it. */
+  /* TODO we could make disorder/ACTION equivalent to disorder?action=ACTION */
   if(getenv("PATH_INFO")) {
+    /* TODO it might be nice to link back to the right place... */
     printf("Content-Type: text/html\n");
     printf("Status: 404\n");
     printf("\n");
     printf("<p>Sorry, PATH_INFO not supported.</p>\n");
     exit(0);
   }
-  cgi_parse();
-  if((conf = getenv("DISORDER_CONFIG"))) configfile = xstrdup(conf);
-  if(getenv("DISORDER_DEBUG")) debugging = 1;
-  if(config_read(0)) exit(EXIT_FAILURE);
+  /* Parse CGI arguments */
+  cgi_init();
+  /* We allow various things to be overridden from the environment.  This is
+   * intended for debugging and is not a documented feature. */
+  if((conf = getenv("DISORDER_CONFIG")))
+    configfile = xstrdup(conf);
+  if(getenv("DISORDER_DEBUG"))
+    debugging = 1;
+  /* Read configuration */
+  if(config_read(0/*!server*/))
+    exit(EXIT_FAILURE);
+  /* Figure out our URL.  This can still be overridden from the config file if
+   * necessary but it shouldn't be necessary in ordinary installations. */
   if(!config->url)
     config->url = infer_url();
-  memset(&g, 0, sizeof g);
-  memset(&s, 0, sizeof s);
-  s.g = &g;
-  g.client = disorder_get_client();
-  output.quote = 1;
-  output.sink = sink_stdio("stdout", stdout);
-  /* See if there's a cookie */
-  cookie_env = getenv("HTTP_COOKIE");
-  if(cookie_env) {
-    /* This will be an HTTP header */
-    if(!parse_cookie(cookie_env, &cd)) {
-      /* Pick the best available cookie from all those offered */
-      best_cookie = -1;
-      for(n = 0; n < cd.ncookies; ++n) {
-       /* Is this the right cookie? */
-       if(strcmp(cd.cookies[n].name, "disorder"))
-         continue;
-       /* Is it better than anything we've seen so far? */
-       if(best_cookie < 0
-          || better_cookie(&cd.cookies[n], &cd.cookies[best_cookie]))
-         best_cookie = n;
-      }
-      if(best_cookie != -1)
-       login_cookie = cd.cookies[best_cookie].value;
-    } else
-      error(0, "could not parse cookie field '%s'", cookie_env);
-  }
-  disorder_cgi_login(&s, &output);
-  disorder_cgi(&output, &s);
-  if(fclose(stdout) < 0) fatal(errno, "error closing stdout");
+  /* Pick up the cookie, if there is one */
+  dcgi_get_cookie();
+  /* Register expansions */
+  mx_register_builtin();
+  dcgi_expansions();
+  /* Update search path.  We look in the config directory first and the data
+   * directory second, so that the latter overrides the former. */
+  mx_search_path(pkgconfdir);
+  mx_search_path(pkgdatadir);
+  /* Never cache anythging */
+  if(printf("Cache-Control: no-cache\n") < 0)
+    fatal(errno, "error writing to stdout");
+  /* Create the initial connection, trying the cookie if we found a suitable
+   * one. */
+  dcgi_login();
+  /* Do whatever the user wanted */
+  dcgi_action(NULL);
+  /* In practice if a write fails that probably means the web server went away,
+   * but we log it anyway. */
+  if(fclose(stdout) < 0)
+    fatal(errno, "error closing stdout");
   return 0;
 }
 
diff --git a/server/dcgi.c b/server/dcgi.c
deleted file mode 100644 (file)
index 33630fa..0000000
+++ /dev/null
@@ -1,1906 +0,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 "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 --git a/server/dcgi.h b/server/dcgi.h
deleted file mode 100644 (file)
index 09e74da..0000000
+++ /dev/null
@@ -1,73 +0,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:
-*/
diff --git a/server/disorder-cgi.h b/server/disorder-cgi.h
new file mode 100644 (file)
index 0000000..beb8581
--- /dev/null
@@ -0,0 +1,129 @@
+/*
+ * 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
+ */
+/** @file server/disorder-cgi.h
+ * @brief Shared header for DisOrder CGI program
+ */
+
+#ifndef DISORDER_CGI_H
+#define DISORDER_CGI_H
+
+#include <config.h>
+#include "types.h"
+
+#include <stdio.h>
+#include <stdarg.h>
+#include <string.h>
+#include <time.h>
+#include <errno.h>
+#include <ctype.h>
+#include <stddef.h>
+
+#include "mem.h"
+#include "kvp.h"
+#include "queue.h"
+#include "rights.h"
+#include "sink.h"
+#include "client.h"
+#include "cgi.h"
+#include "hash.h"
+#include "macros.h"
+#include "printf.h"
+#include "defs.h"
+#include "configuration.h"
+#include "trackname.h"
+#include "table.h"
+#include "vector.h"
+#include "url.h"
+#include "log.h"
+#include "inputline.h"
+#include "split.h"
+#include "mime.h"
+#include "sendmail.h"
+#include "charset.h"
+
+extern disorder_client *dcgi_client;
+extern char *dcgi_cookie;
+extern const char *dcgi_error_string;
+extern const char *dcgi_status_string;
+
+/** @brief Entry in a list of tracks or directories */
+struct dcgi_entry {
+  /** @brief Track name */
+  const char *track;
+  /** @brief Sort key */
+  const char *sort;
+  /** @brief Display key */
+  const char *display;
+};
+
+/** @brief Compare two @ref entry objects */
+int dcgi_compare_entry(const void *a, const void *b);
+
+void dcgi_expand(const char *name, int header);
+void dcgi_action(const char *action);
+void dcgi_error(const char *key);
+void dcgi_login(void);
+void dcgi_lookup(unsigned want);
+void dcgi_lookup_reset(void);
+void dcgi_expansions(void);
+char *dcgi_cookie_header(void);
+void dcgi_login(void);
+void dcgi_get_cookie(void);
+struct queue_entry *dcgi_findtrack(const char *id);
+
+void option_set(const char *name, const char *value);
+const char *option_label(const char *key);
+int option_label_exists(const char *key);
+char **option_columns(const char *name, int *ncolumns);
+
+#define DCGI_QUEUE 0x0001
+#define DCGI_PLAYING 0x0002
+#define DCGI_RECENT 0x0004
+#define DCGI_VOLUME 0x0008
+#define DCGI_NEW 0x0040
+#define DCGI_RIGHTS 0x0080
+#define DCGI_ENABLED 0x0100
+#define DCGI_RANDOM_ENABLED 0x0200
+
+extern struct queue_entry *dcgi_queue;
+extern struct queue_entry *dcgi_playing;
+extern struct queue_entry *dcgi_recent;
+
+extern int dcgi_volume_left;
+extern int dcgi_volume_right;
+
+extern char **dcgi_new;
+extern int dcgi_nnew;
+
+extern rights_type dcgi_rights;
+
+extern int dcgi_enabled;
+extern int dcgi_random_enabled;
+
+#endif /* DISORDER_CGI_H */
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
diff --git a/server/login.c b/server/login.c
new file mode 100644 (file)
index 0000000..891ebe1
--- /dev/null
@@ -0,0 +1,150 @@
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 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 "disorder-cgi.h"
+
+/** @brief Client used by CGI
+ *
+ * The caller should arrange for this to be created before any of
+ * these expansions are used (if it cannot connect then it's safe to
+ * leave it as NULL).
+ */
+disorder_client *dcgi_client;
+
+/** @brief Return true if @p a is better than @p b
+ *
+ * NB. We don't bother checking if the path is right, we merely check for the
+ * longest path.  This isn't a security hole: if the browser wants to send us
+ * bad cookies it's quite capable of sending just the right path anyway.  The
+ * point of choosing the longest path is to avoid using a cookie set by another
+ * CGI script which shares a path prefix with us, which would allow it to
+ * maliciously log users out.
+ *
+ * Such a script could still "maliciously" log someone in, if it had acquired a
+ * suitable cookie.  But it could just log in directly if it had that, so there
+ * is no obvious vulnerability here either.
+ */
+static int better_cookie(const struct cookie *a, const struct cookie *b) {
+  if(a->path && b->path)
+    /* If both have a path then the one with the longest path is best */
+    return strlen(a->path) > strlen(b->path);
+  else if(a->path)
+    /* If only @p a has a path then it is better */
+    return 1;
+  else
+    /* If neither have a path, or if only @p b has a path, then @p b is
+     * better */
+    return 0;
+}
+
+/** @brief Login cookie */
+char *dcgi_cookie;
+
+/** @brief Set @ref login_cookie */
+void dcgi_get_cookie(void) {
+  const char *cookie_env;
+  int n, best_cookie;
+  struct cookiedata cd;
+
+  /* See if there's a cookie */
+  cookie_env = getenv("HTTP_COOKIE");
+  if(cookie_env) {
+    /* This will be an HTTP header */
+    if(!parse_cookie(cookie_env, &cd)) {
+      /* Pick the best available cookie from all those offered */
+      best_cookie = -1;
+      for(n = 0; n < cd.ncookies; ++n) {
+       /* Is this the right cookie? */
+       if(strcmp(cd.cookies[n].name, "disorder"))
+         continue;
+       /* Is it better than anything we've seen so far? */
+       if(best_cookie < 0
+          || better_cookie(&cd.cookies[n], &cd.cookies[best_cookie]))
+         best_cookie = n;
+      }
+      if(best_cookie != -1)
+       dcgi_cookie = cd.cookies[best_cookie].value;
+    } else
+      error(0, "could not parse cookie field '%s'", cookie_env);
+  }
+}
+
+/** @brief Return a Cookie: header */
+char *dcgi_cookie_header(void) {
+  struct dynstr d[1];
+  struct url u;
+  char *s;
+
+  memset(&u, 0, sizeof u);
+  dynstr_init(d);
+  parse_url(config->url, &u);
+  if(dcgi_cookie) {
+    dynstr_append_string(d, "disorder=");
+    dynstr_append_string(d, dcgi_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);
+  byte_xasprintf(&s, "Set-Cookie: %s", d->vec);
+  return s;
+}
+
+/** @brief Log in as the current user or guest if none */
+void dcgi_login(void) {
+  /* Junk old data */
+  dcgi_lookup_reset();
+  /* Junk the old connection if there is one */
+  if(dcgi_client)
+    disorder_close(dcgi_client);
+  /* Create a new connection */
+  dcgi_client = disorder_new(0);
+  /* Reconnect */
+  if(disorder_connect_cookie(dcgi_client, dcgi_cookie)) {
+    dcgi_error("connect");
+    exit(0);
+  }
+  /* If there was a cookie but it went bad, we forget it */
+  if(dcgi_cookie && !strcmp(disorder_user(dcgi_client), "guest"))
+    dcgi_cookie = 0;
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
diff --git a/server/lookup.c b/server/lookup.c
new file mode 100644 (file)
index 0000000..6dc254c
--- /dev/null
@@ -0,0 +1,142 @@
+/*
+ * 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
+ */
+/** @file server/lookup.c
+ * @brief Server lookups
+ *
+ * To improve performance many server lookups are cached.
+ */
+
+#include "disorder-cgi.h"
+
+/** @brief Cached data */
+static unsigned flags;
+
+/** @brief Map of hashes to queud data */
+static hash *queuemap;
+
+struct queue_entry *dcgi_queue;
+struct queue_entry *dcgi_playing;
+struct queue_entry *dcgi_recent;
+
+int dcgi_volume_left;
+int dcgi_volume_right;
+
+char **dcgi_new;
+int dcgi_nnew;
+
+rights_type dcgi_rights;
+
+int dcgi_enabled;
+int dcgi_random_enabled;
+
+static void queuemap_add(struct queue_entry *q) {
+  if(!queuemap)
+    queuemap = hash_new(sizeof (struct queue_entry *));
+  for(; q; q = q->next)
+    hash_add(queuemap, q->id, &q, HASH_INSERT_OR_REPLACE);
+}
+
+/** @brief Fetch cachable data */
+void dcgi_lookup(unsigned want) {
+  unsigned need = want ^ (flags & want);
+  struct queue_entry *r, *rnext;
+  char *rs;
+
+  if(!dcgi_client || !need)
+    return;
+  if(need & DCGI_QUEUE) {
+    disorder_queue(dcgi_client, &dcgi_queue);
+    queuemap_add(dcgi_queue);
+  }
+  if(need & DCGI_PLAYING) {
+    disorder_playing(dcgi_client, &dcgi_playing);
+    queuemap_add(dcgi_playing);
+  }
+  if(need & DCGI_NEW)
+    disorder_new_tracks(dcgi_client, &dcgi_new, &dcgi_nnew, 0);
+  if(need & DCGI_RECENT) {
+    /* we need to reverse the order of the list */
+    disorder_recent(dcgi_client, &r);
+    while(r) {
+      rnext = r->next;
+      r->next = dcgi_recent;
+      dcgi_recent = r;
+      r = rnext;
+    }
+    queuemap_add(dcgi_recent);
+  }
+  if(need & DCGI_VOLUME)
+    disorder_get_volume(dcgi_client,
+                        &dcgi_volume_left, &dcgi_volume_right);
+  if(need & DCGI_RIGHTS) {
+    dcgi_rights = RIGHT_READ;  /* fail-safe */
+    if(!disorder_userinfo(dcgi_client, disorder_user(dcgi_client),
+                          "rights", &rs))
+      parse_rights(rs, &dcgi_rights, 1);
+  }
+  if(need & DCGI_ENABLED)
+    disorder_enabled(dcgi_client, &dcgi_enabled);
+  if(need & DCGI_RANDOM_ENABLED)
+    disorder_random_enabled(dcgi_client, &dcgi_random_enabled);
+  flags |= need;
+}
+
+/** @brief Locate a track by ID */
+struct queue_entry *dcgi_findtrack(const char *id) {
+  struct queue_entry **qq;
+
+  if(queuemap && (qq = hash_find(queuemap, id)))
+    return *qq;
+  dcgi_lookup(DCGI_PLAYING);
+  if(queuemap && (qq = hash_find(queuemap, id)))
+    return *qq;
+  dcgi_lookup(DCGI_QUEUE);
+  if(queuemap && (qq = hash_find(queuemap, id)))
+    return *qq;
+  dcgi_lookup(DCGI_RECENT);
+  if(queuemap && (qq = hash_find(queuemap, id)))
+    return *qq;
+  return NULL;
+}
+
+void dcgi_lookup_reset(void) {
+  /* Forget everything we knew */
+  flags = 0;
+  queuemap = 0;
+  dcgi_recent = 0;
+  dcgi_queue = 0;
+  dcgi_playing = 0;
+  dcgi_rights = 0;
+  dcgi_new = 0;
+  dcgi_nnew = 0;
+  dcgi_enabled = 0;
+  dcgi_random_enabled = 0;
+  dcgi_volume_left = dcgi_volume_right = 0;
+}
+
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
diff --git a/server/macros-disorder.c b/server/macros-disorder.c
new file mode 100644 (file)
index 0000000..eff6313
--- /dev/null
@@ -0,0 +1,1058 @@
+
+/*
+ * 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
+ */
+/** @file server/macros-disorder.c
+ * @brief DisOrder-specific expansions
+ */
+
+#include "disorder-cgi.h"
+
+/** @brief For error template */
+const char *dcgi_error_string;
+
+/** @brief For login template */
+const char *dcgi_status_string;
+
+/** @brief Return @p i as a string */
+static const char *make_index(int i) {
+  char *s;
+
+  byte_xasprintf(&s, "%d", i);
+  return s;
+}
+
+/*! @server-version
+ *
+ * Expands to the server's version string, or a (safe to use) error
+ * value if the server is unavailable or broken.
+ */
+static int exp_server_version(int attribute((unused)) nargs,
+                             char attribute((unused)) **args,
+                             struct sink *output,
+                             void attribute((unused)) *u) {
+  const char *v;
+  
+  if(dcgi_client) {
+    if(disorder_version(dcgi_client, (char **)&v))
+      v = "(cannot get version)";
+  } else
+    v = "(server not running)";
+  return sink_writes(output, cgi_sgmlquote(v)) < 0 ? -1 : 0;
+}
+
+/*! @version
+ *
+ * Expands to the local version string.
+ */
+static int exp_version(int attribute((unused)) nargs,
+                      char attribute((unused)) **args,
+                      struct sink *output,
+                      void attribute((unused)) *u) {
+  return sink_writes(output,
+                     cgi_sgmlquote(disorder_short_version_string)) < 0 ? -1 : 0;
+}
+
+/*! @url
+ *
+ * Expands to the base URL of the web interface.
+ */
+static int exp_url(int attribute((unused)) nargs,
+                  char attribute((unused)) **args,
+                  struct sink *output,
+                  void attribute((unused)) *u) {
+  return sink_writes(output,
+                     cgi_sgmlquote(config->url)) < 0 ? -1 : 0;
+}
+
+/*! @arg{NAME}
+ *
+ * Expands to the UNQUOTED form of CGI argument NAME, or the empty string if
+ * there is no such argument.  Use @argq for a quick way to quote the argument.
+ */
+static int exp_arg(int attribute((unused)) nargs,
+                  char **args,
+                  struct sink *output,
+                  void attribute((unused)) *u) {
+  const char *s = cgi_get(args[0]);
+
+  if(s)
+    return sink_writes(output, s) < 0 ? -1 : 0;
+  else
+    return 0;
+}
+
+/*! @argq{NAME}
+ *
+ * Expands to the (quoted) form of CGI argument NAME, or the empty string if
+ * there is no such argument.  Use @arg for the unquoted argument.
+ */
+static int exp_argq(int attribute((unused)) nargs,
+                    char **args,
+                    struct sink *output,
+                    void attribute((unused)) *u) {
+  const char *s = cgi_get(args[0]);
+
+  if(s)
+    return sink_writes(output, cgi_sgmlquote(s)) < 0 ? -1 : 0;
+  else
+    return 0;
+}
+
+/*! @user
+ *
+ * Expands to the logged-in username (which might be "guest"), or to
+ * the empty string if not connected.
+ */
+static int exp_user(int attribute((unused)) nargs,
+                   char attribute((unused)) **args,
+                   struct sink *output,
+                   void attribute((unused)) *u) {
+  const char *user;
+  
+  if(dcgi_client && (user = disorder_user(dcgi_client)))
+    return sink_writes(output, cgi_sgmlquote(user)) < 0 ? -1 : 0;
+  return 0;
+}
+
+/*! @part{TRACK|ID}{PART}{CONTEXT}
+ *
+ * Expands to a track name part.
+ *
+ * A track may be identified by name or by queue ID.
+ *
+ * CONTEXT may be omitted.  If it is then 'display' is assumed.
+ *
+ * If the CONTEXT is 'short' then the 'display' part is looked up, and the
+ * result truncated according to the length defined by the short_display
+ * configuration directive.
+ */
+static int exp_part(int nargs,
+                   char **args,
+                   struct sink *output,
+                   void attribute((unused)) *u) {
+  const char *track = args[0], *part = args[1];
+  const char *context = nargs > 2 ? args[2] : "display";
+  const char *s;
+
+  if(track[0] != '/') {
+    struct queue_entry *q = dcgi_findtrack(track);
+
+    if(q)
+      track = q->track;
+    else
+      return 0;
+  }
+  if(dcgi_client
+     && !disorder_part(dcgi_client, (char **)&s,
+                       track,
+                       !strcmp(context, "short") ? "display" : context,
+                       part)) {
+    if(!strcmp(context, "short"))
+      s = truncate_for_display(s, config->short_display);
+    return sink_writes(output, cgi_sgmlquote(s)) < 0 ? -1 : 0;
+  }
+  return 0;
+}
+
+/*! @quote{STRING}
+ *
+ * SGML-quotes STRING.  Note that most expansion results are already suitable
+ * quoted, so this expansion is usually not required.
+ */
+static int exp_quote(int attribute((unused)) nargs,
+                     char **args,
+                     struct sink *output,
+                     void attribute((unused)) *u) {
+  return sink_writes(output, cgi_sgmlquote(args[0])) < 0 ? -1 : 0;
+}
+
+/*! @who{ID}
+ *
+ * Expands to the name of the submitter of track ID, which must be a playing
+ * track, in the queue, or in the recent list.
+ */
+static int exp_who(int attribute((unused)) nargs,
+                   char **args,
+                   struct sink *output,
+                   void attribute((unused)) *u) {
+  struct queue_entry *q = dcgi_findtrack(args[0]);
+
+  if(q && q->submitter)
+    return sink_writes(output, cgi_sgmlquote(q->submitter)) < 0 ? -1 : 0;
+  return 0;
+}
+
+/*! @when{ID}
+ *
+ * Expands to the time a track started or is expected to start.  The track must
+ * be a playing track, in the queue, or in the recent list.
+ */
+static int exp_when(int attribute((unused)) nargs,
+                   char **args,
+                   struct sink *output,
+                    void attribute((unused)) *u) {
+  struct queue_entry *q = dcgi_findtrack(args[0]);
+  const struct tm *w = 0;
+
+  if(q) {
+    switch(q->state) {
+    case playing_isscratch:
+    case playing_unplayed:
+    case playing_random:
+      if(q->expected)
+       w = localtime(&q->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(q->played)
+       w = localtime(&q->played);
+      break;
+    }
+    if(w)
+      return sink_printf(output, "%d:%02d", w->tm_hour, w->tm_min) < 0 ? -1 : 0;
+  }
+  return sink_writes(output, "&nbsp;") < 0 ? -1 : 0;
+}
+
+/*! @length{ID|TRACK}
+ *
+ * Expands to the length of a track, identified by its queue ID or its name.
+ * If it is the playing track (identified by ID) then the amount played so far
+ * is included.
+ */
+static int exp_length(int attribute((unused)) nargs,
+                   char **args,
+                   struct sink *output,
+                   void attribute((unused)) *u) {
+  struct queue_entry *q;
+  long length = 0;
+  const char *name;
+
+  if(args[0][0] == '/')
+    /* Track identified by name */
+    name = args[0];
+  else {
+    /* Track identified by queue ID */
+    if(!(q = dcgi_findtrack(args[0])))
+      return 0;
+    if(q->state == playing_started || q->state == playing_paused)
+      if(sink_printf(output, "%ld:%02ld/", q->sofar / 60, q->sofar % 60) < 0)
+        return -1;
+    name = q->track;
+  }
+  if(dcgi_client && disorder_length(dcgi_client, name, &length))
+    return sink_printf(output, "%ld:%02ld",
+                       length / 60, length % 60) < 0 ? -1 : 0;
+  return sink_writes(output, "&nbsp;") < 0 ? -1 : 0;
+}
+
+/*! @removable{ID}
+ *
+ * Expands to "true" if track ID is removable (or scratchable, if it is the
+ * playing track) and "false" otherwise.
+ */
+static int exp_removable(int attribute((unused)) nargs,
+                         char **args,
+                         struct sink *output,
+                         void attribute((unused)) *u) {
+  struct queue_entry *q = dcgi_findtrack(args[0]);
+  /* TODO would be better to reject recent */
+
+  if(!q || !dcgi_client)
+    return mx_bool_result(output, 0);
+  dcgi_lookup(DCGI_RIGHTS);
+  return mx_bool_result(output,
+                        (q == dcgi_playing ? right_scratchable : right_removable)
+                            (dcgi_rights, disorder_user(dcgi_client), q));
+}
+
+/*! @movable{ID}{DIR}
+ *
+ * Expands to "true" if track ID is movable and "false" otherwise.
+ *
+ * DIR (which is optional) should be a non-zero integer.  If it is negative
+ * then the intended move is down (later in the queue), if it is positive then
+ * the intended move is up (earlier in the queue).  The first track is not
+ * movable up and the last track not movable down.
+ */
+static int exp_movable(int  nargs,
+                       char **args,
+                       struct sink *output,
+                       void attribute((unused)) *u) {
+  struct queue_entry *q = dcgi_findtrack(args[0]);
+  /* TODO would be better to recent playing/recent */
+
+  if(!q || !dcgi_client)
+    return mx_bool_result(output, 0);
+  if(nargs > 1) {
+    const long dir = atoi(args[1]);
+
+    if(dir > 0 && q == dcgi_queue)
+      return mx_bool_result(output, 0);
+    if(dir < 0 && q->next == 0) 
+      return mx_bool_result(output, 0);
+  }
+  dcgi_lookup(DCGI_RIGHTS);
+  return mx_bool_result(output,
+                        right_movable(dcgi_rights,
+                                      disorder_user(dcgi_client),
+                                      q));
+}
+
+/*! @playing{TEMPLATE}
+ *
+ * Expands to TEMPLATE, with the following expansions:
+ * - @id: the queue ID of the playing track
+ * - @track: the playing track's
+ UNQUOTED name
+ *
+ * If no track is playing expands to nothing.
+ *
+ * TEMPLATE is optional.  If it is left out then instead expands to the queue
+ * ID of the playing track.
+ */
+static int exp_playing(int nargs,
+                       const struct mx_node **args,
+                       struct sink *output,
+                       void *u) {
+  dcgi_lookup(DCGI_PLAYING);
+  if(!dcgi_playing)
+    return 0;
+  if(!nargs)
+    return sink_writes(output, dcgi_playing->id) < 0 ? -1 : 0;
+  return mx_expand(mx_rewritel(args[0],
+                               "id", dcgi_playing->id,
+                               "track", dcgi_playing->track,
+                               (char *)0),
+                   output, u);
+}
+
+/*! @queue{TEMPLATE}
+ *
+ * For each track in the queue, expands TEMPLATE with the following expansions:
+ * - @id: the queue ID of the track
+ * - @track: the UNQUOTED track name
+ * - @index: the track number from 0
+ * - @parity: "even" or "odd" alternately
+ * - @first: "true" on the first track and "false" otherwise
+ * - @last: "true" on the last track and "false" otherwise
+ */
+static int exp_queue(int attribute((unused)) nargs,
+                     const struct mx_node **args,
+                     struct sink *output,
+                     void *u) {
+  struct queue_entry *q;
+  int rc, i;
+  
+  dcgi_lookup(DCGI_QUEUE);
+  for(q = dcgi_queue, i = 0; q; q = q->next, ++i)
+    if((rc = mx_expand(mx_rewritel(args[0],
+                                   "id", q->id,
+                                   "track", q->track,
+                                   "index", make_index(i),
+                                   "parity", i % 2 ? "odd" : "even",
+                                   "first", q == dcgi_queue ? "true" : "false",
+                                   "last", q->next ? "false" : "true",
+                                   (char *)0),
+                       output, u)))
+      return rc;
+  return 0;
+}
+
+/*! @recent{TEMPLATE}
+ *
+ * For each track in the recently played list, expands TEMPLATE with the
+ * following expansions:
+ * - @id: the queue ID of the track
+ * - @track: the UNQUOTED track name
+ * - @index: the track number from 0
+ * - @parity: "even" or "odd" alternately
+ * - @first: "true" on the first track and "false" otherwise
+ * - @last: "true" on the last track and "false" otherwise
+ */
+static int exp_recent(int attribute((unused)) nargs,
+                      const struct mx_node **args,
+                      struct sink *output,
+                      void *u) {
+  struct queue_entry *q;
+  int rc, i;
+  
+  dcgi_lookup(DCGI_RECENT);
+  for(q = dcgi_recent, i = 0; q; q = q->next, ++i)
+    if((rc = mx_expand(mx_rewritel(args[0],
+                                   "id", q->id,
+                                   "track", q->track,
+                                   "index", make_index(i),
+                                   "parity", i % 2 ? "odd" : "even",
+                                   "first", q == dcgi_recent ? "true" : "false",
+                                   "last", q->next ? "false" : "true",
+                                   (char *)0),
+                       output, u)))
+      return rc;
+  return 0;
+}
+
+/*! @new{TEMPLATE}
+ *
+ * For each track in the newly added list, expands TEMPLATE wit the following
+ * expansions:
+ * - @track: the UNQUOTED track name
+ * - @index: the track number from 0
+ * - @parity: "even" or "odd" alternately
+ * - @first: "true" on the first track and "false" otherwise
+ * - @last: "true" on the last track and "false" otherwise
+ *
+ * Note that unlike @playing, @queue and @recent which are otherwise
+ * superficially similar, there is no @id sub-expansion here.
+ */
+static int exp_new(int attribute((unused)) nargs,
+                   const struct mx_node **args,
+                   struct sink *output,
+                   void *u) {
+  int rc, i;
+  
+  dcgi_lookup(DCGI_NEW);
+  /* TODO perhaps we should generate an ID value for tracks in the new list */
+  for(i = 0; i < dcgi_nnew; ++i)
+    if((rc = mx_expand(mx_rewritel(args[0],
+                                   "track", dcgi_new[i],
+                                   "index", make_index(i),
+                                   "parity", i % 2 ? "odd" : "even",
+                                   "first", i == 0 ? "true" : "false",
+                                   "last", i == dcgi_nnew - 1 ? "false" : "true",
+                                   (char *)0),
+                       output, u)))
+      return rc;
+  return 0;
+}
+
+/*! @volume{CHANNEL}
+ *
+ * Expands to the volume in a given channel.  CHANNEL must be "left" or
+ * "right".
+ */
+static int exp_volume(int attribute((unused)) nargs,
+                      char **args,
+                      struct sink *output,
+                      void attribute((unused)) *u) {
+  dcgi_lookup(DCGI_VOLUME);
+  return sink_printf(output, "%d",
+                     !strcmp(args[0], "left")
+                         ? dcgi_volume_left : dcgi_volume_right) < 0 ? -1 : 0;
+}
+
+/*! @isplaying
+ *
+ * Expands to "true" if there is a playing track, otherwise "false".
+ */
+static int exp_isplaying(int attribute((unused)) nargs,
+                         char attribute((unused)) **args,
+                         struct sink *output,
+                         void attribute((unused)) *u) {
+  dcgi_lookup(DCGI_PLAYING);
+  return mx_bool_result(output, !!dcgi_playing);
+}
+
+/*! @isqueue
+ *
+ * Expands to "true" if there the queue is nonempty, otherwise "false".
+ */
+static int exp_isqueue(int attribute((unused)) nargs,
+                       char attribute((unused)) **args,
+                       struct sink *output,
+                       void attribute((unused)) *u) {
+  dcgi_lookup(DCGI_QUEUE);
+  return mx_bool_result(output, !!dcgi_queue);
+}
+
+/*! @isrecent@
+ *
+ * Expands to "true" if there the recently played list is nonempty, otherwise
+ * "false".
+ */
+static int exp_isrecent(int attribute((unused)) nargs,
+                        char attribute((unused)) **args,
+                        struct sink *output,
+                        void attribute((unused)) *u) {
+  dcgi_lookup(DCGI_RECENT);
+  return mx_bool_result(output, !!dcgi_recent);
+}
+
+/*! @isnew
+ *
+ * Expands to "true" if there the newly added track list is nonempty, otherwise
+ * "false".
+ */
+static int exp_isnew(int attribute((unused)) nargs,
+                     char attribute((unused)) **args,
+                     struct sink *output,
+                     void attribute((unused)) *u) {
+  dcgi_lookup(DCGI_NEW);
+  return mx_bool_result(output, !!dcgi_nnew);
+}
+
+/*! @pref{TRACK}{KEY}
+ *
+ * Expands to a track preference.
+ */
+static int exp_pref(int attribute((unused)) nargs,
+                    char **args,
+                    struct sink *output,
+                    void attribute((unused)) *u) {
+  char *value;
+
+  if(dcgi_client && !disorder_get(dcgi_client, args[0], args[1], &value))
+    return sink_writes(output, cgi_sgmlquote(value)) < 0 ? -1 : 0;
+  return 0;
+}
+
+/*! @prefs{TRACK}{TEMPLATE}
+ *
+ * For each track preference of track TRACK, expands TEMPLATE with the
+ * following expansions:
+ * - @name: the UNQUOTED preference name
+ * - @index: the preference number from 0
+ * - @value: the UNQUOTED preference value
+ * - @parity: "even" or "odd" alternately
+ * - @first: "true" on the first preference and "false" otherwise
+ * - @last: "true" on the last preference and "false" otherwise
+ *
+ * Use @quote to quote preference names and values where necessary; see below.
+ */
+static int exp_prefs(int attribute((unused)) nargs,
+                     const struct mx_node **args,
+                     struct sink *output,
+                     void *u) {
+  int rc, i;
+  struct kvp *k, *head;
+  char *track;
+
+  if((rc = mx_expandstr(args[0], &track, u, "argument #0 (TRACK)")))
+    return rc;
+  if(!dcgi_client || disorder_prefs(dcgi_client, track, &head))
+    return 0;
+  for(k = head, i = 0; k; k = k->next, ++i)
+    if((rc = mx_expand(mx_rewritel(args[1],
+                                   "index", make_index(i),
+                                   "parity", i % 2 ? "odd" : "even",
+                                   "name", k->name,
+                                   "value", k->value,
+                                   "first", k == head ? "true" : "false",
+                                   "last", k->next ? "false" : "true",
+                                   (char *)0),
+                       output, u)))
+      return rc;
+  return 0;
+}
+
+/*! @transform{TRACK}{TYPE}{CONTEXT}
+ *
+ * Transforms a track name (if TYPE is "track") or directory name (if type is
+ * "dir").  CONTEXT should be the context, if it is left out then "display" is
+ * assumed.
+ */
+static int exp_transform(int nargs,
+                         char **args,
+                         struct sink *output,
+                         void attribute((unused)) *u) {
+  const char *t = trackname_transform(args[1], args[0],
+                                      (nargs > 2 ? args[2] : "display"));
+  return sink_writes(output, cgi_sgmlquote(t)) < 0 ? -1 : 0;
+}
+
+/*! @enabled@
+ *
+ * Expands to "true" if playing is enabled, otherwise "false".
+ */
+static int exp_enabled(int attribute((unused)) nargs,
+                       char attribute((unused)) **args,
+                       struct sink *output,
+                       void attribute((unused)) *u) {
+  int e = 0;
+
+  if(dcgi_client)
+    disorder_enabled(dcgi_client, &e);
+  return mx_bool_result(output, e);
+}
+
+/*! @random-enabled
+ *
+ * Expands to "true" if random play is enabled, otherwise "false".
+ */
+static int exp_random_enabled(int attribute((unused)) nargs,
+                              char attribute((unused)) **args,
+                              struct sink *output,
+                              void attribute((unused)) *u) {
+  int e = 0;
+
+  if(dcgi_client)
+    disorder_random_enabled(dcgi_client, &e);
+  return mx_bool_result(output, e);
+}
+
+/*! @trackstate{TRACK}
+ *
+ * Expands to "playing" if TRACK is currently playing, or "queue" if it is in
+ * the queue, otherwise to nothing.
+ */
+static int exp_trackstate(int attribute((unused)) nargs,
+                          char **args,
+                          struct sink *output,
+                          void attribute((unused)) *u) {
+  char *track;
+  struct queue_entry *q;
+
+  if(!dcgi_client)
+    return 0;
+  if(disorder_resolve(dcgi_client, &track, args[0]))
+    return 0;
+  dcgi_lookup(DCGI_PLAYING);
+  if(dcgi_playing && !strcmp(track, dcgi_playing->track))
+    return sink_writes(output, "playing") < 0 ? -1 : 0;
+  dcgi_lookup(DCGI_QUEUE);
+  for(q = dcgi_queue; q; q = q->next)
+    if(!strcmp(track, q->track))
+      return sink_writes(output, "queued") < 0 ? -1 : 0;
+  return 0;
+}
+
+/*! @thisurl
+ *
+ * Expands to an UNQUOTED URL which points back to the current page.  (NB it
+ * might not be byte for byte identical - for instance, CGI arguments might be
+ * re-ordered.)
+ */
+static int exp_thisurl(int attribute((unused)) nargs,
+                       char attribute((unused)) **args,
+                       struct sink *output,
+                       void attribute((unused)) *u) {
+  return sink_writes(output, cgi_thisurl(config->url)) < 0 ? -1 : 0;
+}
+
+/*! @resolve{TRACK}
+ *
+ * Expands to an UNQUOTED name for the TRACK that is not an alias, or to
+ * nothing if it is not a valid track.
+ */
+static int exp_resolve(int attribute((unused)) nargs,
+                       char **args,
+                       struct sink *output,
+                       void attribute((unused)) *u) {
+  char *r;
+
+  if(dcgi_client && !disorder_resolve(dcgi_client, &r, args[0]))
+    return sink_writes(output, r) < 0 ? -1 : 0;
+  return 0;
+}
+
+/*! @paused
+ *
+ * Expands to "true" if the playing track is paused, to "false" if it is
+ * playing (or if there is no playing track at all).
+ */
+static int exp_paused(int attribute((unused)) nargs,
+                      char attribute((unused)) **args,
+                      struct sink *output,
+                     void attribute((unused)) *u) {
+  dcgi_lookup(DCGI_PLAYING);
+  return mx_bool_result(output, (dcgi_playing
+                                 && dcgi_playing->state == playing_paused));
+}
+
+/*! @state{ID}@
+ *
+ * Expands to the current state of track ID.
+ */
+static int exp_state(int attribute((unused)) nargs,
+                     char **args,
+                     struct sink *output,
+                     void attribute((unused)) *u) {
+  struct queue_entry *q = dcgi_findtrack(args[0]);
+
+  if(q)
+    return sink_writes(output, playing_states[q->state]) < 0 ? -1 : 0;
+  return 0;
+}
+
+/*! @right{RIGHT}{WITH-RIGHT}{WITHOUT-RIGHT}@
+ *
+ * Expands to WITH-RIGHT if the current user has right RIGHT, otherwise to
+ * WITHOUT-RIGHT.  The WITHOUT-RIGHT argument may be left out.
+ *
+ * If both WITH-RIGHT and WITHOUT-RIGHT are left out then expands to "true" if
+ * the user has the right and "false" otherwise.
+ *
+ * If there is no connection to the server then expands to nothing (in all
+ * cases).
+ */
+static int exp_right(int nargs,
+                     const struct mx_node **args,
+                     struct sink *output,
+                     void *u) {
+  char *right;
+  rights_type r;
+  int rc;
+
+  if(!dcgi_client)
+    return 0;
+  dcgi_lookup(DCGI_RIGHTS);
+  if((rc = mx_expandstr(args[0], &right, u, "argument #0 (RIGHT)")))
+    return rc;
+  if(parse_rights(right, &r, 1/*report*/))
+    return 0;
+  /* Single-argument form */
+  if(nargs == 1)
+    return mx_bool_result(output, !!(r & dcgi_rights));
+  /* Multiple argument form */
+  if(r & dcgi_rights)
+    return mx_expand(args[1], output, u);
+  if(nargs == 3)
+    return mx_expand(args[2], output, u);
+  return 0;
+}
+
+/*! @userinfo{PROPERTY}
+ *
+ * Expands to the named property of the current user.
+ */
+static int exp_userinfo(int attribute((unused)) nargs,
+                        char **args,
+                        struct sink *output,
+                        void attribute((unused)) *u) {
+  char *v;
+
+  if(dcgi_client
+     && !disorder_userinfo(dcgi_client, disorder_user(dcgi_client),
+                           args[0], &v))
+    return sink_writes(output, v) < 0 ? -1 : 0;
+  return 0;
+}
+
+/*! @error
+ *
+ * Expands to the latest error string.
+ */
+static int exp_error(int attribute((unused)) nargs,
+                     char attribute((unused)) **args,
+                     struct sink *output,
+                     void attribute((unused)) *u) {
+  return sink_writes(output, dcgi_error_string ? dcgi_error_string : "")
+              < 0 ? -1 : 0;
+}
+
+/*! @status
+ *
+ * Expands to the latest status string.
+ */
+static int exp_status(int attribute((unused)) nargs,
+                      char attribute((unused)) **args,
+                      struct sink *output,
+                      void attribute((unused)) *u) {
+  return sink_writes(output, dcgi_status_string ? dcgi_status_string : "")
+              < 0 ? -1 : 0;
+}
+
+/*! @image{NAME}
+ *
+ * Expands to the URL of the image called NAME.
+ *
+ * Firstly if there is a label called images.NAME then the image stem will be
+ * the value of that label.  Otherwise the stem will be NAME.png.
+ *
+ * If the label url.static exists then it will give the base URL for images.
+ * Otherwise the base url will be /disorder/.
+ */
+static int exp_image(int attribute((unused)) nargs,
+                     char **args,
+                     struct sink *output,
+                     void attribute((unused)) *u) {
+  const char *url, *stem;
+  char *labelname;
+
+  /* Compute the stem */
+  byte_xasprintf(&labelname, "images.%s", args[0]);
+  if(option_label_exists(labelname))
+    stem = option_label(labelname);
+  else
+    byte_xasprintf((char **)&stem, "%s.png", args[0]);
+  /* If the stem looks like it's reasonalby complete, use that */
+  if(stem[0] == '/'
+     || !strncmp(stem, "http:", 5)
+     || !strncmp(stem, "https:", 6))
+    url = stem;
+  else {
+    /* Compute the URL */
+    if(option_label_exists("url.static"))
+      byte_xasprintf((char **)&url, "%s/%s",
+                     option_label("url.static"), stem);
+    else
+      /* Default base is /disorder */
+      byte_xasprintf((char **)&url, "/disorder/%s", stem);
+  }
+  return sink_writes(output, cgi_sgmlquote(url)) < 0 ? -1 : 0;
+}
+
+/** @brief Compare two @ref entry objects */
+int dcgi_compare_entry(const void *a, const void *b) {
+  const struct dcgi_entry *ea = a, *eb = b;
+
+  return compare_tracks(ea->sort, eb->sort,
+                       ea->display, eb->display,
+                       ea->track, eb->track);
+}
+
+/** @brief Implementation of exp_tracks() and exp_dirs() */
+static int exp__files_dirs(int nargs,
+                           const struct mx_node **args,
+                           struct sink *output,
+                           void *u,
+                           const char *type,
+                           int (*fn)(disorder_client *client,
+                                     const char *name,
+                                     const char *re,
+                                     char ***vecp,
+                                     int *nvecp)) {
+  char **tracks, *dir, *re;
+  int n, ntracks, rc;
+  const struct mx_node *m;
+  struct dcgi_entry *e;
+
+  if((rc = mx_expandstr(args[0], &dir, u, "argument #0 (DIR)")))
+    return rc;
+  if(nargs == 3)  {
+    if((rc = mx_expandstr(args[1], &re, u, "argument #1 (RE)")))
+      return rc;
+    m = args[2];
+  } else {
+    re = 0;
+    m = args[1];
+  }
+  if(!dcgi_client)
+    return 0;
+  /* Get the list */
+  if(fn(dcgi_client, dir, re, &tracks, &ntracks))
+    return 0;
+  /* Sort it.  NB trackname_transform() does not go to the server. */
+  e = xcalloc(ntracks, sizeof *e);
+  for(n = 0; n < ntracks; ++n) {
+    e->track = tracks[n];
+    e[n].track = tracks[n];
+    e[n].sort = trackname_transform(type, tracks[n], "sort");
+    e[n].display = trackname_transform(type, tracks[n], "display");
+  }
+  qsort(e, ntracks, sizeof (struct dcgi_entry), dcgi_compare_entry);
+  /* Expand the subsiduary templates.  We chuck in @sort and @display because
+   * it is particularly easy to do so. */
+  for(n = 0; n < ntracks; ++n)
+    if((rc = mx_expand(mx_rewritel(m,
+                                   "index", make_index(n),
+                                   "parity", n % 2 ? "odd" : "even",
+                                   "track", tracks[n],
+                                   "first", n == 0 ? "true" : "false",
+                                   "last", n + 1 == ntracks ? "false" : "true",
+                                   "sort", e[n].sort,
+                                   "display", e[n].display,
+                                   (char *)0),
+                       output, u)))
+      return rc;
+  return 0;
+
+}
+
+/*! @tracks{DIR}{RE}{TEMPLATE}
+ *
+ * For each track below DIR, expands TEMPLATE with the
+ * following expansions:
+ * - @track: the UNQUOTED track name
+ * - @index: the track number from 0
+ * - @parity: "even" or "odd" alternately
+ * - @first: "true" on the first track and "false" otherwise
+ * - @last: "true" on the last track and "false" otherwise
+ * - @sort: the sort key for this track
+ * - @display: the UNQUOTED display string for this track
+ *
+ * RE is optional and if present is the regexp to match against.
+ */
+static int exp_tracks(int nargs,
+                      const struct mx_node **args,
+                      struct sink *output,
+                      void *u) {
+  return exp__files_dirs(nargs, args, output, u, "track", disorder_files);
+}
+
+/*! @dirs{DIR}{RE}{TEMPLATE}
+ *
+ * For each directory below DIR, expands TEMPLATE with the
+ * following expansions:
+ * - @track: the UNQUOTED directory name
+ * - @index: the directory number from 0
+ * - @parity: "even" or "odd" alternately
+ * - @first: "true" on the first directory and "false" otherwise
+ * - @last: "true" on the last directory and "false" otherwise
+ * - @sort: the sort key for this directory
+ * - @display: the UNQUOTED display string for this directory
+ *
+ * RE is optional and if present is the regexp to match against.
+ */
+static int exp_dirs(int nargs,
+                    const struct mx_node **args,
+                    struct sink *output,
+                    void *u) {
+  return exp__files_dirs(nargs, args, output, u, "dir", disorder_directories);
+}
+
+static int exp__search_shim(disorder_client *c, const char *terms,
+                            const char attribute((unused)) *re,
+                            char ***vecp, int *nvecp) {
+  return disorder_search(c, terms, vecp, nvecp);
+}
+
+/*! @search{KEYWORDS}{TEMPLATE}
+ *
+ * For each track matching KEYWORDS, expands TEMPLATE with the
+ * following expansions:
+ * - @track: the UNQUOTED directory name
+ * - @index: the directory number from 0
+ * - @parity: "even" or "odd" alternately
+ * - @first: "true" on the first directory and "false" otherwise
+ * - @last: "true" on the last directory and "false" otherwise
+ * - @sort: the sort key for this track
+ * - @display: the UNQUOTED display string for this track
+ */
+static int exp_search(int nargs,
+                      const struct mx_node **args,
+                      struct sink *output,
+                      void *u) {
+  return exp__files_dirs(nargs, args, output, u, "track", exp__search_shim);
+}
+
+/*! @label{NAME}
+ *
+ * Expands to label NAME from options.labels.  Undefined lables expand to the
+ * last dot-separated component, e.g. X.Y.Z to Z.
+ */
+static int exp_label(int attribute((unused)) nargs,
+                     char **args,
+                     struct sink *output,
+                     void attribute((unused)) *u) {
+  return sink_writes(output, option_label(args[0])) < 0 ? -1 : 0;
+}
+
+/*! @breadcrumbs{DIR}{TEMPLATE}
+ *
+ * Expands TEMPLATE for each directory in the path up to DIR, excluding the root
+ * but including DIR itself, with the following expansions:
+ * - @dir: the directory
+ * - @last: "true" if this is the last directory, otherwise "false"
+ *
+ * DIR must be an absolute path.
+ */
+static int exp_breadcrumbs(int attribute((unused)) nargs,
+                           const struct mx_node **args,
+                           struct sink *output,
+                           void attribute((unused)) *u) {
+  int rc;
+  char *dir, *parent, *ptr;
+  
+  if((rc = mx_expandstr(args[0], &dir, u, "argument #0 (DIR)")))
+    return rc;
+  /* Reject relative paths */
+  if(dir[0] != '/') {
+    error(0, "breadcrumbs: '%s' is a relative path", dir);
+    return 0;
+  }
+  /* Skip the root */
+  ptr = dir + 1;
+  while(*ptr) {
+    /* Find the end of this directory */
+    while(*ptr && *ptr != '/')
+      ++ptr;
+    parent = xstrndup(dir, ptr - dir);
+    if((rc = mx_expand(mx_rewritel(args[1],
+                                   "dir", parent,
+                                   "last", *ptr ? "false" : "true",
+                                   (char *)0),
+                       output, u)))
+      return rc;
+    if(*ptr)
+      ++ptr;
+  }
+  return 0;
+}
+  
+/** @brief Register DisOrder-specific expansions */
+void dcgi_expansions(void) {
+  mx_register("arg", 1, 1, exp_arg);
+  mx_register("argq", 1, 1, exp_argq);
+  mx_register("enabled", 0, 0, exp_enabled);
+  mx_register("error", 0, 0, exp_error);
+  mx_register("image", 1, 1, exp_image);
+  mx_register("isnew", 0, 0, exp_isnew);
+  mx_register("isplaying", 0, 0, exp_isplaying);
+  mx_register("isqueue", 0, 0, exp_isqueue);
+  mx_register("isrecent", 0, 0, exp_isrecent);
+  mx_register("label",  1, 1, exp_label);
+  mx_register("length", 1, 1, exp_length);
+  mx_register("movable", 1, 2, exp_movable);
+  mx_register("part", 2, 3, exp_part);
+  mx_register("paused", 0, 0, exp_paused);
+  mx_register("pref", 2, 2, exp_pref);
+  mx_register("quote", 1, 1, exp_quote);
+  mx_register("random-enabled", 0, 0, exp_random_enabled);
+  mx_register("removable", 1, 1, exp_removable);
+  mx_register("resolve", 1, 1, exp_resolve);
+  mx_register("server-version", 0, 0, exp_server_version);
+  mx_register("state", 1, 1, exp_state);
+  mx_register("status", 0, 0, exp_status);
+  mx_register("thisurl", 0, 0, exp_thisurl);
+  mx_register("trackstate", 1, 1, exp_trackstate);
+  mx_register("transform", 2, 3, exp_transform);
+  mx_register("url", 0, 0, exp_url);
+  mx_register("user", 0, 0, exp_user);
+  mx_register("userinfo", 1, 1, exp_userinfo);
+  mx_register("version", 0, 0, exp_version);
+  mx_register("volume", 1, 1, exp_volume);
+  mx_register("when", 1, 1, exp_when);
+  mx_register("who", 1, 1, exp_who);
+  mx_register_magic("breadcrumbs", 2, 2, exp_breadcrumbs);
+  mx_register_magic("dirs", 2, 3, exp_dirs);
+  mx_register_magic("new", 1, 1, exp_new);
+  mx_register_magic("playing", 0, 1, exp_playing);
+  mx_register_magic("prefs", 2, 2, exp_prefs);
+  mx_register_magic("queue", 1, 1, exp_queue);
+  mx_register_magic("recent", 1, 1, exp_recent);
+  mx_register_magic("right", 1, 3, exp_right);
+  mx_register_magic("search", 2, 2, exp_search);
+  mx_register_magic("tracks", 2, 3, exp_tracks);
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
diff --git a/server/options.c b/server/options.c
new file mode 100644 (file)
index 0000000..68f8271
--- /dev/null
@@ -0,0 +1,217 @@
+/*
+ * 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
+ */
+/** @file server/options.c
+ * @brief CGI options
+ *
+ * Options represent an additional configuration system private to the
+ * CGI program.
+ */
+
+#include "disorder-cgi.h"
+
+struct column {
+  int ncolumns;
+  char **columns;
+};
+
+struct read_options_state {
+  const char *name;
+  int line;
+};
+
+static hash *labels;
+static hash *columns;
+
+static void option__readfile(const char *name);
+
+static void option__label(int attribute((unused)) nvec,
+                        char **vec) {
+  option_set(vec[0], vec[1]);
+}
+
+static void option__include(int attribute((unused)) nvec,
+                          char **vec) {
+  option__readfile(vec[0]);
+}
+
+static void option__columns(int nvec,
+                          char **vec) {
+  struct column c;
+
+  c.ncolumns = nvec - 1;
+  c.columns = &vec[1];
+  hash_add(columns, vec[0], &c, HASH_INSERT_OR_REPLACE);
+}
+
+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 },
+};
+
+static void option__split_error(const char *msg,
+                              void *u) {
+  struct read_options_state *cs = u;
+  
+  error(0, "%s:%d: %s", cs->name, cs->line, msg);
+}
+
+static void option__readfile(const char *name) {
+  int n, i;
+  FILE *fp;
+  char **vec, *buffer;
+  struct read_options_state cs;
+
+  if(!(cs.name = mx_find(name, 1/*report*/)))
+    return;
+  if(!(fp = fopen(cs.name, "r")))
+    fatal(errno, "error opening %s", cs.name);
+  cs.line = 0;
+  while(!inputline(cs.name, fp, &buffer, '\n')) {
+    ++cs.line;
+    if(!(vec = split(buffer, &n, SPLIT_COMMENTS|SPLIT_QUOTES,
+                    option__split_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 option__init(void) {
+  static int have_read_options;
+  
+  if(!have_read_options) {
+    have_read_options = 1;
+    labels = hash_new(sizeof (char *));
+    columns = hash_new(sizeof (struct column));
+    option__readfile("options");
+  }
+}
+
+/** @brief Set an option
+ * @param name Option name
+ * @param value Option value
+ *
+ * If the option was already set its value is replaced.
+ *
+ * @p name and @p value are copied.
+ */
+void option_set(const char *name, const char *value) {
+  char *v = xstrdup(value);
+
+  option__init();
+  hash_add(labels, name, &v, HASH_INSERT_OR_REPLACE);
+}
+
+/** @brief Get a label
+ * @param key Name of label
+ * @return Value of label (never NULL)
+ *
+ * If label images.X isn't found then the return value is
+ * <url.static>X.png, allowing url.static to be used to provide a base
+ * for all images with per-image overrides.
+ *
+ * Otherwise undefined labels expand to their last (dot-separated)
+ * component.
+ */
+const char *option_label(const char *key) {
+  const char *label;
+  char **lptr;
+
+  option__init();
+  lptr = hash_find(labels, key);
+  if(lptr)
+    return *lptr;
+  /* 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 = option_label("url.static");
+    byte_xasprintf((char **)&label, "%s%s.png", url_static, key + 7);
+  } else if((label = strrchr(key, '.')))
+    /* X.Y defaults to Y */
+    ++label;
+  else
+    /* otherwise default to label name */
+    label = key;
+  return label;
+}
+
+/** @brief Test whether a label exists
+ * @param key Name of label
+ * @return 1 if label exists, otherwise 0
+ *
+ * Labels that don't exist still have an expansion (per option_label()
+ * documentation), and possibly not even a placeholder one.
+ */
+int option_label_exists(const char *key) {
+  option__init();
+  return !!hash_find(labels, key);
+}
+
+/** @brief Return a column list
+ * @param name Context (playing/recent/etc)
+ * @param ncolumns Where to store column count or NULL
+ * @return Pointer to column list
+ */
+char **option_columns(const char *name, int *ncolumns) {
+  struct column *c;
+
+  option__init();
+  c = hash_find(columns, name);
+  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:
+*/
index 886e6a8..bf55c08 100644 (file)
 # USA
 #
 
-pkgdata_DATA=about.html choose.html credits.html playing.html recent.html \
-            stdhead.html stylesheet.html search.html about.html volume.html \
-            prefs.html help.html choosealpha.html topbar.html \
-            topbarend.html error.html new.html login.html \
-            options options.labels \
+pkgdata_DATA=about.tmpl choose.tmpl playing.tmpl recent.tmpl           \
+            about.tmpl prefs.tmpl help.tmpl error.tmpl                 \
+            new.tmpl login.tmpl macros.tmpl                            \
+            options options.labels                                     \
             options.columns
 static_DATA=disorder.css
 staticdir=${pkgdatadir}/static
diff --git a/templates/about.html b/templates/about.html
deleted file mode 100644 (file)
index 93d0d11..0000000
+++ /dev/null
@@ -1,79 +0,0 @@
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
-<!--
-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
--->
-<html>
- <head>
-@include:stdhead@
-  <title>@label:about.title@</title>
- </head>
- <body>
-@include{topbar}@
-   <p><a
-        title="Visit DisOrder web site"
-        href="http://www.greenend.org.uk/rjk/disorder/">
-       <img
-         src="@image:logo@"
-         alt="About DisOrder"
-         style="border-style:none"></a></p>
-   <h2>Copyright</h2>
-
-   <p><a
-    title="DisOrder web site"
-   href="http://www.greenend.org.uk/rjk/disorder/">DisOrder
-   version @version@</a> - select and play digital
-   audio files</p>
-
-   <p>Copyright &copy; 2003-2008 <a href="http://www.greenend.org.uk/rjk/">Richard Kettlewell</a><br>
-   Portions copyright &copy; 2007 <a href="http://www.chiark.greenend.org.uk/~ryounger/">Ross Younger</a><br>
-   Portions copyright &copy; 2007 Mark Wooding</p>
-
-   <p>Portions extracted from
-   <a href="http://mpg321.sourceforge.net/">MPG321</a>,
-   Copyright &copy; 2001 Joe Drew,
-   Copyright &copy; 2000-2001 Robert Leslie</p>
-
-   <p>This program is free software; you can redistribute it and/or
-   modify it under the terms of the <a
-   href="http://www.gnu.org/copyleft/gpl.html">GNU General Public License</a> as
-   published by the Free Software Foundation; either version 2 of the
-   License, or (at your option) any later version.</p>
-
-   <p>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.</p>
-
-   <p>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</p>
-
-@include{topbarend}@
- </body>
-</html>
-@@
-<!--
-Local variables:
-mode:sgml
-sgml-always-quote-attributes:nil
-sgml-indent-step:1
-sgml-indent-data:t
-End:
--->
diff --git a/templates/about.tmpl b/templates/about.tmpl
new file mode 100644 (file)
index 0000000..0495966
--- /dev/null
@@ -0,0 +1,74 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
+<!--
+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
+-->
+<html>
+ <head>
+@stdhead{about}
+ </head>
+ <body>
+@stdmenu{about}
+
+  <p><a title="Visit DisOrder web site"
+  href="http://www.greenend.org.uk/rjk/disorder/"> <img src="@image{logo}"
+  alt="About DisOrder" style="border-style:none"></a></p>
+
+  <h2>Copyright</h2>
+
+  <p><a title="DisOrder web site"
+  href="http://www.greenend.org.uk/rjk/disorder/">DisOrder version @version</a>
+  - software Jukebox</p>
+
+  <p>Copyright &copy; 2003-2008 <a
+  href="http://www.greenend.org.uk/rjk/">Richard Kettlewell</a><br> Portions
+  copyright &copy; 2007 <a
+  href="http://www.chiark.greenend.org.uk/~ryounger/">Ross Younger</a><br>
+  Portions copyright &copy; 2007, 2008 Mark Wooding</p>
+
+  <p>Portions extracted from <a
+  href="http://mpg321.sourceforge.net/">MPG321</a>, Copyright &copy; 2001 Joe
+  Drew, Copyright &copy; 2000-2001 Robert Leslie</p>
+
+  <p>This program is free software; you can redistribute it and/or modify it
+  under the terms of the <a href="http://www.gnu.org/licenses/old-licenses/gpl-2.0.html">GNU
+  General Public License</a> as published by the Free Software Foundation;
+  either version 2 of the License, or (at your option) any later version.</p>
+
+  <p>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.</p>
+
+  <p>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</p>
+
+@credits
+ </body>
+</html>
+@discard{
+Local variables:
+mode:sgml
+sgml-always-quote-attributes:nil
+sgml-indent-step:1
+sgml-indent-data:t
+indent-tabs-mode:nil
+fill-column:79
+End:
+}@#
diff --git a/templates/choose.html b/templates/choose.html
deleted file mode 100644 (file)
index 844fe9a..0000000
+++ /dev/null
@@ -1,160 +0,0 @@
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
-<!--
-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
--->
-<html>
- <head>
-@include:stdhead@
-  <title>@label:choose.title@</title>
- </head>
- <body>
-@include{topbar}@
-   <h1>@label:choose.title@</h1>
-  
-  @#{always have the first-letter bar, if choosealpha enabled}@
-  @if{@eq{@label:sidebar.choosewhich@}{choosealpha}@}{
-
-   <p class=choosealpha>
-    <a title="Directories starting with 'a'"
-    href="@url@?action=choose&#38;regexp=^(the )?a">A</a> |
-    <a title="Directories starting with 'b'"
-    href="@url@?action=choose&#38;regexp=^(the )?b">B</a> |
-    <a title="Directories starting with 'c'"
-    href="@url@?action=choose&#38;regexp=^(the )?c">C</a> |
-    <a title="Directories starting with 'd'"
-    href="@url@?action=choose&#38;regexp=^(the )?d">D</a> |
-    <a title="Directories starting with 'e'"
-    href="@url@?action=choose&#38;regexp=^(the )?e">E</a> |
-    <a title="Directories starting with 'f'"
-    href="@url@?action=choose&#38;regexp=^(the )?f">F</a> |
-    <a title="Directories starting with 'g'"
-    href="@url@?action=choose&#38;regexp=^(the )?g">G</a> |
-    <a title="Directories starting with 'h'"
-    href="@url@?action=choose&#38;regexp=^(the )?h">H</a> |
-    <a title="Directories starting with 'i'"
-    href="@url@?action=choose&#38;regexp=^(the )?i">I</a> |
-    <a title="Directories starting with 'j'"
-    href="@url@?action=choose&#38;regexp=^(the )?j">J</a> |
-    <a title="Directories starting with 'k'"
-    href="@url@?action=choose&#38;regexp=^(the )?k">K</a> |
-    <a title="Directories starting with 'l'"
-    href="@url@?action=choose&#38;regexp=^(the )?l">L</a> |
-    <a title="Directories starting with 'm'"
-    href="@url@?action=choose&#38;regexp=^(the )?m">M</a> |
-    <a title="Directories starting with 'n'"
-    href="@url@?action=choose&#38;regexp=^(the )?n">N</a> |
-    <a title="Directories starting with 'o'"
-    href="@url@?action=choose&#38;regexp=^(the )?o">O</a> |
-    <a title="Directories starting with 'p'"
-    href="@url@?action=choose&#38;regexp=^(the )?p">P</a> |
-    <a title="Directories starting with 'q'"
-    href="@url@?action=choose&#38;regexp=^(the )?q">Q</a> |
-    <a title="Directories starting with 'r'"
-    href="@url@?action=choose&#38;regexp=^(the )?r">R</a> |
-    <a title="Directories starting with 's'"
-    href="@url@?action=choose&#38;regexp=^(the )?s">S</a> |
-    <a title="Directories starting with 't'"
-    href="@url@?action=choose&#38;regexp=^(?!the [^t])t">T</a> |
-    <a title="Directories starting with 'u'"
-    href="@url@?action=choose&#38;regexp=^(the )?u">U</a> |
-    <a title="Directories starting with 'v'"
-    href="@url@?action=choose&#38;regexp=^(the )?v">V</a> |
-    <a title="Directories starting with 'w'"
-    href="@url@?action=choose&#38;regexp=^(the )?w">W</a> |
-    <a title="Directories starting with 'x'"
-    href="@url@?action=choose&#38;regexp=^(the )?x">X</a> |
-    <a title="Directories starting with 'y'"
-    href="@url@?action=choose&#38;regexp=^(the )?y">Y</a> |
-    <a title="Directories starting with 'z'"
-    href="@url@?action=choose&#38;regexp=^(the )?z">Z</a> |
<