chiark / gitweb /
Start conversion of CGI actions.
authorRichard Kettlewell <rjk@greenend.org.uk>
Sat, 10 May 2008 08:30:04 +0000 (09:30 +0100)
committerRichard Kettlewell <rjk@greenend.org.uk>
Sat, 10 May 2008 08:30:04 +0000 (09:30 +0100)
server/actions.c [new file with mode: 0644]
server/cgi.c
server/cgimain.c
server/dcgi.c
server/macros-disorder.c
server/macros-disorder.h

diff --git a/server/actions.c b/server/actions.c
new file mode 100644 (file)
index 0000000..d6d227d
--- /dev/null
@@ -0,0 +1,134 @@
+/*
+ * 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"
+
+/** @brief Login cookie */
+char *login_cookie;
+
+/** @brief Table of actions */
+static const struct action {
+  /** @brief Action name */
+  const char *name;
+  /** @brief Action handler */
+  void (*handler)(void);
+} actions[] = {
+  { "confirm", act_confirm },
+  { "disable", act_disable },
+  { "edituser", act_edituser },
+  { "enable", act_enable },
+  { "login", act_login },
+  { "logout", act_logout },
+  { "manage", act_manage },
+  { "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 },
+};
+
+/** @brief Expand a template
+ * @param name Base name of template, or NULL to consult CGI args
+ */
+void disorder_cgi_expand(const char *name) {
+  const char *p;
+  
+  /* For unknown actions check that they aren't evil */
+  for(p = name; *p && isalnum((unsigned char)*p); ++p)
+    ;
+  if(*p)
+    fatal(0, "invalid action name '%s'", action);
+  byte_xasprintf((char **)&p, "%s.tmpl", action);
+  if(mx_expand_file(p, sink_stdio(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 disorder_cgi_action(const char *action) {
+  int n;
+  char *s;
+
+  /* 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";
+  }
+  if((n = TABLE_FIND(actions, struct action, name, action)) >= 0)
+    /* Its a known action */
+    actions[n].handler();
+  else
+    /* Just expand the template */
+    disorder_cgi_expand(action);
+}
+
+/** @brief Generate an error page */
+void disorder_cgi_error(const char *msg, ...) {
+  va_list ap;
+
+  va_start(ap, msg);
+  byte_xvasprintf(&error_string, msg, ap);
+  va_end(ap);
+  disorder_cgi_expand("error");
+}
+
+/** @brief Log in as the current user or guest if none */
+void disorder_cgi_login(dcgi_state *ds, struct sink *output) {
+  /* Junk old data */
+  disorder_macros_reset();
+  /* Reconnect */
+  if(disorder_connect_cookie(client, login_cookie)) {
+    disorder_cgi_error("Cannot connect to server");
+    exit(0);
+  }
+  /* If there was a cookie but it went bad, we forget it */
+  if(login_cookie && !strcmp(disorder_user(>client), "guest"))
+    login_cookie = 0;
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
index 92c39ece1325438dd5b38e6f7ccb8134033f0af6..5f9e17fcd1e278983212cccbf759b14533034623 100644 (file)
 #include "unicode.h"
 #include "hash.h"
 
-struct kvp *cgi_args;
-
-/* options */
-struct column {
-  struct column *next;
-  char *name;
-  int ncolumns;
-  char **columns;
-};
-
-/* macros */
-struct cgi_macro {
-  int nargs;
-  char **args;
-  const char *value;
-};
-
-#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_expand_parsed(const char *name,
-                             struct cgi_element *head,
-                             const struct cgi_expansion *expansions,
-                             size_t nexpansions,
-                             cgi_1sink *output,
-                             void *u);
-
 void cgi_header(struct sink *output, const char *name, const char *value) {
   sink_printf(output, "%s: %s\r\n", name, value);
 }
index 4a37c52028edf958ecd9f3ebc7e611bbc4d03b79..77764c0c7604088d0ea146fa7e46883b9745671a 100644 (file)
@@ -110,10 +110,7 @@ int main(int argc, char **argv) {
     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);
+  output = sink_stdio("stdout", stdout);
   /* See if there's a cookie */
   cookie_env = getenv("HTTP_COOKIE");
   if(cookie_env) {
@@ -142,6 +139,8 @@ int main(int argc, char **argv) {
    * directory second, so that the latter overrides the former. */
   mx_search_path(pkgconfdir);
   mx_search_path(pkgdatadir);
+  /* Never cache anythging */
+  cgi_header(output->sink, "Cache-Control", "no-cache");
   /* Create the initial connection, trying the cookie if we found a suitable
    * one. */
   disorder_cgi_login(&s, &output);
index 15faf2fba1ff792a4ca08f8bc107feb8846b22cd..65f0b331be1d47bce0f55738fa80794ec6ae582b 100644 (file)
@@ -1,25 +1,3 @@
-/*
- * 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 "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;
 
@@ -639,31 +589,6 @@ static void act_reminder(cgi_sink *output,
   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_label(int attribute((unused)) nargs,
@@ -1006,144 +931,6 @@ static void exp_image(int attribute((unused)) nargs,
     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 },
-  { "define", 3, 3, EXP_MAGIC, exp_define },
-  { "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
index 9b1f21f57ac97a3d1cd18cccf340aa8b5c7bb5f3..b4fdc29f4fb64353f1f71e83b4fa6fcad8cdff60 100644 (file)
@@ -37,6 +37,9 @@
  */
 disorder_client *client;
 
+/** @brief For error template */
+char *error_string;
+
 /** @brief Cached data */
 static unsigned flags;
 
@@ -142,7 +145,7 @@ static const char *make_index(int i) {
   return s;
 }
 
-/* @server-version@
+/* @server-version
  *
  * Expands to the server's version string, or a (safe to use) error
  * value if the server is unavailable or broken.
@@ -161,7 +164,7 @@ static int exp_server_version(int attribute((unused)) nargs,
   return sink_write(output, cgi_sgmlquote(v)) < 0 ? -1 : 0;
 }
 
-/* @version@
+/* @version
  *
  * Expands to the local version string.
  */
@@ -173,7 +176,7 @@ static int exp_version(int attribute((unused)) nargs,
                    cgi_sgmlquote(disorder_short_version_string)) < 0 ? -1 : 0;
 }
 
-/* @url@
+/* @url
  *
  * Expands to the base URL of the web interface.
  */
@@ -185,7 +188,7 @@ static int exp_url(int attribute((unused)) nargs,
                    cgi_sgmlquote(config->url)) < 0 ? -1 : 0;
 }
 
-/* @arg{NAME}@
+/* @arg{NAME}
  *
  * Expands to the CGI argument NAME, or the empty string if there is
  * no such argument.
@@ -202,7 +205,7 @@ static int exp_arg(int attribute((unused)) nargs,
     return 0;
 }
 
-/* @user@
+/* @user
  *
  * Expands to the logged-in username (which might be "guest"), or to
  * the empty string if not connected.
@@ -351,7 +354,7 @@ static int exp_length(int attribute((unused)) nargs,
   return sink_write(output, "&nbsp;") < 0 ? -1 : 0;
 }
 
-/* @removable{ID}@
+/* @removable{ID}
  *
  * Expands to "true" if track ID is removable (or scratchable, if it is the
  * playing track) and "false" otherwise.
@@ -371,7 +374,7 @@ static int exp_removable(int attribute((unused)) nargs,
                             (rights, disorder_user(client), q));
 }
 
-/* @movable{ID}@
+/* @movable{ID}
  *
  * Expands to "true" if track ID is movable and "false" otherwise.
  */
@@ -512,7 +515,7 @@ static int exp_new(int attribute((unused)) nargs,
   return 0;
 }
 
-/* @volume{CHANNEL}@
+/* @volume{CHANNEL}
  *
  * Expands to the volume in a given channel.  CHANNEL must be "left" or
  * "right".
@@ -527,7 +530,7 @@ static int exp_volume(int attribute((unused)) nargs,
                             ? volume_left : volume_right) < 0 ? -1 : 0;
 }
 
-/* @isplaying@
+/* @isplaying
  *
  * Expands to "true" if there is a playing track, otherwise "false".
  */
@@ -539,7 +542,7 @@ static int exp_isplaying(int attribute((unused)) nargs,
   return mx_bool_result(output, !!playing);
 }
 
-/* @isqueue@
+/* @isqueue
  *
  * Expands to "true" if there the queue is nonempty, otherwise "false".
  */
@@ -564,7 +567,7 @@ static int exp_isrecent(int attribute((unused)) nargs,
   return mx_bool_result(output, !!recent);
 }
 
-/* @isnew@
+/* @isnew
  *
  * Expands to "true" if there the newly added track list is nonempty, otherwise
  * "false".
@@ -577,7 +580,7 @@ static int exp_isnew(int attribute((unused)) nargs,
   return mx_bool_result(output, !!nnew);
 }
 
-/* @pref{TRACK}{KEY}@
+/* @pref{TRACK}{KEY}
  *
  * Expands to a track preference.
  */
@@ -591,7 +594,7 @@ static int exp_pref(int attribute((unused)) nargs,
     return sink_write(output, cgi_sgmlquote(value)) < 0 ? -1 : 0;
 }
 
-/* @prefs{TRACK}{TEMPLATE}@
+/* @prefs{TRACK}{TEMPLATE}
  *
  * For each track preference of track TRACK, expands TEMPLATE with the
  * following expansions:
@@ -629,7 +632,7 @@ static int exp_prefs(int attribute((unused)) nargs,
   return 0;
 }
 
-/* @transform{TRACK}{TYPE}{CONTEXT}@
+/* @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
@@ -659,7 +662,7 @@ static int exp_enabled(int attribute((unused)) nargs,
   return mx_bool_result(output, enabled);
 }
 
-/* @random-enabled@
+/* @random-enabled
  *
  * Expands to "true" if random play is enabled, otherwise "false".
  */
@@ -674,7 +677,7 @@ static int exp_enabled(int attribute((unused)) nargs,
   return mx_bool_result(output, enabled);
 }
 
-/* @trackstate{TRACK}@
+/* @trackstate{TRACK
  *
  * Expands to "playing" if TRACK is currently playing, or "queue" if it is in
  * the queue, otherwise to nothing.
@@ -700,7 +703,7 @@ static int exp_trackstate(int attribute((unused)) nargs,
   return 0;
 }
 
-/* @thisurl@
+/* @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
@@ -713,7 +716,7 @@ static int exp_thisurl(int attribute((unused)) nargs,
   return cgi_thisurl(config->url);
 }
 
-/* @resolve{TRACK}@
+/* @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.
@@ -729,7 +732,7 @@ static int exp_resolve(int attribute((unused)) nargs,
   return 0;
 }
 
-/* @paused@
+/* @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).
@@ -793,7 +796,7 @@ static int exp_right(int nargs,
   return 0;
 }
 
-/* @userinfo{PROPERTY}@
+/* @userinfo{PROPERTY}
  *
  * Expands to the named property of the current user.
  */
@@ -808,10 +811,24 @@ static int exp_userinfo(int attribute((unused)) nargs,
   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_write(output, cgi_sgmlquote(error_string)) < 0 ? -1 : 0;
+}
+
+/* @userinfo{PROPERTY}@
+ *
 /** @brief Register DisOrder-specific expansions */
 void register_disorder_expansions(void) {
   mx_register(exp_arg, 1, 1, "arg");
   mx_register(exp_enabled, 0, 0, "enabled");
+  mx_register(exp_error, 0, 0, "error");
   mx_register(exp_isnew, 0, 0, "isnew");
   mx_register(exp_isplaying, 0, 0, "isplaying");
   mx_register(exp_isqueue, 0, 0, "isplaying");
@@ -844,6 +861,16 @@ void register_disorder_expansions(void) {
   mx_register_magic(exp_recent, 1, 1, "recent");
 }
 
+void disorder_macros_reset(void) {
+  /* Junk the old connection if there is one */
+  if(client)
+    disorder_close(client);
+  /* Create a new connection */
+  client = disorder_new(0);
+  /* Forget everything we knew */
+  flags = 0;
+}
+
 /*
 Local Variables:
 c-basic-offset:2
index bb8950f6736c4babdec36454811eca4cf4bc8b2b..0556e0df2b04fb16fc7caaeda75f8b5ad00288d0 100644 (file)
@@ -1,3 +1,4 @@
+
 /*
  * This file is part of DisOrder.
  * Copyright (C) 2008 Richard Kettlewell
@@ -25,6 +26,7 @@
 #define MACROS_DISORDER_H
 
 extern disorder_client *client;
+extern char *error_string;
 void register_disorder_expansions(void);
 
 #endif /* MACROS_DISORDER_H */