Fossil

Check-in [e14c75676c]
Login

Check-in [e14c75676c]

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

Overview
Comment:Merge the latest trunk enhancements into the quickfilter branch.
Downloads: Tarball | ZIP archive
Timelines: family | ancestors | descendants | both | quickfilter
Files: files | file ages | folders
SHA3-256: e14c75676c3eb42afc4af2408c8dce2594ec3e1a3032db611d57a11fd1f573f1
User & Date: drh 2025-04-25 11:01:39.497
Context
2025-04-25
11:50
readd documentation for FOSSIL_REPOLIST_QUICKFILTER ... (check-in: 1a84e663e9 user: jkosche tags: quickfilter)
11:01
Merge the latest trunk enhancements into the quickfilter branch. ... (check-in: e14c75676c user: drh tags: quickfilter)
00:00
change input field to search, which brings clear button on some browser, as suggested in [forum:forumpost/005643ff358e5d34|forum post] ... (check-in: fa31d18bf4 user: jkosche tags: quickfilter)
2025-04-24
19:42
Block an infinite loop in Th_ReportTaint() that can occur when the vuln-report setting is "fatal" and the error happens again while generating the fatal error page. ... (check-in: 76f1ddb6c2 user: drh tags: trunk)
Changes
Unified Diff Ignore Whitespace Patch
Makefile.in became a regular file.
Changes to extsrc/shell.c.
1160
1161
1162
1163
1164
1165
1166

















1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182

1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194


1195
1196
1197
1198
1199
1200
1201
    }else{
      i++;
    }
  }
  return n;
}
#endif


















/*
** Output string zUtf to stdout as w characters.  If w is negative,
** then right-justify the text.  W is the width in UTF-8 characters, not
** in bytes.  This is different from the %*.*s specification in printf
** since with %*.*s the width is measured in bytes, not characters.
**
** Take into account zero-width and double-width Unicode characters.
** In other words, a zero-width character does not count toward the
** the w limit.  A double-width character counts as two.
*/
static void utf8_width_print(FILE *out, int w, const char *zUtf){
  const unsigned char *a = (const unsigned char*)zUtf;
  unsigned char c;
  int i = 0;
  int n = 0;

  int aw = w<0 ? -w : w;
  if( zUtf==0 ) zUtf = "";
  while( (c = a[i])!=0 ){
    if( (c&0xc0)==0xc0 ){
      int u;
      int len = decodeUtf8(a+i, &u);
      int x = cli_wcwidth(u);
      if( x+n>aw ){
        break;
      }
      i += len;
      n += x;


    }else if( n>=aw ){
      break;
    }else{
      n++;
      i++;
    }
  }







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
















>












>
>







1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
    }else{
      i++;
    }
  }
  return n;
}
#endif

/*
** Check to see if z[] is a valid VT100 escape.  If it is, then
** return the number of bytes in the escape sequence.  Return 0 if
** z[] is not a VT100 escape.
**
** This routine assumes that z[0] is \033 (ESC).
*/
static int isVt100(const unsigned char *z){
  int i;
  if( z[1]!='[' ) return 0;
  i = 2;
  while( z[i]>=0x30 && z[i]<=0x3f ){ i++; }
  while( z[i]>=0x20 && z[i]<=0x2f ){ i++; }
  if( z[i]<0x40 || z[i]>0x7e ) return 0;
  return i+1;
}

/*
** Output string zUtf to stdout as w characters.  If w is negative,
** then right-justify the text.  W is the width in UTF-8 characters, not
** in bytes.  This is different from the %*.*s specification in printf
** since with %*.*s the width is measured in bytes, not characters.
**
** Take into account zero-width and double-width Unicode characters.
** In other words, a zero-width character does not count toward the
** the w limit.  A double-width character counts as two.
*/
static void utf8_width_print(FILE *out, int w, const char *zUtf){
  const unsigned char *a = (const unsigned char*)zUtf;
  unsigned char c;
  int i = 0;
  int n = 0;
  int k;
  int aw = w<0 ? -w : w;
  if( zUtf==0 ) zUtf = "";
  while( (c = a[i])!=0 ){
    if( (c&0xc0)==0xc0 ){
      int u;
      int len = decodeUtf8(a+i, &u);
      int x = cli_wcwidth(u);
      if( x+n>aw ){
        break;
      }
      i += len;
      n += x;
    }else if( c==0x1b && (k = isVt100(&a[i]))>0 ){
      i += k;       
    }else if( n>=aw ){
      break;
    }else{
      n++;
      i++;
    }
  }
6268
6269
6270
6271
6272
6273
6274
6275
6276
6277
6278
6279
6280
6281
6282
6283
**     CREATE TABLE generate_series(
**       value,
**       start HIDDEN,
**       stop HIDDEN,
**       step HIDDEN
**     );
**
** The virtual table also has a rowid, logically equivalent to n+1 where
** "n" is the ascending integer in the aforesaid production definition.
**
** Function arguments in queries against this virtual table are translated
** into equality constraints against successive hidden columns.  In other
** words, the following pairs of queries are equivalent to each other:
**
**    SELECT * FROM generate_series(0,100,5);
**    SELECT * FROM generate_series WHERE start=0 AND stop=100 AND step=5;







|
<







6288
6289
6290
6291
6292
6293
6294
6295

6296
6297
6298
6299
6300
6301
6302
**     CREATE TABLE generate_series(
**       value,
**       start HIDDEN,
**       stop HIDDEN,
**       step HIDDEN
**     );
**
** The virtual table also has a rowid which is an alias for the value.

**
** Function arguments in queries against this virtual table are translated
** into equality constraints against successive hidden columns.  In other
** words, the following pairs of queries are equivalent to each other:
**
**    SELECT * FROM generate_series(0,100,5);
**    SELECT * FROM generate_series WHERE start=0 AND stop=100 AND step=5;
6324
6325
6326
6327
6328
6329
6330

6331
6332
6333
6334
6335
6336
6337
**
*/
/* #include "sqlite3ext.h" */
SQLITE_EXTENSION_INIT1
#include <assert.h>
#include <string.h>
#include <limits.h>


#ifndef SQLITE_OMIT_VIRTUALTABLE
/*
** Return that member of a generate_series(...) sequence whose 0-based
** index is ix. The 0th member is given by smBase. The sequence members
** progress per ix increment by smStep.
*/







>







6343
6344
6345
6346
6347
6348
6349
6350
6351
6352
6353
6354
6355
6356
6357
**
*/
/* #include "sqlite3ext.h" */
SQLITE_EXTENSION_INIT1
#include <assert.h>
#include <string.h>
#include <limits.h>
#include <math.h>

#ifndef SQLITE_OMIT_VIRTUALTABLE
/*
** Return that member of a generate_series(...) sequence whose 0-based
** index is ix. The 0th member is given by smBase. The sequence members
** progress per ix increment by smStep.
*/
6484
6485
6486
6487
6488
6489
6490

6491
6492
6493
6494
6495
6496
6497
  sqlite3_vtab **ppVtab,
  char **pzErrUnused
){
  sqlite3_vtab *pNew;
  int rc;

/* Column numbers */

#define SERIES_COLUMN_VALUE 0
#define SERIES_COLUMN_START 1
#define SERIES_COLUMN_STOP  2
#define SERIES_COLUMN_STEP  3

  (void)pUnused;
  (void)argcUnused;







>







6504
6505
6506
6507
6508
6509
6510
6511
6512
6513
6514
6515
6516
6517
6518
  sqlite3_vtab **ppVtab,
  char **pzErrUnused
){
  sqlite3_vtab *pNew;
  int rc;

/* Column numbers */
#define SERIES_COLUMN_ROWID (-1)
#define SERIES_COLUMN_VALUE 0
#define SERIES_COLUMN_START 1
#define SERIES_COLUMN_STOP  2
#define SERIES_COLUMN_STEP  3

  (void)pUnused;
  (void)argcUnused;
6571
6572
6573
6574
6575
6576
6577
6578
6579
6580
6581
6582
6583
6584
6585
6586
6587
6588
6589
6590
6591
#ifndef LARGEST_UINT64
#define LARGEST_INT64  (0xffffffff|(((sqlite3_int64)0x7fffffff)<<32))
#define LARGEST_UINT64 (0xffffffff|(((sqlite3_uint64)0xffffffff)<<32))
#define SMALLEST_INT64 (((sqlite3_int64)-1) - LARGEST_INT64)
#endif

/*
** Return the rowid for the current row, logically equivalent to n+1 where
** "n" is the ascending integer in the aforesaid production definition.
*/
static int seriesRowid(sqlite3_vtab_cursor *cur, sqlite_int64 *pRowid){
  series_cursor *pCur = (series_cursor*)cur;
  sqlite3_uint64 n = pCur->ss.uSeqIndexNow;
  *pRowid = (sqlite3_int64)((n<LARGEST_UINT64)? n+1 : 0);
  return SQLITE_OK;
}

/*
** Return TRUE if the cursor has been moved off of the last
** row of output.
*/







|
<



|
<







6592
6593
6594
6595
6596
6597
6598
6599

6600
6601
6602
6603

6604
6605
6606
6607
6608
6609
6610
#ifndef LARGEST_UINT64
#define LARGEST_INT64  (0xffffffff|(((sqlite3_int64)0x7fffffff)<<32))
#define LARGEST_UINT64 (0xffffffff|(((sqlite3_uint64)0xffffffff)<<32))
#define SMALLEST_INT64 (((sqlite3_int64)-1) - LARGEST_INT64)
#endif

/*
** The rowid is the same as the value.

*/
static int seriesRowid(sqlite3_vtab_cursor *cur, sqlite_int64 *pRowid){
  series_cursor *pCur = (series_cursor*)cur;
  *pRowid = pCur->ss.iValueNow;

  return SQLITE_OK;
}

/*
** Return TRUE if the cursor has been moved off of the last
** row of output.
*/
6690
6691
6692
6693
6694
6695
6696








6697

6698
6699








6700
6701
6702
6703
6704
6705

6706
6707
6708
6709








6710
6711
6712
6713
6714
6715

6716
6717
6718
6719
6720
6721
6722
  }

  if( idxNum & 0x3380 ){
    /* Extract the maximum range of output values determined by
    ** constraints on the "value" column.
    */
    if( idxNum & 0x0080 ){








      iMin = iMax = sqlite3_value_int64(argv[i++]);

    }else{
      if( idxNum & 0x0300 ){








        iMin = sqlite3_value_int64(argv[i++]);
        if( idxNum & 0x0200 ){
          if( iMin==LARGEST_INT64 ){
            returnNoRows = 1;
          }else{
            iMin++;

          }
        }
      }
      if( idxNum & 0x3000 ){








        iMax = sqlite3_value_int64(argv[i++]);
        if( idxNum & 0x2000 ){
          if( iMax==SMALLEST_INT64 ){
            returnNoRows = 1;
          }else{
            iMax--;

          }
        }
      }
      if( iMin>iMax ){
        returnNoRows = 1;
      }
    }







>
>
>
>
>
>
>
>
|
>


>
>
>
>
>
>
>
>
|
|
|
|
|
|
>




>
>
>
>
>
>
>
>
|
|
|
|
|
|
>







6709
6710
6711
6712
6713
6714
6715
6716
6717
6718
6719
6720
6721
6722
6723
6724
6725
6726
6727
6728
6729
6730
6731
6732
6733
6734
6735
6736
6737
6738
6739
6740
6741
6742
6743
6744
6745
6746
6747
6748
6749
6750
6751
6752
6753
6754
6755
6756
6757
6758
6759
6760
6761
6762
6763
6764
6765
6766
6767
6768
  }

  if( idxNum & 0x3380 ){
    /* Extract the maximum range of output values determined by
    ** constraints on the "value" column.
    */
    if( idxNum & 0x0080 ){
      if( sqlite3_value_numeric_type(argv[i])==SQLITE_FLOAT ){
        double r = sqlite3_value_double(argv[i++]);
        if( r==ceil(r) ){
          iMin = iMax = (sqlite3_int64)r;
        }else{
          returnNoRows = 1;
        }
      }else{
        iMin = iMax = sqlite3_value_int64(argv[i++]);
      }
    }else{
      if( idxNum & 0x0300 ){
        if( sqlite3_value_numeric_type(argv[i])==SQLITE_FLOAT ){
          double r = sqlite3_value_double(argv[i++]);
          if( idxNum & 0x0200 && r==ceil(r) ){
            iMin = (sqlite3_int64)ceil(r+1.0);
          }else{
            iMin = (sqlite3_int64)ceil(r);
          }
        }else{
          iMin = sqlite3_value_int64(argv[i++]);
          if( idxNum & 0x0200 ){
            if( iMin==LARGEST_INT64 ){
              returnNoRows = 1;
            }else{
              iMin++;
            }
          }
        }
      }
      if( idxNum & 0x3000 ){
        if( sqlite3_value_numeric_type(argv[i])==SQLITE_FLOAT ){
          double r = sqlite3_value_double(argv[i++]);
          if( (idxNum & 0x2000)!=0 && r==floor(r) ){
            iMax = (sqlite3_int64)(r-1.0);
          }else{
            iMax = (sqlite3_int64)floor(r);
          }
        }else{
          iMax = sqlite3_value_int64(argv[i++]);
          if( idxNum & 0x2000 ){
            if( iMax==SMALLEST_INT64 ){
              returnNoRows = 1;
            }else{
              iMax--;
            }
          }
        }
      }
      if( iMin>iMax ){
        returnNoRows = 1;
      }
    }
6762
6763
6764
6765
6766
6767
6768
6769
6770
6771
6772
6773
6774
6775
6776
    }
  }


  for(i=0; i<argc; i++){
    if( sqlite3_value_type(argv[i])==SQLITE_NULL ){
      /* If any of the constraints have a NULL value, then return no rows.
      ** See ticket https://www.sqlite.org/src/info/fac496b61722daf2 */
      returnNoRows = 1;
      break;
    }
  }
  if( returnNoRows ){
    pCur->ss.iBase = 1;
    pCur->ss.iTerm = 0;







|







6808
6809
6810
6811
6812
6813
6814
6815
6816
6817
6818
6819
6820
6821
6822
    }
  }


  for(i=0; i<argc; i++){
    if( sqlite3_value_type(argv[i])==SQLITE_NULL ){
      /* If any of the constraints have a NULL value, then return no rows.
      ** See ticket https://sqlite.org/src/info/fac496b61722daf2 */
      returnNoRows = 1;
      break;
    }
  }
  if( returnNoRows ){
    pCur->ss.iBase = 1;
    pCur->ss.iTerm = 0;
6865
6866
6867
6868
6869
6870
6871
6872



6873
6874
6875
6876
6877
6878
6879
        assert( op==SQLITE_INDEX_CONSTRAINT_OFFSET );
        aIdx[4] = i;
        idxNum |= 0x40;
      }
      continue;
    }
    if( pConstraint->iColumn<SERIES_COLUMN_START ){
      if( pConstraint->iColumn==SERIES_COLUMN_VALUE && pConstraint->usable ){



        switch( op ){
          case SQLITE_INDEX_CONSTRAINT_EQ:
          case SQLITE_INDEX_CONSTRAINT_IS: {
            idxNum |=  0x0080;
            idxNum &= ~0x3300;
            aIdx[5] = i;
            aIdx[6] = -1;







|
>
>
>







6911
6912
6913
6914
6915
6916
6917
6918
6919
6920
6921
6922
6923
6924
6925
6926
6927
6928
        assert( op==SQLITE_INDEX_CONSTRAINT_OFFSET );
        aIdx[4] = i;
        idxNum |= 0x40;
      }
      continue;
    }
    if( pConstraint->iColumn<SERIES_COLUMN_START ){
      if( (pConstraint->iColumn==SERIES_COLUMN_VALUE ||
           pConstraint->iColumn==SERIES_COLUMN_ROWID)
       && pConstraint->usable
      ){
        switch( op ){
          case SQLITE_INDEX_CONSTRAINT_EQ:
          case SQLITE_INDEX_CONSTRAINT_IS: {
            idxNum |=  0x0080;
            idxNum &= ~0x3300;
            aIdx[5] = i;
            aIdx[6] = -1;
24195
24196
24197
24198
24199
24200
24201




24202
24203
24204

24205
24206
24207
24208
24209
24210
24211
      do{
        n++;
        j++;
      }while( (n&7)!=0 && n<mxWidth );
      i++;
      continue;
    }




    n++;
    j += 3;
    i++;

  }
  if( n>=mxWidth && bWordWrap  ){
    /* Perhaps try to back up to a better place to break the line */
    for(k=i; k>i/2; k--){
      if( IsSpace(z[k-1]) ) break;
    }
    if( k<=i/2 ){







>
>
>
>
|
|
|
>







24244
24245
24246
24247
24248
24249
24250
24251
24252
24253
24254
24255
24256
24257
24258
24259
24260
24261
24262
24263
24264
24265
      do{
        n++;
        j++;
      }while( (n&7)!=0 && n<mxWidth );
      i++;
      continue;
    }
    if( c==0x1b && p->eEscMode==SHELL_ESC_OFF && (k = isVt100(&z[i]))>0 ){
      i += k;
      j += k;
    }else{
      n++;
      j += 3;
      i++;
    }
  }
  if( n>=mxWidth && bWordWrap  ){
    /* Perhaps try to back up to a better place to break the line */
    for(k=i; k>i/2; k--){
      if( IsSpace(z[k-1]) ) break;
    }
    if( k<=i/2 ){
24263
24264
24265
24266
24267
24268
24269
24270






24271

24272

24273
24274
24275
24276
24277
24278
24279
        zOut[j++] = 0x90;
        zOut[j++] = 0x80 + c;
        break;
      case SHELL_ESC_ASCII:
        zOut[j++] = '^';
        zOut[j++] = 0x40 + c;
        break;
      case SHELL_ESC_OFF:






        zOut[j++] = c;

        break;

    }
    i++;
  }
  zOut[j] = 0;
  return (char*)zOut;
}








|
>
>
>
>
>
>
|
>

>







24317
24318
24319
24320
24321
24322
24323
24324
24325
24326
24327
24328
24329
24330
24331
24332
24333
24334
24335
24336
24337
24338
24339
24340
24341
        zOut[j++] = 0x90;
        zOut[j++] = 0x80 + c;
        break;
      case SHELL_ESC_ASCII:
        zOut[j++] = '^';
        zOut[j++] = 0x40 + c;
        break;
      case SHELL_ESC_OFF: {
        int nn;
        if( c==0x1b && (nn = isVt100(&z[i]))>0 ){
          memcpy(&zOut[j], &z[i], nn);
          j += nn;
          i += nn - 1;
        }else{
          zOut[j++] = c;
        }
        break;
      }
    }
    i++;
  }
  zOut[j] = 0;
  return (char*)zOut;
}

25402
25403
25404
25405
25406
25407
25408

25409
25410
25411
25412
25413
25414
25415
  "        --new           Initialize FILE to an empty database",
  "        --nofollow      Do not follow symbolic links",
  "        --readonly      Open FILE readonly",
  "        --zip           FILE is a ZIP archive",
#ifndef SQLITE_SHELL_FIDDLE
  ".output ?FILE?           Send output to FILE or stdout if FILE is omitted",
  "   If FILE begins with '|' then open it as a pipe.",

  "   Options:",
  "     --bom                 Prefix output with a UTF8 byte-order mark",
  "     -e                    Send output to the system text editor",
  "     --plain               Use text/plain for -w option",
  "     -w                    Send output to a web browser",
  "     -x                    Send output as CSV to a spreadsheet",
#endif







>







25464
25465
25466
25467
25468
25469
25470
25471
25472
25473
25474
25475
25476
25477
25478
  "        --new           Initialize FILE to an empty database",
  "        --nofollow      Do not follow symbolic links",
  "        --readonly      Open FILE readonly",
  "        --zip           FILE is a ZIP archive",
#ifndef SQLITE_SHELL_FIDDLE
  ".output ?FILE?           Send output to FILE or stdout if FILE is omitted",
  "   If FILE begins with '|' then open it as a pipe.",
  "   If FILE is 'off' then output is disabled.",
  "   Options:",
  "     --bom                 Prefix output with a UTF8 byte-order mark",
  "     -e                    Send output to the system text editor",
  "     --plain               Use text/plain for -w option",
  "     -w                    Send output to a web browser",
  "     -x                    Send output as CSV to a spreadsheet",
#endif
27019
27020
27021
27022
27023
27024
27025
27026
27027
27028
27029
27030
27031
27032
27033
    const u8 *aData = sqlite3_column_blob(pStmt, 1);
    int seenPageLabel = 0;
    for(i=0; i<pgSz; i+=16){
      const u8 *aLine = aData+i;
      for(j=0; j<16 && aLine[j]==0; j++){}
      if( j==16 ) continue;
      if( !seenPageLabel ){
        sqlite3_fprintf(p->out, "| page %lld offset %lld\n", pgno, pgno*pgSz);
        seenPageLabel = 1;
      }
      sqlite3_fprintf(p->out, "|  %5d:", i);
      for(j=0; j<16; j++) sqlite3_fprintf(p->out, " %02x", aLine[j]);
      sqlite3_fprintf(p->out, "   ");
      for(j=0; j<16; j++){
        unsigned char c = (unsigned char)aLine[j];







|







27082
27083
27084
27085
27086
27087
27088
27089
27090
27091
27092
27093
27094
27095
27096
    const u8 *aData = sqlite3_column_blob(pStmt, 1);
    int seenPageLabel = 0;
    for(i=0; i<pgSz; i+=16){
      const u8 *aLine = aData+i;
      for(j=0; j<16 && aLine[j]==0; j++){}
      if( j==16 ) continue;
      if( !seenPageLabel ){
        sqlite3_fprintf(p->out, "| page %lld offset %lld\n",pgno,(pgno-1)*pgSz);
        seenPageLabel = 1;
      }
      sqlite3_fprintf(p->out, "|  %5d:", i);
      for(j=0; j<16; j++) sqlite3_fprintf(p->out, " %02x", aLine[j]);
      sqlite3_fprintf(p->out, "   ");
      for(j=0; j<16; j++){
        unsigned char c = (unsigned char)aLine[j];
30397
30398
30399
30400
30401
30402
30403
30404
30405
30406
30407
30408
30409
30410
30411
30412
30413
        && (cli_strncmp(azArg[0], "output", n)==0
            || cli_strncmp(azArg[0], "once", n)==0))
   || (c=='e' && n==5 && cli_strcmp(azArg[0],"excel")==0)
   || (c=='w' && n==3 && cli_strcmp(azArg[0],"www")==0)
  ){
    char *zFile = 0;
    int i;
    int eMode = 0;
    int bOnce = 0;            /* 0: .output, 1: .once, 2: .excel/.www */
    int bPlain = 0;           /* --plain option */
    static const char *zBomUtf8 = "\357\273\277";
    const char *zBom = 0;

    failIfSafeMode(p, "cannot run .%s in safe mode", azArg[0]);
    if( c=='e' ){
      eMode = 'x';
      bOnce = 2;







|
|
|







30460
30461
30462
30463
30464
30465
30466
30467
30468
30469
30470
30471
30472
30473
30474
30475
30476
        && (cli_strncmp(azArg[0], "output", n)==0
            || cli_strncmp(azArg[0], "once", n)==0))
   || (c=='e' && n==5 && cli_strcmp(azArg[0],"excel")==0)
   || (c=='w' && n==3 && cli_strcmp(azArg[0],"www")==0)
  ){
    char *zFile = 0;
    int i;
    int eMode = 0;          /* 0: .outout/.once, 'x'=.excel, 'w'=.www */
    int bOnce = 0;          /* 0: .output, 1: .once, 2: .excel/.www */
    int bPlain = 0;         /* --plain option */
    static const char *zBomUtf8 = "\357\273\277";
    const char *zBom = 0;

    failIfSafeMode(p, "cannot run .%s in safe mode", azArg[0]);
    if( c=='e' ){
      eMode = 'x';
      bOnce = 2;
30428
30429
30430
30431
30432
30433
30434
30435
30436
30437
30438
30439
30440
30441







30442

30443
30444
30445
30446
30447
30448
30449
        }else if( c=='o' && cli_strcmp(z,"-x")==0 ){
          eMode = 'x';  /* spreadsheet */
        }else if( c=='o' && cli_strcmp(z,"-e")==0 ){
          eMode = 'e';  /* text editor */
        }else if( c=='o' && cli_strcmp(z,"-w")==0 ){
          eMode = 'w';  /* Web browser */
        }else{
          sqlite3_fprintf(p->out, 
                          "ERROR: unknown option: \"%s\". Usage:\n", azArg[i]);
          showHelp(p->out, azArg[0]);
          rc = 1;
          goto meta_command_exit;
        }
      }else if( zFile==0 && eMode==0 ){







        zFile = sqlite3_mprintf("%s", z);

        if( zFile && zFile[0]=='|' ){
          while( i+1<nArg ) zFile = sqlite3_mprintf("%z %s", zFile, azArg[++i]);
          break;
        }
      }else{
        sqlite3_fprintf(p->out,
            "ERROR: extra parameter: \"%s\".  Usage:\n", azArg[i]);







|






>
>
>
>
>
>
>
|
>







30491
30492
30493
30494
30495
30496
30497
30498
30499
30500
30501
30502
30503
30504
30505
30506
30507
30508
30509
30510
30511
30512
30513
30514
30515
30516
30517
30518
30519
30520
        }else if( c=='o' && cli_strcmp(z,"-x")==0 ){
          eMode = 'x';  /* spreadsheet */
        }else if( c=='o' && cli_strcmp(z,"-e")==0 ){
          eMode = 'e';  /* text editor */
        }else if( c=='o' && cli_strcmp(z,"-w")==0 ){
          eMode = 'w';  /* Web browser */
        }else{
          sqlite3_fprintf(p->out,
                          "ERROR: unknown option: \"%s\". Usage:\n", azArg[i]);
          showHelp(p->out, azArg[0]);
          rc = 1;
          goto meta_command_exit;
        }
      }else if( zFile==0 && eMode==0 ){
        if( cli_strcmp(z, "off")==0 ){
#ifdef _WIN32
          zFile = sqlite3_mprintf("nul");
#else
          zFile = sqlite3_mprintf("/dev/null");
#endif
        }else{
          zFile = sqlite3_mprintf("%s", z);
        }
        if( zFile && zFile[0]=='|' ){
          while( i+1<nArg ) zFile = sqlite3_mprintf("%z %s", zFile, azArg[++i]);
          break;
        }
      }else{
        sqlite3_fprintf(p->out,
            "ERROR: extra parameter: \"%s\".  Usage:\n", azArg[i]);
Changes to extsrc/sqlite3.c.
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
** the text of this file.  Search for "Begin file sqlite3.h" to find the start
** of the embedded sqlite3.h header file.) Additional code files may be needed
** if you want a wrapper to interface SQLite with your choice of programming
** language. The code for the "sqlite3" command-line shell is also in a
** separate file. This file contains only code for the core SQLite library.
**
** The content in this amalgamation comes from Fossil check-in
** 18bda13e197e4b4ec7464b3e70012f71edc0 with changes in files:
**
**    
*/
#ifndef SQLITE_AMALGAMATION
#define SQLITE_CORE 1
#define SQLITE_AMALGAMATION 1
#ifndef SQLITE_PRIVATE







|







14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
** the text of this file.  Search for "Begin file sqlite3.h" to find the start
** of the embedded sqlite3.h header file.) Additional code files may be needed
** if you want a wrapper to interface SQLite with your choice of programming
** language. The code for the "sqlite3" command-line shell is also in a
** separate file. This file contains only code for the core SQLite library.
**
** The content in this amalgamation comes from Fossil check-in
** d22475b81c4e26ccc50f3b5626d43b32f7a2 with changes in files:
**
**    
*/
#ifndef SQLITE_AMALGAMATION
#define SQLITE_CORE 1
#define SQLITE_AMALGAMATION 1
#ifndef SQLITE_PRIVATE
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
** The SQLITE_VERSION_NUMBER for any given release of SQLite will also
** be larger than the release from which it is derived.  Either Y will
** be held constant and Z will be incremented or else Y will be incremented
** and Z will be reset to zero.
**
** Since [version 3.6.18] ([dateof:3.6.18]),
** SQLite source code has been stored in the
** <a href="http://www.fossil-scm.org/">Fossil configuration management
** system</a>.  ^The SQLITE_SOURCE_ID macro evaluates to
** a string which identifies a particular check-in of SQLite
** within its configuration management system.  ^The SQLITE_SOURCE_ID
** string contains the date and time of the check-in (UTC) and a SHA1
** or SHA3-256 hash of the entire source tree.  If the source code has
** been edited in any way since it was last checked in, then the last
** four hexadecimal digits of the hash may be modified.
**
** See also: [sqlite3_libversion()],
** [sqlite3_libversion_number()], [sqlite3_sourceid()],
** [sqlite_version()] and [sqlite_source_id()].
*/
#define SQLITE_VERSION        "3.50.0"
#define SQLITE_VERSION_NUMBER 3050000
#define SQLITE_SOURCE_ID      "2025-03-16 00:13:29 18bda13e197e4b4ec7464b3e70012f71edc05f73d8b14bb48bad452f81c7e185"

/*
** CAPI3REF: Run-Time Library Version Numbers
** KEYWORDS: sqlite3_version sqlite3_sourceid
**
** These interfaces provide the same information as the [SQLITE_VERSION],
** [SQLITE_VERSION_NUMBER], and [SQLITE_SOURCE_ID] C preprocessor macros







|














|







448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
** The SQLITE_VERSION_NUMBER for any given release of SQLite will also
** be larger than the release from which it is derived.  Either Y will
** be held constant and Z will be incremented or else Y will be incremented
** and Z will be reset to zero.
**
** Since [version 3.6.18] ([dateof:3.6.18]),
** SQLite source code has been stored in the
** <a href="http://fossil-scm.org/">Fossil configuration management
** system</a>.  ^The SQLITE_SOURCE_ID macro evaluates to
** a string which identifies a particular check-in of SQLite
** within its configuration management system.  ^The SQLITE_SOURCE_ID
** string contains the date and time of the check-in (UTC) and a SHA1
** or SHA3-256 hash of the entire source tree.  If the source code has
** been edited in any way since it was last checked in, then the last
** four hexadecimal digits of the hash may be modified.
**
** See also: [sqlite3_libversion()],
** [sqlite3_libversion_number()], [sqlite3_sourceid()],
** [sqlite_version()] and [sqlite_source_id()].
*/
#define SQLITE_VERSION        "3.50.0"
#define SQLITE_VERSION_NUMBER 3050000
#define SQLITE_SOURCE_ID      "2025-04-15 21:59:38 d22475b81c4e26ccc50f3b5626d43b32f7a2de34e5a764539554665bdda735d5"

/*
** CAPI3REF: Run-Time Library Version Numbers
** KEYWORDS: sqlite3_version sqlite3_sourceid
**
** These interfaces provide the same information as the [SQLITE_VERSION],
** [SQLITE_VERSION_NUMBER], and [SQLITE_SOURCE_ID] C preprocessor macros
11944
11945
11946
11947
11948
11949
11950

11951
11952
11953
11954
11955
11956
11957
11958
11959
** </ul>
**
** To clarify, if this function is called and then a changeset constructed
** using [sqlite3session_changeset()], then after applying that changeset to
** database zFrom the contents of the two compatible tables would be
** identical.
**

** It an error if database zFrom does not exist or does not contain the
** required compatible table.
**
** If the operation is successful, SQLITE_OK is returned. Otherwise, an SQLite
** error code. In this case, if argument pzErrMsg is not NULL, *pzErrMsg
** may be set to point to a buffer containing an English language error
** message. It is the responsibility of the caller to free this buffer using
** sqlite3_free().
*/







>
|
|







11944
11945
11946
11947
11948
11949
11950
11951
11952
11953
11954
11955
11956
11957
11958
11959
11960
** </ul>
**
** To clarify, if this function is called and then a changeset constructed
** using [sqlite3session_changeset()], then after applying that changeset to
** database zFrom the contents of the two compatible tables would be
** identical.
**
** Unless the call to this function is a no-op as described above, it is an
** error if database zFrom does not exist or does not contain the required
** compatible table.
**
** If the operation is successful, SQLITE_OK is returned. Otherwise, an SQLite
** error code. In this case, if argument pzErrMsg is not NULL, *pzErrMsg
** may be set to point to a buffer containing an English language error
** message. It is the responsibility of the caller to free this buffer using
** sqlite3_free().
*/
19160
19161
19162
19163
19164
19165
19166

19167
19168
19169
19170
19171
19172
19173
  unsigned isResized:1;    /* True if resizeIndexObject() has been called */
  unsigned isCovering:1;   /* True if this is a covering index */
  unsigned noSkipScan:1;   /* Do not try to use skip-scan if true */
  unsigned hasStat1:1;     /* aiRowLogEst values come from sqlite_stat1 */
  unsigned bLowQual:1;     /* sqlite_stat1 says this is a low-quality index */
  unsigned bNoQuery:1;     /* Do not use this index to optimize queries */
  unsigned bAscKeyBug:1;   /* True if the bba7b69f9849b5bf bug applies */

  unsigned bHasVCol:1;     /* Index references one or more VIRTUAL columns */
  unsigned bHasExpr:1;     /* Index contains an expression, either a literal
                           ** expression, or a reference to a VIRTUAL column */
#ifdef SQLITE_ENABLE_STAT4
  int nSample;             /* Number of elements in aSample[] */
  int mxSample;            /* Number of slots allocated to aSample[] */
  int nSampleCol;          /* Size of IndexSample.anEq[] and so on */







>







19161
19162
19163
19164
19165
19166
19167
19168
19169
19170
19171
19172
19173
19174
19175
  unsigned isResized:1;    /* True if resizeIndexObject() has been called */
  unsigned isCovering:1;   /* True if this is a covering index */
  unsigned noSkipScan:1;   /* Do not try to use skip-scan if true */
  unsigned hasStat1:1;     /* aiRowLogEst values come from sqlite_stat1 */
  unsigned bLowQual:1;     /* sqlite_stat1 says this is a low-quality index */
  unsigned bNoQuery:1;     /* Do not use this index to optimize queries */
  unsigned bAscKeyBug:1;   /* True if the bba7b69f9849b5bf bug applies */
  unsigned bIdxRowid:1;    /* One or more of the index keys is the ROWID */
  unsigned bHasVCol:1;     /* Index references one or more VIRTUAL columns */
  unsigned bHasExpr:1;     /* Index contains an expression, either a literal
                           ** expression, or a reference to a VIRTUAL column */
#ifdef SQLITE_ENABLE_STAT4
  int nSample;             /* Number of elements in aSample[] */
  int mxSample;            /* Number of slots allocated to aSample[] */
  int nSampleCol;          /* Size of IndexSample.anEq[] and so on */
30268
30269
30270
30271
30272
30273
30274


30275
30276
30277
30278
30279
30280
30281
/*
** Include the primary Windows SDK header file.
*/
#include "windows.h"

#ifdef __CYGWIN__
# include <sys/cygwin.h>


# include <errno.h> /* amalgamator: dontcache */
#endif

/*
** Determine if we are dealing with Windows NT.
**
** We ought to be able to determine if we are compiling for Windows 9x or







>
>







30270
30271
30272
30273
30274
30275
30276
30277
30278
30279
30280
30281
30282
30283
30284
30285
/*
** Include the primary Windows SDK header file.
*/
#include "windows.h"

#ifdef __CYGWIN__
# include <sys/cygwin.h>
# include <sys/stat.h> /* amalgamator: dontcache */
# include <unistd.h> /* amalgamator: dontcache */
# include <errno.h> /* amalgamator: dontcache */
#endif

/*
** Determine if we are dealing with Windows NT.
**
** We ought to be able to determine if we are compiling for Windows 9x or
35442
35443
35444
35445
35446
35447
35448
35449
35450
35451
35452
35453
35454
35455
35456
SQLITE_PRIVATE int sqlite3Utf16ByteLen(const void *zIn, int nByte, int nChar){
  int c;
  unsigned char const *z = zIn;
  unsigned char const *zEnd = &z[nByte-1];
  int n = 0;

  if( SQLITE_UTF16NATIVE==SQLITE_UTF16LE ) z++;
  while( n<nChar && ALWAYS(z<=zEnd) ){
    c = z[0];
    z += 2;
    if( c>=0xd8 && c<0xdc && z<=zEnd && z[0]>=0xdc && z[0]<0xe0 ) z += 2;
    n++;
  }
  return (int)(z-(unsigned char const *)zIn)
              - (SQLITE_UTF16NATIVE==SQLITE_UTF16LE);







|







35446
35447
35448
35449
35450
35451
35452
35453
35454
35455
35456
35457
35458
35459
35460
SQLITE_PRIVATE int sqlite3Utf16ByteLen(const void *zIn, int nByte, int nChar){
  int c;
  unsigned char const *z = zIn;
  unsigned char const *zEnd = &z[nByte-1];
  int n = 0;

  if( SQLITE_UTF16NATIVE==SQLITE_UTF16LE ) z++;
  while( n<nChar && z<=zEnd ){
    c = z[0];
    z += 2;
    if( c>=0xd8 && c<0xdc && z<=zEnd && z[0]>=0xdc && z[0]<0xe0 ) z += 2;
    n++;
  }
  return (int)(z-(unsigned char const *)zIn)
              - (SQLITE_UTF16NATIVE==SQLITE_UTF16LE);
47724
47725
47726
47727
47728
47729
47730
47731
47732
47733
47734
47735
47736
47737
47738
47739
47740
47741
47742
47743
47744
47745
47746
47747

#if SQLITE_OS_WINCE
  { "FileTimeToLocalFileTime", (SYSCALL)FileTimeToLocalFileTime, 0 },
#else
  { "FileTimeToLocalFileTime", (SYSCALL)0,                       0 },
#endif

#define osFileTimeToLocalFileTime ((BOOL(WINAPI*)(CONST FILETIME*, \
        LPFILETIME))aSyscall[11].pCurrent)

#if SQLITE_OS_WINCE
  { "FileTimeToSystemTime",    (SYSCALL)FileTimeToSystemTime,    0 },
#else
  { "FileTimeToSystemTime",    (SYSCALL)0,                       0 },
#endif

#define osFileTimeToSystemTime ((BOOL(WINAPI*)(CONST FILETIME*, \
        LPSYSTEMTIME))aSyscall[12].pCurrent)

  { "FlushFileBuffers",        (SYSCALL)FlushFileBuffers,        0 },

#define osFlushFileBuffers ((BOOL(WINAPI*)(HANDLE))aSyscall[13].pCurrent)

#if defined(SQLITE_WIN32_HAS_ANSI)







|








|







47728
47729
47730
47731
47732
47733
47734
47735
47736
47737
47738
47739
47740
47741
47742
47743
47744
47745
47746
47747
47748
47749
47750
47751

#if SQLITE_OS_WINCE
  { "FileTimeToLocalFileTime", (SYSCALL)FileTimeToLocalFileTime, 0 },
#else
  { "FileTimeToLocalFileTime", (SYSCALL)0,                       0 },
#endif

#define osFileTimeToLocalFileTime ((BOOL(WINAPI*)(const FILETIME*, \
        LPFILETIME))aSyscall[11].pCurrent)

#if SQLITE_OS_WINCE
  { "FileTimeToSystemTime",    (SYSCALL)FileTimeToSystemTime,    0 },
#else
  { "FileTimeToSystemTime",    (SYSCALL)0,                       0 },
#endif

#define osFileTimeToSystemTime ((BOOL(WINAPI*)(const FILETIME*, \
        LPSYSTEMTIME))aSyscall[12].pCurrent)

  { "FlushFileBuffers",        (SYSCALL)FlushFileBuffers,        0 },

#define osFlushFileBuffers ((BOOL(WINAPI*)(HANDLE))aSyscall[13].pCurrent)

#if defined(SQLITE_WIN32_HAS_ANSI)
48013
48014
48015
48016
48017
48018
48019
48020
48021
48022
48023
48024
48025
48026
48027

#if !SQLITE_OS_WINCE && !SQLITE_OS_WINRT
  { "LockFile",                (SYSCALL)LockFile,                0 },
#else
  { "LockFile",                (SYSCALL)0,                       0 },
#endif

#ifndef osLockFile
#define osLockFile ((BOOL(WINAPI*)(HANDLE,DWORD,DWORD,DWORD, \
        DWORD))aSyscall[47].pCurrent)
#endif

#if !SQLITE_OS_WINCE
  { "LockFileEx",              (SYSCALL)LockFileEx,              0 },
#else







|







48017
48018
48019
48020
48021
48022
48023
48024
48025
48026
48027
48028
48029
48030
48031

#if !SQLITE_OS_WINCE && !SQLITE_OS_WINRT
  { "LockFile",                (SYSCALL)LockFile,                0 },
#else
  { "LockFile",                (SYSCALL)0,                       0 },
#endif

#if !defined(osLockFile) && defined(SQLITE_WIN32_HAS_ANSI)
#define osLockFile ((BOOL(WINAPI*)(HANDLE,DWORD,DWORD,DWORD, \
        DWORD))aSyscall[47].pCurrent)
#endif

#if !SQLITE_OS_WINCE
  { "LockFileEx",              (SYSCALL)LockFileEx,              0 },
#else
48077
48078
48079
48080
48081
48082
48083
48084
48085
48086
48087
48088
48089
48090
48091
48092
48093
48094
48095
48096
48097
48098
48099
48100
  { "Sleep",                   (SYSCALL)0,                       0 },
#endif

#define osSleep ((VOID(WINAPI*)(DWORD))aSyscall[55].pCurrent)

  { "SystemTimeToFileTime",    (SYSCALL)SystemTimeToFileTime,    0 },

#define osSystemTimeToFileTime ((BOOL(WINAPI*)(CONST SYSTEMTIME*, \
        LPFILETIME))aSyscall[56].pCurrent)

#if !SQLITE_OS_WINCE && !SQLITE_OS_WINRT
  { "UnlockFile",              (SYSCALL)UnlockFile,              0 },
#else
  { "UnlockFile",              (SYSCALL)0,                       0 },
#endif

#ifndef osUnlockFile
#define osUnlockFile ((BOOL(WINAPI*)(HANDLE,DWORD,DWORD,DWORD, \
        DWORD))aSyscall[57].pCurrent)
#endif

#if !SQLITE_OS_WINCE
  { "UnlockFileEx",            (SYSCALL)UnlockFileEx,            0 },
#else







|








|







48081
48082
48083
48084
48085
48086
48087
48088
48089
48090
48091
48092
48093
48094
48095
48096
48097
48098
48099
48100
48101
48102
48103
48104
  { "Sleep",                   (SYSCALL)0,                       0 },
#endif

#define osSleep ((VOID(WINAPI*)(DWORD))aSyscall[55].pCurrent)

  { "SystemTimeToFileTime",    (SYSCALL)SystemTimeToFileTime,    0 },

#define osSystemTimeToFileTime ((BOOL(WINAPI*)(const SYSTEMTIME*, \
        LPFILETIME))aSyscall[56].pCurrent)

#if !SQLITE_OS_WINCE && !SQLITE_OS_WINRT
  { "UnlockFile",              (SYSCALL)UnlockFile,              0 },
#else
  { "UnlockFile",              (SYSCALL)0,                       0 },
#endif

#if !defined(osUnlockFile) && defined(SQLITE_WIN32_HAS_ANSI)
#define osUnlockFile ((BOOL(WINAPI*)(HANDLE,DWORD,DWORD,DWORD, \
        DWORD))aSyscall[57].pCurrent)
#endif

#if !SQLITE_OS_WINCE
  { "UnlockFileEx",            (SYSCALL)UnlockFileEx,            0 },
#else
48314
48315
48316
48317
48318
48319
48320

























































48321
48322
48323
48324
48325
48326
48327
  { "CancelIo",                 (SYSCALL)CancelIo,               0 },
#else
  { "CancelIo",                 (SYSCALL)0,                      0 },
#endif

#define osCancelIo ((BOOL(WINAPI*)(HANDLE))aSyscall[81].pCurrent)


























































}; /* End of the overrideable system calls */

/*
** This is the xSetSystemCall() method of sqlite3_vfs for all of the
** "win32" VFSes.  Return SQLITE_OK upon successfully updating the
** system call pointer, or SQLITE_NOTFOUND if there is no configurable
** system call named zName.







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







48318
48319
48320
48321
48322
48323
48324
48325
48326
48327
48328
48329
48330
48331
48332
48333
48334
48335
48336
48337
48338
48339
48340
48341
48342
48343
48344
48345
48346
48347
48348
48349
48350
48351
48352
48353
48354
48355
48356
48357
48358
48359
48360
48361
48362
48363
48364
48365
48366
48367
48368
48369
48370
48371
48372
48373
48374
48375
48376
48377
48378
48379
48380
48381
48382
48383
48384
48385
48386
48387
48388
  { "CancelIo",                 (SYSCALL)CancelIo,               0 },
#else
  { "CancelIo",                 (SYSCALL)0,                      0 },
#endif

#define osCancelIo ((BOOL(WINAPI*)(HANDLE))aSyscall[81].pCurrent)

#if defined(SQLITE_WIN32_HAS_WIDE) && defined(_WIN32)
  { "GetModuleHandleW",         (SYSCALL)GetModuleHandleW,       0 },
#else
  { "GetModuleHandleW",         (SYSCALL)0,                      0 },
#endif

#define osGetModuleHandleW ((HMODULE(WINAPI*)(LPCWSTR))aSyscall[82].pCurrent)

#ifndef _WIN32
  { "getenv",                   (SYSCALL)getenv,                 0 },
#else
  { "getenv",                   (SYSCALL)0,                      0 },
#endif

#define osGetenv ((const char *(*)(const char *))aSyscall[83].pCurrent)

#ifndef _WIN32
  { "getcwd",                   (SYSCALL)getcwd,                 0 },
#else
  { "getcwd",                   (SYSCALL)0,                      0 },
#endif

#define osGetcwd ((char*(*)(char*,size_t))aSyscall[84].pCurrent)

#ifndef _WIN32
  { "readlink",                 (SYSCALL)readlink,               0 },
#else
  { "readlink",                 (SYSCALL)0,                      0 },
#endif

#define osReadlink ((ssize_t(*)(const char*,char*,size_t))aSyscall[85].pCurrent)

#ifndef _WIN32
  { "lstat",                    (SYSCALL)lstat,                  0 },
#else
  { "lstat",                    (SYSCALL)0,                      0 },
#endif

#define osLstat ((int(*)(const char*,struct stat*))aSyscall[86].pCurrent)

#ifndef _WIN32
  { "__errno",                  (SYSCALL)__errno,                0 },
#else
  { "__errno",                  (SYSCALL)0,                      0 },
#endif

#define osErrno (*((int*(*)(void))aSyscall[87].pCurrent)())

#ifndef _WIN32
  { "cygwin_conv_path",         (SYSCALL)cygwin_conv_path,       0 },
#else
  { "cygwin_conv_path",         (SYSCALL)0,                      0 },
#endif

#define osCygwin_conv_path ((size_t(*)(unsigned int, \
    const void *, void *, size_t))aSyscall[88].pCurrent)

}; /* End of the overrideable system calls */

/*
** This is the xSetSystemCall() method of sqlite3_vfs for all of the
** "win32" VFSes.  Return SQLITE_OK upon successfully updating the
** system call pointer, or SQLITE_NOTFOUND if there is no configurable
** system call named zName.
48487
48488
48489
48490
48491
48492
48493

48494
48495
48496
48497
48498
48499
48500
  }
  sqlite3_mutex_leave(pMem);
  sqlite3_mutex_leave(pMainMtx);
  return rc;
}
#endif /* SQLITE_WIN32_MALLOC */


/*
** This function outputs the specified (ANSI) string to the Win32 debugger
** (if available).
*/

SQLITE_API void sqlite3_win32_write_debug(const char *zBuf, int nBuf){
  char zDbgBuf[SQLITE_WIN32_DBG_BUF_SIZE];







>







48548
48549
48550
48551
48552
48553
48554
48555
48556
48557
48558
48559
48560
48561
48562
  }
  sqlite3_mutex_leave(pMem);
  sqlite3_mutex_leave(pMainMtx);
  return rc;
}
#endif /* SQLITE_WIN32_MALLOC */

#ifdef _WIN32
/*
** This function outputs the specified (ANSI) string to the Win32 debugger
** (if available).
*/

SQLITE_API void sqlite3_win32_write_debug(const char *zBuf, int nBuf){
  char zDbgBuf[SQLITE_WIN32_DBG_BUF_SIZE];
48529
48530
48531
48532
48533
48534
48535

48536
48537
48538
48539
48540
48541
48542
    memcpy(zDbgBuf, zBuf, nMin);
    fprintf(stderr, "%s", zDbgBuf);
  }else{
    fprintf(stderr, "%s", zBuf);
  }
#endif
}


/*
** The following routine suspends the current thread for at least ms
** milliseconds.  This is equivalent to the Win32 Sleep() interface.
*/
#if SQLITE_OS_WINRT
static HANDLE sleepObj = NULL;







>







48591
48592
48593
48594
48595
48596
48597
48598
48599
48600
48601
48602
48603
48604
48605
    memcpy(zDbgBuf, zBuf, nMin);
    fprintf(stderr, "%s", zDbgBuf);
  }else{
    fprintf(stderr, "%s", zBuf);
  }
#endif
}
#endif /* _WIN32 */

/*
** The following routine suspends the current thread for at least ms
** milliseconds.  This is equivalent to the Win32 Sleep() interface.
*/
#if SQLITE_OS_WINRT
static HANDLE sleepObj = NULL;
48829
48830
48831
48832
48833
48834
48835

48836
48837
48838
48839
48840
48841
48842
}

SQLITE_PRIVATE void sqlite3MemSetDefault(void){
  sqlite3_config(SQLITE_CONFIG_MALLOC, sqlite3MemGetWin32());
}
#endif /* SQLITE_WIN32_MALLOC */


/*
** Convert a UTF-8 string to Microsoft Unicode.
**
** Space to hold the returned string is obtained from sqlite3_malloc().
*/
static LPWSTR winUtf8ToUnicode(const char *zText){
  int nChar;







>







48892
48893
48894
48895
48896
48897
48898
48899
48900
48901
48902
48903
48904
48905
48906
}

SQLITE_PRIVATE void sqlite3MemSetDefault(void){
  sqlite3_config(SQLITE_CONFIG_MALLOC, sqlite3MemGetWin32());
}
#endif /* SQLITE_WIN32_MALLOC */

#ifdef _WIN32
/*
** Convert a UTF-8 string to Microsoft Unicode.
**
** Space to hold the returned string is obtained from sqlite3_malloc().
*/
static LPWSTR winUtf8ToUnicode(const char *zText){
  int nChar;
48854
48855
48856
48857
48858
48859
48860

48861
48862
48863
48864
48865
48866
48867
                                nChar);
  if( nChar==0 ){
    sqlite3_free(zWideText);
    zWideText = 0;
  }
  return zWideText;
}


/*
** Convert a Microsoft Unicode string to UTF-8.
**
** Space to hold the returned string is obtained from sqlite3_malloc().
*/
static char *winUnicodeToUtf8(LPCWSTR zWideText){







>







48918
48919
48920
48921
48922
48923
48924
48925
48926
48927
48928
48929
48930
48931
48932
                                nChar);
  if( nChar==0 ){
    sqlite3_free(zWideText);
    zWideText = 0;
  }
  return zWideText;
}
#endif /* _WIN32 */

/*
** Convert a Microsoft Unicode string to UTF-8.
**
** Space to hold the returned string is obtained from sqlite3_malloc().
*/
static char *winUnicodeToUtf8(LPCWSTR zWideText){
48888
48889
48890
48891
48892
48893
48894
48895
48896
48897
48898
48899
48900
48901
48902
48903
48904
48905
48906
48907
48908
48909
48910
48911
48912
48913
48914
48915
48916

48917
48918
48919
48920
48921
48922
48923
/*
** Convert an ANSI string to Microsoft Unicode, using the ANSI or OEM
** code page.
**
** Space to hold the returned string is obtained from sqlite3_malloc().
*/
static LPWSTR winMbcsToUnicode(const char *zText, int useAnsi){
  int nByte;
  LPWSTR zMbcsText;
  int codepage = useAnsi ? CP_ACP : CP_OEMCP;

  nByte = osMultiByteToWideChar(codepage, 0, zText, -1, NULL,
                                0)*sizeof(WCHAR);
  if( nByte==0 ){
    return 0;
  }
  zMbcsText = sqlite3MallocZero( nByte*sizeof(WCHAR) );
  if( zMbcsText==0 ){
    return 0;
  }
  nByte = osMultiByteToWideChar(codepage, 0, zText, -1, zMbcsText,
                                nByte);
  if( nByte==0 ){
    sqlite3_free(zMbcsText);
    zMbcsText = 0;
  }
  return zMbcsText;
}


/*
** Convert a Microsoft Unicode string to a multi-byte character string,
** using the ANSI or OEM code page.
**
** Space to hold the returned string is obtained from sqlite3_malloc().
*/
static char *winUnicodeToMbcs(LPCWSTR zWideText, int useAnsi){







|



|
|
|


|



|
|
|






>







48953
48954
48955
48956
48957
48958
48959
48960
48961
48962
48963
48964
48965
48966
48967
48968
48969
48970
48971
48972
48973
48974
48975
48976
48977
48978
48979
48980
48981
48982
48983
48984
48985
48986
48987
48988
48989
/*
** Convert an ANSI string to Microsoft Unicode, using the ANSI or OEM
** code page.
**
** Space to hold the returned string is obtained from sqlite3_malloc().
*/
static LPWSTR winMbcsToUnicode(const char *zText, int useAnsi){
  int nWideChar;
  LPWSTR zMbcsText;
  int codepage = useAnsi ? CP_ACP : CP_OEMCP;

  nWideChar = osMultiByteToWideChar(codepage, 0, zText, -1, NULL,
                                0);
  if( nWideChar==0 ){
    return 0;
  }
  zMbcsText = sqlite3MallocZero( nWideChar*sizeof(WCHAR) );
  if( zMbcsText==0 ){
    return 0;
  }
  nWideChar = osMultiByteToWideChar(codepage, 0, zText, -1, zMbcsText,
                                nWideChar);
  if( nWideChar==0 ){
    sqlite3_free(zMbcsText);
    zMbcsText = 0;
  }
  return zMbcsText;
}

#ifdef _WIN32
/*
** Convert a Microsoft Unicode string to a multi-byte character string,
** using the ANSI or OEM code page.
**
** Space to hold the returned string is obtained from sqlite3_malloc().
*/
static char *winUnicodeToMbcs(LPCWSTR zWideText, int useAnsi){
48937
48938
48939
48940
48941
48942
48943

48944
48945
48946
48947
48948
48949
48950
48951
48952
48953
48954
48955
48956
48957
48958
48959
48960
48961
48962

48963
48964
48965
48966
48967
48968
48969
                                nByte, 0, 0);
  if( nByte == 0 ){
    sqlite3_free(zText);
    zText = 0;
  }
  return zText;
}


/*
** Convert a multi-byte character string to UTF-8.
**
** Space to hold the returned string is obtained from sqlite3_malloc().
*/
static char *winMbcsToUtf8(const char *zText, int useAnsi){
  char *zTextUtf8;
  LPWSTR zTmpWide;

  zTmpWide = winMbcsToUnicode(zText, useAnsi);
  if( zTmpWide==0 ){
    return 0;
  }
  zTextUtf8 = winUnicodeToUtf8(zTmpWide);
  sqlite3_free(zTmpWide);
  return zTextUtf8;
}


/*
** Convert a UTF-8 string to a multi-byte character string.
**
** Space to hold the returned string is obtained from sqlite3_malloc().
*/
static char *winUtf8ToMbcs(const char *zText, int useAnsi){
  char *zTextMbcs;







>



















>







49003
49004
49005
49006
49007
49008
49009
49010
49011
49012
49013
49014
49015
49016
49017
49018
49019
49020
49021
49022
49023
49024
49025
49026
49027
49028
49029
49030
49031
49032
49033
49034
49035
49036
49037
                                nByte, 0, 0);
  if( nByte == 0 ){
    sqlite3_free(zText);
    zText = 0;
  }
  return zText;
}
#endif /* _WIN32 */

/*
** Convert a multi-byte character string to UTF-8.
**
** Space to hold the returned string is obtained from sqlite3_malloc().
*/
static char *winMbcsToUtf8(const char *zText, int useAnsi){
  char *zTextUtf8;
  LPWSTR zTmpWide;

  zTmpWide = winMbcsToUnicode(zText, useAnsi);
  if( zTmpWide==0 ){
    return 0;
  }
  zTextUtf8 = winUnicodeToUtf8(zTmpWide);
  sqlite3_free(zTmpWide);
  return zTextUtf8;
}

#ifdef _WIN32
/*
** Convert a UTF-8 string to a multi-byte character string.
**
** Space to hold the returned string is obtained from sqlite3_malloc().
*/
static char *winUtf8ToMbcs(const char *zText, int useAnsi){
  char *zTextMbcs;
49005
49006
49007
49008
49009
49010
49011

49012
49013
49014
49015
49016
49017
49018
49019
49020
49021
49022
49023
49024
49025
49026
49027
49028

49029
49030
49031
49032
49033
49034
49035
  }
#endif
#ifndef SQLITE_OMIT_AUTOINIT
  if( sqlite3_initialize() ) return 0;
#endif
  return winUnicodeToUtf8(zWideText);
}


/*
** This is a public wrapper for the winMbcsToUtf8() function.
*/
SQLITE_API char *sqlite3_win32_mbcs_to_utf8(const char *zText){
#ifdef SQLITE_ENABLE_API_ARMOR
  if( !zText ){
    (void)SQLITE_MISUSE_BKPT;
    return 0;
  }
#endif
#ifndef SQLITE_OMIT_AUTOINIT
  if( sqlite3_initialize() ) return 0;
#endif
  return winMbcsToUtf8(zText, osAreFileApisANSI());
}


/*
** This is a public wrapper for the winMbcsToUtf8() function.
*/
SQLITE_API char *sqlite3_win32_mbcs_to_utf8_v2(const char *zText, int useAnsi){
#ifdef SQLITE_ENABLE_API_ARMOR
  if( !zText ){
    (void)SQLITE_MISUSE_BKPT;







>

















>







49073
49074
49075
49076
49077
49078
49079
49080
49081
49082
49083
49084
49085
49086
49087
49088
49089
49090
49091
49092
49093
49094
49095
49096
49097
49098
49099
49100
49101
49102
49103
49104
49105
  }
#endif
#ifndef SQLITE_OMIT_AUTOINIT
  if( sqlite3_initialize() ) return 0;
#endif
  return winUnicodeToUtf8(zWideText);
}
#endif /* _WIN32 */

/*
** This is a public wrapper for the winMbcsToUtf8() function.
*/
SQLITE_API char *sqlite3_win32_mbcs_to_utf8(const char *zText){
#ifdef SQLITE_ENABLE_API_ARMOR
  if( !zText ){
    (void)SQLITE_MISUSE_BKPT;
    return 0;
  }
#endif
#ifndef SQLITE_OMIT_AUTOINIT
  if( sqlite3_initialize() ) return 0;
#endif
  return winMbcsToUtf8(zText, osAreFileApisANSI());
}

#ifdef _WIN32
/*
** This is a public wrapper for the winMbcsToUtf8() function.
*/
SQLITE_API char *sqlite3_win32_mbcs_to_utf8_v2(const char *zText, int useAnsi){
#ifdef SQLITE_ENABLE_API_ARMOR
  if( !zText ){
    (void)SQLITE_MISUSE_BKPT;
49146
49147
49148
49149
49150
49151
49152

49153
49154
49155
49156
49157
49158
49159
*/
SQLITE_API int sqlite3_win32_set_directory(
  unsigned long type, /* Identifier for directory being set or reset */
  void *zValue        /* New value for directory being set or reset */
){
  return sqlite3_win32_set_directory16(type, zValue);
}


/*
** The return value of winGetLastErrorMsg
** is zero if the error message fits in the buffer, or non-zero
** otherwise (if the message was truncated).
*/
static int winGetLastErrorMsg(DWORD lastErrno, int nBuf, char *zBuf){







>







49216
49217
49218
49219
49220
49221
49222
49223
49224
49225
49226
49227
49228
49229
49230
*/
SQLITE_API int sqlite3_win32_set_directory(
  unsigned long type, /* Identifier for directory being set or reset */
  void *zValue        /* New value for directory being set or reset */
){
  return sqlite3_win32_set_directory16(type, zValue);
}
#endif /* _WIN32 */

/*
** The return value of winGetLastErrorMsg
** is zero if the error message fits in the buffer, or non-zero
** otherwise (if the message was truncated).
*/
static int winGetLastErrorMsg(DWORD lastErrno, int nBuf, char *zBuf){
49694
49695
49696
49697
49698
49699
49700

49701
49702
49703

49704
49705
49706
49707
49708
49709
49710
#else
  if( osIsNT() ){
    OVERLAPPED ovlp;
    memset(&ovlp, 0, sizeof(OVERLAPPED));
    ovlp.Offset = offsetLow;
    ovlp.OffsetHigh = offsetHigh;
    return osLockFileEx(*phFile, flags, 0, numBytesLow, numBytesHigh, &ovlp);

  }else{
    return osLockFile(*phFile, offsetLow, offsetHigh, numBytesLow,
                      numBytesHigh);

  }
#endif
}

/*
** Lock a region of nByte bytes starting at offset offset of file hFile.
** Take an EXCLUSIVE lock if parameter bExclusive is true, or a SHARED lock







>



>







49765
49766
49767
49768
49769
49770
49771
49772
49773
49774
49775
49776
49777
49778
49779
49780
49781
49782
49783
#else
  if( osIsNT() ){
    OVERLAPPED ovlp;
    memset(&ovlp, 0, sizeof(OVERLAPPED));
    ovlp.Offset = offsetLow;
    ovlp.OffsetHigh = offsetHigh;
    return osLockFileEx(*phFile, flags, 0, numBytesLow, numBytesHigh, &ovlp);
#ifdef SQLITE_WIN32_HAS_ANSI
  }else{
    return osLockFile(*phFile, offsetLow, offsetHigh, numBytesLow,
                      numBytesHigh);
#endif
  }
#endif
}

/*
** Lock a region of nByte bytes starting at offset offset of file hFile.
** Take an EXCLUSIVE lock if parameter bExclusive is true, or a SHARED lock
49804
49805
49806
49807
49808
49809
49810

49811
49812
49813

49814
49815
49816
49817
49818
49819
49820
#else
  if( osIsNT() ){
    OVERLAPPED ovlp;
    memset(&ovlp, 0, sizeof(OVERLAPPED));
    ovlp.Offset = offsetLow;
    ovlp.OffsetHigh = offsetHigh;
    return osUnlockFileEx(*phFile, 0, numBytesLow, numBytesHigh, &ovlp);

  }else{
    return osUnlockFile(*phFile, offsetLow, offsetHigh, numBytesLow,
                        numBytesHigh);

  }
#endif
}

/*
** Remove an nByte lock starting at offset iOff from HANDLE h.
*/







>



>







49877
49878
49879
49880
49881
49882
49883
49884
49885
49886
49887
49888
49889
49890
49891
49892
49893
49894
49895
#else
  if( osIsNT() ){
    OVERLAPPED ovlp;
    memset(&ovlp, 0, sizeof(OVERLAPPED));
    ovlp.Offset = offsetLow;
    ovlp.OffsetHigh = offsetHigh;
    return osUnlockFileEx(*phFile, 0, numBytesLow, numBytesHigh, &ovlp);
#ifdef SQLITE_WIN32_HAS_ANSI
  }else{
    return osUnlockFile(*phFile, offsetLow, offsetHigh, numBytesLow,
                        numBytesHigh);
#endif
  }
#endif
}

/*
** Remove an nByte lock starting at offset iOff from HANDLE h.
*/
51220
51221
51222
51223
51224
51225
51226
51227













51228
51229
51230
51231































































51232

51233
51234
51235
51236
51237
51238
51239
51240
51241
}


/*
** Convert a UTF-8 filename into whatever form the underlying
** operating system wants filenames in.  Space to hold the result
** is obtained from malloc and must be freed by the calling
** function.













*/
static void *winConvertFromUtf8Filename(const char *zFilename){
  void *zConverted = 0;
  if( osIsNT() ){































































    zConverted = winUtf8ToUnicode(zFilename);

  }
#ifdef SQLITE_WIN32_HAS_ANSI
  else{
    zConverted = winUtf8ToMbcs(zFilename, osAreFileApisANSI());
  }
#endif
  /* caller will handle out of memory */
  return zConverted;
}







|
>
>
>
>
>
>
>
>
>
>
>
>
>




>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>

>

|







51295
51296
51297
51298
51299
51300
51301
51302
51303
51304
51305
51306
51307
51308
51309
51310
51311
51312
51313
51314
51315
51316
51317
51318
51319
51320
51321
51322
51323
51324
51325
51326
51327
51328
51329
51330
51331
51332
51333
51334
51335
51336
51337
51338
51339
51340
51341
51342
51343
51344
51345
51346
51347
51348
51349
51350
51351
51352
51353
51354
51355
51356
51357
51358
51359
51360
51361
51362
51363
51364
51365
51366
51367
51368
51369
51370
51371
51372
51373
51374
51375
51376
51377
51378
51379
51380
51381
51382
51383
51384
51385
51386
51387
51388
51389
51390
51391
51392
51393
}


/*
** Convert a UTF-8 filename into whatever form the underlying
** operating system wants filenames in.  Space to hold the result
** is obtained from malloc and must be freed by the calling
** function
**
** On Cygwin, 3 possible input forms are accepted:
** - If the filename starts with "<drive>:/" or "<drive>:\",
**   it is converted to UTF-16 as-is.
** - If the filename contains '/', it is assumed to be a
**   Cygwin absolute path, it is converted to a win32
**   absolute path in UTF-16.
** - Otherwise it must be a filename only, the win32 filename
**   is returned in UTF-16.
** Note: If the function cygwin_conv_path() fails, only
**   UTF-8 -> UTF-16 conversion will be done. This can only
**   happen when the file path >32k, in which case winUtf8ToUnicode()
**   will fail too.
*/
static void *winConvertFromUtf8Filename(const char *zFilename){
  void *zConverted = 0;
  if( osIsNT() ){
#ifdef __CYGWIN__
    int nChar;
    LPWSTR zWideFilename;

    if( osCygwin_conv_path && !(winIsDriveLetterAndColon(zFilename)
        && winIsDirSep(zFilename[2])) ){
      int nByte;
      int convertflag = CCP_POSIX_TO_WIN_W;
      if( !strchr(zFilename, '/') ) convertflag |= CCP_RELATIVE;
      nByte = (int)osCygwin_conv_path(convertflag,
          zFilename, 0, 0);
      if( nByte>0 ){
        zConverted = sqlite3MallocZero(nByte+12);
        if ( zConverted==0 ){
          return zConverted;
        }
        zWideFilename = zConverted;
        /* Filenames should be prefixed, except when converted
         * full path already starts with "\\?\". */
        if( osCygwin_conv_path(convertflag, zFilename,
                             zWideFilename+4, nByte)==0 ){
          if( (convertflag&CCP_RELATIVE) ){
            memmove(zWideFilename, zWideFilename+4, nByte);
          }else if( memcmp(zWideFilename+4, L"\\\\", 4) ){
            memcpy(zWideFilename, L"\\\\?\\", 8);
          }else if( zWideFilename[6]!='?' ){
            memmove(zWideFilename+6, zWideFilename+4, nByte);
            memcpy(zWideFilename, L"\\\\?\\UNC", 14);
          }else{
            memmove(zWideFilename, zWideFilename+4, nByte);
          }
          return zConverted;
        }
        sqlite3_free(zConverted);
      }
    }
    nChar = osMultiByteToWideChar(CP_UTF8, 0, zFilename, -1, NULL, 0);
    if( nChar==0 ){
      return 0;
    }
    zWideFilename = sqlite3MallocZero( nChar*sizeof(WCHAR)+12 );
    if( zWideFilename==0 ){
      return 0;
    }
    nChar = osMultiByteToWideChar(CP_UTF8, 0, zFilename, -1,
                                  zWideFilename, nChar);
    if( nChar==0 ){
      sqlite3_free(zWideFilename);
      zWideFilename = 0;
    }else if( nChar>MAX_PATH
        && winIsDriveLetterAndColon(zFilename)
        && winIsDirSep(zFilename[2]) ){
      memmove(zWideFilename+4, zWideFilename, nChar*sizeof(WCHAR));
      zWideFilename[2] = '\\';
      memcpy(zWideFilename, L"\\\\?\\", 8);
    }else if( nChar>MAX_PATH
        && winIsDirSep(zFilename[0]) && winIsDirSep(zFilename[1])
        && zFilename[2] != '?' ){
      memmove(zWideFilename+6, zWideFilename, nChar*sizeof(WCHAR));
      memcpy(zWideFilename, L"\\\\?\\UNC", 14);
    }
    zConverted = zWideFilename;
#else
    zConverted = winUtf8ToUnicode(zFilename);
#endif /* __CYGWIN__ */
  }
#if defined(SQLITE_WIN32_HAS_ANSI) && defined(_WIN32)
  else{
    zConverted = winUtf8ToMbcs(zFilename, osAreFileApisANSI());
  }
#endif
  /* caller will handle out of memory */
  return zConverted;
}
52056
52057
52058
52059
52060
52061
52062
52063
52064
52065
52066
52067
52068
52069
52070
/****************************************************************************
**************************** sqlite3_vfs methods ****************************
**
** This division contains the implementation of methods on the
** sqlite3_vfs object.
*/

#if defined(__CYGWIN__)
/*
** Convert a filename from whatever the underlying operating system
** supports for filenames into UTF-8.  Space to hold the result is
** obtained from malloc and must be freed by the calling function.
*/
static char *winConvertToUtf8Filename(const void *zFilename){
  char *zConverted = 0;







|







52208
52209
52210
52211
52212
52213
52214
52215
52216
52217
52218
52219
52220
52221
52222
/****************************************************************************
**************************** sqlite3_vfs methods ****************************
**
** This division contains the implementation of methods on the
** sqlite3_vfs object.
*/

#if 0 /* No longer necessary */
/*
** Convert a filename from whatever the underlying operating system
** supports for filenames into UTF-8.  Space to hold the result is
** obtained from malloc and must be freed by the calling function.
*/
static char *winConvertToUtf8Filename(const void *zFilename){
  char *zConverted = 0;
52089
52090
52091
52092
52093
52094
52095

52096






52097
52098
52099
52100
52101
52102
52103
static int winMakeEndInDirSep(int nBuf, char *zBuf){
  if( zBuf ){
    int nLen = sqlite3Strlen30(zBuf);
    if( nLen>0 ){
      if( winIsDirSep(zBuf[nLen-1]) ){
        return 1;
      }else if( nLen+1<nBuf ){

        zBuf[nLen] = winGetDirSep();






        zBuf[nLen+1] = '\0';
        return 1;
      }
    }
  }
  return 0;
}







>
|
>
>
>
>
>
>







52241
52242
52243
52244
52245
52246
52247
52248
52249
52250
52251
52252
52253
52254
52255
52256
52257
52258
52259
52260
52261
52262
static int winMakeEndInDirSep(int nBuf, char *zBuf){
  if( zBuf ){
    int nLen = sqlite3Strlen30(zBuf);
    if( nLen>0 ){
      if( winIsDirSep(zBuf[nLen-1]) ){
        return 1;
      }else if( nLen+1<nBuf ){
        if( !osGetenv ){
          zBuf[nLen] = winGetDirSep();
        }else if( winIsDriveLetterAndColon(zBuf) && winIsDirSep(zBuf[2]) ){
          zBuf[nLen] = '\\';
          zBuf[2]='\\';
        }else{
          zBuf[nLen] = '/';
        }
        zBuf[nLen+1] = '\0';
        return 1;
      }
    }
  }
  return 0;
}
52116
52117
52118
52119
52120
52121
52122
52123
52124
52125
52126
52127
52128
52129
52130
}

/*
** Create a temporary file name and store the resulting pointer into pzBuf.
** The pointer returned in pzBuf must be freed via sqlite3_free().
*/
static int winGetTempname(sqlite3_vfs *pVfs, char **pzBuf){
  static char zChars[] =
    "abcdefghijklmnopqrstuvwxyz"
    "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
    "0123456789";
  size_t i, j;
  DWORD pid;
  int nPre = sqlite3Strlen30(SQLITE_TEMP_FILE_PREFIX);
  i64 nMax, nBuf, nDir, nLen;







|







52275
52276
52277
52278
52279
52280
52281
52282
52283
52284
52285
52286
52287
52288
52289
}

/*
** Create a temporary file name and store the resulting pointer into pzBuf.
** The pointer returned in pzBuf must be freed via sqlite3_free().
*/
static int winGetTempname(sqlite3_vfs *pVfs, char **pzBuf){
  static const char zChars[] =
    "abcdefghijklmnopqrstuvwxyz"
    "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
    "0123456789";
  size_t i, j;
  DWORD pid;
  int nPre = sqlite3Strlen30(SQLITE_TEMP_FILE_PREFIX);
  i64 nMax, nBuf, nDir, nLen;
52167
52168
52169
52170
52171
52172
52173
52174
52175
52176
52177
52178
52179
52180
52181
52182
52183
52184
52185
52186
52187
52188
52189
52190
52191
52192
52193
52194
52195
52196
52197
52198
52199
52200
52201
52202
52203

52204
52205
52206
52207
52208
52209
52210
52211
52212
52213
52214
52215

52216
52217
52218
52219
52220
52221
52222
52223
52224
52225
52226
52227
52228
52229
52230
52231
      }
      sqlite3_snprintf(nMax, zBuf, "%s", sqlite3_temp_directory);
    }
    sqlite3_mutex_leave(sqlite3MutexAlloc(SQLITE_MUTEX_STATIC_TEMPDIR));
  }

#if defined(__CYGWIN__)
  else{
    static const char *azDirs[] = {
       0, /* getenv("SQLITE_TMPDIR") */
       0, /* getenv("TMPDIR") */
       0, /* getenv("TMP") */
       0, /* getenv("TEMP") */
       0, /* getenv("USERPROFILE") */
       "/var/tmp",
       "/usr/tmp",
       "/tmp",
       ".",
       0        /* List terminator */
    };
    unsigned int i;
    const char *zDir = 0;

    if( !azDirs[0] ) azDirs[0] = getenv("SQLITE_TMPDIR");
    if( !azDirs[1] ) azDirs[1] = getenv("TMPDIR");
    if( !azDirs[2] ) azDirs[2] = getenv("TMP");
    if( !azDirs[3] ) azDirs[3] = getenv("TEMP");
    if( !azDirs[4] ) azDirs[4] = getenv("USERPROFILE");
    for(i=0; i<sizeof(azDirs)/sizeof(azDirs[0]); zDir=azDirs[i++]){
      void *zConverted;
      if( zDir==0 ) continue;
      /* If the path starts with a drive letter followed by the colon
      ** character, assume it is already a native Win32 path; otherwise,
      ** it must be converted to a native Win32 path via the Cygwin API
      ** prior to using it.
      */
      if( winIsDriveLetterAndColon(zDir) ){

        zConverted = winConvertFromUtf8Filename(zDir);
        if( !zConverted ){
          sqlite3_free(zBuf);
          OSTRACE(("TEMP-FILENAME rc=SQLITE_IOERR_NOMEM\n"));
          return SQLITE_IOERR_NOMEM_BKPT;
        }
        if( winIsDir(zConverted) ){
          sqlite3_snprintf(nMax, zBuf, "%s", zDir);
          sqlite3_free(zConverted);
          break;
        }
        sqlite3_free(zConverted);

      }else{
        zConverted = sqlite3MallocZero( nMax+1 );
        if( !zConverted ){
          sqlite3_free(zBuf);
          OSTRACE(("TEMP-FILENAME rc=SQLITE_IOERR_NOMEM\n"));
          return SQLITE_IOERR_NOMEM_BKPT;
        }
        if( cygwin_conv_path(
                osIsNT() ? CCP_POSIX_TO_WIN_W : CCP_POSIX_TO_WIN_A, zDir,
                zConverted, nMax+1)<0 ){
          sqlite3_free(zConverted);
          sqlite3_free(zBuf);
          OSTRACE(("TEMP-FILENAME rc=SQLITE_IOERR_CONVPATH\n"));
          return winLogError(SQLITE_IOERR_CONVPATH, (DWORD)errno,
                             "winGetTempname2", zDir);
        }







|















|
|
|
|
|








<
>












>







|
|







52326
52327
52328
52329
52330
52331
52332
52333
52334
52335
52336
52337
52338
52339
52340
52341
52342
52343
52344
52345
52346
52347
52348
52349
52350
52351
52352
52353
52354
52355
52356
52357
52358
52359
52360
52361

52362
52363
52364
52365
52366
52367
52368
52369
52370
52371
52372
52373
52374
52375
52376
52377
52378
52379
52380
52381
52382
52383
52384
52385
52386
52387
52388
52389
52390
52391
      }
      sqlite3_snprintf(nMax, zBuf, "%s", sqlite3_temp_directory);
    }
    sqlite3_mutex_leave(sqlite3MutexAlloc(SQLITE_MUTEX_STATIC_TEMPDIR));
  }

#if defined(__CYGWIN__)
  else if( osGetenv!=NULL ){
    static const char *azDirs[] = {
       0, /* getenv("SQLITE_TMPDIR") */
       0, /* getenv("TMPDIR") */
       0, /* getenv("TMP") */
       0, /* getenv("TEMP") */
       0, /* getenv("USERPROFILE") */
       "/var/tmp",
       "/usr/tmp",
       "/tmp",
       ".",
       0        /* List terminator */
    };
    unsigned int i;
    const char *zDir = 0;

    if( !azDirs[0] ) azDirs[0] = osGetenv("SQLITE_TMPDIR");
    if( !azDirs[1] ) azDirs[1] = osGetenv("TMPDIR");
    if( !azDirs[2] ) azDirs[2] = osGetenv("TMP");
    if( !azDirs[3] ) azDirs[3] = osGetenv("TEMP");
    if( !azDirs[4] ) azDirs[4] = osGetenv("USERPROFILE");
    for(i=0; i<sizeof(azDirs)/sizeof(azDirs[0]); zDir=azDirs[i++]){
      void *zConverted;
      if( zDir==0 ) continue;
      /* If the path starts with a drive letter followed by the colon
      ** character, assume it is already a native Win32 path; otherwise,
      ** it must be converted to a native Win32 path via the Cygwin API
      ** prior to using it.
      */

      {
        zConverted = winConvertFromUtf8Filename(zDir);
        if( !zConverted ){
          sqlite3_free(zBuf);
          OSTRACE(("TEMP-FILENAME rc=SQLITE_IOERR_NOMEM\n"));
          return SQLITE_IOERR_NOMEM_BKPT;
        }
        if( winIsDir(zConverted) ){
          sqlite3_snprintf(nMax, zBuf, "%s", zDir);
          sqlite3_free(zConverted);
          break;
        }
        sqlite3_free(zConverted);
#if 0 /* No longer necessary */
      }else{
        zConverted = sqlite3MallocZero( nMax+1 );
        if( !zConverted ){
          sqlite3_free(zBuf);
          OSTRACE(("TEMP-FILENAME rc=SQLITE_IOERR_NOMEM\n"));
          return SQLITE_IOERR_NOMEM_BKPT;
        }
        if( osCygwin_conv_path(
                CCP_POSIX_TO_WIN_W, zDir,
                zConverted, nMax+1)<0 ){
          sqlite3_free(zConverted);
          sqlite3_free(zBuf);
          OSTRACE(("TEMP-FILENAME rc=SQLITE_IOERR_CONVPATH\n"));
          return winLogError(SQLITE_IOERR_CONVPATH, (DWORD)errno,
                             "winGetTempname2", zDir);
        }
52243
52244
52245
52246
52247
52248
52249

52250
52251
52252


52253
52254
52255
52256
52257
52258
52259
52260
          }
          sqlite3_snprintf(nMax, zBuf, "%s", zUtf8);
          sqlite3_free(zUtf8);
          sqlite3_free(zConverted);
          break;
        }
        sqlite3_free(zConverted);

      }
    }
  }


#elif !SQLITE_OS_WINRT && !defined(__CYGWIN__)
  else if( osIsNT() ){
    char *zMulti;
    LPWSTR zWidePath = sqlite3MallocZero( nMax*sizeof(WCHAR) );
    if( !zWidePath ){
      sqlite3_free(zBuf);
      OSTRACE(("TEMP-FILENAME rc=SQLITE_IOERR_NOMEM\n"));
      return SQLITE_IOERR_NOMEM_BKPT;







>



>
>
|







52403
52404
52405
52406
52407
52408
52409
52410
52411
52412
52413
52414
52415
52416
52417
52418
52419
52420
52421
52422
52423
          }
          sqlite3_snprintf(nMax, zBuf, "%s", zUtf8);
          sqlite3_free(zUtf8);
          sqlite3_free(zConverted);
          break;
        }
        sqlite3_free(zConverted);
#endif /* No longer necessary */
      }
    }
  }
#endif

#if !SQLITE_OS_WINRT && defined(_WIN32)
  else if( osIsNT() ){
    char *zMulti;
    LPWSTR zWidePath = sqlite3MallocZero( nMax*sizeof(WCHAR) );
    if( !zWidePath ){
      sqlite3_free(zBuf);
      OSTRACE(("TEMP-FILENAME rc=SQLITE_IOERR_NOMEM\n"));
      return SQLITE_IOERR_NOMEM_BKPT;
52370
52371
52372
52373
52374
52375
52376
52377
52378
52379
52380
52381
52382
52383
52384
52385
52386
52387
52388
52389
52390
52391
52392






52393
52394
52395
52396
52397
52398
52399
    while( !(rc = osGetFileAttributesExW((LPCWSTR)zConverted,
                             GetFileExInfoStandard,
                             &sAttrData)) && winRetryIoerr(&cnt, &lastErrno) ){}
    if( !rc ){
      return 0; /* Invalid name? */
    }
    attr = sAttrData.dwFileAttributes;
#if SQLITE_OS_WINCE==0
  }else{
    attr = osGetFileAttributesA((char*)zConverted);
#endif
  }
  return (attr!=INVALID_FILE_ATTRIBUTES) && (attr&FILE_ATTRIBUTE_DIRECTORY);
}

/* forward reference */
static int winAccess(
  sqlite3_vfs *pVfs,         /* Not used on win32 */
  const char *zFilename,     /* Name of file to check */
  int flags,                 /* Type of test to make on this file */
  int *pResOut               /* OUT: Result */
);







/*
** Open a file.
*/
static int winOpen(
  sqlite3_vfs *pVfs,        /* Used to get maximum path length and AppData */
  const char *zName,        /* Name of the file (UTF-8) */
  sqlite3_file *id,         /* Write the SQLite file handle here */







|















>
>
>
>
>
>







52533
52534
52535
52536
52537
52538
52539
52540
52541
52542
52543
52544
52545
52546
52547
52548
52549
52550
52551
52552
52553
52554
52555
52556
52557
52558
52559
52560
52561
52562
52563
52564
52565
52566
52567
52568
    while( !(rc = osGetFileAttributesExW((LPCWSTR)zConverted,
                             GetFileExInfoStandard,
                             &sAttrData)) && winRetryIoerr(&cnt, &lastErrno) ){}
    if( !rc ){
      return 0; /* Invalid name? */
    }
    attr = sAttrData.dwFileAttributes;
#if SQLITE_OS_WINCE==0 && defined(SQLITE_WIN32_HAS_ANSI)
  }else{
    attr = osGetFileAttributesA((char*)zConverted);
#endif
  }
  return (attr!=INVALID_FILE_ATTRIBUTES) && (attr&FILE_ATTRIBUTE_DIRECTORY);
}

/* forward reference */
static int winAccess(
  sqlite3_vfs *pVfs,         /* Not used on win32 */
  const char *zFilename,     /* Name of file to check */
  int flags,                 /* Type of test to make on this file */
  int *pResOut               /* OUT: Result */
);

/*
** The Windows version of xAccess() accepts an extra bit in the flags
** parameter that prevents an anti-virus retry loop.
*/
#define NORETRY 0x4000

/*
** Open a file.
*/
static int winOpen(
  sqlite3_vfs *pVfs,        /* Used to get maximum path length and AppData */
  const char *zName,        /* Name of the file (UTF-8) */
  sqlite3_file *id,         /* Write the SQLite file handle here */
52410
52411
52412
52413
52414
52415
52416

52417
52418
52419
52420
52421
52422
52423
  int isTemp = 0;
#endif
  winVfsAppData *pAppData;
  winFile *pFile = (winFile*)id;
  void *zConverted;              /* Filename in OS encoding */
  const char *zUtf8Name = zName; /* Filename in UTF-8 encoding */
  int cnt = 0;


  /* If argument zPath is a NULL pointer, this function is required to open
  ** a temporary file. Use this buffer to store the file name in.
  */
  char *zTmpname = 0; /* For temporary filename, if necessary. */

  int rc = SQLITE_OK;            /* Function Return Code */







>







52579
52580
52581
52582
52583
52584
52585
52586
52587
52588
52589
52590
52591
52592
52593
  int isTemp = 0;
#endif
  winVfsAppData *pAppData;
  winFile *pFile = (winFile*)id;
  void *zConverted;              /* Filename in OS encoding */
  const char *zUtf8Name = zName; /* Filename in UTF-8 encoding */
  int cnt = 0;
  int isRO = 0;              /* file is known to be accessible readonly */

  /* If argument zPath is a NULL pointer, this function is required to open
  ** a temporary file. Use this buffer to store the file name in.
  */
  char *zTmpname = 0; /* For temporary filename, if necessary. */

  int rc = SQLITE_OK;            /* Function Return Code */
52574
52575
52576
52577
52578
52579
52580
52581
52582
52583
52584
52585
52586
52587
52588
52589
52590
52591
52592
52593
52594
52595
52596
52597
52598
52599
52600
52601
52602
52603
52604
52605
52606
52607
52608
52609
52610
52611
52612
52613
52614
52615
52616
52617
52618
52619
52620
52621
52622
52623
52624
52625
52626
52627
52628
52629
52630
52631
52632
52633
52634
52635
52636
52637
52638
52639
52640
52641
52642
      h = osCreateFile2((LPCWSTR)zConverted,
                        dwDesiredAccess,
                        dwShareMode,
                        dwCreationDisposition,
                        &extendedParameters);
      if( h!=INVALID_HANDLE_VALUE ) break;
      if( isReadWrite ){
        int rc2, isRO = 0;
        sqlite3BeginBenignMalloc();
        rc2 = winAccess(pVfs, zUtf8Name, SQLITE_ACCESS_READ, &isRO);
        sqlite3EndBenignMalloc();
        if( rc2==SQLITE_OK && isRO ) break;
      }
    }while( winRetryIoerr(&cnt, &lastErrno) );
#else
    do{
      h = osCreateFileW((LPCWSTR)zConverted,
                        dwDesiredAccess,
                        dwShareMode, NULL,
                        dwCreationDisposition,
                        dwFlagsAndAttributes,
                        NULL);
      if( h!=INVALID_HANDLE_VALUE ) break;
      if( isReadWrite ){
        int rc2, isRO = 0;
        sqlite3BeginBenignMalloc();
        rc2 = winAccess(pVfs, zUtf8Name, SQLITE_ACCESS_READ, &isRO);
        sqlite3EndBenignMalloc();
        if( rc2==SQLITE_OK && isRO ) break;
      }
    }while( winRetryIoerr(&cnt, &lastErrno) );
#endif
  }
#ifdef SQLITE_WIN32_HAS_ANSI
  else{
    do{
      h = osCreateFileA((LPCSTR)zConverted,
                        dwDesiredAccess,
                        dwShareMode, NULL,
                        dwCreationDisposition,
                        dwFlagsAndAttributes,
                        NULL);
      if( h!=INVALID_HANDLE_VALUE ) break;
      if( isReadWrite ){
        int rc2, isRO = 0;
        sqlite3BeginBenignMalloc();
        rc2 = winAccess(pVfs, zUtf8Name, SQLITE_ACCESS_READ, &isRO);
        sqlite3EndBenignMalloc();
        if( rc2==SQLITE_OK && isRO ) break;
      }
    }while( winRetryIoerr(&cnt, &lastErrno) );
  }
#endif
  winLogIoerr(cnt, __LINE__);

  OSTRACE(("OPEN file=%p, name=%s, access=%lx, rc=%s\n", h, zUtf8Name,
           dwDesiredAccess, (h==INVALID_HANDLE_VALUE) ? "failed" : "ok"));

  if( h==INVALID_HANDLE_VALUE ){
    sqlite3_free(zConverted);
    sqlite3_free(zTmpname);
    if( isReadWrite && !isExclusive ){
      return winOpen(pVfs, zName, id,
         ((flags|SQLITE_OPEN_READONLY) &
                     ~(SQLITE_OPEN_CREATE|SQLITE_OPEN_READWRITE)),
         pOutFlags);
    }else{
      pFile->lastErrno = lastErrno;
      winLogError(SQLITE_CANTOPEN, pFile->lastErrno, "winOpen", zUtf8Name);







|

|














|

|

















|

|














|







52744
52745
52746
52747
52748
52749
52750
52751
52752
52753
52754
52755
52756
52757
52758
52759
52760
52761
52762
52763
52764
52765
52766
52767
52768
52769
52770
52771
52772
52773
52774
52775
52776
52777
52778
52779
52780
52781
52782
52783
52784
52785
52786
52787
52788
52789
52790
52791
52792
52793
52794
52795
52796
52797
52798
52799
52800
52801
52802
52803
52804
52805
52806
52807
52808
52809
52810
52811
52812
      h = osCreateFile2((LPCWSTR)zConverted,
                        dwDesiredAccess,
                        dwShareMode,
                        dwCreationDisposition,
                        &extendedParameters);
      if( h!=INVALID_HANDLE_VALUE ) break;
      if( isReadWrite ){
        int rc2;
        sqlite3BeginBenignMalloc();
        rc2 = winAccess(pVfs, zUtf8Name, SQLITE_ACCESS_READ|NORETRY, &isRO);
        sqlite3EndBenignMalloc();
        if( rc2==SQLITE_OK && isRO ) break;
      }
    }while( winRetryIoerr(&cnt, &lastErrno) );
#else
    do{
      h = osCreateFileW((LPCWSTR)zConverted,
                        dwDesiredAccess,
                        dwShareMode, NULL,
                        dwCreationDisposition,
                        dwFlagsAndAttributes,
                        NULL);
      if( h!=INVALID_HANDLE_VALUE ) break;
      if( isReadWrite ){
        int rc2;
        sqlite3BeginBenignMalloc();
        rc2 = winAccess(pVfs, zUtf8Name, SQLITE_ACCESS_READ|NORETRY, &isRO);
        sqlite3EndBenignMalloc();
        if( rc2==SQLITE_OK && isRO ) break;
      }
    }while( winRetryIoerr(&cnt, &lastErrno) );
#endif
  }
#ifdef SQLITE_WIN32_HAS_ANSI
  else{
    do{
      h = osCreateFileA((LPCSTR)zConverted,
                        dwDesiredAccess,
                        dwShareMode, NULL,
                        dwCreationDisposition,
                        dwFlagsAndAttributes,
                        NULL);
      if( h!=INVALID_HANDLE_VALUE ) break;
      if( isReadWrite ){
        int rc2;
        sqlite3BeginBenignMalloc();
        rc2 = winAccess(pVfs, zUtf8Name, SQLITE_ACCESS_READ|NORETRY, &isRO);
        sqlite3EndBenignMalloc();
        if( rc2==SQLITE_OK && isRO ) break;
      }
    }while( winRetryIoerr(&cnt, &lastErrno) );
  }
#endif
  winLogIoerr(cnt, __LINE__);

  OSTRACE(("OPEN file=%p, name=%s, access=%lx, rc=%s\n", h, zUtf8Name,
           dwDesiredAccess, (h==INVALID_HANDLE_VALUE) ? "failed" : "ok"));

  if( h==INVALID_HANDLE_VALUE ){
    sqlite3_free(zConverted);
    sqlite3_free(zTmpname);
    if( isReadWrite && isRO && !isExclusive ){
      return winOpen(pVfs, zName, id,
         ((flags|SQLITE_OPEN_READONLY) &
                     ~(SQLITE_OPEN_CREATE|SQLITE_OPEN_READWRITE)),
         pOutFlags);
    }else{
      pFile->lastErrno = lastErrno;
      winLogError(SQLITE_CANTOPEN, pFile->lastErrno, "winOpen", zUtf8Name);
52830
52831
52832
52833
52834
52835
52836

52837





52838
52839
52840
52841
52842
52843
52844
  int flags,                 /* Type of test to make on this file */
  int *pResOut               /* OUT: Result */
){
  DWORD attr;
  int rc = 0;
  DWORD lastErrno = 0;
  void *zConverted;

  UNUSED_PARAMETER(pVfs);






  SimulateIOError( return SQLITE_IOERR_ACCESS; );
  OSTRACE(("ACCESS name=%s, flags=%x, pResOut=%p\n",
           zFilename, flags, pResOut));

  if( zFilename==0 ){
    *pResOut = 0;







>

>
>
>
>
>







53000
53001
53002
53003
53004
53005
53006
53007
53008
53009
53010
53011
53012
53013
53014
53015
53016
53017
53018
53019
53020
  int flags,                 /* Type of test to make on this file */
  int *pResOut               /* OUT: Result */
){
  DWORD attr;
  int rc = 0;
  DWORD lastErrno = 0;
  void *zConverted;
  int noRetry = 0;           /* Do not use winRetryIoerr() */
  UNUSED_PARAMETER(pVfs);

  if( (flags & NORETRY)!=0 ){
    noRetry = 1;
    flags &= ~NORETRY;
  }

  SimulateIOError( return SQLITE_IOERR_ACCESS; );
  OSTRACE(("ACCESS name=%s, flags=%x, pResOut=%p\n",
           zFilename, flags, pResOut));

  if( zFilename==0 ){
    *pResOut = 0;
52854
52855
52856
52857
52858
52859
52860


52861

52862
52863
52864
52865
52866
52867
52868
  }
  if( osIsNT() ){
    int cnt = 0;
    WIN32_FILE_ATTRIBUTE_DATA sAttrData;
    memset(&sAttrData, 0, sizeof(sAttrData));
    while( !(rc = osGetFileAttributesExW((LPCWSTR)zConverted,
                             GetFileExInfoStandard,


                             &sAttrData)) && winRetryIoerr(&cnt, &lastErrno) ){}

    if( rc ){
      /* For an SQLITE_ACCESS_EXISTS query, treat a zero-length file
      ** as if it does not exist.
      */
      if(    flags==SQLITE_ACCESS_EXISTS
          && sAttrData.nFileSizeHigh==0
          && sAttrData.nFileSizeLow==0 ){







>
>
|
>







53030
53031
53032
53033
53034
53035
53036
53037
53038
53039
53040
53041
53042
53043
53044
53045
53046
53047
  }
  if( osIsNT() ){
    int cnt = 0;
    WIN32_FILE_ATTRIBUTE_DATA sAttrData;
    memset(&sAttrData, 0, sizeof(sAttrData));
    while( !(rc = osGetFileAttributesExW((LPCWSTR)zConverted,
                             GetFileExInfoStandard,
                             &sAttrData))
       && !noRetry
       && winRetryIoerr(&cnt, &lastErrno)
    ){ /* Loop until true */}
    if( rc ){
      /* For an SQLITE_ACCESS_EXISTS query, treat a zero-length file
      ** as if it does not exist.
      */
      if(    flags==SQLITE_ACCESS_EXISTS
          && sAttrData.nFileSizeHigh==0
          && sAttrData.nFileSizeLow==0 ){
52922
52923
52924
52925
52926
52927
52928

52929
52930
52931
52932
52933
52934
52935
*/
static BOOL winIsDriveLetterAndColon(
  const char *zPathname
){
  return ( sqlite3Isalpha(zPathname[0]) && zPathname[1]==':' );
}


/*
** Returns non-zero if the specified path name should be used verbatim.  If
** non-zero is returned from this function, the calling function must simply
** use the provided path name verbatim -OR- resolve it into a full path name
** using the GetFullPathName Win32 API function (if available).
*/
static BOOL winIsVerbatimPathname(







>







53101
53102
53103
53104
53105
53106
53107
53108
53109
53110
53111
53112
53113
53114
53115
*/
static BOOL winIsDriveLetterAndColon(
  const char *zPathname
){
  return ( sqlite3Isalpha(zPathname[0]) && zPathname[1]==':' );
}

#ifdef _WIN32
/*
** Returns non-zero if the specified path name should be used verbatim.  If
** non-zero is returned from this function, the calling function must simply
** use the provided path name verbatim -OR- resolve it into a full path name
** using the GetFullPathName Win32 API function (if available).
*/
static BOOL winIsVerbatimPathname(
52958
52959
52960
52961
52962
52963
52964
































































52965
52966
52967
52968
52969
52970
52971
52972
52973
52974
52975
52976
52977
52978
52979
52980
52981
52982
52983
52984
52985
52986
52987
52988
52989
52990


52991










































































52992
52993
52994
52995
52996
52997
52998
52999
53000
53001
53002
53003
53004
53005
53006
53007
53008
53009
53010
53011
53012
53013
53014
53015
53016
53017
53018
53019
53020
53021
53022
53023
53024
53025
53026
53027
53028
53029
53030
53031
53032
53033
53034
53035
53036
53037
53038
53039
53040
53041
53042
53043
53044
53045
53046
53047
53048
53049
53050
53051
53052
53053
53054
53055
53056
53057
53058
53059
53060
53061
53062
53063
53064
53065
53066
53067

53068
53069
53070
53071
53072
53073
53074
53075
53076
53077
53078
53079
53080
53081
53082
53083
53084

53085
53086
53087
53088
53089
53090
53091
53092
53093
53094
53095
53096

53097
53098
53099
53100
53101
53102
53103
53104
53105
53106
53107
53108
53109

  /*
  ** If we get to this point, the path name should almost certainly be a purely
  ** relative one (i.e. not a UNC name, not absolute, and not volume relative).
  */
  return FALSE;
}

































































/*
** Turn a relative pathname into a full pathname.  Write the full
** pathname into zOut[].  zOut[] will be at least pVfs->mxPathname
** bytes in size.
*/
static int winFullPathnameNoMutex(
  sqlite3_vfs *pVfs,            /* Pointer to vfs object */
  const char *zRelative,        /* Possibly relative input path */
  int nFull,                    /* Size of output buffer in bytes */
  char *zFull                   /* Output buffer */
){
#if !SQLITE_OS_WINCE && !SQLITE_OS_WINRT && !defined(__CYGWIN__)
  DWORD nByte;
  void *zConverted;
  char *zOut;
#endif

  /* If this path name begins with "/X:" or "\\?\", where "X" is any
  ** alphabetic character, discard the initial "/" from the pathname.
  */
  if( zRelative[0]=='/' && (winIsDriveLetterAndColon(zRelative+1)
       || winIsLongPathPrefix(zRelative+1)) ){
    zRelative++;
  }



#if defined(__CYGWIN__)










































































  SimulateIOError( return SQLITE_ERROR );
  UNUSED_PARAMETER(nFull);
  assert( nFull>=pVfs->mxPathname );
  if ( sqlite3_data_directory && !winIsVerbatimPathname(zRelative) ){
    /*
    ** NOTE: We are dealing with a relative path name and the data
    **       directory has been set.  Therefore, use it as the basis
    **       for converting the relative path name to an absolute
    **       one by prepending the data directory and a slash.
    */
    char *zOut = sqlite3MallocZero( 1+(u64)pVfs->mxPathname );
    if( !zOut ){
      return SQLITE_IOERR_NOMEM_BKPT;
    }
    if( cygwin_conv_path(
            (osIsNT() ? CCP_POSIX_TO_WIN_W : CCP_POSIX_TO_WIN_A) |
            CCP_RELATIVE, zRelative, zOut, pVfs->mxPathname+1)<0 ){
      sqlite3_free(zOut);
      return winLogError(SQLITE_CANTOPEN_CONVPATH, (DWORD)errno,
                         "winFullPathname1", zRelative);
    }else{
      char *zUtf8 = winConvertToUtf8Filename(zOut);
      if( !zUtf8 ){
        sqlite3_free(zOut);
        return SQLITE_IOERR_NOMEM_BKPT;
      }
      sqlite3_snprintf(MIN(nFull, pVfs->mxPathname), zFull, "%s%c%s",
                       sqlite3_data_directory, winGetDirSep(), zUtf8);
      sqlite3_free(zUtf8);
      sqlite3_free(zOut);
    }
  }else{
    char *zOut = sqlite3MallocZero( pVfs->mxPathname+1 );
    if( !zOut ){
      return SQLITE_IOERR_NOMEM_BKPT;
    }
    if( cygwin_conv_path(
            (osIsNT() ? CCP_POSIX_TO_WIN_W : CCP_POSIX_TO_WIN_A),
            zRelative, zOut, pVfs->mxPathname+1)<0 ){
      sqlite3_free(zOut);
      return winLogError(SQLITE_CANTOPEN_CONVPATH, (DWORD)errno,
                         "winFullPathname2", zRelative);
    }else{
      char *zUtf8 = winConvertToUtf8Filename(zOut);
      if( !zUtf8 ){
        sqlite3_free(zOut);
        return SQLITE_IOERR_NOMEM_BKPT;
      }
      sqlite3_snprintf(MIN(nFull, pVfs->mxPathname), zFull, "%s", zUtf8);
      sqlite3_free(zUtf8);
      sqlite3_free(zOut);
    }
  }
  return SQLITE_OK;
#endif

#if (SQLITE_OS_WINCE || SQLITE_OS_WINRT) && !defined(__CYGWIN__)
  SimulateIOError( return SQLITE_ERROR );
  /* WinCE has no concept of a relative pathname, or so I am told. */
  /* WinRT has no way to convert a relative path to an absolute one. */
  if ( sqlite3_data_directory && !winIsVerbatimPathname(zRelative) ){
    /*
    ** NOTE: We are dealing with a relative path name and the data
    **       directory has been set.  Therefore, use it as the basis
    **       for converting the relative path name to an absolute
    **       one by prepending the data directory and a backslash.
    */
    sqlite3_snprintf(MIN(nFull, pVfs->mxPathname), zFull, "%s%c%s",
                     sqlite3_data_directory, winGetDirSep(), zRelative);
  }else{
    sqlite3_snprintf(MIN(nFull, pVfs->mxPathname), zFull, "%s", zRelative);
  }
  return SQLITE_OK;
#endif

#if !SQLITE_OS_WINCE && !SQLITE_OS_WINRT && !defined(__CYGWIN__)

  /* It's odd to simulate an io-error here, but really this is just
  ** using the io-error infrastructure to test that SQLite handles this
  ** function failing. This function could fail if, for example, the
  ** current working directory has been unlinked.
  */
  SimulateIOError( return SQLITE_ERROR );
  if ( sqlite3_data_directory && !winIsVerbatimPathname(zRelative) ){
    /*
    ** NOTE: We are dealing with a relative path name and the data
    **       directory has been set.  Therefore, use it as the basis
    **       for converting the relative path name to an absolute
    **       one by prepending the data directory and a backslash.
    */
    sqlite3_snprintf(MIN(nFull, pVfs->mxPathname), zFull, "%s%c%s",
                     sqlite3_data_directory, winGetDirSep(), zRelative);
    return SQLITE_OK;
  }

  zConverted = winConvertFromUtf8Filename(zRelative);
  if( zConverted==0 ){
    return SQLITE_IOERR_NOMEM_BKPT;
  }
  if( osIsNT() ){
    LPWSTR zTemp;
    nByte = osGetFullPathNameW((LPCWSTR)zConverted, 0, 0, 0);
    if( nByte==0 ){
      sqlite3_free(zConverted);
      return winLogError(SQLITE_CANTOPEN_FULLPATH, osGetLastError(),
                         "winFullPathname1", zRelative);
    }

    zTemp = sqlite3MallocZero( nByte*sizeof(zTemp[0]) + 3*sizeof(zTemp[0]) );
    if( zTemp==0 ){
      sqlite3_free(zConverted);
      return SQLITE_IOERR_NOMEM_BKPT;
    }
    nByte = osGetFullPathNameW((LPCWSTR)zConverted, nByte+3, zTemp, 0);
    if( nByte==0 ){
      sqlite3_free(zConverted);
      sqlite3_free(zTemp);
      return winLogError(SQLITE_CANTOPEN_FULLPATH, osGetLastError(),
                         "winFullPathname2", zRelative);
    }
    sqlite3_free(zConverted);







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>












|
|












>
>
|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>



<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<



|


















|
>

















>












>
|




|







53138
53139
53140
53141
53142
53143
53144
53145
53146
53147
53148
53149
53150
53151
53152
53153
53154
53155
53156
53157
53158
53159
53160
53161
53162
53163
53164
53165
53166
53167
53168
53169
53170
53171
53172
53173
53174
53175
53176
53177
53178
53179
53180
53181
53182
53183
53184
53185
53186
53187
53188
53189
53190
53191
53192
53193
53194
53195
53196
53197
53198
53199
53200
53201
53202
53203
53204
53205
53206
53207
53208
53209
53210
53211
53212
53213
53214
53215
53216
53217
53218
53219
53220
53221
53222
53223
53224
53225
53226
53227
53228
53229
53230
53231
53232
53233
53234
53235
53236
53237
53238
53239
53240
53241
53242
53243
53244
53245
53246
53247
53248
53249
53250
53251
53252
53253
53254
53255
53256
53257
53258
53259
53260
53261
53262
53263
53264
53265
53266
53267
53268
53269
53270
53271
53272
53273
53274
53275
53276
53277
53278
53279
53280
53281
53282
53283
53284
53285
53286
53287
53288
53289
53290
53291
53292
53293
53294
53295
53296
53297
53298
53299
53300
53301
53302
53303
53304
53305
53306
53307
53308
53309
53310
53311
53312
53313
53314





























53315
53316
53317
53318
53319
53320
53321
53322
53323
53324
53325
53326
53327
53328
53329
53330
53331
53332
53333
53334

53335
53336
53337
53338
53339
53340
53341
53342
53343
53344
53345
53346
53347
53348
53349
53350
53351
53352
53353
53354
53355
53356
53357
53358
53359
53360
53361
53362
53363
53364
53365
53366
53367
53368
53369
53370
53371
53372
53373
53374
53375
53376
53377
53378
53379
53380
53381
53382
53383
53384
53385
53386
53387
53388
53389
53390
53391
53392
53393
53394
53395
53396
53397
53398
53399
53400
53401
53402

  /*
  ** If we get to this point, the path name should almost certainly be a purely
  ** relative one (i.e. not a UNC name, not absolute, and not volume relative).
  */
  return FALSE;
}
#endif /* _WIN32 */

#ifdef __CYGWIN__
/*
** Simplify a filename into its canonical form
** by making the following changes:
**
**  * convert any '/' to '\' (win32) or reverse (Cygwin)
**  * removing any trailing and duplicate / (except for UNC paths)
**  * convert /./ into just /
**
** Changes are made in-place.  Return the new name length.
**
** The original filename is in z[0..]. If the path is shortened,
** no-longer used bytes will be written by '\0'.
*/
static void winSimplifyName(char *z){
  int i, j;
  for(i=j=0; z[i]; ++i){
    if( winIsDirSep(z[i]) ){
#if !defined(SQLITE_TEST)
      /* Some test-cases assume that "./foo" and "foo" are different */
      if( z[i+1]=='.' && winIsDirSep(z[i+2]) ){
        ++i;
        continue;
      }
#endif
      if( !z[i+1] || (winIsDirSep(z[i+1]) && (i!=0)) ){
        continue;
      }
      z[j++] = osGetenv?'/':'\\';
    }else{
      z[j++] = z[i];
    }
  }
  while(j<i) z[j++] = '\0';
}

#define SQLITE_MAX_SYMLINKS 100

static int mkFullPathname(
  const char *zPath,              /* Input path */
  char *zOut,                     /* Output buffer */
  int nOut                        /* Allocated size of buffer zOut */
){
  int nPath = sqlite3Strlen30(zPath);
  int iOff = 0;
  if( zPath[0]!='/' ){
    if( osGetcwd(zOut, nOut-2)==0 ){
      return winLogError(SQLITE_CANTOPEN_BKPT, (DWORD)osErrno, "getcwd", zPath);
    }
    iOff = sqlite3Strlen30(zOut);
    zOut[iOff++] = '/';
  }
  if( (iOff+nPath+1)>nOut ){
    /* SQLite assumes that xFullPathname() nul-terminates the output buffer
    ** even if it returns an error.  */
    zOut[iOff] = '\0';
    return SQLITE_CANTOPEN_BKPT;
  }
  sqlite3_snprintf(nOut-iOff, &zOut[iOff], "%s", zPath);
  return SQLITE_OK;
}
#endif /* __CYGWIN__ */

/*
** Turn a relative pathname into a full pathname.  Write the full
** pathname into zOut[].  zOut[] will be at least pVfs->mxPathname
** bytes in size.
*/
static int winFullPathnameNoMutex(
  sqlite3_vfs *pVfs,            /* Pointer to vfs object */
  const char *zRelative,        /* Possibly relative input path */
  int nFull,                    /* Size of output buffer in bytes */
  char *zFull                   /* Output buffer */
){
#if !SQLITE_OS_WINCE && !SQLITE_OS_WINRT
  int nByte;
  void *zConverted;
  char *zOut;
#endif

  /* If this path name begins with "/X:" or "\\?\", where "X" is any
  ** alphabetic character, discard the initial "/" from the pathname.
  */
  if( zRelative[0]=='/' && (winIsDriveLetterAndColon(zRelative+1)
       || winIsLongPathPrefix(zRelative+1)) ){
    zRelative++;
  }

  SimulateIOError( return SQLITE_ERROR );

#ifdef __CYGWIN__
  if( osGetcwd ){
    zFull[nFull-1] = '\0';
    if( !winIsDriveLetterAndColon(zRelative) || !winIsDirSep(zRelative[2]) ){
      int rc = SQLITE_OK;
      int nLink = 1;                /* Number of symbolic links followed so far */
      const char *zIn = zRelative;      /* Input path for each iteration of loop */
      char *zDel = 0;
      struct stat buf;

      UNUSED_PARAMETER(pVfs);

      do {
        /* Call lstat() on path zIn. Set bLink to true if the path is a symbolic
        ** link, or false otherwise.  */
        int bLink = 0;
        if( osLstat && osReadlink ) {
          if( osLstat(zIn, &buf)!=0 ){
            int myErrno = osErrno;
            if( myErrno!=ENOENT ){
              rc = winLogError(SQLITE_CANTOPEN_BKPT, (DWORD)myErrno, "lstat", zIn);
            }
          }else{
            bLink = ((buf.st_mode & 0170000) == 0120000);
          }

          if( bLink ){
            if( zDel==0 ){
              zDel = sqlite3MallocZero(nFull);
              if( zDel==0 ) rc = SQLITE_NOMEM;
            }else if( ++nLink>SQLITE_MAX_SYMLINKS ){
              rc = SQLITE_CANTOPEN_BKPT;
            }

            if( rc==SQLITE_OK ){
              nByte = osReadlink(zIn, zDel, nFull-1);
              if( nByte ==(DWORD)-1 ){
                rc = winLogError(SQLITE_CANTOPEN_BKPT, (DWORD)osErrno, "readlink", zIn);
              }else{
                if( zDel[0]!='/' ){
                  int n;
                  for(n = sqlite3Strlen30(zIn); n>0 && zIn[n-1]!='/'; n--);
                  if( nByte+n+1>nFull ){
                    rc = SQLITE_CANTOPEN_BKPT;
                  }else{
                    memmove(&zDel[n], zDel, nByte+1);
                    memcpy(zDel, zIn, n);
                    nByte += n;
                  }
                }
                zDel[nByte] = '\0';
              }
            }

            zIn = zDel;
          }
        }

        assert( rc!=SQLITE_OK || zIn!=zFull || zIn[0]=='/' );
        if( rc==SQLITE_OK && zIn!=zFull ){
          rc = mkFullPathname(zIn, zFull, nFull);
        }
        if( bLink==0 ) break;
        zIn = zFull;
      }while( rc==SQLITE_OK );

      sqlite3_free(zDel);
      winSimplifyName(zFull);
      return rc;
    }
  }
#endif /* __CYGWIN__ */
#if 0 /* This doesn't work correctly at all! See:
  <https://marc.info/?l=sqlite-users&m=139299149416314&w=2>
*/
  SimulateIOError( return SQLITE_ERROR );
  UNUSED_PARAMETER(nFull);
  assert( nFull>=pVfs->mxPathname );





























  char *zOut = sqlite3MallocZero( pVfs->mxPathname+1 );
  if( !zOut ){
    return SQLITE_IOERR_NOMEM_BKPT;
  }
  if( osCygwin_conv_path(
          CCP_POSIX_TO_WIN_W,
          zRelative, zOut, pVfs->mxPathname+1)<0 ){
    sqlite3_free(zOut);
    return winLogError(SQLITE_CANTOPEN_CONVPATH, (DWORD)errno,
                       "winFullPathname2", zRelative);
  }else{
    char *zUtf8 = winConvertToUtf8Filename(zOut);
    if( !zUtf8 ){
      sqlite3_free(zOut);
      return SQLITE_IOERR_NOMEM_BKPT;
    }
    sqlite3_snprintf(MIN(nFull, pVfs->mxPathname), zFull, "%s", zUtf8);
    sqlite3_free(zUtf8);
    sqlite3_free(zOut);
  }

  return SQLITE_OK;
#endif

#if (SQLITE_OS_WINCE || SQLITE_OS_WINRT) && defined(_WIN32)
  SimulateIOError( return SQLITE_ERROR );
  /* WinCE has no concept of a relative pathname, or so I am told. */
  /* WinRT has no way to convert a relative path to an absolute one. */
  if ( sqlite3_data_directory && !winIsVerbatimPathname(zRelative) ){
    /*
    ** NOTE: We are dealing with a relative path name and the data
    **       directory has been set.  Therefore, use it as the basis
    **       for converting the relative path name to an absolute
    **       one by prepending the data directory and a backslash.
    */
    sqlite3_snprintf(MIN(nFull, pVfs->mxPathname), zFull, "%s%c%s",
                     sqlite3_data_directory, winGetDirSep(), zRelative);
  }else{
    sqlite3_snprintf(MIN(nFull, pVfs->mxPathname), zFull, "%s", zRelative);
  }
  return SQLITE_OK;
#endif

#if !SQLITE_OS_WINCE && !SQLITE_OS_WINRT
#if defined(_WIN32)
  /* It's odd to simulate an io-error here, but really this is just
  ** using the io-error infrastructure to test that SQLite handles this
  ** function failing. This function could fail if, for example, the
  ** current working directory has been unlinked.
  */
  SimulateIOError( return SQLITE_ERROR );
  if ( sqlite3_data_directory && !winIsVerbatimPathname(zRelative) ){
    /*
    ** NOTE: We are dealing with a relative path name and the data
    **       directory has been set.  Therefore, use it as the basis
    **       for converting the relative path name to an absolute
    **       one by prepending the data directory and a backslash.
    */
    sqlite3_snprintf(MIN(nFull, pVfs->mxPathname), zFull, "%s%c%s",
                     sqlite3_data_directory, winGetDirSep(), zRelative);
    return SQLITE_OK;
  }
#endif
  zConverted = winConvertFromUtf8Filename(zRelative);
  if( zConverted==0 ){
    return SQLITE_IOERR_NOMEM_BKPT;
  }
  if( osIsNT() ){
    LPWSTR zTemp;
    nByte = osGetFullPathNameW((LPCWSTR)zConverted, 0, 0, 0);
    if( nByte==0 ){
      sqlite3_free(zConverted);
      return winLogError(SQLITE_CANTOPEN_FULLPATH, osGetLastError(),
                         "winFullPathname1", zRelative);
    }
    nByte += 3;
    zTemp = sqlite3MallocZero( nByte*sizeof(zTemp[0]) );
    if( zTemp==0 ){
      sqlite3_free(zConverted);
      return SQLITE_IOERR_NOMEM_BKPT;
    }
    nByte = osGetFullPathNameW((LPCWSTR)zConverted, nByte, zTemp, 0);
    if( nByte==0 ){
      sqlite3_free(zConverted);
      sqlite3_free(zTemp);
      return winLogError(SQLITE_CANTOPEN_FULLPATH, osGetLastError(),
                         "winFullPathname2", zRelative);
    }
    sqlite3_free(zConverted);
53133
53134
53135
53136
53137
53138
53139


53140

















53141
53142
53143
53144
53145
53146
53147
    }
    sqlite3_free(zConverted);
    zOut = winMbcsToUtf8(zTemp, osAreFileApisANSI());
    sqlite3_free(zTemp);
  }
#endif
  if( zOut ){


    sqlite3_snprintf(MIN(nFull, pVfs->mxPathname), zFull, "%s", zOut);

















    sqlite3_free(zOut);
    return SQLITE_OK;
  }else{
    return SQLITE_IOERR_NOMEM_BKPT;
  }
#endif
}







>
>
|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







53426
53427
53428
53429
53430
53431
53432
53433
53434
53435
53436
53437
53438
53439
53440
53441
53442
53443
53444
53445
53446
53447
53448
53449
53450
53451
53452
53453
53454
53455
53456
53457
53458
53459
    }
    sqlite3_free(zConverted);
    zOut = winMbcsToUtf8(zTemp, osAreFileApisANSI());
    sqlite3_free(zTemp);
  }
#endif
  if( zOut ){
#ifdef __CYGWIN__
    if( memcmp(zOut, "\\\\?\\", 4) ){
      sqlite3_snprintf(MIN(nFull, pVfs->mxPathname), zFull, "%s", zOut);
    }else if( memcmp(zOut+4, "UNC\\", 4) ){
      sqlite3_snprintf(MIN(nFull, pVfs->mxPathname), zFull, "%s", zOut+4);
    }else{
      char *p = zOut+6;
      *p = '\\';
      if( osGetcwd ){
        /* On Cygwin, UNC paths use forward slashes */
        while( *p ){
          if( *p=='\\' ) *p = '/';
          ++p;
        }
      }
      sqlite3_snprintf(MIN(nFull, pVfs->mxPathname), zFull, "%s", zOut+6);
    }
#else
    sqlite3_snprintf(MIN(nFull, pVfs->mxPathname), zFull, "%s", zOut);
#endif /* __CYGWIN__ */
    sqlite3_free(zOut);
    return SQLITE_OK;
  }else{
    return SQLITE_IOERR_NOMEM_BKPT;
  }
#endif
}
53163
53164
53165
53166
53167
53168
53169
53170


53171
53172
53173
53174
53175
53176
53177
#ifndef SQLITE_OMIT_LOAD_EXTENSION
/*
** Interfaces for opening a shared library, finding entry points
** within the shared library, and closing the shared library.
*/
static void *winDlOpen(sqlite3_vfs *pVfs, const char *zFilename){
  HANDLE h;
#if defined(__CYGWIN__)


  int nFull = pVfs->mxPathname+1;
  char *zFull = sqlite3MallocZero( nFull );
  void *zConverted = 0;
  if( zFull==0 ){
    OSTRACE(("DLOPEN name=%s, handle=%p\n", zFilename, (void*)0));
    return 0;
  }







|
>
>







53475
53476
53477
53478
53479
53480
53481
53482
53483
53484
53485
53486
53487
53488
53489
53490
53491
#ifndef SQLITE_OMIT_LOAD_EXTENSION
/*
** Interfaces for opening a shared library, finding entry points
** within the shared library, and closing the shared library.
*/
static void *winDlOpen(sqlite3_vfs *pVfs, const char *zFilename){
  HANDLE h;
#if 0 /* This doesn't work correctly at all! See:
  <https://marc.info/?l=sqlite-users&m=139299149416314&w=2>
*/
  int nFull = pVfs->mxPathname+1;
  char *zFull = sqlite3MallocZero( nFull );
  void *zConverted = 0;
  if( zFull==0 ){
    OSTRACE(("DLOPEN name=%s, handle=%p\n", zFilename, (void*)0));
    return 0;
  }
53530
53531
53532
53533
53534
53535
53536
53537
53538
53539
53540
53541
53542
53543
53544
    winGetSystemCall,      /* xGetSystemCall */
    winNextSystemCall,     /* xNextSystemCall */
  };
#endif

  /* Double-check that the aSyscall[] array has been constructed
  ** correctly.  See ticket [bb3a86e890c8e96ab] */
  assert( ArraySize(aSyscall)==82 );

  /* get memory map allocation granularity */
  memset(&winSysInfo, 0, sizeof(SYSTEM_INFO));
#if SQLITE_OS_WINRT
  osGetNativeSystemInfo(&winSysInfo);
#else
  osGetSystemInfo(&winSysInfo);







|







53844
53845
53846
53847
53848
53849
53850
53851
53852
53853
53854
53855
53856
53857
53858
    winGetSystemCall,      /* xGetSystemCall */
    winNextSystemCall,     /* xNextSystemCall */
  };
#endif

  /* Double-check that the aSyscall[] array has been constructed
  ** correctly.  See ticket [bb3a86e890c8e96ab] */
  assert( ArraySize(aSyscall)==89 );

  /* get memory map allocation granularity */
  memset(&winSysInfo, 0, sizeof(SYSTEM_INFO));
#if SQLITE_OS_WINRT
  osGetNativeSystemInfo(&winSysInfo);
#else
  osGetSystemInfo(&winSysInfo);
66506
66507
66508
66509
66510
66511
66512

66513
66514
66515
66516
66517
66518
66519
66520
66521
66522
66523
  if( aIn ){
    s1 = aIn[0];
    s2 = aIn[1];
  }else{
    s1 = s2 = 0;
  }


  assert( nByte>=8 );
  assert( (nByte&0x00000007)==0 );
  assert( nByte<=65536 );
  assert( nByte%4==0 );

  if( !nativeCksum ){
    do {
      s1 += BYTESWAP32(aData[0]) + s2;
      s2 += BYTESWAP32(aData[1]) + s1;
      aData += 2;
    }while( aData<aEnd );







>
|
<
<
<







66820
66821
66822
66823
66824
66825
66826
66827
66828



66829
66830
66831
66832
66833
66834
66835
  if( aIn ){
    s1 = aIn[0];
    s2 = aIn[1];
  }else{
    s1 = s2 = 0;
  }

  /* nByte is a multiple of 8 between 8 and 65536 */
  assert( nByte>=8 && (nByte&7)==0 && nByte<=65536 );




  if( !nativeCksum ){
    do {
      s1 += BYTESWAP32(aData[0]) + s2;
      s2 += BYTESWAP32(aData[1]) + s1;
      aData += 2;
    }while( aData<aEnd );
83724
83725
83726
83727
83728
83729
83730
83731
83732
83733
83734
83735
83736
83737
83738
**
** A single int or real value always converts to the same strings.  But
** many different strings can be converted into the same int or real.
** If a table contains a numeric value and an index is based on the
** corresponding string value, then it is important that the string be
** derived from the numeric value, not the other way around, to ensure
** that the index and table are consistent.  See ticket
** https://www.sqlite.org/src/info/343634942dd54ab (2018-01-31) for
** an example.
**
** This routine looks at pMem to verify that if it has both a numeric
** representation and a string representation then the string rep has
** been derived from the numeric and not the other way around.  It returns
** true if everything is ok and false if there is a problem.
**







|







84036
84037
84038
84039
84040
84041
84042
84043
84044
84045
84046
84047
84048
84049
84050
**
** A single int or real value always converts to the same strings.  But
** many different strings can be converted into the same int or real.
** If a table contains a numeric value and an index is based on the
** corresponding string value, then it is important that the string be
** derived from the numeric value, not the other way around, to ensure
** that the index and table are consistent.  See ticket
** https://sqlite.org/src/info/343634942dd54ab (2018-01-31) for
** an example.
**
** This routine looks at pMem to verify that if it has both a numeric
** representation and a string representation then the string rep has
** been derived from the numeric and not the other way around.  It returns
** true if everything is ok and false if there is a problem.
**
93012
93013
93014
93015
93016
93017
93018
93019
93020
93021
93022
93023
93024
93025
93026
  sqlite3_uint64 nData,
  void (*xDel)(void*),
  unsigned char enc
){
  assert( xDel!=SQLITE_DYNAMIC );
  if( enc!=SQLITE_UTF8 ){
    if( enc==SQLITE_UTF16 ) enc = SQLITE_UTF16NATIVE;
    nData &= ~(u16)1;
  }
  return bindText(pStmt, i, zData, nData, xDel, enc);
}
#ifndef SQLITE_OMIT_UTF16
SQLITE_API int sqlite3_bind_text16(
  sqlite3_stmt *pStmt,
  int i,







|







93324
93325
93326
93327
93328
93329
93330
93331
93332
93333
93334
93335
93336
93337
93338
  sqlite3_uint64 nData,
  void (*xDel)(void*),
  unsigned char enc
){
  assert( xDel!=SQLITE_DYNAMIC );
  if( enc!=SQLITE_UTF8 ){
    if( enc==SQLITE_UTF16 ) enc = SQLITE_UTF16NATIVE;
    nData &= ~(u64)1;
  }
  return bindText(pStmt, i, zData, nData, xDel, enc);
}
#ifndef SQLITE_OMIT_UTF16
SQLITE_API int sqlite3_bind_text16(
  sqlite3_stmt *pStmt,
  int i,
125164
125165
125166
125167
125168
125169
125170
125171
125172
125173
125174
125175
125176
125177
125178
    if( resizeIndexObject(pParse, pIdx, pIdx->nKeyCol+n) ) return;
    for(i=0, j=pIdx->nKeyCol; i<nPk; i++){
      if( !isDupColumn(pIdx, pIdx->nKeyCol, pPk, i) ){
        testcase( hasColumn(pIdx->aiColumn, pIdx->nKeyCol, pPk->aiColumn[i]) );
        pIdx->aiColumn[j] = pPk->aiColumn[i];
        pIdx->azColl[j] = pPk->azColl[i];
        if( pPk->aSortOrder[i] ){
          /* See ticket https://www.sqlite.org/src/info/bba7b69f9849b5bf */
          pIdx->bAscKeyBug = 1;
        }
        j++;
      }
    }
    assert( pIdx->nColumn>=pIdx->nKeyCol+n );
    assert( pIdx->nColumn>=j );







|







125476
125477
125478
125479
125480
125481
125482
125483
125484
125485
125486
125487
125488
125489
125490
    if( resizeIndexObject(pParse, pIdx, pIdx->nKeyCol+n) ) return;
    for(i=0, j=pIdx->nKeyCol; i<nPk; i++){
      if( !isDupColumn(pIdx, pIdx->nKeyCol, pPk, i) ){
        testcase( hasColumn(pIdx->aiColumn, pIdx->nKeyCol, pPk->aiColumn[i]) );
        pIdx->aiColumn[j] = pPk->aiColumn[i];
        pIdx->azColl[j] = pPk->azColl[i];
        if( pPk->aSortOrder[i] ){
          /* See ticket https://sqlite.org/src/info/bba7b69f9849b5bf */
          pIdx->bAscKeyBug = 1;
        }
        j++;
      }
    }
    assert( pIdx->nColumn>=pIdx->nKeyCol+n );
    assert( pIdx->nColumn>=j );
126541
126542
126543
126544
126545
126546
126547
126548
126549
126550
126551
126552
126553
126554
126555
  sqlite3VdbeAddOp3(v, OP_SorterData, iSorter, regRecord, iIdx);
  if( !pIndex->bAscKeyBug ){
    /* This OP_SeekEnd opcode makes index insert for a REINDEX go much
    ** faster by avoiding unnecessary seeks.  But the optimization does
    ** not work for UNIQUE constraint indexes on WITHOUT ROWID tables
    ** with DESC primary keys, since those indexes have there keys in
    ** a different order from the main table.
    ** See ticket: https://www.sqlite.org/src/info/bba7b69f9849b5bf
    */
    sqlite3VdbeAddOp1(v, OP_SeekEnd, iIdx);
  }
  sqlite3VdbeAddOp2(v, OP_IdxInsert, iIdx, regRecord);
  sqlite3VdbeChangeP5(v, OPFLAG_USESEEKRESULT);
  sqlite3ReleaseTempReg(pParse, regRecord);
  sqlite3VdbeAddOp2(v, OP_SorterNext, iSorter, addr2); VdbeCoverage(v);







|







126853
126854
126855
126856
126857
126858
126859
126860
126861
126862
126863
126864
126865
126866
126867
  sqlite3VdbeAddOp3(v, OP_SorterData, iSorter, regRecord, iIdx);
  if( !pIndex->bAscKeyBug ){
    /* This OP_SeekEnd opcode makes index insert for a REINDEX go much
    ** faster by avoiding unnecessary seeks.  But the optimization does
    ** not work for UNIQUE constraint indexes on WITHOUT ROWID tables
    ** with DESC primary keys, since those indexes have there keys in
    ** a different order from the main table.
    ** See ticket: https://sqlite.org/src/info/bba7b69f9849b5bf
    */
    sqlite3VdbeAddOp1(v, OP_SeekEnd, iIdx);
  }
  sqlite3VdbeAddOp2(v, OP_IdxInsert, iIdx, regRecord);
  sqlite3VdbeChangeP5(v, OPFLAG_USESEEKRESULT);
  sqlite3ReleaseTempReg(pParse, regRecord);
  sqlite3VdbeAddOp2(v, OP_SorterNext, iSorter, addr2); VdbeCoverage(v);
126925
126926
126927
126928
126929
126930
126931

126932
126933
126934
126935
126936
126937
126938
      pIndex->uniqNotNull = 0;
      pIndex->bHasExpr = 1;
    }else{
      j = pCExpr->iColumn;
      assert( j<=0x7fff );
      if( j<0 ){
        j = pTab->iPKey;

      }else{
        if( pTab->aCol[j].notNull==0 ){
          pIndex->uniqNotNull = 0;
        }
        if( pTab->aCol[j].colFlags & COLFLAG_VIRTUAL ){
          pIndex->bHasVCol = 1;
          pIndex->bHasExpr = 1;







>







127237
127238
127239
127240
127241
127242
127243
127244
127245
127246
127247
127248
127249
127250
127251
      pIndex->uniqNotNull = 0;
      pIndex->bHasExpr = 1;
    }else{
      j = pCExpr->iColumn;
      assert( j<=0x7fff );
      if( j<0 ){
        j = pTab->iPKey;
        pIndex->bIdxRowid = 1;
      }else{
        if( pTab->aCol[j].notNull==0 ){
          pIndex->uniqNotNull = 0;
        }
        if( pTab->aCol[j].colFlags & COLFLAG_VIRTUAL ){
          pIndex->bHasVCol = 1;
          pIndex->bHasExpr = 1;
136630
136631
136632
136633
136634
136635
136636
136637
136638
136639
136640
136641
136642
136643
136644
  **
  ** OE_Fail and OE_Ignore must happen before any changes are made.
  ** OE_Update guarantees that only a single row will change, so it
  ** must happen before OE_Replace.  Technically, OE_Abort and OE_Rollback
  ** could happen in any order, but they are grouped up front for
  ** convenience.
  **
  ** 2018-08-14: Ticket https://www.sqlite.org/src/info/908f001483982c43
  ** The order of constraints used to have OE_Update as (2) and OE_Abort
  ** and so forth as (1). But apparently PostgreSQL checks the OE_Update
  ** constraint before any others, so it had to be moved.
  **
  ** Constraint checking code is generated in this order:
  **   (A)  The rowid constraint
  **   (B)  Unique index constraints that do not have OE_Replace as their







|







136943
136944
136945
136946
136947
136948
136949
136950
136951
136952
136953
136954
136955
136956
136957
  **
  ** OE_Fail and OE_Ignore must happen before any changes are made.
  ** OE_Update guarantees that only a single row will change, so it
  ** must happen before OE_Replace.  Technically, OE_Abort and OE_Rollback
  ** could happen in any order, but they are grouped up front for
  ** convenience.
  **
  ** 2018-08-14: Ticket https://sqlite.org/src/info/908f001483982c43
  ** The order of constraints used to have OE_Update as (2) and OE_Abort
  ** and so forth as (1). But apparently PostgreSQL checks the OE_Update
  ** constraint before any others, so it had to be moved.
  **
  ** Constraint checking code is generated in this order:
  **   (A)  The rowid constraint
  **   (B)  Unique index constraints that do not have OE_Replace as their
147783
147784
147785
147786
147787
147788
147789

147790
147791
147792
147793
147794
147795
147796
    }
    sqlite3KeyInfoUnref(pKeyInfo);
  }

multi_select_end:
  pDest->iSdst = dest.iSdst;
  pDest->nSdst = dest.nSdst;

  if( pDelete ){
    sqlite3ParserAddCleanup(pParse, sqlite3SelectDeleteGeneric, pDelete);
  }
  return rc;
}
#endif /* SQLITE_OMIT_COMPOUND_SELECT */








>







148096
148097
148098
148099
148100
148101
148102
148103
148104
148105
148106
148107
148108
148109
148110
    }
    sqlite3KeyInfoUnref(pKeyInfo);
  }

multi_select_end:
  pDest->iSdst = dest.iSdst;
  pDest->nSdst = dest.nSdst;
  pDest->iSDParm2 = dest.iSDParm2;
  if( pDelete ){
    sqlite3ParserAddCleanup(pParse, sqlite3SelectDeleteGeneric, pDelete);
  }
  return rc;
}
#endif /* SQLITE_OMIT_COMPOUND_SELECT */

149393
149394
149395
149396
149397
149398
149399

149400
149401
149402
149403
149404
149405
149406
149407
    assert( pE2->op==TK_COLUMN );
    if( pE2->iTable==pColumn->iTable
     && pE2->iColumn==pColumn->iColumn
    ){
      return;  /* Already present.  Return without doing anything. */
    }
  }

  if( sqlite3ExprAffinity(pColumn)==SQLITE_AFF_BLOB ){
    pConst->bHasAffBlob = 1;
  }

  pConst->nConst++;
  pConst->apExpr = sqlite3DbReallocOrFree(pConst->pParse->db, pConst->apExpr,
                         pConst->nConst*2*sizeof(Expr*));
  if( pConst->apExpr==0 ){







>
|







149707
149708
149709
149710
149711
149712
149713
149714
149715
149716
149717
149718
149719
149720
149721
149722
    assert( pE2->op==TK_COLUMN );
    if( pE2->iTable==pColumn->iTable
     && pE2->iColumn==pColumn->iColumn
    ){
      return;  /* Already present.  Return without doing anything. */
    }
  }
  assert( SQLITE_AFF_NONE<SQLITE_AFF_BLOB );
  if( sqlite3ExprAffinity(pColumn)<=SQLITE_AFF_BLOB ){
    pConst->bHasAffBlob = 1;
  }

  pConst->nConst++;
  pConst->apExpr = sqlite3DbReallocOrFree(pConst->pParse->db, pConst->apExpr,
                         pConst->nConst*2*sizeof(Expr*));
  if( pConst->apExpr==0 ){
149468
149469
149470
149471
149472
149473
149474

149475
149476
149477
149478
149479
149480
149481
149482
    return WRC_Continue;
  }
  for(i=0; i<pConst->nConst; i++){
    Expr *pColumn = pConst->apExpr[i*2];
    if( pColumn==pExpr ) continue;
    if( pColumn->iTable!=pExpr->iTable ) continue;
    if( pColumn->iColumn!=pExpr->iColumn ) continue;

    if( bIgnoreAffBlob && sqlite3ExprAffinity(pColumn)==SQLITE_AFF_BLOB ){
      break;
    }
    /* A match is found.  Add the EP_FixedCol property */
    pConst->nChng++;
    ExprClearProperty(pExpr, EP_Leaf);
    ExprSetProperty(pExpr, EP_FixedCol);
    assert( pExpr->pLeft==0 );







>
|







149783
149784
149785
149786
149787
149788
149789
149790
149791
149792
149793
149794
149795
149796
149797
149798
    return WRC_Continue;
  }
  for(i=0; i<pConst->nConst; i++){
    Expr *pColumn = pConst->apExpr[i*2];
    if( pColumn==pExpr ) continue;
    if( pColumn->iTable!=pExpr->iTable ) continue;
    if( pColumn->iColumn!=pExpr->iColumn ) continue;
    assert( SQLITE_AFF_NONE<SQLITE_AFF_BLOB );
    if( bIgnoreAffBlob && sqlite3ExprAffinity(pColumn)<=SQLITE_AFF_BLOB ){
      break;
    }
    /* A match is found.  Add the EP_FixedCol property */
    pConst->nChng++;
    ExprClearProperty(pExpr, EP_Leaf);
    ExprSetProperty(pExpr, EP_FixedCol);
    assert( pExpr->pLeft==0 );
150121
150122
150123
150124
150125
150126
150127
150128
150129
150130
150131
150132
150133
150134
150135
**    SELECT * FROM (SELECT ... FROM t1 EXCEPT SELECT ... FROM t2)
**     ORDER BY ... COLLATE ...
**
** This transformation is necessary because the multiSelectOrderBy() routine
** above that generates the code for a compound SELECT with an ORDER BY clause
** uses a merge algorithm that requires the same collating sequence on the
** result columns as on the ORDER BY clause.  See ticket
** http://www.sqlite.org/src/info/6709574d2a
**
** This transformation is only needed for EXCEPT, INTERSECT, and UNION.
** The UNION ALL operator works fine with multiSelectOrderBy() even when
** there are COLLATE terms in the ORDER BY.
*/
static int convertCompoundSelectToSubquery(Walker *pWalker, Select *p){
  int i;







|







150437
150438
150439
150440
150441
150442
150443
150444
150445
150446
150447
150448
150449
150450
150451
**    SELECT * FROM (SELECT ... FROM t1 EXCEPT SELECT ... FROM t2)
**     ORDER BY ... COLLATE ...
**
** This transformation is necessary because the multiSelectOrderBy() routine
** above that generates the code for a compound SELECT with an ORDER BY clause
** uses a merge algorithm that requires the same collating sequence on the
** result columns as on the ORDER BY clause.  See ticket
** http://sqlite.org/src/info/6709574d2a
**
** This transformation is only needed for EXCEPT, INTERSECT, and UNION.
** The UNION ALL operator works fine with multiSelectOrderBy() even when
** there are COLLATE terms in the ORDER BY.
*/
static int convertCompoundSelectToSubquery(Walker *pWalker, Select *p){
  int i;
152652
152653
152654
152655
152656
152657
152658






152659
152660
152661
152662
152663
152664
152665
    /* Begin the database scan. */
    TREETRACE(0x2,pParse,p,("WhereBegin\n"));
    pWInfo = sqlite3WhereBegin(pParse, pTabList, pWhere, sSort.pOrderBy,
                               p->pEList, p, wctrlFlags, p->nSelectRow);
    if( pWInfo==0 ) goto select_end;
    if( sqlite3WhereOutputRowCount(pWInfo) < p->nSelectRow ){
      p->nSelectRow = sqlite3WhereOutputRowCount(pWInfo);






    }
    if( sDistinct.isTnct && sqlite3WhereIsDistinct(pWInfo) ){
      sDistinct.eTnctType = sqlite3WhereIsDistinct(pWInfo);
    }
    if( sSort.pOrderBy ){
      sSort.nOBSat = sqlite3WhereIsOrdered(pWInfo);
      sSort.labelOBLopt = sqlite3WhereOrderByLimitOptLabel(pWInfo);







>
>
>
>
>
>







152968
152969
152970
152971
152972
152973
152974
152975
152976
152977
152978
152979
152980
152981
152982
152983
152984
152985
152986
152987
    /* Begin the database scan. */
    TREETRACE(0x2,pParse,p,("WhereBegin\n"));
    pWInfo = sqlite3WhereBegin(pParse, pTabList, pWhere, sSort.pOrderBy,
                               p->pEList, p, wctrlFlags, p->nSelectRow);
    if( pWInfo==0 ) goto select_end;
    if( sqlite3WhereOutputRowCount(pWInfo) < p->nSelectRow ){
      p->nSelectRow = sqlite3WhereOutputRowCount(pWInfo);
      if( pDest->eDest<=SRT_DistQueue && pDest->eDest>=SRT_DistFifo ){
        /* TUNING: For a UNION CTE, because UNION is implies DISTINCT,
        ** reduce the estimated output row count by 8 (LogEst 30).
        ** Search for tag-20250414a to see other cases */
        p->nSelectRow -= 30;
      }
    }
    if( sDistinct.isTnct && sqlite3WhereIsDistinct(pWInfo) ){
      sDistinct.eTnctType = sqlite3WhereIsDistinct(pWInfo);
    }
    if( sSort.pOrderBy ){
      sSort.nOBSat = sqlite3WhereIsOrdered(pWInfo);
      sSort.labelOBLopt = sqlite3WhereOrderByLimitOptLabel(pWInfo);
156923
156924
156925
156926
156927
156928
156929
156930
156931
156932
156933
156934
156935
156936
156937
    /* Default behavior:  Report an error if the argument to VACUUM is
    ** not recognized */
    iDb = sqlite3TwoPartName(pParse, pNm, pNm, &pNm);
    if( iDb<0 ) goto build_vacuum_end;
#else
    /* When SQLITE_BUG_COMPATIBLE_20160819 is defined, unrecognized arguments
    ** to VACUUM are silently ignored.  This is a back-out of a bug fix that
    ** occurred on 2016-08-19 (https://www.sqlite.org/src/info/083f9e6270).
    ** The buggy behavior is required for binary compatibility with some
    ** legacy applications. */
    iDb = sqlite3FindDb(pParse->db, pNm);
    if( iDb<0 ) iDb = 0;
#endif
  }
  if( iDb!=1 ){







|







157245
157246
157247
157248
157249
157250
157251
157252
157253
157254
157255
157256
157257
157258
157259
    /* Default behavior:  Report an error if the argument to VACUUM is
    ** not recognized */
    iDb = sqlite3TwoPartName(pParse, pNm, pNm, &pNm);
    if( iDb<0 ) goto build_vacuum_end;
#else
    /* When SQLITE_BUG_COMPATIBLE_20160819 is defined, unrecognized arguments
    ** to VACUUM are silently ignored.  This is a back-out of a bug fix that
    ** occurred on 2016-08-19 (https://sqlite.org/src/info/083f9e6270).
    ** The buggy behavior is required for binary compatibility with some
    ** legacy applications. */
    iDb = sqlite3FindDb(pParse->db, pNm);
    if( iDb<0 ) iDb = 0;
#endif
  }
  if( iDb!=1 ){
159818
159819
159820
159821
159822
159823
159824
159825
159826
159827
159828
159829
159830
159831
159832
  }
}


/*
** pX is an expression of the form:  (vector) IN (SELECT ...)
** In other words, it is a vector IN operator with a SELECT clause on the
** LHS.  But not all terms in the vector are indexable and the terms might
** not be in the correct order for indexing.
**
** This routine makes a copy of the input pX expression and then adjusts
** the vector on the LHS with corresponding changes to the SELECT so that
** the vector contains only index terms and those terms are in the correct
** order.  The modified IN expression is returned.  The caller is responsible
** for deleting the returned expression.







|







160140
160141
160142
160143
160144
160145
160146
160147
160148
160149
160150
160151
160152
160153
160154
  }
}


/*
** pX is an expression of the form:  (vector) IN (SELECT ...)
** In other words, it is a vector IN operator with a SELECT clause on the
** RHS.  But not all terms in the vector are indexable and the terms might
** not be in the correct order for indexing.
**
** This routine makes a copy of the input pX expression and then adjusts
** the vector on the LHS with corresponding changes to the SELECT so that
** the vector contains only index terms and those terms are in the correct
** order.  The modified IN expression is returned.  The caller is responsible
** for deleting the returned expression.
161640
161641
161642
161643
161644
161645
161646
161647
161648
161649
161650
161651
161652
161653
161654
    ** Actually, each subexpression is converted to "xN AND w" where w is
    ** the "interesting" terms of z - terms that did not originate in the
    ** ON or USING clause of a LEFT JOIN, and terms that are usable as
    ** indices.
    **
    ** This optimization also only applies if the (x1 OR x2 OR ...) term
    ** is not contained in the ON clause of a LEFT JOIN.
    ** See ticket http://www.sqlite.org/src/info/f2369304e4
    **
    ** 2022-02-04:  Do not push down slices of a row-value comparison.
    ** In other words, "w" or "y" may not be a slice of a vector.  Otherwise,
    ** the initialization of the right-hand operand of the vector comparison
    ** might not occur, or might occur only in an OR branch that is not
    ** taken.  dbsqlfuzz 80a9fade844b4fb43564efc972bcb2c68270f5d1.
    **







|







161962
161963
161964
161965
161966
161967
161968
161969
161970
161971
161972
161973
161974
161975
161976
    ** Actually, each subexpression is converted to "xN AND w" where w is
    ** the "interesting" terms of z - terms that did not originate in the
    ** ON or USING clause of a LEFT JOIN, and terms that are usable as
    ** indices.
    **
    ** This optimization also only applies if the (x1 OR x2 OR ...) term
    ** is not contained in the ON clause of a LEFT JOIN.
    ** See ticket http://sqlite.org/src/info/f2369304e4
    **
    ** 2022-02-04:  Do not push down slices of a row-value comparison.
    ** In other words, "w" or "y" may not be a slice of a vector.  Otherwise,
    ** the initialization of the right-hand operand of the vector comparison
    ** might not occur, or might occur only in an OR branch that is not
    ** taken.  dbsqlfuzz 80a9fade844b4fb43564efc972bcb2c68270f5d1.
    **
167613
167614
167615
167616
167617
167618
167619
167620
167621
167622
167623
167624
167625
167626
167627
    }else{
      pNew->nOut = nOutUnadjusted;
    }

    if( (pNew->wsFlags & WHERE_TOP_LIMIT)==0
     && pNew->u.btree.nEq<pProbe->nColumn
     && (pNew->u.btree.nEq<pProbe->nKeyCol ||
           pProbe->idxType!=SQLITE_IDXTYPE_PRIMARYKEY)
    ){
      if( pNew->u.btree.nEq>3 ){
        sqlite3ProgressCheck(pParse);
      }
      whereLoopAddBtreeIndex(pBuilder, pSrc, pProbe, nInMul+nIn);
    }
    pNew->nOut = saved_nOut;







|







167935
167936
167937
167938
167939
167940
167941
167942
167943
167944
167945
167946
167947
167948
167949
    }else{
      pNew->nOut = nOutUnadjusted;
    }

    if( (pNew->wsFlags & WHERE_TOP_LIMIT)==0
     && pNew->u.btree.nEq<pProbe->nColumn
     && (pNew->u.btree.nEq<pProbe->nKeyCol ||
          (pProbe->idxType!=SQLITE_IDXTYPE_PRIMARYKEY && !pProbe->bIdxRowid))
    ){
      if( pNew->u.btree.nEq>3 ){
        sqlite3ProgressCheck(pParse);
      }
      whereLoopAddBtreeIndex(pBuilder, pSrc, pProbe, nInMul+nIn);
    }
    pNew->nOut = saved_nOut;
171071
171072
171073
171074
171075
171076
171077
171078

171079
171080
171081
171082
171083
171084
171085
    if( pWInfo->pOrderBy ){
       whereInterstageHeuristic(pWInfo);
       wherePathSolver(pWInfo, pWInfo->nRowOut<0 ? 1 : pWInfo->nRowOut+1);
       if( db->mallocFailed ) goto whereBeginError;
    }

    /* TUNING:  Assume that a DISTINCT clause on a subquery reduces
    ** the output size by a factor of 8 (LogEst -30).

    */
    if( (pWInfo->wctrlFlags & WHERE_WANT_DISTINCT)!=0 ){
      WHERETRACE(0x0080,("nRowOut reduced from %d to %d due to DISTINCT\n",
                         pWInfo->nRowOut, pWInfo->nRowOut-30));
      pWInfo->nRowOut -= 30;
    }








|
>







171393
171394
171395
171396
171397
171398
171399
171400
171401
171402
171403
171404
171405
171406
171407
171408
    if( pWInfo->pOrderBy ){
       whereInterstageHeuristic(pWInfo);
       wherePathSolver(pWInfo, pWInfo->nRowOut<0 ? 1 : pWInfo->nRowOut+1);
       if( db->mallocFailed ) goto whereBeginError;
    }

    /* TUNING:  Assume that a DISTINCT clause on a subquery reduces
    ** the output size by a factor of 8 (LogEst -30).  Search for
    ** tag-20250414a to see other cases.
    */
    if( (pWInfo->wctrlFlags & WHERE_WANT_DISTINCT)!=0 ){
      WHERETRACE(0x0080,("nRowOut reduced from %d to %d due to DISTINCT\n",
                         pWInfo->nRowOut, pWInfo->nRowOut-30));
      pWInfo->nRowOut -= 30;
    }

188063
188064
188065
188066
188067
188068
188069







188070
188071
188072
188073
188074
188075
188076
**    May you share freely, never taking more than you give.
**
******************************************************************************
**
*/
#ifndef _FTSINT_H
#define _FTSINT_H








#if !defined(NDEBUG) && !defined(SQLITE_DEBUG)
# define NDEBUG 1
#endif

/* FTS3/FTS4 require virtual tables */
#ifdef SQLITE_OMIT_VIRTUALTABLE







>
>
>
>
>
>
>







188386
188387
188388
188389
188390
188391
188392
188393
188394
188395
188396
188397
188398
188399
188400
188401
188402
188403
188404
188405
188406
**    May you share freely, never taking more than you give.
**
******************************************************************************
**
*/
#ifndef _FTSINT_H
#define _FTSINT_H

/* #include <assert.h> */
/* #include <stdlib.h> */
/* #include <stddef.h> */
/* #include <stdio.h> */
/* #include <string.h> */
/* #include <stdarg.h> */

#if !defined(NDEBUG) && !defined(SQLITE_DEBUG)
# define NDEBUG 1
#endif

/* FTS3/FTS4 require virtual tables */
#ifdef SQLITE_OMIT_VIRTUALTABLE
189015
189016
189017
189018
189019
189020
189021
189022
189023
189024
189025
189026
189027
189028
189029
189030
189031
189032
189033
189034
/************** Continuing where we left off in fts3.c ***********************/
#if !defined(SQLITE_CORE) || defined(SQLITE_ENABLE_FTS3)

#if defined(SQLITE_ENABLE_FTS3) && !defined(SQLITE_CORE)
# define SQLITE_CORE 1
#endif

/* #include <assert.h> */
/* #include <stdlib.h> */
/* #include <stddef.h> */
/* #include <stdio.h> */
/* #include <string.h> */
/* #include <stdarg.h> */

/* #include "fts3.h" */
#ifndef SQLITE_CORE
/* # include "sqlite3ext.h" */
  SQLITE_EXTENSION_INIT1
#endif








<
<
<
<
<
<







189345
189346
189347
189348
189349
189350
189351






189352
189353
189354
189355
189356
189357
189358
/************** Continuing where we left off in fts3.c ***********************/
#if !defined(SQLITE_CORE) || defined(SQLITE_ENABLE_FTS3)

#if defined(SQLITE_ENABLE_FTS3) && !defined(SQLITE_CORE)
# define SQLITE_CORE 1
#endif








/* #include "fts3.h" */
#ifndef SQLITE_CORE
/* # include "sqlite3ext.h" */
  SQLITE_EXTENSION_INIT1
#endif

198967
198968
198969
198970
198971
198972
198973
198974
198975
198976
198977
198978
198979
198980
198981
  Fts3tokTable *pTab = (Fts3tokTable *)(pCursor->pVtab);
  UNUSED_PARAMETER(idxStr);
  UNUSED_PARAMETER(nVal);

  fts3tokResetCursor(pCsr);
  if( idxNum==1 ){
    const char *zByte = (const char *)sqlite3_value_text(apVal[0]);
    int nByte = sqlite3_value_bytes(apVal[0]);
    pCsr->zInput = sqlite3_malloc64(nByte+1);
    if( pCsr->zInput==0 ){
      rc = SQLITE_NOMEM;
    }else{
      if( nByte>0 ) memcpy(pCsr->zInput, zByte, nByte);
      pCsr->zInput[nByte] = 0;
      rc = pTab->pMod->xOpen(pTab->pTok, pCsr->zInput, nByte, &pCsr->pCsr);







|







199291
199292
199293
199294
199295
199296
199297
199298
199299
199300
199301
199302
199303
199304
199305
  Fts3tokTable *pTab = (Fts3tokTable *)(pCursor->pVtab);
  UNUSED_PARAMETER(idxStr);
  UNUSED_PARAMETER(nVal);

  fts3tokResetCursor(pCsr);
  if( idxNum==1 ){
    const char *zByte = (const char *)sqlite3_value_text(apVal[0]);
    sqlite3_int64 nByte = sqlite3_value_bytes(apVal[0]);
    pCsr->zInput = sqlite3_malloc64(nByte+1);
    if( pCsr->zInput==0 ){
      rc = SQLITE_NOMEM;
    }else{
      if( nByte>0 ) memcpy(pCsr->zInput, zByte, nByte);
      pCsr->zInput[nByte] = 0;
      rc = pTab->pMod->xOpen(pTab->pTok, pCsr->zInput, nByte, &pCsr->pCsr);
205947
205948
205949
205950
205951
205952
205953
205954
205955
205956
205957
205958
205959
205960
205961
205962
205963
205964
205965
205966
205967
205968
205969
205970
    case FTS3_MATCHINFO_AVGLENGTH:
    case FTS3_MATCHINFO_LENGTH:
    case FTS3_MATCHINFO_LCS:
      nVal = pInfo->nCol;
      break;

    case FTS3_MATCHINFO_LHITS:
      nVal = pInfo->nCol * pInfo->nPhrase;
      break;

    case FTS3_MATCHINFO_LHITS_BM:
      nVal = pInfo->nPhrase * ((pInfo->nCol + 31) / 32);
      break;

    default:
      assert( cArg==FTS3_MATCHINFO_HITS );
      nVal = pInfo->nCol * pInfo->nPhrase * 3;
      break;
  }

  return nVal;
}

static int fts3MatchinfoSelectDoctotal(







|



|




|







206271
206272
206273
206274
206275
206276
206277
206278
206279
206280
206281
206282
206283
206284
206285
206286
206287
206288
206289
206290
206291
206292
206293
206294
    case FTS3_MATCHINFO_AVGLENGTH:
    case FTS3_MATCHINFO_LENGTH:
    case FTS3_MATCHINFO_LCS:
      nVal = pInfo->nCol;
      break;

    case FTS3_MATCHINFO_LHITS:
      nVal = (size_t)pInfo->nCol * pInfo->nPhrase;
      break;

    case FTS3_MATCHINFO_LHITS_BM:
      nVal = (size_t)pInfo->nPhrase * ((pInfo->nCol + 31) / 32);
      break;

    default:
      assert( cArg==FTS3_MATCHINFO_HITS );
      nVal = (size_t)pInfo->nCol * pInfo->nPhrase * 3;
      break;
  }

  return nVal;
}

static int fts3MatchinfoSelectDoctotal(
207514
207515
207516
207517
207518
207519
207520
207521
207522
207523
207524
207525
207526
207527
207528
207529
** Support for JSON-5 extensions was added with version 3.42.0 (2023-05-16).
** All generated JSON text still conforms strictly to RFC-8259, but text
** with JSON-5 extensions is accepted as input.
**
** Beginning with version 3.45.0 (circa 2024-01-01), these routines also
** accept BLOB values that have JSON encoded using a binary representation
** called "JSONB".  The name JSONB comes from PostgreSQL, however the on-disk
** format SQLite JSONB is completely different and incompatible with
** PostgreSQL JSONB.
**
** Decoding and interpreting JSONB is still O(N) where N is the size of
** the input, the same as text JSON.  However, the constant of proportionality
** for JSONB is much smaller due to faster parsing.  The size of each
** element in JSONB is encoded in its header, so there is no need to search
** for delimiters using persnickety syntax rules.  JSONB seems to be about
** 3x faster than text JSON as a result.  JSONB is also tends to be slightly







|
|







207838
207839
207840
207841
207842
207843
207844
207845
207846
207847
207848
207849
207850
207851
207852
207853
** Support for JSON-5 extensions was added with version 3.42.0 (2023-05-16).
** All generated JSON text still conforms strictly to RFC-8259, but text
** with JSON-5 extensions is accepted as input.
**
** Beginning with version 3.45.0 (circa 2024-01-01), these routines also
** accept BLOB values that have JSON encoded using a binary representation
** called "JSONB".  The name JSONB comes from PostgreSQL, however the on-disk
** format for SQLite-JSONB is completely different and incompatible with
** PostgreSQL-JSONB.
**
** Decoding and interpreting JSONB is still O(N) where N is the size of
** the input, the same as text JSON.  However, the constant of proportionality
** for JSONB is much smaller due to faster parsing.  The size of each
** element in JSONB is encoded in its header, so there is no need to search
** for delimiters using persnickety syntax rules.  JSONB seems to be about
** 3x faster than text JSON as a result.  JSONB is also tends to be slightly
207572
207573
207574
207575
207576
207577
207578
207579
207580
207581
207582
207583
207584
207585
207586
207587
207588
207589
207590
207591
207592
207593
207594
207595
207596
**        12           1 byte (0-255)                2
**        13           2 byte (0-65535)              3
**        14           4 byte (0-4294967295)         5
**        15           8 byte (0-1.8e19)             9
**
** The payload size need not be expressed in its minimal form.  For example,
** if the payload size is 10, the size can be expressed in any of 5 different
** ways: (1) (X>>4)==10, (2) (X>>4)==12 following by on 0x0a byte,
** (3) (X>>4)==13 followed by 0x00 and 0x0a, (4) (X>>4)==14 followed by
** 0x00 0x00 0x00 0x0a, or (5) (X>>4)==15 followed by 7 bytes of 0x00 and
** a single byte of 0x0a.  The shorter forms are preferred, of course, but
** sometimes when generating JSONB, the payload size is not known in advance
** and it is convenient to reserve sufficient header space to cover the
** largest possible payload size and then come back later and patch up
** the size when it becomes known, resulting in a non-minimal encoding.
**
** The value (X>>4)==15 is not actually used in the current implementation
** (as SQLite is currently unable handle BLOBs larger than about 2GB)
** but is included in the design to allow for future enhancements.
**
** The payload follows the header.  NULL, TRUE, and FALSE have no payload and
** their payload size must always be zero.  The payload for INT, INT5,
** FLOAT, FLOAT5, TEXT, TEXTJ, TEXT5, and TEXTROW is text.  Note that the
** "..." or '...' delimiters are omitted from the various text encodings.
** The payload for ARRAY and OBJECT is a list of additional elements that







|









|







207896
207897
207898
207899
207900
207901
207902
207903
207904
207905
207906
207907
207908
207909
207910
207911
207912
207913
207914
207915
207916
207917
207918
207919
207920
**        12           1 byte (0-255)                2
**        13           2 byte (0-65535)              3
**        14           4 byte (0-4294967295)         5
**        15           8 byte (0-1.8e19)             9
**
** The payload size need not be expressed in its minimal form.  For example,
** if the payload size is 10, the size can be expressed in any of 5 different
** ways: (1) (X>>4)==10, (2) (X>>4)==12 following by one 0x0a byte,
** (3) (X>>4)==13 followed by 0x00 and 0x0a, (4) (X>>4)==14 followed by
** 0x00 0x00 0x00 0x0a, or (5) (X>>4)==15 followed by 7 bytes of 0x00 and
** a single byte of 0x0a.  The shorter forms are preferred, of course, but
** sometimes when generating JSONB, the payload size is not known in advance
** and it is convenient to reserve sufficient header space to cover the
** largest possible payload size and then come back later and patch up
** the size when it becomes known, resulting in a non-minimal encoding.
**
** The value (X>>4)==15 is not actually used in the current implementation
** (as SQLite is currently unable to handle BLOBs larger than about 2GB)
** but is included in the design to allow for future enhancements.
**
** The payload follows the header.  NULL, TRUE, and FALSE have no payload and
** their payload size must always be zero.  The payload for INT, INT5,
** FLOAT, FLOAT5, TEXT, TEXTJ, TEXT5, and TEXTROW is text.  Note that the
** "..." or '...' delimiters are omitted from the various text encodings.
** The payload for ARRAY and OBJECT is a list of additional elements that
208656
208657
208658
208659
208660
208661
208662
208663
208664
208665
208666
208667
208668
208669
208670
  const void *aPayload
){
  if( jsonBlobExpand(pParse, pParse->nBlob+szPayload+9) ) return;
  jsonBlobAppendNode(pParse, eType, szPayload, aPayload);
}


/* Append an node type byte together with the payload size and
** possibly also the payload.
**
** If aPayload is not NULL, then it is a pointer to the payload which
** is also appended.  If aPayload is NULL, the pParse->aBlob[] array
** is resized (if necessary) so that it is big enough to hold the
** payload, but the payload is not appended and pParse->nBlob is left
** pointing to where the first byte of payload will eventually be.







|







208980
208981
208982
208983
208984
208985
208986
208987
208988
208989
208990
208991
208992
208993
208994
  const void *aPayload
){
  if( jsonBlobExpand(pParse, pParse->nBlob+szPayload+9) ) return;
  jsonBlobAppendNode(pParse, eType, szPayload, aPayload);
}


/* Append a node type byte together with the payload size and
** possibly also the payload.
**
** If aPayload is not NULL, then it is a pointer to the payload which
** is also appended.  If aPayload is NULL, the pParse->aBlob[] array
** is resized (if necessary) so that it is big enough to hold the
** payload, but the payload is not appended and pParse->nBlob is left
** pointing to where the first byte of payload will eventually be.
209990
209991
209992
209993
209994
209995
209996












































































209997
209998
209999
210000
210001
210002
210003
  nBlob = pParse->nBlob;
  pParse->nBlob = pParse->nBlobAlloc;
  (void)jsonbPayloadSize(pParse, iRoot, &sz);
  pParse->nBlob = nBlob;
  sz += pParse->delta;
  pParse->delta += jsonBlobChangePayloadSize(pParse, iRoot, sz);
}













































































/*
** Modify the JSONB blob at pParse->aBlob by removing nDel bytes of
** content beginning at iDel, and replacing them with nIns bytes of
** content given by aIns.
**
** nDel may be zero, in which case no bytes are removed.  But iDel is







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







210314
210315
210316
210317
210318
210319
210320
210321
210322
210323
210324
210325
210326
210327
210328
210329
210330
210331
210332
210333
210334
210335
210336
210337
210338
210339
210340
210341
210342
210343
210344
210345
210346
210347
210348
210349
210350
210351
210352
210353
210354
210355
210356
210357
210358
210359
210360
210361
210362
210363
210364
210365
210366
210367
210368
210369
210370
210371
210372
210373
210374
210375
210376
210377
210378
210379
210380
210381
210382
210383
210384
210385
210386
210387
210388
210389
210390
210391
210392
210393
210394
210395
210396
210397
210398
210399
210400
210401
210402
210403
  nBlob = pParse->nBlob;
  pParse->nBlob = pParse->nBlobAlloc;
  (void)jsonbPayloadSize(pParse, iRoot, &sz);
  pParse->nBlob = nBlob;
  sz += pParse->delta;
  pParse->delta += jsonBlobChangePayloadSize(pParse, iRoot, sz);
}

/*
** If the JSONB at aIns[0..nIns-1] can be expanded (by denormalizing the
** size field) by d bytes, then write the expansion into aOut[] and
** return true.  In this way, an overwrite happens without changing the
** size of the JSONB, which reduces memcpy() operations and also make it
** faster and easier to update the B-Tree entry that contains the JSONB
** in the database.
**
** If the expansion of aIns[] by d bytes cannot be (easily) accomplished
** then return false.
**
** The d parameter is guaranteed to be between 1 and 8.
**
** This routine is an optimization.  A correct answer is obtained if it
** always leaves the output unchanged and returns false.
*/
static int jsonBlobOverwrite(
  u8 *aOut,                 /* Overwrite here */
  const u8 *aIns,           /* New content */
  u32 nIns,                 /* Bytes of new content */
  u32 d                     /* Need to expand new content by this much */
){
  u32 szPayload;       /* Bytes of payload */
  u32 i;               /* New header size, after expansion & a loop counter */
  u8 szHdr;            /* Size of header before expansion */

  /* Lookup table for finding the upper 4 bits of the first byte of the
  ** expanded aIns[], based on the size of the expanded aIns[] header:
  **
  **                             2     3  4     5  6  7  8     9 */
  static const u8 aType[] = { 0xc0, 0xd0, 0, 0xe0, 0, 0, 0, 0xf0 };

  if( (aIns[0]&0x0f)<=2 ) return 0;    /* Cannot enlarge NULL, true, false */
  switch( aIns[0]>>4 ){
    default: {                         /* aIns[] header size 1 */
      if( ((1<<d)&0x116)==0 ) return 0;  /* d must be 1, 2, 4, or 8 */
      i = d + 1;                         /* New hdr sz: 2, 3, 5, or 9 */
      szHdr = 1;
      break;
    }
    case 12: {                         /* aIns[] header size is 2 */
      if( ((1<<d)&0x8a)==0) return 0;    /* d must be 1, 3, or 7 */
      i = d + 2;                         /* New hdr sz: 2, 5, or 9 */
      szHdr = 2;
      break;
    }
    case 13: {                         /* aIns[] header size is 3 */
      if( d!=2 && d!=6 ) return 0;       /* d must be 2 or 6 */
      i = d + 3;                         /* New hdr sz: 5 or 9 */
      szHdr = 3;
      break;
    }
    case 14: {                         /* aIns[] header size is 5 */
      if( d!=4 ) return 0;               /* d must be 4 */
      i = 9;                             /* New hdr sz: 9 */
      szHdr = 5;
      break;
    }
    case 15: {                         /* aIns[] header size is 9 */
      return 0;                          /* No solution */
    }
  }
  assert( i>=2 && i<=9 && aType[i-2]!=0 );
  aOut[0] = (aIns[0] & 0x0f) | aType[i-2];
  memcpy(&aOut[i], &aIns[szHdr], nIns-szHdr);
  szPayload = nIns - szHdr;
  while( 1/*edit-by-break*/ ){
    i--;
    aOut[i] = szPayload & 0xff;
    if( i==1 ) break;
    szPayload >>= 8;
  }
  assert( (szPayload>>8)==0 );
  return 1;
}

/*
** Modify the JSONB blob at pParse->aBlob by removing nDel bytes of
** content beginning at iDel, and replacing them with nIns bytes of
** content given by aIns.
**
** nDel may be zero, in which case no bytes are removed.  But iDel is
210012
210013
210014
210015
210016
210017
210018





210019
210020
210021
210022
210023
210024
210025
210026
210027
210028
210029

210030

210031
210032
210033
210034
210035
210036
210037
  JsonParse *pParse,     /* The JSONB to be modified is in pParse->aBlob */
  u32 iDel,              /* First byte to be removed */
  u32 nDel,              /* Number of bytes to remove */
  const u8 *aIns,        /* Content to insert */
  u32 nIns               /* Bytes of content to insert */
){
  i64 d = (i64)nIns - (i64)nDel;





  if( d!=0 ){
    if( pParse->nBlob + d > pParse->nBlobAlloc ){
      jsonBlobExpand(pParse, pParse->nBlob+d);
      if( pParse->oom ) return;
    }
    memmove(&pParse->aBlob[iDel+nIns],
            &pParse->aBlob[iDel+nDel],
            pParse->nBlob - (iDel+nDel));
    pParse->nBlob += d;
    pParse->delta += d;
  }

  if( nIns && aIns ) memcpy(&pParse->aBlob[iDel], aIns, nIns);

}

/*
** Return the number of escaped newlines to be ignored.
** An escaped newline is a one of the following byte sequences:
**
**    0x5c 0x0a







>
>
>
>
>











>
|
>







210412
210413
210414
210415
210416
210417
210418
210419
210420
210421
210422
210423
210424
210425
210426
210427
210428
210429
210430
210431
210432
210433
210434
210435
210436
210437
210438
210439
210440
210441
210442
210443
210444
  JsonParse *pParse,     /* The JSONB to be modified is in pParse->aBlob */
  u32 iDel,              /* First byte to be removed */
  u32 nDel,              /* Number of bytes to remove */
  const u8 *aIns,        /* Content to insert */
  u32 nIns               /* Bytes of content to insert */
){
  i64 d = (i64)nIns - (i64)nDel;
  if( d<0 && d>=(-8) && aIns!=0
   && jsonBlobOverwrite(&pParse->aBlob[iDel], aIns, nIns, (int)-d)
  ){
    return;
  }
  if( d!=0 ){
    if( pParse->nBlob + d > pParse->nBlobAlloc ){
      jsonBlobExpand(pParse, pParse->nBlob+d);
      if( pParse->oom ) return;
    }
    memmove(&pParse->aBlob[iDel+nIns],
            &pParse->aBlob[iDel+nDel],
            pParse->nBlob - (iDel+nDel));
    pParse->nBlob += d;
    pParse->delta += d;
  }
  if( nIns && aIns ){
    memcpy(&pParse->aBlob[iDel], aIns, nIns);
  }
}

/*
** Return the number of escaped newlines to be ignored.
** An escaped newline is a one of the following byte sequences:
**
**    0x5c 0x0a
210786
210787
210788
210789
210790
210791
210792
210793
210794
210795
210796
210797
210798
210799
210800
  }else{
    sqlite3_result_error_nomem(ctx);
  }
  return 0;
}

/* argv[0] is a BLOB that seems likely to be a JSONB.  Subsequent
** arguments come in parse where each pair contains a JSON path and
** content to insert or set at that patch.  Do the updates
** and return the result.
**
** The specific operation is determined by eEdit, which can be one
** of JEDIT_INS, JEDIT_REPL, or JEDIT_SET.
*/
static void jsonInsertIntoBlob(







|







211193
211194
211195
211196
211197
211198
211199
211200
211201
211202
211203
211204
211205
211206
211207
  }else{
    sqlite3_result_error_nomem(ctx);
  }
  return 0;
}

/* argv[0] is a BLOB that seems likely to be a JSONB.  Subsequent
** arguments come in pairs where each pair contains a JSON path and
** content to insert or set at that patch.  Do the updates
** and return the result.
**
** The specific operation is determined by eEdit, which can be one
** of JEDIT_INS, JEDIT_REPL, or JEDIT_SET.
*/
static void jsonInsertIntoBlob(
227531
227532
227533
227534
227535
227536
227537
227538
227539

227540
227541
227542
227543
227544
227545
227546
  if( sqlite3_value_type(argv[3])!=SQLITE_BLOB
   || sqlite3_value_bytes(argv[3])!=szPage
  ){
    if( sqlite3_value_type(argv[3])==SQLITE_NULL && isInsert && pgno>1 ){
      /* "INSERT INTO dbpage($PGNO,NULL)" causes page number $PGNO and
      ** all subsequent pages to be deleted. */
      pTab->iDbTrunc = iDb;
      pgno--;
      pTab->pgnoTrunc = pgno;

    }else{
      zErr = "bad page value";
      goto update_fail;
    }
  }

  if( dbpageBeginTrans(pTab)!=SQLITE_OK ){







<
|
>







227938
227939
227940
227941
227942
227943
227944

227945
227946
227947
227948
227949
227950
227951
227952
227953
  if( sqlite3_value_type(argv[3])!=SQLITE_BLOB
   || sqlite3_value_bytes(argv[3])!=szPage
  ){
    if( sqlite3_value_type(argv[3])==SQLITE_NULL && isInsert && pgno>1 ){
      /* "INSERT INTO dbpage($PGNO,NULL)" causes page number $PGNO and
      ** all subsequent pages to be deleted. */
      pTab->iDbTrunc = iDb;

      pTab->pgnoTrunc = pgno-1;
      pgno = 1;
    }else{
      zErr = "bad page value";
      goto update_fail;
    }
  }

  if( dbpageBeginTrans(pTab)!=SQLITE_OK ){
228848
228849
228850
228851
228852
228853
228854


228855
228856
228857
228858
228859
228860
228861
  const char *zDb                 /* Name of db - "main", "temp" etc. */
){
  int rc = SQLITE_OK;

  if( pTab->nCol==0 ){
    u8 *abPK;
    assert( pTab->azCol==0 || pTab->abPK==0 );


    rc = sessionTableInfo(pSession, db, zDb,
        pTab->zName, &pTab->nCol, &pTab->nTotalCol, 0, &pTab->azCol,
        &pTab->azDflt, &pTab->aiIdx, &abPK,
        ((pSession==0 || pSession->bImplicitPK) ? &pTab->bRowid : 0)
    );
    if( rc==SQLITE_OK ){
      int i;







>
>







229255
229256
229257
229258
229259
229260
229261
229262
229263
229264
229265
229266
229267
229268
229269
229270
  const char *zDb                 /* Name of db - "main", "temp" etc. */
){
  int rc = SQLITE_OK;

  if( pTab->nCol==0 ){
    u8 *abPK;
    assert( pTab->azCol==0 || pTab->abPK==0 );
    sqlite3_free(pTab->azCol);
    pTab->abPK = 0;
    rc = sessionTableInfo(pSession, db, zDb,
        pTab->zName, &pTab->nCol, &pTab->nTotalCol, 0, &pTab->azCol,
        &pTab->azDflt, &pTab->aiIdx, &abPK,
        ((pSession==0 || pSession->bImplicitPK) ? &pTab->bRowid : 0)
    );
    if( rc==SQLITE_OK ){
      int i;
229855
229856
229857
229858
229859
229860
229861

229862

229863
229864
229865
229866
229867
229868
229869
229870
229871
229872
229873
229874
229875
229876


















229877
229878
229879
229880

229881
229882






229883

229884
229885
229886
229887
229888
229889
229890
  if( pzErrMsg ) *pzErrMsg = 0;
  if( rc==SQLITE_OK ){
    char *zExpr = 0;
    sqlite3 *db = pSession->db;
    SessionTable *pTo;            /* Table zTbl */

    /* Locate and if necessary initialize the target table object */

    rc = sessionFindTable(pSession, zTbl, &pTo);

    if( pTo==0 ) goto diff_out;
    if( sessionInitTable(pSession, pTo, pSession->db, pSession->zDb) ){
      rc = pSession->rc;
      goto diff_out;
    }

    /* Check the table schemas match */
    if( rc==SQLITE_OK ){
      int bHasPk = 0;
      int bMismatch = 0;
      int nCol;                   /* Columns in zFrom.zTbl */
      int bRowid = 0;
      u8 *abPK;
      const char **azCol = 0;


















      rc = sessionTableInfo(0, db, zFrom, zTbl,
          &nCol, 0, 0, &azCol, 0, 0, &abPK,
          pSession->bImplicitPK ? &bRowid : 0
      );

      if( rc==SQLITE_OK ){
        if( pTo->nCol!=nCol ){






          bMismatch = 1;

        }else{
          int i;
          for(i=0; i<nCol; i++){
            if( pTo->abPK[i]!=abPK[i] ) bMismatch = 1;
            if( sqlite3_stricmp(azCol[i], pTo->azCol[i]) ) bMismatch = 1;
            if( abPK[i] ) bHasPk = 1;
          }







>

>










|

|

>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
|
|
|
|
>


>
>
>
>
>
>
|
>







230264
230265
230266
230267
230268
230269
230270
230271
230272
230273
230274
230275
230276
230277
230278
230279
230280
230281
230282
230283
230284
230285
230286
230287
230288
230289
230290
230291
230292
230293
230294
230295
230296
230297
230298
230299
230300
230301
230302
230303
230304
230305
230306
230307
230308
230309
230310
230311
230312
230313
230314
230315
230316
230317
230318
230319
230320
230321
230322
230323
230324
230325
230326
230327
  if( pzErrMsg ) *pzErrMsg = 0;
  if( rc==SQLITE_OK ){
    char *zExpr = 0;
    sqlite3 *db = pSession->db;
    SessionTable *pTo;            /* Table zTbl */

    /* Locate and if necessary initialize the target table object */
    pSession->bAutoAttach++;
    rc = sessionFindTable(pSession, zTbl, &pTo);
    pSession->bAutoAttach--;
    if( pTo==0 ) goto diff_out;
    if( sessionInitTable(pSession, pTo, pSession->db, pSession->zDb) ){
      rc = pSession->rc;
      goto diff_out;
    }

    /* Check the table schemas match */
    if( rc==SQLITE_OK ){
      int bHasPk = 0;
      int bMismatch = 0;
      int nCol = 0;               /* Columns in zFrom.zTbl */
      int bRowid = 0;
      u8 *abPK = 0;
      const char **azCol = 0;
      char *zDbExists = 0;

      /* Check that database zFrom is attached.  */
      zDbExists = sqlite3_mprintf("SELECT * FROM %Q.sqlite_schema", zFrom);
      if( zDbExists==0 ){
        rc = SQLITE_NOMEM;
      }else{
        sqlite3_stmt *pDbExists = 0;
        rc = sqlite3_prepare_v2(db, zDbExists, -1, &pDbExists, 0);
        if( rc==SQLITE_ERROR ){
          rc = SQLITE_OK;
          nCol = -1;
        }
        sqlite3_finalize(pDbExists);
        sqlite3_free(zDbExists);
      }

      if( rc==SQLITE_OK && nCol==0 ){
        rc = sessionTableInfo(0, db, zFrom, zTbl,
            &nCol, 0, 0, &azCol, 0, 0, &abPK,
            pSession->bImplicitPK ? &bRowid : 0
        );
      }
      if( rc==SQLITE_OK ){
        if( pTo->nCol!=nCol ){
          if( nCol<=0 ){
            rc = SQLITE_SCHEMA;
            if( pzErrMsg ){
              *pzErrMsg = sqlite3_mprintf("no such table: %s.%s", zFrom, zTbl);
            }
          }else{
            bMismatch = 1;
          }
        }else{
          int i;
          for(i=0; i<nCol; i++){
            if( pTo->abPK[i]!=abPK[i] ) bMismatch = 1;
            if( sqlite3_stricmp(azCol[i], pTo->azCol[i]) ) bMismatch = 1;
            if( abPK[i] ) bHasPk = 1;
          }
241867
241868
241869
241870
241871
241872
241873
241874

241875
241876
241877
241878
241879
241880
241881
        char c = (char)p->p[i];
        if( c<'0' || c>'9' ){
          sqlite3Fts5ParseError(
              pParse, "expected integer, got \"%.*s\"", p->n, p->p
              );
          return;
        }
        nNear = nNear * 10 + (p->p[i] - '0');

      }
    }else{
      nNear = FTS5_DEFAULT_NEARDIST;
    }
    pNear->nNear = nNear;
  }
}







|
>







242304
242305
242306
242307
242308
242309
242310
242311
242312
242313
242314
242315
242316
242317
242318
242319
        char c = (char)p->p[i];
        if( c<'0' || c>'9' ){
          sqlite3Fts5ParseError(
              pParse, "expected integer, got \"%.*s\"", p->n, p->p
              );
          return;
        }
        if( nNear<214748363 ) nNear = nNear * 10 + (p->p[i] - '0');
        /*  ^^^^^^^^^^^^^^^---  Prevent integer overflow */
      }
    }else{
      nNear = FTS5_DEFAULT_NEARDIST;
    }
    pNear->nNear = nNear;
  }
}
256773
256774
256775
256776
256777
256778
256779
256780
256781
256782
256783
256784
256785
256786
256787
static void fts5SourceIdFunc(
  sqlite3_context *pCtx,          /* Function call context */
  int nArg,                       /* Number of args */
  sqlite3_value **apUnused        /* Function arguments */
){
  assert( nArg==0 );
  UNUSED_PARAM2(nArg, apUnused);
  sqlite3_result_text(pCtx, "fts5: 2025-03-16 00:13:29 18bda13e197e4b4ec7464b3e70012f71edc05f73d8b14bb48bad452f81c7e185", -1, SQLITE_TRANSIENT);
}

/*
** Implementation of fts5_locale(LOCALE, TEXT) function.
**
** If parameter LOCALE is NULL, or a zero-length string, then a copy of
** TEXT is returned. Otherwise, both LOCALE and TEXT are interpreted as







|







257211
257212
257213
257214
257215
257216
257217
257218
257219
257220
257221
257222
257223
257224
257225
static void fts5SourceIdFunc(
  sqlite3_context *pCtx,          /* Function call context */
  int nArg,                       /* Number of args */
  sqlite3_value **apUnused        /* Function arguments */
){
  assert( nArg==0 );
  UNUSED_PARAM2(nArg, apUnused);
  sqlite3_result_text(pCtx, "fts5: 2025-04-15 21:59:38 d22475b81c4e26ccc50f3b5626d43b32f7a2de34e5a764539554665bdda735d5", -1, SQLITE_TRANSIENT);
}

/*
** Implementation of fts5_locale(LOCALE, TEXT) function.
**
** If parameter LOCALE is NULL, or a zero-length string, then a copy of
** TEXT is returned. Otherwise, both LOCALE and TEXT are interpreted as
260836
260837
260838
260839
260840
260841
260842
260843
260844
260845
260846
260847
260848
260849
260850
    for(; i<128 && i<n; i++){
      aAscii[i] = (u8)bToken;
    }
    iTbl++;
  }
  aAscii[0] = 0;                  /* 0x00 is never a token character */
}


/*
** 2015 May 30
**
** The author disclaims copyright to this source code.  In place of
** a legal notice, here is a blessing:
**







<







261274
261275
261276
261277
261278
261279
261280

261281
261282
261283
261284
261285
261286
261287
    for(; i<128 && i<n; i++){
      aAscii[i] = (u8)bToken;
    }
    iTbl++;
  }
  aAscii[0] = 0;                  /* 0x00 is never a token character */
}


/*
** 2015 May 30
**
** The author disclaims copyright to this source code.  In place of
** a legal notice, here is a blessing:
**
Changes to extsrc/sqlite3.h.
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
** The SQLITE_VERSION_NUMBER for any given release of SQLite will also
** be larger than the release from which it is derived.  Either Y will
** be held constant and Z will be incremented or else Y will be incremented
** and Z will be reset to zero.
**
** Since [version 3.6.18] ([dateof:3.6.18]),
** SQLite source code has been stored in the
** <a href="http://www.fossil-scm.org/">Fossil configuration management
** system</a>.  ^The SQLITE_SOURCE_ID macro evaluates to
** a string which identifies a particular check-in of SQLite
** within its configuration management system.  ^The SQLITE_SOURCE_ID
** string contains the date and time of the check-in (UTC) and a SHA1
** or SHA3-256 hash of the entire source tree.  If the source code has
** been edited in any way since it was last checked in, then the last
** four hexadecimal digits of the hash may be modified.
**
** See also: [sqlite3_libversion()],
** [sqlite3_libversion_number()], [sqlite3_sourceid()],
** [sqlite_version()] and [sqlite_source_id()].
*/
#define SQLITE_VERSION        "3.50.0"
#define SQLITE_VERSION_NUMBER 3050000
#define SQLITE_SOURCE_ID      "2025-03-16 00:13:29 18bda13e197e4b4ec7464b3e70012f71edc05f73d8b14bb48bad452f81c7e185"

/*
** CAPI3REF: Run-Time Library Version Numbers
** KEYWORDS: sqlite3_version sqlite3_sourceid
**
** These interfaces provide the same information as the [SQLITE_VERSION],
** [SQLITE_VERSION_NUMBER], and [SQLITE_SOURCE_ID] C preprocessor macros







|














|







129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
** The SQLITE_VERSION_NUMBER for any given release of SQLite will also
** be larger than the release from which it is derived.  Either Y will
** be held constant and Z will be incremented or else Y will be incremented
** and Z will be reset to zero.
**
** Since [version 3.6.18] ([dateof:3.6.18]),
** SQLite source code has been stored in the
** <a href="http://fossil-scm.org/">Fossil configuration management
** system</a>.  ^The SQLITE_SOURCE_ID macro evaluates to
** a string which identifies a particular check-in of SQLite
** within its configuration management system.  ^The SQLITE_SOURCE_ID
** string contains the date and time of the check-in (UTC) and a SHA1
** or SHA3-256 hash of the entire source tree.  If the source code has
** been edited in any way since it was last checked in, then the last
** four hexadecimal digits of the hash may be modified.
**
** See also: [sqlite3_libversion()],
** [sqlite3_libversion_number()], [sqlite3_sourceid()],
** [sqlite_version()] and [sqlite_source_id()].
*/
#define SQLITE_VERSION        "3.50.0"
#define SQLITE_VERSION_NUMBER 3050000
#define SQLITE_SOURCE_ID      "2025-04-15 21:59:38 d22475b81c4e26ccc50f3b5626d43b32f7a2de34e5a764539554665bdda735d5"

/*
** CAPI3REF: Run-Time Library Version Numbers
** KEYWORDS: sqlite3_version sqlite3_sourceid
**
** These interfaces provide the same information as the [SQLITE_VERSION],
** [SQLITE_VERSION_NUMBER], and [SQLITE_SOURCE_ID] C preprocessor macros
11625
11626
11627
11628
11629
11630
11631

11632
11633
11634
11635
11636
11637
11638
11639
11640
** </ul>
**
** To clarify, if this function is called and then a changeset constructed
** using [sqlite3session_changeset()], then after applying that changeset to
** database zFrom the contents of the two compatible tables would be
** identical.
**

** It an error if database zFrom does not exist or does not contain the
** required compatible table.
**
** If the operation is successful, SQLITE_OK is returned. Otherwise, an SQLite
** error code. In this case, if argument pzErrMsg is not NULL, *pzErrMsg
** may be set to point to a buffer containing an English language error
** message. It is the responsibility of the caller to free this buffer using
** sqlite3_free().
*/







>
|
|







11625
11626
11627
11628
11629
11630
11631
11632
11633
11634
11635
11636
11637
11638
11639
11640
11641
** </ul>
**
** To clarify, if this function is called and then a changeset constructed
** using [sqlite3session_changeset()], then after applying that changeset to
** database zFrom the contents of the two compatible tables would be
** identical.
**
** Unless the call to this function is a no-op as described above, it is an
** error if database zFrom does not exist or does not contain the required
** compatible table.
**
** If the operation is successful, SQLITE_OK is returned. Otherwise, an SQLite
** error code. In this case, if argument pzErrMsg is not NULL, *pzErrMsg
** may be set to point to a buffer containing an English language error
** message. It is the responsibility of the caller to free this buffer using
** sqlite3_free().
*/
Changes to skins/blitz/footer.txt.
1
2
3
4
5
6
7
8
9
10
  </div> <!-- end div container -->
</div> <!-- end div middle max-full-width -->
<footer>
  <div class="container">
    <div class="pull-right">
      <a href="https://www.fossil-scm.org/">Fossil $release_version $manifest_version $manifest_date</a>
    </div>
    This page was generated in about <th1>puts [expr {([utime]+[stime]+1000)/1000*0.001}]</th1>s
  </div>
</footer>





|




1
2
3
4
5
6
7
8
9
10
  </div> <!-- end div container -->
</div> <!-- end div middle max-full-width -->
<footer>
  <div class="container">
    <div class="pull-right">
      <a href="https://fossil-scm.org/">Fossil $release_version $manifest_version $manifest_date</a>
    </div>
    This page was generated in about <th1>puts [expr {([utime]+[stime]+1000)/1000*0.001}]</th1>s
  </div>
</footer>
Changes to skins/darkmode/footer.txt.
1
2
3
4
5
6
7
8
<footer>
  <div class="container">
    <div class="pull-right">
      <a href="https://www.fossil-scm.org/">Fossil $release_version $manifest_version $manifest_date</a>
    </div>
    This page was generated in about <th1>puts [expr {([utime]+[stime]+1000)/1000*0.001}]</th1>s
  </div>
</footer>



|




1
2
3
4
5
6
7
8
<footer>
  <div class="container">
    <div class="pull-right">
      <a href="https://fossil-scm.org/">Fossil $release_version $manifest_version $manifest_date</a>
    </div>
    This page was generated in about <th1>puts [expr {([utime]+[stime]+1000)/1000*0.001}]</th1>s
  </div>
</footer>
Changes to skins/default/header.txt.
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
        set logourl $baseurl
      }
      return $logourl
    }
    set logourl [getLogoUrl $baseurl]
    </th1>
    <a href="$logourl">
      <img src="$logo_image_url" border="0" alt="$project_name">
    </a>
  </div>
  <div class="title">
    <h1>$<project_name></h1>
    <span class="page-title">$<title></span>
  </div>
  <div class="status">







|







26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
        set logourl $baseurl
      }
      return $logourl
    }
    set logourl [getLogoUrl $baseurl]
    </th1>
    <a href="$logourl">
      <img src="$logo_image_url" border="0" alt="$<project_name>">
    </a>
  </div>
  <div class="title">
    <h1>$<project_name></h1>
    <span class="page-title">$<title></span>
  </div>
  <div class="status">
Changes to skins/eagle/footer.txt.
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
  }
  proc getVersion { version } {
    set length [string length $version]
    return [string range $version 1 [expr {$length - 2}]]
  }
  set version [getVersion $manifest_version]
  set tclVersion [getTclVersion]
  set fossilUrl https://www.fossil-scm.org
  set fossilDate [string range $manifest_date 0 9]T[string range $manifest_date 11 end]
  </th1>
  This page was generated in about
  <th1>puts [expr {([utime]+[stime]+1000)/1000*0.001}]</th1>s by
  <a href="$fossilUrl/">Fossil</a>
  version $release_version $tclVersion
  <a href="$fossilUrl/index.html/info/$version">$manifest_version</a>







|







8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
  }
  proc getVersion { version } {
    set length [string length $version]
    return [string range $version 1 [expr {$length - 2}]]
  }
  set version [getVersion $manifest_version]
  set tclVersion [getTclVersion]
  set fossilUrl https://fossil-scm.org
  set fossilDate [string range $manifest_date 0 9]T[string range $manifest_date 11 end]
  </th1>
  This page was generated in about
  <th1>puts [expr {([utime]+[stime]+1000)/1000*0.001}]</th1>s by
  <a href="$fossilUrl/">Fossil</a>
  version $release_version $tclVersion
  <a href="$fossilUrl/index.html/info/$version">$manifest_version</a>
Changes to skins/eagle/header.txt.
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
      set logourl [getLogoUrl $baseurl]
    } else {
      # Link logo to the top of the current repo
      set logourl $baseurl
    }
    </th1>
    <a href="$logourl">
      <img src="$logo_image_url" border="0" alt="$project_name">
    </a>
  </div>
  <div class="title">$<title></div>
  <div class="status"><nobr><th1>
     if {[info exists login]} {
       puts "Logged in as $login"
     } else {







|







63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
      set logourl [getLogoUrl $baseurl]
    } else {
      # Link logo to the top of the current repo
      set logourl $baseurl
    }
    </th1>
    <a href="$logourl">
      <img src="$logo_image_url" border="0" alt="$<project_name>">
    </a>
  </div>
  <div class="title">$<title></div>
  <div class="status"><nobr><th1>
     if {[info exists login]} {
       puts "Logged in as $login"
     } else {
Changes to skins/original/footer.txt.
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
  }
  proc getVersion { version } {
    set length [string length $version]
    return [string range $version 1 [expr {$length - 2}]]
  }
  set version [getVersion $manifest_version]
  set tclVersion [getTclVersion]
  set fossilUrl https://www.fossil-scm.org
  set fossilDate [string range $manifest_date 0 9]T[string range $manifest_date 11 end]
  </th1>
  This page was generated in about
  <th1>puts [expr {([utime]+[stime]+1000)/1000*0.001}]</th1>s by
  <a href="$fossilUrl/">Fossil</a>
  version $release_version $tclVersion
  <a href="$fossilUrl/index.html/info/$version">$manifest_version</a>







|







8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
  }
  proc getVersion { version } {
    set length [string length $version]
    return [string range $version 1 [expr {$length - 2}]]
  }
  set version [getVersion $manifest_version]
  set tclVersion [getTclVersion]
  set fossilUrl https://fossil-scm.org
  set fossilDate [string range $manifest_date 0 9]T[string range $manifest_date 11 end]
  </th1>
  This page was generated in about
  <th1>puts [expr {([utime]+[stime]+1000)/1000*0.001}]</th1>s by
  <a href="$fossilUrl/">Fossil</a>
  version $release_version $tclVersion
  <a href="$fossilUrl/index.html/info/$version">$manifest_version</a>
Changes to skins/original/header.txt.
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
        set logourl $baseurl
      }
      return $logourl
    }
    set logourl [getLogoUrl $baseurl]
    </th1>
    <a href="$logourl">
      <img src="$logo_image_url" border="0" alt="$project_name">
    </a>
  </div>
  <div class="title">$<title></div>
  <div class="status"><nobr><th1>
     if {[info exists login]} {
       puts "Logged in as $login"
     } else {







|







57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
        set logourl $baseurl
      }
      return $logourl
    }
    set logourl [getLogoUrl $baseurl]
    </th1>
    <a href="$logourl">
      <img src="$logo_image_url" border="0" alt="$<project_name>">
    </a>
  </div>
  <div class="title">$<title></div>
  <div class="status"><nobr><th1>
     if {[info exists login]} {
       puts "Logged in as $login"
     } else {
Changes to skins/xekri/css.txt.
18
19
20
21
22
23
24

25
26
27
28
29
30
31
  color: #eee;
  font-family: Monospace;
  font-size: 1em;
  min-height: 100%;
}

body {

  margin: 0;
  padding: 0;
  text-size-adjust: none;
}

a {
  color: #40a0ff;







>







18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
  color: #eee;
  font-family: Monospace;
  font-size: 1em;
  min-height: 100%;
}

body {
  background-color: #333;
  margin: 0;
  padding: 0;
  text-size-adjust: none;
}

a {
  color: #40a0ff;
880
881
882
883
884
885
886




































































































887
888
889
890
891
892
893
  white-space:  nowrap;
}

/* the format for the timeline version links */
a.timelineHistLink {
}






































































































/**************************************
 * User Edit
 */

/* layout definition for the capabilities box on the user edit detail page */
div.ueditCapBox {







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
  white-space:  nowrap;
}

/* the format for the timeline version links */
a.timelineHistLink {
}

/* Timeline graph style taken from Ardoise, with
** minor adjustments (2025-03-28) */
.tl-canvas {
  margin: 0 6px 0 10px
}
.tl-rail {
  width: 18px
}
.tl-mergeoffset {
  width: 2px
}
.tl-nodemark {
  margin-top: .8em
}
.tl-node {
  width: 10px;
  height: 10px;
  border: 1px solid #bbb;
  background: #111;
  cursor: pointer
}
.tl-node.leaf:after {
  content: '';
  position: absolute;
  top: 3px;
  left: 3px;
  width: 4px;
  height: 4px;
  background: #bbb
}
.tl-node.closed-leaf svg {
  position: absolute;
  top: 0px;
  left: 0px;
  width: 10px;
  height: 10px;
  color: #bbb;
}
.tl-node.sel:after {
  content: '';
  position: absolute;
  top: 1px;
  left: 1px;
  width: 8px;
  height: 8px;
  background: #ff8000
}
.tl-arrow {
  width: 0;
  height: 0;
  transform: scale(.999);
  border: 0 solid transparent
}
.tl-arrow.u {
  margin-top: -1px;
  border-width: 0 3px;
  border-bottom: 7px solid
}
.tl-arrow.u.sm {
  border-bottom: 5px solid #bbb
}
.tl-line {
  background: #bbb;
  width: 2px
}
.tl-arrow.merge {
  height: 1px;
  border-width: 2px 0
}
.tl-arrow.merge.l {
  border-right: 3px solid #bbb
}
.tl-arrow.merge.r {
  border-left: 3px solid #bbb
}
.tl-line.merge {
  width: 1px
}
.tl-arrow.cherrypick {
  height: 1px;
  border-width: 2px 0;
}
.tl-arrow.cherrypick.l {
  border-right: 3px solid #bbb;
}
.tl-arrow.cherrypick.r {
  border-left: 3px solid #bbb;
}
.tl-line.cherrypick.h {
  width: 0px;
  border-top: 1px dashed #bbb;
  border-left: 0px dashed #bbb;
  background: rgba(255,255,255,0);
}
.tl-line.cherrypick.v {
  width: 0px;
  border-top: 0px dashed #bbb;
  border-left: 1px dashed #bbb;
  background: rgba(255,255,255,0);
}

/**************************************
 * User Edit
 */

/* layout definition for the capabilities box on the user edit detail page */
div.ueditCapBox {
Changes to skins/xekri/header.txt.
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
      set logourl [getLogoUrl $baseurl]
    } else {
      # Link logo to the top of the current repo
      set logourl $baseurl
    }
    </th1>
    <a href="$logourl">
      <img src="$logo_image_url" border="0" alt="$project_name">
    </a>
  </div>
  <div class="title">$<title></div>
  <div class="status"><nobr>
    <th1>
      if {[info exists login]} {
        puts "Logged in as $login"







|







63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
      set logourl [getLogoUrl $baseurl]
    } else {
      # Link logo to the top of the current repo
      set logourl $baseurl
    }
    </th1>
    <a href="$logourl">
      <img src="$logo_image_url" border="0" alt="$<project_name>">
    </a>
  </div>
  <div class="title">$<title></div>
  <div class="status"><nobr>
    <th1>
      if {[info exists login]} {
        puts "Logged in as $login"
Changes to src/add.c.
894
895
896
897
898
899
900
901

902
903
904
905
906
907
908
909
910
911
912
913
914
915
916






917
918
919
920
921
922
923
**
** The original name of the file is zOrig.  The new filename is zNew.
*/
static void mv_one_file(
  int vid,
  const char *zOrig,
  const char *zNew,
  int dryRunFlag

){
  int x = db_int(-1, "SELECT deleted FROM vfile WHERE pathname=%Q %s",
                         zNew, filename_collation());
  if( x>=0 ){
    if( x==0 ){
      if( !filenames_are_case_sensitive() && fossil_stricmp(zOrig,zNew)==0 ){
        /* Case change only */
      }else{
        fossil_fatal("cannot rename '%s' to '%s' since another file named '%s'"
                     " is currently under management", zOrig, zNew, zNew);
      }
    }else{
      fossil_fatal("cannot rename '%s' to '%s' since the delete of '%s' has "
                   "not yet been committed", zOrig, zNew, zNew);
    }






  }
  fossil_print("RENAME %s %s\n", zOrig, zNew);
  if( !dryRunFlag ){
    db_multi_exec(
      "UPDATE vfile SET pathname='%q' WHERE pathname='%q' %s AND vid=%d",
      zNew, zOrig, filename_collation(), vid
    );







|
>















>
>
>
>
>
>







894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
**
** The original name of the file is zOrig.  The new filename is zNew.
*/
static void mv_one_file(
  int vid,
  const char *zOrig,
  const char *zNew,
  int dryRunFlag,
  int moveFiles
){
  int x = db_int(-1, "SELECT deleted FROM vfile WHERE pathname=%Q %s",
                         zNew, filename_collation());
  if( x>=0 ){
    if( x==0 ){
      if( !filenames_are_case_sensitive() && fossil_stricmp(zOrig,zNew)==0 ){
        /* Case change only */
      }else{
        fossil_fatal("cannot rename '%s' to '%s' since another file named '%s'"
                     " is currently under management", zOrig, zNew, zNew);
      }
    }else{
      fossil_fatal("cannot rename '%s' to '%s' since the delete of '%s' has "
                   "not yet been committed", zOrig, zNew, zNew);
    }
  }
  if( moveFiles ){
    if( file_size(zNew, ExtFILE) != -1 ){
      fossil_fatal("cannot rename '%s' to '%s' on disk since another file"
        " named '%s' already exists", zOrig, zNew, zNew);      
    }
  }
  fossil_print("RENAME %s %s\n", zOrig, zNew);
  if( !dryRunFlag ){
    db_multi_exec(
      "UPDATE vfile SET pathname='%q' WHERE pathname='%q' %s AND vid=%d",
      zNew, zOrig, filename_collation(), vid
    );
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
      db_finalize(&q);
    }
  }
  db_prepare(&q, "SELECT f, t FROM mv ORDER BY f");
  while( db_step(&q)==SQLITE_ROW ){
    const char *zFrom = db_column_text(&q, 0);
    const char *zTo = db_column_text(&q, 1);
    mv_one_file(vid, zFrom, zTo, dryRunFlag);
    if( moveFiles ) add_file_to_move(zFrom, zTo);
  }
  db_finalize(&q);
  undo_reset();
  db_end_transaction(0);
  if( moveFiles ) process_files_to_move(dryRunFlag);
  fossil_free(zDest);







|







1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
      db_finalize(&q);
    }
  }
  db_prepare(&q, "SELECT f, t FROM mv ORDER BY f");
  while( db_step(&q)==SQLITE_ROW ){
    const char *zFrom = db_column_text(&q, 0);
    const char *zTo = db_column_text(&q, 1);
    mv_one_file(vid, zFrom, zTo, dryRunFlag, moveFiles);
    if( moveFiles ) add_file_to_move(zFrom, zTo);
  }
  db_finalize(&q);
  undo_reset();
  db_end_transaction(0);
  if( moveFiles ) process_files_to_move(dryRunFlag);
  fossil_free(zDest);
Changes to src/alerts.c.
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
@ --     a - Announcements
@ --     c - Check-ins
@ --     f - Forum posts
@ --     k - ** Special: Unsubscribed using /oneclickunsub
@ --     n - New forum threads
@ --     r - Replies to my own forum posts
@ --     t - Ticket changes
@ --     u - Elevation of users' permissions (admins only)
@ --     w - Wiki changes
@ --     x - Edits to forum posts
@ -- Probably different codes will be added in the future.  In the future
@ -- we might also add a separate table that allows subscribing to email
@ -- notifications for specific branches or tags or tickets.
@ --
@ CREATE TABLE repository.subscriber(







|







49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
@ --     a - Announcements
@ --     c - Check-ins
@ --     f - Forum posts
@ --     k - ** Special: Unsubscribed using /oneclickunsub
@ --     n - New forum threads
@ --     r - Replies to my own forum posts
@ --     t - Ticket changes
@ --     u - Changes of users' permissions (admins only)
@ --     w - Wiki changes
@ --     x - Edits to forum posts
@ -- Probably different codes will be added in the future.  In the future
@ -- we might also add a separate table that allows subscribing to email
@ -- notifications for specific branches or tags or tickets.
@ --
@ CREATE TABLE repository.subscriber(
280
281
282
283
284
285
286



287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315

316

317
318
319
320
321
322
323
324
325
326

327

328
329
330
331
332
333
334
  if( g.perm.Admin ){
    if( fossil_strcmp(g.zPath,"subscribers") ){
      style_submenu_element("Subscribers","%R/subscribers");
    }
    if( fossil_strcmp(g.zPath,"subscribe") ){
      style_submenu_element("Add New Subscriber","%R/subscribe");
    }



  }
}


/*
** WEBPAGE: setup_notification
**
** Administrative page for configuring and controlling email notification.
** Normally accessible via the /Admin/Notification menu.
*/
void setup_notification(void){
  static const char *const azSendMethods[] = {
    "off",   "Disabled",
    "pipe",  "Pipe to a command",
    "db",    "Store in a database",
    "dir",   "Store in a directory",
    "relay", "SMTP relay"
  };
  login_check_credentials();
  if( !g.perm.Setup ){
    login_needed(0);
    return;
  }
  db_begin_transaction();

  alert_submenu_common();
  style_submenu_element("Send Announcement","%R/announce");
  style_set_current_feature("alerts");
  style_header("Email Notification Setup");

  @ <h1>Status</h1>

  @ <table class="label-value">
  if( alert_enabled() ){
    stats_for_email();
  }else{
    @ <th>Disabled</th>
  }
  @ </table>
  @ <hr>
  @ <h1> Configuration </h1>
  @ <form action="%R/setup_notification" method="post"><div>

  @ <input type="submit"  name="submit" value="Apply Changes"><hr>

  login_insert_csrf_secret();

  entry_attribute("Canonical Server URL", 40, "email-url",
                   "eurl", "", 0);
  @ <p><b>Required.</b>
  @ This URL is used as the basename for hyperlinks included in
  @ email alert text.  Omit the trailing "/".







>
>
>













|


|












>
|
>








<

>
|
>







280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329

330
331
332
333
334
335
336
337
338
339
340
  if( g.perm.Admin ){
    if( fossil_strcmp(g.zPath,"subscribers") ){
      style_submenu_element("Subscribers","%R/subscribers");
    }
    if( fossil_strcmp(g.zPath,"subscribe") ){
      style_submenu_element("Add New Subscriber","%R/subscribe");
    }
    if( fossil_strcmp(g.zPath,"setup_notification") ){
      style_submenu_element("Notification Setup","%R/setup_notification");
    }
  }
}


/*
** WEBPAGE: setup_notification
**
** Administrative page for configuring and controlling email notification.
** Normally accessible via the /Admin/Notification menu.
*/
void setup_notification(void){
  static const char *const azSendMethods[] = {
    "off",   "Disabled",
    "relay", "SMTP relay",
    "db",    "Store in a database",
    "dir",   "Store in a directory",
    "pipe",  "Pipe to a command",
  };
  login_check_credentials();
  if( !g.perm.Setup ){
    login_needed(0);
    return;
  }
  db_begin_transaction();

  alert_submenu_common();
  style_submenu_element("Send Announcement","%R/announce");
  style_set_current_feature("alerts");
  style_header("Email Notification Setup");
  @ <form action="%R/setup_notification" method="post"><div>
  @ <h1>Status &ensp; <input type="submit"  name="submit" value="Refresh"></h1>
  @ </form>
  @ <table class="label-value">
  if( alert_enabled() ){
    stats_for_email();
  }else{
    @ <th>Disabled</th>
  }
  @ </table>
  @ <hr>

  @ <form action="%R/setup_notification" method="post"><div>
  @ <h1> Configuration </h1>
  @ <p><input type="submit"  name="submit" value="Apply Changes"></p>
  @ <hr>
  login_insert_csrf_secret();

  entry_attribute("Canonical Server URL", 40, "email-url",
                   "eurl", "", 0);
  @ <p><b>Required.</b>
  @ This URL is used as the basename for hyperlinks included in
  @ email alert text.  Omit the trailing "/".
389
390
391
392
393
394
395

















396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
  multiple_choice_attribute("Email Send Method", "email-send-method", "esm",
       "off", count(azSendMethods)/2, azSendMethods);
  @ <p>How to send email.  Requires auxiliary information from the fields
  @ that follow.  Hint: Use the <a href="%R/announce">/announce</a> page
  @ to send test message to debug this setting.
  @ (Property: "email-send-method")</p>
  alert_schema(1);

















  entry_attribute("Pipe Email Text Into This Command", 60, "email-send-command",
                   "ecmd", "sendmail -ti", 0);
  @ <p>When the send method is "pipe to a command", this is the command
  @ that is run.  Email messages are piped into the standard input of this
  @ command.  The command is expected to extract the sender address,
  @ recipient addresses, and subject from the header of the piped email
  @ text.  (Property: "email-send-command")</p>

  entry_attribute("Store Emails In This Database", 60, "email-send-db",
                   "esdb", "", 0);
  @ <p>When the send method is "store in a database", each email message is
  @ stored in an SQLite database file with the name given here.
  @ (Property: "email-send-db")</p>

  entry_attribute("Store Emails In This Directory", 60, "email-send-dir",
                   "esdir", "", 0);
  @ <p>When the send method is "store in a directory", each email message is
  @ stored as a separate file in the directory shown here.
  @ (Property: "email-send-dir")</p>

  entry_attribute("SMTP Relay Host", 60, "email-send-relayhost",
                   "esrh", "", 0);
  @ <p>When the send method is "SMTP relay", each email message is
  @ transmitted via the SMTP protocol (rfc5321) to a "Mail Submission
  @ Agent" or "MSA" (rfc4409) at the hostname shown here.  Optionally
  @ append a colon and TCP port number (ex: smtp.example.com:587).
  @ The default TCP port number is 25.
  @ (Property: "email-send-relayhost")</p>
  @ <hr>

  @ <p><input type="submit"  name="submit" value="Apply Changes"></p>
  @ </div></form>
  db_end_transaction(0);
  style_finish_page();
}







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







<
<
<
<
<
<
<






<
<
<
<
<
<
<
<







395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425







426
427
428
429
430
431








432
433
434
435
436
437
438
  multiple_choice_attribute("Email Send Method", "email-send-method", "esm",
       "off", count(azSendMethods)/2, azSendMethods);
  @ <p>How to send email.  Requires auxiliary information from the fields
  @ that follow.  Hint: Use the <a href="%R/announce">/announce</a> page
  @ to send test message to debug this setting.
  @ (Property: "email-send-method")</p>
  alert_schema(1);
  entry_attribute("SMTP Relay Host", 60, "email-send-relayhost",
                   "esrh", "localhost", 0);
  @ <p>When the send method is "SMTP relay", each email message is
  @ transmitted via the SMTP protocol (rfc5321) to a "Mail Submission
  @ Agent" or "MSA" (rfc4409) at the hostname shown here.  Optionally
  @ append a colon and TCP port number (ex: smtp.example.com:587).
  @ The default TCP port number is 25.
  @ Usage Hint:  If Fossil is running inside of a chroot jail, then it might
  @ not be able to resolve hostnames.  Work around this by using a raw IP
  @ address or create a "/etc/hosts" file inside the chroot jail.
  @ (Property: "email-send-relayhost")</p>
  @ 
  entry_attribute("Store Emails In This Database", 60, "email-send-db",
                   "esdb", "", 0);
  @ <p>When the send method is "store in a database", each email message is
  @ stored in an SQLite database file with the name given here.
  @ (Property: "email-send-db")</p>
  entry_attribute("Pipe Email Text Into This Command", 60, "email-send-command",
                   "ecmd", "sendmail -ti", 0);
  @ <p>When the send method is "pipe to a command", this is the command
  @ that is run.  Email messages are piped into the standard input of this
  @ command.  The command is expected to extract the sender address,
  @ recipient addresses, and subject from the header of the piped email
  @ text.  (Property: "email-send-command")</p>







  entry_attribute("Store Emails In This Directory", 60, "email-send-dir",
                   "esdir", "", 0);
  @ <p>When the send method is "store in a directory", each email message is
  @ stored as a separate file in the directory shown here.
  @ (Property: "email-send-dir")</p>









  @ <hr>

  @ <p><input type="submit"  name="submit" value="Apply Changes"></p>
  @ </div></form>
  db_end_transaction(0);
  style_finish_page();
}
628
629
630
631
632
633
634
635


636
637
638
639
640

641
642
643





644
645
646
647
648
649
650
    }
  }else if( fossil_strcmp(p->zDest, "pipe")==0 ){
    emailerGetSetting(p, &p->zCmd, "email-send-command");
  }else if( fossil_strcmp(p->zDest, "dir")==0 ){
    emailerGetSetting(p, &p->zDir, "email-send-dir");
  }else if( fossil_strcmp(p->zDest, "blob")==0 ){
    blob_init(&p->out, 0, 0);
  }else if( fossil_strcmp(p->zDest, "relay")==0 ){


    const char *zRelay = 0;
    emailerGetSetting(p, &zRelay, "email-send-relayhost");
    if( zRelay ){
      u32 smtpFlags = SMTP_DIRECT;
      if( mFlags & ALERT_TRACE ) smtpFlags |= SMTP_TRACE_STDOUT;

      p->pSmtp = smtp_session_new(domain_of_addr(p->zFrom), zRelay,
                                  smtpFlags);
      smtp_client_startup(p->pSmtp);





    }
  }
  return p;
}

/*
** Scan the header of the email message in pMsg looking for the







|
>
>





>

|
|
>
>
>
>
>







636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
    }
  }else if( fossil_strcmp(p->zDest, "pipe")==0 ){
    emailerGetSetting(p, &p->zCmd, "email-send-command");
  }else if( fossil_strcmp(p->zDest, "dir")==0 ){
    emailerGetSetting(p, &p->zDir, "email-send-dir");
  }else if( fossil_strcmp(p->zDest, "blob")==0 ){
    blob_init(&p->out, 0, 0);
  }else if( fossil_strcmp(p->zDest, "relay")==0
         || fossil_strcmp(p->zDest, "debug-relay")==0
  ){
    const char *zRelay = 0;
    emailerGetSetting(p, &zRelay, "email-send-relayhost");
    if( zRelay ){
      u32 smtpFlags = SMTP_DIRECT;
      if( mFlags & ALERT_TRACE ) smtpFlags |= SMTP_TRACE_STDOUT;
      blob_init(&p->out, 0, 0);
      p->pSmtp = smtp_session_new(domain_of_addr(p->zFrom), zRelay,
                                  smtpFlags, 0);
      if( p->pSmtp==0 || p->pSmtp->zErr ){
        emailerError(p, "Could not start SMTP session: %s",
                        p->pSmtp ? p->pSmtp->zErr : "reason unknown");
      }else if( p->zDest[0]=='d' ){
        smtp_session_config(p->pSmtp, SMTP_TRACE_BLOB, &p->out);
      }
    }
  }
  return p;
}

/*
** Scan the header of the email message in pMsg looking for the
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
    blob_appendf(pOut, "From: %s <%s@%s>\r\n",
       zFromName, alert_mailbox_name(zFromName), alert_hostname(p->zFrom));
    blob_appendf(pOut, "Sender: <%s>\r\n", p->zFrom);
  }else{
    blob_appendf(pOut, "From: <%s>\r\n", p->zFrom);
  }
  blob_appendf(pOut, "Date: %z\r\n", cgi_rfc822_datestamp(time(0)));
  if( p->zListId  && p->zListId[0] ){
    blob_appendf(pOut, "List-Id: %s\r\n", p->zListId);
  }
  if( strstr(blob_str(pHdr), "\r\nMessage-Id:")==0 ){
    /* Message-id format:  "<$(date)x$(random)@$(from-host)>" where $(date) is
    ** the current unix-time in hex, $(random) is a 64-bit random number,
    ** and $(from) is the domain part of the email-self setting. */
    sqlite3_randomness(sizeof(r1), &r1);
    r2 = time(0);
    blob_appendf(pOut, "Message-Id: <%llxx%016llx@%s>\r\n",







<
<
<







982
983
984
985
986
987
988



989
990
991
992
993
994
995
    blob_appendf(pOut, "From: %s <%s@%s>\r\n",
       zFromName, alert_mailbox_name(zFromName), alert_hostname(p->zFrom));
    blob_appendf(pOut, "Sender: <%s>\r\n", p->zFrom);
  }else{
    blob_appendf(pOut, "From: <%s>\r\n", p->zFrom);
  }
  blob_appendf(pOut, "Date: %z\r\n", cgi_rfc822_datestamp(time(0)));



  if( strstr(blob_str(pHdr), "\r\nMessage-Id:")==0 ){
    /* Message-id format:  "<$(date)x$(random)@$(from-host)>" where $(date) is
    ** the current unix-time in hex, $(random) is a 64-bit random number,
    ** and $(from) is the domain part of the email-self setting. */
    sqlite3_randomness(sizeof(r1), &r1);
    r2 = time(0);
    blob_appendf(pOut, "Message-Id: <%llxx%016llx@%s>\r\n",
1014
1015
1016
1017
1018
1019
1020

1021
1022
1023







1024
1025
1026
1027
1028
1029
1030
  }else if( p->zDir ){
    char *zFile = file_time_tempname(p->zDir, ".email");
    blob_write_to_file(&all, zFile);
    fossil_free(zFile);
  }else if( p->pSmtp ){
    char **azTo = 0;
    int nTo = 0;

    email_header_to(pHdr, &nTo, &azTo);
    if( nTo>0 ){
      smtp_send_msg(p->pSmtp, p->zFrom, nTo, (const char**)azTo,blob_str(&all));







      email_header_to_free(nTo, azTo);
    }
  }else if( strcmp(p->zDest, "stdout")==0 ){
    char **azTo = 0;
    int nTo = 0;
    int i;
    email_header_to(pHdr, &nTo, &azTo);







>

|
|
>
>
>
>
>
>
>







1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
  }else if( p->zDir ){
    char *zFile = file_time_tempname(p->zDir, ".email");
    blob_write_to_file(&all, zFile);
    fossil_free(zFile);
  }else if( p->pSmtp ){
    char **azTo = 0;
    int nTo = 0;
    SmtpSession *pSmtp = p->pSmtp;
    email_header_to(pHdr, &nTo, &azTo);
    if( nTo>0 && !pSmtp->bFatal ){
      smtp_send_msg(pSmtp,p->zFrom,nTo,(const char**)azTo,blob_str(&all));
      if( pSmtp->zErr && !pSmtp->bFatal ){
        smtp_send_msg(pSmtp,p->zFrom,nTo,(const char**)azTo,blob_str(&all));
      }
      if( pSmtp->zErr ){
        fossil_errorlog("SMTP: (%s) %s", pSmtp->bFatal ? "fatal" : "retry",
                        pSmtp->zErr);
      }
      email_header_to_free(nTo, azTo);
    }
  }else if( strcmp(p->zDest, "stdout")==0 ){
    char **azTo = 0;
    int nTo = 0;
    int i;
    email_header_to(pHdr, &nTo, &azTo);
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
*/
/*
** SETTING: email-listid             width=40
** If this setting is not an empty string, then it becomes the argument to
** a "List-ID:" header that is added to all out-bound notification emails.
*/
/*
** SETTING: email-send-relayhost      width=40 sensitive
** This is the hostname and TCP port to which output email messages
** are sent when email-send-method is "relay".  There should be an
** SMTP server configured as a Mail Submission Agent listening on the
** designated host and port and all times.
*/









|







1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
*/
/*
** SETTING: email-listid             width=40
** If this setting is not an empty string, then it becomes the argument to
** a "List-ID:" header that is added to all out-bound notification emails.
*/
/*
** SETTING: email-send-relayhost      width=40 sensitive default=127.0.0.1
** This is the hostname and TCP port to which output email messages
** are sent when email-send-method is "relay".  There should be an
** SMTP server configured as a Mail Submission Agent listening on the
** designated host and port and all times.
*/


1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
  }
  if( g.perm.RdWiki ){
    @  <label><input type="checkbox" name="sw" %s(PCK("sw"))> \
    @  Wiki</label><br>
  }
  if( g.perm.Admin ){
    @  <label><input type="checkbox" name="su" %s(PCK("su"))> \
    @  User permission elevation</label>
  }
  di = PB("di");
  @ </td></tr>
  @ <tr>
  @  <td class="form_label">Delivery:</td>
  @  <td><select size="1" name="di">
  @     <option value="0" %s(di?"":"selected")>Individual Emails</option>







|







1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
  }
  if( g.perm.RdWiki ){
    @  <label><input type="checkbox" name="sw" %s(PCK("sw"))> \
    @  Wiki</label><br>
  }
  if( g.perm.Admin ){
    @  <label><input type="checkbox" name="su" %s(PCK("su"))> \
    @  User permission changes</label>
  }
  di = PB("di");
  @ </td></tr>
  @ <tr>
  @  <td class="form_label">Delivery:</td>
  @  <td><select size="1" name="di">
  @     <option value="0" %s(di?"":"selected")>Individual Emails</option>
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
  }
  if( g.perm.Admin ){
    /* Corner-case bug: if an admin assigns 'u' to a non-admin, that
    ** subscription will get removed if the user later edits their
    ** subscriptions, as non-admins are not permitted to add that
    ** subscription. */
    @  <label><input type="checkbox" name="su" %s(su?"checked":"")>\
    @  User permission elevation</label>
  }
  @ </td></tr>
  if( strchr(ssub,'k')!=0 ){
    @ <tr><td></td><td>&nbsp;&uarr;&nbsp;
    @ Note: User did a one-click unsubscribe</td></tr>
  }
  @ <tr>







|







2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
  }
  if( g.perm.Admin ){
    /* Corner-case bug: if an admin assigns 'u' to a non-admin, that
    ** subscription will get removed if the user later edits their
    ** subscriptions, as non-admins are not permitted to add that
    ** subscription. */
    @  <label><input type="checkbox" name="su" %s(su?"checked":"")>\
    @  User permission changes</label>
  }
  @ </td></tr>
  if( strchr(ssub,'k')!=0 ){
    @ <tr><td></td><td>&nbsp;&uarr;&nbsp;
    @ Note: User did a one-click unsubscribe</td></tr>
  }
  @ <tr>
3189
3190
3191
3192
3193
3194
3195


3196
3197
3198
3199
3200
3201
3202
3203

3204
3205
3206
3207
3208
3209
3210
      if( blob_size(&p->hdr)>0 ){
        /* This alert should be sent as a separate email */
        Blob fhdr, fbody;
        blob_init(&fhdr, 0, 0);
        blob_appendf(&fhdr, "To: <%s>\r\n", zEmail);
        blob_append(&fhdr, blob_buffer(&p->hdr), blob_size(&p->hdr));
        blob_init(&fbody, blob_buffer(&p->txt), blob_size(&p->txt));


        blob_appendf(&fhdr, "List-Unsubscribe: <%s/oneclickunsub/%s>\r\n",
                     zUrl, zCode);
        blob_appendf(&fhdr,
                   "List-Unsubscribe-Post: List-Unsubscribe=One-Click\r\n");
        blob_appendf(&fbody, "\n-- \nUnsubscribe: %s/unsubscribe/%s\n",
           zUrl, zCode);
        /* blob_appendf(&fbody, "Subscription settings: %s/alerts/%s\n",
        **   zUrl, zCode); */

        alert_send(pSender,&fhdr,&fbody,p->zFromName);
        nSent++;
        blob_reset(&fhdr);
        blob_reset(&fbody);
      }else{
        /* Events other than forum posts are gathered together into
        ** a single email message */







>
>
|
|
|
|
|
|
|
|
>







3210
3211
3212
3213
3214
3215
3216
3217
3218
3219
3220
3221
3222
3223
3224
3225
3226
3227
3228
3229
3230
3231
3232
3233
3234
      if( blob_size(&p->hdr)>0 ){
        /* This alert should be sent as a separate email */
        Blob fhdr, fbody;
        blob_init(&fhdr, 0, 0);
        blob_appendf(&fhdr, "To: <%s>\r\n", zEmail);
        blob_append(&fhdr, blob_buffer(&p->hdr), blob_size(&p->hdr));
        blob_init(&fbody, blob_buffer(&p->txt), blob_size(&p->txt));
        if( pSender->zListId  && pSender->zListId[0] ){
           blob_appendf(&fhdr, "List-Id: %s\r\n", pSender->zListId);
           blob_appendf(&fhdr, "List-Unsubscribe: <%s/oneclickunsub/%s>\r\n",
                        zUrl, zCode);
           blob_appendf(&fhdr,
                       "List-Unsubscribe-Post: List-Unsubscribe=One-Click\r\n");
           blob_appendf(&fbody, "\n-- \nUnsubscribe: %s/unsubscribe/%s\n",
                        zUrl, zCode);
           /* blob_appendf(&fbody, "Subscription settings: %s/alerts/%s\n",
           **   zUrl, zCode); */
        }
        alert_send(pSender,&fhdr,&fbody,p->zFromName);
        nSent++;
        blob_reset(&fhdr);
        blob_reset(&fbody);
      }else{
        /* Events other than forum posts are gathered together into
        ** a single email message */
3219
3220
3221
3222
3223
3224
3225


3226
3227

3228
3229
3230

3231
3232
3233
3234
3235
3236
3237
        }
        nHit++;
        blob_append(&body, "\n", 1);
        blob_append(&body, blob_buffer(&p->txt), blob_size(&p->txt));
      }
    }
    if( nHit==0 ) continue;


    blob_appendf(&hdr, "List-Unsubscribe: <%s/oneclickunsub/%s>\r\n",
         zUrl, zCode);

    blob_appendf(&hdr, "List-Unsubscribe-Post: List-Unsubscribe=One-Click\r\n");
    blob_appendf(&body,"\n-- \nSubscription info: %s/alerts/%s\n",
         zUrl, zCode);

    alert_send(pSender,&hdr,&body,0);
    nSent++;
    blob_truncate(&hdr, 0);
    blob_truncate(&body, 0);
  }
  blob_reset(&hdr);
  blob_reset(&body);







>
>
|
|
>
|
|
|
>







3243
3244
3245
3246
3247
3248
3249
3250
3251
3252
3253
3254
3255
3256
3257
3258
3259
3260
3261
3262
3263
3264
3265
        }
        nHit++;
        blob_append(&body, "\n", 1);
        blob_append(&body, blob_buffer(&p->txt), blob_size(&p->txt));
      }
    }
    if( nHit==0 ) continue;
    if( pSender->zListId  && pSender->zListId[0] ){
      blob_appendf(&hdr, "List-Id: %s\r\n", pSender->zListId);
      blob_appendf(&hdr, "List-Unsubscribe: <%s/oneclickunsub/%s>\r\n",
           zUrl, zCode);
      blob_appendf(&hdr,
            "List-Unsubscribe-Post: List-Unsubscribe=One-Click\r\n");
      blob_appendf(&body,"\n-- \nSubscription info: %s/alerts/%s\n",
           zUrl, zCode);
    }
    alert_send(pSender,&hdr,&body,0);
    nSent++;
    blob_truncate(&hdr, 0);
    blob_truncate(&body, 0);
  }
  blob_reset(&hdr);
  blob_reset(&body);
3269
3270
3271
3272
3273
3274
3275

3276
3277
3278
3279
3280
3281
3282
3283









3284
3285
3286
3287
3288
3289
3290
         " WHERE lastContact<=%d AND lastContact>%d"
         "   AND NOT sdonotcall"
         "   AND length(sdigest)>0",
         iNewWarn, iOldWarn
      );
      while( db_step(&q)==SQLITE_ROW ){
        Blob hdr, body;

        blob_init(&hdr, 0, 0);
        blob_init(&body, 0, 0);
        alert_renewal_msg(&hdr, &body,
           db_column_text(&q,0),
           db_column_int(&q,1),
           db_column_text(&q,2),
           db_column_text(&q,3),
           zRepoName, zUrl);









        alert_send(pSender,&hdr,&body,0);
        blob_reset(&hdr);
        blob_reset(&body);
      }
      db_finalize(&q);
      if( (flags & SENDALERT_PRESERVE)==0 ){
        if( iOldWarn>0 ){







>



|




>
>
>
>
>
>
>
>
>







3297
3298
3299
3300
3301
3302
3303
3304
3305
3306
3307
3308
3309
3310
3311
3312
3313
3314
3315
3316
3317
3318
3319
3320
3321
3322
3323
3324
3325
3326
3327
3328
         " WHERE lastContact<=%d AND lastContact>%d"
         "   AND NOT sdonotcall"
         "   AND length(sdigest)>0",
         iNewWarn, iOldWarn
      );
      while( db_step(&q)==SQLITE_ROW ){
        Blob hdr, body;
        const char *zCode = db_column_text(&q,0);
        blob_init(&hdr, 0, 0);
        blob_init(&body, 0, 0);
        alert_renewal_msg(&hdr, &body,
           zCode,
           db_column_int(&q,1),
           db_column_text(&q,2),
           db_column_text(&q,3),
           zRepoName, zUrl);
        if( pSender->zListId  && pSender->zListId[0] ){
           blob_appendf(&hdr, "List-Id: %s\r\n", pSender->zListId);
           blob_appendf(&hdr, "List-Unsubscribe: <%s/oneclickunsub/%s>\r\n",
                        zUrl, zCode);
           blob_appendf(&hdr,
                       "List-Unsubscribe-Post: List-Unsubscribe=One-Click\r\n");
           blob_appendf(&body, "\n-- \nUnsubscribe: %s/unsubscribe/%s\n",
                        zUrl, zCode);
        }
        alert_send(pSender,&hdr,&body,0);
        blob_reset(&hdr);
        blob_reset(&body);
      }
      db_finalize(&q);
      if( (flags & SENDALERT_PRESERVE)==0 ){
        if( iOldWarn>0 ){
3434
3435
3436
3437
3438
3439
3440


3441
3442










3443
3444
3445
3446
3447
3448
3449
3450
3451
3452
3453
  char *zErr;
  const char *zTo = PT("to");
  char *zSubject = PT("subject");
  int bAll = PB("all");
  int bAA = PB("aa");
  int bMods = PB("mods");
  const char *zSub = db_get("email-subname", "[Fossil Repo]");


  int bTest2 = fossil_strcmp(P("name"),"test2")==0;
  Blob hdr, body;










  blob_init(&body, 0, 0);
  blob_init(&hdr, 0, 0);
  blob_appendf(&body, "%s", PT("msg")/*safe-for-%s*/);
  pSender = alert_sender_new(bTest2 ? "blob" : 0, 0);
  if( zTo[0] ){
    blob_appendf(&hdr, "To: <%s>\r\nSubject: %s %s\r\n", zTo, zSub, zSubject);
    alert_send(pSender, &hdr, &body, 0);
  }
  if( bAll || bAA || bMods ){
    Stmt q;
    int nUsed = blob_size(&body);







>
>
|

>
>
>
>
>
>
>
>
>
>



|







3472
3473
3474
3475
3476
3477
3478
3479
3480
3481
3482
3483
3484
3485
3486
3487
3488
3489
3490
3491
3492
3493
3494
3495
3496
3497
3498
3499
3500
3501
3502
3503
  char *zErr;
  const char *zTo = PT("to");
  char *zSubject = PT("subject");
  int bAll = PB("all");
  int bAA = PB("aa");
  int bMods = PB("mods");
  const char *zSub = db_get("email-subname", "[Fossil Repo]");
  const char *zName = P("name");    /* Debugging options */
  const char *zDest = 0;            /* How to send the announcement */
  int bTest = 0;
  Blob hdr, body;

  if( fossil_strcmp(zName, "test2")==0 ){
    bTest = 2;
    zDest = "blob";
  }else if( fossil_strcmp(zName, "test3")==0 ){
    bTest = 3;
    if( fossil_strcmp(db_get("email-send-method",""),"relay")==0 ){
      zDest = "debug-relay";
    }
  }
  blob_init(&body, 0, 0);
  blob_init(&hdr, 0, 0);
  blob_appendf(&body, "%s", PT("msg")/*safe-for-%s*/);
  pSender = alert_sender_new(zDest, 0);
  if( zTo[0] ){
    blob_appendf(&hdr, "To: <%s>\r\nSubject: %s %s\r\n", zTo, zSub, zSubject);
    alert_send(pSender, &hdr, &body, 0);
  }
  if( bAll || bAA || bMods ){
    Stmt q;
    int nUsed = blob_size(&body);
3477
3478
3479
3480
3481
3482
3483
3484
3485
3486
3487






3488
3489
3490

3491
3492
3493
3494
3495
3496
3497
3498
3499
3500
3501
3502
3503
3504
3505
3506
3507
3508
3509
3510
3511
3512

3513

3514




3515
3516
3517
3518
3519
3520
3521



3522
3523
3524
3525
3526
3527
3528
3529
3530
3531
3532
3533
3534

3535
3536
3537
3538
3539
3540
3541
3542
3543
3544
3545
3546
3547
3548
3549
3550
3551
3552






3553
3554
3555
3556
3557
3558
3559
        blob_appendf(&body,"\n-- \nSubscription info: %s/alerts/%s\n",
           zURL, zCode);
      }
      alert_send(pSender, &hdr, &body, 0);
    }
    db_finalize(&q);
  }
  if( bTest2 ){
    /* If the URL is /announce/test2 instead of just /announce, then no
    ** email is actually sent.  Instead, the text of the email that would
    ** have been sent is displayed in the result window. */






    @ <pre style='border: 2px solid blue; padding: 1ex'>
    @ %h(blob_str(&pSender->out))
    @ </pre>

  }
  zErr = pSender->zErr;
  pSender->zErr = 0;
  alert_sender_free(pSender);
  return zErr;
}


/*
** WEBPAGE: announce
**
** A web-form, available to users with the "Send-Announcement" or "A"
** capability, that allows one to send announcements to whomever
** has subscribed to receive announcements.  The administrator can
** also send a message to an arbitrary email address and/or to all
** subscribers regardless of whether or not they have elected to
** receive announcements.
*/
void announce_page(void){
  const char *zAction = "announce"
    /* Maintenance reminder: we need an explicit action=THIS_PAGE on the
    ** form element to avoid that a URL arg of to=... passed to this

    ** page ends up overwriting the form-posted "to" value. This

    ** action value differs for the test1 request path.




    */;

  login_check_credentials();
  if( !g.perm.Announce ){
    login_needed(0);
    return;
  }



  style_set_current_feature("alerts");
  if( fossil_strcmp(P("name"),"test1")==0 ){
    /* Visit the /announce/test1 page to see the CGI variables */
    zAction = "announce/test1";
    @ <p style='border: 1px solid black; padding: 1ex;'>
    cgi_print_all(0, 0, 0);
    @ </p>
  }else if( P("submit")!=0 && cgi_csrf_safe(2) ){
    char *zErr = alert_send_announcement();
    style_header("Announcement Sent");
    if( zErr ){
      @ <h1>Internal Error</h1>
      @ <p>The following error was reported by the system:

      @ <blockquote><pre>
      @ %h(zErr)
      @ </pre></blockquote>
    }else{
      @ <p>The announcement has been sent.
      @ <a href="%h(PD("REQUEST_URI","/"))">Send another</a></p>
    }
    style_finish_page();
    return;
  } else if( !alert_enabled() ){
    style_header("Cannot Send Announcement");
    @ <p>Either you have no subscribers yet, or email alerts are not yet
    @ <a href="https://fossil-scm.org/fossil/doc/trunk/www/alerts.md">set up</a>
    @ for this repository.</p>
    return;
  }

  style_header("Send Announcement");






  @ <form method="POST" action="%R/%s(zAction)">
  login_insert_csrf_secret();
  @ <table class="subscribe">
  if( g.perm.Admin ){
    int aa = PB("aa");
    int all = PB("all");
    int aMod = PB("mods");







|
|
|
|
>
>
>
>
>
>
|


>



















|
|
<
>
|
>
|
>
>
>
>
|
<





>
>
>

|









|
|
>


















>
>
>
>
>
>







3527
3528
3529
3530
3531
3532
3533
3534
3535
3536
3537
3538
3539
3540
3541
3542
3543
3544
3545
3546
3547
3548
3549
3550
3551
3552
3553
3554
3555
3556
3557
3558
3559
3560
3561
3562
3563
3564
3565
3566
3567
3568

3569
3570
3571
3572
3573
3574
3575
3576
3577

3578
3579
3580
3581
3582
3583
3584
3585
3586
3587
3588
3589
3590
3591
3592
3593
3594
3595
3596
3597
3598
3599
3600
3601
3602
3603
3604
3605
3606
3607
3608
3609
3610
3611
3612
3613
3614
3615
3616
3617
3618
3619
3620
3621
3622
3623
3624
3625
3626
3627
3628
3629
3630
        blob_appendf(&body,"\n-- \nSubscription info: %s/alerts/%s\n",
           zURL, zCode);
      }
      alert_send(pSender, &hdr, &body, 0);
    }
    db_finalize(&q);
  }
  if( bTest && blob_size(&pSender->out) ){
    /* If the URL is "/announce/test2" then no email is actually sent.
    ** Instead, the text of the email that would have been sent is
    ** displayed in the result window.
    **
    ** If the URL is "/announce/test3" and the email-send-method is "relay"
    ** then the announcement is sent as it normally would be, but a
    ** transcript of the SMTP conversation with the MTA is shown here.
    */
    blob_trim(&pSender->out);
    @ <pre style='border: 2px solid blue; padding: 1ex;'>
    @ %h(blob_str(&pSender->out))
    @ </pre>
    blob_reset(&pSender->out);
  }
  zErr = pSender->zErr;
  pSender->zErr = 0;
  alert_sender_free(pSender);
  return zErr;
}


/*
** WEBPAGE: announce
**
** A web-form, available to users with the "Send-Announcement" or "A"
** capability, that allows one to send announcements to whomever
** has subscribed to receive announcements.  The administrator can
** also send a message to an arbitrary email address and/or to all
** subscribers regardless of whether or not they have elected to
** receive announcements.
*/
void announce_page(void){
  const char *zAction = "announce";
  const char *zName = PD("name","");

  /*
  ** Debugging Notes:
  **
  **    /announce/test1           ->   Shows query parameter values
  **    /announce/test2           ->   Shows the formatted message but does
  **                                   not send it.
  **    /announce/test3           ->   Sends the message, but also shows
  **                                   the SMTP transcript.
  */

  login_check_credentials();
  if( !g.perm.Announce ){
    login_needed(0);
    return;
  }
  if( !g.perm.Setup ){
    zName = 0;   /* Disable debugging feature for non-admin users */
  }
  style_set_current_feature("alerts");
  if( fossil_strcmp(zName,"test1")==0 ){
    /* Visit the /announce/test1 page to see the CGI variables */
    zAction = "announce/test1";
    @ <p style='border: 1px solid black; padding: 1ex;'>
    cgi_print_all(0, 0, 0);
    @ </p>
  }else if( P("submit")!=0 && cgi_csrf_safe(2) ){
    char *zErr = alert_send_announcement();
    style_header("Announcement Sent");
    if( zErr ){
      @ <h1>Error</h1>
      @ <p>The following error was reported by the
      @ announcement-sending subsystem:
      @ <blockquote><pre>
      @ %h(zErr)
      @ </pre></blockquote>
    }else{
      @ <p>The announcement has been sent.
      @ <a href="%h(PD("REQUEST_URI","/"))">Send another</a></p>
    }
    style_finish_page();
    return;
  } else if( !alert_enabled() ){
    style_header("Cannot Send Announcement");
    @ <p>Either you have no subscribers yet, or email alerts are not yet
    @ <a href="https://fossil-scm.org/fossil/doc/trunk/www/alerts.md">set up</a>
    @ for this repository.</p>
    return;
  }

  style_header("Send Announcement");
  alert_submenu_common();
  if( fossil_strcmp(zName,"test2")==0 ){
    zAction = "announce/test2";
  }else if( fossil_strcmp(zName,"test3")==0 ){
    zAction = "announce/test3";
  }
  @ <form method="POST" action="%R/%s(zAction)">
  login_insert_csrf_secret();
  @ <table class="subscribe">
  if( g.perm.Admin ){
    int aa = PB("aa");
    int all = PB("all");
    int aMod = PB("mods");
3582
3583
3584
3585
3586
3587
3588
3589
3590
3591
3592
3593
3594
3595
3596













3597
3598
  @ <tr>
  @  <td class="form_label">Message:</td>
  @  <td><textarea name="msg" cols="80" rows="10" wrap="virtual">\
  @ %h(PT("msg"))</textarea>
  @ </tr>
  @ <tr>
  @   <td></td>
  if( fossil_strcmp(P("name"),"test2")==0 ){
    @   <td><input type="submit" name="submit" value="Dry Run">
  }else{
    @   <td><input type="submit" name="submit" value="Send Message">
  }
  @ </tr>
  @ </table>
  @ </form>













  style_finish_page();
}







|







>
>
>
>
>
>
>
>
>
>
>
>
>


3653
3654
3655
3656
3657
3658
3659
3660
3661
3662
3663
3664
3665
3666
3667
3668
3669
3670
3671
3672
3673
3674
3675
3676
3677
3678
3679
3680
3681
3682
  @ <tr>
  @  <td class="form_label">Message:</td>
  @  <td><textarea name="msg" cols="80" rows="10" wrap="virtual">\
  @ %h(PT("msg"))</textarea>
  @ </tr>
  @ <tr>
  @   <td></td>
  if( fossil_strcmp(zName,"test2")==0 ){
    @   <td><input type="submit" name="submit" value="Dry Run">
  }else{
    @   <td><input type="submit" name="submit" value="Send Message">
  }
  @ </tr>
  @ </table>
  @ </form>
  if( g.perm.Setup ){
    @ <hr>
    @ <p>Trouble-shooting Options:</p>
    @ <ol>
    @ <li> <a href="%R/announce">Normal Processing</a>
    @ <li> Only <a href="%R/announce/test1">show POST parameters</a>
    @      - Do not send the announcement.
    @ <li> <a href="%R/announce/test2">Show the email text</a> but do
    @      not actually send it.
    @ <li> Send the message and also <a href="%R/announce/test3">show the
    @      SMTP traffic</a> when using "relay" mode.
    @ </ol>
  }
  style_finish_page();
}
Changes to src/backoffice.c.
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
** Backoffice processes should die off after doing whatever work they need
** to do.  In this way, we avoid having lots of idle processes in the
** process table, doing nothing on rarely accessed repositories, and
** if the Fossil binary is updated on a system, the backoffice processes
** will restart using the new binary automatically.
**
** At any point in time there should be at most two backoffice processes.
** There is a main process that is doing the actually work, and there is
** a second stand-by process that is waiting for the main process to finish
** and that will become the main process after a delay.
**
** After any successful web page reply, the backoffice_check_if_needed()
** routine is called.  That routine checks to see if both one or both of
** the backoffice processes are already running.  That routine remembers the
** status in a global variable.
**
** Later, after the repository database is closed, the
** backoffice_run_if_needed() routine is called.  If the prior call
** to backoffice_check_if_needed() indicated that backoffice processing
** might be required, the run_if_needed() attempts to kick off a backoffice
** process.
**
** All work performance by the backoffice is in the backoffice_work()
** routine.
*/
#if defined(_WIN32)
# if defined(_WIN32_WINNT)
#  undef _WIN32_WINNT
# endif
# define _WIN32_WINNT 0x501







|














|







36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
** Backoffice processes should die off after doing whatever work they need
** to do.  In this way, we avoid having lots of idle processes in the
** process table, doing nothing on rarely accessed repositories, and
** if the Fossil binary is updated on a system, the backoffice processes
** will restart using the new binary automatically.
**
** At any point in time there should be at most two backoffice processes.
** There is a main process that is doing the actual work, and there is
** a second stand-by process that is waiting for the main process to finish
** and that will become the main process after a delay.
**
** After any successful web page reply, the backoffice_check_if_needed()
** routine is called.  That routine checks to see if both one or both of
** the backoffice processes are already running.  That routine remembers the
** status in a global variable.
**
** Later, after the repository database is closed, the
** backoffice_run_if_needed() routine is called.  If the prior call
** to backoffice_check_if_needed() indicated that backoffice processing
** might be required, the run_if_needed() attempts to kick off a backoffice
** process.
**
** All work performed by the backoffice is in the backoffice_work()
** routine.
*/
#if defined(_WIN32)
# if defined(_WIN32_WINNT)
#  undef _WIN32_WINNT
# endif
# define _WIN32_WINNT 0x501
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
  sqlite3_uint64 idSelf;
  int lastWarning = 0;
  int warningDelay = 30;
  static int once = 0;

  if( sqlite3_db_readonly(g.db, 0) ) return;
  if( db_is_protected(PROTECT_READONLY) ) return;
  g.zPhase = "backoffice";
  backoffice_error_check_one(&once);
  idSelf = backofficeProcessId();
  while(1){
    tmNow = time(0);
    db_begin_write();
    backofficeReadLease(&x);
    if( x.tmNext>=tmNow







|







483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
  sqlite3_uint64 idSelf;
  int lastWarning = 0;
  int warningDelay = 30;
  static int once = 0;

  if( sqlite3_db_readonly(g.db, 0) ) return;
  if( db_is_protected(PROTECT_READONLY) ) return;
  g.zPhase = "backoffice-pending";
  backoffice_error_check_one(&once);
  idSelf = backofficeProcessId();
  while(1){
    tmNow = time(0);
    db_begin_write();
    backofficeReadLease(&x);
    if( x.tmNext>=tmNow
508
509
510
511
512
513
514

515
516
517
518
519
520
521
    }
    if( x.tmCurrent<tmNow && backofficeProcessDone(x.idCurrent) ){
      /* This process can start doing backoffice work immediately */
      x.idCurrent = idSelf;
      x.tmCurrent = tmNow + BKOFCE_LEASE_TIME;
      x.idNext = 0;
      x.tmNext = 0;

      backofficeWriteLease(&x);
      db_end_transaction(0);
      backofficeTrace("/***** Begin Backoffice Processing %d *****/\n",
                      GETPID());
      backoffice_work();
      break;
    }







>







508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
    }
    if( x.tmCurrent<tmNow && backofficeProcessDone(x.idCurrent) ){
      /* This process can start doing backoffice work immediately */
      x.idCurrent = idSelf;
      x.tmCurrent = tmNow + BKOFCE_LEASE_TIME;
      x.idNext = 0;
      x.tmNext = 0;
      g.zPhase = "backoffice-work";
      backofficeWriteLease(&x);
      db_end_transaction(0);
      backofficeTrace("/***** Begin Backoffice Processing %d *****/\n",
                      GETPID());
      backoffice_work();
      break;
    }
541
542
543
544
545
546
547


548
549
550

551
552
553
554
555
556
557
        /* The sleep was interrupted by a signal from another thread. */
        backofficeTrace("/***** Backoffice Interrupt %d *****/\n", GETPID());
        db_end_transaction(0);
        break;
      }
    }else{
      if( (sqlite3_uint64)(lastWarning+warningDelay) < tmNow ){


        fossil_warning(
           "backoffice process %lld still running after %d seconds",
           x.idCurrent, (int)(BKOFCE_LEASE_TIME + tmNow - x.tmCurrent));

        lastWarning = tmNow;
        warningDelay *= 2;
      }
      if( backofficeSleep(1000) ){
        /* The sleep was interrupted by a signal from another thread. */
        backofficeTrace("/***** Backoffice Interrupt %d *****/\n", GETPID());
        db_end_transaction(0);







>
>
|

|
>







542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
        /* The sleep was interrupted by a signal from another thread. */
        backofficeTrace("/***** Backoffice Interrupt %d *****/\n", GETPID());
        db_end_transaction(0);
        break;
      }
    }else{
      if( (sqlite3_uint64)(lastWarning+warningDelay) < tmNow ){
        sqlite3_int64 runningFor = BKOFCE_LEASE_TIME + tmNow - x.tmCurrent;
        if( warningDelay>=240 && runningFor<1800 ){
          fossil_warning(
           "backoffice process %lld still running after %d seconds",
           x.idCurrent, runningFor);
        }
        lastWarning = tmNow;
        warningDelay *= 2;
      }
      if( backofficeSleep(1000) ){
        /* The sleep was interrupted by a signal from another thread. */
        backofficeTrace("/***** Backoffice Interrupt %d *****/\n", GETPID());
        db_end_transaction(0);
640
641
642
643
644
645
646

647
648

649
650

651
652
653
654
655
656
657
    }
    blob_init(&log, 0, 0);
    backofficeBlob = &log;
    blob_appendf(&log, "%s %s", db_text(0, "SELECT datetime('now')"), zName);
  }

  /* Here is where the actual work of the backoffice happens */

  nThis = alert_backoffice(0);
  if( nThis ){ backoffice_log("%d alerts", nThis); nTotal += nThis; }

  nThis = hook_backoffice();
  if( nThis ){ backoffice_log("%d hooks", nThis); nTotal += nThis; }


  /* Close the log */
  if( backofficeFILE ){
    if( nTotal || backofficeLogDetail ){
      if( nTotal==0 ) backoffice_log("no-op");
#if !defined(_WIN32)
      gettimeofday(&sEnd,0);







>


>


>







644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
    }
    blob_init(&log, 0, 0);
    backofficeBlob = &log;
    blob_appendf(&log, "%s %s", db_text(0, "SELECT datetime('now')"), zName);
  }

  /* Here is where the actual work of the backoffice happens */
  g.zPhase = "backoffice-alerts";
  nThis = alert_backoffice(0);
  if( nThis ){ backoffice_log("%d alerts", nThis); nTotal += nThis; }
  g.zPhase = "backoffice-hooks";
  nThis = hook_backoffice();
  if( nThis ){ backoffice_log("%d hooks", nThis); nTotal += nThis; }
  g.zPhase = "backoffice-close";

  /* Close the log */
  if( backofficeFILE ){
    if( nTotal || backofficeLogDetail ){
      if( nTotal==0 ) backoffice_log("no-op");
#if !defined(_WIN32)
      gettimeofday(&sEnd,0);
Changes to src/branch.c.
655
656
657
658
659
660
661






662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
**        but no GLOB argument.  All other options are supported, with -t being
**        an implied no-op.
**
** > fossil branch new BRANCH-NAME BASIS ?OPTIONS?
**
**        Create a new branch BRANCH-NAME off of check-in BASIS.
**






**        Options:
**          --private             Branch is private (i.e., remains local)
**          --bgcolor COLOR       Use COLOR instead of automatic background
**          --nosign              Do not sign the manifest for the check-in
**                                that creates this branch
**          --nosync              Do not auto-sync prior to creating the branch
**          --date-override DATE  DATE to use instead of 'now'
**          --user-override USER  USER to use instead of the current default
**
**        DATE may be "now" or "YYYY-MM-DDTHH:MM:SS.SSS". If in
**        year-month-day form, it may be truncated, the "T" may be
**        replaced by a space, and it may also name a timezone offset
**        from UTC as "-HH:MM" (westward) or "+HH:MM" (eastward).
**        Either no timezone suffix or "Z" means UTC.
**
** Options:
**    -R|--repository REPO       Run commands on repository REPO
*/
void branch_cmd(void){
  int n;
  const char *zCmd = "list";
  db_find_and_open_repository(0, 0);







>
>
>
>
>
>









<
<
<
<
<
<







655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676






677
678
679
680
681
682
683
**        but no GLOB argument.  All other options are supported, with -t being
**        an implied no-op.
**
** > fossil branch new BRANCH-NAME BASIS ?OPTIONS?
**
**        Create a new branch BRANCH-NAME off of check-in BASIS.
**
**        This command is available for people who want to create a branch
**        in advance.  But  the use of this command is discouraged.  The
**        preferred idiom in Fossil is to create new branches at the point
**        of need, using the "--branch NAME" option to the "fossil commit"
**        command.
**
**        Options:
**          --private             Branch is private (i.e., remains local)
**          --bgcolor COLOR       Use COLOR instead of automatic background
**          --nosign              Do not sign the manifest for the check-in
**                                that creates this branch
**          --nosync              Do not auto-sync prior to creating the branch
**          --date-override DATE  DATE to use instead of 'now'
**          --user-override USER  USER to use instead of the current default
**






** Options:
**    -R|--repository REPO       Run commands on repository REPO
*/
void branch_cmd(void){
  int n;
  const char *zCmd = "list";
  db_find_and_open_repository(0, 0);
868
869
870
871
872
873
874

875
876
877
878
879
880
881
882
    const char *zMergeTo = db_column_text(&q, 3);
    int nCkin = db_column_int(&q, 4);
    const char *zLastCkin = db_column_text(&q, 5);
    const char *zBgClr = db_column_text(&q, 6);
    char *zAge = human_readable_age(rNow - rMtime);
    sqlite3_int64 iMtime = (sqlite3_int64)(rMtime*86400.0);
    if( zMergeTo && zMergeTo[0]==0 ) zMergeTo = 0;

    if( zBgClr == 0 ){
      if( zBranch==0 || strcmp(zBranch,"trunk")==0 ){
        zBgClr = 0;
      }else{
        zBgClr = hash_color(zBranch);
      }
    }
    if( zBgClr && zBgClr[0] && show_colors ){







>
|







868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
    const char *zMergeTo = db_column_text(&q, 3);
    int nCkin = db_column_int(&q, 4);
    const char *zLastCkin = db_column_text(&q, 5);
    const char *zBgClr = db_column_text(&q, 6);
    char *zAge = human_readable_age(rNow - rMtime);
    sqlite3_int64 iMtime = (sqlite3_int64)(rMtime*86400.0);
    if( zMergeTo && zMergeTo[0]==0 ) zMergeTo = 0;
    if( zBgClr ) zBgClr = reasonable_bg_color(zBgClr, 0);
    if( zBgClr==0 ){
      if( zBranch==0 || strcmp(zBranch,"trunk")==0 ){
        zBgClr = 0;
      }else{
        zBgClr = hash_color(zBranch);
      }
    }
    if( zBgClr && zBgClr[0] && show_colors ){
Changes to src/browse.c.
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
      int trunkRid = symbolic_name_to_rid("tag:trunk", "ci");
      linkTrunk = trunkRid && rid != trunkRid;
      linkTip = rid != symbolic_name_to_rid("tip", "ci");
      zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid);
      isSymbolicCI = (sqlite3_strnicmp(zUuid, zCI, strlen(zCI))!=0);
      isBranchCI = branch_includes_uuid(zCI, zUuid);
      if( bDocDir ) zCI = mprintf("%S", zUuid);
      Th_Store("current_checkin", zCI);
    }else{
      zCI = 0;
    }
  }

  assert( isSymbolicCI==0 || (zCI!=0 && zCI[0]!=0) );
  if( zD==0 ){







|







203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
      int trunkRid = symbolic_name_to_rid("tag:trunk", "ci");
      linkTrunk = trunkRid && rid != trunkRid;
      linkTip = rid != symbolic_name_to_rid("tip", "ci");
      zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid);
      isSymbolicCI = (sqlite3_strnicmp(zUuid, zCI, strlen(zCI))!=0);
      isBranchCI = branch_includes_uuid(zCI, zUuid);
      if( bDocDir ) zCI = mprintf("%S", zUuid);
      Th_StoreUnsafe("current_checkin", zCI);
    }else{
      zCI = 0;
    }
  }

  assert( isSymbolicCI==0 || (zCI!=0 && zCI[0]!=0) );
  if( zD==0 ){
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
      linkTip = rid != symbolic_name_to_rid("tip", "ci");
      zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid);
      rNow = db_double(0.0, "SELECT mtime FROM event WHERE objid=%d", rid);
      zNow = db_text("", "SELECT datetime(mtime,toLocal())"
                         " FROM event WHERE objid=%d", rid);
      isSymbolicCI = (sqlite3_strnicmp(zUuid, zCI, strlen(zCI)) != 0);
      isBranchCI = branch_includes_uuid(zCI, zUuid);
      Th_Store("current_checkin", zCI);
    }else{
      zCI = 0;
    }
  }
  if( zCI==0 ){
    rNow = db_double(0.0, "SELECT max(mtime) FROM event");
    zNow = db_text("", "SELECT datetime(max(mtime),toLocal()) FROM event");







|







769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
      linkTip = rid != symbolic_name_to_rid("tip", "ci");
      zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid);
      rNow = db_double(0.0, "SELECT mtime FROM event WHERE objid=%d", rid);
      zNow = db_text("", "SELECT datetime(mtime,toLocal())"
                         " FROM event WHERE objid=%d", rid);
      isSymbolicCI = (sqlite3_strnicmp(zUuid, zCI, strlen(zCI)) != 0);
      isBranchCI = branch_includes_uuid(zCI, zUuid);
      Th_StoreUnsafe("current_checkin", zCI);
    }else{
      zCI = 0;
    }
  }
  if( zCI==0 ){
    rNow = db_double(0.0, "SELECT max(mtime) FROM event");
    zNow = db_text("", "SELECT datetime(max(mtime),toLocal()) FROM event");
Changes to src/cache.c.
397
398
399
400
401
402
403
404
405




406
407
408
409
410
411


412
413
414
415
416


417
418
419
420
421
422
423
424
425
426
427




428
429
430
431
432
433
434
435
436
437
438
439

440
441


442
443
444
445






446



447

448
449


450
451



452

453
454
455
456
457
458
459
460
461
462

/*
** WEBPAGE: cachestat
**
** Show information about the webpage cache.  Requires Setup privilege.
*/
void cache_page(void){
  sqlite3 *db;
  sqlite3_stmt *pStmt;




  char zBuf[100];

  login_check_credentials();
  if( !g.perm.Setup ){ login_needed(0); return; }
  style_set_current_feature("cache");
  style_header("Web Cache Status");


  db = cacheOpen(0);
  if( db==0 ){
    @ The web-page cache is disabled for this repository
  }else{
    char *zDbName = cacheName();


    cache_register_sizename(db);
    pStmt = cacheStmt(db,
         "SELECT key, sz, nRef, datetime(tm,'unixepoch')"
         "  FROM cache"
         " ORDER BY (tm + 3600*min(nRef,48)) DESC"
    );
    if( pStmt ){
      @ <ol>
      while( sqlite3_step(pStmt)==SQLITE_ROW ){
        const unsigned char *zName = sqlite3_column_text(pStmt,0);
        char *zHash = cache_hash_of_key((const char*)zName);




        @ <li><p>%z(href("%R/cacheget?key=%T",zName))%h(zName)</a><br>
        @ size: %,lld(sqlite3_column_int64(pStmt,1))
        @ hit-count: %d(sqlite3_column_int(pStmt,2))
        @ last-access: %s(sqlite3_column_text(pStmt,3)) \
        if( zHash ){
          @ %z(href("%R/timeline?c=%S",zHash))check-in</a>\
          fossil_free(zHash);
        }
        @ </p></li>

      }
      sqlite3_finalize(pStmt);

      @ </ol>
    }


    zDbName = cacheName();
    bigSizeName(sizeof(zBuf), zBuf, file_size(zDbName, ExtFILE));
    @ <p>
    @ cache-file name: %h(zDbName)<br>






    @ cache-file size: %s(zBuf)<br>



    @ max-cache-entry: %d(db_get_int("max-cache-entry",10))

    @ </p>
    @ <p>


    @ Use the "<a href="%R/help?cmd=cache">fossil cache</a>" command
    @ on the command-line to create and configure the web-cache.



    @ </p>

    fossil_free(zDbName);
    sqlite3_close(db);
  }
  style_finish_page();
}

/*
** WEBPAGE: cacheget
**
** Usage:  /cacheget?key=KEY







|

>
>
>
>






>
>
|
|
<
<
|
>
>







<



>
>
>
>

|
|
|

|



|


>
|
|
>
>
|
<
|
|
>
>
>
>
>
>
|
>
>
>
|
>
|
|
>
>
|
|
>
>
>
|
>
|
|
<







397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419


420
421
422
423
424
425
426
427
428
429

430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454

455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481

482
483
484
485
486
487
488

/*
** WEBPAGE: cachestat
**
** Show information about the webpage cache.  Requires Setup privilege.
*/
void cache_page(void){
  sqlite3 *db = 0;
  sqlite3_stmt *pStmt;
  int doInit;
  char *zDbName = cacheName();
  int nEntry = 0;
  int mxEntry = 0;
  char zBuf[100];

  login_check_credentials();
  if( !g.perm.Setup ){ login_needed(0); return; }
  style_set_current_feature("cache");
  style_header("Web Cache Status");
  style_submenu_element("Refresh","%R/cachestat");
  doInit = P("init")!=0 && cgi_csrf_safe(2);
  db = cacheOpen(doInit);
  if( db!=0 ){


    if( P("clear")!=0 && cgi_csrf_safe(2) ){
      sqlite3_exec(db, "DELETE FROM cache; DELETE FROM blob; VACUUM;",0,0,0);
    }
    cache_register_sizename(db);
    pStmt = cacheStmt(db,
         "SELECT key, sz, nRef, datetime(tm,'unixepoch')"
         "  FROM cache"
         " ORDER BY (tm + 3600*min(nRef,48)) DESC"
    );
    if( pStmt ){

      while( sqlite3_step(pStmt)==SQLITE_ROW ){
        const unsigned char *zName = sqlite3_column_text(pStmt,0);
        char *zHash = cache_hash_of_key((const char*)zName);
        if( nEntry==0 ){
          @ <h2>Current Cache Entries:</h2>
          @ <ol>
        }
        @ <li><p>%z(href("%R/cacheget?key=%T",zName))%h(zName)</a><br>
        @ size: %,lld(sqlite3_column_int64(pStmt,1)),
        @ hit-count: %d(sqlite3_column_int(pStmt,2)),
        @ last-access: %s(sqlite3_column_text(pStmt,3))Z \
        if( zHash ){
          @ &rarr; %z(href("%R/timeline?c=%S",zHash))checkin info</a>\
          fossil_free(zHash);
        }
        @ </p></li>
        nEntry++;
      }
      sqlite3_finalize(pStmt);
      if( nEntry ){
        @ </ol>
      }
    }
  }
  @ <h2>About The Web-Cache</h2>

  @ <p>
  @ The web-cache is a separate database file that holds cached copies
  @ tarballs, ZIP archives, and other pages that are expensive to compute
  @ and are likely to be reused.
  @ <form method="post">
  login_insert_csrf_secret();
  @ <ul>
  if( db==0 ){
    @ <li> Web-cache is currently disabled.
    @ <input type="submit" name="init" value="Enable">
  }else{
    bigSizeName(sizeof(zBuf), zBuf, file_size(zDbName, ExtFILE));
    mxEntry = db_get_int("max-cache-entry",10);
    @ <li> Filename of the cache database: <b>%h(zDbName)</b>
    @ <li> Size of the cache database: %s(zBuf)
    @ <li> Maximum number of entries: %d(mxEntry)
    @ <li> Number of cache entries used: %d(nEntry)
    @ <li> Change the max-cache-entry setting on the
    @ <a href="%R/setup_settings">Settings</a> page to adjust the
    @ maximum number of entries in the cache.
    @ <li><input type="submit" name="clear" value="Clear the cache">
    @ <li> Disable the cache by manually deleting the cache database file.
  }
  @ </ul>
  @ </form>
  fossil_free(zDbName);
  if( db ) sqlite3_close(db);

  style_finish_page();
}

/*
** WEBPAGE: cacheget
**
** Usage:  /cacheget?key=KEY
Changes to src/cgi.c.
70
71
72
73
74
75
76

77
78
79
80
81
82
83
# endif
# include <winsock2.h>
# include <ws2tcpip.h>
#else
# include <sys/socket.h>
# include <sys/un.h>
# include <netinet/in.h>

# include <arpa/inet.h>
# include <sys/times.h>
# include <sys/time.h>
# include <sys/wait.h>
# include <sys/select.h>
# include <errno.h>
#endif







>







70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
# endif
# include <winsock2.h>
# include <ws2tcpip.h>
#else
# include <sys/socket.h>
# include <sys/un.h>
# include <netinet/in.h>
# include <netdb.h>
# include <arpa/inet.h>
# include <sys/times.h>
# include <sys/time.h>
# include <sys/wait.h>
# include <sys/select.h>
# include <errno.h>
#endif
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
#define P(x)          cgi_parameter((x),0)
#define PD(x,y)       cgi_parameter((x),(y))
#define PT(x)         cgi_parameter_trimmed((x),0)
#define PDT(x,y)      cgi_parameter_trimmed((x),(y))
#define PB(x)         cgi_parameter_boolean(x)
#define PCK(x)        cgi_parameter_checked(x,1)
#define PIF(x,y)      cgi_parameter_checked(x,y)
#define P_NoBot(x)    cgi_parameter_nosql((x),0)
#define PD_NoBot(x,y) cgi_parameter_nosql((x),(y))

/*
** Shortcut for the cgi_printf() routine.  Instead of using the
**
**    @ ...
**
** notation provided by the translate.c utility, you can also







|
|







102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
#define P(x)          cgi_parameter((x),0)
#define PD(x,y)       cgi_parameter((x),(y))
#define PT(x)         cgi_parameter_trimmed((x),0)
#define PDT(x,y)      cgi_parameter_trimmed((x),(y))
#define PB(x)         cgi_parameter_boolean(x)
#define PCK(x)        cgi_parameter_checked(x,1)
#define PIF(x,y)      cgi_parameter_checked(x,y)
#define P_NoBot(x)    cgi_parameter_no_attack((x),0)
#define PD_NoBot(x,y) cgi_parameter_no_attack((x),(y))

/*
** Shortcut for the cgi_printf() routine.  Instead of using the
**
**    @ ...
**
** notation provided by the translate.c utility, you can also
635
636
637
638
639
640
641



642
643
644
645
646
647
648
  cgi_reset_content();
  cgi_printf("<html>\n<p>Redirect to %h</p>\n</html>\n", zLocation);
  cgi_set_status(iStat, zStat);
  free(zLocation);
  cgi_reply();
  fossil_exit(0);
}



NORETURN void cgi_redirect(const char *zURL){
  cgi_redirect_with_status(zURL, 302, "Moved Temporarily");
}
NORETURN void cgi_redirect_with_method(const char *zURL){
  cgi_redirect_with_status(zURL, 307, "Temporary Redirect");
}
NORETURN void cgi_redirectf(const char *zFormat, ...){







>
>
>







636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
  cgi_reset_content();
  cgi_printf("<html>\n<p>Redirect to %h</p>\n</html>\n", zLocation);
  cgi_set_status(iStat, zStat);
  free(zLocation);
  cgi_reply();
  fossil_exit(0);
}
NORETURN void cgi_redirect_perm(const char *zURL){
  cgi_redirect_with_status(zURL, 301, "Moved Permanently");
}
NORETURN void cgi_redirect(const char *zURL){
  cgi_redirect_with_status(zURL, 302, "Moved Temporarily");
}
NORETURN void cgi_redirect_with_method(const char *zURL){
  cgi_redirect_with_status(zURL, 307, "Temporary Redirect");
}
NORETURN void cgi_redirectf(const char *zFormat, ...){
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638


1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
  cgi_set_status(418,"I'm a teapot");
  cgi_reply();
  fossil_errorlog("Xpossible hack attempt - 418 response on \"%s\"", zName);
  exit(0);
}

/*
** If looks_like_sql_injection() returns true for the given string, calls
** cgi_begone_spider() and does not return, else this function has no
** side effects. The range of checks performed by this function may
** be extended in the future.
**
** Checks are omitted for any logged-in user.
**
** This is NOT a defense against SQL injection.  Fossil should easily be
** proof against SQL injection without this routine.  Rather, this is an
** attempt to avoid denial-of-service caused by persistent spiders that hammer
** the server with dozens or hundreds of SQL injection attempts per second
** against pages (such as /vdiff) that are expensive to compute.  In other
** words, this is an effort to reduce the CPU load imposed by malicious
** spiders.  It is not an effect defense against SQL injection vulnerabilities.


*/
void cgi_value_spider_check(const char *zTxt, const char *zName){
  if( g.zLogin==0 && looks_like_sql_injection(zTxt) ){
    cgi_begone_spider(zName);
  }
}

/*
** A variant of cgi_parameter() with the same semantics except that if
** cgi_parameter(zName,zDefault) returns a value other than zDefault
** then it passes that value to cgi_value_spider_check().
*/
const char *cgi_parameter_nosql(const char *zName, const char *zDefault){
  const char *zTxt = cgi_parameter(zName, zDefault);

  if( zTxt!=zDefault ){
    cgi_value_spider_check(zTxt, zName);
  }
  return zTxt;
}







|






|
|
|
|
|

|
>
>


|









|







1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
  cgi_set_status(418,"I'm a teapot");
  cgi_reply();
  fossil_errorlog("Xpossible hack attempt - 418 response on \"%s\"", zName);
  exit(0);
}

/*
** If looks_like_attack() returns true for the given string, call
** cgi_begone_spider() and does not return, else this function has no
** side effects. The range of checks performed by this function may
** be extended in the future.
**
** Checks are omitted for any logged-in user.
**
** This is the primary defense against attack.  Fossil should easily be
** proof against SQL injection and XSS attacks even without without this
** routine.  Rather, this is an attempt to avoid denial-of-service caused
** by persistent spiders that hammer the server with dozens or hundreds of
** probes per seconds as they look for vulnerabilities. In other
** words, this is an effort to reduce the CPU load imposed by malicious
** spiders.  Though those routine might help make attacks harder, it is
** not itself an impenetrably barrier against attack and should not be
** relied upon as the only defense.
*/
void cgi_value_spider_check(const char *zTxt, const char *zName){
  if( g.zLogin==0 && looks_like_attack(zTxt) ){
    cgi_begone_spider(zName);
  }
}

/*
** A variant of cgi_parameter() with the same semantics except that if
** cgi_parameter(zName,zDefault) returns a value other than zDefault
** then it passes that value to cgi_value_spider_check().
*/
const char *cgi_parameter_no_attack(const char *zName, const char *zDefault){
  const char *zTxt = cgi_parameter(zName, zDefault);

  if( zTxt!=zDefault ){
    cgi_value_spider_check(zTxt, zName);
  }
  return zTxt;
}
2068
2069
2070
2071
2072
2073
2074











2075
2076
2077
2078
2079
2080
2081
2082
2083

2084
2085
2086
2087
2088

2089

2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
    zInput++;
    while( fossil_isspace(*zInput) ){ zInput++; }
  }
  if( zLeftOver ){ *zLeftOver = zInput; }
  return zResult;
}












/*
** Determine the IP address on the other side of a connection.
** Return a pointer to a string.  Or return 0 if unable.
**
** The string is held in a static buffer that is overwritten on
** each call.
*/
char *cgi_remote_ip(int fd){
#if 0

  static char zIp[100];
  struct sockaddr_in6 addr;
  socklen_t sz = sizeof(addr);
  if( getpeername(fd, &addr, &sz) ) return 0;
  zIp[0] = 0;

  if( inet_ntop(AF_INET6, &addr, zIp, sizeof(zIp))==0 ){

    return 0;
  }
  return zIp;
#else
  struct sockaddr_in remoteName;
  socklen_t size = sizeof(struct sockaddr_in);
  if( getpeername(fd, (struct sockaddr*)&remoteName, &size) ) return 0;
  return inet_ntoa(remoteName.sin_addr);
#endif
}

/*
** This routine handles a single HTTP request which is coming in on
** g.httpIn and which replies on g.httpOut
**
** The HTTP request is read from g.httpIn and is used to initialize







>
>
>
>
>
>
>
>
>
>
>








|
>
|
<
<
|
|
>
|
>


|
<
<
<
<
<
<







2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102


2103
2104
2105
2106
2107
2108
2109
2110






2111
2112
2113
2114
2115
2116
2117
    zInput++;
    while( fossil_isspace(*zInput) ){ zInput++; }
  }
  if( zLeftOver ){ *zLeftOver = zInput; }
  return zResult;
}

/*
** All possible forms of an IP address.  Needed to work around GCC strict
** aliasing rules.
*/
typedef union {
  struct sockaddr sa;              /* Abstract superclass */
  struct sockaddr_in sa4;          /* IPv4 */
  struct sockaddr_in6 sa6;         /* IPv6 */
  struct sockaddr_storage sas;     /* Should be the maximum of the above 3 */
} address;

/*
** Determine the IP address on the other side of a connection.
** Return a pointer to a string.  Or return 0 if unable.
**
** The string is held in a static buffer that is overwritten on
** each call.
*/
char *cgi_remote_ip(int fd){
  address remoteAddr;
  socklen_t size = sizeof(remoteAddr);
  static char zHost[NI_MAXHOST];


  if( getpeername(0, &remoteAddr.sa, &size) ){
    return 0;
  }
  if( getnameinfo(&remoteAddr.sa, size, zHost, sizeof(zHost), 0, 0,
                  NI_NUMERICHOST) ){
    return 0;
  }
  return zHost;






}

/*
** This routine handles a single HTTP request which is coming in on
** g.httpIn and which replies on g.httpOut
**
** The HTTP request is read from g.httpIn and is used to initialize
2484
2485
2486
2487
2488
2489
2490
2491
2492
2493
2494
2495
2496
2497
2498
    nHdr -= m+1;
  }
  fossil_free(zToFree);
  fgetc(g.httpIn);  /* Read past the "," separating header from content */
  cgi_init();
}


#if INTERFACE
/*
** Bitmap values for the flags parameter to cgi_http_server().
*/
#define HTTP_SERVER_LOCALHOST      0x0001     /* Bind to 127.0.0.1 only */
#define HTTP_SERVER_SCGI           0x0002     /* SCGI instead of HTTP */
#define HTTP_SERVER_HAD_REPOSITORY 0x0004     /* Was the repository open? */







<







2496
2497
2498
2499
2500
2501
2502

2503
2504
2505
2506
2507
2508
2509
    nHdr -= m+1;
  }
  fossil_free(zToFree);
  fgetc(g.httpIn);  /* Read past the "," separating header from content */
  cgi_init();
}


#if INTERFACE
/*
** Bitmap values for the flags parameter to cgi_http_server().
*/
#define HTTP_SERVER_LOCALHOST      0x0001     /* Bind to 127.0.0.1 only */
#define HTTP_SERVER_SCGI           0x0002     /* SCGI instead of HTTP */
#define HTTP_SERVER_HAD_REPOSITORY 0x0004     /* Was the repository open? */
2527
2528
2529
2530
2531
2532
2533


2534
2535
2536
2537
2538
2539
2540
2541

2542
2543
2544
2545
2546

2547
2548








2549
2550



2551
2552
2553
2554
2555
2556
2557
2558
2559
2560
2561
2562
2563
2564
2565
2566
2567
2568
2569
2570
2571



2572
2573
2574
2575
2576
2577
2578
2579
2580
2581
2582
2583
2584
2585
2586
2587
2588
2589
2590
2591
2592
2593
2594
2595
2596
2597
2598
2599
2600
2601
2602
2603
2604
2605
2606
2607
2608
2609
2610
2611
2612
2613



2614



2615




2616





2617
2618





2619




2620












2621




2622


2623

















2624




2625
2626
2627
2628

2629



2630








2631

2632












2633





2634
2635



2636
2637
2638
2639

2640




2641

2642






2643
2644

2645





2646
2647
2648
2649
2650
2651
2652
2653
2654
2655
2656
2657
2658
2659
2660
2661
2662





2663
2664
2665
2666
2667
2668
2669
2670
2671
2672
2673

2674
2675
2676





2677

2678
2679
2680
2681
2682
2683
2684
2685
2686
2687
2688
2689
2690
2691
2692
2693
2694
2695
2696
2697
2698
2699
2700
2701
2702
2703
2704


2705
2706
2707
2708
2709
2710
2711
2712
2713
2714
2715
  const char *zIpAddr,      /* Bind to this IP address, if not null */
  int flags                 /* HTTP_SERVER_* flags */
){
#if defined(_WIN32)
  /* Use win32_http_server() instead */
  fossil_exit(1);
#else


  int listener = -1;           /* The server socket */
  int connection;              /* A socket for each individual connection */
  int nRequest = 0;            /* Number of requests handled so far */
  fd_set readfds;              /* Set of file descriptors for select() */
  socklen_t lenaddr;           /* Length of the inaddr structure */
  int child;                   /* PID of the child process */
  int nchildren = 0;           /* Number of child processes */
  struct timeval delay;        /* How long to wait inside select() */

  struct sockaddr_in inaddr;   /* The socket address */
  struct sockaddr_un uxaddr;   /* The address for unix-domain sockets */
  int opt = 1;                 /* setsockopt flag */
  int rc;                      /* Result code from system calls */
  int iPort = mnPort;          /* Port to try to use */


  while( iPort<=mxPort ){








    if( flags & HTTP_SERVER_UNIXSOCKET ){
      /* Initialize a Unix socket named g.zSockName */



      assert( g.zSockName!=0 );
      memset(&uxaddr, 0, sizeof(uxaddr));
      if( strlen(g.zSockName)>sizeof(uxaddr.sun_path) ){
        fossil_fatal("name of unix socket too big: %s\nmax size: %d\n",
                     g.zSockName, (int)sizeof(uxaddr.sun_path));
      }
      if( file_isdir(g.zSockName, ExtFILE)!=0 ){
        if( !file_issocket(g.zSockName) ){
          fossil_fatal("cannot name socket \"%s\" because another object"
                       " with that name already exists", g.zSockName);
        }else{
          unlink(g.zSockName);
        }
      }
      uxaddr.sun_family = AF_UNIX;
      strncpy(uxaddr.sun_path, g.zSockName, sizeof(uxaddr.sun_path)-1);
      listener = socket(AF_UNIX, SOCK_STREAM, 0);
      if( listener<0 ){
        fossil_fatal("unable to create a unix socket named %s",
                     g.zSockName);
      }



      /* Set the access permission for the new socket.  Default to 0660.
      ** But use an alternative specified by --socket-mode if available.
      ** Do this before bind() to avoid a race condition. */
      if( g.zSockMode ){
        file_set_mode(g.zSockName, listener, g.zSockMode, 0);
      }else{
        file_set_mode(g.zSockName, listener, "0660", 1);
      }
    }else{
      /* Initialize a TCP/IP socket on port iPort */
      memset(&inaddr, 0, sizeof(inaddr));
      inaddr.sin_family = AF_INET;
      if( zIpAddr ){
        inaddr.sin_addr.s_addr = inet_addr(zIpAddr);
        if( inaddr.sin_addr.s_addr == INADDR_NONE ){
          fossil_fatal("not a valid IP address: %s", zIpAddr);
        }
      }else if( flags & HTTP_SERVER_LOCALHOST ){
        inaddr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
      }else{
        inaddr.sin_addr.s_addr = htonl(INADDR_ANY);
      }
      inaddr.sin_port = htons(iPort);
      listener = socket(AF_INET, SOCK_STREAM, 0);
      if( listener<0 ){
        iPort++;
        continue;
      }
    }

    /* if we can't terminate nicely, at least allow the socket to be reused */
    setsockopt(listener,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
  
    if( flags & HTTP_SERVER_UNIXSOCKET ){
      rc = bind(listener, (struct sockaddr*)&uxaddr, sizeof(uxaddr));
      /* Set the owner of the socket if requested by --socket-owner.  This
      ** must wait until after bind(), after the filesystem object has been
      ** created.  See https://lkml.org/lkml/2004/11/1/84 and
      ** https://fossil-scm.org/forum/forumpost/7517680ef9684c57 */
      if( g.zSockOwner ){
        file_set_owner(g.zSockName, listener, g.zSockOwner);
      }



    }else{



      rc = bind(listener, (struct sockaddr*)&inaddr, sizeof(inaddr));




    }





    if( rc<0 ){
      close(listener);





      iPort++;




      continue;












    }




    break;


  }

















  if( iPort>mxPort ){




    if( flags & HTTP_SERVER_UNIXSOCKET ){
      fossil_fatal("unable to listen on unix socket %s", zIpAddr);
    }else if( mnPort==mxPort ){
      fossil_fatal("unable to open listening socket on port %d", mnPort);

    }else{



      fossil_fatal("unable to open listening socket on any"








                   " port in the range %d..%d", mnPort, mxPort);

    }












  }





  if( iPort>mxPort ) return 1;
  listen(listener,10);



  if( flags & HTTP_SERVER_UNIXSOCKET ){
    fossil_print("Listening for %s requests on unix socket %s\n",
       (flags & HTTP_SERVER_SCGI)!=0 ? "SCGI" :
          g.httpUseSSL?"TLS-encrypted HTTPS":"HTTP",  g.zSockName);

  }else{




    fossil_print("Listening for %s requests on TCP port %d\n",

       (flags & HTTP_SERVER_SCGI)!=0 ? "SCGI" :






          g.httpUseSSL?"TLS-encrypted HTTPS":"HTTP",  iPort);
  }

  fflush(stdout);





  if( zBrowser && (flags & HTTP_SERVER_UNIXSOCKET)==0 ){
    assert( strstr(zBrowser,"%d")!=0 );
    zBrowser = mprintf(zBrowser /*works-like:"%d"*/, iPort);
#if defined(__CYGWIN__)
    /* On Cygwin, we can do better than "echo" */
    if( fossil_strncmp(zBrowser, "echo ", 5)==0 ){
      wchar_t *wUrl = fossil_utf8_to_unicode(zBrowser+5);
      wUrl[wcslen(wUrl)-2] = 0; /* Strip terminating " &" */
      if( (size_t)ShellExecuteW(0, L"open", wUrl, 0, 0, 1)<33 ){
        fossil_warning("cannot start browser\n");
      }
    }else
#endif
    if( fossil_system(zBrowser)<0 ){
      fossil_warning("cannot start browser: %s\n", zBrowser);
    }
  }





  while( 1 ){
#if FOSSIL_MAX_CONNECTIONS>0
    while( nchildren>=FOSSIL_MAX_CONNECTIONS ){
      if( wait(0)>=0 ) nchildren--;
    }
#endif
    delay.tv_sec = 0;
    delay.tv_usec = 100000;
    FD_ZERO(&readfds);
    assert( listener>=0 );
    FD_SET( listener, &readfds);

    select( listener+1, &readfds, 0, 0, &delay);
    if( FD_ISSET(listener, &readfds) ){
      lenaddr = sizeof(inaddr);





      connection = accept(listener, (struct sockaddr*)&inaddr, &lenaddr);

      if( connection>=0 ){
        if( flags & HTTP_SERVER_NOFORK ){
          child = 0;
        }else{
          child = fork();
        }
        if( child!=0 ){
          if( child>0 ){
            nchildren++;
            nRequest++;
          }
          close(connection);
        }else{
          int nErr = 0, fd;
          g.zSockName = 0 /* avoid deleting the socket via atexit() */;
          close(0);
          fd = dup(connection);
          if( fd!=0 ) nErr++;
          close(1);
          fd = dup(connection);
          if( fd!=1 ) nErr++;
          if( 0 && !g.fAnyTrace ){
            close(2);
            fd = dup(connection);
            if( fd!=2 ) nErr++;
          }
          close(connection);


          g.nPendingRequest = nchildren+1;
          g.nRequest = nRequest+1;
          return nErr;
        }
      }
    }
    /* Bury dead children */
    if( nchildren ){
      while(1){
        int iStatus = 0;
        pid_t x = waitpid(-1, &iStatus, WNOHANG);







>
>
|
|






>
|




>

|
>
>
>
>
>
>
>
>
|
|
>
>
>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
>
>
>
|
|
|
|
|
|
|
|
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
|
|
|
|
|
|
|
|
>
>
>
|
>
>
>
|
>
>
>
>

>
>
>
>
>
|
|
>
>
>
>
>
|
>
>
>
>
|
>
>
>
>
>
>
>
>
>
>
>
>

>
>
>
>
|
>
>
|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
|
>
>
>
>
|
<
<
<
>
|
>
>
>
|
>
>
>
>
>
>
>
>
|
>
|
>
>
>
>
>
>
>
>
>
>
>
>
|
>
>
>
>
>
|
|
>
>
>
|
<
<
<
>
|
>
>
>
>
|
>
|
>
>
>
>
>
>
|
|
>
|
>
>
>
>
>

















>
>
>
>
>









|
|
>
|
|
|
>
>
>
>
>
|
>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
>
>
|
|
|
<







2538
2539
2540
2541
2542
2543
2544
2545
2546
2547
2548
2549
2550
2551
2552
2553
2554
2555
2556
2557
2558
2559
2560
2561
2562
2563
2564
2565
2566
2567
2568
2569
2570
2571
2572
2573
2574
2575
2576
2577
2578
2579
2580
2581
2582
2583
2584
2585
2586
2587
2588
2589
2590
2591
2592
2593
2594
2595
2596
2597
2598
2599
2600
2601
2602
2603
2604
2605
2606
2607
2608


























2609
2610
2611
2612
2613
2614
2615
2616
2617
2618
2619
2620
2621
2622
2623
2624
2625
2626
2627
2628
2629
2630
2631
2632
2633
2634
2635
2636
2637
2638
2639
2640
2641
2642
2643
2644
2645
2646
2647
2648
2649
2650
2651
2652
2653
2654
2655
2656
2657
2658
2659
2660
2661
2662
2663
2664
2665
2666
2667
2668
2669
2670
2671
2672
2673
2674
2675
2676
2677
2678
2679
2680
2681
2682
2683
2684
2685
2686
2687
2688
2689
2690
2691



2692
2693
2694
2695
2696
2697
2698
2699
2700
2701
2702
2703
2704
2705
2706
2707
2708
2709
2710
2711
2712
2713
2714
2715
2716
2717
2718
2719
2720
2721
2722
2723
2724
2725
2726
2727
2728
2729
2730
2731
2732



2733
2734
2735
2736
2737
2738
2739
2740
2741
2742
2743
2744
2745
2746
2747
2748
2749
2750
2751
2752
2753
2754
2755
2756
2757
2758
2759
2760
2761
2762
2763
2764
2765
2766
2767
2768
2769
2770
2771
2772
2773
2774
2775
2776
2777
2778
2779
2780
2781
2782
2783
2784
2785
2786
2787
2788
2789
2790
2791
2792
2793
2794
2795
2796
2797
2798
2799
2800
2801
2802
2803
2804
2805
2806
2807
2808
2809
2810
2811
2812
2813
2814
2815
2816
2817
2818
2819
2820
2821
2822
2823
2824
2825
2826
2827
2828
2829
2830
2831
2832

2833
2834
2835
2836
2837
2838
2839
  const char *zIpAddr,      /* Bind to this IP address, if not null */
  int flags                 /* HTTP_SERVER_* flags */
){
#if defined(_WIN32)
  /* Use win32_http_server() instead */
  fossil_exit(1);
#else
  int listen4 = -1;            /* Main socket; IPv4 or unix-domain */
  int listen6 = -1;            /* Aux socket for corresponding IPv6 */
  int mxListen = -1;           /* Maximum of listen4 and listen6 */
  int connection;              /* An incoming connection */
  int nRequest = 0;            /* Number of requests handled so far */
  fd_set readfds;              /* Set of file descriptors for select() */
  socklen_t lenaddr;           /* Length of the inaddr structure */
  int child;                   /* PID of the child process */
  int nchildren = 0;           /* Number of child processes */
  struct timeval delay;        /* How long to wait inside select() */
  struct sockaddr_in6 inaddr6; /* Address for IPv6 */
  struct sockaddr_in inaddr4;  /* Address for IPv4 */
  struct sockaddr_un uxaddr;   /* The address for unix-domain sockets */
  int opt = 1;                 /* setsockopt flag */
  int rc;                      /* Result code from system calls */
  int iPort = mnPort;          /* Port to try to use */
  const char *zRequestType;    /* Type of requests to listen for */


  if( flags & HTTP_SERVER_SCGI ){
    zRequestType = "SCGI";
  }else if( g.httpUseSSL ){
    zRequestType = "TLS-encrypted HTTPS";
  }else{
    zRequestType = "HTTP";
  }

  if( flags & HTTP_SERVER_UNIXSOCKET ){
    /* CASE 1:  A unix socket named g.zSockName.  After creation, set the
    **          permissions on the new socket to g.zSockMode and make the
    **          owner of the socket be g.zSockOwner.
    */
    assert( g.zSockName!=0 );
    memset(&uxaddr, 0, sizeof(uxaddr));
    if( strlen(g.zSockName)>sizeof(uxaddr.sun_path) ){
      fossil_fatal("name of unix socket too big: %s\nmax size: %d\n",
                   g.zSockName, (int)sizeof(uxaddr.sun_path));
    }
    if( file_isdir(g.zSockName, ExtFILE)!=0 ){
      if( !file_issocket(g.zSockName) ){
        fossil_fatal("cannot name socket \"%s\" because another object"
                     " with that name already exists", g.zSockName);
      }else{
        unlink(g.zSockName);
      }
    }
    uxaddr.sun_family = AF_UNIX;
    strncpy(uxaddr.sun_path, g.zSockName, sizeof(uxaddr.sun_path)-1);
    listen4 = socket(AF_UNIX, SOCK_STREAM, 0);
    if( listen4<0 ){
      fossil_fatal("unable to create a unix socket named %s",
                   g.zSockName);
    }
    mxListen = listen4;
    listen6 = -1;

    /* Set the access permission for the new socket.  Default to 0660.
    ** But use an alternative specified by --socket-mode if available.
    ** Do this before bind() to avoid a race condition. */
    if( g.zSockMode ){
      file_set_mode(g.zSockName, listen4, g.zSockMode, 0);
    }else{
      file_set_mode(g.zSockName, listen4, "0660", 1);
    }


























    rc = bind(listen4, (struct sockaddr*)&uxaddr, sizeof(uxaddr));
    /* Set the owner of the socket if requested by --socket-owner.  This
    ** must wait until after bind(), after the filesystem object has been
    ** created.  See https://lkml.org/lkml/2004/11/1/84 and
    ** https://fossil-scm.org/forum/forumpost/7517680ef9684c57 */
    if( g.zSockOwner ){
      file_set_owner(g.zSockName, listen4, g.zSockOwner);
    }
    fossil_print("Listening for %s requests on unix socket %s\n",
                 zRequestType, g.zSockName);
    fflush(stdout);
  }else if( zIpAddr && strchr(zIpAddr,':')!=0 ){
    /* CASE 2: TCP on IPv6 IP address specified by zIpAddr and on port iPort.
    */
    assert( mnPort==mxPort );
    memset(&inaddr6, 0, sizeof(inaddr6));
    inaddr6.sin6_family = AF_INET6;
    inaddr6.sin6_port = htons(iPort);
    if( inet_pton(AF_INET6, zIpAddr, &inaddr6.sin6_addr)==0 ){
      fossil_fatal("not a valid IPv6 address: %s", zIpAddr);
    }
    listen6 = socket(AF_INET6, SOCK_STREAM, 0);
    if( listen6>0 ){
      opt = 1;
      setsockopt(listen6, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
      rc = bind(listen6, (struct sockaddr*)&inaddr6, sizeof(inaddr6));
      if( rc<0 ){
        close(listen6);
        listen6 = -1;
      }
    }
    if( listen6<0 ){
      fossil_fatal("cannot open a listening socket on [%s]:%d",
                   zIpAddr, mnPort);
    }
    mxListen = listen6;
    listen4 = -1;
    fossil_print("Listening for %s requests on [%s]:%d\n",
                 zRequestType, zIpAddr, iPort);
    fflush(stdout);
  }else if( zIpAddr && zIpAddr[0] ){
    /* CASE 3: TCP on IPv4 IP address specified by zIpAddr and on port iPort.
    */
    assert( mnPort==mxPort );
    memset(&inaddr4, 0, sizeof(inaddr4));
    inaddr4.sin_family = AF_INET;
    inaddr4.sin_port = htons(iPort);
    if( strcmp(zIpAddr, "localhost")==0 ) zIpAddr = "127.0.0.1";
    inaddr4.sin_addr.s_addr = inet_addr(zIpAddr);
    if( inaddr4.sin_addr.s_addr == INADDR_NONE ){
      fossil_fatal("not a valid IPv4 address: %s", zIpAddr);
    }
    listen4 = socket(AF_INET, SOCK_STREAM, 0);
    if( listen4>0 ){
      setsockopt(listen4, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
      rc = bind(listen4, (struct sockaddr*)&inaddr4, sizeof(inaddr4));
      if( rc<0 ){
        close(listen6);
        listen4 = -1;
      }
    }
    if( listen4<0 ){
      fossil_fatal("cannot open a listening socket on %s:%d",
                   zIpAddr, mnPort);
    }
    mxListen = listen4;
    listen6 = -1;
    fossil_print("Listening for %s requests on TCP port %s:%d\n",
                 zRequestType, zIpAddr, iPort);
    fflush(stdout);
  }else{
    /* CASE 4: Listen on all available IP addresses, or on only loopback
    **         addresses (if HTTP_SERVER_LOCALHOST).  The TCP port is the
    **         first available in the range of mnPort..mxPort.  Listen
    **         on both IPv4 and IPv6, if possible.  The TCP port scan is done
    **         on IPv4.
    */
    while( iPort<=mxPort ){
      const char *zProto;
      memset(&inaddr4, 0, sizeof(inaddr4));
      inaddr4.sin_family = AF_INET;
      inaddr4.sin_port = htons(iPort);
      if( flags & HTTP_SERVER_LOCALHOST ){



        inaddr4.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
      }else{
        inaddr4.sin_addr.s_addr = htonl(INADDR_ANY);
      }
      listen4 = socket(AF_INET, SOCK_STREAM, 0);
      if( listen4>0 ){
        setsockopt(listen4, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
        rc = bind(listen4, (struct sockaddr*)&inaddr4, sizeof(inaddr4));
        if( rc<0 ){
          close(listen4);
          listen4 = -1;
        }
      }
      if( listen4<0 ){
        iPort++;
        continue;
      }
      mxListen = listen4;

      /* If we get here, that means we found an open TCP port at iPort for
      ** IPv4.  Try to set up a corresponding IPv6 socket on the same port.
      */
      memset(&inaddr6, 0, sizeof(inaddr6));
      inaddr6.sin6_family = AF_INET6;
      inaddr6.sin6_port = htons(iPort);
      if( flags & HTTP_SERVER_LOCALHOST ){
        inaddr6.sin6_addr = in6addr_loopback;
      }else{
        inaddr6.sin6_addr = in6addr_any;
      }
      listen6 = socket(AF_INET6, SOCK_STREAM, 0);
      if( listen6>0 ){
        setsockopt(listen6, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
        setsockopt(listen6, IPPROTO_IPV6, IPV6_V6ONLY, &opt, sizeof(opt));
        rc = bind(listen6, (struct sockaddr*)&inaddr6, sizeof(inaddr6));
        if( rc<0 ){
          close(listen6);
          listen6 = -1;
        }
      }
      if( listen6<0 ){



        zProto = "IPv4 only";
      }else{
        zProto = "IPv4 and IPv6";
        if( listen6>listen4 ) mxListen = listen6;
      }

      fossil_print("Listening for %s requests on TCP port %s%d, %s\n",
                   zRequestType, 
                   (flags & HTTP_SERVER_LOCALHOST)!=0 ? "localhost:" : "",
                   iPort, zProto);
      fflush(stdout);
      break;
    }
    if( iPort>mxPort ){
      fossil_fatal("no available TCP ports in the range %d..%d",
                   mnPort, mxPort);
    }
  }

  /* If we get to this point, that means there is at least one listening
  ** socket on either listen4 or listen6 and perhaps on both. */
  assert( listen4>0 || listen6>0 );
  if( listen4>0 ) listen(listen4,10);
  if( listen6>0 ) listen(listen6,10);
  if( zBrowser && (flags & HTTP_SERVER_UNIXSOCKET)==0 ){
    assert( strstr(zBrowser,"%d")!=0 );
    zBrowser = mprintf(zBrowser /*works-like:"%d"*/, iPort);
#if defined(__CYGWIN__)
    /* On Cygwin, we can do better than "echo" */
    if( fossil_strncmp(zBrowser, "echo ", 5)==0 ){
      wchar_t *wUrl = fossil_utf8_to_unicode(zBrowser+5);
      wUrl[wcslen(wUrl)-2] = 0; /* Strip terminating " &" */
      if( (size_t)ShellExecuteW(0, L"open", wUrl, 0, 0, 1)<33 ){
        fossil_warning("cannot start browser\n");
      }
    }else
#endif
    if( fossil_system(zBrowser)<0 ){
      fossil_warning("cannot start browser: %s\n", zBrowser);
    }
  }

  /* What for incomming requests.  For each request, fork() a child process
  ** to deal with that request.  The child process returns.  The parent
  ** keeps on listening and never returns.
  */
  while( 1 ){
#if FOSSIL_MAX_CONNECTIONS>0
    while( nchildren>=FOSSIL_MAX_CONNECTIONS ){
      if( wait(0)>=0 ) nchildren--;
    }
#endif
    delay.tv_sec = 0;
    delay.tv_usec = 100000;
    FD_ZERO(&readfds);
    assert( listen4>0 || listen6>0 );
    if( listen4>0 ) FD_SET( listen4, &readfds);
    if( listen6>0 ) FD_SET( listen6, &readfds);
    select( mxListen+1, &readfds, 0, 0, &delay);
    if( listen4>0 && FD_ISSET(listen4, &readfds) ){
      lenaddr = sizeof(inaddr4);
      connection = accept(listen4, (struct sockaddr*)&inaddr4, &lenaddr);
    }else if( listen6>0 && FD_ISSET(listen6, &readfds) ){
      lenaddr = sizeof(inaddr6);
      connection = accept(listen6, (struct sockaddr*)&inaddr6, &lenaddr);
    }else{
      connection = -1;
    }
    if( connection>=0 ){
      if( flags & HTTP_SERVER_NOFORK ){
        child = 0;
      }else{
        child = fork();
      }
      if( child!=0 ){
        if( child>0 ){
          nchildren++;
          nRequest++;
        }
        close(connection);
      }else{
        int nErr = 0, fd;
        g.zSockName = 0 /* avoid deleting the socket via atexit() */;
        close(0);
        fd = dup(connection);
        if( fd!=0 ) nErr++;
        close(1);
        fd = dup(connection);
        if( fd!=1 ) nErr++;
        if( 0 && !g.fAnyTrace ){
          close(2);
          fd = dup(connection);
          if( fd!=2 ) nErr++;
        }
        close(connection);
        if( listen4>0 ) close(listen4);
        if( listen6>0 ) close(listen6);
        g.nPendingRequest = nchildren+1;
        g.nRequest = nRequest+1;
        return nErr;

      }
    }
    /* Bury dead children */
    if( nchildren ){
      while(1){
        int iStatus = 0;
        pid_t x = waitpid(-1, &iStatus, WNOHANG);
Changes to src/chat.c.
252
253
254
255
256
257
258
259

260
261
262
263
264
265
266
  @ window.addEventListener('load', function(){
  @ document.body.classList.add('chat');
  @ /*^^^for skins which add their own BODY tag */;
  @ window.fossil.config.chat = {
  @   fromcli: %h(PB("cli")?"true":"false"),
  @   alertSound: "%h(zAlert)",
  @   initSize: %d(db_get_int("chat-initial-history",50)),
  @   imagesInline: !!%d(db_get_boolean("chat-inline-images",1))

  @ };
  ajax_emit_js_preview_modes(0);
  chat_emit_alert_list();
  @ }, false);
  @ </script>
  builtin_request_js("fossil.page.chat.js");
  style_finish_page();







|
>







252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
  @ window.addEventListener('load', function(){
  @ document.body.classList.add('chat');
  @ /*^^^for skins which add their own BODY tag */;
  @ window.fossil.config.chat = {
  @   fromcli: %h(PB("cli")?"true":"false"),
  @   alertSound: "%h(zAlert)",
  @   initSize: %d(db_get_int("chat-initial-history",50)),
  @   imagesInline: !!%d(db_get_boolean("chat-inline-images",1)),
  @   pollTimeout: %d(db_get_int("chat-poll-timeout",420))
  @ };
  ajax_emit_js_preview_modes(0);
  chat_emit_alert_list();
  @ }, false);
  @ </script>
  builtin_request_js("fossil.page.chat.js");
  style_finish_page();
Changes to src/checkin.c.
2434
2435
2436
2437
2438
2439
2440

2441

2442
2443
2444
2445
2446

2447
2448
2449
2450
2451
2452
2453
**
** Options:
**    --allow-conflict           Allow unresolved merge conflicts
**    --allow-empty              Allow a commit with no changes
**    --allow-fork               Allow the commit to fork
**    --allow-older              Allow a commit older than its ancestor
**    --baseline                 Use a baseline manifest in the commit process

**    --branch NEW-BRANCH-NAME   Check in to this new branch

**    --close                    Close the branch being committed
**    --date-override DATETIME   Make DATETIME the time of the check-in.
**                               Useful when importing historical check-ins
**                               from another version control system.
**    --delta                    Use a delta manifest in the commit process

**    --hash                     Verify file status using hashing rather
**                               than relying on filesystem mtimes
**    --if-changes               Make this command a silent no-op if there
**                               are no changes
**    --ignore-clock-skew        If a clock skew is detected, ignore it and
**                               behave as if the user had entered 'yes' to
**                               the question of whether to proceed despite







>

>





>







2434
2435
2436
2437
2438
2439
2440
2441
2442
2443
2444
2445
2446
2447
2448
2449
2450
2451
2452
2453
2454
2455
2456
**
** Options:
**    --allow-conflict           Allow unresolved merge conflicts
**    --allow-empty              Allow a commit with no changes
**    --allow-fork               Allow the commit to fork
**    --allow-older              Allow a commit older than its ancestor
**    --baseline                 Use a baseline manifest in the commit process
**    --bgcolor COLOR            Apply COLOR to this one check-in only
**    --branch NEW-BRANCH-NAME   Check in to this new branch
**    --branchcolor COLOR        Apply given COLOR to the branch
**    --close                    Close the branch being committed
**    --date-override DATETIME   Make DATETIME the time of the check-in.
**                               Useful when importing historical check-ins
**                               from another version control system.
**    --delta                    Use a delta manifest in the commit process
**    --editor NAME              Text editor to use for check-in comment.
**    --hash                     Verify file status using hashing rather
**                               than relying on filesystem mtimes
**    --if-changes               Make this command a silent no-op if there
**                               are no changes
**    --ignore-clock-skew        If a clock skew is detected, ignore it and
**                               behave as if the user had entered 'yes' to
**                               the question of whether to proceed despite
2595
2596
2597
2598
2599
2600
2601

2602
2603
2604
2605
2606
2607
2608
  noSign = db_get_boolean("omitsign", 0)|noSign;
  if( db_get_boolean("clearsign", 0)==0 ){ noSign = 1; }
  useCksum = db_get_boolean("repo-cksum", 1);
  bIgnoreSkew = find_option("ignore-clock-skew",0,0)!=0;
  outputManifest = db_get_manifest_setting(0);
  mxSize = db_large_file_size();
  if( find_option("ignore-oversize",0,0)!=0 ) mxSize = 0;

  verify_all_options();

  /* The --no-warnings flag and the --force flag each imply
  ** the --no-verify-comment flag */
  if( noWarningFlag || forceFlag ){
    noVerifyCom = 1;
  }







>







2598
2599
2600
2601
2602
2603
2604
2605
2606
2607
2608
2609
2610
2611
2612
  noSign = db_get_boolean("omitsign", 0)|noSign;
  if( db_get_boolean("clearsign", 0)==0 ){ noSign = 1; }
  useCksum = db_get_boolean("repo-cksum", 1);
  bIgnoreSkew = find_option("ignore-clock-skew",0,0)!=0;
  outputManifest = db_get_manifest_setting(0);
  mxSize = db_large_file_size();
  if( find_option("ignore-oversize",0,0)!=0 ) mxSize = 0;
  (void)fossil_text_editor();
  verify_all_options();

  /* The --no-warnings flag and the --force flag each imply
  ** the --no-verify-comment flag */
  if( noWarningFlag || forceFlag ){
    noVerifyCom = 1;
  }
Changes to src/color.c.
18
19
20
21
22
23
24















































































































































































































































































25
26
27
28
29
30
31
** This file contains code used to select colors based on branch and
** user names.
**
*/
#include "config.h"
#include <string.h>
#include "color.h"
















































































































































































































































































/*
** Compute a hash on a branch or user name
*/
static unsigned int hash_of_name(const char *z){
  unsigned int h = 0;
  int i;







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
** This file contains code used to select colors based on branch and
** user names.
**
*/
#include "config.h"
#include <string.h>
#include "color.h"

/*
** 140 standard CSS color names and their corresponding RGB values,
** in alphabetical order by name so that we can do a binary search
** for lookup.
*/
static const struct CssColors {
  const char *zName;     /* CSS Color name, lower case */
  unsigned int iRGB;     /* Corresponding RGB value */
} aCssColors[] = {
  { "aliceblue",            0xf0f8ff },
  { "antiquewhite",         0xfaebd7 },
  { "aqua",                 0x00ffff },
  { "aquamarine",           0x7fffd4 },
  { "azure",                0xf0ffff },
  { "beige",                0xf5f5dc },
  { "bisque",               0xffe4c4 },
  { "black",                0x000000 },
  { "blanchedalmond",       0xffebcd },
  { "blue",                 0x0000ff },
  { "blueviolet",           0x8a2be2 },
  { "brown",                0xa52a2a },
  { "burlywood",            0xdeb887 },
  { "cadetblue",            0x5f9ea0 },
  { "chartreuse",           0x7fff00 },
  { "chocolate",            0xd2691e },
  { "coral",                0xff7f50 },
  { "cornflowerblue",       0x6495ed },
  { "cornsilk",             0xfff8dc },
  { "crimson",              0xdc143c },
  { "cyan",                 0x00ffff },
  { "darkblue",             0x00008b },
  { "darkcyan",             0x008b8b },
  { "darkgoldenrod",        0xb8860b },
  { "darkgray",             0xa9a9a9 },
  { "darkgreen",            0x006400 },
  { "darkkhaki",            0xbdb76b },
  { "darkmagenta",          0x8b008b },
  { "darkolivegreen",       0x556b2f },
  { "darkorange",           0xff8c00 },
  { "darkorchid",           0x9932cc },
  { "darkred",              0x8b0000 },
  { "darksalmon",           0xe9967a },
  { "darkseagreen",         0x8fbc8f },
  { "darkslateblue",        0x483d8b },
  { "darkslategray",        0x2f4f4f },
  { "darkturquoise",        0x00ced1 },
  { "darkviolet",           0x9400d3 },
  { "deeppink",             0xff1493 },
  { "deepskyblue",          0x00bfff },
  { "dimgray",              0x696969 },
  { "dodgerblue",           0x1e90ff },
  { "firebrick",            0xb22222 },
  { "floralwhite",          0xfffaf0 },
  { "forestgreen",          0x228b22 },
  { "fuchsia",              0xff00ff },
  { "gainsboro",            0xdcdcdc },
  { "ghostwhite",           0xf8f8ff },
  { "gold",                 0xffd700 },
  { "goldenrod",            0xdaa520 },
  { "gray",                 0x808080 },
  { "green",                0x008000 },
  { "greenyellow",          0xadff2f },
  { "honeydew",             0xf0fff0 },
  { "hotpink",              0xff69b4 },
  { "indianred",            0xcd5c5c },
  { "indigo",               0x4b0082 },
  { "ivory",                0xfffff0 },
  { "khaki",                0xf0e68c },
  { "lavender",             0xe6e6fa },
  { "lavenderblush",        0xfff0f5 },
  { "lawngreen",            0x7cfc00 },
  { "lemonchiffon",         0xfffacd },
  { "lightblue",            0xadd8e6 },
  { "lightcoral",           0xf08080 },
  { "lightcyan",            0xe0ffff },
  { "lightgoldenrodyellow", 0xfafad2 },
  { "lightgrey",            0xd3d3d3 },
  { "lightgreen",           0x90ee90 },
  { "lightpink",            0xffb6c1 },
  { "lightsalmon",          0xffa07a },
  { "lightseagreen",        0x20b2aa },
  { "lightskyblue",         0x87cefa },
  { "lightslategray",       0x778899 },
  { "lightsteelblue",       0xb0c4de },
  { "lightyellow",          0xffffe0 },
  { "lime",                 0x00ff00 },
  { "limegreen",            0x32cd32 },
  { "linen",                0xfaf0e6 },
  { "magenta",              0xff00ff },
  { "maroon",               0x800000 },
  { "mediumaquamarine",     0x66cdaa },
  { "mediumblue",           0x0000cd },
  { "mediumorchid",         0xba55d3 },
  { "mediumpurple",         0x9370d8 },
  { "mediumseagreen",       0x3cb371 },
  { "mediumslateblue",      0x7b68ee },
  { "mediumspringgreen",    0x00fa9a },
  { "mediumturquoise",      0x48d1cc },
  { "mediumvioletred",      0xc71585 },
  { "midnightblue",         0x191970 },
  { "mintcream",            0xf5fffa },
  { "mistyrose",            0xffe4e1 },
  { "moccasin",             0xffe4b5 },
  { "navajowhite",          0xffdead },
  { "navy",                 0x000080 },
  { "oldlace",              0xfdf5e6 },
  { "olive",                0x808000 },
  { "olivedrab",            0x6b8e23 },
  { "orange",               0xffa500 },
  { "orangered",            0xff4500 },
  { "orchid",               0xda70d6 },
  { "palegoldenrod",        0xeee8aa },
  { "palegreen",            0x98fb98 },
  { "paleturquoise",        0xafeeee },
  { "palevioletred",        0xd87093 },
  { "papayawhip",           0xffefd5 },
  { "peachpuff",            0xffdab9 },
  { "peru",                 0xcd853f },
  { "pink",                 0xffc0cb },
  { "plum",                 0xdda0dd },
  { "powderblue",           0xb0e0e6 },
  { "purple",               0x800080 },
  { "red",                  0xff0000 },
  { "rosybrown",            0xbc8f8f },
  { "royalblue",            0x4169e1 },
  { "saddlebrown",          0x8b4513 },
  { "salmon",               0xfa8072 },
  { "sandybrown",           0xf4a460 },
  { "seagreen",             0x2e8b57 },
  { "seashell",             0xfff5ee },
  { "sienna",               0xa0522d },
  { "silver",               0xc0c0c0 },
  { "skyblue",              0x87ceeb },
  { "slateblue",            0x6a5acd },
  { "slategray",            0x708090 },
  { "snow",                 0xfffafa },
  { "springgreen",          0x00ff7f },
  { "steelblue",            0x4682b4 },
  { "tan",                  0xd2b48c },
  { "teal",                 0x008080 },
  { "thistle",              0xd8bfd8 },
  { "tomato",               0xff6347 },
  { "turquoise",            0x40e0d0 },
  { "violet",               0xee82ee },
  { "wheat",                0xf5deb3 },
  { "white",                0xffffff },
  { "whitesmoke",           0xf5f5f5 },
  { "yellow",               0xffff00 },
  { "yellowgreen",          0x9acd32 },
};

/*
** Attempt to translate a CSS color name into an integer that
** represents the equivalent RGB value.  Ignore alpha if provided.
** If the name cannot be translated, return -1.
*/
int color_name_to_rgb(const char *zName){
  if( zName==0 || zName[0]==0 ) return -1;
  if( zName[0]=='#' ){
    int i, v = 0;
    for(i=1; i<=6 && fossil_isxdigit(zName[i]); i++){
      v = v*16 + fossil_hexvalue(zName[i]);
    }
    if( i==4 ){
      v = fossil_hexvalue(zName[1])*0x110000 +
          fossil_hexvalue(zName[2])*0x1100 +
          fossil_hexvalue(zName[3])*0x11;
      return v;
    }
    if( i==7 ){
      return v;
    }
    return -1;
  }else{
    int iMin = 0;
    int iMax = count(aCssColors)-1;
    while( iMin<=iMax ){
      int iMid = (iMin+iMax)/2;
      int c = sqlite3_stricmp(aCssColors[iMid].zName, zName);
      if( c==0 ) return aCssColors[iMid].iRGB;
      if( c<0 ){
        iMin = iMid+1;
      }else{
        iMax = iMid-1;
      }
    }
    return -1;
  }
}

/*
** SETTING: raw-bgcolor                  boolean default=off
**
** Fossil usually tries to adjust user-specified background colors
** for checkins so that the text is readable and so that the color
** is not too garish. This setting disables that filter.  When
** this setting is on, the user-selected background colors are shown
** exactly as requested.
*/

/*
** Shift a color provided by the user so that it is suitable
** for use as a background color in the current skin.
**
** The return value is a #HHHHHH color name contained in
** static space that is overwritten on the next call.
**
** If we cannot make sense of the background color recommendation
** that is the input, then return NULL.
**
** The iFgClr parameter is normally 0.  But for testing purposes, set
** it to 1 for a black foregrounds and 2 for a white foreground.
*/
const char *reasonable_bg_color(const char *zRequested, int iFgClr){
  int iRGB = color_name_to_rgb(zRequested);
  int r, g, b;               /* RGB components of requested color */
  static int systemFg = 0;   /* 1==black-foreground 2==white-foreground */
  int fg;                    /* Foreground color to actually use */
  static char zColor[10];    /* Return value */

  if( iFgClr ){
    fg = iFgClr;
  }else if( systemFg==0 ){
    if( db_get_boolean("raw-bgcolor",0) ){
      fg = systemFg = 3;
    }else{
      fg = systemFg = skin_detail_boolean("white-foreground") ? 2 : 1;
    }
  }else{
    fg = systemFg;
  }
  if( fg>=3 ) return zRequested;

  if( iRGB<0 ) return 0;
  r = (iRGB>>16) & 0xff;
  g = (iRGB>>8) & 0xff;
  b = iRGB & 0xff;
  if( fg==1 ){
    /* Dark text on a light background.  Adjust so that
    ** no color component is less than 255-K, resulting in
    ** a pastel background color.  Color adjustment is quadratic
    ** so that colors that are further out of range have a greater
    ** adjustment. */
    const int K = 79;
    int k, x, m;
    m = r<g ? r : g;
    if( m>b ) m = b;
    k = (m*m)/255 + K;
    x = 255 - k;
    r = (k*r)/255 + x;
    g = (k*g)/255 + x;
    b = (k*b)/255 + x;
  }else{
    /* Light text on a dark background.  Adjust so that 
    ** no color component is greater than K, resulting in
    ** a low-intensity, low-saturation background color.
    ** The color adjustment is quadratic so that colors that
    ** are further out of range have a greater adjustment. */
    const int K = 112;
    int k, m;
    m = r>g ? r : g;
    if( m<b ) m = b;
    k = 255 - (255-K)*(m*m)/65025;
    r = (k*r)/255;
    g = (k*g)/255;
    b = (k*b)/255;
  }
  sqlite3_snprintf(8, zColor, "#%02x%02x%02x", r,g,b);
  return zColor;
}

/*
** Compute a hash on a branch or user name
*/
static unsigned int hash_of_name(const char *z){
  unsigned int h = 0;
  int i;
183
184
185
186
187
188
189


















































































    @ <input type="text" size="30" name='%s(zNm)' value='%h(PD(zNm,""))'><br>
  }
  @ <input type="submit" value="Submit">
  @ <input type="submit" name="rand" value="Random">
  @ </form>
  style_finish_page();
}

























































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
    @ <input type="text" size="30" name='%s(zNm)' value='%h(PD(zNm,""))'><br>
  }
  @ <input type="submit" value="Submit">
  @ <input type="submit" name="rand" value="Random">
  @ </form>
  style_finish_page();
}

/*
** WEBPAGE: test-bgcolor
**
** Show how user-specified background colors will be rendered
** using the reasonable_bg_color() algorithm.
*/
void test_bgcolor_page(void){
  const char *zReq;      /* Requested color name */
  const char *zBG;       /* Actual color provided */
  const char *zBg1;
  char zNm[10];
  static const char *azDflt[] = {
    "red", "orange", "yellow", "green", "blue", "indigo", "violet",
    "tan", "brown", "gray",
  };
  const int N = count(azDflt);
  int i, cnt, iClr, r, g, b;
  char *zFg;
  login_check_credentials();
  style_set_current_feature("test");
  style_header("Background Color Test");
  for(i=cnt=0; i<N; i++){
    sqlite3_snprintf(sizeof(zNm),zNm,"b%c",'a'+i);
    zReq = PD(zNm,azDflt[i]);
    if( zReq==0 || zReq[0]==0 ) continue;
    if( cnt==0 ){
      @ <table border="1" cellspacing="0" cellpadding="10">
      @ <tr>
      @ <th>Requested Background
      @ <th>Light mode
      @ <th>Dark mode
      @ </tr>
    }
    cnt++;
    zBG = reasonable_bg_color(zReq, 0);
    if( zBG==0 ){
      @ <tr><td colspan="3" align="center">\
      @ "%h(zReq)" is not a recognized color name</td></tr>
      continue;
    }
    iClr = color_name_to_rgb(zReq);
    r = (iClr>>16) & 0xff;
    g = (iClr>>8) & 0xff;
    b = iClr & 0xff;
    if( 3*r + 7*g + b > 6*255 ){
      zFg = "black";
    }else{
      zFg = "white";
    }
    if( zReq[0]!='#' ){
      char zReqRGB[12];
      sqlite3_snprintf(sizeof(zReqRGB),zReqRGB,"#%06x",color_name_to_rgb(zReq));
      @ <tr><td style='color:%h(zFg);background-color:%h(zReq);'>\
      @ Requested color "%h(zReq)" (%h(zReqRGB))</td>
    }else{
      @ <tr><td style='color:%h(zFg);background-color:%s(zReq);'>\
      @ Requested color "%h(zReq)"</td>
    }
    zBg1 = reasonable_bg_color(zReq,1);
    @ <td style='color:black;background-color:%h(zBg1);'>\
    @ Background color for dark text: %h(zBg1)</td>
    zBg1 = reasonable_bg_color(zReq,2);
    @ <td style='color:white;background-color:%h(zBg1);'>\
    @ Background color for light text: %h(zBg1)</td></tr>
  }
  if( cnt ){
    @ </table>
    @ <hr>
  }
  @ <form method="POST">
  @ <p>Enter CSS color names below and see them shifted into corresponding
  @ background colors above.</p>
  for(i=0; i<N; i++){
    sqlite3_snprintf(sizeof(zNm),zNm,"b%c",'a'+i);
    @ <input type="text" size="30" name='%s(zNm)' \
    @ value='%h(PD(zNm,azDflt[i]))'><br>
  }
  @ <input type="submit" value="Submit">
  @ </form>
  style_finish_page();
}
Changes to src/db.c.
3367
3368
3369
3370
3371
3372
3373
3374
3375
3376
3377
3378
3379
3380
3381
  db_end_transaction(0);
  if( zTemplate ) db_detach("settingSrc");
  if( zProjectName ) fossil_print("project-name: %s\n", zProjectName);
  if( zProjectDesc ) fossil_print("project-description: %s\n", zProjectDesc);
  fossil_print("project-id: %s\n", db_get("project-code", 0));
  fossil_print("server-id:  %s\n", db_get("server-code", 0));
  zPassword = db_text(0, "SELECT pw FROM user WHERE login=%Q", g.zLogin);
  fossil_print("admin-user: %s (initial password is \"%s\")\n",
               g.zLogin, zPassword);
  hash_user_password(g.zLogin);
}

/*
** SQL functions for debugging.
**







|







3367
3368
3369
3370
3371
3372
3373
3374
3375
3376
3377
3378
3379
3380
3381
  db_end_transaction(0);
  if( zTemplate ) db_detach("settingSrc");
  if( zProjectName ) fossil_print("project-name: %s\n", zProjectName);
  if( zProjectDesc ) fossil_print("project-description: %s\n", zProjectDesc);
  fossil_print("project-id: %s\n", db_get("project-code", 0));
  fossil_print("server-id:  %s\n", db_get("server-code", 0));
  zPassword = db_text(0, "SELECT pw FROM user WHERE login=%Q", g.zLogin);
  fossil_print("admin-user: %s (initial remote-access password is \"%s\")\n",
               g.zLogin, zPassword);
  hash_user_password(g.zLogin);
}

/*
** SQL functions for debugging.
**
Changes to src/default.css.
749
750
751
752
753
754
755

756
757
758
759
760
761
762
}
body.tkt div.content ol.tkt-changes > li:target > ol {
  border-left: 1px solid gold;
}
body.cpage-ckout .file-change-line,
body.cpage-info .file-change-line,
body.cpage-vinfo .file-change-line,

body.cpage-vdiff .file-change-line {
  margin-top: 16px;
  margin-bottom: 16px;
  margin-right: 1em /* keep it from nudging right up against the scrollbar-reveal zone */;
  display: flex;
  flex-direction: row;
  justify-content: space-between;







>







749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
}
body.tkt div.content ol.tkt-changes > li:target > ol {
  border-left: 1px solid gold;
}
body.cpage-ckout .file-change-line,
body.cpage-info .file-change-line,
body.cpage-vinfo .file-change-line,
body.cpage-ci .file-change-line,
body.cpage-vdiff .file-change-line {
  margin-top: 16px;
  margin-bottom: 16px;
  margin-right: 1em /* keep it from nudging right up against the scrollbar-reveal zone */;
  display: flex;
  flex-direction: row;
  justify-content: space-between;
Changes to src/doc.c.
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
  /* The file is now contained in the filebody blob.  Deliver the
  ** file to the user
  */
  zMime = nMiss==0 ? P("mimetype") : 0;
  if( zMime==0 ){
    zMime = mimetype_from_name(zName);
  }
  Th_Store("doc_name", zName);
  if( vid ){
    Th_Store("doc_version", db_text(0, "SELECT '[' || substr(uuid,1,10) || ']'"
                                       "  FROM blob WHERE rid=%d", vid));
    Th_Store("doc_date", db_text(0, "SELECT datetime(mtime) FROM event"
                                    " WHERE objid=%d AND type='ci'", vid));
  }
  cgi_check_for_malice();







|







1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
  /* The file is now contained in the filebody blob.  Deliver the
  ** file to the user
  */
  zMime = nMiss==0 ? P("mimetype") : 0;
  if( zMime==0 ){
    zMime = mimetype_from_name(zName);
  }
  Th_StoreUnsafe("doc_name", zName);
  if( vid ){
    Th_Store("doc_version", db_text(0, "SELECT '[' || substr(uuid,1,10) || ']'"
                                       "  FROM blob WHERE rid=%d", vid));
    Th_Store("doc_date", db_text(0, "SELECT datetime(mtime) FROM event"
                                    " WHERE objid=%d AND type='ci'", vid));
  }
  cgi_check_for_malice();
Changes to src/encode.c.
140
141
142
143
144
145
146








































































147
148
149
150
151
152
153
        j = i+1;
        break;
    }
  }
  if( j<i ) blob_append(p, zIn+j, i-j);
}










































































/*
** Encode a string for HTTP.  This means converting lots of
** characters into the "%HH" where H is a hex digit.  It also
** means converting spaces to "+".
**
** This is the opposite of DeHttpizeString below.







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
        j = i+1;
        break;
    }
  }
  if( j<i ) blob_append(p, zIn+j, i-j);
}

/*
** Make the given string safe for HTML by converting syntax characters
** into alternatives that do not have the special syntactic meaning.
**
**     <     -->     U+227a
**     >     -->     U+227b
**     &     -->     +
**     "     -->     U+201d
**     '     -->     U+2019
**
** Return a pointer to a new string obtained from fossil_malloc().
*/
char *html_lookalike(const char *z, int n){
  unsigned char c;
  int i = 0;
  int count = 0;
  unsigned char *zOut;
  const unsigned char *zIn = (const unsigned char*)z;

  if( n<0 ) n = strlen(z);
  while( i<n ){
    switch( zIn[i] ){
      case '<':   count += 3;       break;
      case '>':   count += 3;       break;
      case '"':   count += 3;       break;
      case '\'':  count += 3;       break;
      case 0:     n = i;            break;
    }
    i++;
  }
  i = 0;
  zOut = fossil_malloc( count+n+1 );
  if( count==0 ){
    memcpy(zOut, zIn, n);
    zOut[n] = 0;
    return (char*)zOut;
  }
  while( n-->0 ){
    c = *(zIn++);
    switch( c ){
      case '<':
        zOut[i++] = 0xe2;
        zOut[i++] = 0x89;
        zOut[i++] = 0xba;
        break;
      case '>':
        zOut[i++] = 0xe2;
        zOut[i++] = 0x89;
        zOut[i++] = 0xbb;
        break;
      case '&':
        zOut[i++] = '+';
        break;
      case '"':
        zOut[i++] = 0xe2;
        zOut[i++] = 0x80;
        zOut[i++] = 0x9d;
        break;
      case '\'':
        zOut[i++] = 0xe2;
        zOut[i++] = 0x80;
        zOut[i++] = 0x99;
        break;
      default:
        zOut[i++] = c;
        break;
    }
  }
  zOut[i] = 0;
  return (char*)zOut;
}


/*
** Encode a string for HTTP.  This means converting lots of
** characters into the "%HH" where H is a hex digit.  It also
** means converting spaces to "+".
**
** This is the opposite of DeHttpizeString below.
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
  *zOut = 0;
  return zRet;
}

/*
** Convert a single HEX digit to an integer
*/
static int AsciiToHex(int c){
  if( c>='a' && c<='f' ){
    c += 10 - 'a';
  }else if( c>='A' && c<='F' ){
    c += 10 - 'A';
  }else if( c>='0' && c<='9' ){
    c -= '0';
  }else{







|







314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
  *zOut = 0;
  return zRet;
}

/*
** Convert a single HEX digit to an integer
*/
int fossil_hexvalue(int c){
  if( c>='a' && c<='f' ){
    c += 10 - 'a';
  }else if( c>='A' && c<='F' ){
    c += 10 - 'A';
  }else if( c>='0' && c<='9' ){
    c -= '0';
  }else{
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
  if( !z ) return 0;

  i = j = 0;
  while( z[i] ){
    switch( z[i] ){
      case '%':
        if( z[i+1] && z[i+2] ){
          z[j] = AsciiToHex(z[i+1]) << 4;
          z[j] |= AsciiToHex(z[i+2]);
          i += 2;
        }
        break;
      case '+':
        z[j] = ' ';
        break;
      default:







|
|







342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
  if( !z ) return 0;

  i = j = 0;
  while( z[i] ){
    switch( z[i] ){
      case '%':
        if( z[i+1] && z[i+2] ){
          z[j] = fossil_hexvalue(z[i+1]) << 4;
          z[j] |= fossil_hexvalue(z[i+2]);
          i += 2;
        }
        break;
      case '+':
        z[j] = ' ';
        break;
      default:
Changes to src/finfo.c.
634
635
636
637
638
639
640


641
642
643
644
645
646
647
    }
    db_reset(&qparent);
    if( zBr==0 ) zBr = "trunk";
    if( uBg ){
      zBgClr = user_color(zUser);
    }else if( brBg || zBgClr==0 || zBgClr[0]==0 ){
      zBgClr = strcmp(zBr,"trunk")==0 ? "" : hash_color(zBr);


    }
    gidx = graph_add_row(pGraph,
                   frid>0 ? (GraphRowId)frid*(mxfnid+1)+fnid : fpid+1000000000,
                   nParent, 0, aParent, zBr, zBgClr,
                   zUuid, 0);
    if( strncmp(zDate, zPrevDate, 10) ){
      sqlite3_snprintf(sizeof(zPrevDate), zPrevDate, "%.10s", zDate);







>
>







634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
    }
    db_reset(&qparent);
    if( zBr==0 ) zBr = "trunk";
    if( uBg ){
      zBgClr = user_color(zUser);
    }else if( brBg || zBgClr==0 || zBgClr[0]==0 ){
      zBgClr = strcmp(zBr,"trunk")==0 ? "" : hash_color(zBr);
    }else if( zBgClr ){
      zBgClr = reasonable_bg_color(zBgClr,0);
    }
    gidx = graph_add_row(pGraph,
                   frid>0 ? (GraphRowId)frid*(mxfnid+1)+fnid : fpid+1000000000,
                   nParent, 0, aParent, zBr, zBgClr,
                   zUuid, 0);
    if( strncmp(zDate, zPrevDate, 10) ){
      sqlite3_snprintf(sizeof(zPrevDate), zPrevDate, "%.10s", zDate);
Changes to src/forum.c.
57
58
59
60
61
62
63

64
65
66
67
68
69
70
*/
struct ForumThread {
  ForumPost *pFirst;     /* First post in chronological order */
  ForumPost *pLast;      /* Last post in chronological order */
  ForumPost *pDisplay;   /* Entries in display order */
  ForumPost *pTail;      /* Last on the display list */
  int mxIndent;          /* Maximum indentation level */

};
#endif /* INTERFACE */

/*
** Return true if the forum post with the given rid has been
** subsequently edited.
*/







>







57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
*/
struct ForumThread {
  ForumPost *pFirst;     /* First post in chronological order */
  ForumPost *pLast;      /* Last post in chronological order */
  ForumPost *pDisplay;   /* Entries in display order */
  ForumPost *pTail;      /* Last on the display list */
  int mxIndent;          /* Maximum indentation level */
  int nArtifact;         /* Number of forum artifacts in this thread */
};
#endif /* INTERFACE */

/*
** Return true if the forum post with the given rid has been
** subsequently edited.
*/
107
108
109
110
111
112
113
114





115
116
117
118
119
120
121
122
** value.  Returns 0 if !p. For an edited chain of post, the tag is
** checked on the pEditHead entry, to simplify subsequent unlocking of
** the post.
**
** If bCheckIrt is true then p's thread in-response-to parents are
** checked (recursively) for closure, else only p is checked.
*/
static int forumpost_is_closed(ForumPost *p, int bCheckIrt){





  while(p){
    if( p->pEditHead ) p = p->pEditHead;
    if( p->iClosed || !bCheckIrt ) return p->iClosed;
    p = p->pIrt;
  }
  return 0;
}








|
>
>
>
>
>
|







108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
** value.  Returns 0 if !p. For an edited chain of post, the tag is
** checked on the pEditHead entry, to simplify subsequent unlocking of
** the post.
**
** If bCheckIrt is true then p's thread in-response-to parents are
** checked (recursively) for closure, else only p is checked.
*/
static int forumpost_is_closed(
  ForumThread *pThread,          /* Thread that the post is a member of */
  ForumPost *p,                  /* the forum post */
  int bCheckIrt                  /* True to check In-Reply-To posts */
){
  int mx = pThread->nArtifact+1;
  while( p && (mx--)>0 ){
    if( p->pEditHead ) p = p->pEditHead;
    if( p->iClosed || !bCheckIrt ) return p->iClosed;
    p = p->pIrt;
  }
  return 0;
}

407
408
409
410
411
412
413

414
415
416
417
418
419
420
    pPost->pNext = 0;
    if( pThread->pLast==0 ){
      pThread->pFirst = pPost;
    }else{
      pThread->pLast->pNext = pPost;
    }
    pThread->pLast = pPost;


    /* Find the in-reply-to post.  Default to the topic post if the replied-to
    ** post cannot be found. */
    if( firt ){
      pPost->pIrt = pThread->pFirst;
      for(p=pThread->pFirst; p; p=p->pNext){
        if( p->fpid==firt ){







>







413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
    pPost->pNext = 0;
    if( pThread->pLast==0 ){
      pThread->pFirst = pPost;
    }else{
      pThread->pLast->pNext = pPost;
    }
    pThread->pLast = pPost;
    pThread->nArtifact++;

    /* Find the in-reply-to post.  Default to the topic post if the replied-to
    ** post cannot be found. */
    if( firt ){
      pPost->pIrt = pThread->pFirst;
      for(p=pThread->pFirst; p; p=p->pNext){
        if( p->fpid==firt ){
518
519
520
521
522
523
524

525
526
527
528
529
530
531
  froot = db_int(0, "SELECT froot FROM forumpost WHERE fpid=%d", fpid);
  if( froot==0 ){
    fossil_fatal("Not a forum post: \"%s\"", zName);
  }
  fossil_print("fpid  = %d\n", fpid);
  fossil_print("froot = %d\n", froot);
  pThread = forumthread_create(froot, 1);

  fossil_print("Chronological:\n");
  fossil_print(
/* 0         1         2         3         4         5         6         7    */
/*  123456789 123456789 123456789 123456789 123456789 123456789 123456789 123 */
  " sid  rev  closed      fpid      pIrt pEditPrev pEditTail hash\n");
  for(p=pThread->pFirst; p; p=p->pNext){
    fossil_print("%4d %4d %7d %9d %9d %9d %9d %8.8s\n",







>







525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
  froot = db_int(0, "SELECT froot FROM forumpost WHERE fpid=%d", fpid);
  if( froot==0 ){
    fossil_fatal("Not a forum post: \"%s\"", zName);
  }
  fossil_print("fpid  = %d\n", fpid);
  fossil_print("froot = %d\n", froot);
  pThread = forumthread_create(froot, 1);
  fossil_print("count = %d\n", pThread->nArtifact);
  fossil_print("Chronological:\n");
  fossil_print(
/* 0         1         2         3         4         5         6         7    */
/*  123456789 123456789 123456789 123456789 123456789 123456789 123456789 123 */
  " sid  rev  closed      fpid      pIrt pEditPrev pEditTail hash\n");
  for(p=pThread->pFirst; p; p=p->pNext){
    fossil_print("%4d %4d %7d %9d %9d %9d %9d %8.8s\n",
563
564
565
566
567
568
569

570
571
572
573
574
575
576
void forumthreadhashlist(void){
  int fpid;
  int froot;
  const char *zName = P("name");
  ForumThread *pThread;
  ForumPost *p;
  char *fuuid;


  login_check_credentials();
  if( !g.perm.Admin ){
    return;
  }
  if( zName==0 ){
    webpage_error("Missing \"name=\" query parameter");







>







571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
void forumthreadhashlist(void){
  int fpid;
  int froot;
  const char *zName = P("name");
  ForumThread *pThread;
  ForumPost *p;
  char *fuuid;
  Stmt q;

  login_check_credentials();
  if( !g.perm.Admin ){
    return;
  }
  if( zName==0 ){
    webpage_error("Missing \"name=\" query parameter");
597
598
599
600
601
602
603

















604
605
606
607
608
609
610
  @ <pre>
  pThread = forumthread_create(froot, 1);
  for(p=pThread->pFirst; p; p=p->pNext){
    @ %h(p->zUuid)
  }
  forumthread_delete(pThread);
  @ </pre>

















  style_finish_page();
}

/*
** Render a forum post for display
*/
void forum_render(







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
  @ <pre>
  pThread = forumthread_create(froot, 1);
  for(p=pThread->pFirst; p; p=p->pNext){
    @ %h(p->zUuid)
  }
  forumthread_delete(pThread);
  @ </pre>
  @ <hr>
  @ <h2>Related FORUMPOST Table Content</h2>
  @ <table border="1" cellpadding="4" cellspacing="0">
  @ <tr><th>fpid<th>froot<th>fprev<th>firt<th>fmtime
  db_prepare(&q, "SELECT fpid, froot, fprev, firt, datetime(fmtime)"
                 "  FROM forumpost"
                 " WHERE froot=%d"
                 " ORDER BY fmtime", froot);
  while( db_step(&q)==SQLITE_ROW ){
    @ <tr><td>%d(db_column_int(&q,0))\
    @ <td>%d(db_column_int(&q,1))\
    @ <td>%d(db_column_int(&q,2))\
    @ <td>%d(db_column_int(&q,3))\
    @ <td>%h(db_column_text(&q,4))</tr>
  }
  @ </table>
  db_finalize(&q);
  style_finish_page();
}

/*
** Render a forum post for display
*/
void forum_render(
723
724
725
726
727
728
729

730
731
732
733
734
735
736
}


/*
** Display a single post in a forum thread.
*/
static void forum_display_post(

  ForumPost *p,         /* Forum post to display */
  int iIndentScale,     /* Indent scale factor */
  int bRaw,             /* True to omit the border */
  int bUnf,             /* True to leave the post unformatted */
  int bHist,            /* True if showing edit history */
  int bSelect,          /* True if this is the selected post */
  char *zQuery          /* Common query string */







>







749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
}


/*
** Display a single post in a forum thread.
*/
static void forum_display_post(
  ForumThread *pThread, /* The thread that this post is a member of */
  ForumPost *p,         /* Forum post to display */
  int iIndentScale,     /* Indent scale factor */
  int bRaw,             /* True to omit the border */
  int bUnf,             /* True to leave the post unformatted */
  int bHist,            /* True if showing edit history */
  int bSelect,          /* True if this is the selected post */
  char *zQuery          /* Common query string */
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
  int iIndent;          /* Indent level */
  int iClosed;          /* True if (sub)thread is closed */
  const char *zMimetype;/* Formatting MIME type */

  /* Get the manifest for the post.  Abort if not found (e.g. shunned). */
  pManifest = manifest_get(p->fpid, CFTYPE_FORUM, 0);
  if( !pManifest ) return;
  iClosed = forumpost_is_closed(p, 1);
  /* When not in raw mode, create the border around the post. */
  if( !bRaw ){
    /* Open the <div> enclosing the post.  Set the class string to mark the post
    ** as selected and/or obsolete. */
    iIndent = (p->pEditHead ? p->pEditHead->nIndent : p->nIndent)-1;
    @ <div id='forum%d(p->fpid)' class='forumTime\
    @ %s(bSelect ? " forumSel" : "")\
    @ %s(iClosed ? " forumClosed" : "")\
    @ %s(p->pEditTail ? " forumObs" : "")' \
    if( iIndent && iIndentScale ){







|


|







772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
  int iIndent;          /* Indent level */
  int iClosed;          /* True if (sub)thread is closed */
  const char *zMimetype;/* Formatting MIME type */

  /* Get the manifest for the post.  Abort if not found (e.g. shunned). */
  pManifest = manifest_get(p->fpid, CFTYPE_FORUM, 0);
  if( !pManifest ) return;
  iClosed = forumpost_is_closed(pThread, p, 1);
  /* When not in raw mode, create the border around the post. */
  if( !bRaw ){
    /* Open the <div> enclosing the post. Set the class string to mark the post
    ** as selected and/or obsolete. */
    iIndent = (p->pEditHead ? p->pEditHead->nIndent : p->nIndent)-1;
    @ <div id='forum%d(p->fpid)' class='forumTime\
    @ %s(bSelect ? " forumSel" : "")\
    @ %s(iClosed ? " forumClosed" : "")\
    @ %s(p->pEditTail ? " forumObs" : "")' \
    if( iIndent && iIndentScale ){
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
    p = mode==FD_CHRONO ? pThread->pFirst : pThread->pDisplay;
    if( !bHist && p->pEditTail ) p = p->pEditTail;
  }

  /* Display the appropriate subset of posts in sequence. */
  while( p ){
    /* Display the post. */
    forum_display_post(p, iIndentScale, mode==FD_RAW,
        bUnf, bHist, p==pSelect, zQuery);

    /* Advance to the next post in the thread. */
    if( mode==FD_CHRONO ){
      /* Chronological mode: display posts (optionally including edits) in their
      ** original commit order. */
      if( bHist ){







|







1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
    p = mode==FD_CHRONO ? pThread->pFirst : pThread->pDisplay;
    if( !bHist && p->pEditTail ) p = p->pEditTail;
  }

  /* Display the appropriate subset of posts in sequence. */
  while( p ){
    /* Display the post. */
    forum_display_post(pThread, p, iIndentScale, mode==FD_RAW,
        bUnf, bHist, p==pSelect, zQuery);

    /* Advance to the next post in the thread. */
    if( mode==FD_CHRONO ){
      /* Chronological mode: display posts (optionally including edits) in their
      ** original commit order. */
      if( bHist ){
Changes to src/fossil.dom.js.
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
    },
    createElemFactory: function(eType){
      return function(){
        return document.createElement(eType);
      };
    },
    remove: function(e){
      if(e.forEach){
        e.forEach(
          (x)=>x.parentNode.removeChild(x)
        );
      }else{
        e.parentNode.removeChild(e);
      }
      return e;
    },
    /**
       Removes all child DOM elements from the given element
       and returns that element.








|

|


|







15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
    },
    createElemFactory: function(eType){
      return function(){
        return document.createElement(eType);
      };
    },
    remove: function(e){
      if(e?.forEach){
        e.forEach(
          (x)=>x?.parentNode?.removeChild(x)
        );
      }else{
        e?.parentNode?.removeChild(e);
      }
      return e;
    },
    /**
       Removes all child DOM elements from the given element
       and returns that element.

Changes to src/fossil.fetch.js.
25
26
27
28
29
30
31
32
33
34
35
36

37









38





39



















40
41
42
43
44
45
46
47
48
49
50
51

52
53
54
55
56
57
58
59
60
61
62



63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
   - onload: callback(responseData) (default = output response to the
   console). In the context of the callback, the options object is
   "this", noting that this call may have amended the options object
   with state other than what the caller provided.

   - onerror: callback(Error object) (default = output error message
   to console.error() and fossil.error()). Triggered if the request
   generates any response other than HTTP 200, suffers a connection
   error or timeout while awaiting a response, or if the onload()
   handler throws an exception. In the context of the callback, the
   options object is "this". Note that this function is intended to be
   used solely for error reporting, not error recovery. Because

   onerror() may be called if onload() throws, it is up to the caller









   to ensure that their onerror() callback references only state which





   is valid in such a case.




















   - method: 'POST' | 'GET' (default = 'GET'). CASE SENSITIVE!

   - payload: anything acceptable by XHR2.send(ARG) (DOMString,
   Document, FormData, Blob, File, ArrayBuffer), or a plain object or
   array, either of which gets JSON.stringify()'d. If payload is set
   then the method is automatically set to 'POST'. By default XHR2
   will set the content type based on the payload type. If an
   object/array is converted to JSON, the contentType option is
   automatically set to 'application/json', and if JSON.stringify() of
   that value fails then the exception is propagated to this
   function's caller.


   - contentType: Optional request content type when POSTing. Ignored
   if the method is not 'POST'.

   - responseType: optional string. One of ("text", "arraybuffer",
   "blob", or "document") (as specified by XHR2). Default = "text".
   As an extension, it supports "json", which tells it that the
   response is expected to be text and that it should be JSON.parse()d
   before passing it on to the onload() callback. If parsing of such
   an object fails, the onload callback is not called, and the
   onerror() callback is passed the exception from the parsing error.




   - urlParams: string|object. If a string, it is assumed to be a
   URI-encoded list of params in the form "key1=val1&key2=val2...",
   with NO leading '?'.  If it is an object, all of its properties get
   converted to that form. Either way, the parameters get appended to
   the URL before submitting the request.

   - responseHeaders: If true, the onload() callback is passed an
   additional argument: a map of all of the response headers. If it's
   a string value, the 2nd argument passed to onload() is instead the
   value of that single header. If it's an array, it's treated as a
   list of headers to return, and the 2nd argument is a map of those
   header values. When a map is passed on, all of its keys are
   lower-cased. When a given header is requested and that header is
   set multiple times, their values are (per the XHR docs)
   concatenated together with ", " between them.

   - beforesend/aftersend: optional callbacks which are called
   without arguments immediately before the request is submitted
   and immediately after it is received, regardless of success or
   error. In the context of the callback, the options object is
   the "this". These can be used to, e.g., keep track of in-flight
   requests and update the UI accordingly, e.g. disabling/enabling







|
<
|
|
|
>
|
>
>
>
>
>
>
>
>
>
|
>
>
>
>
>
|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>











|
>











>
>
>















|







25
26
27
28
29
30
31
32

33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
   - onload: callback(responseData) (default = output response to the
   console). In the context of the callback, the options object is
   "this", noting that this call may have amended the options object
   with state other than what the caller provided.

   - onerror: callback(Error object) (default = output error message
   to console.error() and fossil.error()). Triggered if the request
   generates any response other than HTTP 200, or if the beforesend()

   or onload() handler throws an exception. In the context of the
   callback, the options object is "this". This function is intended
   to be used solely for error reporting, not error recovery. Special
   cases for the Error object:

       1. Timeouts unfortunately show up as a series of 2 events: an
       HTTP 0 followed immediately by an XHR.ontimeout(). The former
       cannot(?) be unambiguously identified as the trigger for the
       pending timeout, so we have no option but to pass it on as-is
       instead of flagging it as a timeout response. The latter will
       trigger the client-provided ontimeout() if it's available (see
       below), else it calls the onerror() callback. An error object
       passed to ontimeout() by fetch() will have (.name='timeout',
       .status=XHR.status).

       2. Else if the response contains a JSON-format exception on the
       server, it will have (.name='json-error',
       status=XHR.status). Any JSON-format result object which has a
       property named "error" is considered to be a server-generated
       error.

       3. Else if it gets a non 2xx HTTP code then it will have
       (.name='http',.status=XHR.status).

       4. If onerror() throws, the exception is suppressed but may
       generate a console error message.

   - ontimeout: callback(Error object). If set, timeout errors are
   reported here, else they are reported through onerror().
   Unfortunately, XHR fires two events for a timeout: an
   onreadystatechange() and an ontimeout(), in that order.  From the
   former, however, we cannot unambiguously identify the error as
   having been caused by a timeout, so clients which set ontimeout()
   will get _two_ callback calls: one with with an HTTP error response
   followed immediately by an ontimeout() response. Error objects
   passed to this will have (.name='timeout', .status=xhr.HttpStatus).
   In the context of the callback, the options object is "this", Like
   onerror(), any exceptions thrown by the ontimeout() handler are
   suppressed, but may generate a console error message. The onerror()
   handler is _not_ called in this case.

   - method: 'POST' | 'GET' (default = 'GET'). CASE SENSITIVE!

   - payload: anything acceptable by XHR2.send(ARG) (DOMString,
   Document, FormData, Blob, File, ArrayBuffer), or a plain object or
   array, either of which gets JSON.stringify()'d. If payload is set
   then the method is automatically set to 'POST'. By default XHR2
   will set the content type based on the payload type. If an
   object/array is converted to JSON, the contentType option is
   automatically set to 'application/json', and if JSON.stringify() of
   that value fails then the exception is propagated to this
   function's caller. (beforesend(), aftersend(), and onerror() are
   NOT triggered in that case.)

   - contentType: Optional request content type when POSTing. Ignored
   if the method is not 'POST'.

   - responseType: optional string. One of ("text", "arraybuffer",
   "blob", or "document") (as specified by XHR2). Default = "text".
   As an extension, it supports "json", which tells it that the
   response is expected to be text and that it should be JSON.parse()d
   before passing it on to the onload() callback. If parsing of such
   an object fails, the onload callback is not called, and the
   onerror() callback is passed the exception from the parsing error.
   If the parsed JSON object has an "error" property, it is assumed to
   be an error string, which is used to populate a new Error object,
   which will gets (.name="json") set on it.

   - urlParams: string|object. If a string, it is assumed to be a
   URI-encoded list of params in the form "key1=val1&key2=val2...",
   with NO leading '?'.  If it is an object, all of its properties get
   converted to that form. Either way, the parameters get appended to
   the URL before submitting the request.

   - responseHeaders: If true, the onload() callback is passed an
   additional argument: a map of all of the response headers. If it's
   a string value, the 2nd argument passed to onload() is instead the
   value of that single header. If it's an array, it's treated as a
   list of headers to return, and the 2nd argument is a map of those
   header values. When a map is passed on, all of its keys are
   lower-cased. When a given header is requested and that header is
   set multiple times, their values are (per the XHR docs)
   concatenated together with "," between them.

   - beforesend/aftersend: optional callbacks which are called
   without arguments immediately before the request is submitted
   and immediately after it is received, regardless of success or
   error. In the context of the callback, the options object is
   the "this". These can be used to, e.g., keep track of in-flight
   requests and update the UI accordingly, e.g. disabling/enabling
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
        const value = parts.join(': ');
        rc[header.toLowerCase()] = value;
      });
      return rc;
    };
  }
  if('/'===uri[0]) uri = uri.substr(1);
  if(!opt) opt = {};
  else if('function'===typeof opt) opt={onload:opt};
  if(!opt.onload) opt.onload = f.onload;
  if(!opt.onerror) opt.onerror = f.onerror;
  if(!opt.beforesend) opt.beforesend = f.beforesend;
  if(!opt.aftersend) opt.aftersend = f.aftersend;
  let payload = opt.payload, jsonResponse = false;
  if(undefined!==payload){







|







168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
        const value = parts.join(': ');
        rc[header.toLowerCase()] = value;
      });
      return rc;
    };
  }
  if('/'===uri[0]) uri = uri.substr(1);
  if(!opt) opt = {}/* should arguably be Object.create(null) */;
  else if('function'===typeof opt) opt={onload:opt};
  if(!opt.onload) opt.onload = f.onload;
  if(!opt.onerror) opt.onerror = f.onerror;
  if(!opt.beforesend) opt.beforesend = f.beforesend;
  if(!opt.aftersend) opt.aftersend = f.aftersend;
  let payload = opt.payload, jsonResponse = false;
  if(undefined!==payload){
162
163
164
165
166
167
168
169
170
171









172









173

174
175
176
177
178
179
180
181
182
183
184
185




186
187
188

189
190
191

192


193





194




195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
       list. We use it as a flag to tell us to JSON.parse()
       the response. */
    jsonResponse = true;
    x.responseType = 'text';
  }else{
    x.responseType = opt.responseType||'text';
  }
  x.ontimeout = function(){
    try{opt.aftersend()}catch(e){/*ignore*/}
    opt.onerror(new Error("XHR timeout of "+x.timeout+"ms expired."));









  };









  x.onreadystatechange = function(){

    if(XMLHttpRequest.DONE !== x.readyState) return;
    try{opt.aftersend()}catch(e){/*ignore*/}
    if(false && 0===x.status){
      /* For reasons unknown, we _sometimes_ trigger x.status==0 in FF
         when the /chat page starts up, but not in Chrome nor in other
         apps. Insofar as has been determined, this happens before a
         request is actually sent and it appears to have no
         side-effects on the app other than to generate an error
         (i.e. no requests/responses are missing). This is a silly
         workaround which may or may not bite us later. If so, it can
         be removed at the cost of an unsightly console error message
         in FF. */




      return;
    }
    if(200!==x.status){

      let err;
      try{
        const j = JSON.parse(x.response);

        if(j.error) err = new Error(j.error);


      }catch(ex){/*ignore*/}





      opt.onerror(err || new Error("HTTP response status "+x.status+"."));




      return;
    }
    const orh = opt.responseHeaders;
    let head;
    if(true===orh){
      head = f.parseResponseHeaders(x.getAllResponseHeaders());
    }else if('string'===typeof orh){
      head = x.getResponseHeader(orh);
    }else if(orh instanceof Array){
      head = {};
      orh.forEach((s)=>{
        if('string' === typeof s) head[s.toLowerCase()] = x.getResponseHeader(s);
      });
    }
    try{
      const args = [(jsonResponse && x.response)
                    ? JSON.parse(x.response) : x.response];
      if(head) args.push(head);
      opt.onload.apply(opt, args);
    }catch(e){
      opt.onerror(e);
    }
  };
  try{opt.beforesend()}
  catch(e){
    opt.onerror(e);
    return;
  }
  x.open(opt.method||'GET', url.join(''), true);
  if('POST'===opt.method && 'string'===typeof opt.contentType){
    x.setRequestHeader('Content-Type',opt.contentType);
  }
  x.timeout = +opt.timeout || f.timeout;







|

|
>
>
>
>
>
>
>
>
>

>
>
>
>
>
>
>
>
>
|
>











|
>
>
>
>



>



>
|
>
>

>
>
>
>
>
|
>
>
>
>



















|
|

|

|
|







199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
       list. We use it as a flag to tell us to JSON.parse()
       the response. */
    jsonResponse = true;
    x.responseType = 'text';
  }else{
    x.responseType = opt.responseType||'text';
  }
  x.ontimeout = function(ev){
    try{opt.aftersend()}catch(e){/*ignore*/}
    const err = new Error("XHR timeout of "+x.timeout+"ms expired.");
    err.status = x.status;
    err.name = 'timeout';
    //console.warn("fetch.ontimeout",ev);
    try{
      (opt.ontimeout || opt.onerror)(err);
    }catch(e){
      /*ignore*/
      console.error("fossil.fetch()'s ontimeout() handler threw",e);
    }
  };
  /* Ensure that if onerror() throws, it's ignored. */
  const origOnError = opt.onerror;
  opt.onerror = (arg)=>{
    try{ origOnError.call(this, arg) }
    catch(e){
      /*ignored*/
      console.error("fossil.fetch()'s onerror() threw",e);
    }
  };
  x.onreadystatechange = function(ev){
    //console.warn("onreadystatechange", x.readyState, ev.target.responseText);
    if(XMLHttpRequest.DONE !== x.readyState) return;
    try{opt.aftersend()}catch(e){/*ignore*/}
    if(false && 0===x.status){
      /* For reasons unknown, we _sometimes_ trigger x.status==0 in FF
         when the /chat page starts up, but not in Chrome nor in other
         apps. Insofar as has been determined, this happens before a
         request is actually sent and it appears to have no
         side-effects on the app other than to generate an error
         (i.e. no requests/responses are missing). This is a silly
         workaround which may or may not bite us later. If so, it can
         be removed at the cost of an unsightly console error message
         in FF.

         2025-04-10: that behavior is now also in Chrome and enabling
         this workaround causes our timeout errors to never arrive.
      */
      return;
    }
    if(200!==x.status){
      //console.warn("Error response",ev.target);
      let err;
      try{
        const j = JSON.parse(x.response);
        if(j.error){
          err = new Error(j.error);
          err.name = 'json.error';
        }
      }catch(ex){/*ignore*/}
      if( !err ){
        /* We can't tell from here whether this was a timeout-capable
           request which timed out on our end or was one which is a
           genuine error. We also don't know whether the server timed
           out the connection before we did. */
        err = new Error("HTTP response status "+x.status+".")
        err.name = 'http';
      }
      err.status = x.status;
      opt.onerror(err);
      return;
    }
    const orh = opt.responseHeaders;
    let head;
    if(true===orh){
      head = f.parseResponseHeaders(x.getAllResponseHeaders());
    }else if('string'===typeof orh){
      head = x.getResponseHeader(orh);
    }else if(orh instanceof Array){
      head = {};
      orh.forEach((s)=>{
        if('string' === typeof s) head[s.toLowerCase()] = x.getResponseHeader(s);
      });
    }
    try{
      const args = [(jsonResponse && x.response)
                    ? JSON.parse(x.response) : x.response];
      if(head) args.push(head);
      opt.onload.apply(opt, args);
    }catch(err){
      opt.onerror(err);
    }
  }/*onreadystatechange()*/;
  try{opt.beforesend()}
  catch(err){
    opt.onerror(err);
    return;
  }
  x.open(opt.method||'GET', url.join(''), true);
  if('POST'===opt.method && 'string'===typeof opt.contentType){
    x.setRequestHeader('Content-Type',opt.contentType);
  }
  x.timeout = +opt.timeout || f.timeout;
Changes to src/fossil.page.chat.js.
1
2
3
4
5
6
7
8
/**
   This file contains the client-side implementation of fossil's /chat
   application.
*/
window.fossil.onPageLoad(function(){
  const F = window.fossil, D = F.dom;
  const E1 = function(selector){
    const e = document.querySelector(selector);
|







1
2
3
4
5
6
7
8
-/**
   This file contains the client-side implementation of fossil's /chat
   application.
*/
window.fossil.onPageLoad(function(){
  const F = window.fossil, D = F.dom;
  const E1 = function(selector){
    const e = document.querySelector(selector);
127
128
129
130
131
132
133


134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160


161
162
163
164
165
166
167
    resized.$disabled = true/*gets deleted when setup is finished*/;
    window.addEventListener('resize', F.debounce(resized, 250), false);
    return resized;
  })();
  fossil.FRK = ForceResizeKludge/*for debugging*/;
  const Chat = ForceResizeKludge.chat = (function(){
    const cs = { // the "Chat" object (result of this function)


      verboseErrors: false /* if true then certain, mostly extraneous,
                              error messages may be sent to the console. */,
      playedBeep: false /* used for the beep-once setting */,
      e:{/*map of certain DOM elements.*/
        messageInjectPoint: E1('#message-inject-point'),
        pageTitle: E1('head title'),
        loadOlderToolbar: undefined /* the load-posts toolbar (dynamically created) */,
        inputWrapper: E1("#chat-input-area"),
        inputElementWrapper: E1('#chat-input-line-wrapper'),
        fileSelectWrapper: E1('#chat-input-file-area'),
        viewMessages: E1('#chat-messages-wrapper'),
        btnSubmit: E1('#chat-button-submit'),
        btnAttach: E1('#chat-button-attach'),
        inputX: E1('#chat-input-field-x'),
        input1: E1('#chat-input-field-single'),
        inputM: E1('#chat-input-field-multi'),
        inputFile: E1('#chat-input-file'),
        contentDiv: E1('div.content'),
        viewConfig: E1('#chat-config'),
        viewPreview: E1('#chat-preview'),
        previewContent: E1('#chat-preview-content'),
        viewSearch: E1('#chat-search'),
        searchContent: E1('#chat-search-content'),
        btnPreview: E1('#chat-button-preview'),
        views: document.querySelectorAll('.chat-view'),
        activeUserListWrapper: E1('#chat-user-list-wrapper'),
        activeUserList: E1('#chat-user-list')


      },
      me: F.user.name,
      mxMsg: F.config.chat.initSize ? -F.config.chat.initSize : -50,
      mnMsg: undefined/*lowest message ID we've seen so far (for history loading)*/,
      pageIsActive: 'visible'===document.visibilityState,
      changesSincePageHidden: 0,
      notificationBubbleColor: 'white',







>
>
|
|





|
|

















|
>
>







127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
    resized.$disabled = true/*gets deleted when setup is finished*/;
    window.addEventListener('resize', F.debounce(resized, 250), false);
    return resized;
  })();
  fossil.FRK = ForceResizeKludge/*for debugging*/;
  const Chat = ForceResizeKludge.chat = (function(){
    const cs = { // the "Chat" object (result of this function)
      beVerbose: false
      //!!window.location.hostname.match("localhost")
      /* if true then certain, mostly extraneous, error messages and
         log messages may be sent to the console. */,
      playedBeep: false /* used for the beep-once setting */,
      e:{/*map of certain DOM elements.*/
        messageInjectPoint: E1('#message-inject-point'),
        pageTitle: E1('head title'),
        loadOlderToolbar: undefined /* the load-posts toolbar (dynamically created) */,
        inputArea: E1("#chat-input-area"),
        inputLineWrapper: E1('#chat-input-line-wrapper'),
        fileSelectWrapper: E1('#chat-input-file-area'),
        viewMessages: E1('#chat-messages-wrapper'),
        btnSubmit: E1('#chat-button-submit'),
        btnAttach: E1('#chat-button-attach'),
        inputX: E1('#chat-input-field-x'),
        input1: E1('#chat-input-field-single'),
        inputM: E1('#chat-input-field-multi'),
        inputFile: E1('#chat-input-file'),
        contentDiv: E1('div.content'),
        viewConfig: E1('#chat-config'),
        viewPreview: E1('#chat-preview'),
        previewContent: E1('#chat-preview-content'),
        viewSearch: E1('#chat-search'),
        searchContent: E1('#chat-search-content'),
        btnPreview: E1('#chat-button-preview'),
        views: document.querySelectorAll('.chat-view'),
        activeUserListWrapper: E1('#chat-user-list-wrapper'),
        activeUserList: E1('#chat-user-list'),
        eMsgPollError: undefined /* current connection error MessageMidget */,
        pollErrorMarker: document.body /* element to toggle 'connection-error' CSS class on */
      },
      me: F.user.name,
      mxMsg: F.config.chat.initSize ? -F.config.chat.initSize : -50,
      mnMsg: undefined/*lowest message ID we've seen so far (for history loading)*/,
      pageIsActive: 'visible'===document.visibilityState,
      changesSincePageHidden: 0,
      notificationBubbleColor: 'white',
177
178
179
180
181
182
183































































































184
185
186
187
188
189
190
           new Date((J - 2440587.5) * 86400000) */
      },
      filterState:{
        activeUser: undefined,
        match: function(uname){
          return this.activeUser===uname || !this.activeUser;
        }































































































      },
      /**
         Gets (no args) or sets (1 arg) the current input text field
         value, taking into account single- vs multi-line input. The
         getter returns a trim()'d string and the setter returns this
         object. As a special case, if arguments[0] is a boolean
         value, it behaves like a getter and, if arguments[0]===true







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
           new Date((J - 2440587.5) * 86400000) */
      },
      filterState:{
        activeUser: undefined,
        match: function(uname){
          return this.activeUser===uname || !this.activeUser;
        }
      },
      /**
         The timer object is used to control connection throttling
         when connection errors arrise. It starts off with a polling
         delay of $initialDelay ms. If there's a connection error,
         that gets bumped by some value for each subsequent error, up
         to some max value.

         The timing of resetting the delay when service returns is,
         because of the long-poll connection and our lack of low-level
         insight into the connection at this level, a bit wonky.
      */
      timer:{
        /* setTimeout() ID for (delayed) starting a Chat.poll(), so
           that it runs at controlled intervals (which change when a
           connection drops and recovers). */
        tidPendingPoll: undefined,
        tidClearPollErr: undefined /*setTimeout() timer id for
                                     reconnection determination. See
                                     clearPollErrOnWait(). */,
        $initialDelay: 1000 /* initial polling interval (ms) */,
        currentDelay: 1000 /* current polling interval */,
        maxDelay: 60000 * 5 /* max interval when backing off for
                              connection errors */,
        minDelay: 5000 /* minimum delay time for a back-off/retry
                          attempt. */,
        errCount: 0 /* Current poller connection error count */,
        minErrForNotify: 4 /* Don't warn for connection errors until this
                              many have occurred */,
        pollTimeout: (1 && window.location.hostname.match(
          "localhost" /*presumably local dev mode*/
        )) ? 15000
          : (+F.config.chat.pollTimeout>0
             ? (1000 * (F.config.chat.pollTimeout - Math.floor(F.config.chat.pollTimeout * 0.1)))
             /* ^^^^^^^^^^^^ we want our timeouts to be slightly shorter
                than the server's so that we can distingished timed-out
                polls on our end from HTTP errors (if the server times
                out). */
             : 30000),
        /** Returns a random fudge value for reconnect attempt times,
            intended to keep the /chat server from getting hammered if
            all clients which were just disconnected all reconnect at
            the same instant. */
        randomInterval: function(factor){
          return Math.floor(Math.random() * factor);
        },
        /** Increments the reconnection delay, within some min/max range. */
        incrDelay: function(){
          if( this.maxDelay > this.currentDelay ){
            if(this.currentDelay < this.minDelay){
              this.currentDelay = this.minDelay + this.randomInterval(this.minDelay);
            }else{
              this.currentDelay = this.currentDelay*2 + this.randomInterval(this.currentDelay);
            }
          }
          return this.currentDelay;
        },
        /** Resets the delay counter to v || its initial value. */
        resetDelay: function(ms=0){
          return this.currentDelay = ms || this.$initialDelay;
        },
        /** Returns true if the timer is set to delayed mode. */
        isDelayed: function(){
          return (this.currentDelay > this.$initialDelay) ? this.currentDelay : 0;
        },
        /**
           Cancels any in-progress pending-poll timer and starts a new
           one with the given delay, defaulting to this.resetDelay().
        */
        startPendingPollTimer: function(delay){
          this.cancelPendingPollTimer().tidPendingPoll
            = setTimeout( Chat.poll, delay || Chat.timer.resetDelay() );
          return this;
        },
        /**
           Cancels any still-active timer set to trigger the next
           Chat.poll().
        */
        cancelPendingPollTimer: function(){
          if( this.tidPendingPoll ){
            clearTimeout(this.tidPendingPoll);
            this.tidPendingPoll = 0;
          }
          return this;
        },
        /**
           Cancels any pending reconnection attempt back-off timer..
        */
        cancelReconnectCheckTimer: function(){
          if( this.tidClearPollErr ){
            clearTimeout(this.tidClearPollErr);
            this.tidClearPollErr = 0;
          }
          return this;
        }
      },
      /**
         Gets (no args) or sets (1 arg) the current input text field
         value, taking into account single- vs multi-line input. The
         getter returns a trim()'d string and the setter returns this
         object. As a special case, if arguments[0] is a boolean
         value, it behaves like a getter and, if arguments[0]===true
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
        return this;
      },

      /**
         If animations are enabled, passes its arguments
         to D.addClassBriefly(), else this is a no-op.
         If cb is a function, it is called after the
         CSS class is removed. Returns this object; 
      */
      animate: function f(e,a,cb){
        if(!f.$disabled){
          D.addClassBriefly(e, a, 0, cb);
        }
        return this;
      }
    };
    cs.e.inputFields = [ cs.e.input1, cs.e.inputM, cs.e.inputX ];
    cs.e.inputFields.$currentIndex = 0;
    cs.e.inputFields.forEach(function(e,ndx){
      if(ndx===cs.e.inputFields.$currentIndex) D.removeClass(e,'hidden');
      else D.addClass(e,'hidden');
    });
    if(D.attr(cs.e.inputX,'contenteditable','plaintext-only').isContentEditable){







|







|







703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
        return this;
      },

      /**
         If animations are enabled, passes its arguments
         to D.addClassBriefly(), else this is a no-op.
         If cb is a function, it is called after the
         CSS class is removed. Returns this object;
      */
      animate: function f(e,a,cb){
        if(!f.$disabled){
          D.addClassBriefly(e, a, 0, cb);
        }
        return this;
      }
    }/*Chat object*/;
    cs.e.inputFields = [ cs.e.input1, cs.e.inputM, cs.e.inputX ];
    cs.e.inputFields.$currentIndex = 0;
    cs.e.inputFields.forEach(function(e,ndx){
      if(ndx===cs.e.inputFields.$currentIndex) D.removeClass(e,'hidden');
      else D.addClass(e,'hidden');
    });
    if(D.attr(cs.e.inputX,'contenteditable','plaintext-only').isContentEditable){
643
644
645
646
647
648
649


650
651
652
653
654
655
656
657
658
659
660

661

662
663
664
665
666






















667
668
669
670
671
672

673
674
675
676
677
678
679
       Accepts any argument types valid for fossil.toast.error().
    */
    cs.reportError = function(/*msg args*/){
      const args = argsToArray(arguments);
      console.error("chat error:",args);
      F.toast.error.apply(F.toast, args);
    };


    /**
       Reports an error in the form of a new message in the chat
       feed. All arguments are appended to the message's content area
       using fossil.dom.append(), so may be of any type supported by
       that function.
    */
    cs.reportErrorAsMessage = function f(/*msg args*/){
      if(undefined === f.$msgid) f.$msgid=0;
      const args = argsToArray(arguments).map(function(v){
        return (v instanceof Error) ? v.message : v;
      });

      console.error("chat error:",args);

      const d = new Date().toISOString(),
            mw = new this.MessageWidget({
              isError: true,
              xfrom: null,
              msgid: "error-"+(++f.$msgid),






















              mtime: d,
              lmtime: d,
              xmsg: args
            });
      this.injectMessageElem(mw.e.body);
      mw.scrollIntoView();

    };

    cs.getMessageElemById = function(id){
      return qs('[data-msgid="'+id+'"]');
    };

    /** Finds the last .message-widget element and returns it or







>
>







<



>
|
>



|
|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>






>







742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757

758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
       Accepts any argument types valid for fossil.toast.error().
    */
    cs.reportError = function(/*msg args*/){
      const args = argsToArray(arguments);
      console.error("chat error:",args);
      F.toast.error.apply(F.toast, args);
    };

    let InternalMsgId = 0;
    /**
       Reports an error in the form of a new message in the chat
       feed. All arguments are appended to the message's content area
       using fossil.dom.append(), so may be of any type supported by
       that function.
    */
    cs.reportErrorAsMessage = function f(/*msg args*/){

      const args = argsToArray(arguments).map(function(v){
        return (v instanceof Error) ? v.message : v;
      });
      if(Chat.beVerbose){
        console.error("chat error:",args);
      }
      const d = new Date().toISOString(),
            mw = new this.MessageWidget({
              isError: true,
              xfrom: undefined,
              msgid: "error-"+(++InternalMsgId),
              mtime: d,
              lmtime: d,
              xmsg: args
            });
      this.injectMessageElem(mw.e.body);
      mw.scrollIntoView();
      return mw;
    };

    /**
       For use by the connection poller to send a "connection
       restored" message.
    */
    cs.reportReconnection = function f(/*msg args*/){
      const args = argsToArray(arguments).map(function(v){
        return (v instanceof Error) ? v.message : v;
      });
      const d = new Date().toISOString(),
            mw = new this.MessageWidget({
              isError: false,
              xfrom: undefined,
              msgid: "reconnect-"+(++InternalMsgId),
              mtime: d,
              lmtime: d,
              xmsg: args
            });
      this.injectMessageElem(mw.e.body);
      mw.scrollIntoView();
      return mw;
    };

    cs.getMessageElemById = function(id){
      return qs('[data-msgid="'+id+'"]');
    };

    /** Finds the last .message-widget element and returns it or
688
689
690
691
692
693
694
695
696
697
698
699














700
701
702
703
704
705
706
707

708

709
710
711
712
713
714
715
    };

    /**
       LOCALLY deletes a message element by the message ID or passing
       the .message-row element. Returns true if it removes an element,
       else false.
    */
    cs.deleteMessageElem = function(id){
      var e;
      if(id instanceof HTMLElement){
        e = id;
        id = e.dataset.msgid;














      }else{
        e = this.getMessageElemById(id);
      }
      if(e && id){
        D.remove(e);
        if(e===this.e.newestMessage){
          this.fetchLastMessageElem();
        }

        F.toast.message("Deleted message "+id+".");

      }
      return !!e;
    };

    /**
       Toggles the given message between its parsed and plain-text
       representations. It requires a server round-trip to collect the







|




>
>
>
>
>
>
>
>
>
>
>
>
>
>
|







>
|
>







813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
    };

    /**
       LOCALLY deletes a message element by the message ID or passing
       the .message-row element. Returns true if it removes an element,
       else false.
    */
    cs.deleteMessageElem = function(id, silent){
      var e;
      if(id instanceof HTMLElement){
        e = id;
        id = e.dataset.msgid;
        delete e.dataset.msgid;
        if( e?.dataset?.alsoRemove ){
          const xId = e.dataset.alsoRemove;
          delete e.dataset.alsoRemove;
          this.deleteMessageElem( xId );
        }
      }else if(id instanceof Chat.MessageWidget) {
        if( this.e.eMsgPollError === e ){
          this.e.eMsgPollError = undefined;
        }
        if(id.e?.body){
          this.deleteMessageElem(id.e.body);
        }
        return;
      } else{
        e = this.getMessageElemById(id);
      }
      if(e && id){
        D.remove(e);
        if(e===this.e.newestMessage){
          this.fetchLastMessageElem();
        }
        if( !silent ){
          F.toast.message("Deleted message "+id+".");
        }
      }
      return !!e;
    };

    /**
       Toggles the given message between its parsed and plain-text
       representations. It requires a server round-trip to collect the
774
775
776
777
778
779
780

781
782
783
784
785
786
787
      }
      // We need to fetch the plain-text version...
      const self = this;
      F.fetch('chat-fetch-one',{
        urlParams:{ name: id, raw: true},
        responseType: 'json',
        onload: function(msg){

          content.$elems[1] = D.append(D.pre(),msg.xmsg);
          content.$elems[1]._xmsgRaw = msg.xmsg/*used for copy-to-clipboard feature*/;
          self.toggleTextMode(e);
        },
        aftersend:function(){
          delete e.$isToggling;
          Chat.ajaxEnd();







>







915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
      }
      // We need to fetch the plain-text version...
      const self = this;
      F.fetch('chat-fetch-one',{
        urlParams:{ name: id, raw: true},
        responseType: 'json',
        onload: function(msg){
          reportConnectionOkay('chat-fetch-one');
          content.$elems[1] = D.append(D.pre(),msg.xmsg);
          content.$elems[1]._xmsgRaw = msg.xmsg/*used for copy-to-clipboard feature*/;
          self.toggleTextMode(e);
        },
        aftersend:function(){
          delete e.$isToggling;
          Chat.ajaxEnd();
832
833
834
835
836
837
838
839
840
841


842

843
844
845
846
847
848
849
        e = id;
        id = e.dataset.msgid;
      }else{
        e = this.getMessageElemById(id);
      }
      if(!(e instanceof HTMLElement)) return;
      if(this.userMayDelete(e)){
        this.ajaxStart();
        F.fetch("chat-delete/" + id, {
          responseType: 'json',


          onload:(r)=>this.deleteMessageElem(r),

          onerror:(err)=>this.reportErrorAsMessage(err)
        });
      }else{
        this.deleteMessageElem(id);
      }
    };
    document.addEventListener('visibilitychange', function(ev){







<


>
>
|
>







974
975
976
977
978
979
980

981
982
983
984
985
986
987
988
989
990
991
992
993
        e = id;
        id = e.dataset.msgid;
      }else{
        e = this.getMessageElemById(id);
      }
      if(!(e instanceof HTMLElement)) return;
      if(this.userMayDelete(e)){

        F.fetch("chat-delete/" + id, {
          responseType: 'json',
          onload:(r)=>{
            reportConnectionOkay('chat-delete');
            this.deleteMessageElem(r);
          },
          onerror:(err)=>this.reportErrorAsMessage(err)
        });
      }else{
        this.deleteMessageElem(id);
      }
    };
    document.addEventListener('visibilitychange', function(ev){
1033
1034
1035
1036
1037
1038
1039

1040
1041
1042
1043
1044
1045
1046
      }
    };

    ctor.prototype = {
      scrollIntoView: function(){
        this.e.content.scrollIntoView();
      },

      setMessage: function(m){
        const ds = this.e.body.dataset;
        ds.timestamp = m.mtime;
        ds.lmtime = m.lmtime;
        ds.msgid = m.msgid;
        ds.xfrom = m.xfrom || '';
        if(m.xfrom === Chat.me){







>







1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
      }
    };

    ctor.prototype = {
      scrollIntoView: function(){
        this.e.content.scrollIntoView();
      },
      //remove: function(silent){Chat.deleteMessageElem(this, silent);},
      setMessage: function(m){
        const ds = this.e.body.dataset;
        ds.timestamp = m.mtime;
        ds.lmtime = m.lmtime;
        ds.msgid = m.msgid;
        ds.xfrom = m.xfrom || '';
        if(m.xfrom === Chat.me){
1210
1211
1212
1213
1214
1215
1216
1217
1218










1219
1220
1221
1222
1223
1224
1225
              const toolbar = D.addClass(D.div(), 'toolbar');
              D.append(this.e, toolbar);
              const btnDeleteLocal = D.button("Delete locally");
              D.append(toolbar, btnDeleteLocal);
              const self = this;
              btnDeleteLocal.addEventListener('click', function(){
                self.hide();
                Chat.deleteMessageElem(eMsg);
              });










              if(Chat.userMayDelete(eMsg)){
                const btnDeleteGlobal = D.button("Delete globally");
                D.append(toolbar, btnDeleteGlobal);
                F.confirmer(btnDeleteGlobal,{
                  pinSize: true,
                  ticks: F.config.confirmerButtonTicks,
                  confirmText: "Confirm delete?",







|

>
>
>
>
>
>
>
>
>
>







1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
              const toolbar = D.addClass(D.div(), 'toolbar');
              D.append(this.e, toolbar);
              const btnDeleteLocal = D.button("Delete locally");
              D.append(toolbar, btnDeleteLocal);
              const self = this;
              btnDeleteLocal.addEventListener('click', function(){
                self.hide();
                Chat.deleteMessageElem(eMsg)
              });
              if( eMsg.classList.contains('notification') ){
                const btnDeletePoll = D.button("Delete /chat notifications?");
                D.append(toolbar, btnDeletePoll);
                btnDeletePoll.addEventListener('click', function(){
                  self.hide();
                  Chat.e.viewMessages.querySelectorAll(
                    '.message-widget.notification:not(.resend-message)'
                  ).forEach(e=>Chat.deleteMessageElem(e, true));
                });
              }
              if(Chat.userMayDelete(eMsg)){
                const btnDeleteGlobal = D.button("Delete globally");
                D.append(toolbar, btnDeleteGlobal);
                F.confirmer(btnDeleteGlobal,{
                  pinSize: true,
                  ticks: F.config.confirmerButtonTicks,
                  confirmText: "Confirm delete?",
1455
1456
1457
1458
1459
1460
1461

1462
1463
1464
1465
1466
1467
1468
          urlParams:{
            q: '',
            n: nFetch,
            i: iFirst
          },
          responseType: "json",
          onload:function(jx){

            if( bDown ) jx.msgs.reverse();
            jx.msgs.forEach((m) => {
              m.isSearchResult = true;
              var mw = new Chat.MessageWidget(m);
              if( bDown ){
                /* Inject the message below this object's body, or
                   append it to Chat.e.searchContent if this element







>







1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
          urlParams:{
            q: '',
            n: nFetch,
            i: iFirst
          },
          responseType: "json",
          onload:function(jx){
            reportConnectionOkay('chat-query');
            if( bDown ) jx.msgs.reverse();
            jx.msgs.forEach((m) => {
              m.isSearchResult = true;
              var mw = new Chat.MessageWidget(m);
              if( bDown ){
                /* Inject the message below this object's body, or
                   append it to Chat.e.searchContent if this element
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
        D.append(dd, D.br(), img);
        const reader = new FileReader();
        reader.onload = (e)=>img.setAttribute('src', e.target.result);
        reader.readAsDataURL(blob);
      }
    };
    Chat.e.inputFile.addEventListener('change', function(ev){
      updateDropZoneContent(this.files && this.files[0] ? this.files[0] : undefined)
    });
    /* Handle image paste from clipboard. TODO: figure out how we can
       paste non-image binary data as if it had been selected via the
       file selection element. */
    const pasteListener = function(event){
      const items = event.clipboardData.items,
            item = items[0];







|







1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
        D.append(dd, D.br(), img);
        const reader = new FileReader();
        reader.onload = (e)=>img.setAttribute('src', e.target.result);
        reader.readAsDataURL(blob);
      }
    };
    Chat.e.inputFile.addEventListener('change', function(ev){
      updateDropZoneContent(this?.files[0])
    });
    /* Handle image paste from clipboard. TODO: figure out how we can
       paste non-image binary data as if it had been selected via the
       file selection element. */
    const pasteListener = function(event){
      const items = event.clipboardData.items,
            item = items[0];
1602
1603
1604
1605
1606
1607
1608

1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627



































1628
1629
1630
1631
1632
1633
1634
    const w = D.addClass(D.div(), 'failed-message');
    D.append(w, D.append(
      D.span(),"This message was not successfully sent to the server:"
    ));
    if(state.msg){
      const ta = D.textarea();
      ta.value = state.msg;

      D.append(w,ta);
    }
    if(state.blob){
      D.append(w,D.append(D.span(),"Attachment: ",(state.blob.name||"unnamed")));
      //console.debug("blob = ",state.blob);
    }
    const buttons = D.addClass(D.div(), 'buttons');
    D.append(w, buttons);
    D.append(buttons, D.button("Discard message?", function(){
      const theMsg = findMessageWidgetParent(w);
      if(theMsg) Chat.deleteMessageElem(theMsg);
    }));
    D.append(buttons, D.button("Edit message and try again?", function(){
      if(state.msg) Chat.inputValue(state.msg);
      if(state.blob) BlobXferState.updateDropZoneContent(state.blob);
      const theMsg = findMessageWidgetParent(w);
      if(theMsg) Chat.deleteMessageElem(theMsg);
    }));
    Chat.reportErrorAsMessage(w);



































  };

  /**
     Submits the contents of the message input field (if not empty)
     and/or the file attachment field to the server. If both are
     empty, this is a no-op.








>


















|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
    const w = D.addClass(D.div(), 'failed-message');
    D.append(w, D.append(
      D.span(),"This message was not successfully sent to the server:"
    ));
    if(state.msg){
      const ta = D.textarea();
      ta.value = state.msg;
      ta.setAttribute('readonly','true');
      D.append(w,ta);
    }
    if(state.blob){
      D.append(w,D.append(D.span(),"Attachment: ",(state.blob.name||"unnamed")));
      //console.debug("blob = ",state.blob);
    }
    const buttons = D.addClass(D.div(), 'buttons');
    D.append(w, buttons);
    D.append(buttons, D.button("Discard message?", function(){
      const theMsg = findMessageWidgetParent(w);
      if(theMsg) Chat.deleteMessageElem(theMsg);
    }));
    D.append(buttons, D.button("Edit message and try again?", function(){
      if(state.msg) Chat.inputValue(state.msg);
      if(state.blob) BlobXferState.updateDropZoneContent(state.blob);
      const theMsg = findMessageWidgetParent(w);
      if(theMsg) Chat.deleteMessageElem(theMsg);
    }));
    D.addClass(Chat.reportErrorAsMessage(w).e.body, "resend-message");
  };

  /* Assume the connection has been established, reset the
     Chat.timer.tidClearPollErr, and (if showMsg and
     !!Chat.e.eMsgPollError) alert the user that the outage appears to
     be over. Also schedule Chat.poll() to run in the very near
     future. */
  const reportConnectionOkay = function(dbgContext, showMsg = true){
    if(Chat.beVerbose){
      console.warn('reportConnectionOkay', dbgContext,
                   'Chat.e.pollErrorMarker classes =',
                   Chat.e.pollErrorMarker.classList,
                   'Chat.timer.tidClearPollErr =',Chat.timer.tidClearPollErr,
                   'Chat.timer =',Chat.timer);
    }
    if( Chat.timer.errCount ){
      D.removeClass(Chat.e.pollErrorMarker, 'connection-error');
      Chat.timer.errCount = 0;
    }
    Chat.timer.cancelReconnectCheckTimer().startPendingPollTimer();
    if( Chat.e.eMsgPollError ) {
      const oldErrMsg = Chat.e.eMsgPollError;
      Chat.e.eMsgPollError = undefined;
      if( showMsg ){
        if(Chat.beVerbose){
          console.log("Poller Connection restored.");
        }
        const m = Chat.reportReconnection("Poller connection restored.");
        if( oldErrMsg ){
          D.remove(oldErrMsg.e?.body.querySelector('button.retry-now'));
        }
        m.e.body.dataset.alsoRemove = oldErrMsg?.e?.body?.dataset?.msgid;
        D.addClass(m.e.body,'poller-connection');
      }
    }
  };

  /**
     Submits the contents of the message input field (if not empty)
     and/or the file attachment field to the server. If both are
     empty, this is a no-op.

1684
1685
1686
1687
1688
1689
1690

1691
1692
1693
1694
1695
1696
1697
      payload: fd,
      responseType: 'text',
      onerror:function(err){
        self.reportErrorAsMessage(err);
        recoverFailedMessage(fallback);
      },
      onload:function(txt){

        if(!txt) return/*success response*/;
        try{
          const json = JSON.parse(txt);
          self.newContent({msgs:[json]});
        }catch(e){
          self.reportError(e);
        }







>







1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
      payload: fd,
      responseType: 'text',
      onerror:function(err){
        self.reportErrorAsMessage(err);
        recoverFailedMessage(fallback);
      },
      onload:function(txt){
        reportConnectionOkay('chat-send');
        if(!txt) return/*success response*/;
        try{
          const json = JSON.parse(txt);
          self.newContent({msgs:[json]});
        }catch(e){
          self.reportError(e);
        }
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
        const v = Chat.inputValue();
        Chat.inputValue('');
        Chat.e.inputFields.$currentIndex = a[2];
        Chat.inputValue(v);
        D.removeClass(a[0], 'hidden');
        D.addClass(a[1], 'hidden');
      }
      Chat.e.inputElementWrapper.classList[
        s.value ? 'add' : 'remove'
      ]('compact');
      Chat.e.inputFields[Chat.e.inputFields.$currentIndex].focus();
    });
    Chat.settings.addListener('edit-ctrl-send',function(s){
      const label = (s.value ? "Ctrl-" : "")+"Enter submits message.";
      Chat.e.inputFields.forEach((e)=>{







|







2317
2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
        const v = Chat.inputValue();
        Chat.inputValue('');
        Chat.e.inputFields.$currentIndex = a[2];
        Chat.inputValue(v);
        D.removeClass(a[0], 'hidden');
        D.addClass(a[1], 'hidden');
      }
      Chat.e.inputLineWrapper.classList[
        s.value ? 'add' : 'remove'
      ]('compact');
      Chat.e.inputFields[Chat.e.inputFields.$currentIndex].focus();
    });
    Chat.settings.addListener('edit-ctrl-send',function(s){
      const label = (s.value ? "Ctrl-" : "")+"Enter submits message.";
      Chat.e.inputFields.forEach((e)=>{
2183
2184
2185
2186
2187
2188
2189

2190
2191
2192
2193
2194
2195
2196
      fd.append('content', txt);
      fd.append('filename','chat.md'
                /*filename needed for mimetype determination*/);
      fd.append('render_mode',F.page.previewModes.wiki);
      F.fetch('ajax/preview-text',{
        payload: fd,
        onload: function(html){

          Chat.setPreviewText(html);
          F.pikchr.addSrcView(Chat.e.viewPreview.querySelectorAll('svg.pikchr'));
        },
        onerror: function(e){
          F.fetch.onerror(e);
          Chat.setPreviewText("ERROR: "+(
            e.message || 'Unknown error fetching preview!'







>







2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
      fd.append('content', txt);
      fd.append('filename','chat.md'
                /*filename needed for mimetype determination*/);
      fd.append('render_mode',F.page.previewModes.wiki);
      F.fetch('ajax/preview-text',{
        payload: fd,
        onload: function(html){
          reportConnectionOkay('ajax/preview-text');
          Chat.setPreviewText(html);
          F.pikchr.addSrcView(Chat.e.viewPreview.querySelectorAll('svg.pikchr'));
        },
        onerror: function(e){
          F.fetch.onerror(e);
          Chat.setPreviewText("ERROR: "+(
            e.message || 'Unknown error fetching preview!'
2320
2321
2322
2323
2324
2325
2326

2327
2328
2329
2330
2331
2332
2333
        },
        responseType: 'json',
        onerror:function(err){
          Chat.reportErrorAsMessage(err);
          Chat._isBatchLoading = false;
        },
        onload:function(x){

          let gotMessages = x.msgs.length;
          newcontent(x,true);
          Chat._isBatchLoading = false;
          Chat.updateActiveUserList();
          if(Chat._gotServerError){
            Chat._gotServerError = false;
            return;







>







2514
2515
2516
2517
2518
2519
2520
2521
2522
2523
2524
2525
2526
2527
2528
        },
        responseType: 'json',
        onerror:function(err){
          Chat.reportErrorAsMessage(err);
          Chat._isBatchLoading = false;
        },
        onload:function(x){
          reportConnectionOkay('loadOldMessages()');
          let gotMessages = x.msgs.length;
          newcontent(x,true);
          Chat._isBatchLoading = false;
          Chat.updateActiveUserList();
          if(Chat._gotServerError){
            Chat._gotServerError = false;
            return;
2409
2410
2411
2412
2413
2414
2415

2416
2417
2418
2419
2420
2421
2422
        payload: fd,
        responseType: 'json',
        onerror:function(err){
          Chat.setCurrentView(Chat.e.viewMessages);
          Chat.reportErrorAsMessage(err);
        },
        onload:function(jx){

          let previd = 0;
          D.clearElement(eMsgTgt);
          jx.msgs.forEach((m)=>{
            m.isSearchResult = true;
            const mw = new Chat.MessageWidget(m);
            const spacer = new Chat.SearchCtxLoader({
              first: jx.first,







>







2604
2605
2606
2607
2608
2609
2610
2611
2612
2613
2614
2615
2616
2617
2618
        payload: fd,
        responseType: 'json',
        onerror:function(err){
          Chat.setCurrentView(Chat.e.viewMessages);
          Chat.reportErrorAsMessage(err);
        },
        onload:function(jx){
          reportConnectionOkay('submitSearch()');
          let previd = 0;
          D.clearElement(eMsgTgt);
          jx.msgs.forEach((m)=>{
            m.isSearchResult = true;
            const mw = new Chat.MessageWidget(m);
            const spacer = new Chat.SearchCtxLoader({
              first: jx.first,
2442
2443
2444
2445
2446
2447
2448








































2449
2450
2451
2452
2453
2454
2455
2456
2457

2458
2459
2460
2461
2462


2463

2464

2465

2466




2467
2468







2469
2470
2471
2472
2473
2474












2475
2476

2477
2478









2479











2480


2481



2482
2483
2484
2485
2486
2487
2488

2489
2490




2491
























2492
2493
2494
2495
2496
2497
2498
2499
2500






2501
2502
2503


2504
2505
2506
2507
2508
2509

2510
2511
2512
2513
2514
2515
2516
2517
2518
2519
2520
2521
2522
2523
2524
2525
2526
2527
2528
2529
2530
                      term );
          }
        }
      }
    );
  }/*Chat.submitSearch()*/;









































  const afterFetch = function f(){
    if(true===f.isFirstCall){
      f.isFirstCall = false;
      Chat.ajaxEnd();
      Chat.e.viewMessages.classList.remove('loading');
      setTimeout(function(){
        Chat.scrollMessagesTo(1);
      }, 250);
    }

    if(Chat._gotServerError && Chat.intervalTimer){
      clearInterval(Chat.intervalTimer);
      Chat.reportErrorAsMessage(
        "Shutting down chat poller due to server-side error. ",
        "Reload this page to reactivate it.");


      delete Chat.intervalTimer;

    }

    poll.running = false;

  };




  afterFetch.isFirstCall = true;
  /**







     FIXME: when polling fails because the remote server is
     reachable but it's not accepting HTTP requests, we should back
     off on polling for a while. e.g. if the remote web server process
     is killed, the poll fails quickly and immediately retries,
     hammering the remote server until the httpd is back up. That
     happens often during development of this application.













     XHR does not offer a direct way of distinguishing between

     HTTP/connection errors, but we can hypothetically use the
     xhrRequest.status value to do so, with status==0 being a









     connection error. We do not currently have a clean way of passing











     that info back to the fossil.fetch() client, so we'll need to


     hammer on that API a bit to get this working.



  */
  const poll = async function f(){
    if(f.running) return;
    f.running = true;
    Chat._isBatchLoading = f.isFirstCall;
    if(true===f.isFirstCall){
      f.isFirstCall = false;

      Chat.ajaxStart();
      Chat.e.viewMessages.classList.add('loading');




    }
























    F.fetch("chat-poll",{
      timeout: 420 * 1000/*FIXME: get the value from the server*/,
      urlParams:{
        name: Chat.mxMsg
      },
      responseType: "json",
      // Disable the ajax start/end handling for this long-polling op:
      beforesend: function(){},
      aftersend: function(){},






      onerror:function(err){
        Chat._isBatchLoading = false;
        if(Chat.verboseErrors) console.error(err);


        /* ^^^ we don't use Chat.reportError() here b/c the polling
           fails exepectedly when it times out, but is then immediately
           resumed, and reportError() produces a loud error message. */
        afterFetch();
      },
      onload:function(y){

        newcontent(y);
        if(Chat._isBatchLoading){
          Chat._isBatchLoading = false;
          Chat.updateActiveUserList();
        }
        afterFetch();
      }
    });
  };
  poll.isFirstCall = true;
  Chat._gotServerError = poll.running = false;
  if( window.fossil.config.chat.fromcli ){
    Chat.chatOnlyMode(true);
  }
  Chat.intervalTimer = setInterval(poll, 1000);
  delete ForceResizeKludge.$disabled;
  ForceResizeKludge();
  Chat.animate.$disabled = false;
  setTimeout( ()=>Chat.inputFocus(), 0 );
  F.page.chat = Chat/* enables testing the APIs via the dev tools */;
});







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
|








>
|
<


|
>
>
|
>
|
>
|
>
|
>
>
>
>
|
<
>
>
>
>
>
>
>
|
<
|
<
<
<
>
>
>
>
>
>
>
>
>
>
>
>
|
<
>
|
<
>
>
>
>
>
>
>
>
>
|
>
>
>
>
>
>
>
>
>
>
>
|
>
>
|
>
>
>

|





>


>
>
>
>
|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>

|





|
|
>
>
>
>
>
>


|
>
>
|
<
|
<


>





|


|





|






2638
2639
2640
2641
2642
2643
2644
2645
2646
2647
2648
2649
2650
2651
2652
2653
2654
2655
2656
2657
2658
2659
2660
2661
2662
2663
2664
2665
2666
2667
2668
2669
2670
2671
2672
2673
2674
2675
2676
2677
2678
2679
2680
2681
2682
2683
2684
2685
2686
2687
2688
2689
2690
2691
2692
2693
2694
2695

2696
2697
2698
2699
2700
2701
2702
2703
2704
2705
2706
2707
2708
2709
2710
2711
2712

2713
2714
2715
2716
2717
2718
2719
2720

2721



2722
2723
2724
2725
2726
2727
2728
2729
2730
2731
2732
2733
2734

2735
2736

2737
2738
2739
2740
2741
2742
2743
2744
2745
2746
2747
2748
2749
2750
2751
2752
2753
2754
2755
2756
2757
2758
2759
2760
2761
2762
2763
2764
2765
2766
2767
2768
2769
2770
2771
2772
2773
2774
2775
2776
2777
2778
2779
2780
2781
2782
2783
2784
2785
2786
2787
2788
2789
2790
2791
2792
2793
2794
2795
2796
2797
2798
2799
2800
2801
2802
2803
2804
2805
2806
2807
2808
2809
2810
2811
2812
2813
2814
2815
2816
2817
2818
2819
2820
2821
2822
2823
2824

2825

2826
2827
2828
2829
2830
2831
2832
2833
2834
2835
2836
2837
2838
2839
2840
2841
2842
2843
2844
2845
2846
2847
2848
2849
                      term );
          }
        }
      }
    );
  }/*Chat.submitSearch()*/;

  /*
    To be called from F.fetch('chat-poll') beforesend() handler.  If
    we're currently in delayed-retry mode and a connection is
    started, try to reset the delay after N time waiting on that
    connection. The fact that the connection is waiting to respond,
    rather than outright failing, is a good hint that the outage is
    over and we can reset the back-off timer.

    Without this, recovery of a connection error won't be reported
    until after the long-poll completes by either receiving new
    messages or timing out. Once a long-poll is in progress, though,
    we "know" that it's up and running again, so can update the UI and
    connection timer to reflect that. That's the job this function
    does.

    Only one of these asynchronous checks will ever be active
    concurrently and only if Chat.timer.isDelayed() is true. i.e. if
    this timer is active or Chat.timer.isDelayed() is false, this is a
    no-op.
  */
  const chatPollBeforeSend = function(){
    //console.warn('chatPollBeforeSend outer', Chat.timer.tidClearPollErr, Chat.timer.currentDelay);
    if( !Chat.timer.tidClearPollErr && Chat.timer.isDelayed() ){
      Chat.timer.tidClearPollErr = setTimeout(()=>{
        //console.warn('chatPollBeforeSend inner');
        Chat.timer.tidClearPollErr = 0;
        if( poll.running ){
          /* This chat-poll F.fetch() is still underway, so let's
             assume the connection is back up until/unless it times
             out or breaks again. */
          reportConnectionOkay('chatPollBeforeSend', true);
        }
      }, Chat.timer.$initialDelay * 4/*kinda arbitrary: not too long for UI wait and
                                       not too short as to make connection unlikely. */ );
    }
  };

  /**
     Deal with the last poll() response and maybe re-start poll().
  */
  const afterPollFetch = function f(err){
    if(true===f.isFirstCall){
      f.isFirstCall = false;
      Chat.ajaxEnd();
      Chat.e.viewMessages.classList.remove('loading');
      setTimeout(function(){
        Chat.scrollMessagesTo(1);
      }, 250);
    }
    Chat.timer.cancelPendingPollTimer();
    if(Chat._gotServerError){

      Chat.reportErrorAsMessage(
        "Shutting down chat poller due to server-side error. ",
        "Reload this page to reactivate it."
      );
    } else {
      if( err && Chat.beVerbose ){
        console.error("afterPollFetch:",err.name,err.status,err.message);
      }
      if( !err || 'timeout'===err.name/*(probably) long-poll expired*/ ){
        /* Restart the poller immediately. */
        reportConnectionOkay('afterPollFetch '+err, false);
      }else{
        /* Delay a while before trying again, noting that other Chat
           APIs may try and succeed at connections before this timer
           resolves, in which case they'll clear this timeout and the
           UI message about the outage. */
        let delay;

        D.addClass(Chat.e.pollErrorMarker, 'connection-error');
        if( ++Chat.timer.errCount < Chat.timer.minErrForNotify ){
          delay = Chat.timer.resetDelay(
            (Chat.timer.minDelay * Chat.timer.errCount)
              + Chat.timer.randomInterval(Chat.timer.minDelay)
          );
          if(Chat.beVerbose){
            console.warn("Ignoring polling error #",Chat.timer.errCount,

                         "for another",delay,"ms" );



          }
        } else {
          delay = Chat.timer.incrDelay();
          //console.warn("afterPollFetch Chat.e.eMsgPollError",Chat.e.eMsgPollError);
          const msg = "Poller connection error. Retrying in "+delay+ " ms.";
          /* Replace the current/newest connection error widget. We could also
             just update its body with the new message, but then its timestamp
             never updates. OTOH, if we replace the message, we lose the
             start time of the outage in the log. It seems more useful to
             update the timestamp so that it doesn't look like it's hung. */
          if( Chat.e.eMsgPollError ){
            Chat.deleteMessageElem(Chat.e.eMsgPollError, false);
          }

          const theMsg = Chat.e.eMsgPollError = Chat.reportErrorAsMessage(msg);
          D.addClass(Chat.e.eMsgPollError.e.body,'poller-connection');

          /* Add a "retry now" button */
          const btnDel = D.addClass(D.button("Retry now"), 'retry-now');
          const eParent = Chat.e.eMsgPollError.e.content;
          D.append(eParent, " ", btnDel);
          btnDel.addEventListener('click', function(){
            D.remove(btnDel);
            D.append(eParent, D.text("retrying..."));
            Chat.timer.cancelPendingPollTimer().currentDelay =
              Chat.timer.resetDelay() +
              1  /*workaround for showing the "connection restored"
                   message, as the +1 will cause
                   Chat.timer.isDelayed() to be true.*/;
            poll();
          });
          //Chat.playNewMessageSound();// browser complains b/c this wasn't via human interaction
        }
        Chat.timer.startPendingPollTimer(delay);
      }
    }
  };
  afterPollFetch.isFirstCall = true;

  /**
     Initiates, if it's not already running, a single long-poll
     request to the /chat-poll endpoint. In the handling of that
     response, it end up will psuedo-recursively calling itself via
     the response-handling process. Despite being async, the implied
     returned Promise is meaningless.
  */
  const poll = Chat.poll = async function f(){
    if(f.running) return;
    f.running = true;
    Chat._isBatchLoading = f.isFirstCall;
    if(true===f.isFirstCall){
      f.isFirstCall = false;
      f.pendingOnError = undefined;
      Chat.ajaxStart();
      Chat.e.viewMessages.classList.add('loading');
      /*
        We manager onerror() results in poll() in a roundabout
        manner: when an onerror() arrives, we stash it aside
        for a moment before processing it.

        This level of indirection is necessary to be able to
        unambiguously identify client-timeout-specific polling errors
        from other errors. Timeouts are always announced in pairs of
        an HTTP 0 and something we can unambiguously identify as a
        timeout (in that order). When we receive an HTTP error we put
        it into this queue.  If an ontimeout() call arrives before
        this error is handled, this error is ignored.  If, however, an
        HTTP error is seen without an accompanying timeout, we handle
        it from here.

        It's kinda like in the curses C API, where you to match
        ALT-X by first getting an ESC event, then an X event, but
        this one is a lot less explicable. (It's almost certainly a
        mis-handling bug in F.fetch(), but it has so far eluded my
        eyes.)
      */
      f.delayPendingOnError = function(err){
        if( f.pendingOnError ){
          const x = f.pendingOnError;
          f.pendingOnError = undefined;
          afterPollFetch(x);
        }
      };
    }
    F.fetch("chat-poll",{
      timeout: Chat.timer.pollTimeout,
      urlParams:{
        name: Chat.mxMsg
      },
      responseType: "json",
      // Disable the ajax start/end handling for this long-polling op:
      beforesend: chatPollBeforeSend,
      aftersend: function(){
        poll.running = false;
      },
      ontimeout: function(err){
        f.pendingOnError = undefined /*strip preceeding non-timeout error, if any*/;
        afterPollFetch(err);
      },
      onerror:function(err){
        Chat._isBatchLoading = false;
        if(Chat.beVerbose){
          console.error("poll.onerror:",err.name,err.status,JSON.stringify(err));
        }
        f.pendingOnError = err;

        setTimeout(f.delayPendingOnError, 100);

      },
      onload:function(y){
        reportConnectionOkay('poll.onload', true);
        newcontent(y);
        if(Chat._isBatchLoading){
          Chat._isBatchLoading = false;
          Chat.updateActiveUserList();
        }
        afterPollFetch();
      }
    });
  }/*poll()*/;
  poll.isFirstCall = true;
  Chat._gotServerError = poll.running = false;
  if( window.fossil.config.chat.fromcli ){
    Chat.chatOnlyMode(true);
  }
  Chat.timer.startPendingPollTimer();
  delete ForceResizeKludge.$disabled;
  ForceResizeKludge();
  Chat.animate.$disabled = false;
  setTimeout( ()=>Chat.inputFocus(), 0 );
  F.page.chat = Chat/* enables testing the APIs via the dev tools */;
});
Changes to src/fossil.popupwidget.js.
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
    );
    return F.toast;
  };

  F.toast = {
    config: {
      position: { x: 5, y: 5 /*viewport-relative, pixels*/ },
      displayTimeMs: 3000
    },
    /**
       Convenience wrapper around a PopupWidget which pops up a shared
       PopupWidget instance to show toast-style messages (commonly
       seen on Android). Its arguments may be anything suitable for
       passing to fossil.dom.append(), and each argument is first
       append()ed to the toast widget, then the widget is shown for







|







284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
    );
    return F.toast;
  };

  F.toast = {
    config: {
      position: { x: 5, y: 5 /*viewport-relative, pixels*/ },
      displayTimeMs: 5000
    },
    /**
       Convenience wrapper around a PopupWidget which pops up a shared
       PopupWidget instance to show toast-style messages (commonly
       seen on Android). Its arguments may be anything suitable for
       passing to fossil.dom.append(), and each argument is first
       append()ed to the toast widget, then the widget is shown for
Changes to src/graph.c.
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
  int nRow;                  /* Number of rows */
  int nHash;                 /* Number of slots in apHash[] */
  u8 hasOffsetMergeRiser;    /* Merge arrow from leaf goes up on a different
                             ** rail that the node */
  u8 bOverfull;              /* Unable to allocate sufficient rails */
  u64 mergeRail;             /* Rails used for merge lines */
  GraphRow **apHash;         /* Hash table of GraphRow objects.  Key: rid */
  u8 aiRailMap[GR_MAX_RAIL]; /* Mapping of rails to actually columns */
};

#endif

/* The N-th bit */
#define BIT(N)  (((u64)1)<<(N))








|







119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
  int nRow;                  /* Number of rows */
  int nHash;                 /* Number of slots in apHash[] */
  u8 hasOffsetMergeRiser;    /* Merge arrow from leaf goes up on a different
                             ** rail that the node */
  u8 bOverfull;              /* Unable to allocate sufficient rails */
  u64 mergeRail;             /* Rails used for merge lines */
  GraphRow **apHash;         /* Hash table of GraphRow objects.  Key: rid */
  u8 aiRailMap[GR_MAX_RAIL+1]; /* Mapping of rails to actually columns */
};

#endif

/* The N-th bit */
#define BIT(N)  (((u64)1)<<(N))

Changes to src/graph.js.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* This module contains javascript needed to render timeline graphs in Fossil.
**
** There can be multiple graphs on a single webpage, but this script is only
** loaded once.  
**
** Prior to sourcing this script, there should be a separate
** <script type='application/json' id='timeline-data-NN'> for each graph,
** each containing JSON like this:
**
**   { "iTableId": INTEGER,         // Table sequence number (NN)
**     "circleNodes": BOOLEAN,      // True for circle nodes. False for squares
**     "showArrowheads": BOOLEAN,   // True for arrowheads. False to omit
**     "iRailPitch": INTEGER,       // Spacing between vertical lines (px)
**     "colorGraph": BOOLEAN,       // True to put color on graph lines
**     "nomo": BOOLEAN,             // True to join merge lines with rails
**     "iTopRow": INTEGER,          // Index of top-most row in the graph
**     "omitDescenders": BOOLEAN,   // Omit ancestor lines off bottom of screen
**     "fileDiff": BOOLEAN,         // True for file diff. False for check-in
**     "scrollToSelect": BOOLEAN,   // Scroll to selection on first render
**     "nrail": INTEGER,            // Number of vertical "rails"
**     "baseUrl": TEXT,             // Top-level URL













<







1
2
3
4
5
6
7
8
9
10
11
12
13

14
15
16
17
18
19
20
/* This module contains javascript needed to render timeline graphs in Fossil.
**
** There can be multiple graphs on a single webpage, but this script is only
** loaded once.  
**
** Prior to sourcing this script, there should be a separate
** <script type='application/json' id='timeline-data-NN'> for each graph,
** each containing JSON like this:
**
**   { "iTableId": INTEGER,         // Table sequence number (NN)
**     "circleNodes": BOOLEAN,      // True for circle nodes. False for squares
**     "showArrowheads": BOOLEAN,   // True for arrowheads. False to omit
**     "iRailPitch": INTEGER,       // Spacing between vertical lines (px)

**     "nomo": BOOLEAN,             // True to join merge lines with rails
**     "iTopRow": INTEGER,          // Index of top-most row in the graph
**     "omitDescenders": BOOLEAN,   // Omit ancestor lines off bottom of screen
**     "fileDiff": BOOLEAN,         // True for file diff. False for check-in
**     "scrollToSelect": BOOLEAN,   // Scroll to selection on first render
**     "nrail": INTEGER,            // Number of vertical "rails"
**     "baseUrl": TEXT,             // Top-level URL
Changes to src/http_ssl.c.
317
318
319
320
321
322
323

324

325
326
327
328
329
330
331
** for future versions of OpenSSL, and explicit initialization may be redundant.
** NOTE TO HACKERS TWEAKING THEIR OPENSSL CONFIGURATION:
** The following OpenSSL configuration options must not be used for this feature
** to be available: `no-autoalginit', `no-winstore'. The Fossil makefiles do not
** currently set these options when building OpenSSL for Windows. */
#if defined(_WIN32)
#if OPENSSL_VERSION_NUMBER >= 0x030200000

    if( SSL_CTX_load_verify_store(sslCtx, "org.openssl.winstore:")==0 ){

      fossil_print("NOTICE: Failed to load the Windows root certificates.\n");
    }
#endif /* OPENSSL_VERSION_NUMBER >= 0x030200000 */
#endif /* _WIN32 */

    /* Load client SSL identity, preferring the filename specified on the
    ** command line */







>
|
>







317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
** for future versions of OpenSSL, and explicit initialization may be redundant.
** NOTE TO HACKERS TWEAKING THEIR OPENSSL CONFIGURATION:
** The following OpenSSL configuration options must not be used for this feature
** to be available: `no-autoalginit', `no-winstore'. The Fossil makefiles do not
** currently set these options when building OpenSSL for Windows. */
#if defined(_WIN32)
#if OPENSSL_VERSION_NUMBER >= 0x030200000
    if( SSLeay()!=0x30500000  /* Don't use for 3.5.0 due to a bug */
     && SSL_CTX_load_verify_store(sslCtx, "org.openssl.winstore:")==0
    ){
      fossil_print("NOTICE: Failed to load the Windows root certificates.\n");
    }
#endif /* OPENSSL_VERSION_NUMBER >= 0x030200000 */
#endif /* _WIN32 */

    /* Load client SSL identity, preferring the filename specified on the
    ** command line */
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
    fossil_print("OpenSSL-version:      (none)\n");
    if( verbose ){
      fossil_print("\n"
         "  The OpenSSL library is not used by this build of Fossil\n\n"
      );
    }
#else
    fossil_print("OpenSSL-version:      %s  (0x%09x)\n",
         SSLeay_version(SSLEAY_VERSION), OPENSSL_VERSION_NUMBER);
    if( verbose ){
      fossil_print("\n"
         "  The version of the OpenSSL library being used\n"
         "  by this instance of Fossil.  Version 3.0.0 or\n"
         "  later is recommended.\n\n"
      );
    }







|
|







999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
    fossil_print("OpenSSL-version:      (none)\n");
    if( verbose ){
      fossil_print("\n"
         "  The OpenSSL library is not used by this build of Fossil\n\n"
      );
    }
#else
    fossil_print("OpenSSL-version:      %s  (0x%09llx)\n",
         SSLeay_version(SSLEAY_VERSION), (unsigned long long)SSLeay());
    if( verbose ){
      fossil_print("\n"
         "  The version of the OpenSSL library being used\n"
         "  by this instance of Fossil.  Version 3.0.0 or\n"
         "  later is recommended.\n\n"
      );
    }
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072

1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
         "    the identity of servers for \"https:\" URLs. These values\n"
         "    come into play when Fossil is used as a TLS client.  These\n"
         "    values are built into your OpenSSL library.\n\n"
      );
    }

#if defined(_WIN32)
#if OPENSSL_VERSION_NUMBER >= 0x030200000
    fossil_print("  OpenSSL-winstore:   Yes\n");
#else /* OPENSSL_VERSION_NUMBER >= 0x030200000 */
    fossil_print("  OpenSSL-winstore:   No\n");
#endif /* OPENSSL_VERSION_NUMBER >= 0x030200000 */
    if( verbose ){
      fossil_print("\n"

         "    OpenSSL 3.2.0, or newer, use the root certificates managed by\n"
         "    the Windows operating system. The installed root certificates\n"
         "    are listed by the command:\n\n"
         "        certutil -store \"ROOT\"\n\n"
      );
    }
#endif /* _WIN32 */

    if( zUsed==0 ) zUsed = "";
    fossil_print("  Trust store used:   %s\n", zUsed);







<
|
|
<
<


>
|
|
|







1061
1062
1063
1064
1065
1066
1067

1068
1069


1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
         "    the identity of servers for \"https:\" URLs. These values\n"
         "    come into play when Fossil is used as a TLS client.  These\n"
         "    values are built into your OpenSSL library.\n\n"
      );
    }

#if defined(_WIN32)

    fossil_print("  OpenSSL-winstore:   %s\n",
         (SSLeay()>=0x30200000 && SSLeay()!=0x30500000) ? "Yes" : "No");


    if( verbose ){
      fossil_print("\n"
         "    OpenSSL 3.2.0, or newer, but not version 3.5.0 due to a bug,\n"
         "    are able to use the root certificates managed by the Windows\n"
         "    operating system. The installed root certificates are listed\n"
         "    by the command:\n\n"
         "        certutil -store \"ROOT\"\n\n"
      );
    }
#endif /* _WIN32 */

    if( zUsed==0 ) zUsed = "";
    fossil_print("  Trust store used:   %s\n", zUsed);
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
** Return the OpenSSL version number being used.  Space to hold
** this name is obtained from fossil_malloc() and should be
** freed by the caller.
*/
char *fossil_openssl_version(void){
#if defined(FOSSIL_ENABLE_SSL)
  return mprintf("%s (0x%09x)\n",
         SSLeay_version(SSLEAY_VERSION), OPENSSL_VERSION_NUMBER);
#else
  return mprintf("none");
#endif
}







|




1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
** Return the OpenSSL version number being used.  Space to hold
** this name is obtained from fossil_malloc() and should be
** freed by the caller.
*/
char *fossil_openssl_version(void){
#if defined(FOSSIL_ENABLE_SSL)
  return mprintf("%s (0x%09x)\n",
         SSLeay_version(SSLEAY_VERSION), (sqlite3_uint64)SSLeay());
#else
  return mprintf("none");
#endif
}
Changes to src/info.c.
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
    const char *zComment;
    const char *zDate;
    const char *zOrigDate;
    int okWiki = 0;
    Blob wiki_read_links = BLOB_INITIALIZER;
    Blob wiki_add_links = BLOB_INITIALIZER;

    Th_Store("current_checkin", zName);
    style_header("Check-in [%S]", zUuid);
    login_anonymous_available();
    zEUser = db_text(0,
                   "SELECT value FROM tagxref"
                   " WHERE tagid=%d AND rid=%d AND tagtype>0",
                    TAG_USER, rid);
    zEComment = db_text(0,







|







949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
    const char *zComment;
    const char *zDate;
    const char *zOrigDate;
    int okWiki = 0;
    Blob wiki_read_links = BLOB_INITIALIZER;
    Blob wiki_add_links = BLOB_INITIALIZER;

    Th_StoreUnsafe("current_checkin", zName);
    style_header("Check-in [%S]", zUuid);
    login_anonymous_available();
    zEUser = db_text(0,
                   "SELECT value FROM tagxref"
                   " WHERE tagid=%d AND rid=%d AND tagtype>0",
                    TAG_USER, rid);
    zEComment = db_text(0,
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
  }
  if( diffType!=2 ){
    @ %z(chref("button","%R/%s/%T?diff=2%s",zPage,zName,zW))\
    @ Side-by-Side&nbsp;Diff</a>
  }
  if( diffType!=0 ){
    if( *zW ){
      @ %z(chref("button","%R/%s/%T",zPage,zName))
      @ Show&nbsp;Whitespace&nbsp;Changes</a>
    }else{
      @ %z(chref("button","%R/%s/%T?w",zPage,zName))
      @ Ignore&nbsp;Whitespace</a>
    }
  }
  if( zParent ){
    @ %z(chref("button","%R/vpatch?from=%!S&to=%!S",zParent,zUuid))
    @ Patch</a>
  }







|


|







1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
  }
  if( diffType!=2 ){
    @ %z(chref("button","%R/%s/%T?diff=2%s",zPage,zName,zW))\
    @ Side-by-Side&nbsp;Diff</a>
  }
  if( diffType!=0 ){
    if( *zW ){
      @ %z(chref("button","%R/%s/%T?diff=%d",zPage,zName,diffType))
      @ Show&nbsp;Whitespace&nbsp;Changes</a>
    }else{
      @ %z(chref("button","%R/%s/%T?diff=%d&w",zPage,zName,diffType))
      @ Ignore&nbsp;Whitespace</a>
    }
  }
  if( zParent ){
    @ %z(chref("button","%R/vpatch?from=%!S&to=%!S",zParent,zUuid))
    @ Patch</a>
  }
1968
1969
1970
1971
1972
1973
1974

1975
1976
1977
1978

1979
1980
1981
1982
1983
1984
1985
  login_check_credentials();
  if( !g.perm.Read ){ login_needed(g.anon.Read); return; }
  diff_config_init(&DCfg, 0);
  diffType = preferred_diff_type();
  if( P("from") && P("to") ){
    v1 = artifact_from_ci_and_filename("from");
    v2 = artifact_from_ci_and_filename("to");

  }else{
    Stmt q;
    v1 = name_to_rid_www("v1");
    v2 = name_to_rid_www("v2");


    /* If the two file versions being compared both have the same
    ** filename, then offer an "Annotate" link that constructs an
    ** annotation between those version. */
    db_prepare(&q,
      "SELECT (SELECT substr(uuid,1,20) FROM blob WHERE rid=a.mid),"
      "       (SELECT substr(uuid,1,20) FROM blob WHERE rid=b.mid),"







>




>







1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
  login_check_credentials();
  if( !g.perm.Read ){ login_needed(g.anon.Read); return; }
  diff_config_init(&DCfg, 0);
  diffType = preferred_diff_type();
  if( P("from") && P("to") ){
    v1 = artifact_from_ci_and_filename("from");
    v2 = artifact_from_ci_and_filename("to");
    if( v1==0 || v2==0 ) fossil_redirect_home();
  }else{
    Stmt q;
    v1 = name_to_rid_www("v1");
    v2 = name_to_rid_www("v2");
    if( v1==0 || v2==0 ) fossil_redirect_home();

    /* If the two file versions being compared both have the same
    ** filename, then offer an "Annotate" link that constructs an
    ** annotation between those version. */
    db_prepare(&q,
      "SELECT (SELECT substr(uuid,1,20) FROM blob WHERE rid=a.mid),"
      "       (SELECT substr(uuid,1,20) FROM blob WHERE rid=b.mid),"
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
      const char *zFN = db_column_text(&q, 2);
      style_submenu_element("Annotate",
        "%R/annotate?origin=%s&checkin=%s&filename=%T",
        zOrig, zCkin, zFN);
    }
    db_finalize(&q);
  }
  if( v1==0 || v2==0 ) fossil_redirect_home();
  zRe = P("regex");
  cgi_check_for_malice();
  if( zRe ) re_compile(&pRe, zRe, 0);
  if( verbose ) objdescFlags |= OBJDESC_DETAIL;
  if( isPatch ){
    Blob c1, c2, *pOut;
    DiffConfig DCfg;







<







2003
2004
2005
2006
2007
2008
2009

2010
2011
2012
2013
2014
2015
2016
      const char *zFN = db_column_text(&q, 2);
      style_submenu_element("Annotate",
        "%R/annotate?origin=%s&checkin=%s&filename=%T",
        zOrig, zCkin, zFN);
    }
    db_finalize(&q);
  }

  zRe = P("regex");
  cgi_check_for_malice();
  if( zRe ) re_compile(&pRe, zRe, 0);
  if( verbose ) objdescFlags |= OBJDESC_DETAIL;
  if( isPatch ){
    Blob c1, c2, *pOut;
    DiffConfig DCfg;
2620
2621
2622
2623
2624
2625
2626

2627
2628
2629
2630
2631
2632

2633
2634
2635
2636
2637
2638
2639
2640


2641
2642
2643
2644
2645
2646
2647
  fossil_print("%b\n", cgi_output_blob());
}

/*
** WEBPAGE: artifact
** WEBPAGE: file
** WEBPAGE: whatis

**
** Typical usage:
**
**    /artifact/HASH
**    /whatis/HASH
**    /file/NAME

**
** Additional query parameters:
**
**   ln              - show line numbers
**   ln=N            - highlight line number N
**   ln=M-N          - highlight lines M through N inclusive
**   ln=M-N+Y-Z      - highlight lines M through N and Y through Z (inclusive)
**   verbose         - show more detail in the description


**   download        - redirect to the download (artifact page only)
**   name=NAME       - filename or hash as a query parameter
**   filename=NAME   - alternative spelling for "name="
**   fn=NAME         - alternative spelling for "name="
**   ci=VERSION      - The specific check-in to use with "name=" to
**                     identify the file.
**   txt             - Force display of unformatted source text







>






>








>
>







2621
2622
2623
2624
2625
2626
2627
2628
2629
2630
2631
2632
2633
2634
2635
2636
2637
2638
2639
2640
2641
2642
2643
2644
2645
2646
2647
2648
2649
2650
2651
2652
  fossil_print("%b\n", cgi_output_blob());
}

/*
** WEBPAGE: artifact
** WEBPAGE: file
** WEBPAGE: whatis
** WEBPAGE: docfile
**
** Typical usage:
**
**    /artifact/HASH
**    /whatis/HASH
**    /file/NAME
**    /docfile/NAME
**
** Additional query parameters:
**
**   ln              - show line numbers
**   ln=N            - highlight line number N
**   ln=M-N          - highlight lines M through N inclusive
**   ln=M-N+Y-Z      - highlight lines M through N and Y through Z (inclusive)
**   verbose         - show more detail in the description
**   brief           - show just the document, not the metadata.  The
**                     /docfile page is an alias for /file?brief
**   download        - redirect to the download (artifact page only)
**   name=NAME       - filename or hash as a query parameter
**   filename=NAME   - alternative spelling for "name="
**   fn=NAME         - alternative spelling for "name="
**   ci=VERSION      - The specific check-in to use with "name=" to
**                     identify the file.
**   txt             - Force display of unformatted source text
2674
2675
2676
2677
2678
2679
2680

2681
2682
2683
2684
2685
2686
2687
2688
2689
2690
2691
2692
2693
2694




2695
2696
2697
2698
2699
2700
2701
  int renderAsSvg = 0;
  int objType;
  int asText;
  const char *zUuid = 0;
  u32 objdescFlags = OBJDESC_BASE;
  int descOnly = fossil_strcmp(g.zPath,"whatis")==0;
  int hashOnly = P("hash")!=0;

  int isFile = fossil_strcmp(g.zPath,"file")==0;
  const char *zLn = P("ln");
  const char *zName = P("name");
  const char *zCI = P("ci");
  HQuery url;
  char *zCIUuid = 0;
  int isSymbolicCI = 0;  /* ci= exists and is a symbolic name, not a hash */
  int isBranchCI = 0;    /* ci= refers to a branch name */
  char *zHeader = 0;

  login_check_credentials();
  if( !g.perm.Read ){ login_needed(g.anon.Read); return; }
  cgi_check_for_malice();
  style_set_current_feature("artifact");





  /* Capture and normalize the name= and ci= query parameters */
  if( zName==0 ){
    zName = P("filename");
    if( zName==0 ){
      zName = P("fn");
    }







>














>
>
>
>







2679
2680
2681
2682
2683
2684
2685
2686
2687
2688
2689
2690
2691
2692
2693
2694
2695
2696
2697
2698
2699
2700
2701
2702
2703
2704
2705
2706
2707
2708
2709
2710
2711
  int renderAsSvg = 0;
  int objType;
  int asText;
  const char *zUuid = 0;
  u32 objdescFlags = OBJDESC_BASE;
  int descOnly = fossil_strcmp(g.zPath,"whatis")==0;
  int hashOnly = P("hash")!=0;
  int docOnly = P("brief")!=0;
  int isFile = fossil_strcmp(g.zPath,"file")==0;
  const char *zLn = P("ln");
  const char *zName = P("name");
  const char *zCI = P("ci");
  HQuery url;
  char *zCIUuid = 0;
  int isSymbolicCI = 0;  /* ci= exists and is a symbolic name, not a hash */
  int isBranchCI = 0;    /* ci= refers to a branch name */
  char *zHeader = 0;

  login_check_credentials();
  if( !g.perm.Read ){ login_needed(g.anon.Read); return; }
  cgi_check_for_malice();
  style_set_current_feature("artifact");
  if( fossil_strcmp(g.zPath, "docfile")==0 ){
    isFile = 1;
    docOnly = 1;
  }

  /* Capture and normalize the name= and ci= query parameters */
  if( zName==0 ){
    zName = P("filename");
    if( zName==0 ){
      zName = P("fn");
    }
2800
2801
2802
2803
2804
2805
2806


2807
2808
2809
2810
2811
2812
2813
2814
    cgi_set_content_type("text/plain");
    cgi_set_content(&uuid);
    return;
  }

  asText = P("txt")!=0;
  if( isFile ){


    if( zCI==0 || fossil_strcmp(zCI,"tip")==0 ){
      zCI = "tip";
      @ <h2>File %z(href("%R/finfo?name=%T&m&ci=tip",zName))%h(zName)</a>
      @ from the %z(href("%R/info/tip"))latest check-in</a></h2>
    }else{
      const char *zPath;
      Blob path;
      blob_zero(&path);







>
>
|







2810
2811
2812
2813
2814
2815
2816
2817
2818
2819
2820
2821
2822
2823
2824
2825
2826
    cgi_set_content_type("text/plain");
    cgi_set_content(&uuid);
    return;
  }

  asText = P("txt")!=0;
  if( isFile ){
    if( docOnly ){
      /* No header */
    }else if( zCI==0 || fossil_strcmp(zCI,"tip")==0 ){
      zCI = "tip";
      @ <h2>File %z(href("%R/finfo?name=%T&m&ci=tip",zName))%h(zName)</a>
      @ from the %z(href("%R/info/tip"))latest check-in</a></h2>
    }else{
      const char *zPath;
      Blob path;
      blob_zero(&path);
2822
2823
2824
2825
2826
2827
2828
2829
2830


2831
2832
2833
2834
2835

2836
2837
2838
2839
2840
2841
2842
      }else if( isSymbolicCI ){
        @ part of check-in %z(href("%R/info/%!S",zCIUuid))%s(zCI)</a></h2>
      }else{
        @ part of check-in %z(href("%R/info/%!S",zCIUuid))%S(zCIUuid)</a></h2>
      }
      blob_reset(&path);
    }
    style_submenu_element("Artifact", "%R/artifact/%S", zUuid);
    zMime = mimetype_from_name(zName);


    style_submenu_element("Annotate", "%R/annotate?filename=%T&checkin=%T",
                          zName, zCI);
    style_submenu_element("Blame", "%R/blame?filename=%T&checkin=%T",
                          zName, zCI);
    style_submenu_element("Doc", "%R/doc/%T/%T", zCI, zName);

    blob_init(&downloadName, zName, -1);
    objType = OBJTYPE_CONTENT;
  }else{
    @ <h2>Artifact
    style_copy_button(1, "hash-ar", 0, 2, "%s", zUuid);
    if( g.perm.Setup ){
      @  (%d(rid)):</h2>







<

>
>
|
|
|
|
|
>







2834
2835
2836
2837
2838
2839
2840

2841
2842
2843
2844
2845
2846
2847
2848
2849
2850
2851
2852
2853
2854
2855
2856
      }else if( isSymbolicCI ){
        @ part of check-in %z(href("%R/info/%!S",zCIUuid))%s(zCI)</a></h2>
      }else{
        @ part of check-in %z(href("%R/info/%!S",zCIUuid))%S(zCIUuid)</a></h2>
      }
      blob_reset(&path);
    }

    zMime = mimetype_from_name(zName);
    if( !docOnly ){
      style_submenu_element("Artifact", "%R/artifact/%S", zUuid);
      style_submenu_element("Annotate", "%R/annotate?filename=%T&checkin=%T",
                            zName, zCI);
      style_submenu_element("Blame", "%R/blame?filename=%T&checkin=%T",
                            zName, zCI);
      style_submenu_element("Doc", "%R/doc/%T/%T", zCI, zName);
    }
    blob_init(&downloadName, zName, -1);
    objType = OBJTYPE_CONTENT;
  }else{
    @ <h2>Artifact
    style_copy_button(1, "hash-ar", 0, 2, "%s", zUuid);
    if( g.perm.Setup ){
      @  (%d(rid)):</h2>
2851
2852
2853
2854
2855
2856
2857
2858
2859
2860
2861
2862
2863
2864
2865
  }
  if( !descOnly && P("download")!=0 ){
    cgi_redirectf("%R/raw/%s?at=%T",
          db_text("x", "SELECT uuid FROM blob WHERE rid=%d", rid),
          file_tail(blob_str(&downloadName)));
    /*NOTREACHED*/
  }
  if( g.perm.Admin ){
    const char *zUuid = db_text("", "SELECT uuid FROM blob WHERE rid=%d", rid);
    if( db_exists("SELECT 1 FROM shun WHERE uuid=%Q", zUuid) ){
      style_submenu_element("Unshun", "%R/shun?accept=%s&sub=1#accshun", zUuid);
    }else{
      style_submenu_element("Shun", "%R/shun?shun=%s#addshun",zUuid);
    }
  }







|







2865
2866
2867
2868
2869
2870
2871
2872
2873
2874
2875
2876
2877
2878
2879
  }
  if( !descOnly && P("download")!=0 ){
    cgi_redirectf("%R/raw/%s?at=%T",
          db_text("x", "SELECT uuid FROM blob WHERE rid=%d", rid),
          file_tail(blob_str(&downloadName)));
    /*NOTREACHED*/
  }
  if( g.perm.Admin && !docOnly ){
    const char *zUuid = db_text("", "SELECT uuid FROM blob WHERE rid=%d", rid);
    if( db_exists("SELECT 1 FROM shun WHERE uuid=%Q", zUuid) ){
      style_submenu_element("Unshun", "%R/shun?accept=%s&sub=1#accshun", zUuid);
    }else{
      style_submenu_element("Shun", "%R/shun?shun=%s#addshun",zUuid);
    }
  }
2896
2897
2898
2899
2900
2901
2902

2903
2904
2905

2906
2907
2908
2909
2910
2911
2912

2913

2914
2915
2916
2917
2918
2919
2920
2921
2922

2923

2924
2925
2926
2927
2928
2929

2930
2931
2932

2933
2934
2935
2936
2937
2938
2939
2940
2941
2942
2943
2944

2945

2946
2947
2948
2949
2950
2951
2952
      const char *zUser = db_column_text(&q,0);
      const char *zDate = db_column_text(&q,1);
      const char *zIp = db_column_text(&q,2);
      @ <p>Received on %s(zDate) from %h(zUser) at %h(zIp).</p>
    }
    db_finalize(&q);
  }

  style_submenu_element("Download", "%R/raw/%s?at=%T", zUuid, file_tail(zName));
  if( db_exists("SELECT 1 FROM mlink WHERE fid=%d", rid) ){
    style_submenu_element("Check-ins Using", "%R/timeline?uf=%s", zUuid);

  }
  if( zMime ){
    if( fossil_strcmp(zMime, "text/html")==0 ){
      if( asText ){
        style_submenu_element("Html", "%s", url_render(&url, "txt", 0, 0, 0));
      }else{
        renderAsHtml = 1;

        style_submenu_element("Text", "%s", url_render(&url, "txt", "1", 0, 0));

      }
    }else if( fossil_strcmp(zMime, "text/x-fossil-wiki")==0
           || fossil_strcmp(zMime, "text/x-markdown")==0
           || fossil_strcmp(zMime, "text/x-pikchr")==0 ){
      if( asText ){
        style_submenu_element(zMime[7]=='p' ? "Pikchr" : "Wiki",
                              "%s", url_render(&url, "txt", 0, 0, 0));
      }else{
        renderAsWiki = 1;

        style_submenu_element("Text", "%s", url_render(&url, "txt", "1", 0, 0));

      }
    }else if( fossil_strcmp(zMime, "image/svg+xml")==0 ){
      if( asText ){
        style_submenu_element("Svg", "%s", url_render(&url, "txt", 0, 0, 0));
      }else{
        renderAsSvg = 1;

        style_submenu_element("Text", "%s", url_render(&url, "txt", "1", 0, 0));
      }
    }

    if( fileedit_is_editable(zName) ){
      style_submenu_element("Edit",
                            "%R/fileedit?filename=%T&checkin=%!S",
                            zName, zCI);
    }
  }
  if( (objType & (OBJTYPE_WIKI|OBJTYPE_TICKET))!=0 ){
    style_submenu_element("Parsed", "%R/info/%s", zUuid);
  }
  if( descOnly ){
    style_submenu_element("Content", "%R/artifact/%s", zUuid);
  }else{

    @ <hr>

    content_get(rid, &content);
    if( renderAsWiki ){
      safe_html_context(DOCSRC_FILE);
      wiki_render_by_mimetype(&content, zMime);
      document_emit_js();
    }else if( renderAsHtml ){
      @ <iframe src="%R/raw/%s(zUuid)"







>
|
|
|
>







>
|
>









>
|
>






>
|
|
|
>
|











>
|
>







2910
2911
2912
2913
2914
2915
2916
2917
2918
2919
2920
2921
2922
2923
2924
2925
2926
2927
2928
2929
2930
2931
2932
2933
2934
2935
2936
2937
2938
2939
2940
2941
2942
2943
2944
2945
2946
2947
2948
2949
2950
2951
2952
2953
2954
2955
2956
2957
2958
2959
2960
2961
2962
2963
2964
2965
2966
2967
2968
2969
2970
2971
2972
2973
2974
2975
2976
      const char *zUser = db_column_text(&q,0);
      const char *zDate = db_column_text(&q,1);
      const char *zIp = db_column_text(&q,2);
      @ <p>Received on %s(zDate) from %h(zUser) at %h(zIp).</p>
    }
    db_finalize(&q);
  }
  if( !docOnly ){
    style_submenu_element("Download", "%R/raw/%s?at=%T",zUuid,file_tail(zName));
    if( db_exists("SELECT 1 FROM mlink WHERE fid=%d", rid) ){
      style_submenu_element("Check-ins Using", "%R/timeline?uf=%s", zUuid);
    }
  }
  if( zMime ){
    if( fossil_strcmp(zMime, "text/html")==0 ){
      if( asText ){
        style_submenu_element("Html", "%s", url_render(&url, "txt", 0, 0, 0));
      }else{
        renderAsHtml = 1;
        if( !docOnly ){
          style_submenu_element("Text", "%s", url_render(&url, "txt","1",0,0));
        }
      }
    }else if( fossil_strcmp(zMime, "text/x-fossil-wiki")==0
           || fossil_strcmp(zMime, "text/x-markdown")==0
           || fossil_strcmp(zMime, "text/x-pikchr")==0 ){
      if( asText ){
        style_submenu_element(zMime[7]=='p' ? "Pikchr" : "Wiki",
                              "%s", url_render(&url, "txt", 0, 0, 0));
      }else{
        renderAsWiki = 1;
        if( !docOnly ){
          style_submenu_element("Text", "%s", url_render(&url, "txt","1",0,0));
        }
      }
    }else if( fossil_strcmp(zMime, "image/svg+xml")==0 ){
      if( asText ){
        style_submenu_element("Svg", "%s", url_render(&url, "txt", 0, 0, 0));
      }else{
        renderAsSvg = 1;
        if( !docOnly ){
          style_submenu_element("Text", "%s", url_render(&url, "txt","1",0,0));
        }
      }
    }
    if( !docOnly && fileedit_is_editable(zName) ){
      style_submenu_element("Edit",
                            "%R/fileedit?filename=%T&checkin=%!S",
                            zName, zCI);
    }
  }
  if( (objType & (OBJTYPE_WIKI|OBJTYPE_TICKET))!=0 ){
    style_submenu_element("Parsed", "%R/info/%s", zUuid);
  }
  if( descOnly ){
    style_submenu_element("Content", "%R/artifact/%s", zUuid);
  }else{
    if( !docOnly || !isFile ){
      @ <hr>
    }
    content_get(rid, &content);
    if( renderAsWiki ){
      safe_html_context(DOCSRC_FILE);
      wiki_render_by_mimetype(&content, zMime);
      document_emit_js();
    }else if( renderAsHtml ){
      @ <iframe src="%R/raw/%s(zUuid)"
3608
3609
3610
3611
3612
3613
3614

3615
3616
3617
3618
3619
3620
3621
                              rid, TAG_BGCOLOR)==2;
  fNewPropagateColor = P("clr")!=0 ? P("pclr")!=0 : fPropagateColor;
  zNewColorFlag = P("newclr") ? " checked" : "";
  zNewTagFlag = P("newtag") ? " checked" : "";
  zNewTag = PDT("tagname","");
  zNewBrFlag = P("newbr") ? " checked" : "";
  zNewBranch = PDT("brname","");

  zCloseFlag = P("close") ? " checked" : "";
  zHideFlag = P("hide") ? " checked" : "";
  if( P("apply") && cgi_csrf_safe(2) ){
    Blob ctrl;
    char *zNow;

    blob_zero(&ctrl);







>







3632
3633
3634
3635
3636
3637
3638
3639
3640
3641
3642
3643
3644
3645
3646
                              rid, TAG_BGCOLOR)==2;
  fNewPropagateColor = P("clr")!=0 ? P("pclr")!=0 : fPropagateColor;
  zNewColorFlag = P("newclr") ? " checked" : "";
  zNewTagFlag = P("newtag") ? " checked" : "";
  zNewTag = PDT("tagname","");
  zNewBrFlag = P("newbr") ? " checked" : "";
  zNewBranch = PDT("brname","");
  zBranchName = branch_of_rid(rid);
  zCloseFlag = P("close") ? " checked" : "";
  zHideFlag = P("hide") ? " checked" : "";
  if( P("apply") && cgi_csrf_safe(2) ){
    Blob ctrl;
    char *zNow;

    blob_zero(&ctrl);
3655
3656
3657
3658
3659
3660
3661






3662
3663
3664
3665
3666
3667


3668
3669
3670
3671
3672
3673
3674
3675
  blob_zero(&comment);
  blob_append(&comment, zNewComment, -1);
  zUuid[10] = 0;
  style_header("Edit Check-in [%s]", zUuid);
  if( P("preview") ){
    Blob suffix;
    int nTag = 0;






    @ <b>Preview:</b>
    @ <blockquote>
    @ <table border=0>
    if( zNewColorFlag[0] && zNewColor && zNewColor[0] ){
      @ <tr><td style="background-color: %h(zNewColor);">
    }else if( zColor[0] ){


      @ <tr><td style="background-color: %h(zColor);">
    }else{
      @ <tr><td>
    }
    @ %!W(blob_str(&comment))
    blob_zero(&suffix);
    blob_appendf(&suffix, "(user: %h", zNewUser);
    db_prepare(&q, "SELECT substr(tagname,5) FROM tagxref, tag"







>
>
>
>
>
>




|

>
>
|







3680
3681
3682
3683
3684
3685
3686
3687
3688
3689
3690
3691
3692
3693
3694
3695
3696
3697
3698
3699
3700
3701
3702
3703
3704
3705
3706
3707
3708
  blob_zero(&comment);
  blob_append(&comment, zNewComment, -1);
  zUuid[10] = 0;
  style_header("Edit Check-in [%s]", zUuid);
  if( P("preview") ){
    Blob suffix;
    int nTag = 0;
    const char *zDplyBr;   /* Branch name used to determine BG color */
    if( zNewBrFlag[0] && zNewBranch[0] ){
      zDplyBr = zNewBranch;
    }else{
      zDplyBr = zBranchName;
    }
    @ <b>Preview:</b>
    @ <blockquote>
    @ <table border=0>
    if( zNewColorFlag[0] && zNewColor && zNewColor[0] ){
      @ <tr><td style="background-color:%h(reasonable_bg_color(zNewColor,0));">
    }else if( zColor[0] ){
      @ <tr><td style="background-color:%h(reasonable_bg_color(zColor,0));">
    }else if( zDplyBr && fossil_strcmp(zDplyBr,"trunk")!=0 ){
      @ <tr><td style="background-color:%h(hash_color(zDplyBr));">
    }else{
      @ <tr><td>
    }
    @ %!W(blob_str(&comment))
    blob_zero(&suffix);
    blob_appendf(&suffix, "(user: %h", zNewUser);
    db_prepare(&q, "SELECT substr(tagname,5) FROM tagxref, tag"
3746
3747
3748
3749
3750
3751
3752
3753
3754
3755
3756
3757
3758
3759
3760
3761
3762
  @ </td></tr>

  @ <tr><th align="right" valign="top">Tags:</th>
  @ <td valign="top">
  @ <label><input type="checkbox" id="newtag" name="newtag"%s(zNewTagFlag)>
  @ Add the following new tag name to this check-in:</label>
  @ <input size="15" name="tagname" id="tagname" value="%h(zNewTag)">
  zBranchName = db_text(0, "SELECT value FROM tagxref, tag"
     " WHERE tagxref.rid=%d AND tagtype>0 AND tagxref.tagid=tag.tagid"
     " AND tagxref.tagid=%d", rid, TAG_BRANCH);
  db_prepare(&q,
     "SELECT tag.tagid, tagname, tagxref.value FROM tagxref, tag"
     " WHERE tagxref.rid=%d AND tagtype>0 AND tagxref.tagid=tag.tagid"
     " ORDER BY CASE WHEN tagname GLOB 'sym-*' THEN substr(tagname,5)"
     "               ELSE tagname END /*sort*/",
     rid
  );







<
<
<







3779
3780
3781
3782
3783
3784
3785



3786
3787
3788
3789
3790
3791
3792
  @ </td></tr>

  @ <tr><th align="right" valign="top">Tags:</th>
  @ <td valign="top">
  @ <label><input type="checkbox" id="newtag" name="newtag"%s(zNewTagFlag)>
  @ Add the following new tag name to this check-in:</label>
  @ <input size="15" name="tagname" id="tagname" value="%h(zNewTag)">



  db_prepare(&q,
     "SELECT tag.tagid, tagname, tagxref.value FROM tagxref, tag"
     " WHERE tagxref.rid=%d AND tagtype>0 AND tagxref.tagid=tag.tagid"
     " ORDER BY CASE WHEN tagname GLOB 'sym-*' THEN substr(tagname,5)"
     "               ELSE tagname END /*sort*/",
     rid
  );
Changes to src/loadctrl.c.
41
42
43
44
45
46
47




















48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
** %fossil test-loadavg
**
** Print the load average on the host machine.
*/
void loadavg_test_cmd(void){
  fossil_print("load-average: %f\n", load_average());
}





















/*
** Abort the current page request if the load average of the host
** computer is too high. Admin and Setup users are exempt from this
** restriction.
*/
void load_control(void){
  double mxLoad = atof(db_get("max-loadavg", "0.0"));
#if 1
  /* Disable this block only to test load restrictions */
  if( mxLoad<=0.0 || mxLoad>=load_average() ) return;

  login_check_credentials();
  if(g.perm.Admin || g.perm.Setup){
    return;
  }
#endif

  style_set_current_feature("test");
  style_header("Server Overload");
  @ <h2>The server load is currently too high.
  @ Please try again later.</h2>
  @ <p>Current load average: %f(load_average()).<br>
  @ Load average limit: %f(mxLoad)</p>
  style_finish_page();
  cgi_set_status(503,"Server Overload");
  cgi_reply();
  exit(0);
}







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>

















<
<
<
<
<
<
<
|




41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84







85
86
87
88
89
** %fossil test-loadavg
**
** Print the load average on the host machine.
*/
void loadavg_test_cmd(void){
  fossil_print("load-average: %f\n", load_average());
}

/*
** WEBPAGE:  test-overload
**
** Generate the response that would normally be shown only when
** service is denied due to an overload condition.  This is for
** testing of the overload warning page.
*/
void overload_page(void){
  double mxLoad = atof(db_get("max-loadavg", "0.0"));
  style_set_current_feature("test");
  style_header("Server Overload");
  @ <h2>The server load is currently too high.
  @ Please try again later.</h2>
  @ <p>Current load average: %f(load_average())<br>
  @ Load average limit: %f(mxLoad)<br>
  @ URL: %h(g.zBaseURL)%h(P("PATH_INFO"))<br>
  @ Timestamp: %h(db_text("","SELECT datetime()"))Z</p>
  style_finish_page();
}

/*
** Abort the current page request if the load average of the host
** computer is too high. Admin and Setup users are exempt from this
** restriction.
*/
void load_control(void){
  double mxLoad = atof(db_get("max-loadavg", "0.0"));
#if 1
  /* Disable this block only to test load restrictions */
  if( mxLoad<=0.0 || mxLoad>=load_average() ) return;

  login_check_credentials();
  if(g.perm.Admin || g.perm.Setup){
    return;
  }
#endif







  overload_page();
  cgi_set_status(503,"Server Overload");
  cgi_reply();
  exit(0);
}
Changes to src/login.c.
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311



1312
1313














1314
1315
1316
1317
1318
1319
1320
**
**    robot-restrict    The value is a list of GLOB patterns for pages
**                      that should restrict robot access.  No restrictions
**                      are applied if this setting is undefined or is
**                      an empty string.
*/
void login_restrict_robot_access(void){
  const char *zReferer;
  const char *zGlob;
  int isMatch = 1;
  int nQP;  /* Number of query parameters other than name= */
  if( g.zLogin!=0 ) return;
  zGlob = db_get("robot-restrict",0);
  if( zGlob==0 || zGlob[0]==0 ) return;
  if( g.isHuman ){



    zReferer = P("HTTP_REFERER");
    if( zReferer && zReferer[0]!=0 ) return;














  }
  nQP = cgi_qp_count();
  if( nQP<1 ) return;
  isMatch = glob_multi_match(zGlob, g.zPath);
  if( !isMatch ) return;

  /* Check for exceptions to the restriction on the number of query







<







>
>
>


>
>
>
>
>
>
>
>
>
>
>
>
>
>







1297
1298
1299
1300
1301
1302
1303

1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
**
**    robot-restrict    The value is a list of GLOB patterns for pages
**                      that should restrict robot access.  No restrictions
**                      are applied if this setting is undefined or is
**                      an empty string.
*/
void login_restrict_robot_access(void){

  const char *zGlob;
  int isMatch = 1;
  int nQP;  /* Number of query parameters other than name= */
  if( g.zLogin!=0 ) return;
  zGlob = db_get("robot-restrict",0);
  if( zGlob==0 || zGlob[0]==0 ) return;
  if( g.isHuman ){
    const char *zReferer;
    const char *zAccept;
    const char *zBr;
    zReferer = P("HTTP_REFERER");
    if( zReferer && zReferer[0]!=0 ) return;

    /* Robots typically do not accept the brotli encoding, at least not
    ** at the time of this writing (2025-04-01), but standard web-browser
    ** all generally do accept brotli.  So if brotli is accepted,
    ** assume we are not talking to a robot.  We might want to revisit this
    ** heuristic in the future...
    */
    if( (zAccept = P("HTTP_ACCEPT_ENCODING"))!=0
     && (zBr = strstr(zAccept,"br"))!=0
     && !fossil_isalnum(zBr[2])
     && (zBr==zAccept || !fossil_isalnum(zBr[-1]))
    ){
      return;
    }
  }
  nQP = cgi_qp_count();
  if( nQP<1 ) return;
  isMatch = glob_multi_match(zGlob, g.zPath);
  if( !isMatch ) return;

  /* Check for exceptions to the restriction on the number of query
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
  ** This feature allows the "fossil ui" command to give the user
  ** full access rights without having to log in.
  */
  zIpAddr = PD("REMOTE_ADDR","nil");
  if( ( cgi_is_loopback(zIpAddr)
       || (g.fSshClient & CGI_SSH_CLIENT)!=0 )
   && g.useLocalauth
   && db_get_int("localauth",0)==0
   && P("HTTPS")==0
  ){
    char *zSeed;
    if( g.localOpen ) zLogin = db_lget("default-user",0);
    if( zLogin!=0 ){
      uid = db_int(0, "SELECT uid FROM user WHERE login=%Q", zLogin);
    }else{







|







1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
  ** This feature allows the "fossil ui" command to give the user
  ** full access rights without having to log in.
  */
  zIpAddr = PD("REMOTE_ADDR","nil");
  if( ( cgi_is_loopback(zIpAddr)
       || (g.fSshClient & CGI_SSH_CLIENT)!=0 )
   && g.useLocalauth
   && db_get_boolean("localauth",0)==0
   && P("HTTPS")==0
  ){
    char *zSeed;
    if( g.localOpen ) zLogin = db_lget("default-user",0);
    if( zLogin!=0 ){
      uid = db_int(0, "SELECT uid FROM user WHERE login=%Q", zLogin);
    }else{
Changes to src/lookslike.c.
476
477
478
479
480
481
482
483
484
485
486
487

488
489
490

491
492
493

494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525









526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
  if( strchr("(_", z[i+n])!=0 ) return 0;
  return 1;
}

/*
** Returns true if the given text contains certain keywords or
** punctuation which indicate that it might be an SQL injection attempt
** or some other kind of mischief.
**
** This is not a defense against vulnerabilities in the Fossil code.
** Rather, this is part of an effort to do early detection of malicious
** spiders to avoid them using up too many CPU cycles.

*/
int looks_like_sql_injection(const char *zTxt){
  unsigned int i;

  if( zTxt==0 ) return 0;
  for(i=0; zTxt[i]; i++){
    switch( zTxt[i] ){

      case ';':
      case '\'':
        return 1;
      case '/':             /* 0123456789 123456789 */
        if( strncmp(zTxt+i+1, "/wp-content/plugins/", 20)==0 ) return 1;
        if( strncmp(zTxt+i+1, "/wp-admin/admin-ajax", 20)==0 ) return 1;
        break;
      case 'a':
      case 'A':
        if( isWholeWord(zTxt, i, "and", 3) ) return 1;
        break;
      case 'n':
      case 'N':
        if( isWholeWord(zTxt, i, "null", 4) ) return 1;
        break;
      case 'o':
      case 'O':
        if( isWholeWord(zTxt, i, "order", 5) && fossil_isspace(zTxt[i+5]) ){
          return 1;
        }
        if( isWholeWord(zTxt, i, "or", 2) ) return 1;
        break;
      case 's':
      case 'S':
        if( isWholeWord(zTxt, i, "select", 6) ) return 1;
        break;
      case 'w':
      case 'W':
        if( isWholeWord(zTxt, i, "waitfor", 7) ) return 1;
        break;
    }
  }









  return 0;
}

/*
** This is a utility routine associated with the test-looks-like-sql-injection
** command.
**
** Read input from zInFile and print only those lines that look like they
** might be SQL injection.
**
** Or if bInvert is true, then show the opposite - those lines that do NOT
** look like SQL injection.
*/
static void show_sql_injection_lines(
  const char *zInFile,       /* Name of input file */
  int bInvert,               /* Invert the sense of the output (-v) */
  int bDeHttpize             /* De-httpize the inputs.  (-d) */
){
  FILE *in;
  char zLine[10000];
  if( zInFile==0 || strcmp(zInFile,"-")==0 ){
    in = stdin;
  }else{
    in = fopen(zInFile, "rb");
    if( in==0 ){
      fossil_fatal("cannot open \"%s\" for reading\n", zInFile);
    }
  }
  while( fgets(zLine, sizeof(zLine), in) ){
    dehttpize(zLine);
    if( (looks_like_sql_injection(zLine)!=0) ^ bInvert ){
      fossil_print("%s", zLine);
    }
  }
  if( in!=stdin ) fclose(in);
}

/*
** COMMAND: test-looks-like-sql-injection
**
** Read lines of input from files named as arguments (or from standard
** input if no arguments are provided) and print those that look like they
** might be part of an SQL injection attack.
**
** Used to test the looks_lide_sql_injection() utility subroutine, possibly
** by piping in actual server log data.
*/
void test_looks_like_sql_injection(void){
  int i;
  int bInvert = find_option("invert","v",0)!=0;
  int bDeHttpize = find_option("dehttpize","d",0)!=0;
  verify_all_options();
  if( g.argc==2 ){
    show_sql_injection_lines(0, bInvert, bDeHttpize);
  }
  for(i=2; i<g.argc; i++){
    show_sql_injection_lines(g.argv[i], bInvert, bDeHttpize);
  }
}







|

|
|
|
>

|

>



>




|
|



|



|




|

|



|



|



>
>
>
>
>
>
>
>
>
|












|
















|







|





|


|





|


|


476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
  if( strchr("(_", z[i+n])!=0 ) return 0;
  return 1;
}

/*
** Returns true if the given text contains certain keywords or
** punctuation which indicate that it might be an SQL injection attempt
** or Cross-site scripting attempt or some other kind of mischief.
**
** This is not a primary defense against vulnerabilities in the Fossil 
** code.  Rather, this is part of an effort to do early detection of malicious
** spiders to avoid them using up too many CPU cycles.  Or, this routine
** can also be thought of as a secondary layer of defense against attacks.
*/
int looks_like_attack(const char *zTxt){
  unsigned int i;
  int rc = 0;
  if( zTxt==0 ) return 0;
  for(i=0; zTxt[i]; i++){
    switch( zTxt[i] ){
      case '<':
      case ';':
      case '\'':
        return 1;
      case '/':             /* 0123456789 123456789 */
        if( strncmp(zTxt+i+1, "/wp-content/plugins/", 20)==0 ) rc = 1;
        if( strncmp(zTxt+i+1, "/wp-admin/admin-ajax", 20)==0 ) rc = 1;
        break;
      case 'a':
      case 'A':
        if( isWholeWord(zTxt, i, "and", 3) ) rc = 1;
        break;
      case 'n':
      case 'N':
        if( isWholeWord(zTxt, i, "null", 4) ) rc = 1;
        break;
      case 'o':
      case 'O':
        if( isWholeWord(zTxt, i, "order", 5) && fossil_isspace(zTxt[i+5]) ){
          rc = 1;
        }
        if( isWholeWord(zTxt, i, "or", 2) ) rc = 1;
        break;
      case 's':
      case 'S':
        if( isWholeWord(zTxt, i, "select", 6) ) rc = 1;
        break;
      case 'w':
      case 'W':
        if( isWholeWord(zTxt, i, "waitfor", 7) ) rc = 1;
        break;
    }
  }
  if( rc ){
    /* The test/markdown-test3.md document which is part of the Fossil source
    ** tree intentionally tries to fake an attack.  Do not report such
    ** errors. */
    const char *zPathInfo = P("PATH_INFO");
    if( sqlite3_strglob("/doc/*/test/markdown-test3.md", zPathInfo)==0 ){
      rc = 0;
    }
  }
  return rc;
}

/*
** This is a utility routine associated with the test-looks-like-sql-injection
** command.
**
** Read input from zInFile and print only those lines that look like they
** might be SQL injection.
**
** Or if bInvert is true, then show the opposite - those lines that do NOT
** look like SQL injection.
*/
static void show_attack_lines(
  const char *zInFile,       /* Name of input file */
  int bInvert,               /* Invert the sense of the output (-v) */
  int bDeHttpize             /* De-httpize the inputs.  (-d) */
){
  FILE *in;
  char zLine[10000];
  if( zInFile==0 || strcmp(zInFile,"-")==0 ){
    in = stdin;
  }else{
    in = fopen(zInFile, "rb");
    if( in==0 ){
      fossil_fatal("cannot open \"%s\" for reading\n", zInFile);
    }
  }
  while( fgets(zLine, sizeof(zLine), in) ){
    dehttpize(zLine);
    if( (looks_like_attack(zLine)!=0) ^ bInvert ){
      fossil_print("%s", zLine);
    }
  }
  if( in!=stdin ) fclose(in);
}

/*
** COMMAND: test-looks-like-attack
**
** Read lines of input from files named as arguments (or from standard
** input if no arguments are provided) and print those that look like they
** might be part of an SQL injection attack.
**
** Used to test the looks_lile_attack() utility subroutine, possibly
** by piping in actual server log data.
*/
void test_looks_like_attack(void){
  int i;
  int bInvert = find_option("invert","v",0)!=0;
  int bDeHttpize = find_option("dehttpize","d",0)!=0;
  verify_all_options();
  if( g.argc==2 ){
    show_attack_lines(0, bInvert, bDeHttpize);
  }
  for(i=2; i<g.argc; i++){
    show_attack_lines(g.argv[i], bInvert, bDeHttpize);
  }
}
Changes to src/main.c.
2051
2052
2053
2054
2055
2056
2057
2058

2059
2060
2061
2062
2063
2064
2065
  /* Use the first element of PATH_INFO as the page name
  ** and deliver the appropriate page back to the user.
  */
  set_base_url(0);
  if( fossil_redirect_to_https_if_needed(2) ) return;
  if( zPathInfo==0 || zPathInfo[0]==0
      || (zPathInfo[0]=='/' && zPathInfo[1]==0) ){
    /* Second special case: If the PATH_INFO is blank, issue a redirect:

    **    (1) to "/ckout" if g.useLocalauth and g.localOpen are both set.
    **    (2) to the home page identified by the "index-page" setting
    **        in the repository CONFIG table
    **    (3) to "/index" if there no "index-page" setting in CONFIG
    */
#ifdef FOSSIL_ENABLE_JSON
    if(g.json.isJsonMode){







|
>







2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
  /* Use the first element of PATH_INFO as the page name
  ** and deliver the appropriate page back to the user.
  */
  set_base_url(0);
  if( fossil_redirect_to_https_if_needed(2) ) return;
  if( zPathInfo==0 || zPathInfo[0]==0
      || (zPathInfo[0]=='/' && zPathInfo[1]==0) ){
    /* Second special case: If the PATH_INFO is blank, issue a
    ** temporary 302 redirect:
    **    (1) to "/ckout" if g.useLocalauth and g.localOpen are both set.
    **    (2) to the home page identified by the "index-page" setting
    **        in the repository CONFIG table
    **    (3) to "/index" if there no "index-page" setting in CONFIG
    */
#ifdef FOSSIL_ENABLE_JSON
    if(g.json.isJsonMode){
2265
2266
2267
2268
2269
2270
2271











2272
2273
2274
2275
2276
2277
2278
**      website we have an CGI at http://fossil.com/index.html (note
**      ".com" instead of ".org") that looks like this:
**
**          #!/usr/bin/fossil
**          redirect: * https://fossil-scm.org/home
**
**      Thus requests to the .com website redirect to the .org website.











*/
static void redirect_web_page(int nRedirect, char **azRedirect){
  int i;                             /* Loop counter */
  const char *zNotFound = 0;         /* Not found URL */
  const char *zName = P("name");
  set_base_url(0);
  if( zName==0 ){







>
>
>
>
>
>
>
>
>
>
>







2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
**      website we have an CGI at http://fossil.com/index.html (note
**      ".com" instead of ".org") that looks like this:
**
**          #!/usr/bin/fossil
**          redirect: * https://fossil-scm.org/home
**
**      Thus requests to the .com website redirect to the .org website.
**      This form uses a 301 Permanent redirect.
**
**      On a "*" redirect, the PATH_INFO and QUERY_STRING of the query
**      that provoked the redirect are appended to the target.  So, for
**      example, if the input URL for the redirect above were
**      "http://www.fossil.com/index.html/timeline?c=20250404", then
**      the redirect would be to:
**
**           https://fossil-scm.org/home/timeline?c=20250404
**                                      ^^^^^^^^^^^^^^^^^^^^
**                                      Copied from input URL
*/
static void redirect_web_page(int nRedirect, char **azRedirect){
  int i;                             /* Loop counter */
  const char *zNotFound = 0;         /* Not found URL */
  const char *zName = P("name");
  set_base_url(0);
  if( zName==0 ){
2295
2296
2297
2298
2299
2300
2301
2302

2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
      }
    }
  }
  if( zNotFound ){
    Blob to;
    const char *z;
    if( strstr(zNotFound, "%s") ){
      cgi_redirectf(zNotFound /*works-like:"%s"*/, zName);

    }
    if( strchr(zNotFound, '?') ){
      cgi_redirect(zNotFound);
    }
    blob_init(&to, zNotFound, -1);
    z = P("PATH_INFO");
    if( z && z[0]=='/' ) blob_append(&to, z, -1);
    z = P("QUERY_STRING");
    if( z && z[0]!=0 ) blob_appendf(&to, "?%s", z);
    cgi_redirect(blob_str(&to));
  }else{
    @ <html>
    @ <head><title>No Such Object</title></head>
    @ <body>
    @ <p>No such object: <b>%h(zName)</b></p>
    @ </body>
    cgi_reply();







|
>


|






|







2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
      }
    }
  }
  if( zNotFound ){
    Blob to;
    const char *z;
    if( strstr(zNotFound, "%s") ){
      char *zTarget = mprintf(zNotFound /*works-like:"%s"*/, zName);
      cgi_redirect_perm(zTarget);
    }
    if( strchr(zNotFound, '?') ){
      cgi_redirect_perm(zNotFound);
    }
    blob_init(&to, zNotFound, -1);
    z = P("PATH_INFO");
    if( z && z[0]=='/' ) blob_append(&to, z, -1);
    z = P("QUERY_STRING");
    if( z && z[0]!=0 ) blob_appendf(&to, "?%s", z);
    cgi_redirect_perm(blob_str(&to));
  }else{
    @ <html>
    @ <head><title>No Such Object</title></head>
    @ <body>
    @ <p>No such object: <b>%h(zName)</b></p>
    @ </body>
    cgi_reply();
2350
2351
2352
2353
2354
2355
2356
2357







2358
2359
2360
2361
2362
2363
2364
**                             or "directory:"
**
**    notfound: URL            When in "directory:" mode, redirect to
**                             URL if no suitable repository is found.
**
**    repolist                 When in "directory:" mode, display a page
**                             showing a list of available repositories if
**                             the URL is "/".







**
**    localauth                Grant administrator privileges to connections
**                             from 127.0.0.1 or ::1.
**
**    nossl                    Signal that no SSL connections are available.
**
**    nocompress               Do not compress HTTP replies.







|
>
>
>
>
>
>
>







2363
2364
2365
2366
2367
2368
2369
2370
2371
2372
2373
2374
2375
2376
2377
2378
2379
2380
2381
2382
2383
2384
**                             or "directory:"
**
**    notfound: URL            When in "directory:" mode, redirect to
**                             URL if no suitable repository is found.
**
**    repolist                 When in "directory:" mode, display a page
**                             showing a list of available repositories if
**                             the URL is "/".  Some control over the display
**                             is accomplished using environment variables.
**                             FOSSIL_REPOLIST_TITLE is the tital of the page.
**                             FOSSIL_REPOLIST_SHOW cause the "Description"
**                             column to display if it contains "description" as
**                             as a substring, and causes the Login-Group column
**                             to display if it contains the "login-group"
**                             substring.
**
**    localauth                Grant administrator privileges to connections
**                             from 127.0.0.1 or ::1.
**
**    nossl                    Signal that no SSL connections are available.
**
**    nocompress               Do not compress HTTP replies.
2392
2393
2394
2395
2396
2397
2398



2399
2400
2401
2402
2403
2404
2405
**
**    redirect: REPO URL       Extract the "name" query parameter and search
**                             REPO for a check-in or ticket that matches the
**                             value of "name", then redirect to URL.  There
**                             can be multiple "redirect:" lines that are
**                             processed in order.  If the REPO is "*", then
**                             an unconditional redirect to URL is taken.



**
**    jsmode: VALUE            Specifies the delivery mode for JavaScript
**                             files. See the help text for the --jsmode
**                             flag of the http command.
**
**    mainmenu: FILE           Override the mainmenu config setting with the
**                             contents of the given file.







>
>
>







2412
2413
2414
2415
2416
2417
2418
2419
2420
2421
2422
2423
2424
2425
2426
2427
2428
**
**    redirect: REPO URL       Extract the "name" query parameter and search
**                             REPO for a check-in or ticket that matches the
**                             value of "name", then redirect to URL.  There
**                             can be multiple "redirect:" lines that are
**                             processed in order.  If the REPO is "*", then
**                             an unconditional redirect to URL is taken.
**                             When "*" is used a 301 permanent redirect is
**                             issued and the tail and query string from the
**                             original query are appeneded onto URL.
**
**    jsmode: VALUE            Specifies the delivery mode for JavaScript
**                             files. See the help text for the --jsmode
**                             flag of the http command.
**
**    mainmenu: FILE           Override the mainmenu config setting with the
**                             contents of the given file.
3050
3051
3052
3053
3054
3055
3056

3057
3058
3059
3060
3061
3062
3063
3064
3065
3066
3067

3068
3069

3070
3071
3072
3073
3074
3075
3076
** and the SSH_CONNECTION environment variable is set.  Use the --test
** option on interactive sessions to avoid that special processing when
** using this command interactively over SSH.  A better solution would be
** to use a different command for "ssh" sync, but we cannot do that without
** breaking legacy.
**
** Options:

**   --nobody            Pretend to be user "nobody"
**   --test              Do not do special "sync" processing when operating
**                       over an SSH link
**   --th-trace          Trace TH1 execution (for debugging purposes)
**   --usercap   CAP     User capability string (Default: "sxy")
**
*/
void cmd_test_http(void){
  const char *zIpAddr;    /* IP address of remote client */
  const char *zUserCap;
  int bTest = 0;


  Th_InitTraceLog();

  zUserCap = find_option("usercap",0,1);
  if( !find_option("nobody",0,0) ){
    if( zUserCap==0 ){
      g.useLocalauth = 1;
      zUserCap = "sxy";
    }
    login_set_capabilities(zUserCap, 0);







>





<





>


>







3073
3074
3075
3076
3077
3078
3079
3080
3081
3082
3083
3084
3085

3086
3087
3088
3089
3090
3091
3092
3093
3094
3095
3096
3097
3098
3099
3100
3101
** and the SSH_CONNECTION environment variable is set.  Use the --test
** option on interactive sessions to avoid that special processing when
** using this command interactively over SSH.  A better solution would be
** to use a different command for "ssh" sync, but we cannot do that without
** breaking legacy.
**
** Options:
**   --csrf-safe N       Set cgi_csrf_safe() to to return N
**   --nobody            Pretend to be user "nobody"
**   --test              Do not do special "sync" processing when operating
**                       over an SSH link
**   --th-trace          Trace TH1 execution (for debugging purposes)
**   --usercap   CAP     User capability string (Default: "sxy")

*/
void cmd_test_http(void){
  const char *zIpAddr;    /* IP address of remote client */
  const char *zUserCap;
  int bTest = 0;
  const char *zCsrfSafe = find_option("csrf-safe",0,1);

  Th_InitTraceLog();
  if( zCsrfSafe ) g.okCsrf = atoi(zCsrfSafe);
  zUserCap = find_option("usercap",0,1);
  if( !find_option("nobody",0,0) ){
    if( zUserCap==0 ){
      g.useLocalauth = 1;
      zUserCap = "sxy";
    }
    login_set_capabilities(zUserCap, 0);
3503
3504
3505
3506
3507
3508
3509
3510

3511
3512
3513
3514
3515
3516
3517
        if( ssh_needs_path_argument(zRemote,-1) ^ isRetry ){
          ssh_add_path_argument(&ssh);
        }
        blob_append_escaped_arg(&ssh, "fossil", 1);
      }else{
        blob_appendf(&ssh, " %$", zFossilCmd);
      }
      blob_appendf(&ssh, " ui --nobrowser --localauth --port %d", iPort);

      if( zNotFound ) blob_appendf(&ssh, " --notfound %!$", zNotFound);
      if( zFileGlob ) blob_appendf(&ssh, " --files-urlenc %T", zFileGlob);
      if( g.zCkoutAlias ) blob_appendf(&ssh," --ckout-alias %!$",g.zCkoutAlias);
      if( zExtPage ){
        if( !file_is_absolute_path(zExtPage) ){
          zExtPage = mprintf("%s/%s", g.argv[2], zExtPage);
        }







|
>







3528
3529
3530
3531
3532
3533
3534
3535
3536
3537
3538
3539
3540
3541
3542
3543
        if( ssh_needs_path_argument(zRemote,-1) ^ isRetry ){
          ssh_add_path_argument(&ssh);
        }
        blob_append_escaped_arg(&ssh, "fossil", 1);
      }else{
        blob_appendf(&ssh, " %$", zFossilCmd);
      }
      blob_appendf(&ssh, " ui --nobrowser --localauth --port 127.0.0.1:%d",
                   iPort);
      if( zNotFound ) blob_appendf(&ssh, " --notfound %!$", zNotFound);
      if( zFileGlob ) blob_appendf(&ssh, " --files-urlenc %T", zFileGlob);
      if( g.zCkoutAlias ) blob_appendf(&ssh," --ckout-alias %!$",g.zCkoutAlias);
      if( zExtPage ){
        if( !file_is_absolute_path(zExtPage) ){
          zExtPage = mprintf("%s/%s", g.argv[2], zExtPage);
        }
3690
3691
3692
3693
3694
3695
3696



3697
3698
3699
3700
3701
3702
3703
3704
3705
3706
3707
3708
3709

3710
3711
3712
3713
3714
3715
3716
3717
3718
3719
3720
3721
3722
**     case=1           Issue a fossil_warning() while generating the page.
**     case=2           Extra db_begin_transaction()
**     case=3           Extra db_end_transaction()
**     case=4           Error during SQL processing
**     case=5           Call the segfault handler
**     case=6           Call webpage_assert()
**     case=7           Call webpage_error()



*/
void test_warning_page(void){
  int iCase = atoi(PD("case","0"));
  int i;
  login_check_credentials();
  if( !g.perm.Admin ){
    login_needed(0);
    return;
  }
  style_set_current_feature("test");
  style_header("Warning Test Page");
  style_submenu_element("Error Log","%R/errorlog");
  if( iCase<1 || iCase>4 ){

    @ <p>Generate a message to the <a href="%R/errorlog">error log</a>
    @ by clicking on one of the following cases:
  }else{
    @ <p>This is the test page for case=%d(iCase).  All possible cases:
  }
  for(i=1; i<=8; i++){
    @ <a href='./test-warning?case=%d(i)'>[%d(i)]</a>
  }
  @ </p>
  @ <p><ol>
  @ <li value='1'> Call fossil_warning()
  if( iCase==1 ){
    fossil_warning("Test warning message from /test-warning");







>
>
>












|
>
|
|
<
<
<
|







3716
3717
3718
3719
3720
3721
3722
3723
3724
3725
3726
3727
3728
3729
3730
3731
3732
3733
3734
3735
3736
3737
3738
3739
3740
3741



3742
3743
3744
3745
3746
3747
3748
3749
**     case=1           Issue a fossil_warning() while generating the page.
**     case=2           Extra db_begin_transaction()
**     case=3           Extra db_end_transaction()
**     case=4           Error during SQL processing
**     case=5           Call the segfault handler
**     case=6           Call webpage_assert()
**     case=7           Call webpage_error()
**     case=8           Simulate a timeout
**     case=9           Simulate a TH1 XSS vulnerability
**     case=10          Simulate a TH1 SQL-injection vulnerability
*/
void test_warning_page(void){
  int iCase = atoi(PD("case","0"));
  int i;
  login_check_credentials();
  if( !g.perm.Admin ){
    login_needed(0);
    return;
  }
  style_set_current_feature("test");
  style_header("Warning Test Page");
  style_submenu_element("Error Log","%R/errorlog");
  @ <p>This page will generate various kinds of errors to test Fossil's
  @ reaction.  Depending on settings, a message might be written
  @ into the <a href="%R/errorlog">error log</a>.  Click on
  @ one of the following hyperlinks to generate a simulated error:



  for(i=1; i<=10; i++){
    @ <a href='./test-warning?case=%d(i)'>[%d(i)]</a>
  }
  @ </p>
  @ <p><ol>
  @ <li value='1'> Call fossil_warning()
  if( iCase==1 ){
    fossil_warning("Test warning message from /test-warning");
3741
3742
3743
3744
3745
3746
3747
3748
3749
3750
3751
3752
3753
3754
3755
3756
3757



















3758
3759
3760
3761
3762
  if( iCase==5 ){
    sigsegv_handler(0);
  }
  @ <li value='6'> call webpage_assert(0)
  if( iCase==6 ){
    webpage_assert( 5==7 );
  }
  @ <li value='7'> call webpage_error()"
  if( iCase==7 ){
    cgi_reset_content();
    webpage_error("Case 7 from /test-warning");
  }
  @ <li value='8'> simulated timeout"
  if( iCase==8 ){
    fossil_set_timeout(1);
    cgi_reset_content();
    sqlite3_sleep(1100);



















  }
  @ </ol>
  @ <p>End of test</p>
  style_finish_page();
}







|




|




>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>





3768
3769
3770
3771
3772
3773
3774
3775
3776
3777
3778
3779
3780
3781
3782
3783
3784
3785
3786
3787
3788
3789
3790
3791
3792
3793
3794
3795
3796
3797
3798
3799
3800
3801
3802
3803
3804
3805
3806
3807
3808
  if( iCase==5 ){
    sigsegv_handler(0);
  }
  @ <li value='6'> call webpage_assert(0)
  if( iCase==6 ){
    webpage_assert( 5==7 );
  }
  @ <li value='7'> call webpage_error()
  if( iCase==7 ){
    cgi_reset_content();
    webpage_error("Case 7 from /test-warning");
  }
  @ <li value='8'> simulated timeout
  if( iCase==8 ){
    fossil_set_timeout(1);
    cgi_reset_content();
    sqlite3_sleep(1100);
  }
  @ <li value='9'> simulated TH1 XSS vulnerability
  @ <li value='10'> simulated TH1 SQL-injection vulnerability
  if( iCase==9 || iCase==10 ){
    const char *zR;
    int n, rc;
    static const char *zTH1[] = {
       /* case 9 */  "html [taint {<b>XSS</b>}]",
       /* case 10 */ "query [taint {SELECT 'SQL-injection' AS msg}] {\n"
                     "  html \"<b>[htmlize $msg]</b>\"\n"
                     "}"
    };
    rc = Th_Eval(g.interp, 0, zTH1[iCase==10], -1);
    zR = Th_GetResult(g.interp, &n);
    if( rc==TH_OK ){
      @ <pre class="th1result">%h(zR)</pre>
    }else{
      @ <pre class="th1error">%h(zR)</pre>
    }
  }
  @ </ol>
  @ <p>End of test</p>
  style_finish_page();
}
Changes to src/manifest.c.
2925
2926
2927
2928
2929
2930
2931
2932
2933
2934
2935
2936
2937
2938
2939
2940
2941
2942
2943
2944
2945
2946
2947
2948
2949
2950
2951
2952
2953
2954
2955
2956
2957
2958
2959
2960
2961
2962
2963
2964
2965
2966
2967
2968
2969
2970
2971
2972
2973
2974
2975
2976
2977
2978
2979
2980
2981
2982
2983
2984
2985
2986
2987
2988
2989
2990
2991
2992
2993
2994
2995
2996
2997
2998
2999
3000
3001
3002
3003
3004


3005
3006

3007
3008
3009
3010

3011
3012
3013
3014

3015
3016
3017
3018
3019
3020
3021
3022
3023
3024
3025
3026
3027
3028
3029
3030
3031
3032
3033
3034
3035
3036
3037
3038
3039
3040
3041
3042
3043
3044
3045
3046
3047
3048
3049
3050
3051
3052
3053
3054
3055
3056
3057
3058
3059
3060
3061
3062
3063
3064
3065
3066
3067
3068
3069
3070
3071
3072
3073
3074


3075


3076
3077
3078
3079
3080
3081
3082
  switch(typeId){
    case CFTYPE_MANIFEST: return "checkin";
    case CFTYPE_CLUSTER: return "cluster";
    case CFTYPE_CONTROL: return "tag";
    case CFTYPE_WIKI: return "wiki";
    case CFTYPE_TICKET: return "ticket";
    case CFTYPE_ATTACHMENT: return "attachment";
    case CFTYPE_EVENT: return "event";
    case CFTYPE_FORUM: return "forumpost";
  }
  return NULL;
}

/*
** Creates a JSON representation of p, appending it to b.
**
** b is not cleared before rendering, so the caller needs to do that
** if it's important for their use case.
**
** Pedantic note: this routine traverses p->aFile directly, rather
** than using manifest_file_next(), so that delta manifests are
** rendered as-is instead of containing their derived F-cards. If that
** policy is ever changed, p will need to be non-const.
*/
void artifact_to_json(Manifest const *p, Blob *b){
  int i;

  blob_append_literal(b, "{");
  blob_appendf(b, "\"uuid\": \"%z\"", rid_to_uuid(p->rid));
  /*blob_appendf(b, ", \"rid\": %d", p->rid); not portable across repos*/
  blob_appendf(b, ", \"type\": %!j", artifact_type_to_name(p->type));
#define ISA(TYPE) if( p->type==TYPE )
#define CARD_LETTER(LETTER) \
  blob_append_literal(b, ",\"" #LETTER "\": ")
#define CARD_STR(LETTER, VAL) \
  assert( VAL ); CARD_LETTER(LETTER); blob_appendf(b, "%!j", VAL)
#define CARD_STR2(LETTER, VAL) \
  if( VAL ) { CARD_STR(LETTER, VAL); } (void)0
#define STR_OR_NULL(VAL)                 \
  if( VAL ) blob_appendf(b, "%!j", VAL); \
  else blob_append(b, "null", 4)
#define KVP_STR(ADDCOMMA, KEY,VAL)  \
  if(ADDCOMMA) blob_append_char(b, ','); \
  blob_appendf(b, "%!j: ", #KEY);   \
  STR_OR_NULL(VAL)

  ISA( CFTYPE_ATTACHMENT ){
    CARD_LETTER(A);
    blob_append_char(b, '{');
    KVP_STR(0, filename, p->zAttachName);
    KVP_STR(1, target, p->zAttachTarget);
    KVP_STR(1, source, p->zAttachSrc);
    blob_append_char(b, '}');
  }
  CARD_STR2(B, p->zBaseline);
  CARD_STR2(C, p->zComment);
  CARD_LETTER(D); blob_appendf(b, "%f", p->rDate);
  ISA( CFTYPE_EVENT ){
    blob_appendf(b, ", \"E\": {\"time\": %f, \"id\": %!j}",
                 p->rEventDate, p->zEventId);
  }
  ISA( CFTYPE_MANIFEST ){
    CARD_LETTER(F);
    blob_append_char(b, '[');
    for( i = 0; i < p->nFile; ++i ){
      ManifestFile const * const pF = &p->aFile[i];
      if( i>0 ) blob_append_char(b, ',');
      blob_append_char(b, '{');
      KVP_STR(0, name, pF->zName);
      KVP_STR(1, uuid, pF->zUuid);
      KVP_STR(1, perm, pF->zPerm);
      KVP_STR(1, oldName, pF->zPrior);
      blob_append_char(b, '}');
    }
    /* Special case: model checkins with no F-card as having an empty
    ** array, rather than no F-cards, to hypothetically simplify
    ** handling in JSON queries. */
    blob_append_char(b, ']');
  }
  CARD_STR2(G, p->zThreadRoot);


  CARD_STR2(H, p->zThreadTitle);
  CARD_STR2(I, p->zInReplyTo);

  if( p->nField ){
    CARD_LETTER(J);
    blob_append_char(b, '[');
    for( i = 0; i < p->nField; ++i ){

      if( i>0 ) blob_append_char(b, ',');
      blob_append_char(b, '{');
      KVP_STR(0, name, p->aField[i].zName);
      KVP_STR(1, value, p->aField[i].zValue);

      blob_append_char(b, '}');
    }
    blob_append_char(b, ']');
  }
  CARD_STR2(K, p->zTicketUuid);
  CARD_STR2(L, p->zWikiTitle);
  ISA( CFTYPE_CLUSTER ){
    CARD_LETTER(M);
    blob_append_char(b, '[');
    for( int i = 0; i < p->nCChild; ++i ){
      if( i>0 ) blob_append_char(b, ',');
      blob_appendf(b, "%!j", p->azCChild[i]);
    }
    blob_append_char(b, ']');
  }
  CARD_STR2(N, p->zMimetype);
  ISA( CFTYPE_MANIFEST ){
    CARD_LETTER(P);
    blob_append_char(b, '[');
    if( p->nParent ){
      for( i = 0; i < p->nParent; ++i ){
        if( i>0 ) blob_append_char(b, ',');
        blob_appendf(b, "%!j", p->azParent[i]);
      }
    }
    /* Special case: model checkins with no P-card as having an empty
    ** array, as per F-cards. */
    blob_append_char(b, ']');
  }
  if( p->nCherrypick ){
    CARD_LETTER(Q);
    blob_append_char(b, '[');
    for( i = 0; i < p->nCherrypick; ++i ){
      if( i>0 ) blob_append_char(b, ',');
      blob_append_char(b, '{');
      blob_appendf(b, "\"type\": \"%c\"", p->aCherrypick[i].zCPTarget[0]);
      KVP_STR(1, target, &p->aCherrypick[i].zCPTarget[1]);
      KVP_STR(1, base, p->aCherrypick[i].zCPBase);
      blob_append_char(b, '}');
    }
    blob_append_char(b, ']');
  }
  CARD_STR2(R, p->zRepoCksum);
  if( p->nTag ){
    CARD_LETTER(T);
    blob_append_char(b, '[');
    for( int i = 0; i < p->nTag; ++i ){
      const char *zName = p->aTag[i].zName;
      if( i>0 ) blob_append_char(b, ',');
      blob_append_char(b, '{');
      blob_appendf(b, "\"type\": \"%c\"", *zName);
      KVP_STR(1, name, &zName[1]);
      KVP_STR(1, target, p->aTag[i].zUuid ? p->aTag[i].zUuid : "*")
        /* We could arguably resolve the "*" as null or p's uuid. */;
      KVP_STR(1, value, p->aTag[i].zValue);
      blob_append_char(b, '}');
    }
    blob_append_char(b, ']');
  }
  CARD_STR2(U, p->zUser);


  CARD_STR2(W, p->zWiki);


  blob_append_literal(b, "}");
#undef CARD_FMT
#undef CARD_LETTER
#undef CARD_STR
#undef CARD_STR2
#undef ISA
#undef KVP_STR







|




















|

|


|









|














|












|








>
>
|
|
>




>


|

>
















|


<
|
|
|
<











|














|









>
>
|
>
>







2925
2926
2927
2928
2929
2930
2931
2932
2933
2934
2935
2936
2937
2938
2939
2940
2941
2942
2943
2944
2945
2946
2947
2948
2949
2950
2951
2952
2953
2954
2955
2956
2957
2958
2959
2960
2961
2962
2963
2964
2965
2966
2967
2968
2969
2970
2971
2972
2973
2974
2975
2976
2977
2978
2979
2980
2981
2982
2983
2984
2985
2986
2987
2988
2989
2990
2991
2992
2993
2994
2995
2996
2997
2998
2999
3000
3001
3002
3003
3004
3005
3006
3007
3008
3009
3010
3011
3012
3013
3014
3015
3016
3017
3018
3019
3020
3021
3022
3023
3024
3025
3026
3027
3028
3029
3030
3031
3032
3033
3034
3035
3036
3037
3038

3039
3040
3041

3042
3043
3044
3045
3046
3047
3048
3049
3050
3051
3052
3053
3054
3055
3056
3057
3058
3059
3060
3061
3062
3063
3064
3065
3066
3067
3068
3069
3070
3071
3072
3073
3074
3075
3076
3077
3078
3079
3080
3081
3082
3083
3084
3085
3086
3087
3088
3089
  switch(typeId){
    case CFTYPE_MANIFEST: return "checkin";
    case CFTYPE_CLUSTER: return "cluster";
    case CFTYPE_CONTROL: return "tag";
    case CFTYPE_WIKI: return "wiki";
    case CFTYPE_TICKET: return "ticket";
    case CFTYPE_ATTACHMENT: return "attachment";
    case CFTYPE_EVENT: return "technote";
    case CFTYPE_FORUM: return "forumpost";
  }
  return NULL;
}

/*
** Creates a JSON representation of p, appending it to b.
**
** b is not cleared before rendering, so the caller needs to do that
** if it's important for their use case.
**
** Pedantic note: this routine traverses p->aFile directly, rather
** than using manifest_file_next(), so that delta manifests are
** rendered as-is instead of containing their derived F-cards. If that
** policy is ever changed, p will need to be non-const.
*/
void artifact_to_json(Manifest const *p, Blob *b){
  int i;

  blob_append_literal(b, "{");
  blob_appendf(b, "\"uuid\":\"%z\"", rid_to_uuid(p->rid));
  /*blob_appendf(b, ", \"rid\": %d", p->rid); not portable across repos*/
  blob_appendf(b, ",\"type\":%!j", artifact_type_to_name(p->type));
#define ISA(TYPE) if( p->type==TYPE )
#define CARD_LETTER(LETTER) \
  blob_append_literal(b, ",\"" #LETTER "\":")
#define CARD_STR(LETTER, VAL) \
  assert( VAL ); CARD_LETTER(LETTER); blob_appendf(b, "%!j", VAL)
#define CARD_STR2(LETTER, VAL) \
  if( VAL ) { CARD_STR(LETTER, VAL); } (void)0
#define STR_OR_NULL(VAL)                 \
  if( VAL ) blob_appendf(b, "%!j", VAL); \
  else blob_append(b, "null", 4)
#define KVP_STR(ADDCOMMA, KEY,VAL)  \
  if(ADDCOMMA) blob_append_char(b, ','); \
  blob_appendf(b, "%!j:", #KEY);   \
  STR_OR_NULL(VAL)

  ISA( CFTYPE_ATTACHMENT ){
    CARD_LETTER(A);
    blob_append_char(b, '{');
    KVP_STR(0, filename, p->zAttachName);
    KVP_STR(1, target, p->zAttachTarget);
    KVP_STR(1, source, p->zAttachSrc);
    blob_append_char(b, '}');
  }
  CARD_STR2(B, p->zBaseline);
  CARD_STR2(C, p->zComment);
  CARD_LETTER(D); blob_appendf(b, "%f", p->rDate);
  ISA( CFTYPE_EVENT ){
    blob_appendf(b, ", \"E\":{\"time\":%f,\"id\":%!j}",
                 p->rEventDate, p->zEventId);
  }
  ISA( CFTYPE_MANIFEST ){
    CARD_LETTER(F);
    blob_append_char(b, '[');
    for( i = 0; i < p->nFile; ++i ){
      ManifestFile const * const pF = &p->aFile[i];
      if( i>0 ) blob_append_char(b, ',');
      blob_append_char(b, '{');
      KVP_STR(0, name, pF->zName);
      KVP_STR(1, uuid, pF->zUuid);
      KVP_STR(1, perm, pF->zPerm);
      KVP_STR(1, rename, pF->zPrior);
      blob_append_char(b, '}');
    }
    /* Special case: model checkins with no F-card as having an empty
    ** array, rather than no F-cards, to hypothetically simplify
    ** handling in JSON queries. */
    blob_append_char(b, ']');
  }
  CARD_STR2(G, p->zThreadRoot);
  ISA( CFTYPE_FORUM ){
    CARD_LETTER(H);
    STR_OR_NULL( (p->zThreadTitle && *p->zThreadTitle) ? p->zThreadTitle : NULL);
    CARD_STR2(I, p->zInReplyTo);
  }
  if( p->nField ){
    CARD_LETTER(J);
    blob_append_char(b, '[');
    for( i = 0; i < p->nField; ++i ){
      const char * zName = p->aField[i].zName;
      if( i>0 ) blob_append_char(b, ',');
      blob_append_char(b, '{');
      KVP_STR(0, name, '+'==*zName ? &zName[1] : zName);
      KVP_STR(1, value, p->aField[i].zValue);
      blob_appendf(b, ",\"append\":%s", '+'==*zName ? "true" : "false");
      blob_append_char(b, '}');
    }
    blob_append_char(b, ']');
  }
  CARD_STR2(K, p->zTicketUuid);
  CARD_STR2(L, p->zWikiTitle);
  ISA( CFTYPE_CLUSTER ){
    CARD_LETTER(M);
    blob_append_char(b, '[');
    for( int i = 0; i < p->nCChild; ++i ){
      if( i>0 ) blob_append_char(b, ',');
      blob_appendf(b, "%!j", p->azCChild[i]);
    }
    blob_append_char(b, ']');
  }
  CARD_STR2(N, p->zMimetype);
  ISA( CFTYPE_MANIFEST || p->nParent>0 ){
    CARD_LETTER(P);
    blob_append_char(b, '[');

    for( i = 0; i < p->nParent; ++i ){
      if( i>0 ) blob_append_char(b, ',');
      blob_appendf(b, "%!j", p->azParent[i]);

    }
    /* Special case: model checkins with no P-card as having an empty
    ** array, as per F-cards. */
    blob_append_char(b, ']');
  }
  if( p->nCherrypick ){
    CARD_LETTER(Q);
    blob_append_char(b, '[');
    for( i = 0; i < p->nCherrypick; ++i ){
      if( i>0 ) blob_append_char(b, ',');
      blob_append_char(b, '{');
      blob_appendf(b, "\"type\":\"%c\"", p->aCherrypick[i].zCPTarget[0]);
      KVP_STR(1, target, &p->aCherrypick[i].zCPTarget[1]);
      KVP_STR(1, base, p->aCherrypick[i].zCPBase);
      blob_append_char(b, '}');
    }
    blob_append_char(b, ']');
  }
  CARD_STR2(R, p->zRepoCksum);
  if( p->nTag ){
    CARD_LETTER(T);
    blob_append_char(b, '[');
    for( int i = 0; i < p->nTag; ++i ){
      const char *zName = p->aTag[i].zName;
      if( i>0 ) blob_append_char(b, ',');
      blob_append_char(b, '{');
      blob_appendf(b, "\"type\":\"%c\"", *zName);
      KVP_STR(1, name, &zName[1]);
      KVP_STR(1, target, p->aTag[i].zUuid ? p->aTag[i].zUuid : "*")
        /* We could arguably resolve the "*" as null or p's uuid. */;
      KVP_STR(1, value, p->aTag[i].zValue);
      blob_append_char(b, '}');
    }
    blob_append_char(b, ']');
  }
  CARD_STR2(U, p->zUser);
  if( p->zWiki || CFTYPE_WIKI==p->type || CFTYPE_FORUM==p->type
      || CFTYPE_EVENT==p->type ){
    CARD_LETTER(W);
    STR_OR_NULL((p->zWiki && *p->zWiki) ? p->zWiki : NULL);
  }
  blob_append_literal(b, "}");
#undef CARD_FMT
#undef CARD_LETTER
#undef CARD_STR
#undef CARD_STR2
#undef ISA
#undef KVP_STR
3162
3163
3164
3165
3166
3167
3168
3169
3170
3171
3172
3173
3174
3175
3176
3177
3178
3179
3180
3181
3182
3183
3184
}



/*
** COMMAND: test-artifact-to-json
**
** Usage:  %fossil test-artifact-to-json ?-pretty? symbolic-name [...names]
**
** Tests the artifact_to_json() and artifact_to_json_by_name() APIs.
*/
void test_manifest_to_json(void){
  int i;
  Blob b = empty_blob;
  Stmt q;
  const int bPretty = find_option("pretty",0,0)!=0;
  int nErr = 0;

  db_find_and_open_repository(0,0);
  db_prepare(&q, "select json_pretty(:json)");
  for( i=2; i<g.argc; ++i ){
    char const *zName = g.argv[i];
    const int rc = artifact_to_json_by_name(zName, &b);







|







|







3169
3170
3171
3172
3173
3174
3175
3176
3177
3178
3179
3180
3181
3182
3183
3184
3185
3186
3187
3188
3189
3190
3191
}



/*
** COMMAND: test-artifact-to-json
**
** Usage:  %fossil test-artifact-to-json ?-pretty|-p? symbolic-name [...names]
**
** Tests the artifact_to_json() and artifact_to_json_by_name() APIs.
*/
void test_manifest_to_json(void){
  int i;
  Blob b = empty_blob;
  Stmt q;
  const int bPretty = find_option("pretty","p",0)!=0;
  int nErr = 0;

  db_find_and_open_repository(0,0);
  db_prepare(&q, "select json_pretty(:json)");
  for( i=2; i<g.argc; ++i ){
    char const *zName = g.argv[i];
    const int rc = artifact_to_json_by_name(zName, &b);
Changes to src/name.c.
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
  if( bRecent==0 || n!=250 ){
    style_submenu_element("Recent","bloblist?n=250&recent");
  }
  if( bUnclst==0 ){
    style_submenu_element("Unclustered","bloblist?unclustered");
  }
  if( g.perm.Admin ){
    style_submenu_element("Artifact Log", "rcvfromlist");
  }
  if( !phantomOnly ){
    style_submenu_element("Phantoms", "bloblist?phan");
  }
  style_submenu_element("Clusters","clusterlist");
  if( g.perm.Private || g.perm.Admin ){
    if( !privOnly ){







|







1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
  if( bRecent==0 || n!=250 ){
    style_submenu_element("Recent","bloblist?n=250&recent");
  }
  if( bUnclst==0 ){
    style_submenu_element("Unclustered","bloblist?unclustered");
  }
  if( g.perm.Admin ){
    style_submenu_element("Xfer Log", "rcvfromlist");
  }
  if( !phantomOnly ){
    style_submenu_element("Phantoms", "bloblist?phan");
  }
  style_submenu_element("Clusters","clusterlist");
  if( g.perm.Private || g.perm.Admin ){
    if( !privOnly ){
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
** generate extra network traffic on every sync request.
*/
void phantom_list_page(void){
  login_check_credentials();
  if( !g.perm.Read ){ login_needed(g.anon.Read); return; }
  style_header("Public Phantom Artifacts");
  if( g.perm.Admin ){
    style_submenu_element("Artifact Log", "rcvfromlist");
    style_submenu_element("Artifact List", "bloblist");
  }
  if( g.perm.Write ){
    style_submenu_element("Artifact Stats", "artifact_stats");
  }
  table_of_public_phantoms();
  style_finish_page();







|







2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
** generate extra network traffic on every sync request.
*/
void phantom_list_page(void){
  login_check_credentials();
  if( !g.perm.Read ){ login_needed(g.anon.Read); return; }
  style_header("Public Phantom Artifacts");
  if( g.perm.Admin ){
    style_submenu_element("Xfer Log", "rcvfromlist");
    style_submenu_element("Artifact List", "bloblist");
  }
  if( g.perm.Write ){
    style_submenu_element("Artifact Stats", "artifact_stats");
  }
  table_of_public_phantoms();
  style_finish_page();
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
void bigbloblist_page(void){
  Stmt q;
  int n = atoi(PD("n","250"));

  login_check_credentials();
  if( !g.perm.Read ){ login_needed(g.anon.Read); return; }
  if( g.perm.Admin ){
    style_submenu_element("Artifact Log", "rcvfromlist");
  }
  if( g.perm.Write ){
    style_submenu_element("Artifact Stats", "artifact_stats");
  }
  style_submenu_element("All Artifacts", "bloblist");
  style_header("%d Largest Artifacts", n);
  db_multi_exec(







|







2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
void bigbloblist_page(void){
  Stmt q;
  int n = atoi(PD("n","250"));

  login_check_credentials();
  if( !g.perm.Read ){ login_needed(g.anon.Read); return; }
  if( g.perm.Admin ){
    style_submenu_element("Xfer Log", "rcvfromlist");
  }
  if( g.perm.Write ){
    style_submenu_element("Artifact Stats", "artifact_stats");
  }
  style_submenu_element("All Artifacts", "bloblist");
  style_header("%d Largest Artifacts", n);
  db_multi_exec(
2201
2202
2203
2204
2205
2206
2207
2208







2209
2210





2211














































2212

2213
2214
2215
2216
2217
2218
2219
}

/*
** COMMAND: test-phantoms
**
** Usage: %fossil test-phantoms
**
** Show all phantom artifacts







*/
void test_phatoms_cmd(void){





  db_find_and_open_repository(0,0);














































  describe_artifacts_to_stdout("IN (SELECT rid FROM blob WHERE size<0)", 0);

}

/* Maximum number of collision examples to remember */
#define MAX_COLLIDE 25

/*
** Generate a report on the number of collisions in artifact hashes







|
>
>
>
>
>
>
>


>
>
>
>
>

>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
|
>







2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
2236
2237
2238
2239
2240
2241
2242
2243
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
}

/*
** COMMAND: test-phantoms
**
** Usage: %fossil test-phantoms
**
** Show all phantom artifacts.  A phantom artifact is one for which there
** is no content. Options:
**
**   --count            Show only a count of the number of phantoms.
**   --delta            Show all delta-phantoms.  A delta-phantom is a
**                      artifact for which there is a delta but the delta
**                      source is a phantom.
**   --list             Just list the phantoms.  Do not try to describe them.
*/
void test_phatoms_cmd(void){
  int bDelta;
  int bList;
  int bCount;
  unsigned nPhantom = 0;
  unsigned nDeltaPhantom = 0;
  db_find_and_open_repository(0,0);
  bDelta = find_option("delta", 0, 0)!=0;
  bList = find_option("list", 0, 0)!=0;
  bCount = find_option("count", 0, 0)!=0;
  verify_all_options();
  if( bList || bCount ){
    Stmt q1, q2;
    db_prepare(&q1, "SELECT rid, uuid FROM blob WHERE size<0");
    while( db_step(&q1)==SQLITE_ROW ){
      int rid = db_column_int(&q1, 0);
      nPhantom++;
      if( !bCount ){
        fossil_print("%S (%d)\n", db_column_text(&q1,1), rid);
      }
      db_prepare(&q2,
        "WITH RECURSIVE deltasof(rid) AS ("
        "  SELECT rid FROM delta WHERE srcid=%d"
        "  UNION"
        "  SELECT delta.rid FROM deltasof, delta"
        "    WHERE delta.srcid=deltasof.rid)"
        "SELECT deltasof.rid, blob.uuid FROM deltasof LEFT JOIN blob"
        "    ON blob.rid=deltasof.rid", rid
      );
      while( db_step(&q2)==SQLITE_ROW ){
        nDeltaPhantom++;
        if( !bCount ){
          fossil_print("   %S (%d)\n", db_column_text(&q2,1),
                                       db_column_int(&q2,0));
        }
      }
      db_finalize(&q2);
    }
    db_finalize(&q1);
    if( nPhantom ){
      fossil_print("Phantoms: %u    Delta-phantoms: %u\n",
                    nPhantom, nDeltaPhantom);
    }
  }else if( bDelta ){
    describe_artifacts_to_stdout(
       "IN (WITH RECURSIVE delta_phantom(rid) AS (\n"
       "      SELECT delta.rid FROM blob, delta\n"
       "       WHERE blob.size<0 AND delta.srcid=blob.rid\n"
       "      UNION\n"
       "      SELECT delta.rid FROM delta_phantom, delta\n"
       "       WHERE delta.srcid=delta_phantom.rid)\n"
       "    SELECT rid FROM delta_phantom)", 0);
  }else{
    describe_artifacts_to_stdout("IN (SELECT rid FROM blob WHERE size<0)", 0);
  }
}

/* Maximum number of collision examples to remember */
#define MAX_COLLIDE 25

/*
** Generate a report on the number of collisions in artifact hashes
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
2320
2321
2322
2323
  sqlite3_int64 szTotal = 0;
  sqlite3_int64 szCTotal = 0;
  login_check_credentials();
  if( !g.perm.Read ){ login_needed(g.anon.Read); return; }
  style_header("All Cluster Artifacts");
  style_submenu_element("All Artifactst", "bloblist");
  if( g.perm.Admin ){
    style_submenu_element("Artifact Log", "rcvfromlist");
  }
  style_submenu_element("Phantoms", "bloblist?phan");
  if( g.perm.Write ){
    style_submenu_element("Artifact Stats", "artifact_stats");
  }

  db_prepare(&q,







|







2368
2369
2370
2371
2372
2373
2374
2375
2376
2377
2378
2379
2380
2381
2382
  sqlite3_int64 szTotal = 0;
  sqlite3_int64 szCTotal = 0;
  login_check_credentials();
  if( !g.perm.Read ){ login_needed(g.anon.Read); return; }
  style_header("All Cluster Artifacts");
  style_submenu_element("All Artifactst", "bloblist");
  if( g.perm.Admin ){
    style_submenu_element("Xfer Log", "rcvfromlist");
  }
  style_submenu_element("Phantoms", "bloblist?phan");
  if( g.perm.Write ){
    style_submenu_element("Artifact Stats", "artifact_stats");
  }

  db_prepare(&q,
Changes to src/printf.c.
1075
1076
1077
1078
1079
1080
1081

1082
1083
1084
1085
1086
1087
1088
void fossil_errorlog(const char *zFormat, ...){
  struct tm *pNow;
  time_t now;
  FILE *out;
  const char *z;
  int i;
  int bDetail = 0;

  va_list ap;
  static const char *const azEnv[] = { "HTTP_HOST", "HTTP_REFERER",
      "HTTP_USER_AGENT",
      "PATH_INFO", "QUERY_STRING", "REMOTE_ADDR", "REQUEST_METHOD",
      "REQUEST_URI", "SCRIPT_NAME" };
  if( g.zErrlog==0 ) return;
  if( g.zErrlog[0]=='-' && g.zErrlog[1]==0 ){







>







1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
void fossil_errorlog(const char *zFormat, ...){
  struct tm *pNow;
  time_t now;
  FILE *out;
  const char *z;
  int i;
  int bDetail = 0;
  int bBrief = 0;
  va_list ap;
  static const char *const azEnv[] = { "HTTP_HOST", "HTTP_REFERER",
      "HTTP_USER_AGENT",
      "PATH_INFO", "QUERY_STRING", "REMOTE_ADDR", "REQUEST_METHOD",
      "REQUEST_URI", "SCRIPT_NAME" };
  if( g.zErrlog==0 ) return;
  if( g.zErrlog[0]=='-' && g.zErrlog[1]==0 ){
1096
1097
1098
1099
1100
1101
1102


1103
1104
1105
1106
1107


1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
  fprintf(out, "------------- %04d-%02d-%02d %02d:%02d:%02d UTC ------------\n",
          pNow->tm_year+1900, pNow->tm_mon+1, pNow->tm_mday,
          pNow->tm_hour, pNow->tm_min, pNow->tm_sec);
  va_start(ap, zFormat);
  if( zFormat[0]=='X' ){
    bDetail = 1;
    zFormat++;


  }
  vfprintf(out, zFormat, ap);
  fprintf(out, "\n");
  va_end(ap);
  if( g.zPhase!=0 ) fprintf(out, "while in %s\n", g.zPhase);


  if( bDetail ){
    cgi_print_all(1,3,out);
  }else{
    for(i=0; i<count(azEnv); i++){
      char *p;
      if( (p = fossil_getenv(azEnv[i]))!=0 && p[0]!=0 ){
        fprintf(out, "%s=%s\n", azEnv[i], p);
        fossil_path_free(p);
      }else if( (z = P(azEnv[i]))!=0 && z[0]!=0 ){
        fprintf(out, "%s=%s\n", azEnv[i], z);
      }
    }
  }
  fclose(out);
}

/*
** The following variable becomes true while processing a fatal error
** or a panic.  If additional "recursive-fatal" errors occur while
** shutting down, the recursive errors are silently ignored.
*/







>
>


|


>
>
|












|







1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
  fprintf(out, "------------- %04d-%02d-%02d %02d:%02d:%02d UTC ------------\n",
          pNow->tm_year+1900, pNow->tm_mon+1, pNow->tm_mday,
          pNow->tm_hour, pNow->tm_min, pNow->tm_sec);
  va_start(ap, zFormat);
  if( zFormat[0]=='X' ){
    bDetail = 1;
    zFormat++;
  }else if( strncmp(zFormat,"SMTP:",5)==0 ){
    bBrief = 1;
  }
  vfprintf(out, zFormat, ap);
  fprintf(out, " (pid %d)\n", (int)getpid());
  va_end(ap);
  if( g.zPhase!=0 ) fprintf(out, "while in %s\n", g.zPhase);
  if( bBrief ){
    /* Say nothing more */
  }else if( bDetail ){
    cgi_print_all(1,3,out);
  }else{
    for(i=0; i<count(azEnv); i++){
      char *p;
      if( (p = fossil_getenv(azEnv[i]))!=0 && p[0]!=0 ){
        fprintf(out, "%s=%s\n", azEnv[i], p);
        fossil_path_free(p);
      }else if( (z = P(azEnv[i]))!=0 && z[0]!=0 ){
        fprintf(out, "%s=%s\n", azEnv[i], z);
      }
    }
  }
  if( out!=stderr ) fclose(out);
}

/*
** The following variable becomes true while processing a fatal error
** or a panic.  If additional "recursive-fatal" errors occur while
** shutting down, the recursive errors are silently ignored.
*/
Changes to src/repolist.c.
29
30
31
32
33
34
35

36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

54
55
56
57
58
59
60
struct RepoInfo {
  char *zRepoName;      /* Name of the repository file */
  int isValid;          /* True if zRepoName is a valid Fossil repository */
  int isRepolistSkin;   /* 1 or 2 if this repository wants to be the skin
                        ** for the repository list.  2 means do use this
                        ** repository but do not display it in the list. */
  char *zProjName;      /* Project Name.  Memory from fossil_malloc() */

  char *zLoginGroup;    /* Name of login group, or NULL.  Malloced() */
  double rMTime;        /* Last update.  Julian day number */
};
#endif

/*
** Discover information about the repository given by
** pRepo->zRepoName.  The discovered information is stored in other
** fields of the RepoInfo object.
*/
static void remote_repo_info(RepoInfo *pRepo){
  sqlite3 *db;
  sqlite3_stmt *pStmt;
  int rc;

  pRepo->isRepolistSkin = 0;
  pRepo->isValid = 0;
  pRepo->zProjName = 0;

  pRepo->zLoginGroup = 0;
  pRepo->rMTime = 0.0;

  g.dbIgnoreErrors++;
  rc = sqlite3_open_v2(pRepo->zRepoName, &db, SQLITE_OPEN_READWRITE, 0);
  if( rc ) goto finish_repo_list;
  rc = sqlite3_prepare_v2(db, "SELECT value FROM config"







>


















>







29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
struct RepoInfo {
  char *zRepoName;      /* Name of the repository file */
  int isValid;          /* True if zRepoName is a valid Fossil repository */
  int isRepolistSkin;   /* 1 or 2 if this repository wants to be the skin
                        ** for the repository list.  2 means do use this
                        ** repository but do not display it in the list. */
  char *zProjName;      /* Project Name.  Memory from fossil_malloc() */
  char *zProjDesc;      /* Project Description.  Memory from fossil_malloc() */
  char *zLoginGroup;    /* Name of login group, or NULL.  Malloced() */
  double rMTime;        /* Last update.  Julian day number */
};
#endif

/*
** Discover information about the repository given by
** pRepo->zRepoName.  The discovered information is stored in other
** fields of the RepoInfo object.
*/
static void remote_repo_info(RepoInfo *pRepo){
  sqlite3 *db;
  sqlite3_stmt *pStmt;
  int rc;

  pRepo->isRepolistSkin = 0;
  pRepo->isValid = 0;
  pRepo->zProjName = 0;
  pRepo->zProjDesc = 0;
  pRepo->zLoginGroup = 0;
  pRepo->rMTime = 0.0;

  g.dbIgnoreErrors++;
  rc = sqlite3_open_v2(pRepo->zRepoName, &db, SQLITE_OPEN_READWRITE, 0);
  if( rc ) goto finish_repo_list;
  rc = sqlite3_prepare_v2(db, "SELECT value FROM config"
69
70
71
72
73
74
75









76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93















94
95
96
97
98
99
100
  rc = sqlite3_prepare_v2(db, "SELECT value FROM config"
                              " WHERE name='project-name'",
                          -1, &pStmt, 0);
  if( rc ) goto finish_repo_list;
  if( sqlite3_step(pStmt)==SQLITE_ROW ){
    pRepo->zProjName = fossil_strdup((char*)sqlite3_column_text(pStmt,0));
  }









  sqlite3_finalize(pStmt);
  rc = sqlite3_prepare_v2(db, "SELECT value FROM config"
                              " WHERE name='login-group-name'",
                          -1, &pStmt, 0);
  if( rc==SQLITE_OK && sqlite3_step(pStmt)==SQLITE_ROW ){
    pRepo->zLoginGroup = fossil_strdup((char*)sqlite3_column_text(pStmt,0));
  }
  sqlite3_finalize(pStmt);
  rc = sqlite3_prepare_v2(db, "SELECT max(mtime) FROM event", -1, &pStmt, 0);
  if( rc==SQLITE_OK && sqlite3_step(pStmt)==SQLITE_ROW ){
    pRepo->rMTime = sqlite3_column_double(pStmt,0);
  }
  pRepo->isValid = 1;
  sqlite3_finalize(pStmt);
finish_repo_list:
  g.dbIgnoreErrors--;
  sqlite3_close(db);
}
















/*
** Generate a web-page that lists all repositories located under the
** g.zRepositoryName directory and return non-zero.
**
** For the special case when g.zRepositoryName is a non-chroot-jail "/",
** compose the list using the "repo:" entries in the global_config







>
>
>
>
>
>
>
>
>


















>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
  rc = sqlite3_prepare_v2(db, "SELECT value FROM config"
                              " WHERE name='project-name'",
                          -1, &pStmt, 0);
  if( rc ) goto finish_repo_list;
  if( sqlite3_step(pStmt)==SQLITE_ROW ){
    pRepo->zProjName = fossil_strdup((char*)sqlite3_column_text(pStmt,0));
  }
  sqlite3_finalize(pStmt);
  if( rc ) goto finish_repo_list;
  rc = sqlite3_prepare_v2(db, "SELECT value FROM config"
                              " WHERE name='project-description'",
                          -1, &pStmt, 0);
  if( rc ) goto finish_repo_list;
  if( sqlite3_step(pStmt)==SQLITE_ROW ){
    pRepo->zProjDesc = fossil_strdup((char*)sqlite3_column_text(pStmt,0));
  }
  sqlite3_finalize(pStmt);
  rc = sqlite3_prepare_v2(db, "SELECT value FROM config"
                              " WHERE name='login-group-name'",
                          -1, &pStmt, 0);
  if( rc==SQLITE_OK && sqlite3_step(pStmt)==SQLITE_ROW ){
    pRepo->zLoginGroup = fossil_strdup((char*)sqlite3_column_text(pStmt,0));
  }
  sqlite3_finalize(pStmt);
  rc = sqlite3_prepare_v2(db, "SELECT max(mtime) FROM event", -1, &pStmt, 0);
  if( rc==SQLITE_OK && sqlite3_step(pStmt)==SQLITE_ROW ){
    pRepo->rMTime = sqlite3_column_double(pStmt,0);
  }
  pRepo->isValid = 1;
  sqlite3_finalize(pStmt);
finish_repo_list:
  g.dbIgnoreErrors--;
  sqlite3_close(db);
}

/*
** SETTING: show-repolist-desc                  boolean default=off
**
** If the value of this setting is "1" globally, then the repository-list
** page will show the description of each repository.  This setting only
** has effect when it is in the global setting database.
*/
/*
** SETTING: show-repolist-lg                    boolean default=off
**
** If the value of this setting is "1" globally, then the repository-list
** page will show the login-group for each repository.  This setting only
** has effect when it is in the global setting database.
*/

/*
** Generate a web-page that lists all repositories located under the
** g.zRepositoryName directory and return non-zero.
**
** For the special case when g.zRepositoryName is a non-chroot-jail "/",
** compose the list using the "repo:" entries in the global_config
114
115
116
117
118
119
120



121
122












123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144


145
146
147
148
149
150
151
  int n = 0;           /* Number of repositories found */
  int allRepo;         /* True if running "fossil ui all".
                       ** False if a directory scan of base for repos */
  Blob html;           /* Html for the body of the repository list */
  char *zSkinRepo = 0; /* Name of the repository database used for skins */
  char *zSkinUrl = 0;  /* URL for the skin database */
  int quickfilter = 0; /* Is quickfilter is enabled? */




  assert( g.db==0 );












  blob_init(&html, 0, 0);
  if( fossil_strcmp(g.zRepositoryName,"/")==0 && !g.fJail ){
    /* For the special case of the "repository directory" being "/",
    ** show all of the repositories named in the ~/.fossil database.
    **
    ** On unix systems, then entries are of the form "repo:/home/..."
    ** and on Windows systems they are like on unix, starting with a "/"
    ** or they can begin with a drive letter: "repo:C:/Users/...".  In either
    ** case, we want returned path to omit any initial "/".
    */
    db_open_config(1, 0);
    db_multi_exec(
       "CREATE TEMP VIEW sfile AS"
       "  SELECT ltrim(substr(name,6),'/') AS 'pathname' FROM global_config"
       "   WHERE name GLOB 'repo:*'"
    );
    allRepo = 1;
  }else{
    /* The default case:  All repositories under the g.zRepositoryName
    ** directory.
    */
    blob_init(&base, g.zRepositoryName, -1);


    sqlite3_open(":memory:", &g.db);
    db_multi_exec("CREATE TABLE sfile(pathname TEXT);");
    db_multi_exec("CREATE TABLE vfile(pathname);");
    vfile_scan(&base, blob_size(&base), 0, 0, 0, ExtFILE);
    db_multi_exec("DELETE FROM sfile WHERE pathname NOT GLOB '*[^/].fossil'"
#if USE_SEE
                  " AND pathname NOT GLOB '*[^/].efossil'"







>
>
>


>
>
>
>
>
>
>
>
>
>
>
>










<











>
>







140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173

174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
  int n = 0;           /* Number of repositories found */
  int allRepo;         /* True if running "fossil ui all".
                       ** False if a directory scan of base for repos */
  Blob html;           /* Html for the body of the repository list */
  char *zSkinRepo = 0; /* Name of the repository database used for skins */
  char *zSkinUrl = 0;  /* URL for the skin database */
  int quickfilter = 0; /* Is quickfilter is enabled? */
  const char *zShow;   /* Value of FOSSIL_REPOLIST_SHOW environment variable */
  int bShowDesc = 0;   /* True to show the description column */
  int bShowLg = 0;     /* True to show the login-group column */

  assert( g.db==0 );
  zShow = P("FOSSIL_REPOLIST_SHOW");
  if( zShow ){
    bShowDesc = strstr(zShow,"description")!=0;
    bShowLg = strstr(zShow,"login-group")!=0;
  }else if( db_open_config(1, 1)
     && db_table_exists("configdb", "global_config")
  ){
    bShowDesc = db_int(bShowDesc, "SELECT value FROM global_config"
                                  " WHERE name='show-repolist-desc'");
    bShowLg = db_int(bShowLg, "SELECT value FROM global_config"
                              " WHERE name='show-repolist-lg'");
  }
  blob_init(&html, 0, 0);
  if( fossil_strcmp(g.zRepositoryName,"/")==0 && !g.fJail ){
    /* For the special case of the "repository directory" being "/",
    ** show all of the repositories named in the ~/.fossil database.
    **
    ** On unix systems, then entries are of the form "repo:/home/..."
    ** and on Windows systems they are like on unix, starting with a "/"
    ** or they can begin with a drive letter: "repo:C:/Users/...".  In either
    ** case, we want returned path to omit any initial "/".
    */

    db_multi_exec(
       "CREATE TEMP VIEW sfile AS"
       "  SELECT ltrim(substr(name,6),'/') AS 'pathname' FROM global_config"
       "   WHERE name GLOB 'repo:*'"
    );
    allRepo = 1;
  }else{
    /* The default case:  All repositories under the g.zRepositoryName
    ** directory.
    */
    blob_init(&base, g.zRepositoryName, -1);
    db_close(0);
    assert( g.db==0 );
    sqlite3_open(":memory:", &g.db);
    db_multi_exec("CREATE TABLE sfile(pathname TEXT);");
    db_multi_exec("CREATE TABLE vfile(pathname);");
    vfile_scan(&base, blob_size(&base), 0, 0, 0, ExtFILE);
    db_multi_exec("DELETE FROM sfile WHERE pathname NOT GLOB '*[^/].fossil'"
#if USE_SEE
                  " AND pathname NOT GLOB '*[^/].efossil'"
159
160
161
162
163
164
165

















166
167
168
169
170









171




172


173
174
175
176
177
178
179
180
    g.db = 0;
    g.repositoryOpen = 0;
    g.localOpen = 0;
    return 0;
  }else{
    Stmt q;
    double rNow;

















    blob_append_sql(&html,
      "<table border='0' class='sortable filterlist' data-init-sort='1'"
      " data-column-types='txtxkxt'><thead>\n"
      "<tr><th>Filename<th width='20'>"
      "<th>Project Name<th width='20'>"









      "<th>Last Modified<th width='20'>"




      "<th>Login Group</tr>\n"


      "</thead><tbody>\n");
    db_prepare(&q, "SELECT pathname"
                   " FROM sfile ORDER BY pathname COLLATE nocase;");
    rNow = db_double(0, "SELECT julianday('now')");
    while( db_step(&q)==SQLITE_ROW ){
      const char *zName = db_column_text(&q, 0);
      int nName = (int)strlen(zName);
      int nSuffix = 7; /* ".fossil" */







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
|

|
|
|
>
>
>
>
>
>
>
>
>
|
>
>
>
>
|
>
>
|







201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
    g.db = 0;
    g.repositoryOpen = 0;
    g.localOpen = 0;
    return 0;
  }else{
    Stmt q;
    double rNow;
    char zType[16];   /* Column type letters for class "sortable" */
    int nType;
    zType[0] = 't';  /* Repo name */
    zType[1] = 'x';  /* Space between repo-name and project-name */
    zType[2] = 't';  /* Project name */
    nType = 3;
    if( bShowDesc ){
      zType[nType++] = 'x';  /* Space between name and description */
      zType[nType++] = 't';  /* Project description */
    }
    zType[nType++] = 'x';    /* space before age */
    zType[nType++] = 'k';    /* Project age */
    if( bShowLg ){
      zType[nType++] = 'x';  /* space before login-group */
      zType[nType++] = 't';  /* Login Group */
    }
    zType[nType] = 0;
    blob_appendf(&html,
      "<table border='0' class='sortable filterlist' data-init-sort='1'"
      " data-column-types='%s' cellspacing='0' cellpadding='0'><thead>\n"
      "<tr><th>Filename</th><th>&emsp;</th>\n"
      "<th%s><nobr>Project Name</nobr></th>\n",
      zType, (bShowDesc ? " width='25%'" : ""));
    if( bShowDesc ){
      blob_appendf(&html,
        "<th>&emsp;</th>\n"
        "<th width='25%%'><nobr>Project Description</nobr></th>\n"
      );
    }
    blob_appendf(&html,
      "<th>&emsp;</th>"
      "<th><nobr>Last Modified</nobr></th>\n"
    );
    if( bShowLg ){
      blob_appendf(&html,
        "<th>&emsp;</th>"
        "<th><nobr>Login Group</nobr></th></tr>\n"
      );
    }
    blob_appendf(&html,"</thead><tbody>\n");
    db_prepare(&q, "SELECT pathname"
                   " FROM sfile ORDER BY pathname COLLATE nocase;");
    rNow = db_double(0, "SELECT julianday('now')");
    while( db_step(&q)==SQLITE_ROW ){
      const char *zName = db_column_text(&q, 0);
      int nName = (int)strlen(zName);
      int nSuffix = 7; /* ".fossil" */
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280

281

282
283
284
285
286









287
288

289
290


291


292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
      iAge = (sqlite3_int64)((rNow - x.rMTime)*86400);
      zAge = human_readable_age(rNow - x.rMTime);
      if( x.rMTime==0.0 ){
        /* This repository has no entry in the "event" table.
        ** Its age will still be maximum, so data-sortkey will work. */
        zAge = mprintf("unknown");
      }
      blob_append_sql(&html, "<tr><td valign='top'>");
      if( !file_ends_with_repository_extension(zName,0) ){
        /* The "fossil server DIRECTORY" and "fossil ui DIRECTORY" commands
        ** do not work for repositories whose names do not end in ".fossil".
        ** So do not hyperlink those cases. */
        blob_append_sql(&html,"%h",zName);
      } else if( sqlite3_strglob("*/.*", zName)==0 ){
        /* Do not show hyperlinks for hidden repos */
        blob_append_sql(&html, "%h (hidden)", zName);
      } else if( allRepo && sqlite3_strglob("[a-zA-Z]:/?*", zName)!=0 ){
        blob_append_sql(&html,
          "<a href='%R/%T/home' target='_blank'>/%h</a>\n",
          zUrl, zName);
      }else if( file_ends_with_repository_extension(zName,1) ){
        /* As described in
        ** https://fossil-scm.org/forum/info/f50f647c97c72fc1: if
        ** foo.fossil and foo/bar.fossil both exist and we create a
        ** link to foo/bar/... then the URI dispatcher will instead
        ** see that as a link to foo.fossil. In such cases, do not
        ** emit a link to foo/bar.fossil. */
        char * zDirPart = file_dirname(zName);
        if( db_exists("SELECT 1 FROM sfile "
                      "WHERE pathname=(%Q || '.fossil') COLLATE nocase"
#if USE_SEE
                      "  OR pathname=(%Q || '.efossil') COLLATE nocase"
#endif
                      , zDirPart
#if USE_SEE
                      , zDirPart
#endif
        ) ){
          blob_append_sql(&html,
            "<s>%h</s> (directory/repo name collision)\n",
            zName);
        }else{
          blob_append_sql(&html,
            "<a href='%R/%T/home' target='_blank'>%h</a>\n",
            zUrl, zName);
        }
        fossil_free(zDirPart);
      }else{
        blob_append_sql(&html,
          "<a href='%R/%T/home' target='_blank'>%h</a>\n",
          zUrl, zName);
      }

      if( x.zProjName ){

        blob_append_sql(&html, "<td></td><td>%h</td>\n", x.zProjName);
        fossil_free(x.zProjName);
      }else{
        blob_append_sql(&html, "<td></td><td></td>\n");
      }









      blob_append_sql(&html,
        "<td></td><td data-sortkey='%08x'>%h</td>\n",

        (int)iAge, zAge);
      fossil_free(zAge);


      if( x.zLoginGroup ){


        blob_append_sql(&html, "<td></td><td>%h</td></tr>\n", x.zLoginGroup);
        fossil_free(x.zLoginGroup);
      }else{
        blob_append_sql(&html, "<td></td><td></td></tr>\n");
      }
      sqlite3_free(zUrl);
    }
    db_finalize(&q);
    blob_append_sql(&html,"</tbody></table>\n");
  }
  if( zSkinRepo ){
    char *zNewBase = mprintf("%s/%s", g.zBaseURL, zSkinUrl);
    g.zBaseURL = 0;
    set_base_url(zNewBase);
    db_open_repository(zSkinRepo);
    fossil_free(zSkinRepo);







|




|


|

|




















|



|





|



>

>
|


|

>
>
>
>
>
>
>
>
>
|
|
>


>
>
|
>
>
|


|




|







303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
      iAge = (sqlite3_int64)((rNow - x.rMTime)*86400);
      zAge = human_readable_age(rNow - x.rMTime);
      if( x.rMTime==0.0 ){
        /* This repository has no entry in the "event" table.
        ** Its age will still be maximum, so data-sortkey will work. */
        zAge = mprintf("unknown");
      }
      blob_appendf(&html, "<tr><td valign='top'><nobr>");
      if( !file_ends_with_repository_extension(zName,0) ){
        /* The "fossil server DIRECTORY" and "fossil ui DIRECTORY" commands
        ** do not work for repositories whose names do not end in ".fossil".
        ** So do not hyperlink those cases. */
        blob_appendf(&html,"%h",zName);
      } else if( sqlite3_strglob("*/.*", zName)==0 ){
        /* Do not show hyperlinks for hidden repos */
        blob_appendf(&html, "%h (hidden)", zName);
      } else if( allRepo && sqlite3_strglob("[a-zA-Z]:/?*", zName)!=0 ){
        blob_appendf(&html,
          "<a href='%R/%T/home' target='_blank'>/%h</a>\n",
          zUrl, zName);
      }else if( file_ends_with_repository_extension(zName,1) ){
        /* As described in
        ** https://fossil-scm.org/forum/info/f50f647c97c72fc1: if
        ** foo.fossil and foo/bar.fossil both exist and we create a
        ** link to foo/bar/... then the URI dispatcher will instead
        ** see that as a link to foo.fossil. In such cases, do not
        ** emit a link to foo/bar.fossil. */
        char * zDirPart = file_dirname(zName);
        if( db_exists("SELECT 1 FROM sfile "
                      "WHERE pathname=(%Q || '.fossil') COLLATE nocase"
#if USE_SEE
                      "  OR pathname=(%Q || '.efossil') COLLATE nocase"
#endif
                      , zDirPart
#if USE_SEE
                      , zDirPart
#endif
        ) ){
          blob_appendf(&html,
            "<s>%h</s> (directory/repo name collision)\n",
            zName);
        }else{
          blob_appendf(&html,
            "<a href='%R/%T/home' target='_blank'>%h</a>\n",
            zUrl, zName);
        }
        fossil_free(zDirPart);
      }else{
        blob_appendf(&html,
          "<a href='%R/%T/home' target='_blank'>%h</a>\n",
          zUrl, zName);
      }
      blob_appendf(&html,"</nobr></td>\n");
      if( x.zProjName ){
        blob_appendf(&html, "<td>&emsp;</td><td valign='top'>%h</td>\n",
                      x.zProjName);
        fossil_free(x.zProjName);
      }else{
        blob_appendf(&html, "<td>&emsp;</td><td></td>\n");
      }
      if( !bShowDesc ){
        /* Do nothing */
      }else if( x.zProjDesc ){
        blob_appendf(&html, "<td>&emsp;</td><td valign='top'>%h</td>\n",
                        x.zProjDesc);
        fossil_free(x.zProjDesc);
      }else{
        blob_appendf(&html, "<td>&emsp;</td><td></td>\n");
      }
      blob_appendf(&html,
        "<td>&emsp;</td><td data-sortkey='%08x' align='center' valign='top'>"
        "<nobr>%h</nobr></td>\n",
        (int)iAge, zAge);
      fossil_free(zAge);
      if( !bShowLg ){
        blob_appendf(&html, "</tr>\n");
      }else if( x.zLoginGroup ){
        blob_appendf(&html, "<td>&emsp;</td><td valign='top'>"
                               "<nobr>%h</nobr></td></tr>\n",
                        x.zLoginGroup);
        fossil_free(x.zLoginGroup);
      }else{
        blob_appendf(&html, "<td>&emsp;</td><td></td></tr>\n");
      }
      sqlite3_free(zUrl);
    }
    db_finalize(&q);
    blob_appendf(&html,"</tbody></table>\n");
  }
  if( zSkinRepo ){
    char *zNewBase = mprintf("%s/%s", g.zBaseURL, zSkinUrl);
    g.zBaseURL = 0;
    set_base_url(zNewBase);
    db_open_repository(zSkinRepo);
    fossil_free(zSkinRepo);
Changes to src/report.c.
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
      rn = 0;
      zTitle = mprintf("Copy Of %s", zTitle);
      zOwner = g.zLogin;
    }
  }
  if( zOwner==0 ) zOwner = g.zLogin;
  style_submenu_element("Cancel", "%R/reportlist");
  if( rn>0 ){
    style_submenu_element("Delete", "%R/rptedit/%d?del1=1", rn);
  }
  style_header("%s", rn>0 ? "Edit Report Format":"Create New Report Format");
  if( zErr ){
    @ <blockquote class="reportError">%h(zErr)</blockquote>
  }
  @ <form action="rptedit" method="post"><div>
  @ <input type="hidden" name="rn" value="%d(rn)">
  @ <p>Report Title:<br>







<
<
<







586
587
588
589
590
591
592



593
594
595
596
597
598
599
      rn = 0;
      zTitle = mprintf("Copy Of %s", zTitle);
      zOwner = g.zLogin;
    }
  }
  if( zOwner==0 ) zOwner = g.zLogin;
  style_submenu_element("Cancel", "%R/reportlist");



  style_header("%s", rn>0 ? "Edit Report Format":"Create New Report Format");
  if( zErr ){
    @ <blockquote class="reportError">%h(zErr)</blockquote>
  }
  @ <form action="rptedit" method="post"><div>
  @ <input type="hidden" name="rn" value="%d(rn)">
  @ <p>Report Title:<br>
904
905
906
907
908
909
910
911

912
913
914
915
916
917
918
  }
  ++pState->nCount;

  /* Output the separator above each entry in a table which has multiple lines
  ** per database entry.
  */
  if( pState->iNewRow>=0 ){
    @ <tr><td colspan=%d(pState->nCol)><font size=1>&nbsp;</font></td></tr>

  }

  /* Output the data for this entry from the database
  */
  zBg = pState->iBg>=0 ? azArg[pState->iBg] : 0;
  if( zBg==0 ) zBg = "white";
  @ <tr style="background-color:%h(zBg)">







|
>







901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
  }
  ++pState->nCount;

  /* Output the separator above each entry in a table which has multiple lines
  ** per database entry.
  */
  if( pState->iNewRow>=0 ){
    @ <tr><td colspan="%d(pState->nCol)" style="padding:0px">
    @ <hr style="margin:0px"></td></tr>
  }

  /* Output the data for this entry from the database
  */
  zBg = pState->iBg>=0 ? azArg[pState->iBg] : 0;
  if( zBg==0 ) zBg = "white";
  @ <tr style="background-color:%h(zBg)">
Changes to src/search.c.
616
617
618
619
620
621
622

623
624
625
626
627
628
629
  const char *zScope = 0;
  const char *zWidth = find_option("width","W",1);
  int bDebug = find_option("debug",0,0)!=0;     /* Undocumented */
  int nLimit = zLimit ? atoi(zLimit) : -1000;
  int width;
  int nTty = 0;          /* VT100 highlight color for matching text */
  const char *zHighlight = 0;


  nTty =  terminal_is_vt100();

  /* Undocumented option to change highlight color */
  zHighlight = find_option("highlight",0,1);
  if( zHighlight ) nTty = atoi(zHighlight);








>







616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
  const char *zScope = 0;
  const char *zWidth = find_option("width","W",1);
  int bDebug = find_option("debug",0,0)!=0;     /* Undocumented */
  int nLimit = zLimit ? atoi(zLimit) : -1000;
  int width;
  int nTty = 0;          /* VT100 highlight color for matching text */
  const char *zHighlight = 0;
  int bFlags = 0;        /* DB open flags */

  nTty =  terminal_is_vt100();

  /* Undocumented option to change highlight color */
  zHighlight = find_option("highlight",0,1);
  if( zHighlight ) nTty = atoi(zHighlight);

664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
  if( find_option("technotes",0,0) ){  srchFlags |= SRCH_TECHNOTE; bFts = 1; }
  if( find_option("tickets",0,0) ){    srchFlags |= SRCH_TKT;      bFts = 1; }
  if( find_option("wiki",0,0) ){       srchFlags |= SRCH_WIKI;     bFts = 1; }

  /* If no search objects are specified, default to "check-in comments" */
  if( srchFlags==0 ) srchFlags = SRCH_CKIN;


  db_find_and_open_repository(0, 0);
  verify_all_options();
  if( g.argc<3 ) return;
  login_set_capabilities("s", 0);
  if( search_restrict(srchFlags)==0 && (srchFlags & SRCH_HELP)==0 ){
    const char *zC1 = 0, *zPlural = "s";
    if( srchFlags & SRCH_TECHNOTE ){  zC1 = "technote"; }
    if( srchFlags & SRCH_TKT ){       zC1 = "ticket";   }







|
|







665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
  if( find_option("technotes",0,0) ){  srchFlags |= SRCH_TECHNOTE; bFts = 1; }
  if( find_option("tickets",0,0) ){    srchFlags |= SRCH_TKT;      bFts = 1; }
  if( find_option("wiki",0,0) ){       srchFlags |= SRCH_WIKI;     bFts = 1; }

  /* If no search objects are specified, default to "check-in comments" */
  if( srchFlags==0 ) srchFlags = SRCH_CKIN;

  if( srchFlags==SRCH_HELP ) bFlags = OPEN_OK_NOT_FOUND|OPEN_SUBSTITUTE;
  db_find_and_open_repository(bFlags, 0);
  verify_all_options();
  if( g.argc<3 ) return;
  login_set_capabilities("s", 0);
  if( search_restrict(srchFlags)==0 && (srchFlags & SRCH_HELP)==0 ){
    const char *zC1 = 0, *zPlural = "s";
    if( srchFlags & SRCH_TECHNOTE ){  zC1 = "technote"; }
    if( srchFlags & SRCH_TKT ){       zC1 = "ticket";   }
Changes to src/security_audit.c.
98
99
100
101
102
103
104

105
106
107
108
109
110
111
  const char *zAnonCap;      /* Capabilities of user "anonymous" and "nobody" */
  const char *zDevCap;       /* Capabilities of user group "developer" */
  const char *zReadCap;      /* Capabilities of user group "reader" */
  const char *zPubPages;     /* GLOB pattern for public pages */
  const char *zSelfCap;      /* Capabilities of self-registered users */
  int hasSelfReg = 0;        /* True if able to self-register */
  const char *zPublicUrl;    /* Canonical access URL */

  Blob cmd;
  char *z;
  int n, i;
  CapabilityString *pCap;
  char **azCSP;              /* Parsed content security policy */

  login_check_credentials();







>







98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
  const char *zAnonCap;      /* Capabilities of user "anonymous" and "nobody" */
  const char *zDevCap;       /* Capabilities of user group "developer" */
  const char *zReadCap;      /* Capabilities of user group "reader" */
  const char *zPubPages;     /* GLOB pattern for public pages */
  const char *zSelfCap;      /* Capabilities of self-registered users */
  int hasSelfReg = 0;        /* True if able to self-register */
  const char *zPublicUrl;    /* Canonical access URL */
  const char *zVulnReport;   /* The vuln-report setting */
  Blob cmd;
  char *z;
  int n, i;
  CapabilityString *pCap;
  char **azCSP;              /* Parsed content security policy */

  login_check_credentials();
360
361
362
363
364
365
366












367
368
369
370
371
372
373
  /* The strict-manifest-syntax setting should be on. */
  if( db_get_boolean("strict-manifest-syntax",1)==0 ){
    @ <li><p><b>WARNING:</b>
    @ The "strict-manifest-syntax"  flag is off.  This is a security
    @ risk.  Turn this setting on (its default) to protect the users
    @ of this repository.
  }













  /* Obsolete:  */
  if( hasAnyCap(zAnonCap, "d") ||
      hasAnyCap(zDevCap,  "d") ||
      hasAnyCap(zReadCap, "d") ){
    @ <li><p><b>WARNING:</b>
    @ One or more users has the <a







>
>
>
>
>
>
>
>
>
>
>
>







361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
  /* The strict-manifest-syntax setting should be on. */
  if( db_get_boolean("strict-manifest-syntax",1)==0 ){
    @ <li><p><b>WARNING:</b>
    @ The "strict-manifest-syntax"  flag is off.  This is a security
    @ risk.  Turn this setting on (its default) to protect the users
    @ of this repository.
  }

  zVulnReport = db_get("vuln-report","log");
  if( fossil_strcmp(zVulnReport,"block")!=0
   && fossil_strcmp(zVulnReport,"fatal")!=0
  ){
    @ <li><p><b>WARNING:</b>
    @ The <a href="%R/help?cmd=vuln-report">vuln-report setting</a>
    @ has a value of "%h(zVulnReport)". This disables defenses against
    @ XSS or SQL-injection vulnerabilities caused by coding errors in
    @ custom TH1 scripts.  For the best security, change
    @ the value of the vuln-report setting to "block" or "fatal".
  }

  /* Obsolete:  */
  if( hasAnyCap(zAnonCap, "d") ||
      hasAnyCap(zDevCap,  "d") ||
      hasAnyCap(zReadCap, "d") ){
    @ <li><p><b>WARNING:</b>
    @ One or more users has the <a
808
809
810
811
812
813
814
815
816
817
818



819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837



838
839
840
841
842
843
844

/*
** WEBPAGE: errorlog
**
** Show the content of the error log.  Only the administrator can view
** this page.
**
**    y=0x01          Show only hack attempts
**    y=0x02          Show only panics and assertion faults
**    y=0x04          Show hung backoffice processes
**    y=0x08          Show POST requests from a different origin



**    y=0x40          Show other uncategorized messages
**
** If y is omitted or is zero, a count of the various message types is
** shown.
*/
void errorlog_page(void){
  i64 szFile;
  FILE *in;
  char *zLog;
  const char *zType = P("y");
  static const int eAllTypes = 0x4f;
  long eType = 0;
  int bOutput = 0;
  int prevWasTime = 0;
  int nHack = 0;
  int nPanic = 0;
  int nOther = 0;
  int nHang = 0;
  int nXPost = 0;



  char z[10000];
  char zTime[10000];

  login_check_credentials();
  if( !g.perm.Admin ){
    login_needed(0);
    return;







|
|
|
|
>
>
>
|









|








>
>
>







821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863

/*
** WEBPAGE: errorlog
**
** Show the content of the error log.  Only the administrator can view
** this page.
**
**    y=0x001          Show only hack attempts
**    y=0x002          Show only panics and assertion faults
**    y=0x004          Show hung backoffice processes
**    y=0x008          Show POST requests from a different origin
**    y=0x010          Show SQLITE_AUTH and similar
**    y=0x020          Show SMTP error reports
**    y=0x040          Show TH1 vulnerability reports
**    y=0x800          Show other uncategorized messages
**
** If y is omitted or is zero, a count of the various message types is
** shown.
*/
void errorlog_page(void){
  i64 szFile;
  FILE *in;
  char *zLog;
  const char *zType = P("y");
  static const int eAllTypes = 0x87f;
  long eType = 0;
  int bOutput = 0;
  int prevWasTime = 0;
  int nHack = 0;
  int nPanic = 0;
  int nOther = 0;
  int nHang = 0;
  int nXPost = 0;
  int nAuth = 0;
  int nSmtp = 0;
  int nVuln = 0;
  char z[10000];
  char zTime[10000];

  login_check_credentials();
  if( !g.perm.Admin ){
    login_needed(0);
    return;
904
905
906
907
908
909
910






911



912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928




929
930
931
932
933
934
935
936
937










938
939
940
941
942
943
944
945
946
    }
    if( eType & 0x04 ){
      @ <li>Hung backoffice processes
    }
    if( eType & 0x08 ){
      @ <li>POST requests from different origin
    }






    if( eType & 0x40 ){



      @ <li>Other uncategorized messages
    }
    @ </ul>
  }
  @ <hr>
  if( eType ){
    @ <pre>
  }
  while( fgets(z, sizeof(z), in) ){
    if( prevWasTime ){
      if( strncmp(z,"possible hack attempt - 418 ", 27)==0 ){
        bOutput = (eType & 0x01)!=0;
        nHack++;
      }else
      if( (strncmp(z,"panic: ", 7)==0 || strstr(z," assertion fault ")!=0) ){
        bOutput = (eType & 0x02)!=0;
        nPanic++;




      }else
      if( sqlite3_strglob("warning: backoffice process * still *",z)==0 ){
        bOutput = (eType & 0x04)!=0;
        nHang++;
      }else
      if( sqlite3_strglob("warning: POST from different origin*",z)==0 ){
        bOutput = (eType & 0x08)!=0;
        nXPost++;
      }else










      {
        bOutput = (eType & 0x40)!=0;
        nOther++;
      }
      if( bOutput ){
        @ %h(zTime)\
      }
    }
    if( strncmp(z, "--------", 8)==0 ){







>
>
>
>
>
>

>
>
>

















>
>
>
>









>
>
>
>
>
>
>
>
>
>

|







923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
    }
    if( eType & 0x04 ){
      @ <li>Hung backoffice processes
    }
    if( eType & 0x08 ){
      @ <li>POST requests from different origin
    }
    if( eType & 0x10 ){
      @ <li>SQLITE_AUTH and similar errors
    }
    if( eType & 0x20 ){
      @ <li>SMTP malfunctions
    }
    if( eType & 0x40 ){
      @ <li>TH1 vulnerabilities
    }
    if( eType & 0x800 ){
      @ <li>Other uncategorized messages
    }
    @ </ul>
  }
  @ <hr>
  if( eType ){
    @ <pre>
  }
  while( fgets(z, sizeof(z), in) ){
    if( prevWasTime ){
      if( strncmp(z,"possible hack attempt - 418 ", 27)==0 ){
        bOutput = (eType & 0x01)!=0;
        nHack++;
      }else
      if( (strncmp(z,"panic: ", 7)==0 || strstr(z," assertion fault ")!=0) ){
        bOutput = (eType & 0x02)!=0;
        nPanic++;
      }else
      if( strncmp(z,"SMTP:", 5)==0 ){
        bOutput = (eType & 0x20)!=0;
        nSmtp++;
      }else
      if( sqlite3_strglob("warning: backoffice process * still *",z)==0 ){
        bOutput = (eType & 0x04)!=0;
        nHang++;
      }else
      if( sqlite3_strglob("warning: POST from different origin*",z)==0 ){
        bOutput = (eType & 0x08)!=0;
        nXPost++;
      }else
      if( sqlite3_strglob("SECURITY: authorizer blocks*",z)==0
       || sqlite3_strglob("warning: SQLITE_AUTH*",z)==0
      ){
        bOutput = (eType & 0x10)!=0;
        nAuth++;
      }else
      if( strncmp(z,"possible", 8)==0 && strstr(z,"tainted")!=0 ){
        bOutput = (eType & 0x40)!=0;
        nVuln++;
      }else
      {
        bOutput = (eType & 0x800)!=0;
        nOther++;
      }
      if( bOutput ){
        @ %h(zTime)\
      }
    }
    if( strncmp(z, "--------", 8)==0 ){
956
957
958
959
960
961
962
963
964
965
966
967
968
969




970
971
972
973
974
975
976
977
978
979








980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
    }
  }
  fclose(in);
  if( eType ){
    @ </pre>
  }
  if( eType==0 ){
    int nNonHack = nPanic + nHang + nOther;
    int nTotal = nNonHack + nHack + nXPost;
    @ <p><table border="a" cellspacing="0" cellpadding="5">
    if( nPanic>0 ){
      @ <tr><td align="right">%d(nPanic)</td>
      @     <td><a href="./errorlog?y=2">Panics</a></td>
    }




    if( nHack>0 ){
      @ <tr><td align="right">%d(nHack)</td>
      @     <td><a href="./errorlog?y=1">Hack Attempts</a></td>
    }
    if( nHang>0 ){
      @ <tr><td align="right">%d(nHang)</td>
      @     <td><a href="./errorlog?y=4/">Hung Backoffice</a></td>
    }
    if( nXPost>0 ){
      @ <tr><td align="right">%d(nXPost)</td>








      @     <td><a href="./errorlog?y=8/">POSTs from different origin</a></td>
    }
    if( nOther>0 ){
      @ <tr><td align="right">%d(nOther)</td>
      @     <td><a href="./errorlog?y=64/">Other</a></td>
    }
    if( nHack+nXPost>0 && nNonHack>0 ){
      @ <tr><td align="right">%d(nNonHack)</td>
      @ <td><a href="%R/errorlog?y=70">Other than hack attempts</a></td>
    }
    @ <tr><td align="right">%d(nTotal)</td>
    if( nTotal>0 ){
      @     <td><a href="./errorlog?y=255">All Messages</a></td>
    }else{
      @     <td>All Messages</td>
    }
    @ </table>
  }
  style_finish_page();
}







|






>
>
>
>






|



>
>
>
>
>
>
>
>
|



|
<
<
<
<



|







998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038




1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
    }
  }
  fclose(in);
  if( eType ){
    @ </pre>
  }
  if( eType==0 ){
    int nNonHack = nPanic + nHang + nAuth + nSmtp + nVuln + nOther;
    int nTotal = nNonHack + nHack + nXPost;
    @ <p><table border="a" cellspacing="0" cellpadding="5">
    if( nPanic>0 ){
      @ <tr><td align="right">%d(nPanic)</td>
      @     <td><a href="./errorlog?y=2">Panics</a></td>
    }
    if( nVuln>0 ){
      @ <tr><td align="right">%d(nVuln)</td>
      @     <td><a href="./errorlog?y=64">TH1 Vulnerabilities</a></td>
    }
    if( nHack>0 ){
      @ <tr><td align="right">%d(nHack)</td>
      @     <td><a href="./errorlog?y=1">Hack Attempts</a></td>
    }
    if( nHang>0 ){
      @ <tr><td align="right">%d(nHang)</td>
      @     <td><a href="./errorlog?y=4">Hung Backoffice</a></td>
    }
    if( nXPost>0 ){
      @ <tr><td align="right">%d(nXPost)</td>
      @     <td><a href="./errorlog?y=8">POSTs from different origin</a></td>
    }
    if( nAuth>0 ){
      @ <tr><td align="right">%d(nAuth)</td>
      @     <td><a href="./errorlog?y=16">SQLITE_AUTH and similar</a></td>
    }
    if( nSmtp>0 ){
      @ <tr><td align="right">%d(nSmtp)</td>
      @     <td><a href="./errorlog?y=32">SMTP faults</a></td>
    }
    if( nOther>0 ){
      @ <tr><td align="right">%d(nOther)</td>
      @     <td><a href="./errorlog?y=2048">Other</a></td>




    }
    @ <tr><td align="right">%d(nTotal)</td>
    if( nTotal>0 ){
      @     <td><a href="./errorlog?y=4095">All Messages</a></td>
    }else{
      @     <td>All Messages</td>
    }
    @ </table>
  }
  style_finish_page();
}
Changes to src/setup.c.
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
  login_check_credentials();
  if( !g.perm.Admin ){
    login_needed(0);
    return;
  }
  style_header("Log Menu");
  @ <table border="0" cellspacing="3">
  
  if( db_get_boolean("admin-log",1)==0 ){
    blob_appendf(&desc,
      "The admin log records configuration changes to the repository.\n"
      "<b>Disabled</b>:  Turn on the "
      " <a href='%R/setup_settings'>admin-log setting</a> to enable."
    );
    setup_menu_entry("Admin Log", 0, blob_str(&desc));
    blob_reset(&desc);
  }else{
    setup_menu_entry("Admin Log", "admin_log",
      "The admin log records configuration changes to the repository\n"
      "in the \"admin_log\" table.\n"
    );
  }
  setup_menu_entry("Artifact Log", "rcvfromlist",
    "The artifact log records when new content is added in the\n"
    "\"rcvfrom\" table.\n"
  );
  if( db_get_boolean("access-log",1) ){
    setup_menu_entry("User Log", "user_log",
      "Login attempts recorded in the \"accesslog\" table."
    );







|














|







199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
  login_check_credentials();
  if( !g.perm.Admin ){
    login_needed(0);
    return;
  }
  style_header("Log Menu");
  @ <table border="0" cellspacing="3">

  if( db_get_boolean("admin-log",1)==0 ){
    blob_appendf(&desc,
      "The admin log records configuration changes to the repository.\n"
      "<b>Disabled</b>:  Turn on the "
      " <a href='%R/setup_settings'>admin-log setting</a> to enable."
    );
    setup_menu_entry("Admin Log", 0, blob_str(&desc));
    blob_reset(&desc);
  }else{
    setup_menu_entry("Admin Log", "admin_log",
      "The admin log records configuration changes to the repository\n"
      "in the \"admin_log\" table.\n"
    );
  }
  setup_menu_entry("Xfer Log", "rcvfromlist",
    "The artifact log records when new content is added in the\n"
    "\"rcvfrom\" table.\n"
  );
  if( db_get_boolean("access-log",1) ){
    setup_menu_entry("User Log", "user_log",
      "Login attempts recorded in the \"accesslog\" table."
    );
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
  @ </blockquote>
  @ <p>For maximum robot defense, "Delay" should be at least 50 milliseconds
  @ and "require a mouse event" should be turned on.  These values only come
  @ into play when the main auto-hyperlink settings is 2 ("UserAgent and
  @ Javascript").</p>
  @
  @ <p>To see if Javascript-base hyperlink enabling mechanism is working,
  @ visit the <a href="%R/test_env">/test_env</a> page (from a separate
  @ web browser that is not logged in, even as "anonymous") and verify
  @ that the "g.jsHref" value is "1".</p>
  @ <p>(Properties: "auto-hyperlink", "auto-hyperlink-delay", and
  @ "auto-hyperlink-mouseover"")</p>
}

/*







|







460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
  @ </blockquote>
  @ <p>For maximum robot defense, "Delay" should be at least 50 milliseconds
  @ and "require a mouse event" should be turned on.  These values only come
  @ into play when the main auto-hyperlink settings is 2 ("UserAgent and
  @ Javascript").</p>
  @
  @ <p>To see if Javascript-base hyperlink enabling mechanism is working,
  @ visit the <a href="%R/test-env">/test-env</a> page (from a separate
  @ web browser that is not logged in, even as "anonymous") and verify
  @ that the "g.jsHref" value is "1".</p>
  @ <p>(Properties: "auto-hyperlink", "auto-hyperlink-delay", and
  @ "auto-hyperlink-mouseover"")</p>
}

/*
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
  @ without the "--localauth" option.
  @ <li> The server is started from CGI without the "localauth" keyword
  @ in the CGI script.
  @ </ol>
  @ (Property: "localauth")
  @
  @ <hr>
  onoff_attribute("Enable /test_env",
     "test_env_enable", "test_env_enable", 0, 0);
  @ <p>When enabled, the %h(g.zBaseURL)/test_env URL is available to all
  @ users.  When disabled (the default) only users Admin and Setup can visit
  @ the /test_env page.
  @ (Property: "test_env_enable")
  @ </p>
  @







|







602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
  @ without the "--localauth" option.
  @ <li> The server is started from CGI without the "localauth" keyword
  @ in the CGI script.
  @ </ol>
  @ (Property: "localauth")
  @
  @ <hr>
  onoff_attribute("Enable /test-env",
     "test_env_enable", "test_env_enable", 0, 0);
  @ <p>When enabled, the %h(g.zBaseURL)/test_env URL is available to all
  @ users.  When disabled (the default) only users Admin and Setup can visit
  @ the /test_env page.
  @ (Property: "test_env_enable")
  @ </p>
  @
1001
1002
1003
1004
1005
1006
1007










1008
1009
1010
1011
1012
1013
1014
  @ <hr>
  onoff_attribute("Break comments at newline characters",
                  "timeline-hard-newlines", "thnl", 0, 0);
  @ <p>In timeline displays, newline characters in check-in comments force
  @ a line break on the display.
  @ (Property: "timeline-hard-newlines")</p>











  @ <hr>
  onoff_attribute("Use Universal Coordinated Time (UTC)",
                  "timeline-utc", "utc", 1, 0);
  @ <p>Show times as UTC (also sometimes called Greenwich Mean Time (GMT) or
  @ Zulu) instead of in local time.  On this server, local time is currently
  tmDiff = db_double(0.0, "SELECT julianday('now')");
  tmDiff = db_double(0.0,







>
>
>
>
>
>
>
>
>
>







1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
  @ <hr>
  onoff_attribute("Break comments at newline characters",
                  "timeline-hard-newlines", "thnl", 0, 0);
  @ <p>In timeline displays, newline characters in check-in comments force
  @ a line break on the display.
  @ (Property: "timeline-hard-newlines")</p>

  @ <hr>
  onoff_attribute("Do not adjust user-selected background colors",
                  "raw-bgcolor", "rbgc", 0, 0);
  @ <p>Fossil normally attempts to adjust the saturation and intensity of
  @ user-specified background colors on check-ins and branches so that the
  @ foreground text is easily readable on all skins.  Enable this setting
  @ to omit that adjustment and use exactly the background color specified
  @ by users.
  @ (Property: "raw-bgcolor")</p>

  @ <hr>
  onoff_attribute("Use Universal Coordinated Time (UTC)",
                  "timeline-utc", "utc", 1, 0);
  @ <p>Show times as UTC (also sometimes called Greenwich Mean Time (GMT) or
  @ Zulu) instead of in local time.  On this server, local time is currently
  tmDiff = db_double(0.0, "SELECT julianday('now')");
  tmDiff = db_double(0.0,
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
  @ The project name will also be used as the RSS feed title.
  @ (Property: "project-name")
  @ </p>
  @ <hr>
  textarea_attribute("Project Description", 3, 80,
                     "project-description", "pd", "", 0);
  @ <p>Describe your project. This will be used in page headers for search
  @ engines as well as a short RSS description.
  @ (Property: "project-description")</p>
  @ <hr>
  entry_attribute("Canonical Server URL", 40, "email-url",
                   "eurl", "", 0);
  @ <p>This is the URL used to access this repository as a server.
  @ Other repositories use this URL to clone or sync against this repository.
  @ This is also the basename for hyperlinks included in email alert text.







|







1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
  @ The project name will also be used as the RSS feed title.
  @ (Property: "project-name")
  @ </p>
  @ <hr>
  textarea_attribute("Project Description", 3, 80,
                     "project-description", "pd", "", 0);
  @ <p>Describe your project. This will be used in page headers for search
  @ engines, the repository listing and a short RSS description.
  @ (Property: "project-description")</p>
  @ <hr>
  entry_attribute("Canonical Server URL", 40, "email-url",
                   "eurl", "", 0);
  @ <p>This is the URL used to access this repository as a server.
  @ Other repositories use this URL to clone or sync against this repository.
  @ This is also the basename for hyperlinks included in email alert text.
Changes to src/setupuser.c.
39
40
41
42
43
44
45

46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
*/
void setup_ulist(void){
  Stmt s;
  double rNow;
  const char *zWith = P("with");
  int bUnusedOnly = P("unused")!=0;
  int bUbg = P("ubg")!=0;


  login_check_credentials();
  if( !g.perm.Admin ){
    login_needed(0);
    return;
  }

  style_submenu_element("Add", "setup_uedit");
  style_submenu_element("Log", "access_log");
  style_submenu_element("Help", "setup_ulist_notes");
  if( alert_tables_exist() ){
    style_submenu_element("Subscribers", "subscribers");
  }
  style_set_current_feature("setup");
  style_header("User List");
  if( (zWith==0 || zWith[0]==0) && !bUnusedOnly ){
    @ <table border=1 cellpadding=2 cellspacing=0 class='userTable'>
    @ <thead><tr>







>






|



|







39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
*/
void setup_ulist(void){
  Stmt s;
  double rNow;
  const char *zWith = P("with");
  int bUnusedOnly = P("unused")!=0;
  int bUbg = P("ubg")!=0;
  int bHaveAlerts;

  login_check_credentials();
  if( !g.perm.Admin ){
    login_needed(0);
    return;
  }
  bHaveAlerts = alert_tables_exist();
  style_submenu_element("Add", "setup_uedit");
  style_submenu_element("Log", "access_log");
  style_submenu_element("Help", "setup_ulist_notes");
  if( bHaveAlerts ){
    style_submenu_element("Subscribers", "subscribers");
  }
  style_set_current_feature("setup");
  style_header("User List");
  if( (zWith==0 || zWith[0]==0) && !bUnusedOnly ){
    @ <table border=1 cellpadding=2 cellspacing=0 class='userTable'>
    @ <thead><tr>
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167

168
169
170
171




172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206


207
208
209
210
211
212
213
214
  }
  if( bUnusedOnly ){
    zWith = mprintf(
        " AND login NOT IN ("
        "SELECT user FROM event WHERE user NOT NULL "
        "UNION ALL SELECT euser FROM event WHERE euser NOT NULL%s)"
        " AND uid NOT IN (SELECT uid FROM rcvfrom)",
        alert_tables_exist() ?
          " UNION ALL SELECT suname FROM subscriber WHERE suname NOT NULL":"");
  }else if( zWith && zWith[0] ){
    zWith = mprintf(" AND fullcap(cap) GLOB '*[%q]*'", zWith);
  }else{
    zWith = "";
  }
  db_prepare(&s,
     "SELECT uid, login, cap, info, date(user.mtime,'unixepoch'),"
     "       lower(login) AS sortkey, "
     "       CASE WHEN info LIKE '%%expires 20%%'"
             "    THEN substr(info,instr(lower(info),'expires')+8,10)"
             "    END AS exp,"
             "atime,"
     "       subscriber.ssub, subscriber.subscriberId,"
     "       user.mtime AS sorttime"

     "  FROM user LEFT JOIN lastAccess ON login=uname"
     "            LEFT JOIN subscriber ON login=suname"
     " WHERE login NOT IN ('anonymous','nobody','developer','reader') %s"
     " ORDER BY sorttime DESC", zWith/*safe-for-%s*/




  );
  rNow = db_double(0.0, "SELECT julianday('now');");
  while( db_step(&s)==SQLITE_ROW ){
    int uid = db_column_int(&s, 0);
    const char *zLogin = db_column_text(&s, 1);
    const char *zCap = db_column_text(&s, 2);
    const char *zInfo = db_column_text(&s, 3);
    const char *zDate = db_column_text(&s, 4);
    const char *zSortKey = db_column_text(&s,5);
    const char *zExp = db_column_text(&s,6);
    double rATime = db_column_double(&s,7);
    char *zAge = 0;
    const char *zSub;
    int sid = db_column_int(&s,9);
    sqlite3_int64 sorttime = db_column_int64(&s, 10);
    if( rATime>0.0 ){
      zAge = human_readable_age(rNow - rATime);
    }
    if( bUbg ){
      @ <tr style='background-color: %h(user_color(zLogin));'>
    }else{
      @ <tr>
    }
    @ <td data-sortkey='%h(zSortKey)'>\
    @ <a href='setup_uedit?id=%d(uid)'>%h(zLogin)</a>
    @ <td>%h(zCap)
    @ <td>%h(zInfo)
    @ <td data-sortkey='%09llx(sorttime)'>%h(zDate?zDate:"")
    @ <td>%h(zExp?zExp:"")
    @ <td data-sortkey='%f(rATime)' style='white-space:nowrap'>%s(zAge?zAge:"")
    if( db_column_type(&s,8)==SQLITE_NULL ){
      @ <td>
    }else if( (zSub = db_column_text(&s,8))==0 || zSub[0]==0 ){
      @ <td><a href="%R/alerts?sid=%d(sid)"><i>off</i></a>
    }else{


      @ <td><a href="%R/alerts?sid=%d(sid)">%h(zSub)</a>
    }

    @ </tr>
    fossil_free(zAge);
  }
  @ </tbody></table>
  db_finalize(&s);







|







|
|
|


|
<
|
>
|
|
|
|
>
>
>
>













|
|















|

|


>
>
|







146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166

167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
  }
  if( bUnusedOnly ){
    zWith = mprintf(
        " AND login NOT IN ("
        "SELECT user FROM event WHERE user NOT NULL "
        "UNION ALL SELECT euser FROM event WHERE euser NOT NULL%s)"
        " AND uid NOT IN (SELECT uid FROM rcvfrom)",
        bHaveAlerts ?
          " UNION ALL SELECT suname FROM subscriber WHERE suname NOT NULL":"");
  }else if( zWith && zWith[0] ){
    zWith = mprintf(" AND fullcap(cap) GLOB '*[%q]*'", zWith);
  }else{
    zWith = "";
  }
  db_prepare(&s,
      /*0-4*/"SELECT uid, login, cap, info, date(user.mtime,'unixepoch'),"
      /* 5 */"lower(login) AS sortkey, "
      /* 6 */"CASE WHEN info LIKE '%%expires 20%%'"
             "    THEN substr(info,instr(lower(info),'expires')+8,10)"
             "    END AS exp,"
      /* 7 */"atime,"

      /* 8 */"user.mtime AS sorttime,"
      /*9-11*/"%s"
             " FROM user LEFT JOIN lastAccess ON login=uname"
             "            LEFT JOIN subscriber ON login=suname"
             " WHERE login NOT IN ('anonymous','nobody','developer','reader') %s"
             " ORDER BY sorttime DESC",
             bHaveAlerts
             ? "subscriber.ssub, subscriber.subscriberId, subscriber.semail"
             : "null, null, null",
             zWith/*safe-for-%s*/
  );
  rNow = db_double(0.0, "SELECT julianday('now');");
  while( db_step(&s)==SQLITE_ROW ){
    int uid = db_column_int(&s, 0);
    const char *zLogin = db_column_text(&s, 1);
    const char *zCap = db_column_text(&s, 2);
    const char *zInfo = db_column_text(&s, 3);
    const char *zDate = db_column_text(&s, 4);
    const char *zSortKey = db_column_text(&s,5);
    const char *zExp = db_column_text(&s,6);
    double rATime = db_column_double(&s,7);
    char *zAge = 0;
    const char *zSub;
    int sid = db_column_int(&s,10);
    sqlite3_int64 sorttime = db_column_int64(&s, 8);
    if( rATime>0.0 ){
      zAge = human_readable_age(rNow - rATime);
    }
    if( bUbg ){
      @ <tr style='background-color: %h(user_color(zLogin));'>
    }else{
      @ <tr>
    }
    @ <td data-sortkey='%h(zSortKey)'>\
    @ <a href='setup_uedit?id=%d(uid)'>%h(zLogin)</a>
    @ <td>%h(zCap)
    @ <td>%h(zInfo)
    @ <td data-sortkey='%09llx(sorttime)'>%h(zDate?zDate:"")
    @ <td>%h(zExp?zExp:"")
    @ <td data-sortkey='%f(rATime)' style='white-space:nowrap'>%s(zAge?zAge:"")
    if( db_column_type(&s,9)==SQLITE_NULL ){
      @ <td>
    }else if( (zSub = db_column_text(&s,9))==0 || zSub[0]==0 ){
      @ <td><a href="%R/alerts?sid=%d(sid)"><i>off</i></a>
    }else{
      const char *zEmail = db_column_text(&s, 11);
      char * zAt = zEmail ? mprintf(" &rarr; %h", zEmail) : mprintf("");
      @ <td><a href="%R/alerts?sid=%d(sid)">%h(zSub)</a>  %z(zAt)
    }

    @ </tr>
    fossil_free(zAge);
  }
  @ </tbody></table>
  db_finalize(&s);
302
303
304
305
306
307
308
309
310
311
312


313
314
315
316




317







318




319



320


















321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
  if( zPw==0 ) return 0;
  if( zPw[0]==0 ) return 1;
  while( zPw[0]=='*' ){ zPw++; }
  return zPw[0]!=0;
}

/*
** Return true if user capability string zNew contains any capability
** letter which is not in user capability string zOrig, else 0.  This
** does not take inherited permissions into account. Either argument
** may be NULL.


*/
static int userHasNewCaps(const char *zOrig, const char *zNew){
  for( ; zNew && *zNew; ++zNew ){
    if( !zOrig || strchr(zOrig,*zNew)==0 ){




      return *zNew;







    }




  }



  return 0;


















}

/*
** Sends notification of user permission elevation changes to all
** subscribers with a "u" subscription. This is a no-op if alerts are
** not enabled.
**
** These subscriptions differ from most, in that:
**
** - They currently lack an "unsubscribe" link.
**
** - Only an admin can assign this subscription, but if a non-admin
**   edits their subscriptions after an admin assigns them this one,
**   this particular one will be lost.  "Feature or bug?" is unclear,
**   but it would be odd for a non-admin to be assigned this
**   capability.
*/
static void alert_user_elevation(const char *zLogin,   /*Affected user*/
                                 int uid,              /*[user].uid*/
                                 int bIsNew,           /*true if new user*/
                                 const char *zOrigCaps,/*Old caps*/
                                 const char *zNewCaps  /*New caps*/){
  Blob hdr, body;
  Stmt q;
  int nBody;
  AlertSender *pSender;
  char *zSubname;
  char *zURL;
  char * zSubject;

  if( !alert_enabled() ) return;
  zSubject = bIsNew
    ? mprintf("New user created: [%q]", zLogin)
    : mprintf("User [%q] permissions elevated", zLogin);
  zURL = db_get("email-url",0);
  zSubname = db_get("email-subname", "[Fossil Repo]");
  blob_init(&body, 0, 0);
  blob_init(&hdr, 0, 0);
  if( bIsNew ){
    blob_appendf(&body, "User [%q] was created by with "
                 "permissions [%q] by user [%q].\n",
                 zLogin, zNewCaps, g.zLogin);
  } else {
    blob_appendf(&body, "Permissions for user [%q] where elevated "
                 "from [%q] to [%q] by user [%q].\n",
                 zLogin, zOrigCaps, zNewCaps, g.zLogin);
  }
  if( zURL ){
    blob_appendf(&body, "\nUser editor: %s/setup_uedit?uid=%d\n", zURL, uid);
  }
  nBody = blob_size(&body);







|
|
|
|
>
>

|
<
|
>
>
>
>
|
>
>
>
>
>
>
>
|
>
>
>
>
|
>
>
>
|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>

















|















|





|



|







309
310
311
312
313
314
315
316
317
318
319
320
321
322
323

324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
  if( zPw==0 ) return 0;
  if( zPw[0]==0 ) return 1;
  while( zPw[0]=='*' ){ zPw++; }
  return zPw[0]!=0;
}

/*
** Return true if user capability strings zOrig and zNew materially
** differ, taking into account that they may be sorted in an arbitary
** order. This does not take inherited permissions into
** account. Either argument may be NULL. A NULL and an empty string
** are considered equivalent here. e.g. "abc" and "cab" are equivalent
** for this purpose, but "aCb" and "acb" are not.
*/
static int userCapsChanged(const char *zOrig, const char *zNew){

  if( !zOrig ){
    return zNew ? (0!=*zNew) : 0;
  }else if( !zNew ){
    return 0!=*zOrig;
  }else if( 0==fossil_strcmp(zOrig, zNew) ){
    return 0;
  }else{
    /* We don't know that zOrig and zNew are sorted equivalently.  The
    ** following steps will compare strings which contain all the same
    ** capabilities letters as equivalent, regardless of the letters'
    ** order in their strings. */
    char aOrig[128]; /* table of zOrig bytes */
    int nOrig = 0, nNew = 0;

    memset( &aOrig[0], 0, sizeof(aOrig) );
    for( ; *zOrig; ++zOrig, ++nOrig ){
      if( 0==(*zOrig & 0x80) ){
        aOrig[(int)*zOrig] = 1;
      }
    }
    for( ; *zNew; ++zNew, ++nNew ){
      if( 0==(*zNew & 0x80) && !aOrig[(int)*zNew] ){
        return 1;
      }
    }
    return nOrig!=nNew;
  }
}

/*
** COMMAND: test-user-caps-changed
**
** Usage: %fossil test-user-caps-changed caps1 caps2
**
*/
void test_user_caps_changed(void){

  char const * zOld = g.argc>2 ? g.argv[2] : NULL;
  char const * zNew = g.argc>3 ? g.argv[3] : NULL;
  fossil_print("Has changes? = %d\n",
               userCapsChanged( zOld, zNew ));
}

/*
** Sends notification of user permission elevation changes to all
** subscribers with a "u" subscription. This is a no-op if alerts are
** not enabled.
**
** These subscriptions differ from most, in that:
**
** - They currently lack an "unsubscribe" link.
**
** - Only an admin can assign this subscription, but if a non-admin
**   edits their subscriptions after an admin assigns them this one,
**   this particular one will be lost.  "Feature or bug?" is unclear,
**   but it would be odd for a non-admin to be assigned this
**   capability.
*/
static void alert_user_cap_change(const char *zLogin,   /*Affected user*/
                                 int uid,              /*[user].uid*/
                                 int bIsNew,           /*true if new user*/
                                 const char *zOrigCaps,/*Old caps*/
                                 const char *zNewCaps  /*New caps*/){
  Blob hdr, body;
  Stmt q;
  int nBody;
  AlertSender *pSender;
  char *zSubname;
  char *zURL;
  char * zSubject;

  if( !alert_enabled() ) return;
  zSubject = bIsNew
    ? mprintf("New user created: [%q]", zLogin)
    : mprintf("User [%q] capabilities changed", zLogin);
  zURL = db_get("email-url",0);
  zSubname = db_get("email-subname", "[Fossil Repo]");
  blob_init(&body, 0, 0);
  blob_init(&hdr, 0, 0);
  if( bIsNew ){
    blob_appendf(&body, "User [%q] was created with "
                 "permissions [%q] by user [%q].\n",
                 zLogin, zNewCaps, g.zLogin);
  } else {
    blob_appendf(&body, "Permissions for user [%q] where changed "
                 "from [%q] to [%q] by user [%q].\n",
                 zLogin, zOrigCaps, zNewCaps, g.zLogin);
  }
  if( zURL ){
    blob_appendf(&body, "\nUser editor: %s/setup_uedit?uid=%d\n", zURL, uid);
  }
  nBody = blob_size(&body);
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
  }else if( zDeleteVerify!=0 ){
    /* Need to verify a delete request */
  }else if( !cgi_csrf_safe(2) ){
    /* This might be a cross-site request forgery, so ignore it */
  }else{
    /* We have all the information we need to make the change to the user */
    char c;
    int bHasNewCaps = 0 /* 1 if user's permissions are increased */;
    const int bIsNew = uid<=0;
    char aCap[70], zNm[4];
    zNm[0] = 'a';
    zNm[2] = 0;
    for(i=0, c='a'; c<='z'; c++){
      zNm[1] = c;
      a[c&0x7f] = ((c!='s' && c!='y') || g.perm.Setup) && P(zNm)!=0;







|







528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
  }else if( zDeleteVerify!=0 ){
    /* Need to verify a delete request */
  }else if( !cgi_csrf_safe(2) ){
    /* This might be a cross-site request forgery, so ignore it */
  }else{
    /* We have all the information we need to make the change to the user */
    char c;
    int bCapsChanged = 0 /* 1 if user's permissions changed */;
    const int bIsNew = uid<=0;
    char aCap[70], zNm[4];
    zNm[0] = 'a';
    zNm[2] = 0;
    for(i=0, c='a'; c<='z'; c++){
      zNm[1] = c;
      a[c&0x7f] = ((c!='s' && c!='y') || g.perm.Setup) && P(zNm)!=0;
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
    for(c='A'; c<='Z'; c++){
      zNm[1] = c;
      a[c&0x7f] = P(zNm)!=0;
      if( a[c&0x7f] ) aCap[i++] = c;
    }

    aCap[i] = 0;
    bHasNewCaps = bIsNew || userHasNewCaps(zOldCaps, &aCap[0]);
    zPw = P("pw");
    zLogin = P("login");
    if( strlen(zLogin)==0 ){
      const char *zRef = cgi_referer("setup_ulist");
      style_header("User Creation Error");
      @ <span class="loginError">Empty login not allowed.</span>
      @







|







550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
    for(c='A'; c<='Z'; c++){
      zNm[1] = c;
      a[c&0x7f] = P(zNm)!=0;
      if( a[c&0x7f] ) aCap[i++] = c;
    }

    aCap[i] = 0;
    bCapsChanged = bIsNew || userCapsChanged(zOldCaps, &aCap[0]);
    zPw = P("pw");
    zLogin = P("login");
    if( strlen(zLogin)==0 ){
      const char *zRef = cgi_referer("setup_ulist");
      style_header("User Creation Error");
      @ <span class="loginError">Empty login not allowed.</span>
      @
611
612
613
614
615
616
617
618


619
620
621
622
623
624
625
626
627
628
629
630
631
632
        style_header("User Change Error");
        admin_log( "Error updating user '%q': %s'.", zLogin, zErr );
        @ <span class="loginError">%h(zErr)</span>
        @
        @ <p><a href="setup_uedit?id=%d(uid)&referer=%T(zRef)">
        @ [Bummer]</a></p>
        style_finish_page();
        if( bHasNewCaps ){


          alert_user_elevation(zLogin, uid, bIsNew, zOldCaps, &aCap[0]);
        }
        return;
      }
    }
    if( bHasNewCaps ){
      alert_user_elevation(zLogin, uid, bIsNew, zOldCaps, &aCap[0]);
    }
    cgi_redirect(cgi_referer("setup_ulist"));
    return;
  }

  /* Load the existing information about the user, if any
  */







|
>
>
|




|
|







655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
        style_header("User Change Error");
        admin_log( "Error updating user '%q': %s'.", zLogin, zErr );
        @ <span class="loginError">%h(zErr)</span>
        @
        @ <p><a href="setup_uedit?id=%d(uid)&referer=%T(zRef)">
        @ [Bummer]</a></p>
        style_finish_page();
        if( bCapsChanged ){
          /* It's possible that caps were updated locally even if
          ** login group updates failed. */
          alert_user_cap_change(zLogin, uid, bIsNew, zOldCaps, &aCap[0]);
        }
        return;
      }
    }
    if( bCapsChanged ){
      alert_user_cap_change(zLogin, uid, bIsNew, zOldCaps, &aCap[0]);
    }
    cgi_redirect(cgi_referer("setup_ulist"));
    return;
  }

  /* Load the existing information about the user, if any
  */
Changes to src/shun.c.
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
  const int perScreen = 500;   /* RCVIDs per page */

  login_check_credentials();
  if( !g.perm.Admin ){
    login_needed(0);
    return;
  }
  style_header("Artifact Receipts");
  style_submenu_element("Log-Menu", "setup-logmenu");
  if( showAll ){
    ofst = 0;
  }else{
    style_submenu_element("All", "rcvfromlist?all=1");
  }
  if( ofst>0 ){







|







371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
  const int perScreen = 500;   /* RCVIDs per page */

  login_check_credentials();
  if( !g.perm.Admin ){
    login_needed(0);
    return;
  }
  style_header("Xfer Log");
  style_submenu_element("Log-Menu", "setup-logmenu");
  if( showAll ){
    ofst = 0;
  }else{
    style_submenu_element("All", "rcvfromlist?all=1");
  }
  if( ofst>0 ){
413
414
415
416
417
418
419

420
421
422
423
424
425
426
427
428
    "       EXISTS(SELECT 1 FROM rcvidSha1 WHERE x=rcvfrom.rcvid),"
    "       EXISTS(SELECT 1 FROM rcvidSha3 WHERE x=rcvfrom.rcvid)"
    "  FROM rcvfrom LEFT JOIN user USING(uid)"
    " ORDER BY rcvid DESC LIMIT %d OFFSET %d",
    showAll ? -1 : perScreen+1, ofst
  );
  @ <p>Whenever new artifacts are added to the repository, either by

  @ push or using the web interface, an entry is made in the RCVFROM table
  @ to record the source of that artifact.  This log facilitates
  @ finding and fixing attempts to inject illicit content into the
  @ repository.</p>
  @
  @ <p>Click on the "rcvid" to show a list of specific artifacts received
  @ by a transaction.  After identifying illicit artifacts, remove them
  @ using the "Shun" button.  If an "rcvid" is not hyperlinked, that means
  @ all artifacts associated with that rcvid have already been shunned







>
|
|







413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
    "       EXISTS(SELECT 1 FROM rcvidSha1 WHERE x=rcvfrom.rcvid),"
    "       EXISTS(SELECT 1 FROM rcvidSha3 WHERE x=rcvfrom.rcvid)"
    "  FROM rcvfrom LEFT JOIN user USING(uid)"
    " ORDER BY rcvid DESC LIMIT %d OFFSET %d",
    showAll ? -1 : perScreen+1, ofst
  );
  @ <p>Whenever new artifacts are added to the repository, either by
  @ push or using the web interface or by "fossil commit" or similar,
  @ an entry is made in the RCVFROM table
  @ to record the source of those artifacts.  This log facilitates
  @ finding and fixing attempts to inject illicit content into the
  @ repository.</p>
  @
  @ <p>Click on the "rcvid" to show a list of specific artifacts received
  @ by a transaction.  After identifying illicit artifacts, remove them
  @ using the "Shun" button.  If an "rcvid" is not hyperlinked, that means
  @ all artifacts associated with that rcvid have already been shunned
Changes to src/sitemap.c.
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297

298
299
300
301
302
303
304
  }
  if( !isPopup ){
    style_header("Test Page Map");
    style_adunit_config(ADUNIT_RIGHT_OK);
  }
  @ <ul id="sitemap" class="columns" style="column-width:20em">
  if( g.perm.Admin || db_get_boolean("test_env_enable",0) ){
    @ <li>%z(href("%R/test_env"))CGI Environment Test</a></li>
  }
  if( g.perm.Read ){
    @ <li>%z(href("%R/test-rename-list"))List of file renames</a></li>
  }
  @ <li>%z(href("%R/test-builtin-files"))List of built-in files</a></li>
  @ <li>%z(href("%R/mimetype_list"))List of MIME types</a></li>
  @ <li>%z(href("%R/hash-color-test"))Hash color test</a>

  if( g.perm.Admin ){
    @ <li>%z(href("%R/test-backlinks"))List of backlinks</a></li>
    @ <li>%z(href("%R/test-backlink-timeline"))Backlink timeline</a></li>
    @ <li>%z(href("%R/phantoms"))List of phantom artifacts</a></li>
    @ <li>%z(href("%R/test-warning"))Error Log test page</a></li>
    @ <li>%z(href("%R/repo_stat1"))Repository <tt>sqlite_stat1</tt> table</a>
    @ <li>%z(href("%R/repo_schema"))Repository schema</a></li>







|







>







283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
  }
  if( !isPopup ){
    style_header("Test Page Map");
    style_adunit_config(ADUNIT_RIGHT_OK);
  }
  @ <ul id="sitemap" class="columns" style="column-width:20em">
  if( g.perm.Admin || db_get_boolean("test_env_enable",0) ){
    @ <li>%z(href("%R/test-env"))CGI Environment Test</a></li>
  }
  if( g.perm.Read ){
    @ <li>%z(href("%R/test-rename-list"))List of file renames</a></li>
  }
  @ <li>%z(href("%R/test-builtin-files"))List of built-in files</a></li>
  @ <li>%z(href("%R/mimetype_list"))List of MIME types</a></li>
  @ <li>%z(href("%R/hash-color-test"))Hash color test</a>
  @ <li>%z(href("%R/test-bgcolor"))Background color test</a>
  if( g.perm.Admin ){
    @ <li>%z(href("%R/test-backlinks"))List of backlinks</a></li>
    @ <li>%z(href("%R/test-backlink-timeline"))Backlink timeline</a></li>
    @ <li>%z(href("%R/phantoms"))List of phantom artifacts</a></li>
    @ <li>%z(href("%R/test-warning"))Error Log test page</a></li>
    @ <li>%z(href("%R/repo_stat1"))Repository <tt>sqlite_stat1</tt> table</a>
    @ <li>%z(href("%R/repo_schema"))Repository schema</a></li>
Changes to src/smtp.c.
154
155
156
157
158
159
160
161

162
163

164
165
166
167
168
169
170
struct SmtpSession {
  const char *zFrom;        /* Domain from which we are sending */
  const char *zDest;        /* Domain that will receive the email */
  char *zHostname;          /* Hostname of SMTP server for zDest */
  u32 smtpFlags;            /* Flags changing the operation */
  FILE *logFile;            /* Write session transcript to this log file */
  Blob *pTranscript;        /* Record session transcript here */
  int atEof;                /* True after connection closes */

  char *zErr;               /* Error message */
  Blob inbuf;               /* Input buffer */

};

/* Allowed values for SmtpSession.smtpFlags */
#define SMTP_TRACE_STDOUT   0x00001     /* Debugging info to console */
#define SMTP_TRACE_FILE     0x00002     /* Debugging info to logFile */
#define SMTP_TRACE_BLOB     0x00004     /* Record transcript */
#define SMTP_DIRECT         0x00008     /* Skip the MX lookup */







|
>


>







154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
struct SmtpSession {
  const char *zFrom;        /* Domain from which we are sending */
  const char *zDest;        /* Domain that will receive the email */
  char *zHostname;          /* Hostname of SMTP server for zDest */
  u32 smtpFlags;            /* Flags changing the operation */
  FILE *logFile;            /* Write session transcript to this log file */
  Blob *pTranscript;        /* Record session transcript here */
  int bOpen;                /* True if connection is Open */
  int bFatal;               /* Error is fatal.  Do not retry */
  char *zErr;               /* Error message */
  Blob inbuf;               /* Input buffer */
  UrlData url;              /* Address of the server */
};

/* Allowed values for SmtpSession.smtpFlags */
#define SMTP_TRACE_STDOUT   0x00001     /* Debugging info to console */
#define SMTP_TRACE_FILE     0x00002     /* Debugging info to logFile */
#define SMTP_TRACE_BLOB     0x00004     /* Record transcript */
#define SMTP_DIRECT         0x00008     /* Skip the MX lookup */
178
179
180
181
182
183
184






















185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244


245









246

247


248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
void smtp_session_free(SmtpSession *pSession){
  socket_close();
  blob_reset(&pSession->inbuf);
  fossil_free(pSession->zHostname);
  fossil_free(pSession->zErr);
  fossil_free(pSession);
}























/*
** Allocate a new SmtpSession object.
**
** Both zFrom and zDest must be specified.
**
** The ... arguments are in this order:
**
**    SMTP_PORT:            int
**    SMTP_TRACE_FILE:      FILE*
**    SMTP_TRACE_BLOB:      Blob*
*/
SmtpSession *smtp_session_new(
  const char *zFrom,    /* Domain for the client */
  const char *zDest,    /* Domain of the server */
  u32 smtpFlags,        /* Flags */
  ...                   /* Arguments depending on the flags */
){
  SmtpSession *p;
  va_list ap;
  UrlData url;

  p = fossil_malloc( sizeof(*p) );
  memset(p, 0, sizeof(*p));
  p->zFrom = zFrom;
  p->zDest = zDest;
  p->smtpFlags = smtpFlags;
  memset(&url, 0, sizeof(url));
  url.port = 25;
  blob_init(&p->inbuf, 0, 0);
  va_start(ap, smtpFlags);
  if( smtpFlags & SMTP_PORT ){
    url.port = va_arg(ap, int);
  }
  if( smtpFlags & SMTP_TRACE_FILE ){
    p->logFile = va_arg(ap, FILE*);
  }
  if( smtpFlags & SMTP_TRACE_BLOB ){
    p->pTranscript = va_arg(ap, Blob*);
  }
  va_end(ap);
  if( (smtpFlags & SMTP_DIRECT)!=0 ){
    int i;
    p->zHostname = fossil_strdup(zDest);
    for(i=0; p->zHostname[i] && p->zHostname[i]!=':'; i++){}
    if( p->zHostname[i]==':' ){
      p->zHostname[i] = 0;
      url.port = atoi(&p->zHostname[i+1]);
    }
  }else{
    p->zHostname = smtp_mx_host(zDest);
  }
  if( p->zHostname==0 ){
    p->atEof = 1;
    p->zErr = mprintf("cannot locate SMTP server for \"%s\"", zDest);
    return p;
  }
  url.name = p->zHostname;
  socket_global_init();
  if( socket_open(&url) ){


    p->atEof = 1;









    p->zErr = socket_errmsg();

    socket_close();


  }
  return p;
}

/*
** Send a single line of output the SMTP client to the server.
*/
static void smtp_send_line(SmtpSession *p, const char *zFormat, ...){
  Blob b = empty_blob;
  va_list ap;
  char *z;
  int n;
  if( p->atEof ) return;
  va_start(ap, zFormat);
  blob_vappendf(&b, zFormat, ap);
  va_end(ap);
  z = blob_buffer(&b);
  n = blob_size(&b);
  assert( n>=2 );
  assert( z[n-1]=='\n' );







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>




|
|
|

|
<
<





|


<
<






<
|

<

|

<
<
<
<
<
<
<






|





<
|


|

|
>
>
|
>
>
>
>
>
>
>
>
>
|
>
|
>
>

<










|







180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217


218
219
220
221
222
223
224
225


226
227
228
229
230
231

232
233

234
235
236







237
238
239
240
241
242
243
244
245
246
247
248

249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272

273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
void smtp_session_free(SmtpSession *pSession){
  socket_close();
  blob_reset(&pSession->inbuf);
  fossil_free(pSession->zHostname);
  fossil_free(pSession->zErr);
  fossil_free(pSession);
}

/*
** Set an error message on the SmtpSession
*/
static void smtp_set_error(
  SmtpSession *p,             /* The SMTP context */
  int bFatal,                 /* Fatal error.  Reset and retry is pointless */
  const char *zFormat,        /* Error message. */
  ...
){
  if( bFatal ) p->bFatal = 1;
  if( p->zErr==0 ){
    va_list ap;
    va_start(ap, zFormat);
    p->zErr = vmprintf(zFormat, ap);
    va_end(ap);
  }
  if( p->bOpen ){
    socket_close();
    p->bOpen = 0;
  }
}

/*
** Allocate a new SmtpSession object.
**
** Both zFrom and zDest must be specified.  smtpFlags may not contain
** either SMTP_TRACE_FILE or SMTP_TRACE_BLOB as those settings must be
** added by a subsequent call to smtp_session_config().
**
** The iPort option is ignored unless SMTP_PORT is set in smtpFlags


*/
SmtpSession *smtp_session_new(
  const char *zFrom,    /* Domain for the client */
  const char *zDest,    /* Domain of the server */
  u32 smtpFlags,        /* Flags */
  int iPort             /* TCP port if the SMTP_PORT flags is present */
){
  SmtpSession *p;



  p = fossil_malloc( sizeof(*p) );
  memset(p, 0, sizeof(*p));
  p->zFrom = zFrom;
  p->zDest = zDest;
  p->smtpFlags = smtpFlags;

  p->url.port = 25;
  blob_init(&p->inbuf, 0, 0);

  if( smtpFlags & SMTP_PORT ){
    p->url.port = iPort;
  }







  if( (smtpFlags & SMTP_DIRECT)!=0 ){
    int i;
    p->zHostname = fossil_strdup(zDest);
    for(i=0; p->zHostname[i] && p->zHostname[i]!=':'; i++){}
    if( p->zHostname[i]==':' ){
      p->zHostname[i] = 0;
      p->url.port = atoi(&p->zHostname[i+1]);
    }
  }else{
    p->zHostname = smtp_mx_host(zDest);
  }
  if( p->zHostname==0 ){

    smtp_set_error(p, 1, "cannot locate SMTP server for \"%s\"", zDest);
    return p;
  }
  p->url.name = p->zHostname;
  socket_global_init();
  p->bOpen = 0;
  return p;
}

/*
** Configure debugging options on SmtpSession.  Add all bits in
** smtpFlags to the settings.  The following bits can be added:
**
**    SMTP_FLAG_FILE:     In which case pArg is the FILE* pointer to use
**
**    SMTP_FLAG_BLOB:     In which case pArg is the Blob* poitner to use.
*/
void smtp_session_config(SmtpSession *p, u32 smtpFlags, void *pArg){
  p->smtpFlags = smtpFlags;
  if( smtpFlags & SMTP_TRACE_FILE ){
    p->logFile = (FILE*)pArg;
  }else if( smtpFlags & SMTP_TRACE_BLOB ){
    p->pTranscript = (Blob*)pArg;
  }

}

/*
** Send a single line of output the SMTP client to the server.
*/
static void smtp_send_line(SmtpSession *p, const char *zFormat, ...){
  Blob b = empty_blob;
  va_list ap;
  char *z;
  int n;
  if( !p->bOpen ) return;
  va_start(ap, zFormat);
  blob_vappendf(&b, zFormat, ap);
  va_end(ap);
  z = blob_buffer(&b);
  n = blob_size(&b);
  assert( n>=2 );
  assert( z[n-1]=='\n' );
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
static void smtp_recv_line(SmtpSession *p, Blob *in){
  int n = blob_size(&p->inbuf);
  char *z = blob_buffer(&p->inbuf);
  int i = blob_tell(&p->inbuf);
  int nDelay = 0;
  if( i<n && z[n-1]=='\n' ){
    blob_line(&p->inbuf, in);
  }else if( p->atEof ){
    blob_init(in, 0, 0);
  }else{
    if( n>0 && i>=n ){
      blob_truncate(&p->inbuf, 0);
      blob_rewind(&p->inbuf);
      n = 0;
    }







|







312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
static void smtp_recv_line(SmtpSession *p, Blob *in){
  int n = blob_size(&p->inbuf);
  char *z = blob_buffer(&p->inbuf);
  int i = blob_tell(&p->inbuf);
  int nDelay = 0;
  if( i<n && z[n-1]=='\n' ){
    blob_line(&p->inbuf, in);
  }else if( !p->bOpen ){
    blob_init(in, 0, 0);
  }else{
    if( n>0 && i>=n ){
      blob_truncate(&p->inbuf, 0);
      blob_rewind(&p->inbuf);
      n = 0;
    }
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
        z[n] = 0;
        if( n>0 && z[n-1]=='\n' ) break;
        if( got==1000 ) continue;
      }
      nDelay++;
      if( nDelay>100 ){
        blob_init(in, 0, 0);
        p->zErr = mprintf("timeout");
        socket_close();
        p->atEof = 1;
        return;
      }else{
        sqlite3_sleep(100);
      }
    }while( n<1 || z[n-1]!='\n' );
    blob_truncate(&p->inbuf, n);
    blob_line(&p->inbuf, in);







|
<
<







335
336
337
338
339
340
341
342


343
344
345
346
347
348
349
        z[n] = 0;
        if( n>0 && z[n-1]=='\n' ) break;
        if( got==1000 ) continue;
      }
      nDelay++;
      if( nDelay>100 ){
        blob_init(in, 0, 0);
        smtp_set_error(p, 1, "client times out waiting on server response");


        return;
      }else{
        sqlite3_sleep(100);
      }
    }while( n<1 || z[n-1]!='\n' );
    blob_truncate(&p->inbuf, n);
    blob_line(&p->inbuf, in);
352
353
354
355
356
357
358

359
360
361
362
363
364
365
  int *pbMore,      /* True if the reply is not complete */
  char **pzArg      /* Argument */
){
  int n;
  char *z;
  blob_truncate(in, 0);
  smtp_recv_line(p, in);

  z = blob_str(in);
  n = blob_size(in);
  if( z[0]=='#' ){
    *piCode = 0;
    *pbMore = 1;
    *pzArg = z;
  }else{







>







373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
  int *pbMore,      /* True if the reply is not complete */
  char **pzArg      /* Argument */
){
  int n;
  char *z;
  blob_truncate(in, 0);
  smtp_recv_line(p, in);
  blob_trim(in);
  z = blob_str(in);
  n = blob_size(in);
  if( z[0]=='#' ){
    *piCode = 0;
    *pbMore = 1;
    *pzArg = z;
  }else{
373
374
375
376
377
378
379

380
381
382
383
384
385

386
387
388
389
390
391
392
393
394
395
396
397
398
399






400
401
402
403

404
405
406
407
408
409
410
411

412
413
414


415
416
417
418
419
420
421
** Have the client send a QUIT message.
*/
int smtp_client_quit(SmtpSession *p){
  Blob in = BLOB_INITIALIZER;
  int iCode = 0;
  int bMore = 0;
  char *zArg = 0;

  smtp_send_line(p, "QUIT\r\n");
  do{
    smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg);
  }while( bMore );
  p->atEof = 1;
  socket_close();

  return 0;
}

/*
** Begin a client SMTP session.  Wait for the initial 220 then send
** the EHLO and wait for a 250.
**
** Return 0 on success and non-zero for a failure.
*/
int smtp_client_startup(SmtpSession *p){
  Blob in = BLOB_INITIALIZER;
  int iCode = 0;
  int bMore = 0;
  char *zArg = 0;






  do{
    smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg);
  }while( bMore );
  if( iCode!=220 ){

    smtp_client_quit(p);
    return 1;
  }
  smtp_send_line(p, "EHLO %s\r\n", p->zFrom);
  do{
    smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg);
  }while( bMore );
  if( iCode!=250 ){

    smtp_client_quit(p);
    return 1;
  }


  return 0;
}

/*
** COMMAND: test-smtp-probe
**
** Usage: %fossil test-smtp-probe DOMAIN [ME]







>
|
|
|
|
|
|
>









|




>
>
>
>
>
>




>








>



>
>







395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
** Have the client send a QUIT message.
*/
int smtp_client_quit(SmtpSession *p){
  Blob in = BLOB_INITIALIZER;
  int iCode = 0;
  int bMore = 0;
  char *zArg = 0;
  if( p->bOpen ){
    smtp_send_line(p, "QUIT\r\n");
    do{
      smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg);
    }while( bMore );
    p->bOpen = 0;
    socket_close();
  }
  return 0;
}

/*
** Begin a client SMTP session.  Wait for the initial 220 then send
** the EHLO and wait for a 250.
**
** Return 0 on success and non-zero for a failure.
*/
static int smtp_client_startup(SmtpSession *p){
  Blob in = BLOB_INITIALIZER;
  int iCode = 0;
  int bMore = 0;
  char *zArg = 0;
  if( p==0 || p->bFatal ) return 1;
  if( socket_open(&p->url) ){
    smtp_set_error(p, 1, "can't open socket: %z", socket_errmsg());
    return 1;
  }
  p->bOpen = 1;
  do{
    smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg);
  }while( bMore );
  if( iCode!=220 ){
    smtp_set_error(p, 1, "conversation begins with: \"%d %s\"",iCode,zArg);
    smtp_client_quit(p);
    return 1;
  }
  smtp_send_line(p, "EHLO %s\r\n", p->zFrom);
  do{
    smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg);
  }while( bMore );
  if( iCode!=250 ){
    smtp_set_error(p, 1, "reply to EHLO with: \"%d %s\"",iCode, zArg);
    smtp_client_quit(p);
    return 1;
  }
  fossil_free(p->zErr);
  p->zErr = 0;
  return 0;
}

/*
** COMMAND: test-smtp-probe
**
** Usage: %fossil test-smtp-probe DOMAIN [ME]
539
540
541
542
543
544
545




546
547
548
549
550



551
552
553
554
555
556



557
558
559
560
561
562



563
564
565
566
567
568
569
570
571
572
573
574
575
576




577
578
579
580
581
582
583
){
  int i;
  int iCode = 0;
  int bMore = 0;
  char *zArg = 0;
  Blob in;
  blob_init(&in, 0, 0);




  smtp_send_line(p, "MAIL FROM:<%s>\r\n", zFrom);
  do{
    smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg);
  }while( bMore );
  if( iCode!=250 ) return 1;



  for(i=0; i<nTo; i++){
    smtp_send_line(p, "RCPT TO:<%s>\r\n", azTo[i]);
    do{
      smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg);
    }while( bMore );
    if( iCode!=250 ) return 1;



  }
  smtp_send_line(p, "DATA\r\n");
  do{
    smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg);
  }while( bMore );
  if( iCode!=354 ) return 1;



  smtp_send_email_body(zMsg, socket_send, 0);
  if( p->smtpFlags & SMTP_TRACE_STDOUT ){
    fossil_print("C: # message content\nC: .\n");
  }
  if( p->smtpFlags & SMTP_TRACE_FILE ){
    fprintf(p->logFile, "C: # message content\nC: .\n");
  }
  if( p->smtpFlags & SMTP_TRACE_BLOB ){
    blob_appendf(p->pTranscript, "C: # message content\nC: .\n");
  }
  do{
    smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg);
  }while( bMore );
  if( iCode!=250 ) return 1;




  return 0;
}

/*
** The input is a base email address of the form "local@domain".
** Return a pointer to just the "domain" part, or 0 if the string
** contains no "@".







>
>
>
>




|
>
>
>





|
>
>
>





|
>
>
>













|
>
>
>
>







573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
){
  int i;
  int iCode = 0;
  int bMore = 0;
  char *zArg = 0;
  Blob in;
  blob_init(&in, 0, 0);
  if( !p->bOpen ){
    if( !p->bFatal ) smtp_client_startup(p);
    if( !p->bOpen ) return 1;
  }
  smtp_send_line(p, "MAIL FROM:<%s>\r\n", zFrom);
  do{
    smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg);
  }while( bMore );
  if( iCode!=250 ){
    smtp_set_error(p, 0,"reply to MAIL FROM: \"%d %s\"",iCode,zArg);
    return 1;
  }
  for(i=0; i<nTo; i++){
    smtp_send_line(p, "RCPT TO:<%s>\r\n", azTo[i]);
    do{
      smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg);
    }while( bMore );
    if( iCode!=250 ){
      smtp_set_error(p, 0,"reply to RCPT TO: \"%d %s\"",iCode,zArg);
      return 1;
    }
  }
  smtp_send_line(p, "DATA\r\n");
  do{
    smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg);
  }while( bMore );
  if( iCode!=354 ){
    smtp_set_error(p, 0, "reply to DATA with: \"%d %s\"",iCode,zArg);
    return 1;
  }
  smtp_send_email_body(zMsg, socket_send, 0);
  if( p->smtpFlags & SMTP_TRACE_STDOUT ){
    fossil_print("C: # message content\nC: .\n");
  }
  if( p->smtpFlags & SMTP_TRACE_FILE ){
    fprintf(p->logFile, "C: # message content\nC: .\n");
  }
  if( p->smtpFlags & SMTP_TRACE_BLOB ){
    blob_appendf(p->pTranscript, "C: # message content\nC: .\n");
  }
  do{
    smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg);
  }while( bMore );
  if( iCode!=250 ){
    smtp_set_error(p, 0, "reply to end-of-DATA with: \"%d %s\"",
                   iCode, zArg);
    return 1;
  }
  return 0;
}

/*
** The input is a base email address of the form "local@domain".
** Return a pointer to just the "domain" part, or 0 if the string
** contains no "@".
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
    zToDomain = domain_of_addr(azTo[0]);
  }
  p = smtp_session_new(zFromDomain, zToDomain, smtpFlags, smtpPort);
  if( p->zErr ){
    fossil_fatal("%s", p->zErr);
  }
  fossil_print("Connection to \"%s\"\n", p->zHostname);
  smtp_client_startup(p);
  smtp_send_msg(p, zFrom, nTo, azTo, blob_str(&body));
  smtp_client_quit(p);
  if( p->zErr ){
    fossil_fatal("ERROR: %s\n", p->zErr);
  }
  smtp_session_free(p);
  blob_reset(&body);
}







<








685
686
687
688
689
690
691

692
693
694
695
696
697
698
699
    zToDomain = domain_of_addr(azTo[0]);
  }
  p = smtp_session_new(zFromDomain, zToDomain, smtpFlags, smtpPort);
  if( p->zErr ){
    fossil_fatal("%s", p->zErr);
  }
  fossil_print("Connection to \"%s\"\n", p->zHostname);

  smtp_send_msg(p, zFrom, nTo, azTo, blob_str(&body));
  smtp_client_quit(p);
  if( p->zErr ){
    fossil_fatal("ERROR: %s\n", p->zErr);
  }
  smtp_session_free(p);
  blob_reset(&body);
}
Changes to src/sorttable.js.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* Javascript code that will enables sorting of the table.  This code is
** derived from
**
**     http://www.webtoolkit.info/sortable-html-table.html
**
** but with extensive modifications.
**
** All tables with class "sortable" are registered with the SortableTable()
** function.  Example:
**
**   <table class='sortable' data-column-types='tnkx' data-init-sort='2'>
**
** Column data types are determined by the data-column-types attribute of
** the table.  The value of data-column-types is a string where each 
** character of the string represents a datatype for one column in the
** table.
**
**       t      Sort by text
**       n      Sort numerically
**       k      Sort by the data-sortkey property
**       x      This column is not sortable













|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* Javascript code that will enables sorting of the table.  This code is
** derived from
**
**     http://www.webtoolkit.info/sortable-html-table.html
**
** but with extensive modifications.
**
** All tables with class "sortable" are registered with the SortableTable()
** function.  Example:
**
**   <table class='sortable' data-column-types='tnkx' data-init-sort='2'>
**
** Column data types are determined by the data-column-types attribute of
** the table.  The value of data-column-types is a string where each
** character of the string represents a datatype for one column in the
** table.
**
**       t      Sort by text
**       n      Sort numerically
**       k      Sort by the data-sortkey property
**       x      This column is not sortable
84
85
86
87
88
89
90


91
92
93
94
95
96
97
98


99
100
101
102
103
104
105
      var clsName = hdrCell.className.replace(/\s*\bsort\s*\w+/, '');
      clsName += ' sort ' + sortType;
      hdrCell.className = clsName;
    }
  }
  this.sortText = function(a,b) {
    var i = thisObject.sortIndex;


    aa = a.cells[i].textContent.replace(/^\W+/,'').toLowerCase();
    bb = b.cells[i].textContent.replace(/^\W+/,'').toLowerCase();
    if(aa<bb) return -1;
    if(aa==bb) return a.rowIndex-b.rowIndex;
    return 1;
  }
  this.sortReverseText = function(a,b) {
    var i = thisObject.sortIndex;


    aa = a.cells[i].textContent.replace(/^\W+/,'').toLowerCase();
    bb = b.cells[i].textContent.replace(/^\W+/,'').toLowerCase();
    if(aa<bb) return +1;
    if(aa==bb) return a.rowIndex-b.rowIndex;
    return -1;
  }
  this.sortNumeric = function(a,b) {







>
>








>
>







84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
      var clsName = hdrCell.className.replace(/\s*\bsort\s*\w+/, '');
      clsName += ' sort ' + sortType;
      hdrCell.className = clsName;
    }
  }
  this.sortText = function(a,b) {
    var i = thisObject.sortIndex;
    if (a.cells.length<=i) return -1; /* see ticket 59d699710b1ab5d4 */
    if (b.cells.length<=i) return 1;
    aa = a.cells[i].textContent.replace(/^\W+/,'').toLowerCase();
    bb = b.cells[i].textContent.replace(/^\W+/,'').toLowerCase();
    if(aa<bb) return -1;
    if(aa==bb) return a.rowIndex-b.rowIndex;
    return 1;
  }
  this.sortReverseText = function(a,b) {
    var i = thisObject.sortIndex;
    if (a.cells.length<=i) return 1; /* see ticket 59d699710b1ab5d4 */
    if (b.cells.length<=i) return -1;
    aa = a.cells[i].textContent.replace(/^\W+/,'').toLowerCase();
    bb = b.cells[i].textContent.replace(/^\W+/,'').toLowerCase();
    if(aa<bb) return +1;
    if(aa==bb) return a.rowIndex-b.rowIndex;
    return -1;
  }
  this.sortNumeric = function(a,b) {
Changes to src/stash.c.
257
258
259
260
261
262
263

264
265
266
267
268
269
270
*/
static int stash_create(void){
  const char *zComment;              /* Comment to add to the stash */
  int stashid;                       /* ID of the new stash */
  int vid;                           /* Current check-out */

  zComment = find_option("comment", "m", 1);

  verify_all_options();
  if( zComment==0 ){
    Blob prompt;                       /* Prompt for stash comment */
    Blob comment;                      /* User comment reply */
#if defined(_WIN32) || defined(__CYGWIN__)
    int bomSize;
    const unsigned char *bom = get_utf8_bom(&bomSize);







>







257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
*/
static int stash_create(void){
  const char *zComment;              /* Comment to add to the stash */
  int stashid;                       /* ID of the new stash */
  int vid;                           /* Current check-out */

  zComment = find_option("comment", "m", 1);
  (void)fossil_text_editor();
  verify_all_options();
  if( zComment==0 ){
    Blob prompt;                       /* Prompt for stash comment */
    Blob comment;                      /* User comment reply */
#if defined(_WIN32) || defined(__CYGWIN__)
    int bomSize;
    const unsigned char *bom = get_utf8_bom(&bomSize);
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521





522
523
524
525
526
527
528

/*
** COMMAND: stash
**
** Usage: %fossil stash SUBCOMMAND ARGS...
**
** > fossil stash
** > fossil stash save ?-m|--comment COMMENT? ?FILES...?
** > fossil stash snapshot ?-m|--comment COMMENT? ?FILES...?
**
**      Save the current changes in the working tree as a new stash.
**      Then revert the changes back to the last check-in.  If FILES
**      are listed, then only stash and revert the named files.  The
**      "save" verb can be omitted if and only if there are no other
**      arguments.  The "snapshot" verb works the same as "save" but
**      omits the revert, keeping the check-out unchanged.





**
** > fossil stash list|ls ?-v|--verbose? ?-W|--width NUM?
**
**      List all changes sets currently stashed.  Show information about
**      individual files in each changeset if -v or --verbose is used.
**
** > fossil stash show|cat ?STASHID? ?DIFF-OPTIONS?







|
|







>
>
>
>
>







507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534

/*
** COMMAND: stash
**
** Usage: %fossil stash SUBCOMMAND ARGS...
**
** > fossil stash
** > fossil stash save ?FILES...?
** > fossil stash snapshot ?FILES...?
**
**      Save the current changes in the working tree as a new stash.
**      Then revert the changes back to the last check-in.  If FILES
**      are listed, then only stash and revert the named files.  The
**      "save" verb can be omitted if and only if there are no other
**      arguments.  The "snapshot" verb works the same as "save" but
**      omits the revert, keeping the check-out unchanged.
**
**      Options:
**         --editor NAME                  Use the NAME editor to enter comment
**         -m|--comment COMMENT           Comment text for the new stash
**
**
** > fossil stash list|ls ?-v|--verbose? ?-W|--width NUM?
**
**      List all changes sets currently stashed.  Show information about
**      individual files in each changeset if -v or --verbose is used.
**
** > fossil stash show|cat ?STASHID? ?DIFF-OPTIONS?
Changes to src/stat.c.
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
  style_submenu_element("Activity Reports", "reports");
  style_submenu_element("Hash Collisions", "hash-collisions");
  style_submenu_element("Artifacts", "bloblist");
  if( sqlite3_compileoption_used("ENABLE_DBSTAT_VTAB") ){
    style_submenu_element("Table Sizes", "repo-tabsize");
  }
  if( g.perm.Admin || g.perm.Setup || db_get_boolean("test_env_enable",0) ){
    style_submenu_element("Environment", "test_env");
  }
  @ <table class="label-value">
  fsize = file_size(g.zRepositoryName, ExtFILE);
  @ <tr><th>Repository&nbsp;Size:</th><td>%,lld(fsize) bytes</td>
  @ </td></tr>
  if( !brief ){
    @ <tr><th>Number&nbsp;Of&nbsp;Artifacts:</th><td>







|







164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
  style_submenu_element("Activity Reports", "reports");
  style_submenu_element("Hash Collisions", "hash-collisions");
  style_submenu_element("Artifacts", "bloblist");
  if( sqlite3_compileoption_used("ENABLE_DBSTAT_VTAB") ){
    style_submenu_element("Table Sizes", "repo-tabsize");
  }
  if( g.perm.Admin || g.perm.Setup || db_get_boolean("test_env_enable",0) ){
    style_submenu_element("Environment", "test-env");
  }
  @ <table class="label-value">
  fsize = file_size(g.zRepositoryName, ExtFILE);
  @ <tr><th>Repository&nbsp;Size:</th><td>%,lld(fsize) bytes</td>
  @ </td></tr>
  if( !brief ){
    @ <tr><th>Number&nbsp;Of&nbsp;Artifacts:</th><td>
Changes to src/statrep.c.
857
858
859
860
861
862
863



864
865
866
867
868
869
870
**                        * m   (merge check-in),
**                        * n   (non-merge check-in)
**                        * f   (forum post)
**                        * w   (wiki page change)
**                        * t   (ticket change)
**                        * g   (tag added or removed)
**                     Defaulting to all event types.



**
** The view-specific query parameters include:
**
** view=byweek:
**
**   y=YYYY            The year to report (default is the server's
**                     current year).







>
>
>







857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
**                        * m   (merge check-in),
**                        * n   (non-merge check-in)
**                        * f   (forum post)
**                        * w   (wiki page change)
**                        * t   (ticket change)
**                        * g   (tag added or removed)
**                     Defaulting to all event types.
**   from=DATETIME     Consider only events after this timestamp (requires to)
**   to=DATETIME       Consider only events before this timestamp (requires from)
**
**
** The view-specific query parameters include:
**
** view=byweek:
**
**   y=YYYY            The year to report (default is the server's
**                     current year).
Changes to src/style.c.
742
743
744
745
746
747
748

749
750
751
752
753
754
755
756
757
758
  ** Do not overwrite the TH1 variable "default_csp" if it exists, as this
  ** allows it to be properly overridden via the TH1 setup script (i.e. it
  ** is evaluated before the header is rendered).
  */
  Th_MaybeStore("default_csp", zDfltCsp);
  fossil_free(zDfltCsp);
  Th_Store("nonce", zNonce);

  Th_Store("project_name", db_get("project-name","Unnamed Fossil Project"));
  Th_Store("project_description", db_get("project-description",""));
  if( zTitle ) Th_Store("title", zTitle);
  Th_Store("baseurl", g.zBaseURL);
  Th_Store("secureurl", fossil_wants_https(1)? g.zHttpsURL: g.zBaseURL);
  Th_Store("home", g.zTop);
  Th_Store("index_page", db_get("index-page","/home"));
  if( local_zCurrentPage==0 ) style_set_current_page("%T", g.zPath);
  Th_Store("current_page", local_zCurrentPage);
  if( g.zPath ){                /* store the first segment of a path; */







>
|
|
|







742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
  ** Do not overwrite the TH1 variable "default_csp" if it exists, as this
  ** allows it to be properly overridden via the TH1 setup script (i.e. it
  ** is evaluated before the header is rendered).
  */
  Th_MaybeStore("default_csp", zDfltCsp);
  fossil_free(zDfltCsp);
  Th_Store("nonce", zNonce);
  Th_StoreUnsafe("project_name",
                 db_get("project-name","Unnamed Fossil Project"));
  Th_StoreUnsafe("project_description", db_get("project-description",""));
  if( zTitle ) Th_Store("title", html_lookalike(zTitle,-1));
  Th_Store("baseurl", g.zBaseURL);
  Th_Store("secureurl", fossil_wants_https(1)? g.zHttpsURL: g.zBaseURL);
  Th_Store("home", g.zTop);
  Th_Store("index_page", db_get("index-page","/home"));
  if( local_zCurrentPage==0 ) style_set_current_page("%T", g.zPath);
  Th_Store("current_page", local_zCurrentPage);
  if( g.zPath ){                /* store the first segment of a path; */
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
  Th_Store("manifest_date", MANIFEST_DATE);
  Th_Store("compiler_name", COMPILER_NAME);
  Th_Store("mainmenu", style_get_mainmenu());
  stylesheet_url_var();
  image_url_var("logo");
  image_url_var("background");
  if( !login_is_nobody() ){
    Th_Store("login", g.zLogin);
  }
  Th_MaybeStore("current_feature", feature_from_page_path(local_zCurrentPage) );
  if( g.ftntsIssues[0] || g.ftntsIssues[1] ||
      g.ftntsIssues[2] || g.ftntsIssues[3] ){
    char buf[80];
    sqlite3_snprintf(sizeof(buf), buf, "%i %i %i %i", g.ftntsIssues[0],
                     g.ftntsIssues[1], g.ftntsIssues[2], g.ftntsIssues[3]);







|







771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
  Th_Store("manifest_date", MANIFEST_DATE);
  Th_Store("compiler_name", COMPILER_NAME);
  Th_Store("mainmenu", style_get_mainmenu());
  stylesheet_url_var();
  image_url_var("logo");
  image_url_var("background");
  if( !login_is_nobody() ){
    Th_Store("login", html_lookalike(g.zLogin,-1));
  }
  Th_MaybeStore("current_feature", feature_from_page_path(local_zCurrentPage) );
  if( g.ftntsIssues[0] || g.ftntsIssues[1] ||
      g.ftntsIssues[2] || g.ftntsIssues[3] ){
    char buf[80];
    sqlite3_snprintf(sizeof(buf), buf, "%i %i %i %i", g.ftntsIssues[0],
                     g.ftntsIssues[1], g.ftntsIssues[2], g.ftntsIssues[3]);
1380
1381
1382
1383
1384
1385
1386

1387
1388
1389
1390
1391
1392
1393
1394
  @ Title: <input type="text" size="50" name="title" value="%h(zTitle)">
  @ <input type="submit" value="Submit">
  @ </form>
  style_finish_page();
}

/*

** WEBPAGE: test_env
**
** Display CGI-variables and other aspects of the run-time
** environment, for debugging and trouble-shooting purposes.
*/
void page_test_env(void){
  webpage_error("");
}







>
|







1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
  @ Title: <input type="text" size="50" name="title" value="%h(zTitle)">
  @ <input type="submit" value="Submit">
  @ </form>
  style_finish_page();
}

/*
** WEBPAGE: test-env
** WEBPAGE: test_env  alias
**
** Display CGI-variables and other aspects of the run-time
** environment, for debugging and trouble-shooting purposes.
*/
void page_test_env(void){
  webpage_error("");
}
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
** query parameters can jump to this routine to render an error
** message screen.
**
** For administators, or if the test_env_enable setting is true, then
** details of the request environment are displayed.  Otherwise, just
** the error message is shown.
**
** If zFormat is an empty string, then this is the /test_env page.
*/
void webpage_error(const char *zFormat, ...){
  int showAll = 0;
  char *zErr = 0;
  int isAuth = 0;
  char zCap[100];








|







1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
** query parameters can jump to this routine to render an error
** message screen.
**
** For administators, or if the test_env_enable setting is true, then
** details of the request environment are displayed.  Otherwise, just
** the error message is shown.
**
** If zFormat is an empty string, then this is the /test-env page.
*/
void webpage_error(const char *zFormat, ...){
  int showAll = 0;
  char *zErr = 0;
  int isAuth = 0;
  char zCap[100];

1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
        blob_zero(&t);
      }
    }
    @ <hr>
    P("HTTP_USER_AGENT");
    P("SERVER_SOFTWARE");
    cgi_print_all(showAll, 0, 0);
    @ <p><form method="POST" action="%R/test_env">
    @ <input type="hidden" name="showall" value="%d(showAll)">
    @ <input type="submit" name="post-test-button" value="POST Test">
    @ </form>
    if( showAll && blob_size(&g.httpHeader)>0 ){
      @ <hr>
      @ <pre>
      @ %h(blob_str(&g.httpHeader))







|







1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
        blob_zero(&t);
      }
    }
    @ <hr>
    P("HTTP_USER_AGENT");
    P("SERVER_SOFTWARE");
    cgi_print_all(showAll, 0, 0);
    @ <p><form method="POST" action="%R/test-env">
    @ <input type="hidden" name="showall" value="%d(showAll)">
    @ <input type="submit" name="post-test-button" value="POST Test">
    @ </form>
    if( showAll && blob_size(&g.httpHeader)>0 ){
      @ <hr>
      @ <pre>
      @ %h(blob_str(&g.httpHeader))
Changes to src/style.chat.css.
211
212
213
214
215
216
217









218
219
220
221
222
223
224
  overflow: auto;
  padding: 0 0.25em;
}
body.chat #chat-messages-wrapper.loading > * {
  /* An attempt at reducing flicker when loading lots of messages. */
  visibility: hidden;
}









body.chat div.content {
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column-reverse;
  /* ^^^^ In order to get good automatic scrolling of new messages on
     the BOTTOM in bottom-up chat mode, such that they scroll up







>
>
>
>
>
>
>
>
>







211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
  overflow: auto;
  padding: 0 0.25em;
}
body.chat #chat-messages-wrapper.loading > * {
  /* An attempt at reducing flicker when loading lots of messages. */
  visibility: hidden;
}

/* Provide a visual cue when polling is offline. */
body.chat.connection-error #chat-input-line-wrapper {
  border-top: medium dotted red;
}
body.chat.fossil-dark-style.connection-error #chat-input-line-wrapper {
  border-color: yellow;
}

body.chat div.content {
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column-reverse;
  /* ^^^^ In order to get good automatic scrolling of new messages on
     the BOTTOM in bottom-up chat mode, such that they scroll up
239
240
241
242
243
244
245

246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
}
body.chat:not(.chat-only-mode) #chat-input-area{
  /* Safari user reports that 2em is necessary to keep the file selection
     widget from overlapping the page footer, whereas a margin of 0 is fine
     for FF/Chrome (and 2em is a *huge* waste of space for those). */
  margin-bottom: 0;
}

.chat-input-field {
  flex: 10 1 auto;
  margin: 0;
}
#chat-input-field-x,
#chat-input-field-multi {
  overflow: auto;
  resize: vertical;
}
#chat-input-field-x {
  display: inline-block/*supposed workaround for Chrome weirdness*/;
  padding: 0.2em;
  background-color: rgba(156,156,156,0.3);
  white-space: pre-wrap;
  /* ^^^ Firefox, when pasting plain text into a contenteditable field,
     loses all newlines unless we explicitly set this. Chrome does not. */
  cursor: text;
  /* ^^^ In some browsers the cursor may not change for a contenteditable
     element until it has focus, causing potential confusion. */
}
#chat-input-field-x:empty::before {
  content: attr(data-placeholder);
  opacity: 0.6;
}
.chat-input-field:not(:focus){
  border-width: 1px;
  border-style: solid;
  border-radius: 0.25em;
}
.chat-input-field:focus{
  /* This transparent border helps avoid the text shifting around
     when the contenteditable attribute causes a border (which we
     apparently cannot style) to be added. */
  border-width: 1px;
  border-style: solid;
  border-color: transparent;
  border-radius: 0.25em;







>
|



|
|



|










|



|




|







248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
}
body.chat:not(.chat-only-mode) #chat-input-area{
  /* Safari user reports that 2em is necessary to keep the file selection
     widget from overlapping the page footer, whereas a margin of 0 is fine
     for FF/Chrome (and 2em is a *huge* waste of space for those). */
  margin-bottom: 0;
}

body.chat .chat-input-field {
  flex: 10 1 auto;
  margin: 0;
}
body.chat #chat-input-field-x,
body.chat #chat-input-field-multi {
  overflow: auto;
  resize: vertical;
}
body.chat #chat-input-field-x {
  display: inline-block/*supposed workaround for Chrome weirdness*/;
  padding: 0.2em;
  background-color: rgba(156,156,156,0.3);
  white-space: pre-wrap;
  /* ^^^ Firefox, when pasting plain text into a contenteditable field,
     loses all newlines unless we explicitly set this. Chrome does not. */
  cursor: text;
  /* ^^^ In some browsers the cursor may not change for a contenteditable
     element until it has focus, causing potential confusion. */
}
body.chat #chat-input-field-x:empty::before {
  content: attr(data-placeholder);
  opacity: 0.6;
}
body.chat .chat-input-field:not(:focus){
  border-width: 1px;
  border-style: solid;
  border-radius: 0.25em;
}
body.chat .chat-input-field:focus{
  /* This transparent border helps avoid the text shifting around
     when the contenteditable attribute causes a border (which we
     apparently cannot style) to be added. */
  border-width: 1px;
  border-style: solid;
  border-color: transparent;
  border-radius: 0.25em;
Changes to src/th.c.
1
2
3
4
5
6
7
8
9
10
11






12
13
14
15
16
17
18

/*
** The implementation of the TH core. This file contains the parser, and
** the implementation of the interface in th.h.
*/

#include "config.h"
#include "th.h"
#include <string.h>
#include <assert.h>







/*
** Values used for element values in the tcl_platform array.
*/

#if !defined(TH_ENGINE)
#  define TH_ENGINE          "TH1"
#endif











>
>
>
>
>
>







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

/*
** The implementation of the TH core. This file contains the parser, and
** the implementation of the interface in th.h.
*/

#include "config.h"
#include "th.h"
#include <string.h>
#include <assert.h>

/*
** External routines
*/
void fossil_panic(const char*,...);
void fossil_errorlog(const char*,...);

/*
** Values used for element values in the tcl_platform array.
*/

#if !defined(TH_ENGINE)
#  define TH_ENGINE          "TH1"
#endif
195
196
197
198
199
200
201

202
203
204
205
206
207
208
209
210
211
212
213








214
215
216
217
218
219
220
221
222
223
224
225

226
227
228

229
230
231
232
233

234
235
236
237
238
239

240
241
242
243
244
245
246

247
248
249
250
251
252
253

254
255
256
257
258
259
260
** The Buffer structure and the thBufferXXX() functions are used to make
** memory allocation easier when building up a result.
*/
struct Buffer {
  char *zBuf;
  int nBuf;
  int nBufAlloc;

};
typedef struct Buffer Buffer;
static void thBufferInit(Buffer *);
static void thBufferFree(Th_Interp *interp, Buffer *);

/*
** This version of memcpy() allows the first and second argument to
** be NULL as long as the number of bytes to copy is zero.
*/
static void th_memcpy(void *dest, const void *src, size_t n){
  if( n>0 ) memcpy(dest,src,n);
}









/*
** Append nAdd bytes of content copied from zAdd to the end of buffer
** pBuffer. If there is not enough space currently allocated, resize
** the allocation to make space.
*/
static void thBufferWriteResize(
  Th_Interp *interp,
  Buffer *pBuffer,
  const char *zAdd,
  int nAdd
){

  int nNew = (pBuffer->nBuf+nAdd)*2+32;
#if defined(TH_MEMDEBUG)
  char *zNew = (char *)Th_Malloc(interp, nNew);

  th_memcpy(zNew, pBuffer->zBuf, pBuffer->nBuf);
  Th_Free(interp, pBuffer->zBuf);
  pBuffer->zBuf = zNew;
#else
  int nOld = pBuffer->nBufAlloc;

  pBuffer->zBuf = Th_Realloc(interp, pBuffer->zBuf, nNew);
  memset(pBuffer->zBuf+nOld, 0, nNew-nOld);
#endif
  pBuffer->nBufAlloc = nNew;
  th_memcpy(&pBuffer->zBuf[pBuffer->nBuf], zAdd, nAdd);
  pBuffer->nBuf += nAdd;

}
static void thBufferWriteFast(
  Th_Interp *interp,
  Buffer *pBuffer,
  const char *zAdd,
  int nAdd
){

  if( pBuffer->nBuf+nAdd > pBuffer->nBufAlloc ){
    thBufferWriteResize(interp, pBuffer, zAdd, nAdd);
  }else{
    if( pBuffer->zBuf ){
      memcpy(pBuffer->zBuf + pBuffer->nBuf, zAdd, nAdd);
    }
    pBuffer->nBuf += nAdd;

  }
}
#define thBufferWrite(a,b,c,d) thBufferWriteFast(a,b,(const char *)c,d)

/*
** Add a single character to a buffer
*/







>












>
>
>
>
>
>
>
>










|

>



>





>






>





|

>

|





>







201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
** The Buffer structure and the thBufferXXX() functions are used to make
** memory allocation easier when building up a result.
*/
struct Buffer {
  char *zBuf;
  int nBuf;
  int nBufAlloc;
  int bTaint;
};
typedef struct Buffer Buffer;
static void thBufferInit(Buffer *);
static void thBufferFree(Th_Interp *interp, Buffer *);

/*
** This version of memcpy() allows the first and second argument to
** be NULL as long as the number of bytes to copy is zero.
*/
static void th_memcpy(void *dest, const void *src, size_t n){
  if( n>0 ) memcpy(dest,src,n);
}

/*
** An oversized string has been encountered.  Do not try to recover.
** Panic the process.
*/
void Th_OversizeString(void){
  fossil_panic("string too large. maximum size 286MB.");
}

/*
** Append nAdd bytes of content copied from zAdd to the end of buffer
** pBuffer. If there is not enough space currently allocated, resize
** the allocation to make space.
*/
static void thBufferWriteResize(
  Th_Interp *interp,
  Buffer *pBuffer,
  const char *zAdd,
  int nAddX
){
  int nAdd = TH1_LEN(nAddX);
  int nNew = (pBuffer->nBuf+nAdd)*2+32;
#if defined(TH_MEMDEBUG)
  char *zNew = (char *)Th_Malloc(interp, nNew);
  TH1_SIZECHECK(nNew);
  th_memcpy(zNew, pBuffer->zBuf, pBuffer->nBuf);
  Th_Free(interp, pBuffer->zBuf);
  pBuffer->zBuf = zNew;
#else
  int nOld = pBuffer->nBufAlloc;
  TH1_SIZECHECK(nNew);
  pBuffer->zBuf = Th_Realloc(interp, pBuffer->zBuf, nNew);
  memset(pBuffer->zBuf+nOld, 0, nNew-nOld);
#endif
  pBuffer->nBufAlloc = nNew;
  th_memcpy(&pBuffer->zBuf[pBuffer->nBuf], zAdd, nAdd);
  pBuffer->nBuf += nAdd;
  TH1_XFER_TAINT(pBuffer->bTaint, nAddX);
}
static void thBufferWriteFast(
  Th_Interp *interp,
  Buffer *pBuffer,
  const char *zAdd,
  int nAddX
){
  int nAdd = TH1_LEN(nAddX);
  if( pBuffer->nBuf+nAdd > pBuffer->nBufAlloc ){
    thBufferWriteResize(interp, pBuffer, zAdd, nAddX);
  }else{
    if( pBuffer->zBuf ){
      memcpy(pBuffer->zBuf + pBuffer->nBuf, zAdd, nAdd);
    }
    pBuffer->nBuf += nAdd;
    TH1_XFER_TAINT(pBuffer->bTaint, nAddX);
  }
}
#define thBufferWrite(a,b,c,d) thBufferWriteFast(a,b,(const char *)c,d)

/*
** Add a single character to a buffer
*/
702
703
704
705
706
707
708

709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
  Th_Interp *interp,
  const char *zWord,
  int nWord
){
  int rc = TH_OK;
  Buffer output;
  int i;


  thBufferInit(&output);

  if( nWord>1 && (zWord[0]=='{' && zWord[nWord-1]=='}') ){
    thBufferWrite(interp, &output, &zWord[1], nWord-2);
  }else{

    /* If the word is surrounded by double-quotes strip these away. */
    if( nWord>1 && (zWord[0]=='"' && zWord[nWord-1]=='"') ){
      zWord++;
      nWord -= 2;
    }

    for(i=0; rc==TH_OK && i<nWord; i++){
      int nGet;

      int (*xGet)(Th_Interp *, const char*, int, int *) = 0;
      int (*xSubst)(Th_Interp *, const char*, int) = 0;

      switch( zWord[i] ){
        case '\\':







>



|
|



|

|


|







723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
  Th_Interp *interp,
  const char *zWord,
  int nWord
){
  int rc = TH_OK;
  Buffer output;
  int i;
  int nn = TH1_LEN(nWord);

  thBufferInit(&output);

  if( nn>1 && (zWord[0]=='{' && zWord[nn-1]=='}') ){
    thBufferWrite(interp, &output, &zWord[1], nn-2);
  }else{

    /* If the word is surrounded by double-quotes strip these away. */
    if( nn>1 && (zWord[0]=='"' && zWord[nn-1]=='"') ){
      zWord++;
      nn -= 2;
    }

    for(i=0; rc==TH_OK && i<nn; i++){
      int nGet;

      int (*xGet)(Th_Interp *, const char*, int, int *) = 0;
      int (*xSubst)(Th_Interp *, const char*, int) = 0;

      switch( zWord[i] ){
        case '\\':
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
          }
        default: {
          thBufferAddChar(interp, &output, zWord[i]);
          continue; /* Go to the next iteration of the for(...) loop */
        }
      }

      rc = xGet(interp, &zWord[i], nWord-i, &nGet);
      if( rc==TH_OK ){
        rc = xSubst(interp, &zWord[i], nGet);
      }
      if( rc==TH_OK ){
        const char *zRes;
        int nRes;
        zRes = Th_GetResult(interp, &nRes);
        thBufferWrite(interp, &output, zRes, nRes);
        i += (nGet-1);
      }
    }
  }

  if( rc==TH_OK ){
    Th_SetResult(interp, output.zBuf, output.nBuf);
  }
  thBufferFree(interp, &output);
  return rc;
}

/*
** Return true if one of the following is true of the buffer pointed







|














|







763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
          }
        default: {
          thBufferAddChar(interp, &output, zWord[i]);
          continue; /* Go to the next iteration of the for(...) loop */
        }
      }

      rc = xGet(interp, &zWord[i], nn-i, &nGet);
      if( rc==TH_OK ){
        rc = xSubst(interp, &zWord[i], nGet);
      }
      if( rc==TH_OK ){
        const char *zRes;
        int nRes;
        zRes = Th_GetResult(interp, &nRes);
        thBufferWrite(interp, &output, zRes, nRes);
        i += (nGet-1);
      }
    }
  }

  if( rc==TH_OK ){
    Th_SetResult(interp, output.zBuf, output.nBuf|output.bTaint);
  }
  thBufferFree(interp, &output);
  return rc;
}

/*
** Return true if one of the following is true of the buffer pointed
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
  int rc = TH_OK;

  Buffer strbuf;
  Buffer lenbuf;
  int nCount = 0;

  const char *zInput = zList;
  int nInput = nList;

  thBufferInit(&strbuf);
  thBufferInit(&lenbuf);

  while( nInput>0 ){
    const char *zWord;
    int nWord;

    thNextSpace(interp, zInput, nInput, &nWord);
    zInput += nWord;
    nInput = nList-(zInput-zList);

    if( TH_OK!=(rc = thNextWord(interp, zInput, nInput, &nWord, 0))
     || TH_OK!=(rc = thSubstWord(interp, zInput, nWord))
    ){
      goto finish;
    }
    zInput = &zInput[nWord];
    nInput = nList-(zInput-zList);
    if( nWord>0 ){
      zWord = Th_GetResult(interp, &nWord);
      thBufferWrite(interp, &strbuf, zWord, nWord);
      thBufferAddChar(interp, &strbuf, 0);
      thBufferWrite(interp, &lenbuf, &nWord, sizeof(int));
      nCount++;
    }







|










|






|
|







846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
  int rc = TH_OK;

  Buffer strbuf;
  Buffer lenbuf;
  int nCount = 0;

  const char *zInput = zList;
  int nInput = TH1_LEN(nList);

  thBufferInit(&strbuf);
  thBufferInit(&lenbuf);

  while( nInput>0 ){
    const char *zWord;
    int nWord;

    thNextSpace(interp, zInput, nInput, &nWord);
    zInput += nWord;
    nInput = TH1_LEN(nList)-(zInput-zList);

    if( TH_OK!=(rc = thNextWord(interp, zInput, nInput, &nWord, 0))
     || TH_OK!=(rc = thSubstWord(interp, zInput, nWord))
    ){
      goto finish;
    }
    zInput = &zInput[TH1_LEN(nWord)];
    nInput = TH1_LEN(nList)-(zInput-zList);
    if( nWord>0 ){
      zWord = Th_GetResult(interp, &nWord);
      thBufferWrite(interp, &strbuf, zWord, nWord);
      thBufferAddChar(interp, &strbuf, 0);
      thBufferWrite(interp, &lenbuf, &nWord, sizeof(int));
      nCount++;
    }
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
    );
    anElem = (int *)&azElem[nCount];
    zElem = (char *)&anElem[nCount];
    th_memcpy(anElem, lenbuf.zBuf, lenbuf.nBuf);
    th_memcpy(zElem, strbuf.zBuf, strbuf.nBuf);
    for(i=0; i<nCount;i++){
      azElem[i] = zElem;
      zElem += (anElem[i] + 1);
    }
    *pazElem = azElem;
    *panElem = anElem;
  }
  if( pnCount ){
    *pnCount = nCount;
  }







|







892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
    );
    anElem = (int *)&azElem[nCount];
    zElem = (char *)&anElem[nCount];
    th_memcpy(anElem, lenbuf.zBuf, lenbuf.nBuf);
    th_memcpy(zElem, strbuf.zBuf, strbuf.nBuf);
    for(i=0; i<nCount;i++){
      azElem[i] = zElem;
      zElem += (TH1_LEN(anElem[i]) + 1);
    }
    *pazElem = azElem;
    *panElem = anElem;
  }
  if( pnCount ){
    *pnCount = nCount;
  }
892
893
894
895
896
897
898
899
900





901
902
903
904
905
906
907
/*
** Evaluate the th1 script contained in the string (zProgram, nProgram)
** in the current stack frame.
*/
static int thEvalLocal(Th_Interp *interp, const char *zProgram, int nProgram){
  int rc = TH_OK;
  const char *zInput = zProgram;
  int nInput = nProgram;






  while( rc==TH_OK && nInput ){
    Th_HashEntry *pEntry;
    int nSpace;
    const char *zFirst;

    char **argv;
    int *argl;







|

>
>
>
>
>







914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
/*
** Evaluate the th1 script contained in the string (zProgram, nProgram)
** in the current stack frame.
*/
static int thEvalLocal(Th_Interp *interp, const char *zProgram, int nProgram){
  int rc = TH_OK;
  const char *zInput = zProgram;
  int nInput = TH1_LEN(nProgram);

  if( TH1_TAINTED(nProgram)
   && Th_ReportTaint(interp, "script", zProgram, nProgram)
  ){
    return TH_ERROR;
  }
  while( rc==TH_OK && nInput ){
    Th_HashEntry *pEntry;
    int nSpace;
    const char *zFirst;

    char **argv;
    int *argl;
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
    */
    rc = thSplitList(interp, zFirst, zInput-zFirst, &argv, &argl, &argc);
    if( rc!=TH_OK ) continue;

    if( argc>0 ){

      /* Look up the command name in the command hash-table. */
      pEntry = Th_HashFind(interp, interp->paCmd, argv[0], argl[0], 0);
      if( !pEntry ){
        Th_ErrorMessage(interp, "no such command: ", argv[0], argl[0]);
        rc = TH_ERROR;
      }

      /* Call the command procedure. */
      if( rc==TH_OK ){
        Th_Command *p = (Th_Command *)(pEntry->pData);
        const char **azArg = (const char **)argv;







|

|







974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
    */
    rc = thSplitList(interp, zFirst, zInput-zFirst, &argv, &argl, &argc);
    if( rc!=TH_OK ) continue;

    if( argc>0 ){

      /* Look up the command name in the command hash-table. */
      pEntry = Th_HashFind(interp, interp->paCmd, argv[0], TH1_LEN(argl[0]),0);
      if( !pEntry ){
        Th_ErrorMessage(interp, "no such command: ", argv[0], TH1_LEN(argl[0]));
        rc = TH_ERROR;
      }

      /* Call the command procedure. */
      if( rc==TH_OK ){
        Th_Command *p = (Th_Command *)(pEntry->pData);
        const char **azArg = (const char **)argv;
1051
1052
1053
1054
1055
1056
1057


1058
1059
1060
1061
1062
1063
1064
  if( !interp->pFrame ){
    rc = TH_ERROR;
  }else{
    int nInput = nProgram;

    if( nInput<0 ){
      nInput = th_strlen(zProgram);


    }
    rc = thEvalLocal(interp, zProgram, nInput);
  }

  interp->pFrame = pSavedFrame;
  return rc;
}







>
>







1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
  if( !interp->pFrame ){
    rc = TH_ERROR;
  }else{
    int nInput = nProgram;

    if( nInput<0 ){
      nInput = th_strlen(zProgram);
    }else{
      nInput = TH1_LEN(nInput);
    }
    rc = thEvalLocal(interp, zProgram, nInput);
  }

  interp->pFrame = pSavedFrame;
  return rc;
}
1093
1094
1095
1096
1097
1098
1099


1100
1101
1102
1103
1104
1105
1106
  const char *zInner = 0;
  int nInner = 0;
  int isGlobal = 0;
  int i;

  if( nVarname<0 ){
    nVarname = th_strlen(zVarname);


  }
  nOuter = nVarname;

  /* If the variable name starts with "::", then do the lookup is in the
  ** uppermost (global) frame.
  */
  if( nVarname>2 && zVarname[0]==':' && zVarname[1]==':' ){







>
>







1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
  const char *zInner = 0;
  int nInner = 0;
  int isGlobal = 0;
  int i;

  if( nVarname<0 ){
    nVarname = th_strlen(zVarname);
  }else{
    nVarname = TH1_LEN(nVarname);
  }
  nOuter = nVarname;

  /* If the variable name starts with "::", then do the lookup is in the
  ** uppermost (global) frame.
  */
  if( nVarname>2 && zVarname[0]==':' && zVarname[1]==':' ){
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
    Th_ErrorMessage(interp, "no such variable:", zVar, nVar);
    return TH_ERROR;
  }

  return Th_SetResult(interp, pValue->zData, pValue->nData);
}

/*
** If interp has a variable with the given name, its value is returned
** and its length is returned via *nOut if nOut is not NULL.  If
** interp has no such var then NULL is returned without setting any
** error state and *nOut, if not NULL, is set to -1. The returned value
** is owned by the interpreter and may be invalidated the next time
** the interpreter is modified.
*/
const char * Th_MaybeGetVar(Th_Interp *interp, const char *zVarName,
                            int *nOut){
  Th_Variable *pValue;

  pValue = thFindValue(interp, zVarName, -1, 0, 0, 1, 0);
  if( !pValue || !pValue->zData ){
    if( nOut!=0 ) *nOut = -1;
    return NULL;
  }
  if( nOut!=0 ) *nOut = pValue->nData;
  return pValue->zData;
}

/*
** Return true if variable (zVar, nVar) exists.
*/
int Th_ExistsVar(Th_Interp *interp, const char *zVar, int nVar){
  Th_Variable *pValue = thFindValue(interp, zVar, nVar, 0, 1, 1, 0);
  return pValue && (pValue->zData || pValue->pHash);
}







<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<







1300
1301
1302
1303
1304
1305
1306





















1307
1308
1309
1310
1311
1312
1313
    Th_ErrorMessage(interp, "no such variable:", zVar, nVar);
    return TH_ERROR;
  }

  return Th_SetResult(interp, pValue->zData, pValue->nData);
}






















/*
** Return true if variable (zVar, nVar) exists.
*/
int Th_ExistsVar(Th_Interp *interp, const char *zVar, int nVar){
  Th_Variable *pValue = thFindValue(interp, zVar, nVar, 0, 1, 1, 0);
  return pValue && (pValue->zData || pValue->pHash);
}
1322
1323
1324
1325
1326
1327
1328

1329

1330
1331
1332
1333
1334
1335
1336


1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
  Th_Interp *interp,
  const char *zVar,
  int nVar,
  const char *zValue,
  int nValue
){
  Th_Variable *pValue;



  pValue = thFindValue(interp, zVar, nVar, 1, 0, 0, 0);
  if( !pValue ){
    return TH_ERROR;
  }

  if( nValue<0 ){
    nValue = th_strlen(zValue);


  }
  if( pValue->zData ){
    Th_Free(interp, pValue->zData);
    pValue->zData = 0;
  }

  assert(zValue || nValue==0);
  pValue->zData = Th_Malloc(interp, nValue+1);
  pValue->zData[nValue] = '\0';
  th_memcpy(pValue->zData, zValue, nValue);
  pValue->nData = nValue;

  return TH_OK;
}

/*
** Create a variable link so that accessing variable (zLocal, nLocal) is







>

>






|
>
>






|
|
|
|







1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
  Th_Interp *interp,
  const char *zVar,
  int nVar,
  const char *zValue,
  int nValue
){
  Th_Variable *pValue;
  int nn;

  nVar = TH1_LEN(nVar);
  pValue = thFindValue(interp, zVar, nVar, 1, 0, 0, 0);
  if( !pValue ){
    return TH_ERROR;
  }

  if( nValue<0 ){
    nn = th_strlen(zValue);
  }else{
    nn = TH1_LEN(nValue);
  }
  if( pValue->zData ){
    Th_Free(interp, pValue->zData);
    pValue->zData = 0;
  }

  assert(zValue || nn==0);
  pValue->zData = Th_Malloc(interp, nn+1);
  pValue->zData[nn] = '\0';
  th_memcpy(pValue->zData, zValue, nn);
  pValue->nData = nValue;

  return TH_OK;
}

/*
** Create a variable link so that accessing variable (zLocal, nLocal) is
1456
1457
1458
1459
1460
1461
1462


1463
1464
1465
1466
1467
1468
1469
** caller is responsible for eventually calling Th_Free() to free
** the returned buffer.
*/
char *th_strdup(Th_Interp *interp, const char *z, int n){
  char *zRes;
  if( n<0 ){
    n = th_strlen(z);


  }
  zRes = Th_Malloc(interp, n+1);
  th_memcpy(zRes, z, n);
  zRes[n] = '\0';
  return zRes;
}








>
>







1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
** caller is responsible for eventually calling Th_Free() to free
** the returned buffer.
*/
char *th_strdup(Th_Interp *interp, const char *z, int n){
  char *zRes;
  if( n<0 ){
    n = th_strlen(z);
  }else{
    n = TH1_LEN(n);
  }
  zRes = Th_Malloc(interp, n+1);
  th_memcpy(zRes, z, n);
  zRes[n] = '\0';
  return zRes;
}

1517
1518
1519
1520
1521
1522
1523

1524
1525
1526
1527
1528
1529
1530
1531
1532
1533

  if( n<0 ){
    n = th_strlen(z);
  }

  if( z && n>0 ){
    char *zResult;

    zResult = Th_Malloc(pInterp, n+1);
    th_memcpy(zResult, z, n);
    zResult[n] = '\0';
    pInterp->zResult = zResult;
    pInterp->nResult = n;
  }

  return TH_OK;
}








>
|
|
|







1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550

  if( n<0 ){
    n = th_strlen(z);
  }

  if( z && n>0 ){
    char *zResult;
    int nn = TH1_LEN(n);
    zResult = Th_Malloc(pInterp, nn+1);
    th_memcpy(zResult, z, nn);
    zResult[nn] = '\0';
    pInterp->zResult = zResult;
    pInterp->nResult = n;
  }

  return TH_OK;
}

1775
1776
1777
1778
1779
1780
1781
1782
1783


1784
1785
1786


1787
1788
1789
1790
1791
1792
1793
  int i;

  int hasSpecialChar = 0;  /* Whitespace or {}[]'" */
  int hasEscapeChar = 0;   /* '}' without matching '{' to the left or a '\\' */
  int nBrace = 0;

  output.zBuf = *pzList;
  output.nBuf = *pnList;
  output.nBufAlloc = output.nBuf;



  if( nElem<0 ){
    nElem = th_strlen(zElem);


  }
  if( output.nBuf>0 ){
    thBufferAddChar(interp, &output, ' ');
  }

  for(i=0; i<nElem; i++){
    char c = zElem[i];







|

>
>



>
>







1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
  int i;

  int hasSpecialChar = 0;  /* Whitespace or {}[]'" */
  int hasEscapeChar = 0;   /* '}' without matching '{' to the left or a '\\' */
  int nBrace = 0;

  output.zBuf = *pzList;
  output.nBuf = TH1_LEN(*pnList);
  output.nBufAlloc = output.nBuf;
  output.bTaint = 0;
  TH1_XFER_TAINT(output.bTaint, *pnList);

  if( nElem<0 ){
    nElem = th_strlen(zElem);
  }else{
    nElem = TH1_LEN(nElem);
  }
  if( output.nBuf>0 ){
    thBufferAddChar(interp, &output, ' ');
  }

  for(i=0; i<nElem; i++){
    char c = zElem[i];
1832
1833
1834
1835
1836
1837
1838
1839

1840
1841
1842


1843
1844
1845

1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
  Th_Interp *interp,           /* Interpreter context */
  char **pzStr,                /* IN/OUT: Ptr to ptr to list */
  int *pnStr,                  /* IN/OUT: Current length of *pzStr */
  const char *zElem,           /* Data to append */
  int nElem                    /* Length of nElem */
){
  char *zNew;
  int nNew;


  if( nElem<0 ){
    nElem = th_strlen(zElem);


  }

  nNew = *pnStr + nElem;

  zNew = Th_Malloc(interp, nNew);
  th_memcpy(zNew, *pzStr, *pnStr);
  th_memcpy(&zNew[*pnStr], zElem, nElem);

  Th_Free(interp, *pzStr);
  *pzStr = zNew;
  *pnStr = nNew;

  return TH_OK;
}

/*
** Initialize an interpreter.
*/







|
>


|
>
>


|
>


|



|







1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
  Th_Interp *interp,           /* Interpreter context */
  char **pzStr,                /* IN/OUT: Ptr to ptr to list */
  int *pnStr,                  /* IN/OUT: Current length of *pzStr */
  const char *zElem,           /* Data to append */
  int nElem                    /* Length of nElem */
){
  char *zNew;
  long long int nNew;
  int nn;

  if( nElem<0 ){
    nn = th_strlen(zElem);
  }else{
    nn = TH1_LEN(nElem);
  }

  nNew = TH1_LEN(*pnStr) + nn;
  TH1_SIZECHECK(nNew);
  zNew = Th_Malloc(interp, nNew);
  th_memcpy(zNew, *pzStr, *pnStr);
  th_memcpy(&zNew[TH1_LEN(*pnStr)], zElem, nn);

  Th_Free(interp, *pzStr);
  *pzStr = zNew;
  *pnStr = (int)nNew;

  return TH_OK;
}

/*
** Initialize an interpreter.
*/
2104
2105
2106
2107
2108
2109
2110

2111
2112
2113
2114
2115
2116

2117
2118
2119
2120
2121
2122
2123
    char *zRight = 0; int nRight = 0;

    /* Evaluate left and right arguments, if they exist. */
    if( pExpr->pLeft ){
      rc = exprEval(interp, pExpr->pLeft);
      if( rc==TH_OK ){
        zLeft = Th_TakeResult(interp, &nLeft);

      }
    }
    if( rc==TH_OK && pExpr->pRight ){
      rc = exprEval(interp, pExpr->pRight);
      if( rc==TH_OK ){
        zRight = Th_TakeResult(interp, &nRight);

      }
    }

    /* Convert arguments to their required forms. */
    if( rc==TH_OK ){
      eArgType = pExpr->pOp->eArgType;
      if( eArgType==ARG_NUMBER ){







>






>







2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
    char *zRight = 0; int nRight = 0;

    /* Evaluate left and right arguments, if they exist. */
    if( pExpr->pLeft ){
      rc = exprEval(interp, pExpr->pLeft);
      if( rc==TH_OK ){
        zLeft = Th_TakeResult(interp, &nLeft);
        nLeft = TH1_LEN(nLeft);
      }
    }
    if( rc==TH_OK && pExpr->pRight ){
      rc = exprEval(interp, pExpr->pRight);
      if( rc==TH_OK ){
        zRight = Th_TakeResult(interp, &nRight);
        nRight = TH1_LEN(nRight);
      }
    }

    /* Convert arguments to their required forms. */
    if( rc==TH_OK ){
      eArgType = pExpr->pOp->eArgType;
      if( eArgType==ARG_NUMBER ){
2158
2159
2160
2161
2162
2163
2164
2165



2166
2167
2168
2169
2170
2171
2172
2173
            rc = TH_ERROR;
            goto finish;
          }
          iRes = iLeft%iRight;
          break;
        case OP_ADD:          iRes = iLeft+iRight;  break;
        case OP_SUBTRACT:     iRes = iLeft-iRight;  break;
        case OP_LEFTSHIFT:    iRes = iLeft<<iRight; break;



        case OP_RIGHTSHIFT:   iRes = iLeft>>iRight; break;
        case OP_LT:           iRes = iLeft<iRight;  break;
        case OP_GT:           iRes = iLeft>iRight;  break;
        case OP_LE:           iRes = iLeft<=iRight; break;
        case OP_GE:           iRes = iLeft>=iRight; break;
        case OP_EQ:           iRes = iLeft==iRight; break;
        case OP_NE:           iRes = iLeft!=iRight; break;
        case OP_BITWISE_AND:  iRes = iLeft&iRight;  break;







|
>
>
>
|







2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
            rc = TH_ERROR;
            goto finish;
          }
          iRes = iLeft%iRight;
          break;
        case OP_ADD:          iRes = iLeft+iRight;  break;
        case OP_SUBTRACT:     iRes = iLeft-iRight;  break;
        case OP_LEFTSHIFT: {
          iRes = (int)(((unsigned int)iLeft)<<(iRight&0x1f));
          break;
        }
        case OP_RIGHTSHIFT:   iRes = iLeft>>(iRight&0x1f); break;
        case OP_LT:           iRes = iLeft<iRight;  break;
        case OP_GT:           iRes = iLeft>iRight;  break;
        case OP_LE:           iRes = iLeft<=iRight; break;
        case OP_GE:           iRes = iLeft>=iRight; break;
        case OP_EQ:           iRes = iLeft==iRight; break;
        case OP_NE:           iRes = iLeft!=iRight; break;
        case OP_BITWISE_AND:  iRes = iLeft&iRight;  break;
2451
2452
2453
2454
2455
2456
2457


2458
2459
2460
2461
2462
2463
2464
  int i;                            /* Loop counter */

  int nToken = 0;
  Expr **apToken = 0;

  if( nExpr<0 ){
    nExpr = th_strlen(zExpr);


  }

  /* Parse the expression to a list of tokens. */
  rc = exprParse(interp, zExpr, nExpr, &apToken, &nToken);

  /* If the parsing was successful, create an expression tree from
  ** the parsed list of tokens. If successful, apToken[0] is set







>
>







2481
2482
2483
2484
2485
2486
2487
2488
2489
2490
2491
2492
2493
2494
2495
2496
  int i;                            /* Loop counter */

  int nToken = 0;
  Expr **apToken = 0;

  if( nExpr<0 ){
    nExpr = th_strlen(zExpr);
  }else{
    nExpr = TH1_LEN(nExpr);
  }

  /* Parse the expression to a list of tokens. */
  rc = exprParse(interp, zExpr, nExpr, &apToken, &nToken);

  /* If the parsing was successful, create an expression tree from
  ** the parsed list of tokens. If successful, apToken[0] is set
2562
2563
2564
2565
2566
2567
2568


2569
2570
2571
2572
2573
2574
2575
  unsigned int iKey = 0;
  int i;
  Th_HashEntry *pRet;
  Th_HashEntry **ppRet;

  if( nKey<0 ){
    nKey = th_strlen(zKey);


  }

  for(i=0; i<nKey; i++){
    iKey = (iKey<<3) ^ iKey ^ zKey[i];
  }
  iKey = iKey % TH_HASHSIZE;








>
>







2594
2595
2596
2597
2598
2599
2600
2601
2602
2603
2604
2605
2606
2607
2608
2609
  unsigned int iKey = 0;
  int i;
  Th_HashEntry *pRet;
  Th_HashEntry **ppRet;

  if( nKey<0 ){
    nKey = th_strlen(zKey);
  }else{
    nKey = TH1_LEN(nKey);
  }

  for(i=0; i<nKey; i++){
    iKey = (iKey<<3) ^ iKey ^ zKey[i];
  }
  iKey = iKey % TH_HASHSIZE;

2795
2796
2797
2798
2799
2800
2801


2802
2803
2804
2805
2806
2807
2808
  int i = 0;
  int iOut = 0;
  int base = 10;
  int (*isdigit)(char) = th_isdigit;

  if( n<0 ){
    n = th_strlen(z);


  }

  if( n>1 && (z[0]=='-' || z[0]=='+') ){
    i = 1;
  }
  if( (n-i)>2 && z[i]=='0' ){
    if( z[i+1]=='x' || z[i+1]=='X' ){







>
>







2829
2830
2831
2832
2833
2834
2835
2836
2837
2838
2839
2840
2841
2842
2843
2844
  int i = 0;
  int iOut = 0;
  int base = 10;
  int (*isdigit)(char) = th_isdigit;

  if( n<0 ){
    n = th_strlen(z);
  }else{
    n = TH1_LEN(n);
  }

  if( n>1 && (z[0]=='-' || z[0]=='+') ){
    i = 1;
  }
  if( (n-i)>2 && z[i]=='0' ){
    if( z[i+1]=='x' || z[i+1]=='X' ){
2854
2855
2856
2857
2858
2859
2860
2861
2862
2863
2864
2865
2866
2867
2868
2869
2870
2871
2872
2873
2874
2875
2876
2877
2878
2879



2880
2881
2882
2883
2884
2885
2886
int Th_ToDouble(
  Th_Interp *interp,
  const char *z,
  int n,
  double *pfOut
){
  if( !sqlite3IsNumber((const char *)z, 0) ){
    Th_ErrorMessage(interp, "expected number, got: \"", z, n);
    return TH_ERROR;
  }

  sqlite3AtoF((const char *)z, pfOut);
  return TH_OK;
}

/*
** Set the result of the interpreter to the th1 representation of
** the integer iVal and return TH_OK.
*/
int Th_SetResultInt(Th_Interp *interp, int iVal){
  int isNegative = 0;
  unsigned int uVal = iVal;
  char zBuf[32];
  char *z = &zBuf[32];

  if( iVal<0 ){



    isNegative = 1;
    uVal = iVal * -1;
  }
  *(--z) = '\0';
  *(--z) = (char)(48+(uVal%10));
  while( (uVal = (uVal/10))>0 ){
    *(--z) = (char)(48+(uVal%10));







|


















>
>
>







2890
2891
2892
2893
2894
2895
2896
2897
2898
2899
2900
2901
2902
2903
2904
2905
2906
2907
2908
2909
2910
2911
2912
2913
2914
2915
2916
2917
2918
2919
2920
2921
2922
2923
2924
2925
int Th_ToDouble(
  Th_Interp *interp,
  const char *z,
  int n,
  double *pfOut
){
  if( !sqlite3IsNumber((const char *)z, 0) ){
    Th_ErrorMessage(interp, "expected number, got: \"", z, TH1_LEN(n));
    return TH_ERROR;
  }

  sqlite3AtoF((const char *)z, pfOut);
  return TH_OK;
}

/*
** Set the result of the interpreter to the th1 representation of
** the integer iVal and return TH_OK.
*/
int Th_SetResultInt(Th_Interp *interp, int iVal){
  int isNegative = 0;
  unsigned int uVal = iVal;
  char zBuf[32];
  char *z = &zBuf[32];

  if( iVal<0 ){
    if( iVal==0x80000000 ){
      return Th_SetResult(interp, "-2147483648", -1);
    }
    isNegative = 1;
    uVal = iVal * -1;
  }
  *(--z) = '\0';
  *(--z) = (char)(48+(uVal%10));
  while( (uVal = (uVal/10))>0 ){
    *(--z) = (char)(48+(uVal%10));
Changes to src/th.h.
1
2
3
4

























5






















6
7
8
9
10
11
12

/* This header file defines the external interface to the custom Scripting
** Language (TH) interpreter.  TH is very similar to Tcl but is not an
** exact clone.

























*/























/*
** Before creating an interpreter, the application must allocate and
** populate an instance of the following structure. It must remain valid
** for the lifetime of the interpreter.
*/
typedef struct Th_Vtab Th_Vtab;
<



>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>

>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>








1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

/* This header file defines the external interface to the custom Scripting
** Language (TH) interpreter.  TH is very similar to Tcl but is not an
** exact clone.
**
** TH1 was original developed to run SQLite tests on SymbianOS.  This version
** of TH1 was repurposed as a scripted language for Fossil, and was heavily
** modified for that purpose, beginning in early 2008.
**
** More recently, TH1 has been enhanced to distinguish between regular text
** and "tainted" text.  "Tainted" text is text that might have originated
** from an outside source and hence might not be trustworthy.  To prevent
** cross-site scripting (XSS) and SQL-injections and similar attacks,
** tainted text should not be used for the following purposes:
**
**     *   executed as TH1 script or expression.
**     *   output as HTML or Javascript
**     *   used as part of an SQL query
**
** Tainted text can be converted into a safe form using commands like
** "htmlize".  And some commands ("query" and "expr") know how to use
** potentially tainted variable values directly, and thus can bypass
** the restrictions above.
**
** Whether a string is clean or tainted is determined by its length integer.
** TH1 limits strings to be no more than 0x0fffffff bytes bytes in length
** (about 268MB - more than sufficient for the purposes of Fossil).  The top
** bit of the length integer is the sign bit, of course.  The next three bits
** are reserved.  One of those, the 0x10000000 bit, marks tainted strings.
*/
#define TH1_MX_STRLEN     0x0fffffff      /* Maximum length of a TH1-C string */
#define TH1_TAINT_BIT     0x10000000      /* The taint bit */
#define TH1_SIGN          0x80000000

/* Convert an integer into a string length.  Negative values remain negative */
#define TH1_LEN(X)        ((TH1_SIGN|TH1_MX_STRLEN)&(X))

/* Return true if the string is tainted */
#define TH1_TAINTED(X)    (((X)&TH1_TAINT_BIT)!=0)

/* Remove taint from a string */
#define TH1_RM_TAINT(X)   ((X)&~TH1_TAINT_BIT)

/* Add taint to a string */
#define TH1_ADD_TAINT(X)  ((X)|TH1_TAINT_BIT)

/* If B is tainted, make A tainted too */
#define TH1_XFER_TAINT(A,B)  (A)|=(TH1_TAINT_BIT&(B))

/* Check to see if a string is too big for TH1 */
#define TH1_SIZECHECK(N)  if((N)>TH1_MX_STRLEN){Th_OversizeString();}
void Th_OversizeString(void);

/*
** Before creating an interpreter, the application must allocate and
** populate an instance of the following structure. It must remain valid
** for the lifetime of the interpreter.
*/
typedef struct Th_Vtab Th_Vtab;
22
23
24
25
26
27
28






29
30
31
32
33
34
35

/*
** Create and delete interpreters.
*/
Th_Interp * Th_CreateInterp(Th_Vtab *);
void Th_DeleteInterp(Th_Interp *);







/*
** Evaluate an TH program in the stack frame identified by parameter
** iFrame, according to the following rules:
**
**   * If iFrame is 0, this means the current frame.
**
**   * If iFrame is negative, then the nth frame up the stack, where n is







>
>
>
>
>
>







68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87

/*
** Create and delete interpreters.
*/
Th_Interp * Th_CreateInterp(Th_Vtab *);
void Th_DeleteInterp(Th_Interp *);

/*
** Report taint in the string zStr,nStr.  That string represents "zTitle"
** If non-zero is returned error out of the caller.
*/
int Th_ReportTaint(Th_Interp*,const char*,const char*zStr,int nStr);

/*
** Evaluate an TH program in the stack frame identified by parameter
** iFrame, according to the following rules:
**
**   * If iFrame is 0, this means the current frame.
**
**   * If iFrame is negative, then the nth frame up the stack, where n is
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
int Th_ExistsVar(Th_Interp *, const char *, int);
int Th_ExistsArrayVar(Th_Interp *, const char *, int);
int Th_GetVar(Th_Interp *, const char *, int);
int Th_SetVar(Th_Interp *, const char *, int, const char *, int);
int Th_LinkVar(Th_Interp *, const char *, int, int, const char *, int);
int Th_UnsetVar(Th_Interp *, const char *, int);

/*
** If interp has a variable with the given name, its value is returned
** and its length is returned via *nOut if nOut is not NULL.  If
** interp has no such var then NULL is returned without setting any
** error state and *nOut, if not NULL, is set to 0. The returned value
** is owned by the interpreter and may be invalidated the next time
** the interpreter is modified.
**
** zVarName must be NUL-terminated.
*/
const char * Th_MaybeGetVar(Th_Interp *interp, const char *zVarName,
                            int *nOut);

typedef int (*Th_CommandProc)(Th_Interp *, void *, int, const char **, int *);

/*
** Register new commands.
*/
int Th_CreateCommand(
  Th_Interp *interp,







<
<
<
<
<
<
<
<
<
<
<
<
<







106
107
108
109
110
111
112













113
114
115
116
117
118
119
int Th_ExistsVar(Th_Interp *, const char *, int);
int Th_ExistsArrayVar(Th_Interp *, const char *, int);
int Th_GetVar(Th_Interp *, const char *, int);
int Th_SetVar(Th_Interp *, const char *, int, const char *, int);
int Th_LinkVar(Th_Interp *, const char *, int, int, const char *, int);
int Th_UnsetVar(Th_Interp *, const char *, int);














typedef int (*Th_CommandProc)(Th_Interp *, void *, int, const char **, int *);

/*
** Register new commands.
*/
int Th_CreateCommand(
  Th_Interp *interp,
Changes to src/th_lang.c.
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
    return Th_WrongNumArgs(interp, "catch script ?varname?");
  }

  rc = Th_Eval(interp, 0, argv[1], -1);
  if( argc==3 ){
    int nResult;
    const char *zResult = Th_GetResult(interp, &nResult);
    Th_SetVar(interp, argv[2], argl[2], zResult, nResult);
  }

  Th_SetResultInt(interp, rc);
  return TH_OK;
}

/*







|







37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
    return Th_WrongNumArgs(interp, "catch script ?varname?");
  }

  rc = Th_Eval(interp, 0, argv[1], -1);
  if( argc==3 ){
    int nResult;
    const char *zResult = Th_GetResult(interp, &nResult);
    Th_SetVar(interp, argv[2], TH1_LEN(argl[2]), zResult, nResult);
  }

  Th_SetResultInt(interp, rc);
  return TH_OK;
}

/*
178
179
180
181
182
183
184

185
186
187
188
189
190

191
192
193


194
195
196
197
198
199
200
201
  char **azVar = 0;
  int *anVar;
  int nVar;
  char **azValue = 0;
  int *anValue;
  int nValue;
  int ii, jj;


  if( argc!=4 ){
    return Th_WrongNumArgs(interp, "foreach varlist list script");
  }
  rc = Th_SplitList(interp, argv[1], argl[1], &azVar, &anVar, &nVar);
  if( rc ) return rc;

  rc = Th_SplitList(interp, argv[2], argl[2], &azValue, &anValue, &nValue);
  for(ii=0; rc==TH_OK && ii<=nValue-nVar; ii+=nVar){
    for(jj=0; jj<nVar; jj++){


      Th_SetVar(interp, azVar[jj], anVar[jj], azValue[ii+jj], anValue[ii+jj]);
    }
    rc = eval_loopbody(interp, argv[3], argl[3]);
  }
  if( rc==TH_BREAK ) rc = TH_OK;
  Th_Free(interp, azVar);
  Th_Free(interp, azValue);
  return rc;







>






>



>
>
|







178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
  char **azVar = 0;
  int *anVar;
  int nVar;
  char **azValue = 0;
  int *anValue;
  int nValue;
  int ii, jj;
  int bTaint = 0;

  if( argc!=4 ){
    return Th_WrongNumArgs(interp, "foreach varlist list script");
  }
  rc = Th_SplitList(interp, argv[1], argl[1], &azVar, &anVar, &nVar);
  if( rc ) return rc;
  TH1_XFER_TAINT(bTaint, argl[2]);
  rc = Th_SplitList(interp, argv[2], argl[2], &azValue, &anValue, &nValue);
  for(ii=0; rc==TH_OK && ii<=nValue-nVar; ii+=nVar){
    for(jj=0; jj<nVar; jj++){
      int x = anValue[ii+jj];
      TH1_XFER_TAINT(x, bTaint);
      Th_SetVar(interp, azVar[jj], anVar[jj], azValue[ii+jj], x);
    }
    rc = eval_loopbody(interp, argv[3], argl[3]);
  }
  if( rc==TH_BREAK ) rc = TH_OK;
  Th_Free(interp, azVar);
  Th_Free(interp, azValue);
  return rc;
213
214
215
216
217
218
219

220
221

222
223
224

225
226
227
228
229
230
231
  int argc,
  const char **argv,
  int *argl
){
  char *zList = 0;
  int nList = 0;
  int i;


  for(i=1; i<argc; i++){

    Th_ListAppend(interp, &zList, &nList, argv[i], argl[i]);
  }


  Th_SetResult(interp, zList, nList);
  Th_Free(interp, zList);

  return TH_OK;
}

/*







>


>



>







217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
  int argc,
  const char **argv,
  int *argl
){
  char *zList = 0;
  int nList = 0;
  int i;
  int bTaint = 0;

  for(i=1; i<argc; i++){
    TH1_XFER_TAINT(bTaint,argl[i]);
    Th_ListAppend(interp, &zList, &nList, argv[i], argl[i]);
  }

  TH1_XFER_TAINT(nList, bTaint);
  Th_SetResult(interp, zList, nList);
  Th_Free(interp, zList);

  return TH_OK;
}

/*
242
243
244
245
246
247
248

249
250
251
252
253
254
255
256
257

258

259
260
261

262
263
264
265
266
267
268
  int argc,
  const char **argv,
  int *argl
){
  char *zList = 0;
  int nList = 0;
  int i, rc;


  if( argc<2 ){
    return Th_WrongNumArgs(interp, "lappend var ...");
  }
  rc = Th_GetVar(interp, argv[1], argl[1]);
  if( rc==TH_OK ){
    zList = Th_TakeResult(interp, &nList);
  }


  for(i=2; i<argc; i++){

    Th_ListAppend(interp, &zList, &nList, argv[i], argl[i]);
  }


  Th_SetVar(interp, argv[1], argl[1], zList, nList);
  Th_SetResult(interp, zList, nList);
  Th_Free(interp, zList);

  return TH_OK;
}








>









>

>



>







249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
  int argc,
  const char **argv,
  int *argl
){
  char *zList = 0;
  int nList = 0;
  int i, rc;
  int bTaint = 0;

  if( argc<2 ){
    return Th_WrongNumArgs(interp, "lappend var ...");
  }
  rc = Th_GetVar(interp, argv[1], argl[1]);
  if( rc==TH_OK ){
    zList = Th_TakeResult(interp, &nList);
  }

  TH1_XFER_TAINT(bTaint, nList);
  for(i=2; i<argc; i++){
    TH1_XFER_TAINT(bTaint, argl[i]);
    Th_ListAppend(interp, &zList, &nList, argv[i], argl[i]);
  }

  TH1_XFER_TAINT(nList, bTaint);
  Th_SetVar(interp, argv[1], argl[1], zList, nList);
  Th_SetResult(interp, zList, nList);
  Th_Free(interp, zList);

  return TH_OK;
}

281
282
283
284
285
286
287

288
289
290
291
292
293
294
295
296

297
298
299


300
301
302
303
304
305
306
307
){
  int iElem;
  int rc;

  char **azElem;
  int *anElem;
  int nCount;


  if( argc!=3 ){
    return Th_WrongNumArgs(interp, "lindex list index");
  }

  if( TH_OK!=Th_ToInt(interp, argv[2], argl[2], &iElem) ){
    return TH_ERROR;
  }


  rc = Th_SplitList(interp, argv[1], argl[1], &azElem, &anElem, &nCount);
  if( rc==TH_OK ){
    if( iElem<nCount && iElem>=0 ){


      Th_SetResult(interp, azElem[iElem], anElem[iElem]);
    }else{
      Th_SetResult(interp, 0, 0);
    }
    Th_Free(interp, azElem);
  }

  return rc;







>









>



>
>
|







292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
){
  int iElem;
  int rc;

  char **azElem;
  int *anElem;
  int nCount;
  int bTaint = 0;

  if( argc!=3 ){
    return Th_WrongNumArgs(interp, "lindex list index");
  }

  if( TH_OK!=Th_ToInt(interp, argv[2], argl[2], &iElem) ){
    return TH_ERROR;
  }

  TH1_XFER_TAINT(bTaint, argl[1]);
  rc = Th_SplitList(interp, argv[1], argl[1], &azElem, &anElem, &nCount);
  if( rc==TH_OK ){
    if( iElem<nCount && iElem>=0 ){
      int sz = anElem[iElem];
      TH1_XFER_TAINT(sz, bTaint);
      Th_SetResult(interp, azElem[iElem], sz);
    }else{
      Th_SetResult(interp, 0, 0);
    }
    Th_Free(interp, azElem);
  }

  return rc;
354
355
356
357
358
359
360

361
362
363
364
365
366
367
368
369
370

  if( argc!=3 ){
    return Th_WrongNumArgs(interp, "lsearch list string");
  }

  rc = Th_SplitList(interp, argv[1], argl[1], &azElem, &anElem, &nCount);
  if( rc==TH_OK ){

    Th_SetResultInt(interp, -1);
    for(i=0; i<nCount; i++){
      if( anElem[i]==argl[2] && 0==memcmp(azElem[i], argv[2], argl[2]) ){
        Th_SetResultInt(interp, i);
        break;
      }
    }
    Th_Free(interp, azElem);
  }








>


|







369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386

  if( argc!=3 ){
    return Th_WrongNumArgs(interp, "lsearch list string");
  }

  rc = Th_SplitList(interp, argv[1], argl[1], &azElem, &anElem, &nCount);
  if( rc==TH_OK ){
    int nn = TH1_LEN(argl[2]);
    Th_SetResultInt(interp, -1);
    for(i=0; i<nCount; i++){
      if( TH1_LEN(anElem[i])==nn && 0==memcmp(azElem[i], argv[2], nn) ){
        Th_SetResultInt(interp, i);
        break;
      }
    }
    Th_Free(interp, azElem);
  }

559
560
561
562
563
564
565
566

567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582

583

584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603

  char *zUsage = 0;                /* Build up a usage message here */
  int nUsage = 0;                  /* Number of bytes at zUsage */

  if( argc!=4 ){
    return Th_WrongNumArgs(interp, "proc name arglist code");
  }
  if( Th_SplitList(interp, argv[2], argl[2], &azParam, &anParam, &nParam) ){

    return TH_ERROR;
  }

  /* Allocate the new ProcDefn structure. */
  nByte = sizeof(ProcDefn) +                        /* ProcDefn structure */
      (sizeof(char *) + sizeof(int)) * nParam +     /* azParam, anParam */
      (sizeof(char *) + sizeof(int)) * nParam +     /* azDefault, anDefault */
      argl[3] +                                     /* zProgram */
      argl[2];    /* Space for copies of parameter names and default values */
  p = (ProcDefn *)Th_Malloc(interp, nByte);

  /* If the last parameter in the parameter list is "args", then set the
  ** ProcDefn.hasArgs flag. The "args" parameter does not require an
  ** entry in the ProcDefn.azParam[] or ProcDefn.azDefault[] arrays.
  */
  if( nParam>0 ){

    if( anParam[nParam-1]==4 && 0==memcmp(azParam[nParam-1], "args", 4) ){

      p->hasArgs = 1;
      nParam--;
    }
  }

  p->nParam    = nParam;
  p->azParam   = (char **)&p[1];
  p->anParam   = (int *)&p->azParam[nParam];
  p->azDefault = (char **)&p->anParam[nParam];
  p->anDefault = (int *)&p->azDefault[nParam];
  p->zProgram = (char *)&p->anDefault[nParam];
  memcpy(p->zProgram, argv[3], argl[3]);
  p->nProgram = argl[3];
  zSpace = &p->zProgram[p->nProgram];

  for(i=0; i<nParam; i++){
    char **az;
    int *an;
    int n;
    if( Th_SplitList(interp, azParam[i], anParam[i], &az, &an, &n) ){







|
>







|
|







>
|
>











|
|







575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622

  char *zUsage = 0;                /* Build up a usage message here */
  int nUsage = 0;                  /* Number of bytes at zUsage */

  if( argc!=4 ){
    return Th_WrongNumArgs(interp, "proc name arglist code");
  }
  if( Th_SplitList(interp, argv[2], TH1_LEN(argl[2]),
                   &azParam, &anParam, &nParam) ){
    return TH_ERROR;
  }

  /* Allocate the new ProcDefn structure. */
  nByte = sizeof(ProcDefn) +                        /* ProcDefn structure */
      (sizeof(char *) + sizeof(int)) * nParam +     /* azParam, anParam */
      (sizeof(char *) + sizeof(int)) * nParam +     /* azDefault, anDefault */
      TH1_LEN(argl[3]) +                            /* zProgram */
      TH1_LEN(argl[2]);   /* Space for copies of param names and dflt values */
  p = (ProcDefn *)Th_Malloc(interp, nByte);

  /* If the last parameter in the parameter list is "args", then set the
  ** ProcDefn.hasArgs flag. The "args" parameter does not require an
  ** entry in the ProcDefn.azParam[] or ProcDefn.azDefault[] arrays.
  */
  if( nParam>0 ){
    if( TH1_LEN(anParam[nParam-1])==4
     && 0==memcmp(azParam[nParam-1], "args", 4)
    ){
      p->hasArgs = 1;
      nParam--;
    }
  }

  p->nParam    = nParam;
  p->azParam   = (char **)&p[1];
  p->anParam   = (int *)&p->azParam[nParam];
  p->azDefault = (char **)&p->anParam[nParam];
  p->anDefault = (int *)&p->azDefault[nParam];
  p->zProgram = (char *)&p->anDefault[nParam];
  memcpy(p->zProgram, argv[3], TH1_LEN(argl[3]));
  p->nProgram = TH1_LEN(argl[3]);
  zSpace = &p->zProgram[p->nProgram];

  for(i=0; i<nParam; i++){
    char **az;
    int *an;
    int n;
    if( Th_SplitList(interp, azParam[i], anParam[i], &az, &an, &n) ){
670
671
672
673
674
675
676
677

678
679
680
681
682
683
684
  int argc,
  const char **argv,
  int *argl
){
  if( argc!=3 ){
    return Th_WrongNumArgs(interp, "rename oldcmd newcmd");
  }
  return Th_RenameCommand(interp, argv[1], argl[1], argv[2], argl[2]);

}

/*
** TH Syntax:
**
**   break    ?value...?
**   continue ?value...?







|
>







689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
  int argc,
  const char **argv,
  int *argl
){
  if( argc!=3 ){
    return Th_WrongNumArgs(interp, "rename oldcmd newcmd");
  }
  return Th_RenameCommand(interp, argv[1], TH1_LEN(argl[1]),
                          argv[2], TH1_LEN(argl[2]));
}

/*
** TH Syntax:
**
**   break    ?value...?
**   continue ?value...?
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
  int iRes = 0;

  if( argc!=4 ){
    return Th_WrongNumArgs(interp, "string compare str1 str2");
  }

  zLeft = argv[2];
  nLeft = argl[2];
  zRight = argv[3];
  nRight = argl[3];

  for(i=0; iRes==0 && i<nLeft && i<nRight; i++){
    iRes = zLeft[i]-zRight[i];
  }
  if( iRes==0 ){
    iRes = nLeft-nRight;
  }







|

|







764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
  int iRes = 0;

  if( argc!=4 ){
    return Th_WrongNumArgs(interp, "string compare str1 str2");
  }

  zLeft = argv[2];
  nLeft = TH1_LEN(argl[2]);
  zRight = argv[3];
  nRight = TH1_LEN(argl[3]);

  for(i=0; iRes==0 && i<nLeft && i<nRight; i++){
    iRes = zLeft[i]-zRight[i];
  }
  if( iRes==0 ){
    iRes = nLeft-nRight;
  }
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
  int nHaystack;
  int iRes = -1;

  if( argc!=4 ){
    return Th_WrongNumArgs(interp, "string first needle haystack");
  }

  nNeedle = argl[2];
  nHaystack = argl[3];

  if( nNeedle && nHaystack && nNeedle<=nHaystack ){
    const char *zNeedle = argv[2];
    const char *zHaystack = argv[3];
    int i;

    for(i=0; i<=(nHaystack-nNeedle); i++){







|
|







797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
  int nHaystack;
  int iRes = -1;

  if( argc!=4 ){
    return Th_WrongNumArgs(interp, "string first needle haystack");
  }

  nNeedle = TH1_LEN(argl[2]);
  nHaystack = TH1_LEN(argl[3]);

  if( nNeedle && nHaystack && nNeedle<=nHaystack ){
    const char *zNeedle = argv[2];
    const char *zHaystack = argv[3];
    int i;

    for(i=0; i<=(nHaystack-nNeedle); i++){
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825


826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870


871
872
873

874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
){
  int iIndex;

  if( argc!=4 ){
    return Th_WrongNumArgs(interp, "string index string index");
  }

  if( argl[3]==3 && 0==memcmp("end", argv[3], 3) ){
    iIndex = argl[2]-1;
  }else if( Th_ToInt(interp, argv[3], argl[3], &iIndex) ){
    Th_ErrorMessage(
        interp, "Expected \"end\" or integer, got:", argv[3], argl[3]);
    return TH_ERROR;
  }

  if( iIndex>=0 && iIndex<argl[2] ){


    return Th_SetResult(interp, &argv[2][iIndex], 1);
  }else{
    return Th_SetResult(interp, 0, 0);
  }
}

/*
** TH Syntax:
**
**   string is CLASS STRING
*/
static int string_is_command(
  Th_Interp *interp, void *ctx, int argc, const char **argv, int *argl
){
  if( argc!=4 ){
    return Th_WrongNumArgs(interp, "string is class string");
  }
  if( argl[2]==5 && 0==memcmp(argv[2], "alnum", 5) ){
    int i;
    int iRes = 1;

    for(i=0; i<argl[3]; i++){
      if( !th_isalnum(argv[3][i]) ){
        iRes = 0;
      }
    }

    return Th_SetResultInt(interp, iRes);
  }else if( argl[2]==6 && 0==memcmp(argv[2], "double", 6) ){
    double fVal;
    if( Th_ToDouble(interp, argv[3], argl[3], &fVal)==TH_OK ){
      return Th_SetResultInt(interp, 1);
    }
    return Th_SetResultInt(interp, 0);
  }else if( argl[2]==7 && 0==memcmp(argv[2], "integer", 7) ){
    int iVal;
    if( Th_ToInt(interp, argv[3], argl[3], &iVal)==TH_OK ){
      return Th_SetResultInt(interp, 1);
    }
    return Th_SetResultInt(interp, 0);
  }else if( argl[2]==4 && 0==memcmp(argv[2], "list", 4) ){
    if( Th_SplitList(interp, argv[3], argl[3], 0, 0, 0)==TH_OK ){
      return Th_SetResultInt(interp, 1);
    }
    return Th_SetResultInt(interp, 0);


  }else{
    Th_ErrorMessage(interp,
        "Expected alnum, double, integer, or list, got:", argv[2], argl[2]);

    return TH_ERROR;
  }
}

/*
** TH Syntax:
**
**   string last NEEDLE HAYSTACK
*/
static int string_last_command(
  Th_Interp *interp, void *ctx, int argc, const char **argv, int *argl
){
  int nNeedle;
  int nHaystack;
  int iRes = -1;

  if( argc!=4 ){
    return Th_WrongNumArgs(interp, "string last needle haystack");
  }

  nNeedle = argl[2];
  nHaystack = argl[3];

  if( nNeedle && nHaystack && nNeedle<=nHaystack ){
    const char *zNeedle = argv[2];
    const char *zHaystack = argv[3];
    int i;

    for(i=nHaystack-nNeedle; i>=0; i--){







|
|






|
>
>
|
















|



|






|





|





|




>
>


|
>




















|
|







830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
){
  int iIndex;

  if( argc!=4 ){
    return Th_WrongNumArgs(interp, "string index string index");
  }

  if( TH1_LEN(argl[3])==3 && 0==memcmp("end", argv[3], 3) ){
    iIndex = TH1_LEN(argl[2])-1;
  }else if( Th_ToInt(interp, argv[3], argl[3], &iIndex) ){
    Th_ErrorMessage(
        interp, "Expected \"end\" or integer, got:", argv[3], argl[3]);
    return TH_ERROR;
  }

  if( iIndex>=0 && iIndex<TH1_LEN(argl[2]) ){
    int sz = 1;
    TH1_XFER_TAINT(sz, argl[2]);
    return Th_SetResult(interp, &argv[2][iIndex], sz);
  }else{
    return Th_SetResult(interp, 0, 0);
  }
}

/*
** TH Syntax:
**
**   string is CLASS STRING
*/
static int string_is_command(
  Th_Interp *interp, void *ctx, int argc, const char **argv, int *argl
){
  if( argc!=4 ){
    return Th_WrongNumArgs(interp, "string is class string");
  }
  if( TH1_LEN(argl[2])==5 && 0==memcmp(argv[2], "alnum", 5) ){
    int i;
    int iRes = 1;

    for(i=0; i<TH1_LEN(argl[3]); i++){
      if( !th_isalnum(argv[3][i]) ){
        iRes = 0;
      }
    }

    return Th_SetResultInt(interp, iRes);
  }else if( TH1_LEN(argl[2])==6 && 0==memcmp(argv[2], "double", 6) ){
    double fVal;
    if( Th_ToDouble(interp, argv[3], argl[3], &fVal)==TH_OK ){
      return Th_SetResultInt(interp, 1);
    }
    return Th_SetResultInt(interp, 0);
  }else if( TH1_LEN(argl[2])==7 && 0==memcmp(argv[2], "integer", 7) ){
    int iVal;
    if( Th_ToInt(interp, argv[3], argl[3], &iVal)==TH_OK ){
      return Th_SetResultInt(interp, 1);
    }
    return Th_SetResultInt(interp, 0);
  }else if( TH1_LEN(argl[2])==4 && 0==memcmp(argv[2], "list", 4) ){
    if( Th_SplitList(interp, argv[3], argl[3], 0, 0, 0)==TH_OK ){
      return Th_SetResultInt(interp, 1);
    }
    return Th_SetResultInt(interp, 0);
  }else if( TH1_LEN(argl[2])==7 && 0==memcmp(argv[2], "tainted", 7) ){
    return Th_SetResultInt(interp, TH1_TAINTED(argl[3]));
  }else{
    Th_ErrorMessage(interp,
        "Expected alnum, double, integer, list, or tainted, got:",
        argv[2], TH1_LEN(argl[2]));
    return TH_ERROR;
  }
}

/*
** TH Syntax:
**
**   string last NEEDLE HAYSTACK
*/
static int string_last_command(
  Th_Interp *interp, void *ctx, int argc, const char **argv, int *argl
){
  int nNeedle;
  int nHaystack;
  int iRes = -1;

  if( argc!=4 ){
    return Th_WrongNumArgs(interp, "string last needle haystack");
  }

  nNeedle = TH1_LEN(argl[2]);
  nHaystack = TH1_LEN(argl[3]);

  if( nNeedle && nHaystack && nNeedle<=nHaystack ){
    const char *zNeedle = argv[2];
    const char *zHaystack = argv[3];
    int i;

    for(i=nHaystack-nNeedle; i>=0; i--){
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960

961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979


980
981
982
983
984
985
986
987
988
989
990
991
992
993

994
995
996
997
998
999
1000
1001
1002
1003
1004



1005
1006
1007
1008
1009


1010
1011
1012
1013
1014
1015
1016
1017
*/
static int string_length_command(
  Th_Interp *interp, void *ctx, int argc, const char **argv, int *argl
){
  if( argc!=3 ){
    return Th_WrongNumArgs(interp, "string length string");
  }
  return Th_SetResultInt(interp, argl[2]);
}

/*
** TH Syntax:
**
**   string match PATTERN STRING
**
*/
static int string_match_command(
  Th_Interp *interp, void *ctx, int argc, const char **argv, int *argl
){
  extern char *fossil_strndup(const char*,int);
  extern void fossil_free(void*);
  char *zPat, *zStr;
  int rc;
  if( argc!=4 ){
    return Th_WrongNumArgs(interp, "string match pattern string");
  }
  zPat = fossil_strndup(argv[2],argl[2]);
  zStr = fossil_strndup(argv[3],argl[3]);
  rc = sqlite3_strglob(zPat,zStr);
  fossil_free(zPat);
  fossil_free(zStr);
  return Th_SetResultInt(interp, !rc);
}

/*
** TH Syntax:
**
**   string range STRING FIRST LAST
*/
static int string_range_command(
  Th_Interp *interp, void *ctx, int argc, const char **argv, int *argl
){
  int iStart;
  int iEnd;


  if( argc!=5 ){
    return Th_WrongNumArgs(interp, "string range string first last");
  }

  if( argl[4]==3 && 0==memcmp("end", argv[4], 3) ){
    iEnd = argl[2];
  }else if( Th_ToInt(interp, argv[4], argl[4], &iEnd) ){
    Th_ErrorMessage(
        interp, "Expected \"end\" or integer, got:", argv[4], argl[4]);
    return TH_ERROR;
  }
  if( Th_ToInt(interp, argv[3], argl[3], &iStart) ){
    return TH_ERROR;
  }

  if( iStart<0 ) iStart = 0;
  if( iEnd>=argl[2] ) iEnd = argl[2]-1;
  if( iStart>iEnd ) iEnd = iStart-1;



  return Th_SetResult(interp, &argv[2][iStart], iEnd-iStart+1);
}

/*
** TH Syntax:
**
**   string repeat STRING COUNT
*/
static int string_repeat_command(
  Th_Interp *interp, void *ctx, int argc, const char **argv, int *argl
){
  int n;
  int i;

  int nByte;
  char *zByte;

  if( argc!=4 ){
    return Th_WrongNumArgs(interp, "string repeat string n");
  }
  if( Th_ToInt(interp, argv[3], argl[3], &n) ){
    return TH_ERROR;
  }

  nByte = argl[2] * n;



  zByte = Th_Malloc(interp, nByte+1);
  for(i=0; i<nByte; i+=argl[2]){
    memcpy(&zByte[i], argv[2], argl[2]);
  }



  Th_SetResult(interp, zByte, nByte);
  Th_Free(interp, zByte);
  return TH_OK;
}

/*
** TH Syntax:
**







|


















|
|
















>





|
|


|







|

>
>

|












>
|









|
>
>
>

|
|


>
>
|







942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
*/
static int string_length_command(
  Th_Interp *interp, void *ctx, int argc, const char **argv, int *argl
){
  if( argc!=3 ){
    return Th_WrongNumArgs(interp, "string length string");
  }
  return Th_SetResultInt(interp, TH1_LEN(argl[2]));
}

/*
** TH Syntax:
**
**   string match PATTERN STRING
**
*/
static int string_match_command(
  Th_Interp *interp, void *ctx, int argc, const char **argv, int *argl
){
  extern char *fossil_strndup(const char*,int);
  extern void fossil_free(void*);
  char *zPat, *zStr;
  int rc;
  if( argc!=4 ){
    return Th_WrongNumArgs(interp, "string match pattern string");
  }
  zPat = fossil_strndup(argv[2],TH1_LEN(argl[2]));
  zStr = fossil_strndup(argv[3],TH1_LEN(argl[3]));
  rc = sqlite3_strglob(zPat,zStr);
  fossil_free(zPat);
  fossil_free(zStr);
  return Th_SetResultInt(interp, !rc);
}

/*
** TH Syntax:
**
**   string range STRING FIRST LAST
*/
static int string_range_command(
  Th_Interp *interp, void *ctx, int argc, const char **argv, int *argl
){
  int iStart;
  int iEnd;
  int sz;

  if( argc!=5 ){
    return Th_WrongNumArgs(interp, "string range string first last");
  }

  if( TH1_LEN(argl[4])==3 && 0==memcmp("end", argv[4], 3) ){
    iEnd = TH1_LEN(argl[2]);
  }else if( Th_ToInt(interp, argv[4], argl[4], &iEnd) ){
    Th_ErrorMessage(
        interp, "Expected \"end\" or integer, got:", argv[4], TH1_LEN(argl[4]));
    return TH_ERROR;
  }
  if( Th_ToInt(interp, argv[3], argl[3], &iStart) ){
    return TH_ERROR;
  }

  if( iStart<0 ) iStart = 0;
  if( iEnd>=TH1_LEN(argl[2]) ) iEnd = TH1_LEN(argl[2])-1;
  if( iStart>iEnd ) iEnd = iStart-1;
  sz = iEnd - iStart + 1;
  TH1_XFER_TAINT(sz, argl[2]);

  return Th_SetResult(interp, &argv[2][iStart], sz);
}

/*
** TH Syntax:
**
**   string repeat STRING COUNT
*/
static int string_repeat_command(
  Th_Interp *interp, void *ctx, int argc, const char **argv, int *argl
){
  int n;
  int i;
  int sz;
  long long int nByte;
  char *zByte;

  if( argc!=4 ){
    return Th_WrongNumArgs(interp, "string repeat string n");
  }
  if( Th_ToInt(interp, argv[3], argl[3], &n) ){
    return TH_ERROR;
  }

  nByte = n;
  sz = TH1_LEN(argl[2]);
  nByte *= sz;
  TH1_SIZECHECK(nByte+1);
  zByte = Th_Malloc(interp, nByte+1);
  for(i=0; i<nByte; i+=sz){
    memcpy(&zByte[i], argv[2], sz);
  }

  n = nByte;
  TH1_XFER_TAINT(n, argl[2]);
  Th_SetResult(interp, zByte, n);
  Th_Free(interp, zByte);
  return TH_OK;
}

/*
** TH Syntax:
**
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038

1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
  int n;
  const char *z;

  if( argc!=3 ){
    return Th_WrongNumArgs(interp, "string trim string");
  }
  z = argv[2];
  n = argl[2];
  if( argl[1]<5 || argv[1][4]=='l' ){
    while( n && th_isspace(z[0]) ){ z++; n--; }
  }
  if( argl[1]<5 || argv[1][4]=='r' ){
    while( n && th_isspace(z[n-1]) ){ n--; }
  }

  Th_SetResult(interp, z, n);
  return TH_OK;
}

/*
** TH Syntax:
**
**   info exists VARNAME
*/
static int info_exists_command(
  Th_Interp *interp, void *ctx, int argc, const char **argv, int *argl
){
  int rc;

  if( argc!=3 ){
    return Th_WrongNumArgs(interp, "info exists var");
  }
  rc = Th_ExistsVar(interp, argv[2], argl[2]);
  Th_SetResultInt(interp, rc);
  return TH_OK;
}

/*
** TH Syntax:
**







|
|


|


>

















|







1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
  int n;
  const char *z;

  if( argc!=3 ){
    return Th_WrongNumArgs(interp, "string trim string");
  }
  z = argv[2];
  n = TH1_LEN(argl[2]);
  if( TH1_LEN(argl[1])<5 || argv[1][4]=='l' ){
    while( n && th_isspace(z[0]) ){ z++; n--; }
  }
  if( TH1_LEN(argl[1])<5 || argv[1][4]=='r' ){
    while( n && th_isspace(z[n-1]) ){ n--; }
  }
  TH1_XFER_TAINT(n, argl[2]);
  Th_SetResult(interp, z, n);
  return TH_OK;
}

/*
** TH Syntax:
**
**   info exists VARNAME
*/
static int info_exists_command(
  Th_Interp *interp, void *ctx, int argc, const char **argv, int *argl
){
  int rc;

  if( argc!=3 ){
    return Th_WrongNumArgs(interp, "info exists var");
  }
  rc = Th_ExistsVar(interp, argv[2], TH1_LEN(argl[2]));
  Th_SetResultInt(interp, rc);
  return TH_OK;
}

/*
** TH Syntax:
**
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
  Th_Interp *interp, void *ctx, int argc, const char **argv, int *argl
){
  int rc;

  if( argc!=3 ){
    return Th_WrongNumArgs(interp, "array exists var");
  }
  rc = Th_ExistsArrayVar(interp, argv[2], argl[2]);
  Th_SetResultInt(interp, rc);
  return TH_OK;
}

/*
** TH Syntax:
**
**   array names VARNAME
*/
static int array_names_command(
  Th_Interp *interp, void *ctx, int argc, const char **argv, int *argl
){
  int rc;
  char *zElem = 0;
  int nElem = 0;

  if( argc!=3 ){
    return Th_WrongNumArgs(interp, "array names varname");
  }
  rc = Th_ListAppendArray(interp, argv[2], argl[2], &zElem, &nElem);
  if( rc!=TH_OK ){
    return rc;
  }
  Th_SetResult(interp, zElem, nElem);
  if( zElem ) Th_Free(interp, zElem);
  return TH_OK;
}







|



















|







1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
  Th_Interp *interp, void *ctx, int argc, const char **argv, int *argl
){
  int rc;

  if( argc!=3 ){
    return Th_WrongNumArgs(interp, "array exists var");
  }
  rc = Th_ExistsArrayVar(interp, argv[2], TH1_LEN(argl[2]));
  Th_SetResultInt(interp, rc);
  return TH_OK;
}

/*
** TH Syntax:
**
**   array names VARNAME
*/
static int array_names_command(
  Th_Interp *interp, void *ctx, int argc, const char **argv, int *argl
){
  int rc;
  char *zElem = 0;
  int nElem = 0;

  if( argc!=3 ){
    return Th_WrongNumArgs(interp, "array names varname");
  }
  rc = Th_ListAppendArray(interp, argv[2], TH1_LEN(argl[2]), &zElem, &nElem);
  if( rc!=TH_OK ){
    return rc;
  }
  Th_SetResult(interp, zElem, nElem);
  if( zElem ) Th_Free(interp, zElem);
  return TH_OK;
}
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181

1182
1183
1184
1185
1186
1187

1188
1189

1190
1191
1192
1193
1194
1195
1196
  int argc,
  const char **argv,
  int *argl
){
  if( argc!=2 ){
    return Th_WrongNumArgs(interp, "unset var");
  }
  return Th_UnsetVar(interp, argv[1], argl[1]);
}

int Th_CallSubCommand(
  Th_Interp *interp,
  void *ctx,
  int argc,
  const char **argv,
  int *argl,
  const Th_SubCommand *aSub
){
  if( argc>1 ){
    int i;
    for(i=0; aSub[i].zName; i++){
      const char *zName = aSub[i].zName;
      if( th_strlen(zName)==argl[1] && 0==memcmp(zName, argv[1], argl[1]) ){

        return aSub[i].xProc(interp, ctx, argc, argv, argl);
      }
    }
  }
  if(argc<2){
    Th_ErrorMessage(interp, "Expected sub-command for", argv[0], argl[0]);

  }else{
    Th_ErrorMessage(interp, "Expected sub-command, got:", argv[1], argl[1]);

  }
  return TH_ERROR;
}

/*
** TH Syntax:
**







|














|
>





|
>

|
>







1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
  int argc,
  const char **argv,
  int *argl
){
  if( argc!=2 ){
    return Th_WrongNumArgs(interp, "unset var");
  }
  return Th_UnsetVar(interp, argv[1], TH1_LEN(argl[1]));
}

int Th_CallSubCommand(
  Th_Interp *interp,
  void *ctx,
  int argc,
  const char **argv,
  int *argl,
  const Th_SubCommand *aSub
){
  if( argc>1 ){
    int i;
    for(i=0; aSub[i].zName; i++){
      const char *zName = aSub[i].zName;
      if( th_strlen(zName)==TH1_LEN(argl[1])
       && 0==memcmp(zName, argv[1], TH1_LEN(argl[1])) ){
        return aSub[i].xProc(interp, ctx, argc, argv, argl);
      }
    }
  }
  if(argc<2){
    Th_ErrorMessage(interp, "Expected sub-command for",
                            argv[0], TH1_LEN(argl[0]));
  }else{
    Th_ErrorMessage(interp, "Expected sub-command, got:",
                             argv[1], TH1_LEN(argl[1]));
  }
  return TH_ERROR;
}

/*
** TH Syntax:
**
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
  int *argl
){
  int iFrame = -1;

  if( argc!=2 && argc!=3 ){
    return Th_WrongNumArgs(interp, "uplevel ?level? script...");
  }
  if( argc==3 && TH_OK!=thToFrame(interp, argv[1], argl[1], &iFrame) ){
    return TH_ERROR;
  }
  return Th_Eval(interp, iFrame, argv[argc-1], -1);
}

/*
** TH Syntax:







|







1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
  int *argl
){
  int iFrame = -1;

  if( argc!=2 && argc!=3 ){
    return Th_WrongNumArgs(interp, "uplevel ?level? script...");
  }
  if( argc==3 && TH_OK!=thToFrame(interp, argv[1], TH1_LEN(argl[1]), &iFrame) ){
    return TH_ERROR;
  }
  return Th_Eval(interp, iFrame, argv[argc-1], -1);
}

/*
** TH Syntax:
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355

1356
1357
1358
1359
1360
1361
1362
  int *argl
){
  int iVar = 1;
  int iFrame = -1;
  int rc = TH_OK;
  int i;

  if( TH_OK==thToFrame(0, argv[1], argl[1], &iFrame) ){
    iVar++;
  }
  if( argc==iVar || (argc-iVar)%2 ){
    return Th_WrongNumArgs(interp,
        "upvar frame othervar myvar ?othervar myvar...?");
  }
  for(i=iVar; rc==TH_OK && i<argc; i=i+2){
    rc = Th_LinkVar(interp, argv[i+1], argl[i+1], iFrame, argv[i], argl[i]);

  }
  return rc;
}

/*
** TH Syntax:
**







|







|
>







1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
  int *argl
){
  int iVar = 1;
  int iFrame = -1;
  int rc = TH_OK;
  int i;

  if( TH_OK==thToFrame(0, argv[1], TH1_LEN(argl[1]), &iFrame) ){
    iVar++;
  }
  if( argc==iVar || (argc-iVar)%2 ){
    return Th_WrongNumArgs(interp,
        "upvar frame othervar myvar ?othervar myvar...?");
  }
  for(i=iVar; rc==TH_OK && i<argc; i=i+2){
    rc = Th_LinkVar(interp, argv[i+1], TH1_LEN(argl[i+1]),
                    iFrame, argv[i], TH1_LEN(argl[i]));
  }
  return rc;
}

/*
** TH Syntax:
**
Changes to src/th_main.c.
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
  const char **argv,
  int *argl
){
  char *zOut;
  if( argc!=2 ){
    return Th_WrongNumArgs(interp, "httpize STRING");
  }
  zOut = httpize((char*)argv[1], argl[1]);
  Th_SetResult(interp, zOut, -1);
  free(zOut);
  return TH_OK;
}

/*
** True if output is enabled.  False if disabled.







|







260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
  const char **argv,
  int *argl
){
  char *zOut;
  if( argc!=2 ){
    return Th_WrongNumArgs(interp, "httpize STRING");
  }
  zOut = httpize((char*)argv[1], TH1_LEN(argl[1]));
  Th_SetResult(interp, zOut, -1);
  free(zOut);
  return TH_OK;
}

/*
** True if output is enabled.  False if disabled.
289
290
291
292
293
294
295
296

297
298
299
300
301
302
303
){
  int rc;
  if( argc<2 || argc>3 ){
    return Th_WrongNumArgs(interp, "enable_output [LABEL] BOOLEAN");
  }
  rc = Th_ToInt(interp, argv[argc-1], argl[argc-1], &enableOutput);
  if( g.thTrace ){
    Th_Trace("enable_output {%.*s} -> %d<br>\n", argl[1],argv[1],enableOutput);

  }
  return rc;
}

/*
** TH1 command: enable_htmlify ?BOOLEAN?
**







|
>







289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
){
  int rc;
  if( argc<2 || argc>3 ){
    return Th_WrongNumArgs(interp, "enable_output [LABEL] BOOLEAN");
  }
  rc = Th_ToInt(interp, argv[argc-1], argl[argc-1], &enableOutput);
  if( g.thTrace ){
    Th_Trace("enable_output {%.*s} -> %d<br>\n",
             TH1_LEN(argl[1]),argv[1],enableOutput);
  }
  return rc;
}

/*
** TH1 command: enable_htmlify ?BOOLEAN?
**
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
                           "enable_htmlify [TRACE_LABEL] ?BOOLEAN?");
  }
  buul = (TH_INIT_NO_ENCODE & g.th1Flags) ? 0 : 1;
  Th_SetResultInt(g.interp, buul);
  if(argc>1){
    if( g.thTrace ){
      Th_Trace("enable_htmlify {%.*s} -> %d<br>\n",
               argl[1],argv[1],buul);
    }
    rc = Th_ToInt(interp, argv[argc-1], argl[argc-1], &buul);
    if(!rc){
      if(buul){
        g.th1Flags &= ~TH_INIT_NO_ENCODE;
      }else{
        g.th1Flags |= TH_INIT_NO_ENCODE;







|







321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
                           "enable_htmlify [TRACE_LABEL] ?BOOLEAN?");
  }
  buul = (TH_INIT_NO_ENCODE & g.th1Flags) ? 0 : 1;
  Th_SetResultInt(g.interp, buul);
  if(argc>1){
    if( g.thTrace ){
      Th_Trace("enable_htmlify {%.*s} -> %d<br>\n",
               TH1_LEN(argl[1]),argv[1],buul);
    }
    rc = Th_ToInt(interp, argv[argc-1], argl[argc-1], &buul);
    if(!rc){
      if(buul){
        g.th1Flags &= ~TH_INIT_NO_ENCODE;
      }else{
        g.th1Flags |= TH_INIT_NO_ENCODE;
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394




395
396
397
398
399
400
401
** Escape all characters with special meaning to HTML if the encode
** parameter is true, with the exception that that flag is ignored if
** g.th1Flags has the TH_INIT_NO_ENCODE flag.
**
** If pOut is NULL and the global pThOut is not then that blob
** is used for output.
*/
static void sendText(Blob * pOut, const char *z, int n, int encode){
  if(0==pOut && pThOut!=0){
    pOut = pThOut;
  }
  if(TH_INIT_NO_ENCODE & g.th1Flags){
    encode = 0;
  }
  if( enableOutput && n ){
    if( n<0 ) n = strlen(z);




    if( encode ){
      z = htmlize(z, n);
      n = strlen(z);
    }
    if(pOut!=0){
      blob_append(pOut, z, n);
    }else if( g.cgiOutput ){







|







|
>
>
>
>







380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
** Escape all characters with special meaning to HTML if the encode
** parameter is true, with the exception that that flag is ignored if
** g.th1Flags has the TH_INIT_NO_ENCODE flag.
**
** If pOut is NULL and the global pThOut is not then that blob
** is used for output.
*/
static void sendText(Blob *pOut, const char *z, int n, int encode){
  if(0==pOut && pThOut!=0){
    pOut = pThOut;
  }
  if(TH_INIT_NO_ENCODE & g.th1Flags){
    encode = 0;
  }
  if( enableOutput && n ){
    if( n<0 ){
      n = strlen(z);
    }else{
      n = TH1_LEN(n);
    }
    if( encode ){
      z = htmlize(z, n);
      n = strlen(z);
    }
    if(pOut!=0){
      blob_append(pOut, z, n);
    }else if( g.cgiOutput ){
523
524
525
526
527
528
529


530
531
532






533
534
535
536
537
538
539
540
static int putsCmd(
  Th_Interp *interp,
  void *pConvert,
  int argc,
  const char **argv,
  int *argl
){


  if( argc!=2 ){
    return Th_WrongNumArgs(interp, "puts STRING");
  }






  sendText(0,(char*)argv[1], argl[1], *(unsigned int*)pConvert);
  return TH_OK;
}

/*
** TH1 command: redirect URL ?withMethod?
**
** Issues an HTTP redirect to the specified URL and then exits the process.







>
>



>
>
>
>
>
>
|







528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
static int putsCmd(
  Th_Interp *interp,
  void *pConvert,
  int argc,
  const char **argv,
  int *argl
){
  int encode = *(unsigned int*)pConvert;
  int n;
  if( argc!=2 ){
    return Th_WrongNumArgs(interp, "puts STRING");
  }
  n = argl[1];
  if( encode==0 && n>0 && TH1_TAINTED(n) ){
    if( Th_ReportTaint(interp, "output string", argv[1], n) ){
      return TH_ERROR;
    }
  }
  sendText(0,(char*)argv[1], TH1_LEN(n), encode);
  return TH_OK;
}

/*
** TH1 command: redirect URL ?withMethod?
**
** Issues an HTTP redirect to the specified URL and then exits the process.
555
556
557
558
559
560
561





562
563
564
565
566
567
568
  if( argc!=2 && argc!=3 ){
    return Th_WrongNumArgs(interp, "redirect URL ?withMethod?");
  }
  if( argc==3 ){
    if( Th_ToInt(interp, argv[2], argl[2], &withMethod) ){
      return TH_ERROR;
    }





  }
  if( withMethod ){
    cgi_redirect_with_method(argv[1]);
  }else{
    cgi_redirect(argv[1]);
  }
  Th_SetResult(interp, argv[1], argl[1]); /* NOT REACHED */







>
>
>
>
>







568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
  if( argc!=2 && argc!=3 ){
    return Th_WrongNumArgs(interp, "redirect URL ?withMethod?");
  }
  if( argc==3 ){
    if( Th_ToInt(interp, argv[2], argl[2], &withMethod) ){
      return TH_ERROR;
    }
  }
  if( TH1_TAINTED(argl[1])
   && Th_ReportTaint(interp,"redirect URL",argv[1],argl[1])
  ){
    return TH_ERROR;
  }
  if( withMethod ){
    cgi_redirect_with_method(argv[1]);
  }else{
    cgi_redirect(argv[1]);
  }
  Th_SetResult(interp, argv[1], argl[1]); /* NOT REACHED */
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
  Blob src, title, body;
  char *zValue = 0;
  int nValue = 0;
  if( argc!=2 ){
    return Th_WrongNumArgs(interp, "markdown STRING");
  }
  blob_zero(&src);
  blob_init(&src, (char*)argv[1], argl[1]);
  blob_zero(&title); blob_zero(&body);
  markdown_to_html(&src, &title, &body);
  Th_ListAppend(interp, &zValue, &nValue, blob_str(&title), blob_size(&title));
  Th_ListAppend(interp, &zValue, &nValue, blob_str(&body), blob_size(&body));
  Th_SetResult(interp, zValue, nValue);
  Th_Free(interp, zValue);
  return TH_OK;







|







676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
  Blob src, title, body;
  char *zValue = 0;
  int nValue = 0;
  if( argc!=2 ){
    return Th_WrongNumArgs(interp, "markdown STRING");
  }
  blob_zero(&src);
  blob_init(&src, (char*)argv[1], TH1_LEN(argl[1]));
  blob_zero(&title); blob_zero(&body);
  markdown_to_html(&src, &title, &body);
  Th_ListAppend(interp, &zValue, &nValue, blob_str(&title), blob_size(&title));
  Th_ListAppend(interp, &zValue, &nValue, blob_str(&body), blob_size(&body));
  Th_SetResult(interp, zValue, nValue);
  Th_Free(interp, zValue);
  return TH_OK;
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
){
  int flags = WIKI_INLINE | WIKI_NOBADLINKS | *(unsigned int*)p;
  if( argc!=2 ){
    return Th_WrongNumArgs(interp, "wiki STRING");
  }
  if( enableOutput ){
    Blob src;
    blob_init(&src, (char*)argv[1], argl[1]);
    wiki_convert(&src, 0, flags);
    blob_reset(&src);
  }
  return TH_OK;
}

/*







|







706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
){
  int flags = WIKI_INLINE | WIKI_NOBADLINKS | *(unsigned int*)p;
  if( argc!=2 ){
    return Th_WrongNumArgs(interp, "wiki STRING");
  }
  if( enableOutput ){
    Blob src;
    blob_init(&src, (char*)argv[1], TH1_LEN(argl[1]));
    wiki_convert(&src, 0, flags);
    blob_reset(&src);
  }
  return TH_OK;
}

/*
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
  const char **argv,
  int *argl
){
  char *zOut;
  if( argc!=2 ){
    return Th_WrongNumArgs(interp, "htmlize STRING");
  }
  zOut = htmlize((char*)argv[1], argl[1]);
  Th_SetResult(interp, zOut, -1);
  free(zOut);
  return TH_OK;
}

/*
** TH1 command: encode64 STRING







|







751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
  const char **argv,
  int *argl
){
  char *zOut;
  if( argc!=2 ){
    return Th_WrongNumArgs(interp, "htmlize STRING");
  }
  zOut = htmlize((char*)argv[1], TH1_LEN(argl[1]));
  Th_SetResult(interp, zOut, -1);
  free(zOut);
  return TH_OK;
}

/*
** TH1 command: encode64 STRING
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
  const char **argv,
  int *argl
){
  char *zOut;
  if( argc!=2 ){
    return Th_WrongNumArgs(interp, "encode64 STRING");
  }
  zOut = encode64((char*)argv[1], argl[1]);
  Th_SetResult(interp, zOut, -1);
  free(zOut);
  return TH_OK;
}

/*
** TH1 command: date
**
** Return a string which is the current time and date.  If the
** -local option is used, the date appears using localtime instead
** of UTC.
*/
static int dateCmd(
  Th_Interp *interp,
  void *p,
  int argc,
  const char **argv,
  int *argl
){
  char *zOut;
  if( argc>=2 && argl[1]==6 && memcmp(argv[1],"-local",6)==0 ){
    zOut = db_text("??", "SELECT datetime('now',toLocal())");
  }else{
    zOut = db_text("??", "SELECT datetime('now')");
  }
  Th_SetResult(interp, zOut, -1);
  free(zOut);
  return TH_OK;







|




















|







773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
  const char **argv,
  int *argl
){
  char *zOut;
  if( argc!=2 ){
    return Th_WrongNumArgs(interp, "encode64 STRING");
  }
  zOut = encode64((char*)argv[1], TH1_LEN(argl[1]));
  Th_SetResult(interp, zOut, -1);
  free(zOut);
  return TH_OK;
}

/*
** TH1 command: date
**
** Return a string which is the current time and date.  If the
** -local option is used, the date appears using localtime instead
** of UTC.
*/
static int dateCmd(
  Th_Interp *interp,
  void *p,
  int argc,
  const char **argv,
  int *argl
){
  char *zOut;
  if( argc>=2 && TH1_LEN(argl[1])==6 && memcmp(argv[1],"-local",6)==0 ){
    zOut = db_text("??", "SELECT datetime('now',toLocal())");
  }else{
    zOut = db_text("??", "SELECT datetime('now')");
  }
  Th_SetResult(interp, zOut, -1);
  free(zOut);
  return TH_OK;
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
  char *zCapList = 0;
  int nCapList = 0;
  if( argc<2 ){
    return Th_WrongNumArgs(interp, "hascap STRING ...");
  }
  for(i=1; rc==1 && i<argc; i++){
    if( g.thTrace ){
      Th_ListAppend(interp, &zCapList, &nCapList, argv[i], argl[i]);
    }
    rc = login_has_capability((char*)argv[i],argl[i],*(int*)p);
  }
  if( g.thTrace ){
    Th_Trace("[%s %#h] => %d<br>\n", argv[0], nCapList, zCapList, rc);
    Th_Free(interp, zCapList);
  }
  Th_SetResultInt(interp, rc);
  return TH_OK;







|

|







826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
  char *zCapList = 0;
  int nCapList = 0;
  if( argc<2 ){
    return Th_WrongNumArgs(interp, "hascap STRING ...");
  }
  for(i=1; rc==1 && i<argc; i++){
    if( g.thTrace ){
      Th_ListAppend(interp, &zCapList, &nCapList, argv[i], TH1_LEN(argl[i]));
    }
    rc = login_has_capability((char*)argv[i],TH1_LEN(argl[i]),*(int*)p);
  }
  if( g.thTrace ){
    Th_Trace("[%s %#h] => %d<br>\n", argv[0], nCapList, zCapList, rc);
    Th_Free(interp, zCapList);
  }
  Th_SetResultInt(interp, rc);
  return TH_OK;
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
  int nCap;
  int rc;
  int i;

  if( argc!=2 ){
    return Th_WrongNumArgs(interp, "capexpr EXPR");
  }
  rc = Th_SplitList(interp, argv[1], argl[1], &azCap, &anCap, &nCap);
  if( rc ) return rc;
  rc = 0;
  for(i=0; i<nCap; i++){
    if( azCap[i][0]=='!' ){
      rc = !login_has_capability(azCap[i]+1, anCap[i]-1, 0);
    }else if( azCap[i][0]=='@' ){
      rc = login_has_capability(azCap[i]+1, anCap[i]-1, LOGIN_ANON);







|







874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
  int nCap;
  int rc;
  int i;

  if( argc!=2 ){
    return Th_WrongNumArgs(interp, "capexpr EXPR");
  }
  rc = Th_SplitList(interp, argv[1], TH1_LEN(argl[1]), &azCap, &anCap, &nCap);
  if( rc ) return rc;
  rc = 0;
  for(i=0; i<nCap; i++){
    if( azCap[i][0]=='!' ){
      rc = !login_has_capability(azCap[i]+1, anCap[i]-1, 0);
    }else if( azCap[i][0]=='@' ){
      rc = login_has_capability(azCap[i]+1, anCap[i]-1, LOGIN_ANON);
919
920
921
922
923
924
925

926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
  int rc = 1, i, j;
  unsigned int searchCap = search_restrict(SRCH_ALL);
  if( argc<2 ){
    return Th_WrongNumArgs(interp, "hascap STRING ...");
  }
  for(i=1; i<argc && rc; i++){
    int match = 0;

    for(j=0; j<argl[i]; j++){
      switch( argv[i][j] ){
        case 'c':  match |= searchCap & SRCH_CKIN;  break;
        case 'd':  match |= searchCap & SRCH_DOC;   break;
        case 't':  match |= searchCap & SRCH_TKT;   break;
        case 'w':  match |= searchCap & SRCH_WIKI;  break;
      }
    }
    if( !match ) rc = 0;
  }
  if( g.thTrace ){
    Th_Trace("[searchable %#h] => %d<br>\n", argl[1], argv[1], rc);
  }
  Th_SetResultInt(interp, rc);
  return TH_OK;
}

/*
** TH1 command: hasfeature STRING







>
|










|







937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
  int rc = 1, i, j;
  unsigned int searchCap = search_restrict(SRCH_ALL);
  if( argc<2 ){
    return Th_WrongNumArgs(interp, "hascap STRING ...");
  }
  for(i=1; i<argc && rc; i++){
    int match = 0;
    int nn = TH1_LEN(argl[i]);
    for(j=0; j<nn; j++){
      switch( argv[i][j] ){
        case 'c':  match |= searchCap & SRCH_CKIN;  break;
        case 'd':  match |= searchCap & SRCH_DOC;   break;
        case 't':  match |= searchCap & SRCH_TKT;   break;
        case 'w':  match |= searchCap & SRCH_WIKI;  break;
      }
    }
    if( !match ) rc = 0;
  }
  if( g.thTrace ){
    Th_Trace("[searchable %#h] => %d<br>\n", TH1_LEN(argl[1]), argv[1], rc);
  }
  Th_SetResultInt(interp, rc);
  return TH_OK;
}

/*
** TH1 command: hasfeature STRING
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
    rc = 1;
  }
#endif
  else if( 0 == fossil_strnicmp( zArg, "markdown\0", 9 ) ){
    rc = 1;
  }
  if( g.thTrace ){
    Th_Trace("[hasfeature %#h] => %d<br>\n", argl[1], zArg, rc);
  }
  Th_SetResultInt(interp, rc);
  return TH_OK;
}


/*







|







1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
    rc = 1;
  }
#endif
  else if( 0 == fossil_strnicmp( zArg, "markdown\0", 9 ) ){
    rc = 1;
  }
  if( g.thTrace ){
    Th_Trace("[hasfeature %#h] => %d<br>\n", TH1_LEN(argl[1]), zArg, rc);
  }
  Th_SetResultInt(interp, rc);
  return TH_OK;
}


/*
1102
1103
1104
1105
1106
1107
1108

1109
1110
1111

1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
  void *p,
  int argc,
  const char **argv,
  int *argl
){
  int rc = 0;
  int i;

  if( argc!=2 ){
    return Th_WrongNumArgs(interp, "anycap STRING");
  }

  for(i=0; rc==0 && i<argl[1]; i++){
    rc = login_has_capability((char*)&argv[1][i],1,0);
  }
  if( g.thTrace ){
    Th_Trace("[anycap %#h] => %d<br>\n", argl[1], argv[1], rc);
  }
  Th_SetResultInt(interp, rc);
  return TH_OK;
}

/*
** TH1 command: combobox NAME TEXT-LIST NUMLINES







>



>
|



|







1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
  void *p,
  int argc,
  const char **argv,
  int *argl
){
  int rc = 0;
  int i;
  int nn;
  if( argc!=2 ){
    return Th_WrongNumArgs(interp, "anycap STRING");
  }
  nn = TH1_LEN(argl[1]);
  for(i=0; rc==0 && i<nn; i++){
    rc = login_has_capability((char*)&argv[1][i],1,0);
  }
  if( g.thTrace ){
    Th_Trace("[anycap %#h] => %d<br>\n", TH1_LEN(argl[1]), argv[1], rc);
  }
  Th_SetResultInt(interp, rc);
  return TH_OK;
}

/*
** TH1 command: combobox NAME TEXT-LIST NUMLINES
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156

1157
1158
1159
1160
1161
1162
1163
){
  if( argc!=4 ){
    return Th_WrongNumArgs(interp, "combobox NAME TEXT-LIST NUMLINES");
  }
  if( enableOutput ){
    int height;
    Blob name;
    int nValue;
    const char *zValue;
    char *z, *zH;
    int nElem;
    int *aszElem;
    char **azElem;
    int i;

    if( Th_ToInt(interp, argv[3], argl[3], &height) ) return TH_ERROR;
    Th_SplitList(interp, argv[2], argl[2], &azElem, &aszElem, &nElem);
    blob_init(&name, (char*)argv[1], argl[1]);
    zValue = Th_Fetch(blob_str(&name), &nValue);

    zH = htmlize(blob_buffer(&name), blob_size(&name));
    z = mprintf("<select id=\"%s\" name=\"%s\" size=\"%d\">", zH, zH, height);
    free(zH);
    sendText(0,z, -1, 0);
    free(z);
    blob_reset(&name);
    for(i=0; i<nElem; i++){







|








|
|

>







1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
){
  if( argc!=4 ){
    return Th_WrongNumArgs(interp, "combobox NAME TEXT-LIST NUMLINES");
  }
  if( enableOutput ){
    int height;
    Blob name;
    int nValue = 0;
    const char *zValue;
    char *z, *zH;
    int nElem;
    int *aszElem;
    char **azElem;
    int i;

    if( Th_ToInt(interp, argv[3], argl[3], &height) ) return TH_ERROR;
    Th_SplitList(interp, argv[2], TH1_LEN(argl[2]), &azElem, &aszElem, &nElem);
    blob_init(&name, (char*)argv[1], TH1_LEN(argl[1]));
    zValue = Th_Fetch(blob_str(&name), &nValue);
    nValue = TH1_LEN(nValue);
    zH = htmlize(blob_buffer(&name), blob_size(&name));
    z = mprintf("<select id=\"%s\" name=\"%s\" size=\"%d\">", zH, zH, height);
    free(zH);
    sendText(0,z, -1, 0);
    free(z);
    blob_reset(&name);
    for(i=0; i<nElem; i++){
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
  int iMin, iMax;
  if( argc!=4 ){
    return Th_WrongNumArgs(interp, "linecount STRING MAX MIN");
  }
  if( Th_ToInt(interp, argv[2], argl[2], &iMax) ) return TH_ERROR;
  if( Th_ToInt(interp, argv[3], argl[3], &iMin) ) return TH_ERROR;
  z = argv[1];
  size = argl[1];
  for(n=1, i=0; i<size; i++){
    if( z[i]=='\n' ){
      n++;
      if( n>=iMax ) break;
    }
  }
  if( n<iMin ) n = iMin;







|







1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
  int iMin, iMax;
  if( argc!=4 ){
    return Th_WrongNumArgs(interp, "linecount STRING MAX MIN");
  }
  if( Th_ToInt(interp, argv[2], argl[2], &iMax) ) return TH_ERROR;
  if( Th_ToInt(interp, argv[3], argl[3], &iMin) ) return TH_ERROR;
  z = argv[1];
  size = TH1_LEN(argl[1]);
  for(n=1, i=0; i<size; i++){
    if( z[i]=='\n' ){
      n++;
      if( n>=iMax ) break;
    }
  }
  if( n<iMin ) n = iMin;
1405
1406
1407
1408
1409
1410
1411
1412

1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430


1431
1432
1433
1434
1435
1436
1437


1438
1439
1440
1441
1442
1443
1444
  }else if( fossil_strnicmp(argv[1], "user\0", 5)==0 ){
    Th_SetResult(interp, g.zLogin ? g.zLogin : zDefault, -1);
    return TH_OK;
  }else if( fossil_strnicmp(argv[1], "vfs\0", 4)==0 ){
    Th_SetResult(interp, g.zVfsName ? g.zVfsName : zDefault, -1);
    return TH_OK;
  }else{
    Th_ErrorMessage(interp, "unsupported global state:", argv[1], argl[1]);

    return TH_ERROR;
  }
}

/*
** TH1 command: getParameter NAME ?DEFAULT?
**
** Return the value of the specified query parameter or the specified default
** value when there is no matching query parameter.
*/
static int getParameterCmd(
  Th_Interp *interp,
  void *p,
  int argc,
  const char **argv,
  int *argl
){
  const char *zDefault = 0;


  if( argc!=2 && argc!=3 ){
    return Th_WrongNumArgs(interp, "getParameter NAME ?DEFAULT?");
  }
  if( argc==3 ){
    zDefault = argv[2];
  }
  Th_SetResult(interp, cgi_parameter(argv[1], zDefault), -1);


  return TH_OK;
}

/*
** TH1 command: setParameter NAME VALUE
**
** Sets the value of the specified query parameter.







|
>


















>
>






|
>
>







1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
  }else if( fossil_strnicmp(argv[1], "user\0", 5)==0 ){
    Th_SetResult(interp, g.zLogin ? g.zLogin : zDefault, -1);
    return TH_OK;
  }else if( fossil_strnicmp(argv[1], "vfs\0", 4)==0 ){
    Th_SetResult(interp, g.zVfsName ? g.zVfsName : zDefault, -1);
    return TH_OK;
  }else{
    Th_ErrorMessage(interp, "unsupported global state:",
                            argv[1], TH1_LEN(argl[1]));
    return TH_ERROR;
  }
}

/*
** TH1 command: getParameter NAME ?DEFAULT?
**
** Return the value of the specified query parameter or the specified default
** value when there is no matching query parameter.
*/
static int getParameterCmd(
  Th_Interp *interp,
  void *p,
  int argc,
  const char **argv,
  int *argl
){
  const char *zDefault = 0;
  const char *zVal;
  int sz;
  if( argc!=2 && argc!=3 ){
    return Th_WrongNumArgs(interp, "getParameter NAME ?DEFAULT?");
  }
  if( argc==3 ){
    zDefault = argv[2];
  }
  zVal = cgi_parameter(argv[1], zDefault);
  sz = th_strlen(zVal);
  Th_SetResult(interp, zVal, TH1_ADD_TAINT(sz));
  return TH_OK;
}

/*
** TH1 command: setParameter NAME VALUE
**
** Sets the value of the specified query parameter.
1846
1847
1848
1849
1850
1851
1852





































1853
1854
1855
1856
1857
1858
1859
  char zUTime[50];
  fossil_cpu_times(0, &x);
  sqlite3_snprintf(sizeof(zUTime), zUTime, "%llu", x);
  Th_SetResult(interp, zUTime, -1);
  return TH_OK;
}







































/*
** TH1 command: randhex  N
**
** Return N*2 random hexadecimal digits with N<50.  If N is omitted,
** use a value of 10.
*/







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
  char zUTime[50];
  fossil_cpu_times(0, &x);
  sqlite3_snprintf(sizeof(zUTime), zUTime, "%llu", x);
  Th_SetResult(interp, zUTime, -1);
  return TH_OK;
}

/*
** TH1 command: taint STRING
**
** Return a copy of STRING that is marked as tainted.
*/
static int taintCmd(
  Th_Interp *interp,
  void *p,
  int argc,
  const char **argv,
  int *argl
){
  if( argc!=2 ){
    return Th_WrongNumArgs(interp, "STRING");
  }
  Th_SetResult(interp, argv[1], TH1_ADD_TAINT(argl[1]));
  return TH_OK;
}

/*
** TH1 command: untaint STRING
**
** Return a copy of STRING that is marked as untainted.
*/
static int untaintCmd(
  Th_Interp *interp,
  void *p,
  int argc,
  const char **argv,
  int *argl
){
  if( argc!=2 ){
    return Th_WrongNumArgs(interp, "STRING");
  }
  Th_SetResult(interp, argv[1], TH1_LEN(argl[1]));
  return TH_OK;
}

/*
** TH1 command: randhex  N
**
** Return N*2 random hexadecimal digits with N<50.  If N is omitted,
** use a value of 10.
*/
1921
1922
1923
1924
1925
1926
1927

1928

1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943







1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
  const char *zTail;
  int n, i;
  int res = TH_OK;
  int nVar;
  char *zErr = 0;
  int noComplain = 0;


  if( argc>3 && argl[1]==11 && strncmp(argv[1], "-nocomplain", 11)==0 ){

    argc--;
    argv++;
    argl++;
    noComplain = 1;
  }
  if( argc!=3 ){
    return Th_WrongNumArgs(interp, "query SQL CODE");
  }
  if( g.db==0 ){
    if( noComplain ) return TH_OK;
    Th_ErrorMessage(interp, "database is not open", 0, 0);
    return TH_ERROR;
  }
  zSql = argv[1];
  nSql = argl[1];







  while( res==TH_OK && nSql>0 ){
    zErr = 0;
    report_restrict_sql(&zErr);
    g.dbIgnoreErrors++;
    rc = sqlite3_prepare_v2(g.db, argv[1], argl[1], &pStmt, &zTail);
    g.dbIgnoreErrors--;
    report_unrestrict_sql();
    if( rc!=0 || zErr!=0 ){
      if( noComplain ) return TH_OK;
      Th_ErrorMessage(interp, "SQL error: ",
                      zErr ? zErr : sqlite3_errmsg(g.db), -1);
      return TH_ERROR;
    }
    n = (int)(zTail - zSql);
    zSql += n;
    nSql -= n;
    if( pStmt==0 ) continue;
    nVar = sqlite3_bind_parameter_count(pStmt);
    for(i=1; i<=nVar; i++){
      const char *zVar = sqlite3_bind_parameter_name(pStmt, i);
      int szVar = zVar ? th_strlen(zVar) : 0;
      if( szVar>1 && zVar[0]=='$'
       && Th_GetVar(interp, zVar+1, szVar-1)==TH_OK ){
        int nVal;
        const char *zVal = Th_GetResult(interp, &nVal);
        sqlite3_bind_text(pStmt, i, zVal, nVal, SQLITE_TRANSIENT);
      }
    }
    while( res==TH_OK && ignore_errors_step(pStmt)==SQLITE_ROW ){
      int nCol = sqlite3_column_count(pStmt);
      for(i=0; i<nCol; i++){
        const char *zCol = sqlite3_column_name(pStmt, i);
        int szCol = th_strlen(zCol);
        const char *zVal = (const char*)sqlite3_column_text(pStmt, i);
        int szVal = sqlite3_column_bytes(pStmt, i);
        Th_SetVar(interp, zCol, szCol, zVal, szVal);
      }
      if( g.thTrace ){
        Th_Trace("query_eval {<pre>%#h</pre>}<br>\n", argl[2], argv[2]);
      }
      res = Th_Eval(interp, 0, argv[2], argl[2]);
      if( g.thTrace ){
        int nTrRes;
        char *zTrRes = (char*)Th_GetResult(g.interp, &nTrRes);
        Th_Trace("[query_eval] => %h {%#h}<br>\n",
                 Th_ReturnCodeName(res, 0), nTrRes, zTrRes);
      }
      if( res==TH_BREAK || res==TH_CONTINUE ) res = TH_OK;
    }
    rc = sqlite3_finalize(pStmt);
    if( rc!=SQLITE_OK ){
      if( noComplain ) return TH_OK;
      Th_ErrorMessage(interp, "SQL error: ", sqlite3_errmsg(g.db), -1);







>
|
>















>
>
>
>
>
>
>




|




















|









|


|

|




|







1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
  const char *zTail;
  int n, i;
  int res = TH_OK;
  int nVar;
  char *zErr = 0;
  int noComplain = 0;

  if( argc>3 && TH1_LEN(argl[1])==11
   && strncmp(argv[1], "-nocomplain", 11)==0
  ){
    argc--;
    argv++;
    argl++;
    noComplain = 1;
  }
  if( argc!=3 ){
    return Th_WrongNumArgs(interp, "query SQL CODE");
  }
  if( g.db==0 ){
    if( noComplain ) return TH_OK;
    Th_ErrorMessage(interp, "database is not open", 0, 0);
    return TH_ERROR;
  }
  zSql = argv[1];
  nSql = argl[1];
  if( TH1_TAINTED(nSql) ){
    if( Th_ReportTaint(interp,"query SQL",zSql,nSql) ){
      return TH_ERROR;
    }
    nSql = TH1_LEN(nSql);
  }

  while( res==TH_OK && nSql>0 ){
    zErr = 0;
    report_restrict_sql(&zErr);
    g.dbIgnoreErrors++;
    rc = sqlite3_prepare_v2(g.db, argv[1], TH1_LEN(argl[1]), &pStmt, &zTail);
    g.dbIgnoreErrors--;
    report_unrestrict_sql();
    if( rc!=0 || zErr!=0 ){
      if( noComplain ) return TH_OK;
      Th_ErrorMessage(interp, "SQL error: ",
                      zErr ? zErr : sqlite3_errmsg(g.db), -1);
      return TH_ERROR;
    }
    n = (int)(zTail - zSql);
    zSql += n;
    nSql -= n;
    if( pStmt==0 ) continue;
    nVar = sqlite3_bind_parameter_count(pStmt);
    for(i=1; i<=nVar; i++){
      const char *zVar = sqlite3_bind_parameter_name(pStmt, i);
      int szVar = zVar ? th_strlen(zVar) : 0;
      if( szVar>1 && zVar[0]=='$'
       && Th_GetVar(interp, zVar+1, szVar-1)==TH_OK ){
        int nVal;
        const char *zVal = Th_GetResult(interp, &nVal);
        sqlite3_bind_text(pStmt, i, zVal, TH1_LEN(nVal), SQLITE_TRANSIENT);
      }
    }
    while( res==TH_OK && ignore_errors_step(pStmt)==SQLITE_ROW ){
      int nCol = sqlite3_column_count(pStmt);
      for(i=0; i<nCol; i++){
        const char *zCol = sqlite3_column_name(pStmt, i);
        int szCol = th_strlen(zCol);
        const char *zVal = (const char*)sqlite3_column_text(pStmt, i);
        int szVal = sqlite3_column_bytes(pStmt, i);
        Th_SetVar(interp, zCol, szCol, zVal, TH1_ADD_TAINT(szVal));
      }
      if( g.thTrace ){
        Th_Trace("query_eval {<pre>%#h</pre>}<br>\n",TH1_LEN(argl[2]),argv[2]);
      }
      res = Th_Eval(interp, 0, argv[2], TH1_LEN(argl[2]));
      if( g.thTrace ){
        int nTrRes;
        char *zTrRes = (char*)Th_GetResult(g.interp, &nTrRes);
        Th_Trace("[query_eval] => %h {%#h}<br>\n",
                 Th_ReturnCodeName(res, 0), TH1_LEN(nTrRes), zTrRes);
      }
      if( res==TH_BREAK || res==TH_CONTINUE ) res = TH_OK;
    }
    rc = sqlite3_finalize(pStmt);
    if( rc!=SQLITE_OK ){
      if( noComplain ) return TH_OK;
      Th_ErrorMessage(interp, "SQL error: ", sqlite3_errmsg(g.db), -1);
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
    rc = TH_ERROR;
  }else{
    Th_SetResult(interp, 0, 0);
    rc = TH_OK;
  }
  if( g.thTrace ){
    Th_Trace("[setting %s%#h] => %d<br>\n", strict ? "strict " : "",
             argl[nArg], argv[nArg], rc);
  }
  return rc;
}

/*
** TH1 command: glob_match ?-one? ?--? patternList string
**







|







2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
    rc = TH_ERROR;
  }else{
    Th_SetResult(interp, 0, 0);
    rc = TH_OK;
  }
  if( g.thTrace ){
    Th_Trace("[setting %s%#h] => %d<br>\n", strict ? "strict " : "",
             TH1_LEN(argl[nArg]), argv[nArg], rc);
  }
  return rc;
}

/*
** TH1 command: glob_match ?-one? ?--? patternList string
**
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
  if( fossil_strcmp(argv[nArg], "--")==0 ) nArg++;
  if( nArg+2!=argc ){
    return Th_WrongNumArgs(interp, REGEXP_WRONGNUMARGS);
  }
  zErr = re_compile(&pRe, argv[nArg], noCase);
  if( !zErr ){
    Th_SetResultInt(interp, re_match(pRe,
        (const unsigned char *)argv[nArg+1], argl[nArg+1]));
    rc = TH_OK;
  }else{
    Th_SetResult(interp, zErr, -1);
    rc = TH_ERROR;
  }
  re_free(pRe);
  return rc;







|







2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
  if( fossil_strcmp(argv[nArg], "--")==0 ) nArg++;
  if( nArg+2!=argc ){
    return Th_WrongNumArgs(interp, REGEXP_WRONGNUMARGS);
  }
  zErr = re_compile(&pRe, argv[nArg], noCase);
  if( !zErr ){
    Th_SetResultInt(interp, re_match(pRe,
        (const unsigned char *)argv[nArg+1], TH1_LEN(argl[nArg+1])));
    rc = TH_OK;
  }else{
    Th_SetResult(interp, zErr, -1);
    rc = TH_ERROR;
  }
  re_free(pRe);
  return rc;
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
  Blob payload;
  ReCompiled *pRe = 0;
  UrlData urlData;

  if( argc<2 || argc>5 ){
    return Th_WrongNumArgs(interp, HTTP_WRONGNUMARGS);
  }
  if( fossil_strnicmp(argv[nArg], "-asynchronous", argl[nArg])==0 ){
    fAsynchronous = 1; nArg++;
  }
  if( fossil_strcmp(argv[nArg], "--")==0 ) nArg++;
  if( nArg+1!=argc && nArg+2!=argc ){
    return Th_WrongNumArgs(interp, REGEXP_WRONGNUMARGS);
  }
  memset(&urlData, '\0', sizeof(urlData));







|







2231
2232
2233
2234
2235
2236
2237
2238
2239
2240
2241
2242
2243
2244
2245
  Blob payload;
  ReCompiled *pRe = 0;
  UrlData urlData;

  if( argc<2 || argc>5 ){
    return Th_WrongNumArgs(interp, HTTP_WRONGNUMARGS);
  }
  if( fossil_strnicmp(argv[nArg], "-asynchronous", TH1_LEN(argl[nArg]))==0 ){
    fAsynchronous = 1; nArg++;
  }
  if( fossil_strcmp(argv[nArg], "--")==0 ) nArg++;
  if( nArg+1!=argc && nArg+2!=argc ){
    return Th_WrongNumArgs(interp, REGEXP_WRONGNUMARGS);
  }
  memset(&urlData, '\0', sizeof(urlData));
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
    Th_SetResult(interp, "url not allowed", -1);
    re_free(pRe);
    return TH_ERROR;
  }
  re_free(pRe);
  blob_zero(&payload);
  if( nArg+2==argc ){
    blob_append(&payload, argv[nArg+1], argl[nArg+1]);
    zType = "POST";
  }else{
    zType = "GET";
  }
  if( fAsynchronous ){
    const char *zSep, *zParams;
    Blob hdr;







|







2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
2270
2271
2272
2273
2274
    Th_SetResult(interp, "url not allowed", -1);
    re_free(pRe);
    return TH_ERROR;
  }
  re_free(pRe);
  blob_zero(&payload);
  if( nArg+2==argc ){
    blob_append(&payload, argv[nArg+1], TH1_LEN(argl[nArg+1]));
    zType = "POST";
  }else{
    zType = "GET";
  }
  if( fAsynchronous ){
    const char *zSep, *zParams;
    Blob hdr;
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
  const char * zStr;
  int nStr, rc;
  if( argc!=2 ){
    return Th_WrongNumArgs(interp, "captureTh1 STRING");
  }
  pOrig = Th_SetOutputBlob(&out);
  zStr = argv[1];
  nStr = argl[1];
  rc = Th_Eval(g.interp, 0, zStr, nStr);
  Th_SetOutputBlob(pOrig);
  if(0==rc){
    Th_SetResult(g.interp, blob_str(&out), blob_size(&out));
  }
  blob_reset(&out);
  return rc;







|







2339
2340
2341
2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
2352
2353
  const char * zStr;
  int nStr, rc;
  if( argc!=2 ){
    return Th_WrongNumArgs(interp, "captureTh1 STRING");
  }
  pOrig = Th_SetOutputBlob(&out);
  zStr = argv[1];
  nStr = TH1_LEN(argl[1]);
  rc = Th_Eval(g.interp, 0, zStr, nStr);
  Th_SetOutputBlob(pOrig);
  if(0==rc){
    Th_SetResult(g.interp, blob_str(&out), blob_size(&out));
  }
  blob_reset(&out);
  return rc;
2385
2386
2387
2388
2389
2390
2391

2392
2393
2394

2395
2396
2397
2398
2399
2400
2401
    {"searchable",    searchableCmd,        0},
    {"setParameter",  setParameterCmd,      0},
    {"setting",       settingCmd,           0},
    {"styleFooter",   styleFooterCmd,       0},
    {"styleHeader",   styleHeaderCmd,       0},
    {"styleScript",   styleScriptCmd,       0},
    {"submenu",       submenuCmd,           0},

    {"tclReady",      tclReadyCmd,          0},
    {"trace",         traceCmd,             0},
    {"stime",         stimeCmd,             0},

    {"unversioned",   unversionedCmd,       0},
    {"utime",         utimeCmd,             0},
    {"verifyCsrf",    verifyCsrfCmd,        0},
    {"verifyLogin",   verifyLoginCmd,       0},
    {"wiki",          wikiCmd,              (void*)&aFlags[0]},
    {"wiki_assoc",    wikiAssocCmd,         0},
    {0, 0, 0}







>



>







2458
2459
2460
2461
2462
2463
2464
2465
2466
2467
2468
2469
2470
2471
2472
2473
2474
2475
2476
    {"searchable",    searchableCmd,        0},
    {"setParameter",  setParameterCmd,      0},
    {"setting",       settingCmd,           0},
    {"styleFooter",   styleFooterCmd,       0},
    {"styleHeader",   styleHeaderCmd,       0},
    {"styleScript",   styleScriptCmd,       0},
    {"submenu",       submenuCmd,           0},
    {"taint",         taintCmd,             0},
    {"tclReady",      tclReadyCmd,          0},
    {"trace",         traceCmd,             0},
    {"stime",         stimeCmd,             0},
    {"untaint",       untaintCmd,           0},
    {"unversioned",   unversionedCmd,       0},
    {"utime",         utimeCmd,             0},
    {"verifyCsrf",    verifyCsrfCmd,        0},
    {"verifyLogin",   verifyLoginCmd,       0},
    {"wiki",          wikiCmd,              (void*)&aFlags[0]},
    {"wiki_assoc",    wikiAssocCmd,         0},
    {0, 0, 0}
2492
2493
2494
2495
2496
2497
2498
















2499
2500
2501
2502
2503
2504
2505
  if( zValue ){
    if( g.thTrace ){
      Th_Trace("set %h {%h}<br>\n", zName, zValue);
    }
    Th_SetVar(g.interp, zName, -1, zValue, strlen(zValue));
  }
}

















/*
** Appends an element to a TH1 list value.  This function is called by the
** transfer subsystem; therefore, it must be very careful to avoid doing
** any unnecessary work.  To that end, the TH1 subsystem will not be called
** or initialized if the list pointer is zero (i.e. which will be the case
** when TH1 transfer hooks are disabled).







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







2567
2568
2569
2570
2571
2572
2573
2574
2575
2576
2577
2578
2579
2580
2581
2582
2583
2584
2585
2586
2587
2588
2589
2590
2591
2592
2593
2594
2595
2596
  if( zValue ){
    if( g.thTrace ){
      Th_Trace("set %h {%h}<br>\n", zName, zValue);
    }
    Th_SetVar(g.interp, zName, -1, zValue, strlen(zValue));
  }
}

/*
** Store a string value in a variable in the interpreter
** with the "taint" marking, so that TH1 knows that this
** variable contains content under the control of the remote
** user and presents a risk of XSS or SQL-injection attacks.
*/
void Th_StoreUnsafe(const char *zName, const char *zValue){
  Th_FossilInit(TH_INIT_DEFAULT);
  if( zValue ){
    if( g.thTrace ){
      Th_Trace("set %h [taint {%h}]<br>\n", zName, zValue);
    }
    Th_SetVar(g.interp, zName, -1, zValue, TH1_ADD_TAINT(strlen(zValue)));
  }
}

/*
** Appends an element to a TH1 list value.  This function is called by the
** transfer subsystem; therefore, it must be very careful to avoid doing
** any unnecessary work.  To that end, the TH1 subsystem will not be called
** or initialized if the list pointer is zero (i.e. which will be the case
** when TH1 transfer hooks are disabled).
2678
2679
2680
2681
2682
2683
2684

2685
2686
2687
2688
2689
2690
2691
  if( rc==TH_ERROR ){
    int nResult = 0;
    char *zResult = (char*)Th_GetResult(g.interp, &nResult);
    /*
    ** Make sure that the TH1 script error was not caused by a "missing"
    ** command hook handler as that is not actually an error condition.
    */

    if( memcmp(zResult, NO_COMMAND_HOOK_ERROR, nResult)!=0 ){
      sendError(0,zResult, nResult, 0);
    }else{
      /*
      ** There is no command hook handler "installed".  This situation
      ** is NOT actually an error.
      */







>







2769
2770
2771
2772
2773
2774
2775
2776
2777
2778
2779
2780
2781
2782
2783
  if( rc==TH_ERROR ){
    int nResult = 0;
    char *zResult = (char*)Th_GetResult(g.interp, &nResult);
    /*
    ** Make sure that the TH1 script error was not caused by a "missing"
    ** command hook handler as that is not actually an error condition.
    */
    nResult = TH1_LEN(nResult);
    if( memcmp(zResult, NO_COMMAND_HOOK_ERROR, nResult)!=0 ){
      sendError(0,zResult, nResult, 0);
    }else{
      /*
      ** There is no command hook handler "installed".  This situation
      ** is NOT actually an error.
      */
2765
2766
2767
2768
2769
2770
2771

2772
2773
2774
2775
2776
2777
2778
  if( rc==TH_ERROR ){
    int nResult = 0;
    char *zResult = (char*)Th_GetResult(g.interp, &nResult);
    /*
    ** Make sure that the TH1 script error was not caused by a "missing"
    ** webpage hook handler as that is not actually an error condition.
    */

    if( memcmp(zResult, NO_WEBPAGE_HOOK_ERROR, nResult)!=0 ){
      sendError(0,zResult, nResult, 1);
    }else{
      /*
      ** There is no webpage hook handler "installed".  This situation
      ** is NOT actually an error.
      */







>







2857
2858
2859
2860
2861
2862
2863
2864
2865
2866
2867
2868
2869
2870
2871
  if( rc==TH_ERROR ){
    int nResult = 0;
    char *zResult = (char*)Th_GetResult(g.interp, &nResult);
    /*
    ** Make sure that the TH1 script error was not caused by a "missing"
    ** webpage hook handler as that is not actually an error condition.
    */
    nResult = TH1_LEN(nResult);
    if( memcmp(zResult, NO_WEBPAGE_HOOK_ERROR, nResult)!=0 ){
      sendError(0,zResult, nResult, 1);
    }else{
      /*
      ** There is no webpage hook handler "installed".  This situation
      ** is NOT actually an error.
      */
2892
2893
2894
2895
2896
2897
2898




2899

2900
2901
2902
2903
2904
2905
2906
2907
2908
2909
2910
2911
2912
2913
2914
2915
2916
2917
2918
2919
        nVar = n;
        encode = 0;
      }
      rc = Th_GetVar(g.interp, (char*)zVar, nVar);
      z += i+1+n;
      i = 0;
      zResult = (char*)Th_GetResult(g.interp, &n);




      sendText(pOut,(char*)zResult, n, encode);

    }else if( z[i]=='<' && isBeginScriptTag(&z[i]) ){
      sendText(pOut,z, i, 0);
      z += i+5;
      for(i=0; z[i] && (z[i]!='<' || !isEndScriptTag(&z[i])); i++){}
      if( g.thTrace ){
        Th_Trace("render_eval {<pre>%#h</pre>}<br>\n", i, z);
      }
      rc = Th_Eval(g.interp, 0, (const char*)z, i);
      if( g.thTrace ){
        int nTrRes;
        char *zTrRes = (char*)Th_GetResult(g.interp, &nTrRes);
        Th_Trace("[render_eval] => %h {%#h}<br>\n",
                 Th_ReturnCodeName(rc, 0), nTrRes, zTrRes);
      }
      if( rc!=TH_OK ) break;
      z += i;
      if( z[0] ){ z += 6; }
      i = 0;
    }else{
      i++;







>
>
>
>
|
>












|







2985
2986
2987
2988
2989
2990
2991
2992
2993
2994
2995
2996
2997
2998
2999
3000
3001
3002
3003
3004
3005
3006
3007
3008
3009
3010
3011
3012
3013
3014
3015
3016
3017
        nVar = n;
        encode = 0;
      }
      rc = Th_GetVar(g.interp, (char*)zVar, nVar);
      z += i+1+n;
      i = 0;
      zResult = (char*)Th_GetResult(g.interp, &n);
      if( !TH1_TAINTED(n)
       || encode 
       || Th_ReportTaint(g.interp, "inline variable", zVar, nVar)==TH_OK
      ){
        sendText(pOut,(char*)zResult, n, encode);
      }
    }else if( z[i]=='<' && isBeginScriptTag(&z[i]) ){
      sendText(pOut,z, i, 0);
      z += i+5;
      for(i=0; z[i] && (z[i]!='<' || !isEndScriptTag(&z[i])); i++){}
      if( g.thTrace ){
        Th_Trace("render_eval {<pre>%#h</pre>}<br>\n", i, z);
      }
      rc = Th_Eval(g.interp, 0, (const char*)z, i);
      if( g.thTrace ){
        int nTrRes;
        char *zTrRes = (char*)Th_GetResult(g.interp, &nTrRes);
        Th_Trace("[render_eval] => %h {%#h}<br>\n",
                 Th_ReturnCodeName(rc, 0), TH1_LEN(nTrRes), zTrRes);
      }
      if( rc!=TH_OK ) break;
      z += i;
      if( z[0] ){ z += 6; }
      i = 0;
    }else{
      i++;
2951
2952
2953
2954
2955
2956
2957




































































2958
2959
2960
2961
2962
2963
2964
    ** Th_SetOutputBlob() has been called. If it has not been called,
    ** pThOut will be 0, which will redirect the output to CGI/stdout,
    ** as appropriate. We need to pass on g.th1Flags for the case of
    ** recursive calls, so that, e.g., TH_INIT_NO_ENCODE does not get
    ** inadvertently toggled off by a recursive call.
    */;
}





































































/*
** COMMAND: test-th-render
**
** Usage: %fossil test-th-render FILE
**
** Read the content of the file named "FILE" as if it were a header or







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







3049
3050
3051
3052
3053
3054
3055
3056
3057
3058
3059
3060
3061
3062
3063
3064
3065
3066
3067
3068
3069
3070
3071
3072
3073
3074
3075
3076
3077
3078
3079
3080
3081
3082
3083
3084
3085
3086
3087
3088
3089
3090
3091
3092
3093
3094
3095
3096
3097
3098
3099
3100
3101
3102
3103
3104
3105
3106
3107
3108
3109
3110
3111
3112
3113
3114
3115
3116
3117
3118
3119
3120
3121
3122
3123
3124
3125
3126
3127
3128
3129
3130
    ** Th_SetOutputBlob() has been called. If it has not been called,
    ** pThOut will be 0, which will redirect the output to CGI/stdout,
    ** as appropriate. We need to pass on g.th1Flags for the case of
    ** recursive calls, so that, e.g., TH_INIT_NO_ENCODE does not get
    ** inadvertently toggled off by a recursive call.
    */;
}

/*
** SETTING: vuln-report           width=8 default=log
**
** This setting controls Fossil's behavior when it encounters a potential
** XSS or SQL-injection vulnerability due to misuse of TH1 configuration
** scripts.  Choices are:
**
**    off            Do nothing.  Ignore the vulnerability.
**
**    log            Write a report of the problem into the error log.
**
**    block          Like "log" but also prevent the offending TH1 command
**                   from running.
**
**    fatal          Render an error message page instead of the requested
**                   page.
*/

/*
** Report misuse of a tainted string in TH1.
**
** The behavior depends on the vuln-report setting.  If "off", this routine
** is a no-op.  Otherwise, right a message into the error log.  If
** vuln-report is "log", that is all that happens.  But for any other
** value of vuln-report, a fatal error is raised.
*/
int Th_ReportTaint(
  Th_Interp *interp,       /* Report error here, if an error is reported */
  const char *zWhere,      /* Where the tainted string appears */
  const char *zStr,        /* The tainted string */
  int nStr                 /* Length of the tainted string */
){
  static const char *zDisp = 0;   /* Dispensation; what to do with the error */
  const char *zVulnType;          /* Type of vulnerability */

  if( zDisp==0 ) zDisp = db_get("vuln-report","log");
  if( is_false(zDisp) ) return 0;
  if( strstr(zWhere,"SQL")!=0 ){
    zVulnType = "SQL-injection";
  }else{
    zVulnType = "XSS";
  }
  nStr = TH1_LEN(nStr);
  fossil_errorlog("possible TH1 %s vulnerability due to tainted %s: \"%.*s\"",
                  zVulnType, zWhere, nStr, zStr);
  if( strcmp(zDisp,"log")==0 ){
    return 0;
  }
  if( strcmp(zDisp,"block")==0 ){
    char *z = mprintf("tainted %s: \"", zWhere);
    Th_ErrorMessage(interp, z, zStr, nStr);
    fossil_free(z);
  }else{
    char *z = mprintf("%#h", nStr, zStr);
    zDisp = "off";
    cgi_reset_content();
    style_submenu_enable(0);
    style_set_current_feature("error");
    style_header("Configuration Error");
    @ <p>Error in a TH1 configuration script: 
    @ tainted %h(zWhere): "%z(z)"
    style_finish_page();
    cgi_reply();
    fossil_exit(1);
  }
  return 1;
}

/*
** COMMAND: test-th-render
**
** Usage: %fossil test-th-render FILE
**
** Read the content of the file named "FILE" as if it were a header or
2990
2991
2992
2993
2994
2995
2996

2997
2998
2999
3000
3001
3002
3003
    g.useLocalauth = 1;
  }
  if( find_option("set-user-caps", 0, 0)!=0 ){
    const char *zCap = fossil_getenv("TH1_TEST_USER_CAPS");
    login_set_capabilities(zCap ? zCap : "sx", 0);
    g.useLocalauth = 1;
  }

  verify_all_options();
  if( g.argc<3 ){
    usage("FILE");
  }
  blob_zero(&in);
  blob_read_from_file(&in, g.argv[2], ExtFILE);
  Th_Render(blob_str(&in));







>







3156
3157
3158
3159
3160
3161
3162
3163
3164
3165
3166
3167
3168
3169
3170
    g.useLocalauth = 1;
  }
  if( find_option("set-user-caps", 0, 0)!=0 ){
    const char *zCap = fossil_getenv("TH1_TEST_USER_CAPS");
    login_set_capabilities(zCap ? zCap : "sx", 0);
    g.useLocalauth = 1;
  }
  db_find_and_open_repository(OPEN_OK_NOT_FOUND|OPEN_SUBSTITUTE,0);
  verify_all_options();
  if( g.argc<3 ){
    usage("FILE");
  }
  blob_zero(&in);
  blob_read_from_file(&in, g.argv[2], ExtFILE);
  Th_Render(blob_str(&in));
3042
3043
3044
3045
3046
3047
3048

3049
3050
3051
3052
3053
3054
3055
    g.useLocalauth = 1;
  }
  if( find_option("set-user-caps", 0, 0)!=0 ){
    const char *zCap = fossil_getenv("TH1_TEST_USER_CAPS");
    login_set_capabilities(zCap ? zCap : "sx", 0);
    g.useLocalauth = 1;
  }

  verify_all_options();
  if( g.argc!=3 ){
    usage("script");
  }
  if(file_isfile(g.argv[2], ExtFILE)){
    blob_read_from_file(&code, g.argv[2], ExtFILE);
    zCode = blob_str(&code);







>







3209
3210
3211
3212
3213
3214
3215
3216
3217
3218
3219
3220
3221
3222
3223
    g.useLocalauth = 1;
  }
  if( find_option("set-user-caps", 0, 0)!=0 ){
    const char *zCap = fossil_getenv("TH1_TEST_USER_CAPS");
    login_set_capabilities(zCap ? zCap : "sx", 0);
    g.useLocalauth = 1;
  }
  db_find_and_open_repository(OPEN_OK_NOT_FOUND|OPEN_SUBSTITUTE,0);
  verify_all_options();
  if( g.argc!=3 ){
    usage("script");
  }
  if(file_isfile(g.argv[2], ExtFILE)){
    blob_read_from_file(&code, g.argv[2], ExtFILE);
    zCode = blob_str(&code);
Changes to src/th_tcl.c.
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
** arguments from TH1 to Tcl.
*/
#define USE_ARGV_TO_OBJV() \
  int objc;                \
  Tcl_Obj **objv;          \
  int obji;

#define COPY_ARGV_TO_OBJV()                                         \
  objc = argc-1;                                                    \
  objv = (Tcl_Obj **)ckalloc((unsigned)(objc * sizeof(Tcl_Obj *))); \
  for(obji=1; obji<argc; obji++){                                   \
    objv[obji-1] = Tcl_NewStringObj(argv[obji], argl[obji]);        \
    Tcl_IncrRefCount(objv[obji-1]);                                 \
  }

#define FREE_ARGV_TO_OBJV()         \
  for(obji=1; obji<argc; obji++){   \
    Tcl_DecrRefCount(objv[obji-1]); \
    objv[obji-1] = 0;               \
  }                                 \







|
|
|
|
|
|







39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
** arguments from TH1 to Tcl.
*/
#define USE_ARGV_TO_OBJV() \
  int objc;                \
  Tcl_Obj **objv;          \
  int obji;

#define COPY_ARGV_TO_OBJV()                                           \
  objc = argc-1;                                                      \
  objv = (Tcl_Obj **)ckalloc((unsigned)(objc * sizeof(Tcl_Obj *)));   \
  for(obji=1; obji<argc; obji++){                                     \
    objv[obji-1] = Tcl_NewStringObj(argv[obji], TH1_LEN(argl[obji])); \
    Tcl_IncrRefCount(objv[obji-1]);                                   \
  }

#define FREE_ARGV_TO_OBJV()         \
  for(obji=1; obji<argc; obji++){   \
    Tcl_DecrRefCount(objv[obji-1]); \
    objv[obji-1] = 0;               \
  }                                 \
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
** when the Tcl library is being loaded dynamically by a stubs-enabled
** application (i.e. the inverse of using a stubs-enabled package).  These are
** the only Tcl API functions that MUST be called prior to being able to call
** Tcl_InitStubs (i.e. because it requires a Tcl interpreter).  For complete
** cleanup if the Tcl stubs initialization fails somehow, the Tcl_DeleteInterp
** and Tcl_Finalize function types are also required.
*/
typedef void (tcl_FindExecutableProc) (const char *);
typedef Tcl_Interp *(tcl_CreateInterpProc) (void);
typedef void (tcl_DeleteInterpProc) (Tcl_Interp *);
typedef void (tcl_FinalizeProc) (void);

/*
** The function types for the "hook" functions to be called before and after a
** TH1 command makes a call to evaluate a Tcl script.  If the "pre" function







|







181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
** when the Tcl library is being loaded dynamically by a stubs-enabled
** application (i.e. the inverse of using a stubs-enabled package).  These are
** the only Tcl API functions that MUST be called prior to being able to call
** Tcl_InitStubs (i.e. because it requires a Tcl interpreter).  For complete
** cleanup if the Tcl stubs initialization fails somehow, the Tcl_DeleteInterp
** and Tcl_Finalize function types are also required.
*/
typedef const char *(tcl_FindExecutableProc) (const char *);
typedef Tcl_Interp *(tcl_CreateInterpProc) (void);
typedef void (tcl_DeleteInterpProc) (Tcl_Interp *);
typedef void (tcl_FinalizeProc) (void);

/*
** The function types for the "hook" functions to be called before and after a
** TH1 command makes a call to evaluate a Tcl script.  If the "pre" function
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
** Creates and initializes a Tcl interpreter for use with the specified TH1
** interpreter.  Stores the created Tcl interpreter in the Tcl context supplied
** by the caller.  This must be declared here because quite a few functions in
** this file need to use it before it can be defined.
*/
static int createTclInterp(Th_Interp *interp, void *pContext);

/*
** Returns the TH1 return code corresponding to the specified Tcl
** return code.
*/
static int getTh1ReturnCode(
  int rc /* The Tcl return code value to convert. */
){
  switch( rc ){
    case /*0*/ TCL_OK:       return /*0*/ TH_OK;
    case /*1*/ TCL_ERROR:    return /*1*/ TH_ERROR;
    case /*2*/ TCL_RETURN:   return /*3*/ TH_RETURN;
    case /*3*/ TCL_BREAK:    return /*2*/ TH_BREAK;
    case /*4*/ TCL_CONTINUE: return /*4*/ TH_CONTINUE;
    default /*?*/:           return /*?*/ rc;
  }
}

/*
** Returns the Tcl return code corresponding to the specified TH1
** return code.
*/
static int getTclReturnCode(
  int rc /* The TH1 return code value to convert. */
){







<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<







319
320
321
322
323
324
325

















326
327
328
329
330
331
332
** Creates and initializes a Tcl interpreter for use with the specified TH1
** interpreter.  Stores the created Tcl interpreter in the Tcl context supplied
** by the caller.  This must be declared here because quite a few functions in
** this file need to use it before it can be defined.
*/
static int createTclInterp(Th_Interp *interp, void *pContext);


















/*
** Returns the Tcl return code corresponding to the specified TH1
** return code.
*/
static int getTclReturnCode(
  int rc /* The TH1 return code value to convert. */
){
385
386
387
388
389
390
391


392
393
394
395
396
397
398
399
400
401
402


403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
** If the length pointer is NULL, the length will not be stored.
*/
static char *getTclResult(
  Tcl_Interp *pInterp,
  int *pN
){
  Tcl_Obj *resultPtr;



  if( !pInterp ){ /* This should not happen. */
    if( pN ) *pN = 0;
    return 0;
  }
  resultPtr = Tcl_GetObjResult(pInterp);
  if( !resultPtr ){ /* This should not happen either? */
    if( pN ) *pN = 0;
    return 0;
  }
  return Tcl_GetStringFromObj(resultPtr, pN);


}

/*
** Tcl context information used by TH1.  This structure definition has been
** copied from and should be kept in sync with the one in "main.c".
*/
struct TclContext {
  int argc;           /* Number of original arguments. */
  char **argv;        /* Full copy of the original arguments. */
  void *hLibrary;     /* The Tcl library module handle. */
  tcl_FindExecutableProc *xFindExecutable; /* Tcl_FindExecutable() pointer. */
  tcl_CreateInterpProc *xCreateInterp;     /* Tcl_CreateInterp() pointer. */
  tcl_DeleteInterpProc *xDeleteInterp;     /* Tcl_DeleteInterp() pointer. */
  tcl_FinalizeProc *xFinalize;             /* Tcl_Finalize() pointer. */
  Tcl_Interp *interp; /* The on-demand created Tcl interpreter. */
  int useObjProc;     /* Non-zero if an objProc can be called directly. */
  int useTip285;      /* Non-zero if TIP #285 is available. */
  const char *setup;  /* The optional Tcl setup script. */
  tcl_NotifyProc *xPreEval;  /* Optional, called before Tcl_Eval*(). */
  void *pPreContext;         /* Optional, provided to xPreEval(). */
  tcl_NotifyProc *xPostEval; /* Optional, called after Tcl_Eval*(). */
  void *pPostContext;        /* Optional, provided to xPostEval(). */
};

/*
** This function calls the configured xPreEval or xPostEval functions, if any.
** May have arbitrary side-effects.  This function returns the result of the
** called notification function or the value of the rc argument if there is no
** notification function configured.
*/
static int notifyPreOrPostEval(
  int bIsPost,
  Th_Interp *interp,
  void *ctx,
  int argc,
  const char **argv,
  int *argl,
  int rc
){
  struct TclContext *tclContext = (struct TclContext *)ctx;
  tcl_NotifyProc *xNotifyProc;

  if( !tclContext ){
    Th_ErrorMessage(interp,
        "invalid Tcl context", (const char *)"", 0);
    return TH_ERROR;
  }
  xNotifyProc = bIsPost ? tclContext->xPostEval : tclContext->xPreEval;
  if( xNotifyProc ){
    rc = xNotifyProc(bIsPost ?
        tclContext->pPostContext : tclContext->pPreContext,
        interp, ctx, argc, argv, argl, rc);
  }
  return rc;
}

/*
** TH1 command: tclEval arg ?arg ...?
**
** Evaluates the Tcl script and returns its result verbatim.  If a Tcl script
** error is generated, it will be transformed into a TH1 script error.  The
** Tcl interpreter will be created automatically if it has not been already.
*/







>
>










|
>
>


















<
<
<
<


<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<







368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407




408
409
































410
411
412
413
414
415
416
** If the length pointer is NULL, the length will not be stored.
*/
static char *getTclResult(
  Tcl_Interp *pInterp,
  int *pN
){
  Tcl_Obj *resultPtr;
  Tcl_Size n;
  char *zRes;

  if( !pInterp ){ /* This should not happen. */
    if( pN ) *pN = 0;
    return 0;
  }
  resultPtr = Tcl_GetObjResult(pInterp);
  if( !resultPtr ){ /* This should not happen either? */
    if( pN ) *pN = 0;
    return 0;
  }
  zRes = Tcl_GetStringFromObj(resultPtr, &n);
  *pN = (int)n;
  return zRes;
}

/*
** Tcl context information used by TH1.  This structure definition has been
** copied from and should be kept in sync with the one in "main.c".
*/
struct TclContext {
  int argc;           /* Number of original arguments. */
  char **argv;        /* Full copy of the original arguments. */
  void *hLibrary;     /* The Tcl library module handle. */
  tcl_FindExecutableProc *xFindExecutable; /* Tcl_FindExecutable() pointer. */
  tcl_CreateInterpProc *xCreateInterp;     /* Tcl_CreateInterp() pointer. */
  tcl_DeleteInterpProc *xDeleteInterp;     /* Tcl_DeleteInterp() pointer. */
  tcl_FinalizeProc *xFinalize;             /* Tcl_Finalize() pointer. */
  Tcl_Interp *interp; /* The on-demand created Tcl interpreter. */
  int useObjProc;     /* Non-zero if an objProc can be called directly. */
  int useTip285;      /* Non-zero if TIP #285 is available. */
  const char *setup;  /* The optional Tcl setup script. */




};

































/*
** TH1 command: tclEval arg ?arg ...?
**
** Evaluates the Tcl script and returns its result verbatim.  If a Tcl script
** error is generated, it will be transformed into a TH1 script error.  The
** Tcl interpreter will be created automatically if it has not been already.
*/
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
    return Th_WrongNumArgs(interp, "tclEval arg ?arg ...?");
  }
  tclInterp = GET_CTX_TCL_INTERP(ctx);
  if( !tclInterp || Tcl_InterpDeleted(tclInterp) ){
    Th_ErrorMessage(interp, "invalid Tcl interpreter", (const char *)"", 0);
    return TH_ERROR;
  }
  rc = notifyPreOrPostEval(0, interp, ctx, argc, argv, argl, rc);
  if( rc!=TH_OK ){
    return rc;
  }
  Tcl_Preserve((ClientData)tclInterp);
  if( argc==2 ){
    objPtr = Tcl_NewStringObj(argv[1], argl[1]);
    Tcl_IncrRefCount(objPtr);
    rc = Tcl_EvalObjEx(tclInterp, objPtr, 0);
    Tcl_DecrRefCount(objPtr); objPtr = 0;
  }else{
    USE_ARGV_TO_OBJV();
    COPY_ARGV_TO_OBJV();
    objPtr = Tcl_ConcatObj(objc, objv);
    Tcl_IncrRefCount(objPtr);
    rc = Tcl_EvalObjEx(tclInterp, objPtr, 0);
    Tcl_DecrRefCount(objPtr); objPtr = 0;
    FREE_ARGV_TO_OBJV();
  }
  zResult = getTclResult(tclInterp, &nResult);
  Th_SetResult(interp, zResult, nResult);
  Tcl_Release((ClientData)tclInterp);
  rc = notifyPreOrPostEval(1, interp, ctx, argc, argv, argl,
                           getTh1ReturnCode(rc));
  return rc;
}

/*
** TH1 command: tclExpr arg ?arg ...?
**
** Evaluates the Tcl expression and returns its result verbatim.  If a Tcl







<
<
<
<


|















<
<







434
435
436
437
438
439
440




441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458


459
460
461
462
463
464
465
    return Th_WrongNumArgs(interp, "tclEval arg ?arg ...?");
  }
  tclInterp = GET_CTX_TCL_INTERP(ctx);
  if( !tclInterp || Tcl_InterpDeleted(tclInterp) ){
    Th_ErrorMessage(interp, "invalid Tcl interpreter", (const char *)"", 0);
    return TH_ERROR;
  }




  Tcl_Preserve((ClientData)tclInterp);
  if( argc==2 ){
    objPtr = Tcl_NewStringObj(argv[1], TH1_LEN(argl[1]));
    Tcl_IncrRefCount(objPtr);
    rc = Tcl_EvalObjEx(tclInterp, objPtr, 0);
    Tcl_DecrRefCount(objPtr); objPtr = 0;
  }else{
    USE_ARGV_TO_OBJV();
    COPY_ARGV_TO_OBJV();
    objPtr = Tcl_ConcatObj(objc, objv);
    Tcl_IncrRefCount(objPtr);
    rc = Tcl_EvalObjEx(tclInterp, objPtr, 0);
    Tcl_DecrRefCount(objPtr); objPtr = 0;
    FREE_ARGV_TO_OBJV();
  }
  zResult = getTclResult(tclInterp, &nResult);
  Th_SetResult(interp, zResult, nResult);
  Tcl_Release((ClientData)tclInterp);


  return rc;
}

/*
** TH1 command: tclExpr arg ?arg ...?
**
** Evaluates the Tcl expression and returns its result verbatim.  If a Tcl
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569

570

571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
    return Th_WrongNumArgs(interp, "tclExpr arg ?arg ...?");
  }
  tclInterp = GET_CTX_TCL_INTERP(ctx);
  if( !tclInterp || Tcl_InterpDeleted(tclInterp) ){
    Th_ErrorMessage(interp, "invalid Tcl interpreter", (const char *)"", 0);
    return TH_ERROR;
  }
  rc = notifyPreOrPostEval(0, interp, ctx, argc, argv, argl, rc);
  if( rc!=TH_OK ){
    return rc;
  }
  Tcl_Preserve((ClientData)tclInterp);
  if( argc==2 ){
    objPtr = Tcl_NewStringObj(argv[1], argl[1]);
    Tcl_IncrRefCount(objPtr);
    rc = Tcl_ExprObj(tclInterp, objPtr, &resultObjPtr);
    Tcl_DecrRefCount(objPtr); objPtr = 0;
  }else{
    USE_ARGV_TO_OBJV();
    COPY_ARGV_TO_OBJV();
    objPtr = Tcl_ConcatObj(objc, objv);
    Tcl_IncrRefCount(objPtr);
    rc = Tcl_ExprObj(tclInterp, objPtr, &resultObjPtr);
    Tcl_DecrRefCount(objPtr); objPtr = 0;
    FREE_ARGV_TO_OBJV();
  }
  if( rc==TCL_OK ){

    zResult = Tcl_GetStringFromObj(resultObjPtr, &nResult);

  }else{
    zResult = getTclResult(tclInterp, &nResult);
  }
  Th_SetResult(interp, zResult, nResult);
  if( rc==TCL_OK ){
    Tcl_DecrRefCount(resultObjPtr); resultObjPtr = 0;
  }
  Tcl_Release((ClientData)tclInterp);
  rc = notifyPreOrPostEval(1, interp, ctx, argc, argv, argl,
                           getTh1ReturnCode(rc));
  return rc;
}

/*
** TH1 command: tclInvoke command ?arg ...?
**
** Invokes the Tcl command using the supplied arguments.  No additional







<
<
<
<


|













>
|
>



|




<
<







488
489
490
491
492
493
494




495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521


522
523
524
525
526
527
528
    return Th_WrongNumArgs(interp, "tclExpr arg ?arg ...?");
  }
  tclInterp = GET_CTX_TCL_INTERP(ctx);
  if( !tclInterp || Tcl_InterpDeleted(tclInterp) ){
    Th_ErrorMessage(interp, "invalid Tcl interpreter", (const char *)"", 0);
    return TH_ERROR;
  }




  Tcl_Preserve((ClientData)tclInterp);
  if( argc==2 ){
    objPtr = Tcl_NewStringObj(argv[1], TH1_LEN(argl[1]));
    Tcl_IncrRefCount(objPtr);
    rc = Tcl_ExprObj(tclInterp, objPtr, &resultObjPtr);
    Tcl_DecrRefCount(objPtr); objPtr = 0;
  }else{
    USE_ARGV_TO_OBJV();
    COPY_ARGV_TO_OBJV();
    objPtr = Tcl_ConcatObj(objc, objv);
    Tcl_IncrRefCount(objPtr);
    rc = Tcl_ExprObj(tclInterp, objPtr, &resultObjPtr);
    Tcl_DecrRefCount(objPtr); objPtr = 0;
    FREE_ARGV_TO_OBJV();
  }
  if( rc==TCL_OK ){
    Tcl_Size szResult = 0;
    zResult = Tcl_GetStringFromObj(resultObjPtr, &szResult);
    nResult = (int)szResult;
  }else{
    zResult = getTclResult(tclInterp, &nResult);
  }
  Th_SetResult(interp, zResult, (int)nResult);
  if( rc==TCL_OK ){
    Tcl_DecrRefCount(resultObjPtr); resultObjPtr = 0;
  }
  Tcl_Release((ClientData)tclInterp);


  return rc;
}

/*
** TH1 command: tclInvoke command ?arg ...?
**
** Invokes the Tcl command using the supplied arguments.  No additional
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
    return Th_WrongNumArgs(interp, "tclInvoke command ?arg ...?");
  }
  tclInterp = GET_CTX_TCL_INTERP(ctx);
  if( !tclInterp || Tcl_InterpDeleted(tclInterp) ){
    Th_ErrorMessage(interp, "invalid Tcl interpreter", (const char *)"", 0);
    return TH_ERROR;
  }
  rc = notifyPreOrPostEval(0, interp, ctx, argc, argv, argl, rc);
  if( rc!=TH_OK ){
    return rc;
  }
  Tcl_Preserve((ClientData)tclInterp);
#if !defined(USE_TCL_EVALOBJV) || !USE_TCL_EVALOBJV
  if( GET_CTX_TCL_USEOBJPROC(ctx) ){
    Tcl_Command command;
    Tcl_CmdInfo cmdInfo;
    Tcl_Obj *objPtr = Tcl_NewStringObj(argv[1], argl[1]);
    Tcl_IncrRefCount(objPtr);
    command = Tcl_GetCommandFromObj(tclInterp, objPtr);
    if( !command || Tcl_GetCommandInfoFromToken(command, &cmdInfo)==0 ){
      Th_ErrorMessage(interp, "Tcl command not found:", argv[1], argl[1]);
      Tcl_DecrRefCount(objPtr); objPtr = 0;
      Tcl_Release((ClientData)tclInterp);
      return TH_ERROR;







<
<
<
<





|







549
550
551
552
553
554
555




556
557
558
559
560
561
562
563
564
565
566
567
568
    return Th_WrongNumArgs(interp, "tclInvoke command ?arg ...?");
  }
  tclInterp = GET_CTX_TCL_INTERP(ctx);
  if( !tclInterp || Tcl_InterpDeleted(tclInterp) ){
    Th_ErrorMessage(interp, "invalid Tcl interpreter", (const char *)"", 0);
    return TH_ERROR;
  }




  Tcl_Preserve((ClientData)tclInterp);
#if !defined(USE_TCL_EVALOBJV) || !USE_TCL_EVALOBJV
  if( GET_CTX_TCL_USEOBJPROC(ctx) ){
    Tcl_Command command;
    Tcl_CmdInfo cmdInfo;
    Tcl_Obj *objPtr = Tcl_NewStringObj(argv[1], TH1_LEN(argl[1]));
    Tcl_IncrRefCount(objPtr);
    command = Tcl_GetCommandFromObj(tclInterp, objPtr);
    if( !command || Tcl_GetCommandInfoFromToken(command, &cmdInfo)==0 ){
      Th_ErrorMessage(interp, "Tcl command not found:", argv[1], argl[1]);
      Tcl_DecrRefCount(objPtr); objPtr = 0;
      Tcl_Release((ClientData)tclInterp);
      return TH_ERROR;
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
    COPY_ARGV_TO_OBJV();
    rc = Tcl_EvalObjv(tclInterp, objc, objv, 0);
    FREE_ARGV_TO_OBJV();
  }
  zResult = getTclResult(tclInterp, &nResult);
  Th_SetResult(interp, zResult, nResult);
  Tcl_Release((ClientData)tclInterp);
  rc = notifyPreOrPostEval(1, interp, ctx, argc, argv, argl,
                           getTh1ReturnCode(rc));
  return rc;
}

/*
** TH1 command: tclIsSafe
**
** Returns non-zero if the Tcl interpreter is "safe".  The Tcl interpreter







<
<







584
585
586
587
588
589
590


591
592
593
594
595
596
597
    COPY_ARGV_TO_OBJV();
    rc = Tcl_EvalObjv(tclInterp, objc, objv, 0);
    FREE_ARGV_TO_OBJV();
  }
  zResult = getTclResult(tclInterp, &nResult);
  Th_SetResult(interp, zResult, nResult);
  Tcl_Release((ClientData)tclInterp);


  return rc;
}

/*
** TH1 command: tclIsSafe
**
** Returns non-zero if the Tcl interpreter is "safe".  The Tcl interpreter
765
766
767
768
769
770
771

772
773
774
775
776
777
778
779
780
781
782
783
784

785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804

805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
  ClientData clientData,
  Tcl_Interp *interp,
  int objc,
  Tcl_Obj *const objv[]
){
  Th_Interp *th1Interp;
  int nArg;

  const char *arg;
  int rc;

  if( objc!=2 ){
    Tcl_WrongNumArgs(interp, 1, objv, "arg");
    return TCL_ERROR;
  }
  th1Interp = (Th_Interp *)clientData;
  if( !th1Interp ){
    Tcl_AppendResult(interp, "invalid TH1 interpreter", NULL);
    return TCL_ERROR;
  }
  arg = Tcl_GetStringFromObj(objv[1], &nArg);

  rc = Th_Eval(th1Interp, 0, arg, nArg);
  arg = Th_GetResult(th1Interp, &nArg);
  Tcl_SetObjResult(interp, Tcl_NewStringObj(arg, nArg));
  return getTclReturnCode(rc);
}

/*
** Tcl command: th1Expr arg
**
** Evaluates the TH1 expression and returns its result verbatim.  If a TH1
** script error is generated, it will be transformed into a Tcl script error.
*/
static int Th1ExprObjCmd(
  ClientData clientData,
  Tcl_Interp *interp,
  int objc,
  Tcl_Obj *const objv[]
){
  Th_Interp *th1Interp;
  int nArg;

  const char *arg;
  int rc;

  if( objc!=2 ){
    Tcl_WrongNumArgs(interp, 1, objv, "arg");
    return TCL_ERROR;
  }
  th1Interp = (Th_Interp *)clientData;
  if( !th1Interp ){
    Tcl_AppendResult(interp, "invalid TH1 interpreter", NULL);
    return TCL_ERROR;
  }
  arg = Tcl_GetStringFromObj(objv[1], &nArg);
  rc = Th_Expr(th1Interp, arg, nArg);
  arg = Th_GetResult(th1Interp, &nArg);
  Tcl_SetObjResult(interp, Tcl_NewStringObj(arg, nArg));
  return getTclReturnCode(rc);
}

/*
** Array of Tcl integration commands.  Used when adding or removing the Tcl
** integration commands from TH1.
*/







>












|
>


|

















>












|
|

|







700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
  ClientData clientData,
  Tcl_Interp *interp,
  int objc,
  Tcl_Obj *const objv[]
){
  Th_Interp *th1Interp;
  int nArg;
  Tcl_Size szArg;
  const char *arg;
  int rc;

  if( objc!=2 ){
    Tcl_WrongNumArgs(interp, 1, objv, "arg");
    return TCL_ERROR;
  }
  th1Interp = (Th_Interp *)clientData;
  if( !th1Interp ){
    Tcl_AppendResult(interp, "invalid TH1 interpreter", NULL);
    return TCL_ERROR;
  }
  arg = Tcl_GetStringFromObj(objv[1], &szArg);
  nArg = (int)szArg;
  rc = Th_Eval(th1Interp, 0, arg, nArg);
  arg = Th_GetResult(th1Interp, &nArg);
  Tcl_SetObjResult(interp, Tcl_NewStringObj(arg, TH1_LEN(nArg)));
  return getTclReturnCode(rc);
}

/*
** Tcl command: th1Expr arg
**
** Evaluates the TH1 expression and returns its result verbatim.  If a TH1
** script error is generated, it will be transformed into a Tcl script error.
*/
static int Th1ExprObjCmd(
  ClientData clientData,
  Tcl_Interp *interp,
  int objc,
  Tcl_Obj *const objv[]
){
  Th_Interp *th1Interp;
  int nArg;
  Tcl_Size szArg;
  const char *arg;
  int rc;

  if( objc!=2 ){
    Tcl_WrongNumArgs(interp, 1, objv, "arg");
    return TCL_ERROR;
  }
  th1Interp = (Th_Interp *)clientData;
  if( !th1Interp ){
    Tcl_AppendResult(interp, "invalid TH1 interpreter", NULL);
    return TCL_ERROR;
  }
  arg = Tcl_GetStringFromObj(objv[1], &szArg);
  rc = Th_Expr(th1Interp, arg, (int)szArg);
  arg = Th_GetResult(th1Interp, &nArg);
  Tcl_SetObjResult(interp, Tcl_NewStringObj(arg, TH1_LEN(nArg)));
  return getTclReturnCode(rc);
}

/*
** Array of Tcl integration commands.  Used when adding or removing the Tcl
** integration commands from TH1.
*/
Changes to src/timeline.c.
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250

  if( cgi_is_loopback(g.zIpAddr) && db_open_local(0) ){
    vid = db_lget_int("checkout", 0);
  }
  zPrevDate[0] = 0;
  mxWikiLen = db_get_int("timeline-max-comment", 0);
  dateFormat = db_get_int("timeline-date-format", 0);
  /*
  ** SETTING: timeline-truncate-at-blank  boolean default=off
  **
  ** If enabled, check-in comments displayed on the timeline are truncated
  ** at the first blank line of the comment text.  The comment text after
  ** the first blank line is only seen in the /info or similar pages that
  ** show details about the check-in.
  */
  bCommentGitStyle = db_get_int("timeline-truncate-at-blank", 0);
  /*
  ** SETTING: timeline-tslink-info       boolean default=off
  **
  ** The hyperlink on the timestamp associated with each timeline entry,
  ** on the far left-hand side of the screen, normally targets another
  ** /timeline page that shows the entry in context.  However, if this
  ** option is turned on, that hyperlink targets the /info page showing
  ** the details of the entry.
  */
  bTimestampLinksToInfo = db_get_boolean("timeline-tslink-info", 0);
  if( (tmFlags & TIMELINE_VIEWS)==0 ){
    tmFlags |= timeline_ss_cookie();
  }
  if( tmFlags & TIMELINE_COLUMNAR ){
    zStyle = "Columnar";
  }else if( tmFlags & TIMELINE_COMPACT ){







|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|







219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250

  if( cgi_is_loopback(g.zIpAddr) && db_open_local(0) ){
    vid = db_lget_int("checkout", 0);
  }
  zPrevDate[0] = 0;
  mxWikiLen = db_get_int("timeline-max-comment", 0);
  dateFormat = db_get_int("timeline-date-format", 0);
/*
** SETTING: timeline-truncate-at-blank  boolean default=off
**
** If enabled, check-in comments displayed on the timeline are truncated
** at the first blank line of the comment text.  The comment text after
** the first blank line is only seen in the /info or similar pages that
** show details about the check-in.
*/
  bCommentGitStyle = db_get_int("timeline-truncate-at-blank", 0);
/*
** SETTING: timeline-tslink-info       boolean default=off
**
** The hyperlink on the timestamp associated with each timeline entry,
** on the far left-hand side of the screen, normally targets another
** /timeline page that shows the entry in context.  However, if this
** option is turned on, that hyperlink targets the /info page showing
** the details of the entry.
*/
  bTimestampLinksToInfo = db_get_boolean("timeline-tslink-info", 0);
  if( (tmFlags & TIMELINE_VIEWS)==0 ){
    tmFlags |= timeline_ss_cookie();
  }
  if( tmFlags & TIMELINE_COLUMNAR ){
    zStyle = "Columnar";
  }else if( tmFlags & TIMELINE_COMPACT ){
400
401
402
403
404
405
406


407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424



425
426
427
428
429
430


431
432
433
434
435
436
437
      }
    }else{
      zDateLink = mprintf("<a>");
    }
    @ <td class="timelineTime">%z(zDateLink)%s(zTime)</a></td>
    @ <td class="timelineGraph">
    if( tmFlags & (TIMELINE_UCOLOR|TIMELINE_DELTA|TIMELINE_NOCOLOR) ){


      if( tmFlags & TIMELINE_UCOLOR ){
        zBgClr = zUser ? user_color(zUser) : 0;
      }else if( tmFlags & TIMELINE_NOCOLOR ){
        zBgClr = 0;
      }else if( zType[0]=='c' ){
        static Stmt qdelta;
        db_static_prepare(&qdelta, "SELECT baseid IS NULL FROM plink"
                                   " WHERE cid=:rid");
        db_bind_int(&qdelta, ":rid", rid);
        if( db_step(&qdelta)!=SQLITE_ROW ){
          zBgClr = 0; /* Not a check-in */
        }else if( db_column_int(&qdelta, 0) ){
          zBgClr = hash_color("b");  /* baseline manifest */
        }else{
          zBgClr = hash_color("f");  /* delta manifest */
        }
        db_reset(&qdelta);
      }



    }
    if( zType[0]=='c'
    && (pGraph || zBgClr==0 || (tmFlags & (TIMELINE_BRCOLOR|TIMELINE_DELTA))!=0)
    ){
      zBr = branch_of_rid(rid);
      if( zBgClr==0 || (tmFlags & TIMELINE_BRCOLOR)!=0 ){


        if( tmFlags & (TIMELINE_DELTA|TIMELINE_NOCOLOR) ){
        }else if( zBr==0 || strcmp(zBr,"trunk")==0 ){
          zBgClr = 0;
        }else{
          zBgClr = hash_color(zBr);
        }
      }







>
>


















>
>
>






>
>







400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
      }
    }else{
      zDateLink = mprintf("<a>");
    }
    @ <td class="timelineTime">%z(zDateLink)%s(zTime)</a></td>
    @ <td class="timelineGraph">
    if( tmFlags & (TIMELINE_UCOLOR|TIMELINE_DELTA|TIMELINE_NOCOLOR) ){
      /* Don't use the requested background color.  Use the background color
      ** override from query parameters instead. */
      if( tmFlags & TIMELINE_UCOLOR ){
        zBgClr = zUser ? user_color(zUser) : 0;
      }else if( tmFlags & TIMELINE_NOCOLOR ){
        zBgClr = 0;
      }else if( zType[0]=='c' ){
        static Stmt qdelta;
        db_static_prepare(&qdelta, "SELECT baseid IS NULL FROM plink"
                                   " WHERE cid=:rid");
        db_bind_int(&qdelta, ":rid", rid);
        if( db_step(&qdelta)!=SQLITE_ROW ){
          zBgClr = 0; /* Not a check-in */
        }else if( db_column_int(&qdelta, 0) ){
          zBgClr = hash_color("b");  /* baseline manifest */
        }else{
          zBgClr = hash_color("f");  /* delta manifest */
        }
        db_reset(&qdelta);
      }
    }else{
      /* Make sure the user-specified background color is reasonable */
      zBgClr = reasonable_bg_color(zBgClr, 0);
    }
    if( zType[0]=='c'
    && (pGraph || zBgClr==0 || (tmFlags & (TIMELINE_BRCOLOR|TIMELINE_DELTA))!=0)
    ){
      zBr = branch_of_rid(rid);
      if( zBgClr==0 || (tmFlags & TIMELINE_BRCOLOR)!=0 ){
        /* If no background color is specified, use a color based on the
        ** branch name */
        if( tmFlags & (TIMELINE_DELTA|TIMELINE_NOCOLOR) ){
        }else if( zBr==0 || strcmp(zBr,"trunk")==0 ){
          zBgClr = 0;
        }else{
          zBgClr = hash_color(zBr);
        }
      }
589
590
591
592
593
594
595









596
597
598
599
600
601
602
        @ %W(blob_str(&truncated))
        blob_reset(&truncated);
        drawDetailEllipsis = 0;
      }else{
        cgi_printf("%W",blob_str(&comment));
      }
    }









    @ </span>
    blob_reset(&comment);

    /* Generate extra information and hyperlinks to follow the comment.
    ** Example:  "(check-in: [abcdefg], user: drh, tags: trunk)"
    */
    if( drawDetailEllipsis ){







>
>
>
>
>
>
>
>
>







596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
        @ %W(blob_str(&truncated))
        blob_reset(&truncated);
        drawDetailEllipsis = 0;
      }else{
        cgi_printf("%W",blob_str(&comment));
      }
    }
 
    if( zType[0]=='c' && strcmp(zUuid, MANIFEST_UUID)==0 ){
      /* This will only ever happen when Fossil is drawing a timeline for
      ** its own self-host repository.  If the timeline shows the specific
      ** check-in corresponding to the current executable, then tag that
      ** check-in with "This is me!". */
      @ <b>&larr; This is me!</b>
    }

    @ </span>
    blob_reset(&comment);

    /* Generate extra information and hyperlinks to follow the comment.
    ** Example:  "(check-in: [abcdefg], user: drh, tags: trunk)"
    */
    if( drawDetailEllipsis ){
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
  /* Finish preliminary processing of tag match queries. */
  matchStyle = match_style(zMatchStyle, MS_EXACT);
  if( zTagName ){
    zType = "ci";
    if( matchStyle==MS_EXACT ){
      /* For exact maching, inhibit links to the selected tag. */
      zThisTag = zTagName;
      Th_Store("current_checkin", zTagName);
    }

    /* Display a checkbox to enable/disable display of related check-ins. */
    if( advancedMenu ){
      style_submenu_checkbox("rel", "Related", 0, 0);
    }








|







1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
  /* Finish preliminary processing of tag match queries. */
  matchStyle = match_style(zMatchStyle, MS_EXACT);
  if( zTagName ){
    zType = "ci";
    if( matchStyle==MS_EXACT ){
      /* For exact maching, inhibit links to the selected tag. */
      zThisTag = zTagName;
      Th_StoreUnsafe("current_checkin", zTagName);
    }

    /* Display a checkbox to enable/disable display of related check-ins. */
    if( advancedMenu ){
      style_submenu_checkbox("rel", "Related", 0, 0);
    }

3731
3732
3733
3734
3735
3736
3737




3738
3739
3740
3741
3742



3743
3744
3745
3746
3747
3748
3749
3750
3751
3752
3753
3754
3755
3756
3757
3758
3759
3760
3761
3762
3763
3764
       * zFilePattern. */
      zFilePattern = 0;
    }
  }

  if( mode==TIMELINE_MODE_NONE ) mode = TIMELINE_MODE_BEFORE;
  blob_zero(&sql);




  blob_append(&sql, timeline_query_for_tty(), -1);
  blob_append_sql(&sql, "\n  AND event.mtime %s %s",
     ( mode==TIMELINE_MODE_BEFORE ||
       mode==TIMELINE_MODE_PARENTS ) ? "<=" : ">=", zDate /*safe-for-%s*/
  );




  /* When zFilePattern is specified, compute complete ancestry;
   * limit later at print_timeline() */
  if( mode==TIMELINE_MODE_CHILDREN || mode==TIMELINE_MODE_PARENTS ){
    db_multi_exec("CREATE TEMP TABLE ok(rid INTEGER PRIMARY KEY)");
    if( mode==TIMELINE_MODE_CHILDREN ){
      compute_descendants(objid, (zFilePattern ? 0 : n));
    }else{
      compute_ancestors(objid, (zFilePattern ? 0 : n), 0, 0);
    }
    blob_append_sql(&sql, "\n  AND blob.rid IN ok");
  }
  if( zType && (zType[0]!='a') ){
    blob_append_sql(&sql, "\n  AND event.type=%Q ", zType);
  }
  if( zFilePattern ){
    blob_append(&sql,
       "\n  AND EXISTS(SELECT 1 FROM mlink\n"
         "              WHERE mlink.mid=event.objid\n"
         "                AND mlink.fnid IN ", -1);
    if( filenames_are_case_sensitive() ){
      blob_append_sql(&sql,







>
>
>
>





>
>
>












<
<
<







3747
3748
3749
3750
3751
3752
3753
3754
3755
3756
3757
3758
3759
3760
3761
3762
3763
3764
3765
3766
3767
3768
3769
3770
3771
3772
3773
3774
3775
3776
3777



3778
3779
3780
3781
3782
3783
3784
       * zFilePattern. */
      zFilePattern = 0;
    }
  }

  if( mode==TIMELINE_MODE_NONE ) mode = TIMELINE_MODE_BEFORE;
  blob_zero(&sql);
  if( mode==TIMELINE_MODE_AFTER ){
    /* Extra outer select to get older rows in reverse order */
    blob_append(&sql, "SELECT *\nFROM (", -1);
  }
  blob_append(&sql, timeline_query_for_tty(), -1);
  blob_append_sql(&sql, "\n  AND event.mtime %s %s",
     ( mode==TIMELINE_MODE_BEFORE ||
       mode==TIMELINE_MODE_PARENTS ) ? "<=" : ">=", zDate /*safe-for-%s*/
  );
  if( zType && (zType[0]!='a') ){
    blob_append_sql(&sql, "\n  AND event.type=%Q ", zType);
  }

  /* When zFilePattern is specified, compute complete ancestry;
   * limit later at print_timeline() */
  if( mode==TIMELINE_MODE_CHILDREN || mode==TIMELINE_MODE_PARENTS ){
    db_multi_exec("CREATE TEMP TABLE ok(rid INTEGER PRIMARY KEY)");
    if( mode==TIMELINE_MODE_CHILDREN ){
      compute_descendants(objid, (zFilePattern ? 0 : n));
    }else{
      compute_ancestors(objid, (zFilePattern ? 0 : n), 0, 0);
    }
    blob_append_sql(&sql, "\n  AND blob.rid IN ok");
  }



  if( zFilePattern ){
    blob_append(&sql,
       "\n  AND EXISTS(SELECT 1 FROM mlink\n"
         "              WHERE mlink.mid=event.objid\n"
         "                AND mlink.fnid IN ", -1);
    if( filenames_are_case_sensitive() ){
      blob_append_sql(&sql,
3792
3793
3794
3795
3796
3797
3798






3799

3800
3801
3802
3803
3804
3805
3806
      "                          AND e.comment LIKE '_checkin/%%'\n"
      "        LEFT JOIN tagxref tx ON tx.rid=b.rid AND tx.tagid=%d\n"
      "          WHERE tx.value='%q'\n"
      ")\n"                                              /* No merge closures */
      "  AND (tagxref.value IS NULL OR tagxref.value='%q')",
      zBr, zBr, zBr, TAG_BRANCH, zBr, zBr);
  }






  blob_append_sql(&sql, "\nORDER BY event.mtime DESC");

  if( iOffset>0 ){
    /* Don't handle LIMIT here, otherwise print_timeline()
     * will not determine the end-marker correctly! */
    blob_append_sql(&sql, "\n LIMIT -1 OFFSET %d", iOffset);
  }
  if( showSql ){
    fossil_print("%s\n", blob_str(&sql));







>
>
>
>
>
>
|
>







3812
3813
3814
3815
3816
3817
3818
3819
3820
3821
3822
3823
3824
3825
3826
3827
3828
3829
3830
3831
3832
3833
      "                          AND e.comment LIKE '_checkin/%%'\n"
      "        LEFT JOIN tagxref tx ON tx.rid=b.rid AND tx.tagid=%d\n"
      "          WHERE tx.value='%q'\n"
      ")\n"                                              /* No merge closures */
      "  AND (tagxref.value IS NULL OR tagxref.value='%q')",
      zBr, zBr, zBr, TAG_BRANCH, zBr, zBr);
  }
  
  if( mode==TIMELINE_MODE_AFTER ){
    /* Complete the above outer select. */
    blob_append_sql(&sql, 
        "\nORDER BY event.mtime LIMIT abs(%d)) t ORDER BY t.mDateTime DESC;", n);
  }else{
    blob_append_sql(&sql, "\nORDER BY event.mtime DESC");
  }
  if( iOffset>0 ){
    /* Don't handle LIMIT here, otherwise print_timeline()
     * will not determine the end-marker correctly! */
    blob_append_sql(&sql, "\n LIMIT -1 OFFSET %d", iOffset);
  }
  if( showSql ){
    fossil_print("%s\n", blob_str(&sql));
Changes to src/tkt.c.
186
187
188
189
190
191
192

193
194
195



196

197
198
199
200
201
202
203
204
205
206
207
208
209
210
211

212
213
214
215


216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
** Otherwise, db_reveal() is a no-op and the content remains
** obscured.
*/
static void initializeVariablesFromDb(void){
  const char *zName;
  Stmt q;
  int i, n, size, j;


  zName = PD("name","-none-");
  db_prepare(&q, "SELECT datetime(tkt_mtime,toLocal()) AS tkt_datetime, *"



                 "  FROM ticket WHERE tkt_uuid GLOB '%q*'",

                 zName);
  if( db_step(&q)==SQLITE_ROW ){
    n = db_column_count(&q);
    for(i=0; i<n; i++){
      const char *zVal = db_column_text(&q, i);
      const char *zName = db_column_name(&q, i);
      char *zRevealed = 0;
      if( zVal==0 ){
        zVal = "";
      }else if( strncmp(zName, "private_", 8)==0 ){
        zVal = zRevealed = db_reveal(zVal);
      }
      if( (j = fieldId(zName))>=0 ){
        aField[j].zValue = mprintf("%s", zVal);
      }else if( memcmp(zName, "tkt_", 4)==0 && Th_Fetch(zName, &size)==0 ){

        Th_Store(zName, zVal);
      }
      free(zRevealed);
    }


  }
  db_finalize(&q);
  for(i=0; i<nField; i++){
    if( Th_Fetch(aField[i].zName, &size)==0 ){
      Th_Store(aField[i].zName, aField[i].zValue);
    }
  }
}

/*
** Transfer all CGI parameters to variables in the interpreter.
*/
static void initializeVariablesFromCGI(void){
  int i;
  const char *z;

  for(i=0; (z = cgi_parameter_name(i))!=0; i++){
    Th_Store(z, P(z));
  }
}

/*
** Information about a single J-card
*/
struct jCardInfo {







>


|
>
>
>

>















>




>
>




|












|







186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
** Otherwise, db_reveal() is a no-op and the content remains
** obscured.
*/
static void initializeVariablesFromDb(void){
  const char *zName;
  Stmt q;
  int i, n, size, j;
  const char *zCTimeColumn = haveTicketCTime ? "tkt_ctime" : "tkt_mtime";

  zName = PD("name","-none-");
  db_prepare(&q, "SELECT datetime(tkt_mtime,toLocal()) AS tkt_datetime, "
                 "datetime(%s,toLocal()) AS tkt_datetime_creation, "
                 "julianday('now') - tkt_mtime, "
                 "julianday('now') - %s, *"
                 "  FROM ticket WHERE tkt_uuid GLOB '%q*'",
                 zCTimeColumn/*safe-for-%s*/, zCTimeColumn/*safe-for-%s*/,
                 zName);
  if( db_step(&q)==SQLITE_ROW ){
    n = db_column_count(&q);
    for(i=0; i<n; i++){
      const char *zVal = db_column_text(&q, i);
      const char *zName = db_column_name(&q, i);
      char *zRevealed = 0;
      if( zVal==0 ){
        zVal = "";
      }else if( strncmp(zName, "private_", 8)==0 ){
        zVal = zRevealed = db_reveal(zVal);
      }
      if( (j = fieldId(zName))>=0 ){
        aField[j].zValue = mprintf("%s", zVal);
      }else if( memcmp(zName, "tkt_", 4)==0 && Th_Fetch(zName, &size)==0 ){
        /* TICKET table columns that begin with "tkt_" are always safe */
        Th_Store(zName, zVal);
      }
      free(zRevealed);
    }
    Th_Store("tkt_mage", human_readable_age(db_column_double(&q, 2)));
    Th_Store("tkt_cage", human_readable_age(db_column_double(&q, 3)));
  }
  db_finalize(&q);
  for(i=0; i<nField; i++){
    if( Th_Fetch(aField[i].zName, &size)==0 ){
      Th_StoreUnsafe(aField[i].zName, aField[i].zValue);
    }
  }
}

/*
** Transfer all CGI parameters to variables in the interpreter.
*/
static void initializeVariablesFromCGI(void){
  int i;
  const char *z;

  for(i=0; (z = cgi_parameter_name(i))!=0; i++){
    Th_StoreUnsafe(z, P(z));
  }
}

/*
** Information about a single J-card
*/
struct jCardInfo {
767
768
769
770
771
772
773
774

775
776
777
778
779
780
781
  if( !showTimeline && g.perm.Hyperlink ){
    style_submenu_element("Timeline", "%R/info/%T", zUuid);
  }
  zFullName = db_text(0,
       "SELECT tkt_uuid FROM ticket"
       " WHERE tkt_uuid GLOB '%q*'", zUuid);
  if( g.perm.WrWiki && g.perm.WrTkt ){
    style_submenu_element("Edit Description", "%R/wikiedit?name=ticket/%T", zFullName);

  }
  if( g.thTrace ) Th_Trace("BEGIN_TKTVIEW<br>\n", -1);
  ticket_init();
  initializeVariablesFromCGI();
  getAllTicketFields();
  initializeVariablesFromDb();
  zScript = ticket_viewpage_code();







|
>







775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
  if( !showTimeline && g.perm.Hyperlink ){
    style_submenu_element("Timeline", "%R/info/%T", zUuid);
  }
  zFullName = db_text(0,
       "SELECT tkt_uuid FROM ticket"
       " WHERE tkt_uuid GLOB '%q*'", zUuid);
  if( g.perm.WrWiki && g.perm.WrTkt ){
    style_submenu_element("Edit Description",
                          "%R/wikiedit?name=ticket/%T", zFullName);
  }
  if( g.thTrace ) Th_Trace("BEGIN_TKTVIEW<br>\n", -1);
  ticket_init();
  initializeVariablesFromCGI();
  getAllTicketFields();
  initializeVariablesFromDb();
  zScript = ticket_viewpage_code();
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
  int idx;

  if( argc!=3 ){
    return Th_WrongNumArgs(interp, "append_field FIELD STRING");
  }
  if( g.thTrace ){
    Th_Trace("append_field %#h {%#h}<br>\n",
              argl[1], argv[1], argl[2], argv[2]);
  }
  for(idx=0; idx<nField; idx++){
    if( memcmp(aField[idx].zName, argv[1], argl[1])==0
        && aField[idx].zName[argl[1]]==0 ){
      break;
    }
  }
  if( idx>=nField ){
    Th_ErrorMessage(g.interp, "no such TICKET column: ", argv[1], argl[1]);
    return TH_ERROR;
  }







|


|
|







819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
  int idx;

  if( argc!=3 ){
    return Th_WrongNumArgs(interp, "append_field FIELD STRING");
  }
  if( g.thTrace ){
    Th_Trace("append_field %#h {%#h}<br>\n",
              TH1_LEN(argl[1]), argv[1], TH1_LEN(argl[2]), argv[2]);
  }
  for(idx=0; idx<nField; idx++){
    if( memcmp(aField[idx].zName, argv[1], TH1_LEN(argl[1]))==0
        && aField[idx].zName[TH1_LEN(argl[1])]==0 ){
      break;
    }
  }
  if( idx>=nField ){
    Th_ErrorMessage(g.interp, "no such TICKET column: ", argv[1], argl[1]);
    return TH_ERROR;
  }
930
931
932
933
934
935
936

937
938
939
940
941
942
943
  }
  for(i=0; i<nField; i++){
    const char *zValue;
    int nValue;
    if( aField[i].zAppend ) continue;
    zValue = Th_Fetch(aField[i].zName, &nValue);
    if( zValue ){

      while( nValue>0 && fossil_isspace(zValue[nValue-1]) ){ nValue--; }
      if( ((aField[i].mUsed & USEDBY_TICKETCHNG)!=0 && nValue>0)
       || memcmp(zValue, aField[i].zValue, nValue)!=0
       ||(int)strlen(aField[i].zValue)!=nValue
      ){
        if( memcmp(aField[i].zName, "private_", 8)==0 ){
          zValue = db_conceal(zValue, nValue);







>







939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
  }
  for(i=0; i<nField; i++){
    const char *zValue;
    int nValue;
    if( aField[i].zAppend ) continue;
    zValue = Th_Fetch(aField[i].zName, &nValue);
    if( zValue ){
      nValue = TH1_LEN(nValue);
      while( nValue>0 && fossil_isspace(zValue[nValue-1]) ){ nValue--; }
      if( ((aField[i].mUsed & USEDBY_TICKETCHNG)!=0 && nValue>0)
       || memcmp(zValue, aField[i].zValue, nValue)!=0
       ||(int)strlen(aField[i].zValue)!=nValue
      ){
        if( memcmp(aField[i].zName, "private_", 8)==0 ){
          zValue = db_conceal(zValue, nValue);
1024
1025
1026
1027
1028
1029
1030

1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051



1052

1053
1054
1055
1056
1057
1058
1059
  initializeVariablesFromDb();
  if( g.zPath[0]=='d' ) showAllFields();
  form_begin(0, "%R/%s", g.zPath);
  if( P("date_override") && g.perm.Setup ){
    @ <input type="hidden" name="date_override" value="%h(P("date_override"))">
  }
  zScript = ticket_newpage_code();

  if( g.zLogin && g.zLogin[0] ){
    int nEmail = 0;
    (void)Th_MaybeGetVar(g.interp, "private_contact", &nEmail);
    uid = nEmail>0
      ? 0 : db_int(0, "SELECT uid FROM user WHERE login=%Q", g.zLogin);
    if( uid ){
      char * zEmail =
        db_text(0, "SELECT find_emailaddr(info) FROM user WHERE uid=%d",
                uid);
      if( zEmail ){
        Th_Store("private_contact", zEmail);
        fossil_free(zEmail);
      }
    }
  }
  Th_Store("login", login_name());
  Th_Store("date", db_text(0, "SELECT datetime('now')"));
  Th_CreateCommand(g.interp, "submit_ticket", submitTicketCmd,
                   (void*)&zNewUuid, 0);
  if( g.thTrace ) Th_Trace("BEGIN_TKTNEW_SCRIPT<br>\n", -1);
  if( Th_Render(zScript)==TH_RETURN && !g.thTrace && zNewUuid ){



    cgi_redirect(mprintf("%R/tktview/%s", zNewUuid));

    return;
  }
  captcha_generate(0);
  @ </form>
  if( g.thTrace ) Th_Trace("END_TKTVIEW<br>\n", -1);
  style_finish_page();
}







>

<
<
<
|





|




|





>
>
>
|
>







1034
1035
1036
1037
1038
1039
1040
1041
1042



1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
  initializeVariablesFromDb();
  if( g.zPath[0]=='d' ) showAllFields();
  form_begin(0, "%R/%s", g.zPath);
  if( P("date_override") && g.perm.Setup ){
    @ <input type="hidden" name="date_override" value="%h(P("date_override"))">
  }
  zScript = ticket_newpage_code();
  Th_Store("private_contact", "");
  if( g.zLogin && g.zLogin[0] ){



    uid = db_int(0, "SELECT uid FROM user WHERE login=%Q", g.zLogin);
    if( uid ){
      char * zEmail =
        db_text(0, "SELECT find_emailaddr(info) FROM user WHERE uid=%d",
                uid);
      if( zEmail ){
        Th_StoreUnsafe("private_contact", zEmail);
        fossil_free(zEmail);
      }
    }
  }
  Th_StoreUnsafe("login", login_name());
  Th_Store("date", db_text(0, "SELECT datetime('now')"));
  Th_CreateCommand(g.interp, "submit_ticket", submitTicketCmd,
                   (void*)&zNewUuid, 0);
  if( g.thTrace ) Th_Trace("BEGIN_TKTNEW_SCRIPT<br>\n", -1);
  if( Th_Render(zScript)==TH_RETURN && !g.thTrace && zNewUuid ){
    if( P("submitandnew") ){
      cgi_redirect(mprintf("%R/tktnew/%s", zNewUuid));
    }else{
      cgi_redirect(mprintf("%R/tktview/%s", zNewUuid));
    }
    return;
  }
  captcha_generate(0);
  @ </form>
  if( g.thTrace ) Th_Trace("END_TKTVIEW<br>\n", -1);
  style_finish_page();
}
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
  getAllTicketFields();
  initializeVariablesFromCGI();
  initializeVariablesFromDb();
  if( g.zPath[0]=='d' ) showAllFields();
  form_begin(0, "%R/%s", g.zPath);
  @ <input type="hidden" name="name" value="%s(zName)">
  zScript = ticket_editpage_code();
  Th_Store("login", login_name());
  Th_Store("date", db_text(0, "SELECT datetime('now')"));
  Th_CreateCommand(g.interp, "append_field", appendRemarkCmd, 0, 0);
  Th_CreateCommand(g.interp, "submit_ticket", submitTicketCmd, (void*)&zName,0);
  if( g.thTrace ) Th_Trace("BEGIN_TKTEDIT_SCRIPT<br>\n", -1);
  if( Th_Render(zScript)==TH_RETURN && !g.thTrace && zName ){
    cgi_redirect(mprintf("%R/tktview/%s", zName));
    return;







|







1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
  getAllTicketFields();
  initializeVariablesFromCGI();
  initializeVariablesFromDb();
  if( g.zPath[0]=='d' ) showAllFields();
  form_begin(0, "%R/%s", g.zPath);
  @ <input type="hidden" name="name" value="%s(zName)">
  zScript = ticket_editpage_code();
  Th_StoreUnsafe("login", login_name());
  Th_Store("date", db_text(0, "SELECT datetime('now')"));
  Th_CreateCommand(g.interp, "append_field", appendRemarkCmd, 0, 0);
  Th_CreateCommand(g.interp, "submit_ticket", submitTicketCmd, (void*)&zName,0);
  if( g.thTrace ) Th_Trace("BEGIN_TKTEDIT_SCRIPT<br>\n", -1);
  if( Th_Render(zScript)==TH_RETURN && !g.thTrace && zName ){
    cgi_redirect(mprintf("%R/tktview/%s", zName));
    return;
Changes to src/tktsetup.c.
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137

  login_check_credentials();
  if( !g.perm.Setup ){
    login_needed(0);
    return;
  }
  style_set_current_feature("tktsetup");
  if( PB("setup") ){
    cgi_redirect("tktsetup");
  }
  isSubmit = P("submit")!=0;
  z = P("x");
  if( z==0 ){
    z = db_get(zDbField, zDfltValue);
  }







|







123
124
125
126
127
128
129
130
131
132
133
134
135
136
137

  login_check_credentials();
  if( !g.perm.Setup ){
    login_needed(0);
    return;
  }
  style_set_current_feature("tktsetup");
  if( P("setup") ){
    cgi_redirect("tktsetup");
  }
  isSubmit = P("submit")!=0;
  z = P("x");
  if( z==0 ){
    z = db_get(zDbField, zDfltValue);
  }
162
163
164
165
166
167
168

169
170
171
172
173
174
175
  @ </p></blockquote>
  @ </div></form>
  @ <hr>
  @ <h2>Default %s(zTitle)</h2>
  @ <blockquote><pre>
  @ %h(zDfltValue)
  @ </pre></blockquote>

  style_finish_page();
}

/*
** WEBPAGE: tktsetup_tab
** Administrative page for defining the "ticket" table used
** to hold ticket information.







>







162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
  @ </p></blockquote>
  @ </div></form>
  @ <hr>
  @ <h2>Default %s(zTitle)</h2>
  @ <blockquote><pre>
  @ %h(zDfltValue)
  @ </pre></blockquote>
  style_submenu_element("Back", "%R/tktsetup");
  style_finish_page();
}

/*
** WEBPAGE: tktsetup_tab
** Administrative page for defining the "ticket" table used
** to hold ticket information.
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
    30
  );
}

static const char zDefaultNew[] =
@ <th1>
@   if {![info exists mutype]} {set mutype Markdown}
@   if {[info exists submit]} {
@      set status Open
@      if {$mutype eq "HTML"} {
@        set mimetype "text/html"
@      } elseif {$mutype eq "Wiki"} {
@        set mimetype "text/x-fossil-wiki"
@      } elseif {$mutype eq "Markdown"} {
@        set mimetype text/x-markdown







|







300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
    30
  );
}

static const char zDefaultNew[] =
@ <th1>
@   if {![info exists mutype]} {set mutype Markdown}
@   if {[info exists submit] || [info exists submitandnew]} {
@      set status Open
@      if {$mutype eq "HTML"} {
@        set mimetype "text/html"
@      } elseif {$mutype eq "Wiki"} {
@        set mimetype "text/x-fossil-wiki"
@      } elseif {$mutype eq "Markdown"} {
@        set mimetype text/x-markdown
347
348
349
350
351
352
353


















354
355
356
357
358
359
360
@ <tr>
@ <td align="right">Severity:</td>
@ <td align="left"><th1>combobox severity $severity_choices 1</th1></td>
@ <td align="left">How debilitating is the problem?  How badly does the problem
@ affect the operation of the product?</td>
@ </tr>
@


















@ <tr>
@ <td align="right">EMail:</td>
@ <td align="left">
@ <input name="private_contact" value="$<private_contact>" size="30">
@ </td>
@ <td align="left"><u>Not publicly visible</u>
@ Used by developers to contact you with questions.</td>







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
@ <tr>
@ <td align="right">Severity:</td>
@ <td align="left"><th1>combobox severity $severity_choices 1</th1></td>
@ <td align="left">How debilitating is the problem?  How badly does the problem
@ affect the operation of the product?</td>
@ </tr>
@
@ <th1>
@   if {[capexpr {w}]} {
@      html {<tr><td class="tktDspLabel">Priority:</td><td>}
@      combobox priority $priority_choices 1
@      html {
@        <td align="left">How important is the affected functionality?</td>
@        </td></tr>
@      }
@
@      html {<tr><td class="tktDspLabel">Subsystem:</td><td>}
@      combobox subsystem $subsystem_choices 1
@      html {
@        <td align="left">Which subsystem is affected?</td>
@        </td></tr>
@      }
@   }
@ </th1>
@
@ <tr>
@ <td align="right">EMail:</td>
@ <td align="left">
@ <input name="private_contact" value="$<private_contact>" size="30">
@ </td>
@ <td align="left"><u>Not publicly visible</u>
@ Used by developers to contact you with questions.</td>
403
404
405
406
407
408
409
410








411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
@
@ <th1>enable_output [info exists preview]</th1>
@ <tr>
@ <td><td align="left">
@ <input type="submit" name="submit" value="Submit">
@ </td>
@ <td align="left">After filling in the information above, press this
@ button to create the new ticket</td>








@ </tr>
@ <th1>enable_output 1</th1>
@
@ <tr>
@ <td><td align="left">
@ <input type="submit" name="cancel" value="Cancel">
@ </td>
@ <td>Abandon and forget this ticket</td>
@ </tr>
@ </table>
;

/*
** Return the code used to generate the new ticket page
*/







|
>
>
>
>
>
>
>
>







|







422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
@
@ <th1>enable_output [info exists preview]</th1>
@ <tr>
@ <td><td align="left">
@ <input type="submit" name="submit" value="Submit">
@ </td>
@ <td align="left">After filling in the information above, press this
@ button to create the new ticket.</td>
@ </tr>
@
@ <tr>
@ <td><td align="left">
@ <input type="submit" name="submitandnew" value="Submit and New">
@ </td>
@ <td align="left">Create the new ticket and start another
@ ticket form with the inputs.</td>
@ </tr>
@ <th1>enable_output 1</th1>
@
@ <tr>
@ <td><td align="left">
@ <input type="submit" name="cancel" value="Cancel">
@ </td>
@ <td>Abandon and forget this ticket.</td>
@ </tr>
@ </table>
;

/*
** Return the code used to generate the new ticket page
*/
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469




470
471
472
473
474
475
476
@ <table cellpadding="5">
@ <tr><td class="tktDspLabel">Ticket&nbsp;Hash:</td>
@ <th1>
@ if {[info exists tkt_uuid]} {
@   html "<td class='tktDspValue' colspan='3'>"
@   copybtn hash-tk 0 $tkt_uuid 2
@   if {[hascap s]} {
@     html " ($tkt_id)"
@   }
@   html "</td></tr>\n"
@ } else {
@   if {[hascap s]} {
@     html "<td class='tktDspValue' colspan='3'>Deleted "
@     html "(0)</td></tr>\n"
@   } else {
@     html "<td class='tktDspValue' colspan='3'>Deleted</td></tr>\n"
@   }
@ }




@ </th1>
@ <tr><td class="tktDspLabel">Title:</td>
@ <td class="tktDspValue" colspan="3">
@ $<title>
@ </td></tr>
@ <tr><td class="tktDspLabel">Status:</td><td class="tktDspValue">
@ $<status>







|










>
>
>
>







479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
@ <table cellpadding="5">
@ <tr><td class="tktDspLabel">Ticket&nbsp;Hash:</td>
@ <th1>
@ if {[info exists tkt_uuid]} {
@   html "<td class='tktDspValue' colspan='3'>"
@   copybtn hash-tk 0 $tkt_uuid 2
@   if {[hascap s]} {
@     puts " ($tkt_id)"
@   }
@   html "</td></tr>\n"
@ } else {
@   if {[hascap s]} {
@     html "<td class='tktDspValue' colspan='3'>Deleted "
@     html "(0)</td></tr>\n"
@   } else {
@     html "<td class='tktDspValue' colspan='3'>Deleted</td></tr>\n"
@   }
@ }
@
@ if {[capexpr {n}]} {
@   submenu link "Copy Ticket" /tktnew/$tkt_uuid
@ }
@ </th1>
@ <tr><td class="tktDspLabel">Title:</td>
@ <td class="tktDspValue" colspan="3">
@ $<title>
@ </td></tr>
@ <tr><td class="tktDspLabel">Status:</td><td class="tktDspValue">
@ $<status>
489
490
491
492
493
494
495
496



497
498
499










500

501
502
503

504
505
506
507


















508








509
510
511
512
513
514
515
@ </td>
@ <td class="tktDspLabel">Resolution:</td><td class="tktDspValue">
@ $<resolution>
@ </td></tr>
@ <tr><td class="tktDspLabel">Last&nbsp;Modified:</td><td class="tktDspValue">
@ <th1>
@ if {[info exists tkt_datetime]} {
@   html $tkt_datetime



@ }
@ </th1>
@ </td>










@ <th1>enable_output [hascap e]</th1>

@   <td class="tktDspLabel">Contact:</td><td class="tktDspValue">
@   $<private_contact>
@   </td>

@ <th1>enable_output 1</th1>
@ </tr>
@ <tr><td class="tktDspLabel">Version&nbsp;Found&nbsp;In:</td>
@ <td colspan="3" valign="top" class="tktDspValue">


















@ $<foundin>








@ </td></tr>
@ </table>
@
@ <th1>
@ wiki_assoc "ticket" $tkt_uuid
@ </th1>
@







|
>
>
>



>
>
>
>
>
>
>
>
>
>

>
|


>

<


>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
|
>
>
>
>
>
>
>
>







520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550

551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
@ </td>
@ <td class="tktDspLabel">Resolution:</td><td class="tktDspValue">
@ $<resolution>
@ </td></tr>
@ <tr><td class="tktDspLabel">Last&nbsp;Modified:</td><td class="tktDspValue">
@ <th1>
@ if {[info exists tkt_datetime]} {
@   puts $tkt_datetime
@ }
@ if {[info exists tkt_mage]} {
@   html "<br>[htmlize $tkt_mage] ago"
@ }
@ </th1>
@ </td>
@ <td class="tktDspLabel">Created:</td><td class="tktDspValue">
@ <th1>
@ if {[info exists tkt_datetime_creation]} {
@   puts $tkt_datetime_creation
@ }
@ if {[info exists tkt_cage]} {
@   html "<br>[htmlize $tkt_cage] ago"
@ }
@ </th1>
@ </td></tr>
@ <th1>enable_output [hascap e]</th1>
@   <tr>
@   <td class="tktDspLabel">Contact:</td><td class="tktDspValue" colspan="3">
@   $<private_contact>
@   </td>
@   </tr>
@ <th1>enable_output 1</th1>

@ <tr><td class="tktDspLabel">Version&nbsp;Found&nbsp;In:</td>
@ <td colspan="3" valign="top" class="tktDspValue">
@ <th1>
@ set versionlink ""
@ set urlfoundin [httpize $foundin]
@ set tagpattern {^[-0-9A-Za-z_\\.]+$}
@ if [regexp $tagpattern $foundin] {
@   query {SELECT count(*) AS match FROM tag
@          WHERE tagname=concat('sym-',$foundin)} {
@     if {$match} {set versionlink "timeline?t=$urlfoundin"}
@   }
@ }
@ set hashpattern {^[0-9a-f]+$}
@ if [regexp $hashpattern $foundin] {
@   set pattern $foundin*
@   query {SELECT count(*) AS match FROM blob WHERE uuid GLOB $pattern} {
@     if {$match} {set versionlink "info/$urlfoundin"}
@   }
@ }
@ if {$versionlink eq ""} {
@   puts $foundin
@ } else {
@   html "<a href=\""
@   puts $versionlink
@   html "\">"
@   puts $foundin
@   html "</a>"
@ }
@ </th1>
@ </td></tr>
@ </table>
@
@ <th1>
@ wiki_assoc "ticket" $tkt_uuid
@ </th1>
@
535
536
537
538
539
540
541
542

543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
@               mimetype as xmimetype, icomment AS xcomment,
@               username AS xusername
@          FROM ticketchng
@         WHERE tkt_id=$tkt_id AND length(icomment)>0} {
@   if {$seenRow} {
@     html "<hr>\n"
@   } else {
@     html "<tr><td class='tktDspLabel' style='text-align:left'>User Comments:</td></tr>\n"

@     html "<tr><td colspan='5' class='tktDspValue'>\n"
@     set seenRow 1
@   }
@   html "<span class='tktDspCommenter'>"
@   html "[htmlize $xlogin]"
@   if {$xlogin ne $xusername && [string length $xusername]>0} {
@     html " (claiming to be [htmlize $xusername])"
@   }
@   html " added on $xdate:"
@   html "</span>\n"
@   if {$alwaysPlaintext || $xmimetype eq "text/plain"} {
@     set r [randhex]
@     if {$xmimetype ne "text/plain"} {html "([htmlize $xmimetype])\n"}
@     wiki "<verbatim-$r>[string trimright $xcomment]</verbatim-$r>\n"
@   } elseif {$xmimetype eq "text/x-fossil-wiki"} {
@     wiki "<p>\n[string trimright $xcomment]\n</p>\n"
@   } elseif {$xmimetype eq "text/x-markdown"} {
@     html [lindex [markdown $xcomment] 1]
@   } elseif {$xmimetype eq "text/html"} {
@     wiki "<p><nowiki>\n[string trimright $xcomment]\n</nowiki>\n"







|
>




|

|

|



|







606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
@               mimetype as xmimetype, icomment AS xcomment,
@               username AS xusername
@          FROM ticketchng
@         WHERE tkt_id=$tkt_id AND length(icomment)>0} {
@   if {$seenRow} {
@     html "<hr>\n"
@   } else {
@     html "<tr><td class='tktDspLabel' style='text-align:left'>\n"
@     html "User Comments:</td></tr>\n"
@     html "<tr><td colspan='5' class='tktDspValue'>\n"
@     set seenRow 1
@   }
@   html "<span class='tktDspCommenter'>"
@   puts $xlogin
@   if {$xlogin ne $xusername && [string length $xusername]>0} {
@     puts " (claiming to be $xusername)"
@   }
@   puts " added on $xdate:"
@   html "</span>\n"
@   if {$alwaysPlaintext || $xmimetype eq "text/plain"} {
@     set r [randhex]
@     if {$xmimetype ne "text/plain"} {puts "($xmimetype)\n"}
@     wiki "<verbatim-$r>[string trimright $xcomment]</verbatim-$r>\n"
@   } elseif {$xmimetype eq "text/x-fossil-wiki"} {
@     wiki "<p>\n[string trimright $xcomment]\n</p>\n"
@   } elseif {$xmimetype eq "text/x-markdown"} {
@     html [lindex [markdown $xcomment] 1]
@   } elseif {$xmimetype eq "text/html"} {
@     wiki "<p><nowiki>\n[string trimright $xcomment]\n</nowiki>\n"
709
710
711
712
713
714
715










































716
717
718
719
720
721
722
@ <tr>
@ <td align="right">
@ <input type="submit" name="cancel" value="Cancel">
@ </td>
@ <td>Abandon this edit</td>
@ </tr>
@










































@ </table>
;

/*
** Return the code used to generate the edit ticket page
*/
const char *ticket_editpage_code(void){







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
@ <tr>
@ <td align="right">
@ <input type="submit" name="cancel" value="Cancel">
@ </td>
@ <td>Abandon this edit</td>
@ </tr>
@
@ <th1>
@ set seenRow 0
@ set alwaysPlaintext [info exists plaintext]
@ query {SELECT datetime(tkt_mtime) AS xdate, login AS xlogin,
@               mimetype as xmimetype, icomment AS xcomment,
@               username AS xusername
@          FROM ticketchng
@         WHERE tkt_id=$tkt_id AND length(icomment)>0} {
@   if {$seenRow} {
@     html "<hr>\n"
@   } else {
@     html "<tr><td colspan='2'><hr></td></tr>\n"
@     html "<tr><td colspan='2' class='tktDspLabel' style='text-align:left'>\n"
@     html "Previous User Comments:</td></tr>\n"
@     html "<tr><td colspan='2' class='tktDspValue'>\n"
@     set seenRow 1
@   }
@   html "<span class='tktDspCommenter'>"
@   puts $xlogin
@   if {$xlogin ne $xusername && [string length $xusername]>0} {
@     puts " (claiming to be $xusername)"
@   }
@   puts " added on $xdate:"
@   html "</span>\n"
@   if {$alwaysPlaintext || $xmimetype eq "text/plain"} {
@     set r [randhex]
@     if {$xmimetype ne "text/plain"} {puts "($xmimetype)\n"}
@     wiki "<verbatim-$r>[string trimright $xcomment]</verbatim-$r>\n"
@   } elseif {$xmimetype eq "text/x-fossil-wiki"} {
@     wiki "<p>\n[string trimright $xcomment]\n</p>\n"
@   } elseif {$xmimetype eq "text/x-markdown"} {
@     html [lindex [markdown $xcomment] 1]
@   } elseif {$xmimetype eq "text/html"} {
@     wiki "<p><nowiki>\n[string trimright $xcomment]\n</nowiki>\n"
@   } else {
@     set r [randhex]
@     wiki "<verbatim-$r links>[string trimright $xcomment]</verbatim-$r>\n"
@   }
@ }
@ if {$seenRow} {html "</td></tr>\n"}
@ </th1>
@
@ </table>
;

/*
** Return the code used to generate the edit ticket page
*/
const char *ticket_editpage_code(void){
807
808
809
810
811
812
813

814
815
816
817
818
819
820
821
@   CASE WHEN status IN ('Open','Verified') THEN '#f2dcdc'
@        WHEN status='Review' THEN '#e8e8e8'
@        WHEN status='Fixed' THEN '#cfe8bd'
@        WHEN status='Tested' THEN '#bde5d6'
@        WHEN status='Deferred' THEN '#cacae5'
@        ELSE '#c8c8c8' END AS 'bgcolor',
@   substr(tkt_uuid,1,10) AS '#',

@   datetime(tkt_mtime) AS 'mtime',
@   type,
@   status,
@   subsystem,
@   title,
@   comment AS '_comments'
@ FROM ticket
;







>
|







921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
@   CASE WHEN status IN ('Open','Verified') THEN '#f2dcdc'
@        WHEN status='Review' THEN '#e8e8e8'
@        WHEN status='Fixed' THEN '#cfe8bd'
@        WHEN status='Tested' THEN '#bde5d6'
@        WHEN status='Deferred' THEN '#cacae5'
@        ELSE '#c8c8c8' END AS 'bgcolor',
@   substr(tkt_uuid,1,10) AS '#',
@   datetime(tkt_ctime) AS 'created',
@   datetime(tkt_mtime) AS 'modified',
@   type,
@   status,
@   subsystem,
@   title,
@   comment AS '_comments'
@ FROM ticket
;
941
942
943
944
945
946
947

948
949
950
  @ <hr>
  @ <p>
  @ <input type="submit"  name="submit" value="Apply Changes">
  @ <input type="submit" name="setup" value="Cancel">
  @ </p>
  @ </div></form>
  db_end_transaction(0);

  style_finish_page();

}







>



1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
  @ <hr>
  @ <p>
  @ <input type="submit"  name="submit" value="Apply Changes">
  @ <input type="submit" name="setup" value="Cancel">
  @ </p>
  @ </div></form>
  db_end_transaction(0);
  style_submenu_element("Back", "%R/tktsetup");
  style_finish_page();

}
Changes to src/undo.c.
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
    blob_zero(&new);
    old_exists = db_column_int(&q, 1);
    old_exe = db_column_int(&q, 2);
    if( old_exists ){
      db_ephemeral_blob(&q, 0, &new);
    }
    if( file_unsafe_in_tree_path(zFullname) ){
      /* do nothign with this unsafe file */
    }else if( old_exists ){
      if( new_exists ){
        fossil_print("%s   %s\n", redoFlag ? "REDO" : "UNDO", zPathname);
      }else{
        fossil_print("NEW    %s\n", zPathname);
      }
      if( new_exists && (new_link || old_link) ){







|







68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
    blob_zero(&new);
    old_exists = db_column_int(&q, 1);
    old_exe = db_column_int(&q, 2);
    if( old_exists ){
      db_ephemeral_blob(&q, 0, &new);
    }
    if( file_unsafe_in_tree_path(zFullname) ){
      /* do nothing with this unsafe file */
    }else if( old_exists ){
      if( new_exists ){
        fossil_print("%s   %s\n", redoFlag ? "REDO" : "UNDO", zPathname);
      }else{
        fossil_print("NEW    %s\n", zPathname);
      }
      if( new_exists && (new_link || old_link) ){
Changes to src/unversioned.c.
244
245
246
247
248
249
250


251
252
253
254
255
256
257
**                           the name to be different in the repository versus
**                           what appears on disk, but it only allows adding
**                           a single file at a time.
**
**    cat FILE ...           Concatenate the content of FILEs to stdout.
**
**    edit FILE              Bring up FILE in a text editor for modification.


**
**    export FILE OUTPUT     Write the content of FILE into OUTPUT on disk
**
**    list | ls              Show all unversioned files held in the local
**                           repository.
**
**                           Options:







>
>







244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
**                           the name to be different in the repository versus
**                           what appears on disk, but it only allows adding
**                           a single file at a time.
**
**    cat FILE ...           Concatenate the content of FILEs to stdout.
**
**    edit FILE              Bring up FILE in a text editor for modification.
**                           Options:
**                             --editor NAME     Name of the text editor to use
**
**    export FILE OUTPUT     Write the content of FILE into OUTPUT on disk
**
**    list | ls              Show all unversioned files held in the local
**                           repository.
**
**                           Options:
359
360
361
362
363
364
365
366
367
368
369
370
371
372



373
374
375
376
377
378
379
  }else if( strncmp(zCmd, "edit", nCmd)==0 ){
    const char *zEditor;    /* Name of the text-editor command */
    const char *zTFile;     /* Temporary file */
    const char *zUVFile;    /* Name of the unversioned file */
    char *zCmd;             /* Command to run the text editor */
    Blob content;           /* Content of the unversioned file */

    verify_all_options();
    if( g.argc!=4) usage("edit UVFILE");
    zUVFile = g.argv[3];
    zEditor = fossil_text_editor();
    if( zEditor==0 ){
      fossil_fatal("no text editor - set the VISUAL env variable");
    }



    zTFile = fossil_temp_filename();
    if( zTFile==0 ) fossil_fatal("cannot find a temporary filename");
    db_begin_transaction();
    content_rcvid_init("#!fossil unversioned edit");
    if( unversioned_content(zUVFile, &content)==0 ){
      fossil_fatal("no such uv-file: %Q", zUVFile);
    }







<
<
<




>
>
>







361
362
363
364
365
366
367



368
369
370
371
372
373
374
375
376
377
378
379
380
381
  }else if( strncmp(zCmd, "edit", nCmd)==0 ){
    const char *zEditor;    /* Name of the text-editor command */
    const char *zTFile;     /* Temporary file */
    const char *zUVFile;    /* Name of the unversioned file */
    char *zCmd;             /* Command to run the text editor */
    Blob content;           /* Content of the unversioned file */




    zEditor = fossil_text_editor();
    if( zEditor==0 ){
      fossil_fatal("no text editor - set the VISUAL env variable");
    }
    verify_all_options();
    if( g.argc!=4) usage("edit UVFILE");
    zUVFile = g.argv[3];
    zTFile = fossil_temp_filename();
    if( zTFile==0 ) fossil_fatal("cannot find a temporary filename");
    db_begin_transaction();
    content_rcvid_init("#!fossil unversioned edit");
    if( unversioned_content(zUVFile, &content)==0 ){
      fossil_fatal("no such uv-file: %Q", zUVFile);
    }
Changes to src/user.c.
324
325
326
327
328
329
330
331
332
333
334







335
336
337
338
339
340
341
**
**        Query or set the capabilities for user USERNAME
**
** > fossil user contact USERNAME ?CONTACT-INFO?
**
**        Query or set contact information for user USERNAME
**
** > fossil user default ?USERNAME?
**
**        Query or set the default user.  The default user is the
**        user for command-line interaction.







**
** > fossil user list | ls
**
**        List all users known to the repository
**
** > fossil user new ?USERNAME? ?CONTACT-INFO? ?PASSWORD?
**







|


|
>
>
>
>
>
>
>







324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
**
**        Query or set the capabilities for user USERNAME
**
** > fossil user contact USERNAME ?CONTACT-INFO?
**
**        Query or set contact information for user USERNAME
**
** > fossil user default ?OPTIONS? ?USERNAME?
**
**        Query or set the default user.  The default user is the
**        user for command-line interaction.  If USERNAME is an
**        empty string, then the default user is unset from the
**        repository and will subsequently be determined by the -U
**        command-line option or by environment variables
**        FOSSIL_USER, USER, LOGNAME, or USERNAME, in that order.
**        OPTIONS:
**
**            -v|--verbose        Show how the default user is computed
**
** > fossil user list | ls
**
**        List all users known to the repository
**
** > fossil user new ?USERNAME? ?CONTACT-INFO? ?PASSWORD?
**
383
384
385
386
387
388
389


390
391
392






393
394
395
396
397
398
399
400





















401
402
403
404
405
406
407
      "INSERT INTO user(login,pw,cap,info,mtime)"
      "VALUES(%B,%Q,%B,%B,now())",
      &login, zPw, &caps, &contact
    );
    db_protect_pop();
    free(zPw);
  }else if( n>=2 && strncmp(g.argv[2],"default",n)==0 ){


    if( g.argc==3 ){
      user_select();
      fossil_print("%s\n", g.zLogin);






    }else{
      if( !db_exists("SELECT 1 FROM user WHERE login=%Q", g.argv[3]) ){
        fossil_fatal("no such user: %s", g.argv[3]);
      }
      if( g.localOpen ){
        db_lset("default-user", g.argv[3]);
      }else{
        db_set("default-user", g.argv[3], 0);





















      }
    }
  }else if(( n>=2 && strncmp(g.argv[2],"list",n)==0 ) ||
           ( n>=2 && strncmp(g.argv[2],"ls",n)==0 )){
    Stmt q;
    db_prepare(&q, "SELECT login, info FROM user ORDER BY login");
    while( db_step(&q)==SQLITE_ROW ){







>
>
|
|
|
>
>
>
>
>
>
|
|
|
|
|
|
|
|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
      "INSERT INTO user(login,pw,cap,info,mtime)"
      "VALUES(%B,%Q,%B,%B,now())",
      &login, zPw, &caps, &contact
    );
    db_protect_pop();
    free(zPw);
  }else if( n>=2 && strncmp(g.argv[2],"default",n)==0 ){
    int eVerbose = find_option("verbose","v",0)!=0;
    verify_all_options();
    if( g.argc>3 ){
      const char *zUser = g.argv[3];
      if( fossil_strcmp(zUser,"")==0 || fossil_stricmp(zUser,"nobody")==0 ){
        db_begin_transaction();
        if( g.localOpen ){
          db_multi_exec("DELETE FROM vvar WHERE name='default-user'");
        }
        db_unset("default-user",0);
        db_commit_transaction();
      }else{
        if( !db_exists("SELECT 1 FROM user WHERE login=%Q", g.argv[3]) ){
          fossil_fatal("no such user: %s", g.argv[3]);
        }
        if( g.localOpen ){
          db_lset("default-user", g.argv[3]);
        }else{
          db_set("default-user", g.argv[3], 0);
        }
      }
    }
    if( g.argc==3 || eVerbose ){
      int eHow = user_select();
      const char *zHow = "???";
      switch( eHow ){
        case 1:  zHow = "-U option"; break;
        case 2:  zHow = "previously set"; break;
        case 3:  zHow = "local check-out"; break;
        case 4:  zHow = "repository"; break;
        case 5:  zHow = "FOSSIL_USER"; break;
        case 6:  zHow = "USER"; break;
        case 7:  zHow = "LOGNAME"; break;
        case 8:  zHow = "USERNAME"; break;
        case 9:  zHow = "URL"; break;
      }
      if( eVerbose ){
        fossil_print("%s (determined by %s)\n", g.zLogin, zHow);
      }else{
        fossil_print("%s\n", g.zLogin);
      }
    }
  }else if(( n>=2 && strncmp(g.argv[2],"list",n)==0 ) ||
           ( n>=2 && strncmp(g.argv[2],"ls",n)==0 )){
    Stmt q;
    db_prepare(&q, "SELECT login, info FROM user ORDER BY login");
    while( db_step(&q)==SQLITE_ROW ){
494
495
496
497
498
499
500


501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
}

/*
** Figure out what user is at the controls.
**
**   (1)  Use the --user and -U command-line options.
**


**   (2)  If the local database is open, check in VVAR.
**
**   (3)  Check the default user in the repository
**
**   (4)  Try the FOSSIL_USER environment variable.
**
**   (5)  Try the USER environment variable.
**
**   (6)  Try the LOGNAME environment variable.
**
**   (7)  Try the USERNAME environment variable.
**
**   (8)  Check if the user can be extracted from the remote URL.
**
** The user name is stored in g.zLogin.  The uid is in g.userUid.
*/
void user_select(void){
  UrlData url;
  if( g.userUid ) return;
  if( g.zLogin ){
    if( attempt_user(g.zLogin)==0 ){
      fossil_fatal("no such user: %s", g.zLogin);
    }else{
      return;
    }
  }

  if( g.localOpen && attempt_user(db_lget("default-user",0)) ) return;

  if( attempt_user(db_get("default-user", 0)) ) return;

  if( attempt_user(fossil_getenv("FOSSIL_USER")) ) return;

  if( attempt_user(fossil_getenv("USER")) ) return;

  if( attempt_user(fossil_getenv("LOGNAME")) ) return;

  if( attempt_user(fossil_getenv("USERNAME")) ) return;

  memset(&url, 0, sizeof(url));
  url_parse_local(0, URL_USE_CONFIG, &url);
  if( url.user && attempt_user(url.user) ) return;

  fossil_print(
    "Cannot figure out who you are!  Consider using the --user\n"
    "command line option, setting your USER environment variable,\n"
    "or setting a default user with \"fossil user default USER\".\n"
  );
  fossil_fatal("cannot determine user");







>
>
|

|

|

|

|

|

|



|

|




|



|

|

|

|

|

|



|







530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
}

/*
** Figure out what user is at the controls.
**
**   (1)  Use the --user and -U command-line options.
**
**   (2)  The name used for login (if there was a login).
**
**   (3)  If the local database is open, check in VVAR.
**
**   (4)  Check the default-user in the repository
**
**   (5)  Try the FOSSIL_USER environment variable.
**
**   (6)  Try the USER environment variable.
**
**   (7)  Try the LOGNAME environment variable.
**
**   (8)  Try the USERNAME environment variable.
**
**   (9)  Check if the user can be extracted from the remote URL.
**
** The user name is stored in g.zLogin.  The uid is in g.userUid.
*/
int user_select(void){
  UrlData url;
  if( g.userUid ) return 1;
  if( g.zLogin ){
    if( attempt_user(g.zLogin)==0 ){
      fossil_fatal("no such user: %s", g.zLogin);
    }else{
      return 2;
    }
  }

  if( g.localOpen && attempt_user(db_lget("default-user",0)) ) return 3;

  if( attempt_user(db_get("default-user", 0)) ) return 4;

  if( attempt_user(fossil_getenv("FOSSIL_USER")) ) return 5;

  if( attempt_user(fossil_getenv("USER")) ) return 6;

  if( attempt_user(fossil_getenv("LOGNAME")) ) return 7;

  if( attempt_user(fossil_getenv("USERNAME")) ) return 8;

  memset(&url, 0, sizeof(url));
  url_parse_local(0, URL_USE_CONFIG, &url);
  if( url.user && attempt_user(url.user) ) return 9;

  fossil_print(
    "Cannot figure out who you are!  Consider using the --user\n"
    "command line option, setting your USER environment variable,\n"
    "or setting a default user with \"fossil user default USER\".\n"
  );
  fossil_fatal("cannot determine user");
Changes to src/util.c.
664
665
666
667
668
669
670

671
672
673
674
675
676



677
678
679
680
681
682
683






684
685
686
687
688
689
690
}

/*
** Return the name of the users preferred text editor.  Return NULL if
** not found.
**
** Search algorithm:

** (1) The local "editor" setting
** (2) The global "editor" setting
** (3) The VISUAL environment variable
** (4) The EDITOR environment variable
** (5) Any of the following programs that are available:
**        notepad, nano, pico, jove, edit, vi, vim, ed,



*/
const char *fossil_text_editor(void){
  const char *zEditor = db_get("editor", 0);
  const char *azStdEd[] = {
    "notepad", "nano", "pico", "jove", "edit", "vi", "vim", "ed"
  };
  int i = 0;






  if( zEditor==0 ){
    zEditor = fossil_getenv("VISUAL");
  }
  if( zEditor==0 ){
    zEditor = fossil_getenv("EDITOR");
  }
  while( zEditor==0 && i<count(azStdEd) ){







>
|
|
|
|
|

>
>
>


|




>
>
>
>
>
>







664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
}

/*
** Return the name of the users preferred text editor.  Return NULL if
** not found.
**
** Search algorithm:
** (1) The value of the --editor command-line argument
** (2) The local "editor" setting
** (3) The global "editor" setting
** (4) The VISUAL environment variable
** (5) The EDITOR environment variable
** (6) Any of the following programs that are available:
**        notepad, nano, pico, jove, edit, vi, vim, ed,
**
** The search only occurs once, the first time this routine is called.
** Second and subsequent invocations always return the same value.
*/
const char *fossil_text_editor(void){
  static const char *zEditor = 0;
  const char *azStdEd[] = {
    "notepad", "nano", "pico", "jove", "edit", "vi", "vim", "ed"
  };
  int i = 0;
  if( zEditor==0 ){
    zEditor = find_option("editor",0,1);
  }
  if( zEditor==0 ){
    zEditor = db_get("editor", 0);
  }
  if( zEditor==0 ){
    zEditor = fossil_getenv("VISUAL");
  }
  if( zEditor==0 ){
    zEditor = fossil_getenv("EDITOR");
  }
  while( zEditor==0 && i<count(azStdEd) ){
Changes to src/winfile.c.
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
                    fi2.FileId[5],  fi2.FileId[4],
                    fi2.FileId[3],  fi2.FileId[2],
                    fi2.FileId[1],  fi2.FileId[0]);
      }
    }
    if( zFileId==0 ){
      if( GetFileInformationByHandle(hFile,&fi) ){
        ULARGE_INTEGER FileId = {
          /*.LowPart = */ fi.nFileIndexLow,
          /*.HighPart = */ fi.nFileIndexHigh
        };
        zFileId = mprintf(
                    "%08x/%016llx",
                    fi.dwVolumeSerialNumber,(u64)FileId.QuadPart);
      }
    }
    CloseHandle(hFile);
  }







|


|







503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
                    fi2.FileId[5],  fi2.FileId[4],
                    fi2.FileId[3],  fi2.FileId[2],
                    fi2.FileId[1],  fi2.FileId[0]);
      }
    }
    if( zFileId==0 ){
      if( GetFileInformationByHandle(hFile,&fi) ){
        ULARGE_INTEGER FileId = {{
          /*.LowPart = */ fi.nFileIndexLow,
          /*.HighPart = */ fi.nFileIndexHigh
        }};
        zFileId = mprintf(
                    "%08x/%016llx",
                    fi.dwVolumeSerialNumber,(u64)FileId.QuadPart);
      }
    }
    CloseHandle(hFile);
  }
Changes to src/xfer.c.
1114
1115
1116
1117
1118
1119
1120












1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
    int sz = db_column_int(&uvq,3);
    if( zHash==0 ){ sz = 0; zHash = "-"; }
    blob_appendf(pXfer->pOut, "uvigot %s %lld %s %d\n",
                 zName, mtime, zHash, sz);
  }
  db_finalize(&uvq);
}













/*
** Called when there is an attempt to transfer private content to and
** from a server without authorization.
*/
static void server_private_xfer_not_authorized(void){
  @ error not\sauthorized\sto\ssync\sprivate\scontent
}

/*
** Return the common TH1 code to evaluate prior to evaluating any other
** TH1 transfer notification scripts.
*/
const char *xfer_common_code(void){







>
>
>
>
>
>
>
>
>
>
>
>






|







1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
    int sz = db_column_int(&uvq,3);
    if( zHash==0 ){ sz = 0; zHash = "-"; }
    blob_appendf(pXfer->pOut, "uvigot %s %lld %s %d\n",
                 zName, mtime, zHash, sz);
  }
  db_finalize(&uvq);
}

/*
** Return a string that contains supplemental information about a
** "not authorized" error.  The string might be empty if no additional
** information is available.
*/
static char *whyNotAuth(void){
  if( g.useLocalauth && db_get_boolean("localauth",0)!=0 ){
    return "\\sbecause\\sthe\\s'localauth'\\ssetting\\sis\\senabled";
  }
  return "";
}

/*
** Called when there is an attempt to transfer private content to and
** from a server without authorization.
*/
static void server_private_xfer_not_authorized(void){
  @ error not\sauthorized\sto\ssync\sprivate\scontent%s(whyNotAuth())
}

/*
** Return the common TH1 code to evaluate prior to evaluating any other
** TH1 transfer notification scripts.
*/
const char *xfer_common_code(void){
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
    **   file HASH DELTASRC SIZE \n CONTENT
    **
    ** Server accepts a file from the client.
    */
    if( blob_eq(&xfer.aToken[0], "file") ){
      if( !isPush ){
        cgi_reset_content();
        @ error not\sauthorized\sto\swrite
        nErr++;
        break;
      }
      xfer_accept_file(&xfer, 0, pzUuidList, pnUuidList);
      if( blob_size(&xfer.err) ){
        cgi_reset_content();
        @ error %T(blob_str(&xfer.err))
        nErr++;
        break;
      }
    }else

    /*   cfile HASH USIZE CSIZE \n CONTENT
    **   cfile HASH DELTASRC USIZE CSIZE \n CONTENT
    **
    ** Server accepts a compressed file from the client.
    */
    if( blob_eq(&xfer.aToken[0], "cfile") ){
      if( !isPush ){
        cgi_reset_content();
        @ error not\sauthorized\sto\swrite
        nErr++;
        break;
      }
      xfer_accept_compressed_file(&xfer, pzUuidList, pnUuidList);
      if( blob_size(&xfer.err) ){
        cgi_reset_content();
        @ error %T(blob_str(&xfer.err))







|




















|







1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
    **   file HASH DELTASRC SIZE \n CONTENT
    **
    ** Server accepts a file from the client.
    */
    if( blob_eq(&xfer.aToken[0], "file") ){
      if( !isPush ){
        cgi_reset_content();
        @ error not\sauthorized\sto\swrite%s(whyNotAuth())
        nErr++;
        break;
      }
      xfer_accept_file(&xfer, 0, pzUuidList, pnUuidList);
      if( blob_size(&xfer.err) ){
        cgi_reset_content();
        @ error %T(blob_str(&xfer.err))
        nErr++;
        break;
      }
    }else

    /*   cfile HASH USIZE CSIZE \n CONTENT
    **   cfile HASH DELTASRC USIZE CSIZE \n CONTENT
    **
    ** Server accepts a compressed file from the client.
    */
    if( blob_eq(&xfer.aToken[0], "cfile") ){
      if( !isPush ){
        cgi_reset_content();
        @ error not\sauthorized\sto\swrite%s(whyNotAuth())
        nErr++;
        break;
      }
      xfer_accept_compressed_file(&xfer, pzUuidList, pnUuidList);
      if( blob_size(&xfer.err) ){
        cgi_reset_content();
        @ error %T(blob_str(&xfer.err))
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
        nErr++;
        break;
      }
      login_check_credentials();
      if( blob_eq(&xfer.aToken[0], "pull") ){
        if( !g.perm.Read ){
          cgi_reset_content();
          @ error not\sauthorized\sto\sread
          nErr++;
          break;
        }
        isPull = 1;
      }else{
        if( !g.perm.Write ){
          if( !isPull ){
            cgi_reset_content();
            @ error not\sauthorized\sto\swrite
            nErr++;
          }else{
            @ message pull\sonly\s-\snot\sauthorized\sto\spush
          }
        }else{
          isPush = 1;
        }
      }
    }else

    /*    clone   ?PROTOCOL-VERSION?  ?SEQUENCE-NUMBER?
    **
    ** The client knows nothing.  Tell all.
    */
    if( blob_eq(&xfer.aToken[0], "clone") ){
      int iVers;
      login_check_credentials();
      if( !g.perm.Clone ){
        cgi_reset_content();
        @ push %s(db_get("server-code", "x")) %s(db_get("project-code", "x"))
        @ error not\sauthorized\sto\sclone
        nErr++;
        break;
      }
      if( db_get_boolean("uv-sync",0) && !uvCatalogSent ){
        @ pragma uv-pull-only
        send_unversioned_catalog(&xfer);
        uvCatalogSent = 1;







|








|


|

















|







1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
        nErr++;
        break;
      }
      login_check_credentials();
      if( blob_eq(&xfer.aToken[0], "pull") ){
        if( !g.perm.Read ){
          cgi_reset_content();
          @ error not\sauthorized\sto\sread%s(whyNotAuth())
          nErr++;
          break;
        }
        isPull = 1;
      }else{
        if( !g.perm.Write ){
          if( !isPull ){
            cgi_reset_content();
            @ error not\sauthorized\sto\swrite%s(whyNotAuth())
            nErr++;
          }else{
            @ message pull\sonly\s-\snot\sauthorized\sto\spush%s(whyNotAuth())
          }
        }else{
          isPush = 1;
        }
      }
    }else

    /*    clone   ?PROTOCOL-VERSION?  ?SEQUENCE-NUMBER?
    **
    ** The client knows nothing.  Tell all.
    */
    if( blob_eq(&xfer.aToken[0], "clone") ){
      int iVers;
      login_check_credentials();
      if( !g.perm.Clone ){
        cgi_reset_content();
        @ push %s(db_get("server-code", "x")) %s(db_get("project-code", "x"))
        @ error not\sauthorized\sto\sclone%s(whyNotAuth())
        nErr++;
        break;
      }
      if( db_get_boolean("uv-sync",0) && !uvCatalogSent ){
        @ pragma uv-pull-only
        send_unversioned_catalog(&xfer);
        uvCatalogSent = 1;
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
        xfer_fatal_error("invalid config record");
        return;
      }
      blob_zero(&content);
      blob_extract(xfer.pIn, size, &content);
      if( !g.perm.Admin ){
        cgi_reset_content();
        @ error not\sauthorized\sto\spush\sconfiguration
        nErr++;
        break;
      }
      configure_receive(zName, &content, CONFIGSET_ALL);
      blob_reset(&content);
      blob_seek(xfer.pIn, 1, BLOB_SEEK_CUR);
    }else







|







1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
        xfer_fatal_error("invalid config record");
        return;
      }
      blob_zero(&content);
      blob_extract(xfer.pIn, size, &content);
      if( !g.perm.Admin ){
        cgi_reset_content();
        @ error not\sauthorized\sto\spush\sconfiguration%s(whyNotAuth())
        nErr++;
        break;
      }
      configure_receive(zName, &content, CONFIGSET_ALL);
      blob_reset(&content);
      blob_seek(xfer.pIn, 1, BLOB_SEEK_CUR);
    }else
Changes to test/many-www.tcl.
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
  /taglist
  /reportlist
  /setup
  /dir
  /wcontent
  /attachlist
  /taglist
  /test_env
  /stat
  /rcvfromlist
  /urllist
  /modreq
  /info/d5c4
  /test-all-help
  /leaves







|







22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
  /taglist
  /reportlist
  /setup
  /dir
  /wcontent
  /attachlist
  /taglist
  /test-env
  /stat
  /rcvfromlist
  /urllist
  /modreq
  /info/d5c4
  /test-all-help
  /leaves
Changes to test/tester.tcl.
354
355
356
357
358
359
360

361
362
363
364
365
366
367
368
369


370
371
372
373
374
375
376
377




378
379

380

381
382
383
384
385
386
387
      max-upload \
      mimetypes \
      mtime-changes \
      mv-rm-files \
      pgp-command \
      preferred-diff-type \
      proxy \

      redirect-to-https \
      relative-paths \
      repo-cksum \
      repolist-skin \
      robot-restrict \
      robots-txt \
      safe-html \
      self-pw-reset \
      self-register \


      sitemap-extra \
      ssh-command \
      ssl-ca-location \
      ssl-identity \
      tclsh \
      th1-setup \
      th1-uri-regexp \
      ticket-default-report \




      timeline-utc \
      user-color-map \

      uv-sync \

      web-browser]

  fossil test-th-eval "hasfeature legacyMvRm"

  if {[normalize_result] eq "1"} {
    lappend result mv-rm-files
  }







>









>
>








>
>
>
>


>

>







354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
      max-upload \
      mimetypes \
      mtime-changes \
      mv-rm-files \
      pgp-command \
      preferred-diff-type \
      proxy \
      raw-bgcolor \
      redirect-to-https \
      relative-paths \
      repo-cksum \
      repolist-skin \
      robot-restrict \
      robots-txt \
      safe-html \
      self-pw-reset \
      self-register \
      show-repolist-desc \
      show-repolist-lg \
      sitemap-extra \
      ssh-command \
      ssl-ca-location \
      ssl-identity \
      tclsh \
      th1-setup \
      th1-uri-regexp \
      ticket-default-report \
      timeline-hard-newlines \
      timeline-plaintext \
      timeline-truncate-at-blank \
      timeline-tslink-info \
      timeline-utc \
      user-color-map \
      verify-comments \
      uv-sync \
      vuln-report \
      web-browser]

  fossil test-th-eval "hasfeature legacyMvRm"

  if {[normalize_result] eq "1"} {
    lappend result mv-rm-files
  }
Added test/th1-taint.test.








































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
#
# Copyright (c) 2025 D. Richard Hipp
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the Simplified BSD License (also
# known as the "2-Clause License" or "FreeBSD License".)
#
# This program is distributed in the hope that it will be useful,
# but without any warranty; without even the implied warranty of
# merchantability or fitness for a particular purpose.
#
# Author contact information:
#   drh@hwaci.com
#   http://www.hwaci.com/drh/
#
############################################################################
#
# TH1 Commands
#

set path [file dirname [info script]]; test_setup

proc taint-test {testnum th1script expected} {
  global fossilexe
  set rc [catch {exec $fossilexe test-th-eval $th1script} got]
  if {$rc} {
    test th1-taint-$testnum 0
    puts $got
    return
  }
  if {$got ne $expected} {
    test th1-taint-$testnum 0
    puts " Expected: $expected"
    puts " Got:      $got"
  } else {
    test th1-taint-$testnum 1
  }
}

taint-test 10 {string is tainted abcd} 0
taint-test 20 {string is tainted [taint abcd]} 1
taint-test 30 {string is tainted [untaint [taint abcd]]} 0
taint-test 40 {string is tainted [untaint abcde]} 0
taint-test 50 {string is tainted "abc[taint def]ghi"} 1
taint-test 60 {set t1 [taint abc]; string is tainted "123 $t1 456"} 1

taint-test 100 {set t1 [taint abc]; lappend t1 def; string is tainted $t1} 1
taint-test 110 {set t1 abc; lappend t1 [taint def]; string is tainted $t1} 1

taint-test 200 {string is tainted [list abc def ghi]} 0
taint-test 210 {string is tainted [list [taint abc] def ghi]} 1
taint-test 220 {string is tainted [list abc [taint def] ghi]} 1
taint-test 230 {string is tainted [list abc def [taint ghi]]} 1

taint-test 300 {
  set res {}
  foreach x [list abc [taint def] ghi] {
    lappend res [string is tainted $x]
  }
  set res
} {1 1 1}
taint-test 310 {
  set res {}
  foreach {x y} [list abc [taint def] ghi jkl] {
    lappend res [string is tainted $x] [string is tainted $y]
  }
  set res
} {1 1 1 1}

taint-test 400 {string is tainted [lindex "abc [taint def] ghi" 0]} 1
taint-test 410 {string is tainted [lindex "abc [taint def] ghi" 1]} 1
taint-test 420 {string is tainted [lindex "abc [taint def] ghi" 2]} 1
taint-test 430 {string is tainted [lindex "abc [taint def] ghi" 3]} 0

taint-test 500 {string is tainted [string index [taint abcdefg] 3]} 1
  
taint-test 600 {string is tainted [string range [taint abcdefg] 3 5]} 1

taint-test 700 {string is tainted [string trim [taint "  abcdefg  "]]} 1
taint-test 710 {string is tainted [string trimright [taint "  abcdefg  "]]} 1
taint-test 720 {string is tainted [string trimleft [taint "  abcdefg  "]]} 1
  

test_cleanup
Changes to test/th1.test.
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
test th1-defHeader-2 {[string match *<body> [normalize_result]] || \
    [string match "*<body class=\"\$current_feature\
                    rpage-\$requested_page\
                    cpage-\$canonical_page\">" [normalize_result]]}

###############################################################################

fossil test-th-eval "styleHeader {Page Title Here}"
test th1-header-1 {$RESULT eq {TH_ERROR: repository unavailable}}

###############################################################################

test_in_checkout th1-header-2 {
  fossil test-th-eval --open-config "styleHeader {Page Title Here}"
} {[regexp -- {<title>Fossil: Page Title Here</title>} $RESULT]}

###############################################################################

fossil test-th-eval "styleFooter"
test th1-footer-1 {$RESULT eq {TH_ERROR: repository unavailable}}

###############################################################################

fossil test-th-eval --open-config "styleFooter"
test th1-footer-2 {$RESULT eq {}}

###############################################################################







|
|









|
|







793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
test th1-defHeader-2 {[string match *<body> [normalize_result]] || \
    [string match "*<body class=\"\$current_feature\
                    rpage-\$requested_page\
                    cpage-\$canonical_page\">" [normalize_result]]}

###############################################################################

#fossil test-th-eval "styleHeader {Page Title Here}"
#test th1-header-1 {$RESULT eq {TH_ERROR: repository unavailable}}

###############################################################################

test_in_checkout th1-header-2 {
  fossil test-th-eval --open-config "styleHeader {Page Title Here}"
} {[regexp -- {<title>Fossil: Page Title Here</title>} $RESULT]}

###############################################################################

#fossil test-th-eval "styleFooter"
#test th1-footer-1 {$RESULT eq {TH_ERROR: repository unavailable}}

###############################################################################

fossil test-th-eval --open-config "styleFooter"
test th1-footer-2 {$RESULT eq {}}

###############################################################################
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924

fossil test-th-eval "artifact"
test th1-artifact-1 {$RESULT eq \
    {TH_ERROR: wrong # args: should be "artifact ID ?FILENAME?"}}

###############################################################################

fossil test-th-eval "artifact tip"
test th1-artifact-2 {$RESULT eq {TH_ERROR: repository unavailable}}

###############################################################################

test_in_checkout th1-artifact-3 {
  fossil test-th-eval --open-config "artifact tip"
} {[regexp -- {F test/th1\.test [0-9a-f]{40,64}} $RESULT]}

###############################################################################

fossil test-th-eval "artifact 0000000000"
test th1-artifact-4 {$RESULT eq {TH_ERROR: repository unavailable}}

###############################################################################

fossil test-th-eval --open-config "artifact 0000000000"
test th1-artifact-5 {$RESULT eq {TH_ERROR: name not found}}

###############################################################################

fossil test-th-eval "artifact tip test/th1.test"
test th1-artifact-6 {$RESULT eq {TH_ERROR: repository unavailable}}

###############################################################################

test_in_checkout th1-artifact-7 {
  fossil test-th-eval --open-config "artifact tip test/th1.test"
} {[regexp -- {th1-artifact-7} $RESULT]}

###############################################################################

fossil test-th-eval "artifact 0000000000 test/th1.test"
test th1-artifact-8 {$RESULT eq {TH_ERROR: repository unavailable}}

###############################################################################

fossil test-th-eval --open-config "artifact 0000000000 test/th1.test"
test th1-artifact-9 {$RESULT eq {TH_ERROR: manifest not found}}

###############################################################################







|
|









|
|








|
|









|
|







877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924

fossil test-th-eval "artifact"
test th1-artifact-1 {$RESULT eq \
    {TH_ERROR: wrong # args: should be "artifact ID ?FILENAME?"}}

###############################################################################

#fossil test-th-eval "artifact tip"
#test th1-artifact-2 {$RESULT eq {TH_ERROR: repository unavailable}}

###############################################################################

test_in_checkout th1-artifact-3 {
  fossil test-th-eval --open-config "artifact tip"
} {[regexp -- {F test/th1\.test [0-9a-f]{40,64}} $RESULT]}

###############################################################################

#fossil test-th-eval "artifact 0000000000"
#test th1-artifact-4 {$RESULT eq {TH_ERROR: repository unavailable}}

###############################################################################

fossil test-th-eval --open-config "artifact 0000000000"
test th1-artifact-5 {$RESULT eq {TH_ERROR: name not found}}

###############################################################################

#fossil test-th-eval "artifact tip test/th1.test"
#test th1-artifact-6 {$RESULT eq {TH_ERROR: repository unavailable}}

###############################################################################

test_in_checkout th1-artifact-7 {
  fossil test-th-eval --open-config "artifact tip test/th1.test"
} {[regexp -- {th1-artifact-7} $RESULT]}

###############################################################################

#fossil test-th-eval "artifact 0000000000 test/th1.test"
#test th1-artifact-8 {$RESULT eq {TH_ERROR: repository unavailable}}

###############################################################################

fossil test-th-eval --open-config "artifact 0000000000 test/th1.test"
test th1-artifact-9 {$RESULT eq {TH_ERROR: manifest not found}}

###############################################################################
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
    test th1-globalState-2 {$RESULT eq \
        [fossil test-th-eval --open-config checkout]}
  }
}

###############################################################################

fossil test-th-eval "globalState configuration"
test th1-globalState-3 {[string length $RESULT] == 0}

###############################################################################

fossil test-th-eval --open-config "globalState configuration"
test th1-globalState-4 {[string length $RESULT] > 0}

###############################################################################







|
|







945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
    test th1-globalState-2 {$RESULT eq \
        [fossil test-th-eval --open-config checkout]}
  }
}

###############################################################################

#fossil test-th-eval "globalState configuration"
#test th1-globalState-3 {[string length $RESULT] == 0}

###############################################################################

fossil test-th-eval --open-config "globalState configuration"
test th1-globalState-4 {[string length $RESULT] > 0}

###############################################################################
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079

1080
1081
1082
1083
1084
1085
1086
###############################################################################

fossil test-th-eval "globalState flags"
test th1-globalState-16 {$RESULT eq "0"}

###############################################################################

fossil test-th-eval "reinitialize; globalState configuration"
test th1-reinitialize-1 {$RESULT eq ""}

###############################################################################

fossil test-th-eval "reinitialize 1; globalState configuration"
test th1-reinitialize-2 {$RESULT ne ""}

###############################################################################

#
# NOTE: This test will fail if the command names are added to TH1, or
#       moved from Tcl builds to plain or the reverse. Sorting the
#       command lists eliminates a dependence on order.
#
fossil test-th-eval "info commands"
set sorted_result [lsort $RESULT]
protOut "Sorted: $sorted_result"
set base_commands {anoncap anycap array artifact break breakpoint \
      builtin_request_js capexpr captureTh1 catch cgiHeaderLine checkout \
      combobox continue copybtn date decorate defHeader dir enable_htmlify \
      enable_output encode64 error expr for foreach getParameter glob_match \
      globalState hascap hasfeature html htmlize http httpize if info \
      insertCsrf lappend lindex linecount list llength lsearch markdown nonce \
      proc puts query randhex redirect regexp reinitialize rename render \
      repository return searchable set setParameter setting stime string \
      styleFooter styleHeader styleScript submenu tclReady trace unset \
      unversioned uplevel upvar utime verifyCsrf verifyLogin wiki}
set tcl_commands {tclEval tclExpr tclInvoke tclIsSafe tclMakeSafe}
if {$th1Tcl} {
  test th1-info-commands-1 {$sorted_result eq [lsort "$base_commands $tcl_commands"]}
} else {
  test th1-info-commands-1 {$sorted_result eq [lsort "$base_commands"]}
}


###############################################################################

fossil test-th-eval "info vars"

if {$th1Hooks} {
  test th1-info-vars-1 {[lsort $RESULT] eq \







|
|













|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<
>







1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078

1079
1080
1081
1082
1083
1084
1085
1086
###############################################################################

fossil test-th-eval "globalState flags"
test th1-globalState-16 {$RESULT eq "0"}

###############################################################################

#fossil test-th-eval "reinitialize; globalState configuration"
#test th1-reinitialize-1 {$RESULT eq ""}

###############################################################################

fossil test-th-eval "reinitialize 1; globalState configuration"
test th1-reinitialize-2 {$RESULT ne ""}

###############################################################################

#
# NOTE: This test will fail if the command names are added to TH1, or
#       moved from Tcl builds to plain or the reverse. Sorting the
#       command lists eliminates a dependence on order.
#
#fossil test-th-eval "info commands"
#set sorted_result [lsort $RESULT]
#protOut "Sorted: $sorted_result"
#set base_commands {anoncap anycap array artifact break breakpoint \
#     builtin_request_js capexpr captureTh1 catch cgiHeaderLine checkout \
#      combobox continue copybtn date decorate defHeader dir enable_htmlify \
#      enable_output encode64 error expr for foreach getParameter glob_match \
#      globalState hascap hasfeature html htmlize http httpize if info \
#      insertCsrf lappend lindex linecount list llength lsearch markdown nonce \
#      proc puts query randhex redirect regexp reinitialize rename render \
#      repository return searchable set setParameter setting stime string \
#      styleFooter styleHeader styleScript submenu tclReady trace unset \
#      unversioned uplevel upvar utime verifyCsrf verifyLogin wiki}
#set tcl_commands {tclEval tclExpr tclInvoke tclIsSafe tclMakeSafe}
#if {$th1Tcl} {
#  test th1-info-commands-1 {$sorted_result eq [lsort "$base_commands $tcl_commands"]}
#} else {
#  test th1-info-commands-1 {$sorted_result eq [lsort "$base_commands"]}

#}

###############################################################################

fossil test-th-eval "info vars"

if {$th1Hooks} {
  test th1-info-vars-1 {[lsort $RESULT] eq \
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
test th1-string-is-3 {$RESULT eq \
{TH_ERROR: wrong # args: should be "string is class string"}}

###############################################################################

fossil test-th-eval {string is other 123}
test th1-string-is-4 {$RESULT eq \
"TH_ERROR: Expected alnum, double, integer, or list, got: other"}

###############################################################################

fossil test-th-eval {string is alnum 123}
test th1-string-is-5 {$RESULT eq "1"}

###############################################################################







|







1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
test th1-string-is-3 {$RESULT eq \
{TH_ERROR: wrong # args: should be "string is class string"}}

###############################################################################

fossil test-th-eval {string is other 123}
test th1-string-is-4 {$RESULT eq \
"TH_ERROR: Expected alnum, double, integer, list, or tainted, got: other"}

###############################################################################

fossil test-th-eval {string is alnum 123}
test th1-string-is-5 {$RESULT eq "1"}

###############################################################################
Added tools/fake-smtpd.tcl.








































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
#!/usr/bin/tclsh
#
# This script is a testing aid for working on the Relay notification method
# in Fossil.
#
# This script listens for connections on port 25 or probably some other TCP
# port specified by the "--port N" option.  It pretend to be an SMTP server,
# though it does not actually relay any email.  Instead, it just prints the
# SMTP conversation on stdout.
#
# If the "--max N" option is used, then the fake SMTP server shuts down
# with an error after receiving N messages from the client.  This can be
# used to test retry capabilities in the client.
#
# Suggested Test Procedure For Fossil Relay Notifications
#
#    1.  Bring up "fossil ui"
#    2.  Configure notification for relay to localhost:8025
#    3.  Start this script in a separate window.  Something like:
#             tclsh fake-smtpd.tcl -port 8025 -max 100
#    4.  Send test messages using Fossil
#
proc conn_puts {chan txt} {
  puts "S: $txt"
  puts $chan $txt
  flush $chan
}
set mxMsg 100000000
proc connection {chan ip port} {
  global mxMsg
  set nMsg 0
  puts "*** begin connection from $ip:$port ***"
  conn_puts $chan "220 localhost fake-SMTPD"
  set inData 0
  while {1} {
    set line [string trimright [gets $chan]]
    if {$line eq ""} {
      if {[eof $chan]} break
    }
    puts "C: $line"
    incr nMsg
    if {$inData} {
      if {$line eq "."} {
        set inData 0
        conn_puts $chan "250 Ok"
      }
    } elseif {$nMsg>$mxMsg} {
      conn_puts $chan "999 I'm done!"
      break
    } elseif {[string match "HELO *" $line]} {
      conn_puts $chan "250 Ok"
    } elseif {[string match "EHLO *" $line]} {
      conn_puts $chan "250-SIZE 100000"
      conn_puts $chan "250 HELP"
    } elseif {[string match "DATA*" $line]} {
      conn_puts $chan "354 End data with <CR><LF>.<CR><LF>"
      set inData 1
    } elseif {[string match "QUIT*" $line]} {
      conn_puts $chan "221 Bye"
      break
    } else {
      conn_puts $chan "250 Ok"
    }
  }
  puts "*** connection closed ($nMsg messages) ***"
  close $chan
}
set port 25
set argc [llength $argv]
for {set i 0} {$i<$argc-1} {incr i} {
   set arg [lindex $argv $i]
   if {$arg eq "-port" || $arg eq "--port"} {
     incr i
     set port [lindex $argv $i]
   }
   if {$arg eq "-max" || $arg eq "--max"} {
     incr i
     set mxMsg [lindex $argv $i]
   }
}
puts "listening on localhost:$port"
socket -server connection $port
set forever 0
vwait forever
Added tools/find-fossil-cgis.tcl.


























































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
#!/usr/bin/tclsh
#
# This script scans a directory hierarchy looking for Fossil CGI files -
# the files that are used to launch Fossil as a CGI program.  For each
# such file found, in prints the name of the file and also the file
# content, indented, if the --print option is used.
#
#     tclsh find-fossil-cgis.tcl [OPTIONS] DIRECTORY
#
# The argument is the directory from which to begin the search.
#
# OPTIONS can be zero or more of the following:
#
#    --has REGEXP               Only show the CGI if the body matches REGEXP.
#                               May be repeated multiple times, in which case
#                               all must match.
#
#    --hasnot REGEXP            Only show the CGI if it does NOT match the
#                               REGEXP.
#
#    --print                    Show the content of the CGI, indented by
#                               three spaces
#
#    --symlink                  Process DIRECTORY arguments that are symlinks.
#                               Normally symlinks are silently ignored.
#
#    -v                         Show progress information for debugging
#
# EXAMPLE USE CASES:
#
# Find all CGIs that do not have the "errorlog:" property set
#
#    find-fossil-cgis.tcl *.website --has '\nrepository:' \
#           --hasnot '\nerrorlog:'
#
# Add the errorlog: property to any CGI that does not have it:
#
#    find-fossil-cgis.tcl *.website --has '\nrepository:' \
#           --hasnot '\nerrorlog:' | while read x
#    do
#      echo 'errorlog: /logs/errors.txt' >>$x
#    done
#
# Find and print all CGIs that do redirects
#
#    find-fossil-cgis.tcl *.website --has '\nredirect:' --print
#


# Find the CGIs in directory $dir.  Invoke recursively to
# scan subdirectories.
#
proc find_in_one_dir {dir} {
  global HAS HASNOT PRINT V
  if {$V>0} {
    puts "# $dir"
  }
  foreach obj [lsort [glob -nocomplain -directory $dir *]] {
    if {[file isdir $obj]} {
      find_in_one_dir $obj
      continue
    }
    if {![file isfile $obj]} continue
    if {[file size $obj]>5000} continue
    if {![file exec $obj]} continue
    if {![file readable $obj]} continue
    set fd [open $obj rb]
    set txt [read $fd]
    close $fd
    if {![string match #!* $txt]} continue
    if {![regexp {fossil} $txt]} continue
    if {![regexp {\nrepository: } $txt] &&
        ![regexp {\ndirectory: } $txt] &&
        ![regexp {\nredirect: } $txt]} continue
    set ok 1
    foreach re $HAS {
      if {![regexp $re $txt]} {set ok 0; break;}
    }
    if {!$ok} continue
    foreach re $HASNOT {
      if {[regexp $re $txt]} {set ok 0; break;}
    }
    if {!$ok} continue
    # 
    # At this point assume we have found a CGI file.
    #
    puts $obj
    if {$PRINT} {
      regsub -all {\n} [string trim $txt] "\n   " out
      puts "   $out"
    }
  }
}
set HAS [list]
set HASNOT [list]
set PRINT 0
set SYMLINK 0
set V 0
set N [llength $argv]
set DIRLIST [list]

# First pass:  Gather all the command-line arguments but do no
# processing.
#
for {set i 0} {$i<$N} {incr i} {
  set dir [lindex $argv $i]
  if {($dir eq "-has" || $dir eq "--has") && $i<[expr {$N-1}]} {
    incr i
    lappend HAS [lindex $argv $i]
    continue
  }
  if {($dir eq "-hasnot" || $dir eq "--hasnot") && $i<[expr {$N-1}]} {
    incr i
    lappend HASNOT [lindex $argv $i]
    continue
  }
  if {$dir eq "-print" || $dir eq "--print"} {
    set PRINT 1
    continue
  }
  if {$dir eq "-symlink" || $dir eq "--symlink"} {
    set SYMLINK 1
    continue
  }
  if {$dir eq "-v"} {
    set V 1
    continue
  }
  if {[file type $dir]=="directory"} {
    lappend DIRLIST $dir
  }
}

# Second pass: Process the non-option arguments.
#
foreach dir $DIRLIST {
  set type [file type $dir]
  if {$type eq "directory" || ($SYMLINK && $type eq "link")} {
    find_in_one_dir $dir
  }
}
Changes to tools/fossil-stress.tcl.
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
  /vdiff?from=2015-01-01&to=trunk&diff=0
  /wcontent
  /fileage
  /dir
  /tree
  /uvlist
  /stat
  /test_env
  /sitemap
  /hash-collisions
  /artifact_stats
  /bloblist
  /bigbloblist
  /wiki_rules
  /md_rules







|







91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
  /vdiff?from=2015-01-01&to=trunk&diff=0
  /wcontent
  /fileage
  /dir
  /tree
  /uvlist
  /stat
  /test-env
  /sitemap
  /hash-collisions
  /artifact_stats
  /bloblist
  /bigbloblist
  /wiki_rules
  /md_rules
Changes to www/aboutcgi.wiki.
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
    In this example: "timeline/four".
<tr><td>QUERY_STRING
    <td>The query string that follows the "?" in the URL, if there is one.
</table>

There are other CGI environment variables beyond those listed above.
Many Fossil servers implement the
[https://fossil-scm.org/home/test_env/two/three?abc=xyz|test_env]
webpage that shows some of the CGI environment
variables that Fossil pays attention to.

In addition to setting various CGI environment variables, if the HTTP
request contains POST content, then the web server relays the POST content
to standard input of the CGI script.








|







65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
    In this example: "timeline/four".
<tr><td>QUERY_STRING
    <td>The query string that follows the "?" in the URL, if there is one.
</table>

There are other CGI environment variables beyond those listed above.
Many Fossil servers implement the
[https://fossil-scm.org/home/test-env/two/three?abc=xyz|test-env]
webpage that shows some of the CGI environment
variables that Fossil pays attention to.

In addition to setting various CGI environment variables, if the HTTP
request contains POST content, then the web server relays the POST content
to standard input of the CGI script.

Changes to www/cgi.wiki.
77
78
79
80
81
82
83






84
85
86
87
88
89
90
[/help?cmd=repolist-skin|repolist-skin] setting.

If no repository has such a non-zero repolist-skin setting, then
the repository list is generic HTML without any decoration, with
the page title taken from the <tt>FOSSIL_REPOLIST_TITLE</tt>
environment variable. The variable can be defined in the CGI
control file using the [#setenv|<tt>setenv:</tt>] statement.







The repolist-generated page recurses into subdirectories and will list
all <tt>*.fossil</tt> files found, with the following exceptions:

   *  Filenames starting with a period are treated as "hidden" and skipped.

   *  Subdirectory names which match the base name of a fossil file in







>
>
>
>
>
>







77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
[/help?cmd=repolist-skin|repolist-skin] setting.

If no repository has such a non-zero repolist-skin setting, then
the repository list is generic HTML without any decoration, with
the page title taken from the <tt>FOSSIL_REPOLIST_TITLE</tt>
environment variable. The variable can be defined in the CGI
control file using the [#setenv|<tt>setenv:</tt>] statement.

The "Project Description" and "Login-Group" columns on the repolist page
are optional.  They are hidden by default.  Show them by putting value "1"
in the global settings "show-repolist-desc" and "show-repolist-lg", or
by setting the <tt>FOSSIL_REPOLIST_SHOW</tt> environment variable to
a string that contains substrings "description" and/or "login-group".

The repolist-generated page recurses into subdirectories and will list
all <tt>*.fossil</tt> files found, with the following exceptions:

   *  Filenames starting with a period are treated as "hidden" and skipped.

   *  Subdirectory names which match the base name of a fossil file in
Changes to www/changes.wiki.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28



29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51

52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
<title>Change Log</title>

<h2 id='v2_26'>Changes for version 2.26 (pending)</h2>

  *  Enhancements to [/help?cmd=diff|fossil diff] and similar:
     <ol type="a">
     <li> The --from can optionally accepts a directory name as its argument,
          and uses files under that directory as the baseline for the diff.
     <li> For "gdiff", if no [/help?cmd=gdiff-command|gdiff-command setting]
          is defined, Fossil tries to do a --tk diff if "tclsh" and "wish"
          are available, or a --by diff if not.
     <li> The "Reload" button is added to --tk diffs, to bring the displayed
          diff up to date with the latest changes on disk.
     <li> Add the "Hide diffs/Show diffs" toggle to web-UI diff pages that show
          diffs of multiple files.
     </ol>
  *  Added the [/help?cmd=/ckout|/ckout web page] to provide information
     about pending changes in a working check-out
  *  Enhancements to the [/help?cmd=ui|fossil ui] command:
     <ol type="a">
     <li> Defaults to using the new [/help?cmd=/ckout|/ckout page] as its
          start page.  Or, if the new "--from PATH" option is present, the
          default start page becomes "/ckout?exbase=PATH".
     <li> The new "--extpage FILENAME" option opens the named file as if it
          where in a [./serverext.wiki|CGI extension].  Example usage: the
          person editing this change log has 
          "fossil ui --extpage www/changes.wiki" running and hence can
          press "Reload" on the web browser to view edits.



     </ol>
  *  Enhancements to [/help?cmd=merge|fossil merge]:
     <ol type="a">
     <li> Added the [/help?cmd=merge-info|fossil merge-info] command and
          especially the --tk option to that command, to provide analysis
          of the most recent merge or update operation.
     <li> When a merge conflict occurs, a new section is added to the conflict
          text that shows Fossil's suggested resolution to the conflict.
     </ol>
  *  Enhancements to [/help?cmd=commit|fossil commit]:
     <ol type="a">
     <li> If Fossil sees potential formatting mistakes (ex: bad hyperlinks)
          in the check-in comment, it will alert the developer and give
          him or her the opportunity to edit the comment before continuing.
          This feature is controllable by the
          [/help?cmd=verify-comments|verify-comments setting].
     <li> The new "--if-changes" option causes the commit to become
          a quiet no-op if there are no pending changes.
     <li> Added the ability to sign check-ins with SSH keys.
     <li> Issue a warning if a user tries to commit on a check-in where the
          branch has been changed.
     <li> The interactive checkin comment prompt shows the formatting rules
          set for that repository.

     </ol>
  *  Deprecate the --comfmtflags and --comment-format global options and
     no longer list them in the built-in help, but keep them working for
     backwards compatibility.
     Alternative TTY comment formatting can still be specified using the
     [/help?cmd=comment-format|comment-format setting], if desired.  The
     default comment format is now called "canonical", not "legacy".
  *  Enhancements to the [/help?cmd=/timeline|/timeline page]:
     <ol type="a">
     <li> Added the "ml=" ("Merge-in List") query parameter that works
          like "rl=" ("Related List") but adds "mionly" style related
          check-ins instead of the full "rel" style.
     <li> For "tl=", "rl=", and "ml=", the order of the branches in the
          graph now tries to match the order of the branches named in
          the list.


|
<
|

|









|

|






|


>
>
>

|







|













>

|





|







1
2
3

4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
<title>Change Log</title>

<h2 id='v2_26'>Changes for version 2.26 (pending)</h2><ol>

 <li>Enhancements to [/help?cmd=diff|fossil diff] and similar:
     <ol type="a">
     <li> The --from can optionally accept a directory name as its argument,
          and uses files under that directory as the baseline for the diff.
     <li> For "gdiff", if no [/help?cmd=gdiff-command|gdiff-command setting]
          is defined, Fossil tries to do a --tk diff if "tclsh" and "wish"
          are available, or a --by diff if not.
     <li> The "Reload" button is added to --tk diffs, to bring the displayed
          diff up to date with the latest changes on disk.
     <li> Add the "Hide diffs/Show diffs" toggle to web-UI diff pages that show
          diffs of multiple files.
     </ol>
 <li>Added the [/help?cmd=/ckout|/ckout web page] to provide information
     about pending changes in a working check-out
 <li>Enhancements to the [/help?cmd=ui|fossil ui] command:
     <ol type="a">
     <li> Defaults to using the new [/help?cmd=/ckout|/ckout page] as its
          start page.  Or, if the new "--from PATH" option is present, the
          default start page becomes "/ckout?exbase=PATH".
     <li> The new "--extpage FILENAME" option opens the named file as if it
          where in a [./serverext.wiki|CGI extension].  Example usage: the
          person editing this change log has
          "fossil ui --extpage www/changes.wiki" running and hence can
          press "Reload" on the web browser to view edits.
     <li> Accept both IPv4 and IPv6 connections on all platforms, including
          Windows and OpenBSD.  This also applies to the "fossil server"
          command.
     </ol>
 <li>Enhancements to [/help?cmd=merge|fossil merge]:
     <ol type="a">
     <li> Added the [/help?cmd=merge-info|fossil merge-info] command and
          especially the --tk option to that command, to provide analysis
          of the most recent merge or update operation.
     <li> When a merge conflict occurs, a new section is added to the conflict
          text that shows Fossil's suggested resolution to the conflict.
     </ol>
 <li>Enhancements to [/help?cmd=commit|fossil commit]:
     <ol type="a">
     <li> If Fossil sees potential formatting mistakes (ex: bad hyperlinks)
          in the check-in comment, it will alert the developer and give
          him or her the opportunity to edit the comment before continuing.
          This feature is controllable by the
          [/help?cmd=verify-comments|verify-comments setting].
     <li> The new "--if-changes" option causes the commit to become
          a quiet no-op if there are no pending changes.
     <li> Added the ability to sign check-ins with SSH keys.
     <li> Issue a warning if a user tries to commit on a check-in where the
          branch has been changed.
     <li> The interactive checkin comment prompt shows the formatting rules
          set for that repository.
     <li> Add the "--editor" option.
     </ol>
 <li>Deprecate the --comfmtflags and --comment-format global options and
     no longer list them in the built-in help, but keep them working for
     backwards compatibility.
     Alternative TTY comment formatting can still be specified using the
     [/help?cmd=comment-format|comment-format setting], if desired.  The
     default comment format is now called "canonical", not "legacy".
 <li>Enhancements to the [/help?cmd=/timeline|/timeline page]:
     <ol type="a">
     <li> Added the "ml=" ("Merge-in List") query parameter that works
          like "rl=" ("Related List") but adds "mionly" style related
          check-ins instead of the full "rel" style.
     <li> For "tl=", "rl=", and "ml=", the order of the branches in the
          graph now tries to match the order of the branches named in
          the list.
82
83
84
85
86
87
88




89



90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113


114
115








116
117
118
119
120











121
122
123
124
125
126
127
128
129
          "ymd" and "yw" query parameters.
     <li> The new "min" query parameter, when added to a from=,to= query,
          collapses long runs of check-ins on the same branch into just
          end-points.
     <li> The p= and d= parameters an reference different check-ins, which
          case the timeline shows those check-ins that are both ancestors
          of p= and descendants of d=.




     </ol>



  *  Added the [/help?cmd=/clusterlist|/clusterlist page] for analysis
     and debugging
  *  Added the "artifact_to_json(NAME)" SQL function that returns a JSON
     decoding of the artifact described by NAME.
  *  Improvements to the [/help?cmd=patch|fossil patch] command:
     <ol type="a">
     <li> Fix a bug in "fossil patch create" that causes
          [/help?cmd=revert|fossil revert] operations that happened
          on individualfiles after a [/help?cmd=merge|fossil merge]
          to be omitted from the patch.
     <li>  Added the [/help?cmd=patch|patch alias] command for managing
           aliases for remote checkout names.
     </ol>
  *  Enhancements to on-line help and the [/help?cmd=help|fossil help] command:
     <ol type="a">
     <li> Add the ability to search the help text, either in the UI
          (on the [/help?cmd=/search|/search page]) or from the command-line
          (using the "[/help?cmd=search|fossil search -h PATTERN]" command.)
     <li> Accepts an optional SUBCOMMAND argument following the
          COMMAND argument and only shows results for the specified
          subcommand, not the entire command.
     <li> The -u (--usage) option shows only the command-line syntax
     <li> The -o (--options) option shows only the command-line options
     </ol>


  *  Added the ability to attach wiki pages to a ticket for extended
     descriptions.








  *  Added the "hash" query parameter to the
     [/help?cmd=/whatis|/whatis webpage].
  *  Add a "user elevation" [/doc/trunk/www/alerts.md|subscription]
     which alerts subscribers when an admin creates a new user or
     adds new permissions to one.











  *  Diverse minor fixes and additions.


<h2 id='v2_25'>Changes for version 2.25 (2024-11-06)</h2>

  *  The "[/help?cmd=ui|fossil ui /]" command now works even for repositories
     that have non-ASCII filenames
  *  Add the [/help?cmd=tree|fossil tree] command.
  *  On case-insensitive filesystems, store files using the filesystem's







>
>
>
>

>
>
>
|

|

|








|










>
>
|
|
>
>
>
>
>
>
>
>
|

|

|
>
>
>
>
>
>
>
>
>
>
>
|
|







85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
          "ymd" and "yw" query parameters.
     <li> The new "min" query parameter, when added to a from=,to= query,
          collapses long runs of check-ins on the same branch into just
          end-points.
     <li> The p= and d= parameters an reference different check-ins, which
          case the timeline shows those check-ins that are both ancestors
          of p= and descendants of d=.
     <li> The saturation and intensity of user-specified checkin and branch
          background colors are automatically adjusted to keep the colors
          compatible with the current skin, unless the
          [/help?cmd=raw-bgcolor|raw-bgcolor setting] is turned on.
     </ol>
 <li>The [/help?cmd=/docfile|/docfile webpage] was added.  It works like
     /doc but keeps the title of markdown documents with the document rather
     that moving it up to the page title.
 <li>Added the [/help?cmd=/clusterlist|/clusterlist page] for analysis
     and debugging
 <li>Added the "artifact_to_json(NAME)" SQL function that returns a JSON
     decoding of the artifact described by NAME.
 <li>Improvements to the [/help?cmd=patch|fossil patch] command:
     <ol type="a">
     <li> Fix a bug in "fossil patch create" that causes
          [/help?cmd=revert|fossil revert] operations that happened
          on individualfiles after a [/help?cmd=merge|fossil merge]
          to be omitted from the patch.
     <li>  Added the [/help?cmd=patch|patch alias] command for managing
           aliases for remote checkout names.
     </ol>
 <li>Enhancements to on-line help and the [/help?cmd=help|fossil help] command:
     <ol type="a">
     <li> Add the ability to search the help text, either in the UI
          (on the [/help?cmd=/search|/search page]) or from the command-line
          (using the "[/help?cmd=search|fossil search -h PATTERN]" command.)
     <li> Accepts an optional SUBCOMMAND argument following the
          COMMAND argument and only shows results for the specified
          subcommand, not the entire command.
     <li> The -u (--usage) option shows only the command-line syntax
     <li> The -o (--options) option shows only the command-line options
     </ol>
 <li>Enhancements to the ticket system:
     <ol type="a">
     <li> Added the ability to attach wiki pages to a ticket for extended
          descriptions.
     <li> Added submenu to the 'View Ticket' page, to use it as
          template for a new ticket.
     <li> Added button 'Submit and New' to create multiple tickets
          in a row.
     <li> Link the version field in ticket view to a matching checkin or tag.
     <li> Show creation time in report and ticket view.
     <li> Show previous comments in edit ticket as reference.
     </ol>
 <li>Added the "hash" query parameter to the
     [/help?cmd=/whatis|/whatis webpage].
 <li>Add a "user permissions changes" [/doc/trunk/www/alerts.md|subscription]
     which alerts subscribers when an admin creates a new user or
     when a user's permissions change.
 <li>Show project description on repository list.
 <li>Make [/help?cmd=/chat|/chat] better-behaved during server outages, reducing
     the frequency of reconnection attempts over time and providing feedback
     to the user when the connection is down.
 <li>The [/doc/trunk/www/th1.md|TH1 script language] now makes a distinction
     between [/doc/trunk/www/th1.md#taint|tainted and untainted string values].
     This is a security enhancement that makes it more difficult to write
     custom TH1 scripts that contain XSS or SQL-injection bugs.  As part of
     this enhancement, the [/help?cmd=vuln-report|vuln-report] setting was
     added to control what Fossil does when it encounters a potential TH1
     security problem.
 <li>Diverse minor fixes and additions.
</ol>

<h2 id='v2_25'>Changes for version 2.25 (2024-11-06)</h2>

  *  The "[/help?cmd=ui|fossil ui /]" command now works even for repositories
     that have non-ASCII filenames
  *  Add the [/help?cmd=tree|fossil tree] command.
  *  On case-insensitive filesystems, store files using the filesystem's
Changes to www/chat.md.
162
163
164
165
166
167
168


















169
170
171
172
173
174
175
in a new timeline entry is announced in the chatroom.  The announcement
appears to come from a user whose name is given by the chat-timeline-user
setting.

This mechanism is similar to [email notification](./alerts.md) except that
the notification is sent via chat instead of via email.




















## Implementation Details

*You do not need to understand how Fossil chat works in order to use it.
But many developers prefer to know how their tools work.
This section is provided for the benefit of those curious developers.*








>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
in a new timeline entry is announced in the chatroom.  The announcement
appears to come from a user whose name is given by the chat-timeline-user
setting.

This mechanism is similar to [email notification](./alerts.md) except that
the notification is sent via chat instead of via email.

## Quirks

  - There is no message-editing capability. This is by design and
    desire of `/chat`'s developers.

  - When `/chat` has problems connecting to the message poller (see
    the next section) it will provide a subtle visual indicator of the
    connection problem - a dotted line along the top of the input
    field.  If the connection recovers within a short time, that
    indicator will go away, otherwise it will pop up a loud message
    signifying that the connection to the poller is down and how long
    it will wait to retry (progressively longer, up to some
    maximum).  `/chat` will recover automatically when the server is
    reachable.  Trying to send messages while the poller connection is
    down is permitted, and the poller will attempt to recover
    immediately if sending of a message succeeds. That applies to any
    operations which send traffic, e.g. if the per-message "toggle
    text mode" button is activated or a message is globally deleted.

## Implementation Details

*You do not need to understand how Fossil chat works in order to use it.
But many developers prefer to know how their tools work.
This section is provided for the benefit of those curious developers.*

Changes to www/env-opts.md.
152
153
154
155
156
157
158
159
160
161

162
163
164
165
166
167
168
for additional information.

`FOSSIL_REPOLIST_TITLE`: The page title of the "Repository List" page
loaded by the `fossil all ui` or `fossil ui /` commands. Only used if
none of the listed repositories has the `repolist_skin` property set.
Can be set from the [CGI control file][cgictlfile].

`FOSSIL_REPOLIST_QUICKFILTER`: Enable or disable the quickfilter on
repository listings, which allows for simple filtering of the listed
repositories.

Can be set from the [CGI control file][cgictlfile].

`FOSSIL_USE_SEE_TEXTKEY`: If set, treat the encryption key string for
SEE as text to be hashed into the actual encryption key.  This has no
effect if Fossil was not compiled with SEE support enabled.









|
|
|
>







152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
for additional information.

`FOSSIL_REPOLIST_TITLE`: The page title of the "Repository List" page
loaded by the `fossil all ui` or `fossil ui /` commands. Only used if
none of the listed repositories has the `repolist_skin` property set.
Can be set from the [CGI control file][cgictlfile].

`FOSSIL_REPOLIST_SHOW`: If this variable exists and has a text value
that contains the substring "description", then the "Project Description"
column appears on the repolist page.  If it contains the substring
"login-group", then the Login-Group column appears on the repolist page.
Can be set from the [CGI control file][cgictlfile].

`FOSSIL_USE_SEE_TEXTKEY`: If set, treat the encryption key string for
SEE as text to be hashed into the actual encryption key.  This has no
effect if Fossil was not compiled with SEE support enabled.


Changes to www/loadmgmt.md.
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96

    chroot_jail_proc /home/www/proc proc ro 0 0

The `/home/www/proc` pathname should be adjusted so that the `/proc`
component is at the root of the chroot jail, of course.

To see if the load-average limiter is functional, visit the
[`/test_env`][hte] page of the server to view the current load average.
If the value for the load average is greater than zero, that means that
it is possible to activate the load-average limiter on that repository.
If the load average shows exactly "0.0", then that means that Fossil is
unable to find the load average. This can either be because it is in a
`chroot(2)` jail without `/proc` access, or because it is running on a
system that does not support `getloadavg()` and so the load-average
limiter will not function.


[503]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.5.4
[hte]: /help?cmd=/test_env
[gla]: https://linux.die.net/man/3/getloadavg
[lin]: http://www.linode.com
[sh]:  ./selfhost.wiki







|










|



75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96

    chroot_jail_proc /home/www/proc proc ro 0 0

The `/home/www/proc` pathname should be adjusted so that the `/proc`
component is at the root of the chroot jail, of course.

To see if the load-average limiter is functional, visit the
[`/test-env`][hte] page of the server to view the current load average.
If the value for the load average is greater than zero, that means that
it is possible to activate the load-average limiter on that repository.
If the load average shows exactly "0.0", then that means that Fossil is
unable to find the load average. This can either be because it is in a
`chroot(2)` jail without `/proc` access, or because it is running on a
system that does not support `getloadavg()` and so the load-average
limiter will not function.


[503]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.5.4
[hte]: /help?cmd=/test-env
[gla]: https://linux.die.net/man/3/getloadavg
[lin]: http://www.linode.com
[sh]:  ./selfhost.wiki
Changes to www/quickstart.wiki.
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54


























55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
or <a href="build.wiki">compile it yourself</a> from sources.
Install Fossil by putting the fossil binary
someplace on your $PATH.

You can test that Fossil is present and working like this:

<pre><b>fossil version
This is fossil version 2.13 [309af345ab] 2020-09-28 04:02:55 UTC
</b></pre>

<h2 id="workflow" name="fslclone">General Work Flow</h2>

Fossil works with repository files (a database in a single file with the project's
complete history) and with checked-out local trees (the working directory
you use to do your work). 
(See [./glossary.md | the glossary] for more background.)
The workflow looks like this:

<ul>
    <li>Create or clone a repository file.  ([/help/init|fossil init] or
        [/help/clone | fossil clone])
    <li>Check out a local tree.  ([/help/open | fossil open])
    <li>Perform operations on the repository (including repository
        configuration).
</ul>

Fossil can be entirely driven from the command line. Many features
can also be conveniently accessed from the built-in web user interface.

The following sections give a brief overview of these
operations.

<h2 id="new">Starting A New Project</h2>

To start a new project with fossil create a new empty repository
this way: ([/help/init | more info])

<pre><b>fossil init</b> <i>repository-filename</i>
</pre>

You can name the database anything you like, and you can place it anywhere in the filesystem.
The <tt>.fossil</tt> extension is traditional but only required if you are going to use the 
<tt>[/help/server | fossil server DIRECTORY]</tt> feature.



























<h2 id="clone">Cloning An Existing Repository</h2>

Most fossil operations interact with a repository that is on the
local disk drive, not on a remote system.  Hence, before accessing
a remote repository it is necessary to make a local copy of that
repository.  Making a local copy of a remote repository is called
"cloning".

Clone a remote repository as follows: ([/help/clone | more info])

<pre><b>fossil clone</b> <i>URL repository-filename</i>
</pre>

The <i>URL</i> specifies the fossil repository
you want to clone.  The <i>repository-filename</i> is the new local
filename into which the cloned repository will be written.  For







|




|
<
<
|
|

















|
<





|
|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>






|
|

|







12
13
14
15
16
17
18
19
20
21
22
23
24


25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
or <a href="build.wiki">compile it yourself</a> from sources.
Install Fossil by putting the fossil binary
someplace on your $PATH.

You can test that Fossil is present and working like this:

<pre><b>fossil version
This is fossil version 2.25 [8f798279d5] 2024-11-06 12:59:09 UTC
</b></pre>

<h2 id="workflow" name="fslclone">General Work Flow</h2>

Fossil works with [./glossary.md#repository | repository files]


and [./glossary.md#check-out | check-out directories] using a
workflow like this:

<ul>
    <li>Create or clone a repository file.  ([/help/init|fossil init] or
        [/help/clone | fossil clone])
    <li>Check out a local tree.  ([/help/open | fossil open])
    <li>Perform operations on the repository (including repository
        configuration).
</ul>

Fossil can be entirely driven from the command line. Many features
can also be conveniently accessed from the built-in web user interface.

The following sections give a brief overview of these
operations.

<h2 id="new">Starting A New Project</h2>

To start a new project with Fossil, [/help/init | create a new empty repository]:


<pre><b>fossil init</b> <i>repository-filename</i>
</pre>

You can name the database anything you like, and you can place it anywhere in the filesystem.
The <tt>.fossil</tt> extension is traditional, but it is only required if you are going to use the 
<tt>[/help/server | fossil server DIRECTORY]</tt> feature.

Next, do something along the lines of:

<pre>
<b>mkdir -p ~/src/project/trunk</b>
<b>cd ~/src/project/trunk</b>
<b>fossil open</b> <i>repository-filename</i>
<b>fossil add</b> foo.c bar.h qux.md
<b>fossil commit</b>
</pre>

If your project directory already exists, obviating the <b>mkdir</b>
step, you will instead need to add the <tt>--force</tt> flag to the
<b>open</b> command to authorize Fossil to open the repo into a
non-empty checkout directory. (This is to avoid accidental opens into,
for example, your home directory.)

The convention of naming your checkout directory after a long-lived
branch name like "trunk" is in support of Fossil's ability to have as
many open checkouts as you like. This author frequently has additional
checkout directories named <tt>../release</tt>, <tt>../scratch</tt>,
etc. The release directory is open to the branch of the same name, while
the scratch directory is used when disturbing one of the other
long-lived checkout directories is undesireable, as when performing a
[/help/bisect | bisect] operation.


<h2 id="clone">Cloning An Existing Repository</h2>

Most fossil operations interact with a repository that is on the
local disk drive, not on a remote system.  Hence, before accessing
a remote repository it is necessary to make a local copy of that
repository, a process called
"[/help/clone | cloning]".

This is done as follows:

<pre><b>fossil clone</b> <i>URL repository-filename</i>
</pre>

The <i>URL</i> specifies the fossil repository
you want to clone.  The <i>repository-filename</i> is the new local
filename into which the cloned repository will be written.  For
79
80
81
82
83
84
85
86
87








88
89
90
91
92
93
94
Clone done, sent: 2424  received: 42965725  ip: 10.10.10.0
Rebuilding repository meta-data...
100% complete...
Extra delta compression... 
Vacuuming the database... 
project-id: 94259BB9F186226D80E49D1FA2DB29F935CCA0333
server-id:  016595e9043054038a9ea9bc526d7f33f7ac0e42
admin-user: exampleuser (password is "yoWgDR42iv")>
</b></pre>









If the remote repository requires a login, include a
userid in the URL like this:

<pre><b>fossil clone https://</b><i>remoteuserid</i><b>@www.example.org/ myclone.fossil</b></pre>

You will be prompted separately for the password.







|

>
>
>
>
>
>
>
>







102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
Clone done, sent: 2424  received: 42965725  ip: 10.10.10.0
Rebuilding repository meta-data...
100% complete...
Extra delta compression... 
Vacuuming the database... 
project-id: 94259BB9F186226D80E49D1FA2DB29F935CCA0333
server-id:  016595e9043054038a9ea9bc526d7f33f7ac0e42
admin-user: exampleuser (intial remote-access password is "yoWgDR42iv")>
</b></pre>

This <i>exampleuser</i> will be used by Fossil as the author of commits when
you checkin changes to the repository.  It is also used by Fossil when you
make your repository available to others using the built-in server mode by
running <tt>[/help/server | fossil server]</tt> and will also be used when
running <tt>[/help/ui | fossil ui]</tt> to view the repository through 
the Fossil UI.  See the quick start topic for setting up a
<a href="#server">server</a> for more details.

If the remote repository requires a login, include a
userid in the URL like this:

<pre><b>fossil clone https://</b><i>remoteuserid</i><b>@www.example.org/ myclone.fossil</b></pre>

You will be prompted separately for the password.
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
[https://www.mercurial-scm.org/|Mercurial].
Fossil can also import [https://subversion.apache.org/|Subversion projects] directly.

<h2 id="checkout">Checking Out A Local Tree</h2>

To work on a project in fossil, you need to check out a local
copy of the source tree.  Create the directory you want to be
the root of your tree and cd into that directory.  Then
do this: ([/help/open | more info])

<pre><b>fossil open</b> <i>repository-filename</i></pre>

for example:

<pre><b>fossil open ../myclone.fossil
    BUILD.txt
    COPYRIGHT-BSD2.txt
    README.md
      ︙
</tt></b></pre>

(or "fossil open ..\myclone.fossil" on Windows).

This leaves you with the newest version of the tree
checked out.
From anywhere underneath the root of your local tree, you
can type commands like the following to find out the status of
your local tree:

<pre>







|
<



|








<
<







156
157
158
159
160
161
162
163

164
165
166
167
168
169
170
171
172
173
174
175


176
177
178
179
180
181
182
[https://www.mercurial-scm.org/|Mercurial].
Fossil can also import [https://subversion.apache.org/|Subversion projects] directly.

<h2 id="checkout">Checking Out A Local Tree</h2>

To work on a project in fossil, you need to check out a local
copy of the source tree.  Create the directory you want to be
the root of your tree, <tt>cd</tt> into that directory, and then:


<pre><b>fossil open</b> <i>repository-filename</i></pre>

For example:

<pre><b>fossil open ../myclone.fossil
    BUILD.txt
    COPYRIGHT-BSD2.txt
    README.md
      ︙
</tt></b></pre>



This leaves you with the newest version of the tree
checked out.
From anywhere underneath the root of your local tree, you
can type commands like the following to find out the status of
your local tree:

<pre>
292
293
294
295
296
297
298
299
300
301
302
303
304
305

306
307
308
309
310
311
312
313

314
315
316
317
318
319
320
321
322
323




324






325








326
327
328
329



330
331
332
333
334
335
336
the first checkin, and the default for any time a branch name is needed but not
specified.

This will get you started on identifying checkins. The
<a href="./checkin_names.wiki">Checkin Names document</a> is a complete reference, including
how timestamps can also be used.

<h2 id="config">Configuring Your Local Repository</h2>

When you create a new repository, either by cloning an existing
project or create a new project of your own, you usually want to do some
local configuration.  This is easily accomplished using the web-server
that is built into fossil.  Start the fossil web server like this:
([/help/ui | more info])


<pre>
<b>fossil ui</b> <i>repository-filename</i>
</pre>

You can omit the <i>repository-filename</i> from the command above
if you are inside a checked-out local tree.


This starts a web server then automatically launches your
web browser and makes it point to this web server.  If your system
has an unusual configuration, fossil might not be able to figure out
how to start your web browser.  In that case, first tell fossil
where to find your web browser using a command like this:

<pre>
<b>fossil setting web-browser</b> <i>path-to-web-browser</i>
</pre>





By default, fossil does not require a login for HTTP connections






coming in from the IP loopback address 127.0.0.1.  You can, and perhaps








should, change this after you create a few users.

When you are finished configuring, just press Control-C or use
the <b>kill</b> command to shut down the mini-server.




<h2 id="sharing">Sharing Changes</h2>

When [./concepts.wiki#workflow|autosync] is turned off,
the changes you [/help/commit | commit] are only
on your local repository.
To share those changes with other repositories, do:







|

|
<
|
<
<
>





|


>
|
<
|
|
|





>
>
>
>
|
>
>
>
>
>
>
|
>
>
>
>
>
>
>
>
|

|
|
>
>
>







320
321
322
323
324
325
326
327
328
329

330


331
332
333
334
335
336
337
338
339
340
341

342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
the first checkin, and the default for any time a branch name is needed but not
specified.

This will get you started on identifying checkins. The
<a href="./checkin_names.wiki">Checkin Names document</a> is a complete reference, including
how timestamps can also be used.

<h2 id="config">Accessing Your Local Repository's Web User Interface</h2>

After you create a new repository, you usually want to do some local

configuration.  This is most easily accomplished by firing up the Fossil


UI:

<pre>
<b>fossil ui</b> <i>repository-filename</i>
</pre>

You can shorten that to just [/help/ui | <b>fossil ui</b>]
if you are inside a checked-out local tree.

This command starts an internal web server, after which Fossil
automatically launches your default browser, pointed at itself,

presenting a special view of the repository, its web user interface.

You may override Fossil's logic for selecting the default browser so:

<pre>
<b>fossil setting web-browser</b> <i>path-to-web-browser</i>
</pre>

When launched this way, Fossil binds its internal web server to the IP
loopback address, 127.0.0.1, which it treats specially, bypassing all
user controls, effectively giving visitors the
[./caps/admin-v-setup.md#apsu | all-powerful Setup capabliity].

Why is that a good idea, you ask? Because it is a safe
presumption that only someone with direct file access to the repository
database file could be using the resulting web interface. Anyone who can
modify the repo DB directly could give themselves any and all access
with a SQL query, or even by direct file manipulation; no amount of
access control matters to such a user.

(Contrast the [#server | many <i>other</i> ways] of setting Fossil up
as an HTTP server, where the repo DB is on the other side of the HTTP
server wall, inaccessible by all means other than Fossil's own
mediation. For this reason, the "localhost bypasses access control"
policy does <i>not</i> apply to these other interfaces. That is a very
good thing, since without this difference in policy, it would be unsafe
to bind a [/help?cmd=server | <b>fossil server</b>] instance to
localhost on a high-numbered port and then reverse-proxy it out to the
world via HTTPS, a practice this author does engage in, with confidence.)

Once you are finished configuring Fossil, you may safely Control-C out
of the <b>fossil&nbsp;ui</b> command to shut down this privileged
built-in web server. Moreover, you may by grace of SQLite do this <i>at
any time</i>: all changes are either committed durably to the repo DB or
rolled back, in their totality. This includes configuration changes.

<h2 id="sharing">Sharing Changes</h2>

When [./concepts.wiki#workflow|autosync] is turned off,
the changes you [/help/commit | commit] are only
on your local repository.
To share those changes with other repositories, do:
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
Is similar to update except that it does not honor the autosync
setting, nor does it merge in local changes - it prefers to overwrite
them and fails if local changes exist unless the <tt>--force</tt>
flag is used.

<h2 id="branch" name="merge">Branching And Merging</h2>

Use the --branch option to the [/help/commit | commit] command
to start a new branch.  Note that in Fossil, branches are normally
created when you commit, not before you start editing.  You can
use the [/help/branch | branch new] command to create a new branch
before you start editing, if you want, but most people just wait
until they are ready to commit.

To merge two branches back together, first
[/help/update | update] to the branch you want to merge into.
Then do a [/help/merge|merge] of the other branch that you want to incorporate
the changes from.  For example, to merge "featureX" changes into "trunk"
do this:








|
|
<
<
<
<







429
430
431
432
433
434
435
436
437




438
439
440
441
442
443
444
Is similar to update except that it does not honor the autosync
setting, nor does it merge in local changes - it prefers to overwrite
them and fails if local changes exist unless the <tt>--force</tt>
flag is used.

<h2 id="branch" name="merge">Branching And Merging</h2>

Use the --branch option to the [/help/commit | commit] command to start
a new branch at the point of need. ([./gitusers.md#bneed | Contrast git].)





To merge two branches back together, first
[/help/update | update] to the branch you want to merge into.
Then do a [/help/merge|merge] of the other branch that you want to incorporate
the changes from.  For example, to merge "featureX" changes into "trunk"
do this:

440
441
442
443
444
445
446

447
448
449
450
451
452
453
454

455
456
457
458
459
460
461
462
463
464


465
466
467
468
469
470
471
472
473
474
475
476
477
478
479

480
481

482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
mistake.  Undo and redo only work for changes that have
not yet been checked in using commit and there is only a single
level of undo/redo.


<h2 id="server">Setting Up A Server</h2>


Fossil can act as a stand-alone web server using one of these
commands:

<pre>
<b>[/help/server | fossil server]</b> <i>repository-filename</i>
<b>[/help/ui | fossil ui]</b> <i>repository-filename</i>
</pre>


The <i>repository-filename</i> can be omitted when these commands
are run from within an open check-out, which is a particularly useful
shortcut with the <b>fossil ui</b> command.

The <b>ui</b> command is intended for accessing the web user interface
from a local desktop. (We sometimes call this mode "Fossil UI.")
The <b>ui</b> command differs from the
<b>server</b> command by binding to the loopback IP
address only (thus making the web UI visible only on the
local machine) and by automatically starting your default web browser,


pointing it at the running UI
server. The localhost restriction exists because it also gives anyone
who can access the resulting web UI full control over the
repository. (This is the [./caps/admin-v-setup.md#apsu | all-powerful
Setup capabliity].)

For cross-machine collaboration, use the <b>server</b> command instead,
which binds on all IP addresses, does not try to start a web browser,
and enforces [./caps/ | Fossil's role-based access control system].

Servers are also easily configured as:

<ul>
<li>[./server/any/inetd.md|inetd]
<li>[./server/debian/service.md|systemd]

<li>[./server/any/cgi.md|CGI]
<li>[./server/any/scgi.md|SCGI]

</ul>

…along with [./server/#matrix | several other options].

The [./selfhost.wiki | self-hosting fossil repositories] use
CGI.

You might <i>need</i> to set up a server, whether you know it yet or
not.  See the [./server/whyuseaserver.wiki | Benefits of a Fossil Server]
article for details.

<h2 id="proxy">HTTP Proxies</h2>

If you are behind a restrictive firewall that requires you to use
an HTTP proxy to reach the internet, then you can configure the proxy
in three different ways.  You can tell fossil about your proxy using
a command-line option on commands that use the network,







>
|
<



<


>
|
|
<

<
<
|
<
<
|
>
>
|
<
<
|
<

<
<
<
|
|


|
<
>
|
|
>




<
<
|
|
<
|







483
484
485
486
487
488
489
490
491

492
493
494

495
496
497
498
499

500


501


502
503
504
505


506

507



508
509
510
511
512

513
514
515
516
517
518
519
520


521
522

523
524
525
526
527
528
529
530
mistake.  Undo and redo only work for changes that have
not yet been checked in using commit and there is only a single
level of undo/redo.


<h2 id="server">Setting Up A Server</h2>

In addition to the inward-facing <b>fossil ui</b> mode covered [#config
| above], Fossil can also act as an outward-facing web server:


<pre>
<b>[/help/server | fossil server]</b> <i>repository-filename</i>

</pre>

Just as with <b>fossil ui</b>, you may omit the
<i>repository-filename</i> parameter when running this from within an open
check-out.




<i>Unlike</i> <b>fossil ui</b> mode, Fossil binds to all network


interfaces by default in this mode, and it enforces the configured
[./caps/ | role-based access controls]. Further, because it is meant to
provide external web service, it doesn't try to launch a local web
browser pointing to a "Fossil UI" presentation; external visitors see


your repository's configured home page instead.





To serve varying needs, there are additional ways to serve a Fossil repo
to external users:

<ul>
<li>[./server/any/cgi.md|CGI], as used by Fossil's [./selfhost.wiki |

    self-hosting repositories]
<li>[./server/any/scgi.md|SCGI]
<li>[./server/any/inetd.md|inetd]
<li>[./server/debian/service.md|systemd]
</ul>

…along with [./server/#matrix | several other options].



We recommend that you read the [./server/whyuseaserver.wiki | Benefits
of a Fossil Server] article, because you might <i>need</i> to do this

and not yet know it.

<h2 id="proxy">HTTP Proxies</h2>

If you are behind a restrictive firewall that requires you to use
an HTTP proxy to reach the internet, then you can configure the proxy
in three different ways.  You can tell fossil about your proxy using
a command-line option on commands that use the network,
Changes to www/server/debian/service.md.
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
restrictions on TCP ports in every OS where `systemd` runs, but you can
create a listener socket on a high-numbered (&ge; 1024) TCP port,
suitable for sharing a Fossil repo to a workgroup on a private LAN.

To do this, write the following in
`~/.local/share/systemd/user/fossil.service`:

```dosini
[Unit]
Description=Fossil user server
After=network-online.target

[Service]
WorkingDirectory=/home/fossil/museum
ExecStart=/home/fossil/bin/fossil server --port 9000 repo.fossil







|







50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
restrictions on TCP ports in every OS where `systemd` runs, but you can
create a listener socket on a high-numbered (&ge; 1024) TCP port,
suitable for sharing a Fossil repo to a workgroup on a private LAN.

To do this, write the following in
`~/.local/share/systemd/user/fossil.service`:

> ```dosini
[Unit]
Description=Fossil user server
After=network-online.target

[Service]
WorkingDirectory=/home/fossil/museum
ExecStart=/home/fossil/bin/fossil server --port 9000 repo.fossil
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
socket listener, which `systemd` calls “[socket activation][sa],”
roughly equivalent to [the ancient `inetd` method](../any/inetd.md).
It’s more complicated, but it has some nice properties.

We first need to define the privileged socket listener by writing
`/etc/systemd/system/fossil.socket`:

```dosini
[Unit]
Description=Fossil socket

[Socket]
Accept=yes
ListenStream=80
NoDelay=true







|







162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
socket listener, which `systemd` calls “[socket activation][sa],”
roughly equivalent to [the ancient `inetd` method](../any/inetd.md).
It’s more complicated, but it has some nice properties.

We first need to define the privileged socket listener by writing
`/etc/systemd/system/fossil.socket`:

> ```dosini
[Unit]
Description=Fossil socket

[Socket]
Accept=yes
ListenStream=80
NoDelay=true
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
This configuration says more or less the same thing as the socket part
of an `inetd` entry [exemplified elsewhere in this
documentation](../any/inetd.md).

Next, create the service definition file in that same directory as
`fossil@.service`:

```dosini
[Unit]
Description=Fossil socket server
After=network-online.target

[Service]
WorkingDirectory=/home/fossil/museum
ExecStart=/home/fossil/bin/fossil http repo.fossil







|







187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
This configuration says more or less the same thing as the socket part
of an `inetd` entry [exemplified elsewhere in this
documentation](../any/inetd.md).

Next, create the service definition file in that same directory as
`fossil@.service`:

> ```dosini
[Unit]
Description=Fossil socket server
After=network-online.target

[Service]
WorkingDirectory=/home/fossil/museum
ExecStart=/home/fossil/bin/fossil http repo.fossil
Changes to www/server/windows/service.md.
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
When the Fossil server will be used at times that files may be locked
during virus scanning, it is prudent to arrange that its directory used
for temporary files is exempted from such scanning. Ordinarily, this
will be a subdirectory named "fossil" in the temporary directory given
by the Windows GetTempPath(...) API, [namely](https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-gettemppathw#remarks)
the value of the first existing environment variable from `%TMP%`, `%TEMP%`,
`%USERPROFILE%`, and `%SystemRoot%`; you can look for their actual values in
your system by accessing the `/test_env` webpage. 
Excluding this subdirectory will avoid certain rare failures where the
fossil.exe process is unable to use the directory normally during a scan.

### <a id='PowerShell'></a>Advanced service installation using PowerShell

As great as `fossil winsrv` is, it does not have one to one reflection of all of
the `fossil server` [options](/help?cmd=server).  When you need to use some of







|







53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
When the Fossil server will be used at times that files may be locked
during virus scanning, it is prudent to arrange that its directory used
for temporary files is exempted from such scanning. Ordinarily, this
will be a subdirectory named "fossil" in the temporary directory given
by the Windows GetTempPath(...) API, [namely](https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-gettemppathw#remarks)
the value of the first existing environment variable from `%TMP%`, `%TEMP%`,
`%USERPROFILE%`, and `%SystemRoot%`; you can look for their actual values in
your system by accessing the `/test-env` webpage. 
Excluding this subdirectory will avoid certain rare failures where the
fossil.exe process is unable to use the directory normally during a scan.

### <a id='PowerShell'></a>Advanced service installation using PowerShell

As great as `fossil winsrv` is, it does not have one to one reflection of all of
the `fossil server` [options](/help?cmd=server).  When you need to use some of
Changes to www/serverext.wiki.
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
Do a web search for
"[https://duckduckgo.com/?q=cgi+environment_variables|cgi environment variables]"
to find more detail about what each of the above variables mean and how
they are used.
Live listings of the values of some or all of these environment variables
can be found at links like these:

  *  [https://fossil-scm.org/home/test_env]
  *  [https://sqlite.org/src/ext/checklist/top/env]

In addition to the standard CGI environment variables listed above, 
Fossil adds the following:

  *  FOSSIL_CAPABILITIES
  *  FOSSIL_NONCE







|







187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
Do a web search for
"[https://duckduckgo.com/?q=cgi+environment_variables|cgi environment variables]"
to find more detail about what each of the above variables mean and how
they are used.
Live listings of the values of some or all of these environment variables
can be found at links like these:

  *  [https://fossil-scm.org/home/test-env]
  *  [https://sqlite.org/src/ext/checklist/top/env]

In addition to the standard CGI environment variables listed above, 
Fossil adds the following:

  *  FOSSIL_CAPABILITIES
  *  FOSSIL_NONCE
Changes to www/th1.md.
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
of the command, is `if`.
The second token is `$current eq "dev"` - an expression.  (The outer {...}
are removed from each token by the command parser.)  The third token
is the `puts "hello"`, with its whitespace and newlines.  The fourth token
is `else` and the fifth and last token is `puts "world"`.

The `if` command evaluates its first argument (the second token)
as an expression, and if that expression is true, evaluates its
second argument (the third token) as a TH1 script.
If the expression is false and the third argument is `else`, then
the fourth argument is evaluated as a TH1 expression.

So, you see, even though the example above spans five lines, it is really
just a single command.








|







66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
of the command, is `if`.
The second token is `$current eq "dev"` - an expression.  (The outer {...}
are removed from each token by the command parser.)  The third token
is the `puts "hello"`, with its whitespace and newlines.  The fourth token
is `else` and the fifth and last token is `puts "world"`.

The `if` command evaluates its first argument (the second token)
as an expression, and if that expression is true, it evaluates its
second argument (the third token) as a TH1 script.
If the expression is false and the third argument is `else`, then
the fourth argument is evaluated as a TH1 expression.

So, you see, even though the example above spans five lines, it is really
just a single command.

104
105
106
107
108
109
110







































111
112
113
114
115
116
117
    return [lindex [regexp -line -inline -nocase -- \
        {^uuid:\s+([0-9A-F]{40}) } [eval [getFossilCommand \
        $repository "" info trunk]]] end]

Those backslashes allow the command to wrap nicely within a standard
terminal width while telling the interpreter to consider those three
lines as a single command.









































Summary of Core TH1 Commands
----------------------------

The original Tcl language (after which TH1 is modeled) has a very rich
repertoire of commands.  TH1, as it is designed to be minimalist and







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
    return [lindex [regexp -line -inline -nocase -- \
        {^uuid:\s+([0-9A-F]{40}) } [eval [getFossilCommand \
        $repository "" info trunk]]] end]

Those backslashes allow the command to wrap nicely within a standard
terminal width while telling the interpreter to consider those three
lines as a single command.

<a id="taint"></a>Tainted And Untainted Strings
-----------------------------------------------

Beginning with Fossil version 2.26 (circa 2025), TH1 distinguishes between
"tainted" and "untainted" strings.  Tainted strings are strings that are
derived from user inputs that might contain text that is designed to subvert
the script.  Untainted strings are known to come from secure sources and
are assumed to contain no malicious content.

Beginning with Fossil version 2.26, and depending on the value of the
[vuln-report setting](/help?cmd=vuln-report), TH1 will prevent tainted
strings from being used in ways that might lead to XSS or SQL-injection
attacks.  This feature helps to ensure that XSS and SQL-injection
vulnerabilities are not *accidentally* added to Fossil when
custom TH1 scripts for headers or footers or tickets are added to a
repository.  Note that the tainted/untainted distinction in strings does
not make it impossible to introduce XSS and SQL-injections vulnerabilities
using poorly-written TH1 scripts; it just makes it more difficult and
less likely to happen by accident.  Developers must still consider the
security implications TH1 customizations they add to Fossil, and take
appropriate precautions when writing custom TH1.  Peer review of TH1
script changes is encouraged.

In Fossil version 2.26, if the vuln-report setting is set to "block"
or "fatal", the [html](#html) and [query](#query) TH1 commands will
fail with an error if their argument is a tainted string.  This helps
to prevent XSS and SQL-injection attacks, respectively.  Note that
the default value of the vuln-report setting is "log", which allows those
commands to continue working and only writes a warning message into the
error log.  <b>Future versions of Fossil may change the default value
of the vuln-report setting to "block" or "fatal".</b>  Fossil users
with customized TH1 scripts are encouraged to audit their customizations
and fix any potential vulnerabilities soon, so as to avoid breakage
caused by future upgrades.  <b>Future versions of Fossil might also
place additional restrictions on the use of tainted strings.</b>
For example, it is likely that future versions of Fossil will disallow
using tainted strings as script, for example as the body of a "for"
loop or of a "proc".


Summary of Core TH1 Commands
----------------------------

The original Tcl language (after which TH1 is modeled) has a very rich
repertoire of commands.  TH1, as it is designed to be minimalist and
145
146
147
148
149
150
151



152
153
154
155
156
157
158
  *  string index STRING INDEX
  *  string is CLASS STRING
  *  string last NEEDLE HAYSTACK ?START-INDEX?
  *  string match PATTERN STRING
  *  string length STRING
  *  string range STRING FIRST LAST
  *  string repeat STRING COUNT



  *  unset VARNAME
  *  uplevel ?LEVEL? SCRIPT
  *  upvar ?FRAME? OTHERVAR MYVAR ?OTHERVAR MYVAR?

All of the above commands work as in the original Tcl.  Refer to the
<a href="https://www.tcl-lang.org/man/tcl/contents.htm">Tcl documentation</a>
for details.







>
>
>







184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
  *  string index STRING INDEX
  *  string is CLASS STRING
  *  string last NEEDLE HAYSTACK ?START-INDEX?
  *  string match PATTERN STRING
  *  string length STRING
  *  string range STRING FIRST LAST
  *  string repeat STRING COUNT
  *  string trim STRING
  *  string trimleft STRING
  *  string trimright STRING
  *  unset VARNAME
  *  uplevel ?LEVEL? SCRIPT
  *  upvar ?FRAME? OTHERVAR MYVAR ?OTHERVAR MYVAR?

All of the above commands work as in the original Tcl.  Refer to the
<a href="https://www.tcl-lang.org/man/tcl/contents.htm">Tcl documentation</a>
for details.
212
213
214
215
216
217
218

219
220
221
222
223
224
225

226
227
228
229
230
231
232
  *  [setParameter](#setParameter)
  *  [setting](#setting)
  *  [stime](#stime)
  *  [styleHeader](#styleHeader)
  *  [styleFooter](#styleFooter)
  *  [styleScript](#styleScript)
  *  [submenu](#submenu)

  *  [tclEval](#tclEval)
  *  [tclExpr](#tclExpr)
  *  [tclInvoke](#tclInvoke)
  *  [tclIsSafe](#tclIsSafe)
  *  [tclMakeSafe](#tclMakeSafe)
  *  [tclReady](#tclReady)
  *  [trace](#trace)

  *  [unversioned content](#unversioned_content)
  *  [unversioned list](#unversioned_list)
  *  [utime](#utime)
  *  [verifyCsrf](#verifyCsrf)
  *  [verifyLogin](#verifyLogin)
  *  [wiki](#wiki)
  *  [wiki_assoc](#wiki_assoc)







>







>







254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
  *  [setParameter](#setParameter)
  *  [setting](#setting)
  *  [stime](#stime)
  *  [styleHeader](#styleHeader)
  *  [styleFooter](#styleFooter)
  *  [styleScript](#styleScript)
  *  [submenu](#submenu)
  *  [taint](#taintCmd)
  *  [tclEval](#tclEval)
  *  [tclExpr](#tclExpr)
  *  [tclInvoke](#tclInvoke)
  *  [tclIsSafe](#tclIsSafe)
  *  [tclMakeSafe](#tclMakeSafe)
  *  [tclReady](#tclReady)
  *  [trace](#trace)
  *  [untaint](#untaintCmd)
  *  [unversioned content](#unversioned_content)
  *  [unversioned list](#unversioned_list)
  *  [utime](#utime)
  *  [verifyCsrf](#verifyCsrf)
  *  [verifyLogin](#verifyLogin)
  *  [wiki](#wiki)
  *  [wiki_assoc](#wiki_assoc)
525
526
527
528
529
530
531
532














533
534
535
536
537
538
539
raise a script error.

<a id="html"></a>TH1 html Command
-----------------------------------

  *  html STRING

Outputs the STRING escaped for HTML.















<a id="htmlize"></a>TH1 htmlize Command
-----------------------------------------

  *  htmlize STRING

Escape all characters of STRING which have special meaning in HTML.







|
>
>
>
>
>
>
>
>
>
>
>
>
>
>







569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
raise a script error.

<a id="html"></a>TH1 html Command
-----------------------------------

  *  html STRING

Outputs the STRING literally.  It is assumed that STRING contains
valid HTML, or that if STRING contains any characters that are
significant to HTML (such as `<`, `>`, `'`, or `&`) have already
been escaped, perhaps by the [htmlize](#htmlize) command.  Use the
[puts](#puts) command to output text that might contain unescaped
HTML markup.

**Beware of XSS attacks!**  If the STRING value to the html command
can be controlled by a hostile user, then he might be able to sneak
in malicious HTML or Javascript which could result in a
cross-site scripting (XSS) attack.  Be careful that all text that
in STRING that might come from user input has been sanitized by the
[htmlize](#htmlize) command or similar.  In recent versions of Fossil,
the STRING value must be [untainted](#taint) or else the "html" command 
will fail.

<a id="htmlize"></a>TH1 htmlize Command
-----------------------------------------

  *  htmlize STRING

Escape all characters of STRING which have special meaning in HTML.
593
594
595
596
597
598
599
600
601




602
603
604
605
606
607
608
609
610
611
612
613

































614
615
616
617
618
619
620
Returns the value of the cryptographic nonce for the request being processed.

<a id="puts"></a>TH1 puts Command
-----------------------------------

  *  puts STRING

Outputs the STRING unchanged, where "unchanged" might, depending on
the context, mean "with some characters escaped for HTML."





<a id="query"></a>TH1 query Command
-------------------------------------

  *  query ?-nocomplain? SQL CODE

Runs the SQL query given by the SQL argument.  For each row in the result
set, run CODE.

In SQL, parameters such as $var are filled in using the value of variable
"var".  Result values are stored in variables with the column name prior
to each invocation of CODE.


































<a id="randhex"></a>TH1 randhex Command
-----------------------------------------

  *  randhex N

Returns a string of N*2 random hexadecimal digits with N<50.  If N is







|
|
>
>
>
>











|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
Returns the value of the cryptographic nonce for the request being processed.

<a id="puts"></a>TH1 puts Command
-----------------------------------

  *  puts STRING

Outputs STRING.  Characters within STRING that have special meaning
in HTML are escaped prior to being output.  Thus is it safe for STRING
to be derived from user inputs.  See also the [html](#html) command
which behaves similarly except does not escape HTML markup.  This
command ("puts") is safe to use on [tainted strings](#taint), but the "html"
command is not.

<a id="query"></a>TH1 query Command
-------------------------------------

  *  query ?-nocomplain? SQL CODE

Runs the SQL query given by the SQL argument.  For each row in the result
set, run CODE.

In SQL, parameters such as $var are filled in using the value of variable
"var".  Result values are stored in variables with the column name prior
to each invocation of CODE.  The names of the variables in which results
are stored can be controlled using "AS name" clauses in the SQL.  As
the database will often contain content that originates from untrusted
users, all result values are marked as [tainted](#taint).

**Beware of SQL injections in the `query` command!**
The SQL argument to the query command should always be literal SQL
text enclosed in {...}.  The SQL argument should never be a double-quoted
string or the value of a \$variable, as those constructs can lead to
an SQL Injection attack.  If you need to include the values of one or
more TH1 variables as part of the SQL, then put \$variable inside the
{...}.  The \$variable keyword will then get passed down into the SQLite
parser which knows to look up the value of \$variable in the TH1 symbol
table.  For example:

~~~
   query {SELECT res FROM tab1 WHERE key=$mykey} {...}
~~~

SQLite will see the \$mykey token in the SQL and will know to resolve it
to the value of the "mykey" TH1 variable, safely and without the possibility
of SQL injection.  The following is unsafe:

~~~
   query "SELECT res FROM tab1 WHERE key='$mykey'" {...}  ;# <-- UNSAFE!
~~~

In this second example, TH1 does the expansion of `$mykey` prior to passing
the text down into SQLite.  So if `$mykey` contains a single-quote character,
followed by additional hostile text, that will result in an SQL injection.

To help guard against SQL-injections, recent versions of Fossil require
that the SQL argument be [untainted](#taint) or else the "query" command
will fail.

<a id="randhex"></a>TH1 randhex Command
-----------------------------------------

  *  randhex N

Returns a string of N*2 random hexadecimal digits with N<50.  If N is
636
637
638
639
640
641
642


643
644
645
646
647
648
649
---------------------------------------

  *  regexp ?-nocase? ?--? exp string

Checks the string against the specified regular expression and returns
non-zero if it matches.  If the regular expression is invalid or cannot
be compiled, an error will be generated.



<a id="reinitialize"></a>TH1 reinitialize Command
---------------------------------------------------

  *  reinitialize ?FLAGS?

Reinitializes the TH1 interpreter using the specified flags.







>
>







731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
---------------------------------------

  *  regexp ?-nocase? ?--? exp string

Checks the string against the specified regular expression and returns
non-zero if it matches.  If the regular expression is invalid or cannot
be compiled, an error will be generated.

See [fossil grep](./grep.md) for details on the regexp syntax.

<a id="reinitialize"></a>TH1 reinitialize Command
---------------------------------------------------

  *  reinitialize ?FLAGS?

Reinitializes the TH1 interpreter using the specified flags.
739
740
741
742
743
744
745














746
747
748
749
750
751
752
<a id="submenu"></a>TH1 submenu Command
-----------------------------------------

  *  submenu link LABEL URL

Add hyperlink to the submenu of the current page.















<a id="tclEval"></a>TH1 tclEval Command
-----------------------------------------

**This command requires the Tcl integration feature.**

  *  tclEval arg ?arg ...?








>
>
>
>
>
>
>
>
>
>
>
>
>
>







836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
<a id="submenu"></a>TH1 submenu Command
-----------------------------------------

  *  submenu link LABEL URL

Add hyperlink to the submenu of the current page.

<a id="taintCmd"></a>TH1 taint Command
-----------------------------------------

  *  taint STRING

This command returns a copy of STRING that has been marked as
[tainted](#taint).  Tainted strings are strings which might be
controlled by an attacker and might contain hostile inputs and
are thus unsafe to use in certain contexts.  For example, tainted
strings should not be output as part of a webpage as they might
contain rogue HTML or Javascript that could lead to an XSS
vulnerability.  Similarly, tainted strings should not be run as
SQL since they might contain an SQL-injection vulerability.

<a id="tclEval"></a>TH1 tclEval Command
-----------------------------------------

**This command requires the Tcl integration feature.**

  *  tclEval arg ?arg ...?

810
811
812
813
814
815
816












817
818
819
820
821
822
823
<a id="trace"></a>TH1 trace Command
-------------------------------------

  *  trace STRING

Generates a TH1 trace message if TH1 tracing is enabled.













<a id="unversioned_content"></a>TH1 unversioned content Command
-----------------------------------------------------------------

  *  unversioned content FILENAME

Attempts to locate the specified unversioned file and return its contents.
An error is generated if the repository is not open or the unversioned file







>
>
>
>
>
>
>
>
>
>
>
>







921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
<a id="trace"></a>TH1 trace Command
-------------------------------------

  *  trace STRING

Generates a TH1 trace message if TH1 tracing is enabled.

<a id="untaintCmd"></a>TH1 taint Command
-----------------------------------------

  *  untaint STRING

This command returns a copy of STRING that has been marked as
[untainted](#taint).  Untainted strings are strings which are
believed to be free of potentially hostile content.  Use this
command with caution, as it overwrites the tainted-string protection
mechanisms that are built into TH1.  If you do not understand all
the implications of executing this command, then do not use it.

<a id="unversioned_content"></a>TH1 unversioned content Command
-----------------------------------------------------------------

  *  unversioned content FILENAME

Attempts to locate the specified unversioned file and return its contents.
An error is generated if the repository is not open or the unversioned file