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 420efc67b2651f0a780c6401f3b50d9163e8ad4b..b3032ad5d17cf4d14e86ae2bb8821ccfbd478d73 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 1dabfe5b297afd17a4498f6a8499a2b046d42b49..3c13dacc67f6d923a521eedad037fc2da16ab309 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 ce385d7715d172415d70ba1348d04121276a5924..20c38ca2f8c21e5e6de1742fe33de05fcb53dea0 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 0e79171ddff7b82a85455a0b06f2483a8869c006..4b01791ebfab3e7b4955cb3694beb276845ad334 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 e1981f62df5b218181f1791dd1e8005fe3aaac36..bd22e56d9baa7de8c721abe92d065a8c57399276 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 e714a22549b0b5b76f700150cb9a76756f791bac..a774cc65a80a6a6f983e5ea39db22c9aa2a2242a 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 ed488e98764827bdec8b13313a4d353cc1476c5e..b304e5af236b3303d712fe5be3f83cf6870e4f44 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 5320060101d180924ee7a76a3ec8bbe3042f72a6..9c9826f22fd2e16265d05b11568086f1857c04a9 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 c080b4d033beb914f22a433e177b05d0b4aa774a..7af7de02974089f2c68aa72a723b1a58b7ccdee3 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 fdc90745064eceacb06174c68fc9136315782e2d..9b6cf62081fe27fb62a0998db67a9b7af7c6de23 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 a77bd3ac7ffe6967122fc730dd9cbee983c49f9d..f38e1d9cec35e06d6381ba8dfa0051ce363cc442 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 484fda296e8937817fda9d749c226e54bba64899..3dfb0edc8208644bc08707f66e877b7b0b82a318 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 da41426a3abfb51cd9f9034f787f975b0ce839df..5db7585f32215cdeb99a03d55c3e99c60d691029 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 a3e9257f86de2580e00ca8f47d17b7607bd48ebe..e904005d4c654bfb498ac9df850f29b9e4ed8fe5 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 cde716135efce1734254d68738e4511ad8893083..15462ae6a7267794aab55c17f1499206bf4c98cc 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 d2e741b4c009365f038d53915fb47e4511576fcf..3eff101093c7aebb5f5ffbb7493b7f2e59d25098 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 65000c7b0e9d9e0e2d704985e5b2c07cf6694ff0..7f56c5d3ff0cea7c6095ac745ef745bedf14c0a3 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 63d726010410ce33ffac79e5ed18f64367f22666..f5dd036d443eff8581549c539b8b6ab4e38e592a 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 3c43da087bb222f0f0a775e5af1561de9066ca7d..7a0ef5dae61733fce51d76ea0f79e822ea30a0a7 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 2080e63eeadebe4ed747fcd4929717b1152ed3bb..9125c0c3d14f31987574d514580c9779bda9b409 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 6d1991167f682963c14d79f85cb00c4b65cc5560..16f3257d25883ae577feffaab1fb06c58ee6e2d5 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 620c8f49dab121f1576ed2ace234040a72df8905..81574db6ae58032cd5c0d948f22da75278f588ef 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 fa966dda477eddc870a0005e26a6261101ddc15b..17fc7f3b6a05e5da88989ad9410f5766428a675c 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 78497a542b94542dcd89a9c1df87c4b297ffb3dd..9ad35b22e88415a6c83a673910e0bc1f2bf5a1f2 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 5a79bb56c75418b4c9e2d5c792f2df1a6ae04298..253f82eac02a61a0edd69ab93eeb3192ac28c194 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 886e6a8b3b733d30bf55e1a37fe96c652fcab846..bf55c08d25ad82e9af6d4ac3fc8b56f2357a41a1 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> |
-    <a title="Directories starting with anything else"
-    href="@url@?action=choose&#38;regexp=^[^a-z]">*</a>
-   </p>
-  }@
-
-   @if{@ne{@arg:directory@}{}@}{
-   <p class=directoryname>@navigate{@arg:directory@}{/<a
-   href="@url@?action=choose&#38;directory=@urlquote{@fullname@}@">@basename@</a>}@:</p>
-   }@
-
-   @if{@isdirectories@}{
-   <div class=directories><div class=filesdirectories>
-    <p class=heading>
-     @label:choose.directories@
-    </p>
-    @choose{directories}{
-    <p class=entry>
-     <a
-     href="@url@?action=choose&#38;directory=@urlquote{@file@}@"
-     title="@label:choose.directory@">
-     <img class=button
-      src="@image:directory@"
-      alt="">
-      @transform{@file@}{dir}{display}@
-     </a>
-    </p>
-    }@
-   </div></div>
-   }@
-   @if{@isfiles@}{
-   <div class=files><div class=filesdirectories>
-    <p class=heading>
-     @label:choose.files@
-    </p>
-    @choose{files}{
-    <p class=entry>
-    @right{prefs}{<a class=imgprefs
-     href="@url@?action=prefs&#38;0_file=@urlquote{@resolve{@file@}@}@"
-     ><img class=button
-      src="@image:edit@"
-      title="@label:choose.prefsverbose@"
-      alt="@label:choose.prefs@"></a>}@
-     <a
-     href="@url@?action=play&#38;file=@urlquote{@file@}@&#38;back=@urlquote{@thisurl@}@"
-     title="@label:choose.play@">@transform{@file@}{track}{display}@</a>
-     @if{@eq{@trackstate{@file@}@}{playing}@}{[<b>playing</b>]}@
-     @if{@eq{@trackstate{@file@}@}{queued}@}{[<b>queued</b>]}@
-    </p>
-    }@
-    <p class=all>
-     @right{prefs}{<a class=imgprefs
-     href="@url@?action=prefs&#38;directory=@urlquote{@arg:directory@}@&#38;back=@urlquote{@thisurl@}@"
-     ><img class=button 
-      src="@image:edit@"
-      title="@label:choose.allprefsverbose@"
-      alt="@label:choose.allprefs@"></a>}@
-     <a class=button href="@url@?action=play&#38;directory=@urlquote{@arg:directory@}@&#38;back=@urlquote{@thisurl@}@">
-      @label:choose.playall@
-     </a>
-    </p>
-   </div></div>
-   }@
-
-@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/choose.tmpl b/templates/choose.tmpl
new file mode 100644 (file)
index 0000000..383f281
--- /dev/null
@@ -0,0 +1,236 @@
+<!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{choose}
+ </head>
+ <body>
+@stdmenu{choose}
+   <h1>@label{choose.title}</h1>
+  
+  @if{@eq{@label{menu.choosewhich}}{choosealpha}}
+     {
+   <p class=choosealpha>
+    <a title="Directories starting with 'a'"
+    href="@url?action=choose&#38;re=^(the )?a">A</a> |
+    <a title="Directories starting with 'b'"
+    href="@url?action=choose&#38;re=^(the )?b">B</a> |
+    <a title="Directories starting with 'c'"
+    href="@url?action=choose&#38;re=^(the )?c">C</a> |
+    <a title="Directories starting with 'd'"
+    href="@url?action=choose&#38;re=^(the )?d">D</a> |
+    <a title="Directories starting with 'e'"
+    href="@url?action=choose&#38;re=^(the )?e">E</a> |
+    <a title="Directories starting with 'f'"
+    href="@url?action=choose&#38;re=^(the )?f">F</a> |
+    <a title="Directories starting with 'g'"
+    href="@url?action=choose&#38;re=^(the )?g">G</a> |
+    <a title="Directories starting with 'h'"
+    href="@url?action=choose&#38;re=^(the )?h">H</a> |
+    <a title="Directories starting with 'i'"
+    href="@url?action=choose&#38;re=^(the )?i">I</a> |
+    <a title="Directories starting with 'j'"
+    href="@url?action=choose&#38;re=^(the )?j">J</a> |
+    <a title="Directories starting with 'k'"
+    href="@url?action=choose&#38;re=^(the )?k">K</a> |
+    <a title="Directories starting with 'l'"
+    href="@url?action=choose&#38;re=^(the )?l">L</a> |
+    <a title="Directories starting with 'm'"
+    href="@url?action=choose&#38;re=^(the )?m">M</a> |
+    <a title="Directories starting with 'n'"
+    href="@url?action=choose&#38;re=^(the )?n">N</a> |
+    <a title="Directories starting with 'o'"
+    href="@url?action=choose&#38;re=^(the )?o">O</a> |
+    <a title="Directories starting with 'p'"
+    href="@url?action=choose&#38;re=^(the )?p">P</a> |
+    <a title="Directories starting with 'q'"
+    href="@url?action=choose&#38;re=^(the )?q">Q</a> |
+    <a title="Directories starting with 'r'"
+    href="@url?action=choose&#38;re=^(the )?r">R</a> |
+    <a title="Directories starting with 's'"
+    href="@url?action=choose&#38;re=^(the )?s">S</a> |
+    <a title="Directories starting with 't'"
+    href="@url?action=choose&#38;re=^(?!the [^t])t">T</a> |
+    <a title="Directories starting with 'u'"
+    href="@url?action=choose&#38;re=^(the )?u">U</a> |
+    <a title="Directories starting with 'v'"
+    href="@url?action=choose&#38;re=^(the )?v">V</a> |
+    <a title="Directories starting with 'w'"
+    href="@url?action=choose&#38;re=^(the )?w">W</a> |
+    <a title="Directories starting with 'x'"
+    href="@url?action=choose&#38;re=^(the )?x">X</a> |
+    <a title="Directories starting with 'y'"
+    href="@url?action=choose&#38;re=^(the )?y">Y</a> |
+    <a title="Directories starting with 'z'"
+    href="@url?action=choose&#38;re=^(the )?z">Z</a> |
+    <a title="Directories starting with anything else"
+    href="@url?action=choose&#38;re=^[^a-z]">*</a>
+   </p>
+}
+
+@# Always have a search form
+   <form class=search
+         action="@url"
+         method=POST
+         enctype="multipart/form-data" accept-charset=utf-8>
+     <p class=search>Enter search terms:
+       <input class=query name=query type=text value="@argq{query}"
+              size=50>
+       <button class=search name=submit type=submit>
+         @label{search.search}
+       </button>
+       <input name=action type=hidden value=choose>
+       <a class=button
+          href="@url?action=choose">
+         @label{search.clear}
+       </a>
+     </p>
+   </form>
+
+   @if{@ne{@arg{query}}{}}
+      {@# There's a search query
+  <table class=search>
+    <tr class=headings>
+     <th class=artist>@label{heading.artist}</th>
+     <th class=album>@label{heading.album}</th>
+     <th class=title>@label{heading.title}</th>
+     <th class=length>@label{heading.length}</th>
+@right{prefs}{
+     <th class=button>&nbsp;</th>}
+    </tr>
+    @search{@arg{query}}{
+    <tr class=@parity>
+     <td class=artist>@martist{search}{@track}</td>
+     <td class=album>@malbum{search}{@track}</td>
+     <td class=title>@mtitleplay{search}{@track}</td>
+     <td class=length>@length{@track}</td>
+     @right{prefs}{
+     <td class=imgbutton>
+      <a class=imgbutton
+         href="@url?action=prefs&#38;track=@urlquote{@track}">
+       <img class=button src="@image{edit}"
+            title="@label{choose.prefsverbose}"
+            alt="@label{choose.prefs}">
+      </a>
+     </td>
+    </tr>}
+    }
+  </table>}
+      {@# No search query
+
+@# We have the following possible cases:
+@#  choose.which  dir       re       result
+@#  choose        ""        -        Ask for top level
+@#  choose        ""        present  Ask for top level with re applied
+@#  choose        present   -        Ask for dir
+@#  choose        present   re       Ask for dir with re applied
+@#  choosealpha   ""        -        Nothing
+@#  choosealpha   ""        present  Ask for top level with re applied
+@#  choosealpha   present   -        Ask for dir
+@#  choosealpha   present   re       Ask for dir with re applied
+@#
+@# So in fact the only oddity is choosealpha + no dir + no re.
+@#
+@# NB we don't really bother distinguishing between re being empty or absent,
+@# and the server deliberately cooperates in this.
+
+   @if{@ne{@arg{dir}}{}}{
+   <p class=directoryname>@#
+@breadcrumbs{@arg{dir}} 
+            {/<a href="@url?action=choose&#38;dir=@urlquote{@dir}">@#
+@basename{@dir}</a>}:</p>}
+
+@#  <p>dir=[@arg{dir}]<br>re=[@arg{re}]<br>which=@label{choose.which}</p>
+
+  @if{@or{@ne{@label{menu.choosewhich}}{choosealpha}}
+         {@ne{@arg{dir}}{}}
+         {@ne{@arg{re}}{}}}
+     {@#
+   <div class=directories>
+    <div class=filesdirectories>
+     @dirs{@arg{dir}}{@arg{re}}{
+      <p class=entry>
+       <a href="@url?action=choose&#38;dir=@urlquote{@track}">
+        <img class=button src="@image{directory}" alt="">
+        @display
+       </a>
+      </p>}
+    </div>
+   </div>
+
+@define{sometracks}{template}{}
+
+   <div class=files>
+    <div class=filesdirectories>
+     @tracks{@arg{dir}}{@arg{re}}{
+      <p class=entry>
+@define{sometracks}{template}{@template}@#
+       @right{prefs}{
+        <a class=imgprefs
+           href="@url?action=prefs&#38;track=@urlquote{@resolve{@track}}">
+         <img class=button
+              src="@image{edit}"
+              title="@label{choose.prefsverbose}"
+              alt="@label{choose.prefs}">
+        </a>
+       }@#
+       <a href="@url?action=play&#38;track=@urlquote{@track}&#38;back=@urlquote{@thisurl}"
+          title="@label{choose.play}">
+        @display
+       </a>
+       @if{@eq{@trackstate{@track}}{playing}}
+          {[<b>playing</b>]}
+       @if{@eq{@trackstate{@track}}{queued}}
+          {[<b>queued</b>]}
+      </p>}
+      @sometracks{
+      <p class=all>
+        @right{prefs}{
+        <a class=imgprefs
+           href="@url?action=prefs&#38;dir=@urlquote{@arg{dir}}">
+         <img class=button
+              src="@image{edit}"
+              title="@label{choose.allprefsverbose}"
+              alt="@label{choose.allprefs}">
+        </a>}
+        <a class=button
+           href="@url?action=play&#38;dir=@urlquote{@arg{dir}}&#38;back=@urlquote{@thisurl}">
+         @label{choose.playall}
+        </a>
+      </p>}
+    </div>
+   </div>
+   }
+   }
+
+@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/choosealpha.html b/templates/choosealpha.html
deleted file mode 100644 (file)
index 4027834..0000000
+++ /dev/null
@@ -1,98 +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>
-
-   <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> |
-    <a title="Directories starting with anything else"
-    href="@url@?action=choose&#38;regexp=^[^a-z]">*</a>
-   </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/credits.html b/templates/credits.html
deleted file mode 100644 (file)
index 8bdadca..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-<p class=credits><a
-href="http://www.greenend.org.uk/rjk/disorder/"
-title="DisOrder web site">DisOrder
-version @version@</a> &copy; 2003-2008 Richard Kettlewell</p>
-@@
-<!--
-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
--->
index a1697c150ce335433f9ca2bffdcb6b5ea584e74a..578297506de52507bb63f306c2b2986bd8950322 100644 (file)
@@ -60,19 +60,19 @@ table {
 
 /* playing, recent and new classes correspond to the tables in playing.html,
  * recent.html and new.html */
-table.playing, table.recent, table.new {
+table.playing, table.recent, table.new, table.search {
   width: 100%                          /* use full screen width */
 }
 
-table.playing th, table.recent th, table.new th {
+table.playing th, table.recent th, table.new th, table.search th {
   text-align: left             /* titles should be left-aligned */
 }
 
-table.playing td, table.recent td, table.new td {
+table.playing td, table.recent td, table.new td, table.search td {
   vertical-align: middle        /* centre cell contents vertically */
 }
 
-table.playing a, table.recent a, table.new a {
+table.playing a, table.recent a, table.new a, table.search a {
   color: black
 }
 
similarity index 80%
rename from templates/error.html
rename to templates/error.tmpl
index 64ad6ff60e917e692b7c6f07cb27e5bad4064abc..715e61d30cee0bac6ef7281cf5f6e5a6f6687a2c 100644 (file)
@@ -20,28 +20,25 @@ USA
 -->
 <html>
  <head>
-@include:stdhead@
-  <title>@label:error.title@</title>
+@stdhead{error}
  </head>
  <body>
-@include{topbar}@
-   <h1>@label:error.title@</h1>
+@stdmenu{error}
+   <h1>@label{error.title}</h1>
     
   <div class=error>
-    <p class=error>@label{error.@label:error@}@</p>
-
-    <p class=error>@label:error.generic@</p>
+    <p class=error>@label{error.@error}</p>
+    <p class=error>@label{error.generic}</p>
   </div>
 
-@include{topbarend}@
+@credits
  </body>
 </html>
-@@
-<!--
+@discard{
 Local variables:
 mode:sgml
 sgml-always-quote-attributes:nil
 sgml-indent-step:1
 sgml-indent-data:t
 End:
--->
+}@#
similarity index 66%
rename from templates/help.html
rename to templates/help.tmpl
index 8e298efbbd387dc019aa65115bc67ccb819ebb47..3c1536f2d5bf341cbdacdbd11e25d781734878ca 100644 (file)
@@ -20,12 +20,11 @@ USA
 -->
 <html>
  <head>
-@include{stdhead}@
-  <title>@label{help.title}@</title>
+@stdhead{help}
  </head>
  <body>
-@include{topbar}@
-   <h1>@label{help.title}@</h1>
+@stdmenu{help}
+   <h1>@label{help.title}</h1>
 
    <h2 class=helptitle>Introduction</h2>
 
@@ -48,8 +47,8 @@ USA
     being listed first.)  Where possible, estimated start times are
     given.</p>
 
-    <p>Each track has a <img class=button src="@image:scratch@"
-    title="@label:playing.scratch@" alt="@label:playing.scratch@">
+    <p>Each track has a <img class=button src="@image{remove}"
+    title="@label{playing.remove}" alt="@label{playing.remove}">
     button next to it.  For the currently playing track this can be
     used to stop playing the track before it has finished; this is
     called &ldquo;scratching&rdquo;.  For a track in the queue it
@@ -76,32 +75,32 @@ USA
 
    <ul>
     <li>Pause.  This button can be used to pause playing (provided the
-    player supports it).  <img width=16 height=16 class=imgbutton
-    src="@image:enabled@"> indicates that playing is paused,
-    <img width=16 height=16 class=imgbutton
-    src="@image:disabled@"> that it is not.</li>
+    player supports it).  <img width=@width height=@height class=imgbutton
+    src="@image{enabled}"> indicates that playing is paused,
+    <img width=@width height=@height class=imgbutton
+    src="@image{disabled}"> that it is not.</li>
 
     <li>Enable/disable random play.  If disabled then queued tracks
     will still be played but if the queue is empty nothing will be
-    picked at random.  <img width=16 height=16 class=imgbutton
-    src="@image:enabled@"> indicates that random play is
-    enabled, <img width=16 height=16 class=imgbutton
-    src="@image:disabled@"> that it is disabled.</li>
+    picked at random.  <img width=@width height=@height class=imgbutton
+    src="@image{enabled}"> indicates that random play is
+    enabled, <img width=@width height=@height class=imgbutton
+    src="@image{disabled}"> that it is disabled.</li>
 
     <li>Enable/disable play.  If disabled then tracks in the queue
     will not be played, but will remain in the queue instead.  <img
-    width=16 height=16 class=imgbutton src="@image:enabled@">
-    indicates that play is enabled, <img width=16 height=16
-    class=imgbutton src="@image:disabled@"> that it is
+    width=@width height=@height class=imgbutton src="@image{enabled}">
+    indicates that play is enabled, <img width=@width height=@height
+    class=imgbutton src="@image{disabled}"> that it is
     disabled.</li>
 
     <li>Volume control.  You can use the <img class=button
-       src="@image:up@"
-       title="@label:volume.increase@"
-       alt="@label:volume.increase@"> and <img
-       src="@image:down@"
-       title="@label:volume.reduce@"
-       alt="@label:volume.reduce@"> buttons to increase or
+       src="@image{up}"
+       title="@label{volume.up}"
+       alt="@label{volume.up}"> and <img
+       src="@image{down}"
+       title="@label{volume.down}"
+       alt="@label{volume.down}"> buttons to increase or
     decrease the volume, or enter new volume settings for the left
     and/or right speakers.</li>
 
@@ -110,15 +109,15 @@ USA
    <p>Below this is the same table of current and queued tracks as for
     the main playing screen, but with extra buttons for managing the
     queue.
-    The <img class=button src="@image:up@"
-     title="@label:playing.up@" alt="@label:playing.up@"> and <img
-     src="@image:down@" title="@label:playing.down@"
-     alt="@label:playing.down@"> buttons on each track move that
+    The <img class=button src="@image{up}"
+     title="@label{playing.up}" alt="@label{playing.up}"> and <img
+     src="@image{down}" title="@label{playing.down}"
+     alt="@label{playing.down}"> buttons on each track move that
     track around in the queue.  Similarly the <img class=button
-     src="@image:upall@" title="@label:playing.upall@"
-     alt="@label:playing.upall@"> and <img
-     src="@image:downall@" title="@label:playing.downall@"
-     alt="@label:playing.downall@"> buttons move each track to the head or
+     src="@image{upall}" title="@label{playing.upall}"
+     alt="@label{playing.upall}"> and <img
+     src="@image{downall}" title="@label{playing.downall}"
+     alt="@label{playing.downall}"> buttons move each track to the head or
     tail of the queue.</p>
 
     <p>If you are not logged in, or if your user has limited rights,
@@ -133,15 +132,15 @@ USA
    <div class=helpsection>
 
     <p>This screen displays recently played tracks, most recent first.
-    The <img class=button src="@image:edit@"
-    title="@label:choose.prefs@" alt="@label:choose.prefs@">
+    The <img class=button src="@image{edit}"
+    title="@label{choose.prefs}" alt="@label{choose.prefs}">
     button can be used to edit the details for a track; see <a
     href="#prefs">Editing Preferences</a> below.</p>
 
     <p>The number of tracks remembered is controlled by the server
     configuration.  See the <span class=configuration>history</span>
     option in <a
-    href="@url@?action=disorder_config.5">disorder_config(5)</a> for
+    href="@url?action=disorder_config.5">disorder_config(5)</a> for
     more details.</p>
 
    </div>
@@ -152,15 +151,15 @@ USA
 
     <p>This screen displays tracks recently added to the database,
     most recent first.  The <img class=button
-    src="@image:edit@" title="@label:choose.prefs@"
-    alt="@label:choose.prefs@"> button can be used to edit the details
+    src="@image{edit}" title="@label{choose.prefs}"
+    alt="@label{choose.prefs}"> button can be used to edit the details
     for a track; see <a href="#prefs">Editing Preferences</a> below,
     and clicking on the track title will add it to the queue.</p>
 
     <p>The time tracks are remembered for is controlled by the server
     configuration.  See the <span
     class=configuration>noticed_history</span> option in <a
-    href="@url@?action=disorder_config.5">disorder_config(5)</a> for
+    href="@url?action=disorder_config.5">disorder_config(5)</a> for
     more details.</p>
 
    </div>
@@ -170,32 +169,66 @@ USA
    <div class=helpsection>
 
     <p>This screen allows you to choose a track to be played, by navigating
-     through the directory structure of the tracks filesystem.  The following
-     buttons appear:</p>
+     through the directory structure of the tracks filesystem.
+
+  @if{@eq{@label{menu.choosewhich}}{choosealpha}}
+     {
+
+     <p>At the top of the page is a list of first letters.  Click on
+     any of these takes you to a list of directories starting with
+     that letter.</p>
+
+     <p>Below this is a search form.  You can enter one or more
+     keywords here and select @label{search.search} and get a list of
+     tracks containing all those keywords.  It is possible to limit
+     search results to tracks with a particular tag, by using
+     <b>tag:</b><i>TAG</i> among the search terms.  Select
+     @label{search.clear} to clear the search results.</p>
+
+     <p>Initially the rest of the page is empty.  However when you
+     have selected an initial letter, you will see a list of the
+     matching directories and files.  Select a directory to enter it,
+     or a file to play it.
+     
+     } 
+     {
+
+     <p>At the top of the page is a search form.  You can enter one or
+     more keywords here and select @label{search.search} and get a
+     list of tracks containing all those keywords.  It is possible to
+     limit search results to tracks with a particular tag, by using
+     <b>tag:</b><i>TAG</i> among the search terms.</p>
+
+     <p>Below this appears a list of top-level files and directories.
+     Select a directory to enter it, or a file to play it.</p>
+     
+     }
 
+     <p>The following icons will also appear:</p>
+     
     <table class=helpbuttons>
      <tbody>
       <tr>
        <td><img
-       class=button src="@image:edit@"
-       title="@label:choose.prefs@"
-       alt="@label:choose.prefs@"></td>
+       class=button src="@image{edit}"
+       title="@label{choose.prefs}"
+       alt="@label{choose.prefs}"></td>
        <td>This button can be used to edit the details for a
        track; see <a href="#prefs">Editing Preferences</a> below.</td>
       </tr>
       <tr>
-       <td><span class=button>@label{choose.playall}@</span></td>
+       <td><span class=button>@label{choose.playall}</span></td>
        <td>This button plays all the tracks in a directory,
        in order.  This is used to efficiently play a whole album.</td>
       </tr>
      </tbody>
     </table>
-
-    <p>This screen has two forms: <a
-    href="@url@?action=choose">choose</a>, which give
-    you all the top-level directories at once, and <a
-    href="@url@?action=choosealpha">choosealpha</a>,
-    which breaks them down by initial letter.</p>
+     
+    <p>NB.  Some keywords, known as &ldquo;stopwords&rdquo;, are
+    excluded from the search, and will never match.  See the <span
+    class=configuration>stopword</span> option in <a
+    href="@url?action=disorder_config.5">disorder_config(5)</a> for
+    further details about this.</p>
 
     <p>This screen will may not be available if you are not logged in
     or if your user has limited rights.</p>
@@ -230,28 +263,6 @@ USA
 
    </div>
 
-   <h2 class=helptitle>Search</h2>
-
-   <div class=helpsection>
-
-    <p>This screen allows you to search for keywords in track names.  If you
-    specify more than one keyword then only tracks containing all of them are
-    listed.  Results are grouped by artist, album and title.</p>
-
-    <p>It is possible to limit results to tracks with a particular
-    tag, by using <b>tag:</b><i>TAG</i> among the search terms.</p>
-
-    <p>Some keywords, known as &ldquo;stopwords&rdquo;, are excluded
-    from the search, and will never match.  See the <span
-    class=configuration>stopword</span> option in <a
-    href="@url@?action=disorder_config.5">disorder_config(5)</a> for
-    further details about this.</p>
-
-    <p>This screen will may not be available if you are not logged in
-    or if your user has limited rights.</p>
-
-   </div>
-
   <h2 class=helptitle><a name=Login>Login</a></h2>
 
   <div class=helpsection>
@@ -312,37 +323,41 @@ USA
 
    <div class=helpsection>
 
-    <p><a href="@url@?action=disorder_config.5">disorder_config(5)</a> -
+    <p><a href="@url?action=disorder_config.5">disorder_config(5)</a> -
      configuration</p>
 
-    <p><a href="@url@?action=disorder.1">disorder(1)</a> - command line
+    <p><a href="@url?action=disorder_templates.5">disorder_templates(5)</a> -
+     template language</p>
+
+    <p><a href="@url?action=disorder.1">disorder(1)</a> - command line
      client</p>
 
-    <p><a href="@url@?action=disobedience.1">disobedience(1)</a> - GTK+
+    <p><a href="@url?action=disobedience.1">disobedience(1)</a> - GTK+
      client</p>
 
-    <p><a href="@url@?action=disorderd.8">disorderd(8)</a> - server</p>
+    <p><a href="@url?action=disorderd.8">disorderd(8)</a> - server</p>
 
-    <p><a href="@url@?action=disorder-dump.8">disorder-dump(8)</a> -
+    <p><a href="@url?action=disorder-dump.8">disorder-dump(8)</a> -
      dump/restore preferences database</p>
 
-    <p><a href="@url@?action=disorder.3">disorder(3)</a> - C API</p>
+    <p><a href="@url?action=disorder.3">disorder(3)</a> - C API</p>
 
-    <p><a href="@url@?action=disorder_protocol.5">disorder_protocol(5)</a> -
+    <p><a href="@url?action=disorder_protocol.5">disorder_protocol(5)</a> -
      DisOrder control protocol</p>
 
    </div>
 
-@include{topbarend}@
+@credits
   </div>
  </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:
--->
+}@#
similarity index 67%
rename from templates/login.html
rename to templates/login.tmpl
index 723b6afcb513f147bd84f8a911c1e3958ca41691..24b26be308db509f34dec208a4aea1fe0972acb8 100644 (file)
@@ -20,45 +20,42 @@ USA
 -->
 <html>
  <head>
-@include:stdhead@
-  <title>@label:login.title@</title>
+@stdhead{login}
  </head>
  <body>
-@include{topbar}@
-   <h1>@label:login.title@</h1>
-
-   @if{@ne{@label:error@}{error}@}{
-   @#{error reporting from some earlier operation}@
-   <div class=error>
-     <p class=error>@label{error.@label:error@}@</p>
-   </div>
-   }@
-
-   @if{@ne{@label:status@}{status}@}{
-   @#{some action succeeded}@
-   <div class=loginstatus>
-     <p>@label{login.@label:status@}@</p>
-   </div>
-   }@
-
-   @if{@eq{@user@}{guest}@}{
-   @#{guest user, allow login and registration}@
+@stdmenu{login}
+   <h1>@label{login.title}</h1>
+
+@if{@ne{@error}{}}
+   {@# Error reporting from some earlier operation
+     <div class=error>
+       <p class=error>@label{error.@error}</p>
+     </div>}
+
+@if{@ne{@status}{}}
+   {@# Some action succeeded
+     <div class=loginstatus>
+       <p>@label{login.@status}</p>
+     </div>}
+
+@if{@eq{@user}{guest}}
+   {@# Guest user, allow login and registration
    <h2>Existing users</h2>
 
    <p>If you have a username, use this form to log in.</p>
 
-   <form class=login action="@url@" method=POST
+   <form class=login action="@url" method=POST
          enctype="multipart/form-data" accept-charset=utf-8>
      <table class=login>
        <tr>
-         <td>@label:login.username@</td>
+         <td>@label{login.username}</td>
          <td>
            <input class=username name=username type=text size=32
-                 value="@arg:username@">
+                 value="@argq{username}">
          </td>
        </tr>
        <tr>
-         <td>@label:login.password@</td>
+         <td>@label{login.password}</td>
          <td>
            <input class=password name=password type=password value=""
                   size=32>
@@ -67,13 +64,13 @@ USA
        <tr>
          <td colspan=2>
            <button class=login name=button type=submit>
-             @label:login.login@
+             @label{login.login}
            </button>
          </td>
        </tr>
      </table>
      <input name=action type=hidden value=login>
-     <input name=back type=hidden value="@arg:back@">
+     <input name=back type=hidden value="@argq{back}">
    </form>
 
    <p>If you've forgotten your password, use this form to request an
@@ -81,20 +78,20 @@ USA
    your email address, and if a reminder has been sent too recently
    then it won't be possible to send one.</p>
 
-   <form class=reminder action="@url@" method=POST
+   <form class=reminder action="@url" method=POST
          enctype="multipart/form-data" accept-charset=utf-8>
      <table class=login>
        <tr>
-         <td>@label:login.username@</td>
+         <td>@label{login.username}</td>
          <td>
            <input class=username name=username type=text size=32
-                 value="@arg:username@">
+                 value="@argq{username}">
          </td>
        </tr>
        <tr>
          <td colspan=2>
            <button class=login name=button type=submit>
-             @label:login.reminder@
+             @label{login.reminder}
            </button>
          </td>
        </tr>
@@ -110,61 +107,60 @@ USA
    which you must visit to activate your login before you can use
    it.<p>
 
-   <form class=register action="@url@" method=POST
+   <form class=register action="@url" method=POST
          enctype="multipart/form-data" accept-charset=utf-8>
      <table class=register>
        <tr>
-         <td>@label:login.username@</td>
+         <td>@label{login.username}</td>
          <td>
            <input class=username name=username type=text size=32
                  value="">
          </td>
-         <td class=extra>@label:login.registerusernameextra@</td>
+         <td class=extra>@label{login.registerusernameextra}</td>
        </tr>
        <tr>
-         <td>@label:login.email@</td>
+         <td>@label{login.email}</td>
          <td>
            <input class=email name=email type=text size=32
                   value="">
          </td>
-         <td class=extra>@label:login.registeremailextra@</td>
+         <td class=extra>@label{login.registeremailextra}</td>
        </tr>
        <tr>
-         <td>@label:login.password1@</td>
+         <td>@label{login.password1}</td>
          <td>
            <input class=password name=password1 type=password size=32
                  value="">
          </td>
-         <td class=extra>@label:login.registerpassword1extra@</td>
+         <td class=extra>@label{login.registerpassword1extra}</td>
        </tr>
        <tr>
-         <td>@label:login.password2@</td>
+         <td>@label{login.password2}</td>
          <td>
            <input class=password name=password2 type=password size=32
                  value="">
          </td>
-         <td class=extra>@label:login.registerpassword2extra@</td>
+         <td class=extra>@label{login.registerpassword2extra}</td>
        </tr>
        <tr>
          <td colspan=3>
            <button class=register name=button>
-             @label:login.register@
+             @label{login.register}
            </button>
          </td>
        </tr>
      </table>
      <input name=action type=hidden value=register>
-   </form>}@
-   }{
-   @#{not the guest user, allow change of details and logout}@
+   </form>}
+   }{@# not the guest user, allow change of details and logout
 
-   <h2>Logged in as @user@</h2>
+   <h2>Logged in as @user</h2>
 
-   <form class=logout action="@url@" method=POST
+   <form class=logout action="@url" method=POST
          enctype="multipart/form-data" accept-charset=utf-8>
      <div class=logout>
        <button class=logout name=submit type=submit>
-         @label:login.logout@
+         @label{login.logout}
        </button>
      </div>
      <input name=action type=hidden value=logout>
@@ -172,37 +168,37 @@ USA
 
    <p>Use this form to change your email address and/or password.</p>
 
-   <form class=edituser action="@url@" method=POST
+   <form class=edituser action="@url" method=POST
          enctype="multipart/form-data" accept-charset=utf-8>
      <table class=edituser>
        <tr>
-         <td>@label:login.email@</td>
+         <td>@label{login.email}</td>
          <td>
            <input class=email name=email type=text size=32
-                 value="@userinfo:email@">
+                 value="@userinfo{email}">
          </td>
-         <td class=extra>@label:login.edituseremailextra@</td>
+         <td class=extra>@label{login.edituseremailextra}</td>
        </tr>
        <tr>
-         <td>@label:login.newpassword@</td>
+         <td>@label{login.newpassword}</td>
          <td>
            <input class=password name=changepassword1 type=password size=32
                  value="">
          </td>
-         <td class=extra>@label:login.edituserpassword1extra@</td>
+         <td class=extra>@label{login.edituserpassword1extra}</td>
        </tr>
        <tr>
-         <td>@label:login.newpassword@</td>
+         <td>@label{login.newpassword}</td>
          <td>
            <input class=password name=changepassword2 type=password size=32
                  value="">
          </td>
-         <td class=extra>@label:login.edituserpassword2extra@</td>
+         <td class=extra>@label{login.edituserpassword2extra}</td>
        </tr>
        <tr>
          <td colspan=3>
            <button class=edituser name=submit type=submit>
-             @label:login.edituser@
+             @label{login.edituser}
            </button>
          </td>
        </tr>
@@ -210,17 +206,18 @@ USA
      <input name=action type=hidden value=edituser>
    </form>
 
-   }@
+   }
 
