Index: src/checkin.c ================================================================== --- src/checkin.c +++ src/checkin.c @@ -276,15 +276,15 @@ int dotfilesFlag; const char *zIgnoreFlag; Blob path, repo; Stmt q; int n; + Glob *pIgnore; + allFlag = find_option("force","f",0)!=0; dotfilesFlag = find_option("dotfiles",0,0)!=0; zIgnoreFlag = find_option("ignore",0,1); - Glob *pIgnore; - db_must_be_within_tree(); if( zIgnoreFlag==0 ){ zIgnoreFlag = db_get("ignore-glob", 0); } db_multi_exec("CREATE TEMP TABLE sfile(x TEXT PRIMARY KEY)"); Index: src/clone.c ================================================================== --- src/clone.c +++ src/clone.c @@ -64,14 +64,14 @@ file_copy(g.urlName, g.argv[3]); db_close(1); db_open_repository(g.argv[3]); db_record_repository_filename(g.argv[3]); db_multi_exec( - "REPLACE INTO config(name,value)" - " VALUES('server-code', lower(hex(randomblob(20))));" - "REPLACE INTO config(name,value)" - " VALUES('last-sync-url', '%q');", + "REPLACE INTO config(name,value,mtime)" + " VALUES('server-code', lower(hex(randomblob(20))),now());" + "REPLACE INTO config(name,value,mtime)" + " VALUES('last-sync-url', '%q',now());", g.urlCanonical ); db_multi_exec( "DELETE FROM blob WHERE rid IN private;" "DELETE FROM delta wHERE rid IN private;" @@ -92,12 +92,12 @@ user_select(); db_set("content-schema", CONTENT_SCHEMA, 0); db_set("aux-schema", AUX_SCHEMA, 0); db_set("last-sync-url", g.argv[2], 0); db_multi_exec( - "REPLACE INTO config(name,value)" - " VALUES('server-code', lower(hex(randomblob(20))));" + "REPLACE INTO config(name,value,mtime)" + " VALUES('server-code', lower(hex(randomblob(20))), now());" ); url_enable_proxy(0); url_get_password_if_needed(); g.xlinkClusterOnly = 1; nErr = client_sync(0,0,1,bPrivate,CONFIGSET_ALL,0); Index: src/configure.c ================================================================== --- src/configure.c +++ src/configure.c @@ -2,11 +2,11 @@ ** 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: @@ -27,18 +27,21 @@ #if INTERFACE /* ** Configuration transfers occur in groups. These are the allowed ** groupings: */ -#define CONFIGSET_SKIN 0x000001 /* WWW interface appearance */ -#define CONFIGSET_TKT 0x000002 /* Ticket configuration */ -#define CONFIGSET_PROJ 0x000004 /* Project name */ -#define CONFIGSET_SHUN 0x000008 /* Shun settings */ -#define CONFIGSET_USER 0x000010 /* The USER table */ -#define CONFIGSET_ADDR 0x000020 /* The CONCEALED table */ +#define CONFIGSET_SKIN 0x000001 /* WWW interface appearance */ +#define CONFIGSET_TKT 0x000002 /* Ticket configuration */ +#define CONFIGSET_PROJ 0x000004 /* Project name */ +#define CONFIGSET_SHUN 0x000008 /* Shun settings */ +#define CONFIGSET_USER 0x000010 /* The USER table */ +#define CONFIGSET_ADDR 0x000020 /* The CONCEALED table */ -#define CONFIGSET_ALL 0xffffff /* Everything */ +#define CONFIGSET_ALL 0x0000ff /* Everything */ + +#define CONFIGSET_OVERWRITE 0x100000 /* Causes overwrite instead of merge */ +#define CONFIGSET_OLDFORMAT 0x200000 /* Use the legacy format */ #endif /* INTERFACE */ /* ** Names of the configuration sets @@ -46,17 +49,17 @@ 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, "Web interface apparance settings" }, - { "shun", CONFIGSET_SHUN, "List of shunned artifacts" }, - { "ticket", CONFIGSET_TKT, "Ticket setup", }, - { "user", CONFIGSET_USER, "Users and privilege settings" }, - { "all", CONFIGSET_ALL, "All of the above" }, + { "/email", CONFIGSET_ADDR, "Concealed email addresses in tickets" }, + { "/project", CONFIGSET_PROJ, "Project name and description" }, + { "/skin", CONFIGSET_SKIN, "Web interface apparance settings" }, + { "/shun", CONFIGSET_SHUN, "List of shunned artifacts" }, + { "/ticket", CONFIGSET_TKT, "Ticket setup", }, + { "/user", CONFIGSET_USER, "Users and privilege settings" }, + { "/all", CONFIGSET_ALL, "All of the above" }, }; /* ** The following is a list of settings that we are willing to @@ -106,15 +109,29 @@ const char *configure_first_name(int iMask){ iConfig = 0; return configure_next_name(iMask); } const char *configure_next_name(int iMask){ - while( iConfig<count(aConfig) ){ - if( aConfig[iConfig].groupMask & iMask ){ - return aConfig[iConfig++].zName; - }else{ - iConfig++; + if( iMask & CONFIGSET_OLDFORMAT ){ + while( iConfig<count(aConfig) ){ + if( aConfig[iConfig].groupMask & iMask ){ + return aConfig[iConfig++].zName; + }else{ + iConfig++; + } + } + }else{ + 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; } @@ -127,12 +144,17 @@ ** login credentials and has sufficient capabilities to access the requested ** information. */ int configure_is_exportable(const char *zName){ int i; + int n = strlen(zName); + if( n>2 && zName[0]=='\'' && zName[n-1]=='\'' ){ + zName++; + n -= 2; + } for(i=0; i<count(aConfig); i++){ - if( fossil_strcmp(zName, aConfig[i].zName)==0 ){ + if( memcmp(zName, aConfig[i].zName, n)==0 && aConfig[i].zName[n]==0 ){ int m = aConfig[i].groupMask; if( !g.okAdmin ){ m &= ~CONFIGSET_USER; } if( !g.okRdAddr ){ @@ -200,38 +222,39 @@ } /* ** Two SQL functions: ** -** flag_test(int) -** flag_clear(int) +** config_is_reset(int) +** config_reset(int) ** -** The flag_test() function takes the integer valued argument and -** ANDs it against the static variable "flag_value" below. The -** function returns TRUE or false depending on the result. The -** flag_clear() function masks off the bits from "flag_value" that +** The config_is_reset() function takes the integer valued argument and +** ANDs it against the static variable "configHasBeenReset" below. The +** function returns TRUE or FALSE depending on the result depending on +** whether or not the corresponding configuration table has been reset. The +** config_reset() function adds the bits to "configHasBeenReset" that ** are given in the argument. ** ** These functions are used below in the WHEN clause of a trigger to ** get the trigger to fire exactly once. */ -static int flag_value = 0xffff; -static void flag_test_function( +static int configHasBeenReset = 0; +static void config_is_reset_function( sqlite3_context *context, int argc, sqlite3_value **argv ){ int m = sqlite3_value_int(argv[0]); - sqlite3_result_int(context, (flag_value&m)!=0 ); + sqlite3_result_int(context, (configHasBeenReset&m)!=0 ); } -static void flag_clear_function( +static void config_reset_function( sqlite3_context *context, int argc, sqlite3_value **argv ){ int m = sqlite3_value_int(argv[0]); - flag_value &= ~m; + configHasBeenReset |= m; } /* ** Create the temporary _xfer_reportfmt and _xfer_user tables that are ** necessary in order to evalute the SQL text generated by the @@ -261,12 +284,14 @@ @ ipaddr TEXT, -- IP address for which cookie is valid @ cexpire DATETIME, -- Time when cookie expires @ info TEXT, -- contact information @ photo BLOB -- JPEG image of this user @ ); - @ INSERT INTO _xfer_reportfmt SELECT * FROM reportfmt; - @ INSERT INTO _xfer_user SELECT * FROM user; + @ INSERT INTO _xfer_reportfmt + @ SELECT rn,owner,title,cols,sqlcode FROM reportfmt; + @ INSERT INTO _xfer_user + @ SELECT uid,login,pw,cap,cookie,ipaddr,cexpire,info,photo FROM user; ; db_multi_exec(zSQL1); /* When the replace flag is set, add triggers that run the first time ** that new data is seen. The triggers run only once and delete all the @@ -273,33 +298,89 @@ ** existing data. */ if( replaceFlag ){ static const char zSQL2[] = @ CREATE TRIGGER _xfer_r1 BEFORE INSERT ON _xfer_reportfmt - @ WHEN flag_test(1) BEGIN + @ WHEN NOT config_is_reset(2) BEGIN @ DELETE FROM _xfer_reportfmt; - @ SELECT flag_clear(1); + @ SELECT config_reset(2); @ END; @ CREATE TRIGGER _xfer_r2 BEFORE INSERT ON _xfer_user - @ WHEN flag_test(2) BEGIN + @ WHEN NOT config_is_reset(16) BEGIN @ DELETE FROM _xfer_user; - @ SELECT flag_clear(2); + @ SELECT config_reset(16); @ END; @ CREATE TEMP TRIGGER _xfer_r3 BEFORE INSERT ON shun - @ WHEN flag_test(4) BEGIN + @ WHEN NOT config_is_reset(8) BEGIN @ DELETE FROM shun; - @ SELECT flag_clear(4); + @ SELECT config_reset(8); @ END; ; - sqlite3_create_function(g.db, "flag_test", 1, SQLITE_UTF8, 0, - flag_test_function, 0, 0); - sqlite3_create_function(g.db, "flag_clear", 1, SQLITE_UTF8, 0, - flag_clear_function, 0, 0); - flag_value = 0xffff; + sqlite3_create_function(g.db, "config_is_reset", 1, SQLITE_UTF8, 0, + config_is_reset_function, 0, 0); + sqlite3_create_function(g.db, "config_reset", 1, SQLITE_UTF8, 0, + config_reset_function, 0, 0); + configHasBeenReset = 0; db_multi_exec(zSQL2); } } + +/* +** After receiving configuration data, call this routine to transfer +** the results into the main database. +*/ +void configure_finalize_receive(void){ + static const char zSQL[] = + @ DELETE FROM user; + @ INSERT INTO user SELECT * FROM _xfer_user; + @ DELETE FROM reportfmt; + @ INSERT INTO reportfmt SELECT * FROM _xfer_reportfmt; + @ DROP TABLE _xfer_user; + @ DROP TABLE _xfer_reportfmt; + ; + db_multi_exec(zSQL); +} + +/* +** 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. ** @@ -344,108 +425,302 @@ ** table like _fer_reportfmt or _xfer_user. Such tables must be created ** ahead of time using configure_prepare_to_receive(). Then after multiple ** calls to this routine, configure_finalize_receive() to transfer the ** information received into the true target table. */ -void configure_receive(const char *zName, Blob *pContent, int mask){ - if( (configure_is_exportable(zName) & mask)==0 ) return; - if( strcmp(zName, "logo-image")==0 ){ - Stmt ins; - db_prepare(&ins, - "REPLACE INTO config(name, value) VALUES(:name, :value)" - ); - db_bind_text(&ins, ":name", zName); - db_bind_blob(&ins, ":value", pContent); - db_step(&ins); - db_finalize(&ins); - }else if( zName[0]=='@' ){ - /* Notice that we are evaluating arbitrary SQL received from the - ** client. But this can only happen if the client has authenticated - ** as an administrator, so presumably we trust the client at this - ** point. - */ - db_multi_exec("%s", blob_str(pContent)); +void configure_receive(const char *zName, Blob *pContent, int groupMask){ + if( zName[0]=='/' ){ + /* The new format */ + char *azToken[12]; + int nToken = 0; + int ii, jj; + int thisMask; + Blob name, value, sql; + static const struct receiveType { + const char *zName; + const char *zPrimKey; + int nField; + const char *azField[4]; + } aType[] = { + { "/config", "name", 1, { "value", 0, 0, 0 } }, + { "@user", "login", 4, { "pw", "cap", "info", "photo" } }, + { "@shun", "uuid", 1, { "scom", 0, 0, 0 } }, + { "@reportfmt", "title", 3, { "owner", "cols", "sqlcode", 0 } }, + { "@concealed", "hash", 1, { "content", 0, 0, 0 } }, + }; + 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) ) break; + } + if( nToken<2 ) return; + if( aType[ii].zName[0]=='/' ){ + thisMask = configure_is_exportable(azToken[1]); + }else{ + thisMask = configure_is_exportable(aType[ii].zName); + } + if( (thisMask & groupMask)==0 ) return; + + blob_zero(&sql); + if( groupMask & CONFIGSET_OVERWRITE ){ + if( (thisMask & configHasBeenReset)==0 && aType[ii].zName[0]!='/' ){ + db_multi_exec("DELETE FROM %s", &aType[ii].zName[1]); + configHasBeenReset |= thisMask; + } + blob_append(&sql, "REPLACE INTO ", -1); + }else{ + blob_append(&sql, "INSERT OR IGNORE INTO ", -1); + } + blob_appendf(&sql, "%s(%s, mtime", &zName[1], aType[ii].zPrimKey); + for(jj=2; jj<nToken; jj+=2){ + blob_appendf(&sql, ",%s", azToken[jj]); + } + blob_appendf(&sql,") VALUES(%s,%s", azToken[1], azToken[0]); + for(jj=2; jj<nToken; jj+=2){ + blob_appendf(&sql, ",%s", azToken[jj+1]); + } + db_multi_exec("%s)", blob_str(&sql)); + if( db_changes()==0 ){ + blob_reset(&sql); + blob_appendf(&sql, "UPDATE %s SET mtime=%s", &zName[1], azToken[0]); + for(jj=2; jj<nToken; jj+=2){ + blob_appendf(&sql, ", %s=%s", azToken[jj], azToken[jj+1]); + } + blob_appendf(&sql, " WHERE %s=%s AND mtime<%s", + aType[ii].zPrimKey, azToken[1], azToken[0]); + db_multi_exec("%s", blob_str(&sql)); + } + blob_reset(&sql); }else{ - db_multi_exec( - "REPLACE INTO config(name,value) VALUES(%Q,%Q)", - zName, blob_str(pContent) - ); + /* Otherwise, the old format */ + if( (configure_is_exportable(zName) & groupMask)==0 ) return; + if( strcmp(zName, "logo-image")==0 ){ + Stmt ins; + db_prepare(&ins, + "REPLACE INTO config(name, value, mtime) VALUES(:name, :value, now())" + ); + db_bind_text(&ins, ":name", zName); + db_bind_blob(&ins, ":value", pContent); + db_step(&ins); + db_finalize(&ins); + }else if( zName[0]=='@' ){ + /* Notice that we are evaluating arbitrary SQL received from the + ** client. But this can only happen if the client has authenticated + ** as an administrator, so presumably we trust the client at this + ** point. + */ + db_multi_exec("%s", blob_str(pContent)); + }else{ + db_multi_exec( + "REPLACE INTO config(name,value,mtime) VALUES(%Q,%Q,now())", + zName, blob_str(pContent) + ); + } + } +} + +/* +** 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.okAdmin = g.okRdAddr = 1; + configure_receive(zName, &content, groupMask); + blob_reset(&content); + blob_seek(pIn, 1, BLOB_SEEK_CUR); + } } } - + /* -** After receiving configuration data, call this routine to transfer -** the results into the main database. +** 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. */ -void configure_finalize_receive(void){ - static const char zSQL[] = - @ DELETE FROM user; - @ INSERT INTO user SELECT * FROM _xfer_user; - @ DELETE FROM reportfmt; - @ INSERT INTO reportfmt SELECT * FROM _xfer_reportfmt; - @ DROP TABLE _xfer_user; - @ DROP TABLE _xfer_reportfmt; - ; - db_multi_exec(zSQL); +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 ){ + db_prepare(&q, "SELECT mtime, quote(login), quote(pw), quote(cap)," + " quote(info), quote(photo) FROM user" + " WHERE mtime>=%lld", iStart); + while( db_step(&q)==SQLITE_ROW ){ + blob_appendf(&rec,"%s %s pw %s cap %s info %s photo %s", + db_column_text(&q, 0), + db_column_text(&q, 1), + db_column_text(&q, 2), + db_column_text(&q, 3), + db_column_text(&q, 4), + db_column_text(&q, 5) + ); + 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 ){ + db_prepare(&q, "SELECT mtime, quote(title), quote(owner), quote(cols)," + " quote(sqlcode) FROM reportfmt" + " WHERE mtime>=%lld", iStart); + while( db_step(&q)==SQLITE_ROW ){ + blob_appendf(&rec,"%s %s owner %s cols %s sqlcode %s", + db_column_text(&q, 0), + db_column_text(&q, 1), + db_column_text(&q, 2), + db_column_text(&q, 3), + db_column_text(&q, 4) + ); + 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); + } + 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]!='@' ){ + 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. */ -static int find_area(const char *z){ +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, n)==0 ){ + if( strncmp(z, &aGroupName[i].zName[1], n)==0 ){ return aGroupName[i].groupMask; } } - printf("Available configuration areas:\n"); - for(i=0; i<count(aGroupName); i++){ - printf(" %-10s %s\n", aGroupName[i].zName, aGroupName[i].zHelp); + if( notFoundIsFatal ){ + printf("Available configuration areas:\n"); + for(i=0; i<count(aGroupName); i++){ + printf(" %-10s %s\n", &aGroupName[i].zName[1], aGroupName[i].zHelp); + } + fossil_fatal("no such configuration area: \"%s\"", z); } - 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 mask, /* Mask indicating which configuration to export */ + 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 */ ){ - int i; Blob out; blob_zero(&out); blob_appendf(&out, - "-- The \"%s\" configuration exported from\n" - "-- repository \"%s\"\n" - "-- on %s\n", + "# The \"%s\" configuration exported from\n" + "# repository \"%s\"\n" + "# on %s\n", zMask, g.zRepositoryName, db_text(0, "SELECT datetime('now')") ); - for(i=0; i<count(aConfig); i++){ - if( (aConfig[i].groupMask & mask)!=0 ){ - const char *zName = aConfig[i].zName; - if( zName[0]!='@' ){ - char *zValue = db_text(0, - "SELECT quote(value) FROM config WHERE name=%Q", zName); - if( zValue ){ - blob_appendf(&out,"REPLACE INTO config VALUES(%Q,%s);\n", - zName, zValue); - } - free(zValue); - }else{ - configure_render_special_name(zName, &out); - } - } - } + configure_send_group(&out, groupMask, iStart); blob_write_to_file(&out, zFilename); blob_reset(&out); } @@ -475,25 +750,31 @@ ** ** %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. +** server is used. Use the --legacy option for the older protocol +** (when talking to servers compiled prior to 2011-04-27.) 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. +** this to work. When the same record exists both locally and on +** the remote end, the one that was most recently changed wins. +** Use the --legacy flag when talking to holder servers. ** ** %fossil configuration reset AREA ** ** Restore the configuration to the default. AREA as above. ** -** WARNING: Do not import, merge, or pull configurations from an untrusted -** source. The inbound configuration is not checked for safety and can -** introduce security vulnerabilities. +** %fossil configuration sync AREA ?URL? +** +** Synchronize configuration changes in the local repository with +** the remote repository at URL. */ void configuration_cmd(void){ int n; const char *zMethod; if( g.argc<3 ){ @@ -502,36 +783,59 @@ db_find_and_open_repository(0, 0); 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 = find_area(g.argv[3]); - export_config(mask, g.argv[3], g.argv[4]); + 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]); db_begin_transaction(); - configure_prepare_to_receive(zMethod[0]=='i'); - db_multi_exec("%s", blob_str(&in)); - configure_finalize_receive(); + if( zMethod[0]=='i' ){ + groupMask = CONFIGSET_ALL | CONFIGSET_OVERWRITE; + }else{ + groupMask = CONFIGSET_ALL; + } + configure_receive_all(&in, groupMask); db_end_transaction(0); }else - if( strncmp(zMethod, "pull", n)==0 || strncmp(zMethod, "push", n)==0 ){ + if( strncmp(zMethod, "pull", n)==0 + || strncmp(zMethod, "push", n)==0 + || strncmp(zMethod, "sync", n)==0 + ){ int mask; const char *zServer; const char *zPw; + int legacyFlag = 0; + int overwriteFlag = 0; + if( zMethod[0]!='s' ) legacyFlag = find_option("legacy",0,0)!=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("pull AREA ?URL?"); } - mask = find_area(g.argv[3]); + mask = configure_name_to_mask(g.argv[3], 1); if( g.argc==5 ){ zServer = g.argv[4]; zPw = 0; g.dontKeepUrl = 1; }else{ @@ -543,25 +847,29 @@ } url_parse(zServer); if( g.urlPasswd==0 && zPw ) g.urlPasswd = mprintf("%s", zPw); user_select(); url_enable_proxy("via proxy: "); + if( legacyFlag ) mask |= CONFIGSET_OLDFORMAT; + if( overwriteFlag ) mask |= CONFIGSET_OVERWRITE; if( strncmp(zMethod, "push", n)==0 ){ client_sync(0,0,0,0,0,mask); - }else{ + }else if( strncmp(zMethod, "pull", n)==0 ){ client_sync(0,0,0,0,mask,0); + }else{ + client_sync(0,0,0,0,mask,mask); } }else if( strncmp(zMethod, "reset", n)==0 ){ int mask, i; char *zBackup; if( g.argc!=4 ) usage("reset AREA"); - mask = find_area(g.argv[3]); + 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], zBackup); + 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_multi_exec("DELETE FROM config WHERE name=%Q", zName); Index: src/db.c ================================================================== --- src/db.c +++ src/db.c @@ -34,10 +34,11 @@ #endif #include <sqlite3.h> #include <sys/types.h> #include <sys/stat.h> #include <unistd.h> +#include <time.h> #include "db.h" #if INTERFACE /* ** An single SQL statement is represented as an instance of the following @@ -606,10 +607,23 @@ } va_end(ap); sqlite3_exec(db, "COMMIT", 0, 0, 0); sqlite3_close(db); } + +/* +** Function to return the number of seconds since 1970. This is +** the same as strftime('%s','now') but is more compact. +*/ +static void db_now_function( + sqlite3_context *context, + int argc, + sqlite3_value **argv +){ + sqlite3_result_int64(context, time(0)); +} + /* ** Open a database file. Return a pointer to the new database ** connection. An error results in process abort. */ @@ -630,10 +644,11 @@ if( rc!=SQLITE_OK ){ db_err(sqlite3_errmsg(db)); } sqlite3_busy_timeout(db, 5000); sqlite3_wal_autocheckpoint(db, 1); /* Set to checkpoint frequently */ + sqlite3_create_function(db, "now", 0, SQLITE_ANY, 0, db_now_function, 0, 0); return db; } /* @@ -1105,14 +1120,14 @@ db_set("content-schema", CONTENT_SCHEMA, 0); db_set("aux-schema", AUX_SCHEMA, 0); if( makeServerCodes ){ db_multi_exec( - "INSERT INTO config(name,value)" - " VALUES('server-code', lower(hex(randomblob(20))));" - "INSERT INTO config(name,value)" - " VALUES('project-code', lower(hex(randomblob(20))));" + "INSERT INTO config(name,value,mtime)" + " VALUES('server-code', lower(hex(randomblob(20))),now());" + "INSERT INTO config(name,value,mtime)" + " VALUES('project-code', lower(hex(randomblob(20))),now());" ); } if( !db_is_global("autosync") ) db_set_int("autosync", 1, 0); if( !db_is_global("localauth") ) db_set_int("localauth", 0, 0); db_create_default_users(0, zDefaultUser); @@ -1295,11 +1310,12 @@ sha1sum_step_text(zContent, n); sha1sum_finish(&out); sqlite3_snprintf(sizeof(zHash), zHash, "%s", blob_str(&out)); blob_reset(&out); db_multi_exec( - "INSERT OR IGNORE INTO concealed VALUES(%Q,%#Q)", + "INSERT OR IGNORE INTO concealed(hash,content,mtime)" + " VALUES(%Q,%#Q,now())", zHash, n, zContent ); } return zHash; } @@ -1406,11 +1422,11 @@ db_swap_connections(); db_multi_exec("REPLACE INTO global_config(name,value) VALUES(%Q,%Q)", zName, zValue); db_swap_connections(); }else{ - db_multi_exec("REPLACE INTO config(name,value) VALUES(%Q,%Q)", + db_multi_exec("REPLACE INTO config(name,value,mtime) VALUES(%Q,%Q,now())", zName, zValue); } if( globalFlag && g.repositoryOpen ){ db_multi_exec("DELETE FROM config WHERE name=%Q", zName); } @@ -1465,11 +1481,11 @@ db_swap_connections(); db_multi_exec("REPLACE INTO global_config(name,value) VALUES(%Q,%d)", zName, value); db_swap_connections(); }else{ - db_multi_exec("REPLACE INTO config(name,value) VALUES(%Q,%d)", + db_multi_exec("REPLACE INTO config(name,value,mtime) VALUES(%Q,%d,now())", zName, value); } if( globalFlag && g.repositoryOpen ){ db_multi_exec("DELETE FROM config WHERE name=%Q", zName); } Index: src/login.c ================================================================== --- src/login.c +++ src/login.c @@ -1257,12 +1257,12 @@ db_multi_exec("DETACH other"); /* Propagate the changes to all other members of the login-group */ zSql = mprintf( "BEGIN;" - "REPLACE INTO config(name, value) VALUES('peer-name-%q', %Q);" - "REPLACE INTO config(name, value) VALUES('peer-repo-%q', %Q);" + "REPLACE INTO config(name,value,mtime) VALUES('peer-name-%q',%Q,now());" + "REPLACE INTO config(name,value,mtime) VALUES('peer-repo-%q',%Q,now());" "COMMIT;", zSelfProjCode, zSelfLabel, zSelfProjCode, zSelfRepo ); login_group_sql(zSql, "<li> ", "</li>", pzErrMsg); fossil_free(zSql); Index: src/rebuild.c ================================================================== --- src/rebuild.c +++ src/rebuild.c @@ -22,13 +22,15 @@ #include <assert.h> #include <dirent.h> #include <errno.h> /* -** Schema changes +** Make changes to the stable part of the schema (the part that is not +** simply deleted and reconstructed on a rebuild) to bring the schema +** up to the latest. */ -static const char zSchemaUpdates[] = +static const char zSchemaUpdates1[] = @ -- Index on the delta table @ -- @ CREATE INDEX IF NOT EXISTS delta_i1 ON delta(srcid); @ @ -- Artifacts that should not be processed are identified in the @@ -39,41 +41,124 @@ @ -- @ -- Shunned artifacts do not exist in the blob table. Hence they @ -- have not artifact ID (rid) and we thus must store their full @ -- UUID. @ -- -@ CREATE TABLE IF NOT EXISTS shun(uuid UNIQUE); +@ CREATE TABLE IF NOT EXISTS shun( +@ uuid UNIQUE, -- UUID of artifact to be shunned. Canonical form +@ mtime INTEGER, -- When added. Seconds since 1970 +@ scom TEXT -- Optional text explaining why the shun occurred +@ ); @ @ -- Artifacts that should not be pushed are stored in the "private" @ -- table. @ -- @ CREATE TABLE IF NOT EXISTS private(rid INTEGER PRIMARY KEY); @ -@ -- An entry in this table describes a database query that generates a -@ -- table of tickets. -@ -- -@ CREATE TABLE IF NOT EXISTS reportfmt( -@ rn integer primary key, -- Report number -@ owner text, -- Owner of this report format (not used) -@ title text, -- Title of this report -@ cols text, -- A color-key specification -@ sqlcode text -- An SQL SELECT statement for this report -@ ); -@ @ -- Some ticket content (such as the originators email address or contact @ -- information) needs to be obscured to protect privacy. This is achieved @ -- by storing an SHA1 hash of the content. For display, the hash is @ -- mapped back into the original text using this table. @ -- @ -- This table contains sensitive information and should not be shared @ -- with unauthorized users. @ -- @ CREATE TABLE IF NOT EXISTS concealed( -@ hash TEXT PRIMARY KEY, -@ content TEXT +@ hash TEXT PRIMARY KEY, -- The SHA1 hash of content +@ mtime INTEGER, -- Time created. Seconds since 1970 +@ content TEXT -- Content intended to be concealed +@ ); +; +static const char zSchemaUpdates2[] = +@ -- An entry in this table describes a database query that generates a +@ -- table of tickets. +@ -- +@ CREATE TABLE IF NOT EXISTS reportfmt( +@ rn INTEGER PRIMARY KEY, -- Report number +@ owner TEXT, -- Owner of this report format (not used) +@ title TEXT UNIQUE, -- Title of this report +@ mtime INTEGER, -- Time last modified. Seconds since 1970 +@ cols TEXT, -- A color-key specification +@ sqlcode TEXT -- An SQL SELECT statement for this report @ ); ; + +static void rebuild_update_schema(void){ + int rc; + db_multi_exec(zSchemaUpdates1); + db_multi_exec(zSchemaUpdates2); + + rc = db_exists("SELECT 1 FROM sqlite_master" + " WHERE name='user' AND sql GLOB '* mtime *'"); + if( rc==0 ){ + db_multi_exec( + "CREATE TEMP TABLE temp_user AS SELECT * FROM user;" + "DROP TABLE user;" + "CREATE TABLE user(\n" + " uid INTEGER PRIMARY KEY,\n" + " login TEXT UNIQUE,\n" + " pw TEXT,\n" + " cap TEXT,\n" + " cookie TEXT,\n" + " ipaddr TEXT,\n" + " cexpire DATETIME,\n" + " info TEXT,\n" + " mtime DATE,\n" + " photo BLOB\n" + ");" + "INSERT OR IGNORE INTO user" + " SELECT uid, login, pw, cap, cookie," + " ipaddr, cexpire, info, now(), photo FROM temp_user;" + "DROP TABLE temp_user;" + ); + } + + rc = db_exists("SELECT 1 FROM sqlite_master" + " WHERE name='config' AND sql GLOB '* mtime *'"); + if( rc==0 ){ + db_multi_exec( + "ALTER TABLE config ADD COLUMN mtime INTEGER;" + "UPDATE config SET mtime=now();" + ); + } + + rc = db_exists("SELECT 1 FROM sqlite_master" + " WHERE name='shun' AND sql GLOB '* mtime *'"); + if( rc==0 ){ + db_multi_exec( + "ALTER TABLE shun ADD COLUMN mtime INTEGER;" + "ALTER TABLE shun ADD COLUMN scom TEXT;" + "UPDATE shun SET mtime=now();" + ); + } + + rc = db_exists("SELECT 1 FROM sqlite_master" + " WHERE name='reportfmt' AND sql GLOB '* mtime *'"); + if( rc==0 ){ + db_multi_exec( + "CREATE TEMP TABLE old_fmt AS SELECT * FROM reportfmt;" + "DROP TABLE reportfmt;" + ); + db_multi_exec(zSchemaUpdates2); + db_multi_exec( + "INSERT OR IGNORE INTO reportfmt(rn,owner,title,cols,sqlcode,mtime)" + " SELECT rn, owner, title, cols, sqlcode, now() FROM old_fmt;" + "INSERT OR IGNORE INTO reportfmt(rn,owner,title,cols,sqlcode,mtime)" + " SELECT rn, owner, title || ' (' || rn || ')', cols, sqlcode, now()" + " FROM old_fmt;" + ); + } + + rc = db_exists("SELECT 1 FROM sqlite_master" + " WHERE name='concealed' AND sql GLOB '* mtime *'"); + if( rc==0 ){ + db_multi_exec( + "ALTER TABLE concealed ADD COLUMN mtime INTEGER;" + "UPDATE concealed SET mtime=now();" + ); + } +} /* ** Variables used to store state information about an on-going "rebuild" ** or "deconstruct". */ @@ -256,11 +341,11 @@ ttyOutput = doOut; processCnt = 0; if (!g.fQuiet) { percent_complete(0); } - db_multi_exec(zSchemaUpdates); + rebuild_update_schema(); for(;;){ zTable = db_text(0, "SELECT name FROM sqlite_master /*scan*/" " WHERE type='table'" " AND name NOT IN ('blob','delta','rcvfrom','user'," @@ -459,12 +544,12 @@ } db_begin_transaction(); ttyOutput = 1; errCnt = rebuild_db(randomizeFlag, 1, doClustering); db_multi_exec( - "REPLACE INTO config(name,value) VALUES('content-schema','%s');" - "REPLACE INTO config(name,value) VALUES('aux-schema','%s');", + "REPLACE INTO config(name,value,mtime) VALUES('content-schema','%s',now());" + "REPLACE INTO config(name,value,mtime) VALUES('aux-schema','%s',now());", CONTENT_SCHEMA, AUX_SCHEMA ); if( errCnt && !forceFlag ){ printf("%d errors. Rolling back changes. Use --force to force a commit.\n", errCnt); Index: src/report.c ================================================================== --- src/report.c +++ src/report.c @@ -360,19 +360,25 @@ }else if( (zTitle = trim_string(zTitle))[0]==0 ){ zErr = "Please supply a title"; }else{ zErr = verify_sql_statement(zSQL); } + if( zErr==0 + && db_exists("SELECT 1 FROM reportfmt WHERE title=%Q and rn<>%d", + zTitle, rn) + ){ + zErr = mprintf("There is already another report named \"%h\"", zTitle); + } if( zErr==0 ){ login_verify_csrf_secret(); if( rn>0 ){ db_multi_exec("UPDATE reportfmt SET title=%Q, sqlcode=%Q," - " owner=%Q, cols=%Q WHERE rn=%d", + " owner=%Q, cols=%Q, mtime=now() WHERE rn=%d", zTitle, zSQL, zOwner, zClrKey, rn); }else{ - db_multi_exec("INSERT INTO reportfmt(title,sqlcode,owner,cols) " - "VALUES(%Q,%Q,%Q,%Q)", + db_multi_exec("INSERT INTO reportfmt(title,sqlcode,owner,cols,mtime) " + "VALUES(%Q,%Q,%Q,%Q,now())", zTitle, zSQL, zOwner, zClrKey); rn = db_last_insert_rowid(); } cgi_redirect(mprintf("rptview?rn=%d", rn)); return; Index: src/schema.c ================================================================== --- src/schema.c +++ src/schema.c @@ -39,12 +39,12 @@ ** changes. The aux tables have an arbitrary version number (typically ** a date) which can change frequently. When the content schema changes, ** we have to execute special procedures to update the schema. When ** the aux schema changes, all we need to do is rebuild the database. */ -#define CONTENT_SCHEMA "1" -#define AUX_SCHEMA "2011-02-25 14:52" +#define CONTENT_SCHEMA "2" +#define AUX_SCHEMA "2011-04-25 19:50" #endif /* INTERFACE */ /* @@ -51,21 +51,25 @@ ** The schema for a repository database. ** ** Schema1[] contains parts of the schema that are fixed and unchanging ** across versions. Schema2[] contains parts of the schema that can ** change from one version to the next. The information in Schema2[] -** can be reconstructed from the information in Schema1[]. +** is reconstructed from the information in Schema1[] by the "rebuild" +** operation. */ const char zRepositorySchema1[] = @ -- The BLOB and DELTA tables contain all records held in the repository. @ -- -@ -- The BLOB.CONTENT column is always compressed using libz. This +@ -- The BLOB.CONTENT column is always compressed using zlib. This @ -- column might hold the full text of the record or it might hold @ -- a delta that is able to reconstruct the record from some other @ -- record. If BLOB.CONTENT holds a delta, then a DELTA table entry @ -- will exist for the record and that entry will point to another @ -- entry that holds the source of the delta. Deltas can be chained. +@ -- +@ -- The blob and delta tables collectively hold the "global state" of +@ -- a Fossil repository. @ -- @ CREATE TABLE blob( @ rid INTEGER PRIMARY KEY, -- Record ID @ rcvid INTEGER, -- Origin of this record @ size INTEGER, -- Size of content. -1 for a phantom. @@ -77,17 +81,24 @@ @ rid INTEGER PRIMARY KEY, -- Record ID @ srcid INTEGER NOT NULL REFERENCES blob -- Record holding source document @ ); @ CREATE INDEX delta_i1 ON delta(srcid); @ +@ ------------------------------------------------------------------------- +@ -- The BLOB and DELTA tables above hold the "global state" of a Fossil +@ -- project; the stuff that is normally exchanged during "sync". The +@ -- "local state" of a repository is contained in the remaining tables of +@ -- the zRepositorySchema1 string. +@ ------------------------------------------------------------------------- +@ @ -- Whenever new blobs are received into the repository, an entry @ -- in this table records the source of the blob. @ -- @ CREATE TABLE rcvfrom( @ rcvid INTEGER PRIMARY KEY, -- Received-From ID @ uid INTEGER REFERENCES user, -- User login -@ mtime DATETIME, -- Time or receipt +@ mtime DATETIME, -- Time of receipt. Julian day. @ nonce TEXT UNIQUE, -- Nonce used for login @ ipaddr TEXT -- Remote IP address. NULL for direct. @ ); @ @ -- Information about users @@ -99,26 +110,28 @@ @ -- hash based on the project-code, the user login, and the cleartext @ -- password. @ -- @ CREATE TABLE user( @ uid INTEGER PRIMARY KEY, -- User ID -@ login TEXT, -- login name of the user +@ login TEXT UNIQUE, -- login name of the user @ pw TEXT, -- password @ cap TEXT, -- Capabilities of this user @ cookie TEXT, -- WWW login cookie @ ipaddr TEXT, -- IP address for which cookie is valid @ cexpire DATETIME, -- Time when cookie expires @ info TEXT, -- contact information +@ mtime DATE, -- last change. seconds since 1970 @ photo BLOB -- JPEG image of this user @ ); @ @ -- The VAR table holds miscellanous information about the repository. @ -- in the form of name-value pairs. @ -- @ CREATE TABLE config( @ name TEXT PRIMARY KEY NOT NULL, -- Primary name of the entry @ value CLOB, -- Content of the named parameter +@ mtime DATE, -- last modified. seconds since 1970 @ CHECK( typeof(name)='text' AND length(name)>=1 ) @ ); @ @ -- Artifacts that should not be processed are identified in the @ -- "shun" table. Artifacts that are control-file forgeries or @@ -128,11 +141,15 @@ @ -- @ -- Shunned artifacts do not exist in the blob table. Hence they @ -- have not artifact ID (rid) and we thus must store their full @ -- UUID. @ -- -@ CREATE TABLE shun(uuid UNIQUE); +@ CREATE TABLE shun( +@ uuid UNIQUE, -- UUID of artifact to be shunned. Canonical form +@ mtime DATE, -- When added. seconds since 1970 +@ scom TEXT -- Optional text explaining why the shun occurred +@ ); @ @ -- Artifacts that should not be pushed are stored in the "private" @ -- table. Private artifacts are omitted from the "unclustered" and @ -- "unsent" tables. @ -- @@ -140,17 +157,19 @@ @ @ -- An entry in this table describes a database query that generates a @ -- table of tickets. @ -- @ CREATE TABLE reportfmt( -@ rn integer primary key, -- Report number -@ owner text, -- Owner of this report format (not used) -@ title text, -- Title of this report -@ cols text, -- A color-key specification -@ sqlcode text -- An SQL SELECT statement for this report +@ rn INTEGER PRIMARY KEY, -- Report number +@ owner TEXT, -- Owner of this report format (not used) +@ title TEXT UNIQUE, -- Title of this report +@ mtime DATE, -- Last modified. seconds since 1970 +@ cols TEXT, -- A color-key specification +@ sqlcode TEXT -- An SQL SELECT statement for this report @ ); -@ INSERT INTO reportfmt(title,cols,sqlcode) VALUES('All Tickets','#ffffff Key: +@ INSERT INTO reportfmt(title,mtime,cols,sqlcode) +@ VALUES('All Tickets',julianday('1970-01-01'),'#ffffff Key: @ #f2dcdc Active @ #e8e8e8 Review @ #cfe8bd Fixed @ #bde5d6 Tested @ #cacae5 Deferred @@ -176,12 +195,13 @@ @ -- @ -- This table contains sensitive information and should not be shared @ -- with unauthorized users. @ -- @ CREATE TABLE concealed( -@ hash TEXT PRIMARY KEY, -@ content TEXT +@ hash TEXT PRIMARY KEY, -- The SHA1 hash of content +@ mtime DATE, -- Time created. Seconds since 1970 +@ content TEXT -- Content intended to be concealed @ ); ; const char zRepositorySchema2[] = @ -- Filenames @@ -214,11 +234,11 @@ @ -- @ CREATE TABLE plink( @ pid INTEGER REFERENCES blob, -- Parent manifest @ cid INTEGER REFERENCES blob, -- Child manifest @ isprim BOOLEAN, -- pid is the primary parent of cid -@ mtime DATETIME, -- the date/time stamp on cid +@ mtime DATETIME, -- the date/time stamp on cid. Julian day. @ UNIQUE(pid, cid) @ ); @ CREATE INDEX plink_i2 ON plink(cid,pid); @ @ -- A "leaf" checkin is a checkin that has no children in the same @@ -232,11 +252,11 @@ @ @ -- Events used to generate a timeline @ -- @ CREATE TABLE event( @ type TEXT, -- Type of event: 'ci', 'w', 'e', 't' -@ mtime DATETIME, -- Date and time when the event occurs +@ mtime DATETIME, -- Time of occurrence. Julian day. @ objid INTEGER PRIMARY KEY, -- Associated record ID @ tagid INTEGER, -- Associated ticket or wiki name tag @ uid INTEGER REFERENCES user, -- User who caused the event @ bgcolor TEXT, -- Color set by 'bgcolor' property @ euser TEXT, -- User set by 'user' property @@ -319,11 +339,11 @@ @ tagid INTEGER REFERENCES tag, -- The tag that added or removed @ tagtype INTEGER, -- 0:-,cancel 1:+,single 2:*,propagate @ srcid INTEGER REFERENCES blob, -- Artifact of tag. 0 for propagated tags @ origid INTEGER REFERENCES blob, -- check-in holding propagated tag @ value TEXT, -- Value of the tag. Might be NULL. -@ mtime TIMESTAMP, -- Time of addition or removal +@ mtime TIMESTAMP, -- Time of addition or removal. Julian day @ rid INTEGER REFERENCE blob, -- Artifact tag is applied to @ UNIQUE(rid, tagid) @ ); @ CREATE INDEX tagxref_i1 ON tagxref(tagid, mtime); @ @@ -334,11 +354,11 @@ @ -- @ CREATE TABLE backlink( @ target TEXT, -- Where the hyperlink points to @ srctype INT, -- 0: check-in 1: ticket 2: wiki @ srcid INT, -- rid for checkin or wiki. tkt_id for ticket. -@ mtime TIMESTAMP, -- time that the hyperlink was added +@ mtime TIMESTAMP, -- time that the hyperlink was added. Julian day. @ UNIQUE(target, srctype, srcid) @ ); @ CREATE INDEX backlink_src ON backlink(srcid, srctype); @ @ -- Each attachment is an entry in the following table. Only @@ -345,11 +365,11 @@ @ -- the most recent attachment (identified by the D card) is saved. @ -- @ CREATE TABLE attachment( @ attachid INTEGER PRIMARY KEY, -- Local id for this attachment @ isLatest BOOLEAN DEFAULT 0, -- True if this is the one to use -@ mtime TIMESTAMP, -- Time when attachment last changed +@ mtime TIMESTAMP, -- Last changed. Julian day. @ src TEXT, -- UUID of the attachment. NULL to delete @ target TEXT, -- Object attached to. Wikiname or Tkt UUID @ filename TEXT, -- Filename for the attachment @ comment TEXT, -- Comment associated with this attachment @ user TEXT -- Name of user adding attachment @@ -443,11 +463,11 @@ @ chnged INT DEFAULT 0, -- 0:unchnged 1:edited 2:m-chng 3:m-add @ deleted BOOLEAN DEFAULT 0, -- True if deleted @ isexe BOOLEAN, -- True if file should be executable @ rid INTEGER, -- Originally from this repository record @ mrid INTEGER, -- Based on this record due to a merge -@ mtime INTEGER, -- Modification time of file on disk +@ mtime INTEGER, -- Mtime of file on disk. sec since 1970 @ pathname TEXT, -- Full pathname relative to root @ origname TEXT, -- Original pathname. NULL if unchanged @ UNIQUE(pathname,vid) @ ); @ Index: src/setup.c ================================================================== --- src/setup.c +++ src/setup.c @@ -354,12 +354,12 @@ style_footer(); return; } login_verify_csrf_secret(); db_multi_exec( - "REPLACE INTO user(uid,login,info,pw,cap) " - "VALUES(nullif(%d,0),%Q,%Q,%Q,'%s')", + "REPLACE INTO user(uid,login,info,pw,cap,mtime) " + "VALUES(nullif(%d,0),%Q,%Q,%Q,'%s',now())", uid, P("login"), P("info"), zPw, zCap ); if( atoi(PD("all","0"))>0 ){ Blob sql; char *zErr = 0; @@ -375,11 +375,12 @@ blob_appendf(&sql, "UPDATE user SET login=%Q," " pw=coalesce(shared_secret(%Q,%Q," "(SELECT value FROM config WHERE name='project-code')),pw)," " info=%Q," - " cap=%Q" + " cap=%Q," + " mtime=now()" " WHERE login=%Q;", zLogin, P("pw"), zLogin, P("info"), zCap, zOldLogin ); login_group_sql(blob_str(&sql), "<li> ", " </li>\n", &zErr); @@ -1286,18 +1287,18 @@ if( P("set")!=0 && zMime && zMime[0] && szImg>0 ){ Blob img; Stmt ins; blob_init(&img, aImg, szImg); db_prepare(&ins, - "REPLACE INTO config(name, value)" - " VALUES('logo-image',:bytes)" + "REPLACE INTO config(name,value,mtime)" + " VALUES('logo-image',:bytes,now())" ); db_bind_blob(&ins, ":bytes", &img); db_step(&ins); db_finalize(&ins); db_multi_exec( - "REPLACE INTO config(name, value) VALUES('logo-mimetype',%Q)", + "REPLACE INTO config(name,value,mtime) VALUES('logo-mimetype',%Q,now())", zMime ); db_end_transaction(0); cgi_redirect("setup_logo"); }else if( P("clr")!=0 ){ Index: src/shun.c ================================================================== --- src/shun.c +++ src/shun.c @@ -81,17 +81,31 @@ @ <b>fossil rebuild</b> command-line before the artifact content @ can pulled in from other respositories.</p> } } if( zUuid && P("add") ){ + int rid, tagid; login_verify_csrf_secret(); - db_multi_exec("INSERT OR IGNORE INTO shun VALUES('%s')", zUuid); + db_multi_exec( + "INSERT OR IGNORE INTO shun(uuid,mtime)" + " VALUES('%s', now())", zUuid); @ <p class="shunned">Artifact @ <a href="%s(g.zTop)/artifact/%s(zUuid)">%s(zUuid)</a> has been @ shunned. It will no longer be pushed. @ It will be removed from the repository the next time the respository @ is rebuilt using the <b>fossil rebuild</b> command-line</p> + db_multi_exec("DELETE FROM attachment WHERE src=%Q", zUuid); + rid = db_int(0, "SELECT rid FROM blob WHERE uuid=%Q", zUuid); + if( rid ){ + db_multi_exec("DELETE FROM event WHERE objid=%d", rid); + } + tagid = db_int(0, "SELECT tagid FROM tag WHERE tagname='tkt-%q'", zUuid); + if( tagid ){ + db_multi_exec("DELETE FROM ticket WHERE tkt_uuid=%Q", zUuid); + db_multi_exec("DELETE FROM tag WHERE tagid=%d", tagid); + db_multi_exec("DELETE FROM tagxref WHERE tagid=%d", tagid); + } } @ <p>A shunned artifact will not be pushed nor accepted in a pull and the @ artifact content will be purged from the repository the next time the @ repository is rebuilt. A list of shunned artifacts can be seen at the @ bottom of this page.</p> Index: src/skins.c ================================================================== --- src/skins.c +++ src/skins.c @@ -25,11 +25,12 @@ /* ** A black-and-white theme with the project title in a bar across the top ** and no logo image. */ static const char zBuiltinSkin1[] = -@ REPLACE INTO config VALUES('css','/* General settings for the entire page */ +@ REPLACE INTO config(name,mtime,value) +@ VALUES('css',now(),'/* General settings for the entire page */ @ body { @ margin: 0ex 1ex; @ padding: 0px; @ background-color: white; @ font-family: sans-serif; @@ -152,11 +153,11 @@ @ table.label-value th { @ vertical-align: top; @ text-align: right; @ padding: 0.2ex 2ex; @ }'); -@ REPLACE INTO config VALUES('header','<html> +@ REPLACE INTO config(name,mtime,value) VALUES('header',now(),'<html> @ <head> @ <title>$<project_name>: $<title></title> @ <link rel="alternate" type="application/rss+xml" title="RSS Feed" @ href="$home/timeline.rss"> @ <link rel="stylesheet" href="$home/style.css?blackwhite" type="text/css" @@ -204,11 +205,12 @@ @ } else { @ html "<a href=''$home/login''>Login</a> " @ } @ </th1></div> @ '); -@ REPLACE INTO config VALUES('footer','<div class="footer"> +@ REPLACE INTO config(name,mtime,value) +@ VALUES('footer',now(),'<div class="footer"> @ Fossil version $manifest_version $manifest_date @ </div> @ </body></html> @ '); ; @@ -216,11 +218,12 @@ /* ** A tan theme with the project title above the user identification ** and no logo image. */ static const char zBuiltinSkin2[] = -@ REPLACE INTO config VALUES('css','/* General settings for the entire page */ +@ REPLACE INTO config(name,mtime,value) +@ VALUES('css',now(),'/* General settings for the entire page */ @ body { @ margin: 0ex 0ex; @ padding: 0px; @ background-color: #fef3bc; @ font-family: sans-serif; @@ -354,11 +357,11 @@ @ vertical-align: top; @ text-align: right; @ padding: 0.2ex 2ex; @ } @ '); -@ REPLACE INTO config VALUES('header','<html> +@ REPLACE INTO config(name,mtime,value) VALUES('header',now(),'<html> @ <head> @ <title>$<project_name>: $<title></title> @ <link rel="alternate" type="application/rss+xml" title="RSS Feed" @ href="$home/timeline.rss"> @ <link rel="stylesheet" href="$home/style.css?tan" type="text/css" @@ -405,11 +408,12 @@ @ } else { @ html "<a href=''$home/login''>Login</a> " @ } @ </th1></div> @ '); -@ REPLACE INTO config VALUES('footer','<div class="footer"> +@ REPLACE INTO config(name,mtime,value) +@ VALUES('footer',now(),'<div class="footer"> @ Fossil version $manifest_version $manifest_date @ </div> @ </body></html> @ '); ; @@ -417,11 +421,12 @@ /* ** Black letters on a white or cream background with the main menu ** stuck on the left-hand side. */ static const char zBuiltinSkin3[] = -@ REPLACE INTO config VALUES('css','/* General settings for the entire page */ +@ REPLACE INTO config(name,mtime,value) +@ VALUES('css',now(),'/* General settings for the entire page */ @ body { @ margin:0px 0px 0px 0px; @ padding:0px; @ font-family:verdana, arial, helvetica, "sans serif"; @ color:#333; @@ -586,11 +591,11 @@ @ table.label-value th { @ vertical-align: top; @ text-align: right; @ padding: 0.2ex 2ex; @ }'); -@ REPLACE INTO config VALUES('header','<html> +@ REPLACE INTO config(name,mtime,value) VALUES('header',now(),'<html> @ <head> @ <title>$<project_name>: $<title></title> @ <link rel="alternate" type="application/rss+xml" title="RSS Feed" @ href="$home/timeline.rss"> @ <link rel="stylesheet" href="$home/style.css?black2" type="text/css" @@ -640,11 +645,11 @@ @ html "<li><a href=''$home/login''>Login</a></li>" @ } @ </th1></ul></div> @ <div id="container"> @ '); -@ REPLACE INTO config VALUES('footer','</div> +@ REPLACE INTO config(name,mtime,value) VALUES('footer',now(),'</div> @ <div class="footer"> @ Fossil version $manifest_version $manifest_date @ </div> @ </body></html> @ '); @@ -653,11 +658,12 @@ /* ** Gradients and rounded corners. */ static const char zBuiltinSkin4[] = -@ REPLACE INTO config VALUES('css','/* General settings for the entire page */ +@ REPLACE INTO config(name,mtime,value) +@ VALUES('css',now(),'/* General settings for the entire page */ @ html { @ min-height: 100%; @ } @ body { @ margin: 0ex 1ex; @@ -880,11 +886,11 @@ @ } @ @ textarea { @ font-size: 1em; @ }'); -@ REPLACE INTO config VALUES('header','<html> +@ REPLACE INTO config(name,mtime,value) VALUES('header',now(),'<html> @ <head> @ <title>$<project_name>: $<title></title> @ <link rel="alternate" type="application/rss+xml" title="RSS Feed" @ href="$home/timeline.rss"> @ <link rel="stylesheet" href="$home/style.css?black2" type="text/css" @@ -934,11 +940,11 @@ @ html "<a href=''$home/login''>Login</a>" @ } @ </th1></ul></div> @ <div id="container"> @ '); -@ REPLACE INTO config VALUES('footer','</div> +@ REPLACE INTO config(name,mtime,value) VALUES('footer',now(),'</div> @ <div class="footer"> @ Fossil version $manifest_version $manifest_date @ </div> @ </body></html> @ '); @@ -984,17 +990,20 @@ ** Memory to hold the returned string is obtained from malloc. */ static char *getSkin(int useDefault){ Blob val; blob_zero(&val); - blob_appendf(&val, "REPLACE INTO config VALUES('css',%Q);\n", + blob_appendf(&val, + "REPLACE INTO config(name,value,mtime) VALUES('css',%Q,now());\n", useDefault ? zDefaultCSS : db_get("css", (char*)zDefaultCSS) ); - blob_appendf(&val, "REPLACE INTO config VALUES('header',%Q);\n", + blob_appendf(&val, + "REPLACE INTO config(name,value,mtime) VALUES('header',%Q,now());\n", useDefault ? zDefaultHeader : db_get("header", (char*)zDefaultHeader) ); - blob_appendf(&val, "REPLACE INTO config VALUES('footer',%Q);\n", + blob_appendf(&val, + "REPLACE INTO config(name,value,mtime) VALUES('footer',%Q,now());\n", useDefault ? zDefaultFooter : db_get("footer", (char*)zDefaultFooter) ); return blob_str(&val); } @@ -1048,11 +1057,11 @@ if( db_exists("SELECT 1 FROM config WHERE name=%Q", zName) || strcmp(zName, "Default")==0 ){ zErr = mprintf("Skin name \"%h\" already exists. " "Choose a different name.", P("sn")); }else{ - db_multi_exec("INSERT INTO config VALUES(%Q,%Q)", + db_multi_exec("INSERT INTO config(name,value,mtime) VALUES(%Q,%Q,now())", zName, zCurrent ); } } @@ -1069,13 +1078,13 @@ seen = db_exists("SELECT 1 FROM config WHERE name GLOB 'skin:*'" " AND value=%Q", zCurrent); } if( !seen ){ db_multi_exec( - "INSERT INTO config VALUES(" + "INSERT INTO config(name,value,mtime) VALUES(" " strftime('skin:Backup On %%Y-%%m-%%d %%H:%%M:%%S')," - " %Q)", zCurrent + " %Q,now())", zCurrent ); } seen = 0; for(i=0; i<sizeof(aBuiltinSkin)/sizeof(aBuiltinSkin[0]); i++){ if( strcmp(aBuiltinSkin[i].zName, z)==0 ){ Index: src/user.c ================================================================== --- src/user.c +++ src/user.c @@ -204,12 +204,12 @@ }else{ prompt_for_password("password: ", &passwd, 1); } zPw = sha1_shared_secret(blob_str(&passwd), blob_str(&login), 0); db_multi_exec( - "INSERT INTO user(login,pw,cap,info)" - "VALUES(%B,%Q,%B,%B)", + "INSERT INTO user(login,pw,cap,info,mtime)" + "VALUES(%B,%Q,%B,%B,now())", &login, zPw, &caps, &contact ); free(zPw); }else if( n>=2 && strncmp(g.argv[2],"default",n)==0 ){ user_select(); @@ -249,11 +249,12 @@ } if( blob_size(&pw)==0 ){ printf("password unchanged\n"); }else{ char *zSecret = sha1_shared_secret(blob_str(&pw), g.argv[3], 0); - db_multi_exec("UPDATE user SET pw=%Q WHERE uid=%d", zSecret, uid); + db_multi_exec("UPDATE user SET pw=%Q, mtime=now() WHERE uid=%d", + zSecret, uid); free(zSecret); } }else if( n>=2 && strncmp(g.argv[2],"capabilities",2)==0 ){ int uid; if( g.argc!=4 && g.argc!=5 ){ @@ -263,12 +264,12 @@ if( uid==0 ){ fossil_fatal("no such user: %s", g.argv[3]); } if( g.argc==5 ){ db_multi_exec( - "UPDATE user SET cap=%Q WHERE uid=%d", g.argv[4], - uid + "UPDATE user SET cap=%Q, mtime=now() WHERE uid=%d", + g.argv[4], uid ); } printf("%s\n", db_text(0, "SELECT cap FROM user WHERE uid=%d", uid)); }else{ fossil_panic("user subcommand should be one of: " @@ -340,12 +341,12 @@ db_finalize(&s); } if( g.userUid==0 ){ db_multi_exec( - "INSERT INTO user(login, pw, cap, info)" - "VALUES('anonymous', '', 'cfghjkmnoqw', '')" + "INSERT INTO user(login, pw, cap, info, mtime)" + "VALUES('anonymous', '', 'cfghjkmnoqw', '', now())" ); g.userUid = db_last_insert_rowid(); g.zLogin = "anonymous"; } } @@ -364,11 +365,11 @@ if( g.argc!=3 ) usage("REPOSITORY"); db_open_repository(g.argv[2]); sqlite3_create_function(g.db, "shared_secret", 2, SQLITE_UTF8, 0, sha1_shared_secret_sql_function, 0, 0); db_multi_exec( - "UPDATE user SET pw=shared_secret(pw,login)" + "UPDATE user SET pw=shared_secret(pw,login), mtime=now()" " WHERE length(pw)>0 AND length(pw)!=40" ); } /* Index: src/xfer.c ================================================================== --- src/xfer.c +++ src/xfer.c @@ -454,11 +454,11 @@ " (SELECT uuid FROM delta, blob" " WHERE delta.rid=:rid AND delta.srcid=blob.rid)" " FROM blob" " WHERE rid=:rid" " AND size>=0" - " AND uuid NOT IN shun" + " AND NOT EXISTS(SELECT 1 FROM shun WHERE shun.uuid=blob.uuid)" ); db_bind_int(&q1, ":rid", rid); rc = db_step(&q1); if( rc==SQLITE_ROW ){ zUuid = db_column_text(&q1, 0); @@ -738,13 +738,16 @@ } db_finalize(&q); } /* -** Send a single config card for configuration item zName +** Send a single old-style config card for configuration item zName. +** +** This routine and the functionality it implements is scheduled for +** removal on 2012-05-01. */ -static void send_config_card(Xfer *pXfer, const char *zName){ +static void send_legacy_config_card(Xfer *pXfer, const char *zName){ if( zName[0]!='@' ){ Blob val; blob_zero(&val); db_blob(&val, "SELECT value FROM config WHERE name=%Q", zName); if( blob_size(&val)>0 ){ @@ -760,11 +763,10 @@ blob_appendf(pXfer->pOut, "config %s %d\n%s\n", zName, blob_size(&content), blob_str(&content)); blob_reset(&content); } } - /* ** Called when there is an attempt to transfer private content to and ** from a server without authorization. */ @@ -1007,11 +1009,11 @@ */ if( blob_eq(&xfer.aToken[0], "login") && xfer.nToken==4 ){ if( disableLogin ){ - g.okRead = g.okWrite = g.okPrivate = 1; + g.okRead = g.okWrite = g.okPrivate = g.okAdmin = 1; }else{ if( check_tail_hash(&xfer.aToken[2], xfer.pIn) || check_login(&xfer.aToken[1], &xfer.aToken[2], &xfer.aToken[3]) ){ cgi_reset_content(); @@ -1029,12 +1031,19 @@ if( blob_eq(&xfer.aToken[0], "reqconfig") && xfer.nToken==2 ){ if( g.okRead ){ char *zName = blob_str(&xfer.aToken[1]); - if( configure_is_exportable(zName) ){ - send_config_card(&xfer, zName); + if( zName[0]=='/' ){ + /* New style configuration transfer */ + int groupMask = configure_name_to_mask(&zName[1], 0); + if( !g.okAdmin ) groupMask &= ~CONFIGSET_USER; + if( !g.okRdAddr ) groupMask &= ~CONFIGSET_ADDR; + configure_send_group(xfer.pOut, groupMask, 0); + }else if( configure_is_exportable(zName) ){ + /* Old style configuration transfer */ + send_legacy_config_card(&xfer, zName); } } }else /* config NAME SIZE \n CONTENT @@ -1052,11 +1061,11 @@ cgi_reset_content(); @ error not\sauthorized\sto\spush\sconfiguration nErr++; break; } - if( !recvConfig ){ + if( !recvConfig && zName[0]=='@' ){ configure_prepare_to_receive(0); recvConfig = 1; } configure_receive(zName, &content, CONFIGSET_ALL); blob_reset(&content); @@ -1185,14 +1194,14 @@ ** gdb fossil ** r test-xfer out.txt */ void cmd_test_xfer(void){ int notUsed; + db_find_and_open_repository(0,0); if( g.argc!=2 && g.argc!=3 ){ usage("?MESSAGEFILE?"); } - db_must_be_within_tree(); blob_zero(&g.cgiIn); blob_read_from_file(&g.cgiIn, g.argc==2 ? "-" : g.argv[2]); disableLogin = 1; page_xfer(); printf("%s\n", cgi_extract_content(¬Used)); @@ -1331,25 +1340,32 @@ while( zName ){ blob_appendf(&send, "reqconfig %s\n", zName); zName = configure_next_name(configRcvMask); nCardSent++; } - if( configRcvMask & (CONFIGSET_USER|CONFIGSET_TKT) ){ - configure_prepare_to_receive(0); + if( (configRcvMask & (CONFIGSET_USER|CONFIGSET_TKT))!=0 + && (configRcvMask & CONFIGSET_OLDFORMAT)!=0 + ){ + int overwrite = (configRcvMask & CONFIGSET_OVERWRITE)!=0; + configure_prepare_to_receive(overwrite); } origConfigRcvMask = configRcvMask; configRcvMask = 0; } /* Send configuration parameters being pushed */ if( configSendMask ){ - const char *zName; - zName = configure_first_name(configSendMask); - while( zName ){ - send_config_card(&xfer, zName); - zName = configure_next_name(configSendMask); - nCardSent++; + if( configSendMask & CONFIGSET_OLDFORMAT ){ + const char *zName; + zName = configure_first_name(configSendMask); + while( zName ){ + send_legacy_config_card(&xfer, zName); + zName = configure_next_name(configSendMask); + nCardSent++; + } + }else{ + nCardSent += configure_send_group(xfer.pOut, configSendMask, 0); } configSendMask = 0; } /* Append randomness to the end of the message. This makes all @@ -1647,11 +1663,13 @@ break; } blobarray_reset(xfer.aToken, xfer.nToken); blob_reset(&xfer.line); } - if( origConfigRcvMask & (CONFIGSET_TKT|CONFIGSET_USER) ){ + if( (configRcvMask & (CONFIGSET_USER|CONFIGSET_TKT))!=0 + && (configRcvMask & CONFIGSET_OLDFORMAT)!=0 + ){ configure_finalize_receive(); } origConfigRcvMask = 0; if( nCardRcvd>0 ){ fossil_print(zValueFormat, "Received:",