1 /*
2 ** Copyright (c) 2006,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 ** This file contains code to implement the basic web page look and feel.
19 **
20 */
21 #include "VERSION.h"
22 #include "config.h"
23 #include "style.h"
24 
25 /*
26 ** Elements of the submenu are collected into the following
27 ** structure and displayed below the main menu.
28 **
29 ** Populate these structure with calls to
30 **
31 **      style_submenu_element()
32 **      style_submenu_entry()
33 **      style_submenu_checkbox()
34 **      style_submenu_binary()
35 **      style_submenu_multichoice()
36 **      style_submenu_sql()
37 **
38 ** prior to calling style_finish_page().  The style_finish_page() routine
39 ** will generate the appropriate HTML text just below the main
40 ** menu.
41 */
42 static struct Submenu {
43   const char *zLabel;        /* Button label */
44   const char *zLink;         /* Jump to this link when button is pressed */
45 } aSubmenu[30];
46 static int nSubmenu = 0;     /* Number of buttons */
47 static struct SubmenuCtrl {
48   const char *zName;           /* Form query parameter */
49   const char *zLabel;          /* Label.  Might be NULL for FF_MULTI */
50   unsigned char eType;         /* FF_ENTRY, FF_MULTI, FF_CHECKBOX */
51   unsigned char eVisible;      /* STYLE_NORMAL or STYLE_DISABLED */
52   short int iSize;             /* Width for FF_ENTRY.  Count for FF_MULTI */
53   const char *const *azChoice; /* value/display pairs for FF_MULTI */
54   const char *zFalse;          /* FF_BINARY label when false */
55   const char *zJS;             /* Javascript to run on toggle */
56 } aSubmenuCtrl[20];
57 static int nSubmenuCtrl = 0;
58 #define FF_ENTRY    1          /* Text entry box */
59 #define FF_MULTI    2          /* Combobox.  Multiple choices. */
60 #define FF_BINARY   3          /* Control for binary query parameter */
61 #define FF_CHECKBOX 4          /* Check-box */
62 
63 #if INTERFACE
64 #define STYLE_NORMAL   0       /* Normal display of control */
65 #define STYLE_DISABLED 1       /* Control is disabled */
66 #endif /* INTERFACE */
67 
68 /*
69 ** Remember that the header has been generated.  The footer is omitted
70 ** if an error occurs before the header.
71 */
72 static int headerHasBeenGenerated = 0;
73 
74 /*
75 ** remember, if a sidebox was used
76 */
77 static int sideboxUsed = 0;
78 
79 /*
80 ** Ad-unit styles.
81 */
82 static unsigned adUnitFlags = 0;
83 
84 /*
85 ** Submenu disable flag
86 */
87 static int submenuEnable = 1;
88 
89 /*
90 ** Disable content-security-policy.
91 ** Warning:  Do not disable the CSP without careful consideration!
92 */
93 static int disableCSP = 0;
94 
95 /*
96 ** Flags for various javascript files needed prior to </body>
97 */
98 static int needHrefJs = 0;      /* href.js */
99 
100 /*
101 ** Extra JS added to the end of the file.
102 */
103 static Blob blobOnLoad = BLOB_INITIALIZER;
104 
105 /*
106 ** Generate and return a anchor tag like this:
107 **
108 **        <a href="URL">
109 **  or    <a id="ID">
110 **
111 ** The form of the anchor tag is determined by the g.javascriptHyperlink
112 ** and g.perm.Hyperlink variables.
113 **
114 **   g.perm.Hyperlink  g.javascriptHyperlink        Returned anchor format
115 **   ----------------  ---------------------        ------------------------
116 **          0                    0                  (empty string)
117 **          0                    1                  (empty string)
118 **          1                    0                  <a href="URL">
119 **          1                    1                  <a id="ID">
120 **
121 ** No anchor tag is generated if g.perm.Hyperlink is false.
122 ** The href="URL" form is used if g.javascriptHyperlink is false.
123 ** If g.javascriptHyperlink is true then the id="ID" form is used and
124 ** javascript is generated in the footer to cause href values to be
125 ** inserted after the page has loaded. The use of the id="ID" form
126 ** instead of href="URL" is a defense against bots.
127 **
128 ** If the user lacks the Hyperlink (h) property and the "auto-hyperlink"
129 ** setting is true, then g.perm.Hyperlink is changed from 0 to 1 and
130 ** g.javascriptHyperlink is set to 1 by login_check_credentials().  Thus
131 ** the g.perm.Hyperlink property will be true even if the user does not
132 ** have the "h" privilege if the "auto-hyperlink" setting is true.
133 **
134 **  User has "h"  auto-hyperlink      g.perm.Hyperlink  g.javascriptHyperlink
135 **  ------------  --------------      ----------------  ---------------------
136 **        0             0                    0                    0
137 **        1             0                    1                    0
138 **        0             1                    1                    1
139 **        1             1                    1                    0
140 **
141 ** So, in other words, tracing input configuration to final actions we have:
142 **
143 **  User has "h"  auto-hyperlink      Returned anchor format
144 **  ------------  --------------      ----------------------
145 **        0             0             (empty string)
146 **        1             0             <a href="URL">
147 **        0             1             <a id="ID">
148 **        1             1             (can't happen)
149 **
150 ** The name of these routines are deliberately kept short so that can be
151 ** easily used within @-lines.  Example:
152 **
153 **      @ %z(href("%R/artifact/%s",zUuid))%h(zFN)</a>
154 **
155 ** Note %z format.  The string returned by this function is always
156 ** obtained from fossil_malloc() so rendering it with %z will reclaim
157 ** that memory space.
158 **
159 ** There are three versions of this routine:
160 **
161 **    (1)   href() does a plain hyperlink
162 **    (2)   xhref() adds extra attribute text
163 **    (3)   chref() adds a class name
164 **
165 ** g.perm.Hyperlink is true if the user has the Hyperlink (h) property.
166 ** Most logged in users should have this property, since we can assume
167 ** that a logged in user is not a bot.  Only "nobody" lacks g.perm.Hyperlink,
168 ** typically.
169 */
xhref(const char * zExtra,const char * zFormat,...)170 char *xhref(const char *zExtra, const char *zFormat, ...){
171   char *zUrl;
172   va_list ap;
173   if( !g.perm.Hyperlink ) return fossil_strdup("");
174   va_start(ap, zFormat);
175   zUrl = vmprintf(zFormat, ap);
176   va_end(ap);
177   if( !g.javascriptHyperlink ){
178     char *zHUrl;
179     if( zExtra ){
180       zHUrl = mprintf("<a %s href=\"%h\">", zExtra, zUrl);
181     }else{
182       zHUrl = mprintf("<a href=\"%h\">", zUrl);
183     }
184     fossil_free(zUrl);
185     return zHUrl;
186   }
187   needHrefJs = 1;
188   if( zExtra==0 ){
189     return mprintf("<a data-href='%z' href='%R/honeypot'>", zUrl);
190   }else{
191     return mprintf("<a %s data-href='%z' href='%R/honeypot'>",
192                    zExtra, zUrl);
193   }
194 }
chref(const char * zExtra,const char * zFormat,...)195 char *chref(const char *zExtra, const char *zFormat, ...){
196   char *zUrl;
197   va_list ap;
198   if( !g.perm.Hyperlink ) return fossil_strdup("");
199   va_start(ap, zFormat);
200   zUrl = vmprintf(zFormat, ap);
201   va_end(ap);
202   if( !g.javascriptHyperlink ){
203     char *zHUrl = mprintf("<a class=\"%s\" href=\"%h\">", zExtra, zUrl);
204     fossil_free(zUrl);
205     return zHUrl;
206   }
207   needHrefJs = 1;
208   return mprintf("<a class='%s' data-href='%z' href='%R/honeypot'>",
209                  zExtra, zUrl);
210 }
href(const char * zFormat,...)211 char *href(const char *zFormat, ...){
212   char *zUrl;
213   va_list ap;
214   if( !g.perm.Hyperlink ) return fossil_strdup("");
215   va_start(ap, zFormat);
216   zUrl = vmprintf(zFormat, ap);
217   va_end(ap);
218   if( !g.javascriptHyperlink ){
219     char *zHUrl = mprintf("<a href=\"%h\">", zUrl);
220     fossil_free(zUrl);
221     return zHUrl;
222   }
223   needHrefJs = 1;
224   return mprintf("<a data-href='%s' href='%R/honeypot'>",
225                   zUrl);
226 }
227 
228 /*
229 ** Generate <form method="post" action=ARG>.  The ARG value is determined
230 ** by the arguments.
231 **
232 ** As a defense against robots, the action=ARG might instead by data-action=ARG
233 ** and javascript (href.js) added to the page so that the data-action= is
234 ** changed into action= after the page loads.  Whether or not this happens
235 ** depends on if the user has the "h" privilege and whether or not the
236 ** auto-hyperlink setting is on.  These setings determine the values of
237 ** variables g.perm.Hyperlink and g.javascriptHyperlink.
238 **
239 **    User has "h"  auto-hyperlink      g.perm.Hyperlink  g.javascriptHyperlink
240 **    ------------  --------------      ----------------  ---------------------
241 **  1:      0             0                    0                    0
242 **  2:      1             0                    1                    0
243 **  3:      0             1                    1                    1
244 **  4:      1             1                    1                    0
245 **
246 ** The data-action=ARG form is used for cases 1 and 3.  In case 1, the href.js
247 ** javascript is omitted and so the form is effectively disabled.
248 */
form_begin(const char * zOtherArgs,const char * zAction,...)249 void form_begin(const char *zOtherArgs, const char *zAction, ...){
250   char *zLink;
251   va_list ap;
252   if( zOtherArgs==0 ) zOtherArgs = "";
253   va_start(ap, zAction);
254   zLink = vmprintf(zAction, ap);
255   va_end(ap);
256   if( g.perm.Hyperlink ){
257     @ <form method="POST" action="%z(zLink)" %s(zOtherArgs)>
258   }else{
259     needHrefJs = 1;
260     @ <form method="POST" data-action='%s(zLink)' action='%R/login' \
261     @ %s(zOtherArgs)>
262   }
263 }
264 
265 /*
266 ** Add a new element to the submenu
267 */
style_submenu_element(const char * zLabel,const char * zLink,...)268 void style_submenu_element(
269   const char *zLabel,
270   const char *zLink,
271   ...
272 ){
273   va_list ap;
274   assert( nSubmenu < count(aSubmenu) );
275   aSubmenu[nSubmenu].zLabel = zLabel;
276   va_start(ap, zLink);
277   aSubmenu[nSubmenu].zLink = vmprintf(zLink, ap);
278   va_end(ap);
279   nSubmenu++;
280 }
style_submenu_entry(const char * zName,const char * zLabel,int iSize,int eVisible)281 void style_submenu_entry(
282   const char *zName,       /* Query parameter name */
283   const char *zLabel,      /* Label before the entry box */
284   int iSize,               /* Size of the entry box */
285   int eVisible             /* Visible or disabled */
286 ){
287   assert( nSubmenuCtrl < count(aSubmenuCtrl) );
288   aSubmenuCtrl[nSubmenuCtrl].zName = zName;
289   aSubmenuCtrl[nSubmenuCtrl].zLabel = zLabel;
290   aSubmenuCtrl[nSubmenuCtrl].iSize = iSize;
291   aSubmenuCtrl[nSubmenuCtrl].eVisible = eVisible;
292   aSubmenuCtrl[nSubmenuCtrl].eType = FF_ENTRY;
293   nSubmenuCtrl++;
294 }
style_submenu_checkbox(const char * zName,const char * zLabel,int eVisible,const char * zJS)295 void style_submenu_checkbox(
296   const char *zName,       /* Query parameter name */
297   const char *zLabel,      /* Label to display after the checkbox */
298   int eVisible,            /* Visible or disabled */
299   const char *zJS          /* Optional javascript to run on toggle */
300 ){
301   assert( nSubmenuCtrl < count(aSubmenuCtrl) );
302   aSubmenuCtrl[nSubmenuCtrl].zName = zName;
303   aSubmenuCtrl[nSubmenuCtrl].zLabel = zLabel;
304   aSubmenuCtrl[nSubmenuCtrl].eVisible = eVisible;
305   aSubmenuCtrl[nSubmenuCtrl].zJS = zJS;
306   aSubmenuCtrl[nSubmenuCtrl].eType = FF_CHECKBOX;
307   nSubmenuCtrl++;
308 }
style_submenu_binary(const char * zName,const char * zTrue,const char * zFalse,int eVisible)309 void style_submenu_binary(
310   const char *zName,       /* Query parameter name */
311   const char *zTrue,       /* Label to show when parameter is true */
312   const char *zFalse,      /* Label to show when the parameter is false */
313   int eVisible             /* Visible or disabled */
314 ){
315   assert( nSubmenuCtrl < count(aSubmenuCtrl) );
316   aSubmenuCtrl[nSubmenuCtrl].zName = zName;
317   aSubmenuCtrl[nSubmenuCtrl].zLabel = zTrue;
318   aSubmenuCtrl[nSubmenuCtrl].zFalse = zFalse;
319   aSubmenuCtrl[nSubmenuCtrl].eVisible = eVisible;
320   aSubmenuCtrl[nSubmenuCtrl].eType = FF_BINARY;
321   nSubmenuCtrl++;
322 }
style_submenu_multichoice(const char * zName,int nChoice,const char * const * azChoice,int eVisible)323 void style_submenu_multichoice(
324   const char *zName,           /* Query parameter name */
325   int nChoice,                 /* Number of options */
326   const char *const *azChoice, /* value/display pairs.  2*nChoice entries */
327   int eVisible                 /* Visible or disabled */
328 ){
329   assert( nSubmenuCtrl < count(aSubmenuCtrl) );
330   aSubmenuCtrl[nSubmenuCtrl].zName = zName;
331   aSubmenuCtrl[nSubmenuCtrl].iSize = nChoice;
332   aSubmenuCtrl[nSubmenuCtrl].azChoice = azChoice;
333   aSubmenuCtrl[nSubmenuCtrl].eVisible = eVisible;
334   aSubmenuCtrl[nSubmenuCtrl].eType = FF_MULTI;
335   nSubmenuCtrl++;
336 }
style_submenu_sql(const char * zName,const char * zLabel,const char * zFormat,...)337 void style_submenu_sql(
338   const char *zName,       /* Query parameter name */
339   const char *zLabel,      /* Label on the control */
340   const char *zFormat,     /* Format string for SQL command for choices */
341   ...                      /* Arguments to the format string */
342 ){
343   Stmt q;
344   int n = 0;
345   int nAlloc = 0;
346   char **az = 0;
347   va_list ap;
348 
349   va_start(ap, zFormat);
350   db_vprepare(&q, 0, zFormat, ap);
351   va_end(ap);
352   while( SQLITE_ROW==db_step(&q) ){
353     if( n+2>=nAlloc ){
354       nAlloc += nAlloc + 20;
355       az = fossil_realloc(az, sizeof(char*)*nAlloc);
356     }
357     az[n++] = fossil_strdup(db_column_text(&q,0));
358     az[n++] = fossil_strdup(db_column_text(&q,1));
359   }
360   db_finalize(&q);
361   if( n>0 ){
362     aSubmenuCtrl[nSubmenuCtrl].zName = zName;
363     aSubmenuCtrl[nSubmenuCtrl].zLabel = zLabel;
364     aSubmenuCtrl[nSubmenuCtrl].iSize = n/2;
365     aSubmenuCtrl[nSubmenuCtrl].azChoice = (const char *const *)az;
366     aSubmenuCtrl[nSubmenuCtrl].eVisible = STYLE_NORMAL;
367     aSubmenuCtrl[nSubmenuCtrl].eType = FF_MULTI;
368     nSubmenuCtrl++;
369   }
370 }
371 
372 /*
373 ** Disable or enable the submenu
374 */
style_submenu_enable(int onOff)375 void style_submenu_enable(int onOff){
376   submenuEnable = onOff;
377 }
378 
379 
380 /*
381 ** Compare two submenu items for sorting purposes
382 */
submenuCompare(const void * a,const void * b)383 static int submenuCompare(const void *a, const void *b){
384   const struct Submenu *A = (const struct Submenu*)a;
385   const struct Submenu *B = (const struct Submenu*)b;
386   return fossil_strcmp(A->zLabel, B->zLabel);
387 }
388 
389 /* Use this for the $current_page variable if it is not NULL.  If it
390 ** is NULL then use g.zPath.
391 */
392 static char *local_zCurrentPage = 0;
393 
394 /*
395 ** Set the desired $current_page to something other than g.zPath
396 */
style_set_current_page(const char * zFormat,...)397 void style_set_current_page(const char *zFormat, ...){
398   fossil_free(local_zCurrentPage);
399   if( zFormat==0 ){
400     local_zCurrentPage = 0;
401   }else{
402     va_list ap;
403     va_start(ap, zFormat);
404     local_zCurrentPage = vmprintf(zFormat, ap);
405     va_end(ap);
406   }
407 }
408 
409 /*
410 ** Create a TH1 variable containing the URL for the stylesheet.
411 **
412 ** The name of the new variable will be "stylesheet_url".
413 **
414 ** The value will be a URL for accessing the appropriate stylesheet.
415 ** This URL will include query parameters such as "id=" and "once&skin="
416 ** to cause the correct stylesheet to be loaded after a skin change
417 ** or after a change to the stylesheet.
418 */
stylesheet_url_var(void)419 static void stylesheet_url_var(void){
420   char *zBuiltin;              /* Auxiliary page-specific CSS page */
421   Blob url;                    /* The URL */
422 
423   /* Initialize the URL to its baseline */
424   url = empty_blob;
425   blob_appendf(&url, "%R/style.css");
426 
427   /* If page-specific CSS exists for the current page, then append
428   ** the pathname for the page-specific CSS.  The default CSS is
429   **
430   **     /style.css
431   **
432   ** But for the "/wikiedit" page (to name but one example), we
433   ** append a path as follows:
434   **
435   **     /style.css/wikiedit
436   **
437   ** The /style.css page (implemented below) will detect this extra "wikiedit"
438   ** path information and include the page-specific CSS along with the
439   ** default CSS when it delivers the page.
440   */
441   zBuiltin = mprintf("style.%s.css", g.zPath);
442   if( builtin_file(zBuiltin,0)!=0 ){
443     blob_appendf(&url, "/%s", g.zPath);
444   }
445   fossil_free(zBuiltin);
446 
447   /* Add query parameters that will change whenever the skin changes
448   ** or after any updates to the CSS files
449   */
450   blob_appendf(&url, "?id=%x", skin_id("css"));
451   if( P("once")!=0 && P("skin")!=0 ){
452     blob_appendf(&url, "&skin=%s&once", skin_in_use());
453   }
454 
455   /* Generate the CSS URL variable */
456   Th_Store("stylesheet_url", blob_str(&url));
457   blob_reset(&url);
458 }
459 
460 /*
461 ** Create a TH1 variable containing the URL for the specified image.
462 ** The resulting variable name will be of the form $[zImageName]_image_url.
463 ** The value will be a URL that includes an id= query parameter that
464 ** changes if the underlying resource changes or if a different skin
465 ** is selected.
466 */
image_url_var(const char * zImageName)467 static void image_url_var(const char *zImageName){
468   char *zVarName;   /* Name of the new TH1 variable */
469   char *zResource;  /* Name of CONFIG entry holding content */
470   char *zUrl;       /* The URL */
471 
472   zResource = mprintf("%s-image", zImageName);
473   zUrl = mprintf("%R/%s?id=%x", zImageName, skin_id(zResource));
474   free(zResource);
475   zVarName = mprintf("%s_image_url", zImageName);
476   Th_Store(zVarName, zUrl);
477   free(zVarName);
478   free(zUrl);
479 }
480 
481 /*
482 ** Output TEXT with a click-to-copy button next to it. Loads the copybtn.js
483 ** Javascript module, and generates HTML elements with the following IDs:
484 **
485 **    TARGETID:       The <span> wrapper around TEXT.
486 **    copy-TARGETID:  The <span> for the copy button.
487 **
488 ** If the FLIPPED argument is non-zero, the copy button is displayed after TEXT.
489 **
490 ** The COPYLENGTH argument defines the length of the substring of TEXT copied to
491 ** clipboard:
492 **
493 **    <= 0:   No limit (default if the argument is omitted).
494 **    >= 3:   Truncate TEXT after COPYLENGTH (single-byte) characters.
495 **       1:   Use the "hash-digits" setting as the limit.
496 **       2:   Use the length appropriate for URLs as the limit (defined at
497 **            compile-time by FOSSIL_HASH_DIGITS_URL, defaults to 16).
498 */
style_copy_button(int bOutputCGI,const char * zTargetId,int bFlipped,int cchLength,const char * zTextFmt,...)499 char *style_copy_button(
500   int bOutputCGI,         /* Don't return result, but send to cgi_printf(). */
501   const char *zTargetId,  /* The TARGETID argument. */
502   int bFlipped,           /* The FLIPPED argument. */
503   int cchLength,          /* The COPYLENGTH argument. */
504   const char *zTextFmt,   /* Formatting of the TEXT argument (htmlized). */
505   ...                     /* Formatting parameters of the TEXT argument. */
506 ){
507   va_list ap;
508   char *zText;
509   char *zResult = 0;
510   va_start(ap,zTextFmt);
511   zText = vmprintf(zTextFmt/*works-like:?*/,ap);
512   va_end(ap);
513   if( cchLength==1 ) cchLength = hash_digits(0);
514   else if( cchLength==2 ) cchLength = hash_digits(1);
515   if( !bFlipped ){
516     const char *zBtnFmt =
517       "<span class=\"nobr\">"
518       "<span "
519       "class=\"copy-button\" "
520       "id=\"copy-%h\" "
521       "data-copytarget=\"%h\" "
522       "data-copylength=\"%d\">"
523       "</span>"
524       "<span id=\"%h\">"
525       "%s"
526       "</span>"
527       "</span>";
528     if( bOutputCGI ){
529       cgi_printf(
530                   zBtnFmt/*works-like:"%h%h%d%h%s"*/,
531                   zTargetId,zTargetId,cchLength,zTargetId,zText);
532     }else{
533       zResult = mprintf(
534                   zBtnFmt/*works-like:"%h%h%d%h%s"*/,
535                   zTargetId,zTargetId,cchLength,zTargetId,zText);
536     }
537   }else{
538     const char *zBtnFmt =
539       "<span class=\"nobr\">"
540       "<span id=\"%h\">"
541       "%s"
542       "</span>"
543       "<span "
544       "class=\"copy-button copy-button-flipped\" "
545       "id=\"copy-%h\" "
546       "data-copytarget=\"%h\" "
547       "data-copylength=\"%d\">"
548       "</span>"
549       "</span>";
550     if( bOutputCGI ){
551       cgi_printf(
552                   zBtnFmt/*works-like:"%h%s%h%h%d"*/,
553                   zTargetId,zText,zTargetId,zTargetId,cchLength);
554     }else{
555       zResult = mprintf(
556                   zBtnFmt/*works-like:"%h%s%h%h%d"*/,
557                   zTargetId,zText,zTargetId,zTargetId,cchLength);
558     }
559   }
560   free(zText);
561   builtin_request_js("copybtn.js");
562   return zResult;
563 }
564 
565 /*
566 ** Return a random nonce that is stored in static space.  For a particular
567 ** run, the same nonce is always returned.
568 */
style_nonce(void)569 char *style_nonce(void){
570   static char zNonce[52];
571   if( zNonce[0]==0 ){
572     unsigned char zSeed[24];
573     sqlite3_randomness(24, zSeed);
574     encode16(zSeed,(unsigned char*)zNonce,24);
575   }
576   return zNonce;
577 }
578 
579 /*
580 ** Return the default Content Security Policy (CSP) string.
581 ** If the toHeader argument is true, then also add the
582 ** CSP to the HTTP reply header.
583 **
584 ** The CSP comes from the "default-csp" setting if it exists and
585 ** is non-empty.  If that setting is an empty string, then the following
586 ** default is used instead:
587 **
588 **     default-src 'self' data:;
589 **     script-src 'self' 'nonce-$nonce';
590 **     style-src 'self' 'unsafe-inline';
591 **     img-src * data:;
592 **
593 ** The text '$nonce' is replaced by style_nonce() if and whereever it
594 ** occurs in the input string.
595 **
596 ** The string returned is obtained from fossil_malloc() and
597 ** should be released by the caller.
598 */
style_csp(int toHeader)599 char *style_csp(int toHeader){
600   static const char zBackupCSP[] =
601    "default-src 'self' data:; "
602    "script-src 'self' 'nonce-$nonce'; "
603    "style-src 'self' 'unsafe-inline'; "
604    "img-src * data:";
605   const char *zFormat;
606   Blob csp;
607   char *zNonce;
608   char *zCsp;
609   int i;
610   if( disableCSP ) return fossil_strdup("");
611   zFormat = db_get("default-csp","");
612   if( zFormat[0]==0 ){
613     zFormat = zBackupCSP;
614   }
615   blob_init(&csp, 0, 0);
616   while( zFormat[0] && (zNonce = strstr(zFormat,"$nonce"))!=0 ){
617     blob_append(&csp, zFormat, (int)(zNonce - zFormat));
618     blob_append(&csp, style_nonce(), -1);
619     zFormat = zNonce + 6;
620   }
621   blob_append(&csp, zFormat, -1);
622   zCsp = blob_str(&csp);
623   /* No whitespace other than actual space characters allowed in the CSP
624   ** string.  See https://fossil-scm.org/forum/forumpost/d29e3af43c */
625   for(i=0; zCsp[i]; i++){ if( fossil_isspace(zCsp[i]) ) zCsp[i] = ' '; }
626   if( toHeader ){
627     cgi_printf_header("Content-Security-Policy: %s\r\n", zCsp);
628   }
629   return zCsp;
630 }
631 
632 /*
633 ** Disable content security policy for the current page.
634 ** WARNING:  Do not do this lightly!
635 **
636 ** This routine must be called before the CSP is sued by
637 ** style_header().
638 */
style_disable_csp(void)639 void style_disable_csp(void){
640   disableCSP = 1;
641 }
642 
643 /*
644 ** Default HTML page header text through <body>.  If the repository-specific
645 ** header template lacks a <body> tag, then all of the following is
646 ** prepended.
647 */
648 static const char zDfltHeader[] =
649 @ <html>
650 @ <head>
651 @ <base href="$baseurl/$current_page" />
652 @ <meta charset="UTF-8">
653 @ <meta http-equiv="Content-Security-Policy" content="$default_csp" />
654 @ <meta name="viewport" content="width=device-width, initial-scale=1.0">
655 @ <title>$<project_name>: $<title></title>
656 @ <link rel="alternate" type="application/rss+xml" title="RSS Feed" \
657 @  href="$home/timeline.rss" />
658 @ <link rel="stylesheet" href="$stylesheet_url" type="text/css" />
659 @ </head>
660 @ <body class="$current_feature">
661 ;
662 
663 /*
664 ** Returns the default page header.
665 */
get_default_header()666 const char *get_default_header(){
667   return zDfltHeader;
668 }
669 
670 /*
671 ** The default TCL list that defines the main menu.
672 */
673 static const char zDfltMainMenu[] =
674 @ Home      /home        *              {}
675 @ Timeline  /timeline    {o r j}        {}
676 @ Files     /dir?ci=tip  oh             desktoponly
677 @ Branches  /brlist      o              wideonly
678 @ Tags      /taglist     o              wideonly
679 @ Forum     /forum       {@2 3 4 5 6}   wideonly
680 @ Chat      /chat        C              wideonly
681 @ Tickets   /ticket      r              wideonly
682 @ Wiki      /wiki        j              wideonly
683 @ Admin     /setup       {a s}          desktoponly
684 @ Logout    /logout      L              wideonly
685 @ Login     /login       !L             wideonly
686 ;
687 
688 /*
689 ** Return the default menu
690 */
style_default_mainmenu(void)691 const char *style_default_mainmenu(void){
692   return zDfltMainMenu;
693 }
694 
695 /*
696 ** Given a URL path, extract the first element as a "feature" name,
697 ** used as the <body class="FEATURE"> value by default, though
698 ** later-running code may override this, typically to group multiple
699 ** Fossil UI URLs into a single "feature" so you can have per-feature
700 ** CSS rules.
701 **
702 ** For example, "body.forum div.markdown blockquote" targets only
703 ** block quotes made in forum posts, leaving other Markdown quotes
704 ** alone.  Because feature class "forum" groups /forummain, /forumpost,
705 ** and /forume2, it works across all renderings of Markdown to HTML
706 ** within the Fossil forum feature.
707 */
feature_from_page_path(const char * zPath)708 static const char* feature_from_page_path(const char *zPath){
709   const char* zSlash = strchr(zPath, '/');
710   if (zSlash) {
711     return fossil_strndup(zPath, zSlash - zPath);
712   } else {
713     return zPath;
714   }
715 }
716 
717 /*
718 ** Override the value of the TH1 variable current_feature, its default
719 ** set by feature_from_page_path().  We do not call this from
720 ** style_init_th1_vars() because that uses Th_MaybeStore() instead to
721 ** allow webpage implementations to call this before style_header()
722 ** to override that "maybe" default with something better.
723 */
style_set_current_feature(const char * zFeature)724 void style_set_current_feature(const char* zFeature){
725   Th_Store("current_feature", zFeature);
726 }
727 
728 /*
729 ** Returns the current mainmenu value from either the --mainmenu flag
730 ** (handled by the server/ui/cgi commands), the "mainmenu" config
731 ** setting, or style_default_mainmenu(), in that order, returning the
732 ** first of those which is defined.
733 */
style_get_mainmenu()734 const char*style_get_mainmenu(){
735   static const char *zMenu = 0;
736   if(!zMenu){
737     if(g.zMainMenuFile){
738       Blob b = empty_blob;
739       blob_read_from_file(&b, g.zMainMenuFile, ExtFILE);
740       zMenu = blob_str(&b);
741     }else{
742       zMenu = db_get("mainmenu", style_default_mainmenu());
743     }
744   }
745   return zMenu;
746 }
747 
748 /*
749 ** Initialize all the default TH1 variables
750 */
style_init_th1_vars(const char * zTitle)751 static void style_init_th1_vars(const char *zTitle){
752   const char *zNonce = style_nonce();
753   char *zDfltCsp;
754 
755   zDfltCsp = style_csp(1);
756   /*
757   ** Do not overwrite the TH1 variable "default_csp" if it exists, as this
758   ** allows it to be properly overridden via the TH1 setup script (i.e. it
759   ** is evaluated before the header is rendered).
760   */
761   Th_MaybeStore("default_csp", zDfltCsp);
762   fossil_free(zDfltCsp);
763   Th_Store("nonce", zNonce);
764   Th_Store("project_name", db_get("project-name","Unnamed Fossil Project"));
765   Th_Store("project_description", db_get("project-description",""));
766   if( zTitle ) Th_Store("title", zTitle);
767   Th_Store("baseurl", g.zBaseURL);
768   Th_Store("secureurl", fossil_wants_https(1)? g.zHttpsURL: g.zBaseURL);
769   Th_Store("home", g.zTop);
770   Th_Store("index_page", db_get("index-page","/home"));
771   if( local_zCurrentPage==0 ) style_set_current_page("%T", g.zPath);
772   Th_Store("current_page", local_zCurrentPage);
773   Th_Store("csrf_token", g.zCsrfToken);
774   Th_Store("release_version", RELEASE_VERSION);
775   Th_Store("manifest_version", MANIFEST_VERSION);
776   Th_Store("manifest_date", MANIFEST_DATE);
777   Th_Store("compiler_name", COMPILER_NAME);
778   Th_Store("mainmenu", style_get_mainmenu());
779   stylesheet_url_var();
780   image_url_var("logo");
781   image_url_var("background");
782   if( !login_is_nobody() ){
783     Th_Store("login", g.zLogin);
784   }
785   Th_MaybeStore("current_feature", feature_from_page_path(local_zCurrentPage) );
786 }
787 
788 /*
789 ** Draw the header.
790 */
style_header(const char * zTitleFormat,...)791 void style_header(const char *zTitleFormat, ...){
792   va_list ap;
793   char *zTitle;
794   const char *zHeader = skin_get("header");
795   login_check_credentials();
796 
797   va_start(ap, zTitleFormat);
798   zTitle = vmprintf(zTitleFormat, ap);
799   va_end(ap);
800 
801   cgi_destination(CGI_HEADER);
802 
803   @ <!DOCTYPE html>
804 
805   if( g.thTrace ) Th_Trace("BEGIN_HEADER<br />\n", -1);
806 
807   /* Generate the header up through the main menu */
808   style_init_th1_vars(zTitle);
809   if( sqlite3_strlike("%<body%", zHeader, 0)!=0 ){
810     Th_Render(zDfltHeader);
811   }
812   if( g.thTrace ) Th_Trace("BEGIN_HEADER_SCRIPT<br />\n", -1);
813   Th_Render(zHeader);
814   if( g.thTrace ) Th_Trace("END_HEADER<br />\n", -1);
815   Th_Unstore("title");   /* Avoid collisions with ticket field names */
816   cgi_destination(CGI_BODY);
817   g.cgiOutput = 1;
818   headerHasBeenGenerated = 1;
819   sideboxUsed = 0;
820   if( g.perm.Debug && P("showqp") ){
821     @ <div class="debug">
822     cgi_print_all(0, 0);
823     @ </div>
824   }
825 }
826 
827 #if INTERFACE
828 /* Allowed parameters for style_adunit() */
829 #define ADUNIT_OFF        0x0001       /* Do not allow ads on this page */
830 #define ADUNIT_RIGHT_OK   0x0002       /* Right-side vertical ads ok here */
831 #endif
832 
833 /*
834 ** Various page implementations can invoke this interface to let the
835 ** style manager know what kinds of ads are appropriate for this page.
836 */
style_adunit_config(unsigned int mFlags)837 void style_adunit_config(unsigned int mFlags){
838   adUnitFlags = mFlags;
839 }
840 
841 /*
842 ** Return the text of an ad-unit, if one should be rendered.  Return
843 ** NULL if no ad-unit is desired.
844 **
845 ** The *pAdFlag value might be set to ADUNIT_RIGHT_OK if this is
846 ** a right-hand vertical ad.
847 */
style_adunit_text(unsigned int * pAdFlag)848 static const char *style_adunit_text(unsigned int *pAdFlag){
849   const char *zAd = 0;
850   *pAdFlag = 0;
851   if( adUnitFlags & ADUNIT_OFF ) return 0;  /* Disallow ads on this page */
852   if( db_get_boolean("adunit-disable",0) ) return 0;
853   if( g.perm.Admin && db_get_boolean("adunit-omit-if-admin",0) ){
854     return 0;
855   }
856   if( !login_is_nobody()
857    && fossil_strcmp(g.zLogin,"anonymous")!=0
858    && db_get_boolean("adunit-omit-if-user",0)
859   ){
860     return 0;
861   }
862   if( (adUnitFlags & ADUNIT_RIGHT_OK)!=0
863    && !fossil_all_whitespace(zAd = db_get("adunit-right", 0))
864    && !cgi_body_contains("<table")
865   ){
866     *pAdFlag = ADUNIT_RIGHT_OK;
867     return zAd;
868   }else if( !fossil_all_whitespace(zAd = db_get("adunit",0)) ){
869     return zAd;
870   }
871   return 0;
872 }
873 
874 /*
875 ** Indicate that the table-sorting javascript is needed.
876 */
style_table_sorter(void)877 void style_table_sorter(void){
878   builtin_request_js("sorttable.js");
879 }
880 
881 /*
882 ** Generate code to load all required javascript files.
883 */
style_load_all_js_files(void)884 static void style_load_all_js_files(void){
885   if( needHrefJs && g.perm.Hyperlink ){
886     int nDelay = db_get_int("auto-hyperlink-delay",0);
887     int bMouseover = db_get_boolean("auto-hyperlink-mouseover",0);
888     @ <script id='href-data' type='application/json'>\
889     @ {"delay":%d(nDelay),"mouseover":%d(bMouseover)}</script>
890   }
891   @ <script nonce="%h(style_nonce())">/* style.c:%d(__LINE__) */
892   @ function debugMsg(msg){
893   @ var n = document.getElementById("debugMsg");
894   @ if(n){n.textContent=msg;}
895   @ }
896   if( needHrefJs && g.perm.Hyperlink ){
897     @ /* href.js */
898     cgi_append_content(builtin_text("href.js"),-1);
899   }
900   if( blob_size(&blobOnLoad)>0 ){
901     @ window.onload = function(){
902     cgi_append_content(blob_buffer(&blobOnLoad), blob_size(&blobOnLoad));
903     cgi_append_content("\n}\n", -1);
904   }
905   @ </script>
906   builtin_fulfill_js_requests();
907 }
908 
909 /*
910 ** Invoke this routine after all of the content for a webpage has been
911 ** generated.  This routine should be called once for every webpage, at
912 ** or near the end of page generation.  This routine does the following:
913 **
914 **   *  Populates the header of the page, including setting up appropriate
915 **      submenu elements.  The header generation is deferred until this point
916 **      so that we know that all style_submenu_element() and similar have
917 **      been received.
918 **
919 **   *  Finalizes the page content.
920 **
921 **   *  Appends the footer.
922 */
923 void style_finish_page(){
924   const char *zFooter;
925   const char *zAd = 0;
926   unsigned int mAdFlags = 0;
927 
928   if( !headerHasBeenGenerated ) return;
929 
930   /* Go back and put the submenu at the top of the page.  We delay the
931   ** creation of the submenu until the end so that we can add elements
932   ** to the submenu while generating page text.
933   */
934   cgi_destination(CGI_HEADER);
935   if( submenuEnable && nSubmenu+nSubmenuCtrl>0 ){
936     int i;
937     if( nSubmenuCtrl ){
938       @ <form id='f01' method='GET' action='%R/%s(g.zPath)'>
939       @ <input type='hidden' name='udc' value='1'>
940       cgi_tag_query_parameter("udc");
941     }
942     @ <div class="submenu">
943     if( nSubmenu>0 ){
944       qsort(aSubmenu, nSubmenu, sizeof(aSubmenu[0]), submenuCompare);
945       for(i=0; i<nSubmenu; i++){
946         struct Submenu *p = &aSubmenu[i];
947         /* switching away from the %h formatting below might be dangerous
948         ** because some places use %s to compose zLabel and zLink;
949         ** e.g. /rptview page
950         */
951         if( p->zLink==0 ){
952           @ <span class="label">%h(p->zLabel)</span>
953         }else{
954           @ <a class="label" href="%h(p->zLink)">%h(p->zLabel)</a>
955         }
956       }
957     }
958     for(i=0; i<nSubmenuCtrl; i++){
959       const char *zQPN = aSubmenuCtrl[i].zName;
960       const char *zDisabled = "";
961       const char *zXtraClass = "";
962       if( aSubmenuCtrl[i].eVisible & STYLE_DISABLED ){
963         zDisabled = " disabled";
964       }else if( zQPN ){
965         cgi_tag_query_parameter(zQPN);
966       }
967       switch( aSubmenuCtrl[i].eType ){
968         case FF_ENTRY:
969           @ <span class='submenuctrl%s(zXtraClass)'>\
970           @ &nbsp;%h(aSubmenuCtrl[i].zLabel)\
971           @ <input type='text' name='%s(zQPN)' value='%h(PD(zQPN, ""))' \
972           if( aSubmenuCtrl[i].iSize<0 ){
973             @ size='%d(-aSubmenuCtrl[i].iSize)' \
974           }else if( aSubmenuCtrl[i].iSize>0 ){
975             @ size='%d(aSubmenuCtrl[i].iSize)' \
976             @ maxlength='%d(aSubmenuCtrl[i].iSize)' \
977           }
978           @ id='submenuctrl-%d(i)'%s(zDisabled)></span>
979           break;
980         case FF_MULTI: {
981           int j;
982           const char *zVal = P(zQPN);
983           if( zXtraClass[0] ){
984             @ <span class='%s(zXtraClass+1)'>
985           }
986           if( aSubmenuCtrl[i].zLabel ){
987             @ &nbsp;%h(aSubmenuCtrl[i].zLabel)\
988           }
989           @ <select class='submenuctrl' size='1' name='%s(zQPN)' \
990           @ id='submenuctrl-%d(i)'%s(zDisabled)>
991           for(j=0; j<aSubmenuCtrl[i].iSize*2; j+=2){
992             const char *zQPV = aSubmenuCtrl[i].azChoice[j];
993             @ <option value='%h(zQPV)'\
994             if( fossil_strcmp(zVal, zQPV)==0 ){
995               @  selected\
996             }
997             @ >%h(aSubmenuCtrl[i].azChoice[j+1])</option>
998           }
999           @ </select>
1000           if( zXtraClass[0] ){
1001             @ </span>
1002           }
1003           break;
1004         }
1005         case FF_BINARY: {
1006           int isTrue = PB(zQPN);
1007           @ <select class='submenuctrl%s(zXtraClass)' size='1' \
1008           @ name='%s(zQPN)' id='submenuctrl-%d(i)'%s(zDisabled)>
1009           @ <option value='1'\
1010           if( isTrue ){
1011             @  selected\
1012           }
1013           @ >%h(aSubmenuCtrl[i].zLabel)</option>
1014           @ <option value='0'\
1015           if( !isTrue ){
1016             @  selected\
1017           }
1018           @ >%h(aSubmenuCtrl[i].zFalse)</option>
1019           @ </select>
1020           break;
1021         }
1022         case FF_CHECKBOX: {
1023           @ <label class='submenuctrl submenuckbox%s(zXtraClass)'>\
1024           @ <input type='checkbox' name='%s(zQPN)' id='submenuctrl-%d(i)' \
1025           if( PB(zQPN) ){
1026             @ checked \
1027           }
1028           if( aSubmenuCtrl[i].zJS ){
1029             @ data-ctrl='%s(aSubmenuCtrl[i].zJS)'%s(zDisabled)>\
1030           }else{
1031             @ %s(zDisabled)>\
1032           }
1033           @ %h(aSubmenuCtrl[i].zLabel)</label>
1034           break;
1035         }
1036       }
1037     }
1038     @ </div>
1039     if( nSubmenuCtrl ){
1040       cgi_query_parameters_to_hidden();
1041       cgi_tag_query_parameter(0);
1042       @ </form>
1043       builtin_request_js("menu.js");
1044     }
1045   }
1046 
1047   zAd = style_adunit_text(&mAdFlags);
1048   if( (mAdFlags & ADUNIT_RIGHT_OK)!=0  ){
1049     @ <div class="content adunit_right_container">
1050     @ <div class="adunit_right">
1051     cgi_append_content(zAd, -1);
1052     @ </div>
1053   }else if( zAd ){
1054     @ <div class="adunit_banner">
1055     cgi_append_content(zAd, -1);
1056     @ </div>
1057   }
1058 
1059   @ <div class="content"><span id="debugMsg"></span>
1060   cgi_destination(CGI_BODY);
1061 
1062   if( sideboxUsed ){
1063     @ <div class="endContent"></div>
1064   }
1065   @ </div>
1066 
1067   /* Put the footer at the bottom of the page. */
1068   zFooter = skin_get("footer");
1069   if( sqlite3_strlike("%</body>%", zFooter, 0)==0 ){
1070     style_load_all_js_files();
1071   }
1072   if( g.thTrace ) Th_Trace("BEGIN_FOOTER<br />\n", -1);
1073   Th_Render(zFooter);
1074   if( g.thTrace ) Th_Trace("END_FOOTER<br />\n", -1);
1075 
1076   /* Render trace log if TH1 tracing is enabled. */
1077   if( g.thTrace ){
1078     cgi_append_content("<span class=\"thTrace\"><hr />\n", -1);
1079     cgi_append_content(blob_str(&g.thLog), blob_size(&g.thLog));
1080     cgi_append_content("</span>\n", -1);
1081   }
1082 
1083   /* Add document end mark if it was not in the footer */
1084   if( sqlite3_strlike("%</body>%", zFooter, 0)!=0 ){
1085     style_load_all_js_files();
1086     @ </body>
1087     @ </html>
1088   }
1089   /* Update the user display prefs cookie if it was modified during
1090   ** this request.
1091   */
1092   cookie_render();
1093 }
1094 
1095 /*
1096 ** Begin a side-box on the right-hand side of a page.  The title and
1097 ** the width of the box are given as arguments.  The width is usually
1098 ** a percentage of total screen width.
1099 */
1100 void style_sidebox_begin(const char *zTitle, const char *zWidth){
1101   sideboxUsed = 1;
1102   @ <div class="sidebox" style="width:%s(zWidth)">
1103   @ <div class="sideboxTitle">%h(zTitle)</div>
1104 }
1105 
1106 /* End the side-box
1107 */
1108 void style_sidebox_end(void){
1109   @ </div>
1110 }
1111 
1112 /*
1113 ** Search string zCss for zSelector.
1114 **
1115 ** Return true if found.  Return false if not found
1116 */
1117 static int containsSelector(const char *zCss, const char *zSelector){
1118   const char *z;
1119   int n;
1120   int selectorLen = (int)strlen(zSelector);
1121 
1122   for(z=zCss; *z; z+=selectorLen){
1123     z = strstr(z, zSelector);
1124     if( z==0 ) return 0;
1125     if( z!=zCss ){
1126       for( n=-1; z+n!=zCss && fossil_isspace(z[n]); n--);
1127       if( z+n!=zCss && z[n]!=',' && z[n]!= '}' && z[n]!='/' ) continue;
1128     }
1129     for( n=selectorLen; z[n] && fossil_isspace(z[n]); n++ );
1130     if( z[n]==',' || z[n]=='{' || z[n]=='/' ) return 1;
1131   }
1132   return 0;
1133 }
1134 
1135 /*
1136 ** COMMAND: test-contains-selector
1137 **
1138 ** Usage: %fossil test-contains-selector FILENAME SELECTOR
1139 **
1140 ** Determine if the CSS stylesheet FILENAME contains SELECTOR.
1141 **
1142 ** Note that as of 2020-05-28, the default rules are always emitted,
1143 ** so the containsSelector() logic is no longer applied when emitting
1144 ** style.css. It is unclear whether this test command is now obsolete
1145 ** or whether it may still serve a purpose.
1146 */
1147 void contains_selector_cmd(void){
1148   int found;
1149   char *zSelector;
1150   Blob css;
1151   if( g.argc!=4 ) usage("FILENAME SELECTOR");
1152   blob_read_from_file(&css, g.argv[2], ExtFILE);
1153   zSelector = g.argv[3];
1154   found = containsSelector(blob_str(&css), zSelector);
1155   fossil_print("%s %s\n", zSelector, found ? "found" : "not found");
1156   blob_reset(&css);
1157 }
1158 
1159 /*
1160 ** WEBPAGE: script.js
1161 **
1162 ** Return the "Javascript" content for the current skin (if there is any)
1163 */
1164 void page_script_js(void){
1165   const char *zScript = skin_get("js");
1166   if( P("test") ){
1167     /* Render the script as plain-text for testing purposes, if the "test"
1168     ** query parameter is present */
1169     cgi_set_content_type("text/plain");
1170   }else{
1171     /* Default behavior is to return javascript */
1172     cgi_set_content_type("application/javascript");
1173   }
1174   style_init_th1_vars(0);
1175   Th_Render(zScript?zScript:"");
1176 }
1177 
1178 /*
1179 ** Check for "name" or "page" query parameters on an /style.css
1180 ** page request.  If present, then page-specific CSS is requested,
1181 ** so add that CSS to pOut.  If the "name" and "page" query parameters
1182 ** are omitted, then pOut is unchnaged.
1183 */
1184 static void page_style_css_append_page_style(Blob *pOut){
1185   const char *zPage = PD("name",P("page"));
1186   char * zFile;
1187   int nFile = 0;
1188   const char *zBuiltin;
1189 
1190   if(zPage==0 || zPage[0]==0){
1191     return;
1192   }
1193   zFile = mprintf("style.%s.css", zPage);
1194   zBuiltin = (const char *)builtin_file(zFile, &nFile);
1195   if(nFile>0){
1196     blob_appendf(pOut,
1197       "\n/***********************************************************\n"
1198       "** Page-specific CSS for \"%s\"\n"
1199       "***********************************************************/\n",
1200       zPage);
1201     blob_append(pOut, zBuiltin, nFile);
1202     fossil_free(zFile);
1203     return;
1204   }
1205   /* Potential TODO: check for aliases/page groups. e.g. group all
1206   ** /forumXYZ CSS into one file, all /setupXYZ into another, etc. As
1207   ** of this writing, doing so would only shave a few kb from
1208   ** default.css. */
1209   fossil_free(zFile);
1210 }
1211 
1212 /*
1213 ** WEBPAGE: style.css
1214 **
1215 ** Return the style sheet.   The style sheet is assemblied from
1216 ** multiple sources, in order:
1217 **
1218 **    (1)   The built-in "default.css" style sheet containing basic defaults.
1219 **
1220 **    (2)   The page-specific style sheet taken from the built-in
1221 **          called "PAGENAME.css" where PAGENAME is the value of the name=
1222 **          or page= query parameters.  If neither name= nor page= exist,
1223 **          then this section is a no-op.
1224 **
1225 **    (3)   The skin-specific "css.txt" file, if there one.
1226 **
1227 ** All of (1), (2), and (3) above (or as many as exist) are concatenated.
1228 ** The result is then run through TH1 with the following variables set:
1229 **
1230 **    *   $basename
1231 **    *   $secureurl
1232 **    *   $home
1233 **    *   $logo
1234 **    *   $background
1235 **
1236 ** The output from TH1 becomes the style sheet.  Fossil always reports
1237 ** that the style sheet is cacheable.
1238 */
1239 void page_style_css(void){
1240   Blob css = empty_blob;
1241   int i;
1242   const char * zDefaults;
1243   const char *zSkin;
1244 
1245   cgi_set_content_type("text/css");
1246   etag_check(0, 0);
1247   /* Emit all default rules... */
1248   zDefaults = (const char*)builtin_file("default.css", &i);
1249   blob_append(&css, zDefaults, i);
1250   /* Page-specific CSS, if any... */
1251   page_style_css_append_page_style(&css);
1252   zSkin = skin_in_use();
1253   if( zSkin==0 ) zSkin = "this repository";
1254   blob_appendf(&css,
1255      "\n/***********************************************************\n"
1256      "** Skin-specific CSS for %s\n"
1257      "***********************************************************/\n",
1258      zSkin);
1259   blob_append(&css,skin_get("css"),-1);
1260   /* Process through TH1 in order to give an opportunity to substitute
1261   ** variables such as $baseurl.
1262   */
1263   Th_Store("baseurl", g.zBaseURL);
1264   Th_Store("secureurl", fossil_wants_https(1)? g.zHttpsURL: g.zBaseURL);
1265   Th_Store("home", g.zTop);
1266   image_url_var("logo");
1267   image_url_var("background");
1268   Th_Render(blob_str(&css));
1269 
1270   /* Tell CGI that the content returned by this page is considered cacheable */
1271   g.isConst = 1;
1272 }
1273 
1274 /*
1275 ** All possible capabilities
1276 */
1277 static const char allCap[] =
1278   "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKL";
1279 
1280 /*
1281 ** Compute the current login capabilities
1282 */
1283 static char *find_capabilities(char *zCap){
1284   int i, j;
1285   char c;
1286   for(i=j=0; (c = allCap[j])!=0; j++){
1287     if( login_has_capability(&c, 1, 0) ) zCap[i++] = c;
1288   }
1289   zCap[i] = 0;
1290   return zCap;
1291 }
1292 
1293 /*
1294 ** Compute the current login capabilities that were
1295 ** contributed by Anonymous
1296 */
1297 static char *find_anon_capabilities(char *zCap){
1298   int i, j;
1299   char c;
1300   for(i=j=0; (c = allCap[j])!=0; j++){
1301     if( login_has_capability(&c, 1, LOGIN_ANON)
1302       && !login_has_capability(&c, 1, 0) ) zCap[i++] = c;
1303   }
1304   zCap[i] = 0;
1305   return zCap;
1306 }
1307 
1308 /*
1309 ** WEBPAGE: test_env
1310 **
1311 ** Display CGI-variables and other aspects of the run-time
1312 ** environment, for debugging and trouble-shooting purposes.
1313 */
1314 void page_test_env(void){
1315   webpage_error("");
1316 }
1317 
1318 /*
1319 ** WEBPAGE: honeypot
1320 ** This page is a honeypot for spiders and bots.
1321 */
1322 void honeypot_page(void){
1323   cgi_set_status(403, "Forbidden");
1324   @ <p>Please enable javascript or log in to see this content</p>
1325 }
1326 
1327 /*
1328 ** Webpages that encounter an error due to missing or incorrect
1329 ** query parameters can jump to this routine to render an error
1330 ** message screen.
1331 **
1332 ** For administators, or if the test_env_enable setting is true, then
1333 ** details of the request environment are displayed.  Otherwise, just
1334 ** the error message is shown.
1335 **
1336 ** If zFormat is an empty string, then this is the /test_env page.
1337 */
1338 void webpage_error(const char *zFormat, ...){
1339   int showAll;
1340   char *zErr = 0;
1341   int isAuth = 0;
1342   char zCap[100];
1343 
1344   login_check_credentials();
1345   if( g.perm.Admin || g.perm.Setup  || db_get_boolean("test_env_enable",0) ){
1346     isAuth = 1;
1347   }
1348   cgi_load_environment();
1349   style_set_current_feature(zFormat[0]==0 ? "test" : "error");
1350   if( zFormat[0] ){
1351     va_list ap;
1352     va_start(ap, zFormat);
1353     zErr = vmprintf(zFormat, ap);
1354     va_end(ap);
1355     style_header("Bad Request");
1356     @ <h1>/%h(g.zPath): %h(zErr)</h1>
1357     showAll = 0;
1358     cgi_set_status(500, "Bad Request");
1359   }else if( !isAuth ){
1360     login_needed(0);
1361     return;
1362   }else{
1363     style_header("Environment Test");
1364     showAll = PB("showall");
1365     style_submenu_checkbox("showall", "Cookies", 0, 0);
1366     style_submenu_element("Stats", "%R/stat");
1367   }
1368 
1369   if( isAuth ){
1370   #if !defined(_WIN32)
1371     @ uid=%d(getuid()), gid=%d(getgid())<br />
1372   #endif
1373     @ g.zBaseURL = %h(g.zBaseURL)<br />
1374     @ g.zHttpsURL = %h(g.zHttpsURL)<br />
1375     @ g.zTop = %h(g.zTop)<br />
1376     @ g.zPath = %h(g.zPath)<br />
1377     @ g.userUid = %d(g.userUid)<br />
1378     @ g.zLogin = %h(g.zLogin)<br />
1379     @ g.isHuman = %d(g.isHuman)<br />
1380     if( g.nRequest ){
1381       @ g.nRequest = %d(g.nRequest)<br />
1382     }
1383     if( g.nPendingRequest>1 ){
1384       @ g.nPendingRequest = %d(g.nPendingRequest)<br />
1385     }
1386     @ capabilities = %s(find_capabilities(zCap))<br />
1387     if( zCap[0] ){
1388       @ anonymous-adds = %s(find_anon_capabilities(zCap))<br />
1389     }
1390     @ g.zRepositoryName = %h(g.zRepositoryName)<br />
1391     @ load_average() = %f(load_average())<br />
1392 #ifndef _WIN32
1393     @ RSS = %.2f(fossil_rss()/1000000.0) MB</br />
1394 #endif
1395     @ cgi_csrf_safe(0) = %d(cgi_csrf_safe(0))<br />
1396     @ fossil_exe_id() = %h(fossil_exe_id())<br />
1397     @ <hr />
1398     P("HTTP_USER_AGENT");
1399     cgi_print_all(showAll, 0);
1400     if( showAll && blob_size(&g.httpHeader)>0 ){
1401       @ <hr />
1402       @ <pre>
1403       @ %h(blob_str(&g.httpHeader))
1404       @ </pre>
1405     }
1406   }
1407   if( zErr && zErr[0] ){
1408     style_finish_page();
1409     cgi_reply();
1410     fossil_exit(1);
1411   }else{
1412     style_finish_page();
1413   }
1414 }
1415 
1416 /*
1417 ** Generate a Not Yet Implemented error page.
1418 */
1419 void webpage_not_yet_implemented(void){
1420   webpage_error("Not yet implemented");
1421 }
1422 
1423 /*
1424 ** Generate a webpage for a webpage_assert().
1425 */
1426 void webpage_assert_page(const char *zFile, int iLine, const char *zExpr){
1427   fossil_warning("assertion fault at %s:%d - %s", zFile, iLine, zExpr);
1428   cgi_reset_content();
1429   webpage_error("assertion fault at %s:%d - %s", zFile, iLine, zExpr);
1430 }
1431 
1432 /*
1433 ** Issue a 404 Not Found error for a webpage
1434 */
1435 void webpage_notfound_error(const char *zFormat, ...){
1436   char *zMsg;
1437   va_list ap;
1438   if( zFormat ){
1439     va_start(ap, zFormat);
1440     zMsg = vmprintf(zFormat, ap);
1441     va_end(ap);
1442   }else{
1443     zMsg = "Not Found";
1444   }
1445   style_set_current_feature("enotfound");
1446   style_header("Not Found");
1447   @ <p>%h(zMsg)</p>
1448   cgi_set_status(404, "Not Found");
1449   style_finish_page();
1450 }
1451 
1452 #if INTERFACE
1453 # define webpage_assert(T) if(!(T)){webpage_assert_page(__FILE__,__LINE__,#T);}
1454 #endif
1455 
1456 /*
1457 ** Returns a pseudo-random input field ID, for use in associating an
1458 ** ID-less input field with a label. The memory is owned by the
1459 ** caller.
1460 */
1461 static char * style_next_input_id(){
1462   static int inputID = 0;
1463   ++inputID;
1464   return mprintf("input-id-%d", inputID);
1465 }
1466 
1467 /*
1468 ** Outputs a labeled checkbox element. zWrapperId is an optional ID
1469 ** value for the containing element (see below). zFieldName is the
1470 ** form element name. zLabel is the label for the checkbox. zValue is
1471 ** the optional value for the checkbox. zTip is an optional tooltip,
1472 ** which gets set as the "title" attribute of the outermost
1473 ** element. If isChecked is true, the checkbox gets the "checked"
1474 ** attribute set, else it is not.
1475 **
1476 ** Resulting structure:
1477 **
1478 ** <div class='input-with-label' title={{zTip}} id={{zWrapperId}}>
1479 **   <input type='checkbox' name={{zFieldName}} value={{zValue}}
1480 **          id='A RANDOM VALUE'
1481 **          {{isChecked ? " checked : ""}}/>
1482 **   <label for='ID OF THE INPUT FIELD'>{{zLabel}}</label>
1483 ** </div>
1484 **
1485 ** zLabel, and zValue are required. zFieldName, zWrapperId, and zTip
1486 ** are may be NULL or empty.
1487 **
1488 ** Be sure that the input-with-label CSS class is defined sensibly, in
1489 ** particular, having its display:inline-block is useful for alignment
1490 ** purposes.
1491 */
1492 void style_labeled_checkbox(const char * zWrapperId,
1493                             const char *zFieldName, const char * zLabel,
1494                             const char * zValue, int isChecked,
1495                             const char * zTip){
1496   char * zLabelID = style_next_input_id();
1497   CX("<div class='input-with-label'");
1498   if(zTip && *zTip){
1499     CX(" title='%h'", zTip);
1500   }
1501   if(zWrapperId && *zWrapperId){
1502     CX(" id='%s'",zWrapperId);
1503   }
1504   CX("><input type='checkbox' id='%s' ", zLabelID);
1505   if(zFieldName && *zFieldName){
1506     CX("name='%s' ",zFieldName);
1507   }
1508   CX("value='%T'%s/>",
1509      zValue ? zValue : "", isChecked ? " checked" : "");
1510   CX("<label for='%s'>%h</label></div>", zLabelID, zLabel);
1511   fossil_free(zLabelID);
1512 }
1513 
1514 /*
1515 ** Outputs a SELECT list from a compile-time list of integers.
1516 ** The vargs must be a list of (const char *, int) pairs, terminated
1517 ** with a single NULL. Each pair is interpreted as...
1518 **
1519 ** If the (const char *) is NULL, it is the end of the list, else
1520 ** a new OPTION entry is created. If the string is empty, the
1521 ** label and value of the OPTION is the integer part of the pair.
1522 ** If the string is not empty, it becomes the label and the integer
1523 ** the value. If that value == selectedValue then that OPTION
1524 ** element gets the 'selected' attribute.
1525 **
1526 ** Note that the pairs are not in (int, const char *) order because
1527 ** there is no well-known integer value which we can definitively use
1528 ** as a list terminator.
1529 **
1530 ** zWrapperId is an optional ID value for the containing element (see
1531 ** below).
1532 **
1533 ** zFieldName is the value of the form element's name attribute. Note
1534 ** that fossil prefers underscores over '-' for separators in form
1535 ** element names.
1536 **
1537 ** zLabel is an optional string to use as a "label" for the element
1538 ** (see below).
1539 **
1540 ** zTooltip is an optional value for the SELECT's title attribute.
1541 **
1542 ** The structure of the emitted HTML is:
1543 **
1544 ** <div class='input-with-label' title={{zToolTip}} id={{zWrapperId}}>
1545 **   <label for='SELECT ELEMENT ID'>{{zLabel}}</label>
1546 **   <select id='RANDOM ID' name={{zFieldName}}>...</select>
1547 ** </div>
1548 **
1549 ** Example:
1550 **
1551 ** style_select_list_int("my-grapes", "my_grapes", "Grapes",
1552 **                      "Select the number of grapes",
1553 **                       atoi(PD("my_field","0")),
1554 **                       "", 1, "2", 2, "Three", 3,
1555 **                       NULL);
1556 **
1557 */
1558 void style_select_list_int(const char * zWrapperId,
1559                            const char *zFieldName, const char * zLabel,
1560                            const char * zToolTip, int selectedVal,
1561                            ... ){
1562   char * zLabelID = style_next_input_id();
1563   va_list vargs;
1564 
1565   va_start(vargs,selectedVal);
1566   CX("<div class='input-with-label'");
1567   if(zToolTip && *zToolTip){
1568     CX(" title='%h'",zToolTip);
1569   }
1570   if(zWrapperId && *zWrapperId){
1571     CX(" id='%s'",zWrapperId);
1572   }
1573   CX(">");
1574   if(zLabel && *zLabel){
1575     CX("<label for='%s'>%h</label>", zLabelID, zLabel);
1576   }
1577   CX("<select name='%s' id='%s'>",zFieldName, zLabelID);
1578   while(1){
1579     const char * zOption = va_arg(vargs,char *);
1580     int v;
1581     if(NULL==zOption){
1582       break;
1583     }
1584     v = va_arg(vargs,int);
1585     CX("<option value='%d'%s>",
1586          v, v==selectedVal ? " selected" : "");
1587     if(*zOption){
1588       CX("%s", zOption);
1589     }else{
1590       CX("%d",v);
1591     }
1592     CX("</option>\n");
1593   }
1594   CX("</select>\n");
1595   CX("</div>\n");
1596   va_end(vargs);
1597   fossil_free(zLabelID);
1598 }
1599 
1600 /*
1601 ** The C-string counterpart of style_select_list_int(), this variant
1602 ** differs only in that its variadic arguments are C-strings in pairs
1603 ** of (optionLabel, optionValue). If a given optionLabel is an empty
1604 ** string, the corresponding optionValue is used as its label. If any
1605 ** given value matches zSelectedVal, that option gets preselected. If
1606 ** no options match zSelectedVal then the first entry is selected by
1607 ** default.
1608 **
1609 ** Any of (zWrapperId, zTooltip, zSelectedVal) may be NULL or empty.
1610 **
1611 ** Example:
1612 **
1613 ** style_select_list_str("my-grapes", "my_grapes", "Grapes",
1614 **                      "Select the number of grapes",
1615 **                       P("my_field"),
1616 **                       "1", "One", "2", "Two", "", "3",
1617 **                       NULL);
1618 */
1619 void style_select_list_str(const char * zWrapperId,
1620                            const char *zFieldName, const char * zLabel,
1621                            const char * zToolTip, char const * zSelectedVal,
1622                            ... ){
1623   char * zLabelID = style_next_input_id();
1624   va_list vargs;
1625 
1626   va_start(vargs,zSelectedVal);
1627   if(!zSelectedVal){
1628     zSelectedVal = __FILE__/*some string we'll never match*/;
1629   }
1630   CX("<div class='input-with-label'");
1631   if(zToolTip && *zToolTip){
1632     CX(" title='%h'",zToolTip);
1633   }
1634   if(zWrapperId && *zWrapperId){
1635     CX(" id='%s'",zWrapperId);
1636   }
1637   CX(">");
1638   if(zLabel && *zLabel){
1639     CX("<label for='%s'>%h</label>", zLabelID, zLabel);
1640   }
1641   CX("<select name='%s' id='%s'>",zFieldName, zLabelID);
1642   while(1){
1643     const char * zLabel = va_arg(vargs,char *);
1644     const char * zVal;
1645     if(NULL==zLabel){
1646       break;
1647     }
1648     zVal = va_arg(vargs,char *);
1649     CX("<option value='%T'%s>",
1650        zVal, 0==fossil_strcmp(zVal, zSelectedVal) ? " selected" : "");
1651     if(*zLabel){
1652       CX("%s", zLabel);
1653     }else{
1654       CX("%h",zVal);
1655     }
1656     CX("</option>\n");
1657   }
1658   CX("</select>\n");
1659   CX("</div>\n");
1660   va_end(vargs);
1661   fossil_free(zLabelID);
1662 }
1663 
1664 /*
1665 ** Generate a <script> with an appropriate nonce.
1666 **
1667 ** zOrigin and iLine are the source code filename and line number
1668 ** that generated this request.
1669 */
1670 void style_script_begin(const char *zOrigin, int iLine){
1671   const char *z;
1672   for(z=zOrigin; z[0]!=0; z++){
1673     if( z[0]=='/' || z[0]=='\\' ){
1674       zOrigin = z+1;
1675     }
1676   }
1677   CX("<script nonce='%s'>/* %s:%d */\n", style_nonce(), zOrigin, iLine);
1678 }
1679 
1680 /* Generate the closing </script> tag
1681 */
1682 void style_script_end(void){
1683   CX("</script>\n");
1684 }
1685 
1686 /*
1687 ** Emits a NOSCRIPT tag with an error message stating that JS is
1688 ** required for the current page. This "should" be called near the top
1689 ** of pages which *require* JS. The inner DIV has the CSS class
1690 ** 'error' and can be styled via a (noscript > .error) CSS selector.
1691 */
1692 void style_emit_noscript_for_js_page(void){
1693   CX("<noscript><div class='error'>"
1694      "This page requires JavaScript (ES2015, a.k.a. ES6, or newer)."
1695      "</div></noscript>");
1696 }
1697