-@include{topbarend}@
+@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/macros.tmpl b/templates/macros.tmpl
new file mode 100644 (file)
index 0000000..1bfd97f
--- /dev/null
@@ -0,0 +1,254 @@
+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
+
+
+------------------------------------------------------------------------
+Standard macros
+
+
+This file is read before any other template.  Its output is discarded
+but side effects (definitions of macros) are retained.  To override it,
+create /etc/disorder/macros.tmpl that includes this file by absolute path
+and then redefines macros as desired.
+------------------------------------------------------------------------
+
+
+@# Standard width and height of an image
+@define {width} {} {16}
+@define {height} {} {16}
+
+@# Standard <head> text
+@#  @name is the name of this page, for the <title> element
+@define {stdhead} {name}
+        {@quiethead
+<title>@label{@name.title}</title>}
+
+@# Non-displaying <head> text
+@define {quiethead} {}
+        {  <link rel=stylesheet
+        type="text/css"
+        href="@label{url.static}/disorder.css">}
+
+@# Standard menu
+@#   @current is the name of the current page, e.g. choosealpha, login
+@define {stdmenu} {current}
+        {  <p class=menubar>
+@menuitem{@current}{playing}{true}
+@menuitem{@current}{recent}{true}
+@menuitem{@current}{choose}{@right{play}}
+@menuitem{@current}{manage}{true}
+@menuitem{@current}{new}{true}
+@menuitem{@current}{login}{true}
+@menuitem{@current}{help}{true}
+@menuitem{@current}{about}{true}
+   </p>
+   <hr>
+}
+
+@# Menu entry
+@#  Used by @stdmenu
+@#  @current is the name of the current page (not necessarily the action that
+@#           got there!)
+@#  @name is the name of the menu item
+@#  @available is true if the menu item is available, else false
+@#  
+@define {menuitem} {current name available}  
+        {@if{@available}
+            {   <a @if{@eq{@current}{@name}}
+                      {class=activemenu}
+                      {class=inactivemenu}
+@if{@eq{name}{playing}}
+   {      href="@url"}
+   {      href="@url?action=@name"}
+      title="@label{menu.@q{@name}verbose}">@label{menu.@name}</a>}
+            {   <span class=invalidmenu 
+         title="@label{menu.@q{@name}verbose}">@label{menu.@name}</span>}}
+
+
+@# Standard footer text
+@define {credits} {}
+        {<p class=credits>
+  <a href="http://www.greenend.org.uk/rjk/disorder/"
+     title="DisOrder web site">DisOrder
+     version @version</a>
+  &copy; 2003-2008 Richard Kettlewell et al
+</p>}
+
+@# Expand to the time that @id will be played
+@#  @what is the section
+@#  @id is the track ID
+@define {mwhen} {what id}
+        {@when{@id}</td>}
+
+@# Expand to the 'who' field for @id
+@#  @what is the section
+@#  @id is the track ID
+@define {mwho} {what id}
+        {@if{@eq{@who{@id}}{}}
+            {@if{@eq{@state{@id}}{random}}
+                {@label{@what.randomtrack}}
+                 {&nbsp;}}
+            {@who{@id}}}
+
+@# Expand to the artist for @track
+@#  @what is the section
+@#  @track is the track name
+@define {martist} {what track}
+        {@right{play}
+               {<a class=directory
+                   href="@url?action=choose&amp;dir=@urlquote{@dirname{@dirname{@track}}}"
+                   title="@label{playing.artistverbose}">@part{@track}{artist}{short}</a>}
+               {<span class=directory
+                      title="@part{@track}{artist}">@part{@track}{artist}{short}</span>}}
+
+@# Expand to the album for @track
+@#  @what is the section
+@#  @track is the track name
+@define {malbum} {what track}
+        {@right{play}
+               {<a class=directory
+                   href="@url?action=choose&amp;dir=@urlquote{@dirname{@track}}"
+                   title="@label{playing.albumverbose}">@part{@track}{album}{short}</a>}
+               {<span class=directory
+                      title="@part{@track}{album}">@part{@track}{album}{short}</span>}}
+
+@# Expand to the title for @track
+@#  @what is the section
+@#  @track is the track name
+@define {mtitle} {what track}
+        {<span title="@part{@track}{title}">@part{@track}{title}{short}</span>}
+
+@# As @mtitle but make a link to play the track
+@#  @what is the section
+@#  @track is the track name
+@define {mtitleplay} {what track}
+        {<a title="@part{@track}{title}" href="@url?action=play&#38;track=@urlquote{@track}&#38;back=@urlquote{@thisurl}">@part{@track}{title}{short}</a>}
+
+@# Expand to the remove/scratch entry for @id
+@#  @what is the section
+@#  @id is the track ID
+@define {mremove} {what id}
+        {@if{@removable{@id}}
+            {<a class=imgbutton
+                href="@url?action=remove&#38;id=@id@back">
+               <img class=button src="@image{remove}"
+                    width=@width height=@height
+                    title="@label{@what.removeverbose}"
+                    alt="@label{@what.scratch}">
+             </a>}
+            {<img class=button src="@image{noremove}"
+                  width=@width height=@height
+                  title="@label{@what.removeverbose}"
+                  alt="@label{@what.scratch}">}}
+
+@# Expand to a move button
+@#  @id is the track ID
+@#  @dir should be a direction: up, upall, down or downall
+@#  @delta should be the distance, +ve for up and -ve for down
+@# This macro calls @movable to check that the move is possible.
+@define {mmove} {id dir delta}
+        {@if{@movable{@id}{@delta}}
+            {<a class=imgbutton
+                href="@url?action=move&#38;id=@id&#38;delta=@delta@back">
+               <img class=button src="@image{@dir}"
+                    width=@width height=@height
+                    title="@label{playing.@q{@dir}verbose}"
+                    alt="@label{playing.@dir}">
+             </a>}
+            {<img class=button src="@image{no@dir}"
+                  width=@width height=@height
+                  title="@label{playing.@q{@dir}verbose}"
+                  alt="@label{playing.@dir}">}}
+
+@# Size of input box for preferences forms
+@define{prefsize}{}{40}
+
+@# Expand to the weight of a track.  This macro knows the default weight,
+@# and does two lookups, which is rather inelegant.
+@#  @track is the track name.
+@define{weight}{track}{@if{@eq{@pref{@track}{weight}}{}} 
+                          {90000}
+                          {@pref{@track}{weight}}}
+
+@# Expand to preference form section for a track
+@#  @index is the track number
+@#  @track is the track name
+@define {mprefs} {index track}
+        {
+   <p class="prefs_head">Preferences for <span class="prefs_track">@quote{@resolve{@track}}</span>:</p>
+   <input type=hidden name="@index@__track" value="@quote{@resolve{@track}}">
+   <table class=prefs>
+    <tr class=headings>
+     <th class="prefs_name">@label{prefs.name}</th>
+     <th class="prefs_value">@label{prefs.value}</th>
+    </tr>
+    <tr class=even>
+     <td class="prefs_name">@label{heading.title}</td>
+     <td class="prefs_value">
+      <input size=@prefsize type=text name="@index@__title"
+             value="@part{@track}{title}{display}">
+     </td>
+    </tr>
+    <tr class=odd>
+     <td class="prefs_name">@label{heading.album}</td>
+     <td class="prefs_value">
+      <input size=@prefsize type=text name="@index@__album"
+             value="@part{@track}{album}{display}">
+     </td>
+    </tr>
+    <tr class=even>
+     <td class="prefs_name">@label{heading.artist}</td>
+     <td class="prefs_value">
+      <input size=@prefsize type=text name="@index@__artist"
+             value="@part{@track}{artist}{display}">
+     </td>
+    </tr>
+    <tr class=odd>
+     <td class="prefs_name">@label{prefs.tags}</td>
+     <td class="prefs_value">
+      <input size=@prefsize type=text name="@index@__tags"
+             value="@pref{@track}{tags}">
+     </td>
+    </tr>
+    <tr class=even>
+     <td class="prefs_name">@label{prefs.weight}</td>
+     <td class="prefs_value">
+      <input size=@prefsize type=text name="@index@__weight"
+             value="@weight{@track}">
+     </td>
+    </tr>
+    <tr class=odd>
+     <td class="prefs_name">@label{prefs.random}</td>
+     <td class="prefs_value">
+      <input type=checkbox value=true
+             name="@index@__random" 
+             @if{@ne{@pref{@track}{pick_at_random}}{0}}
+                {checked}>
+      </td>
+    </tr>
+   </table>
+}
+
+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/new.html b/templates/new.html
deleted file mode 100644 (file)
index 73eea2d..0000000
+++ /dev/null
@@ -1,84 +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:new.title@</title>
- </head>
- <body>
-@include{topbar}@
-  <h1>@label:new.title@</h1>
-
-@#{only display the table if there is something to put in it}@
-@if{@isnew@}{
-  <table class=new>
-    <tr class=headings>
-     <th class=artist>@label:heading.artist@</th>
-     <th class=album>@label:heading.album@</th>
-     <th class=title>@label:heading.title@</th>
-     <th class=length>@label:heading.length@</th>
-@right{prefs}{
-     <th class=button>&nbsp;</th>}@
-    </tr>
-    @new{
-    <tr class=@parity@>
-     <td class=artist>@right{play}{<a class=directory
-       title="@part{artist}@"
-       href="@url@?action=choose&amp;directory=@urlquote{@dirname{@dirname{@part:path@}@}@}@"
-       >@part{short}{artist}@</a>}{<span class=directory
-       title="@part{artist}@"
-       >@part{short}{artist}@</span>}@</td>
-     <td class=album>@right{play}{<a class=directory
-       title="@part{album}@"
-       href="@url@?action=choose&amp;directory=@urlquote{@dirname{@part:path@}@}@"
-       >@part{short}{album}@</a>}{<span class=directory
-       title="@part{album}@"
-       >@part{short}{album}@</span>}@</td>
-     <td class=title>@right{play}{<a class=file
-       title="@part{title}@"
-       href="@url@?action=play&#38;file=@urlquote{@file@}@&#38;back=@urlquote{@thisurl@}@"
-       >@part{short}{title}@</a>}{<span class=file
-       title="@part{title}@"
-       >@part{short}{title}@</span>}@</td>
-     <td class=length>@length@</td>
-@right{prefs}{
-     <td class=imgbutton><a class=imgbutton
-      href="@url@?action=prefs&#38;0_file=@urlquote{@file@}@"><img
-       class=button src="@image:edit@"
-       title="@label:choose.prefsverbose@"
-       alt="@label:choose.prefs@"></a></td>
-    </tr>}@
-    }@
-  </table>
-}@
-
-@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/new.tmpl b/templates/new.tmpl
new file mode 100644 (file)
index 0000000..9de5ccd
--- /dev/null
@@ -0,0 +1,72 @@
+<!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{new}
+ </head>
+ <body>
+@stdmenu{new}
+  <h1>@label{new.title}</h1>
+
+@# Only display the table if there is something to put in it
+@if{@isnew}{
+  <table class=new>
+    <tr class=headings>
+     <th class=artist>@label{heading.artist}</th>
+     <th class=album>@label{heading.album}</th>
+     <th class=title>@label{heading.title}</th>
+     <th class=length>@label{heading.length}</th>
+@right{prefs}{
+     <th class=button>&nbsp;</th>}
+    </tr>
+    @new{
+    <tr class=@parity>
+     <td class=artist>@martist{new}{@track}</td>
+     <td class=album>@malbum{new}{@track}</td>
+     <td class=title>@mtitleplay{new}{@track}</td>
+     <td class=length>@length{@track}</td>
+     @right{prefs}{
+     <td class=imgbutton>
+      <a class=imgbutton
+         href="@url?action=prefs&#38;track=@urlquote{@track}">
+       <img class=button src="@image{edit}"
+            title="@label{choose.prefsverbose}"
+            alt="@label{choose.prefs}">
+      </a>
+     </td>
+    </tr>}
+    }
+  </table>
+}
+
+@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:
+}@#
index 55d661a538ed7ae5c5624cf26a404b67ae4cf12b..c4aac36ae2edfe6e1af35dfbf1ac96d8ff0b2386 100644 (file)
@@ -15,12 +15,12 @@ label       playing.randomtrack     &nbsp;
 label  queue.randomtrack       random
 
 # Short and long text for scratch (remove playing track) button
