Fossil SCM

Merge the latest trunk enhancements into the quickfilter branch.

drh 2025-04-25 11:01 quickfilter merge
Commit e14c75676c3eb42afc4af2408c8dce2594ec3e1a3032db611d57a11fd1f573f1
95 files changed +101 -30 +588 -151 +5 -4 +1 -1 +1 -1 +1 -1 +1 -1 +1 -1 +1 -1 +1 -1 +101 +1 -1 +9 -2 +148 -64 +12 -5 +8 -7 +2 -2 +54 -28 +289 -165 +2 -1 +4 +353 +1 -1 +1 +1 -1 +75 -3 +2 +32 -5 +3 -3 +95 -22 +376 -57 +1 -1 +1 -1 -1 +12 -12 +55 -25 +21 -8 +18 -2 +33 -21 +62 -16 +28 -21 +65 -6 +8 -3 +113 -23 +113 -23 +2 -4 +2 -4 +3 -2 +66 -16 +15 -5 +85 -39 +4 -3 +2 -1 +98 -48 +5 -1 +8 -2 +1 -1 +3 +9 -7 +9 -7 +17 -7 +95 -56 +53 -14 +92 -53 +207 -39 +27 -89 +49 -22 +27 -15 +131 -15 +1 -1 +5 -3 +79 -41 +16 -6 +2 -2 +20 -8 +1 -1 +9 +84 +36 -36 +84 +141 +1 -1 +1 -1 +6 +53 -22 +18 +4 -3 +4 -3 +2 -2 +106 -74 +3 -3 +1 -1 +1 -1 +128 -5
~ Makefile.in ~ extsrc/shell.c ~ extsrc/sqlite3.c ~ extsrc/sqlite3.h ~ skins/blitz/footer.txt ~ skins/darkmode/footer.txt ~ skins/default/header.txt ~ skins/eagle/footer.txt ~ skins/eagle/header.txt ~ skins/original/footer.txt ~ skins/original/header.txt ~ skins/xekri/css.txt ~ skins/xekri/header.txt ~ src/add.c ~ src/alerts.c ~ src/backoffice.c ~ src/branch.c ~ src/browse.c ~ src/cache.c ~ src/cgi.c ~ src/chat.c ~ src/checkin.c ~ src/color.c ~ src/db.c ~ src/default.css ~ src/doc.c ~ src/encode.c ~ src/finfo.c ~ src/forum.c ~ src/fossil.dom.js ~ src/fossil.fetch.js ~ src/fossil.page.chat.js ~ src/fossil.popupwidget.js ~ src/graph.c ~ src/graph.js ~ src/http_ssl.c ~ src/info.c ~ src/loadctrl.c ~ src/login.c ~ src/lookslike.c ~ src/main.c ~ src/manifest.c ~ src/name.c ~ src/printf.c ~ src/repolist.c ~ src/repolist.c ~ src/report.c ~ src/report.c ~ src/search.c ~ src/security_audit.c ~ src/setup.c ~ src/setupuser.c ~ src/shun.c ~ src/sitemap.c ~ src/smtp.c ~ src/sorttable.js ~ src/stash.c ~ src/stat.c ~ src/statrep.c ~ src/style.c ~ src/style.c ~ src/style.chat.css ~ src/th.c ~ src/th.h ~ src/th_lang.c ~ src/th_main.c ~ src/th_tcl.c ~ src/timeline.c ~ src/tkt.c ~ src/tktsetup.c ~ src/undo.c ~ src/unversioned.c ~ src/user.c ~ src/util.c ~ src/winfile.c ~ src/xfer.c ~ test/many-www.tcl ~ test/tester.tcl ~ test/th1-taint.test ~ test/th1.test ~ tools/fake-smtpd.tcl ~ tools/find-fossil-cgis.tcl ~ tools/fossil-stress.tcl ~ www/aboutcgi.wiki ~ www/cgi.wiki ~ www/changes.wiki ~ www/chat.md ~ www/env-opts.md ~ www/env-opts.md ~ www/loadmgmt.md ~ www/quickstart.wiki ~ www/server/debian/service.md ~ www/server/windows/service.md ~ www/serverext.wiki ~ www/th1.md

No diff available

+101 -30
--- extsrc/shell.c
+++ extsrc/shell.c
@@ -1162,10 +1162,27 @@
11621162
}
11631163
}
11641164
return n;
11651165
}
11661166
#endif
1167
+
1168
+/*
1169
+** Check to see if z[] is a valid VT100 escape. If it is, then
1170
+** return the number of bytes in the escape sequence. Return 0 if
1171
+** z[] is not a VT100 escape.
1172
+**
1173
+** This routine assumes that z[0] is \033 (ESC).
1174
+*/
1175
+static int isVt100(const unsigned char *z){
1176
+ int i;
1177
+ if( z[1]!='[' ) return 0;
1178
+ i = 2;
1179
+ while( z[i]>=0x30 && z[i]<=0x3f ){ i++; }
1180
+ while( z[i]>=0x20 && z[i]<=0x2f ){ i++; }
1181
+ if( z[i]<0x40 || z[i]>0x7e ) return 0;
1182
+ return i+1;
1183
+}
11671184
11681185
/*
11691186
** Output string zUtf to stdout as w characters. If w is negative,
11701187
** then right-justify the text. W is the width in UTF-8 characters, not
11711188
** in bytes. This is different from the %*.*s specification in printf
@@ -1178,10 +1195,11 @@
11781195
static void utf8_width_print(FILE *out, int w, const char *zUtf){
11791196
const unsigned char *a = (const unsigned char*)zUtf;
11801197
unsigned char c;
11811198
int i = 0;
11821199
int n = 0;
1200
+ int k;
11831201
int aw = w<0 ? -w : w;
11841202
if( zUtf==0 ) zUtf = "";
11851203
while( (c = a[i])!=0 ){
11861204
if( (c&0xc0)==0xc0 ){
11871205
int u;
@@ -1190,10 +1208,12 @@
11901208
if( x+n>aw ){
11911209
break;
11921210
}
11931211
i += len;
11941212
n += x;
1213
+ }else if( c==0x1b && (k = isVt100(&a[i]))>0 ){
1214
+ i += k;
11951215
}else if( n>=aw ){
11961216
break;
11971217
}else{
11981218
n++;
11991219
i++;
@@ -6270,12 +6290,11 @@
62706290
** start HIDDEN,
62716291
** stop HIDDEN,
62726292
** step HIDDEN
62736293
** );
62746294
**
6275
-** The virtual table also has a rowid, logically equivalent to n+1 where
6276
-** "n" is the ascending integer in the aforesaid production definition.
6295
+** The virtual table also has a rowid which is an alias for the value.
62776296
**
62786297
** Function arguments in queries against this virtual table are translated
62796298
** into equality constraints against successive hidden columns. In other
62806299
** words, the following pairs of queries are equivalent to each other:
62816300
**
@@ -6326,10 +6345,11 @@
63266345
/* #include "sqlite3ext.h" */
63276346
SQLITE_EXTENSION_INIT1
63286347
#include <assert.h>
63296348
#include <string.h>
63306349
#include <limits.h>
6350
+#include <math.h>
63316351
63326352
#ifndef SQLITE_OMIT_VIRTUALTABLE
63336353
/*
63346354
** Return that member of a generate_series(...) sequence whose 0-based
63356355
** index is ix. The 0th member is given by smBase. The sequence members
@@ -6486,10 +6506,11 @@
64866506
){
64876507
sqlite3_vtab *pNew;
64886508
int rc;
64896509
64906510
/* Column numbers */
6511
+#define SERIES_COLUMN_ROWID (-1)
64916512
#define SERIES_COLUMN_VALUE 0
64926513
#define SERIES_COLUMN_START 1
64936514
#define SERIES_COLUMN_STOP 2
64946515
#define SERIES_COLUMN_STEP 3
64956516
@@ -6573,17 +6594,15 @@
65736594
#define LARGEST_UINT64 (0xffffffff|(((sqlite3_uint64)0xffffffff)<<32))
65746595
#define SMALLEST_INT64 (((sqlite3_int64)-1) - LARGEST_INT64)
65756596
#endif
65766597
65776598
/*
6578
-** Return the rowid for the current row, logically equivalent to n+1 where
6579
-** "n" is the ascending integer in the aforesaid production definition.
6599
+** The rowid is the same as the value.
65806600
*/
65816601
static int seriesRowid(sqlite3_vtab_cursor *cur, sqlite_int64 *pRowid){
65826602
series_cursor *pCur = (series_cursor*)cur;
6583
- sqlite3_uint64 n = pCur->ss.uSeqIndexNow;
6584
- *pRowid = (sqlite3_int64)((n<LARGEST_UINT64)? n+1 : 0);
6603
+ *pRowid = pCur->ss.iValueNow;
65856604
return SQLITE_OK;
65866605
}
65876606
65886607
/*
65896608
** Return TRUE if the cursor has been moved off of the last
@@ -6692,29 +6711,56 @@
66926711
if( idxNum & 0x3380 ){
66936712
/* Extract the maximum range of output values determined by
66946713
** constraints on the "value" column.
66956714
*/
66966715
if( idxNum & 0x0080 ){
6697
- iMin = iMax = sqlite3_value_int64(argv[i++]);
6716
+ if( sqlite3_value_numeric_type(argv[i])==SQLITE_FLOAT ){
6717
+ double r = sqlite3_value_double(argv[i++]);
6718
+ if( r==ceil(r) ){
6719
+ iMin = iMax = (sqlite3_int64)r;
6720
+ }else{
6721
+ returnNoRows = 1;
6722
+ }
6723
+ }else{
6724
+ iMin = iMax = sqlite3_value_int64(argv[i++]);
6725
+ }
66986726
}else{
66996727
if( idxNum & 0x0300 ){
6700
- iMin = sqlite3_value_int64(argv[i++]);
6701
- if( idxNum & 0x0200 ){
6702
- if( iMin==LARGEST_INT64 ){
6703
- returnNoRows = 1;
6728
+ if( sqlite3_value_numeric_type(argv[i])==SQLITE_FLOAT ){
6729
+ double r = sqlite3_value_double(argv[i++]);
6730
+ if( idxNum & 0x0200 && r==ceil(r) ){
6731
+ iMin = (sqlite3_int64)ceil(r+1.0);
67046732
}else{
6705
- iMin++;
6733
+ iMin = (sqlite3_int64)ceil(r);
6734
+ }
6735
+ }else{
6736
+ iMin = sqlite3_value_int64(argv[i++]);
6737
+ if( idxNum & 0x0200 ){
6738
+ if( iMin==LARGEST_INT64 ){
6739
+ returnNoRows = 1;
6740
+ }else{
6741
+ iMin++;
6742
+ }
67066743
}
67076744
}
67086745
}
67096746
if( idxNum & 0x3000 ){
6710
- iMax = sqlite3_value_int64(argv[i++]);
6711
- if( idxNum & 0x2000 ){
6712
- if( iMax==SMALLEST_INT64 ){
6713
- returnNoRows = 1;
6747
+ if( sqlite3_value_numeric_type(argv[i])==SQLITE_FLOAT ){
6748
+ double r = sqlite3_value_double(argv[i++]);
6749
+ if( (idxNum & 0x2000)!=0 && r==floor(r) ){
6750
+ iMax = (sqlite3_int64)(r-1.0);
67146751
}else{
6715
- iMax--;
6752
+ iMax = (sqlite3_int64)floor(r);
6753
+ }
6754
+ }else{
6755
+ iMax = sqlite3_value_int64(argv[i++]);
6756
+ if( idxNum & 0x2000 ){
6757
+ if( iMax==SMALLEST_INT64 ){
6758
+ returnNoRows = 1;
6759
+ }else{
6760
+ iMax--;
6761
+ }
67166762
}
67176763
}
67186764
}
67196765
if( iMin>iMax ){
67206766
returnNoRows = 1;
@@ -6764,11 +6810,11 @@
67646810
67656811
67666812
for(i=0; i<argc; i++){
67676813
if( sqlite3_value_type(argv[i])==SQLITE_NULL ){
67686814
/* If any of the constraints have a NULL value, then return no rows.
6769
- ** See ticket https://www.sqlite.org/src/info/fac496b61722daf2 */
6815
+ ** See ticket https://sqlite.org/src/info/fac496b61722daf2 */
67706816
returnNoRows = 1;
67716817
break;
67726818
}
67736819
}
67746820
if( returnNoRows ){
@@ -6867,11 +6913,14 @@
68676913
idxNum |= 0x40;
68686914
}
68696915
continue;
68706916
}
68716917
if( pConstraint->iColumn<SERIES_COLUMN_START ){
6872
- if( pConstraint->iColumn==SERIES_COLUMN_VALUE && pConstraint->usable ){
6918
+ if( (pConstraint->iColumn==SERIES_COLUMN_VALUE ||
6919
+ pConstraint->iColumn==SERIES_COLUMN_ROWID)
6920
+ && pConstraint->usable
6921
+ ){
68736922
switch( op ){
68746923
case SQLITE_INDEX_CONSTRAINT_EQ:
68756924
case SQLITE_INDEX_CONSTRAINT_IS: {
68766925
idxNum |= 0x0080;
68776926
idxNum &= ~0x3300;
@@ -24197,13 +24246,18 @@
2419724246
j++;
2419824247
}while( (n&7)!=0 && n<mxWidth );
2419924248
i++;
2420024249
continue;
2420124250
}
24202
- n++;
24203
- j += 3;
24204
- i++;
24251
+ if( c==0x1b && p->eEscMode==SHELL_ESC_OFF && (k = isVt100(&z[i]))>0 ){
24252
+ i += k;
24253
+ j += k;
24254
+ }else{
24255
+ n++;
24256
+ j += 3;
24257
+ i++;
24258
+ }
2420524259
}
2420624260
if( n>=mxWidth && bWordWrap ){
2420724261
/* Perhaps try to back up to a better place to break the line */
2420824262
for(k=i; k>i/2; k--){
2420924263
if( IsSpace(z[k-1]) ) break;
@@ -24265,13 +24319,21 @@
2426524319
break;
2426624320
case SHELL_ESC_ASCII:
2426724321
zOut[j++] = '^';
2426824322
zOut[j++] = 0x40 + c;
2426924323
break;
24270
- case SHELL_ESC_OFF:
24271
- zOut[j++] = c;
24324
+ case SHELL_ESC_OFF: {
24325
+ int nn;
24326
+ if( c==0x1b && (nn = isVt100(&z[i]))>0 ){
24327
+ memcpy(&zOut[j], &z[i], nn);
24328
+ j += nn;
24329
+ i += nn - 1;
24330
+ }else{
24331
+ zOut[j++] = c;
24332
+ }
2427224333
break;
24334
+ }
2427324335
}
2427424336
i++;
2427524337
}
2427624338
zOut[j] = 0;
2427724339
return (char*)zOut;
@@ -25404,10 +25466,11 @@
2540425466
" --readonly Open FILE readonly",
2540525467
" --zip FILE is a ZIP archive",
2540625468
#ifndef SQLITE_SHELL_FIDDLE
2540725469
".output ?FILE? Send output to FILE or stdout if FILE is omitted",
2540825470
" If FILE begins with '|' then open it as a pipe.",
25471
+ " If FILE is 'off' then output is disabled.",
2540925472
" Options:",
2541025473
" --bom Prefix output with a UTF8 byte-order mark",
2541125474
" -e Send output to the system text editor",
2541225475
" --plain Use text/plain for -w option",
2541325476
" -w Send output to a web browser",
@@ -27021,11 +27084,11 @@
2702127084
for(i=0; i<pgSz; i+=16){
2702227085
const u8 *aLine = aData+i;
2702327086
for(j=0; j<16 && aLine[j]==0; j++){}
2702427087
if( j==16 ) continue;
2702527088
if( !seenPageLabel ){
27026
- sqlite3_fprintf(p->out, "| page %lld offset %lld\n", pgno, pgno*pgSz);
27089
+ sqlite3_fprintf(p->out, "| page %lld offset %lld\n",pgno,(pgno-1)*pgSz);
2702727090
seenPageLabel = 1;
2702827091
}
2702927092
sqlite3_fprintf(p->out, "| %5d:", i);
2703027093
for(j=0; j<16; j++) sqlite3_fprintf(p->out, " %02x", aLine[j]);
2703127094
sqlite3_fprintf(p->out, " ");
@@ -30399,13 +30462,13 @@
3039930462
|| (c=='e' && n==5 && cli_strcmp(azArg[0],"excel")==0)
3040030463
|| (c=='w' && n==3 && cli_strcmp(azArg[0],"www")==0)
3040130464
){
3040230465
char *zFile = 0;
3040330466
int i;
30404
- int eMode = 0;
30405
- int bOnce = 0; /* 0: .output, 1: .once, 2: .excel/.www */
30406
- int bPlain = 0; /* --plain option */
30467
+ int eMode = 0; /* 0: .outout/.once, 'x'=.excel, 'w'=.www */
30468
+ int bOnce = 0; /* 0: .output, 1: .once, 2: .excel/.www */
30469
+ int bPlain = 0; /* --plain option */
3040730470
static const char *zBomUtf8 = "\357\273\277";
3040830471
const char *zBom = 0;
3040930472
3041030473
failIfSafeMode(p, "cannot run .%s in safe mode", azArg[0]);
3041130474
if( c=='e' ){
@@ -30430,18 +30493,26 @@
3043030493
}else if( c=='o' && cli_strcmp(z,"-e")==0 ){
3043130494
eMode = 'e'; /* text editor */
3043230495
}else if( c=='o' && cli_strcmp(z,"-w")==0 ){
3043330496
eMode = 'w'; /* Web browser */
3043430497
}else{
30435
- sqlite3_fprintf(p->out,
30498
+ sqlite3_fprintf(p->out,
3043630499
"ERROR: unknown option: \"%s\". Usage:\n", azArg[i]);
3043730500
showHelp(p->out, azArg[0]);
3043830501
rc = 1;
3043930502
goto meta_command_exit;
3044030503
}
3044130504
}else if( zFile==0 && eMode==0 ){
30442
- zFile = sqlite3_mprintf("%s", z);
30505
+ if( cli_strcmp(z, "off")==0 ){
30506
+#ifdef _WIN32
30507
+ zFile = sqlite3_mprintf("nul");
30508
+#else
30509
+ zFile = sqlite3_mprintf("/dev/null");
30510
+#endif
30511
+ }else{
30512
+ zFile = sqlite3_mprintf("%s", z);
30513
+ }
3044330514
if( zFile && zFile[0]=='|' ){
3044430515
while( i+1<nArg ) zFile = sqlite3_mprintf("%z %s", zFile, azArg[++i]);
3044530516
break;
3044630517
}
3044730518
}else{
3044830519
--- extsrc/shell.c
+++ extsrc/shell.c
@@ -1162,10 +1162,27 @@
1162 }
1163 }
1164 return n;
1165 }
1166 #endif
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1167
1168 /*
1169 ** Output string zUtf to stdout as w characters. If w is negative,
1170 ** then right-justify the text. W is the width in UTF-8 characters, not
1171 ** in bytes. This is different from the %*.*s specification in printf
@@ -1178,10 +1195,11 @@
1178 static void utf8_width_print(FILE *out, int w, const char *zUtf){
1179 const unsigned char *a = (const unsigned char*)zUtf;
1180 unsigned char c;
1181 int i = 0;
1182 int n = 0;
 
1183 int aw = w<0 ? -w : w;
1184 if( zUtf==0 ) zUtf = "";
1185 while( (c = a[i])!=0 ){
1186 if( (c&0xc0)==0xc0 ){
1187 int u;
@@ -1190,10 +1208,12 @@
1190 if( x+n>aw ){
1191 break;
1192 }
1193 i += len;
1194 n += x;
 
 
1195 }else if( n>=aw ){
1196 break;
1197 }else{
1198 n++;
1199 i++;
@@ -6270,12 +6290,11 @@
6270 ** start HIDDEN,
6271 ** stop HIDDEN,
6272 ** step HIDDEN
6273 ** );
6274 **
6275 ** The virtual table also has a rowid, logically equivalent to n+1 where
6276 ** "n" is the ascending integer in the aforesaid production definition.
6277 **
6278 ** Function arguments in queries against this virtual table are translated
6279 ** into equality constraints against successive hidden columns. In other
6280 ** words, the following pairs of queries are equivalent to each other:
6281 **
@@ -6326,10 +6345,11 @@
6326 /* #include "sqlite3ext.h" */
6327 SQLITE_EXTENSION_INIT1
6328 #include <assert.h>
6329 #include <string.h>
6330 #include <limits.h>
 
6331
6332 #ifndef SQLITE_OMIT_VIRTUALTABLE
6333 /*
6334 ** Return that member of a generate_series(...) sequence whose 0-based
6335 ** index is ix. The 0th member is given by smBase. The sequence members
@@ -6486,10 +6506,11 @@
6486 ){
6487 sqlite3_vtab *pNew;
6488 int rc;
6489
6490 /* Column numbers */
 
6491 #define SERIES_COLUMN_VALUE 0
6492 #define SERIES_COLUMN_START 1
6493 #define SERIES_COLUMN_STOP 2
6494 #define SERIES_COLUMN_STEP 3
6495
@@ -6573,17 +6594,15 @@
6573 #define LARGEST_UINT64 (0xffffffff|(((sqlite3_uint64)0xffffffff)<<32))
6574 #define SMALLEST_INT64 (((sqlite3_int64)-1) - LARGEST_INT64)
6575 #endif
6576
6577 /*
6578 ** Return the rowid for the current row, logically equivalent to n+1 where
6579 ** "n" is the ascending integer in the aforesaid production definition.
6580 */
6581 static int seriesRowid(sqlite3_vtab_cursor *cur, sqlite_int64 *pRowid){
6582 series_cursor *pCur = (series_cursor*)cur;
6583 sqlite3_uint64 n = pCur->ss.uSeqIndexNow;
6584 *pRowid = (sqlite3_int64)((n<LARGEST_UINT64)? n+1 : 0);
6585 return SQLITE_OK;
6586 }
6587
6588 /*
6589 ** Return TRUE if the cursor has been moved off of the last
@@ -6692,29 +6711,56 @@
6692 if( idxNum & 0x3380 ){
6693 /* Extract the maximum range of output values determined by
6694 ** constraints on the "value" column.
6695 */
6696 if( idxNum & 0x0080 ){
6697 iMin = iMax = sqlite3_value_int64(argv[i++]);
 
 
 
 
 
 
 
 
 
6698 }else{
6699 if( idxNum & 0x0300 ){
6700 iMin = sqlite3_value_int64(argv[i++]);
6701 if( idxNum & 0x0200 ){
6702 if( iMin==LARGEST_INT64 ){
6703 returnNoRows = 1;
6704 }else{
6705 iMin++;
 
 
 
 
 
 
 
 
 
6706 }
6707 }
6708 }
6709 if( idxNum & 0x3000 ){
6710 iMax = sqlite3_value_int64(argv[i++]);
6711 if( idxNum & 0x2000 ){
6712 if( iMax==SMALLEST_INT64 ){
6713 returnNoRows = 1;
6714 }else{
6715 iMax--;
 
 
 
 
 
 
 
 
 
6716 }
6717 }
6718 }
6719 if( iMin>iMax ){
6720 returnNoRows = 1;
@@ -6764,11 +6810,11 @@
6764
6765
6766 for(i=0; i<argc; i++){
6767 if( sqlite3_value_type(argv[i])==SQLITE_NULL ){
6768 /* If any of the constraints have a NULL value, then return no rows.
6769 ** See ticket https://www.sqlite.org/src/info/fac496b61722daf2 */
6770 returnNoRows = 1;
6771 break;
6772 }
6773 }
6774 if( returnNoRows ){
@@ -6867,11 +6913,14 @@
6867 idxNum |= 0x40;
6868 }
6869 continue;
6870 }
6871 if( pConstraint->iColumn<SERIES_COLUMN_START ){
6872 if( pConstraint->iColumn==SERIES_COLUMN_VALUE && pConstraint->usable ){
 
 
 
6873 switch( op ){
6874 case SQLITE_INDEX_CONSTRAINT_EQ:
6875 case SQLITE_INDEX_CONSTRAINT_IS: {
6876 idxNum |= 0x0080;
6877 idxNum &= ~0x3300;
@@ -24197,13 +24246,18 @@
24197 j++;
24198 }while( (n&7)!=0 && n<mxWidth );
24199 i++;
24200 continue;
24201 }
24202 n++;
24203 j += 3;
24204 i++;
 
 
 
 
 
24205 }
24206 if( n>=mxWidth && bWordWrap ){
24207 /* Perhaps try to back up to a better place to break the line */
24208 for(k=i; k>i/2; k--){
24209 if( IsSpace(z[k-1]) ) break;
@@ -24265,13 +24319,21 @@
24265 break;
24266 case SHELL_ESC_ASCII:
24267 zOut[j++] = '^';
24268 zOut[j++] = 0x40 + c;
24269 break;
24270 case SHELL_ESC_OFF:
24271 zOut[j++] = c;
 
 
 
 
 
 
 
24272 break;
 
24273 }
24274 i++;
24275 }
24276 zOut[j] = 0;
24277 return (char*)zOut;
@@ -25404,10 +25466,11 @@
25404 " --readonly Open FILE readonly",
25405 " --zip FILE is a ZIP archive",
25406 #ifndef SQLITE_SHELL_FIDDLE
25407 ".output ?FILE? Send output to FILE or stdout if FILE is omitted",
25408 " If FILE begins with '|' then open it as a pipe.",
 
25409 " Options:",
25410 " --bom Prefix output with a UTF8 byte-order mark",
25411 " -e Send output to the system text editor",
25412 " --plain Use text/plain for -w option",
25413 " -w Send output to a web browser",
@@ -27021,11 +27084,11 @@
27021 for(i=0; i<pgSz; i+=16){
27022 const u8 *aLine = aData+i;
27023 for(j=0; j<16 && aLine[j]==0; j++){}
27024 if( j==16 ) continue;
27025 if( !seenPageLabel ){
27026 sqlite3_fprintf(p->out, "| page %lld offset %lld\n", pgno, pgno*pgSz);
27027 seenPageLabel = 1;
27028 }
27029 sqlite3_fprintf(p->out, "| %5d:", i);
27030 for(j=0; j<16; j++) sqlite3_fprintf(p->out, " %02x", aLine[j]);
27031 sqlite3_fprintf(p->out, " ");
@@ -30399,13 +30462,13 @@
30399 || (c=='e' && n==5 && cli_strcmp(azArg[0],"excel")==0)
30400 || (c=='w' && n==3 && cli_strcmp(azArg[0],"www")==0)
30401 ){
30402 char *zFile = 0;
30403 int i;
30404 int eMode = 0;
30405 int bOnce = 0; /* 0: .output, 1: .once, 2: .excel/.www */
30406 int bPlain = 0; /* --plain option */
30407 static const char *zBomUtf8 = "\357\273\277";
30408 const char *zBom = 0;
30409
30410 failIfSafeMode(p, "cannot run .%s in safe mode", azArg[0]);
30411 if( c=='e' ){
@@ -30430,18 +30493,26 @@
30430 }else if( c=='o' && cli_strcmp(z,"-e")==0 ){
30431 eMode = 'e'; /* text editor */
30432 }else if( c=='o' && cli_strcmp(z,"-w")==0 ){
30433 eMode = 'w'; /* Web browser */
30434 }else{
30435 sqlite3_fprintf(p->out,
30436 "ERROR: unknown option: \"%s\". Usage:\n", azArg[i]);
30437 showHelp(p->out, azArg[0]);
30438 rc = 1;
30439 goto meta_command_exit;
30440 }
30441 }else if( zFile==0 && eMode==0 ){
30442 zFile = sqlite3_mprintf("%s", z);
 
 
 
 
 
 
 
 
30443 if( zFile && zFile[0]=='|' ){
30444 while( i+1<nArg ) zFile = sqlite3_mprintf("%z %s", zFile, azArg[++i]);
30445 break;
30446 }
30447 }else{
30448
--- extsrc/shell.c
+++ extsrc/shell.c
@@ -1162,10 +1162,27 @@
1162 }
1163 }
1164 return n;
1165 }
1166 #endif
1167
1168 /*
1169 ** Check to see if z[] is a valid VT100 escape. If it is, then
1170 ** return the number of bytes in the escape sequence. Return 0 if
1171 ** z[] is not a VT100 escape.
1172 **
1173 ** This routine assumes that z[0] is \033 (ESC).
1174 */
1175 static int isVt100(const unsigned char *z){
1176 int i;
1177 if( z[1]!='[' ) return 0;
1178 i = 2;
1179 while( z[i]>=0x30 && z[i]<=0x3f ){ i++; }
1180 while( z[i]>=0x20 && z[i]<=0x2f ){ i++; }
1181 if( z[i]<0x40 || z[i]>0x7e ) return 0;
1182 return i+1;
1183 }
1184
1185 /*
1186 ** Output string zUtf to stdout as w characters. If w is negative,
1187 ** then right-justify the text. W is the width in UTF-8 characters, not
1188 ** in bytes. This is different from the %*.*s specification in printf
@@ -1178,10 +1195,11 @@
1195 static void utf8_width_print(FILE *out, int w, const char *zUtf){
1196 const unsigned char *a = (const unsigned char*)zUtf;
1197 unsigned char c;
1198 int i = 0;
1199 int n = 0;
1200 int k;
1201 int aw = w<0 ? -w : w;
1202 if( zUtf==0 ) zUtf = "";
1203 while( (c = a[i])!=0 ){
1204 if( (c&0xc0)==0xc0 ){
1205 int u;
@@ -1190,10 +1208,12 @@
1208 if( x+n>aw ){
1209 break;
1210 }
1211 i += len;
1212 n += x;
1213 }else if( c==0x1b && (k = isVt100(&a[i]))>0 ){
1214 i += k;
1215 }else if( n>=aw ){
1216 break;
1217 }else{
1218 n++;
1219 i++;
@@ -6270,12 +6290,11 @@
6290 ** start HIDDEN,
6291 ** stop HIDDEN,
6292 ** step HIDDEN
6293 ** );
6294 **
6295 ** The virtual table also has a rowid which is an alias for the value.
 
6296 **
6297 ** Function arguments in queries against this virtual table are translated
6298 ** into equality constraints against successive hidden columns. In other
6299 ** words, the following pairs of queries are equivalent to each other:
6300 **
@@ -6326,10 +6345,11 @@
6345 /* #include "sqlite3ext.h" */
6346 SQLITE_EXTENSION_INIT1
6347 #include <assert.h>
6348 #include <string.h>
6349 #include <limits.h>
6350 #include <math.h>
6351
6352 #ifndef SQLITE_OMIT_VIRTUALTABLE
6353 /*
6354 ** Return that member of a generate_series(...) sequence whose 0-based
6355 ** index is ix. The 0th member is given by smBase. The sequence members
@@ -6486,10 +6506,11 @@
6506 ){
6507 sqlite3_vtab *pNew;
6508 int rc;
6509
6510 /* Column numbers */
6511 #define SERIES_COLUMN_ROWID (-1)
6512 #define SERIES_COLUMN_VALUE 0
6513 #define SERIES_COLUMN_START 1
6514 #define SERIES_COLUMN_STOP 2
6515 #define SERIES_COLUMN_STEP 3
6516
@@ -6573,17 +6594,15 @@
6594 #define LARGEST_UINT64 (0xffffffff|(((sqlite3_uint64)0xffffffff)<<32))
6595 #define SMALLEST_INT64 (((sqlite3_int64)-1) - LARGEST_INT64)
6596 #endif
6597
6598 /*
6599 ** The rowid is the same as the value.
 
6600 */
6601 static int seriesRowid(sqlite3_vtab_cursor *cur, sqlite_int64 *pRowid){
6602 series_cursor *pCur = (series_cursor*)cur;
6603 *pRowid = pCur->ss.iValueNow;
 
6604 return SQLITE_OK;
6605 }
6606
6607 /*
6608 ** Return TRUE if the cursor has been moved off of the last
@@ -6692,29 +6711,56 @@
6711 if( idxNum & 0x3380 ){
6712 /* Extract the maximum range of output values determined by
6713 ** constraints on the "value" column.
6714 */
6715 if( idxNum & 0x0080 ){
6716 if( sqlite3_value_numeric_type(argv[i])==SQLITE_FLOAT ){
6717 double r = sqlite3_value_double(argv[i++]);
6718 if( r==ceil(r) ){
6719 iMin = iMax = (sqlite3_int64)r;
6720 }else{
6721 returnNoRows = 1;
6722 }
6723 }else{
6724 iMin = iMax = sqlite3_value_int64(argv[i++]);
6725 }
6726 }else{
6727 if( idxNum & 0x0300 ){
6728 if( sqlite3_value_numeric_type(argv[i])==SQLITE_FLOAT ){
6729 double r = sqlite3_value_double(argv[i++]);
6730 if( idxNum & 0x0200 && r==ceil(r) ){
6731 iMin = (sqlite3_int64)ceil(r+1.0);
6732 }else{
6733 iMin = (sqlite3_int64)ceil(r);
6734 }
6735 }else{
6736 iMin = sqlite3_value_int64(argv[i++]);
6737 if( idxNum & 0x0200 ){
6738 if( iMin==LARGEST_INT64 ){
6739 returnNoRows = 1;
6740 }else{
6741 iMin++;
6742 }
6743 }
6744 }
6745 }
6746 if( idxNum & 0x3000 ){
6747 if( sqlite3_value_numeric_type(argv[i])==SQLITE_FLOAT ){
6748 double r = sqlite3_value_double(argv[i++]);
6749 if( (idxNum & 0x2000)!=0 && r==floor(r) ){
6750 iMax = (sqlite3_int64)(r-1.0);
6751 }else{
6752 iMax = (sqlite3_int64)floor(r);
6753 }
6754 }else{
6755 iMax = sqlite3_value_int64(argv[i++]);
6756 if( idxNum & 0x2000 ){
6757 if( iMax==SMALLEST_INT64 ){
6758 returnNoRows = 1;
6759 }else{
6760 iMax--;
6761 }
6762 }
6763 }
6764 }
6765 if( iMin>iMax ){
6766 returnNoRows = 1;
@@ -6764,11 +6810,11 @@
6810
6811
6812 for(i=0; i<argc; i++){
6813 if( sqlite3_value_type(argv[i])==SQLITE_NULL ){
6814 /* If any of the constraints have a NULL value, then return no rows.
6815 ** See ticket https://sqlite.org/src/info/fac496b61722daf2 */
6816 returnNoRows = 1;
6817 break;
6818 }
6819 }
6820 if( returnNoRows ){
@@ -6867,11 +6913,14 @@
6913 idxNum |= 0x40;
6914 }
6915 continue;
6916 }
6917 if( pConstraint->iColumn<SERIES_COLUMN_START ){
6918 if( (pConstraint->iColumn==SERIES_COLUMN_VALUE ||
6919 pConstraint->iColumn==SERIES_COLUMN_ROWID)
6920 && pConstraint->usable
6921 ){
6922 switch( op ){
6923 case SQLITE_INDEX_CONSTRAINT_EQ:
6924 case SQLITE_INDEX_CONSTRAINT_IS: {
6925 idxNum |= 0x0080;
6926 idxNum &= ~0x3300;
@@ -24197,13 +24246,18 @@
24246 j++;
24247 }while( (n&7)!=0 && n<mxWidth );
24248 i++;
24249 continue;
24250 }
24251 if( c==0x1b && p->eEscMode==SHELL_ESC_OFF && (k = isVt100(&z[i]))>0 ){
24252 i += k;
24253 j += k;
24254 }else{
24255 n++;
24256 j += 3;
24257 i++;
24258 }
24259 }
24260 if( n>=mxWidth && bWordWrap ){
24261 /* Perhaps try to back up to a better place to break the line */
24262 for(k=i; k>i/2; k--){
24263 if( IsSpace(z[k-1]) ) break;
@@ -24265,13 +24319,21 @@
24319 break;
24320 case SHELL_ESC_ASCII:
24321 zOut[j++] = '^';
24322 zOut[j++] = 0x40 + c;
24323 break;
24324 case SHELL_ESC_OFF: {
24325 int nn;
24326 if( c==0x1b && (nn = isVt100(&z[i]))>0 ){
24327 memcpy(&zOut[j], &z[i], nn);
24328 j += nn;
24329 i += nn - 1;
24330 }else{
24331 zOut[j++] = c;
24332 }
24333 break;
24334 }
24335 }
24336 i++;
24337 }
24338 zOut[j] = 0;
24339 return (char*)zOut;
@@ -25404,10 +25466,11 @@
25466 " --readonly Open FILE readonly",
25467 " --zip FILE is a ZIP archive",
25468 #ifndef SQLITE_SHELL_FIDDLE
25469 ".output ?FILE? Send output to FILE or stdout if FILE is omitted",
25470 " If FILE begins with '|' then open it as a pipe.",
25471 " If FILE is 'off' then output is disabled.",
25472 " Options:",
25473 " --bom Prefix output with a UTF8 byte-order mark",
25474 " -e Send output to the system text editor",
25475 " --plain Use text/plain for -w option",
25476 " -w Send output to a web browser",
@@ -27021,11 +27084,11 @@
27084 for(i=0; i<pgSz; i+=16){
27085 const u8 *aLine = aData+i;
27086 for(j=0; j<16 && aLine[j]==0; j++){}
27087 if( j==16 ) continue;
27088 if( !seenPageLabel ){
27089 sqlite3_fprintf(p->out, "| page %lld offset %lld\n",pgno,(pgno-1)*pgSz);
27090 seenPageLabel = 1;
27091 }
27092 sqlite3_fprintf(p->out, "| %5d:", i);
27093 for(j=0; j<16; j++) sqlite3_fprintf(p->out, " %02x", aLine[j]);
27094 sqlite3_fprintf(p->out, " ");
@@ -30399,13 +30462,13 @@
30462 || (c=='e' && n==5 && cli_strcmp(azArg[0],"excel")==0)
30463 || (c=='w' && n==3 && cli_strcmp(azArg[0],"www")==0)
30464 ){
30465 char *zFile = 0;
30466 int i;
30467 int eMode = 0; /* 0: .outout/.once, 'x'=.excel, 'w'=.www */
30468 int bOnce = 0; /* 0: .output, 1: .once, 2: .excel/.www */
30469 int bPlain = 0; /* --plain option */
30470 static const char *zBomUtf8 = "\357\273\277";
30471 const char *zBom = 0;
30472
30473 failIfSafeMode(p, "cannot run .%s in safe mode", azArg[0]);
30474 if( c=='e' ){
@@ -30430,18 +30493,26 @@
30493 }else if( c=='o' && cli_strcmp(z,"-e")==0 ){
30494 eMode = 'e'; /* text editor */
30495 }else if( c=='o' && cli_strcmp(z,"-w")==0 ){
30496 eMode = 'w'; /* Web browser */
30497 }else{
30498 sqlite3_fprintf(p->out,
30499 "ERROR: unknown option: \"%s\". Usage:\n", azArg[i]);
30500 showHelp(p->out, azArg[0]);
30501 rc = 1;
30502 goto meta_command_exit;
30503 }
30504 }else if( zFile==0 && eMode==0 ){
30505 if( cli_strcmp(z, "off")==0 ){
30506 #ifdef _WIN32
30507 zFile = sqlite3_mprintf("nul");
30508 #else
30509 zFile = sqlite3_mprintf("/dev/null");
30510 #endif
30511 }else{
30512 zFile = sqlite3_mprintf("%s", z);
30513 }
30514 if( zFile && zFile[0]=='|' ){
30515 while( i+1<nArg ) zFile = sqlite3_mprintf("%z %s", zFile, azArg[++i]);
30516 break;
30517 }
30518 }else{
30519
+588 -151
--- extsrc/sqlite3.c
+++ extsrc/sqlite3.c
@@ -16,11 +16,11 @@
1616
** if you want a wrapper to interface SQLite with your choice of programming
1717
** language. The code for the "sqlite3" command-line shell is also in a
1818
** separate file. This file contains only code for the core SQLite library.
1919
**
2020
** The content in this amalgamation comes from Fossil check-in
21
-** 18bda13e197e4b4ec7464b3e70012f71edc0 with changes in files:
21
+** d22475b81c4e26ccc50f3b5626d43b32f7a2 with changes in files:
2222
**
2323
**
2424
*/
2525
#ifndef SQLITE_AMALGAMATION
2626
#define SQLITE_CORE 1
@@ -450,11 +450,11 @@
450450
** be held constant and Z will be incremented or else Y will be incremented
451451
** and Z will be reset to zero.
452452
**
453453
** Since [version 3.6.18] ([dateof:3.6.18]),
454454
** SQLite source code has been stored in the
455
-** <a href="http://www.fossil-scm.org/">Fossil configuration management
455
+** <a href="http://fossil-scm.org/">Fossil configuration management
456456
** system</a>. ^The SQLITE_SOURCE_ID macro evaluates to
457457
** a string which identifies a particular check-in of SQLite
458458
** within its configuration management system. ^The SQLITE_SOURCE_ID
459459
** string contains the date and time of the check-in (UTC) and a SHA1
460460
** or SHA3-256 hash of the entire source tree. If the source code has
@@ -465,11 +465,11 @@
465465
** [sqlite3_libversion_number()], [sqlite3_sourceid()],
466466
** [sqlite_version()] and [sqlite_source_id()].
467467
*/
468468
#define SQLITE_VERSION "3.50.0"
469469
#define SQLITE_VERSION_NUMBER 3050000
470
-#define SQLITE_SOURCE_ID "2025-03-16 00:13:29 18bda13e197e4b4ec7464b3e70012f71edc05f73d8b14bb48bad452f81c7e185"
470
+#define SQLITE_SOURCE_ID "2025-04-15 21:59:38 d22475b81c4e26ccc50f3b5626d43b32f7a2de34e5a764539554665bdda735d5"
471471
472472
/*
473473
** CAPI3REF: Run-Time Library Version Numbers
474474
** KEYWORDS: sqlite3_version sqlite3_sourceid
475475
**
@@ -11946,12 +11946,13 @@
1194611946
** To clarify, if this function is called and then a changeset constructed
1194711947
** using [sqlite3session_changeset()], then after applying that changeset to
1194811948
** database zFrom the contents of the two compatible tables would be
1194911949
** identical.
1195011950
**
11951
-** It an error if database zFrom does not exist or does not contain the
11952
-** required compatible table.
11951
+** Unless the call to this function is a no-op as described above, it is an
11952
+** error if database zFrom does not exist or does not contain the required
11953
+** compatible table.
1195311954
**
1195411955
** If the operation is successful, SQLITE_OK is returned. Otherwise, an SQLite
1195511956
** error code. In this case, if argument pzErrMsg is not NULL, *pzErrMsg
1195611957
** may be set to point to a buffer containing an English language error
1195711958
** message. It is the responsibility of the caller to free this buffer using
@@ -19162,10 +19163,11 @@
1916219163
unsigned noSkipScan:1; /* Do not try to use skip-scan if true */
1916319164
unsigned hasStat1:1; /* aiRowLogEst values come from sqlite_stat1 */
1916419165
unsigned bLowQual:1; /* sqlite_stat1 says this is a low-quality index */
1916519166
unsigned bNoQuery:1; /* Do not use this index to optimize queries */
1916619167
unsigned bAscKeyBug:1; /* True if the bba7b69f9849b5bf bug applies */
19168
+ unsigned bIdxRowid:1; /* One or more of the index keys is the ROWID */
1916719169
unsigned bHasVCol:1; /* Index references one or more VIRTUAL columns */
1916819170
unsigned bHasExpr:1; /* Index contains an expression, either a literal
1916919171
** expression, or a reference to a VIRTUAL column */
1917019172
#ifdef SQLITE_ENABLE_STAT4
1917119173
int nSample; /* Number of elements in aSample[] */
@@ -30270,10 +30272,12 @@
3027030272
*/
3027130273
#include "windows.h"
3027230274
3027330275
#ifdef __CYGWIN__
3027430276
# include <sys/cygwin.h>
30277
+# include <sys/stat.h> /* amalgamator: dontcache */
30278
+# include <unistd.h> /* amalgamator: dontcache */
3027530279
# include <errno.h> /* amalgamator: dontcache */
3027630280
#endif
3027730281
3027830282
/*
3027930283
** Determine if we are dealing with Windows NT.
@@ -35444,11 +35448,11 @@
3544435448
unsigned char const *z = zIn;
3544535449
unsigned char const *zEnd = &z[nByte-1];
3544635450
int n = 0;
3544735451
3544835452
if( SQLITE_UTF16NATIVE==SQLITE_UTF16LE ) z++;
35449
- while( n<nChar && ALWAYS(z<=zEnd) ){
35453
+ while( n<nChar && z<=zEnd ){
3545035454
c = z[0];
3545135455
z += 2;
3545235456
if( c>=0xd8 && c<0xdc && z<=zEnd && z[0]>=0xdc && z[0]<0xe0 ) z += 2;
3545335457
n++;
3545435458
}
@@ -47726,20 +47730,20 @@
4772647730
{ "FileTimeToLocalFileTime", (SYSCALL)FileTimeToLocalFileTime, 0 },
4772747731
#else
4772847732
{ "FileTimeToLocalFileTime", (SYSCALL)0, 0 },
4772947733
#endif
4773047734
47731
-#define osFileTimeToLocalFileTime ((BOOL(WINAPI*)(CONST FILETIME*, \
47735
+#define osFileTimeToLocalFileTime ((BOOL(WINAPI*)(const FILETIME*, \
4773247736
LPFILETIME))aSyscall[11].pCurrent)
4773347737
4773447738
#if SQLITE_OS_WINCE
4773547739
{ "FileTimeToSystemTime", (SYSCALL)FileTimeToSystemTime, 0 },
4773647740
#else
4773747741
{ "FileTimeToSystemTime", (SYSCALL)0, 0 },
4773847742
#endif
4773947743
47740
-#define osFileTimeToSystemTime ((BOOL(WINAPI*)(CONST FILETIME*, \
47744
+#define osFileTimeToSystemTime ((BOOL(WINAPI*)(const FILETIME*, \
4774147745
LPSYSTEMTIME))aSyscall[12].pCurrent)
4774247746
4774347747
{ "FlushFileBuffers", (SYSCALL)FlushFileBuffers, 0 },
4774447748
4774547749
#define osFlushFileBuffers ((BOOL(WINAPI*)(HANDLE))aSyscall[13].pCurrent)
@@ -48015,11 +48019,11 @@
4801548019
{ "LockFile", (SYSCALL)LockFile, 0 },
4801648020
#else
4801748021
{ "LockFile", (SYSCALL)0, 0 },
4801848022
#endif
4801948023
48020
-#ifndef osLockFile
48024
+#if !defined(osLockFile) && defined(SQLITE_WIN32_HAS_ANSI)
4802148025
#define osLockFile ((BOOL(WINAPI*)(HANDLE,DWORD,DWORD,DWORD, \
4802248026
DWORD))aSyscall[47].pCurrent)
4802348027
#endif
4802448028
4802548029
#if !SQLITE_OS_WINCE
@@ -48079,20 +48083,20 @@
4807948083
4808048084
#define osSleep ((VOID(WINAPI*)(DWORD))aSyscall[55].pCurrent)
4808148085
4808248086
{ "SystemTimeToFileTime", (SYSCALL)SystemTimeToFileTime, 0 },
4808348087
48084
-#define osSystemTimeToFileTime ((BOOL(WINAPI*)(CONST SYSTEMTIME*, \
48088
+#define osSystemTimeToFileTime ((BOOL(WINAPI*)(const SYSTEMTIME*, \
4808548089
LPFILETIME))aSyscall[56].pCurrent)
4808648090
4808748091
#if !SQLITE_OS_WINCE && !SQLITE_OS_WINRT
4808848092
{ "UnlockFile", (SYSCALL)UnlockFile, 0 },
4808948093
#else
4809048094
{ "UnlockFile", (SYSCALL)0, 0 },
4809148095
#endif
4809248096
48093
-#ifndef osUnlockFile
48097
+#if !defined(osUnlockFile) && defined(SQLITE_WIN32_HAS_ANSI)
4809448098
#define osUnlockFile ((BOOL(WINAPI*)(HANDLE,DWORD,DWORD,DWORD, \
4809548099
DWORD))aSyscall[57].pCurrent)
4809648100
#endif
4809748101
4809848102
#if !SQLITE_OS_WINCE
@@ -48316,10 +48320,67 @@
4831648320
{ "CancelIo", (SYSCALL)0, 0 },
4831748321
#endif
4831848322
4831948323
#define osCancelIo ((BOOL(WINAPI*)(HANDLE))aSyscall[81].pCurrent)
4832048324
48325
+#if defined(SQLITE_WIN32_HAS_WIDE) && defined(_WIN32)
48326
+ { "GetModuleHandleW", (SYSCALL)GetModuleHandleW, 0 },
48327
+#else
48328
+ { "GetModuleHandleW", (SYSCALL)0, 0 },
48329
+#endif
48330
+
48331
+#define osGetModuleHandleW ((HMODULE(WINAPI*)(LPCWSTR))aSyscall[82].pCurrent)
48332
+
48333
+#ifndef _WIN32
48334
+ { "getenv", (SYSCALL)getenv, 0 },
48335
+#else
48336
+ { "getenv", (SYSCALL)0, 0 },
48337
+#endif
48338
+
48339
+#define osGetenv ((const char *(*)(const char *))aSyscall[83].pCurrent)
48340
+
48341
+#ifndef _WIN32
48342
+ { "getcwd", (SYSCALL)getcwd, 0 },
48343
+#else
48344
+ { "getcwd", (SYSCALL)0, 0 },
48345
+#endif
48346
+
48347
+#define osGetcwd ((char*(*)(char*,size_t))aSyscall[84].pCurrent)
48348
+
48349
+#ifndef _WIN32
48350
+ { "readlink", (SYSCALL)readlink, 0 },
48351
+#else
48352
+ { "readlink", (SYSCALL)0, 0 },
48353
+#endif
48354
+
48355
+#define osReadlink ((ssize_t(*)(const char*,char*,size_t))aSyscall[85].pCurrent)
48356
+
48357
+#ifndef _WIN32
48358
+ { "lstat", (SYSCALL)lstat, 0 },
48359
+#else
48360
+ { "lstat", (SYSCALL)0, 0 },
48361
+#endif
48362
+
48363
+#define osLstat ((int(*)(const char*,struct stat*))aSyscall[86].pCurrent)
48364
+
48365
+#ifndef _WIN32
48366
+ { "__errno", (SYSCALL)__errno, 0 },
48367
+#else
48368
+ { "__errno", (SYSCALL)0, 0 },
48369
+#endif
48370
+
48371
+#define osErrno (*((int*(*)(void))aSyscall[87].pCurrent)())
48372
+
48373
+#ifndef _WIN32
48374
+ { "cygwin_conv_path", (SYSCALL)cygwin_conv_path, 0 },
48375
+#else
48376
+ { "cygwin_conv_path", (SYSCALL)0, 0 },
48377
+#endif
48378
+
48379
+#define osCygwin_conv_path ((size_t(*)(unsigned int, \
48380
+ const void *, void *, size_t))aSyscall[88].pCurrent)
48381
+
4832148382
}; /* End of the overrideable system calls */
4832248383
4832348384
/*
4832448385
** This is the xSetSystemCall() method of sqlite3_vfs for all of the
4832548386
** "win32" VFSes. Return SQLITE_OK upon successfully updating the
@@ -48489,10 +48550,11 @@
4848948550
sqlite3_mutex_leave(pMainMtx);
4849048551
return rc;
4849148552
}
4849248553
#endif /* SQLITE_WIN32_MALLOC */
4849348554
48555
+#ifdef _WIN32
4849448556
/*
4849548557
** This function outputs the specified (ANSI) string to the Win32 debugger
4849648558
** (if available).
4849748559
*/
4849848560
@@ -48531,10 +48593,11 @@
4853148593
}else{
4853248594
fprintf(stderr, "%s", zBuf);
4853348595
}
4853448596
#endif
4853548597
}
48598
+#endif /* _WIN32 */
4853648599
4853748600
/*
4853848601
** The following routine suspends the current thread for at least ms
4853948602
** milliseconds. This is equivalent to the Win32 Sleep() interface.
4854048603
*/
@@ -48831,10 +48894,11 @@
4883148894
SQLITE_PRIVATE void sqlite3MemSetDefault(void){
4883248895
sqlite3_config(SQLITE_CONFIG_MALLOC, sqlite3MemGetWin32());
4883348896
}
4883448897
#endif /* SQLITE_WIN32_MALLOC */
4883548898
48899
+#ifdef _WIN32
4883648900
/*
4883748901
** Convert a UTF-8 string to Microsoft Unicode.
4883848902
**
4883948903
** Space to hold the returned string is obtained from sqlite3_malloc().
4884048904
*/
@@ -48856,10 +48920,11 @@
4885648920
sqlite3_free(zWideText);
4885748921
zWideText = 0;
4885848922
}
4885948923
return zWideText;
4886048924
}
48925
+#endif /* _WIN32 */
4886148926
4886248927
/*
4886348928
** Convert a Microsoft Unicode string to UTF-8.
4886448929
**
4886548930
** Space to hold the returned string is obtained from sqlite3_malloc().
@@ -48890,32 +48955,33 @@
4889048955
** code page.
4889148956
**
4889248957
** Space to hold the returned string is obtained from sqlite3_malloc().
4889348958
*/
4889448959
static LPWSTR winMbcsToUnicode(const char *zText, int useAnsi){
48895
- int nByte;
48960
+ int nWideChar;
4889648961
LPWSTR zMbcsText;
4889748962
int codepage = useAnsi ? CP_ACP : CP_OEMCP;
4889848963
48899
- nByte = osMultiByteToWideChar(codepage, 0, zText, -1, NULL,
48900
- 0)*sizeof(WCHAR);
48901
- if( nByte==0 ){
48964
+ nWideChar = osMultiByteToWideChar(codepage, 0, zText, -1, NULL,
48965
+ 0);
48966
+ if( nWideChar==0 ){
4890248967
return 0;
4890348968
}
48904
- zMbcsText = sqlite3MallocZero( nByte*sizeof(WCHAR) );
48969
+ zMbcsText = sqlite3MallocZero( nWideChar*sizeof(WCHAR) );
4890548970
if( zMbcsText==0 ){
4890648971
return 0;
4890748972
}
48908
- nByte = osMultiByteToWideChar(codepage, 0, zText, -1, zMbcsText,
48909
- nByte);
48910
- if( nByte==0 ){
48973
+ nWideChar = osMultiByteToWideChar(codepage, 0, zText, -1, zMbcsText,
48974
+ nWideChar);
48975
+ if( nWideChar==0 ){
4891148976
sqlite3_free(zMbcsText);
4891248977
zMbcsText = 0;
4891348978
}
4891448979
return zMbcsText;
4891548980
}
4891648981
48982
+#ifdef _WIN32
4891748983
/*
4891848984
** Convert a Microsoft Unicode string to a multi-byte character string,
4891948985
** using the ANSI or OEM code page.
4892048986
**
4892148987
** Space to hold the returned string is obtained from sqlite3_malloc().
@@ -48939,10 +49005,11 @@
4893949005
sqlite3_free(zText);
4894049006
zText = 0;
4894149007
}
4894249008
return zText;
4894349009
}
49010
+#endif /* _WIN32 */
4894449011
4894549012
/*
4894649013
** Convert a multi-byte character string to UTF-8.
4894749014
**
4894849015
** Space to hold the returned string is obtained from sqlite3_malloc().
@@ -48958,10 +49025,11 @@
4895849025
zTextUtf8 = winUnicodeToUtf8(zTmpWide);
4895949026
sqlite3_free(zTmpWide);
4896049027
return zTextUtf8;
4896149028
}
4896249029
49030
+#ifdef _WIN32
4896349031
/*
4896449032
** Convert a UTF-8 string to a multi-byte character string.
4896549033
**
4896649034
** Space to hold the returned string is obtained from sqlite3_malloc().
4896749035
*/
@@ -49007,10 +49075,11 @@
4900749075
#ifndef SQLITE_OMIT_AUTOINIT
4900849076
if( sqlite3_initialize() ) return 0;
4900949077
#endif
4901049078
return winUnicodeToUtf8(zWideText);
4901149079
}
49080
+#endif /* _WIN32 */
4901249081
4901349082
/*
4901449083
** This is a public wrapper for the winMbcsToUtf8() function.
4901549084
*/
4901649085
SQLITE_API char *sqlite3_win32_mbcs_to_utf8(const char *zText){
@@ -49024,10 +49093,11 @@
4902449093
if( sqlite3_initialize() ) return 0;
4902549094
#endif
4902649095
return winMbcsToUtf8(zText, osAreFileApisANSI());
4902749096
}
4902849097
49098
+#ifdef _WIN32
4902949099
/*
4903049100
** This is a public wrapper for the winMbcsToUtf8() function.
4903149101
*/
4903249102
SQLITE_API char *sqlite3_win32_mbcs_to_utf8_v2(const char *zText, int useAnsi){
4903349103
#ifdef SQLITE_ENABLE_API_ARMOR
@@ -49148,10 +49218,11 @@
4914849218
unsigned long type, /* Identifier for directory being set or reset */
4914949219
void *zValue /* New value for directory being set or reset */
4915049220
){
4915149221
return sqlite3_win32_set_directory16(type, zValue);
4915249222
}
49223
+#endif /* _WIN32 */
4915349224
4915449225
/*
4915549226
** The return value of winGetLastErrorMsg
4915649227
** is zero if the error message fits in the buffer, or non-zero
4915749228
** otherwise (if the message was truncated).
@@ -49696,13 +49767,15 @@
4969649767
OVERLAPPED ovlp;
4969749768
memset(&ovlp, 0, sizeof(OVERLAPPED));
4969849769
ovlp.Offset = offsetLow;
4969949770
ovlp.OffsetHigh = offsetHigh;
4970049771
return osLockFileEx(*phFile, flags, 0, numBytesLow, numBytesHigh, &ovlp);
49772
+#ifdef SQLITE_WIN32_HAS_ANSI
4970149773
}else{
4970249774
return osLockFile(*phFile, offsetLow, offsetHigh, numBytesLow,
4970349775
numBytesHigh);
49776
+#endif
4970449777
}
4970549778
#endif
4970649779
}
4970749780
4970849781
/*
@@ -49806,13 +49879,15 @@
4980649879
OVERLAPPED ovlp;
4980749880
memset(&ovlp, 0, sizeof(OVERLAPPED));
4980849881
ovlp.Offset = offsetLow;
4980949882
ovlp.OffsetHigh = offsetHigh;
4981049883
return osUnlockFileEx(*phFile, 0, numBytesLow, numBytesHigh, &ovlp);
49884
+#ifdef SQLITE_WIN32_HAS_ANSI
4981149885
}else{
4981249886
return osUnlockFile(*phFile, offsetLow, offsetHigh, numBytesLow,
4981349887
numBytesHigh);
49888
+#endif
4981449889
}
4981549890
#endif
4981649891
}
4981749892
4981849893
/*
@@ -51222,18 +51297,95 @@
5122251297
5122351298
/*
5122451299
** Convert a UTF-8 filename into whatever form the underlying
5122551300
** operating system wants filenames in. Space to hold the result
5122651301
** is obtained from malloc and must be freed by the calling
51227
-** function.
51302
+** function
51303
+**
51304
+** On Cygwin, 3 possible input forms are accepted:
51305
+** - If the filename starts with "<drive>:/" or "<drive>:\",
51306
+** it is converted to UTF-16 as-is.
51307
+** - If the filename contains '/', it is assumed to be a
51308
+** Cygwin absolute path, it is converted to a win32
51309
+** absolute path in UTF-16.
51310
+** - Otherwise it must be a filename only, the win32 filename
51311
+** is returned in UTF-16.
51312
+** Note: If the function cygwin_conv_path() fails, only
51313
+** UTF-8 -> UTF-16 conversion will be done. This can only
51314
+** happen when the file path >32k, in which case winUtf8ToUnicode()
51315
+** will fail too.
5122851316
*/
5122951317
static void *winConvertFromUtf8Filename(const char *zFilename){
5123051318
void *zConverted = 0;
5123151319
if( osIsNT() ){
51320
+#ifdef __CYGWIN__
51321
+ int nChar;
51322
+ LPWSTR zWideFilename;
51323
+
51324
+ if( osCygwin_conv_path && !(winIsDriveLetterAndColon(zFilename)
51325
+ && winIsDirSep(zFilename[2])) ){
51326
+ int nByte;
51327
+ int convertflag = CCP_POSIX_TO_WIN_W;
51328
+ if( !strchr(zFilename, '/') ) convertflag |= CCP_RELATIVE;
51329
+ nByte = (int)osCygwin_conv_path(convertflag,
51330
+ zFilename, 0, 0);
51331
+ if( nByte>0 ){
51332
+ zConverted = sqlite3MallocZero(nByte+12);
51333
+ if ( zConverted==0 ){
51334
+ return zConverted;
51335
+ }
51336
+ zWideFilename = zConverted;
51337
+ /* Filenames should be prefixed, except when converted
51338
+ * full path already starts with "\\?\". */
51339
+ if( osCygwin_conv_path(convertflag, zFilename,
51340
+ zWideFilename+4, nByte)==0 ){
51341
+ if( (convertflag&CCP_RELATIVE) ){
51342
+ memmove(zWideFilename, zWideFilename+4, nByte);
51343
+ }else if( memcmp(zWideFilename+4, L"\\\\", 4) ){
51344
+ memcpy(zWideFilename, L"\\\\?\\", 8);
51345
+ }else if( zWideFilename[6]!='?' ){
51346
+ memmove(zWideFilename+6, zWideFilename+4, nByte);
51347
+ memcpy(zWideFilename, L"\\\\?\\UNC", 14);
51348
+ }else{
51349
+ memmove(zWideFilename, zWideFilename+4, nByte);
51350
+ }
51351
+ return zConverted;
51352
+ }
51353
+ sqlite3_free(zConverted);
51354
+ }
51355
+ }
51356
+ nChar = osMultiByteToWideChar(CP_UTF8, 0, zFilename, -1, NULL, 0);
51357
+ if( nChar==0 ){
51358
+ return 0;
51359
+ }
51360
+ zWideFilename = sqlite3MallocZero( nChar*sizeof(WCHAR)+12 );
51361
+ if( zWideFilename==0 ){
51362
+ return 0;
51363
+ }
51364
+ nChar = osMultiByteToWideChar(CP_UTF8, 0, zFilename, -1,
51365
+ zWideFilename, nChar);
51366
+ if( nChar==0 ){
51367
+ sqlite3_free(zWideFilename);
51368
+ zWideFilename = 0;
51369
+ }else if( nChar>MAX_PATH
51370
+ && winIsDriveLetterAndColon(zFilename)
51371
+ && winIsDirSep(zFilename[2]) ){
51372
+ memmove(zWideFilename+4, zWideFilename, nChar*sizeof(WCHAR));
51373
+ zWideFilename[2] = '\\';
51374
+ memcpy(zWideFilename, L"\\\\?\\", 8);
51375
+ }else if( nChar>MAX_PATH
51376
+ && winIsDirSep(zFilename[0]) && winIsDirSep(zFilename[1])
51377
+ && zFilename[2] != '?' ){
51378
+ memmove(zWideFilename+6, zWideFilename, nChar*sizeof(WCHAR));
51379
+ memcpy(zWideFilename, L"\\\\?\\UNC", 14);
51380
+ }
51381
+ zConverted = zWideFilename;
51382
+#else
5123251383
zConverted = winUtf8ToUnicode(zFilename);
51384
+#endif /* __CYGWIN__ */
5123351385
}
51234
-#ifdef SQLITE_WIN32_HAS_ANSI
51386
+#if defined(SQLITE_WIN32_HAS_ANSI) && defined(_WIN32)
5123551387
else{
5123651388
zConverted = winUtf8ToMbcs(zFilename, osAreFileApisANSI());
5123751389
}
5123851390
#endif
5123951391
/* caller will handle out of memory */
@@ -52058,11 +52210,11 @@
5205852210
**
5205952211
** This division contains the implementation of methods on the
5206052212
** sqlite3_vfs object.
5206152213
*/
5206252214
52063
-#if defined(__CYGWIN__)
52215
+#if 0 /* No longer necessary */
5206452216
/*
5206552217
** Convert a filename from whatever the underlying operating system
5206652218
** supports for filenames into UTF-8. Space to hold the result is
5206752219
** obtained from malloc and must be freed by the calling function.
5206852220
*/
@@ -52091,11 +52243,18 @@
5209152243
int nLen = sqlite3Strlen30(zBuf);
5209252244
if( nLen>0 ){
5209352245
if( winIsDirSep(zBuf[nLen-1]) ){
5209452246
return 1;
5209552247
}else if( nLen+1<nBuf ){
52096
- zBuf[nLen] = winGetDirSep();
52248
+ if( !osGetenv ){
52249
+ zBuf[nLen] = winGetDirSep();
52250
+ }else if( winIsDriveLetterAndColon(zBuf) && winIsDirSep(zBuf[2]) ){
52251
+ zBuf[nLen] = '\\';
52252
+ zBuf[2]='\\';
52253
+ }else{
52254
+ zBuf[nLen] = '/';
52255
+ }
5209752256
zBuf[nLen+1] = '\0';
5209852257
return 1;
5209952258
}
5210052259
}
5210152260
}
@@ -52118,11 +52277,11 @@
5211852277
/*
5211952278
** Create a temporary file name and store the resulting pointer into pzBuf.
5212052279
** The pointer returned in pzBuf must be freed via sqlite3_free().
5212152280
*/
5212252281
static int winGetTempname(sqlite3_vfs *pVfs, char **pzBuf){
52123
- static char zChars[] =
52282
+ static const char zChars[] =
5212452283
"abcdefghijklmnopqrstuvwxyz"
5212552284
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
5212652285
"0123456789";
5212752286
size_t i, j;
5212852287
DWORD pid;
@@ -52169,11 +52328,11 @@
5216952328
}
5217052329
sqlite3_mutex_leave(sqlite3MutexAlloc(SQLITE_MUTEX_STATIC_TEMPDIR));
5217152330
}
5217252331
5217352332
#if defined(__CYGWIN__)
52174
- else{
52333
+ else if( osGetenv!=NULL ){
5217552334
static const char *azDirs[] = {
5217652335
0, /* getenv("SQLITE_TMPDIR") */
5217752336
0, /* getenv("TMPDIR") */
5217852337
0, /* getenv("TMP") */
5217952338
0, /* getenv("TEMP") */
@@ -52185,24 +52344,24 @@
5218552344
0 /* List terminator */
5218652345
};
5218752346
unsigned int i;
5218852347
const char *zDir = 0;
5218952348
52190
- if( !azDirs[0] ) azDirs[0] = getenv("SQLITE_TMPDIR");
52191
- if( !azDirs[1] ) azDirs[1] = getenv("TMPDIR");
52192
- if( !azDirs[2] ) azDirs[2] = getenv("TMP");
52193
- if( !azDirs[3] ) azDirs[3] = getenv("TEMP");
52194
- if( !azDirs[4] ) azDirs[4] = getenv("USERPROFILE");
52349
+ if( !azDirs[0] ) azDirs[0] = osGetenv("SQLITE_TMPDIR");
52350
+ if( !azDirs[1] ) azDirs[1] = osGetenv("TMPDIR");
52351
+ if( !azDirs[2] ) azDirs[2] = osGetenv("TMP");
52352
+ if( !azDirs[3] ) azDirs[3] = osGetenv("TEMP");
52353
+ if( !azDirs[4] ) azDirs[4] = osGetenv("USERPROFILE");
5219552354
for(i=0; i<sizeof(azDirs)/sizeof(azDirs[0]); zDir=azDirs[i++]){
5219652355
void *zConverted;
5219752356
if( zDir==0 ) continue;
5219852357
/* If the path starts with a drive letter followed by the colon
5219952358
** character, assume it is already a native Win32 path; otherwise,
5220052359
** it must be converted to a native Win32 path via the Cygwin API
5220152360
** prior to using it.
5220252361
*/
52203
- if( winIsDriveLetterAndColon(zDir) ){
52362
+ {
5220452363
zConverted = winConvertFromUtf8Filename(zDir);
5220552364
if( !zConverted ){
5220652365
sqlite3_free(zBuf);
5220752366
OSTRACE(("TEMP-FILENAME rc=SQLITE_IOERR_NOMEM\n"));
5220852367
return SQLITE_IOERR_NOMEM_BKPT;
@@ -52211,19 +52370,20 @@
5221152370
sqlite3_snprintf(nMax, zBuf, "%s", zDir);
5221252371
sqlite3_free(zConverted);
5221352372
break;
5221452373
}
5221552374
sqlite3_free(zConverted);
52375
+#if 0 /* No longer necessary */
5221652376
}else{
5221752377
zConverted = sqlite3MallocZero( nMax+1 );
5221852378
if( !zConverted ){
5221952379
sqlite3_free(zBuf);
5222052380
OSTRACE(("TEMP-FILENAME rc=SQLITE_IOERR_NOMEM\n"));
5222152381
return SQLITE_IOERR_NOMEM_BKPT;
5222252382
}
52223
- if( cygwin_conv_path(
52224
- osIsNT() ? CCP_POSIX_TO_WIN_W : CCP_POSIX_TO_WIN_A, zDir,
52383
+ if( osCygwin_conv_path(
52384
+ CCP_POSIX_TO_WIN_W, zDir,
5222552385
zConverted, nMax+1)<0 ){
5222652386
sqlite3_free(zConverted);
5222752387
sqlite3_free(zBuf);
5222852388
OSTRACE(("TEMP-FILENAME rc=SQLITE_IOERR_CONVPATH\n"));
5222952389
return winLogError(SQLITE_IOERR_CONVPATH, (DWORD)errno,
@@ -52245,14 +52405,17 @@
5224552405
sqlite3_free(zUtf8);
5224652406
sqlite3_free(zConverted);
5224752407
break;
5224852408
}
5224952409
sqlite3_free(zConverted);
52410
+#endif /* No longer necessary */
5225052411
}
5225152412
}
5225252413
}
52253
-#elif !SQLITE_OS_WINRT && !defined(__CYGWIN__)
52414
+#endif
52415
+
52416
+#if !SQLITE_OS_WINRT && defined(_WIN32)
5225452417
else if( osIsNT() ){
5225552418
char *zMulti;
5225652419
LPWSTR zWidePath = sqlite3MallocZero( nMax*sizeof(WCHAR) );
5225752420
if( !zWidePath ){
5225852421
sqlite3_free(zBuf);
@@ -52372,11 +52535,11 @@
5237252535
&sAttrData)) && winRetryIoerr(&cnt, &lastErrno) ){}
5237352536
if( !rc ){
5237452537
return 0; /* Invalid name? */
5237552538
}
5237652539
attr = sAttrData.dwFileAttributes;
52377
-#if SQLITE_OS_WINCE==0
52540
+#if SQLITE_OS_WINCE==0 && defined(SQLITE_WIN32_HAS_ANSI)
5237852541
}else{
5237952542
attr = osGetFileAttributesA((char*)zConverted);
5238052543
#endif
5238152544
}
5238252545
return (attr!=INVALID_FILE_ATTRIBUTES) && (attr&FILE_ATTRIBUTE_DIRECTORY);
@@ -52388,10 +52551,16 @@
5238852551
const char *zFilename, /* Name of file to check */
5238952552
int flags, /* Type of test to make on this file */
5239052553
int *pResOut /* OUT: Result */
5239152554
);
5239252555
52556
+/*
52557
+** The Windows version of xAccess() accepts an extra bit in the flags
52558
+** parameter that prevents an anti-virus retry loop.
52559
+*/
52560
+#define NORETRY 0x4000
52561
+
5239352562
/*
5239452563
** Open a file.
5239552564
*/
5239652565
static int winOpen(
5239752566
sqlite3_vfs *pVfs, /* Used to get maximum path length and AppData */
@@ -52412,10 +52581,11 @@
5241252581
winVfsAppData *pAppData;
5241352582
winFile *pFile = (winFile*)id;
5241452583
void *zConverted; /* Filename in OS encoding */
5241552584
const char *zUtf8Name = zName; /* Filename in UTF-8 encoding */
5241652585
int cnt = 0;
52586
+ int isRO = 0; /* file is known to be accessible readonly */
5241752587
5241852588
/* If argument zPath is a NULL pointer, this function is required to open
5241952589
** a temporary file. Use this buffer to store the file name in.
5242052590
*/
5242152591
char *zTmpname = 0; /* For temporary filename, if necessary. */
@@ -52576,13 +52746,13 @@
5257652746
dwShareMode,
5257752747
dwCreationDisposition,
5257852748
&extendedParameters);
5257952749
if( h!=INVALID_HANDLE_VALUE ) break;
5258052750
if( isReadWrite ){
52581
- int rc2, isRO = 0;
52751
+ int rc2;
5258252752
sqlite3BeginBenignMalloc();
52583
- rc2 = winAccess(pVfs, zUtf8Name, SQLITE_ACCESS_READ, &isRO);
52753
+ rc2 = winAccess(pVfs, zUtf8Name, SQLITE_ACCESS_READ|NORETRY, &isRO);
5258452754
sqlite3EndBenignMalloc();
5258552755
if( rc2==SQLITE_OK && isRO ) break;
5258652756
}
5258752757
}while( winRetryIoerr(&cnt, &lastErrno) );
5258852758
#else
@@ -52593,13 +52763,13 @@
5259352763
dwCreationDisposition,
5259452764
dwFlagsAndAttributes,
5259552765
NULL);
5259652766
if( h!=INVALID_HANDLE_VALUE ) break;
5259752767
if( isReadWrite ){
52598
- int rc2, isRO = 0;
52768
+ int rc2;
5259952769
sqlite3BeginBenignMalloc();
52600
- rc2 = winAccess(pVfs, zUtf8Name, SQLITE_ACCESS_READ, &isRO);
52770
+ rc2 = winAccess(pVfs, zUtf8Name, SQLITE_ACCESS_READ|NORETRY, &isRO);
5260152771
sqlite3EndBenignMalloc();
5260252772
if( rc2==SQLITE_OK && isRO ) break;
5260352773
}
5260452774
}while( winRetryIoerr(&cnt, &lastErrno) );
5260552775
#endif
@@ -52613,13 +52783,13 @@
5261352783
dwCreationDisposition,
5261452784
dwFlagsAndAttributes,
5261552785
NULL);
5261652786
if( h!=INVALID_HANDLE_VALUE ) break;
5261752787
if( isReadWrite ){
52618
- int rc2, isRO = 0;
52788
+ int rc2;
5261952789
sqlite3BeginBenignMalloc();
52620
- rc2 = winAccess(pVfs, zUtf8Name, SQLITE_ACCESS_READ, &isRO);
52790
+ rc2 = winAccess(pVfs, zUtf8Name, SQLITE_ACCESS_READ|NORETRY, &isRO);
5262152791
sqlite3EndBenignMalloc();
5262252792
if( rc2==SQLITE_OK && isRO ) break;
5262352793
}
5262452794
}while( winRetryIoerr(&cnt, &lastErrno) );
5262552795
}
@@ -52630,11 +52800,11 @@
5263052800
dwDesiredAccess, (h==INVALID_HANDLE_VALUE) ? "failed" : "ok"));
5263152801
5263252802
if( h==INVALID_HANDLE_VALUE ){
5263352803
sqlite3_free(zConverted);
5263452804
sqlite3_free(zTmpname);
52635
- if( isReadWrite && !isExclusive ){
52805
+ if( isReadWrite && isRO && !isExclusive ){
5263652806
return winOpen(pVfs, zName, id,
5263752807
((flags|SQLITE_OPEN_READONLY) &
5263852808
~(SQLITE_OPEN_CREATE|SQLITE_OPEN_READWRITE)),
5263952809
pOutFlags);
5264052810
}else{
@@ -52832,11 +53002,17 @@
5283253002
){
5283353003
DWORD attr;
5283453004
int rc = 0;
5283553005
DWORD lastErrno = 0;
5283653006
void *zConverted;
53007
+ int noRetry = 0; /* Do not use winRetryIoerr() */
5283753008
UNUSED_PARAMETER(pVfs);
53009
+
53010
+ if( (flags & NORETRY)!=0 ){
53011
+ noRetry = 1;
53012
+ flags &= ~NORETRY;
53013
+ }
5283853014
5283953015
SimulateIOError( return SQLITE_IOERR_ACCESS; );
5284053016
OSTRACE(("ACCESS name=%s, flags=%x, pResOut=%p\n",
5284153017
zFilename, flags, pResOut));
5284253018
@@ -52856,11 +53032,14 @@
5285653032
int cnt = 0;
5285753033
WIN32_FILE_ATTRIBUTE_DATA sAttrData;
5285853034
memset(&sAttrData, 0, sizeof(sAttrData));
5285953035
while( !(rc = osGetFileAttributesExW((LPCWSTR)zConverted,
5286053036
GetFileExInfoStandard,
52861
- &sAttrData)) && winRetryIoerr(&cnt, &lastErrno) ){}
53037
+ &sAttrData))
53038
+ && !noRetry
53039
+ && winRetryIoerr(&cnt, &lastErrno)
53040
+ ){ /* Loop until true */}
5286253041
if( rc ){
5286353042
/* For an SQLITE_ACCESS_EXISTS query, treat a zero-length file
5286453043
** as if it does not exist.
5286553044
*/
5286653045
if( flags==SQLITE_ACCESS_EXISTS
@@ -52924,10 +53103,11 @@
5292453103
const char *zPathname
5292553104
){
5292653105
return ( sqlite3Isalpha(zPathname[0]) && zPathname[1]==':' );
5292753106
}
5292853107
53108
+#ifdef _WIN32
5292953109
/*
5293053110
** Returns non-zero if the specified path name should be used verbatim. If
5293153111
** non-zero is returned from this function, the calling function must simply
5293253112
** use the provided path name verbatim -OR- resolve it into a full path name
5293353113
** using the GetFullPathName Win32 API function (if available).
@@ -52960,10 +53140,74 @@
5296053140
** If we get to this point, the path name should almost certainly be a purely
5296153141
** relative one (i.e. not a UNC name, not absolute, and not volume relative).
5296253142
*/
5296353143
return FALSE;
5296453144
}
53145
+#endif /* _WIN32 */
53146
+
53147
+#ifdef __CYGWIN__
53148
+/*
53149
+** Simplify a filename into its canonical form
53150
+** by making the following changes:
53151
+**
53152
+** * convert any '/' to '\' (win32) or reverse (Cygwin)
53153
+** * removing any trailing and duplicate / (except for UNC paths)
53154
+** * convert /./ into just /
53155
+**
53156
+** Changes are made in-place. Return the new name length.
53157
+**
53158
+** The original filename is in z[0..]. If the path is shortened,
53159
+** no-longer used bytes will be written by '\0'.
53160
+*/
53161
+static void winSimplifyName(char *z){
53162
+ int i, j;
53163
+ for(i=j=0; z[i]; ++i){
53164
+ if( winIsDirSep(z[i]) ){
53165
+#if !defined(SQLITE_TEST)
53166
+ /* Some test-cases assume that "./foo" and "foo" are different */
53167
+ if( z[i+1]=='.' && winIsDirSep(z[i+2]) ){
53168
+ ++i;
53169
+ continue;
53170
+ }
53171
+#endif
53172
+ if( !z[i+1] || (winIsDirSep(z[i+1]) && (i!=0)) ){
53173
+ continue;
53174
+ }
53175
+ z[j++] = osGetenv?'/':'\\';
53176
+ }else{
53177
+ z[j++] = z[i];
53178
+ }
53179
+ }
53180
+ while(j<i) z[j++] = '\0';
53181
+}
53182
+
53183
+#define SQLITE_MAX_SYMLINKS 100
53184
+
53185
+static int mkFullPathname(
53186
+ const char *zPath, /* Input path */
53187
+ char *zOut, /* Output buffer */
53188
+ int nOut /* Allocated size of buffer zOut */
53189
+){
53190
+ int nPath = sqlite3Strlen30(zPath);
53191
+ int iOff = 0;
53192
+ if( zPath[0]!='/' ){
53193
+ if( osGetcwd(zOut, nOut-2)==0 ){
53194
+ return winLogError(SQLITE_CANTOPEN_BKPT, (DWORD)osErrno, "getcwd", zPath);
53195
+ }
53196
+ iOff = sqlite3Strlen30(zOut);
53197
+ zOut[iOff++] = '/';
53198
+ }
53199
+ if( (iOff+nPath+1)>nOut ){
53200
+ /* SQLite assumes that xFullPathname() nul-terminates the output buffer
53201
+ ** even if it returns an error. */
53202
+ zOut[iOff] = '\0';
53203
+ return SQLITE_CANTOPEN_BKPT;
53204
+ }
53205
+ sqlite3_snprintf(nOut-iOff, &zOut[iOff], "%s", zPath);
53206
+ return SQLITE_OK;
53207
+}
53208
+#endif /* __CYGWIN__ */
5296553209
5296653210
/*
5296753211
** Turn a relative pathname into a full pathname. Write the full
5296853212
** pathname into zOut[]. zOut[] will be at least pVfs->mxPathname
5296953213
** bytes in size.
@@ -52972,12 +53216,12 @@
5297253216
sqlite3_vfs *pVfs, /* Pointer to vfs object */
5297353217
const char *zRelative, /* Possibly relative input path */
5297453218
int nFull, /* Size of output buffer in bytes */
5297553219
char *zFull /* Output buffer */
5297653220
){
52977
-#if !SQLITE_OS_WINCE && !SQLITE_OS_WINRT && !defined(__CYGWIN__)
52978
- DWORD nByte;
53221
+#if !SQLITE_OS_WINCE && !SQLITE_OS_WINRT
53222
+ int nByte;
5297953223
void *zConverted;
5298053224
char *zOut;
5298153225
#endif
5298253226
5298353227
/* If this path name begins with "/X:" or "\\?\", where "X" is any
@@ -52986,68 +53230,114 @@
5298653230
if( zRelative[0]=='/' && (winIsDriveLetterAndColon(zRelative+1)
5298753231
|| winIsLongPathPrefix(zRelative+1)) ){
5298853232
zRelative++;
5298953233
}
5299053234
52991
-#if defined(__CYGWIN__)
53235
+ SimulateIOError( return SQLITE_ERROR );
53236
+
53237
+#ifdef __CYGWIN__
53238
+ if( osGetcwd ){
53239
+ zFull[nFull-1] = '\0';
53240
+ if( !winIsDriveLetterAndColon(zRelative) || !winIsDirSep(zRelative[2]) ){
53241
+ int rc = SQLITE_OK;
53242
+ int nLink = 1; /* Number of symbolic links followed so far */
53243
+ const char *zIn = zRelative; /* Input path for each iteration of loop */
53244
+ char *zDel = 0;
53245
+ struct stat buf;
53246
+
53247
+ UNUSED_PARAMETER(pVfs);
53248
+
53249
+ do {
53250
+ /* Call lstat() on path zIn. Set bLink to true if the path is a symbolic
53251
+ ** link, or false otherwise. */
53252
+ int bLink = 0;
53253
+ if( osLstat && osReadlink ) {
53254
+ if( osLstat(zIn, &buf)!=0 ){
53255
+ int myErrno = osErrno;
53256
+ if( myErrno!=ENOENT ){
53257
+ rc = winLogError(SQLITE_CANTOPEN_BKPT, (DWORD)myErrno, "lstat", zIn);
53258
+ }
53259
+ }else{
53260
+ bLink = ((buf.st_mode & 0170000) == 0120000);
53261
+ }
53262
+
53263
+ if( bLink ){
53264
+ if( zDel==0 ){
53265
+ zDel = sqlite3MallocZero(nFull);
53266
+ if( zDel==0 ) rc = SQLITE_NOMEM;
53267
+ }else if( ++nLink>SQLITE_MAX_SYMLINKS ){
53268
+ rc = SQLITE_CANTOPEN_BKPT;
53269
+ }
53270
+
53271
+ if( rc==SQLITE_OK ){
53272
+ nByte = osReadlink(zIn, zDel, nFull-1);
53273
+ if( nByte ==(DWORD)-1 ){
53274
+ rc = winLogError(SQLITE_CANTOPEN_BKPT, (DWORD)osErrno, "readlink", zIn);
53275
+ }else{
53276
+ if( zDel[0]!='/' ){
53277
+ int n;
53278
+ for(n = sqlite3Strlen30(zIn); n>0 && zIn[n-1]!='/'; n--);
53279
+ if( nByte+n+1>nFull ){
53280
+ rc = SQLITE_CANTOPEN_BKPT;
53281
+ }else{
53282
+ memmove(&zDel[n], zDel, nByte+1);
53283
+ memcpy(zDel, zIn, n);
53284
+ nByte += n;
53285
+ }
53286
+ }
53287
+ zDel[nByte] = '\0';
53288
+ }
53289
+ }
53290
+
53291
+ zIn = zDel;
53292
+ }
53293
+ }
53294
+
53295
+ assert( rc!=SQLITE_OK || zIn!=zFull || zIn[0]=='/' );
53296
+ if( rc==SQLITE_OK && zIn!=zFull ){
53297
+ rc = mkFullPathname(zIn, zFull, nFull);
53298
+ }
53299
+ if( bLink==0 ) break;
53300
+ zIn = zFull;
53301
+ }while( rc==SQLITE_OK );
53302
+
53303
+ sqlite3_free(zDel);
53304
+ winSimplifyName(zFull);
53305
+ return rc;
53306
+ }
53307
+ }
53308
+#endif /* __CYGWIN__ */
53309
+#if 0 /* This doesn't work correctly at all! See:
53310
+ <https://marc.info/?l=sqlite-users&m=139299149416314&w=2>
53311
+*/
5299253312
SimulateIOError( return SQLITE_ERROR );
5299353313
UNUSED_PARAMETER(nFull);
5299453314
assert( nFull>=pVfs->mxPathname );
52995
- if ( sqlite3_data_directory && !winIsVerbatimPathname(zRelative) ){
52996
- /*
52997
- ** NOTE: We are dealing with a relative path name and the data
52998
- ** directory has been set. Therefore, use it as the basis
52999
- ** for converting the relative path name to an absolute
53000
- ** one by prepending the data directory and a slash.
53001
- */
53002
- char *zOut = sqlite3MallocZero( 1+(u64)pVfs->mxPathname );
53003
- if( !zOut ){
53004
- return SQLITE_IOERR_NOMEM_BKPT;
53005
- }
53006
- if( cygwin_conv_path(
53007
- (osIsNT() ? CCP_POSIX_TO_WIN_W : CCP_POSIX_TO_WIN_A) |
53008
- CCP_RELATIVE, zRelative, zOut, pVfs->mxPathname+1)<0 ){
53009
- sqlite3_free(zOut);
53010
- return winLogError(SQLITE_CANTOPEN_CONVPATH, (DWORD)errno,
53011
- "winFullPathname1", zRelative);
53012
- }else{
53013
- char *zUtf8 = winConvertToUtf8Filename(zOut);
53014
- if( !zUtf8 ){
53015
- sqlite3_free(zOut);
53016
- return SQLITE_IOERR_NOMEM_BKPT;
53017
- }
53018
- sqlite3_snprintf(MIN(nFull, pVfs->mxPathname), zFull, "%s%c%s",
53019
- sqlite3_data_directory, winGetDirSep(), zUtf8);
53020
- sqlite3_free(zUtf8);
53021
- sqlite3_free(zOut);
53022
- }
53023
- }else{
53024
- char *zOut = sqlite3MallocZero( pVfs->mxPathname+1 );
53025
- if( !zOut ){
53026
- return SQLITE_IOERR_NOMEM_BKPT;
53027
- }
53028
- if( cygwin_conv_path(
53029
- (osIsNT() ? CCP_POSIX_TO_WIN_W : CCP_POSIX_TO_WIN_A),
53030
- zRelative, zOut, pVfs->mxPathname+1)<0 ){
53031
- sqlite3_free(zOut);
53032
- return winLogError(SQLITE_CANTOPEN_CONVPATH, (DWORD)errno,
53033
- "winFullPathname2", zRelative);
53034
- }else{
53035
- char *zUtf8 = winConvertToUtf8Filename(zOut);
53036
- if( !zUtf8 ){
53037
- sqlite3_free(zOut);
53038
- return SQLITE_IOERR_NOMEM_BKPT;
53039
- }
53040
- sqlite3_snprintf(MIN(nFull, pVfs->mxPathname), zFull, "%s", zUtf8);
53041
- sqlite3_free(zUtf8);
53042
- sqlite3_free(zOut);
53043
- }
53044
- }
53045
- return SQLITE_OK;
53046
-#endif
53047
-
53048
-#if (SQLITE_OS_WINCE || SQLITE_OS_WINRT) && !defined(__CYGWIN__)
53315
+ char *zOut = sqlite3MallocZero( pVfs->mxPathname+1 );
53316
+ if( !zOut ){
53317
+ return SQLITE_IOERR_NOMEM_BKPT;
53318
+ }
53319
+ if( osCygwin_conv_path(
53320
+ CCP_POSIX_TO_WIN_W,
53321
+ zRelative, zOut, pVfs->mxPathname+1)<0 ){
53322
+ sqlite3_free(zOut);
53323
+ return winLogError(SQLITE_CANTOPEN_CONVPATH, (DWORD)errno,
53324
+ "winFullPathname2", zRelative);
53325
+ }else{
53326
+ char *zUtf8 = winConvertToUtf8Filename(zOut);
53327
+ if( !zUtf8 ){
53328
+ sqlite3_free(zOut);
53329
+ return SQLITE_IOERR_NOMEM_BKPT;
53330
+ }
53331
+ sqlite3_snprintf(MIN(nFull, pVfs->mxPathname), zFull, "%s", zUtf8);
53332
+ sqlite3_free(zUtf8);
53333
+ sqlite3_free(zOut);
53334
+ }
53335
+ return SQLITE_OK;
53336
+#endif
53337
+
53338
+#if (SQLITE_OS_WINCE || SQLITE_OS_WINRT) && defined(_WIN32)
5304953339
SimulateIOError( return SQLITE_ERROR );
5305053340
/* WinCE has no concept of a relative pathname, or so I am told. */
5305153341
/* WinRT has no way to convert a relative path to an absolute one. */
5305253342
if ( sqlite3_data_directory && !winIsVerbatimPathname(zRelative) ){
5305353343
/*
@@ -53062,11 +53352,12 @@
5306253352
sqlite3_snprintf(MIN(nFull, pVfs->mxPathname), zFull, "%s", zRelative);
5306353353
}
5306453354
return SQLITE_OK;
5306553355
#endif
5306653356
53067
-#if !SQLITE_OS_WINCE && !SQLITE_OS_WINRT && !defined(__CYGWIN__)
53357
+#if !SQLITE_OS_WINCE && !SQLITE_OS_WINRT
53358
+#if defined(_WIN32)
5306853359
/* It's odd to simulate an io-error here, but really this is just
5306953360
** using the io-error infrastructure to test that SQLite handles this
5307053361
** function failing. This function could fail if, for example, the
5307153362
** current working directory has been unlinked.
5307253363
*/
@@ -53080,10 +53371,11 @@
5308053371
*/
5308153372
sqlite3_snprintf(MIN(nFull, pVfs->mxPathname), zFull, "%s%c%s",
5308253373
sqlite3_data_directory, winGetDirSep(), zRelative);
5308353374
return SQLITE_OK;
5308453375
}
53376
+#endif
5308553377
zConverted = winConvertFromUtf8Filename(zRelative);
5308653378
if( zConverted==0 ){
5308753379
return SQLITE_IOERR_NOMEM_BKPT;
5308853380
}
5308953381
if( osIsNT() ){
@@ -53092,16 +53384,17 @@
5309253384
if( nByte==0 ){
5309353385
sqlite3_free(zConverted);
5309453386
return winLogError(SQLITE_CANTOPEN_FULLPATH, osGetLastError(),
5309553387
"winFullPathname1", zRelative);
5309653388
}
53097
- zTemp = sqlite3MallocZero( nByte*sizeof(zTemp[0]) + 3*sizeof(zTemp[0]) );
53389
+ nByte += 3;
53390
+ zTemp = sqlite3MallocZero( nByte*sizeof(zTemp[0]) );
5309853391
if( zTemp==0 ){
5309953392
sqlite3_free(zConverted);
5310053393
return SQLITE_IOERR_NOMEM_BKPT;
5310153394
}
53102
- nByte = osGetFullPathNameW((LPCWSTR)zConverted, nByte+3, zTemp, 0);
53395
+ nByte = osGetFullPathNameW((LPCWSTR)zConverted, nByte, zTemp, 0);
5310353396
if( nByte==0 ){
5310453397
sqlite3_free(zConverted);
5310553398
sqlite3_free(zTemp);
5310653399
return winLogError(SQLITE_CANTOPEN_FULLPATH, osGetLastError(),
5310753400
"winFullPathname2", zRelative);
@@ -53135,11 +53428,30 @@
5313553428
zOut = winMbcsToUtf8(zTemp, osAreFileApisANSI());
5313653429
sqlite3_free(zTemp);
5313753430
}
5313853431
#endif
5313953432
if( zOut ){
53433
+#ifdef __CYGWIN__
53434
+ if( memcmp(zOut, "\\\\?\\", 4) ){
53435
+ sqlite3_snprintf(MIN(nFull, pVfs->mxPathname), zFull, "%s", zOut);
53436
+ }else if( memcmp(zOut+4, "UNC\\", 4) ){
53437
+ sqlite3_snprintf(MIN(nFull, pVfs->mxPathname), zFull, "%s", zOut+4);
53438
+ }else{
53439
+ char *p = zOut+6;
53440
+ *p = '\\';
53441
+ if( osGetcwd ){
53442
+ /* On Cygwin, UNC paths use forward slashes */
53443
+ while( *p ){
53444
+ if( *p=='\\' ) *p = '/';
53445
+ ++p;
53446
+ }
53447
+ }
53448
+ sqlite3_snprintf(MIN(nFull, pVfs->mxPathname), zFull, "%s", zOut+6);
53449
+ }
53450
+#else
5314053451
sqlite3_snprintf(MIN(nFull, pVfs->mxPathname), zFull, "%s", zOut);
53452
+#endif /* __CYGWIN__ */
5314153453
sqlite3_free(zOut);
5314253454
return SQLITE_OK;
5314353455
}else{
5314453456
return SQLITE_IOERR_NOMEM_BKPT;
5314553457
}
@@ -53165,11 +53477,13 @@
5316553477
** Interfaces for opening a shared library, finding entry points
5316653478
** within the shared library, and closing the shared library.
5316753479
*/
5316853480
static void *winDlOpen(sqlite3_vfs *pVfs, const char *zFilename){
5316953481
HANDLE h;
53170
-#if defined(__CYGWIN__)
53482
+#if 0 /* This doesn't work correctly at all! See:
53483
+ <https://marc.info/?l=sqlite-users&m=139299149416314&w=2>
53484
+*/
5317153485
int nFull = pVfs->mxPathname+1;
5317253486
char *zFull = sqlite3MallocZero( nFull );
5317353487
void *zConverted = 0;
5317453488
if( zFull==0 ){
5317553489
OSTRACE(("DLOPEN name=%s, handle=%p\n", zFilename, (void*)0));
@@ -53532,11 +53846,11 @@
5353253846
};
5353353847
#endif
5353453848
5353553849
/* Double-check that the aSyscall[] array has been constructed
5353653850
** correctly. See ticket [bb3a86e890c8e96ab] */
53537
- assert( ArraySize(aSyscall)==82 );
53851
+ assert( ArraySize(aSyscall)==89 );
5353853852
5353953853
/* get memory map allocation granularity */
5354053854
memset(&winSysInfo, 0, sizeof(SYSTEM_INFO));
5354153855
#if SQLITE_OS_WINRT
5354253856
osGetNativeSystemInfo(&winSysInfo);
@@ -66508,14 +66822,12 @@
6650866822
s2 = aIn[1];
6650966823
}else{
6651066824
s1 = s2 = 0;
6651166825
}
6651266826
66513
- assert( nByte>=8 );
66514
- assert( (nByte&0x00000007)==0 );
66515
- assert( nByte<=65536 );
66516
- assert( nByte%4==0 );
66827
+ /* nByte is a multiple of 8 between 8 and 65536 */
66828
+ assert( nByte>=8 && (nByte&7)==0 && nByte<=65536 );
6651766829
6651866830
if( !nativeCksum ){
6651966831
do {
6652066832
s1 += BYTESWAP32(aData[0]) + s2;
6652166833
s2 += BYTESWAP32(aData[1]) + s1;
@@ -83726,11 +84038,11 @@
8372684038
** many different strings can be converted into the same int or real.
8372784039
** If a table contains a numeric value and an index is based on the
8372884040
** corresponding string value, then it is important that the string be
8372984041
** derived from the numeric value, not the other way around, to ensure
8373084042
** that the index and table are consistent. See ticket
83731
-** https://www.sqlite.org/src/info/343634942dd54ab (2018-01-31) for
84043
+** https://sqlite.org/src/info/343634942dd54ab (2018-01-31) for
8373284044
** an example.
8373384045
**
8373484046
** This routine looks at pMem to verify that if it has both a numeric
8373584047
** representation and a string representation then the string rep has
8373684048
** been derived from the numeric and not the other way around. It returns
@@ -93014,11 +93326,11 @@
9301493326
unsigned char enc
9301593327
){
9301693328
assert( xDel!=SQLITE_DYNAMIC );
9301793329
if( enc!=SQLITE_UTF8 ){
9301893330
if( enc==SQLITE_UTF16 ) enc = SQLITE_UTF16NATIVE;
93019
- nData &= ~(u16)1;
93331
+ nData &= ~(u64)1;
9302093332
}
9302193333
return bindText(pStmt, i, zData, nData, xDel, enc);
9302293334
}
9302393335
#ifndef SQLITE_OMIT_UTF16
9302493336
SQLITE_API int sqlite3_bind_text16(
@@ -125166,11 +125478,11 @@
125166125478
if( !isDupColumn(pIdx, pIdx->nKeyCol, pPk, i) ){
125167125479
testcase( hasColumn(pIdx->aiColumn, pIdx->nKeyCol, pPk->aiColumn[i]) );
125168125480
pIdx->aiColumn[j] = pPk->aiColumn[i];
125169125481
pIdx->azColl[j] = pPk->azColl[i];
125170125482
if( pPk->aSortOrder[i] ){
125171
- /* See ticket https://www.sqlite.org/src/info/bba7b69f9849b5bf */
125483
+ /* See ticket https://sqlite.org/src/info/bba7b69f9849b5bf */
125172125484
pIdx->bAscKeyBug = 1;
125173125485
}
125174125486
j++;
125175125487
}
125176125488
}
@@ -126543,11 +126855,11 @@
126543126855
/* This OP_SeekEnd opcode makes index insert for a REINDEX go much
126544126856
** faster by avoiding unnecessary seeks. But the optimization does
126545126857
** not work for UNIQUE constraint indexes on WITHOUT ROWID tables
126546126858
** with DESC primary keys, since those indexes have there keys in
126547126859
** a different order from the main table.
126548
- ** See ticket: https://www.sqlite.org/src/info/bba7b69f9849b5bf
126860
+ ** See ticket: https://sqlite.org/src/info/bba7b69f9849b5bf
126549126861
*/
126550126862
sqlite3VdbeAddOp1(v, OP_SeekEnd, iIdx);
126551126863
}
126552126864
sqlite3VdbeAddOp2(v, OP_IdxInsert, iIdx, regRecord);
126553126865
sqlite3VdbeChangeP5(v, OPFLAG_USESEEKRESULT);
@@ -126927,10 +127239,11 @@
126927127239
}else{
126928127240
j = pCExpr->iColumn;
126929127241
assert( j<=0x7fff );
126930127242
if( j<0 ){
126931127243
j = pTab->iPKey;
127244
+ pIndex->bIdxRowid = 1;
126932127245
}else{
126933127246
if( pTab->aCol[j].notNull==0 ){
126934127247
pIndex->uniqNotNull = 0;
126935127248
}
126936127249
if( pTab->aCol[j].colFlags & COLFLAG_VIRTUAL ){
@@ -136632,11 +136945,11 @@
136632136945
** OE_Update guarantees that only a single row will change, so it
136633136946
** must happen before OE_Replace. Technically, OE_Abort and OE_Rollback
136634136947
** could happen in any order, but they are grouped up front for
136635136948
** convenience.
136636136949
**
136637
- ** 2018-08-14: Ticket https://www.sqlite.org/src/info/908f001483982c43
136950
+ ** 2018-08-14: Ticket https://sqlite.org/src/info/908f001483982c43
136638136951
** The order of constraints used to have OE_Update as (2) and OE_Abort
136639136952
** and so forth as (1). But apparently PostgreSQL checks the OE_Update
136640136953
** constraint before any others, so it had to be moved.
136641136954
**
136642136955
** Constraint checking code is generated in this order:
@@ -147785,10 +148098,11 @@
147785148098
}
147786148099
147787148100
multi_select_end:
147788148101
pDest->iSdst = dest.iSdst;
147789148102
pDest->nSdst = dest.nSdst;
148103
+ pDest->iSDParm2 = dest.iSDParm2;
147790148104
if( pDelete ){
147791148105
sqlite3ParserAddCleanup(pParse, sqlite3SelectDeleteGeneric, pDelete);
147792148106
}
147793148107
return rc;
147794148108
}
@@ -149395,11 +149709,12 @@
149395149709
&& pE2->iColumn==pColumn->iColumn
149396149710
){
149397149711
return; /* Already present. Return without doing anything. */
149398149712
}
149399149713
}
149400
- if( sqlite3ExprAffinity(pColumn)==SQLITE_AFF_BLOB ){
149714
+ assert( SQLITE_AFF_NONE<SQLITE_AFF_BLOB );
149715
+ if( sqlite3ExprAffinity(pColumn)<=SQLITE_AFF_BLOB ){
149401149716
pConst->bHasAffBlob = 1;
149402149717
}
149403149718
149404149719
pConst->nConst++;
149405149720
pConst->apExpr = sqlite3DbReallocOrFree(pConst->pParse->db, pConst->apExpr,
@@ -149470,11 +149785,12 @@
149470149785
for(i=0; i<pConst->nConst; i++){
149471149786
Expr *pColumn = pConst->apExpr[i*2];
149472149787
if( pColumn==pExpr ) continue;
149473149788
if( pColumn->iTable!=pExpr->iTable ) continue;
149474149789
if( pColumn->iColumn!=pExpr->iColumn ) continue;
149475
- if( bIgnoreAffBlob && sqlite3ExprAffinity(pColumn)==SQLITE_AFF_BLOB ){
149790
+ assert( SQLITE_AFF_NONE<SQLITE_AFF_BLOB );
149791
+ if( bIgnoreAffBlob && sqlite3ExprAffinity(pColumn)<=SQLITE_AFF_BLOB ){
149476149792
break;
149477149793
}
149478149794
/* A match is found. Add the EP_FixedCol property */
149479149795
pConst->nChng++;
149480149796
ExprClearProperty(pExpr, EP_Leaf);
@@ -150123,11 +150439,11 @@
150123150439
**
150124150440
** This transformation is necessary because the multiSelectOrderBy() routine
150125150441
** above that generates the code for a compound SELECT with an ORDER BY clause
150126150442
** uses a merge algorithm that requires the same collating sequence on the
150127150443
** result columns as on the ORDER BY clause. See ticket
150128
-** http://www.sqlite.org/src/info/6709574d2a
150444
+** http://sqlite.org/src/info/6709574d2a
150129150445
**
150130150446
** This transformation is only needed for EXCEPT, INTERSECT, and UNION.
150131150447
** The UNION ALL operator works fine with multiSelectOrderBy() even when
150132150448
** there are COLLATE terms in the ORDER BY.
150133150449
*/
@@ -152654,10 +152970,16 @@
152654152970
pWInfo = sqlite3WhereBegin(pParse, pTabList, pWhere, sSort.pOrderBy,
152655152971
p->pEList, p, wctrlFlags, p->nSelectRow);
152656152972
if( pWInfo==0 ) goto select_end;
152657152973
if( sqlite3WhereOutputRowCount(pWInfo) < p->nSelectRow ){
152658152974
p->nSelectRow = sqlite3WhereOutputRowCount(pWInfo);
152975
+ if( pDest->eDest<=SRT_DistQueue && pDest->eDest>=SRT_DistFifo ){
152976
+ /* TUNING: For a UNION CTE, because UNION is implies DISTINCT,
152977
+ ** reduce the estimated output row count by 8 (LogEst 30).
152978
+ ** Search for tag-20250414a to see other cases */
152979
+ p->nSelectRow -= 30;
152980
+ }
152659152981
}
152660152982
if( sDistinct.isTnct && sqlite3WhereIsDistinct(pWInfo) ){
152661152983
sDistinct.eTnctType = sqlite3WhereIsDistinct(pWInfo);
152662152984
}
152663152985
if( sSort.pOrderBy ){
@@ -156925,11 +157247,11 @@
156925157247
iDb = sqlite3TwoPartName(pParse, pNm, pNm, &pNm);
156926157248
if( iDb<0 ) goto build_vacuum_end;
156927157249
#else
156928157250
/* When SQLITE_BUG_COMPATIBLE_20160819 is defined, unrecognized arguments
156929157251
** to VACUUM are silently ignored. This is a back-out of a bug fix that
156930
- ** occurred on 2016-08-19 (https://www.sqlite.org/src/info/083f9e6270).
157252
+ ** occurred on 2016-08-19 (https://sqlite.org/src/info/083f9e6270).
156931157253
** The buggy behavior is required for binary compatibility with some
156932157254
** legacy applications. */
156933157255
iDb = sqlite3FindDb(pParse->db, pNm);
156934157256
if( iDb<0 ) iDb = 0;
156935157257
#endif
@@ -159820,11 +160142,11 @@
159820160142
159821160143
159822160144
/*
159823160145
** pX is an expression of the form: (vector) IN (SELECT ...)
159824160146
** In other words, it is a vector IN operator with a SELECT clause on the
159825
-** LHS. But not all terms in the vector are indexable and the terms might
160147
+** RHS. But not all terms in the vector are indexable and the terms might
159826160148
** not be in the correct order for indexing.
159827160149
**
159828160150
** This routine makes a copy of the input pX expression and then adjusts
159829160151
** the vector on the LHS with corresponding changes to the SELECT so that
159830160152
** the vector contains only index terms and those terms are in the correct
@@ -161642,11 +161964,11 @@
161642161964
** ON or USING clause of a LEFT JOIN, and terms that are usable as
161643161965
** indices.
161644161966
**
161645161967
** This optimization also only applies if the (x1 OR x2 OR ...) term
161646161968
** is not contained in the ON clause of a LEFT JOIN.
161647
- ** See ticket http://www.sqlite.org/src/info/f2369304e4
161969
+ ** See ticket http://sqlite.org/src/info/f2369304e4
161648161970
**
161649161971
** 2022-02-04: Do not push down slices of a row-value comparison.
161650161972
** In other words, "w" or "y" may not be a slice of a vector. Otherwise,
161651161973
** the initialization of the right-hand operand of the vector comparison
161652161974
** might not occur, or might occur only in an OR branch that is not
@@ -167615,11 +167937,11 @@
167615167937
}
167616167938
167617167939
if( (pNew->wsFlags & WHERE_TOP_LIMIT)==0
167618167940
&& pNew->u.btree.nEq<pProbe->nColumn
167619167941
&& (pNew->u.btree.nEq<pProbe->nKeyCol ||
167620
- pProbe->idxType!=SQLITE_IDXTYPE_PRIMARYKEY)
167942
+ (pProbe->idxType!=SQLITE_IDXTYPE_PRIMARYKEY && !pProbe->bIdxRowid))
167621167943
){
167622167944
if( pNew->u.btree.nEq>3 ){
167623167945
sqlite3ProgressCheck(pParse);
167624167946
}
167625167947
whereLoopAddBtreeIndex(pBuilder, pSrc, pProbe, nInMul+nIn);
@@ -171073,11 +171395,12 @@
171073171395
wherePathSolver(pWInfo, pWInfo->nRowOut<0 ? 1 : pWInfo->nRowOut+1);
171074171396
if( db->mallocFailed ) goto whereBeginError;
171075171397
}
171076171398
171077171399
/* TUNING: Assume that a DISTINCT clause on a subquery reduces
171078
- ** the output size by a factor of 8 (LogEst -30).
171400
+ ** the output size by a factor of 8 (LogEst -30). Search for
171401
+ ** tag-20250414a to see other cases.
171079171402
*/
171080171403
if( (pWInfo->wctrlFlags & WHERE_WANT_DISTINCT)!=0 ){
171081171404
WHERETRACE(0x0080,("nRowOut reduced from %d to %d due to DISTINCT\n",
171082171405
pWInfo->nRowOut, pWInfo->nRowOut-30));
171083171406
pWInfo->nRowOut -= 30;
@@ -188065,10 +188388,17 @@
188065188388
******************************************************************************
188066188389
**
188067188390
*/
188068188391
#ifndef _FTSINT_H
188069188392
#define _FTSINT_H
188393
+
188394
+/* #include <assert.h> */
188395
+/* #include <stdlib.h> */
188396
+/* #include <stddef.h> */
188397
+/* #include <stdio.h> */
188398
+/* #include <string.h> */
188399
+/* #include <stdarg.h> */
188070188400
188071188401
#if !defined(NDEBUG) && !defined(SQLITE_DEBUG)
188072188402
# define NDEBUG 1
188073188403
#endif
188074188404
@@ -189017,16 +189347,10 @@
189017189347
189018189348
#if defined(SQLITE_ENABLE_FTS3) && !defined(SQLITE_CORE)
189019189349
# define SQLITE_CORE 1
189020189350
#endif
189021189351
189022
-/* #include <assert.h> */
189023
-/* #include <stdlib.h> */
189024
-/* #include <stddef.h> */
189025
-/* #include <stdio.h> */
189026
-/* #include <string.h> */
189027
-/* #include <stdarg.h> */
189028189352
189029189353
/* #include "fts3.h" */
189030189354
#ifndef SQLITE_CORE
189031189355
/* # include "sqlite3ext.h" */
189032189356
SQLITE_EXTENSION_INIT1
@@ -198969,11 +199293,11 @@
198969199293
UNUSED_PARAMETER(nVal);
198970199294
198971199295
fts3tokResetCursor(pCsr);
198972199296
if( idxNum==1 ){
198973199297
const char *zByte = (const char *)sqlite3_value_text(apVal[0]);
198974
- int nByte = sqlite3_value_bytes(apVal[0]);
199298
+ sqlite3_int64 nByte = sqlite3_value_bytes(apVal[0]);
198975199299
pCsr->zInput = sqlite3_malloc64(nByte+1);
198976199300
if( pCsr->zInput==0 ){
198977199301
rc = SQLITE_NOMEM;
198978199302
}else{
198979199303
if( nByte>0 ) memcpy(pCsr->zInput, zByte, nByte);
@@ -205949,20 +206273,20 @@
205949206273
case FTS3_MATCHINFO_LCS:
205950206274
nVal = pInfo->nCol;
205951206275
break;
205952206276
205953206277
case FTS3_MATCHINFO_LHITS:
205954
- nVal = pInfo->nCol * pInfo->nPhrase;
206278
+ nVal = (size_t)pInfo->nCol * pInfo->nPhrase;
205955206279
break;
205956206280
205957206281
case FTS3_MATCHINFO_LHITS_BM:
205958
- nVal = pInfo->nPhrase * ((pInfo->nCol + 31) / 32);
206282
+ nVal = (size_t)pInfo->nPhrase * ((pInfo->nCol + 31) / 32);
205959206283
break;
205960206284
205961206285
default:
205962206286
assert( cArg==FTS3_MATCHINFO_HITS );
205963
- nVal = pInfo->nCol * pInfo->nPhrase * 3;
206287
+ nVal = (size_t)pInfo->nCol * pInfo->nPhrase * 3;
205964206288
break;
205965206289
}
205966206290
205967206291
return nVal;
205968206292
}
@@ -207516,12 +207840,12 @@
207516207840
** with JSON-5 extensions is accepted as input.
207517207841
**
207518207842
** Beginning with version 3.45.0 (circa 2024-01-01), these routines also
207519207843
** accept BLOB values that have JSON encoded using a binary representation
207520207844
** called "JSONB". The name JSONB comes from PostgreSQL, however the on-disk
207521
-** format SQLite JSONB is completely different and incompatible with
207522
-** PostgreSQL JSONB.
207845
+** format for SQLite-JSONB is completely different and incompatible with
207846
+** PostgreSQL-JSONB.
207523207847
**
207524207848
** Decoding and interpreting JSONB is still O(N) where N is the size of
207525207849
** the input, the same as text JSON. However, the constant of proportionality
207526207850
** for JSONB is much smaller due to faster parsing. The size of each
207527207851
** element in JSONB is encoded in its header, so there is no need to search
@@ -207574,21 +207898,21 @@
207574207898
** 14 4 byte (0-4294967295) 5
207575207899
** 15 8 byte (0-1.8e19) 9
207576207900
**
207577207901
** The payload size need not be expressed in its minimal form. For example,
207578207902
** if the payload size is 10, the size can be expressed in any of 5 different
207579
-** ways: (1) (X>>4)==10, (2) (X>>4)==12 following by on 0x0a byte,
207903
+** ways: (1) (X>>4)==10, (2) (X>>4)==12 following by one 0x0a byte,
207580207904
** (3) (X>>4)==13 followed by 0x00 and 0x0a, (4) (X>>4)==14 followed by
207581207905
** 0x00 0x00 0x00 0x0a, or (5) (X>>4)==15 followed by 7 bytes of 0x00 and
207582207906
** a single byte of 0x0a. The shorter forms are preferred, of course, but
207583207907
** sometimes when generating JSONB, the payload size is not known in advance
207584207908
** and it is convenient to reserve sufficient header space to cover the
207585207909
** largest possible payload size and then come back later and patch up
207586207910
** the size when it becomes known, resulting in a non-minimal encoding.
207587207911
**
207588207912
** The value (X>>4)==15 is not actually used in the current implementation
207589
-** (as SQLite is currently unable handle BLOBs larger than about 2GB)
207913
+** (as SQLite is currently unable to handle BLOBs larger than about 2GB)
207590207914
** but is included in the design to allow for future enhancements.
207591207915
**
207592207916
** The payload follows the header. NULL, TRUE, and FALSE have no payload and
207593207917
** their payload size must always be zero. The payload for INT, INT5,
207594207918
** FLOAT, FLOAT5, TEXT, TEXTJ, TEXT5, and TEXTROW is text. Note that the
@@ -208658,11 +208982,11 @@
208658208982
if( jsonBlobExpand(pParse, pParse->nBlob+szPayload+9) ) return;
208659208983
jsonBlobAppendNode(pParse, eType, szPayload, aPayload);
208660208984
}
208661208985
208662208986
208663
-/* Append an node type byte together with the payload size and
208987
+/* Append a node type byte together with the payload size and
208664208988
** possibly also the payload.
208665208989
**
208666208990
** If aPayload is not NULL, then it is a pointer to the payload which
208667208991
** is also appended. If aPayload is NULL, the pParse->aBlob[] array
208668208992
** is resized (if necessary) so that it is big enough to hold the
@@ -209992,10 +210316,86 @@
209992210316
(void)jsonbPayloadSize(pParse, iRoot, &sz);
209993210317
pParse->nBlob = nBlob;
209994210318
sz += pParse->delta;
209995210319
pParse->delta += jsonBlobChangePayloadSize(pParse, iRoot, sz);
209996210320
}
210321
+
210322
+/*
210323
+** If the JSONB at aIns[0..nIns-1] can be expanded (by denormalizing the
210324
+** size field) by d bytes, then write the expansion into aOut[] and
210325
+** return true. In this way, an overwrite happens without changing the
210326
+** size of the JSONB, which reduces memcpy() operations and also make it
210327
+** faster and easier to update the B-Tree entry that contains the JSONB
210328
+** in the database.
210329
+**
210330
+** If the expansion of aIns[] by d bytes cannot be (easily) accomplished
210331
+** then return false.
210332
+**
210333
+** The d parameter is guaranteed to be between 1 and 8.
210334
+**
210335
+** This routine is an optimization. A correct answer is obtained if it
210336
+** always leaves the output unchanged and returns false.
210337
+*/
210338
+static int jsonBlobOverwrite(
210339
+ u8 *aOut, /* Overwrite here */
210340
+ const u8 *aIns, /* New content */
210341
+ u32 nIns, /* Bytes of new content */
210342
+ u32 d /* Need to expand new content by this much */
210343
+){
210344
+ u32 szPayload; /* Bytes of payload */
210345
+ u32 i; /* New header size, after expansion & a loop counter */
210346
+ u8 szHdr; /* Size of header before expansion */
210347
+
210348
+ /* Lookup table for finding the upper 4 bits of the first byte of the
210349
+ ** expanded aIns[], based on the size of the expanded aIns[] header:
210350
+ **
210351
+ ** 2 3 4 5 6 7 8 9 */
210352
+ static const u8 aType[] = { 0xc0, 0xd0, 0, 0xe0, 0, 0, 0, 0xf0 };
210353
+
210354
+ if( (aIns[0]&0x0f)<=2 ) return 0; /* Cannot enlarge NULL, true, false */
210355
+ switch( aIns[0]>>4 ){
210356
+ default: { /* aIns[] header size 1 */
210357
+ if( ((1<<d)&0x116)==0 ) return 0; /* d must be 1, 2, 4, or 8 */
210358
+ i = d + 1; /* New hdr sz: 2, 3, 5, or 9 */
210359
+ szHdr = 1;
210360
+ break;
210361
+ }
210362
+ case 12: { /* aIns[] header size is 2 */
210363
+ if( ((1<<d)&0x8a)==0) return 0; /* d must be 1, 3, or 7 */
210364
+ i = d + 2; /* New hdr sz: 2, 5, or 9 */
210365
+ szHdr = 2;
210366
+ break;
210367
+ }
210368
+ case 13: { /* aIns[] header size is 3 */
210369
+ if( d!=2 && d!=6 ) return 0; /* d must be 2 or 6 */
210370
+ i = d + 3; /* New hdr sz: 5 or 9 */
210371
+ szHdr = 3;
210372
+ break;
210373
+ }
210374
+ case 14: { /* aIns[] header size is 5 */
210375
+ if( d!=4 ) return 0; /* d must be 4 */
210376
+ i = 9; /* New hdr sz: 9 */
210377
+ szHdr = 5;
210378
+ break;
210379
+ }
210380
+ case 15: { /* aIns[] header size is 9 */
210381
+ return 0; /* No solution */
210382
+ }
210383
+ }
210384
+ assert( i>=2 && i<=9 && aType[i-2]!=0 );
210385
+ aOut[0] = (aIns[0] & 0x0f) | aType[i-2];
210386
+ memcpy(&aOut[i], &aIns[szHdr], nIns-szHdr);
210387
+ szPayload = nIns - szHdr;
210388
+ while( 1/*edit-by-break*/ ){
210389
+ i--;
210390
+ aOut[i] = szPayload & 0xff;
210391
+ if( i==1 ) break;
210392
+ szPayload >>= 8;
210393
+ }
210394
+ assert( (szPayload>>8)==0 );
210395
+ return 1;
210396
+}
209997210397
209998210398
/*
209999210399
** Modify the JSONB blob at pParse->aBlob by removing nDel bytes of
210000210400
** content beginning at iDel, and replacing them with nIns bytes of
210001210401
** content given by aIns.
@@ -210014,10 +210414,15 @@
210014210414
u32 nDel, /* Number of bytes to remove */
210015210415
const u8 *aIns, /* Content to insert */
210016210416
u32 nIns /* Bytes of content to insert */
210017210417
){
210018210418
i64 d = (i64)nIns - (i64)nDel;
210419
+ if( d<0 && d>=(-8) && aIns!=0
210420
+ && jsonBlobOverwrite(&pParse->aBlob[iDel], aIns, nIns, (int)-d)
210421
+ ){
210422
+ return;
210423
+ }
210019210424
if( d!=0 ){
210020210425
if( pParse->nBlob + d > pParse->nBlobAlloc ){
210021210426
jsonBlobExpand(pParse, pParse->nBlob+d);
210022210427
if( pParse->oom ) return;
210023210428
}
@@ -210025,11 +210430,13 @@
210025210430
&pParse->aBlob[iDel+nDel],
210026210431
pParse->nBlob - (iDel+nDel));
210027210432
pParse->nBlob += d;
210028210433
pParse->delta += d;
210029210434
}
210030
- if( nIns && aIns ) memcpy(&pParse->aBlob[iDel], aIns, nIns);
210435
+ if( nIns && aIns ){
210436
+ memcpy(&pParse->aBlob[iDel], aIns, nIns);
210437
+ }
210031210438
}
210032210439
210033210440
/*
210034210441
** Return the number of escaped newlines to be ignored.
210035210442
** An escaped newline is a one of the following byte sequences:
@@ -210788,11 +211195,11 @@
210788211195
}
210789211196
return 0;
210790211197
}
210791211198
210792211199
/* argv[0] is a BLOB that seems likely to be a JSONB. Subsequent
210793
-** arguments come in parse where each pair contains a JSON path and
211200
+** arguments come in pairs where each pair contains a JSON path and
210794211201
** content to insert or set at that patch. Do the updates
210795211202
** and return the result.
210796211203
**
210797211204
** The specific operation is determined by eEdit, which can be one
210798211205
** of JEDIT_INS, JEDIT_REPL, or JEDIT_SET.
@@ -227533,12 +227940,12 @@
227533227940
){
227534227941
if( sqlite3_value_type(argv[3])==SQLITE_NULL && isInsert && pgno>1 ){
227535227942
/* "INSERT INTO dbpage($PGNO,NULL)" causes page number $PGNO and
227536227943
** all subsequent pages to be deleted. */
227537227944
pTab->iDbTrunc = iDb;
227538
- pgno--;
227539
- pTab->pgnoTrunc = pgno;
227945
+ pTab->pgnoTrunc = pgno-1;
227946
+ pgno = 1;
227540227947
}else{
227541227948
zErr = "bad page value";
227542227949
goto update_fail;
227543227950
}
227544227951
}
@@ -228850,10 +229257,12 @@
228850229257
int rc = SQLITE_OK;
228851229258
228852229259
if( pTab->nCol==0 ){
228853229260
u8 *abPK;
228854229261
assert( pTab->azCol==0 || pTab->abPK==0 );
229262
+ sqlite3_free(pTab->azCol);
229263
+ pTab->abPK = 0;
228855229264
rc = sessionTableInfo(pSession, db, zDb,
228856229265
pTab->zName, &pTab->nCol, &pTab->nTotalCol, 0, &pTab->azCol,
228857229266
&pTab->azDflt, &pTab->aiIdx, &abPK,
228858229267
((pSession==0 || pSession->bImplicitPK) ? &pTab->bRowid : 0)
228859229268
);
@@ -229857,11 +230266,13 @@
229857230266
char *zExpr = 0;
229858230267
sqlite3 *db = pSession->db;
229859230268
SessionTable *pTo; /* Table zTbl */
229860230269
229861230270
/* Locate and if necessary initialize the target table object */
230271
+ pSession->bAutoAttach++;
229862230272
rc = sessionFindTable(pSession, zTbl, &pTo);
230273
+ pSession->bAutoAttach--;
229863230274
if( pTo==0 ) goto diff_out;
229864230275
if( sessionInitTable(pSession, pTo, pSession->db, pSession->zDb) ){
229865230276
rc = pSession->rc;
229866230277
goto diff_out;
229867230278
}
@@ -229868,21 +230279,47 @@
229868230279
229869230280
/* Check the table schemas match */
229870230281
if( rc==SQLITE_OK ){
229871230282
int bHasPk = 0;
229872230283
int bMismatch = 0;
229873
- int nCol; /* Columns in zFrom.zTbl */
230284
+ int nCol = 0; /* Columns in zFrom.zTbl */
229874230285
int bRowid = 0;
229875
- u8 *abPK;
230286
+ u8 *abPK = 0;
229876230287
const char **azCol = 0;
229877
- rc = sessionTableInfo(0, db, zFrom, zTbl,
229878
- &nCol, 0, 0, &azCol, 0, 0, &abPK,
229879
- pSession->bImplicitPK ? &bRowid : 0
229880
- );
230288
+ char *zDbExists = 0;
230289
+
230290
+ /* Check that database zFrom is attached. */
230291
+ zDbExists = sqlite3_mprintf("SELECT * FROM %Q.sqlite_schema", zFrom);
230292
+ if( zDbExists==0 ){
230293
+ rc = SQLITE_NOMEM;
230294
+ }else{
230295
+ sqlite3_stmt *pDbExists = 0;
230296
+ rc = sqlite3_prepare_v2(db, zDbExists, -1, &pDbExists, 0);
230297
+ if( rc==SQLITE_ERROR ){
230298
+ rc = SQLITE_OK;
230299
+ nCol = -1;
230300
+ }
230301
+ sqlite3_finalize(pDbExists);
230302
+ sqlite3_free(zDbExists);
230303
+ }
230304
+
230305
+ if( rc==SQLITE_OK && nCol==0 ){
230306
+ rc = sessionTableInfo(0, db, zFrom, zTbl,
230307
+ &nCol, 0, 0, &azCol, 0, 0, &abPK,
230308
+ pSession->bImplicitPK ? &bRowid : 0
230309
+ );
230310
+ }
229881230311
if( rc==SQLITE_OK ){
229882230312
if( pTo->nCol!=nCol ){
229883
- bMismatch = 1;
230313
+ if( nCol<=0 ){
230314
+ rc = SQLITE_SCHEMA;
230315
+ if( pzErrMsg ){
230316
+ *pzErrMsg = sqlite3_mprintf("no such table: %s.%s", zFrom, zTbl);
230317
+ }
230318
+ }else{
230319
+ bMismatch = 1;
230320
+ }
229884230321
}else{
229885230322
int i;
229886230323
for(i=0; i<nCol; i++){
229887230324
if( pTo->abPK[i]!=abPK[i] ) bMismatch = 1;
229888230325
if( sqlite3_stricmp(azCol[i], pTo->azCol[i]) ) bMismatch = 1;
@@ -241869,11 +242306,12 @@
241869242306
sqlite3Fts5ParseError(
241870242307
pParse, "expected integer, got \"%.*s\"", p->n, p->p
241871242308
);
241872242309
return;
241873242310
}
241874
- nNear = nNear * 10 + (p->p[i] - '0');
242311
+ if( nNear<214748363 ) nNear = nNear * 10 + (p->p[i] - '0');
242312
+ /* ^^^^^^^^^^^^^^^--- Prevent integer overflow */
241875242313
}
241876242314
}else{
241877242315
nNear = FTS5_DEFAULT_NEARDIST;
241878242316
}
241879242317
pNear->nNear = nNear;
@@ -256775,11 +257213,11 @@
256775257213
int nArg, /* Number of args */
256776257214
sqlite3_value **apUnused /* Function arguments */
256777257215
){
256778257216
assert( nArg==0 );
256779257217
UNUSED_PARAM2(nArg, apUnused);
256780
- sqlite3_result_text(pCtx, "fts5: 2025-03-16 00:13:29 18bda13e197e4b4ec7464b3e70012f71edc05f73d8b14bb48bad452f81c7e185", -1, SQLITE_TRANSIENT);
257218
+ sqlite3_result_text(pCtx, "fts5: 2025-04-15 21:59:38 d22475b81c4e26ccc50f3b5626d43b32f7a2de34e5a764539554665bdda735d5", -1, SQLITE_TRANSIENT);
256781257219
}
256782257220
256783257221
/*
256784257222
** Implementation of fts5_locale(LOCALE, TEXT) function.
256785257223
**
@@ -260838,11 +261276,10 @@
260838261276
}
260839261277
iTbl++;
260840261278
}
260841261279
aAscii[0] = 0; /* 0x00 is never a token character */
260842261280
}
260843
-
260844261281
260845261282
/*
260846261283
** 2015 May 30
260847261284
**
260848261285
** The author disclaims copyright to this source code. In place of
260849261286
--- extsrc/sqlite3.c
+++ extsrc/sqlite3.c
@@ -16,11 +16,11 @@
16 ** if you want a wrapper to interface SQLite with your choice of programming
17 ** language. The code for the "sqlite3" command-line shell is also in a
18 ** separate file. This file contains only code for the core SQLite library.
19 **
20 ** The content in this amalgamation comes from Fossil check-in
21 ** 18bda13e197e4b4ec7464b3e70012f71edc0 with changes in files:
22 **
23 **
24 */
25 #ifndef SQLITE_AMALGAMATION
26 #define SQLITE_CORE 1
@@ -450,11 +450,11 @@
450 ** be held constant and Z will be incremented or else Y will be incremented
451 ** and Z will be reset to zero.
452 **
453 ** Since [version 3.6.18] ([dateof:3.6.18]),
454 ** SQLite source code has been stored in the
455 ** <a href="http://www.fossil-scm.org/">Fossil configuration management
456 ** system</a>. ^The SQLITE_SOURCE_ID macro evaluates to
457 ** a string which identifies a particular check-in of SQLite
458 ** within its configuration management system. ^The SQLITE_SOURCE_ID
459 ** string contains the date and time of the check-in (UTC) and a SHA1
460 ** or SHA3-256 hash of the entire source tree. If the source code has
@@ -465,11 +465,11 @@
465 ** [sqlite3_libversion_number()], [sqlite3_sourceid()],
466 ** [sqlite_version()] and [sqlite_source_id()].
467 */
468 #define SQLITE_VERSION "3.50.0"
469 #define SQLITE_VERSION_NUMBER 3050000
470 #define SQLITE_SOURCE_ID "2025-03-16 00:13:29 18bda13e197e4b4ec7464b3e70012f71edc05f73d8b14bb48bad452f81c7e185"
471
472 /*
473 ** CAPI3REF: Run-Time Library Version Numbers
474 ** KEYWORDS: sqlite3_version sqlite3_sourceid
475 **
@@ -11946,12 +11946,13 @@
11946 ** To clarify, if this function is called and then a changeset constructed
11947 ** using [sqlite3session_changeset()], then after applying that changeset to
11948 ** database zFrom the contents of the two compatible tables would be
11949 ** identical.
11950 **
11951 ** It an error if database zFrom does not exist or does not contain the
11952 ** required compatible table.
 
11953 **
11954 ** If the operation is successful, SQLITE_OK is returned. Otherwise, an SQLite
11955 ** error code. In this case, if argument pzErrMsg is not NULL, *pzErrMsg
11956 ** may be set to point to a buffer containing an English language error
11957 ** message. It is the responsibility of the caller to free this buffer using
@@ -19162,10 +19163,11 @@
19162 unsigned noSkipScan:1; /* Do not try to use skip-scan if true */
19163 unsigned hasStat1:1; /* aiRowLogEst values come from sqlite_stat1 */
19164 unsigned bLowQual:1; /* sqlite_stat1 says this is a low-quality index */
19165 unsigned bNoQuery:1; /* Do not use this index to optimize queries */
19166 unsigned bAscKeyBug:1; /* True if the bba7b69f9849b5bf bug applies */
 
19167 unsigned bHasVCol:1; /* Index references one or more VIRTUAL columns */
19168 unsigned bHasExpr:1; /* Index contains an expression, either a literal
19169 ** expression, or a reference to a VIRTUAL column */
19170 #ifdef SQLITE_ENABLE_STAT4
19171 int nSample; /* Number of elements in aSample[] */
@@ -30270,10 +30272,12 @@
30270 */
30271 #include "windows.h"
30272
30273 #ifdef __CYGWIN__
30274 # include <sys/cygwin.h>
 
 
30275 # include <errno.h> /* amalgamator: dontcache */
30276 #endif
30277
30278 /*
30279 ** Determine if we are dealing with Windows NT.
@@ -35444,11 +35448,11 @@
35444 unsigned char const *z = zIn;
35445 unsigned char const *zEnd = &z[nByte-1];
35446 int n = 0;
35447
35448 if( SQLITE_UTF16NATIVE==SQLITE_UTF16LE ) z++;
35449 while( n<nChar && ALWAYS(z<=zEnd) ){
35450 c = z[0];
35451 z += 2;
35452 if( c>=0xd8 && c<0xdc && z<=zEnd && z[0]>=0xdc && z[0]<0xe0 ) z += 2;
35453 n++;
35454 }
@@ -47726,20 +47730,20 @@
47726 { "FileTimeToLocalFileTime", (SYSCALL)FileTimeToLocalFileTime, 0 },
47727 #else
47728 { "FileTimeToLocalFileTime", (SYSCALL)0, 0 },
47729 #endif
47730
47731 #define osFileTimeToLocalFileTime ((BOOL(WINAPI*)(CONST FILETIME*, \
47732 LPFILETIME))aSyscall[11].pCurrent)
47733
47734 #if SQLITE_OS_WINCE
47735 { "FileTimeToSystemTime", (SYSCALL)FileTimeToSystemTime, 0 },
47736 #else
47737 { "FileTimeToSystemTime", (SYSCALL)0, 0 },
47738 #endif
47739
47740 #define osFileTimeToSystemTime ((BOOL(WINAPI*)(CONST FILETIME*, \
47741 LPSYSTEMTIME))aSyscall[12].pCurrent)
47742
47743 { "FlushFileBuffers", (SYSCALL)FlushFileBuffers, 0 },
47744
47745 #define osFlushFileBuffers ((BOOL(WINAPI*)(HANDLE))aSyscall[13].pCurrent)
@@ -48015,11 +48019,11 @@
48015 { "LockFile", (SYSCALL)LockFile, 0 },
48016 #else
48017 { "LockFile", (SYSCALL)0, 0 },
48018 #endif
48019
48020 #ifndef osLockFile
48021 #define osLockFile ((BOOL(WINAPI*)(HANDLE,DWORD,DWORD,DWORD, \
48022 DWORD))aSyscall[47].pCurrent)
48023 #endif
48024
48025 #if !SQLITE_OS_WINCE
@@ -48079,20 +48083,20 @@
48079
48080 #define osSleep ((VOID(WINAPI*)(DWORD))aSyscall[55].pCurrent)
48081
48082 { "SystemTimeToFileTime", (SYSCALL)SystemTimeToFileTime, 0 },
48083
48084 #define osSystemTimeToFileTime ((BOOL(WINAPI*)(CONST SYSTEMTIME*, \
48085 LPFILETIME))aSyscall[56].pCurrent)
48086
48087 #if !SQLITE_OS_WINCE && !SQLITE_OS_WINRT
48088 { "UnlockFile", (SYSCALL)UnlockFile, 0 },
48089 #else
48090 { "UnlockFile", (SYSCALL)0, 0 },
48091 #endif
48092
48093 #ifndef osUnlockFile
48094 #define osUnlockFile ((BOOL(WINAPI*)(HANDLE,DWORD,DWORD,DWORD, \
48095 DWORD))aSyscall[57].pCurrent)
48096 #endif
48097
48098 #if !SQLITE_OS_WINCE
@@ -48316,10 +48320,67 @@
48316 { "CancelIo", (SYSCALL)0, 0 },
48317 #endif
48318
48319 #define osCancelIo ((BOOL(WINAPI*)(HANDLE))aSyscall[81].pCurrent)
48320
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48321 }; /* End of the overrideable system calls */
48322
48323 /*
48324 ** This is the xSetSystemCall() method of sqlite3_vfs for all of the
48325 ** "win32" VFSes. Return SQLITE_OK upon successfully updating the
@@ -48489,10 +48550,11 @@
48489 sqlite3_mutex_leave(pMainMtx);
48490 return rc;
48491 }
48492 #endif /* SQLITE_WIN32_MALLOC */
48493
 
48494 /*
48495 ** This function outputs the specified (ANSI) string to the Win32 debugger
48496 ** (if available).
48497 */
48498
@@ -48531,10 +48593,11 @@
48531 }else{
48532 fprintf(stderr, "%s", zBuf);
48533 }
48534 #endif
48535 }
 
48536
48537 /*
48538 ** The following routine suspends the current thread for at least ms
48539 ** milliseconds. This is equivalent to the Win32 Sleep() interface.
48540 */
@@ -48831,10 +48894,11 @@
48831 SQLITE_PRIVATE void sqlite3MemSetDefault(void){
48832 sqlite3_config(SQLITE_CONFIG_MALLOC, sqlite3MemGetWin32());
48833 }
48834 #endif /* SQLITE_WIN32_MALLOC */
48835
 
48836 /*
48837 ** Convert a UTF-8 string to Microsoft Unicode.
48838 **
48839 ** Space to hold the returned string is obtained from sqlite3_malloc().
48840 */
@@ -48856,10 +48920,11 @@
48856 sqlite3_free(zWideText);
48857 zWideText = 0;
48858 }
48859 return zWideText;
48860 }
 
48861
48862 /*
48863 ** Convert a Microsoft Unicode string to UTF-8.
48864 **
48865 ** Space to hold the returned string is obtained from sqlite3_malloc().
@@ -48890,32 +48955,33 @@
48890 ** code page.
48891 **
48892 ** Space to hold the returned string is obtained from sqlite3_malloc().
48893 */
48894 static LPWSTR winMbcsToUnicode(const char *zText, int useAnsi){
48895 int nByte;
48896 LPWSTR zMbcsText;
48897 int codepage = useAnsi ? CP_ACP : CP_OEMCP;
48898
48899 nByte = osMultiByteToWideChar(codepage, 0, zText, -1, NULL,
48900 0)*sizeof(WCHAR);
48901 if( nByte==0 ){
48902 return 0;
48903 }
48904 zMbcsText = sqlite3MallocZero( nByte*sizeof(WCHAR) );
48905 if( zMbcsText==0 ){
48906 return 0;
48907 }
48908 nByte = osMultiByteToWideChar(codepage, 0, zText, -1, zMbcsText,
48909 nByte);
48910 if( nByte==0 ){
48911 sqlite3_free(zMbcsText);
48912 zMbcsText = 0;
48913 }
48914 return zMbcsText;
48915 }
48916
 
48917 /*
48918 ** Convert a Microsoft Unicode string to a multi-byte character string,
48919 ** using the ANSI or OEM code page.
48920 **
48921 ** Space to hold the returned string is obtained from sqlite3_malloc().
@@ -48939,10 +49005,11 @@
48939 sqlite3_free(zText);
48940 zText = 0;
48941 }
48942 return zText;
48943 }
 
48944
48945 /*
48946 ** Convert a multi-byte character string to UTF-8.
48947 **
48948 ** Space to hold the returned string is obtained from sqlite3_malloc().
@@ -48958,10 +49025,11 @@
48958 zTextUtf8 = winUnicodeToUtf8(zTmpWide);
48959 sqlite3_free(zTmpWide);
48960 return zTextUtf8;
48961 }
48962
 
48963 /*
48964 ** Convert a UTF-8 string to a multi-byte character string.
48965 **
48966 ** Space to hold the returned string is obtained from sqlite3_malloc().
48967 */
@@ -49007,10 +49075,11 @@
49007 #ifndef SQLITE_OMIT_AUTOINIT
49008 if( sqlite3_initialize() ) return 0;
49009 #endif
49010 return winUnicodeToUtf8(zWideText);
49011 }
 
49012
49013 /*
49014 ** This is a public wrapper for the winMbcsToUtf8() function.
49015 */
49016 SQLITE_API char *sqlite3_win32_mbcs_to_utf8(const char *zText){
@@ -49024,10 +49093,11 @@
49024 if( sqlite3_initialize() ) return 0;
49025 #endif
49026 return winMbcsToUtf8(zText, osAreFileApisANSI());
49027 }
49028
 
49029 /*
49030 ** This is a public wrapper for the winMbcsToUtf8() function.
49031 */
49032 SQLITE_API char *sqlite3_win32_mbcs_to_utf8_v2(const char *zText, int useAnsi){
49033 #ifdef SQLITE_ENABLE_API_ARMOR
@@ -49148,10 +49218,11 @@
49148 unsigned long type, /* Identifier for directory being set or reset */
49149 void *zValue /* New value for directory being set or reset */
49150 ){
49151 return sqlite3_win32_set_directory16(type, zValue);
49152 }
 
49153
49154 /*
49155 ** The return value of winGetLastErrorMsg
49156 ** is zero if the error message fits in the buffer, or non-zero
49157 ** otherwise (if the message was truncated).
@@ -49696,13 +49767,15 @@
49696 OVERLAPPED ovlp;
49697 memset(&ovlp, 0, sizeof(OVERLAPPED));
49698 ovlp.Offset = offsetLow;
49699 ovlp.OffsetHigh = offsetHigh;
49700 return osLockFileEx(*phFile, flags, 0, numBytesLow, numBytesHigh, &ovlp);
 
49701 }else{
49702 return osLockFile(*phFile, offsetLow, offsetHigh, numBytesLow,
49703 numBytesHigh);
 
49704 }
49705 #endif
49706 }
49707
49708 /*
@@ -49806,13 +49879,15 @@
49806 OVERLAPPED ovlp;
49807 memset(&ovlp, 0, sizeof(OVERLAPPED));
49808 ovlp.Offset = offsetLow;
49809 ovlp.OffsetHigh = offsetHigh;
49810 return osUnlockFileEx(*phFile, 0, numBytesLow, numBytesHigh, &ovlp);
 
49811 }else{
49812 return osUnlockFile(*phFile, offsetLow, offsetHigh, numBytesLow,
49813 numBytesHigh);
 
49814 }
49815 #endif
49816 }
49817
49818 /*
@@ -51222,18 +51297,95 @@
51222
51223 /*
51224 ** Convert a UTF-8 filename into whatever form the underlying
51225 ** operating system wants filenames in. Space to hold the result
51226 ** is obtained from malloc and must be freed by the calling
51227 ** function.
 
 
 
 
 
 
 
 
 
 
 
 
 
51228 */
51229 static void *winConvertFromUtf8Filename(const char *zFilename){
51230 void *zConverted = 0;
51231 if( osIsNT() ){
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51232 zConverted = winUtf8ToUnicode(zFilename);
 
51233 }
51234 #ifdef SQLITE_WIN32_HAS_ANSI
51235 else{
51236 zConverted = winUtf8ToMbcs(zFilename, osAreFileApisANSI());
51237 }
51238 #endif
51239 /* caller will handle out of memory */
@@ -52058,11 +52210,11 @@
52058 **
52059 ** This division contains the implementation of methods on the
52060 ** sqlite3_vfs object.
52061 */
52062
52063 #if defined(__CYGWIN__)
52064 /*
52065 ** Convert a filename from whatever the underlying operating system
52066 ** supports for filenames into UTF-8. Space to hold the result is
52067 ** obtained from malloc and must be freed by the calling function.
52068 */
@@ -52091,11 +52243,18 @@
52091 int nLen = sqlite3Strlen30(zBuf);
52092 if( nLen>0 ){
52093 if( winIsDirSep(zBuf[nLen-1]) ){
52094 return 1;
52095 }else if( nLen+1<nBuf ){
52096 zBuf[nLen] = winGetDirSep();
 
 
 
 
 
 
 
52097 zBuf[nLen+1] = '\0';
52098 return 1;
52099 }
52100 }
52101 }
@@ -52118,11 +52277,11 @@
52118 /*
52119 ** Create a temporary file name and store the resulting pointer into pzBuf.
52120 ** The pointer returned in pzBuf must be freed via sqlite3_free().
52121 */
52122 static int winGetTempname(sqlite3_vfs *pVfs, char **pzBuf){
52123 static char zChars[] =
52124 "abcdefghijklmnopqrstuvwxyz"
52125 "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
52126 "0123456789";
52127 size_t i, j;
52128 DWORD pid;
@@ -52169,11 +52328,11 @@
52169 }
52170 sqlite3_mutex_leave(sqlite3MutexAlloc(SQLITE_MUTEX_STATIC_TEMPDIR));
52171 }
52172
52173 #if defined(__CYGWIN__)
52174 else{
52175 static const char *azDirs[] = {
52176 0, /* getenv("SQLITE_TMPDIR") */
52177 0, /* getenv("TMPDIR") */
52178 0, /* getenv("TMP") */
52179 0, /* getenv("TEMP") */
@@ -52185,24 +52344,24 @@
52185 0 /* List terminator */
52186 };
52187 unsigned int i;
52188 const char *zDir = 0;
52189
52190 if( !azDirs[0] ) azDirs[0] = getenv("SQLITE_TMPDIR");
52191 if( !azDirs[1] ) azDirs[1] = getenv("TMPDIR");
52192 if( !azDirs[2] ) azDirs[2] = getenv("TMP");
52193 if( !azDirs[3] ) azDirs[3] = getenv("TEMP");
52194 if( !azDirs[4] ) azDirs[4] = getenv("USERPROFILE");
52195 for(i=0; i<sizeof(azDirs)/sizeof(azDirs[0]); zDir=azDirs[i++]){
52196 void *zConverted;
52197 if( zDir==0 ) continue;
52198 /* If the path starts with a drive letter followed by the colon
52199 ** character, assume it is already a native Win32 path; otherwise,
52200 ** it must be converted to a native Win32 path via the Cygwin API
52201 ** prior to using it.
52202 */
52203 if( winIsDriveLetterAndColon(zDir) ){
52204 zConverted = winConvertFromUtf8Filename(zDir);
52205 if( !zConverted ){
52206 sqlite3_free(zBuf);
52207 OSTRACE(("TEMP-FILENAME rc=SQLITE_IOERR_NOMEM\n"));
52208 return SQLITE_IOERR_NOMEM_BKPT;
@@ -52211,19 +52370,20 @@
52211 sqlite3_snprintf(nMax, zBuf, "%s", zDir);
52212 sqlite3_free(zConverted);
52213 break;
52214 }
52215 sqlite3_free(zConverted);
 
52216 }else{
52217 zConverted = sqlite3MallocZero( nMax+1 );
52218 if( !zConverted ){
52219 sqlite3_free(zBuf);
52220 OSTRACE(("TEMP-FILENAME rc=SQLITE_IOERR_NOMEM\n"));
52221 return SQLITE_IOERR_NOMEM_BKPT;
52222 }
52223 if( cygwin_conv_path(
52224 osIsNT() ? CCP_POSIX_TO_WIN_W : CCP_POSIX_TO_WIN_A, zDir,
52225 zConverted, nMax+1)<0 ){
52226 sqlite3_free(zConverted);
52227 sqlite3_free(zBuf);
52228 OSTRACE(("TEMP-FILENAME rc=SQLITE_IOERR_CONVPATH\n"));
52229 return winLogError(SQLITE_IOERR_CONVPATH, (DWORD)errno,
@@ -52245,14 +52405,17 @@
52245 sqlite3_free(zUtf8);
52246 sqlite3_free(zConverted);
52247 break;
52248 }
52249 sqlite3_free(zConverted);
 
52250 }
52251 }
52252 }
52253 #elif !SQLITE_OS_WINRT && !defined(__CYGWIN__)
 
 
52254 else if( osIsNT() ){
52255 char *zMulti;
52256 LPWSTR zWidePath = sqlite3MallocZero( nMax*sizeof(WCHAR) );
52257 if( !zWidePath ){
52258 sqlite3_free(zBuf);
@@ -52372,11 +52535,11 @@
52372 &sAttrData)) && winRetryIoerr(&cnt, &lastErrno) ){}
52373 if( !rc ){
52374 return 0; /* Invalid name? */
52375 }
52376 attr = sAttrData.dwFileAttributes;
52377 #if SQLITE_OS_WINCE==0
52378 }else{
52379 attr = osGetFileAttributesA((char*)zConverted);
52380 #endif
52381 }
52382 return (attr!=INVALID_FILE_ATTRIBUTES) && (attr&FILE_ATTRIBUTE_DIRECTORY);
@@ -52388,10 +52551,16 @@
52388 const char *zFilename, /* Name of file to check */
52389 int flags, /* Type of test to make on this file */
52390 int *pResOut /* OUT: Result */
52391 );
52392
 
 
 
 
 
 
52393 /*
52394 ** Open a file.
52395 */
52396 static int winOpen(
52397 sqlite3_vfs *pVfs, /* Used to get maximum path length and AppData */
@@ -52412,10 +52581,11 @@
52412 winVfsAppData *pAppData;
52413 winFile *pFile = (winFile*)id;
52414 void *zConverted; /* Filename in OS encoding */
52415 const char *zUtf8Name = zName; /* Filename in UTF-8 encoding */
52416 int cnt = 0;
 
52417
52418 /* If argument zPath is a NULL pointer, this function is required to open
52419 ** a temporary file. Use this buffer to store the file name in.
52420 */
52421 char *zTmpname = 0; /* For temporary filename, if necessary. */
@@ -52576,13 +52746,13 @@
52576 dwShareMode,
52577 dwCreationDisposition,
52578 &extendedParameters);
52579 if( h!=INVALID_HANDLE_VALUE ) break;
52580 if( isReadWrite ){
52581 int rc2, isRO = 0;
52582 sqlite3BeginBenignMalloc();
52583 rc2 = winAccess(pVfs, zUtf8Name, SQLITE_ACCESS_READ, &isRO);
52584 sqlite3EndBenignMalloc();
52585 if( rc2==SQLITE_OK && isRO ) break;
52586 }
52587 }while( winRetryIoerr(&cnt, &lastErrno) );
52588 #else
@@ -52593,13 +52763,13 @@
52593 dwCreationDisposition,
52594 dwFlagsAndAttributes,
52595 NULL);
52596 if( h!=INVALID_HANDLE_VALUE ) break;
52597 if( isReadWrite ){
52598 int rc2, isRO = 0;
52599 sqlite3BeginBenignMalloc();
52600 rc2 = winAccess(pVfs, zUtf8Name, SQLITE_ACCESS_READ, &isRO);
52601 sqlite3EndBenignMalloc();
52602 if( rc2==SQLITE_OK && isRO ) break;
52603 }
52604 }while( winRetryIoerr(&cnt, &lastErrno) );
52605 #endif
@@ -52613,13 +52783,13 @@
52613 dwCreationDisposition,
52614 dwFlagsAndAttributes,
52615 NULL);
52616 if( h!=INVALID_HANDLE_VALUE ) break;
52617 if( isReadWrite ){
52618 int rc2, isRO = 0;
52619 sqlite3BeginBenignMalloc();
52620 rc2 = winAccess(pVfs, zUtf8Name, SQLITE_ACCESS_READ, &isRO);
52621 sqlite3EndBenignMalloc();
52622 if( rc2==SQLITE_OK && isRO ) break;
52623 }
52624 }while( winRetryIoerr(&cnt, &lastErrno) );
52625 }
@@ -52630,11 +52800,11 @@
52630 dwDesiredAccess, (h==INVALID_HANDLE_VALUE) ? "failed" : "ok"));
52631
52632 if( h==INVALID_HANDLE_VALUE ){
52633 sqlite3_free(zConverted);
52634 sqlite3_free(zTmpname);
52635 if( isReadWrite && !isExclusive ){
52636 return winOpen(pVfs, zName, id,
52637 ((flags|SQLITE_OPEN_READONLY) &
52638 ~(SQLITE_OPEN_CREATE|SQLITE_OPEN_READWRITE)),
52639 pOutFlags);
52640 }else{
@@ -52832,11 +53002,17 @@
52832 ){
52833 DWORD attr;
52834 int rc = 0;
52835 DWORD lastErrno = 0;
52836 void *zConverted;
 
52837 UNUSED_PARAMETER(pVfs);
 
 
 
 
 
52838
52839 SimulateIOError( return SQLITE_IOERR_ACCESS; );
52840 OSTRACE(("ACCESS name=%s, flags=%x, pResOut=%p\n",
52841 zFilename, flags, pResOut));
52842
@@ -52856,11 +53032,14 @@
52856 int cnt = 0;
52857 WIN32_FILE_ATTRIBUTE_DATA sAttrData;
52858 memset(&sAttrData, 0, sizeof(sAttrData));
52859 while( !(rc = osGetFileAttributesExW((LPCWSTR)zConverted,
52860 GetFileExInfoStandard,
52861 &sAttrData)) && winRetryIoerr(&cnt, &lastErrno) ){}
 
 
 
52862 if( rc ){
52863 /* For an SQLITE_ACCESS_EXISTS query, treat a zero-length file
52864 ** as if it does not exist.
52865 */
52866 if( flags==SQLITE_ACCESS_EXISTS
@@ -52924,10 +53103,11 @@
52924 const char *zPathname
52925 ){
52926 return ( sqlite3Isalpha(zPathname[0]) && zPathname[1]==':' );
52927 }
52928
 
52929 /*
52930 ** Returns non-zero if the specified path name should be used verbatim. If
52931 ** non-zero is returned from this function, the calling function must simply
52932 ** use the provided path name verbatim -OR- resolve it into a full path name
52933 ** using the GetFullPathName Win32 API function (if available).
@@ -52960,10 +53140,74 @@
52960 ** If we get to this point, the path name should almost certainly be a purely
52961 ** relative one (i.e. not a UNC name, not absolute, and not volume relative).
52962 */
52963 return FALSE;
52964 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52965
52966 /*
52967 ** Turn a relative pathname into a full pathname. Write the full
52968 ** pathname into zOut[]. zOut[] will be at least pVfs->mxPathname
52969 ** bytes in size.
@@ -52972,12 +53216,12 @@
52972 sqlite3_vfs *pVfs, /* Pointer to vfs object */
52973 const char *zRelative, /* Possibly relative input path */
52974 int nFull, /* Size of output buffer in bytes */
52975 char *zFull /* Output buffer */
52976 ){
52977 #if !SQLITE_OS_WINCE && !SQLITE_OS_WINRT && !defined(__CYGWIN__)
52978 DWORD nByte;
52979 void *zConverted;
52980 char *zOut;
52981 #endif
52982
52983 /* If this path name begins with "/X:" or "\\?\", where "X" is any
@@ -52986,68 +53230,114 @@
52986 if( zRelative[0]=='/' && (winIsDriveLetterAndColon(zRelative+1)
52987 || winIsLongPathPrefix(zRelative+1)) ){
52988 zRelative++;
52989 }
52990
52991 #if defined(__CYGWIN__)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52992 SimulateIOError( return SQLITE_ERROR );
52993 UNUSED_PARAMETER(nFull);
52994 assert( nFull>=pVfs->mxPathname );
52995 if ( sqlite3_data_directory && !winIsVerbatimPathname(zRelative) ){
52996 /*
52997 ** NOTE: We are dealing with a relative path name and the data
52998 ** directory has been set. Therefore, use it as the basis
52999 ** for converting the relative path name to an absolute
53000 ** one by prepending the data directory and a slash.
53001 */
53002 char *zOut = sqlite3MallocZero( 1+(u64)pVfs->mxPathname );
53003 if( !zOut ){
53004 return SQLITE_IOERR_NOMEM_BKPT;
53005 }
53006 if( cygwin_conv_path(
53007 (osIsNT() ? CCP_POSIX_TO_WIN_W : CCP_POSIX_TO_WIN_A) |
53008 CCP_RELATIVE, zRelative, zOut, pVfs->mxPathname+1)<0 ){
53009 sqlite3_free(zOut);
53010 return winLogError(SQLITE_CANTOPEN_CONVPATH, (DWORD)errno,
53011 "winFullPathname1", zRelative);
53012 }else{
53013 char *zUtf8 = winConvertToUtf8Filename(zOut);
53014 if( !zUtf8 ){
53015 sqlite3_free(zOut);
53016 return SQLITE_IOERR_NOMEM_BKPT;
53017 }
53018 sqlite3_snprintf(MIN(nFull, pVfs->mxPathname), zFull, "%s%c%s",
53019 sqlite3_data_directory, winGetDirSep(), zUtf8);
53020 sqlite3_free(zUtf8);
53021 sqlite3_free(zOut);
53022 }
53023 }else{
53024 char *zOut = sqlite3MallocZero( pVfs->mxPathname+1 );
53025 if( !zOut ){
53026 return SQLITE_IOERR_NOMEM_BKPT;
53027 }
53028 if( cygwin_conv_path(
53029 (osIsNT() ? CCP_POSIX_TO_WIN_W : CCP_POSIX_TO_WIN_A),
53030 zRelative, zOut, pVfs->mxPathname+1)<0 ){
53031 sqlite3_free(zOut);
53032 return winLogError(SQLITE_CANTOPEN_CONVPATH, (DWORD)errno,
53033 "winFullPathname2", zRelative);
53034 }else{
53035 char *zUtf8 = winConvertToUtf8Filename(zOut);
53036 if( !zUtf8 ){
53037 sqlite3_free(zOut);
53038 return SQLITE_IOERR_NOMEM_BKPT;
53039 }
53040 sqlite3_snprintf(MIN(nFull, pVfs->mxPathname), zFull, "%s", zUtf8);
53041 sqlite3_free(zUtf8);
53042 sqlite3_free(zOut);
53043 }
53044 }
53045 return SQLITE_OK;
53046 #endif
53047
53048 #if (SQLITE_OS_WINCE || SQLITE_OS_WINRT) && !defined(__CYGWIN__)
53049 SimulateIOError( return SQLITE_ERROR );
53050 /* WinCE has no concept of a relative pathname, or so I am told. */
53051 /* WinRT has no way to convert a relative path to an absolute one. */
53052 if ( sqlite3_data_directory && !winIsVerbatimPathname(zRelative) ){
53053 /*
@@ -53062,11 +53352,12 @@
53062 sqlite3_snprintf(MIN(nFull, pVfs->mxPathname), zFull, "%s", zRelative);
53063 }
53064 return SQLITE_OK;
53065 #endif
53066
53067 #if !SQLITE_OS_WINCE && !SQLITE_OS_WINRT && !defined(__CYGWIN__)
 
53068 /* It's odd to simulate an io-error here, but really this is just
53069 ** using the io-error infrastructure to test that SQLite handles this
53070 ** function failing. This function could fail if, for example, the
53071 ** current working directory has been unlinked.
53072 */
@@ -53080,10 +53371,11 @@
53080 */
53081 sqlite3_snprintf(MIN(nFull, pVfs->mxPathname), zFull, "%s%c%s",
53082 sqlite3_data_directory, winGetDirSep(), zRelative);
53083 return SQLITE_OK;
53084 }
 
53085 zConverted = winConvertFromUtf8Filename(zRelative);
53086 if( zConverted==0 ){
53087 return SQLITE_IOERR_NOMEM_BKPT;
53088 }
53089 if( osIsNT() ){
@@ -53092,16 +53384,17 @@
53092 if( nByte==0 ){
53093 sqlite3_free(zConverted);
53094 return winLogError(SQLITE_CANTOPEN_FULLPATH, osGetLastError(),
53095 "winFullPathname1", zRelative);
53096 }
53097 zTemp = sqlite3MallocZero( nByte*sizeof(zTemp[0]) + 3*sizeof(zTemp[0]) );
 
53098 if( zTemp==0 ){
53099 sqlite3_free(zConverted);
53100 return SQLITE_IOERR_NOMEM_BKPT;
53101 }
53102 nByte = osGetFullPathNameW((LPCWSTR)zConverted, nByte+3, zTemp, 0);
53103 if( nByte==0 ){
53104 sqlite3_free(zConverted);
53105 sqlite3_free(zTemp);
53106 return winLogError(SQLITE_CANTOPEN_FULLPATH, osGetLastError(),
53107 "winFullPathname2", zRelative);
@@ -53135,11 +53428,30 @@
53135 zOut = winMbcsToUtf8(zTemp, osAreFileApisANSI());
53136 sqlite3_free(zTemp);
53137 }
53138 #endif
53139 if( zOut ){
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53140 sqlite3_snprintf(MIN(nFull, pVfs->mxPathname), zFull, "%s", zOut);
 
53141 sqlite3_free(zOut);
53142 return SQLITE_OK;
53143 }else{
53144 return SQLITE_IOERR_NOMEM_BKPT;
53145 }
@@ -53165,11 +53477,13 @@
53165 ** Interfaces for opening a shared library, finding entry points
53166 ** within the shared library, and closing the shared library.
53167 */
53168 static void *winDlOpen(sqlite3_vfs *pVfs, const char *zFilename){
53169 HANDLE h;
53170 #if defined(__CYGWIN__)
 
 
53171 int nFull = pVfs->mxPathname+1;
53172 char *zFull = sqlite3MallocZero( nFull );
53173 void *zConverted = 0;
53174 if( zFull==0 ){
53175 OSTRACE(("DLOPEN name=%s, handle=%p\n", zFilename, (void*)0));
@@ -53532,11 +53846,11 @@
53532 };
53533 #endif
53534
53535 /* Double-check that the aSyscall[] array has been constructed
53536 ** correctly. See ticket [bb3a86e890c8e96ab] */
53537 assert( ArraySize(aSyscall)==82 );
53538
53539 /* get memory map allocation granularity */
53540 memset(&winSysInfo, 0, sizeof(SYSTEM_INFO));
53541 #if SQLITE_OS_WINRT
53542 osGetNativeSystemInfo(&winSysInfo);
@@ -66508,14 +66822,12 @@
66508 s2 = aIn[1];
66509 }else{
66510 s1 = s2 = 0;
66511 }
66512
66513 assert( nByte>=8 );
66514 assert( (nByte&0x00000007)==0 );
66515 assert( nByte<=65536 );
66516 assert( nByte%4==0 );
66517
66518 if( !nativeCksum ){
66519 do {
66520 s1 += BYTESWAP32(aData[0]) + s2;
66521 s2 += BYTESWAP32(aData[1]) + s1;
@@ -83726,11 +84038,11 @@
83726 ** many different strings can be converted into the same int or real.
83727 ** If a table contains a numeric value and an index is based on the
83728 ** corresponding string value, then it is important that the string be
83729 ** derived from the numeric value, not the other way around, to ensure
83730 ** that the index and table are consistent. See ticket
83731 ** https://www.sqlite.org/src/info/343634942dd54ab (2018-01-31) for
83732 ** an example.
83733 **
83734 ** This routine looks at pMem to verify that if it has both a numeric
83735 ** representation and a string representation then the string rep has
83736 ** been derived from the numeric and not the other way around. It returns
@@ -93014,11 +93326,11 @@
93014 unsigned char enc
93015 ){
93016 assert( xDel!=SQLITE_DYNAMIC );
93017 if( enc!=SQLITE_UTF8 ){
93018 if( enc==SQLITE_UTF16 ) enc = SQLITE_UTF16NATIVE;
93019 nData &= ~(u16)1;
93020 }
93021 return bindText(pStmt, i, zData, nData, xDel, enc);
93022 }
93023 #ifndef SQLITE_OMIT_UTF16
93024 SQLITE_API int sqlite3_bind_text16(
@@ -125166,11 +125478,11 @@
125166 if( !isDupColumn(pIdx, pIdx->nKeyCol, pPk, i) ){
125167 testcase( hasColumn(pIdx->aiColumn, pIdx->nKeyCol, pPk->aiColumn[i]) );
125168 pIdx->aiColumn[j] = pPk->aiColumn[i];
125169 pIdx->azColl[j] = pPk->azColl[i];
125170 if( pPk->aSortOrder[i] ){
125171 /* See ticket https://www.sqlite.org/src/info/bba7b69f9849b5bf */
125172 pIdx->bAscKeyBug = 1;
125173 }
125174 j++;
125175 }
125176 }
@@ -126543,11 +126855,11 @@
126543 /* This OP_SeekEnd opcode makes index insert for a REINDEX go much
126544 ** faster by avoiding unnecessary seeks. But the optimization does
126545 ** not work for UNIQUE constraint indexes on WITHOUT ROWID tables
126546 ** with DESC primary keys, since those indexes have there keys in
126547 ** a different order from the main table.
126548 ** See ticket: https://www.sqlite.org/src/info/bba7b69f9849b5bf
126549 */
126550 sqlite3VdbeAddOp1(v, OP_SeekEnd, iIdx);
126551 }
126552 sqlite3VdbeAddOp2(v, OP_IdxInsert, iIdx, regRecord);
126553 sqlite3VdbeChangeP5(v, OPFLAG_USESEEKRESULT);
@@ -126927,10 +127239,11 @@
126927 }else{
126928 j = pCExpr->iColumn;
126929 assert( j<=0x7fff );
126930 if( j<0 ){
126931 j = pTab->iPKey;
 
126932 }else{
126933 if( pTab->aCol[j].notNull==0 ){
126934 pIndex->uniqNotNull = 0;
126935 }
126936 if( pTab->aCol[j].colFlags & COLFLAG_VIRTUAL ){
@@ -136632,11 +136945,11 @@
136632 ** OE_Update guarantees that only a single row will change, so it
136633 ** must happen before OE_Replace. Technically, OE_Abort and OE_Rollback
136634 ** could happen in any order, but they are grouped up front for
136635 ** convenience.
136636 **
136637 ** 2018-08-14: Ticket https://www.sqlite.org/src/info/908f001483982c43
136638 ** The order of constraints used to have OE_Update as (2) and OE_Abort
136639 ** and so forth as (1). But apparently PostgreSQL checks the OE_Update
136640 ** constraint before any others, so it had to be moved.
136641 **
136642 ** Constraint checking code is generated in this order:
@@ -147785,10 +148098,11 @@
147785 }
147786
147787 multi_select_end:
147788 pDest->iSdst = dest.iSdst;
147789 pDest->nSdst = dest.nSdst;
 
147790 if( pDelete ){
147791 sqlite3ParserAddCleanup(pParse, sqlite3SelectDeleteGeneric, pDelete);
147792 }
147793 return rc;
147794 }
@@ -149395,11 +149709,12 @@
149395 && pE2->iColumn==pColumn->iColumn
149396 ){
149397 return; /* Already present. Return without doing anything. */
149398 }
149399 }
149400 if( sqlite3ExprAffinity(pColumn)==SQLITE_AFF_BLOB ){
 
149401 pConst->bHasAffBlob = 1;
149402 }
149403
149404 pConst->nConst++;
149405 pConst->apExpr = sqlite3DbReallocOrFree(pConst->pParse->db, pConst->apExpr,
@@ -149470,11 +149785,12 @@
149470 for(i=0; i<pConst->nConst; i++){
149471 Expr *pColumn = pConst->apExpr[i*2];
149472 if( pColumn==pExpr ) continue;
149473 if( pColumn->iTable!=pExpr->iTable ) continue;
149474 if( pColumn->iColumn!=pExpr->iColumn ) continue;
149475 if( bIgnoreAffBlob && sqlite3ExprAffinity(pColumn)==SQLITE_AFF_BLOB ){
 
149476 break;
149477 }
149478 /* A match is found. Add the EP_FixedCol property */
149479 pConst->nChng++;
149480 ExprClearProperty(pExpr, EP_Leaf);
@@ -150123,11 +150439,11 @@
150123 **
150124 ** This transformation is necessary because the multiSelectOrderBy() routine
150125 ** above that generates the code for a compound SELECT with an ORDER BY clause
150126 ** uses a merge algorithm that requires the same collating sequence on the
150127 ** result columns as on the ORDER BY clause. See ticket
150128 ** http://www.sqlite.org/src/info/6709574d2a
150129 **
150130 ** This transformation is only needed for EXCEPT, INTERSECT, and UNION.
150131 ** The UNION ALL operator works fine with multiSelectOrderBy() even when
150132 ** there are COLLATE terms in the ORDER BY.
150133 */
@@ -152654,10 +152970,16 @@
152654 pWInfo = sqlite3WhereBegin(pParse, pTabList, pWhere, sSort.pOrderBy,
152655 p->pEList, p, wctrlFlags, p->nSelectRow);
152656 if( pWInfo==0 ) goto select_end;
152657 if( sqlite3WhereOutputRowCount(pWInfo) < p->nSelectRow ){
152658 p->nSelectRow = sqlite3WhereOutputRowCount(pWInfo);
 
 
 
 
 
 
152659 }
152660 if( sDistinct.isTnct && sqlite3WhereIsDistinct(pWInfo) ){
152661 sDistinct.eTnctType = sqlite3WhereIsDistinct(pWInfo);
152662 }
152663 if( sSort.pOrderBy ){
@@ -156925,11 +157247,11 @@
156925 iDb = sqlite3TwoPartName(pParse, pNm, pNm, &pNm);
156926 if( iDb<0 ) goto build_vacuum_end;
156927 #else
156928 /* When SQLITE_BUG_COMPATIBLE_20160819 is defined, unrecognized arguments
156929 ** to VACUUM are silently ignored. This is a back-out of a bug fix that
156930 ** occurred on 2016-08-19 (https://www.sqlite.org/src/info/083f9e6270).
156931 ** The buggy behavior is required for binary compatibility with some
156932 ** legacy applications. */
156933 iDb = sqlite3FindDb(pParse->db, pNm);
156934 if( iDb<0 ) iDb = 0;
156935 #endif
@@ -159820,11 +160142,11 @@
159820
159821
159822 /*
159823 ** pX is an expression of the form: (vector) IN (SELECT ...)
159824 ** In other words, it is a vector IN operator with a SELECT clause on the
159825 ** LHS. But not all terms in the vector are indexable and the terms might
159826 ** not be in the correct order for indexing.
159827 **
159828 ** This routine makes a copy of the input pX expression and then adjusts
159829 ** the vector on the LHS with corresponding changes to the SELECT so that
159830 ** the vector contains only index terms and those terms are in the correct
@@ -161642,11 +161964,11 @@
161642 ** ON or USING clause of a LEFT JOIN, and terms that are usable as
161643 ** indices.
161644 **
161645 ** This optimization also only applies if the (x1 OR x2 OR ...) term
161646 ** is not contained in the ON clause of a LEFT JOIN.
161647 ** See ticket http://www.sqlite.org/src/info/f2369304e4
161648 **
161649 ** 2022-02-04: Do not push down slices of a row-value comparison.
161650 ** In other words, "w" or "y" may not be a slice of a vector. Otherwise,
161651 ** the initialization of the right-hand operand of the vector comparison
161652 ** might not occur, or might occur only in an OR branch that is not
@@ -167615,11 +167937,11 @@
167615 }
167616
167617 if( (pNew->wsFlags & WHERE_TOP_LIMIT)==0
167618 && pNew->u.btree.nEq<pProbe->nColumn
167619 && (pNew->u.btree.nEq<pProbe->nKeyCol ||
167620 pProbe->idxType!=SQLITE_IDXTYPE_PRIMARYKEY)
167621 ){
167622 if( pNew->u.btree.nEq>3 ){
167623 sqlite3ProgressCheck(pParse);
167624 }
167625 whereLoopAddBtreeIndex(pBuilder, pSrc, pProbe, nInMul+nIn);
@@ -171073,11 +171395,12 @@
171073 wherePathSolver(pWInfo, pWInfo->nRowOut<0 ? 1 : pWInfo->nRowOut+1);
171074 if( db->mallocFailed ) goto whereBeginError;
171075 }
171076
171077 /* TUNING: Assume that a DISTINCT clause on a subquery reduces
171078 ** the output size by a factor of 8 (LogEst -30).
 
171079 */
171080 if( (pWInfo->wctrlFlags & WHERE_WANT_DISTINCT)!=0 ){
171081 WHERETRACE(0x0080,("nRowOut reduced from %d to %d due to DISTINCT\n",
171082 pWInfo->nRowOut, pWInfo->nRowOut-30));
171083 pWInfo->nRowOut -= 30;
@@ -188065,10 +188388,17 @@
188065 ******************************************************************************
188066 **
188067 */
188068 #ifndef _FTSINT_H
188069 #define _FTSINT_H
 
 
 
 
 
 
 
188070
188071 #if !defined(NDEBUG) && !defined(SQLITE_DEBUG)
188072 # define NDEBUG 1
188073 #endif
188074
@@ -189017,16 +189347,10 @@
189017
189018 #if defined(SQLITE_ENABLE_FTS3) && !defined(SQLITE_CORE)
189019 # define SQLITE_CORE 1
189020 #endif
189021
189022 /* #include <assert.h> */
189023 /* #include <stdlib.h> */
189024 /* #include <stddef.h> */
189025 /* #include <stdio.h> */
189026 /* #include <string.h> */
189027 /* #include <stdarg.h> */
189028
189029 /* #include "fts3.h" */
189030 #ifndef SQLITE_CORE
189031 /* # include "sqlite3ext.h" */
189032 SQLITE_EXTENSION_INIT1
@@ -198969,11 +199293,11 @@
198969 UNUSED_PARAMETER(nVal);
198970
198971 fts3tokResetCursor(pCsr);
198972 if( idxNum==1 ){
198973 const char *zByte = (const char *)sqlite3_value_text(apVal[0]);
198974 int nByte = sqlite3_value_bytes(apVal[0]);
198975 pCsr->zInput = sqlite3_malloc64(nByte+1);
198976 if( pCsr->zInput==0 ){
198977 rc = SQLITE_NOMEM;
198978 }else{
198979 if( nByte>0 ) memcpy(pCsr->zInput, zByte, nByte);
@@ -205949,20 +206273,20 @@
205949 case FTS3_MATCHINFO_LCS:
205950 nVal = pInfo->nCol;
205951 break;
205952
205953 case FTS3_MATCHINFO_LHITS:
205954 nVal = pInfo->nCol * pInfo->nPhrase;
205955 break;
205956
205957 case FTS3_MATCHINFO_LHITS_BM:
205958 nVal = pInfo->nPhrase * ((pInfo->nCol + 31) / 32);
205959 break;
205960
205961 default:
205962 assert( cArg==FTS3_MATCHINFO_HITS );
205963 nVal = pInfo->nCol * pInfo->nPhrase * 3;
205964 break;
205965 }
205966
205967 return nVal;
205968 }
@@ -207516,12 +207840,12 @@
207516 ** with JSON-5 extensions is accepted as input.
207517 **
207518 ** Beginning with version 3.45.0 (circa 2024-01-01), these routines also
207519 ** accept BLOB values that have JSON encoded using a binary representation
207520 ** called "JSONB". The name JSONB comes from PostgreSQL, however the on-disk
207521 ** format SQLite JSONB is completely different and incompatible with
207522 ** PostgreSQL JSONB.
207523 **
207524 ** Decoding and interpreting JSONB is still O(N) where N is the size of
207525 ** the input, the same as text JSON. However, the constant of proportionality
207526 ** for JSONB is much smaller due to faster parsing. The size of each
207527 ** element in JSONB is encoded in its header, so there is no need to search
@@ -207574,21 +207898,21 @@
207574 ** 14 4 byte (0-4294967295) 5
207575 ** 15 8 byte (0-1.8e19) 9
207576 **
207577 ** The payload size need not be expressed in its minimal form. For example,
207578 ** if the payload size is 10, the size can be expressed in any of 5 different
207579 ** ways: (1) (X>>4)==10, (2) (X>>4)==12 following by on 0x0a byte,
207580 ** (3) (X>>4)==13 followed by 0x00 and 0x0a, (4) (X>>4)==14 followed by
207581 ** 0x00 0x00 0x00 0x0a, or (5) (X>>4)==15 followed by 7 bytes of 0x00 and
207582 ** a single byte of 0x0a. The shorter forms are preferred, of course, but
207583 ** sometimes when generating JSONB, the payload size is not known in advance
207584 ** and it is convenient to reserve sufficient header space to cover the
207585 ** largest possible payload size and then come back later and patch up
207586 ** the size when it becomes known, resulting in a non-minimal encoding.
207587 **
207588 ** The value (X>>4)==15 is not actually used in the current implementation
207589 ** (as SQLite is currently unable handle BLOBs larger than about 2GB)
207590 ** but is included in the design to allow for future enhancements.
207591 **
207592 ** The payload follows the header. NULL, TRUE, and FALSE have no payload and
207593 ** their payload size must always be zero. The payload for INT, INT5,
207594 ** FLOAT, FLOAT5, TEXT, TEXTJ, TEXT5, and TEXTROW is text. Note that the
@@ -208658,11 +208982,11 @@
208658 if( jsonBlobExpand(pParse, pParse->nBlob+szPayload+9) ) return;
208659 jsonBlobAppendNode(pParse, eType, szPayload, aPayload);
208660 }
208661
208662
208663 /* Append an node type byte together with the payload size and
208664 ** possibly also the payload.
208665 **
208666 ** If aPayload is not NULL, then it is a pointer to the payload which
208667 ** is also appended. If aPayload is NULL, the pParse->aBlob[] array
208668 ** is resized (if necessary) so that it is big enough to hold the
@@ -209992,10 +210316,86 @@
209992 (void)jsonbPayloadSize(pParse, iRoot, &sz);
209993 pParse->nBlob = nBlob;
209994 sz += pParse->delta;
209995 pParse->delta += jsonBlobChangePayloadSize(pParse, iRoot, sz);
209996 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
209997
209998 /*
209999 ** Modify the JSONB blob at pParse->aBlob by removing nDel bytes of
210000 ** content beginning at iDel, and replacing them with nIns bytes of
210001 ** content given by aIns.
@@ -210014,10 +210414,15 @@
210014 u32 nDel, /* Number of bytes to remove */
210015 const u8 *aIns, /* Content to insert */
210016 u32 nIns /* Bytes of content to insert */
210017 ){
210018 i64 d = (i64)nIns - (i64)nDel;
 
 
 
 
 
210019 if( d!=0 ){
210020 if( pParse->nBlob + d > pParse->nBlobAlloc ){
210021 jsonBlobExpand(pParse, pParse->nBlob+d);
210022 if( pParse->oom ) return;
210023 }
@@ -210025,11 +210430,13 @@
210025 &pParse->aBlob[iDel+nDel],
210026 pParse->nBlob - (iDel+nDel));
210027 pParse->nBlob += d;
210028 pParse->delta += d;
210029 }
210030 if( nIns && aIns ) memcpy(&pParse->aBlob[iDel], aIns, nIns);
 
 
210031 }
210032
210033 /*
210034 ** Return the number of escaped newlines to be ignored.
210035 ** An escaped newline is a one of the following byte sequences:
@@ -210788,11 +211195,11 @@
210788 }
210789 return 0;
210790 }
210791
210792 /* argv[0] is a BLOB that seems likely to be a JSONB. Subsequent
210793 ** arguments come in parse where each pair contains a JSON path and
210794 ** content to insert or set at that patch. Do the updates
210795 ** and return the result.
210796 **
210797 ** The specific operation is determined by eEdit, which can be one
210798 ** of JEDIT_INS, JEDIT_REPL, or JEDIT_SET.
@@ -227533,12 +227940,12 @@
227533 ){
227534 if( sqlite3_value_type(argv[3])==SQLITE_NULL && isInsert && pgno>1 ){
227535 /* "INSERT INTO dbpage($PGNO,NULL)" causes page number $PGNO and
227536 ** all subsequent pages to be deleted. */
227537 pTab->iDbTrunc = iDb;
227538 pgno--;
227539 pTab->pgnoTrunc = pgno;
227540 }else{
227541 zErr = "bad page value";
227542 goto update_fail;
227543 }
227544 }
@@ -228850,10 +229257,12 @@
228850 int rc = SQLITE_OK;
228851
228852 if( pTab->nCol==0 ){
228853 u8 *abPK;
228854 assert( pTab->azCol==0 || pTab->abPK==0 );
 
 
228855 rc = sessionTableInfo(pSession, db, zDb,
228856 pTab->zName, &pTab->nCol, &pTab->nTotalCol, 0, &pTab->azCol,
228857 &pTab->azDflt, &pTab->aiIdx, &abPK,
228858 ((pSession==0 || pSession->bImplicitPK) ? &pTab->bRowid : 0)
228859 );
@@ -229857,11 +230266,13 @@
229857 char *zExpr = 0;
229858 sqlite3 *db = pSession->db;
229859 SessionTable *pTo; /* Table zTbl */
229860
229861 /* Locate and if necessary initialize the target table object */
 
229862 rc = sessionFindTable(pSession, zTbl, &pTo);
 
229863 if( pTo==0 ) goto diff_out;
229864 if( sessionInitTable(pSession, pTo, pSession->db, pSession->zDb) ){
229865 rc = pSession->rc;
229866 goto diff_out;
229867 }
@@ -229868,21 +230279,47 @@
229868
229869 /* Check the table schemas match */
229870 if( rc==SQLITE_OK ){
229871 int bHasPk = 0;
229872 int bMismatch = 0;
229873 int nCol; /* Columns in zFrom.zTbl */
229874 int bRowid = 0;
229875 u8 *abPK;
229876 const char **azCol = 0;
229877 rc = sessionTableInfo(0, db, zFrom, zTbl,
229878 &nCol, 0, 0, &azCol, 0, 0, &abPK,
229879 pSession->bImplicitPK ? &bRowid : 0
229880 );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
229881 if( rc==SQLITE_OK ){
229882 if( pTo->nCol!=nCol ){
229883 bMismatch = 1;
 
 
 
 
 
 
 
229884 }else{
229885 int i;
229886 for(i=0; i<nCol; i++){
229887 if( pTo->abPK[i]!=abPK[i] ) bMismatch = 1;
229888 if( sqlite3_stricmp(azCol[i], pTo->azCol[i]) ) bMismatch = 1;
@@ -241869,11 +242306,12 @@
241869 sqlite3Fts5ParseError(
241870 pParse, "expected integer, got \"%.*s\"", p->n, p->p
241871 );
241872 return;
241873 }
241874 nNear = nNear * 10 + (p->p[i] - '0');
 
241875 }
241876 }else{
241877 nNear = FTS5_DEFAULT_NEARDIST;
241878 }
241879 pNear->nNear = nNear;
@@ -256775,11 +257213,11 @@
256775 int nArg, /* Number of args */
256776 sqlite3_value **apUnused /* Function arguments */
256777 ){
256778 assert( nArg==0 );
256779 UNUSED_PARAM2(nArg, apUnused);
256780 sqlite3_result_text(pCtx, "fts5: 2025-03-16 00:13:29 18bda13e197e4b4ec7464b3e70012f71edc05f73d8b14bb48bad452f81c7e185", -1, SQLITE_TRANSIENT);
256781 }
256782
256783 /*
256784 ** Implementation of fts5_locale(LOCALE, TEXT) function.
256785 **
@@ -260838,11 +261276,10 @@
260838 }
260839 iTbl++;
260840 }
260841 aAscii[0] = 0; /* 0x00 is never a token character */
260842 }
260843
260844
260845 /*
260846 ** 2015 May 30
260847 **
260848 ** The author disclaims copyright to this source code. In place of
260849
--- extsrc/sqlite3.c
+++ extsrc/sqlite3.c
@@ -16,11 +16,11 @@
16 ** if you want a wrapper to interface SQLite with your choice of programming
17 ** language. The code for the "sqlite3" command-line shell is also in a
18 ** separate file. This file contains only code for the core SQLite library.
19 **
20 ** The content in this amalgamation comes from Fossil check-in
21 ** d22475b81c4e26ccc50f3b5626d43b32f7a2 with changes in files:
22 **
23 **
24 */
25 #ifndef SQLITE_AMALGAMATION
26 #define SQLITE_CORE 1
@@ -450,11 +450,11 @@
450 ** be held constant and Z will be incremented or else Y will be incremented
451 ** and Z will be reset to zero.
452 **
453 ** Since [version 3.6.18] ([dateof:3.6.18]),
454 ** SQLite source code has been stored in the
455 ** <a href="http://fossil-scm.org/">Fossil configuration management
456 ** system</a>. ^The SQLITE_SOURCE_ID macro evaluates to
457 ** a string which identifies a particular check-in of SQLite
458 ** within its configuration management system. ^The SQLITE_SOURCE_ID
459 ** string contains the date and time of the check-in (UTC) and a SHA1
460 ** or SHA3-256 hash of the entire source tree. If the source code has
@@ -465,11 +465,11 @@
465 ** [sqlite3_libversion_number()], [sqlite3_sourceid()],
466 ** [sqlite_version()] and [sqlite_source_id()].
467 */
468 #define SQLITE_VERSION "3.50.0"
469 #define SQLITE_VERSION_NUMBER 3050000
470 #define SQLITE_SOURCE_ID "2025-04-15 21:59:38 d22475b81c4e26ccc50f3b5626d43b32f7a2de34e5a764539554665bdda735d5"
471
472 /*
473 ** CAPI3REF: Run-Time Library Version Numbers
474 ** KEYWORDS: sqlite3_version sqlite3_sourceid
475 **
@@ -11946,12 +11946,13 @@
11946 ** To clarify, if this function is called and then a changeset constructed
11947 ** using [sqlite3session_changeset()], then after applying that changeset to
11948 ** database zFrom the contents of the two compatible tables would be
11949 ** identical.
11950 **
11951 ** Unless the call to this function is a no-op as described above, it is an
11952 ** error if database zFrom does not exist or does not contain the required
11953 ** compatible table.
11954 **
11955 ** If the operation is successful, SQLITE_OK is returned. Otherwise, an SQLite
11956 ** error code. In this case, if argument pzErrMsg is not NULL, *pzErrMsg
11957 ** may be set to point to a buffer containing an English language error
11958 ** message. It is the responsibility of the caller to free this buffer using
@@ -19162,10 +19163,11 @@
19163 unsigned noSkipScan:1; /* Do not try to use skip-scan if true */
19164 unsigned hasStat1:1; /* aiRowLogEst values come from sqlite_stat1 */
19165 unsigned bLowQual:1; /* sqlite_stat1 says this is a low-quality index */
19166 unsigned bNoQuery:1; /* Do not use this index to optimize queries */
19167 unsigned bAscKeyBug:1; /* True if the bba7b69f9849b5bf bug applies */
19168 unsigned bIdxRowid:1; /* One or more of the index keys is the ROWID */
19169 unsigned bHasVCol:1; /* Index references one or more VIRTUAL columns */
19170 unsigned bHasExpr:1; /* Index contains an expression, either a literal
19171 ** expression, or a reference to a VIRTUAL column */
19172 #ifdef SQLITE_ENABLE_STAT4
19173 int nSample; /* Number of elements in aSample[] */
@@ -30270,10 +30272,12 @@
30272 */
30273 #include "windows.h"
30274
30275 #ifdef __CYGWIN__
30276 # include <sys/cygwin.h>
30277 # include <sys/stat.h> /* amalgamator: dontcache */
30278 # include <unistd.h> /* amalgamator: dontcache */
30279 # include <errno.h> /* amalgamator: dontcache */
30280 #endif
30281
30282 /*
30283 ** Determine if we are dealing with Windows NT.
@@ -35444,11 +35448,11 @@
35448 unsigned char const *z = zIn;
35449 unsigned char const *zEnd = &z[nByte-1];
35450 int n = 0;
35451
35452 if( SQLITE_UTF16NATIVE==SQLITE_UTF16LE ) z++;
35453 while( n<nChar && z<=zEnd ){
35454 c = z[0];
35455 z += 2;
35456 if( c>=0xd8 && c<0xdc && z<=zEnd && z[0]>=0xdc && z[0]<0xe0 ) z += 2;
35457 n++;
35458 }
@@ -47726,20 +47730,20 @@
47730 { "FileTimeToLocalFileTime", (SYSCALL)FileTimeToLocalFileTime, 0 },
47731 #else
47732 { "FileTimeToLocalFileTime", (SYSCALL)0, 0 },
47733 #endif
47734
47735 #define osFileTimeToLocalFileTime ((BOOL(WINAPI*)(const FILETIME*, \
47736 LPFILETIME))aSyscall[11].pCurrent)
47737
47738 #if SQLITE_OS_WINCE
47739 { "FileTimeToSystemTime", (SYSCALL)FileTimeToSystemTime, 0 },
47740 #else
47741 { "FileTimeToSystemTime", (SYSCALL)0, 0 },
47742 #endif
47743
47744 #define osFileTimeToSystemTime ((BOOL(WINAPI*)(const FILETIME*, \
47745 LPSYSTEMTIME))aSyscall[12].pCurrent)
47746
47747 { "FlushFileBuffers", (SYSCALL)FlushFileBuffers, 0 },
47748
47749 #define osFlushFileBuffers ((BOOL(WINAPI*)(HANDLE))aSyscall[13].pCurrent)
@@ -48015,11 +48019,11 @@
48019 { "LockFile", (SYSCALL)LockFile, 0 },
48020 #else
48021 { "LockFile", (SYSCALL)0, 0 },
48022 #endif
48023
48024 #if !defined(osLockFile) && defined(SQLITE_WIN32_HAS_ANSI)
48025 #define osLockFile ((BOOL(WINAPI*)(HANDLE,DWORD,DWORD,DWORD, \
48026 DWORD))aSyscall[47].pCurrent)
48027 #endif
48028
48029 #if !SQLITE_OS_WINCE
@@ -48079,20 +48083,20 @@
48083
48084 #define osSleep ((VOID(WINAPI*)(DWORD))aSyscall[55].pCurrent)
48085
48086 { "SystemTimeToFileTime", (SYSCALL)SystemTimeToFileTime, 0 },
48087
48088 #define osSystemTimeToFileTime ((BOOL(WINAPI*)(const SYSTEMTIME*, \
48089 LPFILETIME))aSyscall[56].pCurrent)
48090
48091 #if !SQLITE_OS_WINCE && !SQLITE_OS_WINRT
48092 { "UnlockFile", (SYSCALL)UnlockFile, 0 },
48093 #else
48094 { "UnlockFile", (SYSCALL)0, 0 },
48095 #endif
48096
48097 #if !defined(osUnlockFile) && defined(SQLITE_WIN32_HAS_ANSI)
48098 #define osUnlockFile ((BOOL(WINAPI*)(HANDLE,DWORD,DWORD,DWORD, \
48099 DWORD))aSyscall[57].pCurrent)
48100 #endif
48101
48102 #if !SQLITE_OS_WINCE
@@ -48316,10 +48320,67 @@
48320 { "CancelIo", (SYSCALL)0, 0 },
48321 #endif
48322
48323 #define osCancelIo ((BOOL(WINAPI*)(HANDLE))aSyscall[81].pCurrent)
48324
48325 #if defined(SQLITE_WIN32_HAS_WIDE) && defined(_WIN32)
48326 { "GetModuleHandleW", (SYSCALL)GetModuleHandleW, 0 },
48327 #else
48328 { "GetModuleHandleW", (SYSCALL)0, 0 },
48329 #endif
48330
48331 #define osGetModuleHandleW ((HMODULE(WINAPI*)(LPCWSTR))aSyscall[82].pCurrent)
48332
48333 #ifndef _WIN32
48334 { "getenv", (SYSCALL)getenv, 0 },
48335 #else
48336 { "getenv", (SYSCALL)0, 0 },
48337 #endif
48338
48339 #define osGetenv ((const char *(*)(const char *))aSyscall[83].pCurrent)
48340
48341 #ifndef _WIN32
48342 { "getcwd", (SYSCALL)getcwd, 0 },
48343 #else
48344 { "getcwd", (SYSCALL)0, 0 },
48345 #endif
48346
48347 #define osGetcwd ((char*(*)(char*,size_t))aSyscall[84].pCurrent)
48348
48349 #ifndef _WIN32
48350 { "readlink", (SYSCALL)readlink, 0 },
48351 #else
48352 { "readlink", (SYSCALL)0, 0 },
48353 #endif
48354
48355 #define osReadlink ((ssize_t(*)(const char*,char*,size_t))aSyscall[85].pCurrent)
48356
48357 #ifndef _WIN32
48358 { "lstat", (SYSCALL)lstat, 0 },
48359 #else
48360 { "lstat", (SYSCALL)0, 0 },
48361 #endif
48362
48363 #define osLstat ((int(*)(const char*,struct stat*))aSyscall[86].pCurrent)
48364
48365 #ifndef _WIN32
48366 { "__errno", (SYSCALL)__errno, 0 },
48367 #else
48368 { "__errno", (SYSCALL)0, 0 },
48369 #endif
48370
48371 #define osErrno (*((int*(*)(void))aSyscall[87].pCurrent)())
48372
48373 #ifndef _WIN32
48374 { "cygwin_conv_path", (SYSCALL)cygwin_conv_path, 0 },
48375 #else
48376 { "cygwin_conv_path", (SYSCALL)0, 0 },
48377 #endif
48378
48379 #define osCygwin_conv_path ((size_t(*)(unsigned int, \
48380 const void *, void *, size_t))aSyscall[88].pCurrent)
48381
48382 }; /* End of the overrideable system calls */
48383
48384 /*
48385 ** This is the xSetSystemCall() method of sqlite3_vfs for all of the
48386 ** "win32" VFSes. Return SQLITE_OK upon successfully updating the
@@ -48489,10 +48550,11 @@
48550 sqlite3_mutex_leave(pMainMtx);
48551 return rc;
48552 }
48553 #endif /* SQLITE_WIN32_MALLOC */
48554
48555 #ifdef _WIN32
48556 /*
48557 ** This function outputs the specified (ANSI) string to the Win32 debugger
48558 ** (if available).
48559 */
48560
@@ -48531,10 +48593,11 @@
48593 }else{
48594 fprintf(stderr, "%s", zBuf);
48595 }
48596 #endif
48597 }
48598 #endif /* _WIN32 */
48599
48600 /*
48601 ** The following routine suspends the current thread for at least ms
48602 ** milliseconds. This is equivalent to the Win32 Sleep() interface.
48603 */
@@ -48831,10 +48894,11 @@
48894 SQLITE_PRIVATE void sqlite3MemSetDefault(void){
48895 sqlite3_config(SQLITE_CONFIG_MALLOC, sqlite3MemGetWin32());
48896 }
48897 #endif /* SQLITE_WIN32_MALLOC */
48898
48899 #ifdef _WIN32
48900 /*
48901 ** Convert a UTF-8 string to Microsoft Unicode.
48902 **
48903 ** Space to hold the returned string is obtained from sqlite3_malloc().
48904 */
@@ -48856,10 +48920,11 @@
48920 sqlite3_free(zWideText);
48921 zWideText = 0;
48922 }
48923 return zWideText;
48924 }
48925 #endif /* _WIN32 */
48926
48927 /*
48928 ** Convert a Microsoft Unicode string to UTF-8.
48929 **
48930 ** Space to hold the returned string is obtained from sqlite3_malloc().
@@ -48890,32 +48955,33 @@
48955 ** code page.
48956 **
48957 ** Space to hold the returned string is obtained from sqlite3_malloc().
48958 */
48959 static LPWSTR winMbcsToUnicode(const char *zText, int useAnsi){
48960 int nWideChar;
48961 LPWSTR zMbcsText;
48962 int codepage = useAnsi ? CP_ACP : CP_OEMCP;
48963
48964 nWideChar = osMultiByteToWideChar(codepage, 0, zText, -1, NULL,
48965 0);
48966 if( nWideChar==0 ){
48967 return 0;
48968 }
48969 zMbcsText = sqlite3MallocZero( nWideChar*sizeof(WCHAR) );
48970 if( zMbcsText==0 ){
48971 return 0;
48972 }
48973 nWideChar = osMultiByteToWideChar(codepage, 0, zText, -1, zMbcsText,
48974 nWideChar);
48975 if( nWideChar==0 ){
48976 sqlite3_free(zMbcsText);
48977 zMbcsText = 0;
48978 }
48979 return zMbcsText;
48980 }
48981
48982 #ifdef _WIN32
48983 /*
48984 ** Convert a Microsoft Unicode string to a multi-byte character string,
48985 ** using the ANSI or OEM code page.
48986 **
48987 ** Space to hold the returned string is obtained from sqlite3_malloc().
@@ -48939,10 +49005,11 @@
49005 sqlite3_free(zText);
49006 zText = 0;
49007 }
49008 return zText;
49009 }
49010 #endif /* _WIN32 */
49011
49012 /*
49013 ** Convert a multi-byte character string to UTF-8.
49014 **
49015 ** Space to hold the returned string is obtained from sqlite3_malloc().
@@ -48958,10 +49025,11 @@
49025 zTextUtf8 = winUnicodeToUtf8(zTmpWide);
49026 sqlite3_free(zTmpWide);
49027 return zTextUtf8;
49028 }
49029
49030 #ifdef _WIN32
49031 /*
49032 ** Convert a UTF-8 string to a multi-byte character string.
49033 **
49034 ** Space to hold the returned string is obtained from sqlite3_malloc().
49035 */
@@ -49007,10 +49075,11 @@
49075 #ifndef SQLITE_OMIT_AUTOINIT
49076 if( sqlite3_initialize() ) return 0;
49077 #endif
49078 return winUnicodeToUtf8(zWideText);
49079 }
49080 #endif /* _WIN32 */
49081
49082 /*
49083 ** This is a public wrapper for the winMbcsToUtf8() function.
49084 */
49085 SQLITE_API char *sqlite3_win32_mbcs_to_utf8(const char *zText){
@@ -49024,10 +49093,11 @@
49093 if( sqlite3_initialize() ) return 0;
49094 #endif
49095 return winMbcsToUtf8(zText, osAreFileApisANSI());
49096 }
49097
49098 #ifdef _WIN32
49099 /*
49100 ** This is a public wrapper for the winMbcsToUtf8() function.
49101 */
49102 SQLITE_API char *sqlite3_win32_mbcs_to_utf8_v2(const char *zText, int useAnsi){
49103 #ifdef SQLITE_ENABLE_API_ARMOR
@@ -49148,10 +49218,11 @@
49218 unsigned long type, /* Identifier for directory being set or reset */
49219 void *zValue /* New value for directory being set or reset */
49220 ){
49221 return sqlite3_win32_set_directory16(type, zValue);
49222 }
49223 #endif /* _WIN32 */
49224
49225 /*
49226 ** The return value of winGetLastErrorMsg
49227 ** is zero if the error message fits in the buffer, or non-zero
49228 ** otherwise (if the message was truncated).
@@ -49696,13 +49767,15 @@
49767 OVERLAPPED ovlp;
49768 memset(&ovlp, 0, sizeof(OVERLAPPED));
49769 ovlp.Offset = offsetLow;
49770 ovlp.OffsetHigh = offsetHigh;
49771 return osLockFileEx(*phFile, flags, 0, numBytesLow, numBytesHigh, &ovlp);
49772 #ifdef SQLITE_WIN32_HAS_ANSI
49773 }else{
49774 return osLockFile(*phFile, offsetLow, offsetHigh, numBytesLow,
49775 numBytesHigh);
49776 #endif
49777 }
49778 #endif
49779 }
49780
49781 /*
@@ -49806,13 +49879,15 @@
49879 OVERLAPPED ovlp;
49880 memset(&ovlp, 0, sizeof(OVERLAPPED));
49881 ovlp.Offset = offsetLow;
49882 ovlp.OffsetHigh = offsetHigh;
49883 return osUnlockFileEx(*phFile, 0, numBytesLow, numBytesHigh, &ovlp);
49884 #ifdef SQLITE_WIN32_HAS_ANSI
49885 }else{
49886 return osUnlockFile(*phFile, offsetLow, offsetHigh, numBytesLow,
49887 numBytesHigh);
49888 #endif
49889 }
49890 #endif
49891 }
49892
49893 /*
@@ -51222,18 +51297,95 @@
51297
51298 /*
51299 ** Convert a UTF-8 filename into whatever form the underlying
51300 ** operating system wants filenames in. Space to hold the result
51301 ** is obtained from malloc and must be freed by the calling
51302 ** function
51303 **
51304 ** On Cygwin, 3 possible input forms are accepted:
51305 ** - If the filename starts with "<drive>:/" or "<drive>:\",
51306 ** it is converted to UTF-16 as-is.
51307 ** - If the filename contains '/', it is assumed to be a
51308 ** Cygwin absolute path, it is converted to a win32
51309 ** absolute path in UTF-16.
51310 ** - Otherwise it must be a filename only, the win32 filename
51311 ** is returned in UTF-16.
51312 ** Note: If the function cygwin_conv_path() fails, only
51313 ** UTF-8 -> UTF-16 conversion will be done. This can only
51314 ** happen when the file path >32k, in which case winUtf8ToUnicode()
51315 ** will fail too.
51316 */
51317 static void *winConvertFromUtf8Filename(const char *zFilename){
51318 void *zConverted = 0;
51319 if( osIsNT() ){
51320 #ifdef __CYGWIN__
51321 int nChar;
51322 LPWSTR zWideFilename;
51323
51324 if( osCygwin_conv_path && !(winIsDriveLetterAndColon(zFilename)
51325 && winIsDirSep(zFilename[2])) ){
51326 int nByte;
51327 int convertflag = CCP_POSIX_TO_WIN_W;
51328 if( !strchr(zFilename, '/') ) convertflag |= CCP_RELATIVE;
51329 nByte = (int)osCygwin_conv_path(convertflag,
51330 zFilename, 0, 0);
51331 if( nByte>0 ){
51332 zConverted = sqlite3MallocZero(nByte+12);
51333 if ( zConverted==0 ){
51334 return zConverted;
51335 }
51336 zWideFilename = zConverted;
51337 /* Filenames should be prefixed, except when converted
51338 * full path already starts with "\\?\". */
51339 if( osCygwin_conv_path(convertflag, zFilename,
51340 zWideFilename+4, nByte)==0 ){
51341 if( (convertflag&CCP_RELATIVE) ){
51342 memmove(zWideFilename, zWideFilename+4, nByte);
51343 }else if( memcmp(zWideFilename+4, L"\\\\", 4) ){
51344 memcpy(zWideFilename, L"\\\\?\\", 8);
51345 }else if( zWideFilename[6]!='?' ){
51346 memmove(zWideFilename+6, zWideFilename+4, nByte);
51347 memcpy(zWideFilename, L"\\\\?\\UNC", 14);
51348 }else{
51349 memmove(zWideFilename, zWideFilename+4, nByte);
51350 }
51351 return zConverted;
51352 }
51353 sqlite3_free(zConverted);
51354 }
51355 }
51356 nChar = osMultiByteToWideChar(CP_UTF8, 0, zFilename, -1, NULL, 0);
51357 if( nChar==0 ){
51358 return 0;
51359 }
51360 zWideFilename = sqlite3MallocZero( nChar*sizeof(WCHAR)+12 );
51361 if( zWideFilename==0 ){
51362 return 0;
51363 }
51364 nChar = osMultiByteToWideChar(CP_UTF8, 0, zFilename, -1,
51365 zWideFilename, nChar);
51366 if( nChar==0 ){
51367 sqlite3_free(zWideFilename);
51368 zWideFilename = 0;
51369 }else if( nChar>MAX_PATH
51370 && winIsDriveLetterAndColon(zFilename)
51371 && winIsDirSep(zFilename[2]) ){
51372 memmove(zWideFilename+4, zWideFilename, nChar*sizeof(WCHAR));
51373 zWideFilename[2] = '\\';
51374 memcpy(zWideFilename, L"\\\\?\\", 8);
51375 }else if( nChar>MAX_PATH
51376 && winIsDirSep(zFilename[0]) && winIsDirSep(zFilename[1])
51377 && zFilename[2] != '?' ){
51378 memmove(zWideFilename+6, zWideFilename, nChar*sizeof(WCHAR));
51379 memcpy(zWideFilename, L"\\\\?\\UNC", 14);
51380 }
51381 zConverted = zWideFilename;
51382 #else
51383 zConverted = winUtf8ToUnicode(zFilename);
51384 #endif /* __CYGWIN__ */
51385 }
51386 #if defined(SQLITE_WIN32_HAS_ANSI) && defined(_WIN32)
51387 else{
51388 zConverted = winUtf8ToMbcs(zFilename, osAreFileApisANSI());
51389 }
51390 #endif
51391 /* caller will handle out of memory */
@@ -52058,11 +52210,11 @@
52210 **
52211 ** This division contains the implementation of methods on the
52212 ** sqlite3_vfs object.
52213 */
52214
52215 #if 0 /* No longer necessary */
52216 /*
52217 ** Convert a filename from whatever the underlying operating system
52218 ** supports for filenames into UTF-8. Space to hold the result is
52219 ** obtained from malloc and must be freed by the calling function.
52220 */
@@ -52091,11 +52243,18 @@
52243 int nLen = sqlite3Strlen30(zBuf);
52244 if( nLen>0 ){
52245 if( winIsDirSep(zBuf[nLen-1]) ){
52246 return 1;
52247 }else if( nLen+1<nBuf ){
52248 if( !osGetenv ){
52249 zBuf[nLen] = winGetDirSep();
52250 }else if( winIsDriveLetterAndColon(zBuf) && winIsDirSep(zBuf[2]) ){
52251 zBuf[nLen] = '\\';
52252 zBuf[2]='\\';
52253 }else{
52254 zBuf[nLen] = '/';
52255 }
52256 zBuf[nLen+1] = '\0';
52257 return 1;
52258 }
52259 }
52260 }
@@ -52118,11 +52277,11 @@
52277 /*
52278 ** Create a temporary file name and store the resulting pointer into pzBuf.
52279 ** The pointer returned in pzBuf must be freed via sqlite3_free().
52280 */
52281 static int winGetTempname(sqlite3_vfs *pVfs, char **pzBuf){
52282 static const char zChars[] =
52283 "abcdefghijklmnopqrstuvwxyz"
52284 "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
52285 "0123456789";
52286 size_t i, j;
52287 DWORD pid;
@@ -52169,11 +52328,11 @@
52328 }
52329 sqlite3_mutex_leave(sqlite3MutexAlloc(SQLITE_MUTEX_STATIC_TEMPDIR));
52330 }
52331
52332 #if defined(__CYGWIN__)
52333 else if( osGetenv!=NULL ){
52334 static const char *azDirs[] = {
52335 0, /* getenv("SQLITE_TMPDIR") */
52336 0, /* getenv("TMPDIR") */
52337 0, /* getenv("TMP") */
52338 0, /* getenv("TEMP") */
@@ -52185,24 +52344,24 @@
52344 0 /* List terminator */
52345 };
52346 unsigned int i;
52347 const char *zDir = 0;
52348
52349 if( !azDirs[0] ) azDirs[0] = osGetenv("SQLITE_TMPDIR");
52350 if( !azDirs[1] ) azDirs[1] = osGetenv("TMPDIR");
52351 if( !azDirs[2] ) azDirs[2] = osGetenv("TMP");
52352 if( !azDirs[3] ) azDirs[3] = osGetenv("TEMP");
52353 if( !azDirs[4] ) azDirs[4] = osGetenv("USERPROFILE");
52354 for(i=0; i<sizeof(azDirs)/sizeof(azDirs[0]); zDir=azDirs[i++]){
52355 void *zConverted;
52356 if( zDir==0 ) continue;
52357 /* If the path starts with a drive letter followed by the colon
52358 ** character, assume it is already a native Win32 path; otherwise,
52359 ** it must be converted to a native Win32 path via the Cygwin API
52360 ** prior to using it.
52361 */
52362 {
52363 zConverted = winConvertFromUtf8Filename(zDir);
52364 if( !zConverted ){
52365 sqlite3_free(zBuf);
52366 OSTRACE(("TEMP-FILENAME rc=SQLITE_IOERR_NOMEM\n"));
52367 return SQLITE_IOERR_NOMEM_BKPT;
@@ -52211,19 +52370,20 @@
52370 sqlite3_snprintf(nMax, zBuf, "%s", zDir);
52371 sqlite3_free(zConverted);
52372 break;
52373 }
52374 sqlite3_free(zConverted);
52375 #if 0 /* No longer necessary */
52376 }else{
52377 zConverted = sqlite3MallocZero( nMax+1 );
52378 if( !zConverted ){
52379 sqlite3_free(zBuf);
52380 OSTRACE(("TEMP-FILENAME rc=SQLITE_IOERR_NOMEM\n"));
52381 return SQLITE_IOERR_NOMEM_BKPT;
52382 }
52383 if( osCygwin_conv_path(
52384 CCP_POSIX_TO_WIN_W, zDir,
52385 zConverted, nMax+1)<0 ){
52386 sqlite3_free(zConverted);
52387 sqlite3_free(zBuf);
52388 OSTRACE(("TEMP-FILENAME rc=SQLITE_IOERR_CONVPATH\n"));
52389 return winLogError(SQLITE_IOERR_CONVPATH, (DWORD)errno,
@@ -52245,14 +52405,17 @@
52405 sqlite3_free(zUtf8);
52406 sqlite3_free(zConverted);
52407 break;
52408 }
52409 sqlite3_free(zConverted);
52410 #endif /* No longer necessary */
52411 }
52412 }
52413 }
52414 #endif
52415
52416 #if !SQLITE_OS_WINRT && defined(_WIN32)
52417 else if( osIsNT() ){
52418 char *zMulti;
52419 LPWSTR zWidePath = sqlite3MallocZero( nMax*sizeof(WCHAR) );
52420 if( !zWidePath ){
52421 sqlite3_free(zBuf);
@@ -52372,11 +52535,11 @@
52535 &sAttrData)) && winRetryIoerr(&cnt, &lastErrno) ){}
52536 if( !rc ){
52537 return 0; /* Invalid name? */
52538 }
52539 attr = sAttrData.dwFileAttributes;
52540 #if SQLITE_OS_WINCE==0 && defined(SQLITE_WIN32_HAS_ANSI)
52541 }else{
52542 attr = osGetFileAttributesA((char*)zConverted);
52543 #endif
52544 }
52545 return (attr!=INVALID_FILE_ATTRIBUTES) && (attr&FILE_ATTRIBUTE_DIRECTORY);
@@ -52388,10 +52551,16 @@
52551 const char *zFilename, /* Name of file to check */
52552 int flags, /* Type of test to make on this file */
52553 int *pResOut /* OUT: Result */
52554 );
52555
52556 /*
52557 ** The Windows version of xAccess() accepts an extra bit in the flags
52558 ** parameter that prevents an anti-virus retry loop.
52559 */
52560 #define NORETRY 0x4000
52561
52562 /*
52563 ** Open a file.
52564 */
52565 static int winOpen(
52566 sqlite3_vfs *pVfs, /* Used to get maximum path length and AppData */
@@ -52412,10 +52581,11 @@
52581 winVfsAppData *pAppData;
52582 winFile *pFile = (winFile*)id;
52583 void *zConverted; /* Filename in OS encoding */
52584 const char *zUtf8Name = zName; /* Filename in UTF-8 encoding */
52585 int cnt = 0;
52586 int isRO = 0; /* file is known to be accessible readonly */
52587
52588 /* If argument zPath is a NULL pointer, this function is required to open
52589 ** a temporary file. Use this buffer to store the file name in.
52590 */
52591 char *zTmpname = 0; /* For temporary filename, if necessary. */
@@ -52576,13 +52746,13 @@
52746 dwShareMode,
52747 dwCreationDisposition,
52748 &extendedParameters);
52749 if( h!=INVALID_HANDLE_VALUE ) break;
52750 if( isReadWrite ){
52751 int rc2;
52752 sqlite3BeginBenignMalloc();
52753 rc2 = winAccess(pVfs, zUtf8Name, SQLITE_ACCESS_READ|NORETRY, &isRO);
52754 sqlite3EndBenignMalloc();
52755 if( rc2==SQLITE_OK && isRO ) break;
52756 }
52757 }while( winRetryIoerr(&cnt, &lastErrno) );
52758 #else
@@ -52593,13 +52763,13 @@
52763 dwCreationDisposition,
52764 dwFlagsAndAttributes,
52765 NULL);
52766 if( h!=INVALID_HANDLE_VALUE ) break;
52767 if( isReadWrite ){
52768 int rc2;
52769 sqlite3BeginBenignMalloc();
52770 rc2 = winAccess(pVfs, zUtf8Name, SQLITE_ACCESS_READ|NORETRY, &isRO);
52771 sqlite3EndBenignMalloc();
52772 if( rc2==SQLITE_OK && isRO ) break;
52773 }
52774 }while( winRetryIoerr(&cnt, &lastErrno) );
52775 #endif
@@ -52613,13 +52783,13 @@
52783 dwCreationDisposition,
52784 dwFlagsAndAttributes,
52785 NULL);
52786 if( h!=INVALID_HANDLE_VALUE ) break;
52787 if( isReadWrite ){
52788 int rc2;
52789 sqlite3BeginBenignMalloc();
52790 rc2 = winAccess(pVfs, zUtf8Name, SQLITE_ACCESS_READ|NORETRY, &isRO);
52791 sqlite3EndBenignMalloc();
52792 if( rc2==SQLITE_OK && isRO ) break;
52793 }
52794 }while( winRetryIoerr(&cnt, &lastErrno) );
52795 }
@@ -52630,11 +52800,11 @@
52800 dwDesiredAccess, (h==INVALID_HANDLE_VALUE) ? "failed" : "ok"));
52801
52802 if( h==INVALID_HANDLE_VALUE ){
52803 sqlite3_free(zConverted);
52804 sqlite3_free(zTmpname);
52805 if( isReadWrite && isRO && !isExclusive ){
52806 return winOpen(pVfs, zName, id,
52807 ((flags|SQLITE_OPEN_READONLY) &
52808 ~(SQLITE_OPEN_CREATE|SQLITE_OPEN_READWRITE)),
52809 pOutFlags);
52810 }else{
@@ -52832,11 +53002,17 @@
53002 ){
53003 DWORD attr;
53004 int rc = 0;
53005 DWORD lastErrno = 0;
53006 void *zConverted;
53007 int noRetry = 0; /* Do not use winRetryIoerr() */
53008 UNUSED_PARAMETER(pVfs);
53009
53010 if( (flags & NORETRY)!=0 ){
53011 noRetry = 1;
53012 flags &= ~NORETRY;
53013 }
53014
53015 SimulateIOError( return SQLITE_IOERR_ACCESS; );
53016 OSTRACE(("ACCESS name=%s, flags=%x, pResOut=%p\n",
53017 zFilename, flags, pResOut));
53018
@@ -52856,11 +53032,14 @@
53032 int cnt = 0;
53033 WIN32_FILE_ATTRIBUTE_DATA sAttrData;
53034 memset(&sAttrData, 0, sizeof(sAttrData));
53035 while( !(rc = osGetFileAttributesExW((LPCWSTR)zConverted,
53036 GetFileExInfoStandard,
53037 &sAttrData))
53038 && !noRetry
53039 && winRetryIoerr(&cnt, &lastErrno)
53040 ){ /* Loop until true */}
53041 if( rc ){
53042 /* For an SQLITE_ACCESS_EXISTS query, treat a zero-length file
53043 ** as if it does not exist.
53044 */
53045 if( flags==SQLITE_ACCESS_EXISTS
@@ -52924,10 +53103,11 @@
53103 const char *zPathname
53104 ){
53105 return ( sqlite3Isalpha(zPathname[0]) && zPathname[1]==':' );
53106 }
53107
53108 #ifdef _WIN32
53109 /*
53110 ** Returns non-zero if the specified path name should be used verbatim. If
53111 ** non-zero is returned from this function, the calling function must simply
53112 ** use the provided path name verbatim -OR- resolve it into a full path name
53113 ** using the GetFullPathName Win32 API function (if available).
@@ -52960,10 +53140,74 @@
53140 ** If we get to this point, the path name should almost certainly be a purely
53141 ** relative one (i.e. not a UNC name, not absolute, and not volume relative).
53142 */
53143 return FALSE;
53144 }
53145 #endif /* _WIN32 */
53146
53147 #ifdef __CYGWIN__
53148 /*
53149 ** Simplify a filename into its canonical form
53150 ** by making the following changes:
53151 **
53152 ** * convert any '/' to '\' (win32) or reverse (Cygwin)
53153 ** * removing any trailing and duplicate / (except for UNC paths)
53154 ** * convert /./ into just /
53155 **
53156 ** Changes are made in-place. Return the new name length.
53157 **
53158 ** The original filename is in z[0..]. If the path is shortened,
53159 ** no-longer used bytes will be written by '\0'.
53160 */
53161 static void winSimplifyName(char *z){
53162 int i, j;
53163 for(i=j=0; z[i]; ++i){
53164 if( winIsDirSep(z[i]) ){
53165 #if !defined(SQLITE_TEST)
53166 /* Some test-cases assume that "./foo" and "foo" are different */
53167 if( z[i+1]=='.' && winIsDirSep(z[i+2]) ){
53168 ++i;
53169 continue;
53170 }
53171 #endif
53172 if( !z[i+1] || (winIsDirSep(z[i+1]) && (i!=0)) ){
53173 continue;
53174 }
53175 z[j++] = osGetenv?'/':'\\';
53176 }else{
53177 z[j++] = z[i];
53178 }
53179 }
53180 while(j<i) z[j++] = '\0';
53181 }
53182
53183 #define SQLITE_MAX_SYMLINKS 100
53184
53185 static int mkFullPathname(
53186 const char *zPath, /* Input path */
53187 char *zOut, /* Output buffer */
53188 int nOut /* Allocated size of buffer zOut */
53189 ){
53190 int nPath = sqlite3Strlen30(zPath);
53191 int iOff = 0;
53192 if( zPath[0]!='/' ){
53193 if( osGetcwd(zOut, nOut-2)==0 ){
53194 return winLogError(SQLITE_CANTOPEN_BKPT, (DWORD)osErrno, "getcwd", zPath);
53195 }
53196 iOff = sqlite3Strlen30(zOut);
53197 zOut[iOff++] = '/';
53198 }
53199 if( (iOff+nPath+1)>nOut ){
53200 /* SQLite assumes that xFullPathname() nul-terminates the output buffer
53201 ** even if it returns an error. */
53202 zOut[iOff] = '\0';
53203 return SQLITE_CANTOPEN_BKPT;
53204 }
53205 sqlite3_snprintf(nOut-iOff, &zOut[iOff], "%s", zPath);
53206 return SQLITE_OK;
53207 }
53208 #endif /* __CYGWIN__ */
53209
53210 /*
53211 ** Turn a relative pathname into a full pathname. Write the full
53212 ** pathname into zOut[]. zOut[] will be at least pVfs->mxPathname
53213 ** bytes in size.
@@ -52972,12 +53216,12 @@
53216 sqlite3_vfs *pVfs, /* Pointer to vfs object */
53217 const char *zRelative, /* Possibly relative input path */
53218 int nFull, /* Size of output buffer in bytes */
53219 char *zFull /* Output buffer */
53220 ){
53221 #if !SQLITE_OS_WINCE && !SQLITE_OS_WINRT
53222 int nByte;
53223 void *zConverted;
53224 char *zOut;
53225 #endif
53226
53227 /* If this path name begins with "/X:" or "\\?\", where "X" is any
@@ -52986,68 +53230,114 @@
53230 if( zRelative[0]=='/' && (winIsDriveLetterAndColon(zRelative+1)
53231 || winIsLongPathPrefix(zRelative+1)) ){
53232 zRelative++;
53233 }
53234
53235 SimulateIOError( return SQLITE_ERROR );
53236
53237 #ifdef __CYGWIN__
53238 if( osGetcwd ){
53239 zFull[nFull-1] = '\0';
53240 if( !winIsDriveLetterAndColon(zRelative) || !winIsDirSep(zRelative[2]) ){
53241 int rc = SQLITE_OK;
53242 int nLink = 1; /* Number of symbolic links followed so far */
53243 const char *zIn = zRelative; /* Input path for each iteration of loop */
53244 char *zDel = 0;
53245 struct stat buf;
53246
53247 UNUSED_PARAMETER(pVfs);
53248
53249 do {
53250 /* Call lstat() on path zIn. Set bLink to true if the path is a symbolic
53251 ** link, or false otherwise. */
53252 int bLink = 0;
53253 if( osLstat && osReadlink ) {
53254 if( osLstat(zIn, &buf)!=0 ){
53255 int myErrno = osErrno;
53256 if( myErrno!=ENOENT ){
53257 rc = winLogError(SQLITE_CANTOPEN_BKPT, (DWORD)myErrno, "lstat", zIn);
53258 }
53259 }else{
53260 bLink = ((buf.st_mode & 0170000) == 0120000);
53261 }
53262
53263 if( bLink ){
53264 if( zDel==0 ){
53265 zDel = sqlite3MallocZero(nFull);
53266 if( zDel==0 ) rc = SQLITE_NOMEM;
53267 }else if( ++nLink>SQLITE_MAX_SYMLINKS ){
53268 rc = SQLITE_CANTOPEN_BKPT;
53269 }
53270
53271 if( rc==SQLITE_OK ){
53272 nByte = osReadlink(zIn, zDel, nFull-1);
53273 if( nByte ==(DWORD)-1 ){
53274 rc = winLogError(SQLITE_CANTOPEN_BKPT, (DWORD)osErrno, "readlink", zIn);
53275 }else{
53276 if( zDel[0]!='/' ){
53277 int n;
53278 for(n = sqlite3Strlen30(zIn); n>0 && zIn[n-1]!='/'; n--);
53279 if( nByte+n+1>nFull ){
53280 rc = SQLITE_CANTOPEN_BKPT;
53281 }else{
53282 memmove(&zDel[n], zDel, nByte+1);
53283 memcpy(zDel, zIn, n);
53284 nByte += n;
53285 }
53286 }
53287 zDel[nByte] = '\0';
53288 }
53289 }
53290
53291 zIn = zDel;
53292 }
53293 }
53294
53295 assert( rc!=SQLITE_OK || zIn!=zFull || zIn[0]=='/' );
53296 if( rc==SQLITE_OK && zIn!=zFull ){
53297 rc = mkFullPathname(zIn, zFull, nFull);
53298 }
53299 if( bLink==0 ) break;
53300 zIn = zFull;
53301 }while( rc==SQLITE_OK );
53302
53303 sqlite3_free(zDel);
53304 winSimplifyName(zFull);
53305 return rc;
53306 }
53307 }
53308 #endif /* __CYGWIN__ */
53309 #if 0 /* This doesn't work correctly at all! See:
53310 <https://marc.info/?l=sqlite-users&m=139299149416314&w=2>
53311 */
53312 SimulateIOError( return SQLITE_ERROR );
53313 UNUSED_PARAMETER(nFull);
53314 assert( nFull>=pVfs->mxPathname );
53315 char *zOut = sqlite3MallocZero( pVfs->mxPathname+1 );
53316 if( !zOut ){
53317 return SQLITE_IOERR_NOMEM_BKPT;
53318 }
53319 if( osCygwin_conv_path(
53320 CCP_POSIX_TO_WIN_W,
53321 zRelative, zOut, pVfs->mxPathname+1)<0 ){
53322 sqlite3_free(zOut);
53323 return winLogError(SQLITE_CANTOPEN_CONVPATH, (DWORD)errno,
53324 "winFullPathname2", zRelative);
53325 }else{
53326 char *zUtf8 = winConvertToUtf8Filename(zOut);
53327 if( !zUtf8 ){
53328 sqlite3_free(zOut);
53329 return SQLITE_IOERR_NOMEM_BKPT;
53330 }
53331 sqlite3_snprintf(MIN(nFull, pVfs->mxPathname), zFull, "%s", zUtf8);
53332 sqlite3_free(zUtf8);
53333 sqlite3_free(zOut);
53334 }
53335 return SQLITE_OK;
53336 #endif
53337
53338 #if (SQLITE_OS_WINCE || SQLITE_OS_WINRT) && defined(_WIN32)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53339 SimulateIOError( return SQLITE_ERROR );
53340 /* WinCE has no concept of a relative pathname, or so I am told. */
53341 /* WinRT has no way to convert a relative path to an absolute one. */
53342 if ( sqlite3_data_directory && !winIsVerbatimPathname(zRelative) ){
53343 /*
@@ -53062,11 +53352,12 @@
53352 sqlite3_snprintf(MIN(nFull, pVfs->mxPathname), zFull, "%s", zRelative);
53353 }
53354 return SQLITE_OK;
53355 #endif
53356
53357 #if !SQLITE_OS_WINCE && !SQLITE_OS_WINRT
53358 #if defined(_WIN32)
53359 /* It's odd to simulate an io-error here, but really this is just
53360 ** using the io-error infrastructure to test that SQLite handles this
53361 ** function failing. This function could fail if, for example, the
53362 ** current working directory has been unlinked.
53363 */
@@ -53080,10 +53371,11 @@
53371 */
53372 sqlite3_snprintf(MIN(nFull, pVfs->mxPathname), zFull, "%s%c%s",
53373 sqlite3_data_directory, winGetDirSep(), zRelative);
53374 return SQLITE_OK;
53375 }
53376 #endif
53377 zConverted = winConvertFromUtf8Filename(zRelative);
53378 if( zConverted==0 ){
53379 return SQLITE_IOERR_NOMEM_BKPT;
53380 }
53381 if( osIsNT() ){
@@ -53092,16 +53384,17 @@
53384 if( nByte==0 ){
53385 sqlite3_free(zConverted);
53386 return winLogError(SQLITE_CANTOPEN_FULLPATH, osGetLastError(),
53387 "winFullPathname1", zRelative);
53388 }
53389 nByte += 3;
53390 zTemp = sqlite3MallocZero( nByte*sizeof(zTemp[0]) );
53391 if( zTemp==0 ){
53392 sqlite3_free(zConverted);
53393 return SQLITE_IOERR_NOMEM_BKPT;
53394 }
53395 nByte = osGetFullPathNameW((LPCWSTR)zConverted, nByte, zTemp, 0);
53396 if( nByte==0 ){
53397 sqlite3_free(zConverted);
53398 sqlite3_free(zTemp);
53399 return winLogError(SQLITE_CANTOPEN_FULLPATH, osGetLastError(),
53400 "winFullPathname2", zRelative);
@@ -53135,11 +53428,30 @@
53428 zOut = winMbcsToUtf8(zTemp, osAreFileApisANSI());
53429 sqlite3_free(zTemp);
53430 }
53431 #endif
53432 if( zOut ){
53433 #ifdef __CYGWIN__
53434 if( memcmp(zOut, "\\\\?\\", 4) ){
53435 sqlite3_snprintf(MIN(nFull, pVfs->mxPathname), zFull, "%s", zOut);
53436 }else if( memcmp(zOut+4, "UNC\\", 4) ){
53437 sqlite3_snprintf(MIN(nFull, pVfs->mxPathname), zFull, "%s", zOut+4);
53438 }else{
53439 char *p = zOut+6;
53440 *p = '\\';
53441 if( osGetcwd ){
53442 /* On Cygwin, UNC paths use forward slashes */
53443 while( *p ){
53444 if( *p=='\\' ) *p = '/';
53445 ++p;
53446 }
53447 }
53448 sqlite3_snprintf(MIN(nFull, pVfs->mxPathname), zFull, "%s", zOut+6);
53449 }
53450 #else
53451 sqlite3_snprintf(MIN(nFull, pVfs->mxPathname), zFull, "%s", zOut);
53452 #endif /* __CYGWIN__ */
53453 sqlite3_free(zOut);
53454 return SQLITE_OK;
53455 }else{
53456 return SQLITE_IOERR_NOMEM_BKPT;
53457 }
@@ -53165,11 +53477,13 @@
53477 ** Interfaces for opening a shared library, finding entry points
53478 ** within the shared library, and closing the shared library.
53479 */
53480 static void *winDlOpen(sqlite3_vfs *pVfs, const char *zFilename){
53481 HANDLE h;
53482 #if 0 /* This doesn't work correctly at all! See:
53483 <https://marc.info/?l=sqlite-users&m=139299149416314&w=2>
53484 */
53485 int nFull = pVfs->mxPathname+1;
53486 char *zFull = sqlite3MallocZero( nFull );
53487 void *zConverted = 0;
53488 if( zFull==0 ){
53489 OSTRACE(("DLOPEN name=%s, handle=%p\n", zFilename, (void*)0));
@@ -53532,11 +53846,11 @@
53846 };
53847 #endif
53848
53849 /* Double-check that the aSyscall[] array has been constructed
53850 ** correctly. See ticket [bb3a86e890c8e96ab] */
53851 assert( ArraySize(aSyscall)==89 );
53852
53853 /* get memory map allocation granularity */
53854 memset(&winSysInfo, 0, sizeof(SYSTEM_INFO));
53855 #if SQLITE_OS_WINRT
53856 osGetNativeSystemInfo(&winSysInfo);
@@ -66508,14 +66822,12 @@
66822 s2 = aIn[1];
66823 }else{
66824 s1 = s2 = 0;
66825 }
66826
66827 /* nByte is a multiple of 8 between 8 and 65536 */
66828 assert( nByte>=8 && (nByte&7)==0 && nByte<=65536 );
 
 
66829
66830 if( !nativeCksum ){
66831 do {
66832 s1 += BYTESWAP32(aData[0]) + s2;
66833 s2 += BYTESWAP32(aData[1]) + s1;
@@ -83726,11 +84038,11 @@
84038 ** many different strings can be converted into the same int or real.
84039 ** If a table contains a numeric value and an index is based on the
84040 ** corresponding string value, then it is important that the string be
84041 ** derived from the numeric value, not the other way around, to ensure
84042 ** that the index and table are consistent. See ticket
84043 ** https://sqlite.org/src/info/343634942dd54ab (2018-01-31) for
84044 ** an example.
84045 **
84046 ** This routine looks at pMem to verify that if it has both a numeric
84047 ** representation and a string representation then the string rep has
84048 ** been derived from the numeric and not the other way around. It returns
@@ -93014,11 +93326,11 @@
93326 unsigned char enc
93327 ){
93328 assert( xDel!=SQLITE_DYNAMIC );
93329 if( enc!=SQLITE_UTF8 ){
93330 if( enc==SQLITE_UTF16 ) enc = SQLITE_UTF16NATIVE;
93331 nData &= ~(u64)1;
93332 }
93333 return bindText(pStmt, i, zData, nData, xDel, enc);
93334 }
93335 #ifndef SQLITE_OMIT_UTF16
93336 SQLITE_API int sqlite3_bind_text16(
@@ -125166,11 +125478,11 @@
125478 if( !isDupColumn(pIdx, pIdx->nKeyCol, pPk, i) ){
125479 testcase( hasColumn(pIdx->aiColumn, pIdx->nKeyCol, pPk->aiColumn[i]) );
125480 pIdx->aiColumn[j] = pPk->aiColumn[i];
125481 pIdx->azColl[j] = pPk->azColl[i];
125482 if( pPk->aSortOrder[i] ){
125483 /* See ticket https://sqlite.org/src/info/bba7b69f9849b5bf */
125484 pIdx->bAscKeyBug = 1;
125485 }
125486 j++;
125487 }
125488 }
@@ -126543,11 +126855,11 @@
126855 /* This OP_SeekEnd opcode makes index insert for a REINDEX go much
126856 ** faster by avoiding unnecessary seeks. But the optimization does
126857 ** not work for UNIQUE constraint indexes on WITHOUT ROWID tables
126858 ** with DESC primary keys, since those indexes have there keys in
126859 ** a different order from the main table.
126860 ** See ticket: https://sqlite.org/src/info/bba7b69f9849b5bf
126861 */
126862 sqlite3VdbeAddOp1(v, OP_SeekEnd, iIdx);
126863 }
126864 sqlite3VdbeAddOp2(v, OP_IdxInsert, iIdx, regRecord);
126865 sqlite3VdbeChangeP5(v, OPFLAG_USESEEKRESULT);
@@ -126927,10 +127239,11 @@
127239 }else{
127240 j = pCExpr->iColumn;
127241 assert( j<=0x7fff );
127242 if( j<0 ){
127243 j = pTab->iPKey;
127244 pIndex->bIdxRowid = 1;
127245 }else{
127246 if( pTab->aCol[j].notNull==0 ){
127247 pIndex->uniqNotNull = 0;
127248 }
127249 if( pTab->aCol[j].colFlags & COLFLAG_VIRTUAL ){
@@ -136632,11 +136945,11 @@
136945 ** OE_Update guarantees that only a single row will change, so it
136946 ** must happen before OE_Replace. Technically, OE_Abort and OE_Rollback
136947 ** could happen in any order, but they are grouped up front for
136948 ** convenience.
136949 **
136950 ** 2018-08-14: Ticket https://sqlite.org/src/info/908f001483982c43
136951 ** The order of constraints used to have OE_Update as (2) and OE_Abort
136952 ** and so forth as (1). But apparently PostgreSQL checks the OE_Update
136953 ** constraint before any others, so it had to be moved.
136954 **
136955 ** Constraint checking code is generated in this order:
@@ -147785,10 +148098,11 @@
148098 }
148099
148100 multi_select_end:
148101 pDest->iSdst = dest.iSdst;
148102 pDest->nSdst = dest.nSdst;
148103 pDest->iSDParm2 = dest.iSDParm2;
148104 if( pDelete ){
148105 sqlite3ParserAddCleanup(pParse, sqlite3SelectDeleteGeneric, pDelete);
148106 }
148107 return rc;
148108 }
@@ -149395,11 +149709,12 @@
149709 && pE2->iColumn==pColumn->iColumn
149710 ){
149711 return; /* Already present. Return without doing anything. */
149712 }
149713 }
149714 assert( SQLITE_AFF_NONE<SQLITE_AFF_BLOB );
149715 if( sqlite3ExprAffinity(pColumn)<=SQLITE_AFF_BLOB ){
149716 pConst->bHasAffBlob = 1;
149717 }
149718
149719 pConst->nConst++;
149720 pConst->apExpr = sqlite3DbReallocOrFree(pConst->pParse->db, pConst->apExpr,
@@ -149470,11 +149785,12 @@
149785 for(i=0; i<pConst->nConst; i++){
149786 Expr *pColumn = pConst->apExpr[i*2];
149787 if( pColumn==pExpr ) continue;
149788 if( pColumn->iTable!=pExpr->iTable ) continue;
149789 if( pColumn->iColumn!=pExpr->iColumn ) continue;
149790 assert( SQLITE_AFF_NONE<SQLITE_AFF_BLOB );
149791 if( bIgnoreAffBlob && sqlite3ExprAffinity(pColumn)<=SQLITE_AFF_BLOB ){
149792 break;
149793 }
149794 /* A match is found. Add the EP_FixedCol property */
149795 pConst->nChng++;
149796 ExprClearProperty(pExpr, EP_Leaf);
@@ -150123,11 +150439,11 @@
150439 **
150440 ** This transformation is necessary because the multiSelectOrderBy() routine
150441 ** above that generates the code for a compound SELECT with an ORDER BY clause
150442 ** uses a merge algorithm that requires the same collating sequence on the
150443 ** result columns as on the ORDER BY clause. See ticket
150444 ** http://sqlite.org/src/info/6709574d2a
150445 **
150446 ** This transformation is only needed for EXCEPT, INTERSECT, and UNION.
150447 ** The UNION ALL operator works fine with multiSelectOrderBy() even when
150448 ** there are COLLATE terms in the ORDER BY.
150449 */
@@ -152654,10 +152970,16 @@
152970 pWInfo = sqlite3WhereBegin(pParse, pTabList, pWhere, sSort.pOrderBy,
152971 p->pEList, p, wctrlFlags, p->nSelectRow);
152972 if( pWInfo==0 ) goto select_end;
152973 if( sqlite3WhereOutputRowCount(pWInfo) < p->nSelectRow ){
152974 p->nSelectRow = sqlite3WhereOutputRowCount(pWInfo);
152975 if( pDest->eDest<=SRT_DistQueue && pDest->eDest>=SRT_DistFifo ){
152976 /* TUNING: For a UNION CTE, because UNION is implies DISTINCT,
152977 ** reduce the estimated output row count by 8 (LogEst 30).
152978 ** Search for tag-20250414a to see other cases */
152979 p->nSelectRow -= 30;
152980 }
152981 }
152982 if( sDistinct.isTnct && sqlite3WhereIsDistinct(pWInfo) ){
152983 sDistinct.eTnctType = sqlite3WhereIsDistinct(pWInfo);
152984 }
152985 if( sSort.pOrderBy ){
@@ -156925,11 +157247,11 @@
157247 iDb = sqlite3TwoPartName(pParse, pNm, pNm, &pNm);
157248 if( iDb<0 ) goto build_vacuum_end;
157249 #else
157250 /* When SQLITE_BUG_COMPATIBLE_20160819 is defined, unrecognized arguments
157251 ** to VACUUM are silently ignored. This is a back-out of a bug fix that
157252 ** occurred on 2016-08-19 (https://sqlite.org/src/info/083f9e6270).
157253 ** The buggy behavior is required for binary compatibility with some
157254 ** legacy applications. */
157255 iDb = sqlite3FindDb(pParse->db, pNm);
157256 if( iDb<0 ) iDb = 0;
157257 #endif
@@ -159820,11 +160142,11 @@
160142
160143
160144 /*
160145 ** pX is an expression of the form: (vector) IN (SELECT ...)
160146 ** In other words, it is a vector IN operator with a SELECT clause on the
160147 ** RHS. But not all terms in the vector are indexable and the terms might
160148 ** not be in the correct order for indexing.
160149 **
160150 ** This routine makes a copy of the input pX expression and then adjusts
160151 ** the vector on the LHS with corresponding changes to the SELECT so that
160152 ** the vector contains only index terms and those terms are in the correct
@@ -161642,11 +161964,11 @@
161964 ** ON or USING clause of a LEFT JOIN, and terms that are usable as
161965 ** indices.
161966 **
161967 ** This optimization also only applies if the (x1 OR x2 OR ...) term
161968 ** is not contained in the ON clause of a LEFT JOIN.
161969 ** See ticket http://sqlite.org/src/info/f2369304e4
161970 **
161971 ** 2022-02-04: Do not push down slices of a row-value comparison.
161972 ** In other words, "w" or "y" may not be a slice of a vector. Otherwise,
161973 ** the initialization of the right-hand operand of the vector comparison
161974 ** might not occur, or might occur only in an OR branch that is not
@@ -167615,11 +167937,11 @@
167937 }
167938
167939 if( (pNew->wsFlags & WHERE_TOP_LIMIT)==0
167940 && pNew->u.btree.nEq<pProbe->nColumn
167941 && (pNew->u.btree.nEq<pProbe->nKeyCol ||
167942 (pProbe->idxType!=SQLITE_IDXTYPE_PRIMARYKEY && !pProbe->bIdxRowid))
167943 ){
167944 if( pNew->u.btree.nEq>3 ){
167945 sqlite3ProgressCheck(pParse);
167946 }
167947 whereLoopAddBtreeIndex(pBuilder, pSrc, pProbe, nInMul+nIn);
@@ -171073,11 +171395,12 @@
171395 wherePathSolver(pWInfo, pWInfo->nRowOut<0 ? 1 : pWInfo->nRowOut+1);
171396 if( db->mallocFailed ) goto whereBeginError;
171397 }
171398
171399 /* TUNING: Assume that a DISTINCT clause on a subquery reduces
171400 ** the output size by a factor of 8 (LogEst -30). Search for
171401 ** tag-20250414a to see other cases.
171402 */
171403 if( (pWInfo->wctrlFlags & WHERE_WANT_DISTINCT)!=0 ){
171404 WHERETRACE(0x0080,("nRowOut reduced from %d to %d due to DISTINCT\n",
171405 pWInfo->nRowOut, pWInfo->nRowOut-30));
171406 pWInfo->nRowOut -= 30;
@@ -188065,10 +188388,17 @@
188388 ******************************************************************************
188389 **
188390 */
188391 #ifndef _FTSINT_H
188392 #define _FTSINT_H
188393
188394 /* #include <assert.h> */
188395 /* #include <stdlib.h> */
188396 /* #include <stddef.h> */
188397 /* #include <stdio.h> */
188398 /* #include <string.h> */
188399 /* #include <stdarg.h> */
188400
188401 #if !defined(NDEBUG) && !defined(SQLITE_DEBUG)
188402 # define NDEBUG 1
188403 #endif
188404
@@ -189017,16 +189347,10 @@
189347
189348 #if defined(SQLITE_ENABLE_FTS3) && !defined(SQLITE_CORE)
189349 # define SQLITE_CORE 1
189350 #endif
189351
 
 
 
 
 
 
189352
189353 /* #include "fts3.h" */
189354 #ifndef SQLITE_CORE
189355 /* # include "sqlite3ext.h" */
189356 SQLITE_EXTENSION_INIT1
@@ -198969,11 +199293,11 @@
199293 UNUSED_PARAMETER(nVal);
199294
199295 fts3tokResetCursor(pCsr);
199296 if( idxNum==1 ){
199297 const char *zByte = (const char *)sqlite3_value_text(apVal[0]);
199298 sqlite3_int64 nByte = sqlite3_value_bytes(apVal[0]);
199299 pCsr->zInput = sqlite3_malloc64(nByte+1);
199300 if( pCsr->zInput==0 ){
199301 rc = SQLITE_NOMEM;
199302 }else{
199303 if( nByte>0 ) memcpy(pCsr->zInput, zByte, nByte);
@@ -205949,20 +206273,20 @@
206273 case FTS3_MATCHINFO_LCS:
206274 nVal = pInfo->nCol;
206275 break;
206276
206277 case FTS3_MATCHINFO_LHITS:
206278 nVal = (size_t)pInfo->nCol * pInfo->nPhrase;
206279 break;
206280
206281 case FTS3_MATCHINFO_LHITS_BM:
206282 nVal = (size_t)pInfo->nPhrase * ((pInfo->nCol + 31) / 32);
206283 break;
206284
206285 default:
206286 assert( cArg==FTS3_MATCHINFO_HITS );
206287 nVal = (size_t)pInfo->nCol * pInfo->nPhrase * 3;
206288 break;
206289 }
206290
206291 return nVal;
206292 }
@@ -207516,12 +207840,12 @@
207840 ** with JSON-5 extensions is accepted as input.
207841 **
207842 ** Beginning with version 3.45.0 (circa 2024-01-01), these routines also
207843 ** accept BLOB values that have JSON encoded using a binary representation
207844 ** called "JSONB". The name JSONB comes from PostgreSQL, however the on-disk
207845 ** format for SQLite-JSONB is completely different and incompatible with
207846 ** PostgreSQL-JSONB.
207847 **
207848 ** Decoding and interpreting JSONB is still O(N) where N is the size of
207849 ** the input, the same as text JSON. However, the constant of proportionality
207850 ** for JSONB is much smaller due to faster parsing. The size of each
207851 ** element in JSONB is encoded in its header, so there is no need to search
@@ -207574,21 +207898,21 @@
207898 ** 14 4 byte (0-4294967295) 5
207899 ** 15 8 byte (0-1.8e19) 9
207900 **
207901 ** The payload size need not be expressed in its minimal form. For example,
207902 ** if the payload size is 10, the size can be expressed in any of 5 different
207903 ** ways: (1) (X>>4)==10, (2) (X>>4)==12 following by one 0x0a byte,
207904 ** (3) (X>>4)==13 followed by 0x00 and 0x0a, (4) (X>>4)==14 followed by
207905 ** 0x00 0x00 0x00 0x0a, or (5) (X>>4)==15 followed by 7 bytes of 0x00 and
207906 ** a single byte of 0x0a. The shorter forms are preferred, of course, but
207907 ** sometimes when generating JSONB, the payload size is not known in advance
207908 ** and it is convenient to reserve sufficient header space to cover the
207909 ** largest possible payload size and then come back later and patch up
207910 ** the size when it becomes known, resulting in a non-minimal encoding.
207911 **
207912 ** The value (X>>4)==15 is not actually used in the current implementation
207913 ** (as SQLite is currently unable to handle BLOBs larger than about 2GB)
207914 ** but is included in the design to allow for future enhancements.
207915 **
207916 ** The payload follows the header. NULL, TRUE, and FALSE have no payload and
207917 ** their payload size must always be zero. The payload for INT, INT5,
207918 ** FLOAT, FLOAT5, TEXT, TEXTJ, TEXT5, and TEXTROW is text. Note that the
@@ -208658,11 +208982,11 @@
208982 if( jsonBlobExpand(pParse, pParse->nBlob+szPayload+9) ) return;
208983 jsonBlobAppendNode(pParse, eType, szPayload, aPayload);
208984 }
208985
208986
208987 /* Append a node type byte together with the payload size and
208988 ** possibly also the payload.
208989 **
208990 ** If aPayload is not NULL, then it is a pointer to the payload which
208991 ** is also appended. If aPayload is NULL, the pParse->aBlob[] array
208992 ** is resized (if necessary) so that it is big enough to hold the
@@ -209992,10 +210316,86 @@
210316 (void)jsonbPayloadSize(pParse, iRoot, &sz);
210317 pParse->nBlob = nBlob;
210318 sz += pParse->delta;
210319 pParse->delta += jsonBlobChangePayloadSize(pParse, iRoot, sz);
210320 }
210321
210322 /*
210323 ** If the JSONB at aIns[0..nIns-1] can be expanded (by denormalizing the
210324 ** size field) by d bytes, then write the expansion into aOut[] and
210325 ** return true. In this way, an overwrite happens without changing the
210326 ** size of the JSONB, which reduces memcpy() operations and also make it
210327 ** faster and easier to update the B-Tree entry that contains the JSONB
210328 ** in the database.
210329 **
210330 ** If the expansion of aIns[] by d bytes cannot be (easily) accomplished
210331 ** then return false.
210332 **
210333 ** The d parameter is guaranteed to be between 1 and 8.
210334 **
210335 ** This routine is an optimization. A correct answer is obtained if it
210336 ** always leaves the output unchanged and returns false.
210337 */
210338 static int jsonBlobOverwrite(
210339 u8 *aOut, /* Overwrite here */
210340 const u8 *aIns, /* New content */
210341 u32 nIns, /* Bytes of new content */
210342 u32 d /* Need to expand new content by this much */
210343 ){
210344 u32 szPayload; /* Bytes of payload */
210345 u32 i; /* New header size, after expansion & a loop counter */
210346 u8 szHdr; /* Size of header before expansion */
210347
210348 /* Lookup table for finding the upper 4 bits of the first byte of the
210349 ** expanded aIns[], based on the size of the expanded aIns[] header:
210350 **
210351 ** 2 3 4 5 6 7 8 9 */
210352 static const u8 aType[] = { 0xc0, 0xd0, 0, 0xe0, 0, 0, 0, 0xf0 };
210353
210354 if( (aIns[0]&0x0f)<=2 ) return 0; /* Cannot enlarge NULL, true, false */
210355 switch( aIns[0]>>4 ){
210356 default: { /* aIns[] header size 1 */
210357 if( ((1<<d)&0x116)==0 ) return 0; /* d must be 1, 2, 4, or 8 */
210358 i = d + 1; /* New hdr sz: 2, 3, 5, or 9 */
210359 szHdr = 1;
210360 break;
210361 }
210362 case 12: { /* aIns[] header size is 2 */
210363 if( ((1<<d)&0x8a)==0) return 0; /* d must be 1, 3, or 7 */
210364 i = d + 2; /* New hdr sz: 2, 5, or 9 */
210365 szHdr = 2;
210366 break;
210367 }
210368 case 13: { /* aIns[] header size is 3 */
210369 if( d!=2 && d!=6 ) return 0; /* d must be 2 or 6 */
210370 i = d + 3; /* New hdr sz: 5 or 9 */
210371 szHdr = 3;
210372 break;
210373 }
210374 case 14: { /* aIns[] header size is 5 */
210375 if( d!=4 ) return 0; /* d must be 4 */
210376 i = 9; /* New hdr sz: 9 */
210377 szHdr = 5;
210378 break;
210379 }
210380 case 15: { /* aIns[] header size is 9 */
210381 return 0; /* No solution */
210382 }
210383 }
210384 assert( i>=2 && i<=9 && aType[i-2]!=0 );
210385 aOut[0] = (aIns[0] & 0x0f) | aType[i-2];
210386 memcpy(&aOut[i], &aIns[szHdr], nIns-szHdr);
210387 szPayload = nIns - szHdr;
210388 while( 1/*edit-by-break*/ ){
210389 i--;
210390 aOut[i] = szPayload & 0xff;
210391 if( i==1 ) break;
210392 szPayload >>= 8;
210393 }
210394 assert( (szPayload>>8)==0 );
210395 return 1;
210396 }
210397
210398 /*
210399 ** Modify the JSONB blob at pParse->aBlob by removing nDel bytes of
210400 ** content beginning at iDel, and replacing them with nIns bytes of
210401 ** content given by aIns.
@@ -210014,10 +210414,15 @@
210414 u32 nDel, /* Number of bytes to remove */
210415 const u8 *aIns, /* Content to insert */
210416 u32 nIns /* Bytes of content to insert */
210417 ){
210418 i64 d = (i64)nIns - (i64)nDel;
210419 if( d<0 && d>=(-8) && aIns!=0
210420 && jsonBlobOverwrite(&pParse->aBlob[iDel], aIns, nIns, (int)-d)
210421 ){
210422 return;
210423 }
210424 if( d!=0 ){
210425 if( pParse->nBlob + d > pParse->nBlobAlloc ){
210426 jsonBlobExpand(pParse, pParse->nBlob+d);
210427 if( pParse->oom ) return;
210428 }
@@ -210025,11 +210430,13 @@
210430 &pParse->aBlob[iDel+nDel],
210431 pParse->nBlob - (iDel+nDel));
210432 pParse->nBlob += d;
210433 pParse->delta += d;
210434 }
210435 if( nIns && aIns ){
210436 memcpy(&pParse->aBlob[iDel], aIns, nIns);
210437 }
210438 }
210439
210440 /*
210441 ** Return the number of escaped newlines to be ignored.
210442 ** An escaped newline is a one of the following byte sequences:
@@ -210788,11 +211195,11 @@
211195 }
211196 return 0;
211197 }
211198
211199 /* argv[0] is a BLOB that seems likely to be a JSONB. Subsequent
211200 ** arguments come in pairs where each pair contains a JSON path and
211201 ** content to insert or set at that patch. Do the updates
211202 ** and return the result.
211203 **
211204 ** The specific operation is determined by eEdit, which can be one
211205 ** of JEDIT_INS, JEDIT_REPL, or JEDIT_SET.
@@ -227533,12 +227940,12 @@
227940 ){
227941 if( sqlite3_value_type(argv[3])==SQLITE_NULL && isInsert && pgno>1 ){
227942 /* "INSERT INTO dbpage($PGNO,NULL)" causes page number $PGNO and
227943 ** all subsequent pages to be deleted. */
227944 pTab->iDbTrunc = iDb;
227945 pTab->pgnoTrunc = pgno-1;
227946 pgno = 1;
227947 }else{
227948 zErr = "bad page value";
227949 goto update_fail;
227950 }
227951 }
@@ -228850,10 +229257,12 @@
229257 int rc = SQLITE_OK;
229258
229259 if( pTab->nCol==0 ){
229260 u8 *abPK;
229261 assert( pTab->azCol==0 || pTab->abPK==0 );
229262 sqlite3_free(pTab->azCol);
229263 pTab->abPK = 0;
229264 rc = sessionTableInfo(pSession, db, zDb,
229265 pTab->zName, &pTab->nCol, &pTab->nTotalCol, 0, &pTab->azCol,
229266 &pTab->azDflt, &pTab->aiIdx, &abPK,
229267 ((pSession==0 || pSession->bImplicitPK) ? &pTab->bRowid : 0)
229268 );
@@ -229857,11 +230266,13 @@
230266 char *zExpr = 0;
230267 sqlite3 *db = pSession->db;
230268 SessionTable *pTo; /* Table zTbl */
230269
230270 /* Locate and if necessary initialize the target table object */
230271 pSession->bAutoAttach++;
230272 rc = sessionFindTable(pSession, zTbl, &pTo);
230273 pSession->bAutoAttach--;
230274 if( pTo==0 ) goto diff_out;
230275 if( sessionInitTable(pSession, pTo, pSession->db, pSession->zDb) ){
230276 rc = pSession->rc;
230277 goto diff_out;
230278 }
@@ -229868,21 +230279,47 @@
230279
230280 /* Check the table schemas match */
230281 if( rc==SQLITE_OK ){
230282 int bHasPk = 0;
230283 int bMismatch = 0;
230284 int nCol = 0; /* Columns in zFrom.zTbl */
230285 int bRowid = 0;
230286 u8 *abPK = 0;
230287 const char **azCol = 0;
230288 char *zDbExists = 0;
230289
230290 /* Check that database zFrom is attached. */
230291 zDbExists = sqlite3_mprintf("SELECT * FROM %Q.sqlite_schema", zFrom);
230292 if( zDbExists==0 ){
230293 rc = SQLITE_NOMEM;
230294 }else{
230295 sqlite3_stmt *pDbExists = 0;
230296 rc = sqlite3_prepare_v2(db, zDbExists, -1, &pDbExists, 0);
230297 if( rc==SQLITE_ERROR ){
230298 rc = SQLITE_OK;
230299 nCol = -1;
230300 }
230301 sqlite3_finalize(pDbExists);
230302 sqlite3_free(zDbExists);
230303 }
230304
230305 if( rc==SQLITE_OK && nCol==0 ){
230306 rc = sessionTableInfo(0, db, zFrom, zTbl,
230307 &nCol, 0, 0, &azCol, 0, 0, &abPK,
230308 pSession->bImplicitPK ? &bRowid : 0
230309 );
230310 }
230311 if( rc==SQLITE_OK ){
230312 if( pTo->nCol!=nCol ){
230313 if( nCol<=0 ){
230314 rc = SQLITE_SCHEMA;
230315 if( pzErrMsg ){
230316 *pzErrMsg = sqlite3_mprintf("no such table: %s.%s", zFrom, zTbl);
230317 }
230318 }else{
230319 bMismatch = 1;
230320 }
230321 }else{
230322 int i;
230323 for(i=0; i<nCol; i++){
230324 if( pTo->abPK[i]!=abPK[i] ) bMismatch = 1;
230325 if( sqlite3_stricmp(azCol[i], pTo->azCol[i]) ) bMismatch = 1;
@@ -241869,11 +242306,12 @@
242306 sqlite3Fts5ParseError(
242307 pParse, "expected integer, got \"%.*s\"", p->n, p->p
242308 );
242309 return;
242310 }
242311 if( nNear<214748363 ) nNear = nNear * 10 + (p->p[i] - '0');
242312 /* ^^^^^^^^^^^^^^^--- Prevent integer overflow */
242313 }
242314 }else{
242315 nNear = FTS5_DEFAULT_NEARDIST;
242316 }
242317 pNear->nNear = nNear;
@@ -256775,11 +257213,11 @@
257213 int nArg, /* Number of args */
257214 sqlite3_value **apUnused /* Function arguments */
257215 ){
257216 assert( nArg==0 );
257217 UNUSED_PARAM2(nArg, apUnused);
257218 sqlite3_result_text(pCtx, "fts5: 2025-04-15 21:59:38 d22475b81c4e26ccc50f3b5626d43b32f7a2de34e5a764539554665bdda735d5", -1, SQLITE_TRANSIENT);
257219 }
257220
257221 /*
257222 ** Implementation of fts5_locale(LOCALE, TEXT) function.
257223 **
@@ -260838,11 +261276,10 @@
261276 }
261277 iTbl++;
261278 }
261279 aAscii[0] = 0; /* 0x00 is never a token character */
261280 }
 
261281
261282 /*
261283 ** 2015 May 30
261284 **
261285 ** The author disclaims copyright to this source code. In place of
261286
--- extsrc/sqlite3.h
+++ extsrc/sqlite3.h
@@ -131,11 +131,11 @@
131131
** be held constant and Z will be incremented or else Y will be incremented
132132
** and Z will be reset to zero.
133133
**
134134
** Since [version 3.6.18] ([dateof:3.6.18]),
135135
** SQLite source code has been stored in the
136
-** <a href="http://www.fossil-scm.org/">Fossil configuration management
136
+** <a href="http://fossil-scm.org/">Fossil configuration management
137137
** system</a>. ^The SQLITE_SOURCE_ID macro evaluates to
138138
** a string which identifies a particular check-in of SQLite
139139
** within its configuration management system. ^The SQLITE_SOURCE_ID
140140
** string contains the date and time of the check-in (UTC) and a SHA1
141141
** or SHA3-256 hash of the entire source tree. If the source code has
@@ -146,11 +146,11 @@
146146
** [sqlite3_libversion_number()], [sqlite3_sourceid()],
147147
** [sqlite_version()] and [sqlite_source_id()].
148148
*/
149149
#define SQLITE_VERSION "3.50.0"
150150
#define SQLITE_VERSION_NUMBER 3050000
151
-#define SQLITE_SOURCE_ID "2025-03-16 00:13:29 18bda13e197e4b4ec7464b3e70012f71edc05f73d8b14bb48bad452f81c7e185"
151
+#define SQLITE_SOURCE_ID "2025-04-15 21:59:38 d22475b81c4e26ccc50f3b5626d43b32f7a2de34e5a764539554665bdda735d5"
152152
153153
/*
154154
** CAPI3REF: Run-Time Library Version Numbers
155155
** KEYWORDS: sqlite3_version sqlite3_sourceid
156156
**
@@ -11627,12 +11627,13 @@
1162711627
** To clarify, if this function is called and then a changeset constructed
1162811628
** using [sqlite3session_changeset()], then after applying that changeset to
1162911629
** database zFrom the contents of the two compatible tables would be
1163011630
** identical.
1163111631
**
11632
-** It an error if database zFrom does not exist or does not contain the
11633
-** required compatible table.
11632
+** Unless the call to this function is a no-op as described above, it is an
11633
+** error if database zFrom does not exist or does not contain the required
11634
+** compatible table.
1163411635
**
1163511636
** If the operation is successful, SQLITE_OK is returned. Otherwise, an SQLite
1163611637
** error code. In this case, if argument pzErrMsg is not NULL, *pzErrMsg
1163711638
** may be set to point to a buffer containing an English language error
1163811639
** message. It is the responsibility of the caller to free this buffer using
1163911640
--- extsrc/sqlite3.h
+++ extsrc/sqlite3.h
@@ -131,11 +131,11 @@
131 ** be held constant and Z will be incremented or else Y will be incremented
132 ** and Z will be reset to zero.
133 **
134 ** Since [version 3.6.18] ([dateof:3.6.18]),
135 ** SQLite source code has been stored in the
136 ** <a href="http://www.fossil-scm.org/">Fossil configuration management
137 ** system</a>. ^The SQLITE_SOURCE_ID macro evaluates to
138 ** a string which identifies a particular check-in of SQLite
139 ** within its configuration management system. ^The SQLITE_SOURCE_ID
140 ** string contains the date and time of the check-in (UTC) and a SHA1
141 ** or SHA3-256 hash of the entire source tree. If the source code has
@@ -146,11 +146,11 @@
146 ** [sqlite3_libversion_number()], [sqlite3_sourceid()],
147 ** [sqlite_version()] and [sqlite_source_id()].
148 */
149 #define SQLITE_VERSION "3.50.0"
150 #define SQLITE_VERSION_NUMBER 3050000
151 #define SQLITE_SOURCE_ID "2025-03-16 00:13:29 18bda13e197e4b4ec7464b3e70012f71edc05f73d8b14bb48bad452f81c7e185"
152
153 /*
154 ** CAPI3REF: Run-Time Library Version Numbers
155 ** KEYWORDS: sqlite3_version sqlite3_sourceid
156 **
@@ -11627,12 +11627,13 @@
11627 ** To clarify, if this function is called and then a changeset constructed
11628 ** using [sqlite3session_changeset()], then after applying that changeset to
11629 ** database zFrom the contents of the two compatible tables would be
11630 ** identical.
11631 **
11632 ** It an error if database zFrom does not exist or does not contain the
11633 ** required compatible table.
 
11634 **
11635 ** If the operation is successful, SQLITE_OK is returned. Otherwise, an SQLite
11636 ** error code. In this case, if argument pzErrMsg is not NULL, *pzErrMsg
11637 ** may be set to point to a buffer containing an English language error
11638 ** message. It is the responsibility of the caller to free this buffer using
11639
--- extsrc/sqlite3.h
+++ extsrc/sqlite3.h
@@ -131,11 +131,11 @@
131 ** be held constant and Z will be incremented or else Y will be incremented
132 ** and Z will be reset to zero.
133 **
134 ** Since [version 3.6.18] ([dateof:3.6.18]),
135 ** SQLite source code has been stored in the
136 ** <a href="http://fossil-scm.org/">Fossil configuration management
137 ** system</a>. ^The SQLITE_SOURCE_ID macro evaluates to
138 ** a string which identifies a particular check-in of SQLite
139 ** within its configuration management system. ^The SQLITE_SOURCE_ID
140 ** string contains the date and time of the check-in (UTC) and a SHA1
141 ** or SHA3-256 hash of the entire source tree. If the source code has
@@ -146,11 +146,11 @@
146 ** [sqlite3_libversion_number()], [sqlite3_sourceid()],
147 ** [sqlite_version()] and [sqlite_source_id()].
148 */
149 #define SQLITE_VERSION "3.50.0"
150 #define SQLITE_VERSION_NUMBER 3050000
151 #define SQLITE_SOURCE_ID "2025-04-15 21:59:38 d22475b81c4e26ccc50f3b5626d43b32f7a2de34e5a764539554665bdda735d5"
152
153 /*
154 ** CAPI3REF: Run-Time Library Version Numbers
155 ** KEYWORDS: sqlite3_version sqlite3_sourceid
156 **
@@ -11627,12 +11627,13 @@
11627 ** To clarify, if this function is called and then a changeset constructed
11628 ** using [sqlite3session_changeset()], then after applying that changeset to
11629 ** database zFrom the contents of the two compatible tables would be
11630 ** identical.
11631 **
11632 ** Unless the call to this function is a no-op as described above, it is an
11633 ** error if database zFrom does not exist or does not contain the required
11634 ** compatible table.
11635 **
11636 ** If the operation is successful, SQLITE_OK is returned. Otherwise, an SQLite
11637 ** error code. In this case, if argument pzErrMsg is not NULL, *pzErrMsg
11638 ** may be set to point to a buffer containing an English language error
11639 ** message. It is the responsibility of the caller to free this buffer using
11640
--- skins/blitz/footer.txt
+++ skins/blitz/footer.txt
@@ -1,10 +1,10 @@
11
</div> <!-- end div container -->
22
</div> <!-- end div middle max-full-width -->
33
<footer>
44
<div class="container">
55
<div class="pull-right">
6
- <a href="https://www.fossil-scm.org/">Fossil $release_version $manifest_version $manifest_date</a>
6
+ <a href="https://fossil-scm.org/">Fossil $release_version $manifest_version $manifest_date</a>
77
</div>
88
This page was generated in about <th1>puts [expr {([utime]+[stime]+1000)/1000*0.001}]</th1>s
99
</div>
1010
</footer>
1111
--- skins/blitz/footer.txt
+++ skins/blitz/footer.txt
@@ -1,10 +1,10 @@
1 </div> <!-- end div container -->
2 </div> <!-- end div middle max-full-width -->
3 <footer>
4 <div class="container">
5 <div class="pull-right">
6 <a href="https://www.fossil-scm.org/">Fossil $release_version $manifest_version $manifest_date</a>
7 </div>
8 This page was generated in about <th1>puts [expr {([utime]+[stime]+1000)/1000*0.001}]</th1>s
9 </div>
10 </footer>
11
--- skins/blitz/footer.txt
+++ skins/blitz/footer.txt
@@ -1,10 +1,10 @@
1 </div> <!-- end div container -->
2 </div> <!-- end div middle max-full-width -->
3 <footer>
4 <div class="container">
5 <div class="pull-right">
6 <a href="https://fossil-scm.org/">Fossil $release_version $manifest_version $manifest_date</a>
7 </div>
8 This page was generated in about <th1>puts [expr {([utime]+[stime]+1000)/1000*0.001}]</th1>s
9 </div>
10 </footer>
11
--- skins/darkmode/footer.txt
+++ skins/darkmode/footer.txt
@@ -1,8 +1,8 @@
11
<footer>
22
<div class="container">
33
<div class="pull-right">
4
- <a href="https://www.fossil-scm.org/">Fossil $release_version $manifest_version $manifest_date</a>
4
+ <a href="https://fossil-scm.org/">Fossil $release_version $manifest_version $manifest_date</a>
55
</div>
66
This page was generated in about <th1>puts [expr {([utime]+[stime]+1000)/1000*0.001}]</th1>s
77
</div>
88
</footer>
99
--- skins/darkmode/footer.txt
+++ skins/darkmode/footer.txt
@@ -1,8 +1,8 @@
1 <footer>
2 <div class="container">
3 <div class="pull-right">
4 <a href="https://www.fossil-scm.org/">Fossil $release_version $manifest_version $manifest_date</a>
5 </div>
6 This page was generated in about <th1>puts [expr {([utime]+[stime]+1000)/1000*0.001}]</th1>s
7 </div>
8 </footer>
9
--- skins/darkmode/footer.txt
+++ skins/darkmode/footer.txt
@@ -1,8 +1,8 @@
1 <footer>
2 <div class="container">
3 <div class="pull-right">
4 <a href="https://fossil-scm.org/">Fossil $release_version $manifest_version $manifest_date</a>
5 </div>
6 This page was generated in about <th1>puts [expr {([utime]+[stime]+1000)/1000*0.001}]</th1>s
7 </div>
8 </footer>
9
--- skins/default/header.txt
+++ skins/default/header.txt
@@ -28,11 +28,11 @@
2828
return $logourl
2929
}
3030
set logourl [getLogoUrl $baseurl]
3131
</th1>
3232
<a href="$logourl">
33
- <img src="$logo_image_url" border="0" alt="$project_name">
33
+ <img src="$logo_image_url" border="0" alt="$<project_name>">
3434
</a>
3535
</div>
3636
<div class="title">
3737
<h1>$<project_name></h1>
3838
<span class="page-title">$<title></span>
3939
--- skins/default/header.txt
+++ skins/default/header.txt
@@ -28,11 +28,11 @@
28 return $logourl
29 }
30 set logourl [getLogoUrl $baseurl]
31 </th1>
32 <a href="$logourl">
33 <img src="$logo_image_url" border="0" alt="$project_name">
34 </a>
35 </div>
36 <div class="title">
37 <h1>$<project_name></h1>
38 <span class="page-title">$<title></span>
39
--- skins/default/header.txt
+++ skins/default/header.txt
@@ -28,11 +28,11 @@
28 return $logourl
29 }
30 set logourl [getLogoUrl $baseurl]
31 </th1>
32 <a href="$logourl">
33 <img src="$logo_image_url" border="0" alt="$<project_name>">
34 </a>
35 </div>
36 <div class="title">
37 <h1>$<project_name></h1>
38 <span class="page-title">$<title></span>
39
--- skins/eagle/footer.txt
+++ skins/eagle/footer.txt
@@ -10,11 +10,11 @@
1010
set length [string length $version]
1111
return [string range $version 1 [expr {$length - 2}]]
1212
}
1313
set version [getVersion $manifest_version]
1414
set tclVersion [getTclVersion]
15
- set fossilUrl https://www.fossil-scm.org
15
+ set fossilUrl https://fossil-scm.org
1616
set fossilDate [string range $manifest_date 0 9]T[string range $manifest_date 11 end]
1717
</th1>
1818
This page was generated in about
1919
<th1>puts [expr {([utime]+[stime]+1000)/1000*0.001}]</th1>s by
2020
<a href="$fossilUrl/">Fossil</a>
2121
--- skins/eagle/footer.txt
+++ skins/eagle/footer.txt
@@ -10,11 +10,11 @@
10 set length [string length $version]
11 return [string range $version 1 [expr {$length - 2}]]
12 }
13 set version [getVersion $manifest_version]
14 set tclVersion [getTclVersion]
15 set fossilUrl https://www.fossil-scm.org
16 set fossilDate [string range $manifest_date 0 9]T[string range $manifest_date 11 end]
17 </th1>
18 This page was generated in about
19 <th1>puts [expr {([utime]+[stime]+1000)/1000*0.001}]</th1>s by
20 <a href="$fossilUrl/">Fossil</a>
21
--- skins/eagle/footer.txt
+++ skins/eagle/footer.txt
@@ -10,11 +10,11 @@
10 set length [string length $version]
11 return [string range $version 1 [expr {$length - 2}]]
12 }
13 set version [getVersion $manifest_version]
14 set tclVersion [getTclVersion]
15 set fossilUrl https://fossil-scm.org
16 set fossilDate [string range $manifest_date 0 9]T[string range $manifest_date 11 end]
17 </th1>
18 This page was generated in about
19 <th1>puts [expr {([utime]+[stime]+1000)/1000*0.001}]</th1>s by
20 <a href="$fossilUrl/">Fossil</a>
21
--- skins/eagle/header.txt
+++ skins/eagle/header.txt
@@ -65,11 +65,11 @@
6565
# Link logo to the top of the current repo
6666
set logourl $baseurl
6767
}
6868
</th1>
6969
<a href="$logourl">
70
- <img src="$logo_image_url" border="0" alt="$project_name">
70
+ <img src="$logo_image_url" border="0" alt="$<project_name>">
7171
</a>
7272
</div>
7373
<div class="title">$<title></div>
7474
<div class="status"><nobr><th1>
7575
if {[info exists login]} {
7676
--- skins/eagle/header.txt
+++ skins/eagle/header.txt
@@ -65,11 +65,11 @@
65 # Link logo to the top of the current repo
66 set logourl $baseurl
67 }
68 </th1>
69 <a href="$logourl">
70 <img src="$logo_image_url" border="0" alt="$project_name">
71 </a>
72 </div>
73 <div class="title">$<title></div>
74 <div class="status"><nobr><th1>
75 if {[info exists login]} {
76
--- skins/eagle/header.txt
+++ skins/eagle/header.txt
@@ -65,11 +65,11 @@
65 # Link logo to the top of the current repo
66 set logourl $baseurl
67 }
68 </th1>
69 <a href="$logourl">
70 <img src="$logo_image_url" border="0" alt="$<project_name>">
71 </a>
72 </div>
73 <div class="title">$<title></div>
74 <div class="status"><nobr><th1>
75 if {[info exists login]} {
76
--- skins/original/footer.txt
+++ skins/original/footer.txt
@@ -10,11 +10,11 @@
1010
set length [string length $version]
1111
return [string range $version 1 [expr {$length - 2}]]
1212
}
1313
set version [getVersion $manifest_version]
1414
set tclVersion [getTclVersion]
15
- set fossilUrl https://www.fossil-scm.org
15
+ set fossilUrl https://fossil-scm.org
1616
set fossilDate [string range $manifest_date 0 9]T[string range $manifest_date 11 end]
1717
</th1>
1818
This page was generated in about
1919
<th1>puts [expr {([utime]+[stime]+1000)/1000*0.001}]</th1>s by
2020
<a href="$fossilUrl/">Fossil</a>
2121
--- skins/original/footer.txt
+++ skins/original/footer.txt
@@ -10,11 +10,11 @@
10 set length [string length $version]
11 return [string range $version 1 [expr {$length - 2}]]
12 }
13 set version [getVersion $manifest_version]
14 set tclVersion [getTclVersion]
15 set fossilUrl https://www.fossil-scm.org
16 set fossilDate [string range $manifest_date 0 9]T[string range $manifest_date 11 end]
17 </th1>
18 This page was generated in about
19 <th1>puts [expr {([utime]+[stime]+1000)/1000*0.001}]</th1>s by
20 <a href="$fossilUrl/">Fossil</a>
21
--- skins/original/footer.txt
+++ skins/original/footer.txt
@@ -10,11 +10,11 @@
10 set length [string length $version]
11 return [string range $version 1 [expr {$length - 2}]]
12 }
13 set version [getVersion $manifest_version]
14 set tclVersion [getTclVersion]
15 set fossilUrl https://fossil-scm.org
16 set fossilDate [string range $manifest_date 0 9]T[string range $manifest_date 11 end]
17 </th1>
18 This page was generated in about
19 <th1>puts [expr {([utime]+[stime]+1000)/1000*0.001}]</th1>s by
20 <a href="$fossilUrl/">Fossil</a>
21
--- skins/original/header.txt
+++ skins/original/header.txt
@@ -59,11 +59,11 @@
5959
return $logourl
6060
}
6161
set logourl [getLogoUrl $baseurl]
6262
</th1>
6363
<a href="$logourl">
64
- <img src="$logo_image_url" border="0" alt="$project_name">
64
+ <img src="$logo_image_url" border="0" alt="$<project_name>">
6565
</a>
6666
</div>
6767
<div class="title">$<title></div>
6868
<div class="status"><nobr><th1>
6969
if {[info exists login]} {
7070
--- skins/original/header.txt
+++ skins/original/header.txt
@@ -59,11 +59,11 @@
59 return $logourl
60 }
61 set logourl [getLogoUrl $baseurl]
62 </th1>
63 <a href="$logourl">
64 <img src="$logo_image_url" border="0" alt="$project_name">
65 </a>
66 </div>
67 <div class="title">$<title></div>
68 <div class="status"><nobr><th1>
69 if {[info exists login]} {
70
--- skins/original/header.txt
+++ skins/original/header.txt
@@ -59,11 +59,11 @@
59 return $logourl
60 }
61 set logourl [getLogoUrl $baseurl]
62 </th1>
63 <a href="$logourl">
64 <img src="$logo_image_url" border="0" alt="$<project_name>">
65 </a>
66 </div>
67 <div class="title">$<title></div>
68 <div class="status"><nobr><th1>
69 if {[info exists login]} {
70
--- skins/xekri/css.txt
+++ skins/xekri/css.txt
@@ -20,10 +20,11 @@
2020
font-size: 1em;
2121
min-height: 100%;
2222
}
2323
2424
body {
25
+ background-color: #333;
2526
margin: 0;
2627
padding: 0;
2728
text-size-adjust: none;
2829
}
2930
@@ -882,10 +883,110 @@
882883
883884
/* the format for the timeline version links */
884885
a.timelineHistLink {
885886
}
886887
888
+/* Timeline graph style taken from Ardoise, with
889
+** minor adjustments (2025-03-28) */
890
+.tl-canvas {
891
+ margin: 0 6px 0 10px
892
+}
893
+.tl-rail {
894
+ width: 18px
895
+}
896
+.tl-mergeoffset {
897
+ width: 2px
898
+}
899
+.tl-nodemark {
900
+ margin-top: .8em
901
+}
902
+.tl-node {
903
+ width: 10px;
904
+ height: 10px;
905
+ border: 1px solid #bbb;
906
+ background: #111;
907
+ cursor: pointer
908
+}
909
+.tl-node.leaf:after {
910
+ content: '';
911
+ position: absolute;
912
+ top: 3px;
913
+ left: 3px;
914
+ width: 4px;
915
+ height: 4px;
916
+ background: #bbb
917
+}
918
+.tl-node.closed-leaf svg {
919
+ position: absolute;
920
+ top: 0px;
921
+ left: 0px;
922
+ width: 10px;
923
+ height: 10px;
924
+ color: #bbb;
925
+}
926
+.tl-node.sel:after {
927
+ content: '';
928
+ position: absolute;
929
+ top: 1px;
930
+ left: 1px;
931
+ width: 8px;
932
+ height: 8px;
933
+ background: #ff8000
934
+}
935
+.tl-arrow {
936
+ width: 0;
937
+ height: 0;
938
+ transform: scale(.999);
939
+ border: 0 solid transparent
940
+}
941
+.tl-arrow.u {
942
+ margin-top: -1px;
943
+ border-width: 0 3px;
944
+ border-bottom: 7px solid
945
+}
946
+.tl-arrow.u.sm {
947
+ border-bottom: 5px solid #bbb
948
+}
949
+.tl-line {
950
+ background: #bbb;
951
+ width: 2px
952
+}
953
+.tl-arrow.merge {
954
+ height: 1px;
955
+ border-width: 2px 0
956
+}
957
+.tl-arrow.merge.l {
958
+ border-right: 3px solid #bbb
959
+}
960
+.tl-arrow.merge.r {
961
+ border-left: 3px solid #bbb
962
+}
963
+.tl-line.merge {
964
+ width: 1px
965
+}
966
+.tl-arrow.cherrypick {
967
+ height: 1px;
968
+ border-width: 2px 0;
969
+}
970
+.tl-arrow.cherrypick.l {
971
+ border-right: 3px solid #bbb;
972
+}
973
+.tl-arrow.cherrypick.r {
974
+ border-left: 3px solid #bbb;
975
+}
976
+.tl-line.cherrypick.h {
977
+ width: 0px;
978
+ border-top: 1px dashed #bbb;
979
+ border-left: 0px dashed #bbb;
980
+ background: rgba(255,255,255,0);
981
+}
982
+.tl-line.cherrypick.v {
983
+ width: 0px;
984
+ border-top: 0px dashed #bbb;
985
+ border-left: 1px dashed #bbb;
986
+ background: rgba(255,255,255,0);
987
+}
887988
888989
/**************************************
889990
* User Edit
890991
*/
891992
892993
--- skins/xekri/css.txt
+++ skins/xekri/css.txt
@@ -20,10 +20,11 @@
20 font-size: 1em;
21 min-height: 100%;
22 }
23
24 body {
 
25 margin: 0;
26 padding: 0;
27 text-size-adjust: none;
28 }
29
@@ -882,10 +883,110 @@
882
883 /* the format for the timeline version links */
884 a.timelineHistLink {
885 }
886
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
887
888 /**************************************
889 * User Edit
890 */
891
892
--- skins/xekri/css.txt
+++ skins/xekri/css.txt
@@ -20,10 +20,11 @@
20 font-size: 1em;
21 min-height: 100%;
22 }
23
24 body {
25 background-color: #333;
26 margin: 0;
27 padding: 0;
28 text-size-adjust: none;
29 }
30
@@ -882,10 +883,110 @@
883
884 /* the format for the timeline version links */
885 a.timelineHistLink {
886 }
887
888 /* Timeline graph style taken from Ardoise, with
889 ** minor adjustments (2025-03-28) */
890 .tl-canvas {
891 margin: 0 6px 0 10px
892 }
893 .tl-rail {
894 width: 18px
895 }
896 .tl-mergeoffset {
897 width: 2px
898 }
899 .tl-nodemark {
900 margin-top: .8em
901 }
902 .tl-node {
903 width: 10px;
904 height: 10px;
905 border: 1px solid #bbb;
906 background: #111;
907 cursor: pointer
908 }
909 .tl-node.leaf:after {
910 content: '';
911 position: absolute;
912 top: 3px;
913 left: 3px;
914 width: 4px;
915 height: 4px;
916 background: #bbb
917 }
918 .tl-node.closed-leaf svg {
919 position: absolute;
920 top: 0px;
921 left: 0px;
922 width: 10px;
923 height: 10px;
924 color: #bbb;
925 }
926 .tl-node.sel:after {
927 content: '';
928 position: absolute;
929 top: 1px;
930 left: 1px;
931 width: 8px;
932 height: 8px;
933 background: #ff8000
934 }
935 .tl-arrow {
936 width: 0;
937 height: 0;
938 transform: scale(.999);
939 border: 0 solid transparent
940 }
941 .tl-arrow.u {
942 margin-top: -1px;
943 border-width: 0 3px;
944 border-bottom: 7px solid
945 }
946 .tl-arrow.u.sm {
947 border-bottom: 5px solid #bbb
948 }
949 .tl-line {
950 background: #bbb;
951 width: 2px
952 }
953 .tl-arrow.merge {
954 height: 1px;
955 border-width: 2px 0
956 }
957 .tl-arrow.merge.l {
958 border-right: 3px solid #bbb
959 }
960 .tl-arrow.merge.r {
961 border-left: 3px solid #bbb
962 }
963 .tl-line.merge {
964 width: 1px
965 }
966 .tl-arrow.cherrypick {
967 height: 1px;
968 border-width: 2px 0;
969 }
970 .tl-arrow.cherrypick.l {
971 border-right: 3px solid #bbb;
972 }
973 .tl-arrow.cherrypick.r {
974 border-left: 3px solid #bbb;
975 }
976 .tl-line.cherrypick.h {
977 width: 0px;
978 border-top: 1px dashed #bbb;
979 border-left: 0px dashed #bbb;
980 background: rgba(255,255,255,0);
981 }
982 .tl-line.cherrypick.v {
983 width: 0px;
984 border-top: 0px dashed #bbb;
985 border-left: 1px dashed #bbb;
986 background: rgba(255,255,255,0);
987 }
988
989 /**************************************
990 * User Edit
991 */
992
993
--- skins/xekri/header.txt
+++ skins/xekri/header.txt
@@ -65,11 +65,11 @@
6565
# Link logo to the top of the current repo
6666
set logourl $baseurl
6767
}
6868
</th1>
6969
<a href="$logourl">
70
- <img src="$logo_image_url" border="0" alt="$project_name">
70
+ <img src="$logo_image_url" border="0" alt="$<project_name>">
7171
</a>
7272
</div>
7373
<div class="title">$<title></div>
7474
<div class="status"><nobr>
7575
<th1>
7676
--- skins/xekri/header.txt
+++ skins/xekri/header.txt
@@ -65,11 +65,11 @@
65 # Link logo to the top of the current repo
66 set logourl $baseurl
67 }
68 </th1>
69 <a href="$logourl">
70 <img src="$logo_image_url" border="0" alt="$project_name">
71 </a>
72 </div>
73 <div class="title">$<title></div>
74 <div class="status"><nobr>
75 <th1>
76
--- skins/xekri/header.txt
+++ skins/xekri/header.txt
@@ -65,11 +65,11 @@
65 # Link logo to the top of the current repo
66 set logourl $baseurl
67 }
68 </th1>
69 <a href="$logourl">
70 <img src="$logo_image_url" border="0" alt="$<project_name>">
71 </a>
72 </div>
73 <div class="title">$<title></div>
74 <div class="status"><nobr>
75 <th1>
76
+9 -2
--- src/add.c
+++ src/add.c
@@ -896,11 +896,12 @@
896896
*/
897897
static void mv_one_file(
898898
int vid,
899899
const char *zOrig,
900900
const char *zNew,
901
- int dryRunFlag
901
+ int dryRunFlag,
902
+ int moveFiles
902903
){
903904
int x = db_int(-1, "SELECT deleted FROM vfile WHERE pathname=%Q %s",
904905
zNew, filename_collation());
905906
if( x>=0 ){
906907
if( x==0 ){
@@ -912,10 +913,16 @@
912913
}
913914
}else{
914915
fossil_fatal("cannot rename '%s' to '%s' since the delete of '%s' has "
915916
"not yet been committed", zOrig, zNew, zNew);
916917
}
918
+ }
919
+ if( moveFiles ){
920
+ if( file_size(zNew, ExtFILE) != -1 ){
921
+ fossil_fatal("cannot rename '%s' to '%s' on disk since another file"
922
+ " named '%s' already exists", zOrig, zNew, zNew);
923
+ }
917924
}
918925
fossil_print("RENAME %s %s\n", zOrig, zNew);
919926
if( !dryRunFlag ){
920927
db_multi_exec(
921928
"UPDATE vfile SET pathname='%q' WHERE pathname='%q' %s AND vid=%d",
@@ -1135,11 +1142,11 @@
11351142
}
11361143
db_prepare(&q, "SELECT f, t FROM mv ORDER BY f");
11371144
while( db_step(&q)==SQLITE_ROW ){
11381145
const char *zFrom = db_column_text(&q, 0);
11391146
const char *zTo = db_column_text(&q, 1);
1140
- mv_one_file(vid, zFrom, zTo, dryRunFlag);
1147
+ mv_one_file(vid, zFrom, zTo, dryRunFlag, moveFiles);
11411148
if( moveFiles ) add_file_to_move(zFrom, zTo);
11421149
}
11431150
db_finalize(&q);
11441151
undo_reset();
11451152
db_end_transaction(0);
11461153
--- src/add.c
+++ src/add.c
@@ -896,11 +896,12 @@
896 */
897 static void mv_one_file(
898 int vid,
899 const char *zOrig,
900 const char *zNew,
901 int dryRunFlag
 
902 ){
903 int x = db_int(-1, "SELECT deleted FROM vfile WHERE pathname=%Q %s",
904 zNew, filename_collation());
905 if( x>=0 ){
906 if( x==0 ){
@@ -912,10 +913,16 @@
912 }
913 }else{
914 fossil_fatal("cannot rename '%s' to '%s' since the delete of '%s' has "
915 "not yet been committed", zOrig, zNew, zNew);
916 }
 
 
 
 
 
 
917 }
918 fossil_print("RENAME %s %s\n", zOrig, zNew);
919 if( !dryRunFlag ){
920 db_multi_exec(
921 "UPDATE vfile SET pathname='%q' WHERE pathname='%q' %s AND vid=%d",
@@ -1135,11 +1142,11 @@
1135 }
1136 db_prepare(&q, "SELECT f, t FROM mv ORDER BY f");
1137 while( db_step(&q)==SQLITE_ROW ){
1138 const char *zFrom = db_column_text(&q, 0);
1139 const char *zTo = db_column_text(&q, 1);
1140 mv_one_file(vid, zFrom, zTo, dryRunFlag);
1141 if( moveFiles ) add_file_to_move(zFrom, zTo);
1142 }
1143 db_finalize(&q);
1144 undo_reset();
1145 db_end_transaction(0);
1146
--- src/add.c
+++ src/add.c
@@ -896,11 +896,12 @@
896 */
897 static void mv_one_file(
898 int vid,
899 const char *zOrig,
900 const char *zNew,
901 int dryRunFlag,
902 int moveFiles
903 ){
904 int x = db_int(-1, "SELECT deleted FROM vfile WHERE pathname=%Q %s",
905 zNew, filename_collation());
906 if( x>=0 ){
907 if( x==0 ){
@@ -912,10 +913,16 @@
913 }
914 }else{
915 fossil_fatal("cannot rename '%s' to '%s' since the delete of '%s' has "
916 "not yet been committed", zOrig, zNew, zNew);
917 }
918 }
919 if( moveFiles ){
920 if( file_size(zNew, ExtFILE) != -1 ){
921 fossil_fatal("cannot rename '%s' to '%s' on disk since another file"
922 " named '%s' already exists", zOrig, zNew, zNew);
923 }
924 }
925 fossil_print("RENAME %s %s\n", zOrig, zNew);
926 if( !dryRunFlag ){
927 db_multi_exec(
928 "UPDATE vfile SET pathname='%q' WHERE pathname='%q' %s AND vid=%d",
@@ -1135,11 +1142,11 @@
1142 }
1143 db_prepare(&q, "SELECT f, t FROM mv ORDER BY f");
1144 while( db_step(&q)==SQLITE_ROW ){
1145 const char *zFrom = db_column_text(&q, 0);
1146 const char *zTo = db_column_text(&q, 1);
1147 mv_one_file(vid, zFrom, zTo, dryRunFlag, moveFiles);
1148 if( moveFiles ) add_file_to_move(zFrom, zTo);
1149 }
1150 db_finalize(&q);
1151 undo_reset();
1152 db_end_transaction(0);
1153
+148 -64
--- src/alerts.c
+++ src/alerts.c
@@ -51,11 +51,11 @@
5151
@ -- f - Forum posts
5252
@ -- k - ** Special: Unsubscribed using /oneclickunsub
5353
@ -- n - New forum threads
5454
@ -- r - Replies to my own forum posts
5555
@ -- t - Ticket changes
56
-@ -- u - Elevation of users' permissions (admins only)
56
+@ -- u - Changes of users' permissions (admins only)
5757
@ -- w - Wiki changes
5858
@ -- x - Edits to forum posts
5959
@ -- Probably different codes will be added in the future. In the future
6060
@ -- we might also add a separate table that allows subscribing to email
6161
@ -- notifications for specific branches or tags or tickets.
@@ -282,10 +282,13 @@
282282
style_submenu_element("Subscribers","%R/subscribers");
283283
}
284284
if( fossil_strcmp(g.zPath,"subscribe") ){
285285
style_submenu_element("Add New Subscriber","%R/subscribe");
286286
}
287
+ if( fossil_strcmp(g.zPath,"setup_notification") ){
288
+ style_submenu_element("Notification Setup","%R/setup_notification");
289
+ }
287290
}
288291
}
289292
290293
291294
/*
@@ -295,14 +298,14 @@
295298
** Normally accessible via the /Admin/Notification menu.
296299
*/
297300
void setup_notification(void){
298301
static const char *const azSendMethods[] = {
299302
"off", "Disabled",
300
- "pipe", "Pipe to a command",
303
+ "relay", "SMTP relay",
301304
"db", "Store in a database",
302305
"dir", "Store in a directory",
303
- "relay", "SMTP relay"
306
+ "pipe", "Pipe to a command",
304307
};
305308
login_check_credentials();
306309
if( !g.perm.Setup ){
307310
login_needed(0);
308311
return;
@@ -311,22 +314,25 @@
311314
312315
alert_submenu_common();
313316
style_submenu_element("Send Announcement","%R/announce");
314317
style_set_current_feature("alerts");
315318
style_header("Email Notification Setup");
316
- @ <h1>Status</h1>
319
+ @ <form action="%R/setup_notification" method="post"><div>
320
+ @ <h1>Status &ensp; <input type="submit" name="submit" value="Refresh"></h1>
321
+ @ </form>
317322
@ <table class="label-value">
318323
if( alert_enabled() ){
319324
stats_for_email();
320325
}else{
321326
@ <th>Disabled</th>
322327
}
323328
@ </table>
324329
@ <hr>
325
- @ <h1> Configuration </h1>
326330
@ <form action="%R/setup_notification" method="post"><div>
327
- @ <input type="submit" name="submit" value="Apply Changes"><hr>
331
+ @ <h1> Configuration </h1>
332
+ @ <p><input type="submit" name="submit" value="Apply Changes"></p>
333
+ @ <hr>
328334
login_insert_csrf_secret();
329335
330336
entry_attribute("Canonical Server URL", 40, "email-url",
331337
"eurl", "", 0);
332338
@ <p><b>Required.</b>
@@ -391,38 +397,40 @@
391397
@ <p>How to send email. Requires auxiliary information from the fields
392398
@ that follow. Hint: Use the <a href="%R/announce">/announce</a> page
393399
@ to send test message to debug this setting.
394400
@ (Property: "email-send-method")</p>
395401
alert_schema(1);
402
+ entry_attribute("SMTP Relay Host", 60, "email-send-relayhost",
403
+ "esrh", "localhost", 0);
404
+ @ <p>When the send method is "SMTP relay", each email message is
405
+ @ transmitted via the SMTP protocol (rfc5321) to a "Mail Submission
406
+ @ Agent" or "MSA" (rfc4409) at the hostname shown here. Optionally
407
+ @ append a colon and TCP port number (ex: smtp.example.com:587).
408
+ @ The default TCP port number is 25.
409
+ @ Usage Hint: If Fossil is running inside of a chroot jail, then it might
410
+ @ not be able to resolve hostnames. Work around this by using a raw IP
411
+ @ address or create a "/etc/hosts" file inside the chroot jail.
412
+ @ (Property: "email-send-relayhost")</p>
413
+ @
414
+ entry_attribute("Store Emails In This Database", 60, "email-send-db",
415
+ "esdb", "", 0);
416
+ @ <p>When the send method is "store in a database", each email message is
417
+ @ stored in an SQLite database file with the name given here.
418
+ @ (Property: "email-send-db")</p>
396419
entry_attribute("Pipe Email Text Into This Command", 60, "email-send-command",
397420
"ecmd", "sendmail -ti", 0);
398421
@ <p>When the send method is "pipe to a command", this is the command
399422
@ that is run. Email messages are piped into the standard input of this
400423
@ command. The command is expected to extract the sender address,
401424
@ recipient addresses, and subject from the header of the piped email
402425
@ text. (Property: "email-send-command")</p>
403
-
404
- entry_attribute("Store Emails In This Database", 60, "email-send-db",
405
- "esdb", "", 0);
406
- @ <p>When the send method is "store in a database", each email message is
407
- @ stored in an SQLite database file with the name given here.
408
- @ (Property: "email-send-db")</p>
409
-
410426
entry_attribute("Store Emails In This Directory", 60, "email-send-dir",
411427
"esdir", "", 0);
412428
@ <p>When the send method is "store in a directory", each email message is
413429
@ stored as a separate file in the directory shown here.
414430
@ (Property: "email-send-dir")</p>
415431
416
- entry_attribute("SMTP Relay Host", 60, "email-send-relayhost",
417
- "esrh", "", 0);
418
- @ <p>When the send method is "SMTP relay", each email message is
419
- @ transmitted via the SMTP protocol (rfc5321) to a "Mail Submission
420
- @ Agent" or "MSA" (rfc4409) at the hostname shown here. Optionally
421
- @ append a colon and TCP port number (ex: smtp.example.com:587).
422
- @ The default TCP port number is 25.
423
- @ (Property: "email-send-relayhost")</p>
424432
@ <hr>
425433
426434
@ <p><input type="submit" name="submit" value="Apply Changes"></p>
427435
@ </div></form>
428436
db_end_transaction(0);
@@ -630,19 +638,27 @@
630638
emailerGetSetting(p, &p->zCmd, "email-send-command");
631639
}else if( fossil_strcmp(p->zDest, "dir")==0 ){
632640
emailerGetSetting(p, &p->zDir, "email-send-dir");
633641
}else if( fossil_strcmp(p->zDest, "blob")==0 ){
634642
blob_init(&p->out, 0, 0);
635
- }else if( fossil_strcmp(p->zDest, "relay")==0 ){
643
+ }else if( fossil_strcmp(p->zDest, "relay")==0
644
+ || fossil_strcmp(p->zDest, "debug-relay")==0
645
+ ){
636646
const char *zRelay = 0;
637647
emailerGetSetting(p, &zRelay, "email-send-relayhost");
638648
if( zRelay ){
639649
u32 smtpFlags = SMTP_DIRECT;
640650
if( mFlags & ALERT_TRACE ) smtpFlags |= SMTP_TRACE_STDOUT;
651
+ blob_init(&p->out, 0, 0);
641652
p->pSmtp = smtp_session_new(domain_of_addr(p->zFrom), zRelay,
642
- smtpFlags);
643
- smtp_client_startup(p->pSmtp);
653
+ smtpFlags, 0);
654
+ if( p->pSmtp==0 || p->pSmtp->zErr ){
655
+ emailerError(p, "Could not start SMTP session: %s",
656
+ p->pSmtp ? p->pSmtp->zErr : "reason unknown");
657
+ }else if( p->zDest[0]=='d' ){
658
+ smtp_session_config(p->pSmtp, SMTP_TRACE_BLOB, &p->out);
659
+ }
644660
}
645661
}
646662
return p;
647663
}
648664
@@ -968,13 +984,10 @@
968984
blob_appendf(pOut, "Sender: <%s>\r\n", p->zFrom);
969985
}else{
970986
blob_appendf(pOut, "From: <%s>\r\n", p->zFrom);
971987
}
972988
blob_appendf(pOut, "Date: %z\r\n", cgi_rfc822_datestamp(time(0)));
973
- if( p->zListId && p->zListId[0] ){
974
- blob_appendf(pOut, "List-Id: %s\r\n", p->zListId);
975
- }
976989
if( strstr(blob_str(pHdr), "\r\nMessage-Id:")==0 ){
977990
/* Message-id format: "<$(date)x$(random)@$(from-host)>" where $(date) is
978991
** the current unix-time in hex, $(random) is a 64-bit random number,
979992
** and $(from) is the domain part of the email-self setting. */
980993
sqlite3_randomness(sizeof(r1), &r1);
@@ -1016,13 +1029,21 @@
10161029
blob_write_to_file(&all, zFile);
10171030
fossil_free(zFile);
10181031
}else if( p->pSmtp ){
10191032
char **azTo = 0;
10201033
int nTo = 0;
1034
+ SmtpSession *pSmtp = p->pSmtp;
10211035
email_header_to(pHdr, &nTo, &azTo);
1022
- if( nTo>0 ){
1023
- smtp_send_msg(p->pSmtp, p->zFrom, nTo, (const char**)azTo,blob_str(&all));
1036
+ if( nTo>0 && !pSmtp->bFatal ){
1037
+ smtp_send_msg(pSmtp,p->zFrom,nTo,(const char**)azTo,blob_str(&all));
1038
+ if( pSmtp->zErr && !pSmtp->bFatal ){
1039
+ smtp_send_msg(pSmtp,p->zFrom,nTo,(const char**)azTo,blob_str(&all));
1040
+ }
1041
+ if( pSmtp->zErr ){
1042
+ fossil_errorlog("SMTP: (%s) %s", pSmtp->bFatal ? "fatal" : "retry",
1043
+ pSmtp->zErr);
1044
+ }
10241045
email_header_to_free(nTo, azTo);
10251046
}
10261047
}else if( strcmp(p->zDest, "stdout")==0 ){
10271048
char **azTo = 0;
10281049
int nTo = 0;
@@ -1125,11 +1146,11 @@
11251146
** SETTING: email-listid width=40
11261147
** If this setting is not an empty string, then it becomes the argument to
11271148
** a "List-ID:" header that is added to all out-bound notification emails.
11281149
*/
11291150
/*
1130
-** SETTING: email-send-relayhost width=40 sensitive
1151
+** SETTING: email-send-relayhost width=40 sensitive default=127.0.0.1
11311152
** This is the hostname and TCP port to which output email messages
11321153
** are sent when email-send-method is "relay". There should be an
11331154
** SMTP server configured as a Mail Submission Agent listening on the
11341155
** designated host and port and all times.
11351156
*/
@@ -1704,11 +1725,11 @@
17041725
@ <label><input type="checkbox" name="sw" %s(PCK("sw"))> \
17051726
@ Wiki</label><br>
17061727
}
17071728
if( g.perm.Admin ){
17081729
@ <label><input type="checkbox" name="su" %s(PCK("su"))> \
1709
- @ User permission elevation</label>
1730
+ @ User permission changes</label>
17101731
}
17111732
di = PB("di");
17121733
@ </td></tr>
17131734
@ <tr>
17141735
@ <td class="form_label">Delivery:</td>
@@ -2114,11 +2135,11 @@
21142135
/* Corner-case bug: if an admin assigns 'u' to a non-admin, that
21152136
** subscription will get removed if the user later edits their
21162137
** subscriptions, as non-admins are not permitted to add that
21172138
** subscription. */
21182139
@ <label><input type="checkbox" name="su" %s(su?"checked":"")>\
2119
- @ User permission elevation</label>
2140
+ @ User permission changes</label>
21202141
}
21212142
@ </td></tr>
21222143
if( strchr(ssub,'k')!=0 ){
21232144
@ <tr><td></td><td>&nbsp;&uarr;&nbsp;
21242145
@ Note: User did a one-click unsubscribe</td></tr>
@@ -3191,18 +3212,21 @@
31913212
Blob fhdr, fbody;
31923213
blob_init(&fhdr, 0, 0);
31933214
blob_appendf(&fhdr, "To: <%s>\r\n", zEmail);
31943215
blob_append(&fhdr, blob_buffer(&p->hdr), blob_size(&p->hdr));
31953216
blob_init(&fbody, blob_buffer(&p->txt), blob_size(&p->txt));
3196
- blob_appendf(&fhdr, "List-Unsubscribe: <%s/oneclickunsub/%s>\r\n",
3197
- zUrl, zCode);
3198
- blob_appendf(&fhdr,
3199
- "List-Unsubscribe-Post: List-Unsubscribe=One-Click\r\n");
3200
- blob_appendf(&fbody, "\n-- \nUnsubscribe: %s/unsubscribe/%s\n",
3201
- zUrl, zCode);
3202
- /* blob_appendf(&fbody, "Subscription settings: %s/alerts/%s\n",
3203
- ** zUrl, zCode); */
3217
+ if( pSender->zListId && pSender->zListId[0] ){
3218
+ blob_appendf(&fhdr, "List-Id: %s\r\n", pSender->zListId);
3219
+ blob_appendf(&fhdr, "List-Unsubscribe: <%s/oneclickunsub/%s>\r\n",
3220
+ zUrl, zCode);
3221
+ blob_appendf(&fhdr,
3222
+ "List-Unsubscribe-Post: List-Unsubscribe=One-Click\r\n");
3223
+ blob_appendf(&fbody, "\n-- \nUnsubscribe: %s/unsubscribe/%s\n",
3224
+ zUrl, zCode);
3225
+ /* blob_appendf(&fbody, "Subscription settings: %s/alerts/%s\n",
3226
+ ** zUrl, zCode); */
3227
+ }
32043228
alert_send(pSender,&fhdr,&fbody,p->zFromName);
32053229
nSent++;
32063230
blob_reset(&fhdr);
32073231
blob_reset(&fbody);
32083232
}else{
@@ -3221,15 +3245,19 @@
32213245
blob_append(&body, "\n", 1);
32223246
blob_append(&body, blob_buffer(&p->txt), blob_size(&p->txt));
32233247
}
32243248
}
32253249
if( nHit==0 ) continue;
3226
- blob_appendf(&hdr, "List-Unsubscribe: <%s/oneclickunsub/%s>\r\n",
3227
- zUrl, zCode);
3228
- blob_appendf(&hdr, "List-Unsubscribe-Post: List-Unsubscribe=One-Click\r\n");
3229
- blob_appendf(&body,"\n-- \nSubscription info: %s/alerts/%s\n",
3230
- zUrl, zCode);
3250
+ if( pSender->zListId && pSender->zListId[0] ){
3251
+ blob_appendf(&hdr, "List-Id: %s\r\n", pSender->zListId);
3252
+ blob_appendf(&hdr, "List-Unsubscribe: <%s/oneclickunsub/%s>\r\n",
3253
+ zUrl, zCode);
3254
+ blob_appendf(&hdr,
3255
+ "List-Unsubscribe-Post: List-Unsubscribe=One-Click\r\n");
3256
+ blob_appendf(&body,"\n-- \nSubscription info: %s/alerts/%s\n",
3257
+ zUrl, zCode);
3258
+ }
32313259
alert_send(pSender,&hdr,&body,0);
32323260
nSent++;
32333261
blob_truncate(&hdr, 0);
32343262
blob_truncate(&body, 0);
32353263
}
@@ -3271,18 +3299,28 @@
32713299
" AND length(sdigest)>0",
32723300
iNewWarn, iOldWarn
32733301
);
32743302
while( db_step(&q)==SQLITE_ROW ){
32753303
Blob hdr, body;
3304
+ const char *zCode = db_column_text(&q,0);
32763305
blob_init(&hdr, 0, 0);
32773306
blob_init(&body, 0, 0);
32783307
alert_renewal_msg(&hdr, &body,
3279
- db_column_text(&q,0),
3308
+ zCode,
32803309
db_column_int(&q,1),
32813310
db_column_text(&q,2),
32823311
db_column_text(&q,3),
32833312
zRepoName, zUrl);
3313
+ if( pSender->zListId && pSender->zListId[0] ){
3314
+ blob_appendf(&hdr, "List-Id: %s\r\n", pSender->zListId);
3315
+ blob_appendf(&hdr, "List-Unsubscribe: <%s/oneclickunsub/%s>\r\n",
3316
+ zUrl, zCode);
3317
+ blob_appendf(&hdr,
3318
+ "List-Unsubscribe-Post: List-Unsubscribe=One-Click\r\n");
3319
+ blob_appendf(&body, "\n-- \nUnsubscribe: %s/unsubscribe/%s\n",
3320
+ zUrl, zCode);
3321
+ }
32843322
alert_send(pSender,&hdr,&body,0);
32853323
blob_reset(&hdr);
32863324
blob_reset(&body);
32873325
}
32883326
db_finalize(&q);
@@ -3436,16 +3474,28 @@
34363474
char *zSubject = PT("subject");
34373475
int bAll = PB("all");
34383476
int bAA = PB("aa");
34393477
int bMods = PB("mods");
34403478
const char *zSub = db_get("email-subname", "[Fossil Repo]");
3441
- int bTest2 = fossil_strcmp(P("name"),"test2")==0;
3479
+ const char *zName = P("name"); /* Debugging options */
3480
+ const char *zDest = 0; /* How to send the announcement */
3481
+ int bTest = 0;
34423482
Blob hdr, body;
3483
+
3484
+ if( fossil_strcmp(zName, "test2")==0 ){
3485
+ bTest = 2;
3486
+ zDest = "blob";
3487
+ }else if( fossil_strcmp(zName, "test3")==0 ){
3488
+ bTest = 3;
3489
+ if( fossil_strcmp(db_get("email-send-method",""),"relay")==0 ){
3490
+ zDest = "debug-relay";
3491
+ }
3492
+ }
34433493
blob_init(&body, 0, 0);
34443494
blob_init(&hdr, 0, 0);
34453495
blob_appendf(&body, "%s", PT("msg")/*safe-for-%s*/);
3446
- pSender = alert_sender_new(bTest2 ? "blob" : 0, 0);
3496
+ pSender = alert_sender_new(zDest, 0);
34473497
if( zTo[0] ){
34483498
blob_appendf(&hdr, "To: <%s>\r\nSubject: %s %s\r\n", zTo, zSub, zSubject);
34493499
alert_send(pSender, &hdr, &body, 0);
34503500
}
34513501
if( bAll || bAA || bMods ){
@@ -3479,17 +3529,24 @@
34793529
}
34803530
alert_send(pSender, &hdr, &body, 0);
34813531
}
34823532
db_finalize(&q);
34833533
}
3484
- if( bTest2 ){
3485
- /* If the URL is /announce/test2 instead of just /announce, then no
3486
- ** email is actually sent. Instead, the text of the email that would
3487
- ** have been sent is displayed in the result window. */
3488
- @ <pre style='border: 2px solid blue; padding: 1ex'>
3534
+ if( bTest && blob_size(&pSender->out) ){
3535
+ /* If the URL is "/announce/test2" then no email is actually sent.
3536
+ ** Instead, the text of the email that would have been sent is
3537
+ ** displayed in the result window.
3538
+ **
3539
+ ** If the URL is "/announce/test3" and the email-send-method is "relay"
3540
+ ** then the announcement is sent as it normally would be, but a
3541
+ ** transcript of the SMTP conversation with the MTA is shown here.
3542
+ */
3543
+ blob_trim(&pSender->out);
3544
+ @ <pre style='border: 2px solid blue; padding: 1ex;'>
34893545
@ %h(blob_str(&pSender->out))
34903546
@ </pre>
3547
+ blob_reset(&pSender->out);
34913548
}
34923549
zErr = pSender->zErr;
34933550
pSender->zErr = 0;
34943551
alert_sender_free(pSender);
34953552
return zErr;
@@ -3505,35 +3562,43 @@
35053562
** also send a message to an arbitrary email address and/or to all
35063563
** subscribers regardless of whether or not they have elected to
35073564
** receive announcements.
35083565
*/
35093566
void announce_page(void){
3510
- const char *zAction = "announce"
3511
- /* Maintenance reminder: we need an explicit action=THIS_PAGE on the
3512
- ** form element to avoid that a URL arg of to=... passed to this
3513
- ** page ends up overwriting the form-posted "to" value. This
3514
- ** action value differs for the test1 request path.
3515
- */;
3516
-
3567
+ const char *zAction = "announce";
3568
+ const char *zName = PD("name","");
3569
+ /*
3570
+ ** Debugging Notes:
3571
+ **
3572
+ ** /announce/test1 -> Shows query parameter values
3573
+ ** /announce/test2 -> Shows the formatted message but does
3574
+ ** not send it.
3575
+ ** /announce/test3 -> Sends the message, but also shows
3576
+ ** the SMTP transcript.
3577
+ */
35173578
login_check_credentials();
35183579
if( !g.perm.Announce ){
35193580
login_needed(0);
35203581
return;
35213582
}
3583
+ if( !g.perm.Setup ){
3584
+ zName = 0; /* Disable debugging feature for non-admin users */
3585
+ }
35223586
style_set_current_feature("alerts");
3523
- if( fossil_strcmp(P("name"),"test1")==0 ){
3587
+ if( fossil_strcmp(zName,"test1")==0 ){
35243588
/* Visit the /announce/test1 page to see the CGI variables */
35253589
zAction = "announce/test1";
35263590
@ <p style='border: 1px solid black; padding: 1ex;'>
35273591
cgi_print_all(0, 0, 0);
35283592
@ </p>
35293593
}else if( P("submit")!=0 && cgi_csrf_safe(2) ){
35303594
char *zErr = alert_send_announcement();
35313595
style_header("Announcement Sent");
35323596
if( zErr ){
3533
- @ <h1>Internal Error</h1>
3534
- @ <p>The following error was reported by the system:
3597
+ @ <h1>Error</h1>
3598
+ @ <p>The following error was reported by the
3599
+ @ announcement-sending subsystem:
35353600
@ <blockquote><pre>
35363601
@ %h(zErr)
35373602
@ </pre></blockquote>
35383603
}else{
35393604
@ <p>The announcement has been sent.
@@ -3548,10 +3613,16 @@
35483613
@ for this repository.</p>
35493614
return;
35503615
}
35513616
35523617
style_header("Send Announcement");
3618
+ alert_submenu_common();
3619
+ if( fossil_strcmp(zName,"test2")==0 ){
3620
+ zAction = "announce/test2";
3621
+ }else if( fossil_strcmp(zName,"test3")==0 ){
3622
+ zAction = "announce/test3";
3623
+ }
35533624
@ <form method="POST" action="%R/%s(zAction)">
35543625
login_insert_csrf_secret();
35553626
@ <table class="subscribe">
35563627
if( g.perm.Admin ){
35573628
int aa = PB("aa");
@@ -3584,15 +3655,28 @@
35843655
@ <td><textarea name="msg" cols="80" rows="10" wrap="virtual">\
35853656
@ %h(PT("msg"))</textarea>
35863657
@ </tr>
35873658
@ <tr>
35883659
@ <td></td>
3589
- if( fossil_strcmp(P("name"),"test2")==0 ){
3660
+ if( fossil_strcmp(zName,"test2")==0 ){
35903661
@ <td><input type="submit" name="submit" value="Dry Run">
35913662
}else{
35923663
@ <td><input type="submit" name="submit" value="Send Message">
35933664
}
35943665
@ </tr>
35953666
@ </table>
35963667
@ </form>
3668
+ if( g.perm.Setup ){
3669
+ @ <hr>
3670
+ @ <p>Trouble-shooting Options:</p>
3671
+ @ <ol>
3672
+ @ <li> <a href="%R/announce">Normal Processing</a>
3673
+ @ <li> Only <a href="%R/announce/test1">show POST parameters</a>
3674
+ @ - Do not send the announcement.
3675
+ @ <li> <a href="%R/announce/test2">Show the email text</a> but do
3676
+ @ not actually send it.
3677
+ @ <li> Send the message and also <a href="%R/announce/test3">show the
3678
+ @ SMTP traffic</a> when using "relay" mode.
3679
+ @ </ol>
3680
+ }
35973681
style_finish_page();
35983682
}
35993683
--- src/alerts.c
+++ src/alerts.c
@@ -51,11 +51,11 @@
51 @ -- f - Forum posts
52 @ -- k - ** Special: Unsubscribed using /oneclickunsub
53 @ -- n - New forum threads
54 @ -- r - Replies to my own forum posts
55 @ -- t - Ticket changes
56 @ -- u - Elevation of users' permissions (admins only)
57 @ -- w - Wiki changes
58 @ -- x - Edits to forum posts
59 @ -- Probably different codes will be added in the future. In the future
60 @ -- we might also add a separate table that allows subscribing to email
61 @ -- notifications for specific branches or tags or tickets.
@@ -282,10 +282,13 @@
282 style_submenu_element("Subscribers","%R/subscribers");
283 }
284 if( fossil_strcmp(g.zPath,"subscribe") ){
285 style_submenu_element("Add New Subscriber","%R/subscribe");
286 }
 
 
 
287 }
288 }
289
290
291 /*
@@ -295,14 +298,14 @@
295 ** Normally accessible via the /Admin/Notification menu.
296 */
297 void setup_notification(void){
298 static const char *const azSendMethods[] = {
299 "off", "Disabled",
300 "pipe", "Pipe to a command",
301 "db", "Store in a database",
302 "dir", "Store in a directory",
303 "relay", "SMTP relay"
304 };
305 login_check_credentials();
306 if( !g.perm.Setup ){
307 login_needed(0);
308 return;
@@ -311,22 +314,25 @@
311
312 alert_submenu_common();
313 style_submenu_element("Send Announcement","%R/announce");
314 style_set_current_feature("alerts");
315 style_header("Email Notification Setup");
316 @ <h1>Status</h1>
 
 
317 @ <table class="label-value">
318 if( alert_enabled() ){
319 stats_for_email();
320 }else{
321 @ <th>Disabled</th>
322 }
323 @ </table>
324 @ <hr>
325 @ <h1> Configuration </h1>
326 @ <form action="%R/setup_notification" method="post"><div>
327 @ <input type="submit" name="submit" value="Apply Changes"><hr>
 
 
328 login_insert_csrf_secret();
329
330 entry_attribute("Canonical Server URL", 40, "email-url",
331 "eurl", "", 0);
332 @ <p><b>Required.</b>
@@ -391,38 +397,40 @@
391 @ <p>How to send email. Requires auxiliary information from the fields
392 @ that follow. Hint: Use the <a href="%R/announce">/announce</a> page
393 @ to send test message to debug this setting.
394 @ (Property: "email-send-method")</p>
395 alert_schema(1);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
396 entry_attribute("Pipe Email Text Into This Command", 60, "email-send-command",
397 "ecmd", "sendmail -ti", 0);
398 @ <p>When the send method is "pipe to a command", this is the command
399 @ that is run. Email messages are piped into the standard input of this
400 @ command. The command is expected to extract the sender address,
401 @ recipient addresses, and subject from the header of the piped email
402 @ text. (Property: "email-send-command")</p>
403
404 entry_attribute("Store Emails In This Database", 60, "email-send-db",
405 "esdb", "", 0);
406 @ <p>When the send method is "store in a database", each email message is
407 @ stored in an SQLite database file with the name given here.
408 @ (Property: "email-send-db")</p>
409
410 entry_attribute("Store Emails In This Directory", 60, "email-send-dir",
411 "esdir", "", 0);
412 @ <p>When the send method is "store in a directory", each email message is
413 @ stored as a separate file in the directory shown here.
414 @ (Property: "email-send-dir")</p>
415
416 entry_attribute("SMTP Relay Host", 60, "email-send-relayhost",
417 "esrh", "", 0);
418 @ <p>When the send method is "SMTP relay", each email message is
419 @ transmitted via the SMTP protocol (rfc5321) to a "Mail Submission
420 @ Agent" or "MSA" (rfc4409) at the hostname shown here. Optionally
421 @ append a colon and TCP port number (ex: smtp.example.com:587).
422 @ The default TCP port number is 25.
423 @ (Property: "email-send-relayhost")</p>
424 @ <hr>
425
426 @ <p><input type="submit" name="submit" value="Apply Changes"></p>
427 @ </div></form>
428 db_end_transaction(0);
@@ -630,19 +638,27 @@
630 emailerGetSetting(p, &p->zCmd, "email-send-command");
631 }else if( fossil_strcmp(p->zDest, "dir")==0 ){
632 emailerGetSetting(p, &p->zDir, "email-send-dir");
633 }else if( fossil_strcmp(p->zDest, "blob")==0 ){
634 blob_init(&p->out, 0, 0);
635 }else if( fossil_strcmp(p->zDest, "relay")==0 ){
 
 
636 const char *zRelay = 0;
637 emailerGetSetting(p, &zRelay, "email-send-relayhost");
638 if( zRelay ){
639 u32 smtpFlags = SMTP_DIRECT;
640 if( mFlags & ALERT_TRACE ) smtpFlags |= SMTP_TRACE_STDOUT;
 
641 p->pSmtp = smtp_session_new(domain_of_addr(p->zFrom), zRelay,
642 smtpFlags);
643 smtp_client_startup(p->pSmtp);
 
 
 
 
 
644 }
645 }
646 return p;
647 }
648
@@ -968,13 +984,10 @@
968 blob_appendf(pOut, "Sender: <%s>\r\n", p->zFrom);
969 }else{
970 blob_appendf(pOut, "From: <%s>\r\n", p->zFrom);
971 }
972 blob_appendf(pOut, "Date: %z\r\n", cgi_rfc822_datestamp(time(0)));
973 if( p->zListId && p->zListId[0] ){
974 blob_appendf(pOut, "List-Id: %s\r\n", p->zListId);
975 }
976 if( strstr(blob_str(pHdr), "\r\nMessage-Id:")==0 ){
977 /* Message-id format: "<$(date)x$(random)@$(from-host)>" where $(date) is
978 ** the current unix-time in hex, $(random) is a 64-bit random number,
979 ** and $(from) is the domain part of the email-self setting. */
980 sqlite3_randomness(sizeof(r1), &r1);
@@ -1016,13 +1029,21 @@
1016 blob_write_to_file(&all, zFile);
1017 fossil_free(zFile);
1018 }else if( p->pSmtp ){
1019 char **azTo = 0;
1020 int nTo = 0;
 
1021 email_header_to(pHdr, &nTo, &azTo);
1022 if( nTo>0 ){
1023 smtp_send_msg(p->pSmtp, p->zFrom, nTo, (const char**)azTo,blob_str(&all));
 
 
 
 
 
 
 
1024 email_header_to_free(nTo, azTo);
1025 }
1026 }else if( strcmp(p->zDest, "stdout")==0 ){
1027 char **azTo = 0;
1028 int nTo = 0;
@@ -1125,11 +1146,11 @@
1125 ** SETTING: email-listid width=40
1126 ** If this setting is not an empty string, then it becomes the argument to
1127 ** a "List-ID:" header that is added to all out-bound notification emails.
1128 */
1129 /*
1130 ** SETTING: email-send-relayhost width=40 sensitive
1131 ** This is the hostname and TCP port to which output email messages
1132 ** are sent when email-send-method is "relay". There should be an
1133 ** SMTP server configured as a Mail Submission Agent listening on the
1134 ** designated host and port and all times.
1135 */
@@ -1704,11 +1725,11 @@
1704 @ <label><input type="checkbox" name="sw" %s(PCK("sw"))> \
1705 @ Wiki</label><br>
1706 }
1707 if( g.perm.Admin ){
1708 @ <label><input type="checkbox" name="su" %s(PCK("su"))> \
1709 @ User permission elevation</label>
1710 }
1711 di = PB("di");
1712 @ </td></tr>
1713 @ <tr>
1714 @ <td class="form_label">Delivery:</td>
@@ -2114,11 +2135,11 @@
2114 /* Corner-case bug: if an admin assigns 'u' to a non-admin, that
2115 ** subscription will get removed if the user later edits their
2116 ** subscriptions, as non-admins are not permitted to add that
2117 ** subscription. */
2118 @ <label><input type="checkbox" name="su" %s(su?"checked":"")>\
2119 @ User permission elevation</label>
2120 }
2121 @ </td></tr>
2122 if( strchr(ssub,'k')!=0 ){
2123 @ <tr><td></td><td>&nbsp;&uarr;&nbsp;
2124 @ Note: User did a one-click unsubscribe</td></tr>
@@ -3191,18 +3212,21 @@
3191 Blob fhdr, fbody;
3192 blob_init(&fhdr, 0, 0);
3193 blob_appendf(&fhdr, "To: <%s>\r\n", zEmail);
3194 blob_append(&fhdr, blob_buffer(&p->hdr), blob_size(&p->hdr));
3195 blob_init(&fbody, blob_buffer(&p->txt), blob_size(&p->txt));
3196 blob_appendf(&fhdr, "List-Unsubscribe: <%s/oneclickunsub/%s>\r\n",
3197 zUrl, zCode);
3198 blob_appendf(&fhdr,
3199 "List-Unsubscribe-Post: List-Unsubscribe=One-Click\r\n");
3200 blob_appendf(&fbody, "\n-- \nUnsubscribe: %s/unsubscribe/%s\n",
3201 zUrl, zCode);
3202 /* blob_appendf(&fbody, "Subscription settings: %s/alerts/%s\n",
3203 ** zUrl, zCode); */
 
 
 
3204 alert_send(pSender,&fhdr,&fbody,p->zFromName);
3205 nSent++;
3206 blob_reset(&fhdr);
3207 blob_reset(&fbody);
3208 }else{
@@ -3221,15 +3245,19 @@
3221 blob_append(&body, "\n", 1);
3222 blob_append(&body, blob_buffer(&p->txt), blob_size(&p->txt));
3223 }
3224 }
3225 if( nHit==0 ) continue;
3226 blob_appendf(&hdr, "List-Unsubscribe: <%s/oneclickunsub/%s>\r\n",
3227 zUrl, zCode);
3228 blob_appendf(&hdr, "List-Unsubscribe-Post: List-Unsubscribe=One-Click\r\n");
3229 blob_appendf(&body,"\n-- \nSubscription info: %s/alerts/%s\n",
3230 zUrl, zCode);
 
 
 
 
3231 alert_send(pSender,&hdr,&body,0);
3232 nSent++;
3233 blob_truncate(&hdr, 0);
3234 blob_truncate(&body, 0);
3235 }
@@ -3271,18 +3299,28 @@
3271 " AND length(sdigest)>0",
3272 iNewWarn, iOldWarn
3273 );
3274 while( db_step(&q)==SQLITE_ROW ){
3275 Blob hdr, body;
 
3276 blob_init(&hdr, 0, 0);
3277 blob_init(&body, 0, 0);
3278 alert_renewal_msg(&hdr, &body,
3279 db_column_text(&q,0),
3280 db_column_int(&q,1),
3281 db_column_text(&q,2),
3282 db_column_text(&q,3),
3283 zRepoName, zUrl);
 
 
 
 
 
 
 
 
 
3284 alert_send(pSender,&hdr,&body,0);
3285 blob_reset(&hdr);
3286 blob_reset(&body);
3287 }
3288 db_finalize(&q);
@@ -3436,16 +3474,28 @@
3436 char *zSubject = PT("subject");
3437 int bAll = PB("all");
3438 int bAA = PB("aa");
3439 int bMods = PB("mods");
3440 const char *zSub = db_get("email-subname", "[Fossil Repo]");
3441 int bTest2 = fossil_strcmp(P("name"),"test2")==0;
 
 
3442 Blob hdr, body;
 
 
 
 
 
 
 
 
 
 
3443 blob_init(&body, 0, 0);
3444 blob_init(&hdr, 0, 0);
3445 blob_appendf(&body, "%s", PT("msg")/*safe-for-%s*/);
3446 pSender = alert_sender_new(bTest2 ? "blob" : 0, 0);
3447 if( zTo[0] ){
3448 blob_appendf(&hdr, "To: <%s>\r\nSubject: %s %s\r\n", zTo, zSub, zSubject);
3449 alert_send(pSender, &hdr, &body, 0);
3450 }
3451 if( bAll || bAA || bMods ){
@@ -3479,17 +3529,24 @@
3479 }
3480 alert_send(pSender, &hdr, &body, 0);
3481 }
3482 db_finalize(&q);
3483 }
3484 if( bTest2 ){
3485 /* If the URL is /announce/test2 instead of just /announce, then no
3486 ** email is actually sent. Instead, the text of the email that would
3487 ** have been sent is displayed in the result window. */
3488 @ <pre style='border: 2px solid blue; padding: 1ex'>
 
 
 
 
 
 
3489 @ %h(blob_str(&pSender->out))
3490 @ </pre>
 
3491 }
3492 zErr = pSender->zErr;
3493 pSender->zErr = 0;
3494 alert_sender_free(pSender);
3495 return zErr;
@@ -3505,35 +3562,43 @@
3505 ** also send a message to an arbitrary email address and/or to all
3506 ** subscribers regardless of whether or not they have elected to
3507 ** receive announcements.
3508 */
3509 void announce_page(void){
3510 const char *zAction = "announce"
3511 /* Maintenance reminder: we need an explicit action=THIS_PAGE on the
3512 ** form element to avoid that a URL arg of to=... passed to this
3513 ** page ends up overwriting the form-posted "to" value. This
3514 ** action value differs for the test1 request path.
3515 */;
3516
 
 
 
 
3517 login_check_credentials();
3518 if( !g.perm.Announce ){
3519 login_needed(0);
3520 return;
3521 }
 
 
 
3522 style_set_current_feature("alerts");
3523 if( fossil_strcmp(P("name"),"test1")==0 ){
3524 /* Visit the /announce/test1 page to see the CGI variables */
3525 zAction = "announce/test1";
3526 @ <p style='border: 1px solid black; padding: 1ex;'>
3527 cgi_print_all(0, 0, 0);
3528 @ </p>
3529 }else if( P("submit")!=0 && cgi_csrf_safe(2) ){
3530 char *zErr = alert_send_announcement();
3531 style_header("Announcement Sent");
3532 if( zErr ){
3533 @ <h1>Internal Error</h1>
3534 @ <p>The following error was reported by the system:
 
3535 @ <blockquote><pre>
3536 @ %h(zErr)
3537 @ </pre></blockquote>
3538 }else{
3539 @ <p>The announcement has been sent.
@@ -3548,10 +3613,16 @@
3548 @ for this repository.</p>
3549 return;
3550 }
3551
3552 style_header("Send Announcement");
 
 
 
 
 
 
3553 @ <form method="POST" action="%R/%s(zAction)">
3554 login_insert_csrf_secret();
3555 @ <table class="subscribe">
3556 if( g.perm.Admin ){
3557 int aa = PB("aa");
@@ -3584,15 +3655,28 @@
3584 @ <td><textarea name="msg" cols="80" rows="10" wrap="virtual">\
3585 @ %h(PT("msg"))</textarea>
3586 @ </tr>
3587 @ <tr>
3588 @ <td></td>
3589 if( fossil_strcmp(P("name"),"test2")==0 ){
3590 @ <td><input type="submit" name="submit" value="Dry Run">
3591 }else{
3592 @ <td><input type="submit" name="submit" value="Send Message">
3593 }
3594 @ </tr>
3595 @ </table>
3596 @ </form>
 
 
 
 
 
 
 
 
 
 
 
 
 
3597 style_finish_page();
3598 }
3599
--- src/alerts.c
+++ src/alerts.c
@@ -51,11 +51,11 @@
51 @ -- f - Forum posts
52 @ -- k - ** Special: Unsubscribed using /oneclickunsub
53 @ -- n - New forum threads
54 @ -- r - Replies to my own forum posts
55 @ -- t - Ticket changes
56 @ -- u - Changes of users' permissions (admins only)
57 @ -- w - Wiki changes
58 @ -- x - Edits to forum posts
59 @ -- Probably different codes will be added in the future. In the future
60 @ -- we might also add a separate table that allows subscribing to email
61 @ -- notifications for specific branches or tags or tickets.
@@ -282,10 +282,13 @@
282 style_submenu_element("Subscribers","%R/subscribers");
283 }
284 if( fossil_strcmp(g.zPath,"subscribe") ){
285 style_submenu_element("Add New Subscriber","%R/subscribe");
286 }
287 if( fossil_strcmp(g.zPath,"setup_notification") ){
288 style_submenu_element("Notification Setup","%R/setup_notification");
289 }
290 }
291 }
292
293
294 /*
@@ -295,14 +298,14 @@
298 ** Normally accessible via the /Admin/Notification menu.
299 */
300 void setup_notification(void){
301 static const char *const azSendMethods[] = {
302 "off", "Disabled",
303 "relay", "SMTP relay",
304 "db", "Store in a database",
305 "dir", "Store in a directory",
306 "pipe", "Pipe to a command",
307 };
308 login_check_credentials();
309 if( !g.perm.Setup ){
310 login_needed(0);
311 return;
@@ -311,22 +314,25 @@
314
315 alert_submenu_common();
316 style_submenu_element("Send Announcement","%R/announce");
317 style_set_current_feature("alerts");
318 style_header("Email Notification Setup");
319 @ <form action="%R/setup_notification" method="post"><div>
320 @ <h1>Status &ensp; <input type="submit" name="submit" value="Refresh"></h1>
321 @ </form>
322 @ <table class="label-value">
323 if( alert_enabled() ){
324 stats_for_email();
325 }else{
326 @ <th>Disabled</th>
327 }
328 @ </table>
329 @ <hr>
 
330 @ <form action="%R/setup_notification" method="post"><div>
331 @ <h1> Configuration </h1>
332 @ <p><input type="submit" name="submit" value="Apply Changes"></p>
333 @ <hr>
334 login_insert_csrf_secret();
335
336 entry_attribute("Canonical Server URL", 40, "email-url",
337 "eurl", "", 0);
338 @ <p><b>Required.</b>
@@ -391,38 +397,40 @@
397 @ <p>How to send email. Requires auxiliary information from the fields
398 @ that follow. Hint: Use the <a href="%R/announce">/announce</a> page
399 @ to send test message to debug this setting.
400 @ (Property: "email-send-method")</p>
401 alert_schema(1);
402 entry_attribute("SMTP Relay Host", 60, "email-send-relayhost",
403 "esrh", "localhost", 0);
404 @ <p>When the send method is "SMTP relay", each email message is
405 @ transmitted via the SMTP protocol (rfc5321) to a "Mail Submission
406 @ Agent" or "MSA" (rfc4409) at the hostname shown here. Optionally
407 @ append a colon and TCP port number (ex: smtp.example.com:587).
408 @ The default TCP port number is 25.
409 @ Usage Hint: If Fossil is running inside of a chroot jail, then it might
410 @ not be able to resolve hostnames. Work around this by using a raw IP
411 @ address or create a "/etc/hosts" file inside the chroot jail.
412 @ (Property: "email-send-relayhost")</p>
413 @
414 entry_attribute("Store Emails In This Database", 60, "email-send-db",
415 "esdb", "", 0);
416 @ <p>When the send method is "store in a database", each email message is
417 @ stored in an SQLite database file with the name given here.
418 @ (Property: "email-send-db")</p>
419 entry_attribute("Pipe Email Text Into This Command", 60, "email-send-command",
420 "ecmd", "sendmail -ti", 0);
421 @ <p>When the send method is "pipe to a command", this is the command
422 @ that is run. Email messages are piped into the standard input of this
423 @ command. The command is expected to extract the sender address,
424 @ recipient addresses, and subject from the header of the piped email
425 @ text. (Property: "email-send-command")</p>
 
 
 
 
 
 
 
426 entry_attribute("Store Emails In This Directory", 60, "email-send-dir",
427 "esdir", "", 0);
428 @ <p>When the send method is "store in a directory", each email message is
429 @ stored as a separate file in the directory shown here.
430 @ (Property: "email-send-dir")</p>
431
 
 
 
 
 
 
 
 
432 @ <hr>
433
434 @ <p><input type="submit" name="submit" value="Apply Changes"></p>
435 @ </div></form>
436 db_end_transaction(0);
@@ -630,19 +638,27 @@
638 emailerGetSetting(p, &p->zCmd, "email-send-command");
639 }else if( fossil_strcmp(p->zDest, "dir")==0 ){
640 emailerGetSetting(p, &p->zDir, "email-send-dir");
641 }else if( fossil_strcmp(p->zDest, "blob")==0 ){
642 blob_init(&p->out, 0, 0);
643 }else if( fossil_strcmp(p->zDest, "relay")==0
644 || fossil_strcmp(p->zDest, "debug-relay")==0
645 ){
646 const char *zRelay = 0;
647 emailerGetSetting(p, &zRelay, "email-send-relayhost");
648 if( zRelay ){
649 u32 smtpFlags = SMTP_DIRECT;
650 if( mFlags & ALERT_TRACE ) smtpFlags |= SMTP_TRACE_STDOUT;
651 blob_init(&p->out, 0, 0);
652 p->pSmtp = smtp_session_new(domain_of_addr(p->zFrom), zRelay,
653 smtpFlags, 0);
654 if( p->pSmtp==0 || p->pSmtp->zErr ){
655 emailerError(p, "Could not start SMTP session: %s",
656 p->pSmtp ? p->pSmtp->zErr : "reason unknown");
657 }else if( p->zDest[0]=='d' ){
658 smtp_session_config(p->pSmtp, SMTP_TRACE_BLOB, &p->out);
659 }
660 }
661 }
662 return p;
663 }
664
@@ -968,13 +984,10 @@
984 blob_appendf(pOut, "Sender: <%s>\r\n", p->zFrom);
985 }else{
986 blob_appendf(pOut, "From: <%s>\r\n", p->zFrom);
987 }
988 blob_appendf(pOut, "Date: %z\r\n", cgi_rfc822_datestamp(time(0)));
 
 
 
989 if( strstr(blob_str(pHdr), "\r\nMessage-Id:")==0 ){
990 /* Message-id format: "<$(date)x$(random)@$(from-host)>" where $(date) is
991 ** the current unix-time in hex, $(random) is a 64-bit random number,
992 ** and $(from) is the domain part of the email-self setting. */
993 sqlite3_randomness(sizeof(r1), &r1);
@@ -1016,13 +1029,21 @@
1029 blob_write_to_file(&all, zFile);
1030 fossil_free(zFile);
1031 }else if( p->pSmtp ){
1032 char **azTo = 0;
1033 int nTo = 0;
1034 SmtpSession *pSmtp = p->pSmtp;
1035 email_header_to(pHdr, &nTo, &azTo);
1036 if( nTo>0 && !pSmtp->bFatal ){
1037 smtp_send_msg(pSmtp,p->zFrom,nTo,(const char**)azTo,blob_str(&all));
1038 if( pSmtp->zErr && !pSmtp->bFatal ){
1039 smtp_send_msg(pSmtp,p->zFrom,nTo,(const char**)azTo,blob_str(&all));
1040 }
1041 if( pSmtp->zErr ){
1042 fossil_errorlog("SMTP: (%s) %s", pSmtp->bFatal ? "fatal" : "retry",
1043 pSmtp->zErr);
1044 }
1045 email_header_to_free(nTo, azTo);
1046 }
1047 }else if( strcmp(p->zDest, "stdout")==0 ){
1048 char **azTo = 0;
1049 int nTo = 0;
@@ -1125,11 +1146,11 @@
1146 ** SETTING: email-listid width=40
1147 ** If this setting is not an empty string, then it becomes the argument to
1148 ** a "List-ID:" header that is added to all out-bound notification emails.
1149 */
1150 /*
1151 ** SETTING: email-send-relayhost width=40 sensitive default=127.0.0.1
1152 ** This is the hostname and TCP port to which output email messages
1153 ** are sent when email-send-method is "relay". There should be an
1154 ** SMTP server configured as a Mail Submission Agent listening on the
1155 ** designated host and port and all times.
1156 */
@@ -1704,11 +1725,11 @@
1725 @ <label><input type="checkbox" name="sw" %s(PCK("sw"))> \
1726 @ Wiki</label><br>
1727 }
1728 if( g.perm.Admin ){
1729 @ <label><input type="checkbox" name="su" %s(PCK("su"))> \
1730 @ User permission changes</label>
1731 }
1732 di = PB("di");
1733 @ </td></tr>
1734 @ <tr>
1735 @ <td class="form_label">Delivery:</td>
@@ -2114,11 +2135,11 @@
2135 /* Corner-case bug: if an admin assigns 'u' to a non-admin, that
2136 ** subscription will get removed if the user later edits their
2137 ** subscriptions, as non-admins are not permitted to add that
2138 ** subscription. */
2139 @ <label><input type="checkbox" name="su" %s(su?"checked":"")>\
2140 @ User permission changes</label>
2141 }
2142 @ </td></tr>
2143 if( strchr(ssub,'k')!=0 ){
2144 @ <tr><td></td><td>&nbsp;&uarr;&nbsp;
2145 @ Note: User did a one-click unsubscribe</td></tr>
@@ -3191,18 +3212,21 @@
3212 Blob fhdr, fbody;
3213 blob_init(&fhdr, 0, 0);
3214 blob_appendf(&fhdr, "To: <%s>\r\n", zEmail);
3215 blob_append(&fhdr, blob_buffer(&p->hdr), blob_size(&p->hdr));
3216 blob_init(&fbody, blob_buffer(&p->txt), blob_size(&p->txt));
3217 if( pSender->zListId && pSender->zListId[0] ){
3218 blob_appendf(&fhdr, "List-Id: %s\r\n", pSender->zListId);
3219 blob_appendf(&fhdr, "List-Unsubscribe: <%s/oneclickunsub/%s>\r\n",
3220 zUrl, zCode);
3221 blob_appendf(&fhdr,
3222 "List-Unsubscribe-Post: List-Unsubscribe=One-Click\r\n");
3223 blob_appendf(&fbody, "\n-- \nUnsubscribe: %s/unsubscribe/%s\n",
3224 zUrl, zCode);
3225 /* blob_appendf(&fbody, "Subscription settings: %s/alerts/%s\n",
3226 ** zUrl, zCode); */
3227 }
3228 alert_send(pSender,&fhdr,&fbody,p->zFromName);
3229 nSent++;
3230 blob_reset(&fhdr);
3231 blob_reset(&fbody);
3232 }else{
@@ -3221,15 +3245,19 @@
3245 blob_append(&body, "\n", 1);
3246 blob_append(&body, blob_buffer(&p->txt), blob_size(&p->txt));
3247 }
3248 }
3249 if( nHit==0 ) continue;
3250 if( pSender->zListId && pSender->zListId[0] ){
3251 blob_appendf(&hdr, "List-Id: %s\r\n", pSender->zListId);
3252 blob_appendf(&hdr, "List-Unsubscribe: <%s/oneclickunsub/%s>\r\n",
3253 zUrl, zCode);
3254 blob_appendf(&hdr,
3255 "List-Unsubscribe-Post: List-Unsubscribe=One-Click\r\n");
3256 blob_appendf(&body,"\n-- \nSubscription info: %s/alerts/%s\n",
3257 zUrl, zCode);
3258 }
3259 alert_send(pSender,&hdr,&body,0);
3260 nSent++;
3261 blob_truncate(&hdr, 0);
3262 blob_truncate(&body, 0);
3263 }
@@ -3271,18 +3299,28 @@
3299 " AND length(sdigest)>0",
3300 iNewWarn, iOldWarn
3301 );
3302 while( db_step(&q)==SQLITE_ROW ){
3303 Blob hdr, body;
3304 const char *zCode = db_column_text(&q,0);
3305 blob_init(&hdr, 0, 0);
3306 blob_init(&body, 0, 0);
3307 alert_renewal_msg(&hdr, &body,
3308 zCode,
3309 db_column_int(&q,1),
3310 db_column_text(&q,2),
3311 db_column_text(&q,3),
3312 zRepoName, zUrl);
3313 if( pSender->zListId && pSender->zListId[0] ){
3314 blob_appendf(&hdr, "List-Id: %s\r\n", pSender->zListId);
3315 blob_appendf(&hdr, "List-Unsubscribe: <%s/oneclickunsub/%s>\r\n",
3316 zUrl, zCode);
3317 blob_appendf(&hdr,
3318 "List-Unsubscribe-Post: List-Unsubscribe=One-Click\r\n");
3319 blob_appendf(&body, "\n-- \nUnsubscribe: %s/unsubscribe/%s\n",
3320 zUrl, zCode);
3321 }
3322 alert_send(pSender,&hdr,&body,0);
3323 blob_reset(&hdr);
3324 blob_reset(&body);
3325 }
3326 db_finalize(&q);
@@ -3436,16 +3474,28 @@
3474 char *zSubject = PT("subject");
3475 int bAll = PB("all");
3476 int bAA = PB("aa");
3477 int bMods = PB("mods");
3478 const char *zSub = db_get("email-subname", "[Fossil Repo]");
3479 const char *zName = P("name"); /* Debugging options */
3480 const char *zDest = 0; /* How to send the announcement */
3481 int bTest = 0;
3482 Blob hdr, body;
3483
3484 if( fossil_strcmp(zName, "test2")==0 ){
3485 bTest = 2;
3486 zDest = "blob";
3487 }else if( fossil_strcmp(zName, "test3")==0 ){
3488 bTest = 3;
3489 if( fossil_strcmp(db_get("email-send-method",""),"relay")==0 ){
3490 zDest = "debug-relay";
3491 }
3492 }
3493 blob_init(&body, 0, 0);
3494 blob_init(&hdr, 0, 0);
3495 blob_appendf(&body, "%s", PT("msg")/*safe-for-%s*/);
3496 pSender = alert_sender_new(zDest, 0);
3497 if( zTo[0] ){
3498 blob_appendf(&hdr, "To: <%s>\r\nSubject: %s %s\r\n", zTo, zSub, zSubject);
3499 alert_send(pSender, &hdr, &body, 0);
3500 }
3501 if( bAll || bAA || bMods ){
@@ -3479,17 +3529,24 @@
3529 }
3530 alert_send(pSender, &hdr, &body, 0);
3531 }
3532 db_finalize(&q);
3533 }
3534 if( bTest && blob_size(&pSender->out) ){
3535 /* If the URL is "/announce/test2" then no email is actually sent.
3536 ** Instead, the text of the email that would have been sent is
3537 ** displayed in the result window.
3538 **
3539 ** If the URL is "/announce/test3" and the email-send-method is "relay"
3540 ** then the announcement is sent as it normally would be, but a
3541 ** transcript of the SMTP conversation with the MTA is shown here.
3542 */
3543 blob_trim(&pSender->out);
3544 @ <pre style='border: 2px solid blue; padding: 1ex;'>
3545 @ %h(blob_str(&pSender->out))
3546 @ </pre>
3547 blob_reset(&pSender->out);
3548 }
3549 zErr = pSender->zErr;
3550 pSender->zErr = 0;
3551 alert_sender_free(pSender);
3552 return zErr;
@@ -3505,35 +3562,43 @@
3562 ** also send a message to an arbitrary email address and/or to all
3563 ** subscribers regardless of whether or not they have elected to
3564 ** receive announcements.
3565 */
3566 void announce_page(void){
3567 const char *zAction = "announce";
3568 const char *zName = PD("name","");
3569 /*
3570 ** Debugging Notes:
3571 **
3572 ** /announce/test1 -> Shows query parameter values
3573 ** /announce/test2 -> Shows the formatted message but does
3574 ** not send it.
3575 ** /announce/test3 -> Sends the message, but also shows
3576 ** the SMTP transcript.
3577 */
3578 login_check_credentials();
3579 if( !g.perm.Announce ){
3580 login_needed(0);
3581 return;
3582 }
3583 if( !g.perm.Setup ){
3584 zName = 0; /* Disable debugging feature for non-admin users */
3585 }
3586 style_set_current_feature("alerts");
3587 if( fossil_strcmp(zName,"test1")==0 ){
3588 /* Visit the /announce/test1 page to see the CGI variables */
3589 zAction = "announce/test1";
3590 @ <p style='border: 1px solid black; padding: 1ex;'>
3591 cgi_print_all(0, 0, 0);
3592 @ </p>
3593 }else if( P("submit")!=0 && cgi_csrf_safe(2) ){
3594 char *zErr = alert_send_announcement();
3595 style_header("Announcement Sent");
3596 if( zErr ){
3597 @ <h1>Error</h1>
3598 @ <p>The following error was reported by the
3599 @ announcement-sending subsystem:
3600 @ <blockquote><pre>
3601 @ %h(zErr)
3602 @ </pre></blockquote>
3603 }else{
3604 @ <p>The announcement has been sent.
@@ -3548,10 +3613,16 @@
3613 @ for this repository.</p>
3614 return;
3615 }
3616
3617 style_header("Send Announcement");
3618 alert_submenu_common();
3619 if( fossil_strcmp(zName,"test2")==0 ){
3620 zAction = "announce/test2";
3621 }else if( fossil_strcmp(zName,"test3")==0 ){
3622 zAction = "announce/test3";
3623 }
3624 @ <form method="POST" action="%R/%s(zAction)">
3625 login_insert_csrf_secret();
3626 @ <table class="subscribe">
3627 if( g.perm.Admin ){
3628 int aa = PB("aa");
@@ -3584,15 +3655,28 @@
3655 @ <td><textarea name="msg" cols="80" rows="10" wrap="virtual">\
3656 @ %h(PT("msg"))</textarea>
3657 @ </tr>
3658 @ <tr>
3659 @ <td></td>
3660 if( fossil_strcmp(zName,"test2")==0 ){
3661 @ <td><input type="submit" name="submit" value="Dry Run">
3662 }else{
3663 @ <td><input type="submit" name="submit" value="Send Message">
3664 }
3665 @ </tr>
3666 @ </table>
3667 @ </form>
3668 if( g.perm.Setup ){
3669 @ <hr>
3670 @ <p>Trouble-shooting Options:</p>
3671 @ <ol>
3672 @ <li> <a href="%R/announce">Normal Processing</a>
3673 @ <li> Only <a href="%R/announce/test1">show POST parameters</a>
3674 @ - Do not send the announcement.
3675 @ <li> <a href="%R/announce/test2">Show the email text</a> but do
3676 @ not actually send it.
3677 @ <li> Send the message and also <a href="%R/announce/test3">show the
3678 @ SMTP traffic</a> when using "relay" mode.
3679 @ </ol>
3680 }
3681 style_finish_page();
3682 }
3683
+12 -5
--- src/backoffice.c
+++ src/backoffice.c
@@ -38,11 +38,11 @@
3838
** process table, doing nothing on rarely accessed repositories, and
3939
** if the Fossil binary is updated on a system, the backoffice processes
4040
** will restart using the new binary automatically.
4141
**
4242
** At any point in time there should be at most two backoffice processes.
43
-** There is a main process that is doing the actually work, and there is
43
+** There is a main process that is doing the actual work, and there is
4444
** a second stand-by process that is waiting for the main process to finish
4545
** and that will become the main process after a delay.
4646
**
4747
** After any successful web page reply, the backoffice_check_if_needed()
4848
** routine is called. That routine checks to see if both one or both of
@@ -53,11 +53,11 @@
5353
** backoffice_run_if_needed() routine is called. If the prior call
5454
** to backoffice_check_if_needed() indicated that backoffice processing
5555
** might be required, the run_if_needed() attempts to kick off a backoffice
5656
** process.
5757
**
58
-** All work performance by the backoffice is in the backoffice_work()
58
+** All work performed by the backoffice is in the backoffice_work()
5959
** routine.
6060
*/
6161
#if defined(_WIN32)
6262
# if defined(_WIN32_WINNT)
6363
# undef _WIN32_WINNT
@@ -485,11 +485,11 @@
485485
int warningDelay = 30;
486486
static int once = 0;
487487
488488
if( sqlite3_db_readonly(g.db, 0) ) return;
489489
if( db_is_protected(PROTECT_READONLY) ) return;
490
- g.zPhase = "backoffice";
490
+ g.zPhase = "backoffice-pending";
491491
backoffice_error_check_one(&once);
492492
idSelf = backofficeProcessId();
493493
while(1){
494494
tmNow = time(0);
495495
db_begin_write();
@@ -510,10 +510,11 @@
510510
/* This process can start doing backoffice work immediately */
511511
x.idCurrent = idSelf;
512512
x.tmCurrent = tmNow + BKOFCE_LEASE_TIME;
513513
x.idNext = 0;
514514
x.tmNext = 0;
515
+ g.zPhase = "backoffice-work";
515516
backofficeWriteLease(&x);
516517
db_end_transaction(0);
517518
backofficeTrace("/***** Begin Backoffice Processing %d *****/\n",
518519
GETPID());
519520
backoffice_work();
@@ -543,13 +544,16 @@
543544
db_end_transaction(0);
544545
break;
545546
}
546547
}else{
547548
if( (sqlite3_uint64)(lastWarning+warningDelay) < tmNow ){
548
- fossil_warning(
549
+ sqlite3_int64 runningFor = BKOFCE_LEASE_TIME + tmNow - x.tmCurrent;
550
+ if( warningDelay>=240 && runningFor<1800 ){
551
+ fossil_warning(
549552
"backoffice process %lld still running after %d seconds",
550
- x.idCurrent, (int)(BKOFCE_LEASE_TIME + tmNow - x.tmCurrent));
553
+ x.idCurrent, runningFor);
554
+ }
551555
lastWarning = tmNow;
552556
warningDelay *= 2;
553557
}
554558
if( backofficeSleep(1000) ){
555559
/* The sleep was interrupted by a signal from another thread. */
@@ -642,14 +646,17 @@
642646
backofficeBlob = &log;
643647
blob_appendf(&log, "%s %s", db_text(0, "SELECT datetime('now')"), zName);
644648
}
645649
646650
/* Here is where the actual work of the backoffice happens */
651
+ g.zPhase = "backoffice-alerts";
647652
nThis = alert_backoffice(0);
648653
if( nThis ){ backoffice_log("%d alerts", nThis); nTotal += nThis; }
654
+ g.zPhase = "backoffice-hooks";
649655
nThis = hook_backoffice();
650656
if( nThis ){ backoffice_log("%d hooks", nThis); nTotal += nThis; }
657
+ g.zPhase = "backoffice-close";
651658
652659
/* Close the log */
653660
if( backofficeFILE ){
654661
if( nTotal || backofficeLogDetail ){
655662
if( nTotal==0 ) backoffice_log("no-op");
656663
--- src/backoffice.c
+++ src/backoffice.c
@@ -38,11 +38,11 @@
38 ** process table, doing nothing on rarely accessed repositories, and
39 ** if the Fossil binary is updated on a system, the backoffice processes
40 ** will restart using the new binary automatically.
41 **
42 ** At any point in time there should be at most two backoffice processes.
43 ** There is a main process that is doing the actually work, and there is
44 ** a second stand-by process that is waiting for the main process to finish
45 ** and that will become the main process after a delay.
46 **
47 ** After any successful web page reply, the backoffice_check_if_needed()
48 ** routine is called. That routine checks to see if both one or both of
@@ -53,11 +53,11 @@
53 ** backoffice_run_if_needed() routine is called. If the prior call
54 ** to backoffice_check_if_needed() indicated that backoffice processing
55 ** might be required, the run_if_needed() attempts to kick off a backoffice
56 ** process.
57 **
58 ** All work performance by the backoffice is in the backoffice_work()
59 ** routine.
60 */
61 #if defined(_WIN32)
62 # if defined(_WIN32_WINNT)
63 # undef _WIN32_WINNT
@@ -485,11 +485,11 @@
485 int warningDelay = 30;
486 static int once = 0;
487
488 if( sqlite3_db_readonly(g.db, 0) ) return;
489 if( db_is_protected(PROTECT_READONLY) ) return;
490 g.zPhase = "backoffice";
491 backoffice_error_check_one(&once);
492 idSelf = backofficeProcessId();
493 while(1){
494 tmNow = time(0);
495 db_begin_write();
@@ -510,10 +510,11 @@
510 /* This process can start doing backoffice work immediately */
511 x.idCurrent = idSelf;
512 x.tmCurrent = tmNow + BKOFCE_LEASE_TIME;
513 x.idNext = 0;
514 x.tmNext = 0;
 
515 backofficeWriteLease(&x);
516 db_end_transaction(0);
517 backofficeTrace("/***** Begin Backoffice Processing %d *****/\n",
518 GETPID());
519 backoffice_work();
@@ -543,13 +544,16 @@
543 db_end_transaction(0);
544 break;
545 }
546 }else{
547 if( (sqlite3_uint64)(lastWarning+warningDelay) < tmNow ){
548 fossil_warning(
 
 
549 "backoffice process %lld still running after %d seconds",
550 x.idCurrent, (int)(BKOFCE_LEASE_TIME + tmNow - x.tmCurrent));
 
551 lastWarning = tmNow;
552 warningDelay *= 2;
553 }
554 if( backofficeSleep(1000) ){
555 /* The sleep was interrupted by a signal from another thread. */
@@ -642,14 +646,17 @@
642 backofficeBlob = &log;
643 blob_appendf(&log, "%s %s", db_text(0, "SELECT datetime('now')"), zName);
644 }
645
646 /* Here is where the actual work of the backoffice happens */
 
647 nThis = alert_backoffice(0);
648 if( nThis ){ backoffice_log("%d alerts", nThis); nTotal += nThis; }
 
649 nThis = hook_backoffice();
650 if( nThis ){ backoffice_log("%d hooks", nThis); nTotal += nThis; }
 
651
652 /* Close the log */
653 if( backofficeFILE ){
654 if( nTotal || backofficeLogDetail ){
655 if( nTotal==0 ) backoffice_log("no-op");
656
--- src/backoffice.c
+++ src/backoffice.c
@@ -38,11 +38,11 @@
38 ** process table, doing nothing on rarely accessed repositories, and
39 ** if the Fossil binary is updated on a system, the backoffice processes
40 ** will restart using the new binary automatically.
41 **
42 ** At any point in time there should be at most two backoffice processes.
43 ** There is a main process that is doing the actual work, and there is
44 ** a second stand-by process that is waiting for the main process to finish
45 ** and that will become the main process after a delay.
46 **
47 ** After any successful web page reply, the backoffice_check_if_needed()
48 ** routine is called. That routine checks to see if both one or both of
@@ -53,11 +53,11 @@
53 ** backoffice_run_if_needed() routine is called. If the prior call
54 ** to backoffice_check_if_needed() indicated that backoffice processing
55 ** might be required, the run_if_needed() attempts to kick off a backoffice
56 ** process.
57 **
58 ** All work performed by the backoffice is in the backoffice_work()
59 ** routine.
60 */
61 #if defined(_WIN32)
62 # if defined(_WIN32_WINNT)
63 # undef _WIN32_WINNT
@@ -485,11 +485,11 @@
485 int warningDelay = 30;
486 static int once = 0;
487
488 if( sqlite3_db_readonly(g.db, 0) ) return;
489 if( db_is_protected(PROTECT_READONLY) ) return;
490 g.zPhase = "backoffice-pending";
491 backoffice_error_check_one(&once);
492 idSelf = backofficeProcessId();
493 while(1){
494 tmNow = time(0);
495 db_begin_write();
@@ -510,10 +510,11 @@
510 /* This process can start doing backoffice work immediately */
511 x.idCurrent = idSelf;
512 x.tmCurrent = tmNow + BKOFCE_LEASE_TIME;
513 x.idNext = 0;
514 x.tmNext = 0;
515 g.zPhase = "backoffice-work";
516 backofficeWriteLease(&x);
517 db_end_transaction(0);
518 backofficeTrace("/***** Begin Backoffice Processing %d *****/\n",
519 GETPID());
520 backoffice_work();
@@ -543,13 +544,16 @@
544 db_end_transaction(0);
545 break;
546 }
547 }else{
548 if( (sqlite3_uint64)(lastWarning+warningDelay) < tmNow ){
549 sqlite3_int64 runningFor = BKOFCE_LEASE_TIME + tmNow - x.tmCurrent;
550 if( warningDelay>=240 && runningFor<1800 ){
551 fossil_warning(
552 "backoffice process %lld still running after %d seconds",
553 x.idCurrent, runningFor);
554 }
555 lastWarning = tmNow;
556 warningDelay *= 2;
557 }
558 if( backofficeSleep(1000) ){
559 /* The sleep was interrupted by a signal from another thread. */
@@ -642,14 +646,17 @@
646 backofficeBlob = &log;
647 blob_appendf(&log, "%s %s", db_text(0, "SELECT datetime('now')"), zName);
648 }
649
650 /* Here is where the actual work of the backoffice happens */
651 g.zPhase = "backoffice-alerts";
652 nThis = alert_backoffice(0);
653 if( nThis ){ backoffice_log("%d alerts", nThis); nTotal += nThis; }
654 g.zPhase = "backoffice-hooks";
655 nThis = hook_backoffice();
656 if( nThis ){ backoffice_log("%d hooks", nThis); nTotal += nThis; }
657 g.zPhase = "backoffice-close";
658
659 /* Close the log */
660 if( backofficeFILE ){
661 if( nTotal || backofficeLogDetail ){
662 if( nTotal==0 ) backoffice_log("no-op");
663
+8 -7
--- src/branch.c
+++ src/branch.c
@@ -657,25 +657,25 @@
657657
**
658658
** > fossil branch new BRANCH-NAME BASIS ?OPTIONS?
659659
**
660660
** Create a new branch BRANCH-NAME off of check-in BASIS.
661661
**
662
+** This command is available for people who want to create a branch
663
+** in advance. But the use of this command is discouraged. The
664
+** preferred idiom in Fossil is to create new branches at the point
665
+** of need, using the "--branch NAME" option to the "fossil commit"
666
+** command.
667
+**
662668
** Options:
663669
** --private Branch is private (i.e., remains local)
664670
** --bgcolor COLOR Use COLOR instead of automatic background
665671
** --nosign Do not sign the manifest for the check-in
666672
** that creates this branch
667673
** --nosync Do not auto-sync prior to creating the branch
668674
** --date-override DATE DATE to use instead of 'now'
669675
** --user-override USER USER to use instead of the current default
670676
**
671
-** DATE may be "now" or "YYYY-MM-DDTHH:MM:SS.SSS". If in
672
-** year-month-day form, it may be truncated, the "T" may be
673
-** replaced by a space, and it may also name a timezone offset
674
-** from UTC as "-HH:MM" (westward) or "+HH:MM" (eastward).
675
-** Either no timezone suffix or "Z" means UTC.
676
-**
677677
** Options:
678678
** -R|--repository REPO Run commands on repository REPO
679679
*/
680680
void branch_cmd(void){
681681
int n;
@@ -870,11 +870,12 @@
870870
const char *zLastCkin = db_column_text(&q, 5);
871871
const char *zBgClr = db_column_text(&q, 6);
872872
char *zAge = human_readable_age(rNow - rMtime);
873873
sqlite3_int64 iMtime = (sqlite3_int64)(rMtime*86400.0);
874874
if( zMergeTo && zMergeTo[0]==0 ) zMergeTo = 0;
875
- if( zBgClr == 0 ){
875
+ if( zBgClr ) zBgClr = reasonable_bg_color(zBgClr, 0);
876
+ if( zBgClr==0 ){
876877
if( zBranch==0 || strcmp(zBranch,"trunk")==0 ){
877878
zBgClr = 0;
878879
}else{
879880
zBgClr = hash_color(zBranch);
880881
}
881882
--- src/branch.c
+++ src/branch.c
@@ -657,25 +657,25 @@
657 **
658 ** > fossil branch new BRANCH-NAME BASIS ?OPTIONS?
659 **
660 ** Create a new branch BRANCH-NAME off of check-in BASIS.
661 **
 
 
 
 
 
 
662 ** Options:
663 ** --private Branch is private (i.e., remains local)
664 ** --bgcolor COLOR Use COLOR instead of automatic background
665 ** --nosign Do not sign the manifest for the check-in
666 ** that creates this branch
667 ** --nosync Do not auto-sync prior to creating the branch
668 ** --date-override DATE DATE to use instead of 'now'
669 ** --user-override USER USER to use instead of the current default
670 **
671 ** DATE may be "now" or "YYYY-MM-DDTHH:MM:SS.SSS". If in
672 ** year-month-day form, it may be truncated, the "T" may be
673 ** replaced by a space, and it may also name a timezone offset
674 ** from UTC as "-HH:MM" (westward) or "+HH:MM" (eastward).
675 ** Either no timezone suffix or "Z" means UTC.
676 **
677 ** Options:
678 ** -R|--repository REPO Run commands on repository REPO
679 */
680 void branch_cmd(void){
681 int n;
@@ -870,11 +870,12 @@
870 const char *zLastCkin = db_column_text(&q, 5);
871 const char *zBgClr = db_column_text(&q, 6);
872 char *zAge = human_readable_age(rNow - rMtime);
873 sqlite3_int64 iMtime = (sqlite3_int64)(rMtime*86400.0);
874 if( zMergeTo && zMergeTo[0]==0 ) zMergeTo = 0;
875 if( zBgClr == 0 ){
 
876 if( zBranch==0 || strcmp(zBranch,"trunk")==0 ){
877 zBgClr = 0;
878 }else{
879 zBgClr = hash_color(zBranch);
880 }
881
--- src/branch.c
+++ src/branch.c
@@ -657,25 +657,25 @@
657 **
658 ** > fossil branch new BRANCH-NAME BASIS ?OPTIONS?
659 **
660 ** Create a new branch BRANCH-NAME off of check-in BASIS.
661 **
662 ** This command is available for people who want to create a branch
663 ** in advance. But the use of this command is discouraged. The
664 ** preferred idiom in Fossil is to create new branches at the point
665 ** of need, using the "--branch NAME" option to the "fossil commit"
666 ** command.
667 **
668 ** Options:
669 ** --private Branch is private (i.e., remains local)
670 ** --bgcolor COLOR Use COLOR instead of automatic background
671 ** --nosign Do not sign the manifest for the check-in
672 ** that creates this branch
673 ** --nosync Do not auto-sync prior to creating the branch
674 ** --date-override DATE DATE to use instead of 'now'
675 ** --user-override USER USER to use instead of the current default
676 **
 
 
 
 
 
 
677 ** Options:
678 ** -R|--repository REPO Run commands on repository REPO
679 */
680 void branch_cmd(void){
681 int n;
@@ -870,11 +870,12 @@
870 const char *zLastCkin = db_column_text(&q, 5);
871 const char *zBgClr = db_column_text(&q, 6);
872 char *zAge = human_readable_age(rNow - rMtime);
873 sqlite3_int64 iMtime = (sqlite3_int64)(rMtime*86400.0);
874 if( zMergeTo && zMergeTo[0]==0 ) zMergeTo = 0;
875 if( zBgClr ) zBgClr = reasonable_bg_color(zBgClr, 0);
876 if( zBgClr==0 ){
877 if( zBranch==0 || strcmp(zBranch,"trunk")==0 ){
878 zBgClr = 0;
879 }else{
880 zBgClr = hash_color(zBranch);
881 }
882
+2 -2
--- src/browse.c
+++ src/browse.c
@@ -205,11 +205,11 @@
205205
linkTip = rid != symbolic_name_to_rid("tip", "ci");
206206
zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid);
207207
isSymbolicCI = (sqlite3_strnicmp(zUuid, zCI, strlen(zCI))!=0);
208208
isBranchCI = branch_includes_uuid(zCI, zUuid);
209209
if( bDocDir ) zCI = mprintf("%S", zUuid);
210
- Th_Store("current_checkin", zCI);
210
+ Th_StoreUnsafe("current_checkin", zCI);
211211
}else{
212212
zCI = 0;
213213
}
214214
}
215215
@@ -771,11 +771,11 @@
771771
rNow = db_double(0.0, "SELECT mtime FROM event WHERE objid=%d", rid);
772772
zNow = db_text("", "SELECT datetime(mtime,toLocal())"
773773
" FROM event WHERE objid=%d", rid);
774774
isSymbolicCI = (sqlite3_strnicmp(zUuid, zCI, strlen(zCI)) != 0);
775775
isBranchCI = branch_includes_uuid(zCI, zUuid);
776
- Th_Store("current_checkin", zCI);
776
+ Th_StoreUnsafe("current_checkin", zCI);
777777
}else{
778778
zCI = 0;
779779
}
780780
}
781781
if( zCI==0 ){
782782
--- src/browse.c
+++ src/browse.c
@@ -205,11 +205,11 @@
205 linkTip = rid != symbolic_name_to_rid("tip", "ci");
206 zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid);
207 isSymbolicCI = (sqlite3_strnicmp(zUuid, zCI, strlen(zCI))!=0);
208 isBranchCI = branch_includes_uuid(zCI, zUuid);
209 if( bDocDir ) zCI = mprintf("%S", zUuid);
210 Th_Store("current_checkin", zCI);
211 }else{
212 zCI = 0;
213 }
214 }
215
@@ -771,11 +771,11 @@
771 rNow = db_double(0.0, "SELECT mtime FROM event WHERE objid=%d", rid);
772 zNow = db_text("", "SELECT datetime(mtime,toLocal())"
773 " FROM event WHERE objid=%d", rid);
774 isSymbolicCI = (sqlite3_strnicmp(zUuid, zCI, strlen(zCI)) != 0);
775 isBranchCI = branch_includes_uuid(zCI, zUuid);
776 Th_Store("current_checkin", zCI);
777 }else{
778 zCI = 0;
779 }
780 }
781 if( zCI==0 ){
782
--- src/browse.c
+++ src/browse.c
@@ -205,11 +205,11 @@
205 linkTip = rid != symbolic_name_to_rid("tip", "ci");
206 zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid);
207 isSymbolicCI = (sqlite3_strnicmp(zUuid, zCI, strlen(zCI))!=0);
208 isBranchCI = branch_includes_uuid(zCI, zUuid);
209 if( bDocDir ) zCI = mprintf("%S", zUuid);
210 Th_StoreUnsafe("current_checkin", zCI);
211 }else{
212 zCI = 0;
213 }
214 }
215
@@ -771,11 +771,11 @@
771 rNow = db_double(0.0, "SELECT mtime FROM event WHERE objid=%d", rid);
772 zNow = db_text("", "SELECT datetime(mtime,toLocal())"
773 " FROM event WHERE objid=%d", rid);
774 isSymbolicCI = (sqlite3_strnicmp(zUuid, zCI, strlen(zCI)) != 0);
775 isBranchCI = branch_includes_uuid(zCI, zUuid);
776 Th_StoreUnsafe("current_checkin", zCI);
777 }else{
778 zCI = 0;
779 }
780 }
781 if( zCI==0 ){
782
+54 -28
--- src/cache.c
+++ src/cache.c
@@ -399,62 +399,88 @@
399399
** WEBPAGE: cachestat
400400
**
401401
** Show information about the webpage cache. Requires Setup privilege.
402402
*/
403403
void cache_page(void){
404
- sqlite3 *db;
404
+ sqlite3 *db = 0;
405405
sqlite3_stmt *pStmt;
406
+ int doInit;
407
+ char *zDbName = cacheName();
408
+ int nEntry = 0;
409
+ int mxEntry = 0;
406410
char zBuf[100];
407411
408412
login_check_credentials();
409413
if( !g.perm.Setup ){ login_needed(0); return; }
410414
style_set_current_feature("cache");
411415
style_header("Web Cache Status");
412
- db = cacheOpen(0);
413
- if( db==0 ){
414
- @ The web-page cache is disabled for this repository
415
- }else{
416
- char *zDbName = cacheName();
416
+ style_submenu_element("Refresh","%R/cachestat");
417
+ doInit = P("init")!=0 && cgi_csrf_safe(2);
418
+ db = cacheOpen(doInit);
419
+ if( db!=0 ){
420
+ if( P("clear")!=0 && cgi_csrf_safe(2) ){
421
+ sqlite3_exec(db, "DELETE FROM cache; DELETE FROM blob; VACUUM;",0,0,0);
422
+ }
417423
cache_register_sizename(db);
418424
pStmt = cacheStmt(db,
419425
"SELECT key, sz, nRef, datetime(tm,'unixepoch')"
420426
" FROM cache"
421427
" ORDER BY (tm + 3600*min(nRef,48)) DESC"
422428
);
423429
if( pStmt ){
424
- @ <ol>
425430
while( sqlite3_step(pStmt)==SQLITE_ROW ){
426431
const unsigned char *zName = sqlite3_column_text(pStmt,0);
427432
char *zHash = cache_hash_of_key((const char*)zName);
433
+ if( nEntry==0 ){
434
+ @ <h2>Current Cache Entries:</h2>
435
+ @ <ol>
436
+ }
428437
@ <li><p>%z(href("%R/cacheget?key=%T",zName))%h(zName)</a><br>
429
- @ size: %,lld(sqlite3_column_int64(pStmt,1))
430
- @ hit-count: %d(sqlite3_column_int(pStmt,2))
431
- @ last-access: %s(sqlite3_column_text(pStmt,3)) \
438
+ @ size: %,lld(sqlite3_column_int64(pStmt,1)),
439
+ @ hit-count: %d(sqlite3_column_int(pStmt,2)),
440
+ @ last-access: %s(sqlite3_column_text(pStmt,3))Z \
432441
if( zHash ){
433
- @ %z(href("%R/timeline?c=%S",zHash))check-in</a>\
442
+ @ &rarr; %z(href("%R/timeline?c=%S",zHash))checkin info</a>\
434443
fossil_free(zHash);
435444
}
436445
@ </p></li>
437
-
446
+ nEntry++;
438447
}
439448
sqlite3_finalize(pStmt);
440
- @ </ol>
441
- }
442
- zDbName = cacheName();
443
- bigSizeName(sizeof(zBuf), zBuf, file_size(zDbName, ExtFILE));
444
- @ <p>
445
- @ cache-file name: %h(zDbName)<br>
446
- @ cache-file size: %s(zBuf)<br>
447
- @ max-cache-entry: %d(db_get_int("max-cache-entry",10))
448
- @ </p>
449
- @ <p>
450
- @ Use the "<a href="%R/help?cmd=cache">fossil cache</a>" command
451
- @ on the command-line to create and configure the web-cache.
452
- @ </p>
453
- fossil_free(zDbName);
454
- sqlite3_close(db);
455
- }
449
+ if( nEntry ){
450
+ @ </ol>
451
+ }
452
+ }
453
+ }
454
+ @ <h2>About The Web-Cache</h2>
455
+ @ <p>
456
+ @ The web-cache is a separate database file that holds cached copies
457
+ @ tarballs, ZIP archives, and other pages that are expensive to compute
458
+ @ and are likely to be reused.
459
+ @ <form method="post">
460
+ login_insert_csrf_secret();
461
+ @ <ul>
462
+ if( db==0 ){
463
+ @ <li> Web-cache is currently disabled.
464
+ @ <input type="submit" name="init" value="Enable">
465
+ }else{
466
+ bigSizeName(sizeof(zBuf), zBuf, file_size(zDbName, ExtFILE));
467
+ mxEntry = db_get_int("max-cache-entry",10);
468
+ @ <li> Filename of the cache database: <b>%h(zDbName)</b>
469
+ @ <li> Size of the cache database: %s(zBuf)
470
+ @ <li> Maximum number of entries: %d(mxEntry)
471
+ @ <li> Number of cache entries used: %d(nEntry)
472
+ @ <li> Change the max-cache-entry setting on the
473
+ @ <a href="%R/setup_settings">Settings</a> page to adjust the
474
+ @ maximum number of entries in the cache.
475
+ @ <li><input type="submit" name="clear" value="Clear the cache">
476
+ @ <li> Disable the cache by manually deleting the cache database file.
477
+ }
478
+ @ </ul>
479
+ @ </form>
480
+ fossil_free(zDbName);
481
+ if( db ) sqlite3_close(db);
456482
style_finish_page();
457483
}
458484
459485
/*
460486
** WEBPAGE: cacheget
461487
--- src/cache.c
+++ src/cache.c
@@ -399,62 +399,88 @@
399 ** WEBPAGE: cachestat
400 **
401 ** Show information about the webpage cache. Requires Setup privilege.
402 */
403 void cache_page(void){
404 sqlite3 *db;
405 sqlite3_stmt *pStmt;
 
 
 
 
406 char zBuf[100];
407
408 login_check_credentials();
409 if( !g.perm.Setup ){ login_needed(0); return; }
410 style_set_current_feature("cache");
411 style_header("Web Cache Status");
412 db = cacheOpen(0);
413 if( db==0 ){
414 @ The web-page cache is disabled for this repository
415 }else{
416 char *zDbName = cacheName();
 
 
417 cache_register_sizename(db);
418 pStmt = cacheStmt(db,
419 "SELECT key, sz, nRef, datetime(tm,'unixepoch')"
420 " FROM cache"
421 " ORDER BY (tm + 3600*min(nRef,48)) DESC"
422 );
423 if( pStmt ){
424 @ <ol>
425 while( sqlite3_step(pStmt)==SQLITE_ROW ){
426 const unsigned char *zName = sqlite3_column_text(pStmt,0);
427 char *zHash = cache_hash_of_key((const char*)zName);
 
 
 
 
428 @ <li><p>%z(href("%R/cacheget?key=%T",zName))%h(zName)</a><br>
429 @ size: %,lld(sqlite3_column_int64(pStmt,1))
430 @ hit-count: %d(sqlite3_column_int(pStmt,2))
431 @ last-access: %s(sqlite3_column_text(pStmt,3)) \
432 if( zHash ){
433 @ %z(href("%R/timeline?c=%S",zHash))check-in</a>\
434 fossil_free(zHash);
435 }
436 @ </p></li>
437
438 }
439 sqlite3_finalize(pStmt);
440 @ </ol>
441 }
442 zDbName = cacheName();
443 bigSizeName(sizeof(zBuf), zBuf, file_size(zDbName, ExtFILE));
444 @ <p>
445 @ cache-file name: %h(zDbName)<br>
446 @ cache-file size: %s(zBuf)<br>
447 @ max-cache-entry: %d(db_get_int("max-cache-entry",10))
448 @ </p>
449 @ <p>
450 @ Use the "<a href="%R/help?cmd=cache">fossil cache</a>" command
451 @ on the command-line to create and configure the web-cache.
452 @ </p>
453 fossil_free(zDbName);
454 sqlite3_close(db);
455 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
456 style_finish_page();
457 }
458
459 /*
460 ** WEBPAGE: cacheget
461
--- src/cache.c
+++ src/cache.c
@@ -399,62 +399,88 @@
399 ** WEBPAGE: cachestat
400 **
401 ** Show information about the webpage cache. Requires Setup privilege.
402 */
403 void cache_page(void){
404 sqlite3 *db = 0;
405 sqlite3_stmt *pStmt;
406 int doInit;
407 char *zDbName = cacheName();
408 int nEntry = 0;
409 int mxEntry = 0;
410 char zBuf[100];
411
412 login_check_credentials();
413 if( !g.perm.Setup ){ login_needed(0); return; }
414 style_set_current_feature("cache");
415 style_header("Web Cache Status");
416 style_submenu_element("Refresh","%R/cachestat");
417 doInit = P("init")!=0 && cgi_csrf_safe(2);
418 db = cacheOpen(doInit);
419 if( db!=0 ){
420 if( P("clear")!=0 && cgi_csrf_safe(2) ){
421 sqlite3_exec(db, "DELETE FROM cache; DELETE FROM blob; VACUUM;",0,0,0);
422 }
423 cache_register_sizename(db);
424 pStmt = cacheStmt(db,
425 "SELECT key, sz, nRef, datetime(tm,'unixepoch')"
426 " FROM cache"
427 " ORDER BY (tm + 3600*min(nRef,48)) DESC"
428 );
429 if( pStmt ){
 
430 while( sqlite3_step(pStmt)==SQLITE_ROW ){
431 const unsigned char *zName = sqlite3_column_text(pStmt,0);
432 char *zHash = cache_hash_of_key((const char*)zName);
433 if( nEntry==0 ){
434 @ <h2>Current Cache Entries:</h2>
435 @ <ol>
436 }
437 @ <li><p>%z(href("%R/cacheget?key=%T",zName))%h(zName)</a><br>
438 @ size: %,lld(sqlite3_column_int64(pStmt,1)),
439 @ hit-count: %d(sqlite3_column_int(pStmt,2)),
440 @ last-access: %s(sqlite3_column_text(pStmt,3))Z \
441 if( zHash ){
442 @ &rarr; %z(href("%R/timeline?c=%S",zHash))checkin info</a>\
443 fossil_free(zHash);
444 }
445 @ </p></li>
446 nEntry++;
447 }
448 sqlite3_finalize(pStmt);
449 if( nEntry ){
450 @ </ol>
451 }
452 }
453 }
454 @ <h2>About The Web-Cache</h2>
455 @ <p>
456 @ The web-cache is a separate database file that holds cached copies
457 @ tarballs, ZIP archives, and other pages that are expensive to compute
458 @ and are likely to be reused.
459 @ <form method="post">
460 login_insert_csrf_secret();
461 @ <ul>
462 if( db==0 ){
463 @ <li> Web-cache is currently disabled.
464 @ <input type="submit" name="init" value="Enable">
465 }else{
466 bigSizeName(sizeof(zBuf), zBuf, file_size(zDbName, ExtFILE));
467 mxEntry = db_get_int("max-cache-entry",10);
468 @ <li> Filename of the cache database: <b>%h(zDbName)</b>
469 @ <li> Size of the cache database: %s(zBuf)
470 @ <li> Maximum number of entries: %d(mxEntry)
471 @ <li> Number of cache entries used: %d(nEntry)
472 @ <li> Change the max-cache-entry setting on the
473 @ <a href="%R/setup_settings">Settings</a> page to adjust the
474 @ maximum number of entries in the cache.
475 @ <li><input type="submit" name="clear" value="Clear the cache">
476 @ <li> Disable the cache by manually deleting the cache database file.
477 }
478 @ </ul>
479 @ </form>
480 fossil_free(zDbName);
481 if( db ) sqlite3_close(db);
482 style_finish_page();
483 }
484
485 /*
486 ** WEBPAGE: cacheget
487
+289 -165
--- src/cgi.c
+++ src/cgi.c
@@ -72,10 +72,11 @@
7272
# include <ws2tcpip.h>
7373
#else
7474
# include <sys/socket.h>
7575
# include <sys/un.h>
7676
# include <netinet/in.h>
77
+# include <netdb.h>
7778
# include <arpa/inet.h>
7879
# include <sys/times.h>
7980
# include <sys/time.h>
8081
# include <sys/wait.h>
8182
# include <sys/select.h>
@@ -103,12 +104,12 @@
103104
#define PT(x) cgi_parameter_trimmed((x),0)
104105
#define PDT(x,y) cgi_parameter_trimmed((x),(y))
105106
#define PB(x) cgi_parameter_boolean(x)
106107
#define PCK(x) cgi_parameter_checked(x,1)
107108
#define PIF(x,y) cgi_parameter_checked(x,y)
108
-#define P_NoBot(x) cgi_parameter_nosql((x),0)
109
-#define PD_NoBot(x,y) cgi_parameter_nosql((x),(y))
109
+#define P_NoBot(x) cgi_parameter_no_attack((x),0)
110
+#define PD_NoBot(x,y) cgi_parameter_no_attack((x),(y))
110111
111112
/*
112113
** Shortcut for the cgi_printf() routine. Instead of using the
113114
**
114115
** @ ...
@@ -637,10 +638,13 @@
637638
cgi_set_status(iStat, zStat);
638639
free(zLocation);
639640
cgi_reply();
640641
fossil_exit(0);
641642
}
643
+NORETURN void cgi_redirect_perm(const char *zURL){
644
+ cgi_redirect_with_status(zURL, 301, "Moved Permanently");
645
+}
642646
NORETURN void cgi_redirect(const char *zURL){
643647
cgi_redirect_with_status(zURL, 302, "Moved Temporarily");
644648
}
645649
NORETURN void cgi_redirect_with_method(const char *zURL){
646650
cgi_redirect_with_status(zURL, 307, "Temporary Redirect");
@@ -1620,37 +1624,39 @@
16201624
fossil_errorlog("Xpossible hack attempt - 418 response on \"%s\"", zName);
16211625
exit(0);
16221626
}
16231627
16241628
/*
1625
-** If looks_like_sql_injection() returns true for the given string, calls
1629
+** If looks_like_attack() returns true for the given string, call
16261630
** cgi_begone_spider() and does not return, else this function has no
16271631
** side effects. The range of checks performed by this function may
16281632
** be extended in the future.
16291633
**
16301634
** Checks are omitted for any logged-in user.
16311635
**
1632
-** This is NOT a defense against SQL injection. Fossil should easily be
1633
-** proof against SQL injection without this routine. Rather, this is an
1634
-** attempt to avoid denial-of-service caused by persistent spiders that hammer
1635
-** the server with dozens or hundreds of SQL injection attempts per second
1636
-** against pages (such as /vdiff) that are expensive to compute. In other
1636
+** This is the primary defense against attack. Fossil should easily be
1637
+** proof against SQL injection and XSS attacks even without without this
1638
+** routine. Rather, this is an attempt to avoid denial-of-service caused
1639
+** by persistent spiders that hammer the server with dozens or hundreds of
1640
+** probes per seconds as they look for vulnerabilities. In other
16371641
** words, this is an effort to reduce the CPU load imposed by malicious
1638
-** spiders. It is not an effect defense against SQL injection vulnerabilities.
1642
+** spiders. Though those routine might help make attacks harder, it is
1643
+** not itself an impenetrably barrier against attack and should not be
1644
+** relied upon as the only defense.
16391645
*/
16401646
void cgi_value_spider_check(const char *zTxt, const char *zName){
1641
- if( g.zLogin==0 && looks_like_sql_injection(zTxt) ){
1647
+ if( g.zLogin==0 && looks_like_attack(zTxt) ){
16421648
cgi_begone_spider(zName);
16431649
}
16441650
}
16451651
16461652
/*
16471653
** A variant of cgi_parameter() with the same semantics except that if
16481654
** cgi_parameter(zName,zDefault) returns a value other than zDefault
16491655
** then it passes that value to cgi_value_spider_check().
16501656
*/
1651
-const char *cgi_parameter_nosql(const char *zName, const char *zDefault){
1657
+const char *cgi_parameter_no_attack(const char *zName, const char *zDefault){
16521658
const char *zTxt = cgi_parameter(zName, zDefault);
16531659
16541660
if( zTxt!=zDefault ){
16551661
cgi_value_spider_check(zTxt, zName);
16561662
}
@@ -2070,34 +2076,40 @@
20702076
}
20712077
if( zLeftOver ){ *zLeftOver = zInput; }
20722078
return zResult;
20732079
}
20742080
2081
+/*
2082
+** All possible forms of an IP address. Needed to work around GCC strict
2083
+** aliasing rules.
2084
+*/
2085
+typedef union {
2086
+ struct sockaddr sa; /* Abstract superclass */
2087
+ struct sockaddr_in sa4; /* IPv4 */
2088
+ struct sockaddr_in6 sa6; /* IPv6 */
2089
+ struct sockaddr_storage sas; /* Should be the maximum of the above 3 */
2090
+} address;
2091
+
20752092
/*
20762093
** Determine the IP address on the other side of a connection.
20772094
** Return a pointer to a string. Or return 0 if unable.
20782095
**
20792096
** The string is held in a static buffer that is overwritten on
20802097
** each call.
20812098
*/
20822099
char *cgi_remote_ip(int fd){
2083
-#if 0
2084
- static char zIp[100];
2085
- struct sockaddr_in6 addr;
2086
- socklen_t sz = sizeof(addr);
2087
- if( getpeername(fd, &addr, &sz) ) return 0;
2088
- zIp[0] = 0;
2089
- if( inet_ntop(AF_INET6, &addr, zIp, sizeof(zIp))==0 ){
2100
+ address remoteAddr;
2101
+ socklen_t size = sizeof(remoteAddr);
2102
+ static char zHost[NI_MAXHOST];
2103
+ if( getpeername(0, &remoteAddr.sa, &size) ){
2104
+ return 0;
2105
+ }
2106
+ if( getnameinfo(&remoteAddr.sa, size, zHost, sizeof(zHost), 0, 0,
2107
+ NI_NUMERICHOST) ){
20902108
return 0;
20912109
}
2092
- return zIp;
2093
-#else
2094
- struct sockaddr_in remoteName;
2095
- socklen_t size = sizeof(struct sockaddr_in);
2096
- if( getpeername(fd, (struct sockaddr*)&remoteName, &size) ) return 0;
2097
- return inet_ntoa(remoteName.sin_addr);
2098
-#endif
2110
+ return zHost;
20992111
}
21002112
21012113
/*
21022114
** This routine handles a single HTTP request which is coming in on
21032115
** g.httpIn and which replies on g.httpOut
@@ -2486,11 +2498,10 @@
24862498
fossil_free(zToFree);
24872499
fgetc(g.httpIn); /* Read past the "," separating header from content */
24882500
cgi_init();
24892501
}
24902502
2491
-
24922503
#if INTERFACE
24932504
/*
24942505
** Bitmap values for the flags parameter to cgi_http_server().
24952506
*/
24962507
#define HTTP_SERVER_LOCALHOST 0x0001 /* Bind to 127.0.0.1 only */
@@ -2529,122 +2540,222 @@
25292540
){
25302541
#if defined(_WIN32)
25312542
/* Use win32_http_server() instead */
25322543
fossil_exit(1);
25332544
#else
2534
- int listener = -1; /* The server socket */
2535
- int connection; /* A socket for each individual connection */
2545
+ int listen4 = -1; /* Main socket; IPv4 or unix-domain */
2546
+ int listen6 = -1; /* Aux socket for corresponding IPv6 */
2547
+ int mxListen = -1; /* Maximum of listen4 and listen6 */
2548
+ int connection; /* An incoming connection */
25362549
int nRequest = 0; /* Number of requests handled so far */
25372550
fd_set readfds; /* Set of file descriptors for select() */
25382551
socklen_t lenaddr; /* Length of the inaddr structure */
25392552
int child; /* PID of the child process */
25402553
int nchildren = 0; /* Number of child processes */
25412554
struct timeval delay; /* How long to wait inside select() */
2542
- struct sockaddr_in inaddr; /* The socket address */
2555
+ struct sockaddr_in6 inaddr6; /* Address for IPv6 */
2556
+ struct sockaddr_in inaddr4; /* Address for IPv4 */
25432557
struct sockaddr_un uxaddr; /* The address for unix-domain sockets */
25442558
int opt = 1; /* setsockopt flag */
25452559
int rc; /* Result code from system calls */
25462560
int iPort = mnPort; /* Port to try to use */
2547
-
2548
- while( iPort<=mxPort ){
2549
- if( flags & HTTP_SERVER_UNIXSOCKET ){
2550
- /* Initialize a Unix socket named g.zSockName */
2551
- assert( g.zSockName!=0 );
2552
- memset(&uxaddr, 0, sizeof(uxaddr));
2553
- if( strlen(g.zSockName)>sizeof(uxaddr.sun_path) ){
2554
- fossil_fatal("name of unix socket too big: %s\nmax size: %d\n",
2555
- g.zSockName, (int)sizeof(uxaddr.sun_path));
2556
- }
2557
- if( file_isdir(g.zSockName, ExtFILE)!=0 ){
2558
- if( !file_issocket(g.zSockName) ){
2559
- fossil_fatal("cannot name socket \"%s\" because another object"
2560
- " with that name already exists", g.zSockName);
2561
- }else{
2562
- unlink(g.zSockName);
2563
- }
2564
- }
2565
- uxaddr.sun_family = AF_UNIX;
2566
- strncpy(uxaddr.sun_path, g.zSockName, sizeof(uxaddr.sun_path)-1);
2567
- listener = socket(AF_UNIX, SOCK_STREAM, 0);
2568
- if( listener<0 ){
2569
- fossil_fatal("unable to create a unix socket named %s",
2570
- g.zSockName);
2571
- }
2572
- /* Set the access permission for the new socket. Default to 0660.
2573
- ** But use an alternative specified by --socket-mode if available.
2574
- ** Do this before bind() to avoid a race condition. */
2575
- if( g.zSockMode ){
2576
- file_set_mode(g.zSockName, listener, g.zSockMode, 0);
2577
- }else{
2578
- file_set_mode(g.zSockName, listener, "0660", 1);
2579
- }
2580
- }else{
2581
- /* Initialize a TCP/IP socket on port iPort */
2582
- memset(&inaddr, 0, sizeof(inaddr));
2583
- inaddr.sin_family = AF_INET;
2584
- if( zIpAddr ){
2585
- inaddr.sin_addr.s_addr = inet_addr(zIpAddr);
2586
- if( inaddr.sin_addr.s_addr == INADDR_NONE ){
2587
- fossil_fatal("not a valid IP address: %s", zIpAddr);
2588
- }
2589
- }else if( flags & HTTP_SERVER_LOCALHOST ){
2590
- inaddr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
2591
- }else{
2592
- inaddr.sin_addr.s_addr = htonl(INADDR_ANY);
2593
- }
2594
- inaddr.sin_port = htons(iPort);
2595
- listener = socket(AF_INET, SOCK_STREAM, 0);
2596
- if( listener<0 ){
2597
- iPort++;
2598
- continue;
2599
- }
2600
- }
2601
-
2602
- /* if we can't terminate nicely, at least allow the socket to be reused */
2603
- setsockopt(listener,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
2604
-
2605
- if( flags & HTTP_SERVER_UNIXSOCKET ){
2606
- rc = bind(listener, (struct sockaddr*)&uxaddr, sizeof(uxaddr));
2607
- /* Set the owner of the socket if requested by --socket-owner. This
2608
- ** must wait until after bind(), after the filesystem object has been
2609
- ** created. See https://lkml.org/lkml/2004/11/1/84 and
2610
- ** https://fossil-scm.org/forum/forumpost/7517680ef9684c57 */
2611
- if( g.zSockOwner ){
2612
- file_set_owner(g.zSockName, listener, g.zSockOwner);
2613
- }
2614
- }else{
2615
- rc = bind(listener, (struct sockaddr*)&inaddr, sizeof(inaddr));
2616
- }
2617
- if( rc<0 ){
2618
- close(listener);
2619
- iPort++;
2620
- continue;
2621
- }
2622
- break;
2623
- }
2624
- if( iPort>mxPort ){
2625
- if( flags & HTTP_SERVER_UNIXSOCKET ){
2626
- fossil_fatal("unable to listen on unix socket %s", zIpAddr);
2627
- }else if( mnPort==mxPort ){
2628
- fossil_fatal("unable to open listening socket on port %d", mnPort);
2629
- }else{
2630
- fossil_fatal("unable to open listening socket on any"
2631
- " port in the range %d..%d", mnPort, mxPort);
2632
- }
2633
- }
2634
- if( iPort>mxPort ) return 1;
2635
- listen(listener,10);
2636
- if( flags & HTTP_SERVER_UNIXSOCKET ){
2637
- fossil_print("Listening for %s requests on unix socket %s\n",
2638
- (flags & HTTP_SERVER_SCGI)!=0 ? "SCGI" :
2639
- g.httpUseSSL?"TLS-encrypted HTTPS":"HTTP", g.zSockName);
2640
- }else{
2641
- fossil_print("Listening for %s requests on TCP port %d\n",
2642
- (flags & HTTP_SERVER_SCGI)!=0 ? "SCGI" :
2643
- g.httpUseSSL?"TLS-encrypted HTTPS":"HTTP", iPort);
2644
- }
2645
- fflush(stdout);
2561
+ const char *zRequestType; /* Type of requests to listen for */
2562
+
2563
+
2564
+ if( flags & HTTP_SERVER_SCGI ){
2565
+ zRequestType = "SCGI";
2566
+ }else if( g.httpUseSSL ){
2567
+ zRequestType = "TLS-encrypted HTTPS";
2568
+ }else{
2569
+ zRequestType = "HTTP";
2570
+ }
2571
+
2572
+ if( flags & HTTP_SERVER_UNIXSOCKET ){
2573
+ /* CASE 1: A unix socket named g.zSockName. After creation, set the
2574
+ ** permissions on the new socket to g.zSockMode and make the
2575
+ ** owner of the socket be g.zSockOwner.
2576
+ */
2577
+ assert( g.zSockName!=0 );
2578
+ memset(&uxaddr, 0, sizeof(uxaddr));
2579
+ if( strlen(g.zSockName)>sizeof(uxaddr.sun_path) ){
2580
+ fossil_fatal("name of unix socket too big: %s\nmax size: %d\n",
2581
+ g.zSockName, (int)sizeof(uxaddr.sun_path));
2582
+ }
2583
+ if( file_isdir(g.zSockName, ExtFILE)!=0 ){
2584
+ if( !file_issocket(g.zSockName) ){
2585
+ fossil_fatal("cannot name socket \"%s\" because another object"
2586
+ " with that name already exists", g.zSockName);
2587
+ }else{
2588
+ unlink(g.zSockName);
2589
+ }
2590
+ }
2591
+ uxaddr.sun_family = AF_UNIX;
2592
+ strncpy(uxaddr.sun_path, g.zSockName, sizeof(uxaddr.sun_path)-1);
2593
+ listen4 = socket(AF_UNIX, SOCK_STREAM, 0);
2594
+ if( listen4<0 ){
2595
+ fossil_fatal("unable to create a unix socket named %s",
2596
+ g.zSockName);
2597
+ }
2598
+ mxListen = listen4;
2599
+ listen6 = -1;
2600
+
2601
+ /* Set the access permission for the new socket. Default to 0660.
2602
+ ** But use an alternative specified by --socket-mode if available.
2603
+ ** Do this before bind() to avoid a race condition. */
2604
+ if( g.zSockMode ){
2605
+ file_set_mode(g.zSockName, listen4, g.zSockMode, 0);
2606
+ }else{
2607
+ file_set_mode(g.zSockName, listen4, "0660", 1);
2608
+ }
2609
+ rc = bind(listen4, (struct sockaddr*)&uxaddr, sizeof(uxaddr));
2610
+ /* Set the owner of the socket if requested by --socket-owner. This
2611
+ ** must wait until after bind(), after the filesystem object has been
2612
+ ** created. See https://lkml.org/lkml/2004/11/1/84 and
2613
+ ** https://fossil-scm.org/forum/forumpost/7517680ef9684c57 */
2614
+ if( g.zSockOwner ){
2615
+ file_set_owner(g.zSockName, listen4, g.zSockOwner);
2616
+ }
2617
+ fossil_print("Listening for %s requests on unix socket %s\n",
2618
+ zRequestType, g.zSockName);
2619
+ fflush(stdout);
2620
+ }else if( zIpAddr && strchr(zIpAddr,':')!=0 ){
2621
+ /* CASE 2: TCP on IPv6 IP address specified by zIpAddr and on port iPort.
2622
+ */
2623
+ assert( mnPort==mxPort );
2624
+ memset(&inaddr6, 0, sizeof(inaddr6));
2625
+ inaddr6.sin6_family = AF_INET6;
2626
+ inaddr6.sin6_port = htons(iPort);
2627
+ if( inet_pton(AF_INET6, zIpAddr, &inaddr6.sin6_addr)==0 ){
2628
+ fossil_fatal("not a valid IPv6 address: %s", zIpAddr);
2629
+ }
2630
+ listen6 = socket(AF_INET6, SOCK_STREAM, 0);
2631
+ if( listen6>0 ){
2632
+ opt = 1;
2633
+ setsockopt(listen6, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
2634
+ rc = bind(listen6, (struct sockaddr*)&inaddr6, sizeof(inaddr6));
2635
+ if( rc<0 ){
2636
+ close(listen6);
2637
+ listen6 = -1;
2638
+ }
2639
+ }
2640
+ if( listen6<0 ){
2641
+ fossil_fatal("cannot open a listening socket on [%s]:%d",
2642
+ zIpAddr, mnPort);
2643
+ }
2644
+ mxListen = listen6;
2645
+ listen4 = -1;
2646
+ fossil_print("Listening for %s requests on [%s]:%d\n",
2647
+ zRequestType, zIpAddr, iPort);
2648
+ fflush(stdout);
2649
+ }else if( zIpAddr && zIpAddr[0] ){
2650
+ /* CASE 3: TCP on IPv4 IP address specified by zIpAddr and on port iPort.
2651
+ */
2652
+ assert( mnPort==mxPort );
2653
+ memset(&inaddr4, 0, sizeof(inaddr4));
2654
+ inaddr4.sin_family = AF_INET;
2655
+ inaddr4.sin_port = htons(iPort);
2656
+ if( strcmp(zIpAddr, "localhost")==0 ) zIpAddr = "127.0.0.1";
2657
+ inaddr4.sin_addr.s_addr = inet_addr(zIpAddr);
2658
+ if( inaddr4.sin_addr.s_addr == INADDR_NONE ){
2659
+ fossil_fatal("not a valid IPv4 address: %s", zIpAddr);
2660
+ }
2661
+ listen4 = socket(AF_INET, SOCK_STREAM, 0);
2662
+ if( listen4>0 ){
2663
+ setsockopt(listen4, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
2664
+ rc = bind(listen4, (struct sockaddr*)&inaddr4, sizeof(inaddr4));
2665
+ if( rc<0 ){
2666
+ close(listen6);
2667
+ listen4 = -1;
2668
+ }
2669
+ }
2670
+ if( listen4<0 ){
2671
+ fossil_fatal("cannot open a listening socket on %s:%d",
2672
+ zIpAddr, mnPort);
2673
+ }
2674
+ mxListen = listen4;
2675
+ listen6 = -1;
2676
+ fossil_print("Listening for %s requests on TCP port %s:%d\n",
2677
+ zRequestType, zIpAddr, iPort);
2678
+ fflush(stdout);
2679
+ }else{
2680
+ /* CASE 4: Listen on all available IP addresses, or on only loopback
2681
+ ** addresses (if HTTP_SERVER_LOCALHOST). The TCP port is the
2682
+ ** first available in the range of mnPort..mxPort. Listen
2683
+ ** on both IPv4 and IPv6, if possible. The TCP port scan is done
2684
+ ** on IPv4.
2685
+ */
2686
+ while( iPort<=mxPort ){
2687
+ const char *zProto;
2688
+ memset(&inaddr4, 0, sizeof(inaddr4));
2689
+ inaddr4.sin_family = AF_INET;
2690
+ inaddr4.sin_port = htons(iPort);
2691
+ if( flags & HTTP_SERVER_LOCALHOST ){
2692
+ inaddr4.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
2693
+ }else{
2694
+ inaddr4.sin_addr.s_addr = htonl(INADDR_ANY);
2695
+ }
2696
+ listen4 = socket(AF_INET, SOCK_STREAM, 0);
2697
+ if( listen4>0 ){
2698
+ setsockopt(listen4, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
2699
+ rc = bind(listen4, (struct sockaddr*)&inaddr4, sizeof(inaddr4));
2700
+ if( rc<0 ){
2701
+ close(listen4);
2702
+ listen4 = -1;
2703
+ }
2704
+ }
2705
+ if( listen4<0 ){
2706
+ iPort++;
2707
+ continue;
2708
+ }
2709
+ mxListen = listen4;
2710
+
2711
+ /* If we get here, that means we found an open TCP port at iPort for
2712
+ ** IPv4. Try to set up a corresponding IPv6 socket on the same port.
2713
+ */
2714
+ memset(&inaddr6, 0, sizeof(inaddr6));
2715
+ inaddr6.sin6_family = AF_INET6;
2716
+ inaddr6.sin6_port = htons(iPort);
2717
+ if( flags & HTTP_SERVER_LOCALHOST ){
2718
+ inaddr6.sin6_addr = in6addr_loopback;
2719
+ }else{
2720
+ inaddr6.sin6_addr = in6addr_any;
2721
+ }
2722
+ listen6 = socket(AF_INET6, SOCK_STREAM, 0);
2723
+ if( listen6>0 ){
2724
+ setsockopt(listen6, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
2725
+ setsockopt(listen6, IPPROTO_IPV6, IPV6_V6ONLY, &opt, sizeof(opt));
2726
+ rc = bind(listen6, (struct sockaddr*)&inaddr6, sizeof(inaddr6));
2727
+ if( rc<0 ){
2728
+ close(listen6);
2729
+ listen6 = -1;
2730
+ }
2731
+ }
2732
+ if( listen6<0 ){
2733
+ zProto = "IPv4 only";
2734
+ }else{
2735
+ zProto = "IPv4 and IPv6";
2736
+ if( listen6>listen4 ) mxListen = listen6;
2737
+ }
2738
+
2739
+ fossil_print("Listening for %s requests on TCP port %s%d, %s\n",
2740
+ zRequestType,
2741
+ (flags & HTTP_SERVER_LOCALHOST)!=0 ? "localhost:" : "",
2742
+ iPort, zProto);
2743
+ fflush(stdout);
2744
+ break;
2745
+ }
2746
+ if( iPort>mxPort ){
2747
+ fossil_fatal("no available TCP ports in the range %d..%d",
2748
+ mnPort, mxPort);
2749
+ }
2750
+ }
2751
+
2752
+ /* If we get to this point, that means there is at least one listening
2753
+ ** socket on either listen4 or listen6 and perhaps on both. */
2754
+ assert( listen4>0 || listen6>0 );
2755
+ if( listen4>0 ) listen(listen4,10);
2756
+ if( listen6>0 ) listen(listen6,10);
26462757
if( zBrowser && (flags & HTTP_SERVER_UNIXSOCKET)==0 ){
26472758
assert( strstr(zBrowser,"%d")!=0 );
26482759
zBrowser = mprintf(zBrowser /*works-like:"%d"*/, iPort);
26492760
#if defined(__CYGWIN__)
26502761
/* On Cygwin, we can do better than "echo" */
@@ -2658,56 +2769,69 @@
26582769
#endif
26592770
if( fossil_system(zBrowser)<0 ){
26602771
fossil_warning("cannot start browser: %s\n", zBrowser);
26612772
}
26622773
}
2774
+
2775
+ /* What for incomming requests. For each request, fork() a child process
2776
+ ** to deal with that request. The child process returns. The parent
2777
+ ** keeps on listening and never returns.
2778
+ */
26632779
while( 1 ){
26642780
#if FOSSIL_MAX_CONNECTIONS>0
26652781
while( nchildren>=FOSSIL_MAX_CONNECTIONS ){
26662782
if( wait(0)>=0 ) nchildren--;
26672783
}
26682784
#endif
26692785
delay.tv_sec = 0;
26702786
delay.tv_usec = 100000;
26712787
FD_ZERO(&readfds);
2672
- assert( listener>=0 );
2673
- FD_SET( listener, &readfds);
2674
- select( listener+1, &readfds, 0, 0, &delay);
2675
- if( FD_ISSET(listener, &readfds) ){
2676
- lenaddr = sizeof(inaddr);
2677
- connection = accept(listener, (struct sockaddr*)&inaddr, &lenaddr);
2678
- if( connection>=0 ){
2679
- if( flags & HTTP_SERVER_NOFORK ){
2680
- child = 0;
2681
- }else{
2682
- child = fork();
2683
- }
2684
- if( child!=0 ){
2685
- if( child>0 ){
2686
- nchildren++;
2687
- nRequest++;
2688
- }
2689
- close(connection);
2690
- }else{
2691
- int nErr = 0, fd;
2692
- g.zSockName = 0 /* avoid deleting the socket via atexit() */;
2693
- close(0);
2694
- fd = dup(connection);
2695
- if( fd!=0 ) nErr++;
2696
- close(1);
2697
- fd = dup(connection);
2698
- if( fd!=1 ) nErr++;
2699
- if( 0 && !g.fAnyTrace ){
2700
- close(2);
2701
- fd = dup(connection);
2702
- if( fd!=2 ) nErr++;
2703
- }
2704
- close(connection);
2705
- g.nPendingRequest = nchildren+1;
2706
- g.nRequest = nRequest+1;
2707
- return nErr;
2708
- }
2788
+ assert( listen4>0 || listen6>0 );
2789
+ if( listen4>0 ) FD_SET( listen4, &readfds);
2790
+ if( listen6>0 ) FD_SET( listen6, &readfds);
2791
+ select( mxListen+1, &readfds, 0, 0, &delay);
2792
+ if( listen4>0 && FD_ISSET(listen4, &readfds) ){
2793
+ lenaddr = sizeof(inaddr4);
2794
+ connection = accept(listen4, (struct sockaddr*)&inaddr4, &lenaddr);
2795
+ }else if( listen6>0 && FD_ISSET(listen6, &readfds) ){
2796
+ lenaddr = sizeof(inaddr6);
2797
+ connection = accept(listen6, (struct sockaddr*)&inaddr6, &lenaddr);
2798
+ }else{
2799
+ connection = -1;
2800
+ }
2801
+ if( connection>=0 ){
2802
+ if( flags & HTTP_SERVER_NOFORK ){
2803
+ child = 0;
2804
+ }else{
2805
+ child = fork();
2806
+ }
2807
+ if( child!=0 ){
2808
+ if( child>0 ){
2809
+ nchildren++;
2810
+ nRequest++;
2811
+ }
2812
+ close(connection);
2813
+ }else{
2814
+ int nErr = 0, fd;
2815
+ g.zSockName = 0 /* avoid deleting the socket via atexit() */;
2816
+ close(0);
2817
+ fd = dup(connection);
2818
+ if( fd!=0 ) nErr++;
2819
+ close(1);
2820
+ fd = dup(connection);
2821
+ if( fd!=1 ) nErr++;
2822
+ if( 0 && !g.fAnyTrace ){
2823
+ close(2);
2824
+ fd = dup(connection);
2825
+ if( fd!=2 ) nErr++;
2826
+ }
2827
+ close(connection);
2828
+ if( listen4>0 ) close(listen4);
2829
+ if( listen6>0 ) close(listen6);
2830
+ g.nPendingRequest = nchildren+1;
2831
+ g.nRequest = nRequest+1;
2832
+ return nErr;
27092833
}
27102834
}
27112835
/* Bury dead children */
27122836
if( nchildren ){
27132837
while(1){
27142838
--- src/cgi.c
+++ src/cgi.c
@@ -72,10 +72,11 @@
72 # include <ws2tcpip.h>
73 #else
74 # include <sys/socket.h>
75 # include <sys/un.h>
76 # include <netinet/in.h>
 
77 # include <arpa/inet.h>
78 # include <sys/times.h>
79 # include <sys/time.h>
80 # include <sys/wait.h>
81 # include <sys/select.h>
@@ -103,12 +104,12 @@
103 #define PT(x) cgi_parameter_trimmed((x),0)
104 #define PDT(x,y) cgi_parameter_trimmed((x),(y))
105 #define PB(x) cgi_parameter_boolean(x)
106 #define PCK(x) cgi_parameter_checked(x,1)
107 #define PIF(x,y) cgi_parameter_checked(x,y)
108 #define P_NoBot(x) cgi_parameter_nosql((x),0)
109 #define PD_NoBot(x,y) cgi_parameter_nosql((x),(y))
110
111 /*
112 ** Shortcut for the cgi_printf() routine. Instead of using the
113 **
114 ** @ ...
@@ -637,10 +638,13 @@
637 cgi_set_status(iStat, zStat);
638 free(zLocation);
639 cgi_reply();
640 fossil_exit(0);
641 }
 
 
 
642 NORETURN void cgi_redirect(const char *zURL){
643 cgi_redirect_with_status(zURL, 302, "Moved Temporarily");
644 }
645 NORETURN void cgi_redirect_with_method(const char *zURL){
646 cgi_redirect_with_status(zURL, 307, "Temporary Redirect");
@@ -1620,37 +1624,39 @@
1620 fossil_errorlog("Xpossible hack attempt - 418 response on \"%s\"", zName);
1621 exit(0);
1622 }
1623
1624 /*
1625 ** If looks_like_sql_injection() returns true for the given string, calls
1626 ** cgi_begone_spider() and does not return, else this function has no
1627 ** side effects. The range of checks performed by this function may
1628 ** be extended in the future.
1629 **
1630 ** Checks are omitted for any logged-in user.
1631 **
1632 ** This is NOT a defense against SQL injection. Fossil should easily be
1633 ** proof against SQL injection without this routine. Rather, this is an
1634 ** attempt to avoid denial-of-service caused by persistent spiders that hammer
1635 ** the server with dozens or hundreds of SQL injection attempts per second
1636 ** against pages (such as /vdiff) that are expensive to compute. In other
1637 ** words, this is an effort to reduce the CPU load imposed by malicious
1638 ** spiders. It is not an effect defense against SQL injection vulnerabilities.
 
 
1639 */
1640 void cgi_value_spider_check(const char *zTxt, const char *zName){
1641 if( g.zLogin==0 && looks_like_sql_injection(zTxt) ){
1642 cgi_begone_spider(zName);
1643 }
1644 }
1645
1646 /*
1647 ** A variant of cgi_parameter() with the same semantics except that if
1648 ** cgi_parameter(zName,zDefault) returns a value other than zDefault
1649 ** then it passes that value to cgi_value_spider_check().
1650 */
1651 const char *cgi_parameter_nosql(const char *zName, const char *zDefault){
1652 const char *zTxt = cgi_parameter(zName, zDefault);
1653
1654 if( zTxt!=zDefault ){
1655 cgi_value_spider_check(zTxt, zName);
1656 }
@@ -2070,34 +2076,40 @@
2070 }
2071 if( zLeftOver ){ *zLeftOver = zInput; }
2072 return zResult;
2073 }
2074
 
 
 
 
 
 
 
 
 
 
 
2075 /*
2076 ** Determine the IP address on the other side of a connection.
2077 ** Return a pointer to a string. Or return 0 if unable.
2078 **
2079 ** The string is held in a static buffer that is overwritten on
2080 ** each call.
2081 */
2082 char *cgi_remote_ip(int fd){
2083 #if 0
2084 static char zIp[100];
2085 struct sockaddr_in6 addr;
2086 socklen_t sz = sizeof(addr);
2087 if( getpeername(fd, &addr, &sz) ) return 0;
2088 zIp[0] = 0;
2089 if( inet_ntop(AF_INET6, &addr, zIp, sizeof(zIp))==0 ){
 
2090 return 0;
2091 }
2092 return zIp;
2093 #else
2094 struct sockaddr_in remoteName;
2095 socklen_t size = sizeof(struct sockaddr_in);
2096 if( getpeername(fd, (struct sockaddr*)&remoteName, &size) ) return 0;
2097 return inet_ntoa(remoteName.sin_addr);
2098 #endif
2099 }
2100
2101 /*
2102 ** This routine handles a single HTTP request which is coming in on
2103 ** g.httpIn and which replies on g.httpOut
@@ -2486,11 +2498,10 @@
2486 fossil_free(zToFree);
2487 fgetc(g.httpIn); /* Read past the "," separating header from content */
2488 cgi_init();
2489 }
2490
2491
2492 #if INTERFACE
2493 /*
2494 ** Bitmap values for the flags parameter to cgi_http_server().
2495 */
2496 #define HTTP_SERVER_LOCALHOST 0x0001 /* Bind to 127.0.0.1 only */
@@ -2529,122 +2540,222 @@
2529 ){
2530 #if defined(_WIN32)
2531 /* Use win32_http_server() instead */
2532 fossil_exit(1);
2533 #else
2534 int listener = -1; /* The server socket */
2535 int connection; /* A socket for each individual connection */
 
 
2536 int nRequest = 0; /* Number of requests handled so far */
2537 fd_set readfds; /* Set of file descriptors for select() */
2538 socklen_t lenaddr; /* Length of the inaddr structure */
2539 int child; /* PID of the child process */
2540 int nchildren = 0; /* Number of child processes */
2541 struct timeval delay; /* How long to wait inside select() */
2542 struct sockaddr_in inaddr; /* The socket address */
 
2543 struct sockaddr_un uxaddr; /* The address for unix-domain sockets */
2544 int opt = 1; /* setsockopt flag */
2545 int rc; /* Result code from system calls */
2546 int iPort = mnPort; /* Port to try to use */
2547
2548 while( iPort<=mxPort ){
2549 if( flags & HTTP_SERVER_UNIXSOCKET ){
2550 /* Initialize a Unix socket named g.zSockName */
2551 assert( g.zSockName!=0 );
2552 memset(&uxaddr, 0, sizeof(uxaddr));
2553 if( strlen(g.zSockName)>sizeof(uxaddr.sun_path) ){
2554 fossil_fatal("name of unix socket too big: %s\nmax size: %d\n",
2555 g.zSockName, (int)sizeof(uxaddr.sun_path));
2556 }
2557 if( file_isdir(g.zSockName, ExtFILE)!=0 ){
2558 if( !file_issocket(g.zSockName) ){
2559 fossil_fatal("cannot name socket \"%s\" because another object"
2560 " with that name already exists", g.zSockName);
2561 }else{
2562 unlink(g.zSockName);
2563 }
2564 }
2565 uxaddr.sun_family = AF_UNIX;
2566 strncpy(uxaddr.sun_path, g.zSockName, sizeof(uxaddr.sun_path)-1);
2567 listener = socket(AF_UNIX, SOCK_STREAM, 0);
2568 if( listener<0 ){
2569 fossil_fatal("unable to create a unix socket named %s",
2570 g.zSockName);
2571 }
2572 /* Set the access permission for the new socket. Default to 0660.
2573 ** But use an alternative specified by --socket-mode if available.
2574 ** Do this before bind() to avoid a race condition. */
2575 if( g.zSockMode ){
2576 file_set_mode(g.zSockName, listener, g.zSockMode, 0);
2577 }else{
2578 file_set_mode(g.zSockName, listener, "0660", 1);
2579 }
2580 }else{
2581 /* Initialize a TCP/IP socket on port iPort */
2582 memset(&inaddr, 0, sizeof(inaddr));
2583 inaddr.sin_family = AF_INET;
2584 if( zIpAddr ){
2585 inaddr.sin_addr.s_addr = inet_addr(zIpAddr);
2586 if( inaddr.sin_addr.s_addr == INADDR_NONE ){
2587 fossil_fatal("not a valid IP address: %s", zIpAddr);
2588 }
2589 }else if( flags & HTTP_SERVER_LOCALHOST ){
2590 inaddr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
2591 }else{
2592 inaddr.sin_addr.s_addr = htonl(INADDR_ANY);
2593 }
2594 inaddr.sin_port = htons(iPort);
2595 listener = socket(AF_INET, SOCK_STREAM, 0);
2596 if( listener<0 ){
2597 iPort++;
2598 continue;
2599 }
2600 }
2601
2602 /* if we can't terminate nicely, at least allow the socket to be reused */
2603 setsockopt(listener,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
2604
2605 if( flags & HTTP_SERVER_UNIXSOCKET ){
2606 rc = bind(listener, (struct sockaddr*)&uxaddr, sizeof(uxaddr));
2607 /* Set the owner of the socket if requested by --socket-owner. This
2608 ** must wait until after bind(), after the filesystem object has been
2609 ** created. See https://lkml.org/lkml/2004/11/1/84 and
2610 ** https://fossil-scm.org/forum/forumpost/7517680ef9684c57 */
2611 if( g.zSockOwner ){
2612 file_set_owner(g.zSockName, listener, g.zSockOwner);
2613 }
2614 }else{
2615 rc = bind(listener, (struct sockaddr*)&inaddr, sizeof(inaddr));
2616 }
2617 if( rc<0 ){
2618 close(listener);
2619 iPort++;
2620 continue;
2621 }
2622 break;
2623 }
2624 if( iPort>mxPort ){
2625 if( flags & HTTP_SERVER_UNIXSOCKET ){
2626 fossil_fatal("unable to listen on unix socket %s", zIpAddr);
2627 }else if( mnPort==mxPort ){
2628 fossil_fatal("unable to open listening socket on port %d", mnPort);
2629 }else{
2630 fossil_fatal("unable to open listening socket on any"
2631 " port in the range %d..%d", mnPort, mxPort);
2632 }
2633 }
2634 if( iPort>mxPort ) return 1;
2635 listen(listener,10);
2636 if( flags & HTTP_SERVER_UNIXSOCKET ){
2637 fossil_print("Listening for %s requests on unix socket %s\n",
2638 (flags & HTTP_SERVER_SCGI)!=0 ? "SCGI" :
2639 g.httpUseSSL?"TLS-encrypted HTTPS":"HTTP", g.zSockName);
2640 }else{
2641 fossil_print("Listening for %s requests on TCP port %d\n",
2642 (flags & HTTP_SERVER_SCGI)!=0 ? "SCGI" :
2643 g.httpUseSSL?"TLS-encrypted HTTPS":"HTTP", iPort);
2644 }
2645 fflush(stdout);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2646 if( zBrowser && (flags & HTTP_SERVER_UNIXSOCKET)==0 ){
2647 assert( strstr(zBrowser,"%d")!=0 );
2648 zBrowser = mprintf(zBrowser /*works-like:"%d"*/, iPort);
2649 #if defined(__CYGWIN__)
2650 /* On Cygwin, we can do better than "echo" */
@@ -2658,56 +2769,69 @@
2658 #endif
2659 if( fossil_system(zBrowser)<0 ){
2660 fossil_warning("cannot start browser: %s\n", zBrowser);
2661 }
2662 }
 
 
 
 
 
2663 while( 1 ){
2664 #if FOSSIL_MAX_CONNECTIONS>0
2665 while( nchildren>=FOSSIL_MAX_CONNECTIONS ){
2666 if( wait(0)>=0 ) nchildren--;
2667 }
2668 #endif
2669 delay.tv_sec = 0;
2670 delay.tv_usec = 100000;
2671 FD_ZERO(&readfds);
2672 assert( listener>=0 );
2673 FD_SET( listener, &readfds);
2674 select( listener+1, &readfds, 0, 0, &delay);
2675 if( FD_ISSET(listener, &readfds) ){
2676 lenaddr = sizeof(inaddr);
2677 connection = accept(listener, (struct sockaddr*)&inaddr, &lenaddr);
2678 if( connection>=0 ){
2679 if( flags & HTTP_SERVER_NOFORK ){
2680 child = 0;
2681 }else{
2682 child = fork();
2683 }
2684 if( child!=0 ){
2685 if( child>0 ){
2686 nchildren++;
2687 nRequest++;
2688 }
2689 close(connection);
2690 }else{
2691 int nErr = 0, fd;
2692 g.zSockName = 0 /* avoid deleting the socket via atexit() */;
2693 close(0);
2694 fd = dup(connection);
2695 if( fd!=0 ) nErr++;
2696 close(1);
2697 fd = dup(connection);
2698 if( fd!=1 ) nErr++;
2699 if( 0 && !g.fAnyTrace ){
2700 close(2);
2701 fd = dup(connection);
2702 if( fd!=2 ) nErr++;
2703 }
2704 close(connection);
2705 g.nPendingRequest = nchildren+1;
2706 g.nRequest = nRequest+1;
2707 return nErr;
2708 }
 
 
 
 
 
 
 
 
2709 }
2710 }
2711 /* Bury dead children */
2712 if( nchildren ){
2713 while(1){
2714
--- src/cgi.c
+++ src/cgi.c
@@ -72,10 +72,11 @@
72 # include <ws2tcpip.h>
73 #else
74 # include <sys/socket.h>
75 # include <sys/un.h>
76 # include <netinet/in.h>
77 # include <netdb.h>
78 # include <arpa/inet.h>
79 # include <sys/times.h>
80 # include <sys/time.h>
81 # include <sys/wait.h>
82 # include <sys/select.h>
@@ -103,12 +104,12 @@
104 #define PT(x) cgi_parameter_trimmed((x),0)
105 #define PDT(x,y) cgi_parameter_trimmed((x),(y))
106 #define PB(x) cgi_parameter_boolean(x)
107 #define PCK(x) cgi_parameter_checked(x,1)
108 #define PIF(x,y) cgi_parameter_checked(x,y)
109 #define P_NoBot(x) cgi_parameter_no_attack((x),0)
110 #define PD_NoBot(x,y) cgi_parameter_no_attack((x),(y))
111
112 /*
113 ** Shortcut for the cgi_printf() routine. Instead of using the
114 **
115 ** @ ...
@@ -637,10 +638,13 @@
638 cgi_set_status(iStat, zStat);
639 free(zLocation);
640 cgi_reply();
641 fossil_exit(0);
642 }
643 NORETURN void cgi_redirect_perm(const char *zURL){
644 cgi_redirect_with_status(zURL, 301, "Moved Permanently");
645 }
646 NORETURN void cgi_redirect(const char *zURL){
647 cgi_redirect_with_status(zURL, 302, "Moved Temporarily");
648 }
649 NORETURN void cgi_redirect_with_method(const char *zURL){
650 cgi_redirect_with_status(zURL, 307, "Temporary Redirect");
@@ -1620,37 +1624,39 @@
1624 fossil_errorlog("Xpossible hack attempt - 418 response on \"%s\"", zName);
1625 exit(0);
1626 }
1627
1628 /*
1629 ** If looks_like_attack() returns true for the given string, call
1630 ** cgi_begone_spider() and does not return, else this function has no
1631 ** side effects. The range of checks performed by this function may
1632 ** be extended in the future.
1633 **
1634 ** Checks are omitted for any logged-in user.
1635 **
1636 ** This is the primary defense against attack. Fossil should easily be
1637 ** proof against SQL injection and XSS attacks even without without this
1638 ** routine. Rather, this is an attempt to avoid denial-of-service caused
1639 ** by persistent spiders that hammer the server with dozens or hundreds of
1640 ** probes per seconds as they look for vulnerabilities. In other
1641 ** words, this is an effort to reduce the CPU load imposed by malicious
1642 ** spiders. Though those routine might help make attacks harder, it is
1643 ** not itself an impenetrably barrier against attack and should not be
1644 ** relied upon as the only defense.
1645 */
1646 void cgi_value_spider_check(const char *zTxt, const char *zName){
1647 if( g.zLogin==0 && looks_like_attack(zTxt) ){
1648 cgi_begone_spider(zName);
1649 }
1650 }
1651
1652 /*
1653 ** A variant of cgi_parameter() with the same semantics except that if
1654 ** cgi_parameter(zName,zDefault) returns a value other than zDefault
1655 ** then it passes that value to cgi_value_spider_check().
1656 */
1657 const char *cgi_parameter_no_attack(const char *zName, const char *zDefault){
1658 const char *zTxt = cgi_parameter(zName, zDefault);
1659
1660 if( zTxt!=zDefault ){
1661 cgi_value_spider_check(zTxt, zName);
1662 }
@@ -2070,34 +2076,40 @@
2076 }
2077 if( zLeftOver ){ *zLeftOver = zInput; }
2078 return zResult;
2079 }
2080
2081 /*
2082 ** All possible forms of an IP address. Needed to work around GCC strict
2083 ** aliasing rules.
2084 */
2085 typedef union {
2086 struct sockaddr sa; /* Abstract superclass */
2087 struct sockaddr_in sa4; /* IPv4 */
2088 struct sockaddr_in6 sa6; /* IPv6 */
2089 struct sockaddr_storage sas; /* Should be the maximum of the above 3 */
2090 } address;
2091
2092 /*
2093 ** Determine the IP address on the other side of a connection.
2094 ** Return a pointer to a string. Or return 0 if unable.
2095 **
2096 ** The string is held in a static buffer that is overwritten on
2097 ** each call.
2098 */
2099 char *cgi_remote_ip(int fd){
2100 address remoteAddr;
2101 socklen_t size = sizeof(remoteAddr);
2102 static char zHost[NI_MAXHOST];
2103 if( getpeername(0, &remoteAddr.sa, &size) ){
2104 return 0;
2105 }
2106 if( getnameinfo(&remoteAddr.sa, size, zHost, sizeof(zHost), 0, 0,
2107 NI_NUMERICHOST) ){
2108 return 0;
2109 }
2110 return zHost;
 
 
 
 
 
 
2111 }
2112
2113 /*
2114 ** This routine handles a single HTTP request which is coming in on
2115 ** g.httpIn and which replies on g.httpOut
@@ -2486,11 +2498,10 @@
2498 fossil_free(zToFree);
2499 fgetc(g.httpIn); /* Read past the "," separating header from content */
2500 cgi_init();
2501 }
2502
 
2503 #if INTERFACE
2504 /*
2505 ** Bitmap values for the flags parameter to cgi_http_server().
2506 */
2507 #define HTTP_SERVER_LOCALHOST 0x0001 /* Bind to 127.0.0.1 only */
@@ -2529,122 +2540,222 @@
2540 ){
2541 #if defined(_WIN32)
2542 /* Use win32_http_server() instead */
2543 fossil_exit(1);
2544 #else
2545 int listen4 = -1; /* Main socket; IPv4 or unix-domain */
2546 int listen6 = -1; /* Aux socket for corresponding IPv6 */
2547 int mxListen = -1; /* Maximum of listen4 and listen6 */
2548 int connection; /* An incoming connection */
2549 int nRequest = 0; /* Number of requests handled so far */
2550 fd_set readfds; /* Set of file descriptors for select() */
2551 socklen_t lenaddr; /* Length of the inaddr structure */
2552 int child; /* PID of the child process */
2553 int nchildren = 0; /* Number of child processes */
2554 struct timeval delay; /* How long to wait inside select() */
2555 struct sockaddr_in6 inaddr6; /* Address for IPv6 */
2556 struct sockaddr_in inaddr4; /* Address for IPv4 */
2557 struct sockaddr_un uxaddr; /* The address for unix-domain sockets */
2558 int opt = 1; /* setsockopt flag */
2559 int rc; /* Result code from system calls */
2560 int iPort = mnPort; /* Port to try to use */
2561 const char *zRequestType; /* Type of requests to listen for */
2562
2563
2564 if( flags & HTTP_SERVER_SCGI ){
2565 zRequestType = "SCGI";
2566 }else if( g.httpUseSSL ){
2567 zRequestType = "TLS-encrypted HTTPS";
2568 }else{
2569 zRequestType = "HTTP";
2570 }
2571
2572 if( flags & HTTP_SERVER_UNIXSOCKET ){
2573 /* CASE 1: A unix socket named g.zSockName. After creation, set the
2574 ** permissions on the new socket to g.zSockMode and make the
2575 ** owner of the socket be g.zSockOwner.
2576 */
2577 assert( g.zSockName!=0 );
2578 memset(&uxaddr, 0, sizeof(uxaddr));
2579 if( strlen(g.zSockName)>sizeof(uxaddr.sun_path) ){
2580 fossil_fatal("name of unix socket too big: %s\nmax size: %d\n",
2581 g.zSockName, (int)sizeof(uxaddr.sun_path));
2582 }
2583 if( file_isdir(g.zSockName, ExtFILE)!=0 ){
2584 if( !file_issocket(g.zSockName) ){
2585 fossil_fatal("cannot name socket \"%s\" because another object"
2586 " with that name already exists", g.zSockName);
2587 }else{
2588 unlink(g.zSockName);
2589 }
2590 }
2591 uxaddr.sun_family = AF_UNIX;
2592 strncpy(uxaddr.sun_path, g.zSockName, sizeof(uxaddr.sun_path)-1);
2593 listen4 = socket(AF_UNIX, SOCK_STREAM, 0);
2594 if( listen4<0 ){
2595 fossil_fatal("unable to create a unix socket named %s",
2596 g.zSockName);
2597 }
2598 mxListen = listen4;
2599 listen6 = -1;
2600
2601 /* Set the access permission for the new socket. Default to 0660.
2602 ** But use an alternative specified by --socket-mode if available.
2603 ** Do this before bind() to avoid a race condition. */
2604 if( g.zSockMode ){
2605 file_set_mode(g.zSockName, listen4, g.zSockMode, 0);
2606 }else{
2607 file_set_mode(g.zSockName, listen4, "0660", 1);
2608 }
2609 rc = bind(listen4, (struct sockaddr*)&uxaddr, sizeof(uxaddr));
2610 /* Set the owner of the socket if requested by --socket-owner. This
2611 ** must wait until after bind(), after the filesystem object has been
2612 ** created. See https://lkml.org/lkml/2004/11/1/84 and
2613 ** https://fossil-scm.org/forum/forumpost/7517680ef9684c57 */
2614 if( g.zSockOwner ){
2615 file_set_owner(g.zSockName, listen4, g.zSockOwner);
2616 }
2617 fossil_print("Listening for %s requests on unix socket %s\n",
2618 zRequestType, g.zSockName);
2619 fflush(stdout);
2620 }else if( zIpAddr && strchr(zIpAddr,':')!=0 ){
2621 /* CASE 2: TCP on IPv6 IP address specified by zIpAddr and on port iPort.
2622 */
2623 assert( mnPort==mxPort );
2624 memset(&inaddr6, 0, sizeof(inaddr6));
2625 inaddr6.sin6_family = AF_INET6;
2626 inaddr6.sin6_port = htons(iPort);
2627 if( inet_pton(AF_INET6, zIpAddr, &inaddr6.sin6_addr)==0 ){
2628 fossil_fatal("not a valid IPv6 address: %s", zIpAddr);
2629 }
2630 listen6 = socket(AF_INET6, SOCK_STREAM, 0);
2631 if( listen6>0 ){
2632 opt = 1;
2633 setsockopt(listen6, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
2634 rc = bind(listen6, (struct sockaddr*)&inaddr6, sizeof(inaddr6));
2635 if( rc<0 ){
2636 close(listen6);
2637 listen6 = -1;
2638 }
2639 }
2640 if( listen6<0 ){
2641 fossil_fatal("cannot open a listening socket on [%s]:%d",
2642 zIpAddr, mnPort);
2643 }
2644 mxListen = listen6;
2645 listen4 = -1;
2646 fossil_print("Listening for %s requests on [%s]:%d\n",
2647 zRequestType, zIpAddr, iPort);
2648 fflush(stdout);
2649 }else if( zIpAddr && zIpAddr[0] ){
2650 /* CASE 3: TCP on IPv4 IP address specified by zIpAddr and on port iPort.
2651 */
2652 assert( mnPort==mxPort );
2653 memset(&inaddr4, 0, sizeof(inaddr4));
2654 inaddr4.sin_family = AF_INET;
2655 inaddr4.sin_port = htons(iPort);
2656 if( strcmp(zIpAddr, "localhost")==0 ) zIpAddr = "127.0.0.1";
2657 inaddr4.sin_addr.s_addr = inet_addr(zIpAddr);
2658 if( inaddr4.sin_addr.s_addr == INADDR_NONE ){
2659 fossil_fatal("not a valid IPv4 address: %s", zIpAddr);
2660 }
2661 listen4 = socket(AF_INET, SOCK_STREAM, 0);
2662 if( listen4>0 ){
2663 setsockopt(listen4, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
2664 rc = bind(listen4, (struct sockaddr*)&inaddr4, sizeof(inaddr4));
2665 if( rc<0 ){
2666 close(listen6);
2667 listen4 = -1;
2668 }
2669 }
2670 if( listen4<0 ){
2671 fossil_fatal("cannot open a listening socket on %s:%d",
2672 zIpAddr, mnPort);
2673 }
2674 mxListen = listen4;
2675 listen6 = -1;
2676 fossil_print("Listening for %s requests on TCP port %s:%d\n",
2677 zRequestType, zIpAddr, iPort);
2678 fflush(stdout);
2679 }else{
2680 /* CASE 4: Listen on all available IP addresses, or on only loopback
2681 ** addresses (if HTTP_SERVER_LOCALHOST). The TCP port is the
2682 ** first available in the range of mnPort..mxPort. Listen
2683 ** on both IPv4 and IPv6, if possible. The TCP port scan is done
2684 ** on IPv4.
2685 */
2686 while( iPort<=mxPort ){
2687 const char *zProto;
2688 memset(&inaddr4, 0, sizeof(inaddr4));
2689 inaddr4.sin_family = AF_INET;
2690 inaddr4.sin_port = htons(iPort);
2691 if( flags & HTTP_SERVER_LOCALHOST ){
2692 inaddr4.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
2693 }else{
2694 inaddr4.sin_addr.s_addr = htonl(INADDR_ANY);
2695 }
2696 listen4 = socket(AF_INET, SOCK_STREAM, 0);
2697 if( listen4>0 ){
2698 setsockopt(listen4, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
2699 rc = bind(listen4, (struct sockaddr*)&inaddr4, sizeof(inaddr4));
2700 if( rc<0 ){
2701 close(listen4);
2702 listen4 = -1;
2703 }
2704 }
2705 if( listen4<0 ){
2706 iPort++;
2707 continue;
2708 }
2709 mxListen = listen4;
2710
2711 /* If we get here, that means we found an open TCP port at iPort for
2712 ** IPv4. Try to set up a corresponding IPv6 socket on the same port.
2713 */
2714 memset(&inaddr6, 0, sizeof(inaddr6));
2715 inaddr6.sin6_family = AF_INET6;
2716 inaddr6.sin6_port = htons(iPort);
2717 if( flags & HTTP_SERVER_LOCALHOST ){
2718 inaddr6.sin6_addr = in6addr_loopback;
2719 }else{
2720 inaddr6.sin6_addr = in6addr_any;
2721 }
2722 listen6 = socket(AF_INET6, SOCK_STREAM, 0);
2723 if( listen6>0 ){
2724 setsockopt(listen6, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
2725 setsockopt(listen6, IPPROTO_IPV6, IPV6_V6ONLY, &opt, sizeof(opt));
2726 rc = bind(listen6, (struct sockaddr*)&inaddr6, sizeof(inaddr6));
2727 if( rc<0 ){
2728 close(listen6);
2729 listen6 = -1;
2730 }
2731 }
2732 if( listen6<0 ){
2733 zProto = "IPv4 only";
2734 }else{
2735 zProto = "IPv4 and IPv6";
2736 if( listen6>listen4 ) mxListen = listen6;
2737 }
2738
2739 fossil_print("Listening for %s requests on TCP port %s%d, %s\n",
2740 zRequestType,
2741 (flags & HTTP_SERVER_LOCALHOST)!=0 ? "localhost:" : "",
2742 iPort, zProto);
2743 fflush(stdout);
2744 break;
2745 }
2746 if( iPort>mxPort ){
2747 fossil_fatal("no available TCP ports in the range %d..%d",
2748 mnPort, mxPort);
2749 }
2750 }
2751
2752 /* If we get to this point, that means there is at least one listening
2753 ** socket on either listen4 or listen6 and perhaps on both. */
2754 assert( listen4>0 || listen6>0 );
2755 if( listen4>0 ) listen(listen4,10);
2756 if( listen6>0 ) listen(listen6,10);
2757 if( zBrowser && (flags & HTTP_SERVER_UNIXSOCKET)==0 ){
2758 assert( strstr(zBrowser,"%d")!=0 );
2759 zBrowser = mprintf(zBrowser /*works-like:"%d"*/, iPort);
2760 #if defined(__CYGWIN__)
2761 /* On Cygwin, we can do better than "echo" */
@@ -2658,56 +2769,69 @@
2769 #endif
2770 if( fossil_system(zBrowser)<0 ){
2771 fossil_warning("cannot start browser: %s\n", zBrowser);
2772 }
2773 }
2774
2775 /* What for incomming requests. For each request, fork() a child process
2776 ** to deal with that request. The child process returns. The parent
2777 ** keeps on listening and never returns.
2778 */
2779 while( 1 ){
2780 #if FOSSIL_MAX_CONNECTIONS>0
2781 while( nchildren>=FOSSIL_MAX_CONNECTIONS ){
2782 if( wait(0)>=0 ) nchildren--;
2783 }
2784 #endif
2785 delay.tv_sec = 0;
2786 delay.tv_usec = 100000;
2787 FD_ZERO(&readfds);
2788 assert( listen4>0 || listen6>0 );
2789 if( listen4>0 ) FD_SET( listen4, &readfds);
2790 if( listen6>0 ) FD_SET( listen6, &readfds);
2791 select( mxListen+1, &readfds, 0, 0, &delay);
2792 if( listen4>0 && FD_ISSET(listen4, &readfds) ){
2793 lenaddr = sizeof(inaddr4);
2794 connection = accept(listen4, (struct sockaddr*)&inaddr4, &lenaddr);
2795 }else if( listen6>0 && FD_ISSET(listen6, &readfds) ){
2796 lenaddr = sizeof(inaddr6);
2797 connection = accept(listen6, (struct sockaddr*)&inaddr6, &lenaddr);
2798 }else{
2799 connection = -1;
2800 }
2801 if( connection>=0 ){
2802 if( flags & HTTP_SERVER_NOFORK ){
2803 child = 0;
2804 }else{
2805 child = fork();
2806 }
2807 if( child!=0 ){
2808 if( child>0 ){
2809 nchildren++;
2810 nRequest++;
2811 }
2812 close(connection);
2813 }else{
2814 int nErr = 0, fd;
2815 g.zSockName = 0 /* avoid deleting the socket via atexit() */;
2816 close(0);
2817 fd = dup(connection);
2818 if( fd!=0 ) nErr++;
2819 close(1);
2820 fd = dup(connection);
2821 if( fd!=1 ) nErr++;
2822 if( 0 && !g.fAnyTrace ){
2823 close(2);
2824 fd = dup(connection);
2825 if( fd!=2 ) nErr++;
2826 }
2827 close(connection);
2828 if( listen4>0 ) close(listen4);
2829 if( listen6>0 ) close(listen6);
2830 g.nPendingRequest = nchildren+1;
2831 g.nRequest = nRequest+1;
2832 return nErr;
2833 }
2834 }
2835 /* Bury dead children */
2836 if( nchildren ){
2837 while(1){
2838
+2 -1
--- src/chat.c
+++ src/chat.c
@@ -254,11 +254,12 @@
254254
@ /*^^^for skins which add their own BODY tag */;
255255
@ window.fossil.config.chat = {
256256
@ fromcli: %h(PB("cli")?"true":"false"),
257257
@ alertSound: "%h(zAlert)",
258258
@ initSize: %d(db_get_int("chat-initial-history",50)),
259
- @ imagesInline: !!%d(db_get_boolean("chat-inline-images",1))
259
+ @ imagesInline: !!%d(db_get_boolean("chat-inline-images",1)),
260
+ @ pollTimeout: %d(db_get_int("chat-poll-timeout",420))
260261
@ };
261262
ajax_emit_js_preview_modes(0);
262263
chat_emit_alert_list();
263264
@ }, false);
264265
@ </script>
265266
--- src/chat.c
+++ src/chat.c
@@ -254,11 +254,12 @@
254 @ /*^^^for skins which add their own BODY tag */;
255 @ window.fossil.config.chat = {
256 @ fromcli: %h(PB("cli")?"true":"false"),
257 @ alertSound: "%h(zAlert)",
258 @ initSize: %d(db_get_int("chat-initial-history",50)),
259 @ imagesInline: !!%d(db_get_boolean("chat-inline-images",1))
 
260 @ };
261 ajax_emit_js_preview_modes(0);
262 chat_emit_alert_list();
263 @ }, false);
264 @ </script>
265
--- src/chat.c
+++ src/chat.c
@@ -254,11 +254,12 @@
254 @ /*^^^for skins which add their own BODY tag */;
255 @ window.fossil.config.chat = {
256 @ fromcli: %h(PB("cli")?"true":"false"),
257 @ alertSound: "%h(zAlert)",
258 @ initSize: %d(db_get_int("chat-initial-history",50)),
259 @ imagesInline: !!%d(db_get_boolean("chat-inline-images",1)),
260 @ pollTimeout: %d(db_get_int("chat-poll-timeout",420))
261 @ };
262 ajax_emit_js_preview_modes(0);
263 chat_emit_alert_list();
264 @ }, false);
265 @ </script>
266
--- src/checkin.c
+++ src/checkin.c
@@ -2436,16 +2436,19 @@
24362436
** --allow-conflict Allow unresolved merge conflicts
24372437
** --allow-empty Allow a commit with no changes
24382438
** --allow-fork Allow the commit to fork
24392439
** --allow-older Allow a commit older than its ancestor
24402440
** --baseline Use a baseline manifest in the commit process
2441
+** --bgcolor COLOR Apply COLOR to this one check-in only
24412442
** --branch NEW-BRANCH-NAME Check in to this new branch
2443
+** --branchcolor COLOR Apply given COLOR to the branch
24422444
** --close Close the branch being committed
24432445
** --date-override DATETIME Make DATETIME the time of the check-in.
24442446
** Useful when importing historical check-ins
24452447
** from another version control system.
24462448
** --delta Use a delta manifest in the commit process
2449
+** --editor NAME Text editor to use for check-in comment.
24472450
** --hash Verify file status using hashing rather
24482451
** than relying on filesystem mtimes
24492452
** --if-changes Make this command a silent no-op if there
24502453
** are no changes
24512454
** --ignore-clock-skew If a clock skew is detected, ignore it and
@@ -2597,10 +2600,11 @@
25972600
useCksum = db_get_boolean("repo-cksum", 1);
25982601
bIgnoreSkew = find_option("ignore-clock-skew",0,0)!=0;
25992602
outputManifest = db_get_manifest_setting(0);
26002603
mxSize = db_large_file_size();
26012604
if( find_option("ignore-oversize",0,0)!=0 ) mxSize = 0;
2605
+ (void)fossil_text_editor();
26022606
verify_all_options();
26032607
26042608
/* The --no-warnings flag and the --force flag each imply
26052609
** the --no-verify-comment flag */
26062610
if( noWarningFlag || forceFlag ){
26072611
--- src/checkin.c
+++ src/checkin.c
@@ -2436,16 +2436,19 @@
2436 ** --allow-conflict Allow unresolved merge conflicts
2437 ** --allow-empty Allow a commit with no changes
2438 ** --allow-fork Allow the commit to fork
2439 ** --allow-older Allow a commit older than its ancestor
2440 ** --baseline Use a baseline manifest in the commit process
 
2441 ** --branch NEW-BRANCH-NAME Check in to this new branch
 
2442 ** --close Close the branch being committed
2443 ** --date-override DATETIME Make DATETIME the time of the check-in.
2444 ** Useful when importing historical check-ins
2445 ** from another version control system.
2446 ** --delta Use a delta manifest in the commit process
 
2447 ** --hash Verify file status using hashing rather
2448 ** than relying on filesystem mtimes
2449 ** --if-changes Make this command a silent no-op if there
2450 ** are no changes
2451 ** --ignore-clock-skew If a clock skew is detected, ignore it and
@@ -2597,10 +2600,11 @@
2597 useCksum = db_get_boolean("repo-cksum", 1);
2598 bIgnoreSkew = find_option("ignore-clock-skew",0,0)!=0;
2599 outputManifest = db_get_manifest_setting(0);
2600 mxSize = db_large_file_size();
2601 if( find_option("ignore-oversize",0,0)!=0 ) mxSize = 0;
 
2602 verify_all_options();
2603
2604 /* The --no-warnings flag and the --force flag each imply
2605 ** the --no-verify-comment flag */
2606 if( noWarningFlag || forceFlag ){
2607
--- src/checkin.c
+++ src/checkin.c
@@ -2436,16 +2436,19 @@
2436 ** --allow-conflict Allow unresolved merge conflicts
2437 ** --allow-empty Allow a commit with no changes
2438 ** --allow-fork Allow the commit to fork
2439 ** --allow-older Allow a commit older than its ancestor
2440 ** --baseline Use a baseline manifest in the commit process
2441 ** --bgcolor COLOR Apply COLOR to this one check-in only
2442 ** --branch NEW-BRANCH-NAME Check in to this new branch
2443 ** --branchcolor COLOR Apply given COLOR to the branch
2444 ** --close Close the branch being committed
2445 ** --date-override DATETIME Make DATETIME the time of the check-in.
2446 ** Useful when importing historical check-ins
2447 ** from another version control system.
2448 ** --delta Use a delta manifest in the commit process
2449 ** --editor NAME Text editor to use for check-in comment.
2450 ** --hash Verify file status using hashing rather
2451 ** than relying on filesystem mtimes
2452 ** --if-changes Make this command a silent no-op if there
2453 ** are no changes
2454 ** --ignore-clock-skew If a clock skew is detected, ignore it and
@@ -2597,10 +2600,11 @@
2600 useCksum = db_get_boolean("repo-cksum", 1);
2601 bIgnoreSkew = find_option("ignore-clock-skew",0,0)!=0;
2602 outputManifest = db_get_manifest_setting(0);
2603 mxSize = db_large_file_size();
2604 if( find_option("ignore-oversize",0,0)!=0 ) mxSize = 0;
2605 (void)fossil_text_editor();
2606 verify_all_options();
2607
2608 /* The --no-warnings flag and the --force flag each imply
2609 ** the --no-verify-comment flag */
2610 if( noWarningFlag || forceFlag ){
2611
+353
--- src/color.c
+++ src/color.c
@@ -20,10 +20,281 @@
2020
**
2121
*/
2222
#include "config.h"
2323
#include <string.h>
2424
#include "color.h"
25
+
26
+/*
27
+** 140 standard CSS color names and their corresponding RGB values,
28
+** in alphabetical order by name so that we can do a binary search
29
+** for lookup.
30
+*/
31
+static const struct CssColors {
32
+ const char *zName; /* CSS Color name, lower case */
33
+ unsigned int iRGB; /* Corresponding RGB value */
34
+} aCssColors[] = {
35
+ { "aliceblue", 0xf0f8ff },
36
+ { "antiquewhite", 0xfaebd7 },
37
+ { "aqua", 0x00ffff },
38
+ { "aquamarine", 0x7fffd4 },
39
+ { "azure", 0xf0ffff },
40
+ { "beige", 0xf5f5dc },
41
+ { "bisque", 0xffe4c4 },
42
+ { "black", 0x000000 },
43
+ { "blanchedalmond", 0xffebcd },
44
+ { "blue", 0x0000ff },
45
+ { "blueviolet", 0x8a2be2 },
46
+ { "brown", 0xa52a2a },
47
+ { "burlywood", 0xdeb887 },
48
+ { "cadetblue", 0x5f9ea0 },
49
+ { "chartreuse", 0x7fff00 },
50
+ { "chocolate", 0xd2691e },
51
+ { "coral", 0xff7f50 },
52
+ { "cornflowerblue", 0x6495ed },
53
+ { "cornsilk", 0xfff8dc },
54
+ { "crimson", 0xdc143c },
55
+ { "cyan", 0x00ffff },
56
+ { "darkblue", 0x00008b },
57
+ { "darkcyan", 0x008b8b },
58
+ { "darkgoldenrod", 0xb8860b },
59
+ { "darkgray", 0xa9a9a9 },
60
+ { "darkgreen", 0x006400 },
61
+ { "darkkhaki", 0xbdb76b },
62
+ { "darkmagenta", 0x8b008b },
63
+ { "darkolivegreen", 0x556b2f },
64
+ { "darkorange", 0xff8c00 },
65
+ { "darkorchid", 0x9932cc },
66
+ { "darkred", 0x8b0000 },
67
+ { "darksalmon", 0xe9967a },
68
+ { "darkseagreen", 0x8fbc8f },
69
+ { "darkslateblue", 0x483d8b },
70
+ { "darkslategray", 0x2f4f4f },
71
+ { "darkturquoise", 0x00ced1 },
72
+ { "darkviolet", 0x9400d3 },
73
+ { "deeppink", 0xff1493 },
74
+ { "deepskyblue", 0x00bfff },
75
+ { "dimgray", 0x696969 },
76
+ { "dodgerblue", 0x1e90ff },
77
+ { "firebrick", 0xb22222 },
78
+ { "floralwhite", 0xfffaf0 },
79
+ { "forestgreen", 0x228b22 },
80
+ { "fuchsia", 0xff00ff },
81
+ { "gainsboro", 0xdcdcdc },
82
+ { "ghostwhite", 0xf8f8ff },
83
+ { "gold", 0xffd700 },
84
+ { "goldenrod", 0xdaa520 },
85
+ { "gray", 0x808080 },
86
+ { "green", 0x008000 },
87
+ { "greenyellow", 0xadff2f },
88
+ { "honeydew", 0xf0fff0 },
89
+ { "hotpink", 0xff69b4 },
90
+ { "indianred", 0xcd5c5c },
91
+ { "indigo", 0x4b0082 },
92
+ { "ivory", 0xfffff0 },
93
+ { "khaki", 0xf0e68c },
94
+ { "lavender", 0xe6e6fa },
95
+ { "lavenderblush", 0xfff0f5 },
96
+ { "lawngreen", 0x7cfc00 },
97
+ { "lemonchiffon", 0xfffacd },
98
+ { "lightblue", 0xadd8e6 },
99
+ { "lightcoral", 0xf08080 },
100
+ { "lightcyan", 0xe0ffff },
101
+ { "lightgoldenrodyellow", 0xfafad2 },
102
+ { "lightgrey", 0xd3d3d3 },
103
+ { "lightgreen", 0x90ee90 },
104
+ { "lightpink", 0xffb6c1 },
105
+ { "lightsalmon", 0xffa07a },
106
+ { "lightseagreen", 0x20b2aa },
107
+ { "lightskyblue", 0x87cefa },
108
+ { "lightslategray", 0x778899 },
109
+ { "lightsteelblue", 0xb0c4de },
110
+ { "lightyellow", 0xffffe0 },
111
+ { "lime", 0x00ff00 },
112
+ { "limegreen", 0x32cd32 },
113
+ { "linen", 0xfaf0e6 },
114
+ { "magenta", 0xff00ff },
115
+ { "maroon", 0x800000 },
116
+ { "mediumaquamarine", 0x66cdaa },
117
+ { "mediumblue", 0x0000cd },
118
+ { "mediumorchid", 0xba55d3 },
119
+ { "mediumpurple", 0x9370d8 },
120
+ { "mediumseagreen", 0x3cb371 },
121
+ { "mediumslateblue", 0x7b68ee },
122
+ { "mediumspringgreen", 0x00fa9a },
123
+ { "mediumturquoise", 0x48d1cc },
124
+ { "mediumvioletred", 0xc71585 },
125
+ { "midnightblue", 0x191970 },
126
+ { "mintcream", 0xf5fffa },
127
+ { "mistyrose", 0xffe4e1 },
128
+ { "moccasin", 0xffe4b5 },
129
+ { "navajowhite", 0xffdead },
130
+ { "navy", 0x000080 },
131
+ { "oldlace", 0xfdf5e6 },
132
+ { "olive", 0x808000 },
133
+ { "olivedrab", 0x6b8e23 },
134
+ { "orange", 0xffa500 },
135
+ { "orangered", 0xff4500 },
136
+ { "orchid", 0xda70d6 },
137
+ { "palegoldenrod", 0xeee8aa },
138
+ { "palegreen", 0x98fb98 },
139
+ { "paleturquoise", 0xafeeee },
140
+ { "palevioletred", 0xd87093 },
141
+ { "papayawhip", 0xffefd5 },
142
+ { "peachpuff", 0xffdab9 },
143
+ { "peru", 0xcd853f },
144
+ { "pink", 0xffc0cb },
145
+ { "plum", 0xdda0dd },
146
+ { "powderblue", 0xb0e0e6 },
147
+ { "purple", 0x800080 },
148
+ { "red", 0xff0000 },
149
+ { "rosybrown", 0xbc8f8f },
150
+ { "royalblue", 0x4169e1 },
151
+ { "saddlebrown", 0x8b4513 },
152
+ { "salmon", 0xfa8072 },
153
+ { "sandybrown", 0xf4a460 },
154
+ { "seagreen", 0x2e8b57 },
155
+ { "seashell", 0xfff5ee },
156
+ { "sienna", 0xa0522d },
157
+ { "silver", 0xc0c0c0 },
158
+ { "skyblue", 0x87ceeb },
159
+ { "slateblue", 0x6a5acd },
160
+ { "slategray", 0x708090 },
161
+ { "snow", 0xfffafa },
162
+ { "springgreen", 0x00ff7f },
163
+ { "steelblue", 0x4682b4 },
164
+ { "tan", 0xd2b48c },
165
+ { "teal", 0x008080 },
166
+ { "thistle", 0xd8bfd8 },
167
+ { "tomato", 0xff6347 },
168
+ { "turquoise", 0x40e0d0 },
169
+ { "violet", 0xee82ee },
170
+ { "wheat", 0xf5deb3 },
171
+ { "white", 0xffffff },
172
+ { "whitesmoke", 0xf5f5f5 },
173
+ { "yellow", 0xffff00 },
174
+ { "yellowgreen", 0x9acd32 },
175
+};
176
+
177
+/*
178
+** Attempt to translate a CSS color name into an integer that
179
+** represents the equivalent RGB value. Ignore alpha if provided.
180
+** If the name cannot be translated, return -1.
181
+*/
182
+int color_name_to_rgb(const char *zName){
183
+ if( zName==0 || zName[0]==0 ) return -1;
184
+ if( zName[0]=='#' ){
185
+ int i, v = 0;
186
+ for(i=1; i<=6 && fossil_isxdigit(zName[i]); i++){
187
+ v = v*16 + fossil_hexvalue(zName[i]);
188
+ }
189
+ if( i==4 ){
190
+ v = fossil_hexvalue(zName[1])*0x110000 +
191
+ fossil_hexvalue(zName[2])*0x1100 +
192
+ fossil_hexvalue(zName[3])*0x11;
193
+ return v;
194
+ }
195
+ if( i==7 ){
196
+ return v;
197
+ }
198
+ return -1;
199
+ }else{
200
+ int iMin = 0;
201
+ int iMax = count(aCssColors)-1;
202
+ while( iMin<=iMax ){
203
+ int iMid = (iMin+iMax)/2;
204
+ int c = sqlite3_stricmp(aCssColors[iMid].zName, zName);
205
+ if( c==0 ) return aCssColors[iMid].iRGB;
206
+ if( c<0 ){
207
+ iMin = iMid+1;
208
+ }else{
209
+ iMax = iMid-1;
210
+ }
211
+ }
212
+ return -1;
213
+ }
214
+}
215
+
216
+/*
217
+** SETTING: raw-bgcolor boolean default=off
218
+**
219
+** Fossil usually tries to adjust user-specified background colors
220
+** for checkins so that the text is readable and so that the color
221
+** is not too garish. This setting disables that filter. When
222
+** this setting is on, the user-selected background colors are shown
223
+** exactly as requested.
224
+*/
225
+
226
+/*
227
+** Shift a color provided by the user so that it is suitable
228
+** for use as a background color in the current skin.
229
+**
230
+** The return value is a #HHHHHH color name contained in
231
+** static space that is overwritten on the next call.
232
+**
233
+** If we cannot make sense of the background color recommendation
234
+** that is the input, then return NULL.
235
+**
236
+** The iFgClr parameter is normally 0. But for testing purposes, set
237
+** it to 1 for a black foregrounds and 2 for a white foreground.
238
+*/
239
+const char *reasonable_bg_color(const char *zRequested, int iFgClr){
240
+ int iRGB = color_name_to_rgb(zRequested);
241
+ int r, g, b; /* RGB components of requested color */
242
+ static int systemFg = 0; /* 1==black-foreground 2==white-foreground */
243
+ int fg; /* Foreground color to actually use */
244
+ static char zColor[10]; /* Return value */
245
+
246
+ if( iFgClr ){
247
+ fg = iFgClr;
248
+ }else if( systemFg==0 ){
249
+ if( db_get_boolean("raw-bgcolor",0) ){
250
+ fg = systemFg = 3;
251
+ }else{
252
+ fg = systemFg = skin_detail_boolean("white-foreground") ? 2 : 1;
253
+ }
254
+ }else{
255
+ fg = systemFg;
256
+ }
257
+ if( fg>=3 ) return zRequested;
258
+
259
+ if( iRGB<0 ) return 0;
260
+ r = (iRGB>>16) & 0xff;
261
+ g = (iRGB>>8) & 0xff;
262
+ b = iRGB & 0xff;
263
+ if( fg==1 ){
264
+ /* Dark text on a light background. Adjust so that
265
+ ** no color component is less than 255-K, resulting in
266
+ ** a pastel background color. Color adjustment is quadratic
267
+ ** so that colors that are further out of range have a greater
268
+ ** adjustment. */
269
+ const int K = 79;
270
+ int k, x, m;
271
+ m = r<g ? r : g;
272
+ if( m>b ) m = b;
273
+ k = (m*m)/255 + K;
274
+ x = 255 - k;
275
+ r = (k*r)/255 + x;
276
+ g = (k*g)/255 + x;
277
+ b = (k*b)/255 + x;
278
+ }else{
279
+ /* Light text on a dark background. Adjust so that
280
+ ** no color component is greater than K, resulting in
281
+ ** a low-intensity, low-saturation background color.
282
+ ** The color adjustment is quadratic so that colors that
283
+ ** are further out of range have a greater adjustment. */
284
+ const int K = 112;
285
+ int k, m;
286
+ m = r>g ? r : g;
287
+ if( m<b ) m = b;
288
+ k = 255 - (255-K)*(m*m)/65025;
289
+ r = (k*r)/255;
290
+ g = (k*g)/255;
291
+ b = (k*b)/255;
292
+ }
293
+ sqlite3_snprintf(8, zColor, "#%02x%02x%02x", r,g,b);
294
+ return zColor;
295
+}
25296
26297
/*
27298
** Compute a hash on a branch or user name
28299
*/
29300
static unsigned int hash_of_name(const char *z){
@@ -185,5 +456,87 @@
185456
@ <input type="submit" value="Submit">
186457
@ <input type="submit" name="rand" value="Random">
187458
@ </form>
188459
style_finish_page();
189460
}
461
+
462
+/*
463
+** WEBPAGE: test-bgcolor
464
+**
465
+** Show how user-specified background colors will be rendered
466
+** using the reasonable_bg_color() algorithm.
467
+*/
468
+void test_bgcolor_page(void){
469
+ const char *zReq; /* Requested color name */
470
+ const char *zBG; /* Actual color provided */
471
+ const char *zBg1;
472
+ char zNm[10];
473
+ static const char *azDflt[] = {
474
+ "red", "orange", "yellow", "green", "blue", "indigo", "violet",
475
+ "tan", "brown", "gray",
476
+ };
477
+ const int N = count(azDflt);
478
+ int i, cnt, iClr, r, g, b;
479
+ char *zFg;
480
+ login_check_credentials();
481
+ style_set_current_feature("test");
482
+ style_header("Background Color Test");
483
+ for(i=cnt=0; i<N; i++){
484
+ sqlite3_snprintf(sizeof(zNm),zNm,"b%c",'a'+i);
485
+ zReq = PD(zNm,azDflt[i]);
486
+ if( zReq==0 || zReq[0]==0 ) continue;
487
+ if( cnt==0 ){
488
+ @ <table border="1" cellspacing="0" cellpadding="10">
489
+ @ <tr>
490
+ @ <th>Requested Background
491
+ @ <th>Light mode
492
+ @ <th>Dark mode
493
+ @ </tr>
494
+ }
495
+ cnt++;
496
+ zBG = reasonable_bg_color(zReq, 0);
497
+ if( zBG==0 ){
498
+ @ <tr><td colspan="3" align="center">\
499
+ @ "%h(zReq)" is not a recognized color name</td></tr>
500
+ continue;
501
+ }
502
+ iClr = color_name_to_rgb(zReq);
503
+ r = (iClr>>16) & 0xff;
504
+ g = (iClr>>8) & 0xff;
505
+ b = iClr & 0xff;
506
+ if( 3*r + 7*g + b > 6*255 ){
507
+ zFg = "black";
508
+ }else{
509
+ zFg = "white";
510
+ }
511
+ if( zReq[0]!='#' ){
512
+ char zReqRGB[12];
513
+ sqlite3_snprintf(sizeof(zReqRGB),zReqRGB,"#%06x",color_name_to_rgb(zReq));
514
+ @ <tr><td style='color:%h(zFg);background-color:%h(zReq);'>\
515
+ @ Requested color "%h(zReq)" (%h(zReqRGB))</td>
516
+ }else{
517
+ @ <tr><td style='color:%h(zFg);background-color:%s(zReq);'>\
518
+ @ Requested color "%h(zReq)"</td>
519
+ }
520
+ zBg1 = reasonable_bg_color(zReq,1);
521
+ @ <td style='color:black;background-color:%h(zBg1);'>\
522
+ @ Background color for dark text: %h(zBg1)</td>
523
+ zBg1 = reasonable_bg_color(zReq,2);
524
+ @ <td style='color:white;background-color:%h(zBg1);'>\
525
+ @ Background color for light text: %h(zBg1)</td></tr>
526
+ }
527
+ if( cnt ){
528
+ @ </table>
529
+ @ <hr>
530
+ }
531
+ @ <form method="POST">
532
+ @ <p>Enter CSS color names below and see them shifted into corresponding
533
+ @ background colors above.</p>
534
+ for(i=0; i<N; i++){
535
+ sqlite3_snprintf(sizeof(zNm),zNm,"b%c",'a'+i);
536
+ @ <input type="text" size="30" name='%s(zNm)' \
537
+ @ value='%h(PD(zNm,azDflt[i]))'><br>
538
+ }
539
+ @ <input type="submit" value="Submit">
540
+ @ </form>
541
+ style_finish_page();
542
+}
190543
--- src/color.c
+++ src/color.c
@@ -20,10 +20,281 @@
20 **
21 */
22 #include "config.h"
23 #include <string.h>
24 #include "color.h"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
26 /*
27 ** Compute a hash on a branch or user name
28 */
29 static unsigned int hash_of_name(const char *z){
@@ -185,5 +456,87 @@
185 @ <input type="submit" value="Submit">
186 @ <input type="submit" name="rand" value="Random">
187 @ </form>
188 style_finish_page();
189 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
190
--- src/color.c
+++ src/color.c
@@ -20,10 +20,281 @@
20 **
21 */
22 #include "config.h"
23 #include <string.h>
24 #include "color.h"
25
26 /*
27 ** 140 standard CSS color names and their corresponding RGB values,
28 ** in alphabetical order by name so that we can do a binary search
29 ** for lookup.
30 */
31 static const struct CssColors {
32 const char *zName; /* CSS Color name, lower case */
33 unsigned int iRGB; /* Corresponding RGB value */
34 } aCssColors[] = {
35 { "aliceblue", 0xf0f8ff },
36 { "antiquewhite", 0xfaebd7 },
37 { "aqua", 0x00ffff },
38 { "aquamarine", 0x7fffd4 },
39 { "azure", 0xf0ffff },
40 { "beige", 0xf5f5dc },
41 { "bisque", 0xffe4c4 },
42 { "black", 0x000000 },
43 { "blanchedalmond", 0xffebcd },
44 { "blue", 0x0000ff },
45 { "blueviolet", 0x8a2be2 },
46 { "brown", 0xa52a2a },
47 { "burlywood", 0xdeb887 },
48 { "cadetblue", 0x5f9ea0 },
49 { "chartreuse", 0x7fff00 },
50 { "chocolate", 0xd2691e },
51 { "coral", 0xff7f50 },
52 { "cornflowerblue", 0x6495ed },
53 { "cornsilk", 0xfff8dc },
54 { "crimson", 0xdc143c },
55 { "cyan", 0x00ffff },
56 { "darkblue", 0x00008b },
57 { "darkcyan", 0x008b8b },
58 { "darkgoldenrod", 0xb8860b },
59 { "darkgray", 0xa9a9a9 },
60 { "darkgreen", 0x006400 },
61 { "darkkhaki", 0xbdb76b },
62 { "darkmagenta", 0x8b008b },
63 { "darkolivegreen", 0x556b2f },
64 { "darkorange", 0xff8c00 },
65 { "darkorchid", 0x9932cc },
66 { "darkred", 0x8b0000 },
67 { "darksalmon", 0xe9967a },
68 { "darkseagreen", 0x8fbc8f },
69 { "darkslateblue", 0x483d8b },
70 { "darkslategray", 0x2f4f4f },
71 { "darkturquoise", 0x00ced1 },
72 { "darkviolet", 0x9400d3 },
73 { "deeppink", 0xff1493 },
74 { "deepskyblue", 0x00bfff },
75 { "dimgray", 0x696969 },
76 { "dodgerblue", 0x1e90ff },
77 { "firebrick", 0xb22222 },
78 { "floralwhite", 0xfffaf0 },
79 { "forestgreen", 0x228b22 },
80 { "fuchsia", 0xff00ff },
81 { "gainsboro", 0xdcdcdc },
82 { "ghostwhite", 0xf8f8ff },
83 { "gold", 0xffd700 },
84 { "goldenrod", 0xdaa520 },
85 { "gray", 0x808080 },
86 { "green", 0x008000 },
87 { "greenyellow", 0xadff2f },
88 { "honeydew", 0xf0fff0 },
89 { "hotpink", 0xff69b4 },
90 { "indianred", 0xcd5c5c },
91 { "indigo", 0x4b0082 },
92 { "ivory", 0xfffff0 },
93 { "khaki", 0xf0e68c },
94 { "lavender", 0xe6e6fa },
95 { "lavenderblush", 0xfff0f5 },
96 { "lawngreen", 0x7cfc00 },
97 { "lemonchiffon", 0xfffacd },
98 { "lightblue", 0xadd8e6 },
99 { "lightcoral", 0xf08080 },
100 { "lightcyan", 0xe0ffff },
101 { "lightgoldenrodyellow", 0xfafad2 },
102 { "lightgrey", 0xd3d3d3 },
103 { "lightgreen", 0x90ee90 },
104 { "lightpink", 0xffb6c1 },
105 { "lightsalmon", 0xffa07a },
106 { "lightseagreen", 0x20b2aa },
107 { "lightskyblue", 0x87cefa },
108 { "lightslategray", 0x778899 },
109 { "lightsteelblue", 0xb0c4de },
110 { "lightyellow", 0xffffe0 },
111 { "lime", 0x00ff00 },
112 { "limegreen", 0x32cd32 },
113 { "linen", 0xfaf0e6 },
114 { "magenta", 0xff00ff },
115 { "maroon", 0x800000 },
116 { "mediumaquamarine", 0x66cdaa },
117 { "mediumblue", 0x0000cd },
118 { "mediumorchid", 0xba55d3 },
119 { "mediumpurple", 0x9370d8 },
120 { "mediumseagreen", 0x3cb371 },
121 { "mediumslateblue", 0x7b68ee },
122 { "mediumspringgreen", 0x00fa9a },
123 { "mediumturquoise", 0x48d1cc },
124 { "mediumvioletred", 0xc71585 },
125 { "midnightblue", 0x191970 },
126 { "mintcream", 0xf5fffa },
127 { "mistyrose", 0xffe4e1 },
128 { "moccasin", 0xffe4b5 },
129 { "navajowhite", 0xffdead },
130 { "navy", 0x000080 },
131 { "oldlace", 0xfdf5e6 },
132 { "olive", 0x808000 },
133 { "olivedrab", 0x6b8e23 },
134 { "orange", 0xffa500 },
135 { "orangered", 0xff4500 },
136 { "orchid", 0xda70d6 },
137 { "palegoldenrod", 0xeee8aa },
138 { "palegreen", 0x98fb98 },
139 { "paleturquoise", 0xafeeee },
140 { "palevioletred", 0xd87093 },
141 { "papayawhip", 0xffefd5 },
142 { "peachpuff", 0xffdab9 },
143 { "peru", 0xcd853f },
144 { "pink", 0xffc0cb },
145 { "plum", 0xdda0dd },
146 { "powderblue", 0xb0e0e6 },
147 { "purple", 0x800080 },
148 { "red", 0xff0000 },
149 { "rosybrown", 0xbc8f8f },
150 { "royalblue", 0x4169e1 },
151 { "saddlebrown", 0x8b4513 },
152 { "salmon", 0xfa8072 },
153 { "sandybrown", 0xf4a460 },
154 { "seagreen", 0x2e8b57 },
155 { "seashell", 0xfff5ee },
156 { "sienna", 0xa0522d },
157 { "silver", 0xc0c0c0 },
158 { "skyblue", 0x87ceeb },
159 { "slateblue", 0x6a5acd },
160 { "slategray", 0x708090 },
161 { "snow", 0xfffafa },
162 { "springgreen", 0x00ff7f },
163 { "steelblue", 0x4682b4 },
164 { "tan", 0xd2b48c },
165 { "teal", 0x008080 },
166 { "thistle", 0xd8bfd8 },
167 { "tomato", 0xff6347 },
168 { "turquoise", 0x40e0d0 },
169 { "violet", 0xee82ee },
170 { "wheat", 0xf5deb3 },
171 { "white", 0xffffff },
172 { "whitesmoke", 0xf5f5f5 },
173 { "yellow", 0xffff00 },
174 { "yellowgreen", 0x9acd32 },
175 };
176
177 /*
178 ** Attempt to translate a CSS color name into an integer that
179 ** represents the equivalent RGB value. Ignore alpha if provided.
180 ** If the name cannot be translated, return -1.
181 */
182 int color_name_to_rgb(const char *zName){
183 if( zName==0 || zName[0]==0 ) return -1;
184 if( zName[0]=='#' ){
185 int i, v = 0;
186 for(i=1; i<=6 && fossil_isxdigit(zName[i]); i++){
187 v = v*16 + fossil_hexvalue(zName[i]);
188 }
189 if( i==4 ){
190 v = fossil_hexvalue(zName[1])*0x110000 +
191 fossil_hexvalue(zName[2])*0x1100 +
192 fossil_hexvalue(zName[3])*0x11;
193 return v;
194 }
195 if( i==7 ){
196 return v;
197 }
198 return -1;
199 }else{
200 int iMin = 0;
201 int iMax = count(aCssColors)-1;
202 while( iMin<=iMax ){
203 int iMid = (iMin+iMax)/2;
204 int c = sqlite3_stricmp(aCssColors[iMid].zName, zName);
205 if( c==0 ) return aCssColors[iMid].iRGB;
206 if( c<0 ){
207 iMin = iMid+1;
208 }else{
209 iMax = iMid-1;
210 }
211 }
212 return -1;
213 }
214 }
215
216 /*
217 ** SETTING: raw-bgcolor boolean default=off
218 **
219 ** Fossil usually tries to adjust user-specified background colors
220 ** for checkins so that the text is readable and so that the color
221 ** is not too garish. This setting disables that filter. When
222 ** this setting is on, the user-selected background colors are shown
223 ** exactly as requested.
224 */
225
226 /*
227 ** Shift a color provided by the user so that it is suitable
228 ** for use as a background color in the current skin.
229 **
230 ** The return value is a #HHHHHH color name contained in
231 ** static space that is overwritten on the next call.
232 **
233 ** If we cannot make sense of the background color recommendation
234 ** that is the input, then return NULL.
235 **
236 ** The iFgClr parameter is normally 0. But for testing purposes, set
237 ** it to 1 for a black foregrounds and 2 for a white foreground.
238 */
239 const char *reasonable_bg_color(const char *zRequested, int iFgClr){
240 int iRGB = color_name_to_rgb(zRequested);
241 int r, g, b; /* RGB components of requested color */
242 static int systemFg = 0; /* 1==black-foreground 2==white-foreground */
243 int fg; /* Foreground color to actually use */
244 static char zColor[10]; /* Return value */
245
246 if( iFgClr ){
247 fg = iFgClr;
248 }else if( systemFg==0 ){
249 if( db_get_boolean("raw-bgcolor",0) ){
250 fg = systemFg = 3;
251 }else{
252 fg = systemFg = skin_detail_boolean("white-foreground") ? 2 : 1;
253 }
254 }else{
255 fg = systemFg;
256 }
257 if( fg>=3 ) return zRequested;
258
259 if( iRGB<0 ) return 0;
260 r = (iRGB>>16) & 0xff;
261 g = (iRGB>>8) & 0xff;
262 b = iRGB & 0xff;
263 if( fg==1 ){
264 /* Dark text on a light background. Adjust so that
265 ** no color component is less than 255-K, resulting in
266 ** a pastel background color. Color adjustment is quadratic
267 ** so that colors that are further out of range have a greater
268 ** adjustment. */
269 const int K = 79;
270 int k, x, m;
271 m = r<g ? r : g;
272 if( m>b ) m = b;
273 k = (m*m)/255 + K;
274 x = 255 - k;
275 r = (k*r)/255 + x;
276 g = (k*g)/255 + x;
277 b = (k*b)/255 + x;
278 }else{
279 /* Light text on a dark background. Adjust so that
280 ** no color component is greater than K, resulting in
281 ** a low-intensity, low-saturation background color.
282 ** The color adjustment is quadratic so that colors that
283 ** are further out of range have a greater adjustment. */
284 const int K = 112;
285 int k, m;
286 m = r>g ? r : g;
287 if( m<b ) m = b;
288 k = 255 - (255-K)*(m*m)/65025;
289 r = (k*r)/255;
290 g = (k*g)/255;
291 b = (k*b)/255;
292 }
293 sqlite3_snprintf(8, zColor, "#%02x%02x%02x", r,g,b);
294 return zColor;
295 }
296
297 /*
298 ** Compute a hash on a branch or user name
299 */
300 static unsigned int hash_of_name(const char *z){
@@ -185,5 +456,87 @@
456 @ <input type="submit" value="Submit">
457 @ <input type="submit" name="rand" value="Random">
458 @ </form>
459 style_finish_page();
460 }
461
462 /*
463 ** WEBPAGE: test-bgcolor
464 **
465 ** Show how user-specified background colors will be rendered
466 ** using the reasonable_bg_color() algorithm.
467 */
468 void test_bgcolor_page(void){
469 const char *zReq; /* Requested color name */
470 const char *zBG; /* Actual color provided */
471 const char *zBg1;
472 char zNm[10];
473 static const char *azDflt[] = {
474 "red", "orange", "yellow", "green", "blue", "indigo", "violet",
475 "tan", "brown", "gray",
476 };
477 const int N = count(azDflt);
478 int i, cnt, iClr, r, g, b;
479 char *zFg;
480 login_check_credentials();
481 style_set_current_feature("test");
482 style_header("Background Color Test");
483 for(i=cnt=0; i<N; i++){
484 sqlite3_snprintf(sizeof(zNm),zNm,"b%c",'a'+i);
485 zReq = PD(zNm,azDflt[i]);
486 if( zReq==0 || zReq[0]==0 ) continue;
487 if( cnt==0 ){
488 @ <table border="1" cellspacing="0" cellpadding="10">
489 @ <tr>
490 @ <th>Requested Background
491 @ <th>Light mode
492 @ <th>Dark mode
493 @ </tr>
494 }
495 cnt++;
496 zBG = reasonable_bg_color(zReq, 0);
497 if( zBG==0 ){
498 @ <tr><td colspan="3" align="center">\
499 @ "%h(zReq)" is not a recognized color name</td></tr>
500 continue;
501 }
502 iClr = color_name_to_rgb(zReq);
503 r = (iClr>>16) & 0xff;
504 g = (iClr>>8) & 0xff;
505 b = iClr & 0xff;
506 if( 3*r + 7*g + b > 6*255 ){
507 zFg = "black";
508 }else{
509 zFg = "white";
510 }
511 if( zReq[0]!='#' ){
512 char zReqRGB[12];
513 sqlite3_snprintf(sizeof(zReqRGB),zReqRGB,"#%06x",color_name_to_rgb(zReq));
514 @ <tr><td style='color:%h(zFg);background-color:%h(zReq);'>\
515 @ Requested color "%h(zReq)" (%h(zReqRGB))</td>
516 }else{
517 @ <tr><td style='color:%h(zFg);background-color:%s(zReq);'>\
518 @ Requested color "%h(zReq)"</td>
519 }
520 zBg1 = reasonable_bg_color(zReq,1);
521 @ <td style='color:black;background-color:%h(zBg1);'>\
522 @ Background color for dark text: %h(zBg1)</td>
523 zBg1 = reasonable_bg_color(zReq,2);
524 @ <td style='color:white;background-color:%h(zBg1);'>\
525 @ Background color for light text: %h(zBg1)</td></tr>
526 }
527 if( cnt ){
528 @ </table>
529 @ <hr>
530 }
531 @ <form method="POST">
532 @ <p>Enter CSS color names below and see them shifted into corresponding
533 @ background colors above.</p>
534 for(i=0; i<N; i++){
535 sqlite3_snprintf(sizeof(zNm),zNm,"b%c",'a'+i);
536 @ <input type="text" size="30" name='%s(zNm)' \
537 @ value='%h(PD(zNm,azDflt[i]))'><br>
538 }
539 @ <input type="submit" value="Submit">
540 @ </form>
541 style_finish_page();
542 }
543
+1 -1
--- src/db.c
+++ src/db.c
@@ -3369,11 +3369,11 @@
33693369
if( zProjectName ) fossil_print("project-name: %s\n", zProjectName);
33703370
if( zProjectDesc ) fossil_print("project-description: %s\n", zProjectDesc);
33713371
fossil_print("project-id: %s\n", db_get("project-code", 0));
33723372
fossil_print("server-id: %s\n", db_get("server-code", 0));
33733373
zPassword = db_text(0, "SELECT pw FROM user WHERE login=%Q", g.zLogin);
3374
- fossil_print("admin-user: %s (initial password is \"%s\")\n",
3374
+ fossil_print("admin-user: %s (initial remote-access password is \"%s\")\n",
33753375
g.zLogin, zPassword);
33763376
hash_user_password(g.zLogin);
33773377
}
33783378
33793379
/*
33803380
--- src/db.c
+++ src/db.c
@@ -3369,11 +3369,11 @@
3369 if( zProjectName ) fossil_print("project-name: %s\n", zProjectName);
3370 if( zProjectDesc ) fossil_print("project-description: %s\n", zProjectDesc);
3371 fossil_print("project-id: %s\n", db_get("project-code", 0));
3372 fossil_print("server-id: %s\n", db_get("server-code", 0));
3373 zPassword = db_text(0, "SELECT pw FROM user WHERE login=%Q", g.zLogin);
3374 fossil_print("admin-user: %s (initial password is \"%s\")\n",
3375 g.zLogin, zPassword);
3376 hash_user_password(g.zLogin);
3377 }
3378
3379 /*
3380
--- src/db.c
+++ src/db.c
@@ -3369,11 +3369,11 @@
3369 if( zProjectName ) fossil_print("project-name: %s\n", zProjectName);
3370 if( zProjectDesc ) fossil_print("project-description: %s\n", zProjectDesc);
3371 fossil_print("project-id: %s\n", db_get("project-code", 0));
3372 fossil_print("server-id: %s\n", db_get("server-code", 0));
3373 zPassword = db_text(0, "SELECT pw FROM user WHERE login=%Q", g.zLogin);
3374 fossil_print("admin-user: %s (initial remote-access password is \"%s\")\n",
3375 g.zLogin, zPassword);
3376 hash_user_password(g.zLogin);
3377 }
3378
3379 /*
3380
--- src/default.css
+++ src/default.css
@@ -751,10 +751,11 @@
751751
border-left: 1px solid gold;
752752
}
753753
body.cpage-ckout .file-change-line,
754754
body.cpage-info .file-change-line,
755755
body.cpage-vinfo .file-change-line,
756
+body.cpage-ci .file-change-line,
756757
body.cpage-vdiff .file-change-line {
757758
margin-top: 16px;
758759
margin-bottom: 16px;
759760
margin-right: 1em /* keep it from nudging right up against the scrollbar-reveal zone */;
760761
display: flex;
761762
--- src/default.css
+++ src/default.css
@@ -751,10 +751,11 @@
751 border-left: 1px solid gold;
752 }
753 body.cpage-ckout .file-change-line,
754 body.cpage-info .file-change-line,
755 body.cpage-vinfo .file-change-line,
 
756 body.cpage-vdiff .file-change-line {
757 margin-top: 16px;
758 margin-bottom: 16px;
759 margin-right: 1em /* keep it from nudging right up against the scrollbar-reveal zone */;
760 display: flex;
761
--- src/default.css
+++ src/default.css
@@ -751,10 +751,11 @@
751 border-left: 1px solid gold;
752 }
753 body.cpage-ckout .file-change-line,
754 body.cpage-info .file-change-line,
755 body.cpage-vinfo .file-change-line,
756 body.cpage-ci .file-change-line,
757 body.cpage-vdiff .file-change-line {
758 margin-top: 16px;
759 margin-bottom: 16px;
760 margin-right: 1em /* keep it from nudging right up against the scrollbar-reveal zone */;
761 display: flex;
762
+1 -1
--- src/doc.c
+++ src/doc.c
@@ -1052,11 +1052,11 @@
10521052
*/
10531053
zMime = nMiss==0 ? P("mimetype") : 0;
10541054
if( zMime==0 ){
10551055
zMime = mimetype_from_name(zName);
10561056
}
1057
- Th_Store("doc_name", zName);
1057
+ Th_StoreUnsafe("doc_name", zName);
10581058
if( vid ){
10591059
Th_Store("doc_version", db_text(0, "SELECT '[' || substr(uuid,1,10) || ']'"
10601060
" FROM blob WHERE rid=%d", vid));
10611061
Th_Store("doc_date", db_text(0, "SELECT datetime(mtime) FROM event"
10621062
" WHERE objid=%d AND type='ci'", vid));
10631063
--- src/doc.c
+++ src/doc.c
@@ -1052,11 +1052,11 @@
1052 */
1053 zMime = nMiss==0 ? P("mimetype") : 0;
1054 if( zMime==0 ){
1055 zMime = mimetype_from_name(zName);
1056 }
1057 Th_Store("doc_name", zName);
1058 if( vid ){
1059 Th_Store("doc_version", db_text(0, "SELECT '[' || substr(uuid,1,10) || ']'"
1060 " FROM blob WHERE rid=%d", vid));
1061 Th_Store("doc_date", db_text(0, "SELECT datetime(mtime) FROM event"
1062 " WHERE objid=%d AND type='ci'", vid));
1063
--- src/doc.c
+++ src/doc.c
@@ -1052,11 +1052,11 @@
1052 */
1053 zMime = nMiss==0 ? P("mimetype") : 0;
1054 if( zMime==0 ){
1055 zMime = mimetype_from_name(zName);
1056 }
1057 Th_StoreUnsafe("doc_name", zName);
1058 if( vid ){
1059 Th_Store("doc_version", db_text(0, "SELECT '[' || substr(uuid,1,10) || ']'"
1060 " FROM blob WHERE rid=%d", vid));
1061 Th_Store("doc_date", db_text(0, "SELECT datetime(mtime) FROM event"
1062 " WHERE objid=%d AND type='ci'", vid));
1063
+75 -3
--- src/encode.c
+++ src/encode.c
@@ -142,10 +142,82 @@
142142
}
143143
}
144144
if( j<i ) blob_append(p, zIn+j, i-j);
145145
}
146146
147
+/*
148
+** Make the given string safe for HTML by converting syntax characters
149
+** into alternatives that do not have the special syntactic meaning.
150
+**
151
+** < --> U+227a
152
+** > --> U+227b
153
+** & --> +
154
+** " --> U+201d
155
+** ' --> U+2019
156
+**
157
+** Return a pointer to a new string obtained from fossil_malloc().
158
+*/
159
+char *html_lookalike(const char *z, int n){
160
+ unsigned char c;
161
+ int i = 0;
162
+ int count = 0;
163
+ unsigned char *zOut;
164
+ const unsigned char *zIn = (const unsigned char*)z;
165
+
166
+ if( n<0 ) n = strlen(z);
167
+ while( i<n ){
168
+ switch( zIn[i] ){
169
+ case '<': count += 3; break;
170
+ case '>': count += 3; break;
171
+ case '"': count += 3; break;
172
+ case '\'': count += 3; break;
173
+ case 0: n = i; break;
174
+ }
175
+ i++;
176
+ }
177
+ i = 0;
178
+ zOut = fossil_malloc( count+n+1 );
179
+ if( count==0 ){
180
+ memcpy(zOut, zIn, n);
181
+ zOut[n] = 0;
182
+ return (char*)zOut;
183
+ }
184
+ while( n-->0 ){
185
+ c = *(zIn++);
186
+ switch( c ){
187
+ case '<':
188
+ zOut[i++] = 0xe2;
189
+ zOut[i++] = 0x89;
190
+ zOut[i++] = 0xba;
191
+ break;
192
+ case '>':
193
+ zOut[i++] = 0xe2;
194
+ zOut[i++] = 0x89;
195
+ zOut[i++] = 0xbb;
196
+ break;
197
+ case '&':
198
+ zOut[i++] = '+';
199
+ break;
200
+ case '"':
201
+ zOut[i++] = 0xe2;
202
+ zOut[i++] = 0x80;
203
+ zOut[i++] = 0x9d;
204
+ break;
205
+ case '\'':
206
+ zOut[i++] = 0xe2;
207
+ zOut[i++] = 0x80;
208
+ zOut[i++] = 0x99;
209
+ break;
210
+ default:
211
+ zOut[i++] = c;
212
+ break;
213
+ }
214
+ }
215
+ zOut[i] = 0;
216
+ return (char*)zOut;
217
+}
218
+
147219
148220
/*
149221
** Encode a string for HTTP. This means converting lots of
150222
** characters into the "%HH" where H is a hex digit. It also
151223
** means converting spaces to "+".
@@ -244,11 +316,11 @@
244316
}
245317
246318
/*
247319
** Convert a single HEX digit to an integer
248320
*/
249
-static int AsciiToHex(int c){
321
+int fossil_hexvalue(int c){
250322
if( c>='a' && c<='f' ){
251323
c += 10 - 'a';
252324
}else if( c>='A' && c<='F' ){
253325
c += 10 - 'A';
254326
}else if( c>='0' && c<='9' ){
@@ -272,12 +344,12 @@
272344
i = j = 0;
273345
while( z[i] ){
274346
switch( z[i] ){
275347
case '%':
276348
if( z[i+1] && z[i+2] ){
277
- z[j] = AsciiToHex(z[i+1]) << 4;
278
- z[j] |= AsciiToHex(z[i+2]);
349
+ z[j] = fossil_hexvalue(z[i+1]) << 4;
350
+ z[j] |= fossil_hexvalue(z[i+2]);
279351
i += 2;
280352
}
281353
break;
282354
case '+':
283355
z[j] = ' ';
284356
--- src/encode.c
+++ src/encode.c
@@ -142,10 +142,82 @@
142 }
143 }
144 if( j<i ) blob_append(p, zIn+j, i-j);
145 }
146
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
148 /*
149 ** Encode a string for HTTP. This means converting lots of
150 ** characters into the "%HH" where H is a hex digit. It also
151 ** means converting spaces to "+".
@@ -244,11 +316,11 @@
244 }
245
246 /*
247 ** Convert a single HEX digit to an integer
248 */
249 static int AsciiToHex(int c){
250 if( c>='a' && c<='f' ){
251 c += 10 - 'a';
252 }else if( c>='A' && c<='F' ){
253 c += 10 - 'A';
254 }else if( c>='0' && c<='9' ){
@@ -272,12 +344,12 @@
272 i = j = 0;
273 while( z[i] ){
274 switch( z[i] ){
275 case '%':
276 if( z[i+1] && z[i+2] ){
277 z[j] = AsciiToHex(z[i+1]) << 4;
278 z[j] |= AsciiToHex(z[i+2]);
279 i += 2;
280 }
281 break;
282 case '+':
283 z[j] = ' ';
284
--- src/encode.c
+++ src/encode.c
@@ -142,10 +142,82 @@
142 }
143 }
144 if( j<i ) blob_append(p, zIn+j, i-j);
145 }
146
147 /*
148 ** Make the given string safe for HTML by converting syntax characters
149 ** into alternatives that do not have the special syntactic meaning.
150 **
151 ** < --> U+227a
152 ** > --> U+227b
153 ** & --> +
154 ** " --> U+201d
155 ** ' --> U+2019
156 **
157 ** Return a pointer to a new string obtained from fossil_malloc().
158 */
159 char *html_lookalike(const char *z, int n){
160 unsigned char c;
161 int i = 0;
162 int count = 0;
163 unsigned char *zOut;
164 const unsigned char *zIn = (const unsigned char*)z;
165
166 if( n<0 ) n = strlen(z);
167 while( i<n ){
168 switch( zIn[i] ){
169 case '<': count += 3; break;
170 case '>': count += 3; break;
171 case '"': count += 3; break;
172 case '\'': count += 3; break;
173 case 0: n = i; break;
174 }
175 i++;
176 }
177 i = 0;
178 zOut = fossil_malloc( count+n+1 );
179 if( count==0 ){
180 memcpy(zOut, zIn, n);
181 zOut[n] = 0;
182 return (char*)zOut;
183 }
184 while( n-->0 ){
185 c = *(zIn++);
186 switch( c ){
187 case '<':
188 zOut[i++] = 0xe2;
189 zOut[i++] = 0x89;
190 zOut[i++] = 0xba;
191 break;
192 case '>':
193 zOut[i++] = 0xe2;
194 zOut[i++] = 0x89;
195 zOut[i++] = 0xbb;
196 break;
197 case '&':
198 zOut[i++] = '+';
199 break;
200 case '"':
201 zOut[i++] = 0xe2;
202 zOut[i++] = 0x80;
203 zOut[i++] = 0x9d;
204 break;
205 case '\'':
206 zOut[i++] = 0xe2;
207 zOut[i++] = 0x80;
208 zOut[i++] = 0x99;
209 break;
210 default:
211 zOut[i++] = c;
212 break;
213 }
214 }
215 zOut[i] = 0;
216 return (char*)zOut;
217 }
218
219
220 /*
221 ** Encode a string for HTTP. This means converting lots of
222 ** characters into the "%HH" where H is a hex digit. It also
223 ** means converting spaces to "+".
@@ -244,11 +316,11 @@
316 }
317
318 /*
319 ** Convert a single HEX digit to an integer
320 */
321 int fossil_hexvalue(int c){
322 if( c>='a' && c<='f' ){
323 c += 10 - 'a';
324 }else if( c>='A' && c<='F' ){
325 c += 10 - 'A';
326 }else if( c>='0' && c<='9' ){
@@ -272,12 +344,12 @@
344 i = j = 0;
345 while( z[i] ){
346 switch( z[i] ){
347 case '%':
348 if( z[i+1] && z[i+2] ){
349 z[j] = fossil_hexvalue(z[i+1]) << 4;
350 z[j] |= fossil_hexvalue(z[i+2]);
351 i += 2;
352 }
353 break;
354 case '+':
355 z[j] = ' ';
356
--- src/finfo.c
+++ src/finfo.c
@@ -636,10 +636,12 @@
636636
if( zBr==0 ) zBr = "trunk";
637637
if( uBg ){
638638
zBgClr = user_color(zUser);
639639
}else if( brBg || zBgClr==0 || zBgClr[0]==0 ){
640640
zBgClr = strcmp(zBr,"trunk")==0 ? "" : hash_color(zBr);
641
+ }else if( zBgClr ){
642
+ zBgClr = reasonable_bg_color(zBgClr,0);
641643
}
642644
gidx = graph_add_row(pGraph,
643645
frid>0 ? (GraphRowId)frid*(mxfnid+1)+fnid : fpid+1000000000,
644646
nParent, 0, aParent, zBr, zBgClr,
645647
zUuid, 0);
646648
--- src/finfo.c
+++ src/finfo.c
@@ -636,10 +636,12 @@
636 if( zBr==0 ) zBr = "trunk";
637 if( uBg ){
638 zBgClr = user_color(zUser);
639 }else if( brBg || zBgClr==0 || zBgClr[0]==0 ){
640 zBgClr = strcmp(zBr,"trunk")==0 ? "" : hash_color(zBr);
 
 
641 }
642 gidx = graph_add_row(pGraph,
643 frid>0 ? (GraphRowId)frid*(mxfnid+1)+fnid : fpid+1000000000,
644 nParent, 0, aParent, zBr, zBgClr,
645 zUuid, 0);
646
--- src/finfo.c
+++ src/finfo.c
@@ -636,10 +636,12 @@
636 if( zBr==0 ) zBr = "trunk";
637 if( uBg ){
638 zBgClr = user_color(zUser);
639 }else if( brBg || zBgClr==0 || zBgClr[0]==0 ){
640 zBgClr = strcmp(zBr,"trunk")==0 ? "" : hash_color(zBr);
641 }else if( zBgClr ){
642 zBgClr = reasonable_bg_color(zBgClr,0);
643 }
644 gidx = graph_add_row(pGraph,
645 frid>0 ? (GraphRowId)frid*(mxfnid+1)+fnid : fpid+1000000000,
646 nParent, 0, aParent, zBr, zBgClr,
647 zUuid, 0);
648
+32 -5
--- src/forum.c
+++ src/forum.c
@@ -59,10 +59,11 @@
5959
ForumPost *pFirst; /* First post in chronological order */
6060
ForumPost *pLast; /* Last post in chronological order */
6161
ForumPost *pDisplay; /* Entries in display order */
6262
ForumPost *pTail; /* Last on the display list */
6363
int mxIndent; /* Maximum indentation level */
64
+ int nArtifact; /* Number of forum artifacts in this thread */
6465
};
6566
#endif /* INTERFACE */
6667
6768
/*
6869
** Return true if the forum post with the given rid has been
@@ -109,12 +110,17 @@
109110
** the post.
110111
**
111112
** If bCheckIrt is true then p's thread in-response-to parents are
112113
** checked (recursively) for closure, else only p is checked.
113114
*/
114
-static int forumpost_is_closed(ForumPost *p, int bCheckIrt){
115
- while(p){
115
+static int forumpost_is_closed(
116
+ ForumThread *pThread, /* Thread that the post is a member of */
117
+ ForumPost *p, /* the forum post */
118
+ int bCheckIrt /* True to check In-Reply-To posts */
119
+){
120
+ int mx = pThread->nArtifact+1;
121
+ while( p && (mx--)>0 ){
116122
if( p->pEditHead ) p = p->pEditHead;
117123
if( p->iClosed || !bCheckIrt ) return p->iClosed;
118124
p = p->pIrt;
119125
}
120126
return 0;
@@ -409,10 +415,11 @@
409415
pThread->pFirst = pPost;
410416
}else{
411417
pThread->pLast->pNext = pPost;
412418
}
413419
pThread->pLast = pPost;
420
+ pThread->nArtifact++;
414421
415422
/* Find the in-reply-to post. Default to the topic post if the replied-to
416423
** post cannot be found. */
417424
if( firt ){
418425
pPost->pIrt = pThread->pFirst;
@@ -520,10 +527,11 @@
520527
fossil_fatal("Not a forum post: \"%s\"", zName);
521528
}
522529
fossil_print("fpid = %d\n", fpid);
523530
fossil_print("froot = %d\n", froot);
524531
pThread = forumthread_create(froot, 1);
532
+ fossil_print("count = %d\n", pThread->nArtifact);
525533
fossil_print("Chronological:\n");
526534
fossil_print(
527535
/* 0 1 2 3 4 5 6 7 */
528536
/* 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123 */
529537
" sid rev closed fpid pIrt pEditPrev pEditTail hash\n");
@@ -565,10 +573,11 @@
565573
int froot;
566574
const char *zName = P("name");
567575
ForumThread *pThread;
568576
ForumPost *p;
569577
char *fuuid;
578
+ Stmt q;
570579
571580
login_check_credentials();
572581
if( !g.perm.Admin ){
573582
return;
574583
}
@@ -599,10 +608,27 @@
599608
for(p=pThread->pFirst; p; p=p->pNext){
600609
@ %h(p->zUuid)
601610
}
602611
forumthread_delete(pThread);
603612
@ </pre>
613
+ @ <hr>
614
+ @ <h2>Related FORUMPOST Table Content</h2>
615
+ @ <table border="1" cellpadding="4" cellspacing="0">
616
+ @ <tr><th>fpid<th>froot<th>fprev<th>firt<th>fmtime
617
+ db_prepare(&q, "SELECT fpid, froot, fprev, firt, datetime(fmtime)"
618
+ " FROM forumpost"
619
+ " WHERE froot=%d"
620
+ " ORDER BY fmtime", froot);
621
+ while( db_step(&q)==SQLITE_ROW ){
622
+ @ <tr><td>%d(db_column_int(&q,0))\
623
+ @ <td>%d(db_column_int(&q,1))\
624
+ @ <td>%d(db_column_int(&q,2))\
625
+ @ <td>%d(db_column_int(&q,3))\
626
+ @ <td>%h(db_column_text(&q,4))</tr>
627
+ }
628
+ @ </table>
629
+ db_finalize(&q);
604630
style_finish_page();
605631
}
606632
607633
/*
608634
** Render a forum post for display
@@ -725,10 +751,11 @@
725751
726752
/*
727753
** Display a single post in a forum thread.
728754
*/
729755
static void forum_display_post(
756
+ ForumThread *pThread, /* The thread that this post is a member of */
730757
ForumPost *p, /* Forum post to display */
731758
int iIndentScale, /* Indent scale factor */
732759
int bRaw, /* True to omit the border */
733760
int bUnf, /* True to leave the post unformatted */
734761
int bHist, /* True if showing edit history */
@@ -747,14 +774,14 @@
747774
const char *zMimetype;/* Formatting MIME type */
748775
749776
/* Get the manifest for the post. Abort if not found (e.g. shunned). */
750777
pManifest = manifest_get(p->fpid, CFTYPE_FORUM, 0);
751778
if( !pManifest ) return;
752
- iClosed = forumpost_is_closed(p, 1);
779
+ iClosed = forumpost_is_closed(pThread, p, 1);
753780
/* When not in raw mode, create the border around the post. */
754781
if( !bRaw ){
755
- /* Open the <div> enclosing the post. Set the class string to mark the post
782
+ /* Open the <div> enclosing the post. Set the class string to mark the post
756783
** as selected and/or obsolete. */
757784
iIndent = (p->pEditHead ? p->pEditHead->nIndent : p->nIndent)-1;
758785
@ <div id='forum%d(p->fpid)' class='forumTime\
759786
@ %s(bSelect ? " forumSel" : "")\
760787
@ %s(iClosed ? " forumClosed" : "")\
@@ -1027,11 +1054,11 @@
10271054
}
10281055
10291056
/* Display the appropriate subset of posts in sequence. */
10301057
while( p ){
10311058
/* Display the post. */
1032
- forum_display_post(p, iIndentScale, mode==FD_RAW,
1059
+ forum_display_post(pThread, p, iIndentScale, mode==FD_RAW,
10331060
bUnf, bHist, p==pSelect, zQuery);
10341061
10351062
/* Advance to the next post in the thread. */
10361063
if( mode==FD_CHRONO ){
10371064
/* Chronological mode: display posts (optionally including edits) in their
10381065
--- src/forum.c
+++ src/forum.c
@@ -59,10 +59,11 @@
59 ForumPost *pFirst; /* First post in chronological order */
60 ForumPost *pLast; /* Last post in chronological order */
61 ForumPost *pDisplay; /* Entries in display order */
62 ForumPost *pTail; /* Last on the display list */
63 int mxIndent; /* Maximum indentation level */
 
64 };
65 #endif /* INTERFACE */
66
67 /*
68 ** Return true if the forum post with the given rid has been
@@ -109,12 +110,17 @@
109 ** the post.
110 **
111 ** If bCheckIrt is true then p's thread in-response-to parents are
112 ** checked (recursively) for closure, else only p is checked.
113 */
114 static int forumpost_is_closed(ForumPost *p, int bCheckIrt){
115 while(p){
 
 
 
 
 
116 if( p->pEditHead ) p = p->pEditHead;
117 if( p->iClosed || !bCheckIrt ) return p->iClosed;
118 p = p->pIrt;
119 }
120 return 0;
@@ -409,10 +415,11 @@
409 pThread->pFirst = pPost;
410 }else{
411 pThread->pLast->pNext = pPost;
412 }
413 pThread->pLast = pPost;
 
414
415 /* Find the in-reply-to post. Default to the topic post if the replied-to
416 ** post cannot be found. */
417 if( firt ){
418 pPost->pIrt = pThread->pFirst;
@@ -520,10 +527,11 @@
520 fossil_fatal("Not a forum post: \"%s\"", zName);
521 }
522 fossil_print("fpid = %d\n", fpid);
523 fossil_print("froot = %d\n", froot);
524 pThread = forumthread_create(froot, 1);
 
525 fossil_print("Chronological:\n");
526 fossil_print(
527 /* 0 1 2 3 4 5 6 7 */
528 /* 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123 */
529 " sid rev closed fpid pIrt pEditPrev pEditTail hash\n");
@@ -565,10 +573,11 @@
565 int froot;
566 const char *zName = P("name");
567 ForumThread *pThread;
568 ForumPost *p;
569 char *fuuid;
 
570
571 login_check_credentials();
572 if( !g.perm.Admin ){
573 return;
574 }
@@ -599,10 +608,27 @@
599 for(p=pThread->pFirst; p; p=p->pNext){
600 @ %h(p->zUuid)
601 }
602 forumthread_delete(pThread);
603 @ </pre>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
604 style_finish_page();
605 }
606
607 /*
608 ** Render a forum post for display
@@ -725,10 +751,11 @@
725
726 /*
727 ** Display a single post in a forum thread.
728 */
729 static void forum_display_post(
 
730 ForumPost *p, /* Forum post to display */
731 int iIndentScale, /* Indent scale factor */
732 int bRaw, /* True to omit the border */
733 int bUnf, /* True to leave the post unformatted */
734 int bHist, /* True if showing edit history */
@@ -747,14 +774,14 @@
747 const char *zMimetype;/* Formatting MIME type */
748
749 /* Get the manifest for the post. Abort if not found (e.g. shunned). */
750 pManifest = manifest_get(p->fpid, CFTYPE_FORUM, 0);
751 if( !pManifest ) return;
752 iClosed = forumpost_is_closed(p, 1);
753 /* When not in raw mode, create the border around the post. */
754 if( !bRaw ){
755 /* Open the <div> enclosing the post. Set the class string to mark the post
756 ** as selected and/or obsolete. */
757 iIndent = (p->pEditHead ? p->pEditHead->nIndent : p->nIndent)-1;
758 @ <div id='forum%d(p->fpid)' class='forumTime\
759 @ %s(bSelect ? " forumSel" : "")\
760 @ %s(iClosed ? " forumClosed" : "")\
@@ -1027,11 +1054,11 @@
1027 }
1028
1029 /* Display the appropriate subset of posts in sequence. */
1030 while( p ){
1031 /* Display the post. */
1032 forum_display_post(p, iIndentScale, mode==FD_RAW,
1033 bUnf, bHist, p==pSelect, zQuery);
1034
1035 /* Advance to the next post in the thread. */
1036 if( mode==FD_CHRONO ){
1037 /* Chronological mode: display posts (optionally including edits) in their
1038
--- src/forum.c
+++ src/forum.c
@@ -59,10 +59,11 @@
59 ForumPost *pFirst; /* First post in chronological order */
60 ForumPost *pLast; /* Last post in chronological order */
61 ForumPost *pDisplay; /* Entries in display order */
62 ForumPost *pTail; /* Last on the display list */
63 int mxIndent; /* Maximum indentation level */
64 int nArtifact; /* Number of forum artifacts in this thread */
65 };
66 #endif /* INTERFACE */
67
68 /*
69 ** Return true if the forum post with the given rid has been
@@ -109,12 +110,17 @@
110 ** the post.
111 **
112 ** If bCheckIrt is true then p's thread in-response-to parents are
113 ** checked (recursively) for closure, else only p is checked.
114 */
115 static int forumpost_is_closed(
116 ForumThread *pThread, /* Thread that the post is a member of */
117 ForumPost *p, /* the forum post */
118 int bCheckIrt /* True to check In-Reply-To posts */
119 ){
120 int mx = pThread->nArtifact+1;
121 while( p && (mx--)>0 ){
122 if( p->pEditHead ) p = p->pEditHead;
123 if( p->iClosed || !bCheckIrt ) return p->iClosed;
124 p = p->pIrt;
125 }
126 return 0;
@@ -409,10 +415,11 @@
415 pThread->pFirst = pPost;
416 }else{
417 pThread->pLast->pNext = pPost;
418 }
419 pThread->pLast = pPost;
420 pThread->nArtifact++;
421
422 /* Find the in-reply-to post. Default to the topic post if the replied-to
423 ** post cannot be found. */
424 if( firt ){
425 pPost->pIrt = pThread->pFirst;
@@ -520,10 +527,11 @@
527 fossil_fatal("Not a forum post: \"%s\"", zName);
528 }
529 fossil_print("fpid = %d\n", fpid);
530 fossil_print("froot = %d\n", froot);
531 pThread = forumthread_create(froot, 1);
532 fossil_print("count = %d\n", pThread->nArtifact);
533 fossil_print("Chronological:\n");
534 fossil_print(
535 /* 0 1 2 3 4 5 6 7 */
536 /* 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123 */
537 " sid rev closed fpid pIrt pEditPrev pEditTail hash\n");
@@ -565,10 +573,11 @@
573 int froot;
574 const char *zName = P("name");
575 ForumThread *pThread;
576 ForumPost *p;
577 char *fuuid;
578 Stmt q;
579
580 login_check_credentials();
581 if( !g.perm.Admin ){
582 return;
583 }
@@ -599,10 +608,27 @@
608 for(p=pThread->pFirst; p; p=p->pNext){
609 @ %h(p->zUuid)
610 }
611 forumthread_delete(pThread);
612 @ </pre>
613 @ <hr>
614 @ <h2>Related FORUMPOST Table Content</h2>
615 @ <table border="1" cellpadding="4" cellspacing="0">
616 @ <tr><th>fpid<th>froot<th>fprev<th>firt<th>fmtime
617 db_prepare(&q, "SELECT fpid, froot, fprev, firt, datetime(fmtime)"
618 " FROM forumpost"
619 " WHERE froot=%d"
620 " ORDER BY fmtime", froot);
621 while( db_step(&q)==SQLITE_ROW ){
622 @ <tr><td>%d(db_column_int(&q,0))\
623 @ <td>%d(db_column_int(&q,1))\
624 @ <td>%d(db_column_int(&q,2))\
625 @ <td>%d(db_column_int(&q,3))\
626 @ <td>%h(db_column_text(&q,4))</tr>
627 }
628 @ </table>
629 db_finalize(&q);
630 style_finish_page();
631 }
632
633 /*
634 ** Render a forum post for display
@@ -725,10 +751,11 @@
751
752 /*
753 ** Display a single post in a forum thread.
754 */
755 static void forum_display_post(
756 ForumThread *pThread, /* The thread that this post is a member of */
757 ForumPost *p, /* Forum post to display */
758 int iIndentScale, /* Indent scale factor */
759 int bRaw, /* True to omit the border */
760 int bUnf, /* True to leave the post unformatted */
761 int bHist, /* True if showing edit history */
@@ -747,14 +774,14 @@
774 const char *zMimetype;/* Formatting MIME type */
775
776 /* Get the manifest for the post. Abort if not found (e.g. shunned). */
777 pManifest = manifest_get(p->fpid, CFTYPE_FORUM, 0);
778 if( !pManifest ) return;
779 iClosed = forumpost_is_closed(pThread, p, 1);
780 /* When not in raw mode, create the border around the post. */
781 if( !bRaw ){
782 /* Open the <div> enclosing the post. Set the class string to mark the post
783 ** as selected and/or obsolete. */
784 iIndent = (p->pEditHead ? p->pEditHead->nIndent : p->nIndent)-1;
785 @ <div id='forum%d(p->fpid)' class='forumTime\
786 @ %s(bSelect ? " forumSel" : "")\
787 @ %s(iClosed ? " forumClosed" : "")\
@@ -1027,11 +1054,11 @@
1054 }
1055
1056 /* Display the appropriate subset of posts in sequence. */
1057 while( p ){
1058 /* Display the post. */
1059 forum_display_post(pThread, p, iIndentScale, mode==FD_RAW,
1060 bUnf, bHist, p==pSelect, zQuery);
1061
1062 /* Advance to the next post in the thread. */
1063 if( mode==FD_CHRONO ){
1064 /* Chronological mode: display posts (optionally including edits) in their
1065
--- src/fossil.dom.js
+++ src/fossil.dom.js
@@ -17,16 +17,16 @@
1717
return function(){
1818
return document.createElement(eType);
1919
};
2020
},
2121
remove: function(e){
22
- if(e.forEach){
22
+ if(e?.forEach){
2323
e.forEach(
24
- (x)=>x.parentNode.removeChild(x)
24
+ (x)=>x?.parentNode?.removeChild(x)
2525
);
2626
}else{
27
- e.parentNode.removeChild(e);
27
+ e?.parentNode?.removeChild(e);
2828
}
2929
return e;
3030
},
3131
/**
3232
Removes all child DOM elements from the given element
3333
--- src/fossil.dom.js
+++ src/fossil.dom.js
@@ -17,16 +17,16 @@
17 return function(){
18 return document.createElement(eType);
19 };
20 },
21 remove: function(e){
22 if(e.forEach){
23 e.forEach(
24 (x)=>x.parentNode.removeChild(x)
25 );
26 }else{
27 e.parentNode.removeChild(e);
28 }
29 return e;
30 },
31 /**
32 Removes all child DOM elements from the given element
33
--- src/fossil.dom.js
+++ src/fossil.dom.js
@@ -17,16 +17,16 @@
17 return function(){
18 return document.createElement(eType);
19 };
20 },
21 remove: function(e){
22 if(e?.forEach){
23 e.forEach(
24 (x)=>x?.parentNode?.removeChild(x)
25 );
26 }else{
27 e?.parentNode?.removeChild(e);
28 }
29 return e;
30 },
31 /**
32 Removes all child DOM elements from the given element
33
--- src/fossil.fetch.js
+++ src/fossil.fetch.js
@@ -27,18 +27,51 @@
2727
"this", noting that this call may have amended the options object
2828
with state other than what the caller provided.
2929
3030
- onerror: callback(Error object) (default = output error message
3131
to console.error() and fossil.error()). Triggered if the request
32
- generates any response other than HTTP 200, suffers a connection
33
- error or timeout while awaiting a response, or if the onload()
34
- handler throws an exception. In the context of the callback, the
35
- options object is "this". Note that this function is intended to be
36
- used solely for error reporting, not error recovery. Because
37
- onerror() may be called if onload() throws, it is up to the caller
38
- to ensure that their onerror() callback references only state which
39
- is valid in such a case.
32
+ generates any response other than HTTP 200, or if the beforesend()
33
+ or onload() handler throws an exception. In the context of the
34
+ callback, the options object is "this". This function is intended
35
+ to be used solely for error reporting, not error recovery. Special
36
+ cases for the Error object:
37
+
38
+ 1. Timeouts unfortunately show up as a series of 2 events: an
39
+ HTTP 0 followed immediately by an XHR.ontimeout(). The former
40
+ cannot(?) be unambiguously identified as the trigger for the
41
+ pending timeout, so we have no option but to pass it on as-is
42
+ instead of flagging it as a timeout response. The latter will
43
+ trigger the client-provided ontimeout() if it's available (see
44
+ below), else it calls the onerror() callback. An error object
45
+ passed to ontimeout() by fetch() will have (.name='timeout',
46
+ .status=XHR.status).
47
+
48
+ 2. Else if the response contains a JSON-format exception on the
49
+ server, it will have (.name='json-error',
50
+ status=XHR.status). Any JSON-format result object which has a
51
+ property named "error" is considered to be a server-generated
52
+ error.
53
+
54
+ 3. Else if it gets a non 2xx HTTP code then it will have
55
+ (.name='http',.status=XHR.status).
56
+
57
+ 4. If onerror() throws, the exception is suppressed but may
58
+ generate a console error message.
59
+
60
+ - ontimeout: callback(Error object). If set, timeout errors are
61
+ reported here, else they are reported through onerror().
62
+ Unfortunately, XHR fires two events for a timeout: an
63
+ onreadystatechange() and an ontimeout(), in that order. From the
64
+ former, however, we cannot unambiguously identify the error as
65
+ having been caused by a timeout, so clients which set ontimeout()
66
+ will get _two_ callback calls: one with with an HTTP error response
67
+ followed immediately by an ontimeout() response. Error objects
68
+ passed to this will have (.name='timeout', .status=xhr.HttpStatus).
69
+ In the context of the callback, the options object is "this", Like
70
+ onerror(), any exceptions thrown by the ontimeout() handler are
71
+ suppressed, but may generate a console error message. The onerror()
72
+ handler is _not_ called in this case.
4073
4174
- method: 'POST' | 'GET' (default = 'GET'). CASE SENSITIVE!
4275
4376
- payload: anything acceptable by XHR2.send(ARG) (DOMString,
4477
Document, FormData, Blob, File, ArrayBuffer), or a plain object or
@@ -46,11 +79,12 @@
4679
then the method is automatically set to 'POST'. By default XHR2
4780
will set the content type based on the payload type. If an
4881
object/array is converted to JSON, the contentType option is
4982
automatically set to 'application/json', and if JSON.stringify() of
5083
that value fails then the exception is propagated to this
51
- function's caller.
84
+ function's caller. (beforesend(), aftersend(), and onerror() are
85
+ NOT triggered in that case.)
5286
5387
- contentType: Optional request content type when POSTing. Ignored
5488
if the method is not 'POST'.
5589
5690
- responseType: optional string. One of ("text", "arraybuffer",
@@ -58,10 +92,13 @@
5892
As an extension, it supports "json", which tells it that the
5993
response is expected to be text and that it should be JSON.parse()d
6094
before passing it on to the onload() callback. If parsing of such
6195
an object fails, the onload callback is not called, and the
6296
onerror() callback is passed the exception from the parsing error.
97
+ If the parsed JSON object has an "error" property, it is assumed to
98
+ be an error string, which is used to populate a new Error object,
99
+ which will gets (.name="json") set on it.
63100
64101
- urlParams: string|object. If a string, it is assumed to be a
65102
URI-encoded list of params in the form "key1=val1&key2=val2...",
66103
with NO leading '?'. If it is an object, all of its properties get
67104
converted to that form. Either way, the parameters get appended to
@@ -73,11 +110,11 @@
73110
value of that single header. If it's an array, it's treated as a
74111
list of headers to return, and the 2nd argument is a map of those
75112
header values. When a map is passed on, all of its keys are
76113
lower-cased. When a given header is requested and that header is
77114
set multiple times, their values are (per the XHR docs)
78
- concatenated together with ", " between them.
115
+ concatenated together with "," between them.
79116
80117
- beforesend/aftersend: optional callbacks which are called
81118
without arguments immediately before the request is submitted
82119
and immediately after it is received, regardless of success or
83120
error. In the context of the callback, the options object is
@@ -133,11 +170,11 @@
133170
});
134171
return rc;
135172
};
136173
}
137174
if('/'===uri[0]) uri = uri.substr(1);
138
- if(!opt) opt = {};
175
+ if(!opt) opt = {}/* should arguably be Object.create(null) */;
139176
else if('function'===typeof opt) opt={onload:opt};
140177
if(!opt.onload) opt.onload = f.onload;
141178
if(!opt.onerror) opt.onerror = f.onerror;
142179
if(!opt.beforesend) opt.beforesend = f.beforesend;
143180
if(!opt.aftersend) opt.aftersend = f.aftersend;
@@ -164,15 +201,34 @@
164201
jsonResponse = true;
165202
x.responseType = 'text';
166203
}else{
167204
x.responseType = opt.responseType||'text';
168205
}
169
- x.ontimeout = function(){
206
+ x.ontimeout = function(ev){
170207
try{opt.aftersend()}catch(e){/*ignore*/}
171
- opt.onerror(new Error("XHR timeout of "+x.timeout+"ms expired."));
208
+ const err = new Error("XHR timeout of "+x.timeout+"ms expired.");
209
+ err.status = x.status;
210
+ err.name = 'timeout';
211
+ //console.warn("fetch.ontimeout",ev);
212
+ try{
213
+ (opt.ontimeout || opt.onerror)(err);
214
+ }catch(e){
215
+ /*ignore*/
216
+ console.error("fossil.fetch()'s ontimeout() handler threw",e);
217
+ }
218
+ };
219
+ /* Ensure that if onerror() throws, it's ignored. */
220
+ const origOnError = opt.onerror;
221
+ opt.onerror = (arg)=>{
222
+ try{ origOnError.call(this, arg) }
223
+ catch(e){
224
+ /*ignored*/
225
+ console.error("fossil.fetch()'s onerror() threw",e);
226
+ }
172227
};
173
- x.onreadystatechange = function(){
228
+ x.onreadystatechange = function(ev){
229
+ //console.warn("onreadystatechange", x.readyState, ev.target.responseText);
174230
if(XMLHttpRequest.DONE !== x.readyState) return;
175231
try{opt.aftersend()}catch(e){/*ignore*/}
176232
if(false && 0===x.status){
177233
/* For reasons unknown, we _sometimes_ trigger x.status==0 in FF
178234
when the /chat page starts up, but not in Chrome nor in other
@@ -180,20 +236,37 @@
180236
request is actually sent and it appears to have no
181237
side-effects on the app other than to generate an error
182238
(i.e. no requests/responses are missing). This is a silly
183239
workaround which may or may not bite us later. If so, it can
184240
be removed at the cost of an unsightly console error message
185
- in FF. */
241
+ in FF.
242
+
243
+ 2025-04-10: that behavior is now also in Chrome and enabling
244
+ this workaround causes our timeout errors to never arrive.
245
+ */
186246
return;
187247
}
188248
if(200!==x.status){
249
+ //console.warn("Error response",ev.target);
189250
let err;
190251
try{
191252
const j = JSON.parse(x.response);
192
- if(j.error) err = new Error(j.error);
253
+ if(j.error){
254
+ err = new Error(j.error);
255
+ err.name = 'json.error';
256
+ }
193257
}catch(ex){/*ignore*/}
194
- opt.onerror(err || new Error("HTTP response status "+x.status+"."));
258
+ if( !err ){
259
+ /* We can't tell from here whether this was a timeout-capable
260
+ request which timed out on our end or was one which is a
261
+ genuine error. We also don't know whether the server timed
262
+ out the connection before we did. */
263
+ err = new Error("HTTP response status "+x.status+".")
264
+ err.name = 'http';
265
+ }
266
+ err.status = x.status;
267
+ opt.onerror(err);
195268
return;
196269
}
197270
const orh = opt.responseHeaders;
198271
let head;
199272
if(true===orh){
@@ -209,17 +282,17 @@
209282
try{
210283
const args = [(jsonResponse && x.response)
211284
? JSON.parse(x.response) : x.response];
212285
if(head) args.push(head);
213286
opt.onload.apply(opt, args);
214
- }catch(e){
215
- opt.onerror(e);
287
+ }catch(err){
288
+ opt.onerror(err);
216289
}
217
- };
290
+ }/*onreadystatechange()*/;
218291
try{opt.beforesend()}
219
- catch(e){
220
- opt.onerror(e);
292
+ catch(err){
293
+ opt.onerror(err);
221294
return;
222295
}
223296
x.open(opt.method||'GET', url.join(''), true);
224297
if('POST'===opt.method && 'string'===typeof opt.contentType){
225298
x.setRequestHeader('Content-Type',opt.contentType);
226299
--- src/fossil.fetch.js
+++ src/fossil.fetch.js
@@ -27,18 +27,51 @@
27 "this", noting that this call may have amended the options object
28 with state other than what the caller provided.
29
30 - onerror: callback(Error object) (default = output error message
31 to console.error() and fossil.error()). Triggered if the request
32 generates any response other than HTTP 200, suffers a connection
33 error or timeout while awaiting a response, or if the onload()
34 handler throws an exception. In the context of the callback, the
35 options object is "this". Note that this function is intended to be
36 used solely for error reporting, not error recovery. Because
37 onerror() may be called if onload() throws, it is up to the caller
38 to ensure that their onerror() callback references only state which
39 is valid in such a case.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
41 - method: 'POST' | 'GET' (default = 'GET'). CASE SENSITIVE!
42
43 - payload: anything acceptable by XHR2.send(ARG) (DOMString,
44 Document, FormData, Blob, File, ArrayBuffer), or a plain object or
@@ -46,11 +79,12 @@
46 then the method is automatically set to 'POST'. By default XHR2
47 will set the content type based on the payload type. If an
48 object/array is converted to JSON, the contentType option is
49 automatically set to 'application/json', and if JSON.stringify() of
50 that value fails then the exception is propagated to this
51 function's caller.
 
52
53 - contentType: Optional request content type when POSTing. Ignored
54 if the method is not 'POST'.
55
56 - responseType: optional string. One of ("text", "arraybuffer",
@@ -58,10 +92,13 @@
58 As an extension, it supports "json", which tells it that the
59 response is expected to be text and that it should be JSON.parse()d
60 before passing it on to the onload() callback. If parsing of such
61 an object fails, the onload callback is not called, and the
62 onerror() callback is passed the exception from the parsing error.
 
 
 
63
64 - urlParams: string|object. If a string, it is assumed to be a
65 URI-encoded list of params in the form "key1=val1&key2=val2...",
66 with NO leading '?'. If it is an object, all of its properties get
67 converted to that form. Either way, the parameters get appended to
@@ -73,11 +110,11 @@
73 value of that single header. If it's an array, it's treated as a
74 list of headers to return, and the 2nd argument is a map of those
75 header values. When a map is passed on, all of its keys are
76 lower-cased. When a given header is requested and that header is
77 set multiple times, their values are (per the XHR docs)
78 concatenated together with ", " between them.
79
80 - beforesend/aftersend: optional callbacks which are called
81 without arguments immediately before the request is submitted
82 and immediately after it is received, regardless of success or
83 error. In the context of the callback, the options object is
@@ -133,11 +170,11 @@
133 });
134 return rc;
135 };
136 }
137 if('/'===uri[0]) uri = uri.substr(1);
138 if(!opt) opt = {};
139 else if('function'===typeof opt) opt={onload:opt};
140 if(!opt.onload) opt.onload = f.onload;
141 if(!opt.onerror) opt.onerror = f.onerror;
142 if(!opt.beforesend) opt.beforesend = f.beforesend;
143 if(!opt.aftersend) opt.aftersend = f.aftersend;
@@ -164,15 +201,34 @@
164 jsonResponse = true;
165 x.responseType = 'text';
166 }else{
167 x.responseType = opt.responseType||'text';
168 }
169 x.ontimeout = function(){
170 try{opt.aftersend()}catch(e){/*ignore*/}
171 opt.onerror(new Error("XHR timeout of "+x.timeout+"ms expired."));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172 };
173 x.onreadystatechange = function(){
 
174 if(XMLHttpRequest.DONE !== x.readyState) return;
175 try{opt.aftersend()}catch(e){/*ignore*/}
176 if(false && 0===x.status){
177 /* For reasons unknown, we _sometimes_ trigger x.status==0 in FF
178 when the /chat page starts up, but not in Chrome nor in other
@@ -180,20 +236,37 @@
180 request is actually sent and it appears to have no
181 side-effects on the app other than to generate an error
182 (i.e. no requests/responses are missing). This is a silly
183 workaround which may or may not bite us later. If so, it can
184 be removed at the cost of an unsightly console error message
185 in FF. */
 
 
 
 
186 return;
187 }
188 if(200!==x.status){
 
189 let err;
190 try{
191 const j = JSON.parse(x.response);
192 if(j.error) err = new Error(j.error);
 
 
 
193 }catch(ex){/*ignore*/}
194 opt.onerror(err || new Error("HTTP response status "+x.status+"."));
 
 
 
 
 
 
 
 
 
195 return;
196 }
197 const orh = opt.responseHeaders;
198 let head;
199 if(true===orh){
@@ -209,17 +282,17 @@
209 try{
210 const args = [(jsonResponse && x.response)
211 ? JSON.parse(x.response) : x.response];
212 if(head) args.push(head);
213 opt.onload.apply(opt, args);
214 }catch(e){
215 opt.onerror(e);
216 }
217 };
218 try{opt.beforesend()}
219 catch(e){
220 opt.onerror(e);
221 return;
222 }
223 x.open(opt.method||'GET', url.join(''), true);
224 if('POST'===opt.method && 'string'===typeof opt.contentType){
225 x.setRequestHeader('Content-Type',opt.contentType);
226
--- src/fossil.fetch.js
+++ src/fossil.fetch.js
@@ -27,18 +27,51 @@
27 "this", noting that this call may have amended the options object
28 with state other than what the caller provided.
29
30 - onerror: callback(Error object) (default = output error message
31 to console.error() and fossil.error()). Triggered if the request
32 generates any response other than HTTP 200, or if the beforesend()
33 or onload() handler throws an exception. In the context of the
34 callback, the options object is "this". This function is intended
35 to be used solely for error reporting, not error recovery. Special
36 cases for the Error object:
37
38 1. Timeouts unfortunately show up as a series of 2 events: an
39 HTTP 0 followed immediately by an XHR.ontimeout(). The former
40 cannot(?) be unambiguously identified as the trigger for the
41 pending timeout, so we have no option but to pass it on as-is
42 instead of flagging it as a timeout response. The latter will
43 trigger the client-provided ontimeout() if it's available (see
44 below), else it calls the onerror() callback. An error object
45 passed to ontimeout() by fetch() will have (.name='timeout',
46 .status=XHR.status).
47
48 2. Else if the response contains a JSON-format exception on the
49 server, it will have (.name='json-error',
50 status=XHR.status). Any JSON-format result object which has a
51 property named "error" is considered to be a server-generated
52 error.
53
54 3. Else if it gets a non 2xx HTTP code then it will have
55 (.name='http',.status=XHR.status).
56
57 4. If onerror() throws, the exception is suppressed but may
58 generate a console error message.
59
60 - ontimeout: callback(Error object). If set, timeout errors are
61 reported here, else they are reported through onerror().
62 Unfortunately, XHR fires two events for a timeout: an
63 onreadystatechange() and an ontimeout(), in that order. From the
64 former, however, we cannot unambiguously identify the error as
65 having been caused by a timeout, so clients which set ontimeout()
66 will get _two_ callback calls: one with with an HTTP error response
67 followed immediately by an ontimeout() response. Error objects
68 passed to this will have (.name='timeout', .status=xhr.HttpStatus).
69 In the context of the callback, the options object is "this", Like
70 onerror(), any exceptions thrown by the ontimeout() handler are
71 suppressed, but may generate a console error message. The onerror()
72 handler is _not_ called in this case.
73
74 - method: 'POST' | 'GET' (default = 'GET'). CASE SENSITIVE!
75
76 - payload: anything acceptable by XHR2.send(ARG) (DOMString,
77 Document, FormData, Blob, File, ArrayBuffer), or a plain object or
@@ -46,11 +79,12 @@
79 then the method is automatically set to 'POST'. By default XHR2
80 will set the content type based on the payload type. If an
81 object/array is converted to JSON, the contentType option is
82 automatically set to 'application/json', and if JSON.stringify() of
83 that value fails then the exception is propagated to this
84 function's caller. (beforesend(), aftersend(), and onerror() are
85 NOT triggered in that case.)
86
87 - contentType: Optional request content type when POSTing. Ignored
88 if the method is not 'POST'.
89
90 - responseType: optional string. One of ("text", "arraybuffer",
@@ -58,10 +92,13 @@
92 As an extension, it supports "json", which tells it that the
93 response is expected to be text and that it should be JSON.parse()d
94 before passing it on to the onload() callback. If parsing of such
95 an object fails, the onload callback is not called, and the
96 onerror() callback is passed the exception from the parsing error.
97 If the parsed JSON object has an "error" property, it is assumed to
98 be an error string, which is used to populate a new Error object,
99 which will gets (.name="json") set on it.
100
101 - urlParams: string|object. If a string, it is assumed to be a
102 URI-encoded list of params in the form "key1=val1&key2=val2...",
103 with NO leading '?'. If it is an object, all of its properties get
104 converted to that form. Either way, the parameters get appended to
@@ -73,11 +110,11 @@
110 value of that single header. If it's an array, it's treated as a
111 list of headers to return, and the 2nd argument is a map of those
112 header values. When a map is passed on, all of its keys are
113 lower-cased. When a given header is requested and that header is
114 set multiple times, their values are (per the XHR docs)
115 concatenated together with "," between them.
116
117 - beforesend/aftersend: optional callbacks which are called
118 without arguments immediately before the request is submitted
119 and immediately after it is received, regardless of success or
120 error. In the context of the callback, the options object is
@@ -133,11 +170,11 @@
170 });
171 return rc;
172 };
173 }
174 if('/'===uri[0]) uri = uri.substr(1);
175 if(!opt) opt = {}/* should arguably be Object.create(null) */;
176 else if('function'===typeof opt) opt={onload:opt};
177 if(!opt.onload) opt.onload = f.onload;
178 if(!opt.onerror) opt.onerror = f.onerror;
179 if(!opt.beforesend) opt.beforesend = f.beforesend;
180 if(!opt.aftersend) opt.aftersend = f.aftersend;
@@ -164,15 +201,34 @@
201 jsonResponse = true;
202 x.responseType = 'text';
203 }else{
204 x.responseType = opt.responseType||'text';
205 }
206 x.ontimeout = function(ev){
207 try{opt.aftersend()}catch(e){/*ignore*/}
208 const err = new Error("XHR timeout of "+x.timeout+"ms expired.");
209 err.status = x.status;
210 err.name = 'timeout';
211 //console.warn("fetch.ontimeout",ev);
212 try{
213 (opt.ontimeout || opt.onerror)(err);
214 }catch(e){
215 /*ignore*/
216 console.error("fossil.fetch()'s ontimeout() handler threw",e);
217 }
218 };
219 /* Ensure that if onerror() throws, it's ignored. */
220 const origOnError = opt.onerror;
221 opt.onerror = (arg)=>{
222 try{ origOnError.call(this, arg) }
223 catch(e){
224 /*ignored*/
225 console.error("fossil.fetch()'s onerror() threw",e);
226 }
227 };
228 x.onreadystatechange = function(ev){
229 //console.warn("onreadystatechange", x.readyState, ev.target.responseText);
230 if(XMLHttpRequest.DONE !== x.readyState) return;
231 try{opt.aftersend()}catch(e){/*ignore*/}
232 if(false && 0===x.status){
233 /* For reasons unknown, we _sometimes_ trigger x.status==0 in FF
234 when the /chat page starts up, but not in Chrome nor in other
@@ -180,20 +236,37 @@
236 request is actually sent and it appears to have no
237 side-effects on the app other than to generate an error
238 (i.e. no requests/responses are missing). This is a silly
239 workaround which may or may not bite us later. If so, it can
240 be removed at the cost of an unsightly console error message
241 in FF.
242
243 2025-04-10: that behavior is now also in Chrome and enabling
244 this workaround causes our timeout errors to never arrive.
245 */
246 return;
247 }
248 if(200!==x.status){
249 //console.warn("Error response",ev.target);
250 let err;
251 try{
252 const j = JSON.parse(x.response);
253 if(j.error){
254 err = new Error(j.error);
255 err.name = 'json.error';
256 }
257 }catch(ex){/*ignore*/}
258 if( !err ){
259 /* We can't tell from here whether this was a timeout-capable
260 request which timed out on our end or was one which is a
261 genuine error. We also don't know whether the server timed
262 out the connection before we did. */
263 err = new Error("HTTP response status "+x.status+".")
264 err.name = 'http';
265 }
266 err.status = x.status;
267 opt.onerror(err);
268 return;
269 }
270 const orh = opt.responseHeaders;
271 let head;
272 if(true===orh){
@@ -209,17 +282,17 @@
282 try{
283 const args = [(jsonResponse && x.response)
284 ? JSON.parse(x.response) : x.response];
285 if(head) args.push(head);
286 opt.onload.apply(opt, args);
287 }catch(err){
288 opt.onerror(err);
289 }
290 }/*onreadystatechange()*/;
291 try{opt.beforesend()}
292 catch(err){
293 opt.onerror(err);
294 return;
295 }
296 x.open(opt.method||'GET', url.join(''), true);
297 if('POST'===opt.method && 'string'===typeof opt.contentType){
298 x.setRequestHeader('Content-Type',opt.contentType);
299
--- src/fossil.page.chat.js
+++ src/fossil.page.chat.js
@@ -1,6 +1,6 @@
1
-/**
1
+-/**
22
This file contains the client-side implementation of fossil's /chat
33
application.
44
*/
55
window.fossil.onPageLoad(function(){
66
const F = window.fossil, D = F.dom;
@@ -129,19 +129,21 @@
129129
return resized;
130130
})();
131131
fossil.FRK = ForceResizeKludge/*for debugging*/;
132132
const Chat = ForceResizeKludge.chat = (function(){
133133
const cs = { // the "Chat" object (result of this function)
134
- verboseErrors: false /* if true then certain, mostly extraneous,
135
- error messages may be sent to the console. */,
134
+ beVerbose: false
135
+ //!!window.location.hostname.match("localhost")
136
+ /* if true then certain, mostly extraneous, error messages and
137
+ log messages may be sent to the console. */,
136138
playedBeep: false /* used for the beep-once setting */,
137139
e:{/*map of certain DOM elements.*/
138140
messageInjectPoint: E1('#message-inject-point'),
139141
pageTitle: E1('head title'),
140142
loadOlderToolbar: undefined /* the load-posts toolbar (dynamically created) */,
141
- inputWrapper: E1("#chat-input-area"),
142
- inputElementWrapper: E1('#chat-input-line-wrapper'),
143
+ inputArea: E1("#chat-input-area"),
144
+ inputLineWrapper: E1('#chat-input-line-wrapper'),
143145
fileSelectWrapper: E1('#chat-input-file-area'),
144146
viewMessages: E1('#chat-messages-wrapper'),
145147
btnSubmit: E1('#chat-button-submit'),
146148
btnAttach: E1('#chat-button-attach'),
147149
inputX: E1('#chat-input-field-x'),
@@ -155,11 +157,13 @@
155157
viewSearch: E1('#chat-search'),
156158
searchContent: E1('#chat-search-content'),
157159
btnPreview: E1('#chat-button-preview'),
158160
views: document.querySelectorAll('.chat-view'),
159161
activeUserListWrapper: E1('#chat-user-list-wrapper'),
160
- activeUserList: E1('#chat-user-list')
162
+ activeUserList: E1('#chat-user-list'),
163
+ eMsgPollError: undefined /* current connection error MessageMidget */,
164
+ pollErrorMarker: document.body /* element to toggle 'connection-error' CSS class on */
161165
},
162166
me: F.user.name,
163167
mxMsg: F.config.chat.initSize ? -F.config.chat.initSize : -50,
164168
mnMsg: undefined/*lowest message ID we've seen so far (for history loading)*/,
165169
pageIsActive: 'visible'===document.visibilityState,
@@ -179,10 +183,105 @@
179183
filterState:{
180184
activeUser: undefined,
181185
match: function(uname){
182186
return this.activeUser===uname || !this.activeUser;
183187
}
188
+ },
189
+ /**
190
+ The timer object is used to control connection throttling
191
+ when connection errors arrise. It starts off with a polling
192
+ delay of $initialDelay ms. If there's a connection error,
193
+ that gets bumped by some value for each subsequent error, up
194
+ to some max value.
195
+
196
+ The timing of resetting the delay when service returns is,
197
+ because of the long-poll connection and our lack of low-level
198
+ insight into the connection at this level, a bit wonky.
199
+ */
200
+ timer:{
201
+ /* setTimeout() ID for (delayed) starting a Chat.poll(), so
202
+ that it runs at controlled intervals (which change when a
203
+ connection drops and recovers). */
204
+ tidPendingPoll: undefined,
205
+ tidClearPollErr: undefined /*setTimeout() timer id for
206
+ reconnection determination. See
207
+ clearPollErrOnWait(). */,
208
+ $initialDelay: 1000 /* initial polling interval (ms) */,
209
+ currentDelay: 1000 /* current polling interval */,
210
+ maxDelay: 60000 * 5 /* max interval when backing off for
211
+ connection errors */,
212
+ minDelay: 5000 /* minimum delay time for a back-off/retry
213
+ attempt. */,
214
+ errCount: 0 /* Current poller connection error count */,
215
+ minErrForNotify: 4 /* Don't warn for connection errors until this
216
+ many have occurred */,
217
+ pollTimeout: (1 && window.location.hostname.match(
218
+ "localhost" /*presumably local dev mode*/
219
+ )) ? 15000
220
+ : (+F.config.chat.pollTimeout>0
221
+ ? (1000 * (F.config.chat.pollTimeout - Math.floor(F.config.chat.pollTimeout * 0.1)))
222
+ /* ^^^^^^^^^^^^ we want our timeouts to be slightly shorter
223
+ than the server's so that we can distingished timed-out
224
+ polls on our end from HTTP errors (if the server times
225
+ out). */
226
+ : 30000),
227
+ /** Returns a random fudge value for reconnect attempt times,
228
+ intended to keep the /chat server from getting hammered if
229
+ all clients which were just disconnected all reconnect at
230
+ the same instant. */
231
+ randomInterval: function(factor){
232
+ return Math.floor(Math.random() * factor);
233
+ },
234
+ /** Increments the reconnection delay, within some min/max range. */
235
+ incrDelay: function(){
236
+ if( this.maxDelay > this.currentDelay ){
237
+ if(this.currentDelay < this.minDelay){
238
+ this.currentDelay = this.minDelay + this.randomInterval(this.minDelay);
239
+ }else{
240
+ this.currentDelay = this.currentDelay*2 + this.randomInterval(this.currentDelay);
241
+ }
242
+ }
243
+ return this.currentDelay;
244
+ },
245
+ /** Resets the delay counter to v || its initial value. */
246
+ resetDelay: function(ms=0){
247
+ return this.currentDelay = ms || this.$initialDelay;
248
+ },
249
+ /** Returns true if the timer is set to delayed mode. */
250
+ isDelayed: function(){
251
+ return (this.currentDelay > this.$initialDelay) ? this.currentDelay : 0;
252
+ },
253
+ /**
254
+ Cancels any in-progress pending-poll timer and starts a new
255
+ one with the given delay, defaulting to this.resetDelay().
256
+ */
257
+ startPendingPollTimer: function(delay){
258
+ this.cancelPendingPollTimer().tidPendingPoll
259
+ = setTimeout( Chat.poll, delay || Chat.timer.resetDelay() );
260
+ return this;
261
+ },
262
+ /**
263
+ Cancels any still-active timer set to trigger the next
264
+ Chat.poll().
265
+ */
266
+ cancelPendingPollTimer: function(){
267
+ if( this.tidPendingPoll ){
268
+ clearTimeout(this.tidPendingPoll);
269
+ this.tidPendingPoll = 0;
270
+ }
271
+ return this;
272
+ },
273
+ /**
274
+ Cancels any pending reconnection attempt back-off timer..
275
+ */
276
+ cancelReconnectCheckTimer: function(){
277
+ if( this.tidClearPollErr ){
278
+ clearTimeout(this.tidClearPollErr);
279
+ this.tidClearPollErr = 0;
280
+ }
281
+ return this;
282
+ }
184283
},
185284
/**
186285
Gets (no args) or sets (1 arg) the current input text field
187286
value, taking into account single- vs multi-line input. The
188287
getter returns a trim()'d string and the setter returns this
@@ -606,19 +705,19 @@
606705
607706
/**
608707
If animations are enabled, passes its arguments
609708
to D.addClassBriefly(), else this is a no-op.
610709
If cb is a function, it is called after the
611
- CSS class is removed. Returns this object;
710
+ CSS class is removed. Returns this object;
612711
*/
613712
animate: function f(e,a,cb){
614713
if(!f.$disabled){
615714
D.addClassBriefly(e, a, 0, cb);
616715
}
617716
return this;
618717
}
619
- };
718
+ }/*Chat object*/;
620719
cs.e.inputFields = [ cs.e.input1, cs.e.inputM, cs.e.inputX ];
621720
cs.e.inputFields.$currentIndex = 0;
622721
cs.e.inputFields.forEach(function(e,ndx){
623722
if(ndx===cs.e.inputFields.$currentIndex) D.removeClass(e,'hidden');
624723
else D.addClass(e,'hidden');
@@ -645,33 +744,59 @@
645744
cs.reportError = function(/*msg args*/){
646745
const args = argsToArray(arguments);
647746
console.error("chat error:",args);
648747
F.toast.error.apply(F.toast, args);
649748
};
749
+
750
+ let InternalMsgId = 0;
650751
/**
651752
Reports an error in the form of a new message in the chat
652753
feed. All arguments are appended to the message's content area
653754
using fossil.dom.append(), so may be of any type supported by
654755
that function.
655756
*/
656757
cs.reportErrorAsMessage = function f(/*msg args*/){
657
- if(undefined === f.$msgid) f.$msgid=0;
658758
const args = argsToArray(arguments).map(function(v){
659759
return (v instanceof Error) ? v.message : v;
660760
});
661
- console.error("chat error:",args);
761
+ if(Chat.beVerbose){
762
+ console.error("chat error:",args);
763
+ }
662764
const d = new Date().toISOString(),
663765
mw = new this.MessageWidget({
664766
isError: true,
665
- xfrom: null,
666
- msgid: "error-"+(++f.$msgid),
767
+ xfrom: undefined,
768
+ msgid: "error-"+(++InternalMsgId),
769
+ mtime: d,
770
+ lmtime: d,
771
+ xmsg: args
772
+ });
773
+ this.injectMessageElem(mw.e.body);
774
+ mw.scrollIntoView();
775
+ return mw;
776
+ };
777
+
778
+ /**
779
+ For use by the connection poller to send a "connection
780
+ restored" message.
781
+ */
782
+ cs.reportReconnection = function f(/*msg args*/){
783
+ const args = argsToArray(arguments).map(function(v){
784
+ return (v instanceof Error) ? v.message : v;
785
+ });
786
+ const d = new Date().toISOString(),
787
+ mw = new this.MessageWidget({
788
+ isError: false,
789
+ xfrom: undefined,
790
+ msgid: "reconnect-"+(++InternalMsgId),
667791
mtime: d,
668792
lmtime: d,
669793
xmsg: args
670794
});
671795
this.injectMessageElem(mw.e.body);
672796
mw.scrollIntoView();
797
+ return mw;
673798
};
674799
675800
cs.getMessageElemById = function(id){
676801
return qs('[data-msgid="'+id+'"]');
677802
};
@@ -690,24 +815,40 @@
690815
/**
691816
LOCALLY deletes a message element by the message ID or passing
692817
the .message-row element. Returns true if it removes an element,
693818
else false.
694819
*/
695
- cs.deleteMessageElem = function(id){
820
+ cs.deleteMessageElem = function(id, silent){
696821
var e;
697822
if(id instanceof HTMLElement){
698823
e = id;
699824
id = e.dataset.msgid;
700
- }else{
825
+ delete e.dataset.msgid;
826
+ if( e?.dataset?.alsoRemove ){
827
+ const xId = e.dataset.alsoRemove;
828
+ delete e.dataset.alsoRemove;
829
+ this.deleteMessageElem( xId );
830
+ }
831
+ }else if(id instanceof Chat.MessageWidget) {
832
+ if( this.e.eMsgPollError === e ){
833
+ this.e.eMsgPollError = undefined;
834
+ }
835
+ if(id.e?.body){
836
+ this.deleteMessageElem(id.e.body);
837
+ }
838
+ return;
839
+ } else{
701840
e = this.getMessageElemById(id);
702841
}
703842
if(e && id){
704843
D.remove(e);
705844
if(e===this.e.newestMessage){
706845
this.fetchLastMessageElem();
707846
}
708
- F.toast.message("Deleted message "+id+".");
847
+ if( !silent ){
848
+ F.toast.message("Deleted message "+id+".");
849
+ }
709850
}
710851
return !!e;
711852
};
712853
713854
/**
@@ -776,10 +917,11 @@
776917
const self = this;
777918
F.fetch('chat-fetch-one',{
778919
urlParams:{ name: id, raw: true},
779920
responseType: 'json',
780921
onload: function(msg){
922
+ reportConnectionOkay('chat-fetch-one');
781923
content.$elems[1] = D.append(D.pre(),msg.xmsg);
782924
content.$elems[1]._xmsgRaw = msg.xmsg/*used for copy-to-clipboard feature*/;
783925
self.toggleTextMode(e);
784926
},
785927
aftersend:function(){
@@ -834,14 +976,16 @@
834976
}else{
835977
e = this.getMessageElemById(id);
836978
}
837979
if(!(e instanceof HTMLElement)) return;
838980
if(this.userMayDelete(e)){
839
- this.ajaxStart();
840981
F.fetch("chat-delete/" + id, {
841982
responseType: 'json',
842
- onload:(r)=>this.deleteMessageElem(r),
983
+ onload:(r)=>{
984
+ reportConnectionOkay('chat-delete');
985
+ this.deleteMessageElem(r);
986
+ },
843987
onerror:(err)=>this.reportErrorAsMessage(err)
844988
});
845989
}else{
846990
this.deleteMessageElem(id);
847991
}
@@ -1035,10 +1179,11 @@
10351179
10361180
ctor.prototype = {
10371181
scrollIntoView: function(){
10381182
this.e.content.scrollIntoView();
10391183
},
1184
+ //remove: function(silent){Chat.deleteMessageElem(this, silent);},
10401185
setMessage: function(m){
10411186
const ds = this.e.body.dataset;
10421187
ds.timestamp = m.mtime;
10431188
ds.lmtime = m.lmtime;
10441189
ds.msgid = m.msgid;
@@ -1212,12 +1357,22 @@
12121357
const btnDeleteLocal = D.button("Delete locally");
12131358
D.append(toolbar, btnDeleteLocal);
12141359
const self = this;
12151360
btnDeleteLocal.addEventListener('click', function(){
12161361
self.hide();
1217
- Chat.deleteMessageElem(eMsg);
1362
+ Chat.deleteMessageElem(eMsg)
12181363
});
1364
+ if( eMsg.classList.contains('notification') ){
1365
+ const btnDeletePoll = D.button("Delete /chat notifications?");
1366
+ D.append(toolbar, btnDeletePoll);
1367
+ btnDeletePoll.addEventListener('click', function(){
1368
+ self.hide();
1369
+ Chat.e.viewMessages.querySelectorAll(
1370
+ '.message-widget.notification:not(.resend-message)'
1371
+ ).forEach(e=>Chat.deleteMessageElem(e, true));
1372
+ });
1373
+ }
12191374
if(Chat.userMayDelete(eMsg)){
12201375
const btnDeleteGlobal = D.button("Delete globally");
12211376
D.append(toolbar, btnDeleteGlobal);
12221377
F.confirmer(btnDeleteGlobal,{
12231378
pinSize: true,
@@ -1457,10 +1612,11 @@
14571612
n: nFetch,
14581613
i: iFirst
14591614
},
14601615
responseType: "json",
14611616
onload:function(jx){
1617
+ reportConnectionOkay('chat-query');
14621618
if( bDown ) jx.msgs.reverse();
14631619
jx.msgs.forEach((m) => {
14641620
m.isSearchResult = true;
14651621
var mw = new Chat.MessageWidget(m);
14661622
if( bDown ){
@@ -1524,11 +1680,11 @@
15241680
reader.onload = (e)=>img.setAttribute('src', e.target.result);
15251681
reader.readAsDataURL(blob);
15261682
}
15271683
};
15281684
Chat.e.inputFile.addEventListener('change', function(ev){
1529
- updateDropZoneContent(this.files && this.files[0] ? this.files[0] : undefined)
1685
+ updateDropZoneContent(this?.files[0])
15301686
});
15311687
/* Handle image paste from clipboard. TODO: figure out how we can
15321688
paste non-image binary data as if it had been selected via the
15331689
file selection element. */
15341690
const pasteListener = function(event){
@@ -1604,10 +1760,11 @@
16041760
D.span(),"This message was not successfully sent to the server:"
16051761
));
16061762
if(state.msg){
16071763
const ta = D.textarea();
16081764
ta.value = state.msg;
1765
+ ta.setAttribute('readonly','true');
16091766
D.append(w,ta);
16101767
}
16111768
if(state.blob){
16121769
D.append(w,D.append(D.span(),"Attachment: ",(state.blob.name||"unnamed")));
16131770
//console.debug("blob = ",state.blob);
@@ -1622,11 +1779,46 @@
16221779
if(state.msg) Chat.inputValue(state.msg);
16231780
if(state.blob) BlobXferState.updateDropZoneContent(state.blob);
16241781
const theMsg = findMessageWidgetParent(w);
16251782
if(theMsg) Chat.deleteMessageElem(theMsg);
16261783
}));
1627
- Chat.reportErrorAsMessage(w);
1784
+ D.addClass(Chat.reportErrorAsMessage(w).e.body, "resend-message");
1785
+ };
1786
+
1787
+ /* Assume the connection has been established, reset the
1788
+ Chat.timer.tidClearPollErr, and (if showMsg and
1789
+ !!Chat.e.eMsgPollError) alert the user that the outage appears to
1790
+ be over. Also schedule Chat.poll() to run in the very near
1791
+ future. */
1792
+ const reportConnectionOkay = function(dbgContext, showMsg = true){
1793
+ if(Chat.beVerbose){
1794
+ console.warn('reportConnectionOkay', dbgContext,
1795
+ 'Chat.e.pollErrorMarker classes =',
1796
+ Chat.e.pollErrorMarker.classList,
1797
+ 'Chat.timer.tidClearPollErr =',Chat.timer.tidClearPollErr,
1798
+ 'Chat.timer =',Chat.timer);
1799
+ }
1800
+ if( Chat.timer.errCount ){
1801
+ D.removeClass(Chat.e.pollErrorMarker, 'connection-error');
1802
+ Chat.timer.errCount = 0;
1803
+ }
1804
+ Chat.timer.cancelReconnectCheckTimer().startPendingPollTimer();
1805
+ if( Chat.e.eMsgPollError ) {
1806
+ const oldErrMsg = Chat.e.eMsgPollError;
1807
+ Chat.e.eMsgPollError = undefined;
1808
+ if( showMsg ){
1809
+ if(Chat.beVerbose){
1810
+ console.log("Poller Connection restored.");
1811
+ }
1812
+ const m = Chat.reportReconnection("Poller connection restored.");
1813
+ if( oldErrMsg ){
1814
+ D.remove(oldErrMsg.e?.body.querySelector('button.retry-now'));
1815
+ }
1816
+ m.e.body.dataset.alsoRemove = oldErrMsg?.e?.body?.dataset?.msgid;
1817
+ D.addClass(m.e.body,'poller-connection');
1818
+ }
1819
+ }
16281820
};
16291821
16301822
/**
16311823
Submits the contents of the message input field (if not empty)
16321824
and/or the file attachment field to the server. If both are
@@ -1686,10 +1878,11 @@
16861878
onerror:function(err){
16871879
self.reportErrorAsMessage(err);
16881880
recoverFailedMessage(fallback);
16891881
},
16901882
onload:function(txt){
1883
+ reportConnectionOkay('chat-send');
16911884
if(!txt) return/*success response*/;
16921885
try{
16931886
const json = JSON.parse(txt);
16941887
self.newContent({msgs:[json]});
16951888
}catch(e){
@@ -2126,11 +2319,11 @@
21262319
Chat.e.inputFields.$currentIndex = a[2];
21272320
Chat.inputValue(v);
21282321
D.removeClass(a[0], 'hidden');
21292322
D.addClass(a[1], 'hidden');
21302323
}
2131
- Chat.e.inputElementWrapper.classList[
2324
+ Chat.e.inputLineWrapper.classList[
21322325
s.value ? 'add' : 'remove'
21332326
]('compact');
21342327
Chat.e.inputFields[Chat.e.inputFields.$currentIndex].focus();
21352328
});
21362329
Chat.settings.addListener('edit-ctrl-send',function(s){
@@ -2185,10 +2378,11 @@
21852378
/*filename needed for mimetype determination*/);
21862379
fd.append('render_mode',F.page.previewModes.wiki);
21872380
F.fetch('ajax/preview-text',{
21882381
payload: fd,
21892382
onload: function(html){
2383
+ reportConnectionOkay('ajax/preview-text');
21902384
Chat.setPreviewText(html);
21912385
F.pikchr.addSrcView(Chat.e.viewPreview.querySelectorAll('svg.pikchr'));
21922386
},
21932387
onerror: function(e){
21942388
F.fetch.onerror(e);
@@ -2322,10 +2516,11 @@
23222516
onerror:function(err){
23232517
Chat.reportErrorAsMessage(err);
23242518
Chat._isBatchLoading = false;
23252519
},
23262520
onload:function(x){
2521
+ reportConnectionOkay('loadOldMessages()');
23272522
let gotMessages = x.msgs.length;
23282523
newcontent(x,true);
23292524
Chat._isBatchLoading = false;
23302525
Chat.updateActiveUserList();
23312526
if(Chat._gotServerError){
@@ -2411,10 +2606,11 @@
24112606
onerror:function(err){
24122607
Chat.setCurrentView(Chat.e.viewMessages);
24132608
Chat.reportErrorAsMessage(err);
24142609
},
24152610
onload:function(jx){
2611
+ reportConnectionOkay('submitSearch()');
24162612
let previd = 0;
24172613
D.clearElement(eMsgTgt);
24182614
jx.msgs.forEach((m)=>{
24192615
m.isSearchResult = true;
24202616
const mw = new Chat.MessageWidget(m);
@@ -2444,87 +2640,210 @@
24442640
}
24452641
}
24462642
);
24472643
}/*Chat.submitSearch()*/;
24482644
2449
- const afterFetch = function f(){
2645
+ /*
2646
+ To be called from F.fetch('chat-poll') beforesend() handler. If
2647
+ we're currently in delayed-retry mode and a connection is
2648
+ started, try to reset the delay after N time waiting on that
2649
+ connection. The fact that the connection is waiting to respond,
2650
+ rather than outright failing, is a good hint that the outage is
2651
+ over and we can reset the back-off timer.
2652
+
2653
+ Without this, recovery of a connection error won't be reported
2654
+ until after the long-poll completes by either receiving new
2655
+ messages or timing out. Once a long-poll is in progress, though,
2656
+ we "know" that it's up and running again, so can update the UI and
2657
+ connection timer to reflect that. That's the job this function
2658
+ does.
2659
+
2660
+ Only one of these asynchronous checks will ever be active
2661
+ concurrently and only if Chat.timer.isDelayed() is true. i.e. if
2662
+ this timer is active or Chat.timer.isDelayed() is false, this is a
2663
+ no-op.
2664
+ */
2665
+ const chatPollBeforeSend = function(){
2666
+ //console.warn('chatPollBeforeSend outer', Chat.timer.tidClearPollErr, Chat.timer.currentDelay);
2667
+ if( !Chat.timer.tidClearPollErr && Chat.timer.isDelayed() ){
2668
+ Chat.timer.tidClearPollErr = setTimeout(()=>{
2669
+ //console.warn('chatPollBeforeSend inner');
2670
+ Chat.timer.tidClearPollErr = 0;
2671
+ if( poll.running ){
2672
+ /* This chat-poll F.fetch() is still underway, so let's
2673
+ assume the connection is back up until/unless it times
2674
+ out or breaks again. */
2675
+ reportConnectionOkay('chatPollBeforeSend', true);
2676
+ }
2677
+ }, Chat.timer.$initialDelay * 4/*kinda arbitrary: not too long for UI wait and
2678
+ not too short as to make connection unlikely. */ );
2679
+ }
2680
+ };
2681
+
2682
+ /**
2683
+ Deal with the last poll() response and maybe re-start poll().
2684
+ */
2685
+ const afterPollFetch = function f(err){
24502686
if(true===f.isFirstCall){
24512687
f.isFirstCall = false;
24522688
Chat.ajaxEnd();
24532689
Chat.e.viewMessages.classList.remove('loading');
24542690
setTimeout(function(){
24552691
Chat.scrollMessagesTo(1);
24562692
}, 250);
24572693
}
2458
- if(Chat._gotServerError && Chat.intervalTimer){
2459
- clearInterval(Chat.intervalTimer);
2694
+ Chat.timer.cancelPendingPollTimer();
2695
+ if(Chat._gotServerError){
24602696
Chat.reportErrorAsMessage(
24612697
"Shutting down chat poller due to server-side error. ",
2462
- "Reload this page to reactivate it.");
2463
- delete Chat.intervalTimer;
2464
- }
2465
- poll.running = false;
2466
- };
2467
- afterFetch.isFirstCall = true;
2468
- /**
2469
- FIXME: when polling fails because the remote server is
2470
- reachable but it's not accepting HTTP requests, we should back
2471
- off on polling for a while. e.g. if the remote web server process
2472
- is killed, the poll fails quickly and immediately retries,
2473
- hammering the remote server until the httpd is back up. That
2474
- happens often during development of this application.
2475
-
2476
- XHR does not offer a direct way of distinguishing between
2477
- HTTP/connection errors, but we can hypothetically use the
2478
- xhrRequest.status value to do so, with status==0 being a
2479
- connection error. We do not currently have a clean way of passing
2480
- that info back to the fossil.fetch() client, so we'll need to
2481
- hammer on that API a bit to get this working.
2482
- */
2483
- const poll = async function f(){
2698
+ "Reload this page to reactivate it."
2699
+ );
2700
+ } else {
2701
+ if( err && Chat.beVerbose ){
2702
+ console.error("afterPollFetch:",err.name,err.status,err.message);
2703
+ }
2704
+ if( !err || 'timeout'===err.name/*(probably) long-poll expired*/ ){
2705
+ /* Restart the poller immediately. */
2706
+ reportConnectionOkay('afterPollFetch '+err, false);
2707
+ }else{
2708
+ /* Delay a while before trying again, noting that other Chat
2709
+ APIs may try and succeed at connections before this timer
2710
+ resolves, in which case they'll clear this timeout and the
2711
+ UI message about the outage. */
2712
+ let delay;
2713
+ D.addClass(Chat.e.pollErrorMarker, 'connection-error');
2714
+ if( ++Chat.timer.errCount < Chat.timer.minErrForNotify ){
2715
+ delay = Chat.timer.resetDelay(
2716
+ (Chat.timer.minDelay * Chat.timer.errCount)
2717
+ + Chat.timer.randomInterval(Chat.timer.minDelay)
2718
+ );
2719
+ if(Chat.beVerbose){
2720
+ console.warn("Ignoring polling error #",Chat.timer.errCount,
2721
+ "for another",delay,"ms" );
2722
+ }
2723
+ } else {
2724
+ delay = Chat.timer.incrDelay();
2725
+ //console.warn("afterPollFetch Chat.e.eMsgPollError",Chat.e.eMsgPollError);
2726
+ const msg = "Poller connection error. Retrying in "+delay+ " ms.";
2727
+ /* Replace the current/newest connection error widget. We could also
2728
+ just update its body with the new message, but then its timestamp
2729
+ never updates. OTOH, if we replace the message, we lose the
2730
+ start time of the outage in the log. It seems more useful to
2731
+ update the timestamp so that it doesn't look like it's hung. */
2732
+ if( Chat.e.eMsgPollError ){
2733
+ Chat.deleteMessageElem(Chat.e.eMsgPollError, false);
2734
+ }
2735
+ const theMsg = Chat.e.eMsgPollError = Chat.reportErrorAsMessage(msg);
2736
+ D.addClass(Chat.e.eMsgPollError.e.body,'poller-connection');
2737
+ /* Add a "retry now" button */
2738
+ const btnDel = D.addClass(D.button("Retry now"), 'retry-now');
2739
+ const eParent = Chat.e.eMsgPollError.e.content;
2740
+ D.append(eParent, " ", btnDel);
2741
+ btnDel.addEventListener('click', function(){
2742
+ D.remove(btnDel);
2743
+ D.append(eParent, D.text("retrying..."));
2744
+ Chat.timer.cancelPendingPollTimer().currentDelay =
2745
+ Chat.timer.resetDelay() +
2746
+ 1 /*workaround for showing the "connection restored"
2747
+ message, as the +1 will cause
2748
+ Chat.timer.isDelayed() to be true.*/;
2749
+ poll();
2750
+ });
2751
+ //Chat.playNewMessageSound();// browser complains b/c this wasn't via human interaction
2752
+ }
2753
+ Chat.timer.startPendingPollTimer(delay);
2754
+ }
2755
+ }
2756
+ };
2757
+ afterPollFetch.isFirstCall = true;
2758
+
2759
+ /**
2760
+ Initiates, if it's not already running, a single long-poll
2761
+ request to the /chat-poll endpoint. In the handling of that
2762
+ response, it end up will psuedo-recursively calling itself via
2763
+ the response-handling process. Despite being async, the implied
2764
+ returned Promise is meaningless.
2765
+ */
2766
+ const poll = Chat.poll = async function f(){
24842767
if(f.running) return;
24852768
f.running = true;
24862769
Chat._isBatchLoading = f.isFirstCall;
24872770
if(true===f.isFirstCall){
24882771
f.isFirstCall = false;
2772
+ f.pendingOnError = undefined;
24892773
Chat.ajaxStart();
24902774
Chat.e.viewMessages.classList.add('loading');
2775
+ /*
2776
+ We manager onerror() results in poll() in a roundabout
2777
+ manner: when an onerror() arrives, we stash it aside
2778
+ for a moment before processing it.
2779
+
2780
+ This level of indirection is necessary to be able to
2781
+ unambiguously identify client-timeout-specific polling errors
2782
+ from other errors. Timeouts are always announced in pairs of
2783
+ an HTTP 0 and something we can unambiguously identify as a
2784
+ timeout (in that order). When we receive an HTTP error we put
2785
+ it into this queue. If an ontimeout() call arrives before
2786
+ this error is handled, this error is ignored. If, however, an
2787
+ HTTP error is seen without an accompanying timeout, we handle
2788
+ it from here.
2789
+
2790
+ It's kinda like in the curses C API, where you to match
2791
+ ALT-X by first getting an ESC event, then an X event, but
2792
+ this one is a lot less explicable. (It's almost certainly a
2793
+ mis-handling bug in F.fetch(), but it has so far eluded my
2794
+ eyes.)
2795
+ */
2796
+ f.delayPendingOnError = function(err){
2797
+ if( f.pendingOnError ){
2798
+ const x = f.pendingOnError;
2799
+ f.pendingOnError = undefined;
2800
+ afterPollFetch(x);
2801
+ }
2802
+ };
24912803
}
24922804
F.fetch("chat-poll",{
2493
- timeout: 420 * 1000/*FIXME: get the value from the server*/,
2805
+ timeout: Chat.timer.pollTimeout,
24942806
urlParams:{
24952807
name: Chat.mxMsg
24962808
},
24972809
responseType: "json",
24982810
// Disable the ajax start/end handling for this long-polling op:
2499
- beforesend: function(){},
2500
- aftersend: function(){},
2811
+ beforesend: chatPollBeforeSend,
2812
+ aftersend: function(){
2813
+ poll.running = false;
2814
+ },
2815
+ ontimeout: function(err){
2816
+ f.pendingOnError = undefined /*strip preceeding non-timeout error, if any*/;
2817
+ afterPollFetch(err);
2818
+ },
25012819
onerror:function(err){
25022820
Chat._isBatchLoading = false;
2503
- if(Chat.verboseErrors) console.error(err);
2504
- /* ^^^ we don't use Chat.reportError() here b/c the polling
2505
- fails exepectedly when it times out, but is then immediately
2506
- resumed, and reportError() produces a loud error message. */
2507
- afterFetch();
2821
+ if(Chat.beVerbose){
2822
+ console.error("poll.onerror:",err.name,err.status,JSON.stringify(err));
2823
+ }
2824
+ f.pendingOnError = err;
2825
+ setTimeout(f.delayPendingOnError, 100);
25082826
},
25092827
onload:function(y){
2828
+ reportConnectionOkay('poll.onload', true);
25102829
newcontent(y);
25112830
if(Chat._isBatchLoading){
25122831
Chat._isBatchLoading = false;
25132832
Chat.updateActiveUserList();
25142833
}
2515
- afterFetch();
2834
+ afterPollFetch();
25162835
}
25172836
});
2518
- };
2837
+ }/*poll()*/;
25192838
poll.isFirstCall = true;
25202839
Chat._gotServerError = poll.running = false;
25212840
if( window.fossil.config.chat.fromcli ){
25222841
Chat.chatOnlyMode(true);
25232842
}
2524
- Chat.intervalTimer = setInterval(poll, 1000);
2843
+ Chat.timer.startPendingPollTimer();
25252844
delete ForceResizeKludge.$disabled;
25262845
ForceResizeKludge();
25272846
Chat.animate.$disabled = false;
25282847
setTimeout( ()=>Chat.inputFocus(), 0 );
25292848
F.page.chat = Chat/* enables testing the APIs via the dev tools */;
25302849
});
25312850
--- src/fossil.page.chat.js
+++ src/fossil.page.chat.js
@@ -1,6 +1,6 @@
1 /**
2 This file contains the client-side implementation of fossil's /chat
3 application.
4 */
5 window.fossil.onPageLoad(function(){
6 const F = window.fossil, D = F.dom;
@@ -129,19 +129,21 @@
129 return resized;
130 })();
131 fossil.FRK = ForceResizeKludge/*for debugging*/;
132 const Chat = ForceResizeKludge.chat = (function(){
133 const cs = { // the "Chat" object (result of this function)
134 verboseErrors: false /* if true then certain, mostly extraneous,
135 error messages may be sent to the console. */,
 
 
136 playedBeep: false /* used for the beep-once setting */,
137 e:{/*map of certain DOM elements.*/
138 messageInjectPoint: E1('#message-inject-point'),
139 pageTitle: E1('head title'),
140 loadOlderToolbar: undefined /* the load-posts toolbar (dynamically created) */,
141 inputWrapper: E1("#chat-input-area"),
142 inputElementWrapper: E1('#chat-input-line-wrapper'),
143 fileSelectWrapper: E1('#chat-input-file-area'),
144 viewMessages: E1('#chat-messages-wrapper'),
145 btnSubmit: E1('#chat-button-submit'),
146 btnAttach: E1('#chat-button-attach'),
147 inputX: E1('#chat-input-field-x'),
@@ -155,11 +157,13 @@
155 viewSearch: E1('#chat-search'),
156 searchContent: E1('#chat-search-content'),
157 btnPreview: E1('#chat-button-preview'),
158 views: document.querySelectorAll('.chat-view'),
159 activeUserListWrapper: E1('#chat-user-list-wrapper'),
160 activeUserList: E1('#chat-user-list')
 
 
161 },
162 me: F.user.name,
163 mxMsg: F.config.chat.initSize ? -F.config.chat.initSize : -50,
164 mnMsg: undefined/*lowest message ID we've seen so far (for history loading)*/,
165 pageIsActive: 'visible'===document.visibilityState,
@@ -179,10 +183,105 @@
179 filterState:{
180 activeUser: undefined,
181 match: function(uname){
182 return this.activeUser===uname || !this.activeUser;
183 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184 },
185 /**
186 Gets (no args) or sets (1 arg) the current input text field
187 value, taking into account single- vs multi-line input. The
188 getter returns a trim()'d string and the setter returns this
@@ -606,19 +705,19 @@
606
607 /**
608 If animations are enabled, passes its arguments
609 to D.addClassBriefly(), else this is a no-op.
610 If cb is a function, it is called after the
611 CSS class is removed. Returns this object;
612 */
613 animate: function f(e,a,cb){
614 if(!f.$disabled){
615 D.addClassBriefly(e, a, 0, cb);
616 }
617 return this;
618 }
619 };
620 cs.e.inputFields = [ cs.e.input1, cs.e.inputM, cs.e.inputX ];
621 cs.e.inputFields.$currentIndex = 0;
622 cs.e.inputFields.forEach(function(e,ndx){
623 if(ndx===cs.e.inputFields.$currentIndex) D.removeClass(e,'hidden');
624 else D.addClass(e,'hidden');
@@ -645,33 +744,59 @@
645 cs.reportError = function(/*msg args*/){
646 const args = argsToArray(arguments);
647 console.error("chat error:",args);
648 F.toast.error.apply(F.toast, args);
649 };
 
 
650 /**
651 Reports an error in the form of a new message in the chat
652 feed. All arguments are appended to the message's content area
653 using fossil.dom.append(), so may be of any type supported by
654 that function.
655 */
656 cs.reportErrorAsMessage = function f(/*msg args*/){
657 if(undefined === f.$msgid) f.$msgid=0;
658 const args = argsToArray(arguments).map(function(v){
659 return (v instanceof Error) ? v.message : v;
660 });
661 console.error("chat error:",args);
 
 
662 const d = new Date().toISOString(),
663 mw = new this.MessageWidget({
664 isError: true,
665 xfrom: null,
666 msgid: "error-"+(++f.$msgid),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
667 mtime: d,
668 lmtime: d,
669 xmsg: args
670 });
671 this.injectMessageElem(mw.e.body);
672 mw.scrollIntoView();
 
673 };
674
675 cs.getMessageElemById = function(id){
676 return qs('[data-msgid="'+id+'"]');
677 };
@@ -690,24 +815,40 @@
690 /**
691 LOCALLY deletes a message element by the message ID or passing
692 the .message-row element. Returns true if it removes an element,
693 else false.
694 */
695 cs.deleteMessageElem = function(id){
696 var e;
697 if(id instanceof HTMLElement){
698 e = id;
699 id = e.dataset.msgid;
700 }else{
 
 
 
 
 
 
 
 
 
 
 
 
 
 
701 e = this.getMessageElemById(id);
702 }
703 if(e && id){
704 D.remove(e);
705 if(e===this.e.newestMessage){
706 this.fetchLastMessageElem();
707 }
708 F.toast.message("Deleted message "+id+".");
 
 
709 }
710 return !!e;
711 };
712
713 /**
@@ -776,10 +917,11 @@
776 const self = this;
777 F.fetch('chat-fetch-one',{
778 urlParams:{ name: id, raw: true},
779 responseType: 'json',
780 onload: function(msg){
 
781 content.$elems[1] = D.append(D.pre(),msg.xmsg);
782 content.$elems[1]._xmsgRaw = msg.xmsg/*used for copy-to-clipboard feature*/;
783 self.toggleTextMode(e);
784 },
785 aftersend:function(){
@@ -834,14 +976,16 @@
834 }else{
835 e = this.getMessageElemById(id);
836 }
837 if(!(e instanceof HTMLElement)) return;
838 if(this.userMayDelete(e)){
839 this.ajaxStart();
840 F.fetch("chat-delete/" + id, {
841 responseType: 'json',
842 onload:(r)=>this.deleteMessageElem(r),
 
 
 
843 onerror:(err)=>this.reportErrorAsMessage(err)
844 });
845 }else{
846 this.deleteMessageElem(id);
847 }
@@ -1035,10 +1179,11 @@
1035
1036 ctor.prototype = {
1037 scrollIntoView: function(){
1038 this.e.content.scrollIntoView();
1039 },
 
1040 setMessage: function(m){
1041 const ds = this.e.body.dataset;
1042 ds.timestamp = m.mtime;
1043 ds.lmtime = m.lmtime;
1044 ds.msgid = m.msgid;
@@ -1212,12 +1357,22 @@
1212 const btnDeleteLocal = D.button("Delete locally");
1213 D.append(toolbar, btnDeleteLocal);
1214 const self = this;
1215 btnDeleteLocal.addEventListener('click', function(){
1216 self.hide();
1217 Chat.deleteMessageElem(eMsg);
1218 });
 
 
 
 
 
 
 
 
 
 
1219 if(Chat.userMayDelete(eMsg)){
1220 const btnDeleteGlobal = D.button("Delete globally");
1221 D.append(toolbar, btnDeleteGlobal);
1222 F.confirmer(btnDeleteGlobal,{
1223 pinSize: true,
@@ -1457,10 +1612,11 @@
1457 n: nFetch,
1458 i: iFirst
1459 },
1460 responseType: "json",
1461 onload:function(jx){
 
1462 if( bDown ) jx.msgs.reverse();
1463 jx.msgs.forEach((m) => {
1464 m.isSearchResult = true;
1465 var mw = new Chat.MessageWidget(m);
1466 if( bDown ){
@@ -1524,11 +1680,11 @@
1524 reader.onload = (e)=>img.setAttribute('src', e.target.result);
1525 reader.readAsDataURL(blob);
1526 }
1527 };
1528 Chat.e.inputFile.addEventListener('change', function(ev){
1529 updateDropZoneContent(this.files && this.files[0] ? this.files[0] : undefined)
1530 });
1531 /* Handle image paste from clipboard. TODO: figure out how we can
1532 paste non-image binary data as if it had been selected via the
1533 file selection element. */
1534 const pasteListener = function(event){
@@ -1604,10 +1760,11 @@
1604 D.span(),"This message was not successfully sent to the server:"
1605 ));
1606 if(state.msg){
1607 const ta = D.textarea();
1608 ta.value = state.msg;
 
1609 D.append(w,ta);
1610 }
1611 if(state.blob){
1612 D.append(w,D.append(D.span(),"Attachment: ",(state.blob.name||"unnamed")));
1613 //console.debug("blob = ",state.blob);
@@ -1622,11 +1779,46 @@
1622 if(state.msg) Chat.inputValue(state.msg);
1623 if(state.blob) BlobXferState.updateDropZoneContent(state.blob);
1624 const theMsg = findMessageWidgetParent(w);
1625 if(theMsg) Chat.deleteMessageElem(theMsg);
1626 }));
1627 Chat.reportErrorAsMessage(w);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1628 };
1629
1630 /**
1631 Submits the contents of the message input field (if not empty)
1632 and/or the file attachment field to the server. If both are
@@ -1686,10 +1878,11 @@
1686 onerror:function(err){
1687 self.reportErrorAsMessage(err);
1688 recoverFailedMessage(fallback);
1689 },
1690 onload:function(txt){
 
1691 if(!txt) return/*success response*/;
1692 try{
1693 const json = JSON.parse(txt);
1694 self.newContent({msgs:[json]});
1695 }catch(e){
@@ -2126,11 +2319,11 @@
2126 Chat.e.inputFields.$currentIndex = a[2];
2127 Chat.inputValue(v);
2128 D.removeClass(a[0], 'hidden');
2129 D.addClass(a[1], 'hidden');
2130 }
2131 Chat.e.inputElementWrapper.classList[
2132 s.value ? 'add' : 'remove'
2133 ]('compact');
2134 Chat.e.inputFields[Chat.e.inputFields.$currentIndex].focus();
2135 });
2136 Chat.settings.addListener('edit-ctrl-send',function(s){
@@ -2185,10 +2378,11 @@
2185 /*filename needed for mimetype determination*/);
2186 fd.append('render_mode',F.page.previewModes.wiki);
2187 F.fetch('ajax/preview-text',{
2188 payload: fd,
2189 onload: function(html){
 
2190 Chat.setPreviewText(html);
2191 F.pikchr.addSrcView(Chat.e.viewPreview.querySelectorAll('svg.pikchr'));
2192 },
2193 onerror: function(e){
2194 F.fetch.onerror(e);
@@ -2322,10 +2516,11 @@
2322 onerror:function(err){
2323 Chat.reportErrorAsMessage(err);
2324 Chat._isBatchLoading = false;
2325 },
2326 onload:function(x){
 
2327 let gotMessages = x.msgs.length;
2328 newcontent(x,true);
2329 Chat._isBatchLoading = false;
2330 Chat.updateActiveUserList();
2331 if(Chat._gotServerError){
@@ -2411,10 +2606,11 @@
2411 onerror:function(err){
2412 Chat.setCurrentView(Chat.e.viewMessages);
2413 Chat.reportErrorAsMessage(err);
2414 },
2415 onload:function(jx){
 
2416 let previd = 0;
2417 D.clearElement(eMsgTgt);
2418 jx.msgs.forEach((m)=>{
2419 m.isSearchResult = true;
2420 const mw = new Chat.MessageWidget(m);
@@ -2444,87 +2640,210 @@
2444 }
2445 }
2446 );
2447 }/*Chat.submitSearch()*/;
2448
2449 const afterFetch = function f(){
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2450 if(true===f.isFirstCall){
2451 f.isFirstCall = false;
2452 Chat.ajaxEnd();
2453 Chat.e.viewMessages.classList.remove('loading');
2454 setTimeout(function(){
2455 Chat.scrollMessagesTo(1);
2456 }, 250);
2457 }
2458 if(Chat._gotServerError && Chat.intervalTimer){
2459 clearInterval(Chat.intervalTimer);
2460 Chat.reportErrorAsMessage(
2461 "Shutting down chat poller due to server-side error. ",
2462 "Reload this page to reactivate it.");
2463 delete Chat.intervalTimer;
2464 }
2465 poll.running = false;
2466 };
2467 afterFetch.isFirstCall = true;
2468 /**
2469 FIXME: when polling fails because the remote server is
2470 reachable but it's not accepting HTTP requests, we should back
2471 off on polling for a while. e.g. if the remote web server process
2472 is killed, the poll fails quickly and immediately retries,
2473 hammering the remote server until the httpd is back up. That
2474 happens often during development of this application.
2475
2476 XHR does not offer a direct way of distinguishing between
2477 HTTP/connection errors, but we can hypothetically use the
2478 xhrRequest.status value to do so, with status==0 being a
2479 connection error. We do not currently have a clean way of passing
2480 that info back to the fossil.fetch() client, so we'll need to
2481 hammer on that API a bit to get this working.
2482 */
2483 const poll = async function f(){
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2484 if(f.running) return;
2485 f.running = true;
2486 Chat._isBatchLoading = f.isFirstCall;
2487 if(true===f.isFirstCall){
2488 f.isFirstCall = false;
 
2489 Chat.ajaxStart();
2490 Chat.e.viewMessages.classList.add('loading');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2491 }
2492 F.fetch("chat-poll",{
2493 timeout: 420 * 1000/*FIXME: get the value from the server*/,
2494 urlParams:{
2495 name: Chat.mxMsg
2496 },
2497 responseType: "json",
2498 // Disable the ajax start/end handling for this long-polling op:
2499 beforesend: function(){},
2500 aftersend: function(){},
 
 
 
 
 
 
2501 onerror:function(err){
2502 Chat._isBatchLoading = false;
2503 if(Chat.verboseErrors) console.error(err);
2504 /* ^^^ we don't use Chat.reportError() here b/c the polling
2505 fails exepectedly when it times out, but is then immediately
2506 resumed, and reportError() produces a loud error message. */
2507 afterFetch();
2508 },
2509 onload:function(y){
 
2510 newcontent(y);
2511 if(Chat._isBatchLoading){
2512 Chat._isBatchLoading = false;
2513 Chat.updateActiveUserList();
2514 }
2515 afterFetch();
2516 }
2517 });
2518 };
2519 poll.isFirstCall = true;
2520 Chat._gotServerError = poll.running = false;
2521 if( window.fossil.config.chat.fromcli ){
2522 Chat.chatOnlyMode(true);
2523 }
2524 Chat.intervalTimer = setInterval(poll, 1000);
2525 delete ForceResizeKludge.$disabled;
2526 ForceResizeKludge();
2527 Chat.animate.$disabled = false;
2528 setTimeout( ()=>Chat.inputFocus(), 0 );
2529 F.page.chat = Chat/* enables testing the APIs via the dev tools */;
2530 });
2531
--- src/fossil.page.chat.js
+++ src/fossil.page.chat.js
@@ -1,6 +1,6 @@
1 -/**
2 This file contains the client-side implementation of fossil's /chat
3 application.
4 */
5 window.fossil.onPageLoad(function(){
6 const F = window.fossil, D = F.dom;
@@ -129,19 +129,21 @@
129 return resized;
130 })();
131 fossil.FRK = ForceResizeKludge/*for debugging*/;
132 const Chat = ForceResizeKludge.chat = (function(){
133 const cs = { // the "Chat" object (result of this function)
134 beVerbose: false
135 //!!window.location.hostname.match("localhost")
136 /* if true then certain, mostly extraneous, error messages and
137 log messages may be sent to the console. */,
138 playedBeep: false /* used for the beep-once setting */,
139 e:{/*map of certain DOM elements.*/
140 messageInjectPoint: E1('#message-inject-point'),
141 pageTitle: E1('head title'),
142 loadOlderToolbar: undefined /* the load-posts toolbar (dynamically created) */,
143 inputArea: E1("#chat-input-area"),
144 inputLineWrapper: E1('#chat-input-line-wrapper'),
145 fileSelectWrapper: E1('#chat-input-file-area'),
146 viewMessages: E1('#chat-messages-wrapper'),
147 btnSubmit: E1('#chat-button-submit'),
148 btnAttach: E1('#chat-button-attach'),
149 inputX: E1('#chat-input-field-x'),
@@ -155,11 +157,13 @@
157 viewSearch: E1('#chat-search'),
158 searchContent: E1('#chat-search-content'),
159 btnPreview: E1('#chat-button-preview'),
160 views: document.querySelectorAll('.chat-view'),
161 activeUserListWrapper: E1('#chat-user-list-wrapper'),
162 activeUserList: E1('#chat-user-list'),
163 eMsgPollError: undefined /* current connection error MessageMidget */,
164 pollErrorMarker: document.body /* element to toggle 'connection-error' CSS class on */
165 },
166 me: F.user.name,
167 mxMsg: F.config.chat.initSize ? -F.config.chat.initSize : -50,
168 mnMsg: undefined/*lowest message ID we've seen so far (for history loading)*/,
169 pageIsActive: 'visible'===document.visibilityState,
@@ -179,10 +183,105 @@
183 filterState:{
184 activeUser: undefined,
185 match: function(uname){
186 return this.activeUser===uname || !this.activeUser;
187 }
188 },
189 /**
190 The timer object is used to control connection throttling
191 when connection errors arrise. It starts off with a polling
192 delay of $initialDelay ms. If there's a connection error,
193 that gets bumped by some value for each subsequent error, up
194 to some max value.
195
196 The timing of resetting the delay when service returns is,
197 because of the long-poll connection and our lack of low-level
198 insight into the connection at this level, a bit wonky.
199 */
200 timer:{
201 /* setTimeout() ID for (delayed) starting a Chat.poll(), so
202 that it runs at controlled intervals (which change when a
203 connection drops and recovers). */
204 tidPendingPoll: undefined,
205 tidClearPollErr: undefined /*setTimeout() timer id for
206 reconnection determination. See
207 clearPollErrOnWait(). */,
208 $initialDelay: 1000 /* initial polling interval (ms) */,
209 currentDelay: 1000 /* current polling interval */,
210 maxDelay: 60000 * 5 /* max interval when backing off for
211 connection errors */,
212 minDelay: 5000 /* minimum delay time for a back-off/retry
213 attempt. */,
214 errCount: 0 /* Current poller connection error count */,
215 minErrForNotify: 4 /* Don't warn for connection errors until this
216 many have occurred */,
217 pollTimeout: (1 && window.location.hostname.match(
218 "localhost" /*presumably local dev mode*/
219 )) ? 15000
220 : (+F.config.chat.pollTimeout>0
221 ? (1000 * (F.config.chat.pollTimeout - Math.floor(F.config.chat.pollTimeout * 0.1)))
222 /* ^^^^^^^^^^^^ we want our timeouts to be slightly shorter
223 than the server's so that we can distingished timed-out
224 polls on our end from HTTP errors (if the server times
225 out). */
226 : 30000),
227 /** Returns a random fudge value for reconnect attempt times,
228 intended to keep the /chat server from getting hammered if
229 all clients which were just disconnected all reconnect at
230 the same instant. */
231 randomInterval: function(factor){
232 return Math.floor(Math.random() * factor);
233 },
234 /** Increments the reconnection delay, within some min/max range. */
235 incrDelay: function(){
236 if( this.maxDelay > this.currentDelay ){
237 if(this.currentDelay < this.minDelay){
238 this.currentDelay = this.minDelay + this.randomInterval(this.minDelay);
239 }else{
240 this.currentDelay = this.currentDelay*2 + this.randomInterval(this.currentDelay);
241 }
242 }
243 return this.currentDelay;
244 },
245 /** Resets the delay counter to v || its initial value. */
246 resetDelay: function(ms=0){
247 return this.currentDelay = ms || this.$initialDelay;
248 },
249 /** Returns true if the timer is set to delayed mode. */
250 isDelayed: function(){
251 return (this.currentDelay > this.$initialDelay) ? this.currentDelay : 0;
252 },
253 /**
254 Cancels any in-progress pending-poll timer and starts a new
255 one with the given delay, defaulting to this.resetDelay().
256 */
257 startPendingPollTimer: function(delay){
258 this.cancelPendingPollTimer().tidPendingPoll
259 = setTimeout( Chat.poll, delay || Chat.timer.resetDelay() );
260 return this;
261 },
262 /**
263 Cancels any still-active timer set to trigger the next
264 Chat.poll().
265 */
266 cancelPendingPollTimer: function(){
267 if( this.tidPendingPoll ){
268 clearTimeout(this.tidPendingPoll);
269 this.tidPendingPoll = 0;
270 }
271 return this;
272 },
273 /**
274 Cancels any pending reconnection attempt back-off timer..
275 */
276 cancelReconnectCheckTimer: function(){
277 if( this.tidClearPollErr ){
278 clearTimeout(this.tidClearPollErr);
279 this.tidClearPollErr = 0;
280 }
281 return this;
282 }
283 },
284 /**
285 Gets (no args) or sets (1 arg) the current input text field
286 value, taking into account single- vs multi-line input. The
287 getter returns a trim()'d string and the setter returns this
@@ -606,19 +705,19 @@
705
706 /**
707 If animations are enabled, passes its arguments
708 to D.addClassBriefly(), else this is a no-op.
709 If cb is a function, it is called after the
710 CSS class is removed. Returns this object;
711 */
712 animate: function f(e,a,cb){
713 if(!f.$disabled){
714 D.addClassBriefly(e, a, 0, cb);
715 }
716 return this;
717 }
718 }/*Chat object*/;
719 cs.e.inputFields = [ cs.e.input1, cs.e.inputM, cs.e.inputX ];
720 cs.e.inputFields.$currentIndex = 0;
721 cs.e.inputFields.forEach(function(e,ndx){
722 if(ndx===cs.e.inputFields.$currentIndex) D.removeClass(e,'hidden');
723 else D.addClass(e,'hidden');
@@ -645,33 +744,59 @@
744 cs.reportError = function(/*msg args*/){
745 const args = argsToArray(arguments);
746 console.error("chat error:",args);
747 F.toast.error.apply(F.toast, args);
748 };
749
750 let InternalMsgId = 0;
751 /**
752 Reports an error in the form of a new message in the chat
753 feed. All arguments are appended to the message's content area
754 using fossil.dom.append(), so may be of any type supported by
755 that function.
756 */
757 cs.reportErrorAsMessage = function f(/*msg args*/){
 
758 const args = argsToArray(arguments).map(function(v){
759 return (v instanceof Error) ? v.message : v;
760 });
761 if(Chat.beVerbose){
762 console.error("chat error:",args);
763 }
764 const d = new Date().toISOString(),
765 mw = new this.MessageWidget({
766 isError: true,
767 xfrom: undefined,
768 msgid: "error-"+(++InternalMsgId),
769 mtime: d,
770 lmtime: d,
771 xmsg: args
772 });
773 this.injectMessageElem(mw.e.body);
774 mw.scrollIntoView();
775 return mw;
776 };
777
778 /**
779 For use by the connection poller to send a "connection
780 restored" message.
781 */
782 cs.reportReconnection = function f(/*msg args*/){
783 const args = argsToArray(arguments).map(function(v){
784 return (v instanceof Error) ? v.message : v;
785 });
786 const d = new Date().toISOString(),
787 mw = new this.MessageWidget({
788 isError: false,
789 xfrom: undefined,
790 msgid: "reconnect-"+(++InternalMsgId),
791 mtime: d,
792 lmtime: d,
793 xmsg: args
794 });
795 this.injectMessageElem(mw.e.body);
796 mw.scrollIntoView();
797 return mw;
798 };
799
800 cs.getMessageElemById = function(id){
801 return qs('[data-msgid="'+id+'"]');
802 };
@@ -690,24 +815,40 @@
815 /**
816 LOCALLY deletes a message element by the message ID or passing
817 the .message-row element. Returns true if it removes an element,
818 else false.
819 */
820 cs.deleteMessageElem = function(id, silent){
821 var e;
822 if(id instanceof HTMLElement){
823 e = id;
824 id = e.dataset.msgid;
825 delete e.dataset.msgid;
826 if( e?.dataset?.alsoRemove ){
827 const xId = e.dataset.alsoRemove;
828 delete e.dataset.alsoRemove;
829 this.deleteMessageElem( xId );
830 }
831 }else if(id instanceof Chat.MessageWidget) {
832 if( this.e.eMsgPollError === e ){
833 this.e.eMsgPollError = undefined;
834 }
835 if(id.e?.body){
836 this.deleteMessageElem(id.e.body);
837 }
838 return;
839 } else{
840 e = this.getMessageElemById(id);
841 }
842 if(e && id){
843 D.remove(e);
844 if(e===this.e.newestMessage){
845 this.fetchLastMessageElem();
846 }
847 if( !silent ){
848 F.toast.message("Deleted message "+id+".");
849 }
850 }
851 return !!e;
852 };
853
854 /**
@@ -776,10 +917,11 @@
917 const self = this;
918 F.fetch('chat-fetch-one',{
919 urlParams:{ name: id, raw: true},
920 responseType: 'json',
921 onload: function(msg){
922 reportConnectionOkay('chat-fetch-one');
923 content.$elems[1] = D.append(D.pre(),msg.xmsg);
924 content.$elems[1]._xmsgRaw = msg.xmsg/*used for copy-to-clipboard feature*/;
925 self.toggleTextMode(e);
926 },
927 aftersend:function(){
@@ -834,14 +976,16 @@
976 }else{
977 e = this.getMessageElemById(id);
978 }
979 if(!(e instanceof HTMLElement)) return;
980 if(this.userMayDelete(e)){
 
981 F.fetch("chat-delete/" + id, {
982 responseType: 'json',
983 onload:(r)=>{
984 reportConnectionOkay('chat-delete');
985 this.deleteMessageElem(r);
986 },
987 onerror:(err)=>this.reportErrorAsMessage(err)
988 });
989 }else{
990 this.deleteMessageElem(id);
991 }
@@ -1035,10 +1179,11 @@
1179
1180 ctor.prototype = {
1181 scrollIntoView: function(){
1182 this.e.content.scrollIntoView();
1183 },
1184 //remove: function(silent){Chat.deleteMessageElem(this, silent);},
1185 setMessage: function(m){
1186 const ds = this.e.body.dataset;
1187 ds.timestamp = m.mtime;
1188 ds.lmtime = m.lmtime;
1189 ds.msgid = m.msgid;
@@ -1212,12 +1357,22 @@
1357 const btnDeleteLocal = D.button("Delete locally");
1358 D.append(toolbar, btnDeleteLocal);
1359 const self = this;
1360 btnDeleteLocal.addEventListener('click', function(){
1361 self.hide();
1362 Chat.deleteMessageElem(eMsg)
1363 });
1364 if( eMsg.classList.contains('notification') ){
1365 const btnDeletePoll = D.button("Delete /chat notifications?");
1366 D.append(toolbar, btnDeletePoll);
1367 btnDeletePoll.addEventListener('click', function(){
1368 self.hide();
1369 Chat.e.viewMessages.querySelectorAll(
1370 '.message-widget.notification:not(.resend-message)'
1371 ).forEach(e=>Chat.deleteMessageElem(e, true));
1372 });
1373 }
1374 if(Chat.userMayDelete(eMsg)){
1375 const btnDeleteGlobal = D.button("Delete globally");
1376 D.append(toolbar, btnDeleteGlobal);
1377 F.confirmer(btnDeleteGlobal,{
1378 pinSize: true,
@@ -1457,10 +1612,11 @@
1612 n: nFetch,
1613 i: iFirst
1614 },
1615 responseType: "json",
1616 onload:function(jx){
1617 reportConnectionOkay('chat-query');
1618 if( bDown ) jx.msgs.reverse();
1619 jx.msgs.forEach((m) => {
1620 m.isSearchResult = true;
1621 var mw = new Chat.MessageWidget(m);
1622 if( bDown ){
@@ -1524,11 +1680,11 @@
1680 reader.onload = (e)=>img.setAttribute('src', e.target.result);
1681 reader.readAsDataURL(blob);
1682 }
1683 };
1684 Chat.e.inputFile.addEventListener('change', function(ev){
1685 updateDropZoneContent(this?.files[0])
1686 });
1687 /* Handle image paste from clipboard. TODO: figure out how we can
1688 paste non-image binary data as if it had been selected via the
1689 file selection element. */
1690 const pasteListener = function(event){
@@ -1604,10 +1760,11 @@
1760 D.span(),"This message was not successfully sent to the server:"
1761 ));
1762 if(state.msg){
1763 const ta = D.textarea();
1764 ta.value = state.msg;
1765 ta.setAttribute('readonly','true');
1766 D.append(w,ta);
1767 }
1768 if(state.blob){
1769 D.append(w,D.append(D.span(),"Attachment: ",(state.blob.name||"unnamed")));
1770 //console.debug("blob = ",state.blob);
@@ -1622,11 +1779,46 @@
1779 if(state.msg) Chat.inputValue(state.msg);
1780 if(state.blob) BlobXferState.updateDropZoneContent(state.blob);
1781 const theMsg = findMessageWidgetParent(w);
1782 if(theMsg) Chat.deleteMessageElem(theMsg);
1783 }));
1784 D.addClass(Chat.reportErrorAsMessage(w).e.body, "resend-message");
1785 };
1786
1787 /* Assume the connection has been established, reset the
1788 Chat.timer.tidClearPollErr, and (if showMsg and
1789 !!Chat.e.eMsgPollError) alert the user that the outage appears to
1790 be over. Also schedule Chat.poll() to run in the very near
1791 future. */
1792 const reportConnectionOkay = function(dbgContext, showMsg = true){
1793 if(Chat.beVerbose){
1794 console.warn('reportConnectionOkay', dbgContext,
1795 'Chat.e.pollErrorMarker classes =',
1796 Chat.e.pollErrorMarker.classList,
1797 'Chat.timer.tidClearPollErr =',Chat.timer.tidClearPollErr,
1798 'Chat.timer =',Chat.timer);
1799 }
1800 if( Chat.timer.errCount ){
1801 D.removeClass(Chat.e.pollErrorMarker, 'connection-error');
1802 Chat.timer.errCount = 0;
1803 }
1804 Chat.timer.cancelReconnectCheckTimer().startPendingPollTimer();
1805 if( Chat.e.eMsgPollError ) {
1806 const oldErrMsg = Chat.e.eMsgPollError;
1807 Chat.e.eMsgPollError = undefined;
1808 if( showMsg ){
1809 if(Chat.beVerbose){
1810 console.log("Poller Connection restored.");
1811 }
1812 const m = Chat.reportReconnection("Poller connection restored.");
1813 if( oldErrMsg ){
1814 D.remove(oldErrMsg.e?.body.querySelector('button.retry-now'));
1815 }
1816 m.e.body.dataset.alsoRemove = oldErrMsg?.e?.body?.dataset?.msgid;
1817 D.addClass(m.e.body,'poller-connection');
1818 }
1819 }
1820 };
1821
1822 /**
1823 Submits the contents of the message input field (if not empty)
1824 and/or the file attachment field to the server. If both are
@@ -1686,10 +1878,11 @@
1878 onerror:function(err){
1879 self.reportErrorAsMessage(err);
1880 recoverFailedMessage(fallback);
1881 },
1882 onload:function(txt){
1883 reportConnectionOkay('chat-send');
1884 if(!txt) return/*success response*/;
1885 try{
1886 const json = JSON.parse(txt);
1887 self.newContent({msgs:[json]});
1888 }catch(e){
@@ -2126,11 +2319,11 @@
2319 Chat.e.inputFields.$currentIndex = a[2];
2320 Chat.inputValue(v);
2321 D.removeClass(a[0], 'hidden');
2322 D.addClass(a[1], 'hidden');
2323 }
2324 Chat.e.inputLineWrapper.classList[
2325 s.value ? 'add' : 'remove'
2326 ]('compact');
2327 Chat.e.inputFields[Chat.e.inputFields.$currentIndex].focus();
2328 });
2329 Chat.settings.addListener('edit-ctrl-send',function(s){
@@ -2185,10 +2378,11 @@
2378 /*filename needed for mimetype determination*/);
2379 fd.append('render_mode',F.page.previewModes.wiki);
2380 F.fetch('ajax/preview-text',{
2381 payload: fd,
2382 onload: function(html){
2383 reportConnectionOkay('ajax/preview-text');
2384 Chat.setPreviewText(html);
2385 F.pikchr.addSrcView(Chat.e.viewPreview.querySelectorAll('svg.pikchr'));
2386 },
2387 onerror: function(e){
2388 F.fetch.onerror(e);
@@ -2322,10 +2516,11 @@
2516 onerror:function(err){
2517 Chat.reportErrorAsMessage(err);
2518 Chat._isBatchLoading = false;
2519 },
2520 onload:function(x){
2521 reportConnectionOkay('loadOldMessages()');
2522 let gotMessages = x.msgs.length;
2523 newcontent(x,true);
2524 Chat._isBatchLoading = false;
2525 Chat.updateActiveUserList();
2526 if(Chat._gotServerError){
@@ -2411,10 +2606,11 @@
2606 onerror:function(err){
2607 Chat.setCurrentView(Chat.e.viewMessages);
2608 Chat.reportErrorAsMessage(err);
2609 },
2610 onload:function(jx){
2611 reportConnectionOkay('submitSearch()');
2612 let previd = 0;
2613 D.clearElement(eMsgTgt);
2614 jx.msgs.forEach((m)=>{
2615 m.isSearchResult = true;
2616 const mw = new Chat.MessageWidget(m);
@@ -2444,87 +2640,210 @@
2640 }
2641 }
2642 );
2643 }/*Chat.submitSearch()*/;
2644
2645 /*
2646 To be called from F.fetch('chat-poll') beforesend() handler. If
2647 we're currently in delayed-retry mode and a connection is
2648 started, try to reset the delay after N time waiting on that
2649 connection. The fact that the connection is waiting to respond,
2650 rather than outright failing, is a good hint that the outage is
2651 over and we can reset the back-off timer.
2652
2653 Without this, recovery of a connection error won't be reported
2654 until after the long-poll completes by either receiving new
2655 messages or timing out. Once a long-poll is in progress, though,
2656 we "know" that it's up and running again, so can update the UI and
2657 connection timer to reflect that. That's the job this function
2658 does.
2659
2660 Only one of these asynchronous checks will ever be active
2661 concurrently and only if Chat.timer.isDelayed() is true. i.e. if
2662 this timer is active or Chat.timer.isDelayed() is false, this is a
2663 no-op.
2664 */
2665 const chatPollBeforeSend = function(){
2666 //console.warn('chatPollBeforeSend outer', Chat.timer.tidClearPollErr, Chat.timer.currentDelay);
2667 if( !Chat.timer.tidClearPollErr && Chat.timer.isDelayed() ){
2668 Chat.timer.tidClearPollErr = setTimeout(()=>{
2669 //console.warn('chatPollBeforeSend inner');
2670 Chat.timer.tidClearPollErr = 0;
2671 if( poll.running ){
2672 /* This chat-poll F.fetch() is still underway, so let's
2673 assume the connection is back up until/unless it times
2674 out or breaks again. */
2675 reportConnectionOkay('chatPollBeforeSend', true);
2676 }
2677 }, Chat.timer.$initialDelay * 4/*kinda arbitrary: not too long for UI wait and
2678 not too short as to make connection unlikely. */ );
2679 }
2680 };
2681
2682 /**
2683 Deal with the last poll() response and maybe re-start poll().
2684 */
2685 const afterPollFetch = function f(err){
2686 if(true===f.isFirstCall){
2687 f.isFirstCall = false;
2688 Chat.ajaxEnd();
2689 Chat.e.viewMessages.classList.remove('loading');
2690 setTimeout(function(){
2691 Chat.scrollMessagesTo(1);
2692 }, 250);
2693 }
2694 Chat.timer.cancelPendingPollTimer();
2695 if(Chat._gotServerError){
2696 Chat.reportErrorAsMessage(
2697 "Shutting down chat poller due to server-side error. ",
2698 "Reload this page to reactivate it."
2699 );
2700 } else {
2701 if( err && Chat.beVerbose ){
2702 console.error("afterPollFetch:",err.name,err.status,err.message);
2703 }
2704 if( !err || 'timeout'===err.name/*(probably) long-poll expired*/ ){
2705 /* Restart the poller immediately. */
2706 reportConnectionOkay('afterPollFetch '+err, false);
2707 }else{
2708 /* Delay a while before trying again, noting that other Chat
2709 APIs may try and succeed at connections before this timer
2710 resolves, in which case they'll clear this timeout and the
2711 UI message about the outage. */
2712 let delay;
2713 D.addClass(Chat.e.pollErrorMarker, 'connection-error');
2714 if( ++Chat.timer.errCount < Chat.timer.minErrForNotify ){
2715 delay = Chat.timer.resetDelay(
2716 (Chat.timer.minDelay * Chat.timer.errCount)
2717 + Chat.timer.randomInterval(Chat.timer.minDelay)
2718 );
2719 if(Chat.beVerbose){
2720 console.warn("Ignoring polling error #",Chat.timer.errCount,
2721 "for another",delay,"ms" );
2722 }
2723 } else {
2724 delay = Chat.timer.incrDelay();
2725 //console.warn("afterPollFetch Chat.e.eMsgPollError",Chat.e.eMsgPollError);
2726 const msg = "Poller connection error. Retrying in "+delay+ " ms.";
2727 /* Replace the current/newest connection error widget. We could also
2728 just update its body with the new message, but then its timestamp
2729 never updates. OTOH, if we replace the message, we lose the
2730 start time of the outage in the log. It seems more useful to
2731 update the timestamp so that it doesn't look like it's hung. */
2732 if( Chat.e.eMsgPollError ){
2733 Chat.deleteMessageElem(Chat.e.eMsgPollError, false);
2734 }
2735 const theMsg = Chat.e.eMsgPollError = Chat.reportErrorAsMessage(msg);
2736 D.addClass(Chat.e.eMsgPollError.e.body,'poller-connection');
2737 /* Add a "retry now" button */
2738 const btnDel = D.addClass(D.button("Retry now"), 'retry-now');
2739 const eParent = Chat.e.eMsgPollError.e.content;
2740 D.append(eParent, " ", btnDel);
2741 btnDel.addEventListener('click', function(){
2742 D.remove(btnDel);
2743 D.append(eParent, D.text("retrying..."));
2744 Chat.timer.cancelPendingPollTimer().currentDelay =
2745 Chat.timer.resetDelay() +
2746 1 /*workaround for showing the "connection restored"
2747 message, as the +1 will cause
2748 Chat.timer.isDelayed() to be true.*/;
2749 poll();
2750 });
2751 //Chat.playNewMessageSound();// browser complains b/c this wasn't via human interaction
2752 }
2753 Chat.timer.startPendingPollTimer(delay);
2754 }
2755 }
2756 };
2757 afterPollFetch.isFirstCall = true;
2758
2759 /**
2760 Initiates, if it's not already running, a single long-poll
2761 request to the /chat-poll endpoint. In the handling of that
2762 response, it end up will psuedo-recursively calling itself via
2763 the response-handling process. Despite being async, the implied
2764 returned Promise is meaningless.
2765 */
2766 const poll = Chat.poll = async function f(){
2767 if(f.running) return;
2768 f.running = true;
2769 Chat._isBatchLoading = f.isFirstCall;
2770 if(true===f.isFirstCall){
2771 f.isFirstCall = false;
2772 f.pendingOnError = undefined;
2773 Chat.ajaxStart();
2774 Chat.e.viewMessages.classList.add('loading');
2775 /*
2776 We manager onerror() results in poll() in a roundabout
2777 manner: when an onerror() arrives, we stash it aside
2778 for a moment before processing it.
2779
2780 This level of indirection is necessary to be able to
2781 unambiguously identify client-timeout-specific polling errors
2782 from other errors. Timeouts are always announced in pairs of
2783 an HTTP 0 and something we can unambiguously identify as a
2784 timeout (in that order). When we receive an HTTP error we put
2785 it into this queue. If an ontimeout() call arrives before
2786 this error is handled, this error is ignored. If, however, an
2787 HTTP error is seen without an accompanying timeout, we handle
2788 it from here.
2789
2790 It's kinda like in the curses C API, where you to match
2791 ALT-X by first getting an ESC event, then an X event, but
2792 this one is a lot less explicable. (It's almost certainly a
2793 mis-handling bug in F.fetch(), but it has so far eluded my
2794 eyes.)
2795 */
2796 f.delayPendingOnError = function(err){
2797 if( f.pendingOnError ){
2798 const x = f.pendingOnError;
2799 f.pendingOnError = undefined;
2800 afterPollFetch(x);
2801 }
2802 };
2803 }
2804 F.fetch("chat-poll",{
2805 timeout: Chat.timer.pollTimeout,
2806 urlParams:{
2807 name: Chat.mxMsg
2808 },
2809 responseType: "json",
2810 // Disable the ajax start/end handling for this long-polling op:
2811 beforesend: chatPollBeforeSend,
2812 aftersend: function(){
2813 poll.running = false;
2814 },
2815 ontimeout: function(err){
2816 f.pendingOnError = undefined /*strip preceeding non-timeout error, if any*/;
2817 afterPollFetch(err);
2818 },
2819 onerror:function(err){
2820 Chat._isBatchLoading = false;
2821 if(Chat.beVerbose){
2822 console.error("poll.onerror:",err.name,err.status,JSON.stringify(err));
2823 }
2824 f.pendingOnError = err;
2825 setTimeout(f.delayPendingOnError, 100);
2826 },
2827 onload:function(y){
2828 reportConnectionOkay('poll.onload', true);
2829 newcontent(y);
2830 if(Chat._isBatchLoading){
2831 Chat._isBatchLoading = false;
2832 Chat.updateActiveUserList();
2833 }
2834 afterPollFetch();
2835 }
2836 });
2837 }/*poll()*/;
2838 poll.isFirstCall = true;
2839 Chat._gotServerError = poll.running = false;
2840 if( window.fossil.config.chat.fromcli ){
2841 Chat.chatOnlyMode(true);
2842 }
2843 Chat.timer.startPendingPollTimer();
2844 delete ForceResizeKludge.$disabled;
2845 ForceResizeKludge();
2846 Chat.animate.$disabled = false;
2847 setTimeout( ()=>Chat.inputFocus(), 0 );
2848 F.page.chat = Chat/* enables testing the APIs via the dev tools */;
2849 });
2850
--- src/fossil.popupwidget.js
+++ src/fossil.popupwidget.js
@@ -286,11 +286,11 @@
286286
};
287287
288288
F.toast = {
289289
config: {
290290
position: { x: 5, y: 5 /*viewport-relative, pixels*/ },
291
- displayTimeMs: 3000
291
+ displayTimeMs: 5000
292292
},
293293
/**
294294
Convenience wrapper around a PopupWidget which pops up a shared
295295
PopupWidget instance to show toast-style messages (commonly
296296
seen on Android). Its arguments may be anything suitable for
297297
--- src/fossil.popupwidget.js
+++ src/fossil.popupwidget.js
@@ -286,11 +286,11 @@
286 };
287
288 F.toast = {
289 config: {
290 position: { x: 5, y: 5 /*viewport-relative, pixels*/ },
291 displayTimeMs: 3000
292 },
293 /**
294 Convenience wrapper around a PopupWidget which pops up a shared
295 PopupWidget instance to show toast-style messages (commonly
296 seen on Android). Its arguments may be anything suitable for
297
--- src/fossil.popupwidget.js
+++ src/fossil.popupwidget.js
@@ -286,11 +286,11 @@
286 };
287
288 F.toast = {
289 config: {
290 position: { x: 5, y: 5 /*viewport-relative, pixels*/ },
291 displayTimeMs: 5000
292 },
293 /**
294 Convenience wrapper around a PopupWidget which pops up a shared
295 PopupWidget instance to show toast-style messages (commonly
296 seen on Android). Its arguments may be anything suitable for
297
+1 -1
--- src/graph.c
+++ src/graph.c
@@ -121,11 +121,11 @@
121121
u8 hasOffsetMergeRiser; /* Merge arrow from leaf goes up on a different
122122
** rail that the node */
123123
u8 bOverfull; /* Unable to allocate sufficient rails */
124124
u64 mergeRail; /* Rails used for merge lines */
125125
GraphRow **apHash; /* Hash table of GraphRow objects. Key: rid */
126
- u8 aiRailMap[GR_MAX_RAIL]; /* Mapping of rails to actually columns */
126
+ u8 aiRailMap[GR_MAX_RAIL+1]; /* Mapping of rails to actually columns */
127127
};
128128
129129
#endif
130130
131131
/* The N-th bit */
132132
--- src/graph.c
+++ src/graph.c
@@ -121,11 +121,11 @@
121 u8 hasOffsetMergeRiser; /* Merge arrow from leaf goes up on a different
122 ** rail that the node */
123 u8 bOverfull; /* Unable to allocate sufficient rails */
124 u64 mergeRail; /* Rails used for merge lines */
125 GraphRow **apHash; /* Hash table of GraphRow objects. Key: rid */
126 u8 aiRailMap[GR_MAX_RAIL]; /* Mapping of rails to actually columns */
127 };
128
129 #endif
130
131 /* The N-th bit */
132
--- src/graph.c
+++ src/graph.c
@@ -121,11 +121,11 @@
121 u8 hasOffsetMergeRiser; /* Merge arrow from leaf goes up on a different
122 ** rail that the node */
123 u8 bOverfull; /* Unable to allocate sufficient rails */
124 u64 mergeRail; /* Rails used for merge lines */
125 GraphRow **apHash; /* Hash table of GraphRow objects. Key: rid */
126 u8 aiRailMap[GR_MAX_RAIL+1]; /* Mapping of rails to actually columns */
127 };
128
129 #endif
130
131 /* The N-th bit */
132
--- src/graph.js
+++ src/graph.js
@@ -9,11 +9,10 @@
99
**
1010
** { "iTableId": INTEGER, // Table sequence number (NN)
1111
** "circleNodes": BOOLEAN, // True for circle nodes. False for squares
1212
** "showArrowheads": BOOLEAN, // True for arrowheads. False to omit
1313
** "iRailPitch": INTEGER, // Spacing between vertical lines (px)
14
-** "colorGraph": BOOLEAN, // True to put color on graph lines
1514
** "nomo": BOOLEAN, // True to join merge lines with rails
1615
** "iTopRow": INTEGER, // Index of top-most row in the graph
1716
** "omitDescenders": BOOLEAN, // Omit ancestor lines off bottom of screen
1817
** "fileDiff": BOOLEAN, // True for file diff. False for check-in
1918
** "scrollToSelect": BOOLEAN, // Scroll to selection on first render
2019
--- src/graph.js
+++ src/graph.js
@@ -9,11 +9,10 @@
9 **
10 ** { "iTableId": INTEGER, // Table sequence number (NN)
11 ** "circleNodes": BOOLEAN, // True for circle nodes. False for squares
12 ** "showArrowheads": BOOLEAN, // True for arrowheads. False to omit
13 ** "iRailPitch": INTEGER, // Spacing between vertical lines (px)
14 ** "colorGraph": BOOLEAN, // True to put color on graph lines
15 ** "nomo": BOOLEAN, // True to join merge lines with rails
16 ** "iTopRow": INTEGER, // Index of top-most row in the graph
17 ** "omitDescenders": BOOLEAN, // Omit ancestor lines off bottom of screen
18 ** "fileDiff": BOOLEAN, // True for file diff. False for check-in
19 ** "scrollToSelect": BOOLEAN, // Scroll to selection on first render
20
--- src/graph.js
+++ src/graph.js
@@ -9,11 +9,10 @@
9 **
10 ** { "iTableId": INTEGER, // Table sequence number (NN)
11 ** "circleNodes": BOOLEAN, // True for circle nodes. False for squares
12 ** "showArrowheads": BOOLEAN, // True for arrowheads. False to omit
13 ** "iRailPitch": INTEGER, // Spacing between vertical lines (px)
 
14 ** "nomo": BOOLEAN, // True to join merge lines with rails
15 ** "iTopRow": INTEGER, // Index of top-most row in the graph
16 ** "omitDescenders": BOOLEAN, // Omit ancestor lines off bottom of screen
17 ** "fileDiff": BOOLEAN, // True for file diff. False for check-in
18 ** "scrollToSelect": BOOLEAN, // Scroll to selection on first render
19
+12 -12
--- src/http_ssl.c
+++ src/http_ssl.c
@@ -319,11 +319,13 @@
319319
** The following OpenSSL configuration options must not be used for this feature
320320
** to be available: `no-autoalginit', `no-winstore'. The Fossil makefiles do not
321321
** currently set these options when building OpenSSL for Windows. */
322322
#if defined(_WIN32)
323323
#if OPENSSL_VERSION_NUMBER >= 0x030200000
324
- if( SSL_CTX_load_verify_store(sslCtx, "org.openssl.winstore:")==0 ){
324
+ if( SSLeay()!=0x30500000 /* Don't use for 3.5.0 due to a bug */
325
+ && SSL_CTX_load_verify_store(sslCtx, "org.openssl.winstore:")==0
326
+ ){
325327
fossil_print("NOTICE: Failed to load the Windows root certificates.\n");
326328
}
327329
#endif /* OPENSSL_VERSION_NUMBER >= 0x030200000 */
328330
#endif /* _WIN32 */
329331
@@ -999,12 +1001,12 @@
9991001
fossil_print("\n"
10001002
" The OpenSSL library is not used by this build of Fossil\n\n"
10011003
);
10021004
}
10031005
#else
1004
- fossil_print("OpenSSL-version: %s (0x%09x)\n",
1005
- SSLeay_version(SSLEAY_VERSION), OPENSSL_VERSION_NUMBER);
1006
+ fossil_print("OpenSSL-version: %s (0x%09llx)\n",
1007
+ SSLeay_version(SSLEAY_VERSION), (unsigned long long)SSLeay());
10061008
if( verbose ){
10071009
fossil_print("\n"
10081010
" The version of the OpenSSL library being used\n"
10091011
" by this instance of Fossil. Version 3.0.0 or\n"
10101012
" later is recommended.\n\n"
@@ -1061,20 +1063,18 @@
10611063
" values are built into your OpenSSL library.\n\n"
10621064
);
10631065
}
10641066
10651067
#if defined(_WIN32)
1066
-#if OPENSSL_VERSION_NUMBER >= 0x030200000
1067
- fossil_print(" OpenSSL-winstore: Yes\n");
1068
-#else /* OPENSSL_VERSION_NUMBER >= 0x030200000 */
1069
- fossil_print(" OpenSSL-winstore: No\n");
1070
-#endif /* OPENSSL_VERSION_NUMBER >= 0x030200000 */
1068
+ fossil_print(" OpenSSL-winstore: %s\n",
1069
+ (SSLeay()>=0x30200000 && SSLeay()!=0x30500000) ? "Yes" : "No");
10711070
if( verbose ){
10721071
fossil_print("\n"
1073
- " OpenSSL 3.2.0, or newer, use the root certificates managed by\n"
1074
- " the Windows operating system. The installed root certificates\n"
1075
- " are listed by the command:\n\n"
1072
+ " OpenSSL 3.2.0, or newer, but not version 3.5.0 due to a bug,\n"
1073
+ " are able to use the root certificates managed by the Windows\n"
1074
+ " operating system. The installed root certificates are listed\n"
1075
+ " by the command:\n\n"
10761076
" certutil -store \"ROOT\"\n\n"
10771077
);
10781078
}
10791079
#endif /* _WIN32 */
10801080
@@ -1232,10 +1232,10 @@
12321232
** freed by the caller.
12331233
*/
12341234
char *fossil_openssl_version(void){
12351235
#if defined(FOSSIL_ENABLE_SSL)
12361236
return mprintf("%s (0x%09x)\n",
1237
- SSLeay_version(SSLEAY_VERSION), OPENSSL_VERSION_NUMBER);
1237
+ SSLeay_version(SSLEAY_VERSION), (sqlite3_uint64)SSLeay());
12381238
#else
12391239
return mprintf("none");
12401240
#endif
12411241
}
12421242
--- src/http_ssl.c
+++ src/http_ssl.c
@@ -319,11 +319,13 @@
319 ** The following OpenSSL configuration options must not be used for this feature
320 ** to be available: `no-autoalginit', `no-winstore'. The Fossil makefiles do not
321 ** currently set these options when building OpenSSL for Windows. */
322 #if defined(_WIN32)
323 #if OPENSSL_VERSION_NUMBER >= 0x030200000
324 if( SSL_CTX_load_verify_store(sslCtx, "org.openssl.winstore:")==0 ){
 
 
325 fossil_print("NOTICE: Failed to load the Windows root certificates.\n");
326 }
327 #endif /* OPENSSL_VERSION_NUMBER >= 0x030200000 */
328 #endif /* _WIN32 */
329
@@ -999,12 +1001,12 @@
999 fossil_print("\n"
1000 " The OpenSSL library is not used by this build of Fossil\n\n"
1001 );
1002 }
1003 #else
1004 fossil_print("OpenSSL-version: %s (0x%09x)\n",
1005 SSLeay_version(SSLEAY_VERSION), OPENSSL_VERSION_NUMBER);
1006 if( verbose ){
1007 fossil_print("\n"
1008 " The version of the OpenSSL library being used\n"
1009 " by this instance of Fossil. Version 3.0.0 or\n"
1010 " later is recommended.\n\n"
@@ -1061,20 +1063,18 @@
1061 " values are built into your OpenSSL library.\n\n"
1062 );
1063 }
1064
1065 #if defined(_WIN32)
1066 #if OPENSSL_VERSION_NUMBER >= 0x030200000
1067 fossil_print(" OpenSSL-winstore: Yes\n");
1068 #else /* OPENSSL_VERSION_NUMBER >= 0x030200000 */
1069 fossil_print(" OpenSSL-winstore: No\n");
1070 #endif /* OPENSSL_VERSION_NUMBER >= 0x030200000 */
1071 if( verbose ){
1072 fossil_print("\n"
1073 " OpenSSL 3.2.0, or newer, use the root certificates managed by\n"
1074 " the Windows operating system. The installed root certificates\n"
1075 " are listed by the command:\n\n"
 
1076 " certutil -store \"ROOT\"\n\n"
1077 );
1078 }
1079 #endif /* _WIN32 */
1080
@@ -1232,10 +1232,10 @@
1232 ** freed by the caller.
1233 */
1234 char *fossil_openssl_version(void){
1235 #if defined(FOSSIL_ENABLE_SSL)
1236 return mprintf("%s (0x%09x)\n",
1237 SSLeay_version(SSLEAY_VERSION), OPENSSL_VERSION_NUMBER);
1238 #else
1239 return mprintf("none");
1240 #endif
1241 }
1242
--- src/http_ssl.c
+++ src/http_ssl.c
@@ -319,11 +319,13 @@
319 ** The following OpenSSL configuration options must not be used for this feature
320 ** to be available: `no-autoalginit', `no-winstore'. The Fossil makefiles do not
321 ** currently set these options when building OpenSSL for Windows. */
322 #if defined(_WIN32)
323 #if OPENSSL_VERSION_NUMBER >= 0x030200000
324 if( SSLeay()!=0x30500000 /* Don't use for 3.5.0 due to a bug */
325 && SSL_CTX_load_verify_store(sslCtx, "org.openssl.winstore:")==0
326 ){
327 fossil_print("NOTICE: Failed to load the Windows root certificates.\n");
328 }
329 #endif /* OPENSSL_VERSION_NUMBER >= 0x030200000 */
330 #endif /* _WIN32 */
331
@@ -999,12 +1001,12 @@
1001 fossil_print("\n"
1002 " The OpenSSL library is not used by this build of Fossil\n\n"
1003 );
1004 }
1005 #else
1006 fossil_print("OpenSSL-version: %s (0x%09llx)\n",
1007 SSLeay_version(SSLEAY_VERSION), (unsigned long long)SSLeay());
1008 if( verbose ){
1009 fossil_print("\n"
1010 " The version of the OpenSSL library being used\n"
1011 " by this instance of Fossil. Version 3.0.0 or\n"
1012 " later is recommended.\n\n"
@@ -1061,20 +1063,18 @@
1063 " values are built into your OpenSSL library.\n\n"
1064 );
1065 }
1066
1067 #if defined(_WIN32)
1068 fossil_print(" OpenSSL-winstore: %s\n",
1069 (SSLeay()>=0x30200000 && SSLeay()!=0x30500000) ? "Yes" : "No");
 
 
 
1070 if( verbose ){
1071 fossil_print("\n"
1072 " OpenSSL 3.2.0, or newer, but not version 3.5.0 due to a bug,\n"
1073 " are able to use the root certificates managed by the Windows\n"
1074 " operating system. The installed root certificates are listed\n"
1075 " by the command:\n\n"
1076 " certutil -store \"ROOT\"\n\n"
1077 );
1078 }
1079 #endif /* _WIN32 */
1080
@@ -1232,10 +1232,10 @@
1232 ** freed by the caller.
1233 */
1234 char *fossil_openssl_version(void){
1235 #if defined(FOSSIL_ENABLE_SSL)
1236 return mprintf("%s (0x%09x)\n",
1237 SSLeay_version(SSLEAY_VERSION), (sqlite3_uint64)SSLeay());
1238 #else
1239 return mprintf("none");
1240 #endif
1241 }
1242
+55 -25
--- src/info.c
+++ src/info.c
@@ -951,11 +951,11 @@
951951
const char *zOrigDate;
952952
int okWiki = 0;
953953
Blob wiki_read_links = BLOB_INITIALIZER;
954954
Blob wiki_add_links = BLOB_INITIALIZER;
955955
956
- Th_Store("current_checkin", zName);
956
+ Th_StoreUnsafe("current_checkin", zName);
957957
style_header("Check-in [%S]", zUuid);
958958
login_anonymous_available();
959959
zEUser = db_text(0,
960960
"SELECT value FROM tagxref"
961961
" WHERE tagid=%d AND rid=%d AND tagtype>0",
@@ -1182,14 +1182,14 @@
11821182
@ %z(chref("button","%R/%s/%T?diff=2%s",zPage,zName,zW))\
11831183
@ Side-by-Side&nbsp;Diff</a>
11841184
}
11851185
if( diffType!=0 ){
11861186
if( *zW ){
1187
- @ %z(chref("button","%R/%s/%T",zPage,zName))
1187
+ @ %z(chref("button","%R/%s/%T?diff=%d",zPage,zName,diffType))
11881188
@ Show&nbsp;Whitespace&nbsp;Changes</a>
11891189
}else{
1190
- @ %z(chref("button","%R/%s/%T?w",zPage,zName))
1190
+ @ %z(chref("button","%R/%s/%T?diff=%d&w",zPage,zName,diffType))
11911191
@ Ignore&nbsp;Whitespace</a>
11921192
}
11931193
}
11941194
if( zParent ){
11951195
@ %z(chref("button","%R/vpatch?from=%!S&to=%!S",zParent,zUuid))
@@ -1970,14 +1970,16 @@
19701970
diff_config_init(&DCfg, 0);
19711971
diffType = preferred_diff_type();
19721972
if( P("from") && P("to") ){
19731973
v1 = artifact_from_ci_and_filename("from");
19741974
v2 = artifact_from_ci_and_filename("to");
1975
+ if( v1==0 || v2==0 ) fossil_redirect_home();
19751976
}else{
19761977
Stmt q;
19771978
v1 = name_to_rid_www("v1");
19781979
v2 = name_to_rid_www("v2");
1980
+ if( v1==0 || v2==0 ) fossil_redirect_home();
19791981
19801982
/* If the two file versions being compared both have the same
19811983
** filename, then offer an "Annotate" link that constructs an
19821984
** annotation between those version. */
19831985
db_prepare(&q,
@@ -2003,11 +2005,10 @@
20032005
"%R/annotate?origin=%s&checkin=%s&filename=%T",
20042006
zOrig, zCkin, zFN);
20052007
}
20062008
db_finalize(&q);
20072009
}
2008
- if( v1==0 || v2==0 ) fossil_redirect_home();
20092010
zRe = P("regex");
20102011
cgi_check_for_malice();
20112012
if( zRe ) re_compile(&pRe, zRe, 0);
20122013
if( verbose ) objdescFlags |= OBJDESC_DETAIL;
20132014
if( isPatch ){
@@ -2622,24 +2623,28 @@
26222623
26232624
/*
26242625
** WEBPAGE: artifact
26252626
** WEBPAGE: file
26262627
** WEBPAGE: whatis
2628
+** WEBPAGE: docfile
26272629
**
26282630
** Typical usage:
26292631
**
26302632
** /artifact/HASH
26312633
** /whatis/HASH
26322634
** /file/NAME
2635
+** /docfile/NAME
26332636
**
26342637
** Additional query parameters:
26352638
**
26362639
** ln - show line numbers
26372640
** ln=N - highlight line number N
26382641
** ln=M-N - highlight lines M through N inclusive
26392642
** ln=M-N+Y-Z - highlight lines M through N and Y through Z (inclusive)
26402643
** verbose - show more detail in the description
2644
+** brief - show just the document, not the metadata. The
2645
+** /docfile page is an alias for /file?brief
26412646
** download - redirect to the download (artifact page only)
26422647
** name=NAME - filename or hash as a query parameter
26432648
** filename=NAME - alternative spelling for "name="
26442649
** fn=NAME - alternative spelling for "name="
26452650
** ci=VERSION - The specific check-in to use with "name=" to
@@ -2676,10 +2681,11 @@
26762681
int asText;
26772682
const char *zUuid = 0;
26782683
u32 objdescFlags = OBJDESC_BASE;
26792684
int descOnly = fossil_strcmp(g.zPath,"whatis")==0;
26802685
int hashOnly = P("hash")!=0;
2686
+ int docOnly = P("brief")!=0;
26812687
int isFile = fossil_strcmp(g.zPath,"file")==0;
26822688
const char *zLn = P("ln");
26832689
const char *zName = P("name");
26842690
const char *zCI = P("ci");
26852691
HQuery url;
@@ -2690,10 +2696,14 @@
26902696
26912697
login_check_credentials();
26922698
if( !g.perm.Read ){ login_needed(g.anon.Read); return; }
26932699
cgi_check_for_malice();
26942700
style_set_current_feature("artifact");
2701
+ if( fossil_strcmp(g.zPath, "docfile")==0 ){
2702
+ isFile = 1;
2703
+ docOnly = 1;
2704
+ }
26952705
26962706
/* Capture and normalize the name= and ci= query parameters */
26972707
if( zName==0 ){
26982708
zName = P("filename");
26992709
if( zName==0 ){
@@ -2802,11 +2812,13 @@
28022812
return;
28032813
}
28042814
28052815
asText = P("txt")!=0;
28062816
if( isFile ){
2807
- if( zCI==0 || fossil_strcmp(zCI,"tip")==0 ){
2817
+ if( docOnly ){
2818
+ /* No header */
2819
+ }else if( zCI==0 || fossil_strcmp(zCI,"tip")==0 ){
28082820
zCI = "tip";
28092821
@ <h2>File %z(href("%R/finfo?name=%T&m&ci=tip",zName))%h(zName)</a>
28102822
@ from the %z(href("%R/info/tip"))latest check-in</a></h2>
28112823
}else{
28122824
const char *zPath;
@@ -2824,17 +2836,19 @@
28242836
}else{
28252837
@ part of check-in %z(href("%R/info/%!S",zCIUuid))%S(zCIUuid)</a></h2>
28262838
}
28272839
blob_reset(&path);
28282840
}
2829
- style_submenu_element("Artifact", "%R/artifact/%S", zUuid);
28302841
zMime = mimetype_from_name(zName);
2831
- style_submenu_element("Annotate", "%R/annotate?filename=%T&checkin=%T",
2832
- zName, zCI);
2833
- style_submenu_element("Blame", "%R/blame?filename=%T&checkin=%T",
2834
- zName, zCI);
2835
- style_submenu_element("Doc", "%R/doc/%T/%T", zCI, zName);
2842
+ if( !docOnly ){
2843
+ style_submenu_element("Artifact", "%R/artifact/%S", zUuid);
2844
+ style_submenu_element("Annotate", "%R/annotate?filename=%T&checkin=%T",
2845
+ zName, zCI);
2846
+ style_submenu_element("Blame", "%R/blame?filename=%T&checkin=%T",
2847
+ zName, zCI);
2848
+ style_submenu_element("Doc", "%R/doc/%T/%T", zCI, zName);
2849
+ }
28362850
blob_init(&downloadName, zName, -1);
28372851
objType = OBJTYPE_CONTENT;
28382852
}else{
28392853
@ <h2>Artifact
28402854
style_copy_button(1, "hash-ar", 0, 2, "%s", zUuid);
@@ -2853,11 +2867,11 @@
28532867
cgi_redirectf("%R/raw/%s?at=%T",
28542868
db_text("x", "SELECT uuid FROM blob WHERE rid=%d", rid),
28552869
file_tail(blob_str(&downloadName)));
28562870
/*NOTREACHED*/
28572871
}
2858
- if( g.perm.Admin ){
2872
+ if( g.perm.Admin && !docOnly ){
28592873
const char *zUuid = db_text("", "SELECT uuid FROM blob WHERE rid=%d", rid);
28602874
if( db_exists("SELECT 1 FROM shun WHERE uuid=%Q", zUuid) ){
28612875
style_submenu_element("Unshun", "%R/shun?accept=%s&sub=1#accshun", zUuid);
28622876
}else{
28632877
style_submenu_element("Shun", "%R/shun?shun=%s#addshun",zUuid);
@@ -2898,41 +2912,49 @@
28982912
const char *zIp = db_column_text(&q,2);
28992913
@ <p>Received on %s(zDate) from %h(zUser) at %h(zIp).</p>
29002914
}
29012915
db_finalize(&q);
29022916
}
2903
- style_submenu_element("Download", "%R/raw/%s?at=%T", zUuid, file_tail(zName));
2904
- if( db_exists("SELECT 1 FROM mlink WHERE fid=%d", rid) ){
2905
- style_submenu_element("Check-ins Using", "%R/timeline?uf=%s", zUuid);
2917
+ if( !docOnly ){
2918
+ style_submenu_element("Download", "%R/raw/%s?at=%T",zUuid,file_tail(zName));
2919
+ if( db_exists("SELECT 1 FROM mlink WHERE fid=%d", rid) ){
2920
+ style_submenu_element("Check-ins Using", "%R/timeline?uf=%s", zUuid);
2921
+ }
29062922
}
29072923
if( zMime ){
29082924
if( fossil_strcmp(zMime, "text/html")==0 ){
29092925
if( asText ){
29102926
style_submenu_element("Html", "%s", url_render(&url, "txt", 0, 0, 0));
29112927
}else{
29122928
renderAsHtml = 1;
2913
- style_submenu_element("Text", "%s", url_render(&url, "txt", "1", 0, 0));
2929
+ if( !docOnly ){
2930
+ style_submenu_element("Text", "%s", url_render(&url, "txt","1",0,0));
2931
+ }
29142932
}
29152933
}else if( fossil_strcmp(zMime, "text/x-fossil-wiki")==0
29162934
|| fossil_strcmp(zMime, "text/x-markdown")==0
29172935
|| fossil_strcmp(zMime, "text/x-pikchr")==0 ){
29182936
if( asText ){
29192937
style_submenu_element(zMime[7]=='p' ? "Pikchr" : "Wiki",
29202938
"%s", url_render(&url, "txt", 0, 0, 0));
29212939
}else{
29222940
renderAsWiki = 1;
2923
- style_submenu_element("Text", "%s", url_render(&url, "txt", "1", 0, 0));
2941
+ if( !docOnly ){
2942
+ style_submenu_element("Text", "%s", url_render(&url, "txt","1",0,0));
2943
+ }
29242944
}
29252945
}else if( fossil_strcmp(zMime, "image/svg+xml")==0 ){
29262946
if( asText ){
29272947
style_submenu_element("Svg", "%s", url_render(&url, "txt", 0, 0, 0));
29282948
}else{
29292949
renderAsSvg = 1;
2930
- style_submenu_element("Text", "%s", url_render(&url, "txt", "1", 0, 0));
2950
+ if( !docOnly ){
2951
+ style_submenu_element("Text", "%s", url_render(&url, "txt","1",0,0));
2952
+ }
29312953
}
29322954
}
2933
- if( fileedit_is_editable(zName) ){
2955
+ if( !docOnly && fileedit_is_editable(zName) ){
29342956
style_submenu_element("Edit",
29352957
"%R/fileedit?filename=%T&checkin=%!S",
29362958
zName, zCI);
29372959
}
29382960
}
@@ -2940,11 +2962,13 @@
29402962
style_submenu_element("Parsed", "%R/info/%s", zUuid);
29412963
}
29422964
if( descOnly ){
29432965
style_submenu_element("Content", "%R/artifact/%s", zUuid);
29442966
}else{
2945
- @ <hr>
2967
+ if( !docOnly || !isFile ){
2968
+ @ <hr>
2969
+ }
29462970
content_get(rid, &content);
29472971
if( renderAsWiki ){
29482972
safe_html_context(DOCSRC_FILE);
29492973
wiki_render_by_mimetype(&content, zMime);
29502974
document_emit_js();
@@ -3610,10 +3634,11 @@
36103634
zNewColorFlag = P("newclr") ? " checked" : "";
36113635
zNewTagFlag = P("newtag") ? " checked" : "";
36123636
zNewTag = PDT("tagname","");
36133637
zNewBrFlag = P("newbr") ? " checked" : "";
36143638
zNewBranch = PDT("brname","");
3639
+ zBranchName = branch_of_rid(rid);
36153640
zCloseFlag = P("close") ? " checked" : "";
36163641
zHideFlag = P("hide") ? " checked" : "";
36173642
if( P("apply") && cgi_csrf_safe(2) ){
36183643
Blob ctrl;
36193644
char *zNow;
@@ -3657,17 +3682,25 @@
36573682
zUuid[10] = 0;
36583683
style_header("Edit Check-in [%s]", zUuid);
36593684
if( P("preview") ){
36603685
Blob suffix;
36613686
int nTag = 0;
3687
+ const char *zDplyBr; /* Branch name used to determine BG color */
3688
+ if( zNewBrFlag[0] && zNewBranch[0] ){
3689
+ zDplyBr = zNewBranch;
3690
+ }else{
3691
+ zDplyBr = zBranchName;
3692
+ }
36623693
@ <b>Preview:</b>
36633694
@ <blockquote>
36643695
@ <table border=0>
36653696
if( zNewColorFlag[0] && zNewColor && zNewColor[0] ){
3666
- @ <tr><td style="background-color: %h(zNewColor);">
3697
+ @ <tr><td style="background-color:%h(reasonable_bg_color(zNewColor,0));">
36673698
}else if( zColor[0] ){
3668
- @ <tr><td style="background-color: %h(zColor);">
3699
+ @ <tr><td style="background-color:%h(reasonable_bg_color(zColor,0));">
3700
+ }else if( zDplyBr && fossil_strcmp(zDplyBr,"trunk")!=0 ){
3701
+ @ <tr><td style="background-color:%h(hash_color(zDplyBr));">
36693702
}else{
36703703
@ <tr><td>
36713704
}
36723705
@ %!W(blob_str(&comment))
36733706
blob_zero(&suffix);
@@ -3748,13 +3781,10 @@
37483781
@ <tr><th align="right" valign="top">Tags:</th>
37493782
@ <td valign="top">
37503783
@ <label><input type="checkbox" id="newtag" name="newtag"%s(zNewTagFlag)>
37513784
@ Add the following new tag name to this check-in:</label>
37523785
@ <input size="15" name="tagname" id="tagname" value="%h(zNewTag)">
3753
- zBranchName = db_text(0, "SELECT value FROM tagxref, tag"
3754
- " WHERE tagxref.rid=%d AND tagtype>0 AND tagxref.tagid=tag.tagid"
3755
- " AND tagxref.tagid=%d", rid, TAG_BRANCH);
37563786
db_prepare(&q,
37573787
"SELECT tag.tagid, tagname, tagxref.value FROM tagxref, tag"
37583788
" WHERE tagxref.rid=%d AND tagtype>0 AND tagxref.tagid=tag.tagid"
37593789
" ORDER BY CASE WHEN tagname GLOB 'sym-*' THEN substr(tagname,5)"
37603790
" ELSE tagname END /*sort*/",
37613791
--- src/info.c
+++ src/info.c
@@ -951,11 +951,11 @@
951 const char *zOrigDate;
952 int okWiki = 0;
953 Blob wiki_read_links = BLOB_INITIALIZER;
954 Blob wiki_add_links = BLOB_INITIALIZER;
955
956 Th_Store("current_checkin", zName);
957 style_header("Check-in [%S]", zUuid);
958 login_anonymous_available();
959 zEUser = db_text(0,
960 "SELECT value FROM tagxref"
961 " WHERE tagid=%d AND rid=%d AND tagtype>0",
@@ -1182,14 +1182,14 @@
1182 @ %z(chref("button","%R/%s/%T?diff=2%s",zPage,zName,zW))\
1183 @ Side-by-Side&nbsp;Diff</a>
1184 }
1185 if( diffType!=0 ){
1186 if( *zW ){
1187 @ %z(chref("button","%R/%s/%T",zPage,zName))
1188 @ Show&nbsp;Whitespace&nbsp;Changes</a>
1189 }else{
1190 @ %z(chref("button","%R/%s/%T?w",zPage,zName))
1191 @ Ignore&nbsp;Whitespace</a>
1192 }
1193 }
1194 if( zParent ){
1195 @ %z(chref("button","%R/vpatch?from=%!S&to=%!S",zParent,zUuid))
@@ -1970,14 +1970,16 @@
1970 diff_config_init(&DCfg, 0);
1971 diffType = preferred_diff_type();
1972 if( P("from") && P("to") ){
1973 v1 = artifact_from_ci_and_filename("from");
1974 v2 = artifact_from_ci_and_filename("to");
 
1975 }else{
1976 Stmt q;
1977 v1 = name_to_rid_www("v1");
1978 v2 = name_to_rid_www("v2");
 
1979
1980 /* If the two file versions being compared both have the same
1981 ** filename, then offer an "Annotate" link that constructs an
1982 ** annotation between those version. */
1983 db_prepare(&q,
@@ -2003,11 +2005,10 @@
2003 "%R/annotate?origin=%s&checkin=%s&filename=%T",
2004 zOrig, zCkin, zFN);
2005 }
2006 db_finalize(&q);
2007 }
2008 if( v1==0 || v2==0 ) fossil_redirect_home();
2009 zRe = P("regex");
2010 cgi_check_for_malice();
2011 if( zRe ) re_compile(&pRe, zRe, 0);
2012 if( verbose ) objdescFlags |= OBJDESC_DETAIL;
2013 if( isPatch ){
@@ -2622,24 +2623,28 @@
2622
2623 /*
2624 ** WEBPAGE: artifact
2625 ** WEBPAGE: file
2626 ** WEBPAGE: whatis
 
2627 **
2628 ** Typical usage:
2629 **
2630 ** /artifact/HASH
2631 ** /whatis/HASH
2632 ** /file/NAME
 
2633 **
2634 ** Additional query parameters:
2635 **
2636 ** ln - show line numbers
2637 ** ln=N - highlight line number N
2638 ** ln=M-N - highlight lines M through N inclusive
2639 ** ln=M-N+Y-Z - highlight lines M through N and Y through Z (inclusive)
2640 ** verbose - show more detail in the description
 
 
2641 ** download - redirect to the download (artifact page only)
2642 ** name=NAME - filename or hash as a query parameter
2643 ** filename=NAME - alternative spelling for "name="
2644 ** fn=NAME - alternative spelling for "name="
2645 ** ci=VERSION - The specific check-in to use with "name=" to
@@ -2676,10 +2681,11 @@
2676 int asText;
2677 const char *zUuid = 0;
2678 u32 objdescFlags = OBJDESC_BASE;
2679 int descOnly = fossil_strcmp(g.zPath,"whatis")==0;
2680 int hashOnly = P("hash")!=0;
 
2681 int isFile = fossil_strcmp(g.zPath,"file")==0;
2682 const char *zLn = P("ln");
2683 const char *zName = P("name");
2684 const char *zCI = P("ci");
2685 HQuery url;
@@ -2690,10 +2696,14 @@
2690
2691 login_check_credentials();
2692 if( !g.perm.Read ){ login_needed(g.anon.Read); return; }
2693 cgi_check_for_malice();
2694 style_set_current_feature("artifact");
 
 
 
 
2695
2696 /* Capture and normalize the name= and ci= query parameters */
2697 if( zName==0 ){
2698 zName = P("filename");
2699 if( zName==0 ){
@@ -2802,11 +2812,13 @@
2802 return;
2803 }
2804
2805 asText = P("txt")!=0;
2806 if( isFile ){
2807 if( zCI==0 || fossil_strcmp(zCI,"tip")==0 ){
 
 
2808 zCI = "tip";
2809 @ <h2>File %z(href("%R/finfo?name=%T&m&ci=tip",zName))%h(zName)</a>
2810 @ from the %z(href("%R/info/tip"))latest check-in</a></h2>
2811 }else{
2812 const char *zPath;
@@ -2824,17 +2836,19 @@
2824 }else{
2825 @ part of check-in %z(href("%R/info/%!S",zCIUuid))%S(zCIUuid)</a></h2>
2826 }
2827 blob_reset(&path);
2828 }
2829 style_submenu_element("Artifact", "%R/artifact/%S", zUuid);
2830 zMime = mimetype_from_name(zName);
2831 style_submenu_element("Annotate", "%R/annotate?filename=%T&checkin=%T",
2832 zName, zCI);
2833 style_submenu_element("Blame", "%R/blame?filename=%T&checkin=%T",
2834 zName, zCI);
2835 style_submenu_element("Doc", "%R/doc/%T/%T", zCI, zName);
 
 
 
2836 blob_init(&downloadName, zName, -1);
2837 objType = OBJTYPE_CONTENT;
2838 }else{
2839 @ <h2>Artifact
2840 style_copy_button(1, "hash-ar", 0, 2, "%s", zUuid);
@@ -2853,11 +2867,11 @@
2853 cgi_redirectf("%R/raw/%s?at=%T",
2854 db_text("x", "SELECT uuid FROM blob WHERE rid=%d", rid),
2855 file_tail(blob_str(&downloadName)));
2856 /*NOTREACHED*/
2857 }
2858 if( g.perm.Admin ){
2859 const char *zUuid = db_text("", "SELECT uuid FROM blob WHERE rid=%d", rid);
2860 if( db_exists("SELECT 1 FROM shun WHERE uuid=%Q", zUuid) ){
2861 style_submenu_element("Unshun", "%R/shun?accept=%s&sub=1#accshun", zUuid);
2862 }else{
2863 style_submenu_element("Shun", "%R/shun?shun=%s#addshun",zUuid);
@@ -2898,41 +2912,49 @@
2898 const char *zIp = db_column_text(&q,2);
2899 @ <p>Received on %s(zDate) from %h(zUser) at %h(zIp).</p>
2900 }
2901 db_finalize(&q);
2902 }
2903 style_submenu_element("Download", "%R/raw/%s?at=%T", zUuid, file_tail(zName));
2904 if( db_exists("SELECT 1 FROM mlink WHERE fid=%d", rid) ){
2905 style_submenu_element("Check-ins Using", "%R/timeline?uf=%s", zUuid);
 
 
2906 }
2907 if( zMime ){
2908 if( fossil_strcmp(zMime, "text/html")==0 ){
2909 if( asText ){
2910 style_submenu_element("Html", "%s", url_render(&url, "txt", 0, 0, 0));
2911 }else{
2912 renderAsHtml = 1;
2913 style_submenu_element("Text", "%s", url_render(&url, "txt", "1", 0, 0));
 
 
2914 }
2915 }else if( fossil_strcmp(zMime, "text/x-fossil-wiki")==0
2916 || fossil_strcmp(zMime, "text/x-markdown")==0
2917 || fossil_strcmp(zMime, "text/x-pikchr")==0 ){
2918 if( asText ){
2919 style_submenu_element(zMime[7]=='p' ? "Pikchr" : "Wiki",
2920 "%s", url_render(&url, "txt", 0, 0, 0));
2921 }else{
2922 renderAsWiki = 1;
2923 style_submenu_element("Text", "%s", url_render(&url, "txt", "1", 0, 0));
 
 
2924 }
2925 }else if( fossil_strcmp(zMime, "image/svg+xml")==0 ){
2926 if( asText ){
2927 style_submenu_element("Svg", "%s", url_render(&url, "txt", 0, 0, 0));
2928 }else{
2929 renderAsSvg = 1;
2930 style_submenu_element("Text", "%s", url_render(&url, "txt", "1", 0, 0));
 
 
2931 }
2932 }
2933 if( fileedit_is_editable(zName) ){
2934 style_submenu_element("Edit",
2935 "%R/fileedit?filename=%T&checkin=%!S",
2936 zName, zCI);
2937 }
2938 }
@@ -2940,11 +2962,13 @@
2940 style_submenu_element("Parsed", "%R/info/%s", zUuid);
2941 }
2942 if( descOnly ){
2943 style_submenu_element("Content", "%R/artifact/%s", zUuid);
2944 }else{
2945 @ <hr>
 
 
2946 content_get(rid, &content);
2947 if( renderAsWiki ){
2948 safe_html_context(DOCSRC_FILE);
2949 wiki_render_by_mimetype(&content, zMime);
2950 document_emit_js();
@@ -3610,10 +3634,11 @@
3610 zNewColorFlag = P("newclr") ? " checked" : "";
3611 zNewTagFlag = P("newtag") ? " checked" : "";
3612 zNewTag = PDT("tagname","");
3613 zNewBrFlag = P("newbr") ? " checked" : "";
3614 zNewBranch = PDT("brname","");
 
3615 zCloseFlag = P("close") ? " checked" : "";
3616 zHideFlag = P("hide") ? " checked" : "";
3617 if( P("apply") && cgi_csrf_safe(2) ){
3618 Blob ctrl;
3619 char *zNow;
@@ -3657,17 +3682,25 @@
3657 zUuid[10] = 0;
3658 style_header("Edit Check-in [%s]", zUuid);
3659 if( P("preview") ){
3660 Blob suffix;
3661 int nTag = 0;
 
 
 
 
 
 
3662 @ <b>Preview:</b>
3663 @ <blockquote>
3664 @ <table border=0>
3665 if( zNewColorFlag[0] && zNewColor && zNewColor[0] ){
3666 @ <tr><td style="background-color: %h(zNewColor);">
3667 }else if( zColor[0] ){
3668 @ <tr><td style="background-color: %h(zColor);">
 
 
3669 }else{
3670 @ <tr><td>
3671 }
3672 @ %!W(blob_str(&comment))
3673 blob_zero(&suffix);
@@ -3748,13 +3781,10 @@
3748 @ <tr><th align="right" valign="top">Tags:</th>
3749 @ <td valign="top">
3750 @ <label><input type="checkbox" id="newtag" name="newtag"%s(zNewTagFlag)>
3751 @ Add the following new tag name to this check-in:</label>
3752 @ <input size="15" name="tagname" id="tagname" value="%h(zNewTag)">
3753 zBranchName = db_text(0, "SELECT value FROM tagxref, tag"
3754 " WHERE tagxref.rid=%d AND tagtype>0 AND tagxref.tagid=tag.tagid"
3755 " AND tagxref.tagid=%d", rid, TAG_BRANCH);
3756 db_prepare(&q,
3757 "SELECT tag.tagid, tagname, tagxref.value FROM tagxref, tag"
3758 " WHERE tagxref.rid=%d AND tagtype>0 AND tagxref.tagid=tag.tagid"
3759 " ORDER BY CASE WHEN tagname GLOB 'sym-*' THEN substr(tagname,5)"
3760 " ELSE tagname END /*sort*/",
3761
--- src/info.c
+++ src/info.c
@@ -951,11 +951,11 @@
951 const char *zOrigDate;
952 int okWiki = 0;
953 Blob wiki_read_links = BLOB_INITIALIZER;
954 Blob wiki_add_links = BLOB_INITIALIZER;
955
956 Th_StoreUnsafe("current_checkin", zName);
957 style_header("Check-in [%S]", zUuid);
958 login_anonymous_available();
959 zEUser = db_text(0,
960 "SELECT value FROM tagxref"
961 " WHERE tagid=%d AND rid=%d AND tagtype>0",
@@ -1182,14 +1182,14 @@
1182 @ %z(chref("button","%R/%s/%T?diff=2%s",zPage,zName,zW))\
1183 @ Side-by-Side&nbsp;Diff</a>
1184 }
1185 if( diffType!=0 ){
1186 if( *zW ){
1187 @ %z(chref("button","%R/%s/%T?diff=%d",zPage,zName,diffType))
1188 @ Show&nbsp;Whitespace&nbsp;Changes</a>
1189 }else{
1190 @ %z(chref("button","%R/%s/%T?diff=%d&w",zPage,zName,diffType))
1191 @ Ignore&nbsp;Whitespace</a>
1192 }
1193 }
1194 if( zParent ){
1195 @ %z(chref("button","%R/vpatch?from=%!S&to=%!S",zParent,zUuid))
@@ -1970,14 +1970,16 @@
1970 diff_config_init(&DCfg, 0);
1971 diffType = preferred_diff_type();
1972 if( P("from") && P("to") ){
1973 v1 = artifact_from_ci_and_filename("from");
1974 v2 = artifact_from_ci_and_filename("to");
1975 if( v1==0 || v2==0 ) fossil_redirect_home();
1976 }else{
1977 Stmt q;
1978 v1 = name_to_rid_www("v1");
1979 v2 = name_to_rid_www("v2");
1980 if( v1==0 || v2==0 ) fossil_redirect_home();
1981
1982 /* If the two file versions being compared both have the same
1983 ** filename, then offer an "Annotate" link that constructs an
1984 ** annotation between those version. */
1985 db_prepare(&q,
@@ -2003,11 +2005,10 @@
2005 "%R/annotate?origin=%s&checkin=%s&filename=%T",
2006 zOrig, zCkin, zFN);
2007 }
2008 db_finalize(&q);
2009 }
 
2010 zRe = P("regex");
2011 cgi_check_for_malice();
2012 if( zRe ) re_compile(&pRe, zRe, 0);
2013 if( verbose ) objdescFlags |= OBJDESC_DETAIL;
2014 if( isPatch ){
@@ -2622,24 +2623,28 @@
2623
2624 /*
2625 ** WEBPAGE: artifact
2626 ** WEBPAGE: file
2627 ** WEBPAGE: whatis
2628 ** WEBPAGE: docfile
2629 **
2630 ** Typical usage:
2631 **
2632 ** /artifact/HASH
2633 ** /whatis/HASH
2634 ** /file/NAME
2635 ** /docfile/NAME
2636 **
2637 ** Additional query parameters:
2638 **
2639 ** ln - show line numbers
2640 ** ln=N - highlight line number N
2641 ** ln=M-N - highlight lines M through N inclusive
2642 ** ln=M-N+Y-Z - highlight lines M through N and Y through Z (inclusive)
2643 ** verbose - show more detail in the description
2644 ** brief - show just the document, not the metadata. The
2645 ** /docfile page is an alias for /file?brief
2646 ** download - redirect to the download (artifact page only)
2647 ** name=NAME - filename or hash as a query parameter
2648 ** filename=NAME - alternative spelling for "name="
2649 ** fn=NAME - alternative spelling for "name="
2650 ** ci=VERSION - The specific check-in to use with "name=" to
@@ -2676,10 +2681,11 @@
2681 int asText;
2682 const char *zUuid = 0;
2683 u32 objdescFlags = OBJDESC_BASE;
2684 int descOnly = fossil_strcmp(g.zPath,"whatis")==0;
2685 int hashOnly = P("hash")!=0;
2686 int docOnly = P("brief")!=0;
2687 int isFile = fossil_strcmp(g.zPath,"file")==0;
2688 const char *zLn = P("ln");
2689 const char *zName = P("name");
2690 const char *zCI = P("ci");
2691 HQuery url;
@@ -2690,10 +2696,14 @@
2696
2697 login_check_credentials();
2698 if( !g.perm.Read ){ login_needed(g.anon.Read); return; }
2699 cgi_check_for_malice();
2700 style_set_current_feature("artifact");
2701 if( fossil_strcmp(g.zPath, "docfile")==0 ){
2702 isFile = 1;
2703 docOnly = 1;
2704 }
2705
2706 /* Capture and normalize the name= and ci= query parameters */
2707 if( zName==0 ){
2708 zName = P("filename");
2709 if( zName==0 ){
@@ -2802,11 +2812,13 @@
2812 return;
2813 }
2814
2815 asText = P("txt")!=0;
2816 if( isFile ){
2817 if( docOnly ){
2818 /* No header */
2819 }else if( zCI==0 || fossil_strcmp(zCI,"tip")==0 ){
2820 zCI = "tip";
2821 @ <h2>File %z(href("%R/finfo?name=%T&m&ci=tip",zName))%h(zName)</a>
2822 @ from the %z(href("%R/info/tip"))latest check-in</a></h2>
2823 }else{
2824 const char *zPath;
@@ -2824,17 +2836,19 @@
2836 }else{
2837 @ part of check-in %z(href("%R/info/%!S",zCIUuid))%S(zCIUuid)</a></h2>
2838 }
2839 blob_reset(&path);
2840 }
 
2841 zMime = mimetype_from_name(zName);
2842 if( !docOnly ){
2843 style_submenu_element("Artifact", "%R/artifact/%S", zUuid);
2844 style_submenu_element("Annotate", "%R/annotate?filename=%T&checkin=%T",
2845 zName, zCI);
2846 style_submenu_element("Blame", "%R/blame?filename=%T&checkin=%T",
2847 zName, zCI);
2848 style_submenu_element("Doc", "%R/doc/%T/%T", zCI, zName);
2849 }
2850 blob_init(&downloadName, zName, -1);
2851 objType = OBJTYPE_CONTENT;
2852 }else{
2853 @ <h2>Artifact
2854 style_copy_button(1, "hash-ar", 0, 2, "%s", zUuid);
@@ -2853,11 +2867,11 @@
2867 cgi_redirectf("%R/raw/%s?at=%T",
2868 db_text("x", "SELECT uuid FROM blob WHERE rid=%d", rid),
2869 file_tail(blob_str(&downloadName)));
2870 /*NOTREACHED*/
2871 }
2872 if( g.perm.Admin && !docOnly ){
2873 const char *zUuid = db_text("", "SELECT uuid FROM blob WHERE rid=%d", rid);
2874 if( db_exists("SELECT 1 FROM shun WHERE uuid=%Q", zUuid) ){
2875 style_submenu_element("Unshun", "%R/shun?accept=%s&sub=1#accshun", zUuid);
2876 }else{
2877 style_submenu_element("Shun", "%R/shun?shun=%s#addshun",zUuid);
@@ -2898,41 +2912,49 @@
2912 const char *zIp = db_column_text(&q,2);
2913 @ <p>Received on %s(zDate) from %h(zUser) at %h(zIp).</p>
2914 }
2915 db_finalize(&q);
2916 }
2917 if( !docOnly ){
2918 style_submenu_element("Download", "%R/raw/%s?at=%T",zUuid,file_tail(zName));
2919 if( db_exists("SELECT 1 FROM mlink WHERE fid=%d", rid) ){
2920 style_submenu_element("Check-ins Using", "%R/timeline?uf=%s", zUuid);
2921 }
2922 }
2923 if( zMime ){
2924 if( fossil_strcmp(zMime, "text/html")==0 ){
2925 if( asText ){
2926 style_submenu_element("Html", "%s", url_render(&url, "txt", 0, 0, 0));
2927 }else{
2928 renderAsHtml = 1;
2929 if( !docOnly ){
2930 style_submenu_element("Text", "%s", url_render(&url, "txt","1",0,0));
2931 }
2932 }
2933 }else if( fossil_strcmp(zMime, "text/x-fossil-wiki")==0
2934 || fossil_strcmp(zMime, "text/x-markdown")==0
2935 || fossil_strcmp(zMime, "text/x-pikchr")==0 ){
2936 if( asText ){
2937 style_submenu_element(zMime[7]=='p' ? "Pikchr" : "Wiki",
2938 "%s", url_render(&url, "txt", 0, 0, 0));
2939 }else{
2940 renderAsWiki = 1;
2941 if( !docOnly ){
2942 style_submenu_element("Text", "%s", url_render(&url, "txt","1",0,0));
2943 }
2944 }
2945 }else if( fossil_strcmp(zMime, "image/svg+xml")==0 ){
2946 if( asText ){
2947 style_submenu_element("Svg", "%s", url_render(&url, "txt", 0, 0, 0));
2948 }else{
2949 renderAsSvg = 1;
2950 if( !docOnly ){
2951 style_submenu_element("Text", "%s", url_render(&url, "txt","1",0,0));
2952 }
2953 }
2954 }
2955 if( !docOnly && fileedit_is_editable(zName) ){
2956 style_submenu_element("Edit",
2957 "%R/fileedit?filename=%T&checkin=%!S",
2958 zName, zCI);
2959 }
2960 }
@@ -2940,11 +2962,13 @@
2962 style_submenu_element("Parsed", "%R/info/%s", zUuid);
2963 }
2964 if( descOnly ){
2965 style_submenu_element("Content", "%R/artifact/%s", zUuid);
2966 }else{
2967 if( !docOnly || !isFile ){
2968 @ <hr>
2969 }
2970 content_get(rid, &content);
2971 if( renderAsWiki ){
2972 safe_html_context(DOCSRC_FILE);
2973 wiki_render_by_mimetype(&content, zMime);
2974 document_emit_js();
@@ -3610,10 +3634,11 @@
3634 zNewColorFlag = P("newclr") ? " checked" : "";
3635 zNewTagFlag = P("newtag") ? " checked" : "";
3636 zNewTag = PDT("tagname","");
3637 zNewBrFlag = P("newbr") ? " checked" : "";
3638 zNewBranch = PDT("brname","");
3639 zBranchName = branch_of_rid(rid);
3640 zCloseFlag = P("close") ? " checked" : "";
3641 zHideFlag = P("hide") ? " checked" : "";
3642 if( P("apply") && cgi_csrf_safe(2) ){
3643 Blob ctrl;
3644 char *zNow;
@@ -3657,17 +3682,25 @@
3682 zUuid[10] = 0;
3683 style_header("Edit Check-in [%s]", zUuid);
3684 if( P("preview") ){
3685 Blob suffix;
3686 int nTag = 0;
3687 const char *zDplyBr; /* Branch name used to determine BG color */
3688 if( zNewBrFlag[0] && zNewBranch[0] ){
3689 zDplyBr = zNewBranch;
3690 }else{
3691 zDplyBr = zBranchName;
3692 }
3693 @ <b>Preview:</b>
3694 @ <blockquote>
3695 @ <table border=0>
3696 if( zNewColorFlag[0] && zNewColor && zNewColor[0] ){
3697 @ <tr><td style="background-color:%h(reasonable_bg_color(zNewColor,0));">
3698 }else if( zColor[0] ){
3699 @ <tr><td style="background-color:%h(reasonable_bg_color(zColor,0));">
3700 }else if( zDplyBr && fossil_strcmp(zDplyBr,"trunk")!=0 ){
3701 @ <tr><td style="background-color:%h(hash_color(zDplyBr));">
3702 }else{
3703 @ <tr><td>
3704 }
3705 @ %!W(blob_str(&comment))
3706 blob_zero(&suffix);
@@ -3748,13 +3781,10 @@
3781 @ <tr><th align="right" valign="top">Tags:</th>
3782 @ <td valign="top">
3783 @ <label><input type="checkbox" id="newtag" name="newtag"%s(zNewTagFlag)>
3784 @ Add the following new tag name to this check-in:</label>
3785 @ <input size="15" name="tagname" id="tagname" value="%h(zNewTag)">
 
 
 
3786 db_prepare(&q,
3787 "SELECT tag.tagid, tagname, tagxref.value FROM tagxref, tag"
3788 " WHERE tagxref.rid=%d AND tagtype>0 AND tagxref.tagid=tag.tagid"
3789 " ORDER BY CASE WHEN tagname GLOB 'sym-*' THEN substr(tagname,5)"
3790 " ELSE tagname END /*sort*/",
3791
+21 -8
--- src/loadctrl.c
+++ src/loadctrl.c
@@ -43,10 +43,30 @@
4343
** Print the load average on the host machine.
4444
*/
4545
void loadavg_test_cmd(void){
4646
fossil_print("load-average: %f\n", load_average());
4747
}
48
+
49
+/*
50
+** WEBPAGE: test-overload
51
+**
52
+** Generate the response that would normally be shown only when
53
+** service is denied due to an overload condition. This is for
54
+** testing of the overload warning page.
55
+*/
56
+void overload_page(void){
57
+ double mxLoad = atof(db_get("max-loadavg", "0.0"));
58
+ style_set_current_feature("test");
59
+ style_header("Server Overload");
60
+ @ <h2>The server load is currently too high.
61
+ @ Please try again later.</h2>
62
+ @ <p>Current load average: %f(load_average())<br>
63
+ @ Load average limit: %f(mxLoad)<br>
64
+ @ URL: %h(g.zBaseURL)%h(P("PATH_INFO"))<br>
65
+ @ Timestamp: %h(db_text("","SELECT datetime()"))Z</p>
66
+ style_finish_page();
67
+}
4868
4969
/*
5070
** Abort the current page request if the load average of the host
5171
** computer is too high. Admin and Setup users are exempt from this
5272
** restriction.
@@ -60,17 +80,10 @@
6080
login_check_credentials();
6181
if(g.perm.Admin || g.perm.Setup){
6282
return;
6383
}
6484
#endif
65
-
66
- style_set_current_feature("test");
67
- style_header("Server Overload");
68
- @ <h2>The server load is currently too high.
69
- @ Please try again later.</h2>
70
- @ <p>Current load average: %f(load_average()).<br>
71
- @ Load average limit: %f(mxLoad)</p>
72
- style_finish_page();
85
+ overload_page();
7386
cgi_set_status(503,"Server Overload");
7487
cgi_reply();
7588
exit(0);
7689
}
7790
--- src/loadctrl.c
+++ src/loadctrl.c
@@ -43,10 +43,30 @@
43 ** Print the load average on the host machine.
44 */
45 void loadavg_test_cmd(void){
46 fossil_print("load-average: %f\n", load_average());
47 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
49 /*
50 ** Abort the current page request if the load average of the host
51 ** computer is too high. Admin and Setup users are exempt from this
52 ** restriction.
@@ -60,17 +80,10 @@
60 login_check_credentials();
61 if(g.perm.Admin || g.perm.Setup){
62 return;
63 }
64 #endif
65
66 style_set_current_feature("test");
67 style_header("Server Overload");
68 @ <h2>The server load is currently too high.
69 @ Please try again later.</h2>
70 @ <p>Current load average: %f(load_average()).<br>
71 @ Load average limit: %f(mxLoad)</p>
72 style_finish_page();
73 cgi_set_status(503,"Server Overload");
74 cgi_reply();
75 exit(0);
76 }
77
--- src/loadctrl.c
+++ src/loadctrl.c
@@ -43,10 +43,30 @@
43 ** Print the load average on the host machine.
44 */
45 void loadavg_test_cmd(void){
46 fossil_print("load-average: %f\n", load_average());
47 }
48
49 /*
50 ** WEBPAGE: test-overload
51 **
52 ** Generate the response that would normally be shown only when
53 ** service is denied due to an overload condition. This is for
54 ** testing of the overload warning page.
55 */
56 void overload_page(void){
57 double mxLoad = atof(db_get("max-loadavg", "0.0"));
58 style_set_current_feature("test");
59 style_header("Server Overload");
60 @ <h2>The server load is currently too high.
61 @ Please try again later.</h2>
62 @ <p>Current load average: %f(load_average())<br>
63 @ Load average limit: %f(mxLoad)<br>
64 @ URL: %h(g.zBaseURL)%h(P("PATH_INFO"))<br>
65 @ Timestamp: %h(db_text("","SELECT datetime()"))Z</p>
66 style_finish_page();
67 }
68
69 /*
70 ** Abort the current page request if the load average of the host
71 ** computer is too high. Admin and Setup users are exempt from this
72 ** restriction.
@@ -60,17 +80,10 @@
80 login_check_credentials();
81 if(g.perm.Admin || g.perm.Setup){
82 return;
83 }
84 #endif
85 overload_page();
 
 
 
 
 
 
 
86 cgi_set_status(503,"Server Overload");
87 cgi_reply();
88 exit(0);
89 }
90
+18 -2
--- src/login.c
+++ src/login.c
@@ -1299,20 +1299,36 @@
12991299
** that should restrict robot access. No restrictions
13001300
** are applied if this setting is undefined or is
13011301
** an empty string.
13021302
*/
13031303
void login_restrict_robot_access(void){
1304
- const char *zReferer;
13051304
const char *zGlob;
13061305
int isMatch = 1;
13071306
int nQP; /* Number of query parameters other than name= */
13081307
if( g.zLogin!=0 ) return;
13091308
zGlob = db_get("robot-restrict",0);
13101309
if( zGlob==0 || zGlob[0]==0 ) return;
13111310
if( g.isHuman ){
1311
+ const char *zReferer;
1312
+ const char *zAccept;
1313
+ const char *zBr;
13121314
zReferer = P("HTTP_REFERER");
13131315
if( zReferer && zReferer[0]!=0 ) return;
1316
+
1317
+ /* Robots typically do not accept the brotli encoding, at least not
1318
+ ** at the time of this writing (2025-04-01), but standard web-browser
1319
+ ** all generally do accept brotli. So if brotli is accepted,
1320
+ ** assume we are not talking to a robot. We might want to revisit this
1321
+ ** heuristic in the future...
1322
+ */
1323
+ if( (zAccept = P("HTTP_ACCEPT_ENCODING"))!=0
1324
+ && (zBr = strstr(zAccept,"br"))!=0
1325
+ && !fossil_isalnum(zBr[2])
1326
+ && (zBr==zAccept || !fossil_isalnum(zBr[-1]))
1327
+ ){
1328
+ return;
1329
+ }
13141330
}
13151331
nQP = cgi_qp_count();
13161332
if( nQP<1 ) return;
13171333
isMatch = glob_multi_match(zGlob, g.zPath);
13181334
if( !isMatch ) return;
@@ -1400,11 +1416,11 @@
14001416
*/
14011417
zIpAddr = PD("REMOTE_ADDR","nil");
14021418
if( ( cgi_is_loopback(zIpAddr)
14031419
|| (g.fSshClient & CGI_SSH_CLIENT)!=0 )
14041420
&& g.useLocalauth
1405
- && db_get_int("localauth",0)==0
1421
+ && db_get_boolean("localauth",0)==0
14061422
&& P("HTTPS")==0
14071423
){
14081424
char *zSeed;
14091425
if( g.localOpen ) zLogin = db_lget("default-user",0);
14101426
if( zLogin!=0 ){
14111427
--- src/login.c
+++ src/login.c
@@ -1299,20 +1299,36 @@
1299 ** that should restrict robot access. No restrictions
1300 ** are applied if this setting is undefined or is
1301 ** an empty string.
1302 */
1303 void login_restrict_robot_access(void){
1304 const char *zReferer;
1305 const char *zGlob;
1306 int isMatch = 1;
1307 int nQP; /* Number of query parameters other than name= */
1308 if( g.zLogin!=0 ) return;
1309 zGlob = db_get("robot-restrict",0);
1310 if( zGlob==0 || zGlob[0]==0 ) return;
1311 if( g.isHuman ){
 
 
 
1312 zReferer = P("HTTP_REFERER");
1313 if( zReferer && zReferer[0]!=0 ) return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1314 }
1315 nQP = cgi_qp_count();
1316 if( nQP<1 ) return;
1317 isMatch = glob_multi_match(zGlob, g.zPath);
1318 if( !isMatch ) return;
@@ -1400,11 +1416,11 @@
1400 */
1401 zIpAddr = PD("REMOTE_ADDR","nil");
1402 if( ( cgi_is_loopback(zIpAddr)
1403 || (g.fSshClient & CGI_SSH_CLIENT)!=0 )
1404 && g.useLocalauth
1405 && db_get_int("localauth",0)==0
1406 && P("HTTPS")==0
1407 ){
1408 char *zSeed;
1409 if( g.localOpen ) zLogin = db_lget("default-user",0);
1410 if( zLogin!=0 ){
1411
--- src/login.c
+++ src/login.c
@@ -1299,20 +1299,36 @@
1299 ** that should restrict robot access. No restrictions
1300 ** are applied if this setting is undefined or is
1301 ** an empty string.
1302 */
1303 void login_restrict_robot_access(void){
 
1304 const char *zGlob;
1305 int isMatch = 1;
1306 int nQP; /* Number of query parameters other than name= */
1307 if( g.zLogin!=0 ) return;
1308 zGlob = db_get("robot-restrict",0);
1309 if( zGlob==0 || zGlob[0]==0 ) return;
1310 if( g.isHuman ){
1311 const char *zReferer;
1312 const char *zAccept;
1313 const char *zBr;
1314 zReferer = P("HTTP_REFERER");
1315 if( zReferer && zReferer[0]!=0 ) return;
1316
1317 /* Robots typically do not accept the brotli encoding, at least not
1318 ** at the time of this writing (2025-04-01), but standard web-browser
1319 ** all generally do accept brotli. So if brotli is accepted,
1320 ** assume we are not talking to a robot. We might want to revisit this
1321 ** heuristic in the future...
1322 */
1323 if( (zAccept = P("HTTP_ACCEPT_ENCODING"))!=0
1324 && (zBr = strstr(zAccept,"br"))!=0
1325 && !fossil_isalnum(zBr[2])
1326 && (zBr==zAccept || !fossil_isalnum(zBr[-1]))
1327 ){
1328 return;
1329 }
1330 }
1331 nQP = cgi_qp_count();
1332 if( nQP<1 ) return;
1333 isMatch = glob_multi_match(zGlob, g.zPath);
1334 if( !isMatch ) return;
@@ -1400,11 +1416,11 @@
1416 */
1417 zIpAddr = PD("REMOTE_ADDR","nil");
1418 if( ( cgi_is_loopback(zIpAddr)
1419 || (g.fSshClient & CGI_SSH_CLIENT)!=0 )
1420 && g.useLocalauth
1421 && db_get_boolean("localauth",0)==0
1422 && P("HTTPS")==0
1423 ){
1424 char *zSeed;
1425 if( g.localOpen ) zLogin = db_lget("default-user",0);
1426 if( zLogin!=0 ){
1427
+33 -21
--- src/lookslike.c
+++ src/lookslike.c
@@ -478,54 +478,66 @@
478478
}
479479
480480
/*
481481
** Returns true if the given text contains certain keywords or
482482
** punctuation which indicate that it might be an SQL injection attempt
483
-** or some other kind of mischief.
483
+** or Cross-site scripting attempt or some other kind of mischief.
484484
**
485
-** This is not a defense against vulnerabilities in the Fossil code.
486
-** Rather, this is part of an effort to do early detection of malicious
487
-** spiders to avoid them using up too many CPU cycles.
485
+** This is not a primary defense against vulnerabilities in the Fossil
486
+** code. Rather, this is part of an effort to do early detection of malicious
487
+** spiders to avoid them using up too many CPU cycles. Or, this routine
488
+** can also be thought of as a secondary layer of defense against attacks.
488489
*/
489
-int looks_like_sql_injection(const char *zTxt){
490
+int looks_like_attack(const char *zTxt){
490491
unsigned int i;
492
+ int rc = 0;
491493
if( zTxt==0 ) return 0;
492494
for(i=0; zTxt[i]; i++){
493495
switch( zTxt[i] ){
496
+ case '<':
494497
case ';':
495498
case '\'':
496499
return 1;
497500
case '/': /* 0123456789 123456789 */
498
- if( strncmp(zTxt+i+1, "/wp-content/plugins/", 20)==0 ) return 1;
499
- if( strncmp(zTxt+i+1, "/wp-admin/admin-ajax", 20)==0 ) return 1;
501
+ if( strncmp(zTxt+i+1, "/wp-content/plugins/", 20)==0 ) rc = 1;
502
+ if( strncmp(zTxt+i+1, "/wp-admin/admin-ajax", 20)==0 ) rc = 1;
500503
break;
501504
case 'a':
502505
case 'A':
503
- if( isWholeWord(zTxt, i, "and", 3) ) return 1;
506
+ if( isWholeWord(zTxt, i, "and", 3) ) rc = 1;
504507
break;
505508
case 'n':
506509
case 'N':
507
- if( isWholeWord(zTxt, i, "null", 4) ) return 1;
510
+ if( isWholeWord(zTxt, i, "null", 4) ) rc = 1;
508511
break;
509512
case 'o':
510513
case 'O':
511514
if( isWholeWord(zTxt, i, "order", 5) && fossil_isspace(zTxt[i+5]) ){
512
- return 1;
515
+ rc = 1;
513516
}
514
- if( isWholeWord(zTxt, i, "or", 2) ) return 1;
517
+ if( isWholeWord(zTxt, i, "or", 2) ) rc = 1;
515518
break;
516519
case 's':
517520
case 'S':
518
- if( isWholeWord(zTxt, i, "select", 6) ) return 1;
521
+ if( isWholeWord(zTxt, i, "select", 6) ) rc = 1;
519522
break;
520523
case 'w':
521524
case 'W':
522
- if( isWholeWord(zTxt, i, "waitfor", 7) ) return 1;
525
+ if( isWholeWord(zTxt, i, "waitfor", 7) ) rc = 1;
523526
break;
524527
}
525528
}
526
- return 0;
529
+ if( rc ){
530
+ /* The test/markdown-test3.md document which is part of the Fossil source
531
+ ** tree intentionally tries to fake an attack. Do not report such
532
+ ** errors. */
533
+ const char *zPathInfo = P("PATH_INFO");
534
+ if( sqlite3_strglob("/doc/*/test/markdown-test3.md", zPathInfo)==0 ){
535
+ rc = 0;
536
+ }
537
+ }
538
+ return rc;
527539
}
528540
529541
/*
530542
** This is a utility routine associated with the test-looks-like-sql-injection
531543
** command.
@@ -534,11 +546,11 @@
534546
** might be SQL injection.
535547
**
536548
** Or if bInvert is true, then show the opposite - those lines that do NOT
537549
** look like SQL injection.
538550
*/
539
-static void show_sql_injection_lines(
551
+static void show_attack_lines(
540552
const char *zInFile, /* Name of input file */
541553
int bInvert, /* Invert the sense of the output (-v) */
542554
int bDeHttpize /* De-httpize the inputs. (-d) */
543555
){
544556
FILE *in;
@@ -551,34 +563,34 @@
551563
fossil_fatal("cannot open \"%s\" for reading\n", zInFile);
552564
}
553565
}
554566
while( fgets(zLine, sizeof(zLine), in) ){
555567
dehttpize(zLine);
556
- if( (looks_like_sql_injection(zLine)!=0) ^ bInvert ){
568
+ if( (looks_like_attack(zLine)!=0) ^ bInvert ){
557569
fossil_print("%s", zLine);
558570
}
559571
}
560572
if( in!=stdin ) fclose(in);
561573
}
562574
563575
/*
564
-** COMMAND: test-looks-like-sql-injection
576
+** COMMAND: test-looks-like-attack
565577
**
566578
** Read lines of input from files named as arguments (or from standard
567579
** input if no arguments are provided) and print those that look like they
568580
** might be part of an SQL injection attack.
569581
**
570
-** Used to test the looks_lide_sql_injection() utility subroutine, possibly
582
+** Used to test the looks_lile_attack() utility subroutine, possibly
571583
** by piping in actual server log data.
572584
*/
573
-void test_looks_like_sql_injection(void){
585
+void test_looks_like_attack(void){
574586
int i;
575587
int bInvert = find_option("invert","v",0)!=0;
576588
int bDeHttpize = find_option("dehttpize","d",0)!=0;
577589
verify_all_options();
578590
if( g.argc==2 ){
579
- show_sql_injection_lines(0, bInvert, bDeHttpize);
591
+ show_attack_lines(0, bInvert, bDeHttpize);
580592
}
581593
for(i=2; i<g.argc; i++){
582
- show_sql_injection_lines(g.argv[i], bInvert, bDeHttpize);
594
+ show_attack_lines(g.argv[i], bInvert, bDeHttpize);
583595
}
584596
}
585597
--- src/lookslike.c
+++ src/lookslike.c
@@ -478,54 +478,66 @@
478 }
479
480 /*
481 ** Returns true if the given text contains certain keywords or
482 ** punctuation which indicate that it might be an SQL injection attempt
483 ** or some other kind of mischief.
484 **
485 ** This is not a defense against vulnerabilities in the Fossil code.
486 ** Rather, this is part of an effort to do early detection of malicious
487 ** spiders to avoid them using up too many CPU cycles.
 
488 */
489 int looks_like_sql_injection(const char *zTxt){
490 unsigned int i;
 
491 if( zTxt==0 ) return 0;
492 for(i=0; zTxt[i]; i++){
493 switch( zTxt[i] ){
 
494 case ';':
495 case '\'':
496 return 1;
497 case '/': /* 0123456789 123456789 */
498 if( strncmp(zTxt+i+1, "/wp-content/plugins/", 20)==0 ) return 1;
499 if( strncmp(zTxt+i+1, "/wp-admin/admin-ajax", 20)==0 ) return 1;
500 break;
501 case 'a':
502 case 'A':
503 if( isWholeWord(zTxt, i, "and", 3) ) return 1;
504 break;
505 case 'n':
506 case 'N':
507 if( isWholeWord(zTxt, i, "null", 4) ) return 1;
508 break;
509 case 'o':
510 case 'O':
511 if( isWholeWord(zTxt, i, "order", 5) && fossil_isspace(zTxt[i+5]) ){
512 return 1;
513 }
514 if( isWholeWord(zTxt, i, "or", 2) ) return 1;
515 break;
516 case 's':
517 case 'S':
518 if( isWholeWord(zTxt, i, "select", 6) ) return 1;
519 break;
520 case 'w':
521 case 'W':
522 if( isWholeWord(zTxt, i, "waitfor", 7) ) return 1;
523 break;
524 }
525 }
526 return 0;
 
 
 
 
 
 
 
 
 
527 }
528
529 /*
530 ** This is a utility routine associated with the test-looks-like-sql-injection
531 ** command.
@@ -534,11 +546,11 @@
534 ** might be SQL injection.
535 **
536 ** Or if bInvert is true, then show the opposite - those lines that do NOT
537 ** look like SQL injection.
538 */
539 static void show_sql_injection_lines(
540 const char *zInFile, /* Name of input file */
541 int bInvert, /* Invert the sense of the output (-v) */
542 int bDeHttpize /* De-httpize the inputs. (-d) */
543 ){
544 FILE *in;
@@ -551,34 +563,34 @@
551 fossil_fatal("cannot open \"%s\" for reading\n", zInFile);
552 }
553 }
554 while( fgets(zLine, sizeof(zLine), in) ){
555 dehttpize(zLine);
556 if( (looks_like_sql_injection(zLine)!=0) ^ bInvert ){
557 fossil_print("%s", zLine);
558 }
559 }
560 if( in!=stdin ) fclose(in);
561 }
562
563 /*
564 ** COMMAND: test-looks-like-sql-injection
565 **
566 ** Read lines of input from files named as arguments (or from standard
567 ** input if no arguments are provided) and print those that look like they
568 ** might be part of an SQL injection attack.
569 **
570 ** Used to test the looks_lide_sql_injection() utility subroutine, possibly
571 ** by piping in actual server log data.
572 */
573 void test_looks_like_sql_injection(void){
574 int i;
575 int bInvert = find_option("invert","v",0)!=0;
576 int bDeHttpize = find_option("dehttpize","d",0)!=0;
577 verify_all_options();
578 if( g.argc==2 ){
579 show_sql_injection_lines(0, bInvert, bDeHttpize);
580 }
581 for(i=2; i<g.argc; i++){
582 show_sql_injection_lines(g.argv[i], bInvert, bDeHttpize);
583 }
584 }
585
--- src/lookslike.c
+++ src/lookslike.c
@@ -478,54 +478,66 @@
478 }
479
480 /*
481 ** Returns true if the given text contains certain keywords or
482 ** punctuation which indicate that it might be an SQL injection attempt
483 ** or Cross-site scripting attempt or some other kind of mischief.
484 **
485 ** This is not a primary defense against vulnerabilities in the Fossil
486 ** code. Rather, this is part of an effort to do early detection of malicious
487 ** spiders to avoid them using up too many CPU cycles. Or, this routine
488 ** can also be thought of as a secondary layer of defense against attacks.
489 */
490 int looks_like_attack(const char *zTxt){
491 unsigned int i;
492 int rc = 0;
493 if( zTxt==0 ) return 0;
494 for(i=0; zTxt[i]; i++){
495 switch( zTxt[i] ){
496 case '<':
497 case ';':
498 case '\'':
499 return 1;
500 case '/': /* 0123456789 123456789 */
501 if( strncmp(zTxt+i+1, "/wp-content/plugins/", 20)==0 ) rc = 1;
502 if( strncmp(zTxt+i+1, "/wp-admin/admin-ajax", 20)==0 ) rc = 1;
503 break;
504 case 'a':
505 case 'A':
506 if( isWholeWord(zTxt, i, "and", 3) ) rc = 1;
507 break;
508 case 'n':
509 case 'N':
510 if( isWholeWord(zTxt, i, "null", 4) ) rc = 1;
511 break;
512 case 'o':
513 case 'O':
514 if( isWholeWord(zTxt, i, "order", 5) && fossil_isspace(zTxt[i+5]) ){
515 rc = 1;
516 }
517 if( isWholeWord(zTxt, i, "or", 2) ) rc = 1;
518 break;
519 case 's':
520 case 'S':
521 if( isWholeWord(zTxt, i, "select", 6) ) rc = 1;
522 break;
523 case 'w':
524 case 'W':
525 if( isWholeWord(zTxt, i, "waitfor", 7) ) rc = 1;
526 break;
527 }
528 }
529 if( rc ){
530 /* The test/markdown-test3.md document which is part of the Fossil source
531 ** tree intentionally tries to fake an attack. Do not report such
532 ** errors. */
533 const char *zPathInfo = P("PATH_INFO");
534 if( sqlite3_strglob("/doc/*/test/markdown-test3.md", zPathInfo)==0 ){
535 rc = 0;
536 }
537 }
538 return rc;
539 }
540
541 /*
542 ** This is a utility routine associated with the test-looks-like-sql-injection
543 ** command.
@@ -534,11 +546,11 @@
546 ** might be SQL injection.
547 **
548 ** Or if bInvert is true, then show the opposite - those lines that do NOT
549 ** look like SQL injection.
550 */
551 static void show_attack_lines(
552 const char *zInFile, /* Name of input file */
553 int bInvert, /* Invert the sense of the output (-v) */
554 int bDeHttpize /* De-httpize the inputs. (-d) */
555 ){
556 FILE *in;
@@ -551,34 +563,34 @@
563 fossil_fatal("cannot open \"%s\" for reading\n", zInFile);
564 }
565 }
566 while( fgets(zLine, sizeof(zLine), in) ){
567 dehttpize(zLine);
568 if( (looks_like_attack(zLine)!=0) ^ bInvert ){
569 fossil_print("%s", zLine);
570 }
571 }
572 if( in!=stdin ) fclose(in);
573 }
574
575 /*
576 ** COMMAND: test-looks-like-attack
577 **
578 ** Read lines of input from files named as arguments (or from standard
579 ** input if no arguments are provided) and print those that look like they
580 ** might be part of an SQL injection attack.
581 **
582 ** Used to test the looks_lile_attack() utility subroutine, possibly
583 ** by piping in actual server log data.
584 */
585 void test_looks_like_attack(void){
586 int i;
587 int bInvert = find_option("invert","v",0)!=0;
588 int bDeHttpize = find_option("dehttpize","d",0)!=0;
589 verify_all_options();
590 if( g.argc==2 ){
591 show_attack_lines(0, bInvert, bDeHttpize);
592 }
593 for(i=2; i<g.argc; i++){
594 show_attack_lines(g.argv[i], bInvert, bDeHttpize);
595 }
596 }
597
+62 -16
--- src/main.c
+++ src/main.c
@@ -2053,11 +2053,12 @@
20532053
*/
20542054
set_base_url(0);
20552055
if( fossil_redirect_to_https_if_needed(2) ) return;
20562056
if( zPathInfo==0 || zPathInfo[0]==0
20572057
|| (zPathInfo[0]=='/' && zPathInfo[1]==0) ){
2058
- /* Second special case: If the PATH_INFO is blank, issue a redirect:
2058
+ /* Second special case: If the PATH_INFO is blank, issue a
2059
+ ** temporary 302 redirect:
20592060
** (1) to "/ckout" if g.useLocalauth and g.localOpen are both set.
20602061
** (2) to the home page identified by the "index-page" setting
20612062
** in the repository CONFIG table
20622063
** (3) to "/index" if there no "index-page" setting in CONFIG
20632064
*/
@@ -2267,10 +2268,21 @@
22672268
**
22682269
** #!/usr/bin/fossil
22692270
** redirect: * https://fossil-scm.org/home
22702271
**
22712272
** Thus requests to the .com website redirect to the .org website.
2273
+** This form uses a 301 Permanent redirect.
2274
+**
2275
+** On a "*" redirect, the PATH_INFO and QUERY_STRING of the query
2276
+** that provoked the redirect are appended to the target. So, for
2277
+** example, if the input URL for the redirect above were
2278
+** "http://www.fossil.com/index.html/timeline?c=20250404", then
2279
+** the redirect would be to:
2280
+**
2281
+** https://fossil-scm.org/home/timeline?c=20250404
2282
+** ^^^^^^^^^^^^^^^^^^^^
2283
+** Copied from input URL
22722284
*/
22732285
static void redirect_web_page(int nRedirect, char **azRedirect){
22742286
int i; /* Loop counter */
22752287
const char *zNotFound = 0; /* Not found URL */
22762288
const char *zName = P("name");
@@ -2297,21 +2309,22 @@
22972309
}
22982310
if( zNotFound ){
22992311
Blob to;
23002312
const char *z;
23012313
if( strstr(zNotFound, "%s") ){
2302
- cgi_redirectf(zNotFound /*works-like:"%s"*/, zName);
2314
+ char *zTarget = mprintf(zNotFound /*works-like:"%s"*/, zName);
2315
+ cgi_redirect_perm(zTarget);
23032316
}
23042317
if( strchr(zNotFound, '?') ){
2305
- cgi_redirect(zNotFound);
2318
+ cgi_redirect_perm(zNotFound);
23062319
}
23072320
blob_init(&to, zNotFound, -1);
23082321
z = P("PATH_INFO");
23092322
if( z && z[0]=='/' ) blob_append(&to, z, -1);
23102323
z = P("QUERY_STRING");
23112324
if( z && z[0]!=0 ) blob_appendf(&to, "?%s", z);
2312
- cgi_redirect(blob_str(&to));
2325
+ cgi_redirect_perm(blob_str(&to));
23132326
}else{
23142327
@ <html>
23152328
@ <head><title>No Such Object</title></head>
23162329
@ <body>
23172330
@ <p>No such object: <b>%h(zName)</b></p>
@@ -2352,11 +2365,18 @@
23522365
** notfound: URL When in "directory:" mode, redirect to
23532366
** URL if no suitable repository is found.
23542367
**
23552368
** repolist When in "directory:" mode, display a page
23562369
** showing a list of available repositories if
2357
-** the URL is "/".
2370
+** the URL is "/". Some control over the display
2371
+** is accomplished using environment variables.
2372
+** FOSSIL_REPOLIST_TITLE is the tital of the page.
2373
+** FOSSIL_REPOLIST_SHOW cause the "Description"
2374
+** column to display if it contains "description" as
2375
+** as a substring, and causes the Login-Group column
2376
+** to display if it contains the "login-group"
2377
+** substring.
23582378
**
23592379
** localauth Grant administrator privileges to connections
23602380
** from 127.0.0.1 or ::1.
23612381
**
23622382
** nossl Signal that no SSL connections are available.
@@ -2394,10 +2414,13 @@
23942414
** REPO for a check-in or ticket that matches the
23952415
** value of "name", then redirect to URL. There
23962416
** can be multiple "redirect:" lines that are
23972417
** processed in order. If the REPO is "*", then
23982418
** an unconditional redirect to URL is taken.
2419
+** When "*" is used a 301 permanent redirect is
2420
+** issued and the tail and query string from the
2421
+** original query are appeneded onto URL.
23992422
**
24002423
** jsmode: VALUE Specifies the delivery mode for JavaScript
24012424
** files. See the help text for the --jsmode
24022425
** flag of the http command.
24032426
**
@@ -3052,23 +3075,25 @@
30523075
** using this command interactively over SSH. A better solution would be
30533076
** to use a different command for "ssh" sync, but we cannot do that without
30543077
** breaking legacy.
30553078
**
30563079
** Options:
3080
+** --csrf-safe N Set cgi_csrf_safe() to to return N
30573081
** --nobody Pretend to be user "nobody"
30583082
** --test Do not do special "sync" processing when operating
30593083
** over an SSH link
30603084
** --th-trace Trace TH1 execution (for debugging purposes)
30613085
** --usercap CAP User capability string (Default: "sxy")
3062
-**
30633086
*/
30643087
void cmd_test_http(void){
30653088
const char *zIpAddr; /* IP address of remote client */
30663089
const char *zUserCap;
30673090
int bTest = 0;
3091
+ const char *zCsrfSafe = find_option("csrf-safe",0,1);
30683092
30693093
Th_InitTraceLog();
3094
+ if( zCsrfSafe ) g.okCsrf = atoi(zCsrfSafe);
30703095
zUserCap = find_option("usercap",0,1);
30713096
if( !find_option("nobody",0,0) ){
30723097
if( zUserCap==0 ){
30733098
g.useLocalauth = 1;
30743099
zUserCap = "sxy";
@@ -3505,11 +3530,12 @@
35053530
}
35063531
blob_append_escaped_arg(&ssh, "fossil", 1);
35073532
}else{
35083533
blob_appendf(&ssh, " %$", zFossilCmd);
35093534
}
3510
- blob_appendf(&ssh, " ui --nobrowser --localauth --port %d", iPort);
3535
+ blob_appendf(&ssh, " ui --nobrowser --localauth --port 127.0.0.1:%d",
3536
+ iPort);
35113537
if( zNotFound ) blob_appendf(&ssh, " --notfound %!$", zNotFound);
35123538
if( zFileGlob ) blob_appendf(&ssh, " --files-urlenc %T", zFileGlob);
35133539
if( g.zCkoutAlias ) blob_appendf(&ssh," --ckout-alias %!$",g.zCkoutAlias);
35143540
if( zExtPage ){
35153541
if( !file_is_absolute_path(zExtPage) ){
@@ -3692,10 +3718,13 @@
36923718
** case=3 Extra db_end_transaction()
36933719
** case=4 Error during SQL processing
36943720
** case=5 Call the segfault handler
36953721
** case=6 Call webpage_assert()
36963722
** case=7 Call webpage_error()
3723
+** case=8 Simulate a timeout
3724
+** case=9 Simulate a TH1 XSS vulnerability
3725
+** case=10 Simulate a TH1 SQL-injection vulnerability
36973726
*/
36983727
void test_warning_page(void){
36993728
int iCase = atoi(PD("case","0"));
37003729
int i;
37013730
login_check_credentials();
@@ -3704,17 +3733,15 @@
37043733
return;
37053734
}
37063735
style_set_current_feature("test");
37073736
style_header("Warning Test Page");
37083737
style_submenu_element("Error Log","%R/errorlog");
3709
- if( iCase<1 || iCase>4 ){
3710
- @ <p>Generate a message to the <a href="%R/errorlog">error log</a>
3711
- @ by clicking on one of the following cases:
3712
- }else{
3713
- @ <p>This is the test page for case=%d(iCase). All possible cases:
3714
- }
3715
- for(i=1; i<=8; i++){
3738
+ @ <p>This page will generate various kinds of errors to test Fossil's
3739
+ @ reaction. Depending on settings, a message might be written
3740
+ @ into the <a href="%R/errorlog">error log</a>. Click on
3741
+ @ one of the following hyperlinks to generate a simulated error:
3742
+ for(i=1; i<=10; i++){
37163743
@ <a href='./test-warning?case=%d(i)'>[%d(i)]</a>
37173744
}
37183745
@ </p>
37193746
@ <p><ol>
37203747
@ <li value='1'> Call fossil_warning()
@@ -3743,20 +3770,39 @@
37433770
}
37443771
@ <li value='6'> call webpage_assert(0)
37453772
if( iCase==6 ){
37463773
webpage_assert( 5==7 );
37473774
}
3748
- @ <li value='7'> call webpage_error()"
3775
+ @ <li value='7'> call webpage_error()
37493776
if( iCase==7 ){
37503777
cgi_reset_content();
37513778
webpage_error("Case 7 from /test-warning");
37523779
}
3753
- @ <li value='8'> simulated timeout"
3780
+ @ <li value='8'> simulated timeout
37543781
if( iCase==8 ){
37553782
fossil_set_timeout(1);
37563783
cgi_reset_content();
37573784
sqlite3_sleep(1100);
3785
+ }
3786
+ @ <li value='9'> simulated TH1 XSS vulnerability
3787
+ @ <li value='10'> simulated TH1 SQL-injection vulnerability
3788
+ if( iCase==9 || iCase==10 ){
3789
+ const char *zR;
3790
+ int n, rc;
3791
+ static const char *zTH1[] = {
3792
+ /* case 9 */ "html [taint {<b>XSS</b>}]",
3793
+ /* case 10 */ "query [taint {SELECT 'SQL-injection' AS msg}] {\n"
3794
+ " html \"<b>[htmlize $msg]</b>\"\n"
3795
+ "}"
3796
+ };
3797
+ rc = Th_Eval(g.interp, 0, zTH1[iCase==10], -1);
3798
+ zR = Th_GetResult(g.interp, &n);
3799
+ if( rc==TH_OK ){
3800
+ @ <pre class="th1result">%h(zR)</pre>
3801
+ }else{
3802
+ @ <pre class="th1error">%h(zR)</pre>
3803
+ }
37583804
}
37593805
@ </ol>
37603806
@ <p>End of test</p>
37613807
style_finish_page();
37623808
}
37633809
--- src/main.c
+++ src/main.c
@@ -2053,11 +2053,12 @@
2053 */
2054 set_base_url(0);
2055 if( fossil_redirect_to_https_if_needed(2) ) return;
2056 if( zPathInfo==0 || zPathInfo[0]==0
2057 || (zPathInfo[0]=='/' && zPathInfo[1]==0) ){
2058 /* Second special case: If the PATH_INFO is blank, issue a redirect:
 
2059 ** (1) to "/ckout" if g.useLocalauth and g.localOpen are both set.
2060 ** (2) to the home page identified by the "index-page" setting
2061 ** in the repository CONFIG table
2062 ** (3) to "/index" if there no "index-page" setting in CONFIG
2063 */
@@ -2267,10 +2268,21 @@
2267 **
2268 ** #!/usr/bin/fossil
2269 ** redirect: * https://fossil-scm.org/home
2270 **
2271 ** Thus requests to the .com website redirect to the .org website.
 
 
 
 
 
 
 
 
 
 
 
2272 */
2273 static void redirect_web_page(int nRedirect, char **azRedirect){
2274 int i; /* Loop counter */
2275 const char *zNotFound = 0; /* Not found URL */
2276 const char *zName = P("name");
@@ -2297,21 +2309,22 @@
2297 }
2298 if( zNotFound ){
2299 Blob to;
2300 const char *z;
2301 if( strstr(zNotFound, "%s") ){
2302 cgi_redirectf(zNotFound /*works-like:"%s"*/, zName);
 
2303 }
2304 if( strchr(zNotFound, '?') ){
2305 cgi_redirect(zNotFound);
2306 }
2307 blob_init(&to, zNotFound, -1);
2308 z = P("PATH_INFO");
2309 if( z && z[0]=='/' ) blob_append(&to, z, -1);
2310 z = P("QUERY_STRING");
2311 if( z && z[0]!=0 ) blob_appendf(&to, "?%s", z);
2312 cgi_redirect(blob_str(&to));
2313 }else{
2314 @ <html>
2315 @ <head><title>No Such Object</title></head>
2316 @ <body>
2317 @ <p>No such object: <b>%h(zName)</b></p>
@@ -2352,11 +2365,18 @@
2352 ** notfound: URL When in "directory:" mode, redirect to
2353 ** URL if no suitable repository is found.
2354 **
2355 ** repolist When in "directory:" mode, display a page
2356 ** showing a list of available repositories if
2357 ** the URL is "/".
 
 
 
 
 
 
 
2358 **
2359 ** localauth Grant administrator privileges to connections
2360 ** from 127.0.0.1 or ::1.
2361 **
2362 ** nossl Signal that no SSL connections are available.
@@ -2394,10 +2414,13 @@
2394 ** REPO for a check-in or ticket that matches the
2395 ** value of "name", then redirect to URL. There
2396 ** can be multiple "redirect:" lines that are
2397 ** processed in order. If the REPO is "*", then
2398 ** an unconditional redirect to URL is taken.
 
 
 
2399 **
2400 ** jsmode: VALUE Specifies the delivery mode for JavaScript
2401 ** files. See the help text for the --jsmode
2402 ** flag of the http command.
2403 **
@@ -3052,23 +3075,25 @@
3052 ** using this command interactively over SSH. A better solution would be
3053 ** to use a different command for "ssh" sync, but we cannot do that without
3054 ** breaking legacy.
3055 **
3056 ** Options:
 
3057 ** --nobody Pretend to be user "nobody"
3058 ** --test Do not do special "sync" processing when operating
3059 ** over an SSH link
3060 ** --th-trace Trace TH1 execution (for debugging purposes)
3061 ** --usercap CAP User capability string (Default: "sxy")
3062 **
3063 */
3064 void cmd_test_http(void){
3065 const char *zIpAddr; /* IP address of remote client */
3066 const char *zUserCap;
3067 int bTest = 0;
 
3068
3069 Th_InitTraceLog();
 
3070 zUserCap = find_option("usercap",0,1);
3071 if( !find_option("nobody",0,0) ){
3072 if( zUserCap==0 ){
3073 g.useLocalauth = 1;
3074 zUserCap = "sxy";
@@ -3505,11 +3530,12 @@
3505 }
3506 blob_append_escaped_arg(&ssh, "fossil", 1);
3507 }else{
3508 blob_appendf(&ssh, " %$", zFossilCmd);
3509 }
3510 blob_appendf(&ssh, " ui --nobrowser --localauth --port %d", iPort);
 
3511 if( zNotFound ) blob_appendf(&ssh, " --notfound %!$", zNotFound);
3512 if( zFileGlob ) blob_appendf(&ssh, " --files-urlenc %T", zFileGlob);
3513 if( g.zCkoutAlias ) blob_appendf(&ssh," --ckout-alias %!$",g.zCkoutAlias);
3514 if( zExtPage ){
3515 if( !file_is_absolute_path(zExtPage) ){
@@ -3692,10 +3718,13 @@
3692 ** case=3 Extra db_end_transaction()
3693 ** case=4 Error during SQL processing
3694 ** case=5 Call the segfault handler
3695 ** case=6 Call webpage_assert()
3696 ** case=7 Call webpage_error()
 
 
 
3697 */
3698 void test_warning_page(void){
3699 int iCase = atoi(PD("case","0"));
3700 int i;
3701 login_check_credentials();
@@ -3704,17 +3733,15 @@
3704 return;
3705 }
3706 style_set_current_feature("test");
3707 style_header("Warning Test Page");
3708 style_submenu_element("Error Log","%R/errorlog");
3709 if( iCase<1 || iCase>4 ){
3710 @ <p>Generate a message to the <a href="%R/errorlog">error log</a>
3711 @ by clicking on one of the following cases:
3712 }else{
3713 @ <p>This is the test page for case=%d(iCase). All possible cases:
3714 }
3715 for(i=1; i<=8; i++){
3716 @ <a href='./test-warning?case=%d(i)'>[%d(i)]</a>
3717 }
3718 @ </p>
3719 @ <p><ol>
3720 @ <li value='1'> Call fossil_warning()
@@ -3743,20 +3770,39 @@
3743 }
3744 @ <li value='6'> call webpage_assert(0)
3745 if( iCase==6 ){
3746 webpage_assert( 5==7 );
3747 }
3748 @ <li value='7'> call webpage_error()"
3749 if( iCase==7 ){
3750 cgi_reset_content();
3751 webpage_error("Case 7 from /test-warning");
3752 }
3753 @ <li value='8'> simulated timeout"
3754 if( iCase==8 ){
3755 fossil_set_timeout(1);
3756 cgi_reset_content();
3757 sqlite3_sleep(1100);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3758 }
3759 @ </ol>
3760 @ <p>End of test</p>
3761 style_finish_page();
3762 }
3763
--- src/main.c
+++ src/main.c
@@ -2053,11 +2053,12 @@
2053 */
2054 set_base_url(0);
2055 if( fossil_redirect_to_https_if_needed(2) ) return;
2056 if( zPathInfo==0 || zPathInfo[0]==0
2057 || (zPathInfo[0]=='/' && zPathInfo[1]==0) ){
2058 /* Second special case: If the PATH_INFO is blank, issue a
2059 ** temporary 302 redirect:
2060 ** (1) to "/ckout" if g.useLocalauth and g.localOpen are both set.
2061 ** (2) to the home page identified by the "index-page" setting
2062 ** in the repository CONFIG table
2063 ** (3) to "/index" if there no "index-page" setting in CONFIG
2064 */
@@ -2267,10 +2268,21 @@
2268 **
2269 ** #!/usr/bin/fossil
2270 ** redirect: * https://fossil-scm.org/home
2271 **
2272 ** Thus requests to the .com website redirect to the .org website.
2273 ** This form uses a 301 Permanent redirect.
2274 **
2275 ** On a "*" redirect, the PATH_INFO and QUERY_STRING of the query
2276 ** that provoked the redirect are appended to the target. So, for
2277 ** example, if the input URL for the redirect above were
2278 ** "http://www.fossil.com/index.html/timeline?c=20250404", then
2279 ** the redirect would be to:
2280 **
2281 ** https://fossil-scm.org/home/timeline?c=20250404
2282 ** ^^^^^^^^^^^^^^^^^^^^
2283 ** Copied from input URL
2284 */
2285 static void redirect_web_page(int nRedirect, char **azRedirect){
2286 int i; /* Loop counter */
2287 const char *zNotFound = 0; /* Not found URL */
2288 const char *zName = P("name");
@@ -2297,21 +2309,22 @@
2309 }
2310 if( zNotFound ){
2311 Blob to;
2312 const char *z;
2313 if( strstr(zNotFound, "%s") ){
2314 char *zTarget = mprintf(zNotFound /*works-like:"%s"*/, zName);
2315 cgi_redirect_perm(zTarget);
2316 }
2317 if( strchr(zNotFound, '?') ){
2318 cgi_redirect_perm(zNotFound);
2319 }
2320 blob_init(&to, zNotFound, -1);
2321 z = P("PATH_INFO");
2322 if( z && z[0]=='/' ) blob_append(&to, z, -1);
2323 z = P("QUERY_STRING");
2324 if( z && z[0]!=0 ) blob_appendf(&to, "?%s", z);
2325 cgi_redirect_perm(blob_str(&to));
2326 }else{
2327 @ <html>
2328 @ <head><title>No Such Object</title></head>
2329 @ <body>
2330 @ <p>No such object: <b>%h(zName)</b></p>
@@ -2352,11 +2365,18 @@
2365 ** notfound: URL When in "directory:" mode, redirect to
2366 ** URL if no suitable repository is found.
2367 **
2368 ** repolist When in "directory:" mode, display a page
2369 ** showing a list of available repositories if
2370 ** the URL is "/". Some control over the display
2371 ** is accomplished using environment variables.
2372 ** FOSSIL_REPOLIST_TITLE is the tital of the page.
2373 ** FOSSIL_REPOLIST_SHOW cause the "Description"
2374 ** column to display if it contains "description" as
2375 ** as a substring, and causes the Login-Group column
2376 ** to display if it contains the "login-group"
2377 ** substring.
2378 **
2379 ** localauth Grant administrator privileges to connections
2380 ** from 127.0.0.1 or ::1.
2381 **
2382 ** nossl Signal that no SSL connections are available.
@@ -2394,10 +2414,13 @@
2414 ** REPO for a check-in or ticket that matches the
2415 ** value of "name", then redirect to URL. There
2416 ** can be multiple "redirect:" lines that are
2417 ** processed in order. If the REPO is "*", then
2418 ** an unconditional redirect to URL is taken.
2419 ** When "*" is used a 301 permanent redirect is
2420 ** issued and the tail and query string from the
2421 ** original query are appeneded onto URL.
2422 **
2423 ** jsmode: VALUE Specifies the delivery mode for JavaScript
2424 ** files. See the help text for the --jsmode
2425 ** flag of the http command.
2426 **
@@ -3052,23 +3075,25 @@
3075 ** using this command interactively over SSH. A better solution would be
3076 ** to use a different command for "ssh" sync, but we cannot do that without
3077 ** breaking legacy.
3078 **
3079 ** Options:
3080 ** --csrf-safe N Set cgi_csrf_safe() to to return N
3081 ** --nobody Pretend to be user "nobody"
3082 ** --test Do not do special "sync" processing when operating
3083 ** over an SSH link
3084 ** --th-trace Trace TH1 execution (for debugging purposes)
3085 ** --usercap CAP User capability string (Default: "sxy")
 
3086 */
3087 void cmd_test_http(void){
3088 const char *zIpAddr; /* IP address of remote client */
3089 const char *zUserCap;
3090 int bTest = 0;
3091 const char *zCsrfSafe = find_option("csrf-safe",0,1);
3092
3093 Th_InitTraceLog();
3094 if( zCsrfSafe ) g.okCsrf = atoi(zCsrfSafe);
3095 zUserCap = find_option("usercap",0,1);
3096 if( !find_option("nobody",0,0) ){
3097 if( zUserCap==0 ){
3098 g.useLocalauth = 1;
3099 zUserCap = "sxy";
@@ -3505,11 +3530,12 @@
3530 }
3531 blob_append_escaped_arg(&ssh, "fossil", 1);
3532 }else{
3533 blob_appendf(&ssh, " %$", zFossilCmd);
3534 }
3535 blob_appendf(&ssh, " ui --nobrowser --localauth --port 127.0.0.1:%d",
3536 iPort);
3537 if( zNotFound ) blob_appendf(&ssh, " --notfound %!$", zNotFound);
3538 if( zFileGlob ) blob_appendf(&ssh, " --files-urlenc %T", zFileGlob);
3539 if( g.zCkoutAlias ) blob_appendf(&ssh," --ckout-alias %!$",g.zCkoutAlias);
3540 if( zExtPage ){
3541 if( !file_is_absolute_path(zExtPage) ){
@@ -3692,10 +3718,13 @@
3718 ** case=3 Extra db_end_transaction()
3719 ** case=4 Error during SQL processing
3720 ** case=5 Call the segfault handler
3721 ** case=6 Call webpage_assert()
3722 ** case=7 Call webpage_error()
3723 ** case=8 Simulate a timeout
3724 ** case=9 Simulate a TH1 XSS vulnerability
3725 ** case=10 Simulate a TH1 SQL-injection vulnerability
3726 */
3727 void test_warning_page(void){
3728 int iCase = atoi(PD("case","0"));
3729 int i;
3730 login_check_credentials();
@@ -3704,17 +3733,15 @@
3733 return;
3734 }
3735 style_set_current_feature("test");
3736 style_header("Warning Test Page");
3737 style_submenu_element("Error Log","%R/errorlog");
3738 @ <p>This page will generate various kinds of errors to test Fossil's
3739 @ reaction. Depending on settings, a message might be written
3740 @ into the <a href="%R/errorlog">error log</a>. Click on
3741 @ one of the following hyperlinks to generate a simulated error:
3742 for(i=1; i<=10; i++){
 
 
3743 @ <a href='./test-warning?case=%d(i)'>[%d(i)]</a>
3744 }
3745 @ </p>
3746 @ <p><ol>
3747 @ <li value='1'> Call fossil_warning()
@@ -3743,20 +3770,39 @@
3770 }
3771 @ <li value='6'> call webpage_assert(0)
3772 if( iCase==6 ){
3773 webpage_assert( 5==7 );
3774 }
3775 @ <li value='7'> call webpage_error()
3776 if( iCase==7 ){
3777 cgi_reset_content();
3778 webpage_error("Case 7 from /test-warning");
3779 }
3780 @ <li value='8'> simulated timeout
3781 if( iCase==8 ){
3782 fossil_set_timeout(1);
3783 cgi_reset_content();
3784 sqlite3_sleep(1100);
3785 }
3786 @ <li value='9'> simulated TH1 XSS vulnerability
3787 @ <li value='10'> simulated TH1 SQL-injection vulnerability
3788 if( iCase==9 || iCase==10 ){
3789 const char *zR;
3790 int n, rc;
3791 static const char *zTH1[] = {
3792 /* case 9 */ "html [taint {<b>XSS</b>}]",
3793 /* case 10 */ "query [taint {SELECT 'SQL-injection' AS msg}] {\n"
3794 " html \"<b>[htmlize $msg]</b>\"\n"
3795 "}"
3796 };
3797 rc = Th_Eval(g.interp, 0, zTH1[iCase==10], -1);
3798 zR = Th_GetResult(g.interp, &n);
3799 if( rc==TH_OK ){
3800 @ <pre class="th1result">%h(zR)</pre>
3801 }else{
3802 @ <pre class="th1error">%h(zR)</pre>
3803 }
3804 }
3805 @ </ol>
3806 @ <p>End of test</p>
3807 style_finish_page();
3808 }
3809
+28 -21
--- src/manifest.c
+++ src/manifest.c
@@ -2927,11 +2927,11 @@
29272927
case CFTYPE_CLUSTER: return "cluster";
29282928
case CFTYPE_CONTROL: return "tag";
29292929
case CFTYPE_WIKI: return "wiki";
29302930
case CFTYPE_TICKET: return "ticket";
29312931
case CFTYPE_ATTACHMENT: return "attachment";
2932
- case CFTYPE_EVENT: return "event";
2932
+ case CFTYPE_EVENT: return "technote";
29332933
case CFTYPE_FORUM: return "forumpost";
29342934
}
29352935
return NULL;
29362936
}
29372937
@@ -2948,26 +2948,26 @@
29482948
*/
29492949
void artifact_to_json(Manifest const *p, Blob *b){
29502950
int i;
29512951
29522952
blob_append_literal(b, "{");
2953
- blob_appendf(b, "\"uuid\": \"%z\"", rid_to_uuid(p->rid));
2953
+ blob_appendf(b, "\"uuid\":\"%z\"", rid_to_uuid(p->rid));
29542954
/*blob_appendf(b, ", \"rid\": %d", p->rid); not portable across repos*/
2955
- blob_appendf(b, ", \"type\": %!j", artifact_type_to_name(p->type));
2955
+ blob_appendf(b, ",\"type\":%!j", artifact_type_to_name(p->type));
29562956
#define ISA(TYPE) if( p->type==TYPE )
29572957
#define CARD_LETTER(LETTER) \
2958
- blob_append_literal(b, ",\"" #LETTER "\": ")
2958
+ blob_append_literal(b, ",\"" #LETTER "\":")
29592959
#define CARD_STR(LETTER, VAL) \
29602960
assert( VAL ); CARD_LETTER(LETTER); blob_appendf(b, "%!j", VAL)
29612961
#define CARD_STR2(LETTER, VAL) \
29622962
if( VAL ) { CARD_STR(LETTER, VAL); } (void)0
29632963
#define STR_OR_NULL(VAL) \
29642964
if( VAL ) blob_appendf(b, "%!j", VAL); \
29652965
else blob_append(b, "null", 4)
29662966
#define KVP_STR(ADDCOMMA, KEY,VAL) \
29672967
if(ADDCOMMA) blob_append_char(b, ','); \
2968
- blob_appendf(b, "%!j: ", #KEY); \
2968
+ blob_appendf(b, "%!j:", #KEY); \
29692969
STR_OR_NULL(VAL)
29702970
29712971
ISA( CFTYPE_ATTACHMENT ){
29722972
CARD_LETTER(A);
29732973
blob_append_char(b, '{');
@@ -2978,11 +2978,11 @@
29782978
}
29792979
CARD_STR2(B, p->zBaseline);
29802980
CARD_STR2(C, p->zComment);
29812981
CARD_LETTER(D); blob_appendf(b, "%f", p->rDate);
29822982
ISA( CFTYPE_EVENT ){
2983
- blob_appendf(b, ", \"E\": {\"time\": %f, \"id\": %!j}",
2983
+ blob_appendf(b, ", \"E\":{\"time\":%f,\"id\":%!j}",
29842984
p->rEventDate, p->zEventId);
29852985
}
29862986
ISA( CFTYPE_MANIFEST ){
29872987
CARD_LETTER(F);
29882988
blob_append_char(b, '[');
@@ -2991,29 +2991,34 @@
29912991
if( i>0 ) blob_append_char(b, ',');
29922992
blob_append_char(b, '{');
29932993
KVP_STR(0, name, pF->zName);
29942994
KVP_STR(1, uuid, pF->zUuid);
29952995
KVP_STR(1, perm, pF->zPerm);
2996
- KVP_STR(1, oldName, pF->zPrior);
2996
+ KVP_STR(1, rename, pF->zPrior);
29972997
blob_append_char(b, '}');
29982998
}
29992999
/* Special case: model checkins with no F-card as having an empty
30003000
** array, rather than no F-cards, to hypothetically simplify
30013001
** handling in JSON queries. */
30023002
blob_append_char(b, ']');
30033003
}
30043004
CARD_STR2(G, p->zThreadRoot);
3005
- CARD_STR2(H, p->zThreadTitle);
3006
- CARD_STR2(I, p->zInReplyTo);
3005
+ ISA( CFTYPE_FORUM ){
3006
+ CARD_LETTER(H);
3007
+ STR_OR_NULL( (p->zThreadTitle && *p->zThreadTitle) ? p->zThreadTitle : NULL);
3008
+ CARD_STR2(I, p->zInReplyTo);
3009
+ }
30073010
if( p->nField ){
30083011
CARD_LETTER(J);
30093012
blob_append_char(b, '[');
30103013
for( i = 0; i < p->nField; ++i ){
3014
+ const char * zName = p->aField[i].zName;
30113015
if( i>0 ) blob_append_char(b, ',');
30123016
blob_append_char(b, '{');
3013
- KVP_STR(0, name, p->aField[i].zName);
3017
+ KVP_STR(0, name, '+'==*zName ? &zName[1] : zName);
30143018
KVP_STR(1, value, p->aField[i].zValue);
3019
+ blob_appendf(b, ",\"append\":%s", '+'==*zName ? "true" : "false");
30153020
blob_append_char(b, '}');
30163021
}
30173022
blob_append_char(b, ']');
30183023
}
30193024
CARD_STR2(K, p->zTicketUuid);
@@ -3026,18 +3031,16 @@
30263031
blob_appendf(b, "%!j", p->azCChild[i]);
30273032
}
30283033
blob_append_char(b, ']');
30293034
}
30303035
CARD_STR2(N, p->zMimetype);
3031
- ISA( CFTYPE_MANIFEST ){
3036
+ ISA( CFTYPE_MANIFEST || p->nParent>0 ){
30323037
CARD_LETTER(P);
30333038
blob_append_char(b, '[');
3034
- if( p->nParent ){
3035
- for( i = 0; i < p->nParent; ++i ){
3036
- if( i>0 ) blob_append_char(b, ',');
3037
- blob_appendf(b, "%!j", p->azParent[i]);
3038
- }
3039
+ for( i = 0; i < p->nParent; ++i ){
3040
+ if( i>0 ) blob_append_char(b, ',');
3041
+ blob_appendf(b, "%!j", p->azParent[i]);
30393042
}
30403043
/* Special case: model checkins with no P-card as having an empty
30413044
** array, as per F-cards. */
30423045
blob_append_char(b, ']');
30433046
}
@@ -3045,11 +3048,11 @@
30453048
CARD_LETTER(Q);
30463049
blob_append_char(b, '[');
30473050
for( i = 0; i < p->nCherrypick; ++i ){
30483051
if( i>0 ) blob_append_char(b, ',');
30493052
blob_append_char(b, '{');
3050
- blob_appendf(b, "\"type\": \"%c\"", p->aCherrypick[i].zCPTarget[0]);
3053
+ blob_appendf(b, "\"type\":\"%c\"", p->aCherrypick[i].zCPTarget[0]);
30513054
KVP_STR(1, target, &p->aCherrypick[i].zCPTarget[1]);
30523055
KVP_STR(1, base, p->aCherrypick[i].zCPBase);
30533056
blob_append_char(b, '}');
30543057
}
30553058
blob_append_char(b, ']');
@@ -3060,21 +3063,25 @@
30603063
blob_append_char(b, '[');
30613064
for( int i = 0; i < p->nTag; ++i ){
30623065
const char *zName = p->aTag[i].zName;
30633066
if( i>0 ) blob_append_char(b, ',');
30643067
blob_append_char(b, '{');
3065
- blob_appendf(b, "\"type\": \"%c\"", *zName);
3068
+ blob_appendf(b, "\"type\":\"%c\"", *zName);
30663069
KVP_STR(1, name, &zName[1]);
30673070
KVP_STR(1, target, p->aTag[i].zUuid ? p->aTag[i].zUuid : "*")
30683071
/* We could arguably resolve the "*" as null or p's uuid. */;
30693072
KVP_STR(1, value, p->aTag[i].zValue);
30703073
blob_append_char(b, '}');
30713074
}
30723075
blob_append_char(b, ']');
30733076
}
30743077
CARD_STR2(U, p->zUser);
3075
- CARD_STR2(W, p->zWiki);
3078
+ if( p->zWiki || CFTYPE_WIKI==p->type || CFTYPE_FORUM==p->type
3079
+ || CFTYPE_EVENT==p->type ){
3080
+ CARD_LETTER(W);
3081
+ STR_OR_NULL((p->zWiki && *p->zWiki) ? p->zWiki : NULL);
3082
+ }
30763083
blob_append_literal(b, "}");
30773084
#undef CARD_FMT
30783085
#undef CARD_LETTER
30793086
#undef CARD_STR
30803087
#undef CARD_STR2
@@ -3164,19 +3171,19 @@
31643171
31653172
31663173
/*
31673174
** COMMAND: test-artifact-to-json
31683175
**
3169
-** Usage: %fossil test-artifact-to-json ?-pretty? symbolic-name [...names]
3176
+** Usage: %fossil test-artifact-to-json ?-pretty|-p? symbolic-name [...names]
31703177
**
31713178
** Tests the artifact_to_json() and artifact_to_json_by_name() APIs.
31723179
*/
31733180
void test_manifest_to_json(void){
31743181
int i;
31753182
Blob b = empty_blob;
31763183
Stmt q;
3177
- const int bPretty = find_option("pretty",0,0)!=0;
3184
+ const int bPretty = find_option("pretty","p",0)!=0;
31783185
int nErr = 0;
31793186
31803187
db_find_and_open_repository(0,0);
31813188
db_prepare(&q, "select json_pretty(:json)");
31823189
for( i=2; i<g.argc; ++i ){
31833190
--- src/manifest.c
+++ src/manifest.c
@@ -2927,11 +2927,11 @@
2927 case CFTYPE_CLUSTER: return "cluster";
2928 case CFTYPE_CONTROL: return "tag";
2929 case CFTYPE_WIKI: return "wiki";
2930 case CFTYPE_TICKET: return "ticket";
2931 case CFTYPE_ATTACHMENT: return "attachment";
2932 case CFTYPE_EVENT: return "event";
2933 case CFTYPE_FORUM: return "forumpost";
2934 }
2935 return NULL;
2936 }
2937
@@ -2948,26 +2948,26 @@
2948 */
2949 void artifact_to_json(Manifest const *p, Blob *b){
2950 int i;
2951
2952 blob_append_literal(b, "{");
2953 blob_appendf(b, "\"uuid\": \"%z\"", rid_to_uuid(p->rid));
2954 /*blob_appendf(b, ", \"rid\": %d", p->rid); not portable across repos*/
2955 blob_appendf(b, ", \"type\": %!j", artifact_type_to_name(p->type));
2956 #define ISA(TYPE) if( p->type==TYPE )
2957 #define CARD_LETTER(LETTER) \
2958 blob_append_literal(b, ",\"" #LETTER "\": ")
2959 #define CARD_STR(LETTER, VAL) \
2960 assert( VAL ); CARD_LETTER(LETTER); blob_appendf(b, "%!j", VAL)
2961 #define CARD_STR2(LETTER, VAL) \
2962 if( VAL ) { CARD_STR(LETTER, VAL); } (void)0
2963 #define STR_OR_NULL(VAL) \
2964 if( VAL ) blob_appendf(b, "%!j", VAL); \
2965 else blob_append(b, "null", 4)
2966 #define KVP_STR(ADDCOMMA, KEY,VAL) \
2967 if(ADDCOMMA) blob_append_char(b, ','); \
2968 blob_appendf(b, "%!j: ", #KEY); \
2969 STR_OR_NULL(VAL)
2970
2971 ISA( CFTYPE_ATTACHMENT ){
2972 CARD_LETTER(A);
2973 blob_append_char(b, '{');
@@ -2978,11 +2978,11 @@
2978 }
2979 CARD_STR2(B, p->zBaseline);
2980 CARD_STR2(C, p->zComment);
2981 CARD_LETTER(D); blob_appendf(b, "%f", p->rDate);
2982 ISA( CFTYPE_EVENT ){
2983 blob_appendf(b, ", \"E\": {\"time\": %f, \"id\": %!j}",
2984 p->rEventDate, p->zEventId);
2985 }
2986 ISA( CFTYPE_MANIFEST ){
2987 CARD_LETTER(F);
2988 blob_append_char(b, '[');
@@ -2991,29 +2991,34 @@
2991 if( i>0 ) blob_append_char(b, ',');
2992 blob_append_char(b, '{');
2993 KVP_STR(0, name, pF->zName);
2994 KVP_STR(1, uuid, pF->zUuid);
2995 KVP_STR(1, perm, pF->zPerm);
2996 KVP_STR(1, oldName, pF->zPrior);
2997 blob_append_char(b, '}');
2998 }
2999 /* Special case: model checkins with no F-card as having an empty
3000 ** array, rather than no F-cards, to hypothetically simplify
3001 ** handling in JSON queries. */
3002 blob_append_char(b, ']');
3003 }
3004 CARD_STR2(G, p->zThreadRoot);
3005 CARD_STR2(H, p->zThreadTitle);
3006 CARD_STR2(I, p->zInReplyTo);
 
 
 
3007 if( p->nField ){
3008 CARD_LETTER(J);
3009 blob_append_char(b, '[');
3010 for( i = 0; i < p->nField; ++i ){
 
3011 if( i>0 ) blob_append_char(b, ',');
3012 blob_append_char(b, '{');
3013 KVP_STR(0, name, p->aField[i].zName);
3014 KVP_STR(1, value, p->aField[i].zValue);
 
3015 blob_append_char(b, '}');
3016 }
3017 blob_append_char(b, ']');
3018 }
3019 CARD_STR2(K, p->zTicketUuid);
@@ -3026,18 +3031,16 @@
3026 blob_appendf(b, "%!j", p->azCChild[i]);
3027 }
3028 blob_append_char(b, ']');
3029 }
3030 CARD_STR2(N, p->zMimetype);
3031 ISA( CFTYPE_MANIFEST ){
3032 CARD_LETTER(P);
3033 blob_append_char(b, '[');
3034 if( p->nParent ){
3035 for( i = 0; i < p->nParent; ++i ){
3036 if( i>0 ) blob_append_char(b, ',');
3037 blob_appendf(b, "%!j", p->azParent[i]);
3038 }
3039 }
3040 /* Special case: model checkins with no P-card as having an empty
3041 ** array, as per F-cards. */
3042 blob_append_char(b, ']');
3043 }
@@ -3045,11 +3048,11 @@
3045 CARD_LETTER(Q);
3046 blob_append_char(b, '[');
3047 for( i = 0; i < p->nCherrypick; ++i ){
3048 if( i>0 ) blob_append_char(b, ',');
3049 blob_append_char(b, '{');
3050 blob_appendf(b, "\"type\": \"%c\"", p->aCherrypick[i].zCPTarget[0]);
3051 KVP_STR(1, target, &p->aCherrypick[i].zCPTarget[1]);
3052 KVP_STR(1, base, p->aCherrypick[i].zCPBase);
3053 blob_append_char(b, '}');
3054 }
3055 blob_append_char(b, ']');
@@ -3060,21 +3063,25 @@
3060 blob_append_char(b, '[');
3061 for( int i = 0; i < p->nTag; ++i ){
3062 const char *zName = p->aTag[i].zName;
3063 if( i>0 ) blob_append_char(b, ',');
3064 blob_append_char(b, '{');
3065 blob_appendf(b, "\"type\": \"%c\"", *zName);
3066 KVP_STR(1, name, &zName[1]);
3067 KVP_STR(1, target, p->aTag[i].zUuid ? p->aTag[i].zUuid : "*")
3068 /* We could arguably resolve the "*" as null or p's uuid. */;
3069 KVP_STR(1, value, p->aTag[i].zValue);
3070 blob_append_char(b, '}');
3071 }
3072 blob_append_char(b, ']');
3073 }
3074 CARD_STR2(U, p->zUser);
3075 CARD_STR2(W, p->zWiki);
 
 
 
 
3076 blob_append_literal(b, "}");
3077 #undef CARD_FMT
3078 #undef CARD_LETTER
3079 #undef CARD_STR
3080 #undef CARD_STR2
@@ -3164,19 +3171,19 @@
3164
3165
3166 /*
3167 ** COMMAND: test-artifact-to-json
3168 **
3169 ** Usage: %fossil test-artifact-to-json ?-pretty? symbolic-name [...names]
3170 **
3171 ** Tests the artifact_to_json() and artifact_to_json_by_name() APIs.
3172 */
3173 void test_manifest_to_json(void){
3174 int i;
3175 Blob b = empty_blob;
3176 Stmt q;
3177 const int bPretty = find_option("pretty",0,0)!=0;
3178 int nErr = 0;
3179
3180 db_find_and_open_repository(0,0);
3181 db_prepare(&q, "select json_pretty(:json)");
3182 for( i=2; i<g.argc; ++i ){
3183
--- src/manifest.c
+++ src/manifest.c
@@ -2927,11 +2927,11 @@
2927 case CFTYPE_CLUSTER: return "cluster";
2928 case CFTYPE_CONTROL: return "tag";
2929 case CFTYPE_WIKI: return "wiki";
2930 case CFTYPE_TICKET: return "ticket";
2931 case CFTYPE_ATTACHMENT: return "attachment";
2932 case CFTYPE_EVENT: return "technote";
2933 case CFTYPE_FORUM: return "forumpost";
2934 }
2935 return NULL;
2936 }
2937
@@ -2948,26 +2948,26 @@
2948 */
2949 void artifact_to_json(Manifest const *p, Blob *b){
2950 int i;
2951
2952 blob_append_literal(b, "{");
2953 blob_appendf(b, "\"uuid\":\"%z\"", rid_to_uuid(p->rid));
2954 /*blob_appendf(b, ", \"rid\": %d", p->rid); not portable across repos*/
2955 blob_appendf(b, ",\"type\":%!j", artifact_type_to_name(p->type));
2956 #define ISA(TYPE) if( p->type==TYPE )
2957 #define CARD_LETTER(LETTER) \
2958 blob_append_literal(b, ",\"" #LETTER "\":")
2959 #define CARD_STR(LETTER, VAL) \
2960 assert( VAL ); CARD_LETTER(LETTER); blob_appendf(b, "%!j", VAL)
2961 #define CARD_STR2(LETTER, VAL) \
2962 if( VAL ) { CARD_STR(LETTER, VAL); } (void)0
2963 #define STR_OR_NULL(VAL) \
2964 if( VAL ) blob_appendf(b, "%!j", VAL); \
2965 else blob_append(b, "null", 4)
2966 #define KVP_STR(ADDCOMMA, KEY,VAL) \
2967 if(ADDCOMMA) blob_append_char(b, ','); \
2968 blob_appendf(b, "%!j:", #KEY); \
2969 STR_OR_NULL(VAL)
2970
2971 ISA( CFTYPE_ATTACHMENT ){
2972 CARD_LETTER(A);
2973 blob_append_char(b, '{');
@@ -2978,11 +2978,11 @@
2978 }
2979 CARD_STR2(B, p->zBaseline);
2980 CARD_STR2(C, p->zComment);
2981 CARD_LETTER(D); blob_appendf(b, "%f", p->rDate);
2982 ISA( CFTYPE_EVENT ){
2983 blob_appendf(b, ", \"E\":{\"time\":%f,\"id\":%!j}",
2984 p->rEventDate, p->zEventId);
2985 }
2986 ISA( CFTYPE_MANIFEST ){
2987 CARD_LETTER(F);
2988 blob_append_char(b, '[');
@@ -2991,29 +2991,34 @@
2991 if( i>0 ) blob_append_char(b, ',');
2992 blob_append_char(b, '{');
2993 KVP_STR(0, name, pF->zName);
2994 KVP_STR(1, uuid, pF->zUuid);
2995 KVP_STR(1, perm, pF->zPerm);
2996 KVP_STR(1, rename, pF->zPrior);
2997 blob_append_char(b, '}');
2998 }
2999 /* Special case: model checkins with no F-card as having an empty
3000 ** array, rather than no F-cards, to hypothetically simplify
3001 ** handling in JSON queries. */
3002 blob_append_char(b, ']');
3003 }
3004 CARD_STR2(G, p->zThreadRoot);
3005 ISA( CFTYPE_FORUM ){
3006 CARD_LETTER(H);
3007 STR_OR_NULL( (p->zThreadTitle && *p->zThreadTitle) ? p->zThreadTitle : NULL);
3008 CARD_STR2(I, p->zInReplyTo);
3009 }
3010 if( p->nField ){
3011 CARD_LETTER(J);
3012 blob_append_char(b, '[');
3013 for( i = 0; i < p->nField; ++i ){
3014 const char * zName = p->aField[i].zName;
3015 if( i>0 ) blob_append_char(b, ',');
3016 blob_append_char(b, '{');
3017 KVP_STR(0, name, '+'==*zName ? &zName[1] : zName);
3018 KVP_STR(1, value, p->aField[i].zValue);
3019 blob_appendf(b, ",\"append\":%s", '+'==*zName ? "true" : "false");
3020 blob_append_char(b, '}');
3021 }
3022 blob_append_char(b, ']');
3023 }
3024 CARD_STR2(K, p->zTicketUuid);
@@ -3026,18 +3031,16 @@
3031 blob_appendf(b, "%!j", p->azCChild[i]);
3032 }
3033 blob_append_char(b, ']');
3034 }
3035 CARD_STR2(N, p->zMimetype);
3036 ISA( CFTYPE_MANIFEST || p->nParent>0 ){
3037 CARD_LETTER(P);
3038 blob_append_char(b, '[');
3039 for( i = 0; i < p->nParent; ++i ){
3040 if( i>0 ) blob_append_char(b, ',');
3041 blob_appendf(b, "%!j", p->azParent[i]);
 
 
3042 }
3043 /* Special case: model checkins with no P-card as having an empty
3044 ** array, as per F-cards. */
3045 blob_append_char(b, ']');
3046 }
@@ -3045,11 +3048,11 @@
3048 CARD_LETTER(Q);
3049 blob_append_char(b, '[');
3050 for( i = 0; i < p->nCherrypick; ++i ){
3051 if( i>0 ) blob_append_char(b, ',');
3052 blob_append_char(b, '{');
3053 blob_appendf(b, "\"type\":\"%c\"", p->aCherrypick[i].zCPTarget[0]);
3054 KVP_STR(1, target, &p->aCherrypick[i].zCPTarget[1]);
3055 KVP_STR(1, base, p->aCherrypick[i].zCPBase);
3056 blob_append_char(b, '}');
3057 }
3058 blob_append_char(b, ']');
@@ -3060,21 +3063,25 @@
3063 blob_append_char(b, '[');
3064 for( int i = 0; i < p->nTag; ++i ){
3065 const char *zName = p->aTag[i].zName;
3066 if( i>0 ) blob_append_char(b, ',');
3067 blob_append_char(b, '{');
3068 blob_appendf(b, "\"type\":\"%c\"", *zName);
3069 KVP_STR(1, name, &zName[1]);
3070 KVP_STR(1, target, p->aTag[i].zUuid ? p->aTag[i].zUuid : "*")
3071 /* We could arguably resolve the "*" as null or p's uuid. */;
3072 KVP_STR(1, value, p->aTag[i].zValue);
3073 blob_append_char(b, '}');
3074 }
3075 blob_append_char(b, ']');
3076 }
3077 CARD_STR2(U, p->zUser);
3078 if( p->zWiki || CFTYPE_WIKI==p->type || CFTYPE_FORUM==p->type
3079 || CFTYPE_EVENT==p->type ){
3080 CARD_LETTER(W);
3081 STR_OR_NULL((p->zWiki && *p->zWiki) ? p->zWiki : NULL);
3082 }
3083 blob_append_literal(b, "}");
3084 #undef CARD_FMT
3085 #undef CARD_LETTER
3086 #undef CARD_STR
3087 #undef CARD_STR2
@@ -3164,19 +3171,19 @@
3171
3172
3173 /*
3174 ** COMMAND: test-artifact-to-json
3175 **
3176 ** Usage: %fossil test-artifact-to-json ?-pretty|-p? symbolic-name [...names]
3177 **
3178 ** Tests the artifact_to_json() and artifact_to_json_by_name() APIs.
3179 */
3180 void test_manifest_to_json(void){
3181 int i;
3182 Blob b = empty_blob;
3183 Stmt q;
3184 const int bPretty = find_option("pretty","p",0)!=0;
3185 int nErr = 0;
3186
3187 db_find_and_open_repository(0,0);
3188 db_prepare(&q, "select json_pretty(:json)");
3189 for( i=2; i<g.argc; ++i ){
3190
+65 -6
--- src/name.c
+++ src/name.c
@@ -1816,11 +1816,11 @@
18161816
}
18171817
if( bUnclst==0 ){
18181818
style_submenu_element("Unclustered","bloblist?unclustered");
18191819
}
18201820
if( g.perm.Admin ){
1821
- style_submenu_element("Artifact Log", "rcvfromlist");
1821
+ style_submenu_element("Xfer Log", "rcvfromlist");
18221822
}
18231823
if( !phantomOnly ){
18241824
style_submenu_element("Phantoms", "bloblist?phan");
18251825
}
18261826
style_submenu_element("Clusters","clusterlist");
@@ -2005,11 +2005,11 @@
20052005
void phantom_list_page(void){
20062006
login_check_credentials();
20072007
if( !g.perm.Read ){ login_needed(g.anon.Read); return; }
20082008
style_header("Public Phantom Artifacts");
20092009
if( g.perm.Admin ){
2010
- style_submenu_element("Artifact Log", "rcvfromlist");
2010
+ style_submenu_element("Xfer Log", "rcvfromlist");
20112011
style_submenu_element("Artifact List", "bloblist");
20122012
}
20132013
if( g.perm.Write ){
20142014
style_submenu_element("Artifact Stats", "artifact_stats");
20152015
}
@@ -2030,11 +2030,11 @@
20302030
int n = atoi(PD("n","250"));
20312031
20322032
login_check_credentials();
20332033
if( !g.perm.Read ){ login_needed(g.anon.Read); return; }
20342034
if( g.perm.Admin ){
2035
- style_submenu_element("Artifact Log", "rcvfromlist");
2035
+ style_submenu_element("Xfer Log", "rcvfromlist");
20362036
}
20372037
if( g.perm.Write ){
20382038
style_submenu_element("Artifact Stats", "artifact_stats");
20392039
}
20402040
style_submenu_element("All Artifacts", "bloblist");
@@ -2203,15 +2203,74 @@
22032203
/*
22042204
** COMMAND: test-phantoms
22052205
**
22062206
** Usage: %fossil test-phantoms
22072207
**
2208
-** Show all phantom artifacts
2208
+** Show all phantom artifacts. A phantom artifact is one for which there
2209
+** is no content. Options:
2210
+**
2211
+** --count Show only a count of the number of phantoms.
2212
+** --delta Show all delta-phantoms. A delta-phantom is a
2213
+** artifact for which there is a delta but the delta
2214
+** source is a phantom.
2215
+** --list Just list the phantoms. Do not try to describe them.
22092216
*/
22102217
void test_phatoms_cmd(void){
2218
+ int bDelta;
2219
+ int bList;
2220
+ int bCount;
2221
+ unsigned nPhantom = 0;
2222
+ unsigned nDeltaPhantom = 0;
22112223
db_find_and_open_repository(0,0);
2212
- describe_artifacts_to_stdout("IN (SELECT rid FROM blob WHERE size<0)", 0);
2224
+ bDelta = find_option("delta", 0, 0)!=0;
2225
+ bList = find_option("list", 0, 0)!=0;
2226
+ bCount = find_option("count", 0, 0)!=0;
2227
+ verify_all_options();
2228
+ if( bList || bCount ){
2229
+ Stmt q1, q2;
2230
+ db_prepare(&q1, "SELECT rid, uuid FROM blob WHERE size<0");
2231
+ while( db_step(&q1)==SQLITE_ROW ){
2232
+ int rid = db_column_int(&q1, 0);
2233
+ nPhantom++;
2234
+ if( !bCount ){
2235
+ fossil_print("%S (%d)\n", db_column_text(&q1,1), rid);
2236
+ }
2237
+ db_prepare(&q2,
2238
+ "WITH RECURSIVE deltasof(rid) AS ("
2239
+ " SELECT rid FROM delta WHERE srcid=%d"
2240
+ " UNION"
2241
+ " SELECT delta.rid FROM deltasof, delta"
2242
+ " WHERE delta.srcid=deltasof.rid)"
2243
+ "SELECT deltasof.rid, blob.uuid FROM deltasof LEFT JOIN blob"
2244
+ " ON blob.rid=deltasof.rid", rid
2245
+ );
2246
+ while( db_step(&q2)==SQLITE_ROW ){
2247
+ nDeltaPhantom++;
2248
+ if( !bCount ){
2249
+ fossil_print(" %S (%d)\n", db_column_text(&q2,1),
2250
+ db_column_int(&q2,0));
2251
+ }
2252
+ }
2253
+ db_finalize(&q2);
2254
+ }
2255
+ db_finalize(&q1);
2256
+ if( nPhantom ){
2257
+ fossil_print("Phantoms: %u Delta-phantoms: %u\n",
2258
+ nPhantom, nDeltaPhantom);
2259
+ }
2260
+ }else if( bDelta ){
2261
+ describe_artifacts_to_stdout(
2262
+ "IN (WITH RECURSIVE delta_phantom(rid) AS (\n"
2263
+ " SELECT delta.rid FROM blob, delta\n"
2264
+ " WHERE blob.size<0 AND delta.srcid=blob.rid\n"
2265
+ " UNION\n"
2266
+ " SELECT delta.rid FROM delta_phantom, delta\n"
2267
+ " WHERE delta.srcid=delta_phantom.rid)\n"
2268
+ " SELECT rid FROM delta_phantom)", 0);
2269
+ }else{
2270
+ describe_artifacts_to_stdout("IN (SELECT rid FROM blob WHERE size<0)", 0);
2271
+ }
22132272
}
22142273
22152274
/* Maximum number of collision examples to remember */
22162275
#define MAX_COLLIDE 25
22172276
@@ -2311,11 +2370,11 @@
23112370
login_check_credentials();
23122371
if( !g.perm.Read ){ login_needed(g.anon.Read); return; }
23132372
style_header("All Cluster Artifacts");
23142373
style_submenu_element("All Artifactst", "bloblist");
23152374
if( g.perm.Admin ){
2316
- style_submenu_element("Artifact Log", "rcvfromlist");
2375
+ style_submenu_element("Xfer Log", "rcvfromlist");
23172376
}
23182377
style_submenu_element("Phantoms", "bloblist?phan");
23192378
if( g.perm.Write ){
23202379
style_submenu_element("Artifact Stats", "artifact_stats");
23212380
}
23222381
--- src/name.c
+++ src/name.c
@@ -1816,11 +1816,11 @@
1816 }
1817 if( bUnclst==0 ){
1818 style_submenu_element("Unclustered","bloblist?unclustered");
1819 }
1820 if( g.perm.Admin ){
1821 style_submenu_element("Artifact Log", "rcvfromlist");
1822 }
1823 if( !phantomOnly ){
1824 style_submenu_element("Phantoms", "bloblist?phan");
1825 }
1826 style_submenu_element("Clusters","clusterlist");
@@ -2005,11 +2005,11 @@
2005 void phantom_list_page(void){
2006 login_check_credentials();
2007 if( !g.perm.Read ){ login_needed(g.anon.Read); return; }
2008 style_header("Public Phantom Artifacts");
2009 if( g.perm.Admin ){
2010 style_submenu_element("Artifact Log", "rcvfromlist");
2011 style_submenu_element("Artifact List", "bloblist");
2012 }
2013 if( g.perm.Write ){
2014 style_submenu_element("Artifact Stats", "artifact_stats");
2015 }
@@ -2030,11 +2030,11 @@
2030 int n = atoi(PD("n","250"));
2031
2032 login_check_credentials();
2033 if( !g.perm.Read ){ login_needed(g.anon.Read); return; }
2034 if( g.perm.Admin ){
2035 style_submenu_element("Artifact Log", "rcvfromlist");
2036 }
2037 if( g.perm.Write ){
2038 style_submenu_element("Artifact Stats", "artifact_stats");
2039 }
2040 style_submenu_element("All Artifacts", "bloblist");
@@ -2203,15 +2203,74 @@
2203 /*
2204 ** COMMAND: test-phantoms
2205 **
2206 ** Usage: %fossil test-phantoms
2207 **
2208 ** Show all phantom artifacts
 
 
 
 
 
 
 
2209 */
2210 void test_phatoms_cmd(void){
 
 
 
 
 
2211 db_find_and_open_repository(0,0);
2212 describe_artifacts_to_stdout("IN (SELECT rid FROM blob WHERE size<0)", 0);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2213 }
2214
2215 /* Maximum number of collision examples to remember */
2216 #define MAX_COLLIDE 25
2217
@@ -2311,11 +2370,11 @@
2311 login_check_credentials();
2312 if( !g.perm.Read ){ login_needed(g.anon.Read); return; }
2313 style_header("All Cluster Artifacts");
2314 style_submenu_element("All Artifactst", "bloblist");
2315 if( g.perm.Admin ){
2316 style_submenu_element("Artifact Log", "rcvfromlist");
2317 }
2318 style_submenu_element("Phantoms", "bloblist?phan");
2319 if( g.perm.Write ){
2320 style_submenu_element("Artifact Stats", "artifact_stats");
2321 }
2322
--- src/name.c
+++ src/name.c
@@ -1816,11 +1816,11 @@
1816 }
1817 if( bUnclst==0 ){
1818 style_submenu_element("Unclustered","bloblist?unclustered");
1819 }
1820 if( g.perm.Admin ){
1821 style_submenu_element("Xfer Log", "rcvfromlist");
1822 }
1823 if( !phantomOnly ){
1824 style_submenu_element("Phantoms", "bloblist?phan");
1825 }
1826 style_submenu_element("Clusters","clusterlist");
@@ -2005,11 +2005,11 @@
2005 void phantom_list_page(void){
2006 login_check_credentials();
2007 if( !g.perm.Read ){ login_needed(g.anon.Read); return; }
2008 style_header("Public Phantom Artifacts");
2009 if( g.perm.Admin ){
2010 style_submenu_element("Xfer Log", "rcvfromlist");
2011 style_submenu_element("Artifact List", "bloblist");
2012 }
2013 if( g.perm.Write ){
2014 style_submenu_element("Artifact Stats", "artifact_stats");
2015 }
@@ -2030,11 +2030,11 @@
2030 int n = atoi(PD("n","250"));
2031
2032 login_check_credentials();
2033 if( !g.perm.Read ){ login_needed(g.anon.Read); return; }
2034 if( g.perm.Admin ){
2035 style_submenu_element("Xfer Log", "rcvfromlist");
2036 }
2037 if( g.perm.Write ){
2038 style_submenu_element("Artifact Stats", "artifact_stats");
2039 }
2040 style_submenu_element("All Artifacts", "bloblist");
@@ -2203,15 +2203,74 @@
2203 /*
2204 ** COMMAND: test-phantoms
2205 **
2206 ** Usage: %fossil test-phantoms
2207 **
2208 ** Show all phantom artifacts. A phantom artifact is one for which there
2209 ** is no content. Options:
2210 **
2211 ** --count Show only a count of the number of phantoms.
2212 ** --delta Show all delta-phantoms. A delta-phantom is a
2213 ** artifact for which there is a delta but the delta
2214 ** source is a phantom.
2215 ** --list Just list the phantoms. Do not try to describe them.
2216 */
2217 void test_phatoms_cmd(void){
2218 int bDelta;
2219 int bList;
2220 int bCount;
2221 unsigned nPhantom = 0;
2222 unsigned nDeltaPhantom = 0;
2223 db_find_and_open_repository(0,0);
2224 bDelta = find_option("delta", 0, 0)!=0;
2225 bList = find_option("list", 0, 0)!=0;
2226 bCount = find_option("count", 0, 0)!=0;
2227 verify_all_options();
2228 if( bList || bCount ){
2229 Stmt q1, q2;
2230 db_prepare(&q1, "SELECT rid, uuid FROM blob WHERE size<0");
2231 while( db_step(&q1)==SQLITE_ROW ){
2232 int rid = db_column_int(&q1, 0);
2233 nPhantom++;
2234 if( !bCount ){
2235 fossil_print("%S (%d)\n", db_column_text(&q1,1), rid);
2236 }
2237 db_prepare(&q2,
2238 "WITH RECURSIVE deltasof(rid) AS ("
2239 " SELECT rid FROM delta WHERE srcid=%d"
2240 " UNION"
2241 " SELECT delta.rid FROM deltasof, delta"
2242 " WHERE delta.srcid=deltasof.rid)"
2243 "SELECT deltasof.rid, blob.uuid FROM deltasof LEFT JOIN blob"
2244 " ON blob.rid=deltasof.rid", rid
2245 );
2246 while( db_step(&q2)==SQLITE_ROW ){
2247 nDeltaPhantom++;
2248 if( !bCount ){
2249 fossil_print(" %S (%d)\n", db_column_text(&q2,1),
2250 db_column_int(&q2,0));
2251 }
2252 }
2253 db_finalize(&q2);
2254 }
2255 db_finalize(&q1);
2256 if( nPhantom ){
2257 fossil_print("Phantoms: %u Delta-phantoms: %u\n",
2258 nPhantom, nDeltaPhantom);
2259 }
2260 }else if( bDelta ){
2261 describe_artifacts_to_stdout(
2262 "IN (WITH RECURSIVE delta_phantom(rid) AS (\n"
2263 " SELECT delta.rid FROM blob, delta\n"
2264 " WHERE blob.size<0 AND delta.srcid=blob.rid\n"
2265 " UNION\n"
2266 " SELECT delta.rid FROM delta_phantom, delta\n"
2267 " WHERE delta.srcid=delta_phantom.rid)\n"
2268 " SELECT rid FROM delta_phantom)", 0);
2269 }else{
2270 describe_artifacts_to_stdout("IN (SELECT rid FROM blob WHERE size<0)", 0);
2271 }
2272 }
2273
2274 /* Maximum number of collision examples to remember */
2275 #define MAX_COLLIDE 25
2276
@@ -2311,11 +2370,11 @@
2370 login_check_credentials();
2371 if( !g.perm.Read ){ login_needed(g.anon.Read); return; }
2372 style_header("All Cluster Artifacts");
2373 style_submenu_element("All Artifactst", "bloblist");
2374 if( g.perm.Admin ){
2375 style_submenu_element("Xfer Log", "rcvfromlist");
2376 }
2377 style_submenu_element("Phantoms", "bloblist?phan");
2378 if( g.perm.Write ){
2379 style_submenu_element("Artifact Stats", "artifact_stats");
2380 }
2381
+8 -3
--- src/printf.c
+++ src/printf.c
@@ -1077,10 +1077,11 @@
10771077
time_t now;
10781078
FILE *out;
10791079
const char *z;
10801080
int i;
10811081
int bDetail = 0;
1082
+ int bBrief = 0;
10821083
va_list ap;
10831084
static const char *const azEnv[] = { "HTTP_HOST", "HTTP_REFERER",
10841085
"HTTP_USER_AGENT",
10851086
"PATH_INFO", "QUERY_STRING", "REMOTE_ADDR", "REQUEST_METHOD",
10861087
"REQUEST_URI", "SCRIPT_NAME" };
@@ -1098,16 +1099,20 @@
10981099
pNow->tm_hour, pNow->tm_min, pNow->tm_sec);
10991100
va_start(ap, zFormat);
11001101
if( zFormat[0]=='X' ){
11011102
bDetail = 1;
11021103
zFormat++;
1104
+ }else if( strncmp(zFormat,"SMTP:",5)==0 ){
1105
+ bBrief = 1;
11031106
}
11041107
vfprintf(out, zFormat, ap);
1105
- fprintf(out, "\n");
1108
+ fprintf(out, " (pid %d)\n", (int)getpid());
11061109
va_end(ap);
11071110
if( g.zPhase!=0 ) fprintf(out, "while in %s\n", g.zPhase);
1108
- if( bDetail ){
1111
+ if( bBrief ){
1112
+ /* Say nothing more */
1113
+ }else if( bDetail ){
11091114
cgi_print_all(1,3,out);
11101115
}else{
11111116
for(i=0; i<count(azEnv); i++){
11121117
char *p;
11131118
if( (p = fossil_getenv(azEnv[i]))!=0 && p[0]!=0 ){
@@ -1116,11 +1121,11 @@
11161121
}else if( (z = P(azEnv[i]))!=0 && z[0]!=0 ){
11171122
fprintf(out, "%s=%s\n", azEnv[i], z);
11181123
}
11191124
}
11201125
}
1121
- fclose(out);
1126
+ if( out!=stderr ) fclose(out);
11221127
}
11231128
11241129
/*
11251130
** The following variable becomes true while processing a fatal error
11261131
** or a panic. If additional "recursive-fatal" errors occur while
11271132
--- src/printf.c
+++ src/printf.c
@@ -1077,10 +1077,11 @@
1077 time_t now;
1078 FILE *out;
1079 const char *z;
1080 int i;
1081 int bDetail = 0;
 
1082 va_list ap;
1083 static const char *const azEnv[] = { "HTTP_HOST", "HTTP_REFERER",
1084 "HTTP_USER_AGENT",
1085 "PATH_INFO", "QUERY_STRING", "REMOTE_ADDR", "REQUEST_METHOD",
1086 "REQUEST_URI", "SCRIPT_NAME" };
@@ -1098,16 +1099,20 @@
1098 pNow->tm_hour, pNow->tm_min, pNow->tm_sec);
1099 va_start(ap, zFormat);
1100 if( zFormat[0]=='X' ){
1101 bDetail = 1;
1102 zFormat++;
 
 
1103 }
1104 vfprintf(out, zFormat, ap);
1105 fprintf(out, "\n");
1106 va_end(ap);
1107 if( g.zPhase!=0 ) fprintf(out, "while in %s\n", g.zPhase);
1108 if( bDetail ){
 
 
1109 cgi_print_all(1,3,out);
1110 }else{
1111 for(i=0; i<count(azEnv); i++){
1112 char *p;
1113 if( (p = fossil_getenv(azEnv[i]))!=0 && p[0]!=0 ){
@@ -1116,11 +1121,11 @@
1116 }else if( (z = P(azEnv[i]))!=0 && z[0]!=0 ){
1117 fprintf(out, "%s=%s\n", azEnv[i], z);
1118 }
1119 }
1120 }
1121 fclose(out);
1122 }
1123
1124 /*
1125 ** The following variable becomes true while processing a fatal error
1126 ** or a panic. If additional "recursive-fatal" errors occur while
1127
--- src/printf.c
+++ src/printf.c
@@ -1077,10 +1077,11 @@
1077 time_t now;
1078 FILE *out;
1079 const char *z;
1080 int i;
1081 int bDetail = 0;
1082 int bBrief = 0;
1083 va_list ap;
1084 static const char *const azEnv[] = { "HTTP_HOST", "HTTP_REFERER",
1085 "HTTP_USER_AGENT",
1086 "PATH_INFO", "QUERY_STRING", "REMOTE_ADDR", "REQUEST_METHOD",
1087 "REQUEST_URI", "SCRIPT_NAME" };
@@ -1098,16 +1099,20 @@
1099 pNow->tm_hour, pNow->tm_min, pNow->tm_sec);
1100 va_start(ap, zFormat);
1101 if( zFormat[0]=='X' ){
1102 bDetail = 1;
1103 zFormat++;
1104 }else if( strncmp(zFormat,"SMTP:",5)==0 ){
1105 bBrief = 1;
1106 }
1107 vfprintf(out, zFormat, ap);
1108 fprintf(out, " (pid %d)\n", (int)getpid());
1109 va_end(ap);
1110 if( g.zPhase!=0 ) fprintf(out, "while in %s\n", g.zPhase);
1111 if( bBrief ){
1112 /* Say nothing more */
1113 }else if( bDetail ){
1114 cgi_print_all(1,3,out);
1115 }else{
1116 for(i=0; i<count(azEnv); i++){
1117 char *p;
1118 if( (p = fossil_getenv(azEnv[i]))!=0 && p[0]!=0 ){
@@ -1116,11 +1121,11 @@
1121 }else if( (z = P(azEnv[i]))!=0 && z[0]!=0 ){
1122 fprintf(out, "%s=%s\n", azEnv[i], z);
1123 }
1124 }
1125 }
1126 if( out!=stderr ) fclose(out);
1127 }
1128
1129 /*
1130 ** The following variable becomes true while processing a fatal error
1131 ** or a panic. If additional "recursive-fatal" errors occur while
1132
+113 -23
--- src/repolist.c
+++ src/repolist.c
@@ -31,10 +31,11 @@
3131
int isValid; /* True if zRepoName is a valid Fossil repository */
3232
int isRepolistSkin; /* 1 or 2 if this repository wants to be the skin
3333
** for the repository list. 2 means do use this
3434
** repository but do not display it in the list. */
3535
char *zProjName; /* Project Name. Memory from fossil_malloc() */
36
+ char *zProjDesc; /* Project Description. Memory from fossil_malloc() */
3637
char *zLoginGroup; /* Name of login group, or NULL. Malloced() */
3738
double rMTime; /* Last update. Julian day number */
3839
};
3940
#endif
4041
@@ -49,10 +50,11 @@
4950
int rc;
5051
5152
pRepo->isRepolistSkin = 0;
5253
pRepo->isValid = 0;
5354
pRepo->zProjName = 0;
55
+ pRepo->zProjDesc = 0;
5456
pRepo->zLoginGroup = 0;
5557
pRepo->rMTime = 0.0;
5658
5759
g.dbIgnoreErrors++;
5860
rc = sqlite3_open_v2(pRepo->zRepoName, &db, SQLITE_OPEN_READWRITE, 0);
@@ -71,10 +73,19 @@
7173
-1, &pStmt, 0);
7274
if( rc ) goto finish_repo_list;
7375
if( sqlite3_step(pStmt)==SQLITE_ROW ){
7476
pRepo->zProjName = fossil_strdup((char*)sqlite3_column_text(pStmt,0));
7577
}
78
+ sqlite3_finalize(pStmt);
79
+ if( rc ) goto finish_repo_list;
80
+ rc = sqlite3_prepare_v2(db, "SELECT value FROM config"
81
+ " WHERE name='project-description'",
82
+ -1, &pStmt, 0);
83
+ if( rc ) goto finish_repo_list;
84
+ if( sqlite3_step(pStmt)==SQLITE_ROW ){
85
+ pRepo->zProjDesc = fossil_strdup((char*)sqlite3_column_text(pStmt,0));
86
+ }
7687
sqlite3_finalize(pStmt);
7788
rc = sqlite3_prepare_v2(db, "SELECT value FROM config"
7889
" WHERE name='login-group-name'",
7990
-1, &pStmt, 0);
8091
if( rc==SQLITE_OK && sqlite3_step(pStmt)==SQLITE_ROW ){
@@ -89,10 +100,25 @@
89100
sqlite3_finalize(pStmt);
90101
finish_repo_list:
91102
g.dbIgnoreErrors--;
92103
sqlite3_close(db);
93104
}
105
+
106
+/*
107
+** SETTING: show-repolist-desc boolean default=off
108
+**
109
+** If the value of this setting is "1" globally, then the repository-list
110
+** page will show the description of each repository. This setting only
111
+** has effect when it is in the global setting database.
112
+*/
113
+/*
114
+** SETTING: show-repolist-lg boolean default=off
115
+**
116
+** If the value of this setting is "1" globally, then the repository-list
117
+** page will show the login-group for each repository. This setting only
118
+** has effect when it is in the global setting database.
119
+*/
94120
95121
/*
96122
** Generate a web-page that lists all repositories located under the
97123
** g.zRepositoryName directory and return non-zero.
98124
**
@@ -116,12 +142,27 @@
116142
** False if a directory scan of base for repos */
117143
Blob html; /* Html for the body of the repository list */
118144
char *zSkinRepo = 0; /* Name of the repository database used for skins */
119145
char *zSkinUrl = 0; /* URL for the skin database */
120146
int quickfilter = 0; /* Is quickfilter is enabled? */
147
+ const char *zShow; /* Value of FOSSIL_REPOLIST_SHOW environment variable */
148
+ int bShowDesc = 0; /* True to show the description column */
149
+ int bShowLg = 0; /* True to show the login-group column */
121150
122151
assert( g.db==0 );
152
+ zShow = P("FOSSIL_REPOLIST_SHOW");
153
+ if( zShow ){
154
+ bShowDesc = strstr(zShow,"description")!=0;
155
+ bShowLg = strstr(zShow,"login-group")!=0;
156
+ }else if( db_open_config(1, 1)
157
+ && db_table_exists("configdb", "global_config")
158
+ ){
159
+ bShowDesc = db_int(bShowDesc, "SELECT value FROM global_config"
160
+ " WHERE name='show-repolist-desc'");
161
+ bShowLg = db_int(bShowLg, "SELECT value FROM global_config"
162
+ " WHERE name='show-repolist-lg'");
163
+ }
123164
blob_init(&html, 0, 0);
124165
if( fossil_strcmp(g.zRepositoryName,"/")==0 && !g.fJail ){
125166
/* For the special case of the "repository directory" being "/",
126167
** show all of the repositories named in the ~/.fossil database.
127168
**
@@ -128,11 +169,10 @@
128169
** On unix systems, then entries are of the form "repo:/home/..."
129170
** and on Windows systems they are like on unix, starting with a "/"
130171
** or they can begin with a drive letter: "repo:C:/Users/...". In either
131172
** case, we want returned path to omit any initial "/".
132173
*/
133
- db_open_config(1, 0);
134174
db_multi_exec(
135175
"CREATE TEMP VIEW sfile AS"
136176
" SELECT ltrim(substr(name,6),'/') AS 'pathname' FROM global_config"
137177
" WHERE name GLOB 'repo:*'"
138178
);
@@ -140,10 +180,12 @@
140180
}else{
141181
/* The default case: All repositories under the g.zRepositoryName
142182
** directory.
143183
*/
144184
blob_init(&base, g.zRepositoryName, -1);
185
+ db_close(0);
186
+ assert( g.db==0 );
145187
sqlite3_open(":memory:", &g.db);
146188
db_multi_exec("CREATE TABLE sfile(pathname TEXT);");
147189
db_multi_exec("CREATE TABLE vfile(pathname);");
148190
vfile_scan(&base, blob_size(&base), 0, 0, 0, ExtFILE);
149191
db_multi_exec("DELETE FROM sfile WHERE pathname NOT GLOB '*[^/].fossil'"
@@ -161,18 +203,50 @@
161203
g.localOpen = 0;
162204
return 0;
163205
}else{
164206
Stmt q;
165207
double rNow;
166
- blob_append_sql(&html,
208
+ char zType[16]; /* Column type letters for class "sortable" */
209
+ int nType;
210
+ zType[0] = 't'; /* Repo name */
211
+ zType[1] = 'x'; /* Space between repo-name and project-name */
212
+ zType[2] = 't'; /* Project name */
213
+ nType = 3;
214
+ if( bShowDesc ){
215
+ zType[nType++] = 'x'; /* Space between name and description */
216
+ zType[nType++] = 't'; /* Project description */
217
+ }
218
+ zType[nType++] = 'x'; /* space before age */
219
+ zType[nType++] = 'k'; /* Project age */
220
+ if( bShowLg ){
221
+ zType[nType++] = 'x'; /* space before login-group */
222
+ zType[nType++] = 't'; /* Login Group */
223
+ }
224
+ zType[nType] = 0;
225
+ blob_appendf(&html,
167226
"<table border='0' class='sortable filterlist' data-init-sort='1'"
168
- " data-column-types='txtxkxt'><thead>\n"
169
- "<tr><th>Filename<th width='20'>"
170
- "<th>Project Name<th width='20'>"
171
- "<th>Last Modified<th width='20'>"
172
- "<th>Login Group</tr>\n"
173
- "</thead><tbody>\n");
227
+ " data-column-types='%s' cellspacing='0' cellpadding='0'><thead>\n"
228
+ "<tr><th>Filename</th><th>&emsp;</th>\n"
229
+ "<th%s><nobr>Project Name</nobr></th>\n",
230
+ zType, (bShowDesc ? " width='25%'" : ""));
231
+ if( bShowDesc ){
232
+ blob_appendf(&html,
233
+ "<th>&emsp;</th>\n"
234
+ "<th width='25%%'><nobr>Project Description</nobr></th>\n"
235
+ );
236
+ }
237
+ blob_appendf(&html,
238
+ "<th>&emsp;</th>"
239
+ "<th><nobr>Last Modified</nobr></th>\n"
240
+ );
241
+ if( bShowLg ){
242
+ blob_appendf(&html,
243
+ "<th>&emsp;</th>"
244
+ "<th><nobr>Login Group</nobr></th></tr>\n"
245
+ );
246
+ }
247
+ blob_appendf(&html,"</thead><tbody>\n");
174248
db_prepare(&q, "SELECT pathname"
175249
" FROM sfile ORDER BY pathname COLLATE nocase;");
176250
rNow = db_double(0, "SELECT julianday('now')");
177251
while( db_step(&q)==SQLITE_ROW ){
178252
const char *zName = db_column_text(&q, 0);
@@ -231,21 +305,21 @@
231305
if( x.rMTime==0.0 ){
232306
/* This repository has no entry in the "event" table.
233307
** Its age will still be maximum, so data-sortkey will work. */
234308
zAge = mprintf("unknown");
235309
}
236
- blob_append_sql(&html, "<tr><td valign='top'>");
310
+ blob_appendf(&html, "<tr><td valign='top'><nobr>");
237311
if( !file_ends_with_repository_extension(zName,0) ){
238312
/* The "fossil server DIRECTORY" and "fossil ui DIRECTORY" commands
239313
** do not work for repositories whose names do not end in ".fossil".
240314
** So do not hyperlink those cases. */
241
- blob_append_sql(&html,"%h",zName);
315
+ blob_appendf(&html,"%h",zName);
242316
} else if( sqlite3_strglob("*/.*", zName)==0 ){
243317
/* Do not show hyperlinks for hidden repos */
244
- blob_append_sql(&html, "%h (hidden)", zName);
318
+ blob_appendf(&html, "%h (hidden)", zName);
245319
} else if( allRepo && sqlite3_strglob("[a-zA-Z]:/?*", zName)!=0 ){
246
- blob_append_sql(&html,
320
+ blob_appendf(&html,
247321
"<a href='%R/%T/home' target='_blank'>/%h</a>\n",
248322
zUrl, zName);
249323
}else if( file_ends_with_repository_extension(zName,1) ){
250324
/* As described in
251325
** https://fossil-scm.org/forum/info/f50f647c97c72fc1: if
@@ -262,44 +336,60 @@
262336
, zDirPart
263337
#if USE_SEE
264338
, zDirPart
265339
#endif
266340
) ){
267
- blob_append_sql(&html,
341
+ blob_appendf(&html,
268342
"<s>%h</s> (directory/repo name collision)\n",
269343
zName);
270344
}else{
271
- blob_append_sql(&html,
345
+ blob_appendf(&html,
272346
"<a href='%R/%T/home' target='_blank'>%h</a>\n",
273347
zUrl, zName);
274348
}
275349
fossil_free(zDirPart);
276350
}else{
277
- blob_append_sql(&html,
351
+ blob_appendf(&html,
278352
"<a href='%R/%T/home' target='_blank'>%h</a>\n",
279353
zUrl, zName);
280354
}
355
+ blob_appendf(&html,"</nobr></td>\n");
281356
if( x.zProjName ){
282
- blob_append_sql(&html, "<td></td><td>%h</td>\n", x.zProjName);
357
+ blob_appendf(&html, "<td>&emsp;</td><td valign='top'>%h</td>\n",
358
+ x.zProjName);
283359
fossil_free(x.zProjName);
284360
}else{
285
- blob_append_sql(&html, "<td></td><td></td>\n");
361
+ blob_appendf(&html, "<td>&emsp;</td><td></td>\n");
362
+ }
363
+ if( !bShowDesc ){
364
+ /* Do nothing */
365
+ }else if( x.zProjDesc ){
366
+ blob_appendf(&html, "<td>&emsp;</td><td valign='top'>%h</td>\n",
367
+ x.zProjDesc);
368
+ fossil_free(x.zProjDesc);
369
+ }else{
370
+ blob_appendf(&html, "<td>&emsp;</td><td></td>\n");
286371
}
287
- blob_append_sql(&html,
288
- "<td></td><td data-sortkey='%08x'>%h</td>\n",
372
+ blob_appendf(&html,
373
+ "<td>&emsp;</td><td data-sortkey='%08x' align='center' valign='top'>"
374
+ "<nobr>%h</nobr></td>\n",
289375
(int)iAge, zAge);
290376
fossil_free(zAge);
291
- if( x.zLoginGroup ){
292
- blob_append_sql(&html, "<td></td><td>%h</td></tr>\n", x.zLoginGroup);
377
+ if( !bShowLg ){
378
+ blob_appendf(&html, "</tr>\n");
379
+ }else if( x.zLoginGroup ){
380
+ blob_appendf(&html, "<td>&emsp;</td><td valign='top'>"
381
+ "<nobr>%h</nobr></td></tr>\n",
382
+ x.zLoginGroup);
293383
fossil_free(x.zLoginGroup);
294384
}else{
295
- blob_append_sql(&html, "<td></td><td></td></tr>\n");
385
+ blob_appendf(&html, "<td>&emsp;</td><td></td></tr>\n");
296386
}
297387
sqlite3_free(zUrl);
298388
}
299389
db_finalize(&q);
300
- blob_append_sql(&html,"</tbody></table>\n");
390
+ blob_appendf(&html,"</tbody></table>\n");
301391
}
302392
if( zSkinRepo ){
303393
char *zNewBase = mprintf("%s/%s", g.zBaseURL, zSkinUrl);
304394
g.zBaseURL = 0;
305395
set_base_url(zNewBase);
306396
--- src/repolist.c
+++ src/repolist.c
@@ -31,10 +31,11 @@
31 int isValid; /* True if zRepoName is a valid Fossil repository */
32 int isRepolistSkin; /* 1 or 2 if this repository wants to be the skin
33 ** for the repository list. 2 means do use this
34 ** repository but do not display it in the list. */
35 char *zProjName; /* Project Name. Memory from fossil_malloc() */
 
36 char *zLoginGroup; /* Name of login group, or NULL. Malloced() */
37 double rMTime; /* Last update. Julian day number */
38 };
39 #endif
40
@@ -49,10 +50,11 @@
49 int rc;
50
51 pRepo->isRepolistSkin = 0;
52 pRepo->isValid = 0;
53 pRepo->zProjName = 0;
 
54 pRepo->zLoginGroup = 0;
55 pRepo->rMTime = 0.0;
56
57 g.dbIgnoreErrors++;
58 rc = sqlite3_open_v2(pRepo->zRepoName, &db, SQLITE_OPEN_READWRITE, 0);
@@ -71,10 +73,19 @@
71 -1, &pStmt, 0);
72 if( rc ) goto finish_repo_list;
73 if( sqlite3_step(pStmt)==SQLITE_ROW ){
74 pRepo->zProjName = fossil_strdup((char*)sqlite3_column_text(pStmt,0));
75 }
 
 
 
 
 
 
 
 
 
76 sqlite3_finalize(pStmt);
77 rc = sqlite3_prepare_v2(db, "SELECT value FROM config"
78 " WHERE name='login-group-name'",
79 -1, &pStmt, 0);
80 if( rc==SQLITE_OK && sqlite3_step(pStmt)==SQLITE_ROW ){
@@ -89,10 +100,25 @@
89 sqlite3_finalize(pStmt);
90 finish_repo_list:
91 g.dbIgnoreErrors--;
92 sqlite3_close(db);
93 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
95 /*
96 ** Generate a web-page that lists all repositories located under the
97 ** g.zRepositoryName directory and return non-zero.
98 **
@@ -116,12 +142,27 @@
116 ** False if a directory scan of base for repos */
117 Blob html; /* Html for the body of the repository list */
118 char *zSkinRepo = 0; /* Name of the repository database used for skins */
119 char *zSkinUrl = 0; /* URL for the skin database */
120 int quickfilter = 0; /* Is quickfilter is enabled? */
 
 
 
121
122 assert( g.db==0 );
 
 
 
 
 
 
 
 
 
 
 
 
123 blob_init(&html, 0, 0);
124 if( fossil_strcmp(g.zRepositoryName,"/")==0 && !g.fJail ){
125 /* For the special case of the "repository directory" being "/",
126 ** show all of the repositories named in the ~/.fossil database.
127 **
@@ -128,11 +169,10 @@
128 ** On unix systems, then entries are of the form "repo:/home/..."
129 ** and on Windows systems they are like on unix, starting with a "/"
130 ** or they can begin with a drive letter: "repo:C:/Users/...". In either
131 ** case, we want returned path to omit any initial "/".
132 */
133 db_open_config(1, 0);
134 db_multi_exec(
135 "CREATE TEMP VIEW sfile AS"
136 " SELECT ltrim(substr(name,6),'/') AS 'pathname' FROM global_config"
137 " WHERE name GLOB 'repo:*'"
138 );
@@ -140,10 +180,12 @@
140 }else{
141 /* The default case: All repositories under the g.zRepositoryName
142 ** directory.
143 */
144 blob_init(&base, g.zRepositoryName, -1);
 
 
145 sqlite3_open(":memory:", &g.db);
146 db_multi_exec("CREATE TABLE sfile(pathname TEXT);");
147 db_multi_exec("CREATE TABLE vfile(pathname);");
148 vfile_scan(&base, blob_size(&base), 0, 0, 0, ExtFILE);
149 db_multi_exec("DELETE FROM sfile WHERE pathname NOT GLOB '*[^/].fossil'"
@@ -161,18 +203,50 @@
161 g.localOpen = 0;
162 return 0;
163 }else{
164 Stmt q;
165 double rNow;
166 blob_append_sql(&html,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
167 "<table border='0' class='sortable filterlist' data-init-sort='1'"
168 " data-column-types='txtxkxt'><thead>\n"
169 "<tr><th>Filename<th width='20'>"
170 "<th>Project Name<th width='20'>"
171 "<th>Last Modified<th width='20'>"
172 "<th>Login Group</tr>\n"
173 "</thead><tbody>\n");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174 db_prepare(&q, "SELECT pathname"
175 " FROM sfile ORDER BY pathname COLLATE nocase;");
176 rNow = db_double(0, "SELECT julianday('now')");
177 while( db_step(&q)==SQLITE_ROW ){
178 const char *zName = db_column_text(&q, 0);
@@ -231,21 +305,21 @@
231 if( x.rMTime==0.0 ){
232 /* This repository has no entry in the "event" table.
233 ** Its age will still be maximum, so data-sortkey will work. */
234 zAge = mprintf("unknown");
235 }
236 blob_append_sql(&html, "<tr><td valign='top'>");
237 if( !file_ends_with_repository_extension(zName,0) ){
238 /* The "fossil server DIRECTORY" and "fossil ui DIRECTORY" commands
239 ** do not work for repositories whose names do not end in ".fossil".
240 ** So do not hyperlink those cases. */
241 blob_append_sql(&html,"%h",zName);
242 } else if( sqlite3_strglob("*/.*", zName)==0 ){
243 /* Do not show hyperlinks for hidden repos */
244 blob_append_sql(&html, "%h (hidden)", zName);
245 } else if( allRepo && sqlite3_strglob("[a-zA-Z]:/?*", zName)!=0 ){
246 blob_append_sql(&html,
247 "<a href='%R/%T/home' target='_blank'>/%h</a>\n",
248 zUrl, zName);
249 }else if( file_ends_with_repository_extension(zName,1) ){
250 /* As described in
251 ** https://fossil-scm.org/forum/info/f50f647c97c72fc1: if
@@ -262,44 +336,60 @@
262 , zDirPart
263 #if USE_SEE
264 , zDirPart
265 #endif
266 ) ){
267 blob_append_sql(&html,
268 "<s>%h</s> (directory/repo name collision)\n",
269 zName);
270 }else{
271 blob_append_sql(&html,
272 "<a href='%R/%T/home' target='_blank'>%h</a>\n",
273 zUrl, zName);
274 }
275 fossil_free(zDirPart);
276 }else{
277 blob_append_sql(&html,
278 "<a href='%R/%T/home' target='_blank'>%h</a>\n",
279 zUrl, zName);
280 }
 
281 if( x.zProjName ){
282 blob_append_sql(&html, "<td></td><td>%h</td>\n", x.zProjName);
 
283 fossil_free(x.zProjName);
284 }else{
285 blob_append_sql(&html, "<td></td><td></td>\n");
 
 
 
 
 
 
 
 
 
286 }
287 blob_append_sql(&html,
288 "<td></td><td data-sortkey='%08x'>%h</td>\n",
 
289 (int)iAge, zAge);
290 fossil_free(zAge);
291 if( x.zLoginGroup ){
292 blob_append_sql(&html, "<td></td><td>%h</td></tr>\n", x.zLoginGroup);
 
 
 
 
293 fossil_free(x.zLoginGroup);
294 }else{
295 blob_append_sql(&html, "<td></td><td></td></tr>\n");
296 }
297 sqlite3_free(zUrl);
298 }
299 db_finalize(&q);
300 blob_append_sql(&html,"</tbody></table>\n");
301 }
302 if( zSkinRepo ){
303 char *zNewBase = mprintf("%s/%s", g.zBaseURL, zSkinUrl);
304 g.zBaseURL = 0;
305 set_base_url(zNewBase);
306
--- src/repolist.c
+++ src/repolist.c
@@ -31,10 +31,11 @@
31 int isValid; /* True if zRepoName is a valid Fossil repository */
32 int isRepolistSkin; /* 1 or 2 if this repository wants to be the skin
33 ** for the repository list. 2 means do use this
34 ** repository but do not display it in the list. */
35 char *zProjName; /* Project Name. Memory from fossil_malloc() */
36 char *zProjDesc; /* Project Description. Memory from fossil_malloc() */
37 char *zLoginGroup; /* Name of login group, or NULL. Malloced() */
38 double rMTime; /* Last update. Julian day number */
39 };
40 #endif
41
@@ -49,10 +50,11 @@
50 int rc;
51
52 pRepo->isRepolistSkin = 0;
53 pRepo->isValid = 0;
54 pRepo->zProjName = 0;
55 pRepo->zProjDesc = 0;
56 pRepo->zLoginGroup = 0;
57 pRepo->rMTime = 0.0;
58
59 g.dbIgnoreErrors++;
60 rc = sqlite3_open_v2(pRepo->zRepoName, &db, SQLITE_OPEN_READWRITE, 0);
@@ -71,10 +73,19 @@
73 -1, &pStmt, 0);
74 if( rc ) goto finish_repo_list;
75 if( sqlite3_step(pStmt)==SQLITE_ROW ){
76 pRepo->zProjName = fossil_strdup((char*)sqlite3_column_text(pStmt,0));
77 }
78 sqlite3_finalize(pStmt);
79 if( rc ) goto finish_repo_list;
80 rc = sqlite3_prepare_v2(db, "SELECT value FROM config"
81 " WHERE name='project-description'",
82 -1, &pStmt, 0);
83 if( rc ) goto finish_repo_list;
84 if( sqlite3_step(pStmt)==SQLITE_ROW ){
85 pRepo->zProjDesc = fossil_strdup((char*)sqlite3_column_text(pStmt,0));
86 }
87 sqlite3_finalize(pStmt);
88 rc = sqlite3_prepare_v2(db, "SELECT value FROM config"
89 " WHERE name='login-group-name'",
90 -1, &pStmt, 0);
91 if( rc==SQLITE_OK && sqlite3_step(pStmt)==SQLITE_ROW ){
@@ -89,10 +100,25 @@
100 sqlite3_finalize(pStmt);
101 finish_repo_list:
102 g.dbIgnoreErrors--;
103 sqlite3_close(db);
104 }
105
106 /*
107 ** SETTING: show-repolist-desc boolean default=off
108 **
109 ** If the value of this setting is "1" globally, then the repository-list
110 ** page will show the description of each repository. This setting only
111 ** has effect when it is in the global setting database.
112 */
113 /*
114 ** SETTING: show-repolist-lg boolean default=off
115 **
116 ** If the value of this setting is "1" globally, then the repository-list
117 ** page will show the login-group for each repository. This setting only
118 ** has effect when it is in the global setting database.
119 */
120
121 /*
122 ** Generate a web-page that lists all repositories located under the
123 ** g.zRepositoryName directory and return non-zero.
124 **
@@ -116,12 +142,27 @@
142 ** False if a directory scan of base for repos */
143 Blob html; /* Html for the body of the repository list */
144 char *zSkinRepo = 0; /* Name of the repository database used for skins */
145 char *zSkinUrl = 0; /* URL for the skin database */
146 int quickfilter = 0; /* Is quickfilter is enabled? */
147 const char *zShow; /* Value of FOSSIL_REPOLIST_SHOW environment variable */
148 int bShowDesc = 0; /* True to show the description column */
149 int bShowLg = 0; /* True to show the login-group column */
150
151 assert( g.db==0 );
152 zShow = P("FOSSIL_REPOLIST_SHOW");
153 if( zShow ){
154 bShowDesc = strstr(zShow,"description")!=0;
155 bShowLg = strstr(zShow,"login-group")!=0;
156 }else if( db_open_config(1, 1)
157 && db_table_exists("configdb", "global_config")
158 ){
159 bShowDesc = db_int(bShowDesc, "SELECT value FROM global_config"
160 " WHERE name='show-repolist-desc'");
161 bShowLg = db_int(bShowLg, "SELECT value FROM global_config"
162 " WHERE name='show-repolist-lg'");
163 }
164 blob_init(&html, 0, 0);
165 if( fossil_strcmp(g.zRepositoryName,"/")==0 && !g.fJail ){
166 /* For the special case of the "repository directory" being "/",
167 ** show all of the repositories named in the ~/.fossil database.
168 **
@@ -128,11 +169,10 @@
169 ** On unix systems, then entries are of the form "repo:/home/..."
170 ** and on Windows systems they are like on unix, starting with a "/"
171 ** or they can begin with a drive letter: "repo:C:/Users/...". In either
172 ** case, we want returned path to omit any initial "/".
173 */
 
174 db_multi_exec(
175 "CREATE TEMP VIEW sfile AS"
176 " SELECT ltrim(substr(name,6),'/') AS 'pathname' FROM global_config"
177 " WHERE name GLOB 'repo:*'"
178 );
@@ -140,10 +180,12 @@
180 }else{
181 /* The default case: All repositories under the g.zRepositoryName
182 ** directory.
183 */
184 blob_init(&base, g.zRepositoryName, -1);
185 db_close(0);
186 assert( g.db==0 );
187 sqlite3_open(":memory:", &g.db);
188 db_multi_exec("CREATE TABLE sfile(pathname TEXT);");
189 db_multi_exec("CREATE TABLE vfile(pathname);");
190 vfile_scan(&base, blob_size(&base), 0, 0, 0, ExtFILE);
191 db_multi_exec("DELETE FROM sfile WHERE pathname NOT GLOB '*[^/].fossil'"
@@ -161,18 +203,50 @@
203 g.localOpen = 0;
204 return 0;
205 }else{
206 Stmt q;
207 double rNow;
208 char zType[16]; /* Column type letters for class "sortable" */
209 int nType;
210 zType[0] = 't'; /* Repo name */
211 zType[1] = 'x'; /* Space between repo-name and project-name */
212 zType[2] = 't'; /* Project name */
213 nType = 3;
214 if( bShowDesc ){
215 zType[nType++] = 'x'; /* Space between name and description */
216 zType[nType++] = 't'; /* Project description */
217 }
218 zType[nType++] = 'x'; /* space before age */
219 zType[nType++] = 'k'; /* Project age */
220 if( bShowLg ){
221 zType[nType++] = 'x'; /* space before login-group */
222 zType[nType++] = 't'; /* Login Group */
223 }
224 zType[nType] = 0;
225 blob_appendf(&html,
226 "<table border='0' class='sortable filterlist' data-init-sort='1'"
227 " data-column-types='%s' cellspacing='0' cellpadding='0'><thead>\n"
228 "<tr><th>Filename</th><th>&emsp;</th>\n"
229 "<th%s><nobr>Project Name</nobr></th>\n",
230 zType, (bShowDesc ? " width='25%'" : ""));
231 if( bShowDesc ){
232 blob_appendf(&html,
233 "<th>&emsp;</th>\n"
234 "<th width='25%%'><nobr>Project Description</nobr></th>\n"
235 );
236 }
237 blob_appendf(&html,
238 "<th>&emsp;</th>"
239 "<th><nobr>Last Modified</nobr></th>\n"
240 );
241 if( bShowLg ){
242 blob_appendf(&html,
243 "<th>&emsp;</th>"
244 "<th><nobr>Login Group</nobr></th></tr>\n"
245 );
246 }
247 blob_appendf(&html,"</thead><tbody>\n");
248 db_prepare(&q, "SELECT pathname"
249 " FROM sfile ORDER BY pathname COLLATE nocase;");
250 rNow = db_double(0, "SELECT julianday('now')");
251 while( db_step(&q)==SQLITE_ROW ){
252 const char *zName = db_column_text(&q, 0);
@@ -231,21 +305,21 @@
305 if( x.rMTime==0.0 ){
306 /* This repository has no entry in the "event" table.
307 ** Its age will still be maximum, so data-sortkey will work. */
308 zAge = mprintf("unknown");
309 }
310 blob_appendf(&html, "<tr><td valign='top'><nobr>");
311 if( !file_ends_with_repository_extension(zName,0) ){
312 /* The "fossil server DIRECTORY" and "fossil ui DIRECTORY" commands
313 ** do not work for repositories whose names do not end in ".fossil".
314 ** So do not hyperlink those cases. */
315 blob_appendf(&html,"%h",zName);
316 } else if( sqlite3_strglob("*/.*", zName)==0 ){
317 /* Do not show hyperlinks for hidden repos */
318 blob_appendf(&html, "%h (hidden)", zName);
319 } else if( allRepo && sqlite3_strglob("[a-zA-Z]:/?*", zName)!=0 ){
320 blob_appendf(&html,
321 "<a href='%R/%T/home' target='_blank'>/%h</a>\n",
322 zUrl, zName);
323 }else if( file_ends_with_repository_extension(zName,1) ){
324 /* As described in
325 ** https://fossil-scm.org/forum/info/f50f647c97c72fc1: if
@@ -262,44 +336,60 @@
336 , zDirPart
337 #if USE_SEE
338 , zDirPart
339 #endif
340 ) ){
341 blob_appendf(&html,
342 "<s>%h</s> (directory/repo name collision)\n",
343 zName);
344 }else{
345 blob_appendf(&html,
346 "<a href='%R/%T/home' target='_blank'>%h</a>\n",
347 zUrl, zName);
348 }
349 fossil_free(zDirPart);
350 }else{
351 blob_appendf(&html,
352 "<a href='%R/%T/home' target='_blank'>%h</a>\n",
353 zUrl, zName);
354 }
355 blob_appendf(&html,"</nobr></td>\n");
356 if( x.zProjName ){
357 blob_appendf(&html, "<td>&emsp;</td><td valign='top'>%h</td>\n",
358 x.zProjName);
359 fossil_free(x.zProjName);
360 }else{
361 blob_appendf(&html, "<td>&emsp;</td><td></td>\n");
362 }
363 if( !bShowDesc ){
364 /* Do nothing */
365 }else if( x.zProjDesc ){
366 blob_appendf(&html, "<td>&emsp;</td><td valign='top'>%h</td>\n",
367 x.zProjDesc);
368 fossil_free(x.zProjDesc);
369 }else{
370 blob_appendf(&html, "<td>&emsp;</td><td></td>\n");
371 }
372 blob_appendf(&html,
373 "<td>&emsp;</td><td data-sortkey='%08x' align='center' valign='top'>"
374 "<nobr>%h</nobr></td>\n",
375 (int)iAge, zAge);
376 fossil_free(zAge);
377 if( !bShowLg ){
378 blob_appendf(&html, "</tr>\n");
379 }else if( x.zLoginGroup ){
380 blob_appendf(&html, "<td>&emsp;</td><td valign='top'>"
381 "<nobr>%h</nobr></td></tr>\n",
382 x.zLoginGroup);
383 fossil_free(x.zLoginGroup);
384 }else{
385 blob_appendf(&html, "<td>&emsp;</td><td></td></tr>\n");
386 }
387 sqlite3_free(zUrl);
388 }
389 db_finalize(&q);
390 blob_appendf(&html,"</tbody></table>\n");
391 }
392 if( zSkinRepo ){
393 char *zNewBase = mprintf("%s/%s", g.zBaseURL, zSkinUrl);
394 g.zBaseURL = 0;
395 set_base_url(zNewBase);
396
+113 -23
--- src/repolist.c
+++ src/repolist.c
@@ -31,10 +31,11 @@
3131
int isValid; /* True if zRepoName is a valid Fossil repository */
3232
int isRepolistSkin; /* 1 or 2 if this repository wants to be the skin
3333
** for the repository list. 2 means do use this
3434
** repository but do not display it in the list. */
3535
char *zProjName; /* Project Name. Memory from fossil_malloc() */
36
+ char *zProjDesc; /* Project Description. Memory from fossil_malloc() */
3637
char *zLoginGroup; /* Name of login group, or NULL. Malloced() */
3738
double rMTime; /* Last update. Julian day number */
3839
};
3940
#endif
4041
@@ -49,10 +50,11 @@
4950
int rc;
5051
5152
pRepo->isRepolistSkin = 0;
5253
pRepo->isValid = 0;
5354
pRepo->zProjName = 0;
55
+ pRepo->zProjDesc = 0;
5456
pRepo->zLoginGroup = 0;
5557
pRepo->rMTime = 0.0;
5658
5759
g.dbIgnoreErrors++;
5860
rc = sqlite3_open_v2(pRepo->zRepoName, &db, SQLITE_OPEN_READWRITE, 0);
@@ -71,10 +73,19 @@
7173
-1, &pStmt, 0);
7274
if( rc ) goto finish_repo_list;
7375
if( sqlite3_step(pStmt)==SQLITE_ROW ){
7476
pRepo->zProjName = fossil_strdup((char*)sqlite3_column_text(pStmt,0));
7577
}
78
+ sqlite3_finalize(pStmt);
79
+ if( rc ) goto finish_repo_list;
80
+ rc = sqlite3_prepare_v2(db, "SELECT value FROM config"
81
+ " WHERE name='project-description'",
82
+ -1, &pStmt, 0);
83
+ if( rc ) goto finish_repo_list;
84
+ if( sqlite3_step(pStmt)==SQLITE_ROW ){
85
+ pRepo->zProjDesc = fossil_strdup((char*)sqlite3_column_text(pStmt,0));
86
+ }
7687
sqlite3_finalize(pStmt);
7788
rc = sqlite3_prepare_v2(db, "SELECT value FROM config"
7889
" WHERE name='login-group-name'",
7990
-1, &pStmt, 0);
8091
if( rc==SQLITE_OK && sqlite3_step(pStmt)==SQLITE_ROW ){
@@ -89,10 +100,25 @@
89100
sqlite3_finalize(pStmt);
90101
finish_repo_list:
91102
g.dbIgnoreErrors--;
92103
sqlite3_close(db);
93104
}
105
+
106
+/*
107
+** SETTING: show-repolist-desc boolean default=off
108
+**
109
+** If the value of this setting is "1" globally, then the repository-list
110
+** page will show the description of each repository. This setting only
111
+** has effect when it is in the global setting database.
112
+*/
113
+/*
114
+** SETTING: show-repolist-lg boolean default=off
115
+**
116
+** If the value of this setting is "1" globally, then the repository-list
117
+** page will show the login-group for each repository. This setting only
118
+** has effect when it is in the global setting database.
119
+*/
94120
95121
/*
96122
** Generate a web-page that lists all repositories located under the
97123
** g.zRepositoryName directory and return non-zero.
98124
**
@@ -116,12 +142,27 @@
116142
** False if a directory scan of base for repos */
117143
Blob html; /* Html for the body of the repository list */
118144
char *zSkinRepo = 0; /* Name of the repository database used for skins */
119145
char *zSkinUrl = 0; /* URL for the skin database */
120146
int quickfilter = 0; /* Is quickfilter is enabled? */
147
+ const char *zShow; /* Value of FOSSIL_REPOLIST_SHOW environment variable */
148
+ int bShowDesc = 0; /* True to show the description column */
149
+ int bShowLg = 0; /* True to show the login-group column */
121150
122151
assert( g.db==0 );
152
+ zShow = P("FOSSIL_REPOLIST_SHOW");
153
+ if( zShow ){
154
+ bShowDesc = strstr(zShow,"description")!=0;
155
+ bShowLg = strstr(zShow,"login-group")!=0;
156
+ }else if( db_open_config(1, 1)
157
+ && db_table_exists("configdb", "global_config")
158
+ ){
159
+ bShowDesc = db_int(bShowDesc, "SELECT value FROM global_config"
160
+ " WHERE name='show-repolist-desc'");
161
+ bShowLg = db_int(bShowLg, "SELECT value FROM global_config"
162
+ " WHERE name='show-repolist-lg'");
163
+ }
123164
blob_init(&html, 0, 0);
124165
if( fossil_strcmp(g.zRepositoryName,"/")==0 && !g.fJail ){
125166
/* For the special case of the "repository directory" being "/",
126167
** show all of the repositories named in the ~/.fossil database.
127168
**
@@ -128,11 +169,10 @@
128169
** On unix systems, then entries are of the form "repo:/home/..."
129170
** and on Windows systems they are like on unix, starting with a "/"
130171
** or they can begin with a drive letter: "repo:C:/Users/...". In either
131172
** case, we want returned path to omit any initial "/".
132173
*/
133
- db_open_config(1, 0);
134174
db_multi_exec(
135175
"CREATE TEMP VIEW sfile AS"
136176
" SELECT ltrim(substr(name,6),'/') AS 'pathname' FROM global_config"
137177
" WHERE name GLOB 'repo:*'"
138178
);
@@ -140,10 +180,12 @@
140180
}else{
141181
/* The default case: All repositories under the g.zRepositoryName
142182
** directory.
143183
*/
144184
blob_init(&base, g.zRepositoryName, -1);
185
+ db_close(0);
186
+ assert( g.db==0 );
145187
sqlite3_open(":memory:", &g.db);
146188
db_multi_exec("CREATE TABLE sfile(pathname TEXT);");
147189
db_multi_exec("CREATE TABLE vfile(pathname);");
148190
vfile_scan(&base, blob_size(&base), 0, 0, 0, ExtFILE);
149191
db_multi_exec("DELETE FROM sfile WHERE pathname NOT GLOB '*[^/].fossil'"
@@ -161,18 +203,50 @@
161203
g.localOpen = 0;
162204
return 0;
163205
}else{
164206
Stmt q;
165207
double rNow;
166
- blob_append_sql(&html,
208
+ char zType[16]; /* Column type letters for class "sortable" */
209
+ int nType;
210
+ zType[0] = 't'; /* Repo name */
211
+ zType[1] = 'x'; /* Space between repo-name and project-name */
212
+ zType[2] = 't'; /* Project name */
213
+ nType = 3;
214
+ if( bShowDesc ){
215
+ zType[nType++] = 'x'; /* Space between name and description */
216
+ zType[nType++] = 't'; /* Project description */
217
+ }
218
+ zType[nType++] = 'x'; /* space before age */
219
+ zType[nType++] = 'k'; /* Project age */
220
+ if( bShowLg ){
221
+ zType[nType++] = 'x'; /* space before login-group */
222
+ zType[nType++] = 't'; /* Login Group */
223
+ }
224
+ zType[nType] = 0;
225
+ blob_appendf(&html,
167226
"<table border='0' class='sortable filterlist' data-init-sort='1'"
168
- " data-column-types='txtxkxt'><thead>\n"
169
- "<tr><th>Filename<th width='20'>"
170
- "<th>Project Name<th width='20'>"
171
- "<th>Last Modified<th width='20'>"
172
- "<th>Login Group</tr>\n"
173
- "</thead><tbody>\n");
227
+ " data-column-types='%s' cellspacing='0' cellpadding='0'><thead>\n"
228
+ "<tr><th>Filename</th><th>&emsp;</th>\n"
229
+ "<th%s><nobr>Project Name</nobr></th>\n",
230
+ zType, (bShowDesc ? " width='25%'" : ""));
231
+ if( bShowDesc ){
232
+ blob_appendf(&html,
233
+ "<th>&emsp;</th>\n"
234
+ "<th width='25%%'><nobr>Project Description</nobr></th>\n"
235
+ );
236
+ }
237
+ blob_appendf(&html,
238
+ "<th>&emsp;</th>"
239
+ "<th><nobr>Last Modified</nobr></th>\n"
240
+ );
241
+ if( bShowLg ){
242
+ blob_appendf(&html,
243
+ "<th>&emsp;</th>"
244
+ "<th><nobr>Login Group</nobr></th></tr>\n"
245
+ );
246
+ }
247
+ blob_appendf(&html,"</thead><tbody>\n");
174248
db_prepare(&q, "SELECT pathname"
175249
" FROM sfile ORDER BY pathname COLLATE nocase;");
176250
rNow = db_double(0, "SELECT julianday('now')");
177251
while( db_step(&q)==SQLITE_ROW ){
178252
const char *zName = db_column_text(&q, 0);
@@ -231,21 +305,21 @@
231305
if( x.rMTime==0.0 ){
232306
/* This repository has no entry in the "event" table.
233307
** Its age will still be maximum, so data-sortkey will work. */
234308
zAge = mprintf("unknown");
235309
}
236
- blob_append_sql(&html, "<tr><td valign='top'>");
310
+ blob_appendf(&html, "<tr><td valign='top'><nobr>");
237311
if( !file_ends_with_repository_extension(zName,0) ){
238312
/* The "fossil server DIRECTORY" and "fossil ui DIRECTORY" commands
239313
** do not work for repositories whose names do not end in ".fossil".
240314
** So do not hyperlink those cases. */
241
- blob_append_sql(&html,"%h",zName);
315
+ blob_appendf(&html,"%h",zName);
242316
} else if( sqlite3_strglob("*/.*", zName)==0 ){
243317
/* Do not show hyperlinks for hidden repos */
244
- blob_append_sql(&html, "%h (hidden)", zName);
318
+ blob_appendf(&html, "%h (hidden)", zName);
245319
} else if( allRepo && sqlite3_strglob("[a-zA-Z]:/?*", zName)!=0 ){
246
- blob_append_sql(&html,
320
+ blob_appendf(&html,
247321
"<a href='%R/%T/home' target='_blank'>/%h</a>\n",
248322
zUrl, zName);
249323
}else if( file_ends_with_repository_extension(zName,1) ){
250324
/* As described in
251325
** https://fossil-scm.org/forum/info/f50f647c97c72fc1: if
@@ -262,44 +336,60 @@
262336
, zDirPart
263337
#if USE_SEE
264338
, zDirPart
265339
#endif
266340
) ){
267
- blob_append_sql(&html,
341
+ blob_appendf(&html,
268342
"<s>%h</s> (directory/repo name collision)\n",
269343
zName);
270344
}else{
271
- blob_append_sql(&html,
345
+ blob_appendf(&html,
272346
"<a href='%R/%T/home' target='_blank'>%h</a>\n",
273347
zUrl, zName);
274348
}
275349
fossil_free(zDirPart);
276350
}else{
277
- blob_append_sql(&html,
351
+ blob_appendf(&html,
278352
"<a href='%R/%T/home' target='_blank'>%h</a>\n",
279353
zUrl, zName);
280354
}
355
+ blob_appendf(&html,"</nobr></td>\n");
281356
if( x.zProjName ){
282
- blob_append_sql(&html, "<td></td><td>%h</td>\n", x.zProjName);
357
+ blob_appendf(&html, "<td>&emsp;</td><td valign='top'>%h</td>\n",
358
+ x.zProjName);
283359
fossil_free(x.zProjName);
284360
}else{
285
- blob_append_sql(&html, "<td></td><td></td>\n");
361
+ blob_appendf(&html, "<td>&emsp;</td><td></td>\n");
362
+ }
363
+ if( !bShowDesc ){
364
+ /* Do nothing */
365
+ }else if( x.zProjDesc ){
366
+ blob_appendf(&html, "<td>&emsp;</td><td valign='top'>%h</td>\n",
367
+ x.zProjDesc);
368
+ fossil_free(x.zProjDesc);
369
+ }else{
370
+ blob_appendf(&html, "<td>&emsp;</td><td></td>\n");
286371
}
287
- blob_append_sql(&html,
288
- "<td></td><td data-sortkey='%08x'>%h</td>\n",
372
+ blob_appendf(&html,
373
+ "<td>&emsp;</td><td data-sortkey='%08x' align='center' valign='top'>"
374
+ "<nobr>%h</nobr></td>\n",
289375
(int)iAge, zAge);
290376
fossil_free(zAge);
291
- if( x.zLoginGroup ){
292
- blob_append_sql(&html, "<td></td><td>%h</td></tr>\n", x.zLoginGroup);
377
+ if( !bShowLg ){
378
+ blob_appendf(&html, "</tr>\n");
379
+ }else if( x.zLoginGroup ){
380
+ blob_appendf(&html, "<td>&emsp;</td><td valign='top'>"
381
+ "<nobr>%h</nobr></td></tr>\n",
382
+ x.zLoginGroup);
293383
fossil_free(x.zLoginGroup);
294384
}else{
295
- blob_append_sql(&html, "<td></td><td></td></tr>\n");
385
+ blob_appendf(&html, "<td>&emsp;</td><td></td></tr>\n");
296386
}
297387
sqlite3_free(zUrl);
298388
}
299389
db_finalize(&q);
300
- blob_append_sql(&html,"</tbody></table>\n");
390
+ blob_appendf(&html,"</tbody></table>\n");
301391
}
302392
if( zSkinRepo ){
303393
char *zNewBase = mprintf("%s/%s", g.zBaseURL, zSkinUrl);
304394
g.zBaseURL = 0;
305395
set_base_url(zNewBase);
306396
--- src/repolist.c
+++ src/repolist.c
@@ -31,10 +31,11 @@
31 int isValid; /* True if zRepoName is a valid Fossil repository */
32 int isRepolistSkin; /* 1 or 2 if this repository wants to be the skin
33 ** for the repository list. 2 means do use this
34 ** repository but do not display it in the list. */
35 char *zProjName; /* Project Name. Memory from fossil_malloc() */
 
36 char *zLoginGroup; /* Name of login group, or NULL. Malloced() */
37 double rMTime; /* Last update. Julian day number */
38 };
39 #endif
40
@@ -49,10 +50,11 @@
49 int rc;
50
51 pRepo->isRepolistSkin = 0;
52 pRepo->isValid = 0;
53 pRepo->zProjName = 0;
 
54 pRepo->zLoginGroup = 0;
55 pRepo->rMTime = 0.0;
56
57 g.dbIgnoreErrors++;
58 rc = sqlite3_open_v2(pRepo->zRepoName, &db, SQLITE_OPEN_READWRITE, 0);
@@ -71,10 +73,19 @@
71 -1, &pStmt, 0);
72 if( rc ) goto finish_repo_list;
73 if( sqlite3_step(pStmt)==SQLITE_ROW ){
74 pRepo->zProjName = fossil_strdup((char*)sqlite3_column_text(pStmt,0));
75 }
 
 
 
 
 
 
 
 
 
76 sqlite3_finalize(pStmt);
77 rc = sqlite3_prepare_v2(db, "SELECT value FROM config"
78 " WHERE name='login-group-name'",
79 -1, &pStmt, 0);
80 if( rc==SQLITE_OK && sqlite3_step(pStmt)==SQLITE_ROW ){
@@ -89,10 +100,25 @@
89 sqlite3_finalize(pStmt);
90 finish_repo_list:
91 g.dbIgnoreErrors--;
92 sqlite3_close(db);
93 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
95 /*
96 ** Generate a web-page that lists all repositories located under the
97 ** g.zRepositoryName directory and return non-zero.
98 **
@@ -116,12 +142,27 @@
116 ** False if a directory scan of base for repos */
117 Blob html; /* Html for the body of the repository list */
118 char *zSkinRepo = 0; /* Name of the repository database used for skins */
119 char *zSkinUrl = 0; /* URL for the skin database */
120 int quickfilter = 0; /* Is quickfilter is enabled? */
 
 
 
121
122 assert( g.db==0 );
 
 
 
 
 
 
 
 
 
 
 
 
123 blob_init(&html, 0, 0);
124 if( fossil_strcmp(g.zRepositoryName,"/")==0 && !g.fJail ){
125 /* For the special case of the "repository directory" being "/",
126 ** show all of the repositories named in the ~/.fossil database.
127 **
@@ -128,11 +169,10 @@
128 ** On unix systems, then entries are of the form "repo:/home/..."
129 ** and on Windows systems they are like on unix, starting with a "/"
130 ** or they can begin with a drive letter: "repo:C:/Users/...". In either
131 ** case, we want returned path to omit any initial "/".
132 */
133 db_open_config(1, 0);
134 db_multi_exec(
135 "CREATE TEMP VIEW sfile AS"
136 " SELECT ltrim(substr(name,6),'/') AS 'pathname' FROM global_config"
137 " WHERE name GLOB 'repo:*'"
138 );
@@ -140,10 +180,12 @@
140 }else{
141 /* The default case: All repositories under the g.zRepositoryName
142 ** directory.
143 */
144 blob_init(&base, g.zRepositoryName, -1);
 
 
145 sqlite3_open(":memory:", &g.db);
146 db_multi_exec("CREATE TABLE sfile(pathname TEXT);");
147 db_multi_exec("CREATE TABLE vfile(pathname);");
148 vfile_scan(&base, blob_size(&base), 0, 0, 0, ExtFILE);
149 db_multi_exec("DELETE FROM sfile WHERE pathname NOT GLOB '*[^/].fossil'"
@@ -161,18 +203,50 @@
161 g.localOpen = 0;
162 return 0;
163 }else{
164 Stmt q;
165 double rNow;
166 blob_append_sql(&html,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
167 "<table border='0' class='sortable filterlist' data-init-sort='1'"
168 " data-column-types='txtxkxt'><thead>\n"
169 "<tr><th>Filename<th width='20'>"
170 "<th>Project Name<th width='20'>"
171 "<th>Last Modified<th width='20'>"
172 "<th>Login Group</tr>\n"
173 "</thead><tbody>\n");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174 db_prepare(&q, "SELECT pathname"
175 " FROM sfile ORDER BY pathname COLLATE nocase;");
176 rNow = db_double(0, "SELECT julianday('now')");
177 while( db_step(&q)==SQLITE_ROW ){
178 const char *zName = db_column_text(&q, 0);
@@ -231,21 +305,21 @@
231 if( x.rMTime==0.0 ){
232 /* This repository has no entry in the "event" table.
233 ** Its age will still be maximum, so data-sortkey will work. */
234 zAge = mprintf("unknown");
235 }
236 blob_append_sql(&html, "<tr><td valign='top'>");
237 if( !file_ends_with_repository_extension(zName,0) ){
238 /* The "fossil server DIRECTORY" and "fossil ui DIRECTORY" commands
239 ** do not work for repositories whose names do not end in ".fossil".
240 ** So do not hyperlink those cases. */
241 blob_append_sql(&html,"%h",zName);
242 } else if( sqlite3_strglob("*/.*", zName)==0 ){
243 /* Do not show hyperlinks for hidden repos */
244 blob_append_sql(&html, "%h (hidden)", zName);
245 } else if( allRepo && sqlite3_strglob("[a-zA-Z]:/?*", zName)!=0 ){
246 blob_append_sql(&html,
247 "<a href='%R/%T/home' target='_blank'>/%h</a>\n",
248 zUrl, zName);
249 }else if( file_ends_with_repository_extension(zName,1) ){
250 /* As described in
251 ** https://fossil-scm.org/forum/info/f50f647c97c72fc1: if
@@ -262,44 +336,60 @@
262 , zDirPart
263 #if USE_SEE
264 , zDirPart
265 #endif
266 ) ){
267 blob_append_sql(&html,
268 "<s>%h</s> (directory/repo name collision)\n",
269 zName);
270 }else{
271 blob_append_sql(&html,
272 "<a href='%R/%T/home' target='_blank'>%h</a>\n",
273 zUrl, zName);
274 }
275 fossil_free(zDirPart);
276 }else{
277 blob_append_sql(&html,
278 "<a href='%R/%T/home' target='_blank'>%h</a>\n",
279 zUrl, zName);
280 }
 
281 if( x.zProjName ){
282 blob_append_sql(&html, "<td></td><td>%h</td>\n", x.zProjName);
 
283 fossil_free(x.zProjName);
284 }else{
285 blob_append_sql(&html, "<td></td><td></td>\n");
 
 
 
 
 
 
 
 
 
286 }
287 blob_append_sql(&html,
288 "<td></td><td data-sortkey='%08x'>%h</td>\n",
 
289 (int)iAge, zAge);
290 fossil_free(zAge);
291 if( x.zLoginGroup ){
292 blob_append_sql(&html, "<td></td><td>%h</td></tr>\n", x.zLoginGroup);
 
 
 
 
293 fossil_free(x.zLoginGroup);
294 }else{
295 blob_append_sql(&html, "<td></td><td></td></tr>\n");
296 }
297 sqlite3_free(zUrl);
298 }
299 db_finalize(&q);
300 blob_append_sql(&html,"</tbody></table>\n");
301 }
302 if( zSkinRepo ){
303 char *zNewBase = mprintf("%s/%s", g.zBaseURL, zSkinUrl);
304 g.zBaseURL = 0;
305 set_base_url(zNewBase);
306
--- src/repolist.c
+++ src/repolist.c
@@ -31,10 +31,11 @@
31 int isValid; /* True if zRepoName is a valid Fossil repository */
32 int isRepolistSkin; /* 1 or 2 if this repository wants to be the skin
33 ** for the repository list. 2 means do use this
34 ** repository but do not display it in the list. */
35 char *zProjName; /* Project Name. Memory from fossil_malloc() */
36 char *zProjDesc; /* Project Description. Memory from fossil_malloc() */
37 char *zLoginGroup; /* Name of login group, or NULL. Malloced() */
38 double rMTime; /* Last update. Julian day number */
39 };
40 #endif
41
@@ -49,10 +50,11 @@
50 int rc;
51
52 pRepo->isRepolistSkin = 0;
53 pRepo->isValid = 0;
54 pRepo->zProjName = 0;
55 pRepo->zProjDesc = 0;
56 pRepo->zLoginGroup = 0;
57 pRepo->rMTime = 0.0;
58
59 g.dbIgnoreErrors++;
60 rc = sqlite3_open_v2(pRepo->zRepoName, &db, SQLITE_OPEN_READWRITE, 0);
@@ -71,10 +73,19 @@
73 -1, &pStmt, 0);
74 if( rc ) goto finish_repo_list;
75 if( sqlite3_step(pStmt)==SQLITE_ROW ){
76 pRepo->zProjName = fossil_strdup((char*)sqlite3_column_text(pStmt,0));
77 }
78 sqlite3_finalize(pStmt);
79 if( rc ) goto finish_repo_list;
80 rc = sqlite3_prepare_v2(db, "SELECT value FROM config"
81 " WHERE name='project-description'",
82 -1, &pStmt, 0);
83 if( rc ) goto finish_repo_list;
84 if( sqlite3_step(pStmt)==SQLITE_ROW ){
85 pRepo->zProjDesc = fossil_strdup((char*)sqlite3_column_text(pStmt,0));
86 }
87 sqlite3_finalize(pStmt);
88 rc = sqlite3_prepare_v2(db, "SELECT value FROM config"
89 " WHERE name='login-group-name'",
90 -1, &pStmt, 0);
91 if( rc==SQLITE_OK && sqlite3_step(pStmt)==SQLITE_ROW ){
@@ -89,10 +100,25 @@
100 sqlite3_finalize(pStmt);
101 finish_repo_list:
102 g.dbIgnoreErrors--;
103 sqlite3_close(db);
104 }
105
106 /*
107 ** SETTING: show-repolist-desc boolean default=off
108 **
109 ** If the value of this setting is "1" globally, then the repository-list
110 ** page will show the description of each repository. This setting only
111 ** has effect when it is in the global setting database.
112 */
113 /*
114 ** SETTING: show-repolist-lg boolean default=off
115 **
116 ** If the value of this setting is "1" globally, then the repository-list
117 ** page will show the login-group for each repository. This setting only
118 ** has effect when it is in the global setting database.
119 */
120
121 /*
122 ** Generate a web-page that lists all repositories located under the
123 ** g.zRepositoryName directory and return non-zero.
124 **
@@ -116,12 +142,27 @@
142 ** False if a directory scan of base for repos */
143 Blob html; /* Html for the body of the repository list */
144 char *zSkinRepo = 0; /* Name of the repository database used for skins */
145 char *zSkinUrl = 0; /* URL for the skin database */
146 int quickfilter = 0; /* Is quickfilter is enabled? */
147 const char *zShow; /* Value of FOSSIL_REPOLIST_SHOW environment variable */
148 int bShowDesc = 0; /* True to show the description column */
149 int bShowLg = 0; /* True to show the login-group column */
150
151 assert( g.db==0 );
152 zShow = P("FOSSIL_REPOLIST_SHOW");
153 if( zShow ){
154 bShowDesc = strstr(zShow,"description")!=0;
155 bShowLg = strstr(zShow,"login-group")!=0;
156 }else if( db_open_config(1, 1)
157 && db_table_exists("configdb", "global_config")
158 ){
159 bShowDesc = db_int(bShowDesc, "SELECT value FROM global_config"
160 " WHERE name='show-repolist-desc'");
161 bShowLg = db_int(bShowLg, "SELECT value FROM global_config"
162 " WHERE name='show-repolist-lg'");
163 }
164 blob_init(&html, 0, 0);
165 if( fossil_strcmp(g.zRepositoryName,"/")==0 && !g.fJail ){
166 /* For the special case of the "repository directory" being "/",
167 ** show all of the repositories named in the ~/.fossil database.
168 **
@@ -128,11 +169,10 @@
169 ** On unix systems, then entries are of the form "repo:/home/..."
170 ** and on Windows systems they are like on unix, starting with a "/"
171 ** or they can begin with a drive letter: "repo:C:/Users/...". In either
172 ** case, we want returned path to omit any initial "/".
173 */
 
174 db_multi_exec(
175 "CREATE TEMP VIEW sfile AS"
176 " SELECT ltrim(substr(name,6),'/') AS 'pathname' FROM global_config"
177 " WHERE name GLOB 'repo:*'"
178 );
@@ -140,10 +180,12 @@
180 }else{
181 /* The default case: All repositories under the g.zRepositoryName
182 ** directory.
183 */
184 blob_init(&base, g.zRepositoryName, -1);
185 db_close(0);
186 assert( g.db==0 );
187 sqlite3_open(":memory:", &g.db);
188 db_multi_exec("CREATE TABLE sfile(pathname TEXT);");
189 db_multi_exec("CREATE TABLE vfile(pathname);");
190 vfile_scan(&base, blob_size(&base), 0, 0, 0, ExtFILE);
191 db_multi_exec("DELETE FROM sfile WHERE pathname NOT GLOB '*[^/].fossil'"
@@ -161,18 +203,50 @@
203 g.localOpen = 0;
204 return 0;
205 }else{
206 Stmt q;
207 double rNow;
208 char zType[16]; /* Column type letters for class "sortable" */
209 int nType;
210 zType[0] = 't'; /* Repo name */
211 zType[1] = 'x'; /* Space between repo-name and project-name */
212 zType[2] = 't'; /* Project name */
213 nType = 3;
214 if( bShowDesc ){
215 zType[nType++] = 'x'; /* Space between name and description */
216 zType[nType++] = 't'; /* Project description */
217 }
218 zType[nType++] = 'x'; /* space before age */
219 zType[nType++] = 'k'; /* Project age */
220 if( bShowLg ){
221 zType[nType++] = 'x'; /* space before login-group */
222 zType[nType++] = 't'; /* Login Group */
223 }
224 zType[nType] = 0;
225 blob_appendf(&html,
226 "<table border='0' class='sortable filterlist' data-init-sort='1'"
227 " data-column-types='%s' cellspacing='0' cellpadding='0'><thead>\n"
228 "<tr><th>Filename</th><th>&emsp;</th>\n"
229 "<th%s><nobr>Project Name</nobr></th>\n",
230 zType, (bShowDesc ? " width='25%'" : ""));
231 if( bShowDesc ){
232 blob_appendf(&html,
233 "<th>&emsp;</th>\n"
234 "<th width='25%%'><nobr>Project Description</nobr></th>\n"
235 );
236 }
237 blob_appendf(&html,
238 "<th>&emsp;</th>"
239 "<th><nobr>Last Modified</nobr></th>\n"
240 );
241 if( bShowLg ){
242 blob_appendf(&html,
243 "<th>&emsp;</th>"
244 "<th><nobr>Login Group</nobr></th></tr>\n"
245 );
246 }
247 blob_appendf(&html,"</thead><tbody>\n");
248 db_prepare(&q, "SELECT pathname"
249 " FROM sfile ORDER BY pathname COLLATE nocase;");
250 rNow = db_double(0, "SELECT julianday('now')");
251 while( db_step(&q)==SQLITE_ROW ){
252 const char *zName = db_column_text(&q, 0);
@@ -231,21 +305,21 @@
305 if( x.rMTime==0.0 ){
306 /* This repository has no entry in the "event" table.
307 ** Its age will still be maximum, so data-sortkey will work. */
308 zAge = mprintf("unknown");
309 }
310 blob_appendf(&html, "<tr><td valign='top'><nobr>");
311 if( !file_ends_with_repository_extension(zName,0) ){
312 /* The "fossil server DIRECTORY" and "fossil ui DIRECTORY" commands
313 ** do not work for repositories whose names do not end in ".fossil".
314 ** So do not hyperlink those cases. */
315 blob_appendf(&html,"%h",zName);
316 } else if( sqlite3_strglob("*/.*", zName)==0 ){
317 /* Do not show hyperlinks for hidden repos */
318 blob_appendf(&html, "%h (hidden)", zName);
319 } else if( allRepo && sqlite3_strglob("[a-zA-Z]:/?*", zName)!=0 ){
320 blob_appendf(&html,
321 "<a href='%R/%T/home' target='_blank'>/%h</a>\n",
322 zUrl, zName);
323 }else if( file_ends_with_repository_extension(zName,1) ){
324 /* As described in
325 ** https://fossil-scm.org/forum/info/f50f647c97c72fc1: if
@@ -262,44 +336,60 @@
336 , zDirPart
337 #if USE_SEE
338 , zDirPart
339 #endif
340 ) ){
341 blob_appendf(&html,
342 "<s>%h</s> (directory/repo name collision)\n",
343 zName);
344 }else{
345 blob_appendf(&html,
346 "<a href='%R/%T/home' target='_blank'>%h</a>\n",
347 zUrl, zName);
348 }
349 fossil_free(zDirPart);
350 }else{
351 blob_appendf(&html,
352 "<a href='%R/%T/home' target='_blank'>%h</a>\n",
353 zUrl, zName);
354 }
355 blob_appendf(&html,"</nobr></td>\n");
356 if( x.zProjName ){
357 blob_appendf(&html, "<td>&emsp;</td><td valign='top'>%h</td>\n",
358 x.zProjName);
359 fossil_free(x.zProjName);
360 }else{
361 blob_appendf(&html, "<td>&emsp;</td><td></td>\n");
362 }
363 if( !bShowDesc ){
364 /* Do nothing */
365 }else if( x.zProjDesc ){
366 blob_appendf(&html, "<td>&emsp;</td><td valign='top'>%h</td>\n",
367 x.zProjDesc);
368 fossil_free(x.zProjDesc);
369 }else{
370 blob_appendf(&html, "<td>&emsp;</td><td></td>\n");
371 }
372 blob_appendf(&html,
373 "<td>&emsp;</td><td data-sortkey='%08x' align='center' valign='top'>"
374 "<nobr>%h</nobr></td>\n",
375 (int)iAge, zAge);
376 fossil_free(zAge);
377 if( !bShowLg ){
378 blob_appendf(&html, "</tr>\n");
379 }else if( x.zLoginGroup ){
380 blob_appendf(&html, "<td>&emsp;</td><td valign='top'>"
381 "<nobr>%h</nobr></td></tr>\n",
382 x.zLoginGroup);
383 fossil_free(x.zLoginGroup);
384 }else{
385 blob_appendf(&html, "<td>&emsp;</td><td></td></tr>\n");
386 }
387 sqlite3_free(zUrl);
388 }
389 db_finalize(&q);
390 blob_appendf(&html,"</tbody></table>\n");
391 }
392 if( zSkinRepo ){
393 char *zNewBase = mprintf("%s/%s", g.zBaseURL, zSkinUrl);
394 g.zBaseURL = 0;
395 set_base_url(zNewBase);
396
+2 -4
--- src/report.c
+++ src/report.c
@@ -588,13 +588,10 @@
588588
zOwner = g.zLogin;
589589
}
590590
}
591591
if( zOwner==0 ) zOwner = g.zLogin;
592592
style_submenu_element("Cancel", "%R/reportlist");
593
- if( rn>0 ){
594
- style_submenu_element("Delete", "%R/rptedit/%d?del1=1", rn);
595
- }
596593
style_header("%s", rn>0 ? "Edit Report Format":"Create New Report Format");
597594
if( zErr ){
598595
@ <blockquote class="reportError">%h(zErr)</blockquote>
599596
}
600597
@ <form action="rptedit" method="post"><div>
@@ -906,11 +903,12 @@
906903
907904
/* Output the separator above each entry in a table which has multiple lines
908905
** per database entry.
909906
*/
910907
if( pState->iNewRow>=0 ){
911
- @ <tr><td colspan=%d(pState->nCol)><font size=1>&nbsp;</font></td></tr>
908
+ @ <tr><td colspan="%d(pState->nCol)" style="padding:0px">
909
+ @ <hr style="margin:0px"></td></tr>
912910
}
913911
914912
/* Output the data for this entry from the database
915913
*/
916914
zBg = pState->iBg>=0 ? azArg[pState->iBg] : 0;
917915
--- src/report.c
+++ src/report.c
@@ -588,13 +588,10 @@
588 zOwner = g.zLogin;
589 }
590 }
591 if( zOwner==0 ) zOwner = g.zLogin;
592 style_submenu_element("Cancel", "%R/reportlist");
593 if( rn>0 ){
594 style_submenu_element("Delete", "%R/rptedit/%d?del1=1", rn);
595 }
596 style_header("%s", rn>0 ? "Edit Report Format":"Create New Report Format");
597 if( zErr ){
598 @ <blockquote class="reportError">%h(zErr)</blockquote>
599 }
600 @ <form action="rptedit" method="post"><div>
@@ -906,11 +903,12 @@
906
907 /* Output the separator above each entry in a table which has multiple lines
908 ** per database entry.
909 */
910 if( pState->iNewRow>=0 ){
911 @ <tr><td colspan=%d(pState->nCol)><font size=1>&nbsp;</font></td></tr>
 
912 }
913
914 /* Output the data for this entry from the database
915 */
916 zBg = pState->iBg>=0 ? azArg[pState->iBg] : 0;
917
--- src/report.c
+++ src/report.c
@@ -588,13 +588,10 @@
588 zOwner = g.zLogin;
589 }
590 }
591 if( zOwner==0 ) zOwner = g.zLogin;
592 style_submenu_element("Cancel", "%R/reportlist");
 
 
 
593 style_header("%s", rn>0 ? "Edit Report Format":"Create New Report Format");
594 if( zErr ){
595 @ <blockquote class="reportError">%h(zErr)</blockquote>
596 }
597 @ <form action="rptedit" method="post"><div>
@@ -906,11 +903,12 @@
903
904 /* Output the separator above each entry in a table which has multiple lines
905 ** per database entry.
906 */
907 if( pState->iNewRow>=0 ){
908 @ <tr><td colspan="%d(pState->nCol)" style="padding:0px">
909 @ <hr style="margin:0px"></td></tr>
910 }
911
912 /* Output the data for this entry from the database
913 */
914 zBg = pState->iBg>=0 ? azArg[pState->iBg] : 0;
915
+2 -4
--- src/report.c
+++ src/report.c
@@ -588,13 +588,10 @@
588588
zOwner = g.zLogin;
589589
}
590590
}
591591
if( zOwner==0 ) zOwner = g.zLogin;
592592
style_submenu_element("Cancel", "%R/reportlist");
593
- if( rn>0 ){
594
- style_submenu_element("Delete", "%R/rptedit/%d?del1=1", rn);
595
- }
596593
style_header("%s", rn>0 ? "Edit Report Format":"Create New Report Format");
597594
if( zErr ){
598595
@ <blockquote class="reportError">%h(zErr)</blockquote>
599596
}
600597
@ <form action="rptedit" method="post"><div>
@@ -906,11 +903,12 @@
906903
907904
/* Output the separator above each entry in a table which has multiple lines
908905
** per database entry.
909906
*/
910907
if( pState->iNewRow>=0 ){
911
- @ <tr><td colspan=%d(pState->nCol)><font size=1>&nbsp;</font></td></tr>
908
+ @ <tr><td colspan="%d(pState->nCol)" style="padding:0px">
909
+ @ <hr style="margin:0px"></td></tr>
912910
}
913911
914912
/* Output the data for this entry from the database
915913
*/
916914
zBg = pState->iBg>=0 ? azArg[pState->iBg] : 0;
917915
--- src/report.c
+++ src/report.c
@@ -588,13 +588,10 @@
588 zOwner = g.zLogin;
589 }
590 }
591 if( zOwner==0 ) zOwner = g.zLogin;
592 style_submenu_element("Cancel", "%R/reportlist");
593 if( rn>0 ){
594 style_submenu_element("Delete", "%R/rptedit/%d?del1=1", rn);
595 }
596 style_header("%s", rn>0 ? "Edit Report Format":"Create New Report Format");
597 if( zErr ){
598 @ <blockquote class="reportError">%h(zErr)</blockquote>
599 }
600 @ <form action="rptedit" method="post"><div>
@@ -906,11 +903,12 @@
906
907 /* Output the separator above each entry in a table which has multiple lines
908 ** per database entry.
909 */
910 if( pState->iNewRow>=0 ){
911 @ <tr><td colspan=%d(pState->nCol)><font size=1>&nbsp;</font></td></tr>
 
912 }
913
914 /* Output the data for this entry from the database
915 */
916 zBg = pState->iBg>=0 ? azArg[pState->iBg] : 0;
917
--- src/report.c
+++ src/report.c
@@ -588,13 +588,10 @@
588 zOwner = g.zLogin;
589 }
590 }
591 if( zOwner==0 ) zOwner = g.zLogin;
592 style_submenu_element("Cancel", "%R/reportlist");
 
 
 
593 style_header("%s", rn>0 ? "Edit Report Format":"Create New Report Format");
594 if( zErr ){
595 @ <blockquote class="reportError">%h(zErr)</blockquote>
596 }
597 @ <form action="rptedit" method="post"><div>
@@ -906,11 +903,12 @@
903
904 /* Output the separator above each entry in a table which has multiple lines
905 ** per database entry.
906 */
907 if( pState->iNewRow>=0 ){
908 @ <tr><td colspan="%d(pState->nCol)" style="padding:0px">
909 @ <hr style="margin:0px"></td></tr>
910 }
911
912 /* Output the data for this entry from the database
913 */
914 zBg = pState->iBg>=0 ? azArg[pState->iBg] : 0;
915
+3 -2
--- src/search.c
+++ src/search.c
@@ -618,10 +618,11 @@
618618
int bDebug = find_option("debug",0,0)!=0; /* Undocumented */
619619
int nLimit = zLimit ? atoi(zLimit) : -1000;
620620
int width;
621621
int nTty = 0; /* VT100 highlight color for matching text */
622622
const char *zHighlight = 0;
623
+ int bFlags = 0; /* DB open flags */
623624
624625
nTty = terminal_is_vt100();
625626
626627
/* Undocumented option to change highlight color */
627628
zHighlight = find_option("highlight",0,1);
@@ -666,12 +667,12 @@
666667
if( find_option("wiki",0,0) ){ srchFlags |= SRCH_WIKI; bFts = 1; }
667668
668669
/* If no search objects are specified, default to "check-in comments" */
669670
if( srchFlags==0 ) srchFlags = SRCH_CKIN;
670671
671
-
672
- db_find_and_open_repository(0, 0);
672
+ if( srchFlags==SRCH_HELP ) bFlags = OPEN_OK_NOT_FOUND|OPEN_SUBSTITUTE;
673
+ db_find_and_open_repository(bFlags, 0);
673674
verify_all_options();
674675
if( g.argc<3 ) return;
675676
login_set_capabilities("s", 0);
676677
if( search_restrict(srchFlags)==0 && (srchFlags & SRCH_HELP)==0 ){
677678
const char *zC1 = 0, *zPlural = "s";
678679
--- src/search.c
+++ src/search.c
@@ -618,10 +618,11 @@
618 int bDebug = find_option("debug",0,0)!=0; /* Undocumented */
619 int nLimit = zLimit ? atoi(zLimit) : -1000;
620 int width;
621 int nTty = 0; /* VT100 highlight color for matching text */
622 const char *zHighlight = 0;
 
623
624 nTty = terminal_is_vt100();
625
626 /* Undocumented option to change highlight color */
627 zHighlight = find_option("highlight",0,1);
@@ -666,12 +667,12 @@
666 if( find_option("wiki",0,0) ){ srchFlags |= SRCH_WIKI; bFts = 1; }
667
668 /* If no search objects are specified, default to "check-in comments" */
669 if( srchFlags==0 ) srchFlags = SRCH_CKIN;
670
671
672 db_find_and_open_repository(0, 0);
673 verify_all_options();
674 if( g.argc<3 ) return;
675 login_set_capabilities("s", 0);
676 if( search_restrict(srchFlags)==0 && (srchFlags & SRCH_HELP)==0 ){
677 const char *zC1 = 0, *zPlural = "s";
678
--- src/search.c
+++ src/search.c
@@ -618,10 +618,11 @@
618 int bDebug = find_option("debug",0,0)!=0; /* Undocumented */
619 int nLimit = zLimit ? atoi(zLimit) : -1000;
620 int width;
621 int nTty = 0; /* VT100 highlight color for matching text */
622 const char *zHighlight = 0;
623 int bFlags = 0; /* DB open flags */
624
625 nTty = terminal_is_vt100();
626
627 /* Undocumented option to change highlight color */
628 zHighlight = find_option("highlight",0,1);
@@ -666,12 +667,12 @@
667 if( find_option("wiki",0,0) ){ srchFlags |= SRCH_WIKI; bFts = 1; }
668
669 /* If no search objects are specified, default to "check-in comments" */
670 if( srchFlags==0 ) srchFlags = SRCH_CKIN;
671
672 if( srchFlags==SRCH_HELP ) bFlags = OPEN_OK_NOT_FOUND|OPEN_SUBSTITUTE;
673 db_find_and_open_repository(bFlags, 0);
674 verify_all_options();
675 if( g.argc<3 ) return;
676 login_set_capabilities("s", 0);
677 if( search_restrict(srchFlags)==0 && (srchFlags & SRCH_HELP)==0 ){
678 const char *zC1 = 0, *zPlural = "s";
679
--- src/security_audit.c
+++ src/security_audit.c
@@ -100,10 +100,11 @@
100100
const char *zReadCap; /* Capabilities of user group "reader" */
101101
const char *zPubPages; /* GLOB pattern for public pages */
102102
const char *zSelfCap; /* Capabilities of self-registered users */
103103
int hasSelfReg = 0; /* True if able to self-register */
104104
const char *zPublicUrl; /* Canonical access URL */
105
+ const char *zVulnReport; /* The vuln-report setting */
105106
Blob cmd;
106107
char *z;
107108
int n, i;
108109
CapabilityString *pCap;
109110
char **azCSP; /* Parsed content security policy */
@@ -362,10 +363,22 @@
362363
@ <li><p><b>WARNING:</b>
363364
@ The "strict-manifest-syntax" flag is off. This is a security
364365
@ risk. Turn this setting on (its default) to protect the users
365366
@ of this repository.
366367
}
368
+
369
+ zVulnReport = db_get("vuln-report","log");
370
+ if( fossil_strcmp(zVulnReport,"block")!=0
371
+ && fossil_strcmp(zVulnReport,"fatal")!=0
372
+ ){
373
+ @ <li><p><b>WARNING:</b>
374
+ @ The <a href="%R/help?cmd=vuln-report">vuln-report setting</a>
375
+ @ has a value of "%h(zVulnReport)". This disables defenses against
376
+ @ XSS or SQL-injection vulnerabilities caused by coding errors in
377
+ @ custom TH1 scripts. For the best security, change
378
+ @ the value of the vuln-report setting to "block" or "fatal".
379
+ }
367380
368381
/* Obsolete: */
369382
if( hasAnyCap(zAnonCap, "d") ||
370383
hasAnyCap(zDevCap, "d") ||
371384
hasAnyCap(zReadCap, "d") ){
@@ -810,33 +823,39 @@
810823
** WEBPAGE: errorlog
811824
**
812825
** Show the content of the error log. Only the administrator can view
813826
** this page.
814827
**
815
-** y=0x01 Show only hack attempts
816
-** y=0x02 Show only panics and assertion faults
817
-** y=0x04 Show hung backoffice processes
818
-** y=0x08 Show POST requests from a different origin
819
-** y=0x40 Show other uncategorized messages
828
+** y=0x001 Show only hack attempts
829
+** y=0x002 Show only panics and assertion faults
830
+** y=0x004 Show hung backoffice processes
831
+** y=0x008 Show POST requests from a different origin
832
+** y=0x010 Show SQLITE_AUTH and similar
833
+** y=0x020 Show SMTP error reports
834
+** y=0x040 Show TH1 vulnerability reports
835
+** y=0x800 Show other uncategorized messages
820836
**
821837
** If y is omitted or is zero, a count of the various message types is
822838
** shown.
823839
*/
824840
void errorlog_page(void){
825841
i64 szFile;
826842
FILE *in;
827843
char *zLog;
828844
const char *zType = P("y");
829
- static const int eAllTypes = 0x4f;
845
+ static const int eAllTypes = 0x87f;
830846
long eType = 0;
831847
int bOutput = 0;
832848
int prevWasTime = 0;
833849
int nHack = 0;
834850
int nPanic = 0;
835851
int nOther = 0;
836852
int nHang = 0;
837853
int nXPost = 0;
854
+ int nAuth = 0;
855
+ int nSmtp = 0;
856
+ int nVuln = 0;
838857
char z[10000];
839858
char zTime[10000];
840859
841860
login_check_credentials();
842861
if( !g.perm.Admin ){
@@ -906,11 +925,20 @@
906925
@ <li>Hung backoffice processes
907926
}
908927
if( eType & 0x08 ){
909928
@ <li>POST requests from different origin
910929
}
930
+ if( eType & 0x10 ){
931
+ @ <li>SQLITE_AUTH and similar errors
932
+ }
933
+ if( eType & 0x20 ){
934
+ @ <li>SMTP malfunctions
935
+ }
911936
if( eType & 0x40 ){
937
+ @ <li>TH1 vulnerabilities
938
+ }
939
+ if( eType & 0x800 ){
912940
@ <li>Other uncategorized messages
913941
}
914942
@ </ul>
915943
}
916944
@ <hr>
@@ -924,21 +952,35 @@
924952
nHack++;
925953
}else
926954
if( (strncmp(z,"panic: ", 7)==0 || strstr(z," assertion fault ")!=0) ){
927955
bOutput = (eType & 0x02)!=0;
928956
nPanic++;
957
+ }else
958
+ if( strncmp(z,"SMTP:", 5)==0 ){
959
+ bOutput = (eType & 0x20)!=0;
960
+ nSmtp++;
929961
}else
930962
if( sqlite3_strglob("warning: backoffice process * still *",z)==0 ){
931963
bOutput = (eType & 0x04)!=0;
932964
nHang++;
933965
}else
934966
if( sqlite3_strglob("warning: POST from different origin*",z)==0 ){
935967
bOutput = (eType & 0x08)!=0;
936968
nXPost++;
937969
}else
938
- {
970
+ if( sqlite3_strglob("SECURITY: authorizer blocks*",z)==0
971
+ || sqlite3_strglob("warning: SQLITE_AUTH*",z)==0
972
+ ){
973
+ bOutput = (eType & 0x10)!=0;
974
+ nAuth++;
975
+ }else
976
+ if( strncmp(z,"possible", 8)==0 && strstr(z,"tainted")!=0 ){
939977
bOutput = (eType & 0x40)!=0;
978
+ nVuln++;
979
+ }else
980
+ {
981
+ bOutput = (eType & 0x800)!=0;
940982
nOther++;
941983
}
942984
if( bOutput ){
943985
@ %h(zTime)\
944986
}
@@ -958,42 +1000,50 @@
9581000
fclose(in);
9591001
if( eType ){
9601002
@ </pre>
9611003
}
9621004
if( eType==0 ){
963
- int nNonHack = nPanic + nHang + nOther;
1005
+ int nNonHack = nPanic + nHang + nAuth + nSmtp + nVuln + nOther;
9641006
int nTotal = nNonHack + nHack + nXPost;
9651007
@ <p><table border="a" cellspacing="0" cellpadding="5">
9661008
if( nPanic>0 ){
9671009
@ <tr><td align="right">%d(nPanic)</td>
9681010
@ <td><a href="./errorlog?y=2">Panics</a></td>
9691011
}
1012
+ if( nVuln>0 ){
1013
+ @ <tr><td align="right">%d(nVuln)</td>
1014
+ @ <td><a href="./errorlog?y=64">TH1 Vulnerabilities</a></td>
1015
+ }
9701016
if( nHack>0 ){
9711017
@ <tr><td align="right">%d(nHack)</td>
9721018
@ <td><a href="./errorlog?y=1">Hack Attempts</a></td>
9731019
}
9741020
if( nHang>0 ){
9751021
@ <tr><td align="right">%d(nHang)</td>
976
- @ <td><a href="./errorlog?y=4/">Hung Backoffice</a></td>
1022
+ @ <td><a href="./errorlog?y=4">Hung Backoffice</a></td>
9771023
}
9781024
if( nXPost>0 ){
9791025
@ <tr><td align="right">%d(nXPost)</td>
980
- @ <td><a href="./errorlog?y=8/">POSTs from different origin</a></td>
1026
+ @ <td><a href="./errorlog?y=8">POSTs from different origin</a></td>
1027
+ }
1028
+ if( nAuth>0 ){
1029
+ @ <tr><td align="right">%d(nAuth)</td>
1030
+ @ <td><a href="./errorlog?y=16">SQLITE_AUTH and similar</a></td>
1031
+ }
1032
+ if( nSmtp>0 ){
1033
+ @ <tr><td align="right">%d(nSmtp)</td>
1034
+ @ <td><a href="./errorlog?y=32">SMTP faults</a></td>
9811035
}
9821036
if( nOther>0 ){
9831037
@ <tr><td align="right">%d(nOther)</td>
984
- @ <td><a href="./errorlog?y=64/">Other</a></td>
985
- }
986
- if( nHack+nXPost>0 && nNonHack>0 ){
987
- @ <tr><td align="right">%d(nNonHack)</td>
988
- @ <td><a href="%R/errorlog?y=70">Other than hack attempts</a></td>
1038
+ @ <td><a href="./errorlog?y=2048">Other</a></td>
9891039
}
9901040
@ <tr><td align="right">%d(nTotal)</td>
9911041
if( nTotal>0 ){
992
- @ <td><a href="./errorlog?y=255">All Messages</a></td>
1042
+ @ <td><a href="./errorlog?y=4095">All Messages</a></td>
9931043
}else{
9941044
@ <td>All Messages</td>
9951045
}
9961046
@ </table>
9971047
}
9981048
style_finish_page();
9991049
}
10001050
--- src/security_audit.c
+++ src/security_audit.c
@@ -100,10 +100,11 @@
100 const char *zReadCap; /* Capabilities of user group "reader" */
101 const char *zPubPages; /* GLOB pattern for public pages */
102 const char *zSelfCap; /* Capabilities of self-registered users */
103 int hasSelfReg = 0; /* True if able to self-register */
104 const char *zPublicUrl; /* Canonical access URL */
 
105 Blob cmd;
106 char *z;
107 int n, i;
108 CapabilityString *pCap;
109 char **azCSP; /* Parsed content security policy */
@@ -362,10 +363,22 @@
362 @ <li><p><b>WARNING:</b>
363 @ The "strict-manifest-syntax" flag is off. This is a security
364 @ risk. Turn this setting on (its default) to protect the users
365 @ of this repository.
366 }
 
 
 
 
 
 
 
 
 
 
 
 
367
368 /* Obsolete: */
369 if( hasAnyCap(zAnonCap, "d") ||
370 hasAnyCap(zDevCap, "d") ||
371 hasAnyCap(zReadCap, "d") ){
@@ -810,33 +823,39 @@
810 ** WEBPAGE: errorlog
811 **
812 ** Show the content of the error log. Only the administrator can view
813 ** this page.
814 **
815 ** y=0x01 Show only hack attempts
816 ** y=0x02 Show only panics and assertion faults
817 ** y=0x04 Show hung backoffice processes
818 ** y=0x08 Show POST requests from a different origin
819 ** y=0x40 Show other uncategorized messages
 
 
 
820 **
821 ** If y is omitted or is zero, a count of the various message types is
822 ** shown.
823 */
824 void errorlog_page(void){
825 i64 szFile;
826 FILE *in;
827 char *zLog;
828 const char *zType = P("y");
829 static const int eAllTypes = 0x4f;
830 long eType = 0;
831 int bOutput = 0;
832 int prevWasTime = 0;
833 int nHack = 0;
834 int nPanic = 0;
835 int nOther = 0;
836 int nHang = 0;
837 int nXPost = 0;
 
 
 
838 char z[10000];
839 char zTime[10000];
840
841 login_check_credentials();
842 if( !g.perm.Admin ){
@@ -906,11 +925,20 @@
906 @ <li>Hung backoffice processes
907 }
908 if( eType & 0x08 ){
909 @ <li>POST requests from different origin
910 }
 
 
 
 
 
 
911 if( eType & 0x40 ){
 
 
 
912 @ <li>Other uncategorized messages
913 }
914 @ </ul>
915 }
916 @ <hr>
@@ -924,21 +952,35 @@
924 nHack++;
925 }else
926 if( (strncmp(z,"panic: ", 7)==0 || strstr(z," assertion fault ")!=0) ){
927 bOutput = (eType & 0x02)!=0;
928 nPanic++;
 
 
 
 
929 }else
930 if( sqlite3_strglob("warning: backoffice process * still *",z)==0 ){
931 bOutput = (eType & 0x04)!=0;
932 nHang++;
933 }else
934 if( sqlite3_strglob("warning: POST from different origin*",z)==0 ){
935 bOutput = (eType & 0x08)!=0;
936 nXPost++;
937 }else
938 {
 
 
 
 
 
 
939 bOutput = (eType & 0x40)!=0;
 
 
 
 
940 nOther++;
941 }
942 if( bOutput ){
943 @ %h(zTime)\
944 }
@@ -958,42 +1000,50 @@
958 fclose(in);
959 if( eType ){
960 @ </pre>
961 }
962 if( eType==0 ){
963 int nNonHack = nPanic + nHang + nOther;
964 int nTotal = nNonHack + nHack + nXPost;
965 @ <p><table border="a" cellspacing="0" cellpadding="5">
966 if( nPanic>0 ){
967 @ <tr><td align="right">%d(nPanic)</td>
968 @ <td><a href="./errorlog?y=2">Panics</a></td>
969 }
 
 
 
 
970 if( nHack>0 ){
971 @ <tr><td align="right">%d(nHack)</td>
972 @ <td><a href="./errorlog?y=1">Hack Attempts</a></td>
973 }
974 if( nHang>0 ){
975 @ <tr><td align="right">%d(nHang)</td>
976 @ <td><a href="./errorlog?y=4/">Hung Backoffice</a></td>
977 }
978 if( nXPost>0 ){
979 @ <tr><td align="right">%d(nXPost)</td>
980 @ <td><a href="./errorlog?y=8/">POSTs from different origin</a></td>
 
 
 
 
 
 
 
 
981 }
982 if( nOther>0 ){
983 @ <tr><td align="right">%d(nOther)</td>
984 @ <td><a href="./errorlog?y=64/">Other</a></td>
985 }
986 if( nHack+nXPost>0 && nNonHack>0 ){
987 @ <tr><td align="right">%d(nNonHack)</td>
988 @ <td><a href="%R/errorlog?y=70">Other than hack attempts</a></td>
989 }
990 @ <tr><td align="right">%d(nTotal)</td>
991 if( nTotal>0 ){
992 @ <td><a href="./errorlog?y=255">All Messages</a></td>
993 }else{
994 @ <td>All Messages</td>
995 }
996 @ </table>
997 }
998 style_finish_page();
999 }
1000
--- src/security_audit.c
+++ src/security_audit.c
@@ -100,10 +100,11 @@
100 const char *zReadCap; /* Capabilities of user group "reader" */
101 const char *zPubPages; /* GLOB pattern for public pages */
102 const char *zSelfCap; /* Capabilities of self-registered users */
103 int hasSelfReg = 0; /* True if able to self-register */
104 const char *zPublicUrl; /* Canonical access URL */
105 const char *zVulnReport; /* The vuln-report setting */
106 Blob cmd;
107 char *z;
108 int n, i;
109 CapabilityString *pCap;
110 char **azCSP; /* Parsed content security policy */
@@ -362,10 +363,22 @@
363 @ <li><p><b>WARNING:</b>
364 @ The "strict-manifest-syntax" flag is off. This is a security
365 @ risk. Turn this setting on (its default) to protect the users
366 @ of this repository.
367 }
368
369 zVulnReport = db_get("vuln-report","log");
370 if( fossil_strcmp(zVulnReport,"block")!=0
371 && fossil_strcmp(zVulnReport,"fatal")!=0
372 ){
373 @ <li><p><b>WARNING:</b>
374 @ The <a href="%R/help?cmd=vuln-report">vuln-report setting</a>
375 @ has a value of "%h(zVulnReport)". This disables defenses against
376 @ XSS or SQL-injection vulnerabilities caused by coding errors in
377 @ custom TH1 scripts. For the best security, change
378 @ the value of the vuln-report setting to "block" or "fatal".
379 }
380
381 /* Obsolete: */
382 if( hasAnyCap(zAnonCap, "d") ||
383 hasAnyCap(zDevCap, "d") ||
384 hasAnyCap(zReadCap, "d") ){
@@ -810,33 +823,39 @@
823 ** WEBPAGE: errorlog
824 **
825 ** Show the content of the error log. Only the administrator can view
826 ** this page.
827 **
828 ** y=0x001 Show only hack attempts
829 ** y=0x002 Show only panics and assertion faults
830 ** y=0x004 Show hung backoffice processes
831 ** y=0x008 Show POST requests from a different origin
832 ** y=0x010 Show SQLITE_AUTH and similar
833 ** y=0x020 Show SMTP error reports
834 ** y=0x040 Show TH1 vulnerability reports
835 ** y=0x800 Show other uncategorized messages
836 **
837 ** If y is omitted or is zero, a count of the various message types is
838 ** shown.
839 */
840 void errorlog_page(void){
841 i64 szFile;
842 FILE *in;
843 char *zLog;
844 const char *zType = P("y");
845 static const int eAllTypes = 0x87f;
846 long eType = 0;
847 int bOutput = 0;
848 int prevWasTime = 0;
849 int nHack = 0;
850 int nPanic = 0;
851 int nOther = 0;
852 int nHang = 0;
853 int nXPost = 0;
854 int nAuth = 0;
855 int nSmtp = 0;
856 int nVuln = 0;
857 char z[10000];
858 char zTime[10000];
859
860 login_check_credentials();
861 if( !g.perm.Admin ){
@@ -906,11 +925,20 @@
925 @ <li>Hung backoffice processes
926 }
927 if( eType & 0x08 ){
928 @ <li>POST requests from different origin
929 }
930 if( eType & 0x10 ){
931 @ <li>SQLITE_AUTH and similar errors
932 }
933 if( eType & 0x20 ){
934 @ <li>SMTP malfunctions
935 }
936 if( eType & 0x40 ){
937 @ <li>TH1 vulnerabilities
938 }
939 if( eType & 0x800 ){
940 @ <li>Other uncategorized messages
941 }
942 @ </ul>
943 }
944 @ <hr>
@@ -924,21 +952,35 @@
952 nHack++;
953 }else
954 if( (strncmp(z,"panic: ", 7)==0 || strstr(z," assertion fault ")!=0) ){
955 bOutput = (eType & 0x02)!=0;
956 nPanic++;
957 }else
958 if( strncmp(z,"SMTP:", 5)==0 ){
959 bOutput = (eType & 0x20)!=0;
960 nSmtp++;
961 }else
962 if( sqlite3_strglob("warning: backoffice process * still *",z)==0 ){
963 bOutput = (eType & 0x04)!=0;
964 nHang++;
965 }else
966 if( sqlite3_strglob("warning: POST from different origin*",z)==0 ){
967 bOutput = (eType & 0x08)!=0;
968 nXPost++;
969 }else
970 if( sqlite3_strglob("SECURITY: authorizer blocks*",z)==0
971 || sqlite3_strglob("warning: SQLITE_AUTH*",z)==0
972 ){
973 bOutput = (eType & 0x10)!=0;
974 nAuth++;
975 }else
976 if( strncmp(z,"possible", 8)==0 && strstr(z,"tainted")!=0 ){
977 bOutput = (eType & 0x40)!=0;
978 nVuln++;
979 }else
980 {
981 bOutput = (eType & 0x800)!=0;
982 nOther++;
983 }
984 if( bOutput ){
985 @ %h(zTime)\
986 }
@@ -958,42 +1000,50 @@
1000 fclose(in);
1001 if( eType ){
1002 @ </pre>
1003 }
1004 if( eType==0 ){
1005 int nNonHack = nPanic + nHang + nAuth + nSmtp + nVuln + nOther;
1006 int nTotal = nNonHack + nHack + nXPost;
1007 @ <p><table border="a" cellspacing="0" cellpadding="5">
1008 if( nPanic>0 ){
1009 @ <tr><td align="right">%d(nPanic)</td>
1010 @ <td><a href="./errorlog?y=2">Panics</a></td>
1011 }
1012 if( nVuln>0 ){
1013 @ <tr><td align="right">%d(nVuln)</td>
1014 @ <td><a href="./errorlog?y=64">TH1 Vulnerabilities</a></td>
1015 }
1016 if( nHack>0 ){
1017 @ <tr><td align="right">%d(nHack)</td>
1018 @ <td><a href="./errorlog?y=1">Hack Attempts</a></td>
1019 }
1020 if( nHang>0 ){
1021 @ <tr><td align="right">%d(nHang)</td>
1022 @ <td><a href="./errorlog?y=4">Hung Backoffice</a></td>
1023 }
1024 if( nXPost>0 ){
1025 @ <tr><td align="right">%d(nXPost)</td>
1026 @ <td><a href="./errorlog?y=8">POSTs from different origin</a></td>
1027 }
1028 if( nAuth>0 ){
1029 @ <tr><td align="right">%d(nAuth)</td>
1030 @ <td><a href="./errorlog?y=16">SQLITE_AUTH and similar</a></td>
1031 }
1032 if( nSmtp>0 ){
1033 @ <tr><td align="right">%d(nSmtp)</td>
1034 @ <td><a href="./errorlog?y=32">SMTP faults</a></td>
1035 }
1036 if( nOther>0 ){
1037 @ <tr><td align="right">%d(nOther)</td>
1038 @ <td><a href="./errorlog?y=2048">Other</a></td>
 
 
 
 
1039 }
1040 @ <tr><td align="right">%d(nTotal)</td>
1041 if( nTotal>0 ){
1042 @ <td><a href="./errorlog?y=4095">All Messages</a></td>
1043 }else{
1044 @ <td>All Messages</td>
1045 }
1046 @ </table>
1047 }
1048 style_finish_page();
1049 }
1050
+15 -5
--- src/setup.c
+++ src/setup.c
@@ -201,11 +201,11 @@
201201
login_needed(0);
202202
return;
203203
}
204204
style_header("Log Menu");
205205
@ <table border="0" cellspacing="3">
206
-
206
+
207207
if( db_get_boolean("admin-log",1)==0 ){
208208
blob_appendf(&desc,
209209
"The admin log records configuration changes to the repository.\n"
210210
"<b>Disabled</b>: Turn on the "
211211
" <a href='%R/setup_settings'>admin-log setting</a> to enable."
@@ -216,11 +216,11 @@
216216
setup_menu_entry("Admin Log", "admin_log",
217217
"The admin log records configuration changes to the repository\n"
218218
"in the \"admin_log\" table.\n"
219219
);
220220
}
221
- setup_menu_entry("Artifact Log", "rcvfromlist",
221
+ setup_menu_entry("Xfer Log", "rcvfromlist",
222222
"The artifact log records when new content is added in the\n"
223223
"\"rcvfrom\" table.\n"
224224
);
225225
if( db_get_boolean("access-log",1) ){
226226
setup_menu_entry("User Log", "user_log",
@@ -462,11 +462,11 @@
462462
@ and "require a mouse event" should be turned on. These values only come
463463
@ into play when the main auto-hyperlink settings is 2 ("UserAgent and
464464
@ Javascript").</p>
465465
@
466466
@ <p>To see if Javascript-base hyperlink enabling mechanism is working,
467
- @ visit the <a href="%R/test_env">/test_env</a> page (from a separate
467
+ @ visit the <a href="%R/test-env">/test-env</a> page (from a separate
468468
@ web browser that is not logged in, even as "anonymous") and verify
469469
@ that the "g.jsHref" value is "1".</p>
470470
@ <p>(Properties: "auto-hyperlink", "auto-hyperlink-delay", and
471471
@ "auto-hyperlink-mouseover"")</p>
472472
}
@@ -604,11 +604,11 @@
604604
@ in the CGI script.
605605
@ </ol>
606606
@ (Property: "localauth")
607607
@
608608
@ <hr>
609
- onoff_attribute("Enable /test_env",
609
+ onoff_attribute("Enable /test-env",
610610
"test_env_enable", "test_env_enable", 0, 0);
611611
@ <p>When enabled, the %h(g.zBaseURL)/test_env URL is available to all
612612
@ users. When disabled (the default) only users Admin and Setup can visit
613613
@ the /test_env page.
614614
@ (Property: "test_env_enable")
@@ -1003,10 +1003,20 @@
10031003
"timeline-hard-newlines", "thnl", 0, 0);
10041004
@ <p>In timeline displays, newline characters in check-in comments force
10051005
@ a line break on the display.
10061006
@ (Property: "timeline-hard-newlines")</p>
10071007
1008
+ @ <hr>
1009
+ onoff_attribute("Do not adjust user-selected background colors",
1010
+ "raw-bgcolor", "rbgc", 0, 0);
1011
+ @ <p>Fossil normally attempts to adjust the saturation and intensity of
1012
+ @ user-specified background colors on check-ins and branches so that the
1013
+ @ foreground text is easily readable on all skins. Enable this setting
1014
+ @ to omit that adjustment and use exactly the background color specified
1015
+ @ by users.
1016
+ @ (Property: "raw-bgcolor")</p>
1017
+
10081018
@ <hr>
10091019
onoff_attribute("Use Universal Coordinated Time (UTC)",
10101020
"timeline-utc", "utc", 1, 0);
10111021
@ <p>Show times as UTC (also sometimes called Greenwich Mean Time (GMT) or
10121022
@ Zulu) instead of in local time. On this server, local time is currently
@@ -1286,11 +1296,11 @@
12861296
@ </p>
12871297
@ <hr>
12881298
textarea_attribute("Project Description", 3, 80,
12891299
"project-description", "pd", "", 0);
12901300
@ <p>Describe your project. This will be used in page headers for search
1291
- @ engines as well as a short RSS description.
1301
+ @ engines, the repository listing and a short RSS description.
12921302
@ (Property: "project-description")</p>
12931303
@ <hr>
12941304
entry_attribute("Canonical Server URL", 40, "email-url",
12951305
"eurl", "", 0);
12961306
@ <p>This is the URL used to access this repository as a server.
12971307
--- src/setup.c
+++ src/setup.c
@@ -201,11 +201,11 @@
201 login_needed(0);
202 return;
203 }
204 style_header("Log Menu");
205 @ <table border="0" cellspacing="3">
206
207 if( db_get_boolean("admin-log",1)==0 ){
208 blob_appendf(&desc,
209 "The admin log records configuration changes to the repository.\n"
210 "<b>Disabled</b>: Turn on the "
211 " <a href='%R/setup_settings'>admin-log setting</a> to enable."
@@ -216,11 +216,11 @@
216 setup_menu_entry("Admin Log", "admin_log",
217 "The admin log records configuration changes to the repository\n"
218 "in the \"admin_log\" table.\n"
219 );
220 }
221 setup_menu_entry("Artifact Log", "rcvfromlist",
222 "The artifact log records when new content is added in the\n"
223 "\"rcvfrom\" table.\n"
224 );
225 if( db_get_boolean("access-log",1) ){
226 setup_menu_entry("User Log", "user_log",
@@ -462,11 +462,11 @@
462 @ and "require a mouse event" should be turned on. These values only come
463 @ into play when the main auto-hyperlink settings is 2 ("UserAgent and
464 @ Javascript").</p>
465 @
466 @ <p>To see if Javascript-base hyperlink enabling mechanism is working,
467 @ visit the <a href="%R/test_env">/test_env</a> page (from a separate
468 @ web browser that is not logged in, even as "anonymous") and verify
469 @ that the "g.jsHref" value is "1".</p>
470 @ <p>(Properties: "auto-hyperlink", "auto-hyperlink-delay", and
471 @ "auto-hyperlink-mouseover"")</p>
472 }
@@ -604,11 +604,11 @@
604 @ in the CGI script.
605 @ </ol>
606 @ (Property: "localauth")
607 @
608 @ <hr>
609 onoff_attribute("Enable /test_env",
610 "test_env_enable", "test_env_enable", 0, 0);
611 @ <p>When enabled, the %h(g.zBaseURL)/test_env URL is available to all
612 @ users. When disabled (the default) only users Admin and Setup can visit
613 @ the /test_env page.
614 @ (Property: "test_env_enable")
@@ -1003,10 +1003,20 @@
1003 "timeline-hard-newlines", "thnl", 0, 0);
1004 @ <p>In timeline displays, newline characters in check-in comments force
1005 @ a line break on the display.
1006 @ (Property: "timeline-hard-newlines")</p>
1007
 
 
 
 
 
 
 
 
 
 
1008 @ <hr>
1009 onoff_attribute("Use Universal Coordinated Time (UTC)",
1010 "timeline-utc", "utc", 1, 0);
1011 @ <p>Show times as UTC (also sometimes called Greenwich Mean Time (GMT) or
1012 @ Zulu) instead of in local time. On this server, local time is currently
@@ -1286,11 +1296,11 @@
1286 @ </p>
1287 @ <hr>
1288 textarea_attribute("Project Description", 3, 80,
1289 "project-description", "pd", "", 0);
1290 @ <p>Describe your project. This will be used in page headers for search
1291 @ engines as well as a short RSS description.
1292 @ (Property: "project-description")</p>
1293 @ <hr>
1294 entry_attribute("Canonical Server URL", 40, "email-url",
1295 "eurl", "", 0);
1296 @ <p>This is the URL used to access this repository as a server.
1297
--- src/setup.c
+++ src/setup.c
@@ -201,11 +201,11 @@
201 login_needed(0);
202 return;
203 }
204 style_header("Log Menu");
205 @ <table border="0" cellspacing="3">
206
207 if( db_get_boolean("admin-log",1)==0 ){
208 blob_appendf(&desc,
209 "The admin log records configuration changes to the repository.\n"
210 "<b>Disabled</b>: Turn on the "
211 " <a href='%R/setup_settings'>admin-log setting</a> to enable."
@@ -216,11 +216,11 @@
216 setup_menu_entry("Admin Log", "admin_log",
217 "The admin log records configuration changes to the repository\n"
218 "in the \"admin_log\" table.\n"
219 );
220 }
221 setup_menu_entry("Xfer Log", "rcvfromlist",
222 "The artifact log records when new content is added in the\n"
223 "\"rcvfrom\" table.\n"
224 );
225 if( db_get_boolean("access-log",1) ){
226 setup_menu_entry("User Log", "user_log",
@@ -462,11 +462,11 @@
462 @ and "require a mouse event" should be turned on. These values only come
463 @ into play when the main auto-hyperlink settings is 2 ("UserAgent and
464 @ Javascript").</p>
465 @
466 @ <p>To see if Javascript-base hyperlink enabling mechanism is working,
467 @ visit the <a href="%R/test-env">/test-env</a> page (from a separate
468 @ web browser that is not logged in, even as "anonymous") and verify
469 @ that the "g.jsHref" value is "1".</p>
470 @ <p>(Properties: "auto-hyperlink", "auto-hyperlink-delay", and
471 @ "auto-hyperlink-mouseover"")</p>
472 }
@@ -604,11 +604,11 @@
604 @ in the CGI script.
605 @ </ol>
606 @ (Property: "localauth")
607 @
608 @ <hr>
609 onoff_attribute("Enable /test-env",
610 "test_env_enable", "test_env_enable", 0, 0);
611 @ <p>When enabled, the %h(g.zBaseURL)/test_env URL is available to all
612 @ users. When disabled (the default) only users Admin and Setup can visit
613 @ the /test_env page.
614 @ (Property: "test_env_enable")
@@ -1003,10 +1003,20 @@
1003 "timeline-hard-newlines", "thnl", 0, 0);
1004 @ <p>In timeline displays, newline characters in check-in comments force
1005 @ a line break on the display.
1006 @ (Property: "timeline-hard-newlines")</p>
1007
1008 @ <hr>
1009 onoff_attribute("Do not adjust user-selected background colors",
1010 "raw-bgcolor", "rbgc", 0, 0);
1011 @ <p>Fossil normally attempts to adjust the saturation and intensity of
1012 @ user-specified background colors on check-ins and branches so that the
1013 @ foreground text is easily readable on all skins. Enable this setting
1014 @ to omit that adjustment and use exactly the background color specified
1015 @ by users.
1016 @ (Property: "raw-bgcolor")</p>
1017
1018 @ <hr>
1019 onoff_attribute("Use Universal Coordinated Time (UTC)",
1020 "timeline-utc", "utc", 1, 0);
1021 @ <p>Show times as UTC (also sometimes called Greenwich Mean Time (GMT) or
1022 @ Zulu) instead of in local time. On this server, local time is currently
@@ -1286,11 +1296,11 @@
1296 @ </p>
1297 @ <hr>
1298 textarea_attribute("Project Description", 3, 80,
1299 "project-description", "pd", "", 0);
1300 @ <p>Describe your project. This will be used in page headers for search
1301 @ engines, the repository listing and a short RSS description.
1302 @ (Property: "project-description")</p>
1303 @ <hr>
1304 entry_attribute("Canonical Server URL", 40, "email-url",
1305 "eurl", "", 0);
1306 @ <p>This is the URL used to access this repository as a server.
1307
+85 -39
--- src/setupuser.c
+++ src/setupuser.c
@@ -41,21 +41,22 @@
4141
Stmt s;
4242
double rNow;
4343
const char *zWith = P("with");
4444
int bUnusedOnly = P("unused")!=0;
4545
int bUbg = P("ubg")!=0;
46
+ int bHaveAlerts;
4647
4748
login_check_credentials();
4849
if( !g.perm.Admin ){
4950
login_needed(0);
5051
return;
5152
}
52
-
53
+ bHaveAlerts = alert_tables_exist();
5354
style_submenu_element("Add", "setup_uedit");
5455
style_submenu_element("Log", "access_log");
5556
style_submenu_element("Help", "setup_ulist_notes");
56
- if( alert_tables_exist() ){
57
+ if( bHaveAlerts ){
5758
style_submenu_element("Subscribers", "subscribers");
5859
}
5960
style_set_current_feature("setup");
6061
style_header("User List");
6162
if( (zWith==0 || zWith[0]==0) && !bUnusedOnly ){
@@ -147,30 +148,34 @@
147148
zWith = mprintf(
148149
" AND login NOT IN ("
149150
"SELECT user FROM event WHERE user NOT NULL "
150151
"UNION ALL SELECT euser FROM event WHERE euser NOT NULL%s)"
151152
" AND uid NOT IN (SELECT uid FROM rcvfrom)",
152
- alert_tables_exist() ?
153
+ bHaveAlerts ?
153154
" UNION ALL SELECT suname FROM subscriber WHERE suname NOT NULL":"");
154155
}else if( zWith && zWith[0] ){
155156
zWith = mprintf(" AND fullcap(cap) GLOB '*[%q]*'", zWith);
156157
}else{
157158
zWith = "";
158159
}
159160
db_prepare(&s,
160
- "SELECT uid, login, cap, info, date(user.mtime,'unixepoch'),"
161
- " lower(login) AS sortkey, "
162
- " CASE WHEN info LIKE '%%expires 20%%'"
161
+ /*0-4*/"SELECT uid, login, cap, info, date(user.mtime,'unixepoch'),"
162
+ /* 5 */"lower(login) AS sortkey, "
163
+ /* 6 */"CASE WHEN info LIKE '%%expires 20%%'"
163164
" THEN substr(info,instr(lower(info),'expires')+8,10)"
164165
" END AS exp,"
165
- "atime,"
166
- " subscriber.ssub, subscriber.subscriberId,"
167
- " user.mtime AS sorttime"
168
- " FROM user LEFT JOIN lastAccess ON login=uname"
169
- " LEFT JOIN subscriber ON login=suname"
170
- " WHERE login NOT IN ('anonymous','nobody','developer','reader') %s"
171
- " ORDER BY sorttime DESC", zWith/*safe-for-%s*/
166
+ /* 7 */"atime,"
167
+ /* 8 */"user.mtime AS sorttime,"
168
+ /*9-11*/"%s"
169
+ " FROM user LEFT JOIN lastAccess ON login=uname"
170
+ " LEFT JOIN subscriber ON login=suname"
171
+ " WHERE login NOT IN ('anonymous','nobody','developer','reader') %s"
172
+ " ORDER BY sorttime DESC",
173
+ bHaveAlerts
174
+ ? "subscriber.ssub, subscriber.subscriberId, subscriber.semail"
175
+ : "null, null, null",
176
+ zWith/*safe-for-%s*/
172177
);
173178
rNow = db_double(0.0, "SELECT julianday('now');");
174179
while( db_step(&s)==SQLITE_ROW ){
175180
int uid = db_column_int(&s, 0);
176181
const char *zLogin = db_column_text(&s, 1);
@@ -180,12 +185,12 @@
180185
const char *zSortKey = db_column_text(&s,5);
181186
const char *zExp = db_column_text(&s,6);
182187
double rATime = db_column_double(&s,7);
183188
char *zAge = 0;
184189
const char *zSub;
185
- int sid = db_column_int(&s,9);
186
- sqlite3_int64 sorttime = db_column_int64(&s, 10);
190
+ int sid = db_column_int(&s,10);
191
+ sqlite3_int64 sorttime = db_column_int64(&s, 8);
187192
if( rATime>0.0 ){
188193
zAge = human_readable_age(rNow - rATime);
189194
}
190195
if( bUbg ){
191196
@ <tr style='background-color: %h(user_color(zLogin));'>
@@ -197,16 +202,18 @@
197202
@ <td>%h(zCap)
198203
@ <td>%h(zInfo)
199204
@ <td data-sortkey='%09llx(sorttime)'>%h(zDate?zDate:"")
200205
@ <td>%h(zExp?zExp:"")
201206
@ <td data-sortkey='%f(rATime)' style='white-space:nowrap'>%s(zAge?zAge:"")
202
- if( db_column_type(&s,8)==SQLITE_NULL ){
207
+ if( db_column_type(&s,9)==SQLITE_NULL ){
203208
@ <td>
204
- }else if( (zSub = db_column_text(&s,8))==0 || zSub[0]==0 ){
209
+ }else if( (zSub = db_column_text(&s,9))==0 || zSub[0]==0 ){
205210
@ <td><a href="%R/alerts?sid=%d(sid)"><i>off</i></a>
206211
}else{
207
- @ <td><a href="%R/alerts?sid=%d(sid)">%h(zSub)</a>
212
+ const char *zEmail = db_column_text(&s, 11);
213
+ char * zAt = zEmail ? mprintf(" &rarr; %h", zEmail) : mprintf("");
214
+ @ <td><a href="%R/alerts?sid=%d(sid)">%h(zSub)</a> %z(zAt)
208215
}
209216
210217
@ </tr>
211218
fossil_free(zAge);
212219
}
@@ -304,22 +311,59 @@
304311
while( zPw[0]=='*' ){ zPw++; }
305312
return zPw[0]!=0;
306313
}
307314
308315
/*
309
-** Return true if user capability string zNew contains any capability
310
-** letter which is not in user capability string zOrig, else 0. This
311
-** does not take inherited permissions into account. Either argument
312
-** may be NULL.
316
+** Return true if user capability strings zOrig and zNew materially
317
+** differ, taking into account that they may be sorted in an arbitary
318
+** order. This does not take inherited permissions into
319
+** account. Either argument may be NULL. A NULL and an empty string
320
+** are considered equivalent here. e.g. "abc" and "cab" are equivalent
321
+** for this purpose, but "aCb" and "acb" are not.
322
+*/
323
+static int userCapsChanged(const char *zOrig, const char *zNew){
324
+ if( !zOrig ){
325
+ return zNew ? (0!=*zNew) : 0;
326
+ }else if( !zNew ){
327
+ return 0!=*zOrig;
328
+ }else if( 0==fossil_strcmp(zOrig, zNew) ){
329
+ return 0;
330
+ }else{
331
+ /* We don't know that zOrig and zNew are sorted equivalently. The
332
+ ** following steps will compare strings which contain all the same
333
+ ** capabilities letters as equivalent, regardless of the letters'
334
+ ** order in their strings. */
335
+ char aOrig[128]; /* table of zOrig bytes */
336
+ int nOrig = 0, nNew = 0;
337
+
338
+ memset( &aOrig[0], 0, sizeof(aOrig) );
339
+ for( ; *zOrig; ++zOrig, ++nOrig ){
340
+ if( 0==(*zOrig & 0x80) ){
341
+ aOrig[(int)*zOrig] = 1;
342
+ }
343
+ }
344
+ for( ; *zNew; ++zNew, ++nNew ){
345
+ if( 0==(*zNew & 0x80) && !aOrig[(int)*zNew] ){
346
+ return 1;
347
+ }
348
+ }
349
+ return nOrig!=nNew;
350
+ }
351
+}
352
+
353
+/*
354
+** COMMAND: test-user-caps-changed
355
+**
356
+** Usage: %fossil test-user-caps-changed caps1 caps2
357
+**
313358
*/
314
-static int userHasNewCaps(const char *zOrig, const char *zNew){
315
- for( ; zNew && *zNew; ++zNew ){
316
- if( !zOrig || strchr(zOrig,*zNew)==0 ){
317
- return *zNew;
318
- }
319
- }
320
- return 0;
359
+void test_user_caps_changed(void){
360
+
361
+ char const * zOld = g.argc>2 ? g.argv[2] : NULL;
362
+ char const * zNew = g.argc>3 ? g.argv[3] : NULL;
363
+ fossil_print("Has changes? = %d\n",
364
+ userCapsChanged( zOld, zNew ));
321365
}
322366
323367
/*
324368
** Sends notification of user permission elevation changes to all
325369
** subscribers with a "u" subscription. This is a no-op if alerts are
@@ -333,11 +377,11 @@
333377
** edits their subscriptions after an admin assigns them this one,
334378
** this particular one will be lost. "Feature or bug?" is unclear,
335379
** but it would be odd for a non-admin to be assigned this
336380
** capability.
337381
*/
338
-static void alert_user_elevation(const char *zLogin, /*Affected user*/
382
+static void alert_user_cap_change(const char *zLogin, /*Affected user*/
339383
int uid, /*[user].uid*/
340384
int bIsNew, /*true if new user*/
341385
const char *zOrigCaps,/*Old caps*/
342386
const char *zNewCaps /*New caps*/){
343387
Blob hdr, body;
@@ -349,21 +393,21 @@
349393
char * zSubject;
350394
351395
if( !alert_enabled() ) return;
352396
zSubject = bIsNew
353397
? mprintf("New user created: [%q]", zLogin)
354
- : mprintf("User [%q] permissions elevated", zLogin);
398
+ : mprintf("User [%q] capabilities changed", zLogin);
355399
zURL = db_get("email-url",0);
356400
zSubname = db_get("email-subname", "[Fossil Repo]");
357401
blob_init(&body, 0, 0);
358402
blob_init(&hdr, 0, 0);
359403
if( bIsNew ){
360
- blob_appendf(&body, "User [%q] was created by with "
404
+ blob_appendf(&body, "User [%q] was created with "
361405
"permissions [%q] by user [%q].\n",
362406
zLogin, zNewCaps, g.zLogin);
363407
} else {
364
- blob_appendf(&body, "Permissions for user [%q] where elevated "
408
+ blob_appendf(&body, "Permissions for user [%q] where changed "
365409
"from [%q] to [%q] by user [%q].\n",
366410
zLogin, zOrigCaps, zNewCaps, g.zLogin);
367411
}
368412
if( zURL ){
369413
blob_appendf(&body, "\nUser editor: %s/setup_uedit?uid=%d\n", zURL, uid);
@@ -486,11 +530,11 @@
486530
}else if( !cgi_csrf_safe(2) ){
487531
/* This might be a cross-site request forgery, so ignore it */
488532
}else{
489533
/* We have all the information we need to make the change to the user */
490534
char c;
491
- int bHasNewCaps = 0 /* 1 if user's permissions are increased */;
535
+ int bCapsChanged = 0 /* 1 if user's permissions changed */;
492536
const int bIsNew = uid<=0;
493537
char aCap[70], zNm[4];
494538
zNm[0] = 'a';
495539
zNm[2] = 0;
496540
for(i=0, c='a'; c<='z'; c++){
@@ -508,11 +552,11 @@
508552
a[c&0x7f] = P(zNm)!=0;
509553
if( a[c&0x7f] ) aCap[i++] = c;
510554
}
511555
512556
aCap[i] = 0;
513
- bHasNewCaps = bIsNew || userHasNewCaps(zOldCaps, &aCap[0]);
557
+ bCapsChanged = bIsNew || userCapsChanged(zOldCaps, &aCap[0]);
514558
zPw = P("pw");
515559
zLogin = P("login");
516560
if( strlen(zLogin)==0 ){
517561
const char *zRef = cgi_referer("setup_ulist");
518562
style_header("User Creation Error");
@@ -613,18 +657,20 @@
613657
@ <span class="loginError">%h(zErr)</span>
614658
@
615659
@ <p><a href="setup_uedit?id=%d(uid)&referer=%T(zRef)">
616660
@ [Bummer]</a></p>
617661
style_finish_page();
618
- if( bHasNewCaps ){
619
- alert_user_elevation(zLogin, uid, bIsNew, zOldCaps, &aCap[0]);
662
+ if( bCapsChanged ){
663
+ /* It's possible that caps were updated locally even if
664
+ ** login group updates failed. */
665
+ alert_user_cap_change(zLogin, uid, bIsNew, zOldCaps, &aCap[0]);
620666
}
621667
return;
622668
}
623669
}
624
- if( bHasNewCaps ){
625
- alert_user_elevation(zLogin, uid, bIsNew, zOldCaps, &aCap[0]);
670
+ if( bCapsChanged ){
671
+ alert_user_cap_change(zLogin, uid, bIsNew, zOldCaps, &aCap[0]);
626672
}
627673
cgi_redirect(cgi_referer("setup_ulist"));
628674
return;
629675
}
630676
631677
--- src/setupuser.c
+++ src/setupuser.c
@@ -41,21 +41,22 @@
41 Stmt s;
42 double rNow;
43 const char *zWith = P("with");
44 int bUnusedOnly = P("unused")!=0;
45 int bUbg = P("ubg")!=0;
 
46
47 login_check_credentials();
48 if( !g.perm.Admin ){
49 login_needed(0);
50 return;
51 }
52
53 style_submenu_element("Add", "setup_uedit");
54 style_submenu_element("Log", "access_log");
55 style_submenu_element("Help", "setup_ulist_notes");
56 if( alert_tables_exist() ){
57 style_submenu_element("Subscribers", "subscribers");
58 }
59 style_set_current_feature("setup");
60 style_header("User List");
61 if( (zWith==0 || zWith[0]==0) && !bUnusedOnly ){
@@ -147,30 +148,34 @@
147 zWith = mprintf(
148 " AND login NOT IN ("
149 "SELECT user FROM event WHERE user NOT NULL "
150 "UNION ALL SELECT euser FROM event WHERE euser NOT NULL%s)"
151 " AND uid NOT IN (SELECT uid FROM rcvfrom)",
152 alert_tables_exist() ?
153 " UNION ALL SELECT suname FROM subscriber WHERE suname NOT NULL":"");
154 }else if( zWith && zWith[0] ){
155 zWith = mprintf(" AND fullcap(cap) GLOB '*[%q]*'", zWith);
156 }else{
157 zWith = "";
158 }
159 db_prepare(&s,
160 "SELECT uid, login, cap, info, date(user.mtime,'unixepoch'),"
161 " lower(login) AS sortkey, "
162 " CASE WHEN info LIKE '%%expires 20%%'"
163 " THEN substr(info,instr(lower(info),'expires')+8,10)"
164 " END AS exp,"
165 "atime,"
166 " subscriber.ssub, subscriber.subscriberId,"
167 " user.mtime AS sorttime"
168 " FROM user LEFT JOIN lastAccess ON login=uname"
169 " LEFT JOIN subscriber ON login=suname"
170 " WHERE login NOT IN ('anonymous','nobody','developer','reader') %s"
171 " ORDER BY sorttime DESC", zWith/*safe-for-%s*/
 
 
 
 
172 );
173 rNow = db_double(0.0, "SELECT julianday('now');");
174 while( db_step(&s)==SQLITE_ROW ){
175 int uid = db_column_int(&s, 0);
176 const char *zLogin = db_column_text(&s, 1);
@@ -180,12 +185,12 @@
180 const char *zSortKey = db_column_text(&s,5);
181 const char *zExp = db_column_text(&s,6);
182 double rATime = db_column_double(&s,7);
183 char *zAge = 0;
184 const char *zSub;
185 int sid = db_column_int(&s,9);
186 sqlite3_int64 sorttime = db_column_int64(&s, 10);
187 if( rATime>0.0 ){
188 zAge = human_readable_age(rNow - rATime);
189 }
190 if( bUbg ){
191 @ <tr style='background-color: %h(user_color(zLogin));'>
@@ -197,16 +202,18 @@
197 @ <td>%h(zCap)
198 @ <td>%h(zInfo)
199 @ <td data-sortkey='%09llx(sorttime)'>%h(zDate?zDate:"")
200 @ <td>%h(zExp?zExp:"")
201 @ <td data-sortkey='%f(rATime)' style='white-space:nowrap'>%s(zAge?zAge:"")
202 if( db_column_type(&s,8)==SQLITE_NULL ){
203 @ <td>
204 }else if( (zSub = db_column_text(&s,8))==0 || zSub[0]==0 ){
205 @ <td><a href="%R/alerts?sid=%d(sid)"><i>off</i></a>
206 }else{
207 @ <td><a href="%R/alerts?sid=%d(sid)">%h(zSub)</a>
 
 
208 }
209
210 @ </tr>
211 fossil_free(zAge);
212 }
@@ -304,22 +311,59 @@
304 while( zPw[0]=='*' ){ zPw++; }
305 return zPw[0]!=0;
306 }
307
308 /*
309 ** Return true if user capability string zNew contains any capability
310 ** letter which is not in user capability string zOrig, else 0. This
311 ** does not take inherited permissions into account. Either argument
312 ** may be NULL.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
313 */
314 static int userHasNewCaps(const char *zOrig, const char *zNew){
315 for( ; zNew && *zNew; ++zNew ){
316 if( !zOrig || strchr(zOrig,*zNew)==0 ){
317 return *zNew;
318 }
319 }
320 return 0;
321 }
322
323 /*
324 ** Sends notification of user permission elevation changes to all
325 ** subscribers with a "u" subscription. This is a no-op if alerts are
@@ -333,11 +377,11 @@
333 ** edits their subscriptions after an admin assigns them this one,
334 ** this particular one will be lost. "Feature or bug?" is unclear,
335 ** but it would be odd for a non-admin to be assigned this
336 ** capability.
337 */
338 static void alert_user_elevation(const char *zLogin, /*Affected user*/
339 int uid, /*[user].uid*/
340 int bIsNew, /*true if new user*/
341 const char *zOrigCaps,/*Old caps*/
342 const char *zNewCaps /*New caps*/){
343 Blob hdr, body;
@@ -349,21 +393,21 @@
349 char * zSubject;
350
351 if( !alert_enabled() ) return;
352 zSubject = bIsNew
353 ? mprintf("New user created: [%q]", zLogin)
354 : mprintf("User [%q] permissions elevated", zLogin);
355 zURL = db_get("email-url",0);
356 zSubname = db_get("email-subname", "[Fossil Repo]");
357 blob_init(&body, 0, 0);
358 blob_init(&hdr, 0, 0);
359 if( bIsNew ){
360 blob_appendf(&body, "User [%q] was created by with "
361 "permissions [%q] by user [%q].\n",
362 zLogin, zNewCaps, g.zLogin);
363 } else {
364 blob_appendf(&body, "Permissions for user [%q] where elevated "
365 "from [%q] to [%q] by user [%q].\n",
366 zLogin, zOrigCaps, zNewCaps, g.zLogin);
367 }
368 if( zURL ){
369 blob_appendf(&body, "\nUser editor: %s/setup_uedit?uid=%d\n", zURL, uid);
@@ -486,11 +530,11 @@
486 }else if( !cgi_csrf_safe(2) ){
487 /* This might be a cross-site request forgery, so ignore it */
488 }else{
489 /* We have all the information we need to make the change to the user */
490 char c;
491 int bHasNewCaps = 0 /* 1 if user's permissions are increased */;
492 const int bIsNew = uid<=0;
493 char aCap[70], zNm[4];
494 zNm[0] = 'a';
495 zNm[2] = 0;
496 for(i=0, c='a'; c<='z'; c++){
@@ -508,11 +552,11 @@
508 a[c&0x7f] = P(zNm)!=0;
509 if( a[c&0x7f] ) aCap[i++] = c;
510 }
511
512 aCap[i] = 0;
513 bHasNewCaps = bIsNew || userHasNewCaps(zOldCaps, &aCap[0]);
514 zPw = P("pw");
515 zLogin = P("login");
516 if( strlen(zLogin)==0 ){
517 const char *zRef = cgi_referer("setup_ulist");
518 style_header("User Creation Error");
@@ -613,18 +657,20 @@
613 @ <span class="loginError">%h(zErr)</span>
614 @
615 @ <p><a href="setup_uedit?id=%d(uid)&referer=%T(zRef)">
616 @ [Bummer]</a></p>
617 style_finish_page();
618 if( bHasNewCaps ){
619 alert_user_elevation(zLogin, uid, bIsNew, zOldCaps, &aCap[0]);
 
 
620 }
621 return;
622 }
623 }
624 if( bHasNewCaps ){
625 alert_user_elevation(zLogin, uid, bIsNew, zOldCaps, &aCap[0]);
626 }
627 cgi_redirect(cgi_referer("setup_ulist"));
628 return;
629 }
630
631
--- src/setupuser.c
+++ src/setupuser.c
@@ -41,21 +41,22 @@
41 Stmt s;
42 double rNow;
43 const char *zWith = P("with");
44 int bUnusedOnly = P("unused")!=0;
45 int bUbg = P("ubg")!=0;
46 int bHaveAlerts;
47
48 login_check_credentials();
49 if( !g.perm.Admin ){
50 login_needed(0);
51 return;
52 }
53 bHaveAlerts = alert_tables_exist();
54 style_submenu_element("Add", "setup_uedit");
55 style_submenu_element("Log", "access_log");
56 style_submenu_element("Help", "setup_ulist_notes");
57 if( bHaveAlerts ){
58 style_submenu_element("Subscribers", "subscribers");
59 }
60 style_set_current_feature("setup");
61 style_header("User List");
62 if( (zWith==0 || zWith[0]==0) && !bUnusedOnly ){
@@ -147,30 +148,34 @@
148 zWith = mprintf(
149 " AND login NOT IN ("
150 "SELECT user FROM event WHERE user NOT NULL "
151 "UNION ALL SELECT euser FROM event WHERE euser NOT NULL%s)"
152 " AND uid NOT IN (SELECT uid FROM rcvfrom)",
153 bHaveAlerts ?
154 " UNION ALL SELECT suname FROM subscriber WHERE suname NOT NULL":"");
155 }else if( zWith && zWith[0] ){
156 zWith = mprintf(" AND fullcap(cap) GLOB '*[%q]*'", zWith);
157 }else{
158 zWith = "";
159 }
160 db_prepare(&s,
161 /*0-4*/"SELECT uid, login, cap, info, date(user.mtime,'unixepoch'),"
162 /* 5 */"lower(login) AS sortkey, "
163 /* 6 */"CASE WHEN info LIKE '%%expires 20%%'"
164 " THEN substr(info,instr(lower(info),'expires')+8,10)"
165 " END AS exp,"
166 /* 7 */"atime,"
167 /* 8 */"user.mtime AS sorttime,"
168 /*9-11*/"%s"
169 " FROM user LEFT JOIN lastAccess ON login=uname"
170 " LEFT JOIN subscriber ON login=suname"
171 " WHERE login NOT IN ('anonymous','nobody','developer','reader') %s"
172 " ORDER BY sorttime DESC",
173 bHaveAlerts
174 ? "subscriber.ssub, subscriber.subscriberId, subscriber.semail"
175 : "null, null, null",
176 zWith/*safe-for-%s*/
177 );
178 rNow = db_double(0.0, "SELECT julianday('now');");
179 while( db_step(&s)==SQLITE_ROW ){
180 int uid = db_column_int(&s, 0);
181 const char *zLogin = db_column_text(&s, 1);
@@ -180,12 +185,12 @@
185 const char *zSortKey = db_column_text(&s,5);
186 const char *zExp = db_column_text(&s,6);
187 double rATime = db_column_double(&s,7);
188 char *zAge = 0;
189 const char *zSub;
190 int sid = db_column_int(&s,10);
191 sqlite3_int64 sorttime = db_column_int64(&s, 8);
192 if( rATime>0.0 ){
193 zAge = human_readable_age(rNow - rATime);
194 }
195 if( bUbg ){
196 @ <tr style='background-color: %h(user_color(zLogin));'>
@@ -197,16 +202,18 @@
202 @ <td>%h(zCap)
203 @ <td>%h(zInfo)
204 @ <td data-sortkey='%09llx(sorttime)'>%h(zDate?zDate:"")
205 @ <td>%h(zExp?zExp:"")
206 @ <td data-sortkey='%f(rATime)' style='white-space:nowrap'>%s(zAge?zAge:"")
207 if( db_column_type(&s,9)==SQLITE_NULL ){
208 @ <td>
209 }else if( (zSub = db_column_text(&s,9))==0 || zSub[0]==0 ){
210 @ <td><a href="%R/alerts?sid=%d(sid)"><i>off</i></a>
211 }else{
212 const char *zEmail = db_column_text(&s, 11);
213 char * zAt = zEmail ? mprintf(" &rarr; %h", zEmail) : mprintf("");
214 @ <td><a href="%R/alerts?sid=%d(sid)">%h(zSub)</a> %z(zAt)
215 }
216
217 @ </tr>
218 fossil_free(zAge);
219 }
@@ -304,22 +311,59 @@
311 while( zPw[0]=='*' ){ zPw++; }
312 return zPw[0]!=0;
313 }
314
315 /*
316 ** Return true if user capability strings zOrig and zNew materially
317 ** differ, taking into account that they may be sorted in an arbitary
318 ** order. This does not take inherited permissions into
319 ** account. Either argument may be NULL. A NULL and an empty string
320 ** are considered equivalent here. e.g. "abc" and "cab" are equivalent
321 ** for this purpose, but "aCb" and "acb" are not.
322 */
323 static int userCapsChanged(const char *zOrig, const char *zNew){
324 if( !zOrig ){
325 return zNew ? (0!=*zNew) : 0;
326 }else if( !zNew ){
327 return 0!=*zOrig;
328 }else if( 0==fossil_strcmp(zOrig, zNew) ){
329 return 0;
330 }else{
331 /* We don't know that zOrig and zNew are sorted equivalently. The
332 ** following steps will compare strings which contain all the same
333 ** capabilities letters as equivalent, regardless of the letters'
334 ** order in their strings. */
335 char aOrig[128]; /* table of zOrig bytes */
336 int nOrig = 0, nNew = 0;
337
338 memset( &aOrig[0], 0, sizeof(aOrig) );
339 for( ; *zOrig; ++zOrig, ++nOrig ){
340 if( 0==(*zOrig & 0x80) ){
341 aOrig[(int)*zOrig] = 1;
342 }
343 }
344 for( ; *zNew; ++zNew, ++nNew ){
345 if( 0==(*zNew & 0x80) && !aOrig[(int)*zNew] ){
346 return 1;
347 }
348 }
349 return nOrig!=nNew;
350 }
351 }
352
353 /*
354 ** COMMAND: test-user-caps-changed
355 **
356 ** Usage: %fossil test-user-caps-changed caps1 caps2
357 **
358 */
359 void test_user_caps_changed(void){
360
361 char const * zOld = g.argc>2 ? g.argv[2] : NULL;
362 char const * zNew = g.argc>3 ? g.argv[3] : NULL;
363 fossil_print("Has changes? = %d\n",
364 userCapsChanged( zOld, zNew ));
 
365 }
366
367 /*
368 ** Sends notification of user permission elevation changes to all
369 ** subscribers with a "u" subscription. This is a no-op if alerts are
@@ -333,11 +377,11 @@
377 ** edits their subscriptions after an admin assigns them this one,
378 ** this particular one will be lost. "Feature or bug?" is unclear,
379 ** but it would be odd for a non-admin to be assigned this
380 ** capability.
381 */
382 static void alert_user_cap_change(const char *zLogin, /*Affected user*/
383 int uid, /*[user].uid*/
384 int bIsNew, /*true if new user*/
385 const char *zOrigCaps,/*Old caps*/
386 const char *zNewCaps /*New caps*/){
387 Blob hdr, body;
@@ -349,21 +393,21 @@
393 char * zSubject;
394
395 if( !alert_enabled() ) return;
396 zSubject = bIsNew
397 ? mprintf("New user created: [%q]", zLogin)
398 : mprintf("User [%q] capabilities changed", zLogin);
399 zURL = db_get("email-url",0);
400 zSubname = db_get("email-subname", "[Fossil Repo]");
401 blob_init(&body, 0, 0);
402 blob_init(&hdr, 0, 0);
403 if( bIsNew ){
404 blob_appendf(&body, "User [%q] was created with "
405 "permissions [%q] by user [%q].\n",
406 zLogin, zNewCaps, g.zLogin);
407 } else {
408 blob_appendf(&body, "Permissions for user [%q] where changed "
409 "from [%q] to [%q] by user [%q].\n",
410 zLogin, zOrigCaps, zNewCaps, g.zLogin);
411 }
412 if( zURL ){
413 blob_appendf(&body, "\nUser editor: %s/setup_uedit?uid=%d\n", zURL, uid);
@@ -486,11 +530,11 @@
530 }else if( !cgi_csrf_safe(2) ){
531 /* This might be a cross-site request forgery, so ignore it */
532 }else{
533 /* We have all the information we need to make the change to the user */
534 char c;
535 int bCapsChanged = 0 /* 1 if user's permissions changed */;
536 const int bIsNew = uid<=0;
537 char aCap[70], zNm[4];
538 zNm[0] = 'a';
539 zNm[2] = 0;
540 for(i=0, c='a'; c<='z'; c++){
@@ -508,11 +552,11 @@
552 a[c&0x7f] = P(zNm)!=0;
553 if( a[c&0x7f] ) aCap[i++] = c;
554 }
555
556 aCap[i] = 0;
557 bCapsChanged = bIsNew || userCapsChanged(zOldCaps, &aCap[0]);
558 zPw = P("pw");
559 zLogin = P("login");
560 if( strlen(zLogin)==0 ){
561 const char *zRef = cgi_referer("setup_ulist");
562 style_header("User Creation Error");
@@ -613,18 +657,20 @@
657 @ <span class="loginError">%h(zErr)</span>
658 @
659 @ <p><a href="setup_uedit?id=%d(uid)&referer=%T(zRef)">
660 @ [Bummer]</a></p>
661 style_finish_page();
662 if( bCapsChanged ){
663 /* It's possible that caps were updated locally even if
664 ** login group updates failed. */
665 alert_user_cap_change(zLogin, uid, bIsNew, zOldCaps, &aCap[0]);
666 }
667 return;
668 }
669 }
670 if( bCapsChanged ){
671 alert_user_cap_change(zLogin, uid, bIsNew, zOldCaps, &aCap[0]);
672 }
673 cgi_redirect(cgi_referer("setup_ulist"));
674 return;
675 }
676
677
+4 -3
--- src/shun.c
+++ src/shun.c
@@ -373,11 +373,11 @@
373373
login_check_credentials();
374374
if( !g.perm.Admin ){
375375
login_needed(0);
376376
return;
377377
}
378
- style_header("Artifact Receipts");
378
+ style_header("Xfer Log");
379379
style_submenu_element("Log-Menu", "setup-logmenu");
380380
if( showAll ){
381381
ofst = 0;
382382
}else{
383383
style_submenu_element("All", "rcvfromlist?all=1");
@@ -415,12 +415,13 @@
415415
" FROM rcvfrom LEFT JOIN user USING(uid)"
416416
" ORDER BY rcvid DESC LIMIT %d OFFSET %d",
417417
showAll ? -1 : perScreen+1, ofst
418418
);
419419
@ <p>Whenever new artifacts are added to the repository, either by
420
- @ push or using the web interface, an entry is made in the RCVFROM table
421
- @ to record the source of that artifact. This log facilitates
420
+ @ push or using the web interface or by "fossil commit" or similar,
421
+ @ an entry is made in the RCVFROM table
422
+ @ to record the source of those artifacts. This log facilitates
422423
@ finding and fixing attempts to inject illicit content into the
423424
@ repository.</p>
424425
@
425426
@ <p>Click on the "rcvid" to show a list of specific artifacts received
426427
@ by a transaction. After identifying illicit artifacts, remove them
427428
--- src/shun.c
+++ src/shun.c
@@ -373,11 +373,11 @@
373 login_check_credentials();
374 if( !g.perm.Admin ){
375 login_needed(0);
376 return;
377 }
378 style_header("Artifact Receipts");
379 style_submenu_element("Log-Menu", "setup-logmenu");
380 if( showAll ){
381 ofst = 0;
382 }else{
383 style_submenu_element("All", "rcvfromlist?all=1");
@@ -415,12 +415,13 @@
415 " FROM rcvfrom LEFT JOIN user USING(uid)"
416 " ORDER BY rcvid DESC LIMIT %d OFFSET %d",
417 showAll ? -1 : perScreen+1, ofst
418 );
419 @ <p>Whenever new artifacts are added to the repository, either by
420 @ push or using the web interface, an entry is made in the RCVFROM table
421 @ to record the source of that artifact. This log facilitates
 
422 @ finding and fixing attempts to inject illicit content into the
423 @ repository.</p>
424 @
425 @ <p>Click on the "rcvid" to show a list of specific artifacts received
426 @ by a transaction. After identifying illicit artifacts, remove them
427
--- src/shun.c
+++ src/shun.c
@@ -373,11 +373,11 @@
373 login_check_credentials();
374 if( !g.perm.Admin ){
375 login_needed(0);
376 return;
377 }
378 style_header("Xfer Log");
379 style_submenu_element("Log-Menu", "setup-logmenu");
380 if( showAll ){
381 ofst = 0;
382 }else{
383 style_submenu_element("All", "rcvfromlist?all=1");
@@ -415,12 +415,13 @@
415 " FROM rcvfrom LEFT JOIN user USING(uid)"
416 " ORDER BY rcvid DESC LIMIT %d OFFSET %d",
417 showAll ? -1 : perScreen+1, ofst
418 );
419 @ <p>Whenever new artifacts are added to the repository, either by
420 @ push or using the web interface or by "fossil commit" or similar,
421 @ an entry is made in the RCVFROM table
422 @ to record the source of those artifacts. This log facilitates
423 @ finding and fixing attempts to inject illicit content into the
424 @ repository.</p>
425 @
426 @ <p>Click on the "rcvid" to show a list of specific artifacts received
427 @ by a transaction. After identifying illicit artifacts, remove them
428
+2 -1
--- src/sitemap.c
+++ src/sitemap.c
@@ -285,18 +285,19 @@
285285
style_header("Test Page Map");
286286
style_adunit_config(ADUNIT_RIGHT_OK);
287287
}
288288
@ <ul id="sitemap" class="columns" style="column-width:20em">
289289
if( g.perm.Admin || db_get_boolean("test_env_enable",0) ){
290
- @ <li>%z(href("%R/test_env"))CGI Environment Test</a></li>
290
+ @ <li>%z(href("%R/test-env"))CGI Environment Test</a></li>
291291
}
292292
if( g.perm.Read ){
293293
@ <li>%z(href("%R/test-rename-list"))List of file renames</a></li>
294294
}
295295
@ <li>%z(href("%R/test-builtin-files"))List of built-in files</a></li>
296296
@ <li>%z(href("%R/mimetype_list"))List of MIME types</a></li>
297297
@ <li>%z(href("%R/hash-color-test"))Hash color test</a>
298
+ @ <li>%z(href("%R/test-bgcolor"))Background color test</a>
298299
if( g.perm.Admin ){
299300
@ <li>%z(href("%R/test-backlinks"))List of backlinks</a></li>
300301
@ <li>%z(href("%R/test-backlink-timeline"))Backlink timeline</a></li>
301302
@ <li>%z(href("%R/phantoms"))List of phantom artifacts</a></li>
302303
@ <li>%z(href("%R/test-warning"))Error Log test page</a></li>
303304
--- src/sitemap.c
+++ src/sitemap.c
@@ -285,18 +285,19 @@
285 style_header("Test Page Map");
286 style_adunit_config(ADUNIT_RIGHT_OK);
287 }
288 @ <ul id="sitemap" class="columns" style="column-width:20em">
289 if( g.perm.Admin || db_get_boolean("test_env_enable",0) ){
290 @ <li>%z(href("%R/test_env"))CGI Environment Test</a></li>
291 }
292 if( g.perm.Read ){
293 @ <li>%z(href("%R/test-rename-list"))List of file renames</a></li>
294 }
295 @ <li>%z(href("%R/test-builtin-files"))List of built-in files</a></li>
296 @ <li>%z(href("%R/mimetype_list"))List of MIME types</a></li>
297 @ <li>%z(href("%R/hash-color-test"))Hash color test</a>
 
298 if( g.perm.Admin ){
299 @ <li>%z(href("%R/test-backlinks"))List of backlinks</a></li>
300 @ <li>%z(href("%R/test-backlink-timeline"))Backlink timeline</a></li>
301 @ <li>%z(href("%R/phantoms"))List of phantom artifacts</a></li>
302 @ <li>%z(href("%R/test-warning"))Error Log test page</a></li>
303
--- src/sitemap.c
+++ src/sitemap.c
@@ -285,18 +285,19 @@
285 style_header("Test Page Map");
286 style_adunit_config(ADUNIT_RIGHT_OK);
287 }
288 @ <ul id="sitemap" class="columns" style="column-width:20em">
289 if( g.perm.Admin || db_get_boolean("test_env_enable",0) ){
290 @ <li>%z(href("%R/test-env"))CGI Environment Test</a></li>
291 }
292 if( g.perm.Read ){
293 @ <li>%z(href("%R/test-rename-list"))List of file renames</a></li>
294 }
295 @ <li>%z(href("%R/test-builtin-files"))List of built-in files</a></li>
296 @ <li>%z(href("%R/mimetype_list"))List of MIME types</a></li>
297 @ <li>%z(href("%R/hash-color-test"))Hash color test</a>
298 @ <li>%z(href("%R/test-bgcolor"))Background color test</a>
299 if( g.perm.Admin ){
300 @ <li>%z(href("%R/test-backlinks"))List of backlinks</a></li>
301 @ <li>%z(href("%R/test-backlink-timeline"))Backlink timeline</a></li>
302 @ <li>%z(href("%R/phantoms"))List of phantom artifacts</a></li>
303 @ <li>%z(href("%R/test-warning"))Error Log test page</a></li>
304
+98 -48
--- src/smtp.c
+++ src/smtp.c
@@ -156,13 +156,15 @@
156156
const char *zDest; /* Domain that will receive the email */
157157
char *zHostname; /* Hostname of SMTP server for zDest */
158158
u32 smtpFlags; /* Flags changing the operation */
159159
FILE *logFile; /* Write session transcript to this log file */
160160
Blob *pTranscript; /* Record session transcript here */
161
- int atEof; /* True after connection closes */
161
+ int bOpen; /* True if connection is Open */
162
+ int bFatal; /* Error is fatal. Do not retry */
162163
char *zErr; /* Error message */
163164
Blob inbuf; /* Input buffer */
165
+ UrlData url; /* Address of the server */
164166
};
165167
166168
/* Allowed values for SmtpSession.smtpFlags */
167169
#define SMTP_TRACE_STDOUT 0x00001 /* Debugging info to console */
168170
#define SMTP_TRACE_FILE 0x00002 /* Debugging info to logFile */
@@ -180,86 +182,107 @@
180182
blob_reset(&pSession->inbuf);
181183
fossil_free(pSession->zHostname);
182184
fossil_free(pSession->zErr);
183185
fossil_free(pSession);
184186
}
187
+
188
+/*
189
+** Set an error message on the SmtpSession
190
+*/
191
+static void smtp_set_error(
192
+ SmtpSession *p, /* The SMTP context */
193
+ int bFatal, /* Fatal error. Reset and retry is pointless */
194
+ const char *zFormat, /* Error message. */
195
+ ...
196
+){
197
+ if( bFatal ) p->bFatal = 1;
198
+ if( p->zErr==0 ){
199
+ va_list ap;
200
+ va_start(ap, zFormat);
201
+ p->zErr = vmprintf(zFormat, ap);
202
+ va_end(ap);
203
+ }
204
+ if( p->bOpen ){
205
+ socket_close();
206
+ p->bOpen = 0;
207
+ }
208
+}
185209
186210
/*
187211
** Allocate a new SmtpSession object.
188212
**
189
-** Both zFrom and zDest must be specified.
190
-**
191
-** The ... arguments are in this order:
213
+** Both zFrom and zDest must be specified. smtpFlags may not contain
214
+** either SMTP_TRACE_FILE or SMTP_TRACE_BLOB as those settings must be
215
+** added by a subsequent call to smtp_session_config().
192216
**
193
-** SMTP_PORT: int
194
-** SMTP_TRACE_FILE: FILE*
195
-** SMTP_TRACE_BLOB: Blob*
217
+** The iPort option is ignored unless SMTP_PORT is set in smtpFlags
196218
*/
197219
SmtpSession *smtp_session_new(
198220
const char *zFrom, /* Domain for the client */
199221
const char *zDest, /* Domain of the server */
200222
u32 smtpFlags, /* Flags */
201
- ... /* Arguments depending on the flags */
223
+ int iPort /* TCP port if the SMTP_PORT flags is present */
202224
){
203225
SmtpSession *p;
204
- va_list ap;
205
- UrlData url;
206226
207227
p = fossil_malloc( sizeof(*p) );
208228
memset(p, 0, sizeof(*p));
209229
p->zFrom = zFrom;
210230
p->zDest = zDest;
211231
p->smtpFlags = smtpFlags;
212
- memset(&url, 0, sizeof(url));
213
- url.port = 25;
232
+ p->url.port = 25;
214233
blob_init(&p->inbuf, 0, 0);
215
- va_start(ap, smtpFlags);
216234
if( smtpFlags & SMTP_PORT ){
217
- url.port = va_arg(ap, int);
218
- }
219
- if( smtpFlags & SMTP_TRACE_FILE ){
220
- p->logFile = va_arg(ap, FILE*);
221
- }
222
- if( smtpFlags & SMTP_TRACE_BLOB ){
223
- p->pTranscript = va_arg(ap, Blob*);
224
- }
225
- va_end(ap);
235
+ p->url.port = iPort;
236
+ }
226237
if( (smtpFlags & SMTP_DIRECT)!=0 ){
227238
int i;
228239
p->zHostname = fossil_strdup(zDest);
229240
for(i=0; p->zHostname[i] && p->zHostname[i]!=':'; i++){}
230241
if( p->zHostname[i]==':' ){
231242
p->zHostname[i] = 0;
232
- url.port = atoi(&p->zHostname[i+1]);
243
+ p->url.port = atoi(&p->zHostname[i+1]);
233244
}
234245
}else{
235246
p->zHostname = smtp_mx_host(zDest);
236247
}
237248
if( p->zHostname==0 ){
238
- p->atEof = 1;
239
- p->zErr = mprintf("cannot locate SMTP server for \"%s\"", zDest);
249
+ smtp_set_error(p, 1, "cannot locate SMTP server for \"%s\"", zDest);
240250
return p;
241251
}
242
- url.name = p->zHostname;
252
+ p->url.name = p->zHostname;
243253
socket_global_init();
244
- if( socket_open(&url) ){
245
- p->atEof = 1;
246
- p->zErr = socket_errmsg();
247
- socket_close();
248
- }
254
+ p->bOpen = 0;
249255
return p;
250256
}
257
+
258
+/*
259
+** Configure debugging options on SmtpSession. Add all bits in
260
+** smtpFlags to the settings. The following bits can be added:
261
+**
262
+** SMTP_FLAG_FILE: In which case pArg is the FILE* pointer to use
263
+**
264
+** SMTP_FLAG_BLOB: In which case pArg is the Blob* poitner to use.
265
+*/
266
+void smtp_session_config(SmtpSession *p, u32 smtpFlags, void *pArg){
267
+ p->smtpFlags = smtpFlags;
268
+ if( smtpFlags & SMTP_TRACE_FILE ){
269
+ p->logFile = (FILE*)pArg;
270
+ }else if( smtpFlags & SMTP_TRACE_BLOB ){
271
+ p->pTranscript = (Blob*)pArg;
272
+ }
273
+}
251274
252275
/*
253276
** Send a single line of output the SMTP client to the server.
254277
*/
255278
static void smtp_send_line(SmtpSession *p, const char *zFormat, ...){
256279
Blob b = empty_blob;
257280
va_list ap;
258281
char *z;
259282
int n;
260
- if( p->atEof ) return;
283
+ if( !p->bOpen ) return;
261284
va_start(ap, zFormat);
262285
blob_vappendf(&b, zFormat, ap);
263286
va_end(ap);
264287
z = blob_buffer(&b);
265288
n = blob_size(&b);
@@ -291,11 +314,11 @@
291314
char *z = blob_buffer(&p->inbuf);
292315
int i = blob_tell(&p->inbuf);
293316
int nDelay = 0;
294317
if( i<n && z[n-1]=='\n' ){
295318
blob_line(&p->inbuf, in);
296
- }else if( p->atEof ){
319
+ }else if( !p->bOpen ){
297320
blob_init(in, 0, 0);
298321
}else{
299322
if( n>0 && i>=n ){
300323
blob_truncate(&p->inbuf, 0);
301324
blob_rewind(&p->inbuf);
@@ -314,13 +337,11 @@
314337
if( got==1000 ) continue;
315338
}
316339
nDelay++;
317340
if( nDelay>100 ){
318341
blob_init(in, 0, 0);
319
- p->zErr = mprintf("timeout");
320
- socket_close();
321
- p->atEof = 1;
342
+ smtp_set_error(p, 1, "client times out waiting on server response");
322343
return;
323344
}else{
324345
sqlite3_sleep(100);
325346
}
326347
}while( n<1 || z[n-1]!='\n' );
@@ -354,10 +375,11 @@
354375
){
355376
int n;
356377
char *z;
357378
blob_truncate(in, 0);
358379
smtp_recv_line(p, in);
380
+ blob_trim(in);
359381
z = blob_str(in);
360382
n = blob_size(in);
361383
if( z[0]=='#' ){
362384
*piCode = 0;
363385
*pbMore = 1;
@@ -375,45 +397,57 @@
375397
int smtp_client_quit(SmtpSession *p){
376398
Blob in = BLOB_INITIALIZER;
377399
int iCode = 0;
378400
int bMore = 0;
379401
char *zArg = 0;
380
- smtp_send_line(p, "QUIT\r\n");
381
- do{
382
- smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg);
383
- }while( bMore );
384
- p->atEof = 1;
385
- socket_close();
402
+ if( p->bOpen ){
403
+ smtp_send_line(p, "QUIT\r\n");
404
+ do{
405
+ smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg);
406
+ }while( bMore );
407
+ p->bOpen = 0;
408
+ socket_close();
409
+ }
386410
return 0;
387411
}
388412
389413
/*
390414
** Begin a client SMTP session. Wait for the initial 220 then send
391415
** the EHLO and wait for a 250.
392416
**
393417
** Return 0 on success and non-zero for a failure.
394418
*/
395
-int smtp_client_startup(SmtpSession *p){
419
+static int smtp_client_startup(SmtpSession *p){
396420
Blob in = BLOB_INITIALIZER;
397421
int iCode = 0;
398422
int bMore = 0;
399423
char *zArg = 0;
424
+ if( p==0 || p->bFatal ) return 1;
425
+ if( socket_open(&p->url) ){
426
+ smtp_set_error(p, 1, "can't open socket: %z", socket_errmsg());
427
+ return 1;
428
+ }
429
+ p->bOpen = 1;
400430
do{
401431
smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg);
402432
}while( bMore );
403433
if( iCode!=220 ){
434
+ smtp_set_error(p, 1, "conversation begins with: \"%d %s\"",iCode,zArg);
404435
smtp_client_quit(p);
405436
return 1;
406437
}
407438
smtp_send_line(p, "EHLO %s\r\n", p->zFrom);
408439
do{
409440
smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg);
410441
}while( bMore );
411442
if( iCode!=250 ){
443
+ smtp_set_error(p, 1, "reply to EHLO with: \"%d %s\"",iCode, zArg);
412444
smtp_client_quit(p);
413445
return 1;
414446
}
447
+ fossil_free(p->zErr);
448
+ p->zErr = 0;
415449
return 0;
416450
}
417451
418452
/*
419453
** COMMAND: test-smtp-probe
@@ -541,27 +575,40 @@
541575
int iCode = 0;
542576
int bMore = 0;
543577
char *zArg = 0;
544578
Blob in;
545579
blob_init(&in, 0, 0);
580
+ if( !p->bOpen ){
581
+ if( !p->bFatal ) smtp_client_startup(p);
582
+ if( !p->bOpen ) return 1;
583
+ }
546584
smtp_send_line(p, "MAIL FROM:<%s>\r\n", zFrom);
547585
do{
548586
smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg);
549587
}while( bMore );
550
- if( iCode!=250 ) return 1;
588
+ if( iCode!=250 ){
589
+ smtp_set_error(p, 0,"reply to MAIL FROM: \"%d %s\"",iCode,zArg);
590
+ return 1;
591
+ }
551592
for(i=0; i<nTo; i++){
552593
smtp_send_line(p, "RCPT TO:<%s>\r\n", azTo[i]);
553594
do{
554595
smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg);
555596
}while( bMore );
556
- if( iCode!=250 ) return 1;
597
+ if( iCode!=250 ){
598
+ smtp_set_error(p, 0,"reply to RCPT TO: \"%d %s\"",iCode,zArg);
599
+ return 1;
600
+ }
557601
}
558602
smtp_send_line(p, "DATA\r\n");
559603
do{
560604
smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg);
561605
}while( bMore );
562
- if( iCode!=354 ) return 1;
606
+ if( iCode!=354 ){
607
+ smtp_set_error(p, 0, "reply to DATA with: \"%d %s\"",iCode,zArg);
608
+ return 1;
609
+ }
563610
smtp_send_email_body(zMsg, socket_send, 0);
564611
if( p->smtpFlags & SMTP_TRACE_STDOUT ){
565612
fossil_print("C: # message content\nC: .\n");
566613
}
567614
if( p->smtpFlags & SMTP_TRACE_FILE ){
@@ -571,11 +618,15 @@
571618
blob_appendf(p->pTranscript, "C: # message content\nC: .\n");
572619
}
573620
do{
574621
smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg);
575622
}while( bMore );
576
- if( iCode!=250 ) return 1;
623
+ if( iCode!=250 ){
624
+ smtp_set_error(p, 0, "reply to end-of-DATA with: \"%d %s\"",
625
+ iCode, zArg);
626
+ return 1;
627
+ }
577628
return 0;
578629
}
579630
580631
/*
581632
** The input is a base email address of the form "local@domain".
@@ -636,14 +687,13 @@
636687
p = smtp_session_new(zFromDomain, zToDomain, smtpFlags, smtpPort);
637688
if( p->zErr ){
638689
fossil_fatal("%s", p->zErr);
639690
}
640691
fossil_print("Connection to \"%s\"\n", p->zHostname);
641
- smtp_client_startup(p);
642692
smtp_send_msg(p, zFrom, nTo, azTo, blob_str(&body));
643693
smtp_client_quit(p);
644694
if( p->zErr ){
645695
fossil_fatal("ERROR: %s\n", p->zErr);
646696
}
647697
smtp_session_free(p);
648698
blob_reset(&body);
649699
}
650700
--- src/smtp.c
+++ src/smtp.c
@@ -156,13 +156,15 @@
156 const char *zDest; /* Domain that will receive the email */
157 char *zHostname; /* Hostname of SMTP server for zDest */
158 u32 smtpFlags; /* Flags changing the operation */
159 FILE *logFile; /* Write session transcript to this log file */
160 Blob *pTranscript; /* Record session transcript here */
161 int atEof; /* True after connection closes */
 
162 char *zErr; /* Error message */
163 Blob inbuf; /* Input buffer */
 
164 };
165
166 /* Allowed values for SmtpSession.smtpFlags */
167 #define SMTP_TRACE_STDOUT 0x00001 /* Debugging info to console */
168 #define SMTP_TRACE_FILE 0x00002 /* Debugging info to logFile */
@@ -180,86 +182,107 @@
180 blob_reset(&pSession->inbuf);
181 fossil_free(pSession->zHostname);
182 fossil_free(pSession->zErr);
183 fossil_free(pSession);
184 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
186 /*
187 ** Allocate a new SmtpSession object.
188 **
189 ** Both zFrom and zDest must be specified.
190 **
191 ** The ... arguments are in this order:
192 **
193 ** SMTP_PORT: int
194 ** SMTP_TRACE_FILE: FILE*
195 ** SMTP_TRACE_BLOB: Blob*
196 */
197 SmtpSession *smtp_session_new(
198 const char *zFrom, /* Domain for the client */
199 const char *zDest, /* Domain of the server */
200 u32 smtpFlags, /* Flags */
201 ... /* Arguments depending on the flags */
202 ){
203 SmtpSession *p;
204 va_list ap;
205 UrlData url;
206
207 p = fossil_malloc( sizeof(*p) );
208 memset(p, 0, sizeof(*p));
209 p->zFrom = zFrom;
210 p->zDest = zDest;
211 p->smtpFlags = smtpFlags;
212 memset(&url, 0, sizeof(url));
213 url.port = 25;
214 blob_init(&p->inbuf, 0, 0);
215 va_start(ap, smtpFlags);
216 if( smtpFlags & SMTP_PORT ){
217 url.port = va_arg(ap, int);
218 }
219 if( smtpFlags & SMTP_TRACE_FILE ){
220 p->logFile = va_arg(ap, FILE*);
221 }
222 if( smtpFlags & SMTP_TRACE_BLOB ){
223 p->pTranscript = va_arg(ap, Blob*);
224 }
225 va_end(ap);
226 if( (smtpFlags & SMTP_DIRECT)!=0 ){
227 int i;
228 p->zHostname = fossil_strdup(zDest);
229 for(i=0; p->zHostname[i] && p->zHostname[i]!=':'; i++){}
230 if( p->zHostname[i]==':' ){
231 p->zHostname[i] = 0;
232 url.port = atoi(&p->zHostname[i+1]);
233 }
234 }else{
235 p->zHostname = smtp_mx_host(zDest);
236 }
237 if( p->zHostname==0 ){
238 p->atEof = 1;
239 p->zErr = mprintf("cannot locate SMTP server for \"%s\"", zDest);
240 return p;
241 }
242 url.name = p->zHostname;
243 socket_global_init();
244 if( socket_open(&url) ){
245 p->atEof = 1;
246 p->zErr = socket_errmsg();
247 socket_close();
248 }
249 return p;
250 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
251
252 /*
253 ** Send a single line of output the SMTP client to the server.
254 */
255 static void smtp_send_line(SmtpSession *p, const char *zFormat, ...){
256 Blob b = empty_blob;
257 va_list ap;
258 char *z;
259 int n;
260 if( p->atEof ) return;
261 va_start(ap, zFormat);
262 blob_vappendf(&b, zFormat, ap);
263 va_end(ap);
264 z = blob_buffer(&b);
265 n = blob_size(&b);
@@ -291,11 +314,11 @@
291 char *z = blob_buffer(&p->inbuf);
292 int i = blob_tell(&p->inbuf);
293 int nDelay = 0;
294 if( i<n && z[n-1]=='\n' ){
295 blob_line(&p->inbuf, in);
296 }else if( p->atEof ){
297 blob_init(in, 0, 0);
298 }else{
299 if( n>0 && i>=n ){
300 blob_truncate(&p->inbuf, 0);
301 blob_rewind(&p->inbuf);
@@ -314,13 +337,11 @@
314 if( got==1000 ) continue;
315 }
316 nDelay++;
317 if( nDelay>100 ){
318 blob_init(in, 0, 0);
319 p->zErr = mprintf("timeout");
320 socket_close();
321 p->atEof = 1;
322 return;
323 }else{
324 sqlite3_sleep(100);
325 }
326 }while( n<1 || z[n-1]!='\n' );
@@ -354,10 +375,11 @@
354 ){
355 int n;
356 char *z;
357 blob_truncate(in, 0);
358 smtp_recv_line(p, in);
 
359 z = blob_str(in);
360 n = blob_size(in);
361 if( z[0]=='#' ){
362 *piCode = 0;
363 *pbMore = 1;
@@ -375,45 +397,57 @@
375 int smtp_client_quit(SmtpSession *p){
376 Blob in = BLOB_INITIALIZER;
377 int iCode = 0;
378 int bMore = 0;
379 char *zArg = 0;
380 smtp_send_line(p, "QUIT\r\n");
381 do{
382 smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg);
383 }while( bMore );
384 p->atEof = 1;
385 socket_close();
 
 
386 return 0;
387 }
388
389 /*
390 ** Begin a client SMTP session. Wait for the initial 220 then send
391 ** the EHLO and wait for a 250.
392 **
393 ** Return 0 on success and non-zero for a failure.
394 */
395 int smtp_client_startup(SmtpSession *p){
396 Blob in = BLOB_INITIALIZER;
397 int iCode = 0;
398 int bMore = 0;
399 char *zArg = 0;
 
 
 
 
 
 
400 do{
401 smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg);
402 }while( bMore );
403 if( iCode!=220 ){
 
404 smtp_client_quit(p);
405 return 1;
406 }
407 smtp_send_line(p, "EHLO %s\r\n", p->zFrom);
408 do{
409 smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg);
410 }while( bMore );
411 if( iCode!=250 ){
 
412 smtp_client_quit(p);
413 return 1;
414 }
 
 
415 return 0;
416 }
417
418 /*
419 ** COMMAND: test-smtp-probe
@@ -541,27 +575,40 @@
541 int iCode = 0;
542 int bMore = 0;
543 char *zArg = 0;
544 Blob in;
545 blob_init(&in, 0, 0);
 
 
 
 
546 smtp_send_line(p, "MAIL FROM:<%s>\r\n", zFrom);
547 do{
548 smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg);
549 }while( bMore );
550 if( iCode!=250 ) return 1;
 
 
 
551 for(i=0; i<nTo; i++){
552 smtp_send_line(p, "RCPT TO:<%s>\r\n", azTo[i]);
553 do{
554 smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg);
555 }while( bMore );
556 if( iCode!=250 ) return 1;
 
 
 
557 }
558 smtp_send_line(p, "DATA\r\n");
559 do{
560 smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg);
561 }while( bMore );
562 if( iCode!=354 ) return 1;
 
 
 
563 smtp_send_email_body(zMsg, socket_send, 0);
564 if( p->smtpFlags & SMTP_TRACE_STDOUT ){
565 fossil_print("C: # message content\nC: .\n");
566 }
567 if( p->smtpFlags & SMTP_TRACE_FILE ){
@@ -571,11 +618,15 @@
571 blob_appendf(p->pTranscript, "C: # message content\nC: .\n");
572 }
573 do{
574 smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg);
575 }while( bMore );
576 if( iCode!=250 ) return 1;
 
 
 
 
577 return 0;
578 }
579
580 /*
581 ** The input is a base email address of the form "local@domain".
@@ -636,14 +687,13 @@
636 p = smtp_session_new(zFromDomain, zToDomain, smtpFlags, smtpPort);
637 if( p->zErr ){
638 fossil_fatal("%s", p->zErr);
639 }
640 fossil_print("Connection to \"%s\"\n", p->zHostname);
641 smtp_client_startup(p);
642 smtp_send_msg(p, zFrom, nTo, azTo, blob_str(&body));
643 smtp_client_quit(p);
644 if( p->zErr ){
645 fossil_fatal("ERROR: %s\n", p->zErr);
646 }
647 smtp_session_free(p);
648 blob_reset(&body);
649 }
650
--- src/smtp.c
+++ src/smtp.c
@@ -156,13 +156,15 @@
156 const char *zDest; /* Domain that will receive the email */
157 char *zHostname; /* Hostname of SMTP server for zDest */
158 u32 smtpFlags; /* Flags changing the operation */
159 FILE *logFile; /* Write session transcript to this log file */
160 Blob *pTranscript; /* Record session transcript here */
161 int bOpen; /* True if connection is Open */
162 int bFatal; /* Error is fatal. Do not retry */
163 char *zErr; /* Error message */
164 Blob inbuf; /* Input buffer */
165 UrlData url; /* Address of the server */
166 };
167
168 /* Allowed values for SmtpSession.smtpFlags */
169 #define SMTP_TRACE_STDOUT 0x00001 /* Debugging info to console */
170 #define SMTP_TRACE_FILE 0x00002 /* Debugging info to logFile */
@@ -180,86 +182,107 @@
182 blob_reset(&pSession->inbuf);
183 fossil_free(pSession->zHostname);
184 fossil_free(pSession->zErr);
185 fossil_free(pSession);
186 }
187
188 /*
189 ** Set an error message on the SmtpSession
190 */
191 static void smtp_set_error(
192 SmtpSession *p, /* The SMTP context */
193 int bFatal, /* Fatal error. Reset and retry is pointless */
194 const char *zFormat, /* Error message. */
195 ...
196 ){
197 if( bFatal ) p->bFatal = 1;
198 if( p->zErr==0 ){
199 va_list ap;
200 va_start(ap, zFormat);
201 p->zErr = vmprintf(zFormat, ap);
202 va_end(ap);
203 }
204 if( p->bOpen ){
205 socket_close();
206 p->bOpen = 0;
207 }
208 }
209
210 /*
211 ** Allocate a new SmtpSession object.
212 **
213 ** Both zFrom and zDest must be specified. smtpFlags may not contain
214 ** either SMTP_TRACE_FILE or SMTP_TRACE_BLOB as those settings must be
215 ** added by a subsequent call to smtp_session_config().
216 **
217 ** The iPort option is ignored unless SMTP_PORT is set in smtpFlags
 
 
218 */
219 SmtpSession *smtp_session_new(
220 const char *zFrom, /* Domain for the client */
221 const char *zDest, /* Domain of the server */
222 u32 smtpFlags, /* Flags */
223 int iPort /* TCP port if the SMTP_PORT flags is present */
224 ){
225 SmtpSession *p;
 
 
226
227 p = fossil_malloc( sizeof(*p) );
228 memset(p, 0, sizeof(*p));
229 p->zFrom = zFrom;
230 p->zDest = zDest;
231 p->smtpFlags = smtpFlags;
232 p->url.port = 25;
 
233 blob_init(&p->inbuf, 0, 0);
 
234 if( smtpFlags & SMTP_PORT ){
235 p->url.port = iPort;
236 }
 
 
 
 
 
 
 
237 if( (smtpFlags & SMTP_DIRECT)!=0 ){
238 int i;
239 p->zHostname = fossil_strdup(zDest);
240 for(i=0; p->zHostname[i] && p->zHostname[i]!=':'; i++){}
241 if( p->zHostname[i]==':' ){
242 p->zHostname[i] = 0;
243 p->url.port = atoi(&p->zHostname[i+1]);
244 }
245 }else{
246 p->zHostname = smtp_mx_host(zDest);
247 }
248 if( p->zHostname==0 ){
249 smtp_set_error(p, 1, "cannot locate SMTP server for \"%s\"", zDest);
 
250 return p;
251 }
252 p->url.name = p->zHostname;
253 socket_global_init();
254 p->bOpen = 0;
 
 
 
 
255 return p;
256 }
257
258 /*
259 ** Configure debugging options on SmtpSession. Add all bits in
260 ** smtpFlags to the settings. The following bits can be added:
261 **
262 ** SMTP_FLAG_FILE: In which case pArg is the FILE* pointer to use
263 **
264 ** SMTP_FLAG_BLOB: In which case pArg is the Blob* poitner to use.
265 */
266 void smtp_session_config(SmtpSession *p, u32 smtpFlags, void *pArg){
267 p->smtpFlags = smtpFlags;
268 if( smtpFlags & SMTP_TRACE_FILE ){
269 p->logFile = (FILE*)pArg;
270 }else if( smtpFlags & SMTP_TRACE_BLOB ){
271 p->pTranscript = (Blob*)pArg;
272 }
273 }
274
275 /*
276 ** Send a single line of output the SMTP client to the server.
277 */
278 static void smtp_send_line(SmtpSession *p, const char *zFormat, ...){
279 Blob b = empty_blob;
280 va_list ap;
281 char *z;
282 int n;
283 if( !p->bOpen ) return;
284 va_start(ap, zFormat);
285 blob_vappendf(&b, zFormat, ap);
286 va_end(ap);
287 z = blob_buffer(&b);
288 n = blob_size(&b);
@@ -291,11 +314,11 @@
314 char *z = blob_buffer(&p->inbuf);
315 int i = blob_tell(&p->inbuf);
316 int nDelay = 0;
317 if( i<n && z[n-1]=='\n' ){
318 blob_line(&p->inbuf, in);
319 }else if( !p->bOpen ){
320 blob_init(in, 0, 0);
321 }else{
322 if( n>0 && i>=n ){
323 blob_truncate(&p->inbuf, 0);
324 blob_rewind(&p->inbuf);
@@ -314,13 +337,11 @@
337 if( got==1000 ) continue;
338 }
339 nDelay++;
340 if( nDelay>100 ){
341 blob_init(in, 0, 0);
342 smtp_set_error(p, 1, "client times out waiting on server response");
 
 
343 return;
344 }else{
345 sqlite3_sleep(100);
346 }
347 }while( n<1 || z[n-1]!='\n' );
@@ -354,10 +375,11 @@
375 ){
376 int n;
377 char *z;
378 blob_truncate(in, 0);
379 smtp_recv_line(p, in);
380 blob_trim(in);
381 z = blob_str(in);
382 n = blob_size(in);
383 if( z[0]=='#' ){
384 *piCode = 0;
385 *pbMore = 1;
@@ -375,45 +397,57 @@
397 int smtp_client_quit(SmtpSession *p){
398 Blob in = BLOB_INITIALIZER;
399 int iCode = 0;
400 int bMore = 0;
401 char *zArg = 0;
402 if( p->bOpen ){
403 smtp_send_line(p, "QUIT\r\n");
404 do{
405 smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg);
406 }while( bMore );
407 p->bOpen = 0;
408 socket_close();
409 }
410 return 0;
411 }
412
413 /*
414 ** Begin a client SMTP session. Wait for the initial 220 then send
415 ** the EHLO and wait for a 250.
416 **
417 ** Return 0 on success and non-zero for a failure.
418 */
419 static int smtp_client_startup(SmtpSession *p){
420 Blob in = BLOB_INITIALIZER;
421 int iCode = 0;
422 int bMore = 0;
423 char *zArg = 0;
424 if( p==0 || p->bFatal ) return 1;
425 if( socket_open(&p->url) ){
426 smtp_set_error(p, 1, "can't open socket: %z", socket_errmsg());
427 return 1;
428 }
429 p->bOpen = 1;
430 do{
431 smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg);
432 }while( bMore );
433 if( iCode!=220 ){
434 smtp_set_error(p, 1, "conversation begins with: \"%d %s\"",iCode,zArg);
435 smtp_client_quit(p);
436 return 1;
437 }
438 smtp_send_line(p, "EHLO %s\r\n", p->zFrom);
439 do{
440 smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg);
441 }while( bMore );
442 if( iCode!=250 ){
443 smtp_set_error(p, 1, "reply to EHLO with: \"%d %s\"",iCode, zArg);
444 smtp_client_quit(p);
445 return 1;
446 }
447 fossil_free(p->zErr);
448 p->zErr = 0;
449 return 0;
450 }
451
452 /*
453 ** COMMAND: test-smtp-probe
@@ -541,27 +575,40 @@
575 int iCode = 0;
576 int bMore = 0;
577 char *zArg = 0;
578 Blob in;
579 blob_init(&in, 0, 0);
580 if( !p->bOpen ){
581 if( !p->bFatal ) smtp_client_startup(p);
582 if( !p->bOpen ) return 1;
583 }
584 smtp_send_line(p, "MAIL FROM:<%s>\r\n", zFrom);
585 do{
586 smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg);
587 }while( bMore );
588 if( iCode!=250 ){
589 smtp_set_error(p, 0,"reply to MAIL FROM: \"%d %s\"",iCode,zArg);
590 return 1;
591 }
592 for(i=0; i<nTo; i++){
593 smtp_send_line(p, "RCPT TO:<%s>\r\n", azTo[i]);
594 do{
595 smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg);
596 }while( bMore );
597 if( iCode!=250 ){
598 smtp_set_error(p, 0,"reply to RCPT TO: \"%d %s\"",iCode,zArg);
599 return 1;
600 }
601 }
602 smtp_send_line(p, "DATA\r\n");
603 do{
604 smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg);
605 }while( bMore );
606 if( iCode!=354 ){
607 smtp_set_error(p, 0, "reply to DATA with: \"%d %s\"",iCode,zArg);
608 return 1;
609 }
610 smtp_send_email_body(zMsg, socket_send, 0);
611 if( p->smtpFlags & SMTP_TRACE_STDOUT ){
612 fossil_print("C: # message content\nC: .\n");
613 }
614 if( p->smtpFlags & SMTP_TRACE_FILE ){
@@ -571,11 +618,15 @@
618 blob_appendf(p->pTranscript, "C: # message content\nC: .\n");
619 }
620 do{
621 smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg);
622 }while( bMore );
623 if( iCode!=250 ){
624 smtp_set_error(p, 0, "reply to end-of-DATA with: \"%d %s\"",
625 iCode, zArg);
626 return 1;
627 }
628 return 0;
629 }
630
631 /*
632 ** The input is a base email address of the form "local@domain".
@@ -636,14 +687,13 @@
687 p = smtp_session_new(zFromDomain, zToDomain, smtpFlags, smtpPort);
688 if( p->zErr ){
689 fossil_fatal("%s", p->zErr);
690 }
691 fossil_print("Connection to \"%s\"\n", p->zHostname);
 
692 smtp_send_msg(p, zFrom, nTo, azTo, blob_str(&body));
693 smtp_client_quit(p);
694 if( p->zErr ){
695 fossil_fatal("ERROR: %s\n", p->zErr);
696 }
697 smtp_session_free(p);
698 blob_reset(&body);
699 }
700
--- src/sorttable.js
+++ src/sorttable.js
@@ -9,11 +9,11 @@
99
** function. Example:
1010
**
1111
** <table class='sortable' data-column-types='tnkx' data-init-sort='2'>
1212
**
1313
** Column data types are determined by the data-column-types attribute of
14
-** the table. The value of data-column-types is a string where each
14
+** the table. The value of data-column-types is a string where each
1515
** character of the string represents a datatype for one column in the
1616
** table.
1717
**
1818
** t Sort by text
1919
** n Sort numerically
@@ -86,18 +86,22 @@
8686
hdrCell.className = clsName;
8787
}
8888
}
8989
this.sortText = function(a,b) {
9090
var i = thisObject.sortIndex;
91
+ if (a.cells.length<=i) return -1; /* see ticket 59d699710b1ab5d4 */
92
+ if (b.cells.length<=i) return 1;
9193
aa = a.cells[i].textContent.replace(/^\W+/,'').toLowerCase();
9294
bb = b.cells[i].textContent.replace(/^\W+/,'').toLowerCase();
9395
if(aa<bb) return -1;
9496
if(aa==bb) return a.rowIndex-b.rowIndex;
9597
return 1;
9698
}
9799
this.sortReverseText = function(a,b) {
98100
var i = thisObject.sortIndex;
101
+ if (a.cells.length<=i) return 1; /* see ticket 59d699710b1ab5d4 */
102
+ if (b.cells.length<=i) return -1;
99103
aa = a.cells[i].textContent.replace(/^\W+/,'').toLowerCase();
100104
bb = b.cells[i].textContent.replace(/^\W+/,'').toLowerCase();
101105
if(aa<bb) return +1;
102106
if(aa==bb) return a.rowIndex-b.rowIndex;
103107
return -1;
104108
--- src/sorttable.js
+++ src/sorttable.js
@@ -9,11 +9,11 @@
9 ** function. Example:
10 **
11 ** <table class='sortable' data-column-types='tnkx' data-init-sort='2'>
12 **
13 ** Column data types are determined by the data-column-types attribute of
14 ** the table. The value of data-column-types is a string where each
15 ** character of the string represents a datatype for one column in the
16 ** table.
17 **
18 ** t Sort by text
19 ** n Sort numerically
@@ -86,18 +86,22 @@
86 hdrCell.className = clsName;
87 }
88 }
89 this.sortText = function(a,b) {
90 var i = thisObject.sortIndex;
 
 
91 aa = a.cells[i].textContent.replace(/^\W+/,'').toLowerCase();
92 bb = b.cells[i].textContent.replace(/^\W+/,'').toLowerCase();
93 if(aa<bb) return -1;
94 if(aa==bb) return a.rowIndex-b.rowIndex;
95 return 1;
96 }
97 this.sortReverseText = function(a,b) {
98 var i = thisObject.sortIndex;
 
 
99 aa = a.cells[i].textContent.replace(/^\W+/,'').toLowerCase();
100 bb = b.cells[i].textContent.replace(/^\W+/,'').toLowerCase();
101 if(aa<bb) return +1;
102 if(aa==bb) return a.rowIndex-b.rowIndex;
103 return -1;
104
--- src/sorttable.js
+++ src/sorttable.js
@@ -9,11 +9,11 @@
9 ** function. Example:
10 **
11 ** <table class='sortable' data-column-types='tnkx' data-init-sort='2'>
12 **
13 ** Column data types are determined by the data-column-types attribute of
14 ** the table. The value of data-column-types is a string where each
15 ** character of the string represents a datatype for one column in the
16 ** table.
17 **
18 ** t Sort by text
19 ** n Sort numerically
@@ -86,18 +86,22 @@
86 hdrCell.className = clsName;
87 }
88 }
89 this.sortText = function(a,b) {
90 var i = thisObject.sortIndex;
91 if (a.cells.length<=i) return -1; /* see ticket 59d699710b1ab5d4 */
92 if (b.cells.length<=i) return 1;
93 aa = a.cells[i].textContent.replace(/^\W+/,'').toLowerCase();
94 bb = b.cells[i].textContent.replace(/^\W+/,'').toLowerCase();
95 if(aa<bb) return -1;
96 if(aa==bb) return a.rowIndex-b.rowIndex;
97 return 1;
98 }
99 this.sortReverseText = function(a,b) {
100 var i = thisObject.sortIndex;
101 if (a.cells.length<=i) return 1; /* see ticket 59d699710b1ab5d4 */
102 if (b.cells.length<=i) return -1;
103 aa = a.cells[i].textContent.replace(/^\W+/,'').toLowerCase();
104 bb = b.cells[i].textContent.replace(/^\W+/,'').toLowerCase();
105 if(aa<bb) return +1;
106 if(aa==bb) return a.rowIndex-b.rowIndex;
107 return -1;
108
+8 -2
--- src/stash.c
+++ src/stash.c
@@ -259,10 +259,11 @@
259259
const char *zComment; /* Comment to add to the stash */
260260
int stashid; /* ID of the new stash */
261261
int vid; /* Current check-out */
262262
263263
zComment = find_option("comment", "m", 1);
264
+ (void)fossil_text_editor();
264265
verify_all_options();
265266
if( zComment==0 ){
266267
Blob prompt; /* Prompt for stash comment */
267268
Blob comment; /* User comment reply */
268269
#if defined(_WIN32) || defined(__CYGWIN__)
@@ -508,19 +509,24 @@
508509
** COMMAND: stash
509510
**
510511
** Usage: %fossil stash SUBCOMMAND ARGS...
511512
**
512513
** > fossil stash
513
-** > fossil stash save ?-m|--comment COMMENT? ?FILES...?
514
-** > fossil stash snapshot ?-m|--comment COMMENT? ?FILES...?
514
+** > fossil stash save ?FILES...?
515
+** > fossil stash snapshot ?FILES...?
515516
**
516517
** Save the current changes in the working tree as a new stash.
517518
** Then revert the changes back to the last check-in. If FILES
518519
** are listed, then only stash and revert the named files. The
519520
** "save" verb can be omitted if and only if there are no other
520521
** arguments. The "snapshot" verb works the same as "save" but
521522
** omits the revert, keeping the check-out unchanged.
523
+**
524
+** Options:
525
+** --editor NAME Use the NAME editor to enter comment
526
+** -m|--comment COMMENT Comment text for the new stash
527
+**
522528
**
523529
** > fossil stash list|ls ?-v|--verbose? ?-W|--width NUM?
524530
**
525531
** List all changes sets currently stashed. Show information about
526532
** individual files in each changeset if -v or --verbose is used.
527533
--- src/stash.c
+++ src/stash.c
@@ -259,10 +259,11 @@
259 const char *zComment; /* Comment to add to the stash */
260 int stashid; /* ID of the new stash */
261 int vid; /* Current check-out */
262
263 zComment = find_option("comment", "m", 1);
 
264 verify_all_options();
265 if( zComment==0 ){
266 Blob prompt; /* Prompt for stash comment */
267 Blob comment; /* User comment reply */
268 #if defined(_WIN32) || defined(__CYGWIN__)
@@ -508,19 +509,24 @@
508 ** COMMAND: stash
509 **
510 ** Usage: %fossil stash SUBCOMMAND ARGS...
511 **
512 ** > fossil stash
513 ** > fossil stash save ?-m|--comment COMMENT? ?FILES...?
514 ** > fossil stash snapshot ?-m|--comment COMMENT? ?FILES...?
515 **
516 ** Save the current changes in the working tree as a new stash.
517 ** Then revert the changes back to the last check-in. If FILES
518 ** are listed, then only stash and revert the named files. The
519 ** "save" verb can be omitted if and only if there are no other
520 ** arguments. The "snapshot" verb works the same as "save" but
521 ** omits the revert, keeping the check-out unchanged.
 
 
 
 
 
522 **
523 ** > fossil stash list|ls ?-v|--verbose? ?-W|--width NUM?
524 **
525 ** List all changes sets currently stashed. Show information about
526 ** individual files in each changeset if -v or --verbose is used.
527
--- src/stash.c
+++ src/stash.c
@@ -259,10 +259,11 @@
259 const char *zComment; /* Comment to add to the stash */
260 int stashid; /* ID of the new stash */
261 int vid; /* Current check-out */
262
263 zComment = find_option("comment", "m", 1);
264 (void)fossil_text_editor();
265 verify_all_options();
266 if( zComment==0 ){
267 Blob prompt; /* Prompt for stash comment */
268 Blob comment; /* User comment reply */
269 #if defined(_WIN32) || defined(__CYGWIN__)
@@ -508,19 +509,24 @@
509 ** COMMAND: stash
510 **
511 ** Usage: %fossil stash SUBCOMMAND ARGS...
512 **
513 ** > fossil stash
514 ** > fossil stash save ?FILES...?
515 ** > fossil stash snapshot ?FILES...?
516 **
517 ** Save the current changes in the working tree as a new stash.
518 ** Then revert the changes back to the last check-in. If FILES
519 ** are listed, then only stash and revert the named files. The
520 ** "save" verb can be omitted if and only if there are no other
521 ** arguments. The "snapshot" verb works the same as "save" but
522 ** omits the revert, keeping the check-out unchanged.
523 **
524 ** Options:
525 ** --editor NAME Use the NAME editor to enter comment
526 ** -m|--comment COMMENT Comment text for the new stash
527 **
528 **
529 ** > fossil stash list|ls ?-v|--verbose? ?-W|--width NUM?
530 **
531 ** List all changes sets currently stashed. Show information about
532 ** individual files in each changeset if -v or --verbose is used.
533
+1 -1
--- src/stat.c
+++ src/stat.c
@@ -166,11 +166,11 @@
166166
style_submenu_element("Artifacts", "bloblist");
167167
if( sqlite3_compileoption_used("ENABLE_DBSTAT_VTAB") ){
168168
style_submenu_element("Table Sizes", "repo-tabsize");
169169
}
170170
if( g.perm.Admin || g.perm.Setup || db_get_boolean("test_env_enable",0) ){
171
- style_submenu_element("Environment", "test_env");
171
+ style_submenu_element("Environment", "test-env");
172172
}
173173
@ <table class="label-value">
174174
fsize = file_size(g.zRepositoryName, ExtFILE);
175175
@ <tr><th>Repository&nbsp;Size:</th><td>%,lld(fsize) bytes</td>
176176
@ </td></tr>
177177
--- src/stat.c
+++ src/stat.c
@@ -166,11 +166,11 @@
166 style_submenu_element("Artifacts", "bloblist");
167 if( sqlite3_compileoption_used("ENABLE_DBSTAT_VTAB") ){
168 style_submenu_element("Table Sizes", "repo-tabsize");
169 }
170 if( g.perm.Admin || g.perm.Setup || db_get_boolean("test_env_enable",0) ){
171 style_submenu_element("Environment", "test_env");
172 }
173 @ <table class="label-value">
174 fsize = file_size(g.zRepositoryName, ExtFILE);
175 @ <tr><th>Repository&nbsp;Size:</th><td>%,lld(fsize) bytes</td>
176 @ </td></tr>
177
--- src/stat.c
+++ src/stat.c
@@ -166,11 +166,11 @@
166 style_submenu_element("Artifacts", "bloblist");
167 if( sqlite3_compileoption_used("ENABLE_DBSTAT_VTAB") ){
168 style_submenu_element("Table Sizes", "repo-tabsize");
169 }
170 if( g.perm.Admin || g.perm.Setup || db_get_boolean("test_env_enable",0) ){
171 style_submenu_element("Environment", "test-env");
172 }
173 @ <table class="label-value">
174 fsize = file_size(g.zRepositoryName, ExtFILE);
175 @ <tr><th>Repository&nbsp;Size:</th><td>%,lld(fsize) bytes</td>
176 @ </td></tr>
177
--- src/statrep.c
+++ src/statrep.c
@@ -859,10 +859,13 @@
859859
** * f (forum post)
860860
** * w (wiki page change)
861861
** * t (ticket change)
862862
** * g (tag added or removed)
863863
** Defaulting to all event types.
864
+** from=DATETIME Consider only events after this timestamp (requires to)
865
+** to=DATETIME Consider only events before this timestamp (requires from)
866
+**
864867
**
865868
** The view-specific query parameters include:
866869
**
867870
** view=byweek:
868871
**
869872
--- src/statrep.c
+++ src/statrep.c
@@ -859,10 +859,13 @@
859 ** * f (forum post)
860 ** * w (wiki page change)
861 ** * t (ticket change)
862 ** * g (tag added or removed)
863 ** Defaulting to all event types.
 
 
 
864 **
865 ** The view-specific query parameters include:
866 **
867 ** view=byweek:
868 **
869
--- src/statrep.c
+++ src/statrep.c
@@ -859,10 +859,13 @@
859 ** * f (forum post)
860 ** * w (wiki page change)
861 ** * t (ticket change)
862 ** * g (tag added or removed)
863 ** Defaulting to all event types.
864 ** from=DATETIME Consider only events after this timestamp (requires to)
865 ** to=DATETIME Consider only events before this timestamp (requires from)
866 **
867 **
868 ** The view-specific query parameters include:
869 **
870 ** view=byweek:
871 **
872
+9 -7
--- src/style.c
+++ src/style.c
@@ -744,13 +744,14 @@
744744
** is evaluated before the header is rendered).
745745
*/
746746
Th_MaybeStore("default_csp", zDfltCsp);
747747
fossil_free(zDfltCsp);
748748
Th_Store("nonce", zNonce);
749
- Th_Store("project_name", db_get("project-name","Unnamed Fossil Project"));
750
- Th_Store("project_description", db_get("project-description",""));
751
- if( zTitle ) Th_Store("title", zTitle);
749
+ Th_StoreUnsafe("project_name",
750
+ db_get("project-name","Unnamed Fossil Project"));
751
+ Th_StoreUnsafe("project_description", db_get("project-description",""));
752
+ if( zTitle ) Th_Store("title", html_lookalike(zTitle,-1));
752753
Th_Store("baseurl", g.zBaseURL);
753754
Th_Store("secureurl", fossil_wants_https(1)? g.zHttpsURL: g.zBaseURL);
754755
Th_Store("home", g.zTop);
755756
Th_Store("index_page", db_get("index-page","/home"));
756757
if( local_zCurrentPage==0 ) style_set_current_page("%T", g.zPath);
@@ -772,11 +773,11 @@
772773
Th_Store("mainmenu", style_get_mainmenu());
773774
stylesheet_url_var();
774775
image_url_var("logo");
775776
image_url_var("background");
776777
if( !login_is_nobody() ){
777
- Th_Store("login", g.zLogin);
778
+ Th_Store("login", html_lookalike(g.zLogin,-1));
778779
}
779780
Th_MaybeStore("current_feature", feature_from_page_path(local_zCurrentPage) );
780781
if( g.ftntsIssues[0] || g.ftntsIssues[1] ||
781782
g.ftntsIssues[2] || g.ftntsIssues[3] ){
782783
char buf[80];
@@ -1382,11 +1383,12 @@
13821383
@ </form>
13831384
style_finish_page();
13841385
}
13851386
13861387
/*
1387
-** WEBPAGE: test_env
1388
+** WEBPAGE: test-env
1389
+** WEBPAGE: test_env alias
13881390
**
13891391
** Display CGI-variables and other aspects of the run-time
13901392
** environment, for debugging and trouble-shooting purposes.
13911393
*/
13921394
void page_test_env(void){
@@ -1445,11 +1447,11 @@
14451447
**
14461448
** For administators, or if the test_env_enable setting is true, then
14471449
** details of the request environment are displayed. Otherwise, just
14481450
** the error message is shown.
14491451
**
1450
-** If zFormat is an empty string, then this is the /test_env page.
1452
+** If zFormat is an empty string, then this is the /test-env page.
14511453
*/
14521454
void webpage_error(const char *zFormat, ...){
14531455
int showAll = 0;
14541456
char *zErr = 0;
14551457
int isAuth = 0;
@@ -1545,11 +1547,11 @@
15451547
}
15461548
@ <hr>
15471549
P("HTTP_USER_AGENT");
15481550
P("SERVER_SOFTWARE");
15491551
cgi_print_all(showAll, 0, 0);
1550
- @ <p><form method="POST" action="%R/test_env">
1552
+ @ <p><form method="POST" action="%R/test-env">
15511553
@ <input type="hidden" name="showall" value="%d(showAll)">
15521554
@ <input type="submit" name="post-test-button" value="POST Test">
15531555
@ </form>
15541556
if( showAll && blob_size(&g.httpHeader)>0 ){
15551557
@ <hr>
15561558
--- src/style.c
+++ src/style.c
@@ -744,13 +744,14 @@
744 ** is evaluated before the header is rendered).
745 */
746 Th_MaybeStore("default_csp", zDfltCsp);
747 fossil_free(zDfltCsp);
748 Th_Store("nonce", zNonce);
749 Th_Store("project_name", db_get("project-name","Unnamed Fossil Project"));
750 Th_Store("project_description", db_get("project-description",""));
751 if( zTitle ) Th_Store("title", zTitle);
 
752 Th_Store("baseurl", g.zBaseURL);
753 Th_Store("secureurl", fossil_wants_https(1)? g.zHttpsURL: g.zBaseURL);
754 Th_Store("home", g.zTop);
755 Th_Store("index_page", db_get("index-page","/home"));
756 if( local_zCurrentPage==0 ) style_set_current_page("%T", g.zPath);
@@ -772,11 +773,11 @@
772 Th_Store("mainmenu", style_get_mainmenu());
773 stylesheet_url_var();
774 image_url_var("logo");
775 image_url_var("background");
776 if( !login_is_nobody() ){
777 Th_Store("login", g.zLogin);
778 }
779 Th_MaybeStore("current_feature", feature_from_page_path(local_zCurrentPage) );
780 if( g.ftntsIssues[0] || g.ftntsIssues[1] ||
781 g.ftntsIssues[2] || g.ftntsIssues[3] ){
782 char buf[80];
@@ -1382,11 +1383,12 @@
1382 @ </form>
1383 style_finish_page();
1384 }
1385
1386 /*
1387 ** WEBPAGE: test_env
 
1388 **
1389 ** Display CGI-variables and other aspects of the run-time
1390 ** environment, for debugging and trouble-shooting purposes.
1391 */
1392 void page_test_env(void){
@@ -1445,11 +1447,11 @@
1445 **
1446 ** For administators, or if the test_env_enable setting is true, then
1447 ** details of the request environment are displayed. Otherwise, just
1448 ** the error message is shown.
1449 **
1450 ** If zFormat is an empty string, then this is the /test_env page.
1451 */
1452 void webpage_error(const char *zFormat, ...){
1453 int showAll = 0;
1454 char *zErr = 0;
1455 int isAuth = 0;
@@ -1545,11 +1547,11 @@
1545 }
1546 @ <hr>
1547 P("HTTP_USER_AGENT");
1548 P("SERVER_SOFTWARE");
1549 cgi_print_all(showAll, 0, 0);
1550 @ <p><form method="POST" action="%R/test_env">
1551 @ <input type="hidden" name="showall" value="%d(showAll)">
1552 @ <input type="submit" name="post-test-button" value="POST Test">
1553 @ </form>
1554 if( showAll && blob_size(&g.httpHeader)>0 ){
1555 @ <hr>
1556
--- src/style.c
+++ src/style.c
@@ -744,13 +744,14 @@
744 ** is evaluated before the header is rendered).
745 */
746 Th_MaybeStore("default_csp", zDfltCsp);
747 fossil_free(zDfltCsp);
748 Th_Store("nonce", zNonce);
749 Th_StoreUnsafe("project_name",
750 db_get("project-name","Unnamed Fossil Project"));
751 Th_StoreUnsafe("project_description", db_get("project-description",""));
752 if( zTitle ) Th_Store("title", html_lookalike(zTitle,-1));
753 Th_Store("baseurl", g.zBaseURL);
754 Th_Store("secureurl", fossil_wants_https(1)? g.zHttpsURL: g.zBaseURL);
755 Th_Store("home", g.zTop);
756 Th_Store("index_page", db_get("index-page","/home"));
757 if( local_zCurrentPage==0 ) style_set_current_page("%T", g.zPath);
@@ -772,11 +773,11 @@
773 Th_Store("mainmenu", style_get_mainmenu());
774 stylesheet_url_var();
775 image_url_var("logo");
776 image_url_var("background");
777 if( !login_is_nobody() ){
778 Th_Store("login", html_lookalike(g.zLogin,-1));
779 }
780 Th_MaybeStore("current_feature", feature_from_page_path(local_zCurrentPage) );
781 if( g.ftntsIssues[0] || g.ftntsIssues[1] ||
782 g.ftntsIssues[2] || g.ftntsIssues[3] ){
783 char buf[80];
@@ -1382,11 +1383,12 @@
1383 @ </form>
1384 style_finish_page();
1385 }
1386
1387 /*
1388 ** WEBPAGE: test-env
1389 ** WEBPAGE: test_env alias
1390 **
1391 ** Display CGI-variables and other aspects of the run-time
1392 ** environment, for debugging and trouble-shooting purposes.
1393 */
1394 void page_test_env(void){
@@ -1445,11 +1447,11 @@
1447 **
1448 ** For administators, or if the test_env_enable setting is true, then
1449 ** details of the request environment are displayed. Otherwise, just
1450 ** the error message is shown.
1451 **
1452 ** If zFormat is an empty string, then this is the /test-env page.
1453 */
1454 void webpage_error(const char *zFormat, ...){
1455 int showAll = 0;
1456 char *zErr = 0;
1457 int isAuth = 0;
@@ -1545,11 +1547,11 @@
1547 }
1548 @ <hr>
1549 P("HTTP_USER_AGENT");
1550 P("SERVER_SOFTWARE");
1551 cgi_print_all(showAll, 0, 0);
1552 @ <p><form method="POST" action="%R/test-env">
1553 @ <input type="hidden" name="showall" value="%d(showAll)">
1554 @ <input type="submit" name="post-test-button" value="POST Test">
1555 @ </form>
1556 if( showAll && blob_size(&g.httpHeader)>0 ){
1557 @ <hr>
1558
+9 -7
--- src/style.c
+++ src/style.c
@@ -744,13 +744,14 @@
744744
** is evaluated before the header is rendered).
745745
*/
746746
Th_MaybeStore("default_csp", zDfltCsp);
747747
fossil_free(zDfltCsp);
748748
Th_Store("nonce", zNonce);
749
- Th_Store("project_name", db_get("project-name","Unnamed Fossil Project"));
750
- Th_Store("project_description", db_get("project-description",""));
751
- if( zTitle ) Th_Store("title", zTitle);
749
+ Th_StoreUnsafe("project_name",
750
+ db_get("project-name","Unnamed Fossil Project"));
751
+ Th_StoreUnsafe("project_description", db_get("project-description",""));
752
+ if( zTitle ) Th_Store("title", html_lookalike(zTitle,-1));
752753
Th_Store("baseurl", g.zBaseURL);
753754
Th_Store("secureurl", fossil_wants_https(1)? g.zHttpsURL: g.zBaseURL);
754755
Th_Store("home", g.zTop);
755756
Th_Store("index_page", db_get("index-page","/home"));
756757
if( local_zCurrentPage==0 ) style_set_current_page("%T", g.zPath);
@@ -772,11 +773,11 @@
772773
Th_Store("mainmenu", style_get_mainmenu());
773774
stylesheet_url_var();
774775
image_url_var("logo");
775776
image_url_var("background");
776777
if( !login_is_nobody() ){
777
- Th_Store("login", g.zLogin);
778
+ Th_Store("login", html_lookalike(g.zLogin,-1));
778779
}
779780
Th_MaybeStore("current_feature", feature_from_page_path(local_zCurrentPage) );
780781
if( g.ftntsIssues[0] || g.ftntsIssues[1] ||
781782
g.ftntsIssues[2] || g.ftntsIssues[3] ){
782783
char buf[80];
@@ -1382,11 +1383,12 @@
13821383
@ </form>
13831384
style_finish_page();
13841385
}
13851386
13861387
/*
1387
-** WEBPAGE: test_env
1388
+** WEBPAGE: test-env
1389
+** WEBPAGE: test_env alias
13881390
**
13891391
** Display CGI-variables and other aspects of the run-time
13901392
** environment, for debugging and trouble-shooting purposes.
13911393
*/
13921394
void page_test_env(void){
@@ -1445,11 +1447,11 @@
14451447
**
14461448
** For administators, or if the test_env_enable setting is true, then
14471449
** details of the request environment are displayed. Otherwise, just
14481450
** the error message is shown.
14491451
**
1450
-** If zFormat is an empty string, then this is the /test_env page.
1452
+** If zFormat is an empty string, then this is the /test-env page.
14511453
*/
14521454
void webpage_error(const char *zFormat, ...){
14531455
int showAll = 0;
14541456
char *zErr = 0;
14551457
int isAuth = 0;
@@ -1545,11 +1547,11 @@
15451547
}
15461548
@ <hr>
15471549
P("HTTP_USER_AGENT");
15481550
P("SERVER_SOFTWARE");
15491551
cgi_print_all(showAll, 0, 0);
1550
- @ <p><form method="POST" action="%R/test_env">
1552
+ @ <p><form method="POST" action="%R/test-env">
15511553
@ <input type="hidden" name="showall" value="%d(showAll)">
15521554
@ <input type="submit" name="post-test-button" value="POST Test">
15531555
@ </form>
15541556
if( showAll && blob_size(&g.httpHeader)>0 ){
15551557
@ <hr>
15561558
--- src/style.c
+++ src/style.c
@@ -744,13 +744,14 @@
744 ** is evaluated before the header is rendered).
745 */
746 Th_MaybeStore("default_csp", zDfltCsp);
747 fossil_free(zDfltCsp);
748 Th_Store("nonce", zNonce);
749 Th_Store("project_name", db_get("project-name","Unnamed Fossil Project"));
750 Th_Store("project_description", db_get("project-description",""));
751 if( zTitle ) Th_Store("title", zTitle);
 
752 Th_Store("baseurl", g.zBaseURL);
753 Th_Store("secureurl", fossil_wants_https(1)? g.zHttpsURL: g.zBaseURL);
754 Th_Store("home", g.zTop);
755 Th_Store("index_page", db_get("index-page","/home"));
756 if( local_zCurrentPage==0 ) style_set_current_page("%T", g.zPath);
@@ -772,11 +773,11 @@
772 Th_Store("mainmenu", style_get_mainmenu());
773 stylesheet_url_var();
774 image_url_var("logo");
775 image_url_var("background");
776 if( !login_is_nobody() ){
777 Th_Store("login", g.zLogin);
778 }
779 Th_MaybeStore("current_feature", feature_from_page_path(local_zCurrentPage) );
780 if( g.ftntsIssues[0] || g.ftntsIssues[1] ||
781 g.ftntsIssues[2] || g.ftntsIssues[3] ){
782 char buf[80];
@@ -1382,11 +1383,12 @@
1382 @ </form>
1383 style_finish_page();
1384 }
1385
1386 /*
1387 ** WEBPAGE: test_env
 
1388 **
1389 ** Display CGI-variables and other aspects of the run-time
1390 ** environment, for debugging and trouble-shooting purposes.
1391 */
1392 void page_test_env(void){
@@ -1445,11 +1447,11 @@
1445 **
1446 ** For administators, or if the test_env_enable setting is true, then
1447 ** details of the request environment are displayed. Otherwise, just
1448 ** the error message is shown.
1449 **
1450 ** If zFormat is an empty string, then this is the /test_env page.
1451 */
1452 void webpage_error(const char *zFormat, ...){
1453 int showAll = 0;
1454 char *zErr = 0;
1455 int isAuth = 0;
@@ -1545,11 +1547,11 @@
1545 }
1546 @ <hr>
1547 P("HTTP_USER_AGENT");
1548 P("SERVER_SOFTWARE");
1549 cgi_print_all(showAll, 0, 0);
1550 @ <p><form method="POST" action="%R/test_env">
1551 @ <input type="hidden" name="showall" value="%d(showAll)">
1552 @ <input type="submit" name="post-test-button" value="POST Test">
1553 @ </form>
1554 if( showAll && blob_size(&g.httpHeader)>0 ){
1555 @ <hr>
1556
--- src/style.c
+++ src/style.c
@@ -744,13 +744,14 @@
744 ** is evaluated before the header is rendered).
745 */
746 Th_MaybeStore("default_csp", zDfltCsp);
747 fossil_free(zDfltCsp);
748 Th_Store("nonce", zNonce);
749 Th_StoreUnsafe("project_name",
750 db_get("project-name","Unnamed Fossil Project"));
751 Th_StoreUnsafe("project_description", db_get("project-description",""));
752 if( zTitle ) Th_Store("title", html_lookalike(zTitle,-1));
753 Th_Store("baseurl", g.zBaseURL);
754 Th_Store("secureurl", fossil_wants_https(1)? g.zHttpsURL: g.zBaseURL);
755 Th_Store("home", g.zTop);
756 Th_Store("index_page", db_get("index-page","/home"));
757 if( local_zCurrentPage==0 ) style_set_current_page("%T", g.zPath);
@@ -772,11 +773,11 @@
773 Th_Store("mainmenu", style_get_mainmenu());
774 stylesheet_url_var();
775 image_url_var("logo");
776 image_url_var("background");
777 if( !login_is_nobody() ){
778 Th_Store("login", html_lookalike(g.zLogin,-1));
779 }
780 Th_MaybeStore("current_feature", feature_from_page_path(local_zCurrentPage) );
781 if( g.ftntsIssues[0] || g.ftntsIssues[1] ||
782 g.ftntsIssues[2] || g.ftntsIssues[3] ){
783 char buf[80];
@@ -1382,11 +1383,12 @@
1383 @ </form>
1384 style_finish_page();
1385 }
1386
1387 /*
1388 ** WEBPAGE: test-env
1389 ** WEBPAGE: test_env alias
1390 **
1391 ** Display CGI-variables and other aspects of the run-time
1392 ** environment, for debugging and trouble-shooting purposes.
1393 */
1394 void page_test_env(void){
@@ -1445,11 +1447,11 @@
1447 **
1448 ** For administators, or if the test_env_enable setting is true, then
1449 ** details of the request environment are displayed. Otherwise, just
1450 ** the error message is shown.
1451 **
1452 ** If zFormat is an empty string, then this is the /test-env page.
1453 */
1454 void webpage_error(const char *zFormat, ...){
1455 int showAll = 0;
1456 char *zErr = 0;
1457 int isAuth = 0;
@@ -1545,11 +1547,11 @@
1547 }
1548 @ <hr>
1549 P("HTTP_USER_AGENT");
1550 P("SERVER_SOFTWARE");
1551 cgi_print_all(showAll, 0, 0);
1552 @ <p><form method="POST" action="%R/test-env">
1553 @ <input type="hidden" name="showall" value="%d(showAll)">
1554 @ <input type="submit" name="post-test-button" value="POST Test">
1555 @ </form>
1556 if( showAll && blob_size(&g.httpHeader)>0 ){
1557 @ <hr>
1558
--- src/style.chat.css
+++ src/style.chat.css
@@ -213,10 +213,19 @@
213213
}
214214
body.chat #chat-messages-wrapper.loading > * {
215215
/* An attempt at reducing flicker when loading lots of messages. */
216216
visibility: hidden;
217217
}
218
+
219
+/* Provide a visual cue when polling is offline. */
220
+body.chat.connection-error #chat-input-line-wrapper {
221
+ border-top: medium dotted red;
222
+}
223
+body.chat.fossil-dark-style.connection-error #chat-input-line-wrapper {
224
+ border-color: yellow;
225
+}
226
+
218227
body.chat div.content {
219228
margin: 0;
220229
padding: 0;
221230
display: flex;
222231
flex-direction: column-reverse;
@@ -241,20 +250,21 @@
241250
/* Safari user reports that 2em is necessary to keep the file selection
242251
widget from overlapping the page footer, whereas a margin of 0 is fine
243252
for FF/Chrome (and 2em is a *huge* waste of space for those). */
244253
margin-bottom: 0;
245254
}
246
-.chat-input-field {
255
+
256
+body.chat .chat-input-field {
247257
flex: 10 1 auto;
248258
margin: 0;
249259
}
250
-#chat-input-field-x,
251
-#chat-input-field-multi {
260
+body.chat #chat-input-field-x,
261
+body.chat #chat-input-field-multi {
252262
overflow: auto;
253263
resize: vertical;
254264
}
255
-#chat-input-field-x {
265
+body.chat #chat-input-field-x {
256266
display: inline-block/*supposed workaround for Chrome weirdness*/;
257267
padding: 0.2em;
258268
background-color: rgba(156,156,156,0.3);
259269
white-space: pre-wrap;
260270
/* ^^^ Firefox, when pasting plain text into a contenteditable field,
@@ -261,20 +271,20 @@
261271
loses all newlines unless we explicitly set this. Chrome does not. */
262272
cursor: text;
263273
/* ^^^ In some browsers the cursor may not change for a contenteditable
264274
element until it has focus, causing potential confusion. */
265275
}
266
-#chat-input-field-x:empty::before {
276
+body.chat #chat-input-field-x:empty::before {
267277
content: attr(data-placeholder);
268278
opacity: 0.6;
269279
}
270
-.chat-input-field:not(:focus){
280
+body.chat .chat-input-field:not(:focus){
271281
border-width: 1px;
272282
border-style: solid;
273283
border-radius: 0.25em;
274284
}
275
-.chat-input-field:focus{
285
+body.chat .chat-input-field:focus{
276286
/* This transparent border helps avoid the text shifting around
277287
when the contenteditable attribute causes a border (which we
278288
apparently cannot style) to be added. */
279289
border-width: 1px;
280290
border-style: solid;
281291
--- src/style.chat.css
+++ src/style.chat.css
@@ -213,10 +213,19 @@
213 }
214 body.chat #chat-messages-wrapper.loading > * {
215 /* An attempt at reducing flicker when loading lots of messages. */
216 visibility: hidden;
217 }
 
 
 
 
 
 
 
 
 
218 body.chat div.content {
219 margin: 0;
220 padding: 0;
221 display: flex;
222 flex-direction: column-reverse;
@@ -241,20 +250,21 @@
241 /* Safari user reports that 2em is necessary to keep the file selection
242 widget from overlapping the page footer, whereas a margin of 0 is fine
243 for FF/Chrome (and 2em is a *huge* waste of space for those). */
244 margin-bottom: 0;
245 }
246 .chat-input-field {
 
247 flex: 10 1 auto;
248 margin: 0;
249 }
250 #chat-input-field-x,
251 #chat-input-field-multi {
252 overflow: auto;
253 resize: vertical;
254 }
255 #chat-input-field-x {
256 display: inline-block/*supposed workaround for Chrome weirdness*/;
257 padding: 0.2em;
258 background-color: rgba(156,156,156,0.3);
259 white-space: pre-wrap;
260 /* ^^^ Firefox, when pasting plain text into a contenteditable field,
@@ -261,20 +271,20 @@
261 loses all newlines unless we explicitly set this. Chrome does not. */
262 cursor: text;
263 /* ^^^ In some browsers the cursor may not change for a contenteditable
264 element until it has focus, causing potential confusion. */
265 }
266 #chat-input-field-x:empty::before {
267 content: attr(data-placeholder);
268 opacity: 0.6;
269 }
270 .chat-input-field:not(:focus){
271 border-width: 1px;
272 border-style: solid;
273 border-radius: 0.25em;
274 }
275 .chat-input-field:focus{
276 /* This transparent border helps avoid the text shifting around
277 when the contenteditable attribute causes a border (which we
278 apparently cannot style) to be added. */
279 border-width: 1px;
280 border-style: solid;
281
--- src/style.chat.css
+++ src/style.chat.css
@@ -213,10 +213,19 @@
213 }
214 body.chat #chat-messages-wrapper.loading > * {
215 /* An attempt at reducing flicker when loading lots of messages. */
216 visibility: hidden;
217 }
218
219 /* Provide a visual cue when polling is offline. */
220 body.chat.connection-error #chat-input-line-wrapper {
221 border-top: medium dotted red;
222 }
223 body.chat.fossil-dark-style.connection-error #chat-input-line-wrapper {
224 border-color: yellow;
225 }
226
227 body.chat div.content {
228 margin: 0;
229 padding: 0;
230 display: flex;
231 flex-direction: column-reverse;
@@ -241,20 +250,21 @@
250 /* Safari user reports that 2em is necessary to keep the file selection
251 widget from overlapping the page footer, whereas a margin of 0 is fine
252 for FF/Chrome (and 2em is a *huge* waste of space for those). */
253 margin-bottom: 0;
254 }
255
256 body.chat .chat-input-field {
257 flex: 10 1 auto;
258 margin: 0;
259 }
260 body.chat #chat-input-field-x,
261 body.chat #chat-input-field-multi {
262 overflow: auto;
263 resize: vertical;
264 }
265 body.chat #chat-input-field-x {
266 display: inline-block/*supposed workaround for Chrome weirdness*/;
267 padding: 0.2em;
268 background-color: rgba(156,156,156,0.3);
269 white-space: pre-wrap;
270 /* ^^^ Firefox, when pasting plain text into a contenteditable field,
@@ -261,20 +271,20 @@
271 loses all newlines unless we explicitly set this. Chrome does not. */
272 cursor: text;
273 /* ^^^ In some browsers the cursor may not change for a contenteditable
274 element until it has focus, causing potential confusion. */
275 }
276 body.chat #chat-input-field-x:empty::before {
277 content: attr(data-placeholder);
278 opacity: 0.6;
279 }
280 body.chat .chat-input-field:not(:focus){
281 border-width: 1px;
282 border-style: solid;
283 border-radius: 0.25em;
284 }
285 body.chat .chat-input-field:focus{
286 /* This transparent border helps avoid the text shifting around
287 when the contenteditable attribute causes a border (which we
288 apparently cannot style) to be added. */
289 border-width: 1px;
290 border-style: solid;
291
+95 -56
--- src/th.c
+++ src/th.c
@@ -7,10 +7,16 @@
77
#include "config.h"
88
#include "th.h"
99
#include <string.h>
1010
#include <assert.h>
1111
12
+/*
13
+** External routines
14
+*/
15
+void fossil_panic(const char*,...);
16
+void fossil_errorlog(const char*,...);
17
+
1218
/*
1319
** Values used for element values in the tcl_platform array.
1420
*/
1521
1622
#if !defined(TH_ENGINE)
@@ -197,10 +203,11 @@
197203
*/
198204
struct Buffer {
199205
char *zBuf;
200206
int nBuf;
201207
int nBufAlloc;
208
+ int bTaint;
202209
};
203210
typedef struct Buffer Buffer;
204211
static void thBufferInit(Buffer *);
205212
static void thBufferFree(Th_Interp *interp, Buffer *);
206213
@@ -209,10 +216,18 @@
209216
** be NULL as long as the number of bytes to copy is zero.
210217
*/
211218
static void th_memcpy(void *dest, const void *src, size_t n){
212219
if( n>0 ) memcpy(dest,src,n);
213220
}
221
+
222
+/*
223
+** An oversized string has been encountered. Do not try to recover.
224
+** Panic the process.
225
+*/
226
+void Th_OversizeString(void){
227
+ fossil_panic("string too large. maximum size 286MB.");
228
+}
214229
215230
/*
216231
** Append nAdd bytes of content copied from zAdd to the end of buffer
217232
** pBuffer. If there is not enough space currently allocated, resize
218233
** the allocation to make space.
@@ -219,40 +234,46 @@
219234
*/
220235
static void thBufferWriteResize(
221236
Th_Interp *interp,
222237
Buffer *pBuffer,
223238
const char *zAdd,
224
- int nAdd
239
+ int nAddX
225240
){
241
+ int nAdd = TH1_LEN(nAddX);
226242
int nNew = (pBuffer->nBuf+nAdd)*2+32;
227243
#if defined(TH_MEMDEBUG)
228244
char *zNew = (char *)Th_Malloc(interp, nNew);
245
+ TH1_SIZECHECK(nNew);
229246
th_memcpy(zNew, pBuffer->zBuf, pBuffer->nBuf);
230247
Th_Free(interp, pBuffer->zBuf);
231248
pBuffer->zBuf = zNew;
232249
#else
233250
int nOld = pBuffer->nBufAlloc;
251
+ TH1_SIZECHECK(nNew);
234252
pBuffer->zBuf = Th_Realloc(interp, pBuffer->zBuf, nNew);
235253
memset(pBuffer->zBuf+nOld, 0, nNew-nOld);
236254
#endif
237255
pBuffer->nBufAlloc = nNew;
238256
th_memcpy(&pBuffer->zBuf[pBuffer->nBuf], zAdd, nAdd);
239257
pBuffer->nBuf += nAdd;
258
+ TH1_XFER_TAINT(pBuffer->bTaint, nAddX);
240259
}
241260
static void thBufferWriteFast(
242261
Th_Interp *interp,
243262
Buffer *pBuffer,
244263
const char *zAdd,
245
- int nAdd
264
+ int nAddX
246265
){
266
+ int nAdd = TH1_LEN(nAddX);
247267
if( pBuffer->nBuf+nAdd > pBuffer->nBufAlloc ){
248
- thBufferWriteResize(interp, pBuffer, zAdd, nAdd);
268
+ thBufferWriteResize(interp, pBuffer, zAdd, nAddX);
249269
}else{
250270
if( pBuffer->zBuf ){
251271
memcpy(pBuffer->zBuf + pBuffer->nBuf, zAdd, nAdd);
252272
}
253273
pBuffer->nBuf += nAdd;
274
+ TH1_XFER_TAINT(pBuffer->bTaint, nAddX);
254275
}
255276
}
256277
#define thBufferWrite(a,b,c,d) thBufferWriteFast(a,b,(const char *)c,d)
257278
258279
/*
@@ -704,24 +725,25 @@
704725
int nWord
705726
){
706727
int rc = TH_OK;
707728
Buffer output;
708729
int i;
730
+ int nn = TH1_LEN(nWord);
709731
710732
thBufferInit(&output);
711733
712
- if( nWord>1 && (zWord[0]=='{' && zWord[nWord-1]=='}') ){
713
- thBufferWrite(interp, &output, &zWord[1], nWord-2);
734
+ if( nn>1 && (zWord[0]=='{' && zWord[nn-1]=='}') ){
735
+ thBufferWrite(interp, &output, &zWord[1], nn-2);
714736
}else{
715737
716738
/* If the word is surrounded by double-quotes strip these away. */
717
- if( nWord>1 && (zWord[0]=='"' && zWord[nWord-1]=='"') ){
739
+ if( nn>1 && (zWord[0]=='"' && zWord[nn-1]=='"') ){
718740
zWord++;
719
- nWord -= 2;
741
+ nn -= 2;
720742
}
721743
722
- for(i=0; rc==TH_OK && i<nWord; i++){
744
+ for(i=0; rc==TH_OK && i<nn; i++){
723745
int nGet;
724746
725747
int (*xGet)(Th_Interp *, const char*, int, int *) = 0;
726748
int (*xSubst)(Th_Interp *, const char*, int) = 0;
727749
@@ -743,11 +765,11 @@
743765
thBufferAddChar(interp, &output, zWord[i]);
744766
continue; /* Go to the next iteration of the for(...) loop */
745767
}
746768
}
747769
748
- rc = xGet(interp, &zWord[i], nWord-i, &nGet);
770
+ rc = xGet(interp, &zWord[i], nn-i, &nGet);
749771
if( rc==TH_OK ){
750772
rc = xSubst(interp, &zWord[i], nGet);
751773
}
752774
if( rc==TH_OK ){
753775
const char *zRes;
@@ -758,11 +780,11 @@
758780
}
759781
}
760782
}
761783
762784
if( rc==TH_OK ){
763
- Th_SetResult(interp, output.zBuf, output.nBuf);
785
+ Th_SetResult(interp, output.zBuf, output.nBuf|output.bTaint);
764786
}
765787
thBufferFree(interp, &output);
766788
return rc;
767789
}
768790
@@ -826,11 +848,11 @@
826848
Buffer strbuf;
827849
Buffer lenbuf;
828850
int nCount = 0;
829851
830852
const char *zInput = zList;
831
- int nInput = nList;
853
+ int nInput = TH1_LEN(nList);
832854
833855
thBufferInit(&strbuf);
834856
thBufferInit(&lenbuf);
835857
836858
while( nInput>0 ){
@@ -837,19 +859,19 @@
837859
const char *zWord;
838860
int nWord;
839861
840862
thNextSpace(interp, zInput, nInput, &nWord);
841863
zInput += nWord;
842
- nInput = nList-(zInput-zList);
864
+ nInput = TH1_LEN(nList)-(zInput-zList);
843865
844866
if( TH_OK!=(rc = thNextWord(interp, zInput, nInput, &nWord, 0))
845867
|| TH_OK!=(rc = thSubstWord(interp, zInput, nWord))
846868
){
847869
goto finish;
848870
}
849
- zInput = &zInput[nWord];
850
- nInput = nList-(zInput-zList);
871
+ zInput = &zInput[TH1_LEN(nWord)];
872
+ nInput = TH1_LEN(nList)-(zInput-zList);
851873
if( nWord>0 ){
852874
zWord = Th_GetResult(interp, &nWord);
853875
thBufferWrite(interp, &strbuf, zWord, nWord);
854876
thBufferAddChar(interp, &strbuf, 0);
855877
thBufferWrite(interp, &lenbuf, &nWord, sizeof(int));
@@ -872,11 +894,11 @@
872894
zElem = (char *)&anElem[nCount];
873895
th_memcpy(anElem, lenbuf.zBuf, lenbuf.nBuf);
874896
th_memcpy(zElem, strbuf.zBuf, strbuf.nBuf);
875897
for(i=0; i<nCount;i++){
876898
azElem[i] = zElem;
877
- zElem += (anElem[i] + 1);
899
+ zElem += (TH1_LEN(anElem[i]) + 1);
878900
}
879901
*pazElem = azElem;
880902
*panElem = anElem;
881903
}
882904
if( pnCount ){
@@ -894,12 +916,17 @@
894916
** in the current stack frame.
895917
*/
896918
static int thEvalLocal(Th_Interp *interp, const char *zProgram, int nProgram){
897919
int rc = TH_OK;
898920
const char *zInput = zProgram;
899
- int nInput = nProgram;
921
+ int nInput = TH1_LEN(nProgram);
900922
923
+ if( TH1_TAINTED(nProgram)
924
+ && Th_ReportTaint(interp, "script", zProgram, nProgram)
925
+ ){
926
+ return TH_ERROR;
927
+ }
901928
while( rc==TH_OK && nInput ){
902929
Th_HashEntry *pEntry;
903930
int nSpace;
904931
const char *zFirst;
905932
@@ -949,13 +976,13 @@
949976
if( rc!=TH_OK ) continue;
950977
951978
if( argc>0 ){
952979
953980
/* Look up the command name in the command hash-table. */
954
- pEntry = Th_HashFind(interp, interp->paCmd, argv[0], argl[0], 0);
981
+ pEntry = Th_HashFind(interp, interp->paCmd, argv[0], TH1_LEN(argl[0]),0);
955982
if( !pEntry ){
956
- Th_ErrorMessage(interp, "no such command: ", argv[0], argl[0]);
983
+ Th_ErrorMessage(interp, "no such command: ", argv[0], TH1_LEN(argl[0]));
957984
rc = TH_ERROR;
958985
}
959986
960987
/* Call the command procedure. */
961988
if( rc==TH_OK ){
@@ -1053,10 +1080,12 @@
10531080
}else{
10541081
int nInput = nProgram;
10551082
10561083
if( nInput<0 ){
10571084
nInput = th_strlen(zProgram);
1085
+ }else{
1086
+ nInput = TH1_LEN(nInput);
10581087
}
10591088
rc = thEvalLocal(interp, zProgram, nInput);
10601089
}
10611090
10621091
interp->pFrame = pSavedFrame;
@@ -1095,10 +1124,12 @@
10951124
int isGlobal = 0;
10961125
int i;
10971126
10981127
if( nVarname<0 ){
10991128
nVarname = th_strlen(zVarname);
1129
+ }else{
1130
+ nVarname = TH1_LEN(nVarname);
11001131
}
11011132
nOuter = nVarname;
11021133
11031134
/* If the variable name starts with "::", then do the lookup is in the
11041135
** uppermost (global) frame.
@@ -1271,31 +1302,10 @@
12711302
}
12721303
12731304
return Th_SetResult(interp, pValue->zData, pValue->nData);
12741305
}
12751306
1276
-/*
1277
-** If interp has a variable with the given name, its value is returned
1278
-** and its length is returned via *nOut if nOut is not NULL. If
1279
-** interp has no such var then NULL is returned without setting any
1280
-** error state and *nOut, if not NULL, is set to -1. The returned value
1281
-** is owned by the interpreter and may be invalidated the next time
1282
-** the interpreter is modified.
1283
-*/
1284
-const char * Th_MaybeGetVar(Th_Interp *interp, const char *zVarName,
1285
- int *nOut){
1286
- Th_Variable *pValue;
1287
-
1288
- pValue = thFindValue(interp, zVarName, -1, 0, 0, 1, 0);
1289
- if( !pValue || !pValue->zData ){
1290
- if( nOut!=0 ) *nOut = -1;
1291
- return NULL;
1292
- }
1293
- if( nOut!=0 ) *nOut = pValue->nData;
1294
- return pValue->zData;
1295
-}
1296
-
12971307
/*
12981308
** Return true if variable (zVar, nVar) exists.
12991309
*/
13001310
int Th_ExistsVar(Th_Interp *interp, const char *zVar, int nVar){
13011311
Th_Variable *pValue = thFindValue(interp, zVar, nVar, 0, 1, 1, 0);
@@ -1324,28 +1334,32 @@
13241334
int nVar,
13251335
const char *zValue,
13261336
int nValue
13271337
){
13281338
Th_Variable *pValue;
1339
+ int nn;
13291340
1341
+ nVar = TH1_LEN(nVar);
13301342
pValue = thFindValue(interp, zVar, nVar, 1, 0, 0, 0);
13311343
if( !pValue ){
13321344
return TH_ERROR;
13331345
}
13341346
13351347
if( nValue<0 ){
1336
- nValue = th_strlen(zValue);
1348
+ nn = th_strlen(zValue);
1349
+ }else{
1350
+ nn = TH1_LEN(nValue);
13371351
}
13381352
if( pValue->zData ){
13391353
Th_Free(interp, pValue->zData);
13401354
pValue->zData = 0;
13411355
}
13421356
1343
- assert(zValue || nValue==0);
1344
- pValue->zData = Th_Malloc(interp, nValue+1);
1345
- pValue->zData[nValue] = '\0';
1346
- th_memcpy(pValue->zData, zValue, nValue);
1357
+ assert(zValue || nn==0);
1358
+ pValue->zData = Th_Malloc(interp, nn+1);
1359
+ pValue->zData[nn] = '\0';
1360
+ th_memcpy(pValue->zData, zValue, nn);
13471361
pValue->nData = nValue;
13481362
13491363
return TH_OK;
13501364
}
13511365
@@ -1458,10 +1472,12 @@
14581472
*/
14591473
char *th_strdup(Th_Interp *interp, const char *z, int n){
14601474
char *zRes;
14611475
if( n<0 ){
14621476
n = th_strlen(z);
1477
+ }else{
1478
+ n = TH1_LEN(n);
14631479
}
14641480
zRes = Th_Malloc(interp, n+1);
14651481
th_memcpy(zRes, z, n);
14661482
zRes[n] = '\0';
14671483
return zRes;
@@ -1519,13 +1535,14 @@
15191535
n = th_strlen(z);
15201536
}
15211537
15221538
if( z && n>0 ){
15231539
char *zResult;
1524
- zResult = Th_Malloc(pInterp, n+1);
1525
- th_memcpy(zResult, z, n);
1526
- zResult[n] = '\0';
1540
+ int nn = TH1_LEN(n);
1541
+ zResult = Th_Malloc(pInterp, nn+1);
1542
+ th_memcpy(zResult, z, nn);
1543
+ zResult[nn] = '\0';
15271544
pInterp->zResult = zResult;
15281545
pInterp->nResult = n;
15291546
}
15301547
15311548
return TH_OK;
@@ -1777,15 +1794,19 @@
17771794
int hasSpecialChar = 0; /* Whitespace or {}[]'" */
17781795
int hasEscapeChar = 0; /* '}' without matching '{' to the left or a '\\' */
17791796
int nBrace = 0;
17801797
17811798
output.zBuf = *pzList;
1782
- output.nBuf = *pnList;
1799
+ output.nBuf = TH1_LEN(*pnList);
17831800
output.nBufAlloc = output.nBuf;
1801
+ output.bTaint = 0;
1802
+ TH1_XFER_TAINT(output.bTaint, *pnList);
17841803
17851804
if( nElem<0 ){
17861805
nElem = th_strlen(zElem);
1806
+ }else{
1807
+ nElem = TH1_LEN(nElem);
17871808
}
17881809
if( output.nBuf>0 ){
17891810
thBufferAddChar(interp, &output, ' ');
17901811
}
17911812
@@ -1834,24 +1855,28 @@
18341855
int *pnStr, /* IN/OUT: Current length of *pzStr */
18351856
const char *zElem, /* Data to append */
18361857
int nElem /* Length of nElem */
18371858
){
18381859
char *zNew;
1839
- int nNew;
1860
+ long long int nNew;
1861
+ int nn;
18401862
18411863
if( nElem<0 ){
1842
- nElem = th_strlen(zElem);
1864
+ nn = th_strlen(zElem);
1865
+ }else{
1866
+ nn = TH1_LEN(nElem);
18431867
}
18441868
1845
- nNew = *pnStr + nElem;
1869
+ nNew = TH1_LEN(*pnStr) + nn;
1870
+ TH1_SIZECHECK(nNew);
18461871
zNew = Th_Malloc(interp, nNew);
18471872
th_memcpy(zNew, *pzStr, *pnStr);
1848
- th_memcpy(&zNew[*pnStr], zElem, nElem);
1873
+ th_memcpy(&zNew[TH1_LEN(*pnStr)], zElem, nn);
18491874
18501875
Th_Free(interp, *pzStr);
18511876
*pzStr = zNew;
1852
- *pnStr = nNew;
1877
+ *pnStr = (int)nNew;
18531878
18541879
return TH_OK;
18551880
}
18561881
18571882
/*
@@ -2106,16 +2131,18 @@
21062131
/* Evaluate left and right arguments, if they exist. */
21072132
if( pExpr->pLeft ){
21082133
rc = exprEval(interp, pExpr->pLeft);
21092134
if( rc==TH_OK ){
21102135
zLeft = Th_TakeResult(interp, &nLeft);
2136
+ nLeft = TH1_LEN(nLeft);
21112137
}
21122138
}
21132139
if( rc==TH_OK && pExpr->pRight ){
21142140
rc = exprEval(interp, pExpr->pRight);
21152141
if( rc==TH_OK ){
21162142
zRight = Th_TakeResult(interp, &nRight);
2143
+ nRight = TH1_LEN(nRight);
21172144
}
21182145
}
21192146
21202147
/* Convert arguments to their required forms. */
21212148
if( rc==TH_OK ){
@@ -2160,12 +2187,15 @@
21602187
}
21612188
iRes = iLeft%iRight;
21622189
break;
21632190
case OP_ADD: iRes = iLeft+iRight; break;
21642191
case OP_SUBTRACT: iRes = iLeft-iRight; break;
2165
- case OP_LEFTSHIFT: iRes = iLeft<<iRight; break;
2166
- case OP_RIGHTSHIFT: iRes = iLeft>>iRight; break;
2192
+ case OP_LEFTSHIFT: {
2193
+ iRes = (int)(((unsigned int)iLeft)<<(iRight&0x1f));
2194
+ break;
2195
+ }
2196
+ case OP_RIGHTSHIFT: iRes = iLeft>>(iRight&0x1f); break;
21672197
case OP_LT: iRes = iLeft<iRight; break;
21682198
case OP_GT: iRes = iLeft>iRight; break;
21692199
case OP_LE: iRes = iLeft<=iRight; break;
21702200
case OP_GE: iRes = iLeft>=iRight; break;
21712201
case OP_EQ: iRes = iLeft==iRight; break;
@@ -2453,10 +2483,12 @@
24532483
int nToken = 0;
24542484
Expr **apToken = 0;
24552485
24562486
if( nExpr<0 ){
24572487
nExpr = th_strlen(zExpr);
2488
+ }else{
2489
+ nExpr = TH1_LEN(nExpr);
24582490
}
24592491
24602492
/* Parse the expression to a list of tokens. */
24612493
rc = exprParse(interp, zExpr, nExpr, &apToken, &nToken);
24622494
@@ -2564,10 +2596,12 @@
25642596
Th_HashEntry *pRet;
25652597
Th_HashEntry **ppRet;
25662598
25672599
if( nKey<0 ){
25682600
nKey = th_strlen(zKey);
2601
+ }else{
2602
+ nKey = TH1_LEN(nKey);
25692603
}
25702604
25712605
for(i=0; i<nKey; i++){
25722606
iKey = (iKey<<3) ^ iKey ^ zKey[i];
25732607
}
@@ -2797,10 +2831,12 @@
27972831
int base = 10;
27982832
int (*isdigit)(char) = th_isdigit;
27992833
28002834
if( n<0 ){
28012835
n = th_strlen(z);
2836
+ }else{
2837
+ n = TH1_LEN(n);
28022838
}
28032839
28042840
if( n>1 && (z[0]=='-' || z[0]=='+') ){
28052841
i = 1;
28062842
}
@@ -2856,11 +2892,11 @@
28562892
const char *z,
28572893
int n,
28582894
double *pfOut
28592895
){
28602896
if( !sqlite3IsNumber((const char *)z, 0) ){
2861
- Th_ErrorMessage(interp, "expected number, got: \"", z, n);
2897
+ Th_ErrorMessage(interp, "expected number, got: \"", z, TH1_LEN(n));
28622898
return TH_ERROR;
28632899
}
28642900
28652901
sqlite3AtoF((const char *)z, pfOut);
28662902
return TH_OK;
@@ -2875,10 +2911,13 @@
28752911
unsigned int uVal = iVal;
28762912
char zBuf[32];
28772913
char *z = &zBuf[32];
28782914
28792915
if( iVal<0 ){
2916
+ if( iVal==0x80000000 ){
2917
+ return Th_SetResult(interp, "-2147483648", -1);
2918
+ }
28802919
isNegative = 1;
28812920
uVal = iVal * -1;
28822921
}
28832922
*(--z) = '\0';
28842923
*(--z) = (char)(48+(uVal%10));
28852924
--- src/th.c
+++ src/th.c
@@ -7,10 +7,16 @@
7 #include "config.h"
8 #include "th.h"
9 #include <string.h>
10 #include <assert.h>
11
 
 
 
 
 
 
12 /*
13 ** Values used for element values in the tcl_platform array.
14 */
15
16 #if !defined(TH_ENGINE)
@@ -197,10 +203,11 @@
197 */
198 struct Buffer {
199 char *zBuf;
200 int nBuf;
201 int nBufAlloc;
 
202 };
203 typedef struct Buffer Buffer;
204 static void thBufferInit(Buffer *);
205 static void thBufferFree(Th_Interp *interp, Buffer *);
206
@@ -209,10 +216,18 @@
209 ** be NULL as long as the number of bytes to copy is zero.
210 */
211 static void th_memcpy(void *dest, const void *src, size_t n){
212 if( n>0 ) memcpy(dest,src,n);
213 }
 
 
 
 
 
 
 
 
214
215 /*
216 ** Append nAdd bytes of content copied from zAdd to the end of buffer
217 ** pBuffer. If there is not enough space currently allocated, resize
218 ** the allocation to make space.
@@ -219,40 +234,46 @@
219 */
220 static void thBufferWriteResize(
221 Th_Interp *interp,
222 Buffer *pBuffer,
223 const char *zAdd,
224 int nAdd
225 ){
 
226 int nNew = (pBuffer->nBuf+nAdd)*2+32;
227 #if defined(TH_MEMDEBUG)
228 char *zNew = (char *)Th_Malloc(interp, nNew);
 
229 th_memcpy(zNew, pBuffer->zBuf, pBuffer->nBuf);
230 Th_Free(interp, pBuffer->zBuf);
231 pBuffer->zBuf = zNew;
232 #else
233 int nOld = pBuffer->nBufAlloc;
 
234 pBuffer->zBuf = Th_Realloc(interp, pBuffer->zBuf, nNew);
235 memset(pBuffer->zBuf+nOld, 0, nNew-nOld);
236 #endif
237 pBuffer->nBufAlloc = nNew;
238 th_memcpy(&pBuffer->zBuf[pBuffer->nBuf], zAdd, nAdd);
239 pBuffer->nBuf += nAdd;
 
240 }
241 static void thBufferWriteFast(
242 Th_Interp *interp,
243 Buffer *pBuffer,
244 const char *zAdd,
245 int nAdd
246 ){
 
247 if( pBuffer->nBuf+nAdd > pBuffer->nBufAlloc ){
248 thBufferWriteResize(interp, pBuffer, zAdd, nAdd);
249 }else{
250 if( pBuffer->zBuf ){
251 memcpy(pBuffer->zBuf + pBuffer->nBuf, zAdd, nAdd);
252 }
253 pBuffer->nBuf += nAdd;
 
254 }
255 }
256 #define thBufferWrite(a,b,c,d) thBufferWriteFast(a,b,(const char *)c,d)
257
258 /*
@@ -704,24 +725,25 @@
704 int nWord
705 ){
706 int rc = TH_OK;
707 Buffer output;
708 int i;
 
709
710 thBufferInit(&output);
711
712 if( nWord>1 && (zWord[0]=='{' && zWord[nWord-1]=='}') ){
713 thBufferWrite(interp, &output, &zWord[1], nWord-2);
714 }else{
715
716 /* If the word is surrounded by double-quotes strip these away. */
717 if( nWord>1 && (zWord[0]=='"' && zWord[nWord-1]=='"') ){
718 zWord++;
719 nWord -= 2;
720 }
721
722 for(i=0; rc==TH_OK && i<nWord; i++){
723 int nGet;
724
725 int (*xGet)(Th_Interp *, const char*, int, int *) = 0;
726 int (*xSubst)(Th_Interp *, const char*, int) = 0;
727
@@ -743,11 +765,11 @@
743 thBufferAddChar(interp, &output, zWord[i]);
744 continue; /* Go to the next iteration of the for(...) loop */
745 }
746 }
747
748 rc = xGet(interp, &zWord[i], nWord-i, &nGet);
749 if( rc==TH_OK ){
750 rc = xSubst(interp, &zWord[i], nGet);
751 }
752 if( rc==TH_OK ){
753 const char *zRes;
@@ -758,11 +780,11 @@
758 }
759 }
760 }
761
762 if( rc==TH_OK ){
763 Th_SetResult(interp, output.zBuf, output.nBuf);
764 }
765 thBufferFree(interp, &output);
766 return rc;
767 }
768
@@ -826,11 +848,11 @@
826 Buffer strbuf;
827 Buffer lenbuf;
828 int nCount = 0;
829
830 const char *zInput = zList;
831 int nInput = nList;
832
833 thBufferInit(&strbuf);
834 thBufferInit(&lenbuf);
835
836 while( nInput>0 ){
@@ -837,19 +859,19 @@
837 const char *zWord;
838 int nWord;
839
840 thNextSpace(interp, zInput, nInput, &nWord);
841 zInput += nWord;
842 nInput = nList-(zInput-zList);
843
844 if( TH_OK!=(rc = thNextWord(interp, zInput, nInput, &nWord, 0))
845 || TH_OK!=(rc = thSubstWord(interp, zInput, nWord))
846 ){
847 goto finish;
848 }
849 zInput = &zInput[nWord];
850 nInput = nList-(zInput-zList);
851 if( nWord>0 ){
852 zWord = Th_GetResult(interp, &nWord);
853 thBufferWrite(interp, &strbuf, zWord, nWord);
854 thBufferAddChar(interp, &strbuf, 0);
855 thBufferWrite(interp, &lenbuf, &nWord, sizeof(int));
@@ -872,11 +894,11 @@
872 zElem = (char *)&anElem[nCount];
873 th_memcpy(anElem, lenbuf.zBuf, lenbuf.nBuf);
874 th_memcpy(zElem, strbuf.zBuf, strbuf.nBuf);
875 for(i=0; i<nCount;i++){
876 azElem[i] = zElem;
877 zElem += (anElem[i] + 1);
878 }
879 *pazElem = azElem;
880 *panElem = anElem;
881 }
882 if( pnCount ){
@@ -894,12 +916,17 @@
894 ** in the current stack frame.
895 */
896 static int thEvalLocal(Th_Interp *interp, const char *zProgram, int nProgram){
897 int rc = TH_OK;
898 const char *zInput = zProgram;
899 int nInput = nProgram;
900
 
 
 
 
 
901 while( rc==TH_OK && nInput ){
902 Th_HashEntry *pEntry;
903 int nSpace;
904 const char *zFirst;
905
@@ -949,13 +976,13 @@
949 if( rc!=TH_OK ) continue;
950
951 if( argc>0 ){
952
953 /* Look up the command name in the command hash-table. */
954 pEntry = Th_HashFind(interp, interp->paCmd, argv[0], argl[0], 0);
955 if( !pEntry ){
956 Th_ErrorMessage(interp, "no such command: ", argv[0], argl[0]);
957 rc = TH_ERROR;
958 }
959
960 /* Call the command procedure. */
961 if( rc==TH_OK ){
@@ -1053,10 +1080,12 @@
1053 }else{
1054 int nInput = nProgram;
1055
1056 if( nInput<0 ){
1057 nInput = th_strlen(zProgram);
 
 
1058 }
1059 rc = thEvalLocal(interp, zProgram, nInput);
1060 }
1061
1062 interp->pFrame = pSavedFrame;
@@ -1095,10 +1124,12 @@
1095 int isGlobal = 0;
1096 int i;
1097
1098 if( nVarname<0 ){
1099 nVarname = th_strlen(zVarname);
 
 
1100 }
1101 nOuter = nVarname;
1102
1103 /* If the variable name starts with "::", then do the lookup is in the
1104 ** uppermost (global) frame.
@@ -1271,31 +1302,10 @@
1271 }
1272
1273 return Th_SetResult(interp, pValue->zData, pValue->nData);
1274 }
1275
1276 /*
1277 ** If interp has a variable with the given name, its value is returned
1278 ** and its length is returned via *nOut if nOut is not NULL. If
1279 ** interp has no such var then NULL is returned without setting any
1280 ** error state and *nOut, if not NULL, is set to -1. The returned value
1281 ** is owned by the interpreter and may be invalidated the next time
1282 ** the interpreter is modified.
1283 */
1284 const char * Th_MaybeGetVar(Th_Interp *interp, const char *zVarName,
1285 int *nOut){
1286 Th_Variable *pValue;
1287
1288 pValue = thFindValue(interp, zVarName, -1, 0, 0, 1, 0);
1289 if( !pValue || !pValue->zData ){
1290 if( nOut!=0 ) *nOut = -1;
1291 return NULL;
1292 }
1293 if( nOut!=0 ) *nOut = pValue->nData;
1294 return pValue->zData;
1295 }
1296
1297 /*
1298 ** Return true if variable (zVar, nVar) exists.
1299 */
1300 int Th_ExistsVar(Th_Interp *interp, const char *zVar, int nVar){
1301 Th_Variable *pValue = thFindValue(interp, zVar, nVar, 0, 1, 1, 0);
@@ -1324,28 +1334,32 @@
1324 int nVar,
1325 const char *zValue,
1326 int nValue
1327 ){
1328 Th_Variable *pValue;
 
1329
 
1330 pValue = thFindValue(interp, zVar, nVar, 1, 0, 0, 0);
1331 if( !pValue ){
1332 return TH_ERROR;
1333 }
1334
1335 if( nValue<0 ){
1336 nValue = th_strlen(zValue);
 
 
1337 }
1338 if( pValue->zData ){
1339 Th_Free(interp, pValue->zData);
1340 pValue->zData = 0;
1341 }
1342
1343 assert(zValue || nValue==0);
1344 pValue->zData = Th_Malloc(interp, nValue+1);
1345 pValue->zData[nValue] = '\0';
1346 th_memcpy(pValue->zData, zValue, nValue);
1347 pValue->nData = nValue;
1348
1349 return TH_OK;
1350 }
1351
@@ -1458,10 +1472,12 @@
1458 */
1459 char *th_strdup(Th_Interp *interp, const char *z, int n){
1460 char *zRes;
1461 if( n<0 ){
1462 n = th_strlen(z);
 
 
1463 }
1464 zRes = Th_Malloc(interp, n+1);
1465 th_memcpy(zRes, z, n);
1466 zRes[n] = '\0';
1467 return zRes;
@@ -1519,13 +1535,14 @@
1519 n = th_strlen(z);
1520 }
1521
1522 if( z && n>0 ){
1523 char *zResult;
1524 zResult = Th_Malloc(pInterp, n+1);
1525 th_memcpy(zResult, z, n);
1526 zResult[n] = '\0';
 
1527 pInterp->zResult = zResult;
1528 pInterp->nResult = n;
1529 }
1530
1531 return TH_OK;
@@ -1777,15 +1794,19 @@
1777 int hasSpecialChar = 0; /* Whitespace or {}[]'" */
1778 int hasEscapeChar = 0; /* '}' without matching '{' to the left or a '\\' */
1779 int nBrace = 0;
1780
1781 output.zBuf = *pzList;
1782 output.nBuf = *pnList;
1783 output.nBufAlloc = output.nBuf;
 
 
1784
1785 if( nElem<0 ){
1786 nElem = th_strlen(zElem);
 
 
1787 }
1788 if( output.nBuf>0 ){
1789 thBufferAddChar(interp, &output, ' ');
1790 }
1791
@@ -1834,24 +1855,28 @@
1834 int *pnStr, /* IN/OUT: Current length of *pzStr */
1835 const char *zElem, /* Data to append */
1836 int nElem /* Length of nElem */
1837 ){
1838 char *zNew;
1839 int nNew;
 
1840
1841 if( nElem<0 ){
1842 nElem = th_strlen(zElem);
 
 
1843 }
1844
1845 nNew = *pnStr + nElem;
 
1846 zNew = Th_Malloc(interp, nNew);
1847 th_memcpy(zNew, *pzStr, *pnStr);
1848 th_memcpy(&zNew[*pnStr], zElem, nElem);
1849
1850 Th_Free(interp, *pzStr);
1851 *pzStr = zNew;
1852 *pnStr = nNew;
1853
1854 return TH_OK;
1855 }
1856
1857 /*
@@ -2106,16 +2131,18 @@
2106 /* Evaluate left and right arguments, if they exist. */
2107 if( pExpr->pLeft ){
2108 rc = exprEval(interp, pExpr->pLeft);
2109 if( rc==TH_OK ){
2110 zLeft = Th_TakeResult(interp, &nLeft);
 
2111 }
2112 }
2113 if( rc==TH_OK && pExpr->pRight ){
2114 rc = exprEval(interp, pExpr->pRight);
2115 if( rc==TH_OK ){
2116 zRight = Th_TakeResult(interp, &nRight);
 
2117 }
2118 }
2119
2120 /* Convert arguments to their required forms. */
2121 if( rc==TH_OK ){
@@ -2160,12 +2187,15 @@
2160 }
2161 iRes = iLeft%iRight;
2162 break;
2163 case OP_ADD: iRes = iLeft+iRight; break;
2164 case OP_SUBTRACT: iRes = iLeft-iRight; break;
2165 case OP_LEFTSHIFT: iRes = iLeft<<iRight; break;
2166 case OP_RIGHTSHIFT: iRes = iLeft>>iRight; break;
 
 
 
2167 case OP_LT: iRes = iLeft<iRight; break;
2168 case OP_GT: iRes = iLeft>iRight; break;
2169 case OP_LE: iRes = iLeft<=iRight; break;
2170 case OP_GE: iRes = iLeft>=iRight; break;
2171 case OP_EQ: iRes = iLeft==iRight; break;
@@ -2453,10 +2483,12 @@
2453 int nToken = 0;
2454 Expr **apToken = 0;
2455
2456 if( nExpr<0 ){
2457 nExpr = th_strlen(zExpr);
 
 
2458 }
2459
2460 /* Parse the expression to a list of tokens. */
2461 rc = exprParse(interp, zExpr, nExpr, &apToken, &nToken);
2462
@@ -2564,10 +2596,12 @@
2564 Th_HashEntry *pRet;
2565 Th_HashEntry **ppRet;
2566
2567 if( nKey<0 ){
2568 nKey = th_strlen(zKey);
 
 
2569 }
2570
2571 for(i=0; i<nKey; i++){
2572 iKey = (iKey<<3) ^ iKey ^ zKey[i];
2573 }
@@ -2797,10 +2831,12 @@
2797 int base = 10;
2798 int (*isdigit)(char) = th_isdigit;
2799
2800 if( n<0 ){
2801 n = th_strlen(z);
 
 
2802 }
2803
2804 if( n>1 && (z[0]=='-' || z[0]=='+') ){
2805 i = 1;
2806 }
@@ -2856,11 +2892,11 @@
2856 const char *z,
2857 int n,
2858 double *pfOut
2859 ){
2860 if( !sqlite3IsNumber((const char *)z, 0) ){
2861 Th_ErrorMessage(interp, "expected number, got: \"", z, n);
2862 return TH_ERROR;
2863 }
2864
2865 sqlite3AtoF((const char *)z, pfOut);
2866 return TH_OK;
@@ -2875,10 +2911,13 @@
2875 unsigned int uVal = iVal;
2876 char zBuf[32];
2877 char *z = &zBuf[32];
2878
2879 if( iVal<0 ){
 
 
 
2880 isNegative = 1;
2881 uVal = iVal * -1;
2882 }
2883 *(--z) = '\0';
2884 *(--z) = (char)(48+(uVal%10));
2885
--- src/th.c
+++ src/th.c
@@ -7,10 +7,16 @@
7 #include "config.h"
8 #include "th.h"
9 #include <string.h>
10 #include <assert.h>
11
12 /*
13 ** External routines
14 */
15 void fossil_panic(const char*,...);
16 void fossil_errorlog(const char*,...);
17
18 /*
19 ** Values used for element values in the tcl_platform array.
20 */
21
22 #if !defined(TH_ENGINE)
@@ -197,10 +203,11 @@
203 */
204 struct Buffer {
205 char *zBuf;
206 int nBuf;
207 int nBufAlloc;
208 int bTaint;
209 };
210 typedef struct Buffer Buffer;
211 static void thBufferInit(Buffer *);
212 static void thBufferFree(Th_Interp *interp, Buffer *);
213
@@ -209,10 +216,18 @@
216 ** be NULL as long as the number of bytes to copy is zero.
217 */
218 static void th_memcpy(void *dest, const void *src, size_t n){
219 if( n>0 ) memcpy(dest,src,n);
220 }
221
222 /*
223 ** An oversized string has been encountered. Do not try to recover.
224 ** Panic the process.
225 */
226 void Th_OversizeString(void){
227 fossil_panic("string too large. maximum size 286MB.");
228 }
229
230 /*
231 ** Append nAdd bytes of content copied from zAdd to the end of buffer
232 ** pBuffer. If there is not enough space currently allocated, resize
233 ** the allocation to make space.
@@ -219,40 +234,46 @@
234 */
235 static void thBufferWriteResize(
236 Th_Interp *interp,
237 Buffer *pBuffer,
238 const char *zAdd,
239 int nAddX
240 ){
241 int nAdd = TH1_LEN(nAddX);
242 int nNew = (pBuffer->nBuf+nAdd)*2+32;
243 #if defined(TH_MEMDEBUG)
244 char *zNew = (char *)Th_Malloc(interp, nNew);
245 TH1_SIZECHECK(nNew);
246 th_memcpy(zNew, pBuffer->zBuf, pBuffer->nBuf);
247 Th_Free(interp, pBuffer->zBuf);
248 pBuffer->zBuf = zNew;
249 #else
250 int nOld = pBuffer->nBufAlloc;
251 TH1_SIZECHECK(nNew);
252 pBuffer->zBuf = Th_Realloc(interp, pBuffer->zBuf, nNew);
253 memset(pBuffer->zBuf+nOld, 0, nNew-nOld);
254 #endif
255 pBuffer->nBufAlloc = nNew;
256 th_memcpy(&pBuffer->zBuf[pBuffer->nBuf], zAdd, nAdd);
257 pBuffer->nBuf += nAdd;
258 TH1_XFER_TAINT(pBuffer->bTaint, nAddX);
259 }
260 static void thBufferWriteFast(
261 Th_Interp *interp,
262 Buffer *pBuffer,
263 const char *zAdd,
264 int nAddX
265 ){
266 int nAdd = TH1_LEN(nAddX);
267 if( pBuffer->nBuf+nAdd > pBuffer->nBufAlloc ){
268 thBufferWriteResize(interp, pBuffer, zAdd, nAddX);
269 }else{
270 if( pBuffer->zBuf ){
271 memcpy(pBuffer->zBuf + pBuffer->nBuf, zAdd, nAdd);
272 }
273 pBuffer->nBuf += nAdd;
274 TH1_XFER_TAINT(pBuffer->bTaint, nAddX);
275 }
276 }
277 #define thBufferWrite(a,b,c,d) thBufferWriteFast(a,b,(const char *)c,d)
278
279 /*
@@ -704,24 +725,25 @@
725 int nWord
726 ){
727 int rc = TH_OK;
728 Buffer output;
729 int i;
730 int nn = TH1_LEN(nWord);
731
732 thBufferInit(&output);
733
734 if( nn>1 && (zWord[0]=='{' && zWord[nn-1]=='}') ){
735 thBufferWrite(interp, &output, &zWord[1], nn-2);
736 }else{
737
738 /* If the word is surrounded by double-quotes strip these away. */
739 if( nn>1 && (zWord[0]=='"' && zWord[nn-1]=='"') ){
740 zWord++;
741 nn -= 2;
742 }
743
744 for(i=0; rc==TH_OK && i<nn; i++){
745 int nGet;
746
747 int (*xGet)(Th_Interp *, const char*, int, int *) = 0;
748 int (*xSubst)(Th_Interp *, const char*, int) = 0;
749
@@ -743,11 +765,11 @@
765 thBufferAddChar(interp, &output, zWord[i]);
766 continue; /* Go to the next iteration of the for(...) loop */
767 }
768 }
769
770 rc = xGet(interp, &zWord[i], nn-i, &nGet);
771 if( rc==TH_OK ){
772 rc = xSubst(interp, &zWord[i], nGet);
773 }
774 if( rc==TH_OK ){
775 const char *zRes;
@@ -758,11 +780,11 @@
780 }
781 }
782 }
783
784 if( rc==TH_OK ){
785 Th_SetResult(interp, output.zBuf, output.nBuf|output.bTaint);
786 }
787 thBufferFree(interp, &output);
788 return rc;
789 }
790
@@ -826,11 +848,11 @@
848 Buffer strbuf;
849 Buffer lenbuf;
850 int nCount = 0;
851
852 const char *zInput = zList;
853 int nInput = TH1_LEN(nList);
854
855 thBufferInit(&strbuf);
856 thBufferInit(&lenbuf);
857
858 while( nInput>0 ){
@@ -837,19 +859,19 @@
859 const char *zWord;
860 int nWord;
861
862 thNextSpace(interp, zInput, nInput, &nWord);
863 zInput += nWord;
864 nInput = TH1_LEN(nList)-(zInput-zList);
865
866 if( TH_OK!=(rc = thNextWord(interp, zInput, nInput, &nWord, 0))
867 || TH_OK!=(rc = thSubstWord(interp, zInput, nWord))
868 ){
869 goto finish;
870 }
871 zInput = &zInput[TH1_LEN(nWord)];
872 nInput = TH1_LEN(nList)-(zInput-zList);
873 if( nWord>0 ){
874 zWord = Th_GetResult(interp, &nWord);
875 thBufferWrite(interp, &strbuf, zWord, nWord);
876 thBufferAddChar(interp, &strbuf, 0);
877 thBufferWrite(interp, &lenbuf, &nWord, sizeof(int));
@@ -872,11 +894,11 @@
894 zElem = (char *)&anElem[nCount];
895 th_memcpy(anElem, lenbuf.zBuf, lenbuf.nBuf);
896 th_memcpy(zElem, strbuf.zBuf, strbuf.nBuf);
897 for(i=0; i<nCount;i++){
898 azElem[i] = zElem;
899 zElem += (TH1_LEN(anElem[i]) + 1);
900 }
901 *pazElem = azElem;
902 *panElem = anElem;
903 }
904 if( pnCount ){
@@ -894,12 +916,17 @@
916 ** in the current stack frame.
917 */
918 static int thEvalLocal(Th_Interp *interp, const char *zProgram, int nProgram){
919 int rc = TH_OK;
920 const char *zInput = zProgram;
921 int nInput = TH1_LEN(nProgram);
922
923 if( TH1_TAINTED(nProgram)
924 && Th_ReportTaint(interp, "script", zProgram, nProgram)
925 ){
926 return TH_ERROR;
927 }
928 while( rc==TH_OK && nInput ){
929 Th_HashEntry *pEntry;
930 int nSpace;
931 const char *zFirst;
932
@@ -949,13 +976,13 @@
976 if( rc!=TH_OK ) continue;
977
978 if( argc>0 ){
979
980 /* Look up the command name in the command hash-table. */
981 pEntry = Th_HashFind(interp, interp->paCmd, argv[0], TH1_LEN(argl[0]),0);
982 if( !pEntry ){
983 Th_ErrorMessage(interp, "no such command: ", argv[0], TH1_LEN(argl[0]));
984 rc = TH_ERROR;
985 }
986
987 /* Call the command procedure. */
988 if( rc==TH_OK ){
@@ -1053,10 +1080,12 @@
1080 }else{
1081 int nInput = nProgram;
1082
1083 if( nInput<0 ){
1084 nInput = th_strlen(zProgram);
1085 }else{
1086 nInput = TH1_LEN(nInput);
1087 }
1088 rc = thEvalLocal(interp, zProgram, nInput);
1089 }
1090
1091 interp->pFrame = pSavedFrame;
@@ -1095,10 +1124,12 @@
1124 int isGlobal = 0;
1125 int i;
1126
1127 if( nVarname<0 ){
1128 nVarname = th_strlen(zVarname);
1129 }else{
1130 nVarname = TH1_LEN(nVarname);
1131 }
1132 nOuter = nVarname;
1133
1134 /* If the variable name starts with "::", then do the lookup is in the
1135 ** uppermost (global) frame.
@@ -1271,31 +1302,10 @@
1302 }
1303
1304 return Th_SetResult(interp, pValue->zData, pValue->nData);
1305 }
1306
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1307 /*
1308 ** Return true if variable (zVar, nVar) exists.
1309 */
1310 int Th_ExistsVar(Th_Interp *interp, const char *zVar, int nVar){
1311 Th_Variable *pValue = thFindValue(interp, zVar, nVar, 0, 1, 1, 0);
@@ -1324,28 +1334,32 @@
1334 int nVar,
1335 const char *zValue,
1336 int nValue
1337 ){
1338 Th_Variable *pValue;
1339 int nn;
1340
1341 nVar = TH1_LEN(nVar);
1342 pValue = thFindValue(interp, zVar, nVar, 1, 0, 0, 0);
1343 if( !pValue ){
1344 return TH_ERROR;
1345 }
1346
1347 if( nValue<0 ){
1348 nn = th_strlen(zValue);
1349 }else{
1350 nn = TH1_LEN(nValue);
1351 }
1352 if( pValue->zData ){
1353 Th_Free(interp, pValue->zData);
1354 pValue->zData = 0;
1355 }
1356
1357 assert(zValue || nn==0);
1358 pValue->zData = Th_Malloc(interp, nn+1);
1359 pValue->zData[nn] = '\0';
1360 th_memcpy(pValue->zData, zValue, nn);
1361 pValue->nData = nValue;
1362
1363 return TH_OK;
1364 }
1365
@@ -1458,10 +1472,12 @@
1472 */
1473 char *th_strdup(Th_Interp *interp, const char *z, int n){
1474 char *zRes;
1475 if( n<0 ){
1476 n = th_strlen(z);
1477 }else{
1478 n = TH1_LEN(n);
1479 }
1480 zRes = Th_Malloc(interp, n+1);
1481 th_memcpy(zRes, z, n);
1482 zRes[n] = '\0';
1483 return zRes;
@@ -1519,13 +1535,14 @@
1535 n = th_strlen(z);
1536 }
1537
1538 if( z && n>0 ){
1539 char *zResult;
1540 int nn = TH1_LEN(n);
1541 zResult = Th_Malloc(pInterp, nn+1);
1542 th_memcpy(zResult, z, nn);
1543 zResult[nn] = '\0';
1544 pInterp->zResult = zResult;
1545 pInterp->nResult = n;
1546 }
1547
1548 return TH_OK;
@@ -1777,15 +1794,19 @@
1794 int hasSpecialChar = 0; /* Whitespace or {}[]'" */
1795 int hasEscapeChar = 0; /* '}' without matching '{' to the left or a '\\' */
1796 int nBrace = 0;
1797
1798 output.zBuf = *pzList;
1799 output.nBuf = TH1_LEN(*pnList);
1800 output.nBufAlloc = output.nBuf;
1801 output.bTaint = 0;
1802 TH1_XFER_TAINT(output.bTaint, *pnList);
1803
1804 if( nElem<0 ){
1805 nElem = th_strlen(zElem);
1806 }else{
1807 nElem = TH1_LEN(nElem);
1808 }
1809 if( output.nBuf>0 ){
1810 thBufferAddChar(interp, &output, ' ');
1811 }
1812
@@ -1834,24 +1855,28 @@
1855 int *pnStr, /* IN/OUT: Current length of *pzStr */
1856 const char *zElem, /* Data to append */
1857 int nElem /* Length of nElem */
1858 ){
1859 char *zNew;
1860 long long int nNew;
1861 int nn;
1862
1863 if( nElem<0 ){
1864 nn = th_strlen(zElem);
1865 }else{
1866 nn = TH1_LEN(nElem);
1867 }
1868
1869 nNew = TH1_LEN(*pnStr) + nn;
1870 TH1_SIZECHECK(nNew);
1871 zNew = Th_Malloc(interp, nNew);
1872 th_memcpy(zNew, *pzStr, *pnStr);
1873 th_memcpy(&zNew[TH1_LEN(*pnStr)], zElem, nn);
1874
1875 Th_Free(interp, *pzStr);
1876 *pzStr = zNew;
1877 *pnStr = (int)nNew;
1878
1879 return TH_OK;
1880 }
1881
1882 /*
@@ -2106,16 +2131,18 @@
2131 /* Evaluate left and right arguments, if they exist. */
2132 if( pExpr->pLeft ){
2133 rc = exprEval(interp, pExpr->pLeft);
2134 if( rc==TH_OK ){
2135 zLeft = Th_TakeResult(interp, &nLeft);
2136 nLeft = TH1_LEN(nLeft);
2137 }
2138 }
2139 if( rc==TH_OK && pExpr->pRight ){
2140 rc = exprEval(interp, pExpr->pRight);
2141 if( rc==TH_OK ){
2142 zRight = Th_TakeResult(interp, &nRight);
2143 nRight = TH1_LEN(nRight);
2144 }
2145 }
2146
2147 /* Convert arguments to their required forms. */
2148 if( rc==TH_OK ){
@@ -2160,12 +2187,15 @@
2187 }
2188 iRes = iLeft%iRight;
2189 break;
2190 case OP_ADD: iRes = iLeft+iRight; break;
2191 case OP_SUBTRACT: iRes = iLeft-iRight; break;
2192 case OP_LEFTSHIFT: {
2193 iRes = (int)(((unsigned int)iLeft)<<(iRight&0x1f));
2194 break;
2195 }
2196 case OP_RIGHTSHIFT: iRes = iLeft>>(iRight&0x1f); break;
2197 case OP_LT: iRes = iLeft<iRight; break;
2198 case OP_GT: iRes = iLeft>iRight; break;
2199 case OP_LE: iRes = iLeft<=iRight; break;
2200 case OP_GE: iRes = iLeft>=iRight; break;
2201 case OP_EQ: iRes = iLeft==iRight; break;
@@ -2453,10 +2483,12 @@
2483 int nToken = 0;
2484 Expr **apToken = 0;
2485
2486 if( nExpr<0 ){
2487 nExpr = th_strlen(zExpr);
2488 }else{
2489 nExpr = TH1_LEN(nExpr);
2490 }
2491
2492 /* Parse the expression to a list of tokens. */
2493 rc = exprParse(interp, zExpr, nExpr, &apToken, &nToken);
2494
@@ -2564,10 +2596,12 @@
2596 Th_HashEntry *pRet;
2597 Th_HashEntry **ppRet;
2598
2599 if( nKey<0 ){
2600 nKey = th_strlen(zKey);
2601 }else{
2602 nKey = TH1_LEN(nKey);
2603 }
2604
2605 for(i=0; i<nKey; i++){
2606 iKey = (iKey<<3) ^ iKey ^ zKey[i];
2607 }
@@ -2797,10 +2831,12 @@
2831 int base = 10;
2832 int (*isdigit)(char) = th_isdigit;
2833
2834 if( n<0 ){
2835 n = th_strlen(z);
2836 }else{
2837 n = TH1_LEN(n);
2838 }
2839
2840 if( n>1 && (z[0]=='-' || z[0]=='+') ){
2841 i = 1;
2842 }
@@ -2856,11 +2892,11 @@
2892 const char *z,
2893 int n,
2894 double *pfOut
2895 ){
2896 if( !sqlite3IsNumber((const char *)z, 0) ){
2897 Th_ErrorMessage(interp, "expected number, got: \"", z, TH1_LEN(n));
2898 return TH_ERROR;
2899 }
2900
2901 sqlite3AtoF((const char *)z, pfOut);
2902 return TH_OK;
@@ -2875,10 +2911,13 @@
2911 unsigned int uVal = iVal;
2912 char zBuf[32];
2913 char *z = &zBuf[32];
2914
2915 if( iVal<0 ){
2916 if( iVal==0x80000000 ){
2917 return Th_SetResult(interp, "-2147483648", -1);
2918 }
2919 isNegative = 1;
2920 uVal = iVal * -1;
2921 }
2922 *(--z) = '\0';
2923 *(--z) = (char)(48+(uVal%10));
2924
+53 -14
--- src/th.h
+++ src/th.h
@@ -1,10 +1,56 @@
1
-
21
/* This header file defines the external interface to the custom Scripting
32
** Language (TH) interpreter. TH is very similar to Tcl but is not an
43
** exact clone.
4
+**
5
+** TH1 was original developed to run SQLite tests on SymbianOS. This version
6
+** of TH1 was repurposed as a scripted language for Fossil, and was heavily
7
+** modified for that purpose, beginning in early 2008.
8
+**
9
+** More recently, TH1 has been enhanced to distinguish between regular text
10
+** and "tainted" text. "Tainted" text is text that might have originated
11
+** from an outside source and hence might not be trustworthy. To prevent
12
+** cross-site scripting (XSS) and SQL-injections and similar attacks,
13
+** tainted text should not be used for the following purposes:
14
+**
15
+** * executed as TH1 script or expression.
16
+** * output as HTML or Javascript
17
+** * used as part of an SQL query
18
+**
19
+** Tainted text can be converted into a safe form using commands like
20
+** "htmlize". And some commands ("query" and "expr") know how to use
21
+** potentially tainted variable values directly, and thus can bypass
22
+** the restrictions above.
23
+**
24
+** Whether a string is clean or tainted is determined by its length integer.
25
+** TH1 limits strings to be no more than 0x0fffffff bytes bytes in length
26
+** (about 268MB - more than sufficient for the purposes of Fossil). The top
27
+** bit of the length integer is the sign bit, of course. The next three bits
28
+** are reserved. One of those, the 0x10000000 bit, marks tainted strings.
529
*/
30
+#define TH1_MX_STRLEN 0x0fffffff /* Maximum length of a TH1-C string */
31
+#define TH1_TAINT_BIT 0x10000000 /* The taint bit */
32
+#define TH1_SIGN 0x80000000
33
+
34
+/* Convert an integer into a string length. Negative values remain negative */
35
+#define TH1_LEN(X) ((TH1_SIGN|TH1_MX_STRLEN)&(X))
36
+
37
+/* Return true if the string is tainted */
38
+#define TH1_TAINTED(X) (((X)&TH1_TAINT_BIT)!=0)
39
+
40
+/* Remove taint from a string */
41
+#define TH1_RM_TAINT(X) ((X)&~TH1_TAINT_BIT)
42
+
43
+/* Add taint to a string */
44
+#define TH1_ADD_TAINT(X) ((X)|TH1_TAINT_BIT)
45
+
46
+/* If B is tainted, make A tainted too */
47
+#define TH1_XFER_TAINT(A,B) (A)|=(TH1_TAINT_BIT&(B))
48
+
49
+/* Check to see if a string is too big for TH1 */
50
+#define TH1_SIZECHECK(N) if((N)>TH1_MX_STRLEN){Th_OversizeString();}
51
+void Th_OversizeString(void);
652
753
/*
854
** Before creating an interpreter, the application must allocate and
955
** populate an instance of the following structure. It must remain valid
1056
** for the lifetime of the interpreter.
@@ -24,10 +70,16 @@
2470
** Create and delete interpreters.
2571
*/
2672
Th_Interp * Th_CreateInterp(Th_Vtab *);
2773
void Th_DeleteInterp(Th_Interp *);
2874
75
+/*
76
+** Report taint in the string zStr,nStr. That string represents "zTitle"
77
+** If non-zero is returned error out of the caller.
78
+*/
79
+int Th_ReportTaint(Th_Interp*,const char*,const char*zStr,int nStr);
80
+
2981
/*
3082
** Evaluate an TH program in the stack frame identified by parameter
3183
** iFrame, according to the following rules:
3284
**
3385
** * If iFrame is 0, this means the current frame.
@@ -56,23 +108,10 @@
56108
int Th_GetVar(Th_Interp *, const char *, int);
57109
int Th_SetVar(Th_Interp *, const char *, int, const char *, int);
58110
int Th_LinkVar(Th_Interp *, const char *, int, int, const char *, int);
59111
int Th_UnsetVar(Th_Interp *, const char *, int);
60112
61
-/*
62
-** If interp has a variable with the given name, its value is returned
63
-** and its length is returned via *nOut if nOut is not NULL. If
64
-** interp has no such var then NULL is returned without setting any
65
-** error state and *nOut, if not NULL, is set to 0. The returned value
66
-** is owned by the interpreter and may be invalidated the next time
67
-** the interpreter is modified.
68
-**
69
-** zVarName must be NUL-terminated.
70
-*/
71
-const char * Th_MaybeGetVar(Th_Interp *interp, const char *zVarName,
72
- int *nOut);
73
-
74113
typedef int (*Th_CommandProc)(Th_Interp *, void *, int, const char **, int *);
75114
76115
/*
77116
** Register new commands.
78117
*/
79118
--- src/th.h
+++ src/th.h
@@ -1,10 +1,56 @@
1
2 /* This header file defines the external interface to the custom Scripting
3 ** Language (TH) interpreter. TH is very similar to Tcl but is not an
4 ** exact clone.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5 */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
7 /*
8 ** Before creating an interpreter, the application must allocate and
9 ** populate an instance of the following structure. It must remain valid
10 ** for the lifetime of the interpreter.
@@ -24,10 +70,16 @@
24 ** Create and delete interpreters.
25 */
26 Th_Interp * Th_CreateInterp(Th_Vtab *);
27 void Th_DeleteInterp(Th_Interp *);
28
 
 
 
 
 
 
29 /*
30 ** Evaluate an TH program in the stack frame identified by parameter
31 ** iFrame, according to the following rules:
32 **
33 ** * If iFrame is 0, this means the current frame.
@@ -56,23 +108,10 @@
56 int Th_GetVar(Th_Interp *, const char *, int);
57 int Th_SetVar(Th_Interp *, const char *, int, const char *, int);
58 int Th_LinkVar(Th_Interp *, const char *, int, int, const char *, int);
59 int Th_UnsetVar(Th_Interp *, const char *, int);
60
61 /*
62 ** If interp has a variable with the given name, its value is returned
63 ** and its length is returned via *nOut if nOut is not NULL. If
64 ** interp has no such var then NULL is returned without setting any
65 ** error state and *nOut, if not NULL, is set to 0. The returned value
66 ** is owned by the interpreter and may be invalidated the next time
67 ** the interpreter is modified.
68 **
69 ** zVarName must be NUL-terminated.
70 */
71 const char * Th_MaybeGetVar(Th_Interp *interp, const char *zVarName,
72 int *nOut);
73
74 typedef int (*Th_CommandProc)(Th_Interp *, void *, int, const char **, int *);
75
76 /*
77 ** Register new commands.
78 */
79
--- src/th.h
+++ src/th.h
@@ -1,10 +1,56 @@
 
1 /* This header file defines the external interface to the custom Scripting
2 ** Language (TH) interpreter. TH is very similar to Tcl but is not an
3 ** exact clone.
4 **
5 ** TH1 was original developed to run SQLite tests on SymbianOS. This version
6 ** of TH1 was repurposed as a scripted language for Fossil, and was heavily
7 ** modified for that purpose, beginning in early 2008.
8 **
9 ** More recently, TH1 has been enhanced to distinguish between regular text
10 ** and "tainted" text. "Tainted" text is text that might have originated
11 ** from an outside source and hence might not be trustworthy. To prevent
12 ** cross-site scripting (XSS) and SQL-injections and similar attacks,
13 ** tainted text should not be used for the following purposes:
14 **
15 ** * executed as TH1 script or expression.
16 ** * output as HTML or Javascript
17 ** * used as part of an SQL query
18 **
19 ** Tainted text can be converted into a safe form using commands like
20 ** "htmlize". And some commands ("query" and "expr") know how to use
21 ** potentially tainted variable values directly, and thus can bypass
22 ** the restrictions above.
23 **
24 ** Whether a string is clean or tainted is determined by its length integer.
25 ** TH1 limits strings to be no more than 0x0fffffff bytes bytes in length
26 ** (about 268MB - more than sufficient for the purposes of Fossil). The top
27 ** bit of the length integer is the sign bit, of course. The next three bits
28 ** are reserved. One of those, the 0x10000000 bit, marks tainted strings.
29 */
30 #define TH1_MX_STRLEN 0x0fffffff /* Maximum length of a TH1-C string */
31 #define TH1_TAINT_BIT 0x10000000 /* The taint bit */
32 #define TH1_SIGN 0x80000000
33
34 /* Convert an integer into a string length. Negative values remain negative */
35 #define TH1_LEN(X) ((TH1_SIGN|TH1_MX_STRLEN)&(X))
36
37 /* Return true if the string is tainted */
38 #define TH1_TAINTED(X) (((X)&TH1_TAINT_BIT)!=0)
39
40 /* Remove taint from a string */
41 #define TH1_RM_TAINT(X) ((X)&~TH1_TAINT_BIT)
42
43 /* Add taint to a string */
44 #define TH1_ADD_TAINT(X) ((X)|TH1_TAINT_BIT)
45
46 /* If B is tainted, make A tainted too */
47 #define TH1_XFER_TAINT(A,B) (A)|=(TH1_TAINT_BIT&(B))
48
49 /* Check to see if a string is too big for TH1 */
50 #define TH1_SIZECHECK(N) if((N)>TH1_MX_STRLEN){Th_OversizeString();}
51 void Th_OversizeString(void);
52
53 /*
54 ** Before creating an interpreter, the application must allocate and
55 ** populate an instance of the following structure. It must remain valid
56 ** for the lifetime of the interpreter.
@@ -24,10 +70,16 @@
70 ** Create and delete interpreters.
71 */
72 Th_Interp * Th_CreateInterp(Th_Vtab *);
73 void Th_DeleteInterp(Th_Interp *);
74
75 /*
76 ** Report taint in the string zStr,nStr. That string represents "zTitle"
77 ** If non-zero is returned error out of the caller.
78 */
79 int Th_ReportTaint(Th_Interp*,const char*,const char*zStr,int nStr);
80
81 /*
82 ** Evaluate an TH program in the stack frame identified by parameter
83 ** iFrame, according to the following rules:
84 **
85 ** * If iFrame is 0, this means the current frame.
@@ -56,23 +108,10 @@
108 int Th_GetVar(Th_Interp *, const char *, int);
109 int Th_SetVar(Th_Interp *, const char *, int, const char *, int);
110 int Th_LinkVar(Th_Interp *, const char *, int, int, const char *, int);
111 int Th_UnsetVar(Th_Interp *, const char *, int);
112
 
 
 
 
 
 
 
 
 
 
 
 
 
113 typedef int (*Th_CommandProc)(Th_Interp *, void *, int, const char **, int *);
114
115 /*
116 ** Register new commands.
117 */
118
+92 -53
--- src/th_lang.c
+++ src/th_lang.c
@@ -39,11 +39,11 @@
3939
4040
rc = Th_Eval(interp, 0, argv[1], -1);
4141
if( argc==3 ){
4242
int nResult;
4343
const char *zResult = Th_GetResult(interp, &nResult);
44
- Th_SetVar(interp, argv[2], argl[2], zResult, nResult);
44
+ Th_SetVar(interp, argv[2], TH1_LEN(argl[2]), zResult, nResult);
4545
}
4646
4747
Th_SetResultInt(interp, rc);
4848
return TH_OK;
4949
}
@@ -180,20 +180,24 @@
180180
int nVar;
181181
char **azValue = 0;
182182
int *anValue;
183183
int nValue;
184184
int ii, jj;
185
+ int bTaint = 0;
185186
186187
if( argc!=4 ){
187188
return Th_WrongNumArgs(interp, "foreach varlist list script");
188189
}
189190
rc = Th_SplitList(interp, argv[1], argl[1], &azVar, &anVar, &nVar);
190191
if( rc ) return rc;
192
+ TH1_XFER_TAINT(bTaint, argl[2]);
191193
rc = Th_SplitList(interp, argv[2], argl[2], &azValue, &anValue, &nValue);
192194
for(ii=0; rc==TH_OK && ii<=nValue-nVar; ii+=nVar){
193195
for(jj=0; jj<nVar; jj++){
194
- Th_SetVar(interp, azVar[jj], anVar[jj], azValue[ii+jj], anValue[ii+jj]);
196
+ int x = anValue[ii+jj];
197
+ TH1_XFER_TAINT(x, bTaint);
198
+ Th_SetVar(interp, azVar[jj], anVar[jj], azValue[ii+jj], x);
195199
}
196200
rc = eval_loopbody(interp, argv[3], argl[3]);
197201
}
198202
if( rc==TH_BREAK ) rc = TH_OK;
199203
Th_Free(interp, azVar);
@@ -215,15 +219,18 @@
215219
int *argl
216220
){
217221
char *zList = 0;
218222
int nList = 0;
219223
int i;
224
+ int bTaint = 0;
220225
221226
for(i=1; i<argc; i++){
227
+ TH1_XFER_TAINT(bTaint,argl[i]);
222228
Th_ListAppend(interp, &zList, &nList, argv[i], argl[i]);
223229
}
224230
231
+ TH1_XFER_TAINT(nList, bTaint);
225232
Th_SetResult(interp, zList, nList);
226233
Th_Free(interp, zList);
227234
228235
return TH_OK;
229236
}
@@ -244,23 +251,27 @@
244251
int *argl
245252
){
246253
char *zList = 0;
247254
int nList = 0;
248255
int i, rc;
256
+ int bTaint = 0;
249257
250258
if( argc<2 ){
251259
return Th_WrongNumArgs(interp, "lappend var ...");
252260
}
253261
rc = Th_GetVar(interp, argv[1], argl[1]);
254262
if( rc==TH_OK ){
255263
zList = Th_TakeResult(interp, &nList);
256264
}
257265
266
+ TH1_XFER_TAINT(bTaint, nList);
258267
for(i=2; i<argc; i++){
268
+ TH1_XFER_TAINT(bTaint, argl[i]);
259269
Th_ListAppend(interp, &zList, &nList, argv[i], argl[i]);
260270
}
261271
272
+ TH1_XFER_TAINT(nList, bTaint);
262273
Th_SetVar(interp, argv[1], argl[1], zList, nList);
263274
Th_SetResult(interp, zList, nList);
264275
Th_Free(interp, zList);
265276
266277
return TH_OK;
@@ -283,23 +294,27 @@
283294
int rc;
284295
285296
char **azElem;
286297
int *anElem;
287298
int nCount;
299
+ int bTaint = 0;
288300
289301
if( argc!=3 ){
290302
return Th_WrongNumArgs(interp, "lindex list index");
291303
}
292304
293305
if( TH_OK!=Th_ToInt(interp, argv[2], argl[2], &iElem) ){
294306
return TH_ERROR;
295307
}
296308
309
+ TH1_XFER_TAINT(bTaint, argl[1]);
297310
rc = Th_SplitList(interp, argv[1], argl[1], &azElem, &anElem, &nCount);
298311
if( rc==TH_OK ){
299312
if( iElem<nCount && iElem>=0 ){
300
- Th_SetResult(interp, azElem[iElem], anElem[iElem]);
313
+ int sz = anElem[iElem];
314
+ TH1_XFER_TAINT(sz, bTaint);
315
+ Th_SetResult(interp, azElem[iElem], sz);
301316
}else{
302317
Th_SetResult(interp, 0, 0);
303318
}
304319
Th_Free(interp, azElem);
305320
}
@@ -356,13 +371,14 @@
356371
return Th_WrongNumArgs(interp, "lsearch list string");
357372
}
358373
359374
rc = Th_SplitList(interp, argv[1], argl[1], &azElem, &anElem, &nCount);
360375
if( rc==TH_OK ){
376
+ int nn = TH1_LEN(argl[2]);
361377
Th_SetResultInt(interp, -1);
362378
for(i=0; i<nCount; i++){
363
- if( anElem[i]==argl[2] && 0==memcmp(azElem[i], argv[2], argl[2]) ){
379
+ if( TH1_LEN(anElem[i])==nn && 0==memcmp(azElem[i], argv[2], nn) ){
364380
Th_SetResultInt(interp, i);
365381
break;
366382
}
367383
}
368384
Th_Free(interp, azElem);
@@ -561,28 +577,31 @@
561577
int nUsage = 0; /* Number of bytes at zUsage */
562578
563579
if( argc!=4 ){
564580
return Th_WrongNumArgs(interp, "proc name arglist code");
565581
}
566
- if( Th_SplitList(interp, argv[2], argl[2], &azParam, &anParam, &nParam) ){
582
+ if( Th_SplitList(interp, argv[2], TH1_LEN(argl[2]),
583
+ &azParam, &anParam, &nParam) ){
567584
return TH_ERROR;
568585
}
569586
570587
/* Allocate the new ProcDefn structure. */
571588
nByte = sizeof(ProcDefn) + /* ProcDefn structure */
572589
(sizeof(char *) + sizeof(int)) * nParam + /* azParam, anParam */
573590
(sizeof(char *) + sizeof(int)) * nParam + /* azDefault, anDefault */
574
- argl[3] + /* zProgram */
575
- argl[2]; /* Space for copies of parameter names and default values */
591
+ TH1_LEN(argl[3]) + /* zProgram */
592
+ TH1_LEN(argl[2]); /* Space for copies of param names and dflt values */
576593
p = (ProcDefn *)Th_Malloc(interp, nByte);
577594
578595
/* If the last parameter in the parameter list is "args", then set the
579596
** ProcDefn.hasArgs flag. The "args" parameter does not require an
580597
** entry in the ProcDefn.azParam[] or ProcDefn.azDefault[] arrays.
581598
*/
582599
if( nParam>0 ){
583
- if( anParam[nParam-1]==4 && 0==memcmp(azParam[nParam-1], "args", 4) ){
600
+ if( TH1_LEN(anParam[nParam-1])==4
601
+ && 0==memcmp(azParam[nParam-1], "args", 4)
602
+ ){
584603
p->hasArgs = 1;
585604
nParam--;
586605
}
587606
}
588607
@@ -590,12 +609,12 @@
590609
p->azParam = (char **)&p[1];
591610
p->anParam = (int *)&p->azParam[nParam];
592611
p->azDefault = (char **)&p->anParam[nParam];
593612
p->anDefault = (int *)&p->azDefault[nParam];
594613
p->zProgram = (char *)&p->anDefault[nParam];
595
- memcpy(p->zProgram, argv[3], argl[3]);
596
- p->nProgram = argl[3];
614
+ memcpy(p->zProgram, argv[3], TH1_LEN(argl[3]));
615
+ p->nProgram = TH1_LEN(argl[3]);
597616
zSpace = &p->zProgram[p->nProgram];
598617
599618
for(i=0; i<nParam; i++){
600619
char **az;
601620
int *an;
@@ -672,11 +691,12 @@
672691
int *argl
673692
){
674693
if( argc!=3 ){
675694
return Th_WrongNumArgs(interp, "rename oldcmd newcmd");
676695
}
677
- return Th_RenameCommand(interp, argv[1], argl[1], argv[2], argl[2]);
696
+ return Th_RenameCommand(interp, argv[1], TH1_LEN(argl[1]),
697
+ argv[2], TH1_LEN(argl[2]));
678698
}
679699
680700
/*
681701
** TH Syntax:
682702
**
@@ -746,13 +766,13 @@
746766
if( argc!=4 ){
747767
return Th_WrongNumArgs(interp, "string compare str1 str2");
748768
}
749769
750770
zLeft = argv[2];
751
- nLeft = argl[2];
771
+ nLeft = TH1_LEN(argl[2]);
752772
zRight = argv[3];
753
- nRight = argl[3];
773
+ nRight = TH1_LEN(argl[3]);
754774
755775
for(i=0; iRes==0 && i<nLeft && i<nRight; i++){
756776
iRes = zLeft[i]-zRight[i];
757777
}
758778
if( iRes==0 ){
@@ -779,12 +799,12 @@
779799
780800
if( argc!=4 ){
781801
return Th_WrongNumArgs(interp, "string first needle haystack");
782802
}
783803
784
- nNeedle = argl[2];
785
- nHaystack = argl[3];
804
+ nNeedle = TH1_LEN(argl[2]);
805
+ nHaystack = TH1_LEN(argl[3]);
786806
787807
if( nNeedle && nHaystack && nNeedle<=nHaystack ){
788808
const char *zNeedle = argv[2];
789809
const char *zHaystack = argv[3];
790810
int i;
@@ -812,20 +832,22 @@
812832
813833
if( argc!=4 ){
814834
return Th_WrongNumArgs(interp, "string index string index");
815835
}
816836
817
- if( argl[3]==3 && 0==memcmp("end", argv[3], 3) ){
818
- iIndex = argl[2]-1;
837
+ if( TH1_LEN(argl[3])==3 && 0==memcmp("end", argv[3], 3) ){
838
+ iIndex = TH1_LEN(argl[2])-1;
819839
}else if( Th_ToInt(interp, argv[3], argl[3], &iIndex) ){
820840
Th_ErrorMessage(
821841
interp, "Expected \"end\" or integer, got:", argv[3], argl[3]);
822842
return TH_ERROR;
823843
}
824844
825
- if( iIndex>=0 && iIndex<argl[2] ){
826
- return Th_SetResult(interp, &argv[2][iIndex], 1);
845
+ if( iIndex>=0 && iIndex<TH1_LEN(argl[2]) ){
846
+ int sz = 1;
847
+ TH1_XFER_TAINT(sz, argl[2]);
848
+ return Th_SetResult(interp, &argv[2][iIndex], sz);
827849
}else{
828850
return Th_SetResult(interp, 0, 0);
829851
}
830852
}
831853
@@ -838,41 +860,44 @@
838860
Th_Interp *interp, void *ctx, int argc, const char **argv, int *argl
839861
){
840862
if( argc!=4 ){
841863
return Th_WrongNumArgs(interp, "string is class string");
842864
}
843
- if( argl[2]==5 && 0==memcmp(argv[2], "alnum", 5) ){
865
+ if( TH1_LEN(argl[2])==5 && 0==memcmp(argv[2], "alnum", 5) ){
844866
int i;
845867
int iRes = 1;
846868
847
- for(i=0; i<argl[3]; i++){
869
+ for(i=0; i<TH1_LEN(argl[3]); i++){
848870
if( !th_isalnum(argv[3][i]) ){
849871
iRes = 0;
850872
}
851873
}
852874
853875
return Th_SetResultInt(interp, iRes);
854
- }else if( argl[2]==6 && 0==memcmp(argv[2], "double", 6) ){
876
+ }else if( TH1_LEN(argl[2])==6 && 0==memcmp(argv[2], "double", 6) ){
855877
double fVal;
856878
if( Th_ToDouble(interp, argv[3], argl[3], &fVal)==TH_OK ){
857879
return Th_SetResultInt(interp, 1);
858880
}
859881
return Th_SetResultInt(interp, 0);
860
- }else if( argl[2]==7 && 0==memcmp(argv[2], "integer", 7) ){
882
+ }else if( TH1_LEN(argl[2])==7 && 0==memcmp(argv[2], "integer", 7) ){
861883
int iVal;
862884
if( Th_ToInt(interp, argv[3], argl[3], &iVal)==TH_OK ){
863885
return Th_SetResultInt(interp, 1);
864886
}
865887
return Th_SetResultInt(interp, 0);
866
- }else if( argl[2]==4 && 0==memcmp(argv[2], "list", 4) ){
888
+ }else if( TH1_LEN(argl[2])==4 && 0==memcmp(argv[2], "list", 4) ){
867889
if( Th_SplitList(interp, argv[3], argl[3], 0, 0, 0)==TH_OK ){
868890
return Th_SetResultInt(interp, 1);
869891
}
870892
return Th_SetResultInt(interp, 0);
893
+ }else if( TH1_LEN(argl[2])==7 && 0==memcmp(argv[2], "tainted", 7) ){
894
+ return Th_SetResultInt(interp, TH1_TAINTED(argl[3]));
871895
}else{
872896
Th_ErrorMessage(interp,
873
- "Expected alnum, double, integer, or list, got:", argv[2], argl[2]);
897
+ "Expected alnum, double, integer, list, or tainted, got:",
898
+ argv[2], TH1_LEN(argl[2]));
874899
return TH_ERROR;
875900
}
876901
}
877902
878903
/*
@@ -889,12 +914,12 @@
889914
890915
if( argc!=4 ){
891916
return Th_WrongNumArgs(interp, "string last needle haystack");
892917
}
893918
894
- nNeedle = argl[2];
895
- nHaystack = argl[3];
919
+ nNeedle = TH1_LEN(argl[2]);
920
+ nHaystack = TH1_LEN(argl[3]);
896921
897922
if( nNeedle && nHaystack && nNeedle<=nHaystack ){
898923
const char *zNeedle = argv[2];
899924
const char *zHaystack = argv[3];
900925
int i;
@@ -919,11 +944,11 @@
919944
Th_Interp *interp, void *ctx, int argc, const char **argv, int *argl
920945
){
921946
if( argc!=3 ){
922947
return Th_WrongNumArgs(interp, "string length string");
923948
}
924
- return Th_SetResultInt(interp, argl[2]);
949
+ return Th_SetResultInt(interp, TH1_LEN(argl[2]));
925950
}
926951
927952
/*
928953
** TH Syntax:
929954
**
@@ -938,12 +963,12 @@
938963
char *zPat, *zStr;
939964
int rc;
940965
if( argc!=4 ){
941966
return Th_WrongNumArgs(interp, "string match pattern string");
942967
}
943
- zPat = fossil_strndup(argv[2],argl[2]);
944
- zStr = fossil_strndup(argv[3],argl[3]);
968
+ zPat = fossil_strndup(argv[2],TH1_LEN(argl[2]));
969
+ zStr = fossil_strndup(argv[3],TH1_LEN(argl[3]));
945970
rc = sqlite3_strglob(zPat,zStr);
946971
fossil_free(zPat);
947972
fossil_free(zStr);
948973
return Th_SetResultInt(interp, !rc);
949974
}
@@ -956,31 +981,34 @@
956981
static int string_range_command(
957982
Th_Interp *interp, void *ctx, int argc, const char **argv, int *argl
958983
){
959984
int iStart;
960985
int iEnd;
986
+ int sz;
961987
962988
if( argc!=5 ){
963989
return Th_WrongNumArgs(interp, "string range string first last");
964990
}
965991
966
- if( argl[4]==3 && 0==memcmp("end", argv[4], 3) ){
967
- iEnd = argl[2];
992
+ if( TH1_LEN(argl[4])==3 && 0==memcmp("end", argv[4], 3) ){
993
+ iEnd = TH1_LEN(argl[2]);
968994
}else if( Th_ToInt(interp, argv[4], argl[4], &iEnd) ){
969995
Th_ErrorMessage(
970
- interp, "Expected \"end\" or integer, got:", argv[4], argl[4]);
996
+ interp, "Expected \"end\" or integer, got:", argv[4], TH1_LEN(argl[4]));
971997
return TH_ERROR;
972998
}
973999
if( Th_ToInt(interp, argv[3], argl[3], &iStart) ){
9741000
return TH_ERROR;
9751001
}
9761002
9771003
if( iStart<0 ) iStart = 0;
978
- if( iEnd>=argl[2] ) iEnd = argl[2]-1;
1004
+ if( iEnd>=TH1_LEN(argl[2]) ) iEnd = TH1_LEN(argl[2])-1;
9791005
if( iStart>iEnd ) iEnd = iStart-1;
1006
+ sz = iEnd - iStart + 1;
1007
+ TH1_XFER_TAINT(sz, argl[2]);
9801008
981
- return Th_SetResult(interp, &argv[2][iStart], iEnd-iStart+1);
1009
+ return Th_SetResult(interp, &argv[2][iStart], sz);
9821010
}
9831011
9841012
/*
9851013
** TH Syntax:
9861014
**
@@ -989,27 +1017,33 @@
9891017
static int string_repeat_command(
9901018
Th_Interp *interp, void *ctx, int argc, const char **argv, int *argl
9911019
){
9921020
int n;
9931021
int i;
994
- int nByte;
1022
+ int sz;
1023
+ long long int nByte;
9951024
char *zByte;
9961025
9971026
if( argc!=4 ){
9981027
return Th_WrongNumArgs(interp, "string repeat string n");
9991028
}
10001029
if( Th_ToInt(interp, argv[3], argl[3], &n) ){
10011030
return TH_ERROR;
10021031
}
10031032
1004
- nByte = argl[2] * n;
1033
+ nByte = n;
1034
+ sz = TH1_LEN(argl[2]);
1035
+ nByte *= sz;
1036
+ TH1_SIZECHECK(nByte+1);
10051037
zByte = Th_Malloc(interp, nByte+1);
1006
- for(i=0; i<nByte; i+=argl[2]){
1007
- memcpy(&zByte[i], argv[2], argl[2]);
1038
+ for(i=0; i<nByte; i+=sz){
1039
+ memcpy(&zByte[i], argv[2], sz);
10081040
}
10091041
1010
- Th_SetResult(interp, zByte, nByte);
1042
+ n = nByte;
1043
+ TH1_XFER_TAINT(n, argl[2]);
1044
+ Th_SetResult(interp, zByte, n);
10111045
Th_Free(interp, zByte);
10121046
return TH_OK;
10131047
}
10141048
10151049
/*
@@ -1027,17 +1061,18 @@
10271061
10281062
if( argc!=3 ){
10291063
return Th_WrongNumArgs(interp, "string trim string");
10301064
}
10311065
z = argv[2];
1032
- n = argl[2];
1033
- if( argl[1]<5 || argv[1][4]=='l' ){
1066
+ n = TH1_LEN(argl[2]);
1067
+ if( TH1_LEN(argl[1])<5 || argv[1][4]=='l' ){
10341068
while( n && th_isspace(z[0]) ){ z++; n--; }
10351069
}
1036
- if( argl[1]<5 || argv[1][4]=='r' ){
1070
+ if( TH1_LEN(argl[1])<5 || argv[1][4]=='r' ){
10371071
while( n && th_isspace(z[n-1]) ){ n--; }
10381072
}
1073
+ TH1_XFER_TAINT(n, argl[2]);
10391074
Th_SetResult(interp, z, n);
10401075
return TH_OK;
10411076
}
10421077
10431078
/*
@@ -1051,11 +1086,11 @@
10511086
int rc;
10521087
10531088
if( argc!=3 ){
10541089
return Th_WrongNumArgs(interp, "info exists var");
10551090
}
1056
- rc = Th_ExistsVar(interp, argv[2], argl[2]);
1091
+ rc = Th_ExistsVar(interp, argv[2], TH1_LEN(argl[2]));
10571092
Th_SetResultInt(interp, rc);
10581093
return TH_OK;
10591094
}
10601095
10611096
/*
@@ -1117,11 +1152,11 @@
11171152
int rc;
11181153
11191154
if( argc!=3 ){
11201155
return Th_WrongNumArgs(interp, "array exists var");
11211156
}
1122
- rc = Th_ExistsArrayVar(interp, argv[2], argl[2]);
1157
+ rc = Th_ExistsArrayVar(interp, argv[2], TH1_LEN(argl[2]));
11231158
Th_SetResultInt(interp, rc);
11241159
return TH_OK;
11251160
}
11261161
11271162
/*
@@ -1137,11 +1172,11 @@
11371172
int nElem = 0;
11381173
11391174
if( argc!=3 ){
11401175
return Th_WrongNumArgs(interp, "array names varname");
11411176
}
1142
- rc = Th_ListAppendArray(interp, argv[2], argl[2], &zElem, &nElem);
1177
+ rc = Th_ListAppendArray(interp, argv[2], TH1_LEN(argl[2]), &zElem, &nElem);
11431178
if( rc!=TH_OK ){
11441179
return rc;
11451180
}
11461181
Th_SetResult(interp, zElem, nElem);
11471182
if( zElem ) Th_Free(interp, zElem);
@@ -1161,11 +1196,11 @@
11611196
int *argl
11621197
){
11631198
if( argc!=2 ){
11641199
return Th_WrongNumArgs(interp, "unset var");
11651200
}
1166
- return Th_UnsetVar(interp, argv[1], argl[1]);
1201
+ return Th_UnsetVar(interp, argv[1], TH1_LEN(argl[1]));
11671202
}
11681203
11691204
int Th_CallSubCommand(
11701205
Th_Interp *interp,
11711206
void *ctx,
@@ -1176,19 +1211,22 @@
11761211
){
11771212
if( argc>1 ){
11781213
int i;
11791214
for(i=0; aSub[i].zName; i++){
11801215
const char *zName = aSub[i].zName;
1181
- if( th_strlen(zName)==argl[1] && 0==memcmp(zName, argv[1], argl[1]) ){
1216
+ if( th_strlen(zName)==TH1_LEN(argl[1])
1217
+ && 0==memcmp(zName, argv[1], TH1_LEN(argl[1])) ){
11821218
return aSub[i].xProc(interp, ctx, argc, argv, argl);
11831219
}
11841220
}
11851221
}
11861222
if(argc<2){
1187
- Th_ErrorMessage(interp, "Expected sub-command for", argv[0], argl[0]);
1223
+ Th_ErrorMessage(interp, "Expected sub-command for",
1224
+ argv[0], TH1_LEN(argl[0]));
11881225
}else{
1189
- Th_ErrorMessage(interp, "Expected sub-command, got:", argv[1], argl[1]);
1226
+ Th_ErrorMessage(interp, "Expected sub-command, got:",
1227
+ argv[1], TH1_LEN(argl[1]));
11901228
}
11911229
return TH_ERROR;
11921230
}
11931231
11941232
/*
@@ -1319,11 +1357,11 @@
13191357
int iFrame = -1;
13201358
13211359
if( argc!=2 && argc!=3 ){
13221360
return Th_WrongNumArgs(interp, "uplevel ?level? script...");
13231361
}
1324
- if( argc==3 && TH_OK!=thToFrame(interp, argv[1], argl[1], &iFrame) ){
1362
+ if( argc==3 && TH_OK!=thToFrame(interp, argv[1], TH1_LEN(argl[1]), &iFrame) ){
13251363
return TH_ERROR;
13261364
}
13271365
return Th_Eval(interp, iFrame, argv[argc-1], -1);
13281366
}
13291367
@@ -1342,19 +1380,20 @@
13421380
int iVar = 1;
13431381
int iFrame = -1;
13441382
int rc = TH_OK;
13451383
int i;
13461384
1347
- if( TH_OK==thToFrame(0, argv[1], argl[1], &iFrame) ){
1385
+ if( TH_OK==thToFrame(0, argv[1], TH1_LEN(argl[1]), &iFrame) ){
13481386
iVar++;
13491387
}
13501388
if( argc==iVar || (argc-iVar)%2 ){
13511389
return Th_WrongNumArgs(interp,
13521390
"upvar frame othervar myvar ?othervar myvar...?");
13531391
}
13541392
for(i=iVar; rc==TH_OK && i<argc; i=i+2){
1355
- rc = Th_LinkVar(interp, argv[i+1], argl[i+1], iFrame, argv[i], argl[i]);
1393
+ rc = Th_LinkVar(interp, argv[i+1], TH1_LEN(argl[i+1]),
1394
+ iFrame, argv[i], TH1_LEN(argl[i]));
13561395
}
13571396
return rc;
13581397
}
13591398
13601399
/*
13611400
--- src/th_lang.c
+++ src/th_lang.c
@@ -39,11 +39,11 @@
39
40 rc = Th_Eval(interp, 0, argv[1], -1);
41 if( argc==3 ){
42 int nResult;
43 const char *zResult = Th_GetResult(interp, &nResult);
44 Th_SetVar(interp, argv[2], argl[2], zResult, nResult);
45 }
46
47 Th_SetResultInt(interp, rc);
48 return TH_OK;
49 }
@@ -180,20 +180,24 @@
180 int nVar;
181 char **azValue = 0;
182 int *anValue;
183 int nValue;
184 int ii, jj;
 
185
186 if( argc!=4 ){
187 return Th_WrongNumArgs(interp, "foreach varlist list script");
188 }
189 rc = Th_SplitList(interp, argv[1], argl[1], &azVar, &anVar, &nVar);
190 if( rc ) return rc;
 
191 rc = Th_SplitList(interp, argv[2], argl[2], &azValue, &anValue, &nValue);
192 for(ii=0; rc==TH_OK && ii<=nValue-nVar; ii+=nVar){
193 for(jj=0; jj<nVar; jj++){
194 Th_SetVar(interp, azVar[jj], anVar[jj], azValue[ii+jj], anValue[ii+jj]);
 
 
195 }
196 rc = eval_loopbody(interp, argv[3], argl[3]);
197 }
198 if( rc==TH_BREAK ) rc = TH_OK;
199 Th_Free(interp, azVar);
@@ -215,15 +219,18 @@
215 int *argl
216 ){
217 char *zList = 0;
218 int nList = 0;
219 int i;
 
220
221 for(i=1; i<argc; i++){
 
222 Th_ListAppend(interp, &zList, &nList, argv[i], argl[i]);
223 }
224
 
225 Th_SetResult(interp, zList, nList);
226 Th_Free(interp, zList);
227
228 return TH_OK;
229 }
@@ -244,23 +251,27 @@
244 int *argl
245 ){
246 char *zList = 0;
247 int nList = 0;
248 int i, rc;
 
249
250 if( argc<2 ){
251 return Th_WrongNumArgs(interp, "lappend var ...");
252 }
253 rc = Th_GetVar(interp, argv[1], argl[1]);
254 if( rc==TH_OK ){
255 zList = Th_TakeResult(interp, &nList);
256 }
257
 
258 for(i=2; i<argc; i++){
 
259 Th_ListAppend(interp, &zList, &nList, argv[i], argl[i]);
260 }
261
 
262 Th_SetVar(interp, argv[1], argl[1], zList, nList);
263 Th_SetResult(interp, zList, nList);
264 Th_Free(interp, zList);
265
266 return TH_OK;
@@ -283,23 +294,27 @@
283 int rc;
284
285 char **azElem;
286 int *anElem;
287 int nCount;
 
288
289 if( argc!=3 ){
290 return Th_WrongNumArgs(interp, "lindex list index");
291 }
292
293 if( TH_OK!=Th_ToInt(interp, argv[2], argl[2], &iElem) ){
294 return TH_ERROR;
295 }
296
 
297 rc = Th_SplitList(interp, argv[1], argl[1], &azElem, &anElem, &nCount);
298 if( rc==TH_OK ){
299 if( iElem<nCount && iElem>=0 ){
300 Th_SetResult(interp, azElem[iElem], anElem[iElem]);
 
 
301 }else{
302 Th_SetResult(interp, 0, 0);
303 }
304 Th_Free(interp, azElem);
305 }
@@ -356,13 +371,14 @@
356 return Th_WrongNumArgs(interp, "lsearch list string");
357 }
358
359 rc = Th_SplitList(interp, argv[1], argl[1], &azElem, &anElem, &nCount);
360 if( rc==TH_OK ){
 
361 Th_SetResultInt(interp, -1);
362 for(i=0; i<nCount; i++){
363 if( anElem[i]==argl[2] && 0==memcmp(azElem[i], argv[2], argl[2]) ){
364 Th_SetResultInt(interp, i);
365 break;
366 }
367 }
368 Th_Free(interp, azElem);
@@ -561,28 +577,31 @@
561 int nUsage = 0; /* Number of bytes at zUsage */
562
563 if( argc!=4 ){
564 return Th_WrongNumArgs(interp, "proc name arglist code");
565 }
566 if( Th_SplitList(interp, argv[2], argl[2], &azParam, &anParam, &nParam) ){
 
567 return TH_ERROR;
568 }
569
570 /* Allocate the new ProcDefn structure. */
571 nByte = sizeof(ProcDefn) + /* ProcDefn structure */
572 (sizeof(char *) + sizeof(int)) * nParam + /* azParam, anParam */
573 (sizeof(char *) + sizeof(int)) * nParam + /* azDefault, anDefault */
574 argl[3] + /* zProgram */
575 argl[2]; /* Space for copies of parameter names and default values */
576 p = (ProcDefn *)Th_Malloc(interp, nByte);
577
578 /* If the last parameter in the parameter list is "args", then set the
579 ** ProcDefn.hasArgs flag. The "args" parameter does not require an
580 ** entry in the ProcDefn.azParam[] or ProcDefn.azDefault[] arrays.
581 */
582 if( nParam>0 ){
583 if( anParam[nParam-1]==4 && 0==memcmp(azParam[nParam-1], "args", 4) ){
 
 
584 p->hasArgs = 1;
585 nParam--;
586 }
587 }
588
@@ -590,12 +609,12 @@
590 p->azParam = (char **)&p[1];
591 p->anParam = (int *)&p->azParam[nParam];
592 p->azDefault = (char **)&p->anParam[nParam];
593 p->anDefault = (int *)&p->azDefault[nParam];
594 p->zProgram = (char *)&p->anDefault[nParam];
595 memcpy(p->zProgram, argv[3], argl[3]);
596 p->nProgram = argl[3];
597 zSpace = &p->zProgram[p->nProgram];
598
599 for(i=0; i<nParam; i++){
600 char **az;
601 int *an;
@@ -672,11 +691,12 @@
672 int *argl
673 ){
674 if( argc!=3 ){
675 return Th_WrongNumArgs(interp, "rename oldcmd newcmd");
676 }
677 return Th_RenameCommand(interp, argv[1], argl[1], argv[2], argl[2]);
 
678 }
679
680 /*
681 ** TH Syntax:
682 **
@@ -746,13 +766,13 @@
746 if( argc!=4 ){
747 return Th_WrongNumArgs(interp, "string compare str1 str2");
748 }
749
750 zLeft = argv[2];
751 nLeft = argl[2];
752 zRight = argv[3];
753 nRight = argl[3];
754
755 for(i=0; iRes==0 && i<nLeft && i<nRight; i++){
756 iRes = zLeft[i]-zRight[i];
757 }
758 if( iRes==0 ){
@@ -779,12 +799,12 @@
779
780 if( argc!=4 ){
781 return Th_WrongNumArgs(interp, "string first needle haystack");
782 }
783
784 nNeedle = argl[2];
785 nHaystack = argl[3];
786
787 if( nNeedle && nHaystack && nNeedle<=nHaystack ){
788 const char *zNeedle = argv[2];
789 const char *zHaystack = argv[3];
790 int i;
@@ -812,20 +832,22 @@
812
813 if( argc!=4 ){
814 return Th_WrongNumArgs(interp, "string index string index");
815 }
816
817 if( argl[3]==3 && 0==memcmp("end", argv[3], 3) ){
818 iIndex = argl[2]-1;
819 }else if( Th_ToInt(interp, argv[3], argl[3], &iIndex) ){
820 Th_ErrorMessage(
821 interp, "Expected \"end\" or integer, got:", argv[3], argl[3]);
822 return TH_ERROR;
823 }
824
825 if( iIndex>=0 && iIndex<argl[2] ){
826 return Th_SetResult(interp, &argv[2][iIndex], 1);
 
 
827 }else{
828 return Th_SetResult(interp, 0, 0);
829 }
830 }
831
@@ -838,41 +860,44 @@
838 Th_Interp *interp, void *ctx, int argc, const char **argv, int *argl
839 ){
840 if( argc!=4 ){
841 return Th_WrongNumArgs(interp, "string is class string");
842 }
843 if( argl[2]==5 && 0==memcmp(argv[2], "alnum", 5) ){
844 int i;
845 int iRes = 1;
846
847 for(i=0; i<argl[3]; i++){
848 if( !th_isalnum(argv[3][i]) ){
849 iRes = 0;
850 }
851 }
852
853 return Th_SetResultInt(interp, iRes);
854 }else if( argl[2]==6 && 0==memcmp(argv[2], "double", 6) ){
855 double fVal;
856 if( Th_ToDouble(interp, argv[3], argl[3], &fVal)==TH_OK ){
857 return Th_SetResultInt(interp, 1);
858 }
859 return Th_SetResultInt(interp, 0);
860 }else if( argl[2]==7 && 0==memcmp(argv[2], "integer", 7) ){
861 int iVal;
862 if( Th_ToInt(interp, argv[3], argl[3], &iVal)==TH_OK ){
863 return Th_SetResultInt(interp, 1);
864 }
865 return Th_SetResultInt(interp, 0);
866 }else if( argl[2]==4 && 0==memcmp(argv[2], "list", 4) ){
867 if( Th_SplitList(interp, argv[3], argl[3], 0, 0, 0)==TH_OK ){
868 return Th_SetResultInt(interp, 1);
869 }
870 return Th_SetResultInt(interp, 0);
 
 
871 }else{
872 Th_ErrorMessage(interp,
873 "Expected alnum, double, integer, or list, got:", argv[2], argl[2]);
 
874 return TH_ERROR;
875 }
876 }
877
878 /*
@@ -889,12 +914,12 @@
889
890 if( argc!=4 ){
891 return Th_WrongNumArgs(interp, "string last needle haystack");
892 }
893
894 nNeedle = argl[2];
895 nHaystack = argl[3];
896
897 if( nNeedle && nHaystack && nNeedle<=nHaystack ){
898 const char *zNeedle = argv[2];
899 const char *zHaystack = argv[3];
900 int i;
@@ -919,11 +944,11 @@
919 Th_Interp *interp, void *ctx, int argc, const char **argv, int *argl
920 ){
921 if( argc!=3 ){
922 return Th_WrongNumArgs(interp, "string length string");
923 }
924 return Th_SetResultInt(interp, argl[2]);
925 }
926
927 /*
928 ** TH Syntax:
929 **
@@ -938,12 +963,12 @@
938 char *zPat, *zStr;
939 int rc;
940 if( argc!=4 ){
941 return Th_WrongNumArgs(interp, "string match pattern string");
942 }
943 zPat = fossil_strndup(argv[2],argl[2]);
944 zStr = fossil_strndup(argv[3],argl[3]);
945 rc = sqlite3_strglob(zPat,zStr);
946 fossil_free(zPat);
947 fossil_free(zStr);
948 return Th_SetResultInt(interp, !rc);
949 }
@@ -956,31 +981,34 @@
956 static int string_range_command(
957 Th_Interp *interp, void *ctx, int argc, const char **argv, int *argl
958 ){
959 int iStart;
960 int iEnd;
 
961
962 if( argc!=5 ){
963 return Th_WrongNumArgs(interp, "string range string first last");
964 }
965
966 if( argl[4]==3 && 0==memcmp("end", argv[4], 3) ){
967 iEnd = argl[2];
968 }else if( Th_ToInt(interp, argv[4], argl[4], &iEnd) ){
969 Th_ErrorMessage(
970 interp, "Expected \"end\" or integer, got:", argv[4], argl[4]);
971 return TH_ERROR;
972 }
973 if( Th_ToInt(interp, argv[3], argl[3], &iStart) ){
974 return TH_ERROR;
975 }
976
977 if( iStart<0 ) iStart = 0;
978 if( iEnd>=argl[2] ) iEnd = argl[2]-1;
979 if( iStart>iEnd ) iEnd = iStart-1;
 
 
980
981 return Th_SetResult(interp, &argv[2][iStart], iEnd-iStart+1);
982 }
983
984 /*
985 ** TH Syntax:
986 **
@@ -989,27 +1017,33 @@
989 static int string_repeat_command(
990 Th_Interp *interp, void *ctx, int argc, const char **argv, int *argl
991 ){
992 int n;
993 int i;
994 int nByte;
 
995 char *zByte;
996
997 if( argc!=4 ){
998 return Th_WrongNumArgs(interp, "string repeat string n");
999 }
1000 if( Th_ToInt(interp, argv[3], argl[3], &n) ){
1001 return TH_ERROR;
1002 }
1003
1004 nByte = argl[2] * n;
 
 
 
1005 zByte = Th_Malloc(interp, nByte+1);
1006 for(i=0; i<nByte; i+=argl[2]){
1007 memcpy(&zByte[i], argv[2], argl[2]);
1008 }
1009
1010 Th_SetResult(interp, zByte, nByte);
 
 
1011 Th_Free(interp, zByte);
1012 return TH_OK;
1013 }
1014
1015 /*
@@ -1027,17 +1061,18 @@
1027
1028 if( argc!=3 ){
1029 return Th_WrongNumArgs(interp, "string trim string");
1030 }
1031 z = argv[2];
1032 n = argl[2];
1033 if( argl[1]<5 || argv[1][4]=='l' ){
1034 while( n && th_isspace(z[0]) ){ z++; n--; }
1035 }
1036 if( argl[1]<5 || argv[1][4]=='r' ){
1037 while( n && th_isspace(z[n-1]) ){ n--; }
1038 }
 
1039 Th_SetResult(interp, z, n);
1040 return TH_OK;
1041 }
1042
1043 /*
@@ -1051,11 +1086,11 @@
1051 int rc;
1052
1053 if( argc!=3 ){
1054 return Th_WrongNumArgs(interp, "info exists var");
1055 }
1056 rc = Th_ExistsVar(interp, argv[2], argl[2]);
1057 Th_SetResultInt(interp, rc);
1058 return TH_OK;
1059 }
1060
1061 /*
@@ -1117,11 +1152,11 @@
1117 int rc;
1118
1119 if( argc!=3 ){
1120 return Th_WrongNumArgs(interp, "array exists var");
1121 }
1122 rc = Th_ExistsArrayVar(interp, argv[2], argl[2]);
1123 Th_SetResultInt(interp, rc);
1124 return TH_OK;
1125 }
1126
1127 /*
@@ -1137,11 +1172,11 @@
1137 int nElem = 0;
1138
1139 if( argc!=3 ){
1140 return Th_WrongNumArgs(interp, "array names varname");
1141 }
1142 rc = Th_ListAppendArray(interp, argv[2], argl[2], &zElem, &nElem);
1143 if( rc!=TH_OK ){
1144 return rc;
1145 }
1146 Th_SetResult(interp, zElem, nElem);
1147 if( zElem ) Th_Free(interp, zElem);
@@ -1161,11 +1196,11 @@
1161 int *argl
1162 ){
1163 if( argc!=2 ){
1164 return Th_WrongNumArgs(interp, "unset var");
1165 }
1166 return Th_UnsetVar(interp, argv[1], argl[1]);
1167 }
1168
1169 int Th_CallSubCommand(
1170 Th_Interp *interp,
1171 void *ctx,
@@ -1176,19 +1211,22 @@
1176 ){
1177 if( argc>1 ){
1178 int i;
1179 for(i=0; aSub[i].zName; i++){
1180 const char *zName = aSub[i].zName;
1181 if( th_strlen(zName)==argl[1] && 0==memcmp(zName, argv[1], argl[1]) ){
 
1182 return aSub[i].xProc(interp, ctx, argc, argv, argl);
1183 }
1184 }
1185 }
1186 if(argc<2){
1187 Th_ErrorMessage(interp, "Expected sub-command for", argv[0], argl[0]);
 
1188 }else{
1189 Th_ErrorMessage(interp, "Expected sub-command, got:", argv[1], argl[1]);
 
1190 }
1191 return TH_ERROR;
1192 }
1193
1194 /*
@@ -1319,11 +1357,11 @@
1319 int iFrame = -1;
1320
1321 if( argc!=2 && argc!=3 ){
1322 return Th_WrongNumArgs(interp, "uplevel ?level? script...");
1323 }
1324 if( argc==3 && TH_OK!=thToFrame(interp, argv[1], argl[1], &iFrame) ){
1325 return TH_ERROR;
1326 }
1327 return Th_Eval(interp, iFrame, argv[argc-1], -1);
1328 }
1329
@@ -1342,19 +1380,20 @@
1342 int iVar = 1;
1343 int iFrame = -1;
1344 int rc = TH_OK;
1345 int i;
1346
1347 if( TH_OK==thToFrame(0, argv[1], argl[1], &iFrame) ){
1348 iVar++;
1349 }
1350 if( argc==iVar || (argc-iVar)%2 ){
1351 return Th_WrongNumArgs(interp,
1352 "upvar frame othervar myvar ?othervar myvar...?");
1353 }
1354 for(i=iVar; rc==TH_OK && i<argc; i=i+2){
1355 rc = Th_LinkVar(interp, argv[i+1], argl[i+1], iFrame, argv[i], argl[i]);
 
1356 }
1357 return rc;
1358 }
1359
1360 /*
1361
--- src/th_lang.c
+++ src/th_lang.c
@@ -39,11 +39,11 @@
39
40 rc = Th_Eval(interp, 0, argv[1], -1);
41 if( argc==3 ){
42 int nResult;
43 const char *zResult = Th_GetResult(interp, &nResult);
44 Th_SetVar(interp, argv[2], TH1_LEN(argl[2]), zResult, nResult);
45 }
46
47 Th_SetResultInt(interp, rc);
48 return TH_OK;
49 }
@@ -180,20 +180,24 @@
180 int nVar;
181 char **azValue = 0;
182 int *anValue;
183 int nValue;
184 int ii, jj;
185 int bTaint = 0;
186
187 if( argc!=4 ){
188 return Th_WrongNumArgs(interp, "foreach varlist list script");
189 }
190 rc = Th_SplitList(interp, argv[1], argl[1], &azVar, &anVar, &nVar);
191 if( rc ) return rc;
192 TH1_XFER_TAINT(bTaint, argl[2]);
193 rc = Th_SplitList(interp, argv[2], argl[2], &azValue, &anValue, &nValue);
194 for(ii=0; rc==TH_OK && ii<=nValue-nVar; ii+=nVar){
195 for(jj=0; jj<nVar; jj++){
196 int x = anValue[ii+jj];
197 TH1_XFER_TAINT(x, bTaint);
198 Th_SetVar(interp, azVar[jj], anVar[jj], azValue[ii+jj], x);
199 }
200 rc = eval_loopbody(interp, argv[3], argl[3]);
201 }
202 if( rc==TH_BREAK ) rc = TH_OK;
203 Th_Free(interp, azVar);
@@ -215,15 +219,18 @@
219 int *argl
220 ){
221 char *zList = 0;
222 int nList = 0;
223 int i;
224 int bTaint = 0;
225
226 for(i=1; i<argc; i++){
227 TH1_XFER_TAINT(bTaint,argl[i]);
228 Th_ListAppend(interp, &zList, &nList, argv[i], argl[i]);
229 }
230
231 TH1_XFER_TAINT(nList, bTaint);
232 Th_SetResult(interp, zList, nList);
233 Th_Free(interp, zList);
234
235 return TH_OK;
236 }
@@ -244,23 +251,27 @@
251 int *argl
252 ){
253 char *zList = 0;
254 int nList = 0;
255 int i, rc;
256 int bTaint = 0;
257
258 if( argc<2 ){
259 return Th_WrongNumArgs(interp, "lappend var ...");
260 }
261 rc = Th_GetVar(interp, argv[1], argl[1]);
262 if( rc==TH_OK ){
263 zList = Th_TakeResult(interp, &nList);
264 }
265
266 TH1_XFER_TAINT(bTaint, nList);
267 for(i=2; i<argc; i++){
268 TH1_XFER_TAINT(bTaint, argl[i]);
269 Th_ListAppend(interp, &zList, &nList, argv[i], argl[i]);
270 }
271
272 TH1_XFER_TAINT(nList, bTaint);
273 Th_SetVar(interp, argv[1], argl[1], zList, nList);
274 Th_SetResult(interp, zList, nList);
275 Th_Free(interp, zList);
276
277 return TH_OK;
@@ -283,23 +294,27 @@
294 int rc;
295
296 char **azElem;
297 int *anElem;
298 int nCount;
299 int bTaint = 0;
300
301 if( argc!=3 ){
302 return Th_WrongNumArgs(interp, "lindex list index");
303 }
304
305 if( TH_OK!=Th_ToInt(interp, argv[2], argl[2], &iElem) ){
306 return TH_ERROR;
307 }
308
309 TH1_XFER_TAINT(bTaint, argl[1]);
310 rc = Th_SplitList(interp, argv[1], argl[1], &azElem, &anElem, &nCount);
311 if( rc==TH_OK ){
312 if( iElem<nCount && iElem>=0 ){
313 int sz = anElem[iElem];
314 TH1_XFER_TAINT(sz, bTaint);
315 Th_SetResult(interp, azElem[iElem], sz);
316 }else{
317 Th_SetResult(interp, 0, 0);
318 }
319 Th_Free(interp, azElem);
320 }
@@ -356,13 +371,14 @@
371 return Th_WrongNumArgs(interp, "lsearch list string");
372 }
373
374 rc = Th_SplitList(interp, argv[1], argl[1], &azElem, &anElem, &nCount);
375 if( rc==TH_OK ){
376 int nn = TH1_LEN(argl[2]);
377 Th_SetResultInt(interp, -1);
378 for(i=0; i<nCount; i++){
379 if( TH1_LEN(anElem[i])==nn && 0==memcmp(azElem[i], argv[2], nn) ){
380 Th_SetResultInt(interp, i);
381 break;
382 }
383 }
384 Th_Free(interp, azElem);
@@ -561,28 +577,31 @@
577 int nUsage = 0; /* Number of bytes at zUsage */
578
579 if( argc!=4 ){
580 return Th_WrongNumArgs(interp, "proc name arglist code");
581 }
582 if( Th_SplitList(interp, argv[2], TH1_LEN(argl[2]),
583 &azParam, &anParam, &nParam) ){
584 return TH_ERROR;
585 }
586
587 /* Allocate the new ProcDefn structure. */
588 nByte = sizeof(ProcDefn) + /* ProcDefn structure */
589 (sizeof(char *) + sizeof(int)) * nParam + /* azParam, anParam */
590 (sizeof(char *) + sizeof(int)) * nParam + /* azDefault, anDefault */
591 TH1_LEN(argl[3]) + /* zProgram */
592 TH1_LEN(argl[2]); /* Space for copies of param names and dflt values */
593 p = (ProcDefn *)Th_Malloc(interp, nByte);
594
595 /* If the last parameter in the parameter list is "args", then set the
596 ** ProcDefn.hasArgs flag. The "args" parameter does not require an
597 ** entry in the ProcDefn.azParam[] or ProcDefn.azDefault[] arrays.
598 */
599 if( nParam>0 ){
600 if( TH1_LEN(anParam[nParam-1])==4
601 && 0==memcmp(azParam[nParam-1], "args", 4)
602 ){
603 p->hasArgs = 1;
604 nParam--;
605 }
606 }
607
@@ -590,12 +609,12 @@
609 p->azParam = (char **)&p[1];
610 p->anParam = (int *)&p->azParam[nParam];
611 p->azDefault = (char **)&p->anParam[nParam];
612 p->anDefault = (int *)&p->azDefault[nParam];
613 p->zProgram = (char *)&p->anDefault[nParam];
614 memcpy(p->zProgram, argv[3], TH1_LEN(argl[3]));
615 p->nProgram = TH1_LEN(argl[3]);
616 zSpace = &p->zProgram[p->nProgram];
617
618 for(i=0; i<nParam; i++){
619 char **az;
620 int *an;
@@ -672,11 +691,12 @@
691 int *argl
692 ){
693 if( argc!=3 ){
694 return Th_WrongNumArgs(interp, "rename oldcmd newcmd");
695 }
696 return Th_RenameCommand(interp, argv[1], TH1_LEN(argl[1]),
697 argv[2], TH1_LEN(argl[2]));
698 }
699
700 /*
701 ** TH Syntax:
702 **
@@ -746,13 +766,13 @@
766 if( argc!=4 ){
767 return Th_WrongNumArgs(interp, "string compare str1 str2");
768 }
769
770 zLeft = argv[2];
771 nLeft = TH1_LEN(argl[2]);
772 zRight = argv[3];
773 nRight = TH1_LEN(argl[3]);
774
775 for(i=0; iRes==0 && i<nLeft && i<nRight; i++){
776 iRes = zLeft[i]-zRight[i];
777 }
778 if( iRes==0 ){
@@ -779,12 +799,12 @@
799
800 if( argc!=4 ){
801 return Th_WrongNumArgs(interp, "string first needle haystack");
802 }
803
804 nNeedle = TH1_LEN(argl[2]);
805 nHaystack = TH1_LEN(argl[3]);
806
807 if( nNeedle && nHaystack && nNeedle<=nHaystack ){
808 const char *zNeedle = argv[2];
809 const char *zHaystack = argv[3];
810 int i;
@@ -812,20 +832,22 @@
832
833 if( argc!=4 ){
834 return Th_WrongNumArgs(interp, "string index string index");
835 }
836
837 if( TH1_LEN(argl[3])==3 && 0==memcmp("end", argv[3], 3) ){
838 iIndex = TH1_LEN(argl[2])-1;
839 }else if( Th_ToInt(interp, argv[3], argl[3], &iIndex) ){
840 Th_ErrorMessage(
841 interp, "Expected \"end\" or integer, got:", argv[3], argl[3]);
842 return TH_ERROR;
843 }
844
845 if( iIndex>=0 && iIndex<TH1_LEN(argl[2]) ){
846 int sz = 1;
847 TH1_XFER_TAINT(sz, argl[2]);
848 return Th_SetResult(interp, &argv[2][iIndex], sz);
849 }else{
850 return Th_SetResult(interp, 0, 0);
851 }
852 }
853
@@ -838,41 +860,44 @@
860 Th_Interp *interp, void *ctx, int argc, const char **argv, int *argl
861 ){
862 if( argc!=4 ){
863 return Th_WrongNumArgs(interp, "string is class string");
864 }
865 if( TH1_LEN(argl[2])==5 && 0==memcmp(argv[2], "alnum", 5) ){
866 int i;
867 int iRes = 1;
868
869 for(i=0; i<TH1_LEN(argl[3]); i++){
870 if( !th_isalnum(argv[3][i]) ){
871 iRes = 0;
872 }
873 }
874
875 return Th_SetResultInt(interp, iRes);
876 }else if( TH1_LEN(argl[2])==6 && 0==memcmp(argv[2], "double", 6) ){
877 double fVal;
878 if( Th_ToDouble(interp, argv[3], argl[3], &fVal)==TH_OK ){
879 return Th_SetResultInt(interp, 1);
880 }
881 return Th_SetResultInt(interp, 0);
882 }else if( TH1_LEN(argl[2])==7 && 0==memcmp(argv[2], "integer", 7) ){
883 int iVal;
884 if( Th_ToInt(interp, argv[3], argl[3], &iVal)==TH_OK ){
885 return Th_SetResultInt(interp, 1);
886 }
887 return Th_SetResultInt(interp, 0);
888 }else if( TH1_LEN(argl[2])==4 && 0==memcmp(argv[2], "list", 4) ){
889 if( Th_SplitList(interp, argv[3], argl[3], 0, 0, 0)==TH_OK ){
890 return Th_SetResultInt(interp, 1);
891 }
892 return Th_SetResultInt(interp, 0);
893 }else if( TH1_LEN(argl[2])==7 && 0==memcmp(argv[2], "tainted", 7) ){
894 return Th_SetResultInt(interp, TH1_TAINTED(argl[3]));
895 }else{
896 Th_ErrorMessage(interp,
897 "Expected alnum, double, integer, list, or tainted, got:",
898 argv[2], TH1_LEN(argl[2]));
899 return TH_ERROR;
900 }
901 }
902
903 /*
@@ -889,12 +914,12 @@
914
915 if( argc!=4 ){
916 return Th_WrongNumArgs(interp, "string last needle haystack");
917 }
918
919 nNeedle = TH1_LEN(argl[2]);
920 nHaystack = TH1_LEN(argl[3]);
921
922 if( nNeedle && nHaystack && nNeedle<=nHaystack ){
923 const char *zNeedle = argv[2];
924 const char *zHaystack = argv[3];
925 int i;
@@ -919,11 +944,11 @@
944 Th_Interp *interp, void *ctx, int argc, const char **argv, int *argl
945 ){
946 if( argc!=3 ){
947 return Th_WrongNumArgs(interp, "string length string");
948 }
949 return Th_SetResultInt(interp, TH1_LEN(argl[2]));
950 }
951
952 /*
953 ** TH Syntax:
954 **
@@ -938,12 +963,12 @@
963 char *zPat, *zStr;
964 int rc;
965 if( argc!=4 ){
966 return Th_WrongNumArgs(interp, "string match pattern string");
967 }
968 zPat = fossil_strndup(argv[2],TH1_LEN(argl[2]));
969 zStr = fossil_strndup(argv[3],TH1_LEN(argl[3]));
970 rc = sqlite3_strglob(zPat,zStr);
971 fossil_free(zPat);
972 fossil_free(zStr);
973 return Th_SetResultInt(interp, !rc);
974 }
@@ -956,31 +981,34 @@
981 static int string_range_command(
982 Th_Interp *interp, void *ctx, int argc, const char **argv, int *argl
983 ){
984 int iStart;
985 int iEnd;
986 int sz;
987
988 if( argc!=5 ){
989 return Th_WrongNumArgs(interp, "string range string first last");
990 }
991
992 if( TH1_LEN(argl[4])==3 && 0==memcmp("end", argv[4], 3) ){
993 iEnd = TH1_LEN(argl[2]);
994 }else if( Th_ToInt(interp, argv[4], argl[4], &iEnd) ){
995 Th_ErrorMessage(
996 interp, "Expected \"end\" or integer, got:", argv[4], TH1_LEN(argl[4]));
997 return TH_ERROR;
998 }
999 if( Th_ToInt(interp, argv[3], argl[3], &iStart) ){
1000 return TH_ERROR;
1001 }
1002
1003 if( iStart<0 ) iStart = 0;
1004 if( iEnd>=TH1_LEN(argl[2]) ) iEnd = TH1_LEN(argl[2])-1;
1005 if( iStart>iEnd ) iEnd = iStart-1;
1006 sz = iEnd - iStart + 1;
1007 TH1_XFER_TAINT(sz, argl[2]);
1008
1009 return Th_SetResult(interp, &argv[2][iStart], sz);
1010 }
1011
1012 /*
1013 ** TH Syntax:
1014 **
@@ -989,27 +1017,33 @@
1017 static int string_repeat_command(
1018 Th_Interp *interp, void *ctx, int argc, const char **argv, int *argl
1019 ){
1020 int n;
1021 int i;
1022 int sz;
1023 long long int nByte;
1024 char *zByte;
1025
1026 if( argc!=4 ){
1027 return Th_WrongNumArgs(interp, "string repeat string n");
1028 }
1029 if( Th_ToInt(interp, argv[3], argl[3], &n) ){
1030 return TH_ERROR;
1031 }
1032
1033 nByte = n;
1034 sz = TH1_LEN(argl[2]);
1035 nByte *= sz;
1036 TH1_SIZECHECK(nByte+1);
1037 zByte = Th_Malloc(interp, nByte+1);
1038 for(i=0; i<nByte; i+=sz){
1039 memcpy(&zByte[i], argv[2], sz);
1040 }
1041
1042 n = nByte;
1043 TH1_XFER_TAINT(n, argl[2]);
1044 Th_SetResult(interp, zByte, n);
1045 Th_Free(interp, zByte);
1046 return TH_OK;
1047 }
1048
1049 /*
@@ -1027,17 +1061,18 @@
1061
1062 if( argc!=3 ){
1063 return Th_WrongNumArgs(interp, "string trim string");
1064 }
1065 z = argv[2];
1066 n = TH1_LEN(argl[2]);
1067 if( TH1_LEN(argl[1])<5 || argv[1][4]=='l' ){
1068 while( n && th_isspace(z[0]) ){ z++; n--; }
1069 }
1070 if( TH1_LEN(argl[1])<5 || argv[1][4]=='r' ){
1071 while( n && th_isspace(z[n-1]) ){ n--; }
1072 }
1073 TH1_XFER_TAINT(n, argl[2]);
1074 Th_SetResult(interp, z, n);
1075 return TH_OK;
1076 }
1077
1078 /*
@@ -1051,11 +1086,11 @@
1086 int rc;
1087
1088 if( argc!=3 ){
1089 return Th_WrongNumArgs(interp, "info exists var");
1090 }
1091 rc = Th_ExistsVar(interp, argv[2], TH1_LEN(argl[2]));
1092 Th_SetResultInt(interp, rc);
1093 return TH_OK;
1094 }
1095
1096 /*
@@ -1117,11 +1152,11 @@
1152 int rc;
1153
1154 if( argc!=3 ){
1155 return Th_WrongNumArgs(interp, "array exists var");
1156 }
1157 rc = Th_ExistsArrayVar(interp, argv[2], TH1_LEN(argl[2]));
1158 Th_SetResultInt(interp, rc);
1159 return TH_OK;
1160 }
1161
1162 /*
@@ -1137,11 +1172,11 @@
1172 int nElem = 0;
1173
1174 if( argc!=3 ){
1175 return Th_WrongNumArgs(interp, "array names varname");
1176 }
1177 rc = Th_ListAppendArray(interp, argv[2], TH1_LEN(argl[2]), &zElem, &nElem);
1178 if( rc!=TH_OK ){
1179 return rc;
1180 }
1181 Th_SetResult(interp, zElem, nElem);
1182 if( zElem ) Th_Free(interp, zElem);
@@ -1161,11 +1196,11 @@
1196 int *argl
1197 ){
1198 if( argc!=2 ){
1199 return Th_WrongNumArgs(interp, "unset var");
1200 }
1201 return Th_UnsetVar(interp, argv[1], TH1_LEN(argl[1]));
1202 }
1203
1204 int Th_CallSubCommand(
1205 Th_Interp *interp,
1206 void *ctx,
@@ -1176,19 +1211,22 @@
1211 ){
1212 if( argc>1 ){
1213 int i;
1214 for(i=0; aSub[i].zName; i++){
1215 const char *zName = aSub[i].zName;
1216 if( th_strlen(zName)==TH1_LEN(argl[1])
1217 && 0==memcmp(zName, argv[1], TH1_LEN(argl[1])) ){
1218 return aSub[i].xProc(interp, ctx, argc, argv, argl);
1219 }
1220 }
1221 }
1222 if(argc<2){
1223 Th_ErrorMessage(interp, "Expected sub-command for",
1224 argv[0], TH1_LEN(argl[0]));
1225 }else{
1226 Th_ErrorMessage(interp, "Expected sub-command, got:",
1227 argv[1], TH1_LEN(argl[1]));
1228 }
1229 return TH_ERROR;
1230 }
1231
1232 /*
@@ -1319,11 +1357,11 @@
1357 int iFrame = -1;
1358
1359 if( argc!=2 && argc!=3 ){
1360 return Th_WrongNumArgs(interp, "uplevel ?level? script...");
1361 }
1362 if( argc==3 && TH_OK!=thToFrame(interp, argv[1], TH1_LEN(argl[1]), &iFrame) ){
1363 return TH_ERROR;
1364 }
1365 return Th_Eval(interp, iFrame, argv[argc-1], -1);
1366 }
1367
@@ -1342,19 +1380,20 @@
1380 int iVar = 1;
1381 int iFrame = -1;
1382 int rc = TH_OK;
1383 int i;
1384
1385 if( TH_OK==thToFrame(0, argv[1], TH1_LEN(argl[1]), &iFrame) ){
1386 iVar++;
1387 }
1388 if( argc==iVar || (argc-iVar)%2 ){
1389 return Th_WrongNumArgs(interp,
1390 "upvar frame othervar myvar ?othervar myvar...?");
1391 }
1392 for(i=iVar; rc==TH_OK && i<argc; i=i+2){
1393 rc = Th_LinkVar(interp, argv[i+1], TH1_LEN(argl[i+1]),
1394 iFrame, argv[i], TH1_LEN(argl[i]));
1395 }
1396 return rc;
1397 }
1398
1399 /*
1400
+207 -39
--- src/th_main.c
+++ src/th_main.c
@@ -262,11 +262,11 @@
262262
){
263263
char *zOut;
264264
if( argc!=2 ){
265265
return Th_WrongNumArgs(interp, "httpize STRING");
266266
}
267
- zOut = httpize((char*)argv[1], argl[1]);
267
+ zOut = httpize((char*)argv[1], TH1_LEN(argl[1]));
268268
Th_SetResult(interp, zOut, -1);
269269
free(zOut);
270270
return TH_OK;
271271
}
272272
@@ -291,11 +291,12 @@
291291
if( argc<2 || argc>3 ){
292292
return Th_WrongNumArgs(interp, "enable_output [LABEL] BOOLEAN");
293293
}
294294
rc = Th_ToInt(interp, argv[argc-1], argl[argc-1], &enableOutput);
295295
if( g.thTrace ){
296
- Th_Trace("enable_output {%.*s} -> %d<br>\n", argl[1],argv[1],enableOutput);
296
+ Th_Trace("enable_output {%.*s} -> %d<br>\n",
297
+ TH1_LEN(argl[1]),argv[1],enableOutput);
297298
}
298299
return rc;
299300
}
300301
301302
/*
@@ -322,11 +323,11 @@
322323
buul = (TH_INIT_NO_ENCODE & g.th1Flags) ? 0 : 1;
323324
Th_SetResultInt(g.interp, buul);
324325
if(argc>1){
325326
if( g.thTrace ){
326327
Th_Trace("enable_htmlify {%.*s} -> %d<br>\n",
327
- argl[1],argv[1],buul);
328
+ TH1_LEN(argl[1]),argv[1],buul);
328329
}
329330
rc = Th_ToInt(interp, argv[argc-1], argl[argc-1], &buul);
330331
if(!rc){
331332
if(buul){
332333
g.th1Flags &= ~TH_INIT_NO_ENCODE;
@@ -381,19 +382,23 @@
381382
** g.th1Flags has the TH_INIT_NO_ENCODE flag.
382383
**
383384
** If pOut is NULL and the global pThOut is not then that blob
384385
** is used for output.
385386
*/
386
-static void sendText(Blob * pOut, const char *z, int n, int encode){
387
+static void sendText(Blob *pOut, const char *z, int n, int encode){
387388
if(0==pOut && pThOut!=0){
388389
pOut = pThOut;
389390
}
390391
if(TH_INIT_NO_ENCODE & g.th1Flags){
391392
encode = 0;
392393
}
393394
if( enableOutput && n ){
394
- if( n<0 ) n = strlen(z);
395
+ if( n<0 ){
396
+ n = strlen(z);
397
+ }else{
398
+ n = TH1_LEN(n);
399
+ }
395400
if( encode ){
396401
z = htmlize(z, n);
397402
n = strlen(z);
398403
}
399404
if(pOut!=0){
@@ -525,14 +530,22 @@
525530
void *pConvert,
526531
int argc,
527532
const char **argv,
528533
int *argl
529534
){
535
+ int encode = *(unsigned int*)pConvert;
536
+ int n;
530537
if( argc!=2 ){
531538
return Th_WrongNumArgs(interp, "puts STRING");
532539
}
533
- sendText(0,(char*)argv[1], argl[1], *(unsigned int*)pConvert);
540
+ n = argl[1];
541
+ if( encode==0 && n>0 && TH1_TAINTED(n) ){
542
+ if( Th_ReportTaint(interp, "output string", argv[1], n) ){
543
+ return TH_ERROR;
544
+ }
545
+ }
546
+ sendText(0,(char*)argv[1], TH1_LEN(n), encode);
534547
return TH_OK;
535548
}
536549
537550
/*
538551
** TH1 command: redirect URL ?withMethod?
@@ -557,10 +570,15 @@
557570
}
558571
if( argc==3 ){
559572
if( Th_ToInt(interp, argv[2], argl[2], &withMethod) ){
560573
return TH_ERROR;
561574
}
575
+ }
576
+ if( TH1_TAINTED(argl[1])
577
+ && Th_ReportTaint(interp,"redirect URL",argv[1],argl[1])
578
+ ){
579
+ return TH_ERROR;
562580
}
563581
if( withMethod ){
564582
cgi_redirect_with_method(argv[1]);
565583
}else{
566584
cgi_redirect(argv[1]);
@@ -660,11 +678,11 @@
660678
int nValue = 0;
661679
if( argc!=2 ){
662680
return Th_WrongNumArgs(interp, "markdown STRING");
663681
}
664682
blob_zero(&src);
665
- blob_init(&src, (char*)argv[1], argl[1]);
683
+ blob_init(&src, (char*)argv[1], TH1_LEN(argl[1]));
666684
blob_zero(&title); blob_zero(&body);
667685
markdown_to_html(&src, &title, &body);
668686
Th_ListAppend(interp, &zValue, &nValue, blob_str(&title), blob_size(&title));
669687
Th_ListAppend(interp, &zValue, &nValue, blob_str(&body), blob_size(&body));
670688
Th_SetResult(interp, zValue, nValue);
@@ -690,11 +708,11 @@
690708
if( argc!=2 ){
691709
return Th_WrongNumArgs(interp, "wiki STRING");
692710
}
693711
if( enableOutput ){
694712
Blob src;
695
- blob_init(&src, (char*)argv[1], argl[1]);
713
+ blob_init(&src, (char*)argv[1], TH1_LEN(argl[1]));
696714
wiki_convert(&src, 0, flags);
697715
blob_reset(&src);
698716
}
699717
return TH_OK;
700718
}
@@ -735,11 +753,11 @@
735753
){
736754
char *zOut;
737755
if( argc!=2 ){
738756
return Th_WrongNumArgs(interp, "htmlize STRING");
739757
}
740
- zOut = htmlize((char*)argv[1], argl[1]);
758
+ zOut = htmlize((char*)argv[1], TH1_LEN(argl[1]));
741759
Th_SetResult(interp, zOut, -1);
742760
free(zOut);
743761
return TH_OK;
744762
}
745763
@@ -757,11 +775,11 @@
757775
){
758776
char *zOut;
759777
if( argc!=2 ){
760778
return Th_WrongNumArgs(interp, "encode64 STRING");
761779
}
762
- zOut = encode64((char*)argv[1], argl[1]);
780
+ zOut = encode64((char*)argv[1], TH1_LEN(argl[1]));
763781
Th_SetResult(interp, zOut, -1);
764782
free(zOut);
765783
return TH_OK;
766784
}
767785
@@ -778,11 +796,11 @@
778796
int argc,
779797
const char **argv,
780798
int *argl
781799
){
782800
char *zOut;
783
- if( argc>=2 && argl[1]==6 && memcmp(argv[1],"-local",6)==0 ){
801
+ if( argc>=2 && TH1_LEN(argl[1])==6 && memcmp(argv[1],"-local",6)==0 ){
784802
zOut = db_text("??", "SELECT datetime('now',toLocal())");
785803
}else{
786804
zOut = db_text("??", "SELECT datetime('now')");
787805
}
788806
Th_SetResult(interp, zOut, -1);
@@ -810,13 +828,13 @@
810828
if( argc<2 ){
811829
return Th_WrongNumArgs(interp, "hascap STRING ...");
812830
}
813831
for(i=1; rc==1 && i<argc; i++){
814832
if( g.thTrace ){
815
- Th_ListAppend(interp, &zCapList, &nCapList, argv[i], argl[i]);
833
+ Th_ListAppend(interp, &zCapList, &nCapList, argv[i], TH1_LEN(argl[i]));
816834
}
817
- rc = login_has_capability((char*)argv[i],argl[i],*(int*)p);
835
+ rc = login_has_capability((char*)argv[i],TH1_LEN(argl[i]),*(int*)p);
818836
}
819837
if( g.thTrace ){
820838
Th_Trace("[%s %#h] => %d<br>\n", argv[0], nCapList, zCapList, rc);
821839
Th_Free(interp, zCapList);
822840
}
@@ -858,11 +876,11 @@
858876
int i;
859877
860878
if( argc!=2 ){
861879
return Th_WrongNumArgs(interp, "capexpr EXPR");
862880
}
863
- rc = Th_SplitList(interp, argv[1], argl[1], &azCap, &anCap, &nCap);
881
+ rc = Th_SplitList(interp, argv[1], TH1_LEN(argl[1]), &azCap, &anCap, &nCap);
864882
if( rc ) return rc;
865883
rc = 0;
866884
for(i=0; i<nCap; i++){
867885
if( azCap[i][0]=='!' ){
868886
rc = !login_has_capability(azCap[i]+1, anCap[i]-1, 0);
@@ -921,11 +939,12 @@
921939
if( argc<2 ){
922940
return Th_WrongNumArgs(interp, "hascap STRING ...");
923941
}
924942
for(i=1; i<argc && rc; i++){
925943
int match = 0;
926
- for(j=0; j<argl[i]; j++){
944
+ int nn = TH1_LEN(argl[i]);
945
+ for(j=0; j<nn; j++){
927946
switch( argv[i][j] ){
928947
case 'c': match |= searchCap & SRCH_CKIN; break;
929948
case 'd': match |= searchCap & SRCH_DOC; break;
930949
case 't': match |= searchCap & SRCH_TKT; break;
931950
case 'w': match |= searchCap & SRCH_WIKI; break;
@@ -932,11 +951,11 @@
932951
}
933952
}
934953
if( !match ) rc = 0;
935954
}
936955
if( g.thTrace ){
937
- Th_Trace("[searchable %#h] => %d<br>\n", argl[1], argv[1], rc);
956
+ Th_Trace("[searchable %#h] => %d<br>\n", TH1_LEN(argl[1]), argv[1], rc);
938957
}
939958
Th_SetResultInt(interp, rc);
940959
return TH_OK;
941960
}
942961
@@ -1051,11 +1070,11 @@
10511070
#endif
10521071
else if( 0 == fossil_strnicmp( zArg, "markdown\0", 9 ) ){
10531072
rc = 1;
10541073
}
10551074
if( g.thTrace ){
1056
- Th_Trace("[hasfeature %#h] => %d<br>\n", argl[1], zArg, rc);
1075
+ Th_Trace("[hasfeature %#h] => %d<br>\n", TH1_LEN(argl[1]), zArg, rc);
10571076
}
10581077
Th_SetResultInt(interp, rc);
10591078
return TH_OK;
10601079
}
10611080
@@ -1104,18 +1123,20 @@
11041123
const char **argv,
11051124
int *argl
11061125
){
11071126
int rc = 0;
11081127
int i;
1128
+ int nn;
11091129
if( argc!=2 ){
11101130
return Th_WrongNumArgs(interp, "anycap STRING");
11111131
}
1112
- for(i=0; rc==0 && i<argl[1]; i++){
1132
+ nn = TH1_LEN(argl[1]);
1133
+ for(i=0; rc==0 && i<nn; i++){
11131134
rc = login_has_capability((char*)&argv[1][i],1,0);
11141135
}
11151136
if( g.thTrace ){
1116
- Th_Trace("[anycap %#h] => %d<br>\n", argl[1], argv[1], rc);
1137
+ Th_Trace("[anycap %#h] => %d<br>\n", TH1_LEN(argl[1]), argv[1], rc);
11171138
}
11181139
Th_SetResultInt(interp, rc);
11191140
return TH_OK;
11201141
}
11211142
@@ -1140,22 +1161,23 @@
11401161
return Th_WrongNumArgs(interp, "combobox NAME TEXT-LIST NUMLINES");
11411162
}
11421163
if( enableOutput ){
11431164
int height;
11441165
Blob name;
1145
- int nValue;
1166
+ int nValue = 0;
11461167
const char *zValue;
11471168
char *z, *zH;
11481169
int nElem;
11491170
int *aszElem;
11501171
char **azElem;
11511172
int i;
11521173
11531174
if( Th_ToInt(interp, argv[3], argl[3], &height) ) return TH_ERROR;
1154
- Th_SplitList(interp, argv[2], argl[2], &azElem, &aszElem, &nElem);
1155
- blob_init(&name, (char*)argv[1], argl[1]);
1175
+ Th_SplitList(interp, argv[2], TH1_LEN(argl[2]), &azElem, &aszElem, &nElem);
1176
+ blob_init(&name, (char*)argv[1], TH1_LEN(argl[1]));
11561177
zValue = Th_Fetch(blob_str(&name), &nValue);
1178
+ nValue = TH1_LEN(nValue);
11571179
zH = htmlize(blob_buffer(&name), blob_size(&name));
11581180
z = mprintf("<select id=\"%s\" name=\"%s\" size=\"%d\">", zH, zH, height);
11591181
free(zH);
11601182
sendText(0,z, -1, 0);
11611183
free(z);
@@ -1247,11 +1269,11 @@
12471269
return Th_WrongNumArgs(interp, "linecount STRING MAX MIN");
12481270
}
12491271
if( Th_ToInt(interp, argv[2], argl[2], &iMax) ) return TH_ERROR;
12501272
if( Th_ToInt(interp, argv[3], argl[3], &iMin) ) return TH_ERROR;
12511273
z = argv[1];
1252
- size = argl[1];
1274
+ size = TH1_LEN(argl[1]);
12531275
for(n=1, i=0; i<size; i++){
12541276
if( z[i]=='\n' ){
12551277
n++;
12561278
if( n>=iMax ) break;
12571279
}
@@ -1407,11 +1429,12 @@
14071429
return TH_OK;
14081430
}else if( fossil_strnicmp(argv[1], "vfs\0", 4)==0 ){
14091431
Th_SetResult(interp, g.zVfsName ? g.zVfsName : zDefault, -1);
14101432
return TH_OK;
14111433
}else{
1412
- Th_ErrorMessage(interp, "unsupported global state:", argv[1], argl[1]);
1434
+ Th_ErrorMessage(interp, "unsupported global state:",
1435
+ argv[1], TH1_LEN(argl[1]));
14131436
return TH_ERROR;
14141437
}
14151438
}
14161439
14171440
/*
@@ -1426,17 +1449,21 @@
14261449
int argc,
14271450
const char **argv,
14281451
int *argl
14291452
){
14301453
const char *zDefault = 0;
1454
+ const char *zVal;
1455
+ int sz;
14311456
if( argc!=2 && argc!=3 ){
14321457
return Th_WrongNumArgs(interp, "getParameter NAME ?DEFAULT?");
14331458
}
14341459
if( argc==3 ){
14351460
zDefault = argv[2];
14361461
}
1437
- Th_SetResult(interp, cgi_parameter(argv[1], zDefault), -1);
1462
+ zVal = cgi_parameter(argv[1], zDefault);
1463
+ sz = th_strlen(zVal);
1464
+ Th_SetResult(interp, zVal, TH1_ADD_TAINT(sz));
14381465
return TH_OK;
14391466
}
14401467
14411468
/*
14421469
** TH1 command: setParameter NAME VALUE
@@ -1848,10 +1875,47 @@
18481875
sqlite3_snprintf(sizeof(zUTime), zUTime, "%llu", x);
18491876
Th_SetResult(interp, zUTime, -1);
18501877
return TH_OK;
18511878
}
18521879
1880
+/*
1881
+** TH1 command: taint STRING
1882
+**
1883
+** Return a copy of STRING that is marked as tainted.
1884
+*/
1885
+static int taintCmd(
1886
+ Th_Interp *interp,
1887
+ void *p,
1888
+ int argc,
1889
+ const char **argv,
1890
+ int *argl
1891
+){
1892
+ if( argc!=2 ){
1893
+ return Th_WrongNumArgs(interp, "STRING");
1894
+ }
1895
+ Th_SetResult(interp, argv[1], TH1_ADD_TAINT(argl[1]));
1896
+ return TH_OK;
1897
+}
1898
+
1899
+/*
1900
+** TH1 command: untaint STRING
1901
+**
1902
+** Return a copy of STRING that is marked as untainted.
1903
+*/
1904
+static int untaintCmd(
1905
+ Th_Interp *interp,
1906
+ void *p,
1907
+ int argc,
1908
+ const char **argv,
1909
+ int *argl
1910
+){
1911
+ if( argc!=2 ){
1912
+ return Th_WrongNumArgs(interp, "STRING");
1913
+ }
1914
+ Th_SetResult(interp, argv[1], TH1_LEN(argl[1]));
1915
+ return TH_OK;
1916
+}
18531917
18541918
/*
18551919
** TH1 command: randhex N
18561920
**
18571921
** Return N*2 random hexadecimal digits with N<50. If N is omitted,
@@ -1923,11 +1987,13 @@
19231987
int res = TH_OK;
19241988
int nVar;
19251989
char *zErr = 0;
19261990
int noComplain = 0;
19271991
1928
- if( argc>3 && argl[1]==11 && strncmp(argv[1], "-nocomplain", 11)==0 ){
1992
+ if( argc>3 && TH1_LEN(argl[1])==11
1993
+ && strncmp(argv[1], "-nocomplain", 11)==0
1994
+ ){
19291995
argc--;
19301996
argv++;
19311997
argl++;
19321998
noComplain = 1;
19331999
}
@@ -1939,15 +2005,22 @@
19392005
Th_ErrorMessage(interp, "database is not open", 0, 0);
19402006
return TH_ERROR;
19412007
}
19422008
zSql = argv[1];
19432009
nSql = argl[1];
2010
+ if( TH1_TAINTED(nSql) ){
2011
+ if( Th_ReportTaint(interp,"query SQL",zSql,nSql) ){
2012
+ return TH_ERROR;
2013
+ }
2014
+ nSql = TH1_LEN(nSql);
2015
+ }
2016
+
19442017
while( res==TH_OK && nSql>0 ){
19452018
zErr = 0;
19462019
report_restrict_sql(&zErr);
19472020
g.dbIgnoreErrors++;
1948
- rc = sqlite3_prepare_v2(g.db, argv[1], argl[1], &pStmt, &zTail);
2021
+ rc = sqlite3_prepare_v2(g.db, argv[1], TH1_LEN(argl[1]), &pStmt, &zTail);
19492022
g.dbIgnoreErrors--;
19502023
report_unrestrict_sql();
19512024
if( rc!=0 || zErr!=0 ){
19522025
if( noComplain ) return TH_OK;
19532026
Th_ErrorMessage(interp, "SQL error: ",
@@ -1964,31 +2037,31 @@
19642037
int szVar = zVar ? th_strlen(zVar) : 0;
19652038
if( szVar>1 && zVar[0]=='$'
19662039
&& Th_GetVar(interp, zVar+1, szVar-1)==TH_OK ){
19672040
int nVal;
19682041
const char *zVal = Th_GetResult(interp, &nVal);
1969
- sqlite3_bind_text(pStmt, i, zVal, nVal, SQLITE_TRANSIENT);
2042
+ sqlite3_bind_text(pStmt, i, zVal, TH1_LEN(nVal), SQLITE_TRANSIENT);
19702043
}
19712044
}
19722045
while( res==TH_OK && ignore_errors_step(pStmt)==SQLITE_ROW ){
19732046
int nCol = sqlite3_column_count(pStmt);
19742047
for(i=0; i<nCol; i++){
19752048
const char *zCol = sqlite3_column_name(pStmt, i);
19762049
int szCol = th_strlen(zCol);
19772050
const char *zVal = (const char*)sqlite3_column_text(pStmt, i);
19782051
int szVal = sqlite3_column_bytes(pStmt, i);
1979
- Th_SetVar(interp, zCol, szCol, zVal, szVal);
2052
+ Th_SetVar(interp, zCol, szCol, zVal, TH1_ADD_TAINT(szVal));
19802053
}
19812054
if( g.thTrace ){
1982
- Th_Trace("query_eval {<pre>%#h</pre>}<br>\n", argl[2], argv[2]);
2055
+ Th_Trace("query_eval {<pre>%#h</pre>}<br>\n",TH1_LEN(argl[2]),argv[2]);
19832056
}
1984
- res = Th_Eval(interp, 0, argv[2], argl[2]);
2057
+ res = Th_Eval(interp, 0, argv[2], TH1_LEN(argl[2]));
19852058
if( g.thTrace ){
19862059
int nTrRes;
19872060
char *zTrRes = (char*)Th_GetResult(g.interp, &nTrRes);
19882061
Th_Trace("[query_eval] => %h {%#h}<br>\n",
1989
- Th_ReturnCodeName(res, 0), nTrRes, zTrRes);
2062
+ Th_ReturnCodeName(res, 0), TH1_LEN(nTrRes), zTrRes);
19902063
}
19912064
if( res==TH_BREAK || res==TH_CONTINUE ) res = TH_OK;
19922065
}
19932066
rc = sqlite3_finalize(pStmt);
19942067
if( rc!=SQLITE_OK ){
@@ -2038,11 +2111,11 @@
20382111
Th_SetResult(interp, 0, 0);
20392112
rc = TH_OK;
20402113
}
20412114
if( g.thTrace ){
20422115
Th_Trace("[setting %s%#h] => %d<br>\n", strict ? "strict " : "",
2043
- argl[nArg], argv[nArg], rc);
2116
+ TH1_LEN(argl[nArg]), argv[nArg], rc);
20442117
}
20452118
return rc;
20462119
}
20472120
20482121
/*
@@ -2121,11 +2194,11 @@
21212194
return Th_WrongNumArgs(interp, REGEXP_WRONGNUMARGS);
21222195
}
21232196
zErr = re_compile(&pRe, argv[nArg], noCase);
21242197
if( !zErr ){
21252198
Th_SetResultInt(interp, re_match(pRe,
2126
- (const unsigned char *)argv[nArg+1], argl[nArg+1]));
2199
+ (const unsigned char *)argv[nArg+1], TH1_LEN(argl[nArg+1])));
21272200
rc = TH_OK;
21282201
}else{
21292202
Th_SetResult(interp, zErr, -1);
21302203
rc = TH_ERROR;
21312204
}
@@ -2160,11 +2233,11 @@
21602233
UrlData urlData;
21612234
21622235
if( argc<2 || argc>5 ){
21632236
return Th_WrongNumArgs(interp, HTTP_WRONGNUMARGS);
21642237
}
2165
- if( fossil_strnicmp(argv[nArg], "-asynchronous", argl[nArg])==0 ){
2238
+ if( fossil_strnicmp(argv[nArg], "-asynchronous", TH1_LEN(argl[nArg]))==0 ){
21662239
fAsynchronous = 1; nArg++;
21672240
}
21682241
if( fossil_strcmp(argv[nArg], "--")==0 ) nArg++;
21692242
if( nArg+1!=argc && nArg+2!=argc ){
21702243
return Th_WrongNumArgs(interp, REGEXP_WRONGNUMARGS);
@@ -2189,11 +2262,11 @@
21892262
return TH_ERROR;
21902263
}
21912264
re_free(pRe);
21922265
blob_zero(&payload);
21932266
if( nArg+2==argc ){
2194
- blob_append(&payload, argv[nArg+1], argl[nArg+1]);
2267
+ blob_append(&payload, argv[nArg+1], TH1_LEN(argl[nArg+1]));
21952268
zType = "POST";
21962269
}else{
21972270
zType = "GET";
21982271
}
21992272
if( fAsynchronous ){
@@ -2268,11 +2341,11 @@
22682341
if( argc!=2 ){
22692342
return Th_WrongNumArgs(interp, "captureTh1 STRING");
22702343
}
22712344
pOrig = Th_SetOutputBlob(&out);
22722345
zStr = argv[1];
2273
- nStr = argl[1];
2346
+ nStr = TH1_LEN(argl[1]);
22742347
rc = Th_Eval(g.interp, 0, zStr, nStr);
22752348
Th_SetOutputBlob(pOrig);
22762349
if(0==rc){
22772350
Th_SetResult(g.interp, blob_str(&out), blob_size(&out));
22782351
}
@@ -2387,13 +2460,15 @@
23872460
{"setting", settingCmd, 0},
23882461
{"styleFooter", styleFooterCmd, 0},
23892462
{"styleHeader", styleHeaderCmd, 0},
23902463
{"styleScript", styleScriptCmd, 0},
23912464
{"submenu", submenuCmd, 0},
2465
+ {"taint", taintCmd, 0},
23922466
{"tclReady", tclReadyCmd, 0},
23932467
{"trace", traceCmd, 0},
23942468
{"stime", stimeCmd, 0},
2469
+ {"untaint", untaintCmd, 0},
23952470
{"unversioned", unversionedCmd, 0},
23962471
{"utime", utimeCmd, 0},
23972472
{"verifyCsrf", verifyCsrfCmd, 0},
23982473
{"verifyLogin", verifyLoginCmd, 0},
23992474
{"wiki", wikiCmd, (void*)&aFlags[0]},
@@ -2494,10 +2569,26 @@
24942569
Th_Trace("set %h {%h}<br>\n", zName, zValue);
24952570
}
24962571
Th_SetVar(g.interp, zName, -1, zValue, strlen(zValue));
24972572
}
24982573
}
2574
+
2575
+/*
2576
+** Store a string value in a variable in the interpreter
2577
+** with the "taint" marking, so that TH1 knows that this
2578
+** variable contains content under the control of the remote
2579
+** user and presents a risk of XSS or SQL-injection attacks.
2580
+*/
2581
+void Th_StoreUnsafe(const char *zName, const char *zValue){
2582
+ Th_FossilInit(TH_INIT_DEFAULT);
2583
+ if( zValue ){
2584
+ if( g.thTrace ){
2585
+ Th_Trace("set %h [taint {%h}]<br>\n", zName, zValue);
2586
+ }
2587
+ Th_SetVar(g.interp, zName, -1, zValue, TH1_ADD_TAINT(strlen(zValue)));
2588
+ }
2589
+}
24992590
25002591
/*
25012592
** Appends an element to a TH1 list value. This function is called by the
25022593
** transfer subsystem; therefore, it must be very careful to avoid doing
25032594
** any unnecessary work. To that end, the TH1 subsystem will not be called
@@ -2680,10 +2771,11 @@
26802771
char *zResult = (char*)Th_GetResult(g.interp, &nResult);
26812772
/*
26822773
** Make sure that the TH1 script error was not caused by a "missing"
26832774
** command hook handler as that is not actually an error condition.
26842775
*/
2776
+ nResult = TH1_LEN(nResult);
26852777
if( memcmp(zResult, NO_COMMAND_HOOK_ERROR, nResult)!=0 ){
26862778
sendError(0,zResult, nResult, 0);
26872779
}else{
26882780
/*
26892781
** There is no command hook handler "installed". This situation
@@ -2767,10 +2859,11 @@
27672859
char *zResult = (char*)Th_GetResult(g.interp, &nResult);
27682860
/*
27692861
** Make sure that the TH1 script error was not caused by a "missing"
27702862
** webpage hook handler as that is not actually an error condition.
27712863
*/
2864
+ nResult = TH1_LEN(nResult);
27722865
if( memcmp(zResult, NO_WEBPAGE_HOOK_ERROR, nResult)!=0 ){
27732866
sendError(0,zResult, nResult, 1);
27742867
}else{
27752868
/*
27762869
** There is no webpage hook handler "installed". This situation
@@ -2894,11 +2987,16 @@
28942987
}
28952988
rc = Th_GetVar(g.interp, (char*)zVar, nVar);
28962989
z += i+1+n;
28972990
i = 0;
28982991
zResult = (char*)Th_GetResult(g.interp, &n);
2899
- sendText(pOut,(char*)zResult, n, encode);
2992
+ if( !TH1_TAINTED(n)
2993
+ || encode
2994
+ || Th_ReportTaint(g.interp, "inline variable", zVar, nVar)==TH_OK
2995
+ ){
2996
+ sendText(pOut,(char*)zResult, n, encode);
2997
+ }
29002998
}else if( z[i]=='<' && isBeginScriptTag(&z[i]) ){
29012999
sendText(pOut,z, i, 0);
29023000
z += i+5;
29033001
for(i=0; z[i] && (z[i]!='<' || !isEndScriptTag(&z[i])); i++){}
29043002
if( g.thTrace ){
@@ -2907,11 +3005,11 @@
29073005
rc = Th_Eval(g.interp, 0, (const char*)z, i);
29083006
if( g.thTrace ){
29093007
int nTrRes;
29103008
char *zTrRes = (char*)Th_GetResult(g.interp, &nTrRes);
29113009
Th_Trace("[render_eval] => %h {%#h}<br>\n",
2912
- Th_ReturnCodeName(rc, 0), nTrRes, zTrRes);
3010
+ Th_ReturnCodeName(rc, 0), TH1_LEN(nTrRes), zTrRes);
29133011
}
29143012
if( rc!=TH_OK ) break;
29153013
z += i;
29163014
if( z[0] ){ z += 6; }
29173015
i = 0;
@@ -2953,10 +3051,78 @@
29533051
** as appropriate. We need to pass on g.th1Flags for the case of
29543052
** recursive calls, so that, e.g., TH_INIT_NO_ENCODE does not get
29553053
** inadvertently toggled off by a recursive call.
29563054
*/;
29573055
}
3056
+
3057
+/*
3058
+** SETTING: vuln-report width=8 default=log
3059
+**
3060
+** This setting controls Fossil's behavior when it encounters a potential
3061
+** XSS or SQL-injection vulnerability due to misuse of TH1 configuration
3062
+** scripts. Choices are:
3063
+**
3064
+** off Do nothing. Ignore the vulnerability.
3065
+**
3066
+** log Write a report of the problem into the error log.
3067
+**
3068
+** block Like "log" but also prevent the offending TH1 command
3069
+** from running.
3070
+**
3071
+** fatal Render an error message page instead of the requested
3072
+** page.
3073
+*/
3074
+
3075
+/*
3076
+** Report misuse of a tainted string in TH1.
3077
+**
3078
+** The behavior depends on the vuln-report setting. If "off", this routine
3079
+** is a no-op. Otherwise, right a message into the error log. If
3080
+** vuln-report is "log", that is all that happens. But for any other
3081
+** value of vuln-report, a fatal error is raised.
3082
+*/
3083
+int Th_ReportTaint(
3084
+ Th_Interp *interp, /* Report error here, if an error is reported */
3085
+ const char *zWhere, /* Where the tainted string appears */
3086
+ const char *zStr, /* The tainted string */
3087
+ int nStr /* Length of the tainted string */
3088
+){
3089
+ static const char *zDisp = 0; /* Dispensation; what to do with the error */
3090
+ const char *zVulnType; /* Type of vulnerability */
3091
+
3092
+ if( zDisp==0 ) zDisp = db_get("vuln-report","log");
3093
+ if( is_false(zDisp) ) return 0;
3094
+ if( strstr(zWhere,"SQL")!=0 ){
3095
+ zVulnType = "SQL-injection";
3096
+ }else{
3097
+ zVulnType = "XSS";
3098
+ }
3099
+ nStr = TH1_LEN(nStr);
3100
+ fossil_errorlog("possible TH1 %s vulnerability due to tainted %s: \"%.*s\"",
3101
+ zVulnType, zWhere, nStr, zStr);
3102
+ if( strcmp(zDisp,"log")==0 ){
3103
+ return 0;
3104
+ }
3105
+ if( strcmp(zDisp,"block")==0 ){
3106
+ char *z = mprintf("tainted %s: \"", zWhere);
3107
+ Th_ErrorMessage(interp, z, zStr, nStr);
3108
+ fossil_free(z);
3109
+ }else{
3110
+ char *z = mprintf("%#h", nStr, zStr);
3111
+ zDisp = "off";
3112
+ cgi_reset_content();
3113
+ style_submenu_enable(0);
3114
+ style_set_current_feature("error");
3115
+ style_header("Configuration Error");
3116
+ @ <p>Error in a TH1 configuration script:
3117
+ @ tainted %h(zWhere): "%z(z)"
3118
+ style_finish_page();
3119
+ cgi_reply();
3120
+ fossil_exit(1);
3121
+ }
3122
+ return 1;
3123
+}
29583124
29593125
/*
29603126
** COMMAND: test-th-render
29613127
**
29623128
** Usage: %fossil test-th-render FILE
@@ -2992,10 +3158,11 @@
29923158
if( find_option("set-user-caps", 0, 0)!=0 ){
29933159
const char *zCap = fossil_getenv("TH1_TEST_USER_CAPS");
29943160
login_set_capabilities(zCap ? zCap : "sx", 0);
29953161
g.useLocalauth = 1;
29963162
}
3163
+ db_find_and_open_repository(OPEN_OK_NOT_FOUND|OPEN_SUBSTITUTE,0);
29973164
verify_all_options();
29983165
if( g.argc<3 ){
29993166
usage("FILE");
30003167
}
30013168
blob_zero(&in);
@@ -3044,10 +3211,11 @@
30443211
if( find_option("set-user-caps", 0, 0)!=0 ){
30453212
const char *zCap = fossil_getenv("TH1_TEST_USER_CAPS");
30463213
login_set_capabilities(zCap ? zCap : "sx", 0);
30473214
g.useLocalauth = 1;
30483215
}
3216
+ db_find_and_open_repository(OPEN_OK_NOT_FOUND|OPEN_SUBSTITUTE,0);
30493217
verify_all_options();
30503218
if( g.argc!=3 ){
30513219
usage("script");
30523220
}
30533221
if(file_isfile(g.argv[2], ExtFILE)){
30543222
--- src/th_main.c
+++ src/th_main.c
@@ -262,11 +262,11 @@
262 ){
263 char *zOut;
264 if( argc!=2 ){
265 return Th_WrongNumArgs(interp, "httpize STRING");
266 }
267 zOut = httpize((char*)argv[1], argl[1]);
268 Th_SetResult(interp, zOut, -1);
269 free(zOut);
270 return TH_OK;
271 }
272
@@ -291,11 +291,12 @@
291 if( argc<2 || argc>3 ){
292 return Th_WrongNumArgs(interp, "enable_output [LABEL] BOOLEAN");
293 }
294 rc = Th_ToInt(interp, argv[argc-1], argl[argc-1], &enableOutput);
295 if( g.thTrace ){
296 Th_Trace("enable_output {%.*s} -> %d<br>\n", argl[1],argv[1],enableOutput);
 
297 }
298 return rc;
299 }
300
301 /*
@@ -322,11 +323,11 @@
322 buul = (TH_INIT_NO_ENCODE & g.th1Flags) ? 0 : 1;
323 Th_SetResultInt(g.interp, buul);
324 if(argc>1){
325 if( g.thTrace ){
326 Th_Trace("enable_htmlify {%.*s} -> %d<br>\n",
327 argl[1],argv[1],buul);
328 }
329 rc = Th_ToInt(interp, argv[argc-1], argl[argc-1], &buul);
330 if(!rc){
331 if(buul){
332 g.th1Flags &= ~TH_INIT_NO_ENCODE;
@@ -381,19 +382,23 @@
381 ** g.th1Flags has the TH_INIT_NO_ENCODE flag.
382 **
383 ** If pOut is NULL and the global pThOut is not then that blob
384 ** is used for output.
385 */
386 static void sendText(Blob * pOut, const char *z, int n, int encode){
387 if(0==pOut && pThOut!=0){
388 pOut = pThOut;
389 }
390 if(TH_INIT_NO_ENCODE & g.th1Flags){
391 encode = 0;
392 }
393 if( enableOutput && n ){
394 if( n<0 ) n = strlen(z);
 
 
 
 
395 if( encode ){
396 z = htmlize(z, n);
397 n = strlen(z);
398 }
399 if(pOut!=0){
@@ -525,14 +530,22 @@
525 void *pConvert,
526 int argc,
527 const char **argv,
528 int *argl
529 ){
 
 
530 if( argc!=2 ){
531 return Th_WrongNumArgs(interp, "puts STRING");
532 }
533 sendText(0,(char*)argv[1], argl[1], *(unsigned int*)pConvert);
 
 
 
 
 
 
534 return TH_OK;
535 }
536
537 /*
538 ** TH1 command: redirect URL ?withMethod?
@@ -557,10 +570,15 @@
557 }
558 if( argc==3 ){
559 if( Th_ToInt(interp, argv[2], argl[2], &withMethod) ){
560 return TH_ERROR;
561 }
 
 
 
 
 
562 }
563 if( withMethod ){
564 cgi_redirect_with_method(argv[1]);
565 }else{
566 cgi_redirect(argv[1]);
@@ -660,11 +678,11 @@
660 int nValue = 0;
661 if( argc!=2 ){
662 return Th_WrongNumArgs(interp, "markdown STRING");
663 }
664 blob_zero(&src);
665 blob_init(&src, (char*)argv[1], argl[1]);
666 blob_zero(&title); blob_zero(&body);
667 markdown_to_html(&src, &title, &body);
668 Th_ListAppend(interp, &zValue, &nValue, blob_str(&title), blob_size(&title));
669 Th_ListAppend(interp, &zValue, &nValue, blob_str(&body), blob_size(&body));
670 Th_SetResult(interp, zValue, nValue);
@@ -690,11 +708,11 @@
690 if( argc!=2 ){
691 return Th_WrongNumArgs(interp, "wiki STRING");
692 }
693 if( enableOutput ){
694 Blob src;
695 blob_init(&src, (char*)argv[1], argl[1]);
696 wiki_convert(&src, 0, flags);
697 blob_reset(&src);
698 }
699 return TH_OK;
700 }
@@ -735,11 +753,11 @@
735 ){
736 char *zOut;
737 if( argc!=2 ){
738 return Th_WrongNumArgs(interp, "htmlize STRING");
739 }
740 zOut = htmlize((char*)argv[1], argl[1]);
741 Th_SetResult(interp, zOut, -1);
742 free(zOut);
743 return TH_OK;
744 }
745
@@ -757,11 +775,11 @@
757 ){
758 char *zOut;
759 if( argc!=2 ){
760 return Th_WrongNumArgs(interp, "encode64 STRING");
761 }
762 zOut = encode64((char*)argv[1], argl[1]);
763 Th_SetResult(interp, zOut, -1);
764 free(zOut);
765 return TH_OK;
766 }
767
@@ -778,11 +796,11 @@
778 int argc,
779 const char **argv,
780 int *argl
781 ){
782 char *zOut;
783 if( argc>=2 && argl[1]==6 && memcmp(argv[1],"-local",6)==0 ){
784 zOut = db_text("??", "SELECT datetime('now',toLocal())");
785 }else{
786 zOut = db_text("??", "SELECT datetime('now')");
787 }
788 Th_SetResult(interp, zOut, -1);
@@ -810,13 +828,13 @@
810 if( argc<2 ){
811 return Th_WrongNumArgs(interp, "hascap STRING ...");
812 }
813 for(i=1; rc==1 && i<argc; i++){
814 if( g.thTrace ){
815 Th_ListAppend(interp, &zCapList, &nCapList, argv[i], argl[i]);
816 }
817 rc = login_has_capability((char*)argv[i],argl[i],*(int*)p);
818 }
819 if( g.thTrace ){
820 Th_Trace("[%s %#h] => %d<br>\n", argv[0], nCapList, zCapList, rc);
821 Th_Free(interp, zCapList);
822 }
@@ -858,11 +876,11 @@
858 int i;
859
860 if( argc!=2 ){
861 return Th_WrongNumArgs(interp, "capexpr EXPR");
862 }
863 rc = Th_SplitList(interp, argv[1], argl[1], &azCap, &anCap, &nCap);
864 if( rc ) return rc;
865 rc = 0;
866 for(i=0; i<nCap; i++){
867 if( azCap[i][0]=='!' ){
868 rc = !login_has_capability(azCap[i]+1, anCap[i]-1, 0);
@@ -921,11 +939,12 @@
921 if( argc<2 ){
922 return Th_WrongNumArgs(interp, "hascap STRING ...");
923 }
924 for(i=1; i<argc && rc; i++){
925 int match = 0;
926 for(j=0; j<argl[i]; j++){
 
927 switch( argv[i][j] ){
928 case 'c': match |= searchCap & SRCH_CKIN; break;
929 case 'd': match |= searchCap & SRCH_DOC; break;
930 case 't': match |= searchCap & SRCH_TKT; break;
931 case 'w': match |= searchCap & SRCH_WIKI; break;
@@ -932,11 +951,11 @@
932 }
933 }
934 if( !match ) rc = 0;
935 }
936 if( g.thTrace ){
937 Th_Trace("[searchable %#h] => %d<br>\n", argl[1], argv[1], rc);
938 }
939 Th_SetResultInt(interp, rc);
940 return TH_OK;
941 }
942
@@ -1051,11 +1070,11 @@
1051 #endif
1052 else if( 0 == fossil_strnicmp( zArg, "markdown\0", 9 ) ){
1053 rc = 1;
1054 }
1055 if( g.thTrace ){
1056 Th_Trace("[hasfeature %#h] => %d<br>\n", argl[1], zArg, rc);
1057 }
1058 Th_SetResultInt(interp, rc);
1059 return TH_OK;
1060 }
1061
@@ -1104,18 +1123,20 @@
1104 const char **argv,
1105 int *argl
1106 ){
1107 int rc = 0;
1108 int i;
 
1109 if( argc!=2 ){
1110 return Th_WrongNumArgs(interp, "anycap STRING");
1111 }
1112 for(i=0; rc==0 && i<argl[1]; i++){
 
1113 rc = login_has_capability((char*)&argv[1][i],1,0);
1114 }
1115 if( g.thTrace ){
1116 Th_Trace("[anycap %#h] => %d<br>\n", argl[1], argv[1], rc);
1117 }
1118 Th_SetResultInt(interp, rc);
1119 return TH_OK;
1120 }
1121
@@ -1140,22 +1161,23 @@
1140 return Th_WrongNumArgs(interp, "combobox NAME TEXT-LIST NUMLINES");
1141 }
1142 if( enableOutput ){
1143 int height;
1144 Blob name;
1145 int nValue;
1146 const char *zValue;
1147 char *z, *zH;
1148 int nElem;
1149 int *aszElem;
1150 char **azElem;
1151 int i;
1152
1153 if( Th_ToInt(interp, argv[3], argl[3], &height) ) return TH_ERROR;
1154 Th_SplitList(interp, argv[2], argl[2], &azElem, &aszElem, &nElem);
1155 blob_init(&name, (char*)argv[1], argl[1]);
1156 zValue = Th_Fetch(blob_str(&name), &nValue);
 
1157 zH = htmlize(blob_buffer(&name), blob_size(&name));
1158 z = mprintf("<select id=\"%s\" name=\"%s\" size=\"%d\">", zH, zH, height);
1159 free(zH);
1160 sendText(0,z, -1, 0);
1161 free(z);
@@ -1247,11 +1269,11 @@
1247 return Th_WrongNumArgs(interp, "linecount STRING MAX MIN");
1248 }
1249 if( Th_ToInt(interp, argv[2], argl[2], &iMax) ) return TH_ERROR;
1250 if( Th_ToInt(interp, argv[3], argl[3], &iMin) ) return TH_ERROR;
1251 z = argv[1];
1252 size = argl[1];
1253 for(n=1, i=0; i<size; i++){
1254 if( z[i]=='\n' ){
1255 n++;
1256 if( n>=iMax ) break;
1257 }
@@ -1407,11 +1429,12 @@
1407 return TH_OK;
1408 }else if( fossil_strnicmp(argv[1], "vfs\0", 4)==0 ){
1409 Th_SetResult(interp, g.zVfsName ? g.zVfsName : zDefault, -1);
1410 return TH_OK;
1411 }else{
1412 Th_ErrorMessage(interp, "unsupported global state:", argv[1], argl[1]);
 
1413 return TH_ERROR;
1414 }
1415 }
1416
1417 /*
@@ -1426,17 +1449,21 @@
1426 int argc,
1427 const char **argv,
1428 int *argl
1429 ){
1430 const char *zDefault = 0;
 
 
1431 if( argc!=2 && argc!=3 ){
1432 return Th_WrongNumArgs(interp, "getParameter NAME ?DEFAULT?");
1433 }
1434 if( argc==3 ){
1435 zDefault = argv[2];
1436 }
1437 Th_SetResult(interp, cgi_parameter(argv[1], zDefault), -1);
 
 
1438 return TH_OK;
1439 }
1440
1441 /*
1442 ** TH1 command: setParameter NAME VALUE
@@ -1848,10 +1875,47 @@
1848 sqlite3_snprintf(sizeof(zUTime), zUTime, "%llu", x);
1849 Th_SetResult(interp, zUTime, -1);
1850 return TH_OK;
1851 }
1852
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1853
1854 /*
1855 ** TH1 command: randhex N
1856 **
1857 ** Return N*2 random hexadecimal digits with N<50. If N is omitted,
@@ -1923,11 +1987,13 @@
1923 int res = TH_OK;
1924 int nVar;
1925 char *zErr = 0;
1926 int noComplain = 0;
1927
1928 if( argc>3 && argl[1]==11 && strncmp(argv[1], "-nocomplain", 11)==0 ){
 
 
1929 argc--;
1930 argv++;
1931 argl++;
1932 noComplain = 1;
1933 }
@@ -1939,15 +2005,22 @@
1939 Th_ErrorMessage(interp, "database is not open", 0, 0);
1940 return TH_ERROR;
1941 }
1942 zSql = argv[1];
1943 nSql = argl[1];
 
 
 
 
 
 
 
1944 while( res==TH_OK && nSql>0 ){
1945 zErr = 0;
1946 report_restrict_sql(&zErr);
1947 g.dbIgnoreErrors++;
1948 rc = sqlite3_prepare_v2(g.db, argv[1], argl[1], &pStmt, &zTail);
1949 g.dbIgnoreErrors--;
1950 report_unrestrict_sql();
1951 if( rc!=0 || zErr!=0 ){
1952 if( noComplain ) return TH_OK;
1953 Th_ErrorMessage(interp, "SQL error: ",
@@ -1964,31 +2037,31 @@
1964 int szVar = zVar ? th_strlen(zVar) : 0;
1965 if( szVar>1 && zVar[0]=='$'
1966 && Th_GetVar(interp, zVar+1, szVar-1)==TH_OK ){
1967 int nVal;
1968 const char *zVal = Th_GetResult(interp, &nVal);
1969 sqlite3_bind_text(pStmt, i, zVal, nVal, SQLITE_TRANSIENT);
1970 }
1971 }
1972 while( res==TH_OK && ignore_errors_step(pStmt)==SQLITE_ROW ){
1973 int nCol = sqlite3_column_count(pStmt);
1974 for(i=0; i<nCol; i++){
1975 const char *zCol = sqlite3_column_name(pStmt, i);
1976 int szCol = th_strlen(zCol);
1977 const char *zVal = (const char*)sqlite3_column_text(pStmt, i);
1978 int szVal = sqlite3_column_bytes(pStmt, i);
1979 Th_SetVar(interp, zCol, szCol, zVal, szVal);
1980 }
1981 if( g.thTrace ){
1982 Th_Trace("query_eval {<pre>%#h</pre>}<br>\n", argl[2], argv[2]);
1983 }
1984 res = Th_Eval(interp, 0, argv[2], argl[2]);
1985 if( g.thTrace ){
1986 int nTrRes;
1987 char *zTrRes = (char*)Th_GetResult(g.interp, &nTrRes);
1988 Th_Trace("[query_eval] => %h {%#h}<br>\n",
1989 Th_ReturnCodeName(res, 0), nTrRes, zTrRes);
1990 }
1991 if( res==TH_BREAK || res==TH_CONTINUE ) res = TH_OK;
1992 }
1993 rc = sqlite3_finalize(pStmt);
1994 if( rc!=SQLITE_OK ){
@@ -2038,11 +2111,11 @@
2038 Th_SetResult(interp, 0, 0);
2039 rc = TH_OK;
2040 }
2041 if( g.thTrace ){
2042 Th_Trace("[setting %s%#h] => %d<br>\n", strict ? "strict " : "",
2043 argl[nArg], argv[nArg], rc);
2044 }
2045 return rc;
2046 }
2047
2048 /*
@@ -2121,11 +2194,11 @@
2121 return Th_WrongNumArgs(interp, REGEXP_WRONGNUMARGS);
2122 }
2123 zErr = re_compile(&pRe, argv[nArg], noCase);
2124 if( !zErr ){
2125 Th_SetResultInt(interp, re_match(pRe,
2126 (const unsigned char *)argv[nArg+1], argl[nArg+1]));
2127 rc = TH_OK;
2128 }else{
2129 Th_SetResult(interp, zErr, -1);
2130 rc = TH_ERROR;
2131 }
@@ -2160,11 +2233,11 @@
2160 UrlData urlData;
2161
2162 if( argc<2 || argc>5 ){
2163 return Th_WrongNumArgs(interp, HTTP_WRONGNUMARGS);
2164 }
2165 if( fossil_strnicmp(argv[nArg], "-asynchronous", argl[nArg])==0 ){
2166 fAsynchronous = 1; nArg++;
2167 }
2168 if( fossil_strcmp(argv[nArg], "--")==0 ) nArg++;
2169 if( nArg+1!=argc && nArg+2!=argc ){
2170 return Th_WrongNumArgs(interp, REGEXP_WRONGNUMARGS);
@@ -2189,11 +2262,11 @@
2189 return TH_ERROR;
2190 }
2191 re_free(pRe);
2192 blob_zero(&payload);
2193 if( nArg+2==argc ){
2194 blob_append(&payload, argv[nArg+1], argl[nArg+1]);
2195 zType = "POST";
2196 }else{
2197 zType = "GET";
2198 }
2199 if( fAsynchronous ){
@@ -2268,11 +2341,11 @@
2268 if( argc!=2 ){
2269 return Th_WrongNumArgs(interp, "captureTh1 STRING");
2270 }
2271 pOrig = Th_SetOutputBlob(&out);
2272 zStr = argv[1];
2273 nStr = argl[1];
2274 rc = Th_Eval(g.interp, 0, zStr, nStr);
2275 Th_SetOutputBlob(pOrig);
2276 if(0==rc){
2277 Th_SetResult(g.interp, blob_str(&out), blob_size(&out));
2278 }
@@ -2387,13 +2460,15 @@
2387 {"setting", settingCmd, 0},
2388 {"styleFooter", styleFooterCmd, 0},
2389 {"styleHeader", styleHeaderCmd, 0},
2390 {"styleScript", styleScriptCmd, 0},
2391 {"submenu", submenuCmd, 0},
 
2392 {"tclReady", tclReadyCmd, 0},
2393 {"trace", traceCmd, 0},
2394 {"stime", stimeCmd, 0},
 
2395 {"unversioned", unversionedCmd, 0},
2396 {"utime", utimeCmd, 0},
2397 {"verifyCsrf", verifyCsrfCmd, 0},
2398 {"verifyLogin", verifyLoginCmd, 0},
2399 {"wiki", wikiCmd, (void*)&aFlags[0]},
@@ -2494,10 +2569,26 @@
2494 Th_Trace("set %h {%h}<br>\n", zName, zValue);
2495 }
2496 Th_SetVar(g.interp, zName, -1, zValue, strlen(zValue));
2497 }
2498 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2499
2500 /*
2501 ** Appends an element to a TH1 list value. This function is called by the
2502 ** transfer subsystem; therefore, it must be very careful to avoid doing
2503 ** any unnecessary work. To that end, the TH1 subsystem will not be called
@@ -2680,10 +2771,11 @@
2680 char *zResult = (char*)Th_GetResult(g.interp, &nResult);
2681 /*
2682 ** Make sure that the TH1 script error was not caused by a "missing"
2683 ** command hook handler as that is not actually an error condition.
2684 */
 
2685 if( memcmp(zResult, NO_COMMAND_HOOK_ERROR, nResult)!=0 ){
2686 sendError(0,zResult, nResult, 0);
2687 }else{
2688 /*
2689 ** There is no command hook handler "installed". This situation
@@ -2767,10 +2859,11 @@
2767 char *zResult = (char*)Th_GetResult(g.interp, &nResult);
2768 /*
2769 ** Make sure that the TH1 script error was not caused by a "missing"
2770 ** webpage hook handler as that is not actually an error condition.
2771 */
 
2772 if( memcmp(zResult, NO_WEBPAGE_HOOK_ERROR, nResult)!=0 ){
2773 sendError(0,zResult, nResult, 1);
2774 }else{
2775 /*
2776 ** There is no webpage hook handler "installed". This situation
@@ -2894,11 +2987,16 @@
2894 }
2895 rc = Th_GetVar(g.interp, (char*)zVar, nVar);
2896 z += i+1+n;
2897 i = 0;
2898 zResult = (char*)Th_GetResult(g.interp, &n);
2899 sendText(pOut,(char*)zResult, n, encode);
 
 
 
 
 
2900 }else if( z[i]=='<' && isBeginScriptTag(&z[i]) ){
2901 sendText(pOut,z, i, 0);
2902 z += i+5;
2903 for(i=0; z[i] && (z[i]!='<' || !isEndScriptTag(&z[i])); i++){}
2904 if( g.thTrace ){
@@ -2907,11 +3005,11 @@
2907 rc = Th_Eval(g.interp, 0, (const char*)z, i);
2908 if( g.thTrace ){
2909 int nTrRes;
2910 char *zTrRes = (char*)Th_GetResult(g.interp, &nTrRes);
2911 Th_Trace("[render_eval] => %h {%#h}<br>\n",
2912 Th_ReturnCodeName(rc, 0), nTrRes, zTrRes);
2913 }
2914 if( rc!=TH_OK ) break;
2915 z += i;
2916 if( z[0] ){ z += 6; }
2917 i = 0;
@@ -2953,10 +3051,78 @@
2953 ** as appropriate. We need to pass on g.th1Flags for the case of
2954 ** recursive calls, so that, e.g., TH_INIT_NO_ENCODE does not get
2955 ** inadvertently toggled off by a recursive call.
2956 */;
2957 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2958
2959 /*
2960 ** COMMAND: test-th-render
2961 **
2962 ** Usage: %fossil test-th-render FILE
@@ -2992,10 +3158,11 @@
2992 if( find_option("set-user-caps", 0, 0)!=0 ){
2993 const char *zCap = fossil_getenv("TH1_TEST_USER_CAPS");
2994 login_set_capabilities(zCap ? zCap : "sx", 0);
2995 g.useLocalauth = 1;
2996 }
 
2997 verify_all_options();
2998 if( g.argc<3 ){
2999 usage("FILE");
3000 }
3001 blob_zero(&in);
@@ -3044,10 +3211,11 @@
3044 if( find_option("set-user-caps", 0, 0)!=0 ){
3045 const char *zCap = fossil_getenv("TH1_TEST_USER_CAPS");
3046 login_set_capabilities(zCap ? zCap : "sx", 0);
3047 g.useLocalauth = 1;
3048 }
 
3049 verify_all_options();
3050 if( g.argc!=3 ){
3051 usage("script");
3052 }
3053 if(file_isfile(g.argv[2], ExtFILE)){
3054
--- src/th_main.c
+++ src/th_main.c
@@ -262,11 +262,11 @@
262 ){
263 char *zOut;
264 if( argc!=2 ){
265 return Th_WrongNumArgs(interp, "httpize STRING");
266 }
267 zOut = httpize((char*)argv[1], TH1_LEN(argl[1]));
268 Th_SetResult(interp, zOut, -1);
269 free(zOut);
270 return TH_OK;
271 }
272
@@ -291,11 +291,12 @@
291 if( argc<2 || argc>3 ){
292 return Th_WrongNumArgs(interp, "enable_output [LABEL] BOOLEAN");
293 }
294 rc = Th_ToInt(interp, argv[argc-1], argl[argc-1], &enableOutput);
295 if( g.thTrace ){
296 Th_Trace("enable_output {%.*s} -> %d<br>\n",
297 TH1_LEN(argl[1]),argv[1],enableOutput);
298 }
299 return rc;
300 }
301
302 /*
@@ -322,11 +323,11 @@
323 buul = (TH_INIT_NO_ENCODE & g.th1Flags) ? 0 : 1;
324 Th_SetResultInt(g.interp, buul);
325 if(argc>1){
326 if( g.thTrace ){
327 Th_Trace("enable_htmlify {%.*s} -> %d<br>\n",
328 TH1_LEN(argl[1]),argv[1],buul);
329 }
330 rc = Th_ToInt(interp, argv[argc-1], argl[argc-1], &buul);
331 if(!rc){
332 if(buul){
333 g.th1Flags &= ~TH_INIT_NO_ENCODE;
@@ -381,19 +382,23 @@
382 ** g.th1Flags has the TH_INIT_NO_ENCODE flag.
383 **
384 ** If pOut is NULL and the global pThOut is not then that blob
385 ** is used for output.
386 */
387 static void sendText(Blob *pOut, const char *z, int n, int encode){
388 if(0==pOut && pThOut!=0){
389 pOut = pThOut;
390 }
391 if(TH_INIT_NO_ENCODE & g.th1Flags){
392 encode = 0;
393 }
394 if( enableOutput && n ){
395 if( n<0 ){
396 n = strlen(z);
397 }else{
398 n = TH1_LEN(n);
399 }
400 if( encode ){
401 z = htmlize(z, n);
402 n = strlen(z);
403 }
404 if(pOut!=0){
@@ -525,14 +530,22 @@
530 void *pConvert,
531 int argc,
532 const char **argv,
533 int *argl
534 ){
535 int encode = *(unsigned int*)pConvert;
536 int n;
537 if( argc!=2 ){
538 return Th_WrongNumArgs(interp, "puts STRING");
539 }
540 n = argl[1];
541 if( encode==0 && n>0 && TH1_TAINTED(n) ){
542 if( Th_ReportTaint(interp, "output string", argv[1], n) ){
543 return TH_ERROR;
544 }
545 }
546 sendText(0,(char*)argv[1], TH1_LEN(n), encode);
547 return TH_OK;
548 }
549
550 /*
551 ** TH1 command: redirect URL ?withMethod?
@@ -557,10 +570,15 @@
570 }
571 if( argc==3 ){
572 if( Th_ToInt(interp, argv[2], argl[2], &withMethod) ){
573 return TH_ERROR;
574 }
575 }
576 if( TH1_TAINTED(argl[1])
577 && Th_ReportTaint(interp,"redirect URL",argv[1],argl[1])
578 ){
579 return TH_ERROR;
580 }
581 if( withMethod ){
582 cgi_redirect_with_method(argv[1]);
583 }else{
584 cgi_redirect(argv[1]);
@@ -660,11 +678,11 @@
678 int nValue = 0;
679 if( argc!=2 ){
680 return Th_WrongNumArgs(interp, "markdown STRING");
681 }
682 blob_zero(&src);
683 blob_init(&src, (char*)argv[1], TH1_LEN(argl[1]));
684 blob_zero(&title); blob_zero(&body);
685 markdown_to_html(&src, &title, &body);
686 Th_ListAppend(interp, &zValue, &nValue, blob_str(&title), blob_size(&title));
687 Th_ListAppend(interp, &zValue, &nValue, blob_str(&body), blob_size(&body));
688 Th_SetResult(interp, zValue, nValue);
@@ -690,11 +708,11 @@
708 if( argc!=2 ){
709 return Th_WrongNumArgs(interp, "wiki STRING");
710 }
711 if( enableOutput ){
712 Blob src;
713 blob_init(&src, (char*)argv[1], TH1_LEN(argl[1]));
714 wiki_convert(&src, 0, flags);
715 blob_reset(&src);
716 }
717 return TH_OK;
718 }
@@ -735,11 +753,11 @@
753 ){
754 char *zOut;
755 if( argc!=2 ){
756 return Th_WrongNumArgs(interp, "htmlize STRING");
757 }
758 zOut = htmlize((char*)argv[1], TH1_LEN(argl[1]));
759 Th_SetResult(interp, zOut, -1);
760 free(zOut);
761 return TH_OK;
762 }
763
@@ -757,11 +775,11 @@
775 ){
776 char *zOut;
777 if( argc!=2 ){
778 return Th_WrongNumArgs(interp, "encode64 STRING");
779 }
780 zOut = encode64((char*)argv[1], TH1_LEN(argl[1]));
781 Th_SetResult(interp, zOut, -1);
782 free(zOut);
783 return TH_OK;
784 }
785
@@ -778,11 +796,11 @@
796 int argc,
797 const char **argv,
798 int *argl
799 ){
800 char *zOut;
801 if( argc>=2 && TH1_LEN(argl[1])==6 && memcmp(argv[1],"-local",6)==0 ){
802 zOut = db_text("??", "SELECT datetime('now',toLocal())");
803 }else{
804 zOut = db_text("??", "SELECT datetime('now')");
805 }
806 Th_SetResult(interp, zOut, -1);
@@ -810,13 +828,13 @@
828 if( argc<2 ){
829 return Th_WrongNumArgs(interp, "hascap STRING ...");
830 }
831 for(i=1; rc==1 && i<argc; i++){
832 if( g.thTrace ){
833 Th_ListAppend(interp, &zCapList, &nCapList, argv[i], TH1_LEN(argl[i]));
834 }
835 rc = login_has_capability((char*)argv[i],TH1_LEN(argl[i]),*(int*)p);
836 }
837 if( g.thTrace ){
838 Th_Trace("[%s %#h] => %d<br>\n", argv[0], nCapList, zCapList, rc);
839 Th_Free(interp, zCapList);
840 }
@@ -858,11 +876,11 @@
876 int i;
877
878 if( argc!=2 ){
879 return Th_WrongNumArgs(interp, "capexpr EXPR");
880 }
881 rc = Th_SplitList(interp, argv[1], TH1_LEN(argl[1]), &azCap, &anCap, &nCap);
882 if( rc ) return rc;
883 rc = 0;
884 for(i=0; i<nCap; i++){
885 if( azCap[i][0]=='!' ){
886 rc = !login_has_capability(azCap[i]+1, anCap[i]-1, 0);
@@ -921,11 +939,12 @@
939 if( argc<2 ){
940 return Th_WrongNumArgs(interp, "hascap STRING ...");
941 }
942 for(i=1; i<argc && rc; i++){
943 int match = 0;
944 int nn = TH1_LEN(argl[i]);
945 for(j=0; j<nn; j++){
946 switch( argv[i][j] ){
947 case 'c': match |= searchCap & SRCH_CKIN; break;
948 case 'd': match |= searchCap & SRCH_DOC; break;
949 case 't': match |= searchCap & SRCH_TKT; break;
950 case 'w': match |= searchCap & SRCH_WIKI; break;
@@ -932,11 +951,11 @@
951 }
952 }
953 if( !match ) rc = 0;
954 }
955 if( g.thTrace ){
956 Th_Trace("[searchable %#h] => %d<br>\n", TH1_LEN(argl[1]), argv[1], rc);
957 }
958 Th_SetResultInt(interp, rc);
959 return TH_OK;
960 }
961
@@ -1051,11 +1070,11 @@
1070 #endif
1071 else if( 0 == fossil_strnicmp( zArg, "markdown\0", 9 ) ){
1072 rc = 1;
1073 }
1074 if( g.thTrace ){
1075 Th_Trace("[hasfeature %#h] => %d<br>\n", TH1_LEN(argl[1]), zArg, rc);
1076 }
1077 Th_SetResultInt(interp, rc);
1078 return TH_OK;
1079 }
1080
@@ -1104,18 +1123,20 @@
1123 const char **argv,
1124 int *argl
1125 ){
1126 int rc = 0;
1127 int i;
1128 int nn;
1129 if( argc!=2 ){
1130 return Th_WrongNumArgs(interp, "anycap STRING");
1131 }
1132 nn = TH1_LEN(argl[1]);
1133 for(i=0; rc==0 && i<nn; i++){
1134 rc = login_has_capability((char*)&argv[1][i],1,0);
1135 }
1136 if( g.thTrace ){
1137 Th_Trace("[anycap %#h] => %d<br>\n", TH1_LEN(argl[1]), argv[1], rc);
1138 }
1139 Th_SetResultInt(interp, rc);
1140 return TH_OK;
1141 }
1142
@@ -1140,22 +1161,23 @@
1161 return Th_WrongNumArgs(interp, "combobox NAME TEXT-LIST NUMLINES");
1162 }
1163 if( enableOutput ){
1164 int height;
1165 Blob name;
1166 int nValue = 0;
1167 const char *zValue;
1168 char *z, *zH;
1169 int nElem;
1170 int *aszElem;
1171 char **azElem;
1172 int i;
1173
1174 if( Th_ToInt(interp, argv[3], argl[3], &height) ) return TH_ERROR;
1175 Th_SplitList(interp, argv[2], TH1_LEN(argl[2]), &azElem, &aszElem, &nElem);
1176 blob_init(&name, (char*)argv[1], TH1_LEN(argl[1]));
1177 zValue = Th_Fetch(blob_str(&name), &nValue);
1178 nValue = TH1_LEN(nValue);
1179 zH = htmlize(blob_buffer(&name), blob_size(&name));
1180 z = mprintf("<select id=\"%s\" name=\"%s\" size=\"%d\">", zH, zH, height);
1181 free(zH);
1182 sendText(0,z, -1, 0);
1183 free(z);
@@ -1247,11 +1269,11 @@
1269 return Th_WrongNumArgs(interp, "linecount STRING MAX MIN");
1270 }
1271 if( Th_ToInt(interp, argv[2], argl[2], &iMax) ) return TH_ERROR;
1272 if( Th_ToInt(interp, argv[3], argl[3], &iMin) ) return TH_ERROR;
1273 z = argv[1];
1274 size = TH1_LEN(argl[1]);
1275 for(n=1, i=0; i<size; i++){
1276 if( z[i]=='\n' ){
1277 n++;
1278 if( n>=iMax ) break;
1279 }
@@ -1407,11 +1429,12 @@
1429 return TH_OK;
1430 }else if( fossil_strnicmp(argv[1], "vfs\0", 4)==0 ){
1431 Th_SetResult(interp, g.zVfsName ? g.zVfsName : zDefault, -1);
1432 return TH_OK;
1433 }else{
1434 Th_ErrorMessage(interp, "unsupported global state:",
1435 argv[1], TH1_LEN(argl[1]));
1436 return TH_ERROR;
1437 }
1438 }
1439
1440 /*
@@ -1426,17 +1449,21 @@
1449 int argc,
1450 const char **argv,
1451 int *argl
1452 ){
1453 const char *zDefault = 0;
1454 const char *zVal;
1455 int sz;
1456 if( argc!=2 && argc!=3 ){
1457 return Th_WrongNumArgs(interp, "getParameter NAME ?DEFAULT?");
1458 }
1459 if( argc==3 ){
1460 zDefault = argv[2];
1461 }
1462 zVal = cgi_parameter(argv[1], zDefault);
1463 sz = th_strlen(zVal);
1464 Th_SetResult(interp, zVal, TH1_ADD_TAINT(sz));
1465 return TH_OK;
1466 }
1467
1468 /*
1469 ** TH1 command: setParameter NAME VALUE
@@ -1848,10 +1875,47 @@
1875 sqlite3_snprintf(sizeof(zUTime), zUTime, "%llu", x);
1876 Th_SetResult(interp, zUTime, -1);
1877 return TH_OK;
1878 }
1879
1880 /*
1881 ** TH1 command: taint STRING
1882 **
1883 ** Return a copy of STRING that is marked as tainted.
1884 */
1885 static int taintCmd(
1886 Th_Interp *interp,
1887 void *p,
1888 int argc,
1889 const char **argv,
1890 int *argl
1891 ){
1892 if( argc!=2 ){
1893 return Th_WrongNumArgs(interp, "STRING");
1894 }
1895 Th_SetResult(interp, argv[1], TH1_ADD_TAINT(argl[1]));
1896 return TH_OK;
1897 }
1898
1899 /*
1900 ** TH1 command: untaint STRING
1901 **
1902 ** Return a copy of STRING that is marked as untainted.
1903 */
1904 static int untaintCmd(
1905 Th_Interp *interp,
1906 void *p,
1907 int argc,
1908 const char **argv,
1909 int *argl
1910 ){
1911 if( argc!=2 ){
1912 return Th_WrongNumArgs(interp, "STRING");
1913 }
1914 Th_SetResult(interp, argv[1], TH1_LEN(argl[1]));
1915 return TH_OK;
1916 }
1917
1918 /*
1919 ** TH1 command: randhex N
1920 **
1921 ** Return N*2 random hexadecimal digits with N<50. If N is omitted,
@@ -1923,11 +1987,13 @@
1987 int res = TH_OK;
1988 int nVar;
1989 char *zErr = 0;
1990 int noComplain = 0;
1991
1992 if( argc>3 && TH1_LEN(argl[1])==11
1993 && strncmp(argv[1], "-nocomplain", 11)==0
1994 ){
1995 argc--;
1996 argv++;
1997 argl++;
1998 noComplain = 1;
1999 }
@@ -1939,15 +2005,22 @@
2005 Th_ErrorMessage(interp, "database is not open", 0, 0);
2006 return TH_ERROR;
2007 }
2008 zSql = argv[1];
2009 nSql = argl[1];
2010 if( TH1_TAINTED(nSql) ){
2011 if( Th_ReportTaint(interp,"query SQL",zSql,nSql) ){
2012 return TH_ERROR;
2013 }
2014 nSql = TH1_LEN(nSql);
2015 }
2016
2017 while( res==TH_OK && nSql>0 ){
2018 zErr = 0;
2019 report_restrict_sql(&zErr);
2020 g.dbIgnoreErrors++;
2021 rc = sqlite3_prepare_v2(g.db, argv[1], TH1_LEN(argl[1]), &pStmt, &zTail);
2022 g.dbIgnoreErrors--;
2023 report_unrestrict_sql();
2024 if( rc!=0 || zErr!=0 ){
2025 if( noComplain ) return TH_OK;
2026 Th_ErrorMessage(interp, "SQL error: ",
@@ -1964,31 +2037,31 @@
2037 int szVar = zVar ? th_strlen(zVar) : 0;
2038 if( szVar>1 && zVar[0]=='$'
2039 && Th_GetVar(interp, zVar+1, szVar-1)==TH_OK ){
2040 int nVal;
2041 const char *zVal = Th_GetResult(interp, &nVal);
2042 sqlite3_bind_text(pStmt, i, zVal, TH1_LEN(nVal), SQLITE_TRANSIENT);
2043 }
2044 }
2045 while( res==TH_OK && ignore_errors_step(pStmt)==SQLITE_ROW ){
2046 int nCol = sqlite3_column_count(pStmt);
2047 for(i=0; i<nCol; i++){
2048 const char *zCol = sqlite3_column_name(pStmt, i);
2049 int szCol = th_strlen(zCol);
2050 const char *zVal = (const char*)sqlite3_column_text(pStmt, i);
2051 int szVal = sqlite3_column_bytes(pStmt, i);
2052 Th_SetVar(interp, zCol, szCol, zVal, TH1_ADD_TAINT(szVal));
2053 }
2054 if( g.thTrace ){
2055 Th_Trace("query_eval {<pre>%#h</pre>}<br>\n",TH1_LEN(argl[2]),argv[2]);
2056 }
2057 res = Th_Eval(interp, 0, argv[2], TH1_LEN(argl[2]));
2058 if( g.thTrace ){
2059 int nTrRes;
2060 char *zTrRes = (char*)Th_GetResult(g.interp, &nTrRes);
2061 Th_Trace("[query_eval] => %h {%#h}<br>\n",
2062 Th_ReturnCodeName(res, 0), TH1_LEN(nTrRes), zTrRes);
2063 }
2064 if( res==TH_BREAK || res==TH_CONTINUE ) res = TH_OK;
2065 }
2066 rc = sqlite3_finalize(pStmt);
2067 if( rc!=SQLITE_OK ){
@@ -2038,11 +2111,11 @@
2111 Th_SetResult(interp, 0, 0);
2112 rc = TH_OK;
2113 }
2114 if( g.thTrace ){
2115 Th_Trace("[setting %s%#h] => %d<br>\n", strict ? "strict " : "",
2116 TH1_LEN(argl[nArg]), argv[nArg], rc);
2117 }
2118 return rc;
2119 }
2120
2121 /*
@@ -2121,11 +2194,11 @@
2194 return Th_WrongNumArgs(interp, REGEXP_WRONGNUMARGS);
2195 }
2196 zErr = re_compile(&pRe, argv[nArg], noCase);
2197 if( !zErr ){
2198 Th_SetResultInt(interp, re_match(pRe,
2199 (const unsigned char *)argv[nArg+1], TH1_LEN(argl[nArg+1])));
2200 rc = TH_OK;
2201 }else{
2202 Th_SetResult(interp, zErr, -1);
2203 rc = TH_ERROR;
2204 }
@@ -2160,11 +2233,11 @@
2233 UrlData urlData;
2234
2235 if( argc<2 || argc>5 ){
2236 return Th_WrongNumArgs(interp, HTTP_WRONGNUMARGS);
2237 }
2238 if( fossil_strnicmp(argv[nArg], "-asynchronous", TH1_LEN(argl[nArg]))==0 ){
2239 fAsynchronous = 1; nArg++;
2240 }
2241 if( fossil_strcmp(argv[nArg], "--")==0 ) nArg++;
2242 if( nArg+1!=argc && nArg+2!=argc ){
2243 return Th_WrongNumArgs(interp, REGEXP_WRONGNUMARGS);
@@ -2189,11 +2262,11 @@
2262 return TH_ERROR;
2263 }
2264 re_free(pRe);
2265 blob_zero(&payload);
2266 if( nArg+2==argc ){
2267 blob_append(&payload, argv[nArg+1], TH1_LEN(argl[nArg+1]));
2268 zType = "POST";
2269 }else{
2270 zType = "GET";
2271 }
2272 if( fAsynchronous ){
@@ -2268,11 +2341,11 @@
2341 if( argc!=2 ){
2342 return Th_WrongNumArgs(interp, "captureTh1 STRING");
2343 }
2344 pOrig = Th_SetOutputBlob(&out);
2345 zStr = argv[1];
2346 nStr = TH1_LEN(argl[1]);
2347 rc = Th_Eval(g.interp, 0, zStr, nStr);
2348 Th_SetOutputBlob(pOrig);
2349 if(0==rc){
2350 Th_SetResult(g.interp, blob_str(&out), blob_size(&out));
2351 }
@@ -2387,13 +2460,15 @@
2460 {"setting", settingCmd, 0},
2461 {"styleFooter", styleFooterCmd, 0},
2462 {"styleHeader", styleHeaderCmd, 0},
2463 {"styleScript", styleScriptCmd, 0},
2464 {"submenu", submenuCmd, 0},
2465 {"taint", taintCmd, 0},
2466 {"tclReady", tclReadyCmd, 0},
2467 {"trace", traceCmd, 0},
2468 {"stime", stimeCmd, 0},
2469 {"untaint", untaintCmd, 0},
2470 {"unversioned", unversionedCmd, 0},
2471 {"utime", utimeCmd, 0},
2472 {"verifyCsrf", verifyCsrfCmd, 0},
2473 {"verifyLogin", verifyLoginCmd, 0},
2474 {"wiki", wikiCmd, (void*)&aFlags[0]},
@@ -2494,10 +2569,26 @@
2569 Th_Trace("set %h {%h}<br>\n", zName, zValue);
2570 }
2571 Th_SetVar(g.interp, zName, -1, zValue, strlen(zValue));
2572 }
2573 }
2574
2575 /*
2576 ** Store a string value in a variable in the interpreter
2577 ** with the "taint" marking, so that TH1 knows that this
2578 ** variable contains content under the control of the remote
2579 ** user and presents a risk of XSS or SQL-injection attacks.
2580 */
2581 void Th_StoreUnsafe(const char *zName, const char *zValue){
2582 Th_FossilInit(TH_INIT_DEFAULT);
2583 if( zValue ){
2584 if( g.thTrace ){
2585 Th_Trace("set %h [taint {%h}]<br>\n", zName, zValue);
2586 }
2587 Th_SetVar(g.interp, zName, -1, zValue, TH1_ADD_TAINT(strlen(zValue)));
2588 }
2589 }
2590
2591 /*
2592 ** Appends an element to a TH1 list value. This function is called by the
2593 ** transfer subsystem; therefore, it must be very careful to avoid doing
2594 ** any unnecessary work. To that end, the TH1 subsystem will not be called
@@ -2680,10 +2771,11 @@
2771 char *zResult = (char*)Th_GetResult(g.interp, &nResult);
2772 /*
2773 ** Make sure that the TH1 script error was not caused by a "missing"
2774 ** command hook handler as that is not actually an error condition.
2775 */
2776 nResult = TH1_LEN(nResult);
2777 if( memcmp(zResult, NO_COMMAND_HOOK_ERROR, nResult)!=0 ){
2778 sendError(0,zResult, nResult, 0);
2779 }else{
2780 /*
2781 ** There is no command hook handler "installed". This situation
@@ -2767,10 +2859,11 @@
2859 char *zResult = (char*)Th_GetResult(g.interp, &nResult);
2860 /*
2861 ** Make sure that the TH1 script error was not caused by a "missing"
2862 ** webpage hook handler as that is not actually an error condition.
2863 */
2864 nResult = TH1_LEN(nResult);
2865 if( memcmp(zResult, NO_WEBPAGE_HOOK_ERROR, nResult)!=0 ){
2866 sendError(0,zResult, nResult, 1);
2867 }else{
2868 /*
2869 ** There is no webpage hook handler "installed". This situation
@@ -2894,11 +2987,16 @@
2987 }
2988 rc = Th_GetVar(g.interp, (char*)zVar, nVar);
2989 z += i+1+n;
2990 i = 0;
2991 zResult = (char*)Th_GetResult(g.interp, &n);
2992 if( !TH1_TAINTED(n)
2993 || encode
2994 || Th_ReportTaint(g.interp, "inline variable", zVar, nVar)==TH_OK
2995 ){
2996 sendText(pOut,(char*)zResult, n, encode);
2997 }
2998 }else if( z[i]=='<' && isBeginScriptTag(&z[i]) ){
2999 sendText(pOut,z, i, 0);
3000 z += i+5;
3001 for(i=0; z[i] && (z[i]!='<' || !isEndScriptTag(&z[i])); i++){}
3002 if( g.thTrace ){
@@ -2907,11 +3005,11 @@
3005 rc = Th_Eval(g.interp, 0, (const char*)z, i);
3006 if( g.thTrace ){
3007 int nTrRes;
3008 char *zTrRes = (char*)Th_GetResult(g.interp, &nTrRes);
3009 Th_Trace("[render_eval] => %h {%#h}<br>\n",
3010 Th_ReturnCodeName(rc, 0), TH1_LEN(nTrRes), zTrRes);
3011 }
3012 if( rc!=TH_OK ) break;
3013 z += i;
3014 if( z[0] ){ z += 6; }
3015 i = 0;
@@ -2953,10 +3051,78 @@
3051 ** as appropriate. We need to pass on g.th1Flags for the case of
3052 ** recursive calls, so that, e.g., TH_INIT_NO_ENCODE does not get
3053 ** inadvertently toggled off by a recursive call.
3054 */;
3055 }
3056
3057 /*
3058 ** SETTING: vuln-report width=8 default=log
3059 **
3060 ** This setting controls Fossil's behavior when it encounters a potential
3061 ** XSS or SQL-injection vulnerability due to misuse of TH1 configuration
3062 ** scripts. Choices are:
3063 **
3064 ** off Do nothing. Ignore the vulnerability.
3065 **
3066 ** log Write a report of the problem into the error log.
3067 **
3068 ** block Like "log" but also prevent the offending TH1 command
3069 ** from running.
3070 **
3071 ** fatal Render an error message page instead of the requested
3072 ** page.
3073 */
3074
3075 /*
3076 ** Report misuse of a tainted string in TH1.
3077 **
3078 ** The behavior depends on the vuln-report setting. If "off", this routine
3079 ** is a no-op. Otherwise, right a message into the error log. If
3080 ** vuln-report is "log", that is all that happens. But for any other
3081 ** value of vuln-report, a fatal error is raised.
3082 */
3083 int Th_ReportTaint(
3084 Th_Interp *interp, /* Report error here, if an error is reported */
3085 const char *zWhere, /* Where the tainted string appears */
3086 const char *zStr, /* The tainted string */
3087 int nStr /* Length of the tainted string */
3088 ){
3089 static const char *zDisp = 0; /* Dispensation; what to do with the error */
3090 const char *zVulnType; /* Type of vulnerability */
3091
3092 if( zDisp==0 ) zDisp = db_get("vuln-report","log");
3093 if( is_false(zDisp) ) return 0;
3094 if( strstr(zWhere,"SQL")!=0 ){
3095 zVulnType = "SQL-injection";
3096 }else{
3097 zVulnType = "XSS";
3098 }
3099 nStr = TH1_LEN(nStr);
3100 fossil_errorlog("possible TH1 %s vulnerability due to tainted %s: \"%.*s\"",
3101 zVulnType, zWhere, nStr, zStr);
3102 if( strcmp(zDisp,"log")==0 ){
3103 return 0;
3104 }
3105 if( strcmp(zDisp,"block")==0 ){
3106 char *z = mprintf("tainted %s: \"", zWhere);
3107 Th_ErrorMessage(interp, z, zStr, nStr);
3108 fossil_free(z);
3109 }else{
3110 char *z = mprintf("%#h", nStr, zStr);
3111 zDisp = "off";
3112 cgi_reset_content();
3113 style_submenu_enable(0);
3114 style_set_current_feature("error");
3115 style_header("Configuration Error");
3116 @ <p>Error in a TH1 configuration script:
3117 @ tainted %h(zWhere): "%z(z)"
3118 style_finish_page();
3119 cgi_reply();
3120 fossil_exit(1);
3121 }
3122 return 1;
3123 }
3124
3125 /*
3126 ** COMMAND: test-th-render
3127 **
3128 ** Usage: %fossil test-th-render FILE
@@ -2992,10 +3158,11 @@
3158 if( find_option("set-user-caps", 0, 0)!=0 ){
3159 const char *zCap = fossil_getenv("TH1_TEST_USER_CAPS");
3160 login_set_capabilities(zCap ? zCap : "sx", 0);
3161 g.useLocalauth = 1;
3162 }
3163 db_find_and_open_repository(OPEN_OK_NOT_FOUND|OPEN_SUBSTITUTE,0);
3164 verify_all_options();
3165 if( g.argc<3 ){
3166 usage("FILE");
3167 }
3168 blob_zero(&in);
@@ -3044,10 +3211,11 @@
3211 if( find_option("set-user-caps", 0, 0)!=0 ){
3212 const char *zCap = fossil_getenv("TH1_TEST_USER_CAPS");
3213 login_set_capabilities(zCap ? zCap : "sx", 0);
3214 g.useLocalauth = 1;
3215 }
3216 db_find_and_open_repository(OPEN_OK_NOT_FOUND|OPEN_SUBSTITUTE,0);
3217 verify_all_options();
3218 if( g.argc!=3 ){
3219 usage("script");
3220 }
3221 if(file_isfile(g.argv[2], ExtFILE)){
3222
+27 -89
--- src/th_tcl.c
+++ src/th_tcl.c
@@ -41,16 +41,16 @@
4141
#define USE_ARGV_TO_OBJV() \
4242
int objc; \
4343
Tcl_Obj **objv; \
4444
int obji;
4545
46
-#define COPY_ARGV_TO_OBJV() \
47
- objc = argc-1; \
48
- objv = (Tcl_Obj **)ckalloc((unsigned)(objc * sizeof(Tcl_Obj *))); \
49
- for(obji=1; obji<argc; obji++){ \
50
- objv[obji-1] = Tcl_NewStringObj(argv[obji], argl[obji]); \
51
- Tcl_IncrRefCount(objv[obji-1]); \
46
+#define COPY_ARGV_TO_OBJV() \
47
+ objc = argc-1; \
48
+ objv = (Tcl_Obj **)ckalloc((unsigned)(objc * sizeof(Tcl_Obj *))); \
49
+ for(obji=1; obji<argc; obji++){ \
50
+ objv[obji-1] = Tcl_NewStringObj(argv[obji], TH1_LEN(argl[obji])); \
51
+ Tcl_IncrRefCount(objv[obji-1]); \
5252
}
5353
5454
#define FREE_ARGV_TO_OBJV() \
5555
for(obji=1; obji<argc; obji++){ \
5656
Tcl_DecrRefCount(objv[obji-1]); \
@@ -183,11 +183,11 @@
183183
** the only Tcl API functions that MUST be called prior to being able to call
184184
** Tcl_InitStubs (i.e. because it requires a Tcl interpreter). For complete
185185
** cleanup if the Tcl stubs initialization fails somehow, the Tcl_DeleteInterp
186186
** and Tcl_Finalize function types are also required.
187187
*/
188
-typedef void (tcl_FindExecutableProc) (const char *);
188
+typedef const char *(tcl_FindExecutableProc) (const char *);
189189
typedef Tcl_Interp *(tcl_CreateInterpProc) (void);
190190
typedef void (tcl_DeleteInterpProc) (Tcl_Interp *);
191191
typedef void (tcl_FinalizeProc) (void);
192192
193193
/*
@@ -321,27 +321,10 @@
321321
** by the caller. This must be declared here because quite a few functions in
322322
** this file need to use it before it can be defined.
323323
*/
324324
static int createTclInterp(Th_Interp *interp, void *pContext);
325325
326
-/*
327
-** Returns the TH1 return code corresponding to the specified Tcl
328
-** return code.
329
-*/
330
-static int getTh1ReturnCode(
331
- int rc /* The Tcl return code value to convert. */
332
-){
333
- switch( rc ){
334
- case /*0*/ TCL_OK: return /*0*/ TH_OK;
335
- case /*1*/ TCL_ERROR: return /*1*/ TH_ERROR;
336
- case /*2*/ TCL_RETURN: return /*3*/ TH_RETURN;
337
- case /*3*/ TCL_BREAK: return /*2*/ TH_BREAK;
338
- case /*4*/ TCL_CONTINUE: return /*4*/ TH_CONTINUE;
339
- default /*?*/: return /*?*/ rc;
340
- }
341
-}
342
-
343326
/*
344327
** Returns the Tcl return code corresponding to the specified TH1
345328
** return code.
346329
*/
347330
static int getTclReturnCode(
@@ -387,10 +370,12 @@
387370
static char *getTclResult(
388371
Tcl_Interp *pInterp,
389372
int *pN
390373
){
391374
Tcl_Obj *resultPtr;
375
+ Tcl_Size n;
376
+ char *zRes;
392377
393378
if( !pInterp ){ /* This should not happen. */
394379
if( pN ) *pN = 0;
395380
return 0;
396381
}
@@ -397,11 +382,13 @@
397382
resultPtr = Tcl_GetObjResult(pInterp);
398383
if( !resultPtr ){ /* This should not happen either? */
399384
if( pN ) *pN = 0;
400385
return 0;
401386
}
402
- return Tcl_GetStringFromObj(resultPtr, pN);
387
+ zRes = Tcl_GetStringFromObj(resultPtr, &n);
388
+ *pN = (int)n;
389
+ return zRes;
403390
}
404391
405392
/*
406393
** Tcl context information used by TH1. This structure definition has been
407394
** copied from and should be kept in sync with the one in "main.c".
@@ -416,48 +403,12 @@
416403
tcl_FinalizeProc *xFinalize; /* Tcl_Finalize() pointer. */
417404
Tcl_Interp *interp; /* The on-demand created Tcl interpreter. */
418405
int useObjProc; /* Non-zero if an objProc can be called directly. */
419406
int useTip285; /* Non-zero if TIP #285 is available. */
420407
const char *setup; /* The optional Tcl setup script. */
421
- tcl_NotifyProc *xPreEval; /* Optional, called before Tcl_Eval*(). */
422
- void *pPreContext; /* Optional, provided to xPreEval(). */
423
- tcl_NotifyProc *xPostEval; /* Optional, called after Tcl_Eval*(). */
424
- void *pPostContext; /* Optional, provided to xPostEval(). */
425408
};
426409
427
-/*
428
-** This function calls the configured xPreEval or xPostEval functions, if any.
429
-** May have arbitrary side-effects. This function returns the result of the
430
-** called notification function or the value of the rc argument if there is no
431
-** notification function configured.
432
-*/
433
-static int notifyPreOrPostEval(
434
- int bIsPost,
435
- Th_Interp *interp,
436
- void *ctx,
437
- int argc,
438
- const char **argv,
439
- int *argl,
440
- int rc
441
-){
442
- struct TclContext *tclContext = (struct TclContext *)ctx;
443
- tcl_NotifyProc *xNotifyProc;
444
-
445
- if( !tclContext ){
446
- Th_ErrorMessage(interp,
447
- "invalid Tcl context", (const char *)"", 0);
448
- return TH_ERROR;
449
- }
450
- xNotifyProc = bIsPost ? tclContext->xPostEval : tclContext->xPreEval;
451
- if( xNotifyProc ){
452
- rc = xNotifyProc(bIsPost ?
453
- tclContext->pPostContext : tclContext->pPreContext,
454
- interp, ctx, argc, argv, argl, rc);
455
- }
456
- return rc;
457
-}
458
-
459410
/*
460411
** TH1 command: tclEval arg ?arg ...?
461412
**
462413
** Evaluates the Tcl script and returns its result verbatim. If a Tcl script
463414
** error is generated, it will be transformed into a TH1 script error. The
@@ -485,17 +436,13 @@
485436
tclInterp = GET_CTX_TCL_INTERP(ctx);
486437
if( !tclInterp || Tcl_InterpDeleted(tclInterp) ){
487438
Th_ErrorMessage(interp, "invalid Tcl interpreter", (const char *)"", 0);
488439
return TH_ERROR;
489440
}
490
- rc = notifyPreOrPostEval(0, interp, ctx, argc, argv, argl, rc);
491
- if( rc!=TH_OK ){
492
- return rc;
493
- }
494441
Tcl_Preserve((ClientData)tclInterp);
495442
if( argc==2 ){
496
- objPtr = Tcl_NewStringObj(argv[1], argl[1]);
443
+ objPtr = Tcl_NewStringObj(argv[1], TH1_LEN(argl[1]));
497444
Tcl_IncrRefCount(objPtr);
498445
rc = Tcl_EvalObjEx(tclInterp, objPtr, 0);
499446
Tcl_DecrRefCount(objPtr); objPtr = 0;
500447
}else{
501448
USE_ARGV_TO_OBJV();
@@ -507,12 +454,10 @@
507454
FREE_ARGV_TO_OBJV();
508455
}
509456
zResult = getTclResult(tclInterp, &nResult);
510457
Th_SetResult(interp, zResult, nResult);
511458
Tcl_Release((ClientData)tclInterp);
512
- rc = notifyPreOrPostEval(1, interp, ctx, argc, argv, argl,
513
- getTh1ReturnCode(rc));
514459
return rc;
515460
}
516461
517462
/*
518463
** TH1 command: tclExpr arg ?arg ...?
@@ -545,17 +490,13 @@
545490
tclInterp = GET_CTX_TCL_INTERP(ctx);
546491
if( !tclInterp || Tcl_InterpDeleted(tclInterp) ){
547492
Th_ErrorMessage(interp, "invalid Tcl interpreter", (const char *)"", 0);
548493
return TH_ERROR;
549494
}
550
- rc = notifyPreOrPostEval(0, interp, ctx, argc, argv, argl, rc);
551
- if( rc!=TH_OK ){
552
- return rc;
553
- }
554495
Tcl_Preserve((ClientData)tclInterp);
555496
if( argc==2 ){
556
- objPtr = Tcl_NewStringObj(argv[1], argl[1]);
497
+ objPtr = Tcl_NewStringObj(argv[1], TH1_LEN(argl[1]));
557498
Tcl_IncrRefCount(objPtr);
558499
rc = Tcl_ExprObj(tclInterp, objPtr, &resultObjPtr);
559500
Tcl_DecrRefCount(objPtr); objPtr = 0;
560501
}else{
561502
USE_ARGV_TO_OBJV();
@@ -565,21 +506,21 @@
565506
rc = Tcl_ExprObj(tclInterp, objPtr, &resultObjPtr);
566507
Tcl_DecrRefCount(objPtr); objPtr = 0;
567508
FREE_ARGV_TO_OBJV();
568509
}
569510
if( rc==TCL_OK ){
570
- zResult = Tcl_GetStringFromObj(resultObjPtr, &nResult);
511
+ Tcl_Size szResult = 0;
512
+ zResult = Tcl_GetStringFromObj(resultObjPtr, &szResult);
513
+ nResult = (int)szResult;
571514
}else{
572515
zResult = getTclResult(tclInterp, &nResult);
573516
}
574
- Th_SetResult(interp, zResult, nResult);
517
+ Th_SetResult(interp, zResult, (int)nResult);
575518
if( rc==TCL_OK ){
576519
Tcl_DecrRefCount(resultObjPtr); resultObjPtr = 0;
577520
}
578521
Tcl_Release((ClientData)tclInterp);
579
- rc = notifyPreOrPostEval(1, interp, ctx, argc, argv, argl,
580
- getTh1ReturnCode(rc));
581522
return rc;
582523
}
583524
584525
/*
585526
** TH1 command: tclInvoke command ?arg ...?
@@ -610,20 +551,16 @@
610551
tclInterp = GET_CTX_TCL_INTERP(ctx);
611552
if( !tclInterp || Tcl_InterpDeleted(tclInterp) ){
612553
Th_ErrorMessage(interp, "invalid Tcl interpreter", (const char *)"", 0);
613554
return TH_ERROR;
614555
}
615
- rc = notifyPreOrPostEval(0, interp, ctx, argc, argv, argl, rc);
616
- if( rc!=TH_OK ){
617
- return rc;
618
- }
619556
Tcl_Preserve((ClientData)tclInterp);
620557
#if !defined(USE_TCL_EVALOBJV) || !USE_TCL_EVALOBJV
621558
if( GET_CTX_TCL_USEOBJPROC(ctx) ){
622559
Tcl_Command command;
623560
Tcl_CmdInfo cmdInfo;
624
- Tcl_Obj *objPtr = Tcl_NewStringObj(argv[1], argl[1]);
561
+ Tcl_Obj *objPtr = Tcl_NewStringObj(argv[1], TH1_LEN(argl[1]));
625562
Tcl_IncrRefCount(objPtr);
626563
command = Tcl_GetCommandFromObj(tclInterp, objPtr);
627564
if( !command || Tcl_GetCommandInfoFromToken(command, &cmdInfo)==0 ){
628565
Th_ErrorMessage(interp, "Tcl command not found:", argv[1], argl[1]);
629566
Tcl_DecrRefCount(objPtr); objPtr = 0;
@@ -649,12 +586,10 @@
649586
FREE_ARGV_TO_OBJV();
650587
}
651588
zResult = getTclResult(tclInterp, &nResult);
652589
Th_SetResult(interp, zResult, nResult);
653590
Tcl_Release((ClientData)tclInterp);
654
- rc = notifyPreOrPostEval(1, interp, ctx, argc, argv, argl,
655
- getTh1ReturnCode(rc));
656591
return rc;
657592
}
658593
659594
/*
660595
** TH1 command: tclIsSafe
@@ -767,10 +702,11 @@
767702
int objc,
768703
Tcl_Obj *const objv[]
769704
){
770705
Th_Interp *th1Interp;
771706
int nArg;
707
+ Tcl_Size szArg;
772708
const char *arg;
773709
int rc;
774710
775711
if( objc!=2 ){
776712
Tcl_WrongNumArgs(interp, 1, objv, "arg");
@@ -779,14 +715,15 @@
779715
th1Interp = (Th_Interp *)clientData;
780716
if( !th1Interp ){
781717
Tcl_AppendResult(interp, "invalid TH1 interpreter", NULL);
782718
return TCL_ERROR;
783719
}
784
- arg = Tcl_GetStringFromObj(objv[1], &nArg);
720
+ arg = Tcl_GetStringFromObj(objv[1], &szArg);
721
+ nArg = (int)szArg;
785722
rc = Th_Eval(th1Interp, 0, arg, nArg);
786723
arg = Th_GetResult(th1Interp, &nArg);
787
- Tcl_SetObjResult(interp, Tcl_NewStringObj(arg, nArg));
724
+ Tcl_SetObjResult(interp, Tcl_NewStringObj(arg, TH1_LEN(nArg)));
788725
return getTclReturnCode(rc);
789726
}
790727
791728
/*
792729
** Tcl command: th1Expr arg
@@ -800,10 +737,11 @@
800737
int objc,
801738
Tcl_Obj *const objv[]
802739
){
803740
Th_Interp *th1Interp;
804741
int nArg;
742
+ Tcl_Size szArg;
805743
const char *arg;
806744
int rc;
807745
808746
if( objc!=2 ){
809747
Tcl_WrongNumArgs(interp, 1, objv, "arg");
@@ -812,14 +750,14 @@
812750
th1Interp = (Th_Interp *)clientData;
813751
if( !th1Interp ){
814752
Tcl_AppendResult(interp, "invalid TH1 interpreter", NULL);
815753
return TCL_ERROR;
816754
}
817
- arg = Tcl_GetStringFromObj(objv[1], &nArg);
818
- rc = Th_Expr(th1Interp, arg, nArg);
755
+ arg = Tcl_GetStringFromObj(objv[1], &szArg);
756
+ rc = Th_Expr(th1Interp, arg, (int)szArg);
819757
arg = Th_GetResult(th1Interp, &nArg);
820
- Tcl_SetObjResult(interp, Tcl_NewStringObj(arg, nArg));
758
+ Tcl_SetObjResult(interp, Tcl_NewStringObj(arg, TH1_LEN(nArg)));
821759
return getTclReturnCode(rc);
822760
}
823761
824762
/*
825763
** Array of Tcl integration commands. Used when adding or removing the Tcl
826764
--- src/th_tcl.c
+++ src/th_tcl.c
@@ -41,16 +41,16 @@
41 #define USE_ARGV_TO_OBJV() \
42 int objc; \
43 Tcl_Obj **objv; \
44 int obji;
45
46 #define COPY_ARGV_TO_OBJV() \
47 objc = argc-1; \
48 objv = (Tcl_Obj **)ckalloc((unsigned)(objc * sizeof(Tcl_Obj *))); \
49 for(obji=1; obji<argc; obji++){ \
50 objv[obji-1] = Tcl_NewStringObj(argv[obji], argl[obji]); \
51 Tcl_IncrRefCount(objv[obji-1]); \
52 }
53
54 #define FREE_ARGV_TO_OBJV() \
55 for(obji=1; obji<argc; obji++){ \
56 Tcl_DecrRefCount(objv[obji-1]); \
@@ -183,11 +183,11 @@
183 ** the only Tcl API functions that MUST be called prior to being able to call
184 ** Tcl_InitStubs (i.e. because it requires a Tcl interpreter). For complete
185 ** cleanup if the Tcl stubs initialization fails somehow, the Tcl_DeleteInterp
186 ** and Tcl_Finalize function types are also required.
187 */
188 typedef void (tcl_FindExecutableProc) (const char *);
189 typedef Tcl_Interp *(tcl_CreateInterpProc) (void);
190 typedef void (tcl_DeleteInterpProc) (Tcl_Interp *);
191 typedef void (tcl_FinalizeProc) (void);
192
193 /*
@@ -321,27 +321,10 @@
321 ** by the caller. This must be declared here because quite a few functions in
322 ** this file need to use it before it can be defined.
323 */
324 static int createTclInterp(Th_Interp *interp, void *pContext);
325
326 /*
327 ** Returns the TH1 return code corresponding to the specified Tcl
328 ** return code.
329 */
330 static int getTh1ReturnCode(
331 int rc /* The Tcl return code value to convert. */
332 ){
333 switch( rc ){
334 case /*0*/ TCL_OK: return /*0*/ TH_OK;
335 case /*1*/ TCL_ERROR: return /*1*/ TH_ERROR;
336 case /*2*/ TCL_RETURN: return /*3*/ TH_RETURN;
337 case /*3*/ TCL_BREAK: return /*2*/ TH_BREAK;
338 case /*4*/ TCL_CONTINUE: return /*4*/ TH_CONTINUE;
339 default /*?*/: return /*?*/ rc;
340 }
341 }
342
343 /*
344 ** Returns the Tcl return code corresponding to the specified TH1
345 ** return code.
346 */
347 static int getTclReturnCode(
@@ -387,10 +370,12 @@
387 static char *getTclResult(
388 Tcl_Interp *pInterp,
389 int *pN
390 ){
391 Tcl_Obj *resultPtr;
 
 
392
393 if( !pInterp ){ /* This should not happen. */
394 if( pN ) *pN = 0;
395 return 0;
396 }
@@ -397,11 +382,13 @@
397 resultPtr = Tcl_GetObjResult(pInterp);
398 if( !resultPtr ){ /* This should not happen either? */
399 if( pN ) *pN = 0;
400 return 0;
401 }
402 return Tcl_GetStringFromObj(resultPtr, pN);
 
 
403 }
404
405 /*
406 ** Tcl context information used by TH1. This structure definition has been
407 ** copied from and should be kept in sync with the one in "main.c".
@@ -416,48 +403,12 @@
416 tcl_FinalizeProc *xFinalize; /* Tcl_Finalize() pointer. */
417 Tcl_Interp *interp; /* The on-demand created Tcl interpreter. */
418 int useObjProc; /* Non-zero if an objProc can be called directly. */
419 int useTip285; /* Non-zero if TIP #285 is available. */
420 const char *setup; /* The optional Tcl setup script. */
421 tcl_NotifyProc *xPreEval; /* Optional, called before Tcl_Eval*(). */
422 void *pPreContext; /* Optional, provided to xPreEval(). */
423 tcl_NotifyProc *xPostEval; /* Optional, called after Tcl_Eval*(). */
424 void *pPostContext; /* Optional, provided to xPostEval(). */
425 };
426
427 /*
428 ** This function calls the configured xPreEval or xPostEval functions, if any.
429 ** May have arbitrary side-effects. This function returns the result of the
430 ** called notification function or the value of the rc argument if there is no
431 ** notification function configured.
432 */
433 static int notifyPreOrPostEval(
434 int bIsPost,
435 Th_Interp *interp,
436 void *ctx,
437 int argc,
438 const char **argv,
439 int *argl,
440 int rc
441 ){
442 struct TclContext *tclContext = (struct TclContext *)ctx;
443 tcl_NotifyProc *xNotifyProc;
444
445 if( !tclContext ){
446 Th_ErrorMessage(interp,
447 "invalid Tcl context", (const char *)"", 0);
448 return TH_ERROR;
449 }
450 xNotifyProc = bIsPost ? tclContext->xPostEval : tclContext->xPreEval;
451 if( xNotifyProc ){
452 rc = xNotifyProc(bIsPost ?
453 tclContext->pPostContext : tclContext->pPreContext,
454 interp, ctx, argc, argv, argl, rc);
455 }
456 return rc;
457 }
458
459 /*
460 ** TH1 command: tclEval arg ?arg ...?
461 **
462 ** Evaluates the Tcl script and returns its result verbatim. If a Tcl script
463 ** error is generated, it will be transformed into a TH1 script error. The
@@ -485,17 +436,13 @@
485 tclInterp = GET_CTX_TCL_INTERP(ctx);
486 if( !tclInterp || Tcl_InterpDeleted(tclInterp) ){
487 Th_ErrorMessage(interp, "invalid Tcl interpreter", (const char *)"", 0);
488 return TH_ERROR;
489 }
490 rc = notifyPreOrPostEval(0, interp, ctx, argc, argv, argl, rc);
491 if( rc!=TH_OK ){
492 return rc;
493 }
494 Tcl_Preserve((ClientData)tclInterp);
495 if( argc==2 ){
496 objPtr = Tcl_NewStringObj(argv[1], argl[1]);
497 Tcl_IncrRefCount(objPtr);
498 rc = Tcl_EvalObjEx(tclInterp, objPtr, 0);
499 Tcl_DecrRefCount(objPtr); objPtr = 0;
500 }else{
501 USE_ARGV_TO_OBJV();
@@ -507,12 +454,10 @@
507 FREE_ARGV_TO_OBJV();
508 }
509 zResult = getTclResult(tclInterp, &nResult);
510 Th_SetResult(interp, zResult, nResult);
511 Tcl_Release((ClientData)tclInterp);
512 rc = notifyPreOrPostEval(1, interp, ctx, argc, argv, argl,
513 getTh1ReturnCode(rc));
514 return rc;
515 }
516
517 /*
518 ** TH1 command: tclExpr arg ?arg ...?
@@ -545,17 +490,13 @@
545 tclInterp = GET_CTX_TCL_INTERP(ctx);
546 if( !tclInterp || Tcl_InterpDeleted(tclInterp) ){
547 Th_ErrorMessage(interp, "invalid Tcl interpreter", (const char *)"", 0);
548 return TH_ERROR;
549 }
550 rc = notifyPreOrPostEval(0, interp, ctx, argc, argv, argl, rc);
551 if( rc!=TH_OK ){
552 return rc;
553 }
554 Tcl_Preserve((ClientData)tclInterp);
555 if( argc==2 ){
556 objPtr = Tcl_NewStringObj(argv[1], argl[1]);
557 Tcl_IncrRefCount(objPtr);
558 rc = Tcl_ExprObj(tclInterp, objPtr, &resultObjPtr);
559 Tcl_DecrRefCount(objPtr); objPtr = 0;
560 }else{
561 USE_ARGV_TO_OBJV();
@@ -565,21 +506,21 @@
565 rc = Tcl_ExprObj(tclInterp, objPtr, &resultObjPtr);
566 Tcl_DecrRefCount(objPtr); objPtr = 0;
567 FREE_ARGV_TO_OBJV();
568 }
569 if( rc==TCL_OK ){
570 zResult = Tcl_GetStringFromObj(resultObjPtr, &nResult);
 
 
571 }else{
572 zResult = getTclResult(tclInterp, &nResult);
573 }
574 Th_SetResult(interp, zResult, nResult);
575 if( rc==TCL_OK ){
576 Tcl_DecrRefCount(resultObjPtr); resultObjPtr = 0;
577 }
578 Tcl_Release((ClientData)tclInterp);
579 rc = notifyPreOrPostEval(1, interp, ctx, argc, argv, argl,
580 getTh1ReturnCode(rc));
581 return rc;
582 }
583
584 /*
585 ** TH1 command: tclInvoke command ?arg ...?
@@ -610,20 +551,16 @@
610 tclInterp = GET_CTX_TCL_INTERP(ctx);
611 if( !tclInterp || Tcl_InterpDeleted(tclInterp) ){
612 Th_ErrorMessage(interp, "invalid Tcl interpreter", (const char *)"", 0);
613 return TH_ERROR;
614 }
615 rc = notifyPreOrPostEval(0, interp, ctx, argc, argv, argl, rc);
616 if( rc!=TH_OK ){
617 return rc;
618 }
619 Tcl_Preserve((ClientData)tclInterp);
620 #if !defined(USE_TCL_EVALOBJV) || !USE_TCL_EVALOBJV
621 if( GET_CTX_TCL_USEOBJPROC(ctx) ){
622 Tcl_Command command;
623 Tcl_CmdInfo cmdInfo;
624 Tcl_Obj *objPtr = Tcl_NewStringObj(argv[1], argl[1]);
625 Tcl_IncrRefCount(objPtr);
626 command = Tcl_GetCommandFromObj(tclInterp, objPtr);
627 if( !command || Tcl_GetCommandInfoFromToken(command, &cmdInfo)==0 ){
628 Th_ErrorMessage(interp, "Tcl command not found:", argv[1], argl[1]);
629 Tcl_DecrRefCount(objPtr); objPtr = 0;
@@ -649,12 +586,10 @@
649 FREE_ARGV_TO_OBJV();
650 }
651 zResult = getTclResult(tclInterp, &nResult);
652 Th_SetResult(interp, zResult, nResult);
653 Tcl_Release((ClientData)tclInterp);
654 rc = notifyPreOrPostEval(1, interp, ctx, argc, argv, argl,
655 getTh1ReturnCode(rc));
656 return rc;
657 }
658
659 /*
660 ** TH1 command: tclIsSafe
@@ -767,10 +702,11 @@
767 int objc,
768 Tcl_Obj *const objv[]
769 ){
770 Th_Interp *th1Interp;
771 int nArg;
 
772 const char *arg;
773 int rc;
774
775 if( objc!=2 ){
776 Tcl_WrongNumArgs(interp, 1, objv, "arg");
@@ -779,14 +715,15 @@
779 th1Interp = (Th_Interp *)clientData;
780 if( !th1Interp ){
781 Tcl_AppendResult(interp, "invalid TH1 interpreter", NULL);
782 return TCL_ERROR;
783 }
784 arg = Tcl_GetStringFromObj(objv[1], &nArg);
 
785 rc = Th_Eval(th1Interp, 0, arg, nArg);
786 arg = Th_GetResult(th1Interp, &nArg);
787 Tcl_SetObjResult(interp, Tcl_NewStringObj(arg, nArg));
788 return getTclReturnCode(rc);
789 }
790
791 /*
792 ** Tcl command: th1Expr arg
@@ -800,10 +737,11 @@
800 int objc,
801 Tcl_Obj *const objv[]
802 ){
803 Th_Interp *th1Interp;
804 int nArg;
 
805 const char *arg;
806 int rc;
807
808 if( objc!=2 ){
809 Tcl_WrongNumArgs(interp, 1, objv, "arg");
@@ -812,14 +750,14 @@
812 th1Interp = (Th_Interp *)clientData;
813 if( !th1Interp ){
814 Tcl_AppendResult(interp, "invalid TH1 interpreter", NULL);
815 return TCL_ERROR;
816 }
817 arg = Tcl_GetStringFromObj(objv[1], &nArg);
818 rc = Th_Expr(th1Interp, arg, nArg);
819 arg = Th_GetResult(th1Interp, &nArg);
820 Tcl_SetObjResult(interp, Tcl_NewStringObj(arg, nArg));
821 return getTclReturnCode(rc);
822 }
823
824 /*
825 ** Array of Tcl integration commands. Used when adding or removing the Tcl
826
--- src/th_tcl.c
+++ src/th_tcl.c
@@ -41,16 +41,16 @@
41 #define USE_ARGV_TO_OBJV() \
42 int objc; \
43 Tcl_Obj **objv; \
44 int obji;
45
46 #define COPY_ARGV_TO_OBJV() \
47 objc = argc-1; \
48 objv = (Tcl_Obj **)ckalloc((unsigned)(objc * sizeof(Tcl_Obj *))); \
49 for(obji=1; obji<argc; obji++){ \
50 objv[obji-1] = Tcl_NewStringObj(argv[obji], TH1_LEN(argl[obji])); \
51 Tcl_IncrRefCount(objv[obji-1]); \
52 }
53
54 #define FREE_ARGV_TO_OBJV() \
55 for(obji=1; obji<argc; obji++){ \
56 Tcl_DecrRefCount(objv[obji-1]); \
@@ -183,11 +183,11 @@
183 ** the only Tcl API functions that MUST be called prior to being able to call
184 ** Tcl_InitStubs (i.e. because it requires a Tcl interpreter). For complete
185 ** cleanup if the Tcl stubs initialization fails somehow, the Tcl_DeleteInterp
186 ** and Tcl_Finalize function types are also required.
187 */
188 typedef const char *(tcl_FindExecutableProc) (const char *);
189 typedef Tcl_Interp *(tcl_CreateInterpProc) (void);
190 typedef void (tcl_DeleteInterpProc) (Tcl_Interp *);
191 typedef void (tcl_FinalizeProc) (void);
192
193 /*
@@ -321,27 +321,10 @@
321 ** by the caller. This must be declared here because quite a few functions in
322 ** this file need to use it before it can be defined.
323 */
324 static int createTclInterp(Th_Interp *interp, void *pContext);
325
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
326 /*
327 ** Returns the Tcl return code corresponding to the specified TH1
328 ** return code.
329 */
330 static int getTclReturnCode(
@@ -387,10 +370,12 @@
370 static char *getTclResult(
371 Tcl_Interp *pInterp,
372 int *pN
373 ){
374 Tcl_Obj *resultPtr;
375 Tcl_Size n;
376 char *zRes;
377
378 if( !pInterp ){ /* This should not happen. */
379 if( pN ) *pN = 0;
380 return 0;
381 }
@@ -397,11 +382,13 @@
382 resultPtr = Tcl_GetObjResult(pInterp);
383 if( !resultPtr ){ /* This should not happen either? */
384 if( pN ) *pN = 0;
385 return 0;
386 }
387 zRes = Tcl_GetStringFromObj(resultPtr, &n);
388 *pN = (int)n;
389 return zRes;
390 }
391
392 /*
393 ** Tcl context information used by TH1. This structure definition has been
394 ** copied from and should be kept in sync with the one in "main.c".
@@ -416,48 +403,12 @@
403 tcl_FinalizeProc *xFinalize; /* Tcl_Finalize() pointer. */
404 Tcl_Interp *interp; /* The on-demand created Tcl interpreter. */
405 int useObjProc; /* Non-zero if an objProc can be called directly. */
406 int useTip285; /* Non-zero if TIP #285 is available. */
407 const char *setup; /* The optional Tcl setup script. */
 
 
 
 
408 };
409
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
410 /*
411 ** TH1 command: tclEval arg ?arg ...?
412 **
413 ** Evaluates the Tcl script and returns its result verbatim. If a Tcl script
414 ** error is generated, it will be transformed into a TH1 script error. The
@@ -485,17 +436,13 @@
436 tclInterp = GET_CTX_TCL_INTERP(ctx);
437 if( !tclInterp || Tcl_InterpDeleted(tclInterp) ){
438 Th_ErrorMessage(interp, "invalid Tcl interpreter", (const char *)"", 0);
439 return TH_ERROR;
440 }
 
 
 
 
441 Tcl_Preserve((ClientData)tclInterp);
442 if( argc==2 ){
443 objPtr = Tcl_NewStringObj(argv[1], TH1_LEN(argl[1]));
444 Tcl_IncrRefCount(objPtr);
445 rc = Tcl_EvalObjEx(tclInterp, objPtr, 0);
446 Tcl_DecrRefCount(objPtr); objPtr = 0;
447 }else{
448 USE_ARGV_TO_OBJV();
@@ -507,12 +454,10 @@
454 FREE_ARGV_TO_OBJV();
455 }
456 zResult = getTclResult(tclInterp, &nResult);
457 Th_SetResult(interp, zResult, nResult);
458 Tcl_Release((ClientData)tclInterp);
 
 
459 return rc;
460 }
461
462 /*
463 ** TH1 command: tclExpr arg ?arg ...?
@@ -545,17 +490,13 @@
490 tclInterp = GET_CTX_TCL_INTERP(ctx);
491 if( !tclInterp || Tcl_InterpDeleted(tclInterp) ){
492 Th_ErrorMessage(interp, "invalid Tcl interpreter", (const char *)"", 0);
493 return TH_ERROR;
494 }
 
 
 
 
495 Tcl_Preserve((ClientData)tclInterp);
496 if( argc==2 ){
497 objPtr = Tcl_NewStringObj(argv[1], TH1_LEN(argl[1]));
498 Tcl_IncrRefCount(objPtr);
499 rc = Tcl_ExprObj(tclInterp, objPtr, &resultObjPtr);
500 Tcl_DecrRefCount(objPtr); objPtr = 0;
501 }else{
502 USE_ARGV_TO_OBJV();
@@ -565,21 +506,21 @@
506 rc = Tcl_ExprObj(tclInterp, objPtr, &resultObjPtr);
507 Tcl_DecrRefCount(objPtr); objPtr = 0;
508 FREE_ARGV_TO_OBJV();
509 }
510 if( rc==TCL_OK ){
511 Tcl_Size szResult = 0;
512 zResult = Tcl_GetStringFromObj(resultObjPtr, &szResult);
513 nResult = (int)szResult;
514 }else{
515 zResult = getTclResult(tclInterp, &nResult);
516 }
517 Th_SetResult(interp, zResult, (int)nResult);
518 if( rc==TCL_OK ){
519 Tcl_DecrRefCount(resultObjPtr); resultObjPtr = 0;
520 }
521 Tcl_Release((ClientData)tclInterp);
 
 
522 return rc;
523 }
524
525 /*
526 ** TH1 command: tclInvoke command ?arg ...?
@@ -610,20 +551,16 @@
551 tclInterp = GET_CTX_TCL_INTERP(ctx);
552 if( !tclInterp || Tcl_InterpDeleted(tclInterp) ){
553 Th_ErrorMessage(interp, "invalid Tcl interpreter", (const char *)"", 0);
554 return TH_ERROR;
555 }
 
 
 
 
556 Tcl_Preserve((ClientData)tclInterp);
557 #if !defined(USE_TCL_EVALOBJV) || !USE_TCL_EVALOBJV
558 if( GET_CTX_TCL_USEOBJPROC(ctx) ){
559 Tcl_Command command;
560 Tcl_CmdInfo cmdInfo;
561 Tcl_Obj *objPtr = Tcl_NewStringObj(argv[1], TH1_LEN(argl[1]));
562 Tcl_IncrRefCount(objPtr);
563 command = Tcl_GetCommandFromObj(tclInterp, objPtr);
564 if( !command || Tcl_GetCommandInfoFromToken(command, &cmdInfo)==0 ){
565 Th_ErrorMessage(interp, "Tcl command not found:", argv[1], argl[1]);
566 Tcl_DecrRefCount(objPtr); objPtr = 0;
@@ -649,12 +586,10 @@
586 FREE_ARGV_TO_OBJV();
587 }
588 zResult = getTclResult(tclInterp, &nResult);
589 Th_SetResult(interp, zResult, nResult);
590 Tcl_Release((ClientData)tclInterp);
 
 
591 return rc;
592 }
593
594 /*
595 ** TH1 command: tclIsSafe
@@ -767,10 +702,11 @@
702 int objc,
703 Tcl_Obj *const objv[]
704 ){
705 Th_Interp *th1Interp;
706 int nArg;
707 Tcl_Size szArg;
708 const char *arg;
709 int rc;
710
711 if( objc!=2 ){
712 Tcl_WrongNumArgs(interp, 1, objv, "arg");
@@ -779,14 +715,15 @@
715 th1Interp = (Th_Interp *)clientData;
716 if( !th1Interp ){
717 Tcl_AppendResult(interp, "invalid TH1 interpreter", NULL);
718 return TCL_ERROR;
719 }
720 arg = Tcl_GetStringFromObj(objv[1], &szArg);
721 nArg = (int)szArg;
722 rc = Th_Eval(th1Interp, 0, arg, nArg);
723 arg = Th_GetResult(th1Interp, &nArg);
724 Tcl_SetObjResult(interp, Tcl_NewStringObj(arg, TH1_LEN(nArg)));
725 return getTclReturnCode(rc);
726 }
727
728 /*
729 ** Tcl command: th1Expr arg
@@ -800,10 +737,11 @@
737 int objc,
738 Tcl_Obj *const objv[]
739 ){
740 Th_Interp *th1Interp;
741 int nArg;
742 Tcl_Size szArg;
743 const char *arg;
744 int rc;
745
746 if( objc!=2 ){
747 Tcl_WrongNumArgs(interp, 1, objv, "arg");
@@ -812,14 +750,14 @@
750 th1Interp = (Th_Interp *)clientData;
751 if( !th1Interp ){
752 Tcl_AppendResult(interp, "invalid TH1 interpreter", NULL);
753 return TCL_ERROR;
754 }
755 arg = Tcl_GetStringFromObj(objv[1], &szArg);
756 rc = Th_Expr(th1Interp, arg, (int)szArg);
757 arg = Th_GetResult(th1Interp, &nArg);
758 Tcl_SetObjResult(interp, Tcl_NewStringObj(arg, TH1_LEN(nArg)));
759 return getTclReturnCode(rc);
760 }
761
762 /*
763 ** Array of Tcl integration commands. Used when adding or removing the Tcl
764
+49 -22
--- src/timeline.c
+++ src/timeline.c
@@ -221,28 +221,28 @@
221221
vid = db_lget_int("checkout", 0);
222222
}
223223
zPrevDate[0] = 0;
224224
mxWikiLen = db_get_int("timeline-max-comment", 0);
225225
dateFormat = db_get_int("timeline-date-format", 0);
226
- /*
227
- ** SETTING: timeline-truncate-at-blank boolean default=off
228
- **
229
- ** If enabled, check-in comments displayed on the timeline are truncated
230
- ** at the first blank line of the comment text. The comment text after
231
- ** the first blank line is only seen in the /info or similar pages that
232
- ** show details about the check-in.
233
- */
226
+/*
227
+** SETTING: timeline-truncate-at-blank boolean default=off
228
+**
229
+** If enabled, check-in comments displayed on the timeline are truncated
230
+** at the first blank line of the comment text. The comment text after
231
+** the first blank line is only seen in the /info or similar pages that
232
+** show details about the check-in.
233
+*/
234234
bCommentGitStyle = db_get_int("timeline-truncate-at-blank", 0);
235
- /*
236
- ** SETTING: timeline-tslink-info boolean default=off
237
- **
238
- ** The hyperlink on the timestamp associated with each timeline entry,
239
- ** on the far left-hand side of the screen, normally targets another
240
- ** /timeline page that shows the entry in context. However, if this
241
- ** option is turned on, that hyperlink targets the /info page showing
242
- ** the details of the entry.
243
- */
235
+/*
236
+** SETTING: timeline-tslink-info boolean default=off
237
+**
238
+** The hyperlink on the timestamp associated with each timeline entry,
239
+** on the far left-hand side of the screen, normally targets another
240
+** /timeline page that shows the entry in context. However, if this
241
+** option is turned on, that hyperlink targets the /info page showing
242
+** the details of the entry.
243
+*/
244244
bTimestampLinksToInfo = db_get_boolean("timeline-tslink-info", 0);
245245
if( (tmFlags & TIMELINE_VIEWS)==0 ){
246246
tmFlags |= timeline_ss_cookie();
247247
}
248248
if( tmFlags & TIMELINE_COLUMNAR ){
@@ -402,10 +402,12 @@
402402
zDateLink = mprintf("<a>");
403403
}
404404
@ <td class="timelineTime">%z(zDateLink)%s(zTime)</a></td>
405405
@ <td class="timelineGraph">
406406
if( tmFlags & (TIMELINE_UCOLOR|TIMELINE_DELTA|TIMELINE_NOCOLOR) ){
407
+ /* Don't use the requested background color. Use the background color
408
+ ** override from query parameters instead. */
407409
if( tmFlags & TIMELINE_UCOLOR ){
408410
zBgClr = zUser ? user_color(zUser) : 0;
409411
}else if( tmFlags & TIMELINE_NOCOLOR ){
410412
zBgClr = 0;
411413
}else if( zType[0]=='c' ){
@@ -420,16 +422,21 @@
420422
}else{
421423
zBgClr = hash_color("f"); /* delta manifest */
422424
}
423425
db_reset(&qdelta);
424426
}
427
+ }else{
428
+ /* Make sure the user-specified background color is reasonable */
429
+ zBgClr = reasonable_bg_color(zBgClr, 0);
425430
}
426431
if( zType[0]=='c'
427432
&& (pGraph || zBgClr==0 || (tmFlags & (TIMELINE_BRCOLOR|TIMELINE_DELTA))!=0)
428433
){
429434
zBr = branch_of_rid(rid);
430435
if( zBgClr==0 || (tmFlags & TIMELINE_BRCOLOR)!=0 ){
436
+ /* If no background color is specified, use a color based on the
437
+ ** branch name */
431438
if( tmFlags & (TIMELINE_DELTA|TIMELINE_NOCOLOR) ){
432439
}else if( zBr==0 || strcmp(zBr,"trunk")==0 ){
433440
zBgClr = 0;
434441
}else{
435442
zBgClr = hash_color(zBr);
@@ -591,10 +598,19 @@
591598
drawDetailEllipsis = 0;
592599
}else{
593600
cgi_printf("%W",blob_str(&comment));
594601
}
595602
}
603
+
604
+ if( zType[0]=='c' && strcmp(zUuid, MANIFEST_UUID)==0 ){
605
+ /* This will only ever happen when Fossil is drawing a timeline for
606
+ ** its own self-host repository. If the timeline shows the specific
607
+ ** check-in corresponding to the current executable, then tag that
608
+ ** check-in with "This is me!". */
609
+ @ <b>&larr; This is me!</b>
610
+ }
611
+
596612
@ </span>
597613
blob_reset(&comment);
598614
599615
/* Generate extra information and hyperlinks to follow the comment.
600616
** Example: "(check-in: [abcdefg], user: drh, tags: trunk)"
@@ -1872,11 +1888,11 @@
18721888
if( zTagName ){
18731889
zType = "ci";
18741890
if( matchStyle==MS_EXACT ){
18751891
/* For exact maching, inhibit links to the selected tag. */
18761892
zThisTag = zTagName;
1877
- Th_Store("current_checkin", zTagName);
1893
+ Th_StoreUnsafe("current_checkin", zTagName);
18781894
}
18791895
18801896
/* Display a checkbox to enable/disable display of related check-ins. */
18811897
if( advancedMenu ){
18821898
style_submenu_checkbox("rel", "Related", 0, 0);
@@ -3733,15 +3749,22 @@
37333749
}
37343750
}
37353751
37363752
if( mode==TIMELINE_MODE_NONE ) mode = TIMELINE_MODE_BEFORE;
37373753
blob_zero(&sql);
3754
+ if( mode==TIMELINE_MODE_AFTER ){
3755
+ /* Extra outer select to get older rows in reverse order */
3756
+ blob_append(&sql, "SELECT *\nFROM (", -1);
3757
+ }
37383758
blob_append(&sql, timeline_query_for_tty(), -1);
37393759
blob_append_sql(&sql, "\n AND event.mtime %s %s",
37403760
( mode==TIMELINE_MODE_BEFORE ||
37413761
mode==TIMELINE_MODE_PARENTS ) ? "<=" : ">=", zDate /*safe-for-%s*/
37423762
);
3763
+ if( zType && (zType[0]!='a') ){
3764
+ blob_append_sql(&sql, "\n AND event.type=%Q ", zType);
3765
+ }
37433766
37443767
/* When zFilePattern is specified, compute complete ancestry;
37453768
* limit later at print_timeline() */
37463769
if( mode==TIMELINE_MODE_CHILDREN || mode==TIMELINE_MODE_PARENTS ){
37473770
db_multi_exec("CREATE TEMP TABLE ok(rid INTEGER PRIMARY KEY)");
@@ -3750,13 +3773,10 @@
37503773
}else{
37513774
compute_ancestors(objid, (zFilePattern ? 0 : n), 0, 0);
37523775
}
37533776
blob_append_sql(&sql, "\n AND blob.rid IN ok");
37543777
}
3755
- if( zType && (zType[0]!='a') ){
3756
- blob_append_sql(&sql, "\n AND event.type=%Q ", zType);
3757
- }
37583778
if( zFilePattern ){
37593779
blob_append(&sql,
37603780
"\n AND EXISTS(SELECT 1 FROM mlink\n"
37613781
" WHERE mlink.mid=event.objid\n"
37623782
" AND mlink.fnid IN ", -1);
@@ -3794,11 +3814,18 @@
37943814
" WHERE tx.value='%q'\n"
37953815
")\n" /* No merge closures */
37963816
" AND (tagxref.value IS NULL OR tagxref.value='%q')",
37973817
zBr, zBr, zBr, TAG_BRANCH, zBr, zBr);
37983818
}
3799
- blob_append_sql(&sql, "\nORDER BY event.mtime DESC");
3819
+
3820
+ if( mode==TIMELINE_MODE_AFTER ){
3821
+ /* Complete the above outer select. */
3822
+ blob_append_sql(&sql,
3823
+ "\nORDER BY event.mtime LIMIT abs(%d)) t ORDER BY t.mDateTime DESC;", n);
3824
+ }else{
3825
+ blob_append_sql(&sql, "\nORDER BY event.mtime DESC");
3826
+ }
38003827
if( iOffset>0 ){
38013828
/* Don't handle LIMIT here, otherwise print_timeline()
38023829
* will not determine the end-marker correctly! */
38033830
blob_append_sql(&sql, "\n LIMIT -1 OFFSET %d", iOffset);
38043831
}
38053832
--- src/timeline.c
+++ src/timeline.c
@@ -221,28 +221,28 @@
221 vid = db_lget_int("checkout", 0);
222 }
223 zPrevDate[0] = 0;
224 mxWikiLen = db_get_int("timeline-max-comment", 0);
225 dateFormat = db_get_int("timeline-date-format", 0);
226 /*
227 ** SETTING: timeline-truncate-at-blank boolean default=off
228 **
229 ** If enabled, check-in comments displayed on the timeline are truncated
230 ** at the first blank line of the comment text. The comment text after
231 ** the first blank line is only seen in the /info or similar pages that
232 ** show details about the check-in.
233 */
234 bCommentGitStyle = db_get_int("timeline-truncate-at-blank", 0);
235 /*
236 ** SETTING: timeline-tslink-info boolean default=off
237 **
238 ** The hyperlink on the timestamp associated with each timeline entry,
239 ** on the far left-hand side of the screen, normally targets another
240 ** /timeline page that shows the entry in context. However, if this
241 ** option is turned on, that hyperlink targets the /info page showing
242 ** the details of the entry.
243 */
244 bTimestampLinksToInfo = db_get_boolean("timeline-tslink-info", 0);
245 if( (tmFlags & TIMELINE_VIEWS)==0 ){
246 tmFlags |= timeline_ss_cookie();
247 }
248 if( tmFlags & TIMELINE_COLUMNAR ){
@@ -402,10 +402,12 @@
402 zDateLink = mprintf("<a>");
403 }
404 @ <td class="timelineTime">%z(zDateLink)%s(zTime)</a></td>
405 @ <td class="timelineGraph">
406 if( tmFlags & (TIMELINE_UCOLOR|TIMELINE_DELTA|TIMELINE_NOCOLOR) ){
 
 
407 if( tmFlags & TIMELINE_UCOLOR ){
408 zBgClr = zUser ? user_color(zUser) : 0;
409 }else if( tmFlags & TIMELINE_NOCOLOR ){
410 zBgClr = 0;
411 }else if( zType[0]=='c' ){
@@ -420,16 +422,21 @@
420 }else{
421 zBgClr = hash_color("f"); /* delta manifest */
422 }
423 db_reset(&qdelta);
424 }
 
 
 
425 }
426 if( zType[0]=='c'
427 && (pGraph || zBgClr==0 || (tmFlags & (TIMELINE_BRCOLOR|TIMELINE_DELTA))!=0)
428 ){
429 zBr = branch_of_rid(rid);
430 if( zBgClr==0 || (tmFlags & TIMELINE_BRCOLOR)!=0 ){
 
 
431 if( tmFlags & (TIMELINE_DELTA|TIMELINE_NOCOLOR) ){
432 }else if( zBr==0 || strcmp(zBr,"trunk")==0 ){
433 zBgClr = 0;
434 }else{
435 zBgClr = hash_color(zBr);
@@ -591,10 +598,19 @@
591 drawDetailEllipsis = 0;
592 }else{
593 cgi_printf("%W",blob_str(&comment));
594 }
595 }
 
 
 
 
 
 
 
 
 
596 @ </span>
597 blob_reset(&comment);
598
599 /* Generate extra information and hyperlinks to follow the comment.
600 ** Example: "(check-in: [abcdefg], user: drh, tags: trunk)"
@@ -1872,11 +1888,11 @@
1872 if( zTagName ){
1873 zType = "ci";
1874 if( matchStyle==MS_EXACT ){
1875 /* For exact maching, inhibit links to the selected tag. */
1876 zThisTag = zTagName;
1877 Th_Store("current_checkin", zTagName);
1878 }
1879
1880 /* Display a checkbox to enable/disable display of related check-ins. */
1881 if( advancedMenu ){
1882 style_submenu_checkbox("rel", "Related", 0, 0);
@@ -3733,15 +3749,22 @@
3733 }
3734 }
3735
3736 if( mode==TIMELINE_MODE_NONE ) mode = TIMELINE_MODE_BEFORE;
3737 blob_zero(&sql);
 
 
 
 
3738 blob_append(&sql, timeline_query_for_tty(), -1);
3739 blob_append_sql(&sql, "\n AND event.mtime %s %s",
3740 ( mode==TIMELINE_MODE_BEFORE ||
3741 mode==TIMELINE_MODE_PARENTS ) ? "<=" : ">=", zDate /*safe-for-%s*/
3742 );
 
 
 
3743
3744 /* When zFilePattern is specified, compute complete ancestry;
3745 * limit later at print_timeline() */
3746 if( mode==TIMELINE_MODE_CHILDREN || mode==TIMELINE_MODE_PARENTS ){
3747 db_multi_exec("CREATE TEMP TABLE ok(rid INTEGER PRIMARY KEY)");
@@ -3750,13 +3773,10 @@
3750 }else{
3751 compute_ancestors(objid, (zFilePattern ? 0 : n), 0, 0);
3752 }
3753 blob_append_sql(&sql, "\n AND blob.rid IN ok");
3754 }
3755 if( zType && (zType[0]!='a') ){
3756 blob_append_sql(&sql, "\n AND event.type=%Q ", zType);
3757 }
3758 if( zFilePattern ){
3759 blob_append(&sql,
3760 "\n AND EXISTS(SELECT 1 FROM mlink\n"
3761 " WHERE mlink.mid=event.objid\n"
3762 " AND mlink.fnid IN ", -1);
@@ -3794,11 +3814,18 @@
3794 " WHERE tx.value='%q'\n"
3795 ")\n" /* No merge closures */
3796 " AND (tagxref.value IS NULL OR tagxref.value='%q')",
3797 zBr, zBr, zBr, TAG_BRANCH, zBr, zBr);
3798 }
3799 blob_append_sql(&sql, "\nORDER BY event.mtime DESC");
 
 
 
 
 
 
 
3800 if( iOffset>0 ){
3801 /* Don't handle LIMIT here, otherwise print_timeline()
3802 * will not determine the end-marker correctly! */
3803 blob_append_sql(&sql, "\n LIMIT -1 OFFSET %d", iOffset);
3804 }
3805
--- src/timeline.c
+++ src/timeline.c
@@ -221,28 +221,28 @@
221 vid = db_lget_int("checkout", 0);
222 }
223 zPrevDate[0] = 0;
224 mxWikiLen = db_get_int("timeline-max-comment", 0);
225 dateFormat = db_get_int("timeline-date-format", 0);
226 /*
227 ** SETTING: timeline-truncate-at-blank boolean default=off
228 **
229 ** If enabled, check-in comments displayed on the timeline are truncated
230 ** at the first blank line of the comment text. The comment text after
231 ** the first blank line is only seen in the /info or similar pages that
232 ** show details about the check-in.
233 */
234 bCommentGitStyle = db_get_int("timeline-truncate-at-blank", 0);
235 /*
236 ** SETTING: timeline-tslink-info boolean default=off
237 **
238 ** The hyperlink on the timestamp associated with each timeline entry,
239 ** on the far left-hand side of the screen, normally targets another
240 ** /timeline page that shows the entry in context. However, if this
241 ** option is turned on, that hyperlink targets the /info page showing
242 ** the details of the entry.
243 */
244 bTimestampLinksToInfo = db_get_boolean("timeline-tslink-info", 0);
245 if( (tmFlags & TIMELINE_VIEWS)==0 ){
246 tmFlags |= timeline_ss_cookie();
247 }
248 if( tmFlags & TIMELINE_COLUMNAR ){
@@ -402,10 +402,12 @@
402 zDateLink = mprintf("<a>");
403 }
404 @ <td class="timelineTime">%z(zDateLink)%s(zTime)</a></td>
405 @ <td class="timelineGraph">
406 if( tmFlags & (TIMELINE_UCOLOR|TIMELINE_DELTA|TIMELINE_NOCOLOR) ){
407 /* Don't use the requested background color. Use the background color
408 ** override from query parameters instead. */
409 if( tmFlags & TIMELINE_UCOLOR ){
410 zBgClr = zUser ? user_color(zUser) : 0;
411 }else if( tmFlags & TIMELINE_NOCOLOR ){
412 zBgClr = 0;
413 }else if( zType[0]=='c' ){
@@ -420,16 +422,21 @@
422 }else{
423 zBgClr = hash_color("f"); /* delta manifest */
424 }
425 db_reset(&qdelta);
426 }
427 }else{
428 /* Make sure the user-specified background color is reasonable */
429 zBgClr = reasonable_bg_color(zBgClr, 0);
430 }
431 if( zType[0]=='c'
432 && (pGraph || zBgClr==0 || (tmFlags & (TIMELINE_BRCOLOR|TIMELINE_DELTA))!=0)
433 ){
434 zBr = branch_of_rid(rid);
435 if( zBgClr==0 || (tmFlags & TIMELINE_BRCOLOR)!=0 ){
436 /* If no background color is specified, use a color based on the
437 ** branch name */
438 if( tmFlags & (TIMELINE_DELTA|TIMELINE_NOCOLOR) ){
439 }else if( zBr==0 || strcmp(zBr,"trunk")==0 ){
440 zBgClr = 0;
441 }else{
442 zBgClr = hash_color(zBr);
@@ -591,10 +598,19 @@
598 drawDetailEllipsis = 0;
599 }else{
600 cgi_printf("%W",blob_str(&comment));
601 }
602 }
603
604 if( zType[0]=='c' && strcmp(zUuid, MANIFEST_UUID)==0 ){
605 /* This will only ever happen when Fossil is drawing a timeline for
606 ** its own self-host repository. If the timeline shows the specific
607 ** check-in corresponding to the current executable, then tag that
608 ** check-in with "This is me!". */
609 @ <b>&larr; This is me!</b>
610 }
611
612 @ </span>
613 blob_reset(&comment);
614
615 /* Generate extra information and hyperlinks to follow the comment.
616 ** Example: "(check-in: [abcdefg], user: drh, tags: trunk)"
@@ -1872,11 +1888,11 @@
1888 if( zTagName ){
1889 zType = "ci";
1890 if( matchStyle==MS_EXACT ){
1891 /* For exact maching, inhibit links to the selected tag. */
1892 zThisTag = zTagName;
1893 Th_StoreUnsafe("current_checkin", zTagName);
1894 }
1895
1896 /* Display a checkbox to enable/disable display of related check-ins. */
1897 if( advancedMenu ){
1898 style_submenu_checkbox("rel", "Related", 0, 0);
@@ -3733,15 +3749,22 @@
3749 }
3750 }
3751
3752 if( mode==TIMELINE_MODE_NONE ) mode = TIMELINE_MODE_BEFORE;
3753 blob_zero(&sql);
3754 if( mode==TIMELINE_MODE_AFTER ){
3755 /* Extra outer select to get older rows in reverse order */
3756 blob_append(&sql, "SELECT *\nFROM (", -1);
3757 }
3758 blob_append(&sql, timeline_query_for_tty(), -1);
3759 blob_append_sql(&sql, "\n AND event.mtime %s %s",
3760 ( mode==TIMELINE_MODE_BEFORE ||
3761 mode==TIMELINE_MODE_PARENTS ) ? "<=" : ">=", zDate /*safe-for-%s*/
3762 );
3763 if( zType && (zType[0]!='a') ){
3764 blob_append_sql(&sql, "\n AND event.type=%Q ", zType);
3765 }
3766
3767 /* When zFilePattern is specified, compute complete ancestry;
3768 * limit later at print_timeline() */
3769 if( mode==TIMELINE_MODE_CHILDREN || mode==TIMELINE_MODE_PARENTS ){
3770 db_multi_exec("CREATE TEMP TABLE ok(rid INTEGER PRIMARY KEY)");
@@ -3750,13 +3773,10 @@
3773 }else{
3774 compute_ancestors(objid, (zFilePattern ? 0 : n), 0, 0);
3775 }
3776 blob_append_sql(&sql, "\n AND blob.rid IN ok");
3777 }
 
 
 
3778 if( zFilePattern ){
3779 blob_append(&sql,
3780 "\n AND EXISTS(SELECT 1 FROM mlink\n"
3781 " WHERE mlink.mid=event.objid\n"
3782 " AND mlink.fnid IN ", -1);
@@ -3794,11 +3814,18 @@
3814 " WHERE tx.value='%q'\n"
3815 ")\n" /* No merge closures */
3816 " AND (tagxref.value IS NULL OR tagxref.value='%q')",
3817 zBr, zBr, zBr, TAG_BRANCH, zBr, zBr);
3818 }
3819
3820 if( mode==TIMELINE_MODE_AFTER ){
3821 /* Complete the above outer select. */
3822 blob_append_sql(&sql,
3823 "\nORDER BY event.mtime LIMIT abs(%d)) t ORDER BY t.mDateTime DESC;", n);
3824 }else{
3825 blob_append_sql(&sql, "\nORDER BY event.mtime DESC");
3826 }
3827 if( iOffset>0 ){
3828 /* Don't handle LIMIT here, otherwise print_timeline()
3829 * will not determine the end-marker correctly! */
3830 blob_append_sql(&sql, "\n LIMIT -1 OFFSET %d", iOffset);
3831 }
3832
+27 -15
--- src/tkt.c
+++ src/tkt.c
@@ -188,14 +188,19 @@
188188
*/
189189
static void initializeVariablesFromDb(void){
190190
const char *zName;
191191
Stmt q;
192192
int i, n, size, j;
193
+ const char *zCTimeColumn = haveTicketCTime ? "tkt_ctime" : "tkt_mtime";
193194
194195
zName = PD("name","-none-");
195
- db_prepare(&q, "SELECT datetime(tkt_mtime,toLocal()) AS tkt_datetime, *"
196
+ db_prepare(&q, "SELECT datetime(tkt_mtime,toLocal()) AS tkt_datetime, "
197
+ "datetime(%s,toLocal()) AS tkt_datetime_creation, "
198
+ "julianday('now') - tkt_mtime, "
199
+ "julianday('now') - %s, *"
196200
" FROM ticket WHERE tkt_uuid GLOB '%q*'",
201
+ zCTimeColumn/*safe-for-%s*/, zCTimeColumn/*safe-for-%s*/,
197202
zName);
198203
if( db_step(&q)==SQLITE_ROW ){
199204
n = db_column_count(&q);
200205
for(i=0; i<n; i++){
201206
const char *zVal = db_column_text(&q, i);
@@ -207,19 +212,22 @@
207212
zVal = zRevealed = db_reveal(zVal);
208213
}
209214
if( (j = fieldId(zName))>=0 ){
210215
aField[j].zValue = mprintf("%s", zVal);
211216
}else if( memcmp(zName, "tkt_", 4)==0 && Th_Fetch(zName, &size)==0 ){
217
+ /* TICKET table columns that begin with "tkt_" are always safe */
212218
Th_Store(zName, zVal);
213219
}
214220
free(zRevealed);
215221
}
222
+ Th_Store("tkt_mage", human_readable_age(db_column_double(&q, 2)));
223
+ Th_Store("tkt_cage", human_readable_age(db_column_double(&q, 3)));
216224
}
217225
db_finalize(&q);
218226
for(i=0; i<nField; i++){
219227
if( Th_Fetch(aField[i].zName, &size)==0 ){
220
- Th_Store(aField[i].zName, aField[i].zValue);
228
+ Th_StoreUnsafe(aField[i].zName, aField[i].zValue);
221229
}
222230
}
223231
}
224232
225233
/*
@@ -228,11 +236,11 @@
228236
static void initializeVariablesFromCGI(void){
229237
int i;
230238
const char *z;
231239
232240
for(i=0; (z = cgi_parameter_name(i))!=0; i++){
233
- Th_Store(z, P(z));
241
+ Th_StoreUnsafe(z, P(z));
234242
}
235243
}
236244
237245
/*
238246
** Information about a single J-card
@@ -769,11 +777,12 @@
769777
}
770778
zFullName = db_text(0,
771779
"SELECT tkt_uuid FROM ticket"
772780
" WHERE tkt_uuid GLOB '%q*'", zUuid);
773781
if( g.perm.WrWiki && g.perm.WrTkt ){
774
- style_submenu_element("Edit Description", "%R/wikiedit?name=ticket/%T", zFullName);
782
+ style_submenu_element("Edit Description",
783
+ "%R/wikiedit?name=ticket/%T", zFullName);
775784
}
776785
if( g.thTrace ) Th_Trace("BEGIN_TKTVIEW<br>\n", -1);
777786
ticket_init();
778787
initializeVariablesFromCGI();
779788
getAllTicketFields();
@@ -812,15 +821,15 @@
812821
if( argc!=3 ){
813822
return Th_WrongNumArgs(interp, "append_field FIELD STRING");
814823
}
815824
if( g.thTrace ){
816825
Th_Trace("append_field %#h {%#h}<br>\n",
817
- argl[1], argv[1], argl[2], argv[2]);
826
+ TH1_LEN(argl[1]), argv[1], TH1_LEN(argl[2]), argv[2]);
818827
}
819828
for(idx=0; idx<nField; idx++){
820
- if( memcmp(aField[idx].zName, argv[1], argl[1])==0
821
- && aField[idx].zName[argl[1]]==0 ){
829
+ if( memcmp(aField[idx].zName, argv[1], TH1_LEN(argl[1]))==0
830
+ && aField[idx].zName[TH1_LEN(argl[1])]==0 ){
822831
break;
823832
}
824833
}
825834
if( idx>=nField ){
826835
Th_ErrorMessage(g.interp, "no such TICKET column: ", argv[1], argl[1]);
@@ -932,10 +941,11 @@
932941
const char *zValue;
933942
int nValue;
934943
if( aField[i].zAppend ) continue;
935944
zValue = Th_Fetch(aField[i].zName, &nValue);
936945
if( zValue ){
946
+ nValue = TH1_LEN(nValue);
937947
while( nValue>0 && fossil_isspace(zValue[nValue-1]) ){ nValue--; }
938948
if( ((aField[i].mUsed & USEDBY_TICKETCHNG)!=0 && nValue>0)
939949
|| memcmp(zValue, aField[i].zValue, nValue)!=0
940950
||(int)strlen(aField[i].zValue)!=nValue
941951
){
@@ -1026,32 +1036,34 @@
10261036
form_begin(0, "%R/%s", g.zPath);
10271037
if( P("date_override") && g.perm.Setup ){
10281038
@ <input type="hidden" name="date_override" value="%h(P("date_override"))">
10291039
}
10301040
zScript = ticket_newpage_code();
1041
+ Th_Store("private_contact", "");
10311042
if( g.zLogin && g.zLogin[0] ){
1032
- int nEmail = 0;
1033
- (void)Th_MaybeGetVar(g.interp, "private_contact", &nEmail);
1034
- uid = nEmail>0
1035
- ? 0 : db_int(0, "SELECT uid FROM user WHERE login=%Q", g.zLogin);
1043
+ uid = db_int(0, "SELECT uid FROM user WHERE login=%Q", g.zLogin);
10361044
if( uid ){
10371045
char * zEmail =
10381046
db_text(0, "SELECT find_emailaddr(info) FROM user WHERE uid=%d",
10391047
uid);
10401048
if( zEmail ){
1041
- Th_Store("private_contact", zEmail);
1049
+ Th_StoreUnsafe("private_contact", zEmail);
10421050
fossil_free(zEmail);
10431051
}
10441052
}
10451053
}
1046
- Th_Store("login", login_name());
1054
+ Th_StoreUnsafe("login", login_name());
10471055
Th_Store("date", db_text(0, "SELECT datetime('now')"));
10481056
Th_CreateCommand(g.interp, "submit_ticket", submitTicketCmd,
10491057
(void*)&zNewUuid, 0);
10501058
if( g.thTrace ) Th_Trace("BEGIN_TKTNEW_SCRIPT<br>\n", -1);
10511059
if( Th_Render(zScript)==TH_RETURN && !g.thTrace && zNewUuid ){
1052
- cgi_redirect(mprintf("%R/tktview/%s", zNewUuid));
1060
+ if( P("submitandnew") ){
1061
+ cgi_redirect(mprintf("%R/tktnew/%s", zNewUuid));
1062
+ }else{
1063
+ cgi_redirect(mprintf("%R/tktview/%s", zNewUuid));
1064
+ }
10531065
return;
10541066
}
10551067
captcha_generate(0);
10561068
@ </form>
10571069
if( g.thTrace ) Th_Trace("END_TKTVIEW<br>\n", -1);
@@ -1112,11 +1124,11 @@
11121124
initializeVariablesFromDb();
11131125
if( g.zPath[0]=='d' ) showAllFields();
11141126
form_begin(0, "%R/%s", g.zPath);
11151127
@ <input type="hidden" name="name" value="%s(zName)">
11161128
zScript = ticket_editpage_code();
1117
- Th_Store("login", login_name());
1129
+ Th_StoreUnsafe("login", login_name());
11181130
Th_Store("date", db_text(0, "SELECT datetime('now')"));
11191131
Th_CreateCommand(g.interp, "append_field", appendRemarkCmd, 0, 0);
11201132
Th_CreateCommand(g.interp, "submit_ticket", submitTicketCmd, (void*)&zName,0);
11211133
if( g.thTrace ) Th_Trace("BEGIN_TKTEDIT_SCRIPT<br>\n", -1);
11221134
if( Th_Render(zScript)==TH_RETURN && !g.thTrace && zName ){
11231135
--- src/tkt.c
+++ src/tkt.c
@@ -188,14 +188,19 @@
188 */
189 static void initializeVariablesFromDb(void){
190 const char *zName;
191 Stmt q;
192 int i, n, size, j;
 
193
194 zName = PD("name","-none-");
195 db_prepare(&q, "SELECT datetime(tkt_mtime,toLocal()) AS tkt_datetime, *"
 
 
 
196 " FROM ticket WHERE tkt_uuid GLOB '%q*'",
 
197 zName);
198 if( db_step(&q)==SQLITE_ROW ){
199 n = db_column_count(&q);
200 for(i=0; i<n; i++){
201 const char *zVal = db_column_text(&q, i);
@@ -207,19 +212,22 @@
207 zVal = zRevealed = db_reveal(zVal);
208 }
209 if( (j = fieldId(zName))>=0 ){
210 aField[j].zValue = mprintf("%s", zVal);
211 }else if( memcmp(zName, "tkt_", 4)==0 && Th_Fetch(zName, &size)==0 ){
 
212 Th_Store(zName, zVal);
213 }
214 free(zRevealed);
215 }
 
 
216 }
217 db_finalize(&q);
218 for(i=0; i<nField; i++){
219 if( Th_Fetch(aField[i].zName, &size)==0 ){
220 Th_Store(aField[i].zName, aField[i].zValue);
221 }
222 }
223 }
224
225 /*
@@ -228,11 +236,11 @@
228 static void initializeVariablesFromCGI(void){
229 int i;
230 const char *z;
231
232 for(i=0; (z = cgi_parameter_name(i))!=0; i++){
233 Th_Store(z, P(z));
234 }
235 }
236
237 /*
238 ** Information about a single J-card
@@ -769,11 +777,12 @@
769 }
770 zFullName = db_text(0,
771 "SELECT tkt_uuid FROM ticket"
772 " WHERE tkt_uuid GLOB '%q*'", zUuid);
773 if( g.perm.WrWiki && g.perm.WrTkt ){
774 style_submenu_element("Edit Description", "%R/wikiedit?name=ticket/%T", zFullName);
 
775 }
776 if( g.thTrace ) Th_Trace("BEGIN_TKTVIEW<br>\n", -1);
777 ticket_init();
778 initializeVariablesFromCGI();
779 getAllTicketFields();
@@ -812,15 +821,15 @@
812 if( argc!=3 ){
813 return Th_WrongNumArgs(interp, "append_field FIELD STRING");
814 }
815 if( g.thTrace ){
816 Th_Trace("append_field %#h {%#h}<br>\n",
817 argl[1], argv[1], argl[2], argv[2]);
818 }
819 for(idx=0; idx<nField; idx++){
820 if( memcmp(aField[idx].zName, argv[1], argl[1])==0
821 && aField[idx].zName[argl[1]]==0 ){
822 break;
823 }
824 }
825 if( idx>=nField ){
826 Th_ErrorMessage(g.interp, "no such TICKET column: ", argv[1], argl[1]);
@@ -932,10 +941,11 @@
932 const char *zValue;
933 int nValue;
934 if( aField[i].zAppend ) continue;
935 zValue = Th_Fetch(aField[i].zName, &nValue);
936 if( zValue ){
 
937 while( nValue>0 && fossil_isspace(zValue[nValue-1]) ){ nValue--; }
938 if( ((aField[i].mUsed & USEDBY_TICKETCHNG)!=0 && nValue>0)
939 || memcmp(zValue, aField[i].zValue, nValue)!=0
940 ||(int)strlen(aField[i].zValue)!=nValue
941 ){
@@ -1026,32 +1036,34 @@
1026 form_begin(0, "%R/%s", g.zPath);
1027 if( P("date_override") && g.perm.Setup ){
1028 @ <input type="hidden" name="date_override" value="%h(P("date_override"))">
1029 }
1030 zScript = ticket_newpage_code();
 
1031 if( g.zLogin && g.zLogin[0] ){
1032 int nEmail = 0;
1033 (void)Th_MaybeGetVar(g.interp, "private_contact", &nEmail);
1034 uid = nEmail>0
1035 ? 0 : db_int(0, "SELECT uid FROM user WHERE login=%Q", g.zLogin);
1036 if( uid ){
1037 char * zEmail =
1038 db_text(0, "SELECT find_emailaddr(info) FROM user WHERE uid=%d",
1039 uid);
1040 if( zEmail ){
1041 Th_Store("private_contact", zEmail);
1042 fossil_free(zEmail);
1043 }
1044 }
1045 }
1046 Th_Store("login", login_name());
1047 Th_Store("date", db_text(0, "SELECT datetime('now')"));
1048 Th_CreateCommand(g.interp, "submit_ticket", submitTicketCmd,
1049 (void*)&zNewUuid, 0);
1050 if( g.thTrace ) Th_Trace("BEGIN_TKTNEW_SCRIPT<br>\n", -1);
1051 if( Th_Render(zScript)==TH_RETURN && !g.thTrace && zNewUuid ){
1052 cgi_redirect(mprintf("%R/tktview/%s", zNewUuid));
 
 
 
 
1053 return;
1054 }
1055 captcha_generate(0);
1056 @ </form>
1057 if( g.thTrace ) Th_Trace("END_TKTVIEW<br>\n", -1);
@@ -1112,11 +1124,11 @@
1112 initializeVariablesFromDb();
1113 if( g.zPath[0]=='d' ) showAllFields();
1114 form_begin(0, "%R/%s", g.zPath);
1115 @ <input type="hidden" name="name" value="%s(zName)">
1116 zScript = ticket_editpage_code();
1117 Th_Store("login", login_name());
1118 Th_Store("date", db_text(0, "SELECT datetime('now')"));
1119 Th_CreateCommand(g.interp, "append_field", appendRemarkCmd, 0, 0);
1120 Th_CreateCommand(g.interp, "submit_ticket", submitTicketCmd, (void*)&zName,0);
1121 if( g.thTrace ) Th_Trace("BEGIN_TKTEDIT_SCRIPT<br>\n", -1);
1122 if( Th_Render(zScript)==TH_RETURN && !g.thTrace && zName ){
1123
--- src/tkt.c
+++ src/tkt.c
@@ -188,14 +188,19 @@
188 */
189 static void initializeVariablesFromDb(void){
190 const char *zName;
191 Stmt q;
192 int i, n, size, j;
193 const char *zCTimeColumn = haveTicketCTime ? "tkt_ctime" : "tkt_mtime";
194
195 zName = PD("name","-none-");
196 db_prepare(&q, "SELECT datetime(tkt_mtime,toLocal()) AS tkt_datetime, "
197 "datetime(%s,toLocal()) AS tkt_datetime_creation, "
198 "julianday('now') - tkt_mtime, "
199 "julianday('now') - %s, *"
200 " FROM ticket WHERE tkt_uuid GLOB '%q*'",
201 zCTimeColumn/*safe-for-%s*/, zCTimeColumn/*safe-for-%s*/,
202 zName);
203 if( db_step(&q)==SQLITE_ROW ){
204 n = db_column_count(&q);
205 for(i=0; i<n; i++){
206 const char *zVal = db_column_text(&q, i);
@@ -207,19 +212,22 @@
212 zVal = zRevealed = db_reveal(zVal);
213 }
214 if( (j = fieldId(zName))>=0 ){
215 aField[j].zValue = mprintf("%s", zVal);
216 }else if( memcmp(zName, "tkt_", 4)==0 && Th_Fetch(zName, &size)==0 ){
217 /* TICKET table columns that begin with "tkt_" are always safe */
218 Th_Store(zName, zVal);
219 }
220 free(zRevealed);
221 }
222 Th_Store("tkt_mage", human_readable_age(db_column_double(&q, 2)));
223 Th_Store("tkt_cage", human_readable_age(db_column_double(&q, 3)));
224 }
225 db_finalize(&q);
226 for(i=0; i<nField; i++){
227 if( Th_Fetch(aField[i].zName, &size)==0 ){
228 Th_StoreUnsafe(aField[i].zName, aField[i].zValue);
229 }
230 }
231 }
232
233 /*
@@ -228,11 +236,11 @@
236 static void initializeVariablesFromCGI(void){
237 int i;
238 const char *z;
239
240 for(i=0; (z = cgi_parameter_name(i))!=0; i++){
241 Th_StoreUnsafe(z, P(z));
242 }
243 }
244
245 /*
246 ** Information about a single J-card
@@ -769,11 +777,12 @@
777 }
778 zFullName = db_text(0,
779 "SELECT tkt_uuid FROM ticket"
780 " WHERE tkt_uuid GLOB '%q*'", zUuid);
781 if( g.perm.WrWiki && g.perm.WrTkt ){
782 style_submenu_element("Edit Description",
783 "%R/wikiedit?name=ticket/%T", zFullName);
784 }
785 if( g.thTrace ) Th_Trace("BEGIN_TKTVIEW<br>\n", -1);
786 ticket_init();
787 initializeVariablesFromCGI();
788 getAllTicketFields();
@@ -812,15 +821,15 @@
821 if( argc!=3 ){
822 return Th_WrongNumArgs(interp, "append_field FIELD STRING");
823 }
824 if( g.thTrace ){
825 Th_Trace("append_field %#h {%#h}<br>\n",
826 TH1_LEN(argl[1]), argv[1], TH1_LEN(argl[2]), argv[2]);
827 }
828 for(idx=0; idx<nField; idx++){
829 if( memcmp(aField[idx].zName, argv[1], TH1_LEN(argl[1]))==0
830 && aField[idx].zName[TH1_LEN(argl[1])]==0 ){
831 break;
832 }
833 }
834 if( idx>=nField ){
835 Th_ErrorMessage(g.interp, "no such TICKET column: ", argv[1], argl[1]);
@@ -932,10 +941,11 @@
941 const char *zValue;
942 int nValue;
943 if( aField[i].zAppend ) continue;
944 zValue = Th_Fetch(aField[i].zName, &nValue);
945 if( zValue ){
946 nValue = TH1_LEN(nValue);
947 while( nValue>0 && fossil_isspace(zValue[nValue-1]) ){ nValue--; }
948 if( ((aField[i].mUsed & USEDBY_TICKETCHNG)!=0 && nValue>0)
949 || memcmp(zValue, aField[i].zValue, nValue)!=0
950 ||(int)strlen(aField[i].zValue)!=nValue
951 ){
@@ -1026,32 +1036,34 @@
1036 form_begin(0, "%R/%s", g.zPath);
1037 if( P("date_override") && g.perm.Setup ){
1038 @ <input type="hidden" name="date_override" value="%h(P("date_override"))">
1039 }
1040 zScript = ticket_newpage_code();
1041 Th_Store("private_contact", "");
1042 if( g.zLogin && g.zLogin[0] ){
1043 uid = db_int(0, "SELECT uid FROM user WHERE login=%Q", g.zLogin);
 
 
 
1044 if( uid ){
1045 char * zEmail =
1046 db_text(0, "SELECT find_emailaddr(info) FROM user WHERE uid=%d",
1047 uid);
1048 if( zEmail ){
1049 Th_StoreUnsafe("private_contact", zEmail);
1050 fossil_free(zEmail);
1051 }
1052 }
1053 }
1054 Th_StoreUnsafe("login", login_name());
1055 Th_Store("date", db_text(0, "SELECT datetime('now')"));
1056 Th_CreateCommand(g.interp, "submit_ticket", submitTicketCmd,
1057 (void*)&zNewUuid, 0);
1058 if( g.thTrace ) Th_Trace("BEGIN_TKTNEW_SCRIPT<br>\n", -1);
1059 if( Th_Render(zScript)==TH_RETURN && !g.thTrace && zNewUuid ){
1060 if( P("submitandnew") ){
1061 cgi_redirect(mprintf("%R/tktnew/%s", zNewUuid));
1062 }else{
1063 cgi_redirect(mprintf("%R/tktview/%s", zNewUuid));
1064 }
1065 return;
1066 }
1067 captcha_generate(0);
1068 @ </form>
1069 if( g.thTrace ) Th_Trace("END_TKTVIEW<br>\n", -1);
@@ -1112,11 +1124,11 @@
1124 initializeVariablesFromDb();
1125 if( g.zPath[0]=='d' ) showAllFields();
1126 form_begin(0, "%R/%s", g.zPath);
1127 @ <input type="hidden" name="name" value="%s(zName)">
1128 zScript = ticket_editpage_code();
1129 Th_StoreUnsafe("login", login_name());
1130 Th_Store("date", db_text(0, "SELECT datetime('now')"));
1131 Th_CreateCommand(g.interp, "append_field", appendRemarkCmd, 0, 0);
1132 Th_CreateCommand(g.interp, "submit_ticket", submitTicketCmd, (void*)&zName,0);
1133 if( g.thTrace ) Th_Trace("BEGIN_TKTEDIT_SCRIPT<br>\n", -1);
1134 if( Th_Render(zScript)==TH_RETURN && !g.thTrace && zName ){
1135
+131 -15
--- src/tktsetup.c
+++ src/tktsetup.c
@@ -125,11 +125,11 @@
125125
if( !g.perm.Setup ){
126126
login_needed(0);
127127
return;
128128
}
129129
style_set_current_feature("tktsetup");
130
- if( PB("setup") ){
130
+ if( P("setup") ){
131131
cgi_redirect("tktsetup");
132132
}
133133
isSubmit = P("submit")!=0;
134134
z = P("x");
135135
if( z==0 ){
@@ -164,10 +164,11 @@
164164
@ <hr>
165165
@ <h2>Default %s(zTitle)</h2>
166166
@ <blockquote><pre>
167167
@ %h(zDfltValue)
168168
@ </pre></blockquote>
169
+ style_submenu_element("Back", "%R/tktsetup");
169170
style_finish_page();
170171
}
171172
172173
/*
173174
** WEBPAGE: tktsetup_tab
@@ -301,11 +302,11 @@
301302
}
302303
303304
static const char zDefaultNew[] =
304305
@ <th1>
305306
@ if {![info exists mutype]} {set mutype Markdown}
306
-@ if {[info exists submit]} {
307
+@ if {[info exists submit] || [info exists submitandnew]} {
307308
@ set status Open
308309
@ if {$mutype eq "HTML"} {
309310
@ set mimetype "text/html"
310311
@ } elseif {$mutype eq "Wiki"} {
311312
@ set mimetype "text/x-fossil-wiki"
@@ -349,10 +350,28 @@
349350
@ <td align="left"><th1>combobox severity $severity_choices 1</th1></td>
350351
@ <td align="left">How debilitating is the problem? How badly does the problem
351352
@ affect the operation of the product?</td>
352353
@ </tr>
353354
@
355
+@ <th1>
356
+@ if {[capexpr {w}]} {
357
+@ html {<tr><td class="tktDspLabel">Priority:</td><td>}
358
+@ combobox priority $priority_choices 1
359
+@ html {
360
+@ <td align="left">How important is the affected functionality?</td>
361
+@ </td></tr>
362
+@ }
363
+@
364
+@ html {<tr><td class="tktDspLabel">Subsystem:</td><td>}
365
+@ combobox subsystem $subsystem_choices 1
366
+@ html {
367
+@ <td align="left">Which subsystem is affected?</td>
368
+@ </td></tr>
369
+@ }
370
+@ }
371
+@ </th1>
372
+@
354373
@ <tr>
355374
@ <td align="right">EMail:</td>
356375
@ <td align="left">
357376
@ <input name="private_contact" value="$<private_contact>" size="30">
358377
@ </td>
@@ -405,19 +424,27 @@
405424
@ <tr>
406425
@ <td><td align="left">
407426
@ <input type="submit" name="submit" value="Submit">
408427
@ </td>
409428
@ <td align="left">After filling in the information above, press this
410
-@ button to create the new ticket</td>
429
+@ button to create the new ticket.</td>
430
+@ </tr>
431
+@
432
+@ <tr>
433
+@ <td><td align="left">
434
+@ <input type="submit" name="submitandnew" value="Submit and New">
435
+@ </td>
436
+@ <td align="left">Create the new ticket and start another
437
+@ ticket form with the inputs.</td>
411438
@ </tr>
412439
@ <th1>enable_output 1</th1>
413440
@
414441
@ <tr>
415442
@ <td><td align="left">
416443
@ <input type="submit" name="cancel" value="Cancel">
417444
@ </td>
418
-@ <td>Abandon and forget this ticket</td>
445
+@ <td>Abandon and forget this ticket.</td>
419446
@ </tr>
420447
@ </table>
421448
;
422449
423450
/*
@@ -454,11 +481,11 @@
454481
@ <th1>
455482
@ if {[info exists tkt_uuid]} {
456483
@ html "<td class='tktDspValue' colspan='3'>"
457484
@ copybtn hash-tk 0 $tkt_uuid 2
458485
@ if {[hascap s]} {
459
-@ html " ($tkt_id)"
486
+@ puts " ($tkt_id)"
460487
@ }
461488
@ html "</td></tr>\n"
462489
@ } else {
463490
@ if {[hascap s]} {
464491
@ html "<td class='tktDspValue' colspan='3'>Deleted "
@@ -465,10 +492,14 @@
465492
@ html "(0)</td></tr>\n"
466493
@ } else {
467494
@ html "<td class='tktDspValue' colspan='3'>Deleted</td></tr>\n"
468495
@ }
469496
@ }
497
+@
498
+@ if {[capexpr {n}]} {
499
+@ submenu link "Copy Ticket" /tktnew/$tkt_uuid
500
+@ }
470501
@ </th1>
471502
@ <tr><td class="tktDspLabel">Title:</td>
472503
@ <td class="tktDspValue" colspan="3">
473504
@ $<title>
474505
@ </td></tr>
@@ -491,23 +522,63 @@
491522
@ $<resolution>
492523
@ </td></tr>
493524
@ <tr><td class="tktDspLabel">Last&nbsp;Modified:</td><td class="tktDspValue">
494525
@ <th1>
495526
@ if {[info exists tkt_datetime]} {
496
-@ html $tkt_datetime
527
+@ puts $tkt_datetime
528
+@ }
529
+@ if {[info exists tkt_mage]} {
530
+@ html "<br>[htmlize $tkt_mage] ago"
497531
@ }
498532
@ </th1>
499533
@ </td>
534
+@ <td class="tktDspLabel">Created:</td><td class="tktDspValue">
535
+@ <th1>
536
+@ if {[info exists tkt_datetime_creation]} {
537
+@ puts $tkt_datetime_creation
538
+@ }
539
+@ if {[info exists tkt_cage]} {
540
+@ html "<br>[htmlize $tkt_cage] ago"
541
+@ }
542
+@ </th1>
543
+@ </td></tr>
500544
@ <th1>enable_output [hascap e]</th1>
501
-@ <td class="tktDspLabel">Contact:</td><td class="tktDspValue">
545
+@ <tr>
546
+@ <td class="tktDspLabel">Contact:</td><td class="tktDspValue" colspan="3">
502547
@ $<private_contact>
503548
@ </td>
549
+@ </tr>
504550
@ <th1>enable_output 1</th1>
505
-@ </tr>
506551
@ <tr><td class="tktDspLabel">Version&nbsp;Found&nbsp;In:</td>
507552
@ <td colspan="3" valign="top" class="tktDspValue">
508
-@ $<foundin>
553
+@ <th1>
554
+@ set versionlink ""
555
+@ set urlfoundin [httpize $foundin]
556
+@ set tagpattern {^[-0-9A-Za-z_\\.]+$}
557
+@ if [regexp $tagpattern $foundin] {
558
+@ query {SELECT count(*) AS match FROM tag
559
+@ WHERE tagname=concat('sym-',$foundin)} {
560
+@ if {$match} {set versionlink "timeline?t=$urlfoundin"}
561
+@ }
562
+@ }
563
+@ set hashpattern {^[0-9a-f]+$}
564
+@ if [regexp $hashpattern $foundin] {
565
+@ set pattern $foundin*
566
+@ query {SELECT count(*) AS match FROM blob WHERE uuid GLOB $pattern} {
567
+@ if {$match} {set versionlink "info/$urlfoundin"}
568
+@ }
569
+@ }
570
+@ if {$versionlink eq ""} {
571
+@ puts $foundin
572
+@ } else {
573
+@ html "<a href=\""
574
+@ puts $versionlink
575
+@ html "\">"
576
+@ puts $foundin
577
+@ html "</a>"
578
+@ }
579
+@ </th1>
509580
@ </td></tr>
510581
@ </table>
511582
@
512583
@ <th1>
513584
@ wiki_assoc "ticket" $tkt_uuid
@@ -537,24 +608,25 @@
537608
@ FROM ticketchng
538609
@ WHERE tkt_id=$tkt_id AND length(icomment)>0} {
539610
@ if {$seenRow} {
540611
@ html "<hr>\n"
541612
@ } else {
542
-@ html "<tr><td class='tktDspLabel' style='text-align:left'>User Comments:</td></tr>\n"
613
+@ html "<tr><td class='tktDspLabel' style='text-align:left'>\n"
614
+@ html "User Comments:</td></tr>\n"
543615
@ html "<tr><td colspan='5' class='tktDspValue'>\n"
544616
@ set seenRow 1
545617
@ }
546618
@ html "<span class='tktDspCommenter'>"
547
-@ html "[htmlize $xlogin]"
619
+@ puts $xlogin
548620
@ if {$xlogin ne $xusername && [string length $xusername]>0} {
549
-@ html " (claiming to be [htmlize $xusername])"
621
+@ puts " (claiming to be $xusername)"
550622
@ }
551
-@ html " added on $xdate:"
623
+@ puts " added on $xdate:"
552624
@ html "</span>\n"
553625
@ if {$alwaysPlaintext || $xmimetype eq "text/plain"} {
554626
@ set r [randhex]
555
-@ if {$xmimetype ne "text/plain"} {html "([htmlize $xmimetype])\n"}
627
+@ if {$xmimetype ne "text/plain"} {puts "($xmimetype)\n"}
556628
@ wiki "<verbatim-$r>[string trimright $xcomment]</verbatim-$r>\n"
557629
@ } elseif {$xmimetype eq "text/x-fossil-wiki"} {
558630
@ wiki "<p>\n[string trimright $xcomment]\n</p>\n"
559631
@ } elseif {$xmimetype eq "text/x-markdown"} {
560632
@ html [lindex [markdown $xcomment] 1]
@@ -711,10 +783,52 @@
711783
@ <input type="submit" name="cancel" value="Cancel">
712784
@ </td>
713785
@ <td>Abandon this edit</td>
714786
@ </tr>
715787
@
788
+@ <th1>
789
+@ set seenRow 0
790
+@ set alwaysPlaintext [info exists plaintext]
791
+@ query {SELECT datetime(tkt_mtime) AS xdate, login AS xlogin,
792
+@ mimetype as xmimetype, icomment AS xcomment,
793
+@ username AS xusername
794
+@ FROM ticketchng
795
+@ WHERE tkt_id=$tkt_id AND length(icomment)>0} {
796
+@ if {$seenRow} {
797
+@ html "<hr>\n"
798
+@ } else {
799
+@ html "<tr><td colspan='2'><hr></td></tr>\n"
800
+@ html "<tr><td colspan='2' class='tktDspLabel' style='text-align:left'>\n"
801
+@ html "Previous User Comments:</td></tr>\n"
802
+@ html "<tr><td colspan='2' class='tktDspValue'>\n"
803
+@ set seenRow 1
804
+@ }
805
+@ html "<span class='tktDspCommenter'>"
806
+@ puts $xlogin
807
+@ if {$xlogin ne $xusername && [string length $xusername]>0} {
808
+@ puts " (claiming to be $xusername)"
809
+@ }
810
+@ puts " added on $xdate:"
811
+@ html "</span>\n"
812
+@ if {$alwaysPlaintext || $xmimetype eq "text/plain"} {
813
+@ set r [randhex]
814
+@ if {$xmimetype ne "text/plain"} {puts "($xmimetype)\n"}
815
+@ wiki "<verbatim-$r>[string trimright $xcomment]</verbatim-$r>\n"
816
+@ } elseif {$xmimetype eq "text/x-fossil-wiki"} {
817
+@ wiki "<p>\n[string trimright $xcomment]\n</p>\n"
818
+@ } elseif {$xmimetype eq "text/x-markdown"} {
819
+@ html [lindex [markdown $xcomment] 1]
820
+@ } elseif {$xmimetype eq "text/html"} {
821
+@ wiki "<p><nowiki>\n[string trimright $xcomment]\n</nowiki>\n"
822
+@ } else {
823
+@ set r [randhex]
824
+@ wiki "<verbatim-$r links>[string trimright $xcomment]</verbatim-$r>\n"
825
+@ }
826
+@ }
827
+@ if {$seenRow} {html "</td></tr>\n"}
828
+@ </th1>
829
+@
716830
@ </table>
717831
;
718832
719833
/*
720834
** Return the code used to generate the edit ticket page
@@ -809,11 +923,12 @@
809923
@ WHEN status='Fixed' THEN '#cfe8bd'
810924
@ WHEN status='Tested' THEN '#bde5d6'
811925
@ WHEN status='Deferred' THEN '#cacae5'
812926
@ ELSE '#c8c8c8' END AS 'bgcolor',
813927
@ substr(tkt_uuid,1,10) AS '#',
814
-@ datetime(tkt_mtime) AS 'mtime',
928
+@ datetime(tkt_ctime) AS 'created',
929
+@ datetime(tkt_mtime) AS 'modified',
815930
@ type,
816931
@ status,
817932
@ subsystem,
818933
@ title,
819934
@ comment AS '_comments'
@@ -943,8 +1058,9 @@
9431058
@ <input type="submit" name="submit" value="Apply Changes">
9441059
@ <input type="submit" name="setup" value="Cancel">
9451060
@ </p>
9461061
@ </div></form>
9471062
db_end_transaction(0);
1063
+ style_submenu_element("Back", "%R/tktsetup");
9481064
style_finish_page();
9491065
9501066
}
9511067
--- src/tktsetup.c
+++ src/tktsetup.c
@@ -125,11 +125,11 @@
125 if( !g.perm.Setup ){
126 login_needed(0);
127 return;
128 }
129 style_set_current_feature("tktsetup");
130 if( PB("setup") ){
131 cgi_redirect("tktsetup");
132 }
133 isSubmit = P("submit")!=0;
134 z = P("x");
135 if( z==0 ){
@@ -164,10 +164,11 @@
164 @ <hr>
165 @ <h2>Default %s(zTitle)</h2>
166 @ <blockquote><pre>
167 @ %h(zDfltValue)
168 @ </pre></blockquote>
 
169 style_finish_page();
170 }
171
172 /*
173 ** WEBPAGE: tktsetup_tab
@@ -301,11 +302,11 @@
301 }
302
303 static const char zDefaultNew[] =
304 @ <th1>
305 @ if {![info exists mutype]} {set mutype Markdown}
306 @ if {[info exists submit]} {
307 @ set status Open
308 @ if {$mutype eq "HTML"} {
309 @ set mimetype "text/html"
310 @ } elseif {$mutype eq "Wiki"} {
311 @ set mimetype "text/x-fossil-wiki"
@@ -349,10 +350,28 @@
349 @ <td align="left"><th1>combobox severity $severity_choices 1</th1></td>
350 @ <td align="left">How debilitating is the problem? How badly does the problem
351 @ affect the operation of the product?</td>
352 @ </tr>
353 @
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
354 @ <tr>
355 @ <td align="right">EMail:</td>
356 @ <td align="left">
357 @ <input name="private_contact" value="$<private_contact>" size="30">
358 @ </td>
@@ -405,19 +424,27 @@
405 @ <tr>
406 @ <td><td align="left">
407 @ <input type="submit" name="submit" value="Submit">
408 @ </td>
409 @ <td align="left">After filling in the information above, press this
410 @ button to create the new ticket</td>
 
 
 
 
 
 
 
 
411 @ </tr>
412 @ <th1>enable_output 1</th1>
413 @
414 @ <tr>
415 @ <td><td align="left">
416 @ <input type="submit" name="cancel" value="Cancel">
417 @ </td>
418 @ <td>Abandon and forget this ticket</td>
419 @ </tr>
420 @ </table>
421 ;
422
423 /*
@@ -454,11 +481,11 @@
454 @ <th1>
455 @ if {[info exists tkt_uuid]} {
456 @ html "<td class='tktDspValue' colspan='3'>"
457 @ copybtn hash-tk 0 $tkt_uuid 2
458 @ if {[hascap s]} {
459 @ html " ($tkt_id)"
460 @ }
461 @ html "</td></tr>\n"
462 @ } else {
463 @ if {[hascap s]} {
464 @ html "<td class='tktDspValue' colspan='3'>Deleted "
@@ -465,10 +492,14 @@
465 @ html "(0)</td></tr>\n"
466 @ } else {
467 @ html "<td class='tktDspValue' colspan='3'>Deleted</td></tr>\n"
468 @ }
469 @ }
 
 
 
 
470 @ </th1>
471 @ <tr><td class="tktDspLabel">Title:</td>
472 @ <td class="tktDspValue" colspan="3">
473 @ $<title>
474 @ </td></tr>
@@ -491,23 +522,63 @@
491 @ $<resolution>
492 @ </td></tr>
493 @ <tr><td class="tktDspLabel">Last&nbsp;Modified:</td><td class="tktDspValue">
494 @ <th1>
495 @ if {[info exists tkt_datetime]} {
496 @ html $tkt_datetime
 
 
 
497 @ }
498 @ </th1>
499 @ </td>
 
 
 
 
 
 
 
 
 
 
500 @ <th1>enable_output [hascap e]</th1>
501 @ <td class="tktDspLabel">Contact:</td><td class="tktDspValue">
 
502 @ $<private_contact>
503 @ </td>
 
504 @ <th1>enable_output 1</th1>
505 @ </tr>
506 @ <tr><td class="tktDspLabel">Version&nbsp;Found&nbsp;In:</td>
507 @ <td colspan="3" valign="top" class="tktDspValue">
508 @ $<foundin>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
509 @ </td></tr>
510 @ </table>
511 @
512 @ <th1>
513 @ wiki_assoc "ticket" $tkt_uuid
@@ -537,24 +608,25 @@
537 @ FROM ticketchng
538 @ WHERE tkt_id=$tkt_id AND length(icomment)>0} {
539 @ if {$seenRow} {
540 @ html "<hr>\n"
541 @ } else {
542 @ html "<tr><td class='tktDspLabel' style='text-align:left'>User Comments:</td></tr>\n"
 
543 @ html "<tr><td colspan='5' class='tktDspValue'>\n"
544 @ set seenRow 1
545 @ }
546 @ html "<span class='tktDspCommenter'>"
547 @ html "[htmlize $xlogin]"
548 @ if {$xlogin ne $xusername && [string length $xusername]>0} {
549 @ html " (claiming to be [htmlize $xusername])"
550 @ }
551 @ html " added on $xdate:"
552 @ html "</span>\n"
553 @ if {$alwaysPlaintext || $xmimetype eq "text/plain"} {
554 @ set r [randhex]
555 @ if {$xmimetype ne "text/plain"} {html "([htmlize $xmimetype])\n"}
556 @ wiki "<verbatim-$r>[string trimright $xcomment]</verbatim-$r>\n"
557 @ } elseif {$xmimetype eq "text/x-fossil-wiki"} {
558 @ wiki "<p>\n[string trimright $xcomment]\n</p>\n"
559 @ } elseif {$xmimetype eq "text/x-markdown"} {
560 @ html [lindex [markdown $xcomment] 1]
@@ -711,10 +783,52 @@
711 @ <input type="submit" name="cancel" value="Cancel">
712 @ </td>
713 @ <td>Abandon this edit</td>
714 @ </tr>
715 @
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
716 @ </table>
717 ;
718
719 /*
720 ** Return the code used to generate the edit ticket page
@@ -809,11 +923,12 @@
809 @ WHEN status='Fixed' THEN '#cfe8bd'
810 @ WHEN status='Tested' THEN '#bde5d6'
811 @ WHEN status='Deferred' THEN '#cacae5'
812 @ ELSE '#c8c8c8' END AS 'bgcolor',
813 @ substr(tkt_uuid,1,10) AS '#',
814 @ datetime(tkt_mtime) AS 'mtime',
 
815 @ type,
816 @ status,
817 @ subsystem,
818 @ title,
819 @ comment AS '_comments'
@@ -943,8 +1058,9 @@
943 @ <input type="submit" name="submit" value="Apply Changes">
944 @ <input type="submit" name="setup" value="Cancel">
945 @ </p>
946 @ </div></form>
947 db_end_transaction(0);
 
948 style_finish_page();
949
950 }
951
--- src/tktsetup.c
+++ src/tktsetup.c
@@ -125,11 +125,11 @@
125 if( !g.perm.Setup ){
126 login_needed(0);
127 return;
128 }
129 style_set_current_feature("tktsetup");
130 if( P("setup") ){
131 cgi_redirect("tktsetup");
132 }
133 isSubmit = P("submit")!=0;
134 z = P("x");
135 if( z==0 ){
@@ -164,10 +164,11 @@
164 @ <hr>
165 @ <h2>Default %s(zTitle)</h2>
166 @ <blockquote><pre>
167 @ %h(zDfltValue)
168 @ </pre></blockquote>
169 style_submenu_element("Back", "%R/tktsetup");
170 style_finish_page();
171 }
172
173 /*
174 ** WEBPAGE: tktsetup_tab
@@ -301,11 +302,11 @@
302 }
303
304 static const char zDefaultNew[] =
305 @ <th1>
306 @ if {![info exists mutype]} {set mutype Markdown}
307 @ if {[info exists submit] || [info exists submitandnew]} {
308 @ set status Open
309 @ if {$mutype eq "HTML"} {
310 @ set mimetype "text/html"
311 @ } elseif {$mutype eq "Wiki"} {
312 @ set mimetype "text/x-fossil-wiki"
@@ -349,10 +350,28 @@
350 @ <td align="left"><th1>combobox severity $severity_choices 1</th1></td>
351 @ <td align="left">How debilitating is the problem? How badly does the problem
352 @ affect the operation of the product?</td>
353 @ </tr>
354 @
355 @ <th1>
356 @ if {[capexpr {w}]} {
357 @ html {<tr><td class="tktDspLabel">Priority:</td><td>}
358 @ combobox priority $priority_choices 1
359 @ html {
360 @ <td align="left">How important is the affected functionality?</td>
361 @ </td></tr>
362 @ }
363 @
364 @ html {<tr><td class="tktDspLabel">Subsystem:</td><td>}
365 @ combobox subsystem $subsystem_choices 1
366 @ html {
367 @ <td align="left">Which subsystem is affected?</td>
368 @ </td></tr>
369 @ }
370 @ }
371 @ </th1>
372 @
373 @ <tr>
374 @ <td align="right">EMail:</td>
375 @ <td align="left">
376 @ <input name="private_contact" value="$<private_contact>" size="30">
377 @ </td>
@@ -405,19 +424,27 @@
424 @ <tr>
425 @ <td><td align="left">
426 @ <input type="submit" name="submit" value="Submit">
427 @ </td>
428 @ <td align="left">After filling in the information above, press this
429 @ button to create the new ticket.</td>
430 @ </tr>
431 @
432 @ <tr>
433 @ <td><td align="left">
434 @ <input type="submit" name="submitandnew" value="Submit and New">
435 @ </td>
436 @ <td align="left">Create the new ticket and start another
437 @ ticket form with the inputs.</td>
438 @ </tr>
439 @ <th1>enable_output 1</th1>
440 @
441 @ <tr>
442 @ <td><td align="left">
443 @ <input type="submit" name="cancel" value="Cancel">
444 @ </td>
445 @ <td>Abandon and forget this ticket.</td>
446 @ </tr>
447 @ </table>
448 ;
449
450 /*
@@ -454,11 +481,11 @@
481 @ <th1>
482 @ if {[info exists tkt_uuid]} {
483 @ html "<td class='tktDspValue' colspan='3'>"
484 @ copybtn hash-tk 0 $tkt_uuid 2
485 @ if {[hascap s]} {
486 @ puts " ($tkt_id)"
487 @ }
488 @ html "</td></tr>\n"
489 @ } else {
490 @ if {[hascap s]} {
491 @ html "<td class='tktDspValue' colspan='3'>Deleted "
@@ -465,10 +492,14 @@
492 @ html "(0)</td></tr>\n"
493 @ } else {
494 @ html "<td class='tktDspValue' colspan='3'>Deleted</td></tr>\n"
495 @ }
496 @ }
497 @
498 @ if {[capexpr {n}]} {
499 @ submenu link "Copy Ticket" /tktnew/$tkt_uuid
500 @ }
501 @ </th1>
502 @ <tr><td class="tktDspLabel">Title:</td>
503 @ <td class="tktDspValue" colspan="3">
504 @ $<title>
505 @ </td></tr>
@@ -491,23 +522,63 @@
522 @ $<resolution>
523 @ </td></tr>
524 @ <tr><td class="tktDspLabel">Last&nbsp;Modified:</td><td class="tktDspValue">
525 @ <th1>
526 @ if {[info exists tkt_datetime]} {
527 @ puts $tkt_datetime
528 @ }
529 @ if {[info exists tkt_mage]} {
530 @ html "<br>[htmlize $tkt_mage] ago"
531 @ }
532 @ </th1>
533 @ </td>
534 @ <td class="tktDspLabel">Created:</td><td class="tktDspValue">
535 @ <th1>
536 @ if {[info exists tkt_datetime_creation]} {
537 @ puts $tkt_datetime_creation
538 @ }
539 @ if {[info exists tkt_cage]} {
540 @ html "<br>[htmlize $tkt_cage] ago"
541 @ }
542 @ </th1>
543 @ </td></tr>
544 @ <th1>enable_output [hascap e]</th1>
545 @ <tr>
546 @ <td class="tktDspLabel">Contact:</td><td class="tktDspValue" colspan="3">
547 @ $<private_contact>
548 @ </td>
549 @ </tr>
550 @ <th1>enable_output 1</th1>
 
551 @ <tr><td class="tktDspLabel">Version&nbsp;Found&nbsp;In:</td>
552 @ <td colspan="3" valign="top" class="tktDspValue">
553 @ <th1>
554 @ set versionlink ""
555 @ set urlfoundin [httpize $foundin]
556 @ set tagpattern {^[-0-9A-Za-z_\\.]+$}
557 @ if [regexp $tagpattern $foundin] {
558 @ query {SELECT count(*) AS match FROM tag
559 @ WHERE tagname=concat('sym-',$foundin)} {
560 @ if {$match} {set versionlink "timeline?t=$urlfoundin"}
561 @ }
562 @ }
563 @ set hashpattern {^[0-9a-f]+$}
564 @ if [regexp $hashpattern $foundin] {
565 @ set pattern $foundin*
566 @ query {SELECT count(*) AS match FROM blob WHERE uuid GLOB $pattern} {
567 @ if {$match} {set versionlink "info/$urlfoundin"}
568 @ }
569 @ }
570 @ if {$versionlink eq ""} {
571 @ puts $foundin
572 @ } else {
573 @ html "<a href=\""
574 @ puts $versionlink
575 @ html "\">"
576 @ puts $foundin
577 @ html "</a>"
578 @ }
579 @ </th1>
580 @ </td></tr>
581 @ </table>
582 @
583 @ <th1>
584 @ wiki_assoc "ticket" $tkt_uuid
@@ -537,24 +608,25 @@
608 @ FROM ticketchng
609 @ WHERE tkt_id=$tkt_id AND length(icomment)>0} {
610 @ if {$seenRow} {
611 @ html "<hr>\n"
612 @ } else {
613 @ html "<tr><td class='tktDspLabel' style='text-align:left'>\n"
614 @ html "User Comments:</td></tr>\n"
615 @ html "<tr><td colspan='5' class='tktDspValue'>\n"
616 @ set seenRow 1
617 @ }
618 @ html "<span class='tktDspCommenter'>"
619 @ puts $xlogin
620 @ if {$xlogin ne $xusername && [string length $xusername]>0} {
621 @ puts " (claiming to be $xusername)"
622 @ }
623 @ puts " added on $xdate:"
624 @ html "</span>\n"
625 @ if {$alwaysPlaintext || $xmimetype eq "text/plain"} {
626 @ set r [randhex]
627 @ if {$xmimetype ne "text/plain"} {puts "($xmimetype)\n"}
628 @ wiki "<verbatim-$r>[string trimright $xcomment]</verbatim-$r>\n"
629 @ } elseif {$xmimetype eq "text/x-fossil-wiki"} {
630 @ wiki "<p>\n[string trimright $xcomment]\n</p>\n"
631 @ } elseif {$xmimetype eq "text/x-markdown"} {
632 @ html [lindex [markdown $xcomment] 1]
@@ -711,10 +783,52 @@
783 @ <input type="submit" name="cancel" value="Cancel">
784 @ </td>
785 @ <td>Abandon this edit</td>
786 @ </tr>
787 @
788 @ <th1>
789 @ set seenRow 0
790 @ set alwaysPlaintext [info exists plaintext]
791 @ query {SELECT datetime(tkt_mtime) AS xdate, login AS xlogin,
792 @ mimetype as xmimetype, icomment AS xcomment,
793 @ username AS xusername
794 @ FROM ticketchng
795 @ WHERE tkt_id=$tkt_id AND length(icomment)>0} {
796 @ if {$seenRow} {
797 @ html "<hr>\n"
798 @ } else {
799 @ html "<tr><td colspan='2'><hr></td></tr>\n"
800 @ html "<tr><td colspan='2' class='tktDspLabel' style='text-align:left'>\n"
801 @ html "Previous User Comments:</td></tr>\n"
802 @ html "<tr><td colspan='2' class='tktDspValue'>\n"
803 @ set seenRow 1
804 @ }
805 @ html "<span class='tktDspCommenter'>"
806 @ puts $xlogin
807 @ if {$xlogin ne $xusername && [string length $xusername]>0} {
808 @ puts " (claiming to be $xusername)"
809 @ }
810 @ puts " added on $xdate:"
811 @ html "</span>\n"
812 @ if {$alwaysPlaintext || $xmimetype eq "text/plain"} {
813 @ set r [randhex]
814 @ if {$xmimetype ne "text/plain"} {puts "($xmimetype)\n"}
815 @ wiki "<verbatim-$r>[string trimright $xcomment]</verbatim-$r>\n"
816 @ } elseif {$xmimetype eq "text/x-fossil-wiki"} {
817 @ wiki "<p>\n[string trimright $xcomment]\n</p>\n"
818 @ } elseif {$xmimetype eq "text/x-markdown"} {
819 @ html [lindex [markdown $xcomment] 1]
820 @ } elseif {$xmimetype eq "text/html"} {
821 @ wiki "<p><nowiki>\n[string trimright $xcomment]\n</nowiki>\n"
822 @ } else {
823 @ set r [randhex]
824 @ wiki "<verbatim-$r links>[string trimright $xcomment]</verbatim-$r>\n"
825 @ }
826 @ }
827 @ if {$seenRow} {html "</td></tr>\n"}
828 @ </th1>
829 @
830 @ </table>
831 ;
832
833 /*
834 ** Return the code used to generate the edit ticket page
@@ -809,11 +923,12 @@
923 @ WHEN status='Fixed' THEN '#cfe8bd'
924 @ WHEN status='Tested' THEN '#bde5d6'
925 @ WHEN status='Deferred' THEN '#cacae5'
926 @ ELSE '#c8c8c8' END AS 'bgcolor',
927 @ substr(tkt_uuid,1,10) AS '#',
928 @ datetime(tkt_ctime) AS 'created',
929 @ datetime(tkt_mtime) AS 'modified',
930 @ type,
931 @ status,
932 @ subsystem,
933 @ title,
934 @ comment AS '_comments'
@@ -943,8 +1058,9 @@
1058 @ <input type="submit" name="submit" value="Apply Changes">
1059 @ <input type="submit" name="setup" value="Cancel">
1060 @ </p>
1061 @ </div></form>
1062 db_end_transaction(0);
1063 style_submenu_element("Back", "%R/tktsetup");
1064 style_finish_page();
1065
1066 }
1067
+1 -1
--- src/undo.c
+++ src/undo.c
@@ -70,11 +70,11 @@
7070
old_exe = db_column_int(&q, 2);
7171
if( old_exists ){
7272
db_ephemeral_blob(&q, 0, &new);
7373
}
7474
if( file_unsafe_in_tree_path(zFullname) ){
75
- /* do nothign with this unsafe file */
75
+ /* do nothing with this unsafe file */
7676
}else if( old_exists ){
7777
if( new_exists ){
7878
fossil_print("%s %s\n", redoFlag ? "REDO" : "UNDO", zPathname);
7979
}else{
8080
fossil_print("NEW %s\n", zPathname);
8181
--- src/undo.c
+++ src/undo.c
@@ -70,11 +70,11 @@
70 old_exe = db_column_int(&q, 2);
71 if( old_exists ){
72 db_ephemeral_blob(&q, 0, &new);
73 }
74 if( file_unsafe_in_tree_path(zFullname) ){
75 /* do nothign with this unsafe file */
76 }else if( old_exists ){
77 if( new_exists ){
78 fossil_print("%s %s\n", redoFlag ? "REDO" : "UNDO", zPathname);
79 }else{
80 fossil_print("NEW %s\n", zPathname);
81
--- src/undo.c
+++ src/undo.c
@@ -70,11 +70,11 @@
70 old_exe = db_column_int(&q, 2);
71 if( old_exists ){
72 db_ephemeral_blob(&q, 0, &new);
73 }
74 if( file_unsafe_in_tree_path(zFullname) ){
75 /* do nothing with this unsafe file */
76 }else if( old_exists ){
77 if( new_exists ){
78 fossil_print("%s %s\n", redoFlag ? "REDO" : "UNDO", zPathname);
79 }else{
80 fossil_print("NEW %s\n", zPathname);
81
--- src/unversioned.c
+++ src/unversioned.c
@@ -246,10 +246,12 @@
246246
** a single file at a time.
247247
**
248248
** cat FILE ... Concatenate the content of FILEs to stdout.
249249
**
250250
** edit FILE Bring up FILE in a text editor for modification.
251
+** Options:
252
+** --editor NAME Name of the text editor to use
251253
**
252254
** export FILE OUTPUT Write the content of FILE into OUTPUT on disk
253255
**
254256
** list | ls Show all unversioned files held in the local
255257
** repository.
@@ -361,17 +363,17 @@
361363
const char *zTFile; /* Temporary file */
362364
const char *zUVFile; /* Name of the unversioned file */
363365
char *zCmd; /* Command to run the text editor */
364366
Blob content; /* Content of the unversioned file */
365367
366
- verify_all_options();
367
- if( g.argc!=4) usage("edit UVFILE");
368
- zUVFile = g.argv[3];
369368
zEditor = fossil_text_editor();
370369
if( zEditor==0 ){
371370
fossil_fatal("no text editor - set the VISUAL env variable");
372371
}
372
+ verify_all_options();
373
+ if( g.argc!=4) usage("edit UVFILE");
374
+ zUVFile = g.argv[3];
373375
zTFile = fossil_temp_filename();
374376
if( zTFile==0 ) fossil_fatal("cannot find a temporary filename");
375377
db_begin_transaction();
376378
content_rcvid_init("#!fossil unversioned edit");
377379
if( unversioned_content(zUVFile, &content)==0 ){
378380
--- src/unversioned.c
+++ src/unversioned.c
@@ -246,10 +246,12 @@
246 ** a single file at a time.
247 **
248 ** cat FILE ... Concatenate the content of FILEs to stdout.
249 **
250 ** edit FILE Bring up FILE in a text editor for modification.
 
 
251 **
252 ** export FILE OUTPUT Write the content of FILE into OUTPUT on disk
253 **
254 ** list | ls Show all unversioned files held in the local
255 ** repository.
@@ -361,17 +363,17 @@
361 const char *zTFile; /* Temporary file */
362 const char *zUVFile; /* Name of the unversioned file */
363 char *zCmd; /* Command to run the text editor */
364 Blob content; /* Content of the unversioned file */
365
366 verify_all_options();
367 if( g.argc!=4) usage("edit UVFILE");
368 zUVFile = g.argv[3];
369 zEditor = fossil_text_editor();
370 if( zEditor==0 ){
371 fossil_fatal("no text editor - set the VISUAL env variable");
372 }
 
 
 
373 zTFile = fossil_temp_filename();
374 if( zTFile==0 ) fossil_fatal("cannot find a temporary filename");
375 db_begin_transaction();
376 content_rcvid_init("#!fossil unversioned edit");
377 if( unversioned_content(zUVFile, &content)==0 ){
378
--- src/unversioned.c
+++ src/unversioned.c
@@ -246,10 +246,12 @@
246 ** a single file at a time.
247 **
248 ** cat FILE ... Concatenate the content of FILEs to stdout.
249 **
250 ** edit FILE Bring up FILE in a text editor for modification.
251 ** Options:
252 ** --editor NAME Name of the text editor to use
253 **
254 ** export FILE OUTPUT Write the content of FILE into OUTPUT on disk
255 **
256 ** list | ls Show all unversioned files held in the local
257 ** repository.
@@ -361,17 +363,17 @@
363 const char *zTFile; /* Temporary file */
364 const char *zUVFile; /* Name of the unversioned file */
365 char *zCmd; /* Command to run the text editor */
366 Blob content; /* Content of the unversioned file */
367
 
 
 
368 zEditor = fossil_text_editor();
369 if( zEditor==0 ){
370 fossil_fatal("no text editor - set the VISUAL env variable");
371 }
372 verify_all_options();
373 if( g.argc!=4) usage("edit UVFILE");
374 zUVFile = g.argv[3];
375 zTFile = fossil_temp_filename();
376 if( zTFile==0 ) fossil_fatal("cannot find a temporary filename");
377 db_begin_transaction();
378 content_rcvid_init("#!fossil unversioned edit");
379 if( unversioned_content(zUVFile, &content)==0 ){
380
+79 -41
--- src/user.c
+++ src/user.c
@@ -326,14 +326,21 @@
326326
**
327327
** > fossil user contact USERNAME ?CONTACT-INFO?
328328
**
329329
** Query or set contact information for user USERNAME
330330
**
331
-** > fossil user default ?USERNAME?
331
+** > fossil user default ?OPTIONS? ?USERNAME?
332332
**
333333
** Query or set the default user. The default user is the
334
-** user for command-line interaction.
334
+** user for command-line interaction. If USERNAME is an
335
+** empty string, then the default user is unset from the
336
+** repository and will subsequently be determined by the -U
337
+** command-line option or by environment variables
338
+** FOSSIL_USER, USER, LOGNAME, or USERNAME, in that order.
339
+** OPTIONS:
340
+**
341
+** -v|--verbose Show how the default user is computed
335342
**
336343
** > fossil user list | ls
337344
**
338345
** List all users known to the repository
339346
**
@@ -385,21 +392,50 @@
385392
&login, zPw, &caps, &contact
386393
);
387394
db_protect_pop();
388395
free(zPw);
389396
}else if( n>=2 && strncmp(g.argv[2],"default",n)==0 ){
390
- if( g.argc==3 ){
391
- user_select();
392
- fossil_print("%s\n", g.zLogin);
393
- }else{
394
- if( !db_exists("SELECT 1 FROM user WHERE login=%Q", g.argv[3]) ){
395
- fossil_fatal("no such user: %s", g.argv[3]);
396
- }
397
- if( g.localOpen ){
398
- db_lset("default-user", g.argv[3]);
399
- }else{
400
- db_set("default-user", g.argv[3], 0);
397
+ int eVerbose = find_option("verbose","v",0)!=0;
398
+ verify_all_options();
399
+ if( g.argc>3 ){
400
+ const char *zUser = g.argv[3];
401
+ if( fossil_strcmp(zUser,"")==0 || fossil_stricmp(zUser,"nobody")==0 ){
402
+ db_begin_transaction();
403
+ if( g.localOpen ){
404
+ db_multi_exec("DELETE FROM vvar WHERE name='default-user'");
405
+ }
406
+ db_unset("default-user",0);
407
+ db_commit_transaction();
408
+ }else{
409
+ if( !db_exists("SELECT 1 FROM user WHERE login=%Q", g.argv[3]) ){
410
+ fossil_fatal("no such user: %s", g.argv[3]);
411
+ }
412
+ if( g.localOpen ){
413
+ db_lset("default-user", g.argv[3]);
414
+ }else{
415
+ db_set("default-user", g.argv[3], 0);
416
+ }
417
+ }
418
+ }
419
+ if( g.argc==3 || eVerbose ){
420
+ int eHow = user_select();
421
+ const char *zHow = "???";
422
+ switch( eHow ){
423
+ case 1: zHow = "-U option"; break;
424
+ case 2: zHow = "previously set"; break;
425
+ case 3: zHow = "local check-out"; break;
426
+ case 4: zHow = "repository"; break;
427
+ case 5: zHow = "FOSSIL_USER"; break;
428
+ case 6: zHow = "USER"; break;
429
+ case 7: zHow = "LOGNAME"; break;
430
+ case 8: zHow = "USERNAME"; break;
431
+ case 9: zHow = "URL"; break;
432
+ }
433
+ if( eVerbose ){
434
+ fossil_print("%s (determined by %s)\n", g.zLogin, zHow);
435
+ }else{
436
+ fossil_print("%s\n", g.zLogin);
401437
}
402438
}
403439
}else if(( n>=2 && strncmp(g.argv[2],"list",n)==0 ) ||
404440
( n>=2 && strncmp(g.argv[2],"ls",n)==0 )){
405441
Stmt q;
@@ -496,52 +532,54 @@
496532
/*
497533
** Figure out what user is at the controls.
498534
**
499535
** (1) Use the --user and -U command-line options.
500536
**
501
-** (2) If the local database is open, check in VVAR.
502
-**
503
-** (3) Check the default user in the repository
504
-**
505
-** (4) Try the FOSSIL_USER environment variable.
506
-**
507
-** (5) Try the USER environment variable.
508
-**
509
-** (6) Try the LOGNAME environment variable.
510
-**
511
-** (7) Try the USERNAME environment variable.
512
-**
513
-** (8) Check if the user can be extracted from the remote URL.
537
+** (2) The name used for login (if there was a login).
538
+**
539
+** (3) If the local database is open, check in VVAR.
540
+**
541
+** (4) Check the default-user in the repository
542
+**
543
+** (5) Try the FOSSIL_USER environment variable.
544
+**
545
+** (6) Try the USER environment variable.
546
+**
547
+** (7) Try the LOGNAME environment variable.
548
+**
549
+** (8) Try the USERNAME environment variable.
550
+**
551
+** (9) Check if the user can be extracted from the remote URL.
514552
**
515553
** The user name is stored in g.zLogin. The uid is in g.userUid.
516554
*/
517
-void user_select(void){
555
+int user_select(void){
518556
UrlData url;
519
- if( g.userUid ) return;
557
+ if( g.userUid ) return 1;
520558
if( g.zLogin ){
521559
if( attempt_user(g.zLogin)==0 ){
522560
fossil_fatal("no such user: %s", g.zLogin);
523561
}else{
524
- return;
562
+ return 2;
525563
}
526564
}
527565
528
- if( g.localOpen && attempt_user(db_lget("default-user",0)) ) return;
529
-
530
- if( attempt_user(db_get("default-user", 0)) ) return;
531
-
532
- if( attempt_user(fossil_getenv("FOSSIL_USER")) ) return;
533
-
534
- if( attempt_user(fossil_getenv("USER")) ) return;
535
-
536
- if( attempt_user(fossil_getenv("LOGNAME")) ) return;
537
-
538
- if( attempt_user(fossil_getenv("USERNAME")) ) return;
566
+ if( g.localOpen && attempt_user(db_lget("default-user",0)) ) return 3;
567
+
568
+ if( attempt_user(db_get("default-user", 0)) ) return 4;
569
+
570
+ if( attempt_user(fossil_getenv("FOSSIL_USER")) ) return 5;
571
+
572
+ if( attempt_user(fossil_getenv("USER")) ) return 6;
573
+
574
+ if( attempt_user(fossil_getenv("LOGNAME")) ) return 7;
575
+
576
+ if( attempt_user(fossil_getenv("USERNAME")) ) return 8;
539577
540578
memset(&url, 0, sizeof(url));
541579
url_parse_local(0, URL_USE_CONFIG, &url);
542
- if( url.user && attempt_user(url.user) ) return;
580
+ if( url.user && attempt_user(url.user) ) return 9;
543581
544582
fossil_print(
545583
"Cannot figure out who you are! Consider using the --user\n"
546584
"command line option, setting your USER environment variable,\n"
547585
"or setting a default user with \"fossil user default USER\".\n"
548586
--- src/user.c
+++ src/user.c
@@ -326,14 +326,21 @@
326 **
327 ** > fossil user contact USERNAME ?CONTACT-INFO?
328 **
329 ** Query or set contact information for user USERNAME
330 **
331 ** > fossil user default ?USERNAME?
332 **
333 ** Query or set the default user. The default user is the
334 ** user for command-line interaction.
 
 
 
 
 
 
 
335 **
336 ** > fossil user list | ls
337 **
338 ** List all users known to the repository
339 **
@@ -385,21 +392,50 @@
385 &login, zPw, &caps, &contact
386 );
387 db_protect_pop();
388 free(zPw);
389 }else if( n>=2 && strncmp(g.argv[2],"default",n)==0 ){
390 if( g.argc==3 ){
391 user_select();
392 fossil_print("%s\n", g.zLogin);
393 }else{
394 if( !db_exists("SELECT 1 FROM user WHERE login=%Q", g.argv[3]) ){
395 fossil_fatal("no such user: %s", g.argv[3]);
396 }
397 if( g.localOpen ){
398 db_lset("default-user", g.argv[3]);
399 }else{
400 db_set("default-user", g.argv[3], 0);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
401 }
402 }
403 }else if(( n>=2 && strncmp(g.argv[2],"list",n)==0 ) ||
404 ( n>=2 && strncmp(g.argv[2],"ls",n)==0 )){
405 Stmt q;
@@ -496,52 +532,54 @@
496 /*
497 ** Figure out what user is at the controls.
498 **
499 ** (1) Use the --user and -U command-line options.
500 **
501 ** (2) If the local database is open, check in VVAR.
502 **
503 ** (3) Check the default user in the repository
504 **
505 ** (4) Try the FOSSIL_USER environment variable.
506 **
507 ** (5) Try the USER environment variable.
508 **
509 ** (6) Try the LOGNAME environment variable.
510 **
511 ** (7) Try the USERNAME environment variable.
512 **
513 ** (8) Check if the user can be extracted from the remote URL.
 
 
514 **
515 ** The user name is stored in g.zLogin. The uid is in g.userUid.
516 */
517 void user_select(void){
518 UrlData url;
519 if( g.userUid ) return;
520 if( g.zLogin ){
521 if( attempt_user(g.zLogin)==0 ){
522 fossil_fatal("no such user: %s", g.zLogin);
523 }else{
524 return;
525 }
526 }
527
528 if( g.localOpen && attempt_user(db_lget("default-user",0)) ) return;
529
530 if( attempt_user(db_get("default-user", 0)) ) return;
531
532 if( attempt_user(fossil_getenv("FOSSIL_USER")) ) return;
533
534 if( attempt_user(fossil_getenv("USER")) ) return;
535
536 if( attempt_user(fossil_getenv("LOGNAME")) ) return;
537
538 if( attempt_user(fossil_getenv("USERNAME")) ) return;
539
540 memset(&url, 0, sizeof(url));
541 url_parse_local(0, URL_USE_CONFIG, &url);
542 if( url.user && attempt_user(url.user) ) return;
543
544 fossil_print(
545 "Cannot figure out who you are! Consider using the --user\n"
546 "command line option, setting your USER environment variable,\n"
547 "or setting a default user with \"fossil user default USER\".\n"
548
--- src/user.c
+++ src/user.c
@@ -326,14 +326,21 @@
326 **
327 ** > fossil user contact USERNAME ?CONTACT-INFO?
328 **
329 ** Query or set contact information for user USERNAME
330 **
331 ** > fossil user default ?OPTIONS? ?USERNAME?
332 **
333 ** Query or set the default user. The default user is the
334 ** user for command-line interaction. If USERNAME is an
335 ** empty string, then the default user is unset from the
336 ** repository and will subsequently be determined by the -U
337 ** command-line option or by environment variables
338 ** FOSSIL_USER, USER, LOGNAME, or USERNAME, in that order.
339 ** OPTIONS:
340 **
341 ** -v|--verbose Show how the default user is computed
342 **
343 ** > fossil user list | ls
344 **
345 ** List all users known to the repository
346 **
@@ -385,21 +392,50 @@
392 &login, zPw, &caps, &contact
393 );
394 db_protect_pop();
395 free(zPw);
396 }else if( n>=2 && strncmp(g.argv[2],"default",n)==0 ){
397 int eVerbose = find_option("verbose","v",0)!=0;
398 verify_all_options();
399 if( g.argc>3 ){
400 const char *zUser = g.argv[3];
401 if( fossil_strcmp(zUser,"")==0 || fossil_stricmp(zUser,"nobody")==0 ){
402 db_begin_transaction();
403 if( g.localOpen ){
404 db_multi_exec("DELETE FROM vvar WHERE name='default-user'");
405 }
406 db_unset("default-user",0);
407 db_commit_transaction();
408 }else{
409 if( !db_exists("SELECT 1 FROM user WHERE login=%Q", g.argv[3]) ){
410 fossil_fatal("no such user: %s", g.argv[3]);
411 }
412 if( g.localOpen ){
413 db_lset("default-user", g.argv[3]);
414 }else{
415 db_set("default-user", g.argv[3], 0);
416 }
417 }
418 }
419 if( g.argc==3 || eVerbose ){
420 int eHow = user_select();
421 const char *zHow = "???";
422 switch( eHow ){
423 case 1: zHow = "-U option"; break;
424 case 2: zHow = "previously set"; break;
425 case 3: zHow = "local check-out"; break;
426 case 4: zHow = "repository"; break;
427 case 5: zHow = "FOSSIL_USER"; break;
428 case 6: zHow = "USER"; break;
429 case 7: zHow = "LOGNAME"; break;
430 case 8: zHow = "USERNAME"; break;
431 case 9: zHow = "URL"; break;
432 }
433 if( eVerbose ){
434 fossil_print("%s (determined by %s)\n", g.zLogin, zHow);
435 }else{
436 fossil_print("%s\n", g.zLogin);
437 }
438 }
439 }else if(( n>=2 && strncmp(g.argv[2],"list",n)==0 ) ||
440 ( n>=2 && strncmp(g.argv[2],"ls",n)==0 )){
441 Stmt q;
@@ -496,52 +532,54 @@
532 /*
533 ** Figure out what user is at the controls.
534 **
535 ** (1) Use the --user and -U command-line options.
536 **
537 ** (2) The name used for login (if there was a login).
538 **
539 ** (3) If the local database is open, check in VVAR.
540 **
541 ** (4) Check the default-user in the repository
542 **
543 ** (5) Try the FOSSIL_USER environment variable.
544 **
545 ** (6) Try the USER environment variable.
546 **
547 ** (7) Try the LOGNAME environment variable.
548 **
549 ** (8) Try the USERNAME environment variable.
550 **
551 ** (9) Check if the user can be extracted from the remote URL.
552 **
553 ** The user name is stored in g.zLogin. The uid is in g.userUid.
554 */
555 int user_select(void){
556 UrlData url;
557 if( g.userUid ) return 1;
558 if( g.zLogin ){
559 if( attempt_user(g.zLogin)==0 ){
560 fossil_fatal("no such user: %s", g.zLogin);
561 }else{
562 return 2;
563 }
564 }
565
566 if( g.localOpen && attempt_user(db_lget("default-user",0)) ) return 3;
567
568 if( attempt_user(db_get("default-user", 0)) ) return 4;
569
570 if( attempt_user(fossil_getenv("FOSSIL_USER")) ) return 5;
571
572 if( attempt_user(fossil_getenv("USER")) ) return 6;
573
574 if( attempt_user(fossil_getenv("LOGNAME")) ) return 7;
575
576 if( attempt_user(fossil_getenv("USERNAME")) ) return 8;
577
578 memset(&url, 0, sizeof(url));
579 url_parse_local(0, URL_USE_CONFIG, &url);
580 if( url.user && attempt_user(url.user) ) return 9;
581
582 fossil_print(
583 "Cannot figure out who you are! Consider using the --user\n"
584 "command line option, setting your USER environment variable,\n"
585 "or setting a default user with \"fossil user default USER\".\n"
586
+16 -6
--- src/util.c
+++ src/util.c
@@ -666,23 +666,33 @@
666666
/*
667667
** Return the name of the users preferred text editor. Return NULL if
668668
** not found.
669669
**
670670
** Search algorithm:
671
-** (1) The local "editor" setting
672
-** (2) The global "editor" setting
673
-** (3) The VISUAL environment variable
674
-** (4) The EDITOR environment variable
675
-** (5) Any of the following programs that are available:
671
+** (1) The value of the --editor command-line argument
672
+** (2) The local "editor" setting
673
+** (3) The global "editor" setting
674
+** (4) The VISUAL environment variable
675
+** (5) The EDITOR environment variable
676
+** (6) Any of the following programs that are available:
676677
** notepad, nano, pico, jove, edit, vi, vim, ed,
678
+**
679
+** The search only occurs once, the first time this routine is called.
680
+** Second and subsequent invocations always return the same value.
677681
*/
678682
const char *fossil_text_editor(void){
679
- const char *zEditor = db_get("editor", 0);
683
+ static const char *zEditor = 0;
680684
const char *azStdEd[] = {
681685
"notepad", "nano", "pico", "jove", "edit", "vi", "vim", "ed"
682686
};
683687
int i = 0;
688
+ if( zEditor==0 ){
689
+ zEditor = find_option("editor",0,1);
690
+ }
691
+ if( zEditor==0 ){
692
+ zEditor = db_get("editor", 0);
693
+ }
684694
if( zEditor==0 ){
685695
zEditor = fossil_getenv("VISUAL");
686696
}
687697
if( zEditor==0 ){
688698
zEditor = fossil_getenv("EDITOR");
689699
--- src/util.c
+++ src/util.c
@@ -666,23 +666,33 @@
666 /*
667 ** Return the name of the users preferred text editor. Return NULL if
668 ** not found.
669 **
670 ** Search algorithm:
671 ** (1) The local "editor" setting
672 ** (2) The global "editor" setting
673 ** (3) The VISUAL environment variable
674 ** (4) The EDITOR environment variable
675 ** (5) Any of the following programs that are available:
 
676 ** notepad, nano, pico, jove, edit, vi, vim, ed,
 
 
 
677 */
678 const char *fossil_text_editor(void){
679 const char *zEditor = db_get("editor", 0);
680 const char *azStdEd[] = {
681 "notepad", "nano", "pico", "jove", "edit", "vi", "vim", "ed"
682 };
683 int i = 0;
 
 
 
 
 
 
684 if( zEditor==0 ){
685 zEditor = fossil_getenv("VISUAL");
686 }
687 if( zEditor==0 ){
688 zEditor = fossil_getenv("EDITOR");
689
--- src/util.c
+++ src/util.c
@@ -666,23 +666,33 @@
666 /*
667 ** Return the name of the users preferred text editor. Return NULL if
668 ** not found.
669 **
670 ** Search algorithm:
671 ** (1) The value of the --editor command-line argument
672 ** (2) The local "editor" setting
673 ** (3) The global "editor" setting
674 ** (4) The VISUAL environment variable
675 ** (5) The EDITOR environment variable
676 ** (6) Any of the following programs that are available:
677 ** notepad, nano, pico, jove, edit, vi, vim, ed,
678 **
679 ** The search only occurs once, the first time this routine is called.
680 ** Second and subsequent invocations always return the same value.
681 */
682 const char *fossil_text_editor(void){
683 static const char *zEditor = 0;
684 const char *azStdEd[] = {
685 "notepad", "nano", "pico", "jove", "edit", "vi", "vim", "ed"
686 };
687 int i = 0;
688 if( zEditor==0 ){
689 zEditor = find_option("editor",0,1);
690 }
691 if( zEditor==0 ){
692 zEditor = db_get("editor", 0);
693 }
694 if( zEditor==0 ){
695 zEditor = fossil_getenv("VISUAL");
696 }
697 if( zEditor==0 ){
698 zEditor = fossil_getenv("EDITOR");
699
+2 -2
--- src/winfile.c
+++ src/winfile.c
@@ -505,14 +505,14 @@
505505
fi2.FileId[1], fi2.FileId[0]);
506506
}
507507
}
508508
if( zFileId==0 ){
509509
if( GetFileInformationByHandle(hFile,&fi) ){
510
- ULARGE_INTEGER FileId = {
510
+ ULARGE_INTEGER FileId = {{
511511
/*.LowPart = */ fi.nFileIndexLow,
512512
/*.HighPart = */ fi.nFileIndexHigh
513
- };
513
+ }};
514514
zFileId = mprintf(
515515
"%08x/%016llx",
516516
fi.dwVolumeSerialNumber,(u64)FileId.QuadPart);
517517
}
518518
}
519519
--- src/winfile.c
+++ src/winfile.c
@@ -505,14 +505,14 @@
505 fi2.FileId[1], fi2.FileId[0]);
506 }
507 }
508 if( zFileId==0 ){
509 if( GetFileInformationByHandle(hFile,&fi) ){
510 ULARGE_INTEGER FileId = {
511 /*.LowPart = */ fi.nFileIndexLow,
512 /*.HighPart = */ fi.nFileIndexHigh
513 };
514 zFileId = mprintf(
515 "%08x/%016llx",
516 fi.dwVolumeSerialNumber,(u64)FileId.QuadPart);
517 }
518 }
519
--- src/winfile.c
+++ src/winfile.c
@@ -505,14 +505,14 @@
505 fi2.FileId[1], fi2.FileId[0]);
506 }
507 }
508 if( zFileId==0 ){
509 if( GetFileInformationByHandle(hFile,&fi) ){
510 ULARGE_INTEGER FileId = {{
511 /*.LowPart = */ fi.nFileIndexLow,
512 /*.HighPart = */ fi.nFileIndexHigh
513 }};
514 zFileId = mprintf(
515 "%08x/%016llx",
516 fi.dwVolumeSerialNumber,(u64)FileId.QuadPart);
517 }
518 }
519
+20 -8
--- src/xfer.c
+++ src/xfer.c
@@ -1116,17 +1116,29 @@
11161116
blob_appendf(pXfer->pOut, "uvigot %s %lld %s %d\n",
11171117
zName, mtime, zHash, sz);
11181118
}
11191119
db_finalize(&uvq);
11201120
}
1121
+
1122
+/*
1123
+** Return a string that contains supplemental information about a
1124
+** "not authorized" error. The string might be empty if no additional
1125
+** information is available.
1126
+*/
1127
+static char *whyNotAuth(void){
1128
+ if( g.useLocalauth && db_get_boolean("localauth",0)!=0 ){
1129
+ return "\\sbecause\\sthe\\s'localauth'\\ssetting\\sis\\senabled";
1130
+ }
1131
+ return "";
1132
+}
11211133
11221134
/*
11231135
** Called when there is an attempt to transfer private content to and
11241136
** from a server without authorization.
11251137
*/
11261138
static void server_private_xfer_not_authorized(void){
1127
- @ error not\sauthorized\sto\ssync\sprivate\scontent
1139
+ @ error not\sauthorized\sto\ssync\sprivate\scontent%s(whyNotAuth())
11281140
}
11291141
11301142
/*
11311143
** Return the common TH1 code to evaluate prior to evaluating any other
11321144
** TH1 transfer notification scripts.
@@ -1316,11 +1328,11 @@
13161328
** Server accepts a file from the client.
13171329
*/
13181330
if( blob_eq(&xfer.aToken[0], "file") ){
13191331
if( !isPush ){
13201332
cgi_reset_content();
1321
- @ error not\sauthorized\sto\swrite
1333
+ @ error not\sauthorized\sto\swrite%s(whyNotAuth())
13221334
nErr++;
13231335
break;
13241336
}
13251337
xfer_accept_file(&xfer, 0, pzUuidList, pnUuidList);
13261338
if( blob_size(&xfer.err) ){
@@ -1337,11 +1349,11 @@
13371349
** Server accepts a compressed file from the client.
13381350
*/
13391351
if( blob_eq(&xfer.aToken[0], "cfile") ){
13401352
if( !isPush ){
13411353
cgi_reset_content();
1342
- @ error not\sauthorized\sto\swrite
1354
+ @ error not\sauthorized\sto\swrite%s(whyNotAuth())
13431355
nErr++;
13441356
break;
13451357
}
13461358
xfer_accept_compressed_file(&xfer, pzUuidList, pnUuidList);
13471359
if( blob_size(&xfer.err) ){
@@ -1461,23 +1473,23 @@
14611473
}
14621474
login_check_credentials();
14631475
if( blob_eq(&xfer.aToken[0], "pull") ){
14641476
if( !g.perm.Read ){
14651477
cgi_reset_content();
1466
- @ error not\sauthorized\sto\sread
1478
+ @ error not\sauthorized\sto\sread%s(whyNotAuth())
14671479
nErr++;
14681480
break;
14691481
}
14701482
isPull = 1;
14711483
}else{
14721484
if( !g.perm.Write ){
14731485
if( !isPull ){
14741486
cgi_reset_content();
1475
- @ error not\sauthorized\sto\swrite
1487
+ @ error not\sauthorized\sto\swrite%s(whyNotAuth())
14761488
nErr++;
14771489
}else{
1478
- @ message pull\sonly\s-\snot\sauthorized\sto\spush
1490
+ @ message pull\sonly\s-\snot\sauthorized\sto\spush%s(whyNotAuth())
14791491
}
14801492
}else{
14811493
isPush = 1;
14821494
}
14831495
}
@@ -1491,11 +1503,11 @@
14911503
int iVers;
14921504
login_check_credentials();
14931505
if( !g.perm.Clone ){
14941506
cgi_reset_content();
14951507
@ push %s(db_get("server-code", "x")) %s(db_get("project-code", "x"))
1496
- @ error not\sauthorized\sto\sclone
1508
+ @ error not\sauthorized\sto\sclone%s(whyNotAuth())
14971509
nErr++;
14981510
break;
14991511
}
15001512
if( db_get_boolean("uv-sync",0) && !uvCatalogSent ){
15011513
@ pragma uv-pull-only
@@ -1592,11 +1604,11 @@
15921604
}
15931605
blob_zero(&content);
15941606
blob_extract(xfer.pIn, size, &content);
15951607
if( !g.perm.Admin ){
15961608
cgi_reset_content();
1597
- @ error not\sauthorized\sto\spush\sconfiguration
1609
+ @ error not\sauthorized\sto\spush\sconfiguration%s(whyNotAuth())
15981610
nErr++;
15991611
break;
16001612
}
16011613
configure_receive(zName, &content, CONFIGSET_ALL);
16021614
blob_reset(&content);
16031615
--- src/xfer.c
+++ src/xfer.c
@@ -1116,17 +1116,29 @@
1116 blob_appendf(pXfer->pOut, "uvigot %s %lld %s %d\n",
1117 zName, mtime, zHash, sz);
1118 }
1119 db_finalize(&uvq);
1120 }
 
 
 
 
 
 
 
 
 
 
 
 
1121
1122 /*
1123 ** Called when there is an attempt to transfer private content to and
1124 ** from a server without authorization.
1125 */
1126 static void server_private_xfer_not_authorized(void){
1127 @ error not\sauthorized\sto\ssync\sprivate\scontent
1128 }
1129
1130 /*
1131 ** Return the common TH1 code to evaluate prior to evaluating any other
1132 ** TH1 transfer notification scripts.
@@ -1316,11 +1328,11 @@
1316 ** Server accepts a file from the client.
1317 */
1318 if( blob_eq(&xfer.aToken[0], "file") ){
1319 if( !isPush ){
1320 cgi_reset_content();
1321 @ error not\sauthorized\sto\swrite
1322 nErr++;
1323 break;
1324 }
1325 xfer_accept_file(&xfer, 0, pzUuidList, pnUuidList);
1326 if( blob_size(&xfer.err) ){
@@ -1337,11 +1349,11 @@
1337 ** Server accepts a compressed file from the client.
1338 */
1339 if( blob_eq(&xfer.aToken[0], "cfile") ){
1340 if( !isPush ){
1341 cgi_reset_content();
1342 @ error not\sauthorized\sto\swrite
1343 nErr++;
1344 break;
1345 }
1346 xfer_accept_compressed_file(&xfer, pzUuidList, pnUuidList);
1347 if( blob_size(&xfer.err) ){
@@ -1461,23 +1473,23 @@
1461 }
1462 login_check_credentials();
1463 if( blob_eq(&xfer.aToken[0], "pull") ){
1464 if( !g.perm.Read ){
1465 cgi_reset_content();
1466 @ error not\sauthorized\sto\sread
1467 nErr++;
1468 break;
1469 }
1470 isPull = 1;
1471 }else{
1472 if( !g.perm.Write ){
1473 if( !isPull ){
1474 cgi_reset_content();
1475 @ error not\sauthorized\sto\swrite
1476 nErr++;
1477 }else{
1478 @ message pull\sonly\s-\snot\sauthorized\sto\spush
1479 }
1480 }else{
1481 isPush = 1;
1482 }
1483 }
@@ -1491,11 +1503,11 @@
1491 int iVers;
1492 login_check_credentials();
1493 if( !g.perm.Clone ){
1494 cgi_reset_content();
1495 @ push %s(db_get("server-code", "x")) %s(db_get("project-code", "x"))
1496 @ error not\sauthorized\sto\sclone
1497 nErr++;
1498 break;
1499 }
1500 if( db_get_boolean("uv-sync",0) && !uvCatalogSent ){
1501 @ pragma uv-pull-only
@@ -1592,11 +1604,11 @@
1592 }
1593 blob_zero(&content);
1594 blob_extract(xfer.pIn, size, &content);
1595 if( !g.perm.Admin ){
1596 cgi_reset_content();
1597 @ error not\sauthorized\sto\spush\sconfiguration
1598 nErr++;
1599 break;
1600 }
1601 configure_receive(zName, &content, CONFIGSET_ALL);
1602 blob_reset(&content);
1603
--- src/xfer.c
+++ src/xfer.c
@@ -1116,17 +1116,29 @@
1116 blob_appendf(pXfer->pOut, "uvigot %s %lld %s %d\n",
1117 zName, mtime, zHash, sz);
1118 }
1119 db_finalize(&uvq);
1120 }
1121
1122 /*
1123 ** Return a string that contains supplemental information about a
1124 ** "not authorized" error. The string might be empty if no additional
1125 ** information is available.
1126 */
1127 static char *whyNotAuth(void){
1128 if( g.useLocalauth && db_get_boolean("localauth",0)!=0 ){
1129 return "\\sbecause\\sthe\\s'localauth'\\ssetting\\sis\\senabled";
1130 }
1131 return "";
1132 }
1133
1134 /*
1135 ** Called when there is an attempt to transfer private content to and
1136 ** from a server without authorization.
1137 */
1138 static void server_private_xfer_not_authorized(void){
1139 @ error not\sauthorized\sto\ssync\sprivate\scontent%s(whyNotAuth())
1140 }
1141
1142 /*
1143 ** Return the common TH1 code to evaluate prior to evaluating any other
1144 ** TH1 transfer notification scripts.
@@ -1316,11 +1328,11 @@
1328 ** Server accepts a file from the client.
1329 */
1330 if( blob_eq(&xfer.aToken[0], "file") ){
1331 if( !isPush ){
1332 cgi_reset_content();
1333 @ error not\sauthorized\sto\swrite%s(whyNotAuth())
1334 nErr++;
1335 break;
1336 }
1337 xfer_accept_file(&xfer, 0, pzUuidList, pnUuidList);
1338 if( blob_size(&xfer.err) ){
@@ -1337,11 +1349,11 @@
1349 ** Server accepts a compressed file from the client.
1350 */
1351 if( blob_eq(&xfer.aToken[0], "cfile") ){
1352 if( !isPush ){
1353 cgi_reset_content();
1354 @ error not\sauthorized\sto\swrite%s(whyNotAuth())
1355 nErr++;
1356 break;
1357 }
1358 xfer_accept_compressed_file(&xfer, pzUuidList, pnUuidList);
1359 if( blob_size(&xfer.err) ){
@@ -1461,23 +1473,23 @@
1473 }
1474 login_check_credentials();
1475 if( blob_eq(&xfer.aToken[0], "pull") ){
1476 if( !g.perm.Read ){
1477 cgi_reset_content();
1478 @ error not\sauthorized\sto\sread%s(whyNotAuth())
1479 nErr++;
1480 break;
1481 }
1482 isPull = 1;
1483 }else{
1484 if( !g.perm.Write ){
1485 if( !isPull ){
1486 cgi_reset_content();
1487 @ error not\sauthorized\sto\swrite%s(whyNotAuth())
1488 nErr++;
1489 }else{
1490 @ message pull\sonly\s-\snot\sauthorized\sto\spush%s(whyNotAuth())
1491 }
1492 }else{
1493 isPush = 1;
1494 }
1495 }
@@ -1491,11 +1503,11 @@
1503 int iVers;
1504 login_check_credentials();
1505 if( !g.perm.Clone ){
1506 cgi_reset_content();
1507 @ push %s(db_get("server-code", "x")) %s(db_get("project-code", "x"))
1508 @ error not\sauthorized\sto\sclone%s(whyNotAuth())
1509 nErr++;
1510 break;
1511 }
1512 if( db_get_boolean("uv-sync",0) && !uvCatalogSent ){
1513 @ pragma uv-pull-only
@@ -1592,11 +1604,11 @@
1604 }
1605 blob_zero(&content);
1606 blob_extract(xfer.pIn, size, &content);
1607 if( !g.perm.Admin ){
1608 cgi_reset_content();
1609 @ error not\sauthorized\sto\spush\sconfiguration%s(whyNotAuth())
1610 nErr++;
1611 break;
1612 }
1613 configure_receive(zName, &content, CONFIGSET_ALL);
1614 blob_reset(&content);
1615
--- test/many-www.tcl
+++ test/many-www.tcl
@@ -24,11 +24,11 @@
2424
/setup
2525
/dir
2626
/wcontent
2727
/attachlist
2828
/taglist
29
- /test_env
29
+ /test-env
3030
/stat
3131
/rcvfromlist
3232
/urllist
3333
/modreq
3434
/info/d5c4
3535
--- test/many-www.tcl
+++ test/many-www.tcl
@@ -24,11 +24,11 @@
24 /setup
25 /dir
26 /wcontent
27 /attachlist
28 /taglist
29 /test_env
30 /stat
31 /rcvfromlist
32 /urllist
33 /modreq
34 /info/d5c4
35
--- test/many-www.tcl
+++ test/many-www.tcl
@@ -24,11 +24,11 @@
24 /setup
25 /dir
26 /wcontent
27 /attachlist
28 /taglist
29 /test-env
30 /stat
31 /rcvfromlist
32 /urllist
33 /modreq
34 /info/d5c4
35
--- test/tester.tcl
+++ test/tester.tcl
@@ -356,30 +356,39 @@
356356
mtime-changes \
357357
mv-rm-files \
358358
pgp-command \
359359
preferred-diff-type \
360360
proxy \
361
+ raw-bgcolor \
361362
redirect-to-https \
362363
relative-paths \
363364
repo-cksum \
364365
repolist-skin \
365366
robot-restrict \
366367
robots-txt \
367368
safe-html \
368369
self-pw-reset \
369370
self-register \
371
+ show-repolist-desc \
372
+ show-repolist-lg \
370373
sitemap-extra \
371374
ssh-command \
372375
ssl-ca-location \
373376
ssl-identity \
374377
tclsh \
375378
th1-setup \
376379
th1-uri-regexp \
377380
ticket-default-report \
381
+ timeline-hard-newlines \
382
+ timeline-plaintext \
383
+ timeline-truncate-at-blank \
384
+ timeline-tslink-info \
378385
timeline-utc \
379386
user-color-map \
387
+ verify-comments \
380388
uv-sync \
389
+ vuln-report \
381390
web-browser]
382391
383392
fossil test-th-eval "hasfeature legacyMvRm"
384393
385394
if {[normalize_result] eq "1"} {
386395
387396
ADDED test/th1-taint.test
--- test/tester.tcl
+++ test/tester.tcl
@@ -356,30 +356,39 @@
356 mtime-changes \
357 mv-rm-files \
358 pgp-command \
359 preferred-diff-type \
360 proxy \
 
361 redirect-to-https \
362 relative-paths \
363 repo-cksum \
364 repolist-skin \
365 robot-restrict \
366 robots-txt \
367 safe-html \
368 self-pw-reset \
369 self-register \
 
 
370 sitemap-extra \
371 ssh-command \
372 ssl-ca-location \
373 ssl-identity \
374 tclsh \
375 th1-setup \
376 th1-uri-regexp \
377 ticket-default-report \
 
 
 
 
378 timeline-utc \
379 user-color-map \
 
380 uv-sync \
 
381 web-browser]
382
383 fossil test-th-eval "hasfeature legacyMvRm"
384
385 if {[normalize_result] eq "1"} {
386
387 DDED test/th1-taint.test
--- test/tester.tcl
+++ test/tester.tcl
@@ -356,30 +356,39 @@
356 mtime-changes \
357 mv-rm-files \
358 pgp-command \
359 preferred-diff-type \
360 proxy \
361 raw-bgcolor \
362 redirect-to-https \
363 relative-paths \
364 repo-cksum \
365 repolist-skin \
366 robot-restrict \
367 robots-txt \
368 safe-html \
369 self-pw-reset \
370 self-register \
371 show-repolist-desc \
372 show-repolist-lg \
373 sitemap-extra \
374 ssh-command \
375 ssl-ca-location \
376 ssl-identity \
377 tclsh \
378 th1-setup \
379 th1-uri-regexp \
380 ticket-default-report \
381 timeline-hard-newlines \
382 timeline-plaintext \
383 timeline-truncate-at-blank \
384 timeline-tslink-info \
385 timeline-utc \
386 user-color-map \
387 verify-comments \
388 uv-sync \
389 vuln-report \
390 web-browser]
391
392 fossil test-th-eval "hasfeature legacyMvRm"
393
394 if {[normalize_result] eq "1"} {
395
396 DDED test/th1-taint.test
--- a/test/th1-taint.test
+++ b/test/th1-taint.test
@@ -0,0 +1,84 @@
1
+#
2
+# Copyright (c) 2025 D. Richard Hipp
3
+#
4
+# This program is free software; you can redistribute it and/or
5
+# modify it under the terms of the Simplified BSD License (also
6
+# known as the "2-Clause License" or "FreeBSD License".)
7
+#
8
+# This program is distributed in the hope that it will be useful,
9
+# but without any warranty; without even the implied warranty of
10
+# merchantability or fitness for a particular purpose.
11
+#
12
+# Author contact information:
13
+# [email protected]
14
+# http://www.hwaci.com/drh/
15
+#
16
+############################################################################
17
+#
18
+# TH1 Commands
19
+#
20
+
21
+set path [file dirname [info script]]; test_setup
22
+
23
+proc taint-test {testnum th1script expected} {
24
+ global fossilexe
25
+ set rc [catch {exec $fossilexe test-th-eval $th1script} got]
26
+ if {$rc} {
27
+ test th1-taint-$testnum 0
28
+ puts $got
29
+ return
30
+ }
31
+ if {$got ne $expected} {
32
+ test th1-taint-$testnum 0
33
+ puts " Expected: $expected"
34
+ puts " Got: $got"
35
+ } else {
36
+ test th1-taint-$testnum 1
37
+ }
38
+}
39
+
40
+taint-test 10 {string is tainted abcd} 0
41
+taint-test 20 {string is tainted [taint abcd]} 1
42
+taint-test 30 {string is tainted [untaint [taint abcd]]} 0
43
+taint-test 40 {string is tainted [untaint abcde]} 0
44
+taint-test 50 {string is tainted "abc[taint def]ghi"} 1
45
+taint-test 60 {set t1 [taint abc]; string is tainted "123 $t1 456"} 1
46
+
47
+taint-test 100 {set t1 [taint abc]; lappend t1 def; string is tainted $t1} 1
48
+taint-test 110 {set t1 abc; lappend t1 [taint def]; string is tainted $t1} 1
49
+
50
+taint-test 200 {string is tainted [list abc def ghi]} 0
51
+taint-test 210 {string is tainted [list [taint abc] def ghi]} 1
52
+taint-test 220 {string is tainted [list abc [taint def] ghi]} 1
53
+taint-test 230 {string is tainted [list abc def [taint ghi]]} 1
54
+
55
+taint-test 300 {
56
+ set res {}
57
+ foreach x [list abc [taint def] ghi] {
58
+ lappend res [string is tainted $x]
59
+ }
60
+ set res
61
+} {1 1 1}
62
+taint-test 310 {
63
+ set res {}
64
+ foreach {x y} [list abc [taint def] ghi jkl] {
65
+ lappend res [string is tainted $x] [string is tainted $y]
66
+ }
67
+ set res
68
+} {1 1 1 1}
69
+
70
+taint-test 400 {string is tainted [lindex "abc [taint def] ghi" 0]} 1
71
+taint-test 410 {string is tainted [lindex "abc [taint def] ghi" 1]} 1
72
+taint-test 420 {string is tainted [lindex "abc [taint def] ghi" 2]} 1
73
+taint-test 430 {string is tainted [lindex "abc [taint def] ghi" 3]} 0
74
+
75
+taint-test 500 {string is tainted [string index [taint abcdefg] 3]} 1
76
+
77
+taint-test 600 {string is tainted [string range [taint abcdefg] 3 5]} 1
78
+
79
+taint-test 700 {string is tainted [string trim [taint " abcdefg "]]} 1
80
+taint-test 710 {string is tainted [string trimright [taint " abcdefg "]]} 1
81
+taint-test 720 {string is tainted [string trimleft [taint " abcdefg "]]} 1
82
+
83
+
84
+test_cleanup
--- a/test/th1-taint.test
+++ b/test/th1-taint.test
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/test/th1-taint.test
+++ b/test/th1-taint.test
@@ -0,0 +1,84 @@
1 #
2 # Copyright (c) 2025 D. Richard Hipp
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the Simplified BSD License (also
6 # known as the "2-Clause License" or "FreeBSD License".)
7 #
8 # This program is distributed in the hope that it will be useful,
9 # but without any warranty; without even the implied warranty of
10 # merchantability or fitness for a particular purpose.
11 #
12 # Author contact information:
13 # [email protected]
14 # http://www.hwaci.com/drh/
15 #
16 ############################################################################
17 #
18 # TH1 Commands
19 #
20
21 set path [file dirname [info script]]; test_setup
22
23 proc taint-test {testnum th1script expected} {
24 global fossilexe
25 set rc [catch {exec $fossilexe test-th-eval $th1script} got]
26 if {$rc} {
27 test th1-taint-$testnum 0
28 puts $got
29 return
30 }
31 if {$got ne $expected} {
32 test th1-taint-$testnum 0
33 puts " Expected: $expected"
34 puts " Got: $got"
35 } else {
36 test th1-taint-$testnum 1
37 }
38 }
39
40 taint-test 10 {string is tainted abcd} 0
41 taint-test 20 {string is tainted [taint abcd]} 1
42 taint-test 30 {string is tainted [untaint [taint abcd]]} 0
43 taint-test 40 {string is tainted [untaint abcde]} 0
44 taint-test 50 {string is tainted "abc[taint def]ghi"} 1
45 taint-test 60 {set t1 [taint abc]; string is tainted "123 $t1 456"} 1
46
47 taint-test 100 {set t1 [taint abc]; lappend t1 def; string is tainted $t1} 1
48 taint-test 110 {set t1 abc; lappend t1 [taint def]; string is tainted $t1} 1
49
50 taint-test 200 {string is tainted [list abc def ghi]} 0
51 taint-test 210 {string is tainted [list [taint abc] def ghi]} 1
52 taint-test 220 {string is tainted [list abc [taint def] ghi]} 1
53 taint-test 230 {string is tainted [list abc def [taint ghi]]} 1
54
55 taint-test 300 {
56 set res {}
57 foreach x [list abc [taint def] ghi] {
58 lappend res [string is tainted $x]
59 }
60 set res
61 } {1 1 1}
62 taint-test 310 {
63 set res {}
64 foreach {x y} [list abc [taint def] ghi jkl] {
65 lappend res [string is tainted $x] [string is tainted $y]
66 }
67 set res
68 } {1 1 1 1}
69
70 taint-test 400 {string is tainted [lindex "abc [taint def] ghi" 0]} 1
71 taint-test 410 {string is tainted [lindex "abc [taint def] ghi" 1]} 1
72 taint-test 420 {string is tainted [lindex "abc [taint def] ghi" 2]} 1
73 taint-test 430 {string is tainted [lindex "abc [taint def] ghi" 3]} 0
74
75 taint-test 500 {string is tainted [string index [taint abcdefg] 3]} 1
76
77 taint-test 600 {string is tainted [string range [taint abcdefg] 3 5]} 1
78
79 taint-test 700 {string is tainted [string trim [taint " abcdefg "]]} 1
80 taint-test 710 {string is tainted [string trimright [taint " abcdefg "]]} 1
81 taint-test 720 {string is tainted [string trimleft [taint " abcdefg "]]} 1
82
83
84 test_cleanup
+36 -36
--- test/th1.test
+++ test/th1.test
@@ -795,23 +795,23 @@
795795
rpage-\$requested_page\
796796
cpage-\$canonical_page\">" [normalize_result]]}
797797
798798
###############################################################################
799799
800
-fossil test-th-eval "styleHeader {Page Title Here}"
801
-test th1-header-1 {$RESULT eq {TH_ERROR: repository unavailable}}
800
+#fossil test-th-eval "styleHeader {Page Title Here}"
801
+#test th1-header-1 {$RESULT eq {TH_ERROR: repository unavailable}}
802802
803803
###############################################################################
804804
805805
test_in_checkout th1-header-2 {
806806
fossil test-th-eval --open-config "styleHeader {Page Title Here}"
807807
} {[regexp -- {<title>Fossil: Page Title Here</title>} $RESULT]}
808808
809809
###############################################################################
810810
811
-fossil test-th-eval "styleFooter"
812
-test th1-footer-1 {$RESULT eq {TH_ERROR: repository unavailable}}
811
+#fossil test-th-eval "styleFooter"
812
+#test th1-footer-1 {$RESULT eq {TH_ERROR: repository unavailable}}
813813
814814
###############################################################################
815815
816816
fossil test-th-eval --open-config "styleFooter"
817817
test th1-footer-2 {$RESULT eq {}}
@@ -879,44 +879,44 @@
879879
test th1-artifact-1 {$RESULT eq \
880880
{TH_ERROR: wrong # args: should be "artifact ID ?FILENAME?"}}
881881
882882
###############################################################################
883883
884
-fossil test-th-eval "artifact tip"
885
-test th1-artifact-2 {$RESULT eq {TH_ERROR: repository unavailable}}
884
+#fossil test-th-eval "artifact tip"
885
+#test th1-artifact-2 {$RESULT eq {TH_ERROR: repository unavailable}}
886886
887887
###############################################################################
888888
889889
test_in_checkout th1-artifact-3 {
890890
fossil test-th-eval --open-config "artifact tip"
891891
} {[regexp -- {F test/th1\.test [0-9a-f]{40,64}} $RESULT]}
892892
893893
###############################################################################
894894
895
-fossil test-th-eval "artifact 0000000000"
896
-test th1-artifact-4 {$RESULT eq {TH_ERROR: repository unavailable}}
895
+#fossil test-th-eval "artifact 0000000000"
896
+#test th1-artifact-4 {$RESULT eq {TH_ERROR: repository unavailable}}
897897
898898
###############################################################################
899899
900900
fossil test-th-eval --open-config "artifact 0000000000"
901901
test th1-artifact-5 {$RESULT eq {TH_ERROR: name not found}}
902902
903903
###############################################################################
904904
905
-fossil test-th-eval "artifact tip test/th1.test"
906
-test th1-artifact-6 {$RESULT eq {TH_ERROR: repository unavailable}}
905
+#fossil test-th-eval "artifact tip test/th1.test"
906
+#test th1-artifact-6 {$RESULT eq {TH_ERROR: repository unavailable}}
907907
908908
###############################################################################
909909
910910
test_in_checkout th1-artifact-7 {
911911
fossil test-th-eval --open-config "artifact tip test/th1.test"
912912
} {[regexp -- {th1-artifact-7} $RESULT]}
913913
914914
###############################################################################
915915
916
-fossil test-th-eval "artifact 0000000000 test/th1.test"
917
-test th1-artifact-8 {$RESULT eq {TH_ERROR: repository unavailable}}
916
+#fossil test-th-eval "artifact 0000000000 test/th1.test"
917
+#test th1-artifact-8 {$RESULT eq {TH_ERROR: repository unavailable}}
918918
919919
###############################################################################
920920
921921
fossil test-th-eval --open-config "artifact 0000000000 test/th1.test"
922922
test th1-artifact-9 {$RESULT eq {TH_ERROR: manifest not found}}
@@ -947,12 +947,12 @@
947947
}
948948
}
949949
950950
###############################################################################
951951
952
-fossil test-th-eval "globalState configuration"
953
-test th1-globalState-3 {[string length $RESULT] == 0}
952
+#fossil test-th-eval "globalState configuration"
953
+#test th1-globalState-3 {[string length $RESULT] == 0}
954954
955955
###############################################################################
956956
957957
fossil test-th-eval --open-config "globalState configuration"
958958
test th1-globalState-4 {[string length $RESULT] > 0}
@@ -1041,12 +1041,12 @@
10411041
fossil test-th-eval "globalState flags"
10421042
test th1-globalState-16 {$RESULT eq "0"}
10431043
10441044
###############################################################################
10451045
1046
-fossil test-th-eval "reinitialize; globalState configuration"
1047
-test th1-reinitialize-1 {$RESULT eq ""}
1046
+#fossil test-th-eval "reinitialize; globalState configuration"
1047
+#test th1-reinitialize-1 {$RESULT eq ""}
10481048
10491049
###############################################################################
10501050
10511051
fossil test-th-eval "reinitialize 1; globalState configuration"
10521052
test th1-reinitialize-2 {$RESULT ne ""}
@@ -1056,29 +1056,29 @@
10561056
#
10571057
# NOTE: This test will fail if the command names are added to TH1, or
10581058
# moved from Tcl builds to plain or the reverse. Sorting the
10591059
# command lists eliminates a dependence on order.
10601060
#
1061
-fossil test-th-eval "info commands"
1062
-set sorted_result [lsort $RESULT]
1063
-protOut "Sorted: $sorted_result"
1064
-set base_commands {anoncap anycap array artifact break breakpoint \
1065
- builtin_request_js capexpr captureTh1 catch cgiHeaderLine checkout \
1066
- combobox continue copybtn date decorate defHeader dir enable_htmlify \
1067
- enable_output encode64 error expr for foreach getParameter glob_match \
1068
- globalState hascap hasfeature html htmlize http httpize if info \
1069
- insertCsrf lappend lindex linecount list llength lsearch markdown nonce \
1070
- proc puts query randhex redirect regexp reinitialize rename render \
1071
- repository return searchable set setParameter setting stime string \
1072
- styleFooter styleHeader styleScript submenu tclReady trace unset \
1073
- unversioned uplevel upvar utime verifyCsrf verifyLogin wiki}
1074
-set tcl_commands {tclEval tclExpr tclInvoke tclIsSafe tclMakeSafe}
1075
-if {$th1Tcl} {
1076
- test th1-info-commands-1 {$sorted_result eq [lsort "$base_commands $tcl_commands"]}
1077
-} else {
1078
- test th1-info-commands-1 {$sorted_result eq [lsort "$base_commands"]}
1079
-}
1061
+#fossil test-th-eval "info commands"
1062
+#set sorted_result [lsort $RESULT]
1063
+#protOut "Sorted: $sorted_result"
1064
+#set base_commands {anoncap anycap array artifact break breakpoint \
1065
+# builtin_request_js capexpr captureTh1 catch cgiHeaderLine checkout \
1066
+# combobox continue copybtn date decorate defHeader dir enable_htmlify \
1067
+# enable_output encode64 error expr for foreach getParameter glob_match \
1068
+# globalState hascap hasfeature html htmlize http httpize if info \
1069
+# insertCsrf lappend lindex linecount list llength lsearch markdown nonce \
1070
+# proc puts query randhex redirect regexp reinitialize rename render \
1071
+# repository return searchable set setParameter setting stime string \
1072
+# styleFooter styleHeader styleScript submenu tclReady trace unset \
1073
+# unversioned uplevel upvar utime verifyCsrf verifyLogin wiki}
1074
+#set tcl_commands {tclEval tclExpr tclInvoke tclIsSafe tclMakeSafe}
1075
+#if {$th1Tcl} {
1076
+# test th1-info-commands-1 {$sorted_result eq [lsort "$base_commands $tcl_commands"]}
1077
+#} else {
1078
+# test th1-info-commands-1 {$sorted_result eq [lsort "$base_commands"]}
1079
+#}
10801080
10811081
###############################################################################
10821082
10831083
fossil test-th-eval "info vars"
10841084
@@ -1326,11 +1326,11 @@
13261326
13271327
###############################################################################
13281328
13291329
fossil test-th-eval {string is other 123}
13301330
test th1-string-is-4 {$RESULT eq \
1331
-"TH_ERROR: Expected alnum, double, integer, or list, got: other"}
1331
+"TH_ERROR: Expected alnum, double, integer, list, or tainted, got: other"}
13321332
13331333
###############################################################################
13341334
13351335
fossil test-th-eval {string is alnum 123}
13361336
test th1-string-is-5 {$RESULT eq "1"}
13371337
13381338
ADDED tools/fake-smtpd.tcl
13391339
ADDED tools/find-fossil-cgis.tcl
--- test/th1.test
+++ test/th1.test
@@ -795,23 +795,23 @@
795 rpage-\$requested_page\
796 cpage-\$canonical_page\">" [normalize_result]]}
797
798 ###############################################################################
799
800 fossil test-th-eval "styleHeader {Page Title Here}"
801 test th1-header-1 {$RESULT eq {TH_ERROR: repository unavailable}}
802
803 ###############################################################################
804
805 test_in_checkout th1-header-2 {
806 fossil test-th-eval --open-config "styleHeader {Page Title Here}"
807 } {[regexp -- {<title>Fossil: Page Title Here</title>} $RESULT]}
808
809 ###############################################################################
810
811 fossil test-th-eval "styleFooter"
812 test th1-footer-1 {$RESULT eq {TH_ERROR: repository unavailable}}
813
814 ###############################################################################
815
816 fossil test-th-eval --open-config "styleFooter"
817 test th1-footer-2 {$RESULT eq {}}
@@ -879,44 +879,44 @@
879 test th1-artifact-1 {$RESULT eq \
880 {TH_ERROR: wrong # args: should be "artifact ID ?FILENAME?"}}
881
882 ###############################################################################
883
884 fossil test-th-eval "artifact tip"
885 test th1-artifact-2 {$RESULT eq {TH_ERROR: repository unavailable}}
886
887 ###############################################################################
888
889 test_in_checkout th1-artifact-3 {
890 fossil test-th-eval --open-config "artifact tip"
891 } {[regexp -- {F test/th1\.test [0-9a-f]{40,64}} $RESULT]}
892
893 ###############################################################################
894
895 fossil test-th-eval "artifact 0000000000"
896 test th1-artifact-4 {$RESULT eq {TH_ERROR: repository unavailable}}
897
898 ###############################################################################
899
900 fossil test-th-eval --open-config "artifact 0000000000"
901 test th1-artifact-5 {$RESULT eq {TH_ERROR: name not found}}
902
903 ###############################################################################
904
905 fossil test-th-eval "artifact tip test/th1.test"
906 test th1-artifact-6 {$RESULT eq {TH_ERROR: repository unavailable}}
907
908 ###############################################################################
909
910 test_in_checkout th1-artifact-7 {
911 fossil test-th-eval --open-config "artifact tip test/th1.test"
912 } {[regexp -- {th1-artifact-7} $RESULT]}
913
914 ###############################################################################
915
916 fossil test-th-eval "artifact 0000000000 test/th1.test"
917 test th1-artifact-8 {$RESULT eq {TH_ERROR: repository unavailable}}
918
919 ###############################################################################
920
921 fossil test-th-eval --open-config "artifact 0000000000 test/th1.test"
922 test th1-artifact-9 {$RESULT eq {TH_ERROR: manifest not found}}
@@ -947,12 +947,12 @@
947 }
948 }
949
950 ###############################################################################
951
952 fossil test-th-eval "globalState configuration"
953 test th1-globalState-3 {[string length $RESULT] == 0}
954
955 ###############################################################################
956
957 fossil test-th-eval --open-config "globalState configuration"
958 test th1-globalState-4 {[string length $RESULT] > 0}
@@ -1041,12 +1041,12 @@
1041 fossil test-th-eval "globalState flags"
1042 test th1-globalState-16 {$RESULT eq "0"}
1043
1044 ###############################################################################
1045
1046 fossil test-th-eval "reinitialize; globalState configuration"
1047 test th1-reinitialize-1 {$RESULT eq ""}
1048
1049 ###############################################################################
1050
1051 fossil test-th-eval "reinitialize 1; globalState configuration"
1052 test th1-reinitialize-2 {$RESULT ne ""}
@@ -1056,29 +1056,29 @@
1056 #
1057 # NOTE: This test will fail if the command names are added to TH1, or
1058 # moved from Tcl builds to plain or the reverse. Sorting the
1059 # command lists eliminates a dependence on order.
1060 #
1061 fossil test-th-eval "info commands"
1062 set sorted_result [lsort $RESULT]
1063 protOut "Sorted: $sorted_result"
1064 set base_commands {anoncap anycap array artifact break breakpoint \
1065 builtin_request_js capexpr captureTh1 catch cgiHeaderLine checkout \
1066 combobox continue copybtn date decorate defHeader dir enable_htmlify \
1067 enable_output encode64 error expr for foreach getParameter glob_match \
1068 globalState hascap hasfeature html htmlize http httpize if info \
1069 insertCsrf lappend lindex linecount list llength lsearch markdown nonce \
1070 proc puts query randhex redirect regexp reinitialize rename render \
1071 repository return searchable set setParameter setting stime string \
1072 styleFooter styleHeader styleScript submenu tclReady trace unset \
1073 unversioned uplevel upvar utime verifyCsrf verifyLogin wiki}
1074 set tcl_commands {tclEval tclExpr tclInvoke tclIsSafe tclMakeSafe}
1075 if {$th1Tcl} {
1076 test th1-info-commands-1 {$sorted_result eq [lsort "$base_commands $tcl_commands"]}
1077 } else {
1078 test th1-info-commands-1 {$sorted_result eq [lsort "$base_commands"]}
1079 }
1080
1081 ###############################################################################
1082
1083 fossil test-th-eval "info vars"
1084
@@ -1326,11 +1326,11 @@
1326
1327 ###############################################################################
1328
1329 fossil test-th-eval {string is other 123}
1330 test th1-string-is-4 {$RESULT eq \
1331 "TH_ERROR: Expected alnum, double, integer, or list, got: other"}
1332
1333 ###############################################################################
1334
1335 fossil test-th-eval {string is alnum 123}
1336 test th1-string-is-5 {$RESULT eq "1"}
1337
1338 DDED tools/fake-smtpd.tcl
1339 DDED tools/find-fossil-cgis.tcl
--- test/th1.test
+++ test/th1.test
@@ -795,23 +795,23 @@
795 rpage-\$requested_page\
796 cpage-\$canonical_page\">" [normalize_result]]}
797
798 ###############################################################################
799
800 #fossil test-th-eval "styleHeader {Page Title Here}"
801 #test th1-header-1 {$RESULT eq {TH_ERROR: repository unavailable}}
802
803 ###############################################################################
804
805 test_in_checkout th1-header-2 {
806 fossil test-th-eval --open-config "styleHeader {Page Title Here}"
807 } {[regexp -- {<title>Fossil: Page Title Here</title>} $RESULT]}
808
809 ###############################################################################
810
811 #fossil test-th-eval "styleFooter"
812 #test th1-footer-1 {$RESULT eq {TH_ERROR: repository unavailable}}
813
814 ###############################################################################
815
816 fossil test-th-eval --open-config "styleFooter"
817 test th1-footer-2 {$RESULT eq {}}
@@ -879,44 +879,44 @@
879 test th1-artifact-1 {$RESULT eq \
880 {TH_ERROR: wrong # args: should be "artifact ID ?FILENAME?"}}
881
882 ###############################################################################
883
884 #fossil test-th-eval "artifact tip"
885 #test th1-artifact-2 {$RESULT eq {TH_ERROR: repository unavailable}}
886
887 ###############################################################################
888
889 test_in_checkout th1-artifact-3 {
890 fossil test-th-eval --open-config "artifact tip"
891 } {[regexp -- {F test/th1\.test [0-9a-f]{40,64}} $RESULT]}
892
893 ###############################################################################
894
895 #fossil test-th-eval "artifact 0000000000"
896 #test th1-artifact-4 {$RESULT eq {TH_ERROR: repository unavailable}}
897
898 ###############################################################################
899
900 fossil test-th-eval --open-config "artifact 0000000000"
901 test th1-artifact-5 {$RESULT eq {TH_ERROR: name not found}}
902
903 ###############################################################################
904
905 #fossil test-th-eval "artifact tip test/th1.test"
906 #test th1-artifact-6 {$RESULT eq {TH_ERROR: repository unavailable}}
907
908 ###############################################################################
909
910 test_in_checkout th1-artifact-7 {
911 fossil test-th-eval --open-config "artifact tip test/th1.test"
912 } {[regexp -- {th1-artifact-7} $RESULT]}
913
914 ###############################################################################
915
916 #fossil test-th-eval "artifact 0000000000 test/th1.test"
917 #test th1-artifact-8 {$RESULT eq {TH_ERROR: repository unavailable}}
918
919 ###############################################################################
920
921 fossil test-th-eval --open-config "artifact 0000000000 test/th1.test"
922 test th1-artifact-9 {$RESULT eq {TH_ERROR: manifest not found}}
@@ -947,12 +947,12 @@
947 }
948 }
949
950 ###############################################################################
951
952 #fossil test-th-eval "globalState configuration"
953 #test th1-globalState-3 {[string length $RESULT] == 0}
954
955 ###############################################################################
956
957 fossil test-th-eval --open-config "globalState configuration"
958 test th1-globalState-4 {[string length $RESULT] > 0}
@@ -1041,12 +1041,12 @@
1041 fossil test-th-eval "globalState flags"
1042 test th1-globalState-16 {$RESULT eq "0"}
1043
1044 ###############################################################################
1045
1046 #fossil test-th-eval "reinitialize; globalState configuration"
1047 #test th1-reinitialize-1 {$RESULT eq ""}
1048
1049 ###############################################################################
1050
1051 fossil test-th-eval "reinitialize 1; globalState configuration"
1052 test th1-reinitialize-2 {$RESULT ne ""}
@@ -1056,29 +1056,29 @@
1056 #
1057 # NOTE: This test will fail if the command names are added to TH1, or
1058 # moved from Tcl builds to plain or the reverse. Sorting the
1059 # command lists eliminates a dependence on order.
1060 #
1061 #fossil test-th-eval "info commands"
1062 #set sorted_result [lsort $RESULT]
1063 #protOut "Sorted: $sorted_result"
1064 #set base_commands {anoncap anycap array artifact break breakpoint \
1065 # builtin_request_js capexpr captureTh1 catch cgiHeaderLine checkout \
1066 # combobox continue copybtn date decorate defHeader dir enable_htmlify \
1067 # enable_output encode64 error expr for foreach getParameter glob_match \
1068 # globalState hascap hasfeature html htmlize http httpize if info \
1069 # insertCsrf lappend lindex linecount list llength lsearch markdown nonce \
1070 # proc puts query randhex redirect regexp reinitialize rename render \
1071 # repository return searchable set setParameter setting stime string \
1072 # styleFooter styleHeader styleScript submenu tclReady trace unset \
1073 # unversioned uplevel upvar utime verifyCsrf verifyLogin wiki}
1074 #set tcl_commands {tclEval tclExpr tclInvoke tclIsSafe tclMakeSafe}
1075 #if {$th1Tcl} {
1076 # test th1-info-commands-1 {$sorted_result eq [lsort "$base_commands $tcl_commands"]}
1077 #} else {
1078 # test th1-info-commands-1 {$sorted_result eq [lsort "$base_commands"]}
1079 #}
1080
1081 ###############################################################################
1082
1083 fossil test-th-eval "info vars"
1084
@@ -1326,11 +1326,11 @@
1326
1327 ###############################################################################
1328
1329 fossil test-th-eval {string is other 123}
1330 test th1-string-is-4 {$RESULT eq \
1331 "TH_ERROR: Expected alnum, double, integer, list, or tainted, got: other"}
1332
1333 ###############################################################################
1334
1335 fossil test-th-eval {string is alnum 123}
1336 test th1-string-is-5 {$RESULT eq "1"}
1337
1338 DDED tools/fake-smtpd.tcl
1339 DDED tools/find-fossil-cgis.tcl
--- a/tools/fake-smtpd.tcl
+++ b/tools/fake-smtpd.tcl
@@ -0,0 +1,84 @@
1
+#!/usr/bin/tclsh
2
+#
3
+# This script is a testing aid for working on the Relay notification method
4
+# in Fossil.
5
+#
6
+# This script listens for connections on port 25 or probably some other TCP
7
+# port specified by the "--port N" option. It pretend to be an SMTP server,
8
+# though it does not actually relay any email. Instead, it just prints the
9
+# SMTP conversation on stdout.
10
+#
11
+# If the "--max N" option is used, then the fake SMTP server shuts down
12
+# with an error after receiving N messages from the client. This can be
13
+# used to test retry capabilities in the client.
14
+#
15
+# Suggested Test Procedure For Fossil Relay Notifications
16
+#
17
+# 1. Bring up "fossil ui"
18
+# 2. Configure notification for relay to localhost:8025
19
+# 3. Start this script in a separate window. Something like:
20
+# tclsh fake-smtpd.tcl -port 8025 -max 100
21
+# 4. Send test messages using Fossil
22
+#
23
+proc conn_puts {chan txt} {
24
+ puts "S: $txt"
25
+ puts $chan $txt
26
+ flush $chan
27
+}
28
+set mxMsg 100000000
29
+proc connection {chan ip port} {
30
+ global mxMsg
31
+ set nMsg 0
32
+ puts "*** begin connection from $ip:$port ***"
33
+ conn_puts $chan "220 localhost fake-SMTPD"
34
+ set inData 0
35
+ while {1} {
36
+ set line [string trimright [gets $chan]]
37
+ if {$line eq ""} {
38
+ if {[eof $chan]} break
39
+ }
40
+ puts "C: $line"
41
+ incr nMsg
42
+ if {$inData} {
43
+ if {$line eq "."} {
44
+ set inData 0
45
+ conn_puts $chan "250 Ok"
46
+ }
47
+ } elseif {$nMsg>$mxMsg} {
48
+ conn_puts $chan "999 I'm done!"
49
+ break
50
+ } elseif {[string match "HELO *" $line]} {
51
+ conn_puts $chan "250 Ok"
52
+ } elseif {[string match "EHLO *" $line]} {
53
+ conn_puts $chan "250-SIZE 100000"
54
+ conn_puts $chan "250 HELP"
55
+ } elseif {[string match "DATA*" $line]} {
56
+ conn_puts $chan "354 End data with <CR><LF>.<CR><LF>"
57
+ set inData 1
58
+ } elseif {[string match "QUIT*" $line]} {
59
+ conn_puts $chan "221 Bye"
60
+ break
61
+ } else {
62
+ conn_puts $chan "250 Ok"
63
+ }
64
+ }
65
+ puts "*** connection closed ($nMsg messages) ***"
66
+ close $chan
67
+}
68
+set port 25
69
+set argc [llength $argv]
70
+for {set i 0} {$i<$argc-1} {incr i} {
71
+ set arg [lindex $argv $i]
72
+ if {$arg eq "-port" || $arg eq "--port"} {
73
+ incr i
74
+ set port [lindex $argv $i]
75
+ }
76
+ if {$arg eq "-max" || $arg eq "--max"} {
77
+ incr i
78
+ set mxMsg [lindex $argv $i]
79
+ }
80
+}
81
+puts "listening on localhost:$port"
82
+socket -server connection $port
83
+set forever 0
84
+vwait forever
--- a/tools/fake-smtpd.tcl
+++ b/tools/fake-smtpd.tcl
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/tools/fake-smtpd.tcl
+++ b/tools/fake-smtpd.tcl
@@ -0,0 +1,84 @@
1 #!/usr/bin/tclsh
2 #
3 # This script is a testing aid for working on the Relay notification method
4 # in Fossil.
5 #
6 # This script listens for connections on port 25 or probably some other TCP
7 # port specified by the "--port N" option. It pretend to be an SMTP server,
8 # though it does not actually relay any email. Instead, it just prints the
9 # SMTP conversation on stdout.
10 #
11 # If the "--max N" option is used, then the fake SMTP server shuts down
12 # with an error after receiving N messages from the client. This can be
13 # used to test retry capabilities in the client.
14 #
15 # Suggested Test Procedure For Fossil Relay Notifications
16 #
17 # 1. Bring up "fossil ui"
18 # 2. Configure notification for relay to localhost:8025
19 # 3. Start this script in a separate window. Something like:
20 # tclsh fake-smtpd.tcl -port 8025 -max 100
21 # 4. Send test messages using Fossil
22 #
23 proc conn_puts {chan txt} {
24 puts "S: $txt"
25 puts $chan $txt
26 flush $chan
27 }
28 set mxMsg 100000000
29 proc connection {chan ip port} {
30 global mxMsg
31 set nMsg 0
32 puts "*** begin connection from $ip:$port ***"
33 conn_puts $chan "220 localhost fake-SMTPD"
34 set inData 0
35 while {1} {
36 set line [string trimright [gets $chan]]
37 if {$line eq ""} {
38 if {[eof $chan]} break
39 }
40 puts "C: $line"
41 incr nMsg
42 if {$inData} {
43 if {$line eq "."} {
44 set inData 0
45 conn_puts $chan "250 Ok"
46 }
47 } elseif {$nMsg>$mxMsg} {
48 conn_puts $chan "999 I'm done!"
49 break
50 } elseif {[string match "HELO *" $line]} {
51 conn_puts $chan "250 Ok"
52 } elseif {[string match "EHLO *" $line]} {
53 conn_puts $chan "250-SIZE 100000"
54 conn_puts $chan "250 HELP"
55 } elseif {[string match "DATA*" $line]} {
56 conn_puts $chan "354 End data with <CR><LF>.<CR><LF>"
57 set inData 1
58 } elseif {[string match "QUIT*" $line]} {
59 conn_puts $chan "221 Bye"
60 break
61 } else {
62 conn_puts $chan "250 Ok"
63 }
64 }
65 puts "*** connection closed ($nMsg messages) ***"
66 close $chan
67 }
68 set port 25
69 set argc [llength $argv]
70 for {set i 0} {$i<$argc-1} {incr i} {
71 set arg [lindex $argv $i]
72 if {$arg eq "-port" || $arg eq "--port"} {
73 incr i
74 set port [lindex $argv $i]
75 }
76 if {$arg eq "-max" || $arg eq "--max"} {
77 incr i
78 set mxMsg [lindex $argv $i]
79 }
80 }
81 puts "listening on localhost:$port"
82 socket -server connection $port
83 set forever 0
84 vwait forever
--- a/tools/find-fossil-cgis.tcl
+++ b/tools/find-fossil-cgis.tcl
@@ -0,0 +1,141 @@
1
+#!/usr/bin/tclsh
2
+#
3
+# This script scans a directory hierarchy looking for Fossil CGI files -
4
+# the files that are used to launch Fossil as a CGI program. For each
5
+# such file found, in prints the name of the file and also the file
6
+# content, indented, if the --print option is used.
7
+#
8
+# tclsh find-fossil-cgis.tcl [OPTIONS] DIRECTORY
9
+#
10
+# The argument is the directory from which to begin the search.
11
+#
12
+# OPTIONS can be zero or more of the following:
13
+#
14
+# --has REGEXP Only show the CGI if the body matches REGEXP.
15
+# May be repeated multiple times, in which case
16
+# all must match.
17
+#
18
+# --hasnot REGEXP Only show the CGI if it does NOT match the
19
+# REGEXP.
20
+#
21
+# --print Show the content of the CGI, indented by
22
+# three spaces
23
+#
24
+# --symlink Process DIRECTORY arguments that are symlinks.
25
+# Normally symlinks are silently ignored.
26
+#
27
+# -v Show progress information for debugging
28
+#
29
+# EXAMPLE USE CASES:
30
+#
31
+# Find all CGIs that do not have the "errorlog:" property set
32
+#
33
+# find-fossil-cgis.tcl *.website --has '\nrepository:' \
34
+# --hasnot '\nerrorlog:'
35
+#
36
+# Add the errorlog: property to any CGI that does not have it:
37
+#
38
+# find-fossil-cgis.tcl *.website --has '\nrepository:' \
39
+# --hasnot '\nerrorlog:' | while read x
40
+# do
41
+# echo 'errorlog: /logs/errors.txt' >>$x
42
+# done
43
+#
44
+# Find and print all CGIs that do redirects
45
+#
46
+# find-fossil-cgis.tcl *.website --has '\nredirect:' --print
47
+#
48
+
49
+
50
+# Find the CGIs in directory $dir. Invoke recursively to
51
+# scan subdirectories.
52
+#
53
+proc find_in_one_dir {dir} {
54
+ global HAS HASNOT PRINT V
55
+ if {$V>0} {
56
+ puts "# $dir"
57
+ }
58
+ foreach obj [lsort [glob -nocomplain -directory $dir *]] {
59
+ if {[file isdir $obj]} {
60
+ find_in_one_dir $obj
61
+ continue
62
+ }
63
+ if {![file isfile $obj]} continue
64
+ if {[file size $obj]>5000} continue
65
+ if {![file exec $obj]} continue
66
+ if {![file readable $obj]} continue
67
+ set fd [open $obj rb]
68
+ set txt [read $fd]
69
+ close $fd
70
+ if {![string match #!* $txt]} continue
71
+ if {![regexp {fossil} $txt]} continue
72
+ if {![regexp {\nrepository: } $txt] &&
73
+ ![regexp {\ndirectory: } $txt] &&
74
+ ![regexp {\nredirect: } $txt]} continue
75
+ set ok 1
76
+ foreach re $HAS {
77
+ if {![regexp $re $txt]} {set ok 0; break;}
78
+ }
79
+ if {!$ok} continue
80
+ foreach re $HASNOT {
81
+ if {[regexp $re $txt]} {set ok 0; break;}
82
+ }
83
+ if {!$ok} continue
84
+ #
85
+ # At this point assume we have found a CGI file.
86
+ #
87
+ puts $obj
88
+ if {$PRINT} {
89
+ regsub -all {\n} [string trim $txt] "\n " out
90
+ puts " $out"
91
+ }
92
+ }
93
+}
94
+set HAS [list]
95
+set HASNOT [list]
96
+set PRINT 0
97
+set SYMLINK 0
98
+set V 0
99
+set N [llength $argv]
100
+set DIRLIST [list]
101
+
102
+# First pass: Gather all the command-line arguments but do no
103
+# processing.
104
+#
105
+for {set i 0} {$i<$N} {incr i} {
106
+ set dir [lindex $argv $i]
107
+ if {($dir eq "-has" || $dir eq "--has") && $i<[expr {$N-1}]} {
108
+ incr i
109
+ lappend HAS [lindex $argv $i]
110
+ continue
111
+ }
112
+ if {($dir eq "-hasnot" || $dir eq "--hasnot") && $i<[expr {$N-1}]} {
113
+ incr i
114
+ lappend HASNOT [lindex $argv $i]
115
+ continue
116
+ }
117
+ if {$dir eq "-print" || $dir eq "--print"} {
118
+ set PRINT 1
119
+ continue
120
+ }
121
+ if {$dir eq "-symlink" || $dir eq "--symlink"} {
122
+ set SYMLINK 1
123
+ continue
124
+ }
125
+ if {$dir eq "-v"} {
126
+ set V 1
127
+ continue
128
+ }
129
+ if {[file type $dir]=="directory"} {
130
+ lappend DIRLIST $dir
131
+ }
132
+}
133
+
134
+# Second pass: Process the non-option arguments.
135
+#
136
+foreach dir $DIRLIST {
137
+ set type [file type $dir]
138
+ if {$type eq "directory" || ($SYMLINK && $type eq "link")} {
139
+ find_in_one_dir $dir
140
+ }
141
+}
--- a/tools/find-fossil-cgis.tcl
+++ b/tools/find-fossil-cgis.tcl
@@ -0,0 +1,141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/tools/find-fossil-cgis.tcl
+++ b/tools/find-fossil-cgis.tcl
@@ -0,0 +1,141 @@
1 #!/usr/bin/tclsh
2 #
3 # This script scans a directory hierarchy looking for Fossil CGI files -
4 # the files that are used to launch Fossil as a CGI program. For each
5 # such file found, in prints the name of the file and also the file
6 # content, indented, if the --print option is used.
7 #
8 # tclsh find-fossil-cgis.tcl [OPTIONS] DIRECTORY
9 #
10 # The argument is the directory from which to begin the search.
11 #
12 # OPTIONS can be zero or more of the following:
13 #
14 # --has REGEXP Only show the CGI if the body matches REGEXP.
15 # May be repeated multiple times, in which case
16 # all must match.
17 #
18 # --hasnot REGEXP Only show the CGI if it does NOT match the
19 # REGEXP.
20 #
21 # --print Show the content of the CGI, indented by
22 # three spaces
23 #
24 # --symlink Process DIRECTORY arguments that are symlinks.
25 # Normally symlinks are silently ignored.
26 #
27 # -v Show progress information for debugging
28 #
29 # EXAMPLE USE CASES:
30 #
31 # Find all CGIs that do not have the "errorlog:" property set
32 #
33 # find-fossil-cgis.tcl *.website --has '\nrepository:' \
34 # --hasnot '\nerrorlog:'
35 #
36 # Add the errorlog: property to any CGI that does not have it:
37 #
38 # find-fossil-cgis.tcl *.website --has '\nrepository:' \
39 # --hasnot '\nerrorlog:' | while read x
40 # do
41 # echo 'errorlog: /logs/errors.txt' >>$x
42 # done
43 #
44 # Find and print all CGIs that do redirects
45 #
46 # find-fossil-cgis.tcl *.website --has '\nredirect:' --print
47 #
48
49
50 # Find the CGIs in directory $dir. Invoke recursively to
51 # scan subdirectories.
52 #
53 proc find_in_one_dir {dir} {
54 global HAS HASNOT PRINT V
55 if {$V>0} {
56 puts "# $dir"
57 }
58 foreach obj [lsort [glob -nocomplain -directory $dir *]] {
59 if {[file isdir $obj]} {
60 find_in_one_dir $obj
61 continue
62 }
63 if {![file isfile $obj]} continue
64 if {[file size $obj]>5000} continue
65 if {![file exec $obj]} continue
66 if {![file readable $obj]} continue
67 set fd [open $obj rb]
68 set txt [read $fd]
69 close $fd
70 if {![string match #!* $txt]} continue
71 if {![regexp {fossil} $txt]} continue
72 if {![regexp {\nrepository: } $txt] &&
73 ![regexp {\ndirectory: } $txt] &&
74 ![regexp {\nredirect: } $txt]} continue
75 set ok 1
76 foreach re $HAS {
77 if {![regexp $re $txt]} {set ok 0; break;}
78 }
79 if {!$ok} continue
80 foreach re $HASNOT {
81 if {[regexp $re $txt]} {set ok 0; break;}
82 }
83 if {!$ok} continue
84 #
85 # At this point assume we have found a CGI file.
86 #
87 puts $obj
88 if {$PRINT} {
89 regsub -all {\n} [string trim $txt] "\n " out
90 puts " $out"
91 }
92 }
93 }
94 set HAS [list]
95 set HASNOT [list]
96 set PRINT 0
97 set SYMLINK 0
98 set V 0
99 set N [llength $argv]
100 set DIRLIST [list]
101
102 # First pass: Gather all the command-line arguments but do no
103 # processing.
104 #
105 for {set i 0} {$i<$N} {incr i} {
106 set dir [lindex $argv $i]
107 if {($dir eq "-has" || $dir eq "--has") && $i<[expr {$N-1}]} {
108 incr i
109 lappend HAS [lindex $argv $i]
110 continue
111 }
112 if {($dir eq "-hasnot" || $dir eq "--hasnot") && $i<[expr {$N-1}]} {
113 incr i
114 lappend HASNOT [lindex $argv $i]
115 continue
116 }
117 if {$dir eq "-print" || $dir eq "--print"} {
118 set PRINT 1
119 continue
120 }
121 if {$dir eq "-symlink" || $dir eq "--symlink"} {
122 set SYMLINK 1
123 continue
124 }
125 if {$dir eq "-v"} {
126 set V 1
127 continue
128 }
129 if {[file type $dir]=="directory"} {
130 lappend DIRLIST $dir
131 }
132 }
133
134 # Second pass: Process the non-option arguments.
135 #
136 foreach dir $DIRLIST {
137 set type [file type $dir]
138 if {$type eq "directory" || ($SYMLINK && $type eq "link")} {
139 find_in_one_dir $dir
140 }
141 }
--- tools/fossil-stress.tcl
+++ tools/fossil-stress.tcl
@@ -93,11 +93,11 @@
9393
/fileage
9494
/dir
9595
/tree
9696
/uvlist
9797
/stat
98
- /test_env
98
+ /test-env
9999
/sitemap
100100
/hash-collisions
101101
/artifact_stats
102102
/bloblist
103103
/bigbloblist
104104
--- tools/fossil-stress.tcl
+++ tools/fossil-stress.tcl
@@ -93,11 +93,11 @@
93 /fileage
94 /dir
95 /tree
96 /uvlist
97 /stat
98 /test_env
99 /sitemap
100 /hash-collisions
101 /artifact_stats
102 /bloblist
103 /bigbloblist
104
--- tools/fossil-stress.tcl
+++ tools/fossil-stress.tcl
@@ -93,11 +93,11 @@
93 /fileage
94 /dir
95 /tree
96 /uvlist
97 /stat
98 /test-env
99 /sitemap
100 /hash-collisions
101 /artifact_stats
102 /bloblist
103 /bigbloblist
104
--- www/aboutcgi.wiki
+++ www/aboutcgi.wiki
@@ -67,11 +67,11 @@
6767
<td>The query string that follows the "?" in the URL, if there is one.
6868
</table>
6969
7070
There are other CGI environment variables beyond those listed above.
7171
Many Fossil servers implement the
72
-[https://fossil-scm.org/home/test_env/two/three?abc=xyz|test_env]
72
+[https://fossil-scm.org/home/test-env/two/three?abc=xyz|test-env]
7373
webpage that shows some of the CGI environment
7474
variables that Fossil pays attention to.
7575
7676
In addition to setting various CGI environment variables, if the HTTP
7777
request contains POST content, then the web server relays the POST content
7878
--- www/aboutcgi.wiki
+++ www/aboutcgi.wiki
@@ -67,11 +67,11 @@
67 <td>The query string that follows the "?" in the URL, if there is one.
68 </table>
69
70 There are other CGI environment variables beyond those listed above.
71 Many Fossil servers implement the
72 [https://fossil-scm.org/home/test_env/two/three?abc=xyz|test_env]
73 webpage that shows some of the CGI environment
74 variables that Fossil pays attention to.
75
76 In addition to setting various CGI environment variables, if the HTTP
77 request contains POST content, then the web server relays the POST content
78
--- www/aboutcgi.wiki
+++ www/aboutcgi.wiki
@@ -67,11 +67,11 @@
67 <td>The query string that follows the "?" in the URL, if there is one.
68 </table>
69
70 There are other CGI environment variables beyond those listed above.
71 Many Fossil servers implement the
72 [https://fossil-scm.org/home/test-env/two/three?abc=xyz|test-env]
73 webpage that shows some of the CGI environment
74 variables that Fossil pays attention to.
75
76 In addition to setting various CGI environment variables, if the HTTP
77 request contains POST content, then the web server relays the POST content
78
--- www/cgi.wiki
+++ www/cgi.wiki
@@ -79,10 +79,16 @@
7979
If no repository has such a non-zero repolist-skin setting, then
8080
the repository list is generic HTML without any decoration, with
8181
the page title taken from the <tt>FOSSIL_REPOLIST_TITLE</tt>
8282
environment variable. The variable can be defined in the CGI
8383
control file using the [#setenv|<tt>setenv:</tt>] statement.
84
+
85
+The "Project Description" and "Login-Group" columns on the repolist page
86
+are optional. They are hidden by default. Show them by putting value "1"
87
+in the global settings "show-repolist-desc" and "show-repolist-lg", or
88
+by setting the <tt>FOSSIL_REPOLIST_SHOW</tt> environment variable to
89
+a string that contains substrings "description" and/or "login-group".
8490
8591
The repolist-generated page recurses into subdirectories and will list
8692
all <tt>*.fossil</tt> files found, with the following exceptions:
8793
8894
* Filenames starting with a period are treated as "hidden" and skipped.
8995
--- www/cgi.wiki
+++ www/cgi.wiki
@@ -79,10 +79,16 @@
79 If no repository has such a non-zero repolist-skin setting, then
80 the repository list is generic HTML without any decoration, with
81 the page title taken from the <tt>FOSSIL_REPOLIST_TITLE</tt>
82 environment variable. The variable can be defined in the CGI
83 control file using the [#setenv|<tt>setenv:</tt>] statement.
 
 
 
 
 
 
84
85 The repolist-generated page recurses into subdirectories and will list
86 all <tt>*.fossil</tt> files found, with the following exceptions:
87
88 * Filenames starting with a period are treated as "hidden" and skipped.
89
--- www/cgi.wiki
+++ www/cgi.wiki
@@ -79,10 +79,16 @@
79 If no repository has such a non-zero repolist-skin setting, then
80 the repository list is generic HTML without any decoration, with
81 the page title taken from the <tt>FOSSIL_REPOLIST_TITLE</tt>
82 environment variable. The variable can be defined in the CGI
83 control file using the [#setenv|<tt>setenv:</tt>] statement.
84
85 The "Project Description" and "Login-Group" columns on the repolist page
86 are optional. They are hidden by default. Show them by putting value "1"
87 in the global settings "show-repolist-desc" and "show-repolist-lg", or
88 by setting the <tt>FOSSIL_REPOLIST_SHOW</tt> environment variable to
89 a string that contains substrings "description" and/or "login-group".
90
91 The repolist-generated page recurses into subdirectories and will list
92 all <tt>*.fossil</tt> files found, with the following exceptions:
93
94 * Filenames starting with a period are treated as "hidden" and skipped.
95
+53 -22
--- www/changes.wiki
+++ www/changes.wiki
@@ -1,43 +1,45 @@
11
<title>Change Log</title>
22
3
-<h2 id='v2_26'>Changes for version 2.26 (pending)</h2>
4
-
5
- * Enhancements to [/help?cmd=diff|fossil diff] and similar:
3
+<h2 id='v2_26'>Changes for version 2.26 (pending)</h2><ol>
4
+ <li>Enhancements to [/help?cmd=diff|fossil diff] and similar:
65
<ol type="a">
7
- <li> The --from can optionally accepts a directory name as its argument,
6
+ <li> The --from can optionally accept a directory name as its argument,
87
and uses files under that directory as the baseline for the diff.
98
<li> For "gdiff", if no [/help?cmd=gdiff-command|gdiff-command setting]
109
is defined, Fossil tries to do a --tk diff if "tclsh" and "wish"
1110
are available, or a --by diff if not.
1211
<li> The "Reload" button is added to --tk diffs, to bring the displayed
1312
diff up to date with the latest changes on disk.
1413
<li> Add the "Hide diffs/Show diffs" toggle to web-UI diff pages that show
1514
diffs of multiple files.
1615
</ol>
17
- * Added the [/help?cmd=/ckout|/ckout web page] to provide information
16
+ <li>Added the [/help?cmd=/ckout|/ckout web page] to provide information
1817
about pending changes in a working check-out
19
- * Enhancements to the [/help?cmd=ui|fossil ui] command:
18
+ <li>Enhancements to the [/help?cmd=ui|fossil ui] command:
2019
<ol type="a">
2120
<li> Defaults to using the new [/help?cmd=/ckout|/ckout page] as its
2221
start page. Or, if the new "--from PATH" option is present, the
2322
default start page becomes "/ckout?exbase=PATH".
2423
<li> The new "--extpage FILENAME" option opens the named file as if it
2524
where in a [./serverext.wiki|CGI extension]. Example usage: the
26
- person editing this change log has
25
+ person editing this change log has
2726
"fossil ui --extpage www/changes.wiki" running and hence can
2827
press "Reload" on the web browser to view edits.
28
+ <li> Accept both IPv4 and IPv6 connections on all platforms, including
29
+ Windows and OpenBSD. This also applies to the "fossil server"
30
+ command.
2931
</ol>
30
- * Enhancements to [/help?cmd=merge|fossil merge]:
32
+ <li>Enhancements to [/help?cmd=merge|fossil merge]:
3133
<ol type="a">
3234
<li> Added the [/help?cmd=merge-info|fossil merge-info] command and
3335
especially the --tk option to that command, to provide analysis
3436
of the most recent merge or update operation.
3537
<li> When a merge conflict occurs, a new section is added to the conflict
3638
text that shows Fossil's suggested resolution to the conflict.
3739
</ol>
38
- * Enhancements to [/help?cmd=commit|fossil commit]:
40
+ <li>Enhancements to [/help?cmd=commit|fossil commit]:
3941
<ol type="a">
4042
<li> If Fossil sees potential formatting mistakes (ex: bad hyperlinks)
4143
in the check-in comment, it will alert the developer and give
4244
him or her the opportunity to edit the comment before continuing.
4345
This feature is controllable by the
@@ -47,18 +49,19 @@
4749
<li> Added the ability to sign check-ins with SSH keys.
4850
<li> Issue a warning if a user tries to commit on a check-in where the
4951
branch has been changed.
5052
<li> The interactive checkin comment prompt shows the formatting rules
5153
set for that repository.
54
+ <li> Add the "--editor" option.
5255
</ol>
53
- * Deprecate the --comfmtflags and --comment-format global options and
56
+ <li>Deprecate the --comfmtflags and --comment-format global options and
5457
no longer list them in the built-in help, but keep them working for
5558
backwards compatibility.
5659
Alternative TTY comment formatting can still be specified using the
5760
[/help?cmd=comment-format|comment-format setting], if desired. The
5861
default comment format is now called "canonical", not "legacy".
59
- * Enhancements to the [/help?cmd=/timeline|/timeline page]:
62
+ <li>Enhancements to the [/help?cmd=/timeline|/timeline page]:
6063
<ol type="a">
6164
<li> Added the "ml=" ("Merge-in List") query parameter that works
6265
like "rl=" ("Related List") but adds "mionly" style related
6366
check-ins instead of the full "rel" style.
6467
<li> For "tl=", "rl=", and "ml=", the order of the branches in the
@@ -84,25 +87,32 @@
8487
collapses long runs of check-ins on the same branch into just
8588
end-points.
8689
<li> The p= and d= parameters an reference different check-ins, which
8790
case the timeline shows those check-ins that are both ancestors
8891
of p= and descendants of d=.
92
+ <li> The saturation and intensity of user-specified checkin and branch
93
+ background colors are automatically adjusted to keep the colors
94
+ compatible with the current skin, unless the
95
+ [/help?cmd=raw-bgcolor|raw-bgcolor setting] is turned on.
8996
</ol>
90
- * Added the [/help?cmd=/clusterlist|/clusterlist page] for analysis
97
+ <li>The [/help?cmd=/docfile|/docfile webpage] was added. It works like
98
+ /doc but keeps the title of markdown documents with the document rather
99
+ that moving it up to the page title.
100
+ <li>Added the [/help?cmd=/clusterlist|/clusterlist page] for analysis
91101
and debugging
92
- * Added the "artifact_to_json(NAME)" SQL function that returns a JSON
102
+ <li>Added the "artifact_to_json(NAME)" SQL function that returns a JSON
93103
decoding of the artifact described by NAME.
94
- * Improvements to the [/help?cmd=patch|fossil patch] command:
104
+ <li>Improvements to the [/help?cmd=patch|fossil patch] command:
95105
<ol type="a">
96106
<li> Fix a bug in "fossil patch create" that causes
97107
[/help?cmd=revert|fossil revert] operations that happened
98108
on individualfiles after a [/help?cmd=merge|fossil merge]
99109
to be omitted from the patch.
100110
<li> Added the [/help?cmd=patch|patch alias] command for managing
101111
aliases for remote checkout names.
102112
</ol>
103
- * Enhancements to on-line help and the [/help?cmd=help|fossil help] command:
113
+ <li>Enhancements to on-line help and the [/help?cmd=help|fossil help] command:
104114
<ol type="a">
105115
<li> Add the ability to search the help text, either in the UI
106116
(on the [/help?cmd=/search|/search page]) or from the command-line
107117
(using the "[/help?cmd=search|fossil search -h PATTERN]" command.)
108118
<li> Accepts an optional SUBCOMMAND argument following the
@@ -109,19 +119,40 @@
109119
COMMAND argument and only shows results for the specified
110120
subcommand, not the entire command.
111121
<li> The -u (--usage) option shows only the command-line syntax
112122
<li> The -o (--options) option shows only the command-line options
113123
</ol>
114
- * Added the ability to attach wiki pages to a ticket for extended
115
- descriptions.
116
- * Added the "hash" query parameter to the
124
+ <li>Enhancements to the ticket system:
125
+ <ol type="a">
126
+ <li> Added the ability to attach wiki pages to a ticket for extended
127
+ descriptions.
128
+ <li> Added submenu to the 'View Ticket' page, to use it as
129
+ template for a new ticket.
130
+ <li> Added button 'Submit and New' to create multiple tickets
131
+ in a row.
132
+ <li> Link the version field in ticket view to a matching checkin or tag.
133
+ <li> Show creation time in report and ticket view.
134
+ <li> Show previous comments in edit ticket as reference.
135
+ </ol>
136
+ <li>Added the "hash" query parameter to the
117137
[/help?cmd=/whatis|/whatis webpage].
118
- * Add a "user elevation" [/doc/trunk/www/alerts.md|subscription]
138
+ <li>Add a "user permissions changes" [/doc/trunk/www/alerts.md|subscription]
119139
which alerts subscribers when an admin creates a new user or
120
- adds new permissions to one.
121
- * Diverse minor fixes and additions.
122
-
140
+ when a user's permissions change.
141
+ <li>Show project description on repository list.
142
+ <li>Make [/help?cmd=/chat|/chat] better-behaved during server outages, reducing
143
+ the frequency of reconnection attempts over time and providing feedback
144
+ to the user when the connection is down.
145
+ <li>The [/doc/trunk/www/th1.md|TH1 script language] now makes a distinction
146
+ between [/doc/trunk/www/th1.md#taint|tainted and untainted string values].
147
+ This is a security enhancement that makes it more difficult to write
148
+ custom TH1 scripts that contain XSS or SQL-injection bugs. As part of
149
+ this enhancement, the [/help?cmd=vuln-report|vuln-report] setting was
150
+ added to control what Fossil does when it encounters a potential TH1
151
+ security problem.
152
+ <li>Diverse minor fixes and additions.
153
+</ol>
123154
124155
<h2 id='v2_25'>Changes for version 2.25 (2024-11-06)</h2>
125156
126157
* The "[/help?cmd=ui|fossil ui /]" command now works even for repositories
127158
that have non-ASCII filenames
128159
--- www/changes.wiki
+++ www/changes.wiki
@@ -1,43 +1,45 @@
1 <title>Change Log</title>
2
3 <h2 id='v2_26'>Changes for version 2.26 (pending)</h2>
4
5 * Enhancements to [/help?cmd=diff|fossil diff] and similar:
6 <ol type="a">
7 <li> The --from can optionally accepts a directory name as its argument,
8 and uses files under that directory as the baseline for the diff.
9 <li> For "gdiff", if no [/help?cmd=gdiff-command|gdiff-command setting]
10 is defined, Fossil tries to do a --tk diff if "tclsh" and "wish"
11 are available, or a --by diff if not.
12 <li> The "Reload" button is added to --tk diffs, to bring the displayed
13 diff up to date with the latest changes on disk.
14 <li> Add the "Hide diffs/Show diffs" toggle to web-UI diff pages that show
15 diffs of multiple files.
16 </ol>
17 * Added the [/help?cmd=/ckout|/ckout web page] to provide information
18 about pending changes in a working check-out
19 * Enhancements to the [/help?cmd=ui|fossil ui] command:
20 <ol type="a">
21 <li> Defaults to using the new [/help?cmd=/ckout|/ckout page] as its
22 start page. Or, if the new "--from PATH" option is present, the
23 default start page becomes "/ckout?exbase=PATH".
24 <li> The new "--extpage FILENAME" option opens the named file as if it
25 where in a [./serverext.wiki|CGI extension]. Example usage: the
26 person editing this change log has
27 "fossil ui --extpage www/changes.wiki" running and hence can
28 press "Reload" on the web browser to view edits.
 
 
 
29 </ol>
30 * Enhancements to [/help?cmd=merge|fossil merge]:
31 <ol type="a">
32 <li> Added the [/help?cmd=merge-info|fossil merge-info] command and
33 especially the --tk option to that command, to provide analysis
34 of the most recent merge or update operation.
35 <li> When a merge conflict occurs, a new section is added to the conflict
36 text that shows Fossil's suggested resolution to the conflict.
37 </ol>
38 * Enhancements to [/help?cmd=commit|fossil commit]:
39 <ol type="a">
40 <li> If Fossil sees potential formatting mistakes (ex: bad hyperlinks)
41 in the check-in comment, it will alert the developer and give
42 him or her the opportunity to edit the comment before continuing.
43 This feature is controllable by the
@@ -47,18 +49,19 @@
47 <li> Added the ability to sign check-ins with SSH keys.
48 <li> Issue a warning if a user tries to commit on a check-in where the
49 branch has been changed.
50 <li> The interactive checkin comment prompt shows the formatting rules
51 set for that repository.
 
52 </ol>
53 * Deprecate the --comfmtflags and --comment-format global options and
54 no longer list them in the built-in help, but keep them working for
55 backwards compatibility.
56 Alternative TTY comment formatting can still be specified using the
57 [/help?cmd=comment-format|comment-format setting], if desired. The
58 default comment format is now called "canonical", not "legacy".
59 * Enhancements to the [/help?cmd=/timeline|/timeline page]:
60 <ol type="a">
61 <li> Added the "ml=" ("Merge-in List") query parameter that works
62 like "rl=" ("Related List") but adds "mionly" style related
63 check-ins instead of the full "rel" style.
64 <li> For "tl=", "rl=", and "ml=", the order of the branches in the
@@ -84,25 +87,32 @@
84 collapses long runs of check-ins on the same branch into just
85 end-points.
86 <li> The p= and d= parameters an reference different check-ins, which
87 case the timeline shows those check-ins that are both ancestors
88 of p= and descendants of d=.
 
 
 
 
89 </ol>
90 * Added the [/help?cmd=/clusterlist|/clusterlist page] for analysis
 
 
 
91 and debugging
92 * Added the "artifact_to_json(NAME)" SQL function that returns a JSON
93 decoding of the artifact described by NAME.
94 * Improvements to the [/help?cmd=patch|fossil patch] command:
95 <ol type="a">
96 <li> Fix a bug in "fossil patch create" that causes
97 [/help?cmd=revert|fossil revert] operations that happened
98 on individualfiles after a [/help?cmd=merge|fossil merge]
99 to be omitted from the patch.
100 <li> Added the [/help?cmd=patch|patch alias] command for managing
101 aliases for remote checkout names.
102 </ol>
103 * Enhancements to on-line help and the [/help?cmd=help|fossil help] command:
104 <ol type="a">
105 <li> Add the ability to search the help text, either in the UI
106 (on the [/help?cmd=/search|/search page]) or from the command-line
107 (using the "[/help?cmd=search|fossil search -h PATTERN]" command.)
108 <li> Accepts an optional SUBCOMMAND argument following the
@@ -109,19 +119,40 @@
109 COMMAND argument and only shows results for the specified
110 subcommand, not the entire command.
111 <li> The -u (--usage) option shows only the command-line syntax
112 <li> The -o (--options) option shows only the command-line options
113 </ol>
114 * Added the ability to attach wiki pages to a ticket for extended
115 descriptions.
116 * Added the "hash" query parameter to the
 
 
 
 
 
 
 
 
 
 
117 [/help?cmd=/whatis|/whatis webpage].
118 * Add a "user elevation" [/doc/trunk/www/alerts.md|subscription]
119 which alerts subscribers when an admin creates a new user or
120 adds new permissions to one.
121 * Diverse minor fixes and additions.
122
 
 
 
 
 
 
 
 
 
 
 
123
124 <h2 id='v2_25'>Changes for version 2.25 (2024-11-06)</h2>
125
126 * The "[/help?cmd=ui|fossil ui /]" command now works even for repositories
127 that have non-ASCII filenames
128
--- www/changes.wiki
+++ www/changes.wiki
@@ -1,43 +1,45 @@
1 <title>Change Log</title>
2
3 <h2 id='v2_26'>Changes for version 2.26 (pending)</h2><ol>
4 <li>Enhancements to [/help?cmd=diff|fossil diff] and similar:
 
5 <ol type="a">
6 <li> The --from can optionally accept a directory name as its argument,
7 and uses files under that directory as the baseline for the diff.
8 <li> For "gdiff", if no [/help?cmd=gdiff-command|gdiff-command setting]
9 is defined, Fossil tries to do a --tk diff if "tclsh" and "wish"
10 are available, or a --by diff if not.
11 <li> The "Reload" button is added to --tk diffs, to bring the displayed
12 diff up to date with the latest changes on disk.
13 <li> Add the "Hide diffs/Show diffs" toggle to web-UI diff pages that show
14 diffs of multiple files.
15 </ol>
16 <li>Added the [/help?cmd=/ckout|/ckout web page] to provide information
17 about pending changes in a working check-out
18 <li>Enhancements to the [/help?cmd=ui|fossil ui] command:
19 <ol type="a">
20 <li> Defaults to using the new [/help?cmd=/ckout|/ckout page] as its
21 start page. Or, if the new "--from PATH" option is present, the
22 default start page becomes "/ckout?exbase=PATH".
23 <li> The new "--extpage FILENAME" option opens the named file as if it
24 where in a [./serverext.wiki|CGI extension]. Example usage: the
25 person editing this change log has
26 "fossil ui --extpage www/changes.wiki" running and hence can
27 press "Reload" on the web browser to view edits.
28 <li> Accept both IPv4 and IPv6 connections on all platforms, including
29 Windows and OpenBSD. This also applies to the "fossil server"
30 command.
31 </ol>
32 <li>Enhancements to [/help?cmd=merge|fossil merge]:
33 <ol type="a">
34 <li> Added the [/help?cmd=merge-info|fossil merge-info] command and
35 especially the --tk option to that command, to provide analysis
36 of the most recent merge or update operation.
37 <li> When a merge conflict occurs, a new section is added to the conflict
38 text that shows Fossil's suggested resolution to the conflict.
39 </ol>
40 <li>Enhancements to [/help?cmd=commit|fossil commit]:
41 <ol type="a">
42 <li> If Fossil sees potential formatting mistakes (ex: bad hyperlinks)
43 in the check-in comment, it will alert the developer and give
44 him or her the opportunity to edit the comment before continuing.
45 This feature is controllable by the
@@ -47,18 +49,19 @@
49 <li> Added the ability to sign check-ins with SSH keys.
50 <li> Issue a warning if a user tries to commit on a check-in where the
51 branch has been changed.
52 <li> The interactive checkin comment prompt shows the formatting rules
53 set for that repository.
54 <li> Add the "--editor" option.
55 </ol>
56 <li>Deprecate the --comfmtflags and --comment-format global options and
57 no longer list them in the built-in help, but keep them working for
58 backwards compatibility.
59 Alternative TTY comment formatting can still be specified using the
60 [/help?cmd=comment-format|comment-format setting], if desired. The
61 default comment format is now called "canonical", not "legacy".
62 <li>Enhancements to the [/help?cmd=/timeline|/timeline page]:
63 <ol type="a">
64 <li> Added the "ml=" ("Merge-in List") query parameter that works
65 like "rl=" ("Related List") but adds "mionly" style related
66 check-ins instead of the full "rel" style.
67 <li> For "tl=", "rl=", and "ml=", the order of the branches in the
@@ -84,25 +87,32 @@
87 collapses long runs of check-ins on the same branch into just
88 end-points.
89 <li> The p= and d= parameters an reference different check-ins, which
90 case the timeline shows those check-ins that are both ancestors
91 of p= and descendants of d=.
92 <li> The saturation and intensity of user-specified checkin and branch
93 background colors are automatically adjusted to keep the colors
94 compatible with the current skin, unless the
95 [/help?cmd=raw-bgcolor|raw-bgcolor setting] is turned on.
96 </ol>
97 <li>The [/help?cmd=/docfile|/docfile webpage] was added. It works like
98 /doc but keeps the title of markdown documents with the document rather
99 that moving it up to the page title.
100 <li>Added the [/help?cmd=/clusterlist|/clusterlist page] for analysis
101 and debugging
102 <li>Added the "artifact_to_json(NAME)" SQL function that returns a JSON
103 decoding of the artifact described by NAME.
104 <li>Improvements to the [/help?cmd=patch|fossil patch] command:
105 <ol type="a">
106 <li> Fix a bug in "fossil patch create" that causes
107 [/help?cmd=revert|fossil revert] operations that happened
108 on individualfiles after a [/help?cmd=merge|fossil merge]
109 to be omitted from the patch.
110 <li> Added the [/help?cmd=patch|patch alias] command for managing
111 aliases for remote checkout names.
112 </ol>
113 <li>Enhancements to on-line help and the [/help?cmd=help|fossil help] command:
114 <ol type="a">
115 <li> Add the ability to search the help text, either in the UI
116 (on the [/help?cmd=/search|/search page]) or from the command-line
117 (using the "[/help?cmd=search|fossil search -h PATTERN]" command.)
118 <li> Accepts an optional SUBCOMMAND argument following the
@@ -109,19 +119,40 @@
119 COMMAND argument and only shows results for the specified
120 subcommand, not the entire command.
121 <li> The -u (--usage) option shows only the command-line syntax
122 <li> The -o (--options) option shows only the command-line options
123 </ol>
124 <li>Enhancements to the ticket system:
125 <ol type="a">
126 <li> Added the ability to attach wiki pages to a ticket for extended
127 descriptions.
128 <li> Added submenu to the 'View Ticket' page, to use it as
129 template for a new ticket.
130 <li> Added button 'Submit and New' to create multiple tickets
131 in a row.
132 <li> Link the version field in ticket view to a matching checkin or tag.
133 <li> Show creation time in report and ticket view.
134 <li> Show previous comments in edit ticket as reference.
135 </ol>
136 <li>Added the "hash" query parameter to the
137 [/help?cmd=/whatis|/whatis webpage].
138 <li>Add a "user permissions changes" [/doc/trunk/www/alerts.md|subscription]
139 which alerts subscribers when an admin creates a new user or
140 when a user's permissions change.
141 <li>Show project description on repository list.
142 <li>Make [/help?cmd=/chat|/chat] better-behaved during server outages, reducing
143 the frequency of reconnection attempts over time and providing feedback
144 to the user when the connection is down.
145 <li>The [/doc/trunk/www/th1.md|TH1 script language] now makes a distinction
146 between [/doc/trunk/www/th1.md#taint|tainted and untainted string values].
147 This is a security enhancement that makes it more difficult to write
148 custom TH1 scripts that contain XSS or SQL-injection bugs. As part of
149 this enhancement, the [/help?cmd=vuln-report|vuln-report] setting was
150 added to control what Fossil does when it encounters a potential TH1
151 security problem.
152 <li>Diverse minor fixes and additions.
153 </ol>
154
155 <h2 id='v2_25'>Changes for version 2.25 (2024-11-06)</h2>
156
157 * The "[/help?cmd=ui|fossil ui /]" command now works even for repositories
158 that have non-ASCII filenames
159
+18
--- www/chat.md
+++ www/chat.md
@@ -164,10 +164,28 @@
164164
setting.
165165
166166
This mechanism is similar to [email notification](./alerts.md) except that
167167
the notification is sent via chat instead of via email.
168168
169
+## Quirks
170
+
171
+ - There is no message-editing capability. This is by design and
172
+ desire of `/chat`'s developers.
173
+
174
+ - When `/chat` has problems connecting to the message poller (see
175
+ the next section) it will provide a subtle visual indicator of the
176
+ connection problem - a dotted line along the top of the input
177
+ field. If the connection recovers within a short time, that
178
+ indicator will go away, otherwise it will pop up a loud message
179
+ signifying that the connection to the poller is down and how long
180
+ it will wait to retry (progressively longer, up to some
181
+ maximum). `/chat` will recover automatically when the server is
182
+ reachable. Trying to send messages while the poller connection is
183
+ down is permitted, and the poller will attempt to recover
184
+ immediately if sending of a message succeeds. That applies to any
185
+ operations which send traffic, e.g. if the per-message "toggle
186
+ text mode" button is activated or a message is globally deleted.
169187
170188
## Implementation Details
171189
172190
*You do not need to understand how Fossil chat works in order to use it.
173191
But many developers prefer to know how their tools work.
174192
--- www/chat.md
+++ www/chat.md
@@ -164,10 +164,28 @@
164 setting.
165
166 This mechanism is similar to [email notification](./alerts.md) except that
167 the notification is sent via chat instead of via email.
168
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
169
170 ## Implementation Details
171
172 *You do not need to understand how Fossil chat works in order to use it.
173 But many developers prefer to know how their tools work.
174
--- www/chat.md
+++ www/chat.md
@@ -164,10 +164,28 @@
164 setting.
165
166 This mechanism is similar to [email notification](./alerts.md) except that
167 the notification is sent via chat instead of via email.
168
169 ## Quirks
170
171 - There is no message-editing capability. This is by design and
172 desire of `/chat`'s developers.
173
174 - When `/chat` has problems connecting to the message poller (see
175 the next section) it will provide a subtle visual indicator of the
176 connection problem - a dotted line along the top of the input
177 field. If the connection recovers within a short time, that
178 indicator will go away, otherwise it will pop up a loud message
179 signifying that the connection to the poller is down and how long
180 it will wait to retry (progressively longer, up to some
181 maximum). `/chat` will recover automatically when the server is
182 reachable. Trying to send messages while the poller connection is
183 down is permitted, and the poller will attempt to recover
184 immediately if sending of a message succeeds. That applies to any
185 operations which send traffic, e.g. if the per-message "toggle
186 text mode" button is activated or a message is globally deleted.
187
188 ## Implementation Details
189
190 *You do not need to understand how Fossil chat works in order to use it.
191 But many developers prefer to know how their tools work.
192
+4 -3
--- www/env-opts.md
+++ www/env-opts.md
@@ -154,13 +154,14 @@
154154
`FOSSIL_REPOLIST_TITLE`: The page title of the "Repository List" page
155155
loaded by the `fossil all ui` or `fossil ui /` commands. Only used if
156156
none of the listed repositories has the `repolist_skin` property set.
157157
Can be set from the [CGI control file][cgictlfile].
158158
159
-`FOSSIL_REPOLIST_QUICKFILTER`: Enable or disable the quickfilter on
160
-repository listings, which allows for simple filtering of the listed
161
-repositories.
159
+`FOSSIL_REPOLIST_SHOW`: If this variable exists and has a text value
160
+that contains the substring "description", then the "Project Description"
161
+column appears on the repolist page. If it contains the substring
162
+"login-group", then the Login-Group column appears on the repolist page.
162163
Can be set from the [CGI control file][cgictlfile].
163164
164165
`FOSSIL_USE_SEE_TEXTKEY`: If set, treat the encryption key string for
165166
SEE as text to be hashed into the actual encryption key. This has no
166167
effect if Fossil was not compiled with SEE support enabled.
167168
--- www/env-opts.md
+++ www/env-opts.md
@@ -154,13 +154,14 @@
154 `FOSSIL_REPOLIST_TITLE`: The page title of the "Repository List" page
155 loaded by the `fossil all ui` or `fossil ui /` commands. Only used if
156 none of the listed repositories has the `repolist_skin` property set.
157 Can be set from the [CGI control file][cgictlfile].
158
159 `FOSSIL_REPOLIST_QUICKFILTER`: Enable or disable the quickfilter on
160 repository listings, which allows for simple filtering of the listed
161 repositories.
 
162 Can be set from the [CGI control file][cgictlfile].
163
164 `FOSSIL_USE_SEE_TEXTKEY`: If set, treat the encryption key string for
165 SEE as text to be hashed into the actual encryption key. This has no
166 effect if Fossil was not compiled with SEE support enabled.
167
--- www/env-opts.md
+++ www/env-opts.md
@@ -154,13 +154,14 @@
154 `FOSSIL_REPOLIST_TITLE`: The page title of the "Repository List" page
155 loaded by the `fossil all ui` or `fossil ui /` commands. Only used if
156 none of the listed repositories has the `repolist_skin` property set.
157 Can be set from the [CGI control file][cgictlfile].
158
159 `FOSSIL_REPOLIST_SHOW`: If this variable exists and has a text value
160 that contains the substring "description", then the "Project Description"
161 column appears on the repolist page. If it contains the substring
162 "login-group", then the Login-Group column appears on the repolist page.
163 Can be set from the [CGI control file][cgictlfile].
164
165 `FOSSIL_USE_SEE_TEXTKEY`: If set, treat the encryption key string for
166 SEE as text to be hashed into the actual encryption key. This has no
167 effect if Fossil was not compiled with SEE support enabled.
168
+4 -3
--- www/env-opts.md
+++ www/env-opts.md
@@ -154,13 +154,14 @@
154154
`FOSSIL_REPOLIST_TITLE`: The page title of the "Repository List" page
155155
loaded by the `fossil all ui` or `fossil ui /` commands. Only used if
156156
none of the listed repositories has the `repolist_skin` property set.
157157
Can be set from the [CGI control file][cgictlfile].
158158
159
-`FOSSIL_REPOLIST_QUICKFILTER`: Enable or disable the quickfilter on
160
-repository listings, which allows for simple filtering of the listed
161
-repositories.
159
+`FOSSIL_REPOLIST_SHOW`: If this variable exists and has a text value
160
+that contains the substring "description", then the "Project Description"
161
+column appears on the repolist page. If it contains the substring
162
+"login-group", then the Login-Group column appears on the repolist page.
162163
Can be set from the [CGI control file][cgictlfile].
163164
164165
`FOSSIL_USE_SEE_TEXTKEY`: If set, treat the encryption key string for
165166
SEE as text to be hashed into the actual encryption key. This has no
166167
effect if Fossil was not compiled with SEE support enabled.
167168
--- www/env-opts.md
+++ www/env-opts.md
@@ -154,13 +154,14 @@
154 `FOSSIL_REPOLIST_TITLE`: The page title of the "Repository List" page
155 loaded by the `fossil all ui` or `fossil ui /` commands. Only used if
156 none of the listed repositories has the `repolist_skin` property set.
157 Can be set from the [CGI control file][cgictlfile].
158
159 `FOSSIL_REPOLIST_QUICKFILTER`: Enable or disable the quickfilter on
160 repository listings, which allows for simple filtering of the listed
161 repositories.
 
162 Can be set from the [CGI control file][cgictlfile].
163
164 `FOSSIL_USE_SEE_TEXTKEY`: If set, treat the encryption key string for
165 SEE as text to be hashed into the actual encryption key. This has no
166 effect if Fossil was not compiled with SEE support enabled.
167
--- www/env-opts.md
+++ www/env-opts.md
@@ -154,13 +154,14 @@
154 `FOSSIL_REPOLIST_TITLE`: The page title of the "Repository List" page
155 loaded by the `fossil all ui` or `fossil ui /` commands. Only used if
156 none of the listed repositories has the `repolist_skin` property set.
157 Can be set from the [CGI control file][cgictlfile].
158
159 `FOSSIL_REPOLIST_SHOW`: If this variable exists and has a text value
160 that contains the substring "description", then the "Project Description"
161 column appears on the repolist page. If it contains the substring
162 "login-group", then the Login-Group column appears on the repolist page.
163 Can be set from the [CGI control file][cgictlfile].
164
165 `FOSSIL_USE_SEE_TEXTKEY`: If set, treat the encryption key string for
166 SEE as text to be hashed into the actual encryption key. This has no
167 effect if Fossil was not compiled with SEE support enabled.
168
+2 -2
--- www/loadmgmt.md
+++ www/loadmgmt.md
@@ -77,11 +77,11 @@
7777
7878
The `/home/www/proc` pathname should be adjusted so that the `/proc`
7979
component is at the root of the chroot jail, of course.
8080
8181
To see if the load-average limiter is functional, visit the
82
-[`/test_env`][hte] page of the server to view the current load average.
82
+[`/test-env`][hte] page of the server to view the current load average.
8383
If the value for the load average is greater than zero, that means that
8484
it is possible to activate the load-average limiter on that repository.
8585
If the load average shows exactly "0.0", then that means that Fossil is
8686
unable to find the load average. This can either be because it is in a
8787
`chroot(2)` jail without `/proc` access, or because it is running on a
@@ -88,9 +88,9 @@
8888
system that does not support `getloadavg()` and so the load-average
8989
limiter will not function.
9090
9191
9292
[503]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.5.4
93
-[hte]: /help?cmd=/test_env
93
+[hte]: /help?cmd=/test-env
9494
[gla]: https://linux.die.net/man/3/getloadavg
9595
[lin]: http://www.linode.com
9696
[sh]: ./selfhost.wiki
9797
--- www/loadmgmt.md
+++ www/loadmgmt.md
@@ -77,11 +77,11 @@
77
78 The `/home/www/proc` pathname should be adjusted so that the `/proc`
79 component is at the root of the chroot jail, of course.
80
81 To see if the load-average limiter is functional, visit the
82 [`/test_env`][hte] page of the server to view the current load average.
83 If the value for the load average is greater than zero, that means that
84 it is possible to activate the load-average limiter on that repository.
85 If the load average shows exactly "0.0", then that means that Fossil is
86 unable to find the load average. This can either be because it is in a
87 `chroot(2)` jail without `/proc` access, or because it is running on a
@@ -88,9 +88,9 @@
88 system that does not support `getloadavg()` and so the load-average
89 limiter will not function.
90
91
92 [503]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.5.4
93 [hte]: /help?cmd=/test_env
94 [gla]: https://linux.die.net/man/3/getloadavg
95 [lin]: http://www.linode.com
96 [sh]: ./selfhost.wiki
97
--- www/loadmgmt.md
+++ www/loadmgmt.md
@@ -77,11 +77,11 @@
77
78 The `/home/www/proc` pathname should be adjusted so that the `/proc`
79 component is at the root of the chroot jail, of course.
80
81 To see if the load-average limiter is functional, visit the
82 [`/test-env`][hte] page of the server to view the current load average.
83 If the value for the load average is greater than zero, that means that
84 it is possible to activate the load-average limiter on that repository.
85 If the load average shows exactly "0.0", then that means that Fossil is
86 unable to find the load average. This can either be because it is in a
87 `chroot(2)` jail without `/proc` access, or because it is running on a
@@ -88,9 +88,9 @@
88 system that does not support `getloadavg()` and so the load-average
89 limiter will not function.
90
91
92 [503]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.5.4
93 [hte]: /help?cmd=/test-env
94 [gla]: https://linux.die.net/man/3/getloadavg
95 [lin]: http://www.linode.com
96 [sh]: ./selfhost.wiki
97
+106 -74
--- www/quickstart.wiki
+++ www/quickstart.wiki
@@ -14,20 +14,18 @@
1414
someplace on your $PATH.
1515
1616
You can test that Fossil is present and working like this:
1717
1818
<pre><b>fossil version
19
-This is fossil version 2.13 [309af345ab] 2020-09-28 04:02:55 UTC
19
+This is fossil version 2.25 [8f798279d5] 2024-11-06 12:59:09 UTC
2020
</b></pre>
2121
2222
<h2 id="workflow" name="fslclone">General Work Flow</h2>
2323
24
-Fossil works with repository files (a database in a single file with the project's
25
-complete history) and with checked-out local trees (the working directory
26
-you use to do your work).
27
-(See [./glossary.md | the glossary] for more background.)
28
-The workflow looks like this:
24
+Fossil works with [./glossary.md#repository | repository files]
25
+and [./glossary.md#check-out | check-out directories] using a
26
+workflow like this:
2927
3028
<ul>
3129
<li>Create or clone a repository file. ([/help/init|fossil init] or
3230
[/help/clone | fossil clone])
3331
<li>Check out a local tree. ([/help/open | fossil open])
@@ -41,29 +39,54 @@
4139
The following sections give a brief overview of these
4240
operations.
4341
4442
<h2 id="new">Starting A New Project</h2>
4543
46
-To start a new project with fossil create a new empty repository
47
-this way: ([/help/init | more info])
44
+To start a new project with Fossil, [/help/init | create a new empty repository]:
4845
4946
<pre><b>fossil init</b> <i>repository-filename</i>
5047
</pre>
5148
5249
You can name the database anything you like, and you can place it anywhere in the filesystem.
53
-The <tt>.fossil</tt> extension is traditional but only required if you are going to use the
54
-<tt>[/help/server | fossil server DIRECTORY]</tt> feature.”
50
+The <tt>.fossil</tt> extension is traditional, but it is only required if you are going to use the
51
+<tt>[/help/server | fossil server DIRECTORY]</tt> feature.
52
+
53
+Next, do something along the lines of:
54
+
55
+<pre>
56
+<b>mkdir -p ~/src/project/trunk</b>
57
+<b>cd ~/src/project/trunk</b>
58
+<b>fossil open</b> <i>repository-filename</i>
59
+<b>fossil add</b> foo.c bar.h qux.md
60
+<b>fossil commit</b>
61
+</pre>
62
+
63
+If your project directory already exists, obviating the <b>mkdir</b>
64
+step, you will instead need to add the <tt>--force</tt> flag to the
65
+<b>open</b> command to authorize Fossil to open the repo into a
66
+non-empty checkout directory. (This is to avoid accidental opens into,
67
+for example, your home directory.)
68
+
69
+The convention of naming your checkout directory after a long-lived
70
+branch name like "trunk" is in support of Fossil's ability to have as
71
+many open checkouts as you like. This author frequently has additional
72
+checkout directories named <tt>../release</tt>, <tt>../scratch</tt>,
73
+etc. The release directory is open to the branch of the same name, while
74
+the scratch directory is used when disturbing one of the other
75
+long-lived checkout directories is undesireable, as when performing a
76
+[/help/bisect | bisect] operation.
77
+
5578
5679
<h2 id="clone">Cloning An Existing Repository</h2>
5780
5881
Most fossil operations interact with a repository that is on the
5982
local disk drive, not on a remote system. Hence, before accessing
6083
a remote repository it is necessary to make a local copy of that
61
-repository. Making a local copy of a remote repository is called
62
-"cloning".
84
+repository, a process called
85
+"[/help/clone | cloning]".
6386
64
-Clone a remote repository as follows: ([/help/clone | more info])
87
+This is done as follows:
6588
6689
<pre><b>fossil clone</b> <i>URL repository-filename</i>
6790
</pre>
6891
6992
The <i>URL</i> specifies the fossil repository
@@ -81,12 +104,20 @@
81104
100% complete...
82105
Extra delta compression...
83106
Vacuuming the database...
84107
project-id: 94259BB9F186226D80E49D1FA2DB29F935CCA0333
85108
server-id: 016595e9043054038a9ea9bc526d7f33f7ac0e42
86
-admin-user: exampleuser (password is "yoWgDR42iv")>
109
+admin-user: exampleuser (intial remote-access password is "yoWgDR42iv")>
87110
</b></pre>
111
+
112
+This <i>exampleuser</i> will be used by Fossil as the author of commits when
113
+you checkin changes to the repository. It is also used by Fossil when you
114
+make your repository available to others using the built-in server mode by
115
+running <tt>[/help/server | fossil server]</tt> and will also be used when
116
+running <tt>[/help/ui | fossil ui]</tt> to view the repository through
117
+the Fossil UI. See the quick start topic for setting up a
118
+<a href="#server">server</a> for more details.
88119
89120
If the remote repository requires a login, include a
90121
userid in the URL like this:
91122
92123
<pre><b>fossil clone https://</b><i>remoteuserid</i><b>@www.example.org/ myclone.fossil</b></pre>
@@ -127,26 +158,23 @@
127158
128159
<h2 id="checkout">Checking Out A Local Tree</h2>
129160
130161
To work on a project in fossil, you need to check out a local
131162
copy of the source tree. Create the directory you want to be
132
-the root of your tree and cd into that directory. Then
133
-do this: ([/help/open | more info])
163
+the root of your tree, <tt>cd</tt> into that directory, and then:
134164
135165
<pre><b>fossil open</b> <i>repository-filename</i></pre>
136166
137
-for example:
167
+For example:
138168
139169
<pre><b>fossil open ../myclone.fossil
140170
BUILD.txt
141171
COPYRIGHT-BSD2.txt
142172
README.md
143173
144174
</tt></b></pre>
145175
146
-(or "fossil open ..\myclone.fossil" on Windows).
147
-
148176
This leaves you with the newest version of the tree
149177
checked out.
150178
From anywhere underneath the root of your local tree, you
151179
can type commands like the following to find out the status of
152180
your local tree:
@@ -294,41 +322,60 @@
294322
295323
This will get you started on identifying checkins. The
296324
<a href="./checkin_names.wiki">Checkin Names document</a> is a complete reference, including
297325
how timestamps can also be used.
298326
299
-<h2 id="config">Configuring Your Local Repository</h2>
327
+<h2 id="config">Accessing Your Local Repository's Web User Interface</h2>
300328
301
-When you create a new repository, either by cloning an existing
302
-project or create a new project of your own, you usually want to do some
303
-local configuration. This is easily accomplished using the web-server
304
-that is built into fossil. Start the fossil web server like this:
305
-([/help/ui | more info])
329
+After you create a new repository, you usually want to do some local
330
+configuration. This is most easily accomplished by firing up the Fossil
331
+UI:
306332
307333
<pre>
308334
<b>fossil ui</b> <i>repository-filename</i>
309335
</pre>
310336
311
-You can omit the <i>repository-filename</i> from the command above
337
+You can shorten that to just [/help/ui | <b>fossil ui</b>]
312338
if you are inside a checked-out local tree.
313339
314
-This starts a web server then automatically launches your
315
-web browser and makes it point to this web server. If your system
316
-has an unusual configuration, fossil might not be able to figure out
317
-how to start your web browser. In that case, first tell fossil
318
-where to find your web browser using a command like this:
340
+This command starts an internal web server, after which Fossil
341
+automatically launches your default browser, pointed at itself,
342
+presenting a special view of the repository, its web user interface.
343
+
344
+You may override Fossil's logic for selecting the default browser so:
319345
320346
<pre>
321347
<b>fossil setting web-browser</b> <i>path-to-web-browser</i>
322348
</pre>
323349
324
-By default, fossil does not require a login for HTTP connections
325
-coming in from the IP loopback address 127.0.0.1. You can, and perhaps
326
-should, change this after you create a few users.
350
+When launched this way, Fossil binds its internal web server to the IP
351
+loopback address, 127.0.0.1, which it treats specially, bypassing all
352
+user controls, effectively giving visitors the
353
+[./caps/admin-v-setup.md#apsu | all-powerful Setup capabliity].
354
+
355
+Why is that a good idea, you ask? Because it is a safe
356
+presumption that only someone with direct file access to the repository
357
+database file could be using the resulting web interface. Anyone who can
358
+modify the repo DB directly could give themselves any and all access
359
+with a SQL query, or even by direct file manipulation; no amount of
360
+access control matters to such a user.
361
+
362
+(Contrast the [#server | many <i>other</i> ways] of setting Fossil up
363
+as an HTTP server, where the repo DB is on the other side of the HTTP
364
+server wall, inaccessible by all means other than Fossil's own
365
+mediation. For this reason, the "localhost bypasses access control"
366
+policy does <i>not</i> apply to these other interfaces. That is a very
367
+good thing, since without this difference in policy, it would be unsafe
368
+to bind a [/help?cmd=server | <b>fossil server</b>] instance to
369
+localhost on a high-numbered port and then reverse-proxy it out to the
370
+world via HTTPS, a practice this author does engage in, with confidence.)
327371
328
-When you are finished configuring, just press Control-C or use
329
-the <b>kill</b> command to shut down the mini-server.
372
+Once you are finished configuring Fossil, you may safely Control-C out
373
+of the <b>fossil&nbsp;ui</b> command to shut down this privileged
374
+built-in web server. Moreover, you may by grace of SQLite do this <i>at
375
+any time</i>: all changes are either committed durably to the repo DB or
376
+rolled back, in their totality. This includes configuration changes.
330377
331378
<h2 id="sharing">Sharing Changes</h2>
332379
333380
When [./concepts.wiki#workflow|autosync] is turned off,
334381
the changes you [/help/commit | commit] are only
@@ -384,16 +431,12 @@
384431
them and fails if local changes exist unless the <tt>--force</tt>
385432
flag is used.
386433
387434
<h2 id="branch" name="merge">Branching And Merging</h2>
388435
389
-Use the --branch option to the [/help/commit | commit] command
390
-to start a new branch. Note that in Fossil, branches are normally
391
-created when you commit, not before you start editing. You can
392
-use the [/help/branch | branch new] command to create a new branch
393
-before you start editing, if you want, but most people just wait
394
-until they are ready to commit.
436
+Use the --branch option to the [/help/commit | commit] command to start
437
+a new branch at the point of need. ([./gitusers.md#bneed | Contrast git].)
395438
396439
To merge two branches back together, first
397440
[/help/update | update] to the branch you want to merge into.
398441
Then do a [/help/merge|merge] of the other branch that you want to incorporate
399442
the changes from. For example, to merge "featureX" changes into "trunk"
@@ -442,55 +485,44 @@
442485
level of undo/redo.
443486
444487
445488
<h2 id="server">Setting Up A Server</h2>
446489
447
-Fossil can act as a stand-alone web server using one of these
448
-commands:
490
+In addition to the inward-facing <b>fossil ui</b> mode covered [#config
491
+| above], Fossil can also act as an outward-facing web server:
449492
450493
<pre>
451494
<b>[/help/server | fossil server]</b> <i>repository-filename</i>
452
-<b>[/help/ui | fossil ui]</b> <i>repository-filename</i>
453495
</pre>
454496
455
-The <i>repository-filename</i> can be omitted when these commands
456
-are run from within an open check-out, which is a particularly useful
457
-shortcut with the <b>fossil ui</b> command.
458
-
459
-The <b>ui</b> command is intended for accessing the web user interface
460
-from a local desktop. (We sometimes call this mode "Fossil UI.")
461
-The <b>ui</b> command differs from the
462
-<b>server</b> command by binding to the loopback IP
463
-address only (thus making the web UI visible only on the
464
-local machine) and by automatically starting your default web browser,
465
-pointing it at the running UI
466
-server. The localhost restriction exists because it also gives anyone
467
-who can access the resulting web UI full control over the
468
-repository. (This is the [./caps/admin-v-setup.md#apsu | all-powerful
469
-Setup capabliity].)
470
-
471
-For cross-machine collaboration, use the <b>server</b> command instead,
472
-which binds on all IP addresses, does not try to start a web browser,
473
-and enforces [./caps/ | Fossil's role-based access control system].
474
-
475
-Servers are also easily configured as:
497
+Just as with <b>fossil ui</b>, you may omit the
498
+<i>repository-filename</i> parameter when running this from within an open
499
+check-out.
500
+
501
+<i>Unlike</i> <b>fossil ui</b> mode, Fossil binds to all network
502
+interfaces by default in this mode, and it enforces the configured
503
+[./caps/ | role-based access controls]. Further, because it is meant to
504
+provide external web service, it doesn't try to launch a local web
505
+browser pointing to a "Fossil UI" presentation; external visitors see
506
+your repository's configured home page instead.
507
+
508
+To serve varying needs, there are additional ways to serve a Fossil repo
509
+to external users:
476510
477511
<ul>
512
+<li>[./server/any/cgi.md|CGI], as used by Fossil's [./selfhost.wiki |
513
+ self-hosting repositories]
514
+<li>[./server/any/scgi.md|SCGI]
478515
<li>[./server/any/inetd.md|inetd]
479516
<li>[./server/debian/service.md|systemd]
480
-<li>[./server/any/cgi.md|CGI]
481
-<li>[./server/any/scgi.md|SCGI]
482517
</ul>
483518
484519
…along with [./server/#matrix | several other options].
485520
486
-The [./selfhost.wiki | self-hosting fossil repositories] use
487
-CGI.
488
-
489
-You might <i>need</i> to set up a server, whether you know it yet or
490
-not. See the [./server/whyuseaserver.wiki | Benefits of a Fossil Server]
491
-article for details.
521
+We recommend that you read the [./server/whyuseaserver.wiki | Benefits
522
+of a Fossil Server] article, because you might <i>need</i> to do this
523
+and not yet know it.
492524
493525
<h2 id="proxy">HTTP Proxies</h2>
494526
495527
If you are behind a restrictive firewall that requires you to use
496528
an HTTP proxy to reach the internet, then you can configure the proxy
497529
--- www/quickstart.wiki
+++ www/quickstart.wiki
@@ -14,20 +14,18 @@
14 someplace on your $PATH.
15
16 You can test that Fossil is present and working like this:
17
18 <pre><b>fossil version
19 This is fossil version 2.13 [309af345ab] 2020-09-28 04:02:55 UTC
20 </b></pre>
21
22 <h2 id="workflow" name="fslclone">General Work Flow</h2>
23
24 Fossil works with repository files (a database in a single file with the project's
25 complete history) and with checked-out local trees (the working directory
26 you use to do your work).
27 (See [./glossary.md | the glossary] for more background.)
28 The workflow looks like this:
29
30 <ul>
31 <li>Create or clone a repository file. ([/help/init|fossil init] or
32 [/help/clone | fossil clone])
33 <li>Check out a local tree. ([/help/open | fossil open])
@@ -41,29 +39,54 @@
41 The following sections give a brief overview of these
42 operations.
43
44 <h2 id="new">Starting A New Project</h2>
45
46 To start a new project with fossil create a new empty repository
47 this way: ([/help/init | more info])
48
49 <pre><b>fossil init</b> <i>repository-filename</i>
50 </pre>
51
52 You can name the database anything you like, and you can place it anywhere in the filesystem.
53 The <tt>.fossil</tt> extension is traditional but only required if you are going to use the
54 <tt>[/help/server | fossil server DIRECTORY]</tt> feature.”
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
56 <h2 id="clone">Cloning An Existing Repository</h2>
57
58 Most fossil operations interact with a repository that is on the
59 local disk drive, not on a remote system. Hence, before accessing
60 a remote repository it is necessary to make a local copy of that
61 repository. Making a local copy of a remote repository is called
62 "cloning".
63
64 Clone a remote repository as follows: ([/help/clone | more info])
65
66 <pre><b>fossil clone</b> <i>URL repository-filename</i>
67 </pre>
68
69 The <i>URL</i> specifies the fossil repository
@@ -81,12 +104,20 @@
81 100% complete...
82 Extra delta compression...
83 Vacuuming the database...
84 project-id: 94259BB9F186226D80E49D1FA2DB29F935CCA0333
85 server-id: 016595e9043054038a9ea9bc526d7f33f7ac0e42
86 admin-user: exampleuser (password is "yoWgDR42iv")>
87 </b></pre>
 
 
 
 
 
 
 
 
88
89 If the remote repository requires a login, include a
90 userid in the URL like this:
91
92 <pre><b>fossil clone https://</b><i>remoteuserid</i><b>@www.example.org/ myclone.fossil</b></pre>
@@ -127,26 +158,23 @@
127
128 <h2 id="checkout">Checking Out A Local Tree</h2>
129
130 To work on a project in fossil, you need to check out a local
131 copy of the source tree. Create the directory you want to be
132 the root of your tree and cd into that directory. Then
133 do this: ([/help/open | more info])
134
135 <pre><b>fossil open</b> <i>repository-filename</i></pre>
136
137 for example:
138
139 <pre><b>fossil open ../myclone.fossil
140 BUILD.txt
141 COPYRIGHT-BSD2.txt
142 README.md
143
144 </tt></b></pre>
145
146 (or "fossil open ..\myclone.fossil" on Windows).
147
148 This leaves you with the newest version of the tree
149 checked out.
150 From anywhere underneath the root of your local tree, you
151 can type commands like the following to find out the status of
152 your local tree:
@@ -294,41 +322,60 @@
294
295 This will get you started on identifying checkins. The
296 <a href="./checkin_names.wiki">Checkin Names document</a> is a complete reference, including
297 how timestamps can also be used.
298
299 <h2 id="config">Configuring Your Local Repository</h2>
300
301 When you create a new repository, either by cloning an existing
302 project or create a new project of your own, you usually want to do some
303 local configuration. This is easily accomplished using the web-server
304 that is built into fossil. Start the fossil web server like this:
305 ([/help/ui | more info])
306
307 <pre>
308 <b>fossil ui</b> <i>repository-filename</i>
309 </pre>
310
311 You can omit the <i>repository-filename</i> from the command above
312 if you are inside a checked-out local tree.
313
314 This starts a web server then automatically launches your
315 web browser and makes it point to this web server. If your system
316 has an unusual configuration, fossil might not be able to figure out
317 how to start your web browser. In that case, first tell fossil
318 where to find your web browser using a command like this:
319
320 <pre>
321 <b>fossil setting web-browser</b> <i>path-to-web-browser</i>
322 </pre>
323
324 By default, fossil does not require a login for HTTP connections
325 coming in from the IP loopback address 127.0.0.1. You can, and perhaps
326 should, change this after you create a few users.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
327
328 When you are finished configuring, just press Control-C or use
329 the <b>kill</b> command to shut down the mini-server.
 
 
 
330
331 <h2 id="sharing">Sharing Changes</h2>
332
333 When [./concepts.wiki#workflow|autosync] is turned off,
334 the changes you [/help/commit | commit] are only
@@ -384,16 +431,12 @@
384 them and fails if local changes exist unless the <tt>--force</tt>
385 flag is used.
386
387 <h2 id="branch" name="merge">Branching And Merging</h2>
388
389 Use the --branch option to the [/help/commit | commit] command
390 to start a new branch. Note that in Fossil, branches are normally
391 created when you commit, not before you start editing. You can
392 use the [/help/branch | branch new] command to create a new branch
393 before you start editing, if you want, but most people just wait
394 until they are ready to commit.
395
396 To merge two branches back together, first
397 [/help/update | update] to the branch you want to merge into.
398 Then do a [/help/merge|merge] of the other branch that you want to incorporate
399 the changes from. For example, to merge "featureX" changes into "trunk"
@@ -442,55 +485,44 @@
442 level of undo/redo.
443
444
445 <h2 id="server">Setting Up A Server</h2>
446
447 Fossil can act as a stand-alone web server using one of these
448 commands:
449
450 <pre>
451 <b>[/help/server | fossil server]</b> <i>repository-filename</i>
452 <b>[/help/ui | fossil ui]</b> <i>repository-filename</i>
453 </pre>
454
455 The <i>repository-filename</i> can be omitted when these commands
456 are run from within an open check-out, which is a particularly useful
457 shortcut with the <b>fossil ui</b> command.
458
459 The <b>ui</b> command is intended for accessing the web user interface
460 from a local desktop. (We sometimes call this mode "Fossil UI.")
461 The <b>ui</b> command differs from the
462 <b>server</b> command by binding to the loopback IP
463 address only (thus making the web UI visible only on the
464 local machine) and by automatically starting your default web browser,
465 pointing it at the running UI
466 server. The localhost restriction exists because it also gives anyone
467 who can access the resulting web UI full control over the
468 repository. (This is the [./caps/admin-v-setup.md#apsu | all-powerful
469 Setup capabliity].)
470
471 For cross-machine collaboration, use the <b>server</b> command instead,
472 which binds on all IP addresses, does not try to start a web browser,
473 and enforces [./caps/ | Fossil's role-based access control system].
474
475 Servers are also easily configured as:
476
477 <ul>
 
 
 
478 <li>[./server/any/inetd.md|inetd]
479 <li>[./server/debian/service.md|systemd]
480 <li>[./server/any/cgi.md|CGI]
481 <li>[./server/any/scgi.md|SCGI]
482 </ul>
483
484 …along with [./server/#matrix | several other options].
485
486 The [./selfhost.wiki | self-hosting fossil repositories] use
487 CGI.
488
489 You might <i>need</i> to set up a server, whether you know it yet or
490 not. See the [./server/whyuseaserver.wiki | Benefits of a Fossil Server]
491 article for details.
492
493 <h2 id="proxy">HTTP Proxies</h2>
494
495 If you are behind a restrictive firewall that requires you to use
496 an HTTP proxy to reach the internet, then you can configure the proxy
497
--- www/quickstart.wiki
+++ www/quickstart.wiki
@@ -14,20 +14,18 @@
14 someplace on your $PATH.
15
16 You can test that Fossil is present and working like this:
17
18 <pre><b>fossil version
19 This is fossil version 2.25 [8f798279d5] 2024-11-06 12:59:09 UTC
20 </b></pre>
21
22 <h2 id="workflow" name="fslclone">General Work Flow</h2>
23
24 Fossil works with [./glossary.md#repository | repository files]
25 and [./glossary.md#check-out | check-out directories] using a
26 workflow like this:
 
 
27
28 <ul>
29 <li>Create or clone a repository file. ([/help/init|fossil init] or
30 [/help/clone | fossil clone])
31 <li>Check out a local tree. ([/help/open | fossil open])
@@ -41,29 +39,54 @@
39 The following sections give a brief overview of these
40 operations.
41
42 <h2 id="new">Starting A New Project</h2>
43
44 To start a new project with Fossil, [/help/init | create a new empty repository]:
 
45
46 <pre><b>fossil init</b> <i>repository-filename</i>
47 </pre>
48
49 You can name the database anything you like, and you can place it anywhere in the filesystem.
50 The <tt>.fossil</tt> extension is traditional, but it is only required if you are going to use the
51 <tt>[/help/server | fossil server DIRECTORY]</tt> feature.
52
53 Next, do something along the lines of:
54
55 <pre>
56 <b>mkdir -p ~/src/project/trunk</b>
57 <b>cd ~/src/project/trunk</b>
58 <b>fossil open</b> <i>repository-filename</i>
59 <b>fossil add</b> foo.c bar.h qux.md
60 <b>fossil commit</b>
61 </pre>
62
63 If your project directory already exists, obviating the <b>mkdir</b>
64 step, you will instead need to add the <tt>--force</tt> flag to the
65 <b>open</b> command to authorize Fossil to open the repo into a
66 non-empty checkout directory. (This is to avoid accidental opens into,
67 for example, your home directory.)
68
69 The convention of naming your checkout directory after a long-lived
70 branch name like "trunk" is in support of Fossil's ability to have as
71 many open checkouts as you like. This author frequently has additional
72 checkout directories named <tt>../release</tt>, <tt>../scratch</tt>,
73 etc. The release directory is open to the branch of the same name, while
74 the scratch directory is used when disturbing one of the other
75 long-lived checkout directories is undesireable, as when performing a
76 [/help/bisect | bisect] operation.
77
78
79 <h2 id="clone">Cloning An Existing Repository</h2>
80
81 Most fossil operations interact with a repository that is on the
82 local disk drive, not on a remote system. Hence, before accessing
83 a remote repository it is necessary to make a local copy of that
84 repository, a process called
85 "[/help/clone | cloning]".
86
87 This is done as follows:
88
89 <pre><b>fossil clone</b> <i>URL repository-filename</i>
90 </pre>
91
92 The <i>URL</i> specifies the fossil repository
@@ -81,12 +104,20 @@
104 100% complete...
105 Extra delta compression...
106 Vacuuming the database...
107 project-id: 94259BB9F186226D80E49D1FA2DB29F935CCA0333
108 server-id: 016595e9043054038a9ea9bc526d7f33f7ac0e42
109 admin-user: exampleuser (intial remote-access password is "yoWgDR42iv")>
110 </b></pre>
111
112 This <i>exampleuser</i> will be used by Fossil as the author of commits when
113 you checkin changes to the repository. It is also used by Fossil when you
114 make your repository available to others using the built-in server mode by
115 running <tt>[/help/server | fossil server]</tt> and will also be used when
116 running <tt>[/help/ui | fossil ui]</tt> to view the repository through
117 the Fossil UI. See the quick start topic for setting up a
118 <a href="#server">server</a> for more details.
119
120 If the remote repository requires a login, include a
121 userid in the URL like this:
122
123 <pre><b>fossil clone https://</b><i>remoteuserid</i><b>@www.example.org/ myclone.fossil</b></pre>
@@ -127,26 +158,23 @@
158
159 <h2 id="checkout">Checking Out A Local Tree</h2>
160
161 To work on a project in fossil, you need to check out a local
162 copy of the source tree. Create the directory you want to be
163 the root of your tree, <tt>cd</tt> into that directory, and then:
 
164
165 <pre><b>fossil open</b> <i>repository-filename</i></pre>
166
167 For example:
168
169 <pre><b>fossil open ../myclone.fossil
170 BUILD.txt
171 COPYRIGHT-BSD2.txt
172 README.md
173
174 </tt></b></pre>
175
 
 
176 This leaves you with the newest version of the tree
177 checked out.
178 From anywhere underneath the root of your local tree, you
179 can type commands like the following to find out the status of
180 your local tree:
@@ -294,41 +322,60 @@
322
323 This will get you started on identifying checkins. The
324 <a href="./checkin_names.wiki">Checkin Names document</a> is a complete reference, including
325 how timestamps can also be used.
326
327 <h2 id="config">Accessing Your Local Repository's Web User Interface</h2>
328
329 After you create a new repository, you usually want to do some local
330 configuration. This is most easily accomplished by firing up the Fossil
331 UI:
 
 
332
333 <pre>
334 <b>fossil ui</b> <i>repository-filename</i>
335 </pre>
336
337 You can shorten that to just [/help/ui | <b>fossil ui</b>]
338 if you are inside a checked-out local tree.
339
340 This command starts an internal web server, after which Fossil
341 automatically launches your default browser, pointed at itself,
342 presenting a special view of the repository, its web user interface.
343
344 You may override Fossil's logic for selecting the default browser so:
345
346 <pre>
347 <b>fossil setting web-browser</b> <i>path-to-web-browser</i>
348 </pre>
349
350 When launched this way, Fossil binds its internal web server to the IP
351 loopback address, 127.0.0.1, which it treats specially, bypassing all
352 user controls, effectively giving visitors the
353 [./caps/admin-v-setup.md#apsu | all-powerful Setup capabliity].
354
355 Why is that a good idea, you ask? Because it is a safe
356 presumption that only someone with direct file access to the repository
357 database file could be using the resulting web interface. Anyone who can
358 modify the repo DB directly could give themselves any and all access
359 with a SQL query, or even by direct file manipulation; no amount of
360 access control matters to such a user.
361
362 (Contrast the [#server | many <i>other</i> ways] of setting Fossil up
363 as an HTTP server, where the repo DB is on the other side of the HTTP
364 server wall, inaccessible by all means other than Fossil's own
365 mediation. For this reason, the "localhost bypasses access control"
366 policy does <i>not</i> apply to these other interfaces. That is a very
367 good thing, since without this difference in policy, it would be unsafe
368 to bind a [/help?cmd=server | <b>fossil server</b>] instance to
369 localhost on a high-numbered port and then reverse-proxy it out to the
370 world via HTTPS, a practice this author does engage in, with confidence.)
371
372 Once you are finished configuring Fossil, you may safely Control-C out
373 of the <b>fossil&nbsp;ui</b> command to shut down this privileged
374 built-in web server. Moreover, you may by grace of SQLite do this <i>at
375 any time</i>: all changes are either committed durably to the repo DB or
376 rolled back, in their totality. This includes configuration changes.
377
378 <h2 id="sharing">Sharing Changes</h2>
379
380 When [./concepts.wiki#workflow|autosync] is turned off,
381 the changes you [/help/commit | commit] are only
@@ -384,16 +431,12 @@
431 them and fails if local changes exist unless the <tt>--force</tt>
432 flag is used.
433
434 <h2 id="branch" name="merge">Branching And Merging</h2>
435
436 Use the --branch option to the [/help/commit | commit] command to start
437 a new branch at the point of need. ([./gitusers.md#bneed | Contrast git].)
 
 
 
 
438
439 To merge two branches back together, first
440 [/help/update | update] to the branch you want to merge into.
441 Then do a [/help/merge|merge] of the other branch that you want to incorporate
442 the changes from. For example, to merge "featureX" changes into "trunk"
@@ -442,55 +485,44 @@
485 level of undo/redo.
486
487
488 <h2 id="server">Setting Up A Server</h2>
489
490 In addition to the inward-facing <b>fossil ui</b> mode covered [#config
491 | above], Fossil can also act as an outward-facing web server:
492
493 <pre>
494 <b>[/help/server | fossil server]</b> <i>repository-filename</i>
 
495 </pre>
496
497 Just as with <b>fossil ui</b>, you may omit the
498 <i>repository-filename</i> parameter when running this from within an open
499 check-out.
500
501 <i>Unlike</i> <b>fossil ui</b> mode, Fossil binds to all network
502 interfaces by default in this mode, and it enforces the configured
503 [./caps/ | role-based access controls]. Further, because it is meant to
504 provide external web service, it doesn't try to launch a local web
505 browser pointing to a "Fossil UI" presentation; external visitors see
506 your repository's configured home page instead.
507
508 To serve varying needs, there are additional ways to serve a Fossil repo
509 to external users:
 
 
 
 
 
 
 
 
510
511 <ul>
512 <li>[./server/any/cgi.md|CGI], as used by Fossil's [./selfhost.wiki |
513 self-hosting repositories]
514 <li>[./server/any/scgi.md|SCGI]
515 <li>[./server/any/inetd.md|inetd]
516 <li>[./server/debian/service.md|systemd]
 
 
517 </ul>
518
519 …along with [./server/#matrix | several other options].
520
521 We recommend that you read the [./server/whyuseaserver.wiki | Benefits
522 of a Fossil Server] article, because you might <i>need</i> to do this
523 and not yet know it.
 
 
 
524
525 <h2 id="proxy">HTTP Proxies</h2>
526
527 If you are behind a restrictive firewall that requires you to use
528 an HTTP proxy to reach the internet, then you can configure the proxy
529
--- www/server/debian/service.md
+++ www/server/debian/service.md
@@ -52,11 +52,11 @@
5252
suitable for sharing a Fossil repo to a workgroup on a private LAN.
5353
5454
To do this, write the following in
5555
`~/.local/share/systemd/user/fossil.service`:
5656
57
-```dosini
57
+> ```dosini
5858
[Unit]
5959
Description=Fossil user server
6060
After=network-online.target
6161
6262
[Service]
@@ -164,11 +164,11 @@
164164
It’s more complicated, but it has some nice properties.
165165
166166
We first need to define the privileged socket listener by writing
167167
`/etc/systemd/system/fossil.socket`:
168168
169
-```dosini
169
+> ```dosini
170170
[Unit]
171171
Description=Fossil socket
172172
173173
[Socket]
174174
Accept=yes
@@ -189,11 +189,11 @@
189189
documentation](../any/inetd.md).
190190
191191
Next, create the service definition file in that same directory as
192192
`[email protected]`:
193193
194
-```dosini
194
+> ```dosini
195195
[Unit]
196196
Description=Fossil socket server
197197
After=network-online.target
198198
199199
[Service]
200200
--- www/server/debian/service.md
+++ www/server/debian/service.md
@@ -52,11 +52,11 @@
52 suitable for sharing a Fossil repo to a workgroup on a private LAN.
53
54 To do this, write the following in
55 `~/.local/share/systemd/user/fossil.service`:
56
57 ```dosini
58 [Unit]
59 Description=Fossil user server
60 After=network-online.target
61
62 [Service]
@@ -164,11 +164,11 @@
164 It’s more complicated, but it has some nice properties.
165
166 We first need to define the privileged socket listener by writing
167 `/etc/systemd/system/fossil.socket`:
168
169 ```dosini
170 [Unit]
171 Description=Fossil socket
172
173 [Socket]
174 Accept=yes
@@ -189,11 +189,11 @@
189 documentation](../any/inetd.md).
190
191 Next, create the service definition file in that same directory as
192 `[email protected]`:
193
194 ```dosini
195 [Unit]
196 Description=Fossil socket server
197 After=network-online.target
198
199 [Service]
200
--- www/server/debian/service.md
+++ www/server/debian/service.md
@@ -52,11 +52,11 @@
52 suitable for sharing a Fossil repo to a workgroup on a private LAN.
53
54 To do this, write the following in
55 `~/.local/share/systemd/user/fossil.service`:
56
57 > ```dosini
58 [Unit]
59 Description=Fossil user server
60 After=network-online.target
61
62 [Service]
@@ -164,11 +164,11 @@
164 It’s more complicated, but it has some nice properties.
165
166 We first need to define the privileged socket listener by writing
167 `/etc/systemd/system/fossil.socket`:
168
169 > ```dosini
170 [Unit]
171 Description=Fossil socket
172
173 [Socket]
174 Accept=yes
@@ -189,11 +189,11 @@
189 documentation](../any/inetd.md).
190
191 Next, create the service definition file in that same directory as
192 `[email protected]`:
193
194 > ```dosini
195 [Unit]
196 Description=Fossil socket server
197 After=network-online.target
198
199 [Service]
200
--- www/server/windows/service.md
+++ www/server/windows/service.md
@@ -55,11 +55,11 @@
5555
for temporary files is exempted from such scanning. Ordinarily, this
5656
will be a subdirectory named "fossil" in the temporary directory given
5757
by the Windows GetTempPath(...) API, [namely](https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-gettemppathw#remarks)
5858
the value of the first existing environment variable from `%TMP%`, `%TEMP%`,
5959
`%USERPROFILE%`, and `%SystemRoot%`; you can look for their actual values in
60
-your system by accessing the `/test_env` webpage.
60
+your system by accessing the `/test-env` webpage.
6161
Excluding this subdirectory will avoid certain rare failures where the
6262
fossil.exe process is unable to use the directory normally during a scan.
6363
6464
### <a id='PowerShell'></a>Advanced service installation using PowerShell
6565
6666
--- www/server/windows/service.md
+++ www/server/windows/service.md
@@ -55,11 +55,11 @@
55 for temporary files is exempted from such scanning. Ordinarily, this
56 will be a subdirectory named "fossil" in the temporary directory given
57 by the Windows GetTempPath(...) API, [namely](https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-gettemppathw#remarks)
58 the value of the first existing environment variable from `%TMP%`, `%TEMP%`,
59 `%USERPROFILE%`, and `%SystemRoot%`; you can look for their actual values in
60 your system by accessing the `/test_env` webpage.
61 Excluding this subdirectory will avoid certain rare failures where the
62 fossil.exe process is unable to use the directory normally during a scan.
63
64 ### <a id='PowerShell'></a>Advanced service installation using PowerShell
65
66
--- www/server/windows/service.md
+++ www/server/windows/service.md
@@ -55,11 +55,11 @@
55 for temporary files is exempted from such scanning. Ordinarily, this
56 will be a subdirectory named "fossil" in the temporary directory given
57 by the Windows GetTempPath(...) API, [namely](https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-gettemppathw#remarks)
58 the value of the first existing environment variable from `%TMP%`, `%TEMP%`,
59 `%USERPROFILE%`, and `%SystemRoot%`; you can look for their actual values in
60 your system by accessing the `/test-env` webpage.
61 Excluding this subdirectory will avoid certain rare failures where the
62 fossil.exe process is unable to use the directory normally during a scan.
63
64 ### <a id='PowerShell'></a>Advanced service installation using PowerShell
65
66
--- www/serverext.wiki
+++ www/serverext.wiki
@@ -189,11 +189,11 @@
189189
to find more detail about what each of the above variables mean and how
190190
they are used.
191191
Live listings of the values of some or all of these environment variables
192192
can be found at links like these:
193193
194
- * [https://fossil-scm.org/home/test_env]
194
+ * [https://fossil-scm.org/home/test-env]
195195
* [https://sqlite.org/src/ext/checklist/top/env]
196196
197197
In addition to the standard CGI environment variables listed above,
198198
Fossil adds the following:
199199
200200
--- www/serverext.wiki
+++ www/serverext.wiki
@@ -189,11 +189,11 @@
189 to find more detail about what each of the above variables mean and how
190 they are used.
191 Live listings of the values of some or all of these environment variables
192 can be found at links like these:
193
194 * [https://fossil-scm.org/home/test_env]
195 * [https://sqlite.org/src/ext/checklist/top/env]
196
197 In addition to the standard CGI environment variables listed above,
198 Fossil adds the following:
199
200
--- www/serverext.wiki
+++ www/serverext.wiki
@@ -189,11 +189,11 @@
189 to find more detail about what each of the above variables mean and how
190 they are used.
191 Live listings of the values of some or all of these environment variables
192 can be found at links like these:
193
194 * [https://fossil-scm.org/home/test-env]
195 * [https://sqlite.org/src/ext/checklist/top/env]
196
197 In addition to the standard CGI environment variables listed above,
198 Fossil adds the following:
199
200
+128 -5
--- www/th1.md
+++ www/th1.md
@@ -68,11 +68,11 @@
6868
are removed from each token by the command parser.) The third token
6969
is the `puts "hello"`, with its whitespace and newlines. The fourth token
7070
is `else` and the fifth and last token is `puts "world"`.
7171
7272
The `if` command evaluates its first argument (the second token)
73
-as an expression, and if that expression is true, evaluates its
73
+as an expression, and if that expression is true, it evaluates its
7474
second argument (the third token) as a TH1 script.
7575
If the expression is false and the third argument is `else`, then
7676
the fourth argument is evaluated as a TH1 expression.
7777
7878
So, you see, even though the example above spans five lines, it is really
@@ -106,10 +106,49 @@
106106
$repository "" info trunk]]] end]
107107
108108
Those backslashes allow the command to wrap nicely within a standard
109109
terminal width while telling the interpreter to consider those three
110110
lines as a single command.
111
+
112
+<a id="taint"></a>Tainted And Untainted Strings
113
+-----------------------------------------------
114
+
115
+Beginning with Fossil version 2.26 (circa 2025), TH1 distinguishes between
116
+"tainted" and "untainted" strings. Tainted strings are strings that are
117
+derived from user inputs that might contain text that is designed to subvert
118
+the script. Untainted strings are known to come from secure sources and
119
+are assumed to contain no malicious content.
120
+
121
+Beginning with Fossil version 2.26, and depending on the value of the
122
+[vuln-report setting](/help?cmd=vuln-report), TH1 will prevent tainted
123
+strings from being used in ways that might lead to XSS or SQL-injection
124
+attacks. This feature helps to ensure that XSS and SQL-injection
125
+vulnerabilities are not *accidentally* added to Fossil when
126
+custom TH1 scripts for headers or footers or tickets are added to a
127
+repository. Note that the tainted/untainted distinction in strings does
128
+not make it impossible to introduce XSS and SQL-injections vulnerabilities
129
+using poorly-written TH1 scripts; it just makes it more difficult and
130
+less likely to happen by accident. Developers must still consider the
131
+security implications TH1 customizations they add to Fossil, and take
132
+appropriate precautions when writing custom TH1. Peer review of TH1
133
+script changes is encouraged.
134
+
135
+In Fossil version 2.26, if the vuln-report setting is set to "block"
136
+or "fatal", the [html](#html) and [query](#query) TH1 commands will
137
+fail with an error if their argument is a tainted string. This helps
138
+to prevent XSS and SQL-injection attacks, respectively. Note that
139
+the default value of the vuln-report setting is "log", which allows those
140
+commands to continue working and only writes a warning message into the
141
+error log. <b>Future versions of Fossil may change the default value
142
+of the vuln-report setting to "block" or "fatal".</b> Fossil users
143
+with customized TH1 scripts are encouraged to audit their customizations
144
+and fix any potential vulnerabilities soon, so as to avoid breakage
145
+caused by future upgrades. <b>Future versions of Fossil might also
146
+place additional restrictions on the use of tainted strings.</b>
147
+For example, it is likely that future versions of Fossil will disallow
148
+using tainted strings as script, for example as the body of a "for"
149
+loop or of a "proc".
111150
112151
113152
Summary of Core TH1 Commands
114153
----------------------------
115154
@@ -147,10 +186,13 @@
147186
* string last NEEDLE HAYSTACK ?START-INDEX?
148187
* string match PATTERN STRING
149188
* string length STRING
150189
* string range STRING FIRST LAST
151190
* string repeat STRING COUNT
191
+ * string trim STRING
192
+ * string trimleft STRING
193
+ * string trimright STRING
152194
* unset VARNAME
153195
* uplevel ?LEVEL? SCRIPT
154196
* upvar ?FRAME? OTHERVAR MYVAR ?OTHERVAR MYVAR?
155197
156198
All of the above commands work as in the original Tcl. Refer to the
@@ -214,17 +256,19 @@
214256
* [stime](#stime)
215257
* [styleHeader](#styleHeader)
216258
* [styleFooter](#styleFooter)
217259
* [styleScript](#styleScript)
218260
* [submenu](#submenu)
261
+ * [taint](#taintCmd)
219262
* [tclEval](#tclEval)
220263
* [tclExpr](#tclExpr)
221264
* [tclInvoke](#tclInvoke)
222265
* [tclIsSafe](#tclIsSafe)
223266
* [tclMakeSafe](#tclMakeSafe)
224267
* [tclReady](#tclReady)
225268
* [trace](#trace)
269
+ * [untaint](#untaintCmd)
226270
* [unversioned content](#unversioned_content)
227271
* [unversioned list](#unversioned_list)
228272
* [utime](#utime)
229273
* [verifyCsrf](#verifyCsrf)
230274
* [verifyLogin](#verifyLogin)
@@ -527,11 +571,25 @@
527571
<a id="html"></a>TH1 html Command
528572
-----------------------------------
529573
530574
* html STRING
531575
532
-Outputs the STRING escaped for HTML.
576
+Outputs the STRING literally. It is assumed that STRING contains
577
+valid HTML, or that if STRING contains any characters that are
578
+significant to HTML (such as `<`, `>`, `'`, or `&`) have already
579
+been escaped, perhaps by the [htmlize](#htmlize) command. Use the
580
+[puts](#puts) command to output text that might contain unescaped
581
+HTML markup.
582
+
583
+**Beware of XSS attacks!** If the STRING value to the html command
584
+can be controlled by a hostile user, then he might be able to sneak
585
+in malicious HTML or Javascript which could result in a
586
+cross-site scripting (XSS) attack. Be careful that all text that
587
+in STRING that might come from user input has been sanitized by the
588
+[htmlize](#htmlize) command or similar. In recent versions of Fossil,
589
+the STRING value must be [untainted](#taint) or else the "html" command
590
+will fail.
533591
534592
<a id="htmlize"></a>TH1 htmlize Command
535593
-----------------------------------------
536594
537595
* htmlize STRING
@@ -595,12 +653,16 @@
595653
<a id="puts"></a>TH1 puts Command
596654
-----------------------------------
597655
598656
* puts STRING
599657
600
-Outputs the STRING unchanged, where "unchanged" might, depending on
601
-the context, mean "with some characters escaped for HTML."
658
+Outputs STRING. Characters within STRING that have special meaning
659
+in HTML are escaped prior to being output. Thus is it safe for STRING
660
+to be derived from user inputs. See also the [html](#html) command
661
+which behaves similarly except does not escape HTML markup. This
662
+command ("puts") is safe to use on [tainted strings](#taint), but the "html"
663
+command is not.
602664
603665
<a id="query"></a>TH1 query Command
604666
-------------------------------------
605667
606668
* query ?-nocomplain? SQL CODE
@@ -608,11 +670,44 @@
608670
Runs the SQL query given by the SQL argument. For each row in the result
609671
set, run CODE.
610672
611673
In SQL, parameters such as $var are filled in using the value of variable
612674
"var". Result values are stored in variables with the column name prior
613
-to each invocation of CODE.
675
+to each invocation of CODE. The names of the variables in which results
676
+are stored can be controlled using "AS name" clauses in the SQL. As
677
+the database will often contain content that originates from untrusted
678
+users, all result values are marked as [tainted](#taint).
679
+
680
+**Beware of SQL injections in the `query` command!**
681
+The SQL argument to the query command should always be literal SQL
682
+text enclosed in {...}. The SQL argument should never be a double-quoted
683
+string or the value of a \$variable, as those constructs can lead to
684
+an SQL Injection attack. If you need to include the values of one or
685
+more TH1 variables as part of the SQL, then put \$variable inside the
686
+{...}. The \$variable keyword will then get passed down into the SQLite
687
+parser which knows to look up the value of \$variable in the TH1 symbol
688
+table. For example:
689
+
690
+~~~
691
+ query {SELECT res FROM tab1 WHERE key=$mykey} {...}
692
+~~~
693
+
694
+SQLite will see the \$mykey token in the SQL and will know to resolve it
695
+to the value of the "mykey" TH1 variable, safely and without the possibility
696
+of SQL injection. The following is unsafe:
697
+
698
+~~~
699
+ query "SELECT res FROM tab1 WHERE key='$mykey'" {...} ;# <-- UNSAFE!
700
+~~~
701
+
702
+In this second example, TH1 does the expansion of `$mykey` prior to passing
703
+the text down into SQLite. So if `$mykey` contains a single-quote character,
704
+followed by additional hostile text, that will result in an SQL injection.
705
+
706
+To help guard against SQL-injections, recent versions of Fossil require
707
+that the SQL argument be [untainted](#taint) or else the "query" command
708
+will fail.
614709
615710
<a id="randhex"></a>TH1 randhex Command
616711
-----------------------------------------
617712
618713
* randhex N
@@ -638,10 +733,12 @@
638733
* regexp ?-nocase? ?--? exp string
639734
640735
Checks the string against the specified regular expression and returns
641736
non-zero if it matches. If the regular expression is invalid or cannot
642737
be compiled, an error will be generated.
738
+
739
+See [fossil grep](./grep.md) for details on the regexp syntax.
643740
644741
<a id="reinitialize"></a>TH1 reinitialize Command
645742
---------------------------------------------------
646743
647744
* reinitialize ?FLAGS?
@@ -741,10 +838,24 @@
741838
742839
* submenu link LABEL URL
743840
744841
Add hyperlink to the submenu of the current page.
745842
843
+<a id="taintCmd"></a>TH1 taint Command
844
+-----------------------------------------
845
+
846
+ * taint STRING
847
+
848
+This command returns a copy of STRING that has been marked as
849
+[tainted](#taint). Tainted strings are strings which might be
850
+controlled by an attacker and might contain hostile inputs and
851
+are thus unsafe to use in certain contexts. For example, tainted
852
+strings should not be output as part of a webpage as they might
853
+contain rogue HTML or Javascript that could lead to an XSS
854
+vulnerability. Similarly, tainted strings should not be run as
855
+SQL since they might contain an SQL-injection vulerability.
856
+
746857
<a id="tclEval"></a>TH1 tclEval Command
747858
-----------------------------------------
748859
749860
**This command requires the Tcl integration feature.**
750861
@@ -812,10 +923,22 @@
812923
813924
* trace STRING
814925
815926
Generates a TH1 trace message if TH1 tracing is enabled.
816927
928
+<a id="untaintCmd"></a>TH1 taint Command
929
+-----------------------------------------
930
+
931
+ * untaint STRING
932
+
933
+This command returns a copy of STRING that has been marked as
934
+[untainted](#taint). Untainted strings are strings which are
935
+believed to be free of potentially hostile content. Use this
936
+command with caution, as it overwrites the tainted-string protection
937
+mechanisms that are built into TH1. If you do not understand all
938
+the implications of executing this command, then do not use it.
939
+
817940
<a id="unversioned_content"></a>TH1 unversioned content Command
818941
-----------------------------------------------------------------
819942
820943
* unversioned content FILENAME
821944
822945
--- www/th1.md
+++ www/th1.md
@@ -68,11 +68,11 @@
68 are removed from each token by the command parser.) The third token
69 is the `puts "hello"`, with its whitespace and newlines. The fourth token
70 is `else` and the fifth and last token is `puts "world"`.
71
72 The `if` command evaluates its first argument (the second token)
73 as an expression, and if that expression is true, evaluates its
74 second argument (the third token) as a TH1 script.
75 If the expression is false and the third argument is `else`, then
76 the fourth argument is evaluated as a TH1 expression.
77
78 So, you see, even though the example above spans five lines, it is really
@@ -106,10 +106,49 @@
106 $repository "" info trunk]]] end]
107
108 Those backslashes allow the command to wrap nicely within a standard
109 terminal width while telling the interpreter to consider those three
110 lines as a single command.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
112
113 Summary of Core TH1 Commands
114 ----------------------------
115
@@ -147,10 +186,13 @@
147 * string last NEEDLE HAYSTACK ?START-INDEX?
148 * string match PATTERN STRING
149 * string length STRING
150 * string range STRING FIRST LAST
151 * string repeat STRING COUNT
 
 
 
152 * unset VARNAME
153 * uplevel ?LEVEL? SCRIPT
154 * upvar ?FRAME? OTHERVAR MYVAR ?OTHERVAR MYVAR?
155
156 All of the above commands work as in the original Tcl. Refer to the
@@ -214,17 +256,19 @@
214 * [stime](#stime)
215 * [styleHeader](#styleHeader)
216 * [styleFooter](#styleFooter)
217 * [styleScript](#styleScript)
218 * [submenu](#submenu)
 
219 * [tclEval](#tclEval)
220 * [tclExpr](#tclExpr)
221 * [tclInvoke](#tclInvoke)
222 * [tclIsSafe](#tclIsSafe)
223 * [tclMakeSafe](#tclMakeSafe)
224 * [tclReady](#tclReady)
225 * [trace](#trace)
 
226 * [unversioned content](#unversioned_content)
227 * [unversioned list](#unversioned_list)
228 * [utime](#utime)
229 * [verifyCsrf](#verifyCsrf)
230 * [verifyLogin](#verifyLogin)
@@ -527,11 +571,25 @@
527 <a id="html"></a>TH1 html Command
528 -----------------------------------
529
530 * html STRING
531
532 Outputs the STRING escaped for HTML.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
533
534 <a id="htmlize"></a>TH1 htmlize Command
535 -----------------------------------------
536
537 * htmlize STRING
@@ -595,12 +653,16 @@
595 <a id="puts"></a>TH1 puts Command
596 -----------------------------------
597
598 * puts STRING
599
600 Outputs the STRING unchanged, where "unchanged" might, depending on
601 the context, mean "with some characters escaped for HTML."
 
 
 
 
602
603 <a id="query"></a>TH1 query Command
604 -------------------------------------
605
606 * query ?-nocomplain? SQL CODE
@@ -608,11 +670,44 @@
608 Runs the SQL query given by the SQL argument. For each row in the result
609 set, run CODE.
610
611 In SQL, parameters such as $var are filled in using the value of variable
612 "var". Result values are stored in variables with the column name prior
613 to each invocation of CODE.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
614
615 <a id="randhex"></a>TH1 randhex Command
616 -----------------------------------------
617
618 * randhex N
@@ -638,10 +733,12 @@
638 * regexp ?-nocase? ?--? exp string
639
640 Checks the string against the specified regular expression and returns
641 non-zero if it matches. If the regular expression is invalid or cannot
642 be compiled, an error will be generated.
 
 
643
644 <a id="reinitialize"></a>TH1 reinitialize Command
645 ---------------------------------------------------
646
647 * reinitialize ?FLAGS?
@@ -741,10 +838,24 @@
741
742 * submenu link LABEL URL
743
744 Add hyperlink to the submenu of the current page.
745
 
 
 
 
 
 
 
 
 
 
 
 
 
 
746 <a id="tclEval"></a>TH1 tclEval Command
747 -----------------------------------------
748
749 **This command requires the Tcl integration feature.**
750
@@ -812,10 +923,22 @@
812
813 * trace STRING
814
815 Generates a TH1 trace message if TH1 tracing is enabled.
816
 
 
 
 
 
 
 
 
 
 
 
 
817 <a id="unversioned_content"></a>TH1 unversioned content Command
818 -----------------------------------------------------------------
819
820 * unversioned content FILENAME
821
822
--- www/th1.md
+++ www/th1.md
@@ -68,11 +68,11 @@
68 are removed from each token by the command parser.) The third token
69 is the `puts "hello"`, with its whitespace and newlines. The fourth token
70 is `else` and the fifth and last token is `puts "world"`.
71
72 The `if` command evaluates its first argument (the second token)
73 as an expression, and if that expression is true, it evaluates its
74 second argument (the third token) as a TH1 script.
75 If the expression is false and the third argument is `else`, then
76 the fourth argument is evaluated as a TH1 expression.
77
78 So, you see, even though the example above spans five lines, it is really
@@ -106,10 +106,49 @@
106 $repository "" info trunk]]] end]
107
108 Those backslashes allow the command to wrap nicely within a standard
109 terminal width while telling the interpreter to consider those three
110 lines as a single command.
111
112 <a id="taint"></a>Tainted And Untainted Strings
113 -----------------------------------------------
114
115 Beginning with Fossil version 2.26 (circa 2025), TH1 distinguishes between
116 "tainted" and "untainted" strings. Tainted strings are strings that are
117 derived from user inputs that might contain text that is designed to subvert
118 the script. Untainted strings are known to come from secure sources and
119 are assumed to contain no malicious content.
120
121 Beginning with Fossil version 2.26, and depending on the value of the
122 [vuln-report setting](/help?cmd=vuln-report), TH1 will prevent tainted
123 strings from being used in ways that might lead to XSS or SQL-injection
124 attacks. This feature helps to ensure that XSS and SQL-injection
125 vulnerabilities are not *accidentally* added to Fossil when
126 custom TH1 scripts for headers or footers or tickets are added to a
127 repository. Note that the tainted/untainted distinction in strings does
128 not make it impossible to introduce XSS and SQL-injections vulnerabilities
129 using poorly-written TH1 scripts; it just makes it more difficult and
130 less likely to happen by accident. Developers must still consider the
131 security implications TH1 customizations they add to Fossil, and take
132 appropriate precautions when writing custom TH1. Peer review of TH1
133 script changes is encouraged.
134
135 In Fossil version 2.26, if the vuln-report setting is set to "block"
136 or "fatal", the [html](#html) and [query](#query) TH1 commands will
137 fail with an error if their argument is a tainted string. This helps
138 to prevent XSS and SQL-injection attacks, respectively. Note that
139 the default value of the vuln-report setting is "log", which allows those
140 commands to continue working and only writes a warning message into the
141 error log. <b>Future versions of Fossil may change the default value
142 of the vuln-report setting to "block" or "fatal".</b> Fossil users
143 with customized TH1 scripts are encouraged to audit their customizations
144 and fix any potential vulnerabilities soon, so as to avoid breakage
145 caused by future upgrades. <b>Future versions of Fossil might also
146 place additional restrictions on the use of tainted strings.</b>
147 For example, it is likely that future versions of Fossil will disallow
148 using tainted strings as script, for example as the body of a "for"
149 loop or of a "proc".
150
151
152 Summary of Core TH1 Commands
153 ----------------------------
154
@@ -147,10 +186,13 @@
186 * string last NEEDLE HAYSTACK ?START-INDEX?
187 * string match PATTERN STRING
188 * string length STRING
189 * string range STRING FIRST LAST
190 * string repeat STRING COUNT
191 * string trim STRING
192 * string trimleft STRING
193 * string trimright STRING
194 * unset VARNAME
195 * uplevel ?LEVEL? SCRIPT
196 * upvar ?FRAME? OTHERVAR MYVAR ?OTHERVAR MYVAR?
197
198 All of the above commands work as in the original Tcl. Refer to the
@@ -214,17 +256,19 @@
256 * [stime](#stime)
257 * [styleHeader](#styleHeader)
258 * [styleFooter](#styleFooter)
259 * [styleScript](#styleScript)
260 * [submenu](#submenu)
261 * [taint](#taintCmd)
262 * [tclEval](#tclEval)
263 * [tclExpr](#tclExpr)
264 * [tclInvoke](#tclInvoke)
265 * [tclIsSafe](#tclIsSafe)
266 * [tclMakeSafe](#tclMakeSafe)
267 * [tclReady](#tclReady)
268 * [trace](#trace)
269 * [untaint](#untaintCmd)
270 * [unversioned content](#unversioned_content)
271 * [unversioned list](#unversioned_list)
272 * [utime](#utime)
273 * [verifyCsrf](#verifyCsrf)
274 * [verifyLogin](#verifyLogin)
@@ -527,11 +571,25 @@
571 <a id="html"></a>TH1 html Command
572 -----------------------------------
573
574 * html STRING
575
576 Outputs the STRING literally. It is assumed that STRING contains
577 valid HTML, or that if STRING contains any characters that are
578 significant to HTML (such as `<`, `>`, `'`, or `&`) have already
579 been escaped, perhaps by the [htmlize](#htmlize) command. Use the
580 [puts](#puts) command to output text that might contain unescaped
581 HTML markup.
582
583 **Beware of XSS attacks!** If the STRING value to the html command
584 can be controlled by a hostile user, then he might be able to sneak
585 in malicious HTML or Javascript which could result in a
586 cross-site scripting (XSS) attack. Be careful that all text that
587 in STRING that might come from user input has been sanitized by the
588 [htmlize](#htmlize) command or similar. In recent versions of Fossil,
589 the STRING value must be [untainted](#taint) or else the "html" command
590 will fail.
591
592 <a id="htmlize"></a>TH1 htmlize Command
593 -----------------------------------------
594
595 * htmlize STRING
@@ -595,12 +653,16 @@
653 <a id="puts"></a>TH1 puts Command
654 -----------------------------------
655
656 * puts STRING
657
658 Outputs STRING. Characters within STRING that have special meaning
659 in HTML are escaped prior to being output. Thus is it safe for STRING
660 to be derived from user inputs. See also the [html](#html) command
661 which behaves similarly except does not escape HTML markup. This
662 command ("puts") is safe to use on [tainted strings](#taint), but the "html"
663 command is not.
664
665 <a id="query"></a>TH1 query Command
666 -------------------------------------
667
668 * query ?-nocomplain? SQL CODE
@@ -608,11 +670,44 @@
670 Runs the SQL query given by the SQL argument. For each row in the result
671 set, run CODE.
672
673 In SQL, parameters such as $var are filled in using the value of variable
674 "var". Result values are stored in variables with the column name prior
675 to each invocation of CODE. The names of the variables in which results
676 are stored can be controlled using "AS name" clauses in the SQL. As
677 the database will often contain content that originates from untrusted
678 users, all result values are marked as [tainted](#taint).
679
680 **Beware of SQL injections in the `query` command!**
681 The SQL argument to the query command should always be literal SQL
682 text enclosed in {...}. The SQL argument should never be a double-quoted
683 string or the value of a \$variable, as those constructs can lead to
684 an SQL Injection attack. If you need to include the values of one or
685 more TH1 variables as part of the SQL, then put \$variable inside the
686 {...}. The \$variable keyword will then get passed down into the SQLite
687 parser which knows to look up the value of \$variable in the TH1 symbol
688 table. For example:
689
690 ~~~
691 query {SELECT res FROM tab1 WHERE key=$mykey} {...}
692 ~~~
693
694 SQLite will see the \$mykey token in the SQL and will know to resolve it
695 to the value of the "mykey" TH1 variable, safely and without the possibility
696 of SQL injection. The following is unsafe:
697
698 ~~~
699 query "SELECT res FROM tab1 WHERE key='$mykey'" {...} ;# <-- UNSAFE!
700 ~~~
701
702 In this second example, TH1 does the expansion of `$mykey` prior to passing
703 the text down into SQLite. So if `$mykey` contains a single-quote character,
704 followed by additional hostile text, that will result in an SQL injection.
705
706 To help guard against SQL-injections, recent versions of Fossil require
707 that the SQL argument be [untainted](#taint) or else the "query" command
708 will fail.
709
710 <a id="randhex"></a>TH1 randhex Command
711 -----------------------------------------
712
713 * randhex N
@@ -638,10 +733,12 @@
733 * regexp ?-nocase? ?--? exp string
734
735 Checks the string against the specified regular expression and returns
736 non-zero if it matches. If the regular expression is invalid or cannot
737 be compiled, an error will be generated.
738
739 See [fossil grep](./grep.md) for details on the regexp syntax.
740
741 <a id="reinitialize"></a>TH1 reinitialize Command
742 ---------------------------------------------------
743
744 * reinitialize ?FLAGS?
@@ -741,10 +838,24 @@
838
839 * submenu link LABEL URL
840
841 Add hyperlink to the submenu of the current page.
842
843 <a id="taintCmd"></a>TH1 taint Command
844 -----------------------------------------
845
846 * taint STRING
847
848 This command returns a copy of STRING that has been marked as
849 [tainted](#taint). Tainted strings are strings which might be
850 controlled by an attacker and might contain hostile inputs and
851 are thus unsafe to use in certain contexts. For example, tainted
852 strings should not be output as part of a webpage as they might
853 contain rogue HTML or Javascript that could lead to an XSS
854 vulnerability. Similarly, tainted strings should not be run as
855 SQL since they might contain an SQL-injection vulerability.
856
857 <a id="tclEval"></a>TH1 tclEval Command
858 -----------------------------------------
859
860 **This command requires the Tcl integration feature.**
861
@@ -812,10 +923,22 @@
923
924 * trace STRING
925
926 Generates a TH1 trace message if TH1 tracing is enabled.
927
928 <a id="untaintCmd"></a>TH1 taint Command
929 -----------------------------------------
930
931 * untaint STRING
932
933 This command returns a copy of STRING that has been marked as
934 [untainted](#taint). Untainted strings are strings which are
935 believed to be free of potentially hostile content. Use this
936 command with caution, as it overwrites the tainted-string protection
937 mechanisms that are built into TH1. If you do not understand all
938 the implications of executing this command, then do not use it.
939
940 <a id="unversioned_content"></a>TH1 unversioned content Command
941 -----------------------------------------------------------------
942
943 * unversioned content FILENAME
944
945

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button