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