-label  playing.scratch         Scratch
-label  playing.scratchverbose  "stop playing this track"
+label  playing.remove          Scratch
+label  playing.removeverbose   "Stop playing this track"
 
 # Short and long text for remove queued track button
-label  playing.remove          Remove
-label  playing.removeverbose   "remove track from queue"
+label  queue.remove            Remove
+label  queue.removeverbose     "Remove track from queue"
 
 # Text for banner above currently playing track
 label  playing.now             "Now playing"
@@ -33,19 +33,19 @@ label       playing.up              Up
 label  playing.down            Down
 label  playing.upall           Head
 label  playing.downall         Tail
-label  playing.upverbose       "move track earlier in queue"
-label  playing.downverbose     "move track later in queue"
-label  playing.upallverbose    "move track to head of queue"
-label  playing.downallverbose  "move track to end of queue"
+label  playing.upverbose       "Move track earlier in queue"
+label  playing.downverbose     "Move track later in queue"
+label  playing.upallverbose    "Move track to head of queue"
+label  playing.downallverbose  "Move track to end of queue"
 
 # Short and long text for play control buttons
 label  playing.random          "Random play"
 label  playing.playing         "Playing"
 label  playing.pause           Pause
-label  playing.randomdisableverbose    "disable random play"
-label  playing.randomenableverbose     "enable random play"
-label  playing.playingdisableverbose   "disable playing"
-label  playing.playingenableverbose    "enable playing"
+label  playing.randomdisableverbose    "Disable random play"
+label  playing.randomenableverbose     "Enable random play"
+label  playing.playingdisableverbose   "Disable playing"
+label  playing.playingenableverbose    "Enable playing"
 label  playing.pauseverbose    "Pause the current track"
 label  playing.resumeverbose   "Resume play"
 
