Fossil SCM

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

Keyboard Shortcuts

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