summaryrefslogtreecommitdiff
path: root/src/configure.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/configure.c')
-rw-r--r--src/configure.c1157
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);
+}