Fossil SCM

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

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button