Fossil SCM

fossil-scm / src / configure.c
Blame History Raw 1156 lines
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

Keyboard Shortcuts

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