Fossil SCM

Merge from trunk.

brickviking 2025-04-12 01:21 bv-infotool merge
Commit a241019fb5b0642f5002594595268d77aa5070eba9dc35e2bac4bc8bf4f5d84b
+15 -6
--- extsrc/shell.c
+++ extsrc/shell.c
@@ -6810,11 +6810,11 @@
68106810
68116811
68126812
for(i=0; i<argc; i++){
68136813
if( sqlite3_value_type(argv[i])==SQLITE_NULL ){
68146814
/* If any of the constraints have a NULL value, then return no rows.
6815
- ** See ticket https://www.sqlite.org/src/info/fac496b61722daf2 */
6815
+ ** See ticket https://sqlite.org/src/info/fac496b61722daf2 */
68166816
returnNoRows = 1;
68176817
break;
68186818
}
68196819
}
68206820
if( returnNoRows ){
@@ -25466,10 +25466,11 @@
2546625466
" --readonly Open FILE readonly",
2546725467
" --zip FILE is a ZIP archive",
2546825468
#ifndef SQLITE_SHELL_FIDDLE
2546925469
".output ?FILE? Send output to FILE or stdout if FILE is omitted",
2547025470
" If FILE begins with '|' then open it as a pipe.",
25471
+ " If FILE is 'off' then output is disabled.",
2547125472
" Options:",
2547225473
" --bom Prefix output with a UTF8 byte-order mark",
2547325474
" -e Send output to the system text editor",
2547425475
" --plain Use text/plain for -w option",
2547525476
" -w Send output to a web browser",
@@ -30461,13 +30462,13 @@
3046130462
|| (c=='e' && n==5 && cli_strcmp(azArg[0],"excel")==0)
3046230463
|| (c=='w' && n==3 && cli_strcmp(azArg[0],"www")==0)
3046330464
){
3046430465
char *zFile = 0;
3046530466
int i;
30466
- int eMode = 0;
30467
- int bOnce = 0; /* 0: .output, 1: .once, 2: .excel/.www */
30468
- 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 */
3046930470
static const char *zBomUtf8 = "\357\273\277";
3047030471
const char *zBom = 0;
3047130472
3047230473
failIfSafeMode(p, "cannot run .%s in safe mode", azArg[0]);
3047330474
if( c=='e' ){
@@ -30492,18 +30493,26 @@
3049230493
}else if( c=='o' && cli_strcmp(z,"-e")==0 ){
3049330494
eMode = 'e'; /* text editor */
3049430495
}else if( c=='o' && cli_strcmp(z,"-w")==0 ){
3049530496
eMode = 'w'; /* Web browser */
3049630497
}else{
30497
- sqlite3_fprintf(p->out,
30498
+ sqlite3_fprintf(p->out,
3049830499
"ERROR: unknown option: \"%s\". Usage:\n", azArg[i]);
3049930500
showHelp(p->out, azArg[0]);
3050030501
rc = 1;
3050130502
goto meta_command_exit;
3050230503
}
3050330504
}else if( zFile==0 && eMode==0 ){
30504
- 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
+ }
3050530514
if( zFile && zFile[0]=='|' ){
3050630515
while( i+1<nArg ) zFile = sqlite3_mprintf("%z %s", zFile, azArg[++i]);
3050730516
break;
3050830517
}
3050930518
}else{
3051030519
--- extsrc/shell.c
+++ extsrc/shell.c
@@ -6810,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://www.sqlite.org/src/info/fac496b61722daf2 */
6816 returnNoRows = 1;
6817 break;
6818 }
6819 }
6820 if( returnNoRows ){
@@ -25466,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 " Options:",
25472 " --bom Prefix output with a UTF8 byte-order mark",
25473 " -e Send output to the system text editor",
25474 " --plain Use text/plain for -w option",
25475 " -w Send output to a web browser",
@@ -30461,13 +30462,13 @@
30461 || (c=='e' && n==5 && cli_strcmp(azArg[0],"excel")==0)
30462 || (c=='w' && n==3 && cli_strcmp(azArg[0],"www")==0)
30463 ){
30464 char *zFile = 0;
30465 int i;
30466 int eMode = 0;
30467 int bOnce = 0; /* 0: .output, 1: .once, 2: .excel/.www */
30468 int bPlain = 0; /* --plain option */
30469 static const char *zBomUtf8 = "\357\273\277";
30470 const char *zBom = 0;
30471
30472 failIfSafeMode(p, "cannot run .%s in safe mode", azArg[0]);
30473 if( c=='e' ){
@@ -30492,18 +30493,26 @@
30492 }else if( c=='o' && cli_strcmp(z,"-e")==0 ){
30493 eMode = 'e'; /* text editor */
30494 }else if( c=='o' && cli_strcmp(z,"-w")==0 ){
30495 eMode = 'w'; /* Web browser */
30496 }else{
30497 sqlite3_fprintf(p->out,
30498 "ERROR: unknown option: \"%s\". Usage:\n", azArg[i]);
30499 showHelp(p->out, azArg[0]);
30500 rc = 1;
30501 goto meta_command_exit;
30502 }
30503 }else if( zFile==0 && eMode==0 ){
30504 zFile = sqlite3_mprintf("%s", z);
 
 
 
 
 
 
 
 
30505 if( zFile && zFile[0]=='|' ){
30506 while( i+1<nArg ) zFile = sqlite3_mprintf("%z %s", zFile, azArg[++i]);
30507 break;
30508 }
30509 }else{
30510
--- extsrc/shell.c
+++ extsrc/shell.c
@@ -6810,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 ){
@@ -25466,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",
@@ -30461,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' ){
@@ -30492,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
+106 -22
--- 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
-** 121f4d97f9a855131859d342bc2ade5f8c34 with changes in files:
21
+** 20acd630b91609725794ce84f9eda01d5f3c 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-27 23:29:25 121f4d97f9a855131859d342bc2ade5f8c34ba7732029ae156d02cec7cb6dd85"
470
+#define SQLITE_SOURCE_ID "2025-04-10 10:18:07 20acd630b91609725794ce84f9eda01d5f3c898407f0948264830851d25ccaa6"
471471
472472
/*
473473
** CAPI3REF: Run-Time Library Version Numbers
474474
** KEYWORDS: sqlite3_version sqlite3_sourceid
475475
**
@@ -35446,11 +35446,11 @@
3544635446
unsigned char const *z = zIn;
3544735447
unsigned char const *zEnd = &z[nByte-1];
3544835448
int n = 0;
3544935449
3545035450
if( SQLITE_UTF16NATIVE==SQLITE_UTF16LE ) z++;
35451
- while( n<nChar && ALWAYS(z<=zEnd) ){
35451
+ while( n<nChar && z<=zEnd ){
3545235452
c = z[0];
3545335453
z += 2;
3545435454
if( c>=0xd8 && c<0xdc && z<=zEnd && z[0]>=0xdc && z[0]<0xe0 ) z += 2;
3545535455
n++;
3545635456
}
@@ -84036,11 +84036,11 @@
8403684036
** many different strings can be converted into the same int or real.
8403784037
** If a table contains a numeric value and an index is based on the
8403884038
** corresponding string value, then it is important that the string be
8403984039
** derived from the numeric value, not the other way around, to ensure
8404084040
** that the index and table are consistent. See ticket
84041
-** https://www.sqlite.org/src/info/343634942dd54ab (2018-01-31) for
84041
+** https://sqlite.org/src/info/343634942dd54ab (2018-01-31) for
8404284042
** an example.
8404384043
**
8404484044
** This routine looks at pMem to verify that if it has both a numeric
8404584045
** representation and a string representation then the string rep has
8404684046
** been derived from the numeric and not the other way around. It returns
@@ -93324,11 +93324,11 @@
9332493324
unsigned char enc
9332593325
){
9332693326
assert( xDel!=SQLITE_DYNAMIC );
9332793327
if( enc!=SQLITE_UTF8 ){
9332893328
if( enc==SQLITE_UTF16 ) enc = SQLITE_UTF16NATIVE;
93329
- nData &= ~(u16)1;
93329
+ nData &= ~(u64)1;
9333093330
}
9333193331
return bindText(pStmt, i, zData, nData, xDel, enc);
9333293332
}
9333393333
#ifndef SQLITE_OMIT_UTF16
9333493334
SQLITE_API int sqlite3_bind_text16(
@@ -125476,11 +125476,11 @@
125476125476
if( !isDupColumn(pIdx, pIdx->nKeyCol, pPk, i) ){
125477125477
testcase( hasColumn(pIdx->aiColumn, pIdx->nKeyCol, pPk->aiColumn[i]) );
125478125478
pIdx->aiColumn[j] = pPk->aiColumn[i];
125479125479
pIdx->azColl[j] = pPk->azColl[i];
125480125480
if( pPk->aSortOrder[i] ){
125481
- /* See ticket https://www.sqlite.org/src/info/bba7b69f9849b5bf */
125481
+ /* See ticket https://sqlite.org/src/info/bba7b69f9849b5bf */
125482125482
pIdx->bAscKeyBug = 1;
125483125483
}
125484125484
j++;
125485125485
}
125486125486
}
@@ -126853,11 +126853,11 @@
126853126853
/* This OP_SeekEnd opcode makes index insert for a REINDEX go much
126854126854
** faster by avoiding unnecessary seeks. But the optimization does
126855126855
** not work for UNIQUE constraint indexes on WITHOUT ROWID tables
126856126856
** with DESC primary keys, since those indexes have there keys in
126857126857
** a different order from the main table.
126858
- ** See ticket: https://www.sqlite.org/src/info/bba7b69f9849b5bf
126858
+ ** See ticket: https://sqlite.org/src/info/bba7b69f9849b5bf
126859126859
*/
126860126860
sqlite3VdbeAddOp1(v, OP_SeekEnd, iIdx);
126861126861
}
126862126862
sqlite3VdbeAddOp2(v, OP_IdxInsert, iIdx, regRecord);
126863126863
sqlite3VdbeChangeP5(v, OPFLAG_USESEEKRESULT);
@@ -136942,11 +136942,11 @@
136942136942
** OE_Update guarantees that only a single row will change, so it
136943136943
** must happen before OE_Replace. Technically, OE_Abort and OE_Rollback
136944136944
** could happen in any order, but they are grouped up front for
136945136945
** convenience.
136946136946
**
136947
- ** 2018-08-14: Ticket https://www.sqlite.org/src/info/908f001483982c43
136947
+ ** 2018-08-14: Ticket https://sqlite.org/src/info/908f001483982c43
136948136948
** The order of constraints used to have OE_Update as (2) and OE_Abort
136949136949
** and so forth as (1). But apparently PostgreSQL checks the OE_Update
136950136950
** constraint before any others, so it had to be moved.
136951136951
**
136952136952
** Constraint checking code is generated in this order:
@@ -150434,11 +150434,11 @@
150434150434
**
150435150435
** This transformation is necessary because the multiSelectOrderBy() routine
150436150436
** above that generates the code for a compound SELECT with an ORDER BY clause
150437150437
** uses a merge algorithm that requires the same collating sequence on the
150438150438
** result columns as on the ORDER BY clause. See ticket
150439
-** http://www.sqlite.org/src/info/6709574d2a
150439
+** http://sqlite.org/src/info/6709574d2a
150440150440
**
150441150441
** This transformation is only needed for EXCEPT, INTERSECT, and UNION.
150442150442
** The UNION ALL operator works fine with multiSelectOrderBy() even when
150443150443
** there are COLLATE terms in the ORDER BY.
150444150444
*/
@@ -157236,11 +157236,11 @@
157236157236
iDb = sqlite3TwoPartName(pParse, pNm, pNm, &pNm);
157237157237
if( iDb<0 ) goto build_vacuum_end;
157238157238
#else
157239157239
/* When SQLITE_BUG_COMPATIBLE_20160819 is defined, unrecognized arguments
157240157240
** to VACUUM are silently ignored. This is a back-out of a bug fix that
157241
- ** occurred on 2016-08-19 (https://www.sqlite.org/src/info/083f9e6270).
157241
+ ** occurred on 2016-08-19 (https://sqlite.org/src/info/083f9e6270).
157242157242
** The buggy behavior is required for binary compatibility with some
157243157243
** legacy applications. */
157244157244
iDb = sqlite3FindDb(pParse->db, pNm);
157245157245
if( iDb<0 ) iDb = 0;
157246157246
#endif
@@ -161953,11 +161953,11 @@
161953161953
** ON or USING clause of a LEFT JOIN, and terms that are usable as
161954161954
** indices.
161955161955
**
161956161956
** This optimization also only applies if the (x1 OR x2 OR ...) term
161957161957
** is not contained in the ON clause of a LEFT JOIN.
161958
- ** See ticket http://www.sqlite.org/src/info/f2369304e4
161958
+ ** See ticket http://sqlite.org/src/info/f2369304e4
161959161959
**
161960161960
** 2022-02-04: Do not push down slices of a row-value comparison.
161961161961
** In other words, "w" or "y" may not be a slice of a vector. Otherwise,
161962161962
** the initialization of the right-hand operand of the vector comparison
161963161963
** might not occur, or might occur only in an OR branch that is not
@@ -199281,11 +199281,11 @@
199281199281
UNUSED_PARAMETER(nVal);
199282199282
199283199283
fts3tokResetCursor(pCsr);
199284199284
if( idxNum==1 ){
199285199285
const char *zByte = (const char *)sqlite3_value_text(apVal[0]);
199286
- int nByte = sqlite3_value_bytes(apVal[0]);
199286
+ sqlite3_int64 nByte = sqlite3_value_bytes(apVal[0]);
199287199287
pCsr->zInput = sqlite3_malloc64(nByte+1);
199288199288
if( pCsr->zInput==0 ){
199289199289
rc = SQLITE_NOMEM;
199290199290
}else{
199291199291
if( nByte>0 ) memcpy(pCsr->zInput, zByte, nByte);
@@ -207828,12 +207828,12 @@
207828207828
** with JSON-5 extensions is accepted as input.
207829207829
**
207830207830
** Beginning with version 3.45.0 (circa 2024-01-01), these routines also
207831207831
** accept BLOB values that have JSON encoded using a binary representation
207832207832
** called "JSONB". The name JSONB comes from PostgreSQL, however the on-disk
207833
-** format SQLite JSONB is completely different and incompatible with
207834
-** PostgreSQL JSONB.
207833
+** format for SQLite-JSONB is completely different and incompatible with
207834
+** PostgreSQL-JSONB.
207835207835
**
207836207836
** Decoding and interpreting JSONB is still O(N) where N is the size of
207837207837
** the input, the same as text JSON. However, the constant of proportionality
207838207838
** for JSONB is much smaller due to faster parsing. The size of each
207839207839
** element in JSONB is encoded in its header, so there is no need to search
@@ -207886,21 +207886,21 @@
207886207886
** 14 4 byte (0-4294967295) 5
207887207887
** 15 8 byte (0-1.8e19) 9
207888207888
**
207889207889
** The payload size need not be expressed in its minimal form. For example,
207890207890
** if the payload size is 10, the size can be expressed in any of 5 different
207891
-** ways: (1) (X>>4)==10, (2) (X>>4)==12 following by on 0x0a byte,
207891
+** ways: (1) (X>>4)==10, (2) (X>>4)==12 following by one 0x0a byte,
207892207892
** (3) (X>>4)==13 followed by 0x00 and 0x0a, (4) (X>>4)==14 followed by
207893207893
** 0x00 0x00 0x00 0x0a, or (5) (X>>4)==15 followed by 7 bytes of 0x00 and
207894207894
** a single byte of 0x0a. The shorter forms are preferred, of course, but
207895207895
** sometimes when generating JSONB, the payload size is not known in advance
207896207896
** and it is convenient to reserve sufficient header space to cover the
207897207897
** largest possible payload size and then come back later and patch up
207898207898
** the size when it becomes known, resulting in a non-minimal encoding.
207899207899
**
207900207900
** The value (X>>4)==15 is not actually used in the current implementation
207901
-** (as SQLite is currently unable handle BLOBs larger than about 2GB)
207901
+** (as SQLite is currently unable to handle BLOBs larger than about 2GB)
207902207902
** but is included in the design to allow for future enhancements.
207903207903
**
207904207904
** The payload follows the header. NULL, TRUE, and FALSE have no payload and
207905207905
** their payload size must always be zero. The payload for INT, INT5,
207906207906
** FLOAT, FLOAT5, TEXT, TEXTJ, TEXT5, and TEXTROW is text. Note that the
@@ -208970,11 +208970,11 @@
208970208970
if( jsonBlobExpand(pParse, pParse->nBlob+szPayload+9) ) return;
208971208971
jsonBlobAppendNode(pParse, eType, szPayload, aPayload);
208972208972
}
208973208973
208974208974
208975
-/* Append an node type byte together with the payload size and
208975
+/* Append a node type byte together with the payload size and
208976208976
** possibly also the payload.
208977208977
**
208978208978
** If aPayload is not NULL, then it is a pointer to the payload which
208979208979
** is also appended. If aPayload is NULL, the pParse->aBlob[] array
208980208980
** is resized (if necessary) so that it is big enough to hold the
@@ -210304,10 +210304,86 @@
210304210304
(void)jsonbPayloadSize(pParse, iRoot, &sz);
210305210305
pParse->nBlob = nBlob;
210306210306
sz += pParse->delta;
210307210307
pParse->delta += jsonBlobChangePayloadSize(pParse, iRoot, sz);
210308210308
}
210309
+
210310
+/*
210311
+** If the JSONB at aIns[0..nIns-1] can be expanded (by denormalizing the
210312
+** size field) by d bytes, then write the expansion into aOut[] and
210313
+** return true. In this way, an overwrite happens without changing the
210314
+** size of the JSONB, which reduces memcpy() operations and also make it
210315
+** faster and easier to update the B-Tree entry that contains the JSONB
210316
+** in the database.
210317
+**
210318
+** If the expansion of aIns[] by d bytes cannot be (easily) accomplished
210319
+** then return false.
210320
+**
210321
+** The d parameter is guaranteed to be between 1 and 8.
210322
+**
210323
+** This routine is an optimization. A correct answer is obtained if it
210324
+** always leaves the output unchanged and returns false.
210325
+*/
210326
+static int jsonBlobOverwrite(
210327
+ u8 *aOut, /* Overwrite here */
210328
+ const u8 *aIns, /* New content */
210329
+ u32 nIns, /* Bytes of new content */
210330
+ u32 d /* Need to expand new content by this much */
210331
+){
210332
+ u32 szPayload; /* Bytes of payload */
210333
+ u32 i; /* New header size, after expansion & a loop counter */
210334
+ u8 szHdr; /* Size of header before expansion */
210335
+
210336
+ /* Lookup table for finding the upper 4 bits of the first byte of the
210337
+ ** expanded aIns[], based on the size of the expanded aIns[] header:
210338
+ **
210339
+ ** 2 3 4 5 6 7 8 9 */
210340
+ static const u8 aType[] = { 0xc0, 0xd0, 0, 0xe0, 0, 0, 0, 0xf0 };
210341
+
210342
+ if( (aIns[0]&0x0f)<=2 ) return 0; /* Cannot enlarge NULL, true, false */
210343
+ switch( aIns[0]>>4 ){
210344
+ default: { /* aIns[] header size 1 */
210345
+ if( ((1<<d)&0x116)==0 ) return 0; /* d must be 1, 2, 4, or 8 */
210346
+ i = d + 1; /* New hdr sz: 2, 3, 5, or 9 */
210347
+ szHdr = 1;
210348
+ break;
210349
+ }
210350
+ case 12: { /* aIns[] header size is 2 */
210351
+ if( ((1<<d)&0x8a)==0) return 0; /* d must be 1, 3, or 7 */
210352
+ i = d + 2; /* New hdr sz: 2, 5, or 9 */
210353
+ szHdr = 2;
210354
+ break;
210355
+ }
210356
+ case 13: { /* aIns[] header size is 3 */
210357
+ if( d!=2 && d!=6 ) return 0; /* d must be 2 or 6 */
210358
+ i = d + 3; /* New hdr sz: 5 or 9 */
210359
+ szHdr = 3;
210360
+ break;
210361
+ }
210362
+ case 14: { /* aIns[] header size is 5 */
210363
+ if( d!=4 ) return 0; /* d must be 4 */
210364
+ i = 9; /* New hdr sz: 9 */
210365
+ szHdr = 5;
210366
+ break;
210367
+ }
210368
+ case 15: { /* aIns[] header size is 9 */
210369
+ return 0; /* No solution */
210370
+ }
210371
+ }
210372
+ assert( i>=2 && i<=9 && aType[i-2]!=0 );
210373
+ aOut[0] = (aIns[0] & 0x0f) | aType[i-2];
210374
+ memcpy(&aOut[i], &aIns[szHdr], nIns-szHdr);
210375
+ szPayload = nIns - szHdr;
210376
+ while( 1/*edit-by-break*/ ){
210377
+ i--;
210378
+ aOut[i] = szPayload & 0xff;
210379
+ if( i==1 ) break;
210380
+ szPayload >>= 8;
210381
+ }
210382
+ assert( (szPayload>>8)==0 );
210383
+ return 1;
210384
+}
210309210385
210310210386
/*
210311210387
** Modify the JSONB blob at pParse->aBlob by removing nDel bytes of
210312210388
** content beginning at iDel, and replacing them with nIns bytes of
210313210389
** content given by aIns.
@@ -210326,10 +210402,15 @@
210326210402
u32 nDel, /* Number of bytes to remove */
210327210403
const u8 *aIns, /* Content to insert */
210328210404
u32 nIns /* Bytes of content to insert */
210329210405
){
210330210406
i64 d = (i64)nIns - (i64)nDel;
210407
+ if( d<0 && d>=(-8) && aIns!=0
210408
+ && jsonBlobOverwrite(&pParse->aBlob[iDel], aIns, nIns, (int)-d)
210409
+ ){
210410
+ return;
210411
+ }
210331210412
if( d!=0 ){
210332210413
if( pParse->nBlob + d > pParse->nBlobAlloc ){
210333210414
jsonBlobExpand(pParse, pParse->nBlob+d);
210334210415
if( pParse->oom ) return;
210335210416
}
@@ -210337,11 +210418,13 @@
210337210418
&pParse->aBlob[iDel+nDel],
210338210419
pParse->nBlob - (iDel+nDel));
210339210420
pParse->nBlob += d;
210340210421
pParse->delta += d;
210341210422
}
210342
- if( nIns && aIns ) memcpy(&pParse->aBlob[iDel], aIns, nIns);
210423
+ if( nIns && aIns ){
210424
+ memcpy(&pParse->aBlob[iDel], aIns, nIns);
210425
+ }
210343210426
}
210344210427
210345210428
/*
210346210429
** Return the number of escaped newlines to be ignored.
210347210430
** An escaped newline is a one of the following byte sequences:
@@ -211100,11 +211183,11 @@
211100211183
}
211101211184
return 0;
211102211185
}
211103211186
211104211187
/* argv[0] is a BLOB that seems likely to be a JSONB. Subsequent
211105
-** arguments come in parse where each pair contains a JSON path and
211188
+** arguments come in pairs where each pair contains a JSON path and
211106211189
** content to insert or set at that patch. Do the updates
211107211190
** and return the result.
211108211191
**
211109211192
** The specific operation is determined by eEdit, which can be one
211110211193
** of JEDIT_INS, JEDIT_REPL, or JEDIT_SET.
@@ -230169,11 +230252,13 @@
230169230252
char *zExpr = 0;
230170230253
sqlite3 *db = pSession->db;
230171230254
SessionTable *pTo; /* Table zTbl */
230172230255
230173230256
/* Locate and if necessary initialize the target table object */
230257
+ pSession->bAutoAttach++;
230174230258
rc = sessionFindTable(pSession, zTbl, &pTo);
230259
+ pSession->bAutoAttach--;
230175230260
if( pTo==0 ) goto diff_out;
230176230261
if( sessionInitTable(pSession, pTo, pSession->db, pSession->zDb) ){
230177230262
rc = pSession->rc;
230178230263
goto diff_out;
230179230264
}
@@ -257088,11 +257173,11 @@
257088257173
int nArg, /* Number of args */
257089257174
sqlite3_value **apUnused /* Function arguments */
257090257175
){
257091257176
assert( nArg==0 );
257092257177
UNUSED_PARAM2(nArg, apUnused);
257093
- sqlite3_result_text(pCtx, "fts5: 2025-03-27 23:29:25 121f4d97f9a855131859d342bc2ade5f8c34ba7732029ae156d02cec7cb6dd85", -1, SQLITE_TRANSIENT);
257178
+ sqlite3_result_text(pCtx, "fts5: 2025-04-10 10:18:07 20acd630b91609725794ce84f9eda01d5f3c898407f0948264830851d25ccaa6", -1, SQLITE_TRANSIENT);
257094257179
}
257095257180
257096257181
/*
257097257182
** Implementation of fts5_locale(LOCALE, TEXT) function.
257098257183
**
@@ -261151,11 +261236,10 @@
261151261236
}
261152261237
iTbl++;
261153261238
}
261154261239
aAscii[0] = 0; /* 0x00 is never a token character */
261155261240
}
261156
-
261157261241
261158261242
/*
261159261243
** 2015 May 30
261160261244
**
261161261245
** The author disclaims copyright to this source code. In place of
261162261246
--- 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 ** 121f4d97f9a855131859d342bc2ade5f8c34 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-27 23:29:25 121f4d97f9a855131859d342bc2ade5f8c34ba7732029ae156d02cec7cb6dd85"
471
472 /*
473 ** CAPI3REF: Run-Time Library Version Numbers
474 ** KEYWORDS: sqlite3_version sqlite3_sourceid
475 **
@@ -35446,11 +35446,11 @@
35446 unsigned char const *z = zIn;
35447 unsigned char const *zEnd = &z[nByte-1];
35448 int n = 0;
35449
35450 if( SQLITE_UTF16NATIVE==SQLITE_UTF16LE ) z++;
35451 while( n<nChar && ALWAYS(z<=zEnd) ){
35452 c = z[0];
35453 z += 2;
35454 if( c>=0xd8 && c<0xdc && z<=zEnd && z[0]>=0xdc && z[0]<0xe0 ) z += 2;
35455 n++;
35456 }
@@ -84036,11 +84036,11 @@
84036 ** many different strings can be converted into the same int or real.
84037 ** If a table contains a numeric value and an index is based on the
84038 ** corresponding string value, then it is important that the string be
84039 ** derived from the numeric value, not the other way around, to ensure
84040 ** that the index and table are consistent. See ticket
84041 ** https://www.sqlite.org/src/info/343634942dd54ab (2018-01-31) for
84042 ** an example.
84043 **
84044 ** This routine looks at pMem to verify that if it has both a numeric
84045 ** representation and a string representation then the string rep has
84046 ** been derived from the numeric and not the other way around. It returns
@@ -93324,11 +93324,11 @@
93324 unsigned char enc
93325 ){
93326 assert( xDel!=SQLITE_DYNAMIC );
93327 if( enc!=SQLITE_UTF8 ){
93328 if( enc==SQLITE_UTF16 ) enc = SQLITE_UTF16NATIVE;
93329 nData &= ~(u16)1;
93330 }
93331 return bindText(pStmt, i, zData, nData, xDel, enc);
93332 }
93333 #ifndef SQLITE_OMIT_UTF16
93334 SQLITE_API int sqlite3_bind_text16(
@@ -125476,11 +125476,11 @@
125476 if( !isDupColumn(pIdx, pIdx->nKeyCol, pPk, i) ){
125477 testcase( hasColumn(pIdx->aiColumn, pIdx->nKeyCol, pPk->aiColumn[i]) );
125478 pIdx->aiColumn[j] = pPk->aiColumn[i];
125479 pIdx->azColl[j] = pPk->azColl[i];
125480 if( pPk->aSortOrder[i] ){
125481 /* See ticket https://www.sqlite.org/src/info/bba7b69f9849b5bf */
125482 pIdx->bAscKeyBug = 1;
125483 }
125484 j++;
125485 }
125486 }
@@ -126853,11 +126853,11 @@
126853 /* This OP_SeekEnd opcode makes index insert for a REINDEX go much
126854 ** faster by avoiding unnecessary seeks. But the optimization does
126855 ** not work for UNIQUE constraint indexes on WITHOUT ROWID tables
126856 ** with DESC primary keys, since those indexes have there keys in
126857 ** a different order from the main table.
126858 ** See ticket: https://www.sqlite.org/src/info/bba7b69f9849b5bf
126859 */
126860 sqlite3VdbeAddOp1(v, OP_SeekEnd, iIdx);
126861 }
126862 sqlite3VdbeAddOp2(v, OP_IdxInsert, iIdx, regRecord);
126863 sqlite3VdbeChangeP5(v, OPFLAG_USESEEKRESULT);
@@ -136942,11 +136942,11 @@
136942 ** OE_Update guarantees that only a single row will change, so it
136943 ** must happen before OE_Replace. Technically, OE_Abort and OE_Rollback
136944 ** could happen in any order, but they are grouped up front for
136945 ** convenience.
136946 **
136947 ** 2018-08-14: Ticket https://www.sqlite.org/src/info/908f001483982c43
136948 ** The order of constraints used to have OE_Update as (2) and OE_Abort
136949 ** and so forth as (1). But apparently PostgreSQL checks the OE_Update
136950 ** constraint before any others, so it had to be moved.
136951 **
136952 ** Constraint checking code is generated in this order:
@@ -150434,11 +150434,11 @@
150434 **
150435 ** This transformation is necessary because the multiSelectOrderBy() routine
150436 ** above that generates the code for a compound SELECT with an ORDER BY clause
150437 ** uses a merge algorithm that requires the same collating sequence on the
150438 ** result columns as on the ORDER BY clause. See ticket
150439 ** http://www.sqlite.org/src/info/6709574d2a
150440 **
150441 ** This transformation is only needed for EXCEPT, INTERSECT, and UNION.
150442 ** The UNION ALL operator works fine with multiSelectOrderBy() even when
150443 ** there are COLLATE terms in the ORDER BY.
150444 */
@@ -157236,11 +157236,11 @@
157236 iDb = sqlite3TwoPartName(pParse, pNm, pNm, &pNm);
157237 if( iDb<0 ) goto build_vacuum_end;
157238 #else
157239 /* When SQLITE_BUG_COMPATIBLE_20160819 is defined, unrecognized arguments
157240 ** to VACUUM are silently ignored. This is a back-out of a bug fix that
157241 ** occurred on 2016-08-19 (https://www.sqlite.org/src/info/083f9e6270).
157242 ** The buggy behavior is required for binary compatibility with some
157243 ** legacy applications. */
157244 iDb = sqlite3FindDb(pParse->db, pNm);
157245 if( iDb<0 ) iDb = 0;
157246 #endif
@@ -161953,11 +161953,11 @@
161953 ** ON or USING clause of a LEFT JOIN, and terms that are usable as
161954 ** indices.
161955 **
161956 ** This optimization also only applies if the (x1 OR x2 OR ...) term
161957 ** is not contained in the ON clause of a LEFT JOIN.
161958 ** See ticket http://www.sqlite.org/src/info/f2369304e4
161959 **
161960 ** 2022-02-04: Do not push down slices of a row-value comparison.
161961 ** In other words, "w" or "y" may not be a slice of a vector. Otherwise,
161962 ** the initialization of the right-hand operand of the vector comparison
161963 ** might not occur, or might occur only in an OR branch that is not
@@ -199281,11 +199281,11 @@
199281 UNUSED_PARAMETER(nVal);
199282
199283 fts3tokResetCursor(pCsr);
199284 if( idxNum==1 ){
199285 const char *zByte = (const char *)sqlite3_value_text(apVal[0]);
199286 int nByte = sqlite3_value_bytes(apVal[0]);
199287 pCsr->zInput = sqlite3_malloc64(nByte+1);
199288 if( pCsr->zInput==0 ){
199289 rc = SQLITE_NOMEM;
199290 }else{
199291 if( nByte>0 ) memcpy(pCsr->zInput, zByte, nByte);
@@ -207828,12 +207828,12 @@
207828 ** with JSON-5 extensions is accepted as input.
207829 **
207830 ** Beginning with version 3.45.0 (circa 2024-01-01), these routines also
207831 ** accept BLOB values that have JSON encoded using a binary representation
207832 ** called "JSONB". The name JSONB comes from PostgreSQL, however the on-disk
207833 ** format SQLite JSONB is completely different and incompatible with
207834 ** PostgreSQL JSONB.
207835 **
207836 ** Decoding and interpreting JSONB is still O(N) where N is the size of
207837 ** the input, the same as text JSON. However, the constant of proportionality
207838 ** for JSONB is much smaller due to faster parsing. The size of each
207839 ** element in JSONB is encoded in its header, so there is no need to search
@@ -207886,21 +207886,21 @@
207886 ** 14 4 byte (0-4294967295) 5
207887 ** 15 8 byte (0-1.8e19) 9
207888 **
207889 ** The payload size need not be expressed in its minimal form. For example,
207890 ** if the payload size is 10, the size can be expressed in any of 5 different
207891 ** ways: (1) (X>>4)==10, (2) (X>>4)==12 following by on 0x0a byte,
207892 ** (3) (X>>4)==13 followed by 0x00 and 0x0a, (4) (X>>4)==14 followed by
207893 ** 0x00 0x00 0x00 0x0a, or (5) (X>>4)==15 followed by 7 bytes of 0x00 and
207894 ** a single byte of 0x0a. The shorter forms are preferred, of course, but
207895 ** sometimes when generating JSONB, the payload size is not known in advance
207896 ** and it is convenient to reserve sufficient header space to cover the
207897 ** largest possible payload size and then come back later and patch up
207898 ** the size when it becomes known, resulting in a non-minimal encoding.
207899 **
207900 ** The value (X>>4)==15 is not actually used in the current implementation
207901 ** (as SQLite is currently unable handle BLOBs larger than about 2GB)
207902 ** but is included in the design to allow for future enhancements.
207903 **
207904 ** The payload follows the header. NULL, TRUE, and FALSE have no payload and
207905 ** their payload size must always be zero. The payload for INT, INT5,
207906 ** FLOAT, FLOAT5, TEXT, TEXTJ, TEXT5, and TEXTROW is text. Note that the
@@ -208970,11 +208970,11 @@
208970 if( jsonBlobExpand(pParse, pParse->nBlob+szPayload+9) ) return;
208971 jsonBlobAppendNode(pParse, eType, szPayload, aPayload);
208972 }
208973
208974
208975 /* Append an node type byte together with the payload size and
208976 ** possibly also the payload.
208977 **
208978 ** If aPayload is not NULL, then it is a pointer to the payload which
208979 ** is also appended. If aPayload is NULL, the pParse->aBlob[] array
208980 ** is resized (if necessary) so that it is big enough to hold the
@@ -210304,10 +210304,86 @@
210304 (void)jsonbPayloadSize(pParse, iRoot, &sz);
210305 pParse->nBlob = nBlob;
210306 sz += pParse->delta;
210307 pParse->delta += jsonBlobChangePayloadSize(pParse, iRoot, sz);
210308 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
210309
210310 /*
210311 ** Modify the JSONB blob at pParse->aBlob by removing nDel bytes of
210312 ** content beginning at iDel, and replacing them with nIns bytes of
210313 ** content given by aIns.
@@ -210326,10 +210402,15 @@
210326 u32 nDel, /* Number of bytes to remove */
210327 const u8 *aIns, /* Content to insert */
210328 u32 nIns /* Bytes of content to insert */
210329 ){
210330 i64 d = (i64)nIns - (i64)nDel;
 
 
 
 
 
210331 if( d!=0 ){
210332 if( pParse->nBlob + d > pParse->nBlobAlloc ){
210333 jsonBlobExpand(pParse, pParse->nBlob+d);
210334 if( pParse->oom ) return;
210335 }
@@ -210337,11 +210418,13 @@
210337 &pParse->aBlob[iDel+nDel],
210338 pParse->nBlob - (iDel+nDel));
210339 pParse->nBlob += d;
210340 pParse->delta += d;
210341 }
210342 if( nIns && aIns ) memcpy(&pParse->aBlob[iDel], aIns, nIns);
 
 
210343 }
210344
210345 /*
210346 ** Return the number of escaped newlines to be ignored.
210347 ** An escaped newline is a one of the following byte sequences:
@@ -211100,11 +211183,11 @@
211100 }
211101 return 0;
211102 }
211103
211104 /* argv[0] is a BLOB that seems likely to be a JSONB. Subsequent
211105 ** arguments come in parse where each pair contains a JSON path and
211106 ** content to insert or set at that patch. Do the updates
211107 ** and return the result.
211108 **
211109 ** The specific operation is determined by eEdit, which can be one
211110 ** of JEDIT_INS, JEDIT_REPL, or JEDIT_SET.
@@ -230169,11 +230252,13 @@
230169 char *zExpr = 0;
230170 sqlite3 *db = pSession->db;
230171 SessionTable *pTo; /* Table zTbl */
230172
230173 /* Locate and if necessary initialize the target table object */
 
