Fossil SCM

fossil-scm / src / skins.c
Blame History Raw 1442 lines
1
/*
2
** Copyright (c) 2009 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
** Implementation of the Setup page for "skins".
19
*/
20
#include "config.h"
21
#include <assert.h>
22
#include "skins.h"
23
24
/*
25
** SETTING: default-skin width=16
26
**
27
** If the text value if this setting is the name of a built-in skin
28
** then the named skin becomes the default skin for the repository.
29
*/
30
31
/*
32
** An array of available built-in skins.
33
**
34
** To add new built-in skins:
35
**
36
** 1. Pick a name for the new skin. (Here we use "xyzzy").
37
**
38
** 2. Install files skins/xyzzy/css.txt, skins/xyzzy/header.txt,
39
** and skins/xyzzy/footer.txt into the source tree.
40
**
41
** 3. Rerun "tclsh makemake.tcl" in the src/ folder in order to
42
** rebuild the makefiles to reference the new CSS, headers, and footers.
43
**
44
** 4. Make an entry in the following array for the new skin.
45
*/
46
static struct BuiltinSkin {
47
const char *zDesc; /* Description of this skin */
48
const char *zLabel; /* The directory under skins/ holding this skin */
49
char *zSQL; /* Filled in at run-time with SQL to insert this skin */
50
} aBuiltinSkin[] = {
51
{ "Default", "default", 0 },
52
{ "Ardoise", "ardoise", 0 },
53
{ "Black & White", "black_and_white", 0 },
54
{ "Blitz", "blitz", 0 },
55
{ "Dark Mode", "darkmode", 0 },
56
{ "Eagle", "eagle", 0 },
57
{ "Étienne", "etienne", 0 },
58
{ "Khaki", "khaki", 0 },
59
{ "Original", "original", 0 },
60
{ "Plain Gray", "plain_gray", 0 },
61
{ "Xekri", "xekri", 0 },
62
};
63
64
/*
65
** A skin consists of five "files" named here:
66
*/
67
static const char *const azSkinFile[] = {
68
"css", "header", "footer", "details", "js"
69
};
70
71
/*
72
** Alternative skins can be specified in the CGI script or by options
73
** on the "http", "ui", and "server" commands. The alternative skin
74
** name must be one of the aBuiltinSkin[].zLabel names. If there is
75
** a match, that alternative is used.
76
**
77
** The following static variable holds the name of the alternative skin,
78
** or NULL if the skin should be as configured.
79
*/
80
static struct BuiltinSkin *pAltSkin = 0;
81
static char *zAltSkinDir = 0;
82
static int iDraftSkin = 0;
83
/*
84
** Used by skin_use_alternative() to store the current skin rank skin
85
** so that the /skins page can, if warranted, warn the user that skin
86
** changes won't have any effect.
87
*/
88
static int nSkinRank = 6;
89
90
/*
91
** How the specific skin being used was chosen
92
*/
93
#if INTERFACE
94
#define SKIN_FROM_DRAFT 0 /* The "draftN" prefix on the PATH_INFO */
95
#define SKIN_FROM_CMDLINE 1 /* --skin option to server command-line */
96
#define SKIN_FROM_CGI 2 /* skin: parameter in CGI script */
97
#define SKIN_FROM_QPARAM 3 /* skin= query parameter */
98
#define SKIN_FROM_COOKIE 4 /* skin= from fossil_display_settings cookie*/
99
#define SKIN_FROM_SETTING 5 /* Built-in named by "default-skin" setting */
100
#define SKIN_FROM_CUSTOM 6 /* Skin values in CONFIG table */
101
#define SKIN_FROM_DEFAULT 7 /* The built-in named "default" */
102
#define SKIN_FROM_UNKNOWN 8 /* Do not yet know which skin to use */
103
#endif /* INTERFACE */
104
static int iSkinSource = SKIN_FROM_UNKNOWN;
105
106
107
/*
108
** Skin details are a set of key/value pairs that define display
109
** attributes of the skin that cannot be easily specified using CSS
110
** or that need to be known on the server-side.
111
**
112
** The following array holds the value for all known skin details.
113
*/
114
static struct SkinDetail {
115
const char *zName; /* Name of the detail */
116
const char *zValue; /* Value of the detail */
117
} aSkinDetail[] = {
118
{ "pikchr-background", "" },
119
{ "pikchr-fontscale", "" },
120
{ "pikchr-foreground", "" },
121
{ "pikchr-scale", "" },
122
{ "timeline-arrowheads", "1" },
123
{ "timeline-circle-nodes", "0" },
124
{ "timeline-color-graph-lines", "0" },
125
{ "white-foreground", "0" },
126
};
127
128
/*
129
** Invoke this routine to set the alternative skin. Return NULL if the
130
** alternative was successfully installed. Return a string listing all
131
** available skins if zName does not match an available skin. Memory
132
** for the returned string comes from fossil_malloc() and should be freed
133
** by the caller.
134
**
135
** If the alternative skin name contains one or more '/' characters, then
136
** it is assumed to be a directory on disk that holds override css.txt,
137
** footer.txt, and header.txt. This mode can be used for interactive
138
** development of new skins.
139
**
140
** The 2nd parameter is a ranking of how important this alternative
141
** skin declaration is, and lower values trump higher ones. If a call
142
** to this function passes a higher-valued rank than a previous call,
143
** the subsequent call becomes a no-op. Only calls with the same or
144
** lower rank (i.e. higher priority) will overwrite a previous
145
** setting. This approach is used because the CGI/server-time
146
** initialization happens in an order which is incompatible with our
147
** preferred ranking, making it otherwise more invasive to tell the
148
** internals "the --skin flag ranks higher than a URL parameter" (the
149
** former gets initialized before both URL parameters and the /draft
150
** path determination).
151
**
152
** The rankings were initially defined in
153
** https://fossil-scm.org/forum/forumpost/caf8c9a8bb
154
** but where subsequently revised:
155
**
156
** 0) A skin name matching the glob pattern "draft[1-9]" at the start of
157
** the PATH_INFO.
158
**
159
** 1) The --skin flag for commands like "fossil ui", "fossil server", or
160
** "fossil http", or the "skin:" CGI config setting.
161
**
162
** 2) The "skin" display setting cookie or URL argument, in that
163
** order. If the "skin" URL argument is provided and refers to a legal
164
** skin then that will update the display cookie. If the skin name is
165
** illegal it is silently ignored.
166
**
167
** 3) The built-in skin identified by the "default-skin" setting, if such
168
** a setting exists and matches one of the built-in skin names.
169
**
170
** 4) Skin properties (settings "css", "details", "footer", "header",
171
** and "js") from the CONFIG db table
172
**
173
** 5) The built-in skin named "default"
174
**
175
** The iSource integer privides additional detail about where the skin
176
**
177
** As a special case, a NULL or empty name resets zAltSkinDir and
178
** pAltSkin to 0 to indicate that the current config-side skin should
179
** be used (rank 3, above), then returns 0.
180
*/
181
char *skin_use_alternative(const char *zName, int rank, int iSource){
182
int i;
183
Blob err = BLOB_INITIALIZER;
184
if(rank > nSkinRank) return 0;
185
nSkinRank = rank;
186
if( zName && 1==rank && strchr(zName, '/')!=0 ){
187
zAltSkinDir = fossil_strdup(zName);
188
iSkinSource = iSource;
189
return 0;
190
}
191
if( zName && sqlite3_strglob("draft[1-9]", zName)==0 ){
192
skin_use_draft(zName[5] - '0');
193
iSkinSource = iSource;
194
return 0;
195
}
196
if(!zName || !*zName){
197
pAltSkin = 0;
198
zAltSkinDir = 0;
199
return 0;
200
}
201
if( fossil_strcmp(zName, "custom")==0 ){
202
pAltSkin = 0;
203
zAltSkinDir = 0;
204
iSkinSource = iSource;
205
return 0;
206
}
207
for(i=0; i<count(aBuiltinSkin); i++){
208
if( fossil_strcmp(aBuiltinSkin[i].zLabel, zName)==0 ){
209
pAltSkin = &aBuiltinSkin[i];
210
iSkinSource = iSource;
211
return 0;
212
}
213
}
214
blob_appendf(&err, "available skins: %s", aBuiltinSkin[0].zLabel);
215
for(i=1; i<count(aBuiltinSkin); i++){
216
blob_append(&err, " ", 1);
217
blob_append(&err, aBuiltinSkin[i].zLabel, -1);
218
}
219
return blob_str(&err);
220
}
221
222
/*
223
** Look for the --skin command-line option and process it. Or
224
** call fossil_fatal() if an unknown skin is specified.
225
**
226
** This routine is called during command-line parsing for commands
227
** like "fossil ui" and "fossil http".
228
*/
229
void skin_override(void){
230
const char *zSkin = find_option("skin",0,1);
231
if( zSkin ){
232
char *zErr = skin_use_alternative(zSkin, 1, SKIN_FROM_CMDLINE);
233
if( zErr ) fossil_fatal("%s", zErr);
234
}
235
}
236
237
/*
238
** Use one of the draft skins.
239
*/
240
void skin_use_draft(int i){
241
iDraftSkin = i;
242
iSkinSource = SKIN_FROM_DRAFT;
243
}
244
245
/*
246
** The following routines return the various components of the skin
247
** that should be used for the current run.
248
**
249
** zWhat is one of: "css", "header", "footer", "details", "js"
250
*/
251
const char *skin_get(const char *zWhat){
252
const char *zOut;
253
char *z;
254
if( iDraftSkin ){
255
z = mprintf("draft%d-%s", iDraftSkin, zWhat);
256
zOut = db_get(z, 0);
257
fossil_free(z);
258
if( zOut ) return zOut;
259
}
260
if( zAltSkinDir ){
261
char *z = mprintf("%s/%s.txt", zAltSkinDir, zWhat);
262
if( file_isfile(z, ExtFILE) ){
263
Blob x;
264
blob_read_from_file(&x, z, ExtFILE);
265
fossil_free(z);
266
return blob_str(&x);
267
}
268
fossil_free(z);
269
}
270
if( iSkinSource==SKIN_FROM_UNKNOWN ){
271
const char *zDflt = db_get("default-skin", 0);
272
iSkinSource = SKIN_FROM_DEFAULT;
273
if( zDflt!=0 ){
274
int i;
275
for(i=0; i<count(aBuiltinSkin); i++){
276
if( fossil_strcmp(aBuiltinSkin[i].zLabel, zDflt)==0 ){
277
pAltSkin = &aBuiltinSkin[i];
278
iSkinSource = SKIN_FROM_SETTING;
279
break;
280
}
281
}
282
}
283
}
284
if( pAltSkin ){
285
z = mprintf("skins/%s/%s.txt", pAltSkin->zLabel, zWhat);
286
zOut = builtin_text(z);
287
fossil_free(z);
288
}else{
289
zOut = db_get(zWhat, 0);
290
if( zOut==0 ){
291
z = mprintf("skins/default/%s.txt", zWhat);
292
zOut = builtin_text(z);
293
fossil_free(z);
294
}else if( iSkinSource==SKIN_FROM_DEFAULT ){
295
iSkinSource = SKIN_FROM_CUSTOM;
296
}
297
}
298
return zOut;
299
}
300
301
/*
302
** Return the command-line option used to set the skin, or return NULL
303
** if the default skin is being used.
304
*/
305
const char *skin_in_use(void){
306
if( zAltSkinDir ) return zAltSkinDir;
307
if( pAltSkin ) return pAltSkin->zLabel;
308
return 0;
309
}
310
311
/*
312
** Return a pointer to a SkinDetail element. Return 0 if not found.
313
*/
314
static struct SkinDetail *skin_detail_find(const char *zName){
315
int lwr = 0;
316
int upr = count(aSkinDetail);
317
while( upr>=lwr ){
318
int mid = (upr+lwr)/2;
319
int c = fossil_strcmp(aSkinDetail[mid].zName, zName);
320
if( c==0 ) return &aSkinDetail[mid];
321
if( c<0 ){
322
lwr = mid+1;
323
}else{
324
upr = mid-1;
325
}
326
}
327
return 0;
328
}
329
330
/* Initialize the aSkinDetail array using the text in the details.txt
331
** file.
332
*/
333
static void skin_detail_initialize(void){
334
static int isInit = 0;
335
char *zDetail;
336
Blob detail, line, key, value;
337
if( isInit ) return;
338
isInit = 1;
339
zDetail = (char*)skin_get("details");
340
if( zDetail==0 ) return;
341
zDetail = fossil_strdup(zDetail);
342
blob_init(&detail, zDetail, -1);
343
while( blob_line(&detail, &line) ){
344
char *zKey;
345
int nKey;
346
struct SkinDetail *pDetail;
347
if( !blob_token(&line, &key) ) continue;
348
zKey = blob_buffer(&key);
349
if( zKey[0]=='#' ) continue;
350
nKey = blob_size(&key);
351
if( nKey<2 ) continue;
352
if( zKey[nKey-1]!=':' ) continue;
353
zKey[nKey-1] = 0;
354
pDetail = skin_detail_find(zKey);
355
if( pDetail==0 ) continue;
356
if( !blob_token(&line, &value) ) continue;
357
pDetail->zValue = fossil_strdup(blob_str(&value));
358
}
359
blob_reset(&detail);
360
fossil_free(zDetail);
361
}
362
363
/*
364
** Return a skin detail setting
365
*/
366
const char *skin_detail(const char *zName){
367
struct SkinDetail *pDetail;
368
skin_detail_initialize();
369
pDetail = skin_detail_find(zName);
370
if( pDetail==0 ) fossil_fatal("no such skin detail: %s", zName);
371
return pDetail->zValue;
372
}
373
int skin_detail_boolean(const char *zName){
374
return !is_false(skin_detail(zName));
375
}
376
377
/*
378
** Hash function for computing a skin id.
379
*/
380
static unsigned int skin_hash(unsigned int h, const char *z){
381
if( z==0 ) return h;
382
while( z[0] ){
383
h = (h<<11) ^ (h<<1) ^ (h>>3) ^ z[0];
384
z++;
385
}
386
return h;
387
}
388
389
/*
390
** Return an identifier that is (probably) different for every skin
391
** but that is (probably) the same if the skin is unchanged. This
392
** identifier can be attached to resource URLs to force reloading when
393
** the resources change but allow the resources to be read from cache
394
** as long as they are unchanged.
395
**
396
** The zResource argument is the name of a CONFIG setting that
397
** defines the resource. Examples: "css", "logo-image".
398
*/
399
unsigned int skin_id(const char *zResource){
400
unsigned int h = 0;
401
if( zAltSkinDir ){
402
h = skin_hash(0, zAltSkinDir);
403
}else if( pAltSkin ){
404
h = skin_hash(0, pAltSkin->zLabel);
405
}else{
406
char *zMTime = db_get_mtime(zResource, 0, 0);
407
h = skin_hash(0, zMTime);
408
fossil_free(zMTime);
409
}
410
411
/* Change the ID every time Fossil is recompiled */
412
h = skin_hash(h, fossil_exe_id());
413
return h;
414
}
415
416
/*
417
** For a skin named zSkinName, compute the name of the CONFIG table
418
** entry where that skin is stored and return it.
419
**
420
** Return NULL if zSkinName is NULL or an empty string.
421
**
422
** If ifExists is true, and the named skin does not exist, return NULL.
423
*/
424
static char *skinVarName(const char *zSkinName, int ifExists){
425
char *z;
426
if( zSkinName==0 || zSkinName[0]==0 ) return 0;
427
z = mprintf("skin:%s", zSkinName);
428
if( ifExists && !db_exists("SELECT 1 FROM config WHERE name=%Q", z) ){
429
free(z);
430
z = 0;
431
}
432
return z;
433
}
434
435
/*
436
** Return true if there exists a skin name "zSkinName".
437
*/
438
static int skinExists(const char *zSkinName){
439
int i;
440
if( zSkinName==0 ) return 0;
441
for(i=0; i<count(aBuiltinSkin); i++){
442
if( fossil_strcmp(zSkinName, aBuiltinSkin[i].zDesc)==0 ) return 1;
443
}
444
return db_exists("SELECT 1 FROM config WHERE name='skin:%q'", zSkinName);
445
}
446
447
/*
448
** Construct and return an string of SQL statements that represents
449
** a "skin" setting. If zName==0 then return the skin currently
450
** installed. Otherwise, return one of the built-in skins designated
451
** by zName.
452
**
453
** Memory to hold the returned string is obtained from malloc.
454
*/
455
static char *getSkin(const char *zName){
456
const char *z;
457
char *zLabel;
458
int i;
459
Blob val;
460
blob_zero(&val);
461
for(i=0; i<count(azSkinFile); i++){
462
if( zName ){
463
zLabel = mprintf("skins/%s/%s.txt", zName, azSkinFile[i]);
464
z = builtin_text(zLabel);
465
fossil_free(zLabel);
466
}else{
467
z = db_get(azSkinFile[i], 0);
468
if( z==0 ){
469
zLabel = mprintf("skins/default/%s.txt", azSkinFile[i]);
470
z = builtin_text(zLabel);
471
fossil_free(zLabel);
472
}
473
}
474
db_unprotect(PROTECT_CONFIG);
475
blob_appendf(&val,
476
"REPLACE INTO config(name,value,mtime) VALUES(%Q,%Q,now());\n",
477
azSkinFile[i], z
478
);
479
db_protect_pop();
480
}
481
return blob_str(&val);
482
}
483
484
/*
485
** Respond to a Rename button press. Return TRUE if a dialog was painted.
486
** Return FALSE to continue with the main Skins page.
487
*/
488
static int skinRename(void){
489
const char *zOldName;
490
const char *zNewName;
491
int ex = 0;
492
if( P("rename")==0 ) return 0;
493
zOldName = P("sn");
494
zNewName = P("newname");
495
if( zOldName==0 ) return 0;
496
if( zNewName==0 || zNewName[0]==0 || (ex = skinExists(zNewName))!=0 ){
497
if( zNewName==0 ) zNewName = zOldName;
498
style_set_current_feature("skins");
499
style_header("Rename A Skin");
500
if( ex ){
501
@ <p><span class="generalError">There is already another skin
502
@ named "%h(zNewName)". Choose a different name.</span></p>
503
}
504
@ <form action="%R/setup_skin_admin" method="post"><div>
505
@ <table border="0"><tr>
506
@ <tr><td align="right">Current name:<td align="left"><b>%h(zOldName)</b>
507
@ <tr><td align="right">New name:<td align="left">
508
@ <input type="text" size="35" name="newname" value="%h(zNewName)">
509
@ <tr><td><td>
510
@ <input type="hidden" name="sn" value="%h(zOldName)">
511
@ <input type="submit" name="rename" value="Rename">
512
@ <input type="submit" name="canren" value="Cancel">
513
@ </table>
514
login_insert_csrf_secret();
515
@ </div></form>
516
style_finish_page();
517
return 1;
518
}
519
db_unprotect(PROTECT_CONFIG);
520
db_multi_exec(
521
"UPDATE config SET name='skin:%q' WHERE name='skin:%q';",
522
zNewName, zOldName
523
);
524
db_protect_pop();
525
return 0;
526
}
527
528
/*
529
** Respond to a Save button press. Return TRUE if a dialog was painted.
530
** Return FALSE to continue with the main Skins page.
531
*/
532
static int skinSave(const char *zCurrent){
533
const char *zNewName;
534
int ex = 0;
535
if( P("save")==0 ) return 0;
536
zNewName = P("svname");
537
if( zNewName && zNewName[0]!=0 ){
538
}
539
if( zNewName==0 || zNewName[0]==0 || (ex = skinExists(zNewName))!=0 ){
540
if( zNewName==0 ) zNewName = "";
541
style_set_current_feature("skins");
542
style_header("Save Current Skin");
543
if( ex ){
544
@ <p><span class="generalError">There is already another skin
545
@ named "%h(zNewName)". Choose a different name.</span></p>
546
}
547
@ <form action="%R/setup_skin_admin" method="post"><div>
548
@ <table border="0"><tr>
549
@ <tr><td align="right">Name for this skin:<td align="left">
550
@ <input type="text" size="35" name="svname" value="%h(zNewName)">
551
@ <tr><td><td>
552
@ <input type="submit" name="save" value="Save">
553
@ <input type="submit" name="cansave" value="Cancel">
554
@ </table>
555
login_insert_csrf_secret();
556
@ </div></form>
557
style_finish_page();
558
return 1;
559
}
560
db_unprotect(PROTECT_CONFIG);
561
db_multi_exec(
562
"INSERT OR IGNORE INTO config(name, value, mtime)"
563
"VALUES('skin:%q',%Q,now())",
564
zNewName, zCurrent
565
);
566
db_protect_pop();
567
return 0;
568
}
569
570
/*
571
** Return true if a custom skin exists
572
*/
573
static int skin_exists_custom(void){
574
return db_exists("SELECT 1 FROM config WHERE name IN"
575
" ('css','details','footer','header','js')");
576
}
577
578
static void skin_publish(int); /* Forward reference */
579
580
/*
581
** WEBPAGE: setup_skin_admin
582
**
583
** Administrative actions on skins. For administrators only.
584
*/
585
void setup_skin_admin(void){
586
const char *z;
587
char *zName;
588
char *zErr = 0;
589
const char *zCurrent = 0; /* Current skin */
590
int i; /* Loop counter */
591
Stmt q;
592
int once;
593
const char *zOverride = 0;
594
const char *zDfltSkin = 0;
595
int seenDefault = 0;
596
int hasCustom;
597
598
login_check_credentials();
599
if( !g.perm.Admin ){
600
login_needed(0);
601
return;
602
}
603
db_begin_transaction();
604
zCurrent = getSkin(0);
605
for(i=0; i<count(aBuiltinSkin); i++){
606
aBuiltinSkin[i].zSQL = getSkin(aBuiltinSkin[i].zLabel);
607
}
608
609
style_set_current_feature("skins");
610
611
if( cgi_csrf_safe(2) ){
612
/* Process requests to delete a user-defined skin */
613
if( P("del1") && P("sn")!=0 ){
614
style_header("Confirm Custom Skin Delete");
615
@ <form action="%R/setup_skin_admin" method="post"><div>
616
@ <p>Deletion of a custom skin is a permanent action that cannot
617
@ be undone. Please confirm that this is what you want to do:</p>
618
@ <input type="hidden" name="sn" value="%h(P("sn"))">
619
@ <input type="submit" name="del2" value="Confirm - Delete The Skin">
620
@ <input type="submit" name="cancel" value="Cancel - Do Not Delete">
621
login_insert_csrf_secret();
622
@ </div></form>
623
style_finish_page();
624
db_end_transaction(1);
625
return;
626
}
627
if( P("del2")!=0 ){
628
db_unprotect(PROTECT_CONFIG);
629
if( fossil_strcmp(P("sn"),"custom")==0 ){
630
db_multi_exec("DELETE FROM config WHERE name IN"
631
"('css','details','footer','header','js')");
632
}else if( (zName = skinVarName(P("sn"), 1))!=0 ){
633
db_multi_exec("DELETE FROM config WHERE name=%Q", zName);
634
}
635
db_protect_pop();
636
}
637
if( P("draftdel")!=0 ){
638
const char *zDraft = P("name");
639
if( sqlite3_strglob("draft[1-9]",zDraft)==0 ){
640
db_unprotect(PROTECT_CONFIG);
641
db_multi_exec("DELETE FROM config WHERE name GLOB '%q-*'", zDraft);
642
db_protect_pop();
643
}
644
}
645
if( P("editdraft")!=0 ){
646
db_end_transaction(0);
647
cgi_redirectf("%R/setup_skin");
648
return;
649
}
650
if( skinRename() || skinSave(zCurrent) ){
651
db_end_transaction(0);
652
return;
653
}
654
655
if( P("setdflt") && (z = P("bisl"))!=0 ){
656
if( z[0] ){
657
db_set("default-skin", z, 0);
658
}else{
659
db_unset("default-skin", 0);
660
}
661
db_end_transaction(0);
662
cgi_redirectf("%R/setup_skin_admin");
663
return;
664
}
665
666
/* The user pressed one of the "Install" buttons. */
667
if( P("load") && (z = P("sn"))!=0 && z[0] ){
668
int seen = 0;
669
670
/* Check to see if the current skin is already saved. If it is, there
671
** is no need to create a backup */
672
hasCustom = skin_exists_custom();
673
if( hasCustom ){
674
zCurrent = getSkin(0);
675
for(i=0; i<count(aBuiltinSkin); i++){
676
if( fossil_strcmp(aBuiltinSkin[i].zSQL, zCurrent)==0 ){
677
seen = 1;
678
break;
679
}
680
}
681
if( !seen ){
682
seen = db_exists("SELECT 1 FROM config WHERE name GLOB 'skin:*'"
683
" AND value=%Q", zCurrent);
684
if( !seen ){
685
db_unprotect(PROTECT_CONFIG);
686
db_multi_exec(
687
"INSERT INTO config(name,value,mtime) VALUES("
688
" strftime('skin:Backup On %%Y-%%m-%%d %%H:%%M:%%S'),"
689
" %Q,now())", zCurrent
690
);
691
db_protect_pop();
692
}
693
}
694
}
695
seen = 0;
696
if( z[0]>='1' && z[0]<='9' && z[1]==0 ){
697
skin_publish(z[0]-'0');
698
seen = 1;
699
}
700
for(i=0; seen==0 && i<count(aBuiltinSkin); i++){
701
if( fossil_strcmp(aBuiltinSkin[i].zDesc, z)==0 ){
702
seen = 1;
703
zCurrent = aBuiltinSkin[i].zSQL;
704
db_unprotect(PROTECT_CONFIG);
705
db_multi_exec("%s", zCurrent/*safe-for-%s*/);
706
db_protect_pop();
707
break;
708
}
709
}
710
if( !seen ){
711
zName = skinVarName(z,0);
712
zCurrent = db_get(zName, 0);
713
db_unprotect(PROTECT_CONFIG);
714
db_multi_exec("%s", zCurrent/*safe-for-%s*/);
715
db_protect_pop();
716
}
717
}
718
}
719
720
zDfltSkin = db_get("default-skin",0);
721
hasCustom = skin_exists_custom();
722
if( !hasCustom && zDfltSkin==0 ){
723
zDfltSkin = "default";
724
}
725
726
style_header("Skins");
727
if( zErr ){
728
@ <p style="color:red">%h(zErr)</p>
729
}
730
@ <table border="0">
731
@ <tr><td colspan=4><h2>Built-in Skins:</h2></td></tr>
732
for(i=0; i<count(aBuiltinSkin); i++){
733
z = aBuiltinSkin[i].zDesc;
734
@ <tr><td>%d(i+1).<td>%h(z)<td>&nbsp;&nbsp;<td>
735
@ <form action="%R/setup_skin_admin" method="POST">
736
login_insert_csrf_secret();
737
if( zDfltSkin==0 || fossil_strcmp(aBuiltinSkin[i].zLabel, zDfltSkin)!=0 ){
738
/* vvvv--- mnemonic: Built-In Skin Label */
739
@ <input type="hidden" name="bisl" value="%h(aBuiltinSkin[i].zLabel)">
740
@ <input type="submit" name="setdflt" value="Set">
741
}else{
742
@ (Selected)
743
seenDefault = 1;
744
}
745
if( pAltSkin==&aBuiltinSkin[i] && iSkinSource!=SKIN_FROM_SETTING ){
746
@ (Override)
747
zOverride = z;
748
}
749
@ </form></td></tr>
750
}
751
if( zOverride ){
752
@ <tr><td>&nbsp;<td colspan="3">
753
@ <p>Note: Built-in skin "%h(zOverride)" is currently being used because of
754
switch( iSkinSource ){
755
case SKIN_FROM_CMDLINE:
756
@ the --skin command-line option.
757
break;
758
case SKIN_FROM_CGI:
759
@ the "skin:" option on CGI script.
760
break;
761
case SKIN_FROM_QPARAM:
762
@ the "skin=NAME" query parameter.
763
break;
764
case SKIN_FROM_COOKIE:
765
@ the "skin" value of the
766
@ <a href='./fdscookie'>fossil_display_setting</a> cookie.
767
break;
768
case SKIN_FROM_SETTING:
769
@ the "default-skin" setting.
770
break;
771
default:
772
@ reasons unknown. (Fix me!)
773
break;
774
}
775
@ </tr>
776
}
777
i++;
778
@ <tr><td colspan=4><h2>Custom skin:</h2></td></tr>
779
@ <tr><td>%d(i).
780
if( hasCustom ){
781
@ <td>Custom<td>&nbsp;&nbsp;<td>
782
}else{
783
@ <td><i>(None)</i><td>&nbsp;&nbsp;<td>
784
}
785
@ <form method="post">
786
login_insert_csrf_secret();
787
if( hasCustom ){
788
@ <input type="submit" name="save" value="Backup">
789
@ <input type="submit" name="editdraft" value="Edit">
790
if( !seenDefault ){
791
@ (Selected)
792
}else{
793
@ <input type="hidden" name="bisl" value="">
794
@ <input type="submit" name="setdflt" value="Set">
795
@ <input type="submit" name="del1" value="Delete">
796
@ <input type="hidden" name="sn" value="custom">
797
}
798
}else{
799
@ <input type="submit" name="editdraft" value="Create">
800
}
801
@ </form>
802
@ </td></tr>
803
db_prepare(&q,
804
"SELECT substr(name, 6) FROM config"
805
" WHERE name GLOB 'skin:*'"
806
" ORDER BY name"
807
);
808
once = 1;
809
while( db_step(&q)==SQLITE_ROW ){
810
const char *zN = db_column_text(&q, 0);
811
i++;
812
if( once ){
813
once = 0;
814
@ <tr><td colspan=4><h2>Backups of past custom skins:</h2></td></tr>
815
}
816
@ <tr><td>%d(i).<td>%h(zN)<td>&nbsp;&nbsp;<td>
817
@ <form action="%R/setup_skin_admin" method="post">
818
login_insert_csrf_secret();
819
@ <input type="submit" name="load" value="Install">
820
@ <input type="submit" name="del1" value="Delete">
821
@ <input type="submit" name="rename" value="Rename">
822
@ <input type="hidden" name="sn" value="%h(zN)">
823
@ </form></tr>
824
}
825
db_finalize(&q);
826
db_prepare(&q,
827
"SELECT DISTINCT substr(name, 1, 6) FROM config"
828
" WHERE name GLOB 'draft[1-9]-*'"
829
" ORDER BY name"
830
);
831
once = 1;
832
while( db_step(&q)==SQLITE_ROW ){
833
const char *zN = db_column_text(&q, 0);
834
i++;
835
if( once ){
836
once = 0;
837
@ <tr><td colspan=4><h2>Draft skins:</h2></td></tr>
838
}
839
@ <tr><td>%d(i).<td>%h(zN)<td>&nbsp;&nbsp;<td>
840
@ <form action="%R/setup_skin_admin" method="post">
841
login_insert_csrf_secret();
842
@ <input type="submit" name="load" value="Install">
843
@ <input type="submit" name="draftdel" value="Delete">
844
@ <input type="hidden" name="name" value="%h(zN)">
845
@ <input type="hidden" name="sn" value="%h(zN+5)">
846
@ </form></tr>
847
}
848
db_finalize(&q);
849
850
@ </table>
851
style_finish_page();
852
db_end_transaction(0);
853
}
854
855
/*
856
** Generate HTML for a <select> that lists all the available skin names,
857
** except for zExcept if zExcept!=NULL.
858
*/
859
static void skin_emit_skin_selector(
860
const char *zVarName, /* Variable name for the <select> */
861
const char *zDefault, /* The default value, if not NULL */
862
const char *zExcept /* Omit this skin if not NULL */
863
){
864
int i;
865
Stmt s;
866
@ <select size='1' name='%s(zVarName)'>
867
if( fossil_strcmp(zExcept, "current")!=0 && skin_exists_custom() ){
868
@ <option value='current'>Current Custom Skin</option>
869
}
870
for(i=0; i<count(aBuiltinSkin); i++){
871
const char *zName = aBuiltinSkin[i].zLabel;
872
if( fossil_strcmp(zName, zExcept)==0 ) continue;
873
if( fossil_strcmp(zDefault, zName)==0 ){
874
@ <option value='%s(zName)' selected>\
875
@ %h(aBuiltinSkin[i].zDesc)</option>
876
}else{
877
@ <option value='%s(zName)'>\
878
@ %h(aBuiltinSkin[i].zDesc)</option>
879
}
880
}
881
db_prepare(&s, "SELECT DISTINCT substr(name,1,6) FROM config"
882
" WHERE name GLOB 'draft[1-9]-*' ORDER BY 1");
883
while( db_step(&s)==SQLITE_ROW ){
884
const char *zName = db_column_text(&s, 0);
885
if( fossil_strcmp(zName, zExcept)==0 ) continue;
886
if( fossil_strcmp(zDefault, zName)==0 ){
887
@ <option value='%s(zName)' selected>%s(zName)</option>
888
}else{
889
@ <option value='%s(zName)'>%s(zName)</option>
890
}
891
}
892
db_finalize(&s);
893
@ </select>
894
}
895
896
/*
897
** Return the text of one of the skin files.
898
*/
899
static const char *skin_file_content(const char *zLabel, const char *zFile){
900
const char *zResult;
901
if( fossil_strcmp(zLabel, "current")==0 ){
902
zResult = skin_get(zFile);
903
}else if( sqlite3_strglob("draft[1-9]", zLabel)==0 ){
904
zResult = db_get_mprintf("", "%s-%s", zLabel, zFile);
905
}else{
906
int i;
907
for(i=0; i<2; i++){
908
char *zKey = mprintf("skins/%s/%s.txt", zLabel, zFile);
909
zResult = builtin_text(zKey);
910
fossil_free(zKey);
911
if( zResult!=0 ) break;
912
zLabel = "default";
913
}
914
}
915
return zResult;
916
}
917
918
extern const struct strctCssDefaults {
919
/* From the generated default_css.h, which we cannot #include here
920
** without causing an ODR violation.
921
*/
922
const char *elementClass; /* Name of element needed */
923
const char *value; /* CSS text */
924
} cssDefaultList[];
925
926
/*
927
** WEBPAGE: setup_skinedit
928
**
929
** Edit aspects of a skin determined by the w= query parameter.
930
** Requires Admin or Setup privileges.
931
**
932
** w=NUM -- 0=CSS, 1=footer, 2=header, 3=details, 4=js
933
** sk=NUM -- the draft skin number
934
*/
935
void setup_skinedit(void){
936
static const struct sSkinAddr {
937
const char *zFile;
938
const char *zTitle;
939
const char *zSubmenu;
940
} aSkinAttr[] = {
941
/* 0 */ { "css", "CSS", "CSS", },
942
/* 1 */ { "footer", "Page Footer", "Footer", },
943
/* 2 */ { "header", "Page Header", "Header", },
944
/* 3 */ { "details", "Display Details", "Details", },
945
/* 4 */ { "js", "JavaScript", "Script", },
946
};
947
const char *zBasis; /* The baseline file */
948
const char *zOrig; /* Original content prior to editing */
949
const char *zContent; /* Content after editing */
950
const char *zDflt; /* Default content */
951
char *zDraft; /* Which draft: "draft%d" */
952
char *zTitle; /* Title of this page */
953
const char *zFile; /* One of "css", "footer", "header", "details" */
954
int iSkin; /* draft number. 1..9 */
955
int ii; /* Index in aSkinAttr[] of this file */
956
int j; /* Loop counter */
957
int isRevert = 0; /* True if Revert-to-Baseline was pressed */
958
959
login_check_credentials();
960
961
/* Figure out which skin we are editing */
962
iSkin = atoi(PD("sk","1"));
963
if( iSkin<1 || iSkin>9 ) iSkin = 1;
964
965
/* Check that the user is authorized to edit this skin. */
966
if( !g.perm.Admin ){
967
char *zAllowedEditors = "";
968
Glob *pAllowedEditors;
969
int isMatch = 0;
970
if( login_is_individual() ){
971
zAllowedEditors = db_get_mprintf("", "draft%d-users", iSkin);
972
}
973
if( zAllowedEditors[0] ){
974
pAllowedEditors = glob_create(zAllowedEditors);
975
isMatch = glob_match(pAllowedEditors, g.zLogin);
976
glob_free(pAllowedEditors);
977
}
978
if( isMatch==0 ){
979
login_needed(0);
980
return;
981
}
982
}
983
984
/* figure out which file is to be edited */
985
ii = atoi(PD("w","0"));
986
if( ii<0 || ii>count(aSkinAttr) ) ii = 0;
987
zFile = aSkinAttr[ii].zFile;
988
zDraft = mprintf("draft%d", iSkin);
989
zTitle = mprintf("%s for Draft%d", aSkinAttr[ii].zTitle, iSkin);
990
zBasis = PD("basis","current");
991
zDflt = skin_file_content(zBasis, zFile);
992
zOrig = db_get_mprintf(zDflt, "draft%d-%s",iSkin,zFile);
993
zContent = PD(zFile,zOrig);
994
if( P("revert")!=0 && cgi_csrf_safe(2) ){
995
zContent = zDflt;
996
isRevert = 1;
997
}
998
999
db_begin_transaction();
1000
style_set_current_feature("skins");
1001
style_header("%s", zTitle);
1002
for(j=0; j<count(aSkinAttr); j++){
1003
style_submenu_element(aSkinAttr[j].zSubmenu,
1004
"%R/setup_skinedit?w=%d&basis=%h&sk=%d",j,zBasis,iSkin);
1005
}
1006
@ <form action="%R/setup_skinedit" method="post"><div>
1007
login_insert_csrf_secret();
1008
@ <input type='hidden' name='w' value='%d(ii)'>
1009
@ <input type='hidden' name='sk' value='%d(iSkin)'>
1010
@ <h2>Edit %s(zTitle):</h2>
1011
if( P("submit") && cgi_csrf_safe(2)
1012
&& (zOrig==0 || strcmp(zOrig,zContent)!=0)
1013
){
1014
db_set_mprintf(zContent, 0, "draft%d-%s",iSkin,zFile);
1015
}
1016
@ <textarea name="%s(zFile)" rows="10" cols="80">\
1017
@ %h(zContent)</textarea>
1018
@ <br>
1019
@ <input type="submit" name="submit" value="Apply Changes">
1020
if( isRevert ){
1021
@ &larr; Press to complete reversion to "%s(zBasis)"
1022
}else if( fossil_strcmp(zContent,zDflt)!=0 ){
1023
@ <input type="submit" name="revert" value='Revert To "%s(zBasis)"'>
1024
}
1025
@ <hr>
1026
@ Baseline: \
1027
skin_emit_skin_selector("basis", zBasis, zDraft);
1028
@ <input type="submit" name="diff" value="Unified Diff">
1029
@ <input type="submit" name="sbsdiff" value="Side-by-Side Diff">
1030
if( P("diff")!=0 || P("sbsdiff")!=0 ){
1031
Blob from, to, out;
1032
DiffConfig DCfg;
1033
construct_diff_flags(1, &DCfg);
1034
DCfg.diffFlags |= DIFF_STRIP_EOLCR;
1035
if( P("sbsdiff")!=0 ) DCfg.diffFlags |= DIFF_SIDEBYSIDE;
1036
blob_init(&to, zContent, -1);
1037
blob_init(&from, skin_file_content(zBasis, zFile), -1);
1038
blob_zero(&out);
1039
DCfg.diffFlags |= DIFF_HTML | DIFF_NOTTOOBIG;
1040
if( DCfg.diffFlags & DIFF_SIDEBYSIDE ){
1041
text_diff(&from, &to, &out, &DCfg);
1042
@ %s(blob_str(&out))
1043
}else{
1044
DCfg.diffFlags |= DIFF_LINENO;
1045
text_diff(&from, &to, &out, &DCfg);
1046
@ <pre class="udiff">
1047
@ %s(blob_str(&out))
1048
@ </pre>
1049
}
1050
blob_reset(&from);
1051
blob_reset(&to);
1052
blob_reset(&out);
1053
}
1054
@ </div></form>
1055
style_finish_page();
1056
db_end_transaction(0);
1057
}
1058
1059
/*
1060
** Try to initialize draft skin iSkin to the built-in or preexisting
1061
** skin named by zTemplate.
1062
*/
1063
static void skin_initialize_draft(int iSkin, const char *zTemplate){
1064
int i;
1065
if( zTemplate==0 ) return;
1066
for(i=0; i<count(azSkinFile); i++){
1067
const char *z = skin_file_content(zTemplate, azSkinFile[i]);
1068
db_set_mprintf(z, 0, "draft%d-%s", iSkin, azSkinFile[i]);
1069
}
1070
}
1071
1072
/*
1073
** Publish the draft skin iSkin as the new default.
1074
*/
1075
static void skin_publish(int iSkin){
1076
char *zCurrent; /* SQL description of the current skin */
1077
char *zBuiltin; /* SQL description of a built-in skin */
1078
int i;
1079
int seen = 0; /* True if no need to make a backup */
1080
1081
/* Check to see if the current skin is already saved. If it is, there
1082
** is no need to create a backup */
1083
zCurrent = getSkin(0);
1084
for(i=0; i<count(aBuiltinSkin); i++){
1085
zBuiltin = getSkin(aBuiltinSkin[i].zLabel);
1086
if( fossil_strcmp(zBuiltin, zCurrent)==0 ){
1087
seen = 1;
1088
break;
1089
}
1090
}
1091
if( !seen ){
1092
seen = db_exists("SELECT 1 FROM config WHERE name GLOB 'skin:*'"
1093
" AND value=%Q", zCurrent);
1094
}
1095
if( !seen ){
1096
db_unprotect(PROTECT_CONFIG);
1097
db_multi_exec(
1098
"INSERT INTO config(name,value,mtime) VALUES("
1099
" strftime('skin:Backup On %%Y-%%m-%%d %%H:%%M:%%S'),"
1100
" %Q,now())", zCurrent
1101
);
1102
db_protect_pop();
1103
}
1104
1105
/* Publish draft iSkin */
1106
for(i=0; i<count(azSkinFile); i++){
1107
char *zNew = db_get_mprintf("", "draft%d-%s", iSkin, azSkinFile[i]);
1108
db_set(azSkinFile[i]/*works-like:"x"*/, zNew, 0);
1109
}
1110
db_unset("default-skin", 0);
1111
}
1112
1113
/*
1114
** WEBPAGE: setup_skin
1115
**
1116
** Generate a page showing the steps needed to create or edit
1117
** a custom skin.
1118
*/
1119
void setup_skin(void){
1120
int i; /* Loop counter */
1121
int iSkin; /* Which draft skin is being edited */
1122
int isSetup; /* True for an administrator */
1123
int isEditor; /* Others authorized to make edits */
1124
char *zAllowedEditors; /* Who may edit the draft skin */
1125
char *zBase; /* Base URL for draft under test */
1126
static const char *const azTestPages[] = {
1127
"home",
1128
"timeline",
1129
"dir?ci=tip",
1130
"dir?ci=tip&type=tree",
1131
"brlist",
1132
"info/trunk",
1133
};
1134
1135
/* Figure out which skin we are editing */
1136
iSkin = atoi(PD("sk","1"));
1137
if( iSkin<1 || iSkin>9 ) iSkin = 1;
1138
1139
/* Figure out if the current user is allowed to make administrative
1140
** changes and/or edits
1141
*/
1142
login_check_credentials();
1143
if( !login_is_individual() ){
1144
login_needed(0);
1145
return;
1146
}
1147
zAllowedEditors = db_get_mprintf("", "draft%d-users", iSkin);
1148
if( g.perm.Admin ){
1149
isSetup = isEditor = 1;
1150
}else{
1151
Glob *pAllowedEditors;
1152
isSetup = isEditor = 0;
1153
if( zAllowedEditors[0] ){
1154
pAllowedEditors = glob_create(zAllowedEditors);
1155
isEditor = glob_match(pAllowedEditors, g.zLogin);
1156
glob_free(pAllowedEditors);
1157
}
1158
}
1159
1160
/* Initialize the skin, if requested and authorized. */
1161
if( P("init3")!=0 && isEditor ){
1162
skin_initialize_draft(iSkin, P("initskin"));
1163
}
1164
if( P("submit2")!=0 && isSetup ){
1165
db_set_mprintf(PD("editors",""), 0, "draft%d-users", iSkin);
1166
zAllowedEditors = db_get_mprintf("", "draft%d-users", iSkin);
1167
}
1168
1169
/* Publish the draft skin */
1170
if( P("pub7")!=0 && PB("pub7ck1") && PB("pub7ck2") ){
1171
skin_publish(iSkin);
1172
}
1173
1174
style_set_current_feature("skins");
1175
style_header("Customize Skin");
1176
if( g.perm.Admin ){
1177
style_submenu_element("Skin-Admin", "%R/setup_skin_admin");
1178
}
1179
1180
@ <p>Customize the look of this Fossil repository by making changes
1181
@ to the CSS, Header, Footer, and Detail Settings in one of nine "draft"
1182
@ configurations. Then, after verifying that all is working correctly,
1183
@ publish the draft to become the new main Skin. Users can select a skin
1184
@ of their choice from the built-in ones or the locally-edited one via
1185
@ <a href='%R/skins'>the /skins page</a>.</p>
1186
@
1187
@ <a name='step1'></a>
1188
@ <h1>Step 1: Identify Which Draft To Use</h1>
1189
@
1190
@ <p>The main skin of Fossil cannot be edited directly. Instead,
1191
@ edits are made to one of nine draft skins. A draft skin can then
1192
@ be published to become the default skin.
1193
@ Nine separate drafts are available to facilitate A/B testing.</p>
1194
@
1195
@ <form method='POST' action='%R/setup_skin#step2' id='f01'>
1196
@ <p class='skinInput'>Draft skin to edit:
1197
@ <select size='1' name='sk' id='skStep1'>
1198
for(i=1; i<=9; i++){
1199
if( i==iSkin ){
1200
@ <option value='%d(i)' selected>draft%d(i)</option>
1201
}else{
1202
@ <option value='%d(i)'>draft%d(i)</option>
1203
}
1204
}
1205
@ </select>
1206
@ </p>
1207
@ </form>
1208
@
1209
@ <a name='step2'></a>
1210
@ <h1>Step 2: Authenticate</h1>
1211
@
1212
if( isSetup ){
1213
@ <p>As an administrator, you can make any edits you like to this or
1214
@ any other skin. You can also authorize other users to edit this
1215
@ skin. Any user whose login name matches the comma-separated list
1216
@ of GLOB expressions below is given special permission to edit
1217
@ the draft%d(iSkin) skin:
1218
@
1219
@ <form method='POST' action='%R/setup_skin#step2' id='f02'>
1220
@ <p class='skinInput'>
1221
@ <input type='hidden' name='sk' value='%d(iSkin)'>
1222
@ Authorized editors for skin draft%d(iSkin):
1223
@ <input type='text' name='editors' value='%h(zAllowedEditors)'\
1224
@ width='40'>
1225
@ <input type='submit' name='submit2' value='Change'>
1226
@ </p>
1227
@ </form>
1228
}else if( isEditor ){
1229
@ <p>You are authorized to make changes to the draft%d(iSkin) skin.
1230
@ Continue to the <a href='#step3'>next step</a>.</p>
1231
}else{
1232
@ <p>You are not authorized to make changes to the draft%d(iSkin)
1233
@ skin. Contact the administrator of this Fossil repository for
1234
@ further information.</p>
1235
}
1236
@
1237
@ <a name='step3'></a>
1238
@ <h1>Step 3: Initialize The Draft</h1>
1239
@
1240
if( !isEditor ){
1241
@ <p>You are not allowed to initialize draft%d(iSkin). Contact
1242
@ the administrator for this repository for more information.
1243
}else{
1244
char *zDraft = mprintf("draft%d", iSkin);
1245
@ <p>Initialize the draft%d(iSkin) skin to one of the built-in skins
1246
@ or a preexisting skin, to use as a baseline.</p>
1247
@
1248
@ <form method='POST' action='%R/setup_skin#step4' id='f03'>
1249
@ <p class='skinInput'>
1250
@ <input type='hidden' name='sk' value='%d(iSkin)'>
1251
@ Initialize skin <b>draft%d(iSkin)</b> using
1252
skin_emit_skin_selector("initskin", 0, zDraft);
1253
fossil_free(zDraft);
1254
@ <input type='submit' name='init3' value='Go'>
1255
@ </p>
1256
@ </form>
1257
}
1258
@
1259
@ <a name='step4'></a>
1260
@ <h1>Step 4: Make Edits</h1>
1261
@
1262
if( !isEditor ){
1263
@ <p>You are not authorized to make edits to the draft%d(iSkin) skin.
1264
@ Contact the administrator of this Fossil repository for help.</p>
1265
}else{
1266
@ <p>Edit the components of the draft%d(iSkin) skin:
1267
@ <ul>
1268
@ <li><a href='%R/setup_skinedit?w=0&sk=%d(iSkin)' target='_blank'>CSS</a>
1269
@ <li><a href='%R/setup_skinedit?w=2&sk=%d(iSkin)' target='_blank'>\
1270
@ Header</a>
1271
@ <li><a href='%R/setup_skinedit?w=1&sk=%d(iSkin)' target='_blank'>\
1272
@ Footer</a>
1273
@ <li><a href='%R/setup_skinedit?w=3&sk=%d(iSkin)' target='_blank'>\
1274
@ Details</a>
1275
@ <li><a href='%R/setup_skinedit?w=4&sk=%d(iSkin)' target='_blank'>\
1276
@ Javascript</a> (optional)
1277
@ </ul>
1278
}
1279
@
1280
@ <a name='step5'></a>
1281
@ <h1>Step 5: Verify The Draft Skin</h1>
1282
@
1283
@ <p>To test this draft skin, insert text "/draft%d(iSkin)/" just before the
1284
@ operation name in the URL. Here are a few links to try:
1285
@ <ul>
1286
if( iDraftSkin && sqlite3_strglob("*/draft[1-9]", g.zBaseURL)==0 ){
1287
zBase = mprintf("%.*s/draft%d", (int)strlen(g.zBaseURL)-7,g.zBaseURL,iSkin);
1288
}else{
1289
zBase = mprintf("%s/draft%d", g.zBaseURL, iSkin);
1290
}
1291
for(i=0; i<count(azTestPages); i++){
1292
@ <li><a href='%s(zBase)/%s(azTestPages[i])' target='_blank'>\
1293
@ %s(zBase)/%s(azTestPages[i])</a>
1294
}
1295
fossil_free(zBase);
1296
@ </ul>
1297
@
1298
@ <p>You will probably need to press Reload on your browser before any
1299
@ CSS changes will take effect.</p>
1300
@
1301
@ <a hame='step6'></a>
1302
@ <h1>Step 6: Iterate</h1>
1303
@
1304
@ <p>Repeat <a href='#step4'>step 4</a> and
1305
@ <a href='#step5'>step 5</a> as many times as necessary to create
1306
@ a production-ready skin.
1307
@
1308
@ <a name='step7'></a>
1309
@ <h1>Step 7: Publish</h1>
1310
@
1311
if( !g.perm.Admin ){
1312
@ <p>Only administrators are allowed to publish draft skins. Contact
1313
@ an administrator to get this "draft%d(iSkin)" skin published.</p>
1314
}else{
1315
@ <p>When the draft%d(iSkin) skin is ready for production use,
1316
@ make it the default skin by clicking the acknowledgements and
1317
@ pressing the button below:</p>
1318
@
1319
@ <form method='POST' action='%R/setup_skin#step7'>
1320
@ <p class='skinInput'>
1321
@ <input type='hidden' name='sk' value='%d(iSkin)'>
1322
@ <input type='checkbox' name='pub7ck1' value='yes'>\
1323
@ Skin draft%d(iSkin) has been tested and found ready for production.<br>
1324
@ <input type='checkbox' name='pub7ck2' value='yes'>\
1325
@ The current skin should be overwritten with draft%d(iSkin).<br>
1326
@ <input type='submit' name='pub7' value='Publish Draft%d(iSkin)'>
1327
@ </p></form>
1328
@
1329
@ <p>You will probably need to press Reload on your browser after
1330
@ publishing the new skin.</p>
1331
}
1332
@
1333
@ <a name='step8'></a>
1334
@ <h1>Step 8: Cleanup and Undo Actions</h1>
1335
@
1336
if( !g.perm.Admin ){
1337
@ <p>Administrators can optionally save or restore legacy skins, and/or
1338
@ undo a prior publish.
1339
}else{
1340
@ <p>Visit the <a href='%R/setup_skin_admin'>Skin Admin</a> page
1341
@ for cleanup and recovery actions.
1342
}
1343
builtin_request_js("skin.js");
1344
style_finish_page();
1345
}
1346
1347
/*
1348
** WEBPAGE: skins
1349
**
1350
** Show a list of all of the built-in skins, plus the repository skin,
1351
** and provide the user with an opportunity to change to any of them.
1352
*/
1353
void skins_page(void){
1354
int i;
1355
char *zBase = fossil_strdup(g.zTop);
1356
size_t nBase = strlen(zBase);
1357
login_check_credentials();
1358
if( iDraftSkin && sqlite3_strglob("*/draft?", zBase)==0 ){
1359
nBase -= 7;
1360
zBase[nBase] = 0;
1361
}else if( pAltSkin ){
1362
char *zPattern = mprintf("*/skn_%s", pAltSkin->zLabel);
1363
if( sqlite3_strglob(zPattern, zBase)==0 ){
1364
nBase -= strlen(zPattern)-1;
1365
zBase[nBase] = 0;
1366
}
1367
fossil_free(zPattern);
1368
}
1369
style_header("Skins");
1370
if( iDraftSkin || nSkinRank<=1 ){
1371
@ <p class="warning">Warning:
1372
if( iDraftSkin>0 ){
1373
@ you are using a draft skin,
1374
}else{
1375
@ this fossil instance was started with a hard-coded skin
1376
@ value
1377
}
1378
@ which supersedes any option selected below. A skin selected
1379
@ below will be recorded in your
1380
@ "%z(href("%R/fdscookie"))fossil_display_settings</a>" cookie
1381
@ but will not be used so long as the site has a
1382
@ higher-priority skin in place.
1383
@ </p>
1384
}
1385
@ <p>The following skins are available for this repository:</p>
1386
@ <ul>
1387
for(i=0; i<count(aBuiltinSkin); i++){
1388
if( pAltSkin==&aBuiltinSkin[i] ){
1389
@ <li> %h(aBuiltinSkin[i].zDesc) &larr; <i>Currently in use</i>
1390
}else{
1391
char *zUrl = href("%R/skins?skin=%T", aBuiltinSkin[i].zLabel);
1392
@ <li> %z(zUrl)%h(aBuiltinSkin[i].zDesc)</a>
1393
}
1394
}
1395
if( skin_exists_custom() ){
1396
if( pAltSkin==0 && zAltSkinDir==0 && iDraftSkin==0 ){
1397
@ <li> Custom skin for this repository &larr; <i>Currently in use</i>
1398
}else{
1399
@ <li> %z(href("%R/skins?skin=custom"))\
1400
@ Custom skin for this repository</a>
1401
}
1402
}
1403
@ </ul>
1404
if( iSkinSource<SKIN_FROM_CUSTOM ){
1405
@ <p>The current skin is selected by
1406
switch( iSkinSource ){
1407
case SKIN_FROM_DRAFT:
1408
@ the "debugN" prefix on the PATH_INFO portion of the URL.
1409
break;
1410
case SKIN_FROM_CMDLINE:
1411
@ the "--skin" command-line option on the Fossil server.
1412
break;
1413
case SKIN_FROM_CGI:
1414
@ the "skin:" property in the CGI script that runs the Fossil server.
1415
break;
1416
case SKIN_FROM_QPARAM:
1417
@ the "skin=NAME" query parameter on the URL.
1418
break;
1419
case SKIN_FROM_COOKIE:
1420
@ the "skin" property in the
1421
@ "%z(href("%R/fdscookie"))fossil_display_settings</a>" cookie.
1422
break;
1423
case SKIN_FROM_SETTING:
1424
@ the "default-skin" setting on the repository.
1425
break;
1426
}
1427
}
1428
if( iSkinSource==SKIN_FROM_COOKIE || iSkinSource==SKIN_FROM_QPARAM ){
1429
@ <ul>
1430
@ <li> %z(href("%R/skins?skin="))<i>Let Fossil choose \
1431
@ which skin to use</i></a>
1432
@ </ul>
1433
}
1434
style_finish_page();
1435
if( P("skin")!=0 ){
1436
sqlite3_uint64 x;
1437
sqlite3_randomness(sizeof(x), &x);
1438
cgi_redirectf("%R/skins/%llx", x);
1439
}
1440
fossil_free(zBase);
1441
}
1442

Keyboard Shortcuts

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