Replacing hard references to the URL of the config interface
[privoxy.git] / cgiedit.c
1 const char cgiedit_rcs[] = "$Id: cgiedit.c,v 1.8 2001/11/30 23:35:51 jongfoster Exp $";
2 /*********************************************************************
3  *
4  * File        :  $Source: /cvsroot/ijbswa/current/cgiedit.c,v $
5  *
6  * Purpose     :  CGI-based actionsfile editor.
7  *
8  *                Functions declared include:
9  *
10  *
11  * Copyright   :  Written by and Copyright (C) 2001 the SourceForge
12  *                IJBSWA team.  http://ijbswa.sourceforge.net
13  *
14  *                Based on the Internet Junkbuster originally written
15  *                by and Copyright (C) 1997 Anonymous Coders and
16  *                Junkbusters Corporation.  http://www.junkbusters.com
17  *
18  *                This program is free software; you can redistribute it
19  *                and/or modify it under the terms of the GNU General
20  *                Public License as published by the Free Software
21  *                Foundation; either version 2 of the License, or (at
22  *                your option) any later version.
23  *
24  *                This program is distributed in the hope that it will
25  *                be useful, but WITHOUT ANY WARRANTY; without even the
26  *                implied warranty of MERCHANTABILITY or FITNESS FOR A
27  *                PARTICULAR PURPOSE.  See the GNU General Public
28  *                License for more details.
29  *
30  *                The GNU General Public License should be included with
31  *                this file.  If not, you can view it at
32  *                http://www.gnu.org/copyleft/gpl.html
33  *                or write to the Free Software Foundation, Inc., 59
34  *                Temple Place - Suite 330, Boston, MA  02111-1307, USA.
35  *
36  * Revisions   :
37  *    $Log: cgiedit.c,v $
38  *    Revision 1.8  2001/11/30 23:35:51  jongfoster
39  *    Renaming actionsfile to ijb.action
40  *
41  *    Revision 1.7  2001/11/13 00:28:24  jongfoster
42  *    - Renaming parameters from edit-actions-for-url so that they only
43  *      contain legal JavaScript characters.  If we wanted to write
44  *      JavaScript that worked with Netscape 4, this is nessacery.
45  *      (Note that at the moment the JavaScript doesn't actually work
46  *      with Netscape 4, but now this is purely a template issue, not
47  *      one affecting code).
48  *    - Adding new CGIs for use by non-JavaScript browsers:
49  *        edit-actions-url-form
50  *        edit-actions-add-url-form
51  *        edit-actions-remove-url-form
52  *    - Fixing || bug.
53  *
54  *    Revision 1.6  2001/10/29 03:48:09  david__schmidt
55  *    OS/2 native needed a snprintf() routine.  Added one to miscutil, brackedted
56  *    by and __OS2__ ifdef.
57  *
58  *    Revision 1.5  2001/10/25 03:40:48  david__schmidt
59  *    Change in porting tactics: OS/2's EMX porting layer doesn't allow multiple
60  *    threads to call select() simultaneously.  So, it's time to do a real, live,
61  *    native OS/2 port.  See defines for __EMX__ (the porting layer) vs. __OS2__
62  *    (native). Both versions will work, but using __OS2__ offers multi-threading.
63  *
64  *    Revision 1.4  2001/10/23 21:48:19  jongfoster
65  *    Cleaning up error handling in CGI functions - they now send back
66  *    a HTML error page and should never cause a FATAL error.  (Fixes one
67  *    potential source of "denial of service" attacks).
68  *
69  *    CGI actions file editor that works and is actually useful.
70  *
71  *    Ability to toggle JunkBuster remotely using a CGI call.
72  *
73  *    You can turn off both the above features in the main configuration
74  *    file, e.g. if you are running a multi-user proxy.
75  *
76  *    Revision 1.3  2001/10/14 22:12:49  jongfoster
77  *    New version of CGI-based actionsfile editor.
78  *    Major changes, including:
79  *    - Completely new file parser and file output routines
80  *    - edit-actions CGI renamed edit-actions-for-url
81  *    - All CGIs now need a filename parameter, except for...
82  *    - New CGI edit-actions which doesn't need a filename,
83  *      to allow you to start the editor up.
84  *    - edit-actions-submit now works, and now automatically
85  *      redirects you back to the main edit-actions-list handler.
86  *
87  *    Revision 1.2  2001/09/16 17:05:14  jongfoster
88  *    Removing unused #include showarg.h
89  *
90  *    Revision 1.1  2001/09/16 15:47:37  jongfoster
91  *    First version of CGI-based edit interface.  This is very much a
92  *    work-in-progress, and you can't actually use it to edit anything
93  *    yet.  You must #define FEATURE_CGI_EDIT_ACTIONS for these changes
94  *    to have any effect.
95  *
96  *
97  **********************************************************************/
98 \f
99
100 #include "config.h"
101
102 /*
103  * FIXME: Following includes copied from cgi.c - which are actually needed?
104  */
105
106 #include <stdio.h>
107 #include <stdlib.h>
108 #include <sys/types.h>
109 #include <ctype.h>
110 #include <string.h>
111 #include <assert.h>
112 #include <limits.h>
113 #include <sys/stat.h>
114
115 #ifdef _WIN32
116 #define snprintf _snprintf
117 #endif /* def _WIN32 */
118
119 #include "project.h"
120 #include "cgi.h"
121 #include "cgiedit.h"
122 #include "cgisimple.h"
123 #include "list.h"
124 #include "encode.h"
125 #include "actions.h"
126 #include "miscutil.h"
127 #include "errlog.h"
128 #include "loadcfg.h"
129 /* loadcfg.h is for g_bToggleIJB only */
130 #include "urlmatch.h"
131
132 const char cgiedit_h_rcs[] = CGIEDIT_H_VERSION;
133
134
135 #ifdef FEATURE_CGI_EDIT_ACTIONS
136
137 struct file_line
138 {
139    struct file_line * next;
140    char * raw;
141    char * prefix;
142    char * unprocessed;
143    int type;
144
145    union
146    {
147       struct action_spec action[1];
148
149       struct
150       {
151          char * name;
152          char * svalue;
153          int ivalue;
154       } setting;
155
156       /* Add more data types here... e.g.
157
158
159       struct url_spec url[1];
160
161       struct
162       {
163          struct action_spec action[1];
164          const char * name;
165       } alias;
166
167       */
168
169    } data;
170 };
171
172 #define FILE_LINE_UNPROCESSED           1
173 #define FILE_LINE_BLANK                 2
174 #define FILE_LINE_ALIAS_HEADER          3
175 #define FILE_LINE_ALIAS_ENTRY           4
176 #define FILE_LINE_ACTION                5
177 #define FILE_LINE_URL                   6
178 #define FILE_LINE_SETTINGS_HEADER       7
179 #define FILE_LINE_SETTINGS_ENTRY        8
180 #define FILE_LINE_DESCRIPTION_HEADER    9
181 #define FILE_LINE_DESCRIPTION_ENTRY    10
182
183
184 struct editable_file
185 {
186    struct file_line * lines;
187    const char * filename;     /* Full pathname - e.g. "/etc/junkbuster/wibble.action" */
188    const char * identifier;   /* Filename stub - e.g. "wibble".  Use for CGI param. */
189    const char * version_str;  /* Last modification time, as a string.  For CGI param */
190    unsigned version;          /* Last modification time - prevents chaos with
191                                * the browser's "back" button.  Note that this is a
192                                * time_t cast to an unsigned.  When comparing, always
193                                * cast the time_t to an unsigned, and *NOT* vice-versa.
194                                * This may lose the top few bits, but they're not
195                                * significant anyway.
196                                */
197    struct file_line * parse_error; /* On parse error, this is the offending line. */
198    const char * parse_error_text;  /* On parse error, this is the problem.
199                                     * (Statically allocated) */
200 };
201
202 /* FIXME: Following non-static functions should be prototyped in .h or made static */
203
204 /* Functions to read and write arbitrary config files */
205 jb_err edit_read_file(struct client_state *csp,
206                       const struct map *parameters,
207                       int require_version,
208                       const char *suffix,
209                       struct editable_file **pfile);
210 jb_err edit_write_file(struct editable_file * file);
211 void   edit_free_file(struct editable_file * file);
212
213 /* Functions to read and write actions files */
214 jb_err edit_parse_actions_file(struct editable_file * file);
215 jb_err edit_read_actions_file(struct client_state *csp,
216                               struct http_response *rsp,
217                               const struct map *parameters,
218                               int require_version,
219                               struct editable_file **pfile);
220
221 /* Error handlers */
222 jb_err cgi_error_modified(struct client_state *csp,
223                           struct http_response *rsp,
224                           const char *filename);
225 jb_err cgi_error_parse(struct client_state *csp,
226                        struct http_response *rsp,
227                        struct editable_file *file);
228 jb_err cgi_error_file(struct client_state *csp,
229                       struct http_response *rsp,
230                       const char *filename);
231 jb_err cgi_error_disabled(struct client_state *csp,
232                           struct http_response *rsp);
233
234 /* Internal arbitrary config file support functions */
235 static jb_err edit_read_file_lines(FILE *fp, struct file_line ** pfile);
236 static void edit_free_file_lines(struct file_line * first_line);
237 static jb_err simple_read_line(char **dest, FILE *fp);
238 static jb_err edit_read_line(FILE *fp, char **raw_out, char **prefix_out, char **data_out);
239
240 /* Internal actions file support functions */
241 static int match_actions_file_header_line(const char * line, const char * name);
242 static jb_err split_line_on_equals(const char * line, char ** pname, char ** pvalue);
243
244 /* Internal parameter parsing functions */
245 static jb_err get_file_name_param(struct client_state *csp,
246                                   const struct map *parameters,
247                                   const char *param_name,
248                                   const char *suffix,
249                                   char **pfilename,
250                                   const char **pparam);
251 static jb_err get_number_param(struct client_state *csp,
252                                const struct map *parameters,
253                                char *name,
254                                unsigned *pvalue);
255
256 /* Internal actionsfile <==> HTML conversion functions */
257 static jb_err map_radio(struct map * exports,
258                         const char * optionname,
259                         const char * values,
260                         char value);
261 static jb_err actions_to_radio(struct map * exports,
262                                const struct action_spec *action);
263 static jb_err actions_from_radio(const struct map * parameters,
264                                  struct action_spec *action);
265
266
267 static jb_err map_copy_parameter_html(struct map *out,
268                                       const struct map *in,
269                                       const char *name);
270 static jb_err map_copy_parameter_url(struct map *out,
271                                      const struct map *in,
272                                      const char *name);
273
274
275 /*********************************************************************
276  *
277  * Function    :  map_copy_parameter_html
278  *
279  * Description :  Copy a CGI parameter from one map to another, HTML
280  *                encoding it.
281  *
282  * Parameters  :
283  *           1 :  out = target map
284  *           2 :  in = source map
285  *           3 :  name = name of cgi parameter to copy
286  *
287  * Returns     :  JB_ERR_OK on success
288  *                JB_ERR_MEMORY on out-of-memory
289  *                JB_ERR_CGI_PARAMS if the parameter doesn't exist
290  *                                  in the source map
291  *
292  *********************************************************************/
293 static jb_err map_copy_parameter_html(struct map *out,
294                                       const struct map *in,
295                                       const char *name)
296 {
297    const char * value;
298    jb_err err;
299
300    assert(out);
301    assert(in);
302    assert(name);
303
304    value = lookup(in, name);
305    err = map(out, name, 1, html_encode(value), 0);
306
307    if (err)
308    {
309       /* Out of memory */
310       return err;
311    }
312    else if (*value == '\0')
313    {
314       return JB_ERR_CGI_PARAMS;
315    }
316    else
317    {
318       return JB_ERR_OK;
319    }
320 }
321
322
323 /*********************************************************************
324  *
325  * Function    :  map_copy_parameter_html
326  *
327  * Description :  Copy a CGI parameter from one map to another, URL
328  *                encoding it.
329  *
330  * Parameters  :
331  *           1 :  out = target map
332  *           2 :  in = source map
333  *           3 :  name = name of cgi parameter to copy
334  *
335  * Returns     :  JB_ERR_OK on success
336  *                JB_ERR_MEMORY on out-of-memory
337  *                JB_ERR_CGI_PARAMS if the parameter doesn't exist
338  *                                  in the source map
339  *
340  *********************************************************************/
341 static jb_err map_copy_parameter_url(struct map *out,
342                                      const struct map *in,
343                                      const char *name)
344 {
345    const char * value;
346    jb_err err;
347
348    assert(out);
349    assert(in);
350    assert(name);
351
352    value = lookup(in, name);
353    err = map(out, name, 1, url_encode(value), 0);
354
355    if (err)
356    {
357       /* Out of memory */
358       return err;
359    }
360    else if (*value == '\0')
361    {
362       return JB_ERR_CGI_PARAMS;
363    }
364    else
365    {
366       return JB_ERR_OK;
367    }
368 }
369
370
371 /*********************************************************************
372  *
373  * Function    :  cgi_edit_actions_url_form
374  *
375  * Description :  CGI function that displays a form for
376  *                edit-actions-url
377  *
378  * Parameters  :
379  *           1 :  csp = Current client state (buffers, headers, etc...)
380  *           2 :  rsp = http_response data structure for output
381  *           3 :  parameters = map of cgi parameters
382  *
383  * CGI Parameters
384  *    filename : Identifies the file to edit
385  *         ver : File's last-modified time
386  *     section : Line number of section to edit
387  *     pattern : Line number of pattern to edit
388  *      oldval : Current value for pattern
389  *
390  * Returns     :  JB_ERR_OK on success
391  *                JB_ERR_MEMORY on out-of-memory
392  *                JB_ERR_CGI_PARAMS if the CGI parameters are not
393  *                                  specified or not valid.
394  *
395  *********************************************************************/
396 jb_err cgi_edit_actions_url_form(struct client_state *csp,
397                                  struct http_response *rsp,
398                                  const struct map *parameters)
399 {
400    struct map *exports;
401    jb_err err;
402
403    assert(csp);
404    assert(rsp);
405    assert(parameters);
406
407    if (0 == (csp->config->feature_flags & RUNTIME_FEATURE_CGI_EDIT_ACTIONS))
408    {
409       return cgi_error_disabled(csp, rsp);
410    }
411
412    if (NULL == (exports = default_exports(csp, NULL)))
413    {
414       return JB_ERR_MEMORY;
415    }
416
417    err = map_copy_parameter_html(exports, parameters, "section");
418    if (!err) err = map_copy_parameter_html(exports, parameters, "pattern");
419    if (!err) err = map_copy_parameter_html(exports, parameters, "ver");
420    if (!err) err = map_copy_parameter_html(exports, parameters, "filename");
421    if (!err) err = map_copy_parameter_html(exports, parameters, "oldval");
422
423    if (err)
424    {
425       free_map(exports);
426       return err;
427    }
428
429    return template_fill_for_cgi(csp, "edit-actions-url-form", exports, rsp);
430 }
431
432
433 /*********************************************************************
434  *
435  * Function    :  cgi_edit_actions_add_url_form
436  *
437  * Description :  CGI function that displays a form for
438  *                edit-actions-url
439  *
440  * Parameters  :
441  *           1 :  csp = Current client state (buffers, headers, etc...)
442  *           2 :  rsp = http_response data structure for output
443  *           3 :  parameters = map of cgi parameters
444  *
445  * CGI Parameters :
446  *    filename : Identifies the file to edit
447  *         ver : File's last-modified time
448  *     section : Line number of section to edit
449  *
450  * Returns     :  JB_ERR_OK on success
451  *                JB_ERR_MEMORY on out-of-memory
452  *                JB_ERR_CGI_PARAMS if the CGI parameters are not
453  *                                  specified or not valid.
454  *
455  *********************************************************************/
456 jb_err cgi_edit_actions_add_url_form(struct client_state *csp,
457                                      struct http_response *rsp,
458                                      const struct map *parameters)
459 {
460    struct map *exports;
461    jb_err err;
462
463    assert(csp);
464    assert(rsp);
465    assert(parameters);
466
467    if (0 == (csp->config->feature_flags & RUNTIME_FEATURE_CGI_EDIT_ACTIONS))
468    {
469       return cgi_error_disabled(csp, rsp);
470    }
471
472    if (NULL == (exports = default_exports(csp, NULL)))
473    {
474       return JB_ERR_MEMORY;
475    }
476
477    err = map_copy_parameter_html(exports, parameters, "section");
478    if (!err) err = map_copy_parameter_html(exports, parameters, "ver");
479    if (!err) err = map_copy_parameter_html(exports, parameters, "filename");
480
481    if (err)
482    {
483       free_map(exports);
484       return err;
485    }
486
487    return template_fill_for_cgi(csp, "edit-actions-add-url-form", exports, rsp);
488 }
489
490
491 /*********************************************************************
492  *
493  * Function    :  cgi_edit_actions_remove_url_form
494  *
495  * Description :  CGI function that displays a form for
496  *                edit-actions-url
497  *
498  * Parameters  :
499  *           1 :  csp = Current client state (buffers, headers, etc...)
500  *           2 :  rsp = http_response data structure for output
501  *           3 :  parameters = map of cgi parameters
502  *
503  * CGI Parameters :
504  *    filename : Identifies the file to edit
505  *         ver : File's last-modified time
506  *     section : Line number of section to edit
507  *     pattern : Line number of pattern to edit
508  *      oldval : Current value for pattern
509  *
510  * Returns     :  JB_ERR_OK on success
511  *                JB_ERR_MEMORY on out-of-memory
512  *                JB_ERR_CGI_PARAMS if the CGI parameters are not
513  *                                  specified or not valid.
514  *
515  *********************************************************************/
516 jb_err cgi_edit_actions_remove_url_form(struct client_state *csp,
517                                      struct http_response *rsp,
518                                      const struct map *parameters)
519 {
520    struct map *exports;
521    jb_err err;
522
523    assert(csp);
524    assert(rsp);
525    assert(parameters);
526
527    if (0 == (csp->config->feature_flags & RUNTIME_FEATURE_CGI_EDIT_ACTIONS))
528    {
529       return cgi_error_disabled(csp, rsp);
530    }
531
532    if (NULL == (exports = default_exports(csp, NULL)))
533    {
534       return JB_ERR_MEMORY;
535    }
536
537    err = map_copy_parameter_url(exports, parameters, "section");
538    if (!err) err = map_copy_parameter_url(exports, parameters, "pattern");
539    if (!err) err = map_copy_parameter_url(exports, parameters, "ver");
540    if (!err) err = map_copy_parameter_url(exports, parameters, "filename");
541    if (!err) err = map_copy_parameter_html(exports, parameters, "oldval");
542
543    if (err)
544    {
545       free_map(exports);
546       return err;
547    }
548
549    return template_fill_for_cgi(csp, "edit-actions-remove-url-form", exports, rsp);
550 }
551
552
553 /*********************************************************************
554  *
555  * Function    :  simple_read_line
556  *
557  * Description :  Read a single line from a file and return it.
558  *                This is basically a version of fgets() that malloc()s
559  *                it's own line buffer.  Note that the buffer will
560  *                always be a multiple of BUFFER_SIZE bytes long.
561  *                Therefore if you are going to keep the string for
562  *                an extended period of time, you should probably
563  *                strdup() it and free() the original, to save memory.
564  *
565  *
566  * Parameters  :
567  *          1  :  dest = destination for newly malloc'd pointer to
568  *                line data.  Will be set to NULL on error.
569  *          2  :  fp = File to read from
570  *
571  * Returns     :  JB_ERR_OK     on success
572  *                JB_ERR_MEMORY on out-of-memory
573  *                JB_ERR_FILE   on EOF.
574  *
575  *********************************************************************/
576 static jb_err simple_read_line(char **dest, FILE *fp)
577 {
578    int len;
579    char * buf;
580    char * newbuf;
581
582    assert(fp);
583    assert(dest);
584
585    *dest = NULL;
586
587    if (NULL == (buf = malloc(BUFFER_SIZE)))
588    {
589       return JB_ERR_MEMORY;
590    }
591
592    *buf = '\0';
593    len = 0;
594
595    while (FOREVER)
596    {
597       newbuf = buf + len;
598       if ((!fgets(newbuf, BUFFER_SIZE, fp)) || (*newbuf == '\0'))
599       {
600          /* (*newbuf == '\0') should never happen unless fgets fails */
601          if (*buf == '\0')
602          {
603             free(buf);
604             return JB_ERR_FILE;
605          }
606          else
607          {
608             *dest = buf;
609             return JB_ERR_OK;
610          }
611       }
612       len = strlen(buf);
613       if ((buf[len - 1] == '\n') || (buf[len - 1] == '\r'))
614       {
615          *dest = buf;
616          return JB_ERR_OK;
617       }
618
619       if (NULL == (newbuf = realloc(buf, len + BUFFER_SIZE)))
620       {
621          free(buf);
622          return JB_ERR_MEMORY;
623       }
624       buf = newbuf;
625    }
626 }
627
628
629 /*********************************************************************
630  *
631  * Function    :  edit_read_line
632  *
633  * Description :  Read a single non-empty line from a file and return
634  *                it.  Trims comments, leading and trailing whitespace
635  *                and respects escaping of newline and comment char.
636  *                Provides the line in 2 alternative forms: raw and
637  *                preprocessed.
638  *                - raw is the raw data read from the file.  If the
639  *                  line is not modified, then this should be written
640  *                  to the new file.
641  *                - prefix is any comments and blank lines that were
642  *                  read from the file.  If the line is modified, then
643  *                  this should be written out to the file followed
644  *                  by the modified data.  (If this string is non-empty
645  *                  then it will have a newline at the end).
646  *                - data is the actual data that will be parsed
647  *                  further by appropriate routines.
648  *                On EOF, the 3 strings will all be set to NULL and
649  *                0 will be returned.
650  *
651  * Parameters  :
652  *          1  :  fp = File to read from
653  *          2  :  raw_out = destination for newly malloc'd pointer to
654  *                raw line data.  May be NULL if you don't want it.
655  *          3  :  prefix_out = destination for newly malloc'd pointer to
656  *                comments.  May be NULL if you don't want it.
657  *          4  :  data_out = destination for newly malloc'd pointer to
658  *                line data with comments and leading/trailing spaces
659  *                removed, and line continuation performed.  May be
660  *                NULL if you don't want it.
661  *
662  * Returns     :  JB_ERR_OK     on success
663  *                JB_ERR_MEMORY on out-of-memory
664  *                JB_ERR_FILE   on EOF.
665  *
666  *********************************************************************/
667 static jb_err edit_read_line(FILE *fp, char **raw_out, char **prefix_out, char **data_out)
668 {
669    char *p;          /* Temporary pointer   */
670    char *linebuf;    /* Line read from file */
671    char *linestart;  /* Start of linebuf, usually first non-whitespace char */
672    char newline[3];  /* Used to store the newline - "\n", "\r", or "\r\n"   */
673    int contflag = 0; /* Nonzero for line continuation - i.e. line ends '\'  */
674    char *raw;        /* String to be stored in raw_out    */
675    char *prefix;     /* String to be stored in prefix_out */
676    char *data;       /* String to be stored in data_out   */
677    jb_err rval = JB_ERR_OK;
678
679    assert(fp);
680
681    /* Set output parameters to NULL */
682    if (raw_out)
683    {
684       *raw_out    = NULL;
685    }
686    if (prefix_out)
687    {
688       *prefix_out = NULL;
689    }
690    if (data_out)
691    {
692       *data_out   = NULL;
693    }
694
695    /* Set string variables to new, empty strings. */
696
697    raw    = malloc(1);
698    prefix = malloc(1);
699    data   = malloc(1);
700
701    if ((raw == NULL) || (prefix == NULL) || (data == NULL))
702    {
703       freez(raw);
704       freez(prefix);
705       freez(data);
706       return JB_ERR_MEMORY;
707    }
708
709    *raw    = '\0';
710    *prefix = '\0';
711    *data   = '\0';
712
713    /* Main loop.  Loop while we need more data & it's not EOF. */
714
715    while ( (contflag || (*data == '\0'))
716         && (JB_ERR_OK == (rval = simple_read_line(&linebuf, fp))))
717    {
718       if (string_append(&raw,linebuf))
719       {
720          free(prefix);
721          free(data);
722          free(linebuf);
723          return JB_ERR_MEMORY;
724       }
725
726       /* Trim off newline */
727       p = linebuf + strlen(linebuf);
728       if ((p != linebuf) && ((p[-1] == '\r') || (p[-1] == '\n')))
729       {
730          p--;
731          if ((p != linebuf) && ((p[-1] == '\r') || (p[-1] == '\n')))
732          {
733             p--;
734          }
735       }
736       strcpy(newline, p);
737       *p = '\0';
738
739       /* Line continuation? Trim escape and set flag. */
740       contflag = ((p != linebuf) && (*--p == '\\'));
741       if (contflag)
742       {
743          *p = '\0';
744       }
745
746       /* Trim leading spaces if we're at the start of the line */
747       linestart = linebuf;
748       if (*data == '\0')
749       {
750          /* Trim leading spaces */
751          while (*linestart && isspace((int)(unsigned char)*linestart))
752          {
753             linestart++;
754          }
755       }
756
757       /* Handle comment characters. */
758       p = linestart;
759       while ((p = strchr(p, '#')) != NULL)
760       {
761          /* Found a comment char.. */
762          if ((p != linebuf) && (*(p-1) == '\\'))
763          {
764             /* ..and it's escaped, left-shift the line over the escape. */
765             char *q = p - 1;
766             while ((*q = *(q + 1)) != '\0')
767             {
768                q++;
769             }
770             /* Now scan from just after the "#". */
771          }
772          else
773          {
774             /* Real comment.  Save it... */
775             if (p == linestart)
776             {
777                /* Special case:  Line only contains a comment, so all the
778                 * previous whitespace is considered part of the comment.
779                 * Undo the whitespace skipping, if any.
780                 */
781                linestart = linebuf;
782                p = linestart;
783             }
784             string_append(&prefix,p);
785             if (string_append(&prefix,newline))
786             {
787                free(raw);
788                free(data);
789                free(linebuf);
790                return JB_ERR_MEMORY;
791             }
792             *newline = '\0';
793
794             /* ... and chop off the rest of the line */
795             *p = '\0';
796          }
797       } /* END while (there's a # character) */
798
799       /* Write to the buffer */
800       if (*linestart)
801       {
802          if (string_append(&data, linestart))
803          {
804             free(raw);
805             free(prefix);
806             free(linebuf);
807             return JB_ERR_MEMORY;
808          }
809       }
810
811       free(linebuf);
812    } /* END while(we need more data) */
813
814    /* Handle simple_read_line() errors - ignore EOF */
815    if ((rval != JB_ERR_OK) && (rval != JB_ERR_FILE))
816    {
817       free(raw);
818       free(prefix);
819       free(data);
820       return rval;
821    }
822
823
824    if (*raw)
825    {
826       /* Got at least some data */
827
828       /* Remove trailing whitespace */
829       chomp(data);
830
831       if (raw_out)
832       {
833          *raw_out    = raw;
834       }
835       else
836       {
837          free(raw);
838       }
839       if (prefix_out)
840       {
841          *prefix_out = prefix;
842       }
843       else
844       {
845          free(prefix);
846       }
847       if (data_out)
848       {
849          *data_out   = data;
850       }
851       else
852       {
853          free(data);
854       }
855       return JB_ERR_OK;
856    }
857    else
858    {
859       /* EOF and no data there. */
860
861       free(raw);
862       free(prefix);
863       free(data);
864
865       return JB_ERR_FILE;
866    }
867 }
868
869
870 /*********************************************************************
871  *
872  * Function    :  edit_write_file
873  *
874  * Description :  Write a complete file to disk.
875  *
876  * Parameters  :
877  *          1  :  filename = File to write to.
878  *          2  :  file = Data structure to write.
879  *
880  * Returns     :  JB_ERR_OK     on success
881  *                JB_ERR_FILE   on error writing to file.
882  *                JB_ERR_MEMORY on out of memory
883  *
884  *********************************************************************/
885 jb_err edit_write_file(struct editable_file * file)
886 {
887    FILE * fp;
888    struct file_line * cur_line;
889    struct stat statbuf[1];
890    char version_buf[22]; /* 22 = ceil(log10(2^64)) + 2 = max number of
891                             digits in time_t, assuming this is a 64-bit
892                             machine, plus null terminator, plus one
893                             for paranoia */
894
895    assert(file);
896    assert(file->filename);
897
898    if (NULL == (fp = fopen(file->filename, "wt")))
899    {
900       return JB_ERR_FILE;
901    }
902
903    cur_line = file->lines;
904    while (cur_line != NULL)
905    {
906       if (cur_line->raw)
907       {
908          if (fputs(cur_line->raw, fp) < 0)
909          {
910             fclose(fp);
911             return JB_ERR_FILE;
912          }
913       }
914       else
915       {
916          if (cur_line->prefix)
917          {
918             if (fputs(cur_line->prefix, fp) < 0)
919             {
920                fclose(fp);
921                return JB_ERR_FILE;
922             }
923          }
924          if (cur_line->unprocessed)
925          {
926             if (fputs(cur_line->unprocessed, fp) < 0)
927             {
928                fclose(fp);
929                return JB_ERR_FILE;
930             }
931             if (fputs("\n", fp) < 0)
932             {
933                fclose(fp);
934                return JB_ERR_FILE;
935             }
936          }
937          else
938          {
939             /* FIXME: Write data from file->data->whatever */
940             assert(0);
941          }
942       }
943       cur_line = cur_line->next;
944    }
945
946    fclose(fp);
947
948
949    /* Update the version stamp in the file structure, since we just
950     * wrote to the file & changed it's date.
951     */
952    if (stat(file->filename, statbuf) < 0)
953    {
954       /* Error, probably file not found. */
955       return JB_ERR_FILE;
956    }
957    file->version = (unsigned)statbuf->st_mtime;
958
959    /* Correct file->version_str */
960    freez(file->version_str);
961    snprintf(version_buf, 22, "%u", file->version);
962    version_buf[21] = '\0';
963    file->version_str = strdup(version_buf);
964    if (version_buf == NULL)
965    {
966       return JB_ERR_MEMORY;
967    }
968
969    return JB_ERR_OK;
970 }
971
972
973 /*********************************************************************
974  *
975  * Function    :  edit_free_file
976  *
977  * Description :  Free a complete file in memory.
978  *
979  * Parameters  :
980  *          1  :  file = Data structure to free.
981  *
982  * Returns     :  N/A
983  *
984  *********************************************************************/
985 void edit_free_file(struct editable_file * file)
986 {
987    if (!file)
988    {
989       /* Silently ignore NULL pointer */
990       return;
991    }
992
993    edit_free_file_lines(file->lines);
994    freez(file->filename);
995    freez(file->identifier);
996    freez(file->version_str);
997    file->version = 0;
998    file->parse_error_text = NULL; /* Statically allocated */
999    file->parse_error = NULL;
1000
1001    free(file);
1002 }
1003
1004
1005 /*********************************************************************
1006  *
1007  * Function    :  edit_free_file
1008  *
1009  * Description :  Free an entire linked list of file lines.
1010  *
1011  * Parameters  :
1012  *          1  :  first_line = Data structure to free.
1013  *
1014  * Returns     :  N/A
1015  *
1016  *********************************************************************/
1017 static void edit_free_file_lines(struct file_line * first_line)
1018 {
1019    struct file_line * next_line;
1020
1021    while (first_line != NULL)
1022    {
1023       next_line = first_line->next;
1024       first_line->next = NULL;
1025       freez(first_line->raw);
1026       freez(first_line->prefix);
1027       freez(first_line->unprocessed);
1028       switch(first_line->type)
1029       {
1030          case 0: /* special case if memory zeroed */
1031          case FILE_LINE_UNPROCESSED:
1032          case FILE_LINE_BLANK:
1033          case FILE_LINE_ALIAS_HEADER:
1034          case FILE_LINE_SETTINGS_HEADER:
1035          case FILE_LINE_DESCRIPTION_HEADER:
1036          case FILE_LINE_DESCRIPTION_ENTRY:
1037          case FILE_LINE_ALIAS_ENTRY:
1038          case FILE_LINE_URL:
1039             /* No data is stored for these */
1040             break;
1041
1042          case FILE_LINE_ACTION:
1043             free_action(first_line->data.action);
1044             break;
1045
1046          case FILE_LINE_SETTINGS_ENTRY:
1047             freez(first_line->data.setting.name);
1048             freez(first_line->data.setting.svalue);
1049             break;
1050          default:
1051             /* Should never happen */
1052             assert(0);
1053             break;
1054       }
1055       first_line->type = 0; /* paranoia */
1056       free(first_line);
1057       first_line = next_line;
1058    }
1059 }
1060
1061
1062 /*********************************************************************
1063  *
1064  * Function    :  match_actions_file_header_line
1065  *
1066  * Description :  Match an actions file {{header}} line
1067  *
1068  * Parameters  :
1069  *          1  :  line - String from file
1070  *          2  :  name - Header to match against
1071  *
1072  * Returns     :  0 iff they match.
1073  *
1074  *********************************************************************/
1075 static int match_actions_file_header_line(const char * line, const char * name)
1076 {
1077    int len;
1078
1079    assert(line);
1080    assert(name);
1081
1082    /* Look for "{{" */
1083    if ((line[0] != '{') || (line[1] != '{'))
1084    {
1085       return 1;
1086    }
1087    line += 2;
1088
1089    /* Look for optional whitespace */
1090    while ( (*line == ' ') || (*line == '\t') )
1091    {
1092       line++;
1093    }
1094
1095    /* Look for the specified name (case-insensitive) */
1096    len = strlen(name);
1097    if (0 != strncmpic(line, name, len))
1098    {
1099       return 1;
1100    }
1101    line += len;
1102
1103    /* Look for optional whitespace */
1104    while ( (*line == ' ') || (*line == '\t') )
1105    {
1106       line++;
1107    }
1108
1109    /* Look for "}}" and end of string*/
1110    if ((line[0] != '}') || (line[1] != '}') || (line[2] != '\0'))
1111    {
1112       return 1;
1113    }
1114
1115    /* It matched!! */
1116    return 0;
1117 }
1118
1119
1120 /*********************************************************************
1121  *
1122  * Function    :  match_actions_file_header_line
1123  *
1124  * Description :  Match an actions file {{header}} line
1125  *
1126  * Parameters  :
1127  *          1  :  line - String from file.  Must not start with
1128  *                       whitespace (else infinite loop!)
1129  *          2  :  name - Destination for name
1130  *          2  :  name - Destination for value
1131  *
1132  * Returns     :  JB_ERR_OK     on success
1133  *                JB_ERR_MEMORY on out-of-memory
1134  *                JB_ERR_PARSE  if there's no "=" sign, or if there's
1135  *                              nothing before the "=" sign (but empty
1136  *                              values *after* the "=" sign are legal).
1137  *
1138  *********************************************************************/
1139 static jb_err split_line_on_equals(const char * line, char ** pname, char ** pvalue)
1140 {
1141    const char * name_end;
1142    const char * value_start;
1143    int name_len;
1144
1145    assert(line);
1146    assert(pname);
1147    assert(pvalue);
1148    assert(*line != ' ');
1149    assert(*line != '\t');
1150
1151    *pname = NULL;
1152    *pvalue = NULL;
1153
1154    value_start = strchr(line, '=');
1155    if ((value_start == NULL) || (value_start == line))
1156    {
1157       return JB_ERR_PARSE;
1158    }
1159
1160    name_end = value_start - 1;
1161
1162    /* Eat any whitespace before the '=' */
1163    while ((*name_end == ' ') || (*name_end == '\t'))
1164    {
1165       /*
1166        * we already know we must have at least 1 non-ws char
1167        * at start of buf - no need to check
1168        */
1169       name_end--;
1170    }
1171
1172    name_len = name_end - line + 1; /* Length excluding \0 */
1173    if (NULL == (*pname = (char *) malloc(name_len + 1)))
1174    {
1175       return JB_ERR_MEMORY;
1176    }
1177    strncpy(*pname, line, name_len);
1178    (*pname)[name_len] = '\0';
1179
1180    /* Eat any the whitespace after the '=' */
1181    value_start++;
1182    while ((*value_start == ' ') || (*value_start == '\t'))
1183    {
1184       value_start++;
1185    }
1186
1187    if (NULL == (*pvalue = strdup(value_start)))
1188    {
1189       free(*pname);
1190       *pname = NULL;
1191       return JB_ERR_MEMORY;
1192    }
1193
1194    return JB_ERR_OK;
1195 }
1196
1197
1198 /*********************************************************************
1199  *
1200  * Function    :  edit_parse_actions_file
1201  *
1202  * Description :  Parse an actions file in memory.
1203  *
1204  *                Passed linked list must have the "data" member
1205  *                zeroed, and must contain valid "next" and
1206  *                "unprocessed" fields.  The "raw" and "prefix"
1207  *                fields are ignored, and "type" is just overwritten.
1208  *
1209  *                Note that on error the file may have been
1210  *                partially parsed.
1211  *
1212  * Parameters  :
1213  *          1  :  file = Actions file to be parsed in-place.
1214  *
1215  * Returns     :  JB_ERR_OK     on success
1216  *                JB_ERR_MEMORY on out-of-memory
1217  *                JB_ERR_PARSE  on error
1218  *
1219  *********************************************************************/
1220 jb_err edit_parse_actions_file(struct editable_file * file)
1221 {
1222    struct file_line * cur_line;
1223    int len;
1224    const char * text; /* Text from a line */
1225    char * name;  /* For lines of the form name=value */
1226    char * value; /* For lines of the form name=value */
1227    struct action_alias * alias_list = NULL;
1228    jb_err err = JB_ERR_OK;
1229
1230    /* alias_list contains the aliases defined in this file.
1231     * It might be better to use the "file_line.data" fields
1232     * in the relavent places instead.
1233     */
1234
1235    cur_line = file->lines;
1236
1237    /* A note about blank line support: Blank lines should only
1238     * ever occur as the last line in the file.  This function
1239     * is more forgiving than that - FILE_LINE_BLANK can occur
1240     * anywhere.
1241     */
1242
1243    /* Skip leading blanks.  Should only happen if file is
1244     * empty (which is valid, but pointless).
1245     */
1246    while ( (cur_line != NULL)
1247         && (cur_line->unprocessed[0] == '\0') )
1248    {
1249       /* Blank line */
1250       cur_line->type = FILE_LINE_BLANK;
1251       cur_line = cur_line->next;
1252    }
1253
1254    if ( (cur_line != NULL)
1255      && (cur_line->unprocessed[0] != '{') )
1256    {
1257       /* File doesn't start with a header */
1258       file->parse_error = cur_line;
1259       file->parse_error_text = "First (non-comment) line of the file must contain a header.";
1260       return JB_ERR_PARSE;
1261    }
1262
1263    if ( (cur_line != NULL) && (0 ==
1264       match_actions_file_header_line(cur_line->unprocessed, "settings") ) )
1265    {
1266       cur_line->type = FILE_LINE_SETTINGS_HEADER;
1267
1268       cur_line = cur_line->next;
1269       while ((cur_line != NULL) && (cur_line->unprocessed[0] != '{'))
1270       {
1271          if (cur_line->unprocessed[0])
1272          {
1273             cur_line->type = FILE_LINE_SETTINGS_ENTRY;
1274
1275             err = split_line_on_equals(cur_line->unprocessed,
1276                      &cur_line->data.setting.name,
1277                      &cur_line->data.setting.svalue);
1278             if (err == JB_ERR_MEMORY)
1279             {
1280                return err;
1281             }
1282             else if (err != JB_ERR_OK)
1283             {
1284                /* Line does not contain a name=value pair */
1285                file->parse_error = cur_line;
1286                file->parse_error_text = "Expected a name=value pair on this {{description}} line, but couldn't find one.";
1287                return JB_ERR_PARSE;
1288             }
1289          }
1290          else
1291          {
1292             cur_line->type = FILE_LINE_BLANK;
1293          }
1294          cur_line = cur_line->next;
1295       }
1296    }
1297
1298    if ( (cur_line != NULL) && (0 ==
1299       match_actions_file_header_line(cur_line->unprocessed, "description") ) )
1300    {
1301       cur_line->type = FILE_LINE_DESCRIPTION_HEADER;
1302
1303       cur_line = cur_line->next;
1304       while ((cur_line != NULL) && (cur_line->unprocessed[0] != '{'))
1305       {
1306          if (cur_line->unprocessed[0])
1307          {
1308             cur_line->type = FILE_LINE_DESCRIPTION_ENTRY;
1309          }
1310          else
1311          {
1312             cur_line->type = FILE_LINE_BLANK;
1313          }
1314          cur_line = cur_line->next;
1315       }
1316    }
1317
1318    if ( (cur_line != NULL) && (0 ==
1319       match_actions_file_header_line(cur_line->unprocessed, "alias") ) )
1320    {
1321       cur_line->type = FILE_LINE_ALIAS_HEADER;
1322
1323       cur_line = cur_line->next;
1324       while ((cur_line != NULL) && (cur_line->unprocessed[0] != '{'))
1325       {
1326          if (cur_line->unprocessed[0])
1327          {
1328             /* define an alias */
1329             struct action_alias * new_alias;
1330
1331             cur_line->type = FILE_LINE_ALIAS_ENTRY;
1332
1333             err = split_line_on_equals(cur_line->unprocessed, &name, &value);
1334             if (err == JB_ERR_MEMORY)
1335             {
1336                return err;
1337             }
1338             else if (err != JB_ERR_OK)
1339             {
1340                /* Line does not contain a name=value pair */
1341                file->parse_error = cur_line;
1342                file->parse_error_text = "Expected a name=value pair on this {{alias}} line, but couldn't find one.";
1343                return JB_ERR_PARSE;
1344             }
1345
1346             if ((new_alias = zalloc(sizeof(*new_alias))) == NULL)
1347             {
1348                /* Out of memory */
1349                free(name);
1350                free(value);
1351                free_alias_list(alias_list);
1352                return JB_ERR_MEMORY;
1353             }
1354
1355             err = get_actions(value, alias_list, new_alias->action);
1356             if (err)
1357             {
1358                /* Invalid action or out of memory */
1359                free(name);
1360                free(value);
1361                free(new_alias);
1362                free_alias_list(alias_list);
1363                if (err == JB_ERR_MEMORY)
1364                {
1365                   return err;
1366                }
1367                else
1368                {
1369                   /* Line does not contain a name=value pair */
1370                   file->parse_error = cur_line;
1371                   file->parse_error_text = "This alias does not specify a valid set of actions.";
1372                   return JB_ERR_PARSE;
1373                }
1374             }
1375
1376             free(value);
1377
1378             new_alias->name = name;
1379
1380             /* add to list */
1381             new_alias->next = alias_list;
1382             alias_list = new_alias;
1383          }
1384          else
1385          {
1386             cur_line->type = FILE_LINE_BLANK;
1387          }
1388          cur_line = cur_line->next;
1389       }
1390    }
1391
1392    /* Header done, process the main part of the file */
1393    while (cur_line != NULL)
1394    {
1395       /* At this point, (cur_line->unprocessed[0] == '{') */
1396       assert(cur_line->unprocessed[0] == '{');
1397       text = cur_line->unprocessed + 1;
1398       len = strlen(text) - 1;
1399       if (text[len] != '}')
1400       {
1401          /* No closing } on header */
1402          free_alias_list(alias_list);
1403          file->parse_error = cur_line;
1404          file->parse_error_text = "Headers starting with '{' must have a "
1405             "closing bracket ('}').  Headers starting with two brackets ('{{') "
1406             "must close with two brackets ('}}').";
1407          return JB_ERR_PARSE;
1408       }
1409
1410       if (text[0] == '{')
1411       {
1412          /* An invalid {{ header.  */
1413          free_alias_list(alias_list);
1414          file->parse_error = cur_line;
1415          file->parse_error_text = "Unknown or unexpected two-bracket header.  "
1416             "Please remember that the system (two-bracket) headers must "
1417             "appear in the order {{settings}}, {{description}}, {{alias}}, "
1418             "and must appear before any actions (one-bracket) headers.  "
1419             "Also note that system headers may not be repeated.";
1420          return JB_ERR_PARSE;
1421       }
1422
1423       while ( (*text == ' ') || (*text == '\t') )
1424       {
1425          text++;
1426          len--;
1427       }
1428       while ( (len > 0)
1429            && ( (text[len - 1] == ' ')
1430              || (text[len - 1] == '\t') ) )
1431       {
1432          len--;
1433       }
1434
1435       cur_line->type = FILE_LINE_ACTION;
1436
1437       /* Remove {} and make copy */
1438       if (NULL == (value = (char *) malloc(len + 1)))
1439       {
1440          /* Out of memory */
1441          free_alias_list(alias_list);
1442          return JB_ERR_MEMORY;
1443       }
1444       strncpy(value, text, len);
1445       value[len] = '\0';
1446
1447       /* Get actions */
1448       err = get_actions(value, alias_list, cur_line->data.action);
1449       if (err)
1450       {
1451          /* Invalid action or out of memory */
1452          free(value);
1453          free_alias_list(alias_list);
1454          if (err == JB_ERR_MEMORY)
1455          {
1456             return err;
1457          }
1458          else
1459          {
1460             /* Line does not contain a name=value pair */
1461             file->parse_error = cur_line;
1462             file->parse_error_text = "This header does not specify a valid set of actions.";
1463             return JB_ERR_PARSE;
1464          }
1465       }
1466
1467       /* Done with string - it was clobbered anyway */
1468       free(value);
1469
1470       /* Process next line */
1471       cur_line = cur_line->next;
1472
1473       /* Loop processing URL patterns */
1474       while ((cur_line != NULL) && (cur_line->unprocessed[0] != '{'))
1475       {
1476          if (cur_line->unprocessed[0])
1477          {
1478             /* Could parse URL here, but this isn't currently needed */
1479
1480             cur_line->type = FILE_LINE_URL;
1481          }
1482          else
1483          {
1484             cur_line->type = FILE_LINE_BLANK;
1485          }
1486          cur_line = cur_line->next;
1487       }
1488    } /* End main while(cur_line != NULL) loop */
1489
1490    free_alias_list(alias_list);
1491
1492    return JB_ERR_OK;
1493 }
1494
1495
1496 /*********************************************************************
1497  *
1498  * Function    :  edit_read_file_lines
1499  *
1500  * Description :  Read all the lines of a file into memory.
1501  *                Handles whitespace, comments and line continuation.
1502  *
1503  * Parameters  :
1504  *          1  :  fp = File to read from.  On return, this will be
1505  *                     at EOF but it will not have been closed.
1506  *          2  :  pfile = Destination for a linked list of file_lines.
1507  *                        Will be set to NULL on error.
1508  *
1509  * Returns     :  JB_ERR_OK     on success
1510  *                JB_ERR_MEMORY on out-of-memory
1511  *
1512  *********************************************************************/
1513 jb_err edit_read_file_lines(FILE *fp, struct file_line ** pfile)
1514 {
1515    struct file_line * first_line; /* Keep for return value or to free */
1516    struct file_line * cur_line;   /* Current line */
1517    struct file_line * prev_line;  /* Entry with prev_line->next = cur_line */
1518    jb_err rval;
1519
1520    assert(fp);
1521    assert(pfile);
1522
1523    *pfile = NULL;
1524
1525    cur_line = first_line = zalloc(sizeof(struct file_line));
1526    if (cur_line == NULL)
1527    {
1528       return JB_ERR_MEMORY;
1529    }
1530
1531    cur_line->type = FILE_LINE_UNPROCESSED;
1532
1533    rval = edit_read_line(fp, &cur_line->raw, &cur_line->prefix, &cur_line->unprocessed);
1534    if (rval)
1535    {
1536       /* Out of memory or empty file. */
1537       /* Note that empty file is not an error we propogate up */
1538       free(cur_line);
1539       return ((rval == JB_ERR_FILE) ? JB_ERR_OK : rval);
1540    }
1541
1542    do
1543    {
1544       prev_line = cur_line;
1545       cur_line = prev_line->next = zalloc(sizeof(struct file_line));
1546       if (cur_line == NULL)
1547       {
1548          /* Out of memory */
1549          edit_free_file_lines(first_line);
1550          return JB_ERR_MEMORY;
1551       }
1552
1553       cur_line->type = FILE_LINE_UNPROCESSED;
1554
1555       rval = edit_read_line(fp, &cur_line->raw, &cur_line->prefix, &cur_line->unprocessed);
1556       if ((rval != JB_ERR_OK) && (rval != JB_ERR_FILE))
1557       {
1558          /* Out of memory */
1559          edit_free_file_lines(first_line);
1560          return JB_ERR_MEMORY;
1561       }
1562
1563    }
1564    while (rval != JB_ERR_FILE);
1565
1566    /* EOF */
1567
1568    /* We allocated one too many - free it */
1569    prev_line->next = NULL;
1570    free(cur_line);
1571
1572    *pfile = first_line;
1573    return JB_ERR_OK;
1574 }
1575
1576
1577 /*********************************************************************
1578  *
1579  * Function    :  edit_read_file
1580  *
1581  * Description :  Read a complete file into memory.
1582  *                Handles CGI parameter parsing.  If requested, also
1583  *                checks the file's modification timestamp.
1584  *
1585  * Parameters  :
1586  *          1  :  csp = Current client state (buffers, headers, etc...)
1587  *          2  :  parameters = map of cgi parameters.
1588  *          3  :  require_version = true to check "ver" parameter.
1589  *          4  :  suffix = File extension, e.g. ".action".
1590  *          5  :  pfile = Destination for the file.  Will be set
1591  *                        to NULL on error.
1592  *
1593  * CGI Parameters :
1594  *    filename :  The name of the file to read, without the
1595  *                path or ".action" extension.
1596  *         ver :  (Only if require_version is nonzero)
1597  *                Timestamp of the actions file.  If wrong, this
1598  *                function fails with JB_ERR_MODIFIED.
1599  *
1600  * Returns     :  JB_ERR_OK     on success
1601  *                JB_ERR_MEMORY on out-of-memory
1602  *                JB_ERR_CGI_PARAMS if "filename" was not specified
1603  *                                  or is not valid.
1604  *                JB_ERR_FILE   if the file cannot be opened or
1605  *                              contains no data
1606  *                JB_ERR_MODIFIED if version checking was requested and
1607  *                                failed - the file was modified outside
1608  *                                of this CGI editor instance.
1609  *
1610  *********************************************************************/
1611 jb_err edit_read_file(struct client_state *csp,
1612                       const struct map *parameters,
1613                       int require_version,
1614                       const char *suffix,
1615                       struct editable_file **pfile)
1616 {
1617    struct file_line * lines;
1618    FILE * fp;
1619    jb_err err;
1620    char * filename;
1621    const char * identifier;
1622    struct editable_file * file;
1623    unsigned version = 0;
1624    struct stat statbuf[1];
1625    char version_buf[22];
1626
1627    assert(csp);
1628    assert(parameters);
1629    assert(pfile);
1630
1631    *pfile = NULL;
1632
1633    err = get_file_name_param(csp, parameters, "filename", suffix,
1634                              &filename, &identifier);
1635    if (err)
1636    {
1637       return err;
1638    }
1639
1640    if (stat(filename, statbuf) < 0)
1641    {
1642       /* Error, probably file not found. */
1643       free(filename);
1644       return JB_ERR_FILE;
1645    }
1646    version = (unsigned) statbuf->st_mtime;
1647
1648    if (require_version)
1649    {
1650       unsigned specified_version;
1651       err = get_number_param(csp, parameters, "ver", &specified_version);
1652       if (err)
1653       {
1654          free(filename);
1655          return err;
1656       }
1657
1658       if (version != specified_version)
1659       {
1660          return JB_ERR_MODIFIED;
1661       }
1662    }
1663
1664    if (NULL == (fp = fopen(filename,"rt")))
1665    {
1666       free(filename);
1667       return JB_ERR_FILE;
1668    }
1669
1670    err = edit_read_file_lines(fp, &lines);
1671
1672    fclose(fp);
1673
1674    if (err)
1675    {
1676       free(filename);
1677       return err;
1678    }
1679
1680    file = (struct editable_file *) zalloc(sizeof(*file));
1681    if (err)
1682    {
1683       free(filename);
1684       edit_free_file_lines(lines);
1685       return err;
1686    }
1687
1688    file->lines = lines;
1689    file->filename = filename;
1690    file->version = version;
1691    file->identifier = strdup(identifier);
1692
1693    if (file->identifier == NULL)
1694    {
1695       edit_free_file(file);
1696       return JB_ERR_MEMORY;
1697    }
1698
1699    /* Correct file->version_str */
1700    freez(file->version_str);
1701    snprintf(version_buf, 22, "%u", file->version);
1702    version_buf[21] = '\0';
1703    file->version_str = strdup(version_buf);
1704    if (version_buf == NULL)
1705    {
1706       edit_free_file(file);
1707       return JB_ERR_MEMORY;
1708    }
1709
1710    *pfile = file;
1711    return JB_ERR_OK;
1712 }
1713
1714
1715 /*********************************************************************
1716  *
1717  * Function    :  edit_read_actions_file
1718  *
1719  * Description :  Read a complete actions file into memory.
1720  *                Handles CGI parameter parsing.  If requested, also
1721  *                checks the file's modification timestamp.
1722  *
1723  *                If this function detects an error in the categories
1724  *                JB_ERR_FILE, JB_ERR_MODIFIED, or JB_ERR_PARSE,
1725  *                then it handles it by filling in the specified
1726  *                response structure and returning JB_ERR_FILE.
1727  *
1728  * Parameters  :
1729  *          1  :  csp = Current client state (buffers, headers, etc...)
1730  *          2  :  rsp = HTTP response.  Only filled in on error.
1731  *          2  :  parameters = map of cgi parameters.
1732  *          3  :  require_version = true to check "ver" parameter.
1733  *          4  :  pfile = Destination for the file.  Will be set
1734  *                        to NULL on error.
1735  *
1736  * CGI Parameters :
1737  *    filename :  The name of the actions file to read, without the
1738  *                path or ".action" extension.
1739  *         ver :  (Only if require_version is nonzero)
1740  *                Timestamp of the actions file.  If wrong, this
1741  *                function fails with JB_ERR_MODIFIED.
1742  *
1743  * Returns     :  JB_ERR_OK     on success
1744  *                JB_ERR_MEMORY on out-of-memory
1745  *                JB_ERR_CGI_PARAMS if "filename" was not specified
1746  *                                  or is not valid.
1747  *                JB_ERR_FILE  if the file does not contain valid data,
1748  *                             or if file cannot be opened or
1749  *                             contains no data, or if version
1750  *                             checking was requested and failed.
1751  *
1752  *********************************************************************/
1753 jb_err edit_read_actions_file(struct client_state *csp,
1754                               struct http_response *rsp,
1755                               const struct map *parameters,
1756                               int require_version,
1757                               struct editable_file **pfile)
1758 {
1759    jb_err err;
1760    struct editable_file *file;
1761
1762    assert(csp);
1763    assert(parameters);
1764    assert(pfile);
1765
1766    *pfile = NULL;
1767
1768    err = edit_read_file(csp, parameters, require_version, ".action", &file);
1769    if (err)
1770    {
1771       /* Try to handle if possible */
1772       if (err == JB_ERR_FILE)
1773       {
1774          err = cgi_error_file(csp, rsp, lookup(parameters, "filename"));
1775       }
1776       else if (err == JB_ERR_MODIFIED)
1777       {
1778          err = cgi_error_modified(csp, rsp, lookup(parameters, "filename"));
1779       }
1780       if (err == JB_ERR_OK)
1781       {
1782          /*
1783           * Signal to higher-level CGI code that there was a problem but we
1784           * handled it, they should just return JB_ERR_OK.
1785           */
1786          err = JB_ERR_FILE;
1787       }
1788       return err;
1789    }
1790
1791    err = edit_parse_actions_file(file);
1792    if (err)
1793    {
1794       if (err == JB_ERR_PARSE)
1795       {
1796          err = cgi_error_parse(csp, rsp, file);
1797          if (err == JB_ERR_OK)
1798          {
1799             /*
1800              * Signal to higher-level CGI code that there was a problem but we
1801              * handled it, they should just return JB_ERR_OK.
1802              */
1803             err = JB_ERR_FILE;
1804          }
1805       }
1806       edit_free_file(file);
1807       return err;
1808    }
1809
1810    *pfile = file;
1811    return JB_ERR_OK;
1812 }
1813
1814
1815 /*********************************************************************
1816  *
1817  * Function    :  get_file_name_param
1818  *
1819  * Description :  Get the name of the file to edit from the parameters
1820  *                passed to a CGI function.  This function handles
1821  *                security checks such as blocking urls containing
1822  *                "/" or ".", prepending the config file directory,
1823  *                and adding the specified suffix.
1824  *
1825  *                (This is an essential security check, otherwise
1826  *                users may be able to pass "../../../etc/passwd"
1827  *                and overwrite the password file [linux], "prn:"
1828  *                and print random data [Windows], etc...)
1829  *
1830  *                This function only allows filenames contining the
1831  *                characters '-', '_', 'A'-'Z', 'a'-'z', and '0'-'9'.
1832  *                That's probably too restrictive but at least it's
1833  *                secure.
1834  *
1835  * Parameters  :
1836  *           1 :  csp = Current client state (buffers, headers, etc...)
1837  *           2 :  parameters = map of cgi parameters
1838  *           3 :  suffix = File extension, e.g. ".actions"
1839  *           4 :  pfilename = destination for full filename.  Caller
1840  *                free()s.  Set to NULL on error.
1841  *           5 :  pparam = destination for partial filename,
1842  *                suitable for use in another URL.  Allocated as part
1843  *                of the map "parameters", so don't free it.
1844  *                Set to NULL if not specified.
1845  *
1846  * Returns     :  JB_ERR_OK         on success
1847  *                JB_ERR_MEMORY     on out-of-memory
1848  *                JB_ERR_CGI_PARAMS if "filename" was not specified
1849  *                                  or is not valid.
1850  *
1851  *********************************************************************/
1852 static jb_err get_file_name_param(struct client_state *csp,
1853                                   const struct map *parameters,
1854                                   const char *param_name,
1855                                   const char *suffix,
1856                                   char **pfilename,
1857                                   const char **pparam)
1858 {
1859    const char *param;
1860    const char *s;
1861    char *name;
1862    char *fullpath;
1863    char ch;
1864    int len;
1865
1866    assert(csp);
1867    assert(parameters);
1868    assert(suffix);
1869    assert(pfilename);
1870    assert(pparam);
1871
1872    *pfilename = NULL;
1873    *pparam = NULL;
1874
1875    param = lookup(parameters, param_name);
1876    if (!*param)
1877    {
1878       return JB_ERR_CGI_PARAMS;
1879    }
1880
1881    *pparam = param;
1882
1883    len = strlen(param);
1884    if (len >= FILENAME_MAX)
1885    {
1886       /* Too long. */
1887       return JB_ERR_CGI_PARAMS;
1888    }
1889
1890    /* Check every character to see if it's legal */
1891    s = param;
1892    while ((ch = *s++) != '\0')
1893    {
1894       if ( ((ch < 'A') || (ch > 'Z'))
1895         && ((ch < 'a') || (ch > 'z'))
1896         && ((ch < '0') || (ch > '9'))
1897         && (ch != '-')
1898         && (ch != '_') )
1899       {
1900          /* Probable hack attempt. */
1901          return JB_ERR_CGI_PARAMS;
1902       }
1903    }
1904
1905    /* Append extension */
1906    name = malloc(len + strlen(suffix) + 1);
1907    if (name == NULL)
1908    {
1909       return JB_ERR_MEMORY;
1910    }
1911    strcpy(name, param);
1912    strcpy(name + len, suffix);
1913
1914    /* Prepend path */
1915    fullpath = make_path(csp->config->confdir, name);
1916    free(name);
1917    if (fullpath == NULL)
1918    {
1919       return JB_ERR_MEMORY;
1920    }
1921
1922    /* Success */
1923    *pfilename = fullpath;
1924
1925    return JB_ERR_OK;
1926 }
1927
1928
1929 /*********************************************************************
1930  *
1931  * Function    :  get_number_param
1932  *
1933  * Description :  Get a non-negative integer from the parameters
1934  *                passed to a CGI function.
1935  *
1936  * Parameters  :
1937  *           1 :  csp = Current client state (buffers, headers, etc...)
1938  *           2 :  parameters = map of cgi parameters
1939  *           3 :  name = Name of CGI parameter to read
1940  *           4 :  pvalue = destination for value.
1941  *                         Set to -1 on error.
1942  *
1943  * Returns     :  JB_ERR_OK         on success
1944  *                JB_ERR_MEMORY     on out-of-memory
1945  *                JB_ERR_CGI_PARAMS if the parameter was not specified
1946  *                                  or is not valid.
1947  *
1948  *********************************************************************/
1949 static jb_err get_number_param(struct client_state *csp,
1950                                const struct map *parameters,
1951                                char *name,
1952                                unsigned *pvalue)
1953 {
1954    const char *param;
1955    char ch;
1956    unsigned value;
1957
1958    assert(csp);
1959    assert(parameters);
1960    assert(name);
1961    assert(pvalue);
1962
1963    *pvalue = -1;
1964
1965    param = lookup(parameters, name);
1966    if (!*param)
1967    {
1968       return JB_ERR_CGI_PARAMS;
1969    }
1970
1971    /* We don't use atoi because I want to check this carefully... */
1972
1973    value = 0;
1974    while ((ch = *param++) != '\0')
1975    {
1976       if ((ch < '0') || (ch > '9'))
1977       {
1978          return JB_ERR_CGI_PARAMS;
1979       }
1980
1981       ch -= '0';
1982
1983       /* Note:
1984        *
1985        * <limits.h> defines UINT_MAX
1986        *
1987        * (UINT_MAX - ch) / 10 is the largest number that
1988        *     can be safely multiplied by 10 then have ch added.
1989        */
1990       if (value > ((UINT_MAX - (unsigned)ch) / 10U))
1991       {
1992          return JB_ERR_CGI_PARAMS;
1993       }
1994
1995       value = value * 10 + ch;
1996    }
1997
1998    /* Success */
1999    *pvalue = value;
2000
2001    return JB_ERR_OK;
2002 }
2003
2004
2005 /*********************************************************************
2006  *
2007  * Function    :  map_radio
2008  *
2009  * Description :  Map a set of radio button values.  E.g. if you have
2010  *                3 radio buttons, declare them as:
2011  *                  <option type="radio" name="xyz" @xyz-a@>
2012  *                  <option type="radio" name="xyz" @xyz-b@>
2013  *                  <option type="radio" name="xyz" @xyz-c@>
2014  *                Then map one of the @xyz-?@ variables to "checked"
2015  *                and all the others to empty by calling:
2016  *                map_radio(exports, "xyz", "abc", sel)
2017  *                Where 'sel' is 'a', 'b', or 'c'.
2018  *
2019  * Parameters  :
2020  *           1 :  exports = Exports map to modify.
2021  *           2 :  optionname = name for map
2022  *           3 :  values = null-terminated list of values;
2023  *           4 :  value = Selected value.
2024  *
2025  * CGI Parameters : None
2026  *
2027  * Returns     :  JB_ERR_OK     on success
2028  *                JB_ERR_MEMORY on out-of-memory
2029  *
2030  *********************************************************************/
2031 static jb_err map_radio(struct map * exports,
2032                         const char * optionname,
2033                         const char * values,
2034                         char value)
2035 {
2036    int len;
2037    char * buf;
2038    char * p;
2039    char c;
2040
2041    assert(exports);
2042    assert(optionname);
2043    assert(values);
2044
2045    len = strlen(optionname);
2046    buf = malloc(len + 3);
2047    if (buf == NULL)
2048    {
2049       return JB_ERR_MEMORY;
2050    }
2051
2052    strcpy(buf, optionname);
2053    p = buf + len;
2054    *p++ = '-';
2055    p[1] = '\0';
2056
2057    while ((c = *values++) != '\0')
2058    {
2059       if (c != value)
2060       {
2061          *p = c;
2062          if (map(exports, buf, 1, "", 1))
2063          {
2064             free(buf);
2065             return JB_ERR_MEMORY;
2066          }
2067       }
2068    }
2069
2070    *p = value;
2071    if (map(exports, buf, 0, "checked", 1))
2072    {
2073       free(buf);
2074       return JB_ERR_MEMORY;
2075    }
2076
2077    return JB_ERR_OK;
2078 }
2079
2080
2081 /*********************************************************************
2082  *
2083  * Function    :  actions_to_radio
2084  *
2085  * Description :  Converts a actionsfile entry into settings for
2086  *                radio buttons and edit boxes on a HTML form.
2087  *
2088  * Parameters  :
2089  *          1  :  exports = List of substitutions to add to.
2090  *          2  :  action  = Action to read
2091  *
2092  * Returns     :  JB_ERR_OK     on success
2093  *                JB_ERR_MEMORY on out-of-memory
2094  *
2095  *********************************************************************/
2096 static jb_err actions_to_radio(struct map * exports,
2097                                const struct action_spec *action)
2098 {
2099    unsigned mask = action->mask;
2100    unsigned add  = action->add;
2101    int mapped_param;
2102    int checked;
2103    char current_mode;
2104
2105    assert(exports);
2106    assert(action);
2107
2108    mask = action->mask;
2109    add  = action->add;
2110
2111    /* sanity - prevents "-feature +feature" */
2112    mask |= add;
2113
2114
2115 #define DEFINE_ACTION_BOOL(name, bit)                 \
2116    if (!(mask & bit))                                 \
2117    {                                                  \
2118       current_mode = 'n';                             \
2119    }                                                  \
2120    else if (add & bit)                                \
2121    {                                                  \
2122       current_mode = 'y';                             \
2123    }                                                  \
2124    else                                               \
2125    {                                                  \
2126       current_mode = 'x';                             \
2127    }                                                  \
2128    if (map_radio(exports, name, "ynx", current_mode)) \
2129    {                                                  \
2130       return JB_ERR_MEMORY;                           \
2131    }
2132
2133 #define DEFINE_ACTION_STRING(name, bit, index)        \
2134    DEFINE_ACTION_BOOL(name, bit);                     \
2135    mapped_param = 0;
2136
2137 #define DEFINE_CGI_PARAM_RADIO(name, bit, index, value, is_default)  \
2138    if (add & bit)                                                    \
2139    {                                                                 \
2140       checked = !strcmp(action->string[index], value);               \
2141    }                                                                 \
2142    else                                                              \
2143    {                                                                 \
2144       checked = is_default;                                          \
2145    }                                                                 \
2146    mapped_param |= checked;                                          \
2147    if (map(exports, name "-param-" value, 1, (checked ? "checked" : ""), 1)) \
2148    {                                                                 \
2149       return JB_ERR_MEMORY;                                          \
2150    }
2151
2152 #define DEFINE_CGI_PARAM_CUSTOM(name, bit, index, default_val)       \
2153    if (map(exports, name "-param-custom", 1,                         \
2154            ((!mapped_param) ? "checked" : ""), 1))                   \
2155    {                                                                 \
2156       return JB_ERR_MEMORY;                                          \
2157    }                                                                 \
2158    if (map(exports, name "-param", 1,                                \
2159            (((add & bit) && !mapped_param) ?                         \
2160            action->string[index] : default_val), 1))                 \
2161    {                                                                 \
2162       return JB_ERR_MEMORY;                                          \
2163    }
2164
2165 #define DEFINE_CGI_PARAM_NO_RADIO(name, bit, index, default_val)     \
2166    if (map(exports, name "-param", 1,                                \
2167            ((add & bit) ? action->string[index] : default_val), 1))  \
2168    {                                                                 \
2169       return JB_ERR_MEMORY;                                          \
2170    }
2171
2172 #define DEFINE_ACTION_MULTI(name, index)              \
2173    if (action->multi_add[index]->first)               \
2174    {                                                  \
2175       current_mode = 'y';                             \
2176    }                                                  \
2177    else if (action->multi_remove_all[index])          \
2178    {                                                  \
2179       current_mode = 'n';                             \
2180    }                                                  \
2181    else if (action->multi_remove[index]->first)       \
2182    {                                                  \
2183       current_mode = 'y';                             \
2184    }                                                  \
2185    else                                               \
2186    {                                                  \
2187       current_mode = 'x';                             \
2188    }                                                  \
2189    if (map_radio(exports, name, "ynx", current_mode)) \
2190    {                                                  \
2191       return JB_ERR_MEMORY;                           \
2192    }
2193
2194 #define DEFINE_ACTION_ALIAS 0 /* No aliases for output */
2195
2196 #include "actionlist.h"
2197
2198 #undef DEFINE_ACTION_MULTI
2199 #undef DEFINE_ACTION_STRING
2200 #undef DEFINE_ACTION_BOOL
2201 #undef DEFINE_ACTION_ALIAS
2202 #undef DEFINE_CGI_PARAM_CUSTOM
2203 #undef DEFINE_CGI_PARAM_RADIO
2204 #undef DEFINE_CGI_PARAM_NO_RADIO
2205
2206    return JB_ERR_OK;
2207 }
2208
2209
2210 /*********************************************************************
2211  *
2212  * Function    :  javascriptify
2213  *
2214  * Description :  Converts a string into a form JavaScript will like.
2215  *
2216  *                Netscape 4's JavaScript sucks - it doesn't use 
2217  *                "id" parameters, so you have to set the "name"
2218  *                used to submit a form element to something JavaScript
2219  *                will like.  (Or access the elements by index in an
2220  *                array.  That array contains >60 elements and will
2221  *                be changed whenever we add a new action to the
2222  *                editor, so I'm NOT going to use indexes that have
2223  *                to be figured out by hand.)
2224  *
2225  *                Currently the only thing we have to worry about
2226  *                is "-" ==> "_" conversion.
2227  *
2228  *                This is a length-preserving operation so it is
2229  *                carried out in-place, no memory is allocated
2230  *                or freed.
2231  *
2232  * Parameters  :
2233  *          1  :  identifier = String to make JavaScript-friendly.
2234  *
2235  * Returns     :  N/A
2236  *
2237  *********************************************************************/
2238 static void javascriptify(char * identifier)
2239 {
2240    char * p = identifier;
2241    while (NULL != (p = strchr(p, '-')))
2242    {
2243       *p++ = '_';
2244    }
2245 }
2246
2247
2248 /*********************************************************************
2249  *
2250  * Function    :  actions_from_radio
2251  *
2252  * Description :  Converts a map of parameters passed to a CGI function
2253  *                into an actionsfile entry.
2254  *
2255  * Parameters  :
2256  *          1  :  parameters = parameters to the CGI call
2257  *          2  :  action  = Action to change.  Must be valid before
2258  *                          the call, actions not specified will be
2259  *                          left unchanged.
2260  *
2261  * Returns     :  JB_ERR_OK     on success
2262  *                JB_ERR_MEMORY on out-of-memory
2263  *
2264  *********************************************************************/
2265 static jb_err actions_from_radio(const struct map * parameters,
2266                               struct action_spec *action)
2267 {
2268    static int first_time = 1;
2269    const char * param;
2270    char * param_dup;
2271    char ch;
2272    const char * js_name;
2273
2274    assert(parameters);
2275    assert(action);
2276
2277    /* Statics are generally a potential race condition,
2278     * but in this case we're safe and don't need semaphores.
2279     * Be careful if you modify this function.
2280     * - Jon
2281     */
2282
2283 #define JAVASCRIPTIFY(dest_var, string)               \
2284    {                                                  \
2285       static char js_name_arr[] = string;             \
2286       if (first_time)                                 \
2287       {                                               \
2288          javascriptify(js_name_arr);                  \
2289       }                                               \
2290       dest_var = js_name_arr;                         \
2291    }                                                  \
2292
2293 #define DEFINE_ACTION_BOOL(name, bit)                 \
2294    JAVASCRIPTIFY(js_name, name);                      \
2295    param = lookup(parameters, js_name);               \
2296    ch = ijb_toupper(param[0]);                        \
2297    if (ch == 'Y')                                     \
2298    {                                                  \
2299       action->add  |= bit;                            \
2300       action->mask |= bit;                            \
2301    }                                                  \
2302    else if (ch == 'N')                                \
2303    {                                                  \
2304       action->add  &= ~bit;                           \
2305       action->mask &= ~bit;                           \
2306    }                                                  \
2307    else if (ch == 'X')                                \
2308    {                                                  \
2309       action->add  &= ~bit;                           \
2310       action->mask |= bit;                            \
2311    }                                                  \
2312
2313 #define DEFINE_ACTION_STRING(name, bit, index)                 \
2314    JAVASCRIPTIFY(js_name, name);                               \
2315    param = lookup(parameters, js_name);                        \
2316    ch = ijb_toupper(param[0]);                                 \
2317    if (ch == 'Y')                                              \
2318    {                                                           \
2319       JAVASCRIPTIFY(js_name, name "-mode");                    \
2320       param = lookup(parameters, js_name);                     \
2321       if ((*param == '\0') || (0 == strcmp(param, "CUSTOM")))  \
2322       {                                                        \
2323          JAVASCRIPTIFY(js_name, name "-param");                \
2324          param = lookup(parameters, js_name);                  \
2325       }                                                        \
2326       if (*param != '\0')                                      \
2327       {                                                        \
2328          if (NULL == (param_dup = strdup(param)))              \
2329          {                                                     \
2330             return JB_ERR_MEMORY;                              \
2331          }                                                     \
2332          freez(action->string[index]);                         \
2333          action->add  |= bit;                                  \
2334          action->mask |= bit;                                  \
2335          action->string[index] = param_dup;                    \
2336       }                                                        \
2337    }                                                           \
2338    else if (ch == 'N')                                         \
2339    {                                                           \
2340       if (action->add & bit)                                   \
2341       {                                                        \
2342          freez(action->string[index]);                         \
2343       }                                                        \
2344       action->add  &= ~bit;                                    \
2345       action->mask &= ~bit;                                    \
2346    }                                                           \
2347    else if (ch == 'X')                                         \
2348    {                                                           \
2349       if (action->add & bit)                                   \
2350       {                                                        \
2351          freez(action->string[index]);                         \
2352       }                                                        \
2353       action->add  &= ~bit;                                    \
2354       action->mask |= bit;                                     \
2355    }                                                           \
2356
2357 #define DEFINE_ACTION_MULTI(name, index)                       \
2358    JAVASCRIPTIFY(js_name, name);                               \
2359    param = lookup(parameters, js_name);                        \
2360    ch = ijb_toupper((int)param[0]);                            \
2361    if (ch == 'Y')                                              \
2362    {                                                           \
2363       /* FIXME */                                              \
2364    }                                                           \
2365    else if (ch == 'N')                                         \
2366    {                                                           \
2367       list_remove_all(action->multi_add[index]);               \
2368       list_remove_all(action->multi_remove[index]);            \
2369       action->multi_remove_all[index] = 1;                     \
2370    }                                                           \
2371    else if (ch == 'X')                                         \
2372    {                                                           \
2373       list_remove_all(action->multi_add[index]);               \
2374       list_remove_all(action->multi_remove[index]);            \
2375       action->multi_remove_all[index] = 0;                     \
2376    }                                                           \
2377
2378 #define DEFINE_ACTION_ALIAS 0 /* No aliases for URL parsing */
2379
2380 #include "actionlist.h"
2381
2382 #undef DEFINE_ACTION_MULTI
2383 #undef DEFINE_ACTION_STRING
2384 #undef DEFINE_ACTION_BOOL
2385 #undef DEFINE_ACTION_ALIAS
2386 #undef JAVASCRIPTIFY
2387
2388    first_time = 0;
2389
2390    return JB_ERR_OK;
2391 }
2392
2393
2394 /*********************************************************************
2395  *
2396  * Function    :  cgi_error_modified
2397  *
2398  * Description :  CGI function that is called when a file is modified
2399  *                outside the CGI editor.
2400  *
2401  * Parameters  :
2402  *           1 :  csp = Current client state (buffers, headers, etc...)
2403  *           2 :  rsp = http_response data structure for output
2404  *           3 :  filename = The file that was modified.
2405  *
2406  * CGI Parameters : none
2407  *
2408  * Returns     :  JB_ERR_OK on success
2409  *                JB_ERR_MEMORY on out-of-memory error.
2410  *
2411  *********************************************************************/
2412 jb_err cgi_error_modified(struct client_state *csp,
2413                           struct http_response *rsp,
2414                           const char *filename)
2415 {
2416    struct map *exports;
2417    jb_err err;
2418
2419    assert(csp);
2420    assert(rsp);
2421    assert(filename);
2422
2423    if (NULL == (exports = default_exports(csp, NULL)))
2424    {
2425       return JB_ERR_MEMORY;
2426    }
2427
2428    err = map(exports, "filename", 1, filename, 1);
2429    if (err)
2430    {
2431       free_map(exports);
2432       return err;
2433    }
2434
2435    return template_fill_for_cgi(csp, "cgi-error-modified", exports, rsp);
2436 }
2437
2438
2439 /*********************************************************************
2440  *
2441  * Function    :  cgi_error_parse
2442  *
2443  * Description :  CGI function that is called when a file cannot
2444  *                be parsed by the CGI editor.
2445  *
2446  * Parameters  :
2447  *           1 :  csp = Current client state (buffers, headers, etc...)
2448  *           2 :  rsp = http_response data structure for output
2449  *           3 :  file = The file that was modified.
2450  *
2451  * CGI Parameters : none
2452  *
2453  * Returns     :  JB_ERR_OK on success
2454  *                JB_ERR_MEMORY on out-of-memory error.
2455  *
2456  *********************************************************************/
2457 jb_err cgi_error_parse(struct client_state *csp,
2458                        struct http_response *rsp,
2459                        struct editable_file *file)
2460 {
2461    struct map *exports;
2462    jb_err err;
2463    struct file_line *cur_line;
2464
2465    assert(csp);
2466    assert(rsp);
2467    assert(file);
2468
2469    if (NULL == (exports = default_exports(csp, NULL)))
2470    {
2471       return JB_ERR_MEMORY;
2472    }
2473
2474    err = map(exports, "filename", 1, file->identifier, 1);
2475    if (!err) err = map(exports, "parse-error", 1, file->parse_error_text, 1);
2476
2477    cur_line = file->parse_error;
2478    assert(cur_line);
2479
2480    if (!err) err = map(exports, "line-raw", 1, html_encode(cur_line->raw), 0);
2481    if (!err) err = map(exports, "line-data", 1, html_encode(cur_line->unprocessed), 0);
2482
2483    if (err)
2484    {
2485       free_map(exports);
2486       return err;
2487    }
2488
2489    return template_fill_for_cgi(csp, "cgi-error-parse", exports, rsp);
2490 }
2491
2492
2493 /*********************************************************************
2494  *
2495  * Function    :  cgi_error_file
2496  *
2497  * Description :  CGI function that is called when a file cannot be
2498  *                opened by the CGI editor.
2499  *
2500  * Parameters  :
2501  *           1 :  csp = Current client state (buffers, headers, etc...)
2502  *           2 :  rsp = http_response data structure for output
2503  *           3 :  filename = The file that was modified.
2504  *
2505  * CGI Parameters : none
2506  *
2507  * Returns     :  JB_ERR_OK on success
2508  *                JB_ERR_MEMORY on out-of-memory error.
2509  *
2510  *********************************************************************/
2511 jb_err cgi_error_file(struct client_state *csp,
2512                       struct http_response *rsp,
2513                       const char *filename)
2514 {
2515    struct map *exports;
2516    jb_err err;
2517
2518    assert(csp);
2519    assert(rsp);
2520    assert(filename);
2521
2522    if (NULL == (exports = default_exports(csp, NULL)))
2523    {
2524       return JB_ERR_MEMORY;
2525    }
2526
2527    err = map(exports, "filename", 1, filename, 1);
2528    if (err)
2529    {
2530       free_map(exports);
2531       return err;
2532    }
2533
2534    return template_fill_for_cgi(csp, "cgi-error-file", exports, rsp);
2535 }
2536
2537
2538 /*********************************************************************
2539  *
2540  * Function    :  cgi_error_bad_param
2541  *
2542  * Description :  CGI function that is called if the parameters
2543  *                (query string) for a CGI were wrong.
2544  *
2545  * Parameters  :
2546  *           1 :  csp = Current client state (buffers, headers, etc...)
2547  *           2 :  rsp = http_response data structure for output
2548  *
2549  * CGI Parameters : none
2550  *
2551  * Returns     :  JB_ERR_OK on success
2552  *                JB_ERR_MEMORY on out-of-memory error.
2553  *
2554  *********************************************************************/
2555 jb_err cgi_error_disabled(struct client_state *csp,
2556                           struct http_response *rsp)
2557 {
2558    struct map *exports;
2559
2560    assert(csp);
2561    assert(rsp);
2562
2563    if (NULL == (exports = default_exports(csp, NULL)))
2564    {
2565       return JB_ERR_MEMORY;
2566    }
2567
2568    return template_fill_for_cgi(csp, "cgi-error-disabled", exports, rsp);
2569 }
2570
2571
2572 /*********************************************************************
2573  *
2574  * Function    :  cgi_edit_actions
2575  *
2576  * Description :  CGI function that allows the user to choose which
2577  *                actions file to edit.
2578  *
2579  * Parameters  :
2580  *           1 :  csp = Current client state (buffers, headers, etc...)
2581  *           2 :  rsp = http_response data structure for output
2582  *           3 :  parameters = map of cgi parameters
2583  *
2584  * CGI Parameters : None
2585  *
2586  * Returns     :  JB_ERR_OK on success
2587  *                JB_ERR_MEMORY on out-of-memory error
2588  *
2589  *********************************************************************/
2590 jb_err cgi_edit_actions(struct client_state *csp,
2591                         struct http_response *rsp,
2592                         const struct map *parameters)
2593 {
2594
2595    if (0 == (csp->config->feature_flags & RUNTIME_FEATURE_CGI_EDIT_ACTIONS))
2596    {
2597       return cgi_error_disabled(csp, rsp);
2598    }
2599
2600    /* FIXME: Incomplete */
2601    rsp->status = strdup("302 Local Redirect from Junkbuster");
2602    if (rsp->status == NULL)
2603    {
2604       return JB_ERR_MEMORY;
2605    }
2606    if (enlist_unique_header(rsp->headers, "Location",
2607       CGI_PREFIX "edit-actions-list?filename=ijb"))
2608    {
2609       free(rsp->status);
2610       rsp->status = NULL;
2611       return JB_ERR_MEMORY;
2612    }
2613
2614    return JB_ERR_OK;
2615 }
2616
2617
2618 /*********************************************************************
2619  *
2620  * Function    :  cgi_edit_actions_list
2621  *
2622  * Description :  CGI function that edits the actions list.
2623  *                FIXME: This function shouldn't FATAL ever.
2624  *                FIXME: This function doesn't check the retval of map()
2625  * Parameters  :
2626  *           1 :  csp = Current client state (buffers, headers, etc...)
2627  *           2 :  rsp = http_response data structure for output
2628  *           3 :  parameters = map of cgi parameters
2629  *
2630  * CGI Parameters : filename
2631  *
2632  * Returns     :  JB_ERR_OK     on success
2633  *                JB_ERR_MEMORY on out-of-memory
2634  *                JB_ERR_FILE   if the file cannot be opened or
2635  *                              contains no data
2636  *                JB_ERR_CGI_PARAMS if "filename" was not specified
2637  *                                  or is not valid.
2638  *
2639  *********************************************************************/
2640 jb_err cgi_edit_actions_list(struct client_state *csp,
2641                              struct http_response *rsp,
2642                              const struct map *parameters)
2643 {
2644    char * section_template;
2645    char * url_template;
2646    char * sections;
2647    char * urls;
2648    char buf[50];
2649    char * s;
2650    struct map * exports;
2651    struct map * section_exports;
2652    struct map * url_exports;
2653    struct editable_file * file;
2654    struct file_line * cur_line;
2655    unsigned line_number = 0;
2656    int url_1_2;
2657    jb_err err;
2658
2659    if (0 == (csp->config->feature_flags & RUNTIME_FEATURE_CGI_EDIT_ACTIONS))
2660    {
2661       return cgi_error_disabled(csp, rsp);
2662    }
2663
2664    err = edit_read_actions_file(csp, rsp, parameters, 0, &file);
2665    if (err)
2666    {
2667       /* No filename specified, can't read file, or out of memory. */
2668       return (err == JB_ERR_FILE ? JB_ERR_OK : err);
2669    }
2670
2671    if (NULL == (exports = default_exports(csp, NULL)))
2672    {
2673       edit_free_file(file);
2674       return JB_ERR_MEMORY;
2675    }
2676
2677    err = map(exports, "filename", 1, file->identifier, 1);
2678    if (!err) err = map(exports, "ver", 1, file->version_str, 1);
2679
2680    if (err)
2681    {
2682       edit_free_file(file);
2683       free_map(exports);
2684       return err;
2685    }
2686
2687    /* Should do all global exports above this point */
2688
2689    err = template_load(csp, &section_template, "edit-actions-list-section");
2690    if (err)
2691    {
2692       edit_free_file(file);
2693       free_map(exports);
2694       if (err == JB_ERR_FILE)
2695       {
2696          return cgi_error_no_template(csp, rsp, "edit-actions-list-section");
2697       }
2698       return err;
2699    }
2700
2701    err = template_load(csp, &url_template, "edit-actions-list-url");
2702    if (err)
2703    {
2704       free(section_template);
2705       edit_free_file(file);
2706       free_map(exports);
2707       if (err == JB_ERR_FILE)
2708       {
2709          return cgi_error_no_template(csp, rsp, "edit-actions-list-url");
2710       }
2711       return err;
2712    }
2713
2714    err = template_fill(&section_template, exports);
2715    if (err)
2716    {
2717       free(url_template);
2718       edit_free_file(file);
2719       free_map(exports);
2720       free(url_template);
2721       return err;
2722    }
2723
2724    err = template_fill(&url_template, exports);
2725    if (err)
2726    {
2727       free(section_template);
2728       edit_free_file(file);
2729       free_map(exports);
2730       return err;
2731    }
2732
2733    /* Find start of actions in file */
2734    cur_line = file->lines;
2735    line_number = 1;
2736    while ((cur_line != NULL) && (cur_line->type != FILE_LINE_ACTION))
2737    {
2738       cur_line = cur_line->next;
2739       line_number++;
2740    }
2741
2742    if (NULL == (sections = strdup("")))
2743    {
2744       free(section_template);
2745       free(url_template);
2746       edit_free_file(file);
2747       free_map(exports);
2748       return JB_ERR_MEMORY;
2749    }
2750
2751    while ((cur_line != NULL) && (cur_line->type == FILE_LINE_ACTION))
2752    {
2753       if (NULL == (section_exports = new_map()))
2754       {
2755          free(sections);
2756          free(section_template);
2757          free(url_template);
2758          edit_free_file(file);
2759          free_map(exports);
2760          return JB_ERR_MEMORY;
2761       }
2762
2763       snprintf(buf, 50, "%d", line_number);
2764       err = map(section_exports, "sectionid", 1, buf, 1);
2765       if (!err) err = map(section_exports, "actions", 1,
2766                           actions_to_html(cur_line->data.action), 0);
2767
2768       if ( (!err)
2769         && (cur_line->next != NULL)
2770         && (cur_line->next->type == FILE_LINE_URL))
2771       {
2772          /* This section contains at least one URL, don't allow delete */
2773          err = map_block_killer(section_exports, "empty-section");
2774       }
2775
2776       if (err)
2777       {
2778          free(sections);
2779          free(section_template);
2780          free(url_template);
2781          edit_free_file(file);
2782          free_map(exports);
2783          free_map(section_exports);
2784          return err;
2785       }
2786
2787       /* Should do all section-specific exports above this point */
2788
2789       if (NULL == (urls = strdup("")))
2790       {
2791          free(sections);
2792          free(section_template);
2793          free(url_template);
2794          edit_free_file(file);
2795          free_map(exports);
2796          free_map(section_exports);
2797          return JB_ERR_MEMORY;
2798       }
2799
2800       url_1_2 = 2;
2801
2802       cur_line = cur_line->next;
2803       line_number++;
2804
2805       while ((cur_line != NULL) && (cur_line->type == FILE_LINE_URL))
2806       {
2807          if (NULL == (url_exports = new_map()))
2808          {
2809             free(urls);
2810             free(sections);
2811             free(section_template);
2812             free(url_template);
2813             edit_free_file(file);
2814             free_map(exports);
2815             free_map(section_exports);
2816             return JB_ERR_MEMORY;
2817          }
2818
2819          snprintf(buf, 50, "%d", line_number);
2820          err = map(url_exports, "urlid", 1, buf, 1);
2821
2822          snprintf(buf, 50, "%d", url_1_2);
2823          if (!err) err = map(url_exports, "url-1-2", 1, buf, 1);
2824
2825          if (!err) err = map(url_exports, "url-html", 1,
2826                              html_encode(cur_line->unprocessed), 0);
2827          if (!err) err = map(url_exports, "url", 1,
2828                              url_encode(cur_line->unprocessed), 0);
2829
2830          if (err)
2831          {
2832             free(urls);
2833             free(sections);
2834             free(section_template);
2835             free(url_template);
2836             edit_free_file(file);
2837             free_map(exports);
2838             free_map(section_exports);
2839             free_map(url_exports);
2840             return err;
2841          }
2842
2843          if (NULL == (s = strdup(url_template)))
2844          {
2845             free(urls);
2846             free(sections);
2847             free(section_template);
2848             free(url_template);
2849             edit_free_file(file);
2850             free_map(exports);
2851             free_map(section_exports);
2852             free_map(url_exports);
2853             return JB_ERR_MEMORY;
2854          }
2855
2856          err = template_fill(&s, section_exports);
2857          if (!err) err = template_fill(&s, url_exports);
2858          if (!err) err = string_append(&urls, s);
2859
2860          free_map(url_exports);
2861          freez(s);
2862
2863          if (err)
2864          {
2865             freez(urls);
2866             free(sections);
2867             free(section_template);
2868             free(url_template);
2869             edit_free_file(file);
2870             free_map(exports);
2871             free_map(section_exports);
2872             return err;
2873          }
2874
2875          url_1_2 = 3 - url_1_2;
2876
2877          cur_line = cur_line->next;
2878          line_number++;
2879       }
2880
2881       err = map(section_exports, "urls", 1, urls, 0);
2882
2883       if (err)
2884       {
2885          free(sections);
2886          free(section_template);
2887          free(url_template);
2888          edit_free_file(file);
2889          free_map(exports);
2890          free_map(section_exports);
2891          return err;
2892       }
2893
2894       /* Could also do section-specific exports here, but it wouldn't be as fast */
2895
2896       if (NULL == (s = strdup(section_template)))
2897       {
2898          free(sections);
2899          free(section_template);
2900          free(url_template);
2901          edit_free_file(file);
2902          free_map(exports);
2903          free_map(section_exports);
2904          return JB_ERR_MEMORY;
2905       }
2906
2907       err = template_fill(&s, section_exports);
2908       if (!err) err = string_append(&sections, s);
2909
2910       freez(s);
2911       free_map(section_exports);
2912
2913       if (err)
2914       {
2915          freez(sections);
2916          free(section_template);
2917          free(url_template);
2918          edit_free_file(file);
2919          free_map(exports);
2920          return err;
2921       }
2922    }
2923
2924    edit_free_file(file);
2925    free(section_template);
2926    free(url_template);
2927
2928    err = map(exports, "sections", 1, sections, 0);
2929    if (err)
2930    {
2931       free_map(exports);
2932       return err;
2933    }
2934
2935    /* Could also do global exports here, but it wouldn't be as fast */
2936
2937    return template_fill_for_cgi(csp, "edit-actions-list", exports, rsp);
2938 }
2939
2940
2941 /*********************************************************************
2942  *
2943  * Function    :  cgi_edit_actions
2944  *
2945  * Description :  CGI function that edits the Actions list.
2946  *
2947  * Parameters  :
2948  *           1 :  csp = Current client state (buffers, headers, etc...)
2949  *           2 :  rsp = http_response data structure for output
2950  *           3 :  parameters = map of cgi parameters
2951  *
2952  * CGI Parameters : None
2953  *
2954  * Returns     :  JB_ERR_OK     on success
2955  *                JB_ERR_MEMORY on out-of-memory
2956  *                JB_ERR_CGI_PARAMS if the CGI parameters are not
2957  *                                  specified or not valid.
2958  *
2959  *********************************************************************/
2960 jb_err cgi_edit_actions_for_url(struct client_state *csp,
2961                                 struct http_response *rsp,
2962                                 const struct map *parameters)
2963 {
2964    struct map * exports;
2965    unsigned sectionid;
2966    struct editable_file * file;
2967    struct file_line * cur_line;
2968    unsigned line_number;
2969    jb_err err;
2970
2971    if (0 == (csp->config->feature_flags & RUNTIME_FEATURE_CGI_EDIT_ACTIONS))
2972    {
2973       return cgi_error_disabled(csp, rsp);
2974    }
2975
2976    err = get_number_param(csp, parameters, "section", &sectionid);
2977    if (err)
2978    {
2979       return err;
2980    }
2981
2982    err = edit_read_actions_file(csp, rsp, parameters, 1, &file);
2983    if (err)
2984    {
2985       /* No filename specified, can't read file, modified, or out of memory. */
2986       return (err == JB_ERR_FILE ? JB_ERR_OK : err);
2987    }
2988
2989    cur_line = file->lines;
2990
2991    for (line_number = 1; (cur_line != NULL) && (line_number < sectionid); line_number++)
2992    {
2993       cur_line = cur_line->next;
2994    }
2995
2996    if ( (cur_line == NULL)
2997      || (line_number != sectionid)
2998      || (sectionid < 1)
2999      || (cur_line->type != FILE_LINE_ACTION))
3000    {
3001       /* Invalid "sectionid" parameter */
3002       edit_free_file(file);
3003       return JB_ERR_CGI_PARAMS;
3004    }
3005
3006    if (NULL == (exports = default_exports(csp, NULL)))
3007    {
3008       edit_free_file(file);
3009       return JB_ERR_MEMORY;
3010    }
3011
3012    err = map(exports, "filename", 1, file->identifier, 1);
3013    if (!err) err = map(exports, "ver", 1, file->version_str, 1);
3014    if (!err) err = map(exports, "section", 1, lookup(parameters, "section"), 1);
3015
3016    if (!err) err = actions_to_radio(exports, cur_line->data.action);
3017
3018    edit_free_file(file);
3019
3020    if (err)
3021    {
3022       free_map(exports);
3023       return err;
3024    }
3025
3026    return template_fill_for_cgi(csp, "edit-actions-for-url", exports, rsp);
3027 }
3028
3029
3030 /*********************************************************************
3031  *
3032  * Function    :  cgi_edit_actions_submit
3033  *
3034  * Description :  CGI function that actually edits the Actions list.
3035  *
3036  * Parameters  :
3037  *           1 :  csp = Current client state (buffers, headers, etc...)
3038  *           2 :  rsp = http_response data structure for output
3039  *           3 :  parameters = map of cgi parameters
3040  *
3041  * CGI Parameters : None
3042  *
3043  * Returns     :  JB_ERR_OK     on success
3044  *                JB_ERR_MEMORY on out-of-memory
3045  *                JB_ERR_CGI_PARAMS if the CGI parameters are not
3046  *                                  specified or not valid.
3047  *
3048  *********************************************************************/
3049 jb_err cgi_edit_actions_submit(struct client_state *csp,
3050                                struct http_response *rsp,
3051                                const struct map *parameters)
3052 {
3053    unsigned sectionid;
3054    char * actiontext;
3055    char * newtext;
3056    int len;
3057    struct editable_file * file;
3058    struct file_line * cur_line;
3059    unsigned line_number;
3060    char * target;
3061    jb_err err;
3062
3063    if (0 == (csp->config->feature_flags & RUNTIME_FEATURE_CGI_EDIT_ACTIONS))
3064    {
3065       return cgi_error_disabled(csp, rsp);
3066    }
3067
3068    err = get_number_param(csp, parameters, "section", &sectionid);
3069    if (err)
3070    {
3071       return err;
3072    }
3073
3074    err = edit_read_actions_file(csp, rsp, parameters, 1, &file);
3075    if (err)
3076    {
3077       /* No filename specified, can't read file, modified, or out of memory. */
3078       return (err == JB_ERR_FILE ? JB_ERR_OK : err);
3079    }
3080
3081    cur_line = file->lines;
3082
3083    for (line_number = 1; (cur_line != NULL) && (line_number < sectionid); line_number++)
3084    {
3085       cur_line = cur_line->next;
3086    }
3087
3088    if ( (cur_line == NULL)
3089      || (line_number != sectionid)
3090      || (sectionid < 1)
3091      || (cur_line->type != FILE_LINE_ACTION))
3092    {
3093       /* Invalid "sectionid" parameter */
3094       edit_free_file(file);
3095       return JB_ERR_CGI_PARAMS;
3096    }
3097
3098    err = actions_from_radio(parameters, cur_line->data.action);
3099    if(err)
3100    {
3101       /* Out of memory */
3102       edit_free_file(file);
3103       return err;
3104    }
3105
3106    if (NULL == (actiontext = actions_to_text(cur_line->data.action)))
3107    {
3108       /* Out of memory */
3109       edit_free_file(file);
3110       return JB_ERR_MEMORY;
3111    }
3112
3113    len = strlen(actiontext);
3114    if (len == 0)
3115    {
3116       /*
3117        * Empty action - must special-case this.
3118        * Simply setting len to 1 is sufficient...
3119        */
3120       len = 1;
3121    }
3122
3123    if (NULL == (newtext = malloc(len + 2)))
3124    {
3125       /* Out of memory */
3126       free(actiontext);
3127       edit_free_file(file);
3128       return JB_ERR_MEMORY;
3129    }
3130    strcpy(newtext, actiontext);
3131    free(actiontext);
3132    newtext[0]       = '{';
3133    newtext[len]     = '}';
3134    newtext[len + 1] = '\0';
3135
3136    freez(cur_line->raw);
3137    freez(cur_line->unprocessed);
3138    cur_line->unprocessed = newtext;
3139
3140    err = edit_write_file(file);
3141    if (err)
3142    {
3143       /* Error writing file */
3144       edit_free_file(file);
3145       return err;
3146    }
3147
3148    target = strdup(CGI_PREFIX "edit-actions-list?filename=");
3149    string_append(&target, file->identifier);
3150
3151    edit_free_file(file);
3152
3153    if (target == NULL)
3154    {
3155       /* Out of memory */
3156       return JB_ERR_MEMORY;
3157    }
3158
3159    rsp->status = strdup("302 Local Redirect from Junkbuster");
3160    if (rsp->status == NULL)
3161    {
3162       free(target);
3163       return JB_ERR_MEMORY;
3164    }
3165    err = enlist_unique_header(rsp->headers, "Location", target);
3166    free(target);
3167
3168    return err;
3169 }
3170
3171
3172 /*********************************************************************
3173  *
3174  * Function    :  cgi_edit_actions_url
3175  *
3176  * Description :  CGI function that actually edits a URL pattern in
3177  *                an actions file.
3178  *
3179  * Parameters  :
3180  *           1 :  csp = Current client state (buffers, headers, etc...)
3181  *           2 :  rsp = http_response data structure for output
3182  *           3 :  parameters = map of cgi parameters
3183  *
3184  * CGI Parameters :
3185  *    filename : Identifies the file to edit
3186  *         ver : File's last-modified time
3187  *     section : Line number of section to edit
3188  *     pattern : Line number of pattern to edit
3189  *      newval : New value for pattern
3190  *
3191  * Returns     :  JB_ERR_OK     on success
3192  *                JB_ERR_MEMORY on out-of-memory
3193  *                JB_ERR_CGI_PARAMS if the CGI parameters are not
3194  *                                  specified or not valid.
3195  *
3196  *********************************************************************/
3197 jb_err cgi_edit_actions_url(struct client_state *csp,
3198                             struct http_response *rsp,
3199                             const struct map *parameters)
3200 {
3201    unsigned sectionid;
3202    unsigned patternid;
3203    const char * newval;
3204    char * new_pattern;
3205    struct editable_file * file;
3206    struct file_line * cur_line;
3207    unsigned line_number;
3208    char * target;
3209    jb_err err;
3210
3211    if (0 == (csp->config->feature_flags & RUNTIME_FEATURE_CGI_EDIT_ACTIONS))
3212    {
3213       return cgi_error_disabled(csp, rsp);
3214    }
3215
3216    err = get_number_param(csp, parameters, "section", &sectionid);
3217    if (err)
3218    {
3219       return err;
3220    }
3221
3222    err = get_number_param(csp, parameters, "pattern", &patternid);
3223    if (err)
3224    {
3225       return err;
3226    }
3227
3228    newval = lookup(parameters, "newval");
3229
3230    if ((*newval == '\0') || (sectionid < 1U) || (patternid < 1U))
3231    {
3232       return JB_ERR_CGI_PARAMS;
3233    }
3234
3235    err = edit_read_actions_file(csp, rsp, parameters, 1, &file);
3236    if (err)
3237    {
3238       /* No filename specified, can't read file, modified, or out of memory. */
3239       return (err == JB_ERR_FILE ? JB_ERR_OK : err);
3240    }
3241
3242    line_number = 1;
3243    cur_line = file->lines;
3244
3245    while ((cur_line != NULL) && (line_number < sectionid))
3246    {
3247       cur_line = cur_line->next;
3248       line_number++;
3249    }
3250
3251    if ( (cur_line == NULL)
3252      || (cur_line->type != FILE_LINE_ACTION))
3253    {
3254       /* Invalid "sectionid" parameter */
3255       edit_free_file(file);
3256       return JB_ERR_CGI_PARAMS;
3257    }
3258
3259    while (line_number < patternid)
3260    {
3261       cur_line = cur_line->next;
3262       line_number++;
3263
3264       if ( (cur_line == NULL)
3265         || ( (cur_line->type != FILE_LINE_URL)
3266           && (cur_line->type != FILE_LINE_BLANK) ) )
3267       {
3268          /* Invalid "patternid" parameter */
3269          edit_free_file(file);
3270          return JB_ERR_CGI_PARAMS;
3271       }
3272    }
3273
3274    if (cur_line->type != FILE_LINE_URL)
3275    {
3276       /* Invalid "patternid" parameter */
3277       edit_free_file(file);
3278       return JB_ERR_CGI_PARAMS;
3279    }
3280
3281    /* At this point, the line to edit is in cur_line */
3282
3283    new_pattern = strdup(newval);
3284    if (NULL == new_pattern)
3285    {
3286       edit_free_file(file);
3287       return JB_ERR_MEMORY;
3288    }
3289
3290    freez(cur_line->raw);
3291    freez(cur_line->unprocessed);
3292    cur_line->unprocessed = new_pattern;
3293
3294    err = edit_write_file(file);
3295    if (err)
3296    {
3297       /* Error writing file */
3298       edit_free_file(file);
3299       return err;
3300    }
3301
3302    target = strdup(CGI_PREFIX "edit-actions-list?filename=");
3303    string_append(&target, file->identifier);
3304
3305    edit_free_file(file);
3306
3307    if (target == NULL)
3308    {
3309       /* Out of memory */
3310       return JB_ERR_MEMORY;
3311    }
3312
3313    rsp->status = strdup("302 Local Redirect from Junkbuster");
3314    if (rsp->status == NULL)
3315    {
3316       free(target);
3317       return JB_ERR_MEMORY;
3318    }
3319    err = enlist_unique_header(rsp->headers, "Location", target);
3320    free(target);
3321
3322    return err;
3323 }
3324
3325
3326 /*********************************************************************
3327  *
3328  * Function    :  cgi_edit_actions_add_url
3329  *
3330  * Description :  CGI function that actually adds a URL pattern to
3331  *                an actions file.
3332  *
3333  * Parameters  :
3334  *           1 :  csp = Current client state (buffers, headers, etc...)
3335  *           2 :  rsp = http_response data structure for output
3336  *           3 :  parameters = map of cgi parameters
3337  *
3338  * CGI Parameters :
3339  *    filename : Identifies the file to edit
3340  *         ver : File's last-modified time
3341  *     section : Line number of section to edit
3342  *      newval : New pattern
3343  *
3344  * Returns     :  JB_ERR_OK     on success
3345  *                JB_ERR_MEMORY on out-of-memory
3346  *                JB_ERR_CGI_PARAMS if the CGI parameters are not
3347  *                                  specified or not valid.
3348  *
3349  *********************************************************************/
3350 jb_err cgi_edit_actions_add_url(struct client_state *csp,
3351                                 struct http_response *rsp,
3352                                 const struct map *parameters)
3353 {
3354    unsigned sectionid;
3355    const char * newval;
3356    char * new_pattern;
3357    struct file_line * new_line;
3358    struct editable_file * file;
3359    struct file_line * cur_line;
3360    unsigned line_number;
3361    char * target;
3362    jb_err err;
3363    struct url_spec compiled[1];
3364
3365    if (0 == (csp->config->feature_flags & RUNTIME_FEATURE_CGI_EDIT_ACTIONS))
3366    {
3367       return cgi_error_disabled(csp, rsp);
3368    }
3369
3370    err = get_number_param(csp, parameters, "section", &sectionid);
3371    if (err)
3372    {
3373       return err;
3374    }
3375
3376    newval = lookup(parameters, "newval");
3377
3378    if ((*newval == '\0') || (sectionid < 1U))
3379    {
3380       return JB_ERR_CGI_PARAMS;
3381    }
3382
3383    /* Check that regex is valid */
3384    err = create_url_spec(compiled, newval);
3385    if (err)
3386    {
3387       return (err == JB_ERR_MEMORY) ? JB_ERR_MEMORY : JB_ERR_CGI_PARAMS;
3388    }
3389    free_url_spec(compiled);
3390
3391    err = edit_read_actions_file(csp, rsp, parameters, 1, &file);
3392    if (err)
3393    {
3394       /* No filename specified, can't read file, modified, or out of memory. */
3395       return (err == JB_ERR_FILE ? JB_ERR_OK : err);
3396    }
3397
3398    line_number = 1;
3399    cur_line = file->lines;
3400
3401    while ((cur_line != NULL) && (line_number < sectionid))
3402    {
3403       cur_line = cur_line->next;
3404       line_number++;
3405    }
3406
3407    if ( (cur_line == NULL)
3408      || (cur_line->type != FILE_LINE_ACTION))
3409    {
3410       /* Invalid "sectionid" parameter */
3411       edit_free_file(file);
3412       return JB_ERR_CGI_PARAMS;
3413    }
3414
3415    /* At this point, the section header is in cur_line - add after this. */
3416
3417    new_pattern = strdup(newval);
3418    if (NULL == new_pattern)
3419    {
3420       edit_free_file(file);
3421       return JB_ERR_MEMORY;
3422    }
3423
3424    /* Allocate the new line */
3425    new_line = (struct file_line *)zalloc(sizeof(*new_line));
3426    if (new_line == NULL)
3427    {
3428       free(new_pattern);
3429       edit_free_file(file);
3430       return JB_ERR_MEMORY;
3431    }
3432
3433    /* Fill in the data members of the new line */
3434    new_line->raw = NULL;
3435    new_line->prefix = NULL;
3436    new_line->unprocessed = new_pattern;
3437    new_line->type = FILE_LINE_URL;
3438
3439    /* Link new_line into the list, after cur_line */
3440    new_line->next = cur_line->next;
3441    cur_line->next = new_line;
3442
3443    /* Done making changes, now commit */
3444
3445    err = edit_write_file(file);
3446    if (err)
3447    {
3448       /* Error writing file */
3449       edit_free_file(file);
3450       return err;
3451    }
3452
3453    target = strdup(CGI_PREFIX "edit-actions-list?filename=");
3454    string_append(&target, file->identifier);
3455
3456    edit_free_file(file);
3457
3458    if (target == NULL)
3459    {
3460       /* Out of memory */
3461       return JB_ERR_MEMORY;
3462    }
3463
3464    rsp->status = strdup("302 Local Redirect from Junkbuster");
3465    if (rsp->status == NULL)
3466    {
3467       free(target);
3468       return JB_ERR_MEMORY;
3469    }
3470    err = enlist_unique_header(rsp->headers, "Location", target);
3471    free(target);
3472
3473    return err;
3474 }
3475
3476
3477 /*********************************************************************
3478  *
3479  * Function    :  cgi_edit_actions_remove_url
3480  *
3481  * Description :  CGI function that actually removes a URL pattern from
3482  *                the actions file.
3483  *
3484  * Parameters  :
3485  *           1 :  csp = Current client state (buffers, headers, etc...)
3486  *           2 :  rsp = http_response data structure for output
3487  *           3 :  parameters = map of cgi parameters
3488  *
3489  * CGI Parameters :
3490  *    filename : Identifies the file to edit
3491  *         ver : File's last-modified time
3492  *     section : Line number of section to edit
3493  *     pattern : Line number of pattern to edit
3494  *
3495  * Returns     :  JB_ERR_OK     on success
3496  *                JB_ERR_MEMORY on out-of-memory
3497  *                JB_ERR_CGI_PARAMS if the CGI parameters are not
3498  *                                  specified or not valid.
3499  *
3500  *********************************************************************/
3501 jb_err cgi_edit_actions_remove_url(struct client_state *csp,
3502                                    struct http_response *rsp,
3503                                    const struct map *parameters)
3504 {
3505    unsigned sectionid;
3506    unsigned patternid;
3507    struct editable_file * file;
3508    struct file_line * cur_line;
3509    struct file_line * prev_line;
3510    unsigned line_number;
3511    char * target;
3512    jb_err err;
3513
3514    if (0 == (csp->config->feature_flags & RUNTIME_FEATURE_CGI_EDIT_ACTIONS))
3515    {
3516       return cgi_error_disabled(csp, rsp);
3517    }
3518
3519    err = get_number_param(csp, parameters, "section", &sectionid);
3520    if (err)
3521    {
3522       return err;
3523    }
3524
3525    err = get_number_param(csp, parameters, "pattern", &patternid);
3526    if (err)
3527    {
3528       return err;
3529    }
3530
3531
3532    err = edit_read_actions_file(csp, rsp, parameters, 1, &file);
3533    if (err)
3534    {
3535       /* No filename specified, can't read file, modified, or out of memory. */
3536       return (err == JB_ERR_FILE ? JB_ERR_OK : err);
3537    }
3538
3539    line_number = 1;
3540    cur_line = file->lines;
3541
3542    while ((cur_line != NULL) && (line_number < sectionid))
3543    {
3544       cur_line = cur_line->next;
3545       line_number++;
3546    }
3547
3548    if ( (cur_line == NULL)
3549      || (cur_line->type != FILE_LINE_ACTION))
3550    {
3551       /* Invalid "sectionid" parameter */
3552       edit_free_file(file);
3553       return JB_ERR_CGI_PARAMS;
3554    }
3555
3556    prev_line = NULL;
3557    while (line_number < patternid)
3558    {
3559       prev_line = cur_line;
3560       cur_line = cur_line->next;
3561       line_number++;
3562
3563       if ( (cur_line == NULL)
3564         || ( (cur_line->type != FILE_LINE_URL)
3565           && (cur_line->type != FILE_LINE_BLANK) ) )
3566       {
3567          /* Invalid "patternid" parameter */
3568          edit_free_file(file);
3569          return JB_ERR_CGI_PARAMS;
3570       }
3571    }
3572
3573    if (cur_line->type != FILE_LINE_URL)
3574    {
3575       /* Invalid "patternid" parameter */
3576       edit_free_file(file);
3577       return JB_ERR_CGI_PARAMS;
3578    }
3579
3580    assert(prev_line);
3581
3582    /* At this point, the line to remove is in cur_line, and the previous
3583     * one is in prev_line
3584     */
3585
3586    /* Unlink cur_line */
3587    prev_line->next = cur_line->next;
3588    cur_line->next = NULL;
3589
3590    /* Free cur_line */
3591    edit_free_file_lines(cur_line);
3592
3593    err = edit_write_file(file);
3594    if (err)
3595    {
3596       /* Error writing file */
3597       edit_free_file(file);
3598       return err;
3599    }
3600
3601    target = strdup(CGI_PREFIX "edit-actions-list?filename=");
3602    string_append(&target, file->identifier);
3603
3604    edit_free_file(file);
3605
3606    if (target == NULL)
3607    {
3608       /* Out of memory */
3609       return JB_ERR_MEMORY;
3610    }
3611
3612    rsp->status = strdup("302 Local Redirect from Junkbuster");
3613    if (rsp->status == NULL)
3614    {
3615       free(target);
3616       return JB_ERR_MEMORY;
3617    }
3618    err = enlist_unique_header(rsp->headers, "Location", target);
3619    free(target);
3620
3621    return err;
3622 }
3623
3624
3625 /*********************************************************************
3626  *
3627  * Function    :  cgi_edit_actions_section_remove
3628  *
3629  * Description :  CGI function that actually removes a whole section from
3630  *                the actions file.  The section must be empty first
3631  *                (else JB_ERR_CGI_PARAMS).
3632  *
3633  * Parameters  :
3634  *           1 :  csp = Current client state (buffers, headers, etc...)
3635  *           2 :  rsp = http_response data structure for output
3636  *           3 :  parameters = map of cgi parameters
3637  *
3638  * CGI Parameters :
3639  *    filename : Identifies the file to edit
3640  *         ver : File's last-modified time
3641  *     section : Line number of section to edit
3642  *     pattern : Line number of pattern to edit
3643  *
3644  * Returns     :  JB_ERR_OK     on success
3645  *                JB_ERR_MEMORY on out-of-memory
3646  *                JB_ERR_CGI_PARAMS if the CGI parameters are not
3647  *                                  specified or not valid.
3648  *
3649  *********************************************************************/
3650 jb_err cgi_edit_actions_section_remove(struct client_state *csp,
3651                                        struct http_response *rsp,
3652                                        const struct map *parameters)
3653 {
3654    unsigned sectionid;
3655    struct editable_file * file;
3656    struct file_line * cur_line;
3657    struct file_line * prev_line;
3658    unsigned line_number;
3659    char * target;
3660    jb_err err;
3661
3662    if (0 == (csp->config->feature_flags & RUNTIME_FEATURE_CGI_EDIT_ACTIONS))
3663    {
3664       return cgi_error_disabled(csp, rsp);
3665    }
3666
3667    err = get_number_param(csp, parameters, "section", &sectionid);
3668    if (err)
3669    {
3670       return err;
3671    }
3672
3673    err = edit_read_actions_file(csp, rsp, parameters, 1, &file);
3674    if (err)
3675    {
3676       /* No filename specified, can't read file, modified, or out of memory. */
3677       return (err == JB_ERR_FILE ? JB_ERR_OK : err);
3678    }
3679
3680    line_number = 1;
3681    cur_line = file->lines;
3682
3683    prev_line = NULL;
3684    while ((cur_line != NULL) && (line_number < sectionid))
3685    {
3686       prev_line = cur_line;
3687       cur_line = cur_line->next;
3688       line_number++;
3689    }
3690
3691    if ( (cur_line == NULL)
3692      || (cur_line->type != FILE_LINE_ACTION) )
3693    {
3694       /* Invalid "sectionid" parameter */
3695       edit_free_file(file);
3696       return JB_ERR_CGI_PARAMS;
3697    }
3698
3699    if ( (cur_line->next != NULL)
3700      && (cur_line->next->type == FILE_LINE_URL) )
3701    {
3702       /* Section not empty. */
3703       edit_free_file(file);
3704       return JB_ERR_CGI_PARAMS;
3705    }
3706
3707    /* At this point, the line to remove is in cur_line, and the previous
3708     * one is in prev_line
3709     */
3710
3711    /* Unlink cur_line */
3712    if (prev_line == NULL)
3713    {
3714       /* Removing the first line from the file */
3715       file->lines = cur_line->next;
3716    }
3717    else
3718    {
3719       prev_line->next = cur_line->next;
3720    }
3721    cur_line->next = NULL;
3722
3723    /* Free cur_line */
3724    edit_free_file_lines(cur_line);
3725
3726    err = edit_write_file(file);
3727    if (err)
3728    {
3729       /* Error writing file */
3730       edit_free_file(file);
3731       return err;
3732    }
3733
3734    target = strdup(CGI_PREFIX "edit-actions-list?filename=");
3735    string_append(&target, file->identifier);
3736
3737    edit_free_file(file);
3738
3739    if (target == NULL)
3740    {
3741       /* Out of memory */
3742       return JB_ERR_MEMORY;
3743    }
3744
3745    rsp->status = strdup("302 Local Redirect from Junkbuster");
3746    if (rsp->status == NULL)
3747    {
3748       free(target);
3749       return JB_ERR_MEMORY;
3750    }
3751    err = enlist_unique_header(rsp->headers, "Location", target);
3752    free(target);
3753
3754    return err;
3755 }
3756
3757
3758 /*********************************************************************
3759  *
3760  * Function    :  cgi_edit_actions_section_add
3761  *
3762  * Description :  CGI function that adds a new empty section to
3763  *                an actions file.
3764  *
3765  * Parameters  :
3766  *           1 :  csp = Current client state (buffers, headers, etc...)
3767  *           2 :  rsp = http_response data structure for output
3768  *           3 :  parameters = map of cgi parameters
3769  *
3770  * CGI Parameters :
3771  *    filename : Identifies the file to edit
3772  *         ver : File's last-modified time
3773  *     section : Line number of section to add after, 0 for start
3774  *               of file.
3775  *
3776  * Returns     :  JB_ERR_OK     on success
3777  *                JB_ERR_MEMORY on out-of-memory
3778  *                JB_ERR_CGI_PARAMS if the CGI parameters are not
3779  *                                  specified or not valid.
3780  *
3781  *********************************************************************/
3782 jb_err cgi_edit_actions_section_add(struct client_state *csp,
3783                                     struct http_response *rsp,
3784                                     const struct map *parameters)
3785 {
3786    unsigned sectionid;
3787    struct file_line * new_line;
3788    char * new_text;
3789    struct editable_file * file;
3790    struct file_line * cur_line;
3791    unsigned line_number;
3792    char * target;
3793    jb_err err;
3794
3795    if (0 == (csp->config->feature_flags & RUNTIME_FEATURE_CGI_EDIT_ACTIONS))
3796    {
3797       return cgi_error_disabled(csp, rsp);
3798    }
3799
3800    err = get_number_param(csp, parameters, "section", &sectionid);
3801    if (err)
3802    {
3803       return err;
3804    }
3805
3806    err = edit_read_actions_file(csp, rsp, parameters, 1, &file);
3807    if (err)
3808    {
3809       /* No filename specified, can't read file, modified, or out of memory. */
3810       return (err == JB_ERR_FILE ? JB_ERR_OK : err);
3811    }
3812
3813    line_number = 1;
3814    cur_line = file->lines;
3815
3816    if (sectionid < 1U)
3817    {
3818       /* Add to start of file */
3819       if (cur_line != NULL)
3820       {
3821          /* There's something in the file, find the line before the first
3822           * action.
3823           */
3824          while ( (cur_line->next != NULL)
3825               && (cur_line->next->type != FILE_LINE_ACTION) )
3826          {
3827             cur_line = cur_line->next;
3828             line_number++;
3829          }
3830       }
3831    }
3832    else
3833    {
3834       /* Add after stated section. */
3835       while ((cur_line != NULL) && (line_number < sectionid))
3836       {
3837          cur_line = cur_line->next;
3838          line_number++;
3839       }
3840
3841       if ( (cur_line == NULL)
3842         || (cur_line->type != FILE_LINE_ACTION))
3843       {
3844          /* Invalid "sectionid" parameter */
3845          edit_free_file(file);
3846          return JB_ERR_CGI_PARAMS;
3847       }
3848
3849       /* Skip through the section to find the last line in it. */
3850       while ( (cur_line->next != NULL)
3851            && (cur_line->next->type != FILE_LINE_ACTION) )
3852       {
3853          cur_line = cur_line->next;
3854          line_number++;
3855       }
3856    }
3857
3858    /* At this point, the last line in the previous section is in cur_line
3859     * - add after this.  (Or if we need to add as the first line, cur_line
3860     * will be NULL).
3861     */
3862
3863    new_text = strdup("{}");
3864    if (NULL == new_text)
3865    {
3866       edit_free_file(file);
3867       return JB_ERR_MEMORY;
3868    }
3869
3870    /* Allocate the new line */
3871    new_line = (struct file_line *)zalloc(sizeof(*new_line));
3872    if (new_line == NULL)
3873    {
3874       free(new_text);
3875       edit_free_file(file);
3876       return JB_ERR_MEMORY;
3877    }
3878
3879    /* Fill in the data members of the new line */
3880    new_line->raw = NULL;
3881    new_line->prefix = NULL;
3882    new_line->unprocessed = new_text;
3883    new_line->type = FILE_LINE_ACTION;
3884
3885    if (cur_line != NULL)
3886    {
3887       /* Link new_line into the list, after cur_line */
3888       new_line->next = cur_line->next;
3889       cur_line->next = new_line;
3890    }
3891    else
3892    {
3893       /* Link new_line into the list, as first line */
3894       new_line->next = file->lines;
3895       file->lines = new_line;
3896    }
3897
3898    /* Done making changes, now commit */
3899
3900    err = edit_write_file(file);
3901    if (err)
3902    {
3903       /* Error writing file */
3904       edit_free_file(file);
3905       return err;
3906    }
3907
3908    target = strdup(CGI_PREFIX "edit-actions-list?filename=");
3909    string_append(&target, file->identifier);
3910
3911    edit_free_file(file);
3912
3913    if (target == NULL)
3914    {
3915       /* Out of memory */
3916       return JB_ERR_MEMORY;
3917    }
3918
3919    rsp->status = strdup("302 Local Redirect from Junkbuster");
3920    if (rsp->status == NULL)
3921    {
3922       free(target);
3923       return JB_ERR_MEMORY;
3924    }
3925    err = enlist_unique_header(rsp->headers, "Location", target);
3926    free(target);
3927
3928    return err;
3929 }
3930
3931
3932 /*********************************************************************
3933  *
3934  * Function    :  cgi_toggle
3935  *
3936  * Description :  CGI function that adds a new empty section to
3937  *                an actions file.
3938  *
3939  * Parameters  :
3940  *           1 :  csp = Current client state (buffers, headers, etc...)
3941  *           2 :  rsp = http_response data structure for output
3942  *           3 :  parameters = map of cgi parameters
3943  *
3944  * CGI Parameters :
3945  *         set : If present, how to change toggle setting:
3946  *               "enable", "disable", "toggle", or none (default).
3947  *        mini : If present, use mini reply template.
3948  *
3949  * Returns     :  JB_ERR_OK     on success
3950  *                JB_ERR_MEMORY on out-of-memory
3951  *
3952  *********************************************************************/
3953 jb_err cgi_toggle(struct client_state *csp,
3954                   struct http_response *rsp,
3955                   const struct map *parameters)
3956 {
3957    struct map *exports;
3958    char mode;
3959    const char *template_name;
3960    jb_err err;
3961
3962    assert(csp);
3963    assert(rsp);
3964    assert(parameters);
3965
3966    if (0 == (csp->config->feature_flags & RUNTIME_FEATURE_CGI_TOGGLE))
3967    {
3968       return cgi_error_disabled(csp, rsp);
3969    }
3970
3971    if (NULL == (exports = default_exports(csp, "toggle")))
3972    {
3973       return JB_ERR_MEMORY;
3974    }
3975
3976    mode = *(lookup(parameters, "set"));
3977
3978    if (mode == 'e')
3979    {
3980       /* Enable */
3981       g_bToggleIJB = 1;
3982    }
3983    else if (mode == 'd')
3984    {
3985       /* Disable */
3986       g_bToggleIJB = 0;
3987    }
3988    else if (mode == 't')
3989    {
3990       /* Toggle */
3991       g_bToggleIJB = !g_bToggleIJB;
3992    }
3993
3994    err = map_conditional(exports, "enabled", g_bToggleIJB);
3995    if (err)
3996    {
3997       free_map(exports);
3998       return err;
3999    }
4000
4001    template_name = (*(lookup(parameters, "mini"))
4002                  ? "toggle-mini"
4003                  : "toggle");
4004
4005    return template_fill_for_cgi(csp, template_name, exports, rsp);
4006 }
4007
4008
4009 #endif /* def FEATURE_CGI_EDIT_ACTIONS */
4010
4011
4012 /*
4013   Local Variables:
4014   tab-width: 3
4015   end:
4016 */