230174 rc = sessionFindTable(pSession, zTbl, &pTo);
 
230175 if( pTo==0 ) goto diff_out;
230176 if( sessionInitTable(pSession, pTo, pSession->db, pSession->zDb) ){
230177 rc = pSession->rc;
230178 goto diff_out;
230179 }
@@ -257088,11 +257173,11 @@
257088 int nArg, /* Number of args */
257089 sqlite3_value **apUnused /* Function arguments */
257090 ){
257091 assert( nArg==0 );
257092 UNUSED_PARAM2(nArg, apUnused);
257093 sqlite3_result_text(pCtx, "fts5: 2025-03-27 23:29:25 121f4d97f9a855131859d342bc2ade5f8c34ba7732029ae156d02cec7cb6dd85", -1, SQLITE_TRANSIENT);
257094 }
257095
257096 /*
257097 ** Implementation of fts5_locale(LOCALE, TEXT) function.
257098 **
@@ -261151,11 +261236,10 @@
261151 }
261152 iTbl++;
261153 }
261154 aAscii[0] = 0; /* 0x00 is never a token character */
261155 }
261156
261157
261158 /*
261159 ** 2015 May 30
261160 **
261161 ** The author disclaims copyright to this source code. In place of
261162
--- 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 ** 20acd630b91609725794ce84f9eda01d5f3c 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-10 10:18:07 20acd630b91609725794ce84f9eda01d5f3c898407f0948264830851d25ccaa6"
471
472 /*
473 ** CAPI3REF: Run-Time Library Version Numbers
474 ** KEYWORDS: sqlite3_version sqlite3_sourceid
475 **
@@ -35446,11 +35446,11 @@
35446 unsigned char const *z = zIn;
35447 unsigned char const *zEnd = &z[nByte-1];
35448 int n = 0;
35449
35450 if( SQLITE_UTF16NATIVE==SQLITE_UTF16LE ) z++;
35451 while( n<nChar && z<=zEnd ){
35452 c = z[0];
35453 z += 2;
35454 if( c>=0xd8 && c<0xdc && z<=zEnd && z[0]>=0xdc && z[0]<0xe0 ) z += 2;
35455 n++;
35456 }
@@ -84036,11 +84036,11 @@
84036 ** many different strings can be converted into the same int or real.
84037 ** If a table contains a numeric value and an index is based on the
84038 ** corresponding string value, then it is important that the string be
84039 ** derived from the numeric value, not the other way around, to ensure
84040 ** that the index and table are consistent. See ticket
84041 ** https://sqlite.org/src/info/343634942dd54ab (2018-01-31) for
84042 ** an example.
84043 **
84044 ** This routine looks at pMem to verify that if it has both a numeric
84045 ** representation and a string representation then the string rep has
84046 ** been derived from the numeric and not the other way around. It returns
@@ -93324,11 +93324,11 @@
93324 unsigned char enc
93325 ){
93326 assert( xDel!=SQLITE_DYNAMIC );
93327 if( enc!=SQLITE_UTF8 ){
93328 if( enc==SQLITE_UTF16 ) enc = SQLITE_UTF16NATIVE;
93329 nData &= ~(u64)1;
93330 }
93331 return bindText(pStmt, i, zData, nData, xDel, enc);
93332 }
93333 #ifndef SQLITE_OMIT_UTF16
93334 SQLITE_API int sqlite3_bind_text16(
@@ -125476,11 +125476,11 @@
125476 if( !isDupColumn(pIdx, pIdx->nKeyCol, pPk, i) ){
125477 testcase( hasColumn(pIdx->aiColumn, pIdx->nKeyCol, pPk->aiColumn[i]) );
125478 pIdx->aiColumn[j] = pPk->aiColumn[i];
125479 pIdx->azColl[j] = pPk->azColl[i];
125480 if( pPk->aSortOrder[i] ){
125481 /* See ticket https://sqlite.org/src/info/bba7b69f9849b5bf */
125482 pIdx->bAscKeyBug = 1;
125483 }
125484 j++;
125485 }
125486 }
@@ -126853,11 +126853,11 @@
126853 /* This OP_SeekEnd opcode makes index insert for a REINDEX go much
126854 ** faster by avoiding unnecessary seeks. But the optimization does
126855 ** not work for UNIQUE constraint indexes on WITHOUT ROWID tables
126856 ** with DESC primary keys, since those indexes have there keys in
126857 ** a different order from the main table.
126858 ** See ticket: https://sqlite.org/src/info/bba7b69f9849b5bf
126859 */
126860 sqlite3VdbeAddOp1(v, OP_SeekEnd, iIdx);
126861 }
126862 sqlite3VdbeAddOp2(v, OP_IdxInsert, iIdx, regRecord);
126863 sqlite3VdbeChangeP5(v, OPFLAG_USESEEKRESULT);
@@ -136942,11 +136942,11 @@
136942 ** OE_Update guarantees that only a single row will change, so it
136943 ** must happen before OE_Replace. Technically, OE_Abort and OE_Rollback
136944 ** could happen in any order, but they are grouped up front for
136945 ** convenience.
136946 **
136947 ** 2018-08-14: Ticket https://sqlite.org/src/info/908f001483982c43
136948 ** The order of constraints used to have OE_Update as (2) and OE_Abort
136949 ** and so forth as (1). But apparently PostgreSQL checks the OE_Update
136950 ** constraint before any others, so it had to be moved.
136951 **
136952 ** Constraint checking code is generated in this order:
@@ -150434,11 +150434,11 @@
150434 **
150435 ** This transformation is necessary because the multiSelectOrderBy() routine
150436 ** above that generates the code for a compound SELECT with an ORDER BY clause
150437 ** uses a merge algorithm that requires the same collating sequence on the
150438 ** result columns as on the ORDER BY clause. See ticket
150439 ** http://sqlite.org/src/info/6709574d2a
150440 **
150441 ** This transformation is only needed for EXCEPT, INTERSECT, and UNION.
150442 ** The UNION ALL operator works fine with multiSelectOrderBy() even when
150443 ** there are COLLATE terms in the ORDER BY.
150444 */
@@ -157236,11 +157236,11 @@
157236 iDb = sqlite3TwoPartName(pParse, pNm, pNm, &pNm);
157237 if( iDb<0 ) goto build_vacuum_end;
157238 #else
157239 /* When SQLITE_BUG_COMPATIBLE_20160819 is defined, unrecognized arguments
157240 ** to VACUUM are silently ignored. This is a back-out of a bug fix that
157241 ** occurred on 2016-08-19 (https://sqlite.org/src/info/083f9e6270).
157242 ** The buggy behavior is required for binary compatibility with some
157243 ** legacy applications. */
157244 iDb = sqlite3FindDb(pParse->db, pNm);
157245 if( iDb<0 ) iDb = 0;
157246 #endif
@@ -161953,11 +161953,11 @@
161953 ** ON or USING clause of a LEFT JOIN, and terms that are usable as
161954 ** indices.
161955 **
161956 ** This optimization also only applies if the (x1 OR x2 OR ...) term
161957 ** is not contained in the ON clause of a LEFT JOIN.
161958 ** See ticket http://sqlite.org/src/info/f2369304e4
161959 **
161960 ** 2022-02-04: Do not push down slices of a row-value comparison.
161961 ** In other words, "w" or "y" may not be a slice of a vector. Otherwise,
161962 ** the initialization of the right-hand operand of the vector comparison
161963 ** might not occur, or might occur only in an OR branch that is not
@@ -199281,11 +199281,11 @@
199281 UNUSED_PARAMETER(nVal);
199282
199283 fts3tokResetCursor(pCsr);
199284 if( idxNum==1 ){
199285 const char *zByte = (const char *)sqlite3_value_text(apVal[0]);
199286 sqlite3_int64 nByte = sqlite3_value_bytes(apVal[0]);
199287 pCsr->zInput = sqlite3_malloc64(nByte+1);
199288 if( pCsr->zInput==0 ){
199289 rc = SQLITE_NOMEM;
199290 }else{
199291 if( nByte>0 ) memcpy(pCsr->zInput, zByte, nByte);
@@ -207828,12 +207828,12 @@
207828 ** with JSON-5 extensions is accepted as input.
207829 **
207830 ** Beginning with version 3.45.0 (circa 2024-01-01), these routines also
207831 ** accept BLOB values that have JSON encoded using a binary representation
207832 ** called "JSONB". The name JSONB comes from PostgreSQL, however the on-disk
207833 ** format for SQLite-JSONB is completely different and incompatible with
207834 ** PostgreSQL-JSONB.
207835 **
207836 ** Decoding and interpreting JSONB is still O(N) where N is the size of
207837 ** the input, the same as text JSON. However, the constant of proportionality
207838 ** for JSONB is much smaller due to faster parsing. The size of each
207839 ** element in JSONB is encoded in its header, so there is no need to search
@@ -207886,21 +207886,21 @@
207886 ** 14 4 byte (0-4294967295) 5
207887 ** 15 8 byte (0-1.8e19) 9
207888 **
207889 ** The payload size need not be expressed in its minimal form. For example,
207890 ** if the payload size is 10, the size can be expressed in any of 5 different
207891 ** ways: (1) (X>>4)==10, (2) (X>>4)==12 following by one 0x0a byte,
207892 ** (3) (X>>4)==13 followed by 0x00 and 0x0a, (4) (X>>4)==14 followed by
207893 ** 0x00 0x00 0x00 0x0a, or (5) (X>>4)==15 followed by 7 bytes of 0x00 and
207894 ** a single byte of 0x0a. The shorter forms are preferred, of course, but
207895 ** sometimes when generating JSONB, the payload size is not known in advance
207896 ** and it is convenient to reserve sufficient header space to cover the
207897 ** largest possible payload size and then come back later and patch up
207898 ** the size when it becomes known, resulting in a non-minimal encoding.
207899 **
207900 ** The value (X>>4)==15 is not actually used in the current implementation
207901 ** (as SQLite is currently unable to handle BLOBs larger than about 2GB)
207902 ** but is included in the design to allow for future enhancements.
207903 **
207904 ** The payload follows the header. NULL, TRUE, and FALSE have no payload and
207905 ** their payload size must always be zero. The payload for INT, INT5,
207906 ** FLOAT, FLOAT5, TEXT, TEXTJ, TEXT5, and TEXTROW is text. Note that the
@@ -208970,11 +208970,11 @@
208970 if( jsonBlobExpand(pParse, pParse->nBlob+szPayload+9) ) return;
208971 jsonBlobAppendNode(pParse, eType, szPayload, aPayload);
208972 }
208973
208974
208975 /* Append a node type byte together with the payload size and
208976 ** possibly also the payload.
208977 **
208978 ** If aPayload is not NULL, then it is a pointer to the payload which
208979 ** is also appended. If aPayload is NULL, the pParse->aBlob[] array
208980 ** is resized (if necessary) so that it is big enough to hold the
@@ -210304,10 +210304,86 @@
210304 (void)jsonbPayloadSize(pParse, iRoot, &sz);
210305 pParse->nBlob = nBlob;
210306 sz += pParse->delta;
210307 pParse->delta += jsonBlobChangePayloadSize(pParse, iRoot, sz);
210308 }
210309
210310 /*
210311 ** If the JSONB at aIns[0..nIns-1] can be expanded (by denormalizing the
210312 ** size field) by d bytes, then write the expansion into aOut[] and
210313 ** return true. In this way, an overwrite happens without changing the
210314 ** size of the JSONB, which reduces memcpy() operations and also make it
210315 ** faster and easier to update the B-Tree entry that contains the JSONB
210316 ** in the database.
210317 **
210318 ** If the expansion of aIns[] by d bytes cannot be (easily) accomplished
210319 ** then return false.
210320 **
210321 ** The d parameter is guaranteed to be between 1 and 8.
210322 **
210323 ** This routine is an optimization. A correct answer is obtained if it
210324 ** always leaves the output unchanged and returns false.
210325 */
210326 static int jsonBlobOverwrite(
210327 u8 *aOut, /* Overwrite here */
210328 const u8 *aIns, /* New content */
210329 u32 nIns, /* Bytes of new content */
210330 u32 d /* Need to expand new content by this much */
210331 ){
210332 u32 szPayload; /* Bytes of payload */
210333 u32 i; /* New header size, after expansion & a loop counter */
210334 u8 szHdr; /* Size of header before expansion */
210335
210336 /* Lookup table for finding the upper 4 bits of the first byte of the
210337 ** expanded aIns[], based on the size of the expanded aIns[] header:
210338 **
210339 ** 2 3 4 5 6 7 8 9 */
210340 static const u8 aType[] = { 0xc0, 0xd0, 0, 0xe0, 0, 0, 0, 0xf0 };
210341
210342 if( (aIns[0]&0x0f)<=2 ) return 0; /* Cannot enlarge NULL, true, false */
210343 switch( aIns[0]>>4 ){
210344 default: { /* aIns[] header size 1 */
210345 if( ((1<<d)&0x116)==0 ) return 0; /* d must be 1, 2, 4, or 8 */
210346 i = d + 1; /* New hdr sz: 2, 3, 5, or 9 */
210347 szHdr = 1;
210348 break;
210349 }
210350 case 12: { /* aIns[] header size is 2 */
210351 if( ((1<<d)&0x8a)==0) return 0; /* d must be 1, 3, or 7 */
210352 i = d + 2; /* New hdr sz: 2, 5, or 9 */
210353 szHdr = 2;
210354 break;
210355 }
210356 case 13: { /* aIns[] header size is 3 */
210357 if( d!=2 && d!=6 ) return 0; /* d must be 2 or 6 */
210358 i = d + 3; /* New hdr sz: 5 or 9 */
210359 szHdr = 3;
210360 break;
210361 }
210362 case 14: { /* aIns[] header size is 5 */
210363 if( d!=4 ) return 0; /* d must be 4 */
210364 i = 9; /* New hdr sz: 9 */
210365 szHdr = 5;
210366 break;
210367 }
210368 case 15: { /* aIns[] header size is 9 */
210369 return 0; /* No solution */
210370 }
210371 }
210372 assert( i>=2 && i<=9 && aType[i-2]!=0 );
210373 aOut[0] = (aIns[0] & 0x0f) | aType[i-2];
210374 memcpy(&aOut[i], &aIns[szHdr], nIns-szHdr);
210375 szPayload = nIns - szHdr;
210376 while( 1/*edit-by-break*/ ){
210377 i--;
210378 aOut[i] = szPayload & 0xff;
210379 if( i==1 ) break;
210380 szPayload >>= 8;
210381 }
210382 assert( (szPayload>>8)==0 );
210383 return 1;
210384 }
210385
210386 /*
210387 ** Modify the JSONB blob at pParse->aBlob by removing nDel bytes of
210388 ** content beginning at iDel, and replacing them with nIns bytes of
210389 ** content given by aIns.
@@ -210326,10 +210402,15 @@
210402 u32 nDel, /* Number of bytes to remove */
210403 const u8 *aIns, /* Content to insert */
210404 u32 nIns /* Bytes of content to insert */
210405 ){
210406 i64 d = (i64)nIns - (i64)nDel;
210407 if( d<0 && d>=(-8) && aIns!=0
210408 && jsonBlobOverwrite(&pParse->aBlob[iDel], aIns, nIns, (int)-d)
210409 ){
210410 return;
210411 }
210412 if( d!=0 ){
210413 if( pParse->nBlob + d > pParse->nBlobAlloc ){
210414 jsonBlobExpand(pParse, pParse->nBlob+d);
210415 if( pParse->oom ) return;
210416 }
@@ -210337,11 +210418,13 @@
210418 &pParse->aBlob[iDel+nDel],
210419 pParse->nBlob - (iDel+nDel));
210420 pParse->nBlob += d;
210421 pParse->delta += d;
210422 }
210423 if( nIns && aIns ){
210424 memcpy(&pParse->aBlob[iDel], aIns, nIns);
210425 }
210426 }
210427
210428 /*
210429 ** Return the number of escaped newlines to be ignored.
210430 ** An escaped newline is a one of the following byte sequences:
@@ -211100,11 +211183,11 @@
211183 }
211184 return 0;
211185 }
211186
211187 /* argv[0] is a BLOB that seems likely to be a JSONB. Subsequent
211188 ** arguments come in pairs where each pair contains a JSON path and
211189 ** content to insert or set at that patch. Do the updates
211190 ** and return the result.
211191 **
211192 ** The specific operation is determined by eEdit, which can be one
211193 ** of JEDIT_INS, JEDIT_REPL, or JEDIT_SET.
@@ -230169,11 +230252,13 @@
230252 char *zExpr = 0;
230253 sqlite3 *db = pSession->db;
230254 SessionTable *pTo; /* Table zTbl */
230255
230256 /* Locate and if necessary initialize the target table object */
230257 pSession->bAutoAttach++;
230258 rc = sessionFindTable(pSession, zTbl, &pTo);
230259 pSession->bAutoAttach--;
230260 if( pTo==0 ) goto diff_out;
230261 if( sessionInitTable(pSession, pTo, pSession->db, pSession->zDb) ){
230262 rc = pSession->rc;
230263 goto diff_out;
230264 }
@@ -257088,11 +257173,11 @@
257173 int nArg, /* Number of args */
257174 sqlite3_value **apUnused /* Function arguments */
257175 ){
257176 assert( nArg==0 );
257177 UNUSED_PARAM2(nArg, apUnused);
257178 sqlite3_result_text(pCtx, "fts5: 2025-04-10 10:18:07 20acd630b91609725794ce84f9eda01d5f3c898407f0948264830851d25ccaa6", -1, SQLITE_TRANSIENT);
257179 }
257180
257181 /*
257182 ** Implementation of fts5_locale(LOCALE, TEXT) function.
257183 **
@@ -261151,11 +261236,10 @@
261236 }
261237 iTbl++;
261238 }
261239 aAscii[0] = 0; /* 0x00 is never a token character */
261240 }
 