@@ -63,17 +63,17 @@ label       volume.left             ""
 label  volume.right            ""
 
 # Short and long text for volume down/up buttons
-label  volume.reduce           Down
-label  volume.increase         Up
-label  volume.reduceverbose    "reduce volume"
-label  volume.increaseverbose  "increase volume"
+label  volume.down             Down
+label  volume.up               Up
+label  volume.downverbose      "Reduce volume"
+label  volume.upverbose        "Increase volume"
 
 # Amount to increase/reduce volume by
 label  volume.resolution       4
 
-# Long text for linsk to album/artist
-label  playing.artistverbose   "more tracks by this artist"
-label  playing.albumverbose    "more tracks from this album"
+# Long text for links to album/artist
+label  playing.artistverbose   "More tracks by this artist"
+label  playing.albumverbose    "More tracks from this album"
 
 # <TITLE> for recently played page
 label  recent.title            "Recently Played"
@@ -110,8 +110,9 @@ label       choose.play             "Add track to queue"
 # <TITLE> for search page
 label  search.title            Search
 
-# Text for search button
+# Text for search buttons
 label  search.search           Search
+label   search.clear            Clear
 
 # <TITLE> for about page
 label  about.title             "About DisOrder"
@@ -184,6 +185,7 @@ label       error.connect           "Cannot connect to server."
 # Error messages for login.html
 label  error.loginfailed       "Incorrect username and/or password."
 label  error.cookiefailed      "Cannot create login cookie."
