Fossil SCM

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

Keyboard Shortcuts

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