261241
261242 /*
261243 ** 2015 May 30
261244 **
261245 ** The author disclaims copyright to this source code. In place of
261246
--- 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-27 23:29:25 121f4d97f9a855131859d342bc2ade5f8c34ba7732029ae156d02cec7cb6dd85"
151
+#define SQLITE_SOURCE_ID "2025-04-10 10:18:07 20acd630b91609725794ce84f9eda01d5f3c898407f0948264830851d25ccaa6"
152152
153153
/*
154154
** CAPI3REF: Run-Time Library Version Numbers
155155
** KEYWORDS: sqlite3_version sqlite3_sourceid
156156
**
157157
--- 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-27 23:29:25 121f4d97f9a855131859d342bc2ade5f8c34ba7732029ae156d02cec7cb6dd85"
152
153 /*
154 ** CAPI3REF: Run-Time Library Version Numbers
155 ** KEYWORDS: sqlite3_version sqlite3_sourceid
156 **
157
--- 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-10 10:18:07 20acd630b91609725794ce84f9eda01d5f3c898407f0948264830851d25ccaa6"
152
153 /*
154 ** CAPI3REF: Run-Time Library Version Numbers
155 ** KEYWORDS: sqlite3_version sqlite3_sourceid
156 **
157
--- 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/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/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
+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
+107 -44
--- 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,18 +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);
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
+ }
643660
smtp_client_startup(p->pSmtp);
644661
}
645662
}
646663
return p;
647664
}
@@ -1125,11 +1142,11 @@
11251142
** SETTING: email-listid width=40
11261143
** If this setting is not an empty string, then it becomes the argument to
11271144
** a "List-ID:" header that is added to all out-bound notification emails.
11281145
*/
11291146
/*
1130
-** SETTING: email-send-relayhost width=40 sensitive
1147
+** SETTING: email-send-relayhost width=40 sensitive default=127.0.0.1
11311148
** This is the hostname and TCP port to which output email messages
11321149
** are sent when email-send-method is "relay". There should be an
11331150
** SMTP server configured as a Mail Submission Agent listening on the
11341151
** designated host and port and all times.
11351152
*/
@@ -1704,11 +1721,11 @@
17041721
@ <label><input type="checkbox" name="sw" %s(PCK("sw"))> \
17051722
@ Wiki</label><br>
17061723
}
17071724
if( g.perm.Admin ){
17081725
@ <label><input type="checkbox" name="su" %s(PCK("su"))> \
1709
- @ User permission elevation</label>
1726
+ @ User permission changes</label>
17101727
}
17111728
di = PB("di");
17121729
@ </td></tr>
17131730
@ <tr>
17141731
@ <td class="form_label">Delivery:</td>
@@ -2114,11 +2131,11 @@
21142131
/* Corner-case bug: if an admin assigns 'u' to a non-admin, that
21152132
** subscription will get removed if the user later edits their
21162133
** subscriptions, as non-admins are not permitted to add that
21172134
** subscription. */
21182135
@ <label><input type="checkbox" name="su" %s(su?"checked":"")>\
2119
- @ User permission elevation</label>
2136
+ @ User permission changes</label>
21202137
}
21212138
@ </td></tr>
21222139
if( strchr(ssub,'k')!=0 ){
21232140
@ <tr><td></td><td>&nbsp;&uarr;&nbsp;
21242141
@ Note: User did a one-click unsubscribe</td></tr>
@@ -3436,16 +3453,28 @@
34363453
char *zSubject = PT("subject");
34373454
int bAll = PB("all");
34383455
int bAA = PB("aa");
34393456
int bMods = PB("mods");
34403457
const char *zSub = db_get("email-subname", "[Fossil Repo]");
3441
- int bTest2 = fossil_strcmp(P("name"),"test2")==0;
3458
+ const char *zName = P("name"); /* Debugging options */
3459
+ const char *zDest = 0; /* How to send the announcement */
3460
+ int bTest = 0;
34423461
Blob hdr, body;
3462
+
3463
+ if( fossil_strcmp(zName, "test2")==0 ){
3464
+ bTest = 2;
3465
+ zDest = "blob";
3466
+ }else if( fossil_strcmp(zName, "test3")==0 ){
3467
+ bTest = 3;
3468
+ if( fossil_strcmp(db_get("email-send-method",""),"relay")==0 ){
3469
+ zDest = "debug-relay";
3470
+ }
3471
+ }
34433472
blob_init(&body, 0, 0);
34443473
blob_init(&hdr, 0, 0);
34453474
blob_appendf(&body, "%s", PT("msg")/*safe-for-%s*/);
3446
- pSender = alert_sender_new(bTest2 ? "blob" : 0, 0);
3475
+ pSender = alert_sender_new(zDest, 0);
34473476
if( zTo[0] ){
34483477
blob_appendf(&hdr, "To: <%s>\r\nSubject: %s %s\r\n", zTo, zSub, zSubject);
34493478
alert_send(pSender, &hdr, &body, 0);
34503479
}
34513480
if( bAll || bAA || bMods ){
@@ -3479,17 +3508,24 @@
34793508
}
34803509
alert_send(pSender, &hdr, &body, 0);
34813510
}
34823511
db_finalize(&q);
34833512
}
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'>
3513
+ if( bTest && blob_size(&pSender->out) ){
3514
+ /* If the URL is "/announce/test2" then no email is actually sent.
3515
+ ** Instead, the text of the email that would have been sent is
3516
+ ** displayed in the result window.
3517
+ **
3518
+ ** If the URL is "/announce/test3" and the email-send-method is "relay"
3519
+ ** then the announcement is sent as it normally would be, but a
3520
+ ** transcript of the SMTP conversation with the MTA is shown here.
3521
+ */
3522
+ blob_trim(&pSender->out);
3523
+ @ <pre style='border: 2px solid blue; padding: 1ex;'>
34893524
@ %h(blob_str(&pSender->out))
34903525
@ </pre>
3526
+ blob_reset(&pSender->out);
34913527
}
34923528
zErr = pSender->zErr;
34933529
pSender->zErr = 0;
34943530
alert_sender_free(pSender);
34953531
return zErr;
@@ -3505,35 +3541,43 @@
35053541
** also send a message to an arbitrary email address and/or to all
35063542
** subscribers regardless of whether or not they have elected to
35073543
** receive announcements.
35083544
*/
35093545
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
-
3546
+ const char *zAction = "announce";
3547
+ const char *zName = PD("name","");
3548
+ /*
3549
+ ** Debugging Notes:
3550
+ **
3551
+ ** /announce/test1 -> Shows query parameter values
3552
+ ** /announce/test2 -> Shows the formatted message but does
3553
+ ** not send it.
3554
+ ** /announce/test3 -> Sends the message, but also shows
3555
+ ** the SMTP transcript.
3556
+ */
35173557
login_check_credentials();
35183558
if( !g.perm.Announce ){
35193559
login_needed(0);
35203560
return;
35213561
}
3562
+ if( !g.perm.Setup ){
3563
+ zName = 0; /* Disable debugging feature for non-admin users */
3564
+ }
35223565
style_set_current_feature("alerts");
3523
- if( fossil_strcmp(P("name"),"test1")==0 ){
3566
+ if( fossil_strcmp(zName,"test1")==0 ){
35243567
/* Visit the /announce/test1 page to see the CGI variables */
35253568
zAction = "announce/test1";
35263569
@ <p style='border: 1px solid black; padding: 1ex;'>
35273570
cgi_print_all(0, 0, 0);
35283571
@ </p>
35293572
}else if( P("submit")!=0 && cgi_csrf_safe(2) ){
35303573
char *zErr = alert_send_announcement();
35313574
style_header("Announcement Sent");
35323575
if( zErr ){
3533
- @ <h1>Internal Error</h1>
3534
- @ <p>The following error was reported by the system:
3576
+ @ <h1>Error</h1>
3577
+ @ <p>The following error was reported by the
3578
+ @ announcement-sending subsystem:
35353579
@ <blockquote><pre>
35363580
@ %h(zErr)
35373581
@ </pre></blockquote>
35383582
}else{
35393583
@ <p>The announcement has been sent.
@@ -3548,10 +3592,16 @@
35483592
@ for this repository.</p>
35493593
return;
35503594
}
35513595
35523596
style_header("Send Announcement");
3597
+ alert_submenu_common();
3598
+ if( fossil_strcmp(zName,"test2")==0 ){
3599
+ zAction = "announce/test2";
3600
+ }else if( fossil_strcmp(zName,"test3")==0 ){
3601
+ zAction = "announce/test3";
3602
+ }
35533603
@ <form method="POST" action="%R/%s(zAction)">
35543604
login_insert_csrf_secret();
35553605
@ <table class="subscribe">
35563606
if( g.perm.Admin ){
35573607
int aa = PB("aa");
@@ -3584,15 +3634,28 @@
35843634
@ <td><textarea name="msg" cols="80" rows="10" wrap="virtual">\
35853635
@ %h(PT("msg"))</textarea>
35863636
@ </tr>
35873637
@ <tr>
35883638
@ <td></td>
3589
- if( fossil_strcmp(P("name"),"test2")==0 ){
3639
+ if( fossil_strcmp(zName,"test2")==0 ){
35903640
@ <td><input type="submit" name="submit" value="Dry Run">
35913641
}else{
35923642
@ <td><input type="submit" name="submit" value="Send Message">
35933643
}
35943644
@ </tr>
35953645
@ </table>
35963646
@ </form>
3647
+ if( g.perm.Setup ){
3648
+ @ <hr>
3649
+ @ <p>Trouble-shooting Options:</p>
3650
+ @ <ol>
3651
+ @ <li> <a href="%R/announce">Normal Processing</a>
3652
+ @ <li> Only <a href="%R/announce/test1">show POST parameters</a>
3653
+ @ - Do not send the announcement.
3654
+ @ <li> <a href="%R/announce/test2">Show the email text</a> but do
3655
+ @ not actually send it.
3656
+ @ <li> Send the message and also <a href="%R/announce/test3">show the
3657
+ @ SMTP traffic</a> when using "relay" mode.
3658
+ @ </ol>
3659
+ }
35973660
style_finish_page();
35983661
}
35993662
--- 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,18 +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 }
@@ -1125,11 +1142,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 +1721,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 +2131,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>
@@ -3436,16 +3453,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 +3508,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 +3541,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 +3592,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 +3634,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,18 +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 smtp_client_startup(p->pSmtp);
661 }
662 }
663 return p;
664 }
@@ -1125,11 +1142,11 @@
1142 ** SETTING: email-listid width=40
1143 ** If this setting is not an empty string, then it becomes the argument to
1144 ** a "List-ID:" header that is added to all out-bound notification emails.
1145 */
1146 /*
1147 ** SETTING: email-send-relayhost width=40 sensitive default=127.0.0.1
1148 ** This is the hostname and TCP port to which output email messages
1149 ** are sent when email-send-method is "relay". There should be an
1150 ** SMTP server configured as a Mail Submission Agent listening on the
1151 ** designated host and port and all times.
1152 */
@@ -1704,11 +1721,11 @@
1721 @ <label><input type="checkbox" name="sw" %s(PCK("sw"))> \
1722 @ Wiki</label><br>
1723 }
1724 if( g.perm.Admin ){
1725 @ <label><input type="checkbox" name="su" %s(PCK("su"))> \
1726 @ User permission changes</label>
1727 }
1728 di = PB("di");
1729 @ </td></tr>
1730 @ <tr>
1731 @ <td class="form_label">Delivery:</td>
@@ -2114,11 +2131,11 @@
2131 /* Corner-case bug: if an admin assigns 'u' to a non-admin, that
2132 ** subscription will get removed if the user later edits their
2133 ** subscriptions, as non-admins are not permitted to add that
2134 ** subscription. */
2135 @ <label><input type="checkbox" name="su" %s(su?"checked":"")>\
2136 @ User permission changes</label>
2137 }
2138 @ </td></tr>
2139 if( strchr(ssub,'k')!=0 ){
2140 @ <tr><td></td><td>&nbsp;&uarr;&nbsp;
2141 @ Note: User did a one-click unsubscribe</td></tr>
@@ -3436,16 +3453,28 @@
3453 char *zSubject = PT("subject");
3454 int bAll = PB("all");
3455 int bAA = PB("aa");
3456 int bMods = PB("mods");
3457 const char *zSub = db_get("email-subname", "[Fossil Repo]");
3458 const char *zName = P("name"); /* Debugging options */
3459 const char *zDest = 0; /* How to send the announcement */
3460 int bTest = 0;
3461 Blob hdr, body;
3462
3463 if( fossil_strcmp(zName, "test2")==0 ){
3464 bTest = 2;
3465 zDest = "blob";
3466 }else if( fossil_strcmp(zName, "test3")==0 ){
3467 bTest = 3;
3468 if( fossil_strcmp(db_get("email-send-method",""),"relay")==0 ){
3469 zDest = "debug-relay";
3470 }
3471 }
3472 blob_init(&body, 0, 0);
3473 blob_init(&hdr, 0, 0);
3474 blob_appendf(&body, "%s", PT("msg")/*safe-for-%s*/);
3475 pSender = alert_sender_new(zDest, 0);
3476 if( zTo[0] ){
3477 blob_appendf(&hdr, "To: <%s>\r\nSubject: %s %s\r\n", zTo, zSub, zSubject);
3478 alert_send(pSender, &hdr, &body, 0);
3479 }
3480 if( bAll || bAA || bMods ){
@@ -3479,17 +3508,24 @@
3508 }
3509 alert_send(pSender, &hdr, &body, 0);
3510 }
3511 db_finalize(&q);
3512 }
3513 if( bTest && blob_size(&pSender->out) ){
3514 /* If the URL is "/announce/test2" then no email is actually sent.
3515 ** Instead, the text of the email that would have been sent is
3516 ** displayed in the result window.
3517 **
3518 ** If the URL is "/announce/test3" and the email-send-method is "relay"
3519 ** then the announcement is sent as it normally would be, but a
3520 ** transcript of the SMTP conversation with the MTA is shown here.
3521 */
3522 blob_trim(&pSender->out);
3523 @ <pre style='border: 2px solid blue; padding: 1ex;'>
3524 @ %h(blob_str(&pSender->out))
3525 @ </pre>
3526 blob_reset(&pSender->out);
3527 }
3528 zErr = pSender->zErr;
3529 pSender->zErr = 0;
3530 alert_sender_free(pSender);
3531 return zErr;
@@ -3505,35 +3541,43 @@
3541 ** also send a message to an arbitrary email address and/or to all
3542 ** subscribers regardless of whether or not they have elected to
3543 ** receive announcements.
3544 */
3545 void announce_page(void){
3546 const char *zAction = "announce";
3547 const char *zName = PD("name","");
3548 /*
3549 ** Debugging Notes:
3550 **
3551 ** /announce/test1 -> Shows query parameter values
3552 ** /announce/test2 -> Shows the formatted message but does
3553 ** not send it.
3554 ** /announce/test3 -> Sends the message, but also shows
3555 ** the SMTP transcript.
3556 */
3557 login_check_credentials();
3558 if( !g.perm.Announce ){
3559 login_needed(0);
3560 return;
3561 }
3562 if( !g.perm.Setup ){
3563 zName = 0; /* Disable debugging feature for non-admin users */
3564 }
3565 style_set_current_feature("alerts");
3566 if( fossil_strcmp(zName,"test1")==0 ){
3567 /* Visit the /announce/test1 page to see the CGI variables */
3568 zAction = "announce/test1";
3569 @ <p style='border: 1px solid black; padding: 1ex;'>
3570 cgi_print_all(0, 0, 0);
3571 @ </p>
3572 }else if( P("submit")!=0 && cgi_csrf_safe(2) ){
3573 char *zErr = alert_send_announcement();
3574 style_header("Announcement Sent");
3575 if( zErr ){
3576 @ <h1>Error</h1>
3577 @ <p>The following error was reported by the
3578 @ announcement-sending subsystem:
3579 @ <blockquote><pre>
3580 @ %h(zErr)
3581 @ </pre></blockquote>
3582 }else{
3583 @ <p>The announcement has been sent.
@@ -3548,10 +3592,16 @@
3592 @ for this repository.</p>
3593 return;
3594 }
3595
3596 style_header("Send Announcement");
3597 alert_submenu_common();
3598 if( fossil_strcmp(zName,"test2")==0 ){
3599 zAction = "announce/test2";
3600 }else if( fossil_strcmp(zName,"test3")==0 ){
3601 zAction = "announce/test3";
3602 }
3603 @ <form method="POST" action="%R/%s(zAction)">
3604 login_insert_csrf_secret();
3605 @ <table class="subscribe">
3606 if( g.perm.Admin ){
3607 int aa = PB("aa");
@@ -3584,15 +3634,28 @@
3634 @ <td><textarea name="msg" cols="80" rows="10" wrap="virtual">\
3635 @ %h(PT("msg"))</textarea>
3636 @ </tr>
3637 @ <tr>
3638 @ <td></td>
3639 if( fossil_strcmp(zName,"test2")==0 ){
3640 @ <td><input type="submit" name="submit" value="Dry Run">
3641 }else{
3642 @ <td><input type="submit" name="submit" value="Send Message">
3643 }
3644 @ </tr>
3645 @ </table>
3646 @ </form>
3647 if( g.perm.Setup ){
3648 @ <hr>
3649 @ <p>Trouble-shooting Options:</p>
3650 @ <ol>
3651 @ <li> <a href="%R/announce">Normal Processing</a>
3652 @ <li> Only <a href="%R/announce/test1">show POST parameters</a>
3653 @ - Do not send the announcement.
3654 @ <li> <a href="%R/announce/test2">Show the email text</a> but do
3655 @ not actually send it.
3656 @ <li> Send the message and also <a href="%R/announce/test3">show the
3657 @ SMTP traffic</a> when using "relay" mode.
3658 @ </ol>
3659 }
3660 style_finish_page();
3661 }
3662
+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
+6 -6
--- 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;
682682
--- 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;
682
--- 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;
682
+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
+45 -33
--- 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
@@ -2537,11 +2549,11 @@
25372549
fd_set readfds; /* Set of file descriptors for select() */
25382550
socklen_t lenaddr; /* Length of the inaddr structure */
25392551
int child; /* PID of the child process */
25402552
int nchildren = 0; /* Number of child processes */
25412553
struct timeval delay; /* How long to wait inside select() */
2542
- struct sockaddr_in inaddr; /* The socket address */
2554
+ struct sockaddr_in6 inaddr; /* The socket address */
25432555
struct sockaddr_un uxaddr; /* The address for unix-domain sockets */
25442556
int opt = 1; /* setsockopt flag */
25452557
int rc; /* Result code from system calls */
25462558
int iPort = mnPort; /* Port to try to use */
25472559
@@ -2578,23 +2590,22 @@
25782590
file_set_mode(g.zSockName, listener, "0660", 1);
25792591
}
25802592
}else{
25812593
/* Initialize a TCP/IP socket on port iPort */
25822594
memset(&inaddr, 0, sizeof(inaddr));
2583
- inaddr.sin_family = AF_INET;
2595
+ inaddr.sin6_family = AF_INET6;
25842596
if( zIpAddr ){
2585
- inaddr.sin_addr.s_addr = inet_addr(zIpAddr);
2586
- if( inaddr.sin_addr.s_addr == INADDR_NONE ){
2597
+ if( inet_pton(AF_INET6, zIpAddr, &inaddr.sin6_addr)==0 ){
25872598
fossil_fatal("not a valid IP address: %s", zIpAddr);
25882599
}
25892600
}else if( flags & HTTP_SERVER_LOCALHOST ){
2590
- inaddr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
2601
+ inaddr.sin6_addr = in6addr_loopback;
25912602
}else{
2592
- inaddr.sin_addr.s_addr = htonl(INADDR_ANY);
2603
+ inaddr.sin6_addr = in6addr_any;
25932604
}
2594
- inaddr.sin_port = htons(iPort);
2595
- listener = socket(AF_INET, SOCK_STREAM, 0);
2605
+ inaddr.sin6_port = htons(iPort);
2606
+ listener = socket(AF_INET6, SOCK_STREAM, 0);
25962607
if( listener<0 ){
25972608
iPort++;
25982609
continue;
25992610
}
26002611
}
@@ -2700,10 +2711,11 @@
27002711
close(2);
27012712
fd = dup(connection);
27022713
if( fd!=2 ) nErr++;
27032714
}
27042715
close(connection);
2716
+ close(listener);
27052717
g.nPendingRequest = nchildren+1;
27062718
g.nRequest = nRequest+1;
27072719
return nErr;
27082720
}
27092721
}
27102722
--- 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
@@ -2537,11 +2549,11 @@
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
@@ -2578,23 +2590,22 @@
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 }
@@ -2700,10 +2711,11 @@
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
--- 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
@@ -2537,11 +2549,11 @@
2549 fd_set readfds; /* Set of file descriptors for select() */
2550 socklen_t lenaddr; /* Length of the inaddr structure */
2551 int child; /* PID of the child process */
2552 int nchildren = 0; /* Number of child processes */
2553 struct timeval delay; /* How long to wait inside select() */
2554 struct sockaddr_in6 inaddr; /* The socket address */
2555 struct sockaddr_un uxaddr; /* The address for unix-domain sockets */
2556 int opt = 1; /* setsockopt flag */
2557 int rc; /* Result code from system calls */
2558 int iPort = mnPort; /* Port to try to use */
2559
@@ -2578,23 +2590,22 @@
2590 file_set_mode(g.zSockName, listener, "0660", 1);
2591 }
2592 }else{
2593 /* Initialize a TCP/IP socket on port iPort */
2594 memset(&inaddr, 0, sizeof(inaddr));
2595 inaddr.sin6_family = AF_INET6;
2596 if( zIpAddr ){
2597 if( inet_pton(AF_INET6, zIpAddr, &inaddr.sin6_addr)==0 ){
 
2598 fossil_fatal("not a valid IP address: %s", zIpAddr);
2599 }
2600 }else if( flags & HTTP_SERVER_LOCALHOST ){
2601 inaddr.sin6_addr = in6addr_loopback;
2602 }else{
2603 inaddr.sin6_addr = in6addr_any;
2604 }
2605 inaddr.sin6_port = htons(iPort);
2606 listener = socket(AF_INET6, SOCK_STREAM, 0);
2607 if( listener<0 ){
2608 iPort++;
2609 continue;
2610 }
2611 }
@@ -2700,10 +2711,11 @@
2711 close(2);
2712 fd = dup(connection);
2713 if( fd!=2 ) nErr++;
2714 }
2715 close(connection);
2716 close(listener);
2717 g.nPendingRequest = nchildren+1;
2718 g.nRequest = nRequest+1;
2719 return nErr;
2720 }
2721 }
2722
+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
+72
--- src/encode.c
+++ src/encode.c
@@ -141,10 +141,82 @@
141141
break;
142142
}
143143
}
144144
if( j<i ) blob_append(p, zIn+j, i-j);
145145
}
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
+}
146218
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
--- src/encode.c
+++ src/encode.c
@@ -141,10 +141,82 @@
141 break;
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
--- src/encode.c
+++ src/encode.c
@@ -141,10 +141,82 @@
141 break;
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
--- 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
+2 -1
--- src/info.c
+++ src/info.c
@@ -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 ){
20142015
--- src/info.c
+++ src/info.c
@@ -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 ){
2014
--- src/info.c
+++ src/info.c
@@ -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 ){
2015
+17 -1
--- 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;
13191335
--- 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;
1319
--- 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;
1335
+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
+23 -5
--- 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>
@@ -2394,10 +2407,13 @@
23942407
** REPO for a check-in or ticket that matches the
23952408
** value of "name", then redirect to URL. There
23962409
** can be multiple "redirect:" lines that are
23972410
** processed in order. If the REPO is "*", then
23982411
** an unconditional redirect to URL is taken.
2412
+** When "*" is used a 301 permanent redirect is
2413
+** issued and the tail and query string from the
2414
+** original query are appeneded onto URL.
23992415
**
24002416
** jsmode: VALUE Specifies the delivery mode for JavaScript
24012417
** files. See the help text for the --jsmode
24022418
** flag of the http command.
24032419
**
@@ -3052,23 +3068,25 @@
30523068
** using this command interactively over SSH. A better solution would be
30533069
** to use a different command for "ssh" sync, but we cannot do that without
30543070
** breaking legacy.
30553071
**
30563072
** Options:
3073
+** --csrf-safe N Set cgi_csrf_safe() to to return N
30573074
** --nobody Pretend to be user "nobody"
30583075
** --test Do not do special "sync" processing when operating
30593076
** over an SSH link
30603077
** --th-trace Trace TH1 execution (for debugging purposes)
30613078
** --usercap CAP User capability string (Default: "sxy")
3062
-**
30633079
*/
30643080
void cmd_test_http(void){
30653081
const char *zIpAddr; /* IP address of remote client */
30663082
const char *zUserCap;
30673083
int bTest = 0;
3084
+ const char *zCsrfSafe = find_option("csrf-safe",0,1);
30683085
30693086
Th_InitTraceLog();
3087
+ if( zCsrfSafe ) g.okCsrf = atoi(zCsrfSafe);
30703088
zUserCap = find_option("usercap",0,1);
30713089
if( !find_option("nobody",0,0) ){
30723090
if( zUserCap==0 ){
30733091
g.useLocalauth = 1;
30743092
zUserCap = "sxy";
30753093
--- 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>
@@ -2394,10 +2407,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 +3068,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";
3075
--- 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>
@@ -2394,10 +2407,13 @@
2407 ** REPO for a check-in or ticket that matches the
2408 ** value of "name", then redirect to URL. There
2409 ** can be multiple "redirect:" lines that are
2410 ** processed in order. If the REPO is "*", then
2411 ** an unconditional redirect to URL is taken.
2412 ** When "*" is used a 301 permanent redirect is
2413 ** issued and the tail and query string from the
2414 ** original query are appeneded onto URL.
2415 **
2416 ** jsmode: VALUE Specifies the delivery mode for JavaScript
2417 ** files. See the help text for the --jsmode
2418 ** flag of the http command.
2419 **
@@ -3052,23 +3068,25 @@
3068 ** using this command interactively over SSH. A better solution would be
3069 ** to use a different command for "ssh" sync, but we cannot do that without
3070 ** breaking legacy.
3071 **
3072 ** Options:
3073 ** --csrf-safe N Set cgi_csrf_safe() to to return N
3074 ** --nobody Pretend to be user "nobody"
3075 ** --test Do not do special "sync" processing when operating
3076 ** over an SSH link
3077 ** --th-trace Trace TH1 execution (for debugging purposes)
3078 ** --usercap CAP User capability string (Default: "sxy")
 
3079 */
3080 void cmd_test_http(void){
3081 const char *zIpAddr; /* IP address of remote client */
3082 const char *zUserCap;
3083 int bTest = 0;
3084 const char *zCsrfSafe = find_option("csrf-safe",0,1);
3085
3086 Th_InitTraceLog();
3087 if( zCsrfSafe ) g.okCsrf = atoi(zCsrfSafe);
3088 zUserCap = find_option("usercap",0,1);
3089 if( !find_option("nobody",0,0) ){
3090 if( zUserCap==0 ){
3091 g.useLocalauth = 1;
3092 zUserCap = "sxy";
3093
+1 -1
--- src/printf.c
+++ src/printf.c
@@ -1100,11 +1100,11 @@
11001100
if( zFormat[0]=='X' ){
11011101
bDetail = 1;
11021102
zFormat++;
11031103
}
11041104
vfprintf(out, zFormat, ap);
1105
- fprintf(out, "\n");
1105
+ fprintf(out, " (pid %d)\n", (int)getpid());
11061106
va_end(ap);
11071107
if( g.zPhase!=0 ) fprintf(out, "while in %s\n", g.zPhase);
11081108
if( bDetail ){
11091109
cgi_print_all(1,3,out);
11101110
}else{
11111111
--- src/printf.c
+++ src/printf.c
@@ -1100,11 +1100,11 @@
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
--- src/printf.c
+++ src/printf.c
@@ -1100,11 +1100,11 @@
1100 if( zFormat[0]=='X' ){
1101 bDetail = 1;
1102 zFormat++;
1103 }
1104 vfprintf(out, zFormat, ap);
1105 fprintf(out, " (pid %d)\n", (int)getpid());
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
+33 -9
--- 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 ){
@@ -162,15 +173,16 @@
162173
}else{
163174
Stmt q;
164175
double rNow;
165176
blob_append_sql(&html,
166177
"<table border='0' class='sortable' data-init-sort='1'"
167
- " data-column-types='txtxkxt'><thead>\n"
168
- "<tr><th>Filename<th width='20'>"
169
- "<th>Project Name<th width='20'>"
170
- "<th>Last Modified<th width='20'>"
171
- "<th>Login Group</tr>\n"
178
+ " data-column-types='txtxtxkxt' cellspacing='0' cellpadding='0'><thead>\n"
179
+ "<tr><th>Filename<th width='7'>"
180
+ "<th width='25%%'>Project Name<th width='10'>"
181
+ "<th width='25%%'>Project Description<th width='5'>"
182
+ "<th><nobr>Last Modified</nobr><th width='1'>"
183
+ "<th><nobr>Login Group</nobr></tr>\n"
172184
"</thead><tbody>\n");
173185
db_prepare(&q, "SELECT pathname"
174186
" FROM sfile ORDER BY pathname COLLATE nocase;");
175187
rNow = db_double(0, "SELECT julianday('now')");
176188
while( db_step(&q)==SQLITE_ROW ){
@@ -230,11 +242,11 @@
230242
if( x.rMTime==0.0 ){
231243
/* This repository has no entry in the "event" table.
232244
** Its age will still be maximum, so data-sortkey will work. */
233245
zAge = mprintf("unknown");
234246
}
235
- blob_append_sql(&html, "<tr><td valign='top'>");
247
+ blob_append_sql(&html, "<tr><td valign='top'><nobr>");
236248
if( !file_ends_with_repository_extension(zName,0) ){
237249
/* The "fossil server DIRECTORY" and "fossil ui DIRECTORY" commands
238250
** do not work for repositories whose names do not end in ".fossil".
239251
** So do not hyperlink those cases. */
240252
blob_append_sql(&html,"%h",zName);
@@ -275,22 +287,34 @@
275287
}else{
276288
blob_append_sql(&html,
277289
"<a href='%R/%T/home' target='_blank'>%h</a>\n",
278290
zUrl, zName);
279291
}
292
+ blob_append_sql(&html,"</nobr>");
280293
if( x.zProjName ){
281
- blob_append_sql(&html, "<td></td><td>%h</td>\n", x.zProjName);
294
+ blob_append_sql(&html, "<td></td><td valign='top'>%h</td>\n",
295
+ x.zProjName);
282296
fossil_free(x.zProjName);
283297
}else{
284298
blob_append_sql(&html, "<td></td><td></td>\n");
285299
}
300
+ if( x.zProjDesc ){
301
+ blob_append_sql(&html, "<td></td><td valign='top'>%h</td>\n",
302
+ x.zProjDesc);
303
+ fossil_free(x.zProjDesc);
304
+ }else{
305
+ blob_append_sql(&html, "<td></td><td></td>\n");
306
+ }
286307
blob_append_sql(&html,
287
- "<td></td><td data-sortkey='%08x'>%h</td>\n",
308
+ "<td></td><td data-sortkey='%08x' align='center' valign='top'>"
309
+ "<nobr>%h</nobr></td>\n",
288310
(int)iAge, zAge);
289311
fossil_free(zAge);
290312
if( x.zLoginGroup ){
291
- blob_append_sql(&html, "<td></td><td>%h</td></tr>\n", x.zLoginGroup);
313
+ blob_append_sql(&html, "<td></td><td valign='top'>"
314
+ "<nobr>%h</nobr></td></tr>\n",
315
+ x.zLoginGroup);
292316
fossil_free(x.zLoginGroup);
293317
}else{
294318
blob_append_sql(&html, "<td></td><td></td></tr>\n");
295319
}
296320
sqlite3_free(zUrl);
297321
--- 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 ){
@@ -162,15 +173,16 @@
162 }else{
163 Stmt q;
164 double rNow;
165 blob_append_sql(&html,
166 "<table border='0' class='sortable' data-init-sort='1'"
167 " data-column-types='txtxkxt'><thead>\n"
168 "<tr><th>Filename<th width='20'>"
169 "<th>Project Name<th width='20'>"
170 "<th>Last Modified<th width='20'>"
171 "<th>Login Group</tr>\n"
 
172 "</thead><tbody>\n");
173 db_prepare(&q, "SELECT pathname"
174 " FROM sfile ORDER BY pathname COLLATE nocase;");
175 rNow = db_double(0, "SELECT julianday('now')");
176 while( db_step(&q)==SQLITE_ROW ){
@@ -230,11 +242,11 @@
230 if( x.rMTime==0.0 ){
231 /* This repository has no entry in the "event" table.
232 ** Its age will still be maximum, so data-sortkey will work. */
233 zAge = mprintf("unknown");
234 }
235 blob_append_sql(&html, "<tr><td valign='top'>");
236 if( !file_ends_with_repository_extension(zName,0) ){
237 /* The "fossil server DIRECTORY" and "fossil ui DIRECTORY" commands
238 ** do not work for repositories whose names do not end in ".fossil".
239 ** So do not hyperlink those cases. */
240 blob_append_sql(&html,"%h",zName);
@@ -275,22 +287,34 @@
275 }else{
276 blob_append_sql(&html,
277 "<a href='%R/%T/home' target='_blank'>%h</a>\n",
278 zUrl, zName);
279 }
 
280 if( x.zProjName ){
281 blob_append_sql(&html, "<td></td><td>%h</td>\n", x.zProjName);
 
282 fossil_free(x.zProjName);
283 }else{
284 blob_append_sql(&html, "<td></td><td></td>\n");
285 }
 
 
 
 
 
 
 
286 blob_append_sql(&html,
287 "<td></td><td data-sortkey='%08x'>%h</td>\n",
 
288 (int)iAge, zAge);
289 fossil_free(zAge);
290 if( x.zLoginGroup ){
291 blob_append_sql(&html, "<td></td><td>%h</td></tr>\n", x.zLoginGroup);
 
 
292 fossil_free(x.zLoginGroup);
293 }else{
294 blob_append_sql(&html, "<td></td><td></td></tr>\n");
295 }
296 sqlite3_free(zUrl);
297
--- 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 ){
@@ -162,15 +173,16 @@
173 }else{
174 Stmt q;
175 double rNow;
176 blob_append_sql(&html,
177 "<table border='0' class='sortable' data-init-sort='1'"
178 " data-column-types='txtxtxkxt' cellspacing='0' cellpadding='0'><thead>\n"
179 "<tr><th>Filename<th width='7'>"
180 "<th width='25%%'>Project Name<th width='10'>"
181 "<th width='25%%'>Project Description<th width='5'>"
182 "<th><nobr>Last Modified</nobr><th width='1'>"
183 "<th><nobr>Login Group</nobr></tr>\n"
184 "</thead><tbody>\n");
185 db_prepare(&q, "SELECT pathname"
186 " FROM sfile ORDER BY pathname COLLATE nocase;");
187 rNow = db_double(0, "SELECT julianday('now')");
188 while( db_step(&q)==SQLITE_ROW ){
@@ -230,11 +242,11 @@
242 if( x.rMTime==0.0 ){
243 /* This repository has no entry in the "event" table.
244 ** Its age will still be maximum, so data-sortkey will work. */
245 zAge = mprintf("unknown");
246 }
247 blob_append_sql(&html, "<tr><td valign='top'><nobr>");
248 if( !file_ends_with_repository_extension(zName,0) ){
249 /* The "fossil server DIRECTORY" and "fossil ui DIRECTORY" commands
250 ** do not work for repositories whose names do not end in ".fossil".
251 ** So do not hyperlink those cases. */
252 blob_append_sql(&html,"%h",zName);
@@ -275,22 +287,34 @@
287 }else{
288 blob_append_sql(&html,
289 "<a href='%R/%T/home' target='_blank'>%h</a>\n",
290 zUrl, zName);
291 }
292 blob_append_sql(&html,"</nobr>");
293 if( x.zProjName ){
294 blob_append_sql(&html, "<td></td><td valign='top'>%h</td>\n",
295 x.zProjName);
296 fossil_free(x.zProjName);
297 }else{
298 blob_append_sql(&html, "<td></td><td></td>\n");
299 }
300 if( x.zProjDesc ){
301 blob_append_sql(&html, "<td></td><td valign='top'>%h</td>\n",
302 x.zProjDesc);
303 fossil_free(x.zProjDesc);
304 }else{
305 blob_append_sql(&html, "<td></td><td></td>\n");
306 }
307 blob_append_sql(&html,
308 "<td></td><td data-sortkey='%08x' align='center' valign='top'>"
309 "<nobr>%h</nobr></td>\n",
310 (int)iAge, zAge);
311 fossil_free(zAge);
312 if( x.zLoginGroup ){
313 blob_append_sql(&html, "<td></td><td valign='top'>"
314 "<nobr>%h</nobr></td></tr>\n",
315 x.zLoginGroup);
316 fossil_free(x.zLoginGroup);
317 }else{
318 blob_append_sql(&html, "<td></td><td></td></tr>\n");
319 }
320 sqlite3_free(zUrl);
321
+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
+4 -4
--- 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."
@@ -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")
@@ -1296,11 +1296,11 @@
12961296
@ </p>
12971297
@ <hr>
12981298
textarea_attribute("Project Description", 3, 80,
12991299
"project-description", "pd", "", 0);
13001300
@ <p>Describe your project. This will be used in page headers for search
1301
- @ engines as well as a short RSS description.
1301
+ @ engines, the repository listing and a short RSS description.
13021302
@ (Property: "project-description")</p>
13031303
@ <hr>
13041304
entry_attribute("Canonical Server URL", 40, "email-url",
13051305
"eurl", "", 0);
13061306
@ <p>This is the URL used to access this repository as a server.
13071307
--- 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."
@@ -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")
@@ -1296,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 as well as 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
--- 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."
@@ -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")
@@ -1296,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
+70 -28
--- src/setupuser.c
+++ src/setupuser.c
@@ -155,18 +155,19 @@
155155
zWith = mprintf(" AND fullcap(cap) GLOB '*[%q]*'", zWith);
156156
}else{
157157
zWith = "";
158158
}
159159
db_prepare(&s,
160
- "SELECT uid, login, cap, info, date(user.mtime,'unixepoch'),"
161
- " lower(login) AS sortkey, "
160
+ "SELECT uid, login, cap, info, date(user.mtime,'unixepoch')," /* 0..4 */
161
+ " lower(login) AS sortkey, " /* 5 */
162162
" CASE WHEN info LIKE '%%expires 20%%'"
163163
" 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"
164
+ " END AS exp," /* 6 */
165
+ "atime," /* 7 */
166
+ " subscriber.ssub, subscriber.subscriberId," /* 8, 9 */
167
+ " user.mtime AS sorttime," /* 10 */
168
+ " subscriber.semail" /* 11 */
168169
" FROM user LEFT JOIN lastAccess ON login=uname"
169170
" LEFT JOIN subscriber ON login=suname"
170171
" WHERE login NOT IN ('anonymous','nobody','developer','reader') %s"
171172
" ORDER BY sorttime DESC", zWith/*safe-for-%s*/
172173
);
@@ -202,11 +203,13 @@
202203
if( db_column_type(&s,8)==SQLITE_NULL ){
203204
@ <td>
204205
}else if( (zSub = db_column_text(&s,8))==0 || zSub[0]==0 ){
205206
@ <td><a href="%R/alerts?sid=%d(sid)"><i>off</i></a>
206207
}else{
207
- @ <td><a href="%R/alerts?sid=%d(sid)">%h(zSub)</a>
208
+ const char *zEmail = db_column_text(&s, 11);
209
+ char * zAt = zEmail ? mprintf(" &rarr; %h", zEmail) : mprintf("");
210
+ @ <td><a href="%R/alerts?sid=%d(sid)">%h(zSub)</a> %z(zAt)
208211
}
209212
210213
@ </tr>
211214
fossil_free(zAge);
212215
}
@@ -304,22 +307,59 @@
304307
while( zPw[0]=='*' ){ zPw++; }
305308
return zPw[0]!=0;
306309
}
307310
308311
/*
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.
312
+** Return true if user capability strings zOrig and zNew materially
313
+** differ, taking into account that they may be sorted in an arbitary
314
+** order. This does not take inherited permissions into
315
+** account. Either argument may be NULL. A NULL and an empty string
316
+** are considered equivalent here. e.g. "abc" and "cab" are equivalent
317
+** for this purpose, but "aCb" and "acb" are not.
318
+*/
319
+static int userCapsChanged(const char *zOrig, const char *zNew){
320
+ if( !zOrig ){
321
+ return zNew ? (0!=*zNew) : 0;
322
+ }else if( !zNew ){
323
+ return 0!=*zOrig;
324
+ }else if( 0==fossil_strcmp(zOrig, zNew) ){
325
+ return 0;
326
+ }else{
327
+ /* We don't know that zOrig and zNew are sorted equivalently. The
328
+ ** following steps will compare strings which contain all the same
329
+ ** capabilities letters as equivalent, regardless of the letters'
330
+ ** order in their strings. */
331
+ char aOrig[128]; /* table of zOrig bytes */
332
+ int nOrig = 0, nNew = 0;
333
+
334
+ memset( &aOrig[0], 0, sizeof(aOrig) );
335
+ for( ; *zOrig; ++zOrig, ++nOrig ){
336
+ if( 0==(*zOrig & 0x80) ){
337
+ aOrig[(int)*zOrig] = 1;
338
+ }
339
+ }
340
+ for( ; *zNew; ++zNew, ++nNew ){
341
+ if( 0==(*zNew & 0x80) && !aOrig[(int)*zNew] ){
342
+ return 1;
343
+ }
344
+ }
345
+ return nOrig!=nNew;
346
+ }
347
+}
348
+
349
+/*
350
+** COMMAND: test-user-caps-changed
351
+**
352
+** Usage: %fossil test-user-caps-changed caps1 caps2
353
+**
313354
*/
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;
355
+void test_user_caps_changed(void){
356
+
357
+ char const * zOld = g.argc>2 ? g.argv[2] : NULL;
358
+ char const * zNew = g.argc>3 ? g.argv[3] : NULL;
359
+ fossil_print("Has changes? = %d\n",
360
+ userCapsChanged( zOld, zNew ));
321361
}
322362
323363
/*
324364
** Sends notification of user permission elevation changes to all
325365
** subscribers with a "u" subscription. This is a no-op if alerts are
@@ -333,11 +373,11 @@
333373
** edits their subscriptions after an admin assigns them this one,
334374
** this particular one will be lost. "Feature or bug?" is unclear,
335375
** but it would be odd for a non-admin to be assigned this
336376
** capability.
337377
*/
338
-static void alert_user_elevation(const char *zLogin, /*Affected user*/
378
+static void alert_user_cap_change(const char *zLogin, /*Affected user*/
339379
int uid, /*[user].uid*/
340380
int bIsNew, /*true if new user*/
341381
const char *zOrigCaps,/*Old caps*/
342382
const char *zNewCaps /*New caps*/){
343383
Blob hdr, body;
@@ -349,21 +389,21 @@
349389
char * zSubject;
350390
351391
if( !alert_enabled() ) return;
352392
zSubject = bIsNew
353393
? mprintf("New user created: [%q]", zLogin)
354
- : mprintf("User [%q] permissions elevated", zLogin);
394
+ : mprintf("User [%q] capabilities changed", zLogin);
355395
zURL = db_get("email-url",0);
356396
zSubname = db_get("email-subname", "[Fossil Repo]");
357397
blob_init(&body, 0, 0);
358398
blob_init(&hdr, 0, 0);
359399
if( bIsNew ){
360
- blob_appendf(&body, "User [%q] was created by with "
400
+ blob_appendf(&body, "User [%q] was created with "
361401
"permissions [%q] by user [%q].\n",
362402
zLogin, zNewCaps, g.zLogin);
363403
} else {
364
- blob_appendf(&body, "Permissions for user [%q] where elevated "
404
+ blob_appendf(&body, "Permissions for user [%q] where changed "
365405
"from [%q] to [%q] by user [%q].\n",
366406
zLogin, zOrigCaps, zNewCaps, g.zLogin);
367407
}
368408
if( zURL ){
369409
blob_appendf(&body, "\nUser editor: %s/setup_uedit?uid=%d\n", zURL, uid);
@@ -486,11 +526,11 @@
486526
}else if( !cgi_csrf_safe(2) ){
487527
/* This might be a cross-site request forgery, so ignore it */
488528
}else{
489529
/* We have all the information we need to make the change to the user */
490530
char c;
491
- int bHasNewCaps = 0 /* 1 if user's permissions are increased */;
531
+ int bCapsChanged = 0 /* 1 if user's permissions changed */;
492532
const int bIsNew = uid<=0;
493533
char aCap[70], zNm[4];
494534
zNm[0] = 'a';
495535
zNm[2] = 0;
496536
for(i=0, c='a'; c<='z'; c++){
@@ -508,11 +548,11 @@
508548
a[c&0x7f] = P(zNm)!=0;
509549
if( a[c&0x7f] ) aCap[i++] = c;
510550
}
511551
512552
aCap[i] = 0;
513
- bHasNewCaps = bIsNew || userHasNewCaps(zOldCaps, &aCap[0]);
553
+ bCapsChanged = bIsNew || userCapsChanged(zOldCaps, &aCap[0]);
514554
zPw = P("pw");
515555
zLogin = P("login");
516556
if( strlen(zLogin)==0 ){
517557
const char *zRef = cgi_referer("setup_ulist");
518558
style_header("User Creation Error");
@@ -613,18 +653,20 @@
613653
@ <span class="loginError">%h(zErr)</span>
614654
@
615655
@ <p><a href="setup_uedit?id=%d(uid)&referer=%T(zRef)">
616656
@ [Bummer]</a></p>
617657
style_finish_page();
618
- if( bHasNewCaps ){
619
- alert_user_elevation(zLogin, uid, bIsNew, zOldCaps, &aCap[0]);
658
+ if( bCapsChanged ){
659
+ /* It's possible that caps were updated locally even if
660
+ ** login group updates failed. */
661
+ alert_user_cap_change(zLogin, uid, bIsNew, zOldCaps, &aCap[0]);
620662
}
621663
return;
622664
}
623665
}
624
- if( bHasNewCaps ){
625
- alert_user_elevation(zLogin, uid, bIsNew, zOldCaps, &aCap[0]);
666
+ if( bCapsChanged ){
667
+ alert_user_cap_change(zLogin, uid, bIsNew, zOldCaps, &aCap[0]);
626668
}
627669
cgi_redirect(cgi_referer("setup_ulist"));
628670
return;
629671
}
630672
631673
--- src/setupuser.c
+++ src/setupuser.c
@@ -155,18 +155,19 @@
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 );
@@ -202,11 +203,13 @@
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 +307,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 +373,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 +389,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 +526,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 +548,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 +653,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
@@ -155,18 +155,19 @@
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')," /* 0..4 */
161 " lower(login) AS sortkey, " /* 5 */
162 " CASE WHEN info LIKE '%%expires 20%%'"
163 " THEN substr(info,instr(lower(info),'expires')+8,10)"
164 " END AS exp," /* 6 */
165 "atime," /* 7 */
166 " subscriber.ssub, subscriber.subscriberId," /* 8, 9 */
167 " user.mtime AS sorttime," /* 10 */
168 " subscriber.semail" /* 11 */
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", zWith/*safe-for-%s*/
173 );
@@ -202,11 +203,13 @@
203 if( db_column_type(&s,8)==SQLITE_NULL ){
204 @ <td>
205 }else if( (zSub = db_column_text(&s,8))==0 || zSub[0]==0 ){
206 @ <td><a href="%R/alerts?sid=%d(sid)"><i>off</i></a>
207 }else{
208 const char *zEmail = db_column_text(&s, 11);
209 char * zAt = zEmail ? mprintf(" &rarr; %h", zEmail) : mprintf("");
210 @ <td><a href="%R/alerts?sid=%d(sid)">%h(zSub)</a> %z(zAt)
211 }
212
213 @ </tr>
214 fossil_free(zAge);
215 }
@@ -304,22 +307,59 @@
307 while( zPw[0]=='*' ){ zPw++; }
308 return zPw[0]!=0;
309 }
310
311 /*
312 ** Return true if user capability strings zOrig and zNew materially
313 ** differ, taking into account that they may be sorted in an arbitary
314 ** order. This does not take inherited permissions into
315 ** account. Either argument may be NULL. A NULL and an empty string
316 ** are considered equivalent here. e.g. "abc" and "cab" are equivalent
317 ** for this purpose, but "aCb" and "acb" are not.
318 */
319 static int userCapsChanged(const char *zOrig, const char *zNew){
320 if( !zOrig ){
321 return zNew ? (0!=*zNew) : 0;
322 }else if( !zNew ){
323 return 0!=*zOrig;
324 }else if( 0==fossil_strcmp(zOrig, zNew) ){
325 return 0;
326 }else{
327 /* We don't know that zOrig and zNew are sorted equivalently. The
328 ** following steps will compare strings which contain all the same
329 ** capabilities letters as equivalent, regardless of the letters'
330 ** order in their strings. */
331 char aOrig[128]; /* table of zOrig bytes */
332 int nOrig = 0, nNew = 0;
333
334 memset( &aOrig[0], 0, sizeof(aOrig) );
335 for( ; *zOrig; ++zOrig, ++nOrig ){
336 if( 0==(*zOrig & 0x80) ){
337 aOrig[(int)*zOrig] = 1;
338 }
339 }
340 for( ; *zNew; ++zNew, ++nNew ){
341 if( 0==(*zNew & 0x80) && !aOrig[(int)*zNew] ){
342 return 1;
343 }
344 }
345 return nOrig!=nNew;
346 }
347 }
348
349 /*
350 ** COMMAND: test-user-caps-changed
351 **
352 ** Usage: %fossil test-user-caps-changed caps1 caps2
353 **
354 */
355 void test_user_caps_changed(void){
356
357 char const * zOld = g.argc>2 ? g.argv[2] : NULL;
358 char const * zNew = g.argc>3 ? g.argv[3] : NULL;
359 fossil_print("Has changes? = %d\n",
360 userCapsChanged( zOld, zNew ));
 
361 }
362
363 /*
364 ** Sends notification of user permission elevation changes to all
365 ** subscribers with a "u" subscription. This is a no-op if alerts are
@@ -333,11 +373,11 @@
373 ** edits their subscriptions after an admin assigns them this one,
374 ** this particular one will be lost. "Feature or bug?" is unclear,
375 ** but it would be odd for a non-admin to be assigned this
376 ** capability.
377 */
378 static void alert_user_cap_change(const char *zLogin, /*Affected user*/
379 int uid, /*[user].uid*/
380 int bIsNew, /*true if new user*/
381 const char *zOrigCaps,/*Old caps*/
382 const char *zNewCaps /*New caps*/){
383 Blob hdr, body;
@@ -349,21 +389,21 @@
389 char * zSubject;
390
391 if( !alert_enabled() ) return;
392 zSubject = bIsNew
393 ? mprintf("New user created: [%q]", zLogin)
394 : mprintf("User [%q] capabilities changed", zLogin);
395 zURL = db_get("email-url",0);
396 zSubname = db_get("email-subname", "[Fossil Repo]");
397 blob_init(&body, 0, 0);
398 blob_init(&hdr, 0, 0);
399 if( bIsNew ){
400 blob_appendf(&body, "User [%q] was created with "
401 "permissions [%q] by user [%q].\n",
402 zLogin, zNewCaps, g.zLogin);
403 } else {
404 blob_appendf(&body, "Permissions for user [%q] where changed "
405 "from [%q] to [%q] by user [%q].\n",
406 zLogin, zOrigCaps, zNewCaps, g.zLogin);
407 }
408 if( zURL ){
409 blob_appendf(&body, "\nUser editor: %s/setup_uedit?uid=%d\n", zURL, uid);
@@ -486,11 +526,11 @@
526 }else if( !cgi_csrf_safe(2) ){
527 /* This might be a cross-site request forgery, so ignore it */
528 }else{
529 /* We have all the information we need to make the change to the user */
530 char c;
531 int bCapsChanged = 0 /* 1 if user's permissions changed */;
532 const int bIsNew = uid<=0;
533 char aCap[70], zNm[4];
534 zNm[0] = 'a';
535 zNm[2] = 0;
536 for(i=0, c='a'; c<='z'; c++){
@@ -508,11 +548,11 @@
548 a[c&0x7f] = P(zNm)!=0;
549 if( a[c&0x7f] ) aCap[i++] = c;
550 }
551
552 aCap[i] = 0;
553 bCapsChanged = bIsNew || userCapsChanged(zOldCaps, &aCap[0]);
554 zPw = P("pw");
555 zLogin = P("login");
556 if( strlen(zLogin)==0 ){
557 const char *zRef = cgi_referer("setup_ulist");
558 style_header("User Creation Error");
@@ -613,18 +653,20 @@
653 @ <span class="loginError">%h(zErr)</span>
654 @
655 @ <p><a href="setup_uedit?id=%d(uid)&referer=%T(zRef)">
656 @ [Bummer]</a></p>
657 style_finish_page();
658 if( bCapsChanged ){
659 /* It's possible that caps were updated locally even if
660 ** login group updates failed. */
661 alert_user_cap_change(zLogin, uid, bIsNew, zOldCaps, &aCap[0]);
662 }
663 return;
664 }
665 }
666 if( bCapsChanged ){
667 alert_user_cap_change(zLogin, uid, bIsNew, zOldCaps, &aCap[0]);
668 }
669 cgi_redirect(cgi_referer("setup_ulist"));
670 return;
671 }
672
673
+1 -1
--- src/sitemap.c
+++ src/sitemap.c
@@ -285,11 +285,11 @@
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
--- src/sitemap.c
+++ src/sitemap.c
@@ -285,11 +285,11 @@
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
--- src/sitemap.c
+++ src/sitemap.c
@@ -285,11 +285,11 @@
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
+32 -23
--- src/smtp.c
+++ src/smtp.c
@@ -184,26 +184,23 @@
184184
}
185185
186186
/*
187187
** Allocate a new SmtpSession object.
188188
**
189
-** Both zFrom and zDest must be specified.
190
-**
191
-** The ... arguments are in this order:
189
+** Both zFrom and zDest must be specified. smtpFlags may not contain
190
+** either SMTP_TRACE_FILE or SMTP_TRACE_BLOB as those settings must be
191
+** added by a subsequent call to smtp_session_config().
192192
**
193
-** SMTP_PORT: int
194
-** SMTP_TRACE_FILE: FILE*
195
-** SMTP_TRACE_BLOB: Blob*
193
+** The iPort option is ignored unless SMTP_PORT is set in smtpFlags
196194
*/
197195
SmtpSession *smtp_session_new(
198196
const char *zFrom, /* Domain for the client */
199197
const char *zDest, /* Domain of the server */
200198
u32 smtpFlags, /* Flags */
201
- ... /* Arguments depending on the flags */
199
+ int iPort /* TCP port if the SMTP_PORT flags is present */
202200
){
203201
SmtpSession *p;
204
- va_list ap;
205202
UrlData url;
206203
207204
p = fossil_malloc( sizeof(*p) );
208205
memset(p, 0, sizeof(*p));
209206
p->zFrom = zFrom;
@@ -210,21 +207,13 @@
210207
p->zDest = zDest;
211208
p->smtpFlags = smtpFlags;
212209
memset(&url, 0, sizeof(url));
213210
url.port = 25;
214211
blob_init(&p->inbuf, 0, 0);
215
- va_start(ap, smtpFlags);
216212
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);
213
+ url.port = iPort;
214
+ }
226215
if( (smtpFlags & SMTP_DIRECT)!=0 ){
227216
int i;
228217
p->zHostname = fossil_strdup(zDest);
229218
for(i=0; p->zHostname[i] && p->zHostname[i]!=':'; i++){}
230219
if( p->zHostname[i]==':' ){
@@ -246,10 +235,27 @@
246235
p->zErr = socket_errmsg();
247236
socket_close();
248237
}
249238
return p;
250239
}
240
+
241
+/*
242
+** Configure debugging options on SmtpSession. Add all bits in
243
+** smtpFlags to the settings. The following bits can be added:
244
+**
245
+** SMTP_FLAG_FILE: In which case pArg is the FILE* pointer to use
246
+**
247
+** SMTP_FLAG_BLOB: In which case pArg is the Blob* poitner to use.
248
+*/
249
+void smtp_session_config(SmtpSession *p, u32 smtpFlags, void *pArg){
250
+ p->smtpFlags = smtpFlags;
251
+ if( smtpFlags & SMTP_TRACE_FILE ){
252
+ p->logFile = (FILE*)pArg;
253
+ }else if( smtpFlags & SMTP_TRACE_BLOB ){
254
+ p->pTranscript = (Blob*)pArg;
255
+ }
256
+}
251257
252258
/*
253259
** Send a single line of output the SMTP client to the server.
254260
*/
255261
static void smtp_send_line(SmtpSession *p, const char *zFormat, ...){
@@ -375,15 +381,17 @@
375381
int smtp_client_quit(SmtpSession *p){
376382
Blob in = BLOB_INITIALIZER;
377383
int iCode = 0;
378384
int bMore = 0;
379385
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;
386
+ if( !p->atEof ){
387
+ smtp_send_line(p, "QUIT\r\n");
388
+ do{
389
+ smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg);
390
+ }while( bMore );
391
+ p->atEof = 1;
392
+ }
385393
socket_close();
386394
return 0;
387395
}
388396
389397
/*
@@ -395,10 +403,11 @@
395403
int smtp_client_startup(SmtpSession *p){
396404
Blob in = BLOB_INITIALIZER;
397405
int iCode = 0;
398406
int bMore = 0;
399407
char *zArg = 0;
408
+ if( p==0 || p->atEof ) return 1;
400409
do{
401410
smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg);
402411
}while( bMore );
403412
if( iCode!=220 ){
404413
smtp_client_quit(p);
405414
--- src/smtp.c
+++ src/smtp.c
@@ -184,26 +184,23 @@
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,21 +207,13 @@
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]==':' ){
@@ -246,10 +235,27 @@
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, ...){
@@ -375,15 +381,17 @@
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 /*
@@ -395,10 +403,11 @@
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
--- src/smtp.c
+++ src/smtp.c
@@ -184,26 +184,23 @@
184 }
185
186 /*
187 ** Allocate a new SmtpSession object.
188 **
189 ** Both zFrom and zDest must be specified. smtpFlags may not contain
190 ** either SMTP_TRACE_FILE or SMTP_TRACE_BLOB as those settings must be
191 ** added by a subsequent call to smtp_session_config().
192 **
193 ** The iPort option is ignored unless SMTP_PORT is set in smtpFlags
 
 
194 */
195 SmtpSession *smtp_session_new(
196 const char *zFrom, /* Domain for the client */
197 const char *zDest, /* Domain of the server */
198 u32 smtpFlags, /* Flags */
199 int iPort /* TCP port if the SMTP_PORT flags is present */
200 ){
201 SmtpSession *p;
 
202 UrlData url;
203
204 p = fossil_malloc( sizeof(*p) );
205 memset(p, 0, sizeof(*p));
206 p->zFrom = zFrom;
@@ -210,21 +207,13 @@
207 p->zDest = zDest;
208 p->smtpFlags = smtpFlags;
209 memset(&url, 0, sizeof(url));
210 url.port = 25;
211 blob_init(&p->inbuf, 0, 0);
 
212 if( smtpFlags & SMTP_PORT ){
213 url.port = iPort;
214 }
 
 
 
 
 
 
 
215 if( (smtpFlags & SMTP_DIRECT)!=0 ){
216 int i;
217 p->zHostname = fossil_strdup(zDest);
218 for(i=0; p->zHostname[i] && p->zHostname[i]!=':'; i++){}
219 if( p->zHostname[i]==':' ){
@@ -246,10 +235,27 @@
235 p->zErr = socket_errmsg();
236 socket_close();
237 }
238 return p;
239 }
240
241 /*
242 ** Configure debugging options on SmtpSession. Add all bits in
243 ** smtpFlags to the settings. The following bits can be added:
244 **
245 ** SMTP_FLAG_FILE: In which case pArg is the FILE* pointer to use
246 **
247 ** SMTP_FLAG_BLOB: In which case pArg is the Blob* poitner to use.
248 */
249 void smtp_session_config(SmtpSession *p, u32 smtpFlags, void *pArg){
250 p->smtpFlags = smtpFlags;
251 if( smtpFlags & SMTP_TRACE_FILE ){
252 p->logFile = (FILE*)pArg;
253 }else if( smtpFlags & SMTP_TRACE_BLOB ){
254 p->pTranscript = (Blob*)pArg;
255 }
256 }
257
258 /*
259 ** Send a single line of output the SMTP client to the server.
260 */
261 static void smtp_send_line(SmtpSession *p, const char *zFormat, ...){
@@ -375,15 +381,17 @@
381 int smtp_client_quit(SmtpSession *p){
382 Blob in = BLOB_INITIALIZER;
383 int iCode = 0;
384 int bMore = 0;
385 char *zArg = 0;
386 if( !p->atEof ){
387 smtp_send_line(p, "QUIT\r\n");
388 do{
389 smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg);
390 }while( bMore );
391 p->atEof = 1;
392 }
393 socket_close();
394 return 0;
395 }
396
397 /*
@@ -395,10 +403,11 @@
403 int smtp_client_startup(SmtpSession *p){
404 Blob in = BLOB_INITIALIZER;
405 int iCode = 0;
406 int bMore = 0;
407 char *zArg = 0;
408 if( p==0 || p->atEof ) return 1;
409 do{
410 smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg);
411 }while( bMore );
412 if( iCode!=220 ){
413 smtp_client_quit(p);
414
--- 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
+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
+6 -5
--- src/style.c
+++ src/style.c
@@ -746,11 +746,11 @@
746746
Th_MaybeStore("default_csp", zDfltCsp);
747747
fossil_free(zDfltCsp);
748748
Th_Store("nonce", zNonce);
749749
Th_Store("project_name", db_get("project-name","Unnamed Fossil Project"));
750750
Th_Store("project_description", db_get("project-description",""));
751
- if( zTitle ) Th_Store("title", zTitle);
751
+ if( zTitle ) Th_Store("title", html_lookalike(zTitle,-1));
752752
Th_Store("baseurl", g.zBaseURL);
753753
Th_Store("secureurl", fossil_wants_https(1)? g.zHttpsURL: g.zBaseURL);
754754
Th_Store("home", g.zTop);
755755
Th_Store("index_page", db_get("index-page","/home"));
756756
if( local_zCurrentPage==0 ) style_set_current_page("%T", g.zPath);
@@ -772,11 +772,11 @@
772772
Th_Store("mainmenu", style_get_mainmenu());
773773
stylesheet_url_var();
774774
image_url_var("logo");
775775
image_url_var("background");
776776
if( !login_is_nobody() ){
777
- Th_Store("login", g.zLogin);
777
+ Th_Store("login", html_lookalike(g.zLogin,-1));
778778
}
779779
Th_MaybeStore("current_feature", feature_from_page_path(local_zCurrentPage) );
780780
if( g.ftntsIssues[0] || g.ftntsIssues[1] ||
781781
g.ftntsIssues[2] || g.ftntsIssues[3] ){
782782
char buf[80];
@@ -1375,11 +1375,12 @@
13751375
@ </form>
13761376
style_finish_page();
13771377
}
13781378
13791379
/*
1380
-** WEBPAGE: test_env
1380
+** WEBPAGE: test-env
1381
+** WEBPAGE: test_env alias
13811382
**
13821383
** Display CGI-variables and other aspects of the run-time
13831384
** environment, for debugging and trouble-shooting purposes.
13841385
*/
13851386
void page_test_env(void){
@@ -1438,11 +1439,11 @@
14381439
**
14391440
** For administators, or if the test_env_enable setting is true, then
14401441
** details of the request environment are displayed. Otherwise, just
14411442
** the error message is shown.
14421443
**
1443
-** If zFormat is an empty string, then this is the /test_env page.
1444
+** If zFormat is an empty string, then this is the /test-env page.
14441445
*/
14451446
void webpage_error(const char *zFormat, ...){
14461447
int showAll = 0;
14471448
char *zErr = 0;
14481449
int isAuth = 0;
@@ -1538,11 +1539,11 @@
15381539
}
15391540
@ <hr>
15401541
P("HTTP_USER_AGENT");
15411542
P("SERVER_SOFTWARE");
15421543
cgi_print_all(showAll, 0, 0);
1543
- @ <p><form method="POST" action="%R/test_env">
1544
+ @ <p><form method="POST" action="%R/test-env">
15441545
@ <input type="hidden" name="showall" value="%d(showAll)">
15451546
@ <input type="submit" name="post-test-button" value="POST Test">
15461547
@ </form>
15471548
if( showAll && blob_size(&g.httpHeader)>0 ){
15481549
@ <hr>
15491550
--- src/style.c
+++ src/style.c
@@ -746,11 +746,11 @@
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 +772,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];
@@ -1375,11 +1375,12 @@
1375 @ </form>
1376 style_finish_page();
1377 }
1378
1379 /*
1380 ** WEBPAGE: test_env
 
1381 **
1382 ** Display CGI-variables and other aspects of the run-time
1383 ** environment, for debugging and trouble-shooting purposes.
1384 */
1385 void page_test_env(void){
@@ -1438,11 +1439,11 @@
1438 **
1439 ** For administators, or if the test_env_enable setting is true, then
1440 ** details of the request environment are displayed. Otherwise, just
1441 ** the error message is shown.
1442 **
1443 ** If zFormat is an empty string, then this is the /test_env page.
1444 */
1445 void webpage_error(const char *zFormat, ...){
1446 int showAll = 0;
1447 char *zErr = 0;
1448 int isAuth = 0;
@@ -1538,11 +1539,11 @@
1538 }
1539 @ <hr>
1540 P("HTTP_USER_AGENT");
1541 P("SERVER_SOFTWARE");
1542 cgi_print_all(showAll, 0, 0);
1543 @ <p><form method="POST" action="%R/test_env">
1544 @ <input type="hidden" name="showall" value="%d(showAll)">
1545 @ <input type="submit" name="post-test-button" value="POST Test">
1546 @ </form>
1547 if( showAll && blob_size(&g.httpHeader)>0 ){
1548 @ <hr>
1549
--- src/style.c
+++ src/style.c
@@ -746,11 +746,11 @@
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", html_lookalike(zTitle,-1));
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 +772,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", html_lookalike(g.zLogin,-1));
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];
@@ -1375,11 +1375,12 @@
1375 @ </form>
1376 style_finish_page();
1377 }
1378
1379 /*
1380 ** WEBPAGE: test-env
1381 ** WEBPAGE: test_env alias
1382 **
1383 ** Display CGI-variables and other aspects of the run-time
1384 ** environment, for debugging and trouble-shooting purposes.
1385 */
1386 void page_test_env(void){
@@ -1438,11 +1439,11 @@
1439 **
1440 ** For administators, or if the test_env_enable setting is true, then
1441 ** details of the request environment are displayed. Otherwise, just
1442 ** the error message is shown.
1443 **
1444 ** If zFormat is an empty string, then this is the /test-env page.
1445 */
1446 void webpage_error(const char *zFormat, ...){
1447 int showAll = 0;
1448 char *zErr = 0;
1449 int isAuth = 0;
@@ -1538,11 +1539,11 @@
1539 }
1540 @ <hr>
1541 P("HTTP_USER_AGENT");
1542 P("SERVER_SOFTWARE");
1543 cgi_print_all(showAll, 0, 0);
1544 @ <p><form method="POST" action="%R/test-env">
1545 @ <input type="hidden" name="showall" value="%d(showAll)">
1546 @ <input type="submit" name="post-test-button" value="POST Test">
1547 @ </form>
1548 if( showAll && blob_size(&g.httpHeader)>0 ){
1549 @ <hr>
1550
--- 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
+8 -2
--- src/th.c
+++ src/th.c
@@ -2160,12 +2160,15 @@
21602160
}
21612161
iRes = iLeft%iRight;
21622162
break;
21632163
case OP_ADD: iRes = iLeft+iRight; break;
21642164
case OP_SUBTRACT: iRes = iLeft-iRight; break;
2165
- case OP_LEFTSHIFT: iRes = iLeft<<iRight; break;
2166
- case OP_RIGHTSHIFT: iRes = iLeft>>iRight; break;
2165
+ case OP_LEFTSHIFT: {
2166
+ iRes = (int)(((unsigned int)iLeft)<<(iRight&0x1f));
2167
+ break;
2168
+ }
2169
+ case OP_RIGHTSHIFT: iRes = iLeft>>(iRight&0x1f); break;
21672170
case OP_LT: iRes = iLeft<iRight; break;
21682171
case OP_GT: iRes = iLeft>iRight; break;
21692172
case OP_LE: iRes = iLeft<=iRight; break;
21702173
case OP_GE: iRes = iLeft>=iRight; break;
21712174
case OP_EQ: iRes = iLeft==iRight; break;
@@ -2875,10 +2878,13 @@
28752878
unsigned int uVal = iVal;
28762879
char zBuf[32];
28772880
char *z = &zBuf[32];
28782881
28792882
if( iVal<0 ){
2883
+ if( iVal==0x80000000 ){
2884
+ return Th_SetResult(interp, "-2147483648", -1);
2885
+ }
28802886
isNegative = 1;
28812887
uVal = iVal * -1;
28822888
}
28832889
*(--z) = '\0';
28842890
*(--z) = (char)(48+(uVal%10));
28852891
--- src/th.c
+++ src/th.c
@@ -2160,12 +2160,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;
@@ -2875,10 +2878,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
@@ -2160,12 +2160,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: {
2166 iRes = (int)(((unsigned int)iLeft)<<(iRight&0x1f));
2167 break;
2168 }
2169 case OP_RIGHTSHIFT: iRes = iLeft>>(iRight&0x1f); break;
2170 case OP_LT: iRes = iLeft<iRight; break;
2171 case OP_GT: iRes = iLeft>iRight; break;
2172 case OP_LE: iRes = iLeft<=iRight; break;
2173 case OP_GE: iRes = iLeft>=iRight; break;
2174 case OP_EQ: iRes = iLeft==iRight; break;
@@ -2875,10 +2878,13 @@
2878 unsigned int uVal = iVal;
2879 char zBuf[32];
2880 char *z = &zBuf[32];
2881
2882 if( iVal<0 ){
2883 if( iVal==0x80000000 ){
2884 return Th_SetResult(interp, "-2147483648", -1);
2885 }
2886 isNegative = 1;
2887 uVal = iVal * -1;
2888 }
2889 *(--z) = '\0';
2890 *(--z) = (char)(48+(uVal%10));
2891
+24 -4
--- src/timeline.c
+++ src/timeline.c
@@ -598,10 +598,19 @@
598598
drawDetailEllipsis = 0;
599599
}else{
600600
cgi_printf("%W",blob_str(&comment));
601601
}
602602
}
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
+
603612
@ </span>
604613
blob_reset(&comment);
605614
606615
/* Generate extra information and hyperlinks to follow the comment.
607616
** Example: "(check-in: [abcdefg], user: drh, tags: trunk)"
@@ -3740,15 +3749,22 @@
37403749
}
37413750
}
37423751
37433752
if( mode==TIMELINE_MODE_NONE ) mode = TIMELINE_MODE_BEFORE;
37443753
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
+ }
37453758
blob_append(&sql, timeline_query_for_tty(), -1);
37463759
blob_append_sql(&sql, "\n AND event.mtime %s %s",
37473760
( mode==TIMELINE_MODE_BEFORE ||
37483761
mode==TIMELINE_MODE_PARENTS ) ? "<=" : ">=", zDate /*safe-for-%s*/
37493762
);
3763
+ if( zType && (zType[0]!='a') ){
3764
+ blob_append_sql(&sql, "\n AND event.type=%Q ", zType);
3765
+ }
37503766
37513767
/* When zFilePattern is specified, compute complete ancestry;
37523768
* limit later at print_timeline() */
37533769
if( mode==TIMELINE_MODE_CHILDREN || mode==TIMELINE_MODE_PARENTS ){
37543770
db_multi_exec("CREATE TEMP TABLE ok(rid INTEGER PRIMARY KEY)");
@@ -3757,13 +3773,10 @@
37573773
}else{
37583774
compute_ancestors(objid, (zFilePattern ? 0 : n), 0, 0);
37593775
}
37603776
blob_append_sql(&sql, "\n AND blob.rid IN ok");
37613777
}
3762
- if( zType && (zType[0]!='a') ){
3763
- blob_append_sql(&sql, "\n AND event.type=%Q ", zType);
3764
- }
37653778
if( zFilePattern ){
37663779
blob_append(&sql,
37673780
"\n AND EXISTS(SELECT 1 FROM mlink\n"
37683781
" WHERE mlink.mid=event.objid\n"
37693782
" AND mlink.fnid IN ", -1);
@@ -3801,11 +3814,18 @@
38013814
" WHERE tx.value='%q'\n"
38023815
")\n" /* No merge closures */
38033816
" AND (tagxref.value IS NULL OR tagxref.value='%q')",
38043817
zBr, zBr, zBr, TAG_BRANCH, zBr, zBr);
38053818
}
3806
- 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
+ }
38073827
if( iOffset>0 ){
38083828
/* Don't handle LIMIT here, otherwise print_timeline()
38093829
* will not determine the end-marker correctly! */
38103830
blob_append_sql(&sql, "\n LIMIT -1 OFFSET %d", iOffset);
38113831
}
38123832
--- src/timeline.c
+++ src/timeline.c
@@ -598,10 +598,19 @@
598 drawDetailEllipsis = 0;
599 }else{
600 cgi_printf("%W",blob_str(&comment));
601 }
602 }
 
 
 
 
 
 
 
 
 
603 @ </span>
604 blob_reset(&comment);
605
606 /* Generate extra information and hyperlinks to follow the comment.
607 ** Example: "(check-in: [abcdefg], user: drh, tags: trunk)"
@@ -3740,15 +3749,22 @@
3740 }
3741 }
3742
3743 if( mode==TIMELINE_MODE_NONE ) mode = TIMELINE_MODE_BEFORE;
3744 blob_zero(&sql);
 
 
 
 
3745 blob_append(&sql, timeline_query_for_tty(), -1);
3746 blob_append_sql(&sql, "\n AND event.mtime %s %s",
3747 ( mode==TIMELINE_MODE_BEFORE ||
3748 mode==TIMELINE_MODE_PARENTS ) ? "<=" : ">=", zDate /*safe-for-%s*/
3749 );
 
 
 
