Fossil

Check-in Differences
Login

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

Difference From:

[9718f3b0] Version 2.6 (user: drh tags: trunk, release, version-2.6, date: 2018-05-04 12:56:42)

To:

[0cb83dec] Initialize variable to quell compiler warning about potentially unitialized variable. (user: andybradford tags: trunk, date: 2018-07-16 13:33:39)

Changes to Makefile.classic.

    40     40   # To use the included miniz library
    41     41   # FOSSIL_ENABLE_MINIZ = 1
    42     42   # TCC += -DFOSSIL_ENABLE_MINIZ
    43     43   
    44     44   # To add support for HTTPS
    45     45   TCC += -DFOSSIL_ENABLE_SSL
    46     46   
           47  +# To enable legacy mv/rm support
           48  +TCC += -DFOSSIL_ENABLE_LEGACY_MV_RM=1
           49  +
    47     50   #### We sometimes add the -static option here so that we can build a
    48     51   #    static executable that will run in a chroot jail.
    49     52   #LIB = -static
    50     53   TCC += -DFOSSIL_DYNAMIC_BUILD=1
    51     54   
    52     55   #### Extra arguments for linking the finished binary.  Fossil needs
    53     56   #    to link against the Z-Lib compression library unless the miniz

Changes to Makefile.in.

    37     37   #    care about testing the end result, this can be blank.
    38     38   #
    39     39   TCLSH = tclsh
    40     40   
    41     41   CFLAGS = @CFLAGS@
    42     42   LIB =	@LDFLAGS@ @EXTRA_LDFLAGS@ @LIBS@
    43     43   BCCFLAGS =	@CPPFLAGS@ @CFLAGS@
    44         -TCCFLAGS =	@EXTRA_CFLAGS@ @CPPFLAGS@ @CFLAGS@ -DHAVE_AUTOCONFIG_H -D_HAVE_SQLITE_CONFIG_H
           44  +TCCFLAGS =	@EXTRA_CFLAGS@ @CPPFLAGS@ @CFLAGS@ -DHAVE_AUTOCONFIG_H -D_HAVE_SQLITE_CONFIG_H -DFOSSIL_ENABLE_LEGACY_MV_RM=1
    45     45   INSTALLDIR = $(DESTDIR)@prefix@/bin
    46     46   USE_SYSTEM_SQLITE = @USE_SYSTEM_SQLITE@
    47     47   USE_LINENOISE = @USE_LINENOISE@
    48     48   USE_MMAN_H = @USE_MMAN_H@
    49     49   USE_SEE = @USE_SEE@
    50     50   FOSSIL_ENABLE_MINIZ = @FOSSIL_ENABLE_MINIZ@
    51     51   

Added Makefile.osx-jaguar.

            1  +#!/usr/bin/make
            2  +#
            3  +# This is a specially modified version of the Makefile that will build
            4  +# Fossil on Mac OSX Jaguar (10.2) circa 2002.  This Makefile is used for
            5  +# testing on an old PPC iBook.  The use of this old platform helps to verify
            6  +# Fossil and SQLite running on big-endian hardware.
            7  +#
            8  +# To build with this Makefile, run:
            9  +#
           10  +#     make -f Makefile.osx-jaguar
           11  +#
           12  +#
           13  +# This is the top-level makefile for Fossil when the build is occurring
           14  +# on a unix platform.  This works out-of-the-box on most unix platforms.
           15  +# But you are free to vary some of the definitions if desired.
           16  +#
           17  +#### The toplevel directory of the source tree.  Fossil can be built
           18  +#    in a directory that is separate from the source tree.  Just change
           19  +#    the following to point from the build directory to the src/ folder.
           20  +#
           21  +SRCDIR = ./src
           22  +
           23  +#### The directory into which object code files should be written.
           24  +#    Having a "./" prefix in the value of this variable breaks our use of the
           25  +#    "makeheaders" tool when running make on the MinGW platform, apparently
           26  +#    due to some command line argument manipulation performed automatically
           27  +#    by the shell.
           28  +#
           29  +#
           30  +OBJDIR = bld
           31  +
           32  +#### C Compiler and options for use in building executables that
           33  +#    will run on the platform that is doing the build.  This is used
           34  +#    to compile code-generator programs as part of the build process.
           35  +#    See TCC below for the C compiler for building the finished binary.
           36  +#
           37  +BCC = cc
           38  +
           39  +#### The suffix to add to final executable file.  When cross-compiling
           40  +#    to windows, make this ".exe".  Otherwise leave it blank.
           41  +#
           42  +E = 
           43  +
           44  +TCC = cc
           45  +
           46  +#### Tcl shell for use in running the fossil testsuite.  If you do not
           47  +#    care about testing the end result, this can be blank.
           48  +#
           49  +TCLSH = tclsh
           50  +
           51  +# LIB =	  -lz
           52  +LIB = compat/zlib/libz.a
           53  +TCC +=	  -g -O0 -DHAVE_AUTOCONFIG_H
           54  +TCC += -Icompat/zlib
           55  +TCC += -DSQLITE_WITHOUT_ZONEMALLOC
           56  +TCC += -D_BSD_SOURCE=1
           57  +TCC += -DWITHOUT_ICONV
           58  +TCC += -Dsocklen_t=int
           59  +TCC += -DSQLITE_MAX_MMAP_SIZE=0
           60  +TCC += -DFOSSIL_ENABLE_LEGACY_MV_RM=1
           61  +INSTALLDIR = $(DESTDIR)/usr/local/bin
           62  +USE_SYSTEM_SQLITE = 
           63  +USE_LINENOISE = 1
           64  +# FOSSIL_ENABLE_TCL = @FOSSIL_ENABLE_TCL@
           65  +FOSSIL_ENABLE_TCL = 0
           66  +FOSSIL_ENABLE_MINIZ = 0
           67  +
           68  +include $(SRCDIR)/main.mk
           69  +
           70  +distclean: clean
           71  +	rm -f autoconfig.h config.log Makefile

Changes to auto.def.

     6      6       with-openssl:path|auto|tree|none
     7      7                            => {Look for OpenSSL in the given path, automatically, in the source tree, or none}
     8      8       with-miniz=0         => {Use miniz from the source tree}
     9      9       with-zlib:path|auto|tree
    10     10                            => {Look for zlib in the given path, automatically, or in the source tree}
    11     11       with-exec-rel-paths=0
    12     12                            => {Enable relative paths for external diff/gdiff}
    13         -    with-legacy-mv-rm=0  => {Enable legacy behavior for mv/rm (skip checkout files)}
           13  +    with-legacy-mv-rm=1  => {Enable legacy behavior for mv/rm (skip checkout files)}
    14     14       with-th1-docs=0      => {Enable TH1 for embedded documentation pages}
    15     15       with-th1-hooks=0     => {Enable TH1 hooks for commands and web pages}
    16     16       with-tcl:path        => {Enable Tcl integration, with Tcl in the specified path}
    17     17       with-tcl-stubs=0     => {Enable Tcl integration via stubs library mechanism}
    18     18       with-tcl-private-stubs=0
    19     19                            => {Enable Tcl integration via private stubs mechanism}
    20     20       with-mman=0          => {Enable use of POSIX memory APIs from "sys/mman.h"}
................................................................................
   476    476   cc-check-function-in-lib gethostbyname nsl
   477    477   if {![cc-check-function-in-lib socket {socket network}]} {
   478    478       # Last resort, may be Windows
   479    479       if {[is_mingw]} {
   480    480           define-append LIBS -lwsock32
   481    481       }
   482    482   }
          483  +cc-check-function-in-lib ns_name_uncompress resolv
   483    484   cc-check-functions utime
   484    485   cc-check-functions usleep
   485    486   cc-check-functions strchrnul
   486    487   cc-check-functions pledge
   487    488   
   488    489   # Check for getloadavg(), and if it doesn't exist, define FOSSIL_OMIT_LOAD_AVERAGE
   489    490   if {![cc-check-functions getloadavg]} {
................................................................................
   492    493   }
   493    494   
   494    495   # Check for getpassphrase() for Solaris 10 where getpass() truncates to 10 chars
   495    496   if {![cc-check-functions getpassphrase]} {
   496    497       # Haiku needs this
   497    498       cc-check-function-in-lib getpass bsd
   498    499   }
   499         -cc-check-function-in-lib dlopen dl
   500    500   cc-check-function-in-lib sin m
   501    501   
   502    502   # Check for the FuseFS library
   503    503   if {[opt-bool fusefs]} {
   504    504     if {[cc-check-function-in-lib fuse_mount fuse]} {
   505    505        define-append EXTRA_CFLAGS -DFOSSIL_HAVE_FUSEFS
   506    506        define FOSSIL_HAVE_FUSEFS 1
   507    507        define-append LIBS -lfuse
   508    508        msg-result "FuseFS support enabled"
   509    509     }
   510    510   }
          511  +
          512  +# Finally, append -ldl to make sure it's the last in the list.
          513  +# The library order matters in case of static linking.
          514  +if {[check-function-in-lib dlopen dl]} {
          515  +    # Some platforms (*BSD) have the dl functions already in libc and no libdl.
          516  +    # In such case we can link directly without -ldl.
          517  +    define-append LIBS [get-define lib_dlopen]
          518  +}
   511    519   
   512    520   make-template Makefile.in
   513    521   make-config-header autoconfig.h -auto {USE_* FOSSIL_*}

Changes to src/add.c.

   251    251   ** Make arrangements to add one or more files or directories to the
   252    252   ** current checkout at the next commit.
   253    253   **
   254    254   ** When adding files or directories recursively, filenames that begin
   255    255   ** with "." are excluded by default.  To include such files, add
   256    256   ** the "--dotfiles" option to the command-line.
   257    257   **
   258         -** The --ignore and --clean options are comma-separate lists of glob patterns
          258  +** The --ignore and --clean options are comma-separated lists of glob patterns
   259    259   ** for files to be excluded.  Example:  '*.o,*.obj,*.exe'  If the --ignore
   260    260   ** option does not appear on the command line then the "ignore-glob" setting
   261    261   ** is used.  If the --clean option does not appear on the command line then
   262    262   ** the "clean-glob" setting is used.
   263    263   **
   264    264   ** If files are attempted to be added explicitly on the command line which
   265    265   ** match "ignore-glob", a confirmation is asked first. This can be prevented
................................................................................
   857    857   void mv_cmd(void){
   858    858     int i;
   859    859     int vid;
   860    860     int moveFiles;
   861    861     int dryRunFlag;
   862    862     int softFlag;
   863    863     int hardFlag;
          864  +  int origType;
          865  +  int destType;
   864    866     char *zDest;
   865    867     Blob dest;
   866    868     Stmt q;
   867    869   
   868    870     db_must_be_within_tree();
   869    871     dryRunFlag = find_option("dry-run","n",0)!=0;
   870    872     softFlag = find_option("soft",0,0)!=0;
................................................................................
   871    873     hardFlag = find_option("hard",0,0)!=0;
   872    874   
   873    875     /* We should be done with options.. */
   874    876     verify_all_options();
   875    877   
   876    878     vid = db_lget_int("checkout", 0);
   877    879     if( vid==0 ){
   878         -    fossil_fatal("no checkout rename files in");
          880  +    fossil_fatal("no checkout in which to rename files");
   879    881     }
   880    882     if( g.argc<4 ){
   881    883       usage("OLDNAME NEWNAME");
   882    884     }
   883    885     zDest = g.argv[g.argc-1];
   884    886     db_begin_transaction();
   885    887     if( g.argv[1][0]=='r' ){ /* i.e. "rename" */
................................................................................
   898    900     file_tree_name(zDest, &dest, 0, 1);
   899    901     db_multi_exec(
   900    902       "UPDATE vfile SET origname=pathname WHERE origname IS NULL;"
   901    903     );
   902    904     db_multi_exec(
   903    905       "CREATE TEMP TABLE mv(f TEXT UNIQUE ON CONFLICT IGNORE, t TEXT);"
   904    906     );
   905         -  if( file_isdir(zDest, RepoFILE)!=1 ){
          907  +  if( g.argc!=4 ){
          908  +    origType = -1;
          909  +  }else{
          910  +    origType = (file_isdir(g.argv[2], RepoFILE) == 1);
          911  +  }
          912  +  destType = file_isdir(zDest, RepoFILE);
          913  +  if( origType==-1 && destType!=1 ){
          914  +    usage("OLDNAME NEWNAME");
          915  +  }else if( origType==1 && destType==2 ){
          916  +    fossil_fatal("cannot rename '%s' to '%s' since another file named"
          917  +                 " '%s' exists", g.argv[2], zDest, zDest);
          918  +  }else if( origType==0 && destType!=1 ){
   906    919       Blob orig;
   907         -    if( g.argc!=4 ){
   908         -      usage("OLDNAME NEWNAME");
   909         -    }
   910    920       file_tree_name(g.argv[2], &orig, 0, 1);
   911    921       db_multi_exec(
   912    922         "INSERT INTO mv VALUES(%B,%B)", &orig, &dest
   913    923       );
   914    924     }else{
   915    925       if( blob_eq(&dest, ".") ){
   916    926         blob_reset(&dest);
................................................................................
   934    944         );
   935    945         while( db_step(&q)==SQLITE_ROW ){
   936    946           const char *zPath = db_column_text(&q, 0);
   937    947           int nPath = db_column_bytes(&q, 0);
   938    948           const char *zTail;
   939    949           if( nPath==nOrig ){
   940    950             zTail = file_tail(zPath);
          951  +        }else if( destType==1 ){
          952  +          zTail = &zPath[nOrig-strlen(file_tail(zOrig))];
   941    953           }else{
   942    954             zTail = &zPath[nOrig+1];
   943    955           }
   944    956           db_multi_exec(
   945    957             "INSERT INTO mv VALUES('%q','%q%q')",
   946    958             zPath, blob_str(&dest), zTail
   947    959           );

Changes to src/attach.c.

   375    375       zTargetType = mprintf("Ticket <a href=\"%R/tktview/%s\">%S</a>",
   376    376                             zTkt, zTkt);
   377    377     }
   378    378     if( zFrom==0 ) zFrom = mprintf("%s/home", g.zTop);
   379    379     if( P("cancel") ){
   380    380       cgi_redirect(zFrom);
   381    381     }
   382         -  if( P("ok") && szContent>0 && (goodCaptcha = captcha_is_correct()) ){
          382  +  if( P("ok") && szContent>0 && (goodCaptcha = captcha_is_correct(0)) ){
   383    383       int needModerator = (zTkt!=0 && ticket_need_moderation(0)) ||
   384    384                           (zPage!=0 && wiki_need_moderation(0));
   385    385       const char *zComment = PD("comment", "");
   386    386       attach_commit(zName, zTarget, aContent, szContent, needModerator, zComment);
   387    387       cgi_redirect(zFrom);
   388    388     }
   389    389     style_header("Add Attachment");

Changes to src/blob.c.

   291    291         blob_panic();
   292    292       }
   293    293     }
   294    294     memcpy(&pBlob->aData[pBlob->nUsed], aData, nData);
   295    295     pBlob->nUsed += nData;
   296    296     pBlob->aData[pBlob->nUsed] = 0;   /* Blobs are always nul-terminated */
   297    297   }
          298  +
          299  +/*
          300  +** Append a single character to the blob
          301  +*/
          302  +void blob_append_char(Blob *pBlob, char c){
          303  +  if( pBlob->nUsed+1 >= pBlob->nAlloc ){
          304  +    pBlob->xRealloc(pBlob, pBlob->nUsed + pBlob->nAlloc + 100);
          305  +    if( pBlob->nUsed + 1 >= pBlob->nAlloc ){
          306  +      blob_panic();
          307  +    }
          308  +  }
          309  +  pBlob->aData[pBlob->nUsed++] = c;    
          310  +}
   298    311   
   299    312   /*
   300    313   ** Copy a blob
   301    314   */
   302    315   void blob_copy(Blob *pTo, Blob *pFrom){
   303    316     blob_is_init(pFrom);
   304    317     blob_zero(pTo);
................................................................................
   307    320   
   308    321   /*
   309    322   ** Return a pointer to a null-terminated string for a blob.
   310    323   */
   311    324   char *blob_str(Blob *p){
   312    325     blob_is_init(p);
   313    326     if( p->nUsed==0 ){
   314         -    blob_append(p, "", 1); /* NOTE: Changes nUsed. */
          327  +    blob_append_char(p, 0); /* NOTE: Changes nUsed. */
   315    328       p->nUsed = 0;
   316    329     }
   317    330     if( p->aData[p->nUsed]!=0 ){
   318    331       blob_materialize(p);
   319    332     }
   320    333     return p->aData;
   321    334   }
................................................................................
   324    337   ** Return a pointer to a null-terminated string for a blob that has
   325    338   ** been created using blob_append_sql() and not blob_appendf().  If
   326    339   ** text was ever added using blob_appendf() then throw an error.
   327    340   */
   328    341   char *blob_sql_text(Blob *p){
   329    342     blob_is_init(p);
   330    343     if( (p->blobFlags & BLOBFLAG_NotSQL) ){
   331         -    fossil_fatal("Internal error: Use of blob_appendf() to construct SQL text");
          344  +    fossil_panic("use of blob_appendf() to construct SQL text");
   332    345     }
   333    346     return blob_str(p);
   334    347   }
   335    348   
   336    349   
   337    350   /*
   338    351   ** Return a pointer to a null-terminated string for a blob.
................................................................................
   478    491   
   479    492   /*
   480    493   ** Rewind the cursor on a blob back to the beginning.
   481    494   */
   482    495   void blob_rewind(Blob *p){
   483    496     p->iCursor = 0;
   484    497   }
          498  +
          499  +/*
          500  +** Truncate a blob back to zero length
          501  +*/
          502  +void blob_truncate(Blob *p, int sz){
          503  +  if( sz>=0 && sz<p->nUsed ) p->nUsed = sz;
          504  +}
   485    505   
   486    506   /*
   487    507   ** Seek the cursor in a blob to the indicated offset.
   488    508   */
   489    509   int blob_seek(Blob *p, int offset, int whence){
   490    510     if( whence==BLOB_SEEK_SET ){
   491    511       p->iCursor = offset;
................................................................................
   651    671       i++;
   652    672     }
   653    673     if( pTo ){
   654    674       blob_append(pTo, &pFrom->aData[pFrom->iCursor], i - pFrom->iCursor);
   655    675     }
   656    676     pFrom->iCursor = i;
   657    677   }
          678  +
          679  +/*
          680  +** Ensure that the text in pBlob ends with '\n'
          681  +*/
          682  +void blob_add_final_newline(Blob *pBlob){
          683  +  if( pBlob->nUsed<=0 ) return;
          684  +  if( pBlob->aData[pBlob->nUsed-1]!='\n' ){
          685  +    blob_append_char(pBlob, '\n');
          686  +  }
          687  +}
   658    688   
   659    689   /*
   660    690   ** Return true if the blob contains a valid base16 identifier artifact hash.
   661    691   **
   662    692   ** The value returned is actually one of HNAME_SHA1 OR HNAME_K256 if the
   663    693   ** hash is valid.  Both of these are non-zero and therefore "true".
   664    694   ** If the hash is not valid, then HNAME_ERROR is returned, which is zero or
................................................................................
  1235   1265                      zIn, blob_str(&bad), c);
  1236   1266       }
  1237   1267       if( !needEscape && !fossil_isalnum(c) && c!='/' && c!='.' && c!='_' ){
  1238   1268         needEscape = 1;
  1239   1269       }
  1240   1270     }
  1241   1271     if( n>0 && !fossil_isspace(z[n-1]) ){
  1242         -    blob_append(pBlob, " ", 1);
         1272  +    blob_append_char(pBlob, ' ');
  1243   1273     }
  1244         -  if( needEscape ) blob_append(pBlob, &cQuote, 1);
         1274  +  if( needEscape ) blob_append_char(pBlob, cQuote);
  1245   1275     if( zIn[0]=='-' ) blob_append(pBlob, "./", 2);
  1246   1276     blob_append(pBlob, zIn, -1);
  1247         -  if( needEscape ) blob_append(pBlob, &cQuote, 1);
         1277  +  if( needEscape ) blob_append_char(pBlob, cQuote);
  1248   1278   }
  1249   1279   
  1250   1280   /*
  1251   1281   ** A read(2)-like impl for the Blob class. Reads (copies) up to nLen
  1252   1282   ** bytes from pIn, starting at position pIn->iCursor, and copies them
  1253   1283   ** to pDest (which must be valid memory at least nLen bytes long).
  1254   1284   **
................................................................................
  1311   1341           /* swap bytes of unicode representation */
  1312   1342           char zTemp = zUtf8[--i];
  1313   1343           zUtf8[i] = zUtf8[i-1];
  1314   1344           zUtf8[--i] = zTemp;
  1315   1345         }
  1316   1346       }
  1317   1347       /* Make sure the blob contains two terminating 0-bytes */
  1318         -    blob_append(pBlob, "", 1);
         1348  +    blob_append_char(pBlob, 0);
  1319   1349       zUtf8 = blob_str(pBlob) + bomSize;
  1320   1350       zUtf8 = fossil_unicode_to_utf8(zUtf8);
  1321   1351       blob_set_dynamic(pBlob, zUtf8);
  1322   1352     }else if( useMbcs && invalid_utf8(pBlob) ){
  1323   1353   #if defined(_WIN32) || defined(__CYGWIN__)
  1324   1354       zUtf8 = fossil_mbcs_to_utf8(blob_str(pBlob));
  1325   1355       blob_reset(pBlob);

Changes to src/bundle.c.

    58     58     char *zErrMsg = 0;
    59     59     char *zSql;
    60     60     if( !doInit && file_size(zFile, ExtFILE)<0 ){
    61     61       fossil_fatal("no such file: %s", zFile);
    62     62     }
    63     63     assert( g.db );
    64     64     zSql = sqlite3_mprintf("ATTACH %Q AS %Q", zFile, zBName);
    65         -  if( zSql==0 ) fossil_fatal("out of memory");
           65  +  if( zSql==0 ) fossil_panic("out of memory");
    66     66     rc = sqlite3_exec(g.db, zSql, 0, 0, &zErrMsg);
    67     67     sqlite3_free(zSql);
    68     68     if( rc!=SQLITE_OK || zErrMsg ){
    69     69       if( zErrMsg==0 ) zErrMsg = (char*)sqlite3_errmsg(g.db);
    70     70       fossil_fatal("not a valid bundle: %s", zFile);
    71     71     }
    72     72     if( doInit ){
    73     73       db_multi_exec(zBundleInit /*works-like:"%w%w"*/, zBName, zBName);
    74     74     }else{
    75     75       sqlite3_stmt *pStmt;
    76     76       zSql = sqlite3_mprintf("SELECT bcname, bcvalue"
    77     77                              "  FROM \"%w\".bconfig", zBName);
    78         -    if( zSql==0 ) fossil_fatal("out of memory");
           78  +    if( zSql==0 ) fossil_panic("out of memory");
    79     79       rc = sqlite3_prepare(g.db, zSql, -1, &pStmt, 0);
    80     80       if( rc ) fossil_fatal("not a valid bundle: %s", zFile);
    81     81       sqlite3_free(zSql);
    82     82       sqlite3_finalize(pStmt);
    83     83       zSql = sqlite3_mprintf("SELECT blobid, uuid, sz, delta, notes, data"
    84     84                              "  FROM \"%w\".bblob", zBName);
    85         -    if( zSql==0 ) fossil_fatal("out of memory");
           85  +    if( zSql==0 ) fossil_panic("out of memory");
    86     86       rc = sqlite3_prepare(g.db, zSql, -1, &pStmt, 0);
    87     87       if( rc ) fossil_fatal("not a valid bundle: %s", zFile);
    88     88       sqlite3_free(zSql);
    89     89       sqlite3_finalize(pStmt);
    90     90     }
    91     91   }
    92     92   

Changes to src/captcha.c.

   495    495   **
   496    496   ** If no captcha is required or if the correct captcha is supplied, return
   497    497   ** true (non-zero).
   498    498   **
   499    499   ** The query parameters examined are "captchaseed" for the seed value and
   500    500   ** "captcha" for text that the user types in response to the captcha prompt.
   501    501   */
   502         -int captcha_is_correct(void){
          502  +int captcha_is_correct(int bAlwaysNeeded){
   503    503     const char *zSeed;
   504    504     const char *zEntered;
   505    505     const char *zDecode;
   506    506     char z[30];
   507    507     int i;
   508         -  if( !captcha_needed() ){
          508  +  if( !bAlwaysNeeded && !captcha_needed() ){
   509    509       return 1;  /* No captcha needed */
   510    510     }
   511    511     zSeed = P("captchaseed");
   512    512     if( zSeed==0 ) return 0;
   513    513     zEntered = P("captcha");
   514    514     if( zEntered==0 || strlen(zEntered)!=8 ) return 0;
   515    515     zDecode = captcha_decode((unsigned int)atoi(zSeed));
................................................................................
   591    591         return 0;
   592    592       }
   593    593     }
   594    594   #endif
   595    595     zCookieName = mprintf("fossil-cc-%.10s", db_get("project-code","x"));
   596    596     zCookieValue = P(zCookieName);
   597    597     if( zCookieValue && atoi(zCookieValue)==1 ) return 0;
   598         -  if( captcha_is_correct() ){
          598  +  if( captcha_is_correct(0) ){
   599    599       cgi_set_cookie(zCookieName, "1", login_cookie_path(), 8*3600);
   600    600       return 0;
   601    601     }
   602    602   
   603    603     /* This appears to be a spider.  Offer the captcha */
   604    604     style_header("Verification");
   605    605     @ <form method='POST' action='%s(g.zPath)'>

Changes to src/cgi.c.

    54     54   ** does the same except "y" is returned in place of NULL if there is not match.
    55     55   */
    56     56   #define P(x)        cgi_parameter((x),0)
    57     57   #define PD(x,y)     cgi_parameter((x),(y))
    58     58   #define PT(x)       cgi_parameter_trimmed((x),0)
    59     59   #define PDT(x,y)    cgi_parameter_trimmed((x),(y))
    60     60   #define PB(x)       cgi_parameter_boolean(x)
           61  +#define PCK(x)      cgi_parameter_checked(x,1)
           62  +#define PIF(x,y)    cgi_parameter_checked(x,y)
    61     63   
    62     64   
    63     65   /*
    64     66   ** Destinations for output text.
    65     67   */
    66     68   #define CGI_HEADER   0
    67     69   #define CGI_BODY     1
................................................................................
   196    198   ** Append text to the header of an HTTP reply
   197    199   */
   198    200   void cgi_append_header(const char *zLine){
   199    201     blob_append(&extraHeader, zLine, -1);
   200    202   }
   201    203   
   202    204   /*
   203         -** Set a cookie.
          205  +** Set a cookie by queuing up the appropriate HTTP header output. If
          206  +** !g.isHTTP, this is a no-op.
   204    207   **
   205    208   ** Zero lifetime implies a session cookie.
   206    209   */
   207    210   void cgi_set_cookie(
   208    211     const char *zName,    /* Name of the cookie */
   209    212     const char *zValue,   /* Value of the cookie.  Automatically escaped */
   210    213     const char *zPath,    /* Path cookie applies to.  NULL means "/" */
   211    214     int lifetime          /* Expiration of the cookie in seconds from now */
   212    215   ){
   213         -  char *zSecure = "";
   214         -  if( zPath==0 ){
          216  +  char const *zSecure = "";
          217  +  if(!g.isHTTP) return /* e.g. JSON CLI mode, where g.zTop is not set */;
          218  +  else if( zPath==0 ){
   215    219       zPath = g.zTop;
   216    220       if( zPath[0]==0 ) zPath = "/";
   217    221     }
   218    222     if( g.zBaseURL!=0 && strncmp(g.zBaseURL, "https:", 6)==0 ){
   219    223       zSecure = " secure;";
   220    224     }
   221    225     if( lifetime>0 ){
................................................................................
   230    234   }
   231    235   
   232    236   
   233    237   /*
   234    238   ** Return true if the response should be sent with Content-Encoding: gzip.
   235    239   */
   236    240   static int is_gzippable(void){
          241  +  if( g.fNoHttpCompress ) return 0;
   237    242     if( strstr(PD("HTTP_ACCEPT_ENCODING", ""), "gzip")==0 ) return 0;
   238    243     return strncmp(zContentType, "text/", 5)==0
   239    244       || sqlite3_strglob("application/*xml", zContentType)==0
   240    245       || sqlite3_strglob("application/*javascript", zContentType)==0;
   241    246   }
   242    247   
   243    248   /*
................................................................................
   332    337         if( size>0 ){
   333    338           fwrite(blob_buffer(&cgiContent[i]), 1, size, g.httpOut);
   334    339         }
   335    340       }
   336    341     }
   337    342     fflush(g.httpOut);
   338    343     CGIDEBUG(("DONE\n"));
          344  +
          345  +  /* After the webpage has been sent, do any useful background
          346  +  ** processing.
          347  +  */
          348  +  if( iReplyStatus==200 && fossil_strcmp(zContentType,"test/html")==0 ){
          349  +    email_auto_exec();
          350  +  }
   339    351   }
   340    352   
   341    353   /*
   342    354   ** Do a redirect request to the URL given in the argument.
   343    355   **
   344    356   ** The URL must be relative to the base of the fossil server.
   345    357   */
................................................................................
   874    886     if( z==0 ){
   875    887       if( pLog ) fclose(pLog);
   876    888       pLog = 0;
   877    889       return;
   878    890     }
   879    891     if( pLog==0 ){
   880    892       char zFile[50];
          893  +#if defined(_WIN32)
   881    894       unsigned r;
   882    895       sqlite3_randomness(sizeof(r), &r);
   883    896       sqlite3_snprintf(sizeof(zFile), zFile, "httplog-%08x.txt", r);
          897  +#else
          898  +    sqlite3_snprintf(sizeof(zFile), zFile, "httplog-%05d.txt", getpid());
          899  +#endif
   884    900       pLog = fossil_fopen(zFile, "wb");
   885    901       if( pLog ){
   886    902         fprintf(stderr, "# open log on %s\n", zFile);
   887    903       }else{
   888    904         fprintf(stderr, "# failed to open %s\n", zFile);
   889    905         return;
   890    906       }
................................................................................
  1099   1115   */
  1100   1116   char *cgi_parameter_trimmed(const char *zName, const char *zDefault){
  1101   1117     const char *zIn;
  1102   1118     char *zOut;
  1103   1119     int i;
  1104   1120     zIn = cgi_parameter(zName, 0);
  1105   1121     if( zIn==0 ) zIn = zDefault;
         1122  +  if( zIn==0 ) return 0;
  1106   1123     while( fossil_isspace(zIn[0]) ) zIn++;
  1107   1124     zOut = fossil_strdup(zIn);
  1108   1125     for(i=0; zOut[i]; i++){}
  1109   1126     while( i>0 && fossil_isspace(zOut[i-1]) ) zOut[--i] = 0;
  1110   1127     return zOut;
  1111   1128   }
  1112   1129   
................................................................................
  1115   1132   ** or "no" or "off".
  1116   1133   */
  1117   1134   int cgi_parameter_boolean(const char *zName){
  1118   1135     const char *zIn = cgi_parameter(zName, 0);
  1119   1136     if( zIn==0 ) return 0;
  1120   1137     return zIn[0]==0 || is_truth(zIn);
  1121   1138   }
         1139  +
         1140  +/*
         1141  +** Return either an empty string "" or the string "checked" depending
         1142  +** on whether or not parameter zName has value iValue.  If parameter
         1143  +** zName does not exist, that is assumed to be the same as value 0.
         1144  +**
         1145  +** This routine implements the PCK(x) and PIF(x,y) macros.  The PIF(x,y)
         1146  +** macro generateds " checked" if the value of parameter x equals integer y.
         1147  +** PCK(x) is the same as PIF(x,1).  These macros are used to generate
         1148  +** the "checked" attribute on checkbox and radio controls of forms.
         1149  +*/
         1150  +const char *cgi_parameter_checked(const char *zName, int iValue){
         1151  +  const char *zIn = cgi_parameter(zName,0);
         1152  +  int x;
         1153  +  if( zIn==0 ){
         1154  +    x = 0;
         1155  +  }else if( !fossil_isdigit(zIn[0]) ){
         1156  +    x = is_truth(zIn);
         1157  +  }else{
         1158  +    x = atoi(zIn);
         1159  +  }
         1160  +  return x==iValue ? "checked" : "";
         1161  +}
  1122   1162   
  1123   1163   /*
  1124   1164   ** Return the name of the i-th CGI parameter.  Return NULL if there
  1125   1165   ** are fewer than i registered CGI parameters.
  1126   1166   */
  1127   1167   const char *cgi_parameter_name(int i){
  1128   1168     if( i>=0 && i<nUsedQP ){
................................................................................
  1179   1219   
  1180   1220   /*
  1181   1221   ** Print all query parameters on standard output.  Format the
  1182   1222   ** parameters as HTML.  This is used for testing and debugging.
  1183   1223   **
  1184   1224   ** Omit the values of the cookies unless showAll is true.
  1185   1225   */
  1186         -void cgi_print_all(int showAll){
         1226  +void cgi_print_all(int showAll, int onConsole){
  1187   1227     int i;
  1188   1228     cgi_parameter("","");  /* Force the parameters into sorted order */
  1189   1229     for(i=0; i<nUsedQP; i++){
  1190   1230       const char *zName = aParamQP[i].zName;
  1191   1231       if( !showAll ){
  1192   1232         if( fossil_stricmp("HTTP_COOKIE",zName)==0 ) continue;
  1193   1233         if( fossil_strnicmp("fossil-",zName,7)==0 ) continue;
  1194   1234       }
  1195         -    cgi_printf("%h = %h  <br />\n", zName, aParamQP[i].zValue);
         1235  +    if( onConsole ){
         1236  +      fossil_trace("%s = %s\n", zName, aParamQP[i].zValue);
         1237  +    }else{
         1238  +      cgi_printf("%h = %h  <br />\n", zName, aParamQP[i].zValue);
         1239  +    }
  1196   1240     }
  1197   1241   }
  1198   1242   
  1199   1243   /*
  1200   1244   ** Export all untagged query parameters (but not cookies or environment
  1201   1245   ** variables) as hidden values of a form.
  1202   1246   */
................................................................................
  1336   1380       *zInput = 0;
  1337   1381       zInput++;
  1338   1382       while( fossil_isspace(*zInput) ){ zInput++; }
  1339   1383     }
  1340   1384     if( zLeftOver ){ *zLeftOver = zInput; }
  1341   1385     return zResult;
  1342   1386   }
         1387  +
         1388  +/*
         1389  +** Determine the IP address on the other side of a connection.
         1390  +** Return a pointer to a string.  Or return 0 if unable.
         1391  +**
         1392  +** The string is held in a static buffer that is overwritten on
         1393  +** each call.
         1394  +*/
         1395  +char *cgi_remote_ip(int fd){
         1396  +#if 0
         1397  +  static char zIp[100];
         1398  +  struct sockaddr_in6 addr;
         1399  +  socklen_t sz = sizeof(addr);
         1400  +  if( getpeername(fd, &addr, &sz) ) return 0;
         1401  +  zIp[0] = 0;
         1402  +  if( inet_ntop(AF_INET6, &addr, zIp, sizeof(zIp))==0 ){
         1403  +    return 0;
         1404  +  }
         1405  +  return zIp;
         1406  +#else
         1407  +  struct sockaddr_in remoteName;
         1408  +  socklen_t size = sizeof(struct sockaddr_in);
         1409  +  if( getpeername(fd, (struct sockaddr*)&remoteName, &size) ) return 0;
         1410  +  return inet_ntoa(remoteName.sin_addr);
         1411  +#endif
         1412  +}
  1343   1413   
  1344   1414   /*
  1345   1415   ** This routine handles a single HTTP request which is coming in on
  1346   1416   ** g.httpIn and which replies on g.httpOut
  1347   1417   **
  1348   1418   ** The HTTP request is read from g.httpIn and is used to initialize
  1349   1419   ** entries in the cgi_parameter() hash, as if those entries were
................................................................................
  1350   1420   ** environment variables.  A call to cgi_init() completes
  1351   1421   ** the setup.  Once all the setup is finished, this procedure returns
  1352   1422   ** and subsequent code handles the actual generation of the webpage.
  1353   1423   */
  1354   1424   void cgi_handle_http_request(const char *zIpAddr){
  1355   1425     char *z, *zToken;
  1356   1426     int i;
  1357         -  struct sockaddr_in remoteName;
  1358         -  socklen_t size = sizeof(struct sockaddr_in);
  1359   1427     char zLine[2000];     /* A single line of input. */
  1360   1428     g.fullHttpReply = 1;
  1361   1429     if( fgets(zLine, sizeof(zLine),g.httpIn)==0 ){
  1362   1430       malformed_request("missing HTTP header");
  1363   1431     }
  1364   1432     blob_append(&g.httpHeader, zLine, -1);
  1365   1433     cgi_trace(zLine);
................................................................................
  1379   1447     }
  1380   1448     cgi_setenv("REQUEST_URI", zToken);
  1381   1449     cgi_setenv("SCRIPT_NAME", "");
  1382   1450     for(i=0; zToken[i] && zToken[i]!='?'; i++){}
  1383   1451     if( zToken[i] ) zToken[i++] = 0;
  1384   1452     cgi_setenv("PATH_INFO", zToken);
  1385   1453     cgi_setenv("QUERY_STRING", &zToken[i]);
  1386         -  if( zIpAddr==0 &&
  1387         -        getpeername(fileno(g.httpIn), (struct sockaddr*)&remoteName,
  1388         -                                &size)>=0
  1389         -  ){
  1390         -    zIpAddr = inet_ntoa(remoteName.sin_addr);
         1454  +  if( zIpAddr==0 ){
         1455  +    zIpAddr = cgi_remote_ip(fileno(g.httpIn));
  1391   1456     }
  1392   1457     if( zIpAddr ){
  1393   1458       cgi_setenv("REMOTE_ADDR", zIpAddr);
  1394   1459       g.zIpAddr = mprintf("%s", zIpAddr);
  1395   1460     }
  1396   1461   
  1397   1462     /* Get all the optional fields that follow the first line.
................................................................................
  1847   1912             int nErr = 0, fd;
  1848   1913             close(0);
  1849   1914             fd = dup(connection);
  1850   1915             if( fd!=0 ) nErr++;
  1851   1916             close(1);
  1852   1917             fd = dup(connection);
  1853   1918             if( fd!=1 ) nErr++;
  1854         -          if( !g.fAnyTrace ){
         1919  +          if( 0 && !g.fAnyTrace ){
  1855   1920               close(2);
  1856   1921               fd = dup(connection);
  1857   1922               if( fd!=2 ) nErr++;
  1858   1923             }
  1859   1924             close(connection);
  1860   1925             g.nPendingRequest = nchildren+1;
  1861   1926             g.nRequest = nRequest+1;
................................................................................
  1865   1930       }
  1866   1931       /* Bury dead children */
  1867   1932       if( nchildren ){
  1868   1933         while(1){
  1869   1934           int iStatus = 0;
  1870   1935           pid_t x = waitpid(-1, &iStatus, WNOHANG);
  1871   1936           if( x<=0 ) break;
         1937  +        if( WIFSIGNALED(iStatus) && g.fAnyTrace ){
         1938  +          fprintf(stderr, "/***** Child %d exited on signal %d (%s) *****/\n",
         1939  +                  x, WTERMSIG(iStatus), strsignal(WTERMSIG(iStatus)));
         1940  +        }
  1872   1941           nchildren--;
  1873   1942         }
  1874   1943       }  
  1875   1944     }
  1876   1945     /* NOT REACHED */
  1877   1946     fossil_exit(1);
  1878   1947   #endif
................................................................................
  1890   1959       {"Jan", "Feb", "Mar", "Apr", "May", "Jun",
  1891   1960        "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", 0};
  1892   1961   
  1893   1962   
  1894   1963   /*
  1895   1964   ** Returns an RFC822-formatted time string suitable for HTTP headers.
  1896   1965   ** The timezone is always GMT.  The value returned is always a
  1897         -** string obtained from mprintf() and must be freed using free() to
  1898         -** avoid a memory leak.
         1966  +** string obtained from mprintf() and must be freed using fossil_free()
         1967  +** to avoid a memory leak.
  1899   1968   **
  1900   1969   ** See http://www.faqs.org/rfcs/rfc822.html, section 5
  1901   1970   ** and http://www.faqs.org/rfcs/rfc2616.html, section 3.3.
  1902   1971   */
  1903   1972   char *cgi_rfc822_datestamp(time_t now){
  1904   1973     struct tm *pTm;
  1905   1974     pTm = gmtime(&now);
  1906   1975     if( pTm==0 ){
  1907   1976       return mprintf("");
  1908   1977     }else{
  1909         -    return mprintf("%s, %d %s %02d %02d:%02d:%02d GMT",
         1978  +    return mprintf("%s, %d %s %02d %02d:%02d:%02d +0000",
  1910   1979                      azDays[pTm->tm_wday], pTm->tm_mday, azMonths[pTm->tm_mon],
  1911   1980                      pTm->tm_year+1900, pTm->tm_hour, pTm->tm_min, pTm->tm_sec);
  1912   1981     }
  1913   1982   }
  1914   1983   
  1915   1984   /*
  1916   1985   ** Parse an RFC822-formatted timestamp as we'd expect from HTTP and return

Changes to src/checkin.c.

  1190   1190   #if defined(__CYGWIN__)
  1191   1191       zEditor = fossil_utf8_to_path(zEditor, 0);
  1192   1192       blob_add_cr(pPrompt);
  1193   1193   #endif
  1194   1194     }
  1195   1195   #endif
  1196   1196     if( zEditor==0 ){
  1197         -    blob_append(pPrompt,
  1198         -       "#\n"
  1199         -       "# Since no default text editor is set using EDITOR or VISUAL\n"
  1200         -       "# environment variables or the \"fossil set editor\" command,\n"
  1201         -       "# and because no comment was specified using the \"-m\" or \"-M\"\n"
  1202         -       "# command-line options, you will need to enter the comment below.\n"
  1203         -       "# Type \".\" on a line by itself when you are done:\n", -1);
         1197  +    if( blob_size(pPrompt)>0 ){
         1198  +      blob_append(pPrompt,
         1199  +         "#\n"
         1200  +         "# Since no default text editor is set using EDITOR or VISUAL\n"
         1201  +         "# environment variables or the \"fossil set editor\" command,\n"
         1202  +         "# and because no comment was specified using the \"-m\" or \"-M\"\n"
         1203  +         "# command-line options, you will need to enter the comment below.\n"
         1204  +         "# Type \".\" on a line by itself when you are done:\n", -1);
         1205  +    }
  1204   1206       zFile = mprintf("-");
  1205   1207     }else{
  1206   1208       Blob fname;
  1207   1209       blob_zero(&fname);
  1208   1210       if( g.zLocalRoot!=0 ){
  1209   1211         file_relative_name(g.zLocalRoot, &fname, 1);
  1210   1212         zFile = db_text(0, "SELECT '%qci-comment-'||hex(randomblob(6))||'.txt'",
................................................................................
  1214   1216         zFile = mprintf("%s", blob_str(&fname));
  1215   1217       }
  1216   1218       blob_reset(&fname);
  1217   1219     }
  1218   1220   #if defined(_WIN32)
  1219   1221     blob_add_cr(pPrompt);
  1220   1222   #endif
  1221         -  blob_write_to_file(pPrompt, zFile);
         1223  +  if( blob_size(pPrompt)>0 ) blob_write_to_file(pPrompt, zFile);
  1222   1224     if( zEditor ){
  1223   1225       zCmd = mprintf("%s \"%s\"", zEditor, zFile);
  1224   1226       fossil_print("%s\n", zCmd);
  1225   1227       if( fossil_system(zCmd) ){
  1226   1228         fossil_fatal("editor aborted: \"%s\"", zCmd);
  1227   1229       }
  1228   1230   

Changes to src/checkout.c.

   289    289         return;
   290    290       }
   291    291     }else{
   292    292       zVers = g.argv[2];
   293    293     }
   294    294     vid = load_vfile(zVers, forceMissingFlag);
   295    295     if( prior==vid ){
          296  +    db_end_transaction(0);
   296    297       return;
   297    298     }
   298    299     if( !keepFlag ){
   299    300       uncheckout(prior);
   300    301     }
   301    302     db_multi_exec("DELETE FROM vfile WHERE vid!=%d", vid);
   302    303     if( !keepFlag ){

Changes to src/clone.c.

   107    107   **
   108    108   ** By default, your current login name is used to create the default
   109    109   ** admin user. This can be overridden using the -A|--admin-user
   110    110   ** parameter.
   111    111   **
   112    112   ** Options:
   113    113   **    --admin-user|-A USERNAME   Make USERNAME the administrator
          114  +**    --nocompress               Omit extra delta compression
   114    115   **    --once                     Don't remember the URI.
   115    116   **    --private                  Also clone private branches
   116    117   **    --ssl-identity FILENAME    Use the SSL identity if requested by the server
   117    118   **    --ssh-command|-c SSH       Use SSH as the "ssh" command
   118    119   **    --httpauth|-B USER:PASS    Add HTTP Basic Authorization to requests
   119    120   **    -u|--unversioned           Also sync unversioned content
   120    121   **    -v|--verbose               Show more statistics in output
................................................................................
   124    125   void clone_cmd(void){
   125    126     char *zPassword;
   126    127     const char *zDefaultUser;   /* Optional name of the default user */
   127    128     const char *zHttpAuth;      /* HTTP Authorization user:pass information */
   128    129     int nErr = 0;
   129    130     int urlFlags = URL_PROMPT_PW | URL_REMEMBER;
   130    131     int syncFlags = SYNC_CLONE;
          132  +  int noCompress = find_option("nocompress",0,0)!=0;
   131    133   
   132    134     /* Also clone private branches */
   133    135     if( find_option("private",0,0)!=0 ) syncFlags |= SYNC_PRIVATE;
   134    136     if( find_option("once",0,0)!=0) urlFlags &= ~URL_REMEMBER;
   135    137     if( find_option("verbose","v",0)!=0) syncFlags |= SYNC_VERBOSE;
   136    138     if( find_option("unversioned","u",0)!=0 ) syncFlags |= SYNC_UNVERSIONED;
   137    139     zHttpAuth = find_option("httpauth","B",1);
................................................................................
   209    211         fossil_fatal("server returned an error - clone aborted");
   210    212       }
   211    213       db_open_repository(g.argv[3]);
   212    214     }
   213    215     db_begin_transaction();
   214    216     fossil_print("Rebuilding repository meta-data...\n");
   215    217     rebuild_db(0, 1, 0);
   216         -  fossil_print("Extra delta compression... "); fflush(stdout);
   217         -  extra_deltification();
          218  +  if( !noCompress ){
          219  +    fossil_print("Extra delta compression... "); fflush(stdout);
          220  +    extra_deltification();
          221  +    fossil_print("\n");
          222  +  }
   218    223     db_end_transaction(0);
   219         -  fossil_print("\nVacuuming the database... "); fflush(stdout);
          224  +  fossil_print("Vacuuming the database... "); fflush(stdout);
   220    225     if( db_int(0, "PRAGMA page_count")>1000
   221    226      && db_int(0, "PRAGMA page_size")<8192 ){
   222    227        db_multi_exec("PRAGMA page_size=8192;");
   223    228     }
   224    229     db_multi_exec("VACUUM");
   225    230     fossil_print("\nproject-id: %s\n", db_get("project-code", 0));
   226    231     fossil_print("server-id:  %s\n", db_get("server-code", 0));

Changes to src/codecheck1.c.

    37     37   */
    38     38   #include <stdio.h>
    39     39   #include <stdlib.h>
    40     40   #include <ctype.h>
    41     41   #include <string.h>
    42     42   #include <assert.h>
    43     43   
           44  +/*
           45  +** Debugging switch
           46  +*/
           47  +static int eVerbose = 0;
           48  +
    44     49   /*
    45     50   ** Malloc, aborting if it fails.
    46     51   */
    47     52   void *safe_malloc(int nByte){
    48     53     void *x = malloc(nByte);
    49     54     if( x==0 ){
    50     55       fprintf(stderr, "failed to allocate %d bytes\n", nByte);
................................................................................
   196    201   /*
   197    202   ** Return the first non-whitespace characters in z[]
   198    203   */
   199    204   static const char *skip_space(const char *z){
   200    205     while( isspace(z[0]) ){ z++; }
   201    206     return z;
   202    207   }
          208  +
          209  +/*
          210  +** Remove excess whitespace and nested "()" from string z.
          211  +*/
          212  +static char *simplify_expr(char *z){
          213  +  int n = (int)strlen(z);
          214  +  while( n>0 ){
          215  +    if( isspace(z[0]) ){
          216  +      z++;
          217  +      n--;
          218  +      continue;
          219  +    }
          220  +    if( z[0]=='(' && z[n-1]==')' ){
          221  +      z++;
          222  +      n -= 2;
          223  +      continue;
          224  +    }
          225  +    break;
          226  +  }
          227  +  z[n] = 0;
          228  +  return z;
          229  +}
   203    230   
   204    231   /*
   205    232   ** Return true if the input is a string literal.
   206    233   */
   207    234   static int is_string_lit(const char *z){
   208    235     int nu1, nu2;
   209    236     z = next_non_whitespace(z, &nu1, &nu2);
................................................................................
   266    293     "db_setting_inop_rhs",
   267    294   };
   268    295   
   269    296   /*
   270    297   ** Return true if the input is an argument that is safe to use with %s
   271    298   ** while building an SQL statement.
   272    299   */
   273         -static int is_s_safe(const char *z){
          300  +static int is_sql_safe(const char *z){
   274    301     int len, eType;
   275    302     int i;
   276    303   
   277    304     /* A string literal is safe for use with %s */
   278    305     if( is_string_lit(z) ) return 1;
   279    306   
   280    307     /* Certain functions are guaranteed to return a string that is safe
................................................................................
   295    322   
   296    323     /* If the "safe-for-%s" comment appears in the argument, then
   297    324     ** let it through */
   298    325     if( strstr(z, "/*safe-for-%s*/")!=0 ) return 1;
   299    326   
   300    327     return 0;
   301    328   }
          329  +
          330  +/*
          331  +** Return true if the input is an argument that is never safe for use
          332  +** with %s.
          333  +*/
          334  +static int never_safe(const char *z){
          335  +  if( strstr(z,"/*safe-for-%s*/")!=0 ) return 0;
          336  +  if( z[0]=='P' ){
          337  +    if( strncmp(z,"PIF(",4)==0 ) return 0;
          338  +    if( strncmp(z,"PCK(",4)==0 ) return 0;
          339  +    return 1;
          340  +  }
          341  +  if( strncmp(z,"cgi_param",9)==0 ) return 1;
          342  +  return 0;
          343  +}
   302    344   
   303    345   /*
   304    346   ** Processing flags
   305    347   */
   306         -#define FMT_NO_S   0x00001     /* Do not allow %s substitutions */
          348  +#define FMT_SQL   0x00001     /* Generates SQL text */
          349  +#define FMT_HTML  0x00002     /* Generates HTML text */
          350  +#define FMT_URL   0x00004     /* Generates URLs */
          351  +#define FMT_SAFE  0x00008     /* Always safe for %s */
   307    352   
   308    353   /*
   309    354   ** A list of internal Fossil interfaces that take a printf-style format
   310    355   ** string.
   311    356   */
   312    357   struct {
   313    358     const char *zFName;    /* Name of the function */
   314    359     int iFmtArg;           /* Index of format argument.  Leftmost is 1. */
   315    360     unsigned fmtFlags;     /* Processing flags */
   316    361   } aFmtFunc[] = {
   317    362     { "admin_log",               1, 0 },
   318         -  { "blob_append_sql",         2, FMT_NO_S },
          363  +  { "blob_append_sql",         2, FMT_SQL },
   319    364     { "blob_appendf",            2, 0 },
   320         -  { "cgi_debug",               1, 0 },
   321         -  { "cgi_panic",               1, 0 },
   322         -  { "cgi_printf",              1, 0 },
   323         -  { "cgi_redirectf",           1, 0 },
   324         -  { "chref",                   2, 0 },
   325         -  { "db_blob",                 2, FMT_NO_S },
   326         -  { "db_debug",                1, FMT_NO_S },
   327         -  { "db_double",               2, FMT_NO_S },
          365  +  { "cgi_debug",               1, FMT_SAFE },
          366  +  { "cgi_panic",               1, FMT_SAFE },
          367  +  { "cgi_printf",              1, FMT_HTML },
          368  +  { "cgi_redirectf",           1, FMT_URL },
          369  +  { "chref",                   2, FMT_URL },
          370  +  { "db_blob",                 2, FMT_SQL },
          371  +  { "db_debug",                1, FMT_SQL },
          372  +  { "db_double",               2, FMT_SQL },
   328    373     { "db_err",                  1, 0 },
   329         -  { "db_exists",               1, FMT_NO_S },
          374  +  { "db_exists",               1, FMT_SQL },
   330    375     { "db_get_mprintf",          2, 0 },
   331         -  { "db_int",                  2, FMT_NO_S },
   332         -  { "db_int64",                2, FMT_NO_S },
   333         -  { "db_multi_exec",           1, FMT_NO_S },
   334         -  { "db_optional_sql",         2, FMT_NO_S },
   335         -  { "db_prepare",              2, FMT_NO_S },
   336         -  { "db_prepare_ignore_error", 2, FMT_NO_S },
          376  +  { "db_int",                  2, FMT_SQL },
          377  +  { "db_int64",                2, FMT_SQL },
          378  +  { "db_multi_exec",           1, FMT_SQL },
          379  +  { "db_optional_sql",         2, FMT_SQL },
          380  +  { "db_prepare",              2, FMT_SQL },
          381  +  { "db_prepare_ignore_error", 2, FMT_SQL },
   337    382     { "db_set_mprintf",          3, 0 },
   338         -  { "db_static_prepare",       2, FMT_NO_S },
   339         -  { "db_text",                 2, FMT_NO_S },
          383  +  { "db_static_prepare",       2, FMT_SQL },
          384  +  { "db_text",                 2, FMT_SQL },
   340    385     { "db_unset_mprintf",        2, 0 },
   341         -  { "form_begin",              2, 0 },
   342         -  { "fossil_error",            2, 0 },
   343         -  { "fossil_errorlog",         1, 0 },
   344         -  { "fossil_fatal",            1, 0 },
   345         -  { "fossil_fatal_recursive",  1, 0 },
   346         -  { "fossil_panic",            1, 0 },
   347         -  { "fossil_print",            1, 0 },
   348         -  { "fossil_trace",            1, 0 },
   349         -  { "fossil_warning",          1, 0 },
   350         -  { "href",                    1, 0 },
          386  +  { "form_begin",              2, FMT_URL },
          387  +  { "fossil_error",            2, FMT_SAFE },
          388  +  { "fossil_errorlog",         1, FMT_SAFE },
          389  +  { "fossil_fatal",            1, FMT_SAFE },
          390  +  { "fossil_fatal_recursive",  1, FMT_SAFE },
          391  +  { "fossil_panic",            1, FMT_SAFE },
          392  +  { "fossil_print",            1, FMT_SAFE },
          393  +  { "fossil_trace",            1, FMT_SAFE },
          394  +  { "fossil_warning",          1, FMT_SAFE },
          395  +  { "href",                    1, FMT_URL },
   351    396     { "json_new_string_f",       1, 0 },
   352    397     { "json_set_err",            2, 0 },
   353    398     { "json_warn",               2, 0 },
   354    399     { "mprintf",                 1, 0 },
   355    400     { "socket_set_errmsg",       1, 0 },
   356    401     { "ssl_set_errmsg",          1, 0 },
   357         -  { "style_header",            1, 0 },
   358         -  { "style_set_current_page",  1, 0 },
   359         -  { "style_submenu_element",   2, 0 },
   360         -  { "style_submenu_sql",       3, 0 },
   361         -  { "webpage_error",           1, 0 },
   362         -  { "xhref",                   2, 0 },
          402  +  { "style_header",            1, FMT_HTML },
          403  +  { "style_set_current_page",  1, FMT_URL },
          404  +  { "style_submenu_element",   2, FMT_URL },
          405  +  { "style_submenu_sql",       3, FMT_SQL },
          406  +  { "webpage_error",           1, FMT_SAFE },
          407  +  { "xhref",                   2, FMT_URL },
   363    408   };
   364    409   
   365    410   /*
   366    411   ** Determine if the indentifier zIdent of length nIndent is a Fossil
   367    412   ** internal interface that uses a printf-style argument.  Return zero if not.
   368    413   ** Return the index of the format string if true with the left-most
   369    414   ** argument having an index of 1.
................................................................................
   462    507     zCopy = safe_malloc( len + 1 );
   463    508     memcpy(zCopy, zStart+1, len);
   464    509     zCopy[len] = 0;
   465    510     azArg = 0;
   466    511     nArg = 0;
   467    512     z = zCopy;
   468    513     while( z[0] ){
          514  +    char cEnd;
   469    515       len = distance_to(z, ',');
   470         -    azArg = safe_realloc((char*)azArg, (sizeof(azArg[0])+1)*(nArg+1));
   471         -    azArg[nArg++] = skip_space(z);
   472         -    if( z[len]==0 ) break;
          516  +    cEnd = z[len];
   473    517       z[len] = 0;
   474         -    for(i=len-1; i>0 && isspace(z[i]); i--){ z[i] = 0; }
          518  +    azArg = safe_realloc((char*)azArg, (sizeof(azArg[0])+1)*(nArg+1));
          519  +    azArg[nArg++] = simplify_expr(z);
          520  +    if( cEnd==0 ) break;
   475    521       z += len + 1;
   476    522     }
   477    523     acType = (char*)&azArg[nArg];
   478    524     if( fmtArg>nArg ){
   479    525       printf("%s:%d: too few arguments to %.*s()\n",
   480    526              zFilename, lnFCall, szFName, zFCall);
   481    527       nErr++;
................................................................................
   490    536       }else if( (k = formatArgCount(zFmt, nArg, acType))>=0
   491    537                && nArg!=fmtArg+k ){
   492    538         printf("%s:%d: too %s arguments to %.*s() "
   493    539                "- got %d and expected %d\n",
   494    540                zFilename, lnFCall, (nArg<fmtArg+k ? "few" : "many"),
   495    541                szFName, zFCall, nArg, fmtArg+k);
   496    542         nErr++;
   497         -    }else if( fmtFlags & FMT_NO_S ){
          543  +    }else if( (fmtFlags & FMT_SAFE)==0 ){
   498    544         for(i=0; i<nArg && i<k; i++){
   499         -        if( (acType[i]=='s' || acType[i]=='z' || acType[i]=='b')
   500         -         && !is_s_safe(azArg[fmtArg+i])
   501         -        ){
   502         -           printf("%s:%d: Argument %d to %.*s() not safe for SQL\n",
   503         -             zFilename, lnFCall, i+fmtArg, szFName, zFCall);
   504         -           nErr++;
          545  +        if( (acType[i]=='s' || acType[i]=='z' || acType[i]=='b') ){
          546  +          const char *zExpr = azArg[fmtArg+i];
          547  +          if( never_safe(zExpr) ){
          548  +            printf("%s:%d: Argument %d to %.*s() is not safe for"
          549  +                   " a query parameter\n",
          550  +               zFilename, lnFCall, i+fmtArg, szFName, zFCall);
          551  +             nErr++;
          552  +   
          553  +          }else if( (fmtFlags & FMT_SQL)!=0 && !is_sql_safe(zExpr) ){
          554  +            printf("%s:%d: Argument %d to %.*s() not safe for SQL\n",
          555  +               zFilename, lnFCall, i+fmtArg, szFName, zFCall);
          556  +             nErr++;
          557  +          }
   505    558           }
   506    559         }
   507    560       }
   508    561     }
   509    562     if( nErr ){
   510    563       for(i=0; i<nArg; i++){
   511    564         printf("   arg[%d]: %s\n", i, azArg[i]);
   512    565       }
          566  +  }else if( eVerbose>1 ){
          567  +    printf("%s:%d: %.*s() ok for %d arguments\n",
          568  +      zFilename, lnFCall, szFName, zFCall, nArg);
   513    569     }
   514         -
   515    570     free((char*)azArg);
   516    571     free(zCopy);
   517    572     return nErr;
   518    573   }
   519    574   
   520    575   
   521    576   /*
................................................................................
   561    616     }
   562    617     return nErr;
   563    618   }
   564    619   
   565    620   /*
   566    621   ** Check for format-string design rule violations on all files listed
   567    622   ** on the command-line.
          623  +**
          624  +** The eVerbose global variable is incremented with each "-v" argument.
   568    625   */
   569    626   int main(int argc, char **argv){
   570    627     int i;
   571    628     int nErr = 0;
   572    629     for(i=1; i<argc; i++){
   573         -    char *zFile = read_file(argv[i]);
          630  +    char *zFile;
          631  +    if( strcmp(argv[i],"-v")==0 ){
          632  +      eVerbose++;
          633  +      continue;
          634  +    }
          635  +    if( eVerbose>0 ) printf("Processing %s...\n", argv[i]);
          636  +    zFile = read_file(argv[i]);
   574    637       nErr += scan_file(argv[i], zFile);
   575    638       free(zFile);
   576    639     }
   577    640     return nErr;
   578    641   }

Changes to src/configure.c.

    34     34   #define CONFIGSET_TKT       0x000004     /* Ticket configuration */
    35     35   #define CONFIGSET_PROJ      0x000008     /* Project name */
    36     36   #define CONFIGSET_SHUN      0x000010     /* Shun settings */
    37     37   #define CONFIGSET_USER      0x000020     /* The USER table */
    38     38   #define CONFIGSET_ADDR      0x000040     /* The CONCEALED table */
    39     39   #define CONFIGSET_XFER      0x000080     /* Transfer configuration */
    40     40   #define CONFIGSET_ALIAS     0x000100     /* URL Aliases */
           41  +#define CONFIGSET_SCRIBER   0x000200     /* Email subscribers */
           42  +#define CONFIGSET_FORUM     0x000400     /* Forum posts */
    41     43   
    42         -#define CONFIGSET_ALL       0x0001ff     /* Everything */
           44  +#define CONFIGSET_ALL       0x0007ff     /* Everything */
    43     45   
    44     46   #define CONFIGSET_OVERWRITE 0x100000     /* Causes overwrite instead of merge */
    45     47   
    46     48   /*
    47     49   ** This mask is used for the common TH1 configuration settings (i.e. those
    48     50   ** that are not specific to one particular subsystem, such as the transfer
    49     51   ** subsystem).
................................................................................
    56     58   ** Names of the configuration sets
    57     59   */
    58     60   static struct {
    59     61     const char *zName;   /* Name of the configuration set */
    60     62     int groupMask;       /* Mask for that configuration set */
    61     63     const char *zHelp;   /* What it does */
    62     64   } aGroupName[] = {
    63         -  { "/email",        CONFIGSET_ADDR,  "Concealed email addresses in tickets" },
    64         -  { "/project",      CONFIGSET_PROJ,  "Project name and description"         },
    65         -  { "/skin",         CONFIGSET_SKIN | CONFIGSET_CSS,
    66         -                                      "Web interface appearance settings"    },
    67         -  { "/css",          CONFIGSET_CSS,   "Style sheet"                          },
    68         -  { "/shun",         CONFIGSET_SHUN,  "List of shunned artifacts"            },
    69         -  { "/ticket",       CONFIGSET_TKT,   "Ticket setup",                        },
    70         -  { "/user",         CONFIGSET_USER,  "Users and privilege settings"         },
    71         -  { "/xfer",         CONFIGSET_XFER,  "Transfer setup",                      },
    72         -  { "/alias",        CONFIGSET_ALIAS, "URL Aliases",                         },
    73         -  { "/all",          CONFIGSET_ALL,   "All of the above"                     },
           65  +  { "/email",       CONFIGSET_ADDR,  "Concealed email addresses in tickets" },
           66  +  { "/project",     CONFIGSET_PROJ,  "Project name and description"         },
           67  +  { "/skin",        CONFIGSET_SKIN | CONFIGSET_CSS,
           68  +                                     "Web interface appearance settings"    },
           69  +  { "/css",         CONFIGSET_CSS,   "Style sheet"                          },
           70  +  { "/shun",        CONFIGSET_SHUN,  "List of shunned artifacts"            },
           71  +  { "/ticket",      CONFIGSET_TKT,   "Ticket setup",                        },
           72  +  { "/user",        CONFIGSET_USER,  "Users and privilege settings"         },
           73  +  { "/xfer",        CONFIGSET_XFER,  "Transfer setup",                      },
           74  +  { "/alias",       CONFIGSET_ALIAS, "URL Aliases",                         },
           75  +  { "/subscriber",  CONFIGSET_SCRIBER,"Email notification subscriber list" },
           76  +/*{ "/forum",       CONFIGSET_FORUM, "Forum posts",                         },*/
           77  +  { "/all",         CONFIGSET_ALL,   "All of the above"                     },
    74     78   };
    75     79   
    76     80   
    77     81   /*
    78     82   ** The following is a list of settings that we are willing to
    79     83   ** transfer.
    80     84   **
................................................................................
   155    159   
   156    160     { "@concealed",             CONFIGSET_ADDR },
   157    161   
   158    162     { "@shun",                  CONFIGSET_SHUN },
   159    163   
   160    164     { "@alias",                 CONFIGSET_ALIAS },
   161    165   
          166  +  { "@subscriber",            CONFIGSET_SCRIBER },
          167  +
   162    168     { "xfer-common-script",     CONFIGSET_XFER },
   163    169     { "xfer-push-script",       CONFIGSET_XFER },
   164    170     { "xfer-commit-script",     CONFIGSET_XFER },
   165    171     { "xfer-ticket-script",     CONFIGSET_XFER },
   166    172   
   167    173   };
   168    174   static int iConfig = 0;
................................................................................
   211    217     return blob_sql_text(&x);
   212    218   }
   213    219   
   214    220   /*
   215    221   ** Return the mask for the named configuration parameter if it can be
   216    222   ** safely exported.  Return 0 if the parameter is not safe to export.
   217    223   **
   218         -** "Safe" in the previous paragraph means the permission is created to
          224  +** "Safe" in the previous paragraph means the permission is granted to
   219    225   ** export the property.  In other words, the requesting side has presented
   220    226   ** login credentials and has sufficient capabilities to access the requested
   221    227   ** information.
   222    228   */
   223    229   int configure_is_exportable(const char *zName){
   224    230     int i;
   225    231     int n = strlen(zName);
................................................................................
   227    233       zName++;
   228    234       n -= 2;
   229    235     }
   230    236     for(i=0; i<count(aConfig); i++){
   231    237       if( strncmp(zName, aConfig[i].zName, n)==0 && aConfig[i].zName[n]==0 ){
   232    238         int m = aConfig[i].groupMask;
   233    239         if( !g.perm.Admin ){
   234         -        m &= ~CONFIGSET_USER;
          240  +        m &= ~(CONFIGSET_USER|CONFIGSET_SCRIBER);
          241  +      }
          242  +      if( !g.perm.RdForum ){
          243  +        m &= ~(CONFIGSET_FORUM);
   235    244         }
   236    245         if( !g.perm.RdAddr ){
   237    246           m &= ~CONFIGSET_ADDR;
   238    247         }
   239    248         return m;
   240    249       }
   241    250     }
................................................................................
   310    319   ** sync session.
   311    320   **
   312    321   ** Mask consists of one or more CONFIGSET_* values ORed together, to
   313    322   ** designate what types of configuration we are allowed to receive.
   314    323   **
   315    324   ** NEW FORMAT:
   316    325   **
   317         -** zName is one of "/config", "/user", "/shun", "/reportfmt", or "/concealed".
          326  +** zName is one of:
          327  +**
          328  +**     "/config", "/user",  "/shun", "/reportfmt", "/concealed",
          329  +**     "/subscriber",
          330  +**
   318    331   ** zName indicates the table that holds the configuration information being
   319    332   ** transferred.  pContent is a string that consist of alternating Fossil
   320    333   ** and SQL tokens.  The First token is a timestamp in seconds since 1970.
   321    334   ** The second token is a primary key for the table identified by zName.  If
   322    335   ** The entry with the corresponding primary key exists and has a more recent
   323    336   ** mtime, then nothing happens.  If the entry does not exist or if it has
   324    337   ** an older mtime, then the content described by subsequent token pairs is
................................................................................
   330    343   **    NAME        CONTENT
   331    344   **    -------     -----------------------------------------------------------
   332    345   **    /config     $MTIME $NAME value $VALUE
   333    346   **    /user       $MTIME $LOGIN pw $VALUE cap $VALUE info $VALUE photo $VALUE
   334    347   **    /shun       $MTIME $UUID scom $VALUE
   335    348   **    /reportfmt  $MTIME $TITLE owner $VALUE cols $VALUE sqlcode $VALUE
   336    349   **    /concealed  $MTIME $HASH content $VALUE
   337         -**
   338         -** OLD FORMAT:
   339         -**
   340         -** The old format is retained for backwards compatibility, but is deprecated.
   341         -** The cutover from old format to new was on 2011-04-25.  After sufficient
   342         -** time has passed, support for the old format will be removed.
   343         -** Update: Support for the old format was removed on 2017-09-20.
   344         -**
   345         -** zName is either the NAME of an element of the CONFIG table, or else
   346         -** one of the special names "@shun", "@reportfmt", "@user", or "@concealed".
   347         -** If zName is a CONFIG table name, then CONTENT replaces (overwrites) the
   348         -** element in the CONFIG table.  For one of the @-labels, CONTENT is raw
   349         -** SQL that is evaluated.  Note that the raw SQL in CONTENT might not
   350         -** insert directly into the target table but might instead use a proxy
   351         -** table like _fer_reportfmt or _xfer_user.  Such tables must be created
   352         -** ahead of time using configure_prepare_to_receive().  Then after multiple
   353         -** calls to this routine, configure_finalize_receive() to transfer the
   354         -** information received into the true target table.
          350  +**    /subscriber $SMTIME $SEMAIL suname $V ...
   355    351   */
   356    352   void configure_receive(const char *zName, Blob *pContent, int groupMask){
          353  +  int checkMask;   /* Masks for which we must first check existance of tables */
          354  +
          355  +  checkMask = CONFIGSET_SCRIBER;
   357    356     if( zName[0]=='/' ){
   358    357       /* The new format */
   359         -    char *azToken[12];
          358  +    char *azToken[24];
   360    359       int nToken = 0;
   361    360       int ii, jj;
   362    361       int thisMask;
   363    362       Blob name, value, sql;
   364    363       static const struct receiveType {
   365         -      const char *zName;
   366         -      const char *zPrimKey;
   367         -      int nField;
   368         -      const char *azField[4];
          364  +      const char *zName;         /* Configuration key for this table */
          365  +      const char *zPrimKey;      /* Primary key column */
          366  +      int nField;                /* Number of data fields */
          367  +      const char *azField[6];    /* Names of the data fields */
   369    368       } aType[] = {
   370         -      { "/config",    "name",  1, { "value", 0, 0, 0 }              },
   371         -      { "@user",      "login", 4, { "pw", "cap", "info", "photo" }  },
   372         -      { "@shun",      "uuid",  1, { "scom", 0, 0, 0 }               },
   373         -      { "@reportfmt", "title", 3, { "owner", "cols", "sqlcode", 0 } },
   374         -      { "@concealed", "hash",  1, { "content", 0, 0, 0 }            },
          369  +      { "/config",    "name",  1, { "value", 0,0,0,0,0 }           },
          370  +      { "@user",      "login", 4, { "pw","cap","info","photo",0,0} },
          371  +      { "@shun",      "uuid",  1, { "scom", 0,0,0,0,0}             },
          372  +      { "@reportfmt", "title", 3, { "owner","cols","sqlcode",0,0,0}},
          373  +      { "@concealed", "hash",  1, { "content", 0,0,0,0,0 }         },
          374  +      { "@subscriber","semail",6,
          375  +         { "suname","sdigest","sdonotcall","ssub","sctime","smip"}         },
   375    376       };
          377  +
          378  +    /* Locate the receiveType in aType[ii] */
   376    379       for(ii=0; ii<count(aType); ii++){
   377    380         if( fossil_strcmp(&aType[ii].zName[1],&zName[1])==0 ) break;
   378    381       }
   379    382       if( ii>=count(aType) ) return;
          383  +
   380    384       while( blob_token(pContent, &name) && blob_sqltoken(pContent, &value) ){
   381    385         char *z = blob_terminate(&name);
   382    386         if( !safeSql(z) ) return;
   383    387         if( nToken>0 ){
   384    388           for(jj=0; jj<aType[ii].nField; jj++){
   385    389             if( fossil_strcmp(aType[ii].azField[jj], z)==0 ) break;
   386    390           }
................................................................................
   387    391           if( jj>=aType[ii].nField ) continue;
   388    392         }else{
   389    393           if( !safeInt(z) ) return;
   390    394         }
   391    395         azToken[nToken++] = z;
   392    396         azToken[nToken++] = z = blob_terminate(&value);
   393    397         if( !safeSql(z) ) return;
   394         -      if( nToken>=count(azToken) ) break;
          398  +      if( nToken>=count(azToken)-1 ) break;
   395    399       }
   396    400       if( nToken<2 ) return;
   397    401       if( aType[ii].zName[0]=='/' ){
   398    402         thisMask = configure_is_exportable(azToken[1]);
   399    403       }else{
   400    404         thisMask = configure_is_exportable(aType[ii].zName);
   401    405       }
   402    406       if( (thisMask & groupMask)==0 ) return;
          407  +    if( (thisMask & checkMask)!=0 ){
          408  +      if( (thisMask & CONFIGSET_SCRIBER)!=0 ){
          409  +        email_schema(1);
          410  +      }
          411  +      checkMask &= ~thisMask;
          412  +    }
   403    413   
   404    414       blob_zero(&sql);
   405    415       if( groupMask & CONFIGSET_OVERWRITE ){
   406    416         if( (thisMask & configHasBeenReset)==0 && aType[ii].zName[0]!='/' ){
   407    417           db_multi_exec("DELETE FROM \"%w\"", &aType[ii].zName[1]);
   408    418           configHasBeenReset |= thisMask;
   409    419         }
   410    420         blob_append_sql(&sql, "REPLACE INTO ");
   411    421       }else{
   412    422         blob_append_sql(&sql, "INSERT OR IGNORE INTO ");
   413    423       }
   414         -    blob_append_sql(&sql, "\"%w\"(\"%w\", mtime", &zName[1], aType[ii].zPrimKey);
          424  +    blob_append_sql(&sql, "\"%w\"(\"%w\",mtime",
          425  +         &zName[1], aType[ii].zPrimKey);
   415    426       for(jj=2; jj<nToken; jj+=2){
   416    427          blob_append_sql(&sql, ",\"%w\"", azToken[jj]);
   417    428       }
   418    429       blob_append_sql(&sql,") VALUES(%s,%s",
   419         -       azToken[1] /*safe-for-%s*/, azToken[0] /*safe-for-%s*/);
          430  +       azToken[1] /*safe-for-%s*/, azToken[0]/*safe-for-%s*/);
   420    431       for(jj=2; jj<nToken; jj+=2){
   421    432          blob_append_sql(&sql, ",%s", azToken[jj+1] /*safe-for-%s*/);
   422    433       }
   423    434       db_multi_exec("%s)", blob_sql_text(&sql));
   424    435       if( db_changes()==0 ){
   425    436         blob_reset(&sql);
   426    437         blob_append_sql(&sql, "UPDATE \"%w\" SET mtime=%s",
................................................................................
   571    582         );
   572    583         blob_appendf(pOut, "config /config %d\n%s\n",
   573    584                      blob_size(&rec), blob_str(&rec));
   574    585         nCard++;
   575    586         blob_reset(&rec);
   576    587       }
   577    588       db_finalize(&q);
          589  +  }
          590  +  if( (groupMask & CONFIGSET_SCRIBER)!=0
          591  +   && db_table_exists("repository","subscriber")
          592  +  ){
          593  +    db_prepare(&q, "SELECT mtime, quote(semail),"
          594  +                   " quote(suname), quote(sdigest),"
          595  +                   " quote(sdonotcall), quote(ssub),"
          596  +                   " quote(sctime), quote(smip)"
          597  +                   " FROM subscriber WHERE sverified"
          598  +                   " AND mtime>=%lld", iStart);
          599  +    while( db_step(&q)==SQLITE_ROW ){
          600  +      blob_appendf(&rec,
          601  +        "%lld %s suname %s sdigest %s sdonotcall %s ssub %s"
          602  +        " sctime %s smip %s",
          603  +        db_column_int64(&q, 0), /* mtime */
          604  +        db_column_text(&q, 1),  /* semail (PK) */
          605  +        db_column_text(&q, 2),  /* suname */
          606  +        db_column_text(&q, 3),  /* sdigest */
          607  +        db_column_text(&q, 4),  /* sdonotcall */
          608  +        db_column_text(&q, 5),  /* ssub */
          609  +        db_column_text(&q, 6),  /* sctime */
          610  +        db_column_text(&q, 7)   /* smip */
          611  +      );
          612  +      blob_appendf(pOut, "config /subscriber %d\n%s\n",
          613  +                   blob_size(&rec), blob_str(&rec));
          614  +      nCard++;
          615  +      blob_reset(&rec);
          616  +    }
          617  +    db_finalize(&q);
   578    618     }
   579    619     db_prepare(&q, "SELECT mtime, quote(name), quote(value) FROM config"
   580    620                    " WHERE name=:name AND mtime>=%lld", iStart);
   581    621     for(ii=0; ii<count(aConfig); ii++){
   582    622       if( (aConfig[ii].groupMask & groupMask)!=0 && aConfig[ii].zName[0]!='@' ){
   583    623         db_bind_text(&q, ":name", aConfig[ii].zName);
   584    624         while( db_step(&q)==SQLITE_ROW ){
................................................................................
   610    650       if( strncmp(z, &aGroupName[i].zName[1], n)==0 ){
   611    651         return aGroupName[i].groupMask;
   612    652       }
   613    653     }
   614    654     if( notFoundIsFatal ){
   615    655       fossil_print("Available configuration areas:\n");
   616    656       for(i=0; i<count(aGroupName); i++){
   617         -      fossil_print("  %-10s %s\n", &aGroupName[i].zName[1], aGroupName[i].zHelp);
          657  +      fossil_print("  %-13s %s\n",
          658  +            &aGroupName[i].zName[1], aGroupName[i].zHelp);
   618    659       }
   619    660       fossil_fatal("no such configuration area: \"%s\"", z);
   620    661     }
   621    662     return 0;
   622    663   }
   623    664   
   624    665   /*
................................................................................
   790    831         }else if( fossil_strcmp(zName,"@user")==0 ){
   791    832           db_multi_exec("DELETE FROM user");
   792    833           db_create_default_users(0, 0);
   793    834         }else if( fossil_strcmp(zName,"@concealed")==0 ){
   794    835           db_multi_exec("DELETE FROM concealed");
   795    836         }else if( fossil_strcmp(zName,"@shun")==0 ){
   796    837           db_multi_exec("DELETE FROM shun");
          838  +      }else if( fossil_strcmp(zName,"@subscriber")==0 ){
          839  +        if( db_table_exists("repository","subscriber") ){
          840  +          db_multi_exec("DELETE FROM subscriber");
          841  +        }
          842  +      }else if( fossil_strcmp(zName,"@forum")==0 ){
          843  +        if( db_table_exists("repository","forumpost") ){
          844  +          db_multi_exec("DELETE FROM forumpost");
          845  +          db_multi_exec("DELETE FROM forumthread");
          846  +        }
   797    847         }else if( fossil_strcmp(zName,"@reportfmt")==0 ){
   798    848           db_multi_exec("DELETE FROM reportfmt");
   799    849           assert( strchr(zRepositorySchemaDefaultReports,'%')==0 );
   800    850           db_multi_exec(zRepositorySchemaDefaultReports /*works-like:""*/);
   801    851         }
   802    852       }
   803    853       db_end_transaction(0);

Changes to src/db.c.

   122    122       int (*xHook)(void);         /* Functions to call at db_end_transaction() */
   123    123       int sequence;               /* Call functions in sequence order */
   124    124     } aHook[5];
   125    125     char *azDeleteOnFail[3];  /* Files to delete on a failure */
   126    126     char *azBeforeCommit[5];  /* Commands to run prior to COMMIT */
   127    127     int nBeforeCommit;        /* Number of entries in azBeforeCommit */
   128    128     int nPriorChanges;        /* sqlite3_total_changes() at transaction start */
          129  +  const char *zStartFile;   /* File in which transaction was started */
          130  +  int iStartLine;           /* Line of zStartFile where transaction started */
   129    131   } db = {0, 0, 0, 0, 0, 0, };
   130    132   
   131    133   /*
   132    134   ** Arrange for the given file to be deleted on a failure.
   133    135   */
   134    136   void db_delete_on_failure(const char *zFilename){
   135    137     assert( db.nDeleteOnFail<count(db.azDeleteOnFail) );
   136    138     db.azDeleteOnFail[db.nDeleteOnFail++] = fossil_strdup(zFilename);
   137    139   }
          140  +
          141  +/*
          142  +** Return the transaction nesting depth.  0 means we are currently
          143  +** not in a transaction.
          144  +*/
          145  +int db_transaction_nesting_depth(void){
          146  +  return db.nBegin;
          147  +}
          148  +
          149  +/*
          150  +** Return a pointer to a string that is the code point where the
          151  +** current transaction was started.
          152  +*/
          153  +char *db_transaction_start_point(void){
          154  +  return mprintf("%s:%d", db.zStartFile, db.iStartLine);
          155  +}
   138    156   
   139    157   /*
   140    158   ** This routine is called by the SQLite commit-hook mechanism
   141    159   ** just prior to each commit.  All this routine does is verify
   142    160   ** that nBegin really is zero.  That insures that transactions
   143    161   ** cannot commit by any means other than by calling db_end_transaction()
   144    162   ** below.
................................................................................
   149    167     if( db.nBegin ){
   150    168       fossil_panic("illegal commit attempt");
   151    169       return 1;
   152    170     }
   153    171     return 0;
   154    172   }
   155    173   
          174  +/*
          175  +** Silently add the filename and line number as parameter to each
          176  +** db_begin_transaction call.
          177  +*/
          178  +#if INTERFACE
          179  +#define db_begin_transaction() db_begin_transaction_real(__FILE__,__LINE__)
          180  +#endif
          181  +
   156    182   /*
   157    183   ** Begin and end a nested transaction
   158    184   */
   159         -void db_begin_transaction(void){
          185  +void db_begin_transaction_real(const char *zStartFile, int iStartLine){
   160    186     if( db.nBegin==0 ){
   161    187       db_multi_exec("BEGIN");
   162    188       sqlite3_commit_hook(g.db, db_verify_at_commit, 0);
   163    189       db.nPriorChanges = sqlite3_total_changes(g.db);
          190  +    db.doRollback = 0;
          191  +    db.zStartFile = zStartFile;
          192  +    db.iStartLine = iStartLine;
   164    193     }
   165    194     db.nBegin++;
   166    195   }
   167    196   void db_end_transaction(int rollbackFlag){
   168    197     if( g.db==0 ) return;
   169         -  if( db.nBegin<=0 ) return;
   170         -  if( rollbackFlag ) db.doRollback = 1;
          198  +  if( db.nBegin<=0 ){
          199  +    fossil_warning("Extra call to db_end_transaction");
          200  +    return;
          201  +  }
          202  +  if( rollbackFlag ){
          203  +    db.doRollback = 1;
          204  +    if( g.fSqlTrace ) fossil_trace("-- ROLLBACK by request\n");
          205  +  }
   171    206     db.nBegin--;
   172    207     if( db.nBegin==0 ){
   173    208       int i;
   174    209       if( db.doRollback==0 && db.nPriorChanges<sqlite3_total_changes(g.db) ){
   175    210         i = 0;
   176    211         while( db.nBeforeCommit ){
   177    212           db.nBeforeCommit--;
................................................................................
   178    213           sqlite3_exec(g.db, db.azBeforeCommit[i], 0, 0, 0);
   179    214           sqlite3_free(db.azBeforeCommit[i]);
   180    215           i++;
   181    216         }
   182    217         leaf_do_pending_checks();
   183    218       }
   184    219       for(i=0; db.doRollback==0 && i<db.nCommitHook; i++){
   185         -      db.doRollback |= db.aHook[i].xHook();
          220  +      int rc = db.aHook[i].xHook();
          221  +      if( rc ){
          222  +        db.doRollback = 1;
          223  +        if( g.fSqlTrace ) fossil_trace("-- ROLLBACK due to aHook[%d]\n", i);
          224  +      }
   186    225       }
   187    226       while( db.pAllStmt ){
   188    227         db_finalize(db.pAllStmt);
   189    228       }
   190    229       db_multi_exec("%s", db.doRollback ? "ROLLBACK" : "COMMIT");
   191    230       db.doRollback = 0;
   192    231     }
................................................................................
   270    309     va_end(ap);
   271    310     zSql = blob_str(&pStmt->sql);
   272    311     db.nPrepare++;
   273    312     if( flags & DB_PREPARE_PERSISTENT ){
   274    313       prepFlags = SQLITE_PREPARE_PERSISTENT;
   275    314     }
   276    315     rc = sqlite3_prepare_v3(g.db, zSql, -1, prepFlags, &pStmt->pStmt, 0);
   277         -  if( rc!=0 && (flags & DB_PREPARE_IGNORE_ERROR)!=0 ){
          316  +  if( rc!=0 && (flags & DB_PREPARE_IGNORE_ERROR)==0 ){
   278    317       db_err("%s\n%s", sqlite3_errmsg(g.db), zSql);
   279    318     }
   280    319     pStmt->pNext = pStmt->pPrev = 0;
   281    320     pStmt->nStep = 0;
   282    321     pStmt->rc = rc;
   283    322     return rc;
   284    323   }
................................................................................
   308    347       pStmt->pPrev = 0;
   309    348       if( db.pAllStmt ) db.pAllStmt->pPrev = pStmt;
   310    349       db.pAllStmt = pStmt;
   311    350       va_end(ap);
   312    351     }
   313    352     return rc;
   314    353   }
          354  +
          355  +/* Prepare a statement using text placed inside a Blob
          356  +** using blob_append_sql().
          357  +*/
          358  +int db_prepare_blob(Stmt *pStmt, Blob *pSql){
          359  +  int rc;
          360  +  char *zSql;
          361  +  pStmt->sql = *pSql;
          362  +  blob_init(pSql, 0, 0);
          363  +  zSql = blob_sql_text(&pStmt->sql);
          364  +  db.nPrepare++;
          365  +  rc = sqlite3_prepare_v3(g.db, zSql, -1, 0, &pStmt->pStmt, 0);
          366  +  if( rc!=0 ){
          367  +    db_err("%s\n%s", sqlite3_errmsg(g.db), zSql);
          368  +  }
          369  +  pStmt->pNext = pStmt->pPrev = 0;
          370  +  pStmt->nStep = 0;
          371  +  pStmt->rc = rc;
          372  +  return rc;
          373  +}
          374  +
   315    375   
   316    376   /*
   317    377   ** Return the index of a bind parameter
   318    378   */
   319    379   static int paramIdx(Stmt *pStmt, const char *zParamName){
   320    380     int i = sqlite3_bind_parameter_index(pStmt->pStmt, zParamName);
   321    381     if( i==0 ){
................................................................................
   426    486   
   427    487   /*
   428    488   ** Return the rowid of the most recent insert
   429    489   */
   430    490   int db_last_insert_rowid(void){
   431    491     i64 x = sqlite3_last_insert_rowid(g.db);
   432    492     if( x<0 || x>(i64)2147483647 ){
   433         -    fossil_fatal("rowid out of range (0..2147483647)");
          493  +    fossil_panic("rowid out of range (0..2147483647)");
   434    494     }
   435    495     return (int)x;
   436    496   }
   437    497   
   438    498   /*
   439    499   ** Return the number of rows that were changed by the most recent
   440    500   ** INSERT, UPDATE, or DELETE.  Auxiliary changes caused by triggers
................................................................................
   477    537   }
   478    538   char *db_column_malloc(Stmt *pStmt, int N){
   479    539     return mprintf("%s", db_column_text(pStmt, N));
   480    540   }
   481    541   void db_column_blob(Stmt *pStmt, int N, Blob *pBlob){
   482    542     blob_append(pBlob, sqlite3_column_blob(pStmt->pStmt, N),
   483    543                 sqlite3_column_bytes(pStmt->pStmt, N));
          544  +}
          545  +Blob db_column_text_as_blob(Stmt *pStmt, int N){
          546  +  Blob x;
          547  +  blob_init(&x, (char*)sqlite3_column_text(pStmt->pStmt,N),
          548  +            sqlite3_column_bytes(pStmt->pStmt,N));
          549  +  return x;
   484    550   }
   485    551   
   486    552   /*
   487    553   ** Initialize a blob to an ephemeral copy of the content of a
   488    554   ** column in the current row.  The data in the blob will become
   489    555   ** invalid when the statement is stepped or reset.
   490    556   */
................................................................................
   873    939     if( g.fTimeFormat==1 ){
   874    940       sqlite3_result_text(context, "0 seconds", -1, SQLITE_STATIC);
   875    941     }else{
   876    942       sqlite3_result_text(context, "utc", -1, SQLITE_STATIC);
   877    943     }
   878    944   }
   879    945   
          946  +/*
          947  +** If the input is a hexadecimal string, convert that string into a BLOB.
          948  +** If the input is not a hexadecimal string, return NULL.
          949  +*/
          950  +void db_hextoblob(
          951  +  sqlite3_context *context,
          952  +  int argc,
          953  +  sqlite3_value **argv
          954  +){
          955  +  const unsigned char *zIn = sqlite3_value_text(argv[0]);
          956  +  int nIn = sqlite3_value_bytes(argv[0]);
          957  +  unsigned char *zOut;
          958  +  if( zIn==0 ) return;
          959  +  if( nIn&1 ) return;
          960  +  if( !validate16((const char*)zIn, nIn) ) return;
          961  +  zOut = sqlite3_malloc64( nIn/2 );
          962  +  if( zOut==0 ){
          963  +    sqlite3_result_error_nomem(context);
          964  +    return;
          965  +  }
          966  +  decode16(zIn, zOut, nIn);
          967  +  sqlite3_result_blob(context, zOut, nIn/2, sqlite3_free);
          968  +}
   880    969   
   881    970   /*
   882    971   ** Register the SQL functions that are useful both to the internal
   883    972   ** representation and to the "fossil sql" command.
   884    973   */
   885    974   void db_add_aux_functions(sqlite3 *db){
   886    975     sqlite3_create_function(db, "checkin_mtime", 2, SQLITE_UTF8, 0,
................................................................................
   891    980                             db_sym2rid_function, 0, 0);
   892    981     sqlite3_create_function(db, "now", 0, SQLITE_UTF8, 0,
   893    982                             db_now_function, 0, 0);
   894    983     sqlite3_create_function(db, "toLocal", 0, SQLITE_UTF8, 0,
   895    984                             db_tolocal_function, 0, 0);
   896    985     sqlite3_create_function(db, "fromLocal", 0, SQLITE_UTF8, 0,
   897    986                             db_fromlocal_function, 0, 0);
          987  +  sqlite3_create_function(db, "hextoblob", 1, SQLITE_UTF8, 0,
          988  +                          db_hextoblob, 0, 0);
   898    989   }
   899    990   
   900    991   #if USE_SEE
   901    992   /*
   902    993   ** This is a pointer to the saved database encryption key string.
   903    994   */
   904    995   static char *zSavedKey = 0;
................................................................................
   937   1028     size_t blobSize = 0;
   938   1029   
   939   1030     blobSize = blob_size(pKey);
   940   1031     if( blobSize==0 ) return;
   941   1032     fossil_get_page_size(&pageSize);
   942   1033     assert( pageSize>0 );
   943   1034     if( blobSize>pageSize ){
   944         -    fossil_fatal("key blob too large: %u versus %u", blobSize, pageSize);
         1035  +    fossil_panic("key blob too large: %u versus %u", blobSize, pageSize);
   945   1036     }
   946   1037     p = fossil_secure_alloc_page(&n);
   947   1038     assert( p!=NULL );
   948   1039     assert( n==pageSize );
   949   1040     assert( n>=blobSize );
   950   1041     memcpy(p, blob_str(pKey), blobSize);
   951   1042     zSavedKey = p;
................................................................................
   971   1062   ){
   972   1063     if( zSavedKey!=NULL ){
   973   1064       size_t blobSize = blob_size(pKey);
   974   1065       if( blobSize==0 ){
   975   1066         db_unsave_encryption_key();
   976   1067       }else{
   977   1068         if( blobSize>savedKeySize ){
   978         -        fossil_fatal("key blob too large: %u versus %u",
         1069  +        fossil_panic("key blob too large: %u versus %u",
   979   1070                        blobSize, savedKeySize);
   980   1071         }
   981   1072         fossil_secure_zero(zSavedKey, savedKeySize);
   982   1073         memcpy(zSavedKey, blob_str(pKey), blobSize);
   983   1074       }
   984   1075     }else{
   985   1076       db_save_encryption_key(pKey);
................................................................................
  1001   1092     size_t n = 0;
  1002   1093     size_t pageSize = 0;
  1003   1094     HANDLE hProcess = NULL;
  1004   1095   
  1005   1096     fossil_get_page_size(&pageSize);
  1006   1097     assert( pageSize>0 );
  1007   1098     if( nSize>pageSize ){
  1008         -    fossil_fatal("key too large: %u versus %u", nSize, pageSize);
         1099  +    fossil_panic("key too large: %u versus %u", nSize, pageSize);
  1009   1100     }
  1010   1101     p = fossil_secure_alloc_page(&n);
  1011   1102     assert( p!=NULL );
  1012   1103     assert( n==pageSize );
  1013   1104     assert( n>=nSize );
  1014   1105     hProcess = OpenProcess(PROCESS_VM_READ, FALSE, processId);
  1015   1106     if( hProcess!=NULL ){
................................................................................
  1017   1108       if( ReadProcessMemory(hProcess, pAddress, p, nSize, &nRead) ){
  1018   1109         CloseHandle(hProcess);
  1019   1110         if( nRead==nSize ){
  1020   1111           db_unsave_encryption_key();
  1021   1112           zSavedKey = p;
  1022   1113           savedKeySize = n;
  1023   1114         }else{
  1024         -        fossil_fatal("bad size read, %u out of %u bytes at %p from pid %lu",
         1115  +        fossil_panic("bad size read, %u out of %u bytes at %p from pid %lu",
  1025   1116                        nRead, nSize, pAddress, processId);
  1026   1117         }
  1027   1118       }else{
  1028   1119         CloseHandle(hProcess);
  1029         -      fossil_fatal("failed read, %u bytes at %p from pid %lu: %lu", nSize,
         1120  +      fossil_panic("failed read, %u bytes at %p from pid %lu: %lu", nSize,
  1030   1121                      pAddress, processId, GetLastError());
  1031   1122       }
  1032   1123     }else{
  1033         -    fossil_fatal("failed to open pid %lu: %lu", processId, GetLastError());
         1124  +    fossil_panic("failed to open pid %lu: %lu", processId, GetLastError());
  1034   1125     }
  1035   1126   }
  1036   1127   #endif /* defined(_WIN32) */
  1037   1128   #endif /* USE_SEE */
  1038   1129   
  1039   1130   /*
  1040   1131   ** If the database file zDbFile has a name that suggests that it is
................................................................................
  1090   1181   ** connection.  An error results in process abort.
  1091   1182   */
  1092   1183   LOCAL sqlite3 *db_open(const char *zDbName){
  1093   1184     int rc;
  1094   1185     sqlite3 *db;
  1095   1186   
  1096   1187     if( g.fSqlTrace ) fossil_trace("-- sqlite3_open: [%s]\n", zDbName);
         1188  +  if( strcmp(zDbName, g.nameOfExe)==0 ){
         1189  +    extern int sqlite3_appendvfs_init(
         1190  +      sqlite3 *, char **, const sqlite3_api_routines *
         1191  +    );
         1192  +    sqlite3_appendvfs_init(0,0,0);
         1193  +    g.zVfsName = "apndvfs";
         1194  +  }
  1097   1195     rc = sqlite3_open_v2(
  1098   1196          zDbName, &db,
  1099   1197          SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE,
  1100   1198          g.zVfsName
  1101   1199     );
  1102   1200     if( rc!=SQLITE_OK ){
  1103   1201       db_err("[%s]: %s", zDbName, sqlite3_errmsg(db));
................................................................................
  1165   1263   ** the database connection.
  1166   1264   **
  1167   1265   ** After calling this routine, db_database_slot(zLabel) should
  1168   1266   ** return 0.
  1169   1267   */
  1170   1268   void db_set_main_schemaname(sqlite3 *db, const char *zLabel){
  1171   1269     if( sqlite3_db_config(db, SQLITE_DBCONFIG_MAINDBNAME, zLabel) ){
  1172         -    fossil_fatal("Fossil requires a version of SQLite that supports the "
         1270  +    fossil_panic("Fossil requires a version of SQLite that supports the "
  1173   1271                    "SQLITE_DBCONFIG_MAINDBNAME interface.");
  1174   1272     }
  1175   1273   }
  1176   1274   
  1177   1275   /*
  1178   1276   ** Return the slot number for database zLabel.  The first database
  1179   1277   ** opened is slot 0.  The "temp" database is slot 1.  Attached databases
................................................................................
  1222   1320       g.zConfigDbName = 0;
  1223   1321     }else if( g.dbConfig ){
  1224   1322       sqlite3_wal_checkpoint(g.dbConfig, 0);
  1225   1323       sqlite3_close(g.dbConfig);
  1226   1324       g.dbConfig = 0;
  1227   1325       g.zConfigDbName = 0;
  1228   1326     }else if( g.db && 0==iSlot ){
         1327  +    int rc;
  1229   1328       sqlite3_wal_checkpoint(g.db, 0);
  1230         -    sqlite3_close(g.db);
         1329  +    rc = sqlite3_close(g.db);
         1330  +    if( g.fSqlTrace ) fossil_trace("-- db_close_config(%d)\n", rc);
  1231   1331       g.db = 0;
  1232   1332       g.zConfigDbName = 0;
  1233   1333     }
  1234   1334   }
  1235   1335   
  1236   1336   /*
  1237   1337   ** Open the user database in "~/.fossil".  Create the database anew if
................................................................................
  1264   1364           char *zPath = fossil_getenv("HOMEPATH");
  1265   1365           if( zDrive && zPath ) zHome = mprintf("%s%s", zDrive, zPath);
  1266   1366         }
  1267   1367       }
  1268   1368     }
  1269   1369     if( zHome==0 ){
  1270   1370       if( isOptional ) return 0;
  1271         -    fossil_fatal("cannot locate home directory - please set the "
         1371  +    fossil_panic("cannot locate home directory - please set the "
  1272   1372                    "FOSSIL_HOME, LOCALAPPDATA, APPDATA, or HOMEPATH "
  1273   1373                    "environment variables");
  1274   1374     }
  1275   1375   #else
  1276   1376     if( zHome==0 ){
  1277   1377       zHome = fossil_getenv("HOME");
  1278   1378     }
  1279   1379     if( zHome==0 ){
  1280   1380       if( isOptional ) return 0;
  1281         -    fossil_fatal("cannot locate home directory - please set the "
         1381  +    fossil_panic("cannot locate home directory - please set the "
  1282   1382                    "FOSSIL_HOME or HOME environment variables");
  1283   1383     }
  1284   1384   #endif
  1285   1385     if( file_isdir(zHome, ExtFILE)!=1 ){
  1286   1386       if( isOptional ) return 0;
  1287         -    fossil_fatal("invalid home directory: %s", zHome);
         1387  +    fossil_panic("invalid home directory: %s", zHome);
  1288   1388     }
  1289   1389   #if defined(_WIN32) || defined(__CYGWIN__)
  1290   1390     /* . filenames give some window systems problems and many apps problems */
  1291   1391     zDbName = mprintf("%//_fossil", zHome);
  1292   1392   #else
  1293   1393     zDbName = mprintf("%s/.fossil", zHome);
  1294   1394   #endif
  1295   1395     if( file_size(zDbName, ExtFILE)<1024*3 ){
  1296   1396       if( file_access(zHome, W_OK) ){
  1297   1397         if( isOptional ) return 0;
  1298         -      fossil_fatal("home directory %s must be writeable", zHome);
         1398  +      fossil_panic("home directory %s must be writeable", zHome);
  1299   1399       }
  1300   1400       db_init_database(zDbName, zConfigSchema, (char*)0);
  1301   1401     }
  1302   1402     if( file_access(zDbName, W_OK) ){
  1303   1403       if( isOptional ) return 0;
  1304         -    fossil_fatal("configuration file %s must be writeable", zDbName);
         1404  +    fossil_panic("configuration file %s must be writeable", zDbName);
  1305   1405     }
  1306   1406     if( useAttach ){
  1307   1407       db_open_or_attach(zDbName, "configdb");
  1308   1408       g.dbConfig = 0;
  1309   1409     }else{
  1310   1410       g.dbConfig = db_open(zDbName);
  1311   1411       db_set_main_schemaname(g.dbConfig, "configdb");
................................................................................
  1578   1678     }
  1579   1679   rep_not_found:
  1580   1680     if( (bFlags & OPEN_OK_NOT_FOUND)==0 ){
  1581   1681   #ifdef FOSSIL_ENABLE_JSON
  1582   1682       g.json.resultCode = FSL_JSON_E_DB_NOT_FOUND;
  1583   1683   #endif
  1584   1684       if( nArgUsed==0 ){
  1585         -      fossil_fatal("use --repository or -R to specify the repository database");
         1685  +      fossil_panic("use --repository or -R to specify the repository database");
  1586   1686       }else{
  1587         -      fossil_fatal("specify the repository name as a command-line argument");
         1687  +      fossil_panic("specify the repository name as a command-line argument");
  1588   1688       }
  1589   1689     }
  1590   1690   }
  1591   1691   
  1592   1692   /*
  1593   1693   ** Return TRUE if the schema is out-of-date
  1594   1694   */
................................................................................
  1702   1802       sqlite3_status(SQLITE_STATUS_PAGECACHE_OVERFLOW, &cur, &hiwtr, 0);
  1703   1803       fprintf(stderr, "-- PCACHE_OVFLOW          %10d %10d\n", cur, hiwtr);
  1704   1804       fprintf(stderr, "-- prepared statements    %10d\n", db.nPrepare);
  1705   1805     }
  1706   1806     while( db.pAllStmt ){
  1707   1807       db_finalize(db.pAllStmt);
  1708   1808     }
  1709         -  db_end_transaction(1);
         1809  +  if( db.nBegin && reportErrors ){
         1810  +    fossil_warning("Transaction started at %s:%d never commits",
         1811  +                   db.zStartFile, db.iStartLine);
         1812  +    db_end_transaction(1);
         1813  +  }
  1710   1814     pStmt = 0;
  1711         -  g.dbIgnoreErrors++;  /* Stop "database locked" warnings from PRAGMA optimize */
         1815  +  g.dbIgnoreErrors++; /* Stop "database locked" warnings from PRAGMA optimize */
  1712   1816     sqlite3_exec(g.db, "PRAGMA optimize", 0, 0, 0);
  1713   1817     g.dbIgnoreErrors--;
  1714   1818     db_close_config();
  1715   1819   
  1716   1820     /* If the localdb has a lot of unused free space,
  1717   1821     ** then VACUUM it as we shut down.
  1718   1822     */
................................................................................
  1724   1828       }
  1725   1829     }
  1726   1830   
  1727   1831     if( g.db ){
  1728   1832       int rc;
  1729   1833       sqlite3_wal_checkpoint(g.db, 0);
  1730   1834       rc = sqlite3_close(g.db);
         1835  +    if( g.fSqlTrace ) fossil_trace("-- sqlite3_close(%d)\n", rc);
  1731   1836       if( rc==SQLITE_BUSY && reportErrors ){
  1732   1837         while( (pStmt = sqlite3_next_stmt(g.db, pStmt))!=0 ){
  1733   1838           fossil_warning("unfinalized SQL statement: [%s]", sqlite3_sql(pStmt));
  1734   1839         }
  1735   1840       }
  1736   1841       g.db = 0;
  1737   1842     }
................................................................................
  2741   2846     info_cmd();
  2742   2847   }
  2743   2848   
  2744   2849   /*
  2745   2850   ** Print the current value of a setting identified by the pSetting
  2746   2851   ** pointer.
  2747   2852   */
  2748         -static void print_setting(const Setting *pSetting){
         2853  +void print_setting(const Setting *pSetting){
  2749   2854     Stmt q;
  2750   2855     if( g.repositoryOpen ){
  2751   2856       db_prepare(&q,
  2752   2857          "SELECT '(local)', value FROM config WHERE name=%Q"
  2753   2858          " UNION ALL "
  2754   2859          "SELECT '(global)', value FROM global_config WHERE name=%Q",
  2755   2860          pSetting->name, pSetting->name

Changes to src/default_css.txt.

   629    629     display: none;
   630    630   }
   631    631   table.label-value th {
   632    632     vertical-align: top;
   633    633     text-align: right;
   634    634     padding: 0.2ex 1ex;
   635    635   }
          636  +table.forum_post {
          637  +  margin-top: 1ex;
          638  +  margin-bottom: 1ex;
          639  +  margin-left: 0;
          640  +  margin-right: 0;
          641  +  border-spacing: 0;
          642  +}
          643  +span.forum_author {
          644  +  color: #888;
          645  +  font-size: 75%;
          646  +}
          647  +span.forum_author::after {
          648  +  content: " | ";
          649  +}
          650  +span.forum_age {
          651  +  color: #888;
          652  +  font-size: 85%;
          653  +}
          654  +span.forum_buttons {
          655  +  font-size: 85%;
          656  +}
          657  +span.forum_buttons::before {
          658  +  color: #888;
          659  +  content: " | ";
          660  +}
          661  +span.forum_npost {
          662  +  color: #888;
          663  +  font-size: 75%;
          664  +}
          665  +table.forumeditform td {
          666  +  vertical-align: top;
          667  +  border-collapse: collapse;
          668  +  padding: 1px;
          669  +}
          670  +div.forum_body p {
          671  +  margin-top: 0;
          672  +}
          673  +td.form_label {
          674  +  vertical-align: top;
          675  +  text-align: right;
          676  +}

Changes to src/diff.c.

  2214   2214     int rid;               /* Artifact ID of the file being annotated */
  2215   2215     int fnid;              /* Filename ID */
  2216   2216     Stmt q;                /* Query returning all ancestor versions */
  2217   2217     int cnt = 0;           /* Number of versions analyzed */
  2218   2218     int iLimit;            /* Maximum number of versions to analyze */
  2219   2219     sqlite3_int64 mxTime;  /* Halt at this time if not already complete */
  2220   2220   
         2221  +  memset(p, 0, sizeof(*p));
         2222  +
  2221   2223     if( zLimit ){
  2222   2224       if( strcmp(zLimit,"none")==0 ){
  2223   2225         iLimit = 0;
  2224   2226         mxTime = 0;
  2225   2227       }else if( sqlite3_strglob("*[0-9]s", zLimit)==0 ){
  2226   2228         iLimit = 0;
  2227   2229         mxTime = current_time_in_milliseconds() + 1000.0*atof(zLimit);
................................................................................
  2233   2235     }else{
  2234   2236       /* Default limit is as much as we can do in 1.000 seconds */
  2235   2237       iLimit = 0;
  2236   2238       mxTime = current_time_in_milliseconds()+1000;
  2237   2239     }
  2238   2240     db_begin_transaction();
  2239   2241   
  2240         -  /* Get the artificate ID for the check-in begin analyzed */
         2242  +  /* Get the artifact ID for the check-in begin analyzed */
  2241   2243     if( zRevision ){
  2242   2244       cid = name_to_typed_rid(zRevision, "ci");
  2243   2245     }else{
  2244   2246       db_must_be_within_tree();
  2245   2247       cid = db_lget_int("checkout", 0);
  2246   2248     }
  2247   2249     origid = zOrigin ? name_to_typed_rid(zOrigin, "ci") : 0;
................................................................................
  2306   2308         blob_to_utf8_no_bom(&step, 0);
  2307   2309         annotation_step(p, &step, p->nVers-1, annFlags);
  2308   2310         blob_reset(&step);
  2309   2311       }
  2310   2312       p->nVers++;
  2311   2313       cnt++;
  2312   2314     }
         2315  +
         2316  +  if( p->nVers==0 ){
         2317  +    if( zRevision ){
         2318  +      fossil_fatal("file %s does not exist in check-in %s", zFilename, zRevision);
         2319  +    }else{
         2320  +      fossil_fatal("no history for file: %s", zFilename);
         2321  +    }
         2322  +  }
         2323  +
  2313   2324     db_finalize(&q);
  2314   2325     db_end_transaction(0);
  2315   2326   }
  2316   2327   
  2317   2328   /*
  2318   2329   ** Return a color from a gradient.
  2319   2330   */

Changes to src/diffcmd.c.

   834    834   **   --command PROG             External diff program - overrides "diff-command"
   835    835   **   --context|-c N             Use N lines of context
   836    836   **   --diff-binary BOOL         Include binary files when using external commands
   837    837   **   --exec-abs-paths           Force absolute path names with external commands.
   838    838   **   --exec-rel-paths           Force relative path names with external commands.
   839    839   **   --from|-r VERSION          Select VERSION as source for the diff
   840    840   **   --internal|-i              Use internal diff logic
          841  +**   --new-file|-N              Show complete text of added and deleted files
   841    842   **   --numstat                  Show only the number of lines delete and added
   842    843   **   --side-by-side|-y          Side-by-side diff
   843    844   **   --strip-trailing-cr        Strip trailing CR
   844    845   **   --tk                       Launch a Tcl/Tk GUI for display
   845    846   **   --to VERSION               Select VERSION as target for the diff
   846    847   **   --undo                     Diff against the "undo" buffer
   847    848   **   --unified                  Unified diff

Changes to src/dispatch.c.

   165    165     if( z[i]=='?' ){
   166    166       z[i] = 0;
   167    167       zQ = &z[i+1];
   168    168     }else{
   169    169       zQ = &z[i];
   170    170     }
   171    171     if( dispatch_name_search(z, CMDFLAG_WEBPAGE, ppCmd) ){
   172         -    fossil_fatal("\"%s\" aliased to \"%s\" but \"%s\" does not exist",
          172  +    fossil_panic("\"%s\" aliased to \"%s\" but \"%s\" does not exist",
   173    173                    zName, z, z);
   174    174     }
   175    175     z = zQ;
   176    176     while( *z ){
   177    177       char *zName = z;
   178    178       char *zValue = 0;
   179    179       while( *z && *z!='=' && *z!='&' && *z!='!' ){ z++; }

Changes to src/doc.c.

   296    296   ** Verify that all entries in the aMime[] table are in sorted order.
   297    297   ** Abort with a fatal error if any is out-of-order.
   298    298   */
   299    299   static void mimetype_verify(void){
   300    300     int i;
   301    301     for(i=1; i<count(aMime); i++){
   302    302       if( fossil_strcmp(aMime[i-1].zSuffix,aMime[i].zSuffix)>=0 ){
   303         -      fossil_fatal("mimetypes out of sequence: %s before %s",
          303  +      fossil_panic("mimetypes out of sequence: %s before %s",
   304    304                      aMime[i-1].zSuffix, aMime[i].zSuffix);
   305    305       }
   306    306     }
   307    307   }
   308    308   
   309    309   /*
   310    310   ** Guess the mime-type of a document based on its name.
................................................................................
   662    662         zFullpath = mprintf("%s/%s", g.zLocalRoot, zName);
   663    663         if( file_isfile(zFullpath, RepoFILE)
   664    664          && blob_read_from_file(&filebody, zFullpath, RepoFILE)>0 ){
   665    665           rid = 1;  /* Fake RID just to get the loop to end */
   666    666         }
   667    667         fossil_free(zFullpath);
   668    668       }else{
   669         -      vid = name_to_typed_rid(zCheckin, "ci");
   670         -      rid = doc_load_content(vid, zName, &filebody);
          669  +      vid = symbolic_name_to_rid(zCheckin, "ci");
          670  +      rid = vid>0 ? doc_load_content(vid, zName, &filebody) : 0;
   671    671       }
   672    672     }
   673    673     g.zPath = mprintf("%s/%s", g.zPath, zPathSuffix);
   674    674     if( rid==0 ) goto doc_not_found;
   675    675     blob_to_utf8_no_bom(&filebody, 0);
   676    676   
   677    677     /* The file is now contained in the filebody blob.  Deliver the
................................................................................
   762    762     cgi_set_status(404, "Not Found");
   763    763     style_header("Not Found");
   764    764     @ <p>Document %h(zOrigName) not found
   765    765     if( fossil_strcmp(zCheckin,"ckout")!=0 ){
   766    766       @ in %z(href("%R/tree?ci=%T",zCheckin))%h(zCheckin)</a>
   767    767     }
   768    768     style_footer();
   769         -  db_end_transaction(0);
   770    769     return;
   771    770   }
   772    771   
   773    772   /*
   774    773   ** The default logo.
   775    774   */
   776    775   static const unsigned char aLogo[] = {

Added src/email.c.

            1  +/*
            2  +** Copyright (c) 2007 D. Richard Hipp
            3  +**
            4  +** This program is free software; you can redistribute it and/or
            5  +** modify it under the terms of the Simplified BSD License (also
            6  +** known as the "2-Clause License" or "FreeBSD License".)
            7  +**
            8  +** This program is distributed in the hope that it will be useful,
            9  +** but without any warranty; without even the implied warranty of
           10  +** merchantability or fitness for a particular purpose.
           11  +**
           12  +** Author contact information:
           13  +**   drh@hwaci.com
           14  +**   http://www.hwaci.com/drh/
           15  +**
           16  +*******************************************************************************
           17  +**
           18  +** Logic for email notification, also known as "alerts".
           19  +*/
           20  +#include "config.h"
           21  +#include "email.h"
           22  +#include <assert.h>
           23  +#include <time.h>
           24  +
           25  +/*
           26  +** Maximum size of the subscriberCode blob, in bytes
           27  +*/
           28  +#define SUBSCRIBER_CODE_SZ 32
           29  +
           30  +/*
           31  +** SQL code to implement the tables needed by the email notification
           32  +** system.
           33  +*/
           34  +static const char zEmailInit[] =
           35  +@ DROP TABLE IF EXISTS repository.subscriber;
           36  +@ -- Subscribers are distinct from users.  A person can have a log-in in
           37  +@ -- the USER table without being a subscriber.  Or a person can be a
           38  +@ -- subscriber without having a USER table entry.  Or they can have both.
           39  +@ -- In the last case the suname column points from the subscriber entry
           40  +@ -- to the USER entry.
           41  +@ --
           42  +@ -- The ssub field is a string where each character indicates a particular
           43  +@ -- type of event to subscribe to.  Choices:
           44  +@ --     a - Announcements
           45  +@ --     c - Check-ins
           46  +@ --     t - Ticket changes
           47  +@ --     w - Wiki changes
           48  +@ -- Probably different codes will be added in the future.  In the future
           49  +@ -- we might also add a separate table that allows subscribing to email
           50  +@ -- notifications for specific branches or tags or tickets.
           51  +@ --
           52  +@ CREATE TABLE repository.subscriber(
           53  +@   subscriberId INTEGER PRIMARY KEY, -- numeric subscriber ID.  Internal use
           54  +@   subscriberCode BLOB DEFAULT (randomblob(32)) UNIQUE, -- UUID for subscriber
           55  +@   semail TEXT UNIQUE COLLATE nocase,-- email address
           56  +@   suname TEXT,                      -- corresponding USER entry
           57  +@   sverified BOOLEAN DEFAULT true,   -- email address verified
           58  +@   sdonotcall BOOLEAN,               -- true for Do Not Call 
           59  +@   sdigest BOOLEAN,                  -- true for daily digests only
           60  +@   ssub TEXT,                        -- baseline subscriptions
           61  +@   sctime INTDATE,                   -- When this entry was created. unixtime
           62  +@   mtime INTDATE,                    -- Last change.  unixtime
           63  +@   smip TEXT                         -- IP address of last change
           64  +@ );
           65  +@ CREATE INDEX repository.subscriberUname
           66  +@   ON subscriber(suname) WHERE suname IS NOT NULL;
           67  +@ 
           68  +@ DROP TABLE IF EXISTS repository.pending_alert;
           69  +@ -- Email notifications that need to be sent.
           70  +@ --
           71  +@ -- The first character of the eventid determines the event type.
           72  +@ -- Remaining characters determine the specific event.  For example,
           73  +@ -- 'c4413' means check-in with rid=4413.
           74  +@ --
           75  +@ CREATE TABLE repository.pending_alert(
           76  +@   eventid TEXT PRIMARY KEY,         -- Object that changed
           77  +@   sentSep BOOLEAN DEFAULT false,    -- individual emails sent
           78  +@   sentDigest BOOLEAN DEFAULT false  -- digest emails sent
           79  +@ ) WITHOUT ROWID;
           80  +@ 
           81  +@ DROP TABLE IF EXISTS repository.email_bounce;
           82  +@ -- Record bounced emails.  If too many bounces are received within
           83  +@ -- some defined time range, then cancel the subscription.  Older
           84  +@ -- entries are periodically purged.
           85  +@ --
           86  +@ CREATE TABLE repository.email_bounce(
           87  +@   subscriberId INTEGER, -- to whom the email was sent.
           88  +@   sendTime INTEGER,     -- seconds since 1970 when email was sent
           89  +@   rcvdTime INTEGER      -- seconds since 1970 when bounce was received
           90  +@ );
           91  +;
           92  +
           93  +/*
           94  +** Return true if the email notification tables exist.
           95  +*/
           96  +int email_tables_exist(void){
           97  +  return db_table_exists("repository", "subscriber");
           98  +}
           99  +
          100  +/*
          101  +** Make sure the table needed for email notification exist in the repository.
          102  +**
          103  +** If the bOnlyIfEnabled option is true, then tables are only created
          104  +** if the email-send-method is something other than "off".
          105  +*/
          106  +void email_schema(int bOnlyIfEnabled){
          107  +  if( !email_tables_exist() ){
          108  +    if( bOnlyIfEnabled
          109  +     && fossil_strcmp(db_get("email-send-method","off"),"off")==0
          110  +    ){
          111  +      return;  /* Don't create table for disabled email */
          112  +    }
          113  +    db_multi_exec(zEmailInit/*works-like:""*/);
          114  +    email_triggers_enable();
          115  +  }
          116  +}
          117  +
          118  +/*
          119  +** Enable triggers that automatically populate the pending_alert
          120  +** table.
          121  +*/
          122  +void email_triggers_enable(void){
          123  +  if( !db_table_exists("repository","pending_alert") ) return;
          124  +  db_multi_exec(
          125  +    "CREATE TRIGGER IF NOT EXISTS repository.email_trigger1\n"
          126  +    "AFTER INSERT ON event BEGIN\n"
          127  +    "  INSERT INTO pending_alert(eventid)\n"
          128  +    "    SELECT printf('%%.1c%%d',new.type,new.objid) WHERE true\n"
          129  +    "    ON CONFLICT(eventId) DO NOTHING;\n"
          130  +    "END;"
          131  +  );
          132  +}
          133  +
          134  +/*
          135  +** Disable triggers the event_pending triggers.
          136  +**
          137  +** This must be called before rebuilding the EVENT table, for example
          138  +** via the "fossil rebuild" command.
          139  +*/
          140  +void email_triggers_disable(void){
          141  +  db_multi_exec(
          142  +    "DROP TRIGGER IF EXISTS repository.email_trigger1;\n"
          143  +  );
          144  +}
          145  +
          146  +/*
          147  +** Return true if email alerts are active.
          148  +*/
          149  +int email_enabled(void){
          150  +  if( !email_tables_exist() ) return 0;
          151  +  if( fossil_strcmp(db_get("email-send-method","off"),"off")==0 ) return 0;
          152  +  return 1;
          153  +}
          154  +
          155  +/*
          156  +** If the subscriber table does not exist, then paint an error message
          157  +** web page and return true.
          158  +**
          159  +** If the subscriber table does exist, return 0 without doing anything.
          160  +*/
          161  +static int email_webpages_disabled(void){
          162  +  if( email_tables_exist() ) return 0;
          163  +  style_header("Email Alerts Are Disabled");
          164  +  @ <p>Email alerts are disabled on this server</p>
          165  +  style_footer();
          166  +  return 1;
          167  +}
          168  +
          169  +/*
          170  +** Insert a "Subscriber List" submenu link if the current user
          171  +** is an administrator.
          172  +*/
          173  +void email_submenu_common(void){
          174  +  if( g.perm.Admin ){
          175  +    if( fossil_strcmp(g.zPath,"subscribers") ){
          176  +      style_submenu_element("List Subscribers","%R/subscribers");
          177  +    }
          178  +    if( fossil_strcmp(g.zPath,"subscribe") ){
          179  +      style_submenu_element("Add New Subscriber","%R/subscribe");
          180  +    }
          181  +  }
          182  +}
          183  +
          184  +
          185  +/*
          186  +** WEBPAGE: setup_notification
          187  +**
          188  +** Administrative page for configuring and controlling email notification.
          189  +** Normally accessible via the /Admin/Notification menu.
          190  +*/
          191  +void setup_notification(void){
          192  +  static const char *const azSendMethods[] = {
          193  +    "off",   "Disabled",
          194  +    "pipe",  "Pipe to a command",
          195  +    "db",    "Store in a database",
          196  +    "dir",   "Store in a directory",
          197  +    "relay", "SMTP relay"
          198  +  };
          199  +  login_check_credentials();
          200  +  if( !g.perm.Setup ){
          201  +    login_needed(0);
          202  +    return;
          203  +  }
          204  +  db_begin_transaction();
          205  +
          206  +  email_submenu_common();
          207  +  style_submenu_element("Send Announcement","%R/announce");
          208  +  style_header("Email Notification Setup");
          209  +  @ <form action="%R/setup_notification" method="post"><div>
          210  +  @ <input type="submit"  name="submit" value="Apply Changes" /><hr>
          211  +  login_insert_csrf_secret();
          212  +
          213  +  entry_attribute("Canonical Server URL", 40, "email-url",
          214  +                   "eurl", "", 0);
          215  +  @ <p><b>Required.</b>
          216  +  @ This is URL used as the basename for hyperlinks included in
          217  +  @ email alert text.  Omit the trailing "/".
          218  +  @ Suggested value: "%h(g.zBaseURL)"
          219  +  @ (Property: "email-url")</p>
          220  +  @ <hr>
          221  +
          222  +  entry_attribute("\"From\" email address", 20, "email-self",
          223  +                   "eself", "", 0);
          224  +  @ <p><b>Required.</b>
          225  +  @ This is the email from which email notifications are sent.  The
          226  +  @ system administrator should arrange for emails sent to this address
          227  +  @ to be handed off to the "fossil email incoming" command so that Fossil
          228  +  @ can handle bounces. (Property: "email-self")</p>
          229  +  @ <hr>
          230  +
          231  +  entry_attribute("Repository Nickname", 16, "email-subname",
          232  +                   "enn", "", 0);
          233  +  @ <p><b>Required.</b>
          234  +  @ This is short name used to identifies the repository in the
          235  +  @ Subject: line of email alerts.  Traditionally this name is
          236  +  @ included in square brackets.  Examples: "[fossil-src]", "[sqlite-src]".
          237  +  @ (Property: "email-subname")</p>
          238  +  @ <hr>
          239  +
          240  +  onoff_attribute("Automatic Email Exec", "email-autoexec",
          241  +                   "eauto", 0, 0);
          242  +  @ <p>If enabled, then email notifications are automatically
          243  +  @ dispatched after some webpages are accessed.  This eliminates the
          244  +  @ need to have a cron job running to invoke "fossil email exec"
          245  +  @ periodically.
          246  +  @ (Property: "email-autoexec")</p>
          247  +  @ <hr>
          248  +
          249  +  multiple_choice_attribute("Email Send Method", "email-send-method", "esm",
          250  +       "off", count(azSendMethods)/2, azSendMethods);
          251  +  @ <p>How to send email.  Requires auxiliary information from the fields
          252  +  @ that follow.  Hint: Use the <a href="%R/announce">/announce</a> page
          253  +  @ to send test message to debug this setting.
          254  +  @ (Property: "email-send-method")</p>
          255  +  email_schema(1);
          256  +  entry_attribute("Pipe Email Text Into This Command", 60, "email-send-command",
          257  +                   "ecmd", "sendmail -t", 0);
          258  +  @ <p>When the send method is "pipe to a command", this is the command
          259  +  @ that is run.  Email messages are piped into the standard input of this
          260  +  @ command.  The command is expected to extract the sender address,
          261  +  @ recepient addresses, and subject from the header of the piped email
          262  +  @ text.  (Property: "email-send-command")</p>
          263  +
          264  +  entry_attribute("Store Emails In This Database", 60, "email-send-db",
          265  +                   "esdb", "", 0);
          266  +  @ <p>When the send method is "store in a databaes", each email message is
          267  +  @ stored in an SQLite database file with the name given here.
          268  +  @ (Property: "email-send-db")</p>
          269  +
          270  +  entry_attribute("Store Emails In This Directory", 60, "email-send-dir",
          271  +                   "esdir", "", 0);
          272  +  @ <p>When the send method is "store in a directory", each email message is
          273  +  @ stored as a separate file in the directory shown here.
          274  +  @ (Property: "email-send-dir")</p>
          275  +
          276  +  entry_attribute("SMTP Relay Host", 60, "email-send-relayhost",
          277  +                   "esrh", "", 0);
          278  +  @ <p>When the send method is "SMTP relay", each email message is
          279  +  @ transmitted via the SMTP protocol (rfc5321) to a "Mail Submission
          280  +  @ Agent" or "MSA" (rfc4409) at the hostname shown here.  Optionally
          281  +  @ append a colon and TCP port number (ex: smtp.example.com:587).
          282  +  @ The default TCP port number is 25.
          283  +  @ (Property: "email-send-relayhost")</p>
          284  +  @ <hr>
          285  +
          286  +  entry_attribute("Administrator email address", 40, "email-admin",
          287  +                   "eadmin", "", 0);
          288  +  @ <p>This is the email for the human administrator for the system.
          289  +  @ Abuse and trouble reports are send here.
          290  +  @ (Property: "email-admin")</p>
          291  +  @ <hr>
          292  +
          293  +  entry_attribute("Inbound email directory", 40, "email-receive-dir",
          294  +                   "erdir", "", 0);
          295  +  @ <p>Inbound emails can be stored in a directory for analysis as
          296  +  @ a debugging aid.  Put the name of that directory in this entry box.
          297  +  @ Disable saving of inbound email by making this an empty string.
          298  +  @ Abuse and trouble reports are send here.
          299  +  @ (Property: "email-receive-dir")</p>
          300  +  @ <hr>
          301  +  @ <p><input type="submit"  name="submit" value="Apply Changes" /></p>
          302  +  @ </div></form>
          303  +  db_end_transaction(0);
          304  +  style_footer();
          305  +}
          306  +
          307  +#if 0
          308  +/*
          309  +** Encode pMsg as MIME base64 and append it to pOut
          310  +*/
          311  +static void append_base64(Blob *pOut, Blob *pMsg){
          312  +  int n, i, k;
          313  +  char zBuf[100];
          314  +  n = blob_size(pMsg);
          315  +  for(i=0; i<n; i+=54){
          316  +    k = translateBase64(blob_buffer(pMsg)+i, i+54<n ? 54 : n-i, zBuf);
          317  +    blob_append(pOut, zBuf, k);
          318  +    blob_append(pOut, "\r\n", 2);
          319  +  }
          320  +}
          321  +#endif
          322  +
          323  +/*
          324  +** Encode pMsg using the quoted-printable email encoding and
          325  +** append it onto pOut
          326  +*/
          327  +static void append_quoted(Blob *pOut, Blob *pMsg){
          328  +  char *zIn = blob_str(pMsg);
          329  +  char c;
          330  +  int iCol = 0;
          331  +  while( (c = *(zIn++))!=0 ){
          332  +    if( (c>='!' && c<='~' && c!='=' && c!=':')
          333  +     || (c==' ' && zIn[0]!='\r' && zIn[0]!='\n')
          334  +    ){
          335  +      blob_append_char(pOut, c);
          336  +      iCol++;
          337  +      if( iCol>=70 ){
          338  +        blob_append(pOut, "=\r\n", 3);
          339  +        iCol = 0;
          340  +      }
          341  +    }else if( c=='\r' && zIn[0]=='\n' ){
          342  +      zIn++;
          343  +      blob_append(pOut, "\r\n", 2);
          344  +      iCol = 0;
          345  +    }else if( c=='\n' ){
          346  +      blob_append(pOut, "\r\n", 2);
          347  +      iCol = 0;
          348  +    }else{
          349  +      char x[3];
          350  +      x[0] = '=';
          351  +      x[1] = "0123456789ABCDEF"[(c>>4)&0xf];
          352  +      x[2] = "0123456789ABCDEF"[c&0xf];
          353  +      blob_append(pOut, x, 3);
          354  +      iCol += 3;
          355  +    }
          356  +  }
          357  +}
          358  +
          359  +/*
          360  +** Come up with a unique filename in the zDir directory.
          361  +**
          362  +** Space to hold the filename is obtained from mprintf() and must
          363  +** be freed using fossil_free() by the caller.
          364  +*/
          365  +static char *emailTempFilename(const char *zDir){
          366  +  char *zFile = db_text(0,
          367  +     "SELECT %Q||strftime('/%%Y%%m%%d%%H%%M%%S-','now')||hex(randomblob(8))",
          368  +        zDir);
          369  +  return zFile;
          370  +}
          371  +
          372  +#if defined(_WIN32) || defined(WIN32)
          373  +# undef popen
          374  +# define popen _popen
          375  +# undef pclose
          376  +# define pclose _pclose
          377  +#endif
          378  +
          379  +#if INTERFACE
          380  +/*
          381  +** An instance of the following object is used to send emails.
          382  +*/
          383  +struct EmailSender {
          384  +  sqlite3 *db;               /* Database emails are sent to */
          385  +  sqlite3_stmt *pStmt;       /* Stmt to insert into the database */
          386  +  const char *zDest;         /* How to send email. */
          387  +  const char *zDb;           /* Name of database file */
          388  +  const char *zDir;          /* Directory in which to store as email files */
          389  +  const char *zCmd;          /* Command to run for each email */
          390  +  const char *zFrom;         /* Emails come from here */
          391  +  SmtpSession *pSmtp;        /* SMTP relay connection */
          392  +  Blob out;                  /* For zDest=="blob" */
          393  +  char *zErr;                /* Error message */
          394  +  u32 mFlags;                /* Flags */
          395  +  int bImmediateFail;        /* On any error, call fossil_fatal() */
          396  +};
          397  +
          398  +/* Allowed values for EmailSender flags */
          399  +#define EMAIL_IMMEDIATE_FAIL   0x0001   /* Call fossil_fatal() on any error */
          400  +#define EMAIL_SMTP_TRACE       0x0002   /* Write SMTP transcript to console */
          401  +
          402  +#endif /* INTERFACE */
          403  +
          404  +/*
          405  +** Shutdown an emailer.  Clear all information other than the error message.
          406  +*/
          407  +static void emailerShutdown(EmailSender *p){
          408  +  sqlite3_finalize(p->pStmt);
          409  +  p->pStmt = 0;
          410  +  sqlite3_close(p->db);
          411  +  p->db = 0;
          412  +  p->zDb = 0;
          413  +  p->zDir = 0;
          414  +  p->zCmd = 0;
          415  +  if( p->pSmtp ){
          416  +    smtp_client_quit(p->pSmtp);
          417  +    smtp_session_free(p->pSmtp);
          418  +    p->pSmtp = 0;
          419  +  }
          420  +  blob_reset(&p->out);
          421  +}
          422  +
          423  +/*
          424  +** Put the EmailSender into an error state.
          425  +*/
          426  +static void emailerError(EmailSender *p, const char *zFormat, ...){
          427  +  va_list ap;
          428  +  fossil_free(p->zErr);
          429  +  va_start(ap, zFormat);
          430  +  p->zErr = vmprintf(zFormat, ap);
          431  +  va_end(ap);
          432  +  emailerShutdown(p);
          433  +  if( p->mFlags & EMAIL_IMMEDIATE_FAIL ){
          434  +    fossil_fatal("%s", p->zErr);
          435  +  }
          436  +}
          437  +
          438  +/*
          439  +** Free an email sender object
          440  +*/
          441  +void email_sender_free(EmailSender *p){
          442  +  if( p ){
          443  +    emailerShutdown(p);
          444  +    fossil_free(p->zErr);
          445  +    fossil_free(p);
          446  +  }
          447  +}
          448  +
          449  +/*
          450  +** Get an email setting value.  Report an error if not configured.
          451  +** Return 0 on success and one if there is an error.
          452  +*/
          453  +static int emailerGetSetting(
          454  +  EmailSender *p,        /* Where to report the error */
          455  +  const char **pzVal,    /* Write the setting value here */
          456  +  const char *zName      /* Name of the setting */
          457  +){
          458  +  const char *z = db_get(zName, 0);
          459  +  int rc = 0;
          460  +  if( z==0 || z[0]==0 ){
          461  +    emailerError(p, "missing \"%s\" setting", zName);
          462  +    rc = 1;
          463  +  }else{
          464  +    *pzVal = z;
          465  +  }
          466  +  return rc;
          467  +}
          468  +
          469  +/*
          470  +** Create a new EmailSender object.
          471  +**
          472  +** The method used for sending email is determined by various email-*
          473  +** settings, and especially email-send-method.  The repository
          474  +** email-send-method can be overridden by the zAltDest argument to
          475  +** cause a different sending mechanism to be used.  Pass "stdout" to
          476  +** zAltDest to cause all emails to be printed to the console for
          477  +** debugging purposes.
          478  +**
          479  +** The EmailSender object returned must be freed using email_sender_free().
          480  +*/
          481  +EmailSender *email_sender_new(const char *zAltDest, u32 mFlags){
          482  +  EmailSender *p;
          483  +
          484  +  p = fossil_malloc(sizeof(*p));
          485  +  memset(p, 0, sizeof(*p));
          486  +  blob_init(&p->out, 0, 0);
          487  +  p->mFlags = mFlags;
          488  +  if( zAltDest ){
          489  +    p->zDest = zAltDest;
          490  +  }else{
          491  +    p->zDest = db_get("email-send-method","off");
          492  +  }
          493  +  if( fossil_strcmp(p->zDest,"off")==0 ) return p;
          494  +  if( emailerGetSetting(p, &p->zFrom, "email-self") ) return p;
          495  +  if( fossil_strcmp(p->zDest,"db")==0 ){
          496  +    char *zErr;
          497  +    int rc;
          498  +    if( emailerGetSetting(p, &p->zDb, "email-send-db") ) return p;
          499  +    rc = sqlite3_open(p->zDb, &p->db);
          500  +    if( rc ){
          501  +      emailerError(p, "unable to open output database file \"%s\": %s",
          502  +                   p->zDb, sqlite3_errmsg(p->db));
          503  +      return p;
          504  +    }
          505  +    rc = sqlite3_exec(p->db, "CREATE TABLE IF NOT EXISTS email(\n"
          506  +                          "  emailid INTEGER PRIMARY KEY,\n"
          507  +                          "  msg TEXT\n);", 0, 0, &zErr);
          508  +    if( zErr ){
          509  +      emailerError(p, "CREATE TABLE failed with \"%s\"", zErr);
          510  +      sqlite3_free(zErr);
          511  +      return p;
          512  +    }
          513  +    rc = sqlite3_prepare_v2(p->db, "INSERT INTO email(msg) VALUES(?1)", -1,
          514  +                            &p->pStmt, 0);
          515  +    if( rc ){
          516  +      emailerError(p, "cannot prepare INSERT statement: %s",
          517  +                 sqlite3_errmsg(p->db));
          518  +      return p;
          519  +    }
          520  +  }else if( fossil_strcmp(p->zDest, "pipe")==0 ){
          521  +    emailerGetSetting(p, &p->zCmd, "email-send-command");
          522  +  }else if( fossil_strcmp(p->zDest, "dir")==0 ){
          523  +    emailerGetSetting(p, &p->zDir, "email-send-dir");
          524  +  }else if( fossil_strcmp(p->zDest, "blob")==0 ){
          525  +    blob_init(&p->out, 0, 0);
          526  +  }else if( fossil_strcmp(p->zDest, "relay")==0 ){
          527  +    const char *zRelay = 0;
          528  +    emailerGetSetting(p, &zRelay, "email-send-relayhost");
          529  +    if( zRelay ){
          530  +      u32 smtpFlags = SMTP_DIRECT;
          531  +      if( mFlags & EMAIL_SMTP_TRACE ) smtpFlags |= SMTP_TRACE_STDOUT;
          532  +      p->pSmtp = smtp_session_new(p->zFrom, zRelay, smtpFlags);
          533  +      smtp_client_startup(p->pSmtp);
          534  +    }
          535  +  }
          536  +  return p;
          537  +}
          538  +
          539  +/*
          540  +** Scan the header of the email message in pMsg looking for the
          541  +** (first) occurrance of zField.  Fill pValue with the content of
          542  +** that field.
          543  +**
          544  +** This routine initializes pValue.  Any prior content of pValue is
          545  +** discarded (leaked).
          546  +**
          547  +** Return non-zero on success.  Return 0 if no instance of the header
          548  +** is found.
          549  +*/
          550  +int email_header_value(Blob *pMsg, const char *zField, Blob *pValue){
          551  +  int nField = (int)strlen(zField);
          552  +  Blob line;
          553  +  blob_rewind(pMsg);
          554  +  blob_init(pValue,0,0);
          555  +  while( blob_line(pMsg, &line) ){
          556  +    int n, i;
          557  +    char *z;
          558  +    blob_trim(&line);
          559  +    n = blob_size(&line);
          560  +    if( n==0 ) return 0;
          561  +    if( n<nField+1 ) continue;
          562  +    z = blob_buffer(&line);
          563  +    if( sqlite3_strnicmp(z, zField, nField)==0 && z[nField]==':' ){
          564  +      for(i=nField+1; i<n && fossil_isspace(z[i]); i++){}
          565  +      blob_init(pValue, z+i, n-i);
          566  +      while( blob_line(pMsg, &line) ){
          567  +        blob_trim(&line);
          568  +        n = blob_size(&line);
          569  +        if( n==0 ) break;
          570  +        z = blob_buffer(&line);
          571  +        if( !fossil_isspace(z[0]) ) break;
          572  +        for(i=1; i<n && fossil_isspace(z[i]); i++){}
          573  +        blob_append(pValue, " ", 1);
          574  +        blob_append(pValue, z+i, n-i);
          575  +      }
          576  +      return 1;
          577  +    }
          578  +  }
          579  +  return 0;
          580  +}
          581  +
          582  +/*
          583  +** Make a copy of the input string up to but not including the
          584  +** first ">" character.
          585  +**
          586  +** Verify that the string really that is to be copied really is a
          587  +** valid email address.  If it is not, then return NULL.
          588  +**
          589  +** This routine is more restrictive than necessary.  It does not
          590  +** allow comments, IP address, quoted strings, or certain uncommon
          591  +** characters.  The only non-alphanumerics allowed in the local
          592  +** part are "_", "+", "-" and "+".
          593  +*/
          594  +char *email_copy_addr(const char *z){
          595  +  int i;
          596  +  int nAt = 0;
          597  +  int nDot = 0;
          598  +  char c;
          599  +  if( z[0]=='.' ) return 0;  /* Local part cannot begin with "." */
          600  +  for(i=0; (c = z[i])!=0 && c!='>'; i++){
          601  +    if( fossil_isalnum(c) ){
          602  +      /* Alphanumerics are always ok */
          603  +    }else if( c=='@' ){
          604  +      if( nAt ) return 0;   /* Only a single "@"  allowed */
          605  +      if( i>64 ) return 0;  /* Local part too big */
          606  +      nAt = 1;
          607  +      nDot = 0;
          608  +      if( i==0 ) return 0;  /* Disallow empty local part */
          609  +      if( z[i-1]=='.' ) return 0; /* Last char of local cannot be "." */
          610  +      if( z[i+1]=='.' || z[i+1]=='-' ){
          611  +        return 0; /* Domain cannot begin with "." or "-" */
          612  +      }
          613  +    }else if( c=='-' ){
          614  +      if( z[i+1]=='>' ) return 0;  /* Last character cannot be "-" */
          615  +    }else if( c=='.' ){
          616  +      if( z[i+1]=='.' ) return 0;  /* Do not allow ".." */
          617  +      if( z[i+1]=='>' ) return 0;  /* Domain may not end with . */
          618  +      nDot++;
          619  +    }else if( (c=='_' || c=='+') && nAt==0 ){
          620  +      /* _ and + are ok in the local part */
          621  +    }else{
          622  +      return 0;   /* Anything else is an error */
          623  +    }
          624  +  }
          625  +  if( c!='>' ) return 0;      /* Missing final ">" */
          626  +  if( nAt==0 ) return 0;      /* No "@" found anywhere */
          627  +  if( nDot==0 ) return 0;     /* No "." in the domain */
          628  +
          629  +  /* If we reach this point, the email address is valid */
          630  +  return mprintf("%.*s", i, z);
          631  +}
          632  +
          633  +/*
          634  +** Extract all To: header values from the email header supplied.
          635  +** Store them in the array list.
          636  +*/
          637  +void email_header_to(Blob *pMsg, int *pnTo, char ***pazTo){
          638  +  int nTo = 0;
          639  +  char **azTo = 0;
          640  +  Blob v;
          641  +  char *z, *zAddr;
          642  +  int i;
          643  +  
          644  +  email_header_value(pMsg, "to", &v);
          645  +  z = blob_str(&v);
          646  +  for(i=0; z[i]; i++){
          647  +    if( z[i]=='<' && (zAddr = email_copy_addr(&z[i+1]))!=0 ){
          648  +      azTo = fossil_realloc(azTo, sizeof(azTo[0])*(nTo+1) );
          649  +      azTo[nTo++] = zAddr;
          650  +    }
          651  +  }
          652  +  *pnTo = nTo;
          653  +  *pazTo = azTo;
          654  +}
          655  +
          656  +/*
          657  +** Free a list of To addresses obtained from a prior call to 
          658  +** email_header_to()
          659  +*/
          660  +void email_header_to_free(int nTo, char **azTo){
          661  +  int i;
          662  +  for(i=0; i<nTo; i++) fossil_free(azTo[i]);
          663  +  fossil_free(azTo);
          664  +}
          665  +
          666  +/*
          667  +** Send a single email message.
          668  +**
          669  +** The recepient(s) must be specified using  "To:" or "Cc:" or "Bcc:" fields
          670  +** in the header.  Likewise, the header must contains a "Subject:" line.
          671  +** The header might also include fields like "Message-Id:" or
          672  +** "In-Reply-To:".
          673  +**
          674  +** This routine will add fields to the header as follows:
          675  +**
          676  +**     From:
          677  +**     Date:
          678  +**     Message-Id:
          679  +**     Content-Type:
          680  +**     Content-Transfer-Encoding:
          681  +**     
          682  +** The caller maintains ownership of the input Blobs.  This routine will
          683  +** read the Blobs and send them onward to the email system, but it will
          684  +** not free them.
          685  +*/
          686  +void email_send(EmailSender *p, Blob *pHdr, Blob *pBody){
          687  +  Blob all, *pOut;
          688  +  u64 r1, r2;
          689  +  if( fossil_strcmp(p->zDest, "off")==0 ){
          690  +    return;
          691  +  }
          692  +  if( fossil_strcmp(p->zDest, "blob")==0 ){
          693  +    pOut = &p->out;
          694  +    if( blob_size(pOut) ){
          695  +      blob_appendf(pOut, "%.72c\n", '=');
          696  +    }
          697  +  }else{
          698  +    blob_init(&all, 0, 0);
          699  +    pOut = &all;
          700  +  }
          701  +  blob_append(pOut, blob_buffer(pHdr), blob_size(pHdr));
          702  +  blob_appendf(pOut, "From: <%s>\r\n", p->zFrom);
          703  +  blob_appendf(pOut, "Date: %z\r\n", cgi_rfc822_datestamp(time(0)));
          704  +  /* Message-id format:  "<$(date)x$(random).$(from)>" where $(date) is
          705  +  ** the current unix-time in hex, $(random) is a 64-bit random number,
          706  +  ** and $(from) is the sender. */
          707  +  sqlite3_randomness(sizeof(r1), &r1);
          708  +  r2 = time(0);
          709  +  blob_appendf(pOut, "Message-Id: <%llxx%016llx.%s>\r\n", r2, r1, p->zFrom);
          710  +  blob_add_final_newline(pBody);
          711  +  blob_appendf(pOut,"Content-Type: text/plain\r\n");
          712  +#if 0
          713  +  blob_appendf(pOut, "Content-Transfer-Encoding: base64\r\n\r\n");
          714  +  append_base64(pOut, pBody);
          715  +#else
          716  +  blob_appendf(pOut, "Content-Transfer-Encoding: quoted-printable\r\n\r\n");
          717  +  append_quoted(pOut, pBody);
          718  +#endif
          719  +  if( p->pStmt ){
          720  +    int i, rc;
          721  +    sqlite3_bind_text(p->pStmt, 1, blob_str(&all), -1, SQLITE_TRANSIENT);
          722  +    for(i=0; i<100 && sqlite3_step(p->pStmt)==SQLITE_BUSY; i++){
          723  +      sqlite3_sleep(10);
          724  +    }
          725  +    rc = sqlite3_reset(p->pStmt);
          726  +    if( rc!=SQLITE_OK ){
          727  +      emailerError(p, "Failed to insert email message into output queue.\n"
          728  +                      "%s", sqlite3_errmsg(p->db));
          729  +    }
          730  +  }else if( p->zCmd ){
          731  +    FILE *out = popen(p->zCmd, "w");
          732  +    if( out ){
          733  +      fwrite(blob_buffer(&all), 1, blob_size(&all), out);
          734  +      fclose(out);
          735  +    }else{
          736  +      emailerError(p, "Could not open output pipe \"%s\"", p->zCmd);
          737  +    }
          738  +  }else if( p->zDir ){
          739  +    char *zFile = emailTempFilename(p->zDir);
          740  +    blob_write_to_file(&all, zFile);
          741  +    fossil_free(zFile);
          742  +  }else if( p->pSmtp ){
          743  +    char **azTo = 0;
          744  +    int nTo = 0;
          745  +    email_header_to(pHdr, &nTo, &azTo);
          746  +    if( nTo>0 ){
          747  +      smtp_send_msg(p->pSmtp, p->zFrom, nTo, (const char**)azTo,blob_str(&all));
          748  +      email_header_to_free(nTo, azTo);
          749  +    }
          750  +  }else if( strcmp(p->zDest, "stdout")==0 ){
          751  +    char **azTo = 0;
          752  +    int nTo = 0;
          753  +    int i;
          754  +    email_header_to(pHdr, &nTo, &azTo);
          755  +    for(i=0; i<nTo; i++){
          756  +      fossil_print("X-To-Test-%d: [%s]\r\n", i, azTo[i]);
          757  +    }
          758  +    email_header_to_free(nTo, azTo);
          759  +    blob_add_final_newline(&all);
          760  +    fossil_print("%s", blob_str(&all));
          761  +  }
          762  +  blob_reset(&all);
          763  +}
          764  +
          765  +/*
          766  +** Analyze and act on a received email.
          767  +**
          768  +** This routine takes ownership of the Blob parameter and is responsible
          769  +** for freeing that blob when it is done with it.
          770  +**
          771  +** This routine acts on all email messages received from the
          772  +** "fossil email inbound" command.
          773  +*/
          774  +void email_receive(Blob *pMsg){
          775  +  /* To Do:  Look for bounce messages and possibly disable subscriptions */
          776  +  blob_reset(pMsg);
          777  +}
          778  +
          779  +/*
          780  +** SETTING: email-send-method         width=5 default=off
          781  +** Determine the method used to send email.  Allowed values are
          782  +** "off", "relay", "pipe", "dir", "db", and "stdout".  The "off" value
          783  +** means no email is ever sent.  The "relay" value means emails are sent
          784  +** to an Mail Sending Agent using SMTP located at email-send-relayhost.
          785  +** The "pipe" value means email messages are piped into a command 
          786  +** determined by the email-send-command setting. The "dir" value means
          787  +** emails are written to individual files in a directory determined
          788  +** by the email-send-dir setting.  The "db" value means that emails
          789  +** are added to an SQLite database named by the* email-send-db setting.
          790  +** The "stdout" value writes email text to standard output, for debugging.
          791  +*/
          792  +/*
          793  +** SETTING: email-send-command       width=40
          794  +** This is a command to which outbound email content is piped when the
          795  +** email-send-method is set to "pipe".  The command must extract
          796  +** recipient, sender, subject, and all other relevant information
          797  +** from the email header.
          798  +*/
          799  +/*
          800  +** SETTING: email-send-dir           width=40
          801  +** This is a directory into which outbound emails are written as individual
          802  +** files if the email-send-method is set to "dir".
          803  +*/
          804  +/*
          805  +** SETTING: email-send-db            width=40
          806  +** This is an SQLite database file into which outbound emails are written
          807  +** if the email-send-method is set to "db".
          808  +*/
          809  +/*
          810  +** SETTING: email-self               width=40
          811  +** This is the email address for the repository.  Outbound emails add
          812  +** this email address as the "From:" field.
          813  +*/
          814  +/*
          815  +** SETTING: email-receive-dir         width=40
          816  +** Inbound email messages are saved as separate files in this directory,
          817  +** for debugging analysis.  Disable saving of inbound emails omitting
          818  +** this setting, or making it an empty string.
          819  +*/
          820  +/*
          821  +** SETTING: email-send-relayhost      width=40
          822  +** This is the hostname and TCP port to which output email messages
          823  +** are sent when email-send-method is "relay".  There should be an
          824  +** SMTP server configured as a Mail Submission Agent listening on the
          825  +** designated host and port and all times.
          826  +*/
          827  +
          828  +
          829  +/*
          830  +** COMMAND: email
          831  +** 
          832  +** Usage: %fossil email SUBCOMMAND ARGS...
          833  +**
          834  +** Subcommands:
          835  +**
          836  +**    exec                    Compose and send pending email alerts.
          837  +**                            Some installations may want to do this via
          838  +**                            a cron-job to make sure alerts are sent
          839  +**                            in a timely manner.
          840  +**                            Options:
          841  +**
          842  +**                               --digest     Send digests
          843  +**                               --test       Resets to standard output
          844  +**
          845  +**    inbound [FILE]          Receive an inbound email message.  This message
          846  +**                            is analyzed to see if it is a bounce, and if
          847  +**                            necessary, subscribers may be disabled.
          848  +**
          849  +**    reset                   Hard reset of all email notification tables
          850  +**                            in the repository.  This erases all subscription
          851  +**                            information.  Use with extreme care.
          852  +**
          853  +**    send TO [OPTIONS]       Send a single email message using whatever
          854  +**                            email sending mechanism is currently configured.
          855  +**                            Use this for testing the email configuration.
          856  +**                            Options:
          857  +**
          858  +**                              --body FILENAME
          859  +**                              --smtp-trace
          860  +**                              --stdout
          861  +**                              --subject|-S SUBJECT
          862  +**
          863  +**    settings [NAME VALUE]   With no arguments, list all email settings.
          864  +**                            Or change the value of a single email setting.
          865  +**
          866  +**    subscribers [PATTERN]   List all subscribers matching PATTERN.
          867  +**
          868  +**    unsubscribe EMAIL       Remove a single subscriber with the given EMAIL.
          869  +*/
          870  +void email_cmd(void){
          871  +  const char *zCmd;
          872  +  int nCmd;
          873  +  db_find_and_open_repository(0, 0);
          874  +  email_schema(0);
          875  +  zCmd = g.argc>=3 ? g.argv[2] : "x";
          876  +  nCmd = (int)strlen(zCmd);
          877  +  if( strncmp(zCmd, "exec", nCmd)==0 ){
          878  +    u32 eFlags = 0;
          879  +    if( find_option("digest",0,0)!=0 ) eFlags |= SENDALERT_DIGEST;
          880  +    if( find_option("test",0,0)!=0 ){
          881  +      eFlags |= SENDALERT_PRESERVE|SENDALERT_STDOUT;
          882  +    }
          883  +    verify_all_options();
          884  +    email_send_alerts(eFlags);
          885  +  }else
          886  +  if( strncmp(zCmd, "inbound", nCmd)==0 ){
          887  +    Blob email;
          888  +    const char *zInboundDir = db_get("email-receive-dir","");
          889  +    verify_all_options();
          890  +    if( g.argc!=3 && g.argc!=4 ){
          891  +      usage("inbound [FILE]");
          892  +    }
          893  +    blob_read_from_file(&email, g.argc==3 ? "-" : g.argv[3], ExtFILE);
          894  +    if( zInboundDir[0] ){
          895  +      char *zFN = emailTempFilename(zInboundDir);
          896  +      blob_write_to_file(&email, zFN);
          897  +      fossil_free(zFN);
          898  +    }
          899  +    email_receive(&email);
          900  +  }else
          901  +  if( strncmp(zCmd, "reset", nCmd)==0 ){
          902  +    int c;
          903  +    int bForce = find_option("force","f",0)!=0;
          904  +    verify_all_options();
          905  +    if( bForce ){
          906  +      c = 'y';
          907  +    }else{
          908  +      Blob yn;
          909  +      fossil_print(
          910  +          "This will erase all content in the repository tables, thus\n"
          911  +          "deleting all subscriber information.  The information will be\n"
          912  +          "unrecoverable.\n");
          913  +      prompt_user("Continue? (y/N) ", &yn);
          914  +      c = blob_str(&yn)[0];
          915  +      blob_reset(&yn);
          916  +    }
          917  +    if( c=='y' ){
          918  +      email_triggers_disable();
          919  +      db_multi_exec(
          920  +        "DROP TABLE IF EXISTS subscriber;\n"
          921  +        "DROP TABLE IF EXISTS pending_alert;\n"
          922  +        "DROP TABLE IF EXISTS email_bounce;\n"
          923  +        /* Legacy */
          924  +        "DROP TABLE IF EXISTS email_pending;\n"
          925  +        "DROP TABLE IF EXISTS subscription;\n"
          926  +      );
          927  +      email_schema(0);
          928  +    }
          929  +  }else
          930  +  if( strncmp(zCmd, "send", nCmd)==0 ){
          931  +    Blob prompt, body, hdr;
          932  +    const char *zDest = find_option("stdout",0,0)!=0 ? "stdout" : 0;
          933  +    int i;
          934  +    u32 mFlags = EMAIL_IMMEDIATE_FAIL;
          935  +    const char *zSubject = find_option("subject", "S", 1);
          936  +    const char *zSource = find_option("body", 0, 1);
          937  +    EmailSender *pSender;
          938  +    if( find_option("smtp-trace",0,0)!=0 ) mFlags |= EMAIL_SMTP_TRACE;
          939  +    verify_all_options();
          940  +    blob_init(&prompt, 0, 0);
          941  +    blob_init(&body, 0, 0);
          942  +    blob_init(&hdr, 0, 0);
          943  +    blob_appendf(&hdr,"To: ");
          944  +    for(i=3; i<g.argc; i++){
          945  +      if( i>3 ) blob_append(&hdr, ", ", 2);
          946  +      blob_appendf(&hdr, "<%s>", g.argv[i]);
          947  +    }
          948  +    blob_append(&hdr,"\r\n",2);
          949  +    if( zSubject ){
          950  +      blob_appendf(&hdr, "Subject: %s\r\n", zSubject);
          951  +    }
          952  +    if( zSource ){
          953  +      blob_read_from_file(&body, zSource, ExtFILE);
          954  +    }else{
          955  +      prompt_for_user_comment(&body, &prompt);
          956  +    }
          957  +    blob_add_final_newline(&body);
          958  +    pSender = email_sender_new(zDest, mFlags);
          959  +    email_send(pSender, &hdr, &body);
          960  +    email_sender_free(pSender);
          961  +    blob_reset(&hdr);
          962  +    blob_reset(&body);
          963  +    blob_reset(&prompt);
          964  +  }else
          965  +  if( strncmp(zCmd, "settings", nCmd)==0 ){
          966  +    int isGlobal = find_option("global",0,0)!=0;
          967  +    int nSetting;
          968  +    const Setting *pSetting = setting_info(&nSetting);
          969  +    db_open_config(1, 0);
          970  +    verify_all_options();
          971  +    if( g.argc!=3 && g.argc!=5 ) usage("setting [NAME VALUE]");
          972  +    if( g.argc==5 ){
          973  +      const char *zLabel = g.argv[3];
          974  +      if( strncmp(zLabel, "email-", 6)!=0
          975  +       || (pSetting = db_find_setting(zLabel, 1))==0 ){
          976  +        fossil_fatal("not a valid email setting: \"%s\"", zLabel);
          977  +      }
          978  +      db_set(pSetting->name, g.argv[4], isGlobal);
          979  +      g.argc = 3;
          980  +    }
          981  +    pSetting = setting_info(&nSetting);
          982  +    for(; nSetting>0; nSetting--, pSetting++ ){
          983  +      if( strncmp(pSetting->name,"email-",6)!=0 ) continue;
          984  +      print_setting(pSetting);
          985  +    }
          986  +  }else
          987  +  if( strncmp(zCmd, "subscribers", nCmd)==0 ){
          988  +    Stmt q;
          989  +    verify_all_options();
          990  +    if( g.argc!=3 && g.argc!=4 ) usage("subscribers [PATTERN]");
          991  +    if( g.argc==4 ){
          992  +      char *zPattern = g.argv[3];
          993  +      db_prepare(&q,
          994  +        "SELECT semail FROM subscriber"
          995  +        " WHERE semail LIKE '%%%q%%' OR suname LIKE '%%%q%%'"
          996  +        "  OR semail GLOB '*%q*' or suname GLOB '*%q*'"
          997  +        " ORDER BY semail",
          998  +        zPattern, zPattern, zPattern, zPattern);
          999  +    }else{
         1000  +      db_prepare(&q,
         1001  +        "SELECT semail FROM subscriber"
         1002  +        " ORDER BY semail");
         1003  +    }
         1004  +    while( db_step(&q)==SQLITE_ROW ){
         1005  +      fossil_print("%s\n", db_column_text(&q, 0));
         1006  +    }
         1007  +    db_finalize(&q);
         1008  +  }else
         1009  +  if( strncmp(zCmd, "unsubscribe", nCmd)==0 ){
         1010  +    verify_all_options();
         1011  +    if( g.argc!=4 ) usage("unsubscribe EMAIL");
         1012  +    db_multi_exec(
         1013  +      "DELETE FROM subscriber WHERE semail=%Q", g.argv[3]);
         1014  +  }else
         1015  +  {
         1016  +    usage("exec|inbound|reset|send|setting|subscribers|unsubscribe");
         1017  +  }
         1018  +}
         1019  +
         1020  +/*
         1021  +** Do error checking on a submitted subscription form.  Return TRUE
         1022  +** if the submission is valid.  Return false if any problems are seen.
         1023  +*/
         1024  +static int subscribe_error_check(
         1025  +  int *peErr,           /* Type of error */
         1026  +  char **pzErr,         /* Error message text */
         1027  +  int needCaptcha       /* True if captcha check needed */
         1028  +){
         1029  +  const char *zEAddr;
         1030  +  int i, j, n;
         1031  +  char c;
         1032  +
         1033  +  *peErr = 0;
         1034  +  *pzErr = 0;
         1035  +
         1036  +  /* Check the validity of the email address.
         1037  +  **
         1038  +  **  (1) Exactly one '@' character.
         1039  +  **  (2) No other characters besides [a-zA-Z0-9._-]
         1040  +  */
         1041  +  zEAddr = P("e");
         1042  +  if( zEAddr==0 ) return 0;
         1043  +  for(i=j=n=0; (c = zEAddr[i])!=0; i++){
         1044  +    if( c=='@' ){
         1045  +      n = i;
         1046  +      j++;
         1047  +      continue;
         1048  +    }
         1049  +    if( !fossil_isalnum(c) && c!='.' && c!='_' && c!='-' ){
         1050  +      *peErr = 1;
         1051  +      *pzErr = mprintf("illegal character in email address: 0x%x '%c'",
         1052  +                   c, c);
         1053  +      return 0;
         1054  +    }
         1055  +  }
         1056  +  if( j!=1 ){
         1057  +    *peErr = 1;
         1058  +    *pzErr = mprintf("email address should contain exactly one '@'");
         1059  +    return 0;
         1060  +  }
         1061  +  if( n<1 ){
         1062  +    *peErr = 1;
         1063  +    *pzErr = mprintf("name missing before '@' in email address");
         1064  +    return 0;
         1065  +  }
         1066  +  if( n>i-5 ){
         1067  +    *peErr = 1;
         1068  +    *pzErr = mprintf("email domain too short");
         1069  +     return 0;
         1070  +  }
         1071  +
         1072  +  /* Verify the captcha */
         1073  +  if( needCaptcha && !captcha_is_correct(1) ){
         1074  +    *peErr = 2;
         1075  +    *pzErr = mprintf("incorrect security code");
         1076  +    return 0;
         1077  +  }
         1078  +
         1079  +  /* Check to make sure the email address is available for reuse */
         1080  +  if( db_exists("SELECT 1 FROM subscriber WHERE semail=%Q", zEAddr) ){
         1081  +    *peErr = 1;
         1082  +    *pzErr = mprintf("this email address is used by someone else");
         1083  +    return 0;
         1084  +  }
         1085  +
         1086  +  /* If we reach this point, all is well */
         1087  +  return 1;
         1088  +}
         1089  +
         1090  +/*
         1091  +** Text of email message sent in order to confirm a subscription.
         1092  +*/
         1093  +static const char zConfirmMsg[] = 
         1094  +@ Someone has signed you up for email alerts on the Fossil repository
         1095  +@ at %s.
         1096  +@
         1097  +@ To confirm your subscription and begin receiving alerts, click on
         1098  +@ the following hyperlink:
         1099  +@
         1100  +@    %s/alerts/%s
         1101  +@
         1102  +@ Save the hyperlink above!  You can reuse this same hyperlink to
         1103  +@ unsubscribe or to change the kinds of alerts you receive.
         1104  +@
         1105  +@ If you do not want to subscribe, you can simply ignore this message.
         1106  +@ You will not be contacted again.
         1107  +@
         1108  +;
         1109  +
         1110  +/*
         1111  +** WEBPAGE: subscribe
         1112  +**
         1113  +** Allow users to subscribe to email notifications.
         1114  +**
         1115  +** This page is usually run by users who are not logged in.
         1116  +** A logged-in user can add email notifications on the /alerts page.
         1117  +** Access to this page by a logged in user (other than an
         1118  +** administrator) results in a redirect to the /alerts page.
         1119  +**
         1120  +** Administrators can visit this page in order to sign up other
         1121  +** users.
         1122  +**
         1123  +** The Email-Alerts permission ("7") is required to access this
         1124  +** page.  To allow anonymous passers-by to sign up for email
         1125  +** notification, set Email-Alerts on user "nobody" or "anonymous".
         1126  +*/
         1127  +void subscribe_page(void){
         1128  +  int needCaptcha;
         1129  +  unsigned int uSeed;
         1130  +  const char *zDecoded;
         1131  +  char *zCaptcha = 0;
         1132  +  char *zErr = 0;
         1133  +  int eErr = 0;
         1134  +
         1135  +  if( email_webpages_disabled() ) return;
         1136  +  login_check_credentials();
         1137  +  if( !g.perm.EmailAlert ){
         1138  +    login_needed(g.anon.EmailAlert);
         1139  +    return;
         1140  +  }
         1141  +  if( login_is_individual()
         1142  +   && db_exists("SELECT 1 FROM subscriber WHERE suname=%Q",g.zLogin)
         1143  +  ){
         1144  +    /* This person is already signed up for email alerts.  Jump
         1145  +    ** to the screen that lets them edit their alert preferences.
         1146  +    ** Except, administrators can create subscriptions for others so
         1147  +    ** do not jump for them.
         1148  +    */
         1149  +    if( g.perm.Admin ){
         1150  +      /* Admins get a link to admin their own account, but they
         1151  +      ** stay on this page so that they can create subscriptions
         1152  +      ** for other people. */
         1153  +      style_submenu_element("My Subscription","%R/alerts");
         1154  +    }else{
         1155  +      /* Everybody else jumps to the page to administer their own
         1156  +      ** account only. */
         1157  +      cgi_redirectf("%R/alerts");
         1158  +      return;
         1159  +    }
         1160  +  }
         1161  +  email_submenu_common();
         1162  +  needCaptcha = !login_is_individual();
         1163  +  if( P("submit")
         1164  +   && cgi_csrf_safe(1)
         1165  +   && subscribe_error_check(&eErr,&zErr,needCaptcha)
         1166  +  ){
         1167  +    /* A validated request for a new subscription has been received. */
         1168  +    char ssub[20];
         1169  +    const char *zEAddr = P("e");
         1170  +    sqlite3_int64 id;   /* New subscriber Id */
         1171  +    const char *zCode;  /* New subscriber code (in hex) */
         1172  +    int nsub = 0;
         1173  +    const char *suname = PT("suname");
         1174  +    if( suname==0 && needCaptcha==0 && !g.perm.Admin ) suname = g.zLogin;
         1175  +    if( suname && suname[0]==0 ) suname = 0;
         1176  +    if( PB("sa") ) ssub[nsub++] = 'a';
         1177  +    if( g.perm.Read && PB("sc") )    ssub[nsub++] = 'c';
         1178  +    if( g.perm.RdForum && PB("sf") ) ssub[nsub++] = 'f';
         1179  +    if( g.perm.RdTkt && PB("st") )   ssub[nsub++] = 't';
         1180  +    if( g.perm.RdWiki && PB("sw") )  ssub[nsub++] = 'w';
         1181  +    ssub[nsub] = 0;
         1182  +    db_multi_exec(
         1183  +      "INSERT INTO subscriber(semail,suname,"
         1184  +      "  sverified,sdonotcall,sdigest,ssub,sctime,mtime,smip)"
         1185  +      "VALUES(%Q,%Q,%d,0,%d,%Q,now(),now(),%Q)",
         1186  +      /* semail */    zEAddr,
         1187  +      /* suname */    suname,
         1188  +      /* sverified */ needCaptcha==0,
         1189  +      /* sdigest */   PB("di"),
         1190  +      /* ssub */      ssub,
         1191  +      /* smip */      g.zIpAddr
         1192  +    );
         1193  +    id = db_last_insert_rowid();
         1194  +    zCode = db_text(0,
         1195  +         "SELECT hex(subscriberCode) FROM subscriber WHERE subscriberId=%lld",
         1196  +         id);
         1197  +    if( !needCaptcha ){
         1198  +      /* The new subscription has been added on behalf of a logged-in user.
         1199  +      ** No verification is required.  Jump immediately to /alerts page.
         1200  +      */
         1201  +      cgi_redirectf("%R/alerts/%s", zCode);
         1202  +      return;
         1203  +    }else{
         1204  +      /* We need to send a verification email */
         1205  +      Blob hdr, body;
         1206  +      EmailSender *pSender = email_sender_new(0,0);
         1207  +      blob_init(&hdr,0,0);
         1208  +      blob_init(&body,0,0);
         1209  +      blob_appendf(&hdr, "To: <%s>\n", zEAddr);
         1210  +      blob_appendf(&hdr, "Subject: Subscription verification\n");
         1211  +      blob_appendf(&body, zConfirmMsg/*works-like:"%s%s%s"*/,
         1212  +                   g.zBaseURL, g.zBaseURL, zCode);
         1213  +      email_send(pSender, &hdr, &body);
         1214  +      style_header("Email Alert Verification");
         1215  +      if( pSender->zErr ){
         1216  +        @ <h1>Internal Error</h1>
         1217  +        @ <p>The following internal error was encountered while trying
         1218  +        @ to send the confirmation email:
         1219  +        @ <blockquote><pre>
         1220  +        @ %h(pSender->zErr)
         1221  +        @ </pre></blockquote>
         1222  +      }else{
         1223  +        @ <p>An email has been sent to "%h(zEAddr)". That email contains a
         1224  +        @ hyperlink that you must click on in order to activate your
         1225  +        @ subscription.</p>
         1226  +      }
         1227  +      email_sender_free(pSender);
         1228  +      style_footer();
         1229  +    }
         1230  +    return;
         1231  +  }
         1232  +  style_header("Signup For Email Alerts");
         1233  +  if( P("submit")==0 ){
         1234  +    /* If this is the first visit to this page (if this HTTP request did not
         1235  +    ** come from a prior Submit of the form) then default all of the
         1236  +    ** subscription options to "on" */
         1237  +    cgi_set_parameter_nocopy("sa","1",1);
         1238  +    if( g.perm.Read )    cgi_set_parameter_nocopy("sc","1",1);
         1239  +    if( g.perm.RdForum ) cgi_set_parameter_nocopy("sf","1",1);
         1240  +    if( g.perm.RdTkt )   cgi_set_parameter_nocopy("st","1",1);
         1241  +    if( g.perm.RdWiki )  cgi_set_parameter_nocopy("sw","1",1);
         1242  +  }
         1243  +  @ <p>To receive email notifications for changes to this
         1244  +  @ repository, fill out the form below and press "Submit" button.</p>
         1245  +  form_begin(0, "%R/subscribe");
         1246  +  @ <table class="subscribe">
         1247  +  @ <tr>
         1248  +  @  <td class="form_label">Email&nbsp;Address:</td>
         1249  +  @  <td><input type="text" name="e" value="%h(PD("e",""))" size="30"></td>
         1250  +  if( eErr==1 ){
         1251  +    @  <td><span class="loginError">&larr; %h(zErr)</span></td>
         1252  +  }
         1253  +  @ </tr>
         1254  +  if( needCaptcha ){
         1255  +    uSeed = captcha_seed();
         1256  +    zDecoded = captcha_decode(uSeed);
         1257  +    zCaptcha = captcha_render(zDecoded);
         1258  +    @ <tr>
         1259  +    @  <td class="form_label">Security Code:</td>
         1260  +    @  <td><input type="text" name="captcha" value="" size="30">
         1261  +    @  <input type="hidden" name="captchaseed" value="%u(uSeed)"></td>
         1262  +    if( eErr==2 ){
         1263  +      @  <td><span class="loginError">&larr; %h(zErr)</span></td>
         1264  +    }
         1265  +    @ </tr>
         1266  +  }
         1267  +  if( g.perm.Admin ){
         1268  +    @ <tr>
         1269  +    @  <td class="form_label">User:</td>
         1270  +    @  <td><input type="text" name="suname" value="%h(PD("suname",g.zLogin))" \
         1271  +    @  size="30"></td>
         1272  +    if( eErr==3 ){
         1273  +      @  <td><span class="loginError">&larr; %h(zErr)</span></td>
         1274  +    }
         1275  +    @ </tr>
         1276  +  }
         1277  +  @ <tr>
         1278  +  @  <td class="form_label">Options:</td>
         1279  +  @  <td><label><input type="checkbox" name="sa" %s(PCK("sa"))> \
         1280  +  @  Announcements</label><br>
         1281  +  if( g.perm.Read ){
         1282  +    @  <label><input type="checkbox" name="sc" %s(PCK("sc"))> \
         1283  +    @  Check-ins</label><br>
         1284  +  }
         1285  +  if( g.perm.RdForum ){
         1286  +    @  <label><input type="checkbox" name="sf" %s(PCK("sf"))> \
         1287  +    @  Forum Posts</label><br>
         1288  +  }
         1289  +  if( g.perm.RdTkt ){
         1290  +    @  <label><input type="checkbox" name="st" %s(PCK("st"))> \
         1291  +    @  Ticket changes</label><br>
         1292  +  }
         1293  +  if( g.perm.RdWiki ){
         1294  +    @  <label><input type="checkbox" name="sw" %s(PCK("sw"))> \
         1295  +    @  Wiki</label><br>
         1296  +  }
         1297  +  @  <label><input type="checkbox" name="di" %s(PCK("di"))> \
         1298  +  @  Daily digest only</label><br>
         1299  +  if( g.perm.Admin ){
         1300  +    @  <label><input type="checkbox" name="vi" %s(PCK("vi"))> \
         1301  +    @  Verified</label><br>
         1302  +    @  <label><input type="checkbox" name="dnc" %s(PCK("dnc"))> \
         1303  +    @  Do not call</label><br>
         1304  +  }
         1305  +  @ </td>
         1306  +  @ </tr>
         1307  +  @ <tr>
         1308  +  @  <td></td>
         1309  +  if( needCaptcha && !email_enabled() ){
         1310  +    @  <td><input type="submit" name="submit" value="Submit" disabled>
         1311  +    @  (Email current disabled)</td>
         1312  +  }else{
         1313  +    @  <td><input type="submit" name="submit" value="Submit"></td>
         1314  +  }
         1315  +  @ </tr>
         1316  +  @ </table>
         1317  +  if( needCaptcha ){
         1318  +    @ <div class="captcha"><table class="captcha"><tr><td><pre>
         1319  +    @ %h(zCaptcha)
         1320  +    @ </pre>
         1321  +    @ Enter the 8 characters above in the "Security Code" box
         1322  +    @ </td></tr></table></div>
         1323  +  }
         1324  +  @ </form>
         1325  +  fossil_free(zErr);
         1326  +  style_footer();
         1327  +}
         1328  +
         1329  +/*
         1330  +** Either shutdown or completely delete a subscription entry given
         1331  +** by the hex value zName.  Then paint a webpage that explains that
         1332  +** the entry has been removed.
         1333  +*/
         1334  +static void email_unsubscribe(const char *zName){
         1335  +  char *zEmail;
         1336  +  zEmail = db_text(0, "SELECT semail FROM subscriber"
         1337  +                      " WHERE subscriberCode=hextoblob(%Q)", zName);
         1338  +  if( zEmail==0 ){
         1339  +    style_header("Unsubscribe Fail");
         1340  +    @ <p>Unable to locate a subscriber with the requested key</p>
         1341  +  }else{
         1342  +    db_multi_exec(
         1343  +      "DELETE FROM subscriber WHERE subscriberCode=hextoblob(%Q)",
         1344  +      zName
         1345  +    );
         1346  +    style_header("Unsubscribed");
         1347  +    @ <p>The "%h(zEmail)" email address has been delisted.
         1348  +    @ All traces of that email address have been removed</p>
         1349  +  }
         1350  +  style_footer();
         1351  +  return;
         1352  +}
         1353  +
         1354  +/*
         1355  +** WEBPAGE: alerts
         1356  +**
         1357  +** Edit email alert and notification settings.
         1358  +**
         1359  +** The subscriber is identified in either of two ways:
         1360  +**
         1361  +**    (1)  The name= query parameter contains the subscriberCode.
         1362  +**         
         1363  +**    (2)  The user is logged into an account other than "nobody" or
         1364  +**         "anonymous".  In that case the notification settings
         1365  +**         associated with that account can be edited without needing
         1366  +**         to know the subscriber code.
         1367  +*/
         1368  +void alerts_page(void){
         1369  +  const char *zName = P("name");
         1370  +  Stmt q;
         1371  +  int sa, sc, sf, st, sw;
         1372  +  int sdigest, sdonotcall, sverified;
         1373  +  const char *ssub;
         1374  +  const char *semail;
         1375  +  const char *smip;
         1376  +  const char *suname;
         1377  +  const char *mtime;
         1378  +  const char *sctime;
         1379  +  int eErr = 0;
         1380  +  char *zErr = 0;
         1381  +
         1382  +  if( email_webpages_disabled() ) return;
         1383  +  login_check_credentials();
         1384  +  if( zName==0 && login_is_individual() ){
         1385  +    zName = db_text(0, "SELECT hex(subscriberCode) FROM subscriber"
         1386  +                       " WHERE suname=%Q", g.zLogin);
         1387  +  }
         1388  +  if( zName==0 || !validate16(zName, -1) ){
         1389  +    cgi_redirect("subscribe");
         1390  +    return;
         1391  +  }
         1392  +  email_submenu_common();
         1393  +  if( P("submit")!=0 && cgi_csrf_safe(1) ){
         1394  +    int sdonotcall = PB("sdonotcall");
         1395  +    int sdigest = PB("sdigest");
         1396  +    char ssub[10];
         1397  +    int nsub = 0;
         1398  +    if( PB("sa") )                   ssub[nsub++] = 'a';
         1399  +    if( g.perm.Read && PB("sc") )    ssub[nsub++] = 'c';
         1400  +    if( g.perm.RdForum && PB("sf") ) ssub[nsub++] = 'f';
         1401  +    if( g.perm.RdTkt && PB("st") )   ssub[nsub++] = 't';
         1402  +    if( g.perm.RdWiki && PB("sw") )  ssub[nsub++] = 'w';
         1403  +    ssub[nsub] = 0;
         1404  +    if( g.perm.Admin ){
         1405  +      const char *suname = PT("suname");
         1406  +      int sverified = PB("sverified");
         1407  +      if( suname && suname[0]==0 ) suname = 0;
         1408  +      db_multi_exec(
         1409  +        "UPDATE subscriber SET"
         1410  +        " sdonotcall=%d,"
         1411  +        " sdigest=%d,"
         1412  +        " ssub=%Q,"
         1413  +        " mtime=strftime('%%s','now'),"
         1414  +        " smip=%Q,"
         1415  +        " suname=%Q,"
         1416  +        " sverified=%d"
         1417  +        " WHERE subscriberCode=hextoblob(%Q)",
         1418  +        sdonotcall,
         1419  +        sdigest,
         1420  +        ssub,
         1421  +        g.zIpAddr,
         1422  +        suname,
         1423  +        sverified,
         1424  +        zName
         1425  +      );
         1426  +    }else{
         1427  +      db_multi_exec(
         1428  +        "UPDATE subscriber SET"
         1429  +        " sdonotcall=%d,"
         1430  +        " sdigest=%d,"
         1431  +        " ssub=%Q,"
         1432  +        " mtime=strftime('%%s','now'),"
         1433  +        " smip=%Q"
         1434  +        " WHERE subscriberCode=hextoblob(%Q)",
         1435  +        sdonotcall,
         1436  +        sdigest,
         1437  +        ssub,
         1438  +        g.zIpAddr,
         1439  +        zName
         1440  +      );
         1441  +    }
         1442  +  }
         1443  +  if( P("delete")!=0 && cgi_csrf_safe(1) ){
         1444  +    if( !PB("dodelete") ){
         1445  +      eErr = 9;
         1446  +      zErr = mprintf("Select this checkbox and press \"Unsubscribe\" to"
         1447  +                     " unsubscribe");
         1448  +    }else{
         1449  +      email_unsubscribe(zName);
         1450  +      return;
         1451  +    }
         1452  +  }
         1453  +  db_prepare(&q,
         1454  +    "SELECT"
         1455  +    "  semail,"                       /* 0 */
         1456  +    "  sverified,"                    /* 1 */
         1457  +    "  sdonotcall,"                   /* 2 */
         1458  +    "  sdigest,"                      /* 3 */
         1459  +    "  ssub,"                         /* 4 */
         1460  +    "  smip,"                         /* 5 */
         1461  +    "  suname,"                       /* 6 */
         1462  +    "  datetime(mtime,'unixepoch'),"  /* 7 */
         1463  +    "  datetime(sctime,'unixepoch')"  /* 8 */
         1464  +    " FROM subscriber WHERE subscriberCode=hextoblob(%Q)", zName);
         1465  +  if( db_step(&q)!=SQLITE_ROW ){
         1466  +    db_finalize(&q);
         1467  +    cgi_redirect("subscribe");
         1468  +    return;
         1469  +  }
         1470  +  style_header("Update Subscription");
         1471  +  semail = db_column_text(&q, 0);
         1472  +  sverified = db_column_int(&q, 1);
         1473  +  sdonotcall = db_column_int(&q, 2);
         1474  +  sdigest = db_column_int(&q, 3);
         1475  +  ssub = db_column_text(&q, 4);
         1476  +  sa = strchr(ssub,'a')!=0;
         1477  +  sc = strchr(ssub,'c')!=0;
         1478  +  sf = strchr(ssub,'f')!=0;
         1479  +  st = strchr(ssub,'t')!=0;
         1480  +  sw = strchr(ssub,'w')!=0;
         1481  +  smip = db_column_text(&q, 5);
         1482  +  suname = db_column_text(&q, 6);
         1483  +  mtime = db_column_text(&q, 7);
         1484  +  sctime = db_column_text(&q, 8);
         1485  +  if( !g.perm.Admin && !sverified ){
         1486  +    db_multi_exec(
         1487  +      "UPDATE subscriber SET sverified=1 WHERE subscriberCode=hextoblob(%Q)",
         1488  +      zName);
         1489  +    @ <h1>Your email alert subscription has been verified!</h1>
         1490  +    @ <p>Use the form below to update your subscription information.</p>
         1491  +    @ <p>Hint:  Bookmark this page so that you can more easily update
         1492  +    @ your subscription information in the future</p>
         1493  +  }else{
         1494  +    @ <p>Make changes to the email subscription shown below and
         1495  +    @ press "Submit".</p>
         1496  +  }
         1497  +  form_begin(0, "%R/alerts");
         1498  +  @ <input type="hidden" name="name" value="%h(zName)">
         1499  +  @ <table class="subscribe">
         1500  +  @ <tr>
         1501  +  @  <td class="form_label">Email&nbsp;Address:</td>
         1502  +  @  <td>%h(semail)</td>
         1503  +  @ </tr>
         1504  +  if( g.perm.Admin ){
         1505  +    @ <tr>
         1506  +    @  <td class='form_label'>Created:</td>
         1507  +    @  <td>%h(sctime)</td>
         1508  +    @ </tr>
         1509  +    @ <tr>
         1510  +    @  <td class='form_label'>Last Modified:</td>
         1511  +    @  <td>%h(mtime)</td>
         1512  +    @ </tr>
         1513  +    @ <tr>
         1514  +    @  <td class='form_label'>IP Address:</td>
         1515  +    @  <td>%h(smip)</td>
         1516  +    @ </tr>
         1517  +    @ <tr>
         1518  +    @  <td class="form_label">User:</td>
         1519  +    @  <td><input type="text" name="suname" value="%h(suname?suname:"")" \
         1520  +    @  size="30"></td>
         1521  +    @ </tr>
         1522  +  }
         1523  +  @ <tr>
         1524  +  @  <td class="form_label">Options:</td>
         1525  +  @  <td><label><input type="checkbox" name="sa" %s(sa?"checked":"")>\
         1526  +  @  Announcements</label><br>
         1527  +  if( g.perm.Read ){
         1528  +    @  <label><input type="checkbox" name="sc" %s(sc?"checked":"")>\
         1529  +    @  Check-ins</label><br>
         1530  +  }
         1531  +  if( g.perm.RdForum ){
         1532  +    @  <label><input type="checkbox" name="sf" %s(sf?"checked":"")>\
         1533  +    @  Forum Posts</label><br>
         1534  +  }
         1535  +  if( g.perm.RdTkt ){
         1536  +    @  <label><input type="checkbox" name="st" %s(st?"checked":"")>\
         1537  +    @  Ticket changes</label><br>
         1538  +  }
         1539  +  if( g.perm.RdWiki ){
         1540  +    @  <label><input type="checkbox" name="sw" %s(sw?"checked":"")>\
         1541  +    @  Wiki</label><br>
         1542  +  }
         1543  +  @  <label><input type="checkbox" name="sdigest" %s(sdigest?"checked":"")>\
         1544  +  @  Daily digest only</label><br>
         1545  +  if( g.perm.Admin ){
         1546  +    @  <label><input type="checkbox" name="sdonotcall" \
         1547  +    @  %s(sdonotcall?"checked":"")> Do not call</label><br>
         1548  +    @  <label><input type="checkbox" name="sverified" \
         1549  +    @  %s(sverified?"checked":"")>\
         1550  +    @  Verified</label><br>
         1551  +  }
         1552  +  @  <label><input type="checkbox" name="dodelete">
         1553  +  @  Unsubscribe</label> \
         1554  +  if( eErr==9 ){
         1555  +    @ <span class="loginError">&larr; %h(zErr)</span>\
         1556  +  }
         1557  +  @ <br>
         1558  +  @ </td></tr>
         1559  +  @ <tr>
         1560  +  @  <td></td>
         1561  +  @  <td><input type="submit" name="submit" value="Submit">
         1562  +  @  <input type="submit" name="delete" value="Unsubscribe">
         1563  +  @ </tr>
         1564  +  @ </table>
         1565  +  @ </form>
         1566  +  fossil_free(zErr);
         1567  +  db_finalize(&q);
         1568  +  style_footer();
         1569  +}
         1570  +
         1571  +/* This is the message that gets sent to describe how to change
         1572  +** or modify a subscription
         1573  +*/
         1574  +static const char zUnsubMsg[] = 
         1575  +@ To changes your subscription settings at %s visit this link:
         1576  +@
         1577  +@    %s/alerts/%s
         1578  +@
         1579  +@ To completely unsubscribe from %s, visit the following link:
         1580  +@
         1581  +@    %s/unsubscribe/%s
         1582  +;
         1583  +
         1584  +/*
         1585  +** WEBPAGE: unsubscribe
         1586  +**
         1587  +** Users visit this page to be delisted from email alerts.
         1588  +**
         1589  +** If a valid subscriber code is supplied in the name= query parameter,
         1590  +** then that subscriber is delisted.
         1591  +**
         1592  +** Otherwise, If the users is logged in, then they are redirected
         1593  +** to the /alerts page where they have an unsubscribe button.
         1594  +**
         1595  +** Non-logged-in users with no name= query parameter are invited to enter
         1596  +** an email address to which will be sent the unsubscribe link that
         1597  +** contains the correct subscriber code.
         1598  +*/
         1599  +void unsubscribe_page(void){
         1600  +  const char *zName = P("name");
         1601  +  char *zErr = 0;
         1602  +  int eErr = 0;
         1603  +  unsigned int uSeed;
         1604  +  const char *zDecoded;
         1605  +  char *zCaptcha = 0;
         1606  +  int dx;
         1607  +  int bSubmit;
         1608  +  const char *zEAddr;
         1609  +  char *zCode = 0;
         1610  +
         1611  +  /* If a valid subscriber code is supplied, then unsubscribe immediately.
         1612  +  */
         1613  +  if( zName 
         1614  +   && db_exists("SELECT 1 FROM subscriber WHERE subscriberCode=hextoblob(%Q)",
         1615  +                zName)
         1616  +  ){
         1617  +    email_unsubscribe(zName);
         1618  +    return;
         1619  +  }
         1620  +
         1621  +  /* Logged in users are redirected to the /alerts page */
         1622  +  login_check_credentials();
         1623  +  if( login_is_individual() ){
         1624  +    cgi_redirectf("%R/alerts");
         1625  +    return;
         1626  +  }
         1627  +
         1628  +  zEAddr = PD("e","");
         1629  +  dx = atoi(PD("dx","0"));
         1630  +  bSubmit = P("submit")!=0 && P("e")!=0 && cgi_csrf_safe(1);
         1631  +  if( bSubmit ){
         1632  +    if( !captcha_is_correct(1) ){
         1633  +      eErr = 2;
         1634  +      zErr = mprintf("enter the security code shown below");
         1635  +      bSubmit = 0;
         1636  +    }
         1637  +  }
         1638  +  if( bSubmit ){
         1639  +    zCode = db_text(0,"SELECT hex(subscriberCode) FROM subscriber"
         1640  +                      " WHERE semail=%Q", zEAddr);
         1641  +    if( zCode==0 ){
         1642  +      eErr = 1;
         1643  +      zErr = mprintf("not a valid email address");
         1644  +      bSubmit = 0;
         1645  +    }
         1646  +  }
         1647  +  if( bSubmit ){
         1648  +    /* If we get this far, it means that a valid unsubscribe request has
         1649  +    ** been submitted.  Send the appropriate email. */
         1650  +    Blob hdr, body;
         1651  +    EmailSender *pSender = email_sender_new(0,0);
         1652  +    blob_init(&hdr,0,0);
         1653  +    blob_init(&body,0,0);
         1654  +    blob_appendf(&hdr, "To: <%s>\r\n", zEAddr);
         1655  +    blob_appendf(&hdr, "Subject: Unsubscribe Instructions\r\n");
         1656  +    blob_appendf(&body, zUnsubMsg/*works-like:"%s%s%s%s%s%s"*/,
         1657  +                  g.zBaseURL, g.zBaseURL, zCode, g.zBaseURL, g.zBaseURL, zCode);
         1658  +    email_send(pSender, &hdr, &body);
         1659  +    style_header("Unsubscribe Instructions Sent");
         1660  +    if( pSender->zErr ){
         1661  +      @ <h1>Internal Error</h1>
         1662  +      @ <p>The following error was encountered while trying to send an
         1663  +      @ email to %h(zEAddr):
         1664  +      @ <blockquote><pre>
         1665  +      @ %h(pSender->zErr)
         1666  +      @ </pre></blockquote>
         1667  +    }else{
         1668  +      @ <p>An email has been sent to "%h(zEAddr)" that explains how to
         1669  +      @ unsubscribe and/or modify your subscription settings</p>
         1670  +    }
         1671  +    email_sender_free(pSender);
         1672  +    style_footer();
         1673  +    return;
         1674  +  }  
         1675  +
         1676  +  /* Non-logged-in users have to enter an email address to which is
         1677  +  ** sent a message containing the unsubscribe link.
         1678  +  */
         1679  +  style_header("Unsubscribe Request");
         1680  +  @ <p>Fill out the form below to request an email message that will
         1681  +  @ explain how to unsubscribe and/or change your subscription settings.</p>
         1682  +  @
         1683  +  form_begin(0, "%R/unsubscribe");
         1684  +  @ <table class="subscribe">
         1685  +  @ <tr>
         1686  +  @  <td class="form_label">Email&nbsp;Address:</td>
         1687  +  @  <td><input type="text" name="e" value="%h(zEAddr)" size="30"></td>
         1688  +  if( eErr==1 ){
         1689  +    @  <td><span class="loginError">&larr; %h(zErr)</span></td>
         1690  +  }
         1691  +  @ </tr>
         1692  +  uSeed = captcha_seed();
         1693  +  zDecoded = captcha_decode(uSeed);
         1694  +  zCaptcha = captcha_render(zDecoded);
         1695  +  @ <tr>
         1696  +  @  <td class="form_label">Security Code:</td>
         1697  +  @  <td><input type="text" name="captcha" value="" size="30">
         1698  +  @  <input type="hidden" name="captchaseed" value="%u(uSeed)"></td>
         1699  +  if( eErr==2 ){
         1700  +    @  <td><span class="loginError">&larr; %h(zErr)</span></td>
         1701  +  }
         1702  +  @ </tr>
         1703  +  @ <tr>
         1704  +  @  <td class="form_label">Options:</td>
         1705  +  @  <td><label><input type="radio" name="dx" value="0" %s(dx?"":"checked")>\
         1706  +  @  Modify subscription</label><br>
         1707  +  @  <label><input type="radio" name="dx" value="1" %s(dx?"checked":"")>\
         1708  +  @  Completely unsubscribe</label><br>
         1709  +  @ <tr>
         1710  +  @  <td></td>
         1711  +  @  <td><input type="submit" name="submit" value="Submit"></td>
         1712  +  @ </tr>
         1713  +  @ </table>
         1714  +  @ <div class="captcha"><table class="captcha"><tr><td><pre>
         1715  +  @ %h(zCaptcha)
         1716  +  @ </pre>
         1717  +  @ Enter the 8 characters above in the "Security Code" box
         1718  +  @ </td></tr></table></div>
         1719  +  @ </form>
         1720  +  fossil_free(zErr);
         1721  +  style_footer();
         1722  +}
         1723  +
         1724  +/*
         1725  +** WEBPAGE: subscribers
         1726  +**
         1727  +** This page, accessible to administrators only,
         1728  +** shows a list of email notification email addresses.
         1729  +** Clicking on an email takes one to the /alerts page
         1730  +** for that email where the delivery settings can be
         1731  +** modified.
         1732  +*/
         1733  +void subscriber_list_page(void){
         1734  +  Blob sql;
         1735  +  Stmt q;
         1736  +  double rNow;
         1737  +  if( email_webpages_disabled() ) return;
         1738  +  login_check_credentials();
         1739  +  if( !g.perm.Admin ){
         1740  +    login_needed(0);
         1741  +    return;
         1742  +  }
         1743  +  email_submenu_common();
         1744  +  style_header("Subscriber List");
         1745  +  blob_init(&sql, 0, 0);
         1746  +  blob_append_sql(&sql,
         1747  +    "SELECT hex(subscriberCode),"          /* 0 */
         1748  +    "       semail,"                       /* 1 */
         1749  +    "       ssub,"                         /* 2 */
         1750  +    "       suname,"                       /* 3 */
         1751  +    "       sverified,"                    /* 4 */
         1752  +    "       sdigest,"                      /* 5 */
         1753  +    "       date(sctime,'unixepoch'),"     /* 6 */
         1754  +    "       julianday(mtime,'unixepoch')"  /* 7 */
         1755  +    " FROM subscriber"
         1756  +  );
         1757  +  if( P("only")!=0 ){
         1758  +    blob_append_sql(&sql, " WHERE ssub LIKE '%%%q%%'", P("only"));
         1759  +    style_submenu_element("Show All","%R/subscribers");
         1760  +  }
         1761  +  db_prepare_blob(&q, &sql);
         1762  +  rNow = db_double(0.0,"SELECT julianday('now')");
         1763  +  @ <table border="1">
         1764  +  @ <tr>
         1765  +  @ <th>Email
         1766  +  @ <th>Events
         1767  +  @ <th>Digest-Only?
         1768  +  @ <th>User
         1769  +  @ <th>Verified?
         1770  +  @ <th>Last change
         1771  +  @ <th>Created
         1772  +  @ </tr>
         1773  +  while( db_step(&q)==SQLITE_ROW ){
         1774  +    double rAge = rNow - db_column_double(&q, 7);
         1775  +    @ <tr>
         1776  +    @ <td><a href='%R/alerts/%s(db_column_text(&q,0))'>\
         1777  +    @ %h(db_column_text(&q,1))</a></td>
         1778  +    @ <td>%h(db_column_text(&q,2))</td>
         1779  +    @ <td>%s(db_column_int(&q,5)?"digest":"")</td>
         1780  +    @ <td>%h(db_column_text(&q,3))</td>
         1781  +    @ <td>%s(db_column_int(&q,4)?"yes":"pending")</td>
         1782  +    @ <td>%z(human_readable_age(rAge))</td>
         1783  +    @ <td>%h(db_column_text(&q,6))</td>
         1784  +    @ </tr>
         1785  +  }
         1786  +  @ </table>
         1787  +  db_finalize(&q);
         1788  +  style_footer();
         1789  +}
         1790  +
         1791  +#if LOCAL_INTERFACE
         1792  +/*
         1793  +** A single event that might appear in an alert is recorded as an
         1794  +** instance of the following object.
         1795  +*/
         1796  +struct EmailEvent {
         1797  +  int type;          /* 'c', 't', 'w', etc. */
         1798  +  Blob txt;          /* Text description to appear in an alert */
         1799  +  EmailEvent *pNext; /* Next in chronological order */
         1800  +};
         1801  +#endif
         1802  +
         1803  +/*
         1804  +** Free a linked list of EmailEvent objects
         1805  +*/
         1806  +void email_free_eventlist(EmailEvent *p){
         1807  +  while( p ){
         1808  +    EmailEvent *pNext = p->pNext;
         1809  +    blob_reset(&p->txt);
         1810  +    fossil_free(p);
         1811  +    p = pNext;
         1812  +  }
         1813  +}
         1814  +
         1815  +/*
         1816  +** Compute and return a linked list of EmailEvent objects
         1817  +** corresponding to the current content of the temp.wantalert
         1818  +** table which should be defined as follows:
         1819  +**
         1820  +**     CREATE TEMP TABLE wantalert(eventId TEXT);
         1821  +*/
         1822  +EmailEvent *email_compute_event_text(int *pnEvent){
         1823  +  Stmt q;
         1824  +  EmailEvent *p;
         1825  +  EmailEvent anchor;
         1826  +  EmailEvent *pLast;
         1827  +  const char *zUrl = db_get("email-url","http://localhost:8080");
         1828  +
         1829  +  db_prepare(&q,
         1830  +    "SELECT"
         1831  +    " blob.uuid,"  /* 0 */
         1832  +    " datetime(event.mtime),"  /* 1 */
         1833  +    " coalesce(ecomment,comment)"
         1834  +    "  || ' (user: ' || coalesce(euser,user,'?')"
         1835  +    "  || (SELECT case when length(x)>0 then ' tags: ' || x else '' end"
         1836  +    "      FROM (SELECT group_concat(substr(tagname,5), ', ') AS x"
         1837  +    "              FROM tag, tagxref"
         1838  +    "             WHERE tagname GLOB 'sym-*' AND tag.tagid=tagxref.tagid"
         1839  +    "               AND tagxref.rid=blob.rid AND tagxref.tagtype>0))"
         1840  +    "  || ')' as comment,"  /* 2 */
         1841  +    " tagxref.value AS branch,"  /* 3 */
         1842  +    " wantalert.eventId"     /* 4 */
         1843  +    " FROM temp.wantalert JOIN tag CROSS JOIN event CROSS JOIN blob"
         1844  +    "  LEFT JOIN tagxref ON tagxref.tagid=tag.tagid"
         1845  +    "                       AND tagxref.tagtype>0"
         1846  +    "                       AND tagxref.rid=blob.rid"
         1847  +    " WHERE blob.rid=event.objid"
         1848  +    "   AND tag.tagname='branch'"
         1849  +    "   AND event.objid=substr(wantalert.eventId,2)+0"
         1850  +    " ORDER BY event.mtime"
         1851  +  );
         1852  +  memset(&anchor, 0, sizeof(anchor));
         1853  +  pLast = &anchor;
         1854  +  *pnEvent = 0;
         1855  +  while( db_step(&q)==SQLITE_ROW ){
         1856  +    const char *zType = "";
         1857  +    p = fossil_malloc( sizeof(EmailEvent) );
         1858  +    pLast->pNext = p;
         1859  +    pLast = p;
         1860  +    p->type = db_column_text(&q, 4)[0];
         1861  +    p->pNext = 0;
         1862  +    switch( p->type ){
         1863  +      case 'c':  zType = "Check-In";        break;
         1864  +      case 't':  zType = "Wiki Edit";       break;
         1865  +      case 'w':  zType = "Ticket Change";   break;
         1866  +    }
         1867  +    blob_init(&p->txt, 0, 0);
         1868  +    blob_appendf(&p->txt,"== %s %s ==\n%s\n%s/info/%.20s\n",
         1869  +      db_column_text(&q,1),
         1870  +      zType,
         1871  +      db_column_text(&q,2),
         1872  +      zUrl,
         1873  +      db_column_text(&q,0)
         1874  +    );
         1875  +    (*pnEvent)++;
         1876  +  }
         1877  +  db_finalize(&q);
         1878  +  return anchor.pNext;
         1879  +}
         1880  +
         1881  +/*
         1882  +** Put a header on an alert email
         1883  +*/
         1884  +void email_header(Blob *pOut){
         1885  +  blob_appendf(pOut,
         1886  +    "This is an automated email reporting changes "
         1887  +    "on Fossil repository %s (%s/timeline)\n",
         1888  +    db_get("email-subname","(unknown)"),
         1889  +    db_get("email-url","http://localhost:8080"));
         1890  +}
         1891  +
         1892  +/*
         1893  +** Append the "unsubscribe" notification and other footer text to
         1894  +** the end of an email alert being assemblied in pOut.
         1895  +*/
         1896  +void email_footer(Blob *pOut){
         1897  +  blob_appendf(pOut, "\n-- \nTo unsubscribe: %s/unsubscribe\n",
         1898  +     db_get("email-url","http://localhost:8080"));
         1899  +}
         1900  +
         1901  +/*
         1902  +** COMMAND:  test-alert
         1903  +**
         1904  +** Usage: %fossil test-alert EVENTID ...
         1905  +**
         1906  +** Generate the text of an email alert for all of the EVENTIDs
         1907  +** listed on the command-line.  Or if no events are listed on the
         1908  +** command line, generate text for all events named in the
         1909  +** pending_alert table.
         1910  +**
         1911  +** This command is intended for testing and debugging the logic
         1912  +** that generates email alert text.
         1913  +*/
         1914  +void test_alert_cmd(void){
         1915  +  Blob out;
         1916  +  int nEvent;
         1917  +  EmailEvent *pEvent, *p;
         1918  +
         1919  +  db_find_and_open_repository(0, 0);
         1920  +  verify_all_options();
         1921  +  db_begin_transaction();
         1922  +  email_schema(0);
         1923  +  db_multi_exec("CREATE TEMP TABLE wantalert(eventid TEXT)");
         1924  +  if( g.argc==2 ){
         1925  +    db_multi_exec("INSERT INTO wantalert SELECT eventid FROM pending_alert");
         1926  +  }else{
         1927  +    int i;
         1928  +    for(i=2; i<g.argc; i++){
         1929  +      db_multi_exec("INSERT INTO wantalert VALUES(%Q)", g.argv[i]);
         1930  +    }
         1931  +  }
         1932  +  blob_init(&out, 0, 0);
         1933  +  email_header(&out);
         1934  +  pEvent = email_compute_event_text(&nEvent);
         1935  +  for(p=pEvent; p; p=p->pNext){
         1936  +    blob_append(&out, "\n", 1);
         1937  +    blob_append(&out, blob_buffer(&p->txt), blob_size(&p->txt));
         1938  +  }
         1939  +  email_free_eventlist(pEvent);
         1940  +  email_footer(&out);
         1941  +  fossil_print("%s", blob_str(&out));
         1942  +  blob_reset(&out);
         1943  +  db_end_transaction(0);
         1944  +}
         1945  +
         1946  +/*
         1947  +** COMMAND:  test-add-alerts
         1948  +**
         1949  +** Usage: %fossil test-add-alerts EVENTID ...
         1950  +**
         1951  +** Add one or more events to the pending_alert queue.  Use this
         1952  +** command during testing to force email notifications for specific
         1953  +** events.
         1954  +**
         1955  +** EVENTIDs are text.  The first character is 'c', 'w', or 't'
         1956  +** for check-in, wiki, or ticket.  The remaining text is a
         1957  +** integer that references the EVENT.OBJID value for the event.
         1958  +** Run /timeline?showid to see these OBJID values.
         1959  +*/
         1960  +void test_add_alert_cmd(void){
         1961  +  int i;
         1962  +  db_find_and_open_repository(0, 0);
         1963  +  verify_all_options();
         1964  +  db_begin_transaction();
         1965  +  email_schema(0);
         1966  +  for(i=2; i<g.argc; i++){
         1967  +    db_multi_exec("REPLACE INTO pending_alert(eventId) VALUES(%Q)", g.argv[i]);
         1968  +  }
         1969  +  db_end_transaction(0);
         1970  +}
         1971  +
         1972  +#if INTERFACE
         1973  +/*
         1974  +** Flags for email_send_alerts()
         1975  +*/
         1976  +#define SENDALERT_DIGEST      0x0001    /* Send a digest */
         1977  +#define SENDALERT_PRESERVE    0x0002    /* Do not mark the task as done */
         1978  +#define SENDALERT_STDOUT      0x0004    /* Print emails instead of sending */
         1979  +
         1980  +#endif /* INTERFACE */
         1981  +
         1982  +/*
         1983  +** Send alert emails to all subscribers.
         1984  +*/
         1985  +void email_send_alerts(u32 flags){
         1986  +  EmailEvent *pEvents, *p;
         1987  +  int nEvent = 0;
         1988  +  Stmt q;
         1989  +  const char *zDigest = "false";
         1990  +  Blob hdr, body;
         1991  +  const char *zUrl;
         1992  +  const char *zRepoName;
         1993  +  const char *zFrom;
         1994  +  const char *zDest = (flags & SENDALERT_STDOUT) ? "stdout" : 0;
         1995  +  EmailSender *pSender = 0;
         1996  +
         1997  +  if( g.fSqlTrace ) fossil_trace("-- BEGIN email_send_alerts(%u)\n", flags);
         1998  +  db_begin_transaction();
         1999  +  if( !email_enabled() ) goto send_alerts_done;
         2000  +  zUrl = db_get("email-url",0);
         2001  +  if( zUrl==0 ) goto send_alerts_done;
         2002  +  zRepoName = db_get("email-subname",0);
         2003  +  if( zRepoName==0 ) goto send_alerts_done;
         2004  +  zFrom = db_get("email-self",0);
         2005  +  if( zFrom==0 ) goto send_alerts_done;
         2006  +  pSender = email_sender_new(zDest, 0);
         2007  +  db_multi_exec(
         2008  +    "DROP TABLE IF EXISTS temp.wantalert;"
         2009  +    "CREATE TEMP TABLE wantalert(eventId TEXT);"
         2010  +  );
         2011  +  if( flags & SENDALERT_DIGEST ){
         2012  +    db_multi_exec(
         2013  +      "INSERT INTO wantalert SELECT eventid FROM pending_alert"
         2014  +      "  WHERE sentDigest IS FALSE"
         2015  +    );
         2016  +    zDigest = "true";
         2017  +  }else{
         2018  +    db_multi_exec(
         2019  +      "INSERT INTO wantalert SELECT eventid FROM pending_alert"
         2020  +      "  WHERE sentSep IS FALSE"
         2021  +    );
         2022  +  }
         2023  +  pEvents = email_compute_event_text(&nEvent);
         2024  +  if( nEvent==0 ) goto send_alerts_done;
         2025  +  blob_init(&hdr, 0, 0);
         2026  +  blob_init(&body, 0, 0);
         2027  +  db_prepare(&q,
         2028  +     "SELECT"
         2029  +     " hex(subscriberCode),"  /* 0 */
         2030  +     " semail,"               /* 1 */
         2031  +     " ssub"                  /* 2 */
         2032  +     " FROM subscriber"
         2033  +     " WHERE sverified AND NOT sdonotcall"
         2034  +     "  AND sdigest IS %s",
         2035  +     zDigest/*safe-for-%s*/
         2036  +  );
         2037  +  while( db_step(&q)==SQLITE_ROW ){
         2038  +    const char *zCode = db_column_text(&q, 0);
         2039  +    const char *zSub = db_column_text(&q, 2);
         2040  +    const char *zEmail = db_column_text(&q, 1);
         2041  +    int nHit = 0;
         2042  +    for(p=pEvents; p; p=p->pNext){
         2043  +      if( strchr(zSub,p->type)==0 ) continue;
         2044  +      if( nHit==0 ){
         2045  +        blob_appendf(&hdr,"To: <%s>\r\n", zEmail);
         2046  +        blob_appendf(&hdr,"Subject: %s activity alert\r\n", zRepoName);
         2047  +        blob_appendf(&body,
         2048  +          "This is an automated email sent by the Fossil repository "
         2049  +          "at %s to report changes.\n",
         2050  +          zUrl
         2051  +        );
         2052  +      }
         2053  +      nHit++;
         2054  +      blob_append(&body, "\n", 1);
         2055  +      blob_append(&body, blob_buffer(&p->txt), blob_size(&p->txt));
         2056  +    }
         2057  +    if( nHit==0 ) continue;
         2058  +    blob_appendf(&body,"\n-- \nSubscription info: %s/alerts/%s\n",
         2059  +         zUrl, zCode);
         2060  +    email_send(pSender,&hdr,&body);
         2061  +    blob_truncate(&hdr, 0);
         2062  +    blob_truncate(&body, 0);
         2063  +  }
         2064  +  blob_reset(&hdr);
         2065  +  blob_reset(&body);
         2066  +  db_finalize(&q);
         2067  +  email_free_eventlist(pEvents);
         2068  +  if( (flags & SENDALERT_PRESERVE)==0 ){
         2069  +    if( flags & SENDALERT_DIGEST ){
         2070  +      db_multi_exec("UPDATE pending_alert SET sentDigest=true");
         2071  +    }else{
         2072  +      db_multi_exec("UPDATE pending_alert SET sentSep=true");
         2073  +    }
         2074  +    db_multi_exec("DELETE FROM pending_alert WHERE sentDigest AND sentSep");
         2075  +  }
         2076  +send_alerts_done:
         2077  +  email_sender_free(pSender);
         2078  +  if( g.fSqlTrace ) fossil_trace("-- END email_send_alerts(%u)\n", flags);
         2079  +  db_end_transaction(0);
         2080  +}
         2081  +
         2082  +/*
         2083  +** Check to see if any email notifications need to occur, and then
         2084  +** do them.
         2085  +**
         2086  +** This routine is called after certain webpages have been run and
         2087  +** have already responded.
         2088  +*/
         2089  +void email_auto_exec(void){
         2090  +  int iJulianDay;
         2091  +  if( g.db==0 ) return;
         2092  +  if( db_transaction_nesting_depth()!=0 ){
         2093  +    fossil_warning("Called email_auto_exec() from within transaction "
         2094  +                   "started at %z", db_transaction_start_point());
         2095  +    return;
         2096  +  }
         2097  +  db_begin_transaction();
         2098  +  if( !email_tables_exist() ) goto autoexec_done;
         2099  +  if( !db_get_boolean("email-autoexec",0) ) goto autoexec_done;
         2100  +  email_send_alerts(0);
         2101  +  iJulianDay = db_int(0, "SELECT julianday('now')");
         2102  +  if( iJulianDay>db_get_int("email-last-digest",0) ){
         2103  +    if( db_transaction_nesting_depth()!=1 ){
         2104  +      fossil_warning("Transaction nesting error prior to digest processing");
         2105  +    }else{
         2106  +      db_set_int("email-last-digest",iJulianDay,0);
         2107  +      email_send_alerts(SENDALERT_DIGEST);
         2108  +    }
         2109  +  }
         2110  +
         2111  +autoexec_done:
         2112  +  db_end_transaction(0);
         2113  +}
         2114  +
         2115  +/*
         2116  +** WEBPAGE: contact_admin
         2117  +**
         2118  +** A web-form to send an email message to the repository administrator,
         2119  +** or (with appropriate permissions) to anybody.
         2120  +*/
         2121  +void contact_admin_page(void){
         2122  +  const char *zAdminEmail = db_get("email-admin",0);
         2123  +  unsigned int uSeed;
         2124  +  const char *zDecoded;
         2125  +  char *zCaptcha = 0;
         2126  +
         2127  +  login_check_credentials();
         2128  +  if( zAdminEmail==0 || zAdminEmail[0]==0 ){
         2129  +    style_header("Outbound Email Disabled");
         2130  +    @ <p>Outbound email is disabled on this repository
         2131  +    style_footer();
         2132  +    return;
         2133  +  }
         2134  +  if( P("submit")!=0 
         2135  +   && P("subject")!=0
         2136  +   && P("msg")!=0
         2137  +   && P("from")!=0
         2138  +   && cgi_csrf_safe(1)
         2139  +   && captcha_is_correct(0)
         2140  +  ){
         2141  +    Blob hdr, body;
         2142  +    EmailSender *pSender = email_sender_new(0,0);
         2143  +    blob_init(&hdr, 0, 0);
         2144  +    blob_appendf(&hdr, "To: <%s>\r\nSubject: %s administrator message\r\n",
         2145  +                 zAdminEmail, db_get("email-subname","Fossil Repo"));
         2146  +    blob_init(&body, 0, 0);
         2147  +    blob_appendf(&body, "Message from [%s]\n", PT("from")/*safe-for-%s*/);
         2148  +    blob_appendf(&body, "Subject: [%s]\n\n", PT("subject")/*safe-for-%s*/);
         2149  +    blob_appendf(&body, "%s", PT("msg")/*safe-for-%s*/);
         2150  +    email_send(pSender, &hdr, &body);
         2151  +    style_header("Message Sent");
         2152  +    if( pSender->zErr ){
         2153  +      @ <h1>Internal Error</h1>
         2154  +      @ <p>The following error was reported by the system:
         2155  +      @ <blockquote><pre>
         2156  +      @ %h(pSender->zErr)
         2157  +      @ </pre></blockquote>
         2158  +    }else{
         2159  +      @ <p>Your message has been sent to the repository administrator.
         2160  +      @ Thank you for your input.</p>
         2161  +    }
         2162  +    email_sender_free(pSender);
         2163  +    style_footer();
         2164  +    return;
         2165  +  }
         2166  +  if( captcha_needed() ){
         2167  +    uSeed = captcha_seed();
         2168  +    zDecoded = captcha_decode(uSeed);
         2169  +    zCaptcha = captcha_render(zDecoded);
         2170  +  }
         2171  +  style_header("Message To Administrator");
         2172  +  form_begin(0, "%R/contact_admin");
         2173  +  @ <p>Enter a message to the repository administrator below:</p>
         2174  +  @ <table class="subscribe">
         2175  +  if( zCaptcha ){
         2176  +    @ <tr>
         2177  +    @  <td class="form_label">Security&nbsp;Code:</td>
         2178  +    @  <td><input type="text" name="captcha" value="" size="10">
         2179  +    @  <input type="hidden" name="captchaseed" value="%u(uSeed)"></td>
         2180  +    @ </tr>
         2181  +  }
         2182  +  @ <tr>
         2183  +  @  <td class="form_label">Your&nbsp;Email&nbsp;Address:</td>
         2184  +  @  <td><input type="text" name="from" value="%h(PT("from"))" size="30"></td>
         2185  +  @ </tr>
         2186  +  @ <tr>
         2187  +  @  <td class="form_label">Subject:</td>
         2188  +  @  <td><input type="text" name="subject" value="%h(PT("subject"))"\
         2189  +  @  size="80"></td>
         2190  +  @ </tr>
         2191  +  @ <tr>
         2192  +  @  <td class="form_label">Message:</td>
         2193  +  @  <td><textarea name="msg" cols="80" rows="10" wrap="virtual">\
         2194  +  @ %h(PT("msg"))</textarea>
         2195  +  @ </tr>
         2196  +  @ <tr>
         2197  +  @   <td></td>
         2198  +  @   <td><input type="submit" name="submit" value="Send Message">
         2199  +  @ </tr>
         2200  +  @ </table>
         2201  +  if( zCaptcha ){
         2202  +    @ <div class="captcha"><table class="captcha"><tr><td><pre>
         2203  +    @ %h(zCaptcha)
         2204  +    @ </pre>
         2205  +    @ Enter the 8 characters above in the "Security Code" box
         2206  +    @ </td></tr></table></div>
         2207  +  }
         2208  +  @ </form>
         2209  +  style_footer();
         2210  +}
         2211  +
         2212  +/*
         2213  +** Send an annoucement message described by query parameter.
         2214  +** Permission to do this has already been verified.
         2215  +*/
         2216  +static char *email_send_announcement(void){
         2217  +  EmailSender *pSender;
         2218  +  char *zErr;
         2219  +  const char *zTo = PT("to");
         2220  +  char *zSubject = PT("subject");
         2221  +  int bAll = PB("all");
         2222  +  int bAA = PB("aa");
         2223  +  const char *zSub = db_get("email-subname", "[Fossil Repo]");
         2224  +  int bTest2 = fossil_strcmp(P("name"),"test2")==0;
         2225  +  Blob hdr, body;
         2226  +  blob_init(&body, 0, 0);
         2227  +  blob_init(&hdr, 0, 0);
         2228  +  blob_appendf(&body, "%s", PT("msg")/*safe-for-%s*/);
         2229  +  pSender = email_sender_new(bTest2 ? "blob" : 0, 0);
         2230  +  if( zTo[0] ){
         2231  +    blob_appendf(&hdr, "To: <%s>\r\nSubject: %s %s\r\n", zTo, zSub, zSubject);
         2232  +    email_send(pSender, &hdr, &body);
         2233  +  }
         2234  +  if( bAll || bAA ){
         2235  +    Stmt q;
         2236  +    int nUsed = blob_size(&body);
         2237  +    const char *zURL =  db_get("email-url",0);
         2238  +    db_prepare(&q, "SELECT semail, hex(subscriberCode) FROM subscriber "
         2239  +                   " WHERE sverified AND NOT sdonotcall %s",
         2240  +                   bAll ? "" : " AND ssub LIKE '%a%'");
         2241  +    while( db_step(&q)==SQLITE_ROW ){
         2242  +      const char *zCode = db_column_text(&q, 1);
         2243  +      zTo = db_column_text(&q, 0);
         2244  +      blob_truncate(&hdr, 0);
         2245  +      blob_appendf(&hdr, "To: <%s>\r\nSubject: %s %s\r\n", zTo, zSub, zSubject);
         2246  +      if( zURL ){
         2247  +        blob_truncate(&body, nUsed);
         2248  +        blob_appendf(&body,"\n-- \nSubscription info: %s/alerts/%s\n",
         2249  +           zURL, zCode);
         2250  +      }
         2251  +      email_send(pSender, &hdr, &body);
         2252  +    }
         2253  +    db_finalize(&q);
         2254  +  }
         2255  +  if( bTest2 ){
         2256  +    /* If the URL is /announce/test2 instead of just /announce, then no
         2257  +    ** email is actually sent.  Instead, the text of the email that would
         2258  +    ** have been sent is displayed in the result window. */
         2259  +    @ <pre style='border: 2px solid blue; padding: 1ex'>
         2260  +    @ %h(blob_str(&pSender->out))
         2261  +    @ </pre>
         2262  +  }
         2263  +  zErr = pSender->zErr;
         2264  +  pSender->zErr = 0;
         2265  +  email_sender_free(pSender);
         2266  +  return zErr;
         2267  +}
         2268  +
         2269  +
         2270  +/*
         2271  +** WEBPAGE: announce
         2272  +**
         2273  +** A web-form, available to users with the "Send-Announcement" or "A"
         2274  +** capability, that allows one to send announcements to whomever
         2275  +** has subscribed to receive announcements.  The administrator can
         2276  +** also send a message to an arbitrary email address and/or to all
         2277  +** subscribers regardless of whether or not they have elected to
         2278  +** receive announcements.
         2279  +*/
         2280  +void announce_page(void){
         2281  +  login_check_credentials();
         2282  +  if( !g.perm.Announce ){
         2283  +    login_needed(0);
         2284  +    return;
         2285  +  }
         2286  +  if( fossil_strcmp(P("name"),"test1")==0 ){
         2287  +    /* Visit the /announce/test1 page to see the CGI variables */
         2288  +    @ <p style='border: 1px solid black; padding: 1ex;'>
         2289  +    cgi_print_all(0, 0);
         2290  +    @ </p>
         2291  +  }else
         2292  +  if( P("submit")!=0 && cgi_csrf_safe(1) ){
         2293  +    char *zErr = email_send_announcement();
         2294  +    style_header("Announcement Sent");
         2295  +    if( zErr ){
         2296  +      @ <h1>Internal Error</h1>
         2297  +      @ <p>The following error was reported by the system:
         2298  +      @ <blockquote><pre>
         2299  +      @ %h(zErr)
         2300  +      @ </pre></blockquote>
         2301  +    }else{
         2302  +      @ <p>The announcement has been sent.</p>
         2303  +    }
         2304  +    style_footer();    
         2305  +    return;
         2306  +  }
         2307  +  style_header("Send Announcement");
         2308  +  @ <form method="POST">
         2309  +  @ <table class="subscribe">
         2310  +  if( g.perm.Admin ){
         2311  +    int aa = PB("aa");
         2312  +    int all = PB("all");
         2313  +    const char *aack = aa ? "checked" : "";
         2314  +    const char *allck = all ? "checked" : "";
         2315  +    @ <tr>
         2316  +    @  <td class="form_label">To:</td>
         2317  +    @  <td><input type="text" name="to" value="%h(PT("to"))" size="30"><br>
         2318  +    @  <label><input type="checkbox" name="aa" %s(aack)> \
         2319  +    @  All "announcement" subscribers</label> \
         2320  +    @  <a href="%R/subscribers?only=a" target="_blank">(list)</a><br>
         2321  +    @  <label><input type="checkbox" name="all" %s(allck)> \
         2322  +    @  All subscribers</label> \
         2323  +    @  <a href="%R/subscribers" target="_blank">(list)</a><br></td>
         2324  +    @ </tr>
         2325  +  }
         2326  +  @ <tr>
         2327  +  @  <td class="form_label">Subject:</td>
         2328  +  @  <td><input type="text" name="subject" value="%h(PT("subject"))"\
         2329  +  @  size="80"></td>
         2330  +  @ </tr>
         2331  +  @ <tr>
         2332  +  @  <td class="form_label">Message:</td>
         2333  +  @  <td><textarea name="msg" cols="80" rows="10" wrap="virtual">\
         2334  +  @ %h(PT("msg"))</textarea>
         2335  +  @ </tr>
         2336  +  @ <tr>
         2337  +  @   <td></td>
         2338  +  @   <td><input type="submit" name="submit" value="Send Message">
         2339  +  @ </tr>
         2340  +  @ </table>
         2341  +  @ </form>
         2342  +  style_footer();
         2343  +}

Changes to src/encode.c.

   432    432   /*
   433    433   ** The characters used for HTTP base64 encoding.
   434    434   */
   435    435   static unsigned char zBase[] =
   436    436     "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
   437    437   
   438    438   /*
   439         -** Encode a string using a base-64 encoding.
   440         -** The encoding can be reversed using the <b>decode64</b> function.
   441         -**
   442         -** Space to hold the result comes from malloc().
          439  +** Translate nData bytes of content from zData into
          440  +** ((nData+2)/3)*4) bytes of base64 encoded content and
          441  +** put the result in z64.  Add a zero-terminator at the end.
   443    442   */
   444         -char *encode64(const char *zData, int nData){
   445         -  char *z64;
          443  +int translateBase64(const char *zData, int nData, char *z64){
   446    444     int i, n;
   447         -
   448         -  if( nData<=0 ){
   449         -    nData = strlen(zData);
   450         -  }
   451         -  z64 = fossil_malloc( (nData*4)/3 + 8 );
   452    445     for(i=n=0; i+2<nData; i+=3){
   453    446       z64[n++] = zBase[ (zData[i]>>2) & 0x3f ];
   454    447       z64[n++] = zBase[ ((zData[i]<<4) & 0x30) | ((zData[i+1]>>4) & 0x0f) ];
   455    448       z64[n++] = zBase[ ((zData[i+1]<<2) & 0x3c) | ((zData[i+2]>>6) & 0x03) ];
   456    449       z64[n++] = zBase[ zData[i+2] & 0x3f ];
   457    450     }
   458    451     if( i+1<nData ){
................................................................................
   463    456     }else if( i<nData ){
   464    457       z64[n++] = zBase[ (zData[i]>>2) & 0x3f ];
   465    458       z64[n++] = zBase[ ((zData[i]<<4) & 0x30) ];
   466    459       z64[n++] = '=';
   467    460       z64[n++] = '=';
   468    461     }
   469    462     z64[n] = 0;
          463  +  return n;
          464  +}
          465  +
          466  +/*
          467  +** Encode a string using a base-64 encoding.
          468  +** The encoding can be reversed using the <b>decode64</b> function.
          469  +**
          470  +** Space to hold the result comes from malloc().
          471  +*/
          472  +char *encode64(const char *zData, int nData){
          473  +  char *z64;
          474  +  if( nData<=0 ){
          475  +    nData = strlen(zData);
          476  +  }
          477  +  z64 = fossil_malloc( (nData*4)/3 + 8 );
          478  +  translateBase64(zData, nData, z64);
   470    479     return z64;
   471    480   }
   472    481   
   473    482   /*
   474    483   ** COMMAND: test-encode64
   475    484   **
   476    485   ** Usage: %fossil test-encode64 STRING
................................................................................
   481    490     for(i=2; i<g.argc; i++){
   482    491       z = encode64(g.argv[i], -1);
   483    492       fossil_print("%s\n", z);
   484    493       free(z);
   485    494     }
   486    495   }
   487    496   
          497  +
          498  +/* Decode base64 text.  Write the output into zData.  The caller
          499  +** must ensure that zData is large enough.  It is ok for z64 and
          500  +** zData to be the same buffer.  In other words, it is ok to decode
          501  +** in-place.  A zero terminator is always placed at the end of zData.
          502  +*/
          503  +void decodeBase64(const char *z64, int *pnByte, char *zData){
          504  +  const unsigned char *zIn = (const unsigned char*)z64;
          505  +  int i, j, k;
          506  +  int x[4];
          507  +  static int isInit = 0;
          508  +  static signed char trans[256];
          509  +
          510  +  if( !isInit ){
          511  +    for(i=0; i<256; i++){ trans[i] = -1; }
          512  +    for(i=0; zBase[i]; i++){ trans[zBase[i] & 0x7f] = i; }
          513  +    isInit = 1;
          514  +  }
          515  +  for(j=k=0; zIn[0]; zIn++){
          516  +    int v = trans[zIn[0]];
          517  +    if( v>=0 ){
          518  +      x[k++] = v;
          519  +      if( k==4 ){
          520  +        zData[j++] = ((x[0]<<2) & 0xfc) | ((x[1]>>4) & 0x03);
          521  +        zData[j++] = ((x[1]<<4) & 0xf0) | ((x[2]>>2) & 0x0f);
          522  +        zData[j++] = ((x[2]<<6) & 0xc0) | (x[3] & 0x3f);
          523  +        k = 0;
          524  +      }
          525  +    }
          526  +  }
          527  +  if( k>=2 ){
          528  +    zData[j++] = ((x[0]<<2) & 0xfc) | ((x[1]>>4) & 0x03);
          529  +    if( k==3 ){
          530  +      zData[j++] = ((x[1]<<4) & 0xf0) | ((x[2]>>2) & 0x0f);
          531  +    }
          532  +  }
          533  +  zData[j] = 0;
          534  +  *pnByte = j;
          535  +}
          536  +
   488    537   
   489    538   /*
   490    539   ** This function treats its input as a base-64 string and returns the
   491    540   ** decoded value of that string.  Characters of input that are not
   492    541   ** valid base-64 characters (such as spaces and newlines) are ignored.
   493    542   **
   494    543   ** Space to hold the decoded string is obtained from malloc().
   495    544   **
   496    545   ** The number of bytes decoded is returned in *pnByte
   497    546   */
   498    547   char *decode64(const char *z64, int *pnByte){
   499    548     char *zData;
   500         -  int n64;
   501         -  int i, j;
   502         -  int a, b, c, d;
   503         -  static int isInit = 0;
   504         -  static int trans[128];
   505         -
   506         -  if( !isInit ){
   507         -    for(i=0; i<128; i++){ trans[i] = 0; }
   508         -    for(i=0; zBase[i]; i++){ trans[zBase[i] & 0x7f] = i; }
   509         -    isInit = 1;
   510         -  }
   511         -  n64 = strlen(z64);
          549  +  int n64 = (int)strlen(z64);
   512    550     while( n64>0 && z64[n64-1]=='=' ) n64--;
   513    551     zData = fossil_malloc( (n64*3)/4 + 4 );
   514         -  for(i=j=0; i+3<n64; i+=4){
   515         -    a = trans[z64[i] & 0x7f];
   516         -    b = trans[z64[i+1] & 0x7f];
   517         -    c = trans[z64[i+2] & 0x7f];
   518         -    d = trans[z64[i+3] & 0x7f];
   519         -    zData[j++] = ((a<<2) & 0xfc) | ((b>>4) & 0x03);
   520         -    zData[j++] = ((b<<4) & 0xf0) | ((c>>2) & 0x0f);
   521         -    zData[j++] = ((c<<6) & 0xc0) | (d & 0x3f);
   522         -  }
   523         -  if( i+2<n64 ){
   524         -    a = trans[z64[i] & 0x7f];
   525         -    b = trans[z64[i+1] & 0x7f];
   526         -    c = trans[z64[i+2] & 0x7f];
   527         -    zData[j++] = ((a<<2) & 0xfc) | ((b>>4) & 0x03);
   528         -    zData[j++] = ((b<<4) & 0xf0) | ((c>>2) & 0x0f);
   529         -  }else if( i+1<n64 ){
   530         -    a = trans[z64[i] & 0x7f];
   531         -    b = trans[z64[i+1] & 0x7f];
   532         -    zData[j++] = ((a<<2) & 0xfc) | ((b>>4) & 0x03);
   533         -  }
   534         -  zData[j] = 0;
   535         -  *pnByte = j;
          552  +  decodeBase64(z64, pnByte, zData);
   536    553     return zData;
   537    554   }
   538    555   
   539    556   /*
   540    557   ** COMMAND: test-decode64
   541    558   **
   542    559   ** Usage: %fossil test-decode64 STRING
................................................................................
   543    560   */
   544    561   void test_decode64_cmd(void){
   545    562     char *z;
   546    563     int i, n;
   547    564     for(i=2; i<g.argc; i++){
   548    565       z = decode64(g.argv[i], &n);
   549    566       fossil_print("%d: %s\n", n, z);
   550         -    free(z);
          567  +    fossil_free(z);
   551    568     }
   552    569   }
   553    570   
   554    571   /*
   555    572   ** The base-16 encoding using the following characters:
   556    573   **
   557    574   **         0123456789abcdef
................................................................................
   623    640   
   624    641   /*
   625    642   ** Return true if the input string contains only valid base-16 digits.
   626    643   ** If any invalid characters appear in the string, return false.
   627    644   */
   628    645   int validate16(const char *zIn, int nIn){
   629    646     int i;
          647  +  if( nIn<0 ) nIn = (int)strlen(zIn);
   630    648     for(i=0; i<nIn; i++, zIn++){
   631    649       if( zDecode[zIn[0]&0xff]>63 ){
   632    650         return zIn[0]==0;
   633    651       }
   634    652     }
   635    653     return 1;
   636    654   }
................................................................................
   642    660   */
   643    661   void canonical16(char *z, int n){
   644    662     while( *z && n-- ){
   645    663       *z = zEncode[zDecode[(*z)&0x7f]&0x1f];
   646    664       z++;
   647    665     }
   648    666   }
          667  +
          668  +/*
          669  +** Decode a string encoded using "quoted-printable".
          670  +**
          671  +**   (1)  "=" followed by two hex digits becomes a single
          672  +**        byte specified by the two digits
          673  +**
          674  +** The decoding is done in-place.
          675  +*/
          676  +void decodeQuotedPrintable(char *z, int *pnByte){
          677  +  int i, j, c;
          678  +  for(i=j=0; (c = z[i])!=0; i++){
          679  +    if( c=='=' ){
          680  +      if( z[i+1]!='\r' ){
          681  +        decode16((unsigned char*)&z[i+1], (unsigned char*)&z[j], 2);
          682  +        j++;
          683  +      }
          684  +      i += 2;
          685  +    }else{
          686  +      z[j++] = c;
          687  +    }
          688  +  }
          689  +  if( pnByte ) *pnByte = j;
          690  +  z[j] = 0;
          691  +}
   649    692   
   650    693   /* Randomness used for XOR-ing by the obscure() and unobscure() routines */
   651    694   static const unsigned char aObscurer[16] = {
   652    695       0xa7, 0x21, 0x31, 0xe3, 0x2a, 0x50, 0x2c, 0x86,
   653    696       0x4c, 0xa4, 0x52, 0x25, 0xff, 0x49, 0x35, 0x85
   654    697   };
   655    698   

Changes to src/etag.c.

   124    124     if( strcmp(zIfNoneMatch,zETag)!=0 ) return;
   125    125   
   126    126     /* If we get this far, it means that the content has
   127    127     ** not changed and we can do a 304 reply */
   128    128     cgi_reset_content();
   129    129     cgi_set_status(304, "Not Modified");
   130    130     cgi_reply();
          131  +  db_close(0);
   131    132     fossil_exit(0);
   132    133   }
   133    134   
   134    135   /*
   135    136   ** Accept a new Last-Modified time.  This routine should be called by
   136    137   ** page generators that know a valid last-modified time.  This routine
   137    138   ** might generate a 304 Not Modified reply and exit(), never returning.
   138    139   ** Or, if not, it will cause a Last-Modified: header to be included in the
   139    140   ** reply.
   140    141   */
   141    142   void etag_last_modified(sqlite3_int64 mtime){
   142    143     const char *zIfModifiedSince;
   143         -  sqlite3_int64 x, exeMtime;
          144  +  sqlite3_int64 x;
   144    145     assert( iEtagMtime==0 );   /* Only call this routine once */
   145    146     assert( mtime>0 );         /* Only call with a valid mtime */
   146    147     iEtagMtime = mtime;
   147    148   
   148    149     /* Check to see the If-Modified-Since constraint is satisfied */
   149    150     zIfModifiedSince = P("HTTP_IF_MODIFIED_SINCE");
   150    151     if( zIfModifiedSince==0 ) return;
   151    152     x = cgi_rfc822_parsedate(zIfModifiedSince);
   152         -  if( x<=0 || x>mtime ) return;
          153  +  if( x<mtime ) return;
   153    154   
          155  +#if 0  
   154    156     /* If the Fossil executable is more recent than If-Modified-Since,
   155    157     ** go ahead and regenerate the resource. */
   156         -  exeMtime = file_mtime(g.nameOfExe, ExtFILE);
   157         -  if( exeMtime>x ) return;
          158  +  if( file_mtime(g.nameOfExe, ExtFILE)>x ) return;
          159  +#endif
   158    160   
   159    161     /* If we reach this point, it means that the resource has not changed
   160    162     ** and that we should generate a 304 Not Modified reply */
   161    163     cgi_reset_content();
   162    164     cgi_set_status(304, "Not Modified");
   163    165     cgi_reply();
          166  +  db_close(0);
   164    167     fossil_exit(0);
   165    168   }
   166    169   
   167    170   /* Return the ETag, if there is one.
   168    171   */
   169    172   const char *etag_tag(void){
   170    173     return zETag;

Changes to src/event.c.

   585    585   
   586    586     user_select();
   587    587     if (event_commit_common(rid, zId, blob_str(pContent), zETime,
   588    588         zMimeType, zComment, zTags, zClr)==0 ){
   589    589   #ifdef FOSSIL_ENABLE_JSON
   590    590       g.json.resultCode = FSL_JSON_E_ASSERT;
   591    591   #endif
   592         -    fossil_fatal("Internal error: Fossil tried to make an "
          592  +    fossil_panic("Internal error: Fossil tried to make an "
   593    593                    "invalid artifact for the technote.");
   594    594     }
   595    595   }

Changes to src/export.c.

   598    598     }
   599    599     db_finalize(&q);
   600    600     db_finalize(&q2);
   601    601     db_finalize(&q3);
   602    602   
   603    603     /* Output the commit records.
   604    604     */
          605  +  topological_sort_checkins(0);
   605    606     db_prepare(&q,
   606    607       "SELECT strftime('%%s',mtime), objid, coalesce(ecomment,comment),"
   607    608       "       coalesce(euser,user),"
   608    609       "       (SELECT value FROM tagxref WHERE rid=objid AND tagid=%d)"
   609         -    "  FROM event"
   610         -    " WHERE type='ci' AND NOT EXISTS (SELECT 1 FROM oldcommit WHERE objid=rid)"
   611         -    " ORDER BY mtime ASC",
          610  +    "  FROM toponode, event"
          611  +    " WHERE toponode.tid=event.objid"
          612  +    "   AND event.type='ci'"
          613  +    "   AND NOT EXISTS (SELECT 1 FROM oldcommit WHERE toponode.tid=rid)"
          614  +    " ORDER BY toponode.tseq ASC",
   612    615       TAG_BRANCH
   613    616     );
   614    617     db_prepare(&q2, "INSERT INTO oldcommit VALUES (:rid)");
   615    618     while( db_step(&q)==SQLITE_ROW ){
   616    619       Stmt q4;
   617    620       const char *zSecondsSince1970 = db_column_text(&q, 0);
   618    621       int ckinId = db_column_int(&q, 1);
................................................................................
   621    624       const char *zBranch = db_column_text(&q, 4);
   622    625       char *zMark;
   623    626   
   624    627       bag_insert(&vers, ckinId);
   625    628       db_bind_int(&q2, ":rid", ckinId);
   626    629       db_step(&q2);
   627    630       db_reset(&q2);
   628         -    if( zBranch==0 || fossil_strcmp(zBranch, "trunk")==0 ) zBranch = gexport.zTrunkName;
          631  +    if( zBranch==0 || fossil_strcmp(zBranch, "trunk")==0 ){
          632  +      zBranch = gexport.zTrunkName;
          633  +    }
   629    634       zMark = mark_name_from_rid(ckinId, &unused_mark);
   630    635       printf("commit refs/heads/");
   631    636       print_ref(zBranch);
   632    637       printf("\nmark %s\n", zMark);
   633    638       free(zMark);
   634    639       printf("committer");
   635    640       print_person(zUser);
................................................................................
   735    740       if( ferror(f)!=0 || fclose(f)!=0 ){
   736    741         fossil_fatal("error while writing %s", markfile_out);
   737    742       }
   738    743     }
   739    744     bag_clear(&blobs);
   740    745     bag_clear(&vers);
   741    746   }
          747  +
          748  +/*
          749  +** Construct the temporary table toposort as follows:
          750  +**
          751  +**     CREATE TEMP TABLE toponode(
          752  +**        tid INTEGER PRIMARY KEY,   -- Check-in id
          753  +**        tseq INT                   -- integer total order on check-ins.
          754  +**     );
          755  +**
          756  +** This table contains all check-ins of the repository in topological
          757  +** order.  "Topological order" means that every parent check-in comes
          758  +** before all of its children.  Topological order is *almost* the same
          759  +** thing as "ORDER BY event.mtime".  Differences only arrise when there
          760  +** are timewarps.  In as much as Git hates timewarps, we have to compute
          761  +** a correct topological order when doing an export.
          762  +**
          763  +** Since mtime is a usually already nearly in topological order, the
          764  +** algorithm is to start with mtime, then make adjustments as necessary
          765  +** for timewarps.  This is not a great algorithm for the general case,
          766  +** but it is very fast for the overwhelmingly common case where there
          767  +** are few timewarps.
          768  +*/
          769  +int topological_sort_checkins(int bVerbose){
          770  +  int nChange = 0;
          771  +  Stmt q1;
          772  +  Stmt chng;
          773  +  db_multi_exec(
          774  +    "CREATE TEMP TABLE toponode(\n"
          775  +    "  tid INTEGER PRIMARY KEY,\n"
          776  +    "  tseq INT\n"
          777  +    ");\n"
          778  +    "INSERT INTO toponode(tid,tseq) "
          779  +    " SELECT objid, CAST(mtime*8640000 AS int) FROM event WHERE type='ci';\n"
          780  +    "CREATE TEMP TABLE topolink(\n"
          781  +    "  tparent INT,\n"
          782  +    "  tchild INT,\n"
          783  +    "  PRIMARY KEY(tparent,tchild)\n"
          784  +    ") WITHOUT ROWID;"
          785  +    "INSERT INTO topolink(tparent,tchild)"
          786  +    "  SELECT pid, cid FROM plink;\n"
          787  +    "CREATE INDEX topolink_child ON topolink(tchild);\n"
          788  +  );
          789  +
          790  +  /* Find a timewarp instance */
          791  +  db_prepare(&q1,
          792  +    "SELECT P.tseq, C.tid, C.tseq\n"
          793  +    "  FROM toponode P, toponode C, topolink X\n"
          794  +    " WHERE X.tparent=P.tid\n"
          795  +    "   AND X.tchild=C.tid\n"
          796  +    "   AND P.tseq>=C.tseq;"
          797  +  );
          798  +
          799  +  /* Update the timestamp on :tid to have value :tseq */
          800  +  db_prepare(&chng,
          801  +    "UPDATE toponode SET tseq=:tseq WHERE tid=:tid"
          802  +  );
          803  +
          804  +  while( db_step(&q1)==SQLITE_ROW ){
          805  +    i64 iParentTime = db_column_int64(&q1, 0);
          806  +    int iChild = db_column_int(&q1, 1);
          807  +    i64 iChildTime = db_column_int64(&q1, 2);
          808  +    nChange++;
          809  +    if( nChange>10000 ){
          810  +      fossil_fatal("failed to fix all timewarps after 100000 attempts");
          811  +    }
          812  +    db_reset(&q1);
          813  +    db_bind_int64(&chng, ":tid", iChild);
          814  +    db_bind_int64(&chng, ":tseq", iParentTime+1);
          815  +    db_step(&chng);
          816  +    db_reset(&chng);
          817  +    if( bVerbose ){
          818  +      fossil_print("moving %d from %lld to %lld\n",
          819  +                   iChild, iChildTime, iParentTime+1);
          820  +    }
          821  +  }
          822  +
          823  +  db_finalize(&q1);
          824  +  db_finalize(&chng);
          825  +  return nChange;
          826  +}
          827  +
          828  +/*
          829  +** COMMAND: test-topological-sort
          830  +**
          831  +** Invoke the topological_sort_checkins() interface for testing
          832  +** purposes.
          833  +*/
          834  +void test_topological_sort(void){
          835  +  int n;
          836  +  db_find_and_open_repository(0, 0);
          837  +  n = topological_sort_checkins(1);
          838  +  fossil_print("%d reorderings required\n", n);
          839  +}

Changes to src/file.c.

   894    894   */
   895    895   void file_getcwd(char *zBuf, int nBuf){
   896    896   #ifdef _WIN32
   897    897     win32_getcwd(zBuf, nBuf);
   898    898   #else
   899    899     if( getcwd(zBuf, nBuf-1)==0 ){
   900    900       if( errno==ERANGE ){
   901         -      fossil_fatal("pwd too big: max %d", nBuf-1);
          901  +      fossil_panic("pwd too big: max %d", nBuf-1);
   902    902       }else{
   903         -      fossil_fatal("cannot find current working directory; %s",
          903  +      fossil_panic("cannot find current working directory; %s",
   904    904                      strerror(errno));
   905    905       }
   906    906     }
   907    907   #endif
   908    908   }
   909    909   
   910    910   /*
................................................................................
  1612   1612   #if defined(_WIN32) || defined(__CYGWIN__)
  1613   1613     if( z[0]=='/' && fossil_isalpha(z[1]) && z[2]==':' && z[3]=='/' ) z++;
  1614   1614   #else
  1615   1615     while( z[0]=='/' && z[1]=='/' ) z++;
  1616   1616   #endif
  1617   1617     return z;
  1618   1618   }
         1619  +
         1620  +/*
         1621  +** Count the number of objects (files and subdirectores) in a given
         1622  +** directory.  Return the count.  Return -1 of the object is not a
         1623  +** directory.
         1624  +*/
         1625  +int file_directory_size(const char *zDir, const char *zGlob, int omitDotFiles){
         1626  +  void *zNative;
         1627  +  DIR *d;
         1628  +  int n = -1;
         1629  +  zNative = fossil_utf8_to_path(zDir,1);
         1630  +  d = opendir(zNative);
         1631  +  if( d ){
         1632  +    struct dirent *pEntry;
         1633  +    n = 0;
         1634  +    while( (pEntry=readdir(d))!=0 ){
         1635  +      if( pEntry->d_name[0]==0 ) continue;
         1636  +      if( omitDotFiles && pEntry->d_name[0]=='.' ) continue;
         1637  +      if( zGlob ){
         1638  +        char *zUtf8 = fossil_path_to_utf8(pEntry->d_name);
         1639  +        int rc = sqlite3_strglob(zGlob, zUtf8);
         1640  +        fossil_path_free(zUtf8);
         1641  +        if( rc ) continue;
         1642  +      }
         1643  +      n++;
         1644  +    }
         1645  +    closedir(d);
         1646  +  }
         1647  +  fossil_path_free(zNative);
         1648  +  return n;
         1649  +}
         1650  +
         1651  +/*
         1652  +** COMMAND: test-dir-size
         1653  +**
         1654  +** Usage: %fossil test-dir-size NAME [GLOB] [--nodots]
         1655  +**
         1656  +** Return the number of objects in the directory NAME.  If GLOB is
         1657  +** provided, then only count objects that match the GLOB pattern.
         1658  +** if --nodots is specified, omit files that begin with ".".
         1659  +*/
         1660  +void test_dir_size_cmd(void){
         1661  +  int omitDotFiles = find_option("nodots",0,0)!=0;
         1662  +  const char *zGlob;
         1663  +  const char *zDir;
         1664  +  verify_all_options();
         1665  +  if( g.argc!=3 && g.argc!=4 ){
         1666  +    usage("NAME [GLOB] [-nodots]");
         1667  +  }
         1668  +  zDir = g.argv[2];
         1669  +  zGlob = g.argc==4 ? g.argv[3] : 0;
         1670  +  fossil_print("%d\n", file_directory_size(zDir, zGlob, omitDotFiles));
         1671  +}

Added src/forum.c.

            1  +/*
            2  +** Copyright (c) 2018 D. Richard Hipp
            3  +**
            4  +** This program is free software; you can redistribute it and/or
            5  +** modify it under the terms of the Simplified BSD License (also
            6  +** known as the "2-Clause License" or "FreeBSD License".)
            7  +**
            8  +** This program is distributed in the hope that it will be useful,
            9  +** but without any warranty; without even the implied warranty of
           10  +** merchantability or fitness for a particular purpose.
           11  +**
           12  +** Author contact information:
           13  +**   drh@hwaci.com
           14  +**   http://www.hwaci.com/drh/
           15  +**
           16  +*******************************************************************************
           17  +**
           18  +** This file contains code used to generate the user forum.
           19  +*/
           20  +#include "config.h"
           21  +#include <assert.h>
           22  +#include "forum.h"
           23  +
           24  +/*
           25  +** The schema for the tables that manage the forum, if forum is
           26  +** enabled.
           27  +*/
           28  +static const char zForumInit[] = 
           29  +@ CREATE TABLE repository.forumpost(
           30  +@   mpostid INTEGER PRIMARY KEY,  -- unique id for each post (local)
           31  +@   mposthash TEXT,               -- uuid for this post
           32  +@   mthreadid INTEGER,            -- thread to which this post belongs
           33  +@   uname TEXT,                   -- name of user
           34  +@   mtime REAL,                   -- julian day number
           35  +@   mstatus TEXT,                 -- status.  NULL=ok. 'mod'=pending moderation
           36  +@   mimetype TEXT,                -- Mimetype for mbody
           37  +@   ipaddr TEXT,                  -- IP address of post origin
           38  +@   inreplyto INT,                -- Parent posting
           39  +@   mbody TEXT                    -- Content of the post
           40  +@ );
           41  +@ CREATE INDEX repository.forumpost_x1 ON
           42  +@   forumpost(inreplyto,mtime);
           43  +@ CREATE TABLE repository.forumthread(
           44  +@   mthreadid INTEGER PRIMARY KEY,
           45  +@   mthreadhash TEXT,             -- uuid for this thread
           46  +@   mtitle TEXT,                  -- Title or subject line
           47  +@   mtime REAL,                   -- Most recent update
           48  +@   npost INT                     -- Number of posts on this thread
           49  +@ );
           50  +;
           51  +
           52  +/*
           53  +** Create the forum tables in the schema if they do not already
           54  +** exist.
           55  +*/
           56  +static void forum_verify_schema(void){
           57  +  if( !db_table_exists("repository","forumpost") ){
           58  +    db_multi_exec(zForumInit /*works-like:""*/);
           59  +  }
           60  +}
           61  +
           62  +/*
           63  +** WEBPAGE: forum
           64  +** URL: /forum
           65  +** Query parameters:
           66  +**
           67  +**    item=N             Show post N and its replies
           68  +**    
           69  +*/
           70  +void forum_page(void){
           71  +  int itemId;
           72  +  Stmt q;
           73  +  int i;
           74  +
           75  +  login_check_credentials();
           76  +  if( !g.perm.RdForum ){ login_needed(g.anon.RdForum); return; }
           77  +  forum_verify_schema();
           78  +  style_header("Forum");
           79  +  itemId = atoi(PD("item","0"));
           80  +  if( itemId>0 ){
           81  +    int iUp;
           82  +    double rNow;
           83  +    style_submenu_element("Topics", "%R/forum");
           84  +    iUp = db_int(0, "SELECT inreplyto FROM forumpost WHERE mpostid=%d", itemId);
           85  +    if( iUp ){
           86  +      style_submenu_element("Parent", "%R/forum?item=%d", iUp);
           87  +    }
           88  +    rNow = db_double(0.0, "SELECT julianday('now')");
           89  +    /* Show the post given by itemId and all its descendents */
           90  +    db_prepare(&q,
           91  +      "WITH RECURSIVE"
           92  +      " post(id,uname,mstat,mime,ipaddr,parent,mbody,depth,mtime) AS ("
           93  +      "    SELECT mpostid, uname, mstatus, mimetype, ipaddr, inreplyto, mbody,"
           94  +      "           0, mtime FROM forumpost WHERE mpostid=%d"
           95  +      "  UNION"
           96  +      "  SELECT f.mpostid, f.uname, f.mstatus, f.mimetype, f.ipaddr,"
           97  +      "         f.inreplyto, f.mbody, p.depth+1 AS xdepth, f.mtime AS xtime"
           98  +      "    FROM forumpost AS f, post AS p"
           99  +      "   WHERE f.inreplyto=p.id"
          100  +      "   ORDER BY xdepth DESC, xtime ASC"
          101  +      ") SELECT * FROM post;",
          102  +      itemId
          103  +    );
          104  +    while( db_step(&q)==SQLITE_ROW ){
          105  +      int id = db_column_int(&q, 0);
          106  +      const char *zUser = db_column_text(&q, 1);
          107  +      const char *zMime = db_column_text(&q, 3);
          108  +      int iDepth = db_column_int(&q, 7);
          109  +      double rMTime = db_column_double(&q, 8);
          110  +      char *zAge = db_timespan_name(rNow - rMTime);
          111  +      Blob body;
          112  +      @ <!-- Forum post %d(id) -->
          113  +      @ <table class="forum_post">
          114  +      @ <tr>
          115  +      @ <td class="forum_margin" width="%d(iDepth*40)" rowspan="2">
          116  +      @ <td><span class="forum_author">%h(zUser)</span>
          117  +      @ <span class="forum_age">%s(zAge) ago</span>
          118  +      sqlite3_free(zAge);
          119  +      if( g.perm.WrForum ){
          120  +        @ <span class="forum_buttons">
          121  +        if( g.perm.AdminForum || fossil_strcmp(g.zLogin, zUser)==0 ){
          122  +          @ <a href='%R/forumedit?item=%d(id)'>Edit</a>
          123  +        }
          124  +        @ <a href='%R/forumedit?replyto=%d(id)'>Reply</a>
          125  +        @ </span>
          126  +      }
          127  +      @ </tr>
          128  +      @ <tr><td><div class="forum_body">
          129  +      blob_init(&body, db_column_text(&q,6), db_column_bytes(&q,6));
          130  +      wiki_render_by_mimetype(&body, zMime);
          131  +      blob_reset(&body);
          132  +      @ </div></td></tr>
          133  +      @ </table>
          134  +    }
          135  +  }else{
          136  +    /* If we reach this point, that means the users wants a list of
          137  +    ** recent threads.
          138  +    */
          139  +    i = 0;
          140  +    db_prepare(&q,
          141  +      "SELECT a.mtitle, a.npost, b.mpostid"
          142  +      "  FROM forumthread AS a, forumpost AS b "
          143  +      " WHERE a.mthreadid=b.mthreadid"
          144  +      "   AND b.inreplyto IS NULL"
          145  +      " ORDER BY a.mtime DESC LIMIT 40"
          146  +    );
          147  +    if( g.perm.WrForum ){
          148  +      style_submenu_element("New", "%R/forumedit");
          149  +    }
          150  +    @ <h1>Recent Forum Threads</h1>
          151  +    while( db_step(&q)==SQLITE_ROW ){
          152  +      int n = db_column_int(&q,1);
          153  +      int itemid = db_column_int(&q,2);
          154  +      const char *zTitle = db_column_text(&q,0);
          155  +      if( (i++)==0 ){
          156  +        @ <ol>
          157  +      }
          158  +      @ <li><span class="forum_title">
          159  +      @ %z(href("%R/forum?item=%d",itemid))%h(zTitle)</a></span>
          160  +      @ <span class="forum_npost">%d(n) post%s(n==1?"":"s")</span></li>
          161  +    }
          162  +    if( i ){
          163  +      @ </ol>
          164  +    }
          165  +  }
          166  +  style_footer();
          167  +}
          168  +
          169  +/*
          170  +** Use content in CGI parameters "s" (subject), "b" (body), and
          171  +** "mimetype" (mimetype) to create a new forum entry.
          172  +** Return the id of the new forum entry.
          173  +**
          174  +** If any problems occur, return 0 and set *pzErr to a description of
          175  +** the problem.
          176  +**
          177  +** Cases:
          178  +**
          179  +**    itemId==0 && parentId==0        Starting a new thread.
          180  +**    itemId==0 && parentId>0         New reply to parentId
          181  +**    itemId>0 && parentId==0         Edit existing post itemId
          182  +*/
          183  +static int forum_post(int itemId, int parentId, char **pzErr){
          184  +  const char *zSubject = 0;
          185  +  int threadId;
          186  +  double rNow = db_double(0.0, "SELECT julianday('now')");
          187  +  const char *zMime = wiki_filter_mimetypes(P("mimetype"));
          188  +  if( itemId==0 && parentId==0 ){
          189  +    /* Start a new thread.  Subject required. */
          190  +    sqlite3_uint64 r1, r2;
          191  +    zSubject = PT("s");
          192  +    if( zSubject==0 || zSubject[0]==0 ){
          193  +      *pzErr = "\"Subject\" required to start a new thread";
          194  +      return 0;
          195  +    }
          196  +    sqlite3_randomness(sizeof(r1), &r1);
          197  +    sqlite3_randomness(sizeof(r2), &r2);
          198  +    db_multi_exec(
          199  +      "INSERT INTO forumthread(mthreadhash, mtitle, mtime, npost)"
          200  +      "VALUES(lower(hex(randomblob(28))),%Q,%!.17g,1)",
          201  +      zSubject, rNow
          202  +    );
          203  +    threadId = db_last_insert_rowid();
          204  +  }else{
          205  +    threadId = db_int(0, "SELECT mthreadid FROM forumpost"
          206  +                         " WHERE mpostid=%d", itemId ? itemId : parentId);
          207  +  }
          208  +  if( itemId ){
          209  +    if( db_int(0, "SELECT inreplyto IS NULL FROM forumpost"
          210  +                  " WHERE mpostid=%d", itemId) ){
          211  +      db_multi_exec(
          212  +        "UPDATE forumthread SET mtitle=%Q WHERE mthreadid=%d",
          213  +        PT("s"), threadId
          214  +      );
          215  +    }
          216  +    db_multi_exec(
          217  +       "UPDATE forumpost SET"
          218  +       " mtime=%!.17g,"
          219  +       " mimetype=%Q,"
          220  +       " ipaddr=%Q,"
          221  +       " mbody=%Q"
          222  +       " WHERE mpostid=%d",
          223  +       rNow, PT("mimetype"), P("REMOTE_ADDR"), PT("b"), itemId
          224  +    );
          225  +  }else{
          226  +    db_multi_exec(
          227  +       "INSERT INTO forumpost(mposthash,mthreadid,uname,mtime,"
          228  +       "  mstatus,mimetype,ipaddr,inreplyto,mbody) VALUES"
          229  +       "  (lower(hex(randomblob(28))),%d,%Q,%!.17g,%Q,%Q,%Q,nullif(%d,0),%Q)",
          230  +       threadId,g.zLogin,rNow,NULL,zMime,P("REMOTE_ADDR"),parentId,P("b"));
          231  +    itemId = db_last_insert_rowid();
          232  +  }
          233  +  if( zSubject==0 ){
          234  +    db_multi_exec(
          235  +      "UPDATE forumthread SET mtime=%!.17g, npost=npost+1"
          236  +      " WHERE mthreadid=(SELECT mthreadid FROM forumpost WHERE mpostid=%d)",
          237  +      rNow, itemId
          238  +    );
          239  +  }
          240  +  return itemId;
          241  +}
          242  +
          243  +/*
          244  +** WEBPAGE: forumedit
          245  +**
          246  +** Query parameters:
          247  +**
          248  +**    replyto=N      Enter a reply to forum item N
          249  +**    item=N         Edit item N
          250  +**    s=SUBJECT      Subject. New thread only. Omitted for replies
          251  +**    b=BODY         Body of the post
          252  +**    m=MIMETYPE     Mimetype for the body of the post
          253  +**    x              Submit changes
          254  +**    p              Preview changes
          255  +*/
          256  +void forum_edit_page(void){
          257  +  int itemId;
          258  +  int parentId;
          259  +  char *zErr = 0;
          260  +  const char *zMime;
          261  +  const char *zSub;
          262  +
          263  +  login_check_credentials();
          264  +  if( !g.perm.WrForum ){ login_needed(g.anon.WrForum); return; }
          265  +  forum_verify_schema();
          266  +  itemId = atoi(PD("item","0"));
          267  +  parentId = atoi(PD("replyto","0"));
          268  +  if( P("cancel")!=0 ){
          269  +    cgi_redirectf("%R/forum?item=%d", itemId ? itemId : parentId);
          270  +    return;
          271  +  }
          272  +  if( P("x")!=0 && cgi_csrf_safe(1) ){
          273  +    itemId = forum_post(itemId,parentId,&zErr);
          274  +    if( itemId ){
          275  +      cgi_redirectf("%R/forum?item=%d",itemId);
          276  +      return;
          277  +    }
          278  +  }
          279  +  if( itemId && (P("mimetype")==0 || P("b")==0) ){
          280  +    Stmt q;
          281  +    db_prepare(&q, "SELECT mimetype, mbody FROM forumpost"
          282  +                   " WHERE mpostid=%d", itemId);
          283  +    if( db_step(&q)==SQLITE_ROW ){
          284  +      if( P("mimetype")==0 ){
          285  +        cgi_set_query_parameter("mimetype", db_column_text(&q, 0));
          286  +      }
          287  +      if( P("b")==0 ){
          288  +        cgi_set_query_parameter("b", db_column_text(&q, 1));
          289  +      }
          290  +    }
          291  +    db_finalize(&q);
          292  +  }
          293  +  zMime = wiki_filter_mimetypes(P("mimetype"));
          294  +  if( itemId>0 ){
          295  +    style_header("Edit Forum Post");
          296  +  }else if( parentId>0 ){
          297  +    style_header("Comment On Forum Post");
          298  +  }else{
          299  +    style_header("New Forum Thread");
          300  +  }
          301  +  @ <form action="%R/forumedit" method="POST">
          302  +  if( itemId ){
          303  +    @ <input type="hidden" name="item" value="%d(itemId)">
          304  +  }
          305  +  if( parentId ){
          306  +    @ <input type="hidden" name="replyto" value="%d(parentId)">
          307  +  }
          308  +  if( P("p") ){
          309  +    Blob x;
          310  +    @ <div class="forumpreview">
          311  +    if( P("s") ){
          312  +      @ <h1>%h(PT("s"))</h1>
          313  +    }
          314  +    @ <div class="forumpreviewbody">
          315  +    blob_init(&x, PT("b"), -1);
          316  +    wiki_render_by_mimetype(&x, PT("mimetype"));
          317  +    blob_reset(&x);
          318  +    @ </div>
          319  +    @ </div>
          320  +    @ <hr>
          321  +  }
          322  +  @ <table border="0" class="forumeditform"> 
          323  +  if( zErr ){
          324  +    @ <tr><td colspan="2">
          325  +    @ <span class='forumFormErr'>%h(zErr)</span>
          326  +  }
          327  +  if( (itemId==0 && parentId==0)
          328  +   || (itemId && db_int(0, "SELECT inreplyto IS NULL FROM forumpost"
          329  +                           " WHERE mpostid=%d", itemId))
          330  +  ){
          331  +    zSub = PT("s");
          332  +    if( zSub==0 && itemId ){
          333  +      zSub = db_text("",
          334  +         "SELECT mtitle FROM forumthread"
          335  +         " WHERE mthreadid=(SELECT mthreadid FROM forumpost"
          336  +                          "  WHERE mpostid=%d)", itemId);
          337  +    }
          338  +    @ <tr><td>Subject:</td>
          339  +    @ <td><input type='text' class='forumFormSubject' name='s' value='%h(zSub)'>
          340  +  }
          341  +  @ <tr><td>Markup:</td><td>
          342  +  mimetype_option_menu(zMime);
          343  +  @ <tr><td>Comment:</td><td>
          344  +  @ <textarea name="b" class="wikiedit" cols="80"\
          345  +  @  rows="20" wrap="virtual">%h(PD("b",""))</textarea></td>
          346  +  @ <tr><td></td><td>
          347  +  @ <input type="submit" name="p" value="Preview">
          348  +  if( P("p")!=0 ){
          349  +    @ <input type="submit" name="x" value="Submit">
          350  +  }
          351  +  @ <input type="submit" name="cancel" value="Cancel">
          352  +  @ </table>
          353  +  @ </form>
          354  +  style_footer();
          355  +}

Changes to src/graph.c.

   532    532         }
   533    533       }
   534    534       mask = BIT(pRow->iRail);
   535    535       pRow->railInUse |= mask;
   536    536       if( pRow->pChild ){
   537    537         assignChildrenToRail(pRow);
   538    538       }else if( !omitDescenders && count_nonbranch_children(pRow->rid)!=0 ){
   539         -      riser_to_top(pRow);
          539  +      if( !pRow->timeWarp ) riser_to_top(pRow);
   540    540       }
   541    541       if( pParent ){
   542    542         for(pLoop=pParent->pPrev; pLoop && pLoop!=pRow; pLoop=pLoop->pPrev){
   543    543           pLoop->railInUse |= mask;
   544    544         }
   545    545       }
   546    546     }

Changes to src/http_socket.c.

    77     77     socketErrMsg = vmprintf(zFormat, ap);
    78     78     va_end(ap);
    79     79   }
    80     80   
    81     81   /*
    82     82   ** Return the current socket error message
    83     83   */
    84         -const char *socket_errmsg(void){
    85         -  return socketErrMsg;
           84  +char *socket_errmsg(void){
           85  +  char *zResult = socketErrMsg;
           86  +  socketErrMsg = 0;
           87  +  return zResult;
    86     88   }
    87     89   
    88     90   /*
    89     91   ** Call this routine once before any other use of the socket interface.
    90     92   ** This routine does initial configuration of the socket module.
    91     93   */
    92     94   void socket_global_init(void){
................................................................................
   130    132     }
   131    133   }
   132    134   
   133    135   /*
   134    136   ** Open a socket connection.  The identify of the server is determined
   135    137   ** by pUrlData
   136    138   **
   137         -**    pUrlDAta->name       Name of the server.  Ex: www.fossil-scm.org
   138         -**    pUrlDAta->port       TCP/IP port to use.  Ex: 80
          139  +**    pUrlData->name       Name of the server.  Ex: www.fossil-scm.org
          140  +**    pUrlData->port       TCP/IP port to use.  Ex: 80
   139    141   **
   140    142   ** Return the number of errors.
   141    143   */
   142    144   int socket_open(UrlData *pUrlData){
   143    145     int rc = 0;
   144    146     struct addrinfo *ai = 0;
   145    147     struct addrinfo *p;
................................................................................
   188    190     if( ai ) freeaddrinfo(ai);
   189    191     return rc;
   190    192   }
   191    193   
   192    194   /*
   193    195   ** Send content out over the open socket connection.
   194    196   */
   195         -size_t socket_send(void *NotUsed, void *pContent, size_t N){
          197  +size_t socket_send(void *NotUsed, const void *pContent, size_t N){
   196    198     size_t sent;
   197    199     size_t total = 0;
   198    200     while( N>0 ){
   199    201       sent = send(iSocket, pContent, N, 0);
   200    202       if( sent<=0 ) break;
   201    203       total += sent;
   202    204       N -= sent;
................................................................................
   203    205       pContent = (void*)&((char*)pContent)[sent];
   204    206     }
   205    207     return total;
   206    208   }
   207    209   
   208    210   /*
   209    211   ** Receive content back from the open socket connection.
          212  +** Return the number of bytes read.
          213  +**
          214  +** When bDontBlock is false, this function blocks until all N bytes
          215  +** have been read.
   210    216   */
   211         -size_t socket_receive(void *NotUsed, void *pContent, size_t N){
          217  +size_t socket_receive(void *NotUsed, void *pContent, size_t N, int bDontBlock){
   212    218     ssize_t got;
   213    219     size_t total = 0;
          220  +  int flags = 0;
          221  +#ifdef MSG_DONTWAIT
          222  +  if( bDontBlock ) flags |= MSG_DONTWAIT;
          223  +#endif
   214    224     while( N>0 ){
   215    225       /* WinXP fails for large values of N.  So limit it to 64KiB. */
   216         -    got = recv(iSocket, pContent, N>65536 ? 65536 : N, 0);
          226  +    got = recv(iSocket, pContent, N>65536 ? 65536 : N, flags);
   217    227       if( got<=0 ) break;
   218    228       total += (size_t)got;
   219    229       N -= (size_t)got;
   220    230       pContent = (void*)&((char*)pContent)[got];
   221    231     }
   222    232     return total;
   223    233   }

Changes to src/http_ssl.c.

   109    109       if( zCaSetting==0 || zCaSetting[0]=='\0' ){
   110    110         /* CA location not specified, use platform's default certificate store */
   111    111         X509_STORE_set_default_paths(SSL_CTX_get_cert_store(sslCtx));
   112    112       }else{
   113    113         /* User has specified a CA location, make sure it exists and use it */
   114    114         switch( file_isdir(zCaSetting, ExtFILE) ){
   115    115           case 0: { /* doesn't exist */
   116         -          fossil_fatal("ssl-ca-location is set to '%s', "
          116  +          fossil_panic("ssl-ca-location is set to '%s', "
   117    117                 "but is not a file or directory", zCaSetting);
   118    118             break;
   119    119           }
   120    120           case 1: { /* directory */
   121    121             zCaDirectory = zCaSetting;
   122    122             break;
   123    123           }
   124    124           case 2: { /* file */
   125    125             zCaFile = zCaSetting;
   126    126             break;
   127    127           }
   128    128         }
   129    129         if( SSL_CTX_load_verify_locations(sslCtx, zCaFile, zCaDirectory)==0 ){
   130         -        fossil_fatal("Failed to use CA root certificates from "
          130  +        fossil_panic("Failed to use CA root certificates from "
   131    131             "ssl-ca-location '%s'", zCaSetting);
   132    132         }
   133    133       }
   134    134   
   135    135       /* Load client SSL identity, preferring the filename specified on the
   136    136       ** command line */
   137    137       if( g.zSSLIdentity!=0 ){
................................................................................
   139    139       }else{
   140    140         identityFile = db_get("ssl-identity", 0);
   141    141       }
   142    142       if( identityFile!=0 && identityFile[0]!='\0' ){
   143    143         if( SSL_CTX_use_certificate_file(sslCtx,identityFile,SSL_FILETYPE_PEM)!=1
   144    144          || SSL_CTX_use_PrivateKey_file(sslCtx,identityFile,SSL_FILETYPE_PEM)!=1
   145    145         ){
   146         -        fossil_fatal("Could not load SSL identity from %s", identityFile);
          146  +        fossil_panic("Could not load SSL identity from %s", identityFile);
   147    147         }
   148    148       }
   149    149       /* Register a callback to tell the user what to do when the server asks
   150    150       ** for a cert */
   151    151       SSL_CTX_set_client_cert_cb(sslCtx, ssl_client_cert_callback);
   152    152   
   153    153       sslIsInit = 1;

Changes to src/http_transport.c.

   125    125       zHost = mprintf("%s@%s", pUrlData->user, pUrlData->name);
   126    126       blob_append_escaped_arg(&zCmd, zHost);
   127    127       fossil_free(zHost);
   128    128     }else{
   129    129       blob_append_escaped_arg(&zCmd, pUrlData->name);
   130    130     }
   131    131     if( !is_safe_fossil_command(pUrlData->fossil) ){
   132         -    fossil_fatal("the ssh:// URL is asking to run an unsafe command [%s] on "
          132  +    fossil_panic("the ssh:// URL is asking to run an unsafe command [%s] on "
   133    133                    "the server.", pUrlData->fossil);
   134    134     }
   135    135     blob_append_escaped_arg(&zCmd, pUrlData->fossil);
   136    136     blob_append(&zCmd, " test-http", 10);
   137    137     if( pUrlData->path && pUrlData->path[0] ){
   138    138       blob_append_escaped_arg(&zCmd, pUrlData->path);
   139    139     }else{
   140         -    fossil_fatal("ssh:// URI does not specify a path to the repository");
          140  +    fossil_panic("ssh:// URI does not specify a path to the repository");
   141    141     }
   142    142     if( g.fSshTrace ){
   143    143       fossil_print("%s\n", blob_str(&zCmd));  /* Show the whole SSH command */
   144    144     }
   145    145     popen2(blob_str(&zCmd), &sshIn, &sshOut, &sshPid);
   146    146     if( sshPid==0 ){
   147    147       socket_set_errmsg("cannot start ssh tunnel using [%b]", &zCmd);
................................................................................
   179    179         sqlite3_randomness(sizeof(iRandId), &iRandId);
   180    180         transport.zOutFile = mprintf("%s-%llu-out.http",
   181    181                                          g.zRepositoryName, iRandId);
   182    182         transport.zInFile = mprintf("%s-%llu-in.http",
   183    183                                          g.zRepositoryName, iRandId);
   184    184         transport.pFile = fossil_fopen(transport.zOutFile, "wb");
   185    185         if( transport.pFile==0 ){
   186         -        fossil_fatal("cannot output temporary file: %s", transport.zOutFile);
          186  +        fossil_panic("cannot output temporary file: %s", transport.zOutFile);
   187    187         }
   188    188         transport.isOpen = 1;
   189    189       }else{
   190    190         rc = socket_open(pUrlData);
   191    191         if( rc==0 ) transport.isOpen = 1;
   192    192       }
   193    193     }
................................................................................
   323    323       got = ssl_receive(0, zBuf, N);
   324    324       #else
   325    325       got = 0;
   326    326       #endif
   327    327     }else if( pUrlData->isFile ){
   328    328       got = fread(zBuf, 1, N, transport.pFile);
   329    329     }else{
   330         -    got = socket_receive(0, zBuf, N);
          330  +    got = socket_receive(0, zBuf, N, 0);
   331    331     }
   332    332     /* printf("received %d of %d bytes\n", got, N); fflush(stdout); */
   333    333     if( transport.pLog ){
   334    334       fwrite(zBuf, 1, got, transport.pLog);
   335    335       fflush(transport.pLog);
   336    336     }
   337    337     return got;

Changes to src/import.c.

   604    604         fossil_free(gg.aData); gg.aData = 0;
   605    605         gg.nData = atoi(&zLine[5]);
   606    606         if( gg.nData ){
   607    607           int got;
   608    608           gg.aData = fossil_malloc( gg.nData+1 );
   609    609           got = fread(gg.aData, 1, gg.nData, pIn);
   610    610           if( got!=gg.nData ){
   611         -          fossil_fatal("short read: got %d of %d bytes", got, gg.nData);
          611  +          fossil_panic("short read: got %d of %d bytes", got, gg.nData);
   612    612           }
   613    613           gg.aData[got] = '\0';
   614    614           if( gg.zComment==0 &&
   615    615               (gg.xFinish==finish_commit || gg.xFinish==finish_tag) ){
   616    616   	  /* Strip trailing newline, it's appended to the comment. */
   617    617   	  if( gg.aData[got-1] == '\n' )
   618    618   	    gg.aData[got-1] = '\0';

Changes to src/info.c.

  1905   1905         if( iEnd<iStart ) iEnd = iStart;
  1906   1906         db_multi_exec(
  1907   1907           "INSERT OR REPLACE INTO lnos VALUES(%d,%d)", iStart, iEnd
  1908   1908         );
  1909   1909         iStart = iEnd = atoi(&zLn[i++]);
  1910   1910       }while( zLn[i] && iStart && iEnd );
  1911   1911     }
  1912         -  db_prepare(&q, "SELECT min(iStart), iEnd FROM lnos");
         1912  +  db_prepare(&q, "SELECT min(iStart), max(iEnd) FROM lnos");
  1913   1913     if( db_step(&q)==SQLITE_ROW ){
  1914   1914       iStart = db_column_int(&q, 0);
  1915   1915       iEnd = db_column_int(&q, 1);
  1916   1916       iTop = iStart - 15 + (iEnd-iStart)/4;
  1917   1917       if( iTop>iStart - 2 ) iTop = iStart-2;
  1918   1918     }
  1919   1919     db_finalize(&q);

Changes to src/json.c.

   994    994         break;
   995    995       }
   996    996       inFile = (0==strcmp("-",jfile))
   997    997         ? stdin
   998    998         : fossil_fopen(jfile,"rb");
   999    999       if(!inFile){
  1000   1000         g.json.resultCode = FSL_JSON_E_FILE_OPEN_FAILED;
  1001         -      fossil_fatal("Could not open JSON file [%s].",jfile)
         1001  +      fossil_panic("Could not open JSON file [%s].",jfile)
  1002   1002           /* Does not return. */
  1003   1003           ;
  1004   1004       }
  1005   1005       cgi_parse_POST_JSON(inFile, 0);
  1006   1006       if( stdin != inFile ){
  1007   1007         fclose(inFile);
  1008   1008       }

Changes to src/json_branch.c.

   286    286   
   287    287     blob_appendf(&branch, "U %F\n", g.zLogin);
   288    288     md5sum_blob(&branch, &mcksum);
   289    289     blob_appendf(&branch, "Z %b\n", &mcksum);
   290    290   
   291    291     brid = content_put(&branch);
   292    292     if( brid==0 ){
   293         -    fossil_fatal("Problem committing manifest: %s", g.zErrMsg);
          293  +    fossil_panic("Problem committing manifest: %s", g.zErrMsg);
   294    294     }
   295    295     db_multi_exec("INSERT OR IGNORE INTO unsent VALUES(%d)", brid);
   296    296     if( manifest_crosslink(brid, &branch, MC_PERMIT_HOOKS)==0 ){
   297         -    fossil_fatal("%s", g.zErrMsg);
          297  +    fossil_panic("%s", g.zErrMsg);
   298    298     }
   299    299     assert( blob_is_reset(&branch) );
   300    300     content_deltify(rootid, &brid, 1, 0);
   301    301     if( zNewRid ){
   302    302       *zNewRid = brid;
   303    303     }
   304    304   

Changes to src/json_status.c.

   166    166         case -4:  zLabel = "INTEGRATE  ";  break;
   167    167       }
   168    168       blob_append(report, zPrefix, nPrefix);
   169    169       blob_appendf(report, "%s %s\n", zLabel, db_column_text(&q, 0));
   170    170     }
   171    171     db_finalize(&q);
   172    172     if( nErr ){
   173         -    fossil_fatal("aborting due to prior errors");
          173  +    fossil_panic("aborting due to prior errors");
   174    174     }
   175    175   #endif
   176    176     return cson_object_value( oPay );
   177    177   }
   178    178   
   179    179   #endif /* FOSSIL_ENABLE_JSON */

Changes to src/json_timeline.c.

   316    316   ** or 0 for defaults.
   317    317   */
   318    318   cson_value * json_get_changed_files(int rid, int flags){
   319    319     cson_value * rowsV = NULL;
   320    320     cson_array * rows = NULL;
   321    321     Stmt q = empty_Stmt;
   322    322     db_prepare(&q,
   323         -           "SELECT (pid==0) AS isnew,"
   324         -           "       (fid==0) AS isdel,"
   325         -           "       (SELECT name FROM filename WHERE fnid=mlink.fnid) AS name,"
   326         -           "       blob.uuid as uuid,"
   327         -           "       (SELECT uuid FROM blob WHERE rid=pid) as parent,"
   328         -           "       blob.size as size"
   329         -           "  FROM mlink, blob"
   330         -           " WHERE mid=%d AND pid!=fid"
   331         -           " AND blob.rid=fid AND NOT mlink.isaux"
   332         -           " ORDER BY name /*sort*/",
   333         -             rid
   334         -             );
          323  +         "SELECT (pid<=0) AS isnew,"
          324  +         "       (fid==0) AS isdel,"
          325  +         "       (SELECT name FROM filename WHERE fnid=mlink.fnid) AS name,"
          326  +         "       (SELECT uuid FROM blob WHERE rid=fid) as uuid,"
          327  +         "       (SELECT uuid FROM blob WHERE rid=pid) as parent,"
          328  +         "       blob.size as size"
          329  +         "  FROM mlink"
          330  +         " LEFT JOIN blob ON blob.rid=fid"
          331  +         " WHERE mid=%d AND pid!=fid"
          332  +         " AND NOT mlink.isaux"
          333  +         " ORDER BY name /*sort*/",
          334  +         rid
          335  +         );
   335    336     while( (SQLITE_ROW == db_step(&q)) ){
   336    337       cson_value * rowV = cson_value_new_object();
   337    338       cson_object * row = cson_value_get_object(rowV);
   338    339       int const isNew = db_column_int(&q,0);
   339    340       int const isDel = db_column_int(&q,1);
   340    341       char * zDownload = NULL;
   341    342       if(!rowsV){

Changes to src/login.c.

   519    519         return;
   520    520       }
   521    521       if( zQS==0 ){
   522    522         zQS = "?redir=1";
   523    523       }else if( zQS[0]!=0 ){
   524    524         zQS = mprintf("?%s&redir=1", zQS);
   525    525       }
   526         -    cgi_redirectf("%s%s%s", g.zHttpsURL, P("PATH_INFO"), zQS);
          526  +    cgi_redirectf("%s%T%s", g.zHttpsURL, P("PATH_INFO"), zQS);
   527    527       return;
   528    528     }
   529    529     sqlite3_create_function(g.db, "constant_time_cmp", 2, SQLITE_UTF8, 0,
   530    530                     constant_time_cmp_function, 0, 0);
   531    531     zUsername = P("u");
   532    532     zPasswd = P("p");
   533    533     anonFlag = g.zLogin==0 && PB("anon");
................................................................................
   663    663     }
   664    664     if( anonFlag ){
   665    665       @ <input type="hidden" name="anon" value="1" />
   666    666     }
   667    667     if( g.zLogin ){
   668    668       @ <p>Currently logged in as <b>%h(g.zLogin)</b>.
   669    669       @ <input type="submit" name="out" value="Logout"></p>
   670         -    @ <hr />
   671         -    @ <p>Change user:
   672         -  }
   673         -  @ <table class="login_out">
   674         -  @ <tr>
   675         -  @   <td class="login_out_label">User ID:</td>
   676         -  if( anonFlag ){
   677         -    @ <td><input type="text" id="u" name="u" value="anonymous" size="30" /></td>
   678    670     }else{
   679         -    @ <td><input type="text" id="u" name="u" value="" size="30" /></td>
   680         -  }
   681         -  if( P("HTTPS")==0 ){
   682         -    @ <td width="15"><td rowspan="3">
   683         -    @ <p class='securityWarning'>
   684         -    @ Warning: Your password will be sent in the clear over an
   685         -    @ unencrypted connection.
   686         -    if( g.sslNotAvailable ){
   687         -      @ No encrypted connection is available on this server.
          671  +    @ <table class="login_out">
          672  +    @ <tr>
          673  +    @   <td class="form_label">User ID:</td>
          674  +    if( anonFlag ){
          675  +      @ <td><input type="text" id="u" name="u" value="anonymous" size="30"></td>
   688    676       }else{
   689         -      @ Consider logging in at
   690         -      @ <a href='%s(g.zHttpsURL)'>%h(g.zHttpsURL)</a> instead.
   691         -    }
   692         -    @ </p>
   693         -  }
   694         -  @ </tr>
   695         -  @ <tr>
   696         -  @  <td class="login_out_label">Password:</td>
   697         -  @   <td><input type="password" id="p" name="p" value="" size="30" /></td>
   698         -  @ </tr>
   699         -  if( g.zLogin==0 && (anonFlag || zGoto==0) ){
   700         -    zAnonPw = db_text(0, "SELECT pw FROM user"
   701         -                         " WHERE login='anonymous'"
   702         -                         "   AND cap!=''");
   703         -  }
   704         -  @ <tr>
   705         -  @   <td></td>
   706         -  @   <td><input type="submit" name="in" value="Login">
   707         -  @ </tr>
   708         -  @ </table>
   709         -  @ <p>Pressing the Login button grants permission to store a cookie.</p>
   710         -  if( db_get_boolean("self-register", 0) ){
   711         -    @ <p>If you do not have an account, you can
   712         -    @ <a href="%R/register?g=%T(P("G"))">create one</a>.
   713         -  }
   714         -  if( zAnonPw ){
   715         -    unsigned int uSeed = captcha_seed();
   716         -    const char *zDecoded = captcha_decode(uSeed);
   717         -    int bAutoCaptcha = db_get_boolean("auto-captcha", 0);
   718         -    char *zCaptcha = captcha_render(zDecoded);
   719         -
   720         -    @ <p><input type="hidden" name="cs" value="%u(uSeed)" />
   721         -    @ Visitors may enter <b>anonymous</b> as the user-ID with
   722         -    @ the 8-character hexadecimal password shown below:</p>
   723         -    @ <div class="captcha"><table class="captcha"><tr><td><pre>
   724         -    @ %h(zCaptcha)
   725         -    @ </pre></td></tr></table>
   726         -    if( bAutoCaptcha ) {
   727         -       @ <input type="button" value="Fill out captcha" id='autofillButton' \
   728         -       @ data-af='%s(zDecoded)' />
   729         -       style_load_one_js_file("login.js");
   730         -    }
   731         -    @ </div>
   732         -    free(zCaptcha);
   733         -  }
   734         -  @ </form>
   735         -  if( g.zLogin && g.perm.Password ){
          677  +      @ <td><input type="text" id="u" name="u" value="" size="30" /></td>
          678  +    }
          679  +    if( P("HTTPS")==0 ){
          680  +      @ <td width="15"><td rowspan="3">
          681  +      @ <p class='securityWarning'>
          682  +      @ Warning: Your password will be sent in the clear over an
          683  +      @ unencrypted connection.
          684  +      if( g.sslNotAvailable ){
          685  +        @ No encrypted connection is available on this server.
          686  +      }else{
          687  +        @ Consider logging in at
          688  +        @ <a href='%s(g.zHttpsURL)'>%h(g.zHttpsURL)</a> instead.
          689  +      }
          690  +      @ </p>
          691  +    }
          692  +    @ </tr>
          693  +    @ <tr>
          694  +    @  <td class="form_label">Password:</td>
          695  +    @   <td><input type="password" id="p" name="p" value="" size="30" /></td>
          696  +    @ </tr>
          697  +    if( g.zLogin==0 && (anonFlag || zGoto==0) ){
          698  +      zAnonPw = db_text(0, "SELECT pw FROM user"
          699  +                           " WHERE login='anonymous'"
          700  +                           "   AND cap!=''");
          701  +    }
          702  +    @ <tr>
          703  +    @   <td></td>
          704  +    @   <td><input type="submit" name="in" value="Login">
          705  +    @ </tr>
          706  +    @ </table>
          707  +    @ <p>Pressing the Login button grants permission to store a cookie.</p>
          708  +    if( db_get_boolean("self-register", 0) ){
          709  +      @ <p>If you do not have an account, you can
          710  +      @ <a href="%R/register?g=%T(P("G"))">create one</a>.
          711  +    }
          712  +    if( zAnonPw ){
          713  +      unsigned int uSeed = captcha_seed();
          714  +      const char *zDecoded = captcha_decode(uSeed);
          715  +      int bAutoCaptcha = db_get_boolean("auto-captcha", 0);
          716  +      char *zCaptcha = captcha_render(zDecoded);
          717  +  
          718  +      @ <p><input type="hidden" name="cs" value="%u(uSeed)" />
          719  +      @ Visitors may enter <b>anonymous</b> as the user-ID with
          720  +      @ the 8-character hexadecimal password shown below:</p>
          721  +      @ <div class="captcha"><table class="captcha"><tr><td><pre>
          722  +      @ %h(zCaptcha)
          723  +      @ </pre></td></tr></table>
          724  +      if( bAutoCaptcha ) {
          725  +         @ <input type="button" value="Fill out captcha" id='autofillButton' \
          726  +         @ data-af='%s(zDecoded)' />
          727  +         style_load_one_js_file("login.js");
          728  +      }
          729  +      @ </div>
          730  +      free(zCaptcha);
          731  +    }
          732  +    @ </form>
          733  +  }
          734  +  if( login_is_individual() && g.perm.Password ){
          735  +    if( email_enabled() ){
          736  +      @ <hr>
          737  +      @ <p>Configure <a href="%R/alerts">Email Alerts</a>
          738  +      @ for user <b>%h(g.zLogin)</b></p>
          739  +    }
   736    740       @ <hr />
   737    741       @ <p>Change Password for user <b>%h(g.zLogin)</b>:</p>
   738    742       form_begin(0, "%R/login");
   739    743       @ <table>
   740         -    @ <tr><td class="login_out_label">Old Password:</td>
          744  +    @ <tr><td class="form_label">Old Password:</td>
   741    745       @ <td><input type="password" name="p" size="30" /></td></tr>
   742         -    @ <tr><td class="login_out_label">New Password:</td>
          746  +    @ <tr><td class="form_label">New Password:</td>
   743    747       @ <td><input type="password" name="n1" size="30" /></td></tr>
   744         -    @ <tr><td class="login_out_label">Repeat New Password:</td>
          748  +    @ <tr><td class="form_label">Repeat New Password:</td>
   745    749       @ <td><input type="password" name="n2" size="30" /></td></tr>
   746    750       @ <tr><td></td>
   747    751       @ <td><input type="submit" value="Change Password" /></td></tr>
   748    752       @ </table>
   749    753       @ </form>
   750    754     }
   751    755     style_footer();
................................................................................
  1194   1198         case 's':   p->Setup = 1; /* Fall thru into Admin */
  1195   1199         case 'a':   p->Admin = p->RdTkt = p->WrTkt = p->Zip =
  1196   1200                                p->RdWiki = p->WrWiki = p->NewWiki =
  1197   1201                                p->ApndWiki = p->Hyperlink = p->Clone =
  1198   1202                                p->NewTkt = p->Password = p->RdAddr =
  1199   1203                                p->TktFmt = p->Attach = p->ApndTkt =
  1200   1204                                p->ModWiki = p->ModTkt = p->Delete =
         1205  +                             p->RdForum = p->WrForum = p->ModForum =
         1206  +                             p->WrTForum = p->AdminForum =
         1207  +                             p->EmailAlert = p->Announce =
  1201   1208                                p->WrUnver = p->Private = 1;
  1202   1209                                /* Fall thru into Read/Write */
  1203   1210         case 'i':   p->Read = p->Write = 1;                      break;
  1204   1211         case 'o':   p->Read = 1;                                 break;
  1205   1212         case 'z':   p->Zip = 1;                                  break;
  1206   1213   
  1207   1214         case 'd':   p->Delete = 1;                               break;
................................................................................
  1223   1230         case 'c':   p->ApndTkt = 1;                              break;
  1224   1231         case 'q':   p->ModTkt = 1;                               break;
  1225   1232         case 't':   p->TktFmt = 1;                               break;
  1226   1233         case 'b':   p->Attach = 1;                               break;
  1227   1234         case 'x':   p->Private = 1;                              break;
  1228   1235         case 'y':   p->WrUnver = 1;                              break;
  1229   1236   
         1237  +      case '6':   p->AdminForum = 1;
         1238  +      case '5':   p->ModForum = 1;
         1239  +      case '4':   p->WrTForum = 1;
         1240  +      case '3':   p->WrForum = 1;
         1241  +      case '2':   p->RdForum = 1;                              break;
         1242  +
         1243  +      case '7':   p->EmailAlert = 1;                           break;
         1244  +      case 'A':   p->Announce = 1;                             break;
         1245  +
  1230   1246         /* The "u" privileges is a little different.  It recursively
  1231   1247         ** inherits all privileges of the user named "reader" */
  1232   1248         case 'u': {
  1233   1249           if( (flags & LOGIN_IGNORE_UV)==0 ){
  1234   1250             const char *zUser;
  1235   1251             zUser = db_text("", "SELECT cap FROM user WHERE login='reader'");
  1236   1252             login_set_capabilities(zUser, flags | LOGIN_IGNORE_UV);
................................................................................
  1295   1311         case 't':  rc = p->TktFmt;    break;
  1296   1312         /* case 'u': READER    */
  1297   1313         /* case 'v': DEVELOPER */
  1298   1314         case 'w':  rc = p->WrTkt;     break;
  1299   1315         case 'x':  rc = p->Private;   break;
  1300   1316         case 'y':  rc = p->WrUnver;   break;
  1301   1317         case 'z':  rc = p->Zip;       break;
         1318  +      case '2':  rc = p->RdForum;   break;
         1319  +      case '3':  rc = p->WrForum;   break;
         1320  +      case '4':  rc = p->WrTForum;  break;
         1321  +      case '5':  rc = p->ModForum;  break;
         1322  +      case '6':  rc = p->AdminForum;break;
         1323  +      case '7':  rc = p->EmailAlert;break;
         1324  +      case 'A':  rc = p->Announce;  break;
  1302   1325         default:   rc = 0;            break;
  1303   1326       }
  1304   1327     }
  1305   1328     return rc;
  1306   1329   }
  1307   1330   
  1308   1331   /*
................................................................................
  1336   1359   
  1337   1360   /*
  1338   1361   ** Return true if the user is "nobody"
  1339   1362   */
  1340   1363   int login_is_nobody(void){
  1341   1364     return g.zLogin==0 || g.zLogin[0]==0 || fossil_strcmp(g.zLogin,"nobody")==0;
  1342   1365   }
         1366  +
         1367  +/*
         1368  +** Return true if the user is a specific individual, not "nobody" or
         1369  +** "anonymous".
         1370  +*/
         1371  +int login_is_individual(void){
         1372  +  return g.zLogin!=0 && g.zLogin[0]!=0 && fossil_strcmp(g.zLogin,"nobody")!=0
         1373  +           && fossil_strcmp(g.zLogin,"anonymous")!=0;
         1374  +}
  1343   1375   
  1344   1376   /*
  1345   1377   ** Return the login name.  If no login name is specified, return "nobody".
  1346   1378   */
  1347   1379   const char *login_name(void){
  1348   1380     return (g.zLogin && g.zLogin[0]) ? g.zLogin : "nobody";
  1349   1381   }
................................................................................
  1510   1542     form_begin(0, "%R/register");
  1511   1543     if( P("g") ){
  1512   1544       @ <input type="hidden" name="g" value="%h(P("g"))" />
  1513   1545     }
  1514   1546     @ <p><input type="hidden" name="cs" value="%u(uSeed)" />
  1515   1547     @ <table class="login_out">
  1516   1548     @ <tr>
  1517         -  @   <td class="login_out_label" align="right">User ID:</td>
         1549  +  @   <td class="form_label" align="right">User ID:</td>
  1518   1550     @   <td><input type="text" id="u" name="u" value="" size="30" /></td>
  1519   1551     @ </tr>
  1520   1552     @ <tr>
  1521         -  @   <td class="login_out_label" align="right">Password:</td>
         1553  +  @   <td class="form_label" align="right">Password:</td>
  1522   1554     @   <td><input type="password" id="p" name="p" value="" size="30" /></td>
  1523   1555     @ </tr>
  1524   1556     @ <tr>
  1525         -  @   <td class="login_out_label" align="right">Confirm password:</td>
         1557  +  @   <td class="form_label" align="right">Confirm password:</td>
  1526   1558     @   <td><input type="password" id="cp" name="cp" value="" size="30" /></td>
  1527   1559     @ </tr>
  1528   1560     @ <tr>
  1529         -  @   <td class="login_out_label" align="right">Contact info:</td>
         1561  +  @   <td class="form_label" align="right">Contact info:</td>
  1530   1562     @   <td><input type="text" id="c" name="c" value="" size="30" /></td>
  1531   1563     @ </tr>
  1532   1564     @ <tr>
  1533         -  @   <td class="login_out_label" align="right">Captcha text (below):</td>
         1565  +  @   <td class="form_label" align="right">Captcha text (below):</td>
  1534   1566     @   <td><input type="text" id="cap" name="cap" value="" size="30" /></td>
  1535   1567     @ </tr>
  1536   1568     @ <tr><td></td>
  1537   1569     @ <td><input type="submit" name="new" value="Register" /></td></tr>
  1538   1570     @ </table>
  1539   1571     @ <div class="captcha"><table class="captcha"><tr><td><pre>
  1540   1572     @ %h(zCaptcha)

Changes to src/main.c.

    81     81     char ModTkt;           /* q: approve and publish ticket changes (Moderator) */
    82     82     char Attach;           /* b: add attachments */
    83     83     char TktFmt;           /* t: create new ticket report formats */
    84     84     char RdAddr;           /* e: read email addresses or other private data */
    85     85     char Zip;              /* z: download zipped artifact via /zip URL */
    86     86     char Private;          /* x: can send and receive private content */
    87     87     char WrUnver;          /* y: can push unversioned content */
           88  +  char RdForum;          /* 2: Read forum posts */
           89  +  char WrForum;          /* 3: Create new forum posts */
           90  +  char WrTForum;         /* 4: Post to forums not subject to moderation */
           91  +  char ModForum;         /* 5: Moderate (approve or reject) forum posts */
           92  +  char AdminForum;       /* 6: Edit forum posts by other users */
           93  +  char EmailAlert;       /* 7: Sign up for email notifications */
           94  +  char Announce;         /* A: Send announcements */
    88     95   };
    89     96   
    90     97   #ifdef FOSSIL_ENABLE_TCL
    91     98   /*
    92     99   ** All Tcl related context information is in this structure.  This structure
    93    100   ** definition has been copied from and should be kept in sync with the one in
    94    101   ** "th_tcl.c".
................................................................................
   131    138     char *zOpenRevision;    /* Check-in version to use during database open */
   132    139     int localOpen;          /* True if the local database is open */
   133    140     char *zLocalRoot;       /* The directory holding the  local database */
   134    141     int minPrefix;          /* Number of digits needed for a distinct UUID */
   135    142     int eHashPolicy;        /* Current hash policy.  One of HPOLICY_* */
   136    143     int fSqlTrace;          /* True if --sqltrace flag is present */
   137    144     int fSqlStats;          /* True if --sqltrace or --sqlstats are present */
   138         -  int fSqlPrint;          /* True if -sqlprint flag is present */
          145  +  int fSqlPrint;          /* True if --sqlprint flag is present */
          146  +  int fCgiTrace;          /* True if --cgitrace is enabled */
   139    147     int fQuiet;             /* True if -quiet flag is present */
   140    148     int fJail;              /* True if running with a chroot jail */
   141    149     int fHttpTrace;         /* Trace outbound HTTP requests */
   142    150     int fAnyTrace;          /* Any kind of tracing */
   143    151     char *zHttpAuth;        /* HTTP Authorization user:pass information */
   144    152     int fSystemTrace;       /* Trace calls to fossil_system(), --systemtrace */
   145    153     int fSshTrace;          /* Trace the SSH setup traffic */
   146    154     int fSshClient;         /* HTTP client flags for SSH client */
          155  +  int fNoHttpCompress;    /* Do not compress HTTP traffic (for debugging) */
   147    156     char *zSshCmd;          /* SSH command string */
   148    157     int fNoSync;            /* Do not do an autosync ever.  --nosync */
   149    158     int fIPv4;              /* Use only IPv4, not IPv6. --ipv4 */
   150    159     char *zPath;            /* Name of webpage being served */
   151    160     char *zExtra;           /* Extra path information past the webpage name */
   152    161     char *zBaseURL;         /* Full text of the URL being served */
   153    162     char *zHttpsURL;        /* zBaseURL translated to https: */
   154    163     char *zTop;             /* Parent directory of zPath */
   155    164     const char *zContentType;  /* The content type of the input HTTP request */
   156    165     int iErrPriority;       /* Priority of current error message */
   157    166     char *zErrMsg;          /* Text of an error message */
   158    167     int sslNotAvailable;    /* SSL is not available.  Do not redirect to https: */
   159    168     Blob cgiIn;             /* Input to an xfer www method */
   160         -  int cgiOutput;          /* Write error and status messages to CGI */
          169  +  int cgiOutput;          /* 0: command-line 1: CGI. 2: CGI after an error */
   161    170     int xferPanic;          /* Write error messages in XFER protocol */
   162    171     int fullHttpReply;      /* True for full HTTP reply.  False for CGI reply */
   163    172     Th_Interp *interp;      /* The TH1 interpreter */
   164    173     char *th1Setup;         /* The TH1 post-creation setup script, if any */
   165    174     int th1Flags;           /* The TH1 integration state flags */
   166    175     FILE *httpIn;           /* Accept HTTP input from here */
   167    176     FILE *httpOut;          /* Send HTTP output here */
................................................................................
   499    508       }
   500    509     }
   501    510     return zCode;
   502    511   }
   503    512   
   504    513   /* Error logs from SQLite */
   505    514   static void fossil_sqlite_log(void *notUsed, int iCode, const char *zErrmsg){
          515  +  sqlite3_stmt *p;
          516  +  Blob msg;
   506    517   #ifdef __APPLE__
   507    518     /* Disable the file alias warning on apple products because Time Machine
   508         -  ** creates lots of aliases and the warning alarms people. */
          519  +  ** creates lots of aliases and the warnings alarm people. */
   509    520     if( iCode==SQLITE_WARNING ) return;
   510    521   #endif
   511    522   #ifndef FOSSIL_DEBUG
   512    523     /* Disable the automatic index warning except in FOSSIL_DEBUG builds. */
   513    524     if( iCode==SQLITE_WARNING_AUTOINDEX ) return;
   514    525   #endif
   515    526     if( iCode==SQLITE_SCHEMA ) return;
   516    527     if( g.dbIgnoreErrors ) return;
   517    528   #ifdef SQLITE_READONLY_DIRECTORY
   518    529     if( iCode==SQLITE_READONLY_DIRECTORY ){
   519    530       zErrmsg = "database is in a read-only directory";
   520    531     }
   521    532   #endif
   522         -  fossil_warning("%s: %s", fossil_sqlite_return_code_name(iCode), zErrmsg);
          533  +  blob_init(&msg, 0, 0);
          534  +  blob_appendf(&msg, "%s(%d): %s",
          535  +     fossil_sqlite_return_code_name(iCode), iCode, zErrmsg);
          536  +  if( g.db ){
          537  +    for(p=sqlite3_next_stmt(g.db, 0); p; p=sqlite3_next_stmt(g.db,p)){
          538  +      const char *zSql;
          539  +      if( !sqlite3_stmt_busy(p) ) continue;
          540  +      zSql = sqlite3_sql(p);
          541  +      if( zSql==0 ) continue;
          542  +      blob_appendf(&msg, "\nSQL: %s", zSql);
          543  +    }
          544  +  }
          545  +  fossil_warning("%s", blob_str(&msg));
          546  +  blob_reset(&msg);
   523    547   }
   524    548   
   525    549   /*
   526    550   ** This function attempts to find command line options known to contain
   527    551   ** bitwise flags and initializes the associated global variables.  After
   528    552   ** this function executes, all global variables (i.e. in the "g" struct)
   529    553   ** containing option-settable bitwise flag fields must be initialized.
................................................................................
   532    556     const char *zValue = find_option("comfmtflags", 0, 1);
   533    557     if( zValue ){
   534    558       g.comFmtFlags = atoi(zValue);
   535    559     }else{
   536    560       g.comFmtFlags = COMMENT_PRINT_DEFAULT;
   537    561     }
   538    562   }
          563  +
          564  +/*
          565  +** Check to see if the Fossil binary contains an appended repository
          566  +** file using the appendvfs extension.  If so, change command-line arguments
          567  +** to cause Fossil to launch with "fossil ui" on that repo.
          568  +*/
          569  +static int fossilExeHasAppendedRepo(void){
          570  +  extern int deduceDatabaseType(const char*,int);
          571  +  if( 2==deduceDatabaseType(g.nameOfExe,0) ){
          572  +    static char *azAltArgv[] = { 0, "ui", 0, 0 };
          573  +    azAltArgv[0] = g.nameOfExe;
          574  +    azAltArgv[2] = g.nameOfExe;
          575  +    g.argv = azAltArgv;
          576  +    g.argc = 3;
          577  +    return 1;
          578  +  }else{
          579  +    return 0;
          580  +  }
          581  +}
   539    582   
   540    583   /*
   541    584   ** This procedure runs first.
   542    585   */
   543    586   #if defined(_WIN32) && !defined(BROKEN_MINGW_CMDLINE)
   544    587   int _dowildcard = -1; /* This turns on command-line globbing in MinGW-w64 */
   545    588   int wmain(int argc, wchar_t **argv)
................................................................................
   552    595   {
   553    596     const char *zCmdName = "unknown";
   554    597     const CmdOrPage *pCmd = 0;
   555    598     int rc;
   556    599   
   557    600     fossil_limit_memory(1);
   558    601     if( sqlite3_libversion_number()<3014000 ){
   559         -    fossil_fatal("Unsuitable SQLite version %s, must be at least 3.14.0",
          602  +    fossil_panic("Unsuitable SQLite version %s, must be at least 3.14.0",
   560    603                    sqlite3_libversion());
   561    604     }
   562    605     sqlite3_config(SQLITE_CONFIG_MULTITHREAD);
   563    606     sqlite3_config(SQLITE_CONFIG_LOG, fossil_sqlite_log, 0);
   564    607     memset(&g, 0, sizeof(g));
   565    608     g.now = time(0);
   566    609     g.httpHeader = empty_blob;
................................................................................
   590    633       g.zVfsName = fossil_getenv("FOSSIL_VFS");
   591    634     }
   592    635     if( g.zVfsName ){
   593    636       sqlite3_vfs *pVfs = sqlite3_vfs_find(g.zVfsName);
   594    637       if( pVfs ){
   595    638         sqlite3_vfs_register(pVfs, 1);
   596    639       }else{
   597         -      fossil_fatal("no such VFS: \"%s\"", g.zVfsName);
          640  +      fossil_panic("no such VFS: \"%s\"", g.zVfsName);
   598    641       }
   599    642     }
   600    643     if( fossil_getenv("GATEWAY_INTERFACE")!=0 && !find_option("nocgi", 0, 0)){
   601    644       zCmdName = "cgi";
   602    645       g.isHTTP = 1;
   603         -  }else if( g.argc<2 ){
          646  +  }else if( g.argc<2 && !fossilExeHasAppendedRepo() ){
   604    647       fossil_print(
   605    648          "Usage: %s COMMAND ...\n"
   606    649          "   or: %s help           -- for a list of common commands\n"
   607    650          "   or: %s help COMMAND   -- for help with the named command\n",
   608    651          g.argv[0], g.argv[0], g.argv[0]);
   609    652       fossil_print(
   610    653         "\nCommands and filenames may be passed on to fossil from a file\n"
................................................................................
   623    666       g.isHTTP = 0;
   624    667       g.rcvid = 0;
   625    668       g.fQuiet = find_option("quiet", 0, 0)!=0;
   626    669       g.fSqlTrace = find_option("sqltrace", 0, 0)!=0;
   627    670       g.fSqlStats = find_option("sqlstats", 0, 0)!=0;
   628    671       g.fSystemTrace = find_option("systemtrace", 0, 0)!=0;
   629    672       g.fSshTrace = find_option("sshtrace", 0, 0)!=0;
          673  +    g.fCgiTrace = find_option("cgitrace", 0, 0)!=0;
   630    674       g.fSshClient = 0;
   631    675       g.zSshCmd = 0;
   632    676       if( g.fSqlTrace ) g.fSqlStats = 1;
   633    677       g.fHttpTrace = find_option("httptrace", 0, 0)!=0;
   634    678   #ifdef FOSSIL_ENABLE_TH1_HOOKS
   635    679       g.fNoThHook = find_option("no-th-hook", 0, 0)!=0;
   636    680   #endif
   637         -    g.fAnyTrace = g.fSqlTrace|g.fSystemTrace|g.fSshTrace|g.fHttpTrace;
          681  +    g.fAnyTrace = g.fSqlTrace|g.fSystemTrace|g.fSshTrace|
          682  +                  g.fHttpTrace|g.fCgiTrace;
   638    683       g.zHttpAuth = 0;
   639    684       g.zLogin = find_option("user", "U", 1);
   640    685       g.zSSLIdentity = find_option("ssl-identity", 0, 1);
   641    686       g.zErrlog = find_option("errorlog", 0, 1);
   642    687       fossil_init_flags_from_options();
   643    688       if( find_option("utc",0,0) ) g.fTimeFormat = 1;
   644    689       if( find_option("localtime",0,0) ) g.fTimeFormat = 2;
   645    690       if( zChdir && file_chdir(zChdir, 0) ){
   646         -      fossil_fatal("unable to change directories to %s", zChdir);
          691  +      fossil_panic("unable to change directories to %s", zChdir);
   647    692       }
   648    693       if( find_option("help",0,0)!=0 ){
   649    694         /* If --help is found anywhere on the command line, translate the command
   650    695          * to "fossil help cmdname" where "cmdname" is the first argument that
   651    696          * does not begin with a "-" character.  If all arguments start with "-",
   652    697          * translate to "fossil help argv[1] argv[2]...". */
   653    698         int i, nNewArgc;
................................................................................
   688    733         if( fd>=2 ) break;
   689    734         if( fd<0 ) x = errno;
   690    735       }while( nTry++ < 2 );
   691    736       if( fd<2 ){
   692    737         g.cgiOutput = 1;
   693    738         g.httpOut = stdout;
   694    739         g.fullHttpReply = !g.isHTTP;
   695         -      fossil_fatal("file descriptor 2 is not open. (fd=%d, errno=%d)",
          740  +      fossil_panic("file descriptor 2 is not open. (fd=%d, errno=%d)",
   696    741                      fd, x);
   697    742       }
   698    743     }
   699    744   #endif
   700    745     rc = dispatch_name_search(zCmdName, CMDFLAG_COMMAND|CMDFLAG_PREFIX, &pCmd);
   701    746     if( rc==1 ){
   702    747   #ifdef FOSSIL_ENABLE_TH1_HOOKS
................................................................................
   704    749         rc = Th_CommandHook(zCmdName, 0);
   705    750       }else{
   706    751         rc = TH_OK;
   707    752       }
   708    753       if( rc==TH_OK || rc==TH_RETURN || rc==TH_CONTINUE ){
   709    754         if( rc==TH_OK || rc==TH_RETURN ){
   710    755   #endif
   711         -        fossil_fatal("%s: unknown command: %s\n"
          756  +        fossil_panic("%s: unknown command: %s\n"
   712    757                        "%s: use \"help\" for more information",
   713    758                        g.argv[0], zCmdName, g.argv[0]);
   714    759   #ifdef FOSSIL_ENABLE_TH1_HOOKS
   715    760         }
   716    761         if( !g.isHTTP && !g.fNoThHook && (rc==TH_OK || rc==TH_CONTINUE) ){
   717    762           Th_CommandNotify(zCmdName, 0);
   718    763         }
................................................................................
   769    814     return 0;
   770    815   }
   771    816   
   772    817   /*
   773    818   ** Print a usage comment and quit
   774    819   */
   775    820   void usage(const char *zFormat){
   776         -  fossil_fatal("Usage: %s %s %s", g.argv[0], g.argv[1], zFormat);
          821  +  fossil_panic("Usage: %s %s %s", g.argv[0], g.argv[1], zFormat);
   777    822   }
   778    823   
   779    824   /*
   780    825   ** Remove n elements from g.argv beginning with the i-th element.
   781    826   */
   782    827   static void remove_from_argv(int i, int n){
   783    828     int j;
................................................................................
   887    932   ** Any remaining command-line argument begins with "-" print
   888    933   ** an error message and quit.
   889    934   */
   890    935   void verify_all_options(void){
   891    936     int i;
   892    937     for(i=1; i<g.argc; i++){
   893    938       if( g.argv[i][0]=='-' && g.argv[i][1]!=0 ){
   894         -      fossil_fatal(
          939  +      fossil_panic(
   895    940           "unrecognized command-line option, or missing argument: %s",
   896    941           g.argv[i]);
   897    942       }
   898    943     }
   899    944   }
   900    945   
   901    946   /*
................................................................................
  1106   1151       if( strncmp(g.zTop, "http://", 7)==0 ){
  1107   1152         /* it is HTTP, replace prefix with HTTPS. */
  1108   1153         g.zHttpsURL = mprintf("https://%s", &g.zTop[7]);
  1109   1154       }else if( strncmp(g.zTop, "https://", 8)==0 ){
  1110   1155         /* it is already HTTPS, use it. */
  1111   1156         g.zHttpsURL = mprintf("%s", g.zTop);
  1112   1157       }else{
  1113         -      fossil_fatal("argument to --baseurl should be 'http://host/path'"
         1158  +      fossil_panic("argument to --baseurl should be 'http://host/path'"
  1114   1159                      " or 'https://host/path'");
  1115   1160       }
  1116   1161       for(i=n=0; (c = g.zTop[i])!=0; i++){
  1117   1162         if( c=='/' ){
  1118   1163           n++;
  1119   1164           if( n==3 ){
  1120   1165             g.zTop += i;
  1121   1166             break;
  1122   1167           }
  1123   1168         }
  1124   1169       }
  1125   1170       if( g.zTop==g.zBaseURL ){
  1126         -      fossil_fatal("argument to --baseurl should be 'http://host/path'"
         1171  +      fossil_panic("argument to --baseurl should be 'http://host/path'"
  1127   1172                      " or 'https://host/path'");
  1128   1173       }
  1129   1174       if( g.zTop[1]==0 ) g.zTop++;
  1130   1175     }else{
  1131   1176       zHost = PD("HTTP_HOST","");
  1132   1177       zMode = PD("HTTPS","off");
  1133   1178       zCur = PD("SCRIPT_NAME","/");
................................................................................
  1174   1219   ** Assume the user-id and group-id of the repository, or if zRepo
  1175   1220   ** is a directory, of that directory.
  1176   1221   **
  1177   1222   ** The noJail flag means that the chroot jail is not entered.  But
  1178   1223   ** privileges are still lowered to that of the user-id and group-id
  1179   1224   ** of the repository file.
  1180   1225   */
  1181         -static char *enter_chroot_jail(char *zRepo, int noJail){
         1226  +char *enter_chroot_jail(char *zRepo, int noJail){
  1182   1227   #if !defined(_WIN32)
  1183   1228     if( getuid()==0 ){
  1184   1229       int i;
  1185   1230       struct stat sStat;
  1186   1231       Blob dir;
  1187   1232       char *zDir;
  1188   1233       if( g.db!=0 ){
................................................................................
  1190   1235       }
  1191   1236   
  1192   1237       file_canonical_name(zRepo, &dir, 0);
  1193   1238       zDir = blob_str(&dir);
  1194   1239       if( !noJail ){
  1195   1240         if( file_isdir(zDir, ExtFILE)==1 ){
  1196   1241           if( file_chdir(zDir, 1) ){
  1197         -          fossil_fatal("unable to chroot into %s", zDir);
         1242  +          fossil_panic("unable to chroot into %s", zDir);
  1198   1243           }
  1199   1244           g.fJail = 1;
  1200   1245           zRepo = "/";
  1201   1246         }else{
  1202   1247           for(i=strlen(zDir)-1; i>0 && zDir[i]!='/'; i--){}
  1203         -        if( zDir[i]!='/' ) fossil_fatal("bad repository name: %s", zRepo);
         1248  +        if( zDir[i]!='/' ) fossil_panic("bad repository name: %s", zRepo);
  1204   1249           if( i>0 ){
  1205   1250             zDir[i] = 0;
  1206   1251             if( file_chdir(zDir, 1) ){
  1207         -            fossil_fatal("unable to chroot into %s", zDir);
         1252  +            fossil_panic("unable to chroot into %s", zDir);
  1208   1253             }
  1209   1254             zDir[i] = '/';
  1210   1255           }
  1211   1256           zRepo = &zDir[i];
  1212   1257         }
  1213   1258       }
  1214   1259       if( stat(zRepo, &sStat)!=0 ){
  1215         -      fossil_fatal("cannot stat() repository: %s", zRepo);
         1260  +      fossil_panic("cannot stat() repository: %s", zRepo);
  1216   1261       }
  1217   1262       i = setgid(sStat.st_gid);
  1218   1263       i = i || setuid(sStat.st_uid);
  1219   1264       if(i){
  1220         -      fossil_fatal("setgid/uid() failed with errno %d", errno);
         1265  +      fossil_panic("setgid/uid() failed with errno %d", errno);
  1221   1266       }
  1222   1267       if( g.db==0 && file_isfile(zRepo, ExtFILE) ){
  1223   1268         db_open_repository(zRepo);
  1224   1269       }
  1225   1270     }
  1226   1271   #endif
  1227   1272     return zRepo;
................................................................................
  1365   1410       g.zRepositoryName = "/";
  1366   1411     }else{
  1367   1412       g.zRepositoryName = g.argv[2];
  1368   1413     }
  1369   1414     g.httpOut = stdout;
  1370   1415     repo_list_page();
  1371   1416   }
         1417  +
         1418  +/*
         1419  +** Called whenever a crash is encountered while processing a webpage.
         1420  +*/
         1421  +void sigsegv_handler(int x){
         1422  +  fossil_errorlog("Segfault");
         1423  +  exit(1);
         1424  +}
         1425  +
         1426  +/*
         1427  +** Called if a server gets a SIGPIPE.  This often happens when a client
         1428  +** webbrowser opens a connection but never sends the HTTP request
         1429  +*/
         1430  +void sigpipe_handler(int x){
         1431  +#ifndef _WIN32
         1432  +  if( g.fAnyTrace ){
         1433  +    fprintf(stderr,"/**** sigpipe received by subprocess %d ****\n", getpid());
         1434  +  }
         1435  +#endif
         1436  +  fossil_exit(1);
         1437  +}
  1372   1438   
  1373   1439   /*
  1374   1440   ** Preconditions:
  1375   1441   **
  1376   1442   **  * Environment variables are set up according to the CGI standard.
  1377   1443   **
  1378   1444   ** If the repository is known, it has already been opened.  If unknown,
................................................................................
  1398   1464     int allowRepoList           /* Send repo list for "/" URL */
  1399   1465   ){
  1400   1466     const char *zPathInfo = PD("PATH_INFO", "");
  1401   1467     char *zPath = NULL;
  1402   1468     int i;
  1403   1469     const CmdOrPage *pCmd = 0;
  1404   1470     const char *zBase = g.zRepositoryName;
         1471  +
         1472  +#if !defined(_WIN32)
         1473  +  signal(SIGSEGV, sigsegv_handler);
         1474  +#endif
  1405   1475   
  1406   1476     /* Handle universal query parameters */
  1407   1477     if( PB("utc") ){
  1408   1478       g.fTimeFormat = 1;
  1409   1479     }else if( PB("localtime") ){
  1410   1480       g.fTimeFormat = 2;
  1411   1481     }
................................................................................
  1607   1677     if( zPathInfo && strncmp(zPathInfo,"/draft",6)==0
  1608   1678      && zPathInfo[6]>='1' && zPathInfo[6]<='9'
  1609   1679      && (zPathInfo[7]=='/' || zPathInfo[7]==0)
  1610   1680     ){
  1611   1681       int iSkin = zPathInfo[6] - '0';
  1612   1682       char *zNewScript;
  1613   1683       skin_use_draft(iSkin);
  1614         -    zNewScript = mprintf("%s/draft%d", P("SCRIPT_NAME"), iSkin);
         1684  +    zNewScript = mprintf("%T/draft%d", P("SCRIPT_NAME"), iSkin);
  1615   1685       if( g.zTop ) g.zTop = mprintf("%s/draft%d", g.zTop, iSkin);
  1616   1686       if( g.zBaseURL ) g.zBaseURL = mprintf("%s/draft%d", g.zBaseURL, iSkin);
  1617   1687       zPathInfo += 7;
  1618   1688       cgi_replace_parameter("PATH_INFO", zPathInfo);
  1619   1689       cgi_replace_parameter("SCRIPT_NAME", zNewScript);
  1620   1690     }
  1621   1691   
................................................................................
  1739   1809   #endif
  1740   1810       {
  1741   1811         @ <h1>Server Configuration Error</h1>
  1742   1812         @ <p>The database schema on the server is out-of-date.  Please ask
  1743   1813         @ the administrator to run <b>fossil rebuild</b>.</p>
  1744   1814       }
  1745   1815     }else{
         1816  +    if( g.fCgiTrace ){
         1817  +      fossil_trace("######## Calling %s #########\n", pCmd->zName);
         1818  +      cgi_print_all(1, 1);
         1819  +    }
  1746   1820   #ifdef FOSSIL_ENABLE_TH1_HOOKS
  1747   1821       /*
  1748   1822       ** The TH1 return codes from the hook will be handled as follows:
  1749   1823       **
  1750   1824       ** TH_OK: The xFunc() and the TH1 notification will both be executed.
  1751   1825       **
  1752   1826       ** TH_ERROR: The xFunc() will be skipped, the TH1 notification will be
................................................................................
  2151   2225     LPVOID *ppAddress,   /* The extracted pointer value. */
  2152   2226     SIZE_T *pnSize       /* The extracted size value. */
  2153   2227   ){
  2154   2228     unsigned int nSize = 0;
  2155   2229     if( sscanf(zPidKey, "%lu:%p:%u", pProcessId, ppAddress, &nSize)==3 ){
  2156   2230       *pnSize = (SIZE_T)nSize;
  2157   2231     }else{
  2158         -    fossil_fatal("failed to parse pid key");
         2232  +    fossil_panic("failed to parse pid key");
  2159   2233     }
  2160   2234   }
  2161   2235   #endif
  2162   2236   
  2163   2237   /*
  2164   2238   ** undocumented format:
  2165   2239   **
................................................................................
  2200   2274   **
  2201   2275   ** Options:
  2202   2276   **   --baseurl URL    base URL (useful with reverse proxies)
  2203   2277   **   --files GLOB     comma-separate glob patterns for static file to serve
  2204   2278   **   --localauth      enable automatic login for local connections
  2205   2279   **   --host NAME      specify hostname of the server
  2206   2280   **   --https          signal a request coming in via https
         2281  +**   --nocompress     Do not compress HTTP replies
  2207   2282   **   --nojail         drop root privilege but do not enter the chroot jail
  2208   2283   **   --nossl          signal that no SSL connections are available
  2209   2284   **   --notfound URL   use URL as "HTTP 404, object not found" page.
  2210   2285   **   --repolist       If REPOSITORY is directory, URL "/" lists all repos
  2211   2286   **   --scgi           Interpret input as SCGI rather than HTTP
  2212   2287   **   --skin LABEL     Use override skin LABEL
  2213   2288   **   --th-trace       trace TH1 execution (for debugging purposes)
................................................................................
  2245   2320     }
  2246   2321     skin_override();
  2247   2322     zNotFound = find_option("notfound", 0, 1);
  2248   2323     noJail = find_option("nojail",0,0)!=0;
  2249   2324     allowRepoList = find_option("repolist",0,0)!=0;
  2250   2325     g.useLocalauth = find_option("localauth", 0, 0)!=0;
  2251   2326     g.sslNotAvailable = find_option("nossl", 0, 0)!=0;
         2327  +  g.fNoHttpCompress = find_option("nocompress",0,0)!=0;
  2252   2328     useSCGI = find_option("scgi", 0, 0)!=0;
  2253   2329     zAltBase = find_option("baseurl", 0, 1);
  2254   2330     if( zAltBase ) set_base_url(zAltBase);
  2255   2331     if( find_option("https",0,0)!=0 ){
  2256   2332       zIpAddr = fossil_getenv("REMOTE_HOST"); /* From stunnel */
  2257   2333       cgi_replace_parameter("HTTPS","on");
  2258   2334     }
................................................................................
  2270   2346     }
  2271   2347   #endif
  2272   2348   
  2273   2349     /* We should be done with options.. */
  2274   2350     verify_all_options();
  2275   2351   
  2276   2352     if( g.argc!=2 && g.argc!=3 && g.argc!=5 && g.argc!=6 ){
  2277         -    fossil_fatal("no repository specified");
         2353  +    fossil_panic("no repository specified");
  2278   2354     }
  2279   2355     g.cgiOutput = 1;
  2280   2356     g.fullHttpReply = 1;
  2281   2357     if( g.argc>=5 ){
  2282   2358       g.httpIn = fossil_fopen(g.argv[2], "rb");
  2283   2359       g.httpOut = fossil_fopen(g.argv[3], "wb");
  2284   2360       zIpAddr = g.argv[4];
................................................................................
  2335   2411     Th_InitTraceLog();
  2336   2412     login_set_capabilities("sx", 0);
  2337   2413     g.useLocalauth = 1;
  2338   2414     g.httpIn = stdin;
  2339   2415     g.httpOut = stdout;
  2340   2416     find_server_repository(2, 0);
  2341   2417     g.cgiOutput = 1;
         2418  +  g.fNoHttpCompress = 1;
  2342   2419     g.fullHttpReply = 1;
  2343   2420     zIpAddr = cgi_ssh_remote_addr(0);
  2344   2421     if( zIpAddr && zIpAddr[0] ){
  2345   2422       g.fSshClient |= CGI_SSH_CLIENT;
  2346   2423       ssh_request_loop(zIpAddr, 0);
  2347   2424     }else{
  2348   2425       cgi_set_parameter("REMOTE_ADDR", "127.0.0.1");
................................................................................
  2434   2511   **   --page PAGE         Start "ui" on PAGE.  ex: --page "timeline?y=ci"
  2435   2512   **   --files GLOBLIST    Comma-separated list of glob patterns for static files
  2436   2513   **   --localauth         enable automatic login for requests from localhost
  2437   2514   **   --localhost         listen on 127.0.0.1 only (always true for "ui")
  2438   2515   **   --https             signal a request coming in via https
  2439   2516   **   --max-latency N     Do not let any single HTTP request run for more than N
  2440   2517   **                       seconds (only works on unix)
         2518  +**   --nocompress        Do not compress HTTP replies
  2441   2519   **   --nojail            Drop root privileges but do not enter the chroot jail
  2442   2520   **   --nossl             signal that no SSL connections are available
  2443   2521   **   --notfound URL      Redirect
  2444   2522   **   -P|--port TCPPORT   listen to request on port TCPPORT
  2445   2523   **   --th-trace          trace TH1 execution (for debugging purposes)
  2446   2524   **   --repolist          If REPOSITORY is dir, URL "/" lists repos.
  2447   2525   **   --scgi              Accept SCGI rather than HTTP
................................................................................
  2474   2552   #endif
  2475   2553   
  2476   2554   #if defined(_WIN32)
  2477   2555     const char *zStopperFile;    /* Name of file used to terminate server */
  2478   2556     zStopperFile = find_option("stopper", 0, 1);
  2479   2557   #endif
  2480   2558   
         2559  +  if( g.zErrlog==0 ){
         2560  +    g.zErrlog = "-";
         2561  +  }
  2481   2562     zFileGlob = find_option("files-urlenc",0,1);
  2482   2563     if( zFileGlob ){
  2483   2564       char *z = mprintf("%s", zFileGlob);
  2484   2565       dehttpize(z);
  2485   2566       zFileGlob = z;
  2486   2567     }else{
  2487   2568       zFileGlob = find_option("files",0,1);
................................................................................
  2496   2577     zPort = find_option("port", "P", 1);
  2497   2578     isUiCmd = g.argv[1][0]=='u';
  2498   2579     if( isUiCmd ){
  2499   2580       zInitPage = find_option("page", 0, 1);
  2500   2581     }
  2501   2582     zNotFound = find_option("notfound", 0, 1);
  2502   2583     allowRepoList = find_option("repolist",0,0)!=0;
         2584  +  if( find_option("nocompress",0,0)!=0 ) g.fNoHttpCompress = 1;
  2503   2585     zAltBase = find_option("baseurl", 0, 1);
  2504   2586     fCreate = find_option("create",0,0)!=0;
  2505   2587     if( find_option("scgi", 0, 0)!=0 ) flags |= HTTP_SERVER_SCGI;
  2506   2588     if( zAltBase ){
  2507   2589       set_base_url(zAltBase);
  2508   2590     }
  2509   2591     g.sslNotAvailable = find_option("nossl", 0, 0)!=0;
................................................................................
  2591   2673                               zBrowser, zIpAddr, zInitPage);
  2592   2674       }
  2593   2675     }
  2594   2676     if( g.repositoryOpen ) flags |= HTTP_SERVER_HAD_REPOSITORY;
  2595   2677     if( g.localOpen ) flags |= HTTP_SERVER_HAD_CHECKOUT;
  2596   2678     db_close(1);
  2597   2679     if( cgi_http_server(iPort, mxPort, zBrowserCmd, zIpAddr, flags) ){
  2598         -    fossil_fatal("unable to listen on TCP socket %d", iPort);
         2680  +    fossil_panic("unable to listen on TCP socket %d", iPort);
  2599   2681     }
  2600   2682     if( zMaxLatency ){
  2601   2683       signal(SIGALRM, sigalrm_handler);
  2602   2684       alarm(atoi(zMaxLatency));
  2603   2685     }
  2604   2686     g.httpIn = stdin;
  2605   2687     g.httpOut = stdout;
  2606         -  if( g.fHttpTrace || g.fSqlTrace ){
  2607         -    fprintf(stderr, "====== SERVER pid %d =======\n", getpid());
         2688  +
         2689  +#if !defined(_WIN32)
         2690  +  signal(SIGSEGV, sigsegv_handler);
         2691  +  signal(SIGPIPE, sigpipe_handler);
         2692  +#endif
         2693  +
         2694  +  if( g.fAnyTrace ){
         2695  +    fprintf(stderr, "/***** Subprocess %d *****/\n", getpid());
  2608   2696     }
  2609   2697     g.cgiOutput = 1;
  2610   2698     find_server_repository(2, 0);
  2611   2699     if( fossil_strcmp(g.zRepositoryName,"/")==0 ){
  2612   2700       allowRepoList = 1;
  2613   2701     }else{
  2614   2702       g.zRepositoryName = enter_chroot_jail(g.zRepositoryName, noJail);
................................................................................
  2615   2703     }
  2616   2704     if( flags & HTTP_SERVER_SCGI ){
  2617   2705       cgi_handle_scgi_request();
  2618   2706     }else{
  2619   2707       cgi_handle_http_request(0);
  2620   2708     }
  2621   2709     process_one_web_page(zNotFound, glob_create(zFileGlob), allowRepoList);
         2710  +  if( g.fAnyTrace ){
         2711  +    fprintf(stderr, "/***** Webpage finished in subprocess %d *****/\n",
         2712  +            getpid());
         2713  +  }
  2622   2714   #else
  2623   2715     /* Win32 implementation */
  2624   2716     if( isUiCmd ){
  2625   2717       zBrowser = db_get("web-browser", "start");
  2626   2718       if( zIpAddr==0 ){
  2627   2719         zBrowserCmd = mprintf("%s http://localhost:%%d/%s &",
  2628   2720                               zBrowser, zInitPage);
................................................................................
  2673   2765         for(j=0; (c = z[j])!=0; j++){
  2674   2766           fossil_print("%02x", c);
  2675   2767         }
  2676   2768         fossil_print("]\n");
  2677   2769       }
  2678   2770     }
  2679   2771   }
         2772  +
         2773  +/*
         2774  +** WEBPAGE: test-warning
         2775  +**
         2776  +** Test error and warning log operation.  This webpage is accessible to
         2777  +** the administrator only.
         2778  +**
         2779  +**     case=1           Issue a fossil_warning() while generating the page.
         2780  +**     case=2           Extra db_begin_transaction()
         2781  +**     case=3           Extra db_end_transaction()
         2782  +*/
         2783  +void test_warning_page(void){
         2784  +  int iCase = atoi(PD("case","0"));
         2785  +  int i;
         2786  +  login_check_credentials();
         2787  +  if( !g.perm.Setup && !g.perm.Admin ){
         2788  +    login_needed(0);
         2789  +    return;
         2790  +  }
         2791  +  style_header("Warning Test Page");
         2792  +  style_submenu_element("Error Log","%R/errorlog");
         2793  +  if( iCase<1 || iCase>4 ){
         2794  +    @ <p>Generate a message to the <a href="%R/errorlog">error log</a>
         2795  +    @ by clicking on one of the following cases:
         2796  +  }else{
         2797  +    @ <p>This is the test page for case=%d(iCase).  All possible cases:
         2798  +  }
         2799  +  for(i=1; i<=4; i++){
         2800  +    @ <a href='./test-warning?case=%d(i)'>[%d(i)]</a>
         2801  +  }
         2802  +  @ </p>
         2803  +  @ <p><ol>
         2804  +  @ <li value='1'> Call fossil_warning()
         2805  +  if( iCase==1 ){
         2806  +    fossil_warning("Test warning message from /test-warning");
         2807  +  }
         2808  +  @ <li value='2'> Call db_begin_transaction()
         2809  +  if( iCase==2 ){
         2810  +    db_begin_transaction();
         2811  +  }
         2812  +  @ <li value='3'> Call db_end_transaction()
         2813  +  if( iCase==3 ){
         2814  +    db_end_transaction(0);
         2815  +  }
         2816  +  @ <li value='4'> warning during SQL
         2817  +  if( iCase==4 ){
         2818  +    Stmt q;
         2819  +    db_prepare(&q, "SELECT uuid FROM blob LIMIT 5");
         2820  +    db_step(&q);
         2821  +    sqlite3_log(SQLITE_ERROR, "Test warning message during SQL");
         2822  +    db_finalize(&q);
         2823  +  }
         2824  +  @ </ol>
         2825  +  @ <p>End of test</p>
         2826  +  style_footer();
         2827  +}

Changes to src/main.mk.

    40     40     $(SRCDIR)/delta.c \
    41     41     $(SRCDIR)/deltacmd.c \
    42     42     $(SRCDIR)/descendants.c \
    43     43     $(SRCDIR)/diff.c \
    44     44     $(SRCDIR)/diffcmd.c \
    45     45     $(SRCDIR)/dispatch.c \
    46     46     $(SRCDIR)/doc.c \
           47  +  $(SRCDIR)/email.c \
    47     48     $(SRCDIR)/encode.c \
    48     49     $(SRCDIR)/etag.c \
    49     50     $(SRCDIR)/event.c \
    50     51     $(SRCDIR)/export.c \
    51     52     $(SRCDIR)/file.c \
    52     53     $(SRCDIR)/finfo.c \
    53     54     $(SRCDIR)/foci.c \
           55  +  $(SRCDIR)/forum.c \
    54     56     $(SRCDIR)/fshell.c \
    55     57     $(SRCDIR)/fusefs.c \
    56     58     $(SRCDIR)/glob.c \
    57     59     $(SRCDIR)/graph.c \
    58     60     $(SRCDIR)/gzip.c \
    59     61     $(SRCDIR)/hname.c \
    60     62     $(SRCDIR)/http.c \
................................................................................
   109    111     $(SRCDIR)/setup.c \
   110    112     $(SRCDIR)/sha1.c \
   111    113     $(SRCDIR)/sha1hard.c \
   112    114     $(SRCDIR)/sha3.c \
   113    115     $(SRCDIR)/shun.c \
   114    116     $(SRCDIR)/sitemap.c \
   115    117     $(SRCDIR)/skins.c \
          118  +  $(SRCDIR)/smtp.c \
   116    119     $(SRCDIR)/sqlcmd.c \
   117    120     $(SRCDIR)/stash.c \
   118    121     $(SRCDIR)/stat.c \
   119    122     $(SRCDIR)/statrep.c \
   120    123     $(SRCDIR)/style.c \
   121    124     $(SRCDIR)/sync.c \
   122    125     $(SRCDIR)/tag.c \
................................................................................
   131    134     $(SRCDIR)/update.c \
   132    135     $(SRCDIR)/url.c \
   133    136     $(SRCDIR)/user.c \
   134    137     $(SRCDIR)/utf8.c \
   135    138     $(SRCDIR)/util.c \
   136    139     $(SRCDIR)/verify.c \
   137    140     $(SRCDIR)/vfile.c \
          141  +  $(SRCDIR)/webmail.c \
   138    142     $(SRCDIR)/wiki.c \
   139    143     $(SRCDIR)/wikiformat.c \
   140    144     $(SRCDIR)/winfile.c \
   141    145     $(SRCDIR)/winhttp.c \
   142    146     $(SRCDIR)/wysiwyg.c \
   143    147     $(SRCDIR)/xfer.c \
   144    148     $(SRCDIR)/xfersetup.c \
................................................................................
   241    245     $(OBJDIR)/delta_.c \
   242    246     $(OBJDIR)/deltacmd_.c \
   243    247     $(OBJDIR)/descendants_.c \
   244    248     $(OBJDIR)/diff_.c \
   245    249     $(OBJDIR)/diffcmd_.c \
   246    250     $(OBJDIR)/dispatch_.c \
   247    251     $(OBJDIR)/doc_.c \
          252  +  $(OBJDIR)/email_.c \
   248    253     $(OBJDIR)/encode_.c \
   249    254     $(OBJDIR)/etag_.c \
   250    255     $(OBJDIR)/event_.c \
   251    256     $(OBJDIR)/export_.c \
   252    257     $(OBJDIR)/file_.c \
   253    258     $(OBJDIR)/finfo_.c \
   254    259     $(OBJDIR)/foci_.c \
          260  +  $(OBJDIR)/forum_.c \
   255    261     $(OBJDIR)/fshell_.c \
   256    262     $(OBJDIR)/fusefs_.c \
   257    263     $(OBJDIR)/glob_.c \
   258    264     $(OBJDIR)/graph_.c \
   259    265     $(OBJDIR)/gzip_.c \
   260    266     $(OBJDIR)/hname_.c \
   261    267     $(OBJDIR)/http_.c \
................................................................................
   310    316     $(OBJDIR)/setup_.c \
   311    317     $(OBJDIR)/sha1_.c \
   312    318     $(OBJDIR)/sha1hard_.c \
   313    319     $(OBJDIR)/sha3_.c \
   314    320     $(OBJDIR)/shun_.c \
   315    321     $(OBJDIR)/sitemap_.c \
   316    322     $(OBJDIR)/skins_.c \
          323  +  $(OBJDIR)/smtp_.c \
   317    324     $(OBJDIR)/sqlcmd_.c \
   318    325     $(OBJDIR)/stash_.c \
   319    326     $(OBJDIR)/stat_.c \
   320    327     $(OBJDIR)/statrep_.c \
   321    328     $(OBJDIR)/style_.c \
   322    329     $(OBJDIR)/sync_.c \
   323    330     $(OBJDIR)/tag_.c \
................................................................................
   332    339     $(OBJDIR)/update_.c \
   333    340     $(OBJDIR)/url_.c \
   334    341     $(OBJDIR)/user_.c \
   335    342     $(OBJDIR)/utf8_.c \
   336    343     $(OBJDIR)/util_.c \
   337    344     $(OBJDIR)/verify_.c \
   338    345     $(OBJDIR)/vfile_.c \
          346  +  $(OBJDIR)/webmail_.c \
   339    347     $(OBJDIR)/wiki_.c \
   340    348     $(OBJDIR)/wikiformat_.c \
   341    349     $(OBJDIR)/winfile_.c \
   342    350     $(OBJDIR)/winhttp_.c \
   343    351     $(OBJDIR)/wysiwyg_.c \
   344    352     $(OBJDIR)/xfer_.c \
   345    353     $(OBJDIR)/xfersetup_.c \
................................................................................
   371    379    $(OBJDIR)/delta.o \
   372    380    $(OBJDIR)/deltacmd.o \
   373    381    $(OBJDIR)/descendants.o \
   374    382    $(OBJDIR)/diff.o \
   375    383    $(OBJDIR)/diffcmd.o \
   376    384    $(OBJDIR)/dispatch.o \
   377    385    $(OBJDIR)/doc.o \
          386  + $(OBJDIR)/email.o \
   378    387    $(OBJDIR)/encode.o \
   379    388    $(OBJDIR)/etag.o \
   380    389    $(OBJDIR)/event.o \
   381    390    $(OBJDIR)/export.o \
   382    391    $(OBJDIR)/file.o \
   383    392    $(OBJDIR)/finfo.o \
   384    393    $(OBJDIR)/foci.o \
          394  + $(OBJDIR)/forum.o \
   385    395    $(OBJDIR)/fshell.o \
   386    396    $(OBJDIR)/fusefs.o \
   387    397    $(OBJDIR)/glob.o \
   388    398    $(OBJDIR)/graph.o \
   389    399    $(OBJDIR)/gzip.o \
   390    400    $(OBJDIR)/hname.o \
   391    401    $(OBJDIR)/http.o \
................................................................................
   440    450    $(OBJDIR)/setup.o \
   441    451    $(OBJDIR)/sha1.o \
   442    452    $(OBJDIR)/sha1hard.o \
   443    453    $(OBJDIR)/sha3.o \
   444    454    $(OBJDIR)/shun.o \
   445    455    $(OBJDIR)/sitemap.o \
   446    456    $(OBJDIR)/skins.o \
          457  + $(OBJDIR)/smtp.o \
   447    458    $(OBJDIR)/sqlcmd.o \
   448    459    $(OBJDIR)/stash.o \
   449    460    $(OBJDIR)/stat.o \
   450    461    $(OBJDIR)/statrep.o \
   451    462    $(OBJDIR)/style.o \
   452    463    $(OBJDIR)/sync.o \
   453    464    $(OBJDIR)/tag.o \
................................................................................
   462    473    $(OBJDIR)/update.o \
   463    474    $(OBJDIR)/url.o \
   464    475    $(OBJDIR)/user.o \
   465    476    $(OBJDIR)/utf8.o \
   466    477    $(OBJDIR)/util.o \
   467    478    $(OBJDIR)/verify.o \
   468    479    $(OBJDIR)/vfile.o \
          480  + $(OBJDIR)/webmail.o \
   469    481    $(OBJDIR)/wiki.o \
   470    482    $(OBJDIR)/wikiformat.o \
   471    483    $(OBJDIR)/winfile.o \
   472    484    $(OBJDIR)/winhttp.o \
   473    485    $(OBJDIR)/wysiwyg.o \
   474    486    $(OBJDIR)/xfer.o \
   475    487    $(OBJDIR)/xfersetup.o \
................................................................................
   699    711   	$(OBJDIR)/delta_.c:$(OBJDIR)/delta.h \
   700    712   	$(OBJDIR)/deltacmd_.c:$(OBJDIR)/deltacmd.h \
   701    713   	$(OBJDIR)/descendants_.c:$(OBJDIR)/descendants.h \
   702    714   	$(OBJDIR)/diff_.c:$(OBJDIR)/diff.h \
   703    715   	$(OBJDIR)/diffcmd_.c:$(OBJDIR)/diffcmd.h \
   704    716   	$(OBJDIR)/dispatch_.c:$(OBJDIR)/dispatch.h \
   705    717   	$(OBJDIR)/doc_.c:$(OBJDIR)/doc.h \
          718  +	$(OBJDIR)/email_.c:$(OBJDIR)/email.h \
   706    719   	$(OBJDIR)/encode_.c:$(OBJDIR)/encode.h \
   707    720   	$(OBJDIR)/etag_.c:$(OBJDIR)/etag.h \
   708    721   	$(OBJDIR)/event_.c:$(OBJDIR)/event.h \
   709    722   	$(OBJDIR)/export_.c:$(OBJDIR)/export.h \
   710    723   	$(OBJDIR)/file_.c:$(OBJDIR)/file.h \
   711    724   	$(OBJDIR)/finfo_.c:$(OBJDIR)/finfo.h \
   712    725   	$(OBJDIR)/foci_.c:$(OBJDIR)/foci.h \
          726  +	$(OBJDIR)/forum_.c:$(OBJDIR)/forum.h \
   713    727   	$(OBJDIR)/fshell_.c:$(OBJDIR)/fshell.h \
   714    728   	$(OBJDIR)/fusefs_.c:$(OBJDIR)/fusefs.h \
   715    729   	$(OBJDIR)/glob_.c:$(OBJDIR)/glob.h \
   716    730   	$(OBJDIR)/graph_.c:$(OBJDIR)/graph.h \
   717    731   	$(OBJDIR)/gzip_.c:$(OBJDIR)/gzip.h \
   718    732   	$(OBJDIR)/hname_.c:$(OBJDIR)/hname.h \
   719    733   	$(OBJDIR)/http_.c:$(OBJDIR)/http.h \
................................................................................
   768    782   	$(OBJDIR)/setup_.c:$(OBJDIR)/setup.h \
   769    783   	$(OBJDIR)/sha1_.c:$(OBJDIR)/sha1.h \
   770    784   	$(OBJDIR)/sha1hard_.c:$(OBJDIR)/sha1hard.h \
   771    785   	$(OBJDIR)/sha3_.c:$(OBJDIR)/sha3.h \
   772    786   	$(OBJDIR)/shun_.c:$(OBJDIR)/shun.h \
   773    787   	$(OBJDIR)/sitemap_.c:$(OBJDIR)/sitemap.h \
   774    788   	$(OBJDIR)/skins_.c:$(OBJDIR)/skins.h \
          789  +	$(OBJDIR)/smtp_.c:$(OBJDIR)/smtp.h \
   775    790   	$(OBJDIR)/sqlcmd_.c:$(OBJDIR)/sqlcmd.h \
   776    791   	$(OBJDIR)/stash_.c:$(OBJDIR)/stash.h \
   777    792   	$(OBJDIR)/stat_.c:$(OBJDIR)/stat.h \
   778    793   	$(OBJDIR)/statrep_.c:$(OBJDIR)/statrep.h \
   779    794   	$(OBJDIR)/style_.c:$(OBJDIR)/style.h \
   780    795   	$(OBJDIR)/sync_.c:$(OBJDIR)/sync.h \
   781    796   	$(OBJDIR)/tag_.c:$(OBJDIR)/tag.h \
................................................................................
   790    805   	$(OBJDIR)/update_.c:$(OBJDIR)/update.h \
   791    806   	$(OBJDIR)/url_.c:$(OBJDIR)/url.h \
   792    807   	$(OBJDIR)/user_.c:$(OBJDIR)/user.h \
   793    808   	$(OBJDIR)/utf8_.c:$(OBJDIR)/utf8.h \
   794    809   	$(OBJDIR)/util_.c:$(OBJDIR)/util.h \
   795    810   	$(OBJDIR)/verify_.c:$(OBJDIR)/verify.h \
   796    811   	$(OBJDIR)/vfile_.c:$(OBJDIR)/vfile.h \
          812  +	$(OBJDIR)/webmail_.c:$(OBJDIR)/webmail.h \
   797    813   	$(OBJDIR)/wiki_.c:$(OBJDIR)/wiki.h \
   798    814   	$(OBJDIR)/wikiformat_.c:$(OBJDIR)/wikiformat.h \
   799    815   	$(OBJDIR)/winfile_.c:$(OBJDIR)/winfile.h \
   800    816   	$(OBJDIR)/winhttp_.c:$(OBJDIR)/winhttp.h \
   801    817   	$(OBJDIR)/wysiwyg_.c:$(OBJDIR)/wysiwyg.h \
   802    818   	$(OBJDIR)/xfer_.c:$(OBJDIR)/xfer.h \
   803    819   	$(OBJDIR)/xfersetup_.c:$(OBJDIR)/xfersetup.h \
................................................................................
  1036   1052   $(OBJDIR)/doc_.c:	$(SRCDIR)/doc.c $(OBJDIR)/translate
  1037   1053   	$(OBJDIR)/translate $(SRCDIR)/doc.c >$@
  1038   1054   
  1039   1055   $(OBJDIR)/doc.o:	$(OBJDIR)/doc_.c $(OBJDIR)/doc.h $(SRCDIR)/config.h
  1040   1056   	$(XTCC) -o $(OBJDIR)/doc.o -c $(OBJDIR)/doc_.c
  1041   1057   
  1042   1058   $(OBJDIR)/doc.h:	$(OBJDIR)/headers
         1059  +
         1060  +$(OBJDIR)/email_.c:	$(SRCDIR)/email.c $(OBJDIR)/translate
         1061  +	$(OBJDIR)/translate $(SRCDIR)/email.c >$@
         1062  +
         1063  +$(OBJDIR)/email.o:	$(OBJDIR)/email_.c $(OBJDIR)/email.h $(SRCDIR)/config.h
         1064  +	$(XTCC) -o $(OBJDIR)/email.o -c $(OBJDIR)/email_.c
         1065  +
         1066  +$(OBJDIR)/email.h:	$(OBJDIR)/headers
  1043   1067   
  1044   1068   $(OBJDIR)/encode_.c:	$(SRCDIR)/encode.c $(OBJDIR)/translate
  1045   1069   	$(OBJDIR)/translate $(SRCDIR)/encode.c >$@
  1046   1070   
  1047   1071   $(OBJDIR)/encode.o:	$(OBJDIR)/encode_.c $(OBJDIR)/encode.h $(SRCDIR)/config.h
  1048   1072   	$(XTCC) -o $(OBJDIR)/encode.o -c $(OBJDIR)/encode_.c
  1049   1073   
................................................................................
  1092   1116   $(OBJDIR)/foci_.c:	$(SRCDIR)/foci.c $(OBJDIR)/translate
  1093   1117   	$(OBJDIR)/translate $(SRCDIR)/foci.c >$@
  1094   1118   
  1095   1119   $(OBJDIR)/foci.o:	$(OBJDIR)/foci_.c $(OBJDIR)/foci.h $(SRCDIR)/config.h
  1096   1120   	$(XTCC) -o $(OBJDIR)/foci.o -c $(OBJDIR)/foci_.c
  1097   1121   
  1098   1122   $(OBJDIR)/foci.h:	$(OBJDIR)/headers
         1123  +
         1124  +$(OBJDIR)/forum_.c:	$(SRCDIR)/forum.c $(OBJDIR)/translate
         1125  +	$(OBJDIR)/translate $(SRCDIR)/forum.c >$@
         1126  +
         1127  +$(OBJDIR)/forum.o:	$(OBJDIR)/forum_.c $(OBJDIR)/forum.h $(SRCDIR)/config.h
         1128  +	$(XTCC) -o $(OBJDIR)/forum.o -c $(OBJDIR)/forum_.c
         1129  +
         1130  +$(OBJDIR)/forum.h:	$(OBJDIR)/headers
  1099   1131   
  1100   1132   $(OBJDIR)/fshell_.c:	$(SRCDIR)/fshell.c $(OBJDIR)/translate
  1101   1133   	$(OBJDIR)/translate $(SRCDIR)/fshell.c >$@
  1102   1134   
  1103   1135   $(OBJDIR)/fshell.o:	$(OBJDIR)/fshell_.c $(OBJDIR)/fshell.h $(SRCDIR)/config.h
  1104   1136   	$(XTCC) -o $(OBJDIR)/fshell.o -c $(OBJDIR)/fshell_.c
  1105   1137   
................................................................................
  1588   1620   $(OBJDIR)/skins_.c:	$(SRCDIR)/skins.c $(OBJDIR)/translate
  1589   1621   	$(OBJDIR)/translate $(SRCDIR)/skins.c >$@
  1590   1622   
  1591   1623   $(OBJDIR)/skins.o:	$(OBJDIR)/skins_.c $(OBJDIR)/skins.h $(SRCDIR)/config.h
  1592   1624   	$(XTCC) -o $(OBJDIR)/skins.o -c $(OBJDIR)/skins_.c
  1593   1625   
  1594   1626   $(OBJDIR)/skins.h:	$(OBJDIR)/headers
         1627  +
         1628  +$(OBJDIR)/smtp_.c:	$(SRCDIR)/smtp.c $(OBJDIR)/translate
         1629  +	$(OBJDIR)/translate $(SRCDIR)/smtp.c >$@
         1630  +
         1631  +$(OBJDIR)/smtp.o:	$(OBJDIR)/smtp_.c $(OBJDIR)/smtp.h $(SRCDIR)/config.h
         1632  +	$(XTCC) -o $(OBJDIR)/smtp.o -c $(OBJDIR)/smtp_.c
         1633  +
         1634  +$(OBJDIR)/smtp.h:	$(OBJDIR)/headers
  1595   1635   
  1596   1636   $(OBJDIR)/sqlcmd_.c:	$(SRCDIR)/sqlcmd.c $(OBJDIR)/translate
  1597   1637   	$(OBJDIR)/translate $(SRCDIR)/sqlcmd.c >$@
  1598   1638   
  1599   1639   $(OBJDIR)/sqlcmd.o:	$(OBJDIR)/sqlcmd_.c $(OBJDIR)/sqlcmd.h $(SRCDIR)/config.h
  1600   1640   	$(XTCC) -o $(OBJDIR)/sqlcmd.o -c $(OBJDIR)/sqlcmd_.c
  1601   1641   
................................................................................
  1764   1804   $(OBJDIR)/vfile_.c:	$(SRCDIR)/vfile.c $(OBJDIR)/translate
  1765   1805   	$(OBJDIR)/translate $(SRCDIR)/vfile.c >$@
  1766   1806   
  1767   1807   $(OBJDIR)/vfile.o:	$(OBJDIR)/vfile_.c $(OBJDIR)/vfile.h $(SRCDIR)/config.h
  1768   1808   	$(XTCC) -o $(OBJDIR)/vfile.o -c $(OBJDIR)/vfile_.c
  1769   1809   
  1770   1810   $(OBJDIR)/vfile.h:	$(OBJDIR)/headers
         1811  +
         1812  +$(OBJDIR)/webmail_.c:	$(SRCDIR)/webmail.c $(OBJDIR)/translate
         1813  +	$(OBJDIR)/translate $(SRCDIR)/webmail.c >$@
         1814  +
         1815  +$(OBJDIR)/webmail.o:	$(OBJDIR)/webmail_.c $(OBJDIR)/webmail.h $(SRCDIR)/config.h
         1816  +	$(XTCC) -o $(OBJDIR)/webmail.o -c $(OBJDIR)/webmail_.c
         1817  +
         1818  +$(OBJDIR)/webmail.h:	$(OBJDIR)/headers
  1771   1819   
  1772   1820   $(OBJDIR)/wiki_.c:	$(SRCDIR)/wiki.c $(OBJDIR)/translate
  1773   1821   	$(OBJDIR)/translate $(SRCDIR)/wiki.c >$@
  1774   1822   
  1775   1823   $(OBJDIR)/wiki.o:	$(OBJDIR)/wiki_.c $(OBJDIR)/wiki.h $(SRCDIR)/config.h
  1776   1824   	$(XTCC) -o $(OBJDIR)/wiki.o -c $(OBJDIR)/wiki_.c
  1777   1825   

Changes to src/makemake.tcl.

    52     52     delta
    53     53     deltacmd
    54     54     descendants
    55     55     diff
    56     56     diffcmd
    57     57     dispatch
    58     58     doc
           59  +  email
    59     60     encode
    60     61     etag
    61     62     event
    62     63     export
    63     64     file
    64     65     finfo
    65     66     foci
           67  +  forum
    66     68     fshell
    67     69     fusefs
    68     70     glob
    69     71     graph
    70     72     gzip
    71     73     hname
    72     74     http
................................................................................
   120    122     setup
   121    123     sha1
   122    124     sha1hard
   123    125     sha3
   124    126     shun
   125    127     sitemap
   126    128     skins
          129  +  smtp
   127    130     sqlcmd
   128    131     stash
   129    132     stat
   130    133     statrep
   131    134     style
   132    135     sync
   133    136     tag
................................................................................
   142    145     update
   143    146     url
   144    147     user
   145    148     utf8
   146    149     util
   147    150     verify
   148    151     vfile
          152  +  webmail
   149    153     wiki
   150    154     wikiformat
   151    155     winfile
   152    156     winhttp
   153    157     wysiwyg
   154    158     xfer
   155    159     xfersetup
................................................................................
   597    601   
   598    602   #### Enable relative paths in external diff/gdiff
   599    603   #
   600    604   # FOSSIL_ENABLE_EXEC_REL_PATHS = 1
   601    605   
   602    606   #### Enable legacy treatment of mv/rm (skip checkout files)
   603    607   #
   604         -# FOSSIL_ENABLE_LEGACY_MV_RM = 1
          608  +FOSSIL_ENABLE_LEGACY_MV_RM = 1
   605    609   
   606    610   #### Enable TH1 scripts in embedded documentation files
   607    611   #
   608    612   # FOSSIL_ENABLE_TH1_DOCS = 1
   609    613   
   610    614   #### Enable hooks for commands and web pages via TH1
   611    615   #
................................................................................
   933    937   LIB += -lkernel32 -lws2_32
   934    938   else
   935    939   LIB += -lnetapi32 -lkernel32 -luser32 -ladvapi32 -lws2_32
   936    940   endif
   937    941   else
   938    942   LIB += -lkernel32 -lws2_32
   939    943   endif
          944  +
          945  +#### Library required for DNS lookups.
          946  +#
          947  +LIB += -ldnsapi
   940    948   
   941    949   #### Tcl shell for use in running the fossil test suite.  This is only
   942    950   #    used for testing.
   943    951   #
   944    952   TCLSH = tclsh
   945    953   
   946    954   #### Nullsoft installer MakeNSIS location
................................................................................
  1307   1315   
  1308   1316   #SSL   =  -DFOSSIL_ENABLE_SSL=1
  1309   1317   SSL    =
  1310   1318   
  1311   1319   CFLAGS = -o
  1312   1320   BCC    = $(DMDIR)\bin\dmc $(CFLAGS)
  1313   1321   TCC    = $(DMDIR)\bin\dmc $(CFLAGS) $(DMCDEF) $(SSL) $(INCL)
  1314         -LIBS   = $(DMDIR)\extra\lib\ zlib wsock32 advapi32
         1322  +LIBS   = $(DMDIR)\extra\lib\ zlib wsock32 advapi32 dnsapi
  1315   1323   }
  1316   1324   writeln "SQLITE_OPTIONS = [join $SQLITE_OPTIONS { }]\n"
  1317   1325   writeln "SHELL_OPTIONS = [join $SHELL_WIN32_OPTIONS { }]\n"
  1318   1326   writeln -nonewline "SRC   ="
  1319   1327   foreach s [lsort $src] {
  1320   1328     writeln -nonewline " ${s}_.c"
  1321   1329   }
................................................................................
  1513   1521   # Enable the JSON API?
  1514   1522   !ifndef FOSSIL_ENABLE_JSON
  1515   1523   FOSSIL_ENABLE_JSON = 0
  1516   1524   !endif
  1517   1525   
  1518   1526   # Enable legacy treatment of the mv/rm commands?
  1519   1527   !ifndef FOSSIL_ENABLE_LEGACY_MV_RM
  1520         -FOSSIL_ENABLE_LEGACY_MV_RM = 0
         1528  +FOSSIL_ENABLE_LEGACY_MV_RM = 1
  1521   1529   !endif
  1522   1530   
  1523   1531   # Enable use of miniz instead of zlib?
  1524   1532   !ifndef FOSSIL_ENABLE_MINIZ
  1525   1533   FOSSIL_ENABLE_MINIZ = 0
  1526   1534   !endif
  1527   1535   
................................................................................
  1699   1707   CFLAGS    = $(CFLAGS) $(CRTFLAGS) /O2
  1700   1708   !endif
  1701   1709   
  1702   1710   BCC       = $(CC) $(CFLAGS)
  1703   1711   TCC       = $(CC) /c $(CFLAGS) $(MSCDEF) $(INCL)
  1704   1712   RCC       = $(RC) /D_WIN32 /D_MSC_VER $(MSCDEF) $(INCL)
  1705   1713   MTC       = mt
  1706         -LIBS      = ws2_32.lib advapi32.lib
         1714  +LIBS      = ws2_32.lib advapi32.lib dnsapi.lib
  1707   1715   LIBDIR    =
  1708   1716   
  1709   1717   !if $(FOSSIL_DYNAMIC_BUILD)!=0
  1710   1718   TCC       = $(TCC) /DFOSSIL_DYNAMIC_BUILD=1
  1711   1719   RCC       = $(RCC) /DFOSSIL_DYNAMIC_BUILD=1
  1712   1720   !endif
  1713   1721   
................................................................................
  2092   2100   B=..
  2093   2101   SRCDIR=$(B)/src/
  2094   2102   WINDIR=$(B)/win/
  2095   2103   ZLIBSRCDIR=../../zlib/
  2096   2104   
  2097   2105   # define linker command and options
  2098   2106   LINK=$(PellesCDir)/bin/polink.exe
  2099         -LINKFLAGS=-subsystem:console -machine:$(TARGETMACHINE_LN) /LIBPATH:$(PellesCDir)\lib\win$(TARGETEXTEND) /LIBPATH:$(PellesCDir)\lib kernel32.lib advapi32.lib delayimp$(TARGETEXTEND).lib Wsock32.lib Crtmt$(TARGETEXTEND).lib
         2107  +LINKFLAGS=-subsystem:console -machine:$(TARGETMACHINE_LN) /LIBPATH:$(PellesCDir)\lib\win$(TARGETEXTEND) /LIBPATH:$(PellesCDir)\lib kernel32.lib advapi32.lib delayimp$(TARGETEXTEND).lib Wsock32.lib dnsapi.lib Crtmt$(TARGETEXTEND).lib
  2100   2108   
  2101   2109   # define standard C-compiler and flags, used to compile
  2102   2110   # the fossil binary. Some special definitions follow for
  2103   2111   # special files follow
  2104   2112   CC=$(PellesCDir)\bin\pocc.exe
  2105   2113   DEFINES=-D_pgmptr=g.argv[0]
  2106   2114   CCFLAGS=-T$(TARGETMACHINE_CC)-coff -Ot -W2 -Gd -Go -Ze -MT $(DEFINES)

Changes to src/manifest.c.

  1057   1057         if( !throwError ){
  1058   1058           db_multi_exec(
  1059   1059              "INSERT OR IGNORE INTO orphan(rid, baseline) VALUES(%d,%d)",
  1060   1060              p->rid, rid
  1061   1061           );
  1062   1062           return 1;
  1063   1063         }
  1064         -      fossil_fatal("cannot access baseline manifest %S", p->zBaseline);
         1064  +      fossil_panic("cannot access baseline manifest %S", p->zBaseline);
  1065   1065       }
  1066   1066     }
  1067   1067     return 0;
  1068   1068   }
  1069   1069   
  1070   1070   /*
  1071   1071   ** Rewind a manifest-file iterator back to the beginning of the manifest.
................................................................................
  1752   1752       db_reset(&u);
  1753   1753     }
  1754   1754     db_finalize(&q);
  1755   1755     db_finalize(&u);
  1756   1756     if( db_exists("SELECT 1 FROM time_fudge") ){
  1757   1757       db_multi_exec(
  1758   1758         "UPDATE event SET mtime=(SELECT m1 FROM time_fudge WHERE mid=objid)"
  1759         -      " WHERE objid IN (SELECT mid FROM time_fudge);"
         1759  +      " WHERE objid IN (SELECT mid FROM time_fudge)"
         1760  +      " AND (mtime=omtime OR omtime IS NULL)"
  1760   1761       );
  1761   1762     }
  1762   1763     db_multi_exec("DROP TABLE time_fudge;");
  1763   1764   
  1764   1765     db_end_transaction(0);
  1765   1766     manifest_crosslink_busy = 0;
  1766   1767     return ( rc!=TH_ERROR );
................................................................................
  1927   1928     int i, rc = TH_OK;
  1928   1929     Manifest *p;
  1929   1930     int parentid = 0;
  1930   1931     int permitHooks = (flags & MC_PERMIT_HOOKS);
  1931   1932     const char *zScript = 0;
  1932   1933     const char *zUuid = 0;
  1933   1934   
         1935  +  if( g.fSqlTrace ){
         1936  +    fossil_trace("-- manifest_crosslink(%d)\n", rid);
         1937  +  }
  1934   1938     if( (p = manifest_cache_find(rid))!=0 ){
  1935   1939       blob_reset(pContent);
  1936   1940     }else if( (p = manifest_parse(pContent, rid, 0))==0 ){
  1937   1941       assert( blob_is_reset(pContent) || pContent==0 );
  1938   1942       if( (flags & MC_NO_ERRORS)==0 ){
  1939   1943         fossil_error(1, "syntax error in manifest [%S]",
  1940   1944                      db_text(0, "SELECT uuid FROM blob WHERE rid=%d",rid));

Changes to src/merge.c.

   285    285       ** leaf that is (1) not the version currently checked out and (2)
   286    286       ** has not already been merged into the current checkout and (3)
   287    287       ** the leaf is not closed and (4) the leaf is in the same branch
   288    288       ** as the current checkout.
   289    289       */
   290    290       Stmt q;
   291    291       if( pickFlag || backoutFlag || integrateFlag){
   292         -      fossil_fatal("cannot use --backout, --cherrypick or --integrate with a fork merge");
          292  +      fossil_fatal("cannot use --backout, --cherrypick or --integrate "
          293  +                   "with a fork merge");
   293    294       }
   294    295       mid = fossil_find_nearest_fork(vid, db_open_local(0));
   295    296       if( mid==0 ){
   296    297         fossil_fatal("no unmerged forks of branch \"%s\"",
   297    298           db_text(0, "SELECT value FROM tagxref"
   298    299                      " WHERE tagid=%d AND rid=%d AND tagtype>0",
   299    300                      TAG_BRANCH, vid)
................................................................................
   323    324   
   324    325     if( zPivot ){
   325    326       pid = name_to_typed_rid(zPivot, "ci");
   326    327       if( pid==0 || !is_a_version(pid) ){
   327    328         fossil_fatal("not a version: %s", zPivot);
   328    329       }
   329    330       if( pickFlag ){
   330         -      fossil_fatal("incompatible options: --cherrypick & --baseline");
          331  +      fossil_fatal("incompatible options: --cherrypick and --baseline");
   331    332       }
   332    333     }
   333    334     if( pickFlag || backoutFlag ){
   334    335       if( integrateFlag ){
   335         -      fossil_fatal("incompatible options: --integrate & --cherrypick or --backout");
          336  +      fossil_fatal("incompatible options: --integrate and --cherrypick "
          337  +                   "with --backout");
   336    338       }
   337    339       pid = db_int(0, "SELECT pid FROM plink WHERE cid=%d AND isprim", mid);
   338    340       if( pid<=0 ){
   339    341         fossil_fatal("cannot find an ancestor for %s", g.argv[2]);
   340    342       }
   341    343     }else{
   342    344       if( !zPivot ){
................................................................................
   377    379       return;
   378    380     }
   379    381     if( integrateFlag && !is_a_leaf(mid)){
   380    382       fossil_warning("ignoring --integrate: %s is not a leaf", g.argv[2]);
   381    383       integrateFlag = 0;
   382    384     }
   383    385     if( verboseFlag ){
   384         -    print_checkin_description(mid, 12, integrateFlag?"integrate:":"merge-from:");
          386  +    print_checkin_description(mid, 12,
          387  +              integrateFlag ? "integrate:" : "merge-from:");
   385    388       print_checkin_description(pid, 12, "baseline:");
   386    389     }
   387    390     vfile_check_signature(vid, CKSIG_ENOTFILE);
   388    391     db_begin_transaction();
   389    392     if( !dryRunFlag ) undo_begin();
   390    393     if( load_vfile_from_rid(mid) && !forceMissingFlag ){
   391    394       fossil_fatal("missing content, unable to merge");

Changes to src/moderate.c.

   142    142   ** Show all pending moderation request
   143    143   */
   144    144   void modreq_page(void){
   145    145     Blob sql;
   146    146     Stmt q;
   147    147   
   148    148     login_check_credentials();
   149         -  if( !g.perm.RdWiki && !g.perm.RdTkt ){
   150         -    login_needed(g.anon.RdWiki && g.anon.RdTkt);
          149  +  if( !g.perm.ModWiki && !g.perm.ModTkt ){
          150  +    login_needed(g.anon.ModWiki && g.anon.ModTkt);
   151    151       return;
   152    152     }
   153    153     style_header("Pending Moderation Requests");
   154    154     @ <h2>All Pending Moderation Requests</h2>
   155    155     if( moderation_table_exists() ){
   156    156       blob_init(&sql, timeline_query_for_www(), -1);
   157    157       blob_append_sql(&sql,

Changes to src/path.c.

   545    545       g.argv += 2;
   546    546       g.argc -= 2;
   547    547     }
   548    548   }
   549    549   
   550    550   /* Query to extract all rename operations */
   551    551   static const char zRenameQuery[] =
          552  +@ CREATE TEMP TABLE renames AS
          553  +@ SELECT
          554  +@     datetime(event.mtime) AS date,
          555  +@     F.name AS old_name,
          556  +@     T.name AS new_name,
          557  +@     blob.uuid AS checkin
          558  +@   FROM mlink, filename F, filename T, event, blob
          559  +@  WHERE coalesce(mlink.pfnid,0)!=0 AND mlink.pfnid!=mlink.fnid
          560  +@    AND F.fnid=mlink.pfnid
          561  +@    AND T.fnid=mlink.fnid
          562  +@    AND event.objid=mlink.mid
          563  +@    AND event.type='ci'
          564  +@    AND blob.rid=mlink.mid;
          565  +;
          566  +
          567  +/* Query to extract distinct rename operations */
          568  +static const char zDistinctRenameQuery[] =
          569  +@ CREATE TEMP TABLE renames AS
   552    570   @ SELECT
   553         -@     datetime(event.mtime),
          571  +@     min(datetime(event.mtime)) AS date,
   554    572   @     F.name AS old_name,
   555    573   @     T.name AS new_name,
   556         -@     blob.uuid
          574  +@     blob.uuid AS checkin
   557    575   @   FROM mlink, filename F, filename T, event, blob
   558    576   @  WHERE coalesce(mlink.pfnid,0)!=0 AND mlink.pfnid!=mlink.fnid
   559    577   @    AND F.fnid=mlink.pfnid
   560    578   @    AND T.fnid=mlink.fnid
   561    579   @    AND event.objid=mlink.mid
   562    580   @    AND event.type='ci'
   563    581   @    AND blob.rid=mlink.mid
   564         -@  ORDER BY 1 DESC, 2;
          582  +@  GROUP BY 2, 3;
   565    583   ;
   566    584   
   567    585   /*
   568    586   ** WEBPAGE: test-rename-list
   569    587   **
   570    588   ** Print a list of all file rename operations throughout history.
   571    589   ** This page is intended for for testing purposes only and may change
   572    590   ** or be discontinued without notice.
   573    591   */
   574    592   void test_rename_list_page(void){
   575    593     Stmt q;
          594  +  int nRename;
          595  +  int nCheckin;
   576    596   
   577    597     login_check_credentials();
   578    598     if( !g.perm.Read ){ login_needed(g.anon.Read); return; }
   579         -  style_header("List Of File Name Changes");
   580         -  @ <h3>NB: Experimental Page</h3>
   581         -  @ <table border="1" width="100%%">
   582         -  @ <tr><th>Date &amp; Time</th>
          599  +  if( P("all")!=0 ){
          600  +    style_header("List Of All Filename Changes");
          601  +    db_multi_exec("%s", zRenameQuery/*safe-for-%s*/);
          602  +    style_submenu_element("Distinct", "%R/test-rename-list");
          603  +  }else{
          604  +    style_header("List Of Distinct Filename Changes");
          605  +    db_multi_exec("%s", zDistinctRenameQuery/*safe-for-%s*/);
          606  +    style_submenu_element("All", "%R/test-rename-list?all");
          607  +  }
          608  +  nRename = db_int(0, "SELECT count(*) FROM renames;");
          609  +  nCheckin = db_int(0, "SELECT count(DISTINCT checkin) FROM renames;");
          610  +  db_prepare(&q, "SELECT date, old_name, new_name, checkin FROM renames"
          611  +                 " ORDER BY date DESC, old_name ASC");
          612  +  @ <h1>%d(nRename) filename changes in %d(nCheckin) check-ins</h1>
          613  +  @ <table class='sortable' data-column-types='tttt' data-init-sort='1'\
          614  +  @  border="1" cellpadding="2" cellspacing="0">
          615  +  @ <thead><tr><th>Date &amp; Time</th>
   583    616     @ <th>Old Name</th>
   584    617     @ <th>New Name</th>
   585         -  @ <th>Check-in</th></tr>
   586         -  db_prepare(&q, "%s", zRenameQuery/*safe-for-%s*/);
          618  +  @ <th>Check-in</th></tr></thead><tbody>
   587    619     while( db_step(&q)==SQLITE_ROW ){
   588    620       const char *zDate = db_column_text(&q, 0);
   589    621       const char *zOld = db_column_text(&q, 1);
   590    622       const char *zNew = db_column_text(&q, 2);
   591    623       const char *zUuid = db_column_text(&q, 3);
   592    624       @ <tr>
   593    625       @ <td>%z(href("%R/timeline?c=%t",zDate))%s(zDate)</a></td>
   594    626       @ <td>%z(href("%R/finfo?name=%t",zOld))%h(zOld)</a></td>
   595    627       @ <td>%z(href("%R/finfo?name=%t",zNew))%h(zNew)</a></td>
   596    628       @ <td>%z(href("%R/info/%!S",zUuid))%S(zUuid)</a></td></tr>
   597    629     }
   598         -  @ </table>
          630  +  @ </tbody></table>
   599    631     db_finalize(&q);
          632  +  style_table_sorter();
   600    633     style_footer();
   601    634   }

Changes to src/pivot.c.

    36     36   **
    37     37   ** The act of setting the primary resets the pivot-finding algorithm.
    38     38   */
    39     39   void pivot_set_primary(int rid){
    40     40     /* Set up table used to do the search */
    41     41     db_multi_exec(
    42     42       "CREATE TEMP TABLE IF NOT EXISTS aqueue("
    43         -    "  rid INTEGER PRIMARY KEY,"  /* The record id for this version */
           43  +    "  rid INTEGER,"              /* The record id for this version */
    44     44       "  mtime REAL,"               /* Time when this version was created */
    45     45       "  pending BOOLEAN,"          /* True if we have not check this one yet */
    46         -    "  src BOOLEAN"               /* 1 for primary.  0 for others */
    47         -    ");"
           46  +    "  src BOOLEAN,"               /* 1 for primary.  0 for others */
           47  +    "  PRIMARY KEY(rid,src)"
           48  +    ") WITHOUT ROWID;"
    48     49       "DELETE FROM aqueue;"
    49     50       "CREATE INDEX IF NOT EXISTS aqueue_idx1 ON aqueue(pending, mtime);"
    50     51     );
    51     52   
    52     53     /* Insert the primary record */
    53     54     db_multi_exec(
    54     55       "INSERT INTO aqueue(rid, mtime, pending, src)"
................................................................................
   116    117     db_prepare(&u1,
   117    118       "UPDATE aqueue SET pending=0 WHERE rid=:rid"
   118    119     );
   119    120   
   120    121     /* Add to the queue all ancestors of :rid.
   121    122     */
   122    123     db_prepare(&i1,
   123         -    "INSERT OR IGNORE INTO aqueue "
          124  +    "REPLACE INTO aqueue "
   124    125       "SELECT plink.pid,"
   125         -    "       coalesce((SELECT mtime FROM plink X WHERE X.cid=plink.pid), 0.0),"
          126  +    "       coalesce((SELECT mtime FROM event X WHERE X.objid=plink.pid), 0.0),"
   126    127       "       1,"
   127    128       "       aqueue.src "
   128    129       "  FROM plink, aqueue"
   129    130       " WHERE plink.cid=:rid"
   130    131       "   AND aqueue.rid=:rid %s",
   131    132       ignoreMerges ? "AND plink.isprim" : ""
   132    133     );

Changes to src/popen.c.

    23     23   #ifdef _WIN32
    24     24   #include <windows.h>
    25     25   #include <fcntl.h>
    26     26   /*
    27     27   ** Print a fatal error and quit.
    28     28   */
    29     29   static void win32_fatal_error(const char *zMsg){
    30         -  fossil_fatal("%s", zMsg);
           30  +  fossil_panic("%s", zMsg);
    31     31   }
    32     32   #else
    33     33   #include <signal.h>
    34     34   #include <sys/wait.h>
    35     35   #endif
    36     36   
    37     37   /*

Changes to src/printf.c.

   979    979     va_end(ap);
   980    980   }
   981    981   
   982    982   /*
   983    983   ** Write a message to the error log, if the error log filename is
   984    984   ** defined.
   985    985   */
   986         -static void fossil_errorlog(const char *zFormat, ...){
          986  +void fossil_errorlog(const char *zFormat, ...){
   987    987     struct tm *pNow;
   988    988     time_t now;
   989    989     FILE *out;
   990    990     const char *z;
   991    991     int i;
   992    992     va_list ap;
   993         -  static const char *const azEnv[] = { "HTTP_HOST", "HTTP_USER_AGENT",
          993  +  static const char *const azEnv[] = { "HTTP_HOST", "HTTP_REFERER",
          994  +      "HTTP_USER_AGENT",
   994    995         "PATH_INFO", "QUERY_STRING", "REMOTE_ADDR", "REQUEST_METHOD",
   995    996         "REQUEST_URI", "SCRIPT_NAME" };
   996    997     if( g.zErrlog==0 ) return;
   997         -  out = fossil_fopen(g.zErrlog, "a");
   998         -  if( out==0 ) return;
          998  +  if( g.zErrlog[0]=='-' && g.zErrlog[1]==0 ){
          999  +    out = stderr;
         1000  +  }else{
         1001  +    out = fossil_fopen(g.zErrlog, "a");
         1002  +    if( out==0 ) return;
         1003  +  }
   999   1004     now = time(0);
  1000   1005     pNow = gmtime(&now);
  1001   1006     fprintf(out, "------------- %04d-%02d-%02d %02d:%02d:%02d UTC ------------\n",
  1002   1007             pNow->tm_year+1900, pNow->tm_mon+1, pNow->tm_mday+1,
  1003   1008             pNow->tm_hour, pNow->tm_min, pNow->tm_sec);
  1004   1009     va_start(ap, zFormat);
  1005   1010     vfprintf(out, zFormat, ap);
  1006   1011     fprintf(out, "\n");
  1007   1012     va_end(ap);
  1008   1013     for(i=0; i<count(azEnv); i++){
  1009   1014       char *p;
  1010         -    if( (p = fossil_getenv(azEnv[i]))!=0 ){
         1015  +    if( (p = fossil_getenv(azEnv[i]))!=0 && p[0]!=0 ){
  1011   1016         fprintf(out, "%s=%s\n", azEnv[i], p);
  1012   1017         fossil_path_free(p);
  1013         -    }else if( (z = P(azEnv[i]))!=0 ){
         1018  +    }else if( (z = P(azEnv[i]))!=0 && z[0]!=0 ){
  1014   1019         fprintf(out, "%s=%s\n", azEnv[i], z);
  1015   1020       }
  1016   1021     }
  1017   1022     fclose(out);
  1018   1023   }
  1019   1024   
  1020   1025   /*
  1021   1026   ** The following variable becomes true while processing a fatal error
  1022   1027   ** or a panic.  If additional "recursive-fatal" errors occur while
  1023   1028   ** shutting down, the recursive errors are silently ignored.
  1024   1029   */
  1025   1030   static int mainInFatalError = 0;
         1031  +
         1032  +/*
         1033  +** Write error message output
         1034  +*/
         1035  +static int fossil_print_error(int rc, const char *z){
         1036  +#ifdef FOSSIL_ENABLE_JSON
         1037  +  if( g.json.isJsonMode ){
         1038  +    json_err( 0, z, 1 );
         1039  +    if( g.isHTTP ){
         1040  +      rc = 0 /* avoid HTTP 500 */;
         1041  +    }
         1042  +  }
         1043  +  else
         1044  +#endif
         1045  +  if( g.cgiOutput==1 && g.db ){
         1046  +    g.cgiOutput = 2;
         1047  +    cgi_reset_content();
         1048  +    cgi_set_content_type("text/html");
         1049  +    style_header("Bad Request");
         1050  +    @ <p class="generalError">%h(z)</p>
         1051  +    cgi_set_status(400, "Bad Request");
         1052  +    style_footer();
         1053  +    cgi_reply();
         1054  +  }else if( !g.fQuiet ){
         1055  +    fossil_force_newline();
         1056  +    fossil_trace("%s\n", z);
         1057  +  }
         1058  +  return rc;
         1059  +}
  1026   1060   
  1027   1061   /*
  1028   1062   ** Print an error message, rollback all databases, and quit.  These
  1029   1063   ** routines never return.
         1064  +**
         1065  +** The only different between fossil_fatal() and fossil_panic() is that
         1066  +** fossil_panic() makes an entry in the error log whereas fossil_fatal()
         1067  +** does not.  If there is not error log, then both routines work the
         1068  +** same.  Hence, the routines are interchangable for commands and only
         1069  +** make a difference with processing web pages.
         1070  +**
         1071  +** Use fossil_fatal() for malformed inputs that should be reported back
         1072  +** to the user, but which do not represent a configuration problem or bug.
         1073  +**
         1074  +** Use fossil_panic() for any kind of error that should be brought to the
         1075  +** attention of the system administrator.
  1030   1076   */
  1031   1077   NORETURN void fossil_panic(const char *zFormat, ...){
  1032   1078     va_list ap;
  1033   1079     int rc = 1;
  1034   1080     char z[1000];
  1035   1081     static int once = 0;
  1036   1082   
................................................................................
  1038   1084     once = 1;
  1039   1085     mainInFatalError = 1;
  1040   1086     db_force_rollback();
  1041   1087     va_start(ap, zFormat);
  1042   1088     sqlite3_vsnprintf(sizeof(z),z,zFormat, ap);
  1043   1089     va_end(ap);
  1044   1090     fossil_errorlog("panic: %s", z);
  1045         -#ifdef FOSSIL_ENABLE_JSON
  1046         -  if( g.json.isJsonMode ){
  1047         -    json_err( 0, z, 1 );
  1048         -    if( g.isHTTP ){
  1049         -      rc = 0 /* avoid HTTP 500 */;
  1050         -    }
  1051         -  }
  1052         -  else
  1053         -#endif
  1054         -  {
  1055         -    if( g.cgiOutput ){
  1056         -      cgi_printf("<p class=\"generalError\">%h</p>", z);
  1057         -      cgi_reply();
  1058         -    }else if( !g.fQuiet ){
  1059         -      fossil_force_newline();
  1060         -      fossil_puts("Fossil internal error: ", 1);
  1061         -      fossil_puts(z, 1);
  1062         -      fossil_puts("\n", 1);
  1063         -    }
  1064         -  }
         1091  +  rc = fossil_print_error(rc, z);
  1065   1092     exit(rc);
  1066   1093   }
  1067         -
  1068   1094   NORETURN void fossil_fatal(const char *zFormat, ...){
  1069   1095     char *z;
  1070   1096     int rc = 1;
  1071   1097     va_list ap;
  1072   1098     mainInFatalError = 1;
  1073   1099     va_start(ap, zFormat);
  1074   1100     z = vmprintf(zFormat, ap);
  1075   1101     va_end(ap);
  1076         -  fossil_errorlog("fatal: %s", z);
  1077         -#ifdef FOSSIL_ENABLE_JSON
  1078         -  if( g.json.isJsonMode ){
  1079         -    json_err( g.json.resultCode, z, 1 );
  1080         -    if( g.isHTTP ){
  1081         -      rc = 0 /* avoid HTTP 500 */;
  1082         -    }
  1083         -  }
  1084         -  else
  1085         -#endif
  1086         -  {
  1087         -    if( g.cgiOutput ){
  1088         -      g.cgiOutput = 0;
  1089         -      cgi_printf("<p class=\"generalError\">\n%h\n</p>\n", z);
  1090         -      cgi_reply();
  1091         -    }else if( !g.fQuiet ){
  1092         -      fossil_force_newline();
  1093         -      fossil_trace("%s\n", z);
  1094         -    }
  1095         -  }
  1096         -  free(z);
         1102  +  rc = fossil_print_error(rc, z);
         1103  +  fossil_free(z);
  1097   1104     db_force_rollback();
  1098   1105     fossil_exit(rc);
  1099   1106   }
  1100   1107   
  1101   1108   /* This routine works like fossil_fatal() except that if called
  1102   1109   ** recursively, the recursive call is a no-op.
  1103   1110   **
................................................................................
  1112   1119     va_list ap;
  1113   1120     int rc = 1;
  1114   1121     if( mainInFatalError ) return;
  1115   1122     mainInFatalError = 1;
  1116   1123     va_start(ap, zFormat);
  1117   1124     z = vmprintf(zFormat, ap);
  1118   1125     va_end(ap);
  1119         -  fossil_errorlog("fatal: %s", z);
  1120         -#ifdef FOSSIL_ENABLE_JSON
  1121         -  if( g.json.isJsonMode ){
  1122         -    json_err( g.json.resultCode, z, 1 );
  1123         -    if( g.isHTTP ){
  1124         -      rc = 0 /* avoid HTTP 500 */;
  1125         -    }
  1126         -  } else
  1127         -#endif
  1128         -  {
  1129         -    if( g.cgiOutput ){
  1130         -      g.cgiOutput = 0;
  1131         -      cgi_printf("<p class=\"generalError\">\n%h\n</p>\n", z);
  1132         -      cgi_reply();
  1133         -    }else{
  1134         -      fossil_force_newline();
  1135         -      fossil_trace("%s\n", z);
  1136         -    }
  1137         -  }
         1126  +  rc = fossil_print_error(rc, z);
  1138   1127     db_force_rollback();
  1139   1128     fossil_exit(rc);
  1140   1129   }
  1141   1130   
  1142   1131   
  1143         -/* Print a warning message */
         1132  +/* Print a warning message.
         1133  +**
         1134  +** Unlike fossil_fatal() and fossil_panic(), this routine does return
         1135  +** and processing attempts to continue.  A message is written to the
         1136  +** error log, however, so this routine should only be used for situations
         1137  +** that require administrator or developer attention.  Minor problems
         1138  +** in user inputs should not use this routine.
         1139  +*/
  1144   1140   void fossil_warning(const char *zFormat, ...){
  1145   1141     char *z;
  1146   1142     va_list ap;
  1147   1143     va_start(ap, zFormat);
  1148   1144     z = vmprintf(zFormat, ap);
  1149   1145     va_end(ap);
  1150   1146     fossil_errorlog("warning: %s", z);

Changes to src/purge.c.

   120    120       return 0;
   121    121     }
   122    122   
   123    123     /* Make sure we are not removing a manifest that is the baseline of some
   124    124     ** manifest that is being left behind.  This step is not strictly necessary.
   125    125     ** is is just a safety check. */
   126    126     if( purge_baseline_out_from_under_delta(zTab) ){
   127         -    fossil_fatal("attempt to purge a baseline manifest without also purging "
          127  +    fossil_panic("attempt to purge a baseline manifest without also purging "
   128    128                    "all of its deltas");
   129    129     }
   130    130   
   131    131     /* Make sure that no delta that is left behind requires a purged artifact
   132    132     ** as its basis.  If such artifacts exist, go ahead and undelta them now.
   133    133     */
   134    134     db_prepare(&q, "SELECT rid FROM delta WHERE srcid IN \"%w\""

Changes to src/rebuild.c.

   142    142   
   143    143   /*
   144    144   ** Update the repository schema for Fossil version 2.0.  (2017-02-28)
   145    145   **   (1) Change the CHECK constraint on BLOB.UUID so that the length
   146    146   **       is greater than or equal to 40, not exactly equal to 40.
   147    147   */
   148    148   void rebuild_schema_update_2_0(void){
   149         -  char *z = db_text(0, "SELECT sql FROM repository.sqlite_master WHERE name='blob'");
          149  +  char *z = db_text(0, "SELECT sql FROM repository.sqlite_master"
          150  +                       " WHERE name='blob'");
   150    151     if( z ){
   151    152       /* Search for:  length(uuid)==40
   152    153       **              0123456789 12345   */
   153    154       int i;
   154    155       for(i=10; z[i]; i++){
   155    156         if( z[i]=='=' && strncmp(&z[i-6],"(uuid)==40",10)==0 ){
   156    157           z[i] = '>';
................................................................................
   355    356   
   356    357     bag_init(&bagDone);
   357    358     ttyOutput = doOut;
   358    359     processCnt = 0;
   359    360     if (ttyOutput && !g.fQuiet) {
   360    361       percent_complete(0);
   361    362     }
          363  +  email_triggers_disable();
   362    364     rebuild_update_schema();
   363    365     blob_init(&sql, 0, 0);
   364    366     db_prepare(&q,
   365    367        "SELECT name FROM sqlite_master /*scan*/"
   366    368        " WHERE type='table'"
   367    369        " AND name NOT IN ('admin_log', 'blob','delta','rcvfrom','user','alias',"
   368    370                          "'config','shun','private','reportfmt',"
   369    371                          "'concealed','accesslog','modreq',"
   370         -                       "'purgeevent','purgeitem','unversioned')"
          372  +                       "'purgeevent','purgeitem','unversioned',"
          373  +                       "'subscriber','pending_alert','email_bounce')"
   371    374        " AND name NOT GLOB 'sqlite_*'"
   372    375        " AND name NOT GLOB 'fx_*'"
   373    376     );
   374    377     while( db_step(&q)==SQLITE_ROW ){
   375    378       blob_appendf(&sql, "DROP TABLE IF EXISTS \"%w\";\n", db_column_text(&q,0));
   376    379     }
   377    380     db_finalize(&q);
................................................................................
   444    447       percent_complete((processCnt*1000)/totalSize);
   445    448     }
   446    449     if( doClustering ) create_cluster();
   447    450     if( ttyOutput && !g.fQuiet && totalSize>0 ){
   448    451       processCnt += incrSize;
   449    452       percent_complete((processCnt*1000)/totalSize);
   450    453     }
          454  +  email_triggers_enable();
   451    455     if(!g.fQuiet && ttyOutput ){
   452    456       percent_complete(1000);
   453    457       fossil_print("\n");
   454    458     }
   455    459     return errCnt;
   456    460   }
   457    461   

Changes to src/regexp.c.

   734    734   */
   735    735   int re_add_sql_func(sqlite3 *db){
   736    736     return sqlite3_create_function(db, "regexp", 2, SQLITE_UTF8, 0,
   737    737                                    re_sql_func, 0, 0);
   738    738   }
   739    739   
   740    740   /*
   741         -** Run a "grep" over a single file
          741  +** Run a "grep" over a single file read from disk.
   742    742   */
   743         -static void grep(ReCompiled *pRe, const char *zFile, FILE *in){
          743  +static void grep_file(ReCompiled *pRe, const char *zFile, FILE *in){
   744    744     int ln = 0;
   745    745     int n;
   746    746     char zLine[2000];
   747    747     while( fgets(zLine, sizeof(zLine), in) ){
   748    748       ln++;
   749    749       n = (int)strlen(zLine);
   750    750       while( n && (zLine[n-1]=='\n' || zLine[n-1]=='\r') ) n--;
   751    751       if( re_match(pRe, (const unsigned char*)zLine, n) ){
   752         -      printf("%s:%d:%.*s\n", zFile, ln, n, zLine);
          752  +      fossil_print("%s:%d:%.*s\n", zFile, ln, n, zLine);
          753  +    }
          754  +  }
          755  +}
          756  +
          757  +/*
          758  +** Flags for grep_buffer()
          759  +*/
          760  +#define GREP_EXISTS    0x001    /* If any match, print only the name and stop */
          761  +
          762  +/*
          763  +** Run a "grep" over a text file
          764  +*/
          765  +static int grep_buffer(
          766  +  ReCompiled *pRe,
          767  +  const char *zName,
          768  +  const char *z,
          769  +  u32 flags
          770  +){
          771  +  int i, j, n, ln, cnt;
          772  +  for(i=j=ln=cnt=0; z[i]; i=j+1){
          773  +    for(j=i; z[j] && z[j]!='\n'; j++){}
          774  +    n = j - i;
          775  +    if( z[j]=='\n' ) j++;
          776  +    ln++;
          777  +    if( re_match(pRe, (const unsigned char*)(z+i), j-i) ){
          778  +      cnt++;
          779  +      if( flags & GREP_EXISTS ){
          780  +        fossil_print("%s\n", zName);
          781  +        break;
          782  +      }
          783  +      fossil_print("%s:%d:%.*s\n", zName, ln, n, z+i);
   753    784       }
   754    785     }
          786  +  return cnt;
   755    787   }
   756    788   
   757    789   /*
   758    790   ** COMMAND: test-grep
   759    791   **
   760    792   ** Usage: %fossil test-grep REGEXP [FILE...]
   761    793   **
................................................................................
   772    804     int ignoreCase = find_option("ignore-case","i",0)!=0;
   773    805     if( g.argc<3 ){
   774    806       usage("REGEXP [FILE...]");
   775    807     }
   776    808     zErr = re_compile(&pRe, g.argv[2], ignoreCase);
   777    809     if( zErr ) fossil_fatal("%s", zErr);
   778    810     if( g.argc==3 ){
   779         -    grep(pRe, "-", stdin);
          811  +    grep_file(pRe, "-", stdin);
   780    812     }else{
   781    813       int i;
   782    814       for(i=3; i<g.argc; i++){
   783    815         FILE *in = fossil_fopen(g.argv[i], "rb");
   784    816         if( in==0 ){
   785    817           fossil_warning("cannot open \"%s\"", g.argv[i]);
   786    818         }else{
   787         -        grep(pRe, g.argv[i], in);
          819  +        grep_file(pRe, g.argv[i], in);
   788    820           fclose(in);
   789    821         }
   790    822       }
   791    823     }
   792    824     re_free(pRe);
   793    825   }
          826  +
          827  +/*
          828  +** COMMAND: grep
          829  +**
          830  +** Usage: %fossil grep [OPTIONS] PATTERN FILENAME
          831  +**
          832  +** Run grep over all historic version of FILENAME
          833  +**
          834  +** Options:
          835  +**
          836  +**     -i|--ignore-case         Ignore case
          837  +**     -l|--files-with-matches  Print only filenames that match
          838  +**     -v|--verbose             Show each file as it is analyzed
          839  +*/
          840  +void re_grep_cmd(void){
          841  +  u32 flags = 0;
          842  +  int bVerbose = 0;
          843  +  ReCompiled *pRe;
          844  +  const char *zErr;
          845  +  int ignoreCase = 0;
          846  +  Blob fullName;
          847  +
          848  +  if( find_option("ignore-case","i",0)!=0 ) ignoreCase = 1;
          849  +  if( find_option("files-with-matches","l",0)!=0 ) flags |= GREP_EXISTS;
          850  +  if( find_option("verbose","v",0)!=0 ) bVerbose = 1;
          851  +  db_find_and_open_repository(0, 0);
          852  +  verify_all_options();
          853  +  if( g.argc<3 ){
          854  +    usage("REGEXP FILENAME");
          855  +  }
          856  +  zErr = re_compile(&pRe, g.argv[2], ignoreCase);
          857  +  if( zErr ) fossil_fatal("%s", zErr);
          858  +
          859  +  if( file_tree_name(g.argv[3], &fullName, 0, 0) ){
          860  +    int fnid = db_int(0, "SELECT fnid FROM filename WHERE name=%Q",
          861  +                      blob_str(&fullName));
          862  +    if( fnid ){
          863  +      Stmt q;
          864  +      add_content_sql_commands(g.db);
          865  +      db_prepare(&q,
          866  +        "SELECT content(ux), substr(ux,1,10) FROM ("
          867  +        "  SELECT blob.uuid AS ux, min(event.mtime) AS mx"
          868  +        "    FROM mlink, blob, event"
          869  +        "   WHERE mlink.mid=event.objid"
          870  +        "     AND mlink.fid=blob.rid"
          871  +        "     AND mlink.fnid=%d"
          872  +        "   GROUP BY blob.uuid"
          873  +        ") ORDER BY mx DESC;",
          874  +        fnid
          875  +      );
          876  +      while( db_step(&q)==SQLITE_ROW ){
          877  +        if( bVerbose ) fossil_print("%s:\n", db_column_text(&q,1));
          878  +        grep_buffer(pRe, db_column_text(&q,1), db_column_text(&q,0), flags);
          879  +      }
          880  +      db_finalize(&q);
          881  +    }
          882  +  }
          883  +}

Changes to src/security_audit.c.

   323    323         @ The "Server Load Average Limit" on the
   324    324         @ <a href="setup_access">Access Control</a> page is set to %g(r),
   325    325         @ which seems high.  Is this server really a %d((int)r)-core machine?
   326    326       }
   327    327     }
   328    328   #endif
   329    329   
          330  +  if( g.zErrlog==0 || fossil_strcmp(g.zErrlog,"-")==0 ){
          331  +    @ <li><p>
          332  +    @ The server error log is disabled.
          333  +    @ To set up an error log:
          334  +    @ <ul>
          335  +    @ <li>If running from CGI, make an entry "errorlog: <i>FILENAME</i>"
          336  +    @ in the CGI script.
          337  +    @ <li>If running the "fossil server" or "fossil http" commands,
          338  +    @ add the "--errorlog <i>FILENAME</i>" command-line option.
          339  +    @ </ul>
          340  +  }else{
          341  +    FILE *pTest = fossil_fopen(g.zErrlog,"a");
          342  +    if( pTest==0 ){
          343  +      @ <li><p>
          344  +      @ <b>Error:</b>
          345  +      @ There is an error log at "%h(g.zErrlog)" but that file is not
          346  +      @ writable and so no logging will occur.
          347  +    }else{
          348  +      fclose(pTest);
          349  +      @ <li><p>
          350  +      @ The error log at "<a href='%R/errorlog'>%h(g.zErrlog)</a>" that is
          351  +      @ %,lld(file_size(g.zErrlog, ExtFILE)) bytes in size.
          352  +    }
          353  +  }
   330    354   
   331    355     @ </ol>
   332    356     style_footer();
   333    357   }
   334    358   
   335    359   /*
   336    360   ** WEBPAGE: takeitprivate
................................................................................
   366    390     @ <form action="%s(g.zPath)" method="post">
   367    391     @ <input type="submit" name="apply" value="Make It Private">
   368    392     @ <input type="submit" name="cancel" value="Cancel">
   369    393     @ </form>
   370    394   
   371    395     style_footer();
   372    396   }
          397  +
          398  +/*
          399  +** The maximum number of bytes of log to show
          400  +*/
          401  +#define MXSHOWLOG 50000
          402  +
          403  +/*
          404  +** WEBPAGE: errorlog
          405  +**
          406  +** Show the content of the error log.  Only the administrator can view
          407  +** this page.
          408  +*/
          409  +void errorlog_page(void){
          410  +  i64 szFile;
          411  +  FILE *in;
          412  +  char z[10000];
          413  +  login_check_credentials();
          414  +  if( !g.perm.Setup && !g.perm.Admin ){
          415  +    login_needed(0);
          416  +    return;
          417  +  }
          418  +  style_header("Server Error Log");
          419  +  style_submenu_element("Test", "%R/test-warning");
          420  +  if( g.zErrlog==0 || fossil_strcmp(g.zErrlog,"-")==0 ){
          421  +    @ <p>To create a server error log:
          422  +    @ <ol>
          423  +    @ <li><p>
          424  +    @ If the server is running as CGI, then create a line in the CGI file
          425  +    @ like this:
          426  +    @ <blockquote><pre>
          427  +    @ errorlog: <i>FILENAME</i>
          428  +    @ </pre></blockquote>
          429  +    @ <li><p>
          430  +    @ If the server is running using one of 
          431  +    @ the "fossil http" or "fossil server" commands then add
          432  +    @ a command-line option "--errorlog <i>FILENAME</i>" to that
          433  +    @ command.
          434  +    @ </ol>
          435  +    style_footer();
          436  +    return;
          437  +  }
          438  +  if( P("truncate1") && cgi_csrf_safe(1) ){
          439  +    fclose(fopen(g.zErrlog,"w"));
          440  +  }
          441  +  if( P("download") ){
          442  +    Blob log;
          443  +    blob_read_from_file(&log, g.zErrlog, ExtFILE);
          444  +    cgi_set_content_type("text/plain");
          445  +    cgi_set_content(&log);
          446  +    return;
          447  +  }
          448  +  szFile = file_size(g.zErrlog, ExtFILE);
          449  +  if( P("truncate") ){
          450  +    @ <form action="%R/errorlog" method="POST">
          451  +    @ <p>Confirm that you want to truncate the %,lld(szFile)-byte error log:
          452  +    @ <input type="submit" name="truncate1" value="Confirm">
          453  +    @ <input type="submit" name="cancel" value="Cancel">
          454  +    @ </form>
          455  +    style_footer();
          456  +    return;
          457  +  }
          458  +  @ <p>The server error log at "%h(g.zErrlog)" is %,lld(szFile) bytes in size.
          459  +  style_submenu_element("Download", "%R/errorlog?download");
          460  +  style_submenu_element("Truncate", "%R/errorlog?truncate");
          461  +  in = fossil_fopen(g.zErrlog, "rb");
          462  +  if( in==0 ){
          463  +    @ <p class='generalError'>Unable top open that file for reading!</p>
          464  +    style_footer();
          465  +    return;
          466  +  }
          467  +  if( szFile>MXSHOWLOG ){
          468  +    @ Only the last %,d(MXSHOWLOG) bytes are shown.
          469  +    fseek(in, -MXSHOWLOG, SEEK_END);
          470  +  }
          471  +  @ <hr>
          472  +  @ <pre>
          473  +  while( fgets(z, sizeof(z), in) ){
          474  +    @ %h(z)\
          475  +  }
          476  +  fclose(in);
          477  +  @ </pre>
          478  +  style_footer();
          479  +}

Changes to src/setup.c.

     1      1   /*
     2      2   ** Copyright (c) 2007 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/
................................................................................
    72     72     style_header("Server Administration");
    73     73   
    74     74     /* Make sure the header contains <base href="...">.   Issue a warning
    75     75     ** if it does not. */
    76     76     if( !cgi_header_contains("<base href=") ){
    77     77       @ <p class="generalError"><b>Configuration Error:</b> Please add
    78     78       @ <tt>&lt;base href="$secureurl/$current_page"&gt;</tt> after
    79         -    @ <tt>&lt;head&gt;</tt> in the <a href="setup_skinedit?w=2">HTML header</a>!</p>
           79  +    @ <tt>&lt;head&gt;</tt> in the
           80  +    @ <a href="setup_skinedit?w=2">HTML header</a>!</p>
    80     81     }
    81     82   
    82     83   #if !defined(_WIN32)
    83     84     /* Check for /dev/null and /dev/urandom.  We want both devices to be present,
    84     85     ** but they are sometimes omitted (by mistake) from chroot jails. */
    85     86     if( access("/dev/null", R_OK|W_OK) ){
    86     87       @ <p class="generalError">WARNING: Device "/dev/null" is not available
................................................................................
   111    112       " on the same server");
   112    113     setup_menu_entry("Tickets", "tktsetup",
   113    114       "Configure the trouble-ticketing system for this repository");
   114    115     setup_menu_entry("Search","srchsetup",
   115    116       "Configure the built-in search engine");
   116    117     setup_menu_entry("URL Aliases", "waliassetup",
   117    118       "Configure URL aliases");
          119  +  setup_menu_entry("Notification", "setup_notification",
          120  +    "Automatic notifications of changes via outbound email");
          121  +  setup_menu_entry("Email-Server", "setup_smtp",
          122  +    "Activate and configure the built-in email server");
   118    123     setup_menu_entry("Transfers", "xfersetup",
   119    124       "Configure the transfer system for this repository");
   120    125     setup_menu_entry("Skins", "setup_skin",
   121    126       "Select and/or modify the web interface \"skins\"");
   122    127     setup_menu_entry("Moderation", "setup_modreq",
   123    128       "Enable/Disable requiring moderator approval of Wiki and/or Ticket"
   124    129       " changes and attachments.");
................................................................................
   134    139       "Show artifacts that are shunned by this repository");
   135    140     setup_menu_entry("Artifact Receipts Log", "rcvfromlist",
   136    141       "A record of received artifacts and their sources");
   137    142     setup_menu_entry("User Log", "access_log",
   138    143       "A record of login attempts");
   139    144     setup_menu_entry("Administrative Log", "admin_log",
   140    145       "View the admin_log entries");
          146  +  setup_menu_entry("Error Log", "errorlog",
          147  +    "View the Fossil server error log");
   141    148     setup_menu_entry("Unversioned Files", "uvlist?byage=1",
   142    149       "Show all unversioned files held");
   143    150     setup_menu_entry("Stats", "stat",
   144    151       "Repository Status Reports");
   145    152     setup_menu_entry("Sitemap", "sitemap",
   146    153       "Links to miscellaneous pages");
   147    154     setup_menu_entry("SQL", "admin_sql",
................................................................................
   226    233     @ <div class='section'>Users</div>
   227    234     @ <table border=1 cellpadding=2 cellspacing=0 class='userTable sortable' \
   228    235     @  data-column-types='ktxTTK' data-init-sort='2'>
   229    236     @ <thead><tr>
   230    237     @ <th>Login Name<th>Caps<th>Info<th>Date<th>Expire<th>Last Login</tr></thead>
   231    238     @ <tbody>
   232    239     db_multi_exec(
   233         -    "CREATE TEMP TABLE lastAccess(uname TEXT PRIMARY KEY, atime REAL) WITHOUT ROWID;"
          240  +    "CREATE TEMP TABLE lastAccess(uname TEXT PRIMARY KEY, atime REAL)"
          241  +    "WITHOUT ROWID;"
   234    242     );
   235    243     if( db_table_exists("repository","accesslog") ){
   236    244       db_multi_exec(
   237    245         "INSERT INTO lastAccess(uname, atime)"
   238    246         " SELECT uname, max(mtime) FROM ("
   239    247         "    SELECT uname, mtime FROM accesslog WHERE success"
   240    248         "    UNION ALL"
................................................................................
   245    253     }
   246    254     if( zWith && zWith[0] ){
   247    255       zWith = mprintf(" AND cap GLOB '*[%q]*'", zWith);
   248    256     }else{
   249    257       zWith = "";
   250    258     }
   251    259     db_prepare(&s,
   252         -     "SELECT uid, login, cap, info, date(mtime,'unixepoch'), lower(login) AS sortkey, "
          260  +     "SELECT uid, login, cap, info, date(mtime,'unixepoch'),"
          261  +     "       lower(login) AS sortkey, "
   253    262        "       CASE WHEN info LIKE '%%expires 20%%'"
   254    263                "    THEN substr(info,instr(lower(info),'expires')+8,10)"
   255    264                "    END AS exp,"
   256    265                "atime"
   257    266        "  FROM user LEFT JOIN lastAccess ON login=uname"
   258    267        " WHERE login NOT IN ('anonymous','nobody','developer','reader') %s"
   259    268        " ORDER BY sortkey", zWith/*safe-for-%s*/
................................................................................
   269    278       const char *zExp = db_column_text(&s,6);
   270    279       double rATime = db_column_double(&s,7);
   271    280       char *zAge = 0;
   272    281       if( rATime>0.0 ){
   273    282         zAge = human_readable_age(rNow - rATime);
   274    283       }
   275    284       @ <tr>
   276         -    @ <td data-sortkey='%h(zSortKey)'><a href='setup_uedit?id=%d(uid)'>%h(zLogin)</a>
          285  +    @ <td data-sortkey='%h(zSortKey)'>\
          286  +    @ <a href='setup_uedit?id=%d(uid)'>%h(zLogin)</a>
   277    287       @ <td>%h(zCap)
   278    288       @ <td>%h(zInfo)
   279    289       @ <td>%h(zDate?zDate:"")
   280    290       @ <td>%h(zExp?zExp:"")
   281    291       @ <td data-sortkey='%f(rATime)' style='white-space:nowrap'>%s(zAge?zAge:"")
   282    292       @ </tr>
   283    293       fossil_free(zAge);
................................................................................
   298    308        @ <tr><th valign="top">b</th>
   299    309        @   <td><i>Attach:</i> Add attachments to wiki or tickets</td></tr>
   300    310        @ <tr><th valign="top">c</th>
   301    311        @   <td><i>Append-Tkt:</i> Append to tickets</td></tr>
   302    312        @ <tr><th valign="top">d</th>
   303    313        @   <td><i>Delete:</i> Delete wiki and tickets</td></tr>
   304    314        @ <tr><th valign="top">e</th>
   305         -     @   <td><i>Email:</i> View sensitive data such as EMail addresses</td></tr>
          315  +     @   <td><i>View-PII:</i> \
          316  +     @ View sensitive data such as email addresses</td></tr>
   306    317        @ <tr><th valign="top">f</th>
   307    318        @   <td><i>New-Wiki:</i> Create new wiki pages</td></tr>
   308    319        @ <tr><th valign="top">g</th>
   309    320        @   <td><i>Clone:</i> Clone the repository</td></tr>
   310    321        @ <tr><th valign="top">h</th>
   311    322        @   <td><i>Hyperlinks:</i> Show hyperlinks to detailed
   312    323        @   repository history</td></tr>
................................................................................
   344    355        @   <td><i>Write-Tkt:</i> Edit tickets</td></tr>
   345    356        @ <tr><th valign="top">x</th>
   346    357        @   <td><i>Private:</i> Push and/or pull private branches</td></tr>
   347    358        @ <tr><th valign="top">y</th>
   348    359        @   <td><i>Write-Unver:</i> Push unversioned files</td></tr>
   349    360        @ <tr><th valign="top">z</th>
   350    361        @   <td><i>Zip download:</i> Download a ZIP archive or tarball</td></tr>
          362  +     @ <tr><th valign="top">2</th>
          363  +     @   <td><i>Forum-Read:</i> Read forum posts by others </td></tr>
          364  +     @ <tr><th valign="top">3</th>
          365  +     @   <td><i>Forum-Append:</i> Add new forum posts</td></tr>
          366  +     @ <tr><th valign="top">4</th>
          367  +     @   <td><i>Forum-Trusted:</i> Add pre-approved forum posts </td></tr>
          368  +     @ <tr><th valign="top">5</th>
          369  +     @   <td><i>Forum-Moderator:</i> Approve or disapprove forum posts</td></tr>
          370  +     @ <tr><th valign="top">6</th>
          371  +     @   <td><i>Forum-Supervisor:</i> \
          372  +     @ Forum administrator
          373  +     @ <tr><th valign="top">7</th>
          374  +     @   <td><i>Email-Alerts:</i> Sign up for email nofications</td></tr>
          375  +     @ <tr><th valign="top">A</th>
          376  +     @   <td><i>Announce:</i> Send announcements</td></tr>
   351    377     @ </table>
   352    378   }
   353    379   
   354    380   /*
   355    381   ** WEBPAGE: setup_ulist_notes
   356    382   **
   357         -** A documentation page showing notes about user configuration.  This information
   358         -** used to be a side-bar on the user list page, but has been factored out for
   359         -** improved presentation.
          383  +** A documentation page showing notes about user configuration.  This
          384  +** information used to be a side-bar on the user list page, but has been
          385  +** factored out for improved presentation.
   360    386   */
   361    387   void setup_ulist_notes(void){
   362    388     style_header("User Configuration Notes");
   363    389     @ <h1>User Configuration Notes:</h1>
   364    390     @ <ol>
   365    391     @ <li><p>
   366    392     @ Every user, logged in or not, inherits the privileges of
................................................................................
   466    492     /* If we have all the necessary information, write the new or
   467    493     ** modified user record.  After writing the user record, redirect
   468    494     ** to the page that displays a list of users.
   469    495     */
   470    496     doWrite = cgi_all("login","info","pw") && !higherUser && cgi_csrf_safe(1);
   471    497     if( doWrite ){
   472    498       char c;
   473         -    char zCap[50], zNm[4];
          499  +    char zCap[70], zNm[4];
   474    500       zNm[0] = 'a';
   475    501       zNm[2] = 0;
   476    502       for(i=0, c='a'; c<='z'; c++){
   477    503         zNm[1] = c;
   478    504         a[c&0x7f] = (c!='s' || g.perm.Setup) && P(zNm)!=0;
   479    505         if( a[c&0x7f] ) zCap[i++] = c;
   480    506       }
          507  +    for(c='0'; c<='9'; c++){
          508  +      zNm[1] = c;
          509  +      a[c&0x7f] = P(zNm)!=0;
          510  +      if( a[c&0x7f] ) zCap[i++] = c;
          511  +    }
          512  +    for(c='A'; c<='Z'; c++){
          513  +      zNm[1] = c;
          514  +      a[c&0x7f] = P(zNm)!=0;
          515  +      if( a[c&0x7f] ) zCap[i++] = c;
          516  +    }
   481    517   
   482    518       zCap[i] = 0;
   483    519       zPw = P("pw");
   484    520       zLogin = P("login");
   485    521       if( strlen(zLogin)==0 ){
          522  +      const char *zRef = cgi_referer("setup_ulist");
   486    523         style_header("User Creation Error");
   487    524         @ <span class="loginError">Empty login not allowed.</span>
   488    525         @
   489         -      @ <p><a href="setup_uedit?id=%d(uid)&referer=%T(cgi_referer("setup_ulist"))">
          526  +      @ <p><a href="setup_uedit?id=%d(uid)&referer=%T(zRef)">
   490    527         @ [Bummer]</a></p>
   491    528         style_footer();
   492    529         return;
   493    530       }
   494    531       if( isValidPwString(zPw) ){
   495    532         zPw = sha1_shared_secret(zPw, zLogin, 0);
   496    533       }else{
   497    534         zPw = db_text(0, "SELECT pw FROM user WHERE uid=%d", uid);
   498    535       }
   499    536       zOldLogin = db_text(0, "SELECT login FROM user WHERE uid=%d", uid);
   500         -    if( db_exists("SELECT 1 FROM user WHERE login=%Q AND uid!=%d", zLogin, uid) ){
          537  +    if( db_exists("SELECT 1 FROM user WHERE login=%Q AND uid!=%d",zLogin,uid) ){
          538  +      const char *zRef = cgi_referer("setup_ulist");
   501    539         style_header("User Creation Error");
   502    540         @ <span class="loginError">Login "%h(zLogin)" is already used by
   503    541         @ a different user.</span>
   504    542         @
   505         -      @ <p><a href="setup_uedit?id=%d(uid)&referer=%T(cgi_referer("setup_ulist"))">
          543  +      @ <p><a href="setup_uedit?id=%d(uid)&referer=%T(zRef)">
   506    544         @ [Bummer]</a></p>
   507    545         style_footer();
   508    546         return;
   509    547       }
   510    548       login_verify_csrf_secret();
   511    549       db_multi_exec(
   512    550          "REPLACE INTO user(uid,login,info,pw,cap,mtime) "
................................................................................
   541    579         );
   542    580         login_group_sql(blob_str(&sql), "<li> ", " </li>\n", &zErr);
   543    581         blob_reset(&sql);
   544    582         admin_log( "Updated user [%q] in all login groups "
   545    583                    "with capabilities [%q].",
   546    584                    zLogin, zCap );
   547    585         if( zErr ){
          586  +        const char *zRef = cgi_referer("setup_ulist");
   548    587           style_header("User Change Error");
   549    588           admin_log( "Error updating user '%q': %s'.", zLogin, zErr );
   550    589           @ <span class="loginError">%h(zErr)</span>
   551    590           @
   552         -        @ <p><a href="setup_uedit?id=%d(uid)&referer=%T(cgi_referer("setup_ulist"))">
          591  +        @ <p><a href="setup_uedit?id=%d(uid)&referer=%T(zRef)">
   553    592           @ [Bummer]</a></p>
   554    593           style_footer();
   555    594           return;
   556    595         }
   557    596       }
   558    597       cgi_redirect(cgi_referer("setup_ulist"));
   559    598       return;
................................................................................
   562    601     /* Load the existing information about the user, if any
   563    602     */
   564    603     zLogin = "";
   565    604     zInfo = "";
   566    605     zCap = "";
   567    606     zPw = "";
   568    607     for(i='a'; i<='z'; i++) oa[i] = "";
          608  +  for(i='0'; i<='9'; i++) oa[i] = "";
          609  +  for(i='A'; i<='Z'; i++) oa[i] = "";
   569    610     if( uid ){
   570    611       zLogin = db_text("", "SELECT login FROM user WHERE uid=%d", uid);
   571    612       zInfo = db_text("", "SELECT info FROM user WHERE uid=%d", uid);
   572    613       zCap = db_text("", "SELECT cap FROM user WHERE uid=%d", uid);
   573    614       zPw = db_text("", "SELECT pw FROM user WHERE uid=%d", uid);
   574    615       for(i=0; zCap[i]; i++){
   575    616         char c = zCap[i];
   576         -      if( c>='a' && c<='z' ) oa[c&0x7f] = " checked=\"checked\"";
          617  +      if( (c>='a' && c<='z') || (c>='0' && c<='9') || (c>='A' && c<='Z') ){
          618  +        oa[c&0x7f] = " checked=\"checked\"";
          619  +      }
   577    620       }
   578    621     }
   579    622   
   580    623     /* figure out inherited permissions */
   581    624     memset((char *)inherit, 0, sizeof(inherit));
   582    625     if( fossil_strcmp(zLogin, "developer") ){
   583    626       char *z1, *z2;
................................................................................
   662    705     @ <table border=0><tr><td valign="top">
   663    706     if( g.perm.Setup ){
   664    707       @  <label><input type="checkbox" name="as"%s(oa['s']) />
   665    708       @  Setup%s(B('s'))</label><br />
   666    709     }
   667    710     @  <label><input type="checkbox" name="aa"%s(oa['a']) />
   668    711     @  Admin%s(B('a'))</label><br />
          712  +  @  <label><input type="checkbox" name="au"%s(oa['u']) />
          713  +  @  Reader%s(B('u'))</label><br>
          714  +  @  <label><input type="checkbox" name="av"%s(oa['v']) />
          715  +  @  Developer%s(B('v'))</label><br />
   669    716     @  <label><input type="checkbox" name="ad"%s(oa['d']) />
   670    717     @  Delete%s(B('d'))</label><br />
   671    718     @  <label><input type="checkbox" name="ae"%s(oa['e']) />
   672         -  @  Email%s(B('e'))</label><br />
          719  +  @  View-PII%s(B('e'))</label><br />
   673    720     @  <label><input type="checkbox" name="ap"%s(oa['p']) />
   674    721     @  Password%s(B('p'))</label><br />
   675    722     @  <label><input type="checkbox" name="ai"%s(oa['i']) />
   676    723     @  Check-In%s(B('i'))</label><br />
   677    724     @  <label><input type="checkbox" name="ao"%s(oa['o']) />
   678    725     @  Check-Out%s(B('o'))</label><br />
   679    726     @  <label><input type="checkbox" name="ah"%s(oa['h']) />
   680    727     @  Hyperlinks%s(B('h'))</label><br />
   681    728     @  <label><input type="checkbox" name="ab"%s(oa['b']) />
   682         -  @  Attachments%s(B('b'))</label><br />
          729  +  @  Attachments%s(B('b'))</label>
          730  +
   683    731     @ </td><td><td width="40"></td><td valign="top">
   684         -  @  <label><input type="checkbox" name="au"%s(oa['u']) />
   685         -  @  Reader%s(B('u'))</label><br />
   686         -  @  <label><input type="checkbox" name="av"%s(oa['v']) />
   687         -  @  Developer%s(B('v'))</label><br />
   688    732     @  <label><input type="checkbox" name="ag"%s(oa['g']) />
   689    733     @  Clone%s(B('g'))</label><br />
   690    734     @  <label><input type="checkbox" name="aj"%s(oa['j']) />
   691         -  @  Read Wiki%s(B('j'))</label><br />
          735  +  @  Read Wiki%s(B('j'))</label><br>
   692    736     @  <label><input type="checkbox" name="af"%s(oa['f']) />
   693    737     @  New Wiki%s(B('f'))</label><br />
   694    738     @  <label><input type="checkbox" name="am"%s(oa['m']) />
   695    739     @  Append Wiki%s(B('m'))</label><br />
   696    740     @  <label><input type="checkbox" name="ak"%s(oa['k']) />
   697    741     @  Write Wiki%s(B('k'))</label><br />
   698    742     @  <label><input type="checkbox" name="al"%s(oa['l']) />
   699    743     @  Moderate Wiki%s(B('l'))</label><br />
   700         -  @ </td><td><td width="40"></td><td valign="top">
   701    744     @  <label><input type="checkbox" name="ar"%s(oa['r']) />
   702    745     @  Read Ticket%s(B('r'))</label><br />
   703    746     @  <label><input type="checkbox" name="an"%s(oa['n']) />
   704    747     @  New Tickets%s(B('n'))</label><br />
   705    748     @  <label><input type="checkbox" name="ac"%s(oa['c']) />
   706         -  @  Append To Ticket%s(B('c'))</label><br />
          749  +  @  Append To Ticket%s(B('c'))</label><br>
   707    750     @  <label><input type="checkbox" name="aw"%s(oa['w']) />
   708    751     @  Write Tickets%s(B('w'))</label><br />
   709    752     @  <label><input type="checkbox" name="aq"%s(oa['q']) />
   710         -  @  Moderate Tickets%s(B('q'))</label><br />
          753  +  @  Moderate Tickets%s(B('q'))</label>
          754  +
          755  +  @ </td><td><td width="40"></td><td valign="top">
   711    756     @  <label><input type="checkbox" name="at"%s(oa['t']) />
   712    757     @  Ticket Report%s(B('t'))</label><br />
   713    758     @  <label><input type="checkbox" name="ax"%s(oa['x']) />
   714    759     @  Private%s(B('x'))</label><br />
   715    760     @  <label><input type="checkbox" name="ay"%s(oa['y']) />
   716    761     @  Write Unversioned%s(B('y'))</label><br />
   717    762     @  <label><input type="checkbox" name="az"%s(oa['z']) />
   718         -  @  Download Zip%s(B('z'))</label>
          763  +  @  Download Zip%s(B('z'))</label><br />
          764  +  @  <label><input type="checkbox" name="a2"%s(oa['2']) />
          765  +  @  Read Forum%s(B('2'))</label><br />
          766  +  @  <label><input type="checkbox" name="a3"%s(oa['3']) />
          767  +  @  Write Forum%s(B('3'))</label><br />
          768  +  @  <label><input type="checkbox" name="a4"%s(oa['4']) />
          769  +  @  WriteTrusted Forum%s(B('4'))</label><br>
          770  +  @  <label><input type="checkbox" name="a5"%s(oa['5']) />
          771  +  @  Moderate Forum%s(B('5'))</label><br>
          772  +  @  <label><input type="checkbox" name="a6"%s(oa['6']) />
          773  +  @  Supervise Forum%s(B('6'))</label><br>
          774  +  @  <label><input type="checkbox" name="a7"%s(oa['7']) />
          775  +  @  Email Alerts%s(B('7'))</label><br>
          776  +  @  <label><input type="checkbox" name="aA"%s(oa['A']) />
          777  +  @  Send Announcements%s(B('A'))</label>
   719    778     @ </td></tr>
   720    779     @ </table>
   721    780     @   </td>
   722    781     @ </tr>
   723    782     @ <tr>
   724    783     @   <td class="usetupEditLabel">Selected Cap.:</td>
   725    784     @   <td>
   726    785     @     <span id="usetupEditCapability">(missing JS?)</span>
          786  +  @     <a href="%R/setup_ucap_list">(key)</a>
   727    787     @   </td>
   728    788     @ </tr>
   729    789     if( !login_is_special(zLogin) ){
   730    790       @ <tr>
   731    791       @   <td align="right">Password:</td>
   732    792       if( zPw[0] ){
   733    793         /* Obscure the password for all users */
................................................................................
   779    839     @ <li><p>
   780    840     @ The "<span class="ueditInheritNobody"><sub>N</sub></span>" subscript suffix
   781    841     @ indicates the privileges of <span class="usertype">nobody</span> that
   782    842     @ are available to all users regardless of whether or not they are logged in.
   783    843     @ </p></li>
   784    844     @
   785    845     @ <li><p>
   786         -  @ The "<span class="ueditInheritAnonymous"><sub>A</sub></span>" subscript suffix
          846  +  @ The "<span class="ueditInheritAnonymous"><sub>A</sub></span>"
          847  +  @ subscript suffix
   787    848     @ indicates the privileges of <span class="usertype">anonymous</span> that
   788    849     @ are inherited by all logged-in users.
   789    850     @ </p></li>
   790    851     @
   791    852     @ <li><p>
   792         -  @ The "<span class="ueditInheritDeveloper"><sub>D</sub></span>" subscript suffix
   793         -  @ indicates the privileges of <span class="usertype">developer</span> that
          853  +  @ The "<span class="ueditInheritDeveloper"><sub>D</sub></span>"
          854  +  @ subscript suffix indicates the privileges of 
          855  +  @ <span class="usertype">developer</span> that
   794    856     @ are inherited by all users with the
   795    857     @ <span class="capability">Developer</span> privilege.
   796    858     @ </p></li>
   797    859     @
   798    860     @ <li><p>
   799    861     @ The "<span class="ueditInheritReader"><sub>R</sub></span>" subscript suffix
   800    862     @ indicates the privileges of <span class="usertype">reader</span> that
................................................................................
   856    918     @ are allowed to change their own password.  Recommended ON for most
   857    919     @ users but OFF for special users <span class="usertype">developer</span>,
   858    920     @ <span class="usertype">anonymous</span>,
   859    921     @ and <span class="usertype">nobody</span>.
   860    922     @ </p></li>
   861    923     @
   862    924     @ <li><p>
   863         -  @ The <span class="capability">EMail</span> privilege allows the display of
   864         -  @ sensitive information such as the email address of users and contact
          925  +  @ The <span class="capability">View-PII</span> privilege allows the display
          926  +  @ of personally-identifiable information information such as the
          927  +  @ email address of users and contact
   865    928     @ information on tickets. Recommended OFF for
   866    929     @ <span class="usertype">anonymous</span> and for
   867    930     @ <span class="usertype">nobody</span> but ON for
   868    931     @ <span class="usertype">developer</span>.
   869    932     @ </p></li>
   870    933     @
   871    934     @ <li><p>
................................................................................
   918    981     style_footer();
   919    982   }
   920    983   
   921    984   
   922    985   /*
   923    986   ** Generate a checkbox for an attribute.
   924    987   */
   925         -static void onoff_attribute(
          988  +void onoff_attribute(
   926    989     const char *zLabel,   /* The text label on the checkbox */
   927    990     const char *zVar,     /* The corresponding row in the VAR table */
   928    991     const char *zQParm,   /* The query parameter */
   929    992     int dfltVal,          /* Default value if VAR table entry does not exist */
   930    993     int disabled          /* 1 if disabled */
   931    994   ){
   932    995     const char *zQ = P(zQParm);
................................................................................
   971   1034       const int nZQ = (int)strlen(zQ);
   972   1035       login_verify_csrf_secret();
   973   1036       db_set(zVar, zQ, 0);
   974   1037       admin_log("Set entry_attribute %Q to: %.*s%s",
   975   1038                 zVar, 20, zQ, (nZQ>20 ? "..." : ""));
   976   1039       zVal = zQ;
   977   1040     }
   978         -  @ <input type="text" id="%s(zQParm)" name="%s(zQParm)" value="%h(zVal)" size="%d(width)"
         1041  +  @ <input type="text" id="%s(zQParm)" name="%s(zQParm)" value="%h(zVal)" \
         1042  +  @ size="%d(width)" \
   979   1043     if( disabled ){
   980         -    @ disabled="disabled"
         1044  +    @ disabled="disabled" \
   981   1045     }
   982   1046     @ /> <b>%s(zLabel)</b>
   983   1047   }
   984   1048   
   985   1049   /*
   986   1050   ** Generate a text box for an attribute.
   987   1051   */
................................................................................
  1016   1080     }
  1017   1081     return z;
  1018   1082   }
  1019   1083   
  1020   1084   /*
  1021   1085   ** Generate a text box for an attribute.
  1022   1086   */
  1023         -static void multiple_choice_attribute(
         1087  +void multiple_choice_attribute(
  1024   1088     const char *zLabel,   /* The text label on the menu */
  1025   1089     const char *zVar,     /* The corresponding row in the VAR table */
  1026   1090     const char *zQP,      /* The query parameter */
  1027   1091     const char *zDflt,    /* Default value if VAR table entry does not exist */
  1028   1092     int nChoice,          /* Number of choices */
  1029   1093     const char *const *azChoice /* Choices. 2 per choice: (VAR value, Display) */
  1030   1094   ){
................................................................................
  1362   1426       @ </form></p>
  1363   1427       @ <br />For best results, use the same number of <a href="setup_access#ipt">
  1364   1428       @ IP octets</a> in the login cookie across all repositories in the
  1365   1429       @ same Login Group.
  1366   1430       @ <hr /><h2>Implementation Details</h2>
  1367   1431       @ <p>The following are fields from the CONFIG table related to login-groups,
  1368   1432       @ provided here for instructional and debugging purposes:</p>
  1369         -    @ <table border='1' class='sortable' data-column-types='ttt' data-init-sort='1'>
         1433  +    @ <table border='1' class='sortable' data-column-types='ttt' \
         1434  +    @ data-init-sort='1'>
  1370   1435       @ <thead><tr>
  1371   1436       @ <th>Config.Name<th>Config.Value<th>Config.mtime</tr>
  1372   1437       @ </thead><tbody>
  1373   1438       db_prepare(&q, "SELECT name, value, datetime(mtime,'unixepoch') FROM config"
  1374   1439                      " WHERE name GLOB 'peer-*'"
  1375   1440                      "    OR name GLOB 'project-*'"
  1376   1441                      "    OR name GLOB 'login-group-*'"
................................................................................
  1456   1521       @ %s(zTmDiff) hours ahead of UTC.</p>
  1457   1522     }
  1458   1523     @ <p>(Property: "timeline-utc")
  1459   1524     @ <hr />
  1460   1525     multiple_choice_attribute("Per-Item Time Format", "timeline-date-format",
  1461   1526               "tdf", "0", count(azTimeFormats)/2, azTimeFormats);
  1462   1527     @ <p>If the "HH:MM" or "HH:MM:SS" format is selected, then the date is shown
  1463         -  @ in a separate box (using CSS class "timelineDate") whenever the date changes.
  1464         -  @ With the "YYYY-MM-DD&nbsp;HH:MM" and "YYMMDD ..." formats, the complete date
  1465         -  @ and time is shown on every timeline entry using the CSS class "timelineTime".
  1466         -  @ (Preperty: "timeline-date-format")</p>
  1467         -
  1468         -  @ <hr />
  1469         -  onoff_attribute("Show version differences by default",
  1470         -                  "show-version-diffs", "vdiff", 0, 0);
  1471         -  @ <p>The version-information pages linked from the timeline can either
  1472         -  @ show complete diffs of all file changes, or can just list the names of
  1473         -  @ the files that have changed.  Users can get to either page by
  1474         -  @ clicking.  This setting selects the default.
  1475         -  @ (Property: "show-version-diffs")</p>
         1528  +  @ in a separate box (using CSS class "timelineDate") whenever the date
         1529  +  @ changes.  With the "YYYY-MM-DD&nbsp;HH:MM" and "YYMMDD ..." formats,
         1530  +  @ the complete date and time is shown on every timeline entry using the
         1531  +  @ CSS class "timelineTime". (Property: "timeline-date-format")</p>
  1476   1532   
  1477   1533     @ <hr />
  1478   1534     entry_attribute("Max timeline comment length", 6,
  1479   1535                     "timeline-max-comment", "tmc", "0", 0);
  1480   1536     @ <p>The maximum length of a comment to be displayed in a timeline.
  1481   1537     @ "0" there is no length limit.
  1482   1538     @ (Property: "timeline-max-comment")</p>
................................................................................
  1604   1660                        "project-description", "pd", "", 0);
  1605   1661     @ <p>Describe your project. This will be used in page headers for search
  1606   1662     @ engines as well as a short RSS description.
  1607   1663     @ (Property: "project-description")</p>
  1608   1664     @ <hr />
  1609   1665     entry_attribute("Tarball and ZIP-archive Prefix", 20, "short-project-name",
  1610   1666                     "spn", "", 0);
  1611         -  @ <p>This is used as a prefix on the names of generated tarballs and ZIP archive.
  1612         -  @ For best results, keep this prefix brief and avoid special characters such
  1613         -  @ as "/" and "\".
         1667  +  @ <p>This is used as a prefix on the names of generated tarballs and
         1668  +  @ ZIP archive. For best results, keep this prefix brief and avoid special
         1669  +  @ characters such as "/" and "\".
  1614   1670     @ If no tarball prefix is specified, then the full Project Name above is used.
  1615   1671     @ (Property: "short-project-name")
  1616   1672     @ </p>
  1617   1673     @ <hr />
  1618   1674     entry_attribute("Download Tag", 20, "download-tag", "dlt", "trunk", 0);
  1619   1675     @ <p>The <a href='%R/download'>/download</a> page is designed to provide 
  1620   1676     @ a convenient place for newbies
  1621         -  @ to download a ZIP archive or a tarball of the project.  By default, the latest
  1622         -  @ trunk check-in is downloaded.  Change this tag to something else (ex: release)
  1623         -  @ to alter the behavior of the /download page.
         1677  +  @ to download a ZIP archive or a tarball of the project.  By default,
         1678  +  @ the latest trunk check-in is downloaded.  Change this tag to something
         1679  +  @ else (ex: release) to alter the behavior of the /download page.
  1624   1680     @ (Property: "download-tag")
  1625   1681     @ </p>
  1626   1682     @ <hr />
  1627   1683     onoff_attribute("Enable WYSIWYG Wiki Editing",
  1628   1684                     "wysiwyg-wiki", "wysiwyg-wiki", 0, 0);
  1629   1685     @ <p>Enable what-you-see-is-what-you-get (WYSIWYG) editing of wiki pages.
  1630   1686     @ The WYSIWYG editor generates HTML instead of markup, which makes
................................................................................
  1761   1817     @ <input type="submit" name="clear" value="Delete Ad-Unit" />
  1762   1818     @ </div></form>
  1763   1819     @ <hr />
  1764   1820     @ <b>Ad-Unit Notes:</b><ul>
  1765   1821     @ <li>Leave both Ad-Units blank to disable all advertising.
  1766   1822     @ <li>The "Banner Ad-Unit" is used for wide pages.
  1767   1823     @ <li>The "Right-Column Ad-Unit" is used on pages with tall, narrow content.
  1768         -  @ <li>If the "Right-Column Ad-Unit" is blank, the "Banner Ad-Unit" is used on all pages.
         1824  +  @ <li>If the "Right-Column Ad-Unit" is blank, the "Banner Ad-Unit" is
         1825  +  @     used on all pages.
  1769   1826     @ <li>Properties: "adunit", "adunit-right", "adunit-omit-if-admin", and
  1770   1827     @     "adunit-omit-if-user".
  1771   1828     @ <li>Suggested <a href="setup_skinedit?w=0">CSS</a> changes:
  1772   1829     @ <blockquote><pre>
  1773   1830     @ div.adunit_banner {
  1774   1831     @   margin: auto;
  1775   1832     @   width: 100%%;
................................................................................
  1874   1931       );
  1875   1932       db_end_transaction(0);
  1876   1933       cgi_redirect("setup_logo");
  1877   1934     }
  1878   1935     style_header("Edit Project Logo And Background");
  1879   1936     @ <p>The current project logo has a MIME-Type of <b>%h(zLogoMime)</b>
  1880   1937     @ and looks like this:</p>
  1881         -  @ <blockquote><p><img src="%s(g.zTop)/logo/%z(zLogoMtime)" alt="logo" border="1" />
         1938  +  @ <blockquote><p><img src="%s(g.zTop)/logo/%z(zLogoMtime)" \
         1939  +  @ alt="logo" border="1" />
  1882   1940     @ </p></blockquote>
  1883   1941     @
  1884   1942     @ <form action="%s(g.zTop)/setup_logo" method="post"
  1885   1943     @  enctype="multipart/form-data"><div>
  1886   1944     @ <p>The logo is accessible to all users at this URL:
  1887   1945     @ <a href="%s(g.zBaseURL)/logo">%s(g.zBaseURL)/logo</a>.
  1888   1946     @ The logo may or may not appear on each
................................................................................
  1897   1955     @ <input type="submit" name="clrlogo" value="Revert To Default" /></p>
  1898   1956     @ <p>(Properties: "logo-image" and "logo-mimetype")
  1899   1957     @ </div></form>
  1900   1958     @ <hr />
  1901   1959     @
  1902   1960     @ <p>The current background image has a MIME-Type of <b>%h(zBgMime)</b>
  1903   1961     @ and looks like this:</p>
  1904         -  @ <blockquote><p><img src="%s(g.zTop)/background/%z(zBgMtime)" alt="background" border=1 />
         1962  +  @ <blockquote><p><img src="%s(g.zTop)/background/%z(zBgMtime)" \
         1963  +  @ alt="background" border=1 />
  1905   1964     @ </p></blockquote>
  1906   1965     @
  1907   1966     @ <form action="%s(g.zTop)/setup_logo" method="post"
  1908   1967     @  enctype="multipart/form-data"><div>
  1909   1968     @ <p>The background image is accessible to all users at this URL:
  1910   1969     @ <a href="%s(g.zBaseURL)/background">%s(g.zBaseURL)/background</a>.
  1911   1970     @ The background image may or may not appear on each
................................................................................
  1989   2048     @ </ul></p>
  1990   2049   #endif
  1991   2050   
  1992   2051     if( P("configtab") ){
  1993   2052       /* If the user presses the "CONFIG Table Query" button, populate the
  1994   2053       ** query text with a pre-packaged query against the CONFIG table */
  1995   2054       zQ = "SELECT\n"
  1996         -         " CASE WHEN length(name)<50 THEN name ELSE printf('%.50s...',name) END AS name,\n"
         2055  +         " CASE WHEN length(name)<50 THEN name ELSE printf('%.50s...',name)"
         2056  +         "  END AS name,\n"
  1997   2057            " CASE WHEN typeof(value)<>'blob' AND length(value)<80 THEN value\n"
  1998   2058            "           ELSE '...' END AS value,\n"
  1999   2059            " datetime(mtime, 'unixepoch') AS mtime\n"
  2000   2060            "FROM config\n"
  2001   2061            "-- ORDER BY mtime DESC; -- optional";
  2002   2062        go = 1;
  2003   2063     }
................................................................................
  2009   2069     @ <input type="submit" name="go" value="Run SQL">
  2010   2070     @ <input type="submit" name="schema" value="Show Schema">
  2011   2071     @ <input type="submit" name="tablelist" value="List Tables">
  2012   2072     @ <input type="submit" name="configtab" value="CONFIG Table Query">
  2013   2073     @ </form>
  2014   2074     if( P("schema") ){
  2015   2075       zQ = sqlite3_mprintf(
  2016         -            "SELECT sql FROM repository.sqlite_master WHERE sql IS NOT NULL ORDER BY name");
         2076  +            "SELECT sql FROM repository.sqlite_master"
         2077  +            " WHERE sql IS NOT NULL ORDER BY name");
  2017   2078       go = 1;
  2018   2079     }else if( P("tablelist") ){
  2019   2080       zQ = sqlite3_mprintf(
  2020   2081               "SELECT name FROM repository.sqlite_master WHERE type='table'"
  2021   2082               " ORDER BY name");
  2022   2083       go = 1;
  2023   2084     }
................................................................................
  2387   2448     db_finalize(&q);
  2388   2449     @ <tr><td>
  2389   2450     @ <input type='hidden' name='namelist' value='%h(blob_str(&namelist))'>
  2390   2451     @ <input type='submit' name='submit' value="Apply Changes">
  2391   2452     @ </td><td></td></tr>
  2392   2453     @ </table></form>
  2393   2454     @ <hr>
  2394         -  @ <p>When the first term of an incoming URL exactly matches one of the "Aliases" on
  2395         -  @ the left-hand side (LHS) above, the URL is converted into the corresponding form
  2396         -  @ on the right-hand side (RHS).
         2455  +  @ <p>When the first term of an incoming URL exactly matches one of
         2456  +  @ the "Aliases" on the left-hand side (LHS) above, the URL is
         2457  +  @ converted into the corresponding form on the right-hand side (RHS).
  2397   2458     @ <ul>
  2398   2459     @ <li><p>
  2399   2460     @ The LHS is compared against only the first term of the incoming URL.
  2400   2461     @ All LHS entries in the alias table should therefore begin with a
  2401   2462     @ single "/" followed by a single path element.
  2402   2463     @ <li><p>
  2403         -  @ The RHS entries in the alias table should begin with a single "/" followed by
  2404         -  @ a path element, and optionally followed by "?" and a list of query parameters.
         2464  +  @ The RHS entries in the alias table should begin with a single "/"
         2465  +  @ followed by a path element, and optionally followed by "?" and a
         2466  +  @ list of query parameters.
  2405   2467     @ <li><p>
  2406   2468     @ Query parameters on the RHS are added to the set of query parameters
  2407   2469     @ in the incoming URL.
  2408   2470     @ <li><p>
  2409         -  @ If the same query parameter appears in both the incoming URL and on the RHS of the
  2410         -  @ alias, the RHS query parameter value overwrites the value on the incoming URL.
         2471  +  @ If the same query parameter appears in both the incoming URL and
         2472  +  @ on the RHS of the alias, the RHS query parameter value overwrites
         2473  +  @ the value on the incoming URL.
  2411   2474     @ <li><p>
  2412         -  @ If a query parameter on the RHS of the alias is of the form "X!" (a name followed
  2413         -  @ by "!") then the X query parameter is removed from the incoming URL if it exists.
         2475  +  @ If a query parameter on the RHS of the alias is of the form "X!"
         2476  +  @ (a name followed by "!") then the X query parameter is removed
         2477  +  @ from the incoming URL if
         2478  +  @ it exists.
  2414   2479     @ <li><p>
  2415   2480     @ Only a single alias operation occurs.  It is not possible to nest aliases.
  2416   2481     @ The RHS entries must be built-in webpage names.
  2417   2482     @ <li><p>
  2418         -  @ The alias table is only checked if no built-in webpage matches the incoming URL.
  2419         -  @ Hence, it is not possible to override a built-in webpage using aliases.  This is
  2420         -  @ by design.
         2483  +  @ The alias table is only checked if no built-in webpage matches
         2484  +  @ the incoming URL.
         2485  +  @ Hence, it is not possible to override a built-in webpage using aliases.
         2486  +  @ This is by design.
  2421   2487     @ </ul>
  2422   2488     @
  2423   2489     @ <p>To delete an entry from the alias table, change its name or value to an
  2424   2490     @ empty string and press "Apply Changes".
  2425   2491     @
  2426         -  @ <p>To add a new alias, fill in the name and value in the bottom row of the table
  2427         -  @ above and press "Apply Changes".
         2492  +  @ <p>To add a new alias, fill in the name and value in the bottom row
         2493  +  @ of the table above and press "Apply Changes".
  2428   2494     style_footer();
  2429   2495   }

Changes to src/shell.c.

  7615   7615       IdxHashEntry *pEntry;
  7616   7616       sqlite3_stmt *pExplain = 0;
  7617   7617       idxHashClear(&hIdx);
  7618   7618       rc = idxPrintfPrepareStmt(dbm, &pExplain, pzErr,
  7619   7619           "EXPLAIN QUERY PLAN %s", pStmt->zSql
  7620   7620       );
  7621   7621       while( rc==SQLITE_OK && sqlite3_step(pExplain)==SQLITE_ROW ){
  7622         -      int iSelectid = sqlite3_column_int(pExplain, 0);
  7623         -      int iOrder = sqlite3_column_int(pExplain, 1);
  7624         -      int iFrom = sqlite3_column_int(pExplain, 2);
         7622  +      /* int iId = sqlite3_column_int(pExplain, 0); */
         7623  +      /* int iParent = sqlite3_column_int(pExplain, 1); */
         7624  +      /* int iNotUsed = sqlite3_column_int(pExplain, 2); */
  7625   7625         const char *zDetail = (const char*)sqlite3_column_text(pExplain, 3);
  7626   7626         int nDetail = STRLEN(zDetail);
  7627   7627         int i;
  7628   7628   
  7629   7629         for(i=0; i<nDetail; i++){
  7630   7630           const char *zIdx = 0;
  7631   7631           if( memcmp(&zDetail[i], " USING INDEX ", 13)==0 ){
................................................................................
  7644   7644               idxHashAdd(&rc, &hIdx, zSql, 0);
  7645   7645               if( rc ) goto find_indexes_out;
  7646   7646             }
  7647   7647             break;
  7648   7648           }
  7649   7649         }
  7650   7650   
  7651         -      pStmt->zEQP = idxAppendText(&rc, pStmt->zEQP, "%d|%d|%d|%s\n", 
  7652         -          iSelectid, iOrder, iFrom, zDetail
  7653         -      );
         7651  +      if( zDetail[0]!='-' ){
         7652  +        pStmt->zEQP = idxAppendText(&rc, pStmt->zEQP, "%s\n", zDetail);
         7653  +      }
  7654   7654       }
  7655   7655   
  7656   7656       for(pEntry=hIdx.pFirst; pEntry; pEntry=pEntry->pNext){
  7657   7657         pStmt->zIdx = idxAppendText(&rc, pStmt->zIdx, "%s;\n", pEntry->zKey);
  7658   7658       }
  7659   7659   
  7660   7660       idxFinalize(&rc, pExplain);
................................................................................
  8479   8479     sqlite3expert *pExpert;
  8480   8480     int bVerbose;
  8481   8481   };
  8482   8482   
  8483   8483   /* A single line in the EQP output */
  8484   8484   typedef struct EQPGraphRow EQPGraphRow;
  8485   8485   struct EQPGraphRow {
  8486         -  int iSelectId;        /* The SelectID for this row */
         8486  +  int iEqpId;           /* ID for this row */
         8487  +  int iParentId;        /* ID of the parent row */
  8487   8488     EQPGraphRow *pNext;   /* Next row in sequence */
  8488   8489     char zText[1];        /* Text to display for this row */
  8489   8490   };
  8490   8491   
  8491   8492   /* All EQP output is collected into an instance of the following */
  8492   8493   typedef struct EQPGraph EQPGraph;
  8493   8494   struct EQPGraph {
................................................................................
  8501   8502   ** instance of the following structure.
  8502   8503   */
  8503   8504   typedef struct ShellState ShellState;
  8504   8505   struct ShellState {
  8505   8506     sqlite3 *db;           /* The database */
  8506   8507     u8 autoExplain;        /* Automatically turn on .explain mode */
  8507   8508     u8 autoEQP;            /* Run EXPLAIN QUERY PLAN prior to seach SQL stmt */
         8509  +  u8 autoEQPtest;        /* autoEQP is in test mode */
  8508   8510     u8 statsOn;            /* True to display memory stats before each finalize */
  8509   8511     u8 scanstatsOn;        /* True to display scan stats before each finalize */
  8510   8512     u8 openMode;           /* SHELL_OPEN_NORMAL, _APPENDVFS, or _ZIPFILE */
  8511   8513     u8 doXdgOpen;          /* Invoke start/open/xdg-open in output_reset() */
  8512   8514     u8 nEqpLevel;          /* Depth of the EQP output graph */
  8513   8515     unsigned mEqpLines;    /* Mask of veritical lines in the EQP output graph */
  8514   8516     int outCount;          /* Revert to stdout when reaching zero */
................................................................................
  8551   8553   #endif
  8552   8554     ExpertInfo expert;        /* Valid if previous command was ".expert OPT..." */
  8553   8555   };
  8554   8556   
  8555   8557   
  8556   8558   /* Allowed values for ShellState.autoEQP
  8557   8559   */
  8558         -#define AUTOEQP_off      0
  8559         -#define AUTOEQP_on       1
  8560         -#define AUTOEQP_trigger  2
  8561         -#define AUTOEQP_full     3
         8560  +#define AUTOEQP_off      0           /* Automatic EXPLAIN QUERY PLAN is off */
         8561  +#define AUTOEQP_on       1           /* Automatic EQP is on */
         8562  +#define AUTOEQP_trigger  2           /* On and also show plans for triggers */
         8563  +#define AUTOEQP_full     3           /* Show full EXPLAIN */
  8562   8564   
  8563   8565   /* Allowed values for ShellState.openMode
  8564   8566   */
  8565   8567   #define SHELL_OPEN_UNSPEC     0      /* No open-mode specified */
  8566   8568   #define SHELL_OPEN_NORMAL     1      /* Normal database file */
  8567   8569   #define SHELL_OPEN_APPENDVFS  2      /* Use appendvfs */
  8568   8570   #define SHELL_OPEN_ZIPFILE    3      /* Use the zipfile virtual table */
................................................................................
  9165   9167     }
  9166   9168     return 1;
  9167   9169   }
  9168   9170   
  9169   9171   /*
  9170   9172   ** Add a new entry to the EXPLAIN QUERY PLAN data
  9171   9173   */
  9172         -static void eqp_append(ShellState *p, int iSelectId, const char *zText){
         9174  +static void eqp_append(ShellState *p, int iEqpId, int p2, const char *zText){
  9173   9175     EQPGraphRow *pNew;
  9174   9176     int nText = strlen30(zText);
         9177  +  if( p->autoEQPtest ){
         9178  +    utf8_printf(p->out, "%d,%d,%s\n", iEqpId, p2, zText);
         9179  +  }
  9175   9180     pNew = sqlite3_malloc64( sizeof(*pNew) + nText );
  9176   9181     if( pNew==0 ) shell_out_of_memory();
  9177         -  pNew->iSelectId = iSelectId;
         9182  +  pNew->iEqpId = iEqpId;
         9183  +  pNew->iParentId = p2;
  9178   9184     memcpy(pNew->zText, zText, nText+1);
  9179   9185     pNew->pNext = 0;
  9180   9186     if( p->sGraph.pLast ){
  9181   9187       p->sGraph.pLast->pNext = pNew;
  9182   9188     }else{
  9183   9189       p->sGraph.pRow = pNew;
  9184   9190     }
................................................................................
  9194   9200     for(pRow = p->sGraph.pRow; pRow; pRow = pNext){
  9195   9201       pNext = pRow->pNext;
  9196   9202       sqlite3_free(pRow);
  9197   9203     }
  9198   9204     memset(&p->sGraph, 0, sizeof(p->sGraph));
  9199   9205   }
  9200   9206   
  9201         -/* Return the next EXPLAIN QUERY PLAN line with iSelectId that occurs after
         9207  +/* Return the next EXPLAIN QUERY PLAN line with iEqpId that occurs after
  9202   9208   ** pOld, or return the first such line if pOld is NULL
  9203   9209   */
  9204         -static EQPGraphRow *eqp_next_row(ShellState *p, int iSelectId, EQPGraphRow *pOld){
         9210  +static EQPGraphRow *eqp_next_row(ShellState *p, int iEqpId, EQPGraphRow *pOld){
  9205   9211     EQPGraphRow *pRow = pOld ? pOld->pNext : p->sGraph.pRow;
  9206         -  while( pRow && pRow->iSelectId!=iSelectId ) pRow = pRow->pNext;
         9212  +  while( pRow && pRow->iParentId!=iEqpId ) pRow = pRow->pNext;
  9207   9213     return pRow;
  9208   9214   }
  9209   9215   
  9210         -/* Render a single level of the graph shell having iSelectId.  Called
         9216  +/* Render a single level of the graph that has iEqpId as its parent.  Called
  9211   9217   ** recursively to render sublevels.
  9212   9218   */
  9213         -static void eqp_render_level(ShellState *p, int iSelectId){
         9219  +static void eqp_render_level(ShellState *p, int iEqpId){
  9214   9220     EQPGraphRow *pRow, *pNext;
  9215         -  int i;
  9216   9221     int n = strlen30(p->sGraph.zPrefix);
  9217   9222     char *z;
  9218         -  for(pRow = eqp_next_row(p, iSelectId, 0); pRow; pRow = pNext){
  9219         -    pNext = eqp_next_row(p, iSelectId, pRow);
         9223  +  for(pRow = eqp_next_row(p, iEqpId, 0); pRow; pRow = pNext){
         9224  +    pNext = eqp_next_row(p, iEqpId, pRow);
  9220   9225       z = pRow->zText;
  9221   9226       utf8_printf(p->out, "%s%s%s\n", p->sGraph.zPrefix, pNext ? "|--" : "`--", z);
  9222         -    if( n<sizeof(p->sGraph.zPrefix)-7 && (z = strstr(z, " SUBQUER"))!=0 ){
         9227  +    if( n<(int)sizeof(p->sGraph.zPrefix)-7 ){
  9223   9228         memcpy(&p->sGraph.zPrefix[n], pNext ? "|  " : "   ", 4);
  9224         -      if( strncmp(z, " SUBQUERY ", 9)==0 && (i = atoi(z+10))>iSelectId ){
  9225         -        eqp_render_level(p, i);
  9226         -      }else if( strncmp(z, " SUBQUERIES ", 12)==0 ){
  9227         -        i = atoi(z+12);
  9228         -        if( i>iSelectId ){
  9229         -          utf8_printf(p->out, "%s|--SUBQUERY %d\n", p->sGraph.zPrefix, i);
  9230         -          memcpy(&p->sGraph.zPrefix[n+3],"|  ",4);
  9231         -          eqp_render_level(p, i);
  9232         -        }
  9233         -        z = strstr(z, " AND ");
  9234         -        if( z && (i = atoi(z+5))>iSelectId ){
  9235         -          p->sGraph.zPrefix[n+3] = 0;
  9236         -          utf8_printf(p->out, "%s`--SUBQUERY %d\n", p->sGraph.zPrefix, i);
  9237         -          memcpy(&p->sGraph.zPrefix[n+3],"   ",4);
  9238         -          eqp_render_level(p, i);
  9239         -        }
  9240         -      }
         9229  +      eqp_render_level(p, pRow->iEqpId);
  9241   9230         p->sGraph.zPrefix[n] = 0;
  9242   9231       }
  9243   9232     }
  9244   9233   }
  9245   9234   
  9246   9235   /*
  9247   9236   ** Display and reset the EXPLAIN QUERY PLAN data
................................................................................
  9542   9531               output_quoted_escaped_string(p->out, azArg[i]);
  9543   9532             }
  9544   9533           }else if( aiType && aiType[i]==SQLITE_INTEGER ){
  9545   9534             utf8_printf(p->out,"%s", azArg[i]);
  9546   9535           }else if( aiType && aiType[i]==SQLITE_FLOAT ){
  9547   9536             char z[50];
  9548   9537             double r = sqlite3_column_double(p->pStmt, i);
  9549         -          sqlite3_snprintf(50,z,"%!.20g", r);
  9550         -          raw_printf(p->out, "%s", z);
         9538  +          sqlite3_uint64 ur;
         9539  +          memcpy(&ur,&r,sizeof(r));
         9540  +          if( ur==0x7ff0000000000000LL ){
         9541  +            raw_printf(p->out, "1e999");
         9542  +          }else if( ur==0xfff0000000000000LL ){
         9543  +            raw_printf(p->out, "-1e999");
         9544  +          }else{
         9545  +            sqlite3_snprintf(50,z,"%!.20g", r);
         9546  +            raw_printf(p->out, "%s", z);
         9547  +          }
  9551   9548           }else if( aiType && aiType[i]==SQLITE_BLOB && p->pStmt ){
  9552   9549             const void *pBlob = sqlite3_column_blob(p->pStmt, i);
  9553   9550             int nBlob = sqlite3_column_bytes(p->pStmt, i);
  9554   9551             output_hex_blob(p->out, pBlob, nBlob);
  9555   9552           }else if( isNumber(azArg[i], 0) ){
  9556   9553             utf8_printf(p->out,"%s", azArg[i]);
  9557   9554           }else if( ShellHasFlag(p, SHFLG_Newlines) ){
................................................................................
  9612   9609           if( i>0 ) utf8_printf(p->out, "%s", p->colSeparator);
  9613   9610           utf8_printf(p->out,"%s",azArg[i] ? azArg[i] : p->nullValue);
  9614   9611         }
  9615   9612         utf8_printf(p->out, "%s", p->rowSeparator);
  9616   9613         break;
  9617   9614       }
  9618   9615       case MODE_EQP: {
  9619         -      eqp_append(p, atoi(azArg[0]), azArg[3]);
         9616  +      eqp_append(p, atoi(azArg[0]), atoi(azArg[1]), azArg[3]);
  9620   9617         break;
  9621   9618       }
  9622   9619     }
  9623   9620     return 0;
  9624   9621   }
  9625   9622   
  9626   9623   /*
................................................................................
 10076  10073   static void explain_data_prepare(ShellState *p, sqlite3_stmt *pSql){
 10077  10074     const char *zSql;               /* The text of the SQL statement */
 10078  10075     const char *z;                  /* Used to check if this is an EXPLAIN */
 10079  10076     int *abYield = 0;               /* True if op is an OP_Yield */
 10080  10077     int nAlloc = 0;                 /* Allocated size of p->aiIndent[], abYield */
 10081  10078     int iOp;                        /* Index of operation in p->aiIndent[] */
 10082  10079   
 10083         -  const char *azNext[] = { "Next", "Prev", "VPrev", "VNext", "SorterNext",
 10084         -                           "NextIfOpen", "PrevIfOpen", 0 };
        10080  +  const char *azNext[] = { "Next", "Prev", "VPrev", "VNext", "SorterNext", 0 };
 10085  10081     const char *azYield[] = { "Yield", "SeekLT", "SeekGT", "RowSetRead",
 10086  10082                               "Rewind", 0 };
 10087  10083     const char *azGoto[] = { "Goto", 0 };
 10088  10084   
 10089  10085     /* Try to figure out if this is really an EXPLAIN statement. If this
 10090  10086     ** cannot be verified, return early.  */
 10091  10087     if( sqlite3_column_count(pSql)!=8 ){
................................................................................
 10454  10450             sqlite3_db_config(db, SQLITE_DBCONFIG_TRIGGER_EQP, 1, 0);
 10455  10451           }
 10456  10452           zEQP = sqlite3_mprintf("EXPLAIN QUERY PLAN %s", zStmtSql);
 10457  10453           rc = sqlite3_prepare_v2(db, zEQP, -1, &pExplain, 0);
 10458  10454           if( rc==SQLITE_OK ){
 10459  10455             while( sqlite3_step(pExplain)==SQLITE_ROW ){
 10460  10456               const char *zEQPLine = (const char*)sqlite3_column_text(pExplain,3);
 10461         -            int iSelectId = sqlite3_column_int(pExplain, 0);
        10457  +            int iEqpId = sqlite3_column_int(pExplain, 0);
        10458  +            int iParentId = sqlite3_column_int(pExplain, 1);
 10462  10459               if( zEQPLine[0]=='-' ) eqp_render(pArg);
 10463         -            eqp_append(pArg, iSelectId, zEQPLine);
        10460  +            eqp_append(pArg, iEqpId, iParentId, zEQPLine);
 10464  10461             }
 10465  10462             eqp_render(pArg);
 10466  10463           }
 10467  10464           sqlite3_finalize(pExplain);
 10468  10465           sqlite3_free(zEQP);
 10469  10466           if( pArg->autoEQP>=AUTOEQP_full ){
 10470  10467             /* Also do an EXPLAIN for ".eqp full" mode */
................................................................................
 10480  10477             sqlite3_free(zEQP);
 10481  10478           }
 10482  10479           if( pArg->autoEQP>=AUTOEQP_trigger && triggerEQP==0 ){
 10483  10480             sqlite3_db_config(db, SQLITE_DBCONFIG_TRIGGER_EQP, 0, 0);
 10484  10481             /* Reprepare pStmt before reactiving trace modes */
 10485  10482             sqlite3_finalize(pStmt);
 10486  10483             sqlite3_prepare_v2(db, zSql, -1, &pStmt, 0);
        10484  +          if( pArg ) pArg->pStmt = pStmt;
 10487  10485           }
 10488  10486           restore_debug_trace_modes();
 10489  10487         }
 10490  10488   
 10491  10489         if( pArg ){
 10492  10490           pArg->cMode = pArg->mode;
 10493  10491           if( pArg->autoExplain ){
................................................................................
 10831  10829   #if defined(SQLITE_HAVE_ZLIB) && !defined(SQLITE_OMIT_VIRTUALTABLE)
 10832  10830     ".archive ...           Manage SQL archives: \".archive --help\" for details\n"
 10833  10831   #endif
 10834  10832   #ifndef SQLITE_OMIT_AUTHORIZATION
 10835  10833     ".auth ON|OFF           Show authorizer callbacks\n"
 10836  10834   #endif
 10837  10835     ".backup ?DB? FILE      Backup DB (default \"main\") to FILE\n"
        10836  +  "                         Add \"--append\" to open using appendvfs.\n"
 10838  10837     ".bail on|off           Stop after hitting an error.  Default OFF\n"
 10839  10838     ".binary on|off         Turn binary output on or off.  Default OFF\n"
 10840  10839     ".cd DIRECTORY          Change the working directory to DIRECTORY\n"
 10841  10840     ".changes on|off        Show number of rows changed by SQL\n"
 10842  10841     ".check GLOB            Fail if output since .testcase does not match\n"
 10843  10842     ".clone NEWDB           Clone data into NEWDB from the existing database\n"
 10844  10843     ".databases             List names and files of attached databases\n"
        10844  +  ".dbconfig ?op? ?val?   List or change sqlite3_db_config() options\n"
 10845  10845     ".dbinfo ?DB?           Show status information about the database\n"
 10846  10846     ".dump ?TABLE? ...      Dump the database in an SQL text format\n"
 10847  10847     "                         If TABLE specified, only dump tables matching\n"
 10848  10848     "                         LIKE pattern TABLE.\n"
 10849  10849     ".echo on|off           Turn command echo on or off\n"
 10850  10850     ".eqp on|off|full       Enable or disable automatic EXPLAIN QUERY PLAN\n"
 10851  10851     ".excel                 Display the output of next command in a spreadsheet\n"
................................................................................
 11047  11047   ** one of the SHELL_OPEN_* constants.
 11048  11048   **
 11049  11049   ** If the file does not exist or is empty but its name looks like a ZIP
 11050  11050   ** archive and the dfltZip flag is true, then assume it is a ZIP archive.
 11051  11051   ** Otherwise, assume an ordinary database regardless of the filename if
 11052  11052   ** the type cannot be determined from content.
 11053  11053   */
 11054         -static int deduceDatabaseType(const char *zName, int dfltZip){
        11054  +int deduceDatabaseType(const char *zName, int dfltZip){
 11055  11055     FILE *f = fopen(zName, "rb");
 11056  11056     size_t n;
 11057  11057     int rc = SHELL_OPEN_UNSPEC;
 11058  11058     char zBuf[100];
 11059  11059     if( f==0 ){
 11060         -    if( dfltZip && sqlite3_strlike("%.zip",zName,0)==0 ) return SHELL_OPEN_ZIPFILE;
 11061         -    return SHELL_OPEN_NORMAL;
        11060  +    if( dfltZip && sqlite3_strlike("%.zip",zName,0)==0 ){
        11061  +       return SHELL_OPEN_ZIPFILE;
        11062  +    }else{
        11063  +       return SHELL_OPEN_NORMAL;
        11064  +    }
 11062  11065     }
 11063  11066     fseek(f, -25, SEEK_END);
 11064  11067     n = fread(zBuf, 25, 1, f);
 11065  11068     if( n==1 && memcmp(zBuf, "Start-Of-SQLite3-", 17)==0 ){
 11066  11069       rc = SHELL_OPEN_APPENDVFS;
 11067  11070     }else{
 11068  11071       fseek(f, -22, SEEK_END);
 11069  11072       n = fread(zBuf, 22, 1, f);
 11070  11073       if( n==1 && zBuf[0]==0x50 && zBuf[1]==0x4b && zBuf[2]==0x05
 11071  11074          && zBuf[3]==0x06 ){
 11072  11075         rc = SHELL_OPEN_ZIPFILE;
 11073  11076       }else if( n==0 && dfltZip && sqlite3_strlike("%.zip",zName,0)==0 ){
 11074         -      return SHELL_OPEN_ZIPFILE;
        11077  +      rc = SHELL_OPEN_ZIPFILE;
 11075  11078       }
 11076  11079     }
 11077  11080     fclose(f);
 11078  11081     return rc;  
 11079  11082   }
 11080  11083   
        11084  +/* Flags for open_db().
        11085  +**
        11086  +** The default behavior of open_db() is to exit(1) if the database fails to
        11087  +** open.  The OPEN_DB_KEEPALIVE flag changes that so that it prints an error
        11088  +** but still returns without calling exit.
        11089  +**
        11090  +** The OPEN_DB_ZIPFILE flag causes open_db() to prefer to open files as a
        11091  +** ZIP archive if the file does not exist or is empty and its name matches
        11092  +** the *.zip pattern.
        11093  +*/
        11094  +#define OPEN_DB_KEEPALIVE   0x001   /* Return after error if true */
        11095  +#define OPEN_DB_ZIPFILE     0x002   /* Open as ZIP if name matches *.zip */
        11096  +
 11081  11097   /*
 11082  11098   ** Make sure the database is open.  If it is not, then open it.  If
 11083  11099   ** the database fails to open, print an error message and exit.
 11084  11100   */
 11085         -static void open_db(ShellState *p, int keepAlive){
        11101  +static void open_db(ShellState *p, int openFlags){
 11086  11102     if( p->db==0 ){
 11087         -    if( p->openMode==SHELL_OPEN_UNSPEC && access(p->zDbFilename,0)==0 ){
 11088         -      p->openMode = (u8)deduceDatabaseType(p->zDbFilename, 0);
        11103  +    if( p->openMode==SHELL_OPEN_UNSPEC ){
        11104  +      if( p->zDbFilename==0 || p->zDbFilename[0]==0 ){
        11105  +        p->openMode = SHELL_OPEN_NORMAL;
        11106  +      }else{
        11107  +        p->openMode = (u8)deduceDatabaseType(p->zDbFilename, 
        11108  +                             (openFlags & OPEN_DB_ZIPFILE)!=0);
        11109  +      }
 11089  11110       }
 11090  11111       switch( p->openMode ){
 11091  11112         case SHELL_OPEN_APPENDVFS: {
 11092  11113           sqlite3_open_v2(p->zDbFilename, &p->db, 
 11093  11114              SQLITE_OPEN_READWRITE|SQLITE_OPEN_CREATE, "apndvfs");
 11094  11115           break;
 11095  11116         }
................................................................................
 11107  11128           break;
 11108  11129         }
 11109  11130       }
 11110  11131       globalDb = p->db;
 11111  11132       if( p->db==0 || SQLITE_OK!=sqlite3_errcode(p->db) ){
 11112  11133         utf8_printf(stderr,"Error: unable to open database \"%s\": %s\n",
 11113  11134             p->zDbFilename, sqlite3_errmsg(p->db));
 11114         -      if( keepAlive ) return;
        11135  +      if( openFlags & OPEN_DB_KEEPALIVE ) return;
 11115  11136         exit(1);
 11116  11137       }
 11117  11138   #ifndef SQLITE_OMIT_LOAD_EXTENSION
 11118  11139       sqlite3_enable_load_extension(p->db, 1);
 11119  11140   #endif
 11120  11141       sqlite3_fileio_init(p->db, 0, 0);
 11121  11142       sqlite3_shathree_init(p->db, 0, 0);
................................................................................
 11140  11161         char *zSql = sqlite3_mprintf(
 11141  11162            "CREATE VIRTUAL TABLE zip USING zipfile(%Q);", p->zDbFilename);
 11142  11163         sqlite3_exec(p->db, zSql, 0, 0, 0);
 11143  11164         sqlite3_free(zSql);
 11144  11165       }
 11145  11166     }
 11146  11167   }
        11168  +
        11169  +/*
        11170  +** Attempt to close the databaes connection.  Report errors.
        11171  +*/
        11172  +void close_db(sqlite3 *db){
        11173  +  int rc = sqlite3_close(db);
        11174  +  if( rc ){
        11175  +    utf8_printf(stderr, "Error: sqlite3_close() returns %d: %s\n",
        11176  +        rc, sqlite3_errmsg(db));
        11177  +  } 
        11178  +}
 11147  11179   
 11148  11180   #if HAVE_READLINE || HAVE_EDITLINE
 11149  11181   /*
 11150  11182   ** Readline completion callbacks
 11151  11183   */
 11152  11184   static char *readline_completion_generator(const char *text, int state){
 11153  11185     static sqlite3_stmt *pStmt = 0;
................................................................................
 11182  11214     int nLine = strlen30(zLine);
 11183  11215     int i, iStart;
 11184  11216     sqlite3_stmt *pStmt = 0;
 11185  11217     char *zSql;
 11186  11218     char zBuf[1000];
 11187  11219   
 11188  11220     if( nLine>sizeof(zBuf)-30 ) return;
 11189         -  if( zLine[0]=='.' ) return;
        11221  +  if( zLine[0]=='.' || zLine[0]=='#') return;
 11190  11222     for(i=nLine-1; i>=0 && (isalnum(zLine[i]) || zLine[i]=='_'); i--){}
 11191  11223     if( i==nLine-1 ) return;
 11192  11224     iStart = i+1;
 11193  11225     memcpy(zBuf, zLine, iStart);
 11194  11226     zSql = sqlite3_mprintf("SELECT DISTINCT candidate COLLATE nocase"
 11195  11227                            "  FROM completion(%Q,%Q) ORDER BY 1",
 11196  11228                            &zLine[iStart], zLine);
................................................................................
 11721  11753       sqlite3_exec(p->db, "PRAGMA writable_schema=ON;", 0, 0, 0);
 11722  11754       sqlite3_exec(newDb, "BEGIN EXCLUSIVE;", 0, 0, 0);
 11723  11755       tryToCloneSchema(p, newDb, "type='table'", tryToCloneData);
 11724  11756       tryToCloneSchema(p, newDb, "type!='table'", 0);
 11725  11757       sqlite3_exec(newDb, "COMMIT;", 0, 0, 0);
 11726  11758       sqlite3_exec(p->db, "PRAGMA writable_schema=OFF;", 0, 0, 0);
 11727  11759     }
 11728         -  sqlite3_close(newDb);
        11760  +  close_db(newDb);
 11729  11761   }
 11730  11762   
 11731  11763   /*
 11732  11764   ** Change the output file back to stdout.
 11733  11765   **
 11734  11766   ** If the p->doXdgOpen flag is set, that means the output was being
 11735  11767   ** redirected to a temporary file named by p->zTempFile.  In that case,
................................................................................
 12376  12408   typedef struct ArCommand ArCommand;
 12377  12409   struct ArCommand {
 12378  12410     u8 eCmd;                        /* An AR_CMD_* value */
 12379  12411     u8 bVerbose;                    /* True if --verbose */
 12380  12412     u8 bZip;                        /* True if the archive is a ZIP */
 12381  12413     u8 bDryRun;                     /* True if --dry-run */
 12382  12414     u8 bAppend;                     /* True if --append */
        12415  +  u8 fromCmdLine;                 /* Run from -A instead of .archive */
 12383  12416     int nArg;                       /* Number of command arguments */
 12384  12417     char *zSrcTable;                /* "sqlar", "zipfile($file)" or "zip" */
 12385  12418     const char *zFile;              /* --file argument, or NULL */
 12386  12419     const char *zDir;               /* --directory argument, or NULL */
 12387  12420     char **azArg;                   /* Array of command arguments */
 12388  12421     ShellState *p;                  /* Shell state */
 12389  12422     sqlite3 *db;                    /* Database containing the archive */
................................................................................
 12422  12455     return SQLITE_ERROR;
 12423  12456   }
 12424  12457   
 12425  12458   /*
 12426  12459   ** Print an error message for the .ar command to stderr and return 
 12427  12460   ** SQLITE_ERROR.
 12428  12461   */
 12429         -static int arErrorMsg(const char *zFmt, ...){
        12462  +static int arErrorMsg(ArCommand *pAr, const char *zFmt, ...){
 12430  12463     va_list ap;
 12431  12464     char *z;
 12432  12465     va_start(ap, zFmt);
 12433  12466     z = sqlite3_vmprintf(zFmt, ap);
 12434  12467     va_end(ap);
 12435         -  raw_printf(stderr, "Error: %s (try \".ar --help\")\n", z);
        12468  +  utf8_printf(stderr, "Error: %s\n", z);
        12469  +  if( pAr->fromCmdLine ){
        12470  +    utf8_printf(stderr, "Use \"-A\" for more help\n");
        12471  +  }else{
        12472  +    utf8_printf(stderr, "Use \".archive --help\" for more help\n");
        12473  +  }
 12436  12474     sqlite3_free(z);
 12437  12475     return SQLITE_ERROR;
 12438  12476   }
 12439  12477   
 12440  12478   /*
 12441  12479   ** Values for ArCommand.eCmd.
 12442  12480   */
................................................................................
 12459  12497     switch( eSwitch ){
 12460  12498       case AR_CMD_CREATE:
 12461  12499       case AR_CMD_EXTRACT:
 12462  12500       case AR_CMD_LIST:
 12463  12501       case AR_CMD_UPDATE:
 12464  12502       case AR_CMD_HELP:
 12465  12503         if( pAr->eCmd ){
 12466         -        return arErrorMsg("multiple command options");
        12504  +        return arErrorMsg(pAr, "multiple command options");
 12467  12505         }
 12468  12506         pAr->eCmd = eSwitch;
 12469  12507         break;
 12470  12508   
 12471  12509       case AR_SWITCH_DRYRUN:
 12472  12510         pAr->bDryRun = 1;
 12473  12511         break;
................................................................................
 12519  12557     int nSwitch = sizeof(aSwitch) / sizeof(struct ArSwitch);
 12520  12558     struct ArSwitch *pEnd = &aSwitch[nSwitch];
 12521  12559   
 12522  12560     if( nArg<=1 ){
 12523  12561       return arUsage(stderr);
 12524  12562     }else{
 12525  12563       char *z = azArg[1];
 12526         -    memset(pAr, 0, sizeof(ArCommand));
 12527         -
 12528  12564       if( z[0]!='-' ){
 12529  12565         /* Traditional style [tar] invocation */
 12530  12566         int i;
 12531  12567         int iArg = 2;
 12532  12568         for(i=0; z[i]; i++){
 12533  12569           const char *zArg = 0;
 12534  12570           struct ArSwitch *pOpt;
 12535  12571           for(pOpt=&aSwitch[0]; pOpt<pEnd; pOpt++){
 12536  12572             if( z[i]==pOpt->cShort ) break;
 12537  12573           }
 12538  12574           if( pOpt==pEnd ){
 12539         -          return arErrorMsg("unrecognized option: %c", z[i]);
        12575  +          return arErrorMsg(pAr, "unrecognized option: %c", z[i]);
 12540  12576           }
 12541  12577           if( pOpt->bArg ){
 12542  12578             if( iArg>=nArg ){
 12543         -            return arErrorMsg("option requires an argument: %c",z[i]);
        12579  +            return arErrorMsg(pAr, "option requires an argument: %c",z[i]);
 12544  12580             }
 12545  12581             zArg = azArg[iArg++];
 12546  12582           }
 12547  12583           if( arProcessSwitch(pAr, pOpt->eSwitch, zArg) ) return SQLITE_ERROR;
 12548  12584         }
 12549  12585         pAr->nArg = nArg-iArg;
 12550  12586         if( pAr->nArg>0 ){
................................................................................
 12570  12606             for(i=1; i<n; i++){
 12571  12607               const char *zArg = 0;
 12572  12608               struct ArSwitch *pOpt;
 12573  12609               for(pOpt=&aSwitch[0]; pOpt<pEnd; pOpt++){
 12574  12610                 if( z[i]==pOpt->cShort ) break;
 12575  12611               }
 12576  12612               if( pOpt==pEnd ){
 12577         -              return arErrorMsg("unrecognized option: %c\n", z[i]);
        12613  +              return arErrorMsg(pAr, "unrecognized option: %c", z[i]);
 12578  12614               }
 12579  12615               if( pOpt->bArg ){
 12580  12616                 if( i<(n-1) ){
 12581  12617                   zArg = &z[i+1];
 12582  12618                   i = n;
 12583  12619                 }else{
 12584  12620                   if( iArg>=(nArg-1) ){
 12585         -                  return arErrorMsg("option requires an argument: %c\n",z[i]);
        12621  +                  return arErrorMsg(pAr, "option requires an argument: %c",z[i]);
 12586  12622                   }
 12587  12623                   zArg = azArg[++iArg];
 12588  12624                 }
 12589  12625               }
 12590  12626               if( arProcessSwitch(pAr, pOpt->eSwitch, zArg) ) return SQLITE_ERROR;
 12591  12627             }
 12592  12628           }else if( z[2]=='\0' ){
................................................................................
 12600  12636             const char *zArg = 0;             /* Argument for option, if any */
 12601  12637             struct ArSwitch *pMatch = 0;      /* Matching option */
 12602  12638             struct ArSwitch *pOpt;            /* Iterator */
 12603  12639             for(pOpt=&aSwitch[0]; pOpt<pEnd; pOpt++){
 12604  12640               const char *zLong = pOpt->zLong;
 12605  12641               if( (n-2)<=strlen30(zLong) && 0==memcmp(&z[2], zLong, n-2) ){
 12606  12642                 if( pMatch ){
 12607         -                return arErrorMsg("ambiguous option: %s",z);
        12643  +                return arErrorMsg(pAr, "ambiguous option: %s",z);
 12608  12644                 }else{
 12609  12645                   pMatch = pOpt;
 12610  12646                 }
 12611  12647               }
 12612  12648             }
 12613  12649   
 12614  12650             if( pMatch==0 ){
 12615         -            return arErrorMsg("unrecognized option: %s", z);
        12651  +            return arErrorMsg(pAr, "unrecognized option: %s", z);
 12616  12652             }
 12617  12653             if( pMatch->bArg ){
 12618  12654               if( iArg>=(nArg-1) ){
 12619         -              return arErrorMsg("option requires an argument: %s", z);
        12655  +              return arErrorMsg(pAr, "option requires an argument: %s", z);
 12620  12656               }
 12621  12657               zArg = azArg[++iArg];
 12622  12658             }
 12623  12659             if( arProcessSwitch(pAr, pMatch->eSwitch, zArg) ) return SQLITE_ERROR;
 12624  12660           }
 12625  12661         }
 12626  12662       }
................................................................................
 12741  12777           );
 12742  12778         }else{
 12743  12779           utf8_printf(pAr->p->out, "%s\n", sqlite3_column_text(pSql, 0));
 12744  12780         }
 12745  12781       }
 12746  12782     }
 12747  12783     shellFinalize(&rc, pSql);
        12784  +  sqlite3_free(zWhere);
 12748  12785     return rc;
 12749  12786   }
 12750  12787   
 12751  12788   
 12752  12789   /*
 12753  12790   ** Implementation of .ar "eXtract" command. 
 12754  12791   */
 12755  12792   static int arExtractCommand(ArCommand *pAr){
 12756  12793     const char *zSql1 = 
 12757  12794       "SELECT "
 12758  12795       " ($dir || name),"
 12759  12796       " writefile(($dir || name), %s, mode, mtime) "
 12760         -    "FROM %s WHERE (%s) AND (data IS NULL OR $dirOnly = 0)";
        12797  +    "FROM %s WHERE (%s) AND (data IS NULL OR $dirOnly = 0)"
        12798  +    " AND name NOT GLOB '*..[/\\]*'";
 12761  12799   
 12762  12800     const char *azExtraArg[] = { 
 12763  12801       "sqlar_uncompress(data, sz)",
 12764  12802       "data"
 12765  12803     };
 12766  12804   
 12767  12805     sqlite3_stmt *pSql = 0;
................................................................................
 12943  12981   }
 12944  12982   
 12945  12983   /*
 12946  12984   ** Implementation of ".ar" dot command.
 12947  12985   */
 12948  12986   static int arDotCommand(
 12949  12987     ShellState *pState,             /* Current shell tool state */
        12988  +  int fromCmdLine,                /* True if -A command-line option, not .ar cmd */
 12950  12989     char **azArg,                   /* Array of arguments passed to dot command */
 12951  12990     int nArg                        /* Number of entries in azArg[] */
 12952  12991   ){
 12953  12992     ArCommand cmd;
 12954  12993     int rc;
 12955  12994     memset(&cmd, 0, sizeof(cmd));
        12995  +  cmd.fromCmdLine = fromCmdLine;
 12956  12996     rc = arParseCommand(azArg, nArg, &cmd);
 12957  12997     if( rc==SQLITE_OK ){
 12958  12998       int eDbType = SHELL_OPEN_UNSPEC;
 12959  12999       cmd.p = pState;
 12960  13000       cmd.db = pState->db;
 12961  13001       if( cmd.zFile ){
 12962  13002         eDbType = deduceDatabaseType(cmd.zFile, 1);
................................................................................
 12995  13035         }
 12996  13036         sqlite3_fileio_init(cmd.db, 0, 0);
 12997  13037         sqlite3_sqlar_init(cmd.db, 0, 0);
 12998  13038         sqlite3_create_function(cmd.db, "shell_putsnl", 1, SQLITE_UTF8, cmd.p,
 12999  13039                                 shellPutsFunc, 0, 0);
 13000  13040   
 13001  13041       }
 13002         -    if( cmd.zSrcTable==0 && cmd.bZip==0 ){
        13042  +    if( cmd.zSrcTable==0 && cmd.bZip==0 && cmd.eCmd!=AR_CMD_HELP ){
 13003  13043         if( cmd.eCmd!=AR_CMD_CREATE
 13004  13044          && sqlite3_table_column_metadata(cmd.db,0,"sqlar","name",0,0,0,0,0)
 13005  13045         ){
 13006  13046           utf8_printf(stderr, "database does not contain an 'sqlar' table\n");
 13007  13047           rc = SQLITE_ERROR;
 13008  13048           goto end_ar_command;
 13009  13049         }
................................................................................
 13031  13071           assert( cmd.eCmd==AR_CMD_UPDATE );
 13032  13072           rc = arCreateOrUpdateCommand(&cmd, 1);
 13033  13073           break;
 13034  13074       }
 13035  13075     }
 13036  13076   end_ar_command:
 13037  13077     if( cmd.db!=pState->db ){
 13038         -    sqlite3_close(cmd.db);
        13078  +    close_db(cmd.db);
 13039  13079     }
 13040  13080     sqlite3_free(cmd.zSrcTable);
 13041  13081   
 13042  13082     return rc;
 13043  13083   }
 13044  13084   /* End of the ".archive" or ".ar" command logic
 13045  13085   **********************************************************************************/
................................................................................
 13111  13151       }
 13112  13152     }else
 13113  13153   #endif
 13114  13154   
 13115  13155   #if !defined(SQLITE_OMIT_VIRTUALTABLE) && defined(SQLITE_HAVE_ZLIB)
 13116  13156     if( c=='a' && strncmp(azArg[0], "archive", n)==0 ){
 13117  13157       open_db(p, 0);
 13118         -    rc = arDotCommand(p, azArg, nArg);
        13158  +    rc = arDotCommand(p, 0, azArg, nArg);
 13119  13159     }else
 13120  13160   #endif
 13121  13161   
 13122  13162     if( (c=='b' && n>=3 && strncmp(azArg[0], "backup", n)==0)
 13123  13163      || (c=='s' && n>=3 && strncmp(azArg[0], "save", n)==0)
 13124  13164     ){
 13125  13165       const char *zDestFile = 0;
 13126  13166       const char *zDb = 0;
 13127  13167       sqlite3 *pDest;
 13128  13168       sqlite3_backup *pBackup;
 13129  13169       int j;
        13170  +    const char *zVfs = 0;
 13130  13171       for(j=1; j<nArg; j++){
 13131  13172         const char *z = azArg[j];
 13132  13173         if( z[0]=='-' ){
 13133         -        while( z[0]=='-' ) z++;
 13134         -        /* No options to process at this time */
        13174  +        if( z[1]=='-' ) z++;
        13175  +        if( strcmp(z, "-append")==0 ){
        13176  +          zVfs = "apndvfs";
        13177  +        }else
 13135  13178           {
 13136  13179             utf8_printf(stderr, "unknown option: %s\n", azArg[j]);
 13137  13180             return 1;
 13138  13181           }
 13139  13182         }else if( zDestFile==0 ){
 13140  13183           zDestFile = azArg[j];
 13141  13184         }else if( zDb==0 ){
 13142  13185           zDb = zDestFile;
 13143  13186           zDestFile = azArg[j];
 13144  13187         }else{
 13145         -        raw_printf(stderr, "too many arguments to .backup\n");
        13188  +        raw_printf(stderr, "Usage: .backup ?DB? ?--append? FILENAME\n");
 13146  13189           return 1;
 13147  13190         }
 13148  13191       }
 13149  13192       if( zDestFile==0 ){
 13150  13193         raw_printf(stderr, "missing FILENAME argument on .backup\n");
 13151  13194         return 1;
 13152  13195       }
 13153  13196       if( zDb==0 ) zDb = "main";
 13154         -    rc = sqlite3_open(zDestFile, &pDest);
        13197  +    rc = sqlite3_open_v2(zDestFile, &pDest, 
        13198  +                  SQLITE_OPEN_READWRITE|SQLITE_OPEN_CREATE, zVfs);
 13155  13199       if( rc!=SQLITE_OK ){
 13156  13200         utf8_printf(stderr, "Error: cannot open \"%s\"\n", zDestFile);
 13157         -      sqlite3_close(pDest);
        13201  +      close_db(pDest);
 13158  13202         return 1;
 13159  13203       }
 13160  13204       open_db(p, 0);
 13161  13205       pBackup = sqlite3_backup_init(pDest, "main", p->db, zDb);
 13162  13206       if( pBackup==0 ){
 13163  13207         utf8_printf(stderr, "Error: %s\n", sqlite3_errmsg(pDest));
 13164         -      sqlite3_close(pDest);
        13208  +      close_db(pDest);
 13165  13209         return 1;
 13166  13210       }
 13167  13211       while(  (rc = sqlite3_backup_step(pBackup,100))==SQLITE_OK ){}
 13168  13212       sqlite3_backup_finish(pBackup);
 13169  13213       if( rc==SQLITE_DONE ){
 13170  13214         rc = 0;
 13171  13215       }else{
 13172  13216         utf8_printf(stderr, "Error: %s\n", sqlite3_errmsg(pDest));
 13173  13217         rc = 1;
 13174  13218       }
 13175         -    sqlite3_close(pDest);
        13219  +    close_db(pDest);
 13176  13220     }else
 13177  13221   
 13178  13222     if( c=='b' && n>=3 && strncmp(azArg[0], "bail", n)==0 ){
 13179  13223       if( nArg==2 ){
 13180  13224         bail_on_error = booleanValue(azArg[1]);
 13181  13225       }else{
 13182  13226         raw_printf(stderr, "Usage: .bail on|off\n");
................................................................................
 13280  13324       if( zErrMsg ){
 13281  13325         utf8_printf(stderr,"Error: %s\n", zErrMsg);
 13282  13326         sqlite3_free(zErrMsg);
 13283  13327         rc = 1;
 13284  13328       }
 13285  13329     }else
 13286  13330   
 13287         -  if( c=='d' && strncmp(azArg[0], "dbinfo", n)==0 ){
        13331  +  if( c=='d' && n>=3 && strncmp(azArg[0], "dbconfig", n)==0 ){
        13332  +    static const struct DbConfigChoices {const char *zName; int op;} aDbConfig[] = {
        13333  +        { "enable_fkey",      SQLITE_DBCONFIG_ENABLE_FKEY            },
        13334  +        { "enable_trigger",   SQLITE_DBCONFIG_ENABLE_TRIGGER         },
        13335  +        { "fts3_tokenizer",   SQLITE_DBCONFIG_ENABLE_FTS3_TOKENIZER  },
        13336  +        { "load_extension",   SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION  },
        13337  +        { "no_ckpt_on_close", SQLITE_DBCONFIG_NO_CKPT_ON_CLOSE       },
        13338  +        { "enable_qpsg",      SQLITE_DBCONFIG_ENABLE_QPSG            },
        13339  +        { "trigger_eqp",      SQLITE_DBCONFIG_TRIGGER_EQP            },
        13340  +        { "reset_database",   SQLITE_DBCONFIG_RESET_DATABASE         },
        13341  +    };
        13342  +    int ii, v;
        13343  +    open_db(p, 0);
        13344  +    for(ii=0; ii<ArraySize(aDbConfig); ii++){
        13345  +      if( nArg>1 && strcmp(azArg[1], aDbConfig[ii].zName)!=0 ) continue;
        13346  +      if( nArg>=3 ){
        13347  +        sqlite3_db_config(p->db, aDbConfig[ii].op, booleanValue(azArg[2]), 0);
        13348  +      }
        13349  +      sqlite3_db_config(p->db, aDbConfig[ii].op, -1, &v);
        13350  +      utf8_printf(p->out, "%18s %s\n", aDbConfig[ii].zName, v ? "on" : "off");
        13351  +      if( nArg>1 ) break;
        13352  +    }
        13353  +    if( nArg>1 && ii==ArraySize(aDbConfig) ){
        13354  +      utf8_printf(stderr, "Error: unknown dbconfig \"%s\"\n", azArg[1]);
        13355  +      utf8_printf(stderr, "Enter \".dbconfig\" with no arguments for a list\n");
        13356  +    }   
        13357  +  }else
        13358  +
        13359  +  if( c=='d' && n>=3 && strncmp(azArg[0], "dbinfo", n)==0 ){
 13288  13360       rc = shell_dbinfo_command(p, nArg, azArg);
 13289  13361     }else
 13290  13362   
 13291  13363     if( c=='d' && strncmp(azArg[0], "dump", n)==0 ){
 13292  13364       const char *zLike = 0;
 13293  13365       int i;
 13294  13366       int savedShowHeader = p->showHeader;
 13295         -    ShellClearFlag(p, SHFLG_PreserveRowid|SHFLG_Newlines);
        13367  +    int savedShellFlags = p->shellFlgs;
        13368  +    ShellClearFlag(p, SHFLG_PreserveRowid|SHFLG_Newlines|SHFLG_Echo);
 13296  13369       for(i=1; i<nArg; i++){
 13297  13370         if( azArg[i][0]=='-' ){
 13298  13371           const char *z = azArg[i]+1;
 13299  13372           if( z[0]=='-' ) z++;
 13300  13373           if( strcmp(z,"preserve-rowids")==0 ){
 13301  13374   #ifdef SQLITE_OMIT_VIRTUALTABLE
 13302  13375             raw_printf(stderr, "The --preserve-rowids option is not compatible"
................................................................................
 13370  13443         raw_printf(p->out, "PRAGMA writable_schema=OFF;\n");
 13371  13444         p->writableSchema = 0;
 13372  13445       }
 13373  13446       sqlite3_exec(p->db, "PRAGMA writable_schema=OFF;", 0, 0, 0);
 13374  13447       sqlite3_exec(p->db, "RELEASE dump;", 0, 0, 0);
 13375  13448       raw_printf(p->out, p->nErr ? "ROLLBACK; -- due to errors\n" : "COMMIT;\n");
 13376  13449       p->showHeader = savedShowHeader;
        13450  +    p->shellFlgs = savedShellFlags;
 13377  13451     }else
 13378  13452   
 13379  13453     if( c=='e' && strncmp(azArg[0], "echo", n)==0 ){
 13380  13454       if( nArg==2 ){
 13381  13455         setOrClearFlag(p, SHFLG_Echo, azArg[1]);
 13382  13456       }else{
 13383  13457         raw_printf(stderr, "Usage: .echo on|off\n");
 13384  13458         rc = 1;
 13385  13459       }
 13386  13460     }else
 13387  13461   
 13388  13462     if( c=='e' && strncmp(azArg[0], "eqp", n)==0 ){
 13389  13463       if( nArg==2 ){
        13464  +      p->autoEQPtest = 0;
 13390  13465         if( strcmp(azArg[1],"full")==0 ){
 13391  13466           p->autoEQP = AUTOEQP_full;
 13392  13467         }else if( strcmp(azArg[1],"trigger")==0 ){
 13393  13468           p->autoEQP = AUTOEQP_trigger;
        13469  +      }else if( strcmp(azArg[1],"test")==0 ){
        13470  +        p->autoEQP = AUTOEQP_on;
        13471  +        p->autoEQPtest = 1;
 13394  13472         }else{
 13395  13473           p->autoEQP = (u8)booleanValue(azArg[1]);
 13396  13474         }
 13397  13475       }else{
 13398  13476         raw_printf(stderr, "Usage: .eqp off|on|trigger|full\n");
 13399  13477         rc = 1;
 13400  13478       }
................................................................................
 13475  13553         raw_printf(p->out, "/* No STAT tables available */\n");
 13476  13554       }else{
 13477  13555         raw_printf(p->out, "ANALYZE sqlite_master;\n");
 13478  13556         sqlite3_exec(p->db, "SELECT 'ANALYZE sqlite_master'",
 13479  13557                      callback, &data, &zErrMsg);
 13480  13558         data.cMode = data.mode = MODE_Insert;
 13481  13559         data.zDestTable = "sqlite_stat1";
 13482         -      shell_exec(p, "SELECT * FROM sqlite_stat1", &zErrMsg);
        13560  +      shell_exec(&data, "SELECT * FROM sqlite_stat1", &zErrMsg);
 13483  13561         data.zDestTable = "sqlite_stat3";
 13484         -      shell_exec(p, "SELECT * FROM sqlite_stat3", &zErrMsg);
        13562  +      shell_exec(&data, "SELECT * FROM sqlite_stat3", &zErrMsg);
 13485  13563         data.zDestTable = "sqlite_stat4";
 13486         -      shell_exec(p, "SELECT * FROM sqlite_stat4", &zErrMsg);
        13564  +      shell_exec(&data, "SELECT * FROM sqlite_stat4", &zErrMsg);
 13487  13565         raw_printf(p->out, "ANALYZE sqlite_master;\n");
 13488  13566       }
 13489  13567     }else
 13490  13568   
 13491  13569     if( c=='h' && strncmp(azArg[0], "headers", n)==0 ){
 13492  13570       if( nArg==2 ){
 13493  13571         p->showHeader = booleanValue(azArg[1]);
................................................................................
 13952  14030   
 13953  14031     if( c=='o' && strncmp(azArg[0], "open", n)==0 && n>=2 ){
 13954  14032       char *zNewFilename;  /* Name of the database file to open */
 13955  14033       int iName = 1;       /* Index in azArg[] of the filename */
 13956  14034       int newFlag = 0;     /* True to delete file before opening */
 13957  14035       /* Close the existing database */
 13958  14036       session_close_all(p);
 13959         -    sqlite3_close(p->db);
        14037  +    close_db(p->db);
 13960  14038       p->db = 0;
 13961  14039       p->zDbFilename = 0;
 13962  14040       sqlite3_free(p->zFreeOnClose);
 13963  14041       p->zFreeOnClose = 0;
 13964  14042       p->openMode = SHELL_OPEN_UNSPEC;
 13965  14043       /* Check for command-line arguments */
 13966  14044       for(iName=1; iName<nArg && azArg[iName][0]=='-'; iName++){
................................................................................
 13982  14060         }
 13983  14061       }
 13984  14062       /* If a filename is specified, try to open it first */
 13985  14063       zNewFilename = nArg>iName ? sqlite3_mprintf("%s", azArg[iName]) : 0;
 13986  14064       if( zNewFilename ){
 13987  14065         if( newFlag ) shellDeleteFile(zNewFilename);
 13988  14066         p->zDbFilename = zNewFilename;
 13989         -      open_db(p, 1);
        14067  +      open_db(p, OPEN_DB_KEEPALIVE);
 13990  14068         if( p->db==0 ){
 13991  14069           utf8_printf(stderr, "Error: cannot open '%s'\n", zNewFilename);
 13992  14070           sqlite3_free(zNewFilename);
 13993  14071         }else{
 13994  14072           p->zFreeOnClose = zNewFilename;
 13995  14073         }
 13996  14074       }
................................................................................
 14132  14210         raw_printf(stderr, "Usage: .restore ?DB? FILE\n");
 14133  14211         rc = 1;
 14134  14212         goto meta_command_exit;
 14135  14213       }
 14136  14214       rc = sqlite3_open(zSrcFile, &pSrc);
 14137  14215       if( rc!=SQLITE_OK ){
 14138  14216         utf8_printf(stderr, "Error: cannot open \"%s\"\n", zSrcFile);
 14139         -      sqlite3_close(pSrc);
        14217  +      close_db(pSrc);
 14140  14218         return 1;
 14141  14219       }
 14142  14220       open_db(p, 0);
 14143  14221       pBackup = sqlite3_backup_init(p->db, zDb, pSrc, "main");
 14144  14222       if( pBackup==0 ){
 14145  14223         utf8_printf(stderr, "Error: %s\n", sqlite3_errmsg(p->db));
 14146         -      sqlite3_close(pSrc);
        14224  +      close_db(pSrc);
 14147  14225         return 1;
 14148  14226       }
 14149  14227       while( (rc = sqlite3_backup_step(pBackup,100))==SQLITE_OK
 14150  14228             || rc==SQLITE_BUSY  ){
 14151  14229         if( rc==SQLITE_BUSY ){
 14152  14230           if( nTimeout++ >= 3 ) break;
 14153  14231           sqlite3_sleep(100);
................................................................................
 14159  14237       }else if( rc==SQLITE_BUSY || rc==SQLITE_LOCKED ){
 14160  14238         raw_printf(stderr, "Error: source database is busy\n");
 14161  14239         rc = 1;
 14162  14240       }else{
 14163  14241         utf8_printf(stderr, "Error: %s\n", sqlite3_errmsg(p->db));
 14164  14242         rc = 1;
 14165  14243       }
 14166         -    sqlite3_close(pSrc);
        14244  +    close_db(pSrc);
 14167  14245     }else
 14168  14246   
 14169  14247     if( c=='s' && strncmp(azArg[0], "scanstats", n)==0 ){
 14170  14248       if( nArg==2 ){
 14171  14249         p->scanstatsOn = (u8)booleanValue(azArg[1]);
 14172  14250   #ifndef SQLITE_ENABLE_STMT_SCANSTATUS
 14173  14251         raw_printf(stderr, "Warning: .scanstats not available in this build.\n");
................................................................................
 14679  14757           if( strcmp(z,"debug")==0 ){
 14680  14758             bDebug = 1;
 14681  14759           }else
 14682  14760           {
 14683  14761             utf8_printf(stderr, "Unknown option \"%s\" on \"%s\"\n",
 14684  14762                         azArg[i], azArg[0]);
 14685  14763             raw_printf(stderr, "Should be one of: --schema"
 14686         -                             " --sha3-224 --sha3-255 --sha3-384 --sha3-512\n");
        14764  +                             " --sha3-224 --sha3-256 --sha3-384 --sha3-512\n");
 14687  14765             rc = 1;
 14688  14766             goto meta_command_exit;
 14689  14767           }
 14690  14768         }else if( zLike ){
 14691  14769           raw_printf(stderr, "Usage: .sha3sum ?OPTIONS? ?LIKE-PATTERN?\n");
 14692  14770           rc = 1;
 14693  14771           goto meta_command_exit;
................................................................................
 14843  14921       char **azResult;
 14844  14922       int nRow, nAlloc;
 14845  14923       int ii;
 14846  14924       ShellText s;
 14847  14925       initText(&s);
 14848  14926       open_db(p, 0);
 14849  14927       rc = sqlite3_prepare_v2(p->db, "PRAGMA database_list", -1, &pStmt, 0);
 14850         -    if( rc ) return shellDatabaseError(p->db);
        14928  +    if( rc ){
        14929  +      sqlite3_finalize(pStmt);
        14930  +      return shellDatabaseError(p->db);
        14931  +    }
 14851  14932   
 14852  14933       if( nArg>2 && c=='i' ){
 14853  14934         /* It is an historical accident that the .indexes command shows an error
 14854  14935         ** when called with the wrong number of arguments whereas the .tables
 14855  14936         ** command does not. */
 14856  14937         raw_printf(stderr, "Usage: .indexes ?LIKE-PATTERN?\n");
 14857  14938         rc = 1;
        14939  +      sqlite3_finalize(pStmt);
 14858  14940         goto meta_command_exit;
 14859  14941       }
 14860  14942       for(ii=0; sqlite3_step(pStmt)==SQLITE_ROW; ii++){
 14861  14943         const char *zDbName = (const char*)sqlite3_column_text(pStmt, 1);
 14862  14944         if( zDbName==0 ) continue;
 14863  14945         if( s.z && s.z[0] ) appendText(&s, " UNION ALL ", 0);
 14864  14946         if( sqlite3_stricmp(zDbName, "main")==0 ){
................................................................................
 15367  15449     zSql[nSql+1] = 0;
 15368  15450     rc = sqlite3_complete(zSql);
 15369  15451     zSql[nSql] = 0;
 15370  15452     return rc;
 15371  15453   }
 15372  15454   
 15373  15455   /*
 15374         -** Run a single line of SQL
        15456  +** Run a single line of SQL.  Return the number of errors.
 15375  15457   */
 15376  15458   static int runOneSqlLine(ShellState *p, char *zSql, FILE *in, int startline){
 15377  15459     int rc;
 15378  15460     char *zErrMsg = 0;
 15379  15461   
 15380  15462     open_db(p, 0);
 15381  15463     if( ShellHasFlag(p,SHFLG_Backslash) ) resolve_backslashes(zSql);
................................................................................
 15440  15522         seenInterrupt = 0;
 15441  15523       }
 15442  15524       lineno++;
 15443  15525       if( nSql==0 && _all_whitespace(zLine) ){
 15444  15526         if( ShellHasFlag(p, SHFLG_Echo) ) printf("%s\n", zLine);
 15445  15527         continue;
 15446  15528       }
 15447         -    if( zLine && zLine[0]=='.' && nSql==0 ){
        15529  +    if( zLine && (zLine[0]=='.' || zLine[0]=='#') && nSql==0 ){
 15448  15530         if( ShellHasFlag(p, SHFLG_Echo) ) printf("%s\n", zLine);
 15449         -      rc = do_meta_command(zLine, p);
 15450         -      if( rc==2 ){ /* exit requested */
 15451         -        break;
 15452         -      }else if( rc ){
 15453         -        errCnt++;
        15531  +      if( zLine[0]=='.' ){
        15532  +        rc = do_meta_command(zLine, p);
        15533  +        if( rc==2 ){ /* exit requested */
        15534  +          break;
        15535  +        }else if( rc ){
        15536  +          errCnt++;
        15537  +        }
 15454  15538         }
 15455  15539         continue;
 15456  15540       }
 15457  15541       if( line_is_command_terminator(zLine) && line_is_complete(zSql, nSql) ){
 15458  15542         memcpy(zLine,";",2);
 15459  15543       }
 15460  15544       nLine = strlen30(zLine);
................................................................................
 15488  15572         }
 15489  15573       }else if( nSql && _all_whitespace(zSql) ){
 15490  15574         if( ShellHasFlag(p, SHFLG_Echo) ) printf("%s\n", zSql);
 15491  15575         nSql = 0;
 15492  15576       }
 15493  15577     }
 15494  15578     if( nSql && !_all_whitespace(zSql) ){
 15495         -    runOneSqlLine(p, zSql, in, startline);
        15579  +    errCnt += runOneSqlLine(p, zSql, in, startline);
 15496  15580     }
 15497  15581     free(zSql);
 15498  15582     free(zLine);
 15499  15583     return errCnt>0;
 15500  15584   }
 15501  15585   
 15502  15586   /*
................................................................................
 15668  15752   
 15669  15753   /*
 15670  15754   ** Internal check:  Verify that the SQLite is uninitialized.  Print a
 15671  15755   ** error message if it is initialized.
 15672  15756   */
 15673  15757   static void verify_uninitialized(void){
 15674  15758     if( sqlite3_config(-1)==SQLITE_MISUSE ){
 15675         -    utf8_printf(stdout, "WARNING: attempt to configuration SQLite after"
        15759  +    utf8_printf(stdout, "WARNING: attempt to configure SQLite after"
 15676  15760                           " initialization.\n");
 15677  15761     }
 15678  15762   }
 15679  15763   
 15680  15764   /*
 15681  15765   ** Initialize the state information in data
 15682  15766   */
................................................................................
 15749  15833     int i;
 15750  15834     int rc = 0;
 15751  15835     int warnInmemoryDb = 0;
 15752  15836     int readStdin = 1;
 15753  15837     int nCmd = 0;
 15754  15838     char **azCmd = 0;
 15755  15839     const char *zVfs = 0;           /* Value of -vfs command-line option */
        15840  +#if !SQLITE_SHELL_IS_UTF8
        15841  +  char **argvToFree = 0;
        15842  +  int argcToFree = 0;
        15843  +#endif
 15756  15844   
 15757  15845     setBinaryMode(stdin, 0);
 15758  15846     setvbuf(stderr, 0, _IONBF, 0); /* Make sure stderr is unbuffered */
 15759  15847     stdin_is_interactive = isatty(0);
 15760  15848     stdout_is_console = isatty(1);
 15761  15849   
 15762  15850   #if USE_SYSTEM_SQLITE+0!=1
................................................................................
 15772  15860     ** The SQLite memory allocator subsystem has to be enabled in order to
 15773  15861     ** do this.  But we want to run an sqlite3_shutdown() afterwards so that
 15774  15862     ** subsequent sqlite3_config() calls will work.  So copy all results into
 15775  15863     ** memory that does not come from the SQLite memory allocator.
 15776  15864     */
 15777  15865   #if !SQLITE_SHELL_IS_UTF8
 15778  15866     sqlite3_initialize();
 15779         -  argv = malloc(sizeof(argv[0])*argc);
        15867  +  argvToFree = malloc(sizeof(argv[0])*argc*2);
        15868  +  argcToFree = argc;
        15869  +  argv = argvToFree + argc;
 15780  15870     if( argv==0 ) shell_out_of_memory();
 15781  15871     for(i=0; i<argc; i++){
 15782  15872       char *z = sqlite3_win32_unicode_to_utf8(wargv[i]);
 15783  15873       int n;
 15784  15874       if( z==0 ) shell_out_of_memory();
 15785  15875       n = (int)strlen(z);
 15786  15876       argv[i] = malloc( n+1 );
 15787  15877       if( argv[i]==0 ) shell_out_of_memory();
 15788  15878       memcpy(argv[i], z, n+1);
        15879  +    argvToFree[i] = argv[i];
 15789  15880       sqlite3_free(z);
 15790  15881     }
 15791  15882     sqlite3_shutdown();
 15792  15883   #endif
 15793  15884   
 15794  15885     assert( argc>=1 && argv && argv[0] );
 15795  15886     Argv0 = argv[0];
................................................................................
 16103  16194   #if !defined(SQLITE_OMIT_VIRTUALTABLE) && defined(SQLITE_HAVE_ZLIB)
 16104  16195       }else if( strncmp(z, "-A", 2)==0 ){
 16105  16196         if( nCmd>0 ){
 16106  16197           utf8_printf(stderr, "Error: cannot mix regular SQL or dot-commands"
 16107  16198                               " with \"%s\"\n", z);
 16108  16199           return 1;
 16109  16200         }
 16110         -      open_db(&data, 0);
        16201  +      open_db(&data, OPEN_DB_ZIPFILE);
 16111  16202         if( z[2] ){
 16112  16203           argv[i] = &z[2];
 16113         -        arDotCommand(&data, argv+(i-1), argc-(i-1));
        16204  +        arDotCommand(&data, 1, argv+(i-1), argc-(i-1));
 16114  16205         }else{
 16115         -        arDotCommand(&data, argv+i, argc-i);
        16206  +        arDotCommand(&data, 1, argv+i, argc-i);
 16116  16207         }
 16117  16208         readStdin = 0;
 16118  16209         break;
 16119  16210   #endif
 16120  16211       }else{
 16121  16212         utf8_printf(stderr,"%s: Error: unknown option: %s\n", Argv0, z);
 16122  16213         raw_printf(stderr,"Use -help for a list of options.\n");
................................................................................
 16187  16278       }else{
 16188  16279         rc = process_input(&data, stdin);
 16189  16280       }
 16190  16281     }
 16191  16282     set_table_name(&data, 0);
 16192  16283     if( data.db ){
 16193  16284       session_close_all(&data);
 16194         -    sqlite3_close(data.db);
        16285  +    close_db(data.db);
 16195  16286     }
 16196  16287     sqlite3_free(data.zFreeOnClose);
 16197  16288     find_home_dir(1);
 16198  16289     output_reset(&data);
 16199  16290     data.doXdgOpen = 0;
 16200  16291     clearTempFile(&data);
 16201  16292   #if !SQLITE_SHELL_IS_UTF8
 16202         -  for(i=0; i<argc; i++) free(argv[i]);
 16203         -  free(argv);
        16293  +  for(i=0; i<argcToFree; i++) free(argvToFree[i]);
        16294  +  free(argvToFree);
 16204  16295   #endif
        16296  +  /* Clear the global data structure so that valgrind will detect memory
        16297  +  ** leaks */
        16298  +  memset(&data, 0, sizeof(data));
 16205  16299     return rc;
 16206  16300   }

Changes to src/skins.c.

   252    252   /*
   253    253   ** Return a skin detail setting
   254    254   */
   255    255   const char *skin_detail(const char *zName){
   256    256     struct SkinDetail *pDetail;
   257    257     skin_detail_initialize();
   258    258     pDetail = skin_detail_find(zName);
   259         -  if( pDetail==0 ) fossil_fatal("no such skin detail: %s", zName);
          259  +  if( pDetail==0 ) fossil_panic("no such skin detail: %s", zName);
   260    260     return pDetail->zValue;
   261    261   }
   262    262   int skin_detail_boolean(const char *zName){
   263    263     return !is_false(skin_detail(zName));
   264    264   }
   265    265   
   266    266   /*
................................................................................
   718    718   
   719    719     /* Figure out which skin we are editing */
   720    720     iSkin = atoi(PD("sk","1"));
   721    721     if( iSkin<1 || iSkin>9 ) iSkin = 1;
   722    722   
   723    723     /* Check that the user is authorized to edit this skin. */
   724    724     if( !g.perm.Setup ){
   725         -    char *zAllowedEditors = db_get_mprintf("", "draft%d-users", iSkin);
          725  +    char *zAllowedEditors = "";
   726    726       Glob *pAllowedEditors;
          727  +    int isMatch = 0;
          728  +    if( login_is_individual() ){
          729  +      zAllowedEditors = db_get_mprintf("", "draft%d-users", iSkin);
          730  +    }
   727    731       if( zAllowedEditors[0] ){
   728    732         pAllowedEditors = glob_create(zAllowedEditors);
   729         -      if( !glob_match(pAllowedEditors, zAllowedEditors) ){
   730         -        login_needed(0);
   731         -        return;
   732         -      }
          733  +      isMatch = glob_match(pAllowedEditors, g.zLogin);
   733    734         glob_free(pAllowedEditors);
          735  +    }
          736  +    if( isMatch==0 ){
          737  +      login_needed(0);
          738  +      return;
   734    739       }
   735    740     }
   736    741   
   737    742     /* figure out which file is to be edited */
   738    743     ii = atoi(PD("w","0"));
   739    744     if( ii<0 || ii>count(aSkinAttr) ) ii = 0;
   740    745     zFile = aSkinAttr[ii].zFile;
................................................................................
   872    877     iSkin = atoi(PD("sk","1"));
   873    878     if( iSkin<1 || iSkin>9 ) iSkin = 1;
   874    879   
   875    880     /* Figure out if the current user is allowed to make administrative
   876    881     ** changes and/or edits
   877    882     */
   878    883     login_check_credentials();
          884  +  if( !login_is_individual() ){
          885  +    login_needed(0);
          886  +    return;
          887  +  }
   879    888     zAllowedEditors = db_get_mprintf("", "draft%d-users", iSkin);
   880    889     if( g.perm.Setup ){
   881    890       isSetup = isEditor = 1;
   882    891     }else{
   883    892       Glob *pAllowedEditors;
   884    893       isSetup = isEditor = 0;
   885    894       if( zAllowedEditors[0] ){
   886    895         pAllowedEditors = glob_create(zAllowedEditors);
   887         -      isEditor = glob_match(pAllowedEditors, zAllowedEditors);
          896  +      isEditor = glob_match(pAllowedEditors, g.zLogin);
   888    897         glob_free(pAllowedEditors);
   889    898       }
   890    899     }
   891    900   
   892    901     /* Initialize the skin, if requested and authorized. */
   893    902     if( P("init3")!=0 && isEditor ){
   894    903       skin_initialize_draft(iSkin, P("initskin"));
................................................................................
   932    941     @ </p>
   933    942     @
   934    943     @ <a name='step2'></a>
   935    944     @ <h1>Step 2: Authenticate</h1>
   936    945     @
   937    946     if( isSetup ){
   938    947       @ <p>As an administrator, you can make any edits you like to this or
   939         -    @ any other skin.  You can also authorized other users to edit this
   940         -    @ skin.  Any user whose login name matches the comma-separate list
          948  +    @ any other skin.  You can also authorize other users to edit this
          949  +    @ skin.  Any user whose login name matches the comma-separated list
   941    950       @ of GLOB expressions below is given special permission to edit
   942    951       @ the draft%d(iSkin) skin:
   943    952       @
   944    953       @ <form method='POST' action='%R/setup_skin#step2' id='f02'>
   945    954       @ <p class='skinInput'>
   946    955       @ <input type='hidden' name='sk' value='%d(iSkin)'>
   947    956       @ Authorized editors for skin draft%d(iSkin):
................................................................................
  1051   1060       @ publishing the new skin.</p>
  1052   1061     }
  1053   1062     @
  1054   1063     @ <a name='step8'></a>
  1055   1064     @ <h1>Step 8: Cleanup and Undo Actions</h1>
  1056   1065     @
  1057   1066     if( !g.perm.Setup ){
  1058         -    @ <p>Administrators can optionally remove save legacy skins, or
  1059         -    @ undo a prior publish
         1067  +    @ <p>Administrators can optionally save or restore legacy skins, and/or
         1068  +    @ undo a prior publish.
  1060   1069     }else{
  1061   1070       @ <p>Visit the <a href='%R/setup_skin_admin'>Skin Admin</a> page
  1062   1071       @ for cleanup and recovery actions.
  1063   1072     }
  1064   1073     style_load_one_js_file("skin.js");
  1065   1074     style_footer();
  1066   1075   }

Added src/smtp.c.

            1  +/*
            2  +** Copyright (c) 2018 D. Richard Hipp
            3  +**
            4  +** This program is free software; you can redistribute it and/or
            5  +** modify it under the terms of the Simplified BSD License (also
            6  +** known as the "2-Clause License" or "FreeBSD License".)
            7  +**
            8  +** This program is distributed in the hope that it will be useful,
            9  +** but without any warranty; without even the implied warranty of
           10  +** merchantability or fitness for a particular purpose.
           11  +**
           12  +** Author contact information:
           13  +**   drh@hwaci.com
           14  +**   http://www.hwaci.com/drh/
           15  +**
           16  +*******************************************************************************
           17  +**
           18  +** Implementation of SMTP (Simple Mail Transport Protocol) according
           19  +** to RFC 5321.
           20  +*/
           21  +#include "config.h"
           22  +#include "smtp.h"
           23  +#include <assert.h>
           24  +#if defined(__linux__) && !defined(FOSSIL_OMIT_DNS)
           25  +#  include <sys/types.h>
           26  +#  include <netinet/in.h>
           27  +#  include <arpa/nameser.h>
           28  +#  include <resolv.h>
           29  +#  define FOSSIL_UNIX_STYLE_DNS 1
           30  +#endif
           31  +#if defined(_WIN32) && !defined(__MINGW32__) && !defined(__MINGW64__)
           32  +#  include <windows.h>
           33  +#  include <windns.h>
           34  +#  define FOSSIL_WINDOWS_STYLE_DNS 1
           35  +#endif
           36  +
           37  +
           38  +/*
           39  +** Find the hostname for receiving email for the domain given
           40  +** in zDomain.  Return NULL if not found or not implemented.
           41  +** If multiple email receivers are advertized, pick the one with
           42  +** the lowest preference number.
           43  +**
           44  +** The returned string is obtained from fossil_malloc()
           45  +** and should be released using fossil_free().
           46  +*/
           47  +char *smtp_mx_host(const char *zDomain){
           48  +#if defined(FOSSIL_UNIX_STYLE_DNS)
           49  +  int nDns;                       /* Length of the DNS reply */
           50  +  int rc;                         /* Return code from various APIs */
           51  +  int i;                          /* Loop counter */
           52  +  int iBestPriority = 9999999;    /* Best priority */
           53  +  int nRec;                       /* Number of answers */
           54  +  ns_msg h;                       /* DNS reply parser */
           55  +  const unsigned char *pBest = 0; /* RDATA for the best answer */
           56  +  unsigned char aDns[5000];       /* Raw DNS reply content */
           57  +  char zHostname[5000];           /* Hostname for the MX */
           58  +
           59  +  nDns = res_query(zDomain, C_IN, T_MX, aDns, sizeof(aDns));
           60  +  if( nDns<=0 ) return 0;
           61  +  res_init();
           62  +  rc = ns_initparse(aDns,nDns,&h);
           63  +  if( rc ) return 0;
           64  +  nRec = ns_msg_count(h, ns_s_an);
           65  +  for(i=0; i<nRec; i++){
           66  +    ns_rr x;
           67  +    int priority, sz;
           68  +    const unsigned char *p;
           69  +    rc = ns_parserr(&h, ns_s_an, i, &x);
           70  +    if( rc ) continue;
           71  +    p = ns_rr_rdata(x);
           72  +    sz = ns_rr_rdlen(x);
           73  +    if( sz>2 ){
           74  +      priority = p[0]*256 + p[1];
           75  +      if( priority<iBestPriority ){
           76  +        pBest = p;
           77  +        iBestPriority = priority;
           78  +      }
           79  +    }
           80  +  }
           81  +  if( pBest ){
           82  +    ns_name_uncompress(aDns, aDns+nDns, pBest+2,
           83  +                       zHostname, sizeof(zHostname));
           84  +    return fossil_strdup(zHostname);
           85  +  }
           86  +  return 0;
           87  +#elif defined(FOSSIL_WINDOWS_STYLE_DNS)
           88  +  DNS_STATUS status;           /* Return status */
           89  +  PDNS_RECORDA pDnsRecord, p;  /* Pointer to DNS_RECORD structure */
           90  +  int iBestPriority = 9999999; /* Best priority */
           91  +  char *pBest = 0;             /* RDATA for the best answer */
           92  +
           93  +  status = DnsQuery_UTF8(zDomain,            /* Domain name */
           94  +                         DNS_TYPE_MX,        /* DNS record type */
           95  +                         DNS_QUERY_STANDARD, /* Query options */
           96  +                         NULL,               /* List of DNS servers */
           97  +                         &pDnsRecord,        /* Query results */
           98  +                         NULL);              /* Reserved */
           99  +  if( status ) return NULL;
          100  +
          101  +  p = pDnsRecord;
          102  +  while( p ){
          103  +    if( p->Data.MX.wPreference<iBestPriority ){
          104  +      iBestPriority = p->Data.MX.wPreference;
          105  +      pBest = p->Data.MX.pNameExchange;
          106  +    }
          107  +    p = p->pNext;
          108  +  }
          109  +  if( pBest ){
          110  +    pBest = fossil_strdup(pBest); 
          111  +  }
          112  +  DnsRecordListFree(pDnsRecord, DnsFreeRecordListDeep);
          113  +  return pBest;
          114  +#else
          115  +  return 0;
          116  +#endif /* defined(FOSSIL_WINDOWS_STYLE_DNS) */
          117  +}
          118  +
          119  +/*
          120  +** COMMAND: test-find-mx
          121  +**
          122  +** Usage: %fossil test-find-mx DOMAIN ...
          123  +**
          124  +** Do a DNS MX lookup to find the hostname for sending email for
          125  +** DOMAIN.
          126  +*/
          127  +void test_find_mx(void){
          128  +  int i;
          129  +  if( g.argc<=2 ){
          130  +    usage("DOMAIN ...");
          131  +  }
          132  +  for(i=2; i<g.argc; i++){
          133  +    char *z = smtp_mx_host(g.argv[i]);
          134  +    fossil_print("%s: %s\n", g.argv[i], z);
          135  +    fossil_free(z);
          136  +  }
          137  +}
          138  +
          139  +#if INTERFACE
          140  +/*
          141  +** Information about a single SMTP connection.
          142  +*/
          143  +struct SmtpSession {
          144  +  const char *zFrom;        /* Domain from which we are sending */
          145  +  const char *zDest;        /* Domain that will receive the email */
          146  +  char *zHostname;          /* Hostname of SMTP server for zDest */
          147  +  u32 smtpFlags;            /* Flags changing the operation */
          148  +  FILE *logFile;            /* Write session transcript to this log file */
          149  +  Blob *pTranscript;        /* Record session transcript here */
          150  +  int atEof;                /* True after connection closes */
          151  +  char *zErr;               /* Error message */
          152  +  Blob inbuf;               /* Input buffer */
          153  +};
          154  +
          155  +/* Allowed values for SmtpSession.smtpFlags */
          156  +#define SMTP_TRACE_STDOUT   0x00001     /* Debugging info to console */
          157  +#define SMTP_TRACE_FILE     0x00002     /* Debugging info to logFile */
          158  +#define SMTP_TRACE_BLOB     0x00004     /* Record transcript */
          159  +#define SMTP_DIRECT         0x00008     /* Skip the MX lookup */
          160  +#define SMTP_PORT           0x00010     /* Use an alternate port number */
          161  +
          162  +#endif
          163  +
          164  +/*
          165  +** Shutdown an SmtpSession
          166  +*/
          167  +void smtp_session_free(SmtpSession *pSession){
          168  +  socket_close();
          169  +  blob_reset(&pSession->inbuf);
          170  +  fossil_free(pSession->zHostname);
          171  +  fossil_free(pSession->zErr);
          172  +  fossil_free(pSession);
          173  +}
          174  +
          175  +/*
          176  +** Allocate a new SmtpSession object.
          177  +**
          178  +** Both zFrom and zDest must be specified.
          179  +**
          180  +** The ... arguments are in this order:
          181  +**
          182  +**    SMTP_PORT:            int
          183  +**    SMTP_TRACE_FILE:      FILE*
          184  +**    SMTP_TRACE_BLOB:      Blob*
          185  +*/
          186  +SmtpSession *smtp_session_new(
          187  +  const char *zFrom,    /* Domain for the client */
          188  +  const char *zDest,    /* Domain of the server */
          189  +  u32 smtpFlags,        /* Flags */
          190  +  ...                   /* Arguments depending on the flags */
          191  +){
          192  +  SmtpSession *p;
          193  +  va_list ap;
          194  +  UrlData url;
          195  +
          196  +  p = fossil_malloc( sizeof(*p) );
          197  +  memset(p, 0, sizeof(*p));
          198  +  p->zFrom = zFrom;
          199  +  p->zDest = zDest;
          200  +  p->smtpFlags = smtpFlags;
          201  +  memset(&url, 0, sizeof(url));
          202  +  url.port = 25;
          203  +  blob_init(&p->inbuf, 0, 0);
          204  +  va_start(ap, smtpFlags);
          205  +  if( smtpFlags & SMTP_PORT ){
          206  +    url.port = va_arg(ap, int);
          207  +  }
          208  +  if( smtpFlags & SMTP_TRACE_FILE ){
          209  +    p->logFile = va_arg(ap, FILE*);
          210  +  }
          211  +  if( smtpFlags & SMTP_TRACE_BLOB ){
          212  +    p->pTranscript = va_arg(ap, Blob*);
          213  +  }
          214  +  va_end(ap);
          215  +  if( (smtpFlags & SMTP_DIRECT)!=0 ){
          216  +    int i;
          217  +    p->zHostname = fossil_strdup(zDest);
          218  +    for(i=0; p->zHostname[i] && p->zHostname[i]!=':'; i++){}
          219  +    if( p->zHostname[i]==':' ){
          220  +      p->zHostname[i] = 0;
          221  +      url.port = atoi(&p->zHostname[i+1]);
          222  +    }
          223  +  }else{
          224  +    p->zHostname = smtp_mx_host(zDest);
          225  +  }
          226  +  if( p->zHostname==0 ){
          227  +    p->atEof = 1;
          228  +    p->zErr = mprintf("cannot locate SMTP server for \"%s\"", zDest);
          229  +    return p;
          230  +  }
          231  +  url.name = p->zHostname;
          232  +  socket_global_init();
          233  +  if( socket_open(&url) ){
          234  +    p->atEof = 1;
          235  +    p->zErr = socket_errmsg();
          236  +    socket_close();
          237  +  }
          238  +  return p;
          239  +}
          240  +
          241  +/*
          242  +** Send a single line of output the SMTP client to the server.
          243  +*/
          244  +static void smtp_send_line(SmtpSession *p, const char *zFormat, ...){
          245  +  Blob b = empty_blob;
          246  +  va_list ap;
          247  +  char *z;
          248  +  int n;
          249  +  if( p->atEof ) return;
          250  +  va_start(ap, zFormat);
          251  +  blob_vappendf(&b, zFormat, ap);
          252  +  va_end(ap);
          253  +  z = blob_buffer(&b);
          254  +  n = blob_size(&b);
          255  +  assert( n>=2 );
          256  +  assert( z[n-1]=='\n' );
          257  +  assert( z[n-2]=='\r' );
          258  +  if( p->smtpFlags & SMTP_TRACE_STDOUT ){
          259  +    fossil_print("C: %.*s\n", n-2, z);
          260  +  }
          261  +  if( p->smtpFlags & SMTP_TRACE_FILE ){
          262  +    fprintf(p->logFile, "C: %.*s\n", n-2, z);
          263  +  }
          264  +  if( p->smtpFlags & SMTP_TRACE_BLOB ){
          265  +    blob_appendf(p->pTranscript, "C: %.*s\n", n-2, z);
          266  +  }
          267  +  socket_send(0, z, n);
          268  +  blob_reset(&b);
          269  +}
          270  +
          271  +/*
          272  +** Read a line of input received from the SMTP server.  Make in point
          273  +** to the next input line.
          274  +**
          275  +** Content is actually read into the p->in buffer.  Then blob_line()
          276  +** is used to extract individual lines, passing each to "in".
          277  +*/
          278  +static void smtp_recv_line(SmtpSession *p, Blob *in){
          279  +  int n = blob_size(&p->inbuf);
          280  +  char *z = blob_buffer(&p->inbuf);
          281  +  int i = blob_tell(&p->inbuf);
          282  +  int nDelay = 0;
          283  +  if( i<n && z[n-1]=='\n' ){
          284  +    blob_line(&p->inbuf, in);
          285  +  }else if( p->atEof ){
          286  +    blob_init(in, 0, 0);
          287  +  }else{
          288  +    if( n>0 && i>=n ){
          289  +      blob_truncate(&p->inbuf, 0);
          290  +      blob_rewind(&p->inbuf);
          291  +      n = 0;
          292  +    }
          293  +    do{
          294  +      size_t got;
          295  +      blob_resize(&p->inbuf, n+1000);
          296  +      z = blob_buffer(&p->inbuf);
          297  +      got = socket_receive(0, z+n, 1000, 1);
          298  +      if( got>0 ){
          299  +        in->nUsed += got;
          300  +        n += got;
          301  +        z[n] = 0;
          302  +        if( n>0 && z[n-1]=='\n' ) break;
          303  +        if( got==1000 ) continue;
          304  +      }
          305  +      nDelay++;
          306  +      if( nDelay>100 ){
          307  +        blob_init(in, 0, 0);
          308  +        p->zErr = mprintf("timeout");
          309  +        socket_close();
          310  +        p->atEof = 1;
          311  +        return;
          312  +      }else{
          313  +        sqlite3_sleep(100);
          314  +      }
          315  +    }while( n<1 || z[n-1]!='\n' );
          316  +    blob_truncate(&p->inbuf, n);
          317  +    blob_line(&p->inbuf, in);
          318  +  }
          319  +  z = blob_buffer(in);
          320  +  n = blob_size(in);
          321  +  if( n && z[n-1]=='\n' ) n--;
          322  +  if( n && z[n-1]=='\r' ) n--;
          323  +