diff options
| author | cashmere <cashmere@autistici.org> | 2026-09-04 09:05:24 +0200 |
|---|---|---|
| committer | cashmere <cashmere@autistici.org> | 2026-09-04 09:05:24 +0200 |
| commit | 4f49b16079b27d7ce633619e4cddfdf2bffbc952 (patch) | |
| tree | c296c1e7f3c3e2f6859c6d3a1b1b208779b89679 /src/configure.c | |
| download | relic-master.tar.gz | |
Diffstat (limited to 'src/configure.c')
| -rw-r--r-- | src/configure.c | 1157 |
1 files changed, 1157 insertions, 0 deletions
diff --git a/src/configure.c b/src/configure.c new file mode 100644 index 0000000..353ebd6 --- /dev/null +++ b/src/configure.c @@ -0,0 +1,1157 @@ +/* +** Copyright (c) 2008 D. Richard Hipp +** +** This program is free software; you can redistribute it and/or +** modify it under the terms of the Simplified BSD License (also +** known as the "2-Clause License" or "FreeBSD License".) +** +** This program is distributed in the hope that it will be useful, +** but without any warranty; without even the implied warranty of +** merchantability or fitness for a particular purpose. +** +** Author contact information: +** drh@hwaci.com +** http://www.hwaci.com/drh/ +** +******************************************************************************* +** +** This file contains code used to manage repository configurations. +** +** By "repository configure" we mean the local state of a repository +** distinct from the versioned files. +*/ +#include "config.h" +#include "configure.h" +#include <assert.h> + +#if INTERFACE +/* +** Configuration transfers occur in groups. These are the allowed +** groupings: +*/ +#define CONFIGSET_CSS 0x000001 /* Style sheet only */ +#define CONFIGSET_SKIN 0x000002 /* WWW interface appearance */ +#define CONFIGSET_TKT 0x000004 /* Ticket configuration */ +#define CONFIGSET_PROJ 0x000008 /* Project name */ +#define CONFIGSET_SHUN 0x000010 /* Shun settings */ +#define CONFIGSET_USER 0x000020 /* The USER table */ +#define CONFIGSET_ADDR 0x000040 /* The CONCEALED table */ +#define CONFIGSET_XFER 0x000080 /* Transfer configuration */ +#define CONFIGSET_ALIAS 0x000100 /* URL Aliases */ +#define CONFIGSET_SCRIBER 0x000200 /* Email subscribers */ +#define CONFIGSET_IWIKI 0x000400 /* Interwiki codes */ +#define CONFIGSET_ALL 0x0007ff /* Everything */ + +#define CONFIGSET_OVERWRITE 0x100000 /* Causes overwrite instead of merge */ + +/* +** This mask is used for the common TH1 configuration settings (i.e. those +** that are not specific to one particular subsystem, such as the transfer +** subsystem). +*/ +#define CONFIGSET_TH1 (CONFIGSET_SKIN|CONFIGSET_TKT|CONFIGSET_XFER) + +#endif /* INTERFACE */ + +/* +** Names of the configuration sets +*/ +static struct { + const char *zName; /* Name of the configuration set */ + int groupMask; /* Mask for that configuration set */ + const char *zHelp; /* What it does */ +} aGroupName[] = { + { "/email", CONFIGSET_ADDR, "Concealed email addresses in tickets" }, + { "/project", CONFIGSET_PROJ, "Project name and description" }, + { "/skin", CONFIGSET_SKIN | CONFIGSET_CSS, + "Web interface appearance settings" }, + { "/css", CONFIGSET_CSS, "Style sheet" }, + { "/shun", CONFIGSET_SHUN, "List of shunned artifacts" }, + { "/ticket", CONFIGSET_TKT, "Ticket setup", }, + { "/user", CONFIGSET_USER, "Users and privilege settings" }, + { "/xfer", CONFIGSET_XFER, "Transfer setup", }, + { "/alias", CONFIGSET_ALIAS, "URL Aliases", }, + { "/subscriber", CONFIGSET_SCRIBER, "Email notification subscriber list" }, + { "/interwiki", CONFIGSET_IWIKI, "Inter-wiki link prefixes" }, + { "/all", CONFIGSET_ALL, "All of the above" }, +}; + + +/* +** The following is a list of settings that we are willing to +** transfer. +** +** Setting names that begin with an alphabetic characters refer to +** single entries in the CONFIG table. Setting names that begin with +** "@" are for special processing. +*/ +static struct { + const char *zName; /* Name of the configuration parameter */ + int groupMask; /* Which config groups is it part of */ +} aConfig[] = { + { "css", CONFIGSET_CSS }, + { "header", CONFIGSET_SKIN }, + { "mainmenu", CONFIGSET_SKIN }, + { "footer", CONFIGSET_SKIN }, + { "details", CONFIGSET_SKIN }, + { "js", CONFIGSET_SKIN }, + { "default-skin", CONFIGSET_SKIN }, + { "logo-mimetype", CONFIGSET_SKIN }, + { "logo-image", CONFIGSET_SKIN }, + { "background-mimetype", CONFIGSET_SKIN }, + { "background-image", CONFIGSET_SKIN }, + { "icon-mimetype", CONFIGSET_SKIN }, + { "icon-image", CONFIGSET_SKIN }, + { "timeline-date-format", CONFIGSET_SKIN }, + { "timeline-default-style", CONFIGSET_SKIN }, + { "timeline-dwelltime", CONFIGSET_SKIN }, + { "timeline-closetime", CONFIGSET_SKIN }, + { "timeline-hard-newlines", CONFIGSET_SKIN }, + { "timeline-max-comment", CONFIGSET_SKIN }, + { "timeline-plaintext", CONFIGSET_SKIN }, + { "timeline-truncate-at-blank", CONFIGSET_SKIN }, + { "timeline-tslink-info", CONFIGSET_SKIN }, + { "timeline-utc", CONFIGSET_SKIN }, + { "adunit", CONFIGSET_SKIN }, + { "adunit-omit-if-admin", CONFIGSET_SKIN }, + { "adunit-omit-if-user", CONFIGSET_SKIN }, + { "default-csp", CONFIGSET_SKIN }, + { "sitemap-extra", CONFIGSET_SKIN }, + { "safe-html", CONFIGSET_SKIN }, + +#ifdef FOSSIL_ENABLE_TH1_HOOKS + { "th1-hooks", CONFIGSET_TH1 }, +#endif + { "th1-uri-regexp", CONFIGSET_TH1 }, + + { "project-name", CONFIGSET_PROJ }, + { "short-project-name", CONFIGSET_PROJ }, + { "project-description", CONFIGSET_PROJ }, + { "index-page", CONFIGSET_PROJ }, + { "manifest", CONFIGSET_PROJ }, + { "binary-glob", CONFIGSET_PROJ }, + { "clean-glob", CONFIGSET_PROJ }, + { "ignore-glob", CONFIGSET_PROJ }, + { "keep-glob", CONFIGSET_PROJ }, + { "crlf-glob", CONFIGSET_PROJ }, + { "crnl-glob", CONFIGSET_PROJ }, + { "encoding-glob", CONFIGSET_PROJ }, + { "empty-dirs", CONFIGSET_PROJ }, + { "dotfiles", CONFIGSET_PROJ }, + { "parent-project-code", CONFIGSET_PROJ }, + { "parent-project-name", CONFIGSET_PROJ }, + { "hash-policy", CONFIGSET_PROJ }, + { "comment-format", CONFIGSET_PROJ }, + { "mimetypes", CONFIGSET_PROJ }, + { "forbid-delta-manifests", CONFIGSET_PROJ }, + { "mv-rm-files", CONFIGSET_PROJ }, + { "forum-statuses", CONFIGSET_PROJ }, + { "path-to-tag", CONFIGSET_PROJ }, + { "ticket-table", CONFIGSET_TKT }, + { "ticket-common", CONFIGSET_TKT }, + { "ticket-change", CONFIGSET_TKT }, + { "ticket-newpage", CONFIGSET_TKT }, + { "ticket-viewpage", CONFIGSET_TKT }, + { "ticket-editpage", CONFIGSET_TKT }, + { "ticket-reportlist", CONFIGSET_TKT }, + { "ticket-report-template", CONFIGSET_TKT }, + { "ticket-key-template", CONFIGSET_TKT }, + { "ticket-title-expr", CONFIGSET_TKT }, + { "ticket-closed-expr", CONFIGSET_TKT }, + { "@reportfmt", CONFIGSET_TKT }, + + { "@user", CONFIGSET_USER }, + { "user-color-map", CONFIGSET_USER }, + + { "@concealed", CONFIGSET_ADDR }, + + { "@shun", CONFIGSET_SHUN }, + + { "@alias", CONFIGSET_ALIAS }, + + { "@subscriber", CONFIGSET_SCRIBER }, + + { "@interwiki", CONFIGSET_IWIKI }, + + { "xfer-common-script", CONFIGSET_XFER }, + { "xfer-push-script", CONFIGSET_XFER }, + { "xfer-commit-script", CONFIGSET_XFER }, + { "xfer-ticket-script", CONFIGSET_XFER }, + +}; +static int iConfig = 0; + +/* +** Return name of first configuration property matching the given mask. +*/ +const char *configure_first_name(int iMask){ + iConfig = 0; + return configure_next_name(iMask); +} +const char *configure_next_name(int iMask){ + if( iConfig==0 && (iMask & CONFIGSET_ALL)==CONFIGSET_ALL ){ + iConfig = count(aGroupName); + return "/all"; + } + while( iConfig<count(aGroupName)-1 ){ + if( aGroupName[iConfig].groupMask & iMask ){ + return aGroupName[iConfig++].zName; + }else{ + iConfig++; + } + } + return 0; +} + +/* +** Return a pointer to a string that contains the RHS of an IN operator +** that will select CONFIG table names that are part of the configuration +** that matches iMatch. +*/ +const char *configure_inop_rhs(int iMask){ + Blob x; + int i; + const char *zSep = ""; + + blob_zero(&x); + blob_append_sql(&x, "("); + for(i=0; i<count(aConfig); i++){ + if( (aConfig[i].groupMask & iMask)==0 ) continue; + if( aConfig[i].zName[0]=='@' ) continue; + blob_append_sql(&x, "%s'%q'", zSep/*safe-for-%s*/, aConfig[i].zName); + zSep = ","; + } + blob_append_sql(&x, ")"); + return blob_sql_text(&x); +} + +/* +** Return the mask for the named configuration parameter if it can be +** safely exported. Return 0 if the parameter is not safe to export. +** +** "Safe" in the previous paragraph means the permission is granted to +** export the property. In other words, the requesting side has presented +** login credentials and has sufficient capabilities to access the requested +** information. +** +** Settings which are specifically flagged as sensitive will (as of +** 2024-10-15) cause this function to return 0, regardless of user +** permissions. As an example, if the th1-setup setting were not +** sensitive then a malicious repo admin could set that to include +** arbitrary TCL code and affect users who configure fossil with the +** --with-tcl flag. +*/ +int configure_is_exportable(const char *zName){ + int i; + int n = strlen(zName); + Setting *pSet; + if( n>2 && zName[0]=='\'' && zName[n-1]=='\'' ){ + char * zCpy; + zName++; + n -= 2; + zCpy = fossil_strndup(zName, (ssize_t)n); + pSet = db_find_setting(zCpy, 0); + fossil_free(zCpy); + }else{ + pSet = db_find_setting(zName, 0); + } + if( pSet && pSet->sensitive ){ + /* https://fossil-scm.org/forum/forumpost/6179500deadf6ec7 */ + return 0; + } + for(i=0; i<count(aConfig); i++){ + if( strncmp(zName, aConfig[i].zName, n)==0 && aConfig[i].zName[n]==0 ){ + int m = aConfig[i].groupMask; + if( !g.perm.Admin ){ + m &= ~(CONFIGSET_USER|CONFIGSET_SCRIBER); + } + if( !g.perm.RdAddr ){ + m &= ~CONFIGSET_ADDR; + } + return m; + } + } + if( strncmp(zName, "walias:/", 8)==0 ){ + return CONFIGSET_ALIAS; + } + if( strncmp(zName, "interwiki:", 10)==0 ){ + return CONFIGSET_IWIKI; + } + return 0; +} + +/* +** A mask of all configuration tables that have been reset already. +*/ +static int configHasBeenReset = 0; + +/* +** Mask of modified configuration sets +*/ +static int rebuildMask = 0; + +/* +** Rebuild auxiliary tables as required by configuration changes. +*/ +void configure_rebuild(void){ + if( rebuildMask & CONFIGSET_TKT ){ + ticket_rebuild(); + } + rebuildMask = 0; +} + +/* +** Return true if z[] is not a "safe" SQL token. A safe token is one of: +** +** * A string literal +** * A blob literal +** * An integer literal (no floating point) +** * NULL +*/ +static int safeSql(const char *z){ + int i; + if( z==0 || z[0]==0 ) return 0; + if( (z[0]=='x' || z[0]=='X') && z[1]=='\'' ) z++; + if( z[0]=='\'' ){ + for(i=1; z[i]; i++){ + if( z[i]=='\'' ){ + i++; + if( z[i]=='\'' ){ continue; } + return z[i]==0; + } + } + return 0; + }else{ + char c; + for(i=0; (c = z[i])!=0; i++){ + if( !fossil_isalnum(c) ) return 0; + } + } + return 1; +} + +/* +** Return true if z[] consists of nothing but digits +*/ +static int safeInt(const char *z){ + int i; + if( z==0 || z[0]==0 ) return 0; + for(i=0; fossil_isdigit(z[i]); i++){} + return z[i]==0; +} + +/* +** Process a single "config" card received from the other side of a +** sync session. +** +** Mask consists of one or more CONFIGSET_* values ORed together, to +** designate what types of configuration we are allowed to receive. +** +** NEW FORMAT: +** +** zName is one of: +** +** "/config", "/user", "/shun", "/reportfmt", "/concealed", +** "/subscriber", +** +** zName indicates the table that holds the configuration information being +** transferred. pContent is a string that consist of alternating Fossil +** and SQL tokens. The First token is a timestamp in seconds since 1970. +** The second token is a primary key for the table identified by zName. If +** The entry with the corresponding primary key exists and has a more recent +** mtime, then nothing happens. If the entry does not exist or if it has +** an older mtime, then the content described by subsequent token pairs is +** inserted. The first element of each token pair is a column name and +** the second is its value. +** +** In overview, we have: +** +** NAME CONTENT +** ------- ----------------------------------------------------------- +** /config $MTIME $NAME value $VALUE +** /user $MTIME $LOGIN pw $VALUE cap $VALUE info $VALUE photo $VALUE +** /shun $MTIME $UUID scom $VALUE +** /reportfmt $MTIME $TITLE owner $VALUE cols $VALUE sqlcode $VALUE jx $JSON +** /concealed $MTIME $HASH content $VALUE +** /subscriber $SMTIME $SEMAIL suname $V ... +** +** NAME-specific notes: +** +** - /reportftm's $MTIME is in Julian, not the Unix epoch. +*/ +void configure_receive(const char *zName, Blob *pContent, int groupMask){ + int checkMask; /* Masks for which we must first check existence of tables */ + + checkMask = CONFIGSET_SCRIBER; + if( zName[0]=='/' ){ + /* The new format */ + char *azToken[24]; + int nToken = 0; + int ii, jj; + int thisMask; + Blob name, value, sql; + static const struct receiveType { + const char *zName; /* Configuration key for this table */ + const char *zPrimKey; /* Primary key column */ + int nField; /* Number of data fields */ + const char *azField[6]; /* Names of the data fields */ + } aType[] = { + { "/config", "name", 1, { "value", 0,0,0,0,0 } }, + { "@user", "login", 5, { "pw","cap","info","photo","jx",0} }, + { "@shun", "uuid", 1, { "scom", 0,0,0,0,0} }, + { "@reportfmt", "title", 4, { "owner","cols","sqlcode","jx",0,0}}, + { "@concealed", "hash", 1, { "content", 0,0,0,0,0 } }, + { "@subscriber","semail",6, + { "suname","sdigest","sdonotcall","ssub","sctime","smip"} }, + }; + + /* Locate the receiveType in aType[ii] */ + for(ii=0; ii<count(aType); ii++){ + if( fossil_strcmp(&aType[ii].zName[1],&zName[1])==0 ) break; + } + if( ii>=count(aType) ) return; + + while( blob_token(pContent, &name) && blob_sqltoken(pContent, &value) ){ + char *z = blob_terminate(&name); + if( !safeSql(z) ) return; + if( nToken>0 ){ + for(jj=0; jj<aType[ii].nField; jj++){ + if( fossil_strcmp(aType[ii].azField[jj], z)==0 ) break; + } + if( jj>=aType[ii].nField ) continue; + }else{ + if( !safeInt(z) ) return; + } + azToken[nToken++] = z; + azToken[nToken++] = z = blob_terminate(&value); + if( !safeSql(z) ) return; + if( nToken>=count(azToken)-1 ) break; + } + if( nToken<2 ) return; + if( aType[ii].zName[0]=='/' ){ + thisMask = configure_is_exportable(azToken[1]); + if( 0==thisMask ){ + fossil_warning("Skipping non-exportable setting: %s = %s", + azToken[1], nToken>3 ? azToken[3] : "?"); + /* Will be skipped below */ + } + }else{ + thisMask = configure_is_exportable(aType[ii].zName); + } + if( (thisMask & groupMask)==0 ) return; + if( (thisMask & checkMask)!=0 ){ + if( (thisMask & CONFIGSET_SCRIBER)!=0 ){ + alert_schema(1); + } + checkMask &= ~thisMask; + } + + blob_zero(&sql); + if( groupMask & CONFIGSET_OVERWRITE ){ + if( (thisMask & configHasBeenReset)==0 && aType[ii].zName[0]!='/' ){ + db_multi_exec("DELETE FROM \"%w\"", &aType[ii].zName[1]); + configHasBeenReset |= thisMask; + } + blob_append_sql(&sql, "REPLACE INTO "); + }else{ + blob_append_sql(&sql, "INSERT OR IGNORE INTO "); + } + blob_append_sql(&sql, "\"%w\"(\"%w\",mtime", + &zName[1], aType[ii].zPrimKey); + if( fossil_stricmp(zName,"/subscriber")==0 ) alert_schema(0); + for(jj=2; jj<nToken; jj+=2){ + blob_append_sql(&sql, ",\"%w\"", azToken[jj]); + } + blob_append_sql(&sql,") VALUES(%s,%s", + azToken[1] /*safe-for-%s*/, azToken[0]/*safe-for-%s*/); + for(jj=2; jj<nToken; jj+=2){ + blob_append_sql(&sql, ",%s", azToken[jj+1] /*safe-for-%s*/); + } + db_protect_only(PROTECT_SENSITIVE); + + /* Make sure tables have the "jx" column */ + if( strcmp(&zName[1],"user")==0 ){ + user_update_user_table(); + }else if( strcmp(&zName[1],"reportfmt")==0 ){ + report_update_reportfmt_table(); + } + + db_multi_exec("%s)", blob_sql_text(&sql)); + + if( db_changes()==0 ){ + blob_reset(&sql); + blob_append_sql(&sql, "UPDATE \"%w\" SET mtime=%s", + &zName[1], azToken[0]/*safe-for-%s*/); + for(jj=2; jj<nToken; jj+=2){ + blob_append_sql(&sql, ", \"%w\"=%s", + azToken[jj], azToken[jj+1]/*safe-for-%s*/); + } + blob_append_sql(&sql, " WHERE \"%w\"=%s AND mtime<%s", + aType[ii].zPrimKey, azToken[1]/*safe-for-%s*/, + azToken[0]/*safe-for-%s*/); + db_multi_exec("%s", blob_sql_text(&sql)); + } + db_protect_pop(); + blob_reset(&sql); + rebuildMask |= thisMask; + } +} + +/* +** Process a file full of "config" cards. +*/ +void configure_receive_all(Blob *pIn, int groupMask){ + Blob line; + int nToken; + int size; + Blob aToken[4]; + + configHasBeenReset = 0; + while( blob_line(pIn, &line) ){ + if( blob_buffer(&line)[0]=='#' ) continue; + nToken = blob_tokenize(&line, aToken, count(aToken)); + if( blob_eq(&aToken[0],"config") + && nToken==3 + && blob_is_int(&aToken[2], &size) + ){ + const char *zName = blob_str(&aToken[1]); + Blob content; + blob_zero(&content); + blob_extract(pIn, size, &content); + g.perm.Admin = g.perm.RdAddr = 1; + configure_receive(zName, &content, groupMask); + blob_reset(&content); + blob_seek(pIn, 1, BLOB_SEEK_CUR); + } + } +} + + +/* +** Send "config" cards using the new format for all elements of a group +** that have recently changed. +** +** Output goes into pOut. The groupMask identifies the group(s) to be sent. +** Send only entries whose timestamp is later than or equal to iStart. +** +** Return the number of cards sent. +*/ +int configure_send_group( + Blob *pOut, /* Write output here */ + int groupMask, /* Mask of groups to be send */ + sqlite3_int64 iStart /* Only write values changed since this time */ +){ + Stmt q; + Blob rec; + int ii; + int nCard = 0; + + blob_zero(&rec); + if( groupMask & CONFIGSET_SHUN ){ + db_prepare(&q, "SELECT mtime, quote(uuid), quote(scom) FROM shun" + " WHERE mtime>=%lld", iStart); + while( db_step(&q)==SQLITE_ROW ){ + blob_appendf(&rec,"%s %s scom %s", + db_column_text(&q, 0), + db_column_text(&q, 1), + db_column_text(&q, 2) + ); + blob_appendf(pOut, "config /shun %d\n%s\n", + blob_size(&rec), blob_str(&rec)); + nCard++; + blob_reset(&rec); + } + db_finalize(&q); + } + if( groupMask & CONFIGSET_USER ){ + if( db_table_has_column("repository","user","jx") ){ + db_prepare(&q, "SELECT mtime, quote(login), quote(pw), quote(cap)," + " quote(info), quote(photo), quote(jx) FROM user" + " WHERE mtime>=%lld", iStart); + }else{ + db_prepare(&q, "SELECT mtime, quote(login), quote(pw), quote(cap)," + " quote(info), quote(photo), 'NULL' FROM user" + " WHERE mtime>=%lld", iStart); + } + while( db_step(&q)==SQLITE_ROW ){ + const char *z; + blob_appendf(&rec,"%s %s", db_column_text(&q,0), db_column_text(&q,1)); + z = db_column_text(&q,2); + if( strcmp(z,"NULL")!=0 ) blob_appendf(&rec," pw %s", z); + z = db_column_text(&q,3); + if( strcmp(z,"NULL")!=0 ) blob_appendf(&rec," cap %s", z); + z = db_column_text(&q,4); + if( strcmp(z,"NULL")!=0 ) blob_appendf(&rec," info %s", z); + z = db_column_text(&q,5); + if( strcmp(z,"NULL")!=0 ) blob_appendf(&rec," photo %s", z); + z = db_column_text(&q,6); + if( strcmp(z,"NULL")!=0 ) blob_appendf(&rec," jx %s", z); + blob_appendf(pOut, "config /user %d\n%s\n", + blob_size(&rec), blob_str(&rec)); + nCard++; + blob_reset(&rec); + } + db_finalize(&q); + } + if( groupMask & CONFIGSET_TKT ){ + if( db_table_has_column("repository","reportfmt","jx") ){ + db_prepare(&q, "SELECT mtime, quote(title), quote(owner), quote(cols)," + " quote(sqlcode), quote(jx) FROM reportfmt" + " WHERE mtime>=%lld", iStart); + }else{ + db_prepare(&q, "SELECT mtime, quote(title), quote(owner), quote(cols)," + " quote(sqlcode), 'NULL' FROM reportfmt" + " WHERE mtime>=%lld", iStart); + } + while( db_step(&q)==SQLITE_ROW ){ + const char *z; + blob_appendf(&rec,"%s %s", db_column_text(&q,0), db_column_text(&q,1)); + z = db_column_text(&q,2); + if( strcmp(z,"NULL")!=0 ) blob_appendf(&rec," owner %s", z); + z = db_column_text(&q,3); + if( strcmp(z,"NULL")!=0 ) blob_appendf(&rec," cols %s", z); + z = db_column_text(&q,4); + if( strcmp(z,"NULL")!=0 ) blob_appendf(&rec," sqlcode %s", z); + z = db_column_text(&q,5); + if( strcmp(z,"NULL")!=0 ) blob_appendf(&rec," jx %s", z); + blob_appendf(pOut, "config /reportfmt %d\n%s\n", + blob_size(&rec), blob_str(&rec)); + nCard++; + blob_reset(&rec); + } + db_finalize(&q); + } + if( groupMask & CONFIGSET_ADDR ){ + db_prepare(&q, "SELECT mtime, quote(hash), quote(content) FROM concealed" + " WHERE mtime>=%lld", iStart); + while( db_step(&q)==SQLITE_ROW ){ + blob_appendf(&rec,"%s %s content %s", + db_column_text(&q, 0), + db_column_text(&q, 1), + db_column_text(&q, 2) + ); + blob_appendf(pOut, "config /concealed %d\n%s\n", + blob_size(&rec), blob_str(&rec)); + nCard++; + blob_reset(&rec); + } + db_finalize(&q); + } + if( groupMask & CONFIGSET_ALIAS ){ + db_prepare(&q, "SELECT mtime, quote(name), quote(value) FROM config" + " WHERE name GLOB 'walias:/*' AND mtime>=%lld", iStart); + while( db_step(&q)==SQLITE_ROW ){ + blob_appendf(&rec,"%s %s value %s", + db_column_text(&q, 0), + db_column_text(&q, 1), + db_column_text(&q, 2) + ); + blob_appendf(pOut, "config /config %d\n%s\n", + blob_size(&rec), blob_str(&rec)); + nCard++; + blob_reset(&rec); + } + db_finalize(&q); + } + if( groupMask & CONFIGSET_IWIKI ){ + db_prepare(&q, "SELECT mtime, quote(name), quote(value) FROM config" + " WHERE name GLOB 'interwiki:*' AND mtime>=%lld", iStart); + while( db_step(&q)==SQLITE_ROW ){ + blob_appendf(&rec,"%s %s value %s", + db_column_text(&q, 0), + db_column_text(&q, 1), + db_column_text(&q, 2) + ); + blob_appendf(pOut, "config /config %d\n%s\n", + blob_size(&rec), blob_str(&rec)); + nCard++; + blob_reset(&rec); + } + db_finalize(&q); + } + if( (groupMask & CONFIGSET_SCRIBER)!=0 + && db_table_exists("repository","subscriber") + ){ + db_prepare(&q, "SELECT mtime, quote(semail)," + " quote(suname), quote(sdigest)," + " quote(sdonotcall), quote(ssub)," + " quote(sctime), quote(smip)" + " FROM subscriber WHERE sverified" + " AND mtime>=%lld", iStart); + while( db_step(&q)==SQLITE_ROW ){ + blob_appendf(&rec, + "%lld %s suname %s sdigest %s sdonotcall %s ssub %s" + " sctime %s smip %s", + db_column_int64(&q, 0), /* mtime */ + db_column_text(&q, 1), /* semail (PK) */ + db_column_text(&q, 2), /* suname */ + db_column_text(&q, 3), /* sdigest */ + db_column_text(&q, 4), /* sdonotcall */ + db_column_text(&q, 5), /* ssub */ + db_column_text(&q, 6), /* sctime */ + db_column_text(&q, 7) /* smip */ + ); + blob_appendf(pOut, "config /subscriber %d\n%s\n", + blob_size(&rec), blob_str(&rec)); + nCard++; + blob_reset(&rec); + } + db_finalize(&q); + } + db_prepare(&q, "SELECT mtime, quote(name), quote(value) FROM config" + " WHERE name=:name AND mtime>=%lld", iStart); + for(ii=0; ii<count(aConfig); ii++){ + if( (aConfig[ii].groupMask & groupMask)!=0 && aConfig[ii].zName[0]!='@' ){ + const Setting * pSet = db_find_setting(aConfig[ii].zName, 0); + if( pSet && pSet->sensitive ){ + /* https://fossil-scm.org/forum/forumpost/6179500deadf6ec7 */ + continue; + } + db_bind_text(&q, ":name", aConfig[ii].zName); + while( db_step(&q)==SQLITE_ROW ){ + blob_appendf(&rec,"%s %s value %s", + db_column_text(&q, 0), + db_column_text(&q, 1), + db_column_text(&q, 2) + ); + blob_appendf(pOut, "config /config %d\n%s\n", + blob_size(&rec), blob_str(&rec)); + nCard++; + blob_reset(&rec); + } + db_reset(&q); + } + } + db_finalize(&q); + return nCard; +} + +/* +** Identify a configuration group by name. Return its mask. +** Throw an error if no match. +*/ +int configure_name_to_mask(const char *z, int notFoundIsFatal){ + int i; + int n = strlen(z); + for(i=0; i<count(aGroupName); i++){ + if( strncmp(z, &aGroupName[i].zName[1], n)==0 ){ + return aGroupName[i].groupMask; + } + } + if( notFoundIsFatal ){ + fossil_print("Available configuration areas:\n"); + for(i=0; i<count(aGroupName); i++){ + fossil_print(" %-13s %s\n", + &aGroupName[i].zName[1], aGroupName[i].zHelp); + } + fossil_fatal("no such configuration area: \"%s\"", z); + } + return 0; +} + +/* +** Write SQL text into file zFilename that will restore the configuration +** area identified by mask to its current state from any other state. +*/ +static void export_config( + int groupMask, /* Mask indicating which configuration to export */ + const char *zMask, /* Name of the configuration */ + sqlite3_int64 iStart, /* Start date */ + const char *zFilename /* Write into this file */ +){ + Blob out; + blob_zero(&out); + blob_appendf(&out, + "# The \"%s\" configuration exported from\n" + "# repository \"%s\"\n" + "# on %s\n", + zMask, g.zRepositoryName, + db_text(0, "SELECT datetime('now')") + ); + configure_send_group(&out, groupMask, iStart); + blob_write_to_file(&out, zFilename); + blob_reset(&out); +} + + +/* +** COMMAND: configuration* +** +** Usage: %fossil configuration METHOD ... ?OPTIONS? +** +** Where METHOD is one of: export import merge pull push reset. +** +** > fossil configuration export AREA FILENAME +** +** Write to FILENAME exported configuration information for AREA. +** AREA can be one of: +** +** all email interwiki project shun skin +** ticket user alias subscriber +** +** > fossil configuration import FILENAME +** +** Read a configuration from FILENAME, overwriting the current +** configuration. +** +** > fossil configuration merge FILENAME +** +** Read a configuration from FILENAME and merge its values into +** the current configuration. Existing values take priority over +** values read from FILENAME. +** +** > fossil configuration pull AREA ?URL? +** +** Pull and install the configuration from a different server +** identified by URL. If no URL is specified, then the default +** server is used. Use the --overwrite flag to completely +** replace local settings with content received from URL. +** +** > fossil configuration push AREA ?URL? +** +** Push the local configuration into the remote server identified +** by URL. Admin privilege is required on the remote server for +** this to work. When the same record exists both locally and on +** the remote end, the one that was most recently changed wins. +** +** > fossil configuration reset AREA +** +** Restore the configuration to the default. AREA as above. +** +** > fossil configuration sync AREA ?URL? +** +** Synchronize configuration changes in the local repository with +** the remote repository at URL. +** +** Options: +** --proxy PROXY Use PROXY as http proxy during sync operation +** (used by pull, push and sync subcommands) +** -R|--repository REPO Affect repository REPO with changes +** +** See also: [[settings]], [[unset]] +*/ +void configuration_cmd(void){ + int n; + const char *zMethod; + db_find_and_open_repository(0, 0); + db_open_config(0, 0); + if( g.argc<3 ){ + usage("SUBCOMMAND ..."); + } + zMethod = g.argv[2]; + n = strlen(zMethod); + if( strncmp(zMethod, "export", n)==0 ){ + int mask; + const char *zSince = find_option("since",0,1); + sqlite3_int64 iStart; + if( g.argc!=5 ){ + usage("export AREA FILENAME"); + } + mask = configure_name_to_mask(g.argv[3], 1); + if( zSince ){ + iStart = db_multi_exec( + "SELECT coalesce(strftime('%%s',%Q),strftime('%%s','now',%Q))+0", + zSince, zSince + ); + }else{ + iStart = 0; + } + export_config(mask, g.argv[3], iStart, g.argv[4]); + }else + if( strncmp(zMethod, "import", n)==0 + || strncmp(zMethod, "merge", n)==0 ){ + Blob in; + int groupMask; + if( g.argc!=4 ) usage(mprintf("%s FILENAME",zMethod)); + blob_read_from_file(&in, g.argv[3], ExtFILE); + db_begin_transaction(); + if( zMethod[0]=='i' ){ + groupMask = CONFIGSET_ALL | CONFIGSET_OVERWRITE; + }else{ + groupMask = CONFIGSET_ALL; + } + db_unprotect(PROTECT_USER); + configure_receive_all(&in, groupMask); + db_protect_pop(); + db_end_transaction(0); + }else + if( strncmp(zMethod, "pull", n)==0 + || strncmp(zMethod, "push", n)==0 + || strncmp(zMethod, "sync", n)==0 + ){ + int mask; + const char *zServer = 0; + int overwriteFlag = 0; + + if( strncmp(zMethod,"pull",n)==0 ){ + overwriteFlag = find_option("overwrite",0,0)!=0; + } + url_proxy_options(); + if( g.argc!=4 && g.argc!=5 ){ + usage(mprintf("%s AREA ?URL?", zMethod)); + } + mask = configure_name_to_mask(g.argv[3], 1); + if( g.argc==5 ){ + zServer = g.argv[4]; + } + url_parse(zServer, URL_PROMPT_PW|URL_USE_CONFIG); + if( g.url.protocol==0 ) fossil_fatal("no server URL specified"); + user_select(); + url_enable_proxy("via proxy: "); + g.zHttpAuth = get_httpauth(); + if( overwriteFlag ) mask |= CONFIGSET_OVERWRITE; + if( strncmp(zMethod, "push", n)==0 ){ + client_sync(0,0,(unsigned)mask,0,0); + }else if( strncmp(zMethod, "pull", n)==0 ){ + if( overwriteFlag ) db_unprotect(PROTECT_USER); + client_sync(0,(unsigned)mask,0,0,0); + if( overwriteFlag ) db_protect_pop(); + }else{ + client_sync(0,(unsigned)mask,(unsigned)mask,0,0); + } + }else + if( strncmp(zMethod, "reset", n)==0 ){ + int mask, i; + char *zBackup; + if( g.argc!=4 ) usage("reset AREA"); + mask = configure_name_to_mask(g.argv[3], 1); + zBackup = db_text(0, + "SELECT strftime('config-backup-%%Y%%m%%d%%H%%M%%f','now')"); + db_begin_transaction(); + export_config(mask, g.argv[3], 0, zBackup); + for(i=0; i<count(aConfig); i++){ + const char *zName = aConfig[i].zName; + if( (aConfig[i].groupMask & mask)==0 ) continue; + if( zName[0]!='@' ){ + db_unprotect(PROTECT_CONFIG); + db_multi_exec("DELETE FROM config WHERE name=%Q", zName); + db_protect_pop(); + }else if( fossil_strcmp(zName,"@user")==0 ){ + db_unprotect(PROTECT_USER); + db_multi_exec("DELETE FROM user"); + db_protect_pop(); + db_create_default_users(0, 0); + }else if( fossil_strcmp(zName,"@concealed")==0 ){ + db_multi_exec("DELETE FROM concealed"); + }else if( fossil_strcmp(zName,"@shun")==0 ){ + db_multi_exec("DELETE FROM shun"); + }else if( fossil_strcmp(zName,"@subscriber")==0 ){ + if( db_table_exists("repository","subscriber") ){ + db_multi_exec("DELETE FROM subscriber"); + } + }else if( fossil_strcmp(zName,"@forum")==0 ){ + if( db_table_exists("repository","forumpost") ){ + db_multi_exec("DELETE FROM forumpost"); + db_multi_exec("DELETE FROM forumthread"); + } + }else if( fossil_strcmp(zName,"@reportfmt")==0 ){ + db_multi_exec("DELETE FROM reportfmt"); + assert( strchr(zRepositorySchemaDefaultReports,'%')==0 ); + db_multi_exec(zRepositorySchemaDefaultReports /*works-like:""*/); + } + } + db_end_transaction(0); + fossil_print("Configuration reset to factory defaults.\n"); + fossil_print("To recover, use: %s %s import %s\n", + g.argv[0], g.argv[1], zBackup); + rebuildMask |= mask; + }else + { + fossil_fatal("METHOD should be one of:" + " export import merge pull push reset"); + } + configure_rebuild(); +} + + +/* +** COMMAND: test-var-list +** +** Usage: %fossil test-var-list ?PATTERN? ?--unset? ?--mtime? +** +** Show the content of the CONFIG table in a repository. If PATTERN is +** specified, then only show the entries that match that glob pattern. +** Last modification time is shown if the --mtime option is present. +** +** If the --unset option is included, then entries are deleted rather than +** being displayed. WARNING! This cannot be undone. Be sure you know what +** you are doing! The --unset option only works if there is a PATTERN. +** Probably you should run the command once without --unset to make sure +** you know exactly what is being deleted. +** +** If not in an open check-out, use the -R REPO option to specify a +** a repository. +*/ +void test_var_list_cmd(void){ + Stmt q; + int i, j; + const char *zPattern = 0; + int doUnset; + int showMtime; + Blob sql; + Blob ans; + unsigned char zTrans[1000]; + + doUnset = find_option("unset",0,0)!=0; + showMtime = find_option("mtime",0,0)!=0; + db_find_and_open_repository(OPEN_ANY_SCHEMA, 0); + verify_all_options(); + if( g.argc>=3 ){ + zPattern = g.argv[2]; + } + blob_init(&sql,0,0); + blob_appendf(&sql, "SELECT name, value, datetime(mtime,'unixepoch')" + " FROM config"); + if( zPattern ){ + blob_appendf(&sql, " WHERE name GLOB %Q", zPattern); + } + if( showMtime ){ + blob_appendf(&sql, " ORDER BY mtime, name"); + }else{ + blob_appendf(&sql, " ORDER BY name"); + } + db_prepare(&q, "%s", blob_str(&sql)/*safe-for-%s*/); + blob_reset(&sql); +#define MX_VAL 40 +#define MX_NM 28 +#define MX_LONGNM 60 + while( db_step(&q)==SQLITE_ROW ){ + const char *zName = db_column_text(&q,0); + int nName = db_column_bytes(&q,0); + const char *zValue = db_column_text(&q,1); + int szValue = db_column_bytes(&q,1); + const char *zMTime = db_column_text(&q,2); + for(i=j=0; j<MX_VAL && zValue[i]; i++){ + unsigned char c = (unsigned char)zValue[i]; + if( c>=' ' && c<='~' ){ + zTrans[j++] = c; + }else{ + zTrans[j++] = '\\'; + if( c=='\n' ){ + zTrans[j++] = 'n'; + }else if( c=='\r' ){ + zTrans[j++] = 'r'; + }else if( c=='\t' ){ + zTrans[j++] = 't'; + }else{ + zTrans[j++] = '0' + ((c>>6)&7); + zTrans[j++] = '0' + ((c>>3)&7); + zTrans[j++] = '0' + (c&7); + } + } + } + zTrans[j] = 0; + if( i<szValue ){ + sqlite3_snprintf(sizeof(zTrans)-j, (char*)zTrans+j, "...+%d", szValue-i); + j += (int)strlen((char*)zTrans+j); + } + if( showMtime ){ + fossil_print("%s:%*s%s\n", zName, 58-nName, "", zMTime); + }else if( nName<MX_NM-2 ){ + fossil_print("%s:%*s%s\n", zName, MX_NM-1-nName, "", zTrans); + }else if( nName<MX_LONGNM-2 && j<10 ){ + fossil_print("%s:%*s%s\n", zName, MX_LONGNM-1-nName, "", zTrans); + }else{ + fossil_print("%s:\n%*s%s\n", zName, MX_NM, "", zTrans); + } + } + db_finalize(&q); + if( zPattern && doUnset ){ + prompt_user("Delete all of the above? (y/N)? ", &ans); + if( blob_str(&ans)[0]=='y' || blob_str(&ans)[0]=='Y' ){ + db_multi_exec("DELETE FROM config WHERE name GLOB %Q", zPattern); + } + blob_reset(&ans); + } +} + +/* +** COMMAND: test-var-get +** +** Usage: %fossil test-var-get VAR ?FILE? +** +** Write the text of the VAR variable into FILE. If FILE is "-" +** or is omitted then output goes to standard output. VAR can be a +** GLOB pattern. +** +** If not in an open check-out, use the -R REPO option to specify a +** a repository. +*/ +void test_var_get_cmd(void){ + const char *zVar; + const char *zFile; + int n; + Blob x; + db_find_and_open_repository(OPEN_ANY_SCHEMA, 0); + verify_all_options(); + if( g.argc<3 ){ + usage("VAR ?FILE?"); + } + zVar = g.argv[2]; + zFile = g.argc>=4 ? g.argv[3] : "-"; + n = db_int(0, "SELECT count(*) FROM config WHERE name GLOB %Q", zVar); + if( n==0 ){ + fossil_fatal("no match for %Q", zVar); + } + if( n>1 ){ + fossil_fatal("multiple matches: %s", + db_text(0, "SELECT group_concat(quote(name),', ') FROM (" + " SELECT name FROM config WHERE name GLOB %Q ORDER BY 1)", + zVar)); + } + blob_init(&x,0,0); + db_blob(&x, "SELECT value FROM config WHERE name GLOB %Q", zVar); + blob_write_to_file(&x, zFile); +} + +/* +** COMMAND: test-var-set +** +** Usage: %fossil test-var-set VAR ?VALUE? ?--file FILE? +** +** Store VALUE or the content of FILE (exactly one of which must be +** supplied) into variable VAR. Use a FILE of "-" to read from +** standard input. +** +** WARNING: changing the value of a variable can interfere with the +** operation of Fossil. Be sure you know what you are doing. +** +** Use "--blob FILE" instead of "--file FILE" to load a binary blob +** such as a GIF. +*/ +void test_var_set_cmd(void){ + const char *zVar; + const char *zFile; + const char *zBlob; + Blob x; + Stmt ins; + zFile = find_option("file",0,1); + zBlob = find_option("blob",0,1); + db_find_and_open_repository(OPEN_ANY_SCHEMA, 0); + verify_all_options(); + if( g.argc<3 || (zFile==0 && zBlob==0 && g.argc<4) ){ + usage("VAR ?VALUE? ?--file FILE?"); + } + zVar = g.argv[2]; + if( zFile ){ + if( zBlob ) fossil_fatal("cannot do both --file or --blob"); + blob_read_from_file(&x, zFile, ExtFILE); + }else if( zBlob ){ + blob_read_from_file(&x, zBlob, ExtFILE); + }else{ + blob_init(&x,g.argv[3],-1); + } + db_unprotect(PROTECT_CONFIG); + db_prepare(&ins, + "REPLACE INTO config(name,value,mtime)" + "VALUES(%Q,:val,now())", zVar); + if( zBlob ){ + db_bind_blob(&ins, ":val", &x); + }else{ + db_bind_text(&ins, ":val", blob_str(&x)); + } + db_step(&ins); + db_finalize(&ins); + db_protect_pop(); + blob_reset(&x); +} |