3750
3751 /* When zFilePattern is specified, compute complete ancestry;
3752 * limit later at print_timeline() */
3753 if( mode==TIMELINE_MODE_CHILDREN || mode==TIMELINE_MODE_PARENTS ){
3754 db_multi_exec("CREATE TEMP TABLE ok(rid INTEGER PRIMARY KEY)");
@@ -3757,13 +3773,10 @@
3757 }else{
3758 compute_ancestors(objid, (zFilePattern ? 0 : n), 0, 0);
3759 }
3760 blob_append_sql(&sql, "\n AND blob.rid IN ok");
3761 }
3762 if( zType && (zType[0]!='a') ){
3763 blob_append_sql(&sql, "\n AND event.type=%Q ", zType);
3764 }
3765 if( zFilePattern ){
3766 blob_append(&sql,
3767 "\n AND EXISTS(SELECT 1 FROM mlink\n"
3768 " WHERE mlink.mid=event.objid\n"
3769 " AND mlink.fnid IN ", -1);
@@ -3801,11 +3814,18 @@
3801 " WHERE tx.value='%q'\n"
3802 ")\n" /* No merge closures */
3803 " AND (tagxref.value IS NULL OR tagxref.value='%q')",
3804 zBr, zBr, zBr, TAG_BRANCH, zBr, zBr);
3805 }
3806 blob_append_sql(&sql, "\nORDER BY event.mtime DESC");
 
 
 
 
 
 
 
