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