+label  error.revokefailed      "Cannot revoke login cookie."
 label  error.nousername        "No username specified."
 label  error.nopassword        "No password specified."
 label  error.passwordmismatch  "Passwords do not match."
@@ -194,39 +196,40 @@ label     error.noconfirm         "Missing confirmation string."
 label  error.badconfirm        "Invalid confirmation string."
 label  error.badedit           "Cannot edit user details."
 label  error.reminderfailed    "Cannot send a reminder."
+label   error.noright           "Access denied."
 
 # Text appended to all error pages
 label  error.generic           ""
 
-# Displayed text for links in the sidebar (or other menu)
-label  sidebar.playing         Playing
-label  sidebar.choose          Choose
-label  sidebar.random          Random
-label  sidebar.search          Search
-label  sidebar.recent          Recent
-label  sidebar.new             New
-label  sidebar.about           About
-label  sidebar.volume          Volume
-label  sidebar.login           Login
-label  sidebar.help            Help
-label  sidebar.manage          Manage
-
-# Long (i.e. TITLE=) text for sidebar links
-label  sidebar.playingverbose  "current and queued tracks"
-label  sidebar.chooseverbose   "choose tracks"
-label  sidebar.searchverbose   "word search among track names"
-label  sidebar.recentverbose   "recently played tracks"
-label  sidebar.newverbose      "newly added tracks"
-label  sidebar.aboutverbose    "about DisOrder"
-label  sidebar.volumeverbose   "volume control"
-label  sidebar.loginverbose    "log in to DisOrder"
-label  sidebar.helpverbose     "basic user guide"
-label  sidebar.manageverbose   "queue management and volume control"
+# Displayed text for links in the menu
+label  menu.playing            Playing
+label  menu.choose             Choose
+label  menu.random             Random
+label  menu.search             Search
+label  menu.recent             Recent
+label  menu.new                New
+label  menu.about              About
+label  menu.volume             Volume
+label  menu.login              Login
+label  menu.help               Help
+label  menu.manage             Manage
+
+# Long (i.e. TITLE=) text for menu links
+label  menu.playingverbose     "Current and queued tracks"
+label  menu.chooseverbose      "Choose tracks"
+label  menu.searchverbose      "Word search among track names"
+label  menu.recentverbose      "Recently played tracks"
+label  menu.newverbose "Newly added tracks"
+label  menu.aboutverbose       "About DisOrder"
+label  menu.volumeverbose      "Volume control"
+label  menu.loginverbose       "Log in to DisOrder"
+label  menu.helpverbose        "Basic user guide"
+label  menu.manageverbose      "Queue management and volume control"
 
 # This should be 'choose' or 'choosealpha'.  If 'choose' then all artists
 # appear on the same page, otherwise they are broken up by initial letter
 # (which can be more convenient if you have huge numbers).
