1 /*
2 ** Copyright (c) 2008 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 ** This file contains code used to manage repository configurations.
19 **
20 ** By "repository configure" we mean the local state of a repository
21 ** distinct from the versioned files.
22 */
23 #include "config.h"
24 #include "configure.h"
25 #include <assert.h>
26 
27 #if INTERFACE
28 /*
29 ** Configuration transfers occur in groups.  These are the allowed
30 ** groupings:
31 */
32 #define CONFIGSET_CSS       0x000001     /* Style sheet only */
33 #define CONFIGSET_SKIN      0x000002     /* WWW interface appearance */
34 #define CONFIGSET_TKT       0x000004     /* Ticket configuration */
35 #define CONFIGSET_PROJ      0x000008     /* Project name */
36 #define CONFIGSET_SHUN      0x000010     /* Shun settings */
37 #define CONFIGSET_USER      0x000020     /* The USER table */
38 #define CONFIGSET_ADDR      0x000040     /* The CONCEALED table */
39 #define CONFIGSET_XFER      0x000080     /* Transfer configuration */
40 #define CONFIGSET_ALIAS     0x000100     /* URL Aliases */
41 #define CONFIGSET_SCRIBER   0x000200     /* Email subscribers */
42 #define CONFIGSET_IWIKI     0x000400     /* Interwiki codes */
43 #define CONFIGSET_ALL       0x0007ff     /* Everything */
44 
45 #define CONFIGSET_OVERWRITE 0x100000     /* Causes overwrite instead of merge */
46 
47 /*
48 ** This mask is used for the common TH1 configuration settings (i.e. those
49 ** that are not specific to one particular subsystem, such as the transfer
50 ** subsystem).
51 */
52 #define CONFIGSET_TH1       (CONFIGSET_SKIN|CONFIGSET_TKT|CONFIGSET_XFER)
53 
54 #endif /* INTERFACE */
55 
56 /*
57 ** Names of the configuration sets
58 */
59 static struct {
60   const char *zName;   /* Name of the configuration set */
61   int groupMask;       /* Mask for that configuration set */
62   const char *zHelp;   /* What it does */
63 } aGroupName[] = {
64   { "/email",       CONFIGSET_ADDR,    "Concealed email addresses in tickets" },
65   { "/project",     CONFIGSET_PROJ,    "Project name and description"         },
66   { "/skin",        CONFIGSET_SKIN | CONFIGSET_CSS,
67                                        "Web interface appearance settings"    },
68   { "/css",         CONFIGSET_CSS,     "Style sheet"                          },
69   { "/shun",        CONFIGSET_SHUN,    "List of shunned artifacts"            },
70   { "/ticket",      CONFIGSET_TKT,     "Ticket setup",                        },
71   { "/user",        CONFIGSET_USER,    "Users and privilege settings"         },
72   { "/xfer",        CONFIGSET_XFER,    "Transfer setup",                      },
73   { "/alias",       CONFIGSET_ALIAS,   "URL Aliases",                         },
74   { "/subscriber",  CONFIGSET_SCRIBER, "Email notification subscriber list"   },
75   { "/interwiki",   CONFIGSET_IWIKI,   "Inter-wiki link prefixes"             },
76   { "/all",         CONFIGSET_ALL,     "All of the above"                     },
77 };
78 
79 
80 /*
81 ** The following is a list of settings that we are willing to
82 ** transfer.
83 **
84 ** Setting names that begin with an alphabetic characters refer to
85 ** single entries in the CONFIG table.  Setting names that begin with
86 ** "@" are for special processing.
87 */
88 static struct {
89   const char *zName;   /* Name of the configuration parameter */
90   int groupMask;       /* Which config groups is it part of */
91 } aConfig[] = {
92   { "css",                    CONFIGSET_CSS  },
93   { "header",                 CONFIGSET_SKIN },
94   { "mainmenu",               CONFIGSET_SKIN },
95   { "footer",                 CONFIGSET_SKIN },
96   { "details",                CONFIGSET_SKIN },
97   { "js",                     CONFIGSET_SKIN },
98   { "logo-mimetype",          CONFIGSET_SKIN },
99   { "logo-image",             CONFIGSET_SKIN },
100   { "background-mimetype",    CONFIGSET_SKIN },
101   { "background-image",       CONFIGSET_SKIN },
102   { "icon-mimetype",          CONFIGSET_SKIN },
103   { "icon-image",             CONFIGSET_SKIN },
104   { "timeline-block-markup",  CONFIGSET_SKIN },
105   { "timeline-date-format",   CONFIGSET_SKIN },
106   { "timeline-default-style", CONFIGSET_SKIN },
107   { "timeline-dwelltime",     CONFIGSET_SKIN },
108   { "timeline-closetime",     CONFIGSET_SKIN },
109   { "timeline-max-comment",   CONFIGSET_SKIN },
110   { "timeline-plaintext",     CONFIGSET_SKIN },
111   { "timeline-truncate-at-blank", CONFIGSET_SKIN },
112   { "timeline-tslink-info",   CONFIGSET_SKIN },
113   { "timeline-utc",           CONFIGSET_SKIN },
114   { "adunit",                 CONFIGSET_SKIN },
115   { "adunit-omit-if-admin",   CONFIGSET_SKIN },
116   { "adunit-omit-if-user",    CONFIGSET_SKIN },
117   { "default-csp",            CONFIGSET_SKIN },
118   { "sitemap-extra",          CONFIGSET_SKIN },
119   { "safe-html",              CONFIGSET_SKIN },
120 
121 #ifdef FOSSIL_ENABLE_TH1_DOCS
122   { "th1-docs",               CONFIGSET_TH1 },
123 #endif
124 #ifdef FOSSIL_ENABLE_TH1_HOOKS
125   { "th1-hooks",              CONFIGSET_TH1 },
126 #endif
127   { "th1-setup",              CONFIGSET_TH1 },
128   { "th1-uri-regexp",         CONFIGSET_TH1 },
129 
130 #ifdef FOSSIL_ENABLE_TCL
131   { "tcl",                    CONFIGSET_TH1 },
132   { "tcl-setup",              CONFIGSET_TH1 },
133 #endif
134 
135   { "project-name",           CONFIGSET_PROJ },
136   { "short-project-name",     CONFIGSET_PROJ },
137   { "project-description",    CONFIGSET_PROJ },
138   { "index-page",             CONFIGSET_PROJ },
139   { "manifest",               CONFIGSET_PROJ },
140   { "binary-glob",            CONFIGSET_PROJ },
141   { "clean-glob",             CONFIGSET_PROJ },
142   { "ignore-glob",            CONFIGSET_PROJ },
143   { "keep-glob",              CONFIGSET_PROJ },
144   { "crlf-glob",              CONFIGSET_PROJ },
145   { "crnl-glob",              CONFIGSET_PROJ },
146   { "encoding-glob",          CONFIGSET_PROJ },
147   { "empty-dirs",             CONFIGSET_PROJ },
148   { "dotfiles",               CONFIGSET_PROJ },
149   { "parent-project-code",    CONFIGSET_PROJ },
150   { "parent-project-name",    CONFIGSET_PROJ },
151   { "hash-policy",            CONFIGSET_PROJ },
152   { "comment-format",         CONFIGSET_PROJ },
153   { "mimetypes",              CONFIGSET_PROJ },
154   { "forbid-delta-manifests", CONFIGSET_PROJ },
155   { "mv-rm-files",            CONFIGSET_PROJ },
156   { "ticket-table",           CONFIGSET_TKT  },
157   { "ticket-common",          CONFIGSET_TKT  },
158   { "ticket-change",          CONFIGSET_TKT  },
159   { "ticket-newpage",         CONFIGSET_TKT  },
160   { "ticket-viewpage",        CONFIGSET_TKT  },
161   { "ticket-editpage",        CONFIGSET_TKT  },
162   { "ticket-reportlist",      CONFIGSET_TKT  },
163   { "ticket-report-template", CONFIGSET_TKT  },
164   { "ticket-key-template",    CONFIGSET_TKT  },
165   { "ticket-title-expr",      CONFIGSET_TKT  },
166   { "ticket-closed-expr",     CONFIGSET_TKT  },
167   { "@reportfmt",             CONFIGSET_TKT  },
168 
169   { "@user",                  CONFIGSET_USER },
170   { "user-color-map",         CONFIGSET_USER },
171 
172   { "@concealed",             CONFIGSET_ADDR },
173 
174   { "@shun",                  CONFIGSET_SHUN },
175 
176   { "@alias",                 CONFIGSET_ALIAS },
177 
178   { "@subscriber",            CONFIGSET_SCRIBER },
179 
180   { "@interwiki",             CONFIGSET_IWIKI },
181 
182   { "xfer-common-script",     CONFIGSET_XFER },
183   { "xfer-push-script",       CONFIGSET_XFER },
184   { "xfer-commit-script",     CONFIGSET_XFER },
185   { "xfer-ticket-script",     CONFIGSET_XFER },
186 
187 };
188 static int iConfig = 0;
189 
190 /*
191 ** Return name of first configuration property matching the given mask.
192 */
configure_first_name(int iMask)193 const char *configure_first_name(int iMask){
194   iConfig = 0;
195   return configure_next_name(iMask);
196 }
configure_next_name(int iMask)197 const char *configure_next_name(int iMask){
198   if( iConfig==0 && (iMask & CONFIGSET_ALL)==CONFIGSET_ALL ){
199     iConfig = count(aGroupName);
200     return "/all";
201   }
202   while( iConfig<count(aGroupName)-1 ){
203     if( aGroupName[iConfig].groupMask & iMask ){
204       return aGroupName[iConfig++].zName;
205     }else{
206       iConfig++;
207     }
208   }
209   return 0;
210 }
211 
212 /*
213 ** Return a pointer to a string that contains the RHS of an IN operator
214 ** that will select CONFIG table names that are part of the configuration
215 ** that matches iMatch.
216 */
configure_inop_rhs(int iMask)217 const char *configure_inop_rhs(int iMask){
218   Blob x;
219   int i;
220   const char *zSep = "";
221 
222   blob_zero(&x);
223   blob_append_sql(&x, "(");
224   for(i=0; i<count(aConfig); i++){
225     if( (aConfig[i].groupMask & iMask)==0 ) continue;
226     if( aConfig[i].zName[0]=='@' ) continue;
227     blob_append_sql(&x, "%s'%q'", zSep/*safe-for-%s*/, aConfig[i].zName);
228     zSep = ",";
229   }
230   blob_append_sql(&x, ")");
231   return blob_sql_text(&x);
232 }
233 
234 /*
235 ** Return the mask for the named configuration parameter if it can be
236 ** safely exported.  Return 0 if the parameter is not safe to export.
237 **
238 ** "Safe" in the previous paragraph means the permission is granted to
239 ** export the property.  In other words, the requesting side has presented
240 ** login credentials and has sufficient capabilities to access the requested
241 ** information.
242 */
configure_is_exportable(const char * zName)243 int configure_is_exportable(const char *zName){
244   int i;
245   int n = strlen(zName);
246   if( n>2 && zName[0]=='\'' && zName[n-1]=='\'' ){
247     zName++;
248     n -= 2;
249   }
250   for(i=0; i<count(aConfig); i++){
251     if( strncmp(zName, aConfig[i].zName, n)==0 && aConfig[i].zName[n]==0 ){
252       int m = aConfig[i].groupMask;
253       if( !g.perm.Admin ){
254         m &= ~(CONFIGSET_USER|CONFIGSET_SCRIBER);
255       }
256       if( !g.perm.RdAddr ){
257         m &= ~CONFIGSET_ADDR;
258       }
259       return m;
260     }
261   }
262   if( strncmp(zName, "walias:/", 8)==0 ){
263     return CONFIGSET_ALIAS;
264   }
265   if( strncmp(zName, "interwiki:", 10)==0 ){
266     return CONFIGSET_IWIKI;
267   }
268   return 0;
269 }
270 
271 /*
272 ** A mask of all configuration tables that have been reset already.
273 */
274 static int configHasBeenReset = 0;
275 
276 /*
277 ** Mask of modified configuration sets
278 */
279 static int rebuildMask = 0;
280 
281 /*
282 ** Rebuild auxiliary tables as required by configuration changes.
283 */
configure_rebuild(void)284 void configure_rebuild(void){
285   if( rebuildMask & CONFIGSET_TKT ){
286     ticket_rebuild();
287   }
288   rebuildMask = 0;
289 }
290 
291 /*
292 ** Return true if z[] is not a "safe" SQL token.  A safe token is one of:
293 **
294 **   *   A string literal
295 **   *   A blob literal
296 **   *   An integer literal  (no floating point)
297 **   *   NULL
298 */
safeSql(const char * z)299 static int safeSql(const char *z){
300   int i;
301   if( z==0 || z[0]==0 ) return 0;
302   if( (z[0]=='x' || z[0]=='X') && z[1]=='\'' ) z++;
303   if( z[0]=='\'' ){
304     for(i=1; z[i]; i++){
305       if( z[i]=='\'' ){
306         i++;
307         if( z[i]=='\'' ){ continue; }
308         return z[i]==0;
309       }
310     }
311     return 0;
312   }else{
313     char c;
314     for(i=0; (c = z[i])!=0; i++){
315       if( !fossil_isalnum(c) ) return 0;
316     }
317   }
318   return 1;
319 }
320 
321 /*
322 ** Return true if z[] consists of nothing but digits
323 */
safeInt(const char * z)324 static int safeInt(const char *z){
325   int i;
326   if( z==0 || z[0]==0 ) return 0;
327   for(i=0; fossil_isdigit(z[i]); i++){}
328   return z[i]==0;
329 }
330 
331 /*
332 ** Process a single "config" card received from the other side of a
333 ** sync session.
334 **
335 ** Mask consists of one or more CONFIGSET_* values ORed together, to
336 ** designate what types of configuration we are allowed to receive.
337 **
338 ** NEW FORMAT:
339 **
340 ** zName is one of:
341 **
342 **     "/config", "/user",  "/shun", "/reportfmt", "/concealed",
343 **     "/subscriber",
344 **
345 ** zName indicates the table that holds the configuration information being
346 ** transferred.  pContent is a string that consist of alternating Fossil
347 ** and SQL tokens.  The First token is a timestamp in seconds since 1970.
348 ** The second token is a primary key for the table identified by zName.  If
349 ** The entry with the corresponding primary key exists and has a more recent
350 ** mtime, then nothing happens.  If the entry does not exist or if it has
351 ** an older mtime, then the content described by subsequent token pairs is
352 ** inserted.  The first element of each token pair is a column name and
353 ** the second is its value.
354 **
355 ** In overview, we have:
356 **
357 **    NAME        CONTENT
358 **    -------     -----------------------------------------------------------
359 **    /config     $MTIME $NAME value $VALUE
360 **    /user       $MTIME $LOGIN pw $VALUE cap $VALUE info $VALUE photo $VALUE
361 **    /shun       $MTIME $UUID scom $VALUE
362 **    /reportfmt  $MTIME $TITLE owner $VALUE cols $VALUE sqlcode $VALUE
363 **    /concealed  $MTIME $HASH content $VALUE
364 **    /subscriber $SMTIME $SEMAIL suname $V ...
365 */
configure_receive(const char * zName,Blob * pContent,int groupMask)366 void configure_receive(const char *zName, Blob *pContent, int groupMask){
367   int checkMask;   /* Masks for which we must first check existance of tables */
368 
369   checkMask = CONFIGSET_SCRIBER;
370   if( zName[0]=='/' ){
371     /* The new format */
372     char *azToken[24];
373     int nToken = 0;
374     int ii, jj;
375     int thisMask;
376     Blob name, value, sql;
377     static const struct receiveType {
378       const char *zName;         /* Configuration key for this table */
379       const char *zPrimKey;      /* Primary key column */
380       int nField;                /* Number of data fields */
381       const char *azField[6];    /* Names of the data fields */
382     } aType[] = {
383       { "/config",    "name",  1, { "value", 0,0,0,0,0 }           },
384       { "@user",      "login", 4, { "pw","cap","info","photo",0,0} },
385       { "@shun",      "uuid",  1, { "scom", 0,0,0,0,0}             },
386       { "@reportfmt", "title", 3, { "owner","cols","sqlcode",0,0,0}},
387       { "@concealed", "hash",  1, { "content", 0,0,0,0,0 }         },
388       { "@subscriber","semail",6,
389          { "suname","sdigest","sdonotcall","ssub","sctime","smip"}         },
390     };
391 
392     /* Locate the receiveType in aType[ii] */
393     for(ii=0; ii<count(aType); ii++){
394       if( fossil_strcmp(&aType[ii].zName[1],&zName[1])==0 ) break;
395     }
396     if( ii>=count(aType) ) return;
397 
398     while( blob_token(pContent, &name) && blob_sqltoken(pContent, &value) ){
399       char *z = blob_terminate(&name);
400       if( !safeSql(z) ) return;
401       if( nToken>0 ){
402         for(jj=0; jj<aType[ii].nField; jj++){
403           if( fossil_strcmp(aType[ii].azField[jj], z)==0 ) break;
404         }
405         if( jj>=aType[ii].nField ) continue;
406       }else{
407         if( !safeInt(z) ) return;
408       }
409       azToken[nToken++] = z;
410       azToken[nToken++] = z = blob_terminate(&value);
411       if( !safeSql(z) ) return;
412       if( nToken>=count(azToken)-1 ) break;
413     }
414     if( nToken<2 ) return;
415     if( aType[ii].zName[0]=='/' ){
416       thisMask = configure_is_exportable(azToken[1]);
417     }else{
418       thisMask = configure_is_exportable(aType[ii].zName);
419     }
420     if( (thisMask & groupMask)==0 ) return;
421     if( (thisMask & checkMask)!=0 ){
422       if( (thisMask & CONFIGSET_SCRIBER)!=0 ){
423         alert_schema(1);
424       }
425       checkMask &= ~thisMask;
426     }
427 
428     blob_zero(&sql);
429     if( groupMask & CONFIGSET_OVERWRITE ){
430       if( (thisMask & configHasBeenReset)==0 && aType[ii].zName[0]!='/' ){
431         db_multi_exec("DELETE FROM \"%w\"", &aType[ii].zName[1]);
432         configHasBeenReset |= thisMask;
433       }
434       blob_append_sql(&sql, "REPLACE INTO ");
435     }else{
436       blob_append_sql(&sql, "INSERT OR IGNORE INTO ");
437     }
438     blob_append_sql(&sql, "\"%w\"(\"%w\",mtime",
439          &zName[1], aType[ii].zPrimKey);
440     if( fossil_stricmp(zName,"/subscriber")==0 ) alert_schema(0);
441     for(jj=2; jj<nToken; jj+=2){
442        blob_append_sql(&sql, ",\"%w\"", azToken[jj]);
443     }
444     blob_append_sql(&sql,") VALUES(%s,%s",
445        azToken[1] /*safe-for-%s*/, azToken[0]/*safe-for-%s*/);
446     for(jj=2; jj<nToken; jj+=2){
447        blob_append_sql(&sql, ",%s", azToken[jj+1] /*safe-for-%s*/);
448     }
449     db_protect_only(PROTECT_SENSITIVE);
450     db_multi_exec("%s)", blob_sql_text(&sql));
451     if( db_changes()==0 ){
452       blob_reset(&sql);
453       blob_append_sql(&sql, "UPDATE \"%w\" SET mtime=%s",
454                       &zName[1], azToken[0]/*safe-for-%s*/);
455       for(jj=2; jj<nToken; jj+=2){
456         blob_append_sql(&sql, ", \"%w\"=%s",
457                         azToken[jj], azToken[jj+1]/*safe-for-%s*/);
458       }
459       blob_append_sql(&sql, " WHERE \"%w\"=%s AND mtime<%s",
460                    aType[ii].zPrimKey, azToken[1]/*safe-for-%s*/,
461                    azToken[0]/*safe-for-%s*/);
462       db_multi_exec("%s", blob_sql_text(&sql));
463     }
464     db_protect_pop();
465     blob_reset(&sql);
466     rebuildMask |= thisMask;
467   }
468 }
469 
470 /*
471 ** Process a file full of "config" cards.
472 */
configure_receive_all(Blob * pIn,int groupMask)473 void configure_receive_all(Blob *pIn, int groupMask){
474   Blob line;
475   int nToken;
476   int size;
477   Blob aToken[4];
478 
479   configHasBeenReset = 0;
480   while( blob_line(pIn, &line) ){
481     if( blob_buffer(&line)[0]=='#' ) continue;
482     nToken = blob_tokenize(&line, aToken, count(aToken));
483     if( blob_eq(&aToken[0],"config")
484      && nToken==3
485      && blob_is_int(&aToken[2], &size)
486     ){
487       const char *zName = blob_str(&aToken[1]);
488       Blob content;
489       blob_zero(&content);
490       blob_extract(pIn, size, &content);
491       g.perm.Admin = g.perm.RdAddr = 1;
492       configure_receive(zName, &content, groupMask);
493       blob_reset(&content);
494       blob_seek(pIn, 1, BLOB_SEEK_CUR);
495     }
496   }
497 }
498 
499 
500 /*
501 ** Send "config" cards using the new format for all elements of a group
502 ** that have recently changed.
503 **
504 ** Output goes into pOut.  The groupMask identifies the group(s) to be sent.
505 ** Send only entries whose timestamp is later than or equal to iStart.
506 **
507 ** Return the number of cards sent.
508 */
configure_send_group(Blob * pOut,int groupMask,sqlite3_int64 iStart)509 int configure_send_group(
510   Blob *pOut,              /* Write output here */
511   int groupMask,           /* Mask of groups to be send */
512   sqlite3_int64 iStart     /* Only write values changed since this time */
513 ){
514   Stmt q;
515   Blob rec;
516   int ii;
517   int nCard = 0;
518 
519   blob_zero(&rec);
520   if( groupMask & CONFIGSET_SHUN ){
521     db_prepare(&q, "SELECT mtime, quote(uuid), quote(scom) FROM shun"
522                    " WHERE mtime>=%lld", iStart);
523     while( db_step(&q)==SQLITE_ROW ){
524       blob_appendf(&rec,"%s %s scom %s",
525         db_column_text(&q, 0),
526         db_column_text(&q, 1),
527         db_column_text(&q, 2)
528       );
529       blob_appendf(pOut, "config /shun %d\n%s\n",
530                    blob_size(&rec), blob_str(&rec));
531       nCard++;
532       blob_reset(&rec);
533     }
534     db_finalize(&q);
535   }
536   if( groupMask & CONFIGSET_USER ){
537     db_prepare(&q, "SELECT mtime, quote(login), quote(pw), quote(cap),"
538                    "       quote(info), quote(photo) FROM user"
539                    " WHERE mtime>=%lld", iStart);
540     while( db_step(&q)==SQLITE_ROW ){
541       blob_appendf(&rec,"%s %s pw %s cap %s info %s photo %s",
542         db_column_text(&q, 0),
543         db_column_text(&q, 1),
544         db_column_text(&q, 2),
545         db_column_text(&q, 3),
546         db_column_text(&q, 4),
547         db_column_text(&q, 5)
548       );
549       blob_appendf(pOut, "config /user %d\n%s\n",
550                    blob_size(&rec), blob_str(&rec));
551       nCard++;
552       blob_reset(&rec);
553     }
554     db_finalize(&q);
555   }
556   if( groupMask & CONFIGSET_TKT ){
557     db_prepare(&q, "SELECT mtime, quote(title), quote(owner), quote(cols),"
558                    "       quote(sqlcode) FROM reportfmt"
559                    " WHERE mtime>=%lld", iStart);
560     while( db_step(&q)==SQLITE_ROW ){
561       blob_appendf(&rec,"%s %s owner %s cols %s sqlcode %s",
562         db_column_text(&q, 0),
563         db_column_text(&q, 1),
564         db_column_text(&q, 2),
565         db_column_text(&q, 3),
566         db_column_text(&q, 4)
567       );
568       blob_appendf(pOut, "config /reportfmt %d\n%s\n",
569                    blob_size(&rec), blob_str(&rec));
570       nCard++;
571       blob_reset(&rec);
572     }
573     db_finalize(&q);
574   }
575   if( groupMask & CONFIGSET_ADDR ){
576     db_prepare(&q, "SELECT mtime, quote(hash), quote(content) FROM concealed"
577                    " WHERE mtime>=%lld", iStart);
578     while( db_step(&q)==SQLITE_ROW ){
579       blob_appendf(&rec,"%s %s content %s",
580         db_column_text(&q, 0),
581         db_column_text(&q, 1),
582         db_column_text(&q, 2)
583       );
584       blob_appendf(pOut, "config /concealed %d\n%s\n",
585                    blob_size(&rec), blob_str(&rec));
586       nCard++;
587       blob_reset(&rec);
588     }
589     db_finalize(&q);
590   }
591   if( groupMask & CONFIGSET_ALIAS ){
592     db_prepare(&q, "SELECT mtime, quote(name), quote(value) FROM config"
593                    " WHERE name GLOB 'walias:/*' AND mtime>=%lld", iStart);
594     while( db_step(&q)==SQLITE_ROW ){
595       blob_appendf(&rec,"%s %s value %s",
596         db_column_text(&q, 0),
597         db_column_text(&q, 1),
598         db_column_text(&q, 2)
599       );
600       blob_appendf(pOut, "config /config %d\n%s\n",
601                    blob_size(&rec), blob_str(&rec));
602       nCard++;
603       blob_reset(&rec);
604     }
605     db_finalize(&q);
606   }
607   if( groupMask & CONFIGSET_IWIKI ){
608     db_prepare(&q, "SELECT mtime, quote(name), quote(value) FROM config"
609                    " WHERE name GLOB 'interwiki:*' AND mtime>=%lld", iStart);
610     while( db_step(&q)==SQLITE_ROW ){
611       blob_appendf(&rec,"%s %s value %s",
612         db_column_text(&q, 0),
613         db_column_text(&q, 1),
614         db_column_text(&q, 2)
615       );
616       blob_appendf(pOut, "config /config %d\n%s\n",
617                    blob_size(&rec), blob_str(&rec));
618       nCard++;
619       blob_reset(&rec);
620     }
621     db_finalize(&q);
622   }
623   if( (groupMask & CONFIGSET_SCRIBER)!=0
624    && db_table_exists("repository","subscriber")
625   ){
626     db_prepare(&q, "SELECT mtime, quote(semail),"
627                    " quote(suname), quote(sdigest),"
628                    " quote(sdonotcall), quote(ssub),"
629                    " quote(sctime), quote(smip)"
630                    " FROM subscriber WHERE sverified"
631                    " AND mtime>=%lld", iStart);
632     while( db_step(&q)==SQLITE_ROW ){
633       blob_appendf(&rec,
634         "%lld %s suname %s sdigest %s sdonotcall %s ssub %s"
635         " sctime %s smip %s",
636         db_column_int64(&q, 0), /* mtime */
637         db_column_text(&q, 1),  /* semail (PK) */
638         db_column_text(&q, 2),  /* suname */
639         db_column_text(&q, 3),  /* sdigest */
640         db_column_text(&q, 4),  /* sdonotcall */
641         db_column_text(&q, 5),  /* ssub */
642         db_column_text(&q, 6),  /* sctime */
643         db_column_text(&q, 7)   /* smip */
644       );
645       blob_appendf(pOut, "config /subscriber %d\n%s\n",
646                    blob_size(&rec), blob_str(&rec));
647       nCard++;
648       blob_reset(&rec);
649     }
650     db_finalize(&q);
651   }
652   db_prepare(&q, "SELECT mtime, quote(name), quote(value) FROM config"
653                  " WHERE name=:name AND mtime>=%lld", iStart);
654   for(ii=0; ii<count(aConfig); ii++){
655     if( (aConfig[ii].groupMask & groupMask)!=0 && aConfig[ii].zName[0]!='@' ){
656       db_bind_text(&q, ":name", aConfig[ii].zName);
657       while( db_step(&q)==SQLITE_ROW ){
658         blob_appendf(&rec,"%s %s value %s",
659           db_column_text(&q, 0),
660           db_column_text(&q, 1),
661           db_column_text(&q, 2)
662         );
663         blob_appendf(pOut, "config /config %d\n%s\n",
664                      blob_size(&rec), blob_str(&rec));
665         nCard++;
666         blob_reset(&rec);
667       }
668       db_reset(&q);
669     }
670   }
671   db_finalize(&q);
672   return nCard;
673 }
674 
675 /*
676 ** Identify a configuration group by name.  Return its mask.
677 ** Throw an error if no match.
678 */
configure_name_to_mask(const char * z,int notFoundIsFatal)679 int configure_name_to_mask(const char *z, int notFoundIsFatal){
680   int i;
681   int n = strlen(z);
682   for(i=0; i<count(aGroupName); i++){
683     if( strncmp(z, &aGroupName[i].zName[1], n)==0 ){
684       return aGroupName[i].groupMask;
685     }
686   }
687   if( notFoundIsFatal ){
688     fossil_print("Available configuration areas:\n");
689     for(i=0; i<count(aGroupName); i++){
690       fossil_print("  %-13s %s\n",
691             &aGroupName[i].zName[1], aGroupName[i].zHelp);
692     }
693     fossil_fatal("no such configuration area: \"%s\"", z);
694   }
695   return 0;
696 }
697 
698 /*
699 ** Write SQL text into file zFilename that will restore the configuration
700 ** area identified by mask to its current state from any other state.
701 */
export_config(int groupMask,const char * zMask,sqlite3_int64 iStart,const char * zFilename)702 static void export_config(
703   int groupMask,            /* Mask indicating which configuration to export */
704   const char *zMask,        /* Name of the configuration */
705   sqlite3_int64 iStart,     /* Start date */
706   const char *zFilename     /* Write into this file */
707 ){
708   Blob out;
709   blob_zero(&out);
710   blob_appendf(&out,
711     "# The \"%s\" configuration exported from\n"
712     "# repository \"%s\"\n"
713     "# on %s\n",
714     zMask, g.zRepositoryName,
715     db_text(0, "SELECT datetime('now')")
716   );
717   configure_send_group(&out, groupMask, iStart);
718   blob_write_to_file(&out, zFilename);
719   blob_reset(&out);
720 }
721 
722 
723 /*
724 ** COMMAND: configuration*
725 **
726 ** Usage: %fossil configuration METHOD ... ?OPTIONS?
727 **
728 ** Where METHOD is one of: export import merge pull push reset.  All methods
729 ** accept the -R or --repository option to specify a repository.
730 **
731 ** >  fossil configuration export AREA FILENAME
732 **
733 **         Write to FILENAME exported configuration information for AREA.
734 **         AREA can be one of:
735 **
736 **             all email interwiki project shun skin
737 **             ticket user alias subscriber
738 **
739 ** >  fossil configuration import FILENAME
740 **
741 **         Read a configuration from FILENAME, overwriting the current
742 **         configuration.
743 **
744 ** >  fossil configuration merge FILENAME
745 **
746 **         Read a configuration from FILENAME and merge its values into
747 **         the current configuration.  Existing values take priority over
748 **         values read from FILENAME.
749 **
750 ** >  fossil configuration pull AREA ?URL?
751 **
752 **         Pull and install the configuration from a different server
753 **         identified by URL.  If no URL is specified, then the default
754 **         server is used.  Use the --overwrite flag to completely
755 **         replace local settings with content received from URL.
756 **
757 ** >  fossil configuration push AREA ?URL?
758 **
759 **         Push the local configuration into the remote server identified
760 **         by URL.  Admin privilege is required on the remote server for
761 **         this to work.  When the same record exists both locally and on
762 **         the remote end, the one that was most recently changed wins.
763 **
764 ** >  fossil configuration reset AREA
765 **
766 **         Restore the configuration to the default.  AREA as above.
767 **
768 ** >  fossil configuration sync AREA ?URL?
769 **
770 **         Synchronize configuration changes in the local repository with
771 **         the remote repository at URL.
772 **
773 ** Options:
774 **    -R|--repository REPO       Extract info from repository REPO
775 **
776 ** See also: [[settings]], [[unset]]
777 */
configuration_cmd(void)778 void configuration_cmd(void){
779   int n;
780   const char *zMethod;
781   db_find_and_open_repository(0, 0);
782   db_open_config(0, 0);
783   if( g.argc<3 ){
784     usage("SUBCOMMAND ...");
785   }
786   zMethod = g.argv[2];
787   n = strlen(zMethod);
788   if( strncmp(zMethod, "export", n)==0 ){
789     int mask;
790     const char *zSince = find_option("since",0,1);
791     sqlite3_int64 iStart;
792     if( g.argc!=5 ){
793       usage("export AREA FILENAME");
794     }
795     mask = configure_name_to_mask(g.argv[3], 1);
796     if( zSince ){
797       iStart = db_multi_exec(
798          "SELECT coalesce(strftime('%%s',%Q),strftime('%%s','now',%Q))+0",
799          zSince, zSince
800       );
801     }else{
802       iStart = 0;
803     }
804     export_config(mask, g.argv[3], iStart, g.argv[4]);
805   }else
806   if( strncmp(zMethod, "import", n)==0
807        || strncmp(zMethod, "merge", n)==0 ){
808     Blob in;
809     int groupMask;
810     if( g.argc!=4 ) usage(mprintf("%s FILENAME",zMethod));
811     blob_read_from_file(&in, g.argv[3], ExtFILE);
812     db_begin_transaction();
813     if( zMethod[0]=='i' ){
814       groupMask = CONFIGSET_ALL | CONFIGSET_OVERWRITE;
815     }else{
816       groupMask = CONFIGSET_ALL;
817     }
818     db_unprotect(PROTECT_USER);
819     configure_receive_all(&in, groupMask);
820     db_protect_pop();
821     db_end_transaction(0);
822   }else
823   if( strncmp(zMethod, "pull", n)==0
824    || strncmp(zMethod, "push", n)==0
825    || strncmp(zMethod, "sync", n)==0
826   ){
827     int mask;
828     const char *zServer = 0;
829     int overwriteFlag = 0;
830 
831     if( strncmp(zMethod,"pull",n)==0 ){
832       overwriteFlag = find_option("overwrite",0,0)!=0;
833     }
834     url_proxy_options();
835     if( g.argc!=4 && g.argc!=5 ){
836       usage(mprintf("%s AREA ?URL?", zMethod));
837     }
838     mask = configure_name_to_mask(g.argv[3], 1);
839     if( g.argc==5 ){
840       zServer = g.argv[4];
841     }
842     url_parse(zServer, URL_PROMPT_PW);
843     if( g.url.protocol==0 ) fossil_fatal("no server URL specified");
844     user_select();
845     url_enable_proxy("via proxy: ");
846     if( overwriteFlag ) mask |= CONFIGSET_OVERWRITE;
847     if( strncmp(zMethod, "push", n)==0 ){
848       client_sync(0,0,(unsigned)mask,0);
849     }else if( strncmp(zMethod, "pull", n)==0 ){
850       client_sync(0,(unsigned)mask,0,0);
851     }else{
852       client_sync(0,(unsigned)mask,(unsigned)mask,0);
853     }
854   }else
855   if( strncmp(zMethod, "reset", n)==0 ){
856     int mask, i;
857     char *zBackup;
858     if( g.argc!=4 ) usage("reset AREA");
859     mask = configure_name_to_mask(g.argv[3], 1);
860     zBackup = db_text(0,
861        "SELECT strftime('config-backup-%%Y%%m%%d%%H%%M%%f','now')");
862     db_begin_transaction();
863     export_config(mask, g.argv[3], 0, zBackup);
864     for(i=0; i<count(aConfig); i++){
865       const char *zName = aConfig[i].zName;
866       if( (aConfig[i].groupMask & mask)==0 ) continue;
867       if( zName[0]!='@' ){
868         db_unprotect(PROTECT_CONFIG);
869         db_multi_exec("DELETE FROM config WHERE name=%Q", zName);
870         db_protect_pop();
871       }else if( fossil_strcmp(zName,"@user")==0 ){
872         db_unprotect(PROTECT_USER);
873         db_multi_exec("DELETE FROM user");
874         db_protect_pop();
875         db_create_default_users(0, 0);
876       }else if( fossil_strcmp(zName,"@concealed")==0 ){
877         db_multi_exec("DELETE FROM concealed");
878       }else if( fossil_strcmp(zName,"@shun")==0 ){
879         db_multi_exec("DELETE FROM shun");
880       }else if( fossil_strcmp(zName,"@subscriber")==0 ){
881         if( db_table_exists("repository","subscriber") ){
882           db_multi_exec("DELETE FROM subscriber");
883         }
884       }else if( fossil_strcmp(zName,"@forum")==0 ){
885         if( db_table_exists("repository","forumpost") ){
886           db_multi_exec("DELETE FROM forumpost");
887           db_multi_exec("DELETE FROM forumthread");
888         }
889       }else if( fossil_strcmp(zName,"@reportfmt")==0 ){
890         db_multi_exec("DELETE FROM reportfmt");
891         assert( strchr(zRepositorySchemaDefaultReports,'%')==0 );
892         db_multi_exec(zRepositorySchemaDefaultReports /*works-like:""*/);
893       }
894     }
895     db_end_transaction(0);
896     fossil_print("Configuration reset to factory defaults.\n");
897     fossil_print("To recover, use:  %s %s import %s\n",
898             g.argv[0], g.argv[1], zBackup);
899     rebuildMask |= mask;
900   }else
901   {
902     fossil_fatal("METHOD should be one of:"
903                  " export import merge pull push reset");
904   }
905   configure_rebuild();
906 }
907 
908 
909 /*
910 ** COMMAND: test-var-list
911 **
912 ** Usage: %fossil test-var-list ?PATTERN? ?--unset? ?--mtime?
913 **
914 ** Show the content of the CONFIG table in a repository.  If PATTERN is
915 ** specified, then only show the entries that match that glob pattern.
916 ** Last modification time is shown if the --mtime option is present.
917 **
918 ** If the --unset option is included, then entries are deleted rather than
919 ** being displayed.  WARNING! This cannot be undone.  Be sure you know what
920 ** you are doing!  The --unset option only works if there is a PATTERN.
921 ** Probably you should run the command once without --unset to make sure
922 ** you know exactly what is being deleted.
923 **
924 ** If not in an open check-out, use the -R REPO option to specify a
925 ** a repository.
926 */
test_var_list_cmd(void)927 void test_var_list_cmd(void){
928   Stmt q;
929   int i, j;
930   const char *zPattern = 0;
931   int doUnset;
932   int showMtime;
933   Blob sql;
934   Blob ans;
935   unsigned char zTrans[1000];
936 
937   doUnset = find_option("unset",0,0)!=0;
938   showMtime = find_option("mtime",0,0)!=0;
939   db_find_and_open_repository(OPEN_ANY_SCHEMA, 0);
940   verify_all_options();
941   if( g.argc>=3 ){
942     zPattern = g.argv[2];
943   }
944   blob_init(&sql,0,0);
945   blob_appendf(&sql, "SELECT name, value, datetime(mtime,'unixepoch')"
946                      " FROM config");
947   if( zPattern ){
948     blob_appendf(&sql, " WHERE name GLOB %Q", zPattern);
949   }
950   if( showMtime ){
951     blob_appendf(&sql, " ORDER BY mtime, name");
952   }else{
953     blob_appendf(&sql, " ORDER BY name");
954   }
955   db_prepare(&q, "%s", blob_str(&sql)/*safe-for-%s*/);
956   blob_reset(&sql);
957 #define MX_VAL 40
958 #define MX_NM  28
959 #define MX_LONGNM 60
960   while( db_step(&q)==SQLITE_ROW ){
961     const char *zName = db_column_text(&q,0);
962     int nName = db_column_bytes(&q,0);
963     const char *zValue = db_column_text(&q,1);
964     int szValue = db_column_bytes(&q,1);
965     const char *zMTime = db_column_text(&q,2);
966     for(i=j=0; j<MX_VAL && zValue[i]; i++){
967       unsigned char c = (unsigned char)zValue[i];
968       if( c>=' ' && c<='~' ){
969         zTrans[j++] = c;
970       }else{
971         zTrans[j++] = '\\';
972         if( c=='\n' ){
973           zTrans[j++] = 'n';
974         }else if( c=='\r' ){
975           zTrans[j++] = 'r';
976         }else if( c=='\t' ){
977           zTrans[j++] = 't';
978         }else{
979           zTrans[j++] = '0' + ((c>>6)&7);
980           zTrans[j++] = '0' + ((c>>3)&7);
981           zTrans[j++] = '0' + (c&7);
982         }
983       }
984     }
985     zTrans[j] = 0;
986     if( i<szValue ){
987       sqlite3_snprintf(sizeof(zTrans)-j, (char*)zTrans+j, "...+%d", szValue-i);
988       j += (int)strlen((char*)zTrans+j);
989     }
990     if( showMtime ){
991       fossil_print("%s:%*s%s\n", zName, 58-nName, "", zMTime);
992     }else if( nName<MX_NM-2 ){
993       fossil_print("%s:%*s%s\n", zName, MX_NM-1-nName, "", zTrans);
994     }else if( nName<MX_LONGNM-2 && j<10 ){
995       fossil_print("%s:%*s%s\n", zName, MX_LONGNM-1-nName, "", zTrans);
996     }else{
997       fossil_print("%s:\n%*s%s\n", zName, MX_NM, "", zTrans);
998     }
999   }
1000   db_finalize(&q);
1001   if( zPattern && doUnset ){
1002     prompt_user("Delete all of the above? (y/N)? ", &ans);
1003     if( blob_str(&ans)[0]=='y' || blob_str(&ans)[0]=='Y' ){
1004       db_multi_exec("DELETE FROM config WHERE name GLOB %Q", zPattern);
1005     }
1006     blob_reset(&ans);
1007   }
1008 }
1009 
1010 /*
1011 ** COMMAND: test-var-get
1012 **
1013 ** Usage: %fossil test-var-get VAR ?FILE?
1014 **
1015 ** Write the text of the VAR variable into FILE.  If FILE is "-"
1016 ** or is omitted then output goes to standard output.  VAR can be a
1017 ** GLOB pattern.
1018 **
1019 ** If not in an open check-out, use the -R REPO option to specify a
1020 ** a repository.
1021 */
test_var_get_cmd(void)1022 void test_var_get_cmd(void){
1023   const char *zVar;
1024   const char *zFile;
1025   int n;
1026   Blob x;
1027   db_find_and_open_repository(OPEN_ANY_SCHEMA, 0);
1028   verify_all_options();
1029   if( g.argc<3 ){
1030     usage("VAR ?FILE?");
1031   }
1032   zVar = g.argv[2];
1033   zFile = g.argc>=4 ? g.argv[3] : "-";
1034   n = db_int(0, "SELECT count(*) FROM config WHERE name GLOB %Q", zVar);
1035   if( n==0 ){
1036     fossil_fatal("no match for %Q", zVar);
1037   }
1038   if( n>1 ){
1039     fossil_fatal("multiple matches: %s",
1040       db_text(0, "SELECT group_concat(quote(name),', ') FROM ("
1041                  " SELECT name FROM config WHERE name GLOB %Q ORDER BY 1)",
1042                  zVar));
1043   }
1044   blob_init(&x,0,0);
1045   db_blob(&x, "SELECT value FROM config WHERE name GLOB %Q", zVar);
1046   blob_write_to_file(&x, zFile);
1047 }
1048 
1049 /*
1050 ** COMMAND: test-var-set
1051 **
1052 ** Usage: %fossil test-var-set VAR ?VALUE? ?--file FILE?
1053 **
1054 ** Store VALUE or the content of FILE (exactly one of which must be
1055 ** supplied) into variable VAR.  Use a FILE of "-" to read from
1056 ** standard input.
1057 **
1058 ** WARNING: changing the value of a variable can interfere with the
1059 ** operation of Fossil.  Be sure you know what you are doing.
1060 **
1061 ** Use "--blob FILE" instead of "--file FILE" to load a binary blob
1062 ** such as a GIF.
1063 */
test_var_set_cmd(void)1064 void test_var_set_cmd(void){
1065   const char *zVar;
1066   const char *zFile;
1067   const char *zBlob;
1068   Blob x;
1069   Stmt ins;
1070   zFile = find_option("file",0,1);
1071   zBlob = find_option("blob",0,1);
1072   db_find_and_open_repository(OPEN_ANY_SCHEMA, 0);
1073   verify_all_options();
1074   if( g.argc<3 || (zFile==0 && zBlob==0 && g.argc<4) ){
1075     usage("VAR ?VALUE? ?--file FILE?");
1076   }
1077   zVar = g.argv[2];
1078   if( zFile ){
1079     if( zBlob ) fossil_fatal("cannot do both --file or --blob");
1080     blob_read_from_file(&x, zFile, ExtFILE);
1081   }else if( zBlob ){
1082     blob_read_from_file(&x, zBlob, ExtFILE);
1083   }else{
1084     blob_init(&x,g.argv[3],-1);
1085   }
1086   db_unprotect(PROTECT_CONFIG);
1087   db_prepare(&ins,
1088      "REPLACE INTO config(name,value,mtime)"
1089      "VALUES(%Q,:val,now())", zVar);
1090   if( zBlob ){
1091     db_bind_blob(&ins, ":val", &x);
1092   }else{
1093     db_bind_text(&ins, ":val", blob_str(&x));
1094   }
1095   db_step(&ins);
1096   db_finalize(&ins);
1097   db_protect_pop();
1098   blob_reset(&x);
1099 }
1100