Changes On Branch config-sync
Not logged in

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Changes In Branch config-sync Excluding Merge-Ins

This is equivalent to a diff from e17fc71319 to 940faaa37e

2011-04-27
02:10
Merge in the config-sync changes. This is a major schema change and definitely requires a "fossil rebuild". Note that the schema upgrade is irreversible and so you should be certain you want to continue with the new schema before you upgrade. check-in: 1654456ef5 user: drh tags: trunk
02:05
Fix an issue with the --legacy option to configuration pull. Closed-Leaf check-in: 940faaa37e user: drh tags: config-sync
01:40
Fix an out-of-order local variable declaration. Ticket [a659e233cd79a0d]. check-in: 7b45d101dd user: drh tags: config-sync
2011-04-26
00:45
Begin implementing the protocol changes for configuration sync. check-in: f99e3fa9e6 user: drh tags: config-sync
2011-04-25
22:23
Change the definition of a "Leaf" to be any node that has no children of any kind (merge or non-merge) in the same branch. A "rebuild" or a "fossil leaves --recompute" is required to recompute the LEAF table after upgrading to this version. check-in: e17fc71319 user: drh tags: trunk
20:26
Add hyperlink to the annotation of a file in the object description header. check-in: 030a048697 user: drh tags: trunk

Changes to src/checkin.c.

   274    274   void clean_cmd(void){
   275    275     int allFlag;
   276    276     int dotfilesFlag;
   277    277     const char *zIgnoreFlag;
   278    278     Blob path, repo;
   279    279     Stmt q;
   280    280     int n;
          281  +  Glob *pIgnore;
          282  +
   281    283     allFlag = find_option("force","f",0)!=0;
   282    284     dotfilesFlag = find_option("dotfiles",0,0)!=0;
   283    285     zIgnoreFlag = find_option("ignore",0,1);
   284         -  Glob *pIgnore;
   285         -
   286    286     db_must_be_within_tree();
   287    287     if( zIgnoreFlag==0 ){
   288    288       zIgnoreFlag = db_get("ignore-glob", 0);
   289    289     }
   290    290     db_multi_exec("CREATE TEMP TABLE sfile(x TEXT PRIMARY KEY)");
   291    291     n = strlen(g.zLocalRoot);
   292    292     blob_init(&path, g.zLocalRoot, n-1);

Changes to src/clone.c.

    62     62     url_parse(g.argv[2]);
    63     63     if( g.urlIsFile ){
    64     64       file_copy(g.urlName, g.argv[3]);
    65     65       db_close(1);
    66     66       db_open_repository(g.argv[3]);
    67     67       db_record_repository_filename(g.argv[3]);
    68     68       db_multi_exec(
    69         -      "REPLACE INTO config(name,value)"
    70         -      " VALUES('server-code', lower(hex(randomblob(20))));"
    71         -      "REPLACE INTO config(name,value)"
    72         -      " VALUES('last-sync-url', '%q');",
           69  +      "REPLACE INTO config(name,value,mtime)"
           70  +      " VALUES('server-code', lower(hex(randomblob(20))),now());"
           71  +      "REPLACE INTO config(name,value,mtime)"
           72  +      " VALUES('last-sync-url', '%q',now());",
    73     73         g.urlCanonical
    74     74       );
    75     75       db_multi_exec(
    76     76          "DELETE FROM blob WHERE rid IN private;"
    77     77          "DELETE FROM delta wHERE rid IN private;"
    78     78          "DELETE FROM private;"
    79     79       );
................................................................................
    90     90       db_record_repository_filename(g.argv[3]);
    91     91       db_initial_setup(0, zDefaultUser, 0);
    92     92       user_select();
    93     93       db_set("content-schema", CONTENT_SCHEMA, 0);
    94     94       db_set("aux-schema", AUX_SCHEMA, 0);
    95     95       db_set("last-sync-url", g.argv[2], 0);
    96     96       db_multi_exec(
    97         -      "REPLACE INTO config(name,value)"
    98         -      " VALUES('server-code', lower(hex(randomblob(20))));"
           97  +      "REPLACE INTO config(name,value,mtime)"
           98  +      " VALUES('server-code', lower(hex(randomblob(20))), now());"
    99     99       );
   100    100       url_enable_proxy(0);
   101    101       url_get_password_if_needed();
   102    102       g.xlinkClusterOnly = 1;
   103    103       nErr = client_sync(0,0,1,bPrivate,CONFIGSET_ALL,0);
   104    104       g.xlinkClusterOnly = 0;
   105    105       verify_cancel();

Changes to src/configure.c.

     1      1   /*
     2      2   ** Copyright (c) 2008 D. Richard Hipp
     3      3   **
     4      4   ** This program is free software; you can redistribute it and/or
     5      5   ** modify it under the terms of the Simplified BSD License (also
     6      6   ** known as the "2-Clause License" or "FreeBSD License".)
     7         -
            7  +**
     8      8   ** This program is distributed in the hope that it will be useful,
     9      9   ** but without any warranty; without even the implied warranty of
    10     10   ** merchantability or fitness for a particular purpose.
    11     11   **
    12     12   ** Author contact information:
    13     13   **   drh@hwaci.com
    14     14   **   http://www.hwaci.com/drh/
................................................................................
    25     25   #include <assert.h>
    26     26   
    27     27   #if INTERFACE
    28     28   /*
    29     29   ** Configuration transfers occur in groups.  These are the allowed
    30     30   ** groupings:
    31     31   */
    32         -#define CONFIGSET_SKIN   0x000001     /* WWW interface appearance */
    33         -#define CONFIGSET_TKT    0x000002     /* Ticket configuration */
    34         -#define CONFIGSET_PROJ   0x000004     /* Project name */
    35         -#define CONFIGSET_SHUN   0x000008     /* Shun settings */
    36         -#define CONFIGSET_USER   0x000010     /* The USER table */
    37         -#define CONFIGSET_ADDR   0x000020     /* The CONCEALED table */
           32  +#define CONFIGSET_SKIN      0x000001     /* WWW interface appearance */
           33  +#define CONFIGSET_TKT       0x000002     /* Ticket configuration */
           34  +#define CONFIGSET_PROJ      0x000004     /* Project name */
           35  +#define CONFIGSET_SHUN      0x000008     /* Shun settings */
           36  +#define CONFIGSET_USER      0x000010     /* The USER table */
           37  +#define CONFIGSET_ADDR      0x000020     /* The CONCEALED table */
    38     38   
    39         -#define CONFIGSET_ALL    0xffffff     /* Everything */
           39  +#define CONFIGSET_ALL       0x0000ff     /* Everything */
           40  +
           41  +#define CONFIGSET_OVERWRITE 0x100000     /* Causes overwrite instead of merge */
           42  +#define CONFIGSET_OLDFORMAT 0x200000     /* Use the legacy format */
    40     43   
    41     44   #endif /* INTERFACE */
    42     45   
    43     46   /*
    44     47   ** Names of the configuration sets
    45     48   */
    46     49   static struct {
    47     50     const char *zName;   /* Name of the configuration set */
    48     51     int groupMask;       /* Mask for that configuration set */
    49     52     const char *zHelp;   /* What it does */
    50     53   } aGroupName[] = {
    51         -  { "email",        CONFIGSET_ADDR,  "Concealed email addresses in tickets" },
    52         -  { "project",      CONFIGSET_PROJ,  "Project name and description"         },
    53         -  { "skin",         CONFIGSET_SKIN,  "Web interface apparance settings"     },
    54         -  { "shun",         CONFIGSET_SHUN,  "List of shunned artifacts"            },
    55         -  { "ticket",       CONFIGSET_TKT,   "Ticket setup",                        },
    56         -  { "user",         CONFIGSET_USER,  "Users and privilege settings"         },
    57         -  { "all",          CONFIGSET_ALL,   "All of the above"                     },
           54  +  { "/email",        CONFIGSET_ADDR,  "Concealed email addresses in tickets" },
           55  +  { "/project",      CONFIGSET_PROJ,  "Project name and description"         },
           56  +  { "/skin",         CONFIGSET_SKIN,  "Web interface apparance settings"     },
           57  +  { "/shun",         CONFIGSET_SHUN,  "List of shunned artifacts"            },
           58  +  { "/ticket",       CONFIGSET_TKT,   "Ticket setup",                        },
           59  +  { "/user",         CONFIGSET_USER,  "Users and privilege settings"         },
           60  +  { "/all",          CONFIGSET_ALL,   "All of the above"                     },
    58     61   };
    59     62   
    60     63   
    61     64   /*
    62     65   ** The following is a list of settings that we are willing to
    63     66   ** transfer.  
    64     67   **
................................................................................
   104    107   ** Return name of first configuration property matching the given mask.
   105    108   */
   106    109   const char *configure_first_name(int iMask){
   107    110     iConfig = 0;
   108    111     return configure_next_name(iMask);
   109    112   }
   110    113   const char *configure_next_name(int iMask){
   111         -  while( iConfig<count(aConfig) ){
   112         -    if( aConfig[iConfig].groupMask & iMask ){
   113         -      return aConfig[iConfig++].zName;
   114         -    }else{
   115         -      iConfig++;
          114  +  if( iMask & CONFIGSET_OLDFORMAT ){
          115  +    while( iConfig<count(aConfig) ){
          116  +      if( aConfig[iConfig].groupMask & iMask ){
          117  +        return aConfig[iConfig++].zName;
          118  +      }else{
          119  +        iConfig++;
          120  +      }
          121  +    }
          122  +  }else{
          123  +    if( iConfig==0 && (iMask & CONFIGSET_ALL)==CONFIGSET_ALL ){
          124  +      iConfig = count(aGroupName);
          125  +      return "/all";
          126  +    }
          127  +    while( iConfig<count(aGroupName)-1 ){
          128  +      if( aGroupName[iConfig].groupMask & iMask ){
          129  +        return aGroupName[iConfig++].zName;
          130  +      }else{
          131  +        iConfig++;
          132  +      }
   116    133       }
   117    134     }
   118    135     return 0;
   119    136   }
   120    137   
   121    138   /*
   122    139   ** Return the mask for the named configuration parameter if it can be
................................................................................
   125    142   ** "Safe" in the previous paragraph means the permission is created to
   126    143   ** export the property.  In other words, the requesting side has presented
   127    144   ** login credentials and has sufficient capabilities to access the requested
   128    145   ** information.
   129    146   */
   130    147   int configure_is_exportable(const char *zName){
   131    148     int i;
          149  +  int n = strlen(zName);
          150  +  if( n>2 && zName[0]=='\'' && zName[n-1]=='\'' ){
          151  +    zName++;
          152  +    n -= 2;
          153  +  }
   132    154     for(i=0; i<count(aConfig); i++){
   133         -    if( fossil_strcmp(zName, aConfig[i].zName)==0 ){
          155  +    if( memcmp(zName, aConfig[i].zName, n)==0 && aConfig[i].zName[n]==0 ){
   134    156         int m = aConfig[i].groupMask;
   135    157         if( !g.okAdmin ){
   136    158           m &= ~CONFIGSET_USER;
   137    159         }
   138    160         if( !g.okRdAddr ){
   139    161           m &= ~CONFIGSET_ADDR;
   140    162         }
................................................................................
   198    220       db_finalize(&q);
   199    221     }
   200    222   }
   201    223   
   202    224   /*
   203    225   ** Two SQL functions:
   204    226   **
   205         -**        flag_test(int)
   206         -**        flag_clear(int)
          227  +**        config_is_reset(int)
          228  +**        config_reset(int)
   207    229   **
   208         -** The flag_test() function takes the integer valued argument and
   209         -** ANDs it against the static variable "flag_value" below.  The
   210         -** function returns TRUE or false depending on the result.  The
   211         -** flag_clear() function masks off the bits from "flag_value" that
          230  +** The config_is_reset() function takes the integer valued argument and
          231  +** ANDs it against the static variable "configHasBeenReset" below.  The
          232  +** function returns TRUE or FALSE depending on the result depending on 
          233  +** whether or not the corresponding configuration table has been reset.  The
          234  +** config_reset() function adds the bits to "configHasBeenReset" that
   212    235   ** are given in the argument.
   213    236   **
   214    237   ** These functions are used below in the WHEN clause of a trigger to
   215    238   ** get the trigger to fire exactly once.
   216    239   */
   217         -static int flag_value = 0xffff;
   218         -static void flag_test_function(
          240  +static int configHasBeenReset = 0;
          241  +static void config_is_reset_function(
   219    242     sqlite3_context *context,
   220    243     int argc,
   221    244     sqlite3_value **argv
   222    245   ){
   223    246     int m = sqlite3_value_int(argv[0]);
   224         -  sqlite3_result_int(context, (flag_value&m)!=0 );
          247  +  sqlite3_result_int(context, (configHasBeenReset&m)!=0 );
   225    248   }
   226         -static void flag_clear_function(
          249  +static void config_reset_function(
   227    250     sqlite3_context *context,
   228    251     int argc,
   229    252     sqlite3_value **argv
   230    253   ){
   231    254     int m = sqlite3_value_int(argv[0]);
   232         -  flag_value &= ~m;
          255  +  configHasBeenReset |= m;
   233    256   }
   234    257   
   235    258   /*
   236    259   ** Create the temporary _xfer_reportfmt and _xfer_user tables that are
   237    260   ** necessary in order to evalute the SQL text generated by the
   238    261   ** configure_render_special_name() routine.
   239    262   **
................................................................................
   259    282       @   cap TEXT,                       -- Capabilities of this user
   260    283       @   cookie TEXT,                    -- WWW login cookie
   261    284       @   ipaddr TEXT,                    -- IP address for which cookie is valid
   262    285       @   cexpire DATETIME,               -- Time when cookie expires
   263    286       @   info TEXT,                      -- contact information
   264    287       @   photo BLOB                      -- JPEG image of this user
   265    288       @ );
   266         -    @ INSERT INTO _xfer_reportfmt SELECT * FROM reportfmt;
   267         -    @ INSERT INTO _xfer_user SELECT * FROM user;
          289  +    @ INSERT INTO _xfer_reportfmt
          290  +    @    SELECT rn,owner,title,cols,sqlcode FROM reportfmt;
          291  +    @ INSERT INTO _xfer_user
          292  +    @    SELECT uid,login,pw,cap,cookie,ipaddr,cexpire,info,photo FROM user;
   268    293     ;
   269    294     db_multi_exec(zSQL1);
   270    295     
   271    296     /* When the replace flag is set, add triggers that run the first time
   272    297     ** that new data is seen.  The triggers run only once and delete all the
   273    298     ** existing data.
   274    299     */
   275    300     if( replaceFlag ){
   276    301       static const char zSQL2[] =
   277    302         @ CREATE TRIGGER _xfer_r1 BEFORE INSERT ON _xfer_reportfmt
   278         -      @ WHEN flag_test(1) BEGIN
          303  +      @ WHEN NOT config_is_reset(2) BEGIN
   279    304         @   DELETE FROM _xfer_reportfmt;
   280         -      @   SELECT flag_clear(1);
          305  +      @   SELECT config_reset(2);
   281    306         @ END;
   282    307         @ CREATE TRIGGER _xfer_r2 BEFORE INSERT ON _xfer_user
   283         -      @ WHEN flag_test(2) BEGIN
          308  +      @ WHEN NOT config_is_reset(16) BEGIN
   284    309         @   DELETE FROM _xfer_user;
   285         -      @   SELECT flag_clear(2);
          310  +      @   SELECT config_reset(16);
   286    311         @ END;
   287    312         @ CREATE TEMP TRIGGER _xfer_r3 BEFORE INSERT ON shun
   288         -      @ WHEN flag_test(4) BEGIN
          313  +      @ WHEN NOT config_is_reset(8) BEGIN
   289    314         @   DELETE FROM shun;
   290         -      @   SELECT flag_clear(4);
          315  +      @   SELECT config_reset(8);
   291    316         @ END;
   292    317       ;
   293         -    sqlite3_create_function(g.db, "flag_test", 1, SQLITE_UTF8, 0,
   294         -         flag_test_function, 0, 0);
   295         -    sqlite3_create_function(g.db, "flag_clear", 1, SQLITE_UTF8, 0,
   296         -         flag_clear_function, 0, 0);
   297         -    flag_value = 0xffff;
          318  +    sqlite3_create_function(g.db, "config_is_reset", 1, SQLITE_UTF8, 0,
          319  +         config_is_reset_function, 0, 0);
          320  +    sqlite3_create_function(g.db, "config_reset", 1, SQLITE_UTF8, 0,
          321  +         config_reset_function, 0, 0);
          322  +    configHasBeenReset = 0;
   298    323       db_multi_exec(zSQL2);
   299    324     }
   300    325   }
          326  +
          327  +/*
          328  +** After receiving configuration data, call this routine to transfer
          329  +** the results into the main database.
          330  +*/
          331  +void configure_finalize_receive(void){
          332  +  static const char zSQL[] =
          333  +    @ DELETE FROM user;
          334  +    @ INSERT INTO user SELECT * FROM _xfer_user;
          335  +    @ DELETE FROM reportfmt;
          336  +    @ INSERT INTO reportfmt SELECT * FROM _xfer_reportfmt;
          337  +    @ DROP TABLE _xfer_user;
          338  +    @ DROP TABLE _xfer_reportfmt;
          339  +  ;
          340  +  db_multi_exec(zSQL);
          341  +}
          342  +
          343  +/*
          344  +** Return true if z[] is not a "safe" SQL token.  A safe token is one of:
          345  +**
          346  +**   *   A string literal
          347  +**   *   A blob literal
          348  +**   *   An integer literal  (no floating point)
          349  +**   *   NULL
          350  +*/
          351  +static int safeSql(const char *z){
          352  +  int i;
          353  +  if( z==0 || z[0]==0 ) return 0;
          354  +  if( (z[0]=='x' || z[0]=='X') && z[1]=='\'' ) z++;
          355  +  if( z[0]=='\'' ){
          356  +    for(i=1; z[i]; i++){
          357  +      if( z[i]=='\'' ){
          358  +        i++;
          359  +        if( z[i]=='\'' ){ continue; }
          360  +        return z[i]==0;
          361  +      }
          362  +    }
          363  +    return 0;
          364  +  }else{
          365  +    char c;
          366  +    for(i=0; (c = z[i])!=0; i++){
          367  +      if( !fossil_isalnum(c) ) return 0;
          368  +    }
          369  +  }
          370  +  return 1;
          371  +}
          372  +
          373  +/*
          374  +** Return true if z[] consists of nothing but digits
          375  +*/
          376  +static int safeInt(const char *z){
          377  +  int i;
          378  +  if( z==0 || z[0]==0 ) return 0;
          379  +  for(i=0; fossil_isdigit(z[i]); i++){}
          380  +  return z[i]==0;
          381  +}
   301    382   
   302    383   /*
   303    384   ** Process a single "config" card received from the other side of a
   304    385   ** sync session.
   305    386   **
   306    387   ** Mask consists of one or more CONFIGSET_* values ORed together, to
   307    388   ** designate what types of configuration we are allowed to receive.
................................................................................
   342    423   ** SQL that is evaluated.  Note that the raw SQL in CONTENT might not
   343    424   ** insert directly into the target table but might instead use a proxy
   344    425   ** table like _fer_reportfmt or _xfer_user.  Such tables must be created
   345    426   ** ahead of time using configure_prepare_to_receive().  Then after multiple
   346    427   ** calls to this routine, configure_finalize_receive() to transfer the
   347    428   ** information received into the true target table.
   348    429   */
   349         -void configure_receive(const char *zName, Blob *pContent, int mask){
   350         -  if( (configure_is_exportable(zName) & mask)==0 ) return;
   351         -  if( strcmp(zName, "logo-image")==0 ){
   352         -    Stmt ins;
   353         -    db_prepare(&ins,
   354         -      "REPLACE INTO config(name, value) VALUES(:name, :value)"
   355         -    );
   356         -    db_bind_text(&ins, ":name", zName);
   357         -    db_bind_blob(&ins, ":value", pContent);
   358         -    db_step(&ins);
   359         -    db_finalize(&ins);
   360         -  }else if( zName[0]=='@' ){
   361         -    /* Notice that we are evaluating arbitrary SQL received from the
   362         -    ** client.  But this can only happen if the client has authenticated
   363         -    ** as an administrator, so presumably we trust the client at this
   364         -    ** point.
   365         -    */
   366         -    db_multi_exec("%s", blob_str(pContent));
          430  +void configure_receive(const char *zName, Blob *pContent, int groupMask){
          431  +  if( zName[0]=='/' ){
          432  +    /* The new format */
          433  +    char *azToken[12];
          434  +    int nToken = 0;
          435  +    int ii, jj;
          436  +    int thisMask;
          437  +    Blob name, value, sql;
          438  +    static const struct receiveType {
          439  +      const char *zName;
          440  +      const char *zPrimKey;
          441  +      int nField;
          442  +      const char *azField[4];
          443  +    } aType[] = {
          444  +      { "/config",    "name",  1, { "value", 0, 0, 0 }              },
          445  +      { "@user",      "login", 4, { "pw", "cap", "info", "photo" }  },
          446  +      { "@shun",      "uuid",  1, { "scom", 0, 0, 0 }               },
          447  +      { "@reportfmt", "title", 3, { "owner", "cols", "sqlcode", 0 } },
          448  +      { "@concealed", "hash",  1, { "content", 0, 0, 0 }            },
          449  +    };
          450  +    for(ii=0; ii<count(aType); ii++){
          451  +      if( fossil_strcmp(&aType[ii].zName[1],&zName[1])==0 ) break;
          452  +    }
          453  +    if( ii>=count(aType) ) return;
          454  +    while( blob_token(pContent, &name) && blob_sqltoken(pContent, &value) ){
          455  +      char *z = blob_terminate(&name);
          456  +      if( !safeSql(z) ) return;
          457  +      if( nToken>0 ){
          458  +        for(jj=0; jj<aType[ii].nField; jj++){
          459  +          if( fossil_strcmp(aType[ii].azField[jj], z)==0 ) break;
          460  +        }
          461  +        if( jj>=aType[ii].nField ) continue;
          462  +      }else{
          463  +        if( !safeInt(z) ) return;
          464  +      }
          465  +      azToken[nToken++] = z;
          466  +      azToken[nToken++] = z = blob_terminate(&value);
          467  +      if( !safeSql(z) ) return;
          468  +      if( nToken>=count(azToken) ) break;
          469  +    }
          470  +    if( nToken<2 ) return;
          471  +    if( aType[ii].zName[0]=='/' ){
          472  +      thisMask = configure_is_exportable(azToken[1]);
          473  +    }else{
          474  +      thisMask = configure_is_exportable(aType[ii].zName);
          475  +    }
          476  +    if( (thisMask & groupMask)==0 ) return;
          477  +    
          478  +    blob_zero(&sql);
          479  +    if( groupMask & CONFIGSET_OVERWRITE ){
          480  +      if( (thisMask & configHasBeenReset)==0 && aType[ii].zName[0]!='/' ){
          481  +        db_multi_exec("DELETE FROM %s", &aType[ii].zName[1]);
          482  +        configHasBeenReset |= thisMask;
          483  +      }
          484  +      blob_append(&sql, "REPLACE INTO ", -1);
          485  +    }else{
          486  +      blob_append(&sql, "INSERT OR IGNORE INTO ", -1);
          487  +    }
          488  +    blob_appendf(&sql, "%s(%s, mtime", &zName[1], aType[ii].zPrimKey);
          489  +    for(jj=2; jj<nToken; jj+=2){
          490  +       blob_appendf(&sql, ",%s", azToken[jj]);
          491  +    }
          492  +    blob_appendf(&sql,") VALUES(%s,%s", azToken[1], azToken[0]);
          493  +    for(jj=2; jj<nToken; jj+=2){
          494  +       blob_appendf(&sql, ",%s", azToken[jj+1]);
          495  +    }
          496  +    db_multi_exec("%s)", blob_str(&sql));
          497  +    if( db_changes()==0 ){
          498  +      blob_reset(&sql);
          499  +      blob_appendf(&sql, "UPDATE %s SET mtime=%s", &zName[1], azToken[0]);
          500  +      for(jj=2; jj<nToken; jj+=2){
          501  +        blob_appendf(&sql, ", %s=%s", azToken[jj], azToken[jj+1]);
          502  +      }
          503  +      blob_appendf(&sql, " WHERE %s=%s AND mtime<%s",
          504  +                   aType[ii].zPrimKey, azToken[1], azToken[0]);
          505  +      db_multi_exec("%s", blob_str(&sql));
          506  +    }
          507  +    blob_reset(&sql);
   367    508     }else{
   368         -    db_multi_exec(
   369         -       "REPLACE INTO config(name,value) VALUES(%Q,%Q)",
   370         -       zName, blob_str(pContent)
   371         -    );
          509  +    /* Otherwise, the old format */
          510  +    if( (configure_is_exportable(zName) & groupMask)==0 ) return;
          511  +    if( strcmp(zName, "logo-image")==0 ){
          512  +      Stmt ins;
          513  +      db_prepare(&ins,
          514  +        "REPLACE INTO config(name, value, mtime) VALUES(:name, :value, now())"
          515  +      );
          516  +      db_bind_text(&ins, ":name", zName);
          517  +      db_bind_blob(&ins, ":value", pContent);
          518  +      db_step(&ins);
          519  +      db_finalize(&ins);
          520  +    }else if( zName[0]=='@' ){
          521  +      /* Notice that we are evaluating arbitrary SQL received from the
          522  +      ** client.  But this can only happen if the client has authenticated
          523  +      ** as an administrator, so presumably we trust the client at this
          524  +      ** point.
          525  +      */
          526  +      db_multi_exec("%s", blob_str(pContent));
          527  +    }else{
          528  +      db_multi_exec(
          529  +         "REPLACE INTO config(name,value,mtime) VALUES(%Q,%Q,now())",
          530  +         zName, blob_str(pContent)
          531  +      );
          532  +    }
          533  +  }
          534  +}
          535  +
          536  +/*
          537  +** Process a file full of "config" cards.
          538  +*/
          539  +void configure_receive_all(Blob *pIn, int groupMask){
          540  +  Blob line;
          541  +  int nToken;
          542  +  int size;
          543  +  Blob aToken[4];
          544  +
          545  +  configHasBeenReset = 0;
          546  +  while( blob_line(pIn, &line) ){
          547  +    if( blob_buffer(&line)[0]=='#' ) continue;
          548  +    nToken = blob_tokenize(&line, aToken, count(aToken));
          549  +    if( blob_eq(&aToken[0],"config")
          550  +     && nToken==3
          551  +     && blob_is_int(&aToken[2], &size)
          552  +    ){
          553  +      const char *zName = blob_str(&aToken[1]);
          554  +      Blob content;
          555  +      blob_zero(&content);
          556  +      blob_extract(pIn, size, &content);
          557  +      g.okAdmin = g.okRdAddr = 1;
          558  +      configure_receive(zName, &content, groupMask);
          559  +      blob_reset(&content);
          560  +      blob_seek(pIn, 1, BLOB_SEEK_CUR);
          561  +    }
   372    562     }
   373    563   }
   374         -
          564  +    
   375    565   
   376    566   /*
   377         -** After receiving configuration data, call this routine to transfer
   378         -** the results into the main database.
          567  +** Send "config" cards using the new format for all elements of a group
          568  +** that have recently changed.
          569  +**
          570  +** Output goes into pOut.  The groupMask identifies the group(s) to be sent.
          571  +** Send only entries whose timestamp is later than or equal to iStart.
          572  +**
          573  +** Return the number of cards sent.
   379    574   */
   380         -void configure_finalize_receive(void){
   381         -  static const char zSQL[] =
   382         -    @ DELETE FROM user;
   383         -    @ INSERT INTO user SELECT * FROM _xfer_user;
   384         -    @ DELETE FROM reportfmt;
   385         -    @ INSERT INTO reportfmt SELECT * FROM _xfer_reportfmt;
   386         -    @ DROP TABLE _xfer_user;
   387         -    @ DROP TABLE _xfer_reportfmt;
   388         -  ;
   389         -  db_multi_exec(zSQL);
          575  +int configure_send_group(
          576  +  Blob *pOut,              /* Write output here */
          577  +  int groupMask,           /* Mask of groups to be send */
          578  +  sqlite3_int64 iStart     /* Only write values changed since this time */
          579  +){
          580  +  Stmt q;
          581  +  Blob rec;
          582  +  int ii;
          583  +  int nCard = 0;
          584  +
          585  +  blob_zero(&rec);
          586  +  if( groupMask & CONFIGSET_SHUN ){
          587  +    db_prepare(&q, "SELECT mtime, quote(uuid), quote(scom) FROM shun"
          588  +                   " WHERE mtime>=%lld", iStart);
          589  +    while( db_step(&q)==SQLITE_ROW ){
          590  +      blob_appendf(&rec,"%s %s scom %s",
          591  +        db_column_text(&q, 0),
          592  +        db_column_text(&q, 1),
          593  +        db_column_text(&q, 2)
          594  +      );
          595  +      blob_appendf(pOut, "config /shun %d\n%s\n",
          596  +                   blob_size(&rec), blob_str(&rec));
          597  +      nCard++;
          598  +      blob_reset(&rec);
          599  +    }
          600  +    db_finalize(&q);
          601  +  }
          602  +  if( groupMask & CONFIGSET_USER ){
          603  +    db_prepare(&q, "SELECT mtime, quote(login), quote(pw), quote(cap),"
          604  +                   "       quote(info), quote(photo) FROM user"
          605  +                   " WHERE mtime>=%lld", iStart);
          606  +    while( db_step(&q)==SQLITE_ROW ){
          607  +      blob_appendf(&rec,"%s %s pw %s cap %s info %s photo %s",
          608  +        db_column_text(&q, 0),
          609  +        db_column_text(&q, 1),
          610  +        db_column_text(&q, 2),
          611  +        db_column_text(&q, 3),
          612  +        db_column_text(&q, 4),
          613  +        db_column_text(&q, 5)
          614  +      );
          615  +      blob_appendf(pOut, "config /user %d\n%s\n",
          616  +                   blob_size(&rec), blob_str(&rec));
          617  +      nCard++;
          618  +      blob_reset(&rec);
          619  +    }
          620  +    db_finalize(&q);
          621  +  }
          622  +  if( groupMask & CONFIGSET_TKT ){
          623  +    db_prepare(&q, "SELECT mtime, quote(title), quote(owner), quote(cols),"
          624  +                   "       quote(sqlcode) FROM reportfmt"
          625  +                   " WHERE mtime>=%lld", iStart);
          626  +    while( db_step(&q)==SQLITE_ROW ){
          627  +      blob_appendf(&rec,"%s %s owner %s cols %s sqlcode %s",
          628  +        db_column_text(&q, 0),
          629  +        db_column_text(&q, 1),
          630  +        db_column_text(&q, 2),
          631  +        db_column_text(&q, 3),
          632  +        db_column_text(&q, 4)
          633  +      );
          634  +      blob_appendf(pOut, "config /reportfmt %d\n%s\n",
          635  +                   blob_size(&rec), blob_str(&rec));
          636  +      nCard++;
          637  +      blob_reset(&rec);
          638  +    }
          639  +    db_finalize(&q);
          640  +  }
          641  +  if( groupMask & CONFIGSET_ADDR ){
          642  +    db_prepare(&q, "SELECT mtime, quote(hash), quote(content) FROM concealed"
          643  +                   " WHERE mtime>=%lld", iStart);
          644  +    while( db_step(&q)==SQLITE_ROW ){
          645  +      blob_appendf(&rec,"%s %s content %s",
          646  +        db_column_text(&q, 0),
          647  +        db_column_text(&q, 1),
          648  +        db_column_text(&q, 2)
          649  +      );
          650  +      blob_appendf(pOut, "config /concealed %d\n%s\n",
          651  +                   blob_size(&rec), blob_str(&rec));
          652  +      nCard++;
          653  +      blob_reset(&rec);
          654  +    }
          655  +    db_finalize(&q);
          656  +  }
          657  +  db_prepare(&q, "SELECT mtime, quote(name), quote(value) FROM config"
          658  +                 " WHERE name=:name AND mtime>=%lld", iStart);
          659  +  for(ii=0; ii<count(aConfig); ii++){
          660  +    if( (aConfig[ii].groupMask & groupMask)!=0 && aConfig[ii].zName[0]!='@' ){
          661  +      db_bind_text(&q, ":name", aConfig[ii].zName);
          662  +      while( db_step(&q)==SQLITE_ROW ){
          663  +        blob_appendf(&rec,"%s %s value %s",
          664  +          db_column_text(&q, 0),
          665  +          db_column_text(&q, 1),
          666  +          db_column_text(&q, 2)
          667  +        );
          668  +        blob_appendf(pOut, "config /config %d\n%s\n",
          669  +                     blob_size(&rec), blob_str(&rec));
          670  +        nCard++;
          671  +        blob_reset(&rec);
          672  +      }
          673  +      db_reset(&q);
          674  +    }
          675  +  }
          676  +  db_finalize(&q);
          677  +  return nCard;
   390    678   }
   391    679   
   392    680   /*
   393    681   ** Identify a configuration group by name.  Return its mask.
   394    682   ** Throw an error if no match.
   395    683   */
   396         -static int find_area(const char *z){
          684  +int configure_name_to_mask(const char *z, int notFoundIsFatal){
   397    685     int i;
   398    686     int n = strlen(z);
   399    687     for(i=0; i<count(aGroupName); i++){
   400         -    if( strncmp(z, aGroupName[i].zName, n)==0 ){
          688  +    if( strncmp(z, &aGroupName[i].zName[1], n)==0 ){
   401    689         return aGroupName[i].groupMask;
   402    690       }
   403    691     }
   404         -  printf("Available configuration areas:\n");
   405         -  for(i=0; i<count(aGroupName); i++){
   406         -    printf("  %-10s %s\n", aGroupName[i].zName, aGroupName[i].zHelp);
          692  +  if( notFoundIsFatal ){
          693  +    printf("Available configuration areas:\n");
          694  +    for(i=0; i<count(aGroupName); i++){
          695  +      printf("  %-10s %s\n", &aGroupName[i].zName[1], aGroupName[i].zHelp);
          696  +    }
          697  +    fossil_fatal("no such configuration area: \"%s\"", z);
   407    698     }
   408         -  fossil_fatal("no such configuration area: \"%s\"", z);
   409    699     return 0;
   410    700   }
   411    701   
   412    702   /*
   413    703   ** Write SQL text into file zFilename that will restore the configuration
   414    704   ** area identified by mask to its current state from any other state.
   415    705   */
   416    706   static void export_config(
   417         -  int mask,                 /* Mask indicating which configuration to export */
          707  +  int groupMask,            /* Mask indicating which configuration to export */
   418    708     const char *zMask,        /* Name of the configuration */
          709  +  sqlite3_int64 iStart,     /* Start date */
   419    710     const char *zFilename     /* Write into this file */
   420    711   ){
   421         -  int i;
   422    712     Blob out;
   423    713     blob_zero(&out);
   424    714     blob_appendf(&out, 
   425         -    "-- The \"%s\" configuration exported from\n"
   426         -    "-- repository \"%s\"\n"
   427         -    "-- on %s\n",
          715  +    "# The \"%s\" configuration exported from\n"
          716  +    "# repository \"%s\"\n"
          717  +    "# on %s\n",
   428    718       zMask, g.zRepositoryName,
   429    719       db_text(0, "SELECT datetime('now')")
   430    720     );
   431         -  for(i=0; i<count(aConfig); i++){
   432         -    if( (aConfig[i].groupMask & mask)!=0 ){
   433         -      const char *zName = aConfig[i].zName;
   434         -      if( zName[0]!='@' ){
   435         -        char *zValue = db_text(0, 
   436         -            "SELECT quote(value) FROM config WHERE name=%Q", zName);
   437         -        if( zValue ){
   438         -          blob_appendf(&out,"REPLACE INTO config VALUES(%Q,%s);\n", 
   439         -                       zName, zValue);
   440         -        }
   441         -        free(zValue);
   442         -      }else{
   443         -        configure_render_special_name(zName, &out);
   444         -      }
   445         -    }
   446         -  }
          721  +  configure_send_group(&out, groupMask, iStart);
   447    722     blob_write_to_file(&out, zFilename);
   448    723     blob_reset(&out);
   449    724   }
   450    725   
   451    726   
   452    727   /*
   453    728   ** COMMAND: configuration
................................................................................
   473    748   **         the current configuration.  Existing values take priority over
   474    749   **         values read from FILENAME.
   475    750   **
   476    751   **    %fossil configuration pull AREA ?URL?
   477    752   **
   478    753   **         Pull and install the configuration from a different server
   479    754   **         identified by URL.  If no URL is specified, then the default
   480         -**         server is used. 
          755  +**         server is used. Use the --legacy option for the older protocol
          756  +**         (when talking to servers compiled prior to 2011-04-27.)  Use
          757  +**         the --overwrite flag to completely replace local settings with
          758  +**         content received from URL.
   481    759   **
   482    760   **    %fossil configuration push AREA ?URL?
   483    761   **
   484    762   **         Push the local configuration into the remote server identified
   485    763   **         by URL.  Admin privilege is required on the remote server for
   486         -**         this to work.
          764  +**         this to work.  When the same record exists both locally and on
          765  +**         the remote end, the one that was most recently changed wins.
          766  +**         Use the --legacy flag when talking to holder servers.
   487    767   **
   488    768   **    %fossil configuration reset AREA
   489    769   **
   490    770   **         Restore the configuration to the default.  AREA as above.
   491    771   **
   492         -** WARNING: Do not import, merge, or pull configurations from an untrusted
   493         -** source.  The inbound configuration is not checked for safety and can
   494         -** introduce security vulnerabilities.
          772  +**    %fossil configuration sync AREA ?URL?
          773  +**
          774  +**         Synchronize configuration changes in the local repository with
          775  +**         the remote repository at URL.  
   495    776   */
   496    777   void configuration_cmd(void){
   497    778     int n;
   498    779     const char *zMethod;
   499    780     if( g.argc<3 ){
   500    781       usage("export|import|merge|pull|reset ...");
   501    782     }
   502    783     db_find_and_open_repository(0, 0);
   503    784     zMethod = g.argv[2];
   504    785     n = strlen(zMethod);
   505    786     if( strncmp(zMethod, "export", n)==0 ){
   506    787       int mask;
          788  +    const char *zSince = find_option("since",0,1);
          789  +    sqlite3_int64 iStart;
   507    790       if( g.argc!=5 ){
   508    791         usage("export AREA FILENAME");
   509    792       }
   510         -    mask = find_area(g.argv[3]);
   511         -    export_config(mask, g.argv[3], g.argv[4]);
          793  +    mask = configure_name_to_mask(g.argv[3], 1);
          794  +    if( zSince ){
          795  +      iStart = db_multi_exec(
          796  +         "SELECT coalesce(strftime('%%s',%Q),strftime('%%s','now',%Q))+0",
          797  +         zSince, zSince
          798  +      );
          799  +    }else{
          800  +      iStart = 0;
          801  +    }
          802  +    export_config(mask, g.argv[3], iStart, g.argv[4]);
   512    803     }else
   513    804     if( strncmp(zMethod, "import", n)==0 
   514    805          || strncmp(zMethod, "merge", n)==0 ){
   515    806       Blob in;
          807  +    int groupMask;
   516    808       if( g.argc!=4 ) usage(mprintf("%s FILENAME",zMethod));
   517    809       blob_read_from_file(&in, g.argv[3]);
   518    810       db_begin_transaction();
   519         -    configure_prepare_to_receive(zMethod[0]=='i');
   520         -    db_multi_exec("%s", blob_str(&in));
   521         -    configure_finalize_receive();
          811  +    if( zMethod[0]=='i' ){
          812  +      groupMask = CONFIGSET_ALL | CONFIGSET_OVERWRITE;
          813  +    }else{
          814  +      groupMask = CONFIGSET_ALL;
          815  +    }
          816  +    configure_receive_all(&in, groupMask);
   522    817       db_end_transaction(0);
   523    818     }else
   524         -  if( strncmp(zMethod, "pull", n)==0 || strncmp(zMethod, "push", n)==0 ){
          819  +  if( strncmp(zMethod, "pull", n)==0
          820  +   || strncmp(zMethod, "push", n)==0
          821  +   || strncmp(zMethod, "sync", n)==0
          822  +  ){
   525    823       int mask;
   526    824       const char *zServer;
   527    825       const char *zPw;
          826  +    int legacyFlag = 0;
          827  +    int overwriteFlag = 0;
          828  +    if( zMethod[0]!='s' ) legacyFlag = find_option("legacy",0,0)!=0;
          829  +    if( strncmp(zMethod,"pull",n)==0 ){
          830  +      overwriteFlag = find_option("overwrite",0,0)!=0;
          831  +    }
   528    832       url_proxy_options();
   529    833       if( g.argc!=4 && g.argc!=5 ){
   530    834         usage("pull AREA ?URL?");
   531    835       }
   532         -    mask = find_area(g.argv[3]);
          836  +    mask = configure_name_to_mask(g.argv[3], 1);
   533    837       if( g.argc==5 ){
   534    838         zServer = g.argv[4];
   535    839         zPw = 0;
   536    840         g.dontKeepUrl = 1;
   537    841       }else{
   538    842         zServer = db_get("last-sync-url", 0);
   539    843         if( zServer==0 ){
................................................................................
   541    845         }
   542    846         zPw = unobscure(db_get("last-sync-pw", 0));
   543    847       }
   544    848       url_parse(zServer);
   545    849       if( g.urlPasswd==0 && zPw ) g.urlPasswd = mprintf("%s", zPw);
   546    850       user_select();
   547    851       url_enable_proxy("via proxy: ");
          852  +    if( legacyFlag ) mask |= CONFIGSET_OLDFORMAT;
          853  +    if( overwriteFlag ) mask |= CONFIGSET_OVERWRITE;
   548    854       if( strncmp(zMethod, "push", n)==0 ){
   549    855         client_sync(0,0,0,0,0,mask);
   550         -    }else{
          856  +    }else if( strncmp(zMethod, "pull", n)==0 ){
   551    857         client_sync(0,0,0,0,mask,0);
          858  +    }else{
          859  +      client_sync(0,0,0,0,mask,mask);
   552    860       }
   553    861     }else
   554    862     if( strncmp(zMethod, "reset", n)==0 ){
   555    863       int mask, i;
   556    864       char *zBackup;
   557    865       if( g.argc!=4 ) usage("reset AREA");
   558         -    mask = find_area(g.argv[3]);
          866  +    mask = configure_name_to_mask(g.argv[3], 1);
   559    867       zBackup = db_text(0, 
   560    868          "SELECT strftime('config-backup-%%Y%%m%%d%%H%%M%%f','now')");
   561    869       db_begin_transaction();
   562         -    export_config(mask, g.argv[3], zBackup);
          870  +    export_config(mask, g.argv[3], 0, zBackup);
   563    871       for(i=0; i<count(aConfig); i++){
   564    872         const char *zName = aConfig[i].zName;
   565    873         if( (aConfig[i].groupMask & mask)==0 ) continue;
   566    874         if( zName[0]!='@' ){
   567    875           db_multi_exec("DELETE FROM config WHERE name=%Q", zName);
   568    876         }else if( fossil_strcmp(zName,"@user")==0 ){
   569    877           db_multi_exec("DELETE FROM user");

Changes to src/db.c.

    32     32   #if ! defined(_WIN32)
    33     33   #  include <pwd.h>
    34     34   #endif
    35     35   #include <sqlite3.h>
    36     36   #include <sys/types.h>
    37     37   #include <sys/stat.h>
    38     38   #include <unistd.h>
           39  +#include <time.h>
    39     40   #include "db.h"
    40     41   
    41     42   #if INTERFACE
    42     43   /*
    43     44   ** An single SQL statement is represented as an instance of the following
    44     45   ** structure.
    45     46   */
................................................................................
   604    605         db_err(sqlite3_errmsg(db));
   605    606       }
   606    607     }
   607    608     va_end(ap);
   608    609     sqlite3_exec(db, "COMMIT", 0, 0, 0);
   609    610     sqlite3_close(db);
   610    611   }
          612  +
          613  +/*
          614  +** Function to return the number of seconds since 1970.  This is
          615  +** the same as strftime('%s','now') but is more compact.
          616  +*/
          617  +static void db_now_function(
          618  +  sqlite3_context *context,
          619  +  int argc,
          620  +  sqlite3_value **argv
          621  +){
          622  +  sqlite3_result_int64(context, time(0));
          623  +}
          624  +
   611    625   
   612    626   /*
   613    627   ** Open a database file.  Return a pointer to the new database
   614    628   ** connection.  An error results in process abort.
   615    629   */
   616    630   static sqlite3 *openDatabase(const char *zDbName){
   617    631     int rc;
................................................................................
   628    642          zVfs
   629    643     );
   630    644     if( rc!=SQLITE_OK ){
   631    645       db_err(sqlite3_errmsg(db));
   632    646     }
   633    647     sqlite3_busy_timeout(db, 5000); 
   634    648     sqlite3_wal_autocheckpoint(db, 1);  /* Set to checkpoint frequently */
          649  +  sqlite3_create_function(db, "now", 0, SQLITE_ANY, 0, db_now_function, 0, 0);
   635    650     return db;
   636    651   }
   637    652   
   638    653   
   639    654   /*
   640    655   ** zDbName is the name of a database file.  If no other database
   641    656   ** file is open, then open this one.  If another database file is
................................................................................
  1103   1118     Blob hash;
  1104   1119     Blob manifest;
  1105   1120   
  1106   1121     db_set("content-schema", CONTENT_SCHEMA, 0);
  1107   1122     db_set("aux-schema", AUX_SCHEMA, 0);
  1108   1123     if( makeServerCodes ){
  1109   1124       db_multi_exec(
  1110         -      "INSERT INTO config(name,value)"
  1111         -      " VALUES('server-code', lower(hex(randomblob(20))));"
  1112         -      "INSERT INTO config(name,value)"
  1113         -      " VALUES('project-code', lower(hex(randomblob(20))));"
         1125  +      "INSERT INTO config(name,value,mtime)"
         1126  +      " VALUES('server-code', lower(hex(randomblob(20))),now());"
         1127  +      "INSERT INTO config(name,value,mtime)"
         1128  +      " VALUES('project-code', lower(hex(randomblob(20))),now());"
  1114   1129       );
  1115   1130     }
  1116   1131     if( !db_is_global("autosync") ) db_set_int("autosync", 1, 0);
  1117   1132     if( !db_is_global("localauth") ) db_set_int("localauth", 0, 0);
  1118   1133     db_create_default_users(0, zDefaultUser);
  1119   1134     user_select();
  1120   1135   
................................................................................
  1293   1308       zHash[n] = 0;
  1294   1309     }else{
  1295   1310       sha1sum_step_text(zContent, n);
  1296   1311       sha1sum_finish(&out);
  1297   1312       sqlite3_snprintf(sizeof(zHash), zHash, "%s", blob_str(&out));
  1298   1313       blob_reset(&out);
  1299   1314       db_multi_exec(
  1300         -       "INSERT OR IGNORE INTO concealed VALUES(%Q,%#Q)",
         1315  +       "INSERT OR IGNORE INTO concealed(hash,content,mtime)"
         1316  +       " VALUES(%Q,%#Q,now())",
  1301   1317          zHash, n, zContent
  1302   1318       );
  1303   1319     }
  1304   1320     return zHash;
  1305   1321   }
  1306   1322   
  1307   1323   /*
................................................................................
  1404   1420     db_begin_transaction();
  1405   1421     if( globalFlag ){
  1406   1422       db_swap_connections();
  1407   1423       db_multi_exec("REPLACE INTO global_config(name,value) VALUES(%Q,%Q)",
  1408   1424                      zName, zValue);
  1409   1425       db_swap_connections();
  1410   1426     }else{
  1411         -    db_multi_exec("REPLACE INTO config(name,value) VALUES(%Q,%Q)",
         1427  +    db_multi_exec("REPLACE INTO config(name,value,mtime) VALUES(%Q,%Q,now())",
  1412   1428                      zName, zValue);
  1413   1429     }
  1414   1430     if( globalFlag && g.repositoryOpen ){
  1415   1431       db_multi_exec("DELETE FROM config WHERE name=%Q", zName);
  1416   1432     }
  1417   1433     db_end_transaction(0);
  1418   1434   }
................................................................................
  1463   1479   void db_set_int(const char *zName, int value, int globalFlag){
  1464   1480     if( globalFlag ){
  1465   1481       db_swap_connections();
  1466   1482       db_multi_exec("REPLACE INTO global_config(name,value) VALUES(%Q,%d)",
  1467   1483                     zName, value);
  1468   1484       db_swap_connections();
  1469   1485     }else{
  1470         -    db_multi_exec("REPLACE INTO config(name,value) VALUES(%Q,%d)",
         1486  +    db_multi_exec("REPLACE INTO config(name,value,mtime) VALUES(%Q,%d,now())",
  1471   1487                     zName, value);
  1472   1488     }
  1473   1489     if( globalFlag && g.repositoryOpen ){
  1474   1490       db_multi_exec("DELETE FROM config WHERE name=%Q", zName);
  1475   1491     }
  1476   1492   }
  1477   1493   int db_get_boolean(const char *zName, int dflt){

Changes to src/login.c.

  1255   1255     );
  1256   1256     db_end_transaction(0);
  1257   1257     db_multi_exec("DETACH other");
  1258   1258   
  1259   1259     /* Propagate the changes to all other members of the login-group */
  1260   1260     zSql = mprintf(
  1261   1261       "BEGIN;"
  1262         -    "REPLACE INTO config(name, value) VALUES('peer-name-%q', %Q);"
  1263         -    "REPLACE INTO config(name, value) VALUES('peer-repo-%q', %Q);"
         1262  +    "REPLACE INTO config(name,value,mtime) VALUES('peer-name-%q',%Q,now());"
         1263  +    "REPLACE INTO config(name,value,mtime) VALUES('peer-repo-%q',%Q,now());"
  1264   1264       "COMMIT;",
  1265   1265       zSelfProjCode, zSelfLabel, zSelfProjCode, zSelfRepo
  1266   1266     );
  1267   1267     login_group_sql(zSql, "<li> ", "</li>", pzErrMsg);
  1268   1268     fossil_free(zSql);
  1269   1269   }
  1270   1270   

Changes to src/rebuild.c.

    20     20   #include "config.h"
    21     21   #include "rebuild.h"
    22     22   #include <assert.h>
    23     23   #include <dirent.h>
    24     24   #include <errno.h>
    25     25   
    26     26   /*
    27         -** Schema changes
           27  +** Make changes to the stable part of the schema (the part that is not
           28  +** simply deleted and reconstructed on a rebuild) to bring the schema
           29  +** up to the latest.
    28     30   */
    29         -static const char zSchemaUpdates[] =
           31  +static const char zSchemaUpdates1[] =
    30     32   @ -- Index on the delta table
    31     33   @ --
    32     34   @ CREATE INDEX IF NOT EXISTS delta_i1 ON delta(srcid);
    33     35   @
    34     36   @ -- Artifacts that should not be processed are identified in the
    35     37   @ -- "shun" table.  Artifacts that are control-file forgeries or
    36     38   @ -- spam or artifacts whose contents violate administrative policy
................................................................................
    37     39   @ -- can be shunned in order to prevent them from contaminating
    38     40   @ -- the repository.
    39     41   @ --
    40     42   @ -- Shunned artifacts do not exist in the blob table.  Hence they
    41     43   @ -- have not artifact ID (rid) and we thus must store their full
    42     44   @ -- UUID.
    43     45   @ --
    44         -@ CREATE TABLE IF NOT EXISTS shun(uuid UNIQUE);
           46  +@ CREATE TABLE IF NOT EXISTS shun(
           47  +@   uuid UNIQUE,          -- UUID of artifact to be shunned. Canonical form
           48  +@   mtime INTEGER,        -- When added.  Seconds since 1970
           49  +@   scom TEXT             -- Optional text explaining why the shun occurred
           50  +@ );
    45     51   @
    46     52   @ -- Artifacts that should not be pushed are stored in the "private"
    47     53   @ -- table.  
    48     54   @ --
    49     55   @ CREATE TABLE IF NOT EXISTS private(rid INTEGER PRIMARY KEY);
    50     56   @
    51         -@ -- An entry in this table describes a database query that generates a
    52         -@ -- table of tickets.
    53         -@ --
    54         -@ CREATE TABLE IF NOT EXISTS reportfmt(
    55         -@    rn integer primary key,  -- Report number
    56         -@    owner text,              -- Owner of this report format (not used)
    57         -@    title text,              -- Title of this report
    58         -@    cols text,               -- A color-key specification
    59         -@    sqlcode text             -- An SQL SELECT statement for this report
    60         -@ );
    61         -@
    62     57   @ -- Some ticket content (such as the originators email address or contact
    63     58   @ -- information) needs to be obscured to protect privacy.  This is achieved
    64     59   @ -- by storing an SHA1 hash of the content.  For display, the hash is
    65     60   @ -- mapped back into the original text using this table.  
    66     61   @ --
    67     62   @ -- This table contains sensitive information and should not be shared
    68     63   @ -- with unauthorized users.
    69     64   @ --
    70     65   @ CREATE TABLE IF NOT EXISTS concealed(
    71         -@   hash TEXT PRIMARY KEY,
    72         -@   content TEXT
           66  +@   hash TEXT PRIMARY KEY,    -- The SHA1 hash of content
           67  +@   mtime INTEGER,            -- Time created.  Seconds since 1970
           68  +@   content TEXT              -- Content intended to be concealed
           69  +@ );
           70  +;
           71  +static const char zSchemaUpdates2[] =
           72  +@ -- An entry in this table describes a database query that generates a
           73  +@ -- table of tickets.
           74  +@ --
           75  +@ CREATE TABLE IF NOT EXISTS reportfmt(
           76  +@    rn INTEGER PRIMARY KEY,  -- Report number
           77  +@    owner TEXT,              -- Owner of this report format (not used)
           78  +@    title TEXT UNIQUE,       -- Title of this report
           79  +@    mtime INTEGER,           -- Time last modified.  Seconds since 1970
           80  +@    cols TEXT,               -- A color-key specification
           81  +@    sqlcode TEXT             -- An SQL SELECT statement for this report
    73     82   @ );
    74     83   ;
           84  +
           85  +static void rebuild_update_schema(void){
           86  +  int rc;
           87  +  db_multi_exec(zSchemaUpdates1);
           88  +  db_multi_exec(zSchemaUpdates2);
           89  +
           90  +  rc = db_exists("SELECT 1 FROM sqlite_master"
           91  +                 " WHERE name='user' AND sql GLOB '* mtime *'");
           92  +  if( rc==0 ){
           93  +    db_multi_exec(
           94  +      "CREATE TEMP TABLE temp_user AS SELECT * FROM user;"
           95  +      "DROP TABLE user;"
           96  +      "CREATE TABLE user(\n"
           97  +      "  uid INTEGER PRIMARY KEY,\n"
           98  +      "  login TEXT UNIQUE,\n"
           99  +      "  pw TEXT,\n"
          100  +      "  cap TEXT,\n"
          101  +      "  cookie TEXT,\n"
          102  +      "  ipaddr TEXT,\n"
          103  +      "  cexpire DATETIME,\n"
          104  +      "  info TEXT,\n"
          105  +      "  mtime DATE,\n"
          106  +      "  photo BLOB\n"
          107  +      ");"
          108  +      "INSERT OR IGNORE INTO user"
          109  +        " SELECT uid, login, pw, cap, cookie,"
          110  +               " ipaddr, cexpire, info, now(), photo FROM temp_user;"
          111  +      "DROP TABLE temp_user;"
          112  +    );
          113  +  }
          114  +
          115  +  rc = db_exists("SELECT 1 FROM sqlite_master"
          116  +                 " WHERE name='config' AND sql GLOB '* mtime *'");
          117  +  if( rc==0 ){
          118  +    db_multi_exec(
          119  +      "ALTER TABLE config ADD COLUMN mtime INTEGER;"
          120  +      "UPDATE config SET mtime=now();"
          121  +    );
          122  +  }
          123  +
          124  +  rc = db_exists("SELECT 1 FROM sqlite_master"
          125  +                 " WHERE name='shun' AND sql GLOB '* mtime *'");
          126  +  if( rc==0 ){
          127  +    db_multi_exec(
          128  +      "ALTER TABLE shun ADD COLUMN mtime INTEGER;"
          129  +      "ALTER TABLE shun ADD COLUMN scom TEXT;"
          130  +      "UPDATE shun SET mtime=now();"
          131  +    );
          132  +  }
          133  +
          134  +  rc = db_exists("SELECT 1 FROM sqlite_master"
          135  +                 " WHERE name='reportfmt' AND sql GLOB '* mtime *'");
          136  +  if( rc==0 ){
          137  +    db_multi_exec(
          138  +      "CREATE TEMP TABLE old_fmt AS SELECT * FROM reportfmt;"
          139  +      "DROP TABLE reportfmt;"
          140  +    );
          141  +    db_multi_exec(zSchemaUpdates2);
          142  +    db_multi_exec(
          143  +      "INSERT OR IGNORE INTO reportfmt(rn,owner,title,cols,sqlcode,mtime)"
          144  +        " SELECT rn, owner, title, cols, sqlcode, now() FROM old_fmt;"
          145  +      "INSERT OR IGNORE INTO reportfmt(rn,owner,title,cols,sqlcode,mtime)"
          146  +        " SELECT rn, owner, title || ' (' || rn || ')', cols, sqlcode, now()"
          147  +        "   FROM old_fmt;"
          148  +    );
          149  +  }
          150  +
          151  +  rc = db_exists("SELECT 1 FROM sqlite_master"
          152  +                 " WHERE name='concealed' AND sql GLOB '* mtime *'");
          153  +  if( rc==0 ){
          154  +    db_multi_exec(
          155  +      "ALTER TABLE concealed ADD COLUMN mtime INTEGER;"
          156  +      "UPDATE concealed SET mtime=now();"
          157  +    );
          158  +  }
          159  +}  
    75    160   
    76    161   /*
    77    162   ** Variables used to store state information about an on-going "rebuild"
    78    163   ** or "deconstruct".
    79    164   */
    80    165   static int totalSize;       /* Total number of artifacts to process */
    81    166   static int processCnt;      /* Number processed so far */
................................................................................
   254    339   
   255    340     bag_init(&bagDone);
   256    341     ttyOutput = doOut;
   257    342     processCnt = 0;
   258    343     if (!g.fQuiet) {
   259    344       percent_complete(0);
   260    345     }
   261         -  db_multi_exec(zSchemaUpdates);
          346  +  rebuild_update_schema();
   262    347     for(;;){
   263    348       zTable = db_text(0,
   264    349          "SELECT name FROM sqlite_master /*scan*/"
   265    350          " WHERE type='table'"
   266    351          " AND name NOT IN ('blob','delta','rcvfrom','user',"
   267    352                            "'config','shun','private','reportfmt',"
   268    353                            "'concealed','accesslog')"
................................................................................
   457    542       db_close(1);
   458    543       db_open_repository(g.zRepositoryName);
   459    544     }
   460    545     db_begin_transaction();
   461    546     ttyOutput = 1;
   462    547     errCnt = rebuild_db(randomizeFlag, 1, doClustering);
   463    548     db_multi_exec(
   464         -    "REPLACE INTO config(name,value) VALUES('content-schema','%s');"
   465         -    "REPLACE INTO config(name,value) VALUES('aux-schema','%s');",
          549  +    "REPLACE INTO config(name,value,mtime) VALUES('content-schema','%s',now());"
          550  +    "REPLACE INTO config(name,value,mtime) VALUES('aux-schema','%s',now());",
   466    551       CONTENT_SCHEMA, AUX_SCHEMA
   467    552     );
   468    553     if( errCnt && !forceFlag ){
   469    554       printf("%d errors. Rolling back changes. Use --force to force a commit.\n",
   470    555               errCnt);
   471    556       db_end_transaction(1);
   472    557     }else{

Changes to src/report.c.

   358    358       if( zSQL[0]==0 ){
   359    359         zErr = "Please supply an SQL query statement";
   360    360       }else if( (zTitle = trim_string(zTitle))[0]==0 ){
   361    361         zErr = "Please supply a title"; 
   362    362       }else{
   363    363         zErr = verify_sql_statement(zSQL);
   364    364       }
          365  +    if( zErr==0
          366  +     && db_exists("SELECT 1 FROM reportfmt WHERE title=%Q and rn<>%d",
          367  +                  zTitle, rn)
          368  +    ){
          369  +      zErr = mprintf("There is already another report named \"%h\"", zTitle);
          370  +    }
   365    371       if( zErr==0 ){
   366    372         login_verify_csrf_secret();
   367    373         if( rn>0 ){
   368    374           db_multi_exec("UPDATE reportfmt SET title=%Q, sqlcode=%Q,"
   369         -                      " owner=%Q, cols=%Q WHERE rn=%d",
          375  +                      " owner=%Q, cols=%Q, mtime=now() WHERE rn=%d",
   370    376              zTitle, zSQL, zOwner, zClrKey, rn);
   371    377         }else{
   372         -        db_multi_exec("INSERT INTO reportfmt(title,sqlcode,owner,cols) "
   373         -           "VALUES(%Q,%Q,%Q,%Q)",
          378  +        db_multi_exec("INSERT INTO reportfmt(title,sqlcode,owner,cols,mtime) "
          379  +           "VALUES(%Q,%Q,%Q,%Q,now())",
   374    380              zTitle, zSQL, zOwner, zClrKey);
   375    381           rn = db_last_insert_rowid();
   376    382         }
   377    383         cgi_redirect(mprintf("rptview?rn=%d", rn));
   378    384         return;
   379    385       }
   380    386     }else if( rn==0 ){

Changes to src/schema.c.

    37     37   /*
    38     38   ** The content tables have a content version number which rarely
    39     39   ** changes.  The aux tables have an arbitrary version number (typically
    40     40   ** a date) which can change frequently.  When the content schema changes,
    41     41   ** we have to execute special procedures to update the schema.  When
    42     42   ** the aux schema changes, all we need to do is rebuild the database.
    43     43   */
    44         -#define CONTENT_SCHEMA  "1"
    45         -#define AUX_SCHEMA      "2011-02-25 14:52"
           44  +#define CONTENT_SCHEMA  "2"
           45  +#define AUX_SCHEMA      "2011-04-25 19:50"
    46     46   
    47     47   #endif /* INTERFACE */
    48     48   
    49     49   
    50     50   /*
    51     51   ** The schema for a repository database.  
    52     52   **
    53     53   ** Schema1[] contains parts of the schema that are fixed and unchanging
    54     54   ** across versions.  Schema2[] contains parts of the schema that can
    55     55   ** change from one version to the next.  The information in Schema2[]
    56         -** can be reconstructed from the information in Schema1[].
           56  +** is reconstructed from the information in Schema1[] by the "rebuild"
           57  +** operation.
    57     58   */
    58     59   const char zRepositorySchema1[] = 
    59     60   @ -- The BLOB and DELTA tables contain all records held in the repository.
    60     61   @ --
    61         -@ -- The BLOB.CONTENT column is always compressed using libz.  This
           62  +@ -- The BLOB.CONTENT column is always compressed using zlib.  This
    62     63   @ -- column might hold the full text of the record or it might hold
    63     64   @ -- a delta that is able to reconstruct the record from some other
    64     65   @ -- record.  If BLOB.CONTENT holds a delta, then a DELTA table entry
    65     66   @ -- will exist for the record and that entry will point to another
    66     67   @ -- entry that holds the source of the delta.  Deltas can be chained.
           68  +@ --
           69  +@ -- The blob and delta tables collectively hold the "global state" of
           70  +@ -- a Fossil repository.  
    67     71   @ --
    68     72   @ CREATE TABLE blob(
    69     73   @   rid INTEGER PRIMARY KEY,        -- Record ID
    70     74   @   rcvid INTEGER,                  -- Origin of this record
    71     75   @   size INTEGER,                   -- Size of content. -1 for a phantom.
    72     76   @   uuid TEXT UNIQUE NOT NULL,      -- SHA1 hash of the content
    73     77   @   content BLOB,                   -- Compressed content of this record
................................................................................
    75     79   @ );
    76     80   @ CREATE TABLE delta(
    77     81   @   rid INTEGER PRIMARY KEY,                 -- Record ID
    78     82   @   srcid INTEGER NOT NULL REFERENCES blob   -- Record holding source document
    79     83   @ );
    80     84   @ CREATE INDEX delta_i1 ON delta(srcid);
    81     85   @
           86  +@ -------------------------------------------------------------------------
           87  +@ -- The BLOB and DELTA tables above hold the "global state" of a Fossil
           88  +@ -- project; the stuff that is normally exchanged during "sync".  The
           89  +@ -- "local state" of a repository is contained in the remaining tables of
           90  +@ -- the zRepositorySchema1 string.  
           91  +@ -------------------------------------------------------------------------
           92  +@
    82     93   @ -- Whenever new blobs are received into the repository, an entry
    83     94   @ -- in this table records the source of the blob.
    84     95   @ --
    85     96   @ CREATE TABLE rcvfrom(
    86     97   @   rcvid INTEGER PRIMARY KEY,      -- Received-From ID
    87     98   @   uid INTEGER REFERENCES user,    -- User login
    88         -@   mtime DATETIME,                 -- Time or receipt
           99  +@   mtime DATETIME,                 -- Time of receipt.  Julian day.
    89    100   @   nonce TEXT UNIQUE,              -- Nonce used for login
    90    101   @   ipaddr TEXT                     -- Remote IP address.  NULL for direct.
    91    102   @ );
    92    103   @
    93    104   @ -- Information about users
    94    105   @ --
    95    106   @ -- The user.pw field can be either cleartext of the password, or
................................................................................
    97    108   @ -- characters long we assume it is a SHA1 hash.  Otherwise, it is
    98    109   @ -- cleartext.  The sha1_shared_secret() routine computes the password
    99    110   @ -- hash based on the project-code, the user login, and the cleartext
   100    111   @ -- password.
   101    112   @ --
   102    113   @ CREATE TABLE user(
   103    114   @   uid INTEGER PRIMARY KEY,        -- User ID
   104         -@   login TEXT,                     -- login name of the user
          115  +@   login TEXT UNIQUE,              -- login name of the user
   105    116   @   pw TEXT,                        -- password
   106    117   @   cap TEXT,                       -- Capabilities of this user
   107    118   @   cookie TEXT,                    -- WWW login cookie
   108    119   @   ipaddr TEXT,                    -- IP address for which cookie is valid
   109    120   @   cexpire DATETIME,               -- Time when cookie expires
   110    121   @   info TEXT,                      -- contact information
          122  +@   mtime DATE,                     -- last change.  seconds since 1970
   111    123   @   photo BLOB                      -- JPEG image of this user
   112    124   @ );
   113    125   @
   114    126   @ -- The VAR table holds miscellanous information about the repository.
   115    127   @ -- in the form of name-value pairs.
   116    128   @ --
   117    129   @ CREATE TABLE config(
   118    130   @   name TEXT PRIMARY KEY NOT NULL,  -- Primary name of the entry
   119    131   @   value CLOB,                      -- Content of the named parameter
          132  +@   mtime DATE,                      -- last modified.  seconds since 1970
   120    133   @   CHECK( typeof(name)='text' AND length(name)>=1 )
   121    134   @ );
   122    135   @
   123    136   @ -- Artifacts that should not be processed are identified in the
   124    137   @ -- "shun" table.  Artifacts that are control-file forgeries or
   125    138   @ -- spam or artifacts whose contents violate administrative policy
   126    139   @ -- can be shunned in order to prevent them from contaminating
   127    140   @ -- the repository.
   128    141   @ --
   129    142   @ -- Shunned artifacts do not exist in the blob table.  Hence they
   130    143   @ -- have not artifact ID (rid) and we thus must store their full
   131    144   @ -- UUID.
   132    145   @ --
   133         -@ CREATE TABLE shun(uuid UNIQUE);
          146  +@ CREATE TABLE shun(
          147  +@   uuid UNIQUE,          -- UUID of artifact to be shunned. Canonical form
          148  +@   mtime DATE,           -- When added.  seconds since 1970
          149  +@   scom TEXT             -- Optional text explaining why the shun occurred
          150  +@ );
   134    151   @
   135    152   @ -- Artifacts that should not be pushed are stored in the "private"
   136    153   @ -- table.  Private artifacts are omitted from the "unclustered" and
   137    154   @ -- "unsent" tables.
   138    155   @ --
   139    156   @ CREATE TABLE private(rid INTEGER PRIMARY KEY);
   140    157   @
   141    158   @ -- An entry in this table describes a database query that generates a
   142    159   @ -- table of tickets.
   143    160   @ --
   144    161   @ CREATE TABLE reportfmt(
   145         -@    rn integer primary key,  -- Report number
   146         -@    owner text,              -- Owner of this report format (not used)
   147         -@    title text,              -- Title of this report
   148         -@    cols text,               -- A color-key specification
   149         -@    sqlcode text             -- An SQL SELECT statement for this report
          162  +@    rn INTEGER PRIMARY KEY,  -- Report number
          163  +@    owner TEXT,              -- Owner of this report format (not used)
          164  +@    title TEXT UNIQUE,       -- Title of this report
          165  +@    mtime DATE,              -- Last modified.  seconds since 1970
          166  +@    cols TEXT,               -- A color-key specification
          167  +@    sqlcode TEXT             -- An SQL SELECT statement for this report
   150    168   @ );
   151         -@ INSERT INTO reportfmt(title,cols,sqlcode) VALUES('All Tickets','#ffffff Key:
          169  +@ INSERT INTO reportfmt(title,mtime,cols,sqlcode) 
          170  +@ VALUES('All Tickets',julianday('1970-01-01'),'#ffffff Key:
   152    171   @ #f2dcdc Active
   153    172   @ #e8e8e8 Review
   154    173   @ #cfe8bd Fixed
   155    174   @ #bde5d6 Tested
   156    175   @ #cacae5 Deferred
   157    176   @ #c8c8c8 Closed','SELECT
   158    177   @   CASE WHEN status IN (''Open'',''Verified'') THEN ''#f2dcdc''
................................................................................
   174    193   @ -- by storing an SHA1 hash of the content.  For display, the hash is
   175    194   @ -- mapped back into the original text using this table.  
   176    195   @ --
   177    196   @ -- This table contains sensitive information and should not be shared
   178    197   @ -- with unauthorized users.
   179    198   @ --
   180    199   @ CREATE TABLE concealed(
   181         -@   hash TEXT PRIMARY KEY,
   182         -@   content TEXT
          200  +@   hash TEXT PRIMARY KEY,    -- The SHA1 hash of content
          201  +@   mtime DATE,               -- Time created.  Seconds since 1970
          202  +@   content TEXT              -- Content intended to be concealed
   183    203   @ );
   184    204   ;
   185    205   
   186    206   const char zRepositorySchema2[] =
   187    207   @ -- Filenames
   188    208   @ --
   189    209   @ CREATE TABLE filename(
................................................................................
   212    232   @
   213    233   @ -- Parent/child linkages between checkins
   214    234   @ --
   215    235   @ CREATE TABLE plink(
   216    236   @   pid INTEGER REFERENCES blob,    -- Parent manifest
   217    237   @   cid INTEGER REFERENCES blob,    -- Child manifest
   218    238   @   isprim BOOLEAN,                 -- pid is the primary parent of cid
   219         -@   mtime DATETIME,                 -- the date/time stamp on cid
          239  +@   mtime DATETIME,                 -- the date/time stamp on cid.  Julian day.
   220    240   @   UNIQUE(pid, cid)
   221    241   @ );
   222    242   @ CREATE INDEX plink_i2 ON plink(cid,pid);
   223    243   @
   224    244   @ -- A "leaf" checkin is a checkin that has no children in the same
   225    245   @ -- branch.  The set of all leaves is easily computed with a join,
   226    246   @ -- between the plink and tagxref tables, but it is a slower join for
................................................................................
   230    250   @ --
   231    251   @ CREATE TABLE leaf(rid INTEGER PRIMARY KEY);
   232    252   @
   233    253   @ -- Events used to generate a timeline
   234    254   @ --
   235    255   @ CREATE TABLE event(
   236    256   @   type TEXT,                      -- Type of event: 'ci', 'w', 'e', 't'
   237         -@   mtime DATETIME,                 -- Date and time when the event occurs
          257  +@   mtime DATETIME,                 -- Time of occurrence. Julian day.
   238    258   @   objid INTEGER PRIMARY KEY,      -- Associated record ID
   239    259   @   tagid INTEGER,                  -- Associated ticket or wiki name tag
   240    260   @   uid INTEGER REFERENCES user,    -- User who caused the event
   241    261   @   bgcolor TEXT,                   -- Color set by 'bgcolor' property
   242    262   @   euser TEXT,                     -- User set by 'user' property
   243    263   @   user TEXT,                      -- Name of the user
   244    264   @   ecomment TEXT,                  -- Comment set by 'comment' property
................................................................................
   317    337   @ --
   318    338   @ CREATE TABLE tagxref(
   319    339   @   tagid INTEGER REFERENCES tag,   -- The tag that added or removed
   320    340   @   tagtype INTEGER,                -- 0:-,cancel  1:+,single  2:*,propagate
   321    341   @   srcid INTEGER REFERENCES blob,  -- Artifact of tag. 0 for propagated tags
   322    342   @   origid INTEGER REFERENCES blob, -- check-in holding propagated tag
   323    343   @   value TEXT,                     -- Value of the tag.  Might be NULL.
   324         -@   mtime TIMESTAMP,                -- Time of addition or removal
          344  +@   mtime TIMESTAMP,                -- Time of addition or removal. Julian day
   325    345   @   rid INTEGER REFERENCE blob,     -- Artifact tag is applied to
   326    346   @   UNIQUE(rid, tagid)
   327    347   @ );
   328    348   @ CREATE INDEX tagxref_i1 ON tagxref(tagid, mtime);
   329    349   @
   330    350   @ -- When a hyperlink occurs from one artifact to another (for example
   331    351   @ -- when a check-in comment refers to a ticket) an entry is made in
................................................................................
   332    352   @ -- the following table for that hyperlink.  This table is used to
   333    353   @ -- facilitate the display of "back links".
   334    354   @ --
   335    355   @ CREATE TABLE backlink(
   336    356   @   target TEXT,           -- Where the hyperlink points to
   337    357   @   srctype INT,           -- 0: check-in  1: ticket  2: wiki
   338    358   @   srcid INT,             -- rid for checkin or wiki.  tkt_id for ticket.
   339         -@   mtime TIMESTAMP,       -- time that the hyperlink was added
          359  +@   mtime TIMESTAMP,       -- time that the hyperlink was added. Julian day.
   340    360   @   UNIQUE(target, srctype, srcid)
   341    361   @ );
   342    362   @ CREATE INDEX backlink_src ON backlink(srcid, srctype);
   343    363   @
   344    364   @ -- Each attachment is an entry in the following table.  Only
   345    365   @ -- the most recent attachment (identified by the D card) is saved.
   346    366   @ --
   347    367   @ CREATE TABLE attachment(
   348    368   @   attachid INTEGER PRIMARY KEY,   -- Local id for this attachment
   349    369   @   isLatest BOOLEAN DEFAULT 0,     -- True if this is the one to use
   350         -@   mtime TIMESTAMP,                -- Time when attachment last changed
          370  +@   mtime TIMESTAMP,                -- Last changed.  Julian day.
   351    371   @   src TEXT,                       -- UUID of the attachment.  NULL to delete
   352    372   @   target TEXT,                    -- Object attached to. Wikiname or Tkt UUID
   353    373   @   filename TEXT,                  -- Filename for the attachment
   354    374   @   comment TEXT,                   -- Comment associated with this attachment
   355    375   @   user TEXT                       -- Name of user adding attachment
   356    376   @ );
   357    377   @ CREATE INDEX attachment_idx1 ON attachment(target, filename, mtime);
................................................................................
   441    461   @   id INTEGER PRIMARY KEY,           -- ID of the checked out file
   442    462   @   vid INTEGER REFERENCES blob,      -- The baseline this file is part of.
   443    463   @   chnged INT DEFAULT 0,             -- 0:unchnged 1:edited 2:m-chng 3:m-add
   444    464   @   deleted BOOLEAN DEFAULT 0,        -- True if deleted 
   445    465   @   isexe BOOLEAN,                    -- True if file should be executable
   446    466   @   rid INTEGER,                      -- Originally from this repository record
   447    467   @   mrid INTEGER,                     -- Based on this record due to a merge
   448         -@   mtime INTEGER,                    -- Modification time of file on disk
          468  +@   mtime INTEGER,                    -- Mtime of file on disk. sec since 1970
   449    469   @   pathname TEXT,                    -- Full pathname relative to root
   450    470   @   origname TEXT,                    -- Original pathname. NULL if unchanged
   451    471   @   UNIQUE(pathname,vid)
   452    472   @ );
   453    473   @
   454    474   @ -- This table holds a record of uncommitted merges in the local
   455    475   @ -- file tree.  If a VFILE entry with id has merged with another

Changes to src/setup.c.

   352    352         @
   353    353         @ <p><a href="setup_uedit?id=%d(uid)">[Bummer]</a></p>
   354    354         style_footer();
   355    355         return;
   356    356       }
   357    357       login_verify_csrf_secret();
   358    358       db_multi_exec(
   359         -       "REPLACE INTO user(uid,login,info,pw,cap) "
   360         -       "VALUES(nullif(%d,0),%Q,%Q,%Q,'%s')",
          359  +       "REPLACE INTO user(uid,login,info,pw,cap,mtime) "
          360  +       "VALUES(nullif(%d,0),%Q,%Q,%Q,'%s',now())",
   361    361         uid, P("login"), P("info"), zPw, zCap
   362    362       );
   363    363       if( atoi(PD("all","0"))>0 ){
   364    364         Blob sql;
   365    365         char *zErr = 0;
   366    366         blob_zero(&sql);
   367    367         if( zOldLogin==0 ){
................................................................................
   373    373           zOldLogin = zLogin;
   374    374         }
   375    375         blob_appendf(&sql, 
   376    376           "UPDATE user SET login=%Q,"
   377    377           "  pw=coalesce(shared_secret(%Q,%Q,"
   378    378                   "(SELECT value FROM config WHERE name='project-code')),pw),"
   379    379           "  info=%Q,"
   380         -        "  cap=%Q"
          380  +        "  cap=%Q,"
          381  +        "  mtime=now()"
   381    382           " WHERE login=%Q;",
   382    383           zLogin, P("pw"), zLogin, P("info"), zCap,
   383    384           zOldLogin
   384    385         );
   385    386         login_group_sql(blob_str(&sql), "<li> ", " </li>\n", &zErr);
   386    387         blob_reset(&sql);
   387    388         if( zErr ){
................................................................................
  1284   1285     }
  1285   1286     db_begin_transaction();
  1286   1287     if( P("set")!=0 && zMime && zMime[0] && szImg>0 ){
  1287   1288       Blob img;
  1288   1289       Stmt ins;
  1289   1290       blob_init(&img, aImg, szImg);
  1290   1291       db_prepare(&ins,
  1291         -        "REPLACE INTO config(name, value)"
  1292         -        " VALUES('logo-image',:bytes)"
         1292  +        "REPLACE INTO config(name,value,mtime)"
         1293  +        " VALUES('logo-image',:bytes,now())"
  1293   1294       );
  1294   1295       db_bind_blob(&ins, ":bytes", &img);
  1295   1296       db_step(&ins);
  1296   1297       db_finalize(&ins);
  1297   1298       db_multi_exec(
  1298         -       "REPLACE INTO config(name, value) VALUES('logo-mimetype',%Q)",
         1299  +       "REPLACE INTO config(name,value,mtime) VALUES('logo-mimetype',%Q,now())",
  1299   1300          zMime
  1300   1301       );
  1301   1302       db_end_transaction(0);
  1302   1303       cgi_redirect("setup_logo");
  1303   1304     }else if( P("clr")!=0 ){
  1304   1305       db_multi_exec(
  1305   1306          "DELETE FROM config WHERE name GLOB 'logo-*'"

Changes to src/shun.c.

    79     79         @ be shunned.  But it does not exist in the repository.  It
    80     80         @ may be necessary to rebuild the repository using the
    81     81         @ <b>fossil rebuild</b> command-line before the artifact content
    82     82         @ can pulled in from other respositories.</p>
    83     83       }
    84     84     }
    85     85     if( zUuid && P("add") ){
           86  +    int rid, tagid;
    86     87       login_verify_csrf_secret();
    87         -    db_multi_exec("INSERT OR IGNORE INTO shun VALUES('%s')", zUuid);
           88  +    db_multi_exec(
           89  +      "INSERT OR IGNORE INTO shun(uuid,mtime)"
           90  +      " VALUES('%s', now())", zUuid);
    88     91       @ <p class="shunned">Artifact
    89     92       @ <a href="%s(g.zTop)/artifact/%s(zUuid)">%s(zUuid)</a> has been
    90     93       @ shunned.  It will no longer be pushed.
    91     94       @ It will be removed from the repository the next time the respository
    92     95       @ is rebuilt using the <b>fossil rebuild</b> command-line</p>
           96  +    db_multi_exec("DELETE FROM attachment WHERE src=%Q", zUuid);
           97  +    rid = db_int(0, "SELECT rid FROM blob WHERE uuid=%Q", zUuid);
           98  +    if( rid ){
           99  +      db_multi_exec("DELETE FROM event WHERE objid=%d", rid);
          100  +    }
          101  +    tagid = db_int(0, "SELECT tagid FROM tag WHERE tagname='tkt-%q'", zUuid);
          102  +    if( tagid ){
          103  +      db_multi_exec("DELETE FROM ticket WHERE tkt_uuid=%Q", zUuid);
          104  +      db_multi_exec("DELETE FROM tag WHERE tagid=%d", tagid);
          105  +      db_multi_exec("DELETE FROM tagxref WHERE tagid=%d", tagid);
          106  +    }
    93    107     }
    94    108     @ <p>A shunned artifact will not be pushed nor accepted in a pull and the
    95    109     @ artifact content will be purged from the repository the next time the
    96    110     @ repository is rebuilt.  A list of shunned artifacts can be seen at the
    97    111     @ bottom of this page.</p>
    98    112     @ 
    99    113     @ <a name="addshun"></a>

Changes to src/skins.c.

    23     23   
    24     24   /* @-comment: // */
    25     25   /*
    26     26   ** A black-and-white theme with the project title in a bar across the top
    27     27   ** and no logo image.
    28     28   */
    29     29   static const char zBuiltinSkin1[] = 
    30         -@ REPLACE INTO config VALUES('css','/* General settings for the entire page */
           30  +@ REPLACE INTO config(name,mtime,value)
           31  +@ VALUES('css',now(),'/* General settings for the entire page */
    31     32   @ body {
    32     33   @   margin: 0ex 1ex;
    33     34   @   padding: 0px;
    34     35   @   background-color: white;
    35     36   @   font-family: sans-serif;
    36     37   @ }
    37     38   @ 
................................................................................
   150    151   @ 
   151    152   @ /* The label/value pairs on (for example) the vinfo page */
   152    153   @ table.label-value th {
   153    154   @   vertical-align: top;
   154    155   @   text-align: right;
   155    156   @   padding: 0.2ex 2ex;
   156    157   @ }');
   157         -@ REPLACE INTO config VALUES('header','<html>
          158  +@ REPLACE INTO config(name,mtime,value) VALUES('header',now(),'<html>
   158    159   @ <head>
   159    160   @ <title>$<project_name>: $<title></title>
   160    161   @ <link rel="alternate" type="application/rss+xml" title="RSS Feed"
   161    162   @       href="$home/timeline.rss">
   162    163   @ <link rel="stylesheet" href="$home/style.css?blackwhite" type="text/css"
   163    164   @       media="screen">
   164    165   @ </head>
................................................................................
   202    203   @ if {[info exists login]} {
   203    204   @   html "<a href=''$home/login''>Logout</a> "
   204    205   @ } else {
   205    206   @   html "<a href=''$home/login''>Login</a> "
   206    207   @ }
   207    208   @ </th1></div>
   208    209   @ ');
   209         -@ REPLACE INTO config VALUES('footer','<div class="footer">
          210  +@ REPLACE INTO config(name,mtime,value)
          211  +@ VALUES('footer',now(),'<div class="footer">
   210    212   @ Fossil version $manifest_version $manifest_date 
   211    213   @ </div>
   212    214   @ </body></html>
   213    215   @ ');
   214    216   ;
   215    217   
   216    218   /*
   217    219   ** A tan theme with the project title above the user identification
   218    220   ** and no logo image.
   219    221   */
   220    222   static const char zBuiltinSkin2[] = 
   221         -@ REPLACE INTO config VALUES('css','/* General settings for the entire page */
          223  +@ REPLACE INTO config(name,mtime,value)
          224  +@ VALUES('css',now(),'/* General settings for the entire page */
   222    225   @ body {
   223    226   @   margin: 0ex 0ex;
   224    227   @   padding: 0px;
   225    228   @   background-color: #fef3bc;
   226    229   @   font-family: sans-serif;
   227    230   @ }
   228    231   @ 
................................................................................
   352    355   @ /* The label/value pairs on (for example) the ci page */
   353    356   @ table.label-value th {
   354    357   @   vertical-align: top;
   355    358   @   text-align: right;
   356    359   @   padding: 0.2ex 2ex;
   357    360   @ }
   358    361   @ ');
   359         -@ REPLACE INTO config VALUES('header','<html>
          362  +@ REPLACE INTO config(name,mtime,value) VALUES('header',now(),'<html>
   360    363   @ <head>
   361    364   @ <title>$<project_name>: $<title></title>
   362    365   @ <link rel="alternate" type="application/rss+xml" title="RSS Feed"
   363    366   @       href="$home/timeline.rss">
   364    367   @ <link rel="stylesheet" href="$home/style.css?tan" type="text/css"
   365    368   @       media="screen">
   366    369   @ </head>
................................................................................
   403    406   @ if {[info exists login]} {
   404    407   @   html "<a href=''$home/login''>Logout</a> "
   405    408   @ } else {
   406    409   @   html "<a href=''$home/login''>Login</a> "
   407    410   @ }
   408    411   @ </th1></div>
   409    412   @ ');
   410         -@ REPLACE INTO config VALUES('footer','<div class="footer">
          413  +@ REPLACE INTO config(name,mtime,value)
          414  +@ VALUES('footer',now(),'<div class="footer">
   411    415   @ Fossil version $manifest_version $manifest_date
   412    416   @ </div>
   413    417   @ </body></html>
   414    418   @ ');
   415    419   ;
   416    420   
   417    421   /*
   418    422   ** Black letters on a white or cream background with the main menu
   419    423   ** stuck on the left-hand side.
   420    424   */
   421    425   static const char zBuiltinSkin3[] = 
   422         -@ REPLACE INTO config VALUES('css','/* General settings for the entire page */
          426  +@ REPLACE INTO config(name,mtime,value)
          427  +@ VALUES('css',now(),'/* General settings for the entire page */
   423    428   @ body {
   424    429   @     margin:0px 0px 0px 0px;
   425    430   @     padding:0px;
   426    431   @     font-family:verdana, arial, helvetica, "sans serif";
   427    432   @     color:#333;
   428    433   @     background-color:white;
   429    434   @ }
................................................................................
   584    589   @ 
   585    590   @ /* The label/value pairs on (for example) the ci page */
   586    591   @ table.label-value th {
   587    592   @   vertical-align: top;
   588    593   @   text-align: right;
   589    594   @   padding: 0.2ex 2ex;
   590    595   @ }');
   591         -@ REPLACE INTO config VALUES('header','<html>
          596  +@ REPLACE INTO config(name,mtime,value) VALUES('header',now(),'<html>
   592    597   @ <head>
   593    598   @ <title>$<project_name>: $<title></title>
   594    599   @ <link rel="alternate" type="application/rss+xml" title="RSS Feed"
   595    600   @       href="$home/timeline.rss">
   596    601   @ <link rel="stylesheet" href="$home/style.css?black2" type="text/css"
   597    602   @       media="screen">
   598    603   @ </head>
................................................................................
   638    643   @   html "<li><a href=''$home/login''>Logout</a></li>"
   639    644   @ } else {
   640    645   @   html "<li><a href=''$home/login''>Login</a></li>"
   641    646   @ }
   642    647   @ </th1></ul></div>
   643    648   @ <div id="container">
   644    649   @ ');
   645         -@ REPLACE INTO config VALUES('footer','</div>
          650  +@ REPLACE INTO config(name,mtime,value) VALUES('footer',now(),'</div>
   646    651   @ <div class="footer">
   647    652   @ Fossil version $manifest_version $manifest_date
   648    653   @ </div>
   649    654   @ </body></html>
   650    655   @ ');
   651    656   ;
   652    657   
   653    658   
   654    659   /*
   655    660   ** Gradients and rounded corners.
   656    661   */
   657    662   static const char zBuiltinSkin4[] = 
   658         -@ REPLACE INTO config VALUES('css','/* General settings for the entire page */
          663  +@ REPLACE INTO config(name,mtime,value)
          664  +@ VALUES('css',now(),'/* General settings for the entire page */
   659    665   @ html {
   660    666   @   min-height: 100%;
   661    667   @ }
   662    668   @ body {
   663    669   @   margin: 0ex 1ex;
   664    670   @   padding: 0px;
   665    671   @   background-color: white;
................................................................................
   878    884   @ table.report tr td {
   879    885   @   padding: 3px 5px;
   880    886   @ }
   881    887   @ 
   882    888   @ textarea {
   883    889   @   font-size: 1em;
   884    890   @ }');
   885         -@ REPLACE INTO config VALUES('header','<html>
          891  +@ REPLACE INTO config(name,mtime,value) VALUES('header',now(),'<html>
   886    892   @ <head>
   887    893   @ <title>$<project_name>: $<title></title>
   888    894   @ <link rel="alternate" type="application/rss+xml" title="RSS Feed"
   889    895   @       href="$home/timeline.rss">
   890    896   @ <link rel="stylesheet" href="$home/style.css?black2" type="text/css"
   891    897   @       media="screen">
   892    898   @ </head>
................................................................................
   932    938   @   html "<a href=''$home/login''>Logout</a>"
   933    939   @ } else {
   934    940   @   html "<a href=''$home/login''>Login</a>"
   935    941   @ }
   936    942   @ </th1></ul></div>
   937    943   @ <div id="container">
   938    944   @ ');
   939         -@ REPLACE INTO config VALUES('footer','</div>
          945  +@ REPLACE INTO config(name,mtime,value) VALUES('footer',now(),'</div>
   940    946   @ <div class="footer">
   941    947   @ Fossil version $manifest_version $manifest_date
   942    948   @ </div>
   943    949   @ </body></html>
   944    950   @ ');
   945    951   ;
   946    952   
................................................................................
   982    988   ** useDefault==0 or a string for the default skin if useDefault==1.
   983    989   **
   984    990   ** Memory to hold the returned string is obtained from malloc.
   985    991   */
   986    992   static char *getSkin(int useDefault){
   987    993     Blob val;
   988    994     blob_zero(&val);
   989         -  blob_appendf(&val, "REPLACE INTO config VALUES('css',%Q);\n",
          995  +  blob_appendf(&val,
          996  +     "REPLACE INTO config(name,value,mtime) VALUES('css',%Q,now());\n",
   990    997        useDefault ? zDefaultCSS : db_get("css", (char*)zDefaultCSS)
   991    998     );
   992         -  blob_appendf(&val, "REPLACE INTO config VALUES('header',%Q);\n",
          999  +  blob_appendf(&val,
         1000  +     "REPLACE INTO config(name,value,mtime) VALUES('header',%Q,now());\n",
   993   1001        useDefault ? zDefaultHeader : db_get("header", (char*)zDefaultHeader)
   994   1002     );
   995         -  blob_appendf(&val, "REPLACE INTO config VALUES('footer',%Q);\n",
         1003  +  blob_appendf(&val,
         1004  +     "REPLACE INTO config(name,value,mtime) VALUES('footer',%Q,now());\n",
   996   1005        useDefault ? zDefaultFooter : db_get("footer", (char*)zDefaultFooter)
   997   1006     );
   998   1007     return blob_str(&val);
   999   1008   }
  1000   1009   
  1001   1010   /*
  1002   1011   ** Construct the default skin string and fill in the corresponding
................................................................................
  1046   1055   
  1047   1056     if( P("save")!=0 && (zName = skinVarName(P("save"),0))!=0 ){
  1048   1057       if( db_exists("SELECT 1 FROM config WHERE name=%Q", zName)
  1049   1058             || strcmp(zName, "Default")==0 ){
  1050   1059         zErr = mprintf("Skin name \"%h\" already exists. "
  1051   1060                        "Choose a different name.", P("sn"));
  1052   1061       }else{
  1053         -      db_multi_exec("INSERT INTO config VALUES(%Q,%Q)",
         1062  +      db_multi_exec("INSERT INTO config(name,value,mtime) VALUES(%Q,%Q,now())",
  1054   1063            zName, zCurrent
  1055   1064         );
  1056   1065       }
  1057   1066     }
  1058   1067   
  1059   1068     /* The user pressed the "Use This Skin" button. */
  1060   1069     if( P("load") && (z = P("sn"))!=0 && z[0] ){
................................................................................
  1067   1076       }
  1068   1077       if( !seen ){
  1069   1078         seen = db_exists("SELECT 1 FROM config WHERE name GLOB 'skin:*'"
  1070   1079                          " AND value=%Q", zCurrent);
  1071   1080       }
  1072   1081       if( !seen ){
  1073   1082         db_multi_exec(
  1074         -        "INSERT INTO config VALUES("
         1083  +        "INSERT INTO config(name,value,mtime) VALUES("
  1075   1084           "  strftime('skin:Backup On %%Y-%%m-%%d %%H:%%M:%%S'),"
  1076         -        "  %Q)", zCurrent
         1085  +        "  %Q,now())", zCurrent
  1077   1086         );
  1078   1087       }
  1079   1088       seen = 0;
  1080   1089       for(i=0; i<sizeof(aBuiltinSkin)/sizeof(aBuiltinSkin[0]); i++){
  1081   1090         if( strcmp(aBuiltinSkin[i].zName, z)==0 ){
  1082   1091           seen = 1;
  1083   1092           zCurrent = aBuiltinSkin[i].zValue;

Changes to src/user.c.

   202    202       if( g.argc>=6 ){
   203    203         blob_init(&passwd, g.argv[5], -1);
   204    204       }else{
   205    205         prompt_for_password("password: ", &passwd, 1);
   206    206       }
   207    207       zPw = sha1_shared_secret(blob_str(&passwd), blob_str(&login), 0);
   208    208       db_multi_exec(
   209         -      "INSERT INTO user(login,pw,cap,info)"
   210         -      "VALUES(%B,%Q,%B,%B)",
          209  +      "INSERT INTO user(login,pw,cap,info,mtime)"
          210  +      "VALUES(%B,%Q,%B,%B,now())",
   211    211         &login, zPw, &caps, &contact
   212    212       );
   213    213       free(zPw);
   214    214     }else if( n>=2 && strncmp(g.argv[2],"default",n)==0 ){
   215    215       user_select();
   216    216       if( g.argc==3 ){
   217    217         printf("%s\n", g.zLogin);
................................................................................
   247    247         zPrompt = mprintf("New password for %s: ", g.argv[3]);
   248    248         prompt_for_password(zPrompt, &pw, 1);
   249    249       }
   250    250       if( blob_size(&pw)==0 ){
   251    251         printf("password unchanged\n");
   252    252       }else{
   253    253         char *zSecret = sha1_shared_secret(blob_str(&pw), g.argv[3], 0);
   254         -      db_multi_exec("UPDATE user SET pw=%Q WHERE uid=%d", zSecret, uid);
          254  +      db_multi_exec("UPDATE user SET pw=%Q, mtime=now() WHERE uid=%d",
          255  +                    zSecret, uid);
   255    256         free(zSecret);
   256    257       }
   257    258     }else if( n>=2 && strncmp(g.argv[2],"capabilities",2)==0 ){
   258    259       int uid;
   259    260       if( g.argc!=4 && g.argc!=5 ){
   260    261         usage("user capabilities USERNAME ?PERMISSIONS?");
   261    262       }
   262    263       uid = db_int(0, "SELECT uid FROM user WHERE login=%Q", g.argv[3]);
   263    264       if( uid==0 ){
   264    265         fossil_fatal("no such user: %s", g.argv[3]);
   265    266       }
   266    267       if( g.argc==5 ){
   267    268         db_multi_exec(
   268         -        "UPDATE user SET cap=%Q WHERE uid=%d", g.argv[4],
   269         -        uid
          269  +        "UPDATE user SET cap=%Q, mtime=now() WHERE uid=%d",
          270  +        g.argv[4], uid
   270    271         );
   271    272       }
   272    273       printf("%s\n", db_text(0, "SELECT cap FROM user WHERE uid=%d", uid));
   273    274     }else{
   274    275       fossil_panic("user subcommand should be one of: "
   275    276                    "capabilities default list new password");
   276    277     }
................................................................................
   338    339         g.zLogin = mprintf("%s", db_column_text(&s, 1));
   339    340       }
   340    341       db_finalize(&s);
   341    342     }
   342    343   
   343    344     if( g.userUid==0 ){
   344    345       db_multi_exec(
   345         -      "INSERT INTO user(login, pw, cap, info)"
   346         -      "VALUES('anonymous', '', 'cfghjkmnoqw', '')"
          346  +      "INSERT INTO user(login, pw, cap, info, mtime)"
          347  +      "VALUES('anonymous', '', 'cfghjkmnoqw', '', now())"
   347    348       );
   348    349       g.userUid = db_last_insert_rowid();
   349    350       g.zLogin = "anonymous";
   350    351     }
   351    352   }
   352    353   
   353    354   
................................................................................
   362    363   */
   363    364   void user_hash_passwords_cmd(void){
   364    365     if( g.argc!=3 ) usage("REPOSITORY");
   365    366     db_open_repository(g.argv[2]);
   366    367     sqlite3_create_function(g.db, "shared_secret", 2, SQLITE_UTF8, 0,
   367    368                             sha1_shared_secret_sql_function, 0, 0);
   368    369     db_multi_exec(
   369         -    "UPDATE user SET pw=shared_secret(pw,login)"
          370  +    "UPDATE user SET pw=shared_secret(pw,login), mtime=now()"
   370    371       " WHERE length(pw)>0 AND length(pw)!=40"
   371    372     );
   372    373   }
   373    374   
   374    375   /*
   375    376   ** WEBPAGE: access_log
   376    377   **

Changes to src/xfer.c.

   452    452     db_static_prepare(&q1,
   453    453       "SELECT uuid, size, content,"
   454    454            "  (SELECT uuid FROM delta, blob"
   455    455            "    WHERE delta.rid=:rid AND delta.srcid=blob.rid)"
   456    456       " FROM blob"
   457    457       " WHERE rid=:rid"
   458    458       "   AND size>=0"
   459         -    "   AND uuid NOT IN shun"
          459  +    "   AND NOT EXISTS(SELECT 1 FROM shun WHERE shun.uuid=blob.uuid)"
   460    460     );
   461    461     db_bind_int(&q1, ":rid", rid);
   462    462     rc = db_step(&q1);
   463    463     if( rc==SQLITE_ROW ){
   464    464       zUuid = db_column_text(&q1, 0);
   465    465       szU = db_column_int(&q1, 1);
   466    466       szC = db_column_bytes(&q1, 2);
................................................................................
   736    736     while( db_step(&q)==SQLITE_ROW ){
   737    737       blob_appendf(pXfer->pOut, "igot %s\n", db_column_text(&q, 0));
   738    738     }
   739    739     db_finalize(&q);
   740    740   }
   741    741   
   742    742   /*
   743         -** Send a single config card for configuration item zName
          743  +** Send a single old-style config card for configuration item zName.
          744  +**
          745  +** This routine and the functionality it implements is scheduled for
          746  +** removal on 2012-05-01.
   744    747   */
   745         -static void send_config_card(Xfer *pXfer, const char *zName){
          748  +static void send_legacy_config_card(Xfer *pXfer, const char *zName){
   746    749     if( zName[0]!='@' ){
   747    750       Blob val;
   748    751       blob_zero(&val);
   749    752       db_blob(&val, "SELECT value FROM config WHERE name=%Q", zName);
   750    753       if( blob_size(&val)>0 ){
   751    754         blob_appendf(pXfer->pOut, "config %s %d\n", zName, blob_size(&val));
   752    755         blob_append(pXfer->pOut, blob_buffer(&val), blob_size(&val));
................................................................................
   758    761       blob_zero(&content);
   759    762       configure_render_special_name(zName, &content);
   760    763       blob_appendf(pXfer->pOut, "config %s %d\n%s\n", zName,
   761    764                    blob_size(&content), blob_str(&content));
   762    765       blob_reset(&content);
   763    766     }
   764    767   }
   765         -
   766    768   
   767    769   /*
   768    770   ** Called when there is an attempt to transfer private content to and
   769    771   ** from a server without authorization.
   770    772   */
   771    773   static void server_private_xfer_not_authorized(void){
   772    774     @ error not\sauthorized\sto\ssync\sprivate\scontent
................................................................................
  1005   1007       ** Check for a valid login.  This has to happen before anything else.
  1006   1008       ** The client can send multiple logins.  Permissions are cumulative.
  1007   1009       */
  1008   1010       if( blob_eq(&xfer.aToken[0], "login")
  1009   1011        && xfer.nToken==4
  1010   1012       ){
  1011   1013         if( disableLogin ){
  1012         -        g.okRead = g.okWrite = g.okPrivate = 1;
         1014  +        g.okRead = g.okWrite = g.okPrivate = g.okAdmin = 1;
  1013   1015         }else{
  1014   1016           if( check_tail_hash(&xfer.aToken[2], xfer.pIn)
  1015   1017            || check_login(&xfer.aToken[1], &xfer.aToken[2], &xfer.aToken[3])
  1016   1018           ){
  1017   1019             cgi_reset_content();
  1018   1020             @ error login\sfailed
  1019   1021             nErr++;
................................................................................
  1027   1029       ** Request a configuration value
  1028   1030       */
  1029   1031       if( blob_eq(&xfer.aToken[0], "reqconfig")
  1030   1032        && xfer.nToken==2
  1031   1033       ){
  1032   1034         if( g.okRead ){
  1033   1035           char *zName = blob_str(&xfer.aToken[1]);
  1034         -        if( configure_is_exportable(zName) ){
  1035         -          send_config_card(&xfer, zName);
         1036  +        if( zName[0]=='/' ){
         1037  +          /* New style configuration transfer */
         1038  +          int groupMask = configure_name_to_mask(&zName[1], 0);
         1039  +          if( !g.okAdmin ) groupMask &= ~CONFIGSET_USER;
         1040  +          if( !g.okRdAddr ) groupMask &= ~CONFIGSET_ADDR;
         1041  +          configure_send_group(xfer.pOut, groupMask, 0);
         1042  +        }else if( configure_is_exportable(zName) ){
         1043  +          /* Old style configuration transfer */
         1044  +          send_legacy_config_card(&xfer, zName);
  1036   1045           }
  1037   1046         }
  1038   1047       }else
  1039   1048       
  1040   1049       /*   config NAME SIZE \n CONTENT
  1041   1050       **
  1042   1051       ** Receive a configuration value from the client.  This is only
................................................................................
  1050   1059         blob_extract(xfer.pIn, size, &content);
  1051   1060         if( !g.okAdmin ){
  1052   1061           cgi_reset_content();
  1053   1062           @ error not\sauthorized\sto\spush\sconfiguration
  1054   1063           nErr++;
  1055   1064           break;
  1056   1065         }
  1057         -      if( !recvConfig ){
         1066  +      if( !recvConfig && zName[0]=='@' ){
  1058   1067           configure_prepare_to_receive(0);
  1059   1068           recvConfig = 1;
  1060   1069         }
  1061   1070         configure_receive(zName, &content, CONFIGSET_ALL);
  1062   1071         blob_reset(&content);
  1063   1072         blob_seek(xfer.pIn, 1, BLOB_SEEK_CUR);
  1064   1073       }else
................................................................................
  1183   1192   ** server in gdb:
  1184   1193   **
  1185   1194   **     gdb fossil
  1186   1195   **     r test-xfer out.txt
  1187   1196   */
  1188   1197   void cmd_test_xfer(void){
  1189   1198     int notUsed;
         1199  +  db_find_and_open_repository(0,0);
  1190   1200     if( g.argc!=2 && g.argc!=3 ){
  1191   1201       usage("?MESSAGEFILE?");
  1192   1202     }
  1193         -  db_must_be_within_tree();
  1194   1203     blob_zero(&g.cgiIn);
  1195   1204     blob_read_from_file(&g.cgiIn, g.argc==2 ? "-" : g.argv[2]);
  1196   1205     disableLogin = 1;
  1197   1206     page_xfer();
  1198   1207     printf("%s\n", cgi_extract_content(&notUsed));
  1199   1208   }
  1200   1209   
................................................................................
  1329   1338         const char *zName;
  1330   1339         zName = configure_first_name(configRcvMask);
  1331   1340         while( zName ){
  1332   1341           blob_appendf(&send, "reqconfig %s\n", zName);
  1333   1342           zName = configure_next_name(configRcvMask);
  1334   1343           nCardSent++;
  1335   1344         }
  1336         -      if( configRcvMask & (CONFIGSET_USER|CONFIGSET_TKT) ){
  1337         -        configure_prepare_to_receive(0);
         1345  +      if( (configRcvMask & (CONFIGSET_USER|CONFIGSET_TKT))!=0
         1346  +       && (configRcvMask & CONFIGSET_OLDFORMAT)!=0
         1347  +      ){
         1348  +        int overwrite = (configRcvMask & CONFIGSET_OVERWRITE)!=0;
         1349  +        configure_prepare_to_receive(overwrite);
  1338   1350         }
  1339   1351         origConfigRcvMask = configRcvMask;
  1340   1352         configRcvMask = 0;
  1341   1353       }
  1342   1354   
  1343   1355       /* Send configuration parameters being pushed */
  1344   1356       if( configSendMask ){
  1345         -      const char *zName;
  1346         -      zName = configure_first_name(configSendMask);
  1347         -      while( zName ){
  1348         -        send_config_card(&xfer, zName);
  1349         -        zName = configure_next_name(configSendMask);
  1350         -        nCardSent++;
         1357  +      if( configSendMask & CONFIGSET_OLDFORMAT ){
         1358  +        const char *zName;
         1359  +        zName = configure_first_name(configSendMask);
         1360  +        while( zName ){
         1361  +          send_legacy_config_card(&xfer, zName);
         1362  +          zName = configure_next_name(configSendMask);
         1363  +          nCardSent++;
         1364  +        }
         1365  +      }else{
         1366  +        nCardSent += configure_send_group(xfer.pOut, configSendMask, 0);
  1351   1367         }
  1352   1368         configSendMask = 0;
  1353   1369       }
  1354   1370   
  1355   1371       /* Append randomness to the end of the message.  This makes all
  1356   1372       ** messages unique so that that the login-card nonce will always
  1357   1373       ** be unique.
................................................................................
  1645   1661           fossil_warning("%b", &xfer.err);
  1646   1662           nErr++;
  1647   1663           break;
  1648   1664         }
  1649   1665         blobarray_reset(xfer.aToken, xfer.nToken);
  1650   1666         blob_reset(&xfer.line);
  1651   1667       }
  1652         -    if( origConfigRcvMask & (CONFIGSET_TKT|CONFIGSET_USER) ){
         1668  +    if( (configRcvMask & (CONFIGSET_USER|CONFIGSET_TKT))!=0
         1669  +     && (configRcvMask & CONFIGSET_OLDFORMAT)!=0
         1670  +    ){
  1653   1671         configure_finalize_receive();
  1654   1672       }
  1655   1673       origConfigRcvMask = 0;
  1656   1674       if( nCardRcvd>0 ){
  1657   1675         fossil_print(zValueFormat, "Received:",
  1658   1676                      blob_size(&recv), nCardRcvd,
  1659   1677                      xfer.nFileRcvd, xfer.nDeltaRcvd + xfer.nDanglingFile);