-label  sidebar.choosewhich     choose
+label  menu.choosewhich        choose
 
 # Column headings for tables of tracks (playing, queue, recent)
 label  heading.when            When
@@ -236,13 +239,16 @@ label     heading.album           Album
 label  heading.title           Title
 label  heading.length          Length
 
-# Role images.  See the documentation for @images:NAME@.
+# Role images.  See the documentation for @image{NAME}.
 label   images.enabled          tick.png
 label   images.disabled         cross.png
-label   images.scratch          cross.png
-label   images.noscratch        nocross.png
+label   images.remove           cross.png
+label   images.noremove         nocross.png
 label   images.noup             noup.png
 label   images.upall            upup.png
 label   images.noupall          noupup.png
 label   images.downall          downdown.png
 label   images.nodownall        nodowndown.png
+
+# Where to find images etc
+label   url.static              /disorder
diff --git a/templates/playing.html b/templates/playing.html
deleted file mode 100644 (file)
index b45d975..0000000
+++ /dev/null
@@ -1,264 +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>@if{@isplaying@}{@playing{@part:title@}@}{@label:playing.title@}@</title>
- </head>
- <body>
-@include{topbar}@
-   <h1>@label:playing.title@</h1>
-
-   @#{extra control buttons for the management page}@
-   @if{@arg:mgmt@}{
-   <div class=mgmt>
-   <p class=mgmt>
-    @if{@paused@}{
-    <!-- paused -->
-    <a class=button
-    href="@url@?action=resume&#38;mgmt=true"
-     title="@label:playing.resumeverbose@">@label:playing.pause@</a>
-    </a>
-    <img width=16 height=16 class=imgbutton src="@image:enabled@">
-    }{
-    <!-- not paused -->
-    <a class=button
-    href="@url@?action=pause&#38;mgmt=true"
-     title="@label:playing.pauseverbose@">@label:playing.pause@</a>
-    </a>
-    <img width=16 height=16 class=imgbutton src="@image:disabled@">
-    }@
-    @if{@random-enabled@}{
-    <!-- random played enabled -->
-    <a class=button
-    href="@url@?action=random-disable&#38;mgmt=true"
-     title="@label:playing.randomdisableverbose@">@label:playing.random@</a>
-    </a>
-    <img width=16 height=16 class=imgbutton src="@image:enabled@">
-    }{
-    <!-- random played disabled -->
-    <a class=button
-    href="@url@?action=random-enable&#38;mgmt=true"
-     title="@label:playing.randomenableverbose@">@label:playing.random@</a>
-    </a>
-    <img width=16 height=16 class=imgbutton src="@image:disabled@">
-    }@
-    @if{@enabled@}{
-    <!-- playing enabled -->
-    <a class=button
-    href="@url@?action=disable&#38;mgmt=true"
-     title="@label:playing.disableverbose@">@label:playing.playing@</a>
-    </a>
-    <img width=16 height=16 class=imgbutton src="@image:enabled@">
-    }{
-    <!-- playing disabled -->
-    <a class=button
-    href="@url@?action=enable&#38;mgmt=true"
-     title="@label:playing.enableverbose@">@label:playing.playing@</a>
-    </a>
-    <img width=16 height=16 class=imgbutton src="@image:disabled@">
-    }@
-    <form class=volume action="@url@" method=POST
-     enctype="multipart/form-data" accept-charset=utf-8>
-    <span class=volume>
-     @label:playing.volume@
-     @right{volume}{<a class=imgbutton
-      href="@url@?action=volume&#38;delta=-@label:volume.resolution@&#38;back=@urlquote{@thisurl@?mgmt=true}@">
-      <img class=button src="@image:down@"
-       alt="@label:volume.reduce@" title="@label:volume.reduceverbose@">
-     </a>}{<img class=button src="@image:nodown@">}@
-     @label:volume.left@ <input size=3 name=left type=text value="@volume:left@">
-     @label:volume.right@ <input size=3 name=right type=text value="@volume:right@">
-     <input name=back type=hidden value="@thisurl@?mgmt=true">
-     @right{volume}{<button class=search name=submit type=submit>
-      @label:volume.set@
-     </button><input name=action type=hidden value=volume>}@
-     @right{volume}{<a class=imgbutton
-      href="@url@?action=volume&#38;delta=@label:volume.resolution@&#38;back=@urlquote{@thisurl@?mgmt=true}@">
-      <img class=button src="@image:up@"
-       alt="@label:volume.increase@" title="@label:volume.increaseverbose@">
-     </a>}{<img class=button src="@image:noup@">}@
-    </form>
-    </span>
-    </p>
-    </div>
-   }@
-
-@#{only display the table if there is something to put in it}@
-@if{@or{@isplaying@}{@isqueue@}@}{
-   <table class=playing>
-     <tr class=headings>
-      <th class=when>@label:heading.when@</th>
-      <th class=who>@label:heading.who@</th>
-      <th class=artist>@label:heading.artist@</th>
-      <th class=album>@label:heading.album@</th>
-      <th class=title>@label:heading.title@</th>
-      <th class=length>@label:heading.length@</th>
-      <th class=button>&nbsp;</th>
-      @if{@arg:mgmt@}{
-      <th class=imgbutton>&nbsp;</th>
-      <th class=imgbutton>&nbsp;</th>
-      <th class=imgbutton>&nbsp;</th>
-      <th class=imgbutton>&nbsp;</th>
-      }@
-     </tr>
-     @if{@isplaying@}{
-     <tr class=nowplaying>
-      <td colspan=@if{@arg:mgmt@}{11}{7}@>@label:playing.now@</td>
-     </tr>
-     @playing{
-     <tr class=playing>
-      <td class=when>@when@</td>
-      <td class=who>@if{@eq{@who@}{}@}{@if{@eq{@state@}{random}@}{@label:playing.randomtrack@}{&nbsp;}@}{@who@}@</td>
-      <td class=artist>@right{play}{<a class=directory
-       href="@url@?action=choose&amp;directory=@urlquote{@dirname{@dirname{@part:path@}@}@}@"
-       title="@label:playing.artistverbose@"
-       >@part{short}{artist}@</a>}{<span class=directory
-       title="@part{artist}@"
-       >@part{short}{artist}@</span>}@</td>
-      <td class=album>@right{play}{<a class=directory
-       href="@url@?action=choose&amp;directory=@urlquote{@dirname{@part:path@}@}@"
-       title="@label:playing.albumverbose@"
-       >@part{short}{album}@</a>}{<span class=directory
-       title="@part{album}@"
-       >@part{short}{album}@</span>}@</td>
-      <td class=title><span
-       title="@part{title}@">@part{short}{title}@</span></td>
-      <td class=length>@length@</td>
-      <td class=imgbutton>@if{@scratchable@}{<a class=imgbutton
-       href="@url@?action=scratch&#38;id=@id@&#38;mgmt=@arg:mgmt@"><img
-       class=button src="@image:scratch@"
-       title="@label:playing.scratchverbose@"
-       alt="@label:playing.scratch@"></a>}{<img
-       class=button src="@image:noscratch@"
-       title="@label:playing.scratchverbose@"
-       alt="@label:playing.scratch@">}@</td>
-      @if{@arg:mgmt@}{
-      <td class=imgbutton>&nbsp;</td>
-      <td class=imgbutton>&nbsp;</td>
-      <td class=imgbutton>&nbsp;</td>
-      <td class=imgbutton>&nbsp;</td>
-      }@
-     </tr>
-     }@}@
-     @if{@isqueue@}{
-     <tr class=next>
-      <td colspan=@if{@arg:mgmt@}{11}{7}@>@label:playing.next@</td>
-     </tr>
-     @queue{
-     <tr class=@parity@>
-      <td class=when>@when@</td>
-      <td class=who>@if{@eq{@who@}{}@}{@if{@eq{@state@}{random}@}{@label:queue.randomtrack@}{&nbsp;}@}{@who@}@</td>
-      <td class=artist>@right{play}{<a class=directory
-       title="@part{artist}@"
-       href="@url@?action=choose&amp;directory=@urlquote{@dirname{@dirname{@part:path@}@}@}@"
-       >@part{short}{artist}@</a>}{<span class=directory
-       title="@part{artist}@"
-       >@part{short}{artist}@</span>}@</td>
-      <td class=album>@right{play}{<a class=directory
-       title="@part{album}@"
-       href="@url@?action=choose&amp;directory=@urlquote{@dirname{@part:path@}@}@"
-       >@part{short}{album}@</a>}{<span class=directory
-       title="@part{album}@"
-       >@part{short}{album}@}@</td>
-      <td class=title><span
-       title="@part{title}@">@part{short}{title}@</span></td>
-      <td class=length>@length@</td>
-      <td class=imgbutton>@if{@removable@}{<a class=imgbutton
-       href="@url@?action=remove&#38;id=@id@&#38;mgmt=@arg:mgmt@"><img
-       class=button src="@image:scratch@"
-       title="@label:playing.removeverbose@" 
-       alt="@label:playing.remove@"></a>}{<img
-       class=button src="@image:noscratch@"
-       title="@label:playing.removeverbose@"
-       alt="@label:playing.remove@">}@</td>
-
-      @if{@arg:mgmt@}{
-      @if{@or{@isfirst@}
-             {@not{@movable@}@}@}{
-     <!-- cannot move up -->
-     <td class=imgbutton>
-      <img
-       class=button src="@image:noupall@"
-       title="@label:playing.upallverbose@" alt="">
-     <td class=imgbutton>
-      <img
-       class=button src="@image:noup@"
-       title="@label:playing.upverbose@" alt="">
-         }{
-     <!-- can move up -->
-     <td class=imgbutton>
-      <a class=imgbutton
-        href="@url@?action=move&#38;id=@id@&#38;delta=2147483647&#38;mgmt=true"><img
-       class=button src="@image:upall@"
-       title="@label:playing.upallverbose@"
-       alt="@label:playing.upall@"></a>
-     <td class=imgbutton>
-     <a class=imgbutton
-        href="@url@?action=move&#38;id=@id@&#38;delta=1&#38;mgmt=true"><img
-       class=button src="@image:up@"
-       title="@label:playing.upverbose@" alt="@label:playing.up@"></a>
-         }@
-
-      @if{@or{@islast@}
-             {@not{@movable@}@}@}{
-     <!-- cannot move down -->
-     <td class=imgbutton>
-      <img
-       class=button src="@image:nodownall@"
-       title="@label:playing.downallverbose@" alt="">
-     <td class=imgbutton>
-      <img
-       class=button src="@image:nodown@"
-       title="@label:playing.downverbose@" alt="">
-         }{
-     <!-- can move down -->
-     <td class=imgbutton>
-      <a class=imgbutton
-        href="@url@?action=move&#38;id=@id@&#38;delta=-2147483647&#38;mgmt=true"><img
-       class=button src="@image:downall@"
-       title="@label:playing.downallverbose@"
-       alt="@label:playing.downall@"></a>
-     <td class=imgbutton>
-     <a class=imgbutton
-        href="@url@?action=move&#38;id=@id@&#38;delta=-1&#38;mgmt=true"><img
-       class=button src="@image:down@"
-       title="@label:playing.downverbose@" alt="@label:playing.down@"></a>
-         }@
-
-      }@
-     </tr>
-     }@}@
-   </table>
-}@
-
-@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/playing.tmpl b/templates/playing.tmpl
new file mode 100644 (file)
index 0000000..bc0200e
--- /dev/null
@@ -0,0 +1,185 @@
+<!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
+-->
+@discard{
+
+  @# On/off button for pause etc
+  @#  @class should be the class: pause, random, playing
+  @#  @action should be the action to switch to the opposite state
+  @#  @state should be the current state
+  @define {onoff} {class action state}
+          {<a class=button
+              href="@url?action=@action@back"
+              title="@label{playing.@q{action}verbose}">
+             @label{playing.@class}
+           </a>
+           <img width=@width height=@height class=imgbutton
+                src="@image{@state}">}
+
+  @# Volume up/down buttons
+  @#  @dir is the direction: up or down
+  @#  @sign is the sign: + or -
+  @define {vbutton}{dir sign}
+          {@right{volume}
+                 {<a class=imgbutton
+                    href="@url?action=volume&#38;delta=@sign@label{volume.resolution}@back">
+                   <img class=button src="@image{@dir}"
+                         width=@width height=@height
+                        alt="@label{volume.@dir}"
+                        title="@label{volume.@q{@dir}verbose}">
+                 </a>}
+                 {<img class=button width=@width height=@height
+                       src="@image{no@dir}">}}
+
+  @# Expand to @yes for the Manage page and @no for the playing page
+  @define {ifmanage} {yes no}
+           {@if {@eq {@arg{action}}{manage}}
+                {@yes}
+                {@no}}
+
+  @# Expand to &back=manage or to nothing
+  @define {back} {}
+         {@ifmanage{&amp;back=manage}{}}
+}@#
+<html>
+ <head>
+@quiethead
+  <title>@if{@isplaying}
+            {@playing{@part{@id}{title}}}
+            {@label{playing.title}}</title>
+ </head>
+ <body>
+@stdmenu{@ifmanage{manage}{playing}}
+   <h1>@label{playing.title}</h1>
+
+@# Extra control buttons for the management page
+   @ifmanage{
+   <div class=mgmt>
+     <p class=mgmt>
+       @if{@paused}
+         {@onoff{pause}{resume}{enabled}}
+         {@onoff{pause}{pause}{disabled}}
+       @if{@random-enabled}
+         {@onoff{random}{randomdisable}{enabled}}
+         {@onoff{random}{randomenable}{disabled}}
+       @if{@enabled}
+         {@onoff{playing}{disable}{enabled}}
+         {@onoff{playing}{enable}{disabled}}
+       <form class=volume method=POST
+            action="@url"
+            enctype="multipart/form-data" accept-charset=utf-8>
+        <span class=volume>
+          @label{playing.volume}
+          @vbutton{up}{+}
+          @label{volume.left}
+          <input size=3 name=left type=text value="@volume{left}">
+          @label{volume.right}
+          <input size=3 name=right type=text value="@volume{right}">
+          <input name=back type=hidden value="@thisurl?back=manage">
+          @right{volume}{
+            <button class=search name=submit type=submit>
+               @label{volume.set}
+            </button>
+            <input name=action type=hidden value=volume>
+          }
+          @vbutton{down}{-}
+        </span>
+       </form>
+      </p>
+    </div>
+   }{}
+
+@# Only display the table if there is something to put in it
+@if{@or{@isplaying}{@isqueue}}{
+   <table class=playing>
+     <tr class=headings>
+      <th class=when>@label{heading.when}</th>
+      <th class=who>@label{heading.who}</th>
+      <th class=artist>@label{heading.artist}</th>
+      <th class=album>@label{heading.album}</th>
+      <th class=title>@label{heading.title}</th>
+      <th class=length>@label{heading.length}</th>
+      <th class=button>&nbsp;</th>
+      @ifmanage{
+        <th class=imgbutton>&nbsp;</th>
+        <th class=imgbutton>&nbsp;</th>
+        <th class=imgbutton>&nbsp;</th>
+        <th class=imgbutton>&nbsp;</th>
+      }{}
+     </tr>
+     @if{@isplaying}{
+       <tr class=nowplaying>
+        <td colspan=@ifmanage{11}{7}>@label{playing.now}</td>
+       </tr>
+     }
+     @playing{
+       <tr class=playing>
+        <td class=when>@mwhen{playing}{@id}</td>
+        <td class=who>@mwho{playing}{@id}</td>
+        <td class=artist>@martist{playing}{@track}</td>
+        <td class=album>@malbum{playing}{@track}</td>
+        <td class=title>@mtitle{playing}{@track}</td>
+        <td class=length>@length{@id}</td>
+        <td class=imgbutton>@mremove{playing}{@id}</td>
+        @ifmanage{
+          <td class=imgbutton>&nbsp;</td>
+          <td class=imgbutton>&nbsp;</td>
+          <td class=imgbutton>&nbsp;</td>
+          <td class=imgbutton>&nbsp;</td>
+        }{}
+       </tr>
+     }
+     @if{@isqueue}{
+      <tr class=next>
+       <td colspan=@ifmanage{11}{7}>@label{playing.next}</td>
+      </tr>
+     }
+     @queue{
+      <tr class=@parity>
+       <td class=when>@mwhen{queue}{@id}</td>
+       <td class=who>@mwho{queue}{@id}</td>
+       <td class=artist>@martist{queue}{@track}</td>
+       <td class=album>@malbum{queue}{@track}</td>
+       <td class=title>@mtitle{queue}{@track}</td>
+       <td class=length>@length{@id}</td>
+       <td class=imgbutton>@mremove{queue}{@id}</td>
+       @ifmanage{
+         <td class=imgbutton>@mmove{@id}{upall}{2147483647}</td>
+         <td class=imgbutton>@mmove{@id}{up}{1}</td>
+         <td class=imgbutton>@mmove{@id}{down}{-1}</td>
+         <td class=imgbutton>@mmove{@id}{downall}{-2147483647}</td>
+       }{}
+      </tr>
+    }
+   </table>
+}
+
+@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
+End:
+}@#
diff --git a/templates/prefs.html b/templates/prefs.html
deleted file mode 100644 (file)
index 5e88890..0000000
+++ /dev/null
@@ -1,89 +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:prefs.title@</title>
- </head>
- <body>
-@include{topbar}@
-   <h1>@label:prefs.title@</h1>
-
-   <form class=prefs action="@url@" method=POST
-         enctype="multipart/form-data" accept-charset=utf-8>
-    <input type=hidden name="files" value="@nfiles@">
-    <input type=hidden name=parts value="artist album title">
-   @files{
-   <p class="prefs_head">Preferences for <span class="prefs_track">@arg{@index@_file}@</span></p>
-    <input type=hidden name="@index@_file" value="@arg{@index@_file}@">
-    <table class=prefs>
-      <tr class="headings">
-       <th class="prefs_name">@label:prefs.name@</th>
-       <th class="prefs_value">@label:prefs.value@</th>
-      </tr>
-      <tr class=even>
-       <td class="prefs_name">@label:heading.title@</td>
-       <td class="prefs_value"><input size=40 type=text name="@index@_title" value="@part{display}{title}{@arg{@index@_file}@}@"></td>
-      </tr>
-      <tr class=odd>
-       <td class="prefs_name">@label:heading.album@</td>
-       <td class="prefs_value"><input size=40 type=text name="@index@_album" value="@part{display}{album}{@arg{@index@_file}@}@"></td>
-      </tr>
-      <tr class=even>
-       <td class="prefs_name">@label:heading.artist@</td>
-       <td class="prefs_value"><input size=40 type=text name="@index@_artist" value="@part{display}{artist}{@arg{@index@_file}@}@"></td>
-      </tr>
-      <tr class=odd>
-       <td class="prefs_name">@label:prefs.tags@</td>
-       <td class="prefs_value"><input size=40 type=text name="@index@_tags" value="@pref{@arg{@index@_file}@}{tags}@"></td>
-      </tr>
-      <tr class=even>
-       <td class="prefs_name">@label:prefs.weight@</td>
-       <td class="prefs_value"><input size=40 type=text name="@index@_weight" value="@pref{@arg{@index@_file}@}{weight}@"></td>
-      </tr>
-      <tr class=odd>
-       <td class="prefs_name">@label:prefs.random@</td>
-       <td class="prefs_value"><input type=checkbox
-        name="@index@_random" value=true
-       @if{@ne{@pref{@arg{@index@_file}@}{pick_at_random}@}{0}@}{ checked}{}@></td>
-    </table>
-   }@
-    
-    <p>
-     <button type=submit name=submit>
-      @label:prefs.set@
-     </button>
-     <input name=action type=hidden value=prefs>
-    </p>
-   </form>
-
-@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/prefs.tmpl b/templates/prefs.tmpl
new file mode 100644 (file)
index 0000000..74240da
--- /dev/null
@@ -0,0 +1,60 @@
+<!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{prefs}
+ </head>
+ <body>
+@stdmenu{prefs}
+  <h1>@label{prefs.title}</h1>
+
+  <form class=prefs method=POST
+        action="@url"
+        enctype="multipart/form-data"
+        accept-charset="utf-8">
+   <input type=hidden name=parts value="artist album title">
+   <input type=hidden name=context value="display">
+   <input name=action type=hidden value=set>
+   <input name=back type=hidden value="@quote{@thisurl}">
+   @if{@eq{@arg{dir}}{}}
+      {@mprefs{0}{@arg{track}}}
+      {@tracks{@arg{dir}}
+              {@mprefs{@index}{@track}}}
+   <p>
+    <button type=submit name=submit>
+     @label{prefs.set}
+    </button>
+   </p>
+  </form>
+
+@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/recent.html b/templates/recent.html
deleted file mode 100644 (file)
index 787b514..0000000
+++ /dev/null
@@ -1,84 +0,0 @@
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
-<!--
-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
--->
-<html>
- <head>
-@include:stdhead@
-  <title>@label:recent.title@</title>
- </head>
- <body>
-@include{topbar}@
-  <h1>@label:recent.title@</h1>
-
-@#{only display the table if there is something to put in it}@
-@if{@isrecent@}{
-  <table class=recent>
-    <tr class=headings>
-     <th class=when>@label:heading.when@</th>
-     <th class=who>@label:heading.who@</th>
-     <th class=artist>@label:heading.artist@</th>
-     <th class=album>@label:heading.album@</th>
-     <th class=title>@label:heading.title@</th>
-     <th class=length>@label:heading.length@</th>
-@right{prefs}{
-     <th class=button>&nbsp;</th>}@
-    </tr>
-    @recent{
-    <tr class=@parity@>
-     <td class=when>@when@</td>
-     <td class=who>@who@</td>
-     <td class=artist>@right{play}{<a class=directory
-       title="@part{artist}@"
-       href="@url@?action=choose&amp;directory=@urlquote{@dirname{@dirname{@part:path@}@}@}@"
-       >@part{short}{artist}@</a>}{<span class=directory
-       title="@part{artist}@"
-       >@part{short}{artist}@</span>}@</td>
-     <td class=album>@right{play}{<a class=directory
-       title="@part{album}@"
-       href="@url@?action=choose&amp;directory=@urlquote{@dirname{@part:path@}@}@"
-       >@part{short}{album}@</a>}{<span class=directory
-       title="@part{album}@"
-       >@part{short}{album}@</span>}@</td>
-     <td class=title><span
-       title="@part{title}@">@part{short}{title}@</span></td>
-     <td class=length>@length@</td>
-@right{prefs}{
-     <td class=imgbutton><a class=imgbutton
-      href="@url@?action=prefs&#38;0_file=@urlquote{@file@}@"><img
-       class=button src="@image:edit@"
-       title="@label:choose.prefsverbose@"
-       alt="@label:choose.prefs@"></a></td>
-    </tr>}@
-    }@
-  </table>
-}@
-
-@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/recent.tmpl b/templates/recent.tmpl
new file mode 100644 (file)
index 0000000..b43d01a
--- /dev/null
@@ -0,0 +1,75 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
+<!--
+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
+-->
+<html>
+ <head>
+@stdhead{recent}
+ </head>
+ <body>
+@stdmenu{recent}
+  <h1>@label{recent.title}</h1>
+
+@# Only display the table if there is something to put in it
+@if{@isrecent}{
+  <table class=recent>
+    <tr class=headings>
+     <th class=when>@label{heading.when}</th>
+     <th class=who>@label{heading.who}</th>
+     <th class=artist>@label{heading.artist}</th>
+     <th class=album>@label{heading.album}</th>
+     <th class=title>@label{heading.title}</th>
+     <th class=length>@label{heading.length}</th>
+     @right{prefs}{<th class=button>&nbsp;</th>}
+    </tr>
+    @recent{
+    <tr class=@parity>
+     <td class=when>@mwhen{recent}{@id}</td>
+     <td class=who>@mwho{recent}{@id}</td>
+     <td class=artist>@martist{recent}{@track}</td>
+     <td class=album>@malbum{recent}{@track}</td>
+     <td class=title>@mtitle{recent}{@track}</td>
+     <td class=length>@length{@id}</td>
+     @right{prefs}{
+     <td class=imgbutton>
+      <a class=imgbutton
+         href="@url?action=prefs&#38;track=@urlquote{@track}">
+       <img class=button src="@image{edit}"
+            title="@label{choose.prefsverbose}"
+            alt="@label{choose.prefs}">
+      </a>
+     </td>
+    </tr>}
+    }
+  </table>
+}
+
+@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/search.html b/templates/search.html
deleted file mode 100644 (file)
index 1b0fa1c..0000000
+++ /dev/null
@@ -1,77 +0,0 @@
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
-<!--
-This file is part of DisOrder.
-Copyright (C) 2003, 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
--->
-<html>
- <head>
-@include:stdhead@
-  <title>@label:search.title@</title>
- </head>
- <body>
-@include{topbar}@
-   <h1>@label:search.title@</h1>
-
-   <form class=search action="@url@" method=POST
-         enctype="multipart/form-data" accept-charset=utf-8>
-     <p class=search>Enter search terms:
-     <input class=query name=query type=text value="@arg:query@"
-      size=50>
-     <button class=search name=submit type=submit>
-      @label:search.search@
-     </button>
-     <input name=action type=hidden value=search>
-     </p>
-   </form>
-
-   <div class=searchresults>
-    @search{artist}{display}{
-    <div class="search_artist">
-     <p>Artist:
-      <span class="search_artist">@part:artist@</span></p>
-     @search{album}{display}{
-     <div class="search_album">
-      <p>Album:
-       <span class="search_album">@part:album@</span></p>
-      @search{title}{
-      <div class="search_title">
-       <p>Title:
-       <a href="@url@?action=play&#38;file=@urlquote{@file@}@&#38;back=@urlquote{@thisurl@}@">@part:title@</a>
-       @if{@eq{@trackstate{@file@}@}{playing}@}{[<b>playing</b>]}@
-       @if{@eq{@trackstate{@file@}@}{queued}@}{[<b>queued</b>]}@
-       </p>
-      </div>
-      }@
-     </div>
-     }@
-    </div>
-    }@
-   </div>
-
-@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/stdhead.html b/templates/stdhead.html
deleted file mode 100644 (file)
index e1a8174..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-@include:stylesheet@
-@@
-Anything that goes in all html HEAD elements goes here.
-<!--
-This file is part of DisOrder.
-Copyright (C) 2004 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
--->
diff --git a/templates/stylesheet.html b/templates/stylesheet.html
deleted file mode 100644 (file)
index da58e56..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-  <link rel=stylesheet type="text/css" href="@image:disorder.css@">
-@@
-This file is a standard place to put a link to a stylesheet,
-or an embedded stylesheet.
-<!--
-This file is part of DisOrder.
-Copyright (C) 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
--->
diff --git a/templates/topbar.html b/templates/topbar.html
deleted file mode 100644 (file)
index e350d91..0000000
+++ /dev/null
@@ -1,68 +0,0 @@
-<p class=menubar>
-  <a class=@if{@eq{@action@}{playing}@}{activemenu}{inactivemenu}@
- href="@url@"
- title="@label:sidebar.playingverbose@">@label:sidebar.playing@</a>
-  <a class=@if{@eq{@action@}{recent}@}{activemenu}{inactivemenu}@
- href="@url@?action=recent"
- title="@label:sidebar.recentverbose@">@label:sidebar.recent@</a>
-  <a class=@if{@eq{@action@}{new}@}{activemenu}{inactivemenu}@
- href="@url@?action=new"
- title="@label:sidebar.newverbose@">@label:sidebar.new@</a>
-@right{play}{
-  <a class=@if{@or{@eq{@action@}{choose}@}
-                  {@eq{@action@}{choosealpha}@}@}
-              {activemenu}
-              {inactivemenu}@
- href="@url@?action=@label:sidebar.choosewhich@"
- title="@label:sidebar.chooseverbose@">@label:sidebar.choose@</a>}{
-  <span class=invalidmenu
-   title="@label:sidebar.chooseverbose@">@label:sidebar.choose@</span>}@
-@right{play}{
-  <a class=@if{@eq{@action@}{search}@}{activemenu}{inactivemenu}@
- href="@url@?action=search"
- title="@label:sidebar.searchverbose@">@label:sidebar.search@</a>}{
-  <span class=invalidmenu
-   title="@label:sidebar.searchverbose@">@label:sidebar.search@</span>}@
-<!-- disabled by default since now available from 'manage'
-  <a class=@if{@eq{@action@}{volume}@}{activemenu}{inactivemenu}@
- href="@url@?action=volume"
- title="@label:sidebar.volumeverbose@">@label:sidebar.volume@</a>
--->
-  <a class=@if{@eq{@action@}{manage}@}{activemenu}{inactivemenu}@
- href="@url@?mgmt=true"
- title="@label:sidebar.manageverbose@">@label:sidebar.manage@</a>
-  <a class=@if{@or{@eq{@action@}{login}@}
-                  {@eq{@action@}{logout}@}
-                  {@eq{@action@}{register}@}
-                  {@eq{@action@}{reminder}@}
-                  {@eq{@action@}{edituser}@}@}{activemenu}{inactivemenu}@
- href="@url@?action=login"
- title="@label:sidebar.loginverbose@">@label:sidebar.login@</a>
-  <a class=@if{@eq{@action@}{help}@}{activemenu}{inactivemenu}@
- href="@url@?action=help"
- title="@label:sidebar.helpverbose@">@label:sidebar.help@</a>
-  <a class=@if{@eq{@action@}{about}@}{activemenu}{inactivemenu}@
- href="@url@?action=about"
- title="@label:sidebar.aboutverbose@">@label:sidebar.about@</a>
-</p>
-<hr>
-@@
-<!--
-This file is part of DisOrder.
-Copyright (C) 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
--->
diff --git a/templates/topbarend.html b/templates/topbarend.html
deleted file mode 100644 (file)
index 5eca359..0000000
+++ /dev/null
@@ -1,21 +0,0 @@
-@include:credits@
-@@
-<!--
-This file is part of DisOrder.
-Copyright (C) 2005 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
--->
diff --git a/templates/volume.html b/templates/volume.html
deleted file mode 100644 (file)
index dcc69b1..0000000
+++ /dev/null
@@ -1,61 +0,0 @@
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
-<!--
-This file is part of DisOrder.
-Copyright (C) 2004, 2005, 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:volume.title@</title>
- </head>
- <body>
-@include{topbar}@
-   <h1>@label:volume.title@</h1>
-
-   <form class=volume action="@url@" method=POST>
-    <p class=volume>
-     <a class=imgbutton
-      href="@url@?action=volume&#38;delta=-@label:volume.resolution@">
-      <img class=button src="@image:down@"
-       alt="@label:volume.reduce@" title="@label:volume.reduceverbose@">
-     </a>
-     @label:volume.left@ <input size=3 name=left type=text value="@volume:left@">
-     @label:volume.right@ <input size=3 name=right type=text value="@volume:right@">
-     <button class=search name=action type=submit value=volume>
-      @label:volume.set@
-     </button>
-     <a class=imgbutton
-      href="@url@?action=volume&#38;delta=@label:volume.resolution@">
-      <img class=button src="@image:up@"
-       alt="@label:volume.increase@" title="@label:volume.increaseverbose@">
-     </a>
-    </p>
-   </form>
-
-@include{topbarend}@
- </body>
-</html>
-@@
-<!--
-Local variables:
-mode:sgml
-sgml-always-quote-attributes:nil
-sgml-indent-step:1
-sgml-indent-data:t
-End:
--->