1 /*
2 ** Copyright (c) 2007 D. Richard Hipp
3 **
4 ** This program is free software; you can redistribute it and/or
5 ** modify it under the terms of the Simplified BSD License (also
6 ** known as the "2-Clause License" or "FreeBSD License".)
7 **
8 ** This program is distributed in the hope that it will be useful,
9 ** but without any warranty; without even the implied warranty of
10 ** merchantability or fitness for a particular purpose.
11 **
12 ** Author contact information:
13 **   drh@hwaci.com
14 **   http://www.hwaci.com/drh/
15 **
16 *******************************************************************************
17 **
18 ** Implementation of the Setup page
19 */
20 #include "config.h"
21 #include <assert.h>
22 #include "setup.h"
23 
24 /*
25 ** Increment the "cfgcnt" variable, so that ETags will know that
26 ** the configuration has changed.
27 */
setup_incr_cfgcnt(void)28 void setup_incr_cfgcnt(void){
29   static int once = 1;
30   if( once ){
31     once = 0;
32     db_unprotect(PROTECT_CONFIG);
33     db_multi_exec("UPDATE config SET value=value+1 WHERE name='cfgcnt'");
34     if( db_changes()==0 ){
35       db_multi_exec("INSERT INTO config(name,value) VALUES('cfgcnt',1)");
36     }
37     db_protect_pop();
38   }
39 }
40 
41 /*
42 ** Output a single entry for a menu generated using an HTML table.
43 ** If zLink is not NULL or an empty string, then it is the page that
44 ** the menu entry will hyperlink to.  If zLink is NULL or "", then
45 ** the menu entry has no hyperlink - it is disabled.
46 */
setup_menu_entry(const char * zTitle,const char * zLink,const char * zDesc)47 void setup_menu_entry(
48   const char *zTitle,
49   const char *zLink,
50   const char *zDesc
51 ){
52   @ <tr><td valign="top" align="right">
53   if( zLink && zLink[0] ){
54     @ <a href="%s(zLink)">%h(zTitle)</a>
55   }else{
56     @ %h(zTitle)
57   }
58   @ </td><td width="5"></td><td valign="top">%h(zDesc)</td></tr>
59 }
60 
61 
62 
63 /*
64 ** WEBPAGE: setup
65 **
66 ** Main menu for the administrative pages.  Requires Admin or Setup
67 ** privileges.  Links to sub-pages only usable by Setup users are
68 ** shown only to Setup users.
69 */
setup_page(void)70 void setup_page(void){
71   int setup_user = 0;
72   login_check_credentials();
73   if( !g.perm.Admin ){
74     login_needed(0);
75   }
76   setup_user = g.perm.Setup;
77 
78   style_set_current_feature("setup");
79   style_header("Server Administration");
80 
81   /* Make sure the header contains <base href="...">.   Issue a warning
82   ** if it does not. */
83   if( !cgi_header_contains("<base href=") ){
84     @ <p class="generalError"><b>Configuration Error:</b> Please add
85     @ <tt>&lt;base href="$secureurl/$current_page"&gt;</tt> after
86     @ <tt>&lt;head&gt;</tt> in the
87     @ <a href="setup_skinedit?w=2">HTML header</a>!</p>
88   }
89 
90 #if !defined(_WIN32)
91   /* Check for /dev/null and /dev/urandom.  We want both devices to be present,
92   ** but they are sometimes omitted (by mistake) from chroot jails. */
93   if( access("/dev/null", R_OK|W_OK) ){
94     @ <p class="generalError">WARNING: Device "/dev/null" is not available
95     @ for reading and writing.</p>
96   }
97   if( access("/dev/urandom", R_OK) ){
98     @ <p class="generalError">WARNING: Device "/dev/urandom" is not available
99     @ for reading. This means that the pseudo-random number generator used
100     @ by SQLite will be poorly seeded.</p>
101   }
102 #endif
103 
104   @ <table border="0" cellspacing="3">
105   setup_menu_entry("Users", "setup_ulist",
106     "Grant privileges to individual users.");
107   if( setup_user ){
108     setup_menu_entry("Access", "setup_access",
109       "Control access settings.");
110     setup_menu_entry("Configuration", "setup_config",
111       "Configure the WWW components of the repository");
112   }
113   setup_menu_entry("Security-Audit", "secaudit0",
114     "Analyze the current configuration for security problems");
115   if( setup_user ){
116     setup_menu_entry("Settings", "setup_settings",
117       "Web interface to the \"fossil settings\" command");
118   }
119   setup_menu_entry("Timeline", "setup_timeline",
120     "Timeline display preferences");
121   if( setup_user ){
122     setup_menu_entry("Login-Group", "setup_login_group",
123       "Manage single sign-on between this repository and others"
124       " on the same server");
125     setup_menu_entry("Tickets", "tktsetup",
126       "Configure the trouble-ticketing system for this repository");
127     setup_menu_entry("Wiki", "setup_wiki",
128       "Configure the wiki for this repository");
129     setup_menu_entry("Chat", "setup_chat",
130       "Configure the chatroom");
131   }
132   setup_menu_entry("Search","srchsetup",
133     "Configure the built-in search engine");
134   setup_menu_entry("URL Aliases", "waliassetup",
135     "Configure URL aliases");
136   if( setup_user ){
137     setup_menu_entry("Notification", "setup_notification",
138       "Automatic notifications of changes via outbound email");
139     setup_menu_entry("Transfers", "xfersetup",
140       "Configure the transfer system for this repository");
141   }
142   setup_menu_entry("Skins", "setup_skin",
143     "Select and/or modify the web interface \"skins\"");
144   setup_menu_entry("Moderation", "setup_modreq",
145     "Enable/Disable requiring moderator approval of Wiki and/or Ticket"
146     " changes and attachments.");
147   setup_menu_entry("Ad-Unit", "setup_adunit",
148     "Edit HTML text for an ad unit inserted after the menu bar");
149   setup_menu_entry("URLs & Checkouts", "urllist",
150     "Show URLs used to access this repo and known check-outs");
151   if( setup_user ){
152     setup_menu_entry("Web-Cache", "cachestat",
153       "View the status of the expensive-page cache");
154   }
155   setup_menu_entry("Logo", "setup_logo",
156     "Change the logo and background images for the server");
157   setup_menu_entry("Shunned", "shun",
158     "Show artifacts that are shunned by this repository");
159   setup_menu_entry("Artifact Receipts Log", "rcvfromlist",
160     "A record of received artifacts and their sources");
161   setup_menu_entry("User Log", "access_log",
162     "A record of login attempts");
163   setup_menu_entry("Administrative Log", "admin_log",
164     "View the admin_log entries");
165   setup_menu_entry("Error Log", "errorlog",
166     "View the Fossil server error log");
167   setup_menu_entry("Unversioned Files", "uvlist?byage=1",
168     "Show all unversioned files held");
169   setup_menu_entry("Stats", "stat",
170     "Repository Status Reports");
171   setup_menu_entry("Sitemap", "sitemap",
172     "Links to miscellaneous pages");
173   if( setup_user ){
174     setup_menu_entry("SQL", "admin_sql",
175       "Enter raw SQL commands");
176     setup_menu_entry("TH1", "admin_th1",
177       "Enter raw TH1 commands");
178   }
179   @ </table>
180 
181   style_finish_page();
182 }
183 
184 /*
185 ** Generate a checkbox for an attribute.
186 */
187 void onoff_attribute(
188   const char *zLabel,   /* The text label on the checkbox */
189   const char *zVar,     /* The corresponding row in the CONFIG table */
190   const char *zQParm,   /* The query parameter */
191   int dfltVal,          /* Default value if CONFIG table entry does not exist */
192   int disabled          /* 1 if disabled */
193 ){
194   const char *zQ = P(zQParm);
195   int iVal = db_get_boolean(zVar, dfltVal);
196   if( zQ==0 && !disabled && P("submit") ){
197     zQ = "off";
198   }
199   if( zQ ){
200     int iQ = fossil_strcmp(zQ,"on")==0 || atoi(zQ);
201     if( iQ!=iVal ){
202       login_verify_csrf_secret();
203       db_protect_only(PROTECT_NONE);
204       db_set(zVar/*works-like:"x"*/, iQ ? "1" : "0", 0);
205       db_protect_pop();
206       setup_incr_cfgcnt();
207       admin_log("Set option [%q] to [%q].",
208                 zVar, iQ ? "on" : "off");
209       iVal = iQ;
210     }
211   }
212   @ <label><input type="checkbox" name="%s(zQParm)" \
213   @ aria-label="%s(zLabel[0]?zLabel:zQParm)" \
214   if( iVal ){
215     @ checked="checked" \
216   }
217   if( disabled ){
218     @ disabled="disabled" \
219   }
220   @ /> <b>%s(zLabel)</b></label>
221 }
222 
223 /*
224 ** Generate an entry box for an attribute.
225 */
226 void entry_attribute(
227   const char *zLabel,   /* The text label on the entry box */
228   int width,            /* Width of the entry box */
229   const char *zVar,     /* The corresponding row in the CONFIG table */
230   const char *zQParm,   /* The query parameter */
231   const char *zDflt,    /* Default value if CONFIG table entry does not exist */
232   int disabled          /* 1 if disabled */
233 ){
234   const char *zVal = db_get(zVar, zDflt);
235   const char *zQ = P(zQParm);
236   if( zQ && fossil_strcmp(zQ,zVal)!=0 ){
237     const int nZQ = (int)strlen(zQ);
238     login_verify_csrf_secret();
239     setup_incr_cfgcnt();
240     db_protect_only(PROTECT_NONE);
241     db_set(zVar/*works-like:"x"*/, zQ, 0);
242     db_protect_pop();
243     admin_log("Set entry_attribute %Q to: %.*s%s",
244               zVar, 20, zQ, (nZQ>20 ? "..." : ""));
245     zVal = zQ;
246   }
247   @ <input aria-label="%h(zLabel[0]?zLabel:zQParm)" type="text" \
248   @ id="%s(zQParm)" name="%s(zQParm)" value="%h(zVal)" size="%d(width)" \
249   if( disabled ){
250     @ disabled="disabled" \
251   }
252   @ /> <b>%s(zLabel)</b>
253 }
254 
255 /*
256 ** Generate a text box for an attribute.
257 */
258 const char *textarea_attribute(
259   const char *zLabel,   /* The text label on the textarea */
260   int rows,             /* Rows in the textarea */
261   int cols,             /* Columns in the textarea */
262   const char *zVar,     /* The corresponding row in the CONFIG table */
263   const char *zQP,      /* The query parameter */
264   const char *zDflt,    /* Default value if CONFIG table entry does not exist */
265   int disabled          /* 1 if the textarea should  not be editable */
266 ){
267   const char *z = db_get(zVar, zDflt);
268   const char *zQ = P(zQP);
269   if( zQ && !disabled && fossil_strcmp(zQ,z)!=0){
270     const int nZQ = (int)strlen(zQ);
271     login_verify_csrf_secret();
272     db_protect_only(PROTECT_NONE);
273     db_set(zVar/*works-like:"x"*/, zQ, 0);
274     db_protect_pop();
275     setup_incr_cfgcnt();
276     admin_log("Set textarea_attribute %Q to: %.*s%s",
277               zVar, 20, zQ, (nZQ>20 ? "..." : ""));
278     z = zQ;
279   }
280   if( rows>0 && cols>0 ){
281     @ <textarea id="id%s(zQP)" name="%s(zQP)" rows="%d(rows)" \
282     @ aria-label="%h(zLabel[0]?zLabel:zQP)" \
283     if( disabled ){
284       @ disabled="disabled" \
285     }
286     @ cols="%d(cols)">%h(z)</textarea>
287     if( *zLabel ){
288       @ <span class="textareaLabel">%s(zLabel)</span>
289     }
290   }
291   return z;
292 }
293 
294 /*
295 ** Generate a text box for an attribute.
296 */
297 void multiple_choice_attribute(
298   const char *zLabel,   /* The text label on the menu */
299   const char *zVar,     /* The corresponding row in the CONFIG table */
300   const char *zQP,      /* The query parameter */
301   const char *zDflt,    /* Default value if CONFIG table entry does not exist */
302   int nChoice,          /* Number of choices */
303   const char *const *azChoice /* Choices in pairs (VAR value, Display) */
304 ){
305   const char *z = db_get(zVar, zDflt);
306   const char *zQ = P(zQP);
307   int i;
308   if( zQ && fossil_strcmp(zQ,z)!=0){
309     const int nZQ = (int)strlen(zQ);
310     login_verify_csrf_secret();
311     db_unprotect(PROTECT_ALL);
312     db_set(zVar/*works-like:"x"*/, zQ, 0);
313     setup_incr_cfgcnt();
314     db_protect_pop();
315     admin_log("Set multiple_choice_attribute %Q to: %.*s%s",
316               zVar, 20, zQ, (nZQ>20 ? "..." : ""));
317     z = zQ;
318   }
319   @ <select aria-label="%h(zLabel)" size="1" name="%s(zQP)" id="id%s(zQP)">
320   for(i=0; i<nChoice*2; i+=2){
321     const char *zSel = fossil_strcmp(azChoice[i],z)==0 ? " selected" : "";
322     @ <option value="%h(azChoice[i])"%s(zSel)>%h(azChoice[i+1])</option>
323   }
324   @ </select> <b>%h(zLabel)</b>
325 }
326 
327 
328 /*
329 ** WEBPAGE: setup_access
330 **
331 ** The access-control settings page.  Requires Setup privileges.
332 */
333 void setup_access(void){
334   static const char *const azRedirectOpts[] = {
335     "0", "Off",
336     "1", "Login Page Only",
337     "2", "All Pages"
338   };
339   login_check_credentials();
340   if( !g.perm.Setup ){
341     login_needed(0);
342     return;
343   }
344 
345   style_set_current_feature("setup");
346   style_header("Access Control Settings");
347   db_begin_transaction();
348   @ <form action="%R/setup_access" method="post"><div>
349   login_insert_csrf_secret();
350   @ <input type="submit"  name="submit" value="Apply Changes" /></p>
351   @ <hr />
352   multiple_choice_attribute("Redirect to HTTPS",
353      "redirect-to-https", "redirhttps", "0",
354      count(azRedirectOpts)/2, azRedirectOpts);
355   @ <p>Force the use of HTTPS by redirecting to HTTPS when an
356   @ unencrypted request is received.  This feature can be enabled
357   @ for the Login page only, or for all pages.
358   @ <p>Further details:  When enabled, this option causes the $secureurl TH1
359   @ variable is set to an "https:" variant of $baseurl.  Otherwise,
360   @ $secureurl is just an alias for $baseurl.
361   @ (Property: "redirect-to-https".  "0" for off, "1" for Login page only,
362   @ "2" otherwise.)
363   @ <hr />
364   onoff_attribute("Require password for local access",
365      "localauth", "localauth", 0, 0);
366   @ <p>When enabled, the password sign-in is always required for
367   @ web access.  When disabled, unrestricted web access from 127.0.0.1
368   @ is allowed for the <a href="%R/help/ui">fossil ui</a> command or
369   @ from the <a href="%R/help/server">fossil server</a>,
370   @ <a href="%R/help/http">fossil http</a> commands when the
371   @ "--localauth" command line options is used, or from the
372   @ <a href="%R/help/cgi">fossil cgi</a> if a line containing
373   @ the word "localauth" appears in the CGI script.
374   @
375   @ <p>A password is always required if any one or more
376   @ of the following are true:
377   @ <ol>
378   @ <li> This button is checked
379   @ <li> The inbound TCP/IP connection is not from 127.0.0.1
380   @ <li> The server is started using either of the
381   @ <a href="%R/help/server">fossil server</a> or
382   @ <a href="%R/help/server">fossil http</a> commands
383   @ without the "--localauth" option.
384   @ <li> The server is started from CGI without the "localauth" keyword
385   @ in the CGI script.
386   @ </ol>
387   @ (Property: "localauth")
388   @
389   @ <hr />
390   onoff_attribute("Enable /test_env",
391      "test_env_enable", "test_env_enable", 0, 0);
392   @ <p>When enabled, the %h(g.zBaseURL)/test_env URL is available to all
393   @ users.  When disabled (the default) only users Admin and Setup can visit
394   @ the /test_env page.
395   @ (Property: "test_env_enable")
396   @ </p>
397   @
398   @ <hr />
399   onoff_attribute("Enable /artifact_stats",
400      "artifact_stats_enable", "artifact_stats_enable", 0, 0);
401   @ <p>When enabled, the %h(g.zBaseURL)/artifact_stats URL is available to all
402   @ users.  When disabled (the default) only users with check-in privilege may
403   @ access the /artifact_stats page.
404   @ (Property: "artifact_stats_enable")
405   @ </p>
406   @
407   @ <hr />
408   onoff_attribute("Allow REMOTE_USER authentication",
409      "remote_user_ok", "remote_user_ok", 0, 0);
410   @ <p>When enabled, if the REMOTE_USER environment variable is set to the
411   @ login name of a valid user and no other login credentials are available,
412   @ then the REMOTE_USER is accepted as an authenticated user.
413   @ (Property: "remote_user_ok")
414   @ </p>
415   @
416   @ <hr />
417   onoff_attribute("Allow HTTP_AUTHENTICATION authentication",
418      "http_authentication_ok", "http_authentication_ok", 0, 0);
419   @ <p>When enabled, allow the use of the HTTP_AUTHENTICATION environment
420   @ variable or the "Authentication:" HTTP header to find the username and
421   @ password. This is another way of supporting Basic Authentication.
422   @ (Property: "http_authentication_ok")
423   @ </p>
424   @
425   @ <hr />
426   entry_attribute("Login expiration time", 6, "cookie-expire", "cex",
427                   "8766", 0);
428   @ <p>The number of hours for which a login is valid.  This must be a
429   @ positive number.  The default is 8766 hours which is approximately equal
430   @ to a year.
431   @ (Property: "cookie-expire")</p>
432 
433   @ <hr />
434   entry_attribute("Download packet limit", 10, "max-download", "mxdwn",
435                   "5000000", 0);
436   @ <p>Fossil tries to limit out-bound sync, clone, and pull packets
437   @ to this many bytes, uncompressed.  If the client requires more data
438   @ than this, then the client will issue multiple HTTP requests.
439   @ Values below 1 million are not recommended.  5 million is a
440   @ reasonable number.  (Property: "max-download")</p>
441 
442   @ <hr />
443   entry_attribute("Download time limit", 11, "max-download-time", "mxdwnt",
444                   "30", 0);
445 
446   @ <p>Fossil tries to spend less than this many seconds gathering
447   @ the out-bound data of sync, clone, and pull packets.
448   @ If the client request takes longer, a partial reply is given similar
449   @ to the download packet limit. 30s is a reasonable default.
450   @ (Property: "max-download-time")</p>
451 
452   @ <hr />
453   entry_attribute("Server Load Average Limit", 11, "max-loadavg", "mxldavg",
454                   "0.0", 0);
455   @ <p>Some expensive operations (such as computing tarballs, zip archives,
456   @ or annotation/blame pages) are prohibited if the load average on the host
457   @ computer is too large.  Set the threshold for disallowing expensive
458   @ computations here.  Set this to 0.0 to disable the load average limit.
459   @ This limit is only enforced on Unix servers.  On Linux systems,
460   @ access to the /proc virtual filesystem is required, which means this limit
461   @ might not work inside a chroot() jail.
462   @ (Property: "max-loadavg")</p>
463 
464   @ <hr />
465   onoff_attribute(
466       "Enable hyperlinks for \"nobody\" based on User-Agent and Javascript",
467       "auto-hyperlink", "autohyperlink", 1, 0);
468   @ <p>Enable hyperlinks (the equivalent of the "h" permission) for all users,
469   @ including user "nobody", as long as
470   @ <ol><li>the User-Agent string in the
471   @ HTTP header indicates that the request is coming from an actual human
472   @ being, and
473   @ <li>the user agent is able to
474   @ run Javascript in order to set the href= attribute of hyperlinks, and
475   @ <li>mouse movement is detected (optional - see the checkbox below), and
476   @ <li>a number of milliseconds have passed since the page loaded.</ol>
477   @
478   @ <p>This setting is designed to give easy access to humans while
479   @ keeping out robots and spiders.
480   @ You do not normally want a robot to walk your entire repository because
481   @ if it does, your server will end up computing diffs and annotations for
482   @ every historical version of every file and creating ZIPs and tarballs of
483   @ every historical check-in, which can use a lot of CPU and bandwidth
484   @ even for relatively small projects.</p>
485   @
486   @ <p>Additional parameters that control this behavior:</p>
487   @ <blockquote>
488   onoff_attribute("Require mouse movement before enabling hyperlinks",
489                   "auto-hyperlink-mouseover", "ahmo", 0, 0);
490   @ <br />
491   entry_attribute("Delay in milliseconds before enabling hyperlinks", 5,
492                   "auto-hyperlink-delay", "ah-delay", "50", 0);
493   @ </blockquote>
494   @ <p>For maximum robot defense, the "require mouse movement" should
495   @ be turned on and the "Delay" should be at least 50 milliseconds.</p>
496   @ (Properties: "auto-hyperlink",
497   @ "auto-hyperlink-mouseover", and "auto-hyperlink-delay")</p>
498 
499   @ <hr />
500   onoff_attribute("Require a CAPTCHA if not logged in",
501                   "require-captcha", "reqcapt", 1, 0);
502   @ <p>Require a CAPTCHA for edit operations (appending, creating, or
503   @ editing wiki or tickets or adding attachments to wiki or tickets)
504   @ for users who are not logged in. (Property: "require-captcha")</p>
505 
506   @ <hr />
507   entry_attribute("Public pages", 30, "public-pages",
508                   "pubpage", "", 0);
509   @ <p>A comma-separated list of glob patterns for pages that are accessible
510   @ without needing a login and using the privileges given by the
511   @ "Default privileges" setting below.
512   @
513   @ <p>Example use case: Set this field to "/doc/trunk/www/*" and set
514   @ the "Default privileges" to include the "o" privilege
515   @ to give anonymous users read-only permission to the
516   @ latest version of the embedded documentation in the www/ folder without
517   @ allowing them to see the rest of the source code.
518   @ (Property: "public-pages")
519   @ </p>
520 
521   @ <hr />
522   onoff_attribute("Allow users to register themselves",
523                   "self-register", "selfreg", 0, 0);
524   @ <p>Allow users to register themselves on the /register webpage.
525   @ A self-registration creates a new entry in the USER table and
526   @ perhaps also in the SUBSCRIBER table if email notification is
527   @ enabled.
528   @ (Property: "self-register")</p>
529 
530   @ <hr />
531   onoff_attribute("Email verification required for self-registration",
532                   "selfreg-verify", "sfverify", 0, 0);
533   @ <p>If enabled, self-registration creates a new entry in the USER table
534   @ with only capabilities "7".  The default user capabilities are not
535   @ added until the email address associated with the self-registration
536   @ has been verified. This setting only makes sense if
537   @ email notifications are enabled.
538   @ (Property: "selfreg-verify")</p>
539 
540   @ <hr />
541   onoff_attribute("Allow anonymous subscriptions",
542                   "anon-subscribe", "anonsub", 1, 0);
543   @ <p>If disabled, email notification subscriptions are only allowed
544   @ for users with a login.  If Nobody or Anonymous visit the /subscribe
545   @ page, they are redirected to /register or /login.
546   @ (Property: "anon-subscribe")</p>
547 
548   @ <hr />
549   entry_attribute("Authorized subscription email addresses", 35,
550                   "auth-sub-email", "asemail", "", 0);
551   @ <p>This is a comma-separated list of GLOB patterns that specify
552   @ email addresses that are authorized to subscriptions.  If blank
553   @ (the usual case), then any email address can be used to self-register.
554   @ This setting is used to limit subscriptions to members of a particular
555   @ organization or group based on their email address.
556   @ (Property: "auth-sub-email")</p>
557 
558   @ <hr />
559   entry_attribute("Default privileges", 10, "default-perms",
560                   "defaultperms", "u", 0);
561   @ <p>Permissions given to users that... <ul><li>register themselves using
562   @ the self-registration procedure (if enabled), or <li>access "public"
563   @ pages identified by the public-pages glob pattern above, or <li>
564   @ are users newly created by the administrator.</ul>
565   @ <p>Recommended value: "u" for Reader.
566   @ <a href="%R/setup_ucap_list">Capability Key</a>.
567   @ (Property: "default-perms")
568   @ </p>
569 
570   @ <hr />
571   onoff_attribute("Show javascript button to fill in CAPTCHA",
572                   "auto-captcha", "autocaptcha", 0, 0);
573   @ <p>When enabled, a button appears on the login screen for user
574   @ "anonymous" that will automatically fill in the CAPTCHA password.
575   @ This is less secure than forcing the user to do it manually, but is
576   @ probably secure enough and it is certainly more convenient for
577   @ anonymous users.  (Property: "auto-captcha")</p>
578 
579   @ <hr />
580   @ <p><input type="submit"  name="submit" value="Apply Changes" /></p>
581   @ </div></form>
582   db_end_transaction(0);
583   style_finish_page();
584 }
585 
586 /*
587 ** WEBPAGE: setup_login_group
588 **
589 ** Change how the current repository participates in a login
590 ** group.
591 */
592 void setup_login_group(void){
593   const char *zGroup;
594   char *zErrMsg = 0;
595   Blob fullName;
596   char *zSelfRepo;
597   const char *zRepo = PD("repo", "");
598   const char *zLogin = PD("login", "");
599   const char *zPw = PD("pw", "");
600   const char *zNewName = PD("newname", "New Login Group");
601 
602   login_check_credentials();
603   if( !g.perm.Setup ){
604     login_needed(0);
605     return;
606   }
607   file_canonical_name(g.zRepositoryName, &fullName, 0);
608   zSelfRepo = fossil_strdup(blob_str(&fullName));
609   blob_reset(&fullName);
610   if( P("join")!=0 ){
611     login_group_join(zRepo, 1, zLogin, zPw, zNewName, &zErrMsg);
612   }else if( P("leave") ){
613     login_group_leave(&zErrMsg);
614   }
615   style_set_current_feature("setup");
616   style_header("Login Group Configuration");
617   if( zErrMsg ){
618     @ <p class="generalError">%s(zErrMsg)</p>
619   }
620   zGroup = login_group_name();
621   if( zGroup==0 ){
622     @ <p>This repository (in the file named "%h(zSelfRepo)")
623     @ is not currently part of any login-group.
624     @ To join a login group, fill out the form below.</p>
625     @
626     @ <form action="%R/setup_login_group" method="post"><div>
627     login_insert_csrf_secret();
628     @ <blockquote><table border="0">
629     @
630     @ <tr><th align="right" id="rfigtj">Repository filename \
631     @ in group to join:</th>
632     @ <td width="5"></td><td>
633     @ <input aria-labelledby="rfigtj" type="text" size="50" \
634     @ value="%h(zRepo)" name="repo"></td></tr>
635     @
636     @ <tr><th align="right" id="lotar">Login on the above repo:</th>
637     @ <td width="5"></td><td>
638     @ <input aria-labelledby="lotar" type="text" size="20" \
639     @ value="%h(zLogin)" name="login"></td></tr>
640     @
641     @ <tr><th align="right" id="lgpw">Password:</th>
642     @ <td width="5"></td><td>
643     @ <input aria-labelledby="lgpw" type="password" size="20" name="pw">\
644     @ </td></tr>
645     @
646     @ <tr><th align="right" id="nolg">Name of login-group:</th>
647     @ <td width="5"></td><td>
648     @ <input aria-labelledby="nolg" type="text" size="30" \
649     @ value="%h(zNewName)" name="newname">
650     @ (only used if creating a new login-group).</td></tr>
651     @
652     @ <tr><td colspan="3" align="center">
653     @ <input type="submit" value="Join" name="join"></td></tr>
654     @ </table></blockquote></div></form>
655   }else{
656     Stmt q;
657     int n = 0;
658     @ <p>This repository (in the file "%h(zSelfRepo)")
659     @ is currently part of the "<b>%h(zGroup)</b>" login group.
660     @ Other repositories in that group are:</p>
661     @ <table border="0" cellspacing="4">
662     @ <tr><td colspan="2"><th align="left">Project Name<td>
663     @ <th align="left">Repository File</tr>
664     db_prepare(&q,
665        "SELECT value,"
666        "       (SELECT value FROM config"
667        "         WHERE name=('peer-name-' || substr(x.name,11)))"
668        "  FROM config AS x"
669        " WHERE name GLOB 'peer-repo-*'"
670        " ORDER BY value"
671     );
672     while( db_step(&q)==SQLITE_ROW ){
673       const char *zRepo = db_column_text(&q, 0);
674       const char *zTitle = db_column_text(&q, 1);
675       n++;
676       @ <tr><td align="right">%d(n).</td><td width="4">
677       @ <td>%h(zTitle)<td width="10"><td>%h(zRepo)</tr>
678     }
679     db_finalize(&q);
680     @ </table>
681     @
682     @ <p><form action="%R/setup_login_group" method="post"><div>
683     login_insert_csrf_secret();
684     @ To leave this login group press
685     @ <input type="submit" value="Leave Login Group" name="leave">
686     @ </form></p>
687     @ <hr /><h2>Implementation Details</h2>
688     @ <p>The following are fields from the CONFIG table related to login-groups,
689     @ provided here for instructional and debugging purposes:</p>
690     @ <table border='1' class='sortable' data-column-types='ttt' \
691     @ data-init-sort='1'>
692     @ <thead><tr>
693     @ <th>Config.Name<th>Config.Value<th>Config.mtime</tr>
694     @ </thead><tbody>
695     db_prepare(&q, "SELECT name, value, datetime(mtime,'unixepoch') FROM config"
696                    " WHERE name GLOB 'peer-*'"
697                    "    OR name GLOB 'project-*'"
698                    "    OR name GLOB 'login-group-*'"
699                    " ORDER BY name");
700     while( db_step(&q)==SQLITE_ROW ){
701       @ <tr><td>%h(db_column_text(&q,0))</td>
702       @ <td>%h(db_column_text(&q,1))</td>
703       @ <td>%h(db_column_text(&q,2))</td></tr>
704     }
705     db_finalize(&q);
706     @ </tbody></table>
707     style_table_sorter();
708   }
709   style_finish_page();
710 }
711 
712 /*
713 ** WEBPAGE: setup_timeline
714 **
715 ** Edit administrative settings controlling the display of
716 ** timelines.
717 */
718 void setup_timeline(void){
719   double tmDiff;
720   char zTmDiff[20];
721   static const char *const azTimeFormats[] = {
722       "0", "HH:MM",
723       "1", "HH:MM:SS",
724       "2", "YYYY-MM-DD HH:MM",
725       "3", "YYMMDD HH:MM",
726       "4", "(off)"
727   };
728   login_check_credentials();
729   if( !g.perm.Admin ){
730     login_needed(0);
731     return;
732   }
733 
734   style_set_current_feature("setup");
735   style_header("Timeline Display Preferences");
736   db_begin_transaction();
737   @ <form action="%R/setup_timeline" method="post"><div>
738   login_insert_csrf_secret();
739   @ <p><input type="submit"  name="submit" value="Apply Changes" /></p>
740 
741   @ <hr />
742   onoff_attribute("Allow block-markup in timeline",
743                   "timeline-block-markup", "tbm", 0, 0);
744   @ <p>In timeline displays, check-in comments can be displayed with or
745   @ without block markup such as paragraphs, tables, etc.
746   @ (Property: "timeline-block-markup")</p>
747 
748   @ <hr />
749   onoff_attribute("Plaintext comments on timelines",
750                   "timeline-plaintext", "tpt", 0, 0);
751   @ <p>In timeline displays, check-in comments are displayed literally,
752   @ without any wiki or HTML interpretation.  Use CSS to change
753   @ display formatting features such as fonts and line-wrapping behavior.
754   @ (Property: "timeline-plaintext")</p>
755 
756   @ <hr />
757   onoff_attribute("Truncate comment at first blank line (Git-style)",
758                   "timeline-truncate-at-blank", "ttb", 0, 0);
759   @ <p>In timeline displays, check-in comments are displayed only through
760   @ the first blank line.  This is the traditional way to display comments
761   @ in Git repositories (Property: "timeline-truncate-at-blank")</p>
762 
763   @ <hr />
764   onoff_attribute("Break comments at newline characters",
765                   "timeline-hard-newlines", "thnl", 0, 0);
766   @ <p>In timeline displays, newline characters in check-in comments force
767   @ a line break on the display.
768   @ (Property: "timeline-hard-newlines")</p>
769 
770   @ <hr />
771   onoff_attribute("Use Universal Coordinated Time (UTC)",
772                   "timeline-utc", "utc", 1, 0);
773   @ <p>Show times as UTC (also sometimes called Greenwich Mean Time (GMT) or
774   @ Zulu) instead of in local time.  On this server, local time is currently
775   tmDiff = db_double(0.0, "SELECT julianday('now')");
776   tmDiff = db_double(0.0,
777         "SELECT (julianday(%.17g,'localtime')-julianday(%.17g))*24.0",
778         tmDiff, tmDiff);
779   sqlite3_snprintf(sizeof(zTmDiff), zTmDiff, "%.1f", tmDiff);
780   if( strcmp(zTmDiff, "0.0")==0 ){
781     @ the same as UTC and so this setting will make no difference in
782     @ the display.</p>
783   }else if( tmDiff<0.0 ){
784     sqlite3_snprintf(sizeof(zTmDiff), zTmDiff, "%.1f", -tmDiff);
785     @ %s(zTmDiff) hours behind UTC.</p>
786   }else{
787     @ %s(zTmDiff) hours ahead of UTC.</p>
788   }
789   @ <p>(Property: "timeline-utc")
790 
791   @ <hr />
792   multiple_choice_attribute("Style", "timeline-default-style",
793             "tdss", "0", N_TIMELINE_VIEW_STYLE, timeline_view_styles);
794   @ <p>The default timeline viewing style, for when the user has not
795   @ specified an alternative.  (Property: "timeline-default-style")</p>
796 
797   @ <hr />
798   entry_attribute("Default Number Of Rows", 6, "timeline-default-length",
799                   "tldl", "50", 0);
800   @ <p>The maximum number of rows to show on a timeline in the absence
801   @ of a user display preference cookie setting or an explicit n= query
802   @ parameter.  (Property: "timeline-default-length")</p>
803 
804   @ <hr />
805   multiple_choice_attribute("Per-Item Time Format", "timeline-date-format",
806             "tdf", "0", count(azTimeFormats)/2, azTimeFormats);
807   @ <p>If the "HH:MM" or "HH:MM:SS" format is selected, then the date is shown
808   @ in a separate box (using CSS class "timelineDate") whenever the date
809   @ changes.  With the "YYYY-MM-DD&nbsp;HH:MM" and "YYMMDD ..." formats,
810   @ the complete date and time is shown on every timeline entry using the
811   @ CSS class "timelineTime". (Property: "timeline-date-format")</p>
812 
813   @ <hr />
814   entry_attribute("Max timeline comment length", 6,
815                   "timeline-max-comment", "tmc", "0", 0);
816   @ <p>The maximum length of a comment to be displayed in a timeline.
817   @ "0" there is no length limit.
818   @ (Property: "timeline-max-comment")</p>
819 
820   @ <hr />
821   entry_attribute("Tooltip dwell time (milliseconds)", 6,
822                   "timeline-dwelltime", "tdt", "100", 0);
823   @ <br>
824   entry_attribute("Tooltip close time (milliseconds)", 6,
825                   "timeline-closetime", "tct", "250", 0);
826   @ <p>The <strong>dwell time</strong> defines how long the mouse pointer
827   @ should be stationary above an object of the graph before a tooltip
828   @ appears.<br>
829   @ The <strong>close time</strong> defines how long the mouse pointer
830   @ can be away from an object before a tooltip is closed.</p>
831   @ <p>Set <strong>dwell time</strong> to "0" to disable tooltips.<br>
832   @ Set <strong>close time</strong> to "0" to keep tooltips visible until
833   @ the mouse is clicked elsewhere.<p>
834   @ <p>(Properties: "timeline-dwelltime", "timeline-closetime")</p>
835 
836   @ <hr />
837   onoff_attribute("Timestamp hyperlinks to /info",
838                   "timeline-tslink-info", "ttlti", 0, 0);
839   @ <p>The hyperlink on the timestamp associated with each timeline entry,
840   @ on the far left-hand side of the screen, normally targets another
841   @ /timeline page that shows the entry in context.  However, if this
842   @ option is turned on, that hyperlink targets the /info page showing
843   @ the details of the entry.
844   @ <p>The /timeline link is the default since it is often useful to
845   @ see an entry in context, and because that link is not otherwise
846   @ accessible on the timeline.  The /info link is also accessible by
847   @ double-clicking the timeline node or by clicking on the hash that
848   @ follows "check-in:" in the supplemental information section on the
849   @ right of the entry.
850   @ <p>(Properties: "timeline-tslink-info")
851 
852   @ <hr />
853   @ <p><input type="submit"  name="submit" value="Apply Changes" /></p>
854   @ </div></form>
855   db_end_transaction(0);
856   style_finish_page();
857 }
858 
859 /*
860 ** WEBPAGE: setup_settings
861 **
862 ** Change or view miscellaneous settings.  Part of the
863 ** /setup pages requiring Setup privileges.
864 */
865 void setup_settings(void){
866   int nSetting;
867   int i;
868   Setting const *pSet;
869   const Setting *aSetting = setting_info(&nSetting);
870 
871   login_check_credentials();
872   if( !g.perm.Setup ){
873     login_needed(0);
874     return;
875   }
876 
877   style_set_current_feature("setup");
878   style_header("Settings");
879   if(!g.repositoryOpen){
880     /* Provide read-only access to versioned settings,
881        but only if no repo file was explicitly provided. */
882     db_open_local(0);
883   }
884   db_begin_transaction();
885   @ <p>Settings marked with (v) are "versionable" and will be overridden
886   @ by the contents of managed files named
887   @ "<tt>.fossil-settings/</tt><i>SETTING-NAME</i>".
888   @ If the file for a versionable setting exists, the value cannot be
889   @ changed on this screen.</p><hr /><p>
890   @
891   @ <form action="%R/setup_settings" method="post"><div>
892   @ <table border="0"><tr><td valign="top">
893   login_insert_csrf_secret();
894   for(i=0, pSet=aSetting; i<nSetting; i++, pSet++){
895     if( pSet->width==0 ){
896       int hasVersionableValue = pSet->versionable &&
897           (db_get_versioned(pSet->name, NULL)!=0);
898       onoff_attribute("", pSet->name,
899                       pSet->var!=0 ? pSet->var : pSet->name /*works-like:"x"*/,
900                       is_truth(pSet->def), hasVersionableValue);
901       @ <a href='%R/help?cmd=%s(pSet->name)'>%h(pSet->name)</a>
902       if( pSet->versionable ){
903         @  (v)<br />
904       } else {
905         @ <br />
906       }
907     }
908   }
909   @ <br /><input type="submit"  name="submit" value="Apply Changes" />
910   @ </td><td style="width:50px;"></td><td valign="top">
911   @ <table>
912   for(i=0, pSet=aSetting; i<nSetting; i++, pSet++){
913     if( pSet->width>0 && !pSet->forceTextArea ){
914       int hasVersionableValue = pSet->versionable &&
915           (db_get_versioned(pSet->name, NULL)!=0);
916       @ <tr><td>
917       @ <a href='%R/help?cmd=%s(pSet->name)'>%h(pSet->name)</a>
918       if( pSet->versionable ){
919         @  (v)
920       } else {
921         @
922       }
923       @</td><td>
924       entry_attribute("", /*pSet->width*/ 25, pSet->name,
925                       pSet->var!=0 ? pSet->var : pSet->name /*works-like:"x"*/,
926                       (char*)pSet->def, hasVersionableValue);
927       @</td></tr>
928     }
929   }
930   @</table>
931   @ </td><td style="width:50px;"></td><td valign="top">
932   for(i=0, pSet=aSetting; i<nSetting; i++, pSet++){
933     if( pSet->width>0 && pSet->forceTextArea ){
934       int hasVersionableValue = db_get_versioned(pSet->name, NULL)!=0;
935       @ <a href='%R/help?cmd=%s(pSet->name)'>%s(pSet->name)</a>
936       if( pSet->versionable ){
937         @  (v)<br />
938       } else {
939         @ <br />
940       }
941       textarea_attribute("", /*rows*/ 2, /*cols*/ 35, pSet->name,
942                       pSet->var!=0 ? pSet->var : pSet->name /*works-like:"x"*/,
943                       (char*)pSet->def, hasVersionableValue);
944       @<br />
945     }
946   }
947   @ </td></tr></table>
948   @ </div></form>
949   db_end_transaction(0);
950   style_finish_page();
951 }
952 
953 /*
954 ** SETTING:  mainmenu          width=70 block-text
955 **
956 ** The mainmenu setting specifies the entries on the main menu
957 ** for many skins.  The mainmenu should be a TCL list.  Each set
958 ** of four consecutive values defines a single main menu item:
959 **
960 **   *   The first term is text that appears on the menu.
961 **
962 **   *   The second term is a hyperlink to take when a user clicks on the
963 **       entry.  Hyperlinks that start with "/" are relative to the
964 **       repository root.
965 **
966 **   *   The third term is an argument to the TH1 "capexpr" command.
967 **       If capexpr evalutes to true, then the entry is shown.  If not,
968 **       the entry is omitted.  "*" is always true.  "{}" is never true.
969 **
970 **   *   The fourth term is a list of extra class names to apply to the
971 **       new menu entry.  Some skins use classes "desktoponly" and
972 **       "wideonly" to only show the entries when the web browser
973 **       screen is wide or very wide, respectively.
974 **
975 ** Some custom skins might not use this property.  Whether the property
976 ** is used or not a choice made by the skin designer.  Some skins may add
977 ** extra choices (such as the hamburger button) to the menu.
978 */
979 /*
980 ** SETTING: sitemap-extra      width=70 block-text
981 **
982 ** The sitemap-extra setting defines extra links to appear on the
983 ** /sitemap web page as sub-items of the "Home Page" entry before the
984 ** "Documentation Search" entry (if any).  For skins that use the /sitemap
985 ** page to construct a hamburger menu dropdown, new entries added here
986 ** will appear on the hamburger menu.
987 **
988 ** This setting should be a TCL list divided into triples.  Each
989 ** triple defines a new entry:
990 **
991 **   *   The first term is the display name of the /sitemap entry
992 **
993 **   *   The second term is a hyperlink to take when a user clicks on the
994 **       entry.  Hyperlinks that start with "/" are relative to the
995 **       repository root.
996 **
997 **   *   The third term is an argument to the TH1 "capexpr" command.
998 **       If capexpr evalutes to true, then the entry is shown.  If not,
999 **       the entry is omitted.  "*" is always true.
1000 **
1001 ** The default value is blank, meaning no added entries.
1002 */
1003 
1004 
1005 /*
1006 ** WEBPAGE: setup_config
1007 **
1008 ** The "Admin/Configuration" page.  Requires Setup privilege.
1009 */
1010 void setup_config(void){
1011   login_check_credentials();
1012   if( !g.perm.Setup ){
1013     login_needed(0);
1014     return;
1015   }
1016 
1017   style_set_current_feature("setup");
1018   style_header("WWW Configuration");
1019   db_begin_transaction();
1020   @ <form action="%R/setup_config" method="post"><div>
1021   login_insert_csrf_secret();
1022   @ <input type="submit"  name="submit" value="Apply Changes" /></p>
1023   @ <hr />
1024   entry_attribute("Project Name", 60, "project-name", "pn", "", 0);
1025   @ <p>A brief project name so visitors know what this site is about.
1026   @ The project name will also be used as the RSS feed title.
1027   @ (Property: "project-name")
1028   @ </p>
1029   @ <hr />
1030   textarea_attribute("Project Description", 3, 80,
1031                      "project-description", "pd", "", 0);
1032   @ <p>Describe your project. This will be used in page headers for search
1033   @ engines as well as a short RSS description.
1034   @ (Property: "project-description")</p>
1035   @ <hr />
1036   entry_attribute("Tarball and ZIP-archive Prefix", 20, "short-project-name",
1037                   "spn", "", 0);
1038   @ <p>This is used as a prefix on the names of generated tarballs and
1039   @ ZIP archive. For best results, keep this prefix brief and avoid special
1040   @ characters such as "/" and "\".
1041   @ If no tarball prefix is specified, then the full Project Name above is used.
1042   @ (Property: "short-project-name")
1043   @ </p>
1044   @ <hr />
1045   entry_attribute("Download Tag", 20, "download-tag", "dlt", "trunk", 0);
1046   @ <p>The <a href='%R/download'>/download</a> page is designed to provide
1047   @ a convenient place for newbies
1048   @ to download a ZIP archive or a tarball of the project.  By default,
1049   @ the latest trunk check-in is downloaded.  Change this tag to something
1050   @ else (ex: release) to alter the behavior of the /download page.
1051   @ (Property: "download-tag")
1052   @ </p>
1053   @ <hr />
1054   entry_attribute("Index Page", 60, "index-page", "idxpg", "/home", 0);
1055   @ <p>Enter the pathname of the page to display when the "Home" menu
1056   @ option is selected and when no pathname is
1057   @ specified in the URL.  For example, if you visit the url:</p>
1058   @
1059   @ <blockquote><p>%h(g.zBaseURL)</p></blockquote>
1060   @
1061   @ <p>And you have specified an index page of "/home" the above will
1062   @ automatically redirect to:</p>
1063   @
1064   @ <blockquote><p>%h(g.zBaseURL)/home</p></blockquote>
1065   @
1066   @ <p>The default "/home" page displays a Wiki page with the same name
1067   @ as the Project Name specified above.  Some sites prefer to redirect
1068   @ to a documentation page (ex: "/doc/trunk/index.wiki") or to "/timeline".</p>
1069   @
1070   @ <p>Note:  To avoid a redirect loop or other problems, this entry must
1071   @ begin with "/" and it must specify a valid page.  For example,
1072   @ "<b>/home</b>" will work but "<b>home</b>" will not, since it omits the
1073   @ leading "/".</p>
1074   @ <p>(Property: "index-page")
1075   @ <hr>
1076   @ <p>The main menu for the web interface
1077   @ <p>
1078   @
1079   @ <p>This setting should be a TCL list.  Each set of four consecutive
1080   @ values defines a single main menu item:
1081   @ <ol>
1082   @ <li> The first term is text that appears on the menu.
1083   @ <li> The second term is a hyperlink to take when a user clicks on the
1084   @      entry.  Hyperlinks that start with "/" are relative to the
1085   @      repository root.
1086   @ <li> The third term is an argument to the TH1 "capexpr" command.
1087   @      If capexpr evalutes to true, then the entry is shown.  If not,
1088   @      the entry is omitted.  "*" is always true.  "{}" is never true.
1089   @ <li> The fourth term is a list of extra class names to apply to the new
1090   @      menu entry.  Some skins use classes "desktoponly" and "wideonly"
1091   @      to only show the entries when the web browser screen is wide or
1092   @      very wide, respectively.
1093   @ </ol>
1094   @
1095   @ <p>Some custom skins might not use this property. Whether the property
1096   @ is used or not a choice made by the skin designer. Some skins may add extra
1097   @ choices (such as the hamburger button) to the menu that are not shown
1098   @ on this list. (Property: mainmenu)
1099   @ <p>
1100   if(P("resetMenu")!=0){
1101     db_unset("mainmenu", 0);
1102     cgi_delete_parameter("mmenu");
1103   }
1104   textarea_attribute("Main Menu", 12, 80,
1105       "mainmenu", "mmenu", style_default_mainmenu(), 0);
1106   @ </p>
1107   @ <p><input type='checkbox' id='cbResetMenu' name='resetMenu' value='1'>
1108   @ <label for='cbResetMenu'>Reset menu to default value</label>
1109   @ </p>
1110   @ <hr>
1111   @ <p>Extra links to appear on the <a href="%R/sitemap">/sitemap</a> page,
1112   @ as sub-items of the "Home Page" entry, appearing before the
1113   @ "Documentation Search" entry (if any).  In skins that use the /sitemap
1114   @ page to construct a hamburger menu dropdown, new entries added here
1115   @ will appear on the hamburger menu.
1116   @
1117   @ <p>This setting should be a TCL list divided into triples.  Each
1118   @ triple defines a new entry:
1119   @ <ol>
1120   @ <li> The first term is the display name of the /sitemap entry
1121   @ <li> The second term is a hyperlink to take when a user clicks on the
1122   @      entry.  Hyperlinks that start with "/" are relative to the
1123   @      repository root.
1124   @ <li> The third term is an argument to the TH1 "capexpr" command.
1125   @      If capexpr evalutes to true, then the entry is shown.  If not,
1126   @      the entry is omitted.  "*" is always true.
1127   @ </ol>
1128   @
1129   @ <p>The default value is blank, meaning no added entries.
1130   @ (Property: sitemap-extra)
1131   @ <p>
1132   textarea_attribute("Custom Sitemap Entries", 8, 80,
1133       "sitemap-extra", "smextra", "", 0);
1134   @ <hr />
1135   @ <p><input type="submit"  name="submit" value="Apply Changes" /></p>
1136   @ </div></form>
1137   db_end_transaction(0);
1138   style_finish_page();
1139 }
1140 
1141 /*
1142 ** WEBPAGE: setup_wiki
1143 **
1144 ** The "Admin/Wiki" page.  Requires Setup privilege.
1145 */
1146 void setup_wiki(void){
1147   login_check_credentials();
1148   if( !g.perm.Setup ){
1149     login_needed(0);
1150     return;
1151   }
1152 
1153   style_set_current_feature("setup");
1154   style_header("Wiki Configuration");
1155   db_begin_transaction();
1156   @ <form action="%R/setup_wiki" method="post"><div>
1157   login_insert_csrf_secret();
1158   @ <input type="submit"  name="submit" value="Apply Changes" /></p>
1159   @ <hr />
1160   onoff_attribute("Associate Wiki Pages With Branches, Tags, or Checkins",
1161                   "wiki-about", "wiki-about", 1, 0);
1162   @ <p>
1163   @ Associate wiki pages with branches, tags, or checkins, based on
1164   @ the wiki page name.  Wiki pages that begin with "branch/", "checkin/"
1165   @ or "tag/" and which continue with the name of an existing branch, checkin
1166   @ or tag are treated specially when this feature is enabled.
1167   @ <ul>
1168   @ <li> <b>branch/</b><i>branch-name</i>
1169   @ <li> <b>checkin/</b><i>full-checkin-hash</i>
1170   @ <li> <b>tag/</b><i>tag-name</i>
1171   @ </ul>
1172   @ (Property: "wiki-about")</p>
1173   @ <hr />
1174   entry_attribute("Allow Unsafe HTML In Markdown", 6,
1175                   "safe-html", "safe-html", "", 0);
1176   @ <p>Allow "unsafe" HTML (ex: &lt;script&gt;, &lt;form&gt;, etc) to be
1177   @ generated by <a href="%R/md_rules">Markdown-formatted</a> documents.
1178   @ This setting is a string where each character indicates a "type" of
1179   @ document in which to allow unsafe HTML:
1180   @ <ul>
1181   @ <li> <b>b</b> &rarr; checked-in files, embedded documentation
1182   @ <li> <b>f</b> &rarr; forum posts
1183   @ <li> <b>t</b> &rarr; tickets
1184   @ <li> <b>w</b> &rarr; wiki pages
1185   @ </ul>
1186   @ Include letters for each type of document for which unsafe HTML should
1187   @ be allowed.  For example, to allow unsafe HTML only for checked-in files,
1188   @ make this setting be just "<b>b</b>".  To allow unsafe HTML anywhere except
1189   @ in forum posts, make this setting be "<b>btw</b>".  The default is an
1190   @ empty string which means that Fossil never allows Markdown documents
1191   @ to generate unsafe HTML.
1192   @ (Property: "safe-html")</p>
1193   @ <hr />
1194   @ The current interwiki tag map is as follows:
1195   interwiki_append_map_table(cgi_output_blob());
1196   @ <p>Visit <a href="./intermap">%R/intermap</a> for details or to
1197   @ modify the interwiki tag map.
1198   @ <hr />
1199   onoff_attribute("Use HTML as wiki markup language",
1200     "wiki-use-html", "wiki-use-html", 0, 0);
1201   @ <p>Use HTML as the wiki markup language. Wiki links will still be parsed
1202   @ but all other wiki formatting will be ignored.</p>
1203   @ <p><strong>CAUTION:</strong> when
1204   @ enabling, <i>all</i> HTML tags and attributes are accepted in the wiki.
1205   @ No sanitization is done. This means that it is very possible for malicious
1206   @ users to inject dangerous HTML, CSS and JavaScript code into your wiki.</p>
1207   @ <p>This should <strong>only</strong> be enabled when wiki editing is limited
1208   @ to trusted users. It should <strong>not</strong> be used on a publicly
1209   @ editable wiki.</p>
1210   @ (Property: "wiki-use-html")
1211   @ <hr />
1212   @ <p><input type="submit"  name="submit" value="Apply Changes" /></p>
1213   @ </div></form>
1214   db_end_transaction(0);
1215   style_finish_page();
1216 }
1217 
1218 /*
1219 ** WEBPAGE: setup_chat
1220 **
1221 ** The "Admin/Chat" page.  Requires Setup privilege.
1222 */
1223 void setup_chat(void){
1224   static const char *const azAlerts[] = {
1225     "alerts/plunk.wav",  "Plunk",
1226     "alerts/bflat3.wav", "Tone-1",
1227     "alerts/bflat2.wav", "Tone-2",
1228     "alerts/bloop.wav",  "Bloop",
1229   };
1230 
1231   login_check_credentials();
1232   if( !g.perm.Setup ){
1233     login_needed(0);
1234     return;
1235   }
1236 
1237   style_set_current_feature("setup");
1238   style_header("Chat Configuration");
1239   db_begin_transaction();
1240   @ <form action="%R/setup_chat" method="post"><div>
1241   login_insert_csrf_secret();
1242   @ <input type="submit"  name="submit" value="Apply Changes" /></p>
1243   @ <hr />
1244   entry_attribute("Initial Chat History Size", 10,
1245                   "chat-initial-history", "chatih", "50", 0);
1246   @ <p>When /chat first starts up, it preloads up to this many historical
1247   @ messages.
1248   @ (Property: "chat-initial-history")</p>
1249   @ <hr />
1250   entry_attribute("Minimum Number Of Historical Messages To Retain", 10,
1251                   "chat-keep-count", "chatkc", "50", 0);
1252   @ <p>The chat subsystem purges older messages.  But it will always retain
1253   @ the N most recent messages where N is the value of this setting.
1254   @ (Property: "chat-keep-count")</p>
1255   @ <hr />
1256   entry_attribute("Maximum Message Age In Days", 10,
1257                   "chat-keep-days", "chatkd", "7", 0);
1258   @ <p>Chat message are removed after N days, where N is the value of
1259   @ this setting.  N may be fractional.  So, for example, to only keep
1260   @ an historical record of chat messages for 12 hours, set this value
1261   @ to 0.5.
1262   @ (Property: "chat-keep-days")</p>
1263   @ <hr />
1264   entry_attribute("Chat Polling Timeout", 10,
1265                   "chat-poll-timeout", "chatpt", "420", 0);
1266   @ <p>New chat content is downloaded using the "long poll" technique.
1267   @ HTTP requests are made to /chat-poll which blocks waiting on new
1268   @ content to arrive.  But the /chat-poll cannot block forever.  It
1269   @ eventual must give up and return an empty message set.  This setting
1270   @ determines how long /chat-poll will wait before giving up.  The
1271   @ default setting of approximately 7 minutes works well on many systems.
1272   @ Shorter delays might be required on installations that use proxies
1273   @ or web-servers with short timeouts.  For best efficiency, this value
1274   @ should be larger rather than smaller.
1275   @ (Property: "chat-poll-timeout")</p>
1276   @ <hr />
1277 
1278   multiple_choice_attribute("Alert sound",
1279      "chat-alert-sound", "snd", azAlerts[0],
1280      count(azAlerts)/2, azAlerts);
1281   @ <p>The sound used in the client-side chat to indicate that a new
1282   @ chat message has arrived.
1283   @ (Property: "chat-alert-sound")</p>
1284   @ <hr/>
1285   @ <p><input type="submit"  name="submit" value="Apply Changes" /></p>
1286   @ </div></form>
1287   db_end_transaction(0);
1288   @ <script nonce="%h(style_nonce())">
1289   @ (function(){
1290   @   var w = document.getElementById('idsnd');
1291   @   w.onchange = function(){
1292   @     var audio = new Audio('%s(g.zBaseURL)/builtin/' + w.value);
1293   @     audio.currentTime = 0;
1294   @     audio.play();
1295   @   }
1296   @ })();
1297   @ </script>
1298   style_finish_page();
1299 }
1300 
1301 /*
1302 ** WEBPAGE: setup_modreq
1303 **
1304 ** Admin page for setting up moderation of tickets and wiki.
1305 */
1306 void setup_modreq(void){
1307   login_check_credentials();
1308   if( !g.perm.Admin ){
1309     login_needed(0);
1310     return;
1311   }
1312 
1313   style_set_current_feature("setup");
1314   style_header("Moderator For Wiki And Tickets");
1315   db_begin_transaction();
1316   @ <form action="%R/setup_modreq" method="post"><div>
1317   login_insert_csrf_secret();
1318   @ <hr />
1319   onoff_attribute("Moderate ticket changes",
1320      "modreq-tkt", "modreq-tkt", 0, 0);
1321   @ <p>When enabled, any change to tickets is subject to the approval
1322   @ by a ticket moderator - a user with the "q" or Mod-Tkt privilege.
1323   @ Ticket changes enter the system and are shown locally, but are not
1324   @ synced until they are approved.  The moderator has the option to
1325   @ delete the change rather than approve it.  Ticket changes made by
1326   @ a user who has the Mod-Tkt privilege are never subject to
1327   @ moderation. (Property: "modreq-tkt")
1328   @
1329   @ <hr />
1330   onoff_attribute("Moderate wiki changes",
1331      "modreq-wiki", "modreq-wiki", 0, 0);
1332   @ <p>When enabled, any change to wiki is subject to the approval
1333   @ by a wiki moderator - a user with the "l" or Mod-Wiki privilege.
1334   @ Wiki changes enter the system and are shown locally, but are not
1335   @ synced until they are approved.  The moderator has the option to
1336   @ delete the change rather than approve it.  Wiki changes made by
1337   @ a user who has the Mod-Wiki privilege are never subject to
1338   @ moderation. (Property: "modreq-wiki")
1339   @ </p>
1340 
1341   @ <hr />
1342   @ <p><input type="submit"  name="submit" value="Apply Changes" /></p>
1343   @ </div></form>
1344   db_end_transaction(0);
1345   style_finish_page();
1346 
1347 }
1348 
1349 /*
1350 ** WEBPAGE: setup_adunit
1351 **
1352 ** Administrative page for configuring and controlling ad units
1353 ** and how they are displayed.
1354 */
1355 void setup_adunit(void){
1356   login_check_credentials();
1357   if( !g.perm.Admin ){
1358     login_needed(0);
1359     return;
1360   }
1361   db_begin_transaction();
1362   if( P("clear")!=0 && cgi_csrf_safe(1) ){
1363     db_unprotect(PROTECT_CONFIG);
1364     db_multi_exec("DELETE FROM config WHERE name GLOB 'adunit*'");
1365     db_protect_pop();
1366     cgi_replace_parameter("adunit","");
1367     cgi_replace_parameter("adright","");
1368     setup_incr_cfgcnt();
1369   }
1370 
1371   style_set_current_feature("setup");
1372   style_header("Edit Ad Unit");
1373   @ <form action="%R/setup_adunit" method="post"><div>
1374   login_insert_csrf_secret();
1375   @ <b>Banner Ad-Unit:</b><br />
1376  textarea_attribute("", 6, 80, "adunit", "adunit", "", 0);
1377   @ <br />
1378   @ <b>Right-Column Ad-Unit:</b><br />
1379   textarea_attribute("", 6, 80, "adunit-right", "adright", "", 0);
1380   @ <br />
1381   onoff_attribute("Omit ads to administrator",
1382      "adunit-omit-if-admin", "oia", 0, 0);
1383   @ <br />
1384   onoff_attribute("Omit ads to logged-in users",
1385      "adunit-omit-if-user", "oiu", 0, 0);
1386   @ <br />
1387   onoff_attribute("Temporarily disable all ads",
1388      "adunit-disable", "oall", 0, 0);
1389   @ <br />
1390   @ <input type="submit" name="submit" value="Apply Changes" />
1391   @ <input type="submit" name="clear" value="Delete Ad-Unit" />
1392   @ </div></form>
1393   @ <hr />
1394   @ <b>Ad-Unit Notes:</b><ul>
1395   @ <li>Leave both Ad-Units blank to disable all advertising.
1396   @ <li>The "Banner Ad-Unit" is used for wide pages.
1397   @ <li>The "Right-Column Ad-Unit" is used on pages with tall, narrow content.
1398   @ <li>If the "Right-Column Ad-Unit" is blank, the "Banner Ad-Unit" is
1399   @     used on all pages.
1400   @ <li>Properties: "adunit", "adunit-right", "adunit-omit-if-admin", and
1401   @     "adunit-omit-if-user".
1402   @ <li>Suggested <a href="setup_skinedit?w=0">CSS</a> changes:
1403   @ <blockquote><pre>
1404   @ div.adunit_banner {
1405   @   margin: auto;
1406   @   width: 100%%;
1407   @ }
1408   @ div.adunit_right {
1409   @   float: right;
1410   @ }
1411   @ div.adunit_right_container {
1412   @   min-height: <i>height-of-right-column-ad-unit</i>;
1413   @ }
1414   @ </pre></blockquote>
1415   @ <li>For a place-holder Ad-Unit for testing, Copy/Paste the following
1416   @ with appropriate adjustments to "width:" and "height:".
1417   @ <blockquote><pre>
1418   @ &lt;div style='
1419   @   margin: 0 auto;
1420   @   width: 600px;
1421   @   height: 90px;
1422   @   border: 1px solid #f11;
1423   @   background-color: #fcc;
1424   @ '&gt;Demo Ad&lt;/div&gt;
1425   @ </pre></blockquote>
1426   @ </li>
1427   style_finish_page();
1428   db_end_transaction(0);
1429 }
1430 
1431 /*
1432 ** WEBPAGE: setup_logo
1433 **
1434 ** Administrative page for changing the logo, background, and icon images.
1435 */
1436 void setup_logo(void){
1437   const char *zLogoMtime = db_get_mtime("logo-image", 0, 0);
1438   const char *zLogoMime = db_get("logo-mimetype","image/gif");
1439   const char *aLogoImg = P("logoim");
1440   int szLogoImg = atoi(PD("logoim:bytes","0"));
1441   const char *zBgMtime = db_get_mtime("background-image", 0, 0);
1442   const char *zBgMime = db_get("background-mimetype","image/gif");
1443   const char *aBgImg = P("bgim");
1444   int szBgImg = atoi(PD("bgim:bytes","0"));
1445   const char *zIconMtime = db_get_mtime("icon-image", 0, 0);
1446   const char *zIconMime = db_get("icon-mimetype","image/gif");
1447   const char *aIconImg = P("iconim");
1448   int szIconImg = atoi(PD("iconim:bytes","0"));
1449   if( szLogoImg>0 ){
1450     zLogoMime = PD("logoim:mimetype","image/gif");
1451   }
1452   if( szBgImg>0 ){
1453     zBgMime = PD("bgim:mimetype","image/gif");
1454   }
1455   if( szIconImg>0 ){
1456     zIconMime = PD("iconim:mimetype","image/gif");
1457   }
1458   login_check_credentials();
1459   if( !g.perm.Admin ){
1460     login_needed(0);
1461     return;
1462   }
1463   db_begin_transaction();
1464   if( !cgi_csrf_safe(1) ){
1465     /* Allow no state changes if not safe from CSRF */
1466   }else if( P("setlogo")!=0 && zLogoMime && zLogoMime[0] && szLogoImg>0 ){
1467     Blob img;
1468     Stmt ins;
1469     blob_init(&img, aLogoImg, szLogoImg);
1470     db_unprotect(PROTECT_CONFIG);
1471     db_prepare(&ins,
1472         "REPLACE INTO config(name,value,mtime)"
1473         " VALUES('logo-image',:bytes,now())"
1474     );
1475     db_bind_blob(&ins, ":bytes", &img);
1476     db_step(&ins);
1477     db_finalize(&ins);
1478     db_multi_exec(
1479        "REPLACE INTO config(name,value,mtime) VALUES('logo-mimetype',%Q,now())",
1480        zLogoMime
1481     );
1482     db_protect_pop();
1483     db_end_transaction(0);
1484     cgi_redirect("setup_logo");
1485   }else if( P("clrlogo")!=0 ){
1486     db_unprotect(PROTECT_CONFIG);
1487     db_multi_exec(
1488        "DELETE FROM config WHERE name IN "
1489            "('logo-image','logo-mimetype')"
1490     );
1491     db_protect_pop();
1492     db_end_transaction(0);
1493     cgi_redirect("setup_logo");
1494   }else if( P("setbg")!=0 && zBgMime && zBgMime[0] && szBgImg>0 ){
1495     Blob img;
1496     Stmt ins;
1497     blob_init(&img, aBgImg, szBgImg);
1498     db_unprotect(PROTECT_CONFIG);
1499     db_prepare(&ins,
1500         "REPLACE INTO config(name,value,mtime)"
1501         " VALUES('background-image',:bytes,now())"
1502     );
1503     db_bind_blob(&ins, ":bytes", &img);
1504     db_step(&ins);
1505     db_finalize(&ins);
1506     db_multi_exec(
1507        "REPLACE INTO config(name,value,mtime)"
1508        " VALUES('background-mimetype',%Q,now())",
1509        zBgMime
1510     );
1511     db_protect_pop();
1512     db_end_transaction(0);
1513     cgi_redirect("setup_logo");
1514   }else if( P("clrbg")!=0 ){
1515     db_unprotect(PROTECT_CONFIG);
1516     db_multi_exec(
1517        "DELETE FROM config WHERE name IN "
1518            "('background-image','background-mimetype')"
1519     );
1520     db_protect_pop();
1521     db_end_transaction(0);
1522     cgi_redirect("setup_logo");
1523   }else if( P("seticon")!=0 && zIconMime && zIconMime[0] && szIconImg>0 ){
1524     Blob img;
1525     Stmt ins;
1526     blob_init(&img, aIconImg, szIconImg);
1527     db_unprotect(PROTECT_CONFIG);
1528     db_prepare(&ins,
1529         "REPLACE INTO config(name,value,mtime)"
1530         " VALUES('icon-image',:bytes,now())"
1531     );
1532     db_bind_blob(&ins, ":bytes", &img);
1533     db_step(&ins);
1534     db_finalize(&ins);
1535     db_multi_exec(
1536        "REPLACE INTO config(name,value,mtime)"
1537        " VALUES('icon-mimetype',%Q,now())",
1538        zIconMime
1539     );
1540     db_protect_pop();
1541     db_end_transaction(0);
1542     cgi_redirect("setup_logo");
1543   }else if( P("clricon")!=0 ){
1544     db_unprotect(PROTECT_CONFIG);
1545     db_multi_exec(
1546        "DELETE FROM config WHERE name IN "
1547            "('icon-image','icon-mimetype')"
1548     );
1549     db_protect_pop();
1550     db_end_transaction(0);
1551     cgi_redirect("setup_logo");
1552   }
1553   style_set_current_feature("setup");
1554   style_header("Edit Project Logo And Background");
1555   @ <p>The current project logo has a MIME-Type of <b>%h(zLogoMime)</b>
1556   @ and looks like this:</p>
1557   @ <blockquote><p><img src="%R/logo/%z(zLogoMtime)" \
1558   @ alt="logo" border="1" />
1559   @ </p></blockquote>
1560   @
1561   @ <form action="%R/setup_logo" method="post"
1562   @  enctype="multipart/form-data"><div>
1563   @ <p>The logo is accessible to all users at this URL:
1564   @ <a href="%s(g.zBaseURL)/logo">%s(g.zBaseURL)/logo</a>.
1565   @ The logo may or may not appear on each
1566   @ page depending on the <a href="setup_skinedit?w=0">CSS</a> and
1567   @ <a href="setup_skinedit?w=2">header setup</a>.
1568   @ To change the logo image, use the following form:</p>
1569   login_insert_csrf_secret();
1570   @ Logo Image file:
1571   @ <input type="file" name="logoim" size="60" accept="image/*" />
1572   @ <p align="center">
1573   @ <input type="submit" name="setlogo" value="Change Logo" />
1574   @ <input type="submit" name="clrlogo" value="Revert To Default" /></p>
1575   @ <p>(Properties: "logo-image" and "logo-mimetype")
1576   @ </div></form>
1577   @ <hr />
1578   @
1579   @ <p>The current background image has a MIME-Type of <b>%h(zBgMime)</b>
1580   @ and looks like this:</p>
1581   @ <blockquote><p><img src="%R/background/%z(zBgMtime)" \
1582   @ alt="background" border=1 />
1583   @ </p></blockquote>
1584   @
1585   @ <form action="%R/setup_logo" method="post"
1586   @  enctype="multipart/form-data"><div>
1587   @ <p>The background image is accessible to all users at this URL:
1588   @ <a href="%s(g.zBaseURL)/background">%s(g.zBaseURL)/background</a>.
1589   @ The background image may or may not appear on each
1590   @ page depending on the <a href="setup_skinedit?w=0">CSS</a> and
1591   @ <a href="setup_skinedit?w=2">header setup</a>.
1592   @ To change the background image, use the following form:</p>
1593   login_insert_csrf_secret();
1594   @ Background image file:
1595   @ <input type="file" name="bgim" size="60" accept="image/*" />
1596   @ <p align="center">
1597   @ <input type="submit" name="setbg" value="Change Background" />
1598   @ <input type="submit" name="clrbg" value="Revert To Default" /></p>
1599   @ </div></form>
1600   @ <p>(Properties: "background-image" and "background-mimetype")
1601   @ <hr />
1602   @
1603   @ <p>The current icon image has a MIME-Type of <b>%h(zIconMime)</b>
1604   @ and looks like this:</p>
1605   @ <blockquote><p><img src="%R/favicon.ico/%z(zIconMtime)" \
1606   @ alt="icon" border=1 />
1607   @ </p></blockquote>
1608   @
1609   @ <form action="%R/setup_logo" method="post"
1610   @  enctype="multipart/form-data"><div>
1611   @ <p>The icon image is accessible to all users at this URL:
1612   @ <a href="%s(g.zBaseURL)/favicon.ico">%s(g.zBaseURL)/favicon.ico</a>.
1613   @ The icon image may or may not appear on each
1614   @ page depending on the web browser in use and the MIME-Types that it
1615   @ supports for icon images.
1616   @ To change the icon image, use the following form:</p>
1617   login_insert_csrf_secret();
1618   @ Icon image file:
1619   @ <input type="file" name="iconim" size="60" accept="image/*" />
1620   @ <p align="center">
1621   @ <input type="submit" name="seticon" value="Change Icon" />
1622   @ <input type="submit" name="clricon" value="Revert To Default" /></p>
1623   @ </div></form>
1624   @ <p>(Properties: "icon-image" and "icon-mimetype")
1625   @ <hr />
1626   @
1627   @ <p><span class="note">Note:</span>  Your browser has probably cached these
1628   @ images, so you may need to press the Reload button before changes will
1629   @ take effect. </p>
1630   style_finish_page();
1631   db_end_transaction(0);
1632 }
1633 
1634 /*
1635 ** Prevent the RAW SQL feature from being used to ATTACH a different
1636 ** database and query it.
1637 **
1638 ** Actually, the RAW SQL feature only does a single statement per request.
1639 ** So it is not possible to ATTACH and then do a separate query.  This
1640 ** routine is not strictly necessary, therefore.  But it does not hurt
1641 ** to be paranoid.
1642 */
1643 int raw_sql_query_authorizer(
1644   void *pError,
1645   int code,
1646   const char *zArg1,
1647   const char *zArg2,
1648   const char *zArg3,
1649   const char *zArg4
1650 ){
1651   if( code==SQLITE_ATTACH ){
1652     return SQLITE_DENY;
1653   }
1654   return SQLITE_OK;
1655 }
1656 
1657 
1658 /*
1659 ** WEBPAGE: admin_sql
1660 **
1661 ** Run raw SQL commands against the database file using the web interface.
1662 ** Requires Setup privileges.
1663 */
1664 void sql_page(void){
1665   const char *zQ;
1666   int go = P("go")!=0;
1667   login_check_credentials();
1668   if( !g.perm.Setup ){
1669     login_needed(0);
1670     return;
1671   }
1672   add_content_sql_commands(g.db);
1673   zQ = cgi_csrf_safe(1) ? P("q") : 0;
1674   style_set_current_feature("setup");
1675   style_header("Raw SQL Commands");
1676   @ <p><b>Caution:</b> There are no restrictions on the SQL that can be
1677   @ run by this page.  You can do serious and irrepairable damage to the
1678   @ repository.  Proceed with extreme caution.</p>
1679   @
1680 #if 0
1681   @ <p>Only the first statement in the entry box will be run.
1682   @ Any subsequent statements will be silently ignored.</p>
1683   @
1684   @ <p>Database names:<ul><li>repository
1685   if( g.zConfigDbName ){
1686     @ <li>configdb
1687   }
1688   if( g.localOpen ){
1689     @ <li>localdb
1690   }
1691   @ </ul></p>
1692 #endif
1693 
1694   if( P("configtab") ){
1695     /* If the user presses the "CONFIG Table Query" button, populate the
1696     ** query text with a pre-packaged query against the CONFIG table */
1697     zQ = "SELECT\n"
1698          " CASE WHEN length(name)<50 THEN name ELSE printf('%.50s...',name)"
1699          "  END AS name,\n"
1700          " CASE WHEN typeof(value)<>'blob' AND length(value)<80 THEN value\n"
1701          "           ELSE '...' END AS value,\n"
1702          " datetime(mtime, 'unixepoch') AS mtime\n"
1703          "FROM config\n"
1704          "-- ORDER BY mtime DESC; -- optional";
1705      go = 1;
1706   }
1707   @
1708   @ <form method="post" action="%R/admin_sql">
1709   login_insert_csrf_secret();
1710   @ SQL:<br />
1711   @ <textarea name="q" rows="8" cols="80">%h(zQ)</textarea><br />
1712   @ <input type="submit" name="go" value="Run SQL">
1713   @ <input type="submit" name="schema" value="Show Schema">
1714   @ <input type="submit" name="tablelist" value="List Tables">
1715   @ <input type="submit" name="configtab" value="CONFIG Table Query">
1716   @ </form>
1717   if( P("schema") ){
1718     zQ = sqlite3_mprintf(
1719             "SELECT sql FROM repository.sqlite_schema"
1720             " WHERE sql IS NOT NULL ORDER BY name");
1721     go = 1;
1722   }else if( P("tablelist") ){
1723     zQ = sqlite3_mprintf(
1724             "SELECT name FROM repository.sqlite_schema WHERE type='table'"
1725             " ORDER BY name");
1726     go = 1;
1727   }
1728   if( go ){
1729     sqlite3_stmt *pStmt;
1730     int rc;
1731     const char *zTail;
1732     int nCol;
1733     int nRow = 0;
1734     int i;
1735     @ <hr />
1736     login_verify_csrf_secret();
1737     sqlite3_set_authorizer(g.db, raw_sql_query_authorizer, 0);
1738     rc = sqlite3_prepare_v2(g.db, zQ, -1, &pStmt, &zTail);
1739     if( rc!=SQLITE_OK ){
1740       @ <div class="generalError">%h(sqlite3_errmsg(g.db))</div>
1741       sqlite3_finalize(pStmt);
1742     }else if( pStmt==0 ){
1743       /* No-op */
1744     }else if( (nCol = sqlite3_column_count(pStmt))==0 ){
1745       sqlite3_step(pStmt);
1746       rc = sqlite3_finalize(pStmt);
1747       if( rc ){
1748         @ <div class="generalError">%h(sqlite3_errmsg(g.db))</div>
1749       }
1750     }else{
1751       @ <table border=1>
1752       while( sqlite3_step(pStmt)==SQLITE_ROW ){
1753         if( nRow==0 ){
1754           @ <tr>
1755           for(i=0; i<nCol; i++){
1756             @ <th>%h(sqlite3_column_name(pStmt, i))</th>
1757           }
1758           @ </tr>
1759         }
1760         nRow++;
1761         @ <tr>
1762         for(i=0; i<nCol; i++){
1763           switch( sqlite3_column_type(pStmt, i) ){
1764             case SQLITE_INTEGER:
1765             case SQLITE_FLOAT: {
1766                @ <td align="right" valign="top">
1767                @ %s(sqlite3_column_text(pStmt, i))</td>
1768                break;
1769             }
1770             case SQLITE_NULL: {
1771                @ <td valign="top" align="center"><i>NULL</i></td>
1772                break;
1773             }
1774             case SQLITE_TEXT: {
1775                const char *zText = (const char*)sqlite3_column_text(pStmt, i);
1776                @ <td align="left" valign="top"
1777                @ style="white-space:pre;">%h(zText)</td>
1778                break;
1779             }
1780             case SQLITE_BLOB: {
1781                @ <td valign="top" align="center">
1782                @ <i>%d(sqlite3_column_bytes(pStmt, i))-byte BLOB</i></td>
1783                break;
1784             }
1785           }
1786         }
1787         @ </tr>
1788       }
1789       sqlite3_finalize(pStmt);
1790       @ </table>
1791     }
1792   }
1793   style_finish_page();
1794 }
1795 
1796 
1797 /*
1798 ** WEBPAGE: admin_th1
1799 **
1800 ** Run raw TH1 commands using the web interface.  If Tcl integration was
1801 ** enabled at compile-time and the "tcl" setting is enabled, Tcl commands
1802 ** may be run as well.  Requires Admin privilege.
1803 */
1804 void th1_page(void){
1805   const char *zQ = P("q");
1806   int go = P("go")!=0;
1807   login_check_credentials();
1808   if( !g.perm.Setup ){
1809     login_needed(0);
1810     return;
1811   }
1812   style_set_current_feature("setup");
1813   style_header("Raw TH1 Commands");
1814   @ <p><b>Caution:</b> There are no restrictions on the TH1 that can be
1815   @ run by this page.  If Tcl integration was enabled at compile-time and
1816   @ the "tcl" setting is enabled, Tcl commands may be run as well.</p>
1817   @
1818   @ <form method="post" action="%R/admin_th1">
1819   login_insert_csrf_secret();
1820   @ TH1:<br />
1821   @ <textarea name="q" rows="5" cols="80">%h(zQ)</textarea><br />
1822   @ <input type="submit" name="go" value="Run TH1">
1823   @ </form>
1824   if( go ){
1825     const char *zR;
1826     int rc;
1827     int n;
1828     @ <hr />
1829     login_verify_csrf_secret();
1830     rc = Th_Eval(g.interp, 0, zQ, -1);
1831     zR = Th_GetResult(g.interp, &n);
1832     if( rc==TH_OK ){
1833       @ <pre class="th1result">%h(zR)</pre>
1834     }else{
1835       @ <pre class="th1error">%h(zR)</pre>
1836     }
1837   }
1838   style_finish_page();
1839 }
1840 
1841 /*
1842 ** WEBPAGE: admin_log
1843 **
1844 ** Shows the contents of the admin_log table, which is only created if
1845 ** the admin-log setting is enabled. Requires Admin or Setup ('a' or
1846 ** 's') permissions.
1847 */
1848 void page_admin_log(){
1849   Stmt stLog;
1850   int limit;                 /* How many entries to show */
1851   int ofst;                  /* Offset to the first entry */
1852   int fLogEnabled;
1853   int counter = 0;
1854   login_check_credentials();
1855   if( !g.perm.Admin ){
1856     login_needed(0);
1857     return;
1858   }
1859   style_set_current_feature("setup");
1860   style_header("Admin Log");
1861   create_admin_log_table();
1862   limit = atoi(PD("n","200"));
1863   ofst = atoi(PD("x","0"));
1864   fLogEnabled = db_get_boolean("admin-log", 0);
1865   @ <div>Admin logging is %s(fLogEnabled?"on":"off").
1866   @ (Change this on the <a href="setup_settings">settings</a> page.)</div>
1867 
1868   if( ofst>0 ){
1869     int prevx = ofst - limit;
1870     if( prevx<0 ) prevx = 0;
1871     @ <p><a href="admin_log?n=%d(limit)&x=%d(prevx)">[Newer]</a></p>
1872   }
1873   db_prepare(&stLog,
1874     "SELECT datetime(time,'unixepoch'), who, page, what "
1875     "FROM admin_log "
1876     "ORDER BY time DESC");
1877   style_table_sorter();
1878   @ <table class="sortable adminLogTable" width="100%%" \
1879   @  data-column-types='Tttx' data-init-sort='1'>
1880   @ <thead>
1881   @ <th>Time</th>
1882   @ <th>User</th>
1883   @ <th>Page</th>
1884   @ <th width="60%%">Message</th>
1885   @ </thead><tbody>
1886   while( SQLITE_ROW == db_step(&stLog) ){
1887     const char *zTime = db_column_text(&stLog, 0);
1888     const char *zUser = db_column_text(&stLog, 1);
1889     const char *zPage = db_column_text(&stLog, 2);
1890     const char *zMessage = db_column_text(&stLog, 3);
1891     counter++;
1892     if( counter<ofst ) continue;
1893     if( counter>ofst+limit ) break;
1894     @ <tr class="row%d(counter%2)">
1895     @ <td class="adminTime">%s(zTime)</td>
1896     @ <td>%s(zUser)</td>
1897     @ <td>%s(zPage)</td>
1898     @ <td>%h(zMessage)</td>
1899     @ </tr>
1900   }
1901   db_finalize(&stLog);
1902   @ </tbody></table>
1903   if( counter>ofst+limit ){
1904     @ <p><a href="admin_log?n=%d(limit)&x=%d(limit+ofst)">[Older]</a></p>
1905   }
1906   style_finish_page();
1907 }
1908 
1909 /*
1910 ** WEBPAGE: srchsetup
1911 **
1912 ** Configure the search engine.  Requires Admin privilege.
1913 */
1914 void page_srchsetup(){
1915   login_check_credentials();
1916   if( !g.perm.Admin ){
1917     login_needed(0);
1918     return;
1919   }
1920   style_set_current_feature("setup");
1921   style_header("Search Configuration");
1922   @ <form action="%R/srchsetup" method="post"><div>
1923   login_insert_csrf_secret();
1924   @ <div style="text-align:center;font-weight:bold;">
1925   @ Server-specific settings that affect the
1926   @ <a href="%R/search">/search</a> webpage.
1927   @ </div>
1928   @ <hr />
1929   textarea_attribute("Document Glob List", 3, 35, "doc-glob", "dg", "", 0);
1930   @ <p>The "Document Glob List" is a comma- or newline-separated list
1931   @ of GLOB expressions that identify all documents within the source
1932   @ tree that are to be searched when "Document Search" is enabled.
1933   @ Some examples:
1934   @ <table border=0 cellpadding=2 align=center>
1935   @ <tr><td>*.wiki,*.html,*.md,*.txt<td style="width: 4x;">
1936   @ <td>Search all wiki, HTML, Markdown, and Text files</tr>
1937   @ <tr><td>doc/*.md,*/README.txt,README.txt<td>
1938   @ <td>Search all Markdown files in the doc/ subfolder and all README.txt
1939   @ files.</tr>
1940   @ <tr><td>*<td><td>Search all checked-in files</tr>
1941   @ <tr><td><i>(blank)</i><td>
1942   @ <td>Search nothing. (Disables document search).</tr>
1943   @ </table>
1944   @ <hr />
1945   entry_attribute("Document Branch", 20, "doc-branch", "db", "trunk", 0);
1946   @ <p>When searching documents, use the versions of the files found at the
1947   @ type of the "Document Branch" branch.  Recommended value: "trunk".
1948   @ Document search is disabled if blank.
1949   @ <hr />
1950   onoff_attribute("Search Check-in Comments", "search-ci", "sc", 0, 0);
1951   @ <br />
1952   onoff_attribute("Search Documents", "search-doc", "sd", 0, 0);
1953   @ <br />
1954   onoff_attribute("Search Tickets", "search-tkt", "st", 0, 0);
1955   @ <br />
1956   onoff_attribute("Search Wiki", "search-wiki", "sw", 0, 0);
1957   @ <br />
1958   onoff_attribute("Search Tech Notes", "search-technote", "se", 0, 0);
1959   @ <br />
1960   onoff_attribute("Search Forum", "search-forum", "sf", 0, 0);
1961   @ <hr />
1962   @ <p><input type="submit"  name="submit" value="Apply Changes" /></p>
1963   @ <hr />
1964   if( P("fts0") ){
1965     search_drop_index();
1966   }else if( P("fts1") ){
1967     search_drop_index();
1968     search_create_index();
1969     search_fill_index();
1970     search_update_index(search_restrict(SRCH_ALL));
1971   }
1972   if( search_index_exists() ){
1973     @ <p>Currently using an SQLite FTS4 search index. This makes search
1974     @ run faster, especially on large repositories, but takes up space.</p>
1975     onoff_attribute("Use Porter Stemmer","search-stemmer","ss",0,0);
1976     @ <p><input type="submit" name="fts0" value="Delete The Full-Text Index">
1977     @ <input type="submit" name="fts1" value="Rebuild The Full-Text Index">
1978     style_submenu_element("FTS Index Debugging","%R/test-ftsdocs");
1979   }else{
1980     @ <p>The SQLite FTS4 search index is disabled.  All searching will be
1981     @ a full-text scan.  This usually works fine, but can be slow for
1982     @ larger repositories.</p>
1983     onoff_attribute("Use Porter Stemmer","search-stemmer","ss",0,0);
1984     @ <p><input type="submit" name="fts1" value="Create A Full-Text Index">
1985   }
1986   @ </div></form>
1987   style_finish_page();
1988 }
1989 
1990 /*
1991 ** A URL Alias originally called zOldName is now zNewName/zValue.
1992 ** Write SQL to make this change into pSql.
1993 **
1994 ** If zNewName or zValue is an empty string, then delete the entry.
1995 **
1996 ** If zOldName is an empty string, create a new entry.
1997 */
1998 static void setup_update_url_alias(
1999   Blob *pSql,
2000   const char *zOldName,
2001   const char *zNewName,
2002   const char *zValue
2003 ){
2004   if( !cgi_csrf_safe(1) ) return;
2005   if( zNewName[0]==0 || zValue[0]==0 ){
2006     if( zOldName[0] ){
2007       blob_append_sql(pSql,
2008         "DELETE FROM config WHERE name='walias:%q';\n",
2009         zOldName);
2010     }
2011     return;
2012   }
2013   if( zOldName[0]==0 ){
2014     blob_append_sql(pSql,
2015       "INSERT INTO config(name,value,mtime) VALUES('walias:%q',%Q,now());\n",
2016       zNewName, zValue);
2017     return;
2018   }
2019   if( strcmp(zOldName, zNewName)!=0 ){
2020     blob_append_sql(pSql,
2021        "UPDATE config SET name='walias:%q', value=%Q, mtime=now()"
2022        " WHERE name='walias:%q';\n",
2023        zNewName, zValue, zOldName);
2024   }else{
2025     blob_append_sql(pSql,
2026        "UPDATE config SET value=%Q, mtime=now()"
2027        " WHERE name='walias:%q' AND value<>%Q;\n",
2028        zValue, zOldName, zValue);
2029   }
2030 }
2031 
2032 /*
2033 ** WEBPAGE: waliassetup
2034 **
2035 ** Configure the URL aliases
2036 */
2037 void page_waliassetup(){
2038   Stmt q;
2039   int cnt = 0;
2040   Blob namelist;
2041   login_check_credentials();
2042   if( !g.perm.Admin ){
2043     login_needed(0);
2044     return;
2045   }
2046   style_set_current_feature("setup");
2047   style_header("URL Alias Configuration");
2048   if( P("submit")!=0 ){
2049     Blob token;
2050     Blob sql;
2051     const char *zNewName;
2052     const char *zValue;
2053     char zCnt[10];
2054     login_verify_csrf_secret();
2055     blob_init(&namelist, PD("namelist",""), -1);
2056     blob_init(&sql, 0, 0);
2057     while( blob_token(&namelist, &token) ){
2058       const char *zOldName = blob_str(&token);
2059       sqlite3_snprintf(sizeof(zCnt), zCnt, "n%d", cnt);
2060       zNewName = PD(zCnt, "");
2061       sqlite3_snprintf(sizeof(zCnt), zCnt, "v%d", cnt);
2062       zValue = PD(zCnt, "");
2063       setup_update_url_alias(&sql, zOldName, zNewName, zValue);
2064       cnt++;
2065       blob_reset(&token);
2066     }
2067     sqlite3_snprintf(sizeof(zCnt), zCnt, "n%d", cnt);
2068     zNewName = PD(zCnt,"");
2069     sqlite3_snprintf(sizeof(zCnt), zCnt, "v%d", cnt);
2070     zValue = PD(zCnt,"");
2071     setup_update_url_alias(&sql, "", zNewName, zValue);
2072     db_unprotect(PROTECT_CONFIG);
2073     db_multi_exec("%s", blob_sql_text(&sql));
2074     db_protect_pop();
2075     blob_reset(&sql);
2076     blob_reset(&namelist);
2077     cnt = 0;
2078   }
2079   db_prepare(&q,
2080       "SELECT substr(name,8), value FROM config WHERE name GLOB 'walias:/*'"
2081       " UNION ALL SELECT '', ''"
2082   );
2083   @ <form action="%R/waliassetup" method="post"><div>
2084   login_insert_csrf_secret();
2085   @ <table border=0 cellpadding=5>
2086   @ <tr><th>Alias<th>URI That The Alias Maps Into
2087   blob_init(&namelist, 0, 0);
2088   while( db_step(&q)==SQLITE_ROW ){
2089     const char *zName = db_column_text(&q, 0);
2090     const char *zValue = db_column_text(&q, 1);
2091     @ <tr><td>
2092     @ <input type='text' size='20' value='%h(zName)' name='n%d(cnt)'>
2093     @ </td><td>
2094     @ <input type='text' size='80' value='%h(zValue)' name='v%d(cnt)'>
2095     @ </td></tr>
2096     cnt++;
2097     if( blob_size(&namelist)>0 ) blob_append(&namelist, " ", 1);
2098     blob_append(&namelist, zName, -1);
2099   }
2100   db_finalize(&q);
2101   @ <tr><td>
2102   @ <input type='hidden' name='namelist' value='%h(blob_str(&namelist))'>
2103   @ <input type='submit' name='submit' value="Apply Changes">
2104   @ </td><td></td></tr>
2105   @ </table></form>
2106   @ <hr>
2107   @ <p>When the first term of an incoming URL exactly matches one of
2108   @ the "Aliases" on the left-hand side (LHS) above, the URL is
2109   @ converted into the corresponding form on the right-hand side (RHS).
2110   @ <ul>
2111   @ <li><p>
2112   @ The LHS is compared against only the first term of the incoming URL.
2113   @ All LHS entries in the alias table should therefore begin with a
2114   @ single "/" followed by a single path element.
2115   @ <li><p>
2116   @ The RHS entries in the alias table should begin with a single "/"
2117   @ followed by a path element, and optionally followed by "?" and a
2118   @ list of query parameters.
2119   @ <li><p>
2120   @ Query parameters on the RHS are added to the set of query parameters
2121   @ in the incoming URL.
2122   @ <li><p>
2123   @ If the same query parameter appears in both the incoming URL and
2124   @ on the RHS of the alias, the RHS query parameter value overwrites
2125   @ the value on the incoming URL.
2126   @ <li><p>
2127   @ If a query parameter on the RHS of the alias is of the form "X!"
2128   @ (a name followed by "!") then the X query parameter is removed
2129   @ from the incoming URL if
2130   @ it exists.
2131   @ <li><p>
2132   @ Only a single alias operation occurs.  It is not possible to nest aliases.
2133   @ The RHS entries must be built-in webpage names.
2134   @ <li><p>
2135   @ The alias table is only checked if no built-in webpage matches
2136   @ the incoming URL.
2137   @ Hence, it is not possible to override a built-in webpage using aliases.
2138   @ This is by design.
2139   @ </ul>
2140   @
2141   @ <p>To delete an entry from the alias table, change its name or value to an
2142   @ empty string and press "Apply Changes".
2143   @
2144   @ <p>To add a new alias, fill in the name and value in the bottom row
2145   @ of the table above and press "Apply Changes".
2146   style_finish_page();
2147 }
2148