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