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