3807 if( iOffset>0 ){
3808 /* Don't handle LIMIT here, otherwise print_timeline()
3809 * will not determine the end-marker correctly! */
3810 blob_append_sql(&sql, "\n LIMIT -1 OFFSET %d", iOffset);
3811 }
3812
--- src/timeline.c
+++ src/timeline.c
@@ -598,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)"
@@ -3740,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)");
@@ -3757,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);
@@ -3801,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
+7 -5
--- src/tkt.c
+++ src/tkt.c
@@ -1026,15 +1026,13 @@
10261026
form_begin(0, "%R/%s", g.zPath);
10271027
if( P("date_override") && g.perm.Setup ){
10281028
@ <input type="hidden" name="date_override" value="%h(P("date_override"))">
10291029
}
10301030
zScript = ticket_newpage_code();
1031
+ Th_Store("private_contact", "");
10311032
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);
1033
+ uid = db_int(0, "SELECT uid FROM user WHERE login=%Q", g.zLogin);
10361034
if( uid ){
10371035
char * zEmail =
10381036
db_text(0, "SELECT find_emailaddr(info) FROM user WHERE uid=%d",
10391037
uid);
10401038
if( zEmail ){
@@ -1047,11 +1045,15 @@
10471045
Th_Store("date", db_text(0, "SELECT datetime('now')"));
10481046
Th_CreateCommand(g.interp, "submit_ticket", submitTicketCmd,
10491047
(void*)&zNewUuid, 0);
10501048
if( g.thTrace ) Th_Trace("BEGIN_TKTNEW_SCRIPT<br>\n", -1);
10511049
if( Th_Render(zScript)==TH_RETURN && !g.thTrace && zNewUuid ){
1052
- cgi_redirect(mprintf("%R/tktview/%s", zNewUuid));
1050
+ if( P("submitandnew") ){
1051
+ cgi_redirect(mprintf("%R/tktnew/%s", zNewUuid));
1052
+ }else{
1053
+ cgi_redirect(mprintf("%R/tktview/%s", zNewUuid));
1054
+ }
10531055
return;
10541056
}
10551057
captcha_generate(0);
10561058
@ </form>
10571059
if( g.thTrace ) Th_Trace("END_TKTVIEW<br>\n", -1);
10581060
--- src/tkt.c
+++ src/tkt.c
@@ -1026,15 +1026,13 @@
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 ){
@@ -1047,11 +1045,15 @@
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);
1058
--- src/tkt.c
+++ src/tkt.c
@@ -1026,15 +1026,13 @@
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 Th_Store("private_contact", "");
1032 if( g.zLogin && g.zLogin[0] ){
1033 uid = db_int(0, "SELECT uid FROM user WHERE login=%Q", g.zLogin);
 
 
 
1034 if( uid ){
1035 char * zEmail =
1036 db_text(0, "SELECT find_emailaddr(info) FROM user WHERE uid=%d",
1037 uid);
1038 if( zEmail ){
@@ -1047,11 +1045,15 @@
1045 Th_Store("date", db_text(0, "SELECT datetime('now')"));
1046 Th_CreateCommand(g.interp, "submit_ticket", submitTicketCmd,
1047 (void*)&zNewUuid, 0);
1048 if( g.thTrace ) Th_Trace("BEGIN_TKTNEW_SCRIPT<br>\n", -1);
1049 if( Th_Render(zScript)==TH_RETURN && !g.thTrace && zNewUuid ){
1050 if( P("submitandnew") ){
1051 cgi_redirect(mprintf("%R/tktnew/%s", zNewUuid));
1052 }else{
1053 cgi_redirect(mprintf("%R/tktview/%s", zNewUuid));
1054 }
1055 return;
1056 }
1057 captcha_generate(0);
1058 @ </form>
1059 if( g.thTrace ) Th_Trace("END_TKTVIEW<br>\n", -1);
1060
+33 -3
--- src/tktsetup.c
+++ src/tktsetup.c
@@ -301,11 +301,11 @@
301301
}
302302
303303
static const char zDefaultNew[] =
304304
@ <th1>
305305
@ if {![info exists mutype]} {set mutype Markdown}
306
-@ if {[info exists submit]} {
306
+@ if {[info exists submit] || [info exists submitandnew]} {
307307
@ set status Open
308308
@ if {$mutype eq "HTML"} {
309309
@ set mimetype "text/html"
310310
@ } elseif {$mutype eq "Wiki"} {
311311
@ set mimetype "text/x-fossil-wiki"
@@ -349,10 +349,28 @@
349349
@ <td align="left"><th1>combobox severity $severity_choices 1</th1></td>
350350
@ <td align="left">How debilitating is the problem? How badly does the problem
351351
@ affect the operation of the product?</td>
352352
@ </tr>
353353
@
354
+@ <th1>
355
+@ if {[capexpr {w}]} {
356
+@ html {<tr><td class="tktDspLabel">Priority:</td><td>}
357
+@ combobox priority $priority_choices 1
358
+@ html {
359
+@ <td align="left">How important is the affected functionality?</td>
360
+@ </td></tr>
361
+@ }
362
+@
363
+@ html {<tr><td class="tktDspLabel">Subsystem:</td><td>}
364
+@ combobox subsystem $subsystem_choices 1
365
+@ html {
366
+@ <td align="left">Which subsystem is affected?</td>
367
+@ </td></tr>
368
+@ }
369
+@ }
370
+@ </th1>
371
+@
354372
@ <tr>
355373
@ <td align="right">EMail:</td>
356374
@ <td align="left">
357375
@ <input name="private_contact" value="$<private_contact>" size="30">
358376
@ </td>
@@ -405,19 +423,27 @@
405423
@ <tr>
406424
@ <td><td align="left">
407425
@ <input type="submit" name="submit" value="Submit">
408426
@ </td>
409427
@ <td align="left">After filling in the information above, press this
410
-@ button to create the new ticket</td>
428
+@ button to create the new ticket.</td>
429
+@ </tr>
430
+@
431
+@ <tr>
432
+@ <td><td align="left">
433
+@ <input type="submit" name="submitandnew" value="Submit and New">
434
+@ </td>
435
+@ <td align="left">Create the new ticket and start another
436
+@ ticket form with the inputs.</td>
411437
@ </tr>
412438
@ <th1>enable_output 1</th1>
413439
@
414440
@ <tr>
415441
@ <td><td align="left">
416442
@ <input type="submit" name="cancel" value="Cancel">
417443
@ </td>
418
-@ <td>Abandon and forget this ticket</td>
444
+@ <td>Abandon and forget this ticket.</td>
419445
@ </tr>
420446
@ </table>
421447
;
422448
423449
/*
@@ -465,10 +491,14 @@
465491
@ html "(0)</td></tr>\n"
466492
@ } else {
467493
@ html "<td class='tktDspValue' colspan='3'>Deleted</td></tr>\n"
468494
@ }
469495
@ }
496
+@
497
+@ if {[capexpr {n}]} {
498
+@ submenu link "Copy Ticket" /tktnew/$tkt_uuid
499
+@ }
470500
@ </th1>
471501
@ <tr><td class="tktDspLabel">Title:</td>
472502
@ <td class="tktDspValue" colspan="3">
473503
@ $<title>
474504
@ </td></tr>
475505
--- src/tktsetup.c
+++ src/tktsetup.c
@@ -301,11 +301,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 +349,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 +423,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 /*
@@ -465,10 +491,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>
475
--- src/tktsetup.c
+++ src/tktsetup.c
@@ -301,11 +301,11 @@
301 }
302
303 static const char zDefaultNew[] =
304 @ <th1>
305 @ if {![info exists mutype]} {set mutype Markdown}
306 @ if {[info exists submit] || [info exists submitandnew]} {
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 +349,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 @ <th1>
355 @ if {[capexpr {w}]} {
356 @ html {<tr><td class="tktDspLabel">Priority:</td><td>}
357 @ combobox priority $priority_choices 1
358 @ html {
359 @ <td align="left">How important is the affected functionality?</td>
360 @ </td></tr>
361 @ }
362 @
363 @ html {<tr><td class="tktDspLabel">Subsystem:</td><td>}
364 @ combobox subsystem $subsystem_choices 1
365 @ html {
366 @ <td align="left">Which subsystem is affected?</td>
367 @ </td></tr>
368 @ }
369 @ }
370 @ </th1>
371 @
372 @ <tr>
373 @ <td align="right">EMail:</td>
374 @ <td align="left">
375 @ <input name="private_contact" value="$<private_contact>" size="30">
376 @ </td>
@@ -405,19 +423,27 @@
423 @ <tr>
424 @ <td><td align="left">
425 @ <input type="submit" name="submit" value="Submit">
426 @ </td>
427 @ <td align="left">After filling in the information above, press this
428 @ button to create the new ticket.</td>
429 @ </tr>
430 @
431 @ <tr>
432 @ <td><td align="left">
433 @ <input type="submit" name="submitandnew" value="Submit and New">
434 @ </td>
435 @ <td align="left">Create the new ticket and start another
436 @ ticket form with the inputs.</td>
437 @ </tr>
438 @ <th1>enable_output 1</th1>
439 @
440 @ <tr>
441 @ <td><td align="left">
442 @ <input type="submit" name="cancel" value="Cancel">
443 @ </td>
444 @ <td>Abandon and forget this ticket.</td>
445 @ </tr>
446 @ </table>
447 ;
448
449 /*
@@ -465,10 +491,14 @@
491 @ html "(0)</td></tr>\n"
492 @ } else {
493 @ html "<td class='tktDspValue' colspan='3'>Deleted</td></tr>\n"
494 @ }
495 @ }
496 @
497 @ if {[capexpr {n}]} {
498 @ submenu link "Copy Ticket" /tktnew/$tkt_uuid
499 @ }
500 @ </th1>
501 @ <tr><td class="tktDspLabel">Title:</td>
502 @ <td class="tktDspValue" colspan="3">
503 @ $<title>
504 @ </td></tr>
505
+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
--- 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
3636
ADDED tools/find-fossil-cgis.tcl
--- 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
36 DDED tools/find-fossil-cgis.tcl
--- 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
36 DDED tools/find-fossil-cgis.tcl
--- 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
+16 -10
--- www/changes.wiki
+++ www/changes.wiki
@@ -2,11 +2,11 @@
22
33
<h2 id='v2_26'>Changes for version 2.26 (pending)</h2>
44
55
* Enhancements to [/help?cmd=diff|fossil diff] and similar:
66
<ol type="a">
7
- <li> The --from can optionally accepts a directory name as its argument,
7
+ <li> The --from can optionally accept a directory name as its argument,
88
and uses files under that directory as the baseline for the diff.
99
<li> For "gdiff", if no [/help?cmd=gdiff-command|gdiff-command setting]
1010
is defined, Fossil tries to do a --tk diff if "tclsh" and "wish"
1111
are available, or a --by diff if not.
1212
<li> The "Reload" button is added to --tk diffs, to bring the displayed
@@ -21,11 +21,11 @@
2121
<li> Defaults to using the new [/help?cmd=/ckout|/ckout page] as its
2222
start page. Or, if the new "--from PATH" option is present, the
2323
default start page becomes "/ckout?exbase=PATH".
2424
<li> The new "--extpage FILENAME" option opens the named file as if it
2525
where in a [./serverext.wiki|CGI extension]. Example usage: the
26
- person editing this change log has
26
+ person editing this change log has
2727
"fossil ui --extpage www/changes.wiki" running and hence can
2828
press "Reload" on the web browser to view edits.
2929
</ol>
3030
* Enhancements to [/help?cmd=merge|fossil merge]:
3131
<ol type="a">
@@ -85,15 +85,13 @@
8585
end-points.
8686
<li> The p= and d= parameters an reference different check-ins, which
8787
case the timeline shows those check-ins that are both ancestors
8888
of p= and descendants of d=.
8989
<li> The saturation and intensity of user-specified checkin and branch
90
- colors are automatically adjusted to keep the colors
90
+ background colors are automatically adjusted to keep the colors
9191
compatible with the current skin, unless the
92
- [/help?cmd=raw-bgcolor|raw-bgcolor setting] is turned on. The
93
- /test-bgcolor page was added to
94
- test and visualize how these adjustments.
92
+ [/help?cmd=raw-bgcolor|raw-bgcolor setting] is turned on.
9593
</ol>
9694
* The [/help?cmd=/docfile|/docfile webpage] was added. It works like
9795
/doc but keeps the title of markdown documents with the document rather
9896
that moving it up to the page title.
9997
* Added the [/help?cmd=/clusterlist|/clusterlist page] for analysis
@@ -118,17 +116,25 @@
118116
COMMAND argument and only shows results for the specified
119117
subcommand, not the entire command.
120118
<li> The -u (--usage) option shows only the command-line syntax
121119
<li> The -o (--options) option shows only the command-line options
122120
</ol>
123
- * Added the ability to attach wiki pages to a ticket for extended
124
- descriptions.
121
+ * Enhancements to the ticket system:
122
+ <ol type="a">
123
+ <li> Added the ability to attach wiki pages to a ticket for extended
124
+ descriptions.
125
+ <li> Added submenu to the 'View Ticket' page, to use it as
126
+ template for a new ticket.
127
+ <li> Added button 'Submit and New' to create multiple tickets
128
+ in a row.
129
+ </ol>
125130
* Added the "hash" query parameter to the
126131
[/help?cmd=/whatis|/whatis webpage].
127
- * Add a "user elevation" [/doc/trunk/www/alerts.md|subscription]
132
+ * Add a "user permissions changes" [/doc/trunk/www/alerts.md|subscription]
128133
which alerts subscribers when an admin creates a new user or
129
- adds new permissions to one.
134
+ when a user's permissions change.
135
+ * Show project description on repository list.
130136
* Diverse minor fixes and additions.
131137
132138
133139
<h2 id='v2_25'>Changes for version 2.25 (2024-11-06)</h2>
134140
135141
--- www/changes.wiki
+++ www/changes.wiki
@@ -2,11 +2,11 @@
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
@@ -21,11 +21,11 @@
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">
@@ -85,15 +85,13 @@
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 <li> The saturation and intensity of user-specified checkin and branch
90 colors are automatically adjusted to keep the colors
91 compatible with the current skin, unless the
92 [/help?cmd=raw-bgcolor|raw-bgcolor setting] is turned on. The
93 /test-bgcolor page was added to
94 test and visualize how these adjustments.
95 </ol>
96 * The [/help?cmd=/docfile|/docfile webpage] was added. It works like
97 /doc but keeps the title of markdown documents with the document rather
98 that moving it up to the page title.
99 * Added the [/help?cmd=/clusterlist|/clusterlist page] for analysis
@@ -118,17 +116,25 @@
118 COMMAND argument and only shows results for the specified
119 subcommand, not the entire command.
120 <li> The -u (--usage) option shows only the command-line syntax
121 <li> The -o (--options) option shows only the command-line options
122 </ol>
123 * Added the ability to attach wiki pages to a ticket for extended
124 descriptions.
 
 
 
 
 
 
 
125 * Added the "hash" query parameter to the
126 [/help?cmd=/whatis|/whatis webpage].
127 * Add a "user elevation" [/doc/trunk/www/alerts.md|subscription]
128 which alerts subscribers when an admin creates a new user or
129 adds new permissions to one.
 
130 * Diverse minor fixes and additions.
131
132
133 <h2 id='v2_25'>Changes for version 2.25 (2024-11-06)</h2>
134
135
--- www/changes.wiki
+++ www/changes.wiki
@@ -2,11 +2,11 @@
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 accept 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
@@ -21,11 +21,11 @@
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">
@@ -85,15 +85,13 @@
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 <li> The saturation and intensity of user-specified checkin and branch
90 background colors are automatically adjusted to keep the colors
91 compatible with the current skin, unless the
92 [/help?cmd=raw-bgcolor|raw-bgcolor setting] is turned on.
 
 
93 </ol>
94 * The [/help?cmd=/docfile|/docfile webpage] was added. It works like
95 /doc but keeps the title of markdown documents with the document rather
96 that moving it up to the page title.
97 * Added the [/help?cmd=/clusterlist|/clusterlist page] for analysis
@@ -118,17 +116,25 @@
116 COMMAND argument and only shows results for the specified
117 subcommand, not the entire command.
118 <li> The -u (--usage) option shows only the command-line syntax
119 <li> The -o (--options) option shows only the command-line options
120 </ol>
121 * Enhancements to the ticket system:
122 <ol type="a">
123 <li> Added the ability to attach wiki pages to a ticket for extended
124 descriptions.
125 <li> Added submenu to the 'View Ticket' page, to use it as
126 template for a new ticket.
127 <li> Added button 'Submit and New' to create multiple tickets
128 in a row.
129 </ol>
130 * Added the "hash" query parameter to the
131 [/help?cmd=/whatis|/whatis webpage].
132 * Add a "user permissions changes" [/doc/trunk/www/alerts.md|subscription]
133 which alerts subscribers when an admin creates a new user or
134 when a user's permissions change.
135 * Show project description on repository list.
136 * Diverse minor fixes and additions.
137
138
139 <h2 id='v2_25'>Changes for version 2.25 (2024-11-06)</h2>
140
141
+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
+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
--- www/quickstart.wiki
+++ www/quickstart.wiki
@@ -14,11 +14,11 @@
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
2424
Fossil works with repository files (a database in a single file with the project's
@@ -48,12 +48,38 @@
4848
4949
<pre><b>fossil init</b> <i>repository-filename</i>
5050
</pre>
5151
5252
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.”
53
+The <tt>.fossil</tt> extension is traditional, but it is only required if you are going to use the
54
+<tt>[/help/server | fossil server DIRECTORY]</tt> feature.
55
+
56
+Next, do something along the lines of:
57
+
58
+<pre>
59
+<b>mkdir -p ~/src/project/trunk</b>
60
+<b>cd ~/src/project/trunk</b>
61
+<b>fossil open</b> <i>repository-filename</i>
62
+<b>fossil add</b> foo.c bar.h qux.md
63
+<b>fossil commit</b>
64
+</pre>
65
+
66
+If your project directory already exists, obviating the <b>mkdir</b>
67
+step, you will instead need to add the <tt>--force</tt> flag to the
68
+<b>open</b> command to authorize Fossil to open the repo into a
69
+non-empty checkout directory. (This is to avoid accidental opens into,
70
+for example, your home directory.)
71
+
72
+The convention of naming your checkout directory after a long-lived
73
+branch name like "trunk" is in support of Fossil's ability to have as
74
+many open checkouts as you like. This author frequently has additional
75
+checkout directories named <tt>../release</tt>, <tt>../scratch</tt>,
76
+etc. The release directory is open to the branch of the same name, while
77
+the scratch directory is used when disturbing one of the other
78
+long-lived checkout directories is undesireable, as when performing a
79
+[/help/bisect | bisect] operation.
80
+
5581
5682
<h2 id="clone">Cloning An Existing Repository</h2>
5783
5884
Most fossil operations interact with a repository that is on the
5985
local disk drive, not on a remote system. Hence, before accessing
@@ -384,16 +410,12 @@
384410
them and fails if local changes exist unless the <tt>--force</tt>
385411
flag is used.
386412
387413
<h2 id="branch" name="merge">Branching And Merging</h2>
388414
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.
415
+Use the --branch option to the [/help/commit | commit] command to start
416
+a new branch at the point of need. ([./gitusers.md#bneed | Contrast git].)
395417
396418
To merge two branches back together, first
397419
[/help/update | update] to the branch you want to merge into.
398420
Then do a [/help/merge|merge] of the other branch that you want to incorporate
399421
the changes from. For example, to merge "featureX" changes into "trunk"
400422
--- www/quickstart.wiki
+++ www/quickstart.wiki
@@ -14,11 +14,11 @@
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
@@ -48,12 +48,38 @@
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
@@ -384,16 +410,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"
400
--- www/quickstart.wiki
+++ www/quickstart.wiki
@@ -14,11 +14,11 @@
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 repository files (a database in a single file with the project's
@@ -48,12 +48,38 @@
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 it is only required if you are going to use the
54 <tt>[/help/server | fossil server DIRECTORY]</tt> feature.
55
56 Next, do something along the lines of:
57
58 <pre>
59 <b>mkdir -p ~/src/project/trunk</b>
60 <b>cd ~/src/project/trunk</b>
61 <b>fossil open</b> <i>repository-filename</i>
62 <b>fossil add</b> foo.c bar.h qux.md
63 <b>fossil commit</b>
64 </pre>
65
66 If your project directory already exists, obviating the <b>mkdir</b>
67 step, you will instead need to add the <tt>--force</tt> flag to the
68 <b>open</b> command to authorize Fossil to open the repo into a
69 non-empty checkout directory. (This is to avoid accidental opens into,
70 for example, your home directory.)
71
72 The convention of naming your checkout directory after a long-lived
73 branch name like "trunk" is in support of Fossil's ability to have as
74 many open checkouts as you like. This author frequently has additional
75 checkout directories named <tt>../release</tt>, <tt>../scratch</tt>,
76 etc. The release directory is open to the branch of the same name, while
77 the scratch directory is used when disturbing one of the other
78 long-lived checkout directories is undesireable, as when performing a
79 [/help/bisect | bisect] operation.
80
81
82 <h2 id="clone">Cloning An Existing Repository</h2>
83
84 Most fossil operations interact with a repository that is on the
85 local disk drive, not on a remote system. Hence, before accessing
@@ -384,16 +410,12 @@
410 them and fails if local changes exist unless the <tt>--force</tt>
411 flag is used.
412
413 <h2 id="branch" name="merge">Branching And Merging</h2>
414
415 Use the --branch option to the [/help/commit | commit] command to start
416 a new branch at the point of need. ([./gitusers.md#bneed | Contrast git].)
 
 
 
 
417
418 To merge two branches back together, first
419 [/help/update | update] to the branch you want to merge into.
420 Then do a [/help/merge|merge] of the other branch that you want to incorporate
421 the changes from. For example, to merge "featureX" changes into "trunk"
422
--- 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

Keyboard Shortcuts

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