Fossil SCM

Rework the Setup/Skin page so that all edits are done on a draft, then tested, then the draft is published to become the default skin.

drh 2017-12-03 11:18 trunk merge
Commit 106fe618f424f700e9bdc02a67c3d3c0c44905163b6e22c9551272c6969b47a1
+34
--- src/db.c
+++ src/db.c
@@ -2473,10 +2473,44 @@
24732473
return db_int(dflt, "SELECT value FROM vvar WHERE name=%Q", zName);
24742474
}
24752475
void db_lset_int(const char *zName, int value){
24762476
db_multi_exec("REPLACE INTO vvar(name,value) VALUES(%Q,%d)", zName, value);
24772477
}
2478
+
2479
+/* Va-args versions of db_get(), db_set(), and db_unset()
2480
+*/
2481
+char *db_get_mprintf(const char *zFormat, const char *zDefault, ...){
2482
+ va_list ap;
2483
+ char *zName;
2484
+ char *zResult;
2485
+ va_start(ap, zDefault);
2486
+ zName = vmprintf(zFormat, ap);
2487
+ va_end(ap);
2488
+ zResult = db_get(zName, zDefault);
2489
+ fossil_free(zName);
2490
+ return zResult;
2491
+}
2492
+void db_set_mprintf(const char *zFormat, const char *zNew, int iGlobal, ...){
2493
+ va_list ap;
2494
+ char *zName;
2495
+ va_start(ap, iGlobal);
2496
+ zName = vmprintf(zFormat, ap);
2497
+ va_end(ap);
2498
+ db_set(zName, zNew, iGlobal);
2499
+ fossil_free(zName);
2500
+}
2501
+void db_unset_mprintf(const char *zFormat, int iGlobal, ...){
2502
+ va_list ap;
2503
+ char *zName;
2504
+ va_start(ap, iGlobal);
2505
+ zName = vmprintf(zFormat, ap);
2506
+ va_end(ap);
2507
+ db_unset(zName, iGlobal);
2508
+ fossil_free(zName);
2509
+}
2510
+
2511
+
24782512
24792513
#if INTERFACE
24802514
/* Manifest generation flags */
24812515
#define MFESTFLG_RAW 0x01
24822516
#define MFESTFLG_UUID 0x02
24832517
--- src/db.c
+++ src/db.c
@@ -2473,10 +2473,44 @@
2473 return db_int(dflt, "SELECT value FROM vvar WHERE name=%Q", zName);
2474 }
2475 void db_lset_int(const char *zName, int value){
2476 db_multi_exec("REPLACE INTO vvar(name,value) VALUES(%Q,%d)", zName, value);
2477 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2478
2479 #if INTERFACE
2480 /* Manifest generation flags */
2481 #define MFESTFLG_RAW 0x01
2482 #define MFESTFLG_UUID 0x02
2483
--- src/db.c
+++ src/db.c
@@ -2473,10 +2473,44 @@
2473 return db_int(dflt, "SELECT value FROM vvar WHERE name=%Q", zName);
2474 }
2475 void db_lset_int(const char *zName, int value){
2476 db_multi_exec("REPLACE INTO vvar(name,value) VALUES(%Q,%d)", zName, value);
2477 }
2478
2479 /* Va-args versions of db_get(), db_set(), and db_unset()
2480 */
2481 char *db_get_mprintf(const char *zFormat, const char *zDefault, ...){
2482 va_list ap;
2483 char *zName;
2484 char *zResult;
2485 va_start(ap, zDefault);
2486 zName = vmprintf(zFormat, ap);
2487 va_end(ap);
2488 zResult = db_get(zName, zDefault);
2489 fossil_free(zName);
2490 return zResult;
2491 }
2492 void db_set_mprintf(const char *zFormat, const char *zNew, int iGlobal, ...){
2493 va_list ap;
2494 char *zName;
2495 va_start(ap, iGlobal);
2496 zName = vmprintf(zFormat, ap);
2497 va_end(ap);
2498 db_set(zName, zNew, iGlobal);
2499 fossil_free(zName);
2500 }
2501 void db_unset_mprintf(const char *zFormat, int iGlobal, ...){
2502 va_list ap;
2503 char *zName;
2504 va_start(ap, iGlobal);
2505 zName = vmprintf(zFormat, ap);
2506 va_end(ap);
2507 db_unset(zName, iGlobal);
2508 fossil_free(zName);
2509 }
2510
2511
2512
2513 #if INTERFACE
2514 /* Manifest generation flags */
2515 #define MFESTFLG_RAW 0x01
2516 #define MFESTFLG_UUID 0x02
2517
+34
--- src/db.c
+++ src/db.c
@@ -2473,10 +2473,44 @@
24732473
return db_int(dflt, "SELECT value FROM vvar WHERE name=%Q", zName);
24742474
}
24752475
void db_lset_int(const char *zName, int value){
24762476
db_multi_exec("REPLACE INTO vvar(name,value) VALUES(%Q,%d)", zName, value);
24772477
}
2478
+
2479
+/* Va-args versions of db_get(), db_set(), and db_unset()
2480
+*/
2481
+char *db_get_mprintf(const char *zFormat, const char *zDefault, ...){
2482
+ va_list ap;
2483
+ char *zName;
2484
+ char *zResult;
2485
+ va_start(ap, zDefault);
2486
+ zName = vmprintf(zFormat, ap);
2487
+ va_end(ap);
2488
+ zResult = db_get(zName, zDefault);
2489
+ fossil_free(zName);
2490
+ return zResult;
2491
+}
2492
+void db_set_mprintf(const char *zFormat, const char *zNew, int iGlobal, ...){
2493
+ va_list ap;
2494
+ char *zName;
2495
+ va_start(ap, iGlobal);
2496
+ zName = vmprintf(zFormat, ap);
2497
+ va_end(ap);
2498
+ db_set(zName, zNew, iGlobal);
2499
+ fossil_free(zName);
2500
+}
2501
+void db_unset_mprintf(const char *zFormat, int iGlobal, ...){
2502
+ va_list ap;
2503
+ char *zName;
2504
+ va_start(ap, iGlobal);
2505
+ zName = vmprintf(zFormat, ap);
2506
+ va_end(ap);
2507
+ db_unset(zName, iGlobal);
2508
+ fossil_free(zName);
2509
+}
2510
+
2511
+
24782512
24792513
#if INTERFACE
24802514
/* Manifest generation flags */
24812515
#define MFESTFLG_RAW 0x01
24822516
#define MFESTFLG_UUID 0x02
24832517
--- src/db.c
+++ src/db.c
@@ -2473,10 +2473,44 @@
2473 return db_int(dflt, "SELECT value FROM vvar WHERE name=%Q", zName);
2474 }
2475 void db_lset_int(const char *zName, int value){
2476 db_multi_exec("REPLACE INTO vvar(name,value) VALUES(%Q,%d)", zName, value);
2477 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2478
2479 #if INTERFACE
2480 /* Manifest generation flags */
2481 #define MFESTFLG_RAW 0x01
2482 #define MFESTFLG_UUID 0x02
2483
--- src/db.c
+++ src/db.c
@@ -2473,10 +2473,44 @@
2473 return db_int(dflt, "SELECT value FROM vvar WHERE name=%Q", zName);
2474 }
2475 void db_lset_int(const char *zName, int value){
2476 db_multi_exec("REPLACE INTO vvar(name,value) VALUES(%Q,%d)", zName, value);
2477 }
2478
2479 /* Va-args versions of db_get(), db_set(), and db_unset()
2480 */
2481 char *db_get_mprintf(const char *zFormat, const char *zDefault, ...){
2482 va_list ap;
2483 char *zName;
2484 char *zResult;
2485 va_start(ap, zDefault);
2486 zName = vmprintf(zFormat, ap);
2487 va_end(ap);
2488 zResult = db_get(zName, zDefault);
2489 fossil_free(zName);
2490 return zResult;
2491 }
2492 void db_set_mprintf(const char *zFormat, const char *zNew, int iGlobal, ...){
2493 va_list ap;
2494 char *zName;
2495 va_start(ap, iGlobal);
2496 zName = vmprintf(zFormat, ap);
2497 va_end(ap);
2498 db_set(zName, zNew, iGlobal);
2499 fossil_free(zName);
2500 }
2501 void db_unset_mprintf(const char *zFormat, int iGlobal, ...){
2502 va_list ap;
2503 char *zName;
2504 va_start(ap, iGlobal);
2505 zName = vmprintf(zFormat, ap);
2506 va_end(ap);
2507 db_unset(zName, iGlobal);
2508 fossil_free(zName);
2509 }
2510
2511
2512
2513 #if INTERFACE
2514 /* Manifest generation flags */
2515 #define MFESTFLG_RAW 0x01
2516 #define MFESTFLG_UUID 0x02
2517
+27 -2
--- src/main.c
+++ src/main.c
@@ -1542,20 +1542,45 @@
15421542
}
15431543
}
15441544
}
15451545
15461546
/* At this point, the appropriate repository database file will have
1547
- ** been opened. Use the first element of PATH_INFO as the page name
1548
- ** and deliver the appropriate page back to the user.
1547
+ ** been opened.
1548
+ **
1549
+ ** Check to see if the the PATH_INFO begins with "draft[1-9]" and if
1550
+ ** so activate the special handling for draft skins
1551
+ */
1552
+ if( zPathInfo && strncmp(zPathInfo,"/draft",6)==0
1553
+ && zPathInfo[6]>='1' && zPathInfo[6]<='9'
1554
+ && (zPathInfo[7]=='/' || zPathInfo[7]==0)
1555
+ ){
1556
+ int iSkin = zPathInfo[6] - '0';
1557
+ char *zNewScript;
1558
+ skin_use_draft(iSkin);
1559
+ zNewScript = mprintf("%s/draft%d", P("SCRIPT_NAME"), iSkin);
1560
+ if( g.zTop ) g.zTop = mprintf("%s/draft%d", g.zTop, iSkin);
1561
+ if( g.zBaseURL ) g.zBaseURL = mprintf("%s/draft%d", g.zBaseURL, iSkin);
1562
+ zPathInfo += 7;
1563
+ cgi_replace_parameter("PATH_INFO", zPathInfo);
1564
+ cgi_replace_parameter("SCRIPT_NAME", zNewScript);
1565
+ }
1566
+
1567
+ /* If the content type is application/x-fossil or
1568
+ ** application/x-fossil-debug, then a sync/push/pull/clone is
1569
+ ** desired, so default the PATH_INFO to /xfer
15491570
*/
15501571
if( g.zContentType &&
15511572
strncmp(g.zContentType, "application/x-fossil", 20)==0 ){
15521573
/* Special case: If the content mimetype shows that it is "fossil sync"
15531574
** payload, then pretend that the PATH_INFO is /xfer so that we always
15541575
** invoke the sync page. */
15551576
zPathInfo = "/xfer";
15561577
}
1578
+
1579
+ /* Use the first element of PATH_INFO as the page name
1580
+ ** and deliver the appropriate page back to the user.
1581
+ */
15571582
set_base_url(0);
15581583
if( zPathInfo==0 || zPathInfo[0]==0
15591584
|| (zPathInfo[0]=='/' && zPathInfo[1]==0) ){
15601585
/* Second special case: If the PATH_INFO is blank, issue a redirect to
15611586
** the home page identified by the "index-page" setting in the repository
15621587
--- src/main.c
+++ src/main.c
@@ -1542,20 +1542,45 @@
1542 }
1543 }
1544 }
1545
1546 /* At this point, the appropriate repository database file will have
1547 ** been opened. Use the first element of PATH_INFO as the page name
1548 ** and deliver the appropriate page back to the user.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1549 */
1550 if( g.zContentType &&
1551 strncmp(g.zContentType, "application/x-fossil", 20)==0 ){
1552 /* Special case: If the content mimetype shows that it is "fossil sync"
1553 ** payload, then pretend that the PATH_INFO is /xfer so that we always
1554 ** invoke the sync page. */
1555 zPathInfo = "/xfer";
1556 }
 
 
 
 
1557 set_base_url(0);
1558 if( zPathInfo==0 || zPathInfo[0]==0
1559 || (zPathInfo[0]=='/' && zPathInfo[1]==0) ){
1560 /* Second special case: If the PATH_INFO is blank, issue a redirect to
1561 ** the home page identified by the "index-page" setting in the repository
1562
--- src/main.c
+++ src/main.c
@@ -1542,20 +1542,45 @@
1542 }
1543 }
1544 }
1545
1546 /* At this point, the appropriate repository database file will have
1547 ** been opened.
1548 **
1549 ** Check to see if the the PATH_INFO begins with "draft[1-9]" and if
1550 ** so activate the special handling for draft skins
1551 */
1552 if( zPathInfo && strncmp(zPathInfo,"/draft",6)==0
1553 && zPathInfo[6]>='1' && zPathInfo[6]<='9'
1554 && (zPathInfo[7]=='/' || zPathInfo[7]==0)
1555 ){
1556 int iSkin = zPathInfo[6] - '0';
1557 char *zNewScript;
1558 skin_use_draft(iSkin);
1559 zNewScript = mprintf("%s/draft%d", P("SCRIPT_NAME"), iSkin);
1560 if( g.zTop ) g.zTop = mprintf("%s/draft%d", g.zTop, iSkin);
1561 if( g.zBaseURL ) g.zBaseURL = mprintf("%s/draft%d", g.zBaseURL, iSkin);
1562 zPathInfo += 7;
1563 cgi_replace_parameter("PATH_INFO", zPathInfo);
1564 cgi_replace_parameter("SCRIPT_NAME", zNewScript);
1565 }
1566
1567 /* If the content type is application/x-fossil or
1568 ** application/x-fossil-debug, then a sync/push/pull/clone is
1569 ** desired, so default the PATH_INFO to /xfer
1570 */
1571 if( g.zContentType &&
1572 strncmp(g.zContentType, "application/x-fossil", 20)==0 ){
1573 /* Special case: If the content mimetype shows that it is "fossil sync"
1574 ** payload, then pretend that the PATH_INFO is /xfer so that we always
1575 ** invoke the sync page. */
1576 zPathInfo = "/xfer";
1577 }
1578
1579 /* Use the first element of PATH_INFO as the page name
1580 ** and deliver the appropriate page back to the user.
1581 */
1582 set_base_url(0);
1583 if( zPathInfo==0 || zPathInfo[0]==0
1584 || (zPathInfo[0]=='/' && zPathInfo[1]==0) ){
1585 /* Second special case: If the PATH_INFO is blank, issue a redirect to
1586 ** the home page identified by the "index-page" setting in the repository
1587
+27 -2
--- src/main.c
+++ src/main.c
@@ -1542,20 +1542,45 @@
15421542
}
15431543
}
15441544
}
15451545
15461546
/* At this point, the appropriate repository database file will have
1547
- ** been opened. Use the first element of PATH_INFO as the page name
1548
- ** and deliver the appropriate page back to the user.
1547
+ ** been opened.
1548
+ **
1549
+ ** Check to see if the the PATH_INFO begins with "draft[1-9]" and if
1550
+ ** so activate the special handling for draft skins
1551
+ */
1552
+ if( zPathInfo && strncmp(zPathInfo,"/draft",6)==0
1553
+ && zPathInfo[6]>='1' && zPathInfo[6]<='9'
1554
+ && (zPathInfo[7]=='/' || zPathInfo[7]==0)
1555
+ ){
1556
+ int iSkin = zPathInfo[6] - '0';
1557
+ char *zNewScript;
1558
+ skin_use_draft(iSkin);
1559
+ zNewScript = mprintf("%s/draft%d", P("SCRIPT_NAME"), iSkin);
1560
+ if( g.zTop ) g.zTop = mprintf("%s/draft%d", g.zTop, iSkin);
1561
+ if( g.zBaseURL ) g.zBaseURL = mprintf("%s/draft%d", g.zBaseURL, iSkin);
1562
+ zPathInfo += 7;
1563
+ cgi_replace_parameter("PATH_INFO", zPathInfo);
1564
+ cgi_replace_parameter("SCRIPT_NAME", zNewScript);
1565
+ }
1566
+
1567
+ /* If the content type is application/x-fossil or
1568
+ ** application/x-fossil-debug, then a sync/push/pull/clone is
1569
+ ** desired, so default the PATH_INFO to /xfer
15491570
*/
15501571
if( g.zContentType &&
15511572
strncmp(g.zContentType, "application/x-fossil", 20)==0 ){
15521573
/* Special case: If the content mimetype shows that it is "fossil sync"
15531574
** payload, then pretend that the PATH_INFO is /xfer so that we always
15541575
** invoke the sync page. */
15551576
zPathInfo = "/xfer";
15561577
}
1578
+
1579
+ /* Use the first element of PATH_INFO as the page name
1580
+ ** and deliver the appropriate page back to the user.
1581
+ */
15571582
set_base_url(0);
15581583
if( zPathInfo==0 || zPathInfo[0]==0
15591584
|| (zPathInfo[0]=='/' && zPathInfo[1]==0) ){
15601585
/* Second special case: If the PATH_INFO is blank, issue a redirect to
15611586
** the home page identified by the "index-page" setting in the repository
15621587
--- src/main.c
+++ src/main.c
@@ -1542,20 +1542,45 @@
1542 }
1543 }
1544 }
1545
1546 /* At this point, the appropriate repository database file will have
1547 ** been opened. Use the first element of PATH_INFO as the page name
1548 ** and deliver the appropriate page back to the user.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1549 */
1550 if( g.zContentType &&
1551 strncmp(g.zContentType, "application/x-fossil", 20)==0 ){
1552 /* Special case: If the content mimetype shows that it is "fossil sync"
1553 ** payload, then pretend that the PATH_INFO is /xfer so that we always
1554 ** invoke the sync page. */
1555 zPathInfo = "/xfer";
1556 }
 
 
 
 
1557 set_base_url(0);
1558 if( zPathInfo==0 || zPathInfo[0]==0
1559 || (zPathInfo[0]=='/' && zPathInfo[1]==0) ){
1560 /* Second special case: If the PATH_INFO is blank, issue a redirect to
1561 ** the home page identified by the "index-page" setting in the repository
1562
--- src/main.c
+++ src/main.c
@@ -1542,20 +1542,45 @@
1542 }
1543 }
1544 }
1545
1546 /* At this point, the appropriate repository database file will have
1547 ** been opened.
1548 **
1549 ** Check to see if the the PATH_INFO begins with "draft[1-9]" and if
1550 ** so activate the special handling for draft skins
1551 */
1552 if( zPathInfo && strncmp(zPathInfo,"/draft",6)==0
1553 && zPathInfo[6]>='1' && zPathInfo[6]<='9'
1554 && (zPathInfo[7]=='/' || zPathInfo[7]==0)
1555 ){
1556 int iSkin = zPathInfo[6] - '0';
1557 char *zNewScript;
1558 skin_use_draft(iSkin);
1559 zNewScript = mprintf("%s/draft%d", P("SCRIPT_NAME"), iSkin);
1560 if( g.zTop ) g.zTop = mprintf("%s/draft%d", g.zTop, iSkin);
1561 if( g.zBaseURL ) g.zBaseURL = mprintf("%s/draft%d", g.zBaseURL, iSkin);
1562 zPathInfo += 7;
1563 cgi_replace_parameter("PATH_INFO", zPathInfo);
1564 cgi_replace_parameter("SCRIPT_NAME", zNewScript);
1565 }
1566
1567 /* If the content type is application/x-fossil or
1568 ** application/x-fossil-debug, then a sync/push/pull/clone is
1569 ** desired, so default the PATH_INFO to /xfer
1570 */
1571 if( g.zContentType &&
1572 strncmp(g.zContentType, "application/x-fossil", 20)==0 ){
1573 /* Special case: If the content mimetype shows that it is "fossil sync"
1574 ** payload, then pretend that the PATH_INFO is /xfer so that we always
1575 ** invoke the sync page. */
1576 zPathInfo = "/xfer";
1577 }
1578
1579 /* Use the first element of PATH_INFO as the page name
1580 ** and deliver the appropriate page back to the user.
1581 */
1582 set_base_url(0);
1583 if( zPathInfo==0 || zPathInfo[0]==0
1584 || (zPathInfo[0]=='/' && zPathInfo[1]==0) ){
1585 /* Second special case: If the PATH_INFO is blank, issue a redirect to
1586 ** the home page identified by the "index-page" setting in the repository
1587
+399 -46
--- src/skins.c
+++ src/skins.c
@@ -2,11 +2,11 @@
22
** Copyright (c) 2009 D. Richard Hipp
33
**
44
** This program is free software; you can redistribute it and/or
55
** modify it under the terms of the Simplified BSD License (also
66
** known as the "2-Clause License" or "FreeBSD License".)
7
-
7
+**
88
** This program is distributed in the hope that it will be useful,
99
** but without any warranty; without even the implied warranty of
1010
** merchantability or fitness for a particular purpose.
1111
**
1212
** Author contact information:
@@ -53,10 +53,15 @@
5353
{ "Black & White, Menu on Left", "black_and_white", 0 },
5454
{ "Plain Gray, No Logo", "plain_gray", 0 },
5555
{ "Khaki, No Logo", "khaki", 0 },
5656
};
5757
58
+/*
59
+** A skin consists of four "files" named here:
60
+*/
61
+static const char *azSkinFile[] = { "css", "header", "footer", "details" };
62
+
5863
/*
5964
** Alternative skins can be specified in the CGI script or by options
6065
** on the "http", "ui", and "server" commands. The alternative skin
6166
** name must be one of the aBuiltinSkin[].zLabel names. If there is
6267
** a match, that alternative is used.
@@ -64,10 +69,11 @@
6469
** The following static variable holds the name of the alternative skin,
6570
** or NULL if the skin should be as configured.
6671
*/
6772
static struct BuiltinSkin *pAltSkin = 0;
6873
static char *zAltSkinDir = 0;
74
+static int iDraftSkin = 0;
6975
7076
/*
7177
** Skin details are a set of key/value pairs that define display
7278
** attributes of the skin that cannot be easily specified using CSS
7379
** or that need to be known on the server-side.
@@ -100,10 +106,14 @@
100106
int i;
101107
Blob err = BLOB_INITIALIZER;
102108
if( strchr(zName, '/')!=0 ){
103109
zAltSkinDir = fossil_strdup(zName);
104110
return 0;
111
+ }
112
+ if( sqlite3_strglob("draft[1-9]", zName)==0 ){
113
+ skin_use_draft(zName[5] - '0');
114
+ return 0;
105115
}
106116
for(i=0; i<count(aBuiltinSkin); i++){
107117
if( fossil_strcmp(aBuiltinSkin[i].zLabel, zName)==0 ){
108118
pAltSkin = &aBuiltinSkin[i];
109119
return 0;
@@ -126,18 +136,33 @@
126136
if( zSkin ){
127137
char *zErr = skin_use_alternative(zSkin);
128138
if( zErr ) fossil_fatal("%s", zErr);
129139
}
130140
}
141
+
142
+/*
143
+** Use one of the draft skins.
144
+*/
145
+void skin_use_draft(int i){
146
+ iDraftSkin = i;
147
+}
131148
132149
/*
133150
** The following routines return the various components of the skin
134151
** that should be used for the current run.
152
+**
153
+** zWhat is one of: "css", "header", "footer", "details".
135154
*/
136155
const char *skin_get(const char *zWhat){
137156
const char *zOut;
138157
char *z;
158
+ if( iDraftSkin ){
159
+ z = mprintf("draft%d-%s", iDraftSkin, zWhat);
160
+ zOut = db_get(z, 0);
161
+ fossil_free(z);
162
+ if( zOut ) return zOut;
163
+ }
139164
if( zAltSkinDir ){
140165
char *z = mprintf("%s/%s.txt", zAltSkinDir, zWhat);
141166
if( file_isfile(z, ExtFILE) ){
142167
Blob x;
143168
blob_read_from_file(&x, z, ExtFILE);
@@ -311,30 +336,29 @@
311336
** Memory to hold the returned string is obtained from malloc.
312337
*/
313338
static char *getSkin(const char *zName){
314339
const char *z;
315340
char *zLabel;
316
- static const char *azType[] = { "css", "header", "footer", "details" };
317341
int i;
318342
Blob val;
319343
blob_zero(&val);
320
- for(i=0; i<count(azType); i++){
344
+ for(i=0; i<count(azSkinFile); i++){
321345
if( zName ){
322
- zLabel = mprintf("skins/%s/%s.txt", zName, azType[i]);
346
+ zLabel = mprintf("skins/%s/%s.txt", zName, azSkinFile[i]);
323347
z = builtin_text(zLabel);
324348
fossil_free(zLabel);
325349
}else{
326
- z = db_get(azType[i], 0);
350
+ z = db_get(azSkinFile[i], 0);
327351
if( z==0 ){
328
- zLabel = mprintf("skins/default/%s.txt", azType[i]);
352
+ zLabel = mprintf("skins/default/%s.txt", azSkinFile[i]);
329353
z = builtin_text(zLabel);
330354
fossil_free(zLabel);
331355
}
332356
}
333357
blob_appendf(&val,
334358
"REPLACE INTO config(name,value,mtime) VALUES(%Q,%Q,now());\n",
335
- azType[i], z
359
+ azSkinFile[i], z
336360
);
337361
}
338362
return blob_str(&val);
339363
}
340364
@@ -355,11 +379,11 @@
355379
style_header("Rename A Skin");
356380
if( ex ){
357381
@ <p><span class="generalError">There is already another skin
358382
@ named "%h(zNewName)". Choose a different name.</span></p>
359383
}
360
- @ <form action="%s(g.zTop)/setup_skin" method="post"><div>
384
+ @ <form action="%s(g.zTop)/setup_skin_admin" method="post"><div>
361385
@ <table border="0"><tr>
362386
@ <tr><td align="right">Current name:<td align="left"><b>%h(zOldName)</b>
363387
@ <tr><td align="right">New name:<td align="left">
364388
@ <input type="text" size="35" name="newname" value="%h(zNewName)">
365389
@ <tr><td><td>
@@ -395,11 +419,11 @@
395419
style_header("Save Current Skin");
396420
if( ex ){
397421
@ <p><span class="generalError">There is already another skin
398422
@ named "%h(zNewName)". Choose a different name.</span></p>
399423
}
400
- @ <form action="%s(g.zTop)/setup_skin" method="post"><div>
424
+ @ <form action="%s(g.zTop)/setup_skin_admin" method="post"><div>
401425
@ <table border="0"><tr>
402426
@ <tr><td align="right">Name for this skin:<td align="left">
403427
@ <input type="text" size="35" name="svname" value="%h(zNewName)">
404428
@ <tr><td><td>
405429
@ <input type="submit" name="save" value="Save">
@@ -417,23 +441,23 @@
417441
);
418442
return 0;
419443
}
420444
421445
/*
422
-** WEBPAGE: setup_skin
446
+** WEBPAGE: setup_skin_admin
423447
**
424
-** Show a list of available skins with buttons for selecting which
425
-** skin to use. Requires Admin privilege.
448
+** Administrative actions on skins. For administrators only.
426449
*/
427
-void setup_skin(void){
450
+void setup_skin_admin(void){
428451
const char *z;
429452
char *zName;
430453
char *zErr = 0;
431454
const char *zCurrent = 0; /* Current skin */
432455
int i; /* Loop counter */
433456
Stmt q;
434457
int seenCurrent = 0;
458
+ int once;
435459
436460
login_check_credentials();
437461
if( !g.perm.Setup ){
438462
login_needed(0);
439463
return;
@@ -445,11 +469,11 @@
445469
}
446470
447471
/* Process requests to delete a user-defined skin */
448472
if( P("del1") && (zName = skinVarName(P("sn"), 1))!=0 ){
449473
style_header("Confirm Custom Skin Delete");
450
- @ <form action="%s(g.zTop)/setup_skin" method="post"><div>
474
+ @ <form action="%s(g.zTop)/setup_skin_admin" method="post"><div>
451475
@ <p>Deletion of a custom skin is a permanent action that cannot
452476
@ be undone. Please confirm that this is what you want to do:</p>
453477
@ <input type="hidden" name="sn" value="%h(P("sn"))" />
454478
@ <input type="submit" name="del2" value="Confirm - Delete The Skin" />
455479
@ <input type="submit" name="cancel" value="Cancel - Do Not Delete" />
@@ -458,10 +482,16 @@
458482
style_footer();
459483
return;
460484
}
461485
if( P("del2")!=0 && (zName = skinVarName(P("sn"), 1))!=0 ){
462486
db_multi_exec("DELETE FROM config WHERE name=%Q", zName);
487
+ }
488
+ if( P("draftdel")!=0 ){
489
+ const char *zDraft = P("name");
490
+ if( sqlite3_strglob("draft[1-9]",zDraft)==0 ){
491
+ db_multi_exec("DELETE FROM config WHERE name GLOB '%q-*'", zDraft);
492
+ }
463493
}
464494
if( skinRename() ) return;
465495
if( skinSave(zCurrent) ) return;
466496
467497
/* The user pressed one of the "Install" buttons. */
@@ -506,36 +536,20 @@
506536
507537
style_header("Skins");
508538
if( zErr ){
509539
@ <p style="color:red">%h(zErr)</p>
510540
}
511
- @ <p>A "skin" is a combination of
512
- @ <a href="setup_skinedit?w=0">CSS</a>,
513
- @ <a href="setup_skinedit?w=2">Header</a>,
514
- @ <a href="setup_skinedit?w=1">Footer</a>, and
515
- @ <a href="setup_skinedit?w=3">Details</a>
516
- @ that determines the look and feel
517
- @ of the web interface.</p>
518
- @
519
- if( pAltSkin ){
520
- @ <p class="generalError">
521
- @ This page is generated using an skin override named
522
- @ "%h(pAltSkin->zLabel)". You can change the skin configuration
523
- @ below, but the changes will not take effect until the Fossil server
524
- @ is restarted without the override.</p>
525
- @
526
- }
527
- @ <h2>Available Skins:</h2>
528541
@ <table border="0">
542
+ @ <tr><td colspan=4><h2>Built-in Skins:</h2></td></th>
529543
for(i=0; i<count(aBuiltinSkin); i++){
530544
z = aBuiltinSkin[i].zDesc;
531545
@ <tr><td>%d(i+1).<td>%h(z)<td>&nbsp;&nbsp;<td>
532546
if( fossil_strcmp(aBuiltinSkin[i].zSQL, zCurrent)==0 ){
533547
@ (Currently In Use)
534548
seenCurrent = 1;
535549
}else{
536
- @ <form action="%s(g.zTop)/setup_skin" method="post">
550
+ @ <form action="%s(g.zTop)/setup_skin_admin" method="post">
537551
@ <input type="hidden" name="sn" value="%h(z)" />
538552
@ <input type="submit" name="load" value="Install" />
539553
if( pAltSkin==&aBuiltinSkin[i] ){
540554
@ (Current override)
541555
}
@@ -546,16 +560,22 @@
546560
db_prepare(&q,
547561
"SELECT substr(name, 6), value FROM config"
548562
" WHERE name GLOB 'skin:*'"
549563
" ORDER BY name"
550564
);
565
+ once = 1;
551566
while( db_step(&q)==SQLITE_ROW ){
552567
const char *zN = db_column_text(&q, 0);
553568
const char *zV = db_column_text(&q, 1);
554569
i++;
570
+ if( once ){
571
+ once = 0;
572
+ @ <tr><td colspan=4><h2>Skins saved as "skin:*' entries \
573
+ @ in the CONFIG table:</h2></td></tr>
574
+ }
555575
@ <tr><td>%d(i).<td>%h(zN)<td>&nbsp;&nbsp;<td>
556
- @ <form action="%s(g.zTop)/setup_skin" method="post">
576
+ @ <form action="%s(g.zTop)/setup_skin_admin" method="post">
557577
if( fossil_strcmp(zV, zCurrent)==0 ){
558578
@ (Currently In Use)
559579
seenCurrent = 1;
560580
}else{
561581
@ <input type="submit" name="load" value="Install">
@@ -566,28 +586,52 @@
566586
@ </form></tr>
567587
}
568588
db_finalize(&q);
569589
if( !seenCurrent ){
570590
i++;
571
- @ <tr><td>%d(i).<td><i>Current Configuration</i><td>&nbsp;&nbsp;<td>
572
- @ <form action="%s(g.zTop)/setup_skin" method="post">
573
- @ <input type="submit" name="save" value="Save">
591
+ @ <tr><td colspan=4><h2>Current skin in css/header/footer/details entries \
592
+ @ in the CONFIG table:</h2></td></tr>
593
+ @ <tr><td>%d(i).<td><i>Current</i><td>&nbsp;&nbsp;<td>
594
+ @ <form action="%s(g.zTop)/setup_skin_admin" method="post">
595
+ @ <input type="submit" name="save" value="Backup">
574596
@ </form>
575597
}
598
+ db_prepare(&q,
599
+ "SELECT DISTINCT substr(name, 1, 6) FROM config"
600
+ " WHERE name GLOB 'draft[1-9]-*'"
601
+ " ORDER BY name"
602
+ );
603
+ once = 1;
604
+ while( db_step(&q)==SQLITE_ROW ){
605
+ const char *zN = db_column_text(&q, 0);
606
+ i++;
607
+ if( once ){
608
+ once = 0;
609
+ @ <tr><td colspan=4><h2>Draft skins stored as "draft[1-9]-*' entries \
610
+ @ in the CONFIG table:</h2></td></tr>
611
+ }
612
+ @ <tr><td>%d(i).<td>%h(zN)<td>&nbsp;&nbsp;<td>
613
+ @ <form action="%s(g.zTop)/setup_skin_admin" method="post">
614
+ @ <input type="submit" name="draftdel" value="Delete">
615
+ @ <input type="hidden" name="name" value="%h(zN)">
616
+ @ </form></tr>
617
+ }
618
+ db_finalize(&q);
619
+
576620
@ </table>
577621
style_footer();
578622
db_end_transaction(0);
579623
}
580624
581
-
582625
/*
583626
** WEBPAGE: setup_skinedit
584627
**
585628
** Edit aspects of a skin determined by the w= query parameter.
586
-** Requires Admin privileges.
629
+** Requires Setup privileges.
587630
**
588
-** w=N -- 0=CSS, 1=footer, 2=header, 3=details
631
+** w=NUM -- 0=CSS, 1=footer, 2=header, 3=details
632
+** sk=NUM -- the draft skin number
589633
*/
590634
void setup_skinedit(void){
591635
static const struct sSkinAddr {
592636
const char *zFile;
593637
const char *zTitle;
@@ -599,39 +643,61 @@
599643
/* 3 */ { "details", "Display Details", "Details", },
600644
};
601645
const char *zBasis;
602646
const char *zContent;
603647
char *zDflt;
648
+ char *zKey;
649
+ char *zTitle;
650
+ int iSkin;
604651
int ii;
605652
int j;
606653
607654
login_check_credentials();
655
+
656
+ /* Figure out which skin we are editing */
657
+ iSkin = atoi(PD("sk","1"));
658
+ if( iSkin<1 || iSkin>9 ) iSkin = 1;
659
+
660
+ /* Check that the user is authorized to edit this skin. */
608661
if( !g.perm.Setup ){
609
- login_needed(0);
610
- return;
662
+ char *zAllowedEditors = db_get_mprintf("draft%d-users", "", iSkin);
663
+ Glob *pAllowedEditors;
664
+ if( zAllowedEditors[0] ){
665
+ pAllowedEditors = glob_create(zAllowedEditors);
666
+ if( !glob_match(pAllowedEditors, zAllowedEditors) ){
667
+ login_needed(0);
668
+ return;
669
+ }
670
+ glob_free(pAllowedEditors);
671
+ }
611672
}
673
+
674
+ /* figure out which file is to be edited */
612675
ii = atoi(PD("w","0"));
613676
if( ii<0 || ii>count(aSkinAttr) ) ii = 0;
677
+ zKey = mprintf("draft%d-%s", iSkin, aSkinAttr[ii].zFile);
678
+ zTitle = mprintf("%s for Draft%d", aSkinAttr[ii].zTitle, iSkin);
679
+
614680
zBasis = PD("basis","default");
615681
zDflt = mprintf("skins/%s/%s.txt", zBasis, aSkinAttr[ii].zFile);
616682
db_begin_transaction();
617683
if( P("revert")!=0 ){
618684
db_multi_exec("DELETE FROM config WHERE name=%Q", aSkinAttr[ii].zFile);
619685
cgi_replace_parameter(aSkinAttr[ii].zFile, builtin_text(zDflt));
620686
}
621
- style_header("%s", aSkinAttr[ii].zTitle);
687
+ style_header("%s", zTitle);
622688
for(j=0; j<count(aSkinAttr); j++){
623689
if( j==ii ) continue;
624690
style_submenu_element(aSkinAttr[j].zSubmenu,
625
- "%R/setup_skinedit?w=%d&basis=%h",j,zBasis);
691
+ "%R/setup_skinedit?w=%d&basis=%h&sk=%d",j,zBasis,iSkin);
626692
}
627
- style_submenu_element("Skins", "%R/setup_skin");
628693
@ <form action="%s(g.zTop)/setup_skinedit" method="post"><div>
629694
login_insert_csrf_secret();
630695
@ <input type='hidden' name='w' value='%d(ii)'>
631
- @ <h2>Edit %s(aSkinAttr[ii].zTitle):</h2>
632
- zContent = textarea_attribute("", 10, 80, aSkinAttr[ii].zFile,
696
+ @ <input type='hidden' name='sk' value='%d(iSkin)'>
697
+ @ <h2>Edit %s(zTitle):</h2>
698
+ zContent = textarea_attribute("", 10, 80, zKey,
633699
aSkinAttr[ii].zFile, builtin_text(zDflt), 0);
634700
@ <br />
635701
@ <input type="submit" name="submit" value="Apply Changes" />
636702
@ <hr />
637703
@ Baseline: <select size='1' name='basis'>
@@ -668,5 +734,292 @@
668734
}
669735
@ </div></form>
670736
style_footer();
671737
db_end_transaction(0);
672738
}
739
+
740
+/*
741
+** Try to initialize draft skin iSkin to the built-in or preexisting
742
+** skin named by zTemplate.
743
+*/
744
+static void skin_initialize_draft(int iSkin, const char *zTemplate){
745
+ int i;
746
+ if( zTemplate==0 ) return;
747
+ if( strcmp(zTemplate, "current")==0 ){
748
+ for(i=0; i<count(azSkinFile); i++){
749
+ db_set_mprintf("draft%d-%s", db_get(azSkinFile[i],""), 0,
750
+ iSkin, azSkinFile[i]);
751
+ }
752
+ return;
753
+ }
754
+ for(i=0; i<count(aBuiltinSkin); i++){
755
+ if( strcmp(zTemplate, aBuiltinSkin[i].zLabel)==0 ){
756
+ for(i=0; i<count(azSkinFile); i++){
757
+ char *zKey = mprintf("skins/%s/%s.txt", zTemplate, azSkinFile[i]);
758
+ db_set_mprintf("draft%d-%s", builtin_text(zKey), 0,
759
+ iSkin, azSkinFile[i]);
760
+ }
761
+ return;
762
+ }
763
+ }
764
+}
765
+
766
+/*
767
+** Publish the draft skin iSkin as the new default.
768
+*/
769
+static void skin_publish(int iSkin){
770
+ char *zCurrent; /* SQL description of the current skin */
771
+ char *zBuiltin; /* SQL description of a built-in skin */
772
+ int i;
773
+ int seen = 0; /* True if no need to make a backup */
774
+
775
+ /* Check to see if the current skin is already saved. If it is, there
776
+ ** is no need to create a backup */
777
+ zCurrent = getSkin(0);
778
+ for(i=0; i<count(aBuiltinSkin); i++){
779
+ zBuiltin = getSkin(aBuiltinSkin[i].zLabel);
780
+ if( fossil_strcmp(aBuiltinSkin[i].zSQL, zCurrent)==0 ){
781
+ seen = 1;
782
+ break;
783
+ }
784
+ }
785
+ if( !seen ){
786
+ seen = db_exists("SELECT 1 FROM config WHERE name GLOB 'skin:*'"
787
+ " AND value=%Q", zCurrent);
788
+ }
789
+ if( !seen ){
790
+ db_multi_exec(
791
+ "INSERT INTO config(name,value,mtime) VALUES("
792
+ " strftime('skin:Backup On %%Y-%%m-%%d %%H:%%M:%%S'),"
793
+ " %Q,now())", zCurrent
794
+ );
795
+ }
796
+
797
+ /* Publish draft iSkin */
798
+ for(i=0; i<count(azSkinFile); i++){
799
+ char *zNew = db_get_mprintf("draft%d-%s", "", iSkin, azSkinFile[i]);
800
+ db_set(azSkinFile[i], zNew, 0);
801
+ }
802
+}
803
+
804
+/*
805
+** WEBPAGE: setup_skin
806
+**
807
+** Generate a page showing the steps needed to customize a skin.
808
+*/
809
+void setup_skin(void){
810
+ int i; /* Loop counter */
811
+ int iSkin; /* Which draft skin is being edited */
812
+ int isSetup; /* True for an administrator */
813
+ int isEditor; /* Others authorized to make edits */
814
+ char *zAllowedEditors; /* Who may edit the draft skin */
815
+ char *zBase; /* Base URL for draft under test */
816
+ static const char *azTestPages[] = {
817
+ "home",
818
+ "timeline",
819
+ "dir?ci=tip",
820
+ "dir?ci=tip&type=tree",
821
+ "brlist",
822
+ "info/trunk",
823
+ };
824
+
825
+ /* Figure out which skin we are editing */
826
+ iSkin = atoi(PD("sk","1"));
827
+ if( iSkin<1 || iSkin>9 ) iSkin = 1;
828
+
829
+ /* Figure out if the current user is allowed to make administrative
830
+ ** changes and/or edits
831
+ */
832
+ login_check_credentials();
833
+ zAllowedEditors = db_get_mprintf("draft%d-users", "", iSkin);
834
+ if( g.perm.Setup ){
835
+ isSetup = isEditor = 1;
836
+ }else{
837
+ Glob *pAllowedEditors;
838
+ isSetup = isEditor = 0;
839
+ if( zAllowedEditors[0] ){
840
+ pAllowedEditors = glob_create(zAllowedEditors);
841
+ isEditor = glob_match(pAllowedEditors, zAllowedEditors);
842
+ glob_free(pAllowedEditors);
843
+ }
844
+ }
845
+
846
+ /* Initialize the skin, if requested and authorized. */
847
+ if( P("init3")!=0 && isEditor ){
848
+ skin_initialize_draft(iSkin, P("initskin"));
849
+ }
850
+ if( P("submit2")!=0 && isSetup ){
851
+ db_set_mprintf("draft%d-users", PD("editors",""), 0, iSkin);
852
+ zAllowedEditors = db_get_mprintf("draft%d-users", "", iSkin);
853
+ }
854
+
855
+ /* Publish the draft skin */
856
+ if( P("pub7")!=0 && PB("pub7ck1") && PB("pub7ck2") ){
857
+ skin_publish(iSkin);
858
+ }
859
+
860
+ style_header("Customize Skin");
861
+
862
+ @ <p>Customize the look of this Fossil repository by making changes
863
+ @ to the CSS, Header, Footer, and Detail Settings in one of nine "draft"
864
+ @ configurations. Then, after verifying that all is working correctly,
865
+ @ publish the draft to become the new main Skin.<p>
866
+ @
867
+ @ <a name='step1'></a>
868
+ @ <h1>Step 1: Identify Which Draft To Use</h1>
869
+ @
870
+ @ <p>The main skin of Fossil cannot be edited directly. Instead,
871
+ @ edits are made to one of nine draft skins. A draft skin can then
872
+ @ be published to become the default skin.
873
+ @ Nine separate drafts are available to facilitate A/B testing.</p>
874
+ @
875
+ @ <form method='POST' action='%R/setup_skin#step2' id='f01'>
876
+ @ <p class='skinInput'>Draft skin to edit:
877
+ @ <select size='1' name='sk' onchange='gebi("f01").submit()'>
878
+ for(i=1; i<=9; i++){
879
+ if( i==iSkin ){
880
+ @ <option value='%d(i)' selected>draft%d(i)</option>
881
+ }else{
882
+ @ <option value='%d(i)'>draft%d(i)</option>
883
+ }
884
+ }
885
+ @ </select>
886
+ @ </p>
887
+ @
888
+ @ <a name='step2'></a>
889
+ @ <h1>Step 2: Authenticate</h1>
890
+ @
891
+ if( isSetup ){
892
+ @ <p>As an administrator, you can make any edits you like to this or
893
+ @ any other skin. You can also authorized other users to edit this
894
+ @ skin. Any user whose login name matches the comma-separate list
895
+ @ of GLOB expressions below is given special permission to edit
896
+ @ the draft%d(iSkin) skin:
897
+ @
898
+ @ <form method='POST' action='%R/setup_skin#step2' id='f02'>
899
+ @ <p class='skinInput'>
900
+ @ <input type='hidden' name='sk' value='%d(iSkin)'>
901
+ @ Authorized editors for skin draft%d(iSkin):
902
+ @ <input type='text' name='editors' value='%h(zAllowedEditors)'\
903
+ @ width='40'>
904
+ @ <input type='submit' name='submit2' value='Change'>
905
+ @ </p>
906
+ @ </form>
907
+ }else if( isEditor ){
908
+ @ <p>You are authorized to make changes to the draft%d(iSkin) skin.
909
+ @ Continue to the <a href='#step3'>next step</a>.</p>
910
+ }else{
911
+ @ <p>You are not authorized to make changes to the draft%d(iSkin)
912
+ @ skin. Contact the administrator of this Fossil repository for
913
+ @ further information.</p>
914
+ }
915
+ @
916
+ @ <a name='step3'></a>
917
+ @ <h1>Step 3: Initialize The Draft</h1>
918
+ @
919
+ if( !isEditor ){
920
+ @ <p>You are not allowed to initialize draft%(iSkin). Contact
921
+ @ the administrator for this repository for more information.
922
+ }else{
923
+ @ <p>Initialize the draft%d(iSkin) skin to one of the built-in skins
924
+ @ or a preexisting skin, to use as a baseline.</p>
925
+ @
926
+ @ <form method='POST' action='%R/setup_skin#step4' id='f03'>
927
+ @ <p class='skinInput'>
928
+ @ <input type='hidden' name='sk' value='%d(iSkin)'>
929
+ @ Initialize skin <b>draft%d(iSkin)</b> using
930
+ @ <select size='1' name='initskin'>
931
+ @ <option value='current'>Currently In Use</option>
932
+ for(i=0; i<count(aBuiltinSkin); i++){
933
+ @ <option value='%s(aBuiltinSkin[i].zLabel)'>\
934
+ @ %h(aBuiltinSkin[i].zDesc) (built-in)</option>
935
+ }
936
+ @ </select>
937
+ @ <input type='submit' name='init3' value='Go'>
938
+ @ </p>
939
+ @ </form>
940
+ }
941
+ @
942
+ @ <a name='step4'></a>
943
+ @ <h1>Step 4: Make Edits</h1>
944
+ @
945
+ if( !isEditor ){
946
+ @ <p>You are not authorized to make edits to the draft%d(iSkin) skin.
947
+ @ Contact the administrator of this Fossil repository for help.</p>
948
+ }else{
949
+ @ <p>Edit the components of the draft%d(iSkin) skin:
950
+ @ <ul>
951
+ @ <li><a href='%R/setup_skinedit?w=0&sk=%d(iSkin)' target='_blank'>CSS</a>
952
+ @ <li><a href='%R/setup_skinedit?w=2&sk=%d(iSkin)' target='_blank'>\
953
+ @ Header</a>
954
+ @ <li><a href='%R/setup_skinedit?w=1&sk=%d(iSkin)' target='_blank'>\
955
+ @ Footer</a>
956
+ @ <li><a href='%R/setup_skinedit?w=3&sk=%d(iSkin)' target='_blank'>\
957
+ @ Details</a>
958
+ @ </ul>
959
+ }
960
+ @
961
+ @ <a name='step5'></a>
962
+ @ <h1>Step 5: Verify The Draft Skin</h1>
963
+ @
964
+ @ <p>To test this draft skin, insert text "/draft%d(iSkin)/" just before the
965
+ @ operation name in the URL. Here are a few links to try:
966
+ @ <ul>
967
+ if( iDraftSkin && sqlite3_strglob("*/draft[1-9]", g.zBaseURL)==0 ){
968
+ zBase = mprintf("%.*s/draft%d", (int)strlen(g.zBaseURL)-7,g.zBaseURL,iSkin);
969
+ }else{
970
+ zBase = mprintf("%s/draft%d", g.zBaseURL, iSkin);
971
+ }
972
+ for(i=0; i<count(azTestPages); i++){
973
+ @ <li><a href='%s(zBase)/%s(azTestPages[i])' target='_blank'>\
974
+ @ %s(zBase)/%s(azTestPages[i])</a>
975
+ }
976
+ fossil_free(zBase);
977
+ @ </ul>
978
+ @
979
+ @ <p>You will probably need to press Reload on your browser before any
980
+ @ CSS changes will take effect.</p>
981
+ @
982
+ @ <a hame='step6'></a>
983
+ @ <h1>Step 6: Interate</h1>
984
+ @
985
+ @ <p>Repeat <a href='#step4'>step 4</a> and
986
+ @ <a href='#step5'>step 5</a> as many times as necessary to create
987
+ @ a production-ready skin.
988
+ @
989
+ @ <a name='step7'></a>
990
+ @ <h1>Step 7: Publish</h1>
991
+ @
992
+ if( !g.perm.Setup ){
993
+ @ <p>Only administrators are allowed to publish draft skins. Contact
994
+ @ an administrator to get this "draft%d(iSkin)" skin published.</p>
995
+ }else{
996
+ @ <p>When the draft%d(iSkin) skin is ready for production use,
997
+ @ make it the default scan by clicking the acknowledgements and
998
+ @ pressing the button below:</p>
999
+ @
1000
+ @ <form method='POST' action='%R/setup_skin#step7'>
1001
+ @ <p class='skinInput'>
1002
+ @ <input type='hidden' name='sk' value='%d(iSkin)'>
1003
+ @ <input type='checkbox' name='pub7ck1' value='yes'>\
1004
+ @ Skin draft%d(iSkin) has been testing and found ready for production.<br>
1005
+ @ <input type='checkbox' name='pub7ck2' value='yes'>\
1006
+ @ The current skin should be overwritten with draft%d(iSkin).<br>
1007
+ @ <input type='submit' name='pub7' value='Publish Draft%d(iSkin)'>
1008
+ @ </p></form>
1009
+ @
1010
+ @ <p>You will probably need to press Reload on your browser after
1011
+ @ publishing the new skin.</p>
1012
+ }
1013
+ @
1014
+ @ <a name='step8'></a>
1015
+ @ <h1>Step 8: Cleanup and Undo Actions</h1>
1016
+ @
1017
+ if( !g.perm.Setup ){
1018
+ @ <p>Administrators can optionally remove save legacy skins, or
1019
+ @ undo a prior publish
1020
+ }else{
1021
+ @ <p>Visit the <a href='%R/setup_skin_admin'>Skin Admin</a> page
1022
+ @ for cleanup and recovery actions.
1023
+ }
1024
+ style_footer();
1025
+}
6731026
--- src/skins.c
+++ src/skins.c
@@ -2,11 +2,11 @@
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:
@@ -53,10 +53,15 @@
53 { "Black & White, Menu on Left", "black_and_white", 0 },
54 { "Plain Gray, No Logo", "plain_gray", 0 },
55 { "Khaki, No Logo", "khaki", 0 },
56 };
57
 
 
 
 
 
58 /*
59 ** Alternative skins can be specified in the CGI script or by options
60 ** on the "http", "ui", and "server" commands. The alternative skin
61 ** name must be one of the aBuiltinSkin[].zLabel names. If there is
62 ** a match, that alternative is used.
@@ -64,10 +69,11 @@
64 ** The following static variable holds the name of the alternative skin,
65 ** or NULL if the skin should be as configured.
66 */
67 static struct BuiltinSkin *pAltSkin = 0;
68 static char *zAltSkinDir = 0;
 
69
70 /*
71 ** Skin details are a set of key/value pairs that define display
72 ** attributes of the skin that cannot be easily specified using CSS
73 ** or that need to be known on the server-side.
@@ -100,10 +106,14 @@
100 int i;
101 Blob err = BLOB_INITIALIZER;
102 if( strchr(zName, '/')!=0 ){
103 zAltSkinDir = fossil_strdup(zName);
104 return 0;
 
 
 
 
105 }
106 for(i=0; i<count(aBuiltinSkin); i++){
107 if( fossil_strcmp(aBuiltinSkin[i].zLabel, zName)==0 ){
108 pAltSkin = &aBuiltinSkin[i];
109 return 0;
@@ -126,18 +136,33 @@
126 if( zSkin ){
127 char *zErr = skin_use_alternative(zSkin);
128 if( zErr ) fossil_fatal("%s", zErr);
129 }
130 }
 
 
 
 
 
 
 
131
132 /*
133 ** The following routines return the various components of the skin
134 ** that should be used for the current run.
 
 
135 */
136 const char *skin_get(const char *zWhat){
137 const char *zOut;
138 char *z;
 
 
 
 
 
 
139 if( zAltSkinDir ){
140 char *z = mprintf("%s/%s.txt", zAltSkinDir, zWhat);
141 if( file_isfile(z, ExtFILE) ){
142 Blob x;
143 blob_read_from_file(&x, z, ExtFILE);
@@ -311,30 +336,29 @@
311 ** Memory to hold the returned string is obtained from malloc.
312 */
313 static char *getSkin(const char *zName){
314 const char *z;
315 char *zLabel;
316 static const char *azType[] = { "css", "header", "footer", "details" };
317 int i;
318 Blob val;
319 blob_zero(&val);
320 for(i=0; i<count(azType); i++){
321 if( zName ){
322 zLabel = mprintf("skins/%s/%s.txt", zName, azType[i]);
323 z = builtin_text(zLabel);
324 fossil_free(zLabel);
325 }else{
326 z = db_get(azType[i], 0);
327 if( z==0 ){
328 zLabel = mprintf("skins/default/%s.txt", azType[i]);
329 z = builtin_text(zLabel);
330 fossil_free(zLabel);
331 }
332 }
333 blob_appendf(&val,
334 "REPLACE INTO config(name,value,mtime) VALUES(%Q,%Q,now());\n",
335 azType[i], z
336 );
337 }
338 return blob_str(&val);
339 }
340
@@ -355,11 +379,11 @@
355 style_header("Rename A Skin");
356 if( ex ){
357 @ <p><span class="generalError">There is already another skin
358 @ named "%h(zNewName)". Choose a different name.</span></p>
359 }
360 @ <form action="%s(g.zTop)/setup_skin" method="post"><div>
361 @ <table border="0"><tr>
362 @ <tr><td align="right">Current name:<td align="left"><b>%h(zOldName)</b>
363 @ <tr><td align="right">New name:<td align="left">
364 @ <input type="text" size="35" name="newname" value="%h(zNewName)">
365 @ <tr><td><td>
@@ -395,11 +419,11 @@
395 style_header("Save Current Skin");
396 if( ex ){
397 @ <p><span class="generalError">There is already another skin
398 @ named "%h(zNewName)". Choose a different name.</span></p>
399 }
400 @ <form action="%s(g.zTop)/setup_skin" method="post"><div>
401 @ <table border="0"><tr>
402 @ <tr><td align="right">Name for this skin:<td align="left">
403 @ <input type="text" size="35" name="svname" value="%h(zNewName)">
404 @ <tr><td><td>
405 @ <input type="submit" name="save" value="Save">
@@ -417,23 +441,23 @@
417 );
418 return 0;
419 }
420
421 /*
422 ** WEBPAGE: setup_skin
423 **
424 ** Show a list of available skins with buttons for selecting which
425 ** skin to use. Requires Admin privilege.
426 */
427 void setup_skin(void){
428 const char *z;
429 char *zName;
430 char *zErr = 0;
431 const char *zCurrent = 0; /* Current skin */
432 int i; /* Loop counter */
433 Stmt q;
434 int seenCurrent = 0;
 
435
436 login_check_credentials();
437 if( !g.perm.Setup ){
438 login_needed(0);
439 return;
@@ -445,11 +469,11 @@
445 }
446
447 /* Process requests to delete a user-defined skin */
448 if( P("del1") && (zName = skinVarName(P("sn"), 1))!=0 ){
449 style_header("Confirm Custom Skin Delete");
450 @ <form action="%s(g.zTop)/setup_skin" method="post"><div>
451 @ <p>Deletion of a custom skin is a permanent action that cannot
452 @ be undone. Please confirm that this is what you want to do:</p>
453 @ <input type="hidden" name="sn" value="%h(P("sn"))" />
454 @ <input type="submit" name="del2" value="Confirm - Delete The Skin" />
455 @ <input type="submit" name="cancel" value="Cancel - Do Not Delete" />
@@ -458,10 +482,16 @@
458 style_footer();
459 return;
460 }
461 if( P("del2")!=0 && (zName = skinVarName(P("sn"), 1))!=0 ){
462 db_multi_exec("DELETE FROM config WHERE name=%Q", zName);
 
 
 
 
 
 
463 }
464 if( skinRename() ) return;
465 if( skinSave(zCurrent) ) return;
466
467 /* The user pressed one of the "Install" buttons. */
@@ -506,36 +536,20 @@
506
507 style_header("Skins");
508 if( zErr ){
509 @ <p style="color:red">%h(zErr)</p>
510 }
511 @ <p>A "skin" is a combination of
512 @ <a href="setup_skinedit?w=0">CSS</a>,
513 @ <a href="setup_skinedit?w=2">Header</a>,
514 @ <a href="setup_skinedit?w=1">Footer</a>, and
515 @ <a href="setup_skinedit?w=3">Details</a>
516 @ that determines the look and feel
517 @ of the web interface.</p>
518 @
519 if( pAltSkin ){
520 @ <p class="generalError">
521 @ This page is generated using an skin override named
522 @ "%h(pAltSkin->zLabel)". You can change the skin configuration
523 @ below, but the changes will not take effect until the Fossil server
524 @ is restarted without the override.</p>
525 @
526 }
527 @ <h2>Available Skins:</h2>
528 @ <table border="0">
 
529 for(i=0; i<count(aBuiltinSkin); i++){
530 z = aBuiltinSkin[i].zDesc;
531 @ <tr><td>%d(i+1).<td>%h(z)<td>&nbsp;&nbsp;<td>
532 if( fossil_strcmp(aBuiltinSkin[i].zSQL, zCurrent)==0 ){
533 @ (Currently In Use)
534 seenCurrent = 1;
535 }else{
536 @ <form action="%s(g.zTop)/setup_skin" method="post">
537 @ <input type="hidden" name="sn" value="%h(z)" />
538 @ <input type="submit" name="load" value="Install" />
539 if( pAltSkin==&aBuiltinSkin[i] ){
540 @ (Current override)
541 }
@@ -546,16 +560,22 @@
546 db_prepare(&q,
547 "SELECT substr(name, 6), value FROM config"
548 " WHERE name GLOB 'skin:*'"
549 " ORDER BY name"
550 );
 
551 while( db_step(&q)==SQLITE_ROW ){
552 const char *zN = db_column_text(&q, 0);
553 const char *zV = db_column_text(&q, 1);
554 i++;
 
 
 
 
 
555 @ <tr><td>%d(i).<td>%h(zN)<td>&nbsp;&nbsp;<td>
556 @ <form action="%s(g.zTop)/setup_skin" method="post">
557 if( fossil_strcmp(zV, zCurrent)==0 ){
558 @ (Currently In Use)
559 seenCurrent = 1;
560 }else{
561 @ <input type="submit" name="load" value="Install">
@@ -566,28 +586,52 @@
566 @ </form></tr>
567 }
568 db_finalize(&q);
569 if( !seenCurrent ){
570 i++;
571 @ <tr><td>%d(i).<td><i>Current Configuration</i><td>&nbsp;&nbsp;<td>
572 @ <form action="%s(g.zTop)/setup_skin" method="post">
573 @ <input type="submit" name="save" value="Save">
 
 
574 @ </form>
575 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
576 @ </table>
577 style_footer();
578 db_end_transaction(0);
579 }
580
581
582 /*
583 ** WEBPAGE: setup_skinedit
584 **
585 ** Edit aspects of a skin determined by the w= query parameter.
586 ** Requires Admin privileges.
587 **
588 ** w=N -- 0=CSS, 1=footer, 2=header, 3=details
 
589 */
590 void setup_skinedit(void){
591 static const struct sSkinAddr {
592 const char *zFile;
593 const char *zTitle;
@@ -599,39 +643,61 @@
599 /* 3 */ { "details", "Display Details", "Details", },
600 };
601 const char *zBasis;
602 const char *zContent;
603 char *zDflt;
 
 
 
604 int ii;
605 int j;
606
607 login_check_credentials();
 
 
 
 
 
 
608 if( !g.perm.Setup ){
609 login_needed(0);
610 return;
 
 
 
 
 
 
 
 
611 }
 
 
612 ii = atoi(PD("w","0"));
613 if( ii<0 || ii>count(aSkinAttr) ) ii = 0;
 
 
 
614 zBasis = PD("basis","default");
615 zDflt = mprintf("skins/%s/%s.txt", zBasis, aSkinAttr[ii].zFile);
616 db_begin_transaction();
617 if( P("revert")!=0 ){
618 db_multi_exec("DELETE FROM config WHERE name=%Q", aSkinAttr[ii].zFile);
619 cgi_replace_parameter(aSkinAttr[ii].zFile, builtin_text(zDflt));
620 }
621 style_header("%s", aSkinAttr[ii].zTitle);
622 for(j=0; j<count(aSkinAttr); j++){
623 if( j==ii ) continue;
624 style_submenu_element(aSkinAttr[j].zSubmenu,
625 "%R/setup_skinedit?w=%d&basis=%h",j,zBasis);
626 }
627 style_submenu_element("Skins", "%R/setup_skin");
628 @ <form action="%s(g.zTop)/setup_skinedit" method="post"><div>
629 login_insert_csrf_secret();
630 @ <input type='hidden' name='w' value='%d(ii)'>
631 @ <h2>Edit %s(aSkinAttr[ii].zTitle):</h2>
632 zContent = textarea_attribute("", 10, 80, aSkinAttr[ii].zFile,
 
633 aSkinAttr[ii].zFile, builtin_text(zDflt), 0);
634 @ <br />
635 @ <input type="submit" name="submit" value="Apply Changes" />
636 @ <hr />
637 @ Baseline: <select size='1' name='basis'>
@@ -668,5 +734,292 @@
668 }
669 @ </div></form>
670 style_footer();
671 db_end_transaction(0);
672 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
673
--- src/skins.c
+++ src/skins.c
@@ -2,11 +2,11 @@
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:
@@ -53,10 +53,15 @@
53 { "Black & White, Menu on Left", "black_and_white", 0 },
54 { "Plain Gray, No Logo", "plain_gray", 0 },
55 { "Khaki, No Logo", "khaki", 0 },
56 };
57
58 /*
59 ** A skin consists of four "files" named here:
60 */
61 static const char *azSkinFile[] = { "css", "header", "footer", "details" };
62
63 /*
64 ** Alternative skins can be specified in the CGI script or by options
65 ** on the "http", "ui", and "server" commands. The alternative skin
66 ** name must be one of the aBuiltinSkin[].zLabel names. If there is
67 ** a match, that alternative is used.
@@ -64,10 +69,11 @@
69 ** The following static variable holds the name of the alternative skin,
70 ** or NULL if the skin should be as configured.
71 */
72 static struct BuiltinSkin *pAltSkin = 0;
73 static char *zAltSkinDir = 0;
74 static int iDraftSkin = 0;
75
76 /*
77 ** Skin details are a set of key/value pairs that define display
78 ** attributes of the skin that cannot be easily specified using CSS
79 ** or that need to be known on the server-side.
@@ -100,10 +106,14 @@
106 int i;
107 Blob err = BLOB_INITIALIZER;
108 if( strchr(zName, '/')!=0 ){
109 zAltSkinDir = fossil_strdup(zName);
110 return 0;
111 }
112 if( sqlite3_strglob("draft[1-9]", zName)==0 ){
113 skin_use_draft(zName[5] - '0');
114 return 0;
115 }
116 for(i=0; i<count(aBuiltinSkin); i++){
117 if( fossil_strcmp(aBuiltinSkin[i].zLabel, zName)==0 ){
118 pAltSkin = &aBuiltinSkin[i];
119 return 0;
@@ -126,18 +136,33 @@
136 if( zSkin ){
137 char *zErr = skin_use_alternative(zSkin);
138 if( zErr ) fossil_fatal("%s", zErr);
139 }
140 }
141
142 /*
143 ** Use one of the draft skins.
144 */
145 void skin_use_draft(int i){
146 iDraftSkin = i;
147 }
148
149 /*
150 ** The following routines return the various components of the skin
151 ** that should be used for the current run.
152 **
153 ** zWhat is one of: "css", "header", "footer", "details".
154 */
155 const char *skin_get(const char *zWhat){
156 const char *zOut;
157 char *z;
158 if( iDraftSkin ){
159 z = mprintf("draft%d-%s", iDraftSkin, zWhat);
160 zOut = db_get(z, 0);
161 fossil_free(z);
162 if( zOut ) return zOut;
163 }
164 if( zAltSkinDir ){
165 char *z = mprintf("%s/%s.txt", zAltSkinDir, zWhat);
166 if( file_isfile(z, ExtFILE) ){
167 Blob x;
168 blob_read_from_file(&x, z, ExtFILE);
@@ -311,30 +336,29 @@
336 ** Memory to hold the returned string is obtained from malloc.
337 */
338 static char *getSkin(const char *zName){
339 const char *z;
340 char *zLabel;
 
341 int i;
342 Blob val;
343 blob_zero(&val);
344 for(i=0; i<count(azSkinFile); i++){
345 if( zName ){
346 zLabel = mprintf("skins/%s/%s.txt", zName, azSkinFile[i]);
347 z = builtin_text(zLabel);
348 fossil_free(zLabel);
349 }else{
350 z = db_get(azSkinFile[i], 0);
351 if( z==0 ){
352 zLabel = mprintf("skins/default/%s.txt", azSkinFile[i]);
353 z = builtin_text(zLabel);
354 fossil_free(zLabel);
355 }
356 }
357 blob_appendf(&val,
358 "REPLACE INTO config(name,value,mtime) VALUES(%Q,%Q,now());\n",
359 azSkinFile[i], z
360 );
361 }
362 return blob_str(&val);
363 }
364
@@ -355,11 +379,11 @@
379 style_header("Rename A Skin");
380 if( ex ){
381 @ <p><span class="generalError">There is already another skin
382 @ named "%h(zNewName)". Choose a different name.</span></p>
383 }
384 @ <form action="%s(g.zTop)/setup_skin_admin" method="post"><div>
385 @ <table border="0"><tr>
386 @ <tr><td align="right">Current name:<td align="left"><b>%h(zOldName)</b>
387 @ <tr><td align="right">New name:<td align="left">
388 @ <input type="text" size="35" name="newname" value="%h(zNewName)">
389 @ <tr><td><td>
@@ -395,11 +419,11 @@
419 style_header("Save Current Skin");
420 if( ex ){
421 @ <p><span class="generalError">There is already another skin
422 @ named "%h(zNewName)". Choose a different name.</span></p>
423 }
424 @ <form action="%s(g.zTop)/setup_skin_admin" method="post"><div>
425 @ <table border="0"><tr>
426 @ <tr><td align="right">Name for this skin:<td align="left">
427 @ <input type="text" size="35" name="svname" value="%h(zNewName)">
428 @ <tr><td><td>
429 @ <input type="submit" name="save" value="Save">
@@ -417,23 +441,23 @@
441 );
442 return 0;
443 }
444
445 /*
446 ** WEBPAGE: setup_skin_admin
447 **
448 ** Administrative actions on skins. For administrators only.
 
449 */
450 void setup_skin_admin(void){
451 const char *z;
452 char *zName;
453 char *zErr = 0;
454 const char *zCurrent = 0; /* Current skin */
455 int i; /* Loop counter */
456 Stmt q;
457 int seenCurrent = 0;
458 int once;
459
460 login_check_credentials();
461 if( !g.perm.Setup ){
462 login_needed(0);
463 return;
@@ -445,11 +469,11 @@
469 }
470
471 /* Process requests to delete a user-defined skin */
472 if( P("del1") && (zName = skinVarName(P("sn"), 1))!=0 ){
473 style_header("Confirm Custom Skin Delete");
474 @ <form action="%s(g.zTop)/setup_skin_admin" method="post"><div>
475 @ <p>Deletion of a custom skin is a permanent action that cannot
476 @ be undone. Please confirm that this is what you want to do:</p>
477 @ <input type="hidden" name="sn" value="%h(P("sn"))" />
478 @ <input type="submit" name="del2" value="Confirm - Delete The Skin" />
479 @ <input type="submit" name="cancel" value="Cancel - Do Not Delete" />
@@ -458,10 +482,16 @@
482 style_footer();
483 return;
484 }
485 if( P("del2")!=0 && (zName = skinVarName(P("sn"), 1))!=0 ){
486 db_multi_exec("DELETE FROM config WHERE name=%Q", zName);
487 }
488 if( P("draftdel")!=0 ){
489 const char *zDraft = P("name");
490 if( sqlite3_strglob("draft[1-9]",zDraft)==0 ){
491 db_multi_exec("DELETE FROM config WHERE name GLOB '%q-*'", zDraft);
492 }
493 }
494 if( skinRename() ) return;
495 if( skinSave(zCurrent) ) return;
496
497 /* The user pressed one of the "Install" buttons. */
@@ -506,36 +536,20 @@
536
537 style_header("Skins");
538 if( zErr ){
539 @ <p style="color:red">%h(zErr)</p>
540 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
541 @ <table border="0">
542 @ <tr><td colspan=4><h2>Built-in Skins:</h2></td></th>
543 for(i=0; i<count(aBuiltinSkin); i++){
544 z = aBuiltinSkin[i].zDesc;
545 @ <tr><td>%d(i+1).<td>%h(z)<td>&nbsp;&nbsp;<td>
546 if( fossil_strcmp(aBuiltinSkin[i].zSQL, zCurrent)==0 ){
547 @ (Currently In Use)
548 seenCurrent = 1;
549 }else{
550 @ <form action="%s(g.zTop)/setup_skin_admin" method="post">
551 @ <input type="hidden" name="sn" value="%h(z)" />
552 @ <input type="submit" name="load" value="Install" />
553 if( pAltSkin==&aBuiltinSkin[i] ){
554 @ (Current override)
555 }
@@ -546,16 +560,22 @@
560 db_prepare(&q,
561 "SELECT substr(name, 6), value FROM config"
562 " WHERE name GLOB 'skin:*'"
563 " ORDER BY name"
564 );
565 once = 1;
566 while( db_step(&q)==SQLITE_ROW ){
567 const char *zN = db_column_text(&q, 0);
568 const char *zV = db_column_text(&q, 1);
569 i++;
570 if( once ){
571 once = 0;
572 @ <tr><td colspan=4><h2>Skins saved as "skin:*' entries \
573 @ in the CONFIG table:</h2></td></tr>
574 }
575 @ <tr><td>%d(i).<td>%h(zN)<td>&nbsp;&nbsp;<td>
576 @ <form action="%s(g.zTop)/setup_skin_admin" method="post">
577 if( fossil_strcmp(zV, zCurrent)==0 ){
578 @ (Currently In Use)
579 seenCurrent = 1;
580 }else{
581 @ <input type="submit" name="load" value="Install">
@@ -566,28 +586,52 @@
586 @ </form></tr>
587 }
588 db_finalize(&q);
589 if( !seenCurrent ){
590 i++;
591 @ <tr><td colspan=4><h2>Current skin in css/header/footer/details entries \
592 @ in the CONFIG table:</h2></td></tr>
593 @ <tr><td>%d(i).<td><i>Current</i><td>&nbsp;&nbsp;<td>
594 @ <form action="%s(g.zTop)/setup_skin_admin" method="post">
595 @ <input type="submit" name="save" value="Backup">
596 @ </form>
597 }
598 db_prepare(&q,
599 "SELECT DISTINCT substr(name, 1, 6) FROM config"
600 " WHERE name GLOB 'draft[1-9]-*'"
601 " ORDER BY name"
602 );
603 once = 1;
604 while( db_step(&q)==SQLITE_ROW ){
605 const char *zN = db_column_text(&q, 0);
606 i++;
607 if( once ){
608 once = 0;
609 @ <tr><td colspan=4><h2>Draft skins stored as "draft[1-9]-*' entries \
610 @ in the CONFIG table:</h2></td></tr>
611 }
612 @ <tr><td>%d(i).<td>%h(zN)<td>&nbsp;&nbsp;<td>
613 @ <form action="%s(g.zTop)/setup_skin_admin" method="post">
614 @ <input type="submit" name="draftdel" value="Delete">
615 @ <input type="hidden" name="name" value="%h(zN)">
616 @ </form></tr>
617 }
618 db_finalize(&q);
619
620 @ </table>
621 style_footer();
622 db_end_transaction(0);
623 }
624
 
625 /*
626 ** WEBPAGE: setup_skinedit
627 **
628 ** Edit aspects of a skin determined by the w= query parameter.
629 ** Requires Setup privileges.
630 **
631 ** w=NUM -- 0=CSS, 1=footer, 2=header, 3=details
632 ** sk=NUM -- the draft skin number
633 */
634 void setup_skinedit(void){
635 static const struct sSkinAddr {
636 const char *zFile;
637 const char *zTitle;
@@ -599,39 +643,61 @@
643 /* 3 */ { "details", "Display Details", "Details", },
644 };
645 const char *zBasis;
646 const char *zContent;
647 char *zDflt;
648 char *zKey;
649 char *zTitle;
650 int iSkin;
651 int ii;
652 int j;
653
654 login_check_credentials();
655
656 /* Figure out which skin we are editing */
657 iSkin = atoi(PD("sk","1"));
658 if( iSkin<1 || iSkin>9 ) iSkin = 1;
659
660 /* Check that the user is authorized to edit this skin. */
661 if( !g.perm.Setup ){
662 char *zAllowedEditors = db_get_mprintf("draft%d-users", "", iSkin);
663 Glob *pAllowedEditors;
664 if( zAllowedEditors[0] ){
665 pAllowedEditors = glob_create(zAllowedEditors);
666 if( !glob_match(pAllowedEditors, zAllowedEditors) ){
667 login_needed(0);
668 return;
669 }
670 glob_free(pAllowedEditors);
671 }
672 }
673
674 /* figure out which file is to be edited */
675 ii = atoi(PD("w","0"));
676 if( ii<0 || ii>count(aSkinAttr) ) ii = 0;
677 zKey = mprintf("draft%d-%s", iSkin, aSkinAttr[ii].zFile);
678 zTitle = mprintf("%s for Draft%d", aSkinAttr[ii].zTitle, iSkin);
679
680 zBasis = PD("basis","default");
681 zDflt = mprintf("skins/%s/%s.txt", zBasis, aSkinAttr[ii].zFile);
682 db_begin_transaction();
683 if( P("revert")!=0 ){
684 db_multi_exec("DELETE FROM config WHERE name=%Q", aSkinAttr[ii].zFile);
685 cgi_replace_parameter(aSkinAttr[ii].zFile, builtin_text(zDflt));
686 }
687 style_header("%s", zTitle);
688 for(j=0; j<count(aSkinAttr); j++){
689 if( j==ii ) continue;
690 style_submenu_element(aSkinAttr[j].zSubmenu,
691 "%R/setup_skinedit?w=%d&basis=%h&sk=%d",j,zBasis,iSkin);
692 }
 
693 @ <form action="%s(g.zTop)/setup_skinedit" method="post"><div>
694 login_insert_csrf_secret();
695 @ <input type='hidden' name='w' value='%d(ii)'>
696 @ <input type='hidden' name='sk' value='%d(iSkin)'>
697 @ <h2>Edit %s(zTitle):</h2>
698 zContent = textarea_attribute("", 10, 80, zKey,
699 aSkinAttr[ii].zFile, builtin_text(zDflt), 0);
700 @ <br />
701 @ <input type="submit" name="submit" value="Apply Changes" />
702 @ <hr />
703 @ Baseline: <select size='1' name='basis'>
@@ -668,5 +734,292 @@
734 }
735 @ </div></form>
736 style_footer();
737 db_end_transaction(0);
738 }
739
740 /*
741 ** Try to initialize draft skin iSkin to the built-in or preexisting
742 ** skin named by zTemplate.
743 */
744 static void skin_initialize_draft(int iSkin, const char *zTemplate){
745 int i;
746 if( zTemplate==0 ) return;
747 if( strcmp(zTemplate, "current")==0 ){
748 for(i=0; i<count(azSkinFile); i++){
749 db_set_mprintf("draft%d-%s", db_get(azSkinFile[i],""), 0,
750 iSkin, azSkinFile[i]);
751 }
752 return;
753 }
754 for(i=0; i<count(aBuiltinSkin); i++){
755 if( strcmp(zTemplate, aBuiltinSkin[i].zLabel)==0 ){
756 for(i=0; i<count(azSkinFile); i++){
757 char *zKey = mprintf("skins/%s/%s.txt", zTemplate, azSkinFile[i]);
758 db_set_mprintf("draft%d-%s", builtin_text(zKey), 0,
759 iSkin, azSkinFile[i]);
760 }
761 return;
762 }
763 }
764 }
765
766 /*
767 ** Publish the draft skin iSkin as the new default.
768 */
769 static void skin_publish(int iSkin){
770 char *zCurrent; /* SQL description of the current skin */
771 char *zBuiltin; /* SQL description of a built-in skin */
772 int i;
773 int seen = 0; /* True if no need to make a backup */
774
775 /* Check to see if the current skin is already saved. If it is, there
776 ** is no need to create a backup */
777 zCurrent = getSkin(0);
778 for(i=0; i<count(aBuiltinSkin); i++){
779 zBuiltin = getSkin(aBuiltinSkin[i].zLabel);
780 if( fossil_strcmp(aBuiltinSkin[i].zSQL, zCurrent)==0 ){
781 seen = 1;
782 break;
783 }
784 }
785 if( !seen ){
786 seen = db_exists("SELECT 1 FROM config WHERE name GLOB 'skin:*'"
787 " AND value=%Q", zCurrent);
788 }
789 if( !seen ){
790 db_multi_exec(
791 "INSERT INTO config(name,value,mtime) VALUES("
792 " strftime('skin:Backup On %%Y-%%m-%%d %%H:%%M:%%S'),"
793 " %Q,now())", zCurrent
794 );
795 }
796
797 /* Publish draft iSkin */
798 for(i=0; i<count(azSkinFile); i++){
799 char *zNew = db_get_mprintf("draft%d-%s", "", iSkin, azSkinFile[i]);
800 db_set(azSkinFile[i], zNew, 0);
801 }
802 }
803
804 /*
805 ** WEBPAGE: setup_skin
806 **
807 ** Generate a page showing the steps needed to customize a skin.
808 */
809 void setup_skin(void){
810 int i; /* Loop counter */
811 int iSkin; /* Which draft skin is being edited */
812 int isSetup; /* True for an administrator */
813 int isEditor; /* Others authorized to make edits */
814 char *zAllowedEditors; /* Who may edit the draft skin */
815 char *zBase; /* Base URL for draft under test */
816 static const char *azTestPages[] = {
817 "home",
818 "timeline",
819 "dir?ci=tip",
820 "dir?ci=tip&type=tree",
821 "brlist",
822 "info/trunk",
823 };
824
825 /* Figure out which skin we are editing */
826 iSkin = atoi(PD("sk","1"));
827 if( iSkin<1 || iSkin>9 ) iSkin = 1;
828
829 /* Figure out if the current user is allowed to make administrative
830 ** changes and/or edits
831 */
832 login_check_credentials();
833 zAllowedEditors = db_get_mprintf("draft%d-users", "", iSkin);
834 if( g.perm.Setup ){
835 isSetup = isEditor = 1;
836 }else{
837 Glob *pAllowedEditors;
838 isSetup = isEditor = 0;
839 if( zAllowedEditors[0] ){
840 pAllowedEditors = glob_create(zAllowedEditors);
841 isEditor = glob_match(pAllowedEditors, zAllowedEditors);
842 glob_free(pAllowedEditors);
843 }
844 }
845
846 /* Initialize the skin, if requested and authorized. */
847 if( P("init3")!=0 && isEditor ){
848 skin_initialize_draft(iSkin, P("initskin"));
849 }
850 if( P("submit2")!=0 && isSetup ){
851 db_set_mprintf("draft%d-users", PD("editors",""), 0, iSkin);
852 zAllowedEditors = db_get_mprintf("draft%d-users", "", iSkin);
853 }
854
855 /* Publish the draft skin */
856 if( P("pub7")!=0 && PB("pub7ck1") && PB("pub7ck2") ){
857 skin_publish(iSkin);
858 }
859
860 style_header("Customize Skin");
861
862 @ <p>Customize the look of this Fossil repository by making changes
863 @ to the CSS, Header, Footer, and Detail Settings in one of nine "draft"
864 @ configurations. Then, after verifying that all is working correctly,
865 @ publish the draft to become the new main Skin.<p>
866 @
867 @ <a name='step1'></a>
868 @ <h1>Step 1: Identify Which Draft To Use</h1>
869 @
870 @ <p>The main skin of Fossil cannot be edited directly. Instead,
871 @ edits are made to one of nine draft skins. A draft skin can then
872 @ be published to become the default skin.
873 @ Nine separate drafts are available to facilitate A/B testing.</p>
874 @
875 @ <form method='POST' action='%R/setup_skin#step2' id='f01'>
876 @ <p class='skinInput'>Draft skin to edit:
877 @ <select size='1' name='sk' onchange='gebi("f01").submit()'>
878 for(i=1; i<=9; i++){
879 if( i==iSkin ){
880 @ <option value='%d(i)' selected>draft%d(i)</option>
881 }else{
882 @ <option value='%d(i)'>draft%d(i)</option>
883 }
884 }
885 @ </select>
886 @ </p>
887 @
888 @ <a name='step2'></a>
889 @ <h1>Step 2: Authenticate</h1>
890 @
891 if( isSetup ){
892 @ <p>As an administrator, you can make any edits you like to this or
893 @ any other skin. You can also authorized other users to edit this
894 @ skin. Any user whose login name matches the comma-separate list
895 @ of GLOB expressions below is given special permission to edit
896 @ the draft%d(iSkin) skin:
897 @
898 @ <form method='POST' action='%R/setup_skin#step2' id='f02'>
899 @ <p class='skinInput'>
900 @ <input type='hidden' name='sk' value='%d(iSkin)'>
901 @ Authorized editors for skin draft%d(iSkin):
902 @ <input type='text' name='editors' value='%h(zAllowedEditors)'\
903 @ width='40'>
904 @ <input type='submit' name='submit2' value='Change'>
905 @ </p>
906 @ </form>
907 }else if( isEditor ){
908 @ <p>You are authorized to make changes to the draft%d(iSkin) skin.
909 @ Continue to the <a href='#step3'>next step</a>.</p>
910 }else{
911 @ <p>You are not authorized to make changes to the draft%d(iSkin)
912 @ skin. Contact the administrator of this Fossil repository for
913 @ further information.</p>
914 }
915 @
916 @ <a name='step3'></a>
917 @ <h1>Step 3: Initialize The Draft</h1>
918 @
919 if( !isEditor ){
920 @ <p>You are not allowed to initialize draft%(iSkin). Contact
921 @ the administrator for this repository for more information.
922 }else{
923 @ <p>Initialize the draft%d(iSkin) skin to one of the built-in skins
924 @ or a preexisting skin, to use as a baseline.</p>
925 @
926 @ <form method='POST' action='%R/setup_skin#step4' id='f03'>
927 @ <p class='skinInput'>
928 @ <input type='hidden' name='sk' value='%d(iSkin)'>
929 @ Initialize skin <b>draft%d(iSkin)</b> using
930 @ <select size='1' name='initskin'>
931 @ <option value='current'>Currently In Use</option>
932 for(i=0; i<count(aBuiltinSkin); i++){
933 @ <option value='%s(aBuiltinSkin[i].zLabel)'>\
934 @ %h(aBuiltinSkin[i].zDesc) (built-in)</option>
935 }
936 @ </select>
937 @ <input type='submit' name='init3' value='Go'>
938 @ </p>
939 @ </form>
940 }
941 @
942 @ <a name='step4'></a>
943 @ <h1>Step 4: Make Edits</h1>
944 @
945 if( !isEditor ){
946 @ <p>You are not authorized to make edits to the draft%d(iSkin) skin.
947 @ Contact the administrator of this Fossil repository for help.</p>
948 }else{
949 @ <p>Edit the components of the draft%d(iSkin) skin:
950 @ <ul>
951 @ <li><a href='%R/setup_skinedit?w=0&sk=%d(iSkin)' target='_blank'>CSS</a>
952 @ <li><a href='%R/setup_skinedit?w=2&sk=%d(iSkin)' target='_blank'>\
953 @ Header</a>
954 @ <li><a href='%R/setup_skinedit?w=1&sk=%d(iSkin)' target='_blank'>\
955 @ Footer</a>
956 @ <li><a href='%R/setup_skinedit?w=3&sk=%d(iSkin)' target='_blank'>\
957 @ Details</a>
958 @ </ul>
959 }
960 @
961 @ <a name='step5'></a>
962 @ <h1>Step 5: Verify The Draft Skin</h1>
963 @
964 @ <p>To test this draft skin, insert text "/draft%d(iSkin)/" just before the
965 @ operation name in the URL. Here are a few links to try:
966 @ <ul>
967 if( iDraftSkin && sqlite3_strglob("*/draft[1-9]", g.zBaseURL)==0 ){
968 zBase = mprintf("%.*s/draft%d", (int)strlen(g.zBaseURL)-7,g.zBaseURL,iSkin);
969 }else{
970 zBase = mprintf("%s/draft%d", g.zBaseURL, iSkin);
971 }
972 for(i=0; i<count(azTestPages); i++){
973 @ <li><a href='%s(zBase)/%s(azTestPages[i])' target='_blank'>\
974 @ %s(zBase)/%s(azTestPages[i])</a>
975 }
976 fossil_free(zBase);
977 @ </ul>
978 @
979 @ <p>You will probably need to press Reload on your browser before any
980 @ CSS changes will take effect.</p>
981 @
982 @ <a hame='step6'></a>
983 @ <h1>Step 6: Interate</h1>
984 @
985 @ <p>Repeat <a href='#step4'>step 4</a> and
986 @ <a href='#step5'>step 5</a> as many times as necessary to create
987 @ a production-ready skin.
988 @
989 @ <a name='step7'></a>
990 @ <h1>Step 7: Publish</h1>
991 @
992 if( !g.perm.Setup ){
993 @ <p>Only administrators are allowed to publish draft skins. Contact
994 @ an administrator to get this "draft%d(iSkin)" skin published.</p>
995 }else{
996 @ <p>When the draft%d(iSkin) skin is ready for production use,
997 @ make it the default scan by clicking the acknowledgements and
998 @ pressing the button below:</p>
999 @
1000 @ <form method='POST' action='%R/setup_skin#step7'>
1001 @ <p class='skinInput'>
1002 @ <input type='hidden' name='sk' value='%d(iSkin)'>
1003 @ <input type='checkbox' name='pub7ck1' value='yes'>\
1004 @ Skin draft%d(iSkin) has been testing and found ready for production.<br>
1005 @ <input type='checkbox' name='pub7ck2' value='yes'>\
1006 @ The current skin should be overwritten with draft%d(iSkin).<br>
1007 @ <input type='submit' name='pub7' value='Publish Draft%d(iSkin)'>
1008 @ </p></form>
1009 @
1010 @ <p>You will probably need to press Reload on your browser after
1011 @ publishing the new skin.</p>
1012 }
1013 @
1014 @ <a name='step8'></a>
1015 @ <h1>Step 8: Cleanup and Undo Actions</h1>
1016 @
1017 if( !g.perm.Setup ){
1018 @ <p>Administrators can optionally remove save legacy skins, or
1019 @ undo a prior publish
1020 }else{
1021 @ <p>Visit the <a href='%R/setup_skin_admin'>Skin Admin</a> page
1022 @ for cleanup and recovery actions.
1023 }
1024 style_footer();
1025 }
1026
+399 -46
--- src/skins.c
+++ src/skins.c
@@ -2,11 +2,11 @@
22
** Copyright (c) 2009 D. Richard Hipp
33
**
44
** This program is free software; you can redistribute it and/or
55
** modify it under the terms of the Simplified BSD License (also
66
** known as the "2-Clause License" or "FreeBSD License".)
7
-
7
+**
88
** This program is distributed in the hope that it will be useful,
99
** but without any warranty; without even the implied warranty of
1010
** merchantability or fitness for a particular purpose.
1111
**
1212
** Author contact information:
@@ -53,10 +53,15 @@
5353
{ "Black & White, Menu on Left", "black_and_white", 0 },
5454
{ "Plain Gray, No Logo", "plain_gray", 0 },
5555
{ "Khaki, No Logo", "khaki", 0 },
5656
};
5757
58
+/*
59
+** A skin consists of four "files" named here:
60
+*/
61
+static const char *azSkinFile[] = { "css", "header", "footer", "details" };
62
+
5863
/*
5964
** Alternative skins can be specified in the CGI script or by options
6065
** on the "http", "ui", and "server" commands. The alternative skin
6166
** name must be one of the aBuiltinSkin[].zLabel names. If there is
6267
** a match, that alternative is used.
@@ -64,10 +69,11 @@
6469
** The following static variable holds the name of the alternative skin,
6570
** or NULL if the skin should be as configured.
6671
*/
6772
static struct BuiltinSkin *pAltSkin = 0;
6873
static char *zAltSkinDir = 0;
74
+static int iDraftSkin = 0;
6975
7076
/*
7177
** Skin details are a set of key/value pairs that define display
7278
** attributes of the skin that cannot be easily specified using CSS
7379
** or that need to be known on the server-side.
@@ -100,10 +106,14 @@
100106
int i;
101107
Blob err = BLOB_INITIALIZER;
102108
if( strchr(zName, '/')!=0 ){
103109
zAltSkinDir = fossil_strdup(zName);
104110
return 0;
111
+ }
112
+ if( sqlite3_strglob("draft[1-9]", zName)==0 ){
113
+ skin_use_draft(zName[5] - '0');
114
+ return 0;
105115
}
106116
for(i=0; i<count(aBuiltinSkin); i++){
107117
if( fossil_strcmp(aBuiltinSkin[i].zLabel, zName)==0 ){
108118
pAltSkin = &aBuiltinSkin[i];
109119
return 0;
@@ -126,18 +136,33 @@
126136
if( zSkin ){
127137
char *zErr = skin_use_alternative(zSkin);
128138
if( zErr ) fossil_fatal("%s", zErr);
129139
}
130140
}
141
+
142
+/*
143
+** Use one of the draft skins.
144
+*/
145
+void skin_use_draft(int i){
146
+ iDraftSkin = i;
147
+}
131148
132149
/*
133150
** The following routines return the various components of the skin
134151
** that should be used for the current run.
152
+**
153
+** zWhat is one of: "css", "header", "footer", "details".
135154
*/
136155
const char *skin_get(const char *zWhat){
137156
const char *zOut;
138157
char *z;
158
+ if( iDraftSkin ){
159
+ z = mprintf("draft%d-%s", iDraftSkin, zWhat);
160
+ zOut = db_get(z, 0);
161
+ fossil_free(z);
162
+ if( zOut ) return zOut;
163
+ }
139164
if( zAltSkinDir ){
140165
char *z = mprintf("%s/%s.txt", zAltSkinDir, zWhat);
141166
if( file_isfile(z, ExtFILE) ){
142167
Blob x;
143168
blob_read_from_file(&x, z, ExtFILE);
@@ -311,30 +336,29 @@
311336
** Memory to hold the returned string is obtained from malloc.
312337
*/
313338
static char *getSkin(const char *zName){
314339
const char *z;
315340
char *zLabel;
316
- static const char *azType[] = { "css", "header", "footer", "details" };
317341
int i;
318342
Blob val;
319343
blob_zero(&val);
320
- for(i=0; i<count(azType); i++){
344
+ for(i=0; i<count(azSkinFile); i++){
321345
if( zName ){
322
- zLabel = mprintf("skins/%s/%s.txt", zName, azType[i]);
346
+ zLabel = mprintf("skins/%s/%s.txt", zName, azSkinFile[i]);
323347
z = builtin_text(zLabel);
324348
fossil_free(zLabel);
325349
}else{
326
- z = db_get(azType[i], 0);
350
+ z = db_get(azSkinFile[i], 0);
327351
if( z==0 ){
328
- zLabel = mprintf("skins/default/%s.txt", azType[i]);
352
+ zLabel = mprintf("skins/default/%s.txt", azSkinFile[i]);
329353
z = builtin_text(zLabel);
330354
fossil_free(zLabel);
331355
}
332356
}
333357
blob_appendf(&val,
334358
"REPLACE INTO config(name,value,mtime) VALUES(%Q,%Q,now());\n",
335
- azType[i], z
359
+ azSkinFile[i], z
336360
);
337361
}
338362
return blob_str(&val);
339363
}
340364
@@ -355,11 +379,11 @@
355379
style_header("Rename A Skin");
356380
if( ex ){
357381
@ <p><span class="generalError">There is already another skin
358382
@ named "%h(zNewName)". Choose a different name.</span></p>
359383
}
360
- @ <form action="%s(g.zTop)/setup_skin" method="post"><div>
384
+ @ <form action="%s(g.zTop)/setup_skin_admin" method="post"><div>
361385
@ <table border="0"><tr>
362386
@ <tr><td align="right">Current name:<td align="left"><b>%h(zOldName)</b>
363387
@ <tr><td align="right">New name:<td align="left">
364388
@ <input type="text" size="35" name="newname" value="%h(zNewName)">
365389
@ <tr><td><td>
@@ -395,11 +419,11 @@
395419
style_header("Save Current Skin");
396420
if( ex ){
397421
@ <p><span class="generalError">There is already another skin
398422
@ named "%h(zNewName)". Choose a different name.</span></p>
399423
}
400
- @ <form action="%s(g.zTop)/setup_skin" method="post"><div>
424
+ @ <form action="%s(g.zTop)/setup_skin_admin" method="post"><div>
401425
@ <table border="0"><tr>
402426
@ <tr><td align="right">Name for this skin:<td align="left">
403427
@ <input type="text" size="35" name="svname" value="%h(zNewName)">
404428
@ <tr><td><td>
405429
@ <input type="submit" name="save" value="Save">
@@ -417,23 +441,23 @@
417441
);
418442
return 0;
419443
}
420444
421445
/*
422
-** WEBPAGE: setup_skin
446
+** WEBPAGE: setup_skin_admin
423447
**
424
-** Show a list of available skins with buttons for selecting which
425
-** skin to use. Requires Admin privilege.
448
+** Administrative actions on skins. For administrators only.
426449
*/
427
-void setup_skin(void){
450
+void setup_skin_admin(void){
428451
const char *z;
429452
char *zName;
430453
char *zErr = 0;
431454
const char *zCurrent = 0; /* Current skin */
432455
int i; /* Loop counter */
433456
Stmt q;
434457
int seenCurrent = 0;
458
+ int once;
435459
436460
login_check_credentials();
437461
if( !g.perm.Setup ){
438462
login_needed(0);
439463
return;
@@ -445,11 +469,11 @@
445469
}
446470
447471
/* Process requests to delete a user-defined skin */
448472
if( P("del1") && (zName = skinVarName(P("sn"), 1))!=0 ){
449473
style_header("Confirm Custom Skin Delete");
450
- @ <form action="%s(g.zTop)/setup_skin" method="post"><div>
474
+ @ <form action="%s(g.zTop)/setup_skin_admin" method="post"><div>
451475
@ <p>Deletion of a custom skin is a permanent action that cannot
452476
@ be undone. Please confirm that this is what you want to do:</p>
453477
@ <input type="hidden" name="sn" value="%h(P("sn"))" />
454478
@ <input type="submit" name="del2" value="Confirm - Delete The Skin" />
455479
@ <input type="submit" name="cancel" value="Cancel - Do Not Delete" />
@@ -458,10 +482,16 @@
458482
style_footer();
459483
return;
460484
}
461485
if( P("del2")!=0 && (zName = skinVarName(P("sn"), 1))!=0 ){
462486
db_multi_exec("DELETE FROM config WHERE name=%Q", zName);
487
+ }
488
+ if( P("draftdel")!=0 ){
489
+ const char *zDraft = P("name");
490
+ if( sqlite3_strglob("draft[1-9]",zDraft)==0 ){
491
+ db_multi_exec("DELETE FROM config WHERE name GLOB '%q-*'", zDraft);
492
+ }
463493
}
464494
if( skinRename() ) return;
465495
if( skinSave(zCurrent) ) return;
466496
467497
/* The user pressed one of the "Install" buttons. */
@@ -506,36 +536,20 @@
506536
507537
style_header("Skins");
508538
if( zErr ){
509539
@ <p style="color:red">%h(zErr)</p>
510540
}
511
- @ <p>A "skin" is a combination of
512
- @ <a href="setup_skinedit?w=0">CSS</a>,
513
- @ <a href="setup_skinedit?w=2">Header</a>,
514
- @ <a href="setup_skinedit?w=1">Footer</a>, and
515
- @ <a href="setup_skinedit?w=3">Details</a>
516
- @ that determines the look and feel
517
- @ of the web interface.</p>
518
- @
519
- if( pAltSkin ){
520
- @ <p class="generalError">
521
- @ This page is generated using an skin override named
522
- @ "%h(pAltSkin->zLabel)". You can change the skin configuration
523
- @ below, but the changes will not take effect until the Fossil server
524
- @ is restarted without the override.</p>
525
- @
526
- }
527
- @ <h2>Available Skins:</h2>
528541
@ <table border="0">
542
+ @ <tr><td colspan=4><h2>Built-in Skins:</h2></td></th>
529543
for(i=0; i<count(aBuiltinSkin); i++){
530544
z = aBuiltinSkin[i].zDesc;
531545
@ <tr><td>%d(i+1).<td>%h(z)<td>&nbsp;&nbsp;<td>
532546
if( fossil_strcmp(aBuiltinSkin[i].zSQL, zCurrent)==0 ){
533547
@ (Currently In Use)
534548
seenCurrent = 1;
535549
}else{
536
- @ <form action="%s(g.zTop)/setup_skin" method="post">
550
+ @ <form action="%s(g.zTop)/setup_skin_admin" method="post">
537551
@ <input type="hidden" name="sn" value="%h(z)" />
538552
@ <input type="submit" name="load" value="Install" />
539553
if( pAltSkin==&aBuiltinSkin[i] ){
540554
@ (Current override)
541555
}
@@ -546,16 +560,22 @@
546560
db_prepare(&q,
547561
"SELECT substr(name, 6), value FROM config"
548562
" WHERE name GLOB 'skin:*'"
549563
" ORDER BY name"
550564
);
565
+ once = 1;
551566
while( db_step(&q)==SQLITE_ROW ){
552567
const char *zN = db_column_text(&q, 0);
553568
const char *zV = db_column_text(&q, 1);
554569
i++;
570
+ if( once ){
571
+ once = 0;
572
+ @ <tr><td colspan=4><h2>Skins saved as "skin:*' entries \
573
+ @ in the CONFIG table:</h2></td></tr>
574
+ }
555575
@ <tr><td>%d(i).<td>%h(zN)<td>&nbsp;&nbsp;<td>
556
- @ <form action="%s(g.zTop)/setup_skin" method="post">
576
+ @ <form action="%s(g.zTop)/setup_skin_admin" method="post">
557577
if( fossil_strcmp(zV, zCurrent)==0 ){
558578
@ (Currently In Use)
559579
seenCurrent = 1;
560580
}else{
561581
@ <input type="submit" name="load" value="Install">
@@ -566,28 +586,52 @@
566586
@ </form></tr>
567587
}
568588
db_finalize(&q);
569589
if( !seenCurrent ){
570590
i++;
571
- @ <tr><td>%d(i).<td><i>Current Configuration</i><td>&nbsp;&nbsp;<td>
572
- @ <form action="%s(g.zTop)/setup_skin" method="post">
573
- @ <input type="submit" name="save" value="Save">
591
+ @ <tr><td colspan=4><h2>Current skin in css/header/footer/details entries \
592
+ @ in the CONFIG table:</h2></td></tr>
593
+ @ <tr><td>%d(i).<td><i>Current</i><td>&nbsp;&nbsp;<td>
594
+ @ <form action="%s(g.zTop)/setup_skin_admin" method="post">
595
+ @ <input type="submit" name="save" value="Backup">
574596
@ </form>
575597
}
598
+ db_prepare(&q,
599
+ "SELECT DISTINCT substr(name, 1, 6) FROM config"
600
+ " WHERE name GLOB 'draft[1-9]-*'"
601
+ " ORDER BY name"
602
+ );
603
+ once = 1;
604
+ while( db_step(&q)==SQLITE_ROW ){
605
+ const char *zN = db_column_text(&q, 0);
606
+ i++;
607
+ if( once ){
608
+ once = 0;
609
+ @ <tr><td colspan=4><h2>Draft skins stored as "draft[1-9]-*' entries \
610
+ @ in the CONFIG table:</h2></td></tr>
611
+ }
612
+ @ <tr><td>%d(i).<td>%h(zN)<td>&nbsp;&nbsp;<td>
613
+ @ <form action="%s(g.zTop)/setup_skin_admin" method="post">
614
+ @ <input type="submit" name="draftdel" value="Delete">
615
+ @ <input type="hidden" name="name" value="%h(zN)">
616
+ @ </form></tr>
617
+ }
618
+ db_finalize(&q);
619
+
576620
@ </table>
577621
style_footer();
578622
db_end_transaction(0);
579623
}
580624
581
-
582625
/*
583626
** WEBPAGE: setup_skinedit
584627
**
585628
** Edit aspects of a skin determined by the w= query parameter.
586
-** Requires Admin privileges.
629
+** Requires Setup privileges.
587630
**
588
-** w=N -- 0=CSS, 1=footer, 2=header, 3=details
631
+** w=NUM -- 0=CSS, 1=footer, 2=header, 3=details
632
+** sk=NUM -- the draft skin number
589633
*/
590634
void setup_skinedit(void){
591635
static const struct sSkinAddr {
592636
const char *zFile;
593637
const char *zTitle;
@@ -599,39 +643,61 @@
599643
/* 3 */ { "details", "Display Details", "Details", },
600644
};
601645
const char *zBasis;
602646
const char *zContent;
603647
char *zDflt;
648
+ char *zKey;
649
+ char *zTitle;
650
+ int iSkin;
604651
int ii;
605652
int j;
606653
607654
login_check_credentials();
655
+
656
+ /* Figure out which skin we are editing */
657
+ iSkin = atoi(PD("sk","1"));
658
+ if( iSkin<1 || iSkin>9 ) iSkin = 1;
659
+
660
+ /* Check that the user is authorized to edit this skin. */
608661
if( !g.perm.Setup ){
609
- login_needed(0);
610
- return;
662
+ char *zAllowedEditors = db_get_mprintf("draft%d-users", "", iSkin);
663
+ Glob *pAllowedEditors;
664
+ if( zAllowedEditors[0] ){
665
+ pAllowedEditors = glob_create(zAllowedEditors);
666
+ if( !glob_match(pAllowedEditors, zAllowedEditors) ){
667
+ login_needed(0);
668
+ return;
669
+ }
670
+ glob_free(pAllowedEditors);
671
+ }
611672
}
673
+
674
+ /* figure out which file is to be edited */
612675
ii = atoi(PD("w","0"));
613676
if( ii<0 || ii>count(aSkinAttr) ) ii = 0;
677
+ zKey = mprintf("draft%d-%s", iSkin, aSkinAttr[ii].zFile);
678
+ zTitle = mprintf("%s for Draft%d", aSkinAttr[ii].zTitle, iSkin);
679
+
614680
zBasis = PD("basis","default");
615681
zDflt = mprintf("skins/%s/%s.txt", zBasis, aSkinAttr[ii].zFile);
616682
db_begin_transaction();
617683
if( P("revert")!=0 ){
618684
db_multi_exec("DELETE FROM config WHERE name=%Q", aSkinAttr[ii].zFile);
619685
cgi_replace_parameter(aSkinAttr[ii].zFile, builtin_text(zDflt));
620686
}
621
- style_header("%s", aSkinAttr[ii].zTitle);
687
+ style_header("%s", zTitle);
622688
for(j=0; j<count(aSkinAttr); j++){
623689
if( j==ii ) continue;
624690
style_submenu_element(aSkinAttr[j].zSubmenu,
625
- "%R/setup_skinedit?w=%d&basis=%h",j,zBasis);
691
+ "%R/setup_skinedit?w=%d&basis=%h&sk=%d",j,zBasis,iSkin);
626692
}
627
- style_submenu_element("Skins", "%R/setup_skin");
628693
@ <form action="%s(g.zTop)/setup_skinedit" method="post"><div>
629694
login_insert_csrf_secret();
630695
@ <input type='hidden' name='w' value='%d(ii)'>
631
- @ <h2>Edit %s(aSkinAttr[ii].zTitle):</h2>
632
- zContent = textarea_attribute("", 10, 80, aSkinAttr[ii].zFile,
696
+ @ <input type='hidden' name='sk' value='%d(iSkin)'>
697
+ @ <h2>Edit %s(zTitle):</h2>
698
+ zContent = textarea_attribute("", 10, 80, zKey,
633699
aSkinAttr[ii].zFile, builtin_text(zDflt), 0);
634700
@ <br />
635701
@ <input type="submit" name="submit" value="Apply Changes" />
636702
@ <hr />
637703
@ Baseline: <select size='1' name='basis'>
@@ -668,5 +734,292 @@
668734
}
669735
@ </div></form>
670736
style_footer();
671737
db_end_transaction(0);
672738
}
739
+
740
+/*
741
+** Try to initialize draft skin iSkin to the built-in or preexisting
742
+** skin named by zTemplate.
743
+*/
744
+static void skin_initialize_draft(int iSkin, const char *zTemplate){
745
+ int i;
746
+ if( zTemplate==0 ) return;
747
+ if( strcmp(zTemplate, "current")==0 ){
748
+ for(i=0; i<count(azSkinFile); i++){
749
+ db_set_mprintf("draft%d-%s", db_get(azSkinFile[i],""), 0,
750
+ iSkin, azSkinFile[i]);
751
+ }
752
+ return;
753
+ }
754
+ for(i=0; i<count(aBuiltinSkin); i++){
755
+ if( strcmp(zTemplate, aBuiltinSkin[i].zLabel)==0 ){
756
+ for(i=0; i<count(azSkinFile); i++){
757
+ char *zKey = mprintf("skins/%s/%s.txt", zTemplate, azSkinFile[i]);
758
+ db_set_mprintf("draft%d-%s", builtin_text(zKey), 0,
759
+ iSkin, azSkinFile[i]);
760
+ }
761
+ return;
762
+ }
763
+ }
764
+}
765
+
766
+/*
767
+** Publish the draft skin iSkin as the new default.
768
+*/
769
+static void skin_publish(int iSkin){
770
+ char *zCurrent; /* SQL description of the current skin */
771
+ char *zBuiltin; /* SQL description of a built-in skin */
772
+ int i;
773
+ int seen = 0; /* True if no need to make a backup */
774
+
775
+ /* Check to see if the current skin is already saved. If it is, there
776
+ ** is no need to create a backup */
777
+ zCurrent = getSkin(0);
778
+ for(i=0; i<count(aBuiltinSkin); i++){
779
+ zBuiltin = getSkin(aBuiltinSkin[i].zLabel);
780
+ if( fossil_strcmp(aBuiltinSkin[i].zSQL, zCurrent)==0 ){
781
+ seen = 1;
782
+ break;
783
+ }
784
+ }
785
+ if( !seen ){
786
+ seen = db_exists("SELECT 1 FROM config WHERE name GLOB 'skin:*'"
787
+ " AND value=%Q", zCurrent);
788
+ }
789
+ if( !seen ){
790
+ db_multi_exec(
791
+ "INSERT INTO config(name,value,mtime) VALUES("
792
+ " strftime('skin:Backup On %%Y-%%m-%%d %%H:%%M:%%S'),"
793
+ " %Q,now())", zCurrent
794
+ );
795
+ }
796
+
797
+ /* Publish draft iSkin */
798
+ for(i=0; i<count(azSkinFile); i++){
799
+ char *zNew = db_get_mprintf("draft%d-%s", "", iSkin, azSkinFile[i]);
800
+ db_set(azSkinFile[i], zNew, 0);
801
+ }
802
+}
803
+
804
+/*
805
+** WEBPAGE: setup_skin
806
+**
807
+** Generate a page showing the steps needed to customize a skin.
808
+*/
809
+void setup_skin(void){
810
+ int i; /* Loop counter */
811
+ int iSkin; /* Which draft skin is being edited */
812
+ int isSetup; /* True for an administrator */
813
+ int isEditor; /* Others authorized to make edits */
814
+ char *zAllowedEditors; /* Who may edit the draft skin */
815
+ char *zBase; /* Base URL for draft under test */
816
+ static const char *azTestPages[] = {
817
+ "home",
818
+ "timeline",
819
+ "dir?ci=tip",
820
+ "dir?ci=tip&type=tree",
821
+ "brlist",
822
+ "info/trunk",
823
+ };
824
+
825
+ /* Figure out which skin we are editing */
826
+ iSkin = atoi(PD("sk","1"));
827
+ if( iSkin<1 || iSkin>9 ) iSkin = 1;
828
+
829
+ /* Figure out if the current user is allowed to make administrative
830
+ ** changes and/or edits
831
+ */
832
+ login_check_credentials();
833
+ zAllowedEditors = db_get_mprintf("draft%d-users", "", iSkin);
834
+ if( g.perm.Setup ){
835
+ isSetup = isEditor = 1;
836
+ }else{
837
+ Glob *pAllowedEditors;
838
+ isSetup = isEditor = 0;
839
+ if( zAllowedEditors[0] ){
840
+ pAllowedEditors = glob_create(zAllowedEditors);
841
+ isEditor = glob_match(pAllowedEditors, zAllowedEditors);
842
+ glob_free(pAllowedEditors);
843
+ }
844
+ }
845
+
846
+ /* Initialize the skin, if requested and authorized. */
847
+ if( P("init3")!=0 && isEditor ){
848
+ skin_initialize_draft(iSkin, P("initskin"));
849
+ }
850
+ if( P("submit2")!=0 && isSetup ){
851
+ db_set_mprintf("draft%d-users", PD("editors",""), 0, iSkin);
852
+ zAllowedEditors = db_get_mprintf("draft%d-users", "", iSkin);
853
+ }
854
+
855
+ /* Publish the draft skin */
856
+ if( P("pub7")!=0 && PB("pub7ck1") && PB("pub7ck2") ){
857
+ skin_publish(iSkin);
858
+ }
859
+
860
+ style_header("Customize Skin");
861
+
862
+ @ <p>Customize the look of this Fossil repository by making changes
863
+ @ to the CSS, Header, Footer, and Detail Settings in one of nine "draft"
864
+ @ configurations. Then, after verifying that all is working correctly,
865
+ @ publish the draft to become the new main Skin.<p>
866
+ @
867
+ @ <a name='step1'></a>
868
+ @ <h1>Step 1: Identify Which Draft To Use</h1>
869
+ @
870
+ @ <p>The main skin of Fossil cannot be edited directly. Instead,
871
+ @ edits are made to one of nine draft skins. A draft skin can then
872
+ @ be published to become the default skin.
873
+ @ Nine separate drafts are available to facilitate A/B testing.</p>
874
+ @
875
+ @ <form method='POST' action='%R/setup_skin#step2' id='f01'>
876
+ @ <p class='skinInput'>Draft skin to edit:
877
+ @ <select size='1' name='sk' onchange='gebi("f01").submit()'>
878
+ for(i=1; i<=9; i++){
879
+ if( i==iSkin ){
880
+ @ <option value='%d(i)' selected>draft%d(i)</option>
881
+ }else{
882
+ @ <option value='%d(i)'>draft%d(i)</option>
883
+ }
884
+ }
885
+ @ </select>
886
+ @ </p>
887
+ @
888
+ @ <a name='step2'></a>
889
+ @ <h1>Step 2: Authenticate</h1>
890
+ @
891
+ if( isSetup ){
892
+ @ <p>As an administrator, you can make any edits you like to this or
893
+ @ any other skin. You can also authorized other users to edit this
894
+ @ skin. Any user whose login name matches the comma-separate list
895
+ @ of GLOB expressions below is given special permission to edit
896
+ @ the draft%d(iSkin) skin:
897
+ @
898
+ @ <form method='POST' action='%R/setup_skin#step2' id='f02'>
899
+ @ <p class='skinInput'>
900
+ @ <input type='hidden' name='sk' value='%d(iSkin)'>
901
+ @ Authorized editors for skin draft%d(iSkin):
902
+ @ <input type='text' name='editors' value='%h(zAllowedEditors)'\
903
+ @ width='40'>
904
+ @ <input type='submit' name='submit2' value='Change'>
905
+ @ </p>
906
+ @ </form>
907
+ }else if( isEditor ){
908
+ @ <p>You are authorized to make changes to the draft%d(iSkin) skin.
909
+ @ Continue to the <a href='#step3'>next step</a>.</p>
910
+ }else{
911
+ @ <p>You are not authorized to make changes to the draft%d(iSkin)
912
+ @ skin. Contact the administrator of this Fossil repository for
913
+ @ further information.</p>
914
+ }
915
+ @
916
+ @ <a name='step3'></a>
917
+ @ <h1>Step 3: Initialize The Draft</h1>
918
+ @
919
+ if( !isEditor ){
920
+ @ <p>You are not allowed to initialize draft%(iSkin). Contact
921
+ @ the administrator for this repository for more information.
922
+ }else{
923
+ @ <p>Initialize the draft%d(iSkin) skin to one of the built-in skins
924
+ @ or a preexisting skin, to use as a baseline.</p>
925
+ @
926
+ @ <form method='POST' action='%R/setup_skin#step4' id='f03'>
927
+ @ <p class='skinInput'>
928
+ @ <input type='hidden' name='sk' value='%d(iSkin)'>
929
+ @ Initialize skin <b>draft%d(iSkin)</b> using
930
+ @ <select size='1' name='initskin'>
931
+ @ <option value='current'>Currently In Use</option>
932
+ for(i=0; i<count(aBuiltinSkin); i++){
933
+ @ <option value='%s(aBuiltinSkin[i].zLabel)'>\
934
+ @ %h(aBuiltinSkin[i].zDesc) (built-in)</option>
935
+ }
936
+ @ </select>
937
+ @ <input type='submit' name='init3' value='Go'>
938
+ @ </p>
939
+ @ </form>
940
+ }
941
+ @
942
+ @ <a name='step4'></a>
943
+ @ <h1>Step 4: Make Edits</h1>
944
+ @
945
+ if( !isEditor ){
946
+ @ <p>You are not authorized to make edits to the draft%d(iSkin) skin.
947
+ @ Contact the administrator of this Fossil repository for help.</p>
948
+ }else{
949
+ @ <p>Edit the components of the draft%d(iSkin) skin:
950
+ @ <ul>
951
+ @ <li><a href='%R/setup_skinedit?w=0&sk=%d(iSkin)' target='_blank'>CSS</a>
952
+ @ <li><a href='%R/setup_skinedit?w=2&sk=%d(iSkin)' target='_blank'>\
953
+ @ Header</a>
954
+ @ <li><a href='%R/setup_skinedit?w=1&sk=%d(iSkin)' target='_blank'>\
955
+ @ Footer</a>
956
+ @ <li><a href='%R/setup_skinedit?w=3&sk=%d(iSkin)' target='_blank'>\
957
+ @ Details</a>
958
+ @ </ul>
959
+ }
960
+ @
961
+ @ <a name='step5'></a>
962
+ @ <h1>Step 5: Verify The Draft Skin</h1>
963
+ @
964
+ @ <p>To test this draft skin, insert text "/draft%d(iSkin)/" just before the
965
+ @ operation name in the URL. Here are a few links to try:
966
+ @ <ul>
967
+ if( iDraftSkin && sqlite3_strglob("*/draft[1-9]", g.zBaseURL)==0 ){
968
+ zBase = mprintf("%.*s/draft%d", (int)strlen(g.zBaseURL)-7,g.zBaseURL,iSkin);
969
+ }else{
970
+ zBase = mprintf("%s/draft%d", g.zBaseURL, iSkin);
971
+ }
972
+ for(i=0; i<count(azTestPages); i++){
973
+ @ <li><a href='%s(zBase)/%s(azTestPages[i])' target='_blank'>\
974
+ @ %s(zBase)/%s(azTestPages[i])</a>
975
+ }
976
+ fossil_free(zBase);
977
+ @ </ul>
978
+ @
979
+ @ <p>You will probably need to press Reload on your browser before any
980
+ @ CSS changes will take effect.</p>
981
+ @
982
+ @ <a hame='step6'></a>
983
+ @ <h1>Step 6: Interate</h1>
984
+ @
985
+ @ <p>Repeat <a href='#step4'>step 4</a> and
986
+ @ <a href='#step5'>step 5</a> as many times as necessary to create
987
+ @ a production-ready skin.
988
+ @
989
+ @ <a name='step7'></a>
990
+ @ <h1>Step 7: Publish</h1>
991
+ @
992
+ if( !g.perm.Setup ){
993
+ @ <p>Only administrators are allowed to publish draft skins. Contact
994
+ @ an administrator to get this "draft%d(iSkin)" skin published.</p>
995
+ }else{
996
+ @ <p>When the draft%d(iSkin) skin is ready for production use,
997
+ @ make it the default scan by clicking the acknowledgements and
998
+ @ pressing the button below:</p>
999
+ @
1000
+ @ <form method='POST' action='%R/setup_skin#step7'>
1001
+ @ <p class='skinInput'>
1002
+ @ <input type='hidden' name='sk' value='%d(iSkin)'>
1003
+ @ <input type='checkbox' name='pub7ck1' value='yes'>\
1004
+ @ Skin draft%d(iSkin) has been testing and found ready for production.<br>
1005
+ @ <input type='checkbox' name='pub7ck2' value='yes'>\
1006
+ @ The current skin should be overwritten with draft%d(iSkin).<br>
1007
+ @ <input type='submit' name='pub7' value='Publish Draft%d(iSkin)'>
1008
+ @ </p></form>
1009
+ @
1010
+ @ <p>You will probably need to press Reload on your browser after
1011
+ @ publishing the new skin.</p>
1012
+ }
1013
+ @
1014
+ @ <a name='step8'></a>
1015
+ @ <h1>Step 8: Cleanup and Undo Actions</h1>
1016
+ @
1017
+ if( !g.perm.Setup ){
1018
+ @ <p>Administrators can optionally remove save legacy skins, or
1019
+ @ undo a prior publish
1020
+ }else{
1021
+ @ <p>Visit the <a href='%R/setup_skin_admin'>Skin Admin</a> page
1022
+ @ for cleanup and recovery actions.
1023
+ }
1024
+ style_footer();
1025
+}
6731026
--- src/skins.c
+++ src/skins.c
@@ -2,11 +2,11 @@
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:
@@ -53,10 +53,15 @@
53 { "Black & White, Menu on Left", "black_and_white", 0 },
54 { "Plain Gray, No Logo", "plain_gray", 0 },
55 { "Khaki, No Logo", "khaki", 0 },
56 };
57
 
 
 
 
 
58 /*
59 ** Alternative skins can be specified in the CGI script or by options
60 ** on the "http", "ui", and "server" commands. The alternative skin
61 ** name must be one of the aBuiltinSkin[].zLabel names. If there is
62 ** a match, that alternative is used.
@@ -64,10 +69,11 @@
64 ** The following static variable holds the name of the alternative skin,
65 ** or NULL if the skin should be as configured.
66 */
67 static struct BuiltinSkin *pAltSkin = 0;
68 static char *zAltSkinDir = 0;
 
69
70 /*
71 ** Skin details are a set of key/value pairs that define display
72 ** attributes of the skin that cannot be easily specified using CSS
73 ** or that need to be known on the server-side.
@@ -100,10 +106,14 @@
100 int i;
101 Blob err = BLOB_INITIALIZER;
102 if( strchr(zName, '/')!=0 ){
103 zAltSkinDir = fossil_strdup(zName);
104 return 0;
 
 
 
 
105 }
106 for(i=0; i<count(aBuiltinSkin); i++){
107 if( fossil_strcmp(aBuiltinSkin[i].zLabel, zName)==0 ){
108 pAltSkin = &aBuiltinSkin[i];
109 return 0;
@@ -126,18 +136,33 @@
126 if( zSkin ){
127 char *zErr = skin_use_alternative(zSkin);
128 if( zErr ) fossil_fatal("%s", zErr);
129 }
130 }
 
 
 
 
 
 
 
131
132 /*
133 ** The following routines return the various components of the skin
134 ** that should be used for the current run.
 
 
135 */
136 const char *skin_get(const char *zWhat){
137 const char *zOut;
138 char *z;
 
 
 
 
 
 
139 if( zAltSkinDir ){
140 char *z = mprintf("%s/%s.txt", zAltSkinDir, zWhat);
141 if( file_isfile(z, ExtFILE) ){
142 Blob x;
143 blob_read_from_file(&x, z, ExtFILE);
@@ -311,30 +336,29 @@
311 ** Memory to hold the returned string is obtained from malloc.
312 */
313 static char *getSkin(const char *zName){
314 const char *z;
315 char *zLabel;
316 static const char *azType[] = { "css", "header", "footer", "details" };
317 int i;
318 Blob val;
319 blob_zero(&val);
320 for(i=0; i<count(azType); i++){
321 if( zName ){
322 zLabel = mprintf("skins/%s/%s.txt", zName, azType[i]);
323 z = builtin_text(zLabel);
324 fossil_free(zLabel);
325 }else{
326 z = db_get(azType[i], 0);
327 if( z==0 ){
328 zLabel = mprintf("skins/default/%s.txt", azType[i]);
329 z = builtin_text(zLabel);
330 fossil_free(zLabel);
331 }
332 }
333 blob_appendf(&val,
334 "REPLACE INTO config(name,value,mtime) VALUES(%Q,%Q,now());\n",
335 azType[i], z
336 );
337 }
338 return blob_str(&val);
339 }
340
@@ -355,11 +379,11 @@
355 style_header("Rename A Skin");
356 if( ex ){
357 @ <p><span class="generalError">There is already another skin
358 @ named "%h(zNewName)". Choose a different name.</span></p>
359 }
360 @ <form action="%s(g.zTop)/setup_skin" method="post"><div>
361 @ <table border="0"><tr>
362 @ <tr><td align="right">Current name:<td align="left"><b>%h(zOldName)</b>
363 @ <tr><td align="right">New name:<td align="left">
364 @ <input type="text" size="35" name="newname" value="%h(zNewName)">
365 @ <tr><td><td>
@@ -395,11 +419,11 @@
395 style_header("Save Current Skin");
396 if( ex ){
397 @ <p><span class="generalError">There is already another skin
398 @ named "%h(zNewName)". Choose a different name.</span></p>
399 }
400 @ <form action="%s(g.zTop)/setup_skin" method="post"><div>
401 @ <table border="0"><tr>
402 @ <tr><td align="right">Name for this skin:<td align="left">
403 @ <input type="text" size="35" name="svname" value="%h(zNewName)">
404 @ <tr><td><td>
405 @ <input type="submit" name="save" value="Save">
@@ -417,23 +441,23 @@
417 );
418 return 0;
419 }
420
421 /*
422 ** WEBPAGE: setup_skin
423 **
424 ** Show a list of available skins with buttons for selecting which
425 ** skin to use. Requires Admin privilege.
426 */
427 void setup_skin(void){
428 const char *z;
429 char *zName;
430 char *zErr = 0;
431 const char *zCurrent = 0; /* Current skin */
432 int i; /* Loop counter */
433 Stmt q;
434 int seenCurrent = 0;
 
435
436 login_check_credentials();
437 if( !g.perm.Setup ){
438 login_needed(0);
439 return;
@@ -445,11 +469,11 @@
445 }
446
447 /* Process requests to delete a user-defined skin */
448 if( P("del1") && (zName = skinVarName(P("sn"), 1))!=0 ){
449 style_header("Confirm Custom Skin Delete");
450 @ <form action="%s(g.zTop)/setup_skin" method="post"><div>
451 @ <p>Deletion of a custom skin is a permanent action that cannot
452 @ be undone. Please confirm that this is what you want to do:</p>
453 @ <input type="hidden" name="sn" value="%h(P("sn"))" />
454 @ <input type="submit" name="del2" value="Confirm - Delete The Skin" />
455 @ <input type="submit" name="cancel" value="Cancel - Do Not Delete" />
@@ -458,10 +482,16 @@
458 style_footer();
459 return;
460 }
461 if( P("del2")!=0 && (zName = skinVarName(P("sn"), 1))!=0 ){
462 db_multi_exec("DELETE FROM config WHERE name=%Q", zName);
 
 
 
 
 
 
463 }
464 if( skinRename() ) return;
465 if( skinSave(zCurrent) ) return;
466
467 /* The user pressed one of the "Install" buttons. */
@@ -506,36 +536,20 @@
506
507 style_header("Skins");
508 if( zErr ){
509 @ <p style="color:red">%h(zErr)</p>
510 }
511 @ <p>A "skin" is a combination of
512 @ <a href="setup_skinedit?w=0">CSS</a>,
513 @ <a href="setup_skinedit?w=2">Header</a>,
514 @ <a href="setup_skinedit?w=1">Footer</a>, and
515 @ <a href="setup_skinedit?w=3">Details</a>
516 @ that determines the look and feel
517 @ of the web interface.</p>
518 @
519 if( pAltSkin ){
520 @ <p class="generalError">
521 @ This page is generated using an skin override named
522 @ "%h(pAltSkin->zLabel)". You can change the skin configuration
523 @ below, but the changes will not take effect until the Fossil server
524 @ is restarted without the override.</p>
525 @
526 }
527 @ <h2>Available Skins:</h2>
528 @ <table border="0">
 
529 for(i=0; i<count(aBuiltinSkin); i++){
530 z = aBuiltinSkin[i].zDesc;
531 @ <tr><td>%d(i+1).<td>%h(z)<td>&nbsp;&nbsp;<td>
532 if( fossil_strcmp(aBuiltinSkin[i].zSQL, zCurrent)==0 ){
533 @ (Currently In Use)
534 seenCurrent = 1;
535 }else{
536 @ <form action="%s(g.zTop)/setup_skin" method="post">
537 @ <input type="hidden" name="sn" value="%h(z)" />
538 @ <input type="submit" name="load" value="Install" />
539 if( pAltSkin==&aBuiltinSkin[i] ){
540 @ (Current override)
541 }
@@ -546,16 +560,22 @@
546 db_prepare(&q,
547 "SELECT substr(name, 6), value FROM config"
548 " WHERE name GLOB 'skin:*'"
549 " ORDER BY name"
550 );
 
551 while( db_step(&q)==SQLITE_ROW ){
552 const char *zN = db_column_text(&q, 0);
553 const char *zV = db_column_text(&q, 1);
554 i++;
 
 
 
 
 
555 @ <tr><td>%d(i).<td>%h(zN)<td>&nbsp;&nbsp;<td>
556 @ <form action="%s(g.zTop)/setup_skin" method="post">
557 if( fossil_strcmp(zV, zCurrent)==0 ){
558 @ (Currently In Use)
559 seenCurrent = 1;
560 }else{
561 @ <input type="submit" name="load" value="Install">
@@ -566,28 +586,52 @@
566 @ </form></tr>
567 }
568 db_finalize(&q);
569 if( !seenCurrent ){
570 i++;
571 @ <tr><td>%d(i).<td><i>Current Configuration</i><td>&nbsp;&nbsp;<td>
572 @ <form action="%s(g.zTop)/setup_skin" method="post">
573 @ <input type="submit" name="save" value="Save">
 
 
574 @ </form>
575 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
576 @ </table>
577 style_footer();
578 db_end_transaction(0);
579 }
580
581
582 /*
583 ** WEBPAGE: setup_skinedit
584 **
585 ** Edit aspects of a skin determined by the w= query parameter.
586 ** Requires Admin privileges.
587 **
588 ** w=N -- 0=CSS, 1=footer, 2=header, 3=details
 
589 */
590 void setup_skinedit(void){
591 static const struct sSkinAddr {
592 const char *zFile;
593 const char *zTitle;
@@ -599,39 +643,61 @@
599 /* 3 */ { "details", "Display Details", "Details", },
600 };
601 const char *zBasis;
602 const char *zContent;
603 char *zDflt;
 
 
 
604 int ii;
605 int j;
606
607 login_check_credentials();
 
 
 
 
 
 
608 if( !g.perm.Setup ){
609 login_needed(0);
610 return;
 
 
 
 
 
 
 
 
611 }
 
 
612 ii = atoi(PD("w","0"));
613 if( ii<0 || ii>count(aSkinAttr) ) ii = 0;
 
 
 
614 zBasis = PD("basis","default");
615 zDflt = mprintf("skins/%s/%s.txt", zBasis, aSkinAttr[ii].zFile);
616 db_begin_transaction();
617 if( P("revert")!=0 ){
618 db_multi_exec("DELETE FROM config WHERE name=%Q", aSkinAttr[ii].zFile);
619 cgi_replace_parameter(aSkinAttr[ii].zFile, builtin_text(zDflt));
620 }
621 style_header("%s", aSkinAttr[ii].zTitle);
622 for(j=0; j<count(aSkinAttr); j++){
623 if( j==ii ) continue;
624 style_submenu_element(aSkinAttr[j].zSubmenu,
625 "%R/setup_skinedit?w=%d&basis=%h",j,zBasis);
626 }
627 style_submenu_element("Skins", "%R/setup_skin");
628 @ <form action="%s(g.zTop)/setup_skinedit" method="post"><div>
629 login_insert_csrf_secret();
630 @ <input type='hidden' name='w' value='%d(ii)'>
631 @ <h2>Edit %s(aSkinAttr[ii].zTitle):</h2>
632 zContent = textarea_attribute("", 10, 80, aSkinAttr[ii].zFile,
 
633 aSkinAttr[ii].zFile, builtin_text(zDflt), 0);
634 @ <br />
635 @ <input type="submit" name="submit" value="Apply Changes" />
636 @ <hr />
637 @ Baseline: <select size='1' name='basis'>
@@ -668,5 +734,292 @@
668 }
669 @ </div></form>
670 style_footer();
671 db_end_transaction(0);
672 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
673
--- src/skins.c
+++ src/skins.c
@@ -2,11 +2,11 @@
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:
@@ -53,10 +53,15 @@
53 { "Black & White, Menu on Left", "black_and_white", 0 },
54 { "Plain Gray, No Logo", "plain_gray", 0 },
55 { "Khaki, No Logo", "khaki", 0 },
56 };
57
58 /*
59 ** A skin consists of four "files" named here:
60 */
61 static const char *azSkinFile[] = { "css", "header", "footer", "details" };
62
63 /*
64 ** Alternative skins can be specified in the CGI script or by options
65 ** on the "http", "ui", and "server" commands. The alternative skin
66 ** name must be one of the aBuiltinSkin[].zLabel names. If there is
67 ** a match, that alternative is used.
@@ -64,10 +69,11 @@
69 ** The following static variable holds the name of the alternative skin,
70 ** or NULL if the skin should be as configured.
71 */
72 static struct BuiltinSkin *pAltSkin = 0;
73 static char *zAltSkinDir = 0;
74 static int iDraftSkin = 0;
75
76 /*
77 ** Skin details are a set of key/value pairs that define display
78 ** attributes of the skin that cannot be easily specified using CSS
79 ** or that need to be known on the server-side.
@@ -100,10 +106,14 @@
106 int i;
107 Blob err = BLOB_INITIALIZER;
108 if( strchr(zName, '/')!=0 ){
109 zAltSkinDir = fossil_strdup(zName);
110 return 0;
111 }
112 if( sqlite3_strglob("draft[1-9]", zName)==0 ){
113 skin_use_draft(zName[5] - '0');
114 return 0;
115 }
116 for(i=0; i<count(aBuiltinSkin); i++){
117 if( fossil_strcmp(aBuiltinSkin[i].zLabel, zName)==0 ){
118 pAltSkin = &aBuiltinSkin[i];
119 return 0;
@@ -126,18 +136,33 @@
136 if( zSkin ){
137 char *zErr = skin_use_alternative(zSkin);
138 if( zErr ) fossil_fatal("%s", zErr);
139 }
140 }
141
142 /*
143 ** Use one of the draft skins.
144 */
145 void skin_use_draft(int i){
146 iDraftSkin = i;
147 }
148
149 /*
150 ** The following routines return the various components of the skin
151 ** that should be used for the current run.
152 **
153 ** zWhat is one of: "css", "header", "footer", "details".
154 */
155 const char *skin_get(const char *zWhat){
156 const char *zOut;
157 char *z;
158 if( iDraftSkin ){
159 z = mprintf("draft%d-%s", iDraftSkin, zWhat);
160 zOut = db_get(z, 0);
161 fossil_free(z);
162 if( zOut ) return zOut;
163 }
164 if( zAltSkinDir ){
165 char *z = mprintf("%s/%s.txt", zAltSkinDir, zWhat);
166 if( file_isfile(z, ExtFILE) ){
167 Blob x;
168 blob_read_from_file(&x, z, ExtFILE);
@@ -311,30 +336,29 @@
336 ** Memory to hold the returned string is obtained from malloc.
337 */
338 static char *getSkin(const char *zName){
339 const char *z;
340 char *zLabel;
 
341 int i;
342 Blob val;
343 blob_zero(&val);
344 for(i=0; i<count(azSkinFile); i++){
345 if( zName ){
346 zLabel = mprintf("skins/%s/%s.txt", zName, azSkinFile[i]);
347 z = builtin_text(zLabel);
348 fossil_free(zLabel);
349 }else{
350 z = db_get(azSkinFile[i], 0);
351 if( z==0 ){
352 zLabel = mprintf("skins/default/%s.txt", azSkinFile[i]);
353 z = builtin_text(zLabel);
354 fossil_free(zLabel);
355 }
356 }
357 blob_appendf(&val,
358 "REPLACE INTO config(name,value,mtime) VALUES(%Q,%Q,now());\n",
359 azSkinFile[i], z
360 );
361 }
362 return blob_str(&val);
363 }
364
@@ -355,11 +379,11 @@
379 style_header("Rename A Skin");
380 if( ex ){
381 @ <p><span class="generalError">There is already another skin
382 @ named "%h(zNewName)". Choose a different name.</span></p>
383 }
384 @ <form action="%s(g.zTop)/setup_skin_admin" method="post"><div>
385 @ <table border="0"><tr>
386 @ <tr><td align="right">Current name:<td align="left"><b>%h(zOldName)</b>
387 @ <tr><td align="right">New name:<td align="left">
388 @ <input type="text" size="35" name="newname" value="%h(zNewName)">
389 @ <tr><td><td>
@@ -395,11 +419,11 @@
419 style_header("Save Current Skin");
420 if( ex ){
421 @ <p><span class="generalError">There is already another skin
422 @ named "%h(zNewName)". Choose a different name.</span></p>
423 }
424 @ <form action="%s(g.zTop)/setup_skin_admin" method="post"><div>
425 @ <table border="0"><tr>
426 @ <tr><td align="right">Name for this skin:<td align="left">
427 @ <input type="text" size="35" name="svname" value="%h(zNewName)">
428 @ <tr><td><td>
429 @ <input type="submit" name="save" value="Save">
@@ -417,23 +441,23 @@
441 );
442 return 0;
443 }
444
445 /*
446 ** WEBPAGE: setup_skin_admin
447 **
448 ** Administrative actions on skins. For administrators only.
 
449 */
450 void setup_skin_admin(void){
451 const char *z;
452 char *zName;
453 char *zErr = 0;
454 const char *zCurrent = 0; /* Current skin */
455 int i; /* Loop counter */
456 Stmt q;
457 int seenCurrent = 0;
458 int once;
459
460 login_check_credentials();
461 if( !g.perm.Setup ){
462 login_needed(0);
463 return;
@@ -445,11 +469,11 @@
469 }
470
471 /* Process requests to delete a user-defined skin */
472 if( P("del1") && (zName = skinVarName(P("sn"), 1))!=0 ){
473 style_header("Confirm Custom Skin Delete");
474 @ <form action="%s(g.zTop)/setup_skin_admin" method="post"><div>
475 @ <p>Deletion of a custom skin is a permanent action that cannot
476 @ be undone. Please confirm that this is what you want to do:</p>
477 @ <input type="hidden" name="sn" value="%h(P("sn"))" />
478 @ <input type="submit" name="del2" value="Confirm - Delete The Skin" />
479 @ <input type="submit" name="cancel" value="Cancel - Do Not Delete" />
@@ -458,10 +482,16 @@
482 style_footer();
483 return;
484 }
485 if( P("del2")!=0 && (zName = skinVarName(P("sn"), 1))!=0 ){
486 db_multi_exec("DELETE FROM config WHERE name=%Q", zName);
487 }
488 if( P("draftdel")!=0 ){
489 const char *zDraft = P("name");
490 if( sqlite3_strglob("draft[1-9]",zDraft)==0 ){
491 db_multi_exec("DELETE FROM config WHERE name GLOB '%q-*'", zDraft);
492 }
493 }
494 if( skinRename() ) return;
495 if( skinSave(zCurrent) ) return;
496
497 /* The user pressed one of the "Install" buttons. */
@@ -506,36 +536,20 @@
536
537 style_header("Skins");
538 if( zErr ){
539 @ <p style="color:red">%h(zErr)</p>
540 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
541 @ <table border="0">
542 @ <tr><td colspan=4><h2>Built-in Skins:</h2></td></th>
543 for(i=0; i<count(aBuiltinSkin); i++){
544 z = aBuiltinSkin[i].zDesc;
545 @ <tr><td>%d(i+1).<td>%h(z)<td>&nbsp;&nbsp;<td>
546 if( fossil_strcmp(aBuiltinSkin[i].zSQL, zCurrent)==0 ){
547 @ (Currently In Use)
548 seenCurrent = 1;
549 }else{
550 @ <form action="%s(g.zTop)/setup_skin_admin" method="post">
551 @ <input type="hidden" name="sn" value="%h(z)" />
552 @ <input type="submit" name="load" value="Install" />
553 if( pAltSkin==&aBuiltinSkin[i] ){
554 @ (Current override)
555 }
@@ -546,16 +560,22 @@
560 db_prepare(&q,
561 "SELECT substr(name, 6), value FROM config"
562 " WHERE name GLOB 'skin:*'"
563 " ORDER BY name"
564 );
565 once = 1;
566 while( db_step(&q)==SQLITE_ROW ){
567 const char *zN = db_column_text(&q, 0);
568 const char *zV = db_column_text(&q, 1);
569 i++;
570 if( once ){
571 once = 0;
572 @ <tr><td colspan=4><h2>Skins saved as "skin:*' entries \
573 @ in the CONFIG table:</h2></td></tr>
574 }
575 @ <tr><td>%d(i).<td>%h(zN)<td>&nbsp;&nbsp;<td>
576 @ <form action="%s(g.zTop)/setup_skin_admin" method="post">
577 if( fossil_strcmp(zV, zCurrent)==0 ){
578 @ (Currently In Use)
579 seenCurrent = 1;
580 }else{
581 @ <input type="submit" name="load" value="Install">
@@ -566,28 +586,52 @@
586 @ </form></tr>
587 }
588 db_finalize(&q);
589 if( !seenCurrent ){
590 i++;
591 @ <tr><td colspan=4><h2>Current skin in css/header/footer/details entries \
592 @ in the CONFIG table:</h2></td></tr>
593 @ <tr><td>%d(i).<td><i>Current</i><td>&nbsp;&nbsp;<td>
594 @ <form action="%s(g.zTop)/setup_skin_admin" method="post">
595 @ <input type="submit" name="save" value="Backup">
596 @ </form>
597 }
598 db_prepare(&q,
599 "SELECT DISTINCT substr(name, 1, 6) FROM config"
600 " WHERE name GLOB 'draft[1-9]-*'"
601 " ORDER BY name"
602 );
603 once = 1;
604 while( db_step(&q)==SQLITE_ROW ){
605 const char *zN = db_column_text(&q, 0);
606 i++;
607 if( once ){
608 once = 0;
609 @ <tr><td colspan=4><h2>Draft skins stored as "draft[1-9]-*' entries \
610 @ in the CONFIG table:</h2></td></tr>
611 }
612 @ <tr><td>%d(i).<td>%h(zN)<td>&nbsp;&nbsp;<td>
613 @ <form action="%s(g.zTop)/setup_skin_admin" method="post">
614 @ <input type="submit" name="draftdel" value="Delete">
615 @ <input type="hidden" name="name" value="%h(zN)">
616 @ </form></tr>
617 }
618 db_finalize(&q);
619
620 @ </table>
621 style_footer();
622 db_end_transaction(0);
623 }
624
 
625 /*
626 ** WEBPAGE: setup_skinedit
627 **
628 ** Edit aspects of a skin determined by the w= query parameter.
629 ** Requires Setup privileges.
630 **
631 ** w=NUM -- 0=CSS, 1=footer, 2=header, 3=details
632 ** sk=NUM -- the draft skin number
633 */
634 void setup_skinedit(void){
635 static const struct sSkinAddr {
636 const char *zFile;
637 const char *zTitle;
@@ -599,39 +643,61 @@
643 /* 3 */ { "details", "Display Details", "Details", },
644 };
645 const char *zBasis;
646 const char *zContent;
647 char *zDflt;
648 char *zKey;
649 char *zTitle;
650 int iSkin;
651 int ii;
652 int j;
653
654 login_check_credentials();
655
656 /* Figure out which skin we are editing */
657 iSkin = atoi(PD("sk","1"));
658 if( iSkin<1 || iSkin>9 ) iSkin = 1;
659
660 /* Check that the user is authorized to edit this skin. */
661 if( !g.perm.Setup ){
662 char *zAllowedEditors = db_get_mprintf("draft%d-users", "", iSkin);
663 Glob *pAllowedEditors;
664 if( zAllowedEditors[0] ){
665 pAllowedEditors = glob_create(zAllowedEditors);
666 if( !glob_match(pAllowedEditors, zAllowedEditors) ){
667 login_needed(0);
668 return;
669 }
670 glob_free(pAllowedEditors);
671 }
672 }
673
674 /* figure out which file is to be edited */
675 ii = atoi(PD("w","0"));
676 if( ii<0 || ii>count(aSkinAttr) ) ii = 0;
677 zKey = mprintf("draft%d-%s", iSkin, aSkinAttr[ii].zFile);
678 zTitle = mprintf("%s for Draft%d", aSkinAttr[ii].zTitle, iSkin);
679
680 zBasis = PD("basis","default");
681 zDflt = mprintf("skins/%s/%s.txt", zBasis, aSkinAttr[ii].zFile);
682 db_begin_transaction();
683 if( P("revert")!=0 ){
684 db_multi_exec("DELETE FROM config WHERE name=%Q", aSkinAttr[ii].zFile);
685 cgi_replace_parameter(aSkinAttr[ii].zFile, builtin_text(zDflt));
686 }
687 style_header("%s", zTitle);
688 for(j=0; j<count(aSkinAttr); j++){
689 if( j==ii ) continue;
690 style_submenu_element(aSkinAttr[j].zSubmenu,
691 "%R/setup_skinedit?w=%d&basis=%h&sk=%d",j,zBasis,iSkin);
692 }
 
693 @ <form action="%s(g.zTop)/setup_skinedit" method="post"><div>
694 login_insert_csrf_secret();
695 @ <input type='hidden' name='w' value='%d(ii)'>
696 @ <input type='hidden' name='sk' value='%d(iSkin)'>
697 @ <h2>Edit %s(zTitle):</h2>
698 zContent = textarea_attribute("", 10, 80, zKey,
699 aSkinAttr[ii].zFile, builtin_text(zDflt), 0);
700 @ <br />
701 @ <input type="submit" name="submit" value="Apply Changes" />
702 @ <hr />
703 @ Baseline: <select size='1' name='basis'>
@@ -668,5 +734,292 @@
734 }
735 @ </div></form>
736 style_footer();
737 db_end_transaction(0);
738 }
739
740 /*
741 ** Try to initialize draft skin iSkin to the built-in or preexisting
742 ** skin named by zTemplate.
743 */
744 static void skin_initialize_draft(int iSkin, const char *zTemplate){
745 int i;
746 if( zTemplate==0 ) return;
747 if( strcmp(zTemplate, "current")==0 ){
748 for(i=0; i<count(azSkinFile); i++){
749 db_set_mprintf("draft%d-%s", db_get(azSkinFile[i],""), 0,
750 iSkin, azSkinFile[i]);
751 }
752 return;
753 }
754 for(i=0; i<count(aBuiltinSkin); i++){
755 if( strcmp(zTemplate, aBuiltinSkin[i].zLabel)==0 ){
756 for(i=0; i<count(azSkinFile); i++){
757 char *zKey = mprintf("skins/%s/%s.txt", zTemplate, azSkinFile[i]);
758 db_set_mprintf("draft%d-%s", builtin_text(zKey), 0,
759 iSkin, azSkinFile[i]);
760 }
761 return;
762 }
763 }
764 }
765
766 /*
767 ** Publish the draft skin iSkin as the new default.
768 */
769 static void skin_publish(int iSkin){
770 char *zCurrent; /* SQL description of the current skin */
771 char *zBuiltin; /* SQL description of a built-in skin */
772 int i;
773 int seen = 0; /* True if no need to make a backup */
774
775 /* Check to see if the current skin is already saved. If it is, there
776 ** is no need to create a backup */
777 zCurrent = getSkin(0);
778 for(i=0; i<count(aBuiltinSkin); i++){
779 zBuiltin = getSkin(aBuiltinSkin[i].zLabel);
780 if( fossil_strcmp(aBuiltinSkin[i].zSQL, zCurrent)==0 ){
781 seen = 1;
782 break;
783 }
784 }
785 if( !seen ){
786 seen = db_exists("SELECT 1 FROM config WHERE name GLOB 'skin:*'"
787 " AND value=%Q", zCurrent);
788 }
789 if( !seen ){
790 db_multi_exec(
791 "INSERT INTO config(name,value,mtime) VALUES("
792 " strftime('skin:Backup On %%Y-%%m-%%d %%H:%%M:%%S'),"
793 " %Q,now())", zCurrent
794 );
795 }
796
797 /* Publish draft iSkin */
798 for(i=0; i<count(azSkinFile); i++){
799 char *zNew = db_get_mprintf("draft%d-%s", "", iSkin, azSkinFile[i]);
800 db_set(azSkinFile[i], zNew, 0);
801 }
802 }
803
804 /*
805 ** WEBPAGE: setup_skin
806 **
807 ** Generate a page showing the steps needed to customize a skin.
808 */
809 void setup_skin(void){
810 int i; /* Loop counter */
811 int iSkin; /* Which draft skin is being edited */
812 int isSetup; /* True for an administrator */
813 int isEditor; /* Others authorized to make edits */
814 char *zAllowedEditors; /* Who may edit the draft skin */
815 char *zBase; /* Base URL for draft under test */
816 static const char *azTestPages[] = {
817 "home",
818 "timeline",
819 "dir?ci=tip",
820 "dir?ci=tip&type=tree",
821 "brlist",
822 "info/trunk",
823 };
824
825 /* Figure out which skin we are editing */
826 iSkin = atoi(PD("sk","1"));
827 if( iSkin<1 || iSkin>9 ) iSkin = 1;
828
829 /* Figure out if the current user is allowed to make administrative
830 ** changes and/or edits
831 */
832 login_check_credentials();
833 zAllowedEditors = db_get_mprintf("draft%d-users", "", iSkin);
834 if( g.perm.Setup ){
835 isSetup = isEditor = 1;
836 }else{
837 Glob *pAllowedEditors;
838 isSetup = isEditor = 0;
839 if( zAllowedEditors[0] ){
840 pAllowedEditors = glob_create(zAllowedEditors);
841 isEditor = glob_match(pAllowedEditors, zAllowedEditors);
842 glob_free(pAllowedEditors);
843 }
844 }
845
846 /* Initialize the skin, if requested and authorized. */
847 if( P("init3")!=0 && isEditor ){
848 skin_initialize_draft(iSkin, P("initskin"));
849 }
850 if( P("submit2")!=0 && isSetup ){
851 db_set_mprintf("draft%d-users", PD("editors",""), 0, iSkin);
852 zAllowedEditors = db_get_mprintf("draft%d-users", "", iSkin);
853 }
854
855 /* Publish the draft skin */
856 if( P("pub7")!=0 && PB("pub7ck1") && PB("pub7ck2") ){
857 skin_publish(iSkin);
858 }
859
860 style_header("Customize Skin");
861
862 @ <p>Customize the look of this Fossil repository by making changes
863 @ to the CSS, Header, Footer, and Detail Settings in one of nine "draft"
864 @ configurations. Then, after verifying that all is working correctly,
865 @ publish the draft to become the new main Skin.<p>
866 @
867 @ <a name='step1'></a>
868 @ <h1>Step 1: Identify Which Draft To Use</h1>
869 @
870 @ <p>The main skin of Fossil cannot be edited directly. Instead,
871 @ edits are made to one of nine draft skins. A draft skin can then
872 @ be published to become the default skin.
873 @ Nine separate drafts are available to facilitate A/B testing.</p>
874 @
875 @ <form method='POST' action='%R/setup_skin#step2' id='f01'>
876 @ <p class='skinInput'>Draft skin to edit:
877 @ <select size='1' name='sk' onchange='gebi("f01").submit()'>
878 for(i=1; i<=9; i++){
879 if( i==iSkin ){
880 @ <option value='%d(i)' selected>draft%d(i)</option>
881 }else{
882 @ <option value='%d(i)'>draft%d(i)</option>
883 }
884 }
885 @ </select>
886 @ </p>
887 @
888 @ <a name='step2'></a>
889 @ <h1>Step 2: Authenticate</h1>
890 @
891 if( isSetup ){
892 @ <p>As an administrator, you can make any edits you like to this or
893 @ any other skin. You can also authorized other users to edit this
894 @ skin. Any user whose login name matches the comma-separate list
895 @ of GLOB expressions below is given special permission to edit
896 @ the draft%d(iSkin) skin:
897 @
898 @ <form method='POST' action='%R/setup_skin#step2' id='f02'>
899 @ <p class='skinInput'>
900 @ <input type='hidden' name='sk' value='%d(iSkin)'>
901 @ Authorized editors for skin draft%d(iSkin):
902 @ <input type='text' name='editors' value='%h(zAllowedEditors)'\
903 @ width='40'>
904 @ <input type='submit' name='submit2' value='Change'>
905 @ </p>
906 @ </form>
907 }else if( isEditor ){
908 @ <p>You are authorized to make changes to the draft%d(iSkin) skin.
909 @ Continue to the <a href='#step3'>next step</a>.</p>
910 }else{
911 @ <p>You are not authorized to make changes to the draft%d(iSkin)
912 @ skin. Contact the administrator of this Fossil repository for
913 @ further information.</p>
914 }
915 @
916 @ <a name='step3'></a>
917 @ <h1>Step 3: Initialize The Draft</h1>
918 @
919 if( !isEditor ){
920 @ <p>You are not allowed to initialize draft%(iSkin). Contact
921 @ the administrator for this repository for more information.
922 }else{
923 @ <p>Initialize the draft%d(iSkin) skin to one of the built-in skins
924 @ or a preexisting skin, to use as a baseline.</p>
925 @
926 @ <form method='POST' action='%R/setup_skin#step4' id='f03'>
927 @ <p class='skinInput'>
928 @ <input type='hidden' name='sk' value='%d(iSkin)'>
929 @ Initialize skin <b>draft%d(iSkin)</b> using
930 @ <select size='1' name='initskin'>
931 @ <option value='current'>Currently In Use</option>
932 for(i=0; i<count(aBuiltinSkin); i++){
933 @ <option value='%s(aBuiltinSkin[i].zLabel)'>\
934 @ %h(aBuiltinSkin[i].zDesc) (built-in)</option>
935 }
936 @ </select>
937 @ <input type='submit' name='init3' value='Go'>
938 @ </p>
939 @ </form>
940 }
941 @
942 @ <a name='step4'></a>
943 @ <h1>Step 4: Make Edits</h1>
944 @
945 if( !isEditor ){
946 @ <p>You are not authorized to make edits to the draft%d(iSkin) skin.
947 @ Contact the administrator of this Fossil repository for help.</p>
948 }else{
949 @ <p>Edit the components of the draft%d(iSkin) skin:
950 @ <ul>
951 @ <li><a href='%R/setup_skinedit?w=0&sk=%d(iSkin)' target='_blank'>CSS</a>
952 @ <li><a href='%R/setup_skinedit?w=2&sk=%d(iSkin)' target='_blank'>\
953 @ Header</a>
954 @ <li><a href='%R/setup_skinedit?w=1&sk=%d(iSkin)' target='_blank'>\
955 @ Footer</a>
956 @ <li><a href='%R/setup_skinedit?w=3&sk=%d(iSkin)' target='_blank'>\
957 @ Details</a>
958 @ </ul>
959 }
960 @
961 @ <a name='step5'></a>
962 @ <h1>Step 5: Verify The Draft Skin</h1>
963 @
964 @ <p>To test this draft skin, insert text "/draft%d(iSkin)/" just before the
965 @ operation name in the URL. Here are a few links to try:
966 @ <ul>
967 if( iDraftSkin && sqlite3_strglob("*/draft[1-9]", g.zBaseURL)==0 ){
968 zBase = mprintf("%.*s/draft%d", (int)strlen(g.zBaseURL)-7,g.zBaseURL,iSkin);
969 }else{
970 zBase = mprintf("%s/draft%d", g.zBaseURL, iSkin);
971 }
972 for(i=0; i<count(azTestPages); i++){
973 @ <li><a href='%s(zBase)/%s(azTestPages[i])' target='_blank'>\
974 @ %s(zBase)/%s(azTestPages[i])</a>
975 }
976 fossil_free(zBase);
977 @ </ul>
978 @
979 @ <p>You will probably need to press Reload on your browser before any
980 @ CSS changes will take effect.</p>
981 @
982 @ <a hame='step6'></a>
983 @ <h1>Step 6: Interate</h1>
984 @
985 @ <p>Repeat <a href='#step4'>step 4</a> and
986 @ <a href='#step5'>step 5</a> as many times as necessary to create
987 @ a production-ready skin.
988 @
989 @ <a name='step7'></a>
990 @ <h1>Step 7: Publish</h1>
991 @
992 if( !g.perm.Setup ){
993 @ <p>Only administrators are allowed to publish draft skins. Contact
994 @ an administrator to get this "draft%d(iSkin)" skin published.</p>
995 }else{
996 @ <p>When the draft%d(iSkin) skin is ready for production use,
997 @ make it the default scan by clicking the acknowledgements and
998 @ pressing the button below:</p>
999 @
1000 @ <form method='POST' action='%R/setup_skin#step7'>
1001 @ <p class='skinInput'>
1002 @ <input type='hidden' name='sk' value='%d(iSkin)'>
1003 @ <input type='checkbox' name='pub7ck1' value='yes'>\
1004 @ Skin draft%d(iSkin) has been testing and found ready for production.<br>
1005 @ <input type='checkbox' name='pub7ck2' value='yes'>\
1006 @ The current skin should be overwritten with draft%d(iSkin).<br>
1007 @ <input type='submit' name='pub7' value='Publish Draft%d(iSkin)'>
1008 @ </p></form>
1009 @
1010 @ <p>You will probably need to press Reload on your browser after
1011 @ publishing the new skin.</p>
1012 }
1013 @
1014 @ <a name='step8'></a>
1015 @ <h1>Step 8: Cleanup and Undo Actions</h1>
1016 @
1017 if( !g.perm.Setup ){
1018 @ <p>Administrators can optionally remove save legacy skins, or
1019 @ undo a prior publish
1020 }else{
1021 @ <p>Visit the <a href='%R/setup_skin_admin'>Skin Admin</a> page
1022 @ for cleanup and recovery actions.
1023 }
1024 style_footer();
1025 }
1026

Keyboard Shortcuts

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