Fossil SCM
Merge from trunk.
Commit
a241019fb5b0642f5002594595268d77aa5070eba9dc35e2bac4bc8bf4f5d84b
Parent
13c9f74bc8af9b0…
49 files changed
+15
-6
+106
-22
+2
-2
+1
-1
+1
-1
+1
-1
+1
-1
+9
-2
+107
-44
+12
-5
+6
-6
+54
-28
+45
-33
+2
-1
+72
+3
-3
+95
-22
+376
-57
+1
-1
+2
-1
+17
-1
+33
-21
+23
-5
+1
-1
+33
-9
+3
-2
+4
-4
+70
-28
+1
-1
+32
-23
+5
-1
+1
-1
+6
-5
+17
-7
+8
-2
+24
-4
+7
-5
+33
-3
+1
-1
+1
-1
+141
+1
-1
+1
-1
+16
-10
+18
+2
-2
+31
-9
+1
-1
+1
-1
~
extsrc/shell.c
~
extsrc/sqlite3.c
~
extsrc/sqlite3.h
~
skins/blitz/footer.txt
~
skins/darkmode/footer.txt
~
skins/eagle/footer.txt
~
skins/original/footer.txt
~
src/add.c
~
src/alerts.c
~
src/backoffice.c
~
src/branch.c
~
src/cache.c
~
src/cgi.c
~
src/chat.c
~
src/encode.c
~
src/fossil.dom.js
~
src/fossil.fetch.js
~
src/fossil.page.chat.js
~
src/fossil.popupwidget.js
~
src/info.c
~
src/login.c
~
src/lookslike.c
~
src/main.c
~
src/printf.c
~
src/repolist.c
~
src/search.c
~
src/setup.c
~
src/setupuser.c
~
src/sitemap.c
~
src/smtp.c
~
src/sorttable.js
~
src/stat.c
~
src/style.c
~
src/style.chat.css
~
src/th.c
~
src/timeline.c
~
src/tkt.c
~
src/tktsetup.c
~
src/undo.c
~
test/many-www.tcl
~
tools/find-fossil-cgis.tcl
~
tools/fossil-stress.tcl
~
www/aboutcgi.wiki
~
www/changes.wiki
~
www/chat.md
~
www/loadmgmt.md
~
www/quickstart.wiki
~
www/server/windows/service.md
~
www/serverext.wiki
+15
-6
| --- extsrc/shell.c | ||
| +++ extsrc/shell.c | ||
| @@ -6810,11 +6810,11 @@ | ||
| 6810 | 6810 | |
| 6811 | 6811 | |
| 6812 | 6812 | for(i=0; i<argc; i++){ |
| 6813 | 6813 | if( sqlite3_value_type(argv[i])==SQLITE_NULL ){ |
| 6814 | 6814 | /* 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 */ | |
| 6816 | 6816 | returnNoRows = 1; |
| 6817 | 6817 | break; |
| 6818 | 6818 | } |
| 6819 | 6819 | } |
| 6820 | 6820 | if( returnNoRows ){ |
| @@ -25466,10 +25466,11 @@ | ||
| 25466 | 25466 | " --readonly Open FILE readonly", |
| 25467 | 25467 | " --zip FILE is a ZIP archive", |
| 25468 | 25468 | #ifndef SQLITE_SHELL_FIDDLE |
| 25469 | 25469 | ".output ?FILE? Send output to FILE or stdout if FILE is omitted", |
| 25470 | 25470 | " If FILE begins with '|' then open it as a pipe.", |
| 25471 | + " If FILE is 'off' then output is disabled.", | |
| 25471 | 25472 | " Options:", |
| 25472 | 25473 | " --bom Prefix output with a UTF8 byte-order mark", |
| 25473 | 25474 | " -e Send output to the system text editor", |
| 25474 | 25475 | " --plain Use text/plain for -w option", |
| 25475 | 25476 | " -w Send output to a web browser", |
| @@ -30461,13 +30462,13 @@ | ||
| 30461 | 30462 | || (c=='e' && n==5 && cli_strcmp(azArg[0],"excel")==0) |
| 30462 | 30463 | || (c=='w' && n==3 && cli_strcmp(azArg[0],"www")==0) |
| 30463 | 30464 | ){ |
| 30464 | 30465 | char *zFile = 0; |
| 30465 | 30466 | 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 */ | |
| 30469 | 30470 | static const char *zBomUtf8 = "\357\273\277"; |
| 30470 | 30471 | const char *zBom = 0; |
| 30471 | 30472 | |
| 30472 | 30473 | failIfSafeMode(p, "cannot run .%s in safe mode", azArg[0]); |
| 30473 | 30474 | if( c=='e' ){ |
| @@ -30492,18 +30493,26 @@ | ||
| 30492 | 30493 | }else if( c=='o' && cli_strcmp(z,"-e")==0 ){ |
| 30493 | 30494 | eMode = 'e'; /* text editor */ |
| 30494 | 30495 | }else if( c=='o' && cli_strcmp(z,"-w")==0 ){ |
| 30495 | 30496 | eMode = 'w'; /* Web browser */ |
| 30496 | 30497 | }else{ |
| 30497 | - sqlite3_fprintf(p->out, | |
| 30498 | + sqlite3_fprintf(p->out, | |
| 30498 | 30499 | "ERROR: unknown option: \"%s\". Usage:\n", azArg[i]); |
| 30499 | 30500 | showHelp(p->out, azArg[0]); |
| 30500 | 30501 | rc = 1; |
| 30501 | 30502 | goto meta_command_exit; |
| 30502 | 30503 | } |
| 30503 | 30504 | }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 | + } | |
| 30505 | 30514 | if( zFile && zFile[0]=='|' ){ |
| 30506 | 30515 | while( i+1<nArg ) zFile = sqlite3_mprintf("%z %s", zFile, azArg[++i]); |
| 30507 | 30516 | break; |
| 30508 | 30517 | } |
| 30509 | 30518 | }else{ |
| 30510 | 30519 |
| --- 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 @@ | ||
| 16 | 16 | ** if you want a wrapper to interface SQLite with your choice of programming |
| 17 | 17 | ** language. The code for the "sqlite3" command-line shell is also in a |
| 18 | 18 | ** separate file. This file contains only code for the core SQLite library. |
| 19 | 19 | ** |
| 20 | 20 | ** The content in this amalgamation comes from Fossil check-in |
| 21 | -** 121f4d97f9a855131859d342bc2ade5f8c34 with changes in files: | |
| 21 | +** 20acd630b91609725794ce84f9eda01d5f3c with changes in files: | |
| 22 | 22 | ** |
| 23 | 23 | ** |
| 24 | 24 | */ |
| 25 | 25 | #ifndef SQLITE_AMALGAMATION |
| 26 | 26 | #define SQLITE_CORE 1 |
| @@ -450,11 +450,11 @@ | ||
| 450 | 450 | ** be held constant and Z will be incremented or else Y will be incremented |
| 451 | 451 | ** and Z will be reset to zero. |
| 452 | 452 | ** |
| 453 | 453 | ** Since [version 3.6.18] ([dateof:3.6.18]), |
| 454 | 454 | ** 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 | |
| 456 | 456 | ** system</a>. ^The SQLITE_SOURCE_ID macro evaluates to |
| 457 | 457 | ** a string which identifies a particular check-in of SQLite |
| 458 | 458 | ** within its configuration management system. ^The SQLITE_SOURCE_ID |
| 459 | 459 | ** string contains the date and time of the check-in (UTC) and a SHA1 |
| 460 | 460 | ** or SHA3-256 hash of the entire source tree. If the source code has |
| @@ -465,11 +465,11 @@ | ||
| 465 | 465 | ** [sqlite3_libversion_number()], [sqlite3_sourceid()], |
| 466 | 466 | ** [sqlite_version()] and [sqlite_source_id()]. |
| 467 | 467 | */ |
| 468 | 468 | #define SQLITE_VERSION "3.50.0" |
| 469 | 469 | #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" | |
| 471 | 471 | |
| 472 | 472 | /* |
| 473 | 473 | ** CAPI3REF: Run-Time Library Version Numbers |
| 474 | 474 | ** KEYWORDS: sqlite3_version sqlite3_sourceid |
| 475 | 475 | ** |
| @@ -35446,11 +35446,11 @@ | ||
| 35446 | 35446 | unsigned char const *z = zIn; |
| 35447 | 35447 | unsigned char const *zEnd = &z[nByte-1]; |
| 35448 | 35448 | int n = 0; |
| 35449 | 35449 | |
| 35450 | 35450 | if( SQLITE_UTF16NATIVE==SQLITE_UTF16LE ) z++; |
| 35451 | - while( n<nChar && ALWAYS(z<=zEnd) ){ | |
| 35451 | + while( n<nChar && z<=zEnd ){ | |
| 35452 | 35452 | c = z[0]; |
| 35453 | 35453 | z += 2; |
| 35454 | 35454 | if( c>=0xd8 && c<0xdc && z<=zEnd && z[0]>=0xdc && z[0]<0xe0 ) z += 2; |
| 35455 | 35455 | n++; |
| 35456 | 35456 | } |
| @@ -84036,11 +84036,11 @@ | ||
| 84036 | 84036 | ** many different strings can be converted into the same int or real. |
| 84037 | 84037 | ** If a table contains a numeric value and an index is based on the |
| 84038 | 84038 | ** corresponding string value, then it is important that the string be |
| 84039 | 84039 | ** derived from the numeric value, not the other way around, to ensure |
| 84040 | 84040 | ** 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 | |
| 84042 | 84042 | ** an example. |
| 84043 | 84043 | ** |
| 84044 | 84044 | ** This routine looks at pMem to verify that if it has both a numeric |
| 84045 | 84045 | ** representation and a string representation then the string rep has |
| 84046 | 84046 | ** been derived from the numeric and not the other way around. It returns |
| @@ -93324,11 +93324,11 @@ | ||
| 93324 | 93324 | unsigned char enc |
| 93325 | 93325 | ){ |
| 93326 | 93326 | assert( xDel!=SQLITE_DYNAMIC ); |
| 93327 | 93327 | if( enc!=SQLITE_UTF8 ){ |
| 93328 | 93328 | if( enc==SQLITE_UTF16 ) enc = SQLITE_UTF16NATIVE; |
| 93329 | - nData &= ~(u16)1; | |
| 93329 | + nData &= ~(u64)1; | |
| 93330 | 93330 | } |
| 93331 | 93331 | return bindText(pStmt, i, zData, nData, xDel, enc); |
| 93332 | 93332 | } |
| 93333 | 93333 | #ifndef SQLITE_OMIT_UTF16 |
| 93334 | 93334 | SQLITE_API int sqlite3_bind_text16( |
| @@ -125476,11 +125476,11 @@ | ||
| 125476 | 125476 | if( !isDupColumn(pIdx, pIdx->nKeyCol, pPk, i) ){ |
| 125477 | 125477 | testcase( hasColumn(pIdx->aiColumn, pIdx->nKeyCol, pPk->aiColumn[i]) ); |
| 125478 | 125478 | pIdx->aiColumn[j] = pPk->aiColumn[i]; |
| 125479 | 125479 | pIdx->azColl[j] = pPk->azColl[i]; |
| 125480 | 125480 | if( pPk->aSortOrder[i] ){ |
| 125481 | - /* See ticket https://www.sqlite.org/src/info/bba7b69f9849b5bf */ | |
| 125481 | + /* See ticket https://sqlite.org/src/info/bba7b69f9849b5bf */ | |
| 125482 | 125482 | pIdx->bAscKeyBug = 1; |
| 125483 | 125483 | } |
| 125484 | 125484 | j++; |
| 125485 | 125485 | } |
| 125486 | 125486 | } |
| @@ -126853,11 +126853,11 @@ | ||
| 126853 | 126853 | /* This OP_SeekEnd opcode makes index insert for a REINDEX go much |
| 126854 | 126854 | ** faster by avoiding unnecessary seeks. But the optimization does |
| 126855 | 126855 | ** not work for UNIQUE constraint indexes on WITHOUT ROWID tables |
| 126856 | 126856 | ** with DESC primary keys, since those indexes have there keys in |
| 126857 | 126857 | ** 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 | |
| 126859 | 126859 | */ |
| 126860 | 126860 | sqlite3VdbeAddOp1(v, OP_SeekEnd, iIdx); |
| 126861 | 126861 | } |
| 126862 | 126862 | sqlite3VdbeAddOp2(v, OP_IdxInsert, iIdx, regRecord); |
| 126863 | 126863 | sqlite3VdbeChangeP5(v, OPFLAG_USESEEKRESULT); |
| @@ -136942,11 +136942,11 @@ | ||
| 136942 | 136942 | ** OE_Update guarantees that only a single row will change, so it |
| 136943 | 136943 | ** must happen before OE_Replace. Technically, OE_Abort and OE_Rollback |
| 136944 | 136944 | ** could happen in any order, but they are grouped up front for |
| 136945 | 136945 | ** convenience. |
| 136946 | 136946 | ** |
| 136947 | - ** 2018-08-14: Ticket https://www.sqlite.org/src/info/908f001483982c43 | |
| 136947 | + ** 2018-08-14: Ticket https://sqlite.org/src/info/908f001483982c43 | |
| 136948 | 136948 | ** The order of constraints used to have OE_Update as (2) and OE_Abort |
| 136949 | 136949 | ** and so forth as (1). But apparently PostgreSQL checks the OE_Update |
| 136950 | 136950 | ** constraint before any others, so it had to be moved. |
| 136951 | 136951 | ** |
| 136952 | 136952 | ** Constraint checking code is generated in this order: |
| @@ -150434,11 +150434,11 @@ | ||
| 150434 | 150434 | ** |
| 150435 | 150435 | ** This transformation is necessary because the multiSelectOrderBy() routine |
| 150436 | 150436 | ** above that generates the code for a compound SELECT with an ORDER BY clause |
| 150437 | 150437 | ** uses a merge algorithm that requires the same collating sequence on the |
| 150438 | 150438 | ** 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 | |
| 150440 | 150440 | ** |
| 150441 | 150441 | ** This transformation is only needed for EXCEPT, INTERSECT, and UNION. |
| 150442 | 150442 | ** The UNION ALL operator works fine with multiSelectOrderBy() even when |
| 150443 | 150443 | ** there are COLLATE terms in the ORDER BY. |
| 150444 | 150444 | */ |
| @@ -157236,11 +157236,11 @@ | ||
| 157236 | 157236 | iDb = sqlite3TwoPartName(pParse, pNm, pNm, &pNm); |
| 157237 | 157237 | if( iDb<0 ) goto build_vacuum_end; |
| 157238 | 157238 | #else |
| 157239 | 157239 | /* When SQLITE_BUG_COMPATIBLE_20160819 is defined, unrecognized arguments |
| 157240 | 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). | |
| 157241 | + ** occurred on 2016-08-19 (https://sqlite.org/src/info/083f9e6270). | |
| 157242 | 157242 | ** The buggy behavior is required for binary compatibility with some |
| 157243 | 157243 | ** legacy applications. */ |
| 157244 | 157244 | iDb = sqlite3FindDb(pParse->db, pNm); |
| 157245 | 157245 | if( iDb<0 ) iDb = 0; |
| 157246 | 157246 | #endif |
| @@ -161953,11 +161953,11 @@ | ||
| 161953 | 161953 | ** ON or USING clause of a LEFT JOIN, and terms that are usable as |
| 161954 | 161954 | ** indices. |
| 161955 | 161955 | ** |
| 161956 | 161956 | ** This optimization also only applies if the (x1 OR x2 OR ...) term |
| 161957 | 161957 | ** 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 | |
| 161959 | 161959 | ** |
| 161960 | 161960 | ** 2022-02-04: Do not push down slices of a row-value comparison. |
| 161961 | 161961 | ** In other words, "w" or "y" may not be a slice of a vector. Otherwise, |
| 161962 | 161962 | ** the initialization of the right-hand operand of the vector comparison |
| 161963 | 161963 | ** might not occur, or might occur only in an OR branch that is not |
| @@ -199281,11 +199281,11 @@ | ||
| 199281 | 199281 | UNUSED_PARAMETER(nVal); |
| 199282 | 199282 | |
| 199283 | 199283 | fts3tokResetCursor(pCsr); |
| 199284 | 199284 | if( idxNum==1 ){ |
| 199285 | 199285 | 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]); | |
| 199287 | 199287 | pCsr->zInput = sqlite3_malloc64(nByte+1); |
| 199288 | 199288 | if( pCsr->zInput==0 ){ |
| 199289 | 199289 | rc = SQLITE_NOMEM; |
| 199290 | 199290 | }else{ |
| 199291 | 199291 | if( nByte>0 ) memcpy(pCsr->zInput, zByte, nByte); |
| @@ -207828,12 +207828,12 @@ | ||
| 207828 | 207828 | ** with JSON-5 extensions is accepted as input. |
| 207829 | 207829 | ** |
| 207830 | 207830 | ** Beginning with version 3.45.0 (circa 2024-01-01), these routines also |
| 207831 | 207831 | ** accept BLOB values that have JSON encoded using a binary representation |
| 207832 | 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. | |
| 207833 | +** format for SQLite-JSONB is completely different and incompatible with | |
| 207834 | +** PostgreSQL-JSONB. | |
| 207835 | 207835 | ** |
| 207836 | 207836 | ** Decoding and interpreting JSONB is still O(N) where N is the size of |
| 207837 | 207837 | ** the input, the same as text JSON. However, the constant of proportionality |
| 207838 | 207838 | ** for JSONB is much smaller due to faster parsing. The size of each |
| 207839 | 207839 | ** element in JSONB is encoded in its header, so there is no need to search |
| @@ -207886,21 +207886,21 @@ | ||
| 207886 | 207886 | ** 14 4 byte (0-4294967295) 5 |
| 207887 | 207887 | ** 15 8 byte (0-1.8e19) 9 |
| 207888 | 207888 | ** |
| 207889 | 207889 | ** The payload size need not be expressed in its minimal form. For example, |
| 207890 | 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, | |
| 207891 | +** ways: (1) (X>>4)==10, (2) (X>>4)==12 following by one 0x0a byte, | |
| 207892 | 207892 | ** (3) (X>>4)==13 followed by 0x00 and 0x0a, (4) (X>>4)==14 followed by |
| 207893 | 207893 | ** 0x00 0x00 0x00 0x0a, or (5) (X>>4)==15 followed by 7 bytes of 0x00 and |
| 207894 | 207894 | ** a single byte of 0x0a. The shorter forms are preferred, of course, but |
| 207895 | 207895 | ** sometimes when generating JSONB, the payload size is not known in advance |
| 207896 | 207896 | ** and it is convenient to reserve sufficient header space to cover the |
| 207897 | 207897 | ** largest possible payload size and then come back later and patch up |
| 207898 | 207898 | ** the size when it becomes known, resulting in a non-minimal encoding. |
| 207899 | 207899 | ** |
| 207900 | 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) | |
| 207901 | +** (as SQLite is currently unable to handle BLOBs larger than about 2GB) | |
| 207902 | 207902 | ** but is included in the design to allow for future enhancements. |
| 207903 | 207903 | ** |
| 207904 | 207904 | ** The payload follows the header. NULL, TRUE, and FALSE have no payload and |
| 207905 | 207905 | ** their payload size must always be zero. The payload for INT, INT5, |
| 207906 | 207906 | ** FLOAT, FLOAT5, TEXT, TEXTJ, TEXT5, and TEXTROW is text. Note that the |
| @@ -208970,11 +208970,11 @@ | ||
| 208970 | 208970 | if( jsonBlobExpand(pParse, pParse->nBlob+szPayload+9) ) return; |
| 208971 | 208971 | jsonBlobAppendNode(pParse, eType, szPayload, aPayload); |
| 208972 | 208972 | } |
| 208973 | 208973 | |
| 208974 | 208974 | |
| 208975 | -/* Append an node type byte together with the payload size and | |
| 208975 | +/* Append a node type byte together with the payload size and | |
| 208976 | 208976 | ** possibly also the payload. |
| 208977 | 208977 | ** |
| 208978 | 208978 | ** If aPayload is not NULL, then it is a pointer to the payload which |
| 208979 | 208979 | ** is also appended. If aPayload is NULL, the pParse->aBlob[] array |
| 208980 | 208980 | ** is resized (if necessary) so that it is big enough to hold the |
| @@ -210304,10 +210304,86 @@ | ||
| 210304 | 210304 | (void)jsonbPayloadSize(pParse, iRoot, &sz); |
| 210305 | 210305 | pParse->nBlob = nBlob; |
| 210306 | 210306 | sz += pParse->delta; |
| 210307 | 210307 | pParse->delta += jsonBlobChangePayloadSize(pParse, iRoot, sz); |
| 210308 | 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 | +} | |
| 210309 | 210385 | |
| 210310 | 210386 | /* |
| 210311 | 210387 | ** Modify the JSONB blob at pParse->aBlob by removing nDel bytes of |
| 210312 | 210388 | ** content beginning at iDel, and replacing them with nIns bytes of |
| 210313 | 210389 | ** content given by aIns. |
| @@ -210326,10 +210402,15 @@ | ||
| 210326 | 210402 | u32 nDel, /* Number of bytes to remove */ |
| 210327 | 210403 | const u8 *aIns, /* Content to insert */ |
| 210328 | 210404 | u32 nIns /* Bytes of content to insert */ |
| 210329 | 210405 | ){ |
| 210330 | 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 | + } | |
| 210331 | 210412 | if( d!=0 ){ |
| 210332 | 210413 | if( pParse->nBlob + d > pParse->nBlobAlloc ){ |
| 210333 | 210414 | jsonBlobExpand(pParse, pParse->nBlob+d); |
| 210334 | 210415 | if( pParse->oom ) return; |
| 210335 | 210416 | } |
| @@ -210337,11 +210418,13 @@ | ||
| 210337 | 210418 | &pParse->aBlob[iDel+nDel], |
| 210338 | 210419 | pParse->nBlob - (iDel+nDel)); |
| 210339 | 210420 | pParse->nBlob += d; |
| 210340 | 210421 | pParse->delta += d; |
| 210341 | 210422 | } |
| 210342 | - if( nIns && aIns ) memcpy(&pParse->aBlob[iDel], aIns, nIns); | |
| 210423 | + if( nIns && aIns ){ | |
| 210424 | + memcpy(&pParse->aBlob[iDel], aIns, nIns); | |
| 210425 | + } | |
| 210343 | 210426 | } |
| 210344 | 210427 | |
| 210345 | 210428 | /* |
| 210346 | 210429 | ** Return the number of escaped newlines to be ignored. |
| 210347 | 210430 | ** An escaped newline is a one of the following byte sequences: |
| @@ -211100,11 +211183,11 @@ | ||
| 211100 | 211183 | } |
| 211101 | 211184 | return 0; |
| 211102 | 211185 | } |
| 211103 | 211186 | |
| 211104 | 211187 | /* 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 | |
| 211106 | 211189 | ** content to insert or set at that patch. Do the updates |
| 211107 | 211190 | ** and return the result. |
| 211108 | 211191 | ** |
| 211109 | 211192 | ** The specific operation is determined by eEdit, which can be one |
| 211110 | 211193 | ** of JEDIT_INS, JEDIT_REPL, or JEDIT_SET. |
| @@ -230169,11 +230252,13 @@ | ||
| 230169 | 230252 | char *zExpr = 0; |
| 230170 | 230253 | sqlite3 *db = pSession->db; |
| 230171 | 230254 | SessionTable *pTo; /* Table zTbl */ |
| 230172 | 230255 | |
| 230173 | 230256 | /* Locate and if necessary initialize the target table object */ |
| 230257 | + pSession->bAutoAttach++; | |
| 230174 | 230258 | rc = sessionFindTable(pSession, zTbl, &pTo); |
| 230259 | + pSession->bAutoAttach--; | |
| 230175 | 230260 | if( pTo==0 ) goto diff_out; |
| 230176 | 230261 | if( sessionInitTable(pSession, pTo, pSession->db, pSession->zDb) ){ |
| 230177 | 230262 | rc = pSession->rc; |
| 230178 | 230263 | goto diff_out; |
| 230179 | 230264 | } |
| @@ -257088,11 +257173,11 @@ | ||
| 257088 | 257173 | int nArg, /* Number of args */ |
| 257089 | 257174 | sqlite3_value **apUnused /* Function arguments */ |
| 257090 | 257175 | ){ |
| 257091 | 257176 | assert( nArg==0 ); |
| 257092 | 257177 | 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); | |
| 257094 | 257179 | } |
| 257095 | 257180 | |
| 257096 | 257181 | /* |
| 257097 | 257182 | ** Implementation of fts5_locale(LOCALE, TEXT) function. |
| 257098 | 257183 | ** |
| @@ -261151,11 +261236,10 @@ | ||
| 261151 | 261236 | } |
| 261152 | 261237 | iTbl++; |
| 261153 | 261238 | } |
| 261154 | 261239 | aAscii[0] = 0; /* 0x00 is never a token character */ |
| 261155 | 261240 | } |
| 261156 | - | |
| 261157 | 261241 | |
| 261158 | 261242 | /* |
| 261159 | 261243 | ** 2015 May 30 |
| 261160 | 261244 | ** |
| 261161 | 261245 | ** The author disclaims copyright to this source code. In place of |
| 261162 | 261246 |
| --- 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 |
+2
-2
| --- extsrc/sqlite3.h | ||
| +++ extsrc/sqlite3.h | ||
| @@ -131,11 +131,11 @@ | ||
| 131 | 131 | ** be held constant and Z will be incremented or else Y will be incremented |
| 132 | 132 | ** and Z will be reset to zero. |
| 133 | 133 | ** |
| 134 | 134 | ** Since [version 3.6.18] ([dateof:3.6.18]), |
| 135 | 135 | ** 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 | |
| 137 | 137 | ** system</a>. ^The SQLITE_SOURCE_ID macro evaluates to |
| 138 | 138 | ** a string which identifies a particular check-in of SQLite |
| 139 | 139 | ** within its configuration management system. ^The SQLITE_SOURCE_ID |
| 140 | 140 | ** string contains the date and time of the check-in (UTC) and a SHA1 |
| 141 | 141 | ** or SHA3-256 hash of the entire source tree. If the source code has |
| @@ -146,11 +146,11 @@ | ||
| 146 | 146 | ** [sqlite3_libversion_number()], [sqlite3_sourceid()], |
| 147 | 147 | ** [sqlite_version()] and [sqlite_source_id()]. |
| 148 | 148 | */ |
| 149 | 149 | #define SQLITE_VERSION "3.50.0" |
| 150 | 150 | #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" | |
| 152 | 152 | |
| 153 | 153 | /* |
| 154 | 154 | ** CAPI3REF: Run-Time Library Version Numbers |
| 155 | 155 | ** KEYWORDS: sqlite3_version sqlite3_sourceid |
| 156 | 156 | ** |
| 157 | 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://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 |
+1
-1
| --- skins/blitz/footer.txt | ||
| +++ skins/blitz/footer.txt | ||
| @@ -1,10 +1,10 @@ | ||
| 1 | 1 | </div> <!-- end div container --> |
| 2 | 2 | </div> <!-- end div middle max-full-width --> |
| 3 | 3 | <footer> |
| 4 | 4 | <div class="container"> |
| 5 | 5 | <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> | |
| 7 | 7 | </div> |
| 8 | 8 | This page was generated in about <th1>puts [expr {([utime]+[stime]+1000)/1000*0.001}]</th1>s |
| 9 | 9 | </div> |
| 10 | 10 | </footer> |
| 11 | 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://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 |
+1
-1
| --- skins/darkmode/footer.txt | ||
| +++ skins/darkmode/footer.txt | ||
| @@ -1,8 +1,8 @@ | ||
| 1 | 1 | <footer> |
| 2 | 2 | <div class="container"> |
| 3 | 3 | <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> | |
| 5 | 5 | </div> |
| 6 | 6 | This page was generated in about <th1>puts [expr {([utime]+[stime]+1000)/1000*0.001}]</th1>s |
| 7 | 7 | </div> |
| 8 | 8 | </footer> |
| 9 | 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://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 |
+1
-1
| --- skins/eagle/footer.txt | ||
| +++ skins/eagle/footer.txt | ||
| @@ -10,11 +10,11 @@ | ||
| 10 | 10 | set length [string length $version] |
| 11 | 11 | return [string range $version 1 [expr {$length - 2}]] |
| 12 | 12 | } |
| 13 | 13 | set version [getVersion $manifest_version] |
| 14 | 14 | set tclVersion [getTclVersion] |
| 15 | - set fossilUrl https://www.fossil-scm.org | |
| 15 | + set fossilUrl https://fossil-scm.org | |
| 16 | 16 | set fossilDate [string range $manifest_date 0 9]T[string range $manifest_date 11 end] |
| 17 | 17 | </th1> |
| 18 | 18 | This page was generated in about |
| 19 | 19 | <th1>puts [expr {([utime]+[stime]+1000)/1000*0.001}]</th1>s by |
| 20 | 20 | <a href="$fossilUrl/">Fossil</a> |
| 21 | 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://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 |
+1
-1
| --- skins/original/footer.txt | ||
| +++ skins/original/footer.txt | ||
| @@ -10,11 +10,11 @@ | ||
| 10 | 10 | set length [string length $version] |
| 11 | 11 | return [string range $version 1 [expr {$length - 2}]] |
| 12 | 12 | } |
| 13 | 13 | set version [getVersion $manifest_version] |
| 14 | 14 | set tclVersion [getTclVersion] |
| 15 | - set fossilUrl https://www.fossil-scm.org | |
| 15 | + set fossilUrl https://fossil-scm.org | |
| 16 | 16 | set fossilDate [string range $manifest_date 0 9]T[string range $manifest_date 11 end] |
| 17 | 17 | </th1> |
| 18 | 18 | This page was generated in about |
| 19 | 19 | <th1>puts [expr {([utime]+[stime]+1000)/1000*0.001}]</th1>s by |
| 20 | 20 | <a href="$fossilUrl/">Fossil</a> |
| 21 | 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://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 @@ | ||
| 896 | 896 | */ |
| 897 | 897 | static void mv_one_file( |
| 898 | 898 | int vid, |
| 899 | 899 | const char *zOrig, |
| 900 | 900 | const char *zNew, |
| 901 | - int dryRunFlag | |
| 901 | + int dryRunFlag, | |
| 902 | + int moveFiles | |
| 902 | 903 | ){ |
| 903 | 904 | int x = db_int(-1, "SELECT deleted FROM vfile WHERE pathname=%Q %s", |
| 904 | 905 | zNew, filename_collation()); |
| 905 | 906 | if( x>=0 ){ |
| 906 | 907 | if( x==0 ){ |
| @@ -912,10 +913,16 @@ | ||
| 912 | 913 | } |
| 913 | 914 | }else{ |
| 914 | 915 | fossil_fatal("cannot rename '%s' to '%s' since the delete of '%s' has " |
| 915 | 916 | "not yet been committed", zOrig, zNew, zNew); |
| 916 | 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 | + } | |
| 917 | 924 | } |
| 918 | 925 | fossil_print("RENAME %s %s\n", zOrig, zNew); |
| 919 | 926 | if( !dryRunFlag ){ |
| 920 | 927 | db_multi_exec( |
| 921 | 928 | "UPDATE vfile SET pathname='%q' WHERE pathname='%q' %s AND vid=%d", |
| @@ -1135,11 +1142,11 @@ | ||
| 1135 | 1142 | } |
| 1136 | 1143 | db_prepare(&q, "SELECT f, t FROM mv ORDER BY f"); |
| 1137 | 1144 | while( db_step(&q)==SQLITE_ROW ){ |
| 1138 | 1145 | const char *zFrom = db_column_text(&q, 0); |
| 1139 | 1146 | 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); | |
| 1141 | 1148 | if( moveFiles ) add_file_to_move(zFrom, zTo); |
| 1142 | 1149 | } |
| 1143 | 1150 | db_finalize(&q); |
| 1144 | 1151 | undo_reset(); |
| 1145 | 1152 | db_end_transaction(0); |
| 1146 | 1153 |
| --- 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 @@ | ||
| 51 | 51 | @ -- f - Forum posts |
| 52 | 52 | @ -- k - ** Special: Unsubscribed using /oneclickunsub |
| 53 | 53 | @ -- n - New forum threads |
| 54 | 54 | @ -- r - Replies to my own forum posts |
| 55 | 55 | @ -- t - Ticket changes |
| 56 | -@ -- u - Elevation of users' permissions (admins only) | |
| 56 | +@ -- u - Changes of users' permissions (admins only) | |
| 57 | 57 | @ -- w - Wiki changes |
| 58 | 58 | @ -- x - Edits to forum posts |
| 59 | 59 | @ -- Probably different codes will be added in the future. In the future |
| 60 | 60 | @ -- we might also add a separate table that allows subscribing to email |
| 61 | 61 | @ -- notifications for specific branches or tags or tickets. |
| @@ -282,10 +282,13 @@ | ||
| 282 | 282 | style_submenu_element("Subscribers","%R/subscribers"); |
| 283 | 283 | } |
| 284 | 284 | if( fossil_strcmp(g.zPath,"subscribe") ){ |
| 285 | 285 | style_submenu_element("Add New Subscriber","%R/subscribe"); |
| 286 | 286 | } |
| 287 | + if( fossil_strcmp(g.zPath,"setup_notification") ){ | |
| 288 | + style_submenu_element("Notification Setup","%R/setup_notification"); | |
| 289 | + } | |
| 287 | 290 | } |
| 288 | 291 | } |
| 289 | 292 | |
| 290 | 293 | |
| 291 | 294 | /* |
| @@ -295,14 +298,14 @@ | ||
| 295 | 298 | ** Normally accessible via the /Admin/Notification menu. |
| 296 | 299 | */ |
| 297 | 300 | void setup_notification(void){ |
| 298 | 301 | static const char *const azSendMethods[] = { |
| 299 | 302 | "off", "Disabled", |
| 300 | - "pipe", "Pipe to a command", | |
| 303 | + "relay", "SMTP relay", | |
| 301 | 304 | "db", "Store in a database", |
| 302 | 305 | "dir", "Store in a directory", |
| 303 | - "relay", "SMTP relay" | |
| 306 | + "pipe", "Pipe to a command", | |
| 304 | 307 | }; |
| 305 | 308 | login_check_credentials(); |
| 306 | 309 | if( !g.perm.Setup ){ |
| 307 | 310 | login_needed(0); |
| 308 | 311 | return; |
| @@ -311,22 +314,25 @@ | ||
| 311 | 314 | |
| 312 | 315 | alert_submenu_common(); |
| 313 | 316 | style_submenu_element("Send Announcement","%R/announce"); |
| 314 | 317 | style_set_current_feature("alerts"); |
| 315 | 318 | style_header("Email Notification Setup"); |
| 316 | - @ <h1>Status</h1> | |
| 319 | + @ <form action="%R/setup_notification" method="post"><div> | |
| 320 | + @ <h1>Status   <input type="submit" name="submit" value="Refresh"></h1> | |
| 321 | + @ </form> | |
| 317 | 322 | @ <table class="label-value"> |
| 318 | 323 | if( alert_enabled() ){ |
| 319 | 324 | stats_for_email(); |
| 320 | 325 | }else{ |
| 321 | 326 | @ <th>Disabled</th> |
| 322 | 327 | } |
| 323 | 328 | @ </table> |
| 324 | 329 | @ <hr> |
| 325 | - @ <h1> Configuration </h1> | |
| 326 | 330 | @ <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> | |
| 328 | 334 | login_insert_csrf_secret(); |
| 329 | 335 | |
| 330 | 336 | entry_attribute("Canonical Server URL", 40, "email-url", |
| 331 | 337 | "eurl", "", 0); |
| 332 | 338 | @ <p><b>Required.</b> |
| @@ -391,38 +397,40 @@ | ||
| 391 | 397 | @ <p>How to send email. Requires auxiliary information from the fields |
| 392 | 398 | @ that follow. Hint: Use the <a href="%R/announce">/announce</a> page |
| 393 | 399 | @ to send test message to debug this setting. |
| 394 | 400 | @ (Property: "email-send-method")</p> |
| 395 | 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> | |
| 396 | 419 | entry_attribute("Pipe Email Text Into This Command", 60, "email-send-command", |
| 397 | 420 | "ecmd", "sendmail -ti", 0); |
| 398 | 421 | @ <p>When the send method is "pipe to a command", this is the command |
| 399 | 422 | @ that is run. Email messages are piped into the standard input of this |
| 400 | 423 | @ command. The command is expected to extract the sender address, |
| 401 | 424 | @ recipient addresses, and subject from the header of the piped email |
| 402 | 425 | @ 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 | 426 | entry_attribute("Store Emails In This Directory", 60, "email-send-dir", |
| 411 | 427 | "esdir", "", 0); |
| 412 | 428 | @ <p>When the send method is "store in a directory", each email message is |
| 413 | 429 | @ stored as a separate file in the directory shown here. |
| 414 | 430 | @ (Property: "email-send-dir")</p> |
| 415 | 431 | |
| 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 | 432 | @ <hr> |
| 425 | 433 | |
| 426 | 434 | @ <p><input type="submit" name="submit" value="Apply Changes"></p> |
| 427 | 435 | @ </div></form> |
| 428 | 436 | db_end_transaction(0); |
| @@ -630,18 +638,27 @@ | ||
| 630 | 638 | emailerGetSetting(p, &p->zCmd, "email-send-command"); |
| 631 | 639 | }else if( fossil_strcmp(p->zDest, "dir")==0 ){ |
| 632 | 640 | emailerGetSetting(p, &p->zDir, "email-send-dir"); |
| 633 | 641 | }else if( fossil_strcmp(p->zDest, "blob")==0 ){ |
| 634 | 642 | 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 | + ){ | |
| 636 | 646 | const char *zRelay = 0; |
| 637 | 647 | emailerGetSetting(p, &zRelay, "email-send-relayhost"); |
| 638 | 648 | if( zRelay ){ |
| 639 | 649 | u32 smtpFlags = SMTP_DIRECT; |
| 640 | 650 | if( mFlags & ALERT_TRACE ) smtpFlags |= SMTP_TRACE_STDOUT; |
| 651 | + blob_init(&p->out, 0, 0); | |
| 641 | 652 | 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 | + } | |
| 643 | 660 | smtp_client_startup(p->pSmtp); |
| 644 | 661 | } |
| 645 | 662 | } |
| 646 | 663 | return p; |
| 647 | 664 | } |
| @@ -1125,11 +1142,11 @@ | ||
| 1125 | 1142 | ** SETTING: email-listid width=40 |
| 1126 | 1143 | ** If this setting is not an empty string, then it becomes the argument to |
| 1127 | 1144 | ** a "List-ID:" header that is added to all out-bound notification emails. |
| 1128 | 1145 | */ |
| 1129 | 1146 | /* |
| 1130 | -** SETTING: email-send-relayhost width=40 sensitive | |
| 1147 | +** SETTING: email-send-relayhost width=40 sensitive default=127.0.0.1 | |
| 1131 | 1148 | ** This is the hostname and TCP port to which output email messages |
| 1132 | 1149 | ** are sent when email-send-method is "relay". There should be an |
| 1133 | 1150 | ** SMTP server configured as a Mail Submission Agent listening on the |
| 1134 | 1151 | ** designated host and port and all times. |
| 1135 | 1152 | */ |
| @@ -1704,11 +1721,11 @@ | ||
| 1704 | 1721 | @ <label><input type="checkbox" name="sw" %s(PCK("sw"))> \ |
| 1705 | 1722 | @ Wiki</label><br> |
| 1706 | 1723 | } |
| 1707 | 1724 | if( g.perm.Admin ){ |
| 1708 | 1725 | @ <label><input type="checkbox" name="su" %s(PCK("su"))> \ |
| 1709 | - @ User permission elevation</label> | |
| 1726 | + @ User permission changes</label> | |
| 1710 | 1727 | } |
| 1711 | 1728 | di = PB("di"); |
| 1712 | 1729 | @ </td></tr> |
| 1713 | 1730 | @ <tr> |
| 1714 | 1731 | @ <td class="form_label">Delivery:</td> |
| @@ -2114,11 +2131,11 @@ | ||
| 2114 | 2131 | /* Corner-case bug: if an admin assigns 'u' to a non-admin, that |
| 2115 | 2132 | ** subscription will get removed if the user later edits their |
| 2116 | 2133 | ** subscriptions, as non-admins are not permitted to add that |
| 2117 | 2134 | ** subscription. */ |
| 2118 | 2135 | @ <label><input type="checkbox" name="su" %s(su?"checked":"")>\ |
| 2119 | - @ User permission elevation</label> | |
| 2136 | + @ User permission changes</label> | |
| 2120 | 2137 | } |
| 2121 | 2138 | @ </td></tr> |
| 2122 | 2139 | if( strchr(ssub,'k')!=0 ){ |
| 2123 | 2140 | @ <tr><td></td><td> ↑ |
| 2124 | 2141 | @ Note: User did a one-click unsubscribe</td></tr> |
| @@ -3436,16 +3453,28 @@ | ||
| 3436 | 3453 | char *zSubject = PT("subject"); |
| 3437 | 3454 | int bAll = PB("all"); |
| 3438 | 3455 | int bAA = PB("aa"); |
| 3439 | 3456 | int bMods = PB("mods"); |
| 3440 | 3457 | 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; | |
| 3442 | 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 | + } | |
| 3443 | 3472 | blob_init(&body, 0, 0); |
| 3444 | 3473 | blob_init(&hdr, 0, 0); |
| 3445 | 3474 | 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); | |
| 3447 | 3476 | if( zTo[0] ){ |
| 3448 | 3477 | blob_appendf(&hdr, "To: <%s>\r\nSubject: %s %s\r\n", zTo, zSub, zSubject); |
| 3449 | 3478 | alert_send(pSender, &hdr, &body, 0); |
| 3450 | 3479 | } |
| 3451 | 3480 | if( bAll || bAA || bMods ){ |
| @@ -3479,17 +3508,24 @@ | ||
| 3479 | 3508 | } |
| 3480 | 3509 | alert_send(pSender, &hdr, &body, 0); |
| 3481 | 3510 | } |
| 3482 | 3511 | db_finalize(&q); |
| 3483 | 3512 | } |
| 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;'> | |
| 3489 | 3524 | @ %h(blob_str(&pSender->out)) |
| 3490 | 3525 | @ </pre> |
| 3526 | + blob_reset(&pSender->out); | |
| 3491 | 3527 | } |
| 3492 | 3528 | zErr = pSender->zErr; |
| 3493 | 3529 | pSender->zErr = 0; |
| 3494 | 3530 | alert_sender_free(pSender); |
| 3495 | 3531 | return zErr; |
| @@ -3505,35 +3541,43 @@ | ||
| 3505 | 3541 | ** also send a message to an arbitrary email address and/or to all |
| 3506 | 3542 | ** subscribers regardless of whether or not they have elected to |
| 3507 | 3543 | ** receive announcements. |
| 3508 | 3544 | */ |
| 3509 | 3545 | 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 | + */ | |
| 3517 | 3557 | login_check_credentials(); |
| 3518 | 3558 | if( !g.perm.Announce ){ |
| 3519 | 3559 | login_needed(0); |
| 3520 | 3560 | return; |
| 3521 | 3561 | } |
| 3562 | + if( !g.perm.Setup ){ | |
| 3563 | + zName = 0; /* Disable debugging feature for non-admin users */ | |
| 3564 | + } | |
| 3522 | 3565 | style_set_current_feature("alerts"); |
| 3523 | - if( fossil_strcmp(P("name"),"test1")==0 ){ | |
| 3566 | + if( fossil_strcmp(zName,"test1")==0 ){ | |
| 3524 | 3567 | /* Visit the /announce/test1 page to see the CGI variables */ |
| 3525 | 3568 | zAction = "announce/test1"; |
| 3526 | 3569 | @ <p style='border: 1px solid black; padding: 1ex;'> |
| 3527 | 3570 | cgi_print_all(0, 0, 0); |
| 3528 | 3571 | @ </p> |
| 3529 | 3572 | }else if( P("submit")!=0 && cgi_csrf_safe(2) ){ |
| 3530 | 3573 | char *zErr = alert_send_announcement(); |
| 3531 | 3574 | style_header("Announcement Sent"); |
| 3532 | 3575 | 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: | |
| 3535 | 3579 | @ <blockquote><pre> |
| 3536 | 3580 | @ %h(zErr) |
| 3537 | 3581 | @ </pre></blockquote> |
| 3538 | 3582 | }else{ |
| 3539 | 3583 | @ <p>The announcement has been sent. |
| @@ -3548,10 +3592,16 @@ | ||
| 3548 | 3592 | @ for this repository.</p> |
| 3549 | 3593 | return; |
| 3550 | 3594 | } |
| 3551 | 3595 | |
| 3552 | 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 | + } | |
| 3553 | 3603 | @ <form method="POST" action="%R/%s(zAction)"> |
| 3554 | 3604 | login_insert_csrf_secret(); |
| 3555 | 3605 | @ <table class="subscribe"> |
| 3556 | 3606 | if( g.perm.Admin ){ |
| 3557 | 3607 | int aa = PB("aa"); |
| @@ -3584,15 +3634,28 @@ | ||
| 3584 | 3634 | @ <td><textarea name="msg" cols="80" rows="10" wrap="virtual">\ |
| 3585 | 3635 | @ %h(PT("msg"))</textarea> |
| 3586 | 3636 | @ </tr> |
| 3587 | 3637 | @ <tr> |
| 3588 | 3638 | @ <td></td> |
| 3589 | - if( fossil_strcmp(P("name"),"test2")==0 ){ | |
| 3639 | + if( fossil_strcmp(zName,"test2")==0 ){ | |
| 3590 | 3640 | @ <td><input type="submit" name="submit" value="Dry Run"> |
| 3591 | 3641 | }else{ |
| 3592 | 3642 | @ <td><input type="submit" name="submit" value="Send Message"> |
| 3593 | 3643 | } |
| 3594 | 3644 | @ </tr> |
| 3595 | 3645 | @ </table> |
| 3596 | 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 | + } | |
| 3597 | 3660 | style_finish_page(); |
| 3598 | 3661 | } |
| 3599 | 3662 |
| --- 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> ↑ |
| 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   <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> ↑ |
| 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 @@ | ||
| 38 | 38 | ** process table, doing nothing on rarely accessed repositories, and |
| 39 | 39 | ** if the Fossil binary is updated on a system, the backoffice processes |
| 40 | 40 | ** will restart using the new binary automatically. |
| 41 | 41 | ** |
| 42 | 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 | |
| 43 | +** There is a main process that is doing the actual work, and there is | |
| 44 | 44 | ** a second stand-by process that is waiting for the main process to finish |
| 45 | 45 | ** and that will become the main process after a delay. |
| 46 | 46 | ** |
| 47 | 47 | ** After any successful web page reply, the backoffice_check_if_needed() |
| 48 | 48 | ** routine is called. That routine checks to see if both one or both of |
| @@ -53,11 +53,11 @@ | ||
| 53 | 53 | ** backoffice_run_if_needed() routine is called. If the prior call |
| 54 | 54 | ** to backoffice_check_if_needed() indicated that backoffice processing |
| 55 | 55 | ** might be required, the run_if_needed() attempts to kick off a backoffice |
| 56 | 56 | ** process. |
| 57 | 57 | ** |
| 58 | -** All work performance by the backoffice is in the backoffice_work() | |
| 58 | +** All work performed by the backoffice is in the backoffice_work() | |
| 59 | 59 | ** routine. |
| 60 | 60 | */ |
| 61 | 61 | #if defined(_WIN32) |
| 62 | 62 | # if defined(_WIN32_WINNT) |
| 63 | 63 | # undef _WIN32_WINNT |
| @@ -485,11 +485,11 @@ | ||
| 485 | 485 | int warningDelay = 30; |
| 486 | 486 | static int once = 0; |
| 487 | 487 | |
| 488 | 488 | if( sqlite3_db_readonly(g.db, 0) ) return; |
| 489 | 489 | if( db_is_protected(PROTECT_READONLY) ) return; |
| 490 | - g.zPhase = "backoffice"; | |
| 490 | + g.zPhase = "backoffice-pending"; | |
| 491 | 491 | backoffice_error_check_one(&once); |
| 492 | 492 | idSelf = backofficeProcessId(); |
| 493 | 493 | while(1){ |
| 494 | 494 | tmNow = time(0); |
| 495 | 495 | db_begin_write(); |
| @@ -510,10 +510,11 @@ | ||
| 510 | 510 | /* This process can start doing backoffice work immediately */ |
| 511 | 511 | x.idCurrent = idSelf; |
| 512 | 512 | x.tmCurrent = tmNow + BKOFCE_LEASE_TIME; |
| 513 | 513 | x.idNext = 0; |
| 514 | 514 | x.tmNext = 0; |
| 515 | + g.zPhase = "backoffice-work"; | |
| 515 | 516 | backofficeWriteLease(&x); |
| 516 | 517 | db_end_transaction(0); |
| 517 | 518 | backofficeTrace("/***** Begin Backoffice Processing %d *****/\n", |
| 518 | 519 | GETPID()); |
| 519 | 520 | backoffice_work(); |
| @@ -543,13 +544,16 @@ | ||
| 543 | 544 | db_end_transaction(0); |
| 544 | 545 | break; |
| 545 | 546 | } |
| 546 | 547 | }else{ |
| 547 | 548 | 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( | |
| 549 | 552 | "backoffice process %lld still running after %d seconds", |
| 550 | - x.idCurrent, (int)(BKOFCE_LEASE_TIME + tmNow - x.tmCurrent)); | |
| 553 | + x.idCurrent, runningFor); | |
| 554 | + } | |
| 551 | 555 | lastWarning = tmNow; |
| 552 | 556 | warningDelay *= 2; |
| 553 | 557 | } |
| 554 | 558 | if( backofficeSleep(1000) ){ |
| 555 | 559 | /* The sleep was interrupted by a signal from another thread. */ |
| @@ -642,14 +646,17 @@ | ||
| 642 | 646 | backofficeBlob = &log; |
| 643 | 647 | blob_appendf(&log, "%s %s", db_text(0, "SELECT datetime('now')"), zName); |
| 644 | 648 | } |
| 645 | 649 | |
| 646 | 650 | /* Here is where the actual work of the backoffice happens */ |
| 651 | + g.zPhase = "backoffice-alerts"; | |
| 647 | 652 | nThis = alert_backoffice(0); |
| 648 | 653 | if( nThis ){ backoffice_log("%d alerts", nThis); nTotal += nThis; } |
| 654 | + g.zPhase = "backoffice-hooks"; | |
| 649 | 655 | nThis = hook_backoffice(); |
| 650 | 656 | if( nThis ){ backoffice_log("%d hooks", nThis); nTotal += nThis; } |
| 657 | + g.zPhase = "backoffice-close"; | |
| 651 | 658 | |
| 652 | 659 | /* Close the log */ |
| 653 | 660 | if( backofficeFILE ){ |
| 654 | 661 | if( nTotal || backofficeLogDetail ){ |
| 655 | 662 | if( nTotal==0 ) backoffice_log("no-op"); |
| 656 | 663 |
| --- 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 @@ | ||
| 657 | 657 | ** |
| 658 | 658 | ** > fossil branch new BRANCH-NAME BASIS ?OPTIONS? |
| 659 | 659 | ** |
| 660 | 660 | ** Create a new branch BRANCH-NAME off of check-in BASIS. |
| 661 | 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 | +** | |
| 662 | 668 | ** Options: |
| 663 | 669 | ** --private Branch is private (i.e., remains local) |
| 664 | 670 | ** --bgcolor COLOR Use COLOR instead of automatic background |
| 665 | 671 | ** --nosign Do not sign the manifest for the check-in |
| 666 | 672 | ** that creates this branch |
| 667 | 673 | ** --nosync Do not auto-sync prior to creating the branch |
| 668 | 674 | ** --date-override DATE DATE to use instead of 'now' |
| 669 | 675 | ** --user-override USER USER to use instead of the current default |
| 670 | 676 | ** |
| 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 | 677 | ** Options: |
| 678 | 678 | ** -R|--repository REPO Run commands on repository REPO |
| 679 | 679 | */ |
| 680 | 680 | void branch_cmd(void){ |
| 681 | 681 | int n; |
| 682 | 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 | ** 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 @@ | ||
| 399 | 399 | ** WEBPAGE: cachestat |
| 400 | 400 | ** |
| 401 | 401 | ** Show information about the webpage cache. Requires Setup privilege. |
| 402 | 402 | */ |
| 403 | 403 | void cache_page(void){ |
| 404 | - sqlite3 *db; | |
| 404 | + sqlite3 *db = 0; | |
| 405 | 405 | sqlite3_stmt *pStmt; |
| 406 | + int doInit; | |
| 407 | + char *zDbName = cacheName(); | |
| 408 | + int nEntry = 0; | |
| 409 | + int mxEntry = 0; | |
| 406 | 410 | char zBuf[100]; |
| 407 | 411 | |
| 408 | 412 | login_check_credentials(); |
| 409 | 413 | if( !g.perm.Setup ){ login_needed(0); return; } |
| 410 | 414 | style_set_current_feature("cache"); |
| 411 | 415 | 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 | + } | |
| 417 | 423 | cache_register_sizename(db); |
| 418 | 424 | pStmt = cacheStmt(db, |
| 419 | 425 | "SELECT key, sz, nRef, datetime(tm,'unixepoch')" |
| 420 | 426 | " FROM cache" |
| 421 | 427 | " ORDER BY (tm + 3600*min(nRef,48)) DESC" |
| 422 | 428 | ); |
| 423 | 429 | if( pStmt ){ |
| 424 | - @ <ol> | |
| 425 | 430 | while( sqlite3_step(pStmt)==SQLITE_ROW ){ |
| 426 | 431 | const unsigned char *zName = sqlite3_column_text(pStmt,0); |
| 427 | 432 | char *zHash = cache_hash_of_key((const char*)zName); |
| 433 | + if( nEntry==0 ){ | |
| 434 | + @ <h2>Current Cache Entries:</h2> | |
| 435 | + @ <ol> | |
| 436 | + } | |
| 428 | 437 | @ <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 \ | |
| 432 | 441 | if( zHash ){ |
| 433 | - @ %z(href("%R/timeline?c=%S",zHash))check-in</a>\ | |
| 442 | + @ → %z(href("%R/timeline?c=%S",zHash))checkin info</a>\ | |
| 434 | 443 | fossil_free(zHash); |
| 435 | 444 | } |
| 436 | 445 | @ </p></li> |
| 437 | - | |
| 446 | + nEntry++; | |
| 438 | 447 | } |
| 439 | 448 | 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); | |
| 456 | 482 | style_finish_page(); |
| 457 | 483 | } |
| 458 | 484 | |
| 459 | 485 | /* |
| 460 | 486 | ** WEBPAGE: cacheget |
| 461 | 487 |
| --- 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 | @ → %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 @@ | ||
| 72 | 72 | # include <ws2tcpip.h> |
| 73 | 73 | #else |
| 74 | 74 | # include <sys/socket.h> |
| 75 | 75 | # include <sys/un.h> |
| 76 | 76 | # include <netinet/in.h> |
| 77 | +# include <netdb.h> | |
| 77 | 78 | # include <arpa/inet.h> |
| 78 | 79 | # include <sys/times.h> |
| 79 | 80 | # include <sys/time.h> |
| 80 | 81 | # include <sys/wait.h> |
| 81 | 82 | # include <sys/select.h> |
| @@ -103,12 +104,12 @@ | ||
| 103 | 104 | #define PT(x) cgi_parameter_trimmed((x),0) |
| 104 | 105 | #define PDT(x,y) cgi_parameter_trimmed((x),(y)) |
| 105 | 106 | #define PB(x) cgi_parameter_boolean(x) |
| 106 | 107 | #define PCK(x) cgi_parameter_checked(x,1) |
| 107 | 108 | #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)) | |
| 110 | 111 | |
| 111 | 112 | /* |
| 112 | 113 | ** Shortcut for the cgi_printf() routine. Instead of using the |
| 113 | 114 | ** |
| 114 | 115 | ** @ ... |
| @@ -637,10 +638,13 @@ | ||
| 637 | 638 | cgi_set_status(iStat, zStat); |
| 638 | 639 | free(zLocation); |
| 639 | 640 | cgi_reply(); |
| 640 | 641 | fossil_exit(0); |
| 641 | 642 | } |
| 643 | +NORETURN void cgi_redirect_perm(const char *zURL){ | |
| 644 | + cgi_redirect_with_status(zURL, 301, "Moved Permanently"); | |
| 645 | +} | |
| 642 | 646 | NORETURN void cgi_redirect(const char *zURL){ |
| 643 | 647 | cgi_redirect_with_status(zURL, 302, "Moved Temporarily"); |
| 644 | 648 | } |
| 645 | 649 | NORETURN void cgi_redirect_with_method(const char *zURL){ |
| 646 | 650 | cgi_redirect_with_status(zURL, 307, "Temporary Redirect"); |
| @@ -1620,37 +1624,39 @@ | ||
| 1620 | 1624 | fossil_errorlog("Xpossible hack attempt - 418 response on \"%s\"", zName); |
| 1621 | 1625 | exit(0); |
| 1622 | 1626 | } |
| 1623 | 1627 | |
| 1624 | 1628 | /* |
| 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 | |
| 1626 | 1630 | ** cgi_begone_spider() and does not return, else this function has no |
| 1627 | 1631 | ** side effects. The range of checks performed by this function may |
| 1628 | 1632 | ** be extended in the future. |
| 1629 | 1633 | ** |
| 1630 | 1634 | ** Checks are omitted for any logged-in user. |
| 1631 | 1635 | ** |
| 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 | |
| 1637 | 1641 | ** 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. | |
| 1639 | 1645 | */ |
| 1640 | 1646 | 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) ){ | |
| 1642 | 1648 | cgi_begone_spider(zName); |
| 1643 | 1649 | } |
| 1644 | 1650 | } |
| 1645 | 1651 | |
| 1646 | 1652 | /* |
| 1647 | 1653 | ** A variant of cgi_parameter() with the same semantics except that if |
| 1648 | 1654 | ** cgi_parameter(zName,zDefault) returns a value other than zDefault |
| 1649 | 1655 | ** then it passes that value to cgi_value_spider_check(). |
| 1650 | 1656 | */ |
| 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){ | |
| 1652 | 1658 | const char *zTxt = cgi_parameter(zName, zDefault); |
| 1653 | 1659 | |
| 1654 | 1660 | if( zTxt!=zDefault ){ |
| 1655 | 1661 | cgi_value_spider_check(zTxt, zName); |
| 1656 | 1662 | } |
| @@ -2070,34 +2076,40 @@ | ||
| 2070 | 2076 | } |
| 2071 | 2077 | if( zLeftOver ){ *zLeftOver = zInput; } |
| 2072 | 2078 | return zResult; |
| 2073 | 2079 | } |
| 2074 | 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 | + | |
| 2075 | 2092 | /* |
| 2076 | 2093 | ** Determine the IP address on the other side of a connection. |
| 2077 | 2094 | ** Return a pointer to a string. Or return 0 if unable. |
| 2078 | 2095 | ** |
| 2079 | 2096 | ** The string is held in a static buffer that is overwritten on |
| 2080 | 2097 | ** each call. |
| 2081 | 2098 | */ |
| 2082 | 2099 | 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) ){ | |
| 2090 | 2108 | return 0; |
| 2091 | 2109 | } |
| 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; | |
| 2099 | 2111 | } |
| 2100 | 2112 | |
| 2101 | 2113 | /* |
| 2102 | 2114 | ** This routine handles a single HTTP request which is coming in on |
| 2103 | 2115 | ** g.httpIn and which replies on g.httpOut |
| @@ -2537,11 +2549,11 @@ | ||
| 2537 | 2549 | fd_set readfds; /* Set of file descriptors for select() */ |
| 2538 | 2550 | socklen_t lenaddr; /* Length of the inaddr structure */ |
| 2539 | 2551 | int child; /* PID of the child process */ |
| 2540 | 2552 | int nchildren = 0; /* Number of child processes */ |
| 2541 | 2553 | 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 */ | |
| 2543 | 2555 | struct sockaddr_un uxaddr; /* The address for unix-domain sockets */ |
| 2544 | 2556 | int opt = 1; /* setsockopt flag */ |
| 2545 | 2557 | int rc; /* Result code from system calls */ |
| 2546 | 2558 | int iPort = mnPort; /* Port to try to use */ |
| 2547 | 2559 | |
| @@ -2578,23 +2590,22 @@ | ||
| 2578 | 2590 | file_set_mode(g.zSockName, listener, "0660", 1); |
| 2579 | 2591 | } |
| 2580 | 2592 | }else{ |
| 2581 | 2593 | /* Initialize a TCP/IP socket on port iPort */ |
| 2582 | 2594 | memset(&inaddr, 0, sizeof(inaddr)); |
| 2583 | - inaddr.sin_family = AF_INET; | |
| 2595 | + inaddr.sin6_family = AF_INET6; | |
| 2584 | 2596 | 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 ){ | |
| 2587 | 2598 | fossil_fatal("not a valid IP address: %s", zIpAddr); |
| 2588 | 2599 | } |
| 2589 | 2600 | }else if( flags & HTTP_SERVER_LOCALHOST ){ |
| 2590 | - inaddr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); | |
| 2601 | + inaddr.sin6_addr = in6addr_loopback; | |
| 2591 | 2602 | }else{ |
| 2592 | - inaddr.sin_addr.s_addr = htonl(INADDR_ANY); | |
| 2603 | + inaddr.sin6_addr = in6addr_any; | |
| 2593 | 2604 | } |
| 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); | |
| 2596 | 2607 | if( listener<0 ){ |
| 2597 | 2608 | iPort++; |
| 2598 | 2609 | continue; |
| 2599 | 2610 | } |
| 2600 | 2611 | } |
| @@ -2700,10 +2711,11 @@ | ||
| 2700 | 2711 | close(2); |
| 2701 | 2712 | fd = dup(connection); |
| 2702 | 2713 | if( fd!=2 ) nErr++; |
| 2703 | 2714 | } |
| 2704 | 2715 | close(connection); |
| 2716 | + close(listener); | |
| 2705 | 2717 | g.nPendingRequest = nchildren+1; |
| 2706 | 2718 | g.nRequest = nRequest+1; |
| 2707 | 2719 | return nErr; |
| 2708 | 2720 | } |
| 2709 | 2721 | } |
| 2710 | 2722 |
| --- 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 @@ | ||
| 254 | 254 | @ /*^^^for skins which add their own BODY tag */; |
| 255 | 255 | @ window.fossil.config.chat = { |
| 256 | 256 | @ fromcli: %h(PB("cli")?"true":"false"), |
| 257 | 257 | @ alertSound: "%h(zAlert)", |
| 258 | 258 | @ 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)) | |
| 260 | 261 | @ }; |
| 261 | 262 | ajax_emit_js_preview_modes(0); |
| 262 | 263 | chat_emit_alert_list(); |
| 263 | 264 | @ }, false); |
| 264 | 265 | @ </script> |
| 265 | 266 |
| --- 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 @@ | ||
| 141 | 141 | break; |
| 142 | 142 | } |
| 143 | 143 | } |
| 144 | 144 | if( j<i ) blob_append(p, zIn+j, i-j); |
| 145 | 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 | +} | |
| 146 | 218 | |
| 147 | 219 | |
| 148 | 220 | /* |
| 149 | 221 | ** Encode a string for HTTP. This means converting lots of |
| 150 | 222 | ** characters into the "%HH" where H is a hex digit. It also |
| 151 | 223 |
| --- 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 |
+3
-3
| --- src/fossil.dom.js | ||
| +++ src/fossil.dom.js | ||
| @@ -17,16 +17,16 @@ | ||
| 17 | 17 | return function(){ |
| 18 | 18 | return document.createElement(eType); |
| 19 | 19 | }; |
| 20 | 20 | }, |
| 21 | 21 | remove: function(e){ |
| 22 | - if(e.forEach){ | |
| 22 | + if(e?.forEach){ | |
| 23 | 23 | e.forEach( |
| 24 | - (x)=>x.parentNode.removeChild(x) | |
| 24 | + (x)=>x?.parentNode?.removeChild(x) | |
| 25 | 25 | ); |
| 26 | 26 | }else{ |
| 27 | - e.parentNode.removeChild(e); | |
| 27 | + e?.parentNode?.removeChild(e); | |
| 28 | 28 | } |
| 29 | 29 | return e; |
| 30 | 30 | }, |
| 31 | 31 | /** |
| 32 | 32 | Removes all child DOM elements from the given element |
| 33 | 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.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 |
+95
-22
| --- src/fossil.fetch.js | ||
| +++ src/fossil.fetch.js | ||
| @@ -27,18 +27,51 @@ | ||
| 27 | 27 | "this", noting that this call may have amended the options object |
| 28 | 28 | with state other than what the caller provided. |
| 29 | 29 | |
| 30 | 30 | - onerror: callback(Error object) (default = output error message |
| 31 | 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. | |
| 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. | |
| 40 | 73 | |
| 41 | 74 | - method: 'POST' | 'GET' (default = 'GET'). CASE SENSITIVE! |
| 42 | 75 | |
| 43 | 76 | - payload: anything acceptable by XHR2.send(ARG) (DOMString, |
| 44 | 77 | Document, FormData, Blob, File, ArrayBuffer), or a plain object or |
| @@ -46,11 +79,12 @@ | ||
| 46 | 79 | then the method is automatically set to 'POST'. By default XHR2 |
| 47 | 80 | will set the content type based on the payload type. If an |
| 48 | 81 | object/array is converted to JSON, the contentType option is |
| 49 | 82 | automatically set to 'application/json', and if JSON.stringify() of |
| 50 | 83 | 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.) | |
| 52 | 86 | |
| 53 | 87 | - contentType: Optional request content type when POSTing. Ignored |
| 54 | 88 | if the method is not 'POST'. |
| 55 | 89 | |
| 56 | 90 | - responseType: optional string. One of ("text", "arraybuffer", |
| @@ -58,10 +92,13 @@ | ||
| 58 | 92 | As an extension, it supports "json", which tells it that the |
| 59 | 93 | response is expected to be text and that it should be JSON.parse()d |
| 60 | 94 | before passing it on to the onload() callback. If parsing of such |
| 61 | 95 | an object fails, the onload callback is not called, and the |
| 62 | 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. | |
| 63 | 100 | |
| 64 | 101 | - urlParams: string|object. If a string, it is assumed to be a |
| 65 | 102 | URI-encoded list of params in the form "key1=val1&key2=val2...", |
| 66 | 103 | with NO leading '?'. If it is an object, all of its properties get |
| 67 | 104 | converted to that form. Either way, the parameters get appended to |
| @@ -73,11 +110,11 @@ | ||
| 73 | 110 | value of that single header. If it's an array, it's treated as a |
| 74 | 111 | list of headers to return, and the 2nd argument is a map of those |
| 75 | 112 | header values. When a map is passed on, all of its keys are |
| 76 | 113 | lower-cased. When a given header is requested and that header is |
| 77 | 114 | set multiple times, their values are (per the XHR docs) |
| 78 | - concatenated together with ", " between them. | |
| 115 | + concatenated together with "," between them. | |
| 79 | 116 | |
| 80 | 117 | - beforesend/aftersend: optional callbacks which are called |
| 81 | 118 | without arguments immediately before the request is submitted |
| 82 | 119 | and immediately after it is received, regardless of success or |
| 83 | 120 | error. In the context of the callback, the options object is |
| @@ -133,11 +170,11 @@ | ||
| 133 | 170 | }); |
| 134 | 171 | return rc; |
| 135 | 172 | }; |
| 136 | 173 | } |
| 137 | 174 | if('/'===uri[0]) uri = uri.substr(1); |
| 138 | - if(!opt) opt = {}; | |
| 175 | + if(!opt) opt = {}/* should arguably be Object.create(null) */; | |
| 139 | 176 | else if('function'===typeof opt) opt={onload:opt}; |
| 140 | 177 | if(!opt.onload) opt.onload = f.onload; |
| 141 | 178 | if(!opt.onerror) opt.onerror = f.onerror; |
| 142 | 179 | if(!opt.beforesend) opt.beforesend = f.beforesend; |
| 143 | 180 | if(!opt.aftersend) opt.aftersend = f.aftersend; |
| @@ -164,15 +201,34 @@ | ||
| 164 | 201 | jsonResponse = true; |
| 165 | 202 | x.responseType = 'text'; |
| 166 | 203 | }else{ |
| 167 | 204 | x.responseType = opt.responseType||'text'; |
| 168 | 205 | } |
| 169 | - x.ontimeout = function(){ | |
| 206 | + x.ontimeout = function(ev){ | |
| 170 | 207 | 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 | + } | |
| 172 | 227 | }; |
| 173 | - x.onreadystatechange = function(){ | |
| 228 | + x.onreadystatechange = function(ev){ | |
| 229 | + //console.warn("onreadystatechange", x.readyState, ev.target.responseText); | |
| 174 | 230 | if(XMLHttpRequest.DONE !== x.readyState) return; |
| 175 | 231 | try{opt.aftersend()}catch(e){/*ignore*/} |
| 176 | 232 | if(false && 0===x.status){ |
| 177 | 233 | /* For reasons unknown, we _sometimes_ trigger x.status==0 in FF |
| 178 | 234 | when the /chat page starts up, but not in Chrome nor in other |
| @@ -180,20 +236,37 @@ | ||
| 180 | 236 | request is actually sent and it appears to have no |
| 181 | 237 | side-effects on the app other than to generate an error |
| 182 | 238 | (i.e. no requests/responses are missing). This is a silly |
| 183 | 239 | workaround which may or may not bite us later. If so, it can |
| 184 | 240 | 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 | + */ | |
| 186 | 246 | return; |
| 187 | 247 | } |
| 188 | 248 | if(200!==x.status){ |
| 249 | + //console.warn("Error response",ev.target); | |
| 189 | 250 | let err; |
| 190 | 251 | try{ |
| 191 | 252 | 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 | + } | |
| 193 | 257 | }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); | |
| 195 | 268 | return; |
| 196 | 269 | } |
| 197 | 270 | const orh = opt.responseHeaders; |
| 198 | 271 | let head; |
| 199 | 272 | if(true===orh){ |
| @@ -209,17 +282,17 @@ | ||
| 209 | 282 | try{ |
| 210 | 283 | const args = [(jsonResponse && x.response) |
| 211 | 284 | ? JSON.parse(x.response) : x.response]; |
| 212 | 285 | if(head) args.push(head); |
| 213 | 286 | opt.onload.apply(opt, args); |
| 214 | - }catch(e){ | |
| 215 | - opt.onerror(e); | |
| 287 | + }catch(err){ | |
| 288 | + opt.onerror(err); | |
| 216 | 289 | } |
| 217 | - }; | |
| 290 | + }/*onreadystatechange()*/; | |
| 218 | 291 | try{opt.beforesend()} |
| 219 | - catch(e){ | |
| 220 | - opt.onerror(e); | |
| 292 | + catch(err){ | |
| 293 | + opt.onerror(err); | |
| 221 | 294 | return; |
| 222 | 295 | } |
| 223 | 296 | x.open(opt.method||'GET', url.join(''), true); |
| 224 | 297 | if('POST'===opt.method && 'string'===typeof opt.contentType){ |
| 225 | 298 | x.setRequestHeader('Content-Type',opt.contentType); |
| 226 | 299 |
| --- 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 |
+376
-57
| --- src/fossil.page.chat.js | ||
| +++ src/fossil.page.chat.js | ||
| @@ -1,6 +1,6 @@ | ||
| 1 | -/** | |
| 1 | +-/** | |
| 2 | 2 | This file contains the client-side implementation of fossil's /chat |
| 3 | 3 | application. |
| 4 | 4 | */ |
| 5 | 5 | window.fossil.onPageLoad(function(){ |
| 6 | 6 | const F = window.fossil, D = F.dom; |
| @@ -129,19 +129,21 @@ | ||
| 129 | 129 | return resized; |
| 130 | 130 | })(); |
| 131 | 131 | fossil.FRK = ForceResizeKludge/*for debugging*/; |
| 132 | 132 | const Chat = ForceResizeKludge.chat = (function(){ |
| 133 | 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. */, | |
| 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. */, | |
| 136 | 138 | playedBeep: false /* used for the beep-once setting */, |
| 137 | 139 | e:{/*map of certain DOM elements.*/ |
| 138 | 140 | messageInjectPoint: E1('#message-inject-point'), |
| 139 | 141 | pageTitle: E1('head title'), |
| 140 | 142 | 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'), | |
| 143 | 145 | fileSelectWrapper: E1('#chat-input-file-area'), |
| 144 | 146 | viewMessages: E1('#chat-messages-wrapper'), |
| 145 | 147 | btnSubmit: E1('#chat-button-submit'), |
| 146 | 148 | btnAttach: E1('#chat-button-attach'), |
| 147 | 149 | inputX: E1('#chat-input-field-x'), |
| @@ -155,11 +157,13 @@ | ||
| 155 | 157 | viewSearch: E1('#chat-search'), |
| 156 | 158 | searchContent: E1('#chat-search-content'), |
| 157 | 159 | btnPreview: E1('#chat-button-preview'), |
| 158 | 160 | views: document.querySelectorAll('.chat-view'), |
| 159 | 161 | 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 */ | |
| 161 | 165 | }, |
| 162 | 166 | me: F.user.name, |
| 163 | 167 | mxMsg: F.config.chat.initSize ? -F.config.chat.initSize : -50, |
| 164 | 168 | mnMsg: undefined/*lowest message ID we've seen so far (for history loading)*/, |
| 165 | 169 | pageIsActive: 'visible'===document.visibilityState, |
| @@ -179,10 +183,105 @@ | ||
| 179 | 183 | filterState:{ |
| 180 | 184 | activeUser: undefined, |
| 181 | 185 | match: function(uname){ |
| 182 | 186 | return this.activeUser===uname || !this.activeUser; |
| 183 | 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 | + } | |
| 184 | 283 | }, |
| 185 | 284 | /** |
| 186 | 285 | Gets (no args) or sets (1 arg) the current input text field |
| 187 | 286 | value, taking into account single- vs multi-line input. The |
| 188 | 287 | getter returns a trim()'d string and the setter returns this |
| @@ -606,19 +705,19 @@ | ||
| 606 | 705 | |
| 607 | 706 | /** |
| 608 | 707 | If animations are enabled, passes its arguments |
| 609 | 708 | to D.addClassBriefly(), else this is a no-op. |
| 610 | 709 | 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; | |
| 612 | 711 | */ |
| 613 | 712 | animate: function f(e,a,cb){ |
| 614 | 713 | if(!f.$disabled){ |
| 615 | 714 | D.addClassBriefly(e, a, 0, cb); |
| 616 | 715 | } |
| 617 | 716 | return this; |
| 618 | 717 | } |
| 619 | - }; | |
| 718 | + }/*Chat object*/; | |
| 620 | 719 | cs.e.inputFields = [ cs.e.input1, cs.e.inputM, cs.e.inputX ]; |
| 621 | 720 | cs.e.inputFields.$currentIndex = 0; |
| 622 | 721 | cs.e.inputFields.forEach(function(e,ndx){ |
| 623 | 722 | if(ndx===cs.e.inputFields.$currentIndex) D.removeClass(e,'hidden'); |
| 624 | 723 | else D.addClass(e,'hidden'); |
| @@ -645,33 +744,59 @@ | ||
| 645 | 744 | cs.reportError = function(/*msg args*/){ |
| 646 | 745 | const args = argsToArray(arguments); |
| 647 | 746 | console.error("chat error:",args); |
| 648 | 747 | F.toast.error.apply(F.toast, args); |
| 649 | 748 | }; |
| 749 | + | |
| 750 | + let InternalMsgId = 0; | |
| 650 | 751 | /** |
| 651 | 752 | Reports an error in the form of a new message in the chat |
| 652 | 753 | feed. All arguments are appended to the message's content area |
| 653 | 754 | using fossil.dom.append(), so may be of any type supported by |
| 654 | 755 | that function. |
| 655 | 756 | */ |
| 656 | 757 | cs.reportErrorAsMessage = function f(/*msg args*/){ |
| 657 | - if(undefined === f.$msgid) f.$msgid=0; | |
| 658 | 758 | const args = argsToArray(arguments).map(function(v){ |
| 659 | 759 | return (v instanceof Error) ? v.message : v; |
| 660 | 760 | }); |
| 661 | - console.error("chat error:",args); | |
| 761 | + if(Chat.beVerbose){ | |
| 762 | + console.error("chat error:",args); | |
| 763 | + } | |
| 662 | 764 | const d = new Date().toISOString(), |
| 663 | 765 | mw = new this.MessageWidget({ |
| 664 | 766 | 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), | |
| 667 | 791 | mtime: d, |
| 668 | 792 | lmtime: d, |
| 669 | 793 | xmsg: args |
| 670 | 794 | }); |
| 671 | 795 | this.injectMessageElem(mw.e.body); |
| 672 | 796 | mw.scrollIntoView(); |
| 797 | + return mw; | |
| 673 | 798 | }; |
| 674 | 799 | |
| 675 | 800 | cs.getMessageElemById = function(id){ |
| 676 | 801 | return qs('[data-msgid="'+id+'"]'); |
| 677 | 802 | }; |
| @@ -690,24 +815,40 @@ | ||
| 690 | 815 | /** |
| 691 | 816 | LOCALLY deletes a message element by the message ID or passing |
| 692 | 817 | the .message-row element. Returns true if it removes an element, |
| 693 | 818 | else false. |
| 694 | 819 | */ |
| 695 | - cs.deleteMessageElem = function(id){ | |
| 820 | + cs.deleteMessageElem = function(id, silent){ | |
| 696 | 821 | var e; |
| 697 | 822 | if(id instanceof HTMLElement){ |
| 698 | 823 | e = id; |
| 699 | 824 | 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{ | |
| 701 | 840 | e = this.getMessageElemById(id); |
| 702 | 841 | } |
| 703 | 842 | if(e && id){ |
| 704 | 843 | D.remove(e); |
| 705 | 844 | if(e===this.e.newestMessage){ |
| 706 | 845 | this.fetchLastMessageElem(); |
| 707 | 846 | } |
| 708 | - F.toast.message("Deleted message "+id+"."); | |
| 847 | + if( !silent ){ | |
| 848 | + F.toast.message("Deleted message "+id+"."); | |
| 849 | + } | |
| 709 | 850 | } |
| 710 | 851 | return !!e; |
| 711 | 852 | }; |
| 712 | 853 | |
| 713 | 854 | /** |
| @@ -776,10 +917,11 @@ | ||
| 776 | 917 | const self = this; |
| 777 | 918 | F.fetch('chat-fetch-one',{ |
| 778 | 919 | urlParams:{ name: id, raw: true}, |
| 779 | 920 | responseType: 'json', |
| 780 | 921 | onload: function(msg){ |
| 922 | + reportConnectionOkay('chat-fetch-one'); | |
| 781 | 923 | content.$elems[1] = D.append(D.pre(),msg.xmsg); |
| 782 | 924 | content.$elems[1]._xmsgRaw = msg.xmsg/*used for copy-to-clipboard feature*/; |
| 783 | 925 | self.toggleTextMode(e); |
| 784 | 926 | }, |
| 785 | 927 | aftersend:function(){ |
| @@ -834,14 +976,16 @@ | ||
| 834 | 976 | }else{ |
| 835 | 977 | e = this.getMessageElemById(id); |
| 836 | 978 | } |
| 837 | 979 | if(!(e instanceof HTMLElement)) return; |
| 838 | 980 | if(this.userMayDelete(e)){ |
| 839 | - this.ajaxStart(); | |
| 840 | 981 | F.fetch("chat-delete/" + id, { |
| 841 | 982 | responseType: 'json', |
| 842 | - onload:(r)=>this.deleteMessageElem(r), | |
| 983 | + onload:(r)=>{ | |
| 984 | + reportConnectionOkay('chat-delete'); | |
| 985 | + this.deleteMessageElem(r); | |
| 986 | + }, | |
| 843 | 987 | onerror:(err)=>this.reportErrorAsMessage(err) |
| 844 | 988 | }); |
| 845 | 989 | }else{ |
| 846 | 990 | this.deleteMessageElem(id); |
| 847 | 991 | } |
| @@ -1035,10 +1179,11 @@ | ||
| 1035 | 1179 | |
| 1036 | 1180 | ctor.prototype = { |
| 1037 | 1181 | scrollIntoView: function(){ |
| 1038 | 1182 | this.e.content.scrollIntoView(); |
| 1039 | 1183 | }, |
| 1184 | + //remove: function(silent){Chat.deleteMessageElem(this, silent);}, | |
| 1040 | 1185 | setMessage: function(m){ |
| 1041 | 1186 | const ds = this.e.body.dataset; |
| 1042 | 1187 | ds.timestamp = m.mtime; |
| 1043 | 1188 | ds.lmtime = m.lmtime; |
| 1044 | 1189 | ds.msgid = m.msgid; |
| @@ -1212,12 +1357,22 @@ | ||
| 1212 | 1357 | const btnDeleteLocal = D.button("Delete locally"); |
| 1213 | 1358 | D.append(toolbar, btnDeleteLocal); |
| 1214 | 1359 | const self = this; |
| 1215 | 1360 | btnDeleteLocal.addEventListener('click', function(){ |
| 1216 | 1361 | self.hide(); |
| 1217 | - Chat.deleteMessageElem(eMsg); | |
| 1362 | + Chat.deleteMessageElem(eMsg) | |
| 1218 | 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 | + } | |
| 1219 | 1374 | if(Chat.userMayDelete(eMsg)){ |
| 1220 | 1375 | const btnDeleteGlobal = D.button("Delete globally"); |
| 1221 | 1376 | D.append(toolbar, btnDeleteGlobal); |
| 1222 | 1377 | F.confirmer(btnDeleteGlobal,{ |
| 1223 | 1378 | pinSize: true, |
| @@ -1457,10 +1612,11 @@ | ||
| 1457 | 1612 | n: nFetch, |
| 1458 | 1613 | i: iFirst |
| 1459 | 1614 | }, |
| 1460 | 1615 | responseType: "json", |
| 1461 | 1616 | onload:function(jx){ |
| 1617 | + reportConnectionOkay('chat-query'); | |
| 1462 | 1618 | if( bDown ) jx.msgs.reverse(); |
| 1463 | 1619 | jx.msgs.forEach((m) => { |
| 1464 | 1620 | m.isSearchResult = true; |
| 1465 | 1621 | var mw = new Chat.MessageWidget(m); |
| 1466 | 1622 | if( bDown ){ |
| @@ -1524,11 +1680,11 @@ | ||
| 1524 | 1680 | reader.onload = (e)=>img.setAttribute('src', e.target.result); |
| 1525 | 1681 | reader.readAsDataURL(blob); |
| 1526 | 1682 | } |
| 1527 | 1683 | }; |
| 1528 | 1684 | Chat.e.inputFile.addEventListener('change', function(ev){ |
| 1529 | - updateDropZoneContent(this.files && this.files[0] ? this.files[0] : undefined) | |
| 1685 | + updateDropZoneContent(this?.files[0]) | |
| 1530 | 1686 | }); |
| 1531 | 1687 | /* Handle image paste from clipboard. TODO: figure out how we can |
| 1532 | 1688 | paste non-image binary data as if it had been selected via the |
| 1533 | 1689 | file selection element. */ |
| 1534 | 1690 | const pasteListener = function(event){ |
| @@ -1604,10 +1760,11 @@ | ||
| 1604 | 1760 | D.span(),"This message was not successfully sent to the server:" |
| 1605 | 1761 | )); |
| 1606 | 1762 | if(state.msg){ |
| 1607 | 1763 | const ta = D.textarea(); |
| 1608 | 1764 | ta.value = state.msg; |
| 1765 | + ta.setAttribute('readonly','true'); | |
| 1609 | 1766 | D.append(w,ta); |
| 1610 | 1767 | } |
| 1611 | 1768 | if(state.blob){ |
| 1612 | 1769 | D.append(w,D.append(D.span(),"Attachment: ",(state.blob.name||"unnamed"))); |
| 1613 | 1770 | //console.debug("blob = ",state.blob); |
| @@ -1622,11 +1779,46 @@ | ||
| 1622 | 1779 | if(state.msg) Chat.inputValue(state.msg); |
| 1623 | 1780 | if(state.blob) BlobXferState.updateDropZoneContent(state.blob); |
| 1624 | 1781 | const theMsg = findMessageWidgetParent(w); |
| 1625 | 1782 | if(theMsg) Chat.deleteMessageElem(theMsg); |
| 1626 | 1783 | })); |
| 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 | + } | |
| 1628 | 1820 | }; |
| 1629 | 1821 | |
| 1630 | 1822 | /** |
| 1631 | 1823 | Submits the contents of the message input field (if not empty) |
| 1632 | 1824 | and/or the file attachment field to the server. If both are |
| @@ -1686,10 +1878,11 @@ | ||
| 1686 | 1878 | onerror:function(err){ |
| 1687 | 1879 | self.reportErrorAsMessage(err); |
| 1688 | 1880 | recoverFailedMessage(fallback); |
| 1689 | 1881 | }, |
| 1690 | 1882 | onload:function(txt){ |
| 1883 | + reportConnectionOkay('chat-send'); | |
| 1691 | 1884 | if(!txt) return/*success response*/; |
| 1692 | 1885 | try{ |
| 1693 | 1886 | const json = JSON.parse(txt); |
| 1694 | 1887 | self.newContent({msgs:[json]}); |
| 1695 | 1888 | }catch(e){ |
| @@ -2126,11 +2319,11 @@ | ||
| 2126 | 2319 | Chat.e.inputFields.$currentIndex = a[2]; |
| 2127 | 2320 | Chat.inputValue(v); |
| 2128 | 2321 | D.removeClass(a[0], 'hidden'); |
| 2129 | 2322 | D.addClass(a[1], 'hidden'); |
| 2130 | 2323 | } |
| 2131 | - Chat.e.inputElementWrapper.classList[ | |
| 2324 | + Chat.e.inputLineWrapper.classList[ | |
| 2132 | 2325 | s.value ? 'add' : 'remove' |
| 2133 | 2326 | ]('compact'); |
| 2134 | 2327 | Chat.e.inputFields[Chat.e.inputFields.$currentIndex].focus(); |
| 2135 | 2328 | }); |
| 2136 | 2329 | Chat.settings.addListener('edit-ctrl-send',function(s){ |
| @@ -2185,10 +2378,11 @@ | ||
| 2185 | 2378 | /*filename needed for mimetype determination*/); |
| 2186 | 2379 | fd.append('render_mode',F.page.previewModes.wiki); |
| 2187 | 2380 | F.fetch('ajax/preview-text',{ |
| 2188 | 2381 | payload: fd, |
| 2189 | 2382 | onload: function(html){ |
| 2383 | + reportConnectionOkay('ajax/preview-text'); | |
| 2190 | 2384 | Chat.setPreviewText(html); |
| 2191 | 2385 | F.pikchr.addSrcView(Chat.e.viewPreview.querySelectorAll('svg.pikchr')); |
| 2192 | 2386 | }, |
| 2193 | 2387 | onerror: function(e){ |
| 2194 | 2388 | F.fetch.onerror(e); |
| @@ -2322,10 +2516,11 @@ | ||
| 2322 | 2516 | onerror:function(err){ |
| 2323 | 2517 | Chat.reportErrorAsMessage(err); |
| 2324 | 2518 | Chat._isBatchLoading = false; |
| 2325 | 2519 | }, |
| 2326 | 2520 | onload:function(x){ |
| 2521 | + reportConnectionOkay('loadOldMessages()'); | |
| 2327 | 2522 | let gotMessages = x.msgs.length; |
| 2328 | 2523 | newcontent(x,true); |
| 2329 | 2524 | Chat._isBatchLoading = false; |
| 2330 | 2525 | Chat.updateActiveUserList(); |
| 2331 | 2526 | if(Chat._gotServerError){ |
| @@ -2411,10 +2606,11 @@ | ||
| 2411 | 2606 | onerror:function(err){ |
| 2412 | 2607 | Chat.setCurrentView(Chat.e.viewMessages); |
| 2413 | 2608 | Chat.reportErrorAsMessage(err); |
| 2414 | 2609 | }, |
| 2415 | 2610 | onload:function(jx){ |
| 2611 | + reportConnectionOkay('submitSearch()'); | |
| 2416 | 2612 | let previd = 0; |
| 2417 | 2613 | D.clearElement(eMsgTgt); |
| 2418 | 2614 | jx.msgs.forEach((m)=>{ |
| 2419 | 2615 | m.isSearchResult = true; |
| 2420 | 2616 | const mw = new Chat.MessageWidget(m); |
| @@ -2444,87 +2640,210 @@ | ||
| 2444 | 2640 | } |
| 2445 | 2641 | } |
| 2446 | 2642 | ); |
| 2447 | 2643 | }/*Chat.submitSearch()*/; |
| 2448 | 2644 | |
| 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){ | |
| 2450 | 2686 | if(true===f.isFirstCall){ |
| 2451 | 2687 | f.isFirstCall = false; |
| 2452 | 2688 | Chat.ajaxEnd(); |
| 2453 | 2689 | Chat.e.viewMessages.classList.remove('loading'); |
| 2454 | 2690 | setTimeout(function(){ |
| 2455 | 2691 | Chat.scrollMessagesTo(1); |
| 2456 | 2692 | }, 250); |
| 2457 | 2693 | } |
| 2458 | - if(Chat._gotServerError && Chat.intervalTimer){ | |
| 2459 | - clearInterval(Chat.intervalTimer); | |
| 2694 | + Chat.timer.cancelPendingPollTimer(); | |
| 2695 | + if(Chat._gotServerError){ | |
| 2460 | 2696 | Chat.reportErrorAsMessage( |
| 2461 | 2697 | "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(){ | |
| 2484 | 2767 | if(f.running) return; |
| 2485 | 2768 | f.running = true; |
| 2486 | 2769 | Chat._isBatchLoading = f.isFirstCall; |
| 2487 | 2770 | if(true===f.isFirstCall){ |
| 2488 | 2771 | f.isFirstCall = false; |
| 2772 | + f.pendingOnError = undefined; | |
| 2489 | 2773 | Chat.ajaxStart(); |
| 2490 | 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 | + }; | |
| 2491 | 2803 | } |
| 2492 | 2804 | F.fetch("chat-poll",{ |
| 2493 | - timeout: 420 * 1000/*FIXME: get the value from the server*/, | |
| 2805 | + timeout: Chat.timer.pollTimeout, | |
| 2494 | 2806 | urlParams:{ |
| 2495 | 2807 | name: Chat.mxMsg |
| 2496 | 2808 | }, |
| 2497 | 2809 | responseType: "json", |
| 2498 | 2810 | // 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 | + }, | |
| 2501 | 2819 | onerror:function(err){ |
| 2502 | 2820 | 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); | |
| 2508 | 2826 | }, |
| 2509 | 2827 | onload:function(y){ |
| 2828 | + reportConnectionOkay('poll.onload', true); | |
| 2510 | 2829 | newcontent(y); |
| 2511 | 2830 | if(Chat._isBatchLoading){ |
| 2512 | 2831 | Chat._isBatchLoading = false; |
| 2513 | 2832 | Chat.updateActiveUserList(); |
| 2514 | 2833 | } |
| 2515 | - afterFetch(); | |
| 2834 | + afterPollFetch(); | |
| 2516 | 2835 | } |
| 2517 | 2836 | }); |
| 2518 | - }; | |
| 2837 | + }/*poll()*/; | |
| 2519 | 2838 | poll.isFirstCall = true; |
| 2520 | 2839 | Chat._gotServerError = poll.running = false; |
| 2521 | 2840 | if( window.fossil.config.chat.fromcli ){ |
| 2522 | 2841 | Chat.chatOnlyMode(true); |
| 2523 | 2842 | } |
| 2524 | - Chat.intervalTimer = setInterval(poll, 1000); | |
| 2843 | + Chat.timer.startPendingPollTimer(); | |
| 2525 | 2844 | delete ForceResizeKludge.$disabled; |
| 2526 | 2845 | ForceResizeKludge(); |
| 2527 | 2846 | Chat.animate.$disabled = false; |
| 2528 | 2847 | setTimeout( ()=>Chat.inputFocus(), 0 ); |
| 2529 | 2848 | F.page.chat = Chat/* enables testing the APIs via the dev tools */; |
| 2530 | 2849 | }); |
| 2531 | 2850 |
| --- 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 |
+1
-1
| --- src/fossil.popupwidget.js | ||
| +++ src/fossil.popupwidget.js | ||
| @@ -286,11 +286,11 @@ | ||
| 286 | 286 | }; |
| 287 | 287 | |
| 288 | 288 | F.toast = { |
| 289 | 289 | config: { |
| 290 | 290 | position: { x: 5, y: 5 /*viewport-relative, pixels*/ }, |
| 291 | - displayTimeMs: 3000 | |
| 291 | + displayTimeMs: 5000 | |
| 292 | 292 | }, |
| 293 | 293 | /** |
| 294 | 294 | Convenience wrapper around a PopupWidget which pops up a shared |
| 295 | 295 | PopupWidget instance to show toast-style messages (commonly |
| 296 | 296 | seen on Android). Its arguments may be anything suitable for |
| 297 | 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: 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 @@ | ||
| 1970 | 1970 | diff_config_init(&DCfg, 0); |
| 1971 | 1971 | diffType = preferred_diff_type(); |
| 1972 | 1972 | if( P("from") && P("to") ){ |
| 1973 | 1973 | v1 = artifact_from_ci_and_filename("from"); |
| 1974 | 1974 | v2 = artifact_from_ci_and_filename("to"); |
| 1975 | + if( v1==0 || v2==0 ) fossil_redirect_home(); | |
| 1975 | 1976 | }else{ |
| 1976 | 1977 | Stmt q; |
| 1977 | 1978 | v1 = name_to_rid_www("v1"); |
| 1978 | 1979 | v2 = name_to_rid_www("v2"); |
| 1980 | + if( v1==0 || v2==0 ) fossil_redirect_home(); | |
| 1979 | 1981 | |
| 1980 | 1982 | /* If the two file versions being compared both have the same |
| 1981 | 1983 | ** filename, then offer an "Annotate" link that constructs an |
| 1982 | 1984 | ** annotation between those version. */ |
| 1983 | 1985 | db_prepare(&q, |
| @@ -2003,11 +2005,10 @@ | ||
| 2003 | 2005 | "%R/annotate?origin=%s&checkin=%s&filename=%T", |
| 2004 | 2006 | zOrig, zCkin, zFN); |
| 2005 | 2007 | } |
| 2006 | 2008 | db_finalize(&q); |
| 2007 | 2009 | } |
| 2008 | - if( v1==0 || v2==0 ) fossil_redirect_home(); | |
| 2009 | 2010 | zRe = P("regex"); |
| 2010 | 2011 | cgi_check_for_malice(); |
| 2011 | 2012 | if( zRe ) re_compile(&pRe, zRe, 0); |
| 2012 | 2013 | if( verbose ) objdescFlags |= OBJDESC_DETAIL; |
| 2013 | 2014 | if( isPatch ){ |
| 2014 | 2015 |
| --- 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 @@ | ||
| 1299 | 1299 | ** that should restrict robot access. No restrictions |
| 1300 | 1300 | ** are applied if this setting is undefined or is |
| 1301 | 1301 | ** an empty string. |
| 1302 | 1302 | */ |
| 1303 | 1303 | void login_restrict_robot_access(void){ |
| 1304 | - const char *zReferer; | |
| 1305 | 1304 | const char *zGlob; |
| 1306 | 1305 | int isMatch = 1; |
| 1307 | 1306 | int nQP; /* Number of query parameters other than name= */ |
| 1308 | 1307 | if( g.zLogin!=0 ) return; |
| 1309 | 1308 | zGlob = db_get("robot-restrict",0); |
| 1310 | 1309 | if( zGlob==0 || zGlob[0]==0 ) return; |
| 1311 | 1310 | if( g.isHuman ){ |
| 1311 | + const char *zReferer; | |
| 1312 | + const char *zAccept; | |
| 1313 | + const char *zBr; | |
| 1312 | 1314 | zReferer = P("HTTP_REFERER"); |
| 1313 | 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 | + } | |
| 1314 | 1330 | } |
| 1315 | 1331 | nQP = cgi_qp_count(); |
| 1316 | 1332 | if( nQP<1 ) return; |
| 1317 | 1333 | isMatch = glob_multi_match(zGlob, g.zPath); |
| 1318 | 1334 | if( !isMatch ) return; |
| 1319 | 1335 |
| --- 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 @@ | ||
| 478 | 478 | } |
| 479 | 479 | |
| 480 | 480 | /* |
| 481 | 481 | ** Returns true if the given text contains certain keywords or |
| 482 | 482 | ** 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. | |
| 484 | 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. | |
| 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. | |
| 488 | 489 | */ |
| 489 | -int looks_like_sql_injection(const char *zTxt){ | |
| 490 | +int looks_like_attack(const char *zTxt){ | |
| 490 | 491 | unsigned int i; |
| 492 | + int rc = 0; | |
| 491 | 493 | if( zTxt==0 ) return 0; |
| 492 | 494 | for(i=0; zTxt[i]; i++){ |
| 493 | 495 | switch( zTxt[i] ){ |
| 496 | + case '<': | |
| 494 | 497 | case ';': |
| 495 | 498 | case '\'': |
| 496 | 499 | return 1; |
| 497 | 500 | 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; | |
| 500 | 503 | break; |
| 501 | 504 | case 'a': |
| 502 | 505 | case 'A': |
| 503 | - if( isWholeWord(zTxt, i, "and", 3) ) return 1; | |
| 506 | + if( isWholeWord(zTxt, i, "and", 3) ) rc = 1; | |
| 504 | 507 | break; |
| 505 | 508 | case 'n': |
| 506 | 509 | case 'N': |
| 507 | - if( isWholeWord(zTxt, i, "null", 4) ) return 1; | |
| 510 | + if( isWholeWord(zTxt, i, "null", 4) ) rc = 1; | |
| 508 | 511 | break; |
| 509 | 512 | case 'o': |
| 510 | 513 | case 'O': |
| 511 | 514 | if( isWholeWord(zTxt, i, "order", 5) && fossil_isspace(zTxt[i+5]) ){ |
| 512 | - return 1; | |
| 515 | + rc = 1; | |
| 513 | 516 | } |
| 514 | - if( isWholeWord(zTxt, i, "or", 2) ) return 1; | |
| 517 | + if( isWholeWord(zTxt, i, "or", 2) ) rc = 1; | |
| 515 | 518 | break; |
| 516 | 519 | case 's': |
| 517 | 520 | case 'S': |
| 518 | - if( isWholeWord(zTxt, i, "select", 6) ) return 1; | |
| 521 | + if( isWholeWord(zTxt, i, "select", 6) ) rc = 1; | |
| 519 | 522 | break; |
| 520 | 523 | case 'w': |
| 521 | 524 | case 'W': |
| 522 | - if( isWholeWord(zTxt, i, "waitfor", 7) ) return 1; | |
| 525 | + if( isWholeWord(zTxt, i, "waitfor", 7) ) rc = 1; | |
| 523 | 526 | break; |
| 524 | 527 | } |
| 525 | 528 | } |
| 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; | |
| 527 | 539 | } |
| 528 | 540 | |
| 529 | 541 | /* |
| 530 | 542 | ** This is a utility routine associated with the test-looks-like-sql-injection |
| 531 | 543 | ** command. |
| @@ -534,11 +546,11 @@ | ||
| 534 | 546 | ** might be SQL injection. |
| 535 | 547 | ** |
| 536 | 548 | ** Or if bInvert is true, then show the opposite - those lines that do NOT |
| 537 | 549 | ** look like SQL injection. |
| 538 | 550 | */ |
| 539 | -static void show_sql_injection_lines( | |
| 551 | +static void show_attack_lines( | |
| 540 | 552 | const char *zInFile, /* Name of input file */ |
| 541 | 553 | int bInvert, /* Invert the sense of the output (-v) */ |
| 542 | 554 | int bDeHttpize /* De-httpize the inputs. (-d) */ |
| 543 | 555 | ){ |
| 544 | 556 | FILE *in; |
| @@ -551,34 +563,34 @@ | ||
| 551 | 563 | fossil_fatal("cannot open \"%s\" for reading\n", zInFile); |
| 552 | 564 | } |
| 553 | 565 | } |
| 554 | 566 | while( fgets(zLine, sizeof(zLine), in) ){ |
| 555 | 567 | dehttpize(zLine); |
| 556 | - if( (looks_like_sql_injection(zLine)!=0) ^ bInvert ){ | |
| 568 | + if( (looks_like_attack(zLine)!=0) ^ bInvert ){ | |
| 557 | 569 | fossil_print("%s", zLine); |
| 558 | 570 | } |
| 559 | 571 | } |
| 560 | 572 | if( in!=stdin ) fclose(in); |
| 561 | 573 | } |
| 562 | 574 | |
| 563 | 575 | /* |
| 564 | -** COMMAND: test-looks-like-sql-injection | |
| 576 | +** COMMAND: test-looks-like-attack | |
| 565 | 577 | ** |
| 566 | 578 | ** Read lines of input from files named as arguments (or from standard |
| 567 | 579 | ** input if no arguments are provided) and print those that look like they |
| 568 | 580 | ** might be part of an SQL injection attack. |
| 569 | 581 | ** |
| 570 | -** Used to test the looks_lide_sql_injection() utility subroutine, possibly | |
| 582 | +** Used to test the looks_lile_attack() utility subroutine, possibly | |
| 571 | 583 | ** by piping in actual server log data. |
| 572 | 584 | */ |
| 573 | -void test_looks_like_sql_injection(void){ | |
| 585 | +void test_looks_like_attack(void){ | |
| 574 | 586 | int i; |
| 575 | 587 | int bInvert = find_option("invert","v",0)!=0; |
| 576 | 588 | int bDeHttpize = find_option("dehttpize","d",0)!=0; |
| 577 | 589 | verify_all_options(); |
| 578 | 590 | if( g.argc==2 ){ |
| 579 | - show_sql_injection_lines(0, bInvert, bDeHttpize); | |
| 591 | + show_attack_lines(0, bInvert, bDeHttpize); | |
| 580 | 592 | } |
| 581 | 593 | 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); | |
| 583 | 595 | } |
| 584 | 596 | } |
| 585 | 597 |
| --- 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 @@ | ||
| 2053 | 2053 | */ |
| 2054 | 2054 | set_base_url(0); |
| 2055 | 2055 | if( fossil_redirect_to_https_if_needed(2) ) return; |
| 2056 | 2056 | if( zPathInfo==0 || zPathInfo[0]==0 |
| 2057 | 2057 | || (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: | |
| 2059 | 2060 | ** (1) to "/ckout" if g.useLocalauth and g.localOpen are both set. |
| 2060 | 2061 | ** (2) to the home page identified by the "index-page" setting |
| 2061 | 2062 | ** in the repository CONFIG table |
| 2062 | 2063 | ** (3) to "/index" if there no "index-page" setting in CONFIG |
| 2063 | 2064 | */ |
| @@ -2267,10 +2268,21 @@ | ||
| 2267 | 2268 | ** |
| 2268 | 2269 | ** #!/usr/bin/fossil |
| 2269 | 2270 | ** redirect: * https://fossil-scm.org/home |
| 2270 | 2271 | ** |
| 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 | |
| 2272 | 2284 | */ |
| 2273 | 2285 | static void redirect_web_page(int nRedirect, char **azRedirect){ |
| 2274 | 2286 | int i; /* Loop counter */ |
| 2275 | 2287 | const char *zNotFound = 0; /* Not found URL */ |
| 2276 | 2288 | const char *zName = P("name"); |
| @@ -2297,21 +2309,22 @@ | ||
| 2297 | 2309 | } |
| 2298 | 2310 | if( zNotFound ){ |
| 2299 | 2311 | Blob to; |
| 2300 | 2312 | const char *z; |
| 2301 | 2313 | 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); | |
| 2303 | 2316 | } |
| 2304 | 2317 | if( strchr(zNotFound, '?') ){ |
| 2305 | - cgi_redirect(zNotFound); | |
| 2318 | + cgi_redirect_perm(zNotFound); | |
| 2306 | 2319 | } |
| 2307 | 2320 | blob_init(&to, zNotFound, -1); |
| 2308 | 2321 | z = P("PATH_INFO"); |
| 2309 | 2322 | if( z && z[0]=='/' ) blob_append(&to, z, -1); |
| 2310 | 2323 | z = P("QUERY_STRING"); |
| 2311 | 2324 | if( z && z[0]!=0 ) blob_appendf(&to, "?%s", z); |
| 2312 | - cgi_redirect(blob_str(&to)); | |
| 2325 | + cgi_redirect_perm(blob_str(&to)); | |
| 2313 | 2326 | }else{ |
| 2314 | 2327 | @ <html> |
| 2315 | 2328 | @ <head><title>No Such Object</title></head> |
| 2316 | 2329 | @ <body> |
| 2317 | 2330 | @ <p>No such object: <b>%h(zName)</b></p> |
| @@ -2394,10 +2407,13 @@ | ||
| 2394 | 2407 | ** REPO for a check-in or ticket that matches the |
| 2395 | 2408 | ** value of "name", then redirect to URL. There |
| 2396 | 2409 | ** can be multiple "redirect:" lines that are |
| 2397 | 2410 | ** processed in order. If the REPO is "*", then |
| 2398 | 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. | |
| 2399 | 2415 | ** |
| 2400 | 2416 | ** jsmode: VALUE Specifies the delivery mode for JavaScript |
| 2401 | 2417 | ** files. See the help text for the --jsmode |
| 2402 | 2418 | ** flag of the http command. |
| 2403 | 2419 | ** |
| @@ -3052,23 +3068,25 @@ | ||
| 3052 | 3068 | ** using this command interactively over SSH. A better solution would be |
| 3053 | 3069 | ** to use a different command for "ssh" sync, but we cannot do that without |
| 3054 | 3070 | ** breaking legacy. |
| 3055 | 3071 | ** |
| 3056 | 3072 | ** Options: |
| 3073 | +** --csrf-safe N Set cgi_csrf_safe() to to return N | |
| 3057 | 3074 | ** --nobody Pretend to be user "nobody" |
| 3058 | 3075 | ** --test Do not do special "sync" processing when operating |
| 3059 | 3076 | ** over an SSH link |
| 3060 | 3077 | ** --th-trace Trace TH1 execution (for debugging purposes) |
| 3061 | 3078 | ** --usercap CAP User capability string (Default: "sxy") |
| 3062 | -** | |
| 3063 | 3079 | */ |
| 3064 | 3080 | void cmd_test_http(void){ |
| 3065 | 3081 | const char *zIpAddr; /* IP address of remote client */ |
| 3066 | 3082 | const char *zUserCap; |
| 3067 | 3083 | int bTest = 0; |
| 3084 | + const char *zCsrfSafe = find_option("csrf-safe",0,1); | |
| 3068 | 3085 | |
| 3069 | 3086 | Th_InitTraceLog(); |
| 3087 | + if( zCsrfSafe ) g.okCsrf = atoi(zCsrfSafe); | |
| 3070 | 3088 | zUserCap = find_option("usercap",0,1); |
| 3071 | 3089 | if( !find_option("nobody",0,0) ){ |
| 3072 | 3090 | if( zUserCap==0 ){ |
| 3073 | 3091 | g.useLocalauth = 1; |
| 3074 | 3092 | zUserCap = "sxy"; |
| 3075 | 3093 |
| --- 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 @@ | ||
| 1100 | 1100 | if( zFormat[0]=='X' ){ |
| 1101 | 1101 | bDetail = 1; |
| 1102 | 1102 | zFormat++; |
| 1103 | 1103 | } |
| 1104 | 1104 | vfprintf(out, zFormat, ap); |
| 1105 | - fprintf(out, "\n"); | |
| 1105 | + fprintf(out, " (pid %d)\n", (int)getpid()); | |
| 1106 | 1106 | va_end(ap); |
| 1107 | 1107 | if( g.zPhase!=0 ) fprintf(out, "while in %s\n", g.zPhase); |
| 1108 | 1108 | if( bDetail ){ |
| 1109 | 1109 | cgi_print_all(1,3,out); |
| 1110 | 1110 | }else{ |
| 1111 | 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, "\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 @@ | ||
| 31 | 31 | int isValid; /* True if zRepoName is a valid Fossil repository */ |
| 32 | 32 | int isRepolistSkin; /* 1 or 2 if this repository wants to be the skin |
| 33 | 33 | ** for the repository list. 2 means do use this |
| 34 | 34 | ** repository but do not display it in the list. */ |
| 35 | 35 | char *zProjName; /* Project Name. Memory from fossil_malloc() */ |
| 36 | + char *zProjDesc; /* Project Description. Memory from fossil_malloc() */ | |
| 36 | 37 | char *zLoginGroup; /* Name of login group, or NULL. Malloced() */ |
| 37 | 38 | double rMTime; /* Last update. Julian day number */ |
| 38 | 39 | }; |
| 39 | 40 | #endif |
| 40 | 41 | |
| @@ -49,10 +50,11 @@ | ||
| 49 | 50 | int rc; |
| 50 | 51 | |
| 51 | 52 | pRepo->isRepolistSkin = 0; |
| 52 | 53 | pRepo->isValid = 0; |
| 53 | 54 | pRepo->zProjName = 0; |
| 55 | + pRepo->zProjDesc = 0; | |
| 54 | 56 | pRepo->zLoginGroup = 0; |
| 55 | 57 | pRepo->rMTime = 0.0; |
| 56 | 58 | |
| 57 | 59 | g.dbIgnoreErrors++; |
| 58 | 60 | rc = sqlite3_open_v2(pRepo->zRepoName, &db, SQLITE_OPEN_READWRITE, 0); |
| @@ -71,10 +73,19 @@ | ||
| 71 | 73 | -1, &pStmt, 0); |
| 72 | 74 | if( rc ) goto finish_repo_list; |
| 73 | 75 | if( sqlite3_step(pStmt)==SQLITE_ROW ){ |
| 74 | 76 | pRepo->zProjName = fossil_strdup((char*)sqlite3_column_text(pStmt,0)); |
| 75 | 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 | + } | |
| 76 | 87 | sqlite3_finalize(pStmt); |
| 77 | 88 | rc = sqlite3_prepare_v2(db, "SELECT value FROM config" |
| 78 | 89 | " WHERE name='login-group-name'", |
| 79 | 90 | -1, &pStmt, 0); |
| 80 | 91 | if( rc==SQLITE_OK && sqlite3_step(pStmt)==SQLITE_ROW ){ |
| @@ -162,15 +173,16 @@ | ||
| 162 | 173 | }else{ |
| 163 | 174 | Stmt q; |
| 164 | 175 | double rNow; |
| 165 | 176 | blob_append_sql(&html, |
| 166 | 177 | "<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" | |
| 172 | 184 | "</thead><tbody>\n"); |
| 173 | 185 | db_prepare(&q, "SELECT pathname" |
| 174 | 186 | " FROM sfile ORDER BY pathname COLLATE nocase;"); |
| 175 | 187 | rNow = db_double(0, "SELECT julianday('now')"); |
| 176 | 188 | while( db_step(&q)==SQLITE_ROW ){ |
| @@ -230,11 +242,11 @@ | ||
| 230 | 242 | if( x.rMTime==0.0 ){ |
| 231 | 243 | /* This repository has no entry in the "event" table. |
| 232 | 244 | ** Its age will still be maximum, so data-sortkey will work. */ |
| 233 | 245 | zAge = mprintf("unknown"); |
| 234 | 246 | } |
| 235 | - blob_append_sql(&html, "<tr><td valign='top'>"); | |
| 247 | + blob_append_sql(&html, "<tr><td valign='top'><nobr>"); | |
| 236 | 248 | if( !file_ends_with_repository_extension(zName,0) ){ |
| 237 | 249 | /* The "fossil server DIRECTORY" and "fossil ui DIRECTORY" commands |
| 238 | 250 | ** do not work for repositories whose names do not end in ".fossil". |
| 239 | 251 | ** So do not hyperlink those cases. */ |
| 240 | 252 | blob_append_sql(&html,"%h",zName); |
| @@ -275,22 +287,34 @@ | ||
| 275 | 287 | }else{ |
| 276 | 288 | blob_append_sql(&html, |
| 277 | 289 | "<a href='%R/%T/home' target='_blank'>%h</a>\n", |
| 278 | 290 | zUrl, zName); |
| 279 | 291 | } |
| 292 | + blob_append_sql(&html,"</nobr>"); | |
| 280 | 293 | 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); | |
| 282 | 296 | fossil_free(x.zProjName); |
| 283 | 297 | }else{ |
| 284 | 298 | blob_append_sql(&html, "<td></td><td></td>\n"); |
| 285 | 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 | + } | |
| 286 | 307 | 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", | |
| 288 | 310 | (int)iAge, zAge); |
| 289 | 311 | fossil_free(zAge); |
| 290 | 312 | 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); | |
| 292 | 316 | fossil_free(x.zLoginGroup); |
| 293 | 317 | }else{ |
| 294 | 318 | blob_append_sql(&html, "<td></td><td></td></tr>\n"); |
| 295 | 319 | } |
| 296 | 320 | sqlite3_free(zUrl); |
| 297 | 321 |
| --- 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 @@ | ||
| 618 | 618 | int bDebug = find_option("debug",0,0)!=0; /* Undocumented */ |
| 619 | 619 | int nLimit = zLimit ? atoi(zLimit) : -1000; |
| 620 | 620 | int width; |
| 621 | 621 | int nTty = 0; /* VT100 highlight color for matching text */ |
| 622 | 622 | const char *zHighlight = 0; |
| 623 | + int bFlags = 0; /* DB open flags */ | |
| 623 | 624 | |
| 624 | 625 | nTty = terminal_is_vt100(); |
| 625 | 626 | |
| 626 | 627 | /* Undocumented option to change highlight color */ |
| 627 | 628 | zHighlight = find_option("highlight",0,1); |
| @@ -666,12 +667,12 @@ | ||
| 666 | 667 | if( find_option("wiki",0,0) ){ srchFlags |= SRCH_WIKI; bFts = 1; } |
| 667 | 668 | |
| 668 | 669 | /* If no search objects are specified, default to "check-in comments" */ |
| 669 | 670 | if( srchFlags==0 ) srchFlags = SRCH_CKIN; |
| 670 | 671 | |
| 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); | |
| 673 | 674 | verify_all_options(); |
| 674 | 675 | if( g.argc<3 ) return; |
| 675 | 676 | login_set_capabilities("s", 0); |
| 676 | 677 | if( search_restrict(srchFlags)==0 && (srchFlags & SRCH_HELP)==0 ){ |
| 677 | 678 | const char *zC1 = 0, *zPlural = "s"; |
| 678 | 679 |
| --- 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 @@ | ||
| 201 | 201 | login_needed(0); |
| 202 | 202 | return; |
| 203 | 203 | } |
| 204 | 204 | style_header("Log Menu"); |
| 205 | 205 | @ <table border="0" cellspacing="3"> |
| 206 | - | |
| 206 | + | |
| 207 | 207 | if( db_get_boolean("admin-log",1)==0 ){ |
| 208 | 208 | blob_appendf(&desc, |
| 209 | 209 | "The admin log records configuration changes to the repository.\n" |
| 210 | 210 | "<b>Disabled</b>: Turn on the " |
| 211 | 211 | " <a href='%R/setup_settings'>admin-log setting</a> to enable." |
| @@ -462,11 +462,11 @@ | ||
| 462 | 462 | @ and "require a mouse event" should be turned on. These values only come |
| 463 | 463 | @ into play when the main auto-hyperlink settings is 2 ("UserAgent and |
| 464 | 464 | @ Javascript").</p> |
| 465 | 465 | @ |
| 466 | 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 | |
| 467 | + @ visit the <a href="%R/test-env">/test-env</a> page (from a separate | |
| 468 | 468 | @ web browser that is not logged in, even as "anonymous") and verify |
| 469 | 469 | @ that the "g.jsHref" value is "1".</p> |
| 470 | 470 | @ <p>(Properties: "auto-hyperlink", "auto-hyperlink-delay", and |
| 471 | 471 | @ "auto-hyperlink-mouseover"")</p> |
| 472 | 472 | } |
| @@ -604,11 +604,11 @@ | ||
| 604 | 604 | @ in the CGI script. |
| 605 | 605 | @ </ol> |
| 606 | 606 | @ (Property: "localauth") |
| 607 | 607 | @ |
| 608 | 608 | @ <hr> |
| 609 | - onoff_attribute("Enable /test_env", | |
| 609 | + onoff_attribute("Enable /test-env", | |
| 610 | 610 | "test_env_enable", "test_env_enable", 0, 0); |
| 611 | 611 | @ <p>When enabled, the %h(g.zBaseURL)/test_env URL is available to all |
| 612 | 612 | @ users. When disabled (the default) only users Admin and Setup can visit |
| 613 | 613 | @ the /test_env page. |
| 614 | 614 | @ (Property: "test_env_enable") |
| @@ -1296,11 +1296,11 @@ | ||
| 1296 | 1296 | @ </p> |
| 1297 | 1297 | @ <hr> |
| 1298 | 1298 | textarea_attribute("Project Description", 3, 80, |
| 1299 | 1299 | "project-description", "pd", "", 0); |
| 1300 | 1300 | @ <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. | |
| 1302 | 1302 | @ (Property: "project-description")</p> |
| 1303 | 1303 | @ <hr> |
| 1304 | 1304 | entry_attribute("Canonical Server URL", 40, "email-url", |
| 1305 | 1305 | "eurl", "", 0); |
| 1306 | 1306 | @ <p>This is the URL used to access this repository as a server. |
| 1307 | 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 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 @@ | ||
| 155 | 155 | zWith = mprintf(" AND fullcap(cap) GLOB '*[%q]*'", zWith); |
| 156 | 156 | }else{ |
| 157 | 157 | zWith = ""; |
| 158 | 158 | } |
| 159 | 159 | 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 */ | |
| 162 | 162 | " CASE WHEN info LIKE '%%expires 20%%'" |
| 163 | 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" | |
| 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 */ | |
| 168 | 169 | " FROM user LEFT JOIN lastAccess ON login=uname" |
| 169 | 170 | " LEFT JOIN subscriber ON login=suname" |
| 170 | 171 | " WHERE login NOT IN ('anonymous','nobody','developer','reader') %s" |
| 171 | 172 | " ORDER BY sorttime DESC", zWith/*safe-for-%s*/ |
| 172 | 173 | ); |
| @@ -202,11 +203,13 @@ | ||
| 202 | 203 | if( db_column_type(&s,8)==SQLITE_NULL ){ |
| 203 | 204 | @ <td> |
| 204 | 205 | }else if( (zSub = db_column_text(&s,8))==0 || zSub[0]==0 ){ |
| 205 | 206 | @ <td><a href="%R/alerts?sid=%d(sid)"><i>off</i></a> |
| 206 | 207 | }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(" → %h", zEmail) : mprintf(""); | |
| 210 | + @ <td><a href="%R/alerts?sid=%d(sid)">%h(zSub)</a> %z(zAt) | |
| 208 | 211 | } |
| 209 | 212 | |
| 210 | 213 | @ </tr> |
| 211 | 214 | fossil_free(zAge); |
| 212 | 215 | } |
| @@ -304,22 +307,59 @@ | ||
| 304 | 307 | while( zPw[0]=='*' ){ zPw++; } |
| 305 | 308 | return zPw[0]!=0; |
| 306 | 309 | } |
| 307 | 310 | |
| 308 | 311 | /* |
| 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 | +** | |
| 313 | 354 | */ |
| 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 )); | |
| 321 | 361 | } |
| 322 | 362 | |
| 323 | 363 | /* |
| 324 | 364 | ** Sends notification of user permission elevation changes to all |
| 325 | 365 | ** subscribers with a "u" subscription. This is a no-op if alerts are |
| @@ -333,11 +373,11 @@ | ||
| 333 | 373 | ** edits their subscriptions after an admin assigns them this one, |
| 334 | 374 | ** this particular one will be lost. "Feature or bug?" is unclear, |
| 335 | 375 | ** but it would be odd for a non-admin to be assigned this |
| 336 | 376 | ** capability. |
| 337 | 377 | */ |
| 338 | -static void alert_user_elevation(const char *zLogin, /*Affected user*/ | |
| 378 | +static void alert_user_cap_change(const char *zLogin, /*Affected user*/ | |
| 339 | 379 | int uid, /*[user].uid*/ |
| 340 | 380 | int bIsNew, /*true if new user*/ |
| 341 | 381 | const char *zOrigCaps,/*Old caps*/ |
| 342 | 382 | const char *zNewCaps /*New caps*/){ |
| 343 | 383 | Blob hdr, body; |
| @@ -349,21 +389,21 @@ | ||
| 349 | 389 | char * zSubject; |
| 350 | 390 | |
| 351 | 391 | if( !alert_enabled() ) return; |
| 352 | 392 | zSubject = bIsNew |
| 353 | 393 | ? mprintf("New user created: [%q]", zLogin) |
| 354 | - : mprintf("User [%q] permissions elevated", zLogin); | |
| 394 | + : mprintf("User [%q] capabilities changed", zLogin); | |
| 355 | 395 | zURL = db_get("email-url",0); |
| 356 | 396 | zSubname = db_get("email-subname", "[Fossil Repo]"); |
| 357 | 397 | blob_init(&body, 0, 0); |
| 358 | 398 | blob_init(&hdr, 0, 0); |
| 359 | 399 | if( bIsNew ){ |
| 360 | - blob_appendf(&body, "User [%q] was created by with " | |
| 400 | + blob_appendf(&body, "User [%q] was created with " | |
| 361 | 401 | "permissions [%q] by user [%q].\n", |
| 362 | 402 | zLogin, zNewCaps, g.zLogin); |
| 363 | 403 | } else { |
| 364 | - blob_appendf(&body, "Permissions for user [%q] where elevated " | |
| 404 | + blob_appendf(&body, "Permissions for user [%q] where changed " | |
| 365 | 405 | "from [%q] to [%q] by user [%q].\n", |
| 366 | 406 | zLogin, zOrigCaps, zNewCaps, g.zLogin); |
| 367 | 407 | } |
| 368 | 408 | if( zURL ){ |
| 369 | 409 | blob_appendf(&body, "\nUser editor: %s/setup_uedit?uid=%d\n", zURL, uid); |
| @@ -486,11 +526,11 @@ | ||
| 486 | 526 | }else if( !cgi_csrf_safe(2) ){ |
| 487 | 527 | /* This might be a cross-site request forgery, so ignore it */ |
| 488 | 528 | }else{ |
| 489 | 529 | /* We have all the information we need to make the change to the user */ |
| 490 | 530 | char c; |
| 491 | - int bHasNewCaps = 0 /* 1 if user's permissions are increased */; | |
| 531 | + int bCapsChanged = 0 /* 1 if user's permissions changed */; | |
| 492 | 532 | const int bIsNew = uid<=0; |
| 493 | 533 | char aCap[70], zNm[4]; |
| 494 | 534 | zNm[0] = 'a'; |
| 495 | 535 | zNm[2] = 0; |
| 496 | 536 | for(i=0, c='a'; c<='z'; c++){ |
| @@ -508,11 +548,11 @@ | ||
| 508 | 548 | a[c&0x7f] = P(zNm)!=0; |
| 509 | 549 | if( a[c&0x7f] ) aCap[i++] = c; |
| 510 | 550 | } |
| 511 | 551 | |
| 512 | 552 | aCap[i] = 0; |
| 513 | - bHasNewCaps = bIsNew || userHasNewCaps(zOldCaps, &aCap[0]); | |
| 553 | + bCapsChanged = bIsNew || userCapsChanged(zOldCaps, &aCap[0]); | |
| 514 | 554 | zPw = P("pw"); |
| 515 | 555 | zLogin = P("login"); |
| 516 | 556 | if( strlen(zLogin)==0 ){ |
| 517 | 557 | const char *zRef = cgi_referer("setup_ulist"); |
| 518 | 558 | style_header("User Creation Error"); |
| @@ -613,18 +653,20 @@ | ||
| 613 | 653 | @ <span class="loginError">%h(zErr)</span> |
| 614 | 654 | @ |
| 615 | 655 | @ <p><a href="setup_uedit?id=%d(uid)&referer=%T(zRef)"> |
| 616 | 656 | @ [Bummer]</a></p> |
| 617 | 657 | 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]); | |
| 620 | 662 | } |
| 621 | 663 | return; |
| 622 | 664 | } |
| 623 | 665 | } |
| 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]); | |
| 626 | 668 | } |
| 627 | 669 | cgi_redirect(cgi_referer("setup_ulist")); |
| 628 | 670 | return; |
| 629 | 671 | } |
| 630 | 672 | |
| 631 | 673 |
| --- 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(" → %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 @@ | ||
| 285 | 285 | style_header("Test Page Map"); |
| 286 | 286 | style_adunit_config(ADUNIT_RIGHT_OK); |
| 287 | 287 | } |
| 288 | 288 | @ <ul id="sitemap" class="columns" style="column-width:20em"> |
| 289 | 289 | 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> | |
| 291 | 291 | } |
| 292 | 292 | if( g.perm.Read ){ |
| 293 | 293 | @ <li>%z(href("%R/test-rename-list"))List of file renames</a></li> |
| 294 | 294 | } |
| 295 | 295 | @ <li>%z(href("%R/test-builtin-files"))List of built-in files</a></li> |
| 296 | 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 |
| --- 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 @@ | ||
| 184 | 184 | } |
| 185 | 185 | |
| 186 | 186 | /* |
| 187 | 187 | ** Allocate a new SmtpSession object. |
| 188 | 188 | ** |
| 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(). | |
| 192 | 192 | ** |
| 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 | |
| 196 | 194 | */ |
| 197 | 195 | SmtpSession *smtp_session_new( |
| 198 | 196 | const char *zFrom, /* Domain for the client */ |
| 199 | 197 | const char *zDest, /* Domain of the server */ |
| 200 | 198 | u32 smtpFlags, /* Flags */ |
| 201 | - ... /* Arguments depending on the flags */ | |
| 199 | + int iPort /* TCP port if the SMTP_PORT flags is present */ | |
| 202 | 200 | ){ |
| 203 | 201 | SmtpSession *p; |
| 204 | - va_list ap; | |
| 205 | 202 | UrlData url; |
| 206 | 203 | |
| 207 | 204 | p = fossil_malloc( sizeof(*p) ); |
| 208 | 205 | memset(p, 0, sizeof(*p)); |
| 209 | 206 | p->zFrom = zFrom; |
| @@ -210,21 +207,13 @@ | ||
| 210 | 207 | p->zDest = zDest; |
| 211 | 208 | p->smtpFlags = smtpFlags; |
| 212 | 209 | memset(&url, 0, sizeof(url)); |
| 213 | 210 | url.port = 25; |
| 214 | 211 | blob_init(&p->inbuf, 0, 0); |
| 215 | - va_start(ap, smtpFlags); | |
| 216 | 212 | 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 | + } | |
| 226 | 215 | if( (smtpFlags & SMTP_DIRECT)!=0 ){ |
| 227 | 216 | int i; |
| 228 | 217 | p->zHostname = fossil_strdup(zDest); |
| 229 | 218 | for(i=0; p->zHostname[i] && p->zHostname[i]!=':'; i++){} |
| 230 | 219 | if( p->zHostname[i]==':' ){ |
| @@ -246,10 +235,27 @@ | ||
| 246 | 235 | p->zErr = socket_errmsg(); |
| 247 | 236 | socket_close(); |
| 248 | 237 | } |
| 249 | 238 | return p; |
| 250 | 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 | +} | |
| 251 | 257 | |
| 252 | 258 | /* |
| 253 | 259 | ** Send a single line of output the SMTP client to the server. |
| 254 | 260 | */ |
| 255 | 261 | static void smtp_send_line(SmtpSession *p, const char *zFormat, ...){ |
| @@ -375,15 +381,17 @@ | ||
| 375 | 381 | int smtp_client_quit(SmtpSession *p){ |
| 376 | 382 | Blob in = BLOB_INITIALIZER; |
| 377 | 383 | int iCode = 0; |
| 378 | 384 | int bMore = 0; |
| 379 | 385 | 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 | + } | |
| 385 | 393 | socket_close(); |
| 386 | 394 | return 0; |
| 387 | 395 | } |
| 388 | 396 | |
| 389 | 397 | /* |
| @@ -395,10 +403,11 @@ | ||
| 395 | 403 | int smtp_client_startup(SmtpSession *p){ |
| 396 | 404 | Blob in = BLOB_INITIALIZER; |
| 397 | 405 | int iCode = 0; |
| 398 | 406 | int bMore = 0; |
| 399 | 407 | char *zArg = 0; |
| 408 | + if( p==0 || p->atEof ) return 1; | |
| 400 | 409 | do{ |
| 401 | 410 | smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg); |
| 402 | 411 | }while( bMore ); |
| 403 | 412 | if( iCode!=220 ){ |
| 404 | 413 | smtp_client_quit(p); |
| 405 | 414 |
| --- 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 |
+5
-1
| --- src/sorttable.js | ||
| +++ src/sorttable.js | ||
| @@ -9,11 +9,11 @@ | ||
| 9 | 9 | ** function. Example: |
| 10 | 10 | ** |
| 11 | 11 | ** <table class='sortable' data-column-types='tnkx' data-init-sort='2'> |
| 12 | 12 | ** |
| 13 | 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 | |
| 14 | +** the table. The value of data-column-types is a string where each | |
| 15 | 15 | ** character of the string represents a datatype for one column in the |
| 16 | 16 | ** table. |
| 17 | 17 | ** |
| 18 | 18 | ** t Sort by text |
| 19 | 19 | ** n Sort numerically |
| @@ -86,18 +86,22 @@ | ||
| 86 | 86 | hdrCell.className = clsName; |
| 87 | 87 | } |
| 88 | 88 | } |
| 89 | 89 | this.sortText = function(a,b) { |
| 90 | 90 | var i = thisObject.sortIndex; |
| 91 | + if (a.cells.length<=i) return -1; /* see ticket 59d699710b1ab5d4 */ | |
| 92 | + if (b.cells.length<=i) return 1; | |
| 91 | 93 | aa = a.cells[i].textContent.replace(/^\W+/,'').toLowerCase(); |
| 92 | 94 | bb = b.cells[i].textContent.replace(/^\W+/,'').toLowerCase(); |
| 93 | 95 | if(aa<bb) return -1; |
| 94 | 96 | if(aa==bb) return a.rowIndex-b.rowIndex; |
| 95 | 97 | return 1; |
| 96 | 98 | } |
| 97 | 99 | this.sortReverseText = function(a,b) { |
| 98 | 100 | var i = thisObject.sortIndex; |
| 101 | + if (a.cells.length<=i) return 1; /* see ticket 59d699710b1ab5d4 */ | |
| 102 | + if (b.cells.length<=i) return -1; | |
| 99 | 103 | aa = a.cells[i].textContent.replace(/^\W+/,'').toLowerCase(); |
| 100 | 104 | bb = b.cells[i].textContent.replace(/^\W+/,'').toLowerCase(); |
| 101 | 105 | if(aa<bb) return +1; |
| 102 | 106 | if(aa==bb) return a.rowIndex-b.rowIndex; |
| 103 | 107 | return -1; |
| 104 | 108 |
| --- 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 @@ | ||
| 166 | 166 | style_submenu_element("Artifacts", "bloblist"); |
| 167 | 167 | if( sqlite3_compileoption_used("ENABLE_DBSTAT_VTAB") ){ |
| 168 | 168 | style_submenu_element("Table Sizes", "repo-tabsize"); |
| 169 | 169 | } |
| 170 | 170 | 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"); | |
| 172 | 172 | } |
| 173 | 173 | @ <table class="label-value"> |
| 174 | 174 | fsize = file_size(g.zRepositoryName, ExtFILE); |
| 175 | 175 | @ <tr><th>Repository Size:</th><td>%,lld(fsize) bytes</td> |
| 176 | 176 | @ </td></tr> |
| 177 | 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 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 Size:</th><td>%,lld(fsize) bytes</td> |
| 176 | @ </td></tr> |
| 177 |
+6
-5
| --- src/style.c | ||
| +++ src/style.c | ||
| @@ -746,11 +746,11 @@ | ||
| 746 | 746 | Th_MaybeStore("default_csp", zDfltCsp); |
| 747 | 747 | fossil_free(zDfltCsp); |
| 748 | 748 | Th_Store("nonce", zNonce); |
| 749 | 749 | Th_Store("project_name", db_get("project-name","Unnamed Fossil Project")); |
| 750 | 750 | 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)); | |
| 752 | 752 | Th_Store("baseurl", g.zBaseURL); |
| 753 | 753 | Th_Store("secureurl", fossil_wants_https(1)? g.zHttpsURL: g.zBaseURL); |
| 754 | 754 | Th_Store("home", g.zTop); |
| 755 | 755 | Th_Store("index_page", db_get("index-page","/home")); |
| 756 | 756 | if( local_zCurrentPage==0 ) style_set_current_page("%T", g.zPath); |
| @@ -772,11 +772,11 @@ | ||
| 772 | 772 | Th_Store("mainmenu", style_get_mainmenu()); |
| 773 | 773 | stylesheet_url_var(); |
| 774 | 774 | image_url_var("logo"); |
| 775 | 775 | image_url_var("background"); |
| 776 | 776 | if( !login_is_nobody() ){ |
| 777 | - Th_Store("login", g.zLogin); | |
| 777 | + Th_Store("login", html_lookalike(g.zLogin,-1)); | |
| 778 | 778 | } |
| 779 | 779 | Th_MaybeStore("current_feature", feature_from_page_path(local_zCurrentPage) ); |
| 780 | 780 | if( g.ftntsIssues[0] || g.ftntsIssues[1] || |
| 781 | 781 | g.ftntsIssues[2] || g.ftntsIssues[3] ){ |
| 782 | 782 | char buf[80]; |
| @@ -1375,11 +1375,12 @@ | ||
| 1375 | 1375 | @ </form> |
| 1376 | 1376 | style_finish_page(); |
| 1377 | 1377 | } |
| 1378 | 1378 | |
| 1379 | 1379 | /* |
| 1380 | -** WEBPAGE: test_env | |
| 1380 | +** WEBPAGE: test-env | |
| 1381 | +** WEBPAGE: test_env alias | |
| 1381 | 1382 | ** |
| 1382 | 1383 | ** Display CGI-variables and other aspects of the run-time |
| 1383 | 1384 | ** environment, for debugging and trouble-shooting purposes. |
| 1384 | 1385 | */ |
| 1385 | 1386 | void page_test_env(void){ |
| @@ -1438,11 +1439,11 @@ | ||
| 1438 | 1439 | ** |
| 1439 | 1440 | ** For administators, or if the test_env_enable setting is true, then |
| 1440 | 1441 | ** details of the request environment are displayed. Otherwise, just |
| 1441 | 1442 | ** the error message is shown. |
| 1442 | 1443 | ** |
| 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. | |
| 1444 | 1445 | */ |
| 1445 | 1446 | void webpage_error(const char *zFormat, ...){ |
| 1446 | 1447 | int showAll = 0; |
| 1447 | 1448 | char *zErr = 0; |
| 1448 | 1449 | int isAuth = 0; |
| @@ -1538,11 +1539,11 @@ | ||
| 1538 | 1539 | } |
| 1539 | 1540 | @ <hr> |
| 1540 | 1541 | P("HTTP_USER_AGENT"); |
| 1541 | 1542 | P("SERVER_SOFTWARE"); |
| 1542 | 1543 | cgi_print_all(showAll, 0, 0); |
| 1543 | - @ <p><form method="POST" action="%R/test_env"> | |
| 1544 | + @ <p><form method="POST" action="%R/test-env"> | |
| 1544 | 1545 | @ <input type="hidden" name="showall" value="%d(showAll)"> |
| 1545 | 1546 | @ <input type="submit" name="post-test-button" value="POST Test"> |
| 1546 | 1547 | @ </form> |
| 1547 | 1548 | if( showAll && blob_size(&g.httpHeader)>0 ){ |
| 1548 | 1549 | @ <hr> |
| 1549 | 1550 |
| --- 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 |
+17
-7
| --- src/style.chat.css | ||
| +++ src/style.chat.css | ||
| @@ -213,10 +213,19 @@ | ||
| 213 | 213 | } |
| 214 | 214 | body.chat #chat-messages-wrapper.loading > * { |
| 215 | 215 | /* An attempt at reducing flicker when loading lots of messages. */ |
| 216 | 216 | visibility: hidden; |
| 217 | 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 | + | |
| 218 | 227 | body.chat div.content { |
| 219 | 228 | margin: 0; |
| 220 | 229 | padding: 0; |
| 221 | 230 | display: flex; |
| 222 | 231 | flex-direction: column-reverse; |
| @@ -241,20 +250,21 @@ | ||
| 241 | 250 | /* Safari user reports that 2em is necessary to keep the file selection |
| 242 | 251 | widget from overlapping the page footer, whereas a margin of 0 is fine |
| 243 | 252 | for FF/Chrome (and 2em is a *huge* waste of space for those). */ |
| 244 | 253 | margin-bottom: 0; |
| 245 | 254 | } |
| 246 | -.chat-input-field { | |
| 255 | + | |
| 256 | +body.chat .chat-input-field { | |
| 247 | 257 | flex: 10 1 auto; |
| 248 | 258 | margin: 0; |
| 249 | 259 | } |
| 250 | -#chat-input-field-x, | |
| 251 | -#chat-input-field-multi { | |
| 260 | +body.chat #chat-input-field-x, | |
| 261 | +body.chat #chat-input-field-multi { | |
| 252 | 262 | overflow: auto; |
| 253 | 263 | resize: vertical; |
| 254 | 264 | } |
| 255 | -#chat-input-field-x { | |
| 265 | +body.chat #chat-input-field-x { | |
| 256 | 266 | display: inline-block/*supposed workaround for Chrome weirdness*/; |
| 257 | 267 | padding: 0.2em; |
| 258 | 268 | background-color: rgba(156,156,156,0.3); |
| 259 | 269 | white-space: pre-wrap; |
| 260 | 270 | /* ^^^ Firefox, when pasting plain text into a contenteditable field, |
| @@ -261,20 +271,20 @@ | ||
| 261 | 271 | loses all newlines unless we explicitly set this. Chrome does not. */ |
| 262 | 272 | cursor: text; |
| 263 | 273 | /* ^^^ In some browsers the cursor may not change for a contenteditable |
| 264 | 274 | element until it has focus, causing potential confusion. */ |
| 265 | 275 | } |
| 266 | -#chat-input-field-x:empty::before { | |
| 276 | +body.chat #chat-input-field-x:empty::before { | |
| 267 | 277 | content: attr(data-placeholder); |
| 268 | 278 | opacity: 0.6; |
| 269 | 279 | } |
| 270 | -.chat-input-field:not(:focus){ | |
| 280 | +body.chat .chat-input-field:not(:focus){ | |
| 271 | 281 | border-width: 1px; |
| 272 | 282 | border-style: solid; |
| 273 | 283 | border-radius: 0.25em; |
| 274 | 284 | } |
| 275 | -.chat-input-field:focus{ | |
| 285 | +body.chat .chat-input-field:focus{ | |
| 276 | 286 | /* This transparent border helps avoid the text shifting around |
| 277 | 287 | when the contenteditable attribute causes a border (which we |
| 278 | 288 | apparently cannot style) to be added. */ |
| 279 | 289 | border-width: 1px; |
| 280 | 290 | border-style: solid; |
| 281 | 291 |
| --- 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 |
M
src/th.c
+8
-2
| --- src/th.c | ||
| +++ src/th.c | ||
| @@ -2160,12 +2160,15 @@ | ||
| 2160 | 2160 | } |
| 2161 | 2161 | iRes = iLeft%iRight; |
| 2162 | 2162 | break; |
| 2163 | 2163 | case OP_ADD: iRes = iLeft+iRight; break; |
| 2164 | 2164 | 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; | |
| 2167 | 2170 | case OP_LT: iRes = iLeft<iRight; break; |
| 2168 | 2171 | case OP_GT: iRes = iLeft>iRight; break; |
| 2169 | 2172 | case OP_LE: iRes = iLeft<=iRight; break; |
| 2170 | 2173 | case OP_GE: iRes = iLeft>=iRight; break; |
| 2171 | 2174 | case OP_EQ: iRes = iLeft==iRight; break; |
| @@ -2875,10 +2878,13 @@ | ||
| 2875 | 2878 | unsigned int uVal = iVal; |
| 2876 | 2879 | char zBuf[32]; |
| 2877 | 2880 | char *z = &zBuf[32]; |
| 2878 | 2881 | |
| 2879 | 2882 | if( iVal<0 ){ |
| 2883 | + if( iVal==0x80000000 ){ | |
| 2884 | + return Th_SetResult(interp, "-2147483648", -1); | |
| 2885 | + } | |
| 2880 | 2886 | isNegative = 1; |
| 2881 | 2887 | uVal = iVal * -1; |
| 2882 | 2888 | } |
| 2883 | 2889 | *(--z) = '\0'; |
| 2884 | 2890 | *(--z) = (char)(48+(uVal%10)); |
| 2885 | 2891 |
| --- 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 @@ | ||
| 598 | 598 | drawDetailEllipsis = 0; |
| 599 | 599 | }else{ |
| 600 | 600 | cgi_printf("%W",blob_str(&comment)); |
| 601 | 601 | } |
| 602 | 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>← This is me!</b> | |
| 610 | + } | |
| 611 | + | |
| 603 | 612 | @ </span> |
| 604 | 613 | blob_reset(&comment); |
| 605 | 614 | |
| 606 | 615 | /* Generate extra information and hyperlinks to follow the comment. |
| 607 | 616 | ** Example: "(check-in: [abcdefg], user: drh, tags: trunk)" |
| @@ -3740,15 +3749,22 @@ | ||
| 3740 | 3749 | } |
| 3741 | 3750 | } |
| 3742 | 3751 | |
| 3743 | 3752 | if( mode==TIMELINE_MODE_NONE ) mode = TIMELINE_MODE_BEFORE; |
| 3744 | 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 | + } | |
| 3745 | 3758 | blob_append(&sql, timeline_query_for_tty(), -1); |
| 3746 | 3759 | blob_append_sql(&sql, "\n AND event.mtime %s %s", |
| 3747 | 3760 | ( mode==TIMELINE_MODE_BEFORE || |
| 3748 | 3761 | mode==TIMELINE_MODE_PARENTS ) ? "<=" : ">=", zDate /*safe-for-%s*/ |
| 3749 | 3762 | ); |
| 3763 | + if( zType && (zType[0]!='a') ){ | |
| 3764 | + blob_append_sql(&sql, "\n AND event.type=%Q ", zType); | |
| 3765 | + } | |
| 3750 | 3766 | |
| 3751 | 3767 | /* When zFilePattern is specified, compute complete ancestry; |
| 3752 | 3768 | * limit later at print_timeline() */ |
| 3753 | 3769 | if( mode==TIMELINE_MODE_CHILDREN || mode==TIMELINE_MODE_PARENTS ){ |
| 3754 | 3770 | db_multi_exec("CREATE TEMP TABLE ok(rid INTEGER PRIMARY KEY)"); |
| @@ -3757,13 +3773,10 @@ | ||
| 3757 | 3773 | }else{ |
| 3758 | 3774 | compute_ancestors(objid, (zFilePattern ? 0 : n), 0, 0); |
| 3759 | 3775 | } |
| 3760 | 3776 | blob_append_sql(&sql, "\n AND blob.rid IN ok"); |
| 3761 | 3777 | } |
| 3762 | - if( zType && (zType[0]!='a') ){ | |
| 3763 | - blob_append_sql(&sql, "\n AND event.type=%Q ", zType); | |
| 3764 | - } | |
| 3765 | 3778 | if( zFilePattern ){ |
| 3766 | 3779 | blob_append(&sql, |
| 3767 | 3780 | "\n AND EXISTS(SELECT 1 FROM mlink\n" |
| 3768 | 3781 | " WHERE mlink.mid=event.objid\n" |
| 3769 | 3782 | " AND mlink.fnid IN ", -1); |
| @@ -3801,11 +3814,18 @@ | ||
| 3801 | 3814 | " WHERE tx.value='%q'\n" |
| 3802 | 3815 | ")\n" /* No merge closures */ |
| 3803 | 3816 | " AND (tagxref.value IS NULL OR tagxref.value='%q')", |
| 3804 | 3817 | zBr, zBr, zBr, TAG_BRANCH, zBr, zBr); |
| 3805 | 3818 | } |
| 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 | + } | |
| 3807 | 3827 | if( iOffset>0 ){ |
| 3808 | 3828 | /* Don't handle LIMIT here, otherwise print_timeline() |
| 3809 | 3829 | * will not determine the end-marker correctly! */ |
| 3810 | 3830 | blob_append_sql(&sql, "\n LIMIT -1 OFFSET %d", iOffset); |
| 3811 | 3831 | } |
| 3812 | 3832 |
| --- 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>← 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 @@ | ||
| 1026 | 1026 | form_begin(0, "%R/%s", g.zPath); |
| 1027 | 1027 | if( P("date_override") && g.perm.Setup ){ |
| 1028 | 1028 | @ <input type="hidden" name="date_override" value="%h(P("date_override"))"> |
| 1029 | 1029 | } |
| 1030 | 1030 | zScript = ticket_newpage_code(); |
| 1031 | + Th_Store("private_contact", ""); | |
| 1031 | 1032 | 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); | |
| 1036 | 1034 | if( uid ){ |
| 1037 | 1035 | char * zEmail = |
| 1038 | 1036 | db_text(0, "SELECT find_emailaddr(info) FROM user WHERE uid=%d", |
| 1039 | 1037 | uid); |
| 1040 | 1038 | if( zEmail ){ |
| @@ -1047,11 +1045,15 @@ | ||
| 1047 | 1045 | Th_Store("date", db_text(0, "SELECT datetime('now')")); |
| 1048 | 1046 | Th_CreateCommand(g.interp, "submit_ticket", submitTicketCmd, |
| 1049 | 1047 | (void*)&zNewUuid, 0); |
| 1050 | 1048 | if( g.thTrace ) Th_Trace("BEGIN_TKTNEW_SCRIPT<br>\n", -1); |
| 1051 | 1049 | 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 | + } | |
| 1053 | 1055 | return; |
| 1054 | 1056 | } |
| 1055 | 1057 | captcha_generate(0); |
| 1056 | 1058 | @ </form> |
| 1057 | 1059 | if( g.thTrace ) Th_Trace("END_TKTVIEW<br>\n", -1); |
| 1058 | 1060 |
| --- 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 @@ | ||
| 301 | 301 | } |
| 302 | 302 | |
| 303 | 303 | static const char zDefaultNew[] = |
| 304 | 304 | @ <th1> |
| 305 | 305 | @ if {![info exists mutype]} {set mutype Markdown} |
| 306 | -@ if {[info exists submit]} { | |
| 306 | +@ if {[info exists submit] || [info exists submitandnew]} { | |
| 307 | 307 | @ set status Open |
| 308 | 308 | @ if {$mutype eq "HTML"} { |
| 309 | 309 | @ set mimetype "text/html" |
| 310 | 310 | @ } elseif {$mutype eq "Wiki"} { |
| 311 | 311 | @ set mimetype "text/x-fossil-wiki" |
| @@ -349,10 +349,28 @@ | ||
| 349 | 349 | @ <td align="left"><th1>combobox severity $severity_choices 1</th1></td> |
| 350 | 350 | @ <td align="left">How debilitating is the problem? How badly does the problem |
| 351 | 351 | @ affect the operation of the product?</td> |
| 352 | 352 | @ </tr> |
| 353 | 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 | +@ | |
| 354 | 372 | @ <tr> |
| 355 | 373 | @ <td align="right">EMail:</td> |
| 356 | 374 | @ <td align="left"> |
| 357 | 375 | @ <input name="private_contact" value="$<private_contact>" size="30"> |
| 358 | 376 | @ </td> |
| @@ -405,19 +423,27 @@ | ||
| 405 | 423 | @ <tr> |
| 406 | 424 | @ <td><td align="left"> |
| 407 | 425 | @ <input type="submit" name="submit" value="Submit"> |
| 408 | 426 | @ </td> |
| 409 | 427 | @ <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> | |
| 411 | 437 | @ </tr> |
| 412 | 438 | @ <th1>enable_output 1</th1> |
| 413 | 439 | @ |
| 414 | 440 | @ <tr> |
| 415 | 441 | @ <td><td align="left"> |
| 416 | 442 | @ <input type="submit" name="cancel" value="Cancel"> |
| 417 | 443 | @ </td> |
| 418 | -@ <td>Abandon and forget this ticket</td> | |
| 444 | +@ <td>Abandon and forget this ticket.</td> | |
| 419 | 445 | @ </tr> |
| 420 | 446 | @ </table> |
| 421 | 447 | ; |
| 422 | 448 | |
| 423 | 449 | /* |
| @@ -465,10 +491,14 @@ | ||
| 465 | 491 | @ html "(0)</td></tr>\n" |
| 466 | 492 | @ } else { |
| 467 | 493 | @ html "<td class='tktDspValue' colspan='3'>Deleted</td></tr>\n" |
| 468 | 494 | @ } |
| 469 | 495 | @ } |
| 496 | +@ | |
| 497 | +@ if {[capexpr {n}]} { | |
| 498 | +@ submenu link "Copy Ticket" /tktnew/$tkt_uuid | |
| 499 | +@ } | |
| 470 | 500 | @ </th1> |
| 471 | 501 | @ <tr><td class="tktDspLabel">Title:</td> |
| 472 | 502 | @ <td class="tktDspValue" colspan="3"> |
| 473 | 503 | @ $<title> |
| 474 | 504 | @ </td></tr> |
| 475 | 505 |
| --- 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 @@ | ||
| 70 | 70 | old_exe = db_column_int(&q, 2); |
| 71 | 71 | if( old_exists ){ |
| 72 | 72 | db_ephemeral_blob(&q, 0, &new); |
| 73 | 73 | } |
| 74 | 74 | if( file_unsafe_in_tree_path(zFullname) ){ |
| 75 | - /* do nothign with this unsafe file */ | |
| 75 | + /* do nothing with this unsafe file */ | |
| 76 | 76 | }else if( old_exists ){ |
| 77 | 77 | if( new_exists ){ |
| 78 | 78 | fossil_print("%s %s\n", redoFlag ? "REDO" : "UNDO", zPathname); |
| 79 | 79 | }else{ |
| 80 | 80 | fossil_print("NEW %s\n", zPathname); |
| 81 | 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 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 |
+1
-1
| --- test/many-www.tcl | ||
| +++ test/many-www.tcl | ||
| @@ -24,11 +24,11 @@ | ||
| 24 | 24 | /setup |
| 25 | 25 | /dir |
| 26 | 26 | /wcontent |
| 27 | 27 | /attachlist |
| 28 | 28 | /taglist |
| 29 | - /test_env | |
| 29 | + /test-env | |
| 30 | 30 | /stat |
| 31 | 31 | /rcvfromlist |
| 32 | 32 | /urllist |
| 33 | 33 | /modreq |
| 34 | 34 | /info/d5c4 |
| 35 | 35 | |
| 36 | 36 | 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 |
+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 | +} |
| --- 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 | } |
+1
-1
| --- tools/fossil-stress.tcl | ||
| +++ tools/fossil-stress.tcl | ||
| @@ -93,11 +93,11 @@ | ||
| 93 | 93 | /fileage |
| 94 | 94 | /dir |
| 95 | 95 | /tree |
| 96 | 96 | /uvlist |
| 97 | 97 | /stat |
| 98 | - /test_env | |
| 98 | + /test-env | |
| 99 | 99 | /sitemap |
| 100 | 100 | /hash-collisions |
| 101 | 101 | /artifact_stats |
| 102 | 102 | /bloblist |
| 103 | 103 | /bigbloblist |
| 104 | 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 |
| --- 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 |
+1
-1
| --- www/aboutcgi.wiki | ||
| +++ www/aboutcgi.wiki | ||
| @@ -67,11 +67,11 @@ | ||
| 67 | 67 | <td>The query string that follows the "?" in the URL, if there is one. |
| 68 | 68 | </table> |
| 69 | 69 | |
| 70 | 70 | There are other CGI environment variables beyond those listed above. |
| 71 | 71 | 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] | |
| 73 | 73 | webpage that shows some of the CGI environment |
| 74 | 74 | variables that Fossil pays attention to. |
| 75 | 75 | |
| 76 | 76 | In addition to setting various CGI environment variables, if the HTTP |
| 77 | 77 | request contains POST content, then the web server relays the POST content |
| 78 | 78 |
| --- www/aboutcgi.wiki | |
| +++ www/aboutcgi.wiki | |
| @@ -67,11 +67,11 @@ | |
| 67 | <td>The query string that follows the "?" in the URL, if there is one. |
| 68 | </table> |
| 69 | |
| 70 | There are other CGI environment variables beyond those listed above. |
| 71 | Many Fossil servers implement the |
| 72 | [https://fossil-scm.org/home/test_env/two/three?abc=xyz|test_env] |
| 73 | webpage that shows some of the CGI environment |
| 74 | variables that Fossil pays attention to. |
| 75 | |
| 76 | In addition to setting various CGI environment variables, if the HTTP |
| 77 | request contains POST content, then the web server relays the POST content |
| 78 |
| --- www/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 @@ | ||
| 2 | 2 | |
| 3 | 3 | <h2 id='v2_26'>Changes for version 2.26 (pending)</h2> |
| 4 | 4 | |
| 5 | 5 | * Enhancements to [/help?cmd=diff|fossil diff] and similar: |
| 6 | 6 | <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, | |
| 8 | 8 | and uses files under that directory as the baseline for the diff. |
| 9 | 9 | <li> For "gdiff", if no [/help?cmd=gdiff-command|gdiff-command setting] |
| 10 | 10 | is defined, Fossil tries to do a --tk diff if "tclsh" and "wish" |
| 11 | 11 | are available, or a --by diff if not. |
| 12 | 12 | <li> The "Reload" button is added to --tk diffs, to bring the displayed |
| @@ -21,11 +21,11 @@ | ||
| 21 | 21 | <li> Defaults to using the new [/help?cmd=/ckout|/ckout page] as its |
| 22 | 22 | start page. Or, if the new "--from PATH" option is present, the |
| 23 | 23 | default start page becomes "/ckout?exbase=PATH". |
| 24 | 24 | <li> The new "--extpage FILENAME" option opens the named file as if it |
| 25 | 25 | where in a [./serverext.wiki|CGI extension]. Example usage: the |
| 26 | - person editing this change log has | |
| 26 | + person editing this change log has | |
| 27 | 27 | "fossil ui --extpage www/changes.wiki" running and hence can |
| 28 | 28 | press "Reload" on the web browser to view edits. |
| 29 | 29 | </ol> |
| 30 | 30 | * Enhancements to [/help?cmd=merge|fossil merge]: |
| 31 | 31 | <ol type="a"> |
| @@ -85,15 +85,13 @@ | ||
| 85 | 85 | end-points. |
| 86 | 86 | <li> The p= and d= parameters an reference different check-ins, which |
| 87 | 87 | case the timeline shows those check-ins that are both ancestors |
| 88 | 88 | of p= and descendants of d=. |
| 89 | 89 | <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 | |
| 91 | 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. | |
| 92 | + [/help?cmd=raw-bgcolor|raw-bgcolor setting] is turned on. | |
| 95 | 93 | </ol> |
| 96 | 94 | * The [/help?cmd=/docfile|/docfile webpage] was added. It works like |
| 97 | 95 | /doc but keeps the title of markdown documents with the document rather |
| 98 | 96 | that moving it up to the page title. |
| 99 | 97 | * Added the [/help?cmd=/clusterlist|/clusterlist page] for analysis |
| @@ -118,17 +116,25 @@ | ||
| 118 | 116 | COMMAND argument and only shows results for the specified |
| 119 | 117 | subcommand, not the entire command. |
| 120 | 118 | <li> The -u (--usage) option shows only the command-line syntax |
| 121 | 119 | <li> The -o (--options) option shows only the command-line options |
| 122 | 120 | </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> | |
| 125 | 130 | * Added the "hash" query parameter to the |
| 126 | 131 | [/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] | |
| 128 | 133 | 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. | |
| 130 | 136 | * Diverse minor fixes and additions. |
| 131 | 137 | |
| 132 | 138 | |
| 133 | 139 | <h2 id='v2_25'>Changes for version 2.25 (2024-11-06)</h2> |
| 134 | 140 | |
| 135 | 141 |
| --- 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 @@ | ||
| 164 | 164 | setting. |
| 165 | 165 | |
| 166 | 166 | This mechanism is similar to [email notification](./alerts.md) except that |
| 167 | 167 | the notification is sent via chat instead of via email. |
| 168 | 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. | |
| 169 | 187 | |
| 170 | 188 | ## Implementation Details |
| 171 | 189 | |
| 172 | 190 | *You do not need to understand how Fossil chat works in order to use it. |
| 173 | 191 | But many developers prefer to know how their tools work. |
| 174 | 192 |
| --- 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 @@ | ||
| 77 | 77 | |
| 78 | 78 | The `/home/www/proc` pathname should be adjusted so that the `/proc` |
| 79 | 79 | component is at the root of the chroot jail, of course. |
| 80 | 80 | |
| 81 | 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. | |
| 82 | +[`/test-env`][hte] page of the server to view the current load average. | |
| 83 | 83 | If the value for the load average is greater than zero, that means that |
| 84 | 84 | it is possible to activate the load-average limiter on that repository. |
| 85 | 85 | If the load average shows exactly "0.0", then that means that Fossil is |
| 86 | 86 | unable to find the load average. This can either be because it is in a |
| 87 | 87 | `chroot(2)` jail without `/proc` access, or because it is running on a |
| @@ -88,9 +88,9 @@ | ||
| 88 | 88 | system that does not support `getloadavg()` and so the load-average |
| 89 | 89 | limiter will not function. |
| 90 | 90 | |
| 91 | 91 | |
| 92 | 92 | [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 | |
| 94 | 94 | [gla]: https://linux.die.net/man/3/getloadavg |
| 95 | 95 | [lin]: http://www.linode.com |
| 96 | 96 | [sh]: ./selfhost.wiki |
| 97 | 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/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 |
+31
-9
| --- www/quickstart.wiki | ||
| +++ www/quickstart.wiki | ||
| @@ -14,11 +14,11 @@ | ||
| 14 | 14 | someplace on your $PATH. |
| 15 | 15 | |
| 16 | 16 | You can test that Fossil is present and working like this: |
| 17 | 17 | |
| 18 | 18 | <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 | |
| 20 | 20 | </b></pre> |
| 21 | 21 | |
| 22 | 22 | <h2 id="workflow" name="fslclone">General Work Flow</h2> |
| 23 | 23 | |
| 24 | 24 | Fossil works with repository files (a database in a single file with the project's |
| @@ -48,12 +48,38 @@ | ||
| 48 | 48 | |
| 49 | 49 | <pre><b>fossil init</b> <i>repository-filename</i> |
| 50 | 50 | </pre> |
| 51 | 51 | |
| 52 | 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.” | |
| 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 | + | |
| 55 | 81 | |
| 56 | 82 | <h2 id="clone">Cloning An Existing Repository</h2> |
| 57 | 83 | |
| 58 | 84 | Most fossil operations interact with a repository that is on the |
| 59 | 85 | local disk drive, not on a remote system. Hence, before accessing |
| @@ -384,16 +410,12 @@ | ||
| 384 | 410 | them and fails if local changes exist unless the <tt>--force</tt> |
| 385 | 411 | flag is used. |
| 386 | 412 | |
| 387 | 413 | <h2 id="branch" name="merge">Branching And Merging</h2> |
| 388 | 414 | |
| 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].) | |
| 395 | 417 | |
| 396 | 418 | To merge two branches back together, first |
| 397 | 419 | [/help/update | update] to the branch you want to merge into. |
| 398 | 420 | Then do a [/help/merge|merge] of the other branch that you want to incorporate |
| 399 | 421 | the changes from. For example, to merge "featureX" changes into "trunk" |
| 400 | 422 |
| --- 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 |
+1
-1
| --- www/server/windows/service.md | ||
| +++ www/server/windows/service.md | ||
| @@ -55,11 +55,11 @@ | ||
| 55 | 55 | for temporary files is exempted from such scanning. Ordinarily, this |
| 56 | 56 | will be a subdirectory named "fossil" in the temporary directory given |
| 57 | 57 | by the Windows GetTempPath(...) API, [namely](https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-gettemppathw#remarks) |
| 58 | 58 | the value of the first existing environment variable from `%TMP%`, `%TEMP%`, |
| 59 | 59 | `%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. | |
| 61 | 61 | Excluding this subdirectory will avoid certain rare failures where the |
| 62 | 62 | fossil.exe process is unable to use the directory normally during a scan. |
| 63 | 63 | |
| 64 | 64 | ### <a id='PowerShell'></a>Advanced service installation using PowerShell |
| 65 | 65 | |
| 66 | 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/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 |
+1
-1
| --- www/serverext.wiki | ||
| +++ www/serverext.wiki | ||
| @@ -189,11 +189,11 @@ | ||
| 189 | 189 | to find more detail about what each of the above variables mean and how |
| 190 | 190 | they are used. |
| 191 | 191 | Live listings of the values of some or all of these environment variables |
| 192 | 192 | can be found at links like these: |
| 193 | 193 | |
| 194 | - * [https://fossil-scm.org/home/test_env] | |
| 194 | + * [https://fossil-scm.org/home/test-env] | |
| 195 | 195 | * [https://sqlite.org/src/ext/checklist/top/env] |
| 196 | 196 | |
| 197 | 197 | In addition to the standard CGI environment variables listed above, |
| 198 | 198 | Fossil adds the following: |
| 199 | 199 | |
| 200 | 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 |
| --- 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 |