Fossil SCM
Merge all the latest trunk enhancements into the http1-1-chunked branch so that the branch can be more easily tested.
Commit
4a648d7410ad9d6f4baf10e6c14ea0d6e86708552d014fb386bb1c7bb411fa81
Parent
441a35ce2a1f63c…
47 files changed
+112
-40
+448
-221
+19
-6
+17
-41
+43
-85
+30
-37
+72
-10
+931
-109
+20
-1
+3
-2
+2
+163
-1
+26
-14
+1
-1
+14
-7
+542
-81
+663
+8
-5
+4
+1227
-12
+5
-6
+10
-10
+62
-7
+3
-2
+6
-2
+2
+4
-8
+39
-3
+13
-3
+17
-2
+22
-3
+33
+18
+11
-6
+2
-2
+11
-39
+16
-15
+115
-43
+2
+4
+5
-2
+2
-2
+96
-10
+142
-119
+6
-6
+3
-1
+3
-3
~
extsrc/shell.c
~
extsrc/sqlite3.c
~
extsrc/sqlite3.h
~
skins/ardoise/css.txt
~
skins/blitz/css.txt
~
skins/darkmode/css.txt
~
src/ajax.c
~
src/attach.c
~
src/builtin.c
~
src/cgi.c
~
src/comformat.c
~
src/default.css
~
src/delta.c
~
src/diffcmd.c
~
src/event.c
~
src/forum.c
~
src/fossil.attach.js
~
src/fossil.bootstrap.js
~
src/fossil.dom.js
~
src/fossil.page.forumpost.js
~
src/fossil.page.wikiedit.js
~
src/fossil.popupwidget.js
~
src/fossil.storage.js
~
src/fossil.tabs.js
~
src/info.c
~
src/main.mk
~
src/manifest.c
~
src/markdown.md
~
src/stash.c
~
src/statrep.c
~
src/style.c
~
src/style.forum.css
~
src/timeline.c
~
src/tkt.c
~
src/util.c
~
src/wiki.c
~
src/wiki.wiki
~
src/wikiformat.c
~
win/Makefile.mingw
~
win/Makefile.msc
~
www/changes.wiki
~
www/customskin.md
~
www/forum.wiki
~
www/fossil-v-git.wiki
~
www/javascript.md
~
www/server/any/cgi.md
~
www/sync.wiki
+112
-40
| --- extsrc/shell.c | ||
| +++ extsrc/shell.c | ||
| @@ -5424,11 +5424,11 @@ | ||
| 5424 | 5424 | } |
| 5425 | 5425 | j = 0; |
| 5426 | 5426 | }else{ |
| 5427 | 5427 | i = 0; |
| 5428 | 5428 | } |
| 5429 | - (void)memcpy(&p->buffer[j], &data[i], len - i); | |
| 5429 | + if( len-i>0 ) (void)memcpy(&p->buffer[j], &data[i], len - i); | |
| 5430 | 5430 | } |
| 5431 | 5431 | |
| 5432 | 5432 | /* Compute a string using sqlite3_vsnprintf() and hash it */ |
| 5433 | 5433 | static void hash_step_vformat( |
| 5434 | 5434 | SHA1Context *p, /* Add content to this context */ |
| @@ -6071,32 +6071,41 @@ | ||
| 6071 | 6071 | } |
| 6072 | 6072 | z[i] = 0; |
| 6073 | 6073 | sqlite3_result_text(pCtx, z, i, sqlite3_free); |
| 6074 | 6074 | } |
| 6075 | 6075 | |
| 6076 | +/* Forward declaration */ | |
| 6077 | +static void decimal_expand(Decimal *p, int nDigit, int nFrac); | |
| 6078 | + | |
| 6076 | 6079 | /* |
| 6077 | 6080 | ** Round a decimal value to N significant digits. N must be positive. |
| 6078 | 6081 | */ |
| 6079 | 6082 | static void decimal_round(Decimal *p, int N){ |
| 6080 | 6083 | int i; |
| 6081 | - int nZero; | |
| 6084 | + int nZero; /* Number of leading zeros */ | |
| 6082 | 6085 | if( N<1 ) return; |
| 6083 | 6086 | if( p==0 ) return; |
| 6084 | 6087 | if( p->nDigit<=N ) return; |
| 6085 | 6088 | for(nZero=0; nZero<p->nDigit && p->a[nZero]==0; nZero++){} |
| 6086 | 6089 | N += nZero; |
| 6087 | 6090 | if( p->nDigit<=N ) return; |
| 6088 | - if( p->a[N]>4 ){ | |
| 6091 | + if( p->a[N]>=5 ){ | |
| 6092 | + /* If all leading digits are 9, increase the number of digits | |
| 6093 | + ** by adding a new 0 to the front */ | |
| 6094 | + for(i=0; i<N && p->a[i]==9; i++){} | |
| 6095 | + if( i==N ){ | |
| 6096 | + decimal_expand(p, p->nDigit+1, 0); | |
| 6097 | + if( p->oom ) return; | |
| 6098 | + } | |
| 6099 | + | |
| 6100 | + /* Do the rounding */ | |
| 6089 | 6101 | p->a[N-1]++; |
| 6090 | 6102 | for(i=N-1; i>0 && p->a[i]>9; i--){ |
| 6091 | 6103 | p->a[i] = 0; |
| 6092 | 6104 | p->a[i-1]++; |
| 6093 | 6105 | } |
| 6094 | - if( p->a[0]>9 ){ | |
| 6095 | - p->a[0] = 1; | |
| 6096 | - p->nFrac--; | |
| 6097 | - } | |
| 6106 | + assert( p->a[0]<=9 ); | |
| 6098 | 6107 | } |
| 6099 | 6108 | memset(&p->a[N], 0, p->nDigit - N); |
| 6100 | 6109 | } |
| 6101 | 6110 | |
| 6102 | 6111 | /* |
| @@ -8378,19 +8387,26 @@ | ||
| 8378 | 8387 | } |
| 8379 | 8388 | }else{ |
| 8380 | 8389 | iMin = iMax = sqlite3_value_int64(argv[iArg++]); |
| 8381 | 8390 | } |
| 8382 | 8391 | }else{ |
| 8383 | - if( idxNum & 0x0300 ){ /* value>X or value>=X */ | |
| 8392 | + if( idxNum & 0x0300 ){ /* value>X (0x200) or value>=X (0x100) */ | |
| 8384 | 8393 | if( sqlite3_value_numeric_type(argv[iArg])==SQLITE_FLOAT ){ |
| 8385 | 8394 | double r = sqlite3_value_double(argv[iArg++]); |
| 8386 | - if( r<(double)SMALLEST_INT64 ){ | |
| 8395 | + if( r<=(double)SMALLEST_INT64 ){ | |
| 8387 | 8396 | iMin = SMALLEST_INT64; |
| 8388 | - }else if( (idxNum & 0x0200)!=0 && r==seriesCeil(r) ){ | |
| 8389 | - iMin = (sqlite3_int64)seriesCeil(r)+1; | |
| 8397 | + }else if( r>(double)LARGEST_INT64 ){ | |
| 8398 | + goto series_no_rows; | |
| 8390 | 8399 | }else{ |
| 8391 | 8400 | iMin = (sqlite3_int64)seriesCeil(r); |
| 8401 | + if( iMin<0 && r>0.0 ){ | |
| 8402 | + iMin = LARGEST_INT64; | |
| 8403 | + } | |
| 8404 | + if( (idxNum & 0x0200)!=0 && r==seriesCeil(r) ){ | |
| 8405 | + if( iMin==LARGEST_INT64 ) goto series_no_rows; | |
| 8406 | + iMin++; | |
| 8407 | + } | |
| 8392 | 8408 | } |
| 8393 | 8409 | }else{ |
| 8394 | 8410 | iMin = sqlite3_value_int64(argv[iArg++]); |
| 8395 | 8411 | if( (idxNum & 0x0200)!=0 ){ |
| 8396 | 8412 | if( iMin==LARGEST_INT64 ){ |
| @@ -8399,19 +8415,25 @@ | ||
| 8399 | 8415 | iMin++; |
| 8400 | 8416 | } |
| 8401 | 8417 | } |
| 8402 | 8418 | } |
| 8403 | 8419 | } |
| 8404 | - if( idxNum & 0x3000 ){ /* value<X or value<=X */ | |
| 8420 | + if( idxNum & 0x3000 ){ /* value<X (0x2000) or value<=X (0x1000) */ | |
| 8405 | 8421 | if( sqlite3_value_numeric_type(argv[iArg])==SQLITE_FLOAT ){ |
| 8406 | 8422 | double r = sqlite3_value_double(argv[iArg++]); |
| 8407 | - if( r>(double)LARGEST_INT64 ){ | |
| 8423 | + if( r>=(double)LARGEST_INT64 ){ | |
| 8408 | 8424 | iMax = LARGEST_INT64; |
| 8409 | - }else if( (idxNum & 0x2000)!=0 && r==seriesFloor(r) ){ | |
| 8410 | - iMax = ((sqlite3_int64)r)-1; | |
| 8425 | + }else if( r<=(double)SMALLEST_INT64 ){ | |
| 8426 | + goto series_no_rows; | |
| 8411 | 8427 | }else{ |
| 8412 | 8428 | iMax = (sqlite3_int64)seriesFloor(r); |
| 8429 | + if( iMax<0 && r>0.0 ){ | |
| 8430 | + iMax = LARGEST_INT64; | |
| 8431 | + }else if( (idxNum & 0x2000)!=0 && r==seriesFloor(r) ){ | |
| 8432 | + if( iMax==SMALLEST_INT64 ) goto series_no_rows; | |
| 8433 | + iMax--; | |
| 8434 | + } | |
| 8413 | 8435 | } |
| 8414 | 8436 | }else{ |
| 8415 | 8437 | iMax = sqlite3_value_int64(argv[iArg++]); |
| 8416 | 8438 | if( idxNum & 0x2000 ){ |
| 8417 | 8439 | if( iMax==SMALLEST_INT64 ){ |
| @@ -10884,10 +10906,13 @@ | ||
| 10884 | 10906 | ** Return NULL if unable. |
| 10885 | 10907 | ** |
| 10886 | 10908 | ** The file or directory X is not required to exist. The answer is formed |
| 10887 | 10909 | ** by calling system realpath() on the prefix of X that does exist and |
| 10888 | 10910 | ** appending the tail of X that does not (yet) exist. |
| 10911 | +** | |
| 10912 | +** FIXME: This routine sometimes returns NULL rather than raising | |
| 10913 | +** an SQLITE_NOMEM error if an OOM is encountered. | |
| 10889 | 10914 | */ |
| 10890 | 10915 | static void realpathFunc( |
| 10891 | 10916 | sqlite3_context *context, |
| 10892 | 10917 | int argc, |
| 10893 | 10918 | sqlite3_value **argv |
| @@ -10906,10 +10931,11 @@ | ||
| 10906 | 10931 | (void)argc; |
| 10907 | 10932 | zPath = (const char*)sqlite3_value_text(argv[0]); |
| 10908 | 10933 | if( zPath==0 ) return; |
| 10909 | 10934 | if( zPath[0]==0 ) zPath = "."; |
| 10910 | 10935 | zCopy = sqlite3_mprintf("%s",zPath); |
| 10936 | + if( zCopy==0 ) return; | |
| 10911 | 10937 | len = strlen(zCopy); |
| 10912 | 10938 | while( len>1 && (zCopy[len-1]=='/' || (isWin && zCopy[len-1]=='\\')) ){ |
| 10913 | 10939 | len--; |
| 10914 | 10940 | } |
| 10915 | 10941 | zCopy[len] = 0; |
| @@ -12699,10 +12725,11 @@ | ||
| 12699 | 12725 | |
| 12700 | 12726 | for(p=pCsr->pFreeEntry; p; p=pNext){ |
| 12701 | 12727 | pNext = p->pNext; |
| 12702 | 12728 | zipfileEntryFree(p); |
| 12703 | 12729 | } |
| 12730 | + pCsr->pFreeEntry = 0; | |
| 12704 | 12731 | } |
| 12705 | 12732 | |
| 12706 | 12733 | /* |
| 12707 | 12734 | ** Destructor for an ZipfileCsr. |
| 12708 | 12735 | */ |
| @@ -13101,11 +13128,17 @@ | ||
| 13101 | 13128 | } |
| 13102 | 13129 | } |
| 13103 | 13130 | |
| 13104 | 13131 | if( rc==SQLITE_OK ){ |
| 13105 | 13132 | u32 *pt = &pNew->mUnixTime; |
| 13106 | - pNew->cds.zFile = sqlite3_mprintf("%.*s", nFile, aRead); | |
| 13133 | + /* aRead[0..nFile-1] might contain embedded \000 characters | |
| 13134 | + ** See Bug 2026-05-31T11:43:05Z */ | |
| 13135 | + pNew->cds.zFile = sqlite3_malloc64(nFile+1); | |
| 13136 | + if( pNew->cds.zFile!=0 ){ | |
| 13137 | + memcpy(pNew->cds.zFile, aRead, nFile); | |
| 13138 | + pNew->cds.zFile[nFile] = 0; | |
| 13139 | + } | |
| 13107 | 13140 | pNew->aExtra = (u8*)&pNew[1]; |
| 13108 | 13141 | memcpy(pNew->aExtra, &aRead[nFile], nExtra); |
| 13109 | 13142 | if( pNew->cds.zFile==0 ){ |
| 13110 | 13143 | rc = SQLITE_NOMEM; |
| 13111 | 13144 | }else if( 0==zipfileScanExtra(&aRead[nFile], pNew->cds.nExtra, pt) ){ |
| @@ -14205,14 +14238,14 @@ | ||
| 14205 | 14238 | ZipfileBuffer body; |
| 14206 | 14239 | ZipfileBuffer cds; |
| 14207 | 14240 | }; |
| 14208 | 14241 | |
| 14209 | 14242 | static int zipfileBufferGrow(ZipfileBuffer *pBuf, i64 nByte){ |
| 14210 | - if( pBuf->n+nByte>pBuf->nAlloc ){ | |
| 14243 | + if( (pBuf->nAlloc-pBuf->n)<nByte ){ | |
| 14211 | 14244 | u8 *aNew; |
| 14212 | - sqlite3_int64 nNew = pBuf->n ? pBuf->n*2 : 512; | |
| 14213 | - int nReq = pBuf->n + nByte; | |
| 14245 | + i64 nNew = pBuf->n ? (i64)pBuf->n*2 : 512; | |
| 14246 | + i64 nReq = pBuf->n + nByte; | |
| 14214 | 14247 | |
| 14215 | 14248 | while( nNew<nReq ) nNew = nNew*2; |
| 14216 | 14249 | aNew = sqlite3_realloc64(pBuf->a, nNew); |
| 14217 | 14250 | if( aNew==0 ) return SQLITE_NOMEM; |
| 14218 | 14251 | pBuf->a = aNew; |
| @@ -16310,11 +16343,11 @@ | ||
| 16310 | 16343 | p->pTable = pTab; |
| 16311 | 16344 | |
| 16312 | 16345 | /* The statement the vtab will pass to sqlite3_declare_vtab() */ |
| 16313 | 16346 | zInner = idxAppendText(&rc, 0, "CREATE TABLE x("); |
| 16314 | 16347 | for(i=0; i<pTab->nCol; i++){ |
| 16315 | - zInner = idxAppendText(&rc, zInner, "%s%Q COLLATE %s", | |
| 16348 | + zInner = idxAppendText(&rc, zInner, "%s%Q COLLATE %Q", | |
| 16316 | 16349 | (i==0 ? "" : ", "), pTab->aCol[i].zName, pTab->aCol[i].zColl |
| 16317 | 16350 | ); |
| 16318 | 16351 | } |
| 16319 | 16352 | zInner = idxAppendText(&rc, zInner, ")"); |
| 16320 | 16353 | |
| @@ -16510,11 +16543,11 @@ | ||
| 16510 | 16543 | sqlite3_free(zCols); |
| 16511 | 16544 | sqlite3_free(zOrder); |
| 16512 | 16545 | return sqlite3_reset(pIndexXInfo); |
| 16513 | 16546 | } |
| 16514 | 16547 | zCols = idxAppendText(&rc, zCols, |
| 16515 | - "%sx.%Q IS sqlite_expert_rem(%d, x.%Q) COLLATE %s", | |
| 16548 | + "%sx.%Q IS sqlite_expert_rem(%d, x.%Q) COLLATE %Q", | |
| 16516 | 16549 | zComma, zName, nCol, zName, zColl |
| 16517 | 16550 | ); |
| 16518 | 16551 | zOrder = idxAppendText(&rc, zOrder, "%s%d", zComma, ++nCol); |
| 16519 | 16552 | } |
| 16520 | 16553 | sqlite3_reset(pIndexXInfo); |
| @@ -25163,11 +25196,11 @@ | ||
| 25163 | 25196 | /* |
| 25164 | 25197 | ** Return true if either the SQLITE_NO_COLOR compile-time option is used |
| 25165 | 25198 | ** or if the NO_COLOR environment variable exists |
| 25166 | 25199 | */ |
| 25167 | 25200 | static int shellNoColor(void){ |
| 25168 | -#ifdef SQLITE_NO_COLOR | |
| 25201 | +#if defined(SQLITE_NO_COLOR) || defined(SQLITE_SHELL_FIDDLE) | |
| 25169 | 25202 | return 1; |
| 25170 | 25203 | #else |
| 25171 | 25204 | return getenv("NO_COLOR")!=0; |
| 25172 | 25205 | #endif |
| 25173 | 25206 | } |
| @@ -31316,11 +31349,11 @@ | ||
| 31316 | 31349 | typedef struct ArCommand ArCommand; |
| 31317 | 31350 | struct ArCommand { |
| 31318 | 31351 | u8 eCmd; /* An AR_CMD_* value */ |
| 31319 | 31352 | u8 bVerbose; /* True if --verbose */ |
| 31320 | 31353 | u8 bZip; /* True if the archive is a ZIP */ |
| 31321 | - u8 bDryRun; /* True if --dry-run */ | |
| 31354 | + u8 bDryRun; /* 1 for --dry-run, 2 for --debug */ | |
| 31322 | 31355 | u8 bAppend; /* True if --append */ |
| 31323 | 31356 | u8 bGlob; /* True if --glob */ |
| 31324 | 31357 | u8 fromCmdLine; /* Run from -A instead of .archive */ |
| 31325 | 31358 | int nArg; /* Number of command arguments */ |
| 31326 | 31359 | char *zSrcTable; /* "sqlar", "zipfile($file)" or "zip" */ |
| @@ -31378,10 +31411,11 @@ | ||
| 31378 | 31411 | #define AR_SWITCH_FILE 9 |
| 31379 | 31412 | #define AR_SWITCH_DIRECTORY 10 |
| 31380 | 31413 | #define AR_SWITCH_APPEND 11 |
| 31381 | 31414 | #define AR_SWITCH_DRYRUN 12 |
| 31382 | 31415 | #define AR_SWITCH_GLOB 13 |
| 31416 | +#define AR_SWITCH_DEBUG 14 | |
| 31383 | 31417 | |
| 31384 | 31418 | static int arProcessSwitch(ArCommand *pAr, int eSwitch, const char *zArg){ |
| 31385 | 31419 | switch( eSwitch ){ |
| 31386 | 31420 | case AR_CMD_CREATE: |
| 31387 | 31421 | case AR_CMD_EXTRACT: |
| @@ -31395,11 +31429,14 @@ | ||
| 31395 | 31429 | } |
| 31396 | 31430 | pAr->eCmd = eSwitch; |
| 31397 | 31431 | break; |
| 31398 | 31432 | |
| 31399 | 31433 | case AR_SWITCH_DRYRUN: |
| 31400 | - pAr->bDryRun = 1; | |
| 31434 | + if( pAr->bDryRun<2 ) pAr->bDryRun = 1; | |
| 31435 | + break; | |
| 31436 | + case AR_SWITCH_DEBUG: | |
| 31437 | + pAr->bDryRun = 2; | |
| 31401 | 31438 | break; |
| 31402 | 31439 | case AR_SWITCH_GLOB: |
| 31403 | 31440 | pAr->bGlob = 1; |
| 31404 | 31441 | break; |
| 31405 | 31442 | case AR_SWITCH_VERBOSE: |
| @@ -31446,10 +31483,11 @@ | ||
| 31446 | 31483 | { "verbose", 'v', AR_SWITCH_VERBOSE, 0 }, |
| 31447 | 31484 | { "file", 'f', AR_SWITCH_FILE, 1 }, |
| 31448 | 31485 | { "append", 'a', AR_SWITCH_APPEND, 1 }, |
| 31449 | 31486 | { "directory", 'C', AR_SWITCH_DIRECTORY, 1 }, |
| 31450 | 31487 | { "dryrun", 'n', AR_SWITCH_DRYRUN, 0 }, |
| 31488 | + { "debug", 0, AR_SWITCH_DEBUG, 0 }, | |
| 31451 | 31489 | { "glob", 'g', AR_SWITCH_GLOB, 0 }, |
| 31452 | 31490 | }; |
| 31453 | 31491 | int nSwitch = sizeof(aSwitch) / sizeof(struct ArSwitch); |
| 31454 | 31492 | struct ArSwitch *pEnd = &aSwitch[nSwitch]; |
| 31455 | 31493 | |
| @@ -31746,18 +31784,41 @@ | ||
| 31746 | 31784 | |
| 31747 | 31785 | /* |
| 31748 | 31786 | ** Implementation of .ar "eXtract" command. |
| 31749 | 31787 | */ |
| 31750 | 31788 | static int arExtractCommand(ArCommand *pAr){ |
| 31789 | + /* The zSql1[] string is a template for the query that does the | |
| 31790 | + ** extraction. Notes: | |
| 31791 | + ** | |
| 31792 | + ** * $dir is the directory into which the archive is to be extracted | |
| 31793 | + ** * $pass is the integer pass number: 0, 1, or 2 | |
| 31794 | + ** * The dest CTE is created so that realpath($dir) only needs | |
| 31795 | + ** to be called once. | |
| 31796 | + */ | |
| 31751 | 31797 | const char *zSql1 = |
| 31752 | - "WITH dest(dpath,dlen) AS (SELECT realpath($dir),length(realpath($dir)))\n" | |
| 31753 | - "SELECT ($dir || name),\n" | |
| 31754 | - " CASE WHEN $dryrun THEN 0\n" | |
| 31755 | - " ELSE writefile($dir||name, %s, mode, mtime) END\n" | |
| 31798 | + "WITH dest(dpath,dlen) AS (\n" | |
| 31799 | +#ifdef _WIN32 | |
| 31800 | + " SELECT realpath($dir) || '\\',\n" | |
| 31801 | +#else | |
| 31802 | + " SELECT realpath($dir) || '/',\n" | |
| 31803 | +#endif | |
| 31804 | + " 1+length(realpath($dir))\n" | |
| 31805 | + ")\n" | |
| 31806 | + "SELECT\n" | |
| 31807 | + " ($dir || name),\n" | |
| 31808 | + " CASE $dryrun\n" /* vv--- azExtraArg */ | |
| 31809 | + " WHEN 0 THEN writefile($dir||name, %s, mode, mtime)\n" | |
| 31810 | + " WHEN 1 THEN 0\n" | |
| 31811 | + " ELSE shell_putsnl(format('writefile(%%Q,%%s,%%0o,%%d)'," | |
| 31812 | + "$dir||name,quote(%s),mode,mtime)) IS NULL\n" | |
| 31813 | + " END\n" /* ^^--- azExtraArg */ | |
| 31756 | 31814 | " FROM dest CROSS JOIN %s\n" |
| 31757 | - " WHERE (%s)\n" | |
| 31758 | - " AND (data IS NULL OR $pass==0)\n" /* Dirs both passes */ | |
| 31815 | + " WHERE (%s)\n" /* ^^-- pAr->zSrcTable */ | |
| 31816 | + /* ^^--- zWhere */ | |
| 31817 | + " AND (CASE $pass WHEN 0 THEN (mode&0xf000)<>0xa000\n" | |
| 31818 | + " WHEN 1 THEN (mode&0xf000)=0xa000\n" | |
| 31819 | + " ELSE data IS NULL END)\n" | |
| 31759 | 31820 | " AND dpath=substr(realpath($dir||name),1,dlen)\n" /* No escapes */ |
| 31760 | 31821 | " AND name NOT GLOB '*..[/\\]*'\n"; /* No /../ in paths */ |
| 31761 | 31822 | |
| 31762 | 31823 | const char *azExtraArg[] = { |
| 31763 | 31824 | "sqlar_uncompress(data, sz)", |
| @@ -31784,39 +31845,48 @@ | ||
| 31784 | 31845 | } |
| 31785 | 31846 | if( zDir==0 ) rc = SQLITE_NOMEM; |
| 31786 | 31847 | } |
| 31787 | 31848 | |
| 31788 | 31849 | shellPreparePrintf(pAr->db, &rc, &pSql, zSql1, |
| 31789 | - azExtraArg[pAr->bZip], pAr->zSrcTable, zWhere | |
| 31850 | + azExtraArg[pAr->bZip], | |
| 31851 | + azExtraArg[pAr->bZip], | |
| 31852 | + pAr->zSrcTable, | |
| 31853 | + zWhere | |
| 31790 | 31854 | ); |
| 31791 | 31855 | |
| 31792 | 31856 | if( rc==SQLITE_OK ){ |
| 31793 | 31857 | j = sqlite3_bind_parameter_index(pSql, "$dir"); |
| 31794 | 31858 | sqlite3_bind_text(pSql, j, zDir, -1, SQLITE_STATIC); |
| 31795 | 31859 | j = sqlite3_bind_parameter_index(pSql, "$dryrun"); |
| 31796 | 31860 | sqlite3_bind_int(pSql, j, pAr->bDryRun); |
| 31797 | 31861 | |
| 31798 | 31862 | /* Run the SELECT statement twice |
| 31799 | - ** (0) writefile() all files and directories | |
| 31800 | - ** (1) writefile() for directory again | |
| 31801 | - ** The second pass is so that the timestamps for extracted directories | |
| 31863 | + ** (0) writefile() files and directories | |
| 31864 | + ** (1) writefile() symlinks | |
| 31865 | + ** (2) writefile() for directory again | |
| 31866 | + ** The third pass is so that the timestamps for extracted directories | |
| 31802 | 31867 | ** will be reset to the value in the archive, since populating them |
| 31803 | 31868 | ** in the first pass will have changed the timestamp. */ |
| 31804 | - for(i=0; i<2; i++){ | |
| 31869 | + for(i=0; i<3; i++){ | |
| 31870 | + if( pAr->bDryRun>=2 ){ | |
| 31871 | + cli_printf(pAr->out, "*** BEGIN PASS %d ***\n", i+1); | |
| 31872 | + } | |
| 31805 | 31873 | j = sqlite3_bind_parameter_index(pSql, "$pass"); |
| 31806 | 31874 | sqlite3_bind_int(pSql, j, i); |
| 31807 | - if( pAr->bDryRun ){ | |
| 31875 | + if( pAr->bDryRun && i==0 ){ | |
| 31808 | 31876 | cli_printf(pAr->out, "%s\n", sqlite3_sql(pSql)); |
| 31809 | - if( pAr->bVerbose==0 ) break; | |
| 31810 | 31877 | } |
| 31811 | 31878 | while( rc==SQLITE_OK && SQLITE_ROW==sqlite3_step(pSql) ){ |
| 31812 | 31879 | if( i==0 && pAr->bVerbose ){ |
| 31813 | 31880 | cli_printf(pAr->out, "%s\n", sqlite3_column_text(pSql, 0)); |
| 31814 | 31881 | } |
| 31815 | 31882 | } |
| 31816 | - if( pAr->bDryRun ) break; | |
| 31883 | + if( pAr->bDryRun==1 ) break; | |
| 31817 | 31884 | shellReset(&rc, pSql); |
| 31885 | + if( pAr->bDryRun>=2 ){ | |
| 31886 | + cli_printf(pAr->out, "*** END PASS %d ***\n", i+1); | |
| 31887 | + } | |
| 31818 | 31888 | } |
| 31819 | 31889 | shellFinalize(&rc, pSql); |
| 31820 | 31890 | } |
| 31821 | 31891 | |
| 31822 | 31892 | sqlite3_free(zDir); |
| @@ -32327,11 +32397,11 @@ | ||
| 32327 | 32397 | ; |
| 32328 | 32398 | static const char * const zCollectVar = "\ |
| 32329 | 32399 | SELECT\ |
| 32330 | 32400 | '('||x'0a'\ |
| 32331 | 32401 | || group_concat(\ |
| 32332 | - cname||' ANY',\ | |
| 32402 | + cname,\ | |
| 32333 | 32403 | ','||iif((cpos-1)%4>0, ' ', x'0a'||' '))\ |
| 32334 | 32404 | ||')' AS ColsSpec \ |
| 32335 | 32405 | FROM (\ |
| 32336 | 32406 | SELECT cpos, printf('\"%w\"',printf('%!.*s%s', nlen-chop,name,suff)) AS cname \ |
| 32337 | 32407 | FROM ColNames ORDER BY cpos\ |
| @@ -37939,11 +38009,11 @@ | ||
| 37939 | 38009 | } |
| 37940 | 38010 | |
| 37941 | 38011 | /* |
| 37942 | 38012 | ** The callback from atexit(). |
| 37943 | 38013 | */ |
| 37944 | -static void abnormalExit(void){ | |
| 38014 | +static void SQLITE_CDECL abnormalExit(void){ | |
| 37945 | 38015 | if( seenInterrupt ) eputz("Program interrupted.\n"); |
| 37946 | 38016 | if( globalShellState ){ |
| 37947 | 38017 | clearTempFile(globalShellState, 1, 1); |
| 37948 | 38018 | } |
| 37949 | 38019 | } |
| @@ -38498,12 +38568,14 @@ | ||
| 38498 | 38568 | }else if( cli_strcmp(z,"-nullvalue")==0 ){ |
| 38499 | 38569 | modeSetStr(&data.mode.spec.zNull, |
| 38500 | 38570 | cmdline_option_value(argc,argv,++i)); |
| 38501 | 38571 | }else if( cli_strcmp(z,"-header")==0 ){ |
| 38502 | 38572 | data.mode.spec.bTitles = QRF_Yes; |
| 38573 | + data.mode.mFlags |= MFLG_HDR; | |
| 38503 | 38574 | }else if( cli_strcmp(z,"-noheader")==0 ){ |
| 38504 | 38575 | data.mode.spec.bTitles = QRF_No; |
| 38576 | + data.mode.mFlags |= MFLG_HDR; | |
| 38505 | 38577 | }else if( cli_strcmp(z,"-echo")==0 ){ |
| 38506 | 38578 | data.mode.mFlags |= MFLG_ECHO; |
| 38507 | 38579 | }else if( cli_strcmp(z,"-eqp")==0 ){ |
| 38508 | 38580 | data.mode.autoEQP = AUTOEQP_on; |
| 38509 | 38581 | }else if( cli_strcmp(z,"-eqpfull")==0 ){ |
| 38510 | 38582 |
| --- extsrc/shell.c | |
| +++ extsrc/shell.c | |
| @@ -5424,11 +5424,11 @@ | |
| 5424 | } |
| 5425 | j = 0; |
| 5426 | }else{ |
| 5427 | i = 0; |
| 5428 | } |
| 5429 | (void)memcpy(&p->buffer[j], &data[i], len - i); |
| 5430 | } |
| 5431 | |
| 5432 | /* Compute a string using sqlite3_vsnprintf() and hash it */ |
| 5433 | static void hash_step_vformat( |
| 5434 | SHA1Context *p, /* Add content to this context */ |
| @@ -6071,32 +6071,41 @@ | |
| 6071 | } |
| 6072 | z[i] = 0; |
| 6073 | sqlite3_result_text(pCtx, z, i, sqlite3_free); |
| 6074 | } |
| 6075 | |
| 6076 | /* |
| 6077 | ** Round a decimal value to N significant digits. N must be positive. |
| 6078 | */ |
| 6079 | static void decimal_round(Decimal *p, int N){ |
| 6080 | int i; |
| 6081 | int nZero; |
| 6082 | if( N<1 ) return; |
| 6083 | if( p==0 ) return; |
| 6084 | if( p->nDigit<=N ) return; |
| 6085 | for(nZero=0; nZero<p->nDigit && p->a[nZero]==0; nZero++){} |
| 6086 | N += nZero; |
| 6087 | if( p->nDigit<=N ) return; |
| 6088 | if( p->a[N]>4 ){ |
| 6089 | p->a[N-1]++; |
| 6090 | for(i=N-1; i>0 && p->a[i]>9; i--){ |
| 6091 | p->a[i] = 0; |
| 6092 | p->a[i-1]++; |
| 6093 | } |
| 6094 | if( p->a[0]>9 ){ |
| 6095 | p->a[0] = 1; |
| 6096 | p->nFrac--; |
| 6097 | } |
| 6098 | } |
| 6099 | memset(&p->a[N], 0, p->nDigit - N); |
| 6100 | } |
| 6101 | |
| 6102 | /* |
| @@ -8378,19 +8387,26 @@ | |
| 8378 | } |
| 8379 | }else{ |
| 8380 | iMin = iMax = sqlite3_value_int64(argv[iArg++]); |
| 8381 | } |
| 8382 | }else{ |
| 8383 | if( idxNum & 0x0300 ){ /* value>X or value>=X */ |
| 8384 | if( sqlite3_value_numeric_type(argv[iArg])==SQLITE_FLOAT ){ |
| 8385 | double r = sqlite3_value_double(argv[iArg++]); |
| 8386 | if( r<(double)SMALLEST_INT64 ){ |
| 8387 | iMin = SMALLEST_INT64; |
| 8388 | }else if( (idxNum & 0x0200)!=0 && r==seriesCeil(r) ){ |
| 8389 | iMin = (sqlite3_int64)seriesCeil(r)+1; |
| 8390 | }else{ |
| 8391 | iMin = (sqlite3_int64)seriesCeil(r); |
| 8392 | } |
| 8393 | }else{ |
| 8394 | iMin = sqlite3_value_int64(argv[iArg++]); |
| 8395 | if( (idxNum & 0x0200)!=0 ){ |
| 8396 | if( iMin==LARGEST_INT64 ){ |
| @@ -8399,19 +8415,25 @@ | |
| 8399 | iMin++; |
| 8400 | } |
| 8401 | } |
| 8402 | } |
| 8403 | } |
| 8404 | if( idxNum & 0x3000 ){ /* value<X or value<=X */ |
| 8405 | if( sqlite3_value_numeric_type(argv[iArg])==SQLITE_FLOAT ){ |
| 8406 | double r = sqlite3_value_double(argv[iArg++]); |
| 8407 | if( r>(double)LARGEST_INT64 ){ |
| 8408 | iMax = LARGEST_INT64; |
| 8409 | }else if( (idxNum & 0x2000)!=0 && r==seriesFloor(r) ){ |
| 8410 | iMax = ((sqlite3_int64)r)-1; |
| 8411 | }else{ |
| 8412 | iMax = (sqlite3_int64)seriesFloor(r); |
| 8413 | } |
| 8414 | }else{ |
| 8415 | iMax = sqlite3_value_int64(argv[iArg++]); |
| 8416 | if( idxNum & 0x2000 ){ |
| 8417 | if( iMax==SMALLEST_INT64 ){ |
| @@ -10884,10 +10906,13 @@ | |
| 10884 | ** Return NULL if unable. |
| 10885 | ** |
| 10886 | ** The file or directory X is not required to exist. The answer is formed |
| 10887 | ** by calling system realpath() on the prefix of X that does exist and |
| 10888 | ** appending the tail of X that does not (yet) exist. |
| 10889 | */ |
| 10890 | static void realpathFunc( |
| 10891 | sqlite3_context *context, |
| 10892 | int argc, |
| 10893 | sqlite3_value **argv |
| @@ -10906,10 +10931,11 @@ | |
| 10906 | (void)argc; |
| 10907 | zPath = (const char*)sqlite3_value_text(argv[0]); |
| 10908 | if( zPath==0 ) return; |
| 10909 | if( zPath[0]==0 ) zPath = "."; |
| 10910 | zCopy = sqlite3_mprintf("%s",zPath); |
| 10911 | len = strlen(zCopy); |
| 10912 | while( len>1 && (zCopy[len-1]=='/' || (isWin && zCopy[len-1]=='\\')) ){ |
| 10913 | len--; |
| 10914 | } |
| 10915 | zCopy[len] = 0; |
| @@ -12699,10 +12725,11 @@ | |
| 12699 | |
| 12700 | for(p=pCsr->pFreeEntry; p; p=pNext){ |
| 12701 | pNext = p->pNext; |
| 12702 | zipfileEntryFree(p); |
| 12703 | } |
| 12704 | } |
| 12705 | |
| 12706 | /* |
| 12707 | ** Destructor for an ZipfileCsr. |
| 12708 | */ |
| @@ -13101,11 +13128,17 @@ | |
| 13101 | } |
| 13102 | } |
| 13103 | |
| 13104 | if( rc==SQLITE_OK ){ |
| 13105 | u32 *pt = &pNew->mUnixTime; |
| 13106 | pNew->cds.zFile = sqlite3_mprintf("%.*s", nFile, aRead); |
| 13107 | pNew->aExtra = (u8*)&pNew[1]; |
| 13108 | memcpy(pNew->aExtra, &aRead[nFile], nExtra); |
| 13109 | if( pNew->cds.zFile==0 ){ |
| 13110 | rc = SQLITE_NOMEM; |
| 13111 | }else if( 0==zipfileScanExtra(&aRead[nFile], pNew->cds.nExtra, pt) ){ |
| @@ -14205,14 +14238,14 @@ | |
| 14205 | ZipfileBuffer body; |
| 14206 | ZipfileBuffer cds; |
| 14207 | }; |
| 14208 | |
| 14209 | static int zipfileBufferGrow(ZipfileBuffer *pBuf, i64 nByte){ |
| 14210 | if( pBuf->n+nByte>pBuf->nAlloc ){ |
| 14211 | u8 *aNew; |
| 14212 | sqlite3_int64 nNew = pBuf->n ? pBuf->n*2 : 512; |
| 14213 | int nReq = pBuf->n + nByte; |
| 14214 | |
| 14215 | while( nNew<nReq ) nNew = nNew*2; |
| 14216 | aNew = sqlite3_realloc64(pBuf->a, nNew); |
| 14217 | if( aNew==0 ) return SQLITE_NOMEM; |
| 14218 | pBuf->a = aNew; |
| @@ -16310,11 +16343,11 @@ | |
| 16310 | p->pTable = pTab; |
| 16311 | |
| 16312 | /* The statement the vtab will pass to sqlite3_declare_vtab() */ |
| 16313 | zInner = idxAppendText(&rc, 0, "CREATE TABLE x("); |
| 16314 | for(i=0; i<pTab->nCol; i++){ |
| 16315 | zInner = idxAppendText(&rc, zInner, "%s%Q COLLATE %s", |
| 16316 | (i==0 ? "" : ", "), pTab->aCol[i].zName, pTab->aCol[i].zColl |
| 16317 | ); |
| 16318 | } |
| 16319 | zInner = idxAppendText(&rc, zInner, ")"); |
| 16320 | |
| @@ -16510,11 +16543,11 @@ | |
| 16510 | sqlite3_free(zCols); |
| 16511 | sqlite3_free(zOrder); |
| 16512 | return sqlite3_reset(pIndexXInfo); |
| 16513 | } |
| 16514 | zCols = idxAppendText(&rc, zCols, |
| 16515 | "%sx.%Q IS sqlite_expert_rem(%d, x.%Q) COLLATE %s", |
| 16516 | zComma, zName, nCol, zName, zColl |
| 16517 | ); |
| 16518 | zOrder = idxAppendText(&rc, zOrder, "%s%d", zComma, ++nCol); |
| 16519 | } |
| 16520 | sqlite3_reset(pIndexXInfo); |
| @@ -25163,11 +25196,11 @@ | |
| 25163 | /* |
| 25164 | ** Return true if either the SQLITE_NO_COLOR compile-time option is used |
| 25165 | ** or if the NO_COLOR environment variable exists |
| 25166 | */ |
| 25167 | static int shellNoColor(void){ |
| 25168 | #ifdef SQLITE_NO_COLOR |
| 25169 | return 1; |
| 25170 | #else |
| 25171 | return getenv("NO_COLOR")!=0; |
| 25172 | #endif |
| 25173 | } |
| @@ -31316,11 +31349,11 @@ | |
| 31316 | typedef struct ArCommand ArCommand; |
| 31317 | struct ArCommand { |
| 31318 | u8 eCmd; /* An AR_CMD_* value */ |
| 31319 | u8 bVerbose; /* True if --verbose */ |
| 31320 | u8 bZip; /* True if the archive is a ZIP */ |
| 31321 | u8 bDryRun; /* True if --dry-run */ |
| 31322 | u8 bAppend; /* True if --append */ |
| 31323 | u8 bGlob; /* True if --glob */ |
| 31324 | u8 fromCmdLine; /* Run from -A instead of .archive */ |
| 31325 | int nArg; /* Number of command arguments */ |
| 31326 | char *zSrcTable; /* "sqlar", "zipfile($file)" or "zip" */ |
| @@ -31378,10 +31411,11 @@ | |
| 31378 | #define AR_SWITCH_FILE 9 |
| 31379 | #define AR_SWITCH_DIRECTORY 10 |
| 31380 | #define AR_SWITCH_APPEND 11 |
| 31381 | #define AR_SWITCH_DRYRUN 12 |
| 31382 | #define AR_SWITCH_GLOB 13 |
| 31383 | |
| 31384 | static int arProcessSwitch(ArCommand *pAr, int eSwitch, const char *zArg){ |
| 31385 | switch( eSwitch ){ |
| 31386 | case AR_CMD_CREATE: |
| 31387 | case AR_CMD_EXTRACT: |
| @@ -31395,11 +31429,14 @@ | |
| 31395 | } |
| 31396 | pAr->eCmd = eSwitch; |
| 31397 | break; |
| 31398 | |
| 31399 | case AR_SWITCH_DRYRUN: |
| 31400 | pAr->bDryRun = 1; |
| 31401 | break; |
| 31402 | case AR_SWITCH_GLOB: |
| 31403 | pAr->bGlob = 1; |
| 31404 | break; |
| 31405 | case AR_SWITCH_VERBOSE: |
| @@ -31446,10 +31483,11 @@ | |
| 31446 | { "verbose", 'v', AR_SWITCH_VERBOSE, 0 }, |
| 31447 | { "file", 'f', AR_SWITCH_FILE, 1 }, |
| 31448 | { "append", 'a', AR_SWITCH_APPEND, 1 }, |
| 31449 | { "directory", 'C', AR_SWITCH_DIRECTORY, 1 }, |
| 31450 | { "dryrun", 'n', AR_SWITCH_DRYRUN, 0 }, |
| 31451 | { "glob", 'g', AR_SWITCH_GLOB, 0 }, |
| 31452 | }; |
| 31453 | int nSwitch = sizeof(aSwitch) / sizeof(struct ArSwitch); |
| 31454 | struct ArSwitch *pEnd = &aSwitch[nSwitch]; |
| 31455 | |
| @@ -31746,18 +31784,41 @@ | |
| 31746 | |
| 31747 | /* |
| 31748 | ** Implementation of .ar "eXtract" command. |
| 31749 | */ |
| 31750 | static int arExtractCommand(ArCommand *pAr){ |
| 31751 | const char *zSql1 = |
| 31752 | "WITH dest(dpath,dlen) AS (SELECT realpath($dir),length(realpath($dir)))\n" |
| 31753 | "SELECT ($dir || name),\n" |
| 31754 | " CASE WHEN $dryrun THEN 0\n" |
| 31755 | " ELSE writefile($dir||name, %s, mode, mtime) END\n" |
| 31756 | " FROM dest CROSS JOIN %s\n" |
| 31757 | " WHERE (%s)\n" |
| 31758 | " AND (data IS NULL OR $pass==0)\n" /* Dirs both passes */ |
| 31759 | " AND dpath=substr(realpath($dir||name),1,dlen)\n" /* No escapes */ |
| 31760 | " AND name NOT GLOB '*..[/\\]*'\n"; /* No /../ in paths */ |
| 31761 | |
| 31762 | const char *azExtraArg[] = { |
| 31763 | "sqlar_uncompress(data, sz)", |
| @@ -31784,39 +31845,48 @@ | |
| 31784 | } |
| 31785 | if( zDir==0 ) rc = SQLITE_NOMEM; |
| 31786 | } |
| 31787 | |
| 31788 | shellPreparePrintf(pAr->db, &rc, &pSql, zSql1, |
| 31789 | azExtraArg[pAr->bZip], pAr->zSrcTable, zWhere |
| 31790 | ); |
| 31791 | |
| 31792 | if( rc==SQLITE_OK ){ |
| 31793 | j = sqlite3_bind_parameter_index(pSql, "$dir"); |
| 31794 | sqlite3_bind_text(pSql, j, zDir, -1, SQLITE_STATIC); |
| 31795 | j = sqlite3_bind_parameter_index(pSql, "$dryrun"); |
| 31796 | sqlite3_bind_int(pSql, j, pAr->bDryRun); |
| 31797 | |
| 31798 | /* Run the SELECT statement twice |
| 31799 | ** (0) writefile() all files and directories |
| 31800 | ** (1) writefile() for directory again |
| 31801 | ** The second pass is so that the timestamps for extracted directories |
| 31802 | ** will be reset to the value in the archive, since populating them |
| 31803 | ** in the first pass will have changed the timestamp. */ |
| 31804 | for(i=0; i<2; i++){ |
| 31805 | j = sqlite3_bind_parameter_index(pSql, "$pass"); |
| 31806 | sqlite3_bind_int(pSql, j, i); |
| 31807 | if( pAr->bDryRun ){ |
| 31808 | cli_printf(pAr->out, "%s\n", sqlite3_sql(pSql)); |
| 31809 | if( pAr->bVerbose==0 ) break; |
| 31810 | } |
| 31811 | while( rc==SQLITE_OK && SQLITE_ROW==sqlite3_step(pSql) ){ |
| 31812 | if( i==0 && pAr->bVerbose ){ |
| 31813 | cli_printf(pAr->out, "%s\n", sqlite3_column_text(pSql, 0)); |
| 31814 | } |
| 31815 | } |
| 31816 | if( pAr->bDryRun ) break; |
| 31817 | shellReset(&rc, pSql); |
| 31818 | } |
| 31819 | shellFinalize(&rc, pSql); |
| 31820 | } |
| 31821 | |
| 31822 | sqlite3_free(zDir); |
| @@ -32327,11 +32397,11 @@ | |
| 32327 | ; |
| 32328 | static const char * const zCollectVar = "\ |
| 32329 | SELECT\ |
| 32330 | '('||x'0a'\ |
| 32331 | || group_concat(\ |
| 32332 | cname||' ANY',\ |
| 32333 | ','||iif((cpos-1)%4>0, ' ', x'0a'||' '))\ |
| 32334 | ||')' AS ColsSpec \ |
| 32335 | FROM (\ |
| 32336 | SELECT cpos, printf('\"%w\"',printf('%!.*s%s', nlen-chop,name,suff)) AS cname \ |
| 32337 | FROM ColNames ORDER BY cpos\ |
| @@ -37939,11 +38009,11 @@ | |
| 37939 | } |
| 37940 | |
| 37941 | /* |
| 37942 | ** The callback from atexit(). |
| 37943 | */ |
| 37944 | static void abnormalExit(void){ |
| 37945 | if( seenInterrupt ) eputz("Program interrupted.\n"); |
| 37946 | if( globalShellState ){ |
| 37947 | clearTempFile(globalShellState, 1, 1); |
| 37948 | } |
| 37949 | } |
| @@ -38498,12 +38568,14 @@ | |
| 38498 | }else if( cli_strcmp(z,"-nullvalue")==0 ){ |
| 38499 | modeSetStr(&data.mode.spec.zNull, |
| 38500 | cmdline_option_value(argc,argv,++i)); |
| 38501 | }else if( cli_strcmp(z,"-header")==0 ){ |
| 38502 | data.mode.spec.bTitles = QRF_Yes; |
| 38503 | }else if( cli_strcmp(z,"-noheader")==0 ){ |
| 38504 | data.mode.spec.bTitles = QRF_No; |
| 38505 | }else if( cli_strcmp(z,"-echo")==0 ){ |
| 38506 | data.mode.mFlags |= MFLG_ECHO; |
| 38507 | }else if( cli_strcmp(z,"-eqp")==0 ){ |
| 38508 | data.mode.autoEQP = AUTOEQP_on; |
| 38509 | }else if( cli_strcmp(z,"-eqpfull")==0 ){ |
| 38510 |
| --- extsrc/shell.c | |
| +++ extsrc/shell.c | |
| @@ -5424,11 +5424,11 @@ | |
| 5424 | } |
| 5425 | j = 0; |
| 5426 | }else{ |
| 5427 | i = 0; |
| 5428 | } |
| 5429 | if( len-i>0 ) (void)memcpy(&p->buffer[j], &data[i], len - i); |
| 5430 | } |
| 5431 | |
| 5432 | /* Compute a string using sqlite3_vsnprintf() and hash it */ |
| 5433 | static void hash_step_vformat( |
| 5434 | SHA1Context *p, /* Add content to this context */ |
| @@ -6071,32 +6071,41 @@ | |
| 6071 | } |
| 6072 | z[i] = 0; |
| 6073 | sqlite3_result_text(pCtx, z, i, sqlite3_free); |
| 6074 | } |
| 6075 | |
| 6076 | /* Forward declaration */ |
| 6077 | static void decimal_expand(Decimal *p, int nDigit, int nFrac); |
| 6078 | |
| 6079 | /* |
| 6080 | ** Round a decimal value to N significant digits. N must be positive. |
| 6081 | */ |
| 6082 | static void decimal_round(Decimal *p, int N){ |
| 6083 | int i; |
| 6084 | int nZero; /* Number of leading zeros */ |
| 6085 | if( N<1 ) return; |
| 6086 | if( p==0 ) return; |
| 6087 | if( p->nDigit<=N ) return; |
| 6088 | for(nZero=0; nZero<p->nDigit && p->a[nZero]==0; nZero++){} |
| 6089 | N += nZero; |
| 6090 | if( p->nDigit<=N ) return; |
| 6091 | if( p->a[N]>=5 ){ |
| 6092 | /* If all leading digits are 9, increase the number of digits |
| 6093 | ** by adding a new 0 to the front */ |
| 6094 | for(i=0; i<N && p->a[i]==9; i++){} |
| 6095 | if( i==N ){ |
| 6096 | decimal_expand(p, p->nDigit+1, 0); |
| 6097 | if( p->oom ) return; |
| 6098 | } |
| 6099 | |
| 6100 | /* Do the rounding */ |
| 6101 | p->a[N-1]++; |
| 6102 | for(i=N-1; i>0 && p->a[i]>9; i--){ |
| 6103 | p->a[i] = 0; |
| 6104 | p->a[i-1]++; |
| 6105 | } |
| 6106 | assert( p->a[0]<=9 ); |
| 6107 | } |
| 6108 | memset(&p->a[N], 0, p->nDigit - N); |
| 6109 | } |
| 6110 | |
| 6111 | /* |
| @@ -8378,19 +8387,26 @@ | |
| 8387 | } |
| 8388 | }else{ |
| 8389 | iMin = iMax = sqlite3_value_int64(argv[iArg++]); |
| 8390 | } |
| 8391 | }else{ |
| 8392 | if( idxNum & 0x0300 ){ /* value>X (0x200) or value>=X (0x100) */ |
| 8393 | if( sqlite3_value_numeric_type(argv[iArg])==SQLITE_FLOAT ){ |
| 8394 | double r = sqlite3_value_double(argv[iArg++]); |
| 8395 | if( r<=(double)SMALLEST_INT64 ){ |
| 8396 | iMin = SMALLEST_INT64; |
| 8397 | }else if( r>(double)LARGEST_INT64 ){ |
| 8398 | goto series_no_rows; |
| 8399 | }else{ |
| 8400 | iMin = (sqlite3_int64)seriesCeil(r); |
| 8401 | if( iMin<0 && r>0.0 ){ |
| 8402 | iMin = LARGEST_INT64; |
| 8403 | } |
| 8404 | if( (idxNum & 0x0200)!=0 && r==seriesCeil(r) ){ |
| 8405 | if( iMin==LARGEST_INT64 ) goto series_no_rows; |
| 8406 | iMin++; |
| 8407 | } |
| 8408 | } |
| 8409 | }else{ |
| 8410 | iMin = sqlite3_value_int64(argv[iArg++]); |
| 8411 | if( (idxNum & 0x0200)!=0 ){ |
| 8412 | if( iMin==LARGEST_INT64 ){ |
| @@ -8399,19 +8415,25 @@ | |
| 8415 | iMin++; |
| 8416 | } |
| 8417 | } |
| 8418 | } |
| 8419 | } |
| 8420 | if( idxNum & 0x3000 ){ /* value<X (0x2000) or value<=X (0x1000) */ |
| 8421 | if( sqlite3_value_numeric_type(argv[iArg])==SQLITE_FLOAT ){ |
| 8422 | double r = sqlite3_value_double(argv[iArg++]); |
| 8423 | if( r>=(double)LARGEST_INT64 ){ |
| 8424 | iMax = LARGEST_INT64; |
| 8425 | }else if( r<=(double)SMALLEST_INT64 ){ |
| 8426 | goto series_no_rows; |
| 8427 | }else{ |
| 8428 | iMax = (sqlite3_int64)seriesFloor(r); |
| 8429 | if( iMax<0 && r>0.0 ){ |
| 8430 | iMax = LARGEST_INT64; |
| 8431 | }else if( (idxNum & 0x2000)!=0 && r==seriesFloor(r) ){ |
| 8432 | if( iMax==SMALLEST_INT64 ) goto series_no_rows; |
| 8433 | iMax--; |
| 8434 | } |
| 8435 | } |
| 8436 | }else{ |
| 8437 | iMax = sqlite3_value_int64(argv[iArg++]); |
| 8438 | if( idxNum & 0x2000 ){ |
| 8439 | if( iMax==SMALLEST_INT64 ){ |
| @@ -10884,10 +10906,13 @@ | |
| 10906 | ** Return NULL if unable. |
| 10907 | ** |
| 10908 | ** The file or directory X is not required to exist. The answer is formed |
| 10909 | ** by calling system realpath() on the prefix of X that does exist and |
| 10910 | ** appending the tail of X that does not (yet) exist. |
| 10911 | ** |
| 10912 | ** FIXME: This routine sometimes returns NULL rather than raising |
| 10913 | ** an SQLITE_NOMEM error if an OOM is encountered. |
| 10914 | */ |
| 10915 | static void realpathFunc( |
| 10916 | sqlite3_context *context, |
| 10917 | int argc, |
| 10918 | sqlite3_value **argv |
| @@ -10906,10 +10931,11 @@ | |
| 10931 | (void)argc; |
| 10932 | zPath = (const char*)sqlite3_value_text(argv[0]); |
| 10933 | if( zPath==0 ) return; |
| 10934 | if( zPath[0]==0 ) zPath = "."; |
| 10935 | zCopy = sqlite3_mprintf("%s",zPath); |
| 10936 | if( zCopy==0 ) return; |
| 10937 | len = strlen(zCopy); |
| 10938 | while( len>1 && (zCopy[len-1]=='/' || (isWin && zCopy[len-1]=='\\')) ){ |
| 10939 | len--; |
| 10940 | } |
| 10941 | zCopy[len] = 0; |
| @@ -12699,10 +12725,11 @@ | |
| 12725 | |
| 12726 | for(p=pCsr->pFreeEntry; p; p=pNext){ |
| 12727 | pNext = p->pNext; |
| 12728 | zipfileEntryFree(p); |
| 12729 | } |
| 12730 | pCsr->pFreeEntry = 0; |
| 12731 | } |
| 12732 | |
| 12733 | /* |
| 12734 | ** Destructor for an ZipfileCsr. |
| 12735 | */ |
| @@ -13101,11 +13128,17 @@ | |
| 13128 | } |
| 13129 | } |
| 13130 | |
| 13131 | if( rc==SQLITE_OK ){ |
| 13132 | u32 *pt = &pNew->mUnixTime; |
| 13133 | /* aRead[0..nFile-1] might contain embedded \000 characters |
| 13134 | ** See Bug 2026-05-31T11:43:05Z */ |
| 13135 | pNew->cds.zFile = sqlite3_malloc64(nFile+1); |
| 13136 | if( pNew->cds.zFile!=0 ){ |
| 13137 | memcpy(pNew->cds.zFile, aRead, nFile); |
| 13138 | pNew->cds.zFile[nFile] = 0; |
| 13139 | } |
| 13140 | pNew->aExtra = (u8*)&pNew[1]; |
| 13141 | memcpy(pNew->aExtra, &aRead[nFile], nExtra); |
| 13142 | if( pNew->cds.zFile==0 ){ |
| 13143 | rc = SQLITE_NOMEM; |
| 13144 | }else if( 0==zipfileScanExtra(&aRead[nFile], pNew->cds.nExtra, pt) ){ |
| @@ -14205,14 +14238,14 @@ | |
| 14238 | ZipfileBuffer body; |
| 14239 | ZipfileBuffer cds; |
| 14240 | }; |
| 14241 | |
| 14242 | static int zipfileBufferGrow(ZipfileBuffer *pBuf, i64 nByte){ |
| 14243 | if( (pBuf->nAlloc-pBuf->n)<nByte ){ |
| 14244 | u8 *aNew; |
| 14245 | i64 nNew = pBuf->n ? (i64)pBuf->n*2 : 512; |
| 14246 | i64 nReq = pBuf->n + nByte; |
| 14247 | |
| 14248 | while( nNew<nReq ) nNew = nNew*2; |
| 14249 | aNew = sqlite3_realloc64(pBuf->a, nNew); |
| 14250 | if( aNew==0 ) return SQLITE_NOMEM; |
| 14251 | pBuf->a = aNew; |
| @@ -16310,11 +16343,11 @@ | |
| 16343 | p->pTable = pTab; |
| 16344 | |
| 16345 | /* The statement the vtab will pass to sqlite3_declare_vtab() */ |
| 16346 | zInner = idxAppendText(&rc, 0, "CREATE TABLE x("); |
| 16347 | for(i=0; i<pTab->nCol; i++){ |
| 16348 | zInner = idxAppendText(&rc, zInner, "%s%Q COLLATE %Q", |
| 16349 | (i==0 ? "" : ", "), pTab->aCol[i].zName, pTab->aCol[i].zColl |
| 16350 | ); |
| 16351 | } |
| 16352 | zInner = idxAppendText(&rc, zInner, ")"); |
| 16353 | |
| @@ -16510,11 +16543,11 @@ | |
| 16543 | sqlite3_free(zCols); |
| 16544 | sqlite3_free(zOrder); |
| 16545 | return sqlite3_reset(pIndexXInfo); |
| 16546 | } |
| 16547 | zCols = idxAppendText(&rc, zCols, |
| 16548 | "%sx.%Q IS sqlite_expert_rem(%d, x.%Q) COLLATE %Q", |
| 16549 | zComma, zName, nCol, zName, zColl |
| 16550 | ); |
| 16551 | zOrder = idxAppendText(&rc, zOrder, "%s%d", zComma, ++nCol); |
| 16552 | } |
| 16553 | sqlite3_reset(pIndexXInfo); |
| @@ -25163,11 +25196,11 @@ | |
| 25196 | /* |
| 25197 | ** Return true if either the SQLITE_NO_COLOR compile-time option is used |
| 25198 | ** or if the NO_COLOR environment variable exists |
| 25199 | */ |
| 25200 | static int shellNoColor(void){ |
| 25201 | #if defined(SQLITE_NO_COLOR) || defined(SQLITE_SHELL_FIDDLE) |
| 25202 | return 1; |
| 25203 | #else |
| 25204 | return getenv("NO_COLOR")!=0; |
| 25205 | #endif |
| 25206 | } |
| @@ -31316,11 +31349,11 @@ | |
| 31349 | typedef struct ArCommand ArCommand; |
| 31350 | struct ArCommand { |
| 31351 | u8 eCmd; /* An AR_CMD_* value */ |
| 31352 | u8 bVerbose; /* True if --verbose */ |
| 31353 | u8 bZip; /* True if the archive is a ZIP */ |
| 31354 | u8 bDryRun; /* 1 for --dry-run, 2 for --debug */ |
| 31355 | u8 bAppend; /* True if --append */ |
| 31356 | u8 bGlob; /* True if --glob */ |
| 31357 | u8 fromCmdLine; /* Run from -A instead of .archive */ |
| 31358 | int nArg; /* Number of command arguments */ |
| 31359 | char *zSrcTable; /* "sqlar", "zipfile($file)" or "zip" */ |
| @@ -31378,10 +31411,11 @@ | |
| 31411 | #define AR_SWITCH_FILE 9 |
| 31412 | #define AR_SWITCH_DIRECTORY 10 |
| 31413 | #define AR_SWITCH_APPEND 11 |
| 31414 | #define AR_SWITCH_DRYRUN 12 |
| 31415 | #define AR_SWITCH_GLOB 13 |
| 31416 | #define AR_SWITCH_DEBUG 14 |
| 31417 | |
| 31418 | static int arProcessSwitch(ArCommand *pAr, int eSwitch, const char *zArg){ |
| 31419 | switch( eSwitch ){ |
| 31420 | case AR_CMD_CREATE: |
| 31421 | case AR_CMD_EXTRACT: |
| @@ -31395,11 +31429,14 @@ | |
| 31429 | } |
| 31430 | pAr->eCmd = eSwitch; |
| 31431 | break; |
| 31432 | |
| 31433 | case AR_SWITCH_DRYRUN: |
| 31434 | if( pAr->bDryRun<2 ) pAr->bDryRun = 1; |
| 31435 | break; |
| 31436 | case AR_SWITCH_DEBUG: |
| 31437 | pAr->bDryRun = 2; |
| 31438 | break; |
| 31439 | case AR_SWITCH_GLOB: |
| 31440 | pAr->bGlob = 1; |
| 31441 | break; |
| 31442 | case AR_SWITCH_VERBOSE: |
| @@ -31446,10 +31483,11 @@ | |
| 31483 | { "verbose", 'v', AR_SWITCH_VERBOSE, 0 }, |
| 31484 | { "file", 'f', AR_SWITCH_FILE, 1 }, |
| 31485 | { "append", 'a', AR_SWITCH_APPEND, 1 }, |
| 31486 | { "directory", 'C', AR_SWITCH_DIRECTORY, 1 }, |
| 31487 | { "dryrun", 'n', AR_SWITCH_DRYRUN, 0 }, |
| 31488 | { "debug", 0, AR_SWITCH_DEBUG, 0 }, |
| 31489 | { "glob", 'g', AR_SWITCH_GLOB, 0 }, |
| 31490 | }; |
| 31491 | int nSwitch = sizeof(aSwitch) / sizeof(struct ArSwitch); |
| 31492 | struct ArSwitch *pEnd = &aSwitch[nSwitch]; |
| 31493 | |
| @@ -31746,18 +31784,41 @@ | |
| 31784 | |
| 31785 | /* |
| 31786 | ** Implementation of .ar "eXtract" command. |
| 31787 | */ |
| 31788 | static int arExtractCommand(ArCommand *pAr){ |
| 31789 | /* The zSql1[] string is a template for the query that does the |
| 31790 | ** extraction. Notes: |
| 31791 | ** |
| 31792 | ** * $dir is the directory into which the archive is to be extracted |
| 31793 | ** * $pass is the integer pass number: 0, 1, or 2 |
| 31794 | ** * The dest CTE is created so that realpath($dir) only needs |
| 31795 | ** to be called once. |
| 31796 | */ |
| 31797 | const char *zSql1 = |
| 31798 | "WITH dest(dpath,dlen) AS (\n" |
| 31799 | #ifdef _WIN32 |
| 31800 | " SELECT realpath($dir) || '\\',\n" |
| 31801 | #else |
| 31802 | " SELECT realpath($dir) || '/',\n" |
| 31803 | #endif |
| 31804 | " 1+length(realpath($dir))\n" |
| 31805 | ")\n" |
| 31806 | "SELECT\n" |
| 31807 | " ($dir || name),\n" |
| 31808 | " CASE $dryrun\n" /* vv--- azExtraArg */ |
| 31809 | " WHEN 0 THEN writefile($dir||name, %s, mode, mtime)\n" |
| 31810 | " WHEN 1 THEN 0\n" |
| 31811 | " ELSE shell_putsnl(format('writefile(%%Q,%%s,%%0o,%%d)'," |
| 31812 | "$dir||name,quote(%s),mode,mtime)) IS NULL\n" |
| 31813 | " END\n" /* ^^--- azExtraArg */ |
| 31814 | " FROM dest CROSS JOIN %s\n" |
| 31815 | " WHERE (%s)\n" /* ^^-- pAr->zSrcTable */ |
| 31816 | /* ^^--- zWhere */ |
| 31817 | " AND (CASE $pass WHEN 0 THEN (mode&0xf000)<>0xa000\n" |
| 31818 | " WHEN 1 THEN (mode&0xf000)=0xa000\n" |
| 31819 | " ELSE data IS NULL END)\n" |
| 31820 | " AND dpath=substr(realpath($dir||name),1,dlen)\n" /* No escapes */ |
| 31821 | " AND name NOT GLOB '*..[/\\]*'\n"; /* No /../ in paths */ |
| 31822 | |
| 31823 | const char *azExtraArg[] = { |
| 31824 | "sqlar_uncompress(data, sz)", |
| @@ -31784,39 +31845,48 @@ | |
| 31845 | } |
| 31846 | if( zDir==0 ) rc = SQLITE_NOMEM; |
| 31847 | } |
| 31848 | |
| 31849 | shellPreparePrintf(pAr->db, &rc, &pSql, zSql1, |
| 31850 | azExtraArg[pAr->bZip], |
| 31851 | azExtraArg[pAr->bZip], |
| 31852 | pAr->zSrcTable, |
| 31853 | zWhere |
| 31854 | ); |
| 31855 | |
| 31856 | if( rc==SQLITE_OK ){ |
| 31857 | j = sqlite3_bind_parameter_index(pSql, "$dir"); |
| 31858 | sqlite3_bind_text(pSql, j, zDir, -1, SQLITE_STATIC); |
| 31859 | j = sqlite3_bind_parameter_index(pSql, "$dryrun"); |
| 31860 | sqlite3_bind_int(pSql, j, pAr->bDryRun); |
| 31861 | |
| 31862 | /* Run the SELECT statement twice |
| 31863 | ** (0) writefile() files and directories |
| 31864 | ** (1) writefile() symlinks |
| 31865 | ** (2) writefile() for directory again |
| 31866 | ** The third pass is so that the timestamps for extracted directories |
| 31867 | ** will be reset to the value in the archive, since populating them |
| 31868 | ** in the first pass will have changed the timestamp. */ |
| 31869 | for(i=0; i<3; i++){ |
| 31870 | if( pAr->bDryRun>=2 ){ |
| 31871 | cli_printf(pAr->out, "*** BEGIN PASS %d ***\n", i+1); |
| 31872 | } |
| 31873 | j = sqlite3_bind_parameter_index(pSql, "$pass"); |
| 31874 | sqlite3_bind_int(pSql, j, i); |
| 31875 | if( pAr->bDryRun && i==0 ){ |
| 31876 | cli_printf(pAr->out, "%s\n", sqlite3_sql(pSql)); |
| 31877 | } |
| 31878 | while( rc==SQLITE_OK && SQLITE_ROW==sqlite3_step(pSql) ){ |
| 31879 | if( i==0 && pAr->bVerbose ){ |
| 31880 | cli_printf(pAr->out, "%s\n", sqlite3_column_text(pSql, 0)); |
| 31881 | } |
| 31882 | } |
| 31883 | if( pAr->bDryRun==1 ) break; |
| 31884 | shellReset(&rc, pSql); |
| 31885 | if( pAr->bDryRun>=2 ){ |
| 31886 | cli_printf(pAr->out, "*** END PASS %d ***\n", i+1); |
| 31887 | } |
| 31888 | } |
| 31889 | shellFinalize(&rc, pSql); |
| 31890 | } |
| 31891 | |
| 31892 | sqlite3_free(zDir); |
| @@ -32327,11 +32397,11 @@ | |
| 32397 | ; |
| 32398 | static const char * const zCollectVar = "\ |
| 32399 | SELECT\ |
| 32400 | '('||x'0a'\ |
| 32401 | || group_concat(\ |
| 32402 | cname,\ |
| 32403 | ','||iif((cpos-1)%4>0, ' ', x'0a'||' '))\ |
| 32404 | ||')' AS ColsSpec \ |
| 32405 | FROM (\ |
| 32406 | SELECT cpos, printf('\"%w\"',printf('%!.*s%s', nlen-chop,name,suff)) AS cname \ |
| 32407 | FROM ColNames ORDER BY cpos\ |
| @@ -37939,11 +38009,11 @@ | |
| 38009 | } |
| 38010 | |
| 38011 | /* |
| 38012 | ** The callback from atexit(). |
| 38013 | */ |
| 38014 | static void SQLITE_CDECL abnormalExit(void){ |
| 38015 | if( seenInterrupt ) eputz("Program interrupted.\n"); |
| 38016 | if( globalShellState ){ |
| 38017 | clearTempFile(globalShellState, 1, 1); |
| 38018 | } |
| 38019 | } |
| @@ -38498,12 +38568,14 @@ | |
| 38568 | }else if( cli_strcmp(z,"-nullvalue")==0 ){ |
| 38569 | modeSetStr(&data.mode.spec.zNull, |
| 38570 | cmdline_option_value(argc,argv,++i)); |
| 38571 | }else if( cli_strcmp(z,"-header")==0 ){ |
| 38572 | data.mode.spec.bTitles = QRF_Yes; |
| 38573 | data.mode.mFlags |= MFLG_HDR; |
| 38574 | }else if( cli_strcmp(z,"-noheader")==0 ){ |
| 38575 | data.mode.spec.bTitles = QRF_No; |
| 38576 | data.mode.mFlags |= MFLG_HDR; |
| 38577 | }else if( cli_strcmp(z,"-echo")==0 ){ |
| 38578 | data.mode.mFlags |= MFLG_ECHO; |
| 38579 | }else if( cli_strcmp(z,"-eqp")==0 ){ |
| 38580 | data.mode.autoEQP = AUTOEQP_on; |
| 38581 | }else if( cli_strcmp(z,"-eqpfull")==0 ){ |
| 38582 |
+448
-221
| --- 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 | -** 2c605bfb1562d7a3609ad6ffd7446def12f1 with changes in files: | |
| 21 | +** 3f3fb9b638f59ad982beafb7c117f24ddd3d with changes in files: | |
| 22 | 22 | ** |
| 23 | 23 | ** |
| 24 | 24 | */ |
| 25 | 25 | #ifndef SQLITE_AMALGAMATION |
| 26 | 26 | #define SQLITE_CORE 1 |
| @@ -467,14 +467,14 @@ | ||
| 467 | 467 | ** [sqlite3_libversion_number()], [sqlite3_sourceid()], |
| 468 | 468 | ** [sqlite_version()] and [sqlite_source_id()]. |
| 469 | 469 | */ |
| 470 | 470 | #define SQLITE_VERSION "3.54.0" |
| 471 | 471 | #define SQLITE_VERSION_NUMBER 3054000 |
| 472 | -#define SQLITE_SOURCE_ID "2026-05-30 13:23:25 2c605bfb1562d7a3609ad6ffd7446def12f1ac7084e41b9c6723e998c156501d" | |
| 472 | +#define SQLITE_SOURCE_ID "2026-06-16 13:43:08 3f3fb9b638f59ad982beafb7c117f24ddd3da612e62c862510805fa672ffae06" | |
| 473 | 473 | #define SQLITE_SCM_BRANCH "trunk" |
| 474 | 474 | #define SQLITE_SCM_TAGS "" |
| 475 | -#define SQLITE_SCM_DATETIME "2026-05-30T13:23:25.636Z" | |
| 475 | +#define SQLITE_SCM_DATETIME "2026-06-16T13:43:08.110Z" | |
| 476 | 476 | |
| 477 | 477 | /* |
| 478 | 478 | ** CAPI3REF: Run-Time Library Version Numbers |
| 479 | 479 | ** KEYWORDS: sqlite3_version sqlite3_sourceid |
| 480 | 480 | ** |
| @@ -4696,11 +4696,12 @@ | ||
| 4696 | 4696 | ** <dd>The maximum number of columns in a table definition or in the |
| 4697 | 4697 | ** result set of a [SELECT] or the maximum number of columns in an index |
| 4698 | 4698 | ** or in an ORDER BY or GROUP BY clause.</dd>)^ |
| 4699 | 4699 | ** |
| 4700 | 4700 | ** [[SQLITE_LIMIT_EXPR_DEPTH]] ^(<dt>SQLITE_LIMIT_EXPR_DEPTH</dt> |
| 4701 | -** <dd>The maximum depth of the parse tree on any expression.</dd>)^ | |
| 4701 | +** <dd>The maximum depth of the parse tree on any expression and | |
| 4702 | +** the maximum nesting depth for subqueries and VIEWs</dd>)^ | |
| 4702 | 4703 | ** |
| 4703 | 4704 | ** [[SQLITE_LIMIT_PARSER_DEPTH]] ^(<dt>SQLITE_LIMIT_PARSER_DEPTH</dt> |
| 4704 | 4705 | ** <dd>The maximum depth of the LALR(1) parser stack used to analyze |
| 4705 | 4706 | ** input SQL statements.</dd>)^ |
| 4706 | 4707 | ** |
| @@ -4727,11 +4728,12 @@ | ||
| 4727 | 4728 | ** [[SQLITE_LIMIT_VARIABLE_NUMBER]] |
| 4728 | 4729 | ** ^(<dt>SQLITE_LIMIT_VARIABLE_NUMBER</dt> |
| 4729 | 4730 | ** <dd>The maximum index number of any [parameter] in an SQL statement.)^ |
| 4730 | 4731 | ** |
| 4731 | 4732 | ** [[SQLITE_LIMIT_TRIGGER_DEPTH]] ^(<dt>SQLITE_LIMIT_TRIGGER_DEPTH</dt> |
| 4732 | -** <dd>The maximum depth of recursion for triggers.</dd>)^ | |
| 4733 | +** <dd>The maximum depth of recursion for triggers, and the maximum | |
| 4734 | +** nesting depth for separate triggers.</dd>)^ | |
| 4733 | 4735 | ** |
| 4734 | 4736 | ** [[SQLITE_LIMIT_WORKER_THREADS]] ^(<dt>SQLITE_LIMIT_WORKER_THREADS</dt> |
| 4735 | 4737 | ** <dd>The maximum number of auxiliary worker threads that a single |
| 4736 | 4738 | ** [prepared statement] may start.</dd>)^ |
| 4737 | 4739 | ** </dl> |
| @@ -11533,12 +11535,23 @@ | ||
| 11533 | 11535 | ** reopen S as an in-memory database based on the serialization |
| 11534 | 11536 | ** contained in P. If S is a NULL pointer, the main database is |
| 11535 | 11537 | ** used. The serialized database P is N bytes in size. M is the size |
| 11536 | 11538 | ** of the buffer P, which might be larger than N. If M is larger than |
| 11537 | 11539 | ** N, and the SQLITE_DESERIALIZE_READONLY bit is not set in F, then |
| 11538 | -** SQLite is permitted to add content to the in-memory database as | |
| 11539 | -** long as the total size does not exceed M bytes. | |
| 11540 | +** SQLite is permitted to add content to the in-memory database, in | |
| 11541 | +** page-sized chunks, as long as the total size does not exceed M bytes. | |
| 11542 | +** | |
| 11543 | +** The parameter M must be greater than or equal to N. Ideally, M | |
| 11544 | +** should have a value which is N+(512×K)+20 where K determines how | |
| 11545 | +** must extra space is available to hold new content as the database | |
| 11546 | +** grows. K can be 0 if the database is read-only. | |
| 11547 | +** | |
| 11548 | +** If the database content in P is malformed in a malicious way then | |
| 11549 | +** it is possible that SQLite might try to read a few more than N bytes | |
| 11550 | +** from P. If the veracity of the database content P is uncertain, | |
| 11551 | +** then applications are advised to allocate about 20 extra bytes on | |
| 11552 | +** the end of the P buffer to avoid a memory error. | |
| 11540 | 11553 | ** |
| 11541 | 11554 | ** If the SQLITE_DESERIALIZE_FREEONCLOSE bit is set in F, then SQLite will |
| 11542 | 11555 | ** invoke sqlite3_free() on the serialization buffer when the database |
| 11543 | 11556 | ** connection closes. If the SQLITE_DESERIALIZE_RESIZEABLE bit is set, then |
| 11544 | 11557 | ** SQLite will try to increase the buffer size using sqlite3_realloc64() |
| @@ -15809,10 +15822,17 @@ | ||
| 15809 | 15822 | */ |
| 15810 | 15823 | #ifndef offsetof |
| 15811 | 15824 | # define offsetof(ST,M) ((size_t)((char*)&((ST*)0)->M - (char*)0)) |
| 15812 | 15825 | #endif |
| 15813 | 15826 | |
| 15827 | +/* | |
| 15828 | +** sizeof64() is like sizeof(), but always returns a 64-bit value, even | |
| 15829 | +** on 32-bit builds. This can help to avoid overflow by ensuring 64-bit | |
| 15830 | +** arithmetic is used consistently in both 32-bit and 64-bit builds. | |
| 15831 | +*/ | |
| 15832 | +#define sizeof64(X) ((sqlite3_int64)sizeof(X)) | |
| 15833 | + | |
| 15814 | 15834 | /* |
| 15815 | 15835 | ** Work around C99 "flex-array" syntax for pre-C99 compilers, so as |
| 15816 | 15836 | ** to avoid complaints from -fsanitize=strict-bounds. |
| 15817 | 15837 | */ |
| 15818 | 15838 | #if defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L) |
| @@ -20928,10 +20948,11 @@ | ||
| 20928 | 20948 | int nTab; /* Number of previously allocated VDBE cursors */ |
| 20929 | 20949 | int nMem; /* Number of memory cells used so far */ |
| 20930 | 20950 | int szOpAlloc; /* Bytes of memory space allocated for Vdbe.aOp[] */ |
| 20931 | 20951 | int iSelfTab; /* Table associated with an index on expr, or negative |
| 20932 | 20952 | ** of the base register during check-constraint eval */ |
| 20953 | + int nNestSel; /* Number of nested SELECT statements and/or VIEWs */ | |
| 20933 | 20954 | int nLabel; /* The *negative* of the number of labels used */ |
| 20934 | 20955 | int nLabelAlloc; /* Number of slots in aLabel */ |
| 20935 | 20956 | int *aLabel; /* Space to hold the labels */ |
| 20936 | 20957 | ExprList *pConstExpr;/* Constant expressions */ |
| 20937 | 20958 | IndexedExpr *pIdxEpr;/* List of expressions used by active indexes */ |
| @@ -33389,12 +33410,12 @@ | ||
| 33389 | 33410 | if( flag_alternateform ){ |
| 33390 | 33411 | /* For %#q, do unistr()-style backslash escapes for |
| 33391 | 33412 | ** all control characters, and for backslash itself. |
| 33392 | 33413 | ** For %#Q, do the same but only if there is at least |
| 33393 | 33414 | ** one control character. */ |
| 33394 | - u32 nBack = 0; | |
| 33395 | - u32 nCtrl = 0; | |
| 33415 | + i64 nBack = 0; | |
| 33416 | + i64 nCtrl = 0; | |
| 33396 | 33417 | for(k=0; k<i; k++){ |
| 33397 | 33418 | if( escarg[k]=='\\' ){ |
| 33398 | 33419 | nBack++; |
| 33399 | 33420 | }else if( ((u8*)escarg)[k]<=0x1f ){ |
| 33400 | 33421 | nCtrl++; |
| @@ -39255,10 +39276,26 @@ | ||
| 39255 | 39276 | ****************************************************************************** |
| 39256 | 39277 | ** |
| 39257 | 39278 | ** This file contains an experimental VFS layer that operates on a |
| 39258 | 39279 | ** Key/Value storage engine where both keys and values must be pure |
| 39259 | 39280 | ** text. |
| 39281 | +** | |
| 39282 | +** DEBUG AND TEST | |
| 39283 | +** | |
| 39284 | +** For testing on Unix, compile using: | |
| 39285 | +** | |
| 39286 | +** make clean sqlite3d CFLAGS='-DSQLITE_OS_KV_OPTIONAL' | |
| 39287 | +** | |
| 39288 | +** Then start up a shell using something like: | |
| 39289 | +** | |
| 39290 | +** ./sqlite3d 'file:dbname?vfs=kvvfs' | |
| 39291 | +** | |
| 39292 | +** Each K/V entry is stored in a separate file in the working | |
| 39293 | +** directory that has a name like "kvvfs-dbname-*". Due to limitations | |
| 39294 | +** on the key size, the name of the database must be very short - just | |
| 39295 | +** a few characters. If the database name is too long, the VFS will | |
| 39296 | +** malfunction and you will get SQLITE_CORRUPT errors. | |
| 39260 | 39297 | */ |
| 39261 | 39298 | /* #include <sqliteInt.h> */ |
| 39262 | 39299 | #if SQLITE_OS_KV || (SQLITE_OS_UNIX && defined(SQLITE_OS_KV_OPTIONAL)) |
| 39263 | 39300 | |
| 39264 | 39301 | /***************************************************************************** |
| @@ -39707,16 +39744,18 @@ | ||
| 39707 | 39744 | } |
| 39708 | 39745 | if( j+n>nOut ) return -1; |
| 39709 | 39746 | memset(&aOut[j], 0, n); |
| 39710 | 39747 | j += n; |
| 39711 | 39748 | if( c==0 || mult==1 ) break; /* progress stalled if mult==1 */ |
| 39712 | - }else{ | |
| 39749 | + }else if( j<nOut ){ | |
| 39713 | 39750 | aOut[j] = c<<4; |
| 39714 | 39751 | c = kvvfsHexValue[aIn[++i]]; |
| 39715 | 39752 | if( c<0 ) return -1 /* hex bytes are always in pairs */; |
| 39716 | 39753 | aOut[j++] += c; |
| 39717 | 39754 | i++; |
| 39755 | + }else{ | |
| 39756 | + return -1; | |
| 39718 | 39757 | } |
| 39719 | 39758 | } |
| 39720 | 39759 | return j; |
| 39721 | 39760 | } |
| 39722 | 39761 | |
| @@ -40138,10 +40177,22 @@ | ||
| 40138 | 40177 | }else{ |
| 40139 | 40178 | pFile->isJournal = 0; |
| 40140 | 40179 | pFile->base.pMethods = &kvvfs_db_io_methods; |
| 40141 | 40180 | } |
| 40142 | 40181 | if( !pFile->zClass ){ |
| 40182 | +#ifdef SQLITE_WASM | |
| 40183 | + if( strlen(zName) >= (KVRECORD_KEY_SZ | |
| 40184 | + - 6 /* "kvvfs-" */ | |
| 40185 | + - 11 /* "-##########" */) ){ | |
| 40186 | + return SQLITE_CANTOPEN; | |
| 40187 | + } | |
| 40188 | +#else | |
| 40189 | + if( 0!=strcmp(zName, "local") && 0!=strcmp(zName, "session") ){ | |
| 40190 | + /* Historical naming restriction which journaling depends on. */ | |
| 40191 | + return SQLITE_CANTOPEN; | |
| 40192 | + } | |
| 40193 | +#endif | |
| 40143 | 40194 | pFile->zClass = zName; |
| 40144 | 40195 | } |
| 40145 | 40196 | pFile->aData = sqlite3_malloc64(SQLITE_KVOS_SZ); |
| 40146 | 40197 | if( pFile->aData==0 ){ |
| 40147 | 40198 | return SQLITE_NOMEM; |
| @@ -52170,15 +52221,33 @@ | ||
| 52170 | 52221 | # define sqlite3_win_test_unc_locking 0 |
| 52171 | 52222 | #endif |
| 52172 | 52223 | |
| 52173 | 52224 | /* |
| 52174 | 52225 | ** Return true if the string passed as the only argument is likely |
| 52175 | -** to be a UNC path. In other words, if it starts with "\\". | |
| 52226 | +** to be a UNC path. Return false if note. | |
| 52227 | +** | |
| 52228 | +** Return true if: | |
| 52229 | +** | |
| 52230 | +** (1) The name begins with "\\" | |
| 52231 | +** (2) But does not begin with "\\?\C:\" where C can be any alphabetic | |
| 52232 | +** character. | |
| 52233 | +** | |
| 52234 | +** For testing, also return true in all cases if the global variable | |
| 52235 | +** sqlite3_win_test_unc_locking is true. | |
| 52176 | 52236 | */ |
| 52177 | 52237 | static int winIsUNCPath(const char *zFile){ |
| 52178 | 52238 | if( zFile[0]=='\\' && zFile[1]=='\\' ){ |
| 52179 | - return 1; | |
| 52239 | + if( zFile[2]=='?' | |
| 52240 | + && zFile[3]=='\\' | |
| 52241 | + && sqlite3Isalpha(zFile[4]) | |
| 52242 | + && zFile[5]==':' | |
| 52243 | + && winIsDirSep(zFile[6]) | |
| 52244 | + ){ | |
| 52245 | + return sqlite3_win_test_unc_locking; | |
| 52246 | + }else{ | |
| 52247 | + return 1; | |
| 52248 | + } | |
| 52180 | 52249 | } |
| 52181 | 52250 | return sqlite3_win_test_unc_locking; |
| 52182 | 52251 | } |
| 52183 | 52252 | |
| 52184 | 52253 | /* |
| @@ -56926,26 +56995,28 @@ | ||
| 56926 | 56995 | szBulk = -1024 * (i64)pcache1.nInitPage; |
| 56927 | 56996 | } |
| 56928 | 56997 | if( szBulk > pCache->szAlloc*(i64)pCache->nMax ){ |
| 56929 | 56998 | szBulk = pCache->szAlloc*(i64)pCache->nMax; |
| 56930 | 56999 | } |
| 56931 | - zBulk = pCache->pBulk = sqlite3Malloc( szBulk ); | |
| 56932 | - sqlite3EndBenignMalloc(); | |
| 56933 | - if( zBulk ){ | |
| 56934 | - int nBulk = sqlite3MallocSize(zBulk)/pCache->szAlloc; | |
| 56935 | - do{ | |
| 56936 | - PgHdr1 *pX = (PgHdr1*)&zBulk[pCache->szPage]; | |
| 56937 | - pX->page.pBuf = zBulk; | |
| 56938 | - pX->page.pExtra = (u8*)pX + ROUND8(sizeof(*pX)); | |
| 56939 | - assert( EIGHT_BYTE_ALIGNMENT( pX->page.pExtra ) ); | |
| 56940 | - pX->isBulkLocal = 1; | |
| 56941 | - pX->isAnchor = 0; | |
| 56942 | - pX->pNext = pCache->pFree; | |
| 56943 | - pX->pLruPrev = 0; /* Initializing this saves a valgrind error */ | |
| 56944 | - pCache->pFree = pX; | |
| 56945 | - zBulk += pCache->szAlloc; | |
| 56946 | - }while( --nBulk ); | |
| 57000 | + if( szBulk>=pCache->szAlloc ){ | |
| 57001 | + zBulk = pCache->pBulk = sqlite3Malloc( szBulk ); | |
| 57002 | + sqlite3EndBenignMalloc(); | |
| 57003 | + if( zBulk ){ | |
| 57004 | + int nBulk = sqlite3MallocSize(zBulk)/pCache->szAlloc; | |
| 57005 | + do{ | |
| 57006 | + PgHdr1 *pX = (PgHdr1*)&zBulk[pCache->szPage]; | |
| 57007 | + pX->page.pBuf = zBulk; | |
| 57008 | + pX->page.pExtra = (u8*)pX + ROUND8(sizeof(*pX)); | |
| 57009 | + assert( EIGHT_BYTE_ALIGNMENT( pX->page.pExtra ) ); | |
| 57010 | + pX->isBulkLocal = 1; | |
| 57011 | + pX->isAnchor = 0; | |
| 57012 | + pX->pNext = pCache->pFree; | |
| 57013 | + pX->pLruPrev = 0; /* Initializing this saves a valgrind error */ | |
| 57014 | + pCache->pFree = pX; | |
| 57015 | + zBulk += pCache->szAlloc; | |
| 57016 | + }while( --nBulk ); | |
| 57017 | + } | |
| 56947 | 57018 | } |
| 56948 | 57019 | return pCache->pFree!=0; |
| 56949 | 57020 | } |
| 56950 | 57021 | |
| 56951 | 57022 | /* |
| @@ -59839,73 +59910,84 @@ | ||
| 59839 | 59910 | #define pager_set_pagehash(X) |
| 59840 | 59911 | #define CHECK_PAGE(x) |
| 59841 | 59912 | #endif /* SQLITE_CHECK_PAGES */ |
| 59842 | 59913 | |
| 59843 | 59914 | /* |
| 59844 | -** When this is called the journal file for pager pPager must be open. | |
| 59845 | -** This function attempts to read a super-journal file name from the | |
| 59846 | -** end of the file and, if successful, copies it into memory supplied | |
| 59847 | -** by the caller. See comments above writeSuperJournal() for the format | |
| 59848 | -** used to store a super-journal file name at the end of a journal file. | |
| 59849 | -** | |
| 59850 | -** zSuper must point to a buffer of at least nSuper bytes allocated by | |
| 59851 | -** the caller. This should be sqlite3_vfs.mxPathname+1 (to ensure there is | |
| 59852 | -** enough space to write the super-journal name). If the super-journal | |
| 59853 | -** name in the journal is longer than nSuper bytes (including a | |
| 59854 | -** nul-terminator), then this is handled as if no super-journal name | |
| 59855 | -** were present in the journal. | |
| 59856 | -** | |
| 59857 | -** If a super-journal file name is present at the end of the journal | |
| 59858 | -** file, then it is copied into the buffer pointed to by zSuper. A | |
| 59859 | -** nul-terminator byte is appended to the buffer following the | |
| 59860 | -** super-journal file name. | |
| 59861 | -** | |
| 59862 | -** If it is determined that no super-journal file name is present | |
| 59863 | -** zSuper[0] is set to 0 and SQLITE_OK returned. | |
| 59864 | -** | |
| 59865 | -** If an error occurs while reading from the journal file, an SQLite | |
| 59866 | -** error code is returned. | |
| 59867 | -*/ | |
| 59868 | -static int readSuperJournal(sqlite3_file *pJrnl, char *zSuper, u64 nSuper){ | |
| 59915 | +** Free a buffer allocated by the readSuperJournal() function. | |
| 59916 | +*/ | |
| 59917 | +static void freeSuperJournal(char *zSuper){ | |
| 59918 | + if( zSuper ){ | |
| 59919 | + sqlite3_free(&zSuper[-4]); | |
| 59920 | + } | |
| 59921 | +} | |
| 59922 | + | |
| 59923 | +/* | |
| 59924 | +** Parameter pJrnl is a file-handle open on a journal file. This function | |
| 59925 | +** attempts to read a super-journal file name from the end of the journal | |
| 59926 | +** file. If successful, it sets output parameter (*pzSuper) to point to a | |
| 59927 | +** buffer containing the super-journal name as a nul-terminated string. | |
| 59928 | +** The caller is responsible for freeing the buffer using freeSuperJournal(). | |
| 59929 | +** | |
| 59930 | +** Refer to comments above writeSuperJournal() for the format used to store | |
| 59931 | +** a super-journal file name at the end of a journal file. | |
| 59932 | +** | |
| 59933 | +** Parameter nSuper is passed the maximum allowable size of the super journal | |
| 59934 | +** name in bytes. If the super-journal name in the journal is longer than | |
| 59935 | +** nSuper bytes (including a nul-terminator), then this is handled as if no | |
| 59936 | +** super-journal name were present in the journal. | |
| 59937 | +** | |
| 59938 | +** If there is no super-journal name at the end of pJrnl, (*pzSuper) is | |
| 59939 | +** set to 0 and SQLITE_OK is returned. Or, if an error occurs while reading | |
| 59940 | +** the super-journal name, an SQLite error code is returned and (*pzSuper) | |
| 59941 | +** is set to 0. | |
| 59942 | +*/ | |
| 59943 | +static int readSuperJournal(sqlite3_file *pJrnl, u64 nSuper, char **pzSuper){ | |
| 59869 | 59944 | int rc; /* Return code */ |
| 59870 | 59945 | u32 len; /* Length in bytes of super-journal name */ |
| 59871 | 59946 | i64 szJ; /* Total size in bytes of journal file pJrnl */ |
| 59872 | 59947 | u32 cksum; /* MJ checksum value read from journal */ |
| 59873 | - u32 u; /* Unsigned loop counter */ | |
| 59874 | 59948 | unsigned char aMagic[8]; /* A buffer to hold the magic header */ |
| 59875 | - zSuper[0] = '\0'; | |
| 59949 | + char *zOut = 0; | |
| 59876 | 59950 | |
| 59951 | + *pzSuper = 0; | |
| 59877 | 59952 | if( SQLITE_OK!=(rc = sqlite3OsFileSize(pJrnl, &szJ)) |
| 59878 | 59953 | || szJ<16 |
| 59879 | 59954 | || SQLITE_OK!=(rc = read32bits(pJrnl, szJ-16, &len)) |
| 59880 | 59955 | || len>=nSuper |
| 59881 | 59956 | || len>szJ-16 |
| 59882 | 59957 | || len==0 |
| 59883 | 59958 | || SQLITE_OK!=(rc = read32bits(pJrnl, szJ-12, &cksum)) |
| 59884 | 59959 | || SQLITE_OK!=(rc = sqlite3OsRead(pJrnl, aMagic, 8, szJ-8)) |
| 59885 | 59960 | || memcmp(aMagic, aJournalMagic, 8) |
| 59886 | - || SQLITE_OK!=(rc = sqlite3OsRead(pJrnl, zSuper, len, szJ-16-len)) | |
| 59887 | 59961 | ){ |
| 59888 | 59962 | return rc; |
| 59889 | 59963 | } |
| 59890 | 59964 | |
| 59891 | - /* See if the checksum matches the super-journal name */ | |
| 59892 | - for(u=0; u<len; u++){ | |
| 59893 | - cksum -= zSuper[u]; | |
| 59894 | - } | |
| 59895 | - if( cksum ){ | |
| 59896 | - /* If the checksum doesn't add up, then one or more of the disk sectors | |
| 59897 | - ** containing the super-journal filename is corrupted. This means | |
| 59898 | - ** definitely roll back, so just return SQLITE_OK and report a (nul) | |
| 59899 | - ** super-journal filename. | |
| 59900 | - */ | |
| 59901 | - len = 0; | |
| 59902 | - } | |
| 59903 | - zSuper[len] = '\0'; | |
| 59904 | - zSuper[len+1] = '\0'; | |
| 59905 | - | |
| 59906 | - return SQLITE_OK; | |
| 59965 | + zOut = (char*)sqlite3MallocZero(4 + len + 2); | |
| 59966 | + if( !zOut ){ | |
| 59967 | + rc = SQLITE_NOMEM_BKPT; | |
| 59968 | + }else{ | |
| 59969 | + zOut = &zOut[4]; | |
| 59970 | + if( SQLITE_OK==(rc = sqlite3OsRead(pJrnl, zOut, len, szJ-16-len)) ){ | |
| 59971 | + u32 u; /* Unsigned loop counter */ | |
| 59972 | + /* See if the checksum matches the super-journal name */ | |
| 59973 | + for(u=0; u<len; u++){ | |
| 59974 | + cksum -= zOut[u]; | |
| 59975 | + } | |
| 59976 | + } | |
| 59977 | + if( rc!=SQLITE_OK || cksum ){ | |
| 59978 | + /* If the checksum doesn't add up, then one or more of the disk sectors | |
| 59979 | + ** containing the super-journal filename is corrupted. This means | |
| 59980 | + ** definitely roll back, so just return SQLITE_OK and report a (nul) | |
| 59981 | + ** super-journal filename. */ | |
| 59982 | + freeSuperJournal(zOut); | |
| 59983 | + zOut = 0; | |
| 59984 | + } | |
| 59985 | + } | |
| 59986 | + | |
| 59987 | + *pzSuper = zOut; | |
| 59988 | + return rc; | |
| 59907 | 59989 | } |
| 59908 | 59990 | |
| 59909 | 59991 | /* |
| 59910 | 59992 | ** Return the offset of the sector boundary at or immediately |
| 59911 | 59993 | ** following the value in pPager->journalOff, assuming a sector |
| @@ -61106,13 +61188,11 @@ | ||
| 61106 | 61188 | sqlite3_file *pSuper; /* Malloc'd super-journal file descriptor */ |
| 61107 | 61189 | sqlite3_file *pJournal; /* Malloc'd child-journal file descriptor */ |
| 61108 | 61190 | char *zSuperJournal = 0; /* Contents of super-journal file */ |
| 61109 | 61191 | i64 nSuperJournal; /* Size of super-journal file */ |
| 61110 | 61192 | char *zJournal; /* Pointer to one journal within MJ file */ |
| 61111 | - char *zSuperPtr; /* Space to hold super-journal filename */ | |
| 61112 | 61193 | char *zFree = 0; /* Free this buffer */ |
| 61113 | - i64 nSuperPtr; /* Amount of space allocated to zSuperPtr[] */ | |
| 61114 | 61194 | |
| 61115 | 61195 | /* Allocate space for both the pJournal and pSuper file descriptors. |
| 61116 | 61196 | ** If successful, open the super-journal file for reading. |
| 61117 | 61197 | */ |
| 61118 | 61198 | pSuper = (sqlite3_file *)sqlite3MallocZero(2 * (i64)pVfs->szOsFile); |
| @@ -61131,22 +61211,20 @@ | ||
| 61131 | 61211 | ** sufficient space (in zSuperPtr) to hold the names of super-journal |
| 61132 | 61212 | ** files extracted from regular rollback-journals. |
| 61133 | 61213 | */ |
| 61134 | 61214 | rc = sqlite3OsFileSize(pSuper, &nSuperJournal); |
| 61135 | 61215 | if( rc!=SQLITE_OK ) goto delsuper_out; |
| 61136 | - nSuperPtr = 1 + (i64)pVfs->mxPathname; | |
| 61137 | - assert( nSuperJournal>=0 && nSuperPtr>0 ); | |
| 61138 | - zFree = sqlite3Malloc(4 + nSuperJournal + 2 + nSuperPtr + 2); | |
| 61216 | + assert( nSuperJournal>=0 ); | |
| 61217 | + zFree = sqlite3Malloc(4 + nSuperJournal + 2); | |
| 61139 | 61218 | if( !zFree ){ |
| 61140 | 61219 | rc = SQLITE_NOMEM_BKPT; |
| 61141 | 61220 | goto delsuper_out; |
| 61142 | 61221 | }else{ |
| 61143 | 61222 | assert( nSuperJournal<=0x7fffffff ); |
| 61144 | 61223 | } |
| 61145 | 61224 | zFree[0] = zFree[1] = zFree[2] = zFree[3] = 0; |
| 61146 | 61225 | zSuperJournal = &zFree[4]; |
| 61147 | - zSuperPtr = &zSuperJournal[nSuperJournal+2]; | |
| 61148 | 61226 | rc = sqlite3OsRead(pSuper, zSuperJournal, (int)nSuperJournal, 0); |
| 61149 | 61227 | if( rc!=SQLITE_OK ) goto delsuper_out; |
| 61150 | 61228 | zSuperJournal[nSuperJournal] = 0; |
| 61151 | 61229 | zSuperJournal[nSuperJournal+1] = 0; |
| 61152 | 61230 | |
| @@ -61156,10 +61234,12 @@ | ||
| 61156 | 61234 | rc = sqlite3OsAccess(pVfs, zJournal, SQLITE_ACCESS_EXISTS, &exists); |
| 61157 | 61235 | if( rc!=SQLITE_OK ){ |
| 61158 | 61236 | goto delsuper_out; |
| 61159 | 61237 | } |
| 61160 | 61238 | if( exists ){ |
| 61239 | + char *zSuperPtr = 0; | |
| 61240 | + | |
| 61161 | 61241 | /* One of the journals pointed to by the super-journal exists. |
| 61162 | 61242 | ** Open it and check if it points at the super-journal. If |
| 61163 | 61243 | ** so, return without deleting the super-journal file. |
| 61164 | 61244 | ** NB: zJournal is really a MAIN_JOURNAL. But call it a |
| 61165 | 61245 | ** SUPER_JOURNAL here so that the VFS will not send the zJournal |
| @@ -61170,17 +61250,19 @@ | ||
| 61170 | 61250 | rc = sqlite3OsOpen(pVfs, zJournal, pJournal, flags, 0); |
| 61171 | 61251 | if( rc!=SQLITE_OK ){ |
| 61172 | 61252 | goto delsuper_out; |
| 61173 | 61253 | } |
| 61174 | 61254 | |
| 61175 | - rc = readSuperJournal(pJournal, zSuperPtr, nSuperPtr); | |
| 61255 | + rc = readSuperJournal(pJournal, 1+(u64)pVfs->mxPathname, &zSuperPtr); | |
| 61176 | 61256 | sqlite3OsClose(pJournal); |
| 61177 | 61257 | if( rc!=SQLITE_OK ){ |
| 61258 | + assert( zSuperPtr==0 ); | |
| 61178 | 61259 | goto delsuper_out; |
| 61179 | 61260 | } |
| 61180 | 61261 | |
| 61181 | - c = zSuperPtr[0]!=0 && strcmp(zSuperPtr, zSuper)==0; | |
| 61262 | + c = zSuperPtr!=0 && strcmp(zSuperPtr, zSuper)==0; | |
| 61263 | + freeSuperJournal(zSuperPtr); | |
| 61182 | 61264 | if( c ){ |
| 61183 | 61265 | /* We have a match. Do not delete the super-journal file. */ |
| 61184 | 61266 | goto delsuper_out; |
| 61185 | 61267 | } |
| 61186 | 61268 | } |
| @@ -61391,23 +61473,15 @@ | ||
| 61391 | 61473 | |
| 61392 | 61474 | /* Read the super-journal name from the journal, if it is present. |
| 61393 | 61475 | ** If a super-journal file name is specified, but the file is not |
| 61394 | 61476 | ** present on disk, then the journal is not hot and does not need to be |
| 61395 | 61477 | ** played back. |
| 61396 | - ** | |
| 61397 | - ** TODO: Technically the following is an error because it assumes that | |
| 61398 | - ** buffer Pager.pTmpSpace is (mxPathname+1) bytes or larger. i.e. that | |
| 61399 | - ** ((pPager->pageSize+8) >= pPager->pVfs->mxPathname+1). Using os_unix.c, | |
| 61400 | - ** mxPathname is 512, which is the same as the minimum allowable value | |
| 61401 | - ** for pageSize, and so this assumption holds. But it might not for some | |
| 61402 | - ** custom VFS. */ | |
| 61403 | - zSuper = pPager->pTmpSpace; | |
| 61404 | - rc = readSuperJournal(pPager->jfd, zSuper, 1+(i64)pPager->pVfs->mxPathname); | |
| 61405 | - if( rc==SQLITE_OK && zSuper[0] ){ | |
| 61478 | + */ | |
| 61479 | + rc = readSuperJournal(pPager->jfd, 1+(i64)pPager->pVfs->mxPathname, &zSuper); | |
| 61480 | + if( rc==SQLITE_OK && zSuper ){ | |
| 61406 | 61481 | rc = sqlite3OsAccess(pVfs, zSuper, SQLITE_ACCESS_EXISTS, &res); |
| 61407 | 61482 | } |
| 61408 | - zSuper = 0; | |
| 61409 | 61483 | if( rc!=SQLITE_OK || !res ){ |
| 61410 | 61484 | goto end_playback; |
| 61411 | 61485 | } |
| 61412 | 61486 | pPager->journalOff = 0; |
| 61413 | 61487 | needPagerReset = isHot; |
| @@ -61532,34 +61606,24 @@ | ||
| 61532 | 61606 | ** problems for other processes at some point in the future. So, just |
| 61533 | 61607 | ** in case this has happened, clear the changeCountDone flag now. |
| 61534 | 61608 | */ |
| 61535 | 61609 | pPager->changeCountDone = pPager->tempFile; |
| 61536 | 61610 | |
| 61537 | - if( rc==SQLITE_OK ){ | |
| 61538 | - /* Leave 4 bytes of space before the super-journal filename in memory. | |
| 61539 | - ** This is because it may end up being passed to sqlite3OsOpen(), in | |
| 61540 | - ** which case it requires 4 0x00 bytes in memory immediately before | |
| 61541 | - ** the filename. */ | |
| 61542 | - zSuper = &pPager->pTmpSpace[4]; | |
| 61543 | - rc = readSuperJournal(pPager->jfd, zSuper, 1+(i64)pPager->pVfs->mxPathname); | |
| 61544 | - testcase( rc!=SQLITE_OK ); | |
| 61545 | - } | |
| 61546 | 61611 | if( rc==SQLITE_OK |
| 61547 | 61612 | && (pPager->eState>=PAGER_WRITER_DBMOD || pPager->eState==PAGER_OPEN) |
| 61548 | 61613 | ){ |
| 61549 | 61614 | rc = sqlite3PagerSync(pPager, 0); |
| 61550 | 61615 | } |
| 61551 | 61616 | if( rc==SQLITE_OK ){ |
| 61552 | - rc = pager_end_transaction(pPager, zSuper[0]!='\0', 0); | |
| 61617 | + rc = pager_end_transaction(pPager, zSuper!=0, 0); | |
| 61553 | 61618 | testcase( rc!=SQLITE_OK ); |
| 61554 | 61619 | } |
| 61555 | - if( rc==SQLITE_OK && zSuper[0] && res ){ | |
| 61620 | + if( rc==SQLITE_OK && zSuper && res ){ | |
| 61556 | 61621 | /* If there was a super-journal and this routine will return success, |
| 61557 | 61622 | ** see if it is possible to delete the super-journal. |
| 61558 | 61623 | */ |
| 61559 | - assert( zSuper==&pPager->pTmpSpace[4] ); | |
| 61560 | - memset(pPager->pTmpSpace, 0, 4); | |
| 61624 | + assert( memcmp(&zSuper[-4], "\0\0\0\0", 4)==0 ); | |
| 61561 | 61625 | rc = pager_delsuper(pPager, zSuper); |
| 61562 | 61626 | testcase( rc!=SQLITE_OK ); |
| 61563 | 61627 | } |
| 61564 | 61628 | if( isHot && nPlayback ){ |
| 61565 | 61629 | sqlite3_log(SQLITE_NOTICE_RECOVER_ROLLBACK, "recovered %d pages from %s", |
| @@ -61568,10 +61632,11 @@ | ||
| 61568 | 61632 | |
| 61569 | 61633 | /* The Pager.sectorSize variable may have been updated while rolling |
| 61570 | 61634 | ** back a journal created by a process with a different sector size |
| 61571 | 61635 | ** value. Reset it to the correct value for this process. |
| 61572 | 61636 | */ |
| 61637 | + freeSuperJournal(zSuper); | |
| 61573 | 61638 | setSectorSize(pPager); |
| 61574 | 61639 | return rc; |
| 61575 | 61640 | } |
| 61576 | 61641 | |
| 61577 | 61642 | |
| @@ -67426,10 +67491,16 @@ | ||
| 67426 | 67491 | */ |
| 67427 | 67492 | pgno = sqlite3Get4byte(&aFrame[0]); |
| 67428 | 67493 | if( pgno==0 ){ |
| 67429 | 67494 | return 0; |
| 67430 | 67495 | } |
| 67496 | + | |
| 67497 | + /* Need a valid page size | |
| 67498 | + */ | |
| 67499 | + if( !pWal->szPage ){ | |
| 67500 | + return 0; | |
| 67501 | + } | |
| 67431 | 67502 | |
| 67432 | 67503 | /* A frame is only valid if a checksum of the WAL header, |
| 67433 | 67504 | ** all prior frames, the first 16 bytes of this frame-header, |
| 67434 | 67505 | ** and the frame-data matches the checksum in the last 8 |
| 67435 | 67506 | ** bytes of this frame-header. |
| @@ -69281,11 +69352,11 @@ | ||
| 69281 | 69352 | goto begin_unreliable_shm_out; |
| 69282 | 69353 | } |
| 69283 | 69354 | |
| 69284 | 69355 | /* Allocate a buffer to read frames into */ |
| 69285 | 69356 | assert( (pWal->szPage & (pWal->szPage-1))==0 ); |
| 69286 | - assert( pWal->szPage>=512 && pWal->szPage<=65536 ); | |
| 69357 | + assert( (pWal->szPage>=512 && pWal->szPage<=65536) || pWal->szPage==0 ); | |
| 69287 | 69358 | szFrame = pWal->szPage + WAL_FRAME_HDRSIZE; |
| 69288 | 69359 | aFrame = (u8 *)sqlite3_malloc64(szFrame); |
| 69289 | 69360 | if( aFrame==0 ){ |
| 69290 | 69361 | rc = SQLITE_NOMEM_BKPT; |
| 69291 | 69362 | goto begin_unreliable_shm_out; |
| @@ -83086,10 +83157,15 @@ | ||
| 83086 | 83157 | if( pc+info.nSize>usableSize ){ |
| 83087 | 83158 | checkAppendMsg(pCheck, "Extends off end of page"); |
| 83088 | 83159 | doCoverageCheck = 0; |
| 83089 | 83160 | continue; |
| 83090 | 83161 | } |
| 83162 | + if( info.nPayload && info.pPayload[0]<2 ){ | |
| 83163 | + checkAppendMsg(pCheck, "Bad cell header size"); | |
| 83164 | + doCoverageCheck = 0; | |
| 83165 | + continue; | |
| 83166 | + } | |
| 83091 | 83167 | |
| 83092 | 83168 | /* Check for integer primary key out of range */ |
| 83093 | 83169 | if( pPage->intKey ){ |
| 83094 | 83170 | if( keyCanBeEqual ? (info.nKey > maxKey) : (info.nKey >= maxKey) ){ |
| 83095 | 83171 | checkAppendMsg(pCheck, "Rowid %lld out of order", info.nKey); |
| @@ -84596,12 +84672,12 @@ | ||
| 84596 | 84672 | /* Work-around for GCC bug or bugs: |
| 84597 | 84673 | ** https://gcc.gnu.org/bugzilla/show_bug.cgi?id=96270 |
| 84598 | 84674 | ** https://gcc.gnu.org/bugzilla/show_bug.cgi?id=114659 |
| 84599 | 84675 | ** The problem appears to be fixed in GCC 15 */ |
| 84600 | 84676 | i64 x; |
| 84601 | - assert( (MEM_Str&~p->flags)*4==sizeof(x) ); | |
| 84602 | - memcpy(&x, (char*)&p->u, (MEM_Str&~p->flags)*4); | |
| 84677 | + assert( (sqlite3Config.bSmallMalloc!=0xee)*8==sizeof(x) ); | |
| 84678 | + memcpy(&x, (char*)&p->u.i, (sqlite3Config.bSmallMalloc!=0xee)*8); | |
| 84603 | 84679 | p->n = sqlite3Int64ToText(x, zBuf); |
| 84604 | 84680 | #else |
| 84605 | 84681 | p->n = sqlite3Int64ToText(p->u.i, zBuf); |
| 84606 | 84682 | #endif |
| 84607 | 84683 | if( p->flags & MEM_IntReal ){ |
| @@ -95448,11 +95524,11 @@ | ||
| 95448 | 95524 | QueryPerformanceCounter(&tm); |
| 95449 | 95525 | return (sqlite3_uint64)tm.QuadPart; |
| 95450 | 95526 | } |
| 95451 | 95527 | |
| 95452 | 95528 | #elif !defined(__STRICT_ANSI__) && defined(__GNUC__) && \ |
| 95453 | - (defined(i386) || defined(__i386__) || defined(_M_IX86)) | |
| 95529 | + (defined(i586) || defined(__i586__) || defined(_M_IX86)) | |
| 95454 | 95530 | |
| 95455 | 95531 | __inline__ sqlite_uint64 sqlite3Hwtime(void){ |
| 95456 | 95532 | unsigned int lo, hi; |
| 95457 | 95533 | __asm__ __volatile__ ("rdtsc" : "=a" (lo), "=d" (hi)); |
| 95458 | 95534 | return (sqlite_uint64)hi << 32 | lo; |
| @@ -103037,11 +103113,11 @@ | ||
| 103037 | 103113 | if( pFrame ) break; |
| 103038 | 103114 | } |
| 103039 | 103115 | |
| 103040 | 103116 | if( p->nFrame>=db->aLimit[SQLITE_LIMIT_TRIGGER_DEPTH] ){ |
| 103041 | 103117 | rc = SQLITE_ERROR; |
| 103042 | - sqlite3VdbeError(p, "too many levels of trigger recursion"); | |
| 103118 | + sqlite3VdbeError(p, "triggers nested too deep"); | |
| 103043 | 103119 | goto abort_due_to_error; |
| 103044 | 103120 | } |
| 103045 | 103121 | |
| 103046 | 103122 | /* Register pRt is used to store the memory required to save the state |
| 103047 | 103123 | ** of the current program, and the memory required at runtime to execute |
| @@ -110164,11 +110240,11 @@ | ||
| 110164 | 110240 | /* |
| 110165 | 110241 | ** cnt==0 means there was not match. |
| 110166 | 110242 | ** cnt>1 means there were two or more matches. |
| 110167 | 110243 | ** |
| 110168 | 110244 | ** cnt==0 is always an error. cnt>1 is often an error, but might |
| 110169 | - ** be multiple matches for a NATURAL LEFT JOIN or a LEFT JOIN USING. | |
| 110245 | + ** be multiple matches for a NATURAL OUTER JOIN or a OUTER JOIN USING. | |
| 110170 | 110246 | */ |
| 110171 | 110247 | assert( pFJMatch==0 || cnt>0 ); |
| 110172 | 110248 | assert( !ExprHasProperty(pExpr, EP_xIsSelect|EP_IntValue) ); |
| 110173 | 110249 | if( cnt!=1 ){ |
| 110174 | 110250 | const char *zErr; |
| @@ -110247,12 +110323,21 @@ | ||
| 110247 | 110323 | pExpr->op = eNewExprOp; |
| 110248 | 110324 | lookupname_end: |
| 110249 | 110325 | if( cnt==1 ){ |
| 110250 | 110326 | assert( pNC!=0 ); |
| 110251 | 110327 | #ifndef SQLITE_OMIT_AUTHORIZATION |
| 110252 | - if( db->xAuth && (pExpr->op==TK_COLUMN || pExpr->op==TK_TRIGGER) ){ | |
| 110253 | - sqlite3AuthRead(pParse, pExpr, pSchema, pNC->pSrcList); | |
| 110328 | + if( db->xAuth ){ | |
| 110329 | + if( pFJMatch ){ | |
| 110330 | + assert( pExpr->op==TK_FUNCTION ); | |
| 110331 | + assert( sqlite3_stricmp(pExpr->u.zToken,"coalesce")==0 ); | |
| 110332 | + assert( pExpr->x.pList==pFJMatch ); | |
| 110333 | + assert( pFJMatch->nExpr>0 ); | |
| 110334 | + pExpr = pFJMatch->a[0].pExpr; | |
| 110335 | + } | |
| 110336 | + if( pExpr->op==TK_COLUMN || pExpr->op==TK_TRIGGER ){ | |
| 110337 | + sqlite3AuthRead(pParse, pExpr, pSchema, pNC->pSrcList); | |
| 110338 | + } | |
| 110254 | 110339 | } |
| 110255 | 110340 | #endif |
| 110256 | 110341 | /* Increment the nRef value on all name contexts from TopNC up to |
| 110257 | 110342 | ** the point where the name matched. */ |
| 110258 | 110343 | for(;;){ |
| @@ -115930,10 +116015,11 @@ | ||
| 115930 | 116015 | if( i!=nVector ){ |
| 115931 | 116016 | /* Need to reorder the LHS fields according to aiMap */ |
| 115932 | 116017 | int rLhsOrig = rLhs; |
| 115933 | 116018 | rLhs = sqlite3GetTempRange(pParse, nVector); |
| 115934 | 116019 | for(i=0; i<nVector; i++){ |
| 116020 | + testcase( aiMap[i]!=i ); | |
| 115935 | 116021 | sqlite3VdbeAddOp3(v, OP_Copy, rLhsOrig+i, rLhs+aiMap[i], 0); |
| 115936 | 116022 | } |
| 115937 | 116023 | sqlite3ReleaseTempReg(pParse, rLhsOrig); |
| 115938 | 116024 | } |
| 115939 | 116025 | } |
| @@ -115950,11 +116036,12 @@ | ||
| 115950 | 116036 | } |
| 115951 | 116037 | for(i=0; i<nVector; i++){ |
| 115952 | 116038 | Expr *p = sqlite3VectorFieldSubexpr(pExpr->pLeft, i); |
| 115953 | 116039 | if( pParse->nErr ) goto sqlite3ExprCodeIN_oom_error; |
| 115954 | 116040 | if( sqlite3ExprCanBeNull(p) ){ |
| 115955 | - sqlite3VdbeAddOp2(v, OP_IsNull, rLhs+i, destStep2); | |
| 116041 | + testcase( aiMap[i]!=i ); | |
| 116042 | + sqlite3VdbeAddOp2(v, OP_IsNull, rLhs+aiMap[i], destStep2); | |
| 115956 | 116043 | VdbeCoverage(v); |
| 115957 | 116044 | } |
| 115958 | 116045 | } |
| 115959 | 116046 | |
| 115960 | 116047 | /* Step 3. The LHS is now known to be non-NULL. Do the binary search |
| @@ -116024,13 +116111,23 @@ | ||
| 116024 | 116111 | for(i=0; i<nVector; i++){ |
| 116025 | 116112 | Expr *p; |
| 116026 | 116113 | CollSeq *pColl; |
| 116027 | 116114 | int r3 = sqlite3GetTempReg(pParse); |
| 116028 | 116115 | p = sqlite3VectorFieldSubexpr(pLeft, i); |
| 116029 | - pColl = sqlite3ExprCollSeq(pParse, p); | |
| 116030 | - sqlite3VdbeAddOp3(v, OP_Column, iTab, i, r3); | |
| 116031 | - sqlite3VdbeAddOp4(v, OP_Ne, rLhs+i, destNotNull, r3, | |
| 116116 | + if( ExprUseXSelect(pExpr) ){ | |
| 116117 | + Expr *pRhs = pExpr->x.pSelect->pEList->a[i].pExpr; | |
| 116118 | + pColl = sqlite3BinaryCompareCollSeq(pParse, p, pRhs); | |
| 116119 | + }else{ | |
| 116120 | + /* If the RHS of the IN(...) expression are scalar expressions, do | |
| 116121 | + ** not consider their collation sequences. The documentation says | |
| 116122 | + ** "The collating sequence used for expressions of the form "x IN (y, z, | |
| 116123 | + ** ...)" is the collating sequence of x.". */ | |
| 116124 | + pColl = sqlite3ExprCollSeq(pParse, p); | |
| 116125 | + } | |
| 116126 | + testcase( aiMap[i]!=i ); | |
| 116127 | + sqlite3VdbeAddOp3(v, OP_Column, iTab, aiMap[i], r3); | |
| 116128 | + sqlite3VdbeAddOp4(v, OP_Ne, rLhs+aiMap[i], destNotNull, r3, | |
| 116032 | 116129 | (void*)pColl, P4_COLLSEQ); |
| 116033 | 116130 | VdbeCoverage(v); |
| 116034 | 116131 | sqlite3ReleaseTempReg(pParse, r3); |
| 116035 | 116132 | } |
| 116036 | 116133 | sqlite3VdbeAddOp2(v, OP_Goto, 0, destIfNull); |
| @@ -124365,21 +124462,21 @@ | ||
| 124365 | 124462 | }else{ |
| 124366 | 124463 | nIdxCol = pIdx->nColumn; |
| 124367 | 124464 | } |
| 124368 | 124465 | pIdx->nSampleCol = nIdxCol; |
| 124369 | 124466 | pIdx->mxSample = nSample; |
| 124370 | - nByte = ROUND8(sizeof(IndexSample) * nSample); | |
| 124371 | - nByte += sizeof(tRowcnt) * nIdxCol * 3 * nSample; | |
| 124372 | - nByte += nIdxCol * sizeof(tRowcnt); /* Space for Index.aAvgEq[] */ | |
| 124467 | + nByte = ROUND8(sizeof64(IndexSample) * nSample); | |
| 124468 | + nByte += sizeof64(tRowcnt) * nIdxCol * 3 * nSample; | |
| 124469 | + nByte += nIdxCol * sizeof64(tRowcnt); /* Space for Index.aAvgEq[] */ | |
| 124373 | 124470 | |
| 124374 | 124471 | pIdx->aSample = sqlite3DbMallocZero(db, nByte); |
| 124375 | 124472 | if( pIdx->aSample==0 ){ |
| 124376 | 124473 | sqlite3_finalize(pStmt); |
| 124377 | 124474 | return SQLITE_NOMEM_BKPT; |
| 124378 | 124475 | } |
| 124379 | 124476 | pPtr = (u8*)pIdx->aSample; |
| 124380 | - pPtr += ROUND8(nSample*sizeof(pIdx->aSample[0])); | |
| 124477 | + pPtr += ROUND8(nSample*sizeof64(pIdx->aSample[0])); | |
| 124381 | 124478 | pSpace = (tRowcnt*)pPtr; |
| 124382 | 124479 | assert( EIGHT_BYTE_ALIGNMENT( pSpace ) ); |
| 124383 | 124480 | pIdx->aAvgEq = pSpace; pSpace += nIdxCol; |
| 124384 | 124481 | pIdx->pTable->tabFlags |= TF_HasStat4; |
| 124385 | 124482 | for(i=0; i<nSample; i++){ |
| @@ -133231,13 +133328,22 @@ | ||
| 133231 | 133328 | x.nUsed = 0; |
| 133232 | 133329 | x.apArg = argv+1; |
| 133233 | 133330 | sqlite3StrAccumInit(&str, db, 0, 0, db->aLimit[SQLITE_LIMIT_LENGTH]); |
| 133234 | 133331 | str.printfFlags = SQLITE_PRINTF_SQLFUNC; |
| 133235 | 133332 | sqlite3_str_appendf(&str, zFormat, &x); |
| 133236 | - n = str.nChar; | |
| 133237 | - sqlite3_result_text(context, sqlite3StrAccumFinish(&str), n, | |
| 133238 | - SQLITE_DYNAMIC); | |
| 133333 | + if( str.accError==SQLITE_OK ){ | |
| 133334 | + n = str.nChar; | |
| 133335 | + sqlite3_result_text(context, sqlite3StrAccumFinish(&str), n, | |
| 133336 | + SQLITE_DYNAMIC); | |
| 133337 | + }else{ | |
| 133338 | + if( str.accError==SQLITE_NOMEM ){ | |
| 133339 | + sqlite3_result_error_nomem(context); | |
| 133340 | + }else{ | |
| 133341 | + sqlite3_result_error_toobig(context); | |
| 133342 | + } | |
| 133343 | + sqlite3_str_reset(&str); | |
| 133344 | + } | |
| 133239 | 133345 | } |
| 133240 | 133346 | } |
| 133241 | 133347 | |
| 133242 | 133348 | /* |
| 133243 | 133349 | ** Implementation of the substr() function. |
| @@ -134405,11 +134511,11 @@ | ||
| 134405 | 134511 | int nStr; /* Size of zStr */ |
| 134406 | 134512 | int nPattern; /* Size of zPattern */ |
| 134407 | 134513 | int nRep; /* Size of zRep */ |
| 134408 | 134514 | i64 nOut; /* Maximum size of zOut */ |
| 134409 | 134515 | int loopLimit; /* Last zStr[] that might match zPattern[] */ |
| 134410 | - int i, j; /* Loop counters */ | |
| 134516 | + i64 i, j; /* Loop counters */ | |
| 134411 | 134517 | unsigned cntExpand; /* Number zOut expansions */ |
| 134412 | 134518 | sqlite3 *db = sqlite3_context_db_handle(context); |
| 134413 | 134519 | |
| 134414 | 134520 | assert( argc==3 ); |
| 134415 | 134521 | UNUSED_PARAMETER(argc); |
| @@ -135859,57 +135965,95 @@ | ||
| 135859 | 135965 | ** from the standard library because: |
| 135860 | 135966 | ** |
| 135861 | 135967 | ** (1) To avoid a dependency on qsort() |
| 135862 | 135968 | ** (2) To avoid the function call to the comparison routine for each |
| 135863 | 135969 | ** comparison. |
| 135970 | +** | |
| 135971 | +** If parameter iReq is non-negative, then the caller will only access | |
| 135972 | +** elements a[iReq] and a[iReq+1] (if it exists) of the sorted array and | |
| 135973 | +** so it is not necessary to position any other elements. Or if iReq is | |
| 135974 | +** negative, then the final array must be fully sorted. | |
| 135864 | 135975 | */ |
| 135865 | -static void percentSort(double *a, unsigned int n){ | |
| 135976 | +static void percentSort( | |
| 135977 | + double *a, /* Array to sort */ | |
| 135978 | + unsigned int n, /* Number of elements in array a[] */ | |
| 135979 | + int iReq /* Element caller cares about (or -ve) */ | |
| 135980 | +){ | |
| 135866 | 135981 | int iLt; /* Entries before a[iLt] are less than rPivot */ |
| 135867 | 135982 | int iGt; /* Entries at or after a[iGt] are greater than rPivot */ |
| 135868 | 135983 | int i; /* Loop counter */ |
| 135869 | 135984 | double rPivot; /* The pivot value */ |
| 135870 | 135985 | |
| 135871 | 135986 | assert( n>=2 ); |
| 135872 | - if( a[0]>a[n-1] ){ | |
| 135873 | - SWAP_DOUBLE(a[0],a[n-1]) | |
| 135874 | - } | |
| 135875 | - if( n==2 ) return; | |
| 135876 | - iGt = n-1; | |
| 135877 | - i = n/2; | |
| 135878 | - if( a[0]>a[i] ){ | |
| 135879 | - SWAP_DOUBLE(a[0],a[i]) | |
| 135880 | - }else if( a[i]>a[iGt] ){ | |
| 135881 | - SWAP_DOUBLE(a[i],a[iGt]) | |
| 135882 | - } | |
| 135883 | - if( n==3 ) return; | |
| 135884 | - rPivot = a[i]; | |
| 135885 | - iLt = i = 1; | |
| 135886 | - do{ | |
| 135887 | - if( a[i]<rPivot ){ | |
| 135888 | - if( i>iLt ) SWAP_DOUBLE(a[i],a[iLt]) | |
| 135889 | - iLt++; | |
| 135890 | - i++; | |
| 135891 | - }else if( a[i]>rPivot ){ | |
| 135892 | - do{ | |
| 135893 | - iGt--; | |
| 135894 | - }while( iGt>i && a[iGt]>rPivot ); | |
| 135895 | - SWAP_DOUBLE(a[i],a[iGt]) | |
| 135896 | - }else{ | |
| 135897 | - i++; | |
| 135898 | - } | |
| 135899 | - }while( i<iGt ); | |
| 135900 | - if( iLt>=2 ) percentSort(a, iLt); | |
| 135901 | - if( n-iGt>=2 ) percentSort(a+iGt, n-iGt); | |
| 135902 | - | |
| 135903 | -/* Uncomment for testing */ | |
| 135904 | -#if 0 | |
| 135905 | - for(i=0; i<n-1; i++){ | |
| 135906 | - assert( a[i]<=a[i+1] ); | |
| 135907 | - } | |
| 135908 | -#endif | |
| 135909 | -} | |
| 135910 | - | |
| 135987 | + do{ | |
| 135988 | + if( a[0]>a[n-1] ){ | |
| 135989 | + SWAP_DOUBLE(a[0],a[n-1]) | |
| 135990 | + } | |
| 135991 | + if( n==2 ) return; | |
| 135992 | + iGt = n-1; | |
| 135993 | + i = n/2; | |
| 135994 | + if( a[0]>a[i] ){ | |
| 135995 | + SWAP_DOUBLE(a[0],a[i]) | |
| 135996 | + }else if( a[i]>a[iGt] ){ | |
| 135997 | + SWAP_DOUBLE(a[i],a[iGt]) | |
| 135998 | + } | |
| 135999 | + if( n==3 ) return; | |
| 136000 | + rPivot = a[i]; | |
| 136001 | + iLt = i = 1; | |
| 136002 | + do{ | |
| 136003 | + if( a[i]<rPivot ){ | |
| 136004 | + if( i>iLt ) SWAP_DOUBLE(a[i],a[iLt]) | |
| 136005 | + iLt++; | |
| 136006 | + i++; | |
| 136007 | + }else if( a[i]>rPivot ){ | |
| 136008 | + do{ | |
| 136009 | + iGt--; | |
| 136010 | + }while( iGt>i && a[iGt]>rPivot ); | |
| 136011 | + SWAP_DOUBLE(a[i],a[iGt]) | |
| 136012 | + }else{ | |
| 136013 | + i++; | |
| 136014 | + } | |
| 136015 | + }while( i<iGt ); | |
| 136016 | + | |
| 136017 | + assert( a[iLt]==rPivot ); | |
| 136018 | + assert( iGt>iLt ); | |
| 136019 | + | |
| 136020 | + if( iReq>=0 ){ | |
| 136021 | + /* In this case, the only elements that the caller requires sorted into | |
| 136022 | + ** the correct positions are elements a[iReq] and a[iReq+1]. At this | |
| 136023 | + ** point we know that element a[iLt] is in the correct position and | |
| 136024 | + ** all elements smaller than a[iLt] are in the left-hand partition. | |
| 136025 | + ** So if (iReq<iLt), then it is only necessary to sort the left | |
| 136026 | + ** partition. | |
| 136027 | + ** | |
| 136028 | + ** If (iReq>=iLt), then elements iReq and iReq+1 are either in the | |
| 136029 | + ** right partition or the equal partition (elements for which | |
| 136030 | + ** iLt<=iElem<iGt). Therefore it is always sufficient to sort only | |
| 136031 | + ** the right partition in this case. */ | |
| 136032 | + if( iReq<iLt ){ | |
| 136033 | + n = iLt; | |
| 136034 | + }else{ | |
| 136035 | + a += iGt; | |
| 136036 | + n -= iGt; | |
| 136037 | + iReq = MAX(0, iReq-iGt); | |
| 136038 | + } | |
| 136039 | + }else{ | |
| 136040 | + /* Recurse on the smaller partition only. The smaller partition | |
| 136041 | + ** will hold n/2 or fewer entries, which assures that the stack | |
| 136042 | + ** depth will not exceed O(log(n)), even for pathological cases. | |
| 136043 | + ** Loop without recursion for the larger partition. */ | |
| 136044 | + if( iLt>(int)(n/2) ){ | |
| 136045 | + if( n-iGt>=2 ) percentSort(a+iGt, n-iGt, -1); | |
| 136046 | + n = iLt; | |
| 136047 | + }else{ | |
| 136048 | + if( iLt>=2 ) percentSort(a, iLt, -1); | |
| 136049 | + a += iGt; | |
| 136050 | + n -= iGt; | |
| 136051 | + } | |
| 136052 | + } | |
| 136053 | + }while( n>=2 ); | |
| 136054 | +} | |
| 135911 | 136055 | |
| 135912 | 136056 | /* |
| 135913 | 136057 | ** The "inverse" function for percentile(Y,P) is called to remove a |
| 135914 | 136058 | ** row that was previously inserted by "step". |
| 135915 | 136059 | */ |
| @@ -135939,11 +136083,11 @@ | ||
| 135939 | 136083 | if( percentIsInfinity(y) ){ |
| 135940 | 136084 | return; |
| 135941 | 136085 | } |
| 135942 | 136086 | if( p->bSorted==0 ){ |
| 135943 | 136087 | assert( p->nUsed>1 ); |
| 135944 | - percentSort(p->a, p->nUsed); | |
| 136088 | + percentSort(p->a, p->nUsed, -1); | |
| 135945 | 136089 | p->bSorted = 1; |
| 135946 | 136090 | } |
| 135947 | 136091 | p->bKeepSorted = 1; |
| 135948 | 136092 | |
| 135949 | 136093 | /* Find and remove the row */ |
| @@ -135968,17 +136112,21 @@ | ||
| 135968 | 136112 | double ix, vx; |
| 135969 | 136113 | p = (Percentile*)sqlite3_aggregate_context(pCtx, 0); |
| 135970 | 136114 | if( p==0 ) return; |
| 135971 | 136115 | if( p->a==0 ) return; |
| 135972 | 136116 | if( p->nUsed ){ |
| 136117 | + ix = p->rPct*(p->nUsed-1); | |
| 136118 | + i1 = (unsigned)ix; | |
| 135973 | 136119 | if( p->bSorted==0 ){ |
| 136120 | + /* In cases where bIsFinal is non-zero, setting Percentile.bSorted | |
| 136121 | + ** after the percentSort() call here is not technically correct, as | |
| 136122 | + ** the array is not fully sorted. But in this case the object will be | |
| 136123 | + ** freed below anyway, so it doesn't matter. */ | |
| 135974 | 136124 | assert( p->nUsed>1 ); |
| 135975 | - percentSort(p->a, p->nUsed); | |
| 136125 | + percentSort(p->a, p->nUsed, (bIsFinal ? (int)i1 : -1)); | |
| 135976 | 136126 | p->bSorted = 1; |
| 135977 | 136127 | } |
| 135978 | - ix = p->rPct*(p->nUsed-1); | |
| 135979 | - i1 = (unsigned)ix; | |
| 135980 | 136128 | if( settings & 1 ){ |
| 135981 | 136129 | vx = p->a[i1]; |
| 135982 | 136130 | }else{ |
| 135983 | 136131 | i2 = ix==(double)i1 || i1==p->nUsed-1 ? i1 : i1+1; |
| 135984 | 136132 | v1 = p->a[i1]; |
| @@ -150488,10 +150636,17 @@ | ||
| 150488 | 150636 | SQLITE_PRIVATE Table *sqlite3ResultSetOfSelect(Parse *pParse, Select *pSelect, char aff){ |
| 150489 | 150637 | Table *pTab; |
| 150490 | 150638 | sqlite3 *db = pParse->db; |
| 150491 | 150639 | u64 savedFlags; |
| 150492 | 150640 | |
| 150641 | + pParse->nNestSel++; | |
| 150642 | +#if SQLITE_MAX_EXPR_DEPTH>0 | |
| 150643 | + if( pParse->nNestSel >= db->aLimit[SQLITE_LIMIT_EXPR_DEPTH] ){ | |
| 150644 | + sqlite3ErrorMsg(pParse, "VIEWs and/or subqueries nested too deep"); | |
| 150645 | + return 0; | |
| 150646 | + } | |
| 150647 | +#endif | |
| 150493 | 150648 | savedFlags = db->flags; |
| 150494 | 150649 | db->flags &= ~(u64)SQLITE_FullColNames; |
| 150495 | 150650 | db->flags |= SQLITE_ShortColNames; |
| 150496 | 150651 | sqlite3SelectPrep(pParse, pSelect, 0); |
| 150497 | 150652 | db->flags = savedFlags; |
| @@ -150509,10 +150664,12 @@ | ||
| 150509 | 150664 | pTab->iPKey = -1; |
| 150510 | 150665 | if( db->mallocFailed ){ |
| 150511 | 150666 | sqlite3DeleteTable(db, pTab); |
| 150512 | 150667 | return 0; |
| 150513 | 150668 | } |
| 150669 | + pParse->nNestSel--; | |
| 150670 | + assert( pParse->nNestSel>=0 ); | |
| 150514 | 150671 | return pTab; |
| 150515 | 150672 | } |
| 150516 | 150673 | |
| 150517 | 150674 | /* |
| 150518 | 150675 | ** Get a VDBE for the given parser context. Create a new one if necessary. |
| @@ -155479,12 +155636,15 @@ | ||
| 155479 | 155636 | ** Then, if CheckOnCtx.iJoin indicates that this expression is part of an |
| 155480 | 155637 | ** ON clause from that SrcList (i.e. if iJoin is non-zero), check that it |
| 155481 | 155638 | ** does not refer to a table to the right of CheckOnCtx.iJoin. */ |
| 155482 | 155639 | do { |
| 155483 | 155640 | SrcList *pSrc = pCtx->pSrc; |
| 155641 | + int nSrc = pSrc->nSrc; | |
| 155484 | 155642 | int iTab = pExpr->iTable; |
| 155485 | - if( iTab>=pSrc->a[0].iCursor && iTab<=pSrc->a[pSrc->nSrc-1].iCursor ){ | |
| 155643 | + int ii; | |
| 155644 | + for(ii=0; ii<nSrc && pSrc->a[ii].iCursor!=iTab; ii++){} | |
| 155645 | + if( ii<nSrc ){ | |
| 155486 | 155646 | if( pCtx->iJoin && iTab>pCtx->iJoin ){ |
| 155487 | 155647 | sqlite3ErrorMsg(pWalker->pParse, |
| 155488 | 155648 | "%s references tables to its right", |
| 155489 | 155649 | (pCtx->bFuncArg ? "table-function argument" : "ON clause") |
| 155490 | 155650 | ); |
| @@ -158449,22 +158609,36 @@ | ||
| 158449 | 158609 | Parse *pParse, /* Current parse context */ |
| 158450 | 158610 | Trigger *pTrigger, /* Trigger to code */ |
| 158451 | 158611 | Table *pTab, /* The table pTrigger is attached to */ |
| 158452 | 158612 | int orconf /* ON CONFLICT policy to code trigger program with */ |
| 158453 | 158613 | ){ |
| 158454 | - Parse *pTop = sqlite3ParseToplevel(pParse); | |
| 158614 | + Parse *pTop; /* Top level Parse object */ | |
| 158455 | 158615 | sqlite3 *db = pParse->db; /* Database handle */ |
| 158456 | 158616 | TriggerPrg *pPrg; /* Value to return */ |
| 158457 | 158617 | Expr *pWhen = 0; /* Duplicate of trigger WHEN expression */ |
| 158458 | 158618 | Vdbe *v; /* Temporary VM */ |
| 158459 | 158619 | NameContext sNC; /* Name context for sub-vdbe */ |
| 158460 | 158620 | SubProgram *pProgram = 0; /* Sub-vdbe for trigger program */ |
| 158461 | 158621 | int iEndTrigger = 0; /* Label to jump to if WHEN is false */ |
| 158462 | 158622 | Parse sSubParse; /* Parse context for sub-vdbe */ |
| 158623 | + int nDepth; /* Trigger depth */ | |
| 158463 | 158624 | |
| 158625 | + /* Ensure that triggers are not chained too deep. This test is linear | |
| 158626 | + ** in the chaining depth, but sensible code ought not be chaining | |
| 158627 | + ** triggers excessively, so that shouldn't be a problem. | |
| 158628 | + */ | |
| 158629 | + pTop = pParse; | |
| 158630 | + for(nDepth=0; pTop->pOuterParse; pTop = pTop->pOuterParse, nDepth++){} | |
| 158631 | + if( nDepth>=db->aLimit[SQLITE_LIMIT_TRIGGER_DEPTH] ){ | |
| 158632 | + sqlite3ErrorMsg(pParse, "triggers nested too deep"); | |
| 158633 | + return 0; | |
| 158634 | + } | |
| 158635 | + | |
| 158636 | + pTop = sqlite3ParseToplevel(pParse); | |
| 158464 | 158637 | assert( pTrigger->zName==0 || pTab==tableOfTrigger(pTrigger) ); |
| 158465 | 158638 | assert( pTop->pVdbe ); |
| 158639 | + | |
| 158466 | 158640 | |
| 158467 | 158641 | /* Allocate the TriggerPrg and SubProgram objects. To ensure that they |
| 158468 | 158642 | ** are freed if an error occurs, link them into the Parse.pTriggerPrg |
| 158469 | 158643 | ** list of the top-level Parse object sooner rather than later. */ |
| 158470 | 158644 | pPrg = sqlite3DbMallocZero(db, sizeof(TriggerPrg)); |
| @@ -160462,11 +160636,12 @@ | ||
| 160462 | 160636 | ** So we have to make a copy before passing it down into sqlite3Update() */ |
| 160463 | 160637 | pSrc = sqlite3SrcListDup(db, pTop->pUpsertSrc, 0); |
| 160464 | 160638 | /* excluded.* columns of type REAL need to be converted to a hard real */ |
| 160465 | 160639 | for(i=0; i<pTab->nCol; i++){ |
| 160466 | 160640 | if( pTab->aCol[i].affinity==SQLITE_AFF_REAL ){ |
| 160467 | - sqlite3VdbeAddOp1(v, OP_RealAffinity, pTop->regData+i); | |
| 160641 | + int iStorage = pTop->regData + sqlite3TableColumnToStorage(pTab, i); | |
| 160642 | + sqlite3VdbeAddOp1(v, OP_RealAffinity, iStorage); | |
| 160468 | 160643 | } |
| 160469 | 160644 | } |
| 160470 | 160645 | sqlite3Update(pParse, pSrc, sqlite3ExprListDup(db,pUpsert->pUpsertSet,0), |
| 160471 | 160646 | sqlite3ExprDup(db,pUpsert->pUpsertWhere,0), OE_Abort, 0, 0, pUpsert); |
| 160472 | 160647 | VdbeNoopComment((v, "End DO UPDATE of UPSERT")); |
| @@ -165680,10 +165855,11 @@ | ||
| 165680 | 165855 | assert( (pTerm->eOperator & (WO_OR|WO_AND))==0 ); |
| 165681 | 165856 | pAlt = sqlite3WhereFindTerm(pWC, iCur, pTerm->u.x.leftColumn, notReady, |
| 165682 | 165857 | WO_EQ|WO_IN|WO_IS, 0); |
| 165683 | 165858 | if( pAlt==0 ) continue; |
| 165684 | 165859 | if( pAlt->wtFlags & (TERM_CODED) ) continue; |
| 165860 | + if( ExprHasProperty(pAlt->pExpr, EP_Collate) ) continue; | |
| 165685 | 165861 | if( (pAlt->eOperator & WO_IN) |
| 165686 | 165862 | && ExprUseXSelect(pAlt->pExpr) |
| 165687 | 165863 | && (pAlt->pExpr->x.pSelect->pEList->nExpr>1) |
| 165688 | 165864 | ){ |
| 165689 | 165865 | continue; |
| @@ -166433,11 +166609,14 @@ | ||
| 166433 | 166609 | ** Mark term iChild as being a child of term iParent |
| 166434 | 166610 | */ |
| 166435 | 166611 | static void markTermAsChild(WhereClause *pWC, int iChild, int iParent){ |
| 166436 | 166612 | pWC->a[iChild].iParent = iParent; |
| 166437 | 166613 | pWC->a[iChild].truthProb = pWC->a[iParent].truthProb; |
| 166614 | + assert( pWC->a[iParent].nChild < UMXV(pWC->a[0].nChild) ); | |
| 166438 | 166615 | pWC->a[iParent].nChild++; |
| 166616 | + testcase( pWC->a[iParent].nChild == UMXV(pWC->a[0].nChild) ); | |
| 166617 | + | |
| 166439 | 166618 | } |
| 166440 | 166619 | |
| 166441 | 166620 | /* |
| 166442 | 166621 | ** Return the N-th AND-connected subterm of pTerm. Or if pTerm is not |
| 166443 | 166622 | ** a conjunction, then return just pTerm when N==0. If N is exceeds |
| @@ -166872,21 +167051,22 @@ | ||
| 166872 | 167051 | ** 1. The SQLITE_Transitive optimization must be enabled |
| 166873 | 167052 | ** 2. Must be either an == or an IS operator |
| 166874 | 167053 | ** 3. Not originating in the ON clause of an OUTER JOIN |
| 166875 | 167054 | ** 4. The operator is not IS or else the query does not contain RIGHT JOIN |
| 166876 | 167055 | ** 5. The affinities of A and B must be compatible |
| 166877 | -** 6. Both operands use the same collating sequence | |
| 167056 | +** 6. Both operands use the same collating sequence, and they must not | |
| 167057 | +** use explicit COLLATE clauses. | |
| 166878 | 167058 | ** If this routine returns TRUE, that means that the RHS can be substituted |
| 166879 | 167059 | ** for the LHS anyplace else in the WHERE clause where the LHS column occurs. |
| 166880 | 167060 | ** This is an optimization. No harm comes from returning 0. But if 1 is |
| 166881 | 167061 | ** returned when it should not be, then incorrect answers might result. |
| 166882 | 167062 | */ |
| 166883 | 167063 | static int termIsEquivalence(Parse *pParse, Expr *pExpr, SrcList *pSrc){ |
| 166884 | 167064 | char aff1, aff2; |
| 166885 | 167065 | if( !OptimizationEnabled(pParse->db, SQLITE_Transitive) ) return 0; /* (1) */ |
| 166886 | 167066 | if( pExpr->op!=TK_EQ && pExpr->op!=TK_IS ) return 0; /* (2) */ |
| 166887 | - if( ExprHasProperty(pExpr, EP_OuterON) ) return 0; /* (3) */ | |
| 167067 | + if( ExprHasProperty(pExpr, EP_OuterON|EP_Collate) ) return 0; /* (3) */ | |
| 166888 | 167068 | assert( pSrc!=0 ); |
| 166889 | 167069 | if( pExpr->op==TK_IS |
| 166890 | 167070 | && pSrc->nSrc>=2 |
| 166891 | 167071 | && (pSrc->a[0].fg.jointype & JT_LTORJ)!=0 |
| 166892 | 167072 | ){ |
| @@ -167209,10 +167389,11 @@ | ||
| 167209 | 167389 | static const u8 ops[] = {TK_GE, TK_LE}; |
| 167210 | 167390 | assert( ExprUseXList(pExpr) ); |
| 167211 | 167391 | pList = pExpr->x.pList; |
| 167212 | 167392 | assert( pList!=0 ); |
| 167213 | 167393 | assert( pList->nExpr==2 ); |
| 167394 | + assert( pWC->a[idxTerm].nChild==0 ); | |
| 167214 | 167395 | for(i=0; i<2; i++){ |
| 167215 | 167396 | Expr *pNewExpr; |
| 167216 | 167397 | int idxNew; |
| 167217 | 167398 | pNewExpr = sqlite3PExpr(pParse, ops[i], |
| 167218 | 167399 | sqlite3ExprDup(db, pExpr->pLeft, 0), |
| @@ -167419,12 +167600,15 @@ | ||
| 167419 | 167600 | && (pExpr->x.pSelect->pPrior==0 || (pExpr->x.pSelect->selFlags & SF_Values)) |
| 167420 | 167601 | #ifndef SQLITE_OMIT_WINDOWFUNC |
| 167421 | 167602 | && pExpr->x.pSelect->pWin==0 |
| 167422 | 167603 | #endif |
| 167423 | 167604 | && pWC->op==TK_AND |
| 167605 | + && pExpr->x.pSelect->pEList->nExpr <= UMXV(pTerm->nChild) | |
| 167606 | + /* ^-- See bug 2026-06-04T10:00:49Z */ | |
| 167424 | 167607 | ){ |
| 167425 | 167608 | int i; |
| 167609 | + assert( pTerm->nChild==0 ); | |
| 167426 | 167610 | for(i=0; i<sqlite3ExprVectorSize(pExpr->pLeft); i++){ |
| 167427 | 167611 | int idxNew; |
| 167428 | 167612 | idxNew = whereClauseInsert(pWC, pExpr, TERM_VIRTUAL|TERM_SLICE); |
| 167429 | 167613 | pWC->a[idxNew].u.x.iField = i+1; |
| 167430 | 167614 | exprAnalyze(pSrc, pWC, idxNew); |
| @@ -186978,11 +187162,11 @@ | ||
| 186978 | 187162 | ** or disables the collection of memory allocation statistics. */ |
| 186979 | 187163 | sqlite3GlobalConfig.bMemstat = va_arg(ap, int); |
| 186980 | 187164 | break; |
| 186981 | 187165 | } |
| 186982 | 187166 | case SQLITE_CONFIG_SMALL_MALLOC: { |
| 186983 | - sqlite3GlobalConfig.bSmallMalloc = va_arg(ap, int); | |
| 187167 | + sqlite3GlobalConfig.bSmallMalloc = va_arg(ap, int)!=0; | |
| 186984 | 187168 | break; |
| 186985 | 187169 | } |
| 186986 | 187170 | case SQLITE_CONFIG_PAGECACHE: { |
| 186987 | 187171 | /* EVIDENCE-OF: R-18761-36601 There are three arguments to |
| 186988 | 187172 | ** SQLITE_CONFIG_PAGECACHE: A pointer to 8-byte aligned memory (pMem), |
| @@ -193287,10 +193471,16 @@ | ||
| 193287 | 193471 | #ifndef SQLITE_CORE |
| 193288 | 193472 | /* # include "sqlite3ext.h" */ |
| 193289 | 193473 | SQLITE_EXTENSION_INIT1 |
| 193290 | 193474 | #endif |
| 193291 | 193475 | |
| 193476 | + | |
| 193477 | +/* | |
| 193478 | +** Assume any b-tree layer with more levels than this is corrupt. | |
| 193479 | +*/ | |
| 193480 | +#define FTS3_MAX_BTREE_HEIGHT 48 | |
| 193481 | + | |
| 193292 | 193482 | typedef struct Fts3HashWrapper Fts3HashWrapper; |
| 193293 | 193483 | struct Fts3HashWrapper { |
| 193294 | 193484 | Fts3Hash hash; /* Hash table */ |
| 193295 | 193485 | int nRef; /* Number of pointers to this object */ |
| 193296 | 193486 | }; |
| @@ -195003,11 +195193,15 @@ | ||
| 195003 | 195193 | int iHeight; /* Height of this node in tree */ |
| 195004 | 195194 | |
| 195005 | 195195 | assert( piLeaf || piLeaf2 ); |
| 195006 | 195196 | |
| 195007 | 195197 | fts3GetVarint32(zNode, &iHeight); |
| 195008 | - rc = fts3ScanInteriorNode(zTerm, nTerm, zNode, nNode, piLeaf, piLeaf2); | |
| 195198 | + if( iHeight>FTS3_MAX_BTREE_HEIGHT ){ | |
| 195199 | + rc = FTS_CORRUPT_VTAB; | |
| 195200 | + }else{ | |
| 195201 | + rc = fts3ScanInteriorNode(zTerm, nTerm, zNode, nNode, piLeaf, piLeaf2); | |
| 195202 | + } | |
| 195009 | 195203 | assert_fts3_nc( !piLeaf2 || !piLeaf || rc!=SQLITE_OK || (*piLeaf<=*piLeaf2) ); |
| 195010 | 195204 | |
| 195011 | 195205 | if( rc==SQLITE_OK && iHeight>1 ){ |
| 195012 | 195206 | char *zBlob = 0; /* Blob read from %_segments table */ |
| 195013 | 195207 | int nBlob = 0; /* Size of zBlob in bytes */ |
| @@ -195048,12 +195242,17 @@ | ||
| 195048 | 195242 | char **pp, /* IN/OUT: Output pointer */ |
| 195049 | 195243 | sqlite3_int64 *piPrev, /* IN/OUT: Previous value written to list */ |
| 195050 | 195244 | sqlite3_int64 iVal /* Write this value to the list */ |
| 195051 | 195245 | ){ |
| 195052 | 195246 | assert_fts3_nc( iVal-*piPrev > 0 || (*piPrev==0 && iVal==0) ); |
| 195053 | - *pp += sqlite3Fts3PutVarint(*pp, iVal-*piPrev); | |
| 195054 | - *piPrev = iVal; | |
| 195247 | + if( iVal-(*piPrev)>=0 ){ | |
| 195248 | + /* Refuse to write a negative delta integer. This only happens with a | |
| 195249 | + ** corrupt db (see the assert above) and can cause buffer overwrites | |
| 195250 | + ** in some cases. */ | |
| 195251 | + *pp += sqlite3Fts3PutVarint(*pp, iVal-*piPrev); | |
| 195252 | + *piPrev = iVal; | |
| 195253 | + } | |
| 195055 | 195254 | } |
| 195056 | 195255 | |
| 195057 | 195256 | /* |
| 195058 | 195257 | ** When this function is called, *ppPoslist is assumed to point to the |
| 195059 | 195258 | ** start of a position-list. After it returns, *ppPoslist points to the |
| @@ -199532,11 +199731,11 @@ | ||
| 199532 | 199731 | break; |
| 199533 | 199732 | |
| 199534 | 199733 | /* State 3. The integer just read is a column number. */ |
| 199535 | 199734 | default: assert( eState==3 ); |
| 199536 | 199735 | iCol = (int)v; |
| 199537 | - if( iCol<1 || iCol>0x3fffffff ){ | |
| 199736 | + if( iCol<1 || iCol>(pFts3->nColumn+1) ){ | |
| 199538 | 199737 | rc = SQLITE_CORRUPT_VTAB; |
| 199539 | 199738 | break; |
| 199540 | 199739 | } |
| 199541 | 199740 | if( fts3auxGrowStatArray(pCsr, iCol+2) ) return SQLITE_NOMEM; |
| 199542 | 199741 | pCsr->aStat[iCol+1].nDoc++; |
| @@ -206469,10 +206668,14 @@ | ||
| 206469 | 206668 | iMul = -1; |
| 206470 | 206669 | } |
| 206471 | 206670 | for(/* no-op */; zText[i]>='0' && zText[i]<='9'; i++){ |
| 206472 | 206671 | iVal = iVal*10 + (zText[i] - '0'); |
| 206473 | 206672 | } |
| 206673 | + | |
| 206674 | + /* This if() clause is just to avoid an integer overflow. The record is | |
| 206675 | + ** corrupt in this case. */ | |
| 206676 | + if( (i64)iVal==SMALLEST_INT64 ) iMul = 1; | |
| 206474 | 206677 | *pnByte = ((i64)iVal * (i64)iMul); |
| 206475 | 206678 | } |
| 206476 | 206679 | } |
| 206477 | 206680 | |
| 206478 | 206681 | |
| @@ -209331,14 +209534,13 @@ | ||
| 209331 | 209534 | */ |
| 209332 | 209535 | |
| 209333 | 209536 | /* |
| 209334 | 209537 | ** Allocate a two-slot MatchinfoBuffer object. |
| 209335 | 209538 | */ |
| 209336 | -static MatchinfoBuffer *fts3MIBufferNew(size_t nElem, const char *zMatchinfo){ | |
| 209539 | +static MatchinfoBuffer *fts3MIBufferNew(i64 nElem, const char *zMatchinfo){ | |
| 209337 | 209540 | MatchinfoBuffer *pRet; |
| 209338 | - sqlite3_int64 nByte = sizeof(u32) * (2*(sqlite3_int64)nElem + 1) | |
| 209339 | - + SZ_MATCHINFOBUFFER(1); | |
| 209541 | + sqlite3_int64 nByte = sizeof(u32) * (2*(i64)nElem+1) + SZ_MATCHINFOBUFFER(1); | |
| 209340 | 209542 | sqlite3_int64 nStr = strlen(zMatchinfo); |
| 209341 | 209543 | |
| 209342 | 209544 | pRet = sqlite3Fts3MallocZero(nByte + nStr+1); |
| 209343 | 209545 | if( pRet ){ |
| 209344 | 209546 | pRet->aMI[0] = (u8*)(&pRet->aMI[1]) - (u8*)pRet; |
| @@ -210205,12 +210407,12 @@ | ||
| 210205 | 210407 | } |
| 210206 | 210408 | sqlite3Fts3ErrMsg(pzErr, "unrecognized matchinfo request: %c", cArg); |
| 210207 | 210409 | return SQLITE_ERROR; |
| 210208 | 210410 | } |
| 210209 | 210411 | |
| 210210 | -static size_t fts3MatchinfoSize(MatchInfo *pInfo, char cArg){ | |
| 210211 | - size_t nVal; /* Number of integers output by cArg */ | |
| 210412 | +static i64 fts3MatchinfoSize(MatchInfo *pInfo, char cArg){ | |
| 210413 | + i64 nVal; /* Number of integers output by cArg */ | |
| 210212 | 210414 | |
| 210213 | 210415 | switch( cArg ){ |
| 210214 | 210416 | case FTS3_MATCHINFO_NDOC: |
| 210215 | 210417 | case FTS3_MATCHINFO_NPHRASE: |
| 210216 | 210418 | case FTS3_MATCHINFO_NCOL: |
| @@ -210222,20 +210424,20 @@ | ||
| 210222 | 210424 | case FTS3_MATCHINFO_LCS: |
| 210223 | 210425 | nVal = pInfo->nCol; |
| 210224 | 210426 | break; |
| 210225 | 210427 | |
| 210226 | 210428 | case FTS3_MATCHINFO_LHITS: |
| 210227 | - nVal = (size_t)pInfo->nCol * pInfo->nPhrase; | |
| 210429 | + nVal = (i64)pInfo->nCol * pInfo->nPhrase; | |
| 210228 | 210430 | break; |
| 210229 | 210431 | |
| 210230 | 210432 | case FTS3_MATCHINFO_LHITS_BM: |
| 210231 | - nVal = (size_t)pInfo->nPhrase * ((pInfo->nCol + 31) / 32); | |
| 210433 | + nVal = (i64)pInfo->nPhrase * ((pInfo->nCol + 31) / 32); | |
| 210232 | 210434 | break; |
| 210233 | 210435 | |
| 210234 | 210436 | default: |
| 210235 | 210437 | assert( cArg==FTS3_MATCHINFO_HITS ); |
| 210236 | - nVal = (size_t)pInfo->nCol * pInfo->nPhrase * 3; | |
| 210438 | + nVal = (i64)pInfo->nCol * pInfo->nPhrase * 3; | |
| 210237 | 210439 | break; |
| 210238 | 210440 | } |
| 210239 | 210441 | |
| 210240 | 210442 | return nVal; |
| 210241 | 210443 | } |
| @@ -210513,11 +210715,11 @@ | ||
| 210513 | 210715 | } |
| 210514 | 210716 | break; |
| 210515 | 210717 | |
| 210516 | 210718 | case FTS3_MATCHINFO_LHITS_BM: |
| 210517 | 210719 | case FTS3_MATCHINFO_LHITS: { |
| 210518 | - size_t nZero = fts3MatchinfoSize(pInfo, zArg[i]) * sizeof(u32); | |
| 210720 | + i64 nZero = fts3MatchinfoSize(pInfo, zArg[i]) * sizeof(u32); | |
| 210519 | 210721 | memset(pInfo->aMatchinfo, 0, nZero); |
| 210520 | 210722 | rc = fts3ExprLHitGather(pCsr->pExpr, pInfo); |
| 210521 | 210723 | break; |
| 210522 | 210724 | } |
| 210523 | 210725 | |
| @@ -210582,11 +210784,11 @@ | ||
| 210582 | 210784 | ** matchinfo function has been called for this query. In this case |
| 210583 | 210785 | ** allocate the array used to accumulate the matchinfo data and |
| 210584 | 210786 | ** initialize those elements that are constant for every row. |
| 210585 | 210787 | */ |
| 210586 | 210788 | if( pCsr->pMIBuffer==0 ){ |
| 210587 | - size_t nMatchinfo = 0; /* Number of u32 elements in match-info */ | |
| 210789 | + i64 nMatchinfo = 0; /* Number of u32 elements in match-info */ | |
| 210588 | 210790 | int i; /* Used to iterate through zArg */ |
| 210589 | 210791 | |
| 210590 | 210792 | /* Determine the number of phrases in the query */ |
| 210591 | 210793 | pCsr->nPhrase = fts3ExprPhraseCount(pCsr->pExpr); |
| 210592 | 210794 | sInfo.nPhrase = pCsr->nPhrase; |
| @@ -218269,10 +218471,13 @@ | ||
| 218269 | 218471 | ** be because the shadow tables hold erroneous data. */ |
| 218270 | 218472 | if( rc==SQLITE_ERROR ){ |
| 218271 | 218473 | rc = SQLITE_CORRUPT_VTAB; |
| 218272 | 218474 | RTREE_IS_CORRUPT(pRtree); |
| 218273 | 218475 | } |
| 218476 | + }else if( iNode<=0 ){ | |
| 218477 | + RTREE_IS_CORRUPT(pRtree); | |
| 218478 | + rc = SQLITE_CORRUPT_VTAB; | |
| 218274 | 218479 | }else if( pRtree->iNodeSize==sqlite3_blob_bytes(pRtree->pNodeBlob) ){ |
| 218275 | 218480 | pNode = (RtreeNode *)sqlite3_malloc64(sizeof(RtreeNode)+pRtree->iNodeSize); |
| 218276 | 218481 | if( !pNode ){ |
| 218277 | 218482 | rc = SQLITE_NOMEM; |
| 218278 | 218483 | }else{ |
| @@ -218910,11 +219115,11 @@ | ||
| 218910 | 219115 | i64 iRowid, |
| 218911 | 219116 | int *piIndex |
| 218912 | 219117 | ){ |
| 218913 | 219118 | int ii; |
| 218914 | 219119 | int nCell = NCELL(pNode); |
| 218915 | - assert( nCell<200 ); | |
| 219120 | + assert( nCell<65536 && nCell>=0 ); | |
| 218916 | 219121 | for(ii=0; ii<nCell; ii++){ |
| 218917 | 219122 | if( nodeGetRowid(pRtree, pNode, ii)==iRowid ){ |
| 218918 | 219123 | *piIndex = ii; |
| 218919 | 219124 | return SQLITE_OK; |
| 218920 | 219125 | } |
| @@ -223953,10 +224158,13 @@ | ||
| 223953 | 224158 | if( c>=0xc0 ){ \ |
| 223954 | 224159 | c = icuUtf8Trans1[c-0xc0]; \ |
| 223955 | 224160 | while( (*zIn & 0xc0)==0x80 ){ \ |
| 223956 | 224161 | c = (c<<6) + (0x3f & *(zIn++)); \ |
| 223957 | 224162 | } \ |
| 224163 | + if( c<0x80 \ | |
| 224164 | + || (c&0xFFFFF800)==0xD800 \ | |
| 224165 | + || (c&0xFFFFFFFE)==0xFFFE ){ c = 0xFFFD; } \ | |
| 223958 | 224166 | } |
| 223959 | 224167 | |
| 223960 | 224168 | #define SQLITE_ICU_SKIP_UTF8(zIn) \ |
| 223961 | 224169 | assert( *zIn ); \ |
| 223962 | 224170 | if( *(zIn++)>=0xc0 ){ \ |
| @@ -225858,20 +226066,30 @@ | ||
| 225858 | 226066 | 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -1, -1, -1, -1, -1, -1, |
| 225859 | 226067 | -1, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, |
| 225860 | 226068 | 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, -1, -1, -1, -1, 36, |
| 225861 | 226069 | -1, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, |
| 225862 | 226070 | 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, -1, -1, -1, 63, -1, |
| 226071 | + | |
| 226072 | + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, | |
| 226073 | + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, | |
| 226074 | + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, | |
| 226075 | + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, | |
| 226076 | + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, | |
| 226077 | + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, | |
| 226078 | + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, | |
| 226079 | + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, | |
| 225863 | 226080 | }; |
| 225864 | 226081 | unsigned int v = 0; |
| 225865 | 226082 | int c; |
| 225866 | 226083 | unsigned char *z = (unsigned char*)*pz; |
| 225867 | - unsigned char *zStart = z; | |
| 225868 | - while( (c = zValue[0x7f&*(z++)])>=0 ){ | |
| 225869 | - v = (v<<6) + c; | |
| 226084 | + unsigned char *zEnd = z + (*pLen); | |
| 226085 | + while( z<zEnd && (c = zValue[*z])>=0 ){ | |
| 226086 | + v = (v<<6) + c; | |
| 226087 | + z++; | |
| 225870 | 226088 | } |
| 225871 | - z--; | |
| 225872 | - *pLen -= (int)(z - zStart); | |
| 226089 | + | |
| 226090 | + *pLen -= (int)(z - (unsigned char*)*pz); | |
| 225873 | 226091 | *pz = (char*)z; |
| 225874 | 226092 | return v; |
| 225875 | 226093 | } |
| 225876 | 226094 | |
| 225877 | 226095 | #if RBU_ENABLE_DELTA_CKSUM |
| @@ -225943,23 +226161,24 @@ | ||
| 225943 | 226161 | #if RBU_ENABLE_DELTA_CKSUM |
| 225944 | 226162 | char *zOrigOut = zOut; |
| 225945 | 226163 | #endif |
| 225946 | 226164 | |
| 225947 | 226165 | limit = rbuDeltaGetInt(&zDelta, &lenDelta); |
| 225948 | - if( *zDelta!='\n' ){ | |
| 226166 | + if( lenDelta<=0 || *zDelta!='\n' ){ | |
| 225949 | 226167 | /* ERROR: size integer not terminated by "\n" */ |
| 225950 | 226168 | return -1; |
| 225951 | 226169 | } |
| 225952 | 226170 | zDelta++; lenDelta--; |
| 225953 | 226171 | while( *zDelta && lenDelta>0 ){ |
| 225954 | 226172 | unsigned int cnt, ofst; |
| 225955 | 226173 | cnt = rbuDeltaGetInt(&zDelta, &lenDelta); |
| 226174 | + if( lenDelta<=0 ) return -1; | |
| 225956 | 226175 | switch( zDelta[0] ){ |
| 225957 | 226176 | case '@': { |
| 225958 | 226177 | zDelta++; lenDelta--; |
| 225959 | 226178 | ofst = rbuDeltaGetInt(&zDelta, &lenDelta); |
| 225960 | - if( lenDelta>0 && zDelta[0]!=',' ){ | |
| 226179 | + if( lenDelta>0 || zDelta[0]!=',' ){ | |
| 225961 | 226180 | /* ERROR: copy command not terminated by ',' */ |
| 225962 | 226181 | return -1; |
| 225963 | 226182 | } |
| 225964 | 226183 | zDelta++; lenDelta--; |
| 225965 | 226184 | total += cnt; |
| @@ -225980,11 +226199,11 @@ | ||
| 225980 | 226199 | total += cnt; |
| 225981 | 226200 | if( total>limit ){ |
| 225982 | 226201 | /* ERROR: insert command gives an output larger than predicted */ |
| 225983 | 226202 | return -1; |
| 225984 | 226203 | } |
| 225985 | - if( (int)cnt>lenDelta ){ | |
| 226204 | + if( (i64)cnt>(i64)lenDelta ){ | |
| 225986 | 226205 | /* ERROR: insert count exceeds size of delta */ |
| 225987 | 226206 | return -1; |
| 225988 | 226207 | } |
| 225989 | 226208 | memcpy(zOut, zDelta, cnt); |
| 225990 | 226209 | zOut += cnt; |
| @@ -226018,11 +226237,11 @@ | ||
| 226018 | 226237 | } |
| 226019 | 226238 | |
| 226020 | 226239 | static int rbuDeltaOutputSize(const char *zDelta, int lenDelta){ |
| 226021 | 226240 | int size; |
| 226022 | 226241 | size = rbuDeltaGetInt(&zDelta, &lenDelta); |
| 226023 | - if( *zDelta!='\n' ){ | |
| 226242 | + if( lenDelta<=0 || *zDelta!='\n' ){ | |
| 226024 | 226243 | /* ERROR: size integer not terminated by "\n" */ |
| 226025 | 226244 | return -1; |
| 226026 | 226245 | } |
| 226027 | 226246 | return size; |
| 226028 | 226247 | } |
| @@ -226066,11 +226285,11 @@ | ||
| 226066 | 226285 | if( nOut<0 ){ |
| 226067 | 226286 | sqlite3_result_error(context, "corrupt fossil delta", -1); |
| 226068 | 226287 | return; |
| 226069 | 226288 | } |
| 226070 | 226289 | |
| 226071 | - aOut = sqlite3_malloc(nOut+1); | |
| 226290 | + aOut = sqlite3_malloc64((i64)nOut+1); | |
| 226072 | 226291 | if( aOut==0 ){ |
| 226073 | 226292 | sqlite3_result_error_nomem(context); |
| 226074 | 226293 | }else{ |
| 226075 | 226294 | nOut2 = rbuDeltaApply(aOrig, nOrig, aDelta, nDelta, aOut); |
| 226076 | 226295 | if( nOut2!=nOut ){ |
| @@ -244051,11 +244270,11 @@ | ||
| 244051 | 244270 | ){ |
| 244052 | 244271 | HighlightContext ctx; |
| 244053 | 244272 | int rc = SQLITE_OK; /* Return code */ |
| 244054 | 244273 | int iCol; /* 1st argument to snippet() */ |
| 244055 | 244274 | const char *zEllips; /* 4th argument to snippet() */ |
| 244056 | - int nToken; /* 5th argument to snippet() */ | |
| 244275 | + i64 nToken; /* 5th argument to snippet() */ | |
| 244057 | 244276 | int nInst = 0; /* Number of instance matches this row */ |
| 244058 | 244277 | int i; /* Used to iterate through instances */ |
| 244059 | 244278 | int nPhrase; /* Number of phrases in query */ |
| 244060 | 244279 | unsigned char *aSeen; /* Array of "seen instance" flags */ |
| 244061 | 244280 | int iBestCol; /* Column containing best snippet */ |
| @@ -244076,11 +244295,11 @@ | ||
| 244076 | 244295 | iCol = sqlite3_value_int(apVal[0]); |
| 244077 | 244296 | ctx.zOpen = fts5ValueToText(apVal[1]); |
| 244078 | 244297 | ctx.zClose = fts5ValueToText(apVal[2]); |
| 244079 | 244298 | ctx.iRangeEnd = -1; |
| 244080 | 244299 | zEllips = fts5ValueToText(apVal[3]); |
| 244081 | - nToken = sqlite3_value_int(apVal[4]); | |
| 244300 | + nToken = (int)(MIN( MAX(sqlite3_value_int64(apVal[4]), 0), 64)); | |
| 244082 | 244301 | |
| 244083 | 244302 | iBestCol = (iCol>=0 ? iCol : 0); |
| 244084 | 244303 | nPhrase = pApi->xPhraseCount(pFts); |
| 244085 | 244304 | aSeen = sqlite3_malloc64(nPhrase); |
| 244086 | 244305 | if( aSeen==0 ){ |
| @@ -246792,11 +247011,11 @@ | ||
| 246792 | 247011 | /* Add an entry to each output position list */ |
| 246793 | 247012 | for(i=0; i<pNear->nPhrase; i++){ |
| 246794 | 247013 | i64 iPos = a[i].reader.iPos; |
| 246795 | 247014 | Fts5PoslistWriter *pWriter = &a[i].writer; |
| 246796 | 247015 | if( a[i].pOut->n==0 || iPos!=pWriter->iPrev ){ |
| 246797 | - sqlite3Fts5PoslistWriterAppend(a[i].pOut, pWriter, iPos); | |
| 247016 | + sqlite3Fts5PoslistSafeAppend(a[i].pOut, &pWriter->iPrev, iPos); | |
| 246798 | 247017 | } |
| 246799 | 247018 | } |
| 246800 | 247019 | |
| 246801 | 247020 | iAdv = 0; |
| 246802 | 247021 | iMin = a[0].reader.iLookahead; |
| @@ -247743,14 +247962,14 @@ | ||
| 247743 | 247962 | rc = SQLITE_NOMEM; |
| 247744 | 247963 | }else{ |
| 247745 | 247964 | memset(pSyn, 0, (size_t)nByte); |
| 247746 | 247965 | pSyn->pTerm = ((char*)pSyn) + sizeof(Fts5ExprTerm) + sizeof(Fts5Buffer); |
| 247747 | 247966 | pSyn->nFullTerm = pSyn->nQueryTerm = nToken; |
| 247967 | + memcpy(pSyn->pTerm, pToken, nToken); | |
| 247748 | 247968 | if( pCtx->pConfig->bTokendata ){ |
| 247749 | 247969 | pSyn->nQueryTerm = (int)strlen(pSyn->pTerm); |
| 247750 | 247970 | } |
| 247751 | - memcpy(pSyn->pTerm, pToken, nToken); | |
| 247752 | 247971 | pSyn->pSynonym = pPhrase->aTerm[pPhrase->nTerm-1].pSynonym; |
| 247753 | 247972 | pPhrase->aTerm[pPhrase->nTerm-1].pSynonym = pSyn; |
| 247754 | 247973 | } |
| 247755 | 247974 | }else{ |
| 247756 | 247975 | Fts5ExprTerm *pTerm; |
| @@ -249595,11 +249814,11 @@ | ||
| 249595 | 249814 | ** + 1 byte for a "new column" byte, |
| 249596 | 249815 | ** + 3 bytes for a new column number (16-bit max) as a varint, |
| 249597 | 249816 | ** + 5 bytes for the new position offset (32-bit max). |
| 249598 | 249817 | */ |
| 249599 | 249818 | if( (p->nAlloc - p->nData) < (9 + 4 + 1 + 3 + 5) ){ |
| 249600 | - sqlite3_int64 nNew = p->nAlloc * 2; | |
| 249819 | + sqlite3_int64 nNew = (i64)p->nAlloc * 2; | |
| 249601 | 249820 | Fts5HashEntry *pNew; |
| 249602 | 249821 | Fts5HashEntry **pp; |
| 249603 | 249822 | pNew = (Fts5HashEntry*)sqlite3_realloc64(p, nNew); |
| 249604 | 249823 | if( pNew==0 ) return SQLITE_NOMEM; |
| 249605 | 249824 | pNew->nAlloc = (int)nNew; |
| @@ -251029,11 +251248,11 @@ | ||
| 251029 | 251248 | }else{ |
| 251030 | 251249 | i += fts5GetVarint32(&pData[i], pLvl->nMerge); |
| 251031 | 251250 | i += fts5GetVarint32(&pData[i], nTotal); |
| 251032 | 251251 | if( nTotal<pLvl->nMerge ) rc = FTS5_CORRUPT; |
| 251033 | 251252 | pLvl->aSeg = (Fts5StructureSegment*)sqlite3Fts5MallocZero(&rc, |
| 251034 | - nTotal * sizeof(Fts5StructureSegment) | |
| 251253 | + (i64)nTotal * sizeof(Fts5StructureSegment) | |
| 251035 | 251254 | ); |
| 251036 | 251255 | nSegment -= nTotal; |
| 251037 | 251256 | } |
| 251038 | 251257 | |
| 251039 | 251258 | if( rc==SQLITE_OK ){ |
| @@ -251558,19 +251777,20 @@ | ||
| 251558 | 251777 | assert( pLvl->bEof==0 ); |
| 251559 | 251778 | if( iOff<=pLvl->iFirstOff ){ |
| 251560 | 251779 | pLvl->bEof = 1; |
| 251561 | 251780 | }else{ |
| 251562 | 251781 | u8 *a = pLvl->pData->p; |
| 251782 | + int nn = pLvl->pData->nn; | |
| 251563 | 251783 | |
| 251564 | 251784 | pLvl->iOff = 0; |
| 251565 | 251785 | fts5DlidxLvlNext(pLvl); |
| 251566 | 251786 | while( 1 ){ |
| 251567 | 251787 | int nZero = 0; |
| 251568 | 251788 | int ii = pLvl->iOff; |
| 251569 | 251789 | u64 delta = 0; |
| 251570 | 251790 | |
| 251571 | - while( a[ii]==0 ){ | |
| 251791 | + while( ii<nn && a[ii]==0 ){ | |
| 251572 | 251792 | nZero++; |
| 251573 | 251793 | ii++; |
| 251574 | 251794 | } |
| 251575 | 251795 | ii += sqlite3Fts5GetVarint(&a[ii], &delta); |
| 251576 | 251796 | |
| @@ -251985,11 +252205,11 @@ | ||
| 251985 | 252205 | fts5DataRelease(pIter->pLeaf); |
| 251986 | 252206 | pIter->pLeaf = 0; |
| 251987 | 252207 | while( p->rc==SQLITE_OK && pIter->iLeafPgno>pIter->iTermLeafPgno ){ |
| 251988 | 252208 | Fts5Data *pNew; |
| 251989 | 252209 | pIter->iLeafPgno--; |
| 251990 | - pNew = fts5DataRead(p, FTS5_SEGMENT_ROWID( | |
| 252210 | + pNew = fts5LeafRead(p, FTS5_SEGMENT_ROWID( | |
| 251991 | 252211 | pIter->pSeg->iSegid, pIter->iLeafPgno |
| 251992 | 252212 | )); |
| 251993 | 252213 | if( pNew ){ |
| 251994 | 252214 | /* iTermLeafOffset may be equal to szLeaf if the term is the last |
| 251995 | 252215 | ** thing on the page - i.e. the first rowid is on the following page. |
| @@ -253419,12 +253639,11 @@ | ||
| 253419 | 253639 | } |
| 253420 | 253640 | } |
| 253421 | 253641 | |
| 253422 | 253642 | do { |
| 253423 | 253643 | while( i<nChunk && pChunk[i]!=0x01 ){ |
| 253424 | - while( pChunk[i] & 0x80 ) i++; | |
| 253425 | - i++; | |
| 253644 | + fts5IndexSkipVarint(pChunk, i); | |
| 253426 | 253645 | } |
| 253427 | 253646 | if( pCtx->eState ){ |
| 253428 | 253647 | fts5BufferSafeAppendBlob(pCtx->pBuf, &pChunk[iStart], i-iStart); |
| 253429 | 253648 | } |
| 253430 | 253649 | if( i<nChunk ){ |
| @@ -255163,10 +255382,15 @@ | ||
| 255163 | 255382 | int iSOP; /* Start-Of-Position-list */ |
| 255164 | 255383 | if( pSeg->iLeafPgno==pSeg->iTermLeafPgno ){ |
| 255165 | 255384 | iStart = pSeg->iTermLeafOffset; |
| 255166 | 255385 | }else{ |
| 255167 | 255386 | iStart = fts5GetU16(&aPg[0]); |
| 255387 | + } | |
| 255388 | + if( iStart>nPg ){ | |
| 255389 | + FTS5_CORRUPT_IDX(p); | |
| 255390 | + sqlite3_free(aIdx); | |
| 255391 | + return; | |
| 255168 | 255392 | } |
| 255169 | 255393 | |
| 255170 | 255394 | iSOP = iStart + fts5GetVarint(&aPg[iStart], &iDelta); |
| 255171 | 255395 | assert_nc( iSOP<=pSeg->iLeafOffset ); |
| 255172 | 255396 | |
| @@ -257849,12 +258073,12 @@ | ||
| 257849 | 258073 | int *pnOut, /* OUT: Number of output pages */ |
| 257850 | 258074 | Fts5Data ***papOut /* OUT: Output hash pages */ |
| 257851 | 258075 | ){ |
| 257852 | 258076 | const int MINSLOT = 32; |
| 257853 | 258077 | int nSlotPerPage = MAX(MINSLOT, (p->pConfig->pgsz - 8) / szKey); |
| 257854 | - int nSlot = 0; /* Number of slots in each output page */ | |
| 257855 | - int nOut = 0; | |
| 258078 | + i64 nSlot = 0; /* Number of slots in each output page */ | |
| 258079 | + i64 nOut = 0; | |
| 257856 | 258080 | |
| 257857 | 258081 | /* Figure out how many output pages (nOut) and how many slots per |
| 257858 | 258082 | ** page (nSlot). There are three possibilities: |
| 257859 | 258083 | ** |
| 257860 | 258084 | ** 1. The hash table does not yet exist. In this case the new hash |
| @@ -257875,27 +258099,30 @@ | ||
| 257875 | 258099 | /* Case 1. */ |
| 257876 | 258100 | nOut = 1; |
| 257877 | 258101 | nSlot = MINSLOT; |
| 257878 | 258102 | }else if( pSeg->nPgTombstone==1 ){ |
| 257879 | 258103 | /* Case 2. */ |
| 257880 | - int nElem = (int)fts5GetU32(&pData1->p[4]); | |
| 258104 | + u32 nElem = fts5GetU32(&pData1->p[4]); | |
| 257881 | 258105 | assert( pData1 && iPg1==0 ); |
| 257882 | - nOut = 1; | |
| 257883 | - nSlot = MAX(nElem*4, MINSLOT); | |
| 257884 | - if( nSlot>nSlotPerPage ) nOut = 0; | |
| 258106 | + if( nElem>((u32)nSlotPerPage/4) ){ | |
| 258107 | + nOut = 0; | |
| 258108 | + }else{ | |
| 258109 | + nOut = 1; | |
| 258110 | + nSlot = MAX((i64)nElem*4, MINSLOT); | |
| 258111 | + } | |
| 257885 | 258112 | } |
| 257886 | 258113 | if( nOut==0 ){ |
| 257887 | 258114 | /* Case 3. */ |
| 257888 | - nOut = (pSeg->nPgTombstone * 2 + 1); | |
| 258115 | + nOut = ((i64)pSeg->nPgTombstone * 2 + 1); | |
| 257889 | 258116 | nSlot = nSlotPerPage; |
| 257890 | 258117 | } |
| 257891 | 258118 | |
| 257892 | 258119 | /* Allocate the required array and output pages */ |
| 257893 | 258120 | while( 1 ){ |
| 257894 | 258121 | int res = 0; |
| 257895 | - int ii = 0; | |
| 257896 | - int szPage = 0; | |
| 258122 | + i64 ii = 0; | |
| 258123 | + i64 szPage = 0; | |
| 257897 | 258124 | Fts5Data **apOut = 0; |
| 257898 | 258125 | |
| 257899 | 258126 | /* Allocate space for the new hash table */ |
| 257900 | 258127 | assert( nSlot>=MINSLOT ); |
| 257901 | 258128 | apOut = (Fts5Data**)sqlite3Fts5MallocZero(&p->rc, sizeof(Fts5Data*) * nOut); |
| @@ -258429,11 +258656,11 @@ | ||
| 258429 | 258656 | ){ |
| 258430 | 258657 | |
| 258431 | 258658 | /* Check any rowid-less pages that occur before the current leaf. */ |
| 258432 | 258659 | for(iPg=iPrevLeaf+1; iPg<fts5DlidxIterPgno(pDlidx); iPg++){ |
| 258433 | 258660 | iKey = FTS5_SEGMENT_ROWID(iSegid, iPg); |
| 258434 | - pLeaf = fts5DataRead(p, iKey); | |
| 258661 | + pLeaf = fts5LeafRead(p, iKey); | |
| 258435 | 258662 | if( pLeaf ){ |
| 258436 | 258663 | if( fts5LeafFirstRowidOff(pLeaf)!=0 ) FTS5_CORRUPT_ROWID(p, iKey); |
| 258437 | 258664 | fts5DataRelease(pLeaf); |
| 258438 | 258665 | } |
| 258439 | 258666 | } |
| @@ -258440,11 +258667,11 @@ | ||
| 258440 | 258667 | iPrevLeaf = fts5DlidxIterPgno(pDlidx); |
| 258441 | 258668 | |
| 258442 | 258669 | /* Check that the leaf page indicated by the iterator really does |
| 258443 | 258670 | ** contain the rowid suggested by the same. */ |
| 258444 | 258671 | iKey = FTS5_SEGMENT_ROWID(iSegid, iPrevLeaf); |
| 258445 | - pLeaf = fts5DataRead(p, iKey); | |
| 258672 | + pLeaf = fts5LeafRead(p, iKey); | |
| 258446 | 258673 | if( pLeaf ){ |
| 258447 | 258674 | i64 iRowid; |
| 258448 | 258675 | int iRowidOff = fts5LeafFirstRowidOff(pLeaf); |
| 258449 | 258676 | ASSERT_SZLEAF_OK(pLeaf); |
| 258450 | 258677 | if( iRowidOff>=pLeaf->szLeaf ){ |
| @@ -263040,11 +263267,11 @@ | ||
| 263040 | 263267 | int nArg, /* Number of args */ |
| 263041 | 263268 | sqlite3_value **apUnused /* Function arguments */ |
| 263042 | 263269 | ){ |
| 263043 | 263270 | assert( nArg==0 ); |
| 263044 | 263271 | UNUSED_PARAM2(nArg, apUnused); |
| 263045 | - sqlite3_result_text(pCtx, "fts5: 2026-05-30 10:24:03 7487a1c59d3aaea9f8b2569dca76bbccf21948b1e7bd8a1d841e04382db696f4", -1, SQLITE_TRANSIENT); | |
| 263272 | + sqlite3_result_text(pCtx, "fts5: 2026-06-16 13:43:08 3f3fb9b638f59ad982beafb7c117f24ddd3da612e62c862510805fa672ffae06", -1, SQLITE_TRANSIENT); | |
| 263046 | 263273 | } |
| 263047 | 263274 | |
| 263048 | 263275 | /* |
| 263049 | 263276 | ** Implementation of fts5_locale(LOCALE, TEXT) function. |
| 263050 | 263277 | ** |
| 263051 | 263278 |
| --- 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 | ** 2c605bfb1562d7a3609ad6ffd7446def12f1 with changes in files: |
| 22 | ** |
| 23 | ** |
| 24 | */ |
| 25 | #ifndef SQLITE_AMALGAMATION |
| 26 | #define SQLITE_CORE 1 |
| @@ -467,14 +467,14 @@ | |
| 467 | ** [sqlite3_libversion_number()], [sqlite3_sourceid()], |
| 468 | ** [sqlite_version()] and [sqlite_source_id()]. |
| 469 | */ |
| 470 | #define SQLITE_VERSION "3.54.0" |
| 471 | #define SQLITE_VERSION_NUMBER 3054000 |
| 472 | #define SQLITE_SOURCE_ID "2026-05-30 13:23:25 2c605bfb1562d7a3609ad6ffd7446def12f1ac7084e41b9c6723e998c156501d" |
| 473 | #define SQLITE_SCM_BRANCH "trunk" |
| 474 | #define SQLITE_SCM_TAGS "" |
| 475 | #define SQLITE_SCM_DATETIME "2026-05-30T13:23:25.636Z" |
| 476 | |
| 477 | /* |
| 478 | ** CAPI3REF: Run-Time Library Version Numbers |
| 479 | ** KEYWORDS: sqlite3_version sqlite3_sourceid |
| 480 | ** |
| @@ -4696,11 +4696,12 @@ | |
| 4696 | ** <dd>The maximum number of columns in a table definition or in the |
| 4697 | ** result set of a [SELECT] or the maximum number of columns in an index |
| 4698 | ** or in an ORDER BY or GROUP BY clause.</dd>)^ |
| 4699 | ** |
| 4700 | ** [[SQLITE_LIMIT_EXPR_DEPTH]] ^(<dt>SQLITE_LIMIT_EXPR_DEPTH</dt> |
| 4701 | ** <dd>The maximum depth of the parse tree on any expression.</dd>)^ |
| 4702 | ** |
| 4703 | ** [[SQLITE_LIMIT_PARSER_DEPTH]] ^(<dt>SQLITE_LIMIT_PARSER_DEPTH</dt> |
| 4704 | ** <dd>The maximum depth of the LALR(1) parser stack used to analyze |
| 4705 | ** input SQL statements.</dd>)^ |
| 4706 | ** |
| @@ -4727,11 +4728,12 @@ | |
| 4727 | ** [[SQLITE_LIMIT_VARIABLE_NUMBER]] |
| 4728 | ** ^(<dt>SQLITE_LIMIT_VARIABLE_NUMBER</dt> |
| 4729 | ** <dd>The maximum index number of any [parameter] in an SQL statement.)^ |
| 4730 | ** |
| 4731 | ** [[SQLITE_LIMIT_TRIGGER_DEPTH]] ^(<dt>SQLITE_LIMIT_TRIGGER_DEPTH</dt> |
| 4732 | ** <dd>The maximum depth of recursion for triggers.</dd>)^ |
| 4733 | ** |
| 4734 | ** [[SQLITE_LIMIT_WORKER_THREADS]] ^(<dt>SQLITE_LIMIT_WORKER_THREADS</dt> |
| 4735 | ** <dd>The maximum number of auxiliary worker threads that a single |
| 4736 | ** [prepared statement] may start.</dd>)^ |
| 4737 | ** </dl> |
| @@ -11533,12 +11535,23 @@ | |
| 11533 | ** reopen S as an in-memory database based on the serialization |
| 11534 | ** contained in P. If S is a NULL pointer, the main database is |
| 11535 | ** used. The serialized database P is N bytes in size. M is the size |
| 11536 | ** of the buffer P, which might be larger than N. If M is larger than |
| 11537 | ** N, and the SQLITE_DESERIALIZE_READONLY bit is not set in F, then |
| 11538 | ** SQLite is permitted to add content to the in-memory database as |
| 11539 | ** long as the total size does not exceed M bytes. |
| 11540 | ** |
| 11541 | ** If the SQLITE_DESERIALIZE_FREEONCLOSE bit is set in F, then SQLite will |
| 11542 | ** invoke sqlite3_free() on the serialization buffer when the database |
| 11543 | ** connection closes. If the SQLITE_DESERIALIZE_RESIZEABLE bit is set, then |
| 11544 | ** SQLite will try to increase the buffer size using sqlite3_realloc64() |
| @@ -15809,10 +15822,17 @@ | |
| 15809 | */ |
| 15810 | #ifndef offsetof |
| 15811 | # define offsetof(ST,M) ((size_t)((char*)&((ST*)0)->M - (char*)0)) |
| 15812 | #endif |
| 15813 | |
| 15814 | /* |
| 15815 | ** Work around C99 "flex-array" syntax for pre-C99 compilers, so as |
| 15816 | ** to avoid complaints from -fsanitize=strict-bounds. |
| 15817 | */ |
| 15818 | #if defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L) |
| @@ -20928,10 +20948,11 @@ | |
| 20928 | int nTab; /* Number of previously allocated VDBE cursors */ |
| 20929 | int nMem; /* Number of memory cells used so far */ |
| 20930 | int szOpAlloc; /* Bytes of memory space allocated for Vdbe.aOp[] */ |
| 20931 | int iSelfTab; /* Table associated with an index on expr, or negative |
| 20932 | ** of the base register during check-constraint eval */ |
| 20933 | int nLabel; /* The *negative* of the number of labels used */ |
| 20934 | int nLabelAlloc; /* Number of slots in aLabel */ |
| 20935 | int *aLabel; /* Space to hold the labels */ |
| 20936 | ExprList *pConstExpr;/* Constant expressions */ |
| 20937 | IndexedExpr *pIdxEpr;/* List of expressions used by active indexes */ |
| @@ -33389,12 +33410,12 @@ | |
| 33389 | if( flag_alternateform ){ |
| 33390 | /* For %#q, do unistr()-style backslash escapes for |
| 33391 | ** all control characters, and for backslash itself. |
| 33392 | ** For %#Q, do the same but only if there is at least |
| 33393 | ** one control character. */ |
| 33394 | u32 nBack = 0; |
| 33395 | u32 nCtrl = 0; |
| 33396 | for(k=0; k<i; k++){ |
| 33397 | if( escarg[k]=='\\' ){ |
| 33398 | nBack++; |
| 33399 | }else if( ((u8*)escarg)[k]<=0x1f ){ |
| 33400 | nCtrl++; |
| @@ -39255,10 +39276,26 @@ | |
| 39255 | ****************************************************************************** |
| 39256 | ** |
| 39257 | ** This file contains an experimental VFS layer that operates on a |
| 39258 | ** Key/Value storage engine where both keys and values must be pure |
| 39259 | ** text. |
| 39260 | */ |
| 39261 | /* #include <sqliteInt.h> */ |
| 39262 | #if SQLITE_OS_KV || (SQLITE_OS_UNIX && defined(SQLITE_OS_KV_OPTIONAL)) |
| 39263 | |
| 39264 | /***************************************************************************** |
| @@ -39707,16 +39744,18 @@ | |
| 39707 | } |
| 39708 | if( j+n>nOut ) return -1; |
| 39709 | memset(&aOut[j], 0, n); |
| 39710 | j += n; |
| 39711 | if( c==0 || mult==1 ) break; /* progress stalled if mult==1 */ |
| 39712 | }else{ |
| 39713 | aOut[j] = c<<4; |
| 39714 | c = kvvfsHexValue[aIn[++i]]; |
| 39715 | if( c<0 ) return -1 /* hex bytes are always in pairs */; |
| 39716 | aOut[j++] += c; |
| 39717 | i++; |
| 39718 | } |
| 39719 | } |
| 39720 | return j; |
| 39721 | } |
| 39722 | |
| @@ -40138,10 +40177,22 @@ | |
| 40138 | }else{ |
| 40139 | pFile->isJournal = 0; |
| 40140 | pFile->base.pMethods = &kvvfs_db_io_methods; |
| 40141 | } |
| 40142 | if( !pFile->zClass ){ |
| 40143 | pFile->zClass = zName; |
| 40144 | } |
| 40145 | pFile->aData = sqlite3_malloc64(SQLITE_KVOS_SZ); |
| 40146 | if( pFile->aData==0 ){ |
| 40147 | return SQLITE_NOMEM; |
| @@ -52170,15 +52221,33 @@ | |
| 52170 | # define sqlite3_win_test_unc_locking 0 |
| 52171 | #endif |
| 52172 | |
| 52173 | /* |
| 52174 | ** Return true if the string passed as the only argument is likely |
| 52175 | ** to be a UNC path. In other words, if it starts with "\\". |
| 52176 | */ |
| 52177 | static int winIsUNCPath(const char *zFile){ |
| 52178 | if( zFile[0]=='\\' && zFile[1]=='\\' ){ |
| 52179 | return 1; |
| 52180 | } |
| 52181 | return sqlite3_win_test_unc_locking; |
| 52182 | } |
| 52183 | |
| 52184 | /* |
| @@ -56926,26 +56995,28 @@ | |
| 56926 | szBulk = -1024 * (i64)pcache1.nInitPage; |
| 56927 | } |
| 56928 | if( szBulk > pCache->szAlloc*(i64)pCache->nMax ){ |
| 56929 | szBulk = pCache->szAlloc*(i64)pCache->nMax; |
| 56930 | } |
| 56931 | zBulk = pCache->pBulk = sqlite3Malloc( szBulk ); |
| 56932 | sqlite3EndBenignMalloc(); |
| 56933 | if( zBulk ){ |
| 56934 | int nBulk = sqlite3MallocSize(zBulk)/pCache->szAlloc; |
| 56935 | do{ |
| 56936 | PgHdr1 *pX = (PgHdr1*)&zBulk[pCache->szPage]; |
| 56937 | pX->page.pBuf = zBulk; |
| 56938 | pX->page.pExtra = (u8*)pX + ROUND8(sizeof(*pX)); |
| 56939 | assert( EIGHT_BYTE_ALIGNMENT( pX->page.pExtra ) ); |
| 56940 | pX->isBulkLocal = 1; |
| 56941 | pX->isAnchor = 0; |
| 56942 | pX->pNext = pCache->pFree; |
| 56943 | pX->pLruPrev = 0; /* Initializing this saves a valgrind error */ |
| 56944 | pCache->pFree = pX; |
| 56945 | zBulk += pCache->szAlloc; |
| 56946 | }while( --nBulk ); |
| 56947 | } |
| 56948 | return pCache->pFree!=0; |
| 56949 | } |
| 56950 | |
| 56951 | /* |
| @@ -59839,73 +59910,84 @@ | |
| 59839 | #define pager_set_pagehash(X) |
| 59840 | #define CHECK_PAGE(x) |
| 59841 | #endif /* SQLITE_CHECK_PAGES */ |
| 59842 | |
| 59843 | /* |
| 59844 | ** When this is called the journal file for pager pPager must be open. |
| 59845 | ** This function attempts to read a super-journal file name from the |
| 59846 | ** end of the file and, if successful, copies it into memory supplied |
| 59847 | ** by the caller. See comments above writeSuperJournal() for the format |
| 59848 | ** used to store a super-journal file name at the end of a journal file. |
| 59849 | ** |
| 59850 | ** zSuper must point to a buffer of at least nSuper bytes allocated by |
| 59851 | ** the caller. This should be sqlite3_vfs.mxPathname+1 (to ensure there is |
| 59852 | ** enough space to write the super-journal name). If the super-journal |
| 59853 | ** name in the journal is longer than nSuper bytes (including a |
| 59854 | ** nul-terminator), then this is handled as if no super-journal name |
| 59855 | ** were present in the journal. |
| 59856 | ** |
| 59857 | ** If a super-journal file name is present at the end of the journal |
| 59858 | ** file, then it is copied into the buffer pointed to by zSuper. A |
| 59859 | ** nul-terminator byte is appended to the buffer following the |
| 59860 | ** super-journal file name. |
| 59861 | ** |
| 59862 | ** If it is determined that no super-journal file name is present |
| 59863 | ** zSuper[0] is set to 0 and SQLITE_OK returned. |
| 59864 | ** |
| 59865 | ** If an error occurs while reading from the journal file, an SQLite |
| 59866 | ** error code is returned. |
| 59867 | */ |
| 59868 | static int readSuperJournal(sqlite3_file *pJrnl, char *zSuper, u64 nSuper){ |
| 59869 | int rc; /* Return code */ |
| 59870 | u32 len; /* Length in bytes of super-journal name */ |
| 59871 | i64 szJ; /* Total size in bytes of journal file pJrnl */ |
| 59872 | u32 cksum; /* MJ checksum value read from journal */ |
| 59873 | u32 u; /* Unsigned loop counter */ |
| 59874 | unsigned char aMagic[8]; /* A buffer to hold the magic header */ |
| 59875 | zSuper[0] = '\0'; |
| 59876 | |
| 59877 | if( SQLITE_OK!=(rc = sqlite3OsFileSize(pJrnl, &szJ)) |
| 59878 | || szJ<16 |
| 59879 | || SQLITE_OK!=(rc = read32bits(pJrnl, szJ-16, &len)) |
| 59880 | || len>=nSuper |
| 59881 | || len>szJ-16 |
| 59882 | || len==0 |
| 59883 | || SQLITE_OK!=(rc = read32bits(pJrnl, szJ-12, &cksum)) |
| 59884 | || SQLITE_OK!=(rc = sqlite3OsRead(pJrnl, aMagic, 8, szJ-8)) |
| 59885 | || memcmp(aMagic, aJournalMagic, 8) |
| 59886 | || SQLITE_OK!=(rc = sqlite3OsRead(pJrnl, zSuper, len, szJ-16-len)) |
| 59887 | ){ |
| 59888 | return rc; |
| 59889 | } |
| 59890 | |
| 59891 | /* See if the checksum matches the super-journal name */ |
| 59892 | for(u=0; u<len; u++){ |
| 59893 | cksum -= zSuper[u]; |
| 59894 | } |
| 59895 | if( cksum ){ |
| 59896 | /* If the checksum doesn't add up, then one or more of the disk sectors |
| 59897 | ** containing the super-journal filename is corrupted. This means |
| 59898 | ** definitely roll back, so just return SQLITE_OK and report a (nul) |
| 59899 | ** super-journal filename. |
| 59900 | */ |
| 59901 | len = 0; |
| 59902 | } |
| 59903 | zSuper[len] = '\0'; |
| 59904 | zSuper[len+1] = '\0'; |
| 59905 | |
| 59906 | return SQLITE_OK; |
| 59907 | } |
| 59908 | |
| 59909 | /* |
| 59910 | ** Return the offset of the sector boundary at or immediately |
| 59911 | ** following the value in pPager->journalOff, assuming a sector |
| @@ -61106,13 +61188,11 @@ | |
| 61106 | sqlite3_file *pSuper; /* Malloc'd super-journal file descriptor */ |
| 61107 | sqlite3_file *pJournal; /* Malloc'd child-journal file descriptor */ |
| 61108 | char *zSuperJournal = 0; /* Contents of super-journal file */ |
| 61109 | i64 nSuperJournal; /* Size of super-journal file */ |
| 61110 | char *zJournal; /* Pointer to one journal within MJ file */ |
| 61111 | char *zSuperPtr; /* Space to hold super-journal filename */ |
| 61112 | char *zFree = 0; /* Free this buffer */ |
| 61113 | i64 nSuperPtr; /* Amount of space allocated to zSuperPtr[] */ |
| 61114 | |
| 61115 | /* Allocate space for both the pJournal and pSuper file descriptors. |
| 61116 | ** If successful, open the super-journal file for reading. |
| 61117 | */ |
| 61118 | pSuper = (sqlite3_file *)sqlite3MallocZero(2 * (i64)pVfs->szOsFile); |
| @@ -61131,22 +61211,20 @@ | |
| 61131 | ** sufficient space (in zSuperPtr) to hold the names of super-journal |
| 61132 | ** files extracted from regular rollback-journals. |
| 61133 | */ |
| 61134 | rc = sqlite3OsFileSize(pSuper, &nSuperJournal); |
| 61135 | if( rc!=SQLITE_OK ) goto delsuper_out; |
| 61136 | nSuperPtr = 1 + (i64)pVfs->mxPathname; |
| 61137 | assert( nSuperJournal>=0 && nSuperPtr>0 ); |
| 61138 | zFree = sqlite3Malloc(4 + nSuperJournal + 2 + nSuperPtr + 2); |
| 61139 | if( !zFree ){ |
| 61140 | rc = SQLITE_NOMEM_BKPT; |
| 61141 | goto delsuper_out; |
| 61142 | }else{ |
| 61143 | assert( nSuperJournal<=0x7fffffff ); |
| 61144 | } |
| 61145 | zFree[0] = zFree[1] = zFree[2] = zFree[3] = 0; |
| 61146 | zSuperJournal = &zFree[4]; |
| 61147 | zSuperPtr = &zSuperJournal[nSuperJournal+2]; |
| 61148 | rc = sqlite3OsRead(pSuper, zSuperJournal, (int)nSuperJournal, 0); |
| 61149 | if( rc!=SQLITE_OK ) goto delsuper_out; |
| 61150 | zSuperJournal[nSuperJournal] = 0; |
| 61151 | zSuperJournal[nSuperJournal+1] = 0; |
| 61152 | |
| @@ -61156,10 +61234,12 @@ | |
| 61156 | rc = sqlite3OsAccess(pVfs, zJournal, SQLITE_ACCESS_EXISTS, &exists); |
| 61157 | if( rc!=SQLITE_OK ){ |
| 61158 | goto delsuper_out; |
| 61159 | } |
| 61160 | if( exists ){ |
| 61161 | /* One of the journals pointed to by the super-journal exists. |
| 61162 | ** Open it and check if it points at the super-journal. If |
| 61163 | ** so, return without deleting the super-journal file. |
| 61164 | ** NB: zJournal is really a MAIN_JOURNAL. But call it a |
| 61165 | ** SUPER_JOURNAL here so that the VFS will not send the zJournal |
| @@ -61170,17 +61250,19 @@ | |
| 61170 | rc = sqlite3OsOpen(pVfs, zJournal, pJournal, flags, 0); |
| 61171 | if( rc!=SQLITE_OK ){ |
| 61172 | goto delsuper_out; |
| 61173 | } |
| 61174 | |
| 61175 | rc = readSuperJournal(pJournal, zSuperPtr, nSuperPtr); |
| 61176 | sqlite3OsClose(pJournal); |
| 61177 | if( rc!=SQLITE_OK ){ |
| 61178 | goto delsuper_out; |
| 61179 | } |
| 61180 | |
| 61181 | c = zSuperPtr[0]!=0 && strcmp(zSuperPtr, zSuper)==0; |
| 61182 | if( c ){ |
| 61183 | /* We have a match. Do not delete the super-journal file. */ |
| 61184 | goto delsuper_out; |
| 61185 | } |
| 61186 | } |
| @@ -61391,23 +61473,15 @@ | |
| 61391 | |
| 61392 | /* Read the super-journal name from the journal, if it is present. |
| 61393 | ** If a super-journal file name is specified, but the file is not |
| 61394 | ** present on disk, then the journal is not hot and does not need to be |
| 61395 | ** played back. |
| 61396 | ** |
| 61397 | ** TODO: Technically the following is an error because it assumes that |
| 61398 | ** buffer Pager.pTmpSpace is (mxPathname+1) bytes or larger. i.e. that |
| 61399 | ** ((pPager->pageSize+8) >= pPager->pVfs->mxPathname+1). Using os_unix.c, |
| 61400 | ** mxPathname is 512, which is the same as the minimum allowable value |
| 61401 | ** for pageSize, and so this assumption holds. But it might not for some |
| 61402 | ** custom VFS. */ |
| 61403 | zSuper = pPager->pTmpSpace; |
| 61404 | rc = readSuperJournal(pPager->jfd, zSuper, 1+(i64)pPager->pVfs->mxPathname); |
| 61405 | if( rc==SQLITE_OK && zSuper[0] ){ |
| 61406 | rc = sqlite3OsAccess(pVfs, zSuper, SQLITE_ACCESS_EXISTS, &res); |
| 61407 | } |
| 61408 | zSuper = 0; |
| 61409 | if( rc!=SQLITE_OK || !res ){ |
| 61410 | goto end_playback; |
| 61411 | } |
| 61412 | pPager->journalOff = 0; |
| 61413 | needPagerReset = isHot; |
| @@ -61532,34 +61606,24 @@ | |
| 61532 | ** problems for other processes at some point in the future. So, just |
| 61533 | ** in case this has happened, clear the changeCountDone flag now. |
| 61534 | */ |
| 61535 | pPager->changeCountDone = pPager->tempFile; |
| 61536 | |
| 61537 | if( rc==SQLITE_OK ){ |
| 61538 | /* Leave 4 bytes of space before the super-journal filename in memory. |
| 61539 | ** This is because it may end up being passed to sqlite3OsOpen(), in |
| 61540 | ** which case it requires 4 0x00 bytes in memory immediately before |
| 61541 | ** the filename. */ |
| 61542 | zSuper = &pPager->pTmpSpace[4]; |
| 61543 | rc = readSuperJournal(pPager->jfd, zSuper, 1+(i64)pPager->pVfs->mxPathname); |
| 61544 | testcase( rc!=SQLITE_OK ); |
| 61545 | } |
| 61546 | if( rc==SQLITE_OK |
| 61547 | && (pPager->eState>=PAGER_WRITER_DBMOD || pPager->eState==PAGER_OPEN) |
| 61548 | ){ |
| 61549 | rc = sqlite3PagerSync(pPager, 0); |
| 61550 | } |
| 61551 | if( rc==SQLITE_OK ){ |
| 61552 | rc = pager_end_transaction(pPager, zSuper[0]!='\0', 0); |
| 61553 | testcase( rc!=SQLITE_OK ); |
| 61554 | } |
| 61555 | if( rc==SQLITE_OK && zSuper[0] && res ){ |
| 61556 | /* If there was a super-journal and this routine will return success, |
| 61557 | ** see if it is possible to delete the super-journal. |
| 61558 | */ |
| 61559 | assert( zSuper==&pPager->pTmpSpace[4] ); |
| 61560 | memset(pPager->pTmpSpace, 0, 4); |
| 61561 | rc = pager_delsuper(pPager, zSuper); |
| 61562 | testcase( rc!=SQLITE_OK ); |
| 61563 | } |
| 61564 | if( isHot && nPlayback ){ |
| 61565 | sqlite3_log(SQLITE_NOTICE_RECOVER_ROLLBACK, "recovered %d pages from %s", |
| @@ -61568,10 +61632,11 @@ | |
| 61568 | |
| 61569 | /* The Pager.sectorSize variable may have been updated while rolling |
| 61570 | ** back a journal created by a process with a different sector size |
| 61571 | ** value. Reset it to the correct value for this process. |
| 61572 | */ |
| 61573 | setSectorSize(pPager); |
| 61574 | return rc; |
| 61575 | } |
| 61576 | |
| 61577 | |
| @@ -67426,10 +67491,16 @@ | |
| 67426 | */ |
| 67427 | pgno = sqlite3Get4byte(&aFrame[0]); |
| 67428 | if( pgno==0 ){ |
| 67429 | return 0; |
| 67430 | } |
| 67431 | |
| 67432 | /* A frame is only valid if a checksum of the WAL header, |
| 67433 | ** all prior frames, the first 16 bytes of this frame-header, |
| 67434 | ** and the frame-data matches the checksum in the last 8 |
| 67435 | ** bytes of this frame-header. |
| @@ -69281,11 +69352,11 @@ | |
| 69281 | goto begin_unreliable_shm_out; |
| 69282 | } |
| 69283 | |
| 69284 | /* Allocate a buffer to read frames into */ |
| 69285 | assert( (pWal->szPage & (pWal->szPage-1))==0 ); |
| 69286 | assert( pWal->szPage>=512 && pWal->szPage<=65536 ); |
| 69287 | szFrame = pWal->szPage + WAL_FRAME_HDRSIZE; |
| 69288 | aFrame = (u8 *)sqlite3_malloc64(szFrame); |
| 69289 | if( aFrame==0 ){ |
| 69290 | rc = SQLITE_NOMEM_BKPT; |
| 69291 | goto begin_unreliable_shm_out; |
| @@ -83086,10 +83157,15 @@ | |
| 83086 | if( pc+info.nSize>usableSize ){ |
| 83087 | checkAppendMsg(pCheck, "Extends off end of page"); |
| 83088 | doCoverageCheck = 0; |
| 83089 | continue; |
| 83090 | } |
| 83091 | |
| 83092 | /* Check for integer primary key out of range */ |
| 83093 | if( pPage->intKey ){ |
| 83094 | if( keyCanBeEqual ? (info.nKey > maxKey) : (info.nKey >= maxKey) ){ |
| 83095 | checkAppendMsg(pCheck, "Rowid %lld out of order", info.nKey); |
| @@ -84596,12 +84672,12 @@ | |
| 84596 | /* Work-around for GCC bug or bugs: |
| 84597 | ** https://gcc.gnu.org/bugzilla/show_bug.cgi?id=96270 |
| 84598 | ** https://gcc.gnu.org/bugzilla/show_bug.cgi?id=114659 |
| 84599 | ** The problem appears to be fixed in GCC 15 */ |
| 84600 | i64 x; |
| 84601 | assert( (MEM_Str&~p->flags)*4==sizeof(x) ); |
| 84602 | memcpy(&x, (char*)&p->u, (MEM_Str&~p->flags)*4); |
| 84603 | p->n = sqlite3Int64ToText(x, zBuf); |
| 84604 | #else |
| 84605 | p->n = sqlite3Int64ToText(p->u.i, zBuf); |
| 84606 | #endif |
| 84607 | if( p->flags & MEM_IntReal ){ |
| @@ -95448,11 +95524,11 @@ | |
| 95448 | QueryPerformanceCounter(&tm); |
| 95449 | return (sqlite3_uint64)tm.QuadPart; |
| 95450 | } |
| 95451 | |
| 95452 | #elif !defined(__STRICT_ANSI__) && defined(__GNUC__) && \ |
| 95453 | (defined(i386) || defined(__i386__) || defined(_M_IX86)) |
| 95454 | |
| 95455 | __inline__ sqlite_uint64 sqlite3Hwtime(void){ |
| 95456 | unsigned int lo, hi; |
| 95457 | __asm__ __volatile__ ("rdtsc" : "=a" (lo), "=d" (hi)); |
| 95458 | return (sqlite_uint64)hi << 32 | lo; |
| @@ -103037,11 +103113,11 @@ | |
| 103037 | if( pFrame ) break; |
| 103038 | } |
| 103039 | |
| 103040 | if( p->nFrame>=db->aLimit[SQLITE_LIMIT_TRIGGER_DEPTH] ){ |
| 103041 | rc = SQLITE_ERROR; |
| 103042 | sqlite3VdbeError(p, "too many levels of trigger recursion"); |
| 103043 | goto abort_due_to_error; |
| 103044 | } |
| 103045 | |
| 103046 | /* Register pRt is used to store the memory required to save the state |
| 103047 | ** of the current program, and the memory required at runtime to execute |
| @@ -110164,11 +110240,11 @@ | |
| 110164 | /* |
| 110165 | ** cnt==0 means there was not match. |
| 110166 | ** cnt>1 means there were two or more matches. |
| 110167 | ** |
| 110168 | ** cnt==0 is always an error. cnt>1 is often an error, but might |
| 110169 | ** be multiple matches for a NATURAL LEFT JOIN or a LEFT JOIN USING. |
| 110170 | */ |
| 110171 | assert( pFJMatch==0 || cnt>0 ); |
| 110172 | assert( !ExprHasProperty(pExpr, EP_xIsSelect|EP_IntValue) ); |
| 110173 | if( cnt!=1 ){ |
| 110174 | const char *zErr; |
| @@ -110247,12 +110323,21 @@ | |
| 110247 | pExpr->op = eNewExprOp; |
| 110248 | lookupname_end: |
| 110249 | if( cnt==1 ){ |
| 110250 | assert( pNC!=0 ); |
| 110251 | #ifndef SQLITE_OMIT_AUTHORIZATION |
| 110252 | if( db->xAuth && (pExpr->op==TK_COLUMN || pExpr->op==TK_TRIGGER) ){ |
| 110253 | sqlite3AuthRead(pParse, pExpr, pSchema, pNC->pSrcList); |
| 110254 | } |
| 110255 | #endif |
| 110256 | /* Increment the nRef value on all name contexts from TopNC up to |
| 110257 | ** the point where the name matched. */ |
| 110258 | for(;;){ |
| @@ -115930,10 +116015,11 @@ | |
| 115930 | if( i!=nVector ){ |
| 115931 | /* Need to reorder the LHS fields according to aiMap */ |
| 115932 | int rLhsOrig = rLhs; |
| 115933 | rLhs = sqlite3GetTempRange(pParse, nVector); |
| 115934 | for(i=0; i<nVector; i++){ |
| 115935 | sqlite3VdbeAddOp3(v, OP_Copy, rLhsOrig+i, rLhs+aiMap[i], 0); |
| 115936 | } |
| 115937 | sqlite3ReleaseTempReg(pParse, rLhsOrig); |
| 115938 | } |
| 115939 | } |
| @@ -115950,11 +116036,12 @@ | |
| 115950 | } |
| 115951 | for(i=0; i<nVector; i++){ |
| 115952 | Expr *p = sqlite3VectorFieldSubexpr(pExpr->pLeft, i); |
| 115953 | if( pParse->nErr ) goto sqlite3ExprCodeIN_oom_error; |
| 115954 | if( sqlite3ExprCanBeNull(p) ){ |
| 115955 | sqlite3VdbeAddOp2(v, OP_IsNull, rLhs+i, destStep2); |
| 115956 | VdbeCoverage(v); |
| 115957 | } |
| 115958 | } |
| 115959 | |
| 115960 | /* Step 3. The LHS is now known to be non-NULL. Do the binary search |
| @@ -116024,13 +116111,23 @@ | |
| 116024 | for(i=0; i<nVector; i++){ |
| 116025 | Expr *p; |
| 116026 | CollSeq *pColl; |
| 116027 | int r3 = sqlite3GetTempReg(pParse); |
| 116028 | p = sqlite3VectorFieldSubexpr(pLeft, i); |
| 116029 | pColl = sqlite3ExprCollSeq(pParse, p); |
| 116030 | sqlite3VdbeAddOp3(v, OP_Column, iTab, i, r3); |
| 116031 | sqlite3VdbeAddOp4(v, OP_Ne, rLhs+i, destNotNull, r3, |
| 116032 | (void*)pColl, P4_COLLSEQ); |
| 116033 | VdbeCoverage(v); |
| 116034 | sqlite3ReleaseTempReg(pParse, r3); |
| 116035 | } |
| 116036 | sqlite3VdbeAddOp2(v, OP_Goto, 0, destIfNull); |
| @@ -124365,21 +124462,21 @@ | |
| 124365 | }else{ |
| 124366 | nIdxCol = pIdx->nColumn; |
| 124367 | } |
| 124368 | pIdx->nSampleCol = nIdxCol; |
| 124369 | pIdx->mxSample = nSample; |
| 124370 | nByte = ROUND8(sizeof(IndexSample) * nSample); |
| 124371 | nByte += sizeof(tRowcnt) * nIdxCol * 3 * nSample; |
| 124372 | nByte += nIdxCol * sizeof(tRowcnt); /* Space for Index.aAvgEq[] */ |
| 124373 | |
| 124374 | pIdx->aSample = sqlite3DbMallocZero(db, nByte); |
| 124375 | if( pIdx->aSample==0 ){ |
| 124376 | sqlite3_finalize(pStmt); |
| 124377 | return SQLITE_NOMEM_BKPT; |
| 124378 | } |
| 124379 | pPtr = (u8*)pIdx->aSample; |
| 124380 | pPtr += ROUND8(nSample*sizeof(pIdx->aSample[0])); |
| 124381 | pSpace = (tRowcnt*)pPtr; |
| 124382 | assert( EIGHT_BYTE_ALIGNMENT( pSpace ) ); |
| 124383 | pIdx->aAvgEq = pSpace; pSpace += nIdxCol; |
| 124384 | pIdx->pTable->tabFlags |= TF_HasStat4; |
| 124385 | for(i=0; i<nSample; i++){ |
| @@ -133231,13 +133328,22 @@ | |
| 133231 | x.nUsed = 0; |
| 133232 | x.apArg = argv+1; |
| 133233 | sqlite3StrAccumInit(&str, db, 0, 0, db->aLimit[SQLITE_LIMIT_LENGTH]); |
| 133234 | str.printfFlags = SQLITE_PRINTF_SQLFUNC; |
| 133235 | sqlite3_str_appendf(&str, zFormat, &x); |
| 133236 | n = str.nChar; |
| 133237 | sqlite3_result_text(context, sqlite3StrAccumFinish(&str), n, |
| 133238 | SQLITE_DYNAMIC); |
| 133239 | } |
| 133240 | } |
| 133241 | |
| 133242 | /* |
| 133243 | ** Implementation of the substr() function. |
| @@ -134405,11 +134511,11 @@ | |
| 134405 | int nStr; /* Size of zStr */ |
| 134406 | int nPattern; /* Size of zPattern */ |
| 134407 | int nRep; /* Size of zRep */ |
| 134408 | i64 nOut; /* Maximum size of zOut */ |
| 134409 | int loopLimit; /* Last zStr[] that might match zPattern[] */ |
| 134410 | int i, j; /* Loop counters */ |
| 134411 | unsigned cntExpand; /* Number zOut expansions */ |
| 134412 | sqlite3 *db = sqlite3_context_db_handle(context); |
| 134413 | |
| 134414 | assert( argc==3 ); |
| 134415 | UNUSED_PARAMETER(argc); |
| @@ -135859,57 +135965,95 @@ | |
| 135859 | ** from the standard library because: |
| 135860 | ** |
| 135861 | ** (1) To avoid a dependency on qsort() |
| 135862 | ** (2) To avoid the function call to the comparison routine for each |
| 135863 | ** comparison. |
| 135864 | */ |
| 135865 | static void percentSort(double *a, unsigned int n){ |
| 135866 | int iLt; /* Entries before a[iLt] are less than rPivot */ |
| 135867 | int iGt; /* Entries at or after a[iGt] are greater than rPivot */ |
| 135868 | int i; /* Loop counter */ |
| 135869 | double rPivot; /* The pivot value */ |
| 135870 | |
| 135871 | assert( n>=2 ); |
| 135872 | if( a[0]>a[n-1] ){ |
| 135873 | SWAP_DOUBLE(a[0],a[n-1]) |
| 135874 | } |
| 135875 | if( n==2 ) return; |
| 135876 | iGt = n-1; |
| 135877 | i = n/2; |
| 135878 | if( a[0]>a[i] ){ |
| 135879 | SWAP_DOUBLE(a[0],a[i]) |
| 135880 | }else if( a[i]>a[iGt] ){ |
| 135881 | SWAP_DOUBLE(a[i],a[iGt]) |
| 135882 | } |
| 135883 | if( n==3 ) return; |
| 135884 | rPivot = a[i]; |
| 135885 | iLt = i = 1; |
| 135886 | do{ |
| 135887 | if( a[i]<rPivot ){ |
| 135888 | if( i>iLt ) SWAP_DOUBLE(a[i],a[iLt]) |
| 135889 | iLt++; |
| 135890 | i++; |
| 135891 | }else if( a[i]>rPivot ){ |
| 135892 | do{ |
| 135893 | iGt--; |
| 135894 | }while( iGt>i && a[iGt]>rPivot ); |
| 135895 | SWAP_DOUBLE(a[i],a[iGt]) |
| 135896 | }else{ |
| 135897 | i++; |
| 135898 | } |
| 135899 | }while( i<iGt ); |
| 135900 | if( iLt>=2 ) percentSort(a, iLt); |
| 135901 | if( n-iGt>=2 ) percentSort(a+iGt, n-iGt); |
| 135902 | |
| 135903 | /* Uncomment for testing */ |
| 135904 | #if 0 |
| 135905 | for(i=0; i<n-1; i++){ |
| 135906 | assert( a[i]<=a[i+1] ); |
| 135907 | } |
| 135908 | #endif |
| 135909 | } |
| 135910 | |
| 135911 | |
| 135912 | /* |
| 135913 | ** The "inverse" function for percentile(Y,P) is called to remove a |
| 135914 | ** row that was previously inserted by "step". |
| 135915 | */ |
| @@ -135939,11 +136083,11 @@ | |
| 135939 | if( percentIsInfinity(y) ){ |
| 135940 | return; |
| 135941 | } |
| 135942 | if( p->bSorted==0 ){ |
| 135943 | assert( p->nUsed>1 ); |
| 135944 | percentSort(p->a, p->nUsed); |
| 135945 | p->bSorted = 1; |
| 135946 | } |
| 135947 | p->bKeepSorted = 1; |
| 135948 | |
| 135949 | /* Find and remove the row */ |
| @@ -135968,17 +136112,21 @@ | |
| 135968 | double ix, vx; |
| 135969 | p = (Percentile*)sqlite3_aggregate_context(pCtx, 0); |
| 135970 | if( p==0 ) return; |
| 135971 | if( p->a==0 ) return; |
| 135972 | if( p->nUsed ){ |
| 135973 | if( p->bSorted==0 ){ |
| 135974 | assert( p->nUsed>1 ); |
| 135975 | percentSort(p->a, p->nUsed); |
| 135976 | p->bSorted = 1; |
| 135977 | } |
| 135978 | ix = p->rPct*(p->nUsed-1); |
| 135979 | i1 = (unsigned)ix; |
| 135980 | if( settings & 1 ){ |
| 135981 | vx = p->a[i1]; |
| 135982 | }else{ |
| 135983 | i2 = ix==(double)i1 || i1==p->nUsed-1 ? i1 : i1+1; |
| 135984 | v1 = p->a[i1]; |
| @@ -150488,10 +150636,17 @@ | |
| 150488 | SQLITE_PRIVATE Table *sqlite3ResultSetOfSelect(Parse *pParse, Select *pSelect, char aff){ |
| 150489 | Table *pTab; |
| 150490 | sqlite3 *db = pParse->db; |
| 150491 | u64 savedFlags; |
| 150492 | |
| 150493 | savedFlags = db->flags; |
| 150494 | db->flags &= ~(u64)SQLITE_FullColNames; |
| 150495 | db->flags |= SQLITE_ShortColNames; |
| 150496 | sqlite3SelectPrep(pParse, pSelect, 0); |
| 150497 | db->flags = savedFlags; |
| @@ -150509,10 +150664,12 @@ | |
| 150509 | pTab->iPKey = -1; |
| 150510 | if( db->mallocFailed ){ |
| 150511 | sqlite3DeleteTable(db, pTab); |
| 150512 | return 0; |
| 150513 | } |
| 150514 | return pTab; |
| 150515 | } |
| 150516 | |
| 150517 | /* |
| 150518 | ** Get a VDBE for the given parser context. Create a new one if necessary. |
| @@ -155479,12 +155636,15 @@ | |
| 155479 | ** Then, if CheckOnCtx.iJoin indicates that this expression is part of an |
| 155480 | ** ON clause from that SrcList (i.e. if iJoin is non-zero), check that it |
| 155481 | ** does not refer to a table to the right of CheckOnCtx.iJoin. */ |
| 155482 | do { |
| 155483 | SrcList *pSrc = pCtx->pSrc; |
| 155484 | int iTab = pExpr->iTable; |
| 155485 | if( iTab>=pSrc->a[0].iCursor && iTab<=pSrc->a[pSrc->nSrc-1].iCursor ){ |
| 155486 | if( pCtx->iJoin && iTab>pCtx->iJoin ){ |
| 155487 | sqlite3ErrorMsg(pWalker->pParse, |
| 155488 | "%s references tables to its right", |
| 155489 | (pCtx->bFuncArg ? "table-function argument" : "ON clause") |
| 155490 | ); |
| @@ -158449,22 +158609,36 @@ | |
| 158449 | Parse *pParse, /* Current parse context */ |
| 158450 | Trigger *pTrigger, /* Trigger to code */ |
| 158451 | Table *pTab, /* The table pTrigger is attached to */ |
| 158452 | int orconf /* ON CONFLICT policy to code trigger program with */ |
| 158453 | ){ |
| 158454 | Parse *pTop = sqlite3ParseToplevel(pParse); |
| 158455 | sqlite3 *db = pParse->db; /* Database handle */ |
| 158456 | TriggerPrg *pPrg; /* Value to return */ |
| 158457 | Expr *pWhen = 0; /* Duplicate of trigger WHEN expression */ |
| 158458 | Vdbe *v; /* Temporary VM */ |
| 158459 | NameContext sNC; /* Name context for sub-vdbe */ |
| 158460 | SubProgram *pProgram = 0; /* Sub-vdbe for trigger program */ |
| 158461 | int iEndTrigger = 0; /* Label to jump to if WHEN is false */ |
| 158462 | Parse sSubParse; /* Parse context for sub-vdbe */ |
| 158463 | |
| 158464 | assert( pTrigger->zName==0 || pTab==tableOfTrigger(pTrigger) ); |
| 158465 | assert( pTop->pVdbe ); |
| 158466 | |
| 158467 | /* Allocate the TriggerPrg and SubProgram objects. To ensure that they |
| 158468 | ** are freed if an error occurs, link them into the Parse.pTriggerPrg |
| 158469 | ** list of the top-level Parse object sooner rather than later. */ |
| 158470 | pPrg = sqlite3DbMallocZero(db, sizeof(TriggerPrg)); |
| @@ -160462,11 +160636,12 @@ | |
| 160462 | ** So we have to make a copy before passing it down into sqlite3Update() */ |
| 160463 | pSrc = sqlite3SrcListDup(db, pTop->pUpsertSrc, 0); |
| 160464 | /* excluded.* columns of type REAL need to be converted to a hard real */ |
| 160465 | for(i=0; i<pTab->nCol; i++){ |
| 160466 | if( pTab->aCol[i].affinity==SQLITE_AFF_REAL ){ |
| 160467 | sqlite3VdbeAddOp1(v, OP_RealAffinity, pTop->regData+i); |
| 160468 | } |
| 160469 | } |
| 160470 | sqlite3Update(pParse, pSrc, sqlite3ExprListDup(db,pUpsert->pUpsertSet,0), |
| 160471 | sqlite3ExprDup(db,pUpsert->pUpsertWhere,0), OE_Abort, 0, 0, pUpsert); |
| 160472 | VdbeNoopComment((v, "End DO UPDATE of UPSERT")); |
| @@ -165680,10 +165855,11 @@ | |
| 165680 | assert( (pTerm->eOperator & (WO_OR|WO_AND))==0 ); |
| 165681 | pAlt = sqlite3WhereFindTerm(pWC, iCur, pTerm->u.x.leftColumn, notReady, |
| 165682 | WO_EQ|WO_IN|WO_IS, 0); |
| 165683 | if( pAlt==0 ) continue; |
| 165684 | if( pAlt->wtFlags & (TERM_CODED) ) continue; |
| 165685 | if( (pAlt->eOperator & WO_IN) |
| 165686 | && ExprUseXSelect(pAlt->pExpr) |
| 165687 | && (pAlt->pExpr->x.pSelect->pEList->nExpr>1) |
| 165688 | ){ |
| 165689 | continue; |
| @@ -166433,11 +166609,14 @@ | |
| 166433 | ** Mark term iChild as being a child of term iParent |
| 166434 | */ |
| 166435 | static void markTermAsChild(WhereClause *pWC, int iChild, int iParent){ |
| 166436 | pWC->a[iChild].iParent = iParent; |
| 166437 | pWC->a[iChild].truthProb = pWC->a[iParent].truthProb; |
| 166438 | pWC->a[iParent].nChild++; |
| 166439 | } |
| 166440 | |
| 166441 | /* |
| 166442 | ** Return the N-th AND-connected subterm of pTerm. Or if pTerm is not |
| 166443 | ** a conjunction, then return just pTerm when N==0. If N is exceeds |
| @@ -166872,21 +167051,22 @@ | |
| 166872 | ** 1. The SQLITE_Transitive optimization must be enabled |
| 166873 | ** 2. Must be either an == or an IS operator |
| 166874 | ** 3. Not originating in the ON clause of an OUTER JOIN |
| 166875 | ** 4. The operator is not IS or else the query does not contain RIGHT JOIN |
| 166876 | ** 5. The affinities of A and B must be compatible |
| 166877 | ** 6. Both operands use the same collating sequence |
| 166878 | ** If this routine returns TRUE, that means that the RHS can be substituted |
| 166879 | ** for the LHS anyplace else in the WHERE clause where the LHS column occurs. |
| 166880 | ** This is an optimization. No harm comes from returning 0. But if 1 is |
| 166881 | ** returned when it should not be, then incorrect answers might result. |
| 166882 | */ |
| 166883 | static int termIsEquivalence(Parse *pParse, Expr *pExpr, SrcList *pSrc){ |
| 166884 | char aff1, aff2; |
| 166885 | if( !OptimizationEnabled(pParse->db, SQLITE_Transitive) ) return 0; /* (1) */ |
| 166886 | if( pExpr->op!=TK_EQ && pExpr->op!=TK_IS ) return 0; /* (2) */ |
| 166887 | if( ExprHasProperty(pExpr, EP_OuterON) ) return 0; /* (3) */ |
| 166888 | assert( pSrc!=0 ); |
| 166889 | if( pExpr->op==TK_IS |
| 166890 | && pSrc->nSrc>=2 |
| 166891 | && (pSrc->a[0].fg.jointype & JT_LTORJ)!=0 |
| 166892 | ){ |
| @@ -167209,10 +167389,11 @@ | |
| 167209 | static const u8 ops[] = {TK_GE, TK_LE}; |
| 167210 | assert( ExprUseXList(pExpr) ); |
| 167211 | pList = pExpr->x.pList; |
| 167212 | assert( pList!=0 ); |
| 167213 | assert( pList->nExpr==2 ); |
| 167214 | for(i=0; i<2; i++){ |
| 167215 | Expr *pNewExpr; |
| 167216 | int idxNew; |
| 167217 | pNewExpr = sqlite3PExpr(pParse, ops[i], |
| 167218 | sqlite3ExprDup(db, pExpr->pLeft, 0), |
| @@ -167419,12 +167600,15 @@ | |
| 167419 | && (pExpr->x.pSelect->pPrior==0 || (pExpr->x.pSelect->selFlags & SF_Values)) |
| 167420 | #ifndef SQLITE_OMIT_WINDOWFUNC |
| 167421 | && pExpr->x.pSelect->pWin==0 |
| 167422 | #endif |
| 167423 | && pWC->op==TK_AND |
| 167424 | ){ |
| 167425 | int i; |
| 167426 | for(i=0; i<sqlite3ExprVectorSize(pExpr->pLeft); i++){ |
| 167427 | int idxNew; |
| 167428 | idxNew = whereClauseInsert(pWC, pExpr, TERM_VIRTUAL|TERM_SLICE); |
| 167429 | pWC->a[idxNew].u.x.iField = i+1; |
| 167430 | exprAnalyze(pSrc, pWC, idxNew); |
| @@ -186978,11 +187162,11 @@ | |
| 186978 | ** or disables the collection of memory allocation statistics. */ |
| 186979 | sqlite3GlobalConfig.bMemstat = va_arg(ap, int); |
| 186980 | break; |
| 186981 | } |
| 186982 | case SQLITE_CONFIG_SMALL_MALLOC: { |
| 186983 | sqlite3GlobalConfig.bSmallMalloc = va_arg(ap, int); |
| 186984 | break; |
| 186985 | } |
| 186986 | case SQLITE_CONFIG_PAGECACHE: { |
| 186987 | /* EVIDENCE-OF: R-18761-36601 There are three arguments to |
| 186988 | ** SQLITE_CONFIG_PAGECACHE: A pointer to 8-byte aligned memory (pMem), |
| @@ -193287,10 +193471,16 @@ | |
| 193287 | #ifndef SQLITE_CORE |
| 193288 | /* # include "sqlite3ext.h" */ |
| 193289 | SQLITE_EXTENSION_INIT1 |
| 193290 | #endif |
| 193291 | |
| 193292 | typedef struct Fts3HashWrapper Fts3HashWrapper; |
| 193293 | struct Fts3HashWrapper { |
| 193294 | Fts3Hash hash; /* Hash table */ |
| 193295 | int nRef; /* Number of pointers to this object */ |
| 193296 | }; |
| @@ -195003,11 +195193,15 @@ | |
| 195003 | int iHeight; /* Height of this node in tree */ |
| 195004 | |
| 195005 | assert( piLeaf || piLeaf2 ); |
| 195006 | |
| 195007 | fts3GetVarint32(zNode, &iHeight); |
| 195008 | rc = fts3ScanInteriorNode(zTerm, nTerm, zNode, nNode, piLeaf, piLeaf2); |
| 195009 | assert_fts3_nc( !piLeaf2 || !piLeaf || rc!=SQLITE_OK || (*piLeaf<=*piLeaf2) ); |
| 195010 | |
| 195011 | if( rc==SQLITE_OK && iHeight>1 ){ |
| 195012 | char *zBlob = 0; /* Blob read from %_segments table */ |
| 195013 | int nBlob = 0; /* Size of zBlob in bytes */ |
| @@ -195048,12 +195242,17 @@ | |
| 195048 | char **pp, /* IN/OUT: Output pointer */ |
| 195049 | sqlite3_int64 *piPrev, /* IN/OUT: Previous value written to list */ |
| 195050 | sqlite3_int64 iVal /* Write this value to the list */ |
| 195051 | ){ |
| 195052 | assert_fts3_nc( iVal-*piPrev > 0 || (*piPrev==0 && iVal==0) ); |
| 195053 | *pp += sqlite3Fts3PutVarint(*pp, iVal-*piPrev); |
| 195054 | *piPrev = iVal; |
| 195055 | } |
| 195056 | |
| 195057 | /* |
| 195058 | ** When this function is called, *ppPoslist is assumed to point to the |
| 195059 | ** start of a position-list. After it returns, *ppPoslist points to the |
| @@ -199532,11 +199731,11 @@ | |
| 199532 | break; |
| 199533 | |
| 199534 | /* State 3. The integer just read is a column number. */ |
| 199535 | default: assert( eState==3 ); |
| 199536 | iCol = (int)v; |
| 199537 | if( iCol<1 || iCol>0x3fffffff ){ |
| 199538 | rc = SQLITE_CORRUPT_VTAB; |
| 199539 | break; |
| 199540 | } |
| 199541 | if( fts3auxGrowStatArray(pCsr, iCol+2) ) return SQLITE_NOMEM; |
| 199542 | pCsr->aStat[iCol+1].nDoc++; |
| @@ -206469,10 +206668,14 @@ | |
| 206469 | iMul = -1; |
| 206470 | } |
| 206471 | for(/* no-op */; zText[i]>='0' && zText[i]<='9'; i++){ |
| 206472 | iVal = iVal*10 + (zText[i] - '0'); |
| 206473 | } |
| 206474 | *pnByte = ((i64)iVal * (i64)iMul); |
| 206475 | } |
| 206476 | } |
| 206477 | |
| 206478 | |
| @@ -209331,14 +209534,13 @@ | |
| 209331 | */ |
| 209332 | |
| 209333 | /* |
| 209334 | ** Allocate a two-slot MatchinfoBuffer object. |
| 209335 | */ |
| 209336 | static MatchinfoBuffer *fts3MIBufferNew(size_t nElem, const char *zMatchinfo){ |
| 209337 | MatchinfoBuffer *pRet; |
| 209338 | sqlite3_int64 nByte = sizeof(u32) * (2*(sqlite3_int64)nElem + 1) |
| 209339 | + SZ_MATCHINFOBUFFER(1); |
| 209340 | sqlite3_int64 nStr = strlen(zMatchinfo); |
| 209341 | |
| 209342 | pRet = sqlite3Fts3MallocZero(nByte + nStr+1); |
| 209343 | if( pRet ){ |
| 209344 | pRet->aMI[0] = (u8*)(&pRet->aMI[1]) - (u8*)pRet; |
| @@ -210205,12 +210407,12 @@ | |
| 210205 | } |
| 210206 | sqlite3Fts3ErrMsg(pzErr, "unrecognized matchinfo request: %c", cArg); |
| 210207 | return SQLITE_ERROR; |
| 210208 | } |
| 210209 | |
| 210210 | static size_t fts3MatchinfoSize(MatchInfo *pInfo, char cArg){ |
| 210211 | size_t nVal; /* Number of integers output by cArg */ |
| 210212 | |
| 210213 | switch( cArg ){ |
| 210214 | case FTS3_MATCHINFO_NDOC: |
| 210215 | case FTS3_MATCHINFO_NPHRASE: |
| 210216 | case FTS3_MATCHINFO_NCOL: |
| @@ -210222,20 +210424,20 @@ | |
| 210222 | case FTS3_MATCHINFO_LCS: |
| 210223 | nVal = pInfo->nCol; |
| 210224 | break; |
| 210225 | |
| 210226 | case FTS3_MATCHINFO_LHITS: |
| 210227 | nVal = (size_t)pInfo->nCol * pInfo->nPhrase; |
| 210228 | break; |
| 210229 | |
| 210230 | case FTS3_MATCHINFO_LHITS_BM: |
| 210231 | nVal = (size_t)pInfo->nPhrase * ((pInfo->nCol + 31) / 32); |
| 210232 | break; |
| 210233 | |
| 210234 | default: |
| 210235 | assert( cArg==FTS3_MATCHINFO_HITS ); |
| 210236 | nVal = (size_t)pInfo->nCol * pInfo->nPhrase * 3; |
| 210237 | break; |
| 210238 | } |
| 210239 | |
| 210240 | return nVal; |
| 210241 | } |
| @@ -210513,11 +210715,11 @@ | |
| 210513 | } |
| 210514 | break; |
| 210515 | |
| 210516 | case FTS3_MATCHINFO_LHITS_BM: |
| 210517 | case FTS3_MATCHINFO_LHITS: { |
| 210518 | size_t nZero = fts3MatchinfoSize(pInfo, zArg[i]) * sizeof(u32); |
| 210519 | memset(pInfo->aMatchinfo, 0, nZero); |
| 210520 | rc = fts3ExprLHitGather(pCsr->pExpr, pInfo); |
| 210521 | break; |
| 210522 | } |
| 210523 | |
| @@ -210582,11 +210784,11 @@ | |
| 210582 | ** matchinfo function has been called for this query. In this case |
| 210583 | ** allocate the array used to accumulate the matchinfo data and |
| 210584 | ** initialize those elements that are constant for every row. |
| 210585 | */ |
| 210586 | if( pCsr->pMIBuffer==0 ){ |
| 210587 | size_t nMatchinfo = 0; /* Number of u32 elements in match-info */ |
| 210588 | int i; /* Used to iterate through zArg */ |
| 210589 | |
| 210590 | /* Determine the number of phrases in the query */ |
| 210591 | pCsr->nPhrase = fts3ExprPhraseCount(pCsr->pExpr); |
| 210592 | sInfo.nPhrase = pCsr->nPhrase; |
| @@ -218269,10 +218471,13 @@ | |
| 218269 | ** be because the shadow tables hold erroneous data. */ |
| 218270 | if( rc==SQLITE_ERROR ){ |
| 218271 | rc = SQLITE_CORRUPT_VTAB; |
| 218272 | RTREE_IS_CORRUPT(pRtree); |
| 218273 | } |
| 218274 | }else if( pRtree->iNodeSize==sqlite3_blob_bytes(pRtree->pNodeBlob) ){ |
| 218275 | pNode = (RtreeNode *)sqlite3_malloc64(sizeof(RtreeNode)+pRtree->iNodeSize); |
| 218276 | if( !pNode ){ |
| 218277 | rc = SQLITE_NOMEM; |
| 218278 | }else{ |
| @@ -218910,11 +219115,11 @@ | |
| 218910 | i64 iRowid, |
| 218911 | int *piIndex |
| 218912 | ){ |
| 218913 | int ii; |
| 218914 | int nCell = NCELL(pNode); |
| 218915 | assert( nCell<200 ); |
| 218916 | for(ii=0; ii<nCell; ii++){ |
| 218917 | if( nodeGetRowid(pRtree, pNode, ii)==iRowid ){ |
| 218918 | *piIndex = ii; |
| 218919 | return SQLITE_OK; |
| 218920 | } |
| @@ -223953,10 +224158,13 @@ | |
| 223953 | if( c>=0xc0 ){ \ |
| 223954 | c = icuUtf8Trans1[c-0xc0]; \ |
| 223955 | while( (*zIn & 0xc0)==0x80 ){ \ |
| 223956 | c = (c<<6) + (0x3f & *(zIn++)); \ |
| 223957 | } \ |
| 223958 | } |
| 223959 | |
| 223960 | #define SQLITE_ICU_SKIP_UTF8(zIn) \ |
| 223961 | assert( *zIn ); \ |
| 223962 | if( *(zIn++)>=0xc0 ){ \ |
| @@ -225858,20 +226066,30 @@ | |
| 225858 | 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -1, -1, -1, -1, -1, -1, |
| 225859 | -1, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, |
| 225860 | 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, -1, -1, -1, -1, 36, |
| 225861 | -1, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, |
| 225862 | 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, -1, -1, -1, 63, -1, |
| 225863 | }; |
| 225864 | unsigned int v = 0; |
| 225865 | int c; |
| 225866 | unsigned char *z = (unsigned char*)*pz; |
| 225867 | unsigned char *zStart = z; |
| 225868 | while( (c = zValue[0x7f&*(z++)])>=0 ){ |
| 225869 | v = (v<<6) + c; |
| 225870 | } |
| 225871 | z--; |
| 225872 | *pLen -= (int)(z - zStart); |
| 225873 | *pz = (char*)z; |
| 225874 | return v; |
| 225875 | } |
| 225876 | |
| 225877 | #if RBU_ENABLE_DELTA_CKSUM |
| @@ -225943,23 +226161,24 @@ | |
| 225943 | #if RBU_ENABLE_DELTA_CKSUM |
| 225944 | char *zOrigOut = zOut; |
| 225945 | #endif |
| 225946 | |
| 225947 | limit = rbuDeltaGetInt(&zDelta, &lenDelta); |
| 225948 | if( *zDelta!='\n' ){ |
| 225949 | /* ERROR: size integer not terminated by "\n" */ |
| 225950 | return -1; |
| 225951 | } |
| 225952 | zDelta++; lenDelta--; |
| 225953 | while( *zDelta && lenDelta>0 ){ |
| 225954 | unsigned int cnt, ofst; |
| 225955 | cnt = rbuDeltaGetInt(&zDelta, &lenDelta); |
| 225956 | switch( zDelta[0] ){ |
| 225957 | case '@': { |
| 225958 | zDelta++; lenDelta--; |
| 225959 | ofst = rbuDeltaGetInt(&zDelta, &lenDelta); |
| 225960 | if( lenDelta>0 && zDelta[0]!=',' ){ |
| 225961 | /* ERROR: copy command not terminated by ',' */ |
| 225962 | return -1; |
| 225963 | } |
| 225964 | zDelta++; lenDelta--; |
| 225965 | total += cnt; |
| @@ -225980,11 +226199,11 @@ | |
| 225980 | total += cnt; |
| 225981 | if( total>limit ){ |
| 225982 | /* ERROR: insert command gives an output larger than predicted */ |
| 225983 | return -1; |
| 225984 | } |
| 225985 | if( (int)cnt>lenDelta ){ |
| 225986 | /* ERROR: insert count exceeds size of delta */ |
| 225987 | return -1; |
| 225988 | } |
| 225989 | memcpy(zOut, zDelta, cnt); |
| 225990 | zOut += cnt; |
| @@ -226018,11 +226237,11 @@ | |
| 226018 | } |
| 226019 | |
| 226020 | static int rbuDeltaOutputSize(const char *zDelta, int lenDelta){ |
| 226021 | int size; |
| 226022 | size = rbuDeltaGetInt(&zDelta, &lenDelta); |
| 226023 | if( *zDelta!='\n' ){ |
| 226024 | /* ERROR: size integer not terminated by "\n" */ |
| 226025 | return -1; |
| 226026 | } |
| 226027 | return size; |
| 226028 | } |
| @@ -226066,11 +226285,11 @@ | |
| 226066 | if( nOut<0 ){ |
| 226067 | sqlite3_result_error(context, "corrupt fossil delta", -1); |
| 226068 | return; |
| 226069 | } |
| 226070 | |
| 226071 | aOut = sqlite3_malloc(nOut+1); |
| 226072 | if( aOut==0 ){ |
| 226073 | sqlite3_result_error_nomem(context); |
| 226074 | }else{ |
| 226075 | nOut2 = rbuDeltaApply(aOrig, nOrig, aDelta, nDelta, aOut); |
| 226076 | if( nOut2!=nOut ){ |
| @@ -244051,11 +244270,11 @@ | |
| 244051 | ){ |
| 244052 | HighlightContext ctx; |
| 244053 | int rc = SQLITE_OK; /* Return code */ |
| 244054 | int iCol; /* 1st argument to snippet() */ |
| 244055 | const char *zEllips; /* 4th argument to snippet() */ |
| 244056 | int nToken; /* 5th argument to snippet() */ |
| 244057 | int nInst = 0; /* Number of instance matches this row */ |
| 244058 | int i; /* Used to iterate through instances */ |
| 244059 | int nPhrase; /* Number of phrases in query */ |
| 244060 | unsigned char *aSeen; /* Array of "seen instance" flags */ |
| 244061 | int iBestCol; /* Column containing best snippet */ |
| @@ -244076,11 +244295,11 @@ | |
| 244076 | iCol = sqlite3_value_int(apVal[0]); |
| 244077 | ctx.zOpen = fts5ValueToText(apVal[1]); |
| 244078 | ctx.zClose = fts5ValueToText(apVal[2]); |
| 244079 | ctx.iRangeEnd = -1; |
| 244080 | zEllips = fts5ValueToText(apVal[3]); |
| 244081 | nToken = sqlite3_value_int(apVal[4]); |
| 244082 | |
| 244083 | iBestCol = (iCol>=0 ? iCol : 0); |
| 244084 | nPhrase = pApi->xPhraseCount(pFts); |
| 244085 | aSeen = sqlite3_malloc64(nPhrase); |
| 244086 | if( aSeen==0 ){ |
| @@ -246792,11 +247011,11 @@ | |
| 246792 | /* Add an entry to each output position list */ |
| 246793 | for(i=0; i<pNear->nPhrase; i++){ |
| 246794 | i64 iPos = a[i].reader.iPos; |
| 246795 | Fts5PoslistWriter *pWriter = &a[i].writer; |
| 246796 | if( a[i].pOut->n==0 || iPos!=pWriter->iPrev ){ |
| 246797 | sqlite3Fts5PoslistWriterAppend(a[i].pOut, pWriter, iPos); |
| 246798 | } |
| 246799 | } |
| 246800 | |
| 246801 | iAdv = 0; |
| 246802 | iMin = a[0].reader.iLookahead; |
| @@ -247743,14 +247962,14 @@ | |
| 247743 | rc = SQLITE_NOMEM; |
| 247744 | }else{ |
| 247745 | memset(pSyn, 0, (size_t)nByte); |
| 247746 | pSyn->pTerm = ((char*)pSyn) + sizeof(Fts5ExprTerm) + sizeof(Fts5Buffer); |
| 247747 | pSyn->nFullTerm = pSyn->nQueryTerm = nToken; |
| 247748 | if( pCtx->pConfig->bTokendata ){ |
| 247749 | pSyn->nQueryTerm = (int)strlen(pSyn->pTerm); |
| 247750 | } |
| 247751 | memcpy(pSyn->pTerm, pToken, nToken); |
| 247752 | pSyn->pSynonym = pPhrase->aTerm[pPhrase->nTerm-1].pSynonym; |
| 247753 | pPhrase->aTerm[pPhrase->nTerm-1].pSynonym = pSyn; |
| 247754 | } |
| 247755 | }else{ |
| 247756 | Fts5ExprTerm *pTerm; |
| @@ -249595,11 +249814,11 @@ | |
| 249595 | ** + 1 byte for a "new column" byte, |
| 249596 | ** + 3 bytes for a new column number (16-bit max) as a varint, |
| 249597 | ** + 5 bytes for the new position offset (32-bit max). |
| 249598 | */ |
| 249599 | if( (p->nAlloc - p->nData) < (9 + 4 + 1 + 3 + 5) ){ |
| 249600 | sqlite3_int64 nNew = p->nAlloc * 2; |
| 249601 | Fts5HashEntry *pNew; |
| 249602 | Fts5HashEntry **pp; |
| 249603 | pNew = (Fts5HashEntry*)sqlite3_realloc64(p, nNew); |
| 249604 | if( pNew==0 ) return SQLITE_NOMEM; |
| 249605 | pNew->nAlloc = (int)nNew; |
| @@ -251029,11 +251248,11 @@ | |
| 251029 | }else{ |
| 251030 | i += fts5GetVarint32(&pData[i], pLvl->nMerge); |
| 251031 | i += fts5GetVarint32(&pData[i], nTotal); |
| 251032 | if( nTotal<pLvl->nMerge ) rc = FTS5_CORRUPT; |
| 251033 | pLvl->aSeg = (Fts5StructureSegment*)sqlite3Fts5MallocZero(&rc, |
| 251034 | nTotal * sizeof(Fts5StructureSegment) |
| 251035 | ); |
| 251036 | nSegment -= nTotal; |
| 251037 | } |
| 251038 | |
| 251039 | if( rc==SQLITE_OK ){ |
| @@ -251558,19 +251777,20 @@ | |
| 251558 | assert( pLvl->bEof==0 ); |
| 251559 | if( iOff<=pLvl->iFirstOff ){ |
| 251560 | pLvl->bEof = 1; |
| 251561 | }else{ |
| 251562 | u8 *a = pLvl->pData->p; |
| 251563 | |
| 251564 | pLvl->iOff = 0; |
| 251565 | fts5DlidxLvlNext(pLvl); |
| 251566 | while( 1 ){ |
| 251567 | int nZero = 0; |
| 251568 | int ii = pLvl->iOff; |
| 251569 | u64 delta = 0; |
| 251570 | |
| 251571 | while( a[ii]==0 ){ |
| 251572 | nZero++; |
| 251573 | ii++; |
| 251574 | } |
| 251575 | ii += sqlite3Fts5GetVarint(&a[ii], &delta); |
| 251576 | |
| @@ -251985,11 +252205,11 @@ | |
| 251985 | fts5DataRelease(pIter->pLeaf); |
| 251986 | pIter->pLeaf = 0; |
| 251987 | while( p->rc==SQLITE_OK && pIter->iLeafPgno>pIter->iTermLeafPgno ){ |
| 251988 | Fts5Data *pNew; |
| 251989 | pIter->iLeafPgno--; |
| 251990 | pNew = fts5DataRead(p, FTS5_SEGMENT_ROWID( |
| 251991 | pIter->pSeg->iSegid, pIter->iLeafPgno |
| 251992 | )); |
| 251993 | if( pNew ){ |
| 251994 | /* iTermLeafOffset may be equal to szLeaf if the term is the last |
| 251995 | ** thing on the page - i.e. the first rowid is on the following page. |
| @@ -253419,12 +253639,11 @@ | |
| 253419 | } |
| 253420 | } |
| 253421 | |
| 253422 | do { |
| 253423 | while( i<nChunk && pChunk[i]!=0x01 ){ |
| 253424 | while( pChunk[i] & 0x80 ) i++; |
| 253425 | i++; |
| 253426 | } |
| 253427 | if( pCtx->eState ){ |
| 253428 | fts5BufferSafeAppendBlob(pCtx->pBuf, &pChunk[iStart], i-iStart); |
| 253429 | } |
| 253430 | if( i<nChunk ){ |
| @@ -255163,10 +255382,15 @@ | |
| 255163 | int iSOP; /* Start-Of-Position-list */ |
| 255164 | if( pSeg->iLeafPgno==pSeg->iTermLeafPgno ){ |
| 255165 | iStart = pSeg->iTermLeafOffset; |
| 255166 | }else{ |
| 255167 | iStart = fts5GetU16(&aPg[0]); |
| 255168 | } |
| 255169 | |
| 255170 | iSOP = iStart + fts5GetVarint(&aPg[iStart], &iDelta); |
| 255171 | assert_nc( iSOP<=pSeg->iLeafOffset ); |
| 255172 | |
| @@ -257849,12 +258073,12 @@ | |
| 257849 | int *pnOut, /* OUT: Number of output pages */ |
| 257850 | Fts5Data ***papOut /* OUT: Output hash pages */ |
| 257851 | ){ |
| 257852 | const int MINSLOT = 32; |
| 257853 | int nSlotPerPage = MAX(MINSLOT, (p->pConfig->pgsz - 8) / szKey); |
| 257854 | int nSlot = 0; /* Number of slots in each output page */ |
| 257855 | int nOut = 0; |
| 257856 | |
| 257857 | /* Figure out how many output pages (nOut) and how many slots per |
| 257858 | ** page (nSlot). There are three possibilities: |
| 257859 | ** |
| 257860 | ** 1. The hash table does not yet exist. In this case the new hash |
| @@ -257875,27 +258099,30 @@ | |
| 257875 | /* Case 1. */ |
| 257876 | nOut = 1; |
| 257877 | nSlot = MINSLOT; |
| 257878 | }else if( pSeg->nPgTombstone==1 ){ |
| 257879 | /* Case 2. */ |
| 257880 | int nElem = (int)fts5GetU32(&pData1->p[4]); |
| 257881 | assert( pData1 && iPg1==0 ); |
| 257882 | nOut = 1; |
| 257883 | nSlot = MAX(nElem*4, MINSLOT); |
| 257884 | if( nSlot>nSlotPerPage ) nOut = 0; |
| 257885 | } |
| 257886 | if( nOut==0 ){ |
| 257887 | /* Case 3. */ |
| 257888 | nOut = (pSeg->nPgTombstone * 2 + 1); |
| 257889 | nSlot = nSlotPerPage; |
| 257890 | } |
| 257891 | |
| 257892 | /* Allocate the required array and output pages */ |
| 257893 | while( 1 ){ |
| 257894 | int res = 0; |
| 257895 | int ii = 0; |
| 257896 | int szPage = 0; |
| 257897 | Fts5Data **apOut = 0; |
| 257898 | |
| 257899 | /* Allocate space for the new hash table */ |
| 257900 | assert( nSlot>=MINSLOT ); |
| 257901 | apOut = (Fts5Data**)sqlite3Fts5MallocZero(&p->rc, sizeof(Fts5Data*) * nOut); |
| @@ -258429,11 +258656,11 @@ | |
| 258429 | ){ |
| 258430 | |
| 258431 | /* Check any rowid-less pages that occur before the current leaf. */ |
| 258432 | for(iPg=iPrevLeaf+1; iPg<fts5DlidxIterPgno(pDlidx); iPg++){ |
| 258433 | iKey = FTS5_SEGMENT_ROWID(iSegid, iPg); |
| 258434 | pLeaf = fts5DataRead(p, iKey); |
| 258435 | if( pLeaf ){ |
| 258436 | if( fts5LeafFirstRowidOff(pLeaf)!=0 ) FTS5_CORRUPT_ROWID(p, iKey); |
| 258437 | fts5DataRelease(pLeaf); |
| 258438 | } |
| 258439 | } |
| @@ -258440,11 +258667,11 @@ | |
| 258440 | iPrevLeaf = fts5DlidxIterPgno(pDlidx); |
| 258441 | |
| 258442 | /* Check that the leaf page indicated by the iterator really does |
| 258443 | ** contain the rowid suggested by the same. */ |
| 258444 | iKey = FTS5_SEGMENT_ROWID(iSegid, iPrevLeaf); |
| 258445 | pLeaf = fts5DataRead(p, iKey); |
| 258446 | if( pLeaf ){ |
| 258447 | i64 iRowid; |
| 258448 | int iRowidOff = fts5LeafFirstRowidOff(pLeaf); |
| 258449 | ASSERT_SZLEAF_OK(pLeaf); |
| 258450 | if( iRowidOff>=pLeaf->szLeaf ){ |
| @@ -263040,11 +263267,11 @@ | |
| 263040 | int nArg, /* Number of args */ |
| 263041 | sqlite3_value **apUnused /* Function arguments */ |
| 263042 | ){ |
| 263043 | assert( nArg==0 ); |
| 263044 | UNUSED_PARAM2(nArg, apUnused); |
| 263045 | sqlite3_result_text(pCtx, "fts5: 2026-05-30 10:24:03 7487a1c59d3aaea9f8b2569dca76bbccf21948b1e7bd8a1d841e04382db696f4", -1, SQLITE_TRANSIENT); |
| 263046 | } |
| 263047 | |
| 263048 | /* |
| 263049 | ** Implementation of fts5_locale(LOCALE, TEXT) function. |
| 263050 | ** |
| 263051 |
| --- 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 | ** 3f3fb9b638f59ad982beafb7c117f24ddd3d with changes in files: |
| 22 | ** |
| 23 | ** |
| 24 | */ |
| 25 | #ifndef SQLITE_AMALGAMATION |
| 26 | #define SQLITE_CORE 1 |
| @@ -467,14 +467,14 @@ | |
| 467 | ** [sqlite3_libversion_number()], [sqlite3_sourceid()], |
| 468 | ** [sqlite_version()] and [sqlite_source_id()]. |
| 469 | */ |
| 470 | #define SQLITE_VERSION "3.54.0" |
| 471 | #define SQLITE_VERSION_NUMBER 3054000 |
| 472 | #define SQLITE_SOURCE_ID "2026-06-16 13:43:08 3f3fb9b638f59ad982beafb7c117f24ddd3da612e62c862510805fa672ffae06" |
| 473 | #define SQLITE_SCM_BRANCH "trunk" |
| 474 | #define SQLITE_SCM_TAGS "" |
| 475 | #define SQLITE_SCM_DATETIME "2026-06-16T13:43:08.110Z" |
| 476 | |
| 477 | /* |
| 478 | ** CAPI3REF: Run-Time Library Version Numbers |
| 479 | ** KEYWORDS: sqlite3_version sqlite3_sourceid |
| 480 | ** |
| @@ -4696,11 +4696,12 @@ | |
| 4696 | ** <dd>The maximum number of columns in a table definition or in the |
| 4697 | ** result set of a [SELECT] or the maximum number of columns in an index |
| 4698 | ** or in an ORDER BY or GROUP BY clause.</dd>)^ |
| 4699 | ** |
| 4700 | ** [[SQLITE_LIMIT_EXPR_DEPTH]] ^(<dt>SQLITE_LIMIT_EXPR_DEPTH</dt> |
| 4701 | ** <dd>The maximum depth of the parse tree on any expression and |
| 4702 | ** the maximum nesting depth for subqueries and VIEWs</dd>)^ |
| 4703 | ** |
| 4704 | ** [[SQLITE_LIMIT_PARSER_DEPTH]] ^(<dt>SQLITE_LIMIT_PARSER_DEPTH</dt> |
| 4705 | ** <dd>The maximum depth of the LALR(1) parser stack used to analyze |
| 4706 | ** input SQL statements.</dd>)^ |
| 4707 | ** |
| @@ -4727,11 +4728,12 @@ | |
| 4728 | ** [[SQLITE_LIMIT_VARIABLE_NUMBER]] |
| 4729 | ** ^(<dt>SQLITE_LIMIT_VARIABLE_NUMBER</dt> |
| 4730 | ** <dd>The maximum index number of any [parameter] in an SQL statement.)^ |
| 4731 | ** |
| 4732 | ** [[SQLITE_LIMIT_TRIGGER_DEPTH]] ^(<dt>SQLITE_LIMIT_TRIGGER_DEPTH</dt> |
| 4733 | ** <dd>The maximum depth of recursion for triggers, and the maximum |
| 4734 | ** nesting depth for separate triggers.</dd>)^ |
| 4735 | ** |
| 4736 | ** [[SQLITE_LIMIT_WORKER_THREADS]] ^(<dt>SQLITE_LIMIT_WORKER_THREADS</dt> |
| 4737 | ** <dd>The maximum number of auxiliary worker threads that a single |
| 4738 | ** [prepared statement] may start.</dd>)^ |
| 4739 | ** </dl> |
| @@ -11533,12 +11535,23 @@ | |
| 11535 | ** reopen S as an in-memory database based on the serialization |
| 11536 | ** contained in P. If S is a NULL pointer, the main database is |
| 11537 | ** used. The serialized database P is N bytes in size. M is the size |
| 11538 | ** of the buffer P, which might be larger than N. If M is larger than |
| 11539 | ** N, and the SQLITE_DESERIALIZE_READONLY bit is not set in F, then |
| 11540 | ** SQLite is permitted to add content to the in-memory database, in |
| 11541 | ** page-sized chunks, as long as the total size does not exceed M bytes. |
| 11542 | ** |
| 11543 | ** The parameter M must be greater than or equal to N. Ideally, M |
| 11544 | ** should have a value which is N+(512×K)+20 where K determines how |
| 11545 | ** must extra space is available to hold new content as the database |
| 11546 | ** grows. K can be 0 if the database is read-only. |
| 11547 | ** |
| 11548 | ** If the database content in P is malformed in a malicious way then |
| 11549 | ** it is possible that SQLite might try to read a few more than N bytes |
| 11550 | ** from P. If the veracity of the database content P is uncertain, |
| 11551 | ** then applications are advised to allocate about 20 extra bytes on |
| 11552 | ** the end of the P buffer to avoid a memory error. |
| 11553 | ** |
| 11554 | ** If the SQLITE_DESERIALIZE_FREEONCLOSE bit is set in F, then SQLite will |
| 11555 | ** invoke sqlite3_free() on the serialization buffer when the database |
| 11556 | ** connection closes. If the SQLITE_DESERIALIZE_RESIZEABLE bit is set, then |
| 11557 | ** SQLite will try to increase the buffer size using sqlite3_realloc64() |
| @@ -15809,10 +15822,17 @@ | |
| 15822 | */ |
| 15823 | #ifndef offsetof |
| 15824 | # define offsetof(ST,M) ((size_t)((char*)&((ST*)0)->M - (char*)0)) |
| 15825 | #endif |
| 15826 | |
| 15827 | /* |
| 15828 | ** sizeof64() is like sizeof(), but always returns a 64-bit value, even |
| 15829 | ** on 32-bit builds. This can help to avoid overflow by ensuring 64-bit |
| 15830 | ** arithmetic is used consistently in both 32-bit and 64-bit builds. |
| 15831 | */ |
| 15832 | #define sizeof64(X) ((sqlite3_int64)sizeof(X)) |
| 15833 | |
| 15834 | /* |
| 15835 | ** Work around C99 "flex-array" syntax for pre-C99 compilers, so as |
| 15836 | ** to avoid complaints from -fsanitize=strict-bounds. |
| 15837 | */ |
| 15838 | #if defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L) |
| @@ -20928,10 +20948,11 @@ | |
| 20948 | int nTab; /* Number of previously allocated VDBE cursors */ |
| 20949 | int nMem; /* Number of memory cells used so far */ |
| 20950 | int szOpAlloc; /* Bytes of memory space allocated for Vdbe.aOp[] */ |
| 20951 | int iSelfTab; /* Table associated with an index on expr, or negative |
| 20952 | ** of the base register during check-constraint eval */ |
| 20953 | int nNestSel; /* Number of nested SELECT statements and/or VIEWs */ |
| 20954 | int nLabel; /* The *negative* of the number of labels used */ |
| 20955 | int nLabelAlloc; /* Number of slots in aLabel */ |
| 20956 | int *aLabel; /* Space to hold the labels */ |
| 20957 | ExprList *pConstExpr;/* Constant expressions */ |
| 20958 | IndexedExpr *pIdxEpr;/* List of expressions used by active indexes */ |
| @@ -33389,12 +33410,12 @@ | |
| 33410 | if( flag_alternateform ){ |
| 33411 | /* For %#q, do unistr()-style backslash escapes for |
| 33412 | ** all control characters, and for backslash itself. |
| 33413 | ** For %#Q, do the same but only if there is at least |
| 33414 | ** one control character. */ |
| 33415 | i64 nBack = 0; |
| 33416 | i64 nCtrl = 0; |
| 33417 | for(k=0; k<i; k++){ |
| 33418 | if( escarg[k]=='\\' ){ |
| 33419 | nBack++; |
| 33420 | }else if( ((u8*)escarg)[k]<=0x1f ){ |
| 33421 | nCtrl++; |
| @@ -39255,10 +39276,26 @@ | |
| 39276 | ****************************************************************************** |
| 39277 | ** |
| 39278 | ** This file contains an experimental VFS layer that operates on a |
| 39279 | ** Key/Value storage engine where both keys and values must be pure |
| 39280 | ** text. |
| 39281 | ** |
| 39282 | ** DEBUG AND TEST |
| 39283 | ** |
| 39284 | ** For testing on Unix, compile using: |
| 39285 | ** |
| 39286 | ** make clean sqlite3d CFLAGS='-DSQLITE_OS_KV_OPTIONAL' |
| 39287 | ** |
| 39288 | ** Then start up a shell using something like: |
| 39289 | ** |
| 39290 | ** ./sqlite3d 'file:dbname?vfs=kvvfs' |
| 39291 | ** |
| 39292 | ** Each K/V entry is stored in a separate file in the working |
| 39293 | ** directory that has a name like "kvvfs-dbname-*". Due to limitations |
| 39294 | ** on the key size, the name of the database must be very short - just |
| 39295 | ** a few characters. If the database name is too long, the VFS will |
| 39296 | ** malfunction and you will get SQLITE_CORRUPT errors. |
| 39297 | */ |
| 39298 | /* #include <sqliteInt.h> */ |
| 39299 | #if SQLITE_OS_KV || (SQLITE_OS_UNIX && defined(SQLITE_OS_KV_OPTIONAL)) |
| 39300 | |
| 39301 | /***************************************************************************** |
| @@ -39707,16 +39744,18 @@ | |
| 39744 | } |
| 39745 | if( j+n>nOut ) return -1; |
| 39746 | memset(&aOut[j], 0, n); |
| 39747 | j += n; |
| 39748 | if( c==0 || mult==1 ) break; /* progress stalled if mult==1 */ |
| 39749 | }else if( j<nOut ){ |
| 39750 | aOut[j] = c<<4; |
| 39751 | c = kvvfsHexValue[aIn[++i]]; |
| 39752 | if( c<0 ) return -1 /* hex bytes are always in pairs */; |
| 39753 | aOut[j++] += c; |
| 39754 | i++; |
| 39755 | }else{ |
| 39756 | return -1; |
| 39757 | } |
| 39758 | } |
| 39759 | return j; |
| 39760 | } |
| 39761 | |
| @@ -40138,10 +40177,22 @@ | |
| 40177 | }else{ |
| 40178 | pFile->isJournal = 0; |
| 40179 | pFile->base.pMethods = &kvvfs_db_io_methods; |
| 40180 | } |
| 40181 | if( !pFile->zClass ){ |
| 40182 | #ifdef SQLITE_WASM |
| 40183 | if( strlen(zName) >= (KVRECORD_KEY_SZ |
| 40184 | - 6 /* "kvvfs-" */ |
| 40185 | - 11 /* "-##########" */) ){ |
| 40186 | return SQLITE_CANTOPEN; |
| 40187 | } |
| 40188 | #else |
| 40189 | if( 0!=strcmp(zName, "local") && 0!=strcmp(zName, "session") ){ |
| 40190 | /* Historical naming restriction which journaling depends on. */ |
| 40191 | return SQLITE_CANTOPEN; |
| 40192 | } |
| 40193 | #endif |
| 40194 | pFile->zClass = zName; |
| 40195 | } |
| 40196 | pFile->aData = sqlite3_malloc64(SQLITE_KVOS_SZ); |
| 40197 | if( pFile->aData==0 ){ |
| 40198 | return SQLITE_NOMEM; |
| @@ -52170,15 +52221,33 @@ | |
| 52221 | # define sqlite3_win_test_unc_locking 0 |
| 52222 | #endif |
| 52223 | |
| 52224 | /* |
| 52225 | ** Return true if the string passed as the only argument is likely |
| 52226 | ** to be a UNC path. Return false if note. |
| 52227 | ** |
| 52228 | ** Return true if: |
| 52229 | ** |
| 52230 | ** (1) The name begins with "\\" |
| 52231 | ** (2) But does not begin with "\\?\C:\" where C can be any alphabetic |
| 52232 | ** character. |
| 52233 | ** |
| 52234 | ** For testing, also return true in all cases if the global variable |
| 52235 | ** sqlite3_win_test_unc_locking is true. |
| 52236 | */ |
| 52237 | static int winIsUNCPath(const char *zFile){ |
| 52238 | if( zFile[0]=='\\' && zFile[1]=='\\' ){ |
| 52239 | if( zFile[2]=='?' |
| 52240 | && zFile[3]=='\\' |
| 52241 | && sqlite3Isalpha(zFile[4]) |
| 52242 | && zFile[5]==':' |
| 52243 | && winIsDirSep(zFile[6]) |
| 52244 | ){ |
| 52245 | return sqlite3_win_test_unc_locking; |
| 52246 | }else{ |
| 52247 | return 1; |
| 52248 | } |
| 52249 | } |
| 52250 | return sqlite3_win_test_unc_locking; |
| 52251 | } |
| 52252 | |
| 52253 | /* |
| @@ -56926,26 +56995,28 @@ | |
| 56995 | szBulk = -1024 * (i64)pcache1.nInitPage; |
| 56996 | } |
| 56997 | if( szBulk > pCache->szAlloc*(i64)pCache->nMax ){ |
| 56998 | szBulk = pCache->szAlloc*(i64)pCache->nMax; |
| 56999 | } |
| 57000 | if( szBulk>=pCache->szAlloc ){ |
| 57001 | zBulk = pCache->pBulk = sqlite3Malloc( szBulk ); |
| 57002 | sqlite3EndBenignMalloc(); |
| 57003 | if( zBulk ){ |
| 57004 | int nBulk = sqlite3MallocSize(zBulk)/pCache->szAlloc; |
| 57005 | do{ |
| 57006 | PgHdr1 *pX = (PgHdr1*)&zBulk[pCache->szPage]; |
| 57007 | pX->page.pBuf = zBulk; |
| 57008 | pX->page.pExtra = (u8*)pX + ROUND8(sizeof(*pX)); |
| 57009 | assert( EIGHT_BYTE_ALIGNMENT( pX->page.pExtra ) ); |
| 57010 | pX->isBulkLocal = 1; |
| 57011 | pX->isAnchor = 0; |
| 57012 | pX->pNext = pCache->pFree; |
| 57013 | pX->pLruPrev = 0; /* Initializing this saves a valgrind error */ |
| 57014 | pCache->pFree = pX; |
| 57015 | zBulk += pCache->szAlloc; |
| 57016 | }while( --nBulk ); |
| 57017 | } |
| 57018 | } |
| 57019 | return pCache->pFree!=0; |
| 57020 | } |
| 57021 | |
| 57022 | /* |
| @@ -59839,73 +59910,84 @@ | |
| 59910 | #define pager_set_pagehash(X) |
| 59911 | #define CHECK_PAGE(x) |
| 59912 | #endif /* SQLITE_CHECK_PAGES */ |
| 59913 | |
| 59914 | /* |
| 59915 | ** Free a buffer allocated by the readSuperJournal() function. |
| 59916 | */ |
| 59917 | static void freeSuperJournal(char *zSuper){ |
| 59918 | if( zSuper ){ |
| 59919 | sqlite3_free(&zSuper[-4]); |
| 59920 | } |
| 59921 | } |
| 59922 | |
| 59923 | /* |
| 59924 | ** Parameter pJrnl is a file-handle open on a journal file. This function |
| 59925 | ** attempts to read a super-journal file name from the end of the journal |
| 59926 | ** file. If successful, it sets output parameter (*pzSuper) to point to a |
| 59927 | ** buffer containing the super-journal name as a nul-terminated string. |
| 59928 | ** The caller is responsible for freeing the buffer using freeSuperJournal(). |
| 59929 | ** |
| 59930 | ** Refer to comments above writeSuperJournal() for the format used to store |
| 59931 | ** a super-journal file name at the end of a journal file. |
| 59932 | ** |
| 59933 | ** Parameter nSuper is passed the maximum allowable size of the super journal |
| 59934 | ** name in bytes. If the super-journal name in the journal is longer than |
| 59935 | ** nSuper bytes (including a nul-terminator), then this is handled as if no |
| 59936 | ** super-journal name were present in the journal. |
| 59937 | ** |
| 59938 | ** If there is no super-journal name at the end of pJrnl, (*pzSuper) is |
| 59939 | ** set to 0 and SQLITE_OK is returned. Or, if an error occurs while reading |
| 59940 | ** the super-journal name, an SQLite error code is returned and (*pzSuper) |
| 59941 | ** is set to 0. |
| 59942 | */ |
| 59943 | static int readSuperJournal(sqlite3_file *pJrnl, u64 nSuper, char **pzSuper){ |
| 59944 | int rc; /* Return code */ |
| 59945 | u32 len; /* Length in bytes of super-journal name */ |
| 59946 | i64 szJ; /* Total size in bytes of journal file pJrnl */ |
| 59947 | u32 cksum; /* MJ checksum value read from journal */ |
| 59948 | unsigned char aMagic[8]; /* A buffer to hold the magic header */ |
| 59949 | char *zOut = 0; |
| 59950 | |
| 59951 | *pzSuper = 0; |
| 59952 | if( SQLITE_OK!=(rc = sqlite3OsFileSize(pJrnl, &szJ)) |
| 59953 | || szJ<16 |
| 59954 | || SQLITE_OK!=(rc = read32bits(pJrnl, szJ-16, &len)) |
| 59955 | || len>=nSuper |
| 59956 | || len>szJ-16 |
| 59957 | || len==0 |
| 59958 | || SQLITE_OK!=(rc = read32bits(pJrnl, szJ-12, &cksum)) |
| 59959 | || SQLITE_OK!=(rc = sqlite3OsRead(pJrnl, aMagic, 8, szJ-8)) |
| 59960 | || memcmp(aMagic, aJournalMagic, 8) |
| 59961 | ){ |
| 59962 | return rc; |
| 59963 | } |
| 59964 | |
| 59965 | zOut = (char*)sqlite3MallocZero(4 + len + 2); |
| 59966 | if( !zOut ){ |
| 59967 | rc = SQLITE_NOMEM_BKPT; |
| 59968 | }else{ |
| 59969 | zOut = &zOut[4]; |
| 59970 | if( SQLITE_OK==(rc = sqlite3OsRead(pJrnl, zOut, len, szJ-16-len)) ){ |
| 59971 | u32 u; /* Unsigned loop counter */ |
| 59972 | /* See if the checksum matches the super-journal name */ |
| 59973 | for(u=0; u<len; u++){ |
| 59974 | cksum -= zOut[u]; |
| 59975 | } |
| 59976 | } |
| 59977 | if( rc!=SQLITE_OK || cksum ){ |
| 59978 | /* If the checksum doesn't add up, then one or more of the disk sectors |
| 59979 | ** containing the super-journal filename is corrupted. This means |
| 59980 | ** definitely roll back, so just return SQLITE_OK and report a (nul) |
| 59981 | ** super-journal filename. */ |
| 59982 | freeSuperJournal(zOut); |
| 59983 | zOut = 0; |
| 59984 | } |
| 59985 | } |
| 59986 | |
| 59987 | *pzSuper = zOut; |
| 59988 | return rc; |
| 59989 | } |
| 59990 | |
| 59991 | /* |
| 59992 | ** Return the offset of the sector boundary at or immediately |
| 59993 | ** following the value in pPager->journalOff, assuming a sector |
| @@ -61106,13 +61188,11 @@ | |
| 61188 | sqlite3_file *pSuper; /* Malloc'd super-journal file descriptor */ |
| 61189 | sqlite3_file *pJournal; /* Malloc'd child-journal file descriptor */ |
| 61190 | char *zSuperJournal = 0; /* Contents of super-journal file */ |
| 61191 | i64 nSuperJournal; /* Size of super-journal file */ |
| 61192 | char *zJournal; /* Pointer to one journal within MJ file */ |
| 61193 | char *zFree = 0; /* Free this buffer */ |
| 61194 | |
| 61195 | /* Allocate space for both the pJournal and pSuper file descriptors. |
| 61196 | ** If successful, open the super-journal file for reading. |
| 61197 | */ |
| 61198 | pSuper = (sqlite3_file *)sqlite3MallocZero(2 * (i64)pVfs->szOsFile); |
| @@ -61131,22 +61211,20 @@ | |
| 61211 | ** sufficient space (in zSuperPtr) to hold the names of super-journal |
| 61212 | ** files extracted from regular rollback-journals. |
| 61213 | */ |
| 61214 | rc = sqlite3OsFileSize(pSuper, &nSuperJournal); |
| 61215 | if( rc!=SQLITE_OK ) goto delsuper_out; |
| 61216 | assert( nSuperJournal>=0 ); |
| 61217 | zFree = sqlite3Malloc(4 + nSuperJournal + 2); |
| 61218 | if( !zFree ){ |
| 61219 | rc = SQLITE_NOMEM_BKPT; |
| 61220 | goto delsuper_out; |
| 61221 | }else{ |
| 61222 | assert( nSuperJournal<=0x7fffffff ); |
| 61223 | } |
| 61224 | zFree[0] = zFree[1] = zFree[2] = zFree[3] = 0; |
| 61225 | zSuperJournal = &zFree[4]; |
| 61226 | rc = sqlite3OsRead(pSuper, zSuperJournal, (int)nSuperJournal, 0); |
| 61227 | if( rc!=SQLITE_OK ) goto delsuper_out; |
| 61228 | zSuperJournal[nSuperJournal] = 0; |
| 61229 | zSuperJournal[nSuperJournal+1] = 0; |
| 61230 | |
| @@ -61156,10 +61234,12 @@ | |
| 61234 | rc = sqlite3OsAccess(pVfs, zJournal, SQLITE_ACCESS_EXISTS, &exists); |
| 61235 | if( rc!=SQLITE_OK ){ |
| 61236 | goto delsuper_out; |
| 61237 | } |
| 61238 | if( exists ){ |
| 61239 | char *zSuperPtr = 0; |
| 61240 | |
| 61241 | /* One of the journals pointed to by the super-journal exists. |
| 61242 | ** Open it and check if it points at the super-journal. If |
| 61243 | ** so, return without deleting the super-journal file. |
| 61244 | ** NB: zJournal is really a MAIN_JOURNAL. But call it a |
| 61245 | ** SUPER_JOURNAL here so that the VFS will not send the zJournal |
| @@ -61170,17 +61250,19 @@ | |
| 61250 | rc = sqlite3OsOpen(pVfs, zJournal, pJournal, flags, 0); |
| 61251 | if( rc!=SQLITE_OK ){ |
| 61252 | goto delsuper_out; |
| 61253 | } |
| 61254 | |
| 61255 | rc = readSuperJournal(pJournal, 1+(u64)pVfs->mxPathname, &zSuperPtr); |
| 61256 | sqlite3OsClose(pJournal); |
| 61257 | if( rc!=SQLITE_OK ){ |
| 61258 | assert( zSuperPtr==0 ); |
| 61259 | goto delsuper_out; |
| 61260 | } |
| 61261 | |
| 61262 | c = zSuperPtr!=0 && strcmp(zSuperPtr, zSuper)==0; |
| 61263 | freeSuperJournal(zSuperPtr); |
| 61264 | if( c ){ |
| 61265 | /* We have a match. Do not delete the super-journal file. */ |
| 61266 | goto delsuper_out; |
| 61267 | } |
| 61268 | } |
| @@ -61391,23 +61473,15 @@ | |
| 61473 | |
| 61474 | /* Read the super-journal name from the journal, if it is present. |
| 61475 | ** If a super-journal file name is specified, but the file is not |
| 61476 | ** present on disk, then the journal is not hot and does not need to be |
| 61477 | ** played back. |
| 61478 | */ |
| 61479 | rc = readSuperJournal(pPager->jfd, 1+(i64)pPager->pVfs->mxPathname, &zSuper); |
| 61480 | if( rc==SQLITE_OK && zSuper ){ |
| 61481 | rc = sqlite3OsAccess(pVfs, zSuper, SQLITE_ACCESS_EXISTS, &res); |
| 61482 | } |
| 61483 | if( rc!=SQLITE_OK || !res ){ |
| 61484 | goto end_playback; |
| 61485 | } |
| 61486 | pPager->journalOff = 0; |
| 61487 | needPagerReset = isHot; |
| @@ -61532,34 +61606,24 @@ | |
| 61606 | ** problems for other processes at some point in the future. So, just |
| 61607 | ** in case this has happened, clear the changeCountDone flag now. |
| 61608 | */ |
| 61609 | pPager->changeCountDone = pPager->tempFile; |
| 61610 | |
| 61611 | if( rc==SQLITE_OK |
| 61612 | && (pPager->eState>=PAGER_WRITER_DBMOD || pPager->eState==PAGER_OPEN) |
| 61613 | ){ |
| 61614 | rc = sqlite3PagerSync(pPager, 0); |
| 61615 | } |
| 61616 | if( rc==SQLITE_OK ){ |
| 61617 | rc = pager_end_transaction(pPager, zSuper!=0, 0); |
| 61618 | testcase( rc!=SQLITE_OK ); |
| 61619 | } |
| 61620 | if( rc==SQLITE_OK && zSuper && res ){ |
| 61621 | /* If there was a super-journal and this routine will return success, |
| 61622 | ** see if it is possible to delete the super-journal. |
| 61623 | */ |
| 61624 | assert( memcmp(&zSuper[-4], "\0\0\0\0", 4)==0 ); |
| 61625 | rc = pager_delsuper(pPager, zSuper); |
| 61626 | testcase( rc!=SQLITE_OK ); |
| 61627 | } |
| 61628 | if( isHot && nPlayback ){ |
| 61629 | sqlite3_log(SQLITE_NOTICE_RECOVER_ROLLBACK, "recovered %d pages from %s", |
| @@ -61568,10 +61632,11 @@ | |
| 61632 | |
| 61633 | /* The Pager.sectorSize variable may have been updated while rolling |
| 61634 | ** back a journal created by a process with a different sector size |
| 61635 | ** value. Reset it to the correct value for this process. |
| 61636 | */ |
| 61637 | freeSuperJournal(zSuper); |
| 61638 | setSectorSize(pPager); |
| 61639 | return rc; |
| 61640 | } |
| 61641 | |
| 61642 | |
| @@ -67426,10 +67491,16 @@ | |
| 67491 | */ |
| 67492 | pgno = sqlite3Get4byte(&aFrame[0]); |
| 67493 | if( pgno==0 ){ |
| 67494 | return 0; |
| 67495 | } |
| 67496 | |
| 67497 | /* Need a valid page size |
| 67498 | */ |
| 67499 | if( !pWal->szPage ){ |
| 67500 | return 0; |
| 67501 | } |
| 67502 | |
| 67503 | /* A frame is only valid if a checksum of the WAL header, |
| 67504 | ** all prior frames, the first 16 bytes of this frame-header, |
| 67505 | ** and the frame-data matches the checksum in the last 8 |
| 67506 | ** bytes of this frame-header. |
| @@ -69281,11 +69352,11 @@ | |
| 69352 | goto begin_unreliable_shm_out; |
| 69353 | } |
| 69354 | |
| 69355 | /* Allocate a buffer to read frames into */ |
| 69356 | assert( (pWal->szPage & (pWal->szPage-1))==0 ); |
| 69357 | assert( (pWal->szPage>=512 && pWal->szPage<=65536) || pWal->szPage==0 ); |
| 69358 | szFrame = pWal->szPage + WAL_FRAME_HDRSIZE; |
| 69359 | aFrame = (u8 *)sqlite3_malloc64(szFrame); |
| 69360 | if( aFrame==0 ){ |
| 69361 | rc = SQLITE_NOMEM_BKPT; |
| 69362 | goto begin_unreliable_shm_out; |
| @@ -83086,10 +83157,15 @@ | |
| 83157 | if( pc+info.nSize>usableSize ){ |
| 83158 | checkAppendMsg(pCheck, "Extends off end of page"); |
| 83159 | doCoverageCheck = 0; |
| 83160 | continue; |
| 83161 | } |
| 83162 | if( info.nPayload && info.pPayload[0]<2 ){ |
| 83163 | checkAppendMsg(pCheck, "Bad cell header size"); |
| 83164 | doCoverageCheck = 0; |
| 83165 | continue; |
| 83166 | } |
| 83167 | |
| 83168 | /* Check for integer primary key out of range */ |
| 83169 | if( pPage->intKey ){ |
| 83170 | if( keyCanBeEqual ? (info.nKey > maxKey) : (info.nKey >= maxKey) ){ |
| 83171 | checkAppendMsg(pCheck, "Rowid %lld out of order", info.nKey); |
| @@ -84596,12 +84672,12 @@ | |
| 84672 | /* Work-around for GCC bug or bugs: |
| 84673 | ** https://gcc.gnu.org/bugzilla/show_bug.cgi?id=96270 |
| 84674 | ** https://gcc.gnu.org/bugzilla/show_bug.cgi?id=114659 |
| 84675 | ** The problem appears to be fixed in GCC 15 */ |
| 84676 | i64 x; |
| 84677 | assert( (sqlite3Config.bSmallMalloc!=0xee)*8==sizeof(x) ); |
| 84678 | memcpy(&x, (char*)&p->u.i, (sqlite3Config.bSmallMalloc!=0xee)*8); |
| 84679 | p->n = sqlite3Int64ToText(x, zBuf); |
| 84680 | #else |
| 84681 | p->n = sqlite3Int64ToText(p->u.i, zBuf); |
| 84682 | #endif |
| 84683 | if( p->flags & MEM_IntReal ){ |
| @@ -95448,11 +95524,11 @@ | |
| 95524 | QueryPerformanceCounter(&tm); |
| 95525 | return (sqlite3_uint64)tm.QuadPart; |
| 95526 | } |
| 95527 | |
| 95528 | #elif !defined(__STRICT_ANSI__) && defined(__GNUC__) && \ |
| 95529 | (defined(i586) || defined(__i586__) || defined(_M_IX86)) |
| 95530 | |
| 95531 | __inline__ sqlite_uint64 sqlite3Hwtime(void){ |
| 95532 | unsigned int lo, hi; |
| 95533 | __asm__ __volatile__ ("rdtsc" : "=a" (lo), "=d" (hi)); |
| 95534 | return (sqlite_uint64)hi << 32 | lo; |
| @@ -103037,11 +103113,11 @@ | |
| 103113 | if( pFrame ) break; |
| 103114 | } |
| 103115 | |
| 103116 | if( p->nFrame>=db->aLimit[SQLITE_LIMIT_TRIGGER_DEPTH] ){ |
| 103117 | rc = SQLITE_ERROR; |
| 103118 | sqlite3VdbeError(p, "triggers nested too deep"); |
| 103119 | goto abort_due_to_error; |
| 103120 | } |
| 103121 | |
| 103122 | /* Register pRt is used to store the memory required to save the state |
| 103123 | ** of the current program, and the memory required at runtime to execute |
| @@ -110164,11 +110240,11 @@ | |
| 110240 | /* |
| 110241 | ** cnt==0 means there was not match. |
| 110242 | ** cnt>1 means there were two or more matches. |
| 110243 | ** |
| 110244 | ** cnt==0 is always an error. cnt>1 is often an error, but might |
| 110245 | ** be multiple matches for a NATURAL OUTER JOIN or a OUTER JOIN USING. |
| 110246 | */ |
| 110247 | assert( pFJMatch==0 || cnt>0 ); |
| 110248 | assert( !ExprHasProperty(pExpr, EP_xIsSelect|EP_IntValue) ); |
| 110249 | if( cnt!=1 ){ |
| 110250 | const char *zErr; |
| @@ -110247,12 +110323,21 @@ | |
| 110323 | pExpr->op = eNewExprOp; |
| 110324 | lookupname_end: |
| 110325 | if( cnt==1 ){ |
| 110326 | assert( pNC!=0 ); |
| 110327 | #ifndef SQLITE_OMIT_AUTHORIZATION |
| 110328 | if( db->xAuth ){ |
| 110329 | if( pFJMatch ){ |
| 110330 | assert( pExpr->op==TK_FUNCTION ); |
| 110331 | assert( sqlite3_stricmp(pExpr->u.zToken,"coalesce")==0 ); |
| 110332 | assert( pExpr->x.pList==pFJMatch ); |
| 110333 | assert( pFJMatch->nExpr>0 ); |
| 110334 | pExpr = pFJMatch->a[0].pExpr; |
| 110335 | } |
| 110336 | if( pExpr->op==TK_COLUMN || pExpr->op==TK_TRIGGER ){ |
| 110337 | sqlite3AuthRead(pParse, pExpr, pSchema, pNC->pSrcList); |
| 110338 | } |
| 110339 | } |
| 110340 | #endif |
| 110341 | /* Increment the nRef value on all name contexts from TopNC up to |
| 110342 | ** the point where the name matched. */ |
| 110343 | for(;;){ |
| @@ -115930,10 +116015,11 @@ | |
| 116015 | if( i!=nVector ){ |
| 116016 | /* Need to reorder the LHS fields according to aiMap */ |
| 116017 | int rLhsOrig = rLhs; |
| 116018 | rLhs = sqlite3GetTempRange(pParse, nVector); |
| 116019 | for(i=0; i<nVector; i++){ |
| 116020 | testcase( aiMap[i]!=i ); |
| 116021 | sqlite3VdbeAddOp3(v, OP_Copy, rLhsOrig+i, rLhs+aiMap[i], 0); |
| 116022 | } |
| 116023 | sqlite3ReleaseTempReg(pParse, rLhsOrig); |
| 116024 | } |
| 116025 | } |
| @@ -115950,11 +116036,12 @@ | |
| 116036 | } |
| 116037 | for(i=0; i<nVector; i++){ |
| 116038 | Expr *p = sqlite3VectorFieldSubexpr(pExpr->pLeft, i); |
| 116039 | if( pParse->nErr ) goto sqlite3ExprCodeIN_oom_error; |
| 116040 | if( sqlite3ExprCanBeNull(p) ){ |
| 116041 | testcase( aiMap[i]!=i ); |
| 116042 | sqlite3VdbeAddOp2(v, OP_IsNull, rLhs+aiMap[i], destStep2); |
| 116043 | VdbeCoverage(v); |
| 116044 | } |
| 116045 | } |
| 116046 | |
| 116047 | /* Step 3. The LHS is now known to be non-NULL. Do the binary search |
| @@ -116024,13 +116111,23 @@ | |
| 116111 | for(i=0; i<nVector; i++){ |
| 116112 | Expr *p; |
| 116113 | CollSeq *pColl; |
| 116114 | int r3 = sqlite3GetTempReg(pParse); |
| 116115 | p = sqlite3VectorFieldSubexpr(pLeft, i); |
| 116116 | if( ExprUseXSelect(pExpr) ){ |
| 116117 | Expr *pRhs = pExpr->x.pSelect->pEList->a[i].pExpr; |
| 116118 | pColl = sqlite3BinaryCompareCollSeq(pParse, p, pRhs); |
| 116119 | }else{ |
| 116120 | /* If the RHS of the IN(...) expression are scalar expressions, do |
| 116121 | ** not consider their collation sequences. The documentation says |
| 116122 | ** "The collating sequence used for expressions of the form "x IN (y, z, |
| 116123 | ** ...)" is the collating sequence of x.". */ |
| 116124 | pColl = sqlite3ExprCollSeq(pParse, p); |
| 116125 | } |
| 116126 | testcase( aiMap[i]!=i ); |
| 116127 | sqlite3VdbeAddOp3(v, OP_Column, iTab, aiMap[i], r3); |
| 116128 | sqlite3VdbeAddOp4(v, OP_Ne, rLhs+aiMap[i], destNotNull, r3, |
| 116129 | (void*)pColl, P4_COLLSEQ); |
| 116130 | VdbeCoverage(v); |
| 116131 | sqlite3ReleaseTempReg(pParse, r3); |
| 116132 | } |
| 116133 | sqlite3VdbeAddOp2(v, OP_Goto, 0, destIfNull); |
| @@ -124365,21 +124462,21 @@ | |
| 124462 | }else{ |
| 124463 | nIdxCol = pIdx->nColumn; |
| 124464 | } |
| 124465 | pIdx->nSampleCol = nIdxCol; |
| 124466 | pIdx->mxSample = nSample; |
| 124467 | nByte = ROUND8(sizeof64(IndexSample) * nSample); |
| 124468 | nByte += sizeof64(tRowcnt) * nIdxCol * 3 * nSample; |
| 124469 | nByte += nIdxCol * sizeof64(tRowcnt); /* Space for Index.aAvgEq[] */ |
| 124470 | |
| 124471 | pIdx->aSample = sqlite3DbMallocZero(db, nByte); |
| 124472 | if( pIdx->aSample==0 ){ |
| 124473 | sqlite3_finalize(pStmt); |
| 124474 | return SQLITE_NOMEM_BKPT; |
| 124475 | } |
| 124476 | pPtr = (u8*)pIdx->aSample; |
| 124477 | pPtr += ROUND8(nSample*sizeof64(pIdx->aSample[0])); |
| 124478 | pSpace = (tRowcnt*)pPtr; |
| 124479 | assert( EIGHT_BYTE_ALIGNMENT( pSpace ) ); |
| 124480 | pIdx->aAvgEq = pSpace; pSpace += nIdxCol; |
| 124481 | pIdx->pTable->tabFlags |= TF_HasStat4; |
| 124482 | for(i=0; i<nSample; i++){ |
| @@ -133231,13 +133328,22 @@ | |
| 133328 | x.nUsed = 0; |
| 133329 | x.apArg = argv+1; |
| 133330 | sqlite3StrAccumInit(&str, db, 0, 0, db->aLimit[SQLITE_LIMIT_LENGTH]); |
| 133331 | str.printfFlags = SQLITE_PRINTF_SQLFUNC; |
| 133332 | sqlite3_str_appendf(&str, zFormat, &x); |
| 133333 | if( str.accError==SQLITE_OK ){ |
| 133334 | n = str.nChar; |
| 133335 | sqlite3_result_text(context, sqlite3StrAccumFinish(&str), n, |
| 133336 | SQLITE_DYNAMIC); |
| 133337 | }else{ |
| 133338 | if( str.accError==SQLITE_NOMEM ){ |
| 133339 | sqlite3_result_error_nomem(context); |
| 133340 | }else{ |
| 133341 | sqlite3_result_error_toobig(context); |
| 133342 | } |
| 133343 | sqlite3_str_reset(&str); |
| 133344 | } |
| 133345 | } |
| 133346 | } |
| 133347 | |
| 133348 | /* |
| 133349 | ** Implementation of the substr() function. |
| @@ -134405,11 +134511,11 @@ | |
| 134511 | int nStr; /* Size of zStr */ |
| 134512 | int nPattern; /* Size of zPattern */ |
| 134513 | int nRep; /* Size of zRep */ |
| 134514 | i64 nOut; /* Maximum size of zOut */ |
| 134515 | int loopLimit; /* Last zStr[] that might match zPattern[] */ |
| 134516 | i64 i, j; /* Loop counters */ |
| 134517 | unsigned cntExpand; /* Number zOut expansions */ |
| 134518 | sqlite3 *db = sqlite3_context_db_handle(context); |
| 134519 | |
| 134520 | assert( argc==3 ); |
| 134521 | UNUSED_PARAMETER(argc); |
| @@ -135859,57 +135965,95 @@ | |
| 135965 | ** from the standard library because: |
| 135966 | ** |
| 135967 | ** (1) To avoid a dependency on qsort() |
| 135968 | ** (2) To avoid the function call to the comparison routine for each |
| 135969 | ** comparison. |
| 135970 | ** |
| 135971 | ** If parameter iReq is non-negative, then the caller will only access |
| 135972 | ** elements a[iReq] and a[iReq+1] (if it exists) of the sorted array and |
| 135973 | ** so it is not necessary to position any other elements. Or if iReq is |
| 135974 | ** negative, then the final array must be fully sorted. |
| 135975 | */ |
| 135976 | static void percentSort( |
| 135977 | double *a, /* Array to sort */ |
| 135978 | unsigned int n, /* Number of elements in array a[] */ |
| 135979 | int iReq /* Element caller cares about (or -ve) */ |
| 135980 | ){ |
| 135981 | int iLt; /* Entries before a[iLt] are less than rPivot */ |
| 135982 | int iGt; /* Entries at or after a[iGt] are greater than rPivot */ |
| 135983 | int i; /* Loop counter */ |
| 135984 | double rPivot; /* The pivot value */ |
| 135985 | |
| 135986 | assert( n>=2 ); |
| 135987 | do{ |
| 135988 | if( a[0]>a[n-1] ){ |
| 135989 | SWAP_DOUBLE(a[0],a[n-1]) |
| 135990 | } |
| 135991 | if( n==2 ) return; |
| 135992 | iGt = n-1; |
| 135993 | i = n/2; |
| 135994 | if( a[0]>a[i] ){ |
| 135995 | SWAP_DOUBLE(a[0],a[i]) |
| 135996 | }else if( a[i]>a[iGt] ){ |
| 135997 | SWAP_DOUBLE(a[i],a[iGt]) |
| 135998 | } |
| 135999 | if( n==3 ) return; |
| 136000 | rPivot = a[i]; |
| 136001 | iLt = i = 1; |
| 136002 | do{ |
| 136003 | if( a[i]<rPivot ){ |
| 136004 | if( i>iLt ) SWAP_DOUBLE(a[i],a[iLt]) |
| 136005 | iLt++; |
| 136006 | i++; |
| 136007 | }else if( a[i]>rPivot ){ |
| 136008 | do{ |
| 136009 | iGt--; |
| 136010 | }while( iGt>i && a[iGt]>rPivot ); |
| 136011 | SWAP_DOUBLE(a[i],a[iGt]) |
| 136012 | }else{ |
| 136013 | i++; |
| 136014 | } |
| 136015 | }while( i<iGt ); |
| 136016 | |
| 136017 | assert( a[iLt]==rPivot ); |
| 136018 | assert( iGt>iLt ); |
| 136019 | |
| 136020 | if( iReq>=0 ){ |
| 136021 | /* In this case, the only elements that the caller requires sorted into |
| 136022 | ** the correct positions are elements a[iReq] and a[iReq+1]. At this |
| 136023 | ** point we know that element a[iLt] is in the correct position and |
| 136024 | ** all elements smaller than a[iLt] are in the left-hand partition. |
| 136025 | ** So if (iReq<iLt), then it is only necessary to sort the left |
| 136026 | ** partition. |
| 136027 | ** |
| 136028 | ** If (iReq>=iLt), then elements iReq and iReq+1 are either in the |
| 136029 | ** right partition or the equal partition (elements for which |
| 136030 | ** iLt<=iElem<iGt). Therefore it is always sufficient to sort only |
| 136031 | ** the right partition in this case. */ |
| 136032 | if( iReq<iLt ){ |
| 136033 | n = iLt; |
| 136034 | }else{ |
| 136035 | a += iGt; |
| 136036 | n -= iGt; |
| 136037 | iReq = MAX(0, iReq-iGt); |
| 136038 | } |
| 136039 | }else{ |
| 136040 | /* Recurse on the smaller partition only. The smaller partition |
| 136041 | ** will hold n/2 or fewer entries, which assures that the stack |
| 136042 | ** depth will not exceed O(log(n)), even for pathological cases. |
| 136043 | ** Loop without recursion for the larger partition. */ |
| 136044 | if( iLt>(int)(n/2) ){ |
| 136045 | if( n-iGt>=2 ) percentSort(a+iGt, n-iGt, -1); |
| 136046 | n = iLt; |
| 136047 | }else{ |
| 136048 | if( iLt>=2 ) percentSort(a, iLt, -1); |
| 136049 | a += iGt; |
| 136050 | n -= iGt; |
| 136051 | } |
| 136052 | } |
| 136053 | }while( n>=2 ); |
| 136054 | } |
| 136055 | |
| 136056 | /* |
| 136057 | ** The "inverse" function for percentile(Y,P) is called to remove a |
| 136058 | ** row that was previously inserted by "step". |
| 136059 | */ |
| @@ -135939,11 +136083,11 @@ | |
| 136083 | if( percentIsInfinity(y) ){ |
| 136084 | return; |
| 136085 | } |
| 136086 | if( p->bSorted==0 ){ |
| 136087 | assert( p->nUsed>1 ); |
| 136088 | percentSort(p->a, p->nUsed, -1); |
| 136089 | p->bSorted = 1; |
| 136090 | } |
| 136091 | p->bKeepSorted = 1; |
| 136092 | |
| 136093 | /* Find and remove the row */ |
| @@ -135968,17 +136112,21 @@ | |
| 136112 | double ix, vx; |
| 136113 | p = (Percentile*)sqlite3_aggregate_context(pCtx, 0); |
| 136114 | if( p==0 ) return; |
| 136115 | if( p->a==0 ) return; |
| 136116 | if( p->nUsed ){ |
| 136117 | ix = p->rPct*(p->nUsed-1); |
| 136118 | i1 = (unsigned)ix; |
| 136119 | if( p->bSorted==0 ){ |
| 136120 | /* In cases where bIsFinal is non-zero, setting Percentile.bSorted |
| 136121 | ** after the percentSort() call here is not technically correct, as |
| 136122 | ** the array is not fully sorted. But in this case the object will be |
| 136123 | ** freed below anyway, so it doesn't matter. */ |
| 136124 | assert( p->nUsed>1 ); |
| 136125 | percentSort(p->a, p->nUsed, (bIsFinal ? (int)i1 : -1)); |
| 136126 | p->bSorted = 1; |
| 136127 | } |
| 136128 | if( settings & 1 ){ |
| 136129 | vx = p->a[i1]; |
| 136130 | }else{ |
| 136131 | i2 = ix==(double)i1 || i1==p->nUsed-1 ? i1 : i1+1; |
| 136132 | v1 = p->a[i1]; |
| @@ -150488,10 +150636,17 @@ | |
| 150636 | SQLITE_PRIVATE Table *sqlite3ResultSetOfSelect(Parse *pParse, Select *pSelect, char aff){ |
| 150637 | Table *pTab; |
| 150638 | sqlite3 *db = pParse->db; |
| 150639 | u64 savedFlags; |
| 150640 | |
| 150641 | pParse->nNestSel++; |
| 150642 | #if SQLITE_MAX_EXPR_DEPTH>0 |
| 150643 | if( pParse->nNestSel >= db->aLimit[SQLITE_LIMIT_EXPR_DEPTH] ){ |
| 150644 | sqlite3ErrorMsg(pParse, "VIEWs and/or subqueries nested too deep"); |
| 150645 | return 0; |
| 150646 | } |
| 150647 | #endif |
| 150648 | savedFlags = db->flags; |
| 150649 | db->flags &= ~(u64)SQLITE_FullColNames; |
| 150650 | db->flags |= SQLITE_ShortColNames; |
| 150651 | sqlite3SelectPrep(pParse, pSelect, 0); |
| 150652 | db->flags = savedFlags; |
| @@ -150509,10 +150664,12 @@ | |
| 150664 | pTab->iPKey = -1; |
| 150665 | if( db->mallocFailed ){ |
| 150666 | sqlite3DeleteTable(db, pTab); |
| 150667 | return 0; |
| 150668 | } |
| 150669 | pParse->nNestSel--; |
| 150670 | assert( pParse->nNestSel>=0 ); |
| 150671 | return pTab; |
| 150672 | } |
| 150673 | |
| 150674 | /* |
| 150675 | ** Get a VDBE for the given parser context. Create a new one if necessary. |
| @@ -155479,12 +155636,15 @@ | |
| 155636 | ** Then, if CheckOnCtx.iJoin indicates that this expression is part of an |
| 155637 | ** ON clause from that SrcList (i.e. if iJoin is non-zero), check that it |
| 155638 | ** does not refer to a table to the right of CheckOnCtx.iJoin. */ |
| 155639 | do { |
| 155640 | SrcList *pSrc = pCtx->pSrc; |
| 155641 | int nSrc = pSrc->nSrc; |
| 155642 | int iTab = pExpr->iTable; |
| 155643 | int ii; |
| 155644 | for(ii=0; ii<nSrc && pSrc->a[ii].iCursor!=iTab; ii++){} |
| 155645 | if( ii<nSrc ){ |
| 155646 | if( pCtx->iJoin && iTab>pCtx->iJoin ){ |
| 155647 | sqlite3ErrorMsg(pWalker->pParse, |
| 155648 | "%s references tables to its right", |
| 155649 | (pCtx->bFuncArg ? "table-function argument" : "ON clause") |
| 155650 | ); |
| @@ -158449,22 +158609,36 @@ | |
| 158609 | Parse *pParse, /* Current parse context */ |
| 158610 | Trigger *pTrigger, /* Trigger to code */ |
| 158611 | Table *pTab, /* The table pTrigger is attached to */ |
| 158612 | int orconf /* ON CONFLICT policy to code trigger program with */ |
| 158613 | ){ |
| 158614 | Parse *pTop; /* Top level Parse object */ |
| 158615 | sqlite3 *db = pParse->db; /* Database handle */ |
| 158616 | TriggerPrg *pPrg; /* Value to return */ |
| 158617 | Expr *pWhen = 0; /* Duplicate of trigger WHEN expression */ |
| 158618 | Vdbe *v; /* Temporary VM */ |
| 158619 | NameContext sNC; /* Name context for sub-vdbe */ |
| 158620 | SubProgram *pProgram = 0; /* Sub-vdbe for trigger program */ |
| 158621 | int iEndTrigger = 0; /* Label to jump to if WHEN is false */ |
| 158622 | Parse sSubParse; /* Parse context for sub-vdbe */ |
| 158623 | int nDepth; /* Trigger depth */ |
| 158624 | |
| 158625 | /* Ensure that triggers are not chained too deep. This test is linear |
| 158626 | ** in the chaining depth, but sensible code ought not be chaining |
| 158627 | ** triggers excessively, so that shouldn't be a problem. |
| 158628 | */ |
| 158629 | pTop = pParse; |
| 158630 | for(nDepth=0; pTop->pOuterParse; pTop = pTop->pOuterParse, nDepth++){} |
| 158631 | if( nDepth>=db->aLimit[SQLITE_LIMIT_TRIGGER_DEPTH] ){ |
| 158632 | sqlite3ErrorMsg(pParse, "triggers nested too deep"); |
| 158633 | return 0; |
| 158634 | } |
| 158635 | |
| 158636 | pTop = sqlite3ParseToplevel(pParse); |
| 158637 | assert( pTrigger->zName==0 || pTab==tableOfTrigger(pTrigger) ); |
| 158638 | assert( pTop->pVdbe ); |
| 158639 | |
| 158640 | |
| 158641 | /* Allocate the TriggerPrg and SubProgram objects. To ensure that they |
| 158642 | ** are freed if an error occurs, link them into the Parse.pTriggerPrg |
| 158643 | ** list of the top-level Parse object sooner rather than later. */ |
| 158644 | pPrg = sqlite3DbMallocZero(db, sizeof(TriggerPrg)); |
| @@ -160462,11 +160636,12 @@ | |
| 160636 | ** So we have to make a copy before passing it down into sqlite3Update() */ |
| 160637 | pSrc = sqlite3SrcListDup(db, pTop->pUpsertSrc, 0); |
| 160638 | /* excluded.* columns of type REAL need to be converted to a hard real */ |
| 160639 | for(i=0; i<pTab->nCol; i++){ |
| 160640 | if( pTab->aCol[i].affinity==SQLITE_AFF_REAL ){ |
| 160641 | int iStorage = pTop->regData + sqlite3TableColumnToStorage(pTab, i); |
| 160642 | sqlite3VdbeAddOp1(v, OP_RealAffinity, iStorage); |
| 160643 | } |
| 160644 | } |
| 160645 | sqlite3Update(pParse, pSrc, sqlite3ExprListDup(db,pUpsert->pUpsertSet,0), |
| 160646 | sqlite3ExprDup(db,pUpsert->pUpsertWhere,0), OE_Abort, 0, 0, pUpsert); |
| 160647 | VdbeNoopComment((v, "End DO UPDATE of UPSERT")); |
| @@ -165680,10 +165855,11 @@ | |
| 165855 | assert( (pTerm->eOperator & (WO_OR|WO_AND))==0 ); |
| 165856 | pAlt = sqlite3WhereFindTerm(pWC, iCur, pTerm->u.x.leftColumn, notReady, |
| 165857 | WO_EQ|WO_IN|WO_IS, 0); |
| 165858 | if( pAlt==0 ) continue; |
| 165859 | if( pAlt->wtFlags & (TERM_CODED) ) continue; |
| 165860 | if( ExprHasProperty(pAlt->pExpr, EP_Collate) ) continue; |
| 165861 | if( (pAlt->eOperator & WO_IN) |
| 165862 | && ExprUseXSelect(pAlt->pExpr) |
| 165863 | && (pAlt->pExpr->x.pSelect->pEList->nExpr>1) |
| 165864 | ){ |
| 165865 | continue; |
| @@ -166433,11 +166609,14 @@ | |
| 166609 | ** Mark term iChild as being a child of term iParent |
| 166610 | */ |
| 166611 | static void markTermAsChild(WhereClause *pWC, int iChild, int iParent){ |
| 166612 | pWC->a[iChild].iParent = iParent; |
| 166613 | pWC->a[iChild].truthProb = pWC->a[iParent].truthProb; |
| 166614 | assert( pWC->a[iParent].nChild < UMXV(pWC->a[0].nChild) ); |
| 166615 | pWC->a[iParent].nChild++; |
| 166616 | testcase( pWC->a[iParent].nChild == UMXV(pWC->a[0].nChild) ); |
| 166617 | |
| 166618 | } |
| 166619 | |
| 166620 | /* |
| 166621 | ** Return the N-th AND-connected subterm of pTerm. Or if pTerm is not |
| 166622 | ** a conjunction, then return just pTerm when N==0. If N is exceeds |
| @@ -166872,21 +167051,22 @@ | |
| 167051 | ** 1. The SQLITE_Transitive optimization must be enabled |
| 167052 | ** 2. Must be either an == or an IS operator |
| 167053 | ** 3. Not originating in the ON clause of an OUTER JOIN |
| 167054 | ** 4. The operator is not IS or else the query does not contain RIGHT JOIN |
| 167055 | ** 5. The affinities of A and B must be compatible |
| 167056 | ** 6. Both operands use the same collating sequence, and they must not |
| 167057 | ** use explicit COLLATE clauses. |
| 167058 | ** If this routine returns TRUE, that means that the RHS can be substituted |
| 167059 | ** for the LHS anyplace else in the WHERE clause where the LHS column occurs. |
| 167060 | ** This is an optimization. No harm comes from returning 0. But if 1 is |
| 167061 | ** returned when it should not be, then incorrect answers might result. |
| 167062 | */ |
| 167063 | static int termIsEquivalence(Parse *pParse, Expr *pExpr, SrcList *pSrc){ |
| 167064 | char aff1, aff2; |
| 167065 | if( !OptimizationEnabled(pParse->db, SQLITE_Transitive) ) return 0; /* (1) */ |
| 167066 | if( pExpr->op!=TK_EQ && pExpr->op!=TK_IS ) return 0; /* (2) */ |
| 167067 | if( ExprHasProperty(pExpr, EP_OuterON|EP_Collate) ) return 0; /* (3) */ |
| 167068 | assert( pSrc!=0 ); |
| 167069 | if( pExpr->op==TK_IS |
| 167070 | && pSrc->nSrc>=2 |
| 167071 | && (pSrc->a[0].fg.jointype & JT_LTORJ)!=0 |
| 167072 | ){ |
| @@ -167209,10 +167389,11 @@ | |
| 167389 | static const u8 ops[] = {TK_GE, TK_LE}; |
| 167390 | assert( ExprUseXList(pExpr) ); |
| 167391 | pList = pExpr->x.pList; |
| 167392 | assert( pList!=0 ); |
| 167393 | assert( pList->nExpr==2 ); |
| 167394 | assert( pWC->a[idxTerm].nChild==0 ); |
| 167395 | for(i=0; i<2; i++){ |
| 167396 | Expr *pNewExpr; |
| 167397 | int idxNew; |
| 167398 | pNewExpr = sqlite3PExpr(pParse, ops[i], |
| 167399 | sqlite3ExprDup(db, pExpr->pLeft, 0), |
| @@ -167419,12 +167600,15 @@ | |
| 167600 | && (pExpr->x.pSelect->pPrior==0 || (pExpr->x.pSelect->selFlags & SF_Values)) |
| 167601 | #ifndef SQLITE_OMIT_WINDOWFUNC |
| 167602 | && pExpr->x.pSelect->pWin==0 |
| 167603 | #endif |
| 167604 | && pWC->op==TK_AND |
| 167605 | && pExpr->x.pSelect->pEList->nExpr <= UMXV(pTerm->nChild) |
| 167606 | /* ^-- See bug 2026-06-04T10:00:49Z */ |
| 167607 | ){ |
| 167608 | int i; |
| 167609 | assert( pTerm->nChild==0 ); |
| 167610 | for(i=0; i<sqlite3ExprVectorSize(pExpr->pLeft); i++){ |
| 167611 | int idxNew; |
| 167612 | idxNew = whereClauseInsert(pWC, pExpr, TERM_VIRTUAL|TERM_SLICE); |
| 167613 | pWC->a[idxNew].u.x.iField = i+1; |
| 167614 | exprAnalyze(pSrc, pWC, idxNew); |
| @@ -186978,11 +187162,11 @@ | |
| 187162 | ** or disables the collection of memory allocation statistics. */ |
| 187163 | sqlite3GlobalConfig.bMemstat = va_arg(ap, int); |
| 187164 | break; |
| 187165 | } |
| 187166 | case SQLITE_CONFIG_SMALL_MALLOC: { |
| 187167 | sqlite3GlobalConfig.bSmallMalloc = va_arg(ap, int)!=0; |
| 187168 | break; |
| 187169 | } |
| 187170 | case SQLITE_CONFIG_PAGECACHE: { |
| 187171 | /* EVIDENCE-OF: R-18761-36601 There are three arguments to |
| 187172 | ** SQLITE_CONFIG_PAGECACHE: A pointer to 8-byte aligned memory (pMem), |
| @@ -193287,10 +193471,16 @@ | |
| 193471 | #ifndef SQLITE_CORE |
| 193472 | /* # include "sqlite3ext.h" */ |
| 193473 | SQLITE_EXTENSION_INIT1 |
| 193474 | #endif |
| 193475 | |
| 193476 | |
| 193477 | /* |
| 193478 | ** Assume any b-tree layer with more levels than this is corrupt. |
| 193479 | */ |
| 193480 | #define FTS3_MAX_BTREE_HEIGHT 48 |
| 193481 | |
| 193482 | typedef struct Fts3HashWrapper Fts3HashWrapper; |
| 193483 | struct Fts3HashWrapper { |
| 193484 | Fts3Hash hash; /* Hash table */ |
| 193485 | int nRef; /* Number of pointers to this object */ |
| 193486 | }; |
| @@ -195003,11 +195193,15 @@ | |
| 195193 | int iHeight; /* Height of this node in tree */ |
| 195194 | |
| 195195 | assert( piLeaf || piLeaf2 ); |
| 195196 | |
| 195197 | fts3GetVarint32(zNode, &iHeight); |
| 195198 | if( iHeight>FTS3_MAX_BTREE_HEIGHT ){ |
| 195199 | rc = FTS_CORRUPT_VTAB; |
| 195200 | }else{ |
| 195201 | rc = fts3ScanInteriorNode(zTerm, nTerm, zNode, nNode, piLeaf, piLeaf2); |
| 195202 | } |
| 195203 | assert_fts3_nc( !piLeaf2 || !piLeaf || rc!=SQLITE_OK || (*piLeaf<=*piLeaf2) ); |
| 195204 | |
| 195205 | if( rc==SQLITE_OK && iHeight>1 ){ |
| 195206 | char *zBlob = 0; /* Blob read from %_segments table */ |
| 195207 | int nBlob = 0; /* Size of zBlob in bytes */ |
| @@ -195048,12 +195242,17 @@ | |
| 195242 | char **pp, /* IN/OUT: Output pointer */ |
| 195243 | sqlite3_int64 *piPrev, /* IN/OUT: Previous value written to list */ |
| 195244 | sqlite3_int64 iVal /* Write this value to the list */ |
| 195245 | ){ |
| 195246 | assert_fts3_nc( iVal-*piPrev > 0 || (*piPrev==0 && iVal==0) ); |
| 195247 | if( iVal-(*piPrev)>=0 ){ |
| 195248 | /* Refuse to write a negative delta integer. This only happens with a |
| 195249 | ** corrupt db (see the assert above) and can cause buffer overwrites |
| 195250 | ** in some cases. */ |
| 195251 | *pp += sqlite3Fts3PutVarint(*pp, iVal-*piPrev); |
| 195252 | *piPrev = iVal; |
| 195253 | } |
| 195254 | } |
| 195255 | |
| 195256 | /* |
| 195257 | ** When this function is called, *ppPoslist is assumed to point to the |
| 195258 | ** start of a position-list. After it returns, *ppPoslist points to the |
| @@ -199532,11 +199731,11 @@ | |
| 199731 | break; |
| 199732 | |
| 199733 | /* State 3. The integer just read is a column number. */ |
| 199734 | default: assert( eState==3 ); |
| 199735 | iCol = (int)v; |
| 199736 | if( iCol<1 || iCol>(pFts3->nColumn+1) ){ |
| 199737 | rc = SQLITE_CORRUPT_VTAB; |
| 199738 | break; |
| 199739 | } |
| 199740 | if( fts3auxGrowStatArray(pCsr, iCol+2) ) return SQLITE_NOMEM; |
| 199741 | pCsr->aStat[iCol+1].nDoc++; |
| @@ -206469,10 +206668,14 @@ | |
| 206668 | iMul = -1; |
| 206669 | } |
| 206670 | for(/* no-op */; zText[i]>='0' && zText[i]<='9'; i++){ |
| 206671 | iVal = iVal*10 + (zText[i] - '0'); |
| 206672 | } |
| 206673 | |
| 206674 | /* This if() clause is just to avoid an integer overflow. The record is |
| 206675 | ** corrupt in this case. */ |
| 206676 | if( (i64)iVal==SMALLEST_INT64 ) iMul = 1; |
| 206677 | *pnByte = ((i64)iVal * (i64)iMul); |
| 206678 | } |
| 206679 | } |
| 206680 | |
| 206681 | |
| @@ -209331,14 +209534,13 @@ | |
| 209534 | */ |
| 209535 | |
| 209536 | /* |
| 209537 | ** Allocate a two-slot MatchinfoBuffer object. |
| 209538 | */ |
| 209539 | static MatchinfoBuffer *fts3MIBufferNew(i64 nElem, const char *zMatchinfo){ |
| 209540 | MatchinfoBuffer *pRet; |
| 209541 | sqlite3_int64 nByte = sizeof(u32) * (2*(i64)nElem+1) + SZ_MATCHINFOBUFFER(1); |
| 209542 | sqlite3_int64 nStr = strlen(zMatchinfo); |
| 209543 | |
| 209544 | pRet = sqlite3Fts3MallocZero(nByte + nStr+1); |
| 209545 | if( pRet ){ |
| 209546 | pRet->aMI[0] = (u8*)(&pRet->aMI[1]) - (u8*)pRet; |
| @@ -210205,12 +210407,12 @@ | |
| 210407 | } |
| 210408 | sqlite3Fts3ErrMsg(pzErr, "unrecognized matchinfo request: %c", cArg); |
| 210409 | return SQLITE_ERROR; |
| 210410 | } |
| 210411 | |
| 210412 | static i64 fts3MatchinfoSize(MatchInfo *pInfo, char cArg){ |
| 210413 | i64 nVal; /* Number of integers output by cArg */ |
| 210414 | |
| 210415 | switch( cArg ){ |
| 210416 | case FTS3_MATCHINFO_NDOC: |
| 210417 | case FTS3_MATCHINFO_NPHRASE: |
| 210418 | case FTS3_MATCHINFO_NCOL: |
| @@ -210222,20 +210424,20 @@ | |
| 210424 | case FTS3_MATCHINFO_LCS: |
| 210425 | nVal = pInfo->nCol; |
| 210426 | break; |
| 210427 | |
| 210428 | case FTS3_MATCHINFO_LHITS: |
| 210429 | nVal = (i64)pInfo->nCol * pInfo->nPhrase; |
| 210430 | break; |
| 210431 | |
| 210432 | case FTS3_MATCHINFO_LHITS_BM: |
| 210433 | nVal = (i64)pInfo->nPhrase * ((pInfo->nCol + 31) / 32); |
| 210434 | break; |
| 210435 | |
| 210436 | default: |
| 210437 | assert( cArg==FTS3_MATCHINFO_HITS ); |
| 210438 | nVal = (i64)pInfo->nCol * pInfo->nPhrase * 3; |
| 210439 | break; |
| 210440 | } |
| 210441 | |
| 210442 | return nVal; |
| 210443 | } |
| @@ -210513,11 +210715,11 @@ | |
| 210715 | } |
| 210716 | break; |
| 210717 | |
| 210718 | case FTS3_MATCHINFO_LHITS_BM: |
| 210719 | case FTS3_MATCHINFO_LHITS: { |
| 210720 | i64 nZero = fts3MatchinfoSize(pInfo, zArg[i]) * sizeof(u32); |
| 210721 | memset(pInfo->aMatchinfo, 0, nZero); |
| 210722 | rc = fts3ExprLHitGather(pCsr->pExpr, pInfo); |
| 210723 | break; |
| 210724 | } |
| 210725 | |
| @@ -210582,11 +210784,11 @@ | |
| 210784 | ** matchinfo function has been called for this query. In this case |
| 210785 | ** allocate the array used to accumulate the matchinfo data and |
| 210786 | ** initialize those elements that are constant for every row. |
| 210787 | */ |
| 210788 | if( pCsr->pMIBuffer==0 ){ |
| 210789 | i64 nMatchinfo = 0; /* Number of u32 elements in match-info */ |
| 210790 | int i; /* Used to iterate through zArg */ |
| 210791 | |
| 210792 | /* Determine the number of phrases in the query */ |
| 210793 | pCsr->nPhrase = fts3ExprPhraseCount(pCsr->pExpr); |
| 210794 | sInfo.nPhrase = pCsr->nPhrase; |
| @@ -218269,10 +218471,13 @@ | |
| 218471 | ** be because the shadow tables hold erroneous data. */ |
| 218472 | if( rc==SQLITE_ERROR ){ |
| 218473 | rc = SQLITE_CORRUPT_VTAB; |
| 218474 | RTREE_IS_CORRUPT(pRtree); |
| 218475 | } |
| 218476 | }else if( iNode<=0 ){ |
| 218477 | RTREE_IS_CORRUPT(pRtree); |
| 218478 | rc = SQLITE_CORRUPT_VTAB; |
| 218479 | }else if( pRtree->iNodeSize==sqlite3_blob_bytes(pRtree->pNodeBlob) ){ |
| 218480 | pNode = (RtreeNode *)sqlite3_malloc64(sizeof(RtreeNode)+pRtree->iNodeSize); |
| 218481 | if( !pNode ){ |
| 218482 | rc = SQLITE_NOMEM; |
| 218483 | }else{ |
| @@ -218910,11 +219115,11 @@ | |
| 219115 | i64 iRowid, |
| 219116 | int *piIndex |
| 219117 | ){ |
| 219118 | int ii; |
| 219119 | int nCell = NCELL(pNode); |
| 219120 | assert( nCell<65536 && nCell>=0 ); |
| 219121 | for(ii=0; ii<nCell; ii++){ |
| 219122 | if( nodeGetRowid(pRtree, pNode, ii)==iRowid ){ |
| 219123 | *piIndex = ii; |
| 219124 | return SQLITE_OK; |
| 219125 | } |
| @@ -223953,10 +224158,13 @@ | |
| 224158 | if( c>=0xc0 ){ \ |
| 224159 | c = icuUtf8Trans1[c-0xc0]; \ |
| 224160 | while( (*zIn & 0xc0)==0x80 ){ \ |
| 224161 | c = (c<<6) + (0x3f & *(zIn++)); \ |
| 224162 | } \ |
| 224163 | if( c<0x80 \ |
| 224164 | || (c&0xFFFFF800)==0xD800 \ |
| 224165 | || (c&0xFFFFFFFE)==0xFFFE ){ c = 0xFFFD; } \ |
| 224166 | } |
| 224167 | |
| 224168 | #define SQLITE_ICU_SKIP_UTF8(zIn) \ |
| 224169 | assert( *zIn ); \ |
| 224170 | if( *(zIn++)>=0xc0 ){ \ |
| @@ -225858,20 +226066,30 @@ | |
| 226066 | 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -1, -1, -1, -1, -1, -1, |
| 226067 | -1, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, |
| 226068 | 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, -1, -1, -1, -1, 36, |
| 226069 | -1, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, |
| 226070 | 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, -1, -1, -1, 63, -1, |
| 226071 | |
| 226072 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, |
| 226073 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, |
| 226074 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, |
| 226075 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, |
| 226076 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, |
| 226077 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, |
| 226078 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, |
| 226079 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, |
| 226080 | }; |
| 226081 | unsigned int v = 0; |
| 226082 | int c; |
| 226083 | unsigned char *z = (unsigned char*)*pz; |
| 226084 | unsigned char *zEnd = z + (*pLen); |
| 226085 | while( z<zEnd && (c = zValue[*z])>=0 ){ |
| 226086 | v = (v<<6) + c; |
| 226087 | z++; |
| 226088 | } |
| 226089 | |
| 226090 | *pLen -= (int)(z - (unsigned char*)*pz); |
| 226091 | *pz = (char*)z; |
| 226092 | return v; |
| 226093 | } |
| 226094 | |
| 226095 | #if RBU_ENABLE_DELTA_CKSUM |
| @@ -225943,23 +226161,24 @@ | |
| 226161 | #if RBU_ENABLE_DELTA_CKSUM |
| 226162 | char *zOrigOut = zOut; |
| 226163 | #endif |
| 226164 | |
| 226165 | limit = rbuDeltaGetInt(&zDelta, &lenDelta); |
| 226166 | if( lenDelta<=0 || *zDelta!='\n' ){ |
| 226167 | /* ERROR: size integer not terminated by "\n" */ |
| 226168 | return -1; |
| 226169 | } |
| 226170 | zDelta++; lenDelta--; |
| 226171 | while( *zDelta && lenDelta>0 ){ |
| 226172 | unsigned int cnt, ofst; |
| 226173 | cnt = rbuDeltaGetInt(&zDelta, &lenDelta); |
| 226174 | if( lenDelta<=0 ) return -1; |
| 226175 | switch( zDelta[0] ){ |
| 226176 | case '@': { |
| 226177 | zDelta++; lenDelta--; |
| 226178 | ofst = rbuDeltaGetInt(&zDelta, &lenDelta); |
| 226179 | if( lenDelta>0 || zDelta[0]!=',' ){ |
| 226180 | /* ERROR: copy command not terminated by ',' */ |
| 226181 | return -1; |
| 226182 | } |
| 226183 | zDelta++; lenDelta--; |
| 226184 | total += cnt; |
| @@ -225980,11 +226199,11 @@ | |
| 226199 | total += cnt; |
| 226200 | if( total>limit ){ |
| 226201 | /* ERROR: insert command gives an output larger than predicted */ |
| 226202 | return -1; |
| 226203 | } |
| 226204 | if( (i64)cnt>(i64)lenDelta ){ |
| 226205 | /* ERROR: insert count exceeds size of delta */ |
| 226206 | return -1; |
| 226207 | } |
| 226208 | memcpy(zOut, zDelta, cnt); |
| 226209 | zOut += cnt; |
| @@ -226018,11 +226237,11 @@ | |
| 226237 | } |
| 226238 | |
| 226239 | static int rbuDeltaOutputSize(const char *zDelta, int lenDelta){ |
| 226240 | int size; |
| 226241 | size = rbuDeltaGetInt(&zDelta, &lenDelta); |
| 226242 | if( lenDelta<=0 || *zDelta!='\n' ){ |
| 226243 | /* ERROR: size integer not terminated by "\n" */ |
| 226244 | return -1; |
| 226245 | } |
| 226246 | return size; |
| 226247 | } |
| @@ -226066,11 +226285,11 @@ | |
| 226285 | if( nOut<0 ){ |
| 226286 | sqlite3_result_error(context, "corrupt fossil delta", -1); |
| 226287 | return; |
| 226288 | } |
| 226289 | |
| 226290 | aOut = sqlite3_malloc64((i64)nOut+1); |
| 226291 | if( aOut==0 ){ |
| 226292 | sqlite3_result_error_nomem(context); |
| 226293 | }else{ |
| 226294 | nOut2 = rbuDeltaApply(aOrig, nOrig, aDelta, nDelta, aOut); |
| 226295 | if( nOut2!=nOut ){ |
| @@ -244051,11 +244270,11 @@ | |
| 244270 | ){ |
| 244271 | HighlightContext ctx; |
| 244272 | int rc = SQLITE_OK; /* Return code */ |
| 244273 | int iCol; /* 1st argument to snippet() */ |
| 244274 | const char *zEllips; /* 4th argument to snippet() */ |
| 244275 | i64 nToken; /* 5th argument to snippet() */ |
| 244276 | int nInst = 0; /* Number of instance matches this row */ |
| 244277 | int i; /* Used to iterate through instances */ |
| 244278 | int nPhrase; /* Number of phrases in query */ |
| 244279 | unsigned char *aSeen; /* Array of "seen instance" flags */ |
| 244280 | int iBestCol; /* Column containing best snippet */ |
| @@ -244076,11 +244295,11 @@ | |
| 244295 | iCol = sqlite3_value_int(apVal[0]); |
| 244296 | ctx.zOpen = fts5ValueToText(apVal[1]); |
| 244297 | ctx.zClose = fts5ValueToText(apVal[2]); |
| 244298 | ctx.iRangeEnd = -1; |
| 244299 | zEllips = fts5ValueToText(apVal[3]); |
| 244300 | nToken = (int)(MIN( MAX(sqlite3_value_int64(apVal[4]), 0), 64)); |
| 244301 | |
| 244302 | iBestCol = (iCol>=0 ? iCol : 0); |
| 244303 | nPhrase = pApi->xPhraseCount(pFts); |
| 244304 | aSeen = sqlite3_malloc64(nPhrase); |
| 244305 | if( aSeen==0 ){ |
| @@ -246792,11 +247011,11 @@ | |
| 247011 | /* Add an entry to each output position list */ |
| 247012 | for(i=0; i<pNear->nPhrase; i++){ |
| 247013 | i64 iPos = a[i].reader.iPos; |
| 247014 | Fts5PoslistWriter *pWriter = &a[i].writer; |
| 247015 | if( a[i].pOut->n==0 || iPos!=pWriter->iPrev ){ |
| 247016 | sqlite3Fts5PoslistSafeAppend(a[i].pOut, &pWriter->iPrev, iPos); |
| 247017 | } |
| 247018 | } |
| 247019 | |
| 247020 | iAdv = 0; |
| 247021 | iMin = a[0].reader.iLookahead; |
| @@ -247743,14 +247962,14 @@ | |
| 247962 | rc = SQLITE_NOMEM; |
| 247963 | }else{ |
| 247964 | memset(pSyn, 0, (size_t)nByte); |
| 247965 | pSyn->pTerm = ((char*)pSyn) + sizeof(Fts5ExprTerm) + sizeof(Fts5Buffer); |
| 247966 | pSyn->nFullTerm = pSyn->nQueryTerm = nToken; |
| 247967 | memcpy(pSyn->pTerm, pToken, nToken); |
| 247968 | if( pCtx->pConfig->bTokendata ){ |
| 247969 | pSyn->nQueryTerm = (int)strlen(pSyn->pTerm); |
| 247970 | } |
| 247971 | pSyn->pSynonym = pPhrase->aTerm[pPhrase->nTerm-1].pSynonym; |
| 247972 | pPhrase->aTerm[pPhrase->nTerm-1].pSynonym = pSyn; |
| 247973 | } |
| 247974 | }else{ |
| 247975 | Fts5ExprTerm *pTerm; |
| @@ -249595,11 +249814,11 @@ | |
| 249814 | ** + 1 byte for a "new column" byte, |
| 249815 | ** + 3 bytes for a new column number (16-bit max) as a varint, |
| 249816 | ** + 5 bytes for the new position offset (32-bit max). |
| 249817 | */ |
| 249818 | if( (p->nAlloc - p->nData) < (9 + 4 + 1 + 3 + 5) ){ |
| 249819 | sqlite3_int64 nNew = (i64)p->nAlloc * 2; |
| 249820 | Fts5HashEntry *pNew; |
| 249821 | Fts5HashEntry **pp; |
| 249822 | pNew = (Fts5HashEntry*)sqlite3_realloc64(p, nNew); |
| 249823 | if( pNew==0 ) return SQLITE_NOMEM; |
| 249824 | pNew->nAlloc = (int)nNew; |
| @@ -251029,11 +251248,11 @@ | |
| 251248 | }else{ |
| 251249 | i += fts5GetVarint32(&pData[i], pLvl->nMerge); |
| 251250 | i += fts5GetVarint32(&pData[i], nTotal); |
| 251251 | if( nTotal<pLvl->nMerge ) rc = FTS5_CORRUPT; |
| 251252 | pLvl->aSeg = (Fts5StructureSegment*)sqlite3Fts5MallocZero(&rc, |
| 251253 | (i64)nTotal * sizeof(Fts5StructureSegment) |
| 251254 | ); |
| 251255 | nSegment -= nTotal; |
| 251256 | } |
| 251257 | |
| 251258 | if( rc==SQLITE_OK ){ |
| @@ -251558,19 +251777,20 @@ | |
| 251777 | assert( pLvl->bEof==0 ); |
| 251778 | if( iOff<=pLvl->iFirstOff ){ |
| 251779 | pLvl->bEof = 1; |
| 251780 | }else{ |
| 251781 | u8 *a = pLvl->pData->p; |
| 251782 | int nn = pLvl->pData->nn; |
| 251783 | |
| 251784 | pLvl->iOff = 0; |
| 251785 | fts5DlidxLvlNext(pLvl); |
| 251786 | while( 1 ){ |
| 251787 | int nZero = 0; |
| 251788 | int ii = pLvl->iOff; |
| 251789 | u64 delta = 0; |
| 251790 | |
| 251791 | while( ii<nn && a[ii]==0 ){ |
| 251792 | nZero++; |
| 251793 | ii++; |
| 251794 | } |
| 251795 | ii += sqlite3Fts5GetVarint(&a[ii], &delta); |
| 251796 | |
| @@ -251985,11 +252205,11 @@ | |
| 252205 | fts5DataRelease(pIter->pLeaf); |
| 252206 | pIter->pLeaf = 0; |
| 252207 | while( p->rc==SQLITE_OK && pIter->iLeafPgno>pIter->iTermLeafPgno ){ |
| 252208 | Fts5Data *pNew; |
| 252209 | pIter->iLeafPgno--; |
| 252210 | pNew = fts5LeafRead(p, FTS5_SEGMENT_ROWID( |
| 252211 | pIter->pSeg->iSegid, pIter->iLeafPgno |
| 252212 | )); |
| 252213 | if( pNew ){ |
| 252214 | /* iTermLeafOffset may be equal to szLeaf if the term is the last |
| 252215 | ** thing on the page - i.e. the first rowid is on the following page. |
| @@ -253419,12 +253639,11 @@ | |
| 253639 | } |
| 253640 | } |
| 253641 | |
| 253642 | do { |
| 253643 | while( i<nChunk && pChunk[i]!=0x01 ){ |
| 253644 | fts5IndexSkipVarint(pChunk, i); |
| 253645 | } |
| 253646 | if( pCtx->eState ){ |
| 253647 | fts5BufferSafeAppendBlob(pCtx->pBuf, &pChunk[iStart], i-iStart); |
| 253648 | } |
| 253649 | if( i<nChunk ){ |
| @@ -255163,10 +255382,15 @@ | |
| 255382 | int iSOP; /* Start-Of-Position-list */ |
| 255383 | if( pSeg->iLeafPgno==pSeg->iTermLeafPgno ){ |
| 255384 | iStart = pSeg->iTermLeafOffset; |
| 255385 | }else{ |
| 255386 | iStart = fts5GetU16(&aPg[0]); |
| 255387 | } |
| 255388 | if( iStart>nPg ){ |
| 255389 | FTS5_CORRUPT_IDX(p); |
| 255390 | sqlite3_free(aIdx); |
| 255391 | return; |
| 255392 | } |
| 255393 | |
| 255394 | iSOP = iStart + fts5GetVarint(&aPg[iStart], &iDelta); |
| 255395 | assert_nc( iSOP<=pSeg->iLeafOffset ); |
| 255396 | |
| @@ -257849,12 +258073,12 @@ | |
| 258073 | int *pnOut, /* OUT: Number of output pages */ |
| 258074 | Fts5Data ***papOut /* OUT: Output hash pages */ |
| 258075 | ){ |
| 258076 | const int MINSLOT = 32; |
| 258077 | int nSlotPerPage = MAX(MINSLOT, (p->pConfig->pgsz - 8) / szKey); |
| 258078 | i64 nSlot = 0; /* Number of slots in each output page */ |
| 258079 | i64 nOut = 0; |
| 258080 | |
| 258081 | /* Figure out how many output pages (nOut) and how many slots per |
| 258082 | ** page (nSlot). There are three possibilities: |
| 258083 | ** |
| 258084 | ** 1. The hash table does not yet exist. In this case the new hash |
| @@ -257875,27 +258099,30 @@ | |
| 258099 | /* Case 1. */ |
| 258100 | nOut = 1; |
| 258101 | nSlot = MINSLOT; |
| 258102 | }else if( pSeg->nPgTombstone==1 ){ |
| 258103 | /* Case 2. */ |
| 258104 | u32 nElem = fts5GetU32(&pData1->p[4]); |
| 258105 | assert( pData1 && iPg1==0 ); |
| 258106 | if( nElem>((u32)nSlotPerPage/4) ){ |
| 258107 | nOut = 0; |
| 258108 | }else{ |
| 258109 | nOut = 1; |
| 258110 | nSlot = MAX((i64)nElem*4, MINSLOT); |
| 258111 | } |
| 258112 | } |
| 258113 | if( nOut==0 ){ |
| 258114 | /* Case 3. */ |
| 258115 | nOut = ((i64)pSeg->nPgTombstone * 2 + 1); |
| 258116 | nSlot = nSlotPerPage; |
| 258117 | } |
| 258118 | |
| 258119 | /* Allocate the required array and output pages */ |
| 258120 | while( 1 ){ |
| 258121 | int res = 0; |
| 258122 | i64 ii = 0; |
| 258123 | i64 szPage = 0; |
| 258124 | Fts5Data **apOut = 0; |
| 258125 | |
| 258126 | /* Allocate space for the new hash table */ |
| 258127 | assert( nSlot>=MINSLOT ); |
| 258128 | apOut = (Fts5Data**)sqlite3Fts5MallocZero(&p->rc, sizeof(Fts5Data*) * nOut); |
| @@ -258429,11 +258656,11 @@ | |
| 258656 | ){ |
| 258657 | |
| 258658 | /* Check any rowid-less pages that occur before the current leaf. */ |
| 258659 | for(iPg=iPrevLeaf+1; iPg<fts5DlidxIterPgno(pDlidx); iPg++){ |
| 258660 | iKey = FTS5_SEGMENT_ROWID(iSegid, iPg); |
| 258661 | pLeaf = fts5LeafRead(p, iKey); |
| 258662 | if( pLeaf ){ |
| 258663 | if( fts5LeafFirstRowidOff(pLeaf)!=0 ) FTS5_CORRUPT_ROWID(p, iKey); |
| 258664 | fts5DataRelease(pLeaf); |
| 258665 | } |
| 258666 | } |
| @@ -258440,11 +258667,11 @@ | |
| 258667 | iPrevLeaf = fts5DlidxIterPgno(pDlidx); |
| 258668 | |
| 258669 | /* Check that the leaf page indicated by the iterator really does |
| 258670 | ** contain the rowid suggested by the same. */ |
| 258671 | iKey = FTS5_SEGMENT_ROWID(iSegid, iPrevLeaf); |
| 258672 | pLeaf = fts5LeafRead(p, iKey); |
| 258673 | if( pLeaf ){ |
| 258674 | i64 iRowid; |
| 258675 | int iRowidOff = fts5LeafFirstRowidOff(pLeaf); |
| 258676 | ASSERT_SZLEAF_OK(pLeaf); |
| 258677 | if( iRowidOff>=pLeaf->szLeaf ){ |
| @@ -263040,11 +263267,11 @@ | |
| 263267 | int nArg, /* Number of args */ |
| 263268 | sqlite3_value **apUnused /* Function arguments */ |
| 263269 | ){ |
| 263270 | assert( nArg==0 ); |
| 263271 | UNUSED_PARAM2(nArg, apUnused); |
| 263272 | sqlite3_result_text(pCtx, "fts5: 2026-06-16 13:43:08 3f3fb9b638f59ad982beafb7c117f24ddd3da612e62c862510805fa672ffae06", -1, SQLITE_TRANSIENT); |
| 263273 | } |
| 263274 | |
| 263275 | /* |
| 263276 | ** Implementation of fts5_locale(LOCALE, TEXT) function. |
| 263277 | ** |
| 263278 |
+19
-6
| --- extsrc/sqlite3.h | ||
| +++ extsrc/sqlite3.h | ||
| @@ -146,14 +146,14 @@ | ||
| 146 | 146 | ** [sqlite3_libversion_number()], [sqlite3_sourceid()], |
| 147 | 147 | ** [sqlite_version()] and [sqlite_source_id()]. |
| 148 | 148 | */ |
| 149 | 149 | #define SQLITE_VERSION "3.54.0" |
| 150 | 150 | #define SQLITE_VERSION_NUMBER 3054000 |
| 151 | -#define SQLITE_SOURCE_ID "2026-05-30 13:23:25 2c605bfb1562d7a3609ad6ffd7446def12f1ac7084e41b9c6723e998c156501d" | |
| 151 | +#define SQLITE_SOURCE_ID "2026-06-16 13:43:08 3f3fb9b638f59ad982beafb7c117f24ddd3da612e62c862510805fa672ffae06" | |
| 152 | 152 | #define SQLITE_SCM_BRANCH "trunk" |
| 153 | 153 | #define SQLITE_SCM_TAGS "" |
| 154 | -#define SQLITE_SCM_DATETIME "2026-05-30T13:23:25.636Z" | |
| 154 | +#define SQLITE_SCM_DATETIME "2026-06-16T13:43:08.110Z" | |
| 155 | 155 | |
| 156 | 156 | /* |
| 157 | 157 | ** CAPI3REF: Run-Time Library Version Numbers |
| 158 | 158 | ** KEYWORDS: sqlite3_version sqlite3_sourceid |
| 159 | 159 | ** |
| @@ -4375,11 +4375,12 @@ | ||
| 4375 | 4375 | ** <dd>The maximum number of columns in a table definition or in the |
| 4376 | 4376 | ** result set of a [SELECT] or the maximum number of columns in an index |
| 4377 | 4377 | ** or in an ORDER BY or GROUP BY clause.</dd>)^ |
| 4378 | 4378 | ** |
| 4379 | 4379 | ** [[SQLITE_LIMIT_EXPR_DEPTH]] ^(<dt>SQLITE_LIMIT_EXPR_DEPTH</dt> |
| 4380 | -** <dd>The maximum depth of the parse tree on any expression.</dd>)^ | |
| 4380 | +** <dd>The maximum depth of the parse tree on any expression and | |
| 4381 | +** the maximum nesting depth for subqueries and VIEWs</dd>)^ | |
| 4381 | 4382 | ** |
| 4382 | 4383 | ** [[SQLITE_LIMIT_PARSER_DEPTH]] ^(<dt>SQLITE_LIMIT_PARSER_DEPTH</dt> |
| 4383 | 4384 | ** <dd>The maximum depth of the LALR(1) parser stack used to analyze |
| 4384 | 4385 | ** input SQL statements.</dd>)^ |
| 4385 | 4386 | ** |
| @@ -4406,11 +4407,12 @@ | ||
| 4406 | 4407 | ** [[SQLITE_LIMIT_VARIABLE_NUMBER]] |
| 4407 | 4408 | ** ^(<dt>SQLITE_LIMIT_VARIABLE_NUMBER</dt> |
| 4408 | 4409 | ** <dd>The maximum index number of any [parameter] in an SQL statement.)^ |
| 4409 | 4410 | ** |
| 4410 | 4411 | ** [[SQLITE_LIMIT_TRIGGER_DEPTH]] ^(<dt>SQLITE_LIMIT_TRIGGER_DEPTH</dt> |
| 4411 | -** <dd>The maximum depth of recursion for triggers.</dd>)^ | |
| 4412 | +** <dd>The maximum depth of recursion for triggers, and the maximum | |
| 4413 | +** nesting depth for separate triggers.</dd>)^ | |
| 4412 | 4414 | ** |
| 4413 | 4415 | ** [[SQLITE_LIMIT_WORKER_THREADS]] ^(<dt>SQLITE_LIMIT_WORKER_THREADS</dt> |
| 4414 | 4416 | ** <dd>The maximum number of auxiliary worker threads that a single |
| 4415 | 4417 | ** [prepared statement] may start.</dd>)^ |
| 4416 | 4418 | ** </dl> |
| @@ -11212,12 +11214,23 @@ | ||
| 11212 | 11214 | ** reopen S as an in-memory database based on the serialization |
| 11213 | 11215 | ** contained in P. If S is a NULL pointer, the main database is |
| 11214 | 11216 | ** used. The serialized database P is N bytes in size. M is the size |
| 11215 | 11217 | ** of the buffer P, which might be larger than N. If M is larger than |
| 11216 | 11218 | ** N, and the SQLITE_DESERIALIZE_READONLY bit is not set in F, then |
| 11217 | -** SQLite is permitted to add content to the in-memory database as | |
| 11218 | -** long as the total size does not exceed M bytes. | |
| 11219 | +** SQLite is permitted to add content to the in-memory database, in | |
| 11220 | +** page-sized chunks, as long as the total size does not exceed M bytes. | |
| 11221 | +** | |
| 11222 | +** The parameter M must be greater than or equal to N. Ideally, M | |
| 11223 | +** should have a value which is N+(512×K)+20 where K determines how | |
| 11224 | +** must extra space is available to hold new content as the database | |
| 11225 | +** grows. K can be 0 if the database is read-only. | |
| 11226 | +** | |
| 11227 | +** If the database content in P is malformed in a malicious way then | |
| 11228 | +** it is possible that SQLite might try to read a few more than N bytes | |
| 11229 | +** from P. If the veracity of the database content P is uncertain, | |
| 11230 | +** then applications are advised to allocate about 20 extra bytes on | |
| 11231 | +** the end of the P buffer to avoid a memory error. | |
| 11219 | 11232 | ** |
| 11220 | 11233 | ** If the SQLITE_DESERIALIZE_FREEONCLOSE bit is set in F, then SQLite will |
| 11221 | 11234 | ** invoke sqlite3_free() on the serialization buffer when the database |
| 11222 | 11235 | ** connection closes. If the SQLITE_DESERIALIZE_RESIZEABLE bit is set, then |
| 11223 | 11236 | ** SQLite will try to increase the buffer size using sqlite3_realloc64() |
| 11224 | 11237 |
| --- extsrc/sqlite3.h | |
| +++ extsrc/sqlite3.h | |
| @@ -146,14 +146,14 @@ | |
| 146 | ** [sqlite3_libversion_number()], [sqlite3_sourceid()], |
| 147 | ** [sqlite_version()] and [sqlite_source_id()]. |
| 148 | */ |
| 149 | #define SQLITE_VERSION "3.54.0" |
| 150 | #define SQLITE_VERSION_NUMBER 3054000 |
| 151 | #define SQLITE_SOURCE_ID "2026-05-30 13:23:25 2c605bfb1562d7a3609ad6ffd7446def12f1ac7084e41b9c6723e998c156501d" |
| 152 | #define SQLITE_SCM_BRANCH "trunk" |
| 153 | #define SQLITE_SCM_TAGS "" |
| 154 | #define SQLITE_SCM_DATETIME "2026-05-30T13:23:25.636Z" |
| 155 | |
| 156 | /* |
| 157 | ** CAPI3REF: Run-Time Library Version Numbers |
| 158 | ** KEYWORDS: sqlite3_version sqlite3_sourceid |
| 159 | ** |
| @@ -4375,11 +4375,12 @@ | |
| 4375 | ** <dd>The maximum number of columns in a table definition or in the |
| 4376 | ** result set of a [SELECT] or the maximum number of columns in an index |
| 4377 | ** or in an ORDER BY or GROUP BY clause.</dd>)^ |
| 4378 | ** |
| 4379 | ** [[SQLITE_LIMIT_EXPR_DEPTH]] ^(<dt>SQLITE_LIMIT_EXPR_DEPTH</dt> |
| 4380 | ** <dd>The maximum depth of the parse tree on any expression.</dd>)^ |
| 4381 | ** |
| 4382 | ** [[SQLITE_LIMIT_PARSER_DEPTH]] ^(<dt>SQLITE_LIMIT_PARSER_DEPTH</dt> |
| 4383 | ** <dd>The maximum depth of the LALR(1) parser stack used to analyze |
| 4384 | ** input SQL statements.</dd>)^ |
| 4385 | ** |
| @@ -4406,11 +4407,12 @@ | |
| 4406 | ** [[SQLITE_LIMIT_VARIABLE_NUMBER]] |
| 4407 | ** ^(<dt>SQLITE_LIMIT_VARIABLE_NUMBER</dt> |
| 4408 | ** <dd>The maximum index number of any [parameter] in an SQL statement.)^ |
| 4409 | ** |
| 4410 | ** [[SQLITE_LIMIT_TRIGGER_DEPTH]] ^(<dt>SQLITE_LIMIT_TRIGGER_DEPTH</dt> |
| 4411 | ** <dd>The maximum depth of recursion for triggers.</dd>)^ |
| 4412 | ** |
| 4413 | ** [[SQLITE_LIMIT_WORKER_THREADS]] ^(<dt>SQLITE_LIMIT_WORKER_THREADS</dt> |
| 4414 | ** <dd>The maximum number of auxiliary worker threads that a single |
| 4415 | ** [prepared statement] may start.</dd>)^ |
| 4416 | ** </dl> |
| @@ -11212,12 +11214,23 @@ | |
| 11212 | ** reopen S as an in-memory database based on the serialization |
| 11213 | ** contained in P. If S is a NULL pointer, the main database is |
| 11214 | ** used. The serialized database P is N bytes in size. M is the size |
| 11215 | ** of the buffer P, which might be larger than N. If M is larger than |
| 11216 | ** N, and the SQLITE_DESERIALIZE_READONLY bit is not set in F, then |
| 11217 | ** SQLite is permitted to add content to the in-memory database as |
| 11218 | ** long as the total size does not exceed M bytes. |
| 11219 | ** |
| 11220 | ** If the SQLITE_DESERIALIZE_FREEONCLOSE bit is set in F, then SQLite will |
| 11221 | ** invoke sqlite3_free() on the serialization buffer when the database |
| 11222 | ** connection closes. If the SQLITE_DESERIALIZE_RESIZEABLE bit is set, then |
| 11223 | ** SQLite will try to increase the buffer size using sqlite3_realloc64() |
| 11224 |
| --- extsrc/sqlite3.h | |
| +++ extsrc/sqlite3.h | |
| @@ -146,14 +146,14 @@ | |
| 146 | ** [sqlite3_libversion_number()], [sqlite3_sourceid()], |
| 147 | ** [sqlite_version()] and [sqlite_source_id()]. |
| 148 | */ |
| 149 | #define SQLITE_VERSION "3.54.0" |
| 150 | #define SQLITE_VERSION_NUMBER 3054000 |
| 151 | #define SQLITE_SOURCE_ID "2026-06-16 13:43:08 3f3fb9b638f59ad982beafb7c117f24ddd3da612e62c862510805fa672ffae06" |
| 152 | #define SQLITE_SCM_BRANCH "trunk" |
| 153 | #define SQLITE_SCM_TAGS "" |
| 154 | #define SQLITE_SCM_DATETIME "2026-06-16T13:43:08.110Z" |
| 155 | |
| 156 | /* |
| 157 | ** CAPI3REF: Run-Time Library Version Numbers |
| 158 | ** KEYWORDS: sqlite3_version sqlite3_sourceid |
| 159 | ** |
| @@ -4375,11 +4375,12 @@ | |
| 4375 | ** <dd>The maximum number of columns in a table definition or in the |
| 4376 | ** result set of a [SELECT] or the maximum number of columns in an index |
| 4377 | ** or in an ORDER BY or GROUP BY clause.</dd>)^ |
| 4378 | ** |
| 4379 | ** [[SQLITE_LIMIT_EXPR_DEPTH]] ^(<dt>SQLITE_LIMIT_EXPR_DEPTH</dt> |
| 4380 | ** <dd>The maximum depth of the parse tree on any expression and |
| 4381 | ** the maximum nesting depth for subqueries and VIEWs</dd>)^ |
| 4382 | ** |
| 4383 | ** [[SQLITE_LIMIT_PARSER_DEPTH]] ^(<dt>SQLITE_LIMIT_PARSER_DEPTH</dt> |
| 4384 | ** <dd>The maximum depth of the LALR(1) parser stack used to analyze |
| 4385 | ** input SQL statements.</dd>)^ |
| 4386 | ** |
| @@ -4406,11 +4407,12 @@ | |
| 4407 | ** [[SQLITE_LIMIT_VARIABLE_NUMBER]] |
| 4408 | ** ^(<dt>SQLITE_LIMIT_VARIABLE_NUMBER</dt> |
| 4409 | ** <dd>The maximum index number of any [parameter] in an SQL statement.)^ |
| 4410 | ** |
| 4411 | ** [[SQLITE_LIMIT_TRIGGER_DEPTH]] ^(<dt>SQLITE_LIMIT_TRIGGER_DEPTH</dt> |
| 4412 | ** <dd>The maximum depth of recursion for triggers, and the maximum |
| 4413 | ** nesting depth for separate triggers.</dd>)^ |
| 4414 | ** |
| 4415 | ** [[SQLITE_LIMIT_WORKER_THREADS]] ^(<dt>SQLITE_LIMIT_WORKER_THREADS</dt> |
| 4416 | ** <dd>The maximum number of auxiliary worker threads that a single |
| 4417 | ** [prepared statement] may start.</dd>)^ |
| 4418 | ** </dl> |
| @@ -11212,12 +11214,23 @@ | |
| 11214 | ** reopen S as an in-memory database based on the serialization |
| 11215 | ** contained in P. If S is a NULL pointer, the main database is |
| 11216 | ** used. The serialized database P is N bytes in size. M is the size |
| 11217 | ** of the buffer P, which might be larger than N. If M is larger than |
| 11218 | ** N, and the SQLITE_DESERIALIZE_READONLY bit is not set in F, then |
| 11219 | ** SQLite is permitted to add content to the in-memory database, in |
| 11220 | ** page-sized chunks, as long as the total size does not exceed M bytes. |
| 11221 | ** |
| 11222 | ** The parameter M must be greater than or equal to N. Ideally, M |
| 11223 | ** should have a value which is N+(512×K)+20 where K determines how |
| 11224 | ** must extra space is available to hold new content as the database |
| 11225 | ** grows. K can be 0 if the database is read-only. |
| 11226 | ** |
| 11227 | ** If the database content in P is malformed in a malicious way then |
| 11228 | ** it is possible that SQLite might try to read a few more than N bytes |
| 11229 | ** from P. If the veracity of the database content P is uncertain, |
| 11230 | ** then applications are advised to allocate about 20 extra bytes on |
| 11231 | ** the end of the P buffer to avoid a memory error. |
| 11232 | ** |
| 11233 | ** If the SQLITE_DESERIALIZE_FREEONCLOSE bit is set in F, then SQLite will |
| 11234 | ** invoke sqlite3_free() on the serialization buffer when the database |
| 11235 | ** connection closes. If the SQLITE_DESERIALIZE_RESIZEABLE bit is set, then |
| 11236 | ** SQLite will try to increase the buffer size using sqlite3_realloc64() |
| 11237 |
+17
-41
| --- skins/ardoise/css.txt | ||
| +++ skins/ardoise/css.txt | ||
| @@ -154,24 +154,19 @@ | ||
| 154 | 154 | button, |
| 155 | 155 | select { |
| 156 | 156 | text-transform: none |
| 157 | 157 | } |
| 158 | 158 | button, |
| 159 | -html input[type=button], | |
| 159 | +input[type=button], | |
| 160 | 160 | input[type=reset], |
| 161 | -input[type=submit] { | |
| 161 | +input[type=submit]{ | |
| 162 | 162 | cursor: pointer |
| 163 | 163 | } |
| 164 | -button[disabled], | |
| 165 | -html input[disabled] { | |
| 164 | +button:disabled, | |
| 165 | +input:disabled { | |
| 166 | 166 | cursor: default |
| 167 | 167 | } |
| 168 | -button::-moz-focus-inner, | |
| 169 | -input::-moz-focus-inner { | |
| 170 | - border: 0; | |
| 171 | - padding: 0 | |
| 172 | -} | |
| 173 | 168 | input { |
| 174 | 169 | line-height: normal |
| 175 | 170 | } |
| 176 | 171 | input[type=checkbox], |
| 177 | 172 | input[type=radio] { |
| @@ -307,11 +302,11 @@ | ||
| 307 | 302 | text-decoration: none; |
| 308 | 303 | text-align: center; |
| 309 | 304 | white-space: nowrap; |
| 310 | 305 | cursor: pointer |
| 311 | 306 | } |
| 312 | -input[type=submit]:disabled { | |
| 307 | +input:disabled { | |
| 313 | 308 | color: rgb(70,70,70); |
| 314 | 309 | background-color: rgb(153,153,153); |
| 315 | 310 | } |
| 316 | 311 | |
| 317 | 312 | @media (min-width:550px) { |
| @@ -416,30 +411,20 @@ | ||
| 416 | 411 | .offset-by-eleven.columns { |
| 417 | 412 | margin-left: 95.33333% |
| 418 | 413 | } |
| 419 | 414 | } |
| 420 | 415 | .button, |
| 421 | -button { | |
| 422 | - color: #aaa; | |
| 423 | - background-color: #444; | |
| 424 | - border-radius: 5px; | |
| 425 | - border: 0 | |
| 426 | -} | |
| 416 | +button, | |
| 427 | 417 | input[type=button], |
| 428 | 418 | input[type=reset], |
| 429 | 419 | input[type=submit] { |
| 430 | 420 | color: #ddd; |
| 431 | 421 | background-color: #446979; |
| 432 | 422 | border: 0; |
| 433 | 423 | border-radius: 5px |
| 434 | 424 | } |
| 435 | -.button:hover, | |
| 436 | -button:hover { | |
| 437 | - color: #444; | |
| 438 | - background-color: #aaa; | |
| 439 | - outline: 0 | |
| 440 | -} | |
| 425 | +button:hover, | |
| 441 | 426 | input[type=button]:hover, |
| 442 | 427 | input[type=reset]:hover, |
| 443 | 428 | input[type=submit]:hover { |
| 444 | 429 | color: #446979; |
| 445 | 430 | background-color: #ddd; |
| @@ -452,29 +437,20 @@ | ||
| 452 | 437 | input[type=submit]:focus { |
| 453 | 438 | color: #333; |
| 454 | 439 | border-color: #888; |
| 455 | 440 | outline: 0 |
| 456 | 441 | } |
| 457 | -.button.button-primary, | |
| 458 | -.button.button-primary:focus, | |
| 459 | -.button.button-primary:hover, | |
| 460 | -button.button-primary, | |
| 461 | -button.button-primary:focus, | |
| 462 | -button.button-primary:hover, | |
| 463 | -input[type=button].button-primary, | |
| 464 | -input[type=button].button-primary:focus, | |
| 465 | -input[type=button].button-primary:hover, | |
| 466 | -input[type=reset].button-primary, | |
| 467 | -input[type=reset].button-primary:focus, | |
| 468 | -input[type=reset].button-primary:hover, | |
| 469 | -input[type=submit].button-primary, | |
| 470 | -input[type=submit].button-primary:focus, | |
| 471 | -input[type=submit].button-primary:hover { | |
| 472 | - color: #303536; | |
| 473 | - background-color: #ff8000; | |
| 474 | - border-color: #ff8000 | |
| 475 | -} | |
| 442 | + | |
| 443 | +button:disabled, | |
| 444 | +input[type=button]:disabled, | |
| 445 | +input[type=reset]:disabled, | |
| 446 | +input[type=submit]:disabled{ | |
| 447 | + color: #ddd; | |
| 448 | + background-color: #7f7f7f; | |
| 449 | + opacity: 0.8; | |
| 450 | +} | |
| 451 | + | |
| 476 | 452 | input[type=email], |
| 477 | 453 | input[type=number], |
| 478 | 454 | input[type=password], |
| 479 | 455 | input[type=search], |
| 480 | 456 | input[type=tel], |
| 481 | 457 |
| --- skins/ardoise/css.txt | |
| +++ skins/ardoise/css.txt | |
| @@ -154,24 +154,19 @@ | |
| 154 | button, |
| 155 | select { |
| 156 | text-transform: none |
| 157 | } |
| 158 | button, |
| 159 | html input[type=button], |
| 160 | input[type=reset], |
| 161 | input[type=submit] { |
| 162 | cursor: pointer |
| 163 | } |
| 164 | button[disabled], |
| 165 | html input[disabled] { |
| 166 | cursor: default |
| 167 | } |
| 168 | button::-moz-focus-inner, |
| 169 | input::-moz-focus-inner { |
| 170 | border: 0; |
| 171 | padding: 0 |
| 172 | } |
| 173 | input { |
| 174 | line-height: normal |
| 175 | } |
| 176 | input[type=checkbox], |
| 177 | input[type=radio] { |
| @@ -307,11 +302,11 @@ | |
| 307 | text-decoration: none; |
| 308 | text-align: center; |
| 309 | white-space: nowrap; |
| 310 | cursor: pointer |
| 311 | } |
| 312 | input[type=submit]:disabled { |
| 313 | color: rgb(70,70,70); |
| 314 | background-color: rgb(153,153,153); |
| 315 | } |
| 316 | |
| 317 | @media (min-width:550px) { |
| @@ -416,30 +411,20 @@ | |
| 416 | .offset-by-eleven.columns { |
| 417 | margin-left: 95.33333% |
| 418 | } |
| 419 | } |
| 420 | .button, |
| 421 | button { |
| 422 | color: #aaa; |
| 423 | background-color: #444; |
| 424 | border-radius: 5px; |
| 425 | border: 0 |
| 426 | } |
| 427 | input[type=button], |
| 428 | input[type=reset], |
| 429 | input[type=submit] { |
| 430 | color: #ddd; |
| 431 | background-color: #446979; |
| 432 | border: 0; |
| 433 | border-radius: 5px |
| 434 | } |
| 435 | .button:hover, |
| 436 | button:hover { |
| 437 | color: #444; |
| 438 | background-color: #aaa; |
| 439 | outline: 0 |
| 440 | } |
| 441 | input[type=button]:hover, |
| 442 | input[type=reset]:hover, |
| 443 | input[type=submit]:hover { |
| 444 | color: #446979; |
| 445 | background-color: #ddd; |
| @@ -452,29 +437,20 @@ | |
| 452 | input[type=submit]:focus { |
| 453 | color: #333; |
| 454 | border-color: #888; |
| 455 | outline: 0 |
| 456 | } |
| 457 | .button.button-primary, |
| 458 | .button.button-primary:focus, |
| 459 | .button.button-primary:hover, |
| 460 | button.button-primary, |
| 461 | button.button-primary:focus, |
| 462 | button.button-primary:hover, |
| 463 | input[type=button].button-primary, |
| 464 | input[type=button].button-primary:focus, |
| 465 | input[type=button].button-primary:hover, |
| 466 | input[type=reset].button-primary, |
| 467 | input[type=reset].button-primary:focus, |
| 468 | input[type=reset].button-primary:hover, |
| 469 | input[type=submit].button-primary, |
| 470 | input[type=submit].button-primary:focus, |
| 471 | input[type=submit].button-primary:hover { |
| 472 | color: #303536; |
| 473 | background-color: #ff8000; |
| 474 | border-color: #ff8000 |
| 475 | } |
| 476 | input[type=email], |
| 477 | input[type=number], |
| 478 | input[type=password], |
| 479 | input[type=search], |
| 480 | input[type=tel], |
| 481 |
| --- skins/ardoise/css.txt | |
| +++ skins/ardoise/css.txt | |
| @@ -154,24 +154,19 @@ | |
| 154 | button, |
| 155 | select { |
| 156 | text-transform: none |
| 157 | } |
| 158 | button, |
| 159 | input[type=button], |
| 160 | input[type=reset], |
| 161 | input[type=submit]{ |
| 162 | cursor: pointer |
| 163 | } |
| 164 | button:disabled, |
| 165 | input:disabled { |
| 166 | cursor: default |
| 167 | } |
| 168 | input { |
| 169 | line-height: normal |
| 170 | } |
| 171 | input[type=checkbox], |
| 172 | input[type=radio] { |
| @@ -307,11 +302,11 @@ | |
| 302 | text-decoration: none; |
| 303 | text-align: center; |
| 304 | white-space: nowrap; |
| 305 | cursor: pointer |
| 306 | } |
| 307 | input:disabled { |
| 308 | color: rgb(70,70,70); |
| 309 | background-color: rgb(153,153,153); |
| 310 | } |
| 311 | |
| 312 | @media (min-width:550px) { |
| @@ -416,30 +411,20 @@ | |
| 411 | .offset-by-eleven.columns { |
| 412 | margin-left: 95.33333% |
| 413 | } |
| 414 | } |
| 415 | .button, |
| 416 | button, |
| 417 | input[type=button], |
| 418 | input[type=reset], |
| 419 | input[type=submit] { |
| 420 | color: #ddd; |
| 421 | background-color: #446979; |
| 422 | border: 0; |
| 423 | border-radius: 5px |
| 424 | } |
| 425 | button:hover, |
| 426 | input[type=button]:hover, |
| 427 | input[type=reset]:hover, |
| 428 | input[type=submit]:hover { |
| 429 | color: #446979; |
| 430 | background-color: #ddd; |
| @@ -452,29 +437,20 @@ | |
| 437 | input[type=submit]:focus { |
| 438 | color: #333; |
| 439 | border-color: #888; |
| 440 | outline: 0 |
| 441 | } |
| 442 | |
| 443 | button:disabled, |
| 444 | input[type=button]:disabled, |
| 445 | input[type=reset]:disabled, |
| 446 | input[type=submit]:disabled{ |
| 447 | color: #ddd; |
| 448 | background-color: #7f7f7f; |
| 449 | opacity: 0.8; |
| 450 | } |
| 451 | |
| 452 | input[type=email], |
| 453 | input[type=number], |
| 454 | input[type=password], |
| 455 | input[type=search], |
| 456 | input[type=tel], |
| 457 |
+43
-85
| --- skins/blitz/css.txt | ||
| +++ skins/blitz/css.txt | ||
| @@ -1,17 +1,9 @@ | ||
| 1 | 1 | /*! normalize.css v3.0.2 | MIT License | git.io/normalize */ |
| 2 | 2 | |
| 3 | -/** | |
| 4 | - * 1. Set default font family to sans-serif. | |
| 5 | - * 2. Prevent iOS text size adjust after orientation change, without disabling | |
| 6 | - * user zoom. | |
| 7 | - */ | |
| 8 | - | |
| 9 | 3 | html { |
| 10 | 4 | font-family: sans-serif; /* 1 */ |
| 11 | - -ms-text-size-adjust: 100%; /* 2 */ | |
| 12 | - -webkit-text-size-adjust: 100%; /* 2 */ | |
| 13 | 5 | } |
| 14 | 6 | |
| 15 | 7 | /** |
| 16 | 8 | * Remove default margin. |
| 17 | 9 | */ |
| @@ -207,11 +199,10 @@ | ||
| 207 | 199 | /** |
| 208 | 200 | * Address differences between Firefox and other browsers. |
| 209 | 201 | */ |
| 210 | 202 | |
| 211 | 203 | hr { |
| 212 | - -moz-box-sizing: content-box; | |
| 213 | 204 | box-sizing: content-box; |
| 214 | 205 | height: 0; |
| 215 | 206 | } |
| 216 | 207 | |
| 217 | 208 | /** |
| @@ -277,47 +268,27 @@ | ||
| 277 | 268 | button, |
| 278 | 269 | select { |
| 279 | 270 | text-transform: none; |
| 280 | 271 | } |
| 281 | 272 | |
| 282 | -/** | |
| 283 | - * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` | |
| 284 | - * and `video` controls. | |
| 285 | - * 2. Correct inability to style clickable `input` types in iOS. | |
| 286 | - * 3. Improve usability and consistency of cursor style between image-type | |
| 287 | - * `input` and others. | |
| 288 | - */ | |
| 289 | - | |
| 290 | 273 | button, |
| 291 | -html input[type="button"], /* 1 */ | |
| 292 | -input[type="reset"], | |
| 293 | -input[type="submit"], | |
| 294 | -input[type="button"].submit, | |
| 295 | -button.submit{ | |
| 296 | - -webkit-appearance: button; /* 2 */ | |
| 297 | - cursor: pointer; /* 3 */ | |
| 274 | +input[type=button], | |
| 275 | +input[type=reset], | |
| 276 | +input[type=submit]{ | |
| 277 | + cursor: pointer; | |
| 298 | 278 | } |
| 299 | 279 | |
| 300 | 280 | /** |
| 301 | 281 | * Re-set default cursor for disabled elements. |
| 302 | 282 | */ |
| 303 | - | |
| 304 | -button[disabled], | |
| 305 | -html input[disabled] { | |
| 283 | +button:disabled, | |
| 284 | +input[type=button]:disabled, | |
| 285 | +input[type=reset]:disabled, | |
| 286 | +input[type=submit]:disabled{ | |
| 306 | 287 | cursor: default; |
| 307 | 288 | } |
| 308 | 289 | |
| 309 | -/** | |
| 310 | - * Remove inner padding and border in Firefox 4+. | |
| 311 | - */ | |
| 312 | - | |
| 313 | -button::-moz-focus-inner, | |
| 314 | -input::-moz-focus-inner { | |
| 315 | - border: 0; | |
| 316 | - padding: 0; | |
| 317 | -} | |
| 318 | - | |
| 319 | 290 | /** |
| 320 | 291 | * Address Firefox 4+ setting `line-height` on `input` using `!important` in |
| 321 | 292 | * the UA stylesheet. |
| 322 | 293 | */ |
| 323 | 294 | |
| @@ -348,20 +319,11 @@ | ||
| 348 | 319 | input[type="number"]::-webkit-inner-spin-button, |
| 349 | 320 | input[type="number"]::-webkit-outer-spin-button { |
| 350 | 321 | height: auto; |
| 351 | 322 | } |
| 352 | 323 | |
| 353 | -/** | |
| 354 | - * 1. Address `appearance` set to `searchfield` in Safari and Chrome. | |
| 355 | - * 2. Address `box-sizing` set to `border-box` in Safari and Chrome | |
| 356 | - * (include `-moz` to future-proof). | |
| 357 | - */ | |
| 358 | - | |
| 359 | 324 | input[type="search"] { |
| 360 | - -webkit-appearance: textfield; /* 1 */ | |
| 361 | - -moz-box-sizing: content-box; | |
| 362 | - -webkit-box-sizing: content-box; /* 2 */ | |
| 363 | 325 | box-sizing: content-box; |
| 364 | 326 | } |
| 365 | 327 | |
| 366 | 328 | /** |
| 367 | 329 | * Remove inner padding and search cancel button in Safari and Chrome on OS X. |
| @@ -517,73 +479,65 @@ | ||
| 517 | 479 | |
| 518 | 480 | /* Buttons |
| 519 | 481 | ––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––– */ |
| 520 | 482 | .button, |
| 521 | 483 | button, |
| 522 | -input[type="button"], | |
| 523 | -input[type="reset"], | |
| 524 | -input[type="submit"], | |
| 525 | -input[type="button"].submit, | |
| 526 | -button.submit{ | |
| 484 | +input[type=button], | |
| 485 | +input[type=reset], | |
| 486 | +input[type=submit]{ | |
| 527 | 487 | display: inline-block; |
| 528 | 488 | height: 3.3rem; |
| 529 | 489 | padding: 0 2.2rem; |
| 530 | - color: #555 !important; | |
| 490 | + color: #444; | |
| 491 | + background-color: #f8f8f8;; | |
| 492 | + border-color: #446979; | |
| 531 | 493 | text-align: center; |
| 532 | 494 | font-size: 1.1rem; |
| 533 | 495 | font-weight: 700; |
| 534 | 496 | line-height: 3.3rem; |
| 535 | 497 | letter-spacing: .08rem; |
| 536 | 498 | text-transform: uppercase; |
| 537 | 499 | text-decoration: none; |
| 538 | 500 | white-space: nowrap; |
| 539 | - background-color: transparent; | |
| 540 | 501 | border-radius: 4px; |
| 541 | - border: 1px solid #ccc; | |
| 502 | + /*border: 1px solid #ccc;*/ | |
| 503 | + border: 1px solid #446979; | |
| 542 | 504 | cursor: pointer; |
| 543 | 505 | box-sizing: border-box; |
| 544 | 506 | } |
| 545 | 507 | |
| 546 | 508 | .button:hover, |
| 547 | 509 | button:hover, |
| 548 | -input[type="button"]:hover, | |
| 549 | -input[type="reset"]:hover, | |
| 550 | -.button:focus, | |
| 510 | +input[type=button]:hover, | |
| 511 | +input[type=reset]:hover, | |
| 512 | +input[type=submit]:hover, | |
| 513 | +.button:hover, | |
| 551 | 514 | button:focus, |
| 552 | -input[type="button"]:focus, | |
| 553 | -input[type="reset"]:focus { | |
| 554 | - color: #444 !important; | |
| 555 | - background-color: #eee; | |
| 556 | - border-color: #aaa; | |
| 515 | +input[type=button]:focus, | |
| 516 | +input[type=reset]:focus, | |
| 517 | +input[type=submit]:focus{ | |
| 557 | 518 | outline: 0; |
| 558 | -} | |
| 559 | - | |
| 560 | -input[type="submit"], | |
| 561 | -input[type="button"].submit, | |
| 562 | -button.submit{ | |
| 563 | - color: white !important; | |
| 564 | - background-color: #446979; | |
| 565 | - border-color: #446979; | |
| 566 | -} | |
| 567 | - | |
| 568 | -input[type="submit"]:hover, | |
| 569 | -input[type="submit"]:focus, | |
| 570 | -input[type="button"].submit:hover, | |
| 571 | -input[type="button"].submit:focus, | |
| 572 | -button.submit:hover, | |
| 573 | -button.submit:focus{ | |
| 574 | - color: white !important; | |
| 519 | + color: white; | |
| 575 | 520 | background-color: #648898; |
| 576 | 521 | border-color: #648898; |
| 577 | 522 | } |
| 578 | 523 | |
| 579 | -input[type="submit"]:disabled, | |
| 580 | -input[type="button"].submit:disabled, | |
| 581 | -button.submit:disabled{ | |
| 582 | - color: rgb(128,128,128); | |
| 524 | +.button:disabled, | |
| 525 | +button:disabled, | |
| 526 | +input[type=button]:disabled, | |
| 527 | +input[type=reset]:disabled, | |
| 528 | +input[type=submit]:disabled{ | |
| 529 | + color: #444; | |
| 583 | 530 | background-color: rgb(153,153,153); |
| 584 | 531 | } |
| 532 | + | |
| 533 | +.content a.button, | |
| 534 | +.submenu a.button, | |
| 535 | +.submenu a.button:visited { | |
| 536 | + color: #444; | |
| 537 | + background: #f8f8f8; | |
| 538 | +} | |
| 585 | 539 | |
| 586 | 540 | |
| 587 | 541 | /* Forms |
| 588 | 542 | ––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––– */ |
| 589 | 543 | input[type="email"], |
| @@ -611,12 +565,10 @@ | ||
| 611 | 565 | input[type="text"], |
| 612 | 566 | input[type="tel"], |
| 613 | 567 | input[type="url"], |
| 614 | 568 | input[type="password"], |
| 615 | 569 | textarea { |
| 616 | - -webkit-appearance: none; | |
| 617 | - -moz-appearance: none; | |
| 618 | 570 | appearance: none; |
| 619 | 571 | } |
| 620 | 572 | |
| 621 | 573 | textarea { |
| 622 | 574 | height: inherit; |
| @@ -1293,5 +1245,11 @@ | ||
| 1293 | 1245 | } |
| 1294 | 1246 | |
| 1295 | 1247 | body.forum .forumPosts.fileage a:visited { |
| 1296 | 1248 | color: #648999; |
| 1297 | 1249 | } |
| 1250 | + | |
| 1251 | +/* Chat | |
| 1252 | +––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––– */ | |
| 1253 | +body.chat .chat-input-field:focus { | |
| 1254 | + border-color: #446979; | |
| 1255 | +} | |
| 1298 | 1256 |
| --- skins/blitz/css.txt | |
| +++ skins/blitz/css.txt | |
| @@ -1,17 +1,9 @@ | |
| 1 | /*! normalize.css v3.0.2 | MIT License | git.io/normalize */ |
| 2 | |
| 3 | /** |
| 4 | * 1. Set default font family to sans-serif. |
| 5 | * 2. Prevent iOS text size adjust after orientation change, without disabling |
| 6 | * user zoom. |
| 7 | */ |
| 8 | |
| 9 | html { |
| 10 | font-family: sans-serif; /* 1 */ |
| 11 | -ms-text-size-adjust: 100%; /* 2 */ |
| 12 | -webkit-text-size-adjust: 100%; /* 2 */ |
| 13 | } |
| 14 | |
| 15 | /** |
| 16 | * Remove default margin. |
| 17 | */ |
| @@ -207,11 +199,10 @@ | |
| 207 | /** |
| 208 | * Address differences between Firefox and other browsers. |
| 209 | */ |
| 210 | |
| 211 | hr { |
| 212 | -moz-box-sizing: content-box; |
| 213 | box-sizing: content-box; |
| 214 | height: 0; |
| 215 | } |
| 216 | |
| 217 | /** |
| @@ -277,47 +268,27 @@ | |
| 277 | button, |
| 278 | select { |
| 279 | text-transform: none; |
| 280 | } |
| 281 | |
| 282 | /** |
| 283 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` |
| 284 | * and `video` controls. |
| 285 | * 2. Correct inability to style clickable `input` types in iOS. |
| 286 | * 3. Improve usability and consistency of cursor style between image-type |
| 287 | * `input` and others. |
| 288 | */ |
| 289 | |
| 290 | button, |
| 291 | html input[type="button"], /* 1 */ |
| 292 | input[type="reset"], |
| 293 | input[type="submit"], |
| 294 | input[type="button"].submit, |
| 295 | button.submit{ |
| 296 | -webkit-appearance: button; /* 2 */ |
| 297 | cursor: pointer; /* 3 */ |
| 298 | } |
| 299 | |
| 300 | /** |
| 301 | * Re-set default cursor for disabled elements. |
| 302 | */ |
| 303 | |
| 304 | button[disabled], |
| 305 | html input[disabled] { |
| 306 | cursor: default; |
| 307 | } |
| 308 | |
| 309 | /** |
| 310 | * Remove inner padding and border in Firefox 4+. |
| 311 | */ |
| 312 | |
| 313 | button::-moz-focus-inner, |
| 314 | input::-moz-focus-inner { |
| 315 | border: 0; |
| 316 | padding: 0; |
| 317 | } |
| 318 | |
| 319 | /** |
| 320 | * Address Firefox 4+ setting `line-height` on `input` using `!important` in |
| 321 | * the UA stylesheet. |
| 322 | */ |
| 323 | |
| @@ -348,20 +319,11 @@ | |
| 348 | input[type="number"]::-webkit-inner-spin-button, |
| 349 | input[type="number"]::-webkit-outer-spin-button { |
| 350 | height: auto; |
| 351 | } |
| 352 | |
| 353 | /** |
| 354 | * 1. Address `appearance` set to `searchfield` in Safari and Chrome. |
| 355 | * 2. Address `box-sizing` set to `border-box` in Safari and Chrome |
| 356 | * (include `-moz` to future-proof). |
| 357 | */ |
| 358 | |
| 359 | input[type="search"] { |
| 360 | -webkit-appearance: textfield; /* 1 */ |
| 361 | -moz-box-sizing: content-box; |
| 362 | -webkit-box-sizing: content-box; /* 2 */ |
| 363 | box-sizing: content-box; |
| 364 | } |
| 365 | |
| 366 | /** |
| 367 | * Remove inner padding and search cancel button in Safari and Chrome on OS X. |
| @@ -517,73 +479,65 @@ | |
| 517 | |
| 518 | /* Buttons |
| 519 | ––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––– */ |
| 520 | .button, |
| 521 | button, |
| 522 | input[type="button"], |
| 523 | input[type="reset"], |
| 524 | input[type="submit"], |
| 525 | input[type="button"].submit, |
| 526 | button.submit{ |
| 527 | display: inline-block; |
| 528 | height: 3.3rem; |
| 529 | padding: 0 2.2rem; |
| 530 | color: #555 !important; |
| 531 | text-align: center; |
| 532 | font-size: 1.1rem; |
| 533 | font-weight: 700; |
| 534 | line-height: 3.3rem; |
| 535 | letter-spacing: .08rem; |
| 536 | text-transform: uppercase; |
| 537 | text-decoration: none; |
| 538 | white-space: nowrap; |
| 539 | background-color: transparent; |
| 540 | border-radius: 4px; |
| 541 | border: 1px solid #ccc; |
| 542 | cursor: pointer; |
| 543 | box-sizing: border-box; |
| 544 | } |
| 545 | |
| 546 | .button:hover, |
| 547 | button:hover, |
| 548 | input[type="button"]:hover, |
| 549 | input[type="reset"]:hover, |
| 550 | .button:focus, |
| 551 | button:focus, |
| 552 | input[type="button"]:focus, |
| 553 | input[type="reset"]:focus { |
| 554 | color: #444 !important; |
| 555 | background-color: #eee; |
| 556 | border-color: #aaa; |
| 557 | outline: 0; |
| 558 | } |
| 559 | |
| 560 | input[type="submit"], |
| 561 | input[type="button"].submit, |
| 562 | button.submit{ |
| 563 | color: white !important; |
| 564 | background-color: #446979; |
| 565 | border-color: #446979; |
| 566 | } |
| 567 | |
| 568 | input[type="submit"]:hover, |
| 569 | input[type="submit"]:focus, |
| 570 | input[type="button"].submit:hover, |
| 571 | input[type="button"].submit:focus, |
| 572 | button.submit:hover, |
| 573 | button.submit:focus{ |
| 574 | color: white !important; |
| 575 | background-color: #648898; |
| 576 | border-color: #648898; |
| 577 | } |
| 578 | |
| 579 | input[type="submit"]:disabled, |
| 580 | input[type="button"].submit:disabled, |
| 581 | button.submit:disabled{ |
| 582 | color: rgb(128,128,128); |
| 583 | background-color: rgb(153,153,153); |
| 584 | } |
| 585 | |
| 586 | |
| 587 | /* Forms |
| 588 | ––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––– */ |
| 589 | input[type="email"], |
| @@ -611,12 +565,10 @@ | |
| 611 | input[type="text"], |
| 612 | input[type="tel"], |
| 613 | input[type="url"], |
| 614 | input[type="password"], |
| 615 | textarea { |
| 616 | -webkit-appearance: none; |
| 617 | -moz-appearance: none; |
| 618 | appearance: none; |
| 619 | } |
| 620 | |
| 621 | textarea { |
| 622 | height: inherit; |
| @@ -1293,5 +1245,11 @@ | |
| 1293 | } |
| 1294 | |
| 1295 | body.forum .forumPosts.fileage a:visited { |
| 1296 | color: #648999; |
| 1297 | } |
| 1298 |
| --- skins/blitz/css.txt | |
| +++ skins/blitz/css.txt | |
| @@ -1,17 +1,9 @@ | |
| 1 | /*! normalize.css v3.0.2 | MIT License | git.io/normalize */ |
| 2 | |
| 3 | html { |
| 4 | font-family: sans-serif; /* 1 */ |
| 5 | } |
| 6 | |
| 7 | /** |
| 8 | * Remove default margin. |
| 9 | */ |
| @@ -207,11 +199,10 @@ | |
| 199 | /** |
| 200 | * Address differences between Firefox and other browsers. |
| 201 | */ |
| 202 | |
| 203 | hr { |
| 204 | box-sizing: content-box; |
| 205 | height: 0; |
| 206 | } |
| 207 | |
| 208 | /** |
| @@ -277,47 +268,27 @@ | |
| 268 | button, |
| 269 | select { |
| 270 | text-transform: none; |
| 271 | } |
| 272 | |
| 273 | button, |
| 274 | input[type=button], |
| 275 | input[type=reset], |
| 276 | input[type=submit]{ |
| 277 | cursor: pointer; |
| 278 | } |
| 279 | |
| 280 | /** |
| 281 | * Re-set default cursor for disabled elements. |
| 282 | */ |
| 283 | button:disabled, |
| 284 | input[type=button]:disabled, |
| 285 | input[type=reset]:disabled, |
| 286 | input[type=submit]:disabled{ |
| 287 | cursor: default; |
| 288 | } |
| 289 | |
| 290 | /** |
| 291 | * Address Firefox 4+ setting `line-height` on `input` using `!important` in |
| 292 | * the UA stylesheet. |
| 293 | */ |
| 294 | |
| @@ -348,20 +319,11 @@ | |
| 319 | input[type="number"]::-webkit-inner-spin-button, |
| 320 | input[type="number"]::-webkit-outer-spin-button { |
| 321 | height: auto; |
| 322 | } |
| 323 | |
| 324 | input[type="search"] { |
| 325 | box-sizing: content-box; |
| 326 | } |
| 327 | |
| 328 | /** |
| 329 | * Remove inner padding and search cancel button in Safari and Chrome on OS X. |
| @@ -517,73 +479,65 @@ | |
| 479 | |
| 480 | /* Buttons |
| 481 | ––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––– */ |
| 482 | .button, |
| 483 | button, |
| 484 | input[type=button], |
| 485 | input[type=reset], |
| 486 | input[type=submit]{ |
| 487 | display: inline-block; |
| 488 | height: 3.3rem; |
| 489 | padding: 0 2.2rem; |
| 490 | color: #444; |
| 491 | background-color: #f8f8f8;; |
| 492 | border-color: #446979; |
| 493 | text-align: center; |
| 494 | font-size: 1.1rem; |
| 495 | font-weight: 700; |
| 496 | line-height: 3.3rem; |
| 497 | letter-spacing: .08rem; |
| 498 | text-transform: uppercase; |
| 499 | text-decoration: none; |
| 500 | white-space: nowrap; |
| 501 | border-radius: 4px; |
| 502 | /*border: 1px solid #ccc;*/ |
| 503 | border: 1px solid #446979; |
| 504 | cursor: pointer; |
| 505 | box-sizing: border-box; |
| 506 | } |
| 507 | |
| 508 | .button:hover, |
| 509 | button:hover, |
| 510 | input[type=button]:hover, |
| 511 | input[type=reset]:hover, |
| 512 | input[type=submit]:hover, |
| 513 | .button:hover, |
| 514 | button:focus, |
| 515 | input[type=button]:focus, |
| 516 | input[type=reset]:focus, |
| 517 | input[type=submit]:focus{ |
| 518 | outline: 0; |
| 519 | color: white; |
| 520 | background-color: #648898; |
| 521 | border-color: #648898; |
| 522 | } |
| 523 | |
| 524 | .button:disabled, |
| 525 | button:disabled, |
| 526 | input[type=button]:disabled, |
| 527 | input[type=reset]:disabled, |
| 528 | input[type=submit]:disabled{ |
| 529 | color: #444; |
| 530 | background-color: rgb(153,153,153); |
| 531 | } |
| 532 | |
| 533 | .content a.button, |
| 534 | .submenu a.button, |
| 535 | .submenu a.button:visited { |
| 536 | color: #444; |
| 537 | background: #f8f8f8; |
| 538 | } |
| 539 | |
| 540 | |
| 541 | /* Forms |
| 542 | ––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––– */ |
| 543 | input[type="email"], |
| @@ -611,12 +565,10 @@ | |
| 565 | input[type="text"], |
| 566 | input[type="tel"], |
| 567 | input[type="url"], |
| 568 | input[type="password"], |
| 569 | textarea { |
| 570 | appearance: none; |
| 571 | } |
| 572 | |
| 573 | textarea { |
| 574 | height: inherit; |
| @@ -1293,5 +1245,11 @@ | |
| 1245 | } |
| 1246 | |
| 1247 | body.forum .forumPosts.fileage a:visited { |
| 1248 | color: #648999; |
| 1249 | } |
| 1250 | |
| 1251 | /* Chat |
| 1252 | ––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––– */ |
| 1253 | body.chat .chat-input-field:focus { |
| 1254 | border-color: #446979; |
| 1255 | } |
| 1256 |
+30
-37
| --- skins/darkmode/css.txt | ||
| +++ skins/darkmode/css.txt | ||
| @@ -98,50 +98,56 @@ | ||
| 98 | 98 | } |
| 99 | 99 | .fileage tr:hover, |
| 100 | 100 | div.filetreeline:hover { |
| 101 | 101 | background-color: #333; |
| 102 | 102 | } |
| 103 | -div.file-change-line button { | |
| 103 | +div.file-change-line button{ | |
| 104 | 104 | background-color: #484848 |
| 105 | 105 | } |
| 106 | +button, | |
| 107 | +input, | |
| 108 | +optgroup, | |
| 109 | +select, | |
| 110 | +textarea { | |
| 111 | + background: inherit; | |
| 112 | + color: inherit; | |
| 113 | + font: inherit; | |
| 114 | + margin: 0 | |
| 115 | +} | |
| 106 | 116 | .button, |
| 107 | -button { | |
| 108 | - color: #aaa; | |
| 109 | - background-color: #484848; | |
| 110 | - border-radius: 5px; | |
| 111 | - border: 0 | |
| 112 | -} | |
| 113 | -.button:hover, | |
| 114 | -button:hover { | |
| 115 | - background-color: #FF4500f0; | |
| 116 | - color: rgba(24,24,24,0.8); | |
| 117 | - outline: 0 | |
| 118 | -} | |
| 117 | +button, | |
| 119 | 118 | input[type=button], |
| 120 | -input[type=reset], | |
| 121 | -input[type=submit] { | |
| 119 | +input[type=submit], | |
| 120 | +input[type=reset]{ | |
| 122 | 121 | color: #ddd; |
| 123 | 122 | background-color: #446979; |
| 123 | + border-radius: 5px; | |
| 124 | 124 | border: 0; |
| 125 | - border-radius: 5px | |
| 126 | 125 | } |
| 126 | +.button:hover, | |
| 127 | +button:hover, | |
| 127 | 128 | input[type=button]:hover, |
| 128 | -input[type=reset]:hover, | |
| 129 | -input[type=submit]:hover { | |
| 129 | +input[type=submit]:hover, | |
| 130 | +input[type=reset]:hover { | |
| 130 | 131 | background-color: #FF4500f0; |
| 131 | 132 | color: rgba(24,24,24,0.8); |
| 132 | - outline: 0 | |
| 133 | + outline: 0; | |
| 133 | 134 | } |
| 134 | -input[type=submit]:disabled { | |
| 135 | - color: #363636; | |
| 136 | - background-color: #707070; | |
| 135 | +.button:disabled, | |
| 136 | +button:disabled, | |
| 137 | +input[type=button]:disabled, | |
| 138 | +input[type=submit]:disabled, | |
| 139 | +input[type=reset]:disabled{ | |
| 140 | + color: #222; | |
| 141 | + background-color: #b0b0b0; | |
| 142 | + opacity: 0.8; | |
| 137 | 143 | } |
| 138 | 144 | .button:focus, |
| 139 | 145 | button:focus, |
| 140 | 146 | input[type=button]:focus, |
| 141 | -input[type=reset]:focus, | |
| 142 | -input[type=submit]:focus { | |
| 147 | +input[type=submit]:focus, | |
| 148 | +input[type=reset]:focus { | |
| 143 | 149 | outline: 2px outset #333; |
| 144 | 150 | border-color: #888; |
| 145 | 151 | } |
| 146 | 152 | |
| 147 | 153 | /* All page content from the bottom of the menu or submenu down to |
| @@ -529,23 +535,10 @@ | ||
| 529 | 535 | } |
| 530 | 536 | span.snippet>mark { |
| 531 | 537 | color: white; |
| 532 | 538 | font-weight: bold; |
| 533 | 539 | } |
| 534 | -button, | |
| 535 | -input, | |
| 536 | -optgroup, | |
| 537 | -select, | |
| 538 | -textarea { | |
| 539 | - background: inherit; | |
| 540 | - color: inherit; | |
| 541 | - font: inherit; | |
| 542 | - margin: 0 | |
| 543 | -} | |
| 544 | -button { | |
| 545 | - background-color: rgba(45,45,45,0.75); | |
| 546 | -} | |
| 547 | 540 | input, textarea, select { |
| 548 | 541 | border: 1px solid rgba(127, 201, 255, 0.9); |
| 549 | 542 | padding: 1px; |
| 550 | 543 | } |
| 551 | 544 | select { |
| 552 | 545 |
| --- skins/darkmode/css.txt | |
| +++ skins/darkmode/css.txt | |
| @@ -98,50 +98,56 @@ | |
| 98 | } |
| 99 | .fileage tr:hover, |
| 100 | div.filetreeline:hover { |
| 101 | background-color: #333; |
| 102 | } |
| 103 | div.file-change-line button { |
| 104 | background-color: #484848 |
| 105 | } |
| 106 | .button, |
| 107 | button { |
| 108 | color: #aaa; |
| 109 | background-color: #484848; |
| 110 | border-radius: 5px; |
| 111 | border: 0 |
| 112 | } |
| 113 | .button:hover, |
| 114 | button:hover { |
| 115 | background-color: #FF4500f0; |
| 116 | color: rgba(24,24,24,0.8); |
| 117 | outline: 0 |
| 118 | } |
| 119 | input[type=button], |
| 120 | input[type=reset], |
| 121 | input[type=submit] { |
| 122 | color: #ddd; |
| 123 | background-color: #446979; |
| 124 | border: 0; |
| 125 | border-radius: 5px |
| 126 | } |
| 127 | input[type=button]:hover, |
| 128 | input[type=reset]:hover, |
| 129 | input[type=submit]:hover { |
| 130 | background-color: #FF4500f0; |
| 131 | color: rgba(24,24,24,0.8); |
| 132 | outline: 0 |
| 133 | } |
| 134 | input[type=submit]:disabled { |
| 135 | color: #363636; |
| 136 | background-color: #707070; |
| 137 | } |
| 138 | .button:focus, |
| 139 | button:focus, |
| 140 | input[type=button]:focus, |
| 141 | input[type=reset]:focus, |
| 142 | input[type=submit]:focus { |
| 143 | outline: 2px outset #333; |
| 144 | border-color: #888; |
| 145 | } |
| 146 | |
| 147 | /* All page content from the bottom of the menu or submenu down to |
| @@ -529,23 +535,10 @@ | |
| 529 | } |
| 530 | span.snippet>mark { |
| 531 | color: white; |
| 532 | font-weight: bold; |
| 533 | } |
| 534 | button, |
| 535 | input, |
| 536 | optgroup, |
| 537 | select, |
| 538 | textarea { |
| 539 | background: inherit; |
| 540 | color: inherit; |
| 541 | font: inherit; |
| 542 | margin: 0 |
| 543 | } |
| 544 | button { |
| 545 | background-color: rgba(45,45,45,0.75); |
| 546 | } |
| 547 | input, textarea, select { |
| 548 | border: 1px solid rgba(127, 201, 255, 0.9); |
| 549 | padding: 1px; |
| 550 | } |
| 551 | select { |
| 552 |
| --- skins/darkmode/css.txt | |
| +++ skins/darkmode/css.txt | |
| @@ -98,50 +98,56 @@ | |
| 98 | } |
| 99 | .fileage tr:hover, |
| 100 | div.filetreeline:hover { |
| 101 | background-color: #333; |
| 102 | } |
| 103 | div.file-change-line button{ |
| 104 | background-color: #484848 |
| 105 | } |
| 106 | button, |
| 107 | input, |
| 108 | optgroup, |
| 109 | select, |
| 110 | textarea { |
| 111 | background: inherit; |
| 112 | color: inherit; |
| 113 | font: inherit; |
| 114 | margin: 0 |
| 115 | } |
| 116 | .button, |
| 117 | button, |
| 118 | input[type=button], |
| 119 | input[type=submit], |
| 120 | input[type=reset]{ |
| 121 | color: #ddd; |
| 122 | background-color: #446979; |
| 123 | border-radius: 5px; |
| 124 | border: 0; |
| 125 | } |
| 126 | .button:hover, |
| 127 | button:hover, |
| 128 | input[type=button]:hover, |
| 129 | input[type=submit]:hover, |
| 130 | input[type=reset]:hover { |
| 131 | background-color: #FF4500f0; |
| 132 | color: rgba(24,24,24,0.8); |
| 133 | outline: 0; |
| 134 | } |
| 135 | .button:disabled, |
| 136 | button:disabled, |
| 137 | input[type=button]:disabled, |
| 138 | input[type=submit]:disabled, |
| 139 | input[type=reset]:disabled{ |
| 140 | color: #222; |
| 141 | background-color: #b0b0b0; |
| 142 | opacity: 0.8; |
| 143 | } |
| 144 | .button:focus, |
| 145 | button:focus, |
| 146 | input[type=button]:focus, |
| 147 | input[type=submit]:focus, |
| 148 | input[type=reset]:focus { |
| 149 | outline: 2px outset #333; |
| 150 | border-color: #888; |
| 151 | } |
| 152 | |
| 153 | /* All page content from the bottom of the menu or submenu down to |
| @@ -529,23 +535,10 @@ | |
| 535 | } |
| 536 | span.snippet>mark { |
| 537 | color: white; |
| 538 | font-weight: bold; |
| 539 | } |
| 540 | input, textarea, select { |
| 541 | border: 1px solid rgba(127, 201, 255, 0.9); |
| 542 | padding: 1px; |
| 543 | } |
| 544 | select { |
| 545 |
+72
-10
| --- src/ajax.c | ||
| +++ src/ajax.c | ||
| @@ -191,23 +191,52 @@ | ||
| 191 | 191 | ** {error: formatted message} |
| 192 | 192 | ** |
| 193 | 193 | ** If httpCode<=0 then it defaults to 500. |
| 194 | 194 | ** |
| 195 | 195 | ** After calling this, the caller should immediately return. |
| 196 | +** | |
| 197 | +** Returns the resulting http code. | |
| 196 | 198 | */ |
| 197 | -void ajax_route_error(int httpCode, const char * zFmt, ...){ | |
| 199 | +int ajax_route_error(int httpCode, const char * zFmt, ...){ | |
| 198 | 200 | Blob msg = empty_blob; |
| 199 | 201 | Blob content = empty_blob; |
| 200 | 202 | va_list vargs; |
| 203 | + | |
| 204 | + if( httpCode<=0 ) httpCode=500; | |
| 201 | 205 | va_start(vargs,zFmt); |
| 202 | 206 | blob_vappendf(&msg, zFmt, vargs); |
| 203 | 207 | va_end(vargs); |
| 204 | 208 | blob_appendf(&content,"{\"error\":%!j}", blob_str(&msg)); |
| 205 | 209 | blob_reset(&msg); |
| 206 | 210 | cgi_set_content(&content); |
| 207 | - cgi_set_status(httpCode>0 ? httpCode : 500, "Error"); | |
| 211 | + cgi_set_status(httpCode, "Error"); | |
| 208 | 212 | cgi_set_content_type("application/json"); |
| 213 | + return httpCode; | |
| 214 | +} | |
| 215 | + | |
| 216 | +void ajax_route_error_forbidden(){ | |
| 217 | + ajax_route_error(403, "Permission denied."); | |
| 218 | +} | |
| 219 | + | |
| 220 | +void ajax_route_error_captcha(){ | |
| 221 | + ajax_route_error(400, "Invalid captcha response."); | |
| 222 | +} | |
| 223 | + | |
| 224 | +void ajax_route_error_csrf(){ | |
| 225 | + ajax_route_error(403, "Invalid CSRF signature."); | |
| 226 | +} | |
| 227 | + | |
| 228 | +void ajax_route_error_404(const char *zMsg){ | |
| 229 | + ajax_route_error(404, "%s", zMsg ? zMsg : "Resource not found."); | |
| 230 | +} | |
| 231 | + | |
| 232 | +int ajax_check_csrf(int level){ | |
| 233 | + if( 0==cgi_csrf_safe(level) ){ | |
| 234 | + ajax_route_error_csrf(); | |
| 235 | + return 0; | |
| 236 | + } | |
| 237 | + return 1; | |
| 209 | 238 | } |
| 210 | 239 | |
| 211 | 240 | /* |
| 212 | 241 | ** Performs bootstrapping common to the /ajax/xyz AJAX routes, such as |
| 213 | 242 | ** logging in the user. |
| @@ -224,17 +253,18 @@ | ||
| 224 | 253 | int ajax_route_bootstrap(int requireWrite, int requirePost){ |
| 225 | 254 | login_check_credentials(); |
| 226 | 255 | if( requireWrite!=0 && g.perm.Write==0 ){ |
| 227 | 256 | ajax_route_error(403,"Write permissions required."); |
| 228 | 257 | return 0; |
| 229 | - }else if(0==cgi_csrf_safe(requirePost)){ | |
| 258 | + }else if(requirePost && 0==cgi_csrf_safe(requirePost)){ | |
| 230 | 259 | ajax_route_error(403, |
| 231 | 260 | "CSRF violation (make sure sending of HTTP " |
| 232 | 261 | "Referer headers is enabled for XHR " |
| 233 | 262 | "connections)."); |
| 234 | 263 | return 0; |
| 235 | 264 | } |
| 265 | + cgi_set_content_type("application/json"); | |
| 236 | 266 | return 1; |
| 237 | 267 | } |
| 238 | 268 | |
| 239 | 269 | /* |
| 240 | 270 | ** Helper for collecting filename/check-in request parameters. |
| @@ -283,12 +313,10 @@ | ||
| 283 | 313 | ** AJAX_RENDER_PLAIN_TEXT mode. |
| 284 | 314 | ** |
| 285 | 315 | ** iframe_height=integer (default=40) Height, in EMs of HTML preview |
| 286 | 316 | ** iframe. |
| 287 | 317 | ** |
| 288 | -** User must have Write access to use this page. | |
| 289 | -** | |
| 290 | 318 | ** Responds with the HTML content of the preview. On error it produces |
| 291 | 319 | ** a JSON response as documented for ajax_route_error(). |
| 292 | 320 | ** |
| 293 | 321 | ** Extra response headers: |
| 294 | 322 | ** |
| @@ -341,10 +369,37 @@ | ||
| 341 | 369 | } |
| 342 | 370 | if(zRenderMode!=0){ |
| 343 | 371 | cgi_printf_header("x-ajax-render-mode: %s\r\n", zRenderMode); |
| 344 | 372 | } |
| 345 | 373 | } |
| 374 | + | |
| 375 | +/* | |
| 376 | +** AJAX route /ajax/artifact.json. | |
| 377 | +** URL arguments: | |
| 378 | +** | |
| 379 | +** uuid=ARTIFACT_ID REQUIRED | |
| 380 | +** | |
| 381 | +** and emits either: | |
| 382 | +** | |
| 383 | +** { error: "..." } | |
| 384 | +** | |
| 385 | +** with a non-200 response code or the artifact's manifest in JSON | |
| 386 | +** form with a 200 response code. | |
| 387 | +*/ | |
| 388 | +void ajax_route_artifact_json(void){ | |
| 389 | + const char *zUuid = P("uuid"); | |
| 390 | + Blob json = BLOB_INITIALIZER; | |
| 391 | + login_check_credentials(); | |
| 392 | + if( ! g.perm.Read ){ | |
| 393 | + ajax_route_error_forbidden(); | |
| 394 | + }else if( artifact_to_json_by_name(zUuid, &json) ){ | |
| 395 | + @ %b(&json) | |
| 396 | + }else{ | |
| 397 | + ajax_route_error_404("Cannot resolve artifact ID."); | |
| 398 | + } | |
| 399 | + blob_reset(&json); | |
| 400 | +} | |
| 346 | 401 | |
| 347 | 402 | #if INTERFACE |
| 348 | 403 | /* |
| 349 | 404 | ** Internal mapping of ajax sub-route names to various metadata. |
| 350 | 405 | */ |
| @@ -351,11 +406,12 @@ | ||
| 351 | 406 | struct AjaxRoute { |
| 352 | 407 | const char *zName; /* Name part of the route after "ajax/" */ |
| 353 | 408 | void (*xCallback)(); /* Impl function for the route. */ |
| 354 | 409 | int bWriteMode; /* True if requires write mode */ |
| 355 | 410 | int bPost; /* True if requires POST (i.e. CSRF |
| 356 | - ** verification) */ | |
| 411 | + ** verification). Value is passed to | |
| 412 | + ** cgi_csrf_safe(). */ | |
| 357 | 413 | }; |
| 358 | 414 | typedef struct AjaxRoute AjaxRoute; |
| 359 | 415 | #endif /*INTERFACE*/ |
| 360 | 416 | |
| 361 | 417 | /* |
| @@ -392,20 +448,26 @@ | ||
| 392 | 448 | const char * zName = P("name"); |
| 393 | 449 | AjaxRoute routeName = {0,0,0,0}; |
| 394 | 450 | const AjaxRoute * pRoute = 0; |
| 395 | 451 | const AjaxRoute routes[] = { |
| 396 | 452 | /* Keep these sorted by zName (for bsearch()) */ |
| 453 | + {"artifact.json", ajax_route_artifact_json, 0, 0}, | |
| 397 | 454 | {"preview-text", ajax_route_preview_text, 0, 1 |
| 398 | - /* Note that this does not require write permissions in the repo. | |
| 399 | - ** It should arguably require write permissions but doing means | |
| 400 | - ** that /chat does not work without check-in permissions: | |
| 455 | + /* Preview does not require write permissions in the repo. It | |
| 456 | + ** should arguably require write permissions simply to limit abuse | |
| 457 | + ** but doing means that /chat does not work without check-in | |
| 458 | + ** permissions: | |
| 401 | 459 | ** |
| 402 | 460 | ** https://fossil-scm.org/forum/forumpost/ed4a762b3a557898 |
| 403 | 461 | ** |
| 404 | 462 | ** This particular route is used by /fileedit and /chat, whereas |
| 405 | 463 | ** /wikiedit uses a simpler wiki-specific route. |
| 406 | - */ } | |
| 464 | + */ | |
| 465 | + /* TODO (2026-06-09): preview.txt, preview.md, preview.wiki as | |
| 466 | + ** shorthand for preview-text?filename=X.(txt|md|wiki), noting that | |
| 467 | + ** the filename is only used for mimetype determination. */ | |
| 468 | + } | |
| 407 | 469 | }; |
| 408 | 470 | |
| 409 | 471 | if(zName==0 || zName[0]==0){ |
| 410 | 472 | ajax_route_error(400,"Missing required [route] 'name' parameter."); |
| 411 | 473 | return; |
| 412 | 474 |
| --- src/ajax.c | |
| +++ src/ajax.c | |
| @@ -191,23 +191,52 @@ | |
| 191 | ** {error: formatted message} |
| 192 | ** |
| 193 | ** If httpCode<=0 then it defaults to 500. |
| 194 | ** |
| 195 | ** After calling this, the caller should immediately return. |
| 196 | */ |
| 197 | void ajax_route_error(int httpCode, const char * zFmt, ...){ |
| 198 | Blob msg = empty_blob; |
| 199 | Blob content = empty_blob; |
| 200 | va_list vargs; |
| 201 | va_start(vargs,zFmt); |
| 202 | blob_vappendf(&msg, zFmt, vargs); |
| 203 | va_end(vargs); |
| 204 | blob_appendf(&content,"{\"error\":%!j}", blob_str(&msg)); |
| 205 | blob_reset(&msg); |
| 206 | cgi_set_content(&content); |
| 207 | cgi_set_status(httpCode>0 ? httpCode : 500, "Error"); |
| 208 | cgi_set_content_type("application/json"); |
| 209 | } |
| 210 | |
| 211 | /* |
| 212 | ** Performs bootstrapping common to the /ajax/xyz AJAX routes, such as |
| 213 | ** logging in the user. |
| @@ -224,17 +253,18 @@ | |
| 224 | int ajax_route_bootstrap(int requireWrite, int requirePost){ |
| 225 | login_check_credentials(); |
| 226 | if( requireWrite!=0 && g.perm.Write==0 ){ |
| 227 | ajax_route_error(403,"Write permissions required."); |
| 228 | return 0; |
| 229 | }else if(0==cgi_csrf_safe(requirePost)){ |
| 230 | ajax_route_error(403, |
| 231 | "CSRF violation (make sure sending of HTTP " |
| 232 | "Referer headers is enabled for XHR " |
| 233 | "connections)."); |
| 234 | return 0; |
| 235 | } |
| 236 | return 1; |
| 237 | } |
| 238 | |
| 239 | /* |
| 240 | ** Helper for collecting filename/check-in request parameters. |
| @@ -283,12 +313,10 @@ | |
| 283 | ** AJAX_RENDER_PLAIN_TEXT mode. |
| 284 | ** |
| 285 | ** iframe_height=integer (default=40) Height, in EMs of HTML preview |
| 286 | ** iframe. |
| 287 | ** |
| 288 | ** User must have Write access to use this page. |
| 289 | ** |
| 290 | ** Responds with the HTML content of the preview. On error it produces |
| 291 | ** a JSON response as documented for ajax_route_error(). |
| 292 | ** |
| 293 | ** Extra response headers: |
| 294 | ** |
| @@ -341,10 +369,37 @@ | |
| 341 | } |
| 342 | if(zRenderMode!=0){ |
| 343 | cgi_printf_header("x-ajax-render-mode: %s\r\n", zRenderMode); |
| 344 | } |
| 345 | } |
| 346 | |
| 347 | #if INTERFACE |
| 348 | /* |
| 349 | ** Internal mapping of ajax sub-route names to various metadata. |
| 350 | */ |
| @@ -351,11 +406,12 @@ | |
| 351 | struct AjaxRoute { |
| 352 | const char *zName; /* Name part of the route after "ajax/" */ |
| 353 | void (*xCallback)(); /* Impl function for the route. */ |
| 354 | int bWriteMode; /* True if requires write mode */ |
| 355 | int bPost; /* True if requires POST (i.e. CSRF |
| 356 | ** verification) */ |
| 357 | }; |
| 358 | typedef struct AjaxRoute AjaxRoute; |
| 359 | #endif /*INTERFACE*/ |
| 360 | |
| 361 | /* |
| @@ -392,20 +448,26 @@ | |
| 392 | const char * zName = P("name"); |
| 393 | AjaxRoute routeName = {0,0,0,0}; |
| 394 | const AjaxRoute * pRoute = 0; |
| 395 | const AjaxRoute routes[] = { |
| 396 | /* Keep these sorted by zName (for bsearch()) */ |
| 397 | {"preview-text", ajax_route_preview_text, 0, 1 |
| 398 | /* Note that this does not require write permissions in the repo. |
| 399 | ** It should arguably require write permissions but doing means |
| 400 | ** that /chat does not work without check-in permissions: |
| 401 | ** |
| 402 | ** https://fossil-scm.org/forum/forumpost/ed4a762b3a557898 |
| 403 | ** |
| 404 | ** This particular route is used by /fileedit and /chat, whereas |
| 405 | ** /wikiedit uses a simpler wiki-specific route. |
| 406 | */ } |
| 407 | }; |
| 408 | |
| 409 | if(zName==0 || zName[0]==0){ |
| 410 | ajax_route_error(400,"Missing required [route] 'name' parameter."); |
| 411 | return; |
| 412 |
| --- src/ajax.c | |
| +++ src/ajax.c | |
| @@ -191,23 +191,52 @@ | |
| 191 | ** {error: formatted message} |
| 192 | ** |
| 193 | ** If httpCode<=0 then it defaults to 500. |
| 194 | ** |
| 195 | ** After calling this, the caller should immediately return. |
| 196 | ** |
| 197 | ** Returns the resulting http code. |
| 198 | */ |
| 199 | int ajax_route_error(int httpCode, const char * zFmt, ...){ |
| 200 | Blob msg = empty_blob; |
| 201 | Blob content = empty_blob; |
| 202 | va_list vargs; |
| 203 | |
| 204 | if( httpCode<=0 ) httpCode=500; |
| 205 | va_start(vargs,zFmt); |
| 206 | blob_vappendf(&msg, zFmt, vargs); |
| 207 | va_end(vargs); |
| 208 | blob_appendf(&content,"{\"error\":%!j}", blob_str(&msg)); |
| 209 | blob_reset(&msg); |
| 210 | cgi_set_content(&content); |
| 211 | cgi_set_status(httpCode, "Error"); |
| 212 | cgi_set_content_type("application/json"); |
| 213 | return httpCode; |
| 214 | } |
| 215 | |
| 216 | void ajax_route_error_forbidden(){ |
| 217 | ajax_route_error(403, "Permission denied."); |
| 218 | } |
| 219 | |
| 220 | void ajax_route_error_captcha(){ |
| 221 | ajax_route_error(400, "Invalid captcha response."); |
| 222 | } |
| 223 | |
| 224 | void ajax_route_error_csrf(){ |
| 225 | ajax_route_error(403, "Invalid CSRF signature."); |
| 226 | } |
| 227 | |
| 228 | void ajax_route_error_404(const char *zMsg){ |
| 229 | ajax_route_error(404, "%s", zMsg ? zMsg : "Resource not found."); |
| 230 | } |
| 231 | |
| 232 | int ajax_check_csrf(int level){ |
| 233 | if( 0==cgi_csrf_safe(level) ){ |
| 234 | ajax_route_error_csrf(); |
| 235 | return 0; |
| 236 | } |
| 237 | return 1; |
| 238 | } |
| 239 | |
| 240 | /* |
| 241 | ** Performs bootstrapping common to the /ajax/xyz AJAX routes, such as |
| 242 | ** logging in the user. |
| @@ -224,17 +253,18 @@ | |
| 253 | int ajax_route_bootstrap(int requireWrite, int requirePost){ |
| 254 | login_check_credentials(); |
| 255 | if( requireWrite!=0 && g.perm.Write==0 ){ |
| 256 | ajax_route_error(403,"Write permissions required."); |
| 257 | return 0; |
| 258 | }else if(requirePost && 0==cgi_csrf_safe(requirePost)){ |
| 259 | ajax_route_error(403, |
| 260 | "CSRF violation (make sure sending of HTTP " |
| 261 | "Referer headers is enabled for XHR " |
| 262 | "connections)."); |
| 263 | return 0; |
| 264 | } |
| 265 | cgi_set_content_type("application/json"); |
| 266 | return 1; |
| 267 | } |
| 268 | |
| 269 | /* |
| 270 | ** Helper for collecting filename/check-in request parameters. |
| @@ -283,12 +313,10 @@ | |
| 313 | ** AJAX_RENDER_PLAIN_TEXT mode. |
| 314 | ** |
| 315 | ** iframe_height=integer (default=40) Height, in EMs of HTML preview |
| 316 | ** iframe. |
| 317 | ** |
| 318 | ** Responds with the HTML content of the preview. On error it produces |
| 319 | ** a JSON response as documented for ajax_route_error(). |
| 320 | ** |
| 321 | ** Extra response headers: |
| 322 | ** |
| @@ -341,10 +369,37 @@ | |
| 369 | } |
| 370 | if(zRenderMode!=0){ |
| 371 | cgi_printf_header("x-ajax-render-mode: %s\r\n", zRenderMode); |
| 372 | } |
| 373 | } |
| 374 | |
| 375 | /* |
| 376 | ** AJAX route /ajax/artifact.json. |
| 377 | ** URL arguments: |
| 378 | ** |
| 379 | ** uuid=ARTIFACT_ID REQUIRED |
| 380 | ** |
| 381 | ** and emits either: |
| 382 | ** |
| 383 | ** { error: "..." } |
| 384 | ** |
| 385 | ** with a non-200 response code or the artifact's manifest in JSON |
| 386 | ** form with a 200 response code. |
| 387 | */ |
| 388 | void ajax_route_artifact_json(void){ |
| 389 | const char *zUuid = P("uuid"); |
| 390 | Blob json = BLOB_INITIALIZER; |
| 391 | login_check_credentials(); |
| 392 | if( ! g.perm.Read ){ |
| 393 | ajax_route_error_forbidden(); |
| 394 | }else if( artifact_to_json_by_name(zUuid, &json) ){ |
| 395 | @ %b(&json) |
| 396 | }else{ |
| 397 | ajax_route_error_404("Cannot resolve artifact ID."); |
| 398 | } |
| 399 | blob_reset(&json); |
| 400 | } |
| 401 | |
| 402 | #if INTERFACE |
| 403 | /* |
| 404 | ** Internal mapping of ajax sub-route names to various metadata. |
| 405 | */ |
| @@ -351,11 +406,12 @@ | |
| 406 | struct AjaxRoute { |
| 407 | const char *zName; /* Name part of the route after "ajax/" */ |
| 408 | void (*xCallback)(); /* Impl function for the route. */ |
| 409 | int bWriteMode; /* True if requires write mode */ |
| 410 | int bPost; /* True if requires POST (i.e. CSRF |
| 411 | ** verification). Value is passed to |
| 412 | ** cgi_csrf_safe(). */ |
| 413 | }; |
| 414 | typedef struct AjaxRoute AjaxRoute; |
| 415 | #endif /*INTERFACE*/ |
| 416 | |
| 417 | /* |
| @@ -392,20 +448,26 @@ | |
| 448 | const char * zName = P("name"); |
| 449 | AjaxRoute routeName = {0,0,0,0}; |
| 450 | const AjaxRoute * pRoute = 0; |
| 451 | const AjaxRoute routes[] = { |
| 452 | /* Keep these sorted by zName (for bsearch()) */ |
| 453 | {"artifact.json", ajax_route_artifact_json, 0, 0}, |
| 454 | {"preview-text", ajax_route_preview_text, 0, 1 |
| 455 | /* Preview does not require write permissions in the repo. It |
| 456 | ** should arguably require write permissions simply to limit abuse |
| 457 | ** but doing means that /chat does not work without check-in |
| 458 | ** permissions: |
| 459 | ** |
| 460 | ** https://fossil-scm.org/forum/forumpost/ed4a762b3a557898 |
| 461 | ** |
| 462 | ** This particular route is used by /fileedit and /chat, whereas |
| 463 | ** /wikiedit uses a simpler wiki-specific route. |
| 464 | */ |
| 465 | /* TODO (2026-06-09): preview.txt, preview.md, preview.wiki as |
| 466 | ** shorthand for preview-text?filename=X.(txt|md|wiki), noting that |
| 467 | ** the filename is only used for mimetype determination. */ |
| 468 | } |
| 469 | }; |
| 470 | |
| 471 | if(zName==0 || zName[0]==0){ |
| 472 | ajax_route_error(400,"Missing required [route] 'name' parameter."); |
| 473 | return; |
| 474 |
+931
-109
| --- src/attach.c | ||
| +++ src/attach.c | ||
| @@ -22,41 +22,188 @@ | ||
| 22 | 22 | #include <assert.h> |
| 23 | 23 | |
| 24 | 24 | /* |
| 25 | 25 | ** Given a presumedly legal attachment target name, this guesses the |
| 26 | 26 | ** target type and returns one of CFTYPE_FORUM, CFTYPE_WIKI, |
| 27 | -** CFTYPE_TICKET, or CFTYPE_EVENT. Returns 0 if it cannot | |
| 28 | -** distinguish the target type. | |
| 27 | +** CFTYPE_TICKET, or CFTYPE_EVENT. Returns 0 if it cannot distinguish | |
| 28 | +** the target type. | |
| 29 | +** | |
| 30 | +** zTarget is an attachment target name: wiki page name, tech-note ID, | |
| 31 | +** ticket ID, or forumpost hash. | |
| 32 | +** | |
| 33 | +** If bFull is true then it requires zTarget to be a full ID for | |
| 34 | +** tech-notes and tickets, otherwise such IDs may be prefixes. If | |
| 35 | +** bFull is false then tech-notes and tickets will perform a prefix | |
| 36 | +** match, but it is up to the caller to provide enough of a prefix to | |
| 37 | +** rule out ambiguity[^1]. When called repeatedly, this routine can | |
| 38 | +** run a bit faster and more efficiently if bFull is true, but some | |
| 39 | +** historical use cases call for prefix matches. | |
| 40 | +** | |
| 41 | +** Wiki page names always require an exact match. | |
| 42 | +** | |
| 43 | +** Forum posts are a special case: | |
| 44 | +** | |
| 45 | +** - They ignore the bFull flag. That is, they will do prefix matches | |
| 46 | +** but will not match an ambiguous prefix. | |
| 47 | +** | |
| 48 | +** - It is up to the caller to, if needed, resolve zTarget using | |
| 49 | +** forumpost_head_rid2() to resolve the RID of the earliest version | |
| 50 | +** of the post, as that is the only one which attachments should | |
| 51 | +** target. | |
| 29 | 52 | ** |
| 30 | -** In the case of CFTYPE_FORUM, it is up to the caller to ensure that, | |
| 31 | -** if needed, they resolve zTarget using forumpost_head_rid2() so that | |
| 32 | -** they get the RID of the earliest version of the post, as that is | |
| 33 | -** the only one which attachments should target. | |
| 53 | +** [^1]: Historically (from the perspective of 2026-06) attachment | |
| 54 | +** target lookups have used GLOB prefix matching but have taken no | |
| 55 | +** measures to ensure that the prefix is unambiguous. Ergo we do the | |
| 56 | +** same here. It is assumed that the caller passes enough of a prefix | |
| 57 | +** to be unambiguous and that's worked out fine so far. | |
| 34 | 58 | */ |
| 35 | -int attachment_target_type(const char *zTarget){ | |
| 36 | - static Stmt q = empty_Stmt_m; | |
| 37 | - int rc = 0; | |
| 38 | - if( forumpost_head_rid2(zTarget)>0 ){ | |
| 59 | +int attachment_target_type(const char *zTarget, int bFull){ | |
| 60 | + if( !zTarget || !zTarget[0] || strlen(zTarget)>64/*vs. abuse*/ ){ | |
| 61 | + return 0; | |
| 62 | + } | |
| 63 | + if( symbolic_name_to_rid(zTarget, "f")>0 ){ | |
| 64 | + /* Check forum posts first because they are the most likely target | |
| 65 | + ** as of 2026. We should arguably use something more | |
| 66 | + ** specialized/efficient than symbolic_name_to_rid(). */ | |
| 39 | 67 | return CFTYPE_FORUM; |
| 40 | 68 | } |
| 41 | - if( !q.pStmt ){ | |
| 42 | - db_static_prepare( | |
| 43 | - &q, | |
| 69 | + if( bFull ){ | |
| 70 | + static Stmt q = empty_Stmt_m; | |
| 71 | + int rc = 0; | |
| 72 | + if( !q.pStmt ){ | |
| 73 | + db_static_prepare( | |
| 74 | + &q, | |
| 75 | + "SELECT CASE " | |
| 76 | + /* Ordered by presumed likelihood of attachments. */ | |
| 77 | + "WHEN (SELECT 1 FROM tag WHERE tagname='tkt-'||:tgt) THEN %d\n" | |
| 78 | + "WHEN (SELECT 1 FROM tag WHERE tagname='wiki-'||:tgt) THEN %d\n" | |
| 79 | + "WHEN (SELECT 1 FROM tag WHERE tagname='event-'||:tgt) THEN %d\n" | |
| 80 | + "ELSE 0 END", | |
| 81 | + CFTYPE_TICKET, CFTYPE_WIKI, CFTYPE_EVENT | |
| 82 | + ); | |
| 83 | + } | |
| 84 | + db_bind_text(&q, ":tgt", zTarget); | |
| 85 | + if( SQLITE_ROW==db_step(&q) ){ | |
| 86 | + rc = db_column_int(&q, 0); | |
| 87 | + } | |
| 88 | + db_reset(&q); | |
| 89 | + return rc; | |
| 90 | + }else{ | |
| 91 | + return db_int( | |
| 92 | + 0, | |
| 44 | 93 | "SELECT CASE " |
| 45 | - "WHEN 'tkt-'||:tgt IN (SELECT tagname FROM tag) THEN %d " | |
| 46 | - "WHEN 'event-'||:tgt IN (SELECT tagname FROM tag) THEN %d " | |
| 47 | - "WHEN 'wiki-'||:tgt IN (SELECT tagname FROM tag) THEN %d " | |
| 94 | + "WHEN (SELECT tagid FROM tag WHERE tagname GLOB 'tkt-%q*')" | |
| 95 | + " THEN %d\n" | |
| 96 | + "WHEN (SELECT tagid FROM tag WHERE tagname='wiki-%q')" | |
| 97 | + " THEN %d\n" | |
| 98 | + "WHEN (SELECT tagid FROM tag WHERE tagname GLOB 'event-%q*')" | |
| 99 | + " THEN %d\n" | |
| 48 | 100 | "ELSE 0 END", |
| 49 | - CFTYPE_TICKET, CFTYPE_EVENT, CFTYPE_WIKI | |
| 101 | + zTarget, CFTYPE_TICKET, | |
| 102 | + zTarget, CFTYPE_WIKI, | |
| 103 | + zTarget, CFTYPE_EVENT | |
| 50 | 104 | ); |
| 51 | 105 | } |
| 52 | - db_bind_text(&q, ":tgt", zTarget); | |
| 53 | - if( SQLITE_ROW==db_step(&q) ){ | |
| 54 | - rc = db_column_int(&q, 0); | |
| 106 | +} | |
| 107 | + | |
| 108 | +/* | |
| 109 | +** Given an attachment target name, returns the target's blob.rid. | |
| 110 | +** zTarget and bFull work as described for attachment_target_type(). | |
| 111 | +** | |
| 112 | +** For forum posts, this always returns the RID of the first version | |
| 113 | +** of the post, as attachments should always target that instance. | |
| 114 | +*/ | |
| 115 | +int attachment_target_rid(const char *zTarget, int bFull){ | |
| 116 | + int rid = 0; | |
| 117 | + const int eType = attachment_target_type(zTarget, bFull); | |
| 118 | + switch(eType){ | |
| 119 | + case CFTYPE_TICKET: | |
| 120 | + case CFTYPE_EVENT:{ | |
| 121 | + const char *zTagPrefix = (eType==CFTYPE_EVENT) ? "event" : "tkt"; | |
| 122 | + rid = db_int( | |
| 123 | + 0, "SELECT b.rid FROM blob b, tag t, tagxref x\n" | |
| 124 | + "WHERE tagname %s '%s-%q%s'\n" | |
| 125 | + "AND x.tagtype>0\n" | |
| 126 | + "AND x.tagid=t.tagid\n" | |
| 127 | + "AND x.rid=b.rid\n" | |
| 128 | + "ORDER BY x.mtime DESC", | |
| 129 | + bFull ? "=" : "GLOB"/*safe-for-%s*/, | |
| 130 | + zTagPrefix/*safe-for-%s*/, | |
| 131 | + zTarget, | |
| 132 | + bFull ? "" : "*"/*safe-for-%s*/ | |
| 133 | + ); | |
| 134 | + break; | |
| 135 | + } | |
| 136 | + case CFTYPE_FORUM: | |
| 137 | + rid = db_int( | |
| 138 | + 0, "SELECT f.fpid FROM forumpost f, blob b\n" | |
| 139 | + "WHERE f.fpid=b.rid\n" | |
| 140 | + "AND b.uuid %s '%q%s'", | |
| 141 | + bFull ? "=" : "GLOB"/*safe-for-%s*/, | |
| 142 | + zTarget, | |
| 143 | + bFull ? "" : "*"/*safe-for-%s*/ | |
| 144 | + ); | |
| 145 | + if( rid>0 ){ | |
| 146 | + rid = forumpost_head_rid(rid); | |
| 147 | + } | |
| 148 | + break; | |
| 149 | + case CFTYPE_WIKI: | |
| 150 | + rid = db_int( | |
| 151 | + 0, "SELECT b.rid FROM blob b, tag t, tagxref x\n" | |
| 152 | + "WHERE tagname='wiki-%q'\n" | |
| 153 | + "AND x.tagtype>0\n" | |
| 154 | + "AND x.tagid=t.tagid\n" | |
| 155 | + "AND x.rid=b.rid\n" | |
| 156 | + "ORDER BY x.mtime DESC", | |
| 157 | + zTarget | |
| 158 | + ); | |
| 159 | + break; | |
| 160 | + default: | |
| 161 | + break; | |
| 162 | + } | |
| 163 | + return rid; | |
| 164 | +} | |
| 165 | + | |
| 166 | +/* | |
| 167 | +** For a given aritfact ID and type (from the CFTYPE_xyz enum), | |
| 168 | +** returns true if the current user could hypothetically apply and | |
| 169 | +** attachment to it, else returns 0. | |
| 170 | +** | |
| 171 | +** The rid is currently only relevant when eArtifactType is | |
| 172 | +** CFTYPE_FORUM. For forum posts, it checks precisely the rid given, | |
| 173 | +** not the head RID, to keep non-admins from attaching files to | |
| 174 | +** threads which have since been taken over by another user (this | |
| 175 | +** happens when an admin edits another user's post). | |
| 176 | +*/ | |
| 177 | +int attach_user_may(int rid, int eArtifactType){ | |
| 178 | + if( g.perm.Admin ) return 1; | |
| 179 | + if( !login_is_individual() ) return 0; | |
| 180 | + switch(eArtifactType){ | |
| 181 | + case CFTYPE_FORUM: | |
| 182 | + return g.perm.AttachForum && forumpost_is_owner(rid, 0); | |
| 183 | + case CFTYPE_WIKI: | |
| 184 | + return g.perm.ApndWiki && g.perm.Attach; | |
| 185 | + case CFTYPE_TICKET: | |
| 186 | + return g.perm.ApndTkt && g.perm.Attach; | |
| 187 | + case CFTYPE_EVENT: | |
| 188 | + return g.perm.Write && g.perm.ApndWiki && g.perm.Attach; | |
| 189 | + default: | |
| 190 | + return 0; | |
| 55 | 191 | } |
| 56 | - db_reset(&q); | |
| 57 | - return rc; | |
| 192 | +} | |
| 193 | + | |
| 194 | +/* | |
| 195 | +** Emits a single-button FORM which invokes | |
| 196 | +** /attachadd with target=$zTarget. | |
| 197 | +*/ | |
| 198 | +void attach_render_attachadd_button(const char *zTarget){ | |
| 199 | + /* This could be changed from POST to GET, and arguably should so | |
| 200 | + ** that the target=X part becomes part of the resulting URL. */ | |
| 201 | + @ <form method="post" action="%R/attachadd">\ | |
| 202 | + @ <input type="hidden" name="target" value="%T(zTarget)">\ | |
| 203 | + @ <input type="submit" value="Attach..."> | |
| 204 | + @ </form>\ | |
| 58 | 205 | } |
| 59 | 206 | |
| 60 | 207 | /* |
| 61 | 208 | ** WEBPAGE: attachlist |
| 62 | 209 | ** List attachments. |
| @@ -69,20 +216,21 @@ | ||
| 69 | 216 | ** At most one of technote=, tkt=, forumpost=, or page= may be supplied. |
| 70 | 217 | ** |
| 71 | 218 | ** If none are given, all attachments are listed. If one is given, only |
| 72 | 219 | ** attachments for the designated technote, ticket or wiki page are shown. |
| 73 | 220 | ** |
| 74 | -** HASH may be just a prefix of the relevant technical note or ticket | |
| 75 | -** artifact hash, in which case all attachments of all technical notes or | |
| 76 | -** tickets with the prefix will be listed. Forum posts, on the other hand, | |
| 77 | -** require a unique hash prefix. | |
| 221 | +** HASH may be just a prefix of the relevant forum post, technical | |
| 222 | +** note, or ticket artifact hash, in which case all attachments of all | |
| 223 | +** technical notes or tickets with the prefix will be listed. Forum | |
| 224 | +** posts, on the other hand, require a unique hash or hash prefix. | |
| 78 | 225 | */ |
| 79 | 226 | void attachlist_page(void){ |
| 80 | 227 | const char *zPage = P("page"); |
| 81 | 228 | const char *zTkt = P("tkt"); |
| 82 | 229 | const char *zTechNote = P("technote"); |
| 83 | 230 | const char *zForumPost = P("forumpost"); |
| 231 | + char *zLink = 0; | |
| 84 | 232 | Blob sql; |
| 85 | 233 | Stmt q; |
| 86 | 234 | |
| 87 | 235 | if( zPage && zTkt ) zTkt = 0; |
| 88 | 236 | login_check_credentials(); |
| @@ -102,32 +250,47 @@ | ||
| 102 | 250 | if( fnid<=0 ){ |
| 103 | 251 | webpage_error("Invalid forum post ID: %h", zForumPost); |
| 104 | 252 | } |
| 105 | 253 | blob_append_sql(&sql, " WHERE target=" |
| 106 | 254 | "(SELECT uuid FROM blob WHERE rid=%d)", fnid); |
| 255 | + zLink = mprintf("forum post <a href='%R/forumpost/%t'>%#h</a>", | |
| 256 | + zForumPost, hash_digits(0), zForumPost); | |
| 107 | 257 | }else if( zPage ){ |
| 108 | 258 | if( g.perm.RdWiki==0 ){ login_needed(g.anon.RdWiki); return; } |
| 109 | 259 | style_header("Attachments To Wiki page %h", zPage); |
| 110 | 260 | blob_append_sql(&sql, " WHERE target=%Q", zPage); |
| 261 | + zLink = mprintf("wiki page <a href='%R/wiki?name=%t'>%h</a>", | |
| 262 | + zPage, zPage); | |
| 111 | 263 | }else if( zTkt ){ |
| 112 | 264 | if( g.perm.RdTkt==0 ){ login_needed(g.anon.RdTkt); return; } |
| 113 | 265 | style_header("Attachments To Ticket %S", zTkt); |
| 114 | 266 | blob_append_sql(&sql, " WHERE target GLOB '%q*'", zTkt); |
| 267 | + zLink = mprintf("ticket <a href='%R/tktview?name=%t'>%#h</a>", | |
| 268 | + zTkt, hash_digits(0), zTkt); | |
| 115 | 269 | }else if( zTechNote ){ |
| 116 | 270 | if( g.perm.RdWiki==0 ){ login_needed(g.anon.RdWiki); return; } |
| 117 | 271 | style_header("Attachments To Tech Note %S", zTechNote); |
| 118 | 272 | blob_append_sql(&sql, " WHERE target GLOB '%q*'", |
| 119 | 273 | zTechNote); |
| 274 | + zLink = mprintf("tech-note <a href='%R/technote?name=%t'>%#h</a>", | |
| 275 | + zTechNote, hash_digits(0), zTechNote); | |
| 120 | 276 | }else{ |
| 121 | 277 | if( g.perm.RdTkt==0 && g.perm.RdWiki==0 ){ |
| 122 | 278 | login_needed(g.anon.RdTkt || g.anon.RdWiki); |
| 123 | 279 | return; |
| 124 | 280 | } |
| 125 | 281 | style_header("All Attachments"); |
| 126 | 282 | } |
| 127 | 283 | blob_append_sql(&sql, " ORDER BY mtime DESC"); |
| 128 | 284 | db_prepare(&q, "%s", blob_sql_text(&sql)); |
| 285 | + | |
| 286 | + if( zLink ){ | |
| 287 | + @ <h2>Attachments for %s(zLink)</h2> | |
| 288 | + fossil_free(zLink); | |
| 289 | + zLink = 0; | |
| 290 | + } | |
| 291 | + | |
| 129 | 292 | @ <ol> |
| 130 | 293 | while( db_step(&q)==SQLITE_ROW ){ |
| 131 | 294 | const char *zDate; |
| 132 | 295 | const char *zSrc; |
| 133 | 296 | const char *zTarget; |
| @@ -137,10 +300,11 @@ | ||
| 137 | 300 | const char *zUuid; |
| 138 | 301 | const char *zDispUser; |
| 139 | 302 | const int attachid = db_column_int(&q, 7); |
| 140 | 303 | int type; |
| 141 | 304 | int i; |
| 305 | + int bDeleted; | |
| 142 | 306 | char *zUrlTail = 0; |
| 143 | 307 | |
| 144 | 308 | if( moderation_pending(attachid) |
| 145 | 309 | && !moderation_user_could(attachid, 1, 0) ){ |
| 146 | 310 | /* Elide entries which are currently pending moderation unless |
| @@ -160,11 +324,12 @@ | ||
| 160 | 324 | if( zFilename[i]=='/' && zFilename[i+1]!=0 ){ |
| 161 | 325 | zFilename = &zFilename[i+1]; |
| 162 | 326 | i = -1; |
| 163 | 327 | } |
| 164 | 328 | } |
| 165 | - type = attachment_target_type(zTarget); | |
| 329 | + bDeleted = 0==zSrc || 0==zSrc[0]; | |
| 330 | + type = attachment_target_type(zTarget, 1); | |
| 166 | 331 | switch( type ){ |
| 167 | 332 | case CFTYPE_TICKET: |
| 168 | 333 | zUrlTail = mprintf("tkt=%s&file=%t", zTarget, zFilename); |
| 169 | 334 | break; |
| 170 | 335 | case CFTYPE_EVENT: |
| @@ -176,20 +341,32 @@ | ||
| 176 | 341 | case CFTYPE_WIKI: |
| 177 | 342 | zUrlTail = mprintf("page=%t&file=%t", zTarget, zFilename); |
| 178 | 343 | break; |
| 179 | 344 | } |
| 180 | 345 | @ <li><p> |
| 181 | - @ Attachment %z(href("%R/ainfo/%!S",zUuid))%S(zUuid)</a> | |
| 346 | + if( bDeleted ){ | |
| 347 | + @ <s>\ | |
| 348 | + } | |
| 349 | + @ Attachment %z(href("%R/ainfo/%!S",zUuid))%S(zUuid)</a>\ | |
| 182 | 350 | moderation_pending_www(attachid); |
| 183 | - @ <br><a href="%R/attachview?%s(zUrlTail)">%h(zFilename)</a> | |
| 184 | - @ [<a href="%R/attachdownload/%t(zFilename)?%s(zUrlTail)">download</a>]<br> | |
| 351 | + @ <br>\ | |
| 352 | + @ <a href="%R/attachview?%s(zUrlTail)">%h(zFilename)</a> | |
| 353 | + @ [<a href="%R/attachdownload/%t(zFilename)?%s(zUrlTail)">download</a>]\ | |
| 354 | + if( bDeleted ){ | |
| 355 | + @ </s> | |
| 356 | + } | |
| 357 | + @ <br> | |
| 185 | 358 | if( zComment ) while( fossil_isspace(zComment[0]) ) zComment++; |
| 186 | 359 | if( zComment && zComment[0] ){ |
| 187 | - @ %!W(zComment)<br> | |
| 360 | + /* FIXME (2026-06-05): Honor the N-card (comment mimetype). %W | |
| 361 | + ** (historically used here) assumes fossil-wiki and the | |
| 362 | + ** fileformat.wiki doc has always claimed that it defaults to | |
| 363 | + ** text/plain. /ainfo assumes it is plain text. */ | |
| 364 | + @ %h(zComment)<br> | |
| 188 | 365 | } |
| 189 | 366 | if( zForumPost==0 && zPage==0 && zTkt==0 && zTechNote==0 ){ |
| 190 | - if( zSrc==0 || zSrc[0]==0 ){ | |
| 367 | + if( bDeleted ){ | |
| 191 | 368 | zSrc = "Deleted from"; |
| 192 | 369 | }else { |
| 193 | 370 | zSrc = "Added to"; |
| 194 | 371 | } |
| 195 | 372 | switch( type ){ |
| @@ -362,41 +539,81 @@ | ||
| 362 | 539 | Manifest *pManifest; |
| 363 | 540 | |
| 364 | 541 | db_begin_transaction(); |
| 365 | 542 | blob_init(&content, aContent, szContent); |
| 366 | 543 | pManifest = manifest_parse(&content, 0, 0); |
| 544 | + addCompress = pManifest!=0; | |
| 367 | 545 | manifest_destroy(pManifest); |
| 368 | 546 | blob_init(&content, aContent, szContent); |
| 369 | - if( pManifest ){ | |
| 547 | + if( addCompress ){ | |
| 370 | 548 | blob_compress(&content, &content); |
| 371 | - addCompress = 1; | |
| 372 | 549 | } |
| 373 | 550 | rid = content_put_ex(&content, 0, 0, 0, needModerator); |
| 374 | - zUUID = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid); | |
| 551 | + zUUID = rid_to_uuid(rid); | |
| 375 | 552 | blob_zero(&manifest); |
| 376 | 553 | for(i=n=0; zName[i]; i++){ |
| 377 | 554 | if( zName[i]=='/' || zName[i]=='\\' ) n = i+1; |
| 378 | 555 | } |
| 379 | 556 | zName += n; |
| 380 | 557 | if( zName[0]==0 ) zName = "unknown"; |
| 381 | 558 | blob_appendf(&manifest, "A %F%s %F %s\n", |
| 382 | 559 | zName, addCompress ? ".gz" : "", zTarget, zUUID); |
| 383 | - while( fossil_isspace(zComment[0]) ) zComment++; | |
| 384 | - n = strlen(zComment); | |
| 385 | - while( n>0 && fossil_isspace(zComment[n-1]) ){ n--; } | |
| 386 | - if( n>0 ){ | |
| 387 | - blob_appendf(&manifest, "C %#F\n", n, zComment); | |
| 560 | + if( zComment!=0 && zComment[0]!=0 ){ | |
| 561 | + while( fossil_isspace(zComment[0]) ) zComment++; | |
| 562 | + n = strlen(zComment); | |
| 563 | + while( n>0 && fossil_isspace(zComment[n-1]) ){ n--; } | |
| 564 | + if( n>0 ){ | |
| 565 | + blob_appendf(&manifest, "C %#F\n", n, zComment); | |
| 566 | + } | |
| 388 | 567 | } |
| 389 | 568 | zDate = date_in_standard_format("now"); |
| 390 | - blob_appendf(&manifest, "D %s\n", zDate); | |
| 569 | + blob_appendf(&manifest, "D %z\n", zDate); | |
| 391 | 570 | blob_appendf(&manifest, "U %F\n", login_name()); |
| 392 | 571 | md5sum_blob(&manifest, &cksum); |
| 393 | 572 | blob_appendf(&manifest, "Z %b\n", &cksum); |
| 394 | 573 | attach_put(&manifest, rid, needModerator); |
| 395 | 574 | assert( blob_is_reset(&manifest) ); |
| 396 | 575 | db_end_transaction(0); |
| 397 | 576 | } |
| 577 | + | |
| 578 | +/* | |
| 579 | +** Renders the "legacy" (static) /attachadd form. One of the first | |
| 580 | +** four arguments must be non-NULL and the other three must be NULL. | |
| 581 | +** zComment may be NULL, as may zFrom. See the call sites for more | |
| 582 | +** context. | |
| 583 | +*/ | |
| 584 | +static void attach_render_legacy_form(const char *zForumPost, | |
| 585 | + const char *zTechNote, | |
| 586 | + const char *zTicket, | |
| 587 | + const char *zWikiPage, | |
| 588 | + const char *zComment, | |
| 589 | + const char *zFrom){ | |
| 590 | + form_begin("enctype='multipart/form-data' id='attachadd-legacy-form'", | |
| 591 | + "%R/attachadd"); | |
| 592 | + @ <div>\ | |
| 593 | + @ File to Attach: | |
| 594 | + @ <input type="file" name="f" size="60"><br> | |
| 595 | + @ Description:<br> | |
| 596 | + @ <textarea name="comment" cols="80" rows="5" wrap="virtual"\ | |
| 597 | + @ >%h(zComment)</textarea><br> | |
| 598 | + if( zForumPost ){ | |
| 599 | + @ <input type="hidden" name="forumpost" value="%h(zForumPost)">\ | |
| 600 | + }else if( zTicket ){ | |
| 601 | + @ <input type="hidden" name="tkt" value="%h(zTicket)">\ | |
| 602 | + }else if( zTechNote ){ | |
| 603 | + @ <input type="hidden" name="technote" value="%h(zTechNote)">\ | |
| 604 | + }else if( zWikiPage ){ | |
| 605 | + @ <input type="hidden" name="page" value="%h(zWikiPage)">\ | |
| 606 | + } | |
| 607 | + @ <input type="hidden" name="from" value="%h(zFrom)">\ | |
| 608 | + @ <input type="submit" name="ok" value="Add Attachment">\ | |
| 609 | + @ <input type="submit" name="cancel" value="Cancel">\ | |
| 610 | + @ </div> | |
| 611 | + captcha_generate(0); | |
| 612 | + login_insert_csrf_secret(); | |
| 613 | + @ </form> | |
| 614 | +} | |
| 398 | 615 | |
| 399 | 616 | /* |
| 400 | 617 | ** WEBPAGE: attachadd |
| 401 | 618 | ** Add a new attachment. |
| 402 | 619 | ** |
| @@ -404,33 +621,54 @@ | ||
| 404 | 621 | ** page=WIKIPAGE |
| 405 | 622 | ** technote=HASH |
| 406 | 623 | ** forumpost=HASH |
| 407 | 624 | ** from=URL |
| 408 | 625 | ** |
| 626 | +** Adds a POSTed file attachment to the given target. | |
| 627 | +** | |
| 628 | +** Or the "version 2" interface: | |
| 629 | +** | |
| 630 | +** target=ATTACHMENT_TARGET | |
| 631 | +** | |
| 632 | +** Behaves as documented for attachaddV2_page(). | |
| 409 | 633 | */ |
| 410 | 634 | void attachadd_page(void){ |
| 411 | - const char *zPage = P("page"); | |
| 412 | - const char *zForumPost = P("forumpost"); | |
| 413 | - const char *zTkt = P("tkt"); | |
| 414 | - const char *zTechNote = P("technote"); | |
| 415 | - const char *zFrom = P("from"); | |
| 416 | - const char *aContent = P("f"); | |
| 417 | - const char *zName = PD("f:filename","unknown"); | |
| 418 | - const char *zComment = PD("comment", ""); | |
| 635 | + const char *zPage; | |
| 636 | + const char *zForumPost; | |
| 637 | + const char *zTkt; | |
| 638 | + const char *zTechNote; | |
| 639 | + const char *aContent; | |
| 640 | + const char *zName; | |
| 641 | + const char *zComment; | |
| 419 | 642 | const char *zTarget; |
| 420 | - char * zTo = 0; | |
| 643 | + const char *zFrom; /* Origin page - redirect here after saving */ | |
| 644 | + char *zTo = 0; /* Optionally redirect here after saving */ | |
| 421 | 645 | char *zTargetType = 0; |
| 422 | 646 | char *zExtraFree = 0; |
| 423 | - int szContent = atoi(PD("f:bytes","0")); | |
| 647 | + int szContent; | |
| 424 | 648 | int goodCaptcha = 1; |
| 425 | 649 | int szLimit = 0; |
| 426 | 650 | |
| 651 | + if( P("target")!=0 ){ | |
| 652 | + attachaddV2_page(); | |
| 653 | + return; | |
| 654 | + } | |
| 655 | + zPage = P("page"); | |
| 656 | + zForumPost = P("forumpost"); | |
| 657 | + zTkt = P("tkt"); | |
| 658 | + zTechNote = P("technote"); | |
| 659 | + zFrom = P("from"); | |
| 660 | + aContent = P("f"); | |
| 661 | + zName = PD("f:filename","unknown"); | |
| 662 | + zComment = PD("comment", ""); | |
| 663 | + szContent = atoi(PD("f:bytes","0")); | |
| 664 | + | |
| 427 | 665 | if( zFrom==0 ) zFrom = mprintf("%R/home"); |
| 428 | 666 | if( P("cancel") ) cgi_redirect(zFrom); |
| 429 | 667 | if( (!!zPage + !!zTkt + !!zTechNote + !!zForumPost)!=1 ){ |
| 430 | 668 | webpage_error("Requires exactly one one: page=X, tkt=X, forumpost=X," |
| 431 | - " or technote=X"); | |
| 669 | + " technote=X, or target=X"); | |
| 432 | 670 | } |
| 433 | 671 | login_check_credentials(); |
| 434 | 672 | if( zForumPost ){ |
| 435 | 673 | int fpid; |
| 436 | 674 | if( g.perm.AttachForum==0 ){ |
| @@ -445,26 +683,23 @@ | ||
| 445 | 683 | "forum posts."); |
| 446 | 684 | } |
| 447 | 685 | zTarget = zExtraFree = rid_to_uuid(fpid); |
| 448 | 686 | zTargetType = mprintf("Forum post <a href=\"%R/forumpost/%S\">%h</a>", |
| 449 | 687 | zTarget, zForumPost); |
| 450 | - zTo = 1 | |
| 451 | - ? mprintf("%R/forumpost/%S", zTarget) | |
| 452 | - : mprintf("%R/attachview?forumpost=%T&file=%T", | |
| 453 | - zTarget, zName) | |
| 454 | - /* Or we could return directly to the forum post. */; | |
| 688 | + zTo = zFrom ? 0 : mprintf("%R/forumpost/%S", zTarget); | |
| 455 | 689 | }else if( zPage ){ |
| 456 | 690 | if( g.perm.ApndWiki==0 || g.perm.Attach==0 ){ |
| 457 | 691 | login_needed(g.anon.ApndWiki && g.anon.Attach); |
| 458 | 692 | return; |
| 459 | 693 | } |
| 460 | 694 | if( !db_exists("SELECT 1 FROM tag WHERE tagname='wiki-%q'", zPage) ){ |
| 461 | 695 | fossil_redirect_home(); |
| 462 | 696 | } |
| 463 | 697 | zTarget = zPage; |
| 464 | - zTargetType = mprintf("Wiki Page <a href=\"%R/wiki?name=%h\">%h</a>", | |
| 698 | + zTargetType = mprintf("Wiki Page <a href=\"%R/wiki?name=%t\">%h</a>", | |
| 465 | 699 | zPage, zPage); |
| 700 | + zTo = zFrom ? 0 : mprintf("%R/wiki?name=%T", zTarget); | |
| 466 | 701 | }else if ( zTechNote ){ |
| 467 | 702 | if( g.perm.Write==0 || g.perm.ApndWiki==0 || g.perm.Attach==0 ){ |
| 468 | 703 | login_needed(g.anon.Write && g.anon.ApndWiki && g.anon.Attach); |
| 469 | 704 | return; |
| 470 | 705 | } |
| @@ -474,11 +709,11 @@ | ||
| 474 | 709 | if( zTechNote==0) fossil_redirect_home(); |
| 475 | 710 | } |
| 476 | 711 | zTarget = zTechNote; |
| 477 | 712 | zTargetType = mprintf("Tech Note <a href=\"%R/technote/%s\">%S</a>", |
| 478 | 713 | zTechNote, zTechNote); |
| 479 | - | |
| 714 | + zTo = zFrom ? 0 : mprintf("%R/technote/%S", zTarget); | |
| 480 | 715 | }else{ |
| 481 | 716 | assert( zTkt ); |
| 482 | 717 | if( g.perm.ApndTkt==0 || g.perm.Attach==0 ){ |
| 483 | 718 | login_needed(g.anon.ApndTkt && g.anon.Attach); |
| 484 | 719 | return; |
| @@ -487,21 +722,25 @@ | ||
| 487 | 722 | zTkt = db_text(0, "SELECT substr(tagname,5) FROM tag" |
| 488 | 723 | " WHERE tagname GLOB 'tkt-%q*'", zTkt); |
| 489 | 724 | if( zTkt==0 ) fossil_redirect_home(); |
| 490 | 725 | } |
| 491 | 726 | zTarget = zTkt; |
| 492 | - zTargetType = mprintf("Ticket <a href=\"%R/tktview/%s\">%S</a>", | |
| 727 | + zTargetType = mprintf("Ticket <a href=\"%R/tktview/%S\">%S</a>", | |
| 493 | 728 | zTkt, zTkt); |
| 729 | + zTo = zFrom ? 0 : mprintf("%R/tktview/%S", zTarget); | |
| 494 | 730 | } |
| 495 | 731 | szLimit = db_get_int("attachment-size-limit", 0); |
| 496 | 732 | if( szContent<0 || (szLimit && szContent>szLimit) ){ |
| 497 | 733 | /* This check must be done late so that zTargetType is set up. */ |
| 498 | 734 | @ <p class="generalError">Attachment %h(zName) is too large. |
| 499 | 735 | @ <a href="%R/help/attachment-size-limit">Limit</a> is |
| 500 | 736 | @ %d(szLimit ? szLimit : 0x7fffffff) bytes</p> |
| 501 | 737 | /* Fall through and render form. */ |
| 502 | - }else if( P("ok") && szContent>0 && (goodCaptcha = captcha_is_correct(0)) ){ | |
| 738 | + }else if( P("ok") | |
| 739 | + && cgi_csrf_safe(2) | |
| 740 | + && szContent>0 | |
| 741 | + && (goodCaptcha = captcha_is_correct(0)) ){ | |
| 503 | 742 | int needModerator = (zForumPost!=0 && forum_need_moderation()) || |
| 504 | 743 | (zTkt!=0 && ticket_need_moderation(0)) || |
| 505 | 744 | (zPage!=0 && wiki_need_moderation(0)); |
| 506 | 745 | attach_commit(zName, zTarget, aContent, szContent, needModerator, zComment); |
| 507 | 746 | cgi_redirect(zTo ? zTo : zFrom); |
| @@ -511,35 +750,374 @@ | ||
| 511 | 750 | style_header("Add Attachment"); |
| 512 | 751 | if( !goodCaptcha ){ |
| 513 | 752 | @ <p class="generalError">Error: Incorrect security code.</p> |
| 514 | 753 | } |
| 515 | 754 | @ <h2>Add Attachment To %s(zTargetType)</h2> |
| 516 | - form_begin("enctype='multipart/form-data'", "%R/attachadd"); | |
| 517 | - @ <div> | |
| 518 | - @ File to Attach: | |
| 519 | - @ <input type="file" name="f" size="60"><br> | |
| 520 | - @ Description:<br> | |
| 521 | - @ <textarea name="comment" cols="80" rows="5" wrap="virtual"\ | |
| 522 | - @ >%h(zComment)</textarea><br> | |
| 523 | - if( zForumPost ){ | |
| 524 | - @ <input type="hidden" name="forumpost" value="%h(zTarget)"> | |
| 525 | - }else if( zTkt ){ | |
| 526 | - @ <input type="hidden" name="tkt" value="%h(zTkt)"> | |
| 527 | - }else if( zTechNote ){ | |
| 528 | - @ <input type="hidden" name="technote" value="%h(zTechNote)"> | |
| 755 | + attach_render_legacy_form(zForumPost, zTechNote, zTkt, zPage, | |
| 756 | + zComment, zFrom); | |
| 757 | + builtin_fossil_js_bundle_or("attach", NULL); | |
| 758 | + style_finish_page(); | |
| 759 | + fossil_free(zTargetType); | |
| 760 | + fossil_free(zExtraFree); | |
| 761 | +} | |
| 762 | + | |
| 763 | +/* | |
| 764 | +** WEBPAGE: attachadd_ajax_post hidden | |
| 765 | +** | |
| 766 | +** Used by attachadd V2 to handle attachments via POST requests with: | |
| 767 | +** | |
| 768 | +** target=ATTACHMENT_TARGET | |
| 769 | +** file1..fileN=FILE_OBJECTS | |
| 770 | +** dryrun=0|1 | |
| 771 | +** | |
| 772 | +** Each posted file in the set file1..fileN gets attached to the given | |
| 773 | +** target, permissions permitting. If dryrun>0 then the change is | |
| 774 | +** rolled back instead of committed. target=X must refer to a full | |
| 775 | +** target ID, not a prefix. | |
| 776 | +** | |
| 777 | +** Responds with JSON: an empty object on success and | |
| 778 | +** {error:"message"} on error. The on-success response structure is | |
| 779 | +** subject to amendment. | |
| 780 | +*/ | |
| 781 | +void attachadd_ajax_post(void){ | |
| 782 | + const char *zTarget; | |
| 783 | + char *zExtraFree = 0; | |
| 784 | + int eTgtType = 0; | |
| 785 | + int bNeedsModeration = 0; | |
| 786 | + int goodCaptcha = 1; | |
| 787 | + int bRollback = 0; /* Roll back if true. */ | |
| 788 | + | |
| 789 | + if( ! ajax_route_bootstrap(0, 1) ){ | |
| 790 | + return; | |
| 791 | + }else if( !(goodCaptcha = captcha_is_correct(0)) ){ | |
| 792 | + goto ajax_err_403; | |
| 793 | + }else if( !ajax_check_csrf(2) ){ | |
| 794 | + return; | |
| 795 | + } | |
| 796 | + db_begin_transaction(); | |
| 797 | + zTarget = P("target"); | |
| 798 | + eTgtType = attachment_target_type(zTarget, 1); | |
| 799 | + CX("{"); | |
| 800 | + switch( eTgtType ){ | |
| 801 | + default: | |
| 802 | + case 0: | |
| 803 | + ajax_route_error(400, "Invalid attachment target."); | |
| 804 | + db_rollback_transaction(); | |
| 805 | + return; | |
| 806 | + case CFTYPE_FORUM:{ | |
| 807 | + int fpid; | |
| 808 | + if( g.perm.AttachForum==0 ){ | |
| 809 | + goto ajax_err_403; | |
| 810 | + } | |
| 811 | + fpid = forumpost_head_rid2(zTarget); | |
| 812 | + if( fpid<=0 ){ | |
| 813 | + goto ajax_err_404; | |
| 814 | + }else if( !g.perm.Admin && !forumpost_is_owner(fpid, 0) ){ | |
| 815 | + ajax_route_error(403, "Only admins can attach files to " | |
| 816 | + "other users' forum posts."); | |
| 817 | + db_rollback_transaction(); | |
| 818 | + return; | |
| 819 | + } | |
| 820 | + zTarget = zExtraFree = rid_to_uuid(fpid); | |
| 821 | + bNeedsModeration = forum_need_moderation(); | |
| 822 | + break; | |
| 823 | + } | |
| 824 | + case CFTYPE_EVENT:{ | |
| 825 | + if( g.perm.Write==0 || g.perm.ApndWiki==0 || g.perm.Attach==0 ){ | |
| 826 | + goto ajax_err_403; | |
| 827 | + } | |
| 828 | + if( !db_exists("SELECT 1 FROM tag WHERE tagname='event-%q'", | |
| 829 | + zTarget) ){ | |
| 830 | + zTarget = zExtraFree = | |
| 831 | + db_text(0, "SELECT substr(tagname,7) FROM tag" | |
| 832 | + " WHERE tagname GLOB 'event-%q*'", zTarget); | |
| 833 | + if( zTarget==0){ | |
| 834 | + goto ajax_err_404; | |
| 835 | + } | |
| 836 | + } | |
| 837 | + bNeedsModeration = 0; | |
| 838 | + break; | |
| 839 | + } | |
| 840 | + case CFTYPE_TICKET:{ | |
| 841 | + if( g.perm.ApndTkt==0 || g.perm.Attach==0 ){ | |
| 842 | + goto ajax_err_403; | |
| 843 | + } | |
| 844 | + if( !db_exists("SELECT 1 FROM tag WHERE tagname='tkt-%q'", | |
| 845 | + zTarget) ){ | |
| 846 | + zTarget = db_text(0, "SELECT substr(tagname,5) FROM tag" | |
| 847 | + " WHERE tagname GLOB 'tkt-%q*'", zTarget); | |
| 848 | + if( zTarget==0 ){ | |
| 849 | + goto ajax_err_404; | |
| 850 | + } | |
| 851 | + } | |
| 852 | + bNeedsModeration = ticket_need_moderation(0); | |
| 853 | + break; | |
| 854 | + } | |
| 855 | + case CFTYPE_WIKI:{ | |
| 856 | + if( g.perm.ApndWiki==0 || g.perm.Attach==0 ){ | |
| 857 | + goto ajax_err_403; | |
| 858 | + } | |
| 859 | + if( !db_exists("SELECT 1 FROM tag WHERE tagname='wiki-%q'", | |
| 860 | + zTarget) ){ | |
| 861 | + goto ajax_err_404; | |
| 862 | + } | |
| 863 | + bNeedsModeration = wiki_need_moderation(0); | |
| 864 | + break; | |
| 865 | + } | |
| 866 | + } | |
| 867 | + | |
| 868 | + if( attachments_ajax_from_POST(zTarget, bNeedsModeration)>=0 ){ | |
| 869 | + CX("}"); | |
| 870 | + if( atoi(PD("dryrun","0"))>0 ){ | |
| 871 | + bRollback = 1; | |
| 872 | + } | |
| 873 | + }/*else error response was set up*/ | |
| 874 | + fossil_free(zExtraFree); | |
| 875 | + db_end_transaction(bRollback); | |
| 876 | + return; | |
| 877 | +ajax_err_403: | |
| 878 | + if( db_transaction_nesting_depth()>0 ){ | |
| 879 | + db_rollback_transaction(); | |
| 880 | + } | |
| 881 | + ajax_route_error_forbidden(); | |
| 882 | + return; | |
| 883 | +ajax_err_404: | |
| 884 | + assert( db_transaction_nesting_depth()>0 ); | |
| 885 | + db_rollback_transaction(); | |
| 886 | + ajax_route_error(404, "Target not found."); | |
| 887 | + return; | |
| 888 | +} | |
| 889 | + | |
| 890 | +/* | |
| 891 | +** A helper for AJAX-style routines which accept file attachments via | |
| 892 | +** POST. zTarget must be a full attachment target. bNeedsModeration | |
| 893 | +** must be true if the attachment requires moderation. | |
| 894 | +** | |
| 895 | +** It is up to the caller to have validated all security measures | |
| 896 | +** before calling this. | |
| 897 | +** | |
| 898 | +** This looks for POSTed files names "file1".."fileN", stopping when | |
| 899 | +** it finds no entry. Returns the number of entries attached to the | |
| 900 | +** target or a negative value on error (in which case the current db | |
| 901 | +** transaction will be in a rollback state). | |
| 902 | +** | |
| 903 | +** The only errors are currently attachment size limit violations: | |
| 904 | +** attachments must have a non-0 size and if the attachment-size-limit | |
| 905 | +** setting is >0 then each file's size must be <= that. | |
| 906 | +** | |
| 907 | +** If this returns a negative value, it will have populated an error | |
| 908 | +** response using ajax_route_error(). On success it produces no | |
| 909 | +** output. | |
| 910 | +** | |
| 911 | +** ACHTUNG: if zTarget is a forum post, it "really should" be the ID | |
| 912 | +** of the first version of that post, as that's where attachments are | |
| 913 | +** intended to be applied so that they can be found and removed | |
| 914 | +** consistently. Potential TODO is have this function do that if | |
| 915 | +** attachment_target_type(zTarget,1)!=0 but it would (for current | |
| 916 | +** uses) require duplicating work already done in the callers. | |
| 917 | +*/ | |
| 918 | +int attachments_ajax_from_POST(const char *zTarget, int bNeedsModeration){ | |
| 919 | + int i; | |
| 920 | + int rc = 0; | |
| 921 | + int n = 0; | |
| 922 | + int szLimit; /* attachment-max-size setting */ | |
| 923 | + char aKeyPrefix[20]; /* Buffer for key "file%d" */ | |
| 924 | + char aKeySize[30]; /* Buffer for key "file%d:bytes" */ | |
| 925 | + char aKeyName[30]; /* Buffer for key "file%d:filename" */ | |
| 926 | + char aKeyDesc[30]; /* Buffer for key "file%d_desc" */ | |
| 927 | + | |
| 928 | + db_begin_transaction(); | |
| 929 | + szLimit = db_get_int("attachment-size-limit", 0); | |
| 930 | + | |
| 931 | + for(i = 1; ; ++i, ++n){ | |
| 932 | + /* Look for P("fileN"), where N=1..n */ | |
| 933 | + const char *zContent; | |
| 934 | + const char *zFilename; | |
| 935 | + int szContent; | |
| 936 | + sqlite3_snprintf(sizeof(aKeyPrefix), aKeyPrefix, "file%d", i); | |
| 937 | + zContent = P(aKeyPrefix); | |
| 938 | + if( !zContent ){ | |
| 939 | + /* End of the list. */ | |
| 940 | + break; | |
| 941 | + } | |
| 942 | + sqlite3_snprintf(sizeof(aKeySize), aKeySize, "%s:bytes", | |
| 943 | + aKeyPrefix); | |
| 944 | + szContent = atoi(PD(aKeySize,"-1")); | |
| 945 | + if( szContent<=0 ){ | |
| 946 | + rc = -ajax_route_error(400,"Invalid file size: %d", szContent); | |
| 947 | + break; | |
| 948 | + }else if( szLimit>0 && szContent>szLimit ){ | |
| 949 | + rc = -ajax_route_error(413, "File size limit is %d bytes.", szLimit); | |
| 950 | + break; | |
| 951 | + }else{ | |
| 952 | + sqlite3_snprintf(sizeof(aKeyName), aKeyName, "%s:filename", | |
| 953 | + aKeyPrefix); | |
| 954 | + sqlite3_snprintf(sizeof(aKeyDesc), aKeyDesc, "%s_desc", | |
| 955 | + aKeyPrefix); | |
| 956 | + if( 0==(zFilename=P(aKeyName)) ){ | |
| 957 | + rc = -ajax_route_error(400, "Missing filename."); | |
| 958 | + break; | |
| 959 | + } | |
| 960 | + attach_commit(zFilename, zTarget, zContent, szContent, | |
| 961 | + bNeedsModeration, P(aKeyDesc)); | |
| 962 | + } | |
| 963 | + } | |
| 964 | + if( rc<0 ){ | |
| 965 | + db_rollback_transaction(); | |
| 966 | + return rc; | |
| 529 | 967 | }else{ |
| 530 | - @ <input type="hidden" name="page" value="%h(zPage)"> | |
| 968 | + db_commit_transaction(); | |
| 969 | + return n; | |
| 970 | + } | |
| 971 | +} | |
| 972 | + | |
| 973 | +/* | |
| 974 | +** Proxy for /attachadd?target=X | |
| 975 | +** | |
| 976 | +** Lists attachments for, and can add them to, a target artifact. | |
| 977 | +** | |
| 978 | +** target=TKT_HASH|WIKIPAGE_NAME|TECHNOTE_HASH|FORUMPOST_HASH | |
| 979 | +** from=ORIGINATING_URL | |
| 980 | +** | |
| 981 | +** Works like /attachadd but uses a JS-based interactive attachment | |
| 982 | +** selector. | |
| 983 | +** | |
| 984 | +** from=X tells it where to redirect to when it's done. | |
| 985 | +** | |
| 986 | +** This page requires a post-2018-ish JS-capable browser. | |
| 987 | +*/ | |
| 988 | +void attachaddV2_page(void){ | |
| 989 | + const char *zFrom = P("from"); | |
| 990 | + const char *zTarget = P("target"); | |
| 991 | + char *zTo = 0; | |
| 992 | + char *zTargetType = 0; | |
| 993 | + char *zExtraFree = 0; | |
| 994 | + int eTgtType = 0; | |
| 995 | + int goodCaptcha = 1; | |
| 996 | + char const * noJsArgs[] = {0,0,0,0}; /* Args for noscript form */ | |
| 997 | + | |
| 998 | + if( P("cancel") ) cgi_redirect(zFrom); | |
| 999 | + if( 0==zTarget ){ | |
| 1000 | + webpage_error("Requires target=X"); | |
| 1001 | + } | |
| 1002 | + login_check_credentials(); | |
| 1003 | + eTgtType = attachment_target_type(zTarget, 1); | |
| 1004 | + switch( eTgtType ){ | |
| 1005 | + default: | |
| 1006 | + case 0: | |
| 1007 | + webpage_error("Cannot resolve target=%h.", zTarget); | |
| 1008 | + break; | |
| 1009 | + case CFTYPE_FORUM:{ | |
| 1010 | + int fpid; | |
| 1011 | + if( g.perm.AttachForum==0 ){ | |
| 1012 | + login_needed(g.anon.AttachForum); | |
| 1013 | + return; | |
| 1014 | + } | |
| 1015 | + fpid = forumpost_head_rid2(zTarget); | |
| 1016 | + if( fpid<=0 ){ | |
| 1017 | + webpage_error("Invalid forum post ID: %h", zTarget); | |
| 1018 | + }else if( !g.perm.Admin && !forumpost_is_owner(fpid, 0) ){ | |
| 1019 | + webpage_error("Only admins can attach files to other users' " | |
| 1020 | + "forum posts."); | |
| 1021 | + } | |
| 1022 | + zTarget = zExtraFree = rid_to_uuid(fpid); | |
| 1023 | + noJsArgs[0] = zTarget; | |
| 1024 | + zTargetType = mprintf( | |
| 1025 | + "Forum post <a href=\"%R/forumpost/%S\">%.16h</a>", | |
| 1026 | + zTarget, zTarget | |
| 1027 | + ); | |
| 1028 | + zTo = mprintf("%R/forumpost/%S", zTarget); | |
| 1029 | + break; | |
| 1030 | + } | |
| 1031 | + case CFTYPE_EVENT:{ | |
| 1032 | + if( g.perm.Write==0 || g.perm.ApndWiki==0 || g.perm.Attach==0 ){ | |
| 1033 | + login_needed(g.anon.Write && g.anon.ApndWiki && g.anon.Attach); | |
| 1034 | + return; | |
| 1035 | + } | |
| 1036 | + if( !db_exists("SELECT 1 FROM tag WHERE tagname='event-%q'", | |
| 1037 | + zTarget) ){ | |
| 1038 | + zTarget = db_text(0, "SELECT substr(tagname,7) FROM tag" | |
| 1039 | + " WHERE tagname GLOB 'event-%q*'", | |
| 1040 | + zTarget); | |
| 1041 | + if( zTarget==0) fossil_redirect_home(); | |
| 1042 | + } | |
| 1043 | + zTo = zFrom ? 0 : mprintf("%R/technote?name=%T", zTarget); | |
| 1044 | + zTargetType = mprintf("Tech-note <a href=\"%R/technote/%s\">%S</a>", | |
| 1045 | + zTarget, zTarget); | |
| 1046 | + noJsArgs[1] = zTarget; | |
| 1047 | + break; | |
| 1048 | + } | |
| 1049 | + case CFTYPE_TICKET:{ | |
| 1050 | + if( g.perm.ApndTkt==0 || g.perm.Attach==0 ){ | |
| 1051 | + login_needed(g.anon.ApndTkt && g.anon.Attach); | |
| 1052 | + return; | |
| 1053 | + } | |
| 1054 | + if( !db_exists("SELECT 1 FROM tag WHERE tagname='tkt-%q'", | |
| 1055 | + zTarget) ){ | |
| 1056 | + zTarget = db_text(0, "SELECT substr(tagname,5) FROM tag" | |
| 1057 | + " WHERE tagname GLOB 'tkt-%q*'", zTarget); | |
| 1058 | + if( zTarget==0 ) fossil_redirect_home(); | |
| 1059 | + } | |
| 1060 | + zTo = zFrom ? 0 : mprintf("%R/tktview/%t", zTarget); | |
| 1061 | + zTargetType = mprintf("Ticket <a href=\"%R/tktview/%s\">%S</a>", | |
| 1062 | + zTarget, zTarget); | |
| 1063 | + noJsArgs[2] = zTarget; | |
| 1064 | + break; | |
| 1065 | + } | |
| 1066 | + case CFTYPE_WIKI:{ | |
| 1067 | + if( g.perm.ApndWiki==0 || g.perm.Attach==0 ){ | |
| 1068 | + login_needed(g.anon.ApndWiki && g.anon.Attach); | |
| 1069 | + return; | |
| 1070 | + } | |
| 1071 | + if( !db_exists("SELECT 1 FROM tag WHERE tagname='wiki-%q'", | |
| 1072 | + zTarget) ){ | |
| 1073 | + fossil_redirect_home(); | |
| 1074 | + } | |
| 1075 | + zTo = zFrom ? 0 : mprintf("%R/wiki?name=%T", zTarget); | |
| 1076 | + zTargetType = mprintf( | |
| 1077 | + "Wiki page <a href=\"%R/wiki?name=%h\">%h</a>", | |
| 1078 | + zTarget, zTarget | |
| 1079 | + ); | |
| 1080 | + noJsArgs[3] = zTarget; | |
| 1081 | + break; | |
| 1082 | + } | |
| 1083 | + } | |
| 1084 | + | |
| 1085 | + db_begin_transaction(); | |
| 1086 | + | |
| 1087 | + style_set_current_feature("attach"); | |
| 1088 | + style_header("Add Attachment"); | |
| 1089 | + if( !goodCaptcha ){ | |
| 1090 | + @ <p class="generalError">Error: Incorrect security code.</p> | |
| 1091 | + } | |
| 1092 | + @ <h2>Attachments for %s(zTargetType)</h2> | |
| 1093 | + attachment_list(zTarget, NULL, | |
| 1094 | + ATTACHLIST_SIZE | ATTACHLIST_HIDE_UNAPPROVED); | |
| 1095 | + attach_render_legacy_form( | |
| 1096 | + noJsArgs[0], noJsArgs[1], noJsArgs[2], | |
| 1097 | + noJsArgs[3], 0, | |
| 1098 | + zFrom ? zFrom : (zTo ? zTo : (zTo=mprintf("%R/home"))) | |
| 1099 | + ); | |
| 1100 | + @ <div id='attachadd-form-wrapper' class='hidden'> | |
| 1101 | + /* fossil.attach.js populates this DIV with the attachment widget, | |
| 1102 | + ** imports these hidden fields, and removes the legacy form. */ | |
| 1103 | + @ <input type="hidden" name="target" value="%h(zTarget)"> | |
| 1104 | + if( zFrom ){ | |
| 1105 | + @ <input type="hidden" name="from" value="%h(zFrom)"> | |
| 531 | 1106 | } |
| 532 | - @ <input type="hidden" name="from" value="%h(zFrom)"> | |
| 533 | - @ <input type="submit" name="ok" value="Add Attachment"> | |
| 534 | - @ <input type="submit" name="cancel" value="Cancel"> | |
| 535 | - @ </div> | |
| 1107 | + if( zTo ){ | |
| 1108 | + @ <input type="hidden" name="to" value="%h(zTo)"> | |
| 1109 | + } | |
| 536 | 1110 | captcha_generate(0); |
| 537 | - @ </form> | |
| 1111 | + login_insert_csrf_secret(); | |
| 1112 | + @ </div> | |
| 1113 | + builtin_fossil_js_bundle_or("attach", NULL); | |
| 1114 | + db_end_transaction(0); | |
| 538 | 1115 | style_finish_page(); |
| 539 | 1116 | fossil_free(zTargetType); |
| 540 | 1117 | fossil_free(zExtraFree); |
| 1118 | + fossil_free(zTo); | |
| 541 | 1119 | } |
| 542 | 1120 | |
| 543 | 1121 | /* |
| 544 | 1122 | ** WEBPAGE: ainfo |
| 545 | 1123 | ** URL: /ainfo?name=ARTIFACTID |
| @@ -600,15 +1178,17 @@ | ||
| 600 | 1178 | && db_exists("SELECT 1 FROM ticket WHERE tkt_uuid='%q'", zTarget) |
| 601 | 1179 | ){ |
| 602 | 1180 | if( !g.perm.RdTkt ){ login_needed(g.anon.RdTkt); return; } |
| 603 | 1181 | zTktUuid = zTarget; |
| 604 | 1182 | showDelMenu = g.perm.WrTkt; |
| 605 | - }else if( db_exists("SELECT 1 FROM tag WHERE tagname='wiki-%q'",zTarget) ){ | |
| 1183 | + }else if( db_exists("SELECT 1 FROM tag WHERE tagname='wiki-%q'", | |
| 1184 | + zTarget) ){ | |
| 606 | 1185 | if( !g.perm.RdWiki ){ login_needed(g.anon.RdWiki); return; } |
| 607 | 1186 | zWikiName = zTarget; |
| 608 | 1187 | showDelMenu = g.perm.WrWiki; |
| 609 | - }else if( db_exists("SELECT 1 FROM tag WHERE tagname='event-%q'",zTarget) ){ | |
| 1188 | + }else if( db_exists("SELECT 1 FROM tag WHERE tagname='event-%q'", | |
| 1189 | + zTarget) ){ | |
| 610 | 1190 | if( !g.perm.RdWiki ){ login_needed(g.anon.RdWiki); return; } |
| 611 | 1191 | zTNUuid = zTarget; |
| 612 | 1192 | showDelMenu = g.perm.Write && g.perm.WrWiki; |
| 613 | 1193 | } |
| 614 | 1194 | if( showDelMenu ){ |
| @@ -632,11 +1212,13 @@ | ||
| 632 | 1212 | Blob cksum; |
| 633 | 1213 | const char *zFile = zName; |
| 634 | 1214 | |
| 635 | 1215 | if( !bUserIsOwner ){ |
| 636 | 1216 | if( zForumPost ? !forumpost_may_close() : !g.perm.Admin ){ |
| 637 | - webpage_error("Only admins can delete other users' attachments."); | |
| 1217 | + webpage_error( | |
| 1218 | + "Only admins can delete other users' attachments." | |
| 1219 | + ); | |
| 638 | 1220 | } |
| 639 | 1221 | } |
| 640 | 1222 | db_begin_transaction(); |
| 641 | 1223 | blob_zero(&manifest); |
| 642 | 1224 | for(i=n=0; zFile[i]; i++){ |
| @@ -656,14 +1238,14 @@ | ||
| 656 | 1238 | @ <p>The attachment below has been deleted.</p> |
| 657 | 1239 | fossil_free(zNewDate); |
| 658 | 1240 | } |
| 659 | 1241 | |
| 660 | 1242 | if( P("del") |
| 661 | - && ((zForumPost && (bUserIsOwner || forumpost_may_close())) || | |
| 662 | - (zTktUuid && g.perm.WrTkt) || | |
| 663 | - (zWikiName && g.perm.WrWiki) || | |
| 664 | - (zTNUuid && g.perm.Write && g.perm.WrWiki)) | |
| 1243 | + && ((zForumPost && (bUserIsOwner || forumpost_may_close())) | |
| 1244 | + || (zTktUuid && g.perm.WrTkt) | |
| 1245 | + || (zWikiName && g.perm.WrWiki) | |
| 1246 | + || (zTNUuid && g.perm.Write && g.perm.WrWiki)) | |
| 665 | 1247 | ){ |
| 666 | 1248 | form_begin(0, "%R/ainfo/%!S", zUuid); |
| 667 | 1249 | @ <p>Confirm you want to delete the attachment shown below. |
| 668 | 1250 | @ <input type="submit" name="confirm" value="Confirm"> |
| 669 | 1251 | login_insert_csrf_secret(); |
| @@ -711,20 +1293,22 @@ | ||
| 711 | 1293 | @ (%d(rid)) |
| 712 | 1294 | } |
| 713 | 1295 | modPending = moderation_pending_www(rid); |
| 714 | 1296 | if( zForumPost ){ |
| 715 | 1297 | @ <tr><th>Forum Post:</th> |
| 716 | - @ <td>%z(href("%R/forumpost/%s",zForumPost))%h(zForumPost)</a></td></tr> | |
| 1298 | + @ <td>%z(href("%R/forumpost/%s",zForumPost))%h(zForumPost)</a>\ | |
| 1299 | + @ </td></tr> | |
| 717 | 1300 | }else if( zTktUuid ){ |
| 718 | 1301 | @ <tr><th>Ticket:</th> |
| 719 | 1302 | @ <td>%z(href("%R/tktview/%s",zTktUuid))%s(zTktUuid)</a></td></tr> |
| 720 | 1303 | }else if( zTNUuid ){ |
| 721 | 1304 | @ <tr><th>Tech Note:</th> |
| 722 | 1305 | @ <td>%z(href("%R/technote/%s",zTNUuid))%s(zTNUuid)</a></td></tr> |
| 723 | 1306 | }else if( zWikiName ){ |
| 724 | 1307 | @ <tr><th>Wiki Page:</th> |
| 725 | - @ <td>%z(href("%R/wiki?name=%t",zWikiName))%h(zWikiName)</a></td></tr> | |
| 1308 | + @ <td>%z(href("%R/wiki?name=%t",zWikiName))%h(zWikiName)</a>\ | |
| 1309 | + @ </td></tr> | |
| 726 | 1310 | } |
| 727 | 1311 | @ <tr><th>Date:</th><td> |
| 728 | 1312 | hyperlink_to_date(zDate, "</td></tr>"); |
| 729 | 1313 | @ <tr><th>User:</th><td> |
| 730 | 1314 | hyperlink_to_user(pAttach->zUser, zDate, "</td></tr>"); |
| @@ -735,22 +1319,42 @@ | ||
| 735 | 1319 | } |
| 736 | 1320 | @ <tr><th>Filename:</th><td>%h(zName)</td></tr> |
| 737 | 1321 | if( g.perm.Setup ){ |
| 738 | 1322 | @ <tr><th>MIME-Type:</th><td>%h(zMime)</td></tr> |
| 739 | 1323 | } |
| 740 | - @ <tr><th valign="top">Description:</th><td valign="top">%h(zDesc)</td></tr> | |
| 1324 | + @ <tr><th valign="top">Description:</th>\ | |
| 1325 | + /* FIXME (2026-06-05): Honor the N-card (comment mimetype). */ | |
| 1326 | + @ <td valign="top">%h(zDesc)</td></tr> | |
| 741 | 1327 | @ </table> |
| 742 | 1328 | |
| 743 | 1329 | if( modPending && (isModerator || bUserIsOwner) ){ |
| 744 | 1330 | @ <div class="section">Moderation</div> |
| 745 | 1331 | @ <blockquote> |
| 746 | 1332 | form_begin(0, "%R/ainfo/%s", zUuid); |
| 747 | 1333 | @ <label><input type="radio" name="modaction" value="delete"> |
| 748 | 1334 | @ Delete this attachment</label><br> |
| 749 | 1335 | if( isModerator ){ |
| 750 | - @ <label><input type="radio" name="modaction" value="approve"> | |
| 751 | - @ Approve this attachment</label><br> | |
| 1336 | +#if 0 | |
| 1337 | + /* TODO/FIXME (2026-06-03): only allow approval of an attachment | |
| 1338 | + ** if its target has been approved. Without this, we can end up | |
| 1339 | + ** with stale attachments which refer to rejected targets. We | |
| 1340 | + ** need a type-specific RID/UUID here, which requires | |
| 1341 | + ** refactoring above to get it. */ | |
| 1342 | + const int tgtid = 0; | |
| 1343 | + if( moderation_pending(tgtid) ){ | |
| 1344 | + @ <label><input type="radio" name="modaction" \ | |
| 1345 | + @ disabled value="approve"> | |
| 1346 | + @ <span class='modpending'>Cannot approve: | |
| 1347 | + @ target is pending moderation</span>\ | |
| 1348 | + @ </label><br> | |
| 1349 | + }else | |
| 1350 | +#else | |
| 1351 | + { | |
| 1352 | + @ <label><input type="radio" name="modaction" value="approve"> | |
| 1353 | + @ Approve this attachment</label><br> | |
| 1354 | + } | |
| 1355 | +#endif | |
| 752 | 1356 | } |
| 753 | 1357 | @ <input type="submit" value="Submit"> |
| 754 | 1358 | login_insert_csrf_secret(); |
| 755 | 1359 | @ </form> |
| 756 | 1360 | @ </blockquote> |
| @@ -767,11 +1371,12 @@ | ||
| 767 | 1371 | const char *z; |
| 768 | 1372 | content_get(ridSrc, &attach); |
| 769 | 1373 | blob_to_utf8_no_bom(&attach, 0); |
| 770 | 1374 | z = blob_str(&attach); |
| 771 | 1375 | if( zLn ){ |
| 772 | - output_text_with_line_numbers(z, blob_size(&attach), zName, zLn, 1); | |
| 1376 | + output_text_with_line_numbers(z, blob_size(&attach), | |
| 1377 | + zName, zLn, 1); | |
| 773 | 1378 | }else{ |
| 774 | 1379 | @ <pre> |
| 775 | 1380 | @ %h(z) |
| 776 | 1381 | @ </pre> |
| 777 | 1382 | } |
| @@ -798,10 +1403,13 @@ | ||
| 798 | 1403 | */ |
| 799 | 1404 | #define ATTACHLIST_HRULE_ABOVE 0x01 /* Insert <hr> above header */ |
| 800 | 1405 | #define ATTACHLIST_TARGET_BLANK 0x02 /* use target=_blank for links */ |
| 801 | 1406 | #define ATTACHLIST_SIZE 0x04 /* add size */ |
| 802 | 1407 | #define ATTACHLIST_HIDE_UNAPPROVED 0x08 /* Hide pending-moderation files */ |
| 1408 | +#define ATTACHLIST_DETAILS_CLOSED 0x10 /* Wrap in a closed DETAILS element */ | |
| 1409 | +#define ATTACHLIST_DETAILS_OPEN 0x20 /* Wrap in an open DETAILS element */ | |
| 1410 | +#define ATTACHLIST_HIDE_EMPTY 0x40 /* Skip if size<1 */ | |
| 803 | 1411 | #endif |
| 804 | 1412 | |
| 805 | 1413 | /* |
| 806 | 1414 | ** Output HTML to show a list of attachments. |
| 807 | 1415 | */ |
| @@ -810,19 +1418,25 @@ | ||
| 810 | 1418 | const char *zHeader, /* Header to display with attachments */ |
| 811 | 1419 | const int flags /* ATTACHLIST_... flags */ |
| 812 | 1420 | ){ |
| 813 | 1421 | int cnt = 0; |
| 814 | 1422 | char szBuf[36] = {0}; /* scratchpad for attachment size value */ |
| 815 | - const char * zLinkTgt = (ATTACHLIST_TARGET_BLANK & flags) | |
| 1423 | + const char *zLinkTgt = (ATTACHLIST_TARGET_BLANK & flags) | |
| 816 | 1424 | ? " target=\"_blank\"" : ""; |
| 1425 | + const int bUseDetail = flags & | |
| 1426 | + (ATTACHLIST_DETAILS_CLOSED | ATTACHLIST_DETAILS_OPEN); | |
| 817 | 1427 | Stmt q; |
| 1428 | + | |
| 818 | 1429 | db_prepare(&q, |
| 819 | - "SELECT datetime(mtime,toLocal()), filename, user," | |
| 820 | - " (SELECT uuid FROM blob WHERE rid=attachid), src, target, " | |
| 821 | - " attachid " | |
| 822 | - " FROM attachment" | |
| 823 | - " WHERE isLatest AND src!='' AND target=%Q" | |
| 1430 | + "SELECT datetime(mtime,toLocal()), a.filename, a.user," | |
| 1431 | + " b1.uuid, a.src, a.target, a.attachid, b2.size\n" | |
| 1432 | + " FROM attachment a, blob b1, blob b2\n" | |
| 1433 | + " WHERE a.isLatest\n" | |
| 1434 | + " AND a.src IS NOT NULL\n" | |
| 1435 | + " AND a.target=%Q\n" | |
| 1436 | + " AND b1.rid=a.attachid\n" | |
| 1437 | + " AND b2.uuid=a.src\n" | |
| 824 | 1438 | " ORDER BY mtime DESC", |
| 825 | 1439 | zTarget |
| 826 | 1440 | ); |
| 827 | 1441 | while( db_step(&q)==SQLITE_ROW ){ |
| 828 | 1442 | const char *zDate = db_column_text(&q, 0); |
| @@ -832,36 +1446,51 @@ | ||
| 832 | 1446 | const char *zSrc = db_column_text(&q, 4); |
| 833 | 1447 | const char *zTarget = db_column_text(&q, 5); |
| 834 | 1448 | const char *zDispUser = zUser && zUser[0] ? zUser : "anonymous"; |
| 835 | 1449 | const char *zTypeArg = 0; /* URL arg name for /attachdownload */ |
| 836 | 1450 | const int aid = db_column_int(&q, 6); |
| 837 | - const int iAType = attachment_target_type(zTarget); | |
| 1451 | + const int sz = db_column_int(&q, 7); | |
| 838 | 1452 | if( (flags & ATTACHLIST_HIDE_UNAPPROVED) |
| 839 | 1453 | && moderation_pending(aid) |
| 840 | 1454 | && !moderation_user_could(aid, 1, 0) ){ |
| 841 | 1455 | continue; |
| 1456 | + } | |
| 1457 | + if( sz<1 && (flags & ATTACHLIST_HIDE_EMPTY) ){ | |
| 1458 | + /* Deleted or phantom items. */ | |
| 1459 | + continue; | |
| 842 | 1460 | } |
| 843 | 1461 | if( cnt==0 ){ |
| 844 | - @ <section class='attachlist'> | |
| 1462 | + if( bUseDetail ){ | |
| 1463 | + @ <details class='attachlist' | |
| 1464 | + if( ATTACHLIST_DETAILS_OPEN & flags ){ | |
| 1465 | + @ open | |
| 1466 | + } | |
| 1467 | + @ > | |
| 1468 | + }else{ | |
| 1469 | + @ <section class='attachlist'> | |
| 1470 | + } | |
| 845 | 1471 | if( flags & ATTACHLIST_HRULE_ABOVE ){ |
| 846 | 1472 | @ <hr> |
| 847 | 1473 | } |
| 848 | - @ %s(zHeader) | |
| 1474 | + if( bUseDetail ){ | |
| 1475 | + @ <summary>%s(zHeader)</summary> | |
| 1476 | + }else{ | |
| 1477 | + @ %s(zHeader) | |
| 1478 | + } | |
| 849 | 1479 | @ <ul> |
| 850 | 1480 | } |
| 851 | 1481 | cnt++; |
| 852 | - switch( iAType ){ | |
| 1482 | + switch( attachment_target_type(zTarget, 1) ){ | |
| 853 | 1483 | case CFTYPE_TICKET: zTypeArg = "tkt"; break; |
| 854 | 1484 | case CFTYPE_FORUM: zTypeArg = "forumpost"; break; |
| 855 | 1485 | case CFTYPE_EVENT: zTypeArg = "technote"; break; |
| 856 | 1486 | case CFTYPE_WIKI: |
| 857 | 1487 | default: zTypeArg = "page"; break; |
| 858 | 1488 | } |
| 859 | 1489 | @ <li> |
| 860 | 1490 | @ <a href="%R/artifact/%!S(zSrc)"%s(zLinkTgt)>%h(zFile)</a> |
| 861 | 1491 | if( flags & ATTACHLIST_SIZE ){ |
| 862 | - const int sz = db_int(0,"SELECT size FROM blob WHERE uuid=%Q", zSrc); | |
| 863 | 1492 | sqlite3_snprintf(sizeof(szBuf), szBuf, " %d bytes", sz); |
| 864 | 1493 | } |
| 865 | 1494 | @ [<a href="%R/attachdownload/%t(zFile)?%s(zTypeArg)=%t(zTarget)\ |
| 866 | 1495 | @&file=%t(zFile)%s(zLinkTgt)">download</a>%s(szBuf)] |
| 867 | 1496 | @ added by %h(zDispUser) on |
| @@ -870,11 +1499,15 @@ | ||
| 870 | 1499 | moderation_pending_www(aid); |
| 871 | 1500 | @ </li> |
| 872 | 1501 | } |
| 873 | 1502 | if( cnt ){ |
| 874 | 1503 | @ </ul> |
| 875 | - @ </section> | |
| 1504 | + if( bUseDetail ){ | |
| 1505 | + @ </details> | |
| 1506 | + }else{ | |
| 1507 | + @ </section> | |
| 1508 | + } | |
| 876 | 1509 | } |
| 877 | 1510 | db_finalize(&q); |
| 878 | 1511 | } |
| 879 | 1512 | |
| 880 | 1513 | /* |
| @@ -1023,15 +1656,204 @@ | ||
| 1023 | 1656 | } |
| 1024 | 1657 | for(i = 2; i < g.argc; ++i){ |
| 1025 | 1658 | const char *zPage = g.argv[i]; |
| 1026 | 1659 | db_bind_text(&q, ":tgtname", zPage); |
| 1027 | 1660 | while(SQLITE_ROW == db_step(&q)){ |
| 1028 | - const char * zTime = db_column_text(&q, 0); | |
| 1029 | - const char * zSrc = db_column_text(&q, 1); | |
| 1030 | - const char * zTarget = db_column_text(&q, 2); | |
| 1031 | - const char * zName = db_column_text(&q, 3); | |
| 1661 | + const char *zTime = db_column_text(&q, 0); | |
| 1662 | + const char *zSrc = db_column_text(&q, 1); | |
| 1663 | + const char *zTarget = db_column_text(&q, 2); | |
| 1664 | + const char *zName = db_column_text(&q, 3); | |
| 1032 | 1665 | printf("%-20s %s %.12s %s\n", zTarget, zTime, zSrc, zName); |
| 1033 | 1666 | } |
| 1034 | 1667 | db_reset(&q); |
| 1035 | 1668 | } |
| 1036 | 1669 | db_finalize(&q); |
| 1037 | 1670 | } |
| 1671 | + | |
| 1672 | +/* | |
| 1673 | +** Renders the list of attachments for artifact pManifest as JSON to | |
| 1674 | +** blob pOut. If pManifest->type is not one of (CFTYPE_TICKET, | |
| 1675 | +** CFTYPE_FORUM, CFTYPE_EVENT, CFTYPE_WIKI) then it behaves as if the | |
| 1676 | +** result set is empty. | |
| 1677 | +** | |
| 1678 | +** If there are no matching attachments then its behavior depends on | |
| 1679 | +** emptyPolicy: | |
| 1680 | +** | |
| 1681 | +** <0 = emit a JSON NULL | |
| 1682 | +** 0 = emit no output | |
| 1683 | +** >0 = emit an empty JSON array | |
| 1684 | +** | |
| 1685 | +** If bLatestOnly is true then only the most recent entry for a given | |
| 1686 | +** attachment is emitted, else all versions are emitted in descending | |
| 1687 | +** mtime order. | |
| 1688 | +** | |
| 1689 | +** Returns the number of attachments. | |
| 1690 | +** | |
| 1691 | +** Output format: | |
| 1692 | +** | |
| 1693 | +** [{ | |
| 1694 | +** "uuid": attachment artifact hash, | |
| 1695 | +** "src": hash of the attachment blob, | |
| 1696 | +** "target": wiki page name or ticket/event ID, | |
| 1697 | +** "filename": filename of attachment, | |
| 1698 | +** "mtime": ISO-8601 timestamp UTC, | |
| 1699 | +** "isLatest": true if this is the latest version of this file | |
| 1700 | +** else false, | |
| 1701 | +** }, ...once per attachment] | |
| 1702 | +** | |
| 1703 | +*/ | |
| 1704 | +int attachments_to_json(const Manifest *pManifest, | |
| 1705 | + Blob *pOut, int bLatestOnly, | |
| 1706 | + int emptyPolicy){ | |
| 1707 | + int i = 0; | |
| 1708 | + Stmt q = empty_Stmt; | |
| 1709 | + char *zToFree = 0; | |
| 1710 | + const char *zTgt = 0; | |
| 1711 | + switch(pManifest->type){ | |
| 1712 | + case CFTYPE_FORUM: zTgt = zToFree = rid_to_uuid(pManifest->rid); | |
| 1713 | + break; | |
| 1714 | + case CFTYPE_WIKI: zTgt = pManifest->zWikiTitle; break; | |
| 1715 | + case CFTYPE_EVENT: zTgt = pManifest->zEventId; break; | |
| 1716 | + case CFTYPE_TICKET: zTgt = pManifest->zTicketUuid; break; | |
| 1717 | + default: | |
| 1718 | + goto empty_result; | |
| 1719 | + } | |
| 1720 | + db_prepare(&q, | |
| 1721 | + "SELECT datetime(mtime), a.src, a.target, a.filename, a.isLatest,\n" | |
| 1722 | + " b2.size, b1.uuid, a.user, a.comment\n" | |
| 1723 | + " FROM attachment a, blob b1, blob b2\n" | |
| 1724 | + " WHERE a.target=%Q\n" | |
| 1725 | + " AND a.src IS NOT NULL\n" | |
| 1726 | + " AND b1.rid=a.attachid\n" | |
| 1727 | + " AND b2.uuid=a.src\n" | |
| 1728 | + " AND (a.isLatest OR %d)\n" | |
| 1729 | + " ORDER BY a.target, a.isLatest DESC, a.mtime DESC\n", | |
| 1730 | + zTgt, !bLatestOnly | |
| 1731 | + ); | |
| 1732 | + while(SQLITE_ROW == db_step(&q)){ | |
| 1733 | + const char *zTime = db_column_text(&q, 0); | |
| 1734 | + const char *zSrc = db_column_text(&q, 1); | |
| 1735 | + const char *zTarget = db_column_text(&q, 2); | |
| 1736 | + const char *zName = db_column_text(&q, 3); | |
| 1737 | + const int isLatest = db_column_int(&q, 4); | |
| 1738 | + const int sz = db_column_int(&q, 5); | |
| 1739 | + const char *zUuid = db_column_text(&q, 6); | |
| 1740 | + const char *zUser = db_column_text(&q, 7); | |
| 1741 | + const char *zComment = db_column_text(&q, 8); | |
| 1742 | + if(!i++){ | |
| 1743 | + blob_append_char(pOut, '['); | |
| 1744 | + }else{ | |
| 1745 | + blob_append_char(pOut, ','); | |
| 1746 | + } | |
| 1747 | + blob_appendf( | |
| 1748 | + pOut, | |
| 1749 | + "{\"uuid\": %!j, \"src\": %!j, \"target\": %!j, " | |
| 1750 | + "\"filename\": %!j, \"size\":%d, \"mtime\": %!j, " | |
| 1751 | + "\"isLatest\": %s, \"user\": %!j, \"comment\": ", | |
| 1752 | + zUuid, zSrc, zTarget, | |
| 1753 | + zName, sz, zTime, isLatest ? "true" : "false", | |
| 1754 | + zUser | |
| 1755 | + ); | |
| 1756 | + if( zComment && zComment[0] ){ | |
| 1757 | + blob_appendf(pOut, "%!j", zComment); | |
| 1758 | + }else{ | |
| 1759 | + blob_append_literal(pOut, "null"); | |
| 1760 | + } | |
| 1761 | + blob_append_char(pOut, '}'); | |
| 1762 | + } | |
| 1763 | + fossil_free(zToFree); | |
| 1764 | + db_finalize(&q); | |
| 1765 | + if(!i){ | |
| 1766 | + empty_result: | |
| 1767 | + if( emptyPolicy>0 ){ | |
| 1768 | + blob_append_literal(pOut, "[]"); | |
| 1769 | + }else if( emptyPolicy<0 ){ | |
| 1770 | + blob_append_literal(pOut, "null"); | |
| 1771 | + } | |
| 1772 | + }else{ | |
| 1773 | + blob_append_char(pOut, ']'); | |
| 1774 | + } | |
| 1775 | + return i; | |
| 1776 | +} | |
| 1777 | + | |
| 1778 | +/* | |
| 1779 | +** COMMAND: test-attachment-target | |
| 1780 | +** | |
| 1781 | +** Usage: %fossil test-attachment-target TARGET_ID... | |
| 1782 | +*/ | |
| 1783 | +void test_attachment_target_type_cmd(void){ | |
| 1784 | + int i; | |
| 1785 | + verify_all_options(); | |
| 1786 | + db_find_and_open_repository(0, 0); | |
| 1787 | + if( g.argc<3 ){ | |
| 1788 | + usage("test-attachment-target TARGET_ID"); | |
| 1789 | + return; | |
| 1790 | + } | |
| 1791 | + for( i = 2; i < g.argc; ++i ){ | |
| 1792 | + const char *zTarget = g.argv[i]; | |
| 1793 | + const int rid = attachment_target_rid(zTarget, 0); | |
| 1794 | + const int type = attachment_target_type(zTarget, 0); | |
| 1795 | + const char *zType = "<invalid>"; | |
| 1796 | + switch(type){ | |
| 1797 | + case CFTYPE_EVENT: zType = "technote"; break; | |
| 1798 | + case CFTYPE_FORUM: zType = "forumpost"; break; | |
| 1799 | + case CFTYPE_TICKET: zType = "ticket"; break; | |
| 1800 | + case CFTYPE_WIKI: zType = "wiki"; break; | |
| 1801 | + } | |
| 1802 | + fossil_print("%-20s = %-9s #%d %z\n", | |
| 1803 | + zTarget, zType, rid, | |
| 1804 | + rid>0 ? rid_to_uuid(rid) : 0); | |
| 1805 | + } | |
| 1806 | +} | |
| 1807 | + | |
| 1808 | + | |
| 1809 | +/* | |
| 1810 | +** COMMAND: test-attachments-to-json | |
| 1811 | +** | |
| 1812 | +** Usage: %fossil test-attachments-to-json TARGET_ID... | |
| 1813 | +** | |
| 1814 | +** Options: | |
| 1815 | +** --old List all versions of attachments. Default is to | |
| 1816 | +** list only the latest. | |
| 1817 | +** --full Require a full target ID, not a prefix. | |
| 1818 | +** | |
| 1819 | +** Emits a JSON array of attachments for the given attachment targets. | |
| 1820 | +** The given IDs must be wiki page names, ticket hashes, tech-note | |
| 1821 | +** hashes, or forum post hashes. By default it accepts hash prefixes | |
| 1822 | +** but does no detection of ambiguity or cross-type prefix collisions | |
| 1823 | +** so may emit curious results if given short, colliding IDs. | |
| 1824 | +*/ | |
| 1825 | +void test_attachments_to_json_cmd(void){ | |
| 1826 | + const int emptyPolicy = 1; | |
| 1827 | + const int bLatestOnly = find_option("old",0,0)==0; | |
| 1828 | + const int bFullId = find_option("full",0,0)!=0; | |
| 1829 | + int i; | |
| 1830 | + | |
| 1831 | + verify_all_options(); | |
| 1832 | + db_find_and_open_repository(0, 0); | |
| 1833 | + if( g.argc<3 ){ | |
| 1834 | + usage("test-attachments-to-json TARGET_ID"); | |
| 1835 | + return; | |
| 1836 | + } | |
| 1837 | + for( i = 2; i < g.argc; ++i ){ | |
| 1838 | + const char *zTarget = g.argv[i]; | |
| 1839 | + const int rid = attachment_target_rid(zTarget, bFullId); | |
| 1840 | + if( 0==rid ){ | |
| 1841 | + fossil_print("** cannot resolve %s\n", zTarget); | |
| 1842 | + }else{ | |
| 1843 | + Blob b = BLOB_INITIALIZER; | |
| 1844 | + Manifest *pManifest = manifest_get(rid, CFTYPE_ANY, NULL); | |
| 1845 | + assert( pManifest ); | |
| 1846 | + attachments_to_json(pManifest, &b, bLatestOnly, emptyPolicy); | |
| 1847 | + fossil_print("Attachments for %s: ", zTarget); | |
| 1848 | + if( b.nUsed ){ | |
| 1849 | + char *zPretty = db_text(0,"SELECT json_pretty(%B)", &b); | |
| 1850 | + fossil_print("%s\n", zPretty); | |
| 1851 | + fossil_free(zPretty); | |
| 1852 | + }else{ | |
| 1853 | + fossil_print("none\n"); | |
| 1854 | + } | |
| 1855 | + blob_reset(&b); | |
| 1856 | + manifest_destroy(pManifest); | |
| 1857 | + } | |
| 1858 | + } | |
| 1859 | +} | |
| 1038 | 1860 |
| --- src/attach.c | |
| +++ src/attach.c | |
| @@ -22,41 +22,188 @@ | |
| 22 | #include <assert.h> |
| 23 | |
| 24 | /* |
| 25 | ** Given a presumedly legal attachment target name, this guesses the |
| 26 | ** target type and returns one of CFTYPE_FORUM, CFTYPE_WIKI, |
| 27 | ** CFTYPE_TICKET, or CFTYPE_EVENT. Returns 0 if it cannot |
| 28 | ** distinguish the target type. |
| 29 | ** |
| 30 | ** In the case of CFTYPE_FORUM, it is up to the caller to ensure that, |
| 31 | ** if needed, they resolve zTarget using forumpost_head_rid2() so that |
| 32 | ** they get the RID of the earliest version of the post, as that is |
| 33 | ** the only one which attachments should target. |
| 34 | */ |
| 35 | int attachment_target_type(const char *zTarget){ |
| 36 | static Stmt q = empty_Stmt_m; |
| 37 | int rc = 0; |
| 38 | if( forumpost_head_rid2(zTarget)>0 ){ |
| 39 | return CFTYPE_FORUM; |
| 40 | } |
| 41 | if( !q.pStmt ){ |
| 42 | db_static_prepare( |
| 43 | &q, |
| 44 | "SELECT CASE " |
| 45 | "WHEN 'tkt-'||:tgt IN (SELECT tagname FROM tag) THEN %d " |
| 46 | "WHEN 'event-'||:tgt IN (SELECT tagname FROM tag) THEN %d " |
| 47 | "WHEN 'wiki-'||:tgt IN (SELECT tagname FROM tag) THEN %d " |
| 48 | "ELSE 0 END", |
| 49 | CFTYPE_TICKET, CFTYPE_EVENT, CFTYPE_WIKI |
| 50 | ); |
| 51 | } |
| 52 | db_bind_text(&q, ":tgt", zTarget); |
| 53 | if( SQLITE_ROW==db_step(&q) ){ |
| 54 | rc = db_column_int(&q, 0); |
| 55 | } |
| 56 | db_reset(&q); |
| 57 | return rc; |
| 58 | } |
| 59 | |
| 60 | /* |
| 61 | ** WEBPAGE: attachlist |
| 62 | ** List attachments. |
| @@ -69,20 +216,21 @@ | |
| 69 | ** At most one of technote=, tkt=, forumpost=, or page= may be supplied. |
| 70 | ** |
| 71 | ** If none are given, all attachments are listed. If one is given, only |
| 72 | ** attachments for the designated technote, ticket or wiki page are shown. |
| 73 | ** |
| 74 | ** HASH may be just a prefix of the relevant technical note or ticket |
| 75 | ** artifact hash, in which case all attachments of all technical notes or |
| 76 | ** tickets with the prefix will be listed. Forum posts, on the other hand, |
| 77 | ** require a unique hash prefix. |
| 78 | */ |
| 79 | void attachlist_page(void){ |
| 80 | const char *zPage = P("page"); |
| 81 | const char *zTkt = P("tkt"); |
| 82 | const char *zTechNote = P("technote"); |
| 83 | const char *zForumPost = P("forumpost"); |
| 84 | Blob sql; |
| 85 | Stmt q; |
| 86 | |
| 87 | if( zPage && zTkt ) zTkt = 0; |
| 88 | login_check_credentials(); |
| @@ -102,32 +250,47 @@ | |
| 102 | if( fnid<=0 ){ |
| 103 | webpage_error("Invalid forum post ID: %h", zForumPost); |
| 104 | } |
| 105 | blob_append_sql(&sql, " WHERE target=" |
| 106 | "(SELECT uuid FROM blob WHERE rid=%d)", fnid); |
| 107 | }else if( zPage ){ |
| 108 | if( g.perm.RdWiki==0 ){ login_needed(g.anon.RdWiki); return; } |
| 109 | style_header("Attachments To Wiki page %h", zPage); |
| 110 | blob_append_sql(&sql, " WHERE target=%Q", zPage); |
| 111 | }else if( zTkt ){ |
| 112 | if( g.perm.RdTkt==0 ){ login_needed(g.anon.RdTkt); return; } |
| 113 | style_header("Attachments To Ticket %S", zTkt); |
| 114 | blob_append_sql(&sql, " WHERE target GLOB '%q*'", zTkt); |
| 115 | }else if( zTechNote ){ |
| 116 | if( g.perm.RdWiki==0 ){ login_needed(g.anon.RdWiki); return; } |
| 117 | style_header("Attachments To Tech Note %S", zTechNote); |
| 118 | blob_append_sql(&sql, " WHERE target GLOB '%q*'", |
| 119 | zTechNote); |
| 120 | }else{ |
| 121 | if( g.perm.RdTkt==0 && g.perm.RdWiki==0 ){ |
| 122 | login_needed(g.anon.RdTkt || g.anon.RdWiki); |
| 123 | return; |
| 124 | } |
| 125 | style_header("All Attachments"); |
| 126 | } |
| 127 | blob_append_sql(&sql, " ORDER BY mtime DESC"); |
| 128 | db_prepare(&q, "%s", blob_sql_text(&sql)); |
| 129 | @ <ol> |
| 130 | while( db_step(&q)==SQLITE_ROW ){ |
| 131 | const char *zDate; |
| 132 | const char *zSrc; |
| 133 | const char *zTarget; |
| @@ -137,10 +300,11 @@ | |
| 137 | const char *zUuid; |
| 138 | const char *zDispUser; |
| 139 | const int attachid = db_column_int(&q, 7); |
| 140 | int type; |
| 141 | int i; |
| 142 | char *zUrlTail = 0; |
| 143 | |
| 144 | if( moderation_pending(attachid) |
| 145 | && !moderation_user_could(attachid, 1, 0) ){ |
| 146 | /* Elide entries which are currently pending moderation unless |
| @@ -160,11 +324,12 @@ | |
| 160 | if( zFilename[i]=='/' && zFilename[i+1]!=0 ){ |
| 161 | zFilename = &zFilename[i+1]; |
| 162 | i = -1; |
| 163 | } |
| 164 | } |
| 165 | type = attachment_target_type(zTarget); |
| 166 | switch( type ){ |
| 167 | case CFTYPE_TICKET: |
| 168 | zUrlTail = mprintf("tkt=%s&file=%t", zTarget, zFilename); |
| 169 | break; |
| 170 | case CFTYPE_EVENT: |
| @@ -176,20 +341,32 @@ | |
| 176 | case CFTYPE_WIKI: |
| 177 | zUrlTail = mprintf("page=%t&file=%t", zTarget, zFilename); |
| 178 | break; |
| 179 | } |
| 180 | @ <li><p> |
| 181 | @ Attachment %z(href("%R/ainfo/%!S",zUuid))%S(zUuid)</a> |
| 182 | moderation_pending_www(attachid); |
| 183 | @ <br><a href="%R/attachview?%s(zUrlTail)">%h(zFilename)</a> |
| 184 | @ [<a href="%R/attachdownload/%t(zFilename)?%s(zUrlTail)">download</a>]<br> |
| 185 | if( zComment ) while( fossil_isspace(zComment[0]) ) zComment++; |
| 186 | if( zComment && zComment[0] ){ |
| 187 | @ %!W(zComment)<br> |
| 188 | } |
| 189 | if( zForumPost==0 && zPage==0 && zTkt==0 && zTechNote==0 ){ |
| 190 | if( zSrc==0 || zSrc[0]==0 ){ |
| 191 | zSrc = "Deleted from"; |
| 192 | }else { |
| 193 | zSrc = "Added to"; |
| 194 | } |
| 195 | switch( type ){ |
| @@ -362,41 +539,81 @@ | |
| 362 | Manifest *pManifest; |
| 363 | |
| 364 | db_begin_transaction(); |
| 365 | blob_init(&content, aContent, szContent); |
| 366 | pManifest = manifest_parse(&content, 0, 0); |
| 367 | manifest_destroy(pManifest); |
| 368 | blob_init(&content, aContent, szContent); |
| 369 | if( pManifest ){ |
| 370 | blob_compress(&content, &content); |
| 371 | addCompress = 1; |
| 372 | } |
| 373 | rid = content_put_ex(&content, 0, 0, 0, needModerator); |
| 374 | zUUID = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid); |
| 375 | blob_zero(&manifest); |
| 376 | for(i=n=0; zName[i]; i++){ |
| 377 | if( zName[i]=='/' || zName[i]=='\\' ) n = i+1; |
| 378 | } |
| 379 | zName += n; |
| 380 | if( zName[0]==0 ) zName = "unknown"; |
| 381 | blob_appendf(&manifest, "A %F%s %F %s\n", |
| 382 | zName, addCompress ? ".gz" : "", zTarget, zUUID); |
| 383 | while( fossil_isspace(zComment[0]) ) zComment++; |
| 384 | n = strlen(zComment); |
| 385 | while( n>0 && fossil_isspace(zComment[n-1]) ){ n--; } |
| 386 | if( n>0 ){ |
| 387 | blob_appendf(&manifest, "C %#F\n", n, zComment); |
| 388 | } |
| 389 | zDate = date_in_standard_format("now"); |
| 390 | blob_appendf(&manifest, "D %s\n", zDate); |
| 391 | blob_appendf(&manifest, "U %F\n", login_name()); |
| 392 | md5sum_blob(&manifest, &cksum); |
| 393 | blob_appendf(&manifest, "Z %b\n", &cksum); |
| 394 | attach_put(&manifest, rid, needModerator); |
| 395 | assert( blob_is_reset(&manifest) ); |
| 396 | db_end_transaction(0); |
| 397 | } |
| 398 | |
| 399 | /* |
| 400 | ** WEBPAGE: attachadd |
| 401 | ** Add a new attachment. |
| 402 | ** |
| @@ -404,33 +621,54 @@ | |
| 404 | ** page=WIKIPAGE |
| 405 | ** technote=HASH |
| 406 | ** forumpost=HASH |
| 407 | ** from=URL |
| 408 | ** |
| 409 | */ |
| 410 | void attachadd_page(void){ |
| 411 | const char *zPage = P("page"); |
| 412 | const char *zForumPost = P("forumpost"); |
| 413 | const char *zTkt = P("tkt"); |
| 414 | const char *zTechNote = P("technote"); |
| 415 | const char *zFrom = P("from"); |
| 416 | const char *aContent = P("f"); |
| 417 | const char *zName = PD("f:filename","unknown"); |
| 418 | const char *zComment = PD("comment", ""); |
| 419 | const char *zTarget; |
| 420 | char * zTo = 0; |
| 421 | char *zTargetType = 0; |
| 422 | char *zExtraFree = 0; |
| 423 | int szContent = atoi(PD("f:bytes","0")); |
| 424 | int goodCaptcha = 1; |
| 425 | int szLimit = 0; |
| 426 | |
| 427 | if( zFrom==0 ) zFrom = mprintf("%R/home"); |
| 428 | if( P("cancel") ) cgi_redirect(zFrom); |
| 429 | if( (!!zPage + !!zTkt + !!zTechNote + !!zForumPost)!=1 ){ |
| 430 | webpage_error("Requires exactly one one: page=X, tkt=X, forumpost=X," |
| 431 | " or technote=X"); |
| 432 | } |
| 433 | login_check_credentials(); |
| 434 | if( zForumPost ){ |
| 435 | int fpid; |
| 436 | if( g.perm.AttachForum==0 ){ |
| @@ -445,26 +683,23 @@ | |
| 445 | "forum posts."); |
| 446 | } |
| 447 | zTarget = zExtraFree = rid_to_uuid(fpid); |
| 448 | zTargetType = mprintf("Forum post <a href=\"%R/forumpost/%S\">%h</a>", |
| 449 | zTarget, zForumPost); |
| 450 | zTo = 1 |
| 451 | ? mprintf("%R/forumpost/%S", zTarget) |
| 452 | : mprintf("%R/attachview?forumpost=%T&file=%T", |
| 453 | zTarget, zName) |
| 454 | /* Or we could return directly to the forum post. */; |
| 455 | }else if( zPage ){ |
| 456 | if( g.perm.ApndWiki==0 || g.perm.Attach==0 ){ |
| 457 | login_needed(g.anon.ApndWiki && g.anon.Attach); |
| 458 | return; |
| 459 | } |
| 460 | if( !db_exists("SELECT 1 FROM tag WHERE tagname='wiki-%q'", zPage) ){ |
| 461 | fossil_redirect_home(); |
| 462 | } |
| 463 | zTarget = zPage; |
| 464 | zTargetType = mprintf("Wiki Page <a href=\"%R/wiki?name=%h\">%h</a>", |
| 465 | zPage, zPage); |
| 466 | }else if ( zTechNote ){ |
| 467 | if( g.perm.Write==0 || g.perm.ApndWiki==0 || g.perm.Attach==0 ){ |
| 468 | login_needed(g.anon.Write && g.anon.ApndWiki && g.anon.Attach); |
| 469 | return; |
| 470 | } |
| @@ -474,11 +709,11 @@ | |
| 474 | if( zTechNote==0) fossil_redirect_home(); |
| 475 | } |
| 476 | zTarget = zTechNote; |
| 477 | zTargetType = mprintf("Tech Note <a href=\"%R/technote/%s\">%S</a>", |
| 478 | zTechNote, zTechNote); |
| 479 | |
| 480 | }else{ |
| 481 | assert( zTkt ); |
| 482 | if( g.perm.ApndTkt==0 || g.perm.Attach==0 ){ |
| 483 | login_needed(g.anon.ApndTkt && g.anon.Attach); |
| 484 | return; |
| @@ -487,21 +722,25 @@ | |
| 487 | zTkt = db_text(0, "SELECT substr(tagname,5) FROM tag" |
| 488 | " WHERE tagname GLOB 'tkt-%q*'", zTkt); |
| 489 | if( zTkt==0 ) fossil_redirect_home(); |
| 490 | } |
| 491 | zTarget = zTkt; |
| 492 | zTargetType = mprintf("Ticket <a href=\"%R/tktview/%s\">%S</a>", |
| 493 | zTkt, zTkt); |
| 494 | } |
| 495 | szLimit = db_get_int("attachment-size-limit", 0); |
| 496 | if( szContent<0 || (szLimit && szContent>szLimit) ){ |
| 497 | /* This check must be done late so that zTargetType is set up. */ |
| 498 | @ <p class="generalError">Attachment %h(zName) is too large. |
| 499 | @ <a href="%R/help/attachment-size-limit">Limit</a> is |
| 500 | @ %d(szLimit ? szLimit : 0x7fffffff) bytes</p> |
| 501 | /* Fall through and render form. */ |
| 502 | }else if( P("ok") && szContent>0 && (goodCaptcha = captcha_is_correct(0)) ){ |
| 503 | int needModerator = (zForumPost!=0 && forum_need_moderation()) || |
| 504 | (zTkt!=0 && ticket_need_moderation(0)) || |
| 505 | (zPage!=0 && wiki_need_moderation(0)); |
| 506 | attach_commit(zName, zTarget, aContent, szContent, needModerator, zComment); |
| 507 | cgi_redirect(zTo ? zTo : zFrom); |
| @@ -511,35 +750,374 @@ | |
| 511 | style_header("Add Attachment"); |
| 512 | if( !goodCaptcha ){ |
| 513 | @ <p class="generalError">Error: Incorrect security code.</p> |
| 514 | } |
| 515 | @ <h2>Add Attachment To %s(zTargetType)</h2> |
| 516 | form_begin("enctype='multipart/form-data'", "%R/attachadd"); |
| 517 | @ <div> |
| 518 | @ File to Attach: |
| 519 | @ <input type="file" name="f" size="60"><br> |
| 520 | @ Description:<br> |
| 521 | @ <textarea name="comment" cols="80" rows="5" wrap="virtual"\ |
| 522 | @ >%h(zComment)</textarea><br> |
| 523 | if( zForumPost ){ |
| 524 | @ <input type="hidden" name="forumpost" value="%h(zTarget)"> |
| 525 | }else if( zTkt ){ |
| 526 | @ <input type="hidden" name="tkt" value="%h(zTkt)"> |
| 527 | }else if( zTechNote ){ |
| 528 | @ <input type="hidden" name="technote" value="%h(zTechNote)"> |
| 529 | }else{ |
| 530 | @ <input type="hidden" name="page" value="%h(zPage)"> |
| 531 | } |
| 532 | @ <input type="hidden" name="from" value="%h(zFrom)"> |
| 533 | @ <input type="submit" name="ok" value="Add Attachment"> |
| 534 | @ <input type="submit" name="cancel" value="Cancel"> |
| 535 | @ </div> |
| 536 | captcha_generate(0); |
| 537 | @ </form> |
| 538 | style_finish_page(); |
| 539 | fossil_free(zTargetType); |
| 540 | fossil_free(zExtraFree); |
| 541 | } |
| 542 | |
| 543 | /* |
| 544 | ** WEBPAGE: ainfo |
| 545 | ** URL: /ainfo?name=ARTIFACTID |
| @@ -600,15 +1178,17 @@ | |
| 600 | && db_exists("SELECT 1 FROM ticket WHERE tkt_uuid='%q'", zTarget) |
| 601 | ){ |
| 602 | if( !g.perm.RdTkt ){ login_needed(g.anon.RdTkt); return; } |
| 603 | zTktUuid = zTarget; |
| 604 | showDelMenu = g.perm.WrTkt; |
| 605 | }else if( db_exists("SELECT 1 FROM tag WHERE tagname='wiki-%q'",zTarget) ){ |
| 606 | if( !g.perm.RdWiki ){ login_needed(g.anon.RdWiki); return; } |
| 607 | zWikiName = zTarget; |
| 608 | showDelMenu = g.perm.WrWiki; |
| 609 | }else if( db_exists("SELECT 1 FROM tag WHERE tagname='event-%q'",zTarget) ){ |
| 610 | if( !g.perm.RdWiki ){ login_needed(g.anon.RdWiki); return; } |
| 611 | zTNUuid = zTarget; |
| 612 | showDelMenu = g.perm.Write && g.perm.WrWiki; |
| 613 | } |
| 614 | if( showDelMenu ){ |
| @@ -632,11 +1212,13 @@ | |
| 632 | Blob cksum; |
| 633 | const char *zFile = zName; |
| 634 | |
| 635 | if( !bUserIsOwner ){ |
| 636 | if( zForumPost ? !forumpost_may_close() : !g.perm.Admin ){ |
| 637 | webpage_error("Only admins can delete other users' attachments."); |
| 638 | } |
| 639 | } |
| 640 | db_begin_transaction(); |
| 641 | blob_zero(&manifest); |
| 642 | for(i=n=0; zFile[i]; i++){ |
| @@ -656,14 +1238,14 @@ | |
| 656 | @ <p>The attachment below has been deleted.</p> |
| 657 | fossil_free(zNewDate); |
| 658 | } |
| 659 | |
| 660 | if( P("del") |
| 661 | && ((zForumPost && (bUserIsOwner || forumpost_may_close())) || |
| 662 | (zTktUuid && g.perm.WrTkt) || |
| 663 | (zWikiName && g.perm.WrWiki) || |
| 664 | (zTNUuid && g.perm.Write && g.perm.WrWiki)) |
| 665 | ){ |
| 666 | form_begin(0, "%R/ainfo/%!S", zUuid); |
| 667 | @ <p>Confirm you want to delete the attachment shown below. |
| 668 | @ <input type="submit" name="confirm" value="Confirm"> |
| 669 | login_insert_csrf_secret(); |
| @@ -711,20 +1293,22 @@ | |
| 711 | @ (%d(rid)) |
| 712 | } |
| 713 | modPending = moderation_pending_www(rid); |
| 714 | if( zForumPost ){ |
| 715 | @ <tr><th>Forum Post:</th> |
| 716 | @ <td>%z(href("%R/forumpost/%s",zForumPost))%h(zForumPost)</a></td></tr> |
| 717 | }else if( zTktUuid ){ |
| 718 | @ <tr><th>Ticket:</th> |
| 719 | @ <td>%z(href("%R/tktview/%s",zTktUuid))%s(zTktUuid)</a></td></tr> |
| 720 | }else if( zTNUuid ){ |
| 721 | @ <tr><th>Tech Note:</th> |
| 722 | @ <td>%z(href("%R/technote/%s",zTNUuid))%s(zTNUuid)</a></td></tr> |
| 723 | }else if( zWikiName ){ |
| 724 | @ <tr><th>Wiki Page:</th> |
| 725 | @ <td>%z(href("%R/wiki?name=%t",zWikiName))%h(zWikiName)</a></td></tr> |
| 726 | } |
| 727 | @ <tr><th>Date:</th><td> |
| 728 | hyperlink_to_date(zDate, "</td></tr>"); |
| 729 | @ <tr><th>User:</th><td> |
| 730 | hyperlink_to_user(pAttach->zUser, zDate, "</td></tr>"); |
| @@ -735,22 +1319,42 @@ | |
| 735 | } |
| 736 | @ <tr><th>Filename:</th><td>%h(zName)</td></tr> |
| 737 | if( g.perm.Setup ){ |
| 738 | @ <tr><th>MIME-Type:</th><td>%h(zMime)</td></tr> |
| 739 | } |
| 740 | @ <tr><th valign="top">Description:</th><td valign="top">%h(zDesc)</td></tr> |
| 741 | @ </table> |
| 742 | |
| 743 | if( modPending && (isModerator || bUserIsOwner) ){ |
| 744 | @ <div class="section">Moderation</div> |
| 745 | @ <blockquote> |
| 746 | form_begin(0, "%R/ainfo/%s", zUuid); |
| 747 | @ <label><input type="radio" name="modaction" value="delete"> |
| 748 | @ Delete this attachment</label><br> |
| 749 | if( isModerator ){ |
| 750 | @ <label><input type="radio" name="modaction" value="approve"> |
| 751 | @ Approve this attachment</label><br> |
| 752 | } |
| 753 | @ <input type="submit" value="Submit"> |
| 754 | login_insert_csrf_secret(); |
| 755 | @ </form> |
| 756 | @ </blockquote> |
| @@ -767,11 +1371,12 @@ | |
| 767 | const char *z; |
| 768 | content_get(ridSrc, &attach); |
| 769 | blob_to_utf8_no_bom(&attach, 0); |
| 770 | z = blob_str(&attach); |
| 771 | if( zLn ){ |
| 772 | output_text_with_line_numbers(z, blob_size(&attach), zName, zLn, 1); |
| 773 | }else{ |
| 774 | @ <pre> |
| 775 | @ %h(z) |
| 776 | @ </pre> |
| 777 | } |
| @@ -798,10 +1403,13 @@ | |
| 798 | */ |
| 799 | #define ATTACHLIST_HRULE_ABOVE 0x01 /* Insert <hr> above header */ |
| 800 | #define ATTACHLIST_TARGET_BLANK 0x02 /* use target=_blank for links */ |
| 801 | #define ATTACHLIST_SIZE 0x04 /* add size */ |
| 802 | #define ATTACHLIST_HIDE_UNAPPROVED 0x08 /* Hide pending-moderation files */ |
| 803 | #endif |
| 804 | |
| 805 | /* |
| 806 | ** Output HTML to show a list of attachments. |
| 807 | */ |
| @@ -810,19 +1418,25 @@ | |
| 810 | const char *zHeader, /* Header to display with attachments */ |
| 811 | const int flags /* ATTACHLIST_... flags */ |
| 812 | ){ |
| 813 | int cnt = 0; |
| 814 | char szBuf[36] = {0}; /* scratchpad for attachment size value */ |
| 815 | const char * zLinkTgt = (ATTACHLIST_TARGET_BLANK & flags) |
| 816 | ? " target=\"_blank\"" : ""; |
| 817 | Stmt q; |
| 818 | db_prepare(&q, |
| 819 | "SELECT datetime(mtime,toLocal()), filename, user," |
| 820 | " (SELECT uuid FROM blob WHERE rid=attachid), src, target, " |
| 821 | " attachid " |
| 822 | " FROM attachment" |
| 823 | " WHERE isLatest AND src!='' AND target=%Q" |
| 824 | " ORDER BY mtime DESC", |
| 825 | zTarget |
| 826 | ); |
| 827 | while( db_step(&q)==SQLITE_ROW ){ |
| 828 | const char *zDate = db_column_text(&q, 0); |
| @@ -832,36 +1446,51 @@ | |
| 832 | const char *zSrc = db_column_text(&q, 4); |
| 833 | const char *zTarget = db_column_text(&q, 5); |
| 834 | const char *zDispUser = zUser && zUser[0] ? zUser : "anonymous"; |
| 835 | const char *zTypeArg = 0; /* URL arg name for /attachdownload */ |
| 836 | const int aid = db_column_int(&q, 6); |
| 837 | const int iAType = attachment_target_type(zTarget); |
| 838 | if( (flags & ATTACHLIST_HIDE_UNAPPROVED) |
| 839 | && moderation_pending(aid) |
| 840 | && !moderation_user_could(aid, 1, 0) ){ |
| 841 | continue; |
| 842 | } |
| 843 | if( cnt==0 ){ |
| 844 | @ <section class='attachlist'> |
| 845 | if( flags & ATTACHLIST_HRULE_ABOVE ){ |
| 846 | @ <hr> |
| 847 | } |
| 848 | @ %s(zHeader) |
| 849 | @ <ul> |
| 850 | } |
| 851 | cnt++; |
| 852 | switch( iAType ){ |
| 853 | case CFTYPE_TICKET: zTypeArg = "tkt"; break; |
| 854 | case CFTYPE_FORUM: zTypeArg = "forumpost"; break; |
| 855 | case CFTYPE_EVENT: zTypeArg = "technote"; break; |
| 856 | case CFTYPE_WIKI: |
| 857 | default: zTypeArg = "page"; break; |
| 858 | } |
| 859 | @ <li> |
| 860 | @ <a href="%R/artifact/%!S(zSrc)"%s(zLinkTgt)>%h(zFile)</a> |
| 861 | if( flags & ATTACHLIST_SIZE ){ |
| 862 | const int sz = db_int(0,"SELECT size FROM blob WHERE uuid=%Q", zSrc); |
| 863 | sqlite3_snprintf(sizeof(szBuf), szBuf, " %d bytes", sz); |
| 864 | } |
| 865 | @ [<a href="%R/attachdownload/%t(zFile)?%s(zTypeArg)=%t(zTarget)\ |
| 866 | @&file=%t(zFile)%s(zLinkTgt)">download</a>%s(szBuf)] |
| 867 | @ added by %h(zDispUser) on |
| @@ -870,11 +1499,15 @@ | |
| 870 | moderation_pending_www(aid); |
| 871 | @ </li> |
| 872 | } |
| 873 | if( cnt ){ |
| 874 | @ </ul> |
| 875 | @ </section> |
| 876 | } |
| 877 | db_finalize(&q); |
| 878 | } |
| 879 | |
| 880 | /* |
| @@ -1023,15 +1656,204 @@ | |
| 1023 | } |
| 1024 | for(i = 2; i < g.argc; ++i){ |
| 1025 | const char *zPage = g.argv[i]; |
| 1026 | db_bind_text(&q, ":tgtname", zPage); |
| 1027 | while(SQLITE_ROW == db_step(&q)){ |
| 1028 | const char * zTime = db_column_text(&q, 0); |
| 1029 | const char * zSrc = db_column_text(&q, 1); |
| 1030 | const char * zTarget = db_column_text(&q, 2); |
| 1031 | const char * zName = db_column_text(&q, 3); |
| 1032 | printf("%-20s %s %.12s %s\n", zTarget, zTime, zSrc, zName); |
| 1033 | } |
| 1034 | db_reset(&q); |
| 1035 | } |
| 1036 | db_finalize(&q); |
| 1037 | } |
| 1038 |
| --- src/attach.c | |
| +++ src/attach.c | |
| @@ -22,41 +22,188 @@ | |
| 22 | #include <assert.h> |
| 23 | |
| 24 | /* |
| 25 | ** Given a presumedly legal attachment target name, this guesses the |
| 26 | ** target type and returns one of CFTYPE_FORUM, CFTYPE_WIKI, |
| 27 | ** CFTYPE_TICKET, or CFTYPE_EVENT. Returns 0 if it cannot distinguish |
| 28 | ** the target type. |
| 29 | ** |
| 30 | ** zTarget is an attachment target name: wiki page name, tech-note ID, |
| 31 | ** ticket ID, or forumpost hash. |
| 32 | ** |
| 33 | ** If bFull is true then it requires zTarget to be a full ID for |
| 34 | ** tech-notes and tickets, otherwise such IDs may be prefixes. If |
| 35 | ** bFull is false then tech-notes and tickets will perform a prefix |
| 36 | ** match, but it is up to the caller to provide enough of a prefix to |
| 37 | ** rule out ambiguity[^1]. When called repeatedly, this routine can |
| 38 | ** run a bit faster and more efficiently if bFull is true, but some |
| 39 | ** historical use cases call for prefix matches. |
| 40 | ** |
| 41 | ** Wiki page names always require an exact match. |
| 42 | ** |
| 43 | ** Forum posts are a special case: |
| 44 | ** |
| 45 | ** - They ignore the bFull flag. That is, they will do prefix matches |
| 46 | ** but will not match an ambiguous prefix. |
| 47 | ** |
| 48 | ** - It is up to the caller to, if needed, resolve zTarget using |
| 49 | ** forumpost_head_rid2() to resolve the RID of the earliest version |
| 50 | ** of the post, as that is the only one which attachments should |
| 51 | ** target. |
| 52 | ** |
| 53 | ** [^1]: Historically (from the perspective of 2026-06) attachment |
| 54 | ** target lookups have used GLOB prefix matching but have taken no |
| 55 | ** measures to ensure that the prefix is unambiguous. Ergo we do the |
| 56 | ** same here. It is assumed that the caller passes enough of a prefix |
| 57 | ** to be unambiguous and that's worked out fine so far. |
| 58 | */ |
| 59 | int attachment_target_type(const char *zTarget, int bFull){ |
| 60 | if( !zTarget || !zTarget[0] || strlen(zTarget)>64/*vs. abuse*/ ){ |
| 61 | return 0; |
| 62 | } |
| 63 | if( symbolic_name_to_rid(zTarget, "f")>0 ){ |
| 64 | /* Check forum posts first because they are the most likely target |
| 65 | ** as of 2026. We should arguably use something more |
| 66 | ** specialized/efficient than symbolic_name_to_rid(). */ |
| 67 | return CFTYPE_FORUM; |
| 68 | } |
| 69 | if( bFull ){ |
| 70 | static Stmt q = empty_Stmt_m; |
| 71 | int rc = 0; |
| 72 | if( !q.pStmt ){ |
| 73 | db_static_prepare( |
| 74 | &q, |
| 75 | "SELECT CASE " |
| 76 | /* Ordered by presumed likelihood of attachments. */ |
| 77 | "WHEN (SELECT 1 FROM tag WHERE tagname='tkt-'||:tgt) THEN %d\n" |
| 78 | "WHEN (SELECT 1 FROM tag WHERE tagname='wiki-'||:tgt) THEN %d\n" |
| 79 | "WHEN (SELECT 1 FROM tag WHERE tagname='event-'||:tgt) THEN %d\n" |
| 80 | "ELSE 0 END", |
| 81 | CFTYPE_TICKET, CFTYPE_WIKI, CFTYPE_EVENT |
| 82 | ); |
| 83 | } |
| 84 | db_bind_text(&q, ":tgt", zTarget); |
| 85 | if( SQLITE_ROW==db_step(&q) ){ |
| 86 | rc = db_column_int(&q, 0); |
| 87 | } |
| 88 | db_reset(&q); |
| 89 | return rc; |
| 90 | }else{ |
| 91 | return db_int( |
| 92 | 0, |
| 93 | "SELECT CASE " |
| 94 | "WHEN (SELECT tagid FROM tag WHERE tagname GLOB 'tkt-%q*')" |
| 95 | " THEN %d\n" |
| 96 | "WHEN (SELECT tagid FROM tag WHERE tagname='wiki-%q')" |
| 97 | " THEN %d\n" |
| 98 | "WHEN (SELECT tagid FROM tag WHERE tagname GLOB 'event-%q*')" |
| 99 | " THEN %d\n" |
| 100 | "ELSE 0 END", |
| 101 | zTarget, CFTYPE_TICKET, |
| 102 | zTarget, CFTYPE_WIKI, |
| 103 | zTarget, CFTYPE_EVENT |
| 104 | ); |
| 105 | } |
| 106 | } |
| 107 | |
| 108 | /* |
| 109 | ** Given an attachment target name, returns the target's blob.rid. |
| 110 | ** zTarget and bFull work as described for attachment_target_type(). |
| 111 | ** |
| 112 | ** For forum posts, this always returns the RID of the first version |
| 113 | ** of the post, as attachments should always target that instance. |
| 114 | */ |
| 115 | int attachment_target_rid(const char *zTarget, int bFull){ |
| 116 | int rid = 0; |
| 117 | const int eType = attachment_target_type(zTarget, bFull); |
| 118 | switch(eType){ |
| 119 | case CFTYPE_TICKET: |
| 120 | case CFTYPE_EVENT:{ |
| 121 | const char *zTagPrefix = (eType==CFTYPE_EVENT) ? "event" : "tkt"; |
| 122 | rid = db_int( |
| 123 | 0, "SELECT b.rid FROM blob b, tag t, tagxref x\n" |
| 124 | "WHERE tagname %s '%s-%q%s'\n" |
| 125 | "AND x.tagtype>0\n" |
| 126 | "AND x.tagid=t.tagid\n" |
| 127 | "AND x.rid=b.rid\n" |
| 128 | "ORDER BY x.mtime DESC", |
| 129 | bFull ? "=" : "GLOB"/*safe-for-%s*/, |
| 130 | zTagPrefix/*safe-for-%s*/, |
| 131 | zTarget, |
| 132 | bFull ? "" : "*"/*safe-for-%s*/ |
| 133 | ); |
| 134 | break; |
| 135 | } |
| 136 | case CFTYPE_FORUM: |
| 137 | rid = db_int( |
| 138 | 0, "SELECT f.fpid FROM forumpost f, blob b\n" |
| 139 | "WHERE f.fpid=b.rid\n" |
| 140 | "AND b.uuid %s '%q%s'", |
| 141 | bFull ? "=" : "GLOB"/*safe-for-%s*/, |
| 142 | zTarget, |
| 143 | bFull ? "" : "*"/*safe-for-%s*/ |
| 144 | ); |
| 145 | if( rid>0 ){ |
| 146 | rid = forumpost_head_rid(rid); |
| 147 | } |
| 148 | break; |
| 149 | case CFTYPE_WIKI: |
| 150 | rid = db_int( |
| 151 | 0, "SELECT b.rid FROM blob b, tag t, tagxref x\n" |
| 152 | "WHERE tagname='wiki-%q'\n" |
| 153 | "AND x.tagtype>0\n" |
| 154 | "AND x.tagid=t.tagid\n" |
| 155 | "AND x.rid=b.rid\n" |
| 156 | "ORDER BY x.mtime DESC", |
| 157 | zTarget |
| 158 | ); |
| 159 | break; |
| 160 | default: |
| 161 | break; |
| 162 | } |
| 163 | return rid; |
| 164 | } |
| 165 | |
| 166 | /* |
| 167 | ** For a given aritfact ID and type (from the CFTYPE_xyz enum), |
| 168 | ** returns true if the current user could hypothetically apply and |
| 169 | ** attachment to it, else returns 0. |
| 170 | ** |
| 171 | ** The rid is currently only relevant when eArtifactType is |
| 172 | ** CFTYPE_FORUM. For forum posts, it checks precisely the rid given, |
| 173 | ** not the head RID, to keep non-admins from attaching files to |
| 174 | ** threads which have since been taken over by another user (this |
| 175 | ** happens when an admin edits another user's post). |
| 176 | */ |
| 177 | int attach_user_may(int rid, int eArtifactType){ |
| 178 | if( g.perm.Admin ) return 1; |
| 179 | if( !login_is_individual() ) return 0; |
| 180 | switch(eArtifactType){ |
| 181 | case CFTYPE_FORUM: |
| 182 | return g.perm.AttachForum && forumpost_is_owner(rid, 0); |
| 183 | case CFTYPE_WIKI: |
| 184 | return g.perm.ApndWiki && g.perm.Attach; |
| 185 | case CFTYPE_TICKET: |
| 186 | return g.perm.ApndTkt && g.perm.Attach; |
| 187 | case CFTYPE_EVENT: |
| 188 | return g.perm.Write && g.perm.ApndWiki && g.perm.Attach; |
| 189 | default: |
| 190 | return 0; |
| 191 | } |
| 192 | } |
| 193 | |
| 194 | /* |
| 195 | ** Emits a single-button FORM which invokes |
| 196 | ** /attachadd with target=$zTarget. |
| 197 | */ |
| 198 | void attach_render_attachadd_button(const char *zTarget){ |
| 199 | /* This could be changed from POST to GET, and arguably should so |
| 200 | ** that the target=X part becomes part of the resulting URL. */ |
| 201 | @ <form method="post" action="%R/attachadd">\ |
| 202 | @ <input type="hidden" name="target" value="%T(zTarget)">\ |
| 203 | @ <input type="submit" value="Attach..."> |
| 204 | @ </form>\ |
| 205 | } |
| 206 | |
| 207 | /* |
| 208 | ** WEBPAGE: attachlist |
| 209 | ** List attachments. |
| @@ -69,20 +216,21 @@ | |
| 216 | ** At most one of technote=, tkt=, forumpost=, or page= may be supplied. |
| 217 | ** |
| 218 | ** If none are given, all attachments are listed. If one is given, only |
| 219 | ** attachments for the designated technote, ticket or wiki page are shown. |
| 220 | ** |
| 221 | ** HASH may be just a prefix of the relevant forum post, technical |
| 222 | ** note, or ticket artifact hash, in which case all attachments of all |
| 223 | ** technical notes or tickets with the prefix will be listed. Forum |
| 224 | ** posts, on the other hand, require a unique hash or hash prefix. |
| 225 | */ |
| 226 | void attachlist_page(void){ |
| 227 | const char *zPage = P("page"); |
| 228 | const char *zTkt = P("tkt"); |
| 229 | const char *zTechNote = P("technote"); |
| 230 | const char *zForumPost = P("forumpost"); |
| 231 | char *zLink = 0; |
| 232 | Blob sql; |
| 233 | Stmt q; |
| 234 | |
| 235 | if( zPage && zTkt ) zTkt = 0; |
| 236 | login_check_credentials(); |
| @@ -102,32 +250,47 @@ | |
| 250 | if( fnid<=0 ){ |
| 251 | webpage_error("Invalid forum post ID: %h", zForumPost); |
| 252 | } |
| 253 | blob_append_sql(&sql, " WHERE target=" |
| 254 | "(SELECT uuid FROM blob WHERE rid=%d)", fnid); |
| 255 | zLink = mprintf("forum post <a href='%R/forumpost/%t'>%#h</a>", |
| 256 | zForumPost, hash_digits(0), zForumPost); |
| 257 | }else if( zPage ){ |
| 258 | if( g.perm.RdWiki==0 ){ login_needed(g.anon.RdWiki); return; } |
| 259 | style_header("Attachments To Wiki page %h", zPage); |
| 260 | blob_append_sql(&sql, " WHERE target=%Q", zPage); |
| 261 | zLink = mprintf("wiki page <a href='%R/wiki?name=%t'>%h</a>", |
| 262 | zPage, zPage); |
| 263 | }else if( zTkt ){ |
| 264 | if( g.perm.RdTkt==0 ){ login_needed(g.anon.RdTkt); return; } |
| 265 | style_header("Attachments To Ticket %S", zTkt); |
| 266 | blob_append_sql(&sql, " WHERE target GLOB '%q*'", zTkt); |
| 267 | zLink = mprintf("ticket <a href='%R/tktview?name=%t'>%#h</a>", |
| 268 | zTkt, hash_digits(0), zTkt); |
| 269 | }else if( zTechNote ){ |
| 270 | if( g.perm.RdWiki==0 ){ login_needed(g.anon.RdWiki); return; } |
| 271 | style_header("Attachments To Tech Note %S", zTechNote); |
| 272 | blob_append_sql(&sql, " WHERE target GLOB '%q*'", |
| 273 | zTechNote); |
| 274 | zLink = mprintf("tech-note <a href='%R/technote?name=%t'>%#h</a>", |
| 275 | zTechNote, hash_digits(0), zTechNote); |
| 276 | }else{ |
| 277 | if( g.perm.RdTkt==0 && g.perm.RdWiki==0 ){ |
| 278 | login_needed(g.anon.RdTkt || g.anon.RdWiki); |
| 279 | return; |
| 280 | } |
| 281 | style_header("All Attachments"); |
| 282 | } |
| 283 | blob_append_sql(&sql, " ORDER BY mtime DESC"); |
| 284 | db_prepare(&q, "%s", blob_sql_text(&sql)); |
| 285 | |
| 286 | if( zLink ){ |
| 287 | @ <h2>Attachments for %s(zLink)</h2> |
| 288 | fossil_free(zLink); |
| 289 | zLink = 0; |
| 290 | } |
| 291 | |
| 292 | @ <ol> |
| 293 | while( db_step(&q)==SQLITE_ROW ){ |
| 294 | const char *zDate; |
| 295 | const char *zSrc; |
| 296 | const char *zTarget; |
| @@ -137,10 +300,11 @@ | |
| 300 | const char *zUuid; |
| 301 | const char *zDispUser; |
| 302 | const int attachid = db_column_int(&q, 7); |
| 303 | int type; |
| 304 | int i; |
| 305 | int bDeleted; |
| 306 | char *zUrlTail = 0; |
| 307 | |
| 308 | if( moderation_pending(attachid) |
| 309 | && !moderation_user_could(attachid, 1, 0) ){ |
| 310 | /* Elide entries which are currently pending moderation unless |
| @@ -160,11 +324,12 @@ | |
| 324 | if( zFilename[i]=='/' && zFilename[i+1]!=0 ){ |
| 325 | zFilename = &zFilename[i+1]; |
| 326 | i = -1; |
| 327 | } |
| 328 | } |
| 329 | bDeleted = 0==zSrc || 0==zSrc[0]; |
| 330 | type = attachment_target_type(zTarget, 1); |
| 331 | switch( type ){ |
| 332 | case CFTYPE_TICKET: |
| 333 | zUrlTail = mprintf("tkt=%s&file=%t", zTarget, zFilename); |
| 334 | break; |
| 335 | case CFTYPE_EVENT: |
| @@ -176,20 +341,32 @@ | |
| 341 | case CFTYPE_WIKI: |
| 342 | zUrlTail = mprintf("page=%t&file=%t", zTarget, zFilename); |
| 343 | break; |
| 344 | } |
| 345 | @ <li><p> |
| 346 | if( bDeleted ){ |
| 347 | @ <s>\ |
| 348 | } |
| 349 | @ Attachment %z(href("%R/ainfo/%!S",zUuid))%S(zUuid)</a>\ |
| 350 | moderation_pending_www(attachid); |
| 351 | @ <br>\ |
| 352 | @ <a href="%R/attachview?%s(zUrlTail)">%h(zFilename)</a> |
| 353 | @ [<a href="%R/attachdownload/%t(zFilename)?%s(zUrlTail)">download</a>]\ |
| 354 | if( bDeleted ){ |
| 355 | @ </s> |
| 356 | } |
| 357 | @ <br> |
| 358 | if( zComment ) while( fossil_isspace(zComment[0]) ) zComment++; |
| 359 | if( zComment && zComment[0] ){ |
| 360 | /* FIXME (2026-06-05): Honor the N-card (comment mimetype). %W |
| 361 | ** (historically used here) assumes fossil-wiki and the |
| 362 | ** fileformat.wiki doc has always claimed that it defaults to |
| 363 | ** text/plain. /ainfo assumes it is plain text. */ |
| 364 | @ %h(zComment)<br> |
| 365 | } |
| 366 | if( zForumPost==0 && zPage==0 && zTkt==0 && zTechNote==0 ){ |
| 367 | if( bDeleted ){ |
| 368 | zSrc = "Deleted from"; |
| 369 | }else { |
| 370 | zSrc = "Added to"; |
| 371 | } |
| 372 | switch( type ){ |
| @@ -362,41 +539,81 @@ | |
| 539 | Manifest *pManifest; |
| 540 | |
| 541 | db_begin_transaction(); |
| 542 | blob_init(&content, aContent, szContent); |
| 543 | pManifest = manifest_parse(&content, 0, 0); |
| 544 | addCompress = pManifest!=0; |
| 545 | manifest_destroy(pManifest); |
| 546 | blob_init(&content, aContent, szContent); |
| 547 | if( addCompress ){ |
| 548 | blob_compress(&content, &content); |
| 549 | } |
| 550 | rid = content_put_ex(&content, 0, 0, 0, needModerator); |
| 551 | zUUID = rid_to_uuid(rid); |
| 552 | blob_zero(&manifest); |
| 553 | for(i=n=0; zName[i]; i++){ |
| 554 | if( zName[i]=='/' || zName[i]=='\\' ) n = i+1; |
| 555 | } |
| 556 | zName += n; |
| 557 | if( zName[0]==0 ) zName = "unknown"; |
| 558 | blob_appendf(&manifest, "A %F%s %F %s\n", |
| 559 | zName, addCompress ? ".gz" : "", zTarget, zUUID); |
| 560 | if( zComment!=0 && zComment[0]!=0 ){ |
| 561 | while( fossil_isspace(zComment[0]) ) zComment++; |
| 562 | n = strlen(zComment); |
| 563 | while( n>0 && fossil_isspace(zComment[n-1]) ){ n--; } |
| 564 | if( n>0 ){ |
| 565 | blob_appendf(&manifest, "C %#F\n", n, zComment); |
| 566 | } |
| 567 | } |
| 568 | zDate = date_in_standard_format("now"); |
| 569 | blob_appendf(&manifest, "D %z\n", zDate); |
| 570 | blob_appendf(&manifest, "U %F\n", login_name()); |
| 571 | md5sum_blob(&manifest, &cksum); |
| 572 | blob_appendf(&manifest, "Z %b\n", &cksum); |
| 573 | attach_put(&manifest, rid, needModerator); |
| 574 | assert( blob_is_reset(&manifest) ); |
| 575 | db_end_transaction(0); |
| 576 | } |
| 577 | |
| 578 | /* |
| 579 | ** Renders the "legacy" (static) /attachadd form. One of the first |
| 580 | ** four arguments must be non-NULL and the other three must be NULL. |
| 581 | ** zComment may be NULL, as may zFrom. See the call sites for more |
| 582 | ** context. |
| 583 | */ |
| 584 | static void attach_render_legacy_form(const char *zForumPost, |
| 585 | const char *zTechNote, |
| 586 | const char *zTicket, |
| 587 | const char *zWikiPage, |
| 588 | const char *zComment, |
| 589 | const char *zFrom){ |
| 590 | form_begin("enctype='multipart/form-data' id='attachadd-legacy-form'", |
| 591 | "%R/attachadd"); |
| 592 | @ <div>\ |
| 593 | @ File to Attach: |
| 594 | @ <input type="file" name="f" size="60"><br> |
| 595 | @ Description:<br> |
| 596 | @ <textarea name="comment" cols="80" rows="5" wrap="virtual"\ |
| 597 | @ >%h(zComment)</textarea><br> |
| 598 | if( zForumPost ){ |
| 599 | @ <input type="hidden" name="forumpost" value="%h(zForumPost)">\ |
| 600 | }else if( zTicket ){ |
| 601 | @ <input type="hidden" name="tkt" value="%h(zTicket)">\ |
| 602 | }else if( zTechNote ){ |
| 603 | @ <input type="hidden" name="technote" value="%h(zTechNote)">\ |
| 604 | }else if( zWikiPage ){ |
| 605 | @ <input type="hidden" name="page" value="%h(zWikiPage)">\ |
| 606 | } |
| 607 | @ <input type="hidden" name="from" value="%h(zFrom)">\ |
| 608 | @ <input type="submit" name="ok" value="Add Attachment">\ |
| 609 | @ <input type="submit" name="cancel" value="Cancel">\ |
| 610 | @ </div> |
| 611 | captcha_generate(0); |
| 612 | login_insert_csrf_secret(); |
| 613 | @ </form> |
| 614 | } |
| 615 | |
| 616 | /* |
| 617 | ** WEBPAGE: attachadd |
| 618 | ** Add a new attachment. |
| 619 | ** |
| @@ -404,33 +621,54 @@ | |
| 621 | ** page=WIKIPAGE |
| 622 | ** technote=HASH |
| 623 | ** forumpost=HASH |
| 624 | ** from=URL |
| 625 | ** |
| 626 | ** Adds a POSTed file attachment to the given target. |
| 627 | ** |
| 628 | ** Or the "version 2" interface: |
| 629 | ** |
| 630 | ** target=ATTACHMENT_TARGET |
| 631 | ** |
| 632 | ** Behaves as documented for attachaddV2_page(). |
| 633 | */ |
| 634 | void attachadd_page(void){ |
| 635 | const char *zPage; |
| 636 | const char *zForumPost; |
| 637 | const char *zTkt; |
| 638 | const char *zTechNote; |
| 639 | const char *aContent; |
| 640 | const char *zName; |
| 641 | const char *zComment; |
| 642 | const char *zTarget; |
| 643 | const char *zFrom; /* Origin page - redirect here after saving */ |
| 644 | char *zTo = 0; /* Optionally redirect here after saving */ |
| 645 | char *zTargetType = 0; |
| 646 | char *zExtraFree = 0; |
| 647 | int szContent; |
| 648 | int goodCaptcha = 1; |
| 649 | int szLimit = 0; |
| 650 | |
| 651 | if( P("target")!=0 ){ |
| 652 | attachaddV2_page(); |
| 653 | return; |
| 654 | } |
| 655 | zPage = P("page"); |
| 656 | zForumPost = P("forumpost"); |
| 657 | zTkt = P("tkt"); |
| 658 | zTechNote = P("technote"); |
| 659 | zFrom = P("from"); |
| 660 | aContent = P("f"); |
| 661 | zName = PD("f:filename","unknown"); |
| 662 | zComment = PD("comment", ""); |
| 663 | szContent = atoi(PD("f:bytes","0")); |
| 664 | |
| 665 | if( zFrom==0 ) zFrom = mprintf("%R/home"); |
| 666 | if( P("cancel") ) cgi_redirect(zFrom); |
| 667 | if( (!!zPage + !!zTkt + !!zTechNote + !!zForumPost)!=1 ){ |
| 668 | webpage_error("Requires exactly one one: page=X, tkt=X, forumpost=X," |
| 669 | " technote=X, or target=X"); |
| 670 | } |
| 671 | login_check_credentials(); |
| 672 | if( zForumPost ){ |
| 673 | int fpid; |
| 674 | if( g.perm.AttachForum==0 ){ |
| @@ -445,26 +683,23 @@ | |
| 683 | "forum posts."); |
| 684 | } |
| 685 | zTarget = zExtraFree = rid_to_uuid(fpid); |
| 686 | zTargetType = mprintf("Forum post <a href=\"%R/forumpost/%S\">%h</a>", |
| 687 | zTarget, zForumPost); |
| 688 | zTo = zFrom ? 0 : mprintf("%R/forumpost/%S", zTarget); |
| 689 | }else if( zPage ){ |
| 690 | if( g.perm.ApndWiki==0 || g.perm.Attach==0 ){ |
| 691 | login_needed(g.anon.ApndWiki && g.anon.Attach); |
| 692 | return; |
| 693 | } |
| 694 | if( !db_exists("SELECT 1 FROM tag WHERE tagname='wiki-%q'", zPage) ){ |
| 695 | fossil_redirect_home(); |
| 696 | } |
| 697 | zTarget = zPage; |
| 698 | zTargetType = mprintf("Wiki Page <a href=\"%R/wiki?name=%t\">%h</a>", |
| 699 | zPage, zPage); |
| 700 | zTo = zFrom ? 0 : mprintf("%R/wiki?name=%T", zTarget); |
| 701 | }else if ( zTechNote ){ |
| 702 | if( g.perm.Write==0 || g.perm.ApndWiki==0 || g.perm.Attach==0 ){ |
| 703 | login_needed(g.anon.Write && g.anon.ApndWiki && g.anon.Attach); |
| 704 | return; |
| 705 | } |
| @@ -474,11 +709,11 @@ | |
| 709 | if( zTechNote==0) fossil_redirect_home(); |
| 710 | } |
| 711 | zTarget = zTechNote; |
| 712 | zTargetType = mprintf("Tech Note <a href=\"%R/technote/%s\">%S</a>", |
| 713 | zTechNote, zTechNote); |
| 714 | zTo = zFrom ? 0 : mprintf("%R/technote/%S", zTarget); |
| 715 | }else{ |
| 716 | assert( zTkt ); |
| 717 | if( g.perm.ApndTkt==0 || g.perm.Attach==0 ){ |
| 718 | login_needed(g.anon.ApndTkt && g.anon.Attach); |
| 719 | return; |
| @@ -487,21 +722,25 @@ | |
| 722 | zTkt = db_text(0, "SELECT substr(tagname,5) FROM tag" |
| 723 | " WHERE tagname GLOB 'tkt-%q*'", zTkt); |
| 724 | if( zTkt==0 ) fossil_redirect_home(); |
| 725 | } |
| 726 | zTarget = zTkt; |
| 727 | zTargetType = mprintf("Ticket <a href=\"%R/tktview/%S\">%S</a>", |
| 728 | zTkt, zTkt); |
| 729 | zTo = zFrom ? 0 : mprintf("%R/tktview/%S", zTarget); |
| 730 | } |
| 731 | szLimit = db_get_int("attachment-size-limit", 0); |
| 732 | if( szContent<0 || (szLimit && szContent>szLimit) ){ |
| 733 | /* This check must be done late so that zTargetType is set up. */ |
| 734 | @ <p class="generalError">Attachment %h(zName) is too large. |
| 735 | @ <a href="%R/help/attachment-size-limit">Limit</a> is |
| 736 | @ %d(szLimit ? szLimit : 0x7fffffff) bytes</p> |
| 737 | /* Fall through and render form. */ |
| 738 | }else if( P("ok") |
| 739 | && cgi_csrf_safe(2) |
| 740 | && szContent>0 |
| 741 | && (goodCaptcha = captcha_is_correct(0)) ){ |
| 742 | int needModerator = (zForumPost!=0 && forum_need_moderation()) || |
| 743 | (zTkt!=0 && ticket_need_moderation(0)) || |
| 744 | (zPage!=0 && wiki_need_moderation(0)); |
| 745 | attach_commit(zName, zTarget, aContent, szContent, needModerator, zComment); |
| 746 | cgi_redirect(zTo ? zTo : zFrom); |
| @@ -511,35 +750,374 @@ | |
| 750 | style_header("Add Attachment"); |
| 751 | if( !goodCaptcha ){ |
| 752 | @ <p class="generalError">Error: Incorrect security code.</p> |
| 753 | } |
| 754 | @ <h2>Add Attachment To %s(zTargetType)</h2> |
| 755 | attach_render_legacy_form(zForumPost, zTechNote, zTkt, zPage, |
| 756 | zComment, zFrom); |
| 757 | builtin_fossil_js_bundle_or("attach", NULL); |
| 758 | style_finish_page(); |
| 759 | fossil_free(zTargetType); |
| 760 | fossil_free(zExtraFree); |
| 761 | } |
| 762 | |
| 763 | /* |
| 764 | ** WEBPAGE: attachadd_ajax_post hidden |
| 765 | ** |
| 766 | ** Used by attachadd V2 to handle attachments via POST requests with: |
| 767 | ** |
| 768 | ** target=ATTACHMENT_TARGET |
| 769 | ** file1..fileN=FILE_OBJECTS |
| 770 | ** dryrun=0|1 |
| 771 | ** |
| 772 | ** Each posted file in the set file1..fileN gets attached to the given |
| 773 | ** target, permissions permitting. If dryrun>0 then the change is |
| 774 | ** rolled back instead of committed. target=X must refer to a full |
| 775 | ** target ID, not a prefix. |
| 776 | ** |
| 777 | ** Responds with JSON: an empty object on success and |
| 778 | ** {error:"message"} on error. The on-success response structure is |
| 779 | ** subject to amendment. |
| 780 | */ |
| 781 | void attachadd_ajax_post(void){ |
| 782 | const char *zTarget; |
| 783 | char *zExtraFree = 0; |
| 784 | int eTgtType = 0; |
| 785 | int bNeedsModeration = 0; |
| 786 | int goodCaptcha = 1; |
| 787 | int bRollback = 0; /* Roll back if true. */ |
| 788 | |
| 789 | if( ! ajax_route_bootstrap(0, 1) ){ |
| 790 | return; |
| 791 | }else if( !(goodCaptcha = captcha_is_correct(0)) ){ |
| 792 | goto ajax_err_403; |
| 793 | }else if( !ajax_check_csrf(2) ){ |
| 794 | return; |
| 795 | } |
| 796 | db_begin_transaction(); |
| 797 | zTarget = P("target"); |
| 798 | eTgtType = attachment_target_type(zTarget, 1); |
| 799 | CX("{"); |
| 800 | switch( eTgtType ){ |
| 801 | default: |
| 802 | case 0: |
| 803 | ajax_route_error(400, "Invalid attachment target."); |
| 804 | db_rollback_transaction(); |
| 805 | return; |
| 806 | case CFTYPE_FORUM:{ |
| 807 | int fpid; |
| 808 | if( g.perm.AttachForum==0 ){ |
| 809 | goto ajax_err_403; |
| 810 | } |
| 811 | fpid = forumpost_head_rid2(zTarget); |
| 812 | if( fpid<=0 ){ |
| 813 | goto ajax_err_404; |
| 814 | }else if( !g.perm.Admin && !forumpost_is_owner(fpid, 0) ){ |
| 815 | ajax_route_error(403, "Only admins can attach files to " |
| 816 | "other users' forum posts."); |
| 817 | db_rollback_transaction(); |
| 818 | return; |
| 819 | } |
| 820 | zTarget = zExtraFree = rid_to_uuid(fpid); |
| 821 | bNeedsModeration = forum_need_moderation(); |
| 822 | break; |
| 823 | } |
| 824 | case CFTYPE_EVENT:{ |
| 825 | if( g.perm.Write==0 || g.perm.ApndWiki==0 || g.perm.Attach==0 ){ |
| 826 | goto ajax_err_403; |
| 827 | } |
| 828 | if( !db_exists("SELECT 1 FROM tag WHERE tagname='event-%q'", |
| 829 | zTarget) ){ |
| 830 | zTarget = zExtraFree = |
| 831 | db_text(0, "SELECT substr(tagname,7) FROM tag" |
| 832 | " WHERE tagname GLOB 'event-%q*'", zTarget); |
| 833 | if( zTarget==0){ |
| 834 | goto ajax_err_404; |
| 835 | } |
| 836 | } |
| 837 | bNeedsModeration = 0; |
| 838 | break; |
| 839 | } |
| 840 | case CFTYPE_TICKET:{ |
| 841 | if( g.perm.ApndTkt==0 || g.perm.Attach==0 ){ |
| 842 | goto ajax_err_403; |
| 843 | } |
| 844 | if( !db_exists("SELECT 1 FROM tag WHERE tagname='tkt-%q'", |
| 845 | zTarget) ){ |
| 846 | zTarget = db_text(0, "SELECT substr(tagname,5) FROM tag" |
| 847 | " WHERE tagname GLOB 'tkt-%q*'", zTarget); |
| 848 | if( zTarget==0 ){ |
| 849 | goto ajax_err_404; |
| 850 | } |
| 851 | } |
| 852 | bNeedsModeration = ticket_need_moderation(0); |
| 853 | break; |
| 854 | } |
| 855 | case CFTYPE_WIKI:{ |
| 856 | if( g.perm.ApndWiki==0 || g.perm.Attach==0 ){ |
| 857 | goto ajax_err_403; |
| 858 | } |
| 859 | if( !db_exists("SELECT 1 FROM tag WHERE tagname='wiki-%q'", |
| 860 | zTarget) ){ |
| 861 | goto ajax_err_404; |
| 862 | } |
| 863 | bNeedsModeration = wiki_need_moderation(0); |
| 864 | break; |
| 865 | } |
| 866 | } |
| 867 | |
| 868 | if( attachments_ajax_from_POST(zTarget, bNeedsModeration)>=0 ){ |
| 869 | CX("}"); |
| 870 | if( atoi(PD("dryrun","0"))>0 ){ |
| 871 | bRollback = 1; |
| 872 | } |
| 873 | }/*else error response was set up*/ |
| 874 | fossil_free(zExtraFree); |
| 875 | db_end_transaction(bRollback); |
| 876 | return; |
| 877 | ajax_err_403: |
| 878 | if( db_transaction_nesting_depth()>0 ){ |
| 879 | db_rollback_transaction(); |
| 880 | } |
| 881 | ajax_route_error_forbidden(); |
| 882 | return; |
| 883 | ajax_err_404: |
| 884 | assert( db_transaction_nesting_depth()>0 ); |
| 885 | db_rollback_transaction(); |
| 886 | ajax_route_error(404, "Target not found."); |
| 887 | return; |
| 888 | } |
| 889 | |
| 890 | /* |
| 891 | ** A helper for AJAX-style routines which accept file attachments via |
| 892 | ** POST. zTarget must be a full attachment target. bNeedsModeration |
| 893 | ** must be true if the attachment requires moderation. |
| 894 | ** |
| 895 | ** It is up to the caller to have validated all security measures |
| 896 | ** before calling this. |
| 897 | ** |
| 898 | ** This looks for POSTed files names "file1".."fileN", stopping when |
| 899 | ** it finds no entry. Returns the number of entries attached to the |
| 900 | ** target or a negative value on error (in which case the current db |
| 901 | ** transaction will be in a rollback state). |
| 902 | ** |
| 903 | ** The only errors are currently attachment size limit violations: |
| 904 | ** attachments must have a non-0 size and if the attachment-size-limit |
| 905 | ** setting is >0 then each file's size must be <= that. |
| 906 | ** |
| 907 | ** If this returns a negative value, it will have populated an error |
| 908 | ** response using ajax_route_error(). On success it produces no |
| 909 | ** output. |
| 910 | ** |
| 911 | ** ACHTUNG: if zTarget is a forum post, it "really should" be the ID |
| 912 | ** of the first version of that post, as that's where attachments are |
| 913 | ** intended to be applied so that they can be found and removed |
| 914 | ** consistently. Potential TODO is have this function do that if |
| 915 | ** attachment_target_type(zTarget,1)!=0 but it would (for current |
| 916 | ** uses) require duplicating work already done in the callers. |
| 917 | */ |
| 918 | int attachments_ajax_from_POST(const char *zTarget, int bNeedsModeration){ |
| 919 | int i; |
| 920 | int rc = 0; |
| 921 | int n = 0; |
| 922 | int szLimit; /* attachment-max-size setting */ |
| 923 | char aKeyPrefix[20]; /* Buffer for key "file%d" */ |
| 924 | char aKeySize[30]; /* Buffer for key "file%d:bytes" */ |
| 925 | char aKeyName[30]; /* Buffer for key "file%d:filename" */ |
| 926 | char aKeyDesc[30]; /* Buffer for key "file%d_desc" */ |
| 927 | |
| 928 | db_begin_transaction(); |
| 929 | szLimit = db_get_int("attachment-size-limit", 0); |
| 930 | |
| 931 | for(i = 1; ; ++i, ++n){ |
| 932 | /* Look for P("fileN"), where N=1..n */ |
| 933 | const char *zContent; |
| 934 | const char *zFilename; |
| 935 | int szContent; |
| 936 | sqlite3_snprintf(sizeof(aKeyPrefix), aKeyPrefix, "file%d", i); |
| 937 | zContent = P(aKeyPrefix); |
| 938 | if( !zContent ){ |
| 939 | /* End of the list. */ |
| 940 | break; |
| 941 | } |
| 942 | sqlite3_snprintf(sizeof(aKeySize), aKeySize, "%s:bytes", |
| 943 | aKeyPrefix); |
| 944 | szContent = atoi(PD(aKeySize,"-1")); |
| 945 | if( szContent<=0 ){ |
| 946 | rc = -ajax_route_error(400,"Invalid file size: %d", szContent); |
| 947 | break; |
| 948 | }else if( szLimit>0 && szContent>szLimit ){ |
| 949 | rc = -ajax_route_error(413, "File size limit is %d bytes.", szLimit); |
| 950 | break; |
| 951 | }else{ |
| 952 | sqlite3_snprintf(sizeof(aKeyName), aKeyName, "%s:filename", |
| 953 | aKeyPrefix); |
| 954 | sqlite3_snprintf(sizeof(aKeyDesc), aKeyDesc, "%s_desc", |
| 955 | aKeyPrefix); |
| 956 | if( 0==(zFilename=P(aKeyName)) ){ |
| 957 | rc = -ajax_route_error(400, "Missing filename."); |
| 958 | break; |
| 959 | } |
| 960 | attach_commit(zFilename, zTarget, zContent, szContent, |
| 961 | bNeedsModeration, P(aKeyDesc)); |
| 962 | } |
| 963 | } |
| 964 | if( rc<0 ){ |
| 965 | db_rollback_transaction(); |
| 966 | return rc; |
| 967 | }else{ |
| 968 | db_commit_transaction(); |
| 969 | return n; |
| 970 | } |
| 971 | } |
| 972 | |
| 973 | /* |
| 974 | ** Proxy for /attachadd?target=X |
| 975 | ** |
| 976 | ** Lists attachments for, and can add them to, a target artifact. |
| 977 | ** |
| 978 | ** target=TKT_HASH|WIKIPAGE_NAME|TECHNOTE_HASH|FORUMPOST_HASH |
| 979 | ** from=ORIGINATING_URL |
| 980 | ** |
| 981 | ** Works like /attachadd but uses a JS-based interactive attachment |
| 982 | ** selector. |
| 983 | ** |
| 984 | ** from=X tells it where to redirect to when it's done. |
| 985 | ** |
| 986 | ** This page requires a post-2018-ish JS-capable browser. |
| 987 | */ |
| 988 | void attachaddV2_page(void){ |
| 989 | const char *zFrom = P("from"); |
| 990 | const char *zTarget = P("target"); |
| 991 | char *zTo = 0; |
| 992 | char *zTargetType = 0; |
| 993 | char *zExtraFree = 0; |
| 994 | int eTgtType = 0; |
| 995 | int goodCaptcha = 1; |
| 996 | char const * noJsArgs[] = {0,0,0,0}; /* Args for noscript form */ |
| 997 | |
| 998 | if( P("cancel") ) cgi_redirect(zFrom); |
| 999 | if( 0==zTarget ){ |
| 1000 | webpage_error("Requires target=X"); |
| 1001 | } |
| 1002 | login_check_credentials(); |
| 1003 | eTgtType = attachment_target_type(zTarget, 1); |
| 1004 | switch( eTgtType ){ |
| 1005 | default: |
| 1006 | case 0: |
| 1007 | webpage_error("Cannot resolve target=%h.", zTarget); |
| 1008 | break; |
| 1009 | case CFTYPE_FORUM:{ |
| 1010 | int fpid; |
| 1011 | if( g.perm.AttachForum==0 ){ |
| 1012 | login_needed(g.anon.AttachForum); |
| 1013 | return; |
| 1014 | } |
| 1015 | fpid = forumpost_head_rid2(zTarget); |
| 1016 | if( fpid<=0 ){ |
| 1017 | webpage_error("Invalid forum post ID: %h", zTarget); |
| 1018 | }else if( !g.perm.Admin && !forumpost_is_owner(fpid, 0) ){ |
| 1019 | webpage_error("Only admins can attach files to other users' " |
| 1020 | "forum posts."); |
| 1021 | } |
| 1022 | zTarget = zExtraFree = rid_to_uuid(fpid); |
| 1023 | noJsArgs[0] = zTarget; |
| 1024 | zTargetType = mprintf( |
| 1025 | "Forum post <a href=\"%R/forumpost/%S\">%.16h</a>", |
| 1026 | zTarget, zTarget |
| 1027 | ); |
| 1028 | zTo = mprintf("%R/forumpost/%S", zTarget); |
| 1029 | break; |
| 1030 | } |
| 1031 | case CFTYPE_EVENT:{ |
| 1032 | if( g.perm.Write==0 || g.perm.ApndWiki==0 || g.perm.Attach==0 ){ |
| 1033 | login_needed(g.anon.Write && g.anon.ApndWiki && g.anon.Attach); |
| 1034 | return; |
| 1035 | } |
| 1036 | if( !db_exists("SELECT 1 FROM tag WHERE tagname='event-%q'", |
| 1037 | zTarget) ){ |
| 1038 | zTarget = db_text(0, "SELECT substr(tagname,7) FROM tag" |
| 1039 | " WHERE tagname GLOB 'event-%q*'", |
| 1040 | zTarget); |
| 1041 | if( zTarget==0) fossil_redirect_home(); |
| 1042 | } |
| 1043 | zTo = zFrom ? 0 : mprintf("%R/technote?name=%T", zTarget); |
| 1044 | zTargetType = mprintf("Tech-note <a href=\"%R/technote/%s\">%S</a>", |
| 1045 | zTarget, zTarget); |
| 1046 | noJsArgs[1] = zTarget; |
| 1047 | break; |
| 1048 | } |
| 1049 | case CFTYPE_TICKET:{ |
| 1050 | if( g.perm.ApndTkt==0 || g.perm.Attach==0 ){ |
| 1051 | login_needed(g.anon.ApndTkt && g.anon.Attach); |
| 1052 | return; |
| 1053 | } |
| 1054 | if( !db_exists("SELECT 1 FROM tag WHERE tagname='tkt-%q'", |
| 1055 | zTarget) ){ |
| 1056 | zTarget = db_text(0, "SELECT substr(tagname,5) FROM tag" |
| 1057 | " WHERE tagname GLOB 'tkt-%q*'", zTarget); |
| 1058 | if( zTarget==0 ) fossil_redirect_home(); |
| 1059 | } |
| 1060 | zTo = zFrom ? 0 : mprintf("%R/tktview/%t", zTarget); |
| 1061 | zTargetType = mprintf("Ticket <a href=\"%R/tktview/%s\">%S</a>", |
| 1062 | zTarget, zTarget); |
| 1063 | noJsArgs[2] = zTarget; |
| 1064 | break; |
| 1065 | } |
| 1066 | case CFTYPE_WIKI:{ |
| 1067 | if( g.perm.ApndWiki==0 || g.perm.Attach==0 ){ |
| 1068 | login_needed(g.anon.ApndWiki && g.anon.Attach); |
| 1069 | return; |
| 1070 | } |
| 1071 | if( !db_exists("SELECT 1 FROM tag WHERE tagname='wiki-%q'", |
| 1072 | zTarget) ){ |
| 1073 | fossil_redirect_home(); |
| 1074 | } |
| 1075 | zTo = zFrom ? 0 : mprintf("%R/wiki?name=%T", zTarget); |
| 1076 | zTargetType = mprintf( |
| 1077 | "Wiki page <a href=\"%R/wiki?name=%h\">%h</a>", |
| 1078 | zTarget, zTarget |
| 1079 | ); |
| 1080 | noJsArgs[3] = zTarget; |
| 1081 | break; |
| 1082 | } |
| 1083 | } |
| 1084 | |
| 1085 | db_begin_transaction(); |
| 1086 | |
| 1087 | style_set_current_feature("attach"); |
| 1088 | style_header("Add Attachment"); |
| 1089 | if( !goodCaptcha ){ |
| 1090 | @ <p class="generalError">Error: Incorrect security code.</p> |
| 1091 | } |
| 1092 | @ <h2>Attachments for %s(zTargetType)</h2> |
| 1093 | attachment_list(zTarget, NULL, |
| 1094 | ATTACHLIST_SIZE | ATTACHLIST_HIDE_UNAPPROVED); |
| 1095 | attach_render_legacy_form( |
| 1096 | noJsArgs[0], noJsArgs[1], noJsArgs[2], |
| 1097 | noJsArgs[3], 0, |
| 1098 | zFrom ? zFrom : (zTo ? zTo : (zTo=mprintf("%R/home"))) |
| 1099 | ); |
| 1100 | @ <div id='attachadd-form-wrapper' class='hidden'> |
| 1101 | /* fossil.attach.js populates this DIV with the attachment widget, |
| 1102 | ** imports these hidden fields, and removes the legacy form. */ |
| 1103 | @ <input type="hidden" name="target" value="%h(zTarget)"> |
| 1104 | if( zFrom ){ |
| 1105 | @ <input type="hidden" name="from" value="%h(zFrom)"> |
| 1106 | } |
| 1107 | if( zTo ){ |
| 1108 | @ <input type="hidden" name="to" value="%h(zTo)"> |
| 1109 | } |
| 1110 | captcha_generate(0); |
| 1111 | login_insert_csrf_secret(); |
| 1112 | @ </div> |
| 1113 | builtin_fossil_js_bundle_or("attach", NULL); |
| 1114 | db_end_transaction(0); |
| 1115 | style_finish_page(); |
| 1116 | fossil_free(zTargetType); |
| 1117 | fossil_free(zExtraFree); |
| 1118 | fossil_free(zTo); |
| 1119 | } |
| 1120 | |
| 1121 | /* |
| 1122 | ** WEBPAGE: ainfo |
| 1123 | ** URL: /ainfo?name=ARTIFACTID |
| @@ -600,15 +1178,17 @@ | |
| 1178 | && db_exists("SELECT 1 FROM ticket WHERE tkt_uuid='%q'", zTarget) |
| 1179 | ){ |
| 1180 | if( !g.perm.RdTkt ){ login_needed(g.anon.RdTkt); return; } |
| 1181 | zTktUuid = zTarget; |
| 1182 | showDelMenu = g.perm.WrTkt; |
| 1183 | }else if( db_exists("SELECT 1 FROM tag WHERE tagname='wiki-%q'", |
| 1184 | zTarget) ){ |
| 1185 | if( !g.perm.RdWiki ){ login_needed(g.anon.RdWiki); return; } |
| 1186 | zWikiName = zTarget; |
| 1187 | showDelMenu = g.perm.WrWiki; |
| 1188 | }else if( db_exists("SELECT 1 FROM tag WHERE tagname='event-%q'", |
| 1189 | zTarget) ){ |
| 1190 | if( !g.perm.RdWiki ){ login_needed(g.anon.RdWiki); return; } |
| 1191 | zTNUuid = zTarget; |
| 1192 | showDelMenu = g.perm.Write && g.perm.WrWiki; |
| 1193 | } |
| 1194 | if( showDelMenu ){ |
| @@ -632,11 +1212,13 @@ | |
| 1212 | Blob cksum; |
| 1213 | const char *zFile = zName; |
| 1214 | |
| 1215 | if( !bUserIsOwner ){ |
| 1216 | if( zForumPost ? !forumpost_may_close() : !g.perm.Admin ){ |
| 1217 | webpage_error( |
| 1218 | "Only admins can delete other users' attachments." |
| 1219 | ); |
| 1220 | } |
| 1221 | } |
| 1222 | db_begin_transaction(); |
| 1223 | blob_zero(&manifest); |
| 1224 | for(i=n=0; zFile[i]; i++){ |
| @@ -656,14 +1238,14 @@ | |
| 1238 | @ <p>The attachment below has been deleted.</p> |
| 1239 | fossil_free(zNewDate); |
| 1240 | } |
| 1241 | |
| 1242 | if( P("del") |
| 1243 | && ((zForumPost && (bUserIsOwner || forumpost_may_close())) |
| 1244 | || (zTktUuid && g.perm.WrTkt) |
| 1245 | || (zWikiName && g.perm.WrWiki) |
| 1246 | || (zTNUuid && g.perm.Write && g.perm.WrWiki)) |
| 1247 | ){ |
| 1248 | form_begin(0, "%R/ainfo/%!S", zUuid); |
| 1249 | @ <p>Confirm you want to delete the attachment shown below. |
| 1250 | @ <input type="submit" name="confirm" value="Confirm"> |
| 1251 | login_insert_csrf_secret(); |
| @@ -711,20 +1293,22 @@ | |
| 1293 | @ (%d(rid)) |
| 1294 | } |
| 1295 | modPending = moderation_pending_www(rid); |
| 1296 | if( zForumPost ){ |
| 1297 | @ <tr><th>Forum Post:</th> |
| 1298 | @ <td>%z(href("%R/forumpost/%s",zForumPost))%h(zForumPost)</a>\ |
| 1299 | @ </td></tr> |
| 1300 | }else if( zTktUuid ){ |
| 1301 | @ <tr><th>Ticket:</th> |
| 1302 | @ <td>%z(href("%R/tktview/%s",zTktUuid))%s(zTktUuid)</a></td></tr> |
| 1303 | }else if( zTNUuid ){ |
| 1304 | @ <tr><th>Tech Note:</th> |
| 1305 | @ <td>%z(href("%R/technote/%s",zTNUuid))%s(zTNUuid)</a></td></tr> |
| 1306 | }else if( zWikiName ){ |
| 1307 | @ <tr><th>Wiki Page:</th> |
| 1308 | @ <td>%z(href("%R/wiki?name=%t",zWikiName))%h(zWikiName)</a>\ |
| 1309 | @ </td></tr> |
| 1310 | } |
| 1311 | @ <tr><th>Date:</th><td> |
| 1312 | hyperlink_to_date(zDate, "</td></tr>"); |
| 1313 | @ <tr><th>User:</th><td> |
| 1314 | hyperlink_to_user(pAttach->zUser, zDate, "</td></tr>"); |
| @@ -735,22 +1319,42 @@ | |
| 1319 | } |
| 1320 | @ <tr><th>Filename:</th><td>%h(zName)</td></tr> |
| 1321 | if( g.perm.Setup ){ |
| 1322 | @ <tr><th>MIME-Type:</th><td>%h(zMime)</td></tr> |
| 1323 | } |
| 1324 | @ <tr><th valign="top">Description:</th>\ |
| 1325 | /* FIXME (2026-06-05): Honor the N-card (comment mimetype). */ |
| 1326 | @ <td valign="top">%h(zDesc)</td></tr> |
| 1327 | @ </table> |
| 1328 | |
| 1329 | if( modPending && (isModerator || bUserIsOwner) ){ |
| 1330 | @ <div class="section">Moderation</div> |
| 1331 | @ <blockquote> |
| 1332 | form_begin(0, "%R/ainfo/%s", zUuid); |
| 1333 | @ <label><input type="radio" name="modaction" value="delete"> |
| 1334 | @ Delete this attachment</label><br> |
| 1335 | if( isModerator ){ |
| 1336 | #if 0 |
| 1337 | /* TODO/FIXME (2026-06-03): only allow approval of an attachment |
| 1338 | ** if its target has been approved. Without this, we can end up |
| 1339 | ** with stale attachments which refer to rejected targets. We |
| 1340 | ** need a type-specific RID/UUID here, which requires |
| 1341 | ** refactoring above to get it. */ |
| 1342 | const int tgtid = 0; |
| 1343 | if( moderation_pending(tgtid) ){ |
| 1344 | @ <label><input type="radio" name="modaction" \ |
| 1345 | @ disabled value="approve"> |
| 1346 | @ <span class='modpending'>Cannot approve: |
| 1347 | @ target is pending moderation</span>\ |
| 1348 | @ </label><br> |
| 1349 | }else |
| 1350 | #else |
| 1351 | { |
| 1352 | @ <label><input type="radio" name="modaction" value="approve"> |
| 1353 | @ Approve this attachment</label><br> |
| 1354 | } |
| 1355 | #endif |
| 1356 | } |
| 1357 | @ <input type="submit" value="Submit"> |
| 1358 | login_insert_csrf_secret(); |
| 1359 | @ </form> |
| 1360 | @ </blockquote> |
| @@ -767,11 +1371,12 @@ | |
| 1371 | const char *z; |
| 1372 | content_get(ridSrc, &attach); |
| 1373 | blob_to_utf8_no_bom(&attach, 0); |
| 1374 | z = blob_str(&attach); |
| 1375 | if( zLn ){ |
| 1376 | output_text_with_line_numbers(z, blob_size(&attach), |
| 1377 | zName, zLn, 1); |
| 1378 | }else{ |
| 1379 | @ <pre> |
| 1380 | @ %h(z) |
| 1381 | @ </pre> |
| 1382 | } |
| @@ -798,10 +1403,13 @@ | |
| 1403 | */ |
| 1404 | #define ATTACHLIST_HRULE_ABOVE 0x01 /* Insert <hr> above header */ |
| 1405 | #define ATTACHLIST_TARGET_BLANK 0x02 /* use target=_blank for links */ |
| 1406 | #define ATTACHLIST_SIZE 0x04 /* add size */ |
| 1407 | #define ATTACHLIST_HIDE_UNAPPROVED 0x08 /* Hide pending-moderation files */ |
| 1408 | #define ATTACHLIST_DETAILS_CLOSED 0x10 /* Wrap in a closed DETAILS element */ |
| 1409 | #define ATTACHLIST_DETAILS_OPEN 0x20 /* Wrap in an open DETAILS element */ |
| 1410 | #define ATTACHLIST_HIDE_EMPTY 0x40 /* Skip if size<1 */ |
| 1411 | #endif |
| 1412 | |
| 1413 | /* |
| 1414 | ** Output HTML to show a list of attachments. |
| 1415 | */ |
| @@ -810,19 +1418,25 @@ | |
| 1418 | const char *zHeader, /* Header to display with attachments */ |
| 1419 | const int flags /* ATTACHLIST_... flags */ |
| 1420 | ){ |
| 1421 | int cnt = 0; |
| 1422 | char szBuf[36] = {0}; /* scratchpad for attachment size value */ |
| 1423 | const char *zLinkTgt = (ATTACHLIST_TARGET_BLANK & flags) |
| 1424 | ? " target=\"_blank\"" : ""; |
| 1425 | const int bUseDetail = flags & |
| 1426 | (ATTACHLIST_DETAILS_CLOSED | ATTACHLIST_DETAILS_OPEN); |
| 1427 | Stmt q; |
| 1428 | |
| 1429 | db_prepare(&q, |
| 1430 | "SELECT datetime(mtime,toLocal()), a.filename, a.user," |
| 1431 | " b1.uuid, a.src, a.target, a.attachid, b2.size\n" |
| 1432 | " FROM attachment a, blob b1, blob b2\n" |
| 1433 | " WHERE a.isLatest\n" |
| 1434 | " AND a.src IS NOT NULL\n" |
| 1435 | " AND a.target=%Q\n" |
| 1436 | " AND b1.rid=a.attachid\n" |
| 1437 | " AND b2.uuid=a.src\n" |
| 1438 | " ORDER BY mtime DESC", |
| 1439 | zTarget |
| 1440 | ); |
| 1441 | while( db_step(&q)==SQLITE_ROW ){ |
| 1442 | const char *zDate = db_column_text(&q, 0); |
| @@ -832,36 +1446,51 @@ | |
| 1446 | const char *zSrc = db_column_text(&q, 4); |
| 1447 | const char *zTarget = db_column_text(&q, 5); |
| 1448 | const char *zDispUser = zUser && zUser[0] ? zUser : "anonymous"; |
| 1449 | const char *zTypeArg = 0; /* URL arg name for /attachdownload */ |
| 1450 | const int aid = db_column_int(&q, 6); |
| 1451 | const int sz = db_column_int(&q, 7); |
| 1452 | if( (flags & ATTACHLIST_HIDE_UNAPPROVED) |
| 1453 | && moderation_pending(aid) |
| 1454 | && !moderation_user_could(aid, 1, 0) ){ |
| 1455 | continue; |
| 1456 | } |
| 1457 | if( sz<1 && (flags & ATTACHLIST_HIDE_EMPTY) ){ |
| 1458 | /* Deleted or phantom items. */ |
| 1459 | continue; |
| 1460 | } |
| 1461 | if( cnt==0 ){ |
| 1462 | if( bUseDetail ){ |
| 1463 | @ <details class='attachlist' |
| 1464 | if( ATTACHLIST_DETAILS_OPEN & flags ){ |
| 1465 | @ open |
| 1466 | } |
| 1467 | @ > |
| 1468 | }else{ |
| 1469 | @ <section class='attachlist'> |
| 1470 | } |
| 1471 | if( flags & ATTACHLIST_HRULE_ABOVE ){ |
| 1472 | @ <hr> |
| 1473 | } |
| 1474 | if( bUseDetail ){ |
| 1475 | @ <summary>%s(zHeader)</summary> |
| 1476 | }else{ |
| 1477 | @ %s(zHeader) |
| 1478 | } |
| 1479 | @ <ul> |
| 1480 | } |
| 1481 | cnt++; |
| 1482 | switch( attachment_target_type(zTarget, 1) ){ |
| 1483 | case CFTYPE_TICKET: zTypeArg = "tkt"; break; |
| 1484 | case CFTYPE_FORUM: zTypeArg = "forumpost"; break; |
| 1485 | case CFTYPE_EVENT: zTypeArg = "technote"; break; |
| 1486 | case CFTYPE_WIKI: |
| 1487 | default: zTypeArg = "page"; break; |
| 1488 | } |
| 1489 | @ <li> |
| 1490 | @ <a href="%R/artifact/%!S(zSrc)"%s(zLinkTgt)>%h(zFile)</a> |
| 1491 | if( flags & ATTACHLIST_SIZE ){ |
| 1492 | sqlite3_snprintf(sizeof(szBuf), szBuf, " %d bytes", sz); |
| 1493 | } |
| 1494 | @ [<a href="%R/attachdownload/%t(zFile)?%s(zTypeArg)=%t(zTarget)\ |
| 1495 | @&file=%t(zFile)%s(zLinkTgt)">download</a>%s(szBuf)] |
| 1496 | @ added by %h(zDispUser) on |
| @@ -870,11 +1499,15 @@ | |
| 1499 | moderation_pending_www(aid); |
| 1500 | @ </li> |
| 1501 | } |
| 1502 | if( cnt ){ |
| 1503 | @ </ul> |
| 1504 | if( bUseDetail ){ |
| 1505 | @ </details> |
| 1506 | }else{ |
| 1507 | @ </section> |
| 1508 | } |
| 1509 | } |
| 1510 | db_finalize(&q); |
| 1511 | } |
| 1512 | |
| 1513 | /* |
| @@ -1023,15 +1656,204 @@ | |
| 1656 | } |
| 1657 | for(i = 2; i < g.argc; ++i){ |
| 1658 | const char *zPage = g.argv[i]; |
| 1659 | db_bind_text(&q, ":tgtname", zPage); |
| 1660 | while(SQLITE_ROW == db_step(&q)){ |
| 1661 | const char *zTime = db_column_text(&q, 0); |
| 1662 | const char *zSrc = db_column_text(&q, 1); |
| 1663 | const char *zTarget = db_column_text(&q, 2); |
| 1664 | const char *zName = db_column_text(&q, 3); |
| 1665 | printf("%-20s %s %.12s %s\n", zTarget, zTime, zSrc, zName); |
| 1666 | } |
| 1667 | db_reset(&q); |
| 1668 | } |
| 1669 | db_finalize(&q); |
| 1670 | } |
| 1671 | |
| 1672 | /* |
| 1673 | ** Renders the list of attachments for artifact pManifest as JSON to |
| 1674 | ** blob pOut. If pManifest->type is not one of (CFTYPE_TICKET, |
| 1675 | ** CFTYPE_FORUM, CFTYPE_EVENT, CFTYPE_WIKI) then it behaves as if the |
| 1676 | ** result set is empty. |
| 1677 | ** |
| 1678 | ** If there are no matching attachments then its behavior depends on |
| 1679 | ** emptyPolicy: |
| 1680 | ** |
| 1681 | ** <0 = emit a JSON NULL |
| 1682 | ** 0 = emit no output |
| 1683 | ** >0 = emit an empty JSON array |
| 1684 | ** |
| 1685 | ** If bLatestOnly is true then only the most recent entry for a given |
| 1686 | ** attachment is emitted, else all versions are emitted in descending |
| 1687 | ** mtime order. |
| 1688 | ** |
| 1689 | ** Returns the number of attachments. |
| 1690 | ** |
| 1691 | ** Output format: |
| 1692 | ** |
| 1693 | ** [{ |
| 1694 | ** "uuid": attachment artifact hash, |
| 1695 | ** "src": hash of the attachment blob, |
| 1696 | ** "target": wiki page name or ticket/event ID, |
| 1697 | ** "filename": filename of attachment, |
| 1698 | ** "mtime": ISO-8601 timestamp UTC, |
| 1699 | ** "isLatest": true if this is the latest version of this file |
| 1700 | ** else false, |
| 1701 | ** }, ...once per attachment] |
| 1702 | ** |
| 1703 | */ |
| 1704 | int attachments_to_json(const Manifest *pManifest, |
| 1705 | Blob *pOut, int bLatestOnly, |
| 1706 | int emptyPolicy){ |
| 1707 | int i = 0; |
| 1708 | Stmt q = empty_Stmt; |
| 1709 | char *zToFree = 0; |
| 1710 | const char *zTgt = 0; |
| 1711 | switch(pManifest->type){ |
| 1712 | case CFTYPE_FORUM: zTgt = zToFree = rid_to_uuid(pManifest->rid); |
| 1713 | break; |
| 1714 | case CFTYPE_WIKI: zTgt = pManifest->zWikiTitle; break; |
| 1715 | case CFTYPE_EVENT: zTgt = pManifest->zEventId; break; |
| 1716 | case CFTYPE_TICKET: zTgt = pManifest->zTicketUuid; break; |
| 1717 | default: |
| 1718 | goto empty_result; |
| 1719 | } |
| 1720 | db_prepare(&q, |
| 1721 | "SELECT datetime(mtime), a.src, a.target, a.filename, a.isLatest,\n" |
| 1722 | " b2.size, b1.uuid, a.user, a.comment\n" |
| 1723 | " FROM attachment a, blob b1, blob b2\n" |
| 1724 | " WHERE a.target=%Q\n" |
| 1725 | " AND a.src IS NOT NULL\n" |
| 1726 | " AND b1.rid=a.attachid\n" |
| 1727 | " AND b2.uuid=a.src\n" |
| 1728 | " AND (a.isLatest OR %d)\n" |
| 1729 | " ORDER BY a.target, a.isLatest DESC, a.mtime DESC\n", |
| 1730 | zTgt, !bLatestOnly |
| 1731 | ); |
| 1732 | while(SQLITE_ROW == db_step(&q)){ |
| 1733 | const char *zTime = db_column_text(&q, 0); |
| 1734 | const char *zSrc = db_column_text(&q, 1); |
| 1735 | const char *zTarget = db_column_text(&q, 2); |
| 1736 | const char *zName = db_column_text(&q, 3); |
| 1737 | const int isLatest = db_column_int(&q, 4); |
| 1738 | const int sz = db_column_int(&q, 5); |
| 1739 | const char *zUuid = db_column_text(&q, 6); |
| 1740 | const char *zUser = db_column_text(&q, 7); |
| 1741 | const char *zComment = db_column_text(&q, 8); |
| 1742 | if(!i++){ |
| 1743 | blob_append_char(pOut, '['); |
| 1744 | }else{ |
| 1745 | blob_append_char(pOut, ','); |
| 1746 | } |
| 1747 | blob_appendf( |
| 1748 | pOut, |
| 1749 | "{\"uuid\": %!j, \"src\": %!j, \"target\": %!j, " |
| 1750 | "\"filename\": %!j, \"size\":%d, \"mtime\": %!j, " |
| 1751 | "\"isLatest\": %s, \"user\": %!j, \"comment\": ", |
| 1752 | zUuid, zSrc, zTarget, |
| 1753 | zName, sz, zTime, isLatest ? "true" : "false", |
| 1754 | zUser |
| 1755 | ); |
| 1756 | if( zComment && zComment[0] ){ |
| 1757 | blob_appendf(pOut, "%!j", zComment); |
| 1758 | }else{ |
| 1759 | blob_append_literal(pOut, "null"); |
| 1760 | } |
| 1761 | blob_append_char(pOut, '}'); |
| 1762 | } |
| 1763 | fossil_free(zToFree); |
| 1764 | db_finalize(&q); |
| 1765 | if(!i){ |
| 1766 | empty_result: |
| 1767 | if( emptyPolicy>0 ){ |
| 1768 | blob_append_literal(pOut, "[]"); |
| 1769 | }else if( emptyPolicy<0 ){ |
| 1770 | blob_append_literal(pOut, "null"); |
| 1771 | } |
| 1772 | }else{ |
| 1773 | blob_append_char(pOut, ']'); |
| 1774 | } |
| 1775 | return i; |
| 1776 | } |
| 1777 | |
| 1778 | /* |
| 1779 | ** COMMAND: test-attachment-target |
| 1780 | ** |
| 1781 | ** Usage: %fossil test-attachment-target TARGET_ID... |
| 1782 | */ |
| 1783 | void test_attachment_target_type_cmd(void){ |
| 1784 | int i; |
| 1785 | verify_all_options(); |
| 1786 | db_find_and_open_repository(0, 0); |
| 1787 | if( g.argc<3 ){ |
| 1788 | usage("test-attachment-target TARGET_ID"); |
| 1789 | return; |
| 1790 | } |
| 1791 | for( i = 2; i < g.argc; ++i ){ |
| 1792 | const char *zTarget = g.argv[i]; |
| 1793 | const int rid = attachment_target_rid(zTarget, 0); |
| 1794 | const int type = attachment_target_type(zTarget, 0); |
| 1795 | const char *zType = "<invalid>"; |
| 1796 | switch(type){ |
| 1797 | case CFTYPE_EVENT: zType = "technote"; break; |
| 1798 | case CFTYPE_FORUM: zType = "forumpost"; break; |
| 1799 | case CFTYPE_TICKET: zType = "ticket"; break; |
| 1800 | case CFTYPE_WIKI: zType = "wiki"; break; |
| 1801 | } |
| 1802 | fossil_print("%-20s = %-9s #%d %z\n", |
| 1803 | zTarget, zType, rid, |
| 1804 | rid>0 ? rid_to_uuid(rid) : 0); |
| 1805 | } |
| 1806 | } |
| 1807 | |
| 1808 | |
| 1809 | /* |
| 1810 | ** COMMAND: test-attachments-to-json |
| 1811 | ** |
| 1812 | ** Usage: %fossil test-attachments-to-json TARGET_ID... |
| 1813 | ** |
| 1814 | ** Options: |
| 1815 | ** --old List all versions of attachments. Default is to |
| 1816 | ** list only the latest. |
| 1817 | ** --full Require a full target ID, not a prefix. |
| 1818 | ** |
| 1819 | ** Emits a JSON array of attachments for the given attachment targets. |
| 1820 | ** The given IDs must be wiki page names, ticket hashes, tech-note |
| 1821 | ** hashes, or forum post hashes. By default it accepts hash prefixes |
| 1822 | ** but does no detection of ambiguity or cross-type prefix collisions |
| 1823 | ** so may emit curious results if given short, colliding IDs. |
| 1824 | */ |
| 1825 | void test_attachments_to_json_cmd(void){ |
| 1826 | const int emptyPolicy = 1; |
| 1827 | const int bLatestOnly = find_option("old",0,0)==0; |
| 1828 | const int bFullId = find_option("full",0,0)!=0; |
| 1829 | int i; |
| 1830 | |
| 1831 | verify_all_options(); |
| 1832 | db_find_and_open_repository(0, 0); |
| 1833 | if( g.argc<3 ){ |
| 1834 | usage("test-attachments-to-json TARGET_ID"); |
| 1835 | return; |
| 1836 | } |
| 1837 | for( i = 2; i < g.argc; ++i ){ |
| 1838 | const char *zTarget = g.argv[i]; |
| 1839 | const int rid = attachment_target_rid(zTarget, bFullId); |
| 1840 | if( 0==rid ){ |
| 1841 | fossil_print("** cannot resolve %s\n", zTarget); |
| 1842 | }else{ |
| 1843 | Blob b = BLOB_INITIALIZER; |
| 1844 | Manifest *pManifest = manifest_get(rid, CFTYPE_ANY, NULL); |
| 1845 | assert( pManifest ); |
| 1846 | attachments_to_json(pManifest, &b, bLatestOnly, emptyPolicy); |
| 1847 | fossil_print("Attachments for %s: ", zTarget); |
| 1848 | if( b.nUsed ){ |
| 1849 | char *zPretty = db_text(0,"SELECT json_pretty(%B)", &b); |
| 1850 | fossil_print("%s\n", zPretty); |
| 1851 | fossil_free(zPretty); |
| 1852 | }else{ |
| 1853 | fossil_print("none\n"); |
| 1854 | } |
| 1855 | blob_reset(&b); |
| 1856 | manifest_destroy(pManifest); |
| 1857 | } |
| 1858 | } |
| 1859 | } |
| 1860 |
+20
-1
| --- src/builtin.c | ||
| +++ src/builtin.c | ||
| @@ -668,22 +668,39 @@ | ||
| 668 | 668 | CX("editStateMarkers: {" |
| 669 | 669 | "/*Symbolic markers to denote certain edit states.*/" |
| 670 | 670 | "isNew:'[+]', isModified:'[*]', isDeleted:'[-]'},\n"); |
| 671 | 671 | CX("confirmerButtonTicks: 3 " |
| 672 | 672 | "/*default fossil.confirmer tick count.*/,\n"); |
| 673 | + CX("attachmentSizeLimit: %d,\n", | |
| 674 | + db_get_int("attachment-size-limit",0)); | |
| 673 | 675 | /* Inject certain info about the current skin... */ |
| 674 | 676 | CX("skin:{"); |
| 675 | 677 | /* can leak a local filesystem path: |
| 676 | 678 | CX("name: %!j,", skin_in_use());*/ |
| 677 | 679 | CX("isDark: %s" |
| 678 | 680 | "/*true if the current skin has the 'white-foreground' detail*/", |
| 679 | 681 | skin_detail_boolean("white-foreground") ? "true" : "false"); |
| 680 | 682 | CX("}\n"/*fossil.config.skin*/); |
| 681 | 683 | CX("};\n"/* fossil.config */); |
| 684 | + if( forum_statuses()->n>1 ){ | |
| 685 | + const ForumStatusList * fsl = forum_statuses(); | |
| 686 | + int i; | |
| 687 | + CX("window.fossil.config.forumStatuses = ["); | |
| 688 | + for(i = 0; i < fsl->n; ++i){ | |
| 689 | + const ForumStatus *fs = &fsl->aStatus[i]; | |
| 690 | + if(i) CX(","); | |
| 691 | + CX("{label:%!j, value:%!j}", fs->zLabel, fs->zValue); | |
| 692 | + } | |
| 693 | + CX("];\n"); | |
| 694 | + } | |
| 695 | +#define JBOOL(COND) ((COND) ? "true" : "false") | |
| 682 | 696 | CX("window.fossil.user = {"); |
| 683 | 697 | CX("name: %!j,", (g.zLogin&&*g.zLogin) ? g.zLogin : "guest"); |
| 684 | - CX("isAdmin: %s", (g.perm.Admin || g.perm.Setup) ? "true" : "false"); | |
| 698 | + CX("isAdmin: %s,", JBOOL(g.perm.Admin || g.perm.Setup)); | |
| 699 | + CX("mayAttachForum: %s,", JBOOL(g.perm.AttachForum)); | |
| 700 | + CX("enableDebug: %s,", JBOOL(g.perm.Debug || g.perm.Admin)); | |
| 701 | + CX("isIndividual: %s", JBOOL(login_is_individual())); | |
| 685 | 702 | CX("};\n"/*fossil.user*/); |
| 686 | 703 | CX("if(fossil.config.skin.isDark) " |
| 687 | 704 | "document.body.classList.add('fossil-dark-style');\n"); |
| 688 | 705 | /* |
| 689 | 706 | ** fossil.page holds info about the current page. This is also |
| @@ -699,10 +716,11 @@ | ||
| 699 | 716 | } |
| 700 | 717 | /* The remaining window.fossil bootstrap code is not dependent on |
| 701 | 718 | ** C-runtime state... */ |
| 702 | 719 | builtin_request_js("fossil.bootstrap.js"); |
| 703 | 720 | } |
| 721 | +#undef JBOOL | |
| 704 | 722 | } |
| 705 | 723 | |
| 706 | 724 | /* |
| 707 | 725 | ** Given the NAME part of fossil.NAME.js, this function checks whether |
| 708 | 726 | ** that module has been emitted by this function before. If it has, |
| @@ -732,10 +750,11 @@ | ||
| 732 | 750 | ** entries: all known deps of this one. Each |
| 733 | 751 | ** REQUIRES an EXPLICIT trailing \0, including |
| 734 | 752 | ** the final one! */ |
| 735 | 753 | } fjs[] = { |
| 736 | 754 | /* This list ordering isn't strictly important. */ |
| 755 | + {"attach", 0, "dom\0"}, | |
| 737 | 756 | {"confirmer", 0, 0}, |
| 738 | 757 | {"copybutton", 0, "dom\0"}, |
| 739 | 758 | {"diff", 0, "dom\0fetch\0storage\0" |
| 740 | 759 | /* maintenance note: "diff" needs "storage" for storing the |
| 741 | 760 | ** sbs-sync-scroll toggle. */}, |
| 742 | 761 |
| --- src/builtin.c | |
| +++ src/builtin.c | |
| @@ -668,22 +668,39 @@ | |
| 668 | CX("editStateMarkers: {" |
| 669 | "/*Symbolic markers to denote certain edit states.*/" |
| 670 | "isNew:'[+]', isModified:'[*]', isDeleted:'[-]'},\n"); |
| 671 | CX("confirmerButtonTicks: 3 " |
| 672 | "/*default fossil.confirmer tick count.*/,\n"); |
| 673 | /* Inject certain info about the current skin... */ |
| 674 | CX("skin:{"); |
| 675 | /* can leak a local filesystem path: |
| 676 | CX("name: %!j,", skin_in_use());*/ |
| 677 | CX("isDark: %s" |
| 678 | "/*true if the current skin has the 'white-foreground' detail*/", |
| 679 | skin_detail_boolean("white-foreground") ? "true" : "false"); |
| 680 | CX("}\n"/*fossil.config.skin*/); |
| 681 | CX("};\n"/* fossil.config */); |
| 682 | CX("window.fossil.user = {"); |
| 683 | CX("name: %!j,", (g.zLogin&&*g.zLogin) ? g.zLogin : "guest"); |
| 684 | CX("isAdmin: %s", (g.perm.Admin || g.perm.Setup) ? "true" : "false"); |
| 685 | CX("};\n"/*fossil.user*/); |
| 686 | CX("if(fossil.config.skin.isDark) " |
| 687 | "document.body.classList.add('fossil-dark-style');\n"); |
| 688 | /* |
| 689 | ** fossil.page holds info about the current page. This is also |
| @@ -699,10 +716,11 @@ | |
| 699 | } |
| 700 | /* The remaining window.fossil bootstrap code is not dependent on |
| 701 | ** C-runtime state... */ |
| 702 | builtin_request_js("fossil.bootstrap.js"); |
| 703 | } |
| 704 | } |
| 705 | |
| 706 | /* |
| 707 | ** Given the NAME part of fossil.NAME.js, this function checks whether |
| 708 | ** that module has been emitted by this function before. If it has, |
| @@ -732,10 +750,11 @@ | |
| 732 | ** entries: all known deps of this one. Each |
| 733 | ** REQUIRES an EXPLICIT trailing \0, including |
| 734 | ** the final one! */ |
| 735 | } fjs[] = { |
| 736 | /* This list ordering isn't strictly important. */ |
| 737 | {"confirmer", 0, 0}, |
| 738 | {"copybutton", 0, "dom\0"}, |
| 739 | {"diff", 0, "dom\0fetch\0storage\0" |
| 740 | /* maintenance note: "diff" needs "storage" for storing the |
| 741 | ** sbs-sync-scroll toggle. */}, |
| 742 |
| --- src/builtin.c | |
| +++ src/builtin.c | |
| @@ -668,22 +668,39 @@ | |
| 668 | CX("editStateMarkers: {" |
| 669 | "/*Symbolic markers to denote certain edit states.*/" |
| 670 | "isNew:'[+]', isModified:'[*]', isDeleted:'[-]'},\n"); |
| 671 | CX("confirmerButtonTicks: 3 " |
| 672 | "/*default fossil.confirmer tick count.*/,\n"); |
| 673 | CX("attachmentSizeLimit: %d,\n", |
| 674 | db_get_int("attachment-size-limit",0)); |
| 675 | /* Inject certain info about the current skin... */ |
| 676 | CX("skin:{"); |
| 677 | /* can leak a local filesystem path: |
| 678 | CX("name: %!j,", skin_in_use());*/ |
| 679 | CX("isDark: %s" |
| 680 | "/*true if the current skin has the 'white-foreground' detail*/", |
| 681 | skin_detail_boolean("white-foreground") ? "true" : "false"); |
| 682 | CX("}\n"/*fossil.config.skin*/); |
| 683 | CX("};\n"/* fossil.config */); |
| 684 | if( forum_statuses()->n>1 ){ |
| 685 | const ForumStatusList * fsl = forum_statuses(); |
| 686 | int i; |
| 687 | CX("window.fossil.config.forumStatuses = ["); |
| 688 | for(i = 0; i < fsl->n; ++i){ |
| 689 | const ForumStatus *fs = &fsl->aStatus[i]; |
| 690 | if(i) CX(","); |
| 691 | CX("{label:%!j, value:%!j}", fs->zLabel, fs->zValue); |
| 692 | } |
| 693 | CX("];\n"); |
| 694 | } |
| 695 | #define JBOOL(COND) ((COND) ? "true" : "false") |
| 696 | CX("window.fossil.user = {"); |
| 697 | CX("name: %!j,", (g.zLogin&&*g.zLogin) ? g.zLogin : "guest"); |
| 698 | CX("isAdmin: %s,", JBOOL(g.perm.Admin || g.perm.Setup)); |
| 699 | CX("mayAttachForum: %s,", JBOOL(g.perm.AttachForum)); |
| 700 | CX("enableDebug: %s,", JBOOL(g.perm.Debug || g.perm.Admin)); |
| 701 | CX("isIndividual: %s", JBOOL(login_is_individual())); |
| 702 | CX("};\n"/*fossil.user*/); |
| 703 | CX("if(fossil.config.skin.isDark) " |
| 704 | "document.body.classList.add('fossil-dark-style');\n"); |
| 705 | /* |
| 706 | ** fossil.page holds info about the current page. This is also |
| @@ -699,10 +716,11 @@ | |
| 716 | } |
| 717 | /* The remaining window.fossil bootstrap code is not dependent on |
| 718 | ** C-runtime state... */ |
| 719 | builtin_request_js("fossil.bootstrap.js"); |
| 720 | } |
| 721 | #undef JBOOL |
| 722 | } |
| 723 | |
| 724 | /* |
| 725 | ** Given the NAME part of fossil.NAME.js, this function checks whether |
| 726 | ** that module has been emitted by this function before. If it has, |
| @@ -732,10 +750,11 @@ | |
| 750 | ** entries: all known deps of this one. Each |
| 751 | ** REQUIRES an EXPLICIT trailing \0, including |
| 752 | ** the final one! */ |
| 753 | } fjs[] = { |
| 754 | /* This list ordering isn't strictly important. */ |
| 755 | {"attach", 0, "dom\0"}, |
| 756 | {"confirmer", 0, 0}, |
| 757 | {"copybutton", 0, "dom\0"}, |
| 758 | {"diff", 0, "dom\0fetch\0storage\0" |
| 759 | /* maintenance note: "diff" needs "storage" for storing the |
| 760 | ** sbs-sync-scroll toggle. */}, |
| 761 |
+3
-2
| --- src/cgi.c | ||
| +++ src/cgi.c | ||
| @@ -1335,12 +1335,13 @@ | ||
| 1335 | 1335 | } |
| 1336 | 1336 | fossil_free(zErr); |
| 1337 | 1337 | } |
| 1338 | 1338 | } |
| 1339 | 1339 | if( !g.syncInfo.zLoginCard && 0!=(z=(char*)P("x-f-l-c")) ){ |
| 1340 | - /* x-f-l-c (X-Fossil-Login-Card card transmitted via cookie | |
| 1341 | - ** instead of in the sync payload. */ | |
| 1340 | + /* x-f-l-c (X-Fossil-Login-Card) transmitted via cookie instead of | |
| 1341 | + ** in the sync payload. The format of this value is the same as a | |
| 1342 | + ** "login" card, as parsed by xfer.c:page_xfer(). */ | |
| 1342 | 1343 | rc |= 0x04; |
| 1343 | 1344 | g.syncInfo.zLoginCard = fossil_strdup(z); |
| 1344 | 1345 | g.syncInfo.fLoginCardMode |= 0x02; |
| 1345 | 1346 | cgi_delete_parameter("x-f-l-c"); |
| 1346 | 1347 | } |
| 1347 | 1348 |
| --- src/cgi.c | |
| +++ src/cgi.c | |
| @@ -1335,12 +1335,13 @@ | |
| 1335 | } |
| 1336 | fossil_free(zErr); |
| 1337 | } |
| 1338 | } |
| 1339 | if( !g.syncInfo.zLoginCard && 0!=(z=(char*)P("x-f-l-c")) ){ |
| 1340 | /* x-f-l-c (X-Fossil-Login-Card card transmitted via cookie |
| 1341 | ** instead of in the sync payload. */ |
| 1342 | rc |= 0x04; |
| 1343 | g.syncInfo.zLoginCard = fossil_strdup(z); |
| 1344 | g.syncInfo.fLoginCardMode |= 0x02; |
| 1345 | cgi_delete_parameter("x-f-l-c"); |
| 1346 | } |
| 1347 |
| --- src/cgi.c | |
| +++ src/cgi.c | |
| @@ -1335,12 +1335,13 @@ | |
| 1335 | } |
| 1336 | fossil_free(zErr); |
| 1337 | } |
| 1338 | } |
| 1339 | if( !g.syncInfo.zLoginCard && 0!=(z=(char*)P("x-f-l-c")) ){ |
| 1340 | /* x-f-l-c (X-Fossil-Login-Card) transmitted via cookie instead of |
| 1341 | ** in the sync payload. The format of this value is the same as a |
| 1342 | ** "login" card, as parsed by xfer.c:page_xfer(). */ |
| 1343 | rc |= 0x04; |
| 1344 | g.syncInfo.zLoginCard = fossil_strdup(z); |
| 1345 | g.syncInfo.fLoginCardMode |= 0x02; |
| 1346 | cgi_delete_parameter("x-f-l-c"); |
| 1347 | } |
| 1348 |
+2
| --- src/comformat.c | ||
| +++ src/comformat.c | ||
| @@ -33,10 +33,12 @@ | ||
| 33 | 33 | */ |
| 34 | 34 | #define COMMENT_PRINT_TRIM_CRLF ((u32)0x00000002) /* Trim leading CR/LF. */ |
| 35 | 35 | #define COMMENT_PRINT_TRIM_SPACE ((u32)0x00000004) /* Trim leading/trailing. */ |
| 36 | 36 | #define COMMENT_PRINT_WORD_BREAK ((u32)0x00000008) /* Break lines on words. */ |
| 37 | 37 | #define COMMENT_PRINT_ORIG_BREAK ((u32)0x00000010) /* Break before original. */ |
| 38 | +#define COMMENT_PRINT_SUMMARY ((u32)0x00000020) | |
| 39 | + /* Truncate after first blank line. */ | |
| 38 | 40 | #endif |
| 39 | 41 | |
| 40 | 42 | /********* Code copied from SQLite src/shell.c.in on 2024-09-30 **********/ |
| 41 | 43 | /* Lookup table to estimate the number of columns consumed by a Unicode |
| 42 | 44 | ** character. |
| 43 | 45 |
| --- src/comformat.c | |
| +++ src/comformat.c | |
| @@ -33,10 +33,12 @@ | |
| 33 | */ |
| 34 | #define COMMENT_PRINT_TRIM_CRLF ((u32)0x00000002) /* Trim leading CR/LF. */ |
| 35 | #define COMMENT_PRINT_TRIM_SPACE ((u32)0x00000004) /* Trim leading/trailing. */ |
| 36 | #define COMMENT_PRINT_WORD_BREAK ((u32)0x00000008) /* Break lines on words. */ |
| 37 | #define COMMENT_PRINT_ORIG_BREAK ((u32)0x00000010) /* Break before original. */ |
| 38 | #endif |
| 39 | |
| 40 | /********* Code copied from SQLite src/shell.c.in on 2024-09-30 **********/ |
| 41 | /* Lookup table to estimate the number of columns consumed by a Unicode |
| 42 | ** character. |
| 43 |
| --- src/comformat.c | |
| +++ src/comformat.c | |
| @@ -33,10 +33,12 @@ | |
| 33 | */ |
| 34 | #define COMMENT_PRINT_TRIM_CRLF ((u32)0x00000002) /* Trim leading CR/LF. */ |
| 35 | #define COMMENT_PRINT_TRIM_SPACE ((u32)0x00000004) /* Trim leading/trailing. */ |
| 36 | #define COMMENT_PRINT_WORD_BREAK ((u32)0x00000008) /* Break lines on words. */ |
| 37 | #define COMMENT_PRINT_ORIG_BREAK ((u32)0x00000010) /* Break before original. */ |
| 38 | #define COMMENT_PRINT_SUMMARY ((u32)0x00000020) |
| 39 | /* Truncate after first blank line. */ |
| 40 | #endif |
| 41 | |
| 42 | /********* Code copied from SQLite src/shell.c.in on 2024-09-30 **********/ |
| 43 | /* Lookup table to estimate the number of columns consumed by a Unicode |
| 44 | ** character. |
| 45 |
+163
-1
| --- src/default.css | ||
| +++ src/default.css | ||
| @@ -1505,10 +1505,13 @@ | ||
| 1505 | 1505 | } |
| 1506 | 1506 | table.numbered-lines td.line-numbers > pre { |
| 1507 | 1507 | margin: 0.25em/*must match top PADDING of td.file-content |
| 1508 | 1508 | > pre > code*/ 0 0 0; |
| 1509 | 1509 | padding: 0; |
| 1510 | + line-height: inherit; | |
| 1511 | + font-size: inherit; | |
| 1512 | + font-family: inherit; | |
| 1510 | 1513 | } |
| 1511 | 1514 | table.numbered-lines td.line-numbers span { |
| 1512 | 1515 | display: inline-block; |
| 1513 | 1516 | margin: 0; |
| 1514 | 1517 | padding: 0; |
| @@ -1693,11 +1696,11 @@ | ||
| 1693 | 1696 | -0.6836761,0.240014 -1.4255375,0.720042 V 3.0698267 q 0.8800513,-0.3054724 1.6073661,-0.4509353 \ |
| 1694 | 1697 | 0.7273151,-0.145463 1.403718,-0.145463 1.7746486,0 2.7056104,0.727315 0.930965,0.720042 \ |
| 1695 | 1698 | 0.930965,2.1092135 0,0.7127686 -0.283654,1.2800746 -0.283652,0.5600324 -0.967329,1.2073428 \ |
| 1696 | 1699 | L 10.025425,8.2119439 Q 9.530851,8.6628792 9.3781148,8.9392588 9.2253789,9.2083654 \ |
| 1697 | 1700 | 9.2253789,9.535657 Z M 6.5997716,10.939376 h 2.6256073 v 2.589241 H 6.5997716 Z' \ |
| 1698 | -style='fill:%23f8f8f8;stroke-width:1.35412836' /%3e%3c/svg%3e "); | |
| 1701 | +style='fill:%23f8f8f8;stroke-width:1.35412836' /%3e%3c/svg%3e "); | |
| 1699 | 1702 | background-repeat: no-repeat; |
| 1700 | 1703 | background-position: center; |
| 1701 | 1704 | /* When not using a background image, this additional style works |
| 1702 | 1705 | reasonably well along with a ::before content of "?": */ |
| 1703 | 1706 | /*border-width: 1px; |
| @@ -2007,10 +2010,169 @@ | ||
| 2007 | 2010 | margin: 0; |
| 2008 | 2011 | } |
| 2009 | 2012 | div.helpPage blockquote { |
| 2010 | 2013 | margin-left: 0.2em; |
| 2011 | 2014 | } |
| 2015 | + | |
| 2016 | +/* .attach* = styles for file attachments */ | |
| 2017 | +section.attachlist {} | |
| 2018 | +details.attachlist > summary { | |
| 2019 | + cursor: pointer; | |
| 2020 | +} | |
| 2021 | +/* .Attacher is the top container element used by the JS Attacher | |
| 2022 | + class in fossil.attach.js. */ | |
| 2023 | +.Attacher { | |
| 2024 | + margin-bottom: 1em; | |
| 2025 | + display: flex; | |
| 2026 | + flex-direction: column; | |
| 2027 | + gap: 0.75em; | |
| 2028 | +} | |
| 2029 | +.Attacher.reverse { | |
| 2030 | + flex-direction: column-reverse; | |
| 2031 | +} | |
| 2032 | +.Attacher .attach-row { | |
| 2033 | + display: flex; | |
| 2034 | + flex-direction: column; | |
| 2035 | + gap: 0.5em; | |
| 2036 | + padding: 0.75em; | |
| 2037 | + border: 1px dashed #ccc; | |
| 2038 | + border-radius: 0.25em; | |
| 2039 | + background-color: #fafafa; | |
| 2040 | +} | |
| 2041 | +.Attacher .error { | |
| 2042 | + padding: 0.5em; | |
| 2043 | + background-color: #d32f2f; | |
| 2044 | + color: #fff; | |
| 2045 | +} | |
| 2046 | +.Attacher .error a { | |
| 2047 | + color: inherit; | |
| 2048 | + text-decoration: underline; | |
| 2049 | +} | |
| 2050 | +body.fossil-dark-style .Attacher .attach-row { | |
| 2051 | + background-color: initial; | |
| 2052 | +} | |
| 2053 | +.Attacher .attach-dropzone { | |
| 2054 | + padding: 1em; | |
| 2055 | + text-align: center; | |
| 2056 | + background: #ffffff; | |
| 2057 | + border: 1px solid #ddd; | |
| 2058 | + cursor: pointer; | |
| 2059 | + border-radius: 0.25em; | |
| 2060 | + transition: background-color 0.15s linear; | |
| 2061 | + display: flex; | |
| 2062 | + flex-direction: row; | |
| 2063 | + flex-wrap: nowrap; | |
| 2064 | +} | |
| 2065 | +body.fossil-dark-style .Attacher .attach-dropzone{ | |
| 2066 | + background: initial; | |
| 2067 | +} | |
| 2068 | +.Attacher .attach-dropzone.populated { | |
| 2069 | + background-color: #f1f8e9; | |
| 2070 | + border-color: #8bc34a; | |
| 2071 | + border-style: solid; | |
| 2072 | + text-align: left; | |
| 2073 | +} | |
| 2074 | +body.fossil-dark-style .Attacher .attach-dropzone.populated{ | |
| 2075 | + background-color: initial; | |
| 2076 | +} | |
| 2077 | +.Attacher .attach-dropzone.dragover { | |
| 2078 | + background-color: #e1f5fe; | |
| 2079 | + color: black; | |
| 2080 | + border-color: #03a9f4; | |
| 2081 | +} | |
| 2082 | +body.fossil-dark-style .Attacher .attach-dropzone.dragover{ | |
| 2083 | + background-color: #e1f5fe; | |
| 2084 | + border-color: #03a9f4; | |
| 2085 | +} | |
| 2086 | +.Attacher .thumbnail { | |
| 2087 | + max-width: 10em; | |
| 2088 | + max-height: 10em; | |
| 2089 | + margin: 0 1em; | |
| 2090 | +} | |
| 2091 | +.Attacher .attach-row-info{ | |
| 2092 | + font-family: monospace; | |
| 2093 | + flex-grow: 1; | |
| 2094 | + display: flex; | |
| 2095 | + flex-direction: column; | |
| 2096 | +} | |
| 2097 | +.Attacher .attach-filename {} | |
| 2098 | +.Attacher .attach-size {/*size and mimetype*/} | |
| 2099 | +.Attacher .attach-desc { | |
| 2100 | + max-width: initial; | |
| 2101 | + width: 100%; | |
| 2102 | + box-sizing: border-box; | |
| 2103 | + min-height: 4em; | |
| 2104 | + padding: 0.5em; | |
| 2105 | + font-family: inherit; | |
| 2106 | + resize: vertical; | |
| 2107 | +} | |
| 2108 | +.Attacher .attach-row-remove { | |
| 2109 | + align-self: center; | |
| 2110 | + padding: 0.25em 0.75em; | |
| 2111 | + margin-left: 1em; | |
| 2112 | + background-color: #d32f2f; | |
| 2113 | + color: #fff; | |
| 2114 | + border: none; | |
| 2115 | + border-radius: 0.25em; | |
| 2116 | + cursor: pointer; | |
| 2117 | + font-weight: bold; | |
| 2118 | +} | |
| 2119 | +.Attacher .attach-row-remove:hover { | |
| 2120 | + background-color: #b71c1c; | |
| 2121 | +} | |
| 2122 | +.Attacher .attach-controls { | |
| 2123 | + display: flex; | |
| 2124 | + flex-direction: row; | |
| 2125 | + gap: 1em; | |
| 2126 | +} | |
| 2127 | +.Attacher .attach-controls .attach-add-button { | |
| 2128 | + padding: 0.5em 1em; | |
| 2129 | + cursor: pointer; | |
| 2130 | + flex-grow: 2; | |
| 2131 | + line-height: initial/*work around an inherited alignment quirk*/; | |
| 2132 | +} | |
| 2133 | + | |
| 2134 | +/* .animate-X and their associated @keyframes are used by various | |
| 2135 | + widgets to animate their comings and goings. */ | |
| 2136 | +.animate-entrance { | |
| 2137 | + animation: slideFadeIn 0.25s linear forwards; | |
| 2138 | + transform-origin: top; | |
| 2139 | + overflow: hidden /*prevent content bleeding during expansion*/; | |
| 2140 | +} | |
| 2141 | +.animate-exit { | |
| 2142 | + animation: slideFadeOut 0.25s linear forwards; | |
| 2143 | + transform-origin: top; | |
| 2144 | + overflow: hidden; | |
| 2145 | +} | |
| 2146 | +@keyframes slideFadeIn { | |
| 2147 | + 0% { | |
| 2148 | + opacity: 0; | |
| 2149 | + transform: translateY(-1em/*must match slideFadeOut*/); | |
| 2150 | + max-height: 0; | |
| 2151 | + } | |
| 2152 | + 100% { | |
| 2153 | + opacity: 1; | |
| 2154 | + transform: translateY(0); | |
| 2155 | + max-height: 100em /*a value safely larger than the widget*/; | |
| 2156 | + } | |
| 2157 | +} | |
| 2158 | +@keyframes slideFadeOut { | |
| 2159 | + 0% { | |
| 2160 | + opacity: 1; | |
| 2161 | + transform: translateY(0); | |
| 2162 | + max-height: 100em /*must match slideFadeIn*/; | |
| 2163 | + } | |
| 2164 | + 100% { | |
| 2165 | + opacity: 0; | |
| 2166 | + transform: translateY(-1em/*must match slideFadeIn*/); | |
| 2167 | + max-height: 0; | |
| 2168 | + padding-top: 0; | |
| 2169 | + padding-bottom: 0; | |
| 2170 | + margin-top: 0; | |
| 2171 | + margin-bottom: 0; | |
| 2172 | + } | |
| 2173 | +} | |
| 2012 | 2174 | |
| 2013 | 2175 | /* Objects in the "desktoponly" class are invisible on mobile */ |
| 2014 | 2176 | @media screen and (max-width: 600px) { |
| 2015 | 2177 | .desktoponly { |
| 2016 | 2178 | display: none; |
| 2017 | 2179 |
| --- src/default.css | |
| +++ src/default.css | |
| @@ -1505,10 +1505,13 @@ | |
| 1505 | } |
| 1506 | table.numbered-lines td.line-numbers > pre { |
| 1507 | margin: 0.25em/*must match top PADDING of td.file-content |
| 1508 | > pre > code*/ 0 0 0; |
| 1509 | padding: 0; |
| 1510 | } |
| 1511 | table.numbered-lines td.line-numbers span { |
| 1512 | display: inline-block; |
| 1513 | margin: 0; |
| 1514 | padding: 0; |
| @@ -1693,11 +1696,11 @@ | |
| 1693 | -0.6836761,0.240014 -1.4255375,0.720042 V 3.0698267 q 0.8800513,-0.3054724 1.6073661,-0.4509353 \ |
| 1694 | 0.7273151,-0.145463 1.403718,-0.145463 1.7746486,0 2.7056104,0.727315 0.930965,0.720042 \ |
| 1695 | 0.930965,2.1092135 0,0.7127686 -0.283654,1.2800746 -0.283652,0.5600324 -0.967329,1.2073428 \ |
| 1696 | L 10.025425,8.2119439 Q 9.530851,8.6628792 9.3781148,8.9392588 9.2253789,9.2083654 \ |
| 1697 | 9.2253789,9.535657 Z M 6.5997716,10.939376 h 2.6256073 v 2.589241 H 6.5997716 Z' \ |
| 1698 | style='fill:%23f8f8f8;stroke-width:1.35412836' /%3e%3c/svg%3e "); |
| 1699 | background-repeat: no-repeat; |
| 1700 | background-position: center; |
| 1701 | /* When not using a background image, this additional style works |
| 1702 | reasonably well along with a ::before content of "?": */ |
| 1703 | /*border-width: 1px; |
| @@ -2007,10 +2010,169 @@ | |
| 2007 | margin: 0; |
| 2008 | } |
| 2009 | div.helpPage blockquote { |
| 2010 | margin-left: 0.2em; |
| 2011 | } |
| 2012 | |
| 2013 | /* Objects in the "desktoponly" class are invisible on mobile */ |
| 2014 | @media screen and (max-width: 600px) { |
| 2015 | .desktoponly { |
| 2016 | display: none; |
| 2017 |
| --- src/default.css | |
| +++ src/default.css | |
| @@ -1505,10 +1505,13 @@ | |
| 1505 | } |
| 1506 | table.numbered-lines td.line-numbers > pre { |
| 1507 | margin: 0.25em/*must match top PADDING of td.file-content |
| 1508 | > pre > code*/ 0 0 0; |
| 1509 | padding: 0; |
| 1510 | line-height: inherit; |
| 1511 | font-size: inherit; |
| 1512 | font-family: inherit; |
| 1513 | } |
| 1514 | table.numbered-lines td.line-numbers span { |
| 1515 | display: inline-block; |
| 1516 | margin: 0; |
| 1517 | padding: 0; |
| @@ -1693,11 +1696,11 @@ | |
| 1696 | -0.6836761,0.240014 -1.4255375,0.720042 V 3.0698267 q 0.8800513,-0.3054724 1.6073661,-0.4509353 \ |
| 1697 | 0.7273151,-0.145463 1.403718,-0.145463 1.7746486,0 2.7056104,0.727315 0.930965,0.720042 \ |
| 1698 | 0.930965,2.1092135 0,0.7127686 -0.283654,1.2800746 -0.283652,0.5600324 -0.967329,1.2073428 \ |
| 1699 | L 10.025425,8.2119439 Q 9.530851,8.6628792 9.3781148,8.9392588 9.2253789,9.2083654 \ |
| 1700 | 9.2253789,9.535657 Z M 6.5997716,10.939376 h 2.6256073 v 2.589241 H 6.5997716 Z' \ |
| 1701 | style='fill:%23f8f8f8;stroke-width:1.35412836' /%3e%3c/svg%3e "); |
| 1702 | background-repeat: no-repeat; |
| 1703 | background-position: center; |
| 1704 | /* When not using a background image, this additional style works |
| 1705 | reasonably well along with a ::before content of "?": */ |
| 1706 | /*border-width: 1px; |
| @@ -2007,10 +2010,169 @@ | |
| 2010 | margin: 0; |
| 2011 | } |
| 2012 | div.helpPage blockquote { |
| 2013 | margin-left: 0.2em; |
| 2014 | } |
| 2015 | |
| 2016 | /* .attach* = styles for file attachments */ |
| 2017 | section.attachlist {} |
| 2018 | details.attachlist > summary { |
| 2019 | cursor: pointer; |
| 2020 | } |
| 2021 | /* .Attacher is the top container element used by the JS Attacher |
| 2022 | class in fossil.attach.js. */ |
| 2023 | .Attacher { |
| 2024 | margin-bottom: 1em; |
| 2025 | display: flex; |
| 2026 | flex-direction: column; |
| 2027 | gap: 0.75em; |
| 2028 | } |
| 2029 | .Attacher.reverse { |
| 2030 | flex-direction: column-reverse; |
| 2031 | } |
| 2032 | .Attacher .attach-row { |
| 2033 | display: flex; |
| 2034 | flex-direction: column; |
| 2035 | gap: 0.5em; |
| 2036 | padding: 0.75em; |
| 2037 | border: 1px dashed #ccc; |
| 2038 | border-radius: 0.25em; |
| 2039 | background-color: #fafafa; |
| 2040 | } |
| 2041 | .Attacher .error { |
| 2042 | padding: 0.5em; |
| 2043 | background-color: #d32f2f; |
| 2044 | color: #fff; |
| 2045 | } |
| 2046 | .Attacher .error a { |
| 2047 | color: inherit; |
| 2048 | text-decoration: underline; |
| 2049 | } |
| 2050 | body.fossil-dark-style .Attacher .attach-row { |
| 2051 | background-color: initial; |
| 2052 | } |
| 2053 | .Attacher .attach-dropzone { |
| 2054 | padding: 1em; |
| 2055 | text-align: center; |
| 2056 | background: #ffffff; |
| 2057 | border: 1px solid #ddd; |
| 2058 | cursor: pointer; |
| 2059 | border-radius: 0.25em; |
| 2060 | transition: background-color 0.15s linear; |
| 2061 | display: flex; |
| 2062 | flex-direction: row; |
| 2063 | flex-wrap: nowrap; |
| 2064 | } |
| 2065 | body.fossil-dark-style .Attacher .attach-dropzone{ |
| 2066 | background: initial; |
| 2067 | } |
| 2068 | .Attacher .attach-dropzone.populated { |
| 2069 | background-color: #f1f8e9; |
| 2070 | border-color: #8bc34a; |
| 2071 | border-style: solid; |
| 2072 | text-align: left; |
| 2073 | } |
| 2074 | body.fossil-dark-style .Attacher .attach-dropzone.populated{ |
| 2075 | background-color: initial; |
| 2076 | } |
| 2077 | .Attacher .attach-dropzone.dragover { |
| 2078 | background-color: #e1f5fe; |
| 2079 | color: black; |
| 2080 | border-color: #03a9f4; |
| 2081 | } |
| 2082 | body.fossil-dark-style .Attacher .attach-dropzone.dragover{ |
| 2083 | background-color: #e1f5fe; |
| 2084 | border-color: #03a9f4; |
| 2085 | } |
| 2086 | .Attacher .thumbnail { |
| 2087 | max-width: 10em; |
| 2088 | max-height: 10em; |
| 2089 | margin: 0 1em; |
| 2090 | } |
| 2091 | .Attacher .attach-row-info{ |
| 2092 | font-family: monospace; |
| 2093 | flex-grow: 1; |
| 2094 | display: flex; |
| 2095 | flex-direction: column; |
| 2096 | } |
| 2097 | .Attacher .attach-filename {} |
| 2098 | .Attacher .attach-size {/*size and mimetype*/} |
| 2099 | .Attacher .attach-desc { |
| 2100 | max-width: initial; |
| 2101 | width: 100%; |
| 2102 | box-sizing: border-box; |
| 2103 | min-height: 4em; |
| 2104 | padding: 0.5em; |
| 2105 | font-family: inherit; |
| 2106 | resize: vertical; |
| 2107 | } |
| 2108 | .Attacher .attach-row-remove { |
| 2109 | align-self: center; |
| 2110 | padding: 0.25em 0.75em; |
| 2111 | margin-left: 1em; |
| 2112 | background-color: #d32f2f; |
| 2113 | color: #fff; |
| 2114 | border: none; |
| 2115 | border-radius: 0.25em; |
| 2116 | cursor: pointer; |
| 2117 | font-weight: bold; |
| 2118 | } |
| 2119 | .Attacher .attach-row-remove:hover { |
| 2120 | background-color: #b71c1c; |
| 2121 | } |
| 2122 | .Attacher .attach-controls { |
| 2123 | display: flex; |
| 2124 | flex-direction: row; |
| 2125 | gap: 1em; |
| 2126 | } |
| 2127 | .Attacher .attach-controls .attach-add-button { |
| 2128 | padding: 0.5em 1em; |
| 2129 | cursor: pointer; |
| 2130 | flex-grow: 2; |
| 2131 | line-height: initial/*work around an inherited alignment quirk*/; |
| 2132 | } |
| 2133 | |
| 2134 | /* .animate-X and their associated @keyframes are used by various |
| 2135 | widgets to animate their comings and goings. */ |
| 2136 | .animate-entrance { |
| 2137 | animation: slideFadeIn 0.25s linear forwards; |
| 2138 | transform-origin: top; |
| 2139 | overflow: hidden /*prevent content bleeding during expansion*/; |
| 2140 | } |
| 2141 | .animate-exit { |
| 2142 | animation: slideFadeOut 0.25s linear forwards; |
| 2143 | transform-origin: top; |
| 2144 | overflow: hidden; |
| 2145 | } |
| 2146 | @keyframes slideFadeIn { |
| 2147 | 0% { |
| 2148 | opacity: 0; |
| 2149 | transform: translateY(-1em/*must match slideFadeOut*/); |
| 2150 | max-height: 0; |
| 2151 | } |
| 2152 | 100% { |
| 2153 | opacity: 1; |
| 2154 | transform: translateY(0); |
| 2155 | max-height: 100em /*a value safely larger than the widget*/; |
| 2156 | } |
| 2157 | } |
| 2158 | @keyframes slideFadeOut { |
| 2159 | 0% { |
| 2160 | opacity: 1; |
| 2161 | transform: translateY(0); |
| 2162 | max-height: 100em /*must match slideFadeIn*/; |
| 2163 | } |
| 2164 | 100% { |
| 2165 | opacity: 0; |
| 2166 | transform: translateY(-1em/*must match slideFadeIn*/); |
| 2167 | max-height: 0; |
| 2168 | padding-top: 0; |
| 2169 | padding-bottom: 0; |
| 2170 | margin-top: 0; |
| 2171 | margin-bottom: 0; |
| 2172 | } |
| 2173 | } |
| 2174 | |
| 2175 | /* Objects in the "desktoponly" class are invisible on mobile */ |
| 2176 | @media screen and (max-width: 600px) { |
| 2177 | .desktoponly { |
| 2178 | display: none; |
| 2179 |
+26
-14
| --- src/delta.c | ||
| +++ src/delta.c | ||
| @@ -186,20 +186,29 @@ | ||
| 186 | 186 | 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -1, -1, -1, -1, -1, -1, |
| 187 | 187 | -1, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, |
| 188 | 188 | 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, -1, -1, -1, -1, 36, |
| 189 | 189 | -1, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, |
| 190 | 190 | 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, -1, -1, -1, 63, -1, |
| 191 | + | |
| 192 | + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, | |
| 193 | + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, | |
| 194 | + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, | |
| 195 | + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, | |
| 196 | + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, | |
| 197 | + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, | |
| 198 | + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, | |
| 199 | + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, | |
| 191 | 200 | }; |
| 192 | 201 | unsigned int v = 0; |
| 193 | 202 | int c; |
| 194 | 203 | unsigned char *z = (unsigned char*)*pz; |
| 195 | - unsigned char *zStart = z; | |
| 196 | - while( (c = zValue[0x7f&*(z++)])>=0 ){ | |
| 197 | - v = (v<<6) + c; | |
| 204 | + unsigned char *zEnd = z + (*pLen); | |
| 205 | + while( z<zEnd && (c = zValue[*z])>=0 ){ | |
| 206 | + v = (v<<6) + c; | |
| 207 | + z++; | |
| 198 | 208 | } |
| 199 | - z--; | |
| 200 | - *pLen -= z - zStart; | |
| 209 | + *pLen -= (int)(z - (unsigned char*)*pz); | |
| 201 | 210 | *pz = (char*)z; |
| 202 | 211 | return v; |
| 203 | 212 | } |
| 204 | 213 | |
| 205 | 214 | /* |
| @@ -537,11 +546,11 @@ | ||
| 537 | 546 | ** needed. |
| 538 | 547 | */ |
| 539 | 548 | int delta_output_size(const char *zDelta, int lenDelta){ |
| 540 | 549 | int size; |
| 541 | 550 | size = getInt(&zDelta, &lenDelta); |
| 542 | - if( *zDelta!='\n' ){ | |
| 551 | + if( lenDelta<=0 || *zDelta!='\n' ){ | |
| 543 | 552 | /* ERROR: size integer not terminated by "\n" */ |
| 544 | 553 | return -1; |
| 545 | 554 | } |
| 546 | 555 | return size; |
| 547 | 556 | } |
| @@ -574,28 +583,30 @@ | ||
| 574 | 583 | int lenDelta, /* Length of the delta */ |
| 575 | 584 | char *zOut /* Write the output into this preallocated buffer */ |
| 576 | 585 | ){ |
| 577 | 586 | sqlite3_uint64 limit; |
| 578 | 587 | sqlite3_uint64 total = 0; |
| 588 | + | |
| 579 | 589 | #ifdef FOSSIL_ENABLE_DELTA_CKSUM_TEST |
| 580 | 590 | char *zOrigOut = zOut; |
| 581 | 591 | #endif |
| 582 | 592 | |
| 583 | 593 | limit = getInt(&zDelta, &lenDelta); |
| 584 | - if( *zDelta!='\n' ){ | |
| 594 | + if( lenDelta<=0 || *zDelta!='\n' ){ | |
| 585 | 595 | /* ERROR: size integer not terminated by "\n" */ |
| 586 | 596 | return -1; |
| 587 | 597 | } |
| 588 | - zDelta++; lenDelta--; | |
| 589 | - while( *zDelta && lenDelta>0 ){ | |
| 598 | + zDelta++; lenDelta--; /* Skip the \n */ | |
| 599 | + while( lenDelta>0 && zDelta[0] ){ | |
| 590 | 600 | unsigned int cnt, ofst; |
| 591 | 601 | cnt = getInt(&zDelta, &lenDelta); |
| 602 | + if( lenDelta<=0 ) return -1; | |
| 592 | 603 | switch( zDelta[0] ){ |
| 593 | 604 | case '@': { |
| 594 | 605 | zDelta++; lenDelta--; |
| 595 | 606 | ofst = getInt(&zDelta, &lenDelta); |
| 596 | - if( lenDelta>0 && zDelta[0]!=',' ){ | |
| 607 | + if( lenDelta<=0 || zDelta[0]!=',' ){ | |
| 597 | 608 | /* ERROR: copy command not terminated by ',' */ |
| 598 | 609 | return -1; |
| 599 | 610 | } |
| 600 | 611 | zDelta++; lenDelta--; |
| 601 | 612 | DEBUG1( printf("COPY %d from %d\n", cnt, ofst); ) |
| @@ -618,11 +629,11 @@ | ||
| 618 | 629 | if( total>limit ){ |
| 619 | 630 | /* ERROR: insert command gives an output larger than predicted */ |
| 620 | 631 | return -1; |
| 621 | 632 | } |
| 622 | 633 | DEBUG1( printf("INSERT %d\n", cnt); ) |
| 623 | - if( (int)cnt>lenDelta ){ | |
| 634 | + if( (i64)cnt>(i64)lenDelta ){ | |
| 624 | 635 | /* ERROR: insert count exceeds size of delta */ |
| 625 | 636 | return -1; |
| 626 | 637 | } |
| 627 | 638 | memcpy(zOut, zDelta, cnt); |
| 628 | 639 | zOut += cnt; |
| @@ -668,23 +679,24 @@ | ||
| 668 | 679 | ){ |
| 669 | 680 | unsigned int nInsert = 0; |
| 670 | 681 | unsigned int nCopy = 0; |
| 671 | 682 | |
| 672 | 683 | (void)getInt(&zDelta, &lenDelta); |
| 673 | - if( *zDelta!='\n' ){ | |
| 684 | + if( lenDelta<=0 || *zDelta!='\n' ){ | |
| 674 | 685 | /* ERROR: size integer not terminated by "\n" */ |
| 675 | 686 | return -1; |
| 676 | 687 | } |
| 677 | 688 | zDelta++; lenDelta--; |
| 678 | 689 | while( *zDelta && lenDelta>0 ){ |
| 679 | 690 | unsigned int cnt; |
| 680 | 691 | cnt = getInt(&zDelta, &lenDelta); |
| 692 | + if( lenDelta<=0 ) break; | |
| 681 | 693 | switch( zDelta[0] ){ |
| 682 | 694 | case '@': { |
| 683 | 695 | zDelta++; lenDelta--; |
| 684 | 696 | (void)getInt(&zDelta, &lenDelta); |
| 685 | - if( lenDelta>0 && zDelta[0]!=',' ){ | |
| 697 | + if( lenDelta<=0 || zDelta[0]!=',' ){ | |
| 686 | 698 | /* ERROR: copy command not terminated by ',' */ |
| 687 | 699 | return -1; |
| 688 | 700 | } |
| 689 | 701 | zDelta++; lenDelta--; |
| 690 | 702 | nCopy += cnt; |
| @@ -691,11 +703,11 @@ | ||
| 691 | 703 | break; |
| 692 | 704 | } |
| 693 | 705 | case ':': { |
| 694 | 706 | zDelta++; lenDelta--; |
| 695 | 707 | nInsert += cnt; |
| 696 | - if( (int)cnt>lenDelta ){ | |
| 708 | + if( (i64)cnt>(i64)lenDelta ){ | |
| 697 | 709 | /* ERROR: insert count exceeds size of delta */ |
| 698 | 710 | return -1; |
| 699 | 711 | } |
| 700 | 712 | zDelta += cnt; |
| 701 | 713 | lenDelta -= cnt; |
| 702 | 714 |
| --- src/delta.c | |
| +++ src/delta.c | |
| @@ -186,20 +186,29 @@ | |
| 186 | 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -1, -1, -1, -1, -1, -1, |
| 187 | -1, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, |
| 188 | 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, -1, -1, -1, -1, 36, |
| 189 | -1, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, |
| 190 | 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, -1, -1, -1, 63, -1, |
| 191 | }; |
| 192 | unsigned int v = 0; |
| 193 | int c; |
| 194 | unsigned char *z = (unsigned char*)*pz; |
| 195 | unsigned char *zStart = z; |
| 196 | while( (c = zValue[0x7f&*(z++)])>=0 ){ |
| 197 | v = (v<<6) + c; |
| 198 | } |
| 199 | z--; |
| 200 | *pLen -= z - zStart; |
| 201 | *pz = (char*)z; |
| 202 | return v; |
| 203 | } |
| 204 | |
| 205 | /* |
| @@ -537,11 +546,11 @@ | |
| 537 | ** needed. |
| 538 | */ |
| 539 | int delta_output_size(const char *zDelta, int lenDelta){ |
| 540 | int size; |
| 541 | size = getInt(&zDelta, &lenDelta); |
| 542 | if( *zDelta!='\n' ){ |
| 543 | /* ERROR: size integer not terminated by "\n" */ |
| 544 | return -1; |
| 545 | } |
| 546 | return size; |
| 547 | } |
| @@ -574,28 +583,30 @@ | |
| 574 | int lenDelta, /* Length of the delta */ |
| 575 | char *zOut /* Write the output into this preallocated buffer */ |
| 576 | ){ |
| 577 | sqlite3_uint64 limit; |
| 578 | sqlite3_uint64 total = 0; |
| 579 | #ifdef FOSSIL_ENABLE_DELTA_CKSUM_TEST |
| 580 | char *zOrigOut = zOut; |
| 581 | #endif |
| 582 | |
| 583 | limit = getInt(&zDelta, &lenDelta); |
| 584 | if( *zDelta!='\n' ){ |
| 585 | /* ERROR: size integer not terminated by "\n" */ |
| 586 | return -1; |
| 587 | } |
| 588 | zDelta++; lenDelta--; |
| 589 | while( *zDelta && lenDelta>0 ){ |
| 590 | unsigned int cnt, ofst; |
| 591 | cnt = getInt(&zDelta, &lenDelta); |
| 592 | switch( zDelta[0] ){ |
| 593 | case '@': { |
| 594 | zDelta++; lenDelta--; |
| 595 | ofst = getInt(&zDelta, &lenDelta); |
| 596 | if( lenDelta>0 && zDelta[0]!=',' ){ |
| 597 | /* ERROR: copy command not terminated by ',' */ |
| 598 | return -1; |
| 599 | } |
| 600 | zDelta++; lenDelta--; |
| 601 | DEBUG1( printf("COPY %d from %d\n", cnt, ofst); ) |
| @@ -618,11 +629,11 @@ | |
| 618 | if( total>limit ){ |
| 619 | /* ERROR: insert command gives an output larger than predicted */ |
| 620 | return -1; |
| 621 | } |
| 622 | DEBUG1( printf("INSERT %d\n", cnt); ) |
| 623 | if( (int)cnt>lenDelta ){ |
| 624 | /* ERROR: insert count exceeds size of delta */ |
| 625 | return -1; |
| 626 | } |
| 627 | memcpy(zOut, zDelta, cnt); |
| 628 | zOut += cnt; |
| @@ -668,23 +679,24 @@ | |
| 668 | ){ |
| 669 | unsigned int nInsert = 0; |
| 670 | unsigned int nCopy = 0; |
| 671 | |
| 672 | (void)getInt(&zDelta, &lenDelta); |
| 673 | if( *zDelta!='\n' ){ |
| 674 | /* ERROR: size integer not terminated by "\n" */ |
| 675 | return -1; |
| 676 | } |
| 677 | zDelta++; lenDelta--; |
| 678 | while( *zDelta && lenDelta>0 ){ |
| 679 | unsigned int cnt; |
| 680 | cnt = getInt(&zDelta, &lenDelta); |
| 681 | switch( zDelta[0] ){ |
| 682 | case '@': { |
| 683 | zDelta++; lenDelta--; |
| 684 | (void)getInt(&zDelta, &lenDelta); |
| 685 | if( lenDelta>0 && zDelta[0]!=',' ){ |
| 686 | /* ERROR: copy command not terminated by ',' */ |
| 687 | return -1; |
| 688 | } |
| 689 | zDelta++; lenDelta--; |
| 690 | nCopy += cnt; |
| @@ -691,11 +703,11 @@ | |
| 691 | break; |
| 692 | } |
| 693 | case ':': { |
| 694 | zDelta++; lenDelta--; |
| 695 | nInsert += cnt; |
| 696 | if( (int)cnt>lenDelta ){ |
| 697 | /* ERROR: insert count exceeds size of delta */ |
| 698 | return -1; |
| 699 | } |
| 700 | zDelta += cnt; |
| 701 | lenDelta -= cnt; |
| 702 |
| --- src/delta.c | |
| +++ src/delta.c | |
| @@ -186,20 +186,29 @@ | |
| 186 | 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -1, -1, -1, -1, -1, -1, |
| 187 | -1, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, |
| 188 | 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, -1, -1, -1, -1, 36, |
| 189 | -1, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, |
| 190 | 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, -1, -1, -1, 63, -1, |
| 191 | |
| 192 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, |
| 193 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, |
| 194 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, |
| 195 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, |
| 196 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, |
| 197 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, |
| 198 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, |
| 199 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, |
| 200 | }; |
| 201 | unsigned int v = 0; |
| 202 | int c; |
| 203 | unsigned char *z = (unsigned char*)*pz; |
| 204 | unsigned char *zEnd = z + (*pLen); |
| 205 | while( z<zEnd && (c = zValue[*z])>=0 ){ |
| 206 | v = (v<<6) + c; |
| 207 | z++; |
| 208 | } |
| 209 | *pLen -= (int)(z - (unsigned char*)*pz); |
| 210 | *pz = (char*)z; |
| 211 | return v; |
| 212 | } |
| 213 | |
| 214 | /* |
| @@ -537,11 +546,11 @@ | |
| 546 | ** needed. |
| 547 | */ |
| 548 | int delta_output_size(const char *zDelta, int lenDelta){ |
| 549 | int size; |
| 550 | size = getInt(&zDelta, &lenDelta); |
| 551 | if( lenDelta<=0 || *zDelta!='\n' ){ |
| 552 | /* ERROR: size integer not terminated by "\n" */ |
| 553 | return -1; |
| 554 | } |
| 555 | return size; |
| 556 | } |
| @@ -574,28 +583,30 @@ | |
| 583 | int lenDelta, /* Length of the delta */ |
| 584 | char *zOut /* Write the output into this preallocated buffer */ |
| 585 | ){ |
| 586 | sqlite3_uint64 limit; |
| 587 | sqlite3_uint64 total = 0; |
| 588 | |
| 589 | #ifdef FOSSIL_ENABLE_DELTA_CKSUM_TEST |
| 590 | char *zOrigOut = zOut; |
| 591 | #endif |
| 592 | |
| 593 | limit = getInt(&zDelta, &lenDelta); |
| 594 | if( lenDelta<=0 || *zDelta!='\n' ){ |
| 595 | /* ERROR: size integer not terminated by "\n" */ |
| 596 | return -1; |
| 597 | } |
| 598 | zDelta++; lenDelta--; /* Skip the \n */ |
| 599 | while( lenDelta>0 && zDelta[0] ){ |
| 600 | unsigned int cnt, ofst; |
| 601 | cnt = getInt(&zDelta, &lenDelta); |
| 602 | if( lenDelta<=0 ) return -1; |
| 603 | switch( zDelta[0] ){ |
| 604 | case '@': { |
| 605 | zDelta++; lenDelta--; |
| 606 | ofst = getInt(&zDelta, &lenDelta); |
| 607 | if( lenDelta<=0 || zDelta[0]!=',' ){ |
| 608 | /* ERROR: copy command not terminated by ',' */ |
| 609 | return -1; |
| 610 | } |
| 611 | zDelta++; lenDelta--; |
| 612 | DEBUG1( printf("COPY %d from %d\n", cnt, ofst); ) |
| @@ -618,11 +629,11 @@ | |
| 629 | if( total>limit ){ |
| 630 | /* ERROR: insert command gives an output larger than predicted */ |
| 631 | return -1; |
| 632 | } |
| 633 | DEBUG1( printf("INSERT %d\n", cnt); ) |
| 634 | if( (i64)cnt>(i64)lenDelta ){ |
| 635 | /* ERROR: insert count exceeds size of delta */ |
| 636 | return -1; |
| 637 | } |
| 638 | memcpy(zOut, zDelta, cnt); |
| 639 | zOut += cnt; |
| @@ -668,23 +679,24 @@ | |
| 679 | ){ |
| 680 | unsigned int nInsert = 0; |
| 681 | unsigned int nCopy = 0; |
| 682 | |
| 683 | (void)getInt(&zDelta, &lenDelta); |
| 684 | if( lenDelta<=0 || *zDelta!='\n' ){ |
| 685 | /* ERROR: size integer not terminated by "\n" */ |
| 686 | return -1; |
| 687 | } |
| 688 | zDelta++; lenDelta--; |
| 689 | while( *zDelta && lenDelta>0 ){ |
| 690 | unsigned int cnt; |
| 691 | cnt = getInt(&zDelta, &lenDelta); |
| 692 | if( lenDelta<=0 ) break; |
| 693 | switch( zDelta[0] ){ |
| 694 | case '@': { |
| 695 | zDelta++; lenDelta--; |
| 696 | (void)getInt(&zDelta, &lenDelta); |
| 697 | if( lenDelta<=0 || zDelta[0]!=',' ){ |
| 698 | /* ERROR: copy command not terminated by ',' */ |
| 699 | return -1; |
| 700 | } |
| 701 | zDelta++; lenDelta--; |
| 702 | nCopy += cnt; |
| @@ -691,11 +703,11 @@ | |
| 703 | break; |
| 704 | } |
| 705 | case ':': { |
| 706 | zDelta++; lenDelta--; |
| 707 | nInsert += cnt; |
| 708 | if( (i64)cnt>(i64)lenDelta ){ |
| 709 | /* ERROR: insert count exceeds size of delta */ |
| 710 | return -1; |
| 711 | } |
| 712 | zDelta += cnt; |
| 713 | lenDelta -= cnt; |
| 714 |
+1
-1
| --- src/diffcmd.c | ||
| +++ src/diffcmd.c | ||
| @@ -776,11 +776,11 @@ | ||
| 776 | 776 | |
| 777 | 777 | /* |
| 778 | 778 | ** Return true if the disk file is identical to the Blob. Return zero |
| 779 | 779 | ** if the files differ in any way. |
| 780 | 780 | */ |
| 781 | -static int file_same_as_blob(Blob *blob, const char *zDiskFile){ | |
| 781 | +int file_same_as_blob(Blob *blob, const char *zDiskFile){ | |
| 782 | 782 | Blob file; |
| 783 | 783 | int rc = 0; |
| 784 | 784 | if( blob_size(blob)!=file_size(zDiskFile, ExtFILE) ) return 0; |
| 785 | 785 | blob_zero(&file); |
| 786 | 786 | blob_read_from_file(&file, zDiskFile, ExtFILE); |
| 787 | 787 |
| --- src/diffcmd.c | |
| +++ src/diffcmd.c | |
| @@ -776,11 +776,11 @@ | |
| 776 | |
| 777 | /* |
| 778 | ** Return true if the disk file is identical to the Blob. Return zero |
| 779 | ** if the files differ in any way. |
| 780 | */ |
| 781 | static int file_same_as_blob(Blob *blob, const char *zDiskFile){ |
| 782 | Blob file; |
| 783 | int rc = 0; |
| 784 | if( blob_size(blob)!=file_size(zDiskFile, ExtFILE) ) return 0; |
| 785 | blob_zero(&file); |
| 786 | blob_read_from_file(&file, zDiskFile, ExtFILE); |
| 787 |
| --- src/diffcmd.c | |
| +++ src/diffcmd.c | |
| @@ -776,11 +776,11 @@ | |
| 776 | |
| 777 | /* |
| 778 | ** Return true if the disk file is identical to the Blob. Return zero |
| 779 | ** if the files differ in any way. |
| 780 | */ |
| 781 | int file_same_as_blob(Blob *blob, const char *zDiskFile){ |
| 782 | Blob file; |
| 783 | int rc = 0; |
| 784 | if( blob_size(blob)!=file_size(zDiskFile, ExtFILE) ) return 0; |
| 785 | blob_zero(&file); |
| 786 | blob_read_from_file(&file, zDiskFile, ExtFILE); |
| 787 |
+14
-7
| --- src/event.c | ||
| +++ src/event.c | ||
| @@ -117,11 +117,15 @@ | ||
| 117 | 117 | style_header("No Such Tech-Note"); |
| 118 | 118 | @ Cannot locate a technical note called <b>%h(zId)</b>. |
| 119 | 119 | style_finish_page(); |
| 120 | 120 | return; |
| 121 | 121 | } |
| 122 | - zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid); | |
| 122 | + zUuid = rid_to_uuid(rid); | |
| 123 | + zFullId = db_text(0, "SELECT SUBSTR(tagname,7)" | |
| 124 | + " FROM tag" | |
| 125 | + " WHERE tagname GLOB 'event-%q*'", | |
| 126 | + zId); | |
| 123 | 127 | zVerbose = P("v"); |
| 124 | 128 | if( !zVerbose ){ |
| 125 | 129 | zVerbose = P("verbose"); |
| 126 | 130 | } |
| 127 | 131 | if( !zVerbose ){ |
| @@ -157,11 +161,11 @@ | ||
| 157 | 161 | style_header("%s", blob_str(&title)); |
| 158 | 162 | if( g.perm.WrWiki && g.perm.Write && nextRid==0 ){ |
| 159 | 163 | style_submenu_element("Edit", "%R/technoteedit?name=%!S", zId); |
| 160 | 164 | if( g.perm.Attach ){ |
| 161 | 165 | style_submenu_element("Attach", |
| 162 | - "%R/attachadd?technote=%!S&from=%R/technote/%!S", zId, zId); | |
| 166 | + "%R/attachadd?target=%s&from=%R/technote/%!S", zFullId, zId); | |
| 163 | 167 | } |
| 164 | 168 | } |
| 165 | 169 | zETime = db_text(0, "SELECT datetime(%.17g)", pTNote->rEventDate); |
| 166 | 170 | style_submenu_element("Context", "%R/timeline?c=%.20s", zId); |
| 167 | 171 | if( g.perm.Hyperlink ){ |
| @@ -225,15 +229,18 @@ | ||
| 225 | 229 | }else{ |
| 226 | 230 | @ <pre> |
| 227 | 231 | @ %h(blob_str(&fullbody)) |
| 228 | 232 | @ </pre> |
| 229 | 233 | } |
| 230 | - zFullId = db_text(0, "SELECT SUBSTR(tagname,7)" | |
| 231 | - " FROM tag" | |
| 232 | - " WHERE tagname GLOB 'event-%q*'", | |
| 233 | - zId); | |
| 234 | - attachment_list(zFullId, "<h2>Attachments:</h2>", 1); | |
| 234 | + { | |
| 235 | + char * z = mprintf( | |
| 236 | + "<h2><a href='%R/attachlist?technote=%t'>Attachments</a>:</h2>", | |
| 237 | + zFullId | |
| 238 | + ); | |
| 239 | + attachment_list(zFullId, z, 1); | |
| 240 | + fossil_free(z); | |
| 241 | + } | |
| 235 | 242 | document_emit_js(); |
| 236 | 243 | style_finish_page(); |
| 237 | 244 | manifest_destroy(pTNote); |
| 238 | 245 | } |
| 239 | 246 | |
| 240 | 247 |
| --- src/event.c | |
| +++ src/event.c | |
| @@ -117,11 +117,15 @@ | |
| 117 | style_header("No Such Tech-Note"); |
| 118 | @ Cannot locate a technical note called <b>%h(zId)</b>. |
| 119 | style_finish_page(); |
| 120 | return; |
| 121 | } |
| 122 | zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid); |
| 123 | zVerbose = P("v"); |
| 124 | if( !zVerbose ){ |
| 125 | zVerbose = P("verbose"); |
| 126 | } |
| 127 | if( !zVerbose ){ |
| @@ -157,11 +161,11 @@ | |
| 157 | style_header("%s", blob_str(&title)); |
| 158 | if( g.perm.WrWiki && g.perm.Write && nextRid==0 ){ |
| 159 | style_submenu_element("Edit", "%R/technoteedit?name=%!S", zId); |
| 160 | if( g.perm.Attach ){ |
| 161 | style_submenu_element("Attach", |
| 162 | "%R/attachadd?technote=%!S&from=%R/technote/%!S", zId, zId); |
| 163 | } |
| 164 | } |
| 165 | zETime = db_text(0, "SELECT datetime(%.17g)", pTNote->rEventDate); |
| 166 | style_submenu_element("Context", "%R/timeline?c=%.20s", zId); |
| 167 | if( g.perm.Hyperlink ){ |
| @@ -225,15 +229,18 @@ | |
| 225 | }else{ |
| 226 | @ <pre> |
| 227 | @ %h(blob_str(&fullbody)) |
| 228 | @ </pre> |
| 229 | } |
| 230 | zFullId = db_text(0, "SELECT SUBSTR(tagname,7)" |
| 231 | " FROM tag" |
| 232 | " WHERE tagname GLOB 'event-%q*'", |
| 233 | zId); |
| 234 | attachment_list(zFullId, "<h2>Attachments:</h2>", 1); |
| 235 | document_emit_js(); |
| 236 | style_finish_page(); |
| 237 | manifest_destroy(pTNote); |
| 238 | } |
| 239 | |
| 240 |
| --- src/event.c | |
| +++ src/event.c | |
| @@ -117,11 +117,15 @@ | |
| 117 | style_header("No Such Tech-Note"); |
| 118 | @ Cannot locate a technical note called <b>%h(zId)</b>. |
| 119 | style_finish_page(); |
| 120 | return; |
| 121 | } |
| 122 | zUuid = rid_to_uuid(rid); |
| 123 | zFullId = db_text(0, "SELECT SUBSTR(tagname,7)" |
| 124 | " FROM tag" |
| 125 | " WHERE tagname GLOB 'event-%q*'", |
| 126 | zId); |
| 127 | zVerbose = P("v"); |
| 128 | if( !zVerbose ){ |
| 129 | zVerbose = P("verbose"); |
| 130 | } |
| 131 | if( !zVerbose ){ |
| @@ -157,11 +161,11 @@ | |
| 161 | style_header("%s", blob_str(&title)); |
| 162 | if( g.perm.WrWiki && g.perm.Write && nextRid==0 ){ |
| 163 | style_submenu_element("Edit", "%R/technoteedit?name=%!S", zId); |
| 164 | if( g.perm.Attach ){ |
| 165 | style_submenu_element("Attach", |
| 166 | "%R/attachadd?target=%s&from=%R/technote/%!S", zFullId, zId); |
| 167 | } |
| 168 | } |
| 169 | zETime = db_text(0, "SELECT datetime(%.17g)", pTNote->rEventDate); |
| 170 | style_submenu_element("Context", "%R/timeline?c=%.20s", zId); |
| 171 | if( g.perm.Hyperlink ){ |
| @@ -225,15 +229,18 @@ | |
| 229 | }else{ |
| 230 | @ <pre> |
| 231 | @ %h(blob_str(&fullbody)) |
| 232 | @ </pre> |
| 233 | } |
| 234 | { |
| 235 | char * z = mprintf( |
| 236 | "<h2><a href='%R/attachlist?technote=%t'>Attachments</a>:</h2>", |
| 237 | zFullId |
| 238 | ); |
| 239 | attachment_list(zFullId, z, 1); |
| 240 | fossil_free(z); |
| 241 | } |
| 242 | document_emit_js(); |
| 243 | style_finish_page(); |
| 244 | manifest_destroy(pTNote); |
| 245 | } |
| 246 | |
| 247 |
+542
-81
| --- src/forum.c | ||
| +++ src/forum.c | ||
| @@ -96,11 +96,11 @@ | ||
| 96 | 96 | |
| 97 | 97 | /* |
| 98 | 98 | ** Returns a high-level representation of the forum-statuses setting. |
| 99 | 99 | ** This is a singleton, cached across calls. |
| 100 | 100 | */ |
| 101 | -static const ForumStatusList * forum_statuses(void){ | |
| 101 | +const ForumStatusList * forum_statuses(void){ | |
| 102 | 102 | static ForumStatusList fses = {0,0}; |
| 103 | 103 | static int once = 0; |
| 104 | 104 | while( !once ){ |
| 105 | 105 | ++once; |
| 106 | 106 | /* Read `forum-statuses` setting and transform it into the |
| @@ -151,11 +151,13 @@ | ||
| 151 | 151 | ** found, the corresponding object is returned. If no match is found |
| 152 | 152 | ** then (A) if bFirst is false then 0 is returned, else (B) the first |
| 153 | 153 | ** entry in the list is returned, noting that the list may be empty, |
| 154 | 154 | ** in which case 0 is returned. |
| 155 | 155 | */ |
| 156 | -const ForumStatus * forum_status_by_value(const char *z, int bFirst){ | |
| 156 | +static const ForumStatus * forum_status_by_value( | |
| 157 | + const char *z, int bFirst | |
| 158 | +){ | |
| 157 | 159 | const ForumStatusList * const fses = forum_statuses(); |
| 158 | 160 | const ForumStatus * fs0 = 0; |
| 159 | 161 | unsigned int i; |
| 160 | 162 | if( !fses->n ) return 0; |
| 161 | 163 | for( i = 0; i < fses->n; ++i ){ |
| @@ -227,18 +229,18 @@ | ||
| 227 | 229 | } |
| 228 | 230 | |
| 229 | 231 | /* |
| 230 | 232 | ** Works like forumpost_head_rid() but expects zUuid to be an |
| 231 | 233 | ** unambiguous forum post name. It may be a hash prefix, so long as |
| 232 | -** it's unambiguous. Returns 0 if the name cannot be unambiguously | |
| 233 | -** resolved as a forum post. | |
| 234 | +** it's unambiguous. Returns the rid of the head post, -1 if the name | |
| 235 | +** is ambiguous, and 0 if the name cannot be resolved as a forum post. | |
| 234 | 236 | */ |
| 235 | 237 | int forumpost_head_rid2(const char *zUuid){ |
| 236 | 238 | const int fpid = symbolic_name_to_rid(zUuid, "f"); |
| 237 | 239 | return fpid>0 |
| 238 | 240 | ? forumpost_head_rid(fpid) |
| 239 | - : 0; | |
| 241 | + : fpid; | |
| 240 | 242 | } |
| 241 | 243 | |
| 242 | 244 | /* |
| 243 | 245 | ** Given a forum post RID and user name, returns true if zUserName |
| 244 | 246 | ** matches the event.(euser,user) field for a formpost entry with the |
| @@ -408,18 +410,17 @@ | ||
| 408 | 410 | ** no tag is added. Similarly, it will only remove a tag from a post |
| 409 | 411 | ** which has its own tag, and will not remove an inherited one from a |
| 410 | 412 | ** parent post. |
| 411 | 413 | ** |
| 412 | 414 | ** If addTag is true and frid is already tagged, this is a |
| 413 | -** no-op. Likewise, if addTag is false and frid is not tagged | |
| 414 | -** (not accounting for an inherited closed tag), this is a no-op. | |
| 415 | -** | |
| 416 | -** If bCheckIrt is true then the forum post IRT hierarchy is searched | |
| 417 | -** for the tag, otherwise only the given RID is checked. | |
| 418 | -** | |
| 419 | -** Returns true if it actually creates a new tag, else false. Fails | |
| 420 | -** fatally on error. | |
| 415 | +** no-op. Likewise, if addTag is false and frid is not tagged (not | |
| 416 | +** accounting for a tag inherited via an in-response-to post), this is | |
| 417 | +** a no-op. | |
| 418 | +** | |
| 419 | +** Returns a positive value (a new tag.tagid value) if it actually | |
| 420 | +** creates a new tag, else 0. On error it returns a negative alue | |
| 421 | +** and g.zErrMsg "should" contain details. | |
| 421 | 422 | ** |
| 422 | 423 | ** If it returns true then state from previously-loaded posts may be |
| 423 | 424 | ** invalidated if they refer to the amended post or a response to it. |
| 424 | 425 | ** e.g. if zTagName is "closed" then ForumPost::iClosed values may be |
| 425 | 426 | ** stale. |
| @@ -438,11 +439,12 @@ | ||
| 438 | 439 | ** |
| 439 | 440 | ** - The applied tag is propagating so so that "closed" tags can |
| 440 | 441 | ** account for how edits of posts are handled. This differs from |
| 441 | 442 | ** closure of a branch, where a non-propagating tag is used. |
| 442 | 443 | */ |
| 443 | -static int forumpost_tag(int frid, const char *zTagName, int addTag, | |
| 444 | +static int forumpost_tag(int frid, int addTag, | |
| 445 | + const char *zTagName, | |
| 444 | 446 | const char *zValue){ |
| 445 | 447 | Blob artifact = BLOB_INITIALIZER; /* Output artifact */ |
| 446 | 448 | Blob cksum = BLOB_INITIALIZER; /* Z-card */ |
| 447 | 449 | int iTagged; /* true if frid is already tagged */ |
| 448 | 450 | int trid; /* RID of new control artifact */ |
| @@ -461,11 +463,11 @@ | ||
| 461 | 463 | zValue = 0; |
| 462 | 464 | } |
| 463 | 465 | if( addTag && iTagged ){ |
| 464 | 466 | char *zOld = 0; |
| 465 | 467 | int cmp; |
| 466 | - rid_has_tag2(iTagged, zTagName, &zOld); | |
| 468 | + rid_has_tag2(frid, zTagName, &zOld); | |
| 467 | 469 | cmp = fossil_strcmp(zOld, zValue); |
| 468 | 470 | fossil_free(zOld); |
| 469 | 471 | if( 0==cmp ){ |
| 470 | 472 | /* Same value - leave it as is. */ |
| 471 | 473 | db_end_transaction(0); |
| @@ -481,22 +483,58 @@ | ||
| 481 | 483 | md5sum_blob(&artifact, &cksum); |
| 482 | 484 | blob_appendf(&artifact, "Z %b\n", &cksum); |
| 483 | 485 | blob_reset(&cksum); |
| 484 | 486 | trid = content_put_ex(&artifact, 0, 0, 0, 0); |
| 485 | 487 | if( trid==0 ){ |
| 486 | - fossil_fatal("Error saving tag artifact: %s", g.zErrMsg); | |
| 488 | + return -1; | |
| 487 | 489 | } |
| 488 | 490 | if( manifest_crosslink(trid, &artifact, MC_NONE)==0 ){ |
| 489 | - fossil_fatal("%s", g.zErrMsg); | |
| 491 | + return -2; | |
| 490 | 492 | } |
| 491 | 493 | assert( blob_is_reset(&artifact) ); |
| 492 | 494 | db_add_unsent(trid); |
| 493 | 495 | admin_log("Tag forum post %S with %c%s", |
| 494 | 496 | zUuid, addTag ? '*' : '-', zTagName); |
| 495 | 497 | fossil_free(zUuid); |
| 496 | 498 | db_end_transaction(0); |
| 497 | - return 1; | |
| 499 | + return trid; | |
| 500 | +} | |
| 501 | + | |
| 502 | +/* | |
| 503 | +** COMMAND: test-forumpost-tag | |
| 504 | +** | |
| 505 | +** Usage: %fossil test-forumpost-tag ?-cancel? THREADID TAGNAME TAGVAL | |
| 506 | +** | |
| 507 | +** A tester for forumpost_tag(). It always rolls back changes. | |
| 508 | +*/ | |
| 509 | +void test_forumpost_tag_command(void){ | |
| 510 | + int fpid; | |
| 511 | + int rc; | |
| 512 | + const char *zPost; | |
| 513 | + const char *zTag; | |
| 514 | + const char *zVal; | |
| 515 | + const int bAdd = find_option("cancel","",0)==0; | |
| 516 | + | |
| 517 | + db_find_and_open_repository(0,0); | |
| 518 | + verify_all_options(); | |
| 519 | + if( g.argc<5 ){ | |
| 520 | + usage("forum-post-id tag-name value"); | |
| 521 | + } | |
| 522 | + zPost = g.argv[2]; | |
| 523 | + zTag = g.argv[3]; | |
| 524 | + zVal = g.argv[4]; | |
| 525 | + | |
| 526 | + db_begin_transaction(); | |
| 527 | + fpid = forumpost_head_rid2(zPost); | |
| 528 | + if( fpid<=0 ){ | |
| 529 | + fossil_fatal("Cannot resolve post ID %s", zPost); | |
| 530 | + } | |
| 531 | + fossil_print("%s => %d => %z\n", zTag, fpid, | |
| 532 | + rid_to_uuid(fpid)); | |
| 533 | + rc = forumpost_tag(fpid, bAdd, zTag, zVal); | |
| 534 | + fossil_print("tag fpid=%d taxgxref.tagid=%d\n", fpid, rc); | |
| 535 | + db_end_transaction(1); | |
| 498 | 536 | } |
| 499 | 537 | |
| 500 | 538 | /* |
| 501 | 539 | ** Returns true if the forum-close-policy setting is true, else false, |
| 502 | 540 | ** caching the result for subsequent calls. |
| @@ -894,11 +932,19 @@ | ||
| 894 | 932 | break; |
| 895 | 933 | } |
| 896 | 934 | } |
| 897 | 935 | if( !sCurrent ) sCurrent = &fss->aStatus[0]; |
| 898 | 936 | assert( sCurrent ); |
| 899 | - @ <span class='forum-status-selection'> | |
| 937 | + @ <fieldset class='forum-status-selection'>\ | |
| 938 | + @ <legend>Status \ | |
| 939 | + @ <span class='help-buttonlet initially-hidden'>\ | |
| 940 | + @ Moderators and the post's owner may change \ | |
| 941 | + @ the status of this thread unless it is still. \ | |
| 942 | + @ pending moderation. See \ | |
| 943 | + @ <a href='%R/help/forum-statuses' target='_new'>\ | |
| 944 | + @ /help/forum-statuses</a></span>\ | |
| 945 | + @ </legend>\ | |
| 900 | 946 | if( forum_may_set_status(fp->fpid) ){ |
| 901 | 947 | @ <form method="post" action='%R/forumpost_status'> |
| 902 | 948 | login_insert_csrf_secret(); |
| 903 | 949 | @ <input type='hidden' name='fpid' value='%s(fp->zUuid)' /> |
| 904 | 950 | @ <select name='status' data-fpid='%s(fp->zUuid)'\ |
| @@ -917,11 +963,11 @@ | ||
| 917 | 963 | @ </form> |
| 918 | 964 | /* Form is activated in fossil.page.forumpost.js */ |
| 919 | 965 | }else{ |
| 920 | 966 | @ <button disabled>Status: %h(sCurrent->zLabel)</button> |
| 921 | 967 | } |
| 922 | - @ </span> | |
| 968 | + @ </fieldset> | |
| 923 | 969 | fossil_free(zCurrent); |
| 924 | 970 | } |
| 925 | 971 | } |
| 926 | 972 | |
| 927 | 973 | /* |
| @@ -1045,17 +1091,26 @@ | ||
| 1045 | 1091 | /* |
| 1046 | 1092 | ** Renders the attachment list for the given forum post. |
| 1047 | 1093 | ** Emits no output if there are no attachments. |
| 1048 | 1094 | */ |
| 1049 | 1095 | static void forum_render_attachment_list(const char *zUuid){ |
| 1050 | - char * zLbl = mprintf("<a href='%R/attachlist?forumpost=%s'>" | |
| 1051 | - "Attachments:</a>", zUuid); | |
| 1052 | - attachment_list(zUuid, zLbl, | |
| 1053 | - ATTACHLIST_HRULE_ABOVE | |
| 1054 | - | ATTACHLIST_SIZE | |
| 1055 | - | ATTACHLIST_HIDE_UNAPPROVED); | |
| 1056 | - fossil_free(zLbl); | |
| 1096 | +#if 1 | |
| 1097 | + attachment_list(zUuid, "📎 Attachments", 0 | |
| 1098 | + | ATTACHLIST_SIZE | |
| 1099 | + | ATTACHLIST_HIDE_UNAPPROVED | |
| 1100 | + | ATTACHLIST_DETAILS_CLOSED | |
| 1101 | + | ATTACHLIST_HIDE_EMPTY); | |
| 1102 | +#else | |
| 1103 | + char * zLbl = mprintf("<a href='%R/attachlist?forumpost=%!S'>" | |
| 1104 | + "Attachments</a>:", zUuid); | |
| 1105 | + attachment_list(zUuid, zLbl, | |
| 1106 | + ATTACHLIST_HRULE_ABOVE | |
| 1107 | + | ATTACHLIST_SIZE | |
| 1108 | + | ATTACHLIST_HIDE_UNAPPROVED | |
| 1109 | + | ATTACHLIST_HIDE_EMPTY); | |
| 1110 | + fossil_free(zLbl); | |
| 1111 | +#endif | |
| 1057 | 1112 | } |
| 1058 | 1113 | |
| 1059 | 1114 | /* |
| 1060 | 1115 | ** Renders the attachment list for p or (if not NULL) pEditHead. |
| 1061 | 1116 | */ |
| @@ -1098,24 +1153,33 @@ | ||
| 1098 | 1153 | |
| 1099 | 1154 | /* Get the manifest for the post. Abort if not found (e.g. shunned). */ |
| 1100 | 1155 | pManifest = manifest_get(p->fpid, CFTYPE_FORUM, 0); |
| 1101 | 1156 | if( !pManifest ) return; |
| 1102 | 1157 | iClosed = forumpost_is_closed(pThread, p, 1); |
| 1158 | + bPrivate = content_is_private(p->fpid); | |
| 1159 | + bSameUser = login_is_individual() | |
| 1160 | + && fossil_strcmp(pManifest->zUser, g.zLogin)==0; | |
| 1103 | 1161 | /* When not in raw mode, create the border around the post. */ |
| 1104 | 1162 | if( !bRaw ){ |
| 1105 | 1163 | /* Open the <div> enclosing the post. Set the class string to mark the post |
| 1106 | 1164 | ** as selected and/or obsolete. */ |
| 1107 | 1165 | iIndent = (p->pEditHead ? p->pEditHead->nIndent : p->nIndent)-1; |
| 1108 | - @ <div id='forum%d(p->fpid)' class='forumTime\ | |
| 1166 | + @ <div id='forum%d(p->fpid)' class='forumpost forumTime\ | |
| 1109 | 1167 | @ %s(bSelect ? " forumSel" : "")\ |
| 1110 | 1168 | @ %s(iClosed ? " forumClosed" : "")\ |
| 1111 | 1169 | @ %s(p->pEditTail ? " forumObs" : "")' \ |
| 1112 | 1170 | if( iIndent && iIndentScale ){ |
| 1113 | - @ style='margin-left:%d(iIndent*iIndentScale)ex;'> | |
| 1114 | - }else{ | |
| 1115 | - @ > | |
| 1171 | + @ style='margin-left:%d(iIndent*iIndentScale)ex;' \ | |
| 1172 | + } | |
| 1173 | + /* These data-X fields are used by the JS editor. */ | |
| 1174 | + if( p->pIrt ){ | |
| 1175 | + @ data-firt="%s(p->pIrt->zUuid)" \ | |
| 1176 | + } | |
| 1177 | + if( p->pEditHead ){ | |
| 1178 | + @ data-fedithead="%s(p->pEditHead->zUuid)" \ | |
| 1116 | 1179 | } |
| 1180 | + @ data-fpid="%s(p->zUuid)">\ | |
| 1117 | 1181 | |
| 1118 | 1182 | /* If this is the first post (or an edit thereof), emit the thread title. */ |
| 1119 | 1183 | if( pManifest->zThreadTitle ){ |
| 1120 | 1184 | @ <h1>%h(pManifest->zThreadTitle)</h1> |
| 1121 | 1185 | } |
| @@ -1196,32 +1260,33 @@ | ||
| 1196 | 1260 | /* Provide a link to the raw source code. */ |
| 1197 | 1261 | if( !bUnf ){ |
| 1198 | 1262 | @ %z(href("%R/forumpost/%!S?raw",p->zUuid))[source]</a> |
| 1199 | 1263 | } |
| 1200 | 1264 | @ </h3> |
| 1265 | + | |
| 1266 | + if( bPrivate && (bSameUser || g.perm.Admin || g.perm.ModForum) ){ | |
| 1267 | + moderation_pending_www(p->fpid); | |
| 1268 | + } | |
| 1201 | 1269 | }/*!bRaw*/ |
| 1202 | 1270 | |
| 1203 | - /* Check if this post is approved, also if it's by the current user. */ | |
| 1204 | - bPrivate = content_is_private(p->fpid); | |
| 1205 | - bSameUser = login_is_individual() | |
| 1206 | - && fossil_strcmp(pManifest->zUser, g.zLogin)==0; | |
| 1207 | - | |
| 1208 | - /* Render the post if the user is able to see it. */ | |
| 1271 | + /* Check if this post is approved, also if it's by the current user. | |
| 1272 | + Render the post if the user is able to see it. */ | |
| 1209 | 1273 | if( bPrivate && !g.perm.ModForum && !bSameUser ){ |
| 1210 | 1274 | @ <p><span class="modpending">Awaiting Moderator Approval</span></p> |
| 1211 | 1275 | }else{ |
| 1212 | 1276 | if( bRaw || bUnf || p->pEditTail ){ |
| 1213 | 1277 | zMimetype = "text/plain"; |
| 1214 | 1278 | }else{ |
| 1215 | 1279 | zMimetype = pManifest->zMimetype; |
| 1216 | 1280 | } |
| 1217 | 1281 | forum_render(0, zMimetype, pManifest->zWiki, 0, !bRaw); |
| 1218 | - forum_render_attachment_list2(p); | |
| 1219 | 1282 | } |
| 1220 | 1283 | |
| 1221 | 1284 | /* When not in raw mode, finish creating the border around the post. */ |
| 1222 | 1285 | if( !bRaw ){ |
| 1286 | + int bBrBeforeAttach = 0; /* Layout kludge for Attach button */ | |
| 1287 | + forum_render_attachment_list2(p); | |
| 1223 | 1288 | /* If the user is able to write to the forum and if this post has not been |
| 1224 | 1289 | ** edited, create a form with various interaction buttons. */ |
| 1225 | 1290 | if( g.perm.WrForum && !p->pEditTail ){ |
| 1226 | 1291 | @ <div class="forumpost-single-controls">\ |
| 1227 | 1292 | @ <form action="%R/forumedit" method="POST"> |
| @@ -1250,10 +1315,11 @@ | ||
| 1250 | 1315 | @ <br><label><input type="checkbox" name="trust"> |
| 1251 | 1316 | @ Trust user "%h(pManifest->zUser)" so that future posts by \ |
| 1252 | 1317 | @ "%h(pManifest->zUser)" do not require moderation. |
| 1253 | 1318 | @ </label> |
| 1254 | 1319 | @ <input type="hidden" name="trustuser" value="%h(pManifest->zUser)"> |
| 1320 | + bBrBeforeAttach = 1 /* slightly unmangle the layout */; | |
| 1255 | 1321 | } |
| 1256 | 1322 | }else if( bSameUser ){ |
| 1257 | 1323 | /* Allow users to delete (reject) their own pending posts. */ |
| 1258 | 1324 | @ <input type="submit" name="reject" value="Delete"> |
| 1259 | 1325 | } |
| @@ -1273,23 +1339,19 @@ | ||
| 1273 | 1339 | @ %s(iClosed ? "action-reopen" : "action-close")'/> |
| 1274 | 1340 | /* ^^^ activated by fossil.page.forumpost.js */ |
| 1275 | 1341 | } |
| 1276 | 1342 | @ </form> |
| 1277 | 1343 | } |
| 1278 | - if( g.perm.Admin || | |
| 1279 | - (login_is_individual() | |
| 1280 | - && forumpost_is_owner(p/*not pHead*/->fpid, 0)) ){ | |
| 1344 | + if( attach_user_may(p/*not pHead*/->fpid, CFTYPE_FORUM) ){ | |
| 1281 | 1345 | /* When an admin edits someone else's post, the admin |
| 1282 | 1346 | ** effectively takes over ownership of it (and we currently |
| 1283 | 1347 | ** have no way of passing it back). Because of this, we |
| 1284 | 1348 | ** check the ownership of `p` instead of `pHead`. */ |
| 1285 | - @ <form method="post" action="%R/attachadd">\ | |
| 1286 | - @ <input type="hidden" name="forumpost" value="%T(pHead->zUuid)"> | |
| 1287 | - @ <input type="submit" value="Attach..."> | |
| 1288 | - login_insert_csrf_secret(); | |
| 1289 | - moderation_pending_www(p->fpid); | |
| 1290 | - @ </form> | |
| 1349 | + if( bBrBeforeAttach ){ | |
| 1350 | + @ <br> | |
| 1351 | + } | |
| 1352 | + attach_render_attachadd_button(pHead->zUuid); | |
| 1291 | 1353 | } |
| 1292 | 1354 | } |
| 1293 | 1355 | @ </div> |
| 1294 | 1356 | } |
| 1295 | 1357 | if( !p->pIrt && (flags & FDISPLAY_SELECTED)){ |
| @@ -1475,11 +1537,12 @@ | ||
| 1475 | 1537 | ** to all forum-related pages. It does not include page-specific |
| 1476 | 1538 | ** code (e.g. "forum.js"). |
| 1477 | 1539 | */ |
| 1478 | 1540 | static void forum_emit_js(void){ |
| 1479 | 1541 | builtin_fossil_js_bundle_or("copybutton", "pikchr", "confirmer", |
| 1480 | - NULL); | |
| 1542 | + "attach", "tabs", "storage", | |
| 1543 | + "popupwidget", NULL); | |
| 1481 | 1544 | builtin_request_js("fossil.page.forumpost.js"); |
| 1482 | 1545 | } |
| 1483 | 1546 | |
| 1484 | 1547 | /* |
| 1485 | 1548 | ** WEBPAGE: forumpost |
| @@ -1635,21 +1698,13 @@ | ||
| 1635 | 1698 | if( g.perm.WrTForum ) return 0; |
| 1636 | 1699 | if( g.perm.ModForum ) return 0; |
| 1637 | 1700 | return 1; |
| 1638 | 1701 | } |
| 1639 | 1702 | |
| 1640 | -/* | |
| 1641 | -** Return true if the string is white-space only. | |
| 1642 | -*/ | |
| 1643 | -static int whitespace_only(const char *z){ | |
| 1644 | - if( z==0 ) return 1; | |
| 1645 | - while( z[0] && fossil_isspace(z[0]) ){ z++; } | |
| 1646 | - return z[0]==0; | |
| 1647 | -} | |
| 1648 | - | |
| 1649 | 1703 | /* Flags for use with forum_post() */ |
| 1650 | 1704 | #define FPOST_NO_ALERT 1 /* do not send any alerts */ |
| 1705 | +#define FPOST_DRYRUN 2 /* do not save the artifact */ | |
| 1651 | 1706 | |
| 1652 | 1707 | /* |
| 1653 | 1708 | ** Return a flags value for use with the final argument to |
| 1654 | 1709 | ** forum_post(), extracted from the CGI environment. |
| 1655 | 1710 | */ |
| @@ -1656,10 +1711,13 @@ | ||
| 1656 | 1711 | static int forum_post_flags(void){ |
| 1657 | 1712 | int iPostFlags = 0; |
| 1658 | 1713 | if( g.perm.Debug && P("fpsilent")!=0 ){ |
| 1659 | 1714 | iPostFlags |= FPOST_NO_ALERT; |
| 1660 | 1715 | } |
| 1716 | + if( P("dryrun")!=0 ){ | |
| 1717 | + iPostFlags |= FPOST_DRYRUN; | |
| 1718 | + } | |
| 1661 | 1719 | return iPostFlags; |
| 1662 | 1720 | } |
| 1663 | 1721 | |
| 1664 | 1722 | /* |
| 1665 | 1723 | ** Add a new Forum Post artifact to the repository. |
| @@ -1687,11 +1745,11 @@ | ||
| 1687 | 1745 | if( !g.perm.Admin && (iEdit || iInReplyTo) |
| 1688 | 1746 | && forum_rid_is_tagged(iEdit ? iEdit : iInReplyTo, "closed", 1) ){ |
| 1689 | 1747 | forumpost_error_closed(); |
| 1690 | 1748 | return 0; |
| 1691 | 1749 | } |
| 1692 | - if( iEdit==0 && whitespace_only(zContent) ){ | |
| 1750 | + if( iEdit==0 && fossil_all_whitespace(zContent) ){ | |
| 1693 | 1751 | return 0; |
| 1694 | 1752 | } |
| 1695 | 1753 | if( iInReplyTo==0 && iEdit>0 ){ |
| 1696 | 1754 | iBasis = iEdit; |
| 1697 | 1755 | iInReplyTo = db_int(0, "SELECT firt FROM forumpost WHERE fpid=%d", iEdit); |
| @@ -1712,17 +1770,19 @@ | ||
| 1712 | 1770 | fossil_free(zG); |
| 1713 | 1771 | } |
| 1714 | 1772 | if( zTitle ){ |
| 1715 | 1773 | blob_appendf(&x, "H %F\n", zTitle); |
| 1716 | 1774 | } |
| 1717 | - zI = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", iInReplyTo); | |
| 1775 | + zI = rid_to_uuid(iInReplyTo); | |
| 1718 | 1776 | if( zI ){ |
| 1719 | 1777 | blob_appendf(&x, "I %s\n", zI); |
| 1720 | 1778 | fossil_free(zI); |
| 1721 | 1779 | } |
| 1722 | - if( fossil_strcmp(zMimetype,"text/x-fossil-wiki")!=0 ){ | |
| 1723 | - blob_appendf(&x, "N %s\n", zMimetype); | |
| 1780 | + if( zMimetype!=0 | |
| 1781 | + && zMimetype[0]!=0 | |
| 1782 | + && fossil_strcmp(zMimetype,"text/x-fossil-wiki")!=0 ){ | |
| 1783 | + blob_appendf(&x, "N %F\n", zMimetype); | |
| 1724 | 1784 | } |
| 1725 | 1785 | if( iEdit>0 ){ |
| 1726 | 1786 | char *zP = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", iEdit); |
| 1727 | 1787 | if( zP==0 ) webpage_error("missing edit artifact %d", iEdit); |
| 1728 | 1788 | blob_appendf(&x, "P %s\n", zP); |
| @@ -1750,11 +1810,11 @@ | ||
| 1750 | 1810 | webpage_error("malformed forum post artifact - %s", blob_str(&errMsg)); |
| 1751 | 1811 | } |
| 1752 | 1812 | webpage_assert( pPost->type==CFTYPE_FORUM ); |
| 1753 | 1813 | manifest_destroy(pPost); |
| 1754 | 1814 | |
| 1755 | - if( P("dryrun") ){ | |
| 1815 | + if( (iFlags & FPOST_DRYRUN)!=0 ){ | |
| 1756 | 1816 | @ <div class='debug'> |
| 1757 | 1817 | @ This is the artifact that would have been generated: |
| 1758 | 1818 | @ <pre>%h(blob_str(&x))</pre> |
| 1759 | 1819 | @ </div> |
| 1760 | 1820 | blob_reset(&x); |
| @@ -1813,12 +1873,17 @@ | ||
| 1813 | 1873 | int addTag, int validFpid){ |
| 1814 | 1874 | if( !cgi_csrf_safe(2) ){ |
| 1815 | 1875 | webpage_error("CSRF validation failed"); |
| 1816 | 1876 | }else{ |
| 1817 | 1877 | const int fpid = validFpid>0 ? validFpid : forum_validate_fpid_param(); |
| 1818 | - forumpost_tag(fpid, zTag, addTag, zVal); | |
| 1819 | - cgi_redirectf("%R/forumpost/%S",P("fpid")); | |
| 1878 | + if( fpid>0 ){ | |
| 1879 | + if( forumpost_tag(fpid, addTag, zTag, zVal) < 0 ){ | |
| 1880 | + webpage_error("Tagging artifact failed: %s", g.zErrMsg); | |
| 1881 | + }else{ | |
| 1882 | + cgi_redirectf("%R/forumpost/%S",P("fpid")); | |
| 1883 | + } | |
| 1884 | + } | |
| 1820 | 1885 | } |
| 1821 | 1886 | } |
| 1822 | 1887 | |
| 1823 | 1888 | /* |
| 1824 | 1889 | ** WEBPAGE: forumpost_close hidden |
| @@ -1954,22 +2019,24 @@ | ||
| 1954 | 2019 | } |
| 1955 | 2020 | } |
| 1956 | 2021 | |
| 1957 | 2022 | /* |
| 1958 | 2023 | ** If the user has AttachForum permissions, emit a notice that |
| 1959 | -** attachments may be added after saving. If p is not NULL, | |
| 1960 | -** also emit its list of attachments. | |
| 2024 | +** attachments may be added after saving. If p is not NULL, also emit | |
| 2025 | +** its list of attachments. We only emit this for No-JS environments, | |
| 2026 | +** as in JS environments the interactive forum editor includes | |
| 2027 | +** attachment support. | |
| 1961 | 2028 | */ |
| 1962 | 2029 | static void forum_render_attachment_notice(void){ |
| 1963 | 2030 | if( g.perm.AttachForum ){ |
| 1964 | - @ <div>You will be able to attach files to this post after saving | |
| 1965 | - @ it.</div> | |
| 2031 | + @ <noscript><div>You will be able to attach files to this post | |
| 2032 | + @ after saving it.</div></noscript> | |
| 1966 | 2033 | } |
| 1967 | 2034 | } |
| 1968 | 2035 | |
| 1969 | 2036 | /* |
| 1970 | -** WEBPAGE: forume1 | |
| 2037 | +** WEBPAGE: forume1 hidden | |
| 1971 | 2038 | ** |
| 1972 | 2039 | ** Start a new forum thread. |
| 1973 | 2040 | */ |
| 1974 | 2041 | void forumnew_page(void){ |
| 1975 | 2042 | const char *zTitle = PDT("title",""); |
| @@ -1983,36 +2050,45 @@ | ||
| 1983 | 2050 | } |
| 1984 | 2051 | if( P("submit") && cgi_csrf_safe(2) ){ |
| 1985 | 2052 | if( forum_post(zTitle, 0, 0, 0, zMimetype, zContent, |
| 1986 | 2053 | forum_post_flags()) ) return; |
| 1987 | 2054 | } |
| 1988 | - if( P("preview") && !whitespace_only(zContent) ){ | |
| 2055 | + if( P("preview") && !fossil_all_whitespace(zContent) ){ | |
| 1989 | 2056 | @ <h1>Preview:</h1> |
| 1990 | 2057 | forum_render(zTitle, zMimetype, zContent, "forumEdit", 1); |
| 1991 | 2058 | } |
| 1992 | 2059 | style_set_current_feature("forum"); |
| 1993 | 2060 | style_header("New Forum Thread"); |
| 1994 | - @ <form action="%R/forume1" method="POST"> | |
| 2061 | + | |
| 2062 | + @ <form action="%R/forume1" method="POST" \ | |
| 2063 | + @ class="remove-if-replaced"> | |
| 1995 | 2064 | @ <h1>New Thread:</h1> |
| 1996 | 2065 | forum_from_line(); |
| 1997 | 2066 | forum_post_widget(zTitle, zMimetype, zContent); |
| 1998 | 2067 | @ <input type="submit" name="preview" value="Preview"> |
| 1999 | - if( P("preview") && !whitespace_only(zContent) ){ | |
| 2068 | + if( P("preview") && !fossil_all_whitespace(zContent) ){ | |
| 2000 | 2069 | @ <input type="submit" name="submit" value="Submit"> |
| 2001 | 2070 | }else{ |
| 2002 | 2071 | @ <input type="submit" name="submit" value="Submit" disabled> |
| 2003 | 2072 | } |
| 2004 | 2073 | forum_render_debug_options(); |
| 2005 | 2074 | login_insert_csrf_secret(); |
| 2006 | 2075 | @ </form> |
| 2076 | + /* When JS is disabled the block above will work. When it's | |
| 2077 | + enabled, the above will be removed and JS will render the editor | |
| 2078 | + form in the next element. */ | |
| 2079 | + @ <div hidden id='forumnew-placeholder'> | |
| 2080 | + @ <input type='hidden' name='title' value='%h(zTitle)'> | |
| 2081 | + login_insert_csrf_secret(); | |
| 2082 | + @ </div> | |
| 2007 | 2083 | forum_render_attachment_notice(); |
| 2008 | 2084 | forum_emit_js(); |
| 2009 | 2085 | style_finish_page(); |
| 2010 | 2086 | } |
| 2011 | 2087 | |
| 2012 | 2088 | /* |
| 2013 | -** WEBPAGE: forume2 | |
| 2089 | +** WEBPAGE: forume2 hidden | |
| 2014 | 2090 | ** |
| 2015 | 2091 | ** Edit an existing forum message. |
| 2016 | 2092 | ** Query parameters: |
| 2017 | 2093 | ** |
| 2018 | 2094 | ** fpid=X Hash of the post to be edited. REQUIRED |
| @@ -2097,11 +2173,11 @@ | ||
| 2097 | 2173 | style_set_current_feature("forum"); |
| 2098 | 2174 | isDelete = P("nullout")!=0; |
| 2099 | 2175 | if( P("submit") |
| 2100 | 2176 | && isCsrfSafe |
| 2101 | 2177 | && (zContent = PDT("content",""))!=0 |
| 2102 | - && (!whitespace_only(zContent) || isDelete) | |
| 2178 | + && (isDelete || !fossil_all_whitespace(zContent)) | |
| 2103 | 2179 | ){ |
| 2104 | 2180 | int done = 1; |
| 2105 | 2181 | const char *zMimetype = PD("mimetype",DEFAULT_FORUM_MIMETYPE); |
| 2106 | 2182 | if( bReply ){ |
| 2107 | 2183 | done = forum_post(0, fpid, 0, 0, zMimetype, zContent, |
| @@ -2175,11 +2251,11 @@ | ||
| 2175 | 2251 | zDisplayName = display_name_from_login(pPost->zUser); |
| 2176 | 2252 | @ <h3 class='forumPostHdr'>By %s(zDisplayName) on %h(zDate)</h3> |
| 2177 | 2253 | fossil_free(zDisplayName); |
| 2178 | 2254 | fossil_free(zDate); |
| 2179 | 2255 | forum_render(0, pPost->zMimetype, pPost->zWiki, "forumEdit", 1); |
| 2180 | - if( bPreview && !whitespace_only(zContent) ){ | |
| 2256 | + if( bPreview && !fossil_all_whitespace(zContent) ){ | |
| 2181 | 2257 | @ <h2>Preview:</h2> |
| 2182 | 2258 | forum_render(0, zMimetype,zContent, "forumEdit", 1); |
| 2183 | 2259 | } |
| 2184 | 2260 | @ <h2>Enter Reply:</h2> |
| 2185 | 2261 | @ <form action="%R/forume2" method="POST"> |
| @@ -2190,11 +2266,11 @@ | ||
| 2190 | 2266 | } |
| 2191 | 2267 | if( !isDelete ){ |
| 2192 | 2268 | @ <input type="submit" name="preview" value="Preview"> |
| 2193 | 2269 | } |
| 2194 | 2270 | @ <input type="submit" name="cancel" value="Cancel"> |
| 2195 | - if( (bPreview && !whitespace_only(zContent)) || isDelete ){ | |
| 2271 | + if( isDelete || (bPreview && !fossil_all_whitespace(zContent)) ){ | |
| 2196 | 2272 | if( !iClosed || g.perm.Admin ) { |
| 2197 | 2273 | @ <input type="submit" name="submit" value="Submit"> |
| 2198 | 2274 | } |
| 2199 | 2275 | } |
| 2200 | 2276 | forum_render_debug_options(); |
| @@ -2201,11 +2277,13 @@ | ||
| 2201 | 2277 | login_insert_csrf_secret(); |
| 2202 | 2278 | @ </form> |
| 2203 | 2279 | if( !bReply ){ |
| 2204 | 2280 | forum_render_attachment_list(rid_to_uuid(fpid)); |
| 2205 | 2281 | } |
| 2206 | - forum_render_attachment_notice(); | |
| 2282 | + if( !isDelete ){ | |
| 2283 | + forum_render_attachment_notice(); | |
| 2284 | + } | |
| 2207 | 2285 | forum_emit_js(); |
| 2208 | 2286 | style_finish_page(); |
| 2209 | 2287 | } |
| 2210 | 2288 | |
| 2211 | 2289 | /* |
| @@ -2214,21 +2292,21 @@ | ||
| 2214 | 2292 | ** to closed posts. If false, only administrators may do so. Note that |
| 2215 | 2293 | ** this only affects the forum web UI, not post-closing tags which |
| 2216 | 2294 | ** arrive via the command-line or from synchronization with a remote. |
| 2217 | 2295 | ** This policy also determines whether moderators may delete forum |
| 2218 | 2296 | ** attachments. |
| 2219 | -*/ | |
| 2220 | -/* | |
| 2297 | +** | |
| 2221 | 2298 | ** SETTING: forum-title width=20 default=Forum |
| 2222 | 2299 | ** This is the name or "title" of the Forum for this repository. The |
| 2223 | 2300 | ** default is just "Forum". But in some setups, admins might want to |
| 2224 | 2301 | ** change it to "Developer Forum" or "User Forum" or whatever other name |
| 2225 | 2302 | ** seems more appropriate for the particular usage. |
| 2226 | 2303 | ** |
| 2227 | 2304 | ** SETTING: attachment-size-limit width=16 |
| 2228 | -** The maximum number of bytes for an attachment. The default (or 0) is | |
| 2229 | -** unlimited but a limit may be imposed by the web server or a proxy. | |
| 2305 | +** The maximum number of bytes for an attachment to a wiki page, | |
| 2306 | +** ticket, tech note, or forum post. The default (or 0) is unlimited | |
| 2307 | +** but a limit may be imposed by the web server or a proxy. | |
| 2230 | 2308 | ** |
| 2231 | 2309 | ** SETTING: forum-statuses width=40 block-text |
| 2232 | 2310 | ** This JSON5-formatted value defines an array of objects describing |
| 2233 | 2311 | ** the available statuses of forum posts. Each entry of the array must |
| 2234 | 2312 | ** be an object in the form {label:"X",value:"Y"}. |
| @@ -2723,5 +2801,388 @@ | ||
| 2723 | 2801 | ** URL arg when the status selection list is activated. */ |
| 2724 | 2802 | forum_emit_js(); |
| 2725 | 2803 | } |
| 2726 | 2804 | style_finish_page(); |
| 2727 | 2805 | } |
| 2806 | + | |
| 2807 | +/* | |
| 2808 | +** The AJAX counterpart of forum_post(). | |
| 2809 | +** | |
| 2810 | +** Returns the new artifact's RID on success, 0 if no changes were | |
| 2811 | +** necessary (e.g. an empty new post or dry-run mode), and a negative | |
| 2812 | +** value on error. If it returns a negative value then it will have | |
| 2813 | +** populated the ajax response state with an error object. | |
| 2814 | +** | |
| 2815 | +** zTitle must be NULL if iInReplyTo>0 and must be non-empty if | |
| 2816 | +** iInReplyTo==0. | |
| 2817 | +** | |
| 2818 | +** The caller must have started a transaction and must roll it back if | |
| 2819 | +** this call returns <=0, noting that only the negative-value case is | |
| 2820 | +** an error. | |
| 2821 | +** | |
| 2822 | +** This function does some work to try to ensure that duplicate | |
| 2823 | +** entries are not save (this can happen as a side effect of the forum | |
| 2824 | +** post editor added in 2026-06). If the given post will not have been | |
| 2825 | +** materially edited by these changes, they are not applied and the | |
| 2826 | +** rid of the existing entry is used. | |
| 2827 | +** | |
| 2828 | +** Maintenance reminders: | |
| 2829 | +** | |
| 2830 | +** - iInReplyTo==0 && iEdit==0: new thread | |
| 2831 | +** - iInReplyTo==0 && iEdit>0 : edit top post or response | |
| 2832 | +** - iInReplyTo>0 && iEdit==0: new response | |
| 2833 | +** - iInReplyTo>0 && iEdit>0 : edit response | |
| 2834 | +*/ | |
| 2835 | +static int forum_post_ajax( | |
| 2836 | + const char *zTitle, /* Title. NULL for replies */ | |
| 2837 | + int iInReplyTo, /* Post replying to. 0 for new threads */ | |
| 2838 | + int iEdit, /* Post being edited, or zero for a new post */ | |
| 2839 | + const char *zUser, /* Username. NULL means use login name */ | |
| 2840 | + const char *zMimetype, /* Mimetype of content. */ | |
| 2841 | + const char *zContent, /* Content */ | |
| 2842 | + int iFlags /* FPOST_xyz flag values */ | |
| 2843 | +){ | |
| 2844 | + char *zI; | |
| 2845 | + char *zG; | |
| 2846 | + char *zP = 0; | |
| 2847 | + int iBasis; | |
| 2848 | + Blob x = BLOB_INITIALIZER, | |
| 2849 | + cksum = BLOB_INITIALIZER, | |
| 2850 | + formatCheck = BLOB_INITIALIZER, | |
| 2851 | + errMsg = BLOB_INITIALIZER; | |
| 2852 | + Manifest *pPost = 0; | |
| 2853 | + int nContent = zContent ? (int)strlen(zContent) : 0; | |
| 2854 | + int rc = 0; | |
| 2855 | + | |
| 2856 | + assert( db_transaction_nesting_depth()>0 ); | |
| 2857 | + schema_forum(); | |
| 2858 | + if( iEdit==0 && fossil_all_whitespace(zContent) ){ | |
| 2859 | + return 0; | |
| 2860 | + } | |
| 2861 | + if( !g.perm.Admin && (iEdit || iInReplyTo) | |
| 2862 | + && forum_rid_is_tagged(iEdit ? iEdit : iInReplyTo, "closed", 1) ){ | |
| 2863 | + return -ajax_route_error(400, "Thread is closed."); | |
| 2864 | + } | |
| 2865 | + if( 0==iInReplyTo && fossil_all_whitespace(zTitle) ){ | |
| 2866 | + return -ajax_route_error(400, "Empty title is not permitted."); | |
| 2867 | + } | |
| 2868 | + | |
| 2869 | + if( zUser==0 ){ | |
| 2870 | + if( login_is_nobody() ){ | |
| 2871 | + zUser = "anonymous"; | |
| 2872 | + }else{ | |
| 2873 | + zUser = login_name(); | |
| 2874 | + } | |
| 2875 | + } | |
| 2876 | + if( iEdit>0 | |
| 2877 | + && !g.perm.Admin | |
| 2878 | + && !forumpost_is_owner(iEdit, zUser) ){ | |
| 2879 | + return -ajax_route_error( | |
| 2880 | + 403, "Only admins may edit other peoples' posts." | |
| 2881 | + ); | |
| 2882 | + } | |
| 2883 | + if( iInReplyTo==0 && iEdit>0 ){ | |
| 2884 | + iBasis = iEdit; | |
| 2885 | + iInReplyTo = db_int(0, "SELECT firt FROM forumpost WHERE fpid=%d", | |
| 2886 | + iEdit); | |
| 2887 | + }else{ | |
| 2888 | + iBasis = iInReplyTo; | |
| 2889 | + /* TODO (2026-06-008) If (iInReplyTo>0 && iEdit>0), validate that | |
| 2890 | + ** iInReplyTo is connected to iEdit properly, else we risk | |
| 2891 | + ** reparenting the new edit and having unrepredictable downstream | |
| 2892 | + ** side effects. */ | |
| 2893 | + } | |
| 2894 | + | |
| 2895 | + if( 0!=zMimetype && 0==zMimetype[0] ){ | |
| 2896 | + zMimetype = 0; | |
| 2897 | + } | |
| 2898 | + | |
| 2899 | + if( 0!=zTitle && 0==zTitle[0] ) zTitle = 0; | |
| 2900 | + webpage_assert( (zTitle==0)+(iInReplyTo==0)==1 ); | |
| 2901 | + | |
| 2902 | + if( iEdit>0 ){ | |
| 2903 | + int cmp; | |
| 2904 | + pPost = manifest_get(iEdit, CFTYPE_FORUM, 0); | |
| 2905 | + if( pPost==0 ){ | |
| 2906 | + rc = -ajax_route_error(404, "Missing edit artifact %d", iEdit); | |
| 2907 | + goto post_ajax_end; | |
| 2908 | + } | |
| 2909 | + /* | |
| 2910 | + ** If the old content matches the new then do not save a new copy. | |
| 2911 | + ** It's easy to get re-posts of unedited content via the forum | |
| 2912 | + ** editor, especially since the one added in 2026-06, where a | |
| 2913 | + ** post's status and attachments may be amended from the editor | |
| 2914 | + ** without modifying any of the post's content. In the legacy | |
| 2915 | + ** editor such "out-of-band" changes weren't possible and users | |
| 2916 | + ** have never made a practice of re-posting unedited content. | |
| 2917 | + ** | |
| 2918 | + ** We compare the following fields to the original: user, mimetype, | |
| 2919 | + ** content, and (for root posts only) the title. | |
| 2920 | + */ | |
| 2921 | + cmp = (0==pPost->zInReplyTo) | |
| 2922 | + ? fossil_strcmp(pPost->zThreadTitle, zTitle) | |
| 2923 | + : 0; | |
| 2924 | + if( 0==cmp ){ | |
| 2925 | + cmp=fossil_strcmp(pPost->zWiki, zContent); | |
| 2926 | + if( 0==cmp ){ | |
| 2927 | + cmp = fossil_strcmp(pPost->zUser, zUser); | |
| 2928 | + } | |
| 2929 | + if( 0==cmp | |
| 2930 | + && 0!=(cmp=fossil_strcmp(pPost->zMimetype, zMimetype)) ){ | |
| 2931 | + /* Extra mimetype checks for a common condition seen elsewhere */ | |
| 2932 | + if( (0==zMimetype | |
| 2933 | + && 0==fossil_strcmp(pPost->zMimetype, "text/x-fossil-wiki")) | |
| 2934 | + || (0==pPost->zMimetype | |
| 2935 | + && 0==fossil_strcmp(zMimetype, "text/x-fossil-wiki")) ){ | |
| 2936 | + cmp = 0; | |
| 2937 | + } | |
| 2938 | + } | |
| 2939 | + if( 0==cmp ){ | |
| 2940 | + rc = iEdit; | |
| 2941 | + goto post_ajax_end; | |
| 2942 | + } | |
| 2943 | + } | |
| 2944 | + zP = rid_to_uuid(iEdit); | |
| 2945 | + } | |
| 2946 | + | |
| 2947 | + /* Write the new artifact */ | |
| 2948 | + blob_init(&x, 0, 0); | |
| 2949 | + blob_appendf(&x, "D %z\n", date_in_standard_format("now")); | |
| 2950 | + zG = db_text( | |
| 2951 | + 0, | |
| 2952 | + "SELECT uuid FROM blob, forumpost" | |
| 2953 | + " WHERE blob.rid==forumpost.froot" | |
| 2954 | + " AND forumpost.fpid=%d", | |
| 2955 | + iBasis | |
| 2956 | + ); | |
| 2957 | + if( zG ){ | |
| 2958 | + blob_appendf(&x, "G %z\n", zG); | |
| 2959 | + } | |
| 2960 | + if( zTitle ){ | |
| 2961 | + blob_appendf(&x, "H %F\n", zTitle); | |
| 2962 | + } | |
| 2963 | + if( iInReplyTo>0 ){ | |
| 2964 | + zI = rid_to_uuid(iInReplyTo); | |
| 2965 | + if( 0==zI ){ | |
| 2966 | + rc = -ajax_route_error(404, "Missing in-reply-to artifact %d", | |
| 2967 | + iInReplyTo); | |
| 2968 | + goto post_ajax_end; | |
| 2969 | + } | |
| 2970 | + blob_appendf(&x, "I %z\n", zI); | |
| 2971 | + } | |
| 2972 | + if( zMimetype!=0 | |
| 2973 | + && fossil_strcmp(zMimetype,"text/x-fossil-wiki")!=0 ){ | |
| 2974 | + blob_appendf(&x, "N %F\n", zMimetype); | |
| 2975 | + } | |
| 2976 | + if( zP ){ | |
| 2977 | + blob_appendf(&x, "P %s\n", zP); | |
| 2978 | + } | |
| 2979 | + | |
| 2980 | + blob_appendf(&x, "U %F\n", zUser); | |
| 2981 | + blob_appendf(&x, "W %d\n%s\n", nContent, zContent); | |
| 2982 | + md5sum_blob(&x, &cksum); | |
| 2983 | + blob_appendf(&x, "Z %b\n", &cksum); | |
| 2984 | + blob_reset(&cksum); | |
| 2985 | + | |
| 2986 | + /* Verify that the artifact we are creating is well-formed */ | |
| 2987 | + blob_init(&formatCheck, 0, 0); | |
| 2988 | + blob_init(&errMsg, 0, 0); | |
| 2989 | + blob_copy(&formatCheck, &x); | |
| 2990 | + pPost = manifest_parse(&formatCheck, 0, &errMsg); | |
| 2991 | + if( pPost==0 ){ | |
| 2992 | + ajax_route_error(500, "Malformed forum post artifact: %b", &errMsg); | |
| 2993 | + rc = -500; | |
| 2994 | + goto post_ajax_end; | |
| 2995 | + } | |
| 2996 | + webpage_assert( pPost->type==CFTYPE_FORUM ); | |
| 2997 | + | |
| 2998 | + if( (iFlags & FPOST_DRYRUN)!=0 ){ | |
| 2999 | + rc = 0; | |
| 3000 | + }else{ | |
| 3001 | + int nrid; | |
| 3002 | + db_begin_transaction(); | |
| 3003 | + nrid = wiki_put(&x, iEdit>0 ? iEdit : 0, forum_need_moderation()); | |
| 3004 | + blob_reset(&x); | |
| 3005 | + if( (iFlags & FPOST_NO_ALERT)!=0 ){ | |
| 3006 | + alert_unqueue('f', nrid); | |
| 3007 | + } | |
| 3008 | + rc = nrid; | |
| 3009 | + db_end_transaction(0); | |
| 3010 | + } | |
| 3011 | +post_ajax_end: | |
| 3012 | + manifest_destroy(pPost); | |
| 3013 | + fossil_free(zP); | |
| 3014 | + blob_reset(&x); | |
| 3015 | + blob_reset(&cksum); | |
| 3016 | + blob_reset(&formatCheck); | |
| 3017 | + return rc; | |
| 3018 | +} | |
| 3019 | +/* | |
| 3020 | +** WEBPAGE: forumajax_save hidden | |
| 3021 | +** | |
| 3022 | +** WIP | |
| 3023 | +** | |
| 3024 | +** Response JSON: | |
| 3025 | +** | |
| 3026 | +** { uuid: hash, ...tbd } | |
| 3027 | +*/ | |
| 3028 | +void forum_ajax_save_page(void){ | |
| 3029 | + const char *zFpid; | |
| 3030 | + const char *zTitle; | |
| 3031 | + const char *zIrt; | |
| 3032 | + const char *zMimetype; | |
| 3033 | + const char *zContent; | |
| 3034 | + const char *zStatus; | |
| 3035 | + const int bHasAttachment = P("file1")!=0; | |
| 3036 | + Manifest *pPost = 0; | |
| 3037 | + char *zNewUuid = 0; | |
| 3038 | + int firt = 0; /* In-reply-to rid or 0 */ | |
| 3039 | + int fpid = 0; /* Post rid being edited or 0 */ | |
| 3040 | + int rc = 0; /* Result code. */ | |
| 3041 | + int nrid = 0; /* New artifact rid. */ | |
| 3042 | + int iPostFlags; /* forum_post_flags() (after perms check) */ | |
| 3043 | + int bRollback; /* True = roll back. */ | |
| 3044 | + int nAttach = 0; /* Number of attachments added */ | |
| 3045 | + int bStatusSet = 0; /* True if status tag set. */ | |
| 3046 | + | |
| 3047 | + if( !ajax_route_bootstrap(0, 1) ){ | |
| 3048 | + return; | |
| 3049 | + }else if( !g.perm.WrForum | |
| 3050 | + || (bHasAttachment && !g.perm.AttachForum) ){ | |
| 3051 | + ajax_route_error_forbidden(); | |
| 3052 | + return; | |
| 3053 | + }else if( !ajax_check_csrf(2) ){ | |
| 3054 | + ajax_route_error_csrf(); | |
| 3055 | + return; | |
| 3056 | + } | |
| 3057 | + | |
| 3058 | + iPostFlags = forum_post_flags(/*must come after permissions init*/); | |
| 3059 | + bRollback = (FPOST_DRYRUN & iPostFlags); | |
| 3060 | + zFpid = P("fpid"); | |
| 3061 | + zIrt = P("firt"); | |
| 3062 | + zMimetype = P("mimetype"); | |
| 3063 | + zContent = P("content"); | |
| 3064 | + zStatus = P("status"); | |
| 3065 | + db_begin_transaction(); | |
| 3066 | + if( zFpid && zFpid[0] ){ | |
| 3067 | + fpid = symbolic_name_to_rid(zFpid, "f"); | |
| 3068 | + if( fpid<0 ){ | |
| 3069 | + rc = -ajax_route_error(400, "Ambiguous forum ID."); | |
| 3070 | + goto ajax_save_end; | |
| 3071 | + }else if( 0==fpid | |
| 3072 | + || 0==(pPost = manifest_get(fpid, CFTYPE_FORUM, 0)) ){ | |
| 3073 | + rc = -ajax_route_error(404, "Cannot resolve forum post ID."); | |
| 3074 | + goto ajax_save_end; | |
| 3075 | + } | |
| 3076 | + } | |
| 3077 | + /* | |
| 3078 | + ** Problem: if we derive firt from fpid/pPost then there's a race | |
| 3079 | + ** condition where the IRT post is edited between the time that this | |
| 3080 | + ** edit was initiated and when it is posted: the new edit's IRT will | |
| 3081 | + ** point to the edit which was made in the meantime, not the one the | |
| 3082 | + ** user intended to respond to. However, if we accept firt from the | |
| 3083 | + ** enviornment, we "really should" validate that it's actually in | |
| 3084 | + ** the current chain, to prohibit that malicious posts could move | |
| 3085 | + ** posts around. | |
| 3086 | + ** | |
| 3087 | + ** forum_post_ajax() will, if fpid>0 && !firt, select fpid's current | |
| 3088 | + ** firt. | |
| 3089 | + */ | |
| 3090 | + if( zIrt && zIrt[0] ){ | |
| 3091 | + firt = symbolic_name_to_rid(zIrt, "f"); | |
| 3092 | + if( firt<0 ){ | |
| 3093 | + rc = -ajax_route_error(400, "Ambiguous in-reply-do ID."); | |
| 3094 | + goto ajax_save_end; | |
| 3095 | + }else if( 0==firt ){ | |
| 3096 | + rc = -ajax_route_error(404, "Cannot resolve in-reply-do ID."); | |
| 3097 | + goto ajax_save_end; | |
| 3098 | + } | |
| 3099 | + } | |
| 3100 | + | |
| 3101 | + if( 0 ){ | |
| 3102 | + rc = -ajax_route_error(400, "Save is TODO. " | |
| 3103 | + "iPostFlags=%d debug=%d", | |
| 3104 | + iPostFlags, g.perm.Debug); | |
| 3105 | + goto ajax_save_end; | |
| 3106 | + } | |
| 3107 | + | |
| 3108 | + zTitle = firt ? 0 : P("title"); | |
| 3109 | + nrid = forum_post_ajax(zTitle, firt, fpid, 0, zMimetype, | |
| 3110 | + zContent, iPostFlags); | |
| 3111 | + if( nrid<0 ){ | |
| 3112 | + rc = nrid; | |
| 3113 | + goto ajax_save_end; | |
| 3114 | + }else if( nrid==0 ){ | |
| 3115 | + if( 0==(FPOST_DRYRUN & iPostFlags) ){ | |
| 3116 | + bRollback = 1; | |
| 3117 | + CX("{\"message\": \"No saving needed.\"}\n"); | |
| 3118 | + }else{ | |
| 3119 | + CX("{\"message\": \"Rolled back for dry-run.\"," | |
| 3120 | + "\"iPostFlags\":%d}\n", iPostFlags); | |
| 3121 | + } | |
| 3122 | + goto ajax_save_end; | |
| 3123 | + }else{ | |
| 3124 | + const int bNeedsModeration = forum_need_moderation(); | |
| 3125 | + const int fpHead = forumpost_head_rid(nrid); | |
| 3126 | + assert( nrid>0 ); | |
| 3127 | + assert( fpHead>0 ); | |
| 3128 | + zNewUuid = rid_to_uuid(nrid); | |
| 3129 | + if( 0!=P("file1") ){ | |
| 3130 | + /* Attachments */ | |
| 3131 | + if( !g.perm.Admin && !g.perm.AttachForum ){ | |
| 3132 | + rc = -ajax_route_error(403, "No permission no attach files."); | |
| 3133 | + goto ajax_save_end; | |
| 3134 | + }else{ | |
| 3135 | + char *zRoot = (nrid==fpHead) ? 0 : rid_to_uuid(fpHead); | |
| 3136 | + nAttach = attachments_ajax_from_POST(zRoot ? zRoot : zNewUuid, | |
| 3137 | + bNeedsModeration); | |
| 3138 | + fossil_free(zRoot); | |
| 3139 | + if( nAttach<0 ){ | |
| 3140 | + rc = nAttach; | |
| 3141 | + goto ajax_save_end; | |
| 3142 | + } | |
| 3143 | + if( nAttach>0 | |
| 3144 | + && (iPostFlags & FPOST_NO_ALERT)!=0 | |
| 3145 | + && db_table_exists("repository","pending_alert") ){ | |
| 3146 | + /* Unqueue any alerts for these attachments. Recall that | |
| 3147 | + ** they're attached to the first version of the post, which | |
| 3148 | + ** means we actually risk cancelling _other_ pending | |
| 3149 | + ** notifications for attachments on this same post. C'est la | |
| 3150 | + ** vie.*/ | |
| 3151 | + db_multi_exec( | |
| 3152 | + "WITH x(id) AS (\n" | |
| 3153 | + " SELECT 'f%d'\n" | |
| 3154 | + " UNION ALL\n" | |
| 3155 | + " SELECT 'f'||a.attachid FROM blob b, attachment a\n" | |
| 3156 | + " WHERE b.rid=%d\n" | |
| 3157 | + " AND b.uuid=a.target\n" | |
| 3158 | + ") DELETE FROM pending_alert WHERE eventid IN x", | |
| 3159 | + fpHead, fpHead | |
| 3160 | + ); | |
| 3161 | + } | |
| 3162 | + } | |
| 3163 | + } | |
| 3164 | + if( 0==bNeedsModeration | |
| 3165 | + /* ^^^ Do not allow a status tag on a pending-moderation post | |
| 3166 | + ** because it will introduce a reference to an artifact which | |
| 3167 | + ** will become a phantom if it is rejected by a moderator. */ | |
| 3168 | + && zStatus!=0 && zStatus[0]!=0 | |
| 3169 | + && forum_may_set_status(nrid) | |
| 3170 | + && (bStatusSet=forumpost_tag(nrid, 1, "status", zStatus))<0 ){ | |
| 3171 | + rc = -ajax_route_error(500, "Tagging failed: %s", g.zErrMsg); | |
| 3172 | + goto ajax_save_end; | |
| 3173 | + } | |
| 3174 | + } | |
| 3175 | + | |
| 3176 | + assert( 0==rc ); | |
| 3177 | + assert( zNewUuid ); | |
| 3178 | + CX("{\"uuid\": %!j, \"attachedCount\": %d, " | |
| 3179 | + "\"statusModified\": %d, " | |
| 3180 | + "\"dryrun\": %s, \"iPostFlags\":%d}\n", | |
| 3181 | + zNewUuid, nAttach, | |
| 3182 | + bStatusSet, bRollback ? "true" : "false", iPostFlags); | |
| 3183 | + | |
| 3184 | +ajax_save_end: | |
| 3185 | + manifest_destroy(pPost); | |
| 3186 | + fossil_free(zNewUuid); | |
| 3187 | + db_end_transaction(rc || bRollback); | |
| 3188 | +} | |
| 2728 | 3189 | |
| 2729 | 3190 | ADDED src/fossil.attach.js |
| --- src/forum.c | |
| +++ src/forum.c | |
| @@ -96,11 +96,11 @@ | |
| 96 | |
| 97 | /* |
| 98 | ** Returns a high-level representation of the forum-statuses setting. |
| 99 | ** This is a singleton, cached across calls. |
| 100 | */ |
| 101 | static const ForumStatusList * forum_statuses(void){ |
| 102 | static ForumStatusList fses = {0,0}; |
| 103 | static int once = 0; |
| 104 | while( !once ){ |
| 105 | ++once; |
| 106 | /* Read `forum-statuses` setting and transform it into the |
| @@ -151,11 +151,13 @@ | |
| 151 | ** found, the corresponding object is returned. If no match is found |
| 152 | ** then (A) if bFirst is false then 0 is returned, else (B) the first |
| 153 | ** entry in the list is returned, noting that the list may be empty, |
| 154 | ** in which case 0 is returned. |
| 155 | */ |
| 156 | const ForumStatus * forum_status_by_value(const char *z, int bFirst){ |
| 157 | const ForumStatusList * const fses = forum_statuses(); |
| 158 | const ForumStatus * fs0 = 0; |
| 159 | unsigned int i; |
| 160 | if( !fses->n ) return 0; |
| 161 | for( i = 0; i < fses->n; ++i ){ |
| @@ -227,18 +229,18 @@ | |
| 227 | } |
| 228 | |
| 229 | /* |
| 230 | ** Works like forumpost_head_rid() but expects zUuid to be an |
| 231 | ** unambiguous forum post name. It may be a hash prefix, so long as |
| 232 | ** it's unambiguous. Returns 0 if the name cannot be unambiguously |
| 233 | ** resolved as a forum post. |
| 234 | */ |
| 235 | int forumpost_head_rid2(const char *zUuid){ |
| 236 | const int fpid = symbolic_name_to_rid(zUuid, "f"); |
| 237 | return fpid>0 |
| 238 | ? forumpost_head_rid(fpid) |
| 239 | : 0; |
| 240 | } |
| 241 | |
| 242 | /* |
| 243 | ** Given a forum post RID and user name, returns true if zUserName |
| 244 | ** matches the event.(euser,user) field for a formpost entry with the |
| @@ -408,18 +410,17 @@ | |
| 408 | ** no tag is added. Similarly, it will only remove a tag from a post |
| 409 | ** which has its own tag, and will not remove an inherited one from a |
| 410 | ** parent post. |
| 411 | ** |
| 412 | ** If addTag is true and frid is already tagged, this is a |
| 413 | ** no-op. Likewise, if addTag is false and frid is not tagged |
| 414 | ** (not accounting for an inherited closed tag), this is a no-op. |
| 415 | ** |
| 416 | ** If bCheckIrt is true then the forum post IRT hierarchy is searched |
| 417 | ** for the tag, otherwise only the given RID is checked. |
| 418 | ** |
| 419 | ** Returns true if it actually creates a new tag, else false. Fails |
| 420 | ** fatally on error. |
| 421 | ** |
| 422 | ** If it returns true then state from previously-loaded posts may be |
| 423 | ** invalidated if they refer to the amended post or a response to it. |
| 424 | ** e.g. if zTagName is "closed" then ForumPost::iClosed values may be |
| 425 | ** stale. |
| @@ -438,11 +439,12 @@ | |
| 438 | ** |
| 439 | ** - The applied tag is propagating so so that "closed" tags can |
| 440 | ** account for how edits of posts are handled. This differs from |
| 441 | ** closure of a branch, where a non-propagating tag is used. |
| 442 | */ |
| 443 | static int forumpost_tag(int frid, const char *zTagName, int addTag, |
| 444 | const char *zValue){ |
| 445 | Blob artifact = BLOB_INITIALIZER; /* Output artifact */ |
| 446 | Blob cksum = BLOB_INITIALIZER; /* Z-card */ |
| 447 | int iTagged; /* true if frid is already tagged */ |
| 448 | int trid; /* RID of new control artifact */ |
| @@ -461,11 +463,11 @@ | |
| 461 | zValue = 0; |
| 462 | } |
| 463 | if( addTag && iTagged ){ |
| 464 | char *zOld = 0; |
| 465 | int cmp; |
| 466 | rid_has_tag2(iTagged, zTagName, &zOld); |
| 467 | cmp = fossil_strcmp(zOld, zValue); |
| 468 | fossil_free(zOld); |
| 469 | if( 0==cmp ){ |
| 470 | /* Same value - leave it as is. */ |
| 471 | db_end_transaction(0); |
| @@ -481,22 +483,58 @@ | |
| 481 | md5sum_blob(&artifact, &cksum); |
| 482 | blob_appendf(&artifact, "Z %b\n", &cksum); |
| 483 | blob_reset(&cksum); |
| 484 | trid = content_put_ex(&artifact, 0, 0, 0, 0); |
| 485 | if( trid==0 ){ |
| 486 | fossil_fatal("Error saving tag artifact: %s", g.zErrMsg); |
| 487 | } |
| 488 | if( manifest_crosslink(trid, &artifact, MC_NONE)==0 ){ |
| 489 | fossil_fatal("%s", g.zErrMsg); |
| 490 | } |
| 491 | assert( blob_is_reset(&artifact) ); |
| 492 | db_add_unsent(trid); |
| 493 | admin_log("Tag forum post %S with %c%s", |
| 494 | zUuid, addTag ? '*' : '-', zTagName); |
| 495 | fossil_free(zUuid); |
| 496 | db_end_transaction(0); |
| 497 | return 1; |
| 498 | } |
| 499 | |
| 500 | /* |
| 501 | ** Returns true if the forum-close-policy setting is true, else false, |
| 502 | ** caching the result for subsequent calls. |
| @@ -894,11 +932,19 @@ | |
| 894 | break; |
| 895 | } |
| 896 | } |
| 897 | if( !sCurrent ) sCurrent = &fss->aStatus[0]; |
| 898 | assert( sCurrent ); |
| 899 | @ <span class='forum-status-selection'> |
| 900 | if( forum_may_set_status(fp->fpid) ){ |
| 901 | @ <form method="post" action='%R/forumpost_status'> |
| 902 | login_insert_csrf_secret(); |
| 903 | @ <input type='hidden' name='fpid' value='%s(fp->zUuid)' /> |
| 904 | @ <select name='status' data-fpid='%s(fp->zUuid)'\ |
| @@ -917,11 +963,11 @@ | |
| 917 | @ </form> |
| 918 | /* Form is activated in fossil.page.forumpost.js */ |
| 919 | }else{ |
| 920 | @ <button disabled>Status: %h(sCurrent->zLabel)</button> |
| 921 | } |
| 922 | @ </span> |
| 923 | fossil_free(zCurrent); |
| 924 | } |
| 925 | } |
| 926 | |
| 927 | /* |
| @@ -1045,17 +1091,26 @@ | |
| 1045 | /* |
| 1046 | ** Renders the attachment list for the given forum post. |
| 1047 | ** Emits no output if there are no attachments. |
| 1048 | */ |
| 1049 | static void forum_render_attachment_list(const char *zUuid){ |
| 1050 | char * zLbl = mprintf("<a href='%R/attachlist?forumpost=%s'>" |
| 1051 | "Attachments:</a>", zUuid); |
| 1052 | attachment_list(zUuid, zLbl, |
| 1053 | ATTACHLIST_HRULE_ABOVE |
| 1054 | | ATTACHLIST_SIZE |
| 1055 | | ATTACHLIST_HIDE_UNAPPROVED); |
| 1056 | fossil_free(zLbl); |
| 1057 | } |
| 1058 | |
| 1059 | /* |
| 1060 | ** Renders the attachment list for p or (if not NULL) pEditHead. |
| 1061 | */ |
| @@ -1098,24 +1153,33 @@ | |
| 1098 | |
| 1099 | /* Get the manifest for the post. Abort if not found (e.g. shunned). */ |
| 1100 | pManifest = manifest_get(p->fpid, CFTYPE_FORUM, 0); |
| 1101 | if( !pManifest ) return; |
| 1102 | iClosed = forumpost_is_closed(pThread, p, 1); |
| 1103 | /* When not in raw mode, create the border around the post. */ |
| 1104 | if( !bRaw ){ |
| 1105 | /* Open the <div> enclosing the post. Set the class string to mark the post |
| 1106 | ** as selected and/or obsolete. */ |
| 1107 | iIndent = (p->pEditHead ? p->pEditHead->nIndent : p->nIndent)-1; |
| 1108 | @ <div id='forum%d(p->fpid)' class='forumTime\ |
| 1109 | @ %s(bSelect ? " forumSel" : "")\ |
| 1110 | @ %s(iClosed ? " forumClosed" : "")\ |
| 1111 | @ %s(p->pEditTail ? " forumObs" : "")' \ |
| 1112 | if( iIndent && iIndentScale ){ |
| 1113 | @ style='margin-left:%d(iIndent*iIndentScale)ex;'> |
| 1114 | }else{ |
| 1115 | @ > |
| 1116 | } |
| 1117 | |
| 1118 | /* If this is the first post (or an edit thereof), emit the thread title. */ |
| 1119 | if( pManifest->zThreadTitle ){ |
| 1120 | @ <h1>%h(pManifest->zThreadTitle)</h1> |
| 1121 | } |
| @@ -1196,32 +1260,33 @@ | |
| 1196 | /* Provide a link to the raw source code. */ |
| 1197 | if( !bUnf ){ |
| 1198 | @ %z(href("%R/forumpost/%!S?raw",p->zUuid))[source]</a> |
| 1199 | } |
| 1200 | @ </h3> |
| 1201 | }/*!bRaw*/ |
| 1202 | |
| 1203 | /* Check if this post is approved, also if it's by the current user. */ |
| 1204 | bPrivate = content_is_private(p->fpid); |
| 1205 | bSameUser = login_is_individual() |
| 1206 | && fossil_strcmp(pManifest->zUser, g.zLogin)==0; |
| 1207 | |
| 1208 | /* Render the post if the user is able to see it. */ |
| 1209 | if( bPrivate && !g.perm.ModForum && !bSameUser ){ |
| 1210 | @ <p><span class="modpending">Awaiting Moderator Approval</span></p> |
| 1211 | }else{ |
| 1212 | if( bRaw || bUnf || p->pEditTail ){ |
| 1213 | zMimetype = "text/plain"; |
| 1214 | }else{ |
| 1215 | zMimetype = pManifest->zMimetype; |
| 1216 | } |
| 1217 | forum_render(0, zMimetype, pManifest->zWiki, 0, !bRaw); |
| 1218 | forum_render_attachment_list2(p); |
| 1219 | } |
| 1220 | |
| 1221 | /* When not in raw mode, finish creating the border around the post. */ |
| 1222 | if( !bRaw ){ |
| 1223 | /* If the user is able to write to the forum and if this post has not been |
| 1224 | ** edited, create a form with various interaction buttons. */ |
| 1225 | if( g.perm.WrForum && !p->pEditTail ){ |
| 1226 | @ <div class="forumpost-single-controls">\ |
| 1227 | @ <form action="%R/forumedit" method="POST"> |
| @@ -1250,10 +1315,11 @@ | |
| 1250 | @ <br><label><input type="checkbox" name="trust"> |
| 1251 | @ Trust user "%h(pManifest->zUser)" so that future posts by \ |
| 1252 | @ "%h(pManifest->zUser)" do not require moderation. |
| 1253 | @ </label> |
| 1254 | @ <input type="hidden" name="trustuser" value="%h(pManifest->zUser)"> |
| 1255 | } |
| 1256 | }else if( bSameUser ){ |
| 1257 | /* Allow users to delete (reject) their own pending posts. */ |
| 1258 | @ <input type="submit" name="reject" value="Delete"> |
| 1259 | } |
| @@ -1273,23 +1339,19 @@ | |
| 1273 | @ %s(iClosed ? "action-reopen" : "action-close")'/> |
| 1274 | /* ^^^ activated by fossil.page.forumpost.js */ |
| 1275 | } |
| 1276 | @ </form> |
| 1277 | } |
| 1278 | if( g.perm.Admin || |
| 1279 | (login_is_individual() |
| 1280 | && forumpost_is_owner(p/*not pHead*/->fpid, 0)) ){ |
| 1281 | /* When an admin edits someone else's post, the admin |
| 1282 | ** effectively takes over ownership of it (and we currently |
| 1283 | ** have no way of passing it back). Because of this, we |
| 1284 | ** check the ownership of `p` instead of `pHead`. */ |
| 1285 | @ <form method="post" action="%R/attachadd">\ |
| 1286 | @ <input type="hidden" name="forumpost" value="%T(pHead->zUuid)"> |
| 1287 | @ <input type="submit" value="Attach..."> |
| 1288 | login_insert_csrf_secret(); |
| 1289 | moderation_pending_www(p->fpid); |
| 1290 | @ </form> |
| 1291 | } |
| 1292 | } |
| 1293 | @ </div> |
| 1294 | } |
| 1295 | if( !p->pIrt && (flags & FDISPLAY_SELECTED)){ |
| @@ -1475,11 +1537,12 @@ | |
| 1475 | ** to all forum-related pages. It does not include page-specific |
| 1476 | ** code (e.g. "forum.js"). |
| 1477 | */ |
| 1478 | static void forum_emit_js(void){ |
| 1479 | builtin_fossil_js_bundle_or("copybutton", "pikchr", "confirmer", |
| 1480 | NULL); |
| 1481 | builtin_request_js("fossil.page.forumpost.js"); |
| 1482 | } |
| 1483 | |
| 1484 | /* |
| 1485 | ** WEBPAGE: forumpost |
| @@ -1635,21 +1698,13 @@ | |
| 1635 | if( g.perm.WrTForum ) return 0; |
| 1636 | if( g.perm.ModForum ) return 0; |
| 1637 | return 1; |
| 1638 | } |
| 1639 | |
| 1640 | /* |
| 1641 | ** Return true if the string is white-space only. |
| 1642 | */ |
| 1643 | static int whitespace_only(const char *z){ |
| 1644 | if( z==0 ) return 1; |
| 1645 | while( z[0] && fossil_isspace(z[0]) ){ z++; } |
| 1646 | return z[0]==0; |
| 1647 | } |
| 1648 | |
| 1649 | /* Flags for use with forum_post() */ |
| 1650 | #define FPOST_NO_ALERT 1 /* do not send any alerts */ |
| 1651 | |
| 1652 | /* |
| 1653 | ** Return a flags value for use with the final argument to |
| 1654 | ** forum_post(), extracted from the CGI environment. |
| 1655 | */ |
| @@ -1656,10 +1711,13 @@ | |
| 1656 | static int forum_post_flags(void){ |
| 1657 | int iPostFlags = 0; |
| 1658 | if( g.perm.Debug && P("fpsilent")!=0 ){ |
| 1659 | iPostFlags |= FPOST_NO_ALERT; |
| 1660 | } |
| 1661 | return iPostFlags; |
| 1662 | } |
| 1663 | |
| 1664 | /* |
| 1665 | ** Add a new Forum Post artifact to the repository. |
| @@ -1687,11 +1745,11 @@ | |
| 1687 | if( !g.perm.Admin && (iEdit || iInReplyTo) |
| 1688 | && forum_rid_is_tagged(iEdit ? iEdit : iInReplyTo, "closed", 1) ){ |
| 1689 | forumpost_error_closed(); |
| 1690 | return 0; |
| 1691 | } |
| 1692 | if( iEdit==0 && whitespace_only(zContent) ){ |
| 1693 | return 0; |
| 1694 | } |
| 1695 | if( iInReplyTo==0 && iEdit>0 ){ |
| 1696 | iBasis = iEdit; |
| 1697 | iInReplyTo = db_int(0, "SELECT firt FROM forumpost WHERE fpid=%d", iEdit); |
| @@ -1712,17 +1770,19 @@ | |
| 1712 | fossil_free(zG); |
| 1713 | } |
| 1714 | if( zTitle ){ |
| 1715 | blob_appendf(&x, "H %F\n", zTitle); |
| 1716 | } |
| 1717 | zI = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", iInReplyTo); |
| 1718 | if( zI ){ |
| 1719 | blob_appendf(&x, "I %s\n", zI); |
| 1720 | fossil_free(zI); |
| 1721 | } |
| 1722 | if( fossil_strcmp(zMimetype,"text/x-fossil-wiki")!=0 ){ |
| 1723 | blob_appendf(&x, "N %s\n", zMimetype); |
| 1724 | } |
| 1725 | if( iEdit>0 ){ |
| 1726 | char *zP = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", iEdit); |
| 1727 | if( zP==0 ) webpage_error("missing edit artifact %d", iEdit); |
| 1728 | blob_appendf(&x, "P %s\n", zP); |
| @@ -1750,11 +1810,11 @@ | |
| 1750 | webpage_error("malformed forum post artifact - %s", blob_str(&errMsg)); |
| 1751 | } |
| 1752 | webpage_assert( pPost->type==CFTYPE_FORUM ); |
| 1753 | manifest_destroy(pPost); |
| 1754 | |
| 1755 | if( P("dryrun") ){ |
| 1756 | @ <div class='debug'> |
| 1757 | @ This is the artifact that would have been generated: |
| 1758 | @ <pre>%h(blob_str(&x))</pre> |
| 1759 | @ </div> |
| 1760 | blob_reset(&x); |
| @@ -1813,12 +1873,17 @@ | |
| 1813 | int addTag, int validFpid){ |
| 1814 | if( !cgi_csrf_safe(2) ){ |
| 1815 | webpage_error("CSRF validation failed"); |
| 1816 | }else{ |
| 1817 | const int fpid = validFpid>0 ? validFpid : forum_validate_fpid_param(); |
| 1818 | forumpost_tag(fpid, zTag, addTag, zVal); |
| 1819 | cgi_redirectf("%R/forumpost/%S",P("fpid")); |
| 1820 | } |
| 1821 | } |
| 1822 | |
| 1823 | /* |
| 1824 | ** WEBPAGE: forumpost_close hidden |
| @@ -1954,22 +2019,24 @@ | |
| 1954 | } |
| 1955 | } |
| 1956 | |
| 1957 | /* |
| 1958 | ** If the user has AttachForum permissions, emit a notice that |
| 1959 | ** attachments may be added after saving. If p is not NULL, |
| 1960 | ** also emit its list of attachments. |
| 1961 | */ |
| 1962 | static void forum_render_attachment_notice(void){ |
| 1963 | if( g.perm.AttachForum ){ |
| 1964 | @ <div>You will be able to attach files to this post after saving |
| 1965 | @ it.</div> |
| 1966 | } |
| 1967 | } |
| 1968 | |
| 1969 | /* |
| 1970 | ** WEBPAGE: forume1 |
| 1971 | ** |
| 1972 | ** Start a new forum thread. |
| 1973 | */ |
| 1974 | void forumnew_page(void){ |
| 1975 | const char *zTitle = PDT("title",""); |
| @@ -1983,36 +2050,45 @@ | |
| 1983 | } |
| 1984 | if( P("submit") && cgi_csrf_safe(2) ){ |
| 1985 | if( forum_post(zTitle, 0, 0, 0, zMimetype, zContent, |
| 1986 | forum_post_flags()) ) return; |
| 1987 | } |
| 1988 | if( P("preview") && !whitespace_only(zContent) ){ |
| 1989 | @ <h1>Preview:</h1> |
| 1990 | forum_render(zTitle, zMimetype, zContent, "forumEdit", 1); |
| 1991 | } |
| 1992 | style_set_current_feature("forum"); |
| 1993 | style_header("New Forum Thread"); |
| 1994 | @ <form action="%R/forume1" method="POST"> |
| 1995 | @ <h1>New Thread:</h1> |
| 1996 | forum_from_line(); |
| 1997 | forum_post_widget(zTitle, zMimetype, zContent); |
| 1998 | @ <input type="submit" name="preview" value="Preview"> |
| 1999 | if( P("preview") && !whitespace_only(zContent) ){ |
| 2000 | @ <input type="submit" name="submit" value="Submit"> |
| 2001 | }else{ |
| 2002 | @ <input type="submit" name="submit" value="Submit" disabled> |
| 2003 | } |
| 2004 | forum_render_debug_options(); |
| 2005 | login_insert_csrf_secret(); |
| 2006 | @ </form> |
| 2007 | forum_render_attachment_notice(); |
| 2008 | forum_emit_js(); |
| 2009 | style_finish_page(); |
| 2010 | } |
| 2011 | |
| 2012 | /* |
| 2013 | ** WEBPAGE: forume2 |
| 2014 | ** |
| 2015 | ** Edit an existing forum message. |
| 2016 | ** Query parameters: |
| 2017 | ** |
| 2018 | ** fpid=X Hash of the post to be edited. REQUIRED |
| @@ -2097,11 +2173,11 @@ | |
| 2097 | style_set_current_feature("forum"); |
| 2098 | isDelete = P("nullout")!=0; |
| 2099 | if( P("submit") |
| 2100 | && isCsrfSafe |
| 2101 | && (zContent = PDT("content",""))!=0 |
| 2102 | && (!whitespace_only(zContent) || isDelete) |
| 2103 | ){ |
| 2104 | int done = 1; |
| 2105 | const char *zMimetype = PD("mimetype",DEFAULT_FORUM_MIMETYPE); |
| 2106 | if( bReply ){ |
| 2107 | done = forum_post(0, fpid, 0, 0, zMimetype, zContent, |
| @@ -2175,11 +2251,11 @@ | |
| 2175 | zDisplayName = display_name_from_login(pPost->zUser); |
| 2176 | @ <h3 class='forumPostHdr'>By %s(zDisplayName) on %h(zDate)</h3> |
| 2177 | fossil_free(zDisplayName); |
| 2178 | fossil_free(zDate); |
| 2179 | forum_render(0, pPost->zMimetype, pPost->zWiki, "forumEdit", 1); |
| 2180 | if( bPreview && !whitespace_only(zContent) ){ |
| 2181 | @ <h2>Preview:</h2> |
| 2182 | forum_render(0, zMimetype,zContent, "forumEdit", 1); |
| 2183 | } |
| 2184 | @ <h2>Enter Reply:</h2> |
| 2185 | @ <form action="%R/forume2" method="POST"> |
| @@ -2190,11 +2266,11 @@ | |
| 2190 | } |
| 2191 | if( !isDelete ){ |
| 2192 | @ <input type="submit" name="preview" value="Preview"> |
| 2193 | } |
| 2194 | @ <input type="submit" name="cancel" value="Cancel"> |
| 2195 | if( (bPreview && !whitespace_only(zContent)) || isDelete ){ |
| 2196 | if( !iClosed || g.perm.Admin ) { |
| 2197 | @ <input type="submit" name="submit" value="Submit"> |
| 2198 | } |
| 2199 | } |
| 2200 | forum_render_debug_options(); |
| @@ -2201,11 +2277,13 @@ | |
| 2201 | login_insert_csrf_secret(); |
| 2202 | @ </form> |
| 2203 | if( !bReply ){ |
| 2204 | forum_render_attachment_list(rid_to_uuid(fpid)); |
| 2205 | } |
| 2206 | forum_render_attachment_notice(); |
| 2207 | forum_emit_js(); |
| 2208 | style_finish_page(); |
| 2209 | } |
| 2210 | |
| 2211 | /* |
| @@ -2214,21 +2292,21 @@ | |
| 2214 | ** to closed posts. If false, only administrators may do so. Note that |
| 2215 | ** this only affects the forum web UI, not post-closing tags which |
| 2216 | ** arrive via the command-line or from synchronization with a remote. |
| 2217 | ** This policy also determines whether moderators may delete forum |
| 2218 | ** attachments. |
| 2219 | */ |
| 2220 | /* |
| 2221 | ** SETTING: forum-title width=20 default=Forum |
| 2222 | ** This is the name or "title" of the Forum for this repository. The |
| 2223 | ** default is just "Forum". But in some setups, admins might want to |
| 2224 | ** change it to "Developer Forum" or "User Forum" or whatever other name |
| 2225 | ** seems more appropriate for the particular usage. |
| 2226 | ** |
| 2227 | ** SETTING: attachment-size-limit width=16 |
| 2228 | ** The maximum number of bytes for an attachment. The default (or 0) is |
| 2229 | ** unlimited but a limit may be imposed by the web server or a proxy. |
| 2230 | ** |
| 2231 | ** SETTING: forum-statuses width=40 block-text |
| 2232 | ** This JSON5-formatted value defines an array of objects describing |
| 2233 | ** the available statuses of forum posts. Each entry of the array must |
| 2234 | ** be an object in the form {label:"X",value:"Y"}. |
| @@ -2723,5 +2801,388 @@ | |
| 2723 | ** URL arg when the status selection list is activated. */ |
| 2724 | forum_emit_js(); |
| 2725 | } |
| 2726 | style_finish_page(); |
| 2727 | } |
| 2728 | |
| 2729 | DDED src/fossil.attach.js |
| --- src/forum.c | |
| +++ src/forum.c | |
| @@ -96,11 +96,11 @@ | |
| 96 | |
| 97 | /* |
| 98 | ** Returns a high-level representation of the forum-statuses setting. |
| 99 | ** This is a singleton, cached across calls. |
| 100 | */ |
| 101 | const ForumStatusList * forum_statuses(void){ |
| 102 | static ForumStatusList fses = {0,0}; |
| 103 | static int once = 0; |
| 104 | while( !once ){ |
| 105 | ++once; |
| 106 | /* Read `forum-statuses` setting and transform it into the |
| @@ -151,11 +151,13 @@ | |
| 151 | ** found, the corresponding object is returned. If no match is found |
| 152 | ** then (A) if bFirst is false then 0 is returned, else (B) the first |
| 153 | ** entry in the list is returned, noting that the list may be empty, |
| 154 | ** in which case 0 is returned. |
| 155 | */ |
| 156 | static const ForumStatus * forum_status_by_value( |
| 157 | const char *z, int bFirst |
| 158 | ){ |
| 159 | const ForumStatusList * const fses = forum_statuses(); |
| 160 | const ForumStatus * fs0 = 0; |
| 161 | unsigned int i; |
| 162 | if( !fses->n ) return 0; |
| 163 | for( i = 0; i < fses->n; ++i ){ |
| @@ -227,18 +229,18 @@ | |
| 229 | } |
| 230 | |
| 231 | /* |
| 232 | ** Works like forumpost_head_rid() but expects zUuid to be an |
| 233 | ** unambiguous forum post name. It may be a hash prefix, so long as |
| 234 | ** it's unambiguous. Returns the rid of the head post, -1 if the name |
| 235 | ** is ambiguous, and 0 if the name cannot be resolved as a forum post. |
| 236 | */ |
| 237 | int forumpost_head_rid2(const char *zUuid){ |
| 238 | const int fpid = symbolic_name_to_rid(zUuid, "f"); |
| 239 | return fpid>0 |
| 240 | ? forumpost_head_rid(fpid) |
| 241 | : fpid; |
| 242 | } |
| 243 | |
| 244 | /* |
| 245 | ** Given a forum post RID and user name, returns true if zUserName |
| 246 | ** matches the event.(euser,user) field for a formpost entry with the |
| @@ -408,18 +410,17 @@ | |
| 410 | ** no tag is added. Similarly, it will only remove a tag from a post |
| 411 | ** which has its own tag, and will not remove an inherited one from a |
| 412 | ** parent post. |
| 413 | ** |
| 414 | ** If addTag is true and frid is already tagged, this is a |
| 415 | ** no-op. Likewise, if addTag is false and frid is not tagged (not |
| 416 | ** accounting for a tag inherited via an in-response-to post), this is |
| 417 | ** a no-op. |
| 418 | ** |
| 419 | ** Returns a positive value (a new tag.tagid value) if it actually |
| 420 | ** creates a new tag, else 0. On error it returns a negative alue |
| 421 | ** and g.zErrMsg "should" contain details. |
| 422 | ** |
| 423 | ** If it returns true then state from previously-loaded posts may be |
| 424 | ** invalidated if they refer to the amended post or a response to it. |
| 425 | ** e.g. if zTagName is "closed" then ForumPost::iClosed values may be |
| 426 | ** stale. |
| @@ -438,11 +439,12 @@ | |
| 439 | ** |
| 440 | ** - The applied tag is propagating so so that "closed" tags can |
| 441 | ** account for how edits of posts are handled. This differs from |
| 442 | ** closure of a branch, where a non-propagating tag is used. |
| 443 | */ |
| 444 | static int forumpost_tag(int frid, int addTag, |
| 445 | const char *zTagName, |
| 446 | const char *zValue){ |
| 447 | Blob artifact = BLOB_INITIALIZER; /* Output artifact */ |
| 448 | Blob cksum = BLOB_INITIALIZER; /* Z-card */ |
| 449 | int iTagged; /* true if frid is already tagged */ |
| 450 | int trid; /* RID of new control artifact */ |
| @@ -461,11 +463,11 @@ | |
| 463 | zValue = 0; |
| 464 | } |
| 465 | if( addTag && iTagged ){ |
| 466 | char *zOld = 0; |
| 467 | int cmp; |
| 468 | rid_has_tag2(frid, zTagName, &zOld); |
| 469 | cmp = fossil_strcmp(zOld, zValue); |
| 470 | fossil_free(zOld); |
| 471 | if( 0==cmp ){ |
| 472 | /* Same value - leave it as is. */ |
| 473 | db_end_transaction(0); |
| @@ -481,22 +483,58 @@ | |
| 483 | md5sum_blob(&artifact, &cksum); |
| 484 | blob_appendf(&artifact, "Z %b\n", &cksum); |
| 485 | blob_reset(&cksum); |
| 486 | trid = content_put_ex(&artifact, 0, 0, 0, 0); |
| 487 | if( trid==0 ){ |
| 488 | return -1; |
| 489 | } |
| 490 | if( manifest_crosslink(trid, &artifact, MC_NONE)==0 ){ |
| 491 | return -2; |
| 492 | } |
| 493 | assert( blob_is_reset(&artifact) ); |
| 494 | db_add_unsent(trid); |
| 495 | admin_log("Tag forum post %S with %c%s", |
| 496 | zUuid, addTag ? '*' : '-', zTagName); |
| 497 | fossil_free(zUuid); |
| 498 | db_end_transaction(0); |
| 499 | return trid; |
| 500 | } |
| 501 | |
| 502 | /* |
| 503 | ** COMMAND: test-forumpost-tag |
| 504 | ** |
| 505 | ** Usage: %fossil test-forumpost-tag ?-cancel? THREADID TAGNAME TAGVAL |
| 506 | ** |
| 507 | ** A tester for forumpost_tag(). It always rolls back changes. |
| 508 | */ |
| 509 | void test_forumpost_tag_command(void){ |
| 510 | int fpid; |
| 511 | int rc; |
| 512 | const char *zPost; |
| 513 | const char *zTag; |
| 514 | const char *zVal; |
| 515 | const int bAdd = find_option("cancel","",0)==0; |
| 516 | |
| 517 | db_find_and_open_repository(0,0); |
| 518 | verify_all_options(); |
| 519 | if( g.argc<5 ){ |
| 520 | usage("forum-post-id tag-name value"); |
| 521 | } |
| 522 | zPost = g.argv[2]; |
| 523 | zTag = g.argv[3]; |
| 524 | zVal = g.argv[4]; |
| 525 | |
| 526 | db_begin_transaction(); |
| 527 | fpid = forumpost_head_rid2(zPost); |
| 528 | if( fpid<=0 ){ |
| 529 | fossil_fatal("Cannot resolve post ID %s", zPost); |
| 530 | } |
| 531 | fossil_print("%s => %d => %z\n", zTag, fpid, |
| 532 | rid_to_uuid(fpid)); |
| 533 | rc = forumpost_tag(fpid, bAdd, zTag, zVal); |
| 534 | fossil_print("tag fpid=%d taxgxref.tagid=%d\n", fpid, rc); |
| 535 | db_end_transaction(1); |
| 536 | } |
| 537 | |
| 538 | /* |
| 539 | ** Returns true if the forum-close-policy setting is true, else false, |
| 540 | ** caching the result for subsequent calls. |
| @@ -894,11 +932,19 @@ | |
| 932 | break; |
| 933 | } |
| 934 | } |
| 935 | if( !sCurrent ) sCurrent = &fss->aStatus[0]; |
| 936 | assert( sCurrent ); |
| 937 | @ <fieldset class='forum-status-selection'>\ |
| 938 | @ <legend>Status \ |
| 939 | @ <span class='help-buttonlet initially-hidden'>\ |
| 940 | @ Moderators and the post's owner may change \ |
| 941 | @ the status of this thread unless it is still. \ |
| 942 | @ pending moderation. See \ |
| 943 | @ <a href='%R/help/forum-statuses' target='_new'>\ |
| 944 | @ /help/forum-statuses</a></span>\ |
| 945 | @ </legend>\ |
| 946 | if( forum_may_set_status(fp->fpid) ){ |
| 947 | @ <form method="post" action='%R/forumpost_status'> |
| 948 | login_insert_csrf_secret(); |
| 949 | @ <input type='hidden' name='fpid' value='%s(fp->zUuid)' /> |
| 950 | @ <select name='status' data-fpid='%s(fp->zUuid)'\ |
| @@ -917,11 +963,11 @@ | |
| 963 | @ </form> |
| 964 | /* Form is activated in fossil.page.forumpost.js */ |
| 965 | }else{ |
| 966 | @ <button disabled>Status: %h(sCurrent->zLabel)</button> |
| 967 | } |
| 968 | @ </fieldset> |
| 969 | fossil_free(zCurrent); |
| 970 | } |
| 971 | } |
| 972 | |
| 973 | /* |
| @@ -1045,17 +1091,26 @@ | |
| 1091 | /* |
| 1092 | ** Renders the attachment list for the given forum post. |
| 1093 | ** Emits no output if there are no attachments. |
| 1094 | */ |
| 1095 | static void forum_render_attachment_list(const char *zUuid){ |
| 1096 | #if 1 |
| 1097 | attachment_list(zUuid, "📎 Attachments", 0 |
| 1098 | | ATTACHLIST_SIZE |
| 1099 | | ATTACHLIST_HIDE_UNAPPROVED |
| 1100 | | ATTACHLIST_DETAILS_CLOSED |
| 1101 | | ATTACHLIST_HIDE_EMPTY); |
| 1102 | #else |
| 1103 | char * zLbl = mprintf("<a href='%R/attachlist?forumpost=%!S'>" |
| 1104 | "Attachments</a>:", zUuid); |
| 1105 | attachment_list(zUuid, zLbl, |
| 1106 | ATTACHLIST_HRULE_ABOVE |
| 1107 | | ATTACHLIST_SIZE |
| 1108 | | ATTACHLIST_HIDE_UNAPPROVED |
| 1109 | | ATTACHLIST_HIDE_EMPTY); |
| 1110 | fossil_free(zLbl); |
| 1111 | #endif |
| 1112 | } |
| 1113 | |
| 1114 | /* |
| 1115 | ** Renders the attachment list for p or (if not NULL) pEditHead. |
| 1116 | */ |
| @@ -1098,24 +1153,33 @@ | |
| 1153 | |
| 1154 | /* Get the manifest for the post. Abort if not found (e.g. shunned). */ |
| 1155 | pManifest = manifest_get(p->fpid, CFTYPE_FORUM, 0); |
| 1156 | if( !pManifest ) return; |
| 1157 | iClosed = forumpost_is_closed(pThread, p, 1); |
| 1158 | bPrivate = content_is_private(p->fpid); |
| 1159 | bSameUser = login_is_individual() |
| 1160 | && fossil_strcmp(pManifest->zUser, g.zLogin)==0; |
| 1161 | /* When not in raw mode, create the border around the post. */ |
| 1162 | if( !bRaw ){ |
| 1163 | /* Open the <div> enclosing the post. Set the class string to mark the post |
| 1164 | ** as selected and/or obsolete. */ |
| 1165 | iIndent = (p->pEditHead ? p->pEditHead->nIndent : p->nIndent)-1; |
| 1166 | @ <div id='forum%d(p->fpid)' class='forumpost forumTime\ |
| 1167 | @ %s(bSelect ? " forumSel" : "")\ |
| 1168 | @ %s(iClosed ? " forumClosed" : "")\ |
| 1169 | @ %s(p->pEditTail ? " forumObs" : "")' \ |
| 1170 | if( iIndent && iIndentScale ){ |
| 1171 | @ style='margin-left:%d(iIndent*iIndentScale)ex;' \ |
| 1172 | } |
| 1173 | /* These data-X fields are used by the JS editor. */ |
| 1174 | if( p->pIrt ){ |
| 1175 | @ data-firt="%s(p->pIrt->zUuid)" \ |
| 1176 | } |
| 1177 | if( p->pEditHead ){ |
| 1178 | @ data-fedithead="%s(p->pEditHead->zUuid)" \ |
| 1179 | } |
| 1180 | @ data-fpid="%s(p->zUuid)">\ |
| 1181 | |
| 1182 | /* If this is the first post (or an edit thereof), emit the thread title. */ |
| 1183 | if( pManifest->zThreadTitle ){ |
| 1184 | @ <h1>%h(pManifest->zThreadTitle)</h1> |
| 1185 | } |
| @@ -1196,32 +1260,33 @@ | |
| 1260 | /* Provide a link to the raw source code. */ |
| 1261 | if( !bUnf ){ |
| 1262 | @ %z(href("%R/forumpost/%!S?raw",p->zUuid))[source]</a> |
| 1263 | } |
| 1264 | @ </h3> |
| 1265 | |
| 1266 | if( bPrivate && (bSameUser || g.perm.Admin || g.perm.ModForum) ){ |
| 1267 | moderation_pending_www(p->fpid); |
| 1268 | } |
| 1269 | }/*!bRaw*/ |
| 1270 | |
| 1271 | /* Check if this post is approved, also if it's by the current user. |
| 1272 | Render the post if the user is able to see it. */ |
| 1273 | if( bPrivate && !g.perm.ModForum && !bSameUser ){ |
| 1274 | @ <p><span class="modpending">Awaiting Moderator Approval</span></p> |
| 1275 | }else{ |
| 1276 | if( bRaw || bUnf || p->pEditTail ){ |
| 1277 | zMimetype = "text/plain"; |
| 1278 | }else{ |
| 1279 | zMimetype = pManifest->zMimetype; |
| 1280 | } |
| 1281 | forum_render(0, zMimetype, pManifest->zWiki, 0, !bRaw); |
| 1282 | } |
| 1283 | |
| 1284 | /* When not in raw mode, finish creating the border around the post. */ |
| 1285 | if( !bRaw ){ |
| 1286 | int bBrBeforeAttach = 0; /* Layout kludge for Attach button */ |
| 1287 | forum_render_attachment_list2(p); |
| 1288 | /* If the user is able to write to the forum and if this post has not been |
| 1289 | ** edited, create a form with various interaction buttons. */ |
| 1290 | if( g.perm.WrForum && !p->pEditTail ){ |
| 1291 | @ <div class="forumpost-single-controls">\ |
| 1292 | @ <form action="%R/forumedit" method="POST"> |
| @@ -1250,10 +1315,11 @@ | |
| 1315 | @ <br><label><input type="checkbox" name="trust"> |
| 1316 | @ Trust user "%h(pManifest->zUser)" so that future posts by \ |
| 1317 | @ "%h(pManifest->zUser)" do not require moderation. |
| 1318 | @ </label> |
| 1319 | @ <input type="hidden" name="trustuser" value="%h(pManifest->zUser)"> |
| 1320 | bBrBeforeAttach = 1 /* slightly unmangle the layout */; |
| 1321 | } |
| 1322 | }else if( bSameUser ){ |
| 1323 | /* Allow users to delete (reject) their own pending posts. */ |
| 1324 | @ <input type="submit" name="reject" value="Delete"> |
| 1325 | } |
| @@ -1273,23 +1339,19 @@ | |
| 1339 | @ %s(iClosed ? "action-reopen" : "action-close")'/> |
| 1340 | /* ^^^ activated by fossil.page.forumpost.js */ |
| 1341 | } |
| 1342 | @ </form> |
| 1343 | } |
| 1344 | if( attach_user_may(p/*not pHead*/->fpid, CFTYPE_FORUM) ){ |
| 1345 | /* When an admin edits someone else's post, the admin |
| 1346 | ** effectively takes over ownership of it (and we currently |
| 1347 | ** have no way of passing it back). Because of this, we |
| 1348 | ** check the ownership of `p` instead of `pHead`. */ |
| 1349 | if( bBrBeforeAttach ){ |
| 1350 | @ <br> |
| 1351 | } |
| 1352 | attach_render_attachadd_button(pHead->zUuid); |
| 1353 | } |
| 1354 | } |
| 1355 | @ </div> |
| 1356 | } |
| 1357 | if( !p->pIrt && (flags & FDISPLAY_SELECTED)){ |
| @@ -1475,11 +1537,12 @@ | |
| 1537 | ** to all forum-related pages. It does not include page-specific |
| 1538 | ** code (e.g. "forum.js"). |
| 1539 | */ |
| 1540 | static void forum_emit_js(void){ |
| 1541 | builtin_fossil_js_bundle_or("copybutton", "pikchr", "confirmer", |
| 1542 | "attach", "tabs", "storage", |
| 1543 | "popupwidget", NULL); |
| 1544 | builtin_request_js("fossil.page.forumpost.js"); |
| 1545 | } |
| 1546 | |
| 1547 | /* |
| 1548 | ** WEBPAGE: forumpost |
| @@ -1635,21 +1698,13 @@ | |
| 1698 | if( g.perm.WrTForum ) return 0; |
| 1699 | if( g.perm.ModForum ) return 0; |
| 1700 | return 1; |
| 1701 | } |
| 1702 | |
| 1703 | /* Flags for use with forum_post() */ |
| 1704 | #define FPOST_NO_ALERT 1 /* do not send any alerts */ |
| 1705 | #define FPOST_DRYRUN 2 /* do not save the artifact */ |
| 1706 | |
| 1707 | /* |
| 1708 | ** Return a flags value for use with the final argument to |
| 1709 | ** forum_post(), extracted from the CGI environment. |
| 1710 | */ |
| @@ -1656,10 +1711,13 @@ | |
| 1711 | static int forum_post_flags(void){ |
| 1712 | int iPostFlags = 0; |
| 1713 | if( g.perm.Debug && P("fpsilent")!=0 ){ |
| 1714 | iPostFlags |= FPOST_NO_ALERT; |
| 1715 | } |
| 1716 | if( P("dryrun")!=0 ){ |
| 1717 | iPostFlags |= FPOST_DRYRUN; |
| 1718 | } |
| 1719 | return iPostFlags; |
| 1720 | } |
| 1721 | |
| 1722 | /* |
| 1723 | ** Add a new Forum Post artifact to the repository. |
| @@ -1687,11 +1745,11 @@ | |
| 1745 | if( !g.perm.Admin && (iEdit || iInReplyTo) |
| 1746 | && forum_rid_is_tagged(iEdit ? iEdit : iInReplyTo, "closed", 1) ){ |
| 1747 | forumpost_error_closed(); |
| 1748 | return 0; |
| 1749 | } |
| 1750 | if( iEdit==0 && fossil_all_whitespace(zContent) ){ |
| 1751 | return 0; |
| 1752 | } |
| 1753 | if( iInReplyTo==0 && iEdit>0 ){ |
| 1754 | iBasis = iEdit; |
| 1755 | iInReplyTo = db_int(0, "SELECT firt FROM forumpost WHERE fpid=%d", iEdit); |
| @@ -1712,17 +1770,19 @@ | |
| 1770 | fossil_free(zG); |
| 1771 | } |
| 1772 | if( zTitle ){ |
| 1773 | blob_appendf(&x, "H %F\n", zTitle); |
| 1774 | } |
| 1775 | zI = rid_to_uuid(iInReplyTo); |
| 1776 | if( zI ){ |
| 1777 | blob_appendf(&x, "I %s\n", zI); |
| 1778 | fossil_free(zI); |
| 1779 | } |
| 1780 | if( zMimetype!=0 |
| 1781 | && zMimetype[0]!=0 |
| 1782 | && fossil_strcmp(zMimetype,"text/x-fossil-wiki")!=0 ){ |
| 1783 | blob_appendf(&x, "N %F\n", zMimetype); |
| 1784 | } |
| 1785 | if( iEdit>0 ){ |
| 1786 | char *zP = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", iEdit); |
| 1787 | if( zP==0 ) webpage_error("missing edit artifact %d", iEdit); |
| 1788 | blob_appendf(&x, "P %s\n", zP); |
| @@ -1750,11 +1810,11 @@ | |
| 1810 | webpage_error("malformed forum post artifact - %s", blob_str(&errMsg)); |
| 1811 | } |
| 1812 | webpage_assert( pPost->type==CFTYPE_FORUM ); |
| 1813 | manifest_destroy(pPost); |
| 1814 | |
| 1815 | if( (iFlags & FPOST_DRYRUN)!=0 ){ |
| 1816 | @ <div class='debug'> |
| 1817 | @ This is the artifact that would have been generated: |
| 1818 | @ <pre>%h(blob_str(&x))</pre> |
| 1819 | @ </div> |
| 1820 | blob_reset(&x); |
| @@ -1813,12 +1873,17 @@ | |
| 1873 | int addTag, int validFpid){ |
| 1874 | if( !cgi_csrf_safe(2) ){ |
| 1875 | webpage_error("CSRF validation failed"); |
| 1876 | }else{ |
| 1877 | const int fpid = validFpid>0 ? validFpid : forum_validate_fpid_param(); |
| 1878 | if( fpid>0 ){ |
| 1879 | if( forumpost_tag(fpid, addTag, zTag, zVal) < 0 ){ |
| 1880 | webpage_error("Tagging artifact failed: %s", g.zErrMsg); |
| 1881 | }else{ |
| 1882 | cgi_redirectf("%R/forumpost/%S",P("fpid")); |
| 1883 | } |
| 1884 | } |
| 1885 | } |
| 1886 | } |
| 1887 | |
| 1888 | /* |
| 1889 | ** WEBPAGE: forumpost_close hidden |
| @@ -1954,22 +2019,24 @@ | |
| 2019 | } |
| 2020 | } |
| 2021 | |
| 2022 | /* |
| 2023 | ** If the user has AttachForum permissions, emit a notice that |
| 2024 | ** attachments may be added after saving. If p is not NULL, also emit |
| 2025 | ** its list of attachments. We only emit this for No-JS environments, |
| 2026 | ** as in JS environments the interactive forum editor includes |
| 2027 | ** attachment support. |
| 2028 | */ |
| 2029 | static void forum_render_attachment_notice(void){ |
| 2030 | if( g.perm.AttachForum ){ |
| 2031 | @ <noscript><div>You will be able to attach files to this post |
| 2032 | @ after saving it.</div></noscript> |
| 2033 | } |
| 2034 | } |
| 2035 | |
| 2036 | /* |
| 2037 | ** WEBPAGE: forume1 hidden |
| 2038 | ** |
| 2039 | ** Start a new forum thread. |
| 2040 | */ |
| 2041 | void forumnew_page(void){ |
| 2042 | const char *zTitle = PDT("title",""); |
| @@ -1983,36 +2050,45 @@ | |
| 2050 | } |
| 2051 | if( P("submit") && cgi_csrf_safe(2) ){ |
| 2052 | if( forum_post(zTitle, 0, 0, 0, zMimetype, zContent, |
| 2053 | forum_post_flags()) ) return; |
| 2054 | } |
| 2055 | if( P("preview") && !fossil_all_whitespace(zContent) ){ |
| 2056 | @ <h1>Preview:</h1> |
| 2057 | forum_render(zTitle, zMimetype, zContent, "forumEdit", 1); |
| 2058 | } |
| 2059 | style_set_current_feature("forum"); |
| 2060 | style_header("New Forum Thread"); |
| 2061 | |
| 2062 | @ <form action="%R/forume1" method="POST" \ |
| 2063 | @ class="remove-if-replaced"> |
| 2064 | @ <h1>New Thread:</h1> |
| 2065 | forum_from_line(); |
| 2066 | forum_post_widget(zTitle, zMimetype, zContent); |
| 2067 | @ <input type="submit" name="preview" value="Preview"> |
| 2068 | if( P("preview") && !fossil_all_whitespace(zContent) ){ |
| 2069 | @ <input type="submit" name="submit" value="Submit"> |
| 2070 | }else{ |
| 2071 | @ <input type="submit" name="submit" value="Submit" disabled> |
| 2072 | } |
| 2073 | forum_render_debug_options(); |
| 2074 | login_insert_csrf_secret(); |
| 2075 | @ </form> |
| 2076 | /* When JS is disabled the block above will work. When it's |
| 2077 | enabled, the above will be removed and JS will render the editor |
| 2078 | form in the next element. */ |
| 2079 | @ <div hidden id='forumnew-placeholder'> |
| 2080 | @ <input type='hidden' name='title' value='%h(zTitle)'> |
| 2081 | login_insert_csrf_secret(); |
| 2082 | @ </div> |
| 2083 | forum_render_attachment_notice(); |
| 2084 | forum_emit_js(); |
| 2085 | style_finish_page(); |
| 2086 | } |
| 2087 | |
| 2088 | /* |
| 2089 | ** WEBPAGE: forume2 hidden |
| 2090 | ** |
| 2091 | ** Edit an existing forum message. |
| 2092 | ** Query parameters: |
| 2093 | ** |
| 2094 | ** fpid=X Hash of the post to be edited. REQUIRED |
| @@ -2097,11 +2173,11 @@ | |
| 2173 | style_set_current_feature("forum"); |
| 2174 | isDelete = P("nullout")!=0; |
| 2175 | if( P("submit") |
| 2176 | && isCsrfSafe |
| 2177 | && (zContent = PDT("content",""))!=0 |
| 2178 | && (isDelete || !fossil_all_whitespace(zContent)) |
| 2179 | ){ |
| 2180 | int done = 1; |
| 2181 | const char *zMimetype = PD("mimetype",DEFAULT_FORUM_MIMETYPE); |
| 2182 | if( bReply ){ |
| 2183 | done = forum_post(0, fpid, 0, 0, zMimetype, zContent, |
| @@ -2175,11 +2251,11 @@ | |
| 2251 | zDisplayName = display_name_from_login(pPost->zUser); |
| 2252 | @ <h3 class='forumPostHdr'>By %s(zDisplayName) on %h(zDate)</h3> |
| 2253 | fossil_free(zDisplayName); |
| 2254 | fossil_free(zDate); |
| 2255 | forum_render(0, pPost->zMimetype, pPost->zWiki, "forumEdit", 1); |
| 2256 | if( bPreview && !fossil_all_whitespace(zContent) ){ |
| 2257 | @ <h2>Preview:</h2> |
| 2258 | forum_render(0, zMimetype,zContent, "forumEdit", 1); |
| 2259 | } |
| 2260 | @ <h2>Enter Reply:</h2> |
| 2261 | @ <form action="%R/forume2" method="POST"> |
| @@ -2190,11 +2266,11 @@ | |
| 2266 | } |
| 2267 | if( !isDelete ){ |
| 2268 | @ <input type="submit" name="preview" value="Preview"> |
| 2269 | } |
| 2270 | @ <input type="submit" name="cancel" value="Cancel"> |
| 2271 | if( isDelete || (bPreview && !fossil_all_whitespace(zContent)) ){ |
| 2272 | if( !iClosed || g.perm.Admin ) { |
| 2273 | @ <input type="submit" name="submit" value="Submit"> |
| 2274 | } |
| 2275 | } |
| 2276 | forum_render_debug_options(); |
| @@ -2201,11 +2277,13 @@ | |
| 2277 | login_insert_csrf_secret(); |
| 2278 | @ </form> |
| 2279 | if( !bReply ){ |
| 2280 | forum_render_attachment_list(rid_to_uuid(fpid)); |
| 2281 | } |
| 2282 | if( !isDelete ){ |
| 2283 | forum_render_attachment_notice(); |
| 2284 | } |
| 2285 | forum_emit_js(); |
| 2286 | style_finish_page(); |
| 2287 | } |
| 2288 | |
| 2289 | /* |
| @@ -2214,21 +2292,21 @@ | |
| 2292 | ** to closed posts. If false, only administrators may do so. Note that |
| 2293 | ** this only affects the forum web UI, not post-closing tags which |
| 2294 | ** arrive via the command-line or from synchronization with a remote. |
| 2295 | ** This policy also determines whether moderators may delete forum |
| 2296 | ** attachments. |
| 2297 | ** |
| 2298 | ** SETTING: forum-title width=20 default=Forum |
| 2299 | ** This is the name or "title" of the Forum for this repository. The |
| 2300 | ** default is just "Forum". But in some setups, admins might want to |
| 2301 | ** change it to "Developer Forum" or "User Forum" or whatever other name |
| 2302 | ** seems more appropriate for the particular usage. |
| 2303 | ** |
| 2304 | ** SETTING: attachment-size-limit width=16 |
| 2305 | ** The maximum number of bytes for an attachment to a wiki page, |
| 2306 | ** ticket, tech note, or forum post. The default (or 0) is unlimited |
| 2307 | ** but a limit may be imposed by the web server or a proxy. |
| 2308 | ** |
| 2309 | ** SETTING: forum-statuses width=40 block-text |
| 2310 | ** This JSON5-formatted value defines an array of objects describing |
| 2311 | ** the available statuses of forum posts. Each entry of the array must |
| 2312 | ** be an object in the form {label:"X",value:"Y"}. |
| @@ -2723,5 +2801,388 @@ | |
| 2801 | ** URL arg when the status selection list is activated. */ |
| 2802 | forum_emit_js(); |
| 2803 | } |
| 2804 | style_finish_page(); |
| 2805 | } |
| 2806 | |
| 2807 | /* |
| 2808 | ** The AJAX counterpart of forum_post(). |
| 2809 | ** |
| 2810 | ** Returns the new artifact's RID on success, 0 if no changes were |
| 2811 | ** necessary (e.g. an empty new post or dry-run mode), and a negative |
| 2812 | ** value on error. If it returns a negative value then it will have |
| 2813 | ** populated the ajax response state with an error object. |
| 2814 | ** |
| 2815 | ** zTitle must be NULL if iInReplyTo>0 and must be non-empty if |
| 2816 | ** iInReplyTo==0. |
| 2817 | ** |
| 2818 | ** The caller must have started a transaction and must roll it back if |
| 2819 | ** this call returns <=0, noting that only the negative-value case is |
| 2820 | ** an error. |
| 2821 | ** |
| 2822 | ** This function does some work to try to ensure that duplicate |
| 2823 | ** entries are not save (this can happen as a side effect of the forum |
| 2824 | ** post editor added in 2026-06). If the given post will not have been |
| 2825 | ** materially edited by these changes, they are not applied and the |
| 2826 | ** rid of the existing entry is used. |
| 2827 | ** |
| 2828 | ** Maintenance reminders: |
| 2829 | ** |
| 2830 | ** - iInReplyTo==0 && iEdit==0: new thread |
| 2831 | ** - iInReplyTo==0 && iEdit>0 : edit top post or response |
| 2832 | ** - iInReplyTo>0 && iEdit==0: new response |
| 2833 | ** - iInReplyTo>0 && iEdit>0 : edit response |
| 2834 | */ |
| 2835 | static int forum_post_ajax( |
| 2836 | const char *zTitle, /* Title. NULL for replies */ |
| 2837 | int iInReplyTo, /* Post replying to. 0 for new threads */ |
| 2838 | int iEdit, /* Post being edited, or zero for a new post */ |
| 2839 | const char *zUser, /* Username. NULL means use login name */ |
| 2840 | const char *zMimetype, /* Mimetype of content. */ |
| 2841 | const char *zContent, /* Content */ |
| 2842 | int iFlags /* FPOST_xyz flag values */ |
| 2843 | ){ |
| 2844 | char *zI; |
| 2845 | char *zG; |
| 2846 | char *zP = 0; |
| 2847 | int iBasis; |
| 2848 | Blob x = BLOB_INITIALIZER, |
| 2849 | cksum = BLOB_INITIALIZER, |
| 2850 | formatCheck = BLOB_INITIALIZER, |
| 2851 | errMsg = BLOB_INITIALIZER; |
| 2852 | Manifest *pPost = 0; |
| 2853 | int nContent = zContent ? (int)strlen(zContent) : 0; |
| 2854 | int rc = 0; |
| 2855 | |
| 2856 | assert( db_transaction_nesting_depth()>0 ); |
| 2857 | schema_forum(); |
| 2858 | if( iEdit==0 && fossil_all_whitespace(zContent) ){ |
| 2859 | return 0; |
| 2860 | } |
| 2861 | if( !g.perm.Admin && (iEdit || iInReplyTo) |
| 2862 | && forum_rid_is_tagged(iEdit ? iEdit : iInReplyTo, "closed", 1) ){ |
| 2863 | return -ajax_route_error(400, "Thread is closed."); |
| 2864 | } |
| 2865 | if( 0==iInReplyTo && fossil_all_whitespace(zTitle) ){ |
| 2866 | return -ajax_route_error(400, "Empty title is not permitted."); |
| 2867 | } |
| 2868 | |
| 2869 | if( zUser==0 ){ |
| 2870 | if( login_is_nobody() ){ |
| 2871 | zUser = "anonymous"; |
| 2872 | }else{ |
| 2873 | zUser = login_name(); |
| 2874 | } |
| 2875 | } |
| 2876 | if( iEdit>0 |
| 2877 | && !g.perm.Admin |
| 2878 | && !forumpost_is_owner(iEdit, zUser) ){ |
| 2879 | return -ajax_route_error( |
| 2880 | 403, "Only admins may edit other peoples' posts." |
| 2881 | ); |
| 2882 | } |
| 2883 | if( iInReplyTo==0 && iEdit>0 ){ |
| 2884 | iBasis = iEdit; |
| 2885 | iInReplyTo = db_int(0, "SELECT firt FROM forumpost WHERE fpid=%d", |
| 2886 | iEdit); |
| 2887 | }else{ |
| 2888 | iBasis = iInReplyTo; |
| 2889 | /* TODO (2026-06-008) If (iInReplyTo>0 && iEdit>0), validate that |
| 2890 | ** iInReplyTo is connected to iEdit properly, else we risk |
| 2891 | ** reparenting the new edit and having unrepredictable downstream |
| 2892 | ** side effects. */ |
| 2893 | } |
| 2894 | |
| 2895 | if( 0!=zMimetype && 0==zMimetype[0] ){ |
| 2896 | zMimetype = 0; |
| 2897 | } |
| 2898 | |
| 2899 | if( 0!=zTitle && 0==zTitle[0] ) zTitle = 0; |
| 2900 | webpage_assert( (zTitle==0)+(iInReplyTo==0)==1 ); |
| 2901 | |
| 2902 | if( iEdit>0 ){ |
| 2903 | int cmp; |
| 2904 | pPost = manifest_get(iEdit, CFTYPE_FORUM, 0); |
| 2905 | if( pPost==0 ){ |
| 2906 | rc = -ajax_route_error(404, "Missing edit artifact %d", iEdit); |
| 2907 | goto post_ajax_end; |
| 2908 | } |
| 2909 | /* |
| 2910 | ** If the old content matches the new then do not save a new copy. |
| 2911 | ** It's easy to get re-posts of unedited content via the forum |
| 2912 | ** editor, especially since the one added in 2026-06, where a |
| 2913 | ** post's status and attachments may be amended from the editor |
| 2914 | ** without modifying any of the post's content. In the legacy |
| 2915 | ** editor such "out-of-band" changes weren't possible and users |
| 2916 | ** have never made a practice of re-posting unedited content. |
| 2917 | ** |
| 2918 | ** We compare the following fields to the original: user, mimetype, |
| 2919 | ** content, and (for root posts only) the title. |
| 2920 | */ |
| 2921 | cmp = (0==pPost->zInReplyTo) |
| 2922 | ? fossil_strcmp(pPost->zThreadTitle, zTitle) |
| 2923 | : 0; |
| 2924 | if( 0==cmp ){ |
| 2925 | cmp=fossil_strcmp(pPost->zWiki, zContent); |
| 2926 | if( 0==cmp ){ |
| 2927 | cmp = fossil_strcmp(pPost->zUser, zUser); |
| 2928 | } |
| 2929 | if( 0==cmp |
| 2930 | && 0!=(cmp=fossil_strcmp(pPost->zMimetype, zMimetype)) ){ |
| 2931 | /* Extra mimetype checks for a common condition seen elsewhere */ |
| 2932 | if( (0==zMimetype |
| 2933 | && 0==fossil_strcmp(pPost->zMimetype, "text/x-fossil-wiki")) |
| 2934 | || (0==pPost->zMimetype |
| 2935 | && 0==fossil_strcmp(zMimetype, "text/x-fossil-wiki")) ){ |
| 2936 | cmp = 0; |
| 2937 | } |
| 2938 | } |
| 2939 | if( 0==cmp ){ |
| 2940 | rc = iEdit; |
| 2941 | goto post_ajax_end; |
| 2942 | } |
| 2943 | } |
| 2944 | zP = rid_to_uuid(iEdit); |
| 2945 | } |
| 2946 | |
| 2947 | /* Write the new artifact */ |
| 2948 | blob_init(&x, 0, 0); |
| 2949 | blob_appendf(&x, "D %z\n", date_in_standard_format("now")); |
| 2950 | zG = db_text( |
| 2951 | 0, |
| 2952 | "SELECT uuid FROM blob, forumpost" |
| 2953 | " WHERE blob.rid==forumpost.froot" |
| 2954 | " AND forumpost.fpid=%d", |
| 2955 | iBasis |
| 2956 | ); |
| 2957 | if( zG ){ |
| 2958 | blob_appendf(&x, "G %z\n", zG); |
| 2959 | } |
| 2960 | if( zTitle ){ |
| 2961 | blob_appendf(&x, "H %F\n", zTitle); |
| 2962 | } |
| 2963 | if( iInReplyTo>0 ){ |
| 2964 | zI = rid_to_uuid(iInReplyTo); |
| 2965 | if( 0==zI ){ |
| 2966 | rc = -ajax_route_error(404, "Missing in-reply-to artifact %d", |
| 2967 | iInReplyTo); |
| 2968 | goto post_ajax_end; |
| 2969 | } |
| 2970 | blob_appendf(&x, "I %z\n", zI); |
| 2971 | } |
| 2972 | if( zMimetype!=0 |
| 2973 | && fossil_strcmp(zMimetype,"text/x-fossil-wiki")!=0 ){ |
| 2974 | blob_appendf(&x, "N %F\n", zMimetype); |
| 2975 | } |
| 2976 | if( zP ){ |
| 2977 | blob_appendf(&x, "P %s\n", zP); |
| 2978 | } |
| 2979 | |
| 2980 | blob_appendf(&x, "U %F\n", zUser); |
| 2981 | blob_appendf(&x, "W %d\n%s\n", nContent, zContent); |
| 2982 | md5sum_blob(&x, &cksum); |
| 2983 | blob_appendf(&x, "Z %b\n", &cksum); |
| 2984 | blob_reset(&cksum); |
| 2985 | |
| 2986 | /* Verify that the artifact we are creating is well-formed */ |
| 2987 | blob_init(&formatCheck, 0, 0); |
| 2988 | blob_init(&errMsg, 0, 0); |
| 2989 | blob_copy(&formatCheck, &x); |
| 2990 | pPost = manifest_parse(&formatCheck, 0, &errMsg); |
| 2991 | if( pPost==0 ){ |
| 2992 | ajax_route_error(500, "Malformed forum post artifact: %b", &errMsg); |
| 2993 | rc = -500; |
| 2994 | goto post_ajax_end; |
| 2995 | } |
| 2996 | webpage_assert( pPost->type==CFTYPE_FORUM ); |
| 2997 | |
| 2998 | if( (iFlags & FPOST_DRYRUN)!=0 ){ |
| 2999 | rc = 0; |
| 3000 | }else{ |
| 3001 | int nrid; |
| 3002 | db_begin_transaction(); |
| 3003 | nrid = wiki_put(&x, iEdit>0 ? iEdit : 0, forum_need_moderation()); |
| 3004 | blob_reset(&x); |
| 3005 | if( (iFlags & FPOST_NO_ALERT)!=0 ){ |
| 3006 | alert_unqueue('f', nrid); |
| 3007 | } |
| 3008 | rc = nrid; |
| 3009 | db_end_transaction(0); |
| 3010 | } |
| 3011 | post_ajax_end: |
| 3012 | manifest_destroy(pPost); |
| 3013 | fossil_free(zP); |
| 3014 | blob_reset(&x); |
| 3015 | blob_reset(&cksum); |
| 3016 | blob_reset(&formatCheck); |
| 3017 | return rc; |
| 3018 | } |
| 3019 | /* |
| 3020 | ** WEBPAGE: forumajax_save hidden |
| 3021 | ** |
| 3022 | ** WIP |
| 3023 | ** |
| 3024 | ** Response JSON: |
| 3025 | ** |
| 3026 | ** { uuid: hash, ...tbd } |
| 3027 | */ |
| 3028 | void forum_ajax_save_page(void){ |
| 3029 | const char *zFpid; |
| 3030 | const char *zTitle; |
| 3031 | const char *zIrt; |
| 3032 | const char *zMimetype; |
| 3033 | const char *zContent; |
| 3034 | const char *zStatus; |
| 3035 | const int bHasAttachment = P("file1")!=0; |
| 3036 | Manifest *pPost = 0; |
| 3037 | char *zNewUuid = 0; |
| 3038 | int firt = 0; /* In-reply-to rid or 0 */ |
| 3039 | int fpid = 0; /* Post rid being edited or 0 */ |
| 3040 | int rc = 0; /* Result code. */ |
| 3041 | int nrid = 0; /* New artifact rid. */ |
| 3042 | int iPostFlags; /* forum_post_flags() (after perms check) */ |
| 3043 | int bRollback; /* True = roll back. */ |
| 3044 | int nAttach = 0; /* Number of attachments added */ |
| 3045 | int bStatusSet = 0; /* True if status tag set. */ |
| 3046 | |
| 3047 | if( !ajax_route_bootstrap(0, 1) ){ |
| 3048 | return; |
| 3049 | }else if( !g.perm.WrForum |
| 3050 | || (bHasAttachment && !g.perm.AttachForum) ){ |
| 3051 | ajax_route_error_forbidden(); |
| 3052 | return; |
| 3053 | }else if( !ajax_check_csrf(2) ){ |
| 3054 | ajax_route_error_csrf(); |
| 3055 | return; |
| 3056 | } |
| 3057 | |
| 3058 | iPostFlags = forum_post_flags(/*must come after permissions init*/); |
| 3059 | bRollback = (FPOST_DRYRUN & iPostFlags); |
| 3060 | zFpid = P("fpid"); |
| 3061 | zIrt = P("firt"); |
| 3062 | zMimetype = P("mimetype"); |
| 3063 | zContent = P("content"); |
| 3064 | zStatus = P("status"); |
| 3065 | db_begin_transaction(); |
| 3066 | if( zFpid && zFpid[0] ){ |
| 3067 | fpid = symbolic_name_to_rid(zFpid, "f"); |
| 3068 | if( fpid<0 ){ |
| 3069 | rc = -ajax_route_error(400, "Ambiguous forum ID."); |
| 3070 | goto ajax_save_end; |
| 3071 | }else if( 0==fpid |
| 3072 | || 0==(pPost = manifest_get(fpid, CFTYPE_FORUM, 0)) ){ |
| 3073 | rc = -ajax_route_error(404, "Cannot resolve forum post ID."); |
| 3074 | goto ajax_save_end; |
| 3075 | } |
| 3076 | } |
| 3077 | /* |
| 3078 | ** Problem: if we derive firt from fpid/pPost then there's a race |
| 3079 | ** condition where the IRT post is edited between the time that this |
| 3080 | ** edit was initiated and when it is posted: the new edit's IRT will |
| 3081 | ** point to the edit which was made in the meantime, not the one the |
| 3082 | ** user intended to respond to. However, if we accept firt from the |
| 3083 | ** enviornment, we "really should" validate that it's actually in |
| 3084 | ** the current chain, to prohibit that malicious posts could move |
| 3085 | ** posts around. |
| 3086 | ** |
| 3087 | ** forum_post_ajax() will, if fpid>0 && !firt, select fpid's current |
| 3088 | ** firt. |
| 3089 | */ |
| 3090 | if( zIrt && zIrt[0] ){ |
| 3091 | firt = symbolic_name_to_rid(zIrt, "f"); |
| 3092 | if( firt<0 ){ |
| 3093 | rc = -ajax_route_error(400, "Ambiguous in-reply-do ID."); |
| 3094 | goto ajax_save_end; |
| 3095 | }else if( 0==firt ){ |
| 3096 | rc = -ajax_route_error(404, "Cannot resolve in-reply-do ID."); |
| 3097 | goto ajax_save_end; |
| 3098 | } |
| 3099 | } |
| 3100 | |
| 3101 | if( 0 ){ |
| 3102 | rc = -ajax_route_error(400, "Save is TODO. " |
| 3103 | "iPostFlags=%d debug=%d", |
| 3104 | iPostFlags, g.perm.Debug); |
| 3105 | goto ajax_save_end; |
| 3106 | } |
| 3107 | |
| 3108 | zTitle = firt ? 0 : P("title"); |
| 3109 | nrid = forum_post_ajax(zTitle, firt, fpid, 0, zMimetype, |
| 3110 | zContent, iPostFlags); |
| 3111 | if( nrid<0 ){ |
| 3112 | rc = nrid; |
| 3113 | goto ajax_save_end; |
| 3114 | }else if( nrid==0 ){ |
| 3115 | if( 0==(FPOST_DRYRUN & iPostFlags) ){ |
| 3116 | bRollback = 1; |
| 3117 | CX("{\"message\": \"No saving needed.\"}\n"); |
| 3118 | }else{ |
| 3119 | CX("{\"message\": \"Rolled back for dry-run.\"," |
| 3120 | "\"iPostFlags\":%d}\n", iPostFlags); |
| 3121 | } |
| 3122 | goto ajax_save_end; |
| 3123 | }else{ |
| 3124 | const int bNeedsModeration = forum_need_moderation(); |
| 3125 | const int fpHead = forumpost_head_rid(nrid); |
| 3126 | assert( nrid>0 ); |
| 3127 | assert( fpHead>0 ); |
| 3128 | zNewUuid = rid_to_uuid(nrid); |
| 3129 | if( 0!=P("file1") ){ |
| 3130 | /* Attachments */ |
| 3131 | if( !g.perm.Admin && !g.perm.AttachForum ){ |
| 3132 | rc = -ajax_route_error(403, "No permission no attach files."); |
| 3133 | goto ajax_save_end; |
| 3134 | }else{ |
| 3135 | char *zRoot = (nrid==fpHead) ? 0 : rid_to_uuid(fpHead); |
| 3136 | nAttach = attachments_ajax_from_POST(zRoot ? zRoot : zNewUuid, |
| 3137 | bNeedsModeration); |
| 3138 | fossil_free(zRoot); |
| 3139 | if( nAttach<0 ){ |
| 3140 | rc = nAttach; |
| 3141 | goto ajax_save_end; |
| 3142 | } |
| 3143 | if( nAttach>0 |
| 3144 | && (iPostFlags & FPOST_NO_ALERT)!=0 |
| 3145 | && db_table_exists("repository","pending_alert") ){ |
| 3146 | /* Unqueue any alerts for these attachments. Recall that |
| 3147 | ** they're attached to the first version of the post, which |
| 3148 | ** means we actually risk cancelling _other_ pending |
| 3149 | ** notifications for attachments on this same post. C'est la |
| 3150 | ** vie.*/ |
| 3151 | db_multi_exec( |
| 3152 | "WITH x(id) AS (\n" |
| 3153 | " SELECT 'f%d'\n" |
| 3154 | " UNION ALL\n" |
| 3155 | " SELECT 'f'||a.attachid FROM blob b, attachment a\n" |
| 3156 | " WHERE b.rid=%d\n" |
| 3157 | " AND b.uuid=a.target\n" |
| 3158 | ") DELETE FROM pending_alert WHERE eventid IN x", |
| 3159 | fpHead, fpHead |
| 3160 | ); |
| 3161 | } |
| 3162 | } |
| 3163 | } |
| 3164 | if( 0==bNeedsModeration |
| 3165 | /* ^^^ Do not allow a status tag on a pending-moderation post |
| 3166 | ** because it will introduce a reference to an artifact which |
| 3167 | ** will become a phantom if it is rejected by a moderator. */ |
| 3168 | && zStatus!=0 && zStatus[0]!=0 |
| 3169 | && forum_may_set_status(nrid) |
| 3170 | && (bStatusSet=forumpost_tag(nrid, 1, "status", zStatus))<0 ){ |
| 3171 | rc = -ajax_route_error(500, "Tagging failed: %s", g.zErrMsg); |
| 3172 | goto ajax_save_end; |
| 3173 | } |
| 3174 | } |
| 3175 | |
| 3176 | assert( 0==rc ); |
| 3177 | assert( zNewUuid ); |
| 3178 | CX("{\"uuid\": %!j, \"attachedCount\": %d, " |
| 3179 | "\"statusModified\": %d, " |
| 3180 | "\"dryrun\": %s, \"iPostFlags\":%d}\n", |
| 3181 | zNewUuid, nAttach, |
| 3182 | bStatusSet, bRollback ? "true" : "false", iPostFlags); |
| 3183 | |
| 3184 | ajax_save_end: |
| 3185 | manifest_destroy(pPost); |
| 3186 | fossil_free(zNewUuid); |
| 3187 | db_end_transaction(rc || bRollback); |
| 3188 | } |
| 3189 | |
| 3190 | DDED src/fossil.attach.js |
+663
| --- a/src/fossil.attach.js | ||
| +++ b/src/fossil.attach.js | ||
| @@ -0,0 +1,663 @@ | ||
| 1 | +"use strict"; | |
| 2 | +/** | |
| 3 | + Utility for interactive file attachment. Supports attachment | |
| 4 | + selection from a file dialog, from the clipboard, or drag/drop. | |
| 5 | + | |
| 6 | + Requires that window.fossil has already been set up. | |
| 7 | + Depends on fossil.dom. | |
| 8 | +*/ | |
| 9 | +(function(namespace){ | |
| 10 | + "use strict"; | |
| 11 | + const F = namespace, D = F.dom; | |
| 12 | + | |
| 13 | + let idCounter = 0; | |
| 14 | + /** | |
| 15 | + Implements a multi-file selector widget. Intended to be plugged | |
| 16 | + in to places in Fossil's UI where attachments can be assigned to | |
| 17 | + an artifact. | |
| 18 | + */ | |
| 19 | + class Attacher { | |
| 20 | + /* Options. */ | |
| 21 | + #opt; | |
| 22 | + /* List of objects representing each row. */ | |
| 23 | + #rows = []; | |
| 24 | + /* DOM elements */ | |
| 25 | + #e = Object.create(null); | |
| 26 | + /* Proxy for various events this object fires. */ | |
| 27 | + #events = new EventTarget(); | |
| 28 | + | |
| 29 | + /** | |
| 30 | + Options: | |
| 31 | + | |
| 32 | + opt.container: Optional DOM element to append the resulting | |
| 33 | + widget to. If not set, the client can get access to the widget | |
| 34 | + element using this.body. | |
| 35 | + | |
| 36 | + opt.addButtonLabel: optional label for the "add attachment" | |
| 37 | + button, defaulting to something generic. | |
| 38 | + | |
| 39 | + opt.limit: optional max number of attachments to allow. This | |
| 40 | + defaults to "some sensible value". | |
| 41 | + | |
| 42 | + opt.startWith[=0]: if >0 then that many file selection widgets | |
| 43 | + are automatically activated, as if the user had tapped the Add | |
| 44 | + button that many times. | |
| 45 | + | |
| 46 | + opt.description[=true]: if true then show the file description | |
| 47 | + field, otherwise elide it. | |
| 48 | + | |
| 49 | + opt.reverse[=false]: reverses the flow of the widget such that | |
| 50 | + the Add button stays on the top and rows are ordered | |
| 51 | + most-recently-added. | |
| 52 | + | |
| 53 | + opt.controls = [array of DOM elements]. Optional DOM elements | |
| 54 | + to inject into the UI element which wraps the "Add" button. | |
| 55 | + See this.controlsElement. | |
| 56 | + | |
| 57 | + opt.listener = function or object: {add: func, remove: func, | |
| 58 | + populate: func}: if these are functions they are registered as | |
| 59 | + listeners for 'entry-added', 'entry-removed', and/or | |
| 60 | + 'entry-populated' events, described below. opt.listener.all, if | |
| 61 | + set, is used as a fallback for any of 'add', 'remove', or | |
| 62 | + 'populate' which are not set. If opt.listener is a function | |
| 63 | + then it behaves as if listener={all: thatFunction}. | |
| 64 | + | |
| 65 | + Events: | |
| 66 | + | |
| 67 | + This class fires CustomEvents for certain changes: | |
| 68 | + | |
| 69 | + 'entry-added' and 'entry-removed' trigger when an attachment | |
| 70 | + entry row is added/removed. Its event.detail is: | |
| 71 | + | |
| 72 | + {attacher: this, row: object, type: 'same as event type'}. | |
| 73 | + | |
| 74 | + 'entry-populated' is triggered when a visible entry gets | |
| 75 | + content attached to it, with the same detail structure as | |
| 76 | + described above. | |
| 77 | + | |
| 78 | + The public structure of the row object passed to each is | |
| 79 | + currently TBD. | |
| 80 | + */ | |
| 81 | + constructor(opt){ | |
| 82 | + this.#opt = opt = F.nu({ | |
| 83 | + addButtonLabel: false, | |
| 84 | + startWith: 0, | |
| 85 | + limit: 0, | |
| 86 | + dryRun: undefined, | |
| 87 | + description: true, | |
| 88 | + reverse: false | |
| 89 | + }, opt); | |
| 90 | + this.#e.body = D.addClass(D.div(), 'Attacher'); | |
| 91 | + if( opt.reverse ) this.#e.body.classList.add('reverse'); | |
| 92 | + const eBtnAdd = this.#e.btnAdd = D.addClass( | |
| 93 | + D.button(this.#opt.addButtonLabel || 'Add attachment', | |
| 94 | + ()=>this.#addRow()), | |
| 95 | + 'attach-add-button' | |
| 96 | + ); | |
| 97 | + eBtnAdd.type = 'button'; | |
| 98 | + opt.ownsAddButton = true; | |
| 99 | + this.#e.err = D.addClass(D.div(), 'error', 'hidden'); | |
| 100 | + this.#e.body.append(this.#e.err); | |
| 101 | + this.#e.err.addEventListener('dblclick',()=>this.reportError()); | |
| 102 | + | |
| 103 | + const eControls = this.#e.controls = | |
| 104 | + D.addClass(D.div(), 'attach-controls'); | |
| 105 | + eControls.append(eBtnAdd); | |
| 106 | + if( opt.container ){ | |
| 107 | + opt.container.appendChild(this.#e.body); | |
| 108 | + } | |
| 109 | + this.#e.body.appendChild(eControls); | |
| 110 | + if( opt.listener ){ | |
| 111 | + const doCb = (eventType, key)=>{ | |
| 112 | + const f = (opt.listener instanceof Function) | |
| 113 | + ? opt.listener | |
| 114 | + : (opt.listener[key] || opt.listener.all); | |
| 115 | + if( f instanceof Function ){ | |
| 116 | + this.addEventListener(eventType, f); | |
| 117 | + } | |
| 118 | + }; | |
| 119 | + doCb('entry-added', 'add'); | |
| 120 | + doCb('entry-removed', 'remove'); | |
| 121 | + doCb('entry-populated', 'populate'); | |
| 122 | + } | |
| 123 | + if( opt.dryRun ){ | |
| 124 | + /* Add dry-run toggle for testing. */ | |
| 125 | + const eLbl = D.label(false, "Dry-run?"); | |
| 126 | + const eCb = D.checkbox(true); | |
| 127 | + eLbl.append(eCb); | |
| 128 | + eControls.append(eLbl); | |
| 129 | + eCb.checked = opt.dryRun = true; | |
| 130 | + eCb.addEventListener('change',()=>opt.dryRun=eCb.checked); | |
| 131 | + } | |
| 132 | + if( Array.isArray(opt.controls) ){ | |
| 133 | + eControls.append(...opt.controls); | |
| 134 | + } | |
| 135 | + if( opt.startWith > 0 ){ | |
| 136 | + for(let i = 0; i < opt.startWith; ++i ){ | |
| 137 | + this.#addRow(); | |
| 138 | + } | |
| 139 | + }else{ | |
| 140 | + this.#updateControls(); | |
| 141 | + } | |
| 142 | + } | |
| 143 | + | |
| 144 | + | |
| 145 | + get widget(){ | |
| 146 | + return this.#e.body; | |
| 147 | + } | |
| 148 | + | |
| 149 | + addEventListener(...args){ | |
| 150 | + return this.#events.addEventListener(...args); | |
| 151 | + } | |
| 152 | + | |
| 153 | + removeEventListener(...args){ | |
| 154 | + return this.#events.removeEventListener(...args); | |
| 155 | + } | |
| 156 | + | |
| 157 | + /** Returns true if any visible input widgets have content | |
| 158 | + selected. */ | |
| 159 | + get isPopulated(){ | |
| 160 | + for(let r of this.#rows){ | |
| 161 | + if( r.file ) return true; | |
| 162 | + } | |
| 163 | + return false; | |
| 164 | + } | |
| 165 | + | |
| 166 | + get isDryRun(){ | |
| 167 | + return !!this.#opt.dryRun; | |
| 168 | + } | |
| 169 | + /** | |
| 170 | + Returns the DOM element (div.attach-controls) which wraps the | |
| 171 | + "Add" button. Clients may add buttons to it. | |
| 172 | + */ | |
| 173 | + get controlsElement(){ | |
| 174 | + return this.#e.controls; | |
| 175 | + } | |
| 176 | + | |
| 177 | + /** | |
| 178 | + Reports an error by appending each argument to the error widget | |
| 179 | + and unhiding it. If passed no arugments, it clears and hides | |
| 180 | + the error widget. | |
| 181 | + */ | |
| 182 | + reportError(...msg){ | |
| 183 | + const e = this.#e.err; | |
| 184 | + D.clearElement(e); | |
| 185 | + if( msg.length ){ | |
| 186 | + e.classList.remove('hidden'); | |
| 187 | + e.append(...msg); | |
| 188 | + }else{ | |
| 189 | + e.classList.add('hidden'); | |
| 190 | + } | |
| 191 | + } | |
| 192 | + | |
| 193 | + #removeRow(rowObj){ | |
| 194 | + const er = rowObj.e.row; | |
| 195 | + if( er.parentNode ){ | |
| 196 | + this.#rows = this.#rows.filter(v=>v!==rowObj); | |
| 197 | + this.#updateControls(); | |
| 198 | + er.classList.add('animate-exit'); | |
| 199 | + er.addEventListener('animationend', ()=>er.remove(), {once: true}); | |
| 200 | + this.#events.dispatchEvent( | |
| 201 | + new CustomEvent('entry-removed',{ | |
| 202 | + detail: F.nu({ | |
| 203 | + type: 'entry-removed', | |
| 204 | + row: rowObj, | |
| 205 | + attacher: this | |
| 206 | + }) | |
| 207 | + }) | |
| 208 | + ); | |
| 209 | + } | |
| 210 | + } | |
| 211 | + | |
| 212 | + /** | |
| 213 | + Removes all attachments and clears the error state. | |
| 214 | + */ | |
| 215 | + clear(){ | |
| 216 | + for(const r of [...this.#rows/*clone because this updates #rows*/]){ | |
| 217 | + this.#removeRow(r); | |
| 218 | + } | |
| 219 | + this.reportError(); | |
| 220 | + } | |
| 221 | + | |
| 222 | + /** | |
| 223 | + Hides or shows the Add button, as appropriate. | |
| 224 | + */ | |
| 225 | + #updateControls(){ | |
| 226 | + const b = this.#e.btnAdd; | |
| 227 | + if( this.#opt.limit>0 && this.#rows.length >= this.#opt.limit ){ | |
| 228 | + b.classList.add('hidden'); | |
| 229 | + D.disable(b); | |
| 230 | + //F.toast.warning("Attachment form limit reached."); | |
| 231 | + }else{ | |
| 232 | + b.classList.remove('hidden'); | |
| 233 | + D.enable(b); | |
| 234 | + if( this.#opt.ownsAddButton ){ | |
| 235 | + this.#e.body.append(this.#e.controls/*move to the end*/); | |
| 236 | + } | |
| 237 | + } | |
| 238 | + } | |
| 239 | + | |
| 240 | + /** | |
| 241 | + Returns the "Add" button widget, Passing control of it to the | |
| 242 | + caller so that they can place it in another location. This | |
| 243 | + object will still manage its enabled/disabled/hidden state but | |
| 244 | + will no longer move it when adding a row. | |
| 245 | + */ | |
| 246 | + takeAddButton(){ | |
| 247 | + if( this.#opt.ownsAddButton ){ | |
| 248 | + this.#opt.ownsAddButton; | |
| 249 | + } | |
| 250 | + return this.#e.btnAdd; | |
| 251 | + } | |
| 252 | + /** | |
| 253 | + Sets rowObj.e.err up with an error message, or clears it if | |
| 254 | + passed only 1 argument. | |
| 255 | + */ | |
| 256 | + #rowError(rowObj,...msg){ | |
| 257 | + let e = rowObj.e.err; | |
| 258 | + if( e ){ | |
| 259 | + D.clearElement(e); | |
| 260 | + }else{ | |
| 261 | + if( !msg.length ) return; | |
| 262 | + e = rowObj.e.err = D.addClass(D.span(), 'error'); | |
| 263 | + rowObj.e.info.append(e); | |
| 264 | + } | |
| 265 | + if( msg.length ){ | |
| 266 | + e.append(...msg); | |
| 267 | + e.classList.remove('hidden'); | |
| 268 | + }else{ | |
| 269 | + e.classList.add('hidden'); | |
| 270 | + } | |
| 271 | + } | |
| 272 | + | |
| 273 | + #addRow(){ | |
| 274 | + const id = ++idCounter; | |
| 275 | + const rowObj = F.nu({ | |
| 276 | + id, file: null, mimeType: '' | |
| 277 | + }); | |
| 278 | + const eRow = D.addClass(D.div(), 'attach-row'); | |
| 279 | + const eDropzone = D.addClass(D.div(), 'attach-dropzone'); | |
| 280 | + const eFile = D.addClass( | |
| 281 | + D.input('file'), 'attach-file-input', 'hidden' | |
| 282 | + ); | |
| 283 | + const eInfo = D.addClass(D.span(), 'attach-row-info'); | |
| 284 | + const eFilename = D.append( | |
| 285 | + D.addClass(D.span(), 'attach-filename'), | |
| 286 | + "Select/drop file or click the outer border and tap your "+ | |
| 287 | + "platform's conventional <paste> keyboard shortcut." | |
| 288 | + ); | |
| 289 | + const eSize = D.addClass(D.span(), 'attach-size'); | |
| 290 | + eInfo.append(eFilename, eSize); | |
| 291 | + const eDesc = this.#opt.description | |
| 292 | + ? D.addClass( | |
| 293 | + D.attr(D.textarea(), 'placeholder', | |
| 294 | + 'Optional description...'), | |
| 295 | + 'attach-desc' | |
| 296 | + ) | |
| 297 | + : undefined; | |
| 298 | + const eRemove = D.addClass( | |
| 299 | + D.button('X', (ev)=>{ | |
| 300 | + ev.stopPropagation(); | |
| 301 | + this.#removeRow(rowObj); | |
| 302 | + }), | |
| 303 | + 'attach-row-remove' | |
| 304 | + ); | |
| 305 | + eRemove.setAttribute('title', 'Remove this attachment.'); | |
| 306 | + eRemove.type = 'button'; | |
| 307 | + | |
| 308 | + D.append(eDropzone, eInfo, eFile, eRemove); | |
| 309 | + eDropzone.addEventListener('click', ()=>eFile.click()); | |
| 310 | + eFile.addEventListener('change', (ev)=>{ | |
| 311 | + if( ev.target.files.length ){ | |
| 312 | + this.#injestBlob(rowObj, ev.target.files[0]); | |
| 313 | + } | |
| 314 | + }); | |
| 315 | + | |
| 316 | + eDropzone.addEventListener('dragover', (ev)=>{ | |
| 317 | + ev.preventDefault(); | |
| 318 | + eDropzone.classList.add('dragover'); | |
| 319 | + }); | |
| 320 | + eDropzone.addEventListener('dragleave', (ev)=>{ | |
| 321 | + eDropzone.classList.remove('dragover'); | |
| 322 | + }); | |
| 323 | + const handleDrop = (ev, theRealRowObj)=>{ | |
| 324 | + ev.preventDefault(); | |
| 325 | + eDropzone.classList.remove('dragover'); | |
| 326 | + if( ev.dataTransfer.files.length ){ | |
| 327 | + const r = theRealRowObj || rowObj; | |
| 328 | + this.#injestBlob(r, ev.dataTransfer.files[0]); | |
| 329 | + } | |
| 330 | + }; | |
| 331 | + /* Isn't working? eBtnAdd.addEventListener('drop', (ev)=>{ | |
| 332 | + this.#addRow(); | |
| 333 | + handleDrop(ev, this.#rows[this.#rows.length-1]); | |
| 334 | + });*/ | |
| 335 | + eDropzone.addEventListener('drop', handleDrop); | |
| 336 | + const pasteImage = (event, item)=>{ | |
| 337 | + if( item.type.indexOf('image') === 0 ) { | |
| 338 | + event.preventDefault(); | |
| 339 | + const blob = item.getAsFile(); | |
| 340 | + if( blob.name === 'image.png' ){ | |
| 341 | + /* Workaround to attempt to avoid name collisions when pasting | |
| 342 | + multiple images. We cannot, at this level, unambiguously | |
| 343 | + distinguish a ctrl-v of bitmap data vs a ctrl-v of an image | |
| 344 | + file copied via a desktop file manager. */ | |
| 345 | + rowObj.overrideName = `pasted-image-${Date.now()}.png`; | |
| 346 | + } | |
| 347 | + this.#injestBlob(rowObj, blob); | |
| 348 | + return true; | |
| 349 | + } | |
| 350 | + return false; | |
| 351 | + }; | |
| 352 | + const pasteThing = (event, thing)=>{ | |
| 353 | + if( pasteImage(event, thing) ) return true; | |
| 354 | + if( 'file' === thing.kind ){ | |
| 355 | + event.preventDefault(); | |
| 356 | + const blob = thing.getAsFile(); | |
| 357 | + if( blob ){ | |
| 358 | + this.#injestBlob(rowObj, blob); | |
| 359 | + return true; | |
| 360 | + } | |
| 361 | + } | |
| 362 | + return false; | |
| 363 | + }; | |
| 364 | + eDesc?.addEventListener?.('paste', (e) => { | |
| 365 | + e.stopPropagation(); | |
| 366 | + const items = (e.clipboardData || e.originalEvent.clipboardData)?.items; | |
| 367 | + if( !items ) return; | |
| 368 | + for( let i = 0; i < items.length; ++i ){ | |
| 369 | + const item = items[i]; | |
| 370 | + if( pasteThing(e, item) ){ | |
| 371 | + break; | |
| 372 | + } | |
| 373 | + } | |
| 374 | + }); | |
| 375 | + eRow.addEventListener('paste', (e) => { | |
| 376 | + const items = (e.clipboardData || e.originalEvent.clipboardData)?.items; | |
| 377 | + if( !items ) return; | |
| 378 | + for( let i = 0; i < items.length; ++i ){ | |
| 379 | + const item = items[i]; | |
| 380 | + if( item.type === 'text/plain' ){ | |
| 381 | + e.preventDefault(); | |
| 382 | + item.getAsString((text) => { | |
| 383 | + rowObj.overrideName = `pasted-text-${Date.now()}.txt`; | |
| 384 | + const blob = new File([text], rowObj.overrideName, | |
| 385 | + {type: 'text/plain'}); | |
| 386 | + this.#injestBlob(rowObj, blob); | |
| 387 | + }); | |
| 388 | + break; | |
| 389 | + }else if( pasteThing(e, item) ){ | |
| 390 | + break; | |
| 391 | + } | |
| 392 | + } | |
| 393 | + }); | |
| 394 | + eRow.append(eDropzone); | |
| 395 | + if( eDesc ) eRow.append(eDesc); | |
| 396 | + rowObj.e = F.nu({ | |
| 397 | + dropzone: eDropzone, | |
| 398 | + info: eInfo, | |
| 399 | + filename: eFilename, | |
| 400 | + size: eSize, | |
| 401 | + desc: eDesc, | |
| 402 | + row: eRow, | |
| 403 | + remove: eRemove | |
| 404 | + }); | |
| 405 | + this.#e.body.append(eRow); | |
| 406 | + eRow.classList.add('animate-entrance'); | |
| 407 | + requestAnimationFrame(() => { | |
| 408 | + eRow.scrollIntoView({ | |
| 409 | + behavior: 'smooth', | |
| 410 | + block: 'nearest', | |
| 411 | + inline: 'nearest' | |
| 412 | + }); | |
| 413 | + }); | |
| 414 | + | |
| 415 | + this.#rows.push( rowObj ); | |
| 416 | + this.#updateControls(); | |
| 417 | + this.#events.dispatchEvent( | |
| 418 | + new CustomEvent('entry-added',{ | |
| 419 | + detail: F.nu({ | |
| 420 | + type: 'entry-added', | |
| 421 | + row: rowObj, | |
| 422 | + attacher: this | |
| 423 | + }) | |
| 424 | + }) | |
| 425 | + ); | |
| 426 | + if( 0 ){ | |
| 427 | + /* To allow immediate ctrl-v, we need a trick... | |
| 428 | + But don't do this because it will interfere with, e.g., | |
| 429 | + the forum editor. */ | |
| 430 | + D.attr(eRow, 'tabindex', '-1'); | |
| 431 | + eRow.focus(); | |
| 432 | + } | |
| 433 | + } | |
| 434 | + | |
| 435 | + #rowMatchingName(name){ | |
| 436 | + for(let r of this.#rows){ | |
| 437 | + if( r.file?.name===name ) return r; | |
| 438 | + } | |
| 439 | + } | |
| 440 | + | |
| 441 | + /** | |
| 442 | + Injects the given File object as the attached content for the | |
| 443 | + given row. If the object's name collides with another row, | |
| 444 | + rowObj is removed from this widget and the old row is instead | |
| 445 | + re-populated with the new file. | |
| 446 | + | |
| 447 | + If rowObj.overrideName is set then the given file gets wrapped | |
| 448 | + with that name before attaching it, and that property is | |
| 449 | + removed from rowObj. This is intended only for communicating | |
| 450 | + auto-generated names for pasted data. | |
| 451 | + */ | |
| 452 | + #injestBlob(rowObj, file){ | |
| 453 | + if( !file ) return; | |
| 454 | + const old = this.#rowMatchingName(file.name); | |
| 455 | + if( rowObj.overrideName ){ | |
| 456 | + if( rowObj.overrideName !== file.name ){ | |
| 457 | + file = new File([file], rowObj.overrideName, {type: file.type}); | |
| 458 | + } | |
| 459 | + rowObj.overrideName = undefined; | |
| 460 | + } | |
| 461 | + if( old && rowObj !== old ){ | |
| 462 | + /* | |
| 463 | + Fossil attachments treat the name as a unique-per-target | |
| 464 | + key, with the newest one being the primary. If a name is | |
| 465 | + given twice, remove the new entry and reuse the older | |
| 466 | + one. There are conceivable, but also unlikely, cases where | |
| 467 | + this will have unintended side-effects, e.g. attaching both | |
| 468 | + /foo/bar and /baz/bar, but that seems like a lesser evil | |
| 469 | + than attaching the same file N times, leading to N | |
| 470 | + attachment artifacts. | |
| 471 | + */ | |
| 472 | + /* recycle `old` instead to avoid UI flicker. */ | |
| 473 | + this.#rowError(old); | |
| 474 | + this.#removeRow(rowObj); | |
| 475 | + rowObj = old; | |
| 476 | + } | |
| 477 | + | |
| 478 | + let szLbl; | |
| 479 | + if( file.size < 500000 ){ | |
| 480 | + szLbl = file.size + ' bytes'; | |
| 481 | + }else if( file.size < 1000000 ){ | |
| 482 | + szLbl = (file.size / 1024).toFixed(2)+' KB'; | |
| 483 | + }else{ | |
| 484 | + szLbl = (file.size / (1024 * 1024)).toFixed(2)+' MB'; | |
| 485 | + } | |
| 486 | + this.#rowError(rowObj); | |
| 487 | + rowObj.file = file; | |
| 488 | + rowObj.mimeType = file.type || 'application/octet-stream'; | |
| 489 | + D.clearElement(rowObj.e.filename).append(file.name || 'Pasted Content'); | |
| 490 | + D.clearElement(rowObj.e.size).append(szLbl, ' ', rowObj.mimeType || ''); | |
| 491 | + rowObj.e.dropzone.classList.add('populated'); | |
| 492 | + if( rowObj.e.desc ){ | |
| 493 | + rowObj.e.desc.classList.remove('hidden'); | |
| 494 | + } | |
| 495 | + if( rowObj.e.thumbnail ){ | |
| 496 | + rowObj.e.thumbnail.remove(); | |
| 497 | + rowObj.e.thumbnail = undefined; | |
| 498 | + } | |
| 499 | + if( file.type?.startsWith?.('image/') || file.type==='BITMAP' ){ | |
| 500 | + /* Add a thumbnail */ | |
| 501 | + const img = rowObj.e.thumbnail = D.img(); | |
| 502 | + rowObj.e.dropzone.insertBefore(img, rowObj.e.remove); | |
| 503 | + img.classList.add('thumbnail'); | |
| 504 | + const reader = new FileReader(); | |
| 505 | + reader.onload = (e)=>img.setAttribute('src', e.target.result); | |
| 506 | + reader.readAsDataURL(file); | |
| 507 | + } | |
| 508 | + if( F.config.attachmentSizeLimit>0 | |
| 509 | + && file.size>F.config.attachmentSizeLimit ){ | |
| 510 | + /* Problem: tapping this link propagates its click event through | |
| 511 | + to eDropzone. Thus... */ | |
| 512 | + const eLink = D.a(F.repoUrl('help/attachment-size-limit'),'limit'); | |
| 513 | + eLink.addEventListener('click', ev=>ev.stopPropagation()); | |
| 514 | + this.#rowError(rowObj, "Too large: ", eLink, | |
| 515 | + " is ",F.config.attachmentSizeLimit," bytes"); | |
| 516 | + rowObj.ok = false; | |
| 517 | + }else if( !file.size ){ | |
| 518 | + this.#rowError(rowObj, "Cannot attach zero-byte files."); | |
| 519 | + rowObj.ok = false; | |
| 520 | + }else{ | |
| 521 | + rowObj.ok = true; | |
| 522 | + } | |
| 523 | + this.#events.dispatchEvent( | |
| 524 | + new CustomEvent('entry-populated',{ | |
| 525 | + detail: F.nu({ | |
| 526 | + type: 'entry-populated', | |
| 527 | + row: rowObj, | |
| 528 | + attacher: this | |
| 529 | + }) | |
| 530 | + }) | |
| 531 | + ); | |
| 532 | + } | |
| 533 | + | |
| 534 | + /** | |
| 535 | + Returns an array of objects describing the currently-selected | |
| 536 | + attachments. | |
| 537 | + */ | |
| 538 | + collectState(){ | |
| 539 | + const rv = []; | |
| 540 | + for(let r of this.#rows){ | |
| 541 | + if( !r.e.dropzone?.classList?.contains?.('populated') ){ | |
| 542 | + continue; | |
| 543 | + } | |
| 544 | + rv.push(F.nu({ | |
| 545 | + name: r.name || r.file.name, | |
| 546 | + content: r.file, | |
| 547 | + description: r.e.desc?.value ?? '', | |
| 548 | + mimeType: r.mimeType | |
| 549 | + })); | |
| 550 | + } | |
| 551 | + return rv; | |
| 552 | + } | |
| 553 | + | |
| 554 | + /** | |
| 555 | + Populates the given FormData object with entries named | |
| 556 | + ${namePrefix}${N}, each representing a selected file and N | |
| 557 | + being a 1-based incremental counter. For entries which have a | |
| 558 | + description, it also sets ${namePrefix}${N}_desc. | |
| 559 | + */ | |
| 560 | + populateFormData(fd, namePrefix='file'){ | |
| 561 | + const st = this.collectState(); | |
| 562 | + let i = 0; | |
| 563 | + for( ; i < st.length; ++i){ | |
| 564 | + const s = st[i]; | |
| 565 | + const suffix = i+1; | |
| 566 | + fd.append(`${namePrefix}${suffix}`, s.content, s.name); | |
| 567 | + const d = s.description?.trim?.(); | |
| 568 | + if( d ){ | |
| 569 | + fd.append(`${namePrefix}${suffix}_desc`, d); | |
| 570 | + } | |
| 571 | + } | |
| 572 | + return i; | |
| 573 | + } | |
| 574 | + }/*Attacher*/; | |
| 575 | + F.Attacher = Attacher; | |
| 576 | + | |
| 577 | + F.onPageLoad(function(){ | |
| 578 | + const eAttachWrapper = document.querySelector('#attachadd-form-wrapper'); | |
| 579 | + if( eAttachWrapper ){ | |
| 580 | + /* This page is /attachadd v2. eAttachWrapper holds | |
| 581 | + input[type=hidden] fields for use in attaching files and is | |
| 582 | + where we inject a file attachment widget. */ | |
| 583 | + document.body.querySelectorAll('#attachadd-legacy-form').forEach(e=>e.remove()); | |
| 584 | + | |
| 585 | + eAttachWrapper.classList.remove('hidden'); | |
| 586 | + const urlArgs = new URLSearchParams(window.location.search); | |
| 587 | + let zTarget = urlArgs.get('target'); | |
| 588 | + let zTo = urlArgs.get('to') || urlArgs.get('from'); | |
| 589 | + const eBtnSubmit = D.button("Submit"); | |
| 590 | + eBtnSubmit.type = 'button'; | |
| 591 | + const updateBtnSubmit = (attacher)=>{ | |
| 592 | + if( attacher.isPopulated ){ | |
| 593 | + eBtnSubmit.removeAttribute('disabled'); | |
| 594 | + }else{ | |
| 595 | + eBtnSubmit.setAttribute('disabled', ''); | |
| 596 | + } | |
| 597 | + }; | |
| 598 | + const cbAttacherChange = (ev)=>{ | |
| 599 | + const a = ev.detail.attacher; | |
| 600 | + updateBtnSubmit(a); | |
| 601 | + }; | |
| 602 | + const att = new Attacher({ | |
| 603 | + container: eAttachWrapper, | |
| 604 | + startWith: 1, | |
| 605 | + listener: cbAttacherChange, | |
| 606 | + controls: [eBtnSubmit], | |
| 607 | + description: true | |
| 608 | + }); | |
| 609 | + eBtnSubmit.addEventListener('click', async (ev)=>{ | |
| 610 | + att.reportError(); | |
| 611 | + const li = att.collectState(); | |
| 612 | + if( !li.length ) return; | |
| 613 | + if( eBtnSubmit.dataset.submitted ) return; | |
| 614 | + eBtnSubmit.dataset.submitted = 1; | |
| 615 | + D.disable(eBtnSubmit); | |
| 616 | + const fd = new FormData(); | |
| 617 | + att.populateFormData(fd); | |
| 618 | + for( const eIn of eAttachWrapper.querySelectorAll( | |
| 619 | + 'input[type="hidden"]' | |
| 620 | + ) ){ | |
| 621 | + /* Copy over hidden input fields emitted by the server. */ | |
| 622 | + if( eIn.name==='target' ){ | |
| 623 | + zTarget = eIn.value; | |
| 624 | + }else if( eIn.name==='to' || (eIn.name==='from' && !zTo) ){ | |
| 625 | + zTo = eIn.value; | |
| 626 | + } | |
| 627 | + fd.append(eIn.name, eIn.value) | |
| 628 | + } | |
| 629 | + if( att.isDryRun ){ | |
| 630 | + fd.append('dryrun', '1'); | |
| 631 | + } | |
| 632 | + let err; | |
| 633 | + const resp = await window.fetch(F.repoUrl('attachadd_ajax_post'), { | |
| 634 | + method: 'POST', | |
| 635 | + body: fd | |
| 636 | + }).catch((e)=>{ | |
| 637 | + err = e; | |
| 638 | + }); | |
| 639 | + D.enable(eBtnSubmit); | |
| 640 | + delete eBtnSubmit.dataset.submitted; | |
| 641 | + const jr = err ? undefined : await resp.json().catch(()=>{}); | |
| 642 | + if( err || jr?.error || !resp.ok ){ | |
| 643 | + const msg = err ? err.message : (jr?.error || resp.statusText); | |
| 644 | + att.reportError("Attaching failed: ", msg); | |
| 645 | + }else{ | |
| 646 | + att.clear(); | |
| 647 | + let to = zTo || jr?.redirect; | |
| 648 | + if( to ){ | |
| 649 | + if( '/'!==to[0] ){ | |
| 650 | + to = F.repoUrl(to); | |
| 651 | + } | |
| 652 | + window.location = to; | |
| 653 | + }else if( zTarget ){ | |
| 654 | + window.location = '?target='+zTarget+'&'+Date.now(); | |
| 655 | + } | |
| 656 | + } | |
| 657 | + })/*submit handler*/; | |
| 658 | + updateBtnSubmit(att); | |
| 659 | + F.page.attacher = att /* only for testing via dev console */; | |
| 660 | + }/* /attachadd */ | |
| 661 | + })/*onPageLoad()*/; | |
| 662 | + | |
| 663 | +})(window.fossil); |
| --- a/src/fossil.attach.js | |
| +++ b/src/fossil.attach.js | |
| @@ -0,0 +1,663 @@ | |
| --- a/src/fossil.attach.js | |
| +++ b/src/fossil.attach.js | |
| @@ -0,0 +1,663 @@ | |
| 1 | "use strict"; |
| 2 | /** |
| 3 | Utility for interactive file attachment. Supports attachment |
| 4 | selection from a file dialog, from the clipboard, or drag/drop. |
| 5 | |
| 6 | Requires that window.fossil has already been set up. |
| 7 | Depends on fossil.dom. |
| 8 | */ |
| 9 | (function(namespace){ |
| 10 | "use strict"; |
| 11 | const F = namespace, D = F.dom; |
| 12 | |
| 13 | let idCounter = 0; |
| 14 | /** |
| 15 | Implements a multi-file selector widget. Intended to be plugged |
| 16 | in to places in Fossil's UI where attachments can be assigned to |
| 17 | an artifact. |
| 18 | */ |
| 19 | class Attacher { |
| 20 | /* Options. */ |
| 21 | #opt; |
| 22 | /* List of objects representing each row. */ |
| 23 | #rows = []; |
| 24 | /* DOM elements */ |
| 25 | #e = Object.create(null); |
| 26 | /* Proxy for various events this object fires. */ |
| 27 | #events = new EventTarget(); |
| 28 | |
| 29 | /** |
| 30 | Options: |
| 31 | |
| 32 | opt.container: Optional DOM element to append the resulting |
| 33 | widget to. If not set, the client can get access to the widget |
| 34 | element using this.body. |
| 35 | |
| 36 | opt.addButtonLabel: optional label for the "add attachment" |
| 37 | button, defaulting to something generic. |
| 38 | |
| 39 | opt.limit: optional max number of attachments to allow. This |
| 40 | defaults to "some sensible value". |
| 41 | |
| 42 | opt.startWith[=0]: if >0 then that many file selection widgets |
| 43 | are automatically activated, as if the user had tapped the Add |
| 44 | button that many times. |
| 45 | |
| 46 | opt.description[=true]: if true then show the file description |
| 47 | field, otherwise elide it. |
| 48 | |
| 49 | opt.reverse[=false]: reverses the flow of the widget such that |
| 50 | the Add button stays on the top and rows are ordered |
| 51 | most-recently-added. |
| 52 | |
| 53 | opt.controls = [array of DOM elements]. Optional DOM elements |
| 54 | to inject into the UI element which wraps the "Add" button. |
| 55 | See this.controlsElement. |
| 56 | |
| 57 | opt.listener = function or object: {add: func, remove: func, |
| 58 | populate: func}: if these are functions they are registered as |
| 59 | listeners for 'entry-added', 'entry-removed', and/or |
| 60 | 'entry-populated' events, described below. opt.listener.all, if |
| 61 | set, is used as a fallback for any of 'add', 'remove', or |
| 62 | 'populate' which are not set. If opt.listener is a function |
| 63 | then it behaves as if listener={all: thatFunction}. |
| 64 | |
| 65 | Events: |
| 66 | |
| 67 | This class fires CustomEvents for certain changes: |
| 68 | |
| 69 | 'entry-added' and 'entry-removed' trigger when an attachment |
| 70 | entry row is added/removed. Its event.detail is: |
| 71 | |
| 72 | {attacher: this, row: object, type: 'same as event type'}. |
| 73 | |
| 74 | 'entry-populated' is triggered when a visible entry gets |
| 75 | content attached to it, with the same detail structure as |
| 76 | described above. |
| 77 | |
| 78 | The public structure of the row object passed to each is |
| 79 | currently TBD. |
| 80 | */ |
| 81 | constructor(opt){ |
| 82 | this.#opt = opt = F.nu({ |
| 83 | addButtonLabel: false, |
| 84 | startWith: 0, |
| 85 | limit: 0, |
| 86 | dryRun: undefined, |
| 87 | description: true, |
| 88 | reverse: false |
| 89 | }, opt); |
| 90 | this.#e.body = D.addClass(D.div(), 'Attacher'); |
| 91 | if( opt.reverse ) this.#e.body.classList.add('reverse'); |
| 92 | const eBtnAdd = this.#e.btnAdd = D.addClass( |
| 93 | D.button(this.#opt.addButtonLabel || 'Add attachment', |
| 94 | ()=>this.#addRow()), |
| 95 | 'attach-add-button' |
| 96 | ); |
| 97 | eBtnAdd.type = 'button'; |
| 98 | opt.ownsAddButton = true; |
| 99 | this.#e.err = D.addClass(D.div(), 'error', 'hidden'); |
| 100 | this.#e.body.append(this.#e.err); |
| 101 | this.#e.err.addEventListener('dblclick',()=>this.reportError()); |
| 102 | |
| 103 | const eControls = this.#e.controls = |
| 104 | D.addClass(D.div(), 'attach-controls'); |
| 105 | eControls.append(eBtnAdd); |
| 106 | if( opt.container ){ |
| 107 | opt.container.appendChild(this.#e.body); |
| 108 | } |
| 109 | this.#e.body.appendChild(eControls); |
| 110 | if( opt.listener ){ |
| 111 | const doCb = (eventType, key)=>{ |
| 112 | const f = (opt.listener instanceof Function) |
| 113 | ? opt.listener |
| 114 | : (opt.listener[key] || opt.listener.all); |
| 115 | if( f instanceof Function ){ |
| 116 | this.addEventListener(eventType, f); |
| 117 | } |
| 118 | }; |
| 119 | doCb('entry-added', 'add'); |
| 120 | doCb('entry-removed', 'remove'); |
| 121 | doCb('entry-populated', 'populate'); |
| 122 | } |
| 123 | if( opt.dryRun ){ |
| 124 | /* Add dry-run toggle for testing. */ |
| 125 | const eLbl = D.label(false, "Dry-run?"); |
| 126 | const eCb = D.checkbox(true); |
| 127 | eLbl.append(eCb); |
| 128 | eControls.append(eLbl); |
| 129 | eCb.checked = opt.dryRun = true; |
| 130 | eCb.addEventListener('change',()=>opt.dryRun=eCb.checked); |
| 131 | } |
| 132 | if( Array.isArray(opt.controls) ){ |
| 133 | eControls.append(...opt.controls); |
| 134 | } |
| 135 | if( opt.startWith > 0 ){ |
| 136 | for(let i = 0; i < opt.startWith; ++i ){ |
| 137 | this.#addRow(); |
| 138 | } |
| 139 | }else{ |
| 140 | this.#updateControls(); |
| 141 | } |
| 142 | } |
| 143 | |
| 144 | |
| 145 | get widget(){ |
| 146 | return this.#e.body; |
| 147 | } |
| 148 | |
| 149 | addEventListener(...args){ |
| 150 | return this.#events.addEventListener(...args); |
| 151 | } |
| 152 | |
| 153 | removeEventListener(...args){ |
| 154 | return this.#events.removeEventListener(...args); |
| 155 | } |
| 156 | |
| 157 | /** Returns true if any visible input widgets have content |
| 158 | selected. */ |
| 159 | get isPopulated(){ |
| 160 | for(let r of this.#rows){ |
| 161 | if( r.file ) return true; |
| 162 | } |
| 163 | return false; |
| 164 | } |
| 165 | |
| 166 | get isDryRun(){ |
| 167 | return !!this.#opt.dryRun; |
| 168 | } |
| 169 | /** |
| 170 | Returns the DOM element (div.attach-controls) which wraps the |
| 171 | "Add" button. Clients may add buttons to it. |
| 172 | */ |
| 173 | get controlsElement(){ |
| 174 | return this.#e.controls; |
| 175 | } |
| 176 | |
| 177 | /** |
| 178 | Reports an error by appending each argument to the error widget |
| 179 | and unhiding it. If passed no arugments, it clears and hides |
| 180 | the error widget. |
| 181 | */ |
| 182 | reportError(...msg){ |
| 183 | const e = this.#e.err; |
| 184 | D.clearElement(e); |
| 185 | if( msg.length ){ |
| 186 | e.classList.remove('hidden'); |
| 187 | e.append(...msg); |
| 188 | }else{ |
| 189 | e.classList.add('hidden'); |
| 190 | } |
| 191 | } |
| 192 | |
| 193 | #removeRow(rowObj){ |
| 194 | const er = rowObj.e.row; |
| 195 | if( er.parentNode ){ |
| 196 | this.#rows = this.#rows.filter(v=>v!==rowObj); |
| 197 | this.#updateControls(); |
| 198 | er.classList.add('animate-exit'); |
| 199 | er.addEventListener('animationend', ()=>er.remove(), {once: true}); |
| 200 | this.#events.dispatchEvent( |
| 201 | new CustomEvent('entry-removed',{ |
| 202 | detail: F.nu({ |
| 203 | type: 'entry-removed', |
| 204 | row: rowObj, |
| 205 | attacher: this |
| 206 | }) |
| 207 | }) |
| 208 | ); |
| 209 | } |
| 210 | } |
| 211 | |
| 212 | /** |
| 213 | Removes all attachments and clears the error state. |
| 214 | */ |
| 215 | clear(){ |
| 216 | for(const r of [...this.#rows/*clone because this updates #rows*/]){ |
| 217 | this.#removeRow(r); |
| 218 | } |
| 219 | this.reportError(); |
| 220 | } |
| 221 | |
| 222 | /** |
| 223 | Hides or shows the Add button, as appropriate. |
| 224 | */ |
| 225 | #updateControls(){ |
| 226 | const b = this.#e.btnAdd; |
| 227 | if( this.#opt.limit>0 && this.#rows.length >= this.#opt.limit ){ |
| 228 | b.classList.add('hidden'); |
| 229 | D.disable(b); |
| 230 | //F.toast.warning("Attachment form limit reached."); |
| 231 | }else{ |
| 232 | b.classList.remove('hidden'); |
| 233 | D.enable(b); |
| 234 | if( this.#opt.ownsAddButton ){ |
| 235 | this.#e.body.append(this.#e.controls/*move to the end*/); |
| 236 | } |
| 237 | } |
| 238 | } |
| 239 | |
| 240 | /** |
| 241 | Returns the "Add" button widget, Passing control of it to the |
| 242 | caller so that they can place it in another location. This |
| 243 | object will still manage its enabled/disabled/hidden state but |
| 244 | will no longer move it when adding a row. |
| 245 | */ |
| 246 | takeAddButton(){ |
| 247 | if( this.#opt.ownsAddButton ){ |
| 248 | this.#opt.ownsAddButton; |
| 249 | } |
| 250 | return this.#e.btnAdd; |
| 251 | } |
| 252 | /** |
| 253 | Sets rowObj.e.err up with an error message, or clears it if |
| 254 | passed only 1 argument. |
| 255 | */ |
| 256 | #rowError(rowObj,...msg){ |
| 257 | let e = rowObj.e.err; |
| 258 | if( e ){ |
| 259 | D.clearElement(e); |
| 260 | }else{ |
| 261 | if( !msg.length ) return; |
| 262 | e = rowObj.e.err = D.addClass(D.span(), 'error'); |
| 263 | rowObj.e.info.append(e); |
| 264 | } |
| 265 | if( msg.length ){ |
| 266 | e.append(...msg); |
| 267 | e.classList.remove('hidden'); |
| 268 | }else{ |
| 269 | e.classList.add('hidden'); |
| 270 | } |
| 271 | } |
| 272 | |
| 273 | #addRow(){ |
| 274 | const id = ++idCounter; |
| 275 | const rowObj = F.nu({ |
| 276 | id, file: null, mimeType: '' |
| 277 | }); |
| 278 | const eRow = D.addClass(D.div(), 'attach-row'); |
| 279 | const eDropzone = D.addClass(D.div(), 'attach-dropzone'); |
| 280 | const eFile = D.addClass( |
| 281 | D.input('file'), 'attach-file-input', 'hidden' |
| 282 | ); |
| 283 | const eInfo = D.addClass(D.span(), 'attach-row-info'); |
| 284 | const eFilename = D.append( |
| 285 | D.addClass(D.span(), 'attach-filename'), |
| 286 | "Select/drop file or click the outer border and tap your "+ |
| 287 | "platform's conventional <paste> keyboard shortcut." |
| 288 | ); |
| 289 | const eSize = D.addClass(D.span(), 'attach-size'); |
| 290 | eInfo.append(eFilename, eSize); |
| 291 | const eDesc = this.#opt.description |
| 292 | ? D.addClass( |
| 293 | D.attr(D.textarea(), 'placeholder', |
| 294 | 'Optional description...'), |
| 295 | 'attach-desc' |
| 296 | ) |
| 297 | : undefined; |
| 298 | const eRemove = D.addClass( |
| 299 | D.button('X', (ev)=>{ |
| 300 | ev.stopPropagation(); |
| 301 | this.#removeRow(rowObj); |
| 302 | }), |
| 303 | 'attach-row-remove' |
| 304 | ); |
| 305 | eRemove.setAttribute('title', 'Remove this attachment.'); |
| 306 | eRemove.type = 'button'; |
| 307 | |
| 308 | D.append(eDropzone, eInfo, eFile, eRemove); |
| 309 | eDropzone.addEventListener('click', ()=>eFile.click()); |
| 310 | eFile.addEventListener('change', (ev)=>{ |
| 311 | if( ev.target.files.length ){ |
| 312 | this.#injestBlob(rowObj, ev.target.files[0]); |
| 313 | } |
| 314 | }); |
| 315 | |
| 316 | eDropzone.addEventListener('dragover', (ev)=>{ |
| 317 | ev.preventDefault(); |
| 318 | eDropzone.classList.add('dragover'); |
| 319 | }); |
| 320 | eDropzone.addEventListener('dragleave', (ev)=>{ |
| 321 | eDropzone.classList.remove('dragover'); |
| 322 | }); |
| 323 | const handleDrop = (ev, theRealRowObj)=>{ |
| 324 | ev.preventDefault(); |
| 325 | eDropzone.classList.remove('dragover'); |
| 326 | if( ev.dataTransfer.files.length ){ |
| 327 | const r = theRealRowObj || rowObj; |
| 328 | this.#injestBlob(r, ev.dataTransfer.files[0]); |
| 329 | } |
| 330 | }; |
| 331 | /* Isn't working? eBtnAdd.addEventListener('drop', (ev)=>{ |
| 332 | this.#addRow(); |
| 333 | handleDrop(ev, this.#rows[this.#rows.length-1]); |
| 334 | });*/ |
| 335 | eDropzone.addEventListener('drop', handleDrop); |
| 336 | const pasteImage = (event, item)=>{ |
| 337 | if( item.type.indexOf('image') === 0 ) { |
| 338 | event.preventDefault(); |
| 339 | const blob = item.getAsFile(); |
| 340 | if( blob.name === 'image.png' ){ |
| 341 | /* Workaround to attempt to avoid name collisions when pasting |
| 342 | multiple images. We cannot, at this level, unambiguously |
| 343 | distinguish a ctrl-v of bitmap data vs a ctrl-v of an image |
| 344 | file copied via a desktop file manager. */ |
| 345 | rowObj.overrideName = `pasted-image-${Date.now()}.png`; |
| 346 | } |
| 347 | this.#injestBlob(rowObj, blob); |
| 348 | return true; |
| 349 | } |
| 350 | return false; |
| 351 | }; |
| 352 | const pasteThing = (event, thing)=>{ |
| 353 | if( pasteImage(event, thing) ) return true; |
| 354 | if( 'file' === thing.kind ){ |
| 355 | event.preventDefault(); |
| 356 | const blob = thing.getAsFile(); |
| 357 | if( blob ){ |
| 358 | this.#injestBlob(rowObj, blob); |
| 359 | return true; |
| 360 | } |
| 361 | } |
| 362 | return false; |
| 363 | }; |
| 364 | eDesc?.addEventListener?.('paste', (e) => { |
| 365 | e.stopPropagation(); |
| 366 | const items = (e.clipboardData || e.originalEvent.clipboardData)?.items; |
| 367 | if( !items ) return; |
| 368 | for( let i = 0; i < items.length; ++i ){ |
| 369 | const item = items[i]; |
| 370 | if( pasteThing(e, item) ){ |
| 371 | break; |
| 372 | } |
| 373 | } |
| 374 | }); |
| 375 | eRow.addEventListener('paste', (e) => { |
| 376 | const items = (e.clipboardData || e.originalEvent.clipboardData)?.items; |
| 377 | if( !items ) return; |
| 378 | for( let i = 0; i < items.length; ++i ){ |
| 379 | const item = items[i]; |
| 380 | if( item.type === 'text/plain' ){ |
| 381 | e.preventDefault(); |
| 382 | item.getAsString((text) => { |
| 383 | rowObj.overrideName = `pasted-text-${Date.now()}.txt`; |
| 384 | const blob = new File([text], rowObj.overrideName, |
| 385 | {type: 'text/plain'}); |
| 386 | this.#injestBlob(rowObj, blob); |
| 387 | }); |
| 388 | break; |
| 389 | }else if( pasteThing(e, item) ){ |
| 390 | break; |
| 391 | } |
| 392 | } |
| 393 | }); |
| 394 | eRow.append(eDropzone); |
| 395 | if( eDesc ) eRow.append(eDesc); |
| 396 | rowObj.e = F.nu({ |
| 397 | dropzone: eDropzone, |
| 398 | info: eInfo, |
| 399 | filename: eFilename, |
| 400 | size: eSize, |
| 401 | desc: eDesc, |
| 402 | row: eRow, |
| 403 | remove: eRemove |
| 404 | }); |
| 405 | this.#e.body.append(eRow); |
| 406 | eRow.classList.add('animate-entrance'); |
| 407 | requestAnimationFrame(() => { |
| 408 | eRow.scrollIntoView({ |
| 409 | behavior: 'smooth', |
| 410 | block: 'nearest', |
| 411 | inline: 'nearest' |
| 412 | }); |
| 413 | }); |
| 414 | |
| 415 | this.#rows.push( rowObj ); |
| 416 | this.#updateControls(); |
| 417 | this.#events.dispatchEvent( |
| 418 | new CustomEvent('entry-added',{ |
| 419 | detail: F.nu({ |
| 420 | type: 'entry-added', |
| 421 | row: rowObj, |
| 422 | attacher: this |
| 423 | }) |
| 424 | }) |
| 425 | ); |
| 426 | if( 0 ){ |
| 427 | /* To allow immediate ctrl-v, we need a trick... |
| 428 | But don't do this because it will interfere with, e.g., |
| 429 | the forum editor. */ |
| 430 | D.attr(eRow, 'tabindex', '-1'); |
| 431 | eRow.focus(); |
| 432 | } |
| 433 | } |
| 434 | |
| 435 | #rowMatchingName(name){ |
| 436 | for(let r of this.#rows){ |
| 437 | if( r.file?.name===name ) return r; |
| 438 | } |
| 439 | } |
| 440 | |
| 441 | /** |
| 442 | Injects the given File object as the attached content for the |
| 443 | given row. If the object's name collides with another row, |
| 444 | rowObj is removed from this widget and the old row is instead |
| 445 | re-populated with the new file. |
| 446 | |
| 447 | If rowObj.overrideName is set then the given file gets wrapped |
| 448 | with that name before attaching it, and that property is |
| 449 | removed from rowObj. This is intended only for communicating |
| 450 | auto-generated names for pasted data. |
| 451 | */ |
| 452 | #injestBlob(rowObj, file){ |
| 453 | if( !file ) return; |
| 454 | const old = this.#rowMatchingName(file.name); |
| 455 | if( rowObj.overrideName ){ |
| 456 | if( rowObj.overrideName !== file.name ){ |
| 457 | file = new File([file], rowObj.overrideName, {type: file.type}); |
| 458 | } |
| 459 | rowObj.overrideName = undefined; |
| 460 | } |
| 461 | if( old && rowObj !== old ){ |
| 462 | /* |
| 463 | Fossil attachments treat the name as a unique-per-target |
| 464 | key, with the newest one being the primary. If a name is |
| 465 | given twice, remove the new entry and reuse the older |
| 466 | one. There are conceivable, but also unlikely, cases where |
| 467 | this will have unintended side-effects, e.g. attaching both |
| 468 | /foo/bar and /baz/bar, but that seems like a lesser evil |
| 469 | than attaching the same file N times, leading to N |
| 470 | attachment artifacts. |
| 471 | */ |
| 472 | /* recycle `old` instead to avoid UI flicker. */ |
| 473 | this.#rowError(old); |
| 474 | this.#removeRow(rowObj); |
| 475 | rowObj = old; |
| 476 | } |
| 477 | |
| 478 | let szLbl; |
| 479 | if( file.size < 500000 ){ |
| 480 | szLbl = file.size + ' bytes'; |
| 481 | }else if( file.size < 1000000 ){ |
| 482 | szLbl = (file.size / 1024).toFixed(2)+' KB'; |
| 483 | }else{ |
| 484 | szLbl = (file.size / (1024 * 1024)).toFixed(2)+' MB'; |
| 485 | } |
| 486 | this.#rowError(rowObj); |
| 487 | rowObj.file = file; |
| 488 | rowObj.mimeType = file.type || 'application/octet-stream'; |
| 489 | D.clearElement(rowObj.e.filename).append(file.name || 'Pasted Content'); |
| 490 | D.clearElement(rowObj.e.size).append(szLbl, ' ', rowObj.mimeType || ''); |
| 491 | rowObj.e.dropzone.classList.add('populated'); |
| 492 | if( rowObj.e.desc ){ |
| 493 | rowObj.e.desc.classList.remove('hidden'); |
| 494 | } |
| 495 | if( rowObj.e.thumbnail ){ |
| 496 | rowObj.e.thumbnail.remove(); |
| 497 | rowObj.e.thumbnail = undefined; |
| 498 | } |
| 499 | if( file.type?.startsWith?.('image/') || file.type==='BITMAP' ){ |
| 500 | /* Add a thumbnail */ |
| 501 | const img = rowObj.e.thumbnail = D.img(); |
| 502 | rowObj.e.dropzone.insertBefore(img, rowObj.e.remove); |
| 503 | img.classList.add('thumbnail'); |
| 504 | const reader = new FileReader(); |
| 505 | reader.onload = (e)=>img.setAttribute('src', e.target.result); |
| 506 | reader.readAsDataURL(file); |
| 507 | } |
| 508 | if( F.config.attachmentSizeLimit>0 |
| 509 | && file.size>F.config.attachmentSizeLimit ){ |
| 510 | /* Problem: tapping this link propagates its click event through |
| 511 | to eDropzone. Thus... */ |
| 512 | const eLink = D.a(F.repoUrl('help/attachment-size-limit'),'limit'); |
| 513 | eLink.addEventListener('click', ev=>ev.stopPropagation()); |
| 514 | this.#rowError(rowObj, "Too large: ", eLink, |
| 515 | " is ",F.config.attachmentSizeLimit," bytes"); |
| 516 | rowObj.ok = false; |
| 517 | }else if( !file.size ){ |
| 518 | this.#rowError(rowObj, "Cannot attach zero-byte files."); |
| 519 | rowObj.ok = false; |
| 520 | }else{ |
| 521 | rowObj.ok = true; |
| 522 | } |
| 523 | this.#events.dispatchEvent( |
| 524 | new CustomEvent('entry-populated',{ |
| 525 | detail: F.nu({ |
| 526 | type: 'entry-populated', |
| 527 | row: rowObj, |
| 528 | attacher: this |
| 529 | }) |
| 530 | }) |
| 531 | ); |
| 532 | } |
| 533 | |
| 534 | /** |
| 535 | Returns an array of objects describing the currently-selected |
| 536 | attachments. |
| 537 | */ |
| 538 | collectState(){ |
| 539 | const rv = []; |
| 540 | for(let r of this.#rows){ |
| 541 | if( !r.e.dropzone?.classList?.contains?.('populated') ){ |
| 542 | continue; |
| 543 | } |
| 544 | rv.push(F.nu({ |
| 545 | name: r.name || r.file.name, |
| 546 | content: r.file, |
| 547 | description: r.e.desc?.value ?? '', |
| 548 | mimeType: r.mimeType |
| 549 | })); |
| 550 | } |
| 551 | return rv; |
| 552 | } |
| 553 | |
| 554 | /** |
| 555 | Populates the given FormData object with entries named |
| 556 | ${namePrefix}${N}, each representing a selected file and N |
| 557 | being a 1-based incremental counter. For entries which have a |
| 558 | description, it also sets ${namePrefix}${N}_desc. |
| 559 | */ |
| 560 | populateFormData(fd, namePrefix='file'){ |
| 561 | const st = this.collectState(); |
| 562 | let i = 0; |
| 563 | for( ; i < st.length; ++i){ |
| 564 | const s = st[i]; |
| 565 | const suffix = i+1; |
| 566 | fd.append(`${namePrefix}${suffix}`, s.content, s.name); |
| 567 | const d = s.description?.trim?.(); |
| 568 | if( d ){ |
| 569 | fd.append(`${namePrefix}${suffix}_desc`, d); |
| 570 | } |
| 571 | } |
| 572 | return i; |
| 573 | } |
| 574 | }/*Attacher*/; |
| 575 | F.Attacher = Attacher; |
| 576 | |
| 577 | F.onPageLoad(function(){ |
| 578 | const eAttachWrapper = document.querySelector('#attachadd-form-wrapper'); |
| 579 | if( eAttachWrapper ){ |
| 580 | /* This page is /attachadd v2. eAttachWrapper holds |
| 581 | input[type=hidden] fields for use in attaching files and is |
| 582 | where we inject a file attachment widget. */ |
| 583 | document.body.querySelectorAll('#attachadd-legacy-form').forEach(e=>e.remove()); |
| 584 | |
| 585 | eAttachWrapper.classList.remove('hidden'); |
| 586 | const urlArgs = new URLSearchParams(window.location.search); |
| 587 | let zTarget = urlArgs.get('target'); |
| 588 | let zTo = urlArgs.get('to') || urlArgs.get('from'); |
| 589 | const eBtnSubmit = D.button("Submit"); |
| 590 | eBtnSubmit.type = 'button'; |
| 591 | const updateBtnSubmit = (attacher)=>{ |
| 592 | if( attacher.isPopulated ){ |
| 593 | eBtnSubmit.removeAttribute('disabled'); |
| 594 | }else{ |
| 595 | eBtnSubmit.setAttribute('disabled', ''); |
| 596 | } |
| 597 | }; |
| 598 | const cbAttacherChange = (ev)=>{ |
| 599 | const a = ev.detail.attacher; |
| 600 | updateBtnSubmit(a); |
| 601 | }; |
| 602 | const att = new Attacher({ |
| 603 | container: eAttachWrapper, |
| 604 | startWith: 1, |
| 605 | listener: cbAttacherChange, |
| 606 | controls: [eBtnSubmit], |
| 607 | description: true |
| 608 | }); |
| 609 | eBtnSubmit.addEventListener('click', async (ev)=>{ |
| 610 | att.reportError(); |
| 611 | const li = att.collectState(); |
| 612 | if( !li.length ) return; |
| 613 | if( eBtnSubmit.dataset.submitted ) return; |
| 614 | eBtnSubmit.dataset.submitted = 1; |
| 615 | D.disable(eBtnSubmit); |
| 616 | const fd = new FormData(); |
| 617 | att.populateFormData(fd); |
| 618 | for( const eIn of eAttachWrapper.querySelectorAll( |
| 619 | 'input[type="hidden"]' |
| 620 | ) ){ |
| 621 | /* Copy over hidden input fields emitted by the server. */ |
| 622 | if( eIn.name==='target' ){ |
| 623 | zTarget = eIn.value; |
| 624 | }else if( eIn.name==='to' || (eIn.name==='from' && !zTo) ){ |
| 625 | zTo = eIn.value; |
| 626 | } |
| 627 | fd.append(eIn.name, eIn.value) |
| 628 | } |
| 629 | if( att.isDryRun ){ |
| 630 | fd.append('dryrun', '1'); |
| 631 | } |
| 632 | let err; |
| 633 | const resp = await window.fetch(F.repoUrl('attachadd_ajax_post'), { |
| 634 | method: 'POST', |
| 635 | body: fd |
| 636 | }).catch((e)=>{ |
| 637 | err = e; |
| 638 | }); |
| 639 | D.enable(eBtnSubmit); |
| 640 | delete eBtnSubmit.dataset.submitted; |
| 641 | const jr = err ? undefined : await resp.json().catch(()=>{}); |
| 642 | if( err || jr?.error || !resp.ok ){ |
| 643 | const msg = err ? err.message : (jr?.error || resp.statusText); |
| 644 | att.reportError("Attaching failed: ", msg); |
| 645 | }else{ |
| 646 | att.clear(); |
| 647 | let to = zTo || jr?.redirect; |
| 648 | if( to ){ |
| 649 | if( '/'!==to[0] ){ |
| 650 | to = F.repoUrl(to); |
| 651 | } |
| 652 | window.location = to; |
| 653 | }else if( zTarget ){ |
| 654 | window.location = '?target='+zTarget+'&'+Date.now(); |
| 655 | } |
| 656 | } |
| 657 | })/*submit handler*/; |
| 658 | updateBtnSubmit(att); |
| 659 | F.page.attacher = att /* only for testing via dev console */; |
| 660 | }/* /attachadd */ |
| 661 | })/*onPageLoad()*/; |
| 662 | |
| 663 | })(window.fossil); |
+8
-5
| --- src/fossil.bootstrap.js | ||
| +++ src/fossil.bootstrap.js | ||
| @@ -17,10 +17,14 @@ | ||
| 17 | 17 | initialized that object. |
| 18 | 18 | */ |
| 19 | 19 | |
| 20 | 20 | const F = global.fossil; |
| 21 | 21 | |
| 22 | + /** Creates a prototype-less plain object with properties derived | |
| 23 | + from all of its object-type arguments. */ | |
| 24 | + F.nu = (...obj)=>Object.assign(Object.create(null),...obj); | |
| 25 | + | |
| 22 | 26 | /** |
| 23 | 27 | Returns the current time in something approximating |
| 24 | 28 | ISO-8601 format. |
| 25 | 29 | */ |
| 26 | 30 | const timestring = function f(){ |
| @@ -54,11 +58,11 @@ | ||
| 54 | 58 | ** removed from the object. Pass it a falsy value to clear the target |
| 55 | 59 | ** element. |
| 56 | 60 | ** |
| 57 | 61 | ** Returns this object. |
| 58 | 62 | */ |
| 59 | - F.message = function f(msg){ | |
| 63 | + F.message = function f(){ | |
| 60 | 64 | const args = Array.prototype.slice.call(arguments,0); |
| 61 | 65 | const tgt = f.targetElement; |
| 62 | 66 | if(args.length) args.unshift( |
| 63 | 67 | localTimeString()+':' |
| 64 | 68 | //timestring(),'UTC:' |
| @@ -84,26 +88,25 @@ | ||
| 84 | 88 | F.message.targetElement.addEventListener( |
| 85 | 89 | 'dblclick', ()=>F.message(), false |
| 86 | 90 | ); |
| 87 | 91 | } |
| 88 | 92 | /* |
| 89 | - ** By default fossil.error() sends its first argument to | |
| 93 | + ** By default fossil.error() sends all arguments to | |
| 90 | 94 | ** console.error(). If fossil.message.targetElement (yes, |
| 91 | 95 | ** fossil.message) is set, it adds the 'error' CSS class to |
| 92 | 96 | ** that element and sets its content as defined for message(). |
| 93 | 97 | ** |
| 94 | 98 | ** Returns this object. |
| 95 | 99 | */ |
| 96 | - F.error = function f(msg){ | |
| 100 | + F.error = function f(){ | |
| 97 | 101 | const args = Array.prototype.slice.call(arguments,0); |
| 98 | 102 | const tgt = F.message.targetElement; |
| 99 | 103 | args.unshift(timestring(),'UTC:'); |
| 100 | 104 | if(tgt){ |
| 101 | 105 | tgt.classList.add('error'); |
| 102 | 106 | tgt.innerText = args.join(' '); |
| 103 | - } | |
| 104 | - else{ | |
| 107 | + }else{ | |
| 105 | 108 | args.unshift('Fossil error:'); |
| 106 | 109 | console.error.apply(console,args); |
| 107 | 110 | } |
| 108 | 111 | return this; |
| 109 | 112 | }; |
| 110 | 113 |
| --- src/fossil.bootstrap.js | |
| +++ src/fossil.bootstrap.js | |
| @@ -17,10 +17,14 @@ | |
| 17 | initialized that object. |
| 18 | */ |
| 19 | |
| 20 | const F = global.fossil; |
| 21 | |
| 22 | /** |
| 23 | Returns the current time in something approximating |
| 24 | ISO-8601 format. |
| 25 | */ |
| 26 | const timestring = function f(){ |
| @@ -54,11 +58,11 @@ | |
| 54 | ** removed from the object. Pass it a falsy value to clear the target |
| 55 | ** element. |
| 56 | ** |
| 57 | ** Returns this object. |
| 58 | */ |
| 59 | F.message = function f(msg){ |
| 60 | const args = Array.prototype.slice.call(arguments,0); |
| 61 | const tgt = f.targetElement; |
| 62 | if(args.length) args.unshift( |
| 63 | localTimeString()+':' |
| 64 | //timestring(),'UTC:' |
| @@ -84,26 +88,25 @@ | |
| 84 | F.message.targetElement.addEventListener( |
| 85 | 'dblclick', ()=>F.message(), false |
| 86 | ); |
| 87 | } |
| 88 | /* |
| 89 | ** By default fossil.error() sends its first argument to |
| 90 | ** console.error(). If fossil.message.targetElement (yes, |
| 91 | ** fossil.message) is set, it adds the 'error' CSS class to |
| 92 | ** that element and sets its content as defined for message(). |
| 93 | ** |
| 94 | ** Returns this object. |
| 95 | */ |
| 96 | F.error = function f(msg){ |
| 97 | const args = Array.prototype.slice.call(arguments,0); |
| 98 | const tgt = F.message.targetElement; |
| 99 | args.unshift(timestring(),'UTC:'); |
| 100 | if(tgt){ |
| 101 | tgt.classList.add('error'); |
| 102 | tgt.innerText = args.join(' '); |
| 103 | } |
| 104 | else{ |
| 105 | args.unshift('Fossil error:'); |
| 106 | console.error.apply(console,args); |
| 107 | } |
| 108 | return this; |
| 109 | }; |
| 110 |
| --- src/fossil.bootstrap.js | |
| +++ src/fossil.bootstrap.js | |
| @@ -17,10 +17,14 @@ | |
| 17 | initialized that object. |
| 18 | */ |
| 19 | |
| 20 | const F = global.fossil; |
| 21 | |
| 22 | /** Creates a prototype-less plain object with properties derived |
| 23 | from all of its object-type arguments. */ |
| 24 | F.nu = (...obj)=>Object.assign(Object.create(null),...obj); |
| 25 | |
| 26 | /** |
| 27 | Returns the current time in something approximating |
| 28 | ISO-8601 format. |
| 29 | */ |
| 30 | const timestring = function f(){ |
| @@ -54,11 +58,11 @@ | |
| 58 | ** removed from the object. Pass it a falsy value to clear the target |
| 59 | ** element. |
| 60 | ** |
| 61 | ** Returns this object. |
| 62 | */ |
| 63 | F.message = function f(){ |
| 64 | const args = Array.prototype.slice.call(arguments,0); |
| 65 | const tgt = f.targetElement; |
| 66 | if(args.length) args.unshift( |
| 67 | localTimeString()+':' |
| 68 | //timestring(),'UTC:' |
| @@ -84,26 +88,25 @@ | |
| 88 | F.message.targetElement.addEventListener( |
| 89 | 'dblclick', ()=>F.message(), false |
| 90 | ); |
| 91 | } |
| 92 | /* |
| 93 | ** By default fossil.error() sends all arguments to |
| 94 | ** console.error(). If fossil.message.targetElement (yes, |
| 95 | ** fossil.message) is set, it adds the 'error' CSS class to |
| 96 | ** that element and sets its content as defined for message(). |
| 97 | ** |
| 98 | ** Returns this object. |
| 99 | */ |
| 100 | F.error = function f(){ |
| 101 | const args = Array.prototype.slice.call(arguments,0); |
| 102 | const tgt = F.message.targetElement; |
| 103 | args.unshift(timestring(),'UTC:'); |
| 104 | if(tgt){ |
| 105 | tgt.classList.add('error'); |
| 106 | tgt.innerText = args.join(' '); |
| 107 | }else{ |
| 108 | args.unshift('Fossil error:'); |
| 109 | console.error.apply(console,args); |
| 110 | } |
| 111 | return this; |
| 112 | }; |
| 113 |
+4
| --- src/fossil.dom.js | ||
| +++ src/fossil.dom.js | ||
| @@ -80,10 +80,14 @@ | ||
| 80 | 80 | Returns a LABEL element. If passed an argument, |
| 81 | 81 | it must be an id or an HTMLElement with an id, |
| 82 | 82 | and that id is set as the 'for' attribute of the |
| 83 | 83 | label. If passed 2 arguments, the 2nd is text or |
| 84 | 84 | a DOM element to append to the label. |
| 85 | + | |
| 86 | + 2026-06: this is a goofy interface. Generally simpler that | |
| 87 | + dealing with IDs is to embed the target control within the label | |
| 88 | + element. | |
| 85 | 89 | */ |
| 86 | 90 | dom.label = function(forElem, text){ |
| 87 | 91 | const rc = document.createElement('label'); |
| 88 | 92 | if(forElem){ |
| 89 | 93 | if(forElem instanceof HTMLElement){ |
| 90 | 94 |
| --- src/fossil.dom.js | |
| +++ src/fossil.dom.js | |
| @@ -80,10 +80,14 @@ | |
| 80 | Returns a LABEL element. If passed an argument, |
| 81 | it must be an id or an HTMLElement with an id, |
| 82 | and that id is set as the 'for' attribute of the |
| 83 | label. If passed 2 arguments, the 2nd is text or |
| 84 | a DOM element to append to the label. |
| 85 | */ |
| 86 | dom.label = function(forElem, text){ |
| 87 | const rc = document.createElement('label'); |
| 88 | if(forElem){ |
| 89 | if(forElem instanceof HTMLElement){ |
| 90 |
| --- src/fossil.dom.js | |
| +++ src/fossil.dom.js | |
| @@ -80,10 +80,14 @@ | |
| 80 | Returns a LABEL element. If passed an argument, |
| 81 | it must be an id or an HTMLElement with an id, |
| 82 | and that id is set as the 'for' attribute of the |
| 83 | label. If passed 2 arguments, the 2nd is text or |
| 84 | a DOM element to append to the label. |
| 85 | |
| 86 | 2026-06: this is a goofy interface. Generally simpler that |
| 87 | dealing with IDs is to embed the target control within the label |
| 88 | element. |
| 89 | */ |
| 90 | dom.label = function(forElem, text){ |
| 91 | const rc = document.createElement('label'); |
| 92 | if(forElem){ |
| 93 | if(forElem instanceof HTMLElement){ |
| 94 |
+1227
-12
| --- src/fossil.page.forumpost.js | ||
| +++ src/fossil.page.forumpost.js | ||
| @@ -1,24 +1,852 @@ | ||
| 1 | +/** | |
| 2 | + Code for the forum family of pages. Requires fossil.X where X is | |
| 3 | + (copybutton, pikchr, confirmer, attach, tabs, storage). | |
| 4 | +*/ | |
| 1 | 5 | (function(F/*the fossil object*/){ |
| 2 | 6 | "use strict"; |
| 3 | 7 | /* JS code for /forumpost and friends. Requires fossil.dom |
| 4 | 8 | and can optionally use fossil.pikchr. */ |
| 5 | 9 | const P = F.page, D = F.dom; |
| 6 | 10 | |
| 11 | + let idCounter = 0; | |
| 12 | + | |
| 13 | + /* | |
| 14 | + The problem: when previewing the bottom-most post of a thread, the | |
| 15 | + preview widget's size changes cause the page to scroll | |
| 16 | + unpredictably as the bottom boundary of the page moves. A weird | |
| 17 | + workaround (not invented here) is to add dummy blank padding to | |
| 18 | + the page to allow the preview widget to grow and shrink without | |
| 19 | + (usually) scrolling, but whether it does so really depends on its | |
| 20 | + size. | |
| 21 | + | |
| 22 | + We could maybe get the same effect by adding this size as | |
| 23 | + padding-bottom to document.body instead of as a new element. | |
| 24 | + */ | |
| 25 | + const dummyPadding = D.div(); | |
| 26 | + dummyPadding.style.height = '75em'; | |
| 27 | + /* Keep track of ForumPostEditor instances so we can remove this | |
| 28 | + padding when none are active. */ | |
| 29 | + dummyPadding.refs = new Set(); | |
| 30 | + F.dummyPadding = dummyPadding /* only for debugging */; | |
| 31 | + | |
| 32 | + /** | |
| 33 | + A forum post editor widget for new posts and responses. | |
| 34 | + */ | |
| 35 | + class ForumPostEditor { | |
| 36 | + /* Options */ | |
| 37 | + #opt; | |
| 38 | + /* Dom elements */ | |
| 39 | + #e; | |
| 40 | + /* F.Attacher instance */ | |
| 41 | + #att; | |
| 42 | + /* Is waiting on a pending remote response. */ | |
| 43 | + #isWaiting = false; | |
| 44 | + /* F.TabManager */ | |
| 45 | + #tabs; | |
| 46 | + /* Elements to disable while an XHR is pending. */ | |
| 47 | + #toDisable = []; | |
| 48 | + /* DOM element of the current active tab. */ | |
| 49 | + #activeTab; | |
| 50 | + /* Extra input[type=hidden] fields imported from fossil's | |
| 51 | + static page generation. */ | |
| 52 | + #extraFields; | |
| 53 | + /* Persistent draft message object. */ | |
| 54 | + #draft; | |
| 55 | + | |
| 56 | + /** | |
| 57 | + Options: | |
| 58 | + | |
| 59 | + opt.draftKey[string=undefined]: if set then this object's state | |
| 60 | + will be stored in fossil.storage when the relevant input fields | |
| 61 | + lose focus. If old state is found, the form is pre-populated | |
| 62 | + from it. The state is cleared on a discard() or successful | |
| 63 | + submit. | |
| 64 | + | |
| 65 | + opt.ondiscard[=function]: if set, a Discard button is added | |
| 66 | + which, when activated, clears the current draft and removes | |
| 67 | + this object's widget from the DOM. After doing so, | |
| 68 | + opt.ondiscard() is called and passed this object. Exceptions | |
| 69 | + thrown by ondiscard() are ignored but may be logged. | |
| 70 | + | |
| 71 | + opt.onsubmit[=function]: if set, this function is called | |
| 72 | + immediately after the post has been successfully saved, and | |
| 73 | + passed this object and a JSON-format response object from the | |
| 74 | + save request. It is generally then up to the caller to close() | |
| 75 | + this object and/or redirect to /forumpost/${arguments[1].uuid}. | |
| 76 | + | |
| 77 | + opt.onclose[=function]: like opt.onsubmit, this function is | |
| 78 | + called when this.close() is called, and passed no arguments. | |
| 79 | + onclose() is called before the widget is removed from the dom | |
| 80 | + and _does not_ fire if it is not in the DOM. | |
| 81 | + | |
| 82 | + opt.hiddenFields: an optional list of input elements to | |
| 83 | + incorporate into the form for requests which request the | |
| 84 | + preview or save the post. | |
| 85 | + | |
| 86 | + opt.inReplyTo=uuid: if this is a response to a post, this | |
| 87 | + is the full forum post uuid of the being-replied-to post. | |
| 88 | + | |
| 89 | + opt.edit=artifactObject: if this is an edit of an existing | |
| 90 | + post, this is the full JSON-format artifact of the forum post | |
| 91 | + the being-edited post, as returned by /ajax/artifact.json. | |
| 92 | + | |
| 93 | + opt.status: optional current status tag value for opt.edit, | |
| 94 | + if known. This is used for pre-selecting a status value. | |
| 95 | + | |
| 96 | + opt.hideStash[bool=false]: if true, the "Stash" button does not | |
| 97 | + get added. Intended for use with /forumnew. | |
| 98 | + */ | |
| 99 | + constructor(opt){ | |
| 100 | + opt = this.#opt = F.nu({ | |
| 101 | + draftKey: undefined, | |
| 102 | + hideStash: false | |
| 103 | + }, opt); | |
| 104 | + opt.isNewThread = !opt.inReplyTo && !opt.edit; | |
| 105 | + if( opt.draftKey ){ | |
| 106 | + this.#draft = F.nu(F.storage.getJSON(opt.draftKey, {})); | |
| 107 | + } | |
| 108 | + const e = this.#e = F.nu({ | |
| 109 | + mimetype: F.nu(), | |
| 110 | + button: F.nu() | |
| 111 | + }); | |
| 112 | + //console.debug("Setting up FPE opt =",opt); | |
| 113 | + const wrapper = e.widget = D.addClass(D.div(), 'ForumPostEditor'); | |
| 114 | + D.clearElement(wrapper); | |
| 115 | + | |
| 116 | + if( !opt.inReplyTo ){ | |
| 117 | + /* Title... */ | |
| 118 | + e.titleBar = D.addClass(D.div(),'titlebar'); | |
| 119 | + e.title = D.attr( | |
| 120 | + D.addClass(D.input('text'), 'title'), | |
| 121 | + 'placeholder', | |
| 122 | + 'Thread title (required)' | |
| 123 | + ); | |
| 124 | + e.title.setAttribute('maxlength', 125); | |
| 125 | + e.titleBar.append( | |
| 126 | + D.append(D.span(), "Title:"), | |
| 127 | + e.title | |
| 128 | + ); | |
| 129 | + if( this.#draft ){ | |
| 130 | + e.title.addEventListener('blur', ()=>{ | |
| 131 | + this.#draft.title = e.title.value; | |
| 132 | + this.#storeDraft(); | |
| 133 | + }); | |
| 134 | + e.title.value = this.#draft.title || opt.edit?.H || ''; | |
| 135 | + }else if( opt.edit?.H ){ | |
| 136 | + e.title.value = opt.edit.H; | |
| 137 | + } | |
| 138 | + wrapper.append(e.titleBar); | |
| 139 | + } | |
| 140 | + | |
| 141 | + { /* Mimetype... */ | |
| 142 | + e.mimetype.wrapper = D.addClass(D.div(), 'mimetype-wrapper'); | |
| 143 | + const sel = e.mimetype.select = D.addClass(D.select(), 'mimetype-select'); | |
| 144 | + sel.setAttribute('title', 'Markup format for this post.'); | |
| 145 | + this.#toDisable.push(sel); | |
| 146 | + let i = 0; | |
| 147 | + D.option(sel, '', '- Markup format -').disabled = true; | |
| 148 | + for(const [k,v] of Object.entries({ | |
| 149 | + 'text/x-markdown': 'Markdown', | |
| 150 | + 'text/x-fossil-wiki': 'Fossil Wiki', | |
| 151 | + 'text/plain': 'Plain text' | |
| 152 | + })) { | |
| 153 | + D.option(sel, k, v); | |
| 154 | + } | |
| 155 | + sel.value = opt.mimetype | |
| 156 | + || this.#draft?.mimetype | |
| 157 | + || F.storage.get('forum-mimetype', sel.options[1].value); | |
| 158 | + sel.addEventListener('change',ev=>{ | |
| 159 | + if( this.#draft && this.#draft.mimetype!==ev.target.value ){ | |
| 160 | + this.#draft.mimetype = ev.target.value; | |
| 161 | + this.#storeDraft(); | |
| 162 | + } | |
| 163 | + F.storage.set('forum-mimetype', ev.target.value); | |
| 164 | + }); | |
| 165 | + e.mimetype.wrapper.append(sel); | |
| 166 | + } | |
| 167 | + | |
| 168 | + e.buttons = D.addClass(D.div(), 'buttons'); | |
| 169 | + { /* Preview/submit buttons... */ | |
| 170 | + e.button.preview = D.attr( | |
| 171 | + D.button("Preview", e=>this.#preview()), | |
| 172 | + 'title', | |
| 173 | + 'Preview your edits.' | |
| 174 | + ); | |
| 175 | + e.button.submit = D.attr( | |
| 176 | + D.button("Submit"), | |
| 177 | + 'title', | |
| 178 | + 'Save any edits to the server. Not permitted until Preview has been used.' | |
| 179 | + ); | |
| 180 | + if( this.#draft && !opt.hideStash ){ | |
| 181 | + e.button.stash = D.attr( | |
| 182 | + D.button( | |
| 183 | + "Stash", e=>this.close() | |
| 184 | + /* This could be called Close, but that would semantically | |
| 185 | + collide with the Close [this post] button. All "Stash" | |
| 186 | + does is close the widget. */ | |
| 187 | + ), | |
| 188 | + 'title', "Close this editor and stash any edits locally." | |
| 189 | + ); | |
| 190 | + } | |
| 191 | + if( opt.ondiscard instanceof Function ){ | |
| 192 | + e.button.discard = D.attr( | |
| 193 | + D.button('Discard'), | |
| 194 | + 'title', | |
| 195 | + 'Close the editor and discard all local edits.' | |
| 196 | + ); | |
| 197 | + } | |
| 198 | + if( 1 ){ | |
| 199 | + F.confirmer(e.button.submit, { | |
| 200 | + confirmText: "Confirm submit...", | |
| 201 | + onconfirm: ()=>this.#submit() | |
| 202 | + }); | |
| 203 | + if( e.button.discard ){ | |
| 204 | + F.confirmer(e.button.discard, { | |
| 205 | + confirmText: "Really discard?", | |
| 206 | + onconfirm: ()=>this.discard() | |
| 207 | + }); | |
| 208 | + } | |
| 209 | + }else{ | |
| 210 | + e.button.submit.addEventListener('click', ()=>this.#submit()); | |
| 211 | + if( e.button.discard ){ | |
| 212 | + e.button.submit.addEventListener('click', ()=>this.discard()); | |
| 213 | + } | |
| 214 | + } | |
| 215 | + e.button.submit.setAttribute('disabled', ''); | |
| 216 | + wrapper.append(e.buttons); | |
| 217 | + | |
| 218 | + e.error = D.addClass(D.div(), 'error', 'hidden'); | |
| 219 | + wrapper.append(e.error); | |
| 220 | + e.error.addEventListener('dblclick',()=>this.reportError()); | |
| 221 | + } | |
| 222 | + | |
| 223 | + const idPrefix = 'FormPostEditor'+(++idCounter)/* TabManager requires IDs */; | |
| 224 | + { /* Main tabs... */ | |
| 225 | + e.tabs = D.attr( | |
| 226 | + D.addClass(D.div(), 'tab-container'), | |
| 227 | + 'id', idPrefix+'-tabs' | |
| 228 | + ); | |
| 229 | + this.#tabs = new F.TabManager(e.tabs); | |
| 230 | + this.#tabs.addEventListener('before-switch-to', (ev)=>{ | |
| 231 | + //console.debug("Switching to tab",ev.detail); | |
| 232 | + switch( (this.#activeTab = ev.detail) ){ | |
| 233 | + case e.preview: | |
| 234 | + this.#e.button.preview.click(); | |
| 235 | + break; | |
| 236 | + case e.help: | |
| 237 | + if( e.help.$needsInit ){ | |
| 238 | + delete e.help.$needsInit; | |
| 239 | + this.#initHelpTab(); | |
| 240 | + } | |
| 241 | + break; | |
| 242 | + case e.tabAttach: | |
| 243 | + if( !this.#att ) this.#initAttacherTab(); | |
| 244 | + break; | |
| 245 | + } | |
| 246 | + }); | |
| 247 | + wrapper.append( e.tabs ); | |
| 248 | + | |
| 249 | + e.tabEdit = D.div(); | |
| 250 | + e.tabEdit.classList.add('editor-wrapper'); | |
| 251 | + e.editor = D.attr( | |
| 252 | + D.addClass(D.textarea(), 'editor'), | |
| 253 | + 'placeholder', | |
| 254 | + 'Your message to other forum-goers...' | |
| 255 | + ); | |
| 256 | + e.tabEdit.append(e.editor); | |
| 257 | + e.tabEdit.dataset.tabLabel = (opt.edit || !opt.inReplyTo) | |
| 258 | + ? 'Edit' : 'Reply'; | |
| 259 | + this.#tabs.addTab( e.tabEdit ); | |
| 260 | + this.#tabs.switchToTab( e.tabEdit ); | |
| 261 | + if( this.#draft ){ | |
| 262 | + this.editorContent = this.#draft.content || opt.edit?.W || ''; | |
| 263 | + e.editor.addEventListener( | |
| 264 | + 'blur', ()=>{ | |
| 265 | + this.#draft.content = this.editorContent; | |
| 266 | + this.#storeDraft(); | |
| 267 | + } | |
| 268 | + ); | |
| 269 | + }else if( opt.edit?.W ){ | |
| 270 | + this.editorContent = opt.artifact.W; | |
| 271 | + } | |
| 272 | + e.preview = D.addClass(D.div(), 'preview'); | |
| 273 | + e.preview.dataset.tabLabel = 'Preview'; | |
| 274 | + this.#toDisable.push(e.button.preview); | |
| 275 | + this.#tabs.addTab( e.preview ); | |
| 276 | + } | |
| 277 | + | |
| 278 | + if( F.user.enableDebug ){ | |
| 279 | + e.debug = D.addClass(D.div(), 'debug'); | |
| 280 | + e.debug.dataset.tabLabel = 'Debug'; | |
| 281 | + e.debug.setAttribute('id', idPrefix+'-debug'); | |
| 282 | + for(const [k,v] of Object.entries({ | |
| 283 | + dryrun: 'Dry run', | |
| 284 | + domod: 'Require moderation approval', | |
| 285 | + //showqp: 'Show query parameters', | |
| 286 | + fpsilent: 'Do not send notification emails' | |
| 287 | + })){ | |
| 288 | + const lbl = D.label(false, v); | |
| 289 | + lbl.prepend(D.checkbox(k)); | |
| 290 | + e.debug.append(lbl); | |
| 291 | + } | |
| 292 | + this.#tabs.addTab(e.debug); | |
| 293 | + } | |
| 294 | + e.buttons.append(e.mimetype.wrapper); | |
| 295 | + | |
| 296 | + if( opt.edit | |
| 297 | + && !opt.inReplyTo | |
| 298 | + && F.config.forumStatuses?.length>0 ){ | |
| 299 | + const sel = e.status = D.select(); | |
| 300 | + sel.setAttribute('title', 'The status tag value for this post.'); | |
| 301 | + D.option(sel, "", "- Status -").disabled = true; | |
| 302 | + for( const status of F.config.forumStatuses ){ | |
| 303 | + D.option(sel, status.value, status.label); | |
| 304 | + } | |
| 305 | + e.buttons.append(sel); | |
| 306 | + if( opt.status ){ | |
| 307 | + sel.value = opt.status; | |
| 308 | + }else if( this.#draft ){ | |
| 309 | + if( this.#draft.status ){ | |
| 310 | + sel.value = this.#draft.status; | |
| 311 | + }else{ | |
| 312 | + this.#draft.status = sel.value = F.config.forumStatuses[0].value; | |
| 313 | + } | |
| 314 | + sel.addEventListener('change',ev=>{ | |
| 315 | + const v = sel.value; | |
| 316 | + if( this.#draft.status !== v ){ | |
| 317 | + this.#draft.status = v; | |
| 318 | + this.#storeDraft(); | |
| 319 | + } | |
| 320 | + }); | |
| 321 | + } | |
| 322 | + }/*e.status*/ | |
| 323 | + | |
| 324 | + if( F.user.mayAttachForum ){ | |
| 325 | + //e.buttons.append( e.button.addAttach = this.#att.takeAddButton() ); | |
| 326 | + e.tabAttach = D.div(); | |
| 327 | + e.tabAttach.setAttribute('id', idPrefix+'-attach'); | |
| 328 | + e.tabAttach.dataset.tabLabel = 'Attachments'; | |
| 329 | + this.#tabs.addTab(e.tabAttach); | |
| 330 | + /* Reminder: we don't currently have a way to disable/enable | |
| 331 | + an Attacher's controls during ajax traffic. */ | |
| 332 | + } | |
| 333 | + e.buttons.append(e.button.preview, e.button.submit); | |
| 334 | + if( e.button.stash ){ | |
| 335 | + e.buttons.append(e.button.stash); | |
| 336 | + this.#toDisable.push(e.button.stash); | |
| 337 | + } | |
| 338 | + if( e.button.discard ){ | |
| 339 | + e.buttons.append(e.button.discard); | |
| 340 | + this.#toDisable.push(e.button.discard); | |
| 341 | + } | |
| 342 | + | |
| 343 | + e.help = D.attr(D.div(), 'id', idPrefix+'-help'); | |
| 344 | + e.help.$needsInit = true; | |
| 345 | + e.help.dataset.tabLabel = 'Help'; | |
| 346 | + this.#tabs.addTab(e.help); | |
| 347 | + | |
| 348 | + if( opt.hiddenFields ){ | |
| 349 | + this.addHiddenFields( opt.hiddenFields ); | |
| 350 | + delete opt.hiddenFields; | |
| 351 | + } | |
| 352 | + | |
| 353 | + { /* Shift-enter pieces... */ | |
| 354 | + const eCb = D.checkbox(1); | |
| 355 | + const eLbl = D.label(); | |
| 356 | + const eHelp = D.append( | |
| 357 | + D.span(), [ | |
| 358 | + 'When checked, shift-enter will toggle between preview ', | |
| 359 | + 'and edit modes, which is generally useful but some ', | |
| 360 | + 'software keyboards misinteract with it. If the preview ', | |
| 361 | + 'starts when tapping Enter, turn this setting off.' | |
| 362 | + ].join('') | |
| 363 | + ); | |
| 364 | + eCb.checked = F.storage.getBool( | |
| 365 | + 'edit-shift-enter-preview', | |
| 366 | + true | |
| 367 | + /* Maintenance reminder: this setting is shared across | |
| 368 | + several apps, like /chat, /wikiedit, and /fileedit. */ | |
| 369 | + ); | |
| 370 | + eCb.addEventListener('change', (ev)=>{ | |
| 371 | + F.storage.set('edit-shift-enter-preview', eCb.checked); | |
| 372 | + }); | |
| 373 | + F.helpButtonlets.setup(eHelp); | |
| 374 | + eLbl.append("Shift-enter toggles preview?", eCb, eHelp); | |
| 375 | + e.tabEdit.append(eLbl); | |
| 376 | + const isShiftEnter = (ev)=>eCb.checked && ev.shiftKey && 13===ev.keyCode; | |
| 377 | + e.editor.addEventListener('keydown',(ev)=>{ | |
| 378 | + /** | |
| 379 | + If eCb.checked is true, a keyboard combo of shift-enter | |
| 380 | + (from the editor) toggles between preview and edit modes. | |
| 381 | + This is normally desired but at least one software | |
| 382 | + keyboard is known to misinteract with this, treating an | |
| 383 | + Enter after automatically-capitalized letters as a | |
| 384 | + shift-enter: | |
| 385 | + | |
| 386 | + https://fossil-scm.org/forum/forumpost/dbd5b68366147ce8 | |
| 387 | + */ | |
| 388 | + if(!isShiftEnter(ev)) return; | |
| 389 | + ev.preventDefault(); | |
| 390 | + ev.stopPropagation(); | |
| 391 | + e.editor.blur(/*force draft update if needed*/); | |
| 392 | + this.#tabs.switchToTab(e.preview); | |
| 393 | + }, false); | |
| 394 | + // If we're in the preview tab, have ctrl-enter switch back to the editor. | |
| 395 | + document.body.addEventListener('keydown',(ev)=>{ | |
| 396 | + if(!isShiftEnter(ev)) return; | |
| 397 | + if(this.#activeTab !== e.tabEdit){ | |
| 398 | + ev.preventDefault(); | |
| 399 | + ev.stopPropagation(); | |
| 400 | + this.#tabs.switchToTab(e.tabEdit); | |
| 401 | + e.editor.focus(/*slow as molasses for long docs, as focus() | |
| 402 | + forces a document reflow. */); | |
| 403 | + return false; | |
| 404 | + } | |
| 405 | + }, true); | |
| 406 | + }/*shift-enter preview bits*/ | |
| 407 | + | |
| 408 | + if(0){ /* Needs to be optional */ | |
| 409 | + const elemsToToggle = document.body.querySelectorAll( | |
| 410 | + ':scope > header, :scope > nav' | |
| 411 | + ); | |
| 412 | + e.button.toggleHeader = | |
| 413 | + D.button('Toggle header', e=>{ | |
| 414 | + for(const et of elemsToToggle){ | |
| 415 | + et.classList.toggle('hidden'); | |
| 416 | + } | |
| 417 | + }); | |
| 418 | + e.buttons.append(e.button.toggleHeader); | |
| 419 | + } | |
| 420 | + | |
| 421 | + { | |
| 422 | + const eLbl = D.label(false, "Posting as "+F.user.name) | |
| 423 | + eLbl.classList.add('logged-in-as'); | |
| 424 | + e.buttons.append(eLbl); | |
| 425 | + } | |
| 426 | + | |
| 427 | + }/*constructor*/ | |
| 428 | + | |
| 429 | + /* | |
| 430 | + ** Removes this object from the DOM. It has no side effects if | |
| 431 | + ** it's not in the DOM. | |
| 432 | + */ | |
| 433 | + close(){ | |
| 434 | + const e = this.#e.widget; | |
| 435 | + if( e?.parentNode ){ | |
| 436 | + if( this.#opt.onclose instanceof Function ){ | |
| 437 | + try{this.#opt.onclose();} | |
| 438 | + catch(e){ | |
| 439 | + console.error("ForumPostEditor.onclose() threw:",e); | |
| 440 | + } | |
| 441 | + } | |
| 442 | + //console.debug("FPE discarding", this); | |
| 443 | + e.classList.add('animate-exit'); | |
| 444 | + e.addEventListener('animationend', ()=>e.remove(), {once: true}); | |
| 445 | + dummyPadding.refs.delete(this); | |
| 446 | + if( 0===dummyPadding.refs.size ){ | |
| 447 | + dummyPadding.remove(); | |
| 448 | + } | |
| 449 | + } | |
| 450 | + } | |
| 451 | + | |
| 452 | + /* | |
| 453 | + ** Discards any draft edits then calls close(). If an ondiscard | |
| 454 | + ** callback was provided to the constructor then it is called | |
| 455 | + ** before the drafts are cleared and any exceptions it throws are | |
| 456 | + ** ignored (but may be logged). | |
| 457 | + */ | |
| 458 | + discard(){ | |
| 459 | + if( this.#opt.ondiscard instanceof Function ){ | |
| 460 | + try{this.#opt.ondiscard(this);} | |
| 461 | + catch(e){ | |
| 462 | + console.error("ForumPostEditor.ondiscard() threw:",e); | |
| 463 | + } | |
| 464 | + } | |
| 465 | + this.#clearDraft(); | |
| 466 | + this.close(); | |
| 467 | + } | |
| 468 | + | |
| 469 | + /** This widget's top-most DOM element. */ | |
| 470 | + get widget(){ | |
| 471 | + if( !dummyPadding.parentElement ){ | |
| 472 | + document.body.append(dummyPadding); | |
| 473 | + } | |
| 474 | + dummyPadding.refs.add(this); | |
| 475 | + return this.#e.widget; | |
| 476 | + } | |
| 477 | + | |
| 478 | + get editorContent(){ | |
| 479 | + /* We wrap access to the editor's contents in a getter/setter so | |
| 480 | + that we can eventually add optional use of a contenteditable | |
| 481 | + edit field, as those are generally more comfortable. The code | |
| 482 | + for that is in fossil.page.chat.js. */ | |
| 483 | + return this.#e.editor.value; | |
| 484 | + } | |
| 485 | + | |
| 486 | + set editorContent(v){ | |
| 487 | + this.#e.editor.value = v; | |
| 488 | + } | |
| 489 | + | |
| 490 | + /** | |
| 491 | + Reports an error by appending each argument to the error widget | |
| 492 | + and unhiding it. If passed no arugments, it clears and hides | |
| 493 | + the error widget. | |
| 494 | + */ | |
| 495 | + reportError(...msg){ | |
| 496 | + const e = this.#e.error; | |
| 497 | + D.clearElement(e); | |
| 498 | + if( msg.length ){ | |
| 499 | + console.error('ForumPostEditor:',...msg); | |
| 500 | + e.classList.remove('hidden'); | |
| 501 | + e.append( | |
| 502 | + ...msg, D.br(), | |
| 503 | + D.button("Clear", ()=>this.reportError()) | |
| 504 | + /* Looks horrid in the Blitz skin */ | |
| 505 | + ); | |
| 506 | + }else{ | |
| 507 | + e.classList.add('hidden'); | |
| 508 | + } | |
| 509 | + } | |
| 510 | + | |
| 511 | + /** | |
| 512 | + Adds a list of input[type=hidden] form fields to this object, | |
| 513 | + imported from the server-generated HTML. This is used for | |
| 514 | + collecting, e.g., the CSRF token and an initial page title. | |
| 515 | + */ | |
| 516 | + addHiddenFields(list){ | |
| 517 | + this.#extraFields ??= []; | |
| 518 | + for( const f of list ){ | |
| 519 | + if( !f ) continue; | |
| 520 | + if( 'title'===f.name && this.#e.title ){ | |
| 521 | + if( f.value && this.#opt.isNewThread && !this.#e.title.value ){ | |
| 522 | + this.#e.title.value = f.value; | |
| 523 | + } | |
| 524 | + }else{ | |
| 525 | + this.#extraFields.push(f); | |
| 526 | + } | |
| 527 | + } | |
| 528 | + } | |
| 529 | + | |
| 530 | + get mimetype(){ | |
| 531 | + return this.#e.mimetype.select.value; | |
| 532 | + } | |
| 533 | + | |
| 534 | + get title(){ | |
| 535 | + return this.#e.title?.value || this.#opt.edit?.H; | |
| 536 | + } | |
| 537 | + | |
| 538 | + #initHelpTab(){ | |
| 539 | + const eh = this.#e.help; | |
| 540 | + const list = D.ul(); | |
| 541 | + D.append( | |
| 542 | + D.li(list), | |
| 543 | + D.attr(D.a(F.repoUrl('markup_help'), 'Markup styles'), | |
| 544 | + 'target', '_new') | |
| 545 | + ); | |
| 546 | + D.append( | |
| 547 | + D.li(list), | |
| 548 | + "WARNING: draft edits are keyed on the ID of the message they ", | |
| 549 | + "are editing or responding to. Attempting to edit or reply to ", | |
| 550 | + "the same post from multiple tabs will cause the most-recently-edited ", | |
| 551 | + "one to overwrite the draft slot for that post. In browsers which support ", | |
| 552 | + "Web Locks, a second attempt to edit or reply to a post will be blocked ", | |
| 553 | + "and an error will be shown explaining the problem." | |
| 554 | + ); | |
| 555 | + if( this.#e.status ){ | |
| 556 | + D.append( | |
| 557 | + D.li(list), | |
| 558 | + "Tip: changing just the status in the editor will change only that, ", | |
| 559 | + "not a whole new (but unedited) copy of the post." | |
| 560 | + ); | |
| 561 | + } | |
| 562 | + eh.append(list); | |
| 563 | + } | |
| 564 | + | |
| 565 | + #initAttacherTab(){ | |
| 566 | + this.#att = new F.Attacher({ | |
| 567 | + reverse: true | |
| 568 | + }); | |
| 569 | + if( this.#opt.edit ){ | |
| 570 | + const eNote = D.append( | |
| 571 | + D.div(), | |
| 572 | + "Tip: attachments can be added to posts without editing them ", | |
| 573 | + "by visiting ", | |
| 574 | + D.attr( | |
| 575 | + D.a(F.repoUrl('attachadd?target='+this.#opt.edit.uuid), '/attachadd'), | |
| 576 | + 'target', | |
| 577 | + '_new' | |
| 578 | + ), | |
| 579 | + ".", | |
| 580 | + ); | |
| 581 | + this.#e.tabAttach.append(eNote); | |
| 582 | + } | |
| 583 | + this.#e.tabAttach.append(this.#att.widget); | |
| 584 | + } | |
| 585 | + | |
| 586 | + #newFormData(addThisContent){ | |
| 587 | + const fd = new FormData; | |
| 588 | + for(const f of this.#extraFields){ | |
| 589 | + fd.append(f.name, f.value); | |
| 590 | + } | |
| 591 | + let v; | |
| 592 | + if( this.#opt.inReplyTo ){ | |
| 593 | + fd.append( 'firt', this.#opt.inReplyTo ); | |
| 594 | + }else if( (v = (this.#e.title?.value?.trim?.() || this.#opt.edit?.H)) ){ | |
| 595 | + fd.append('title', v); | |
| 596 | + } | |
| 597 | + fd.append('mimetype', this.mimetype); | |
| 598 | + fd.append('content', addThisContent || this.editorContent.trim()); | |
| 599 | + return fd; | |
| 600 | + } | |
| 601 | + | |
| 602 | + async #fetchPreview(content){ | |
| 603 | + /* TODO: fetch preview */ | |
| 604 | + const e = this.#e; | |
| 605 | + const fd = /*no: this.#newFormData(content); */ | |
| 606 | + new FormData; | |
| 607 | + let ext; | |
| 608 | + switch(this.mimetype){ | |
| 609 | + case 'text/x-markdown': ext = 'md'; break; | |
| 610 | + case 'text/x-fossil-wiki': ext = 'wiki'; break; | |
| 611 | + default: ext = 'txt'; break; | |
| 612 | + } | |
| 613 | + fd.append('filename', 'x.'+ext/*for mimetype determination*/); | |
| 614 | + fd.append('content', this.editorContent.trim()); | |
| 615 | + return window | |
| 616 | + .fetch(F.repoUrl('ajax/preview-text'),{ | |
| 617 | + method: 'POST', | |
| 618 | + body: fd | |
| 619 | + }) | |
| 620 | + .then(r=>r.text()) | |
| 621 | + .then(t=>{ | |
| 622 | + if( /^\{.*}$/.test(t) ){ | |
| 623 | + const o = JSON.parse(t); | |
| 624 | + throw new Error(o.error); | |
| 625 | + } | |
| 626 | + return t; | |
| 627 | + }); | |
| 628 | + } | |
| 629 | + | |
| 630 | + #setPreviewContent(rawHtml){ | |
| 631 | + /** | |
| 632 | + Append the new content then remove the old, to help reduce | |
| 633 | + jumping-around of the UI if the preview is cleared then | |
| 634 | + repopulated. | |
| 635 | + */ | |
| 636 | + const preview = this.#e.preview; | |
| 637 | + const childs = [...preview.childNodes]; | |
| 638 | + D.parseHtml(preview, rawHtml); | |
| 639 | + D.remove(childs); | |
| 640 | + //preview.style.removeProperty('height'); | |
| 641 | + if(F.pikchr && 'text/x-markdown'===this.mimetype){ | |
| 642 | + F.pikchr.addSrcView( | |
| 643 | + preview.querySelectorAll('svg.pikchr') | |
| 644 | + ); | |
| 645 | + } | |
| 646 | + } | |
| 647 | + | |
| 648 | + async #preview(){ | |
| 649 | + if( this.#isWaiting ) return; | |
| 650 | + const e = this.#e; | |
| 651 | + if( e.preview !== this.#activeTab ){ | |
| 652 | + this.#tabs.switchToTab(e.preview); | |
| 653 | + /* Will recurse into here */ | |
| 654 | + return; | |
| 655 | + } | |
| 656 | + const content = this.editorContent.trim(); | |
| 657 | + //console.debug("content to preview", content); | |
| 658 | + if( !content ){ | |
| 659 | + return; | |
| 660 | + } | |
| 661 | + if( 0 | |
| 662 | + && !e.preview.firstElementChild ){ | |
| 663 | + /* On an initial first preview, inherit the editor's height to | |
| 664 | + reduce jumping-around of the UI. */ | |
| 665 | + if( 0 /* does not work: height of the editor is "auto" */ ){ | |
| 666 | + const c = window.getComputedStyle(e.editor/*tabEdit*/); | |
| 667 | + e.preview.style.height = c.height; | |
| 668 | + }else{ | |
| 669 | + e.preview.style.height = '20em'; | |
| 670 | + } | |
| 671 | + } | |
| 672 | + this.#isWaiting = true; | |
| 673 | + D.disable(this.#toDisable, e.button.submit); | |
| 674 | + this.#fetchPreview(content) | |
| 675 | + .then((c)=>{ | |
| 676 | + this.#setPreviewContent(c); | |
| 677 | + D.enable(e.button.submit); | |
| 678 | + }) | |
| 679 | + .catch(err=>{ | |
| 680 | + e.preview.textContent = "Error fetching preview: "+err.message; | |
| 681 | + console.error("Error fetching preview:",err); | |
| 682 | + this.reportError(err.message); | |
| 683 | + }) | |
| 684 | + .finally(()=>{ | |
| 685 | + this.#isWaiting = false; | |
| 686 | + D.enable(this.#toDisable); | |
| 687 | + }); | |
| 688 | + } | |
| 689 | + | |
| 690 | + #validate(tgt){ | |
| 691 | + if( this.#e.title ){ | |
| 692 | + const v = this.#e.title.value.trim(); | |
| 693 | + if( !v ){ | |
| 694 | + this.reportError("A non-empty title is required."); | |
| 695 | + return; | |
| 696 | + } | |
| 697 | + } | |
| 698 | + return true; | |
| 699 | + } | |
| 700 | + | |
| 701 | + #submit(){ | |
| 702 | + if( this.#isWaiting ) return; | |
| 703 | + if( !this.#validate() ) return; | |
| 704 | + this.#isWaiting = true; | |
| 705 | + const e = this.#e; | |
| 706 | + D.disable(e.button.submit); | |
| 707 | + const fd = this.#newFormData(); | |
| 708 | + if( this.#e.status ){ | |
| 709 | + /* Send the status only if it was modified, otherwise we may | |
| 710 | + add a superfluous tag. */ | |
| 711 | + const v = this.#e.status.value; | |
| 712 | + if( this.#e.status.dataset.originalValue !== v ){ | |
| 713 | + fd.append("status", v); | |
| 714 | + } | |
| 715 | + } | |
| 716 | + if( e.debug ){ | |
| 717 | + e.debug.querySelectorAll('input[type=checkbox]').forEach(cb=>{ | |
| 718 | + if( cb.checked ){ | |
| 719 | + fd.append(cb.value, 1); | |
| 720 | + //console.debug("Forum post debug option:",cb); | |
| 721 | + } | |
| 722 | + }); | |
| 723 | + } | |
| 724 | + if( this.#att ){ | |
| 725 | + this.#att.populateFormData(fd); | |
| 726 | + } | |
| 727 | + //console.warn("Ready to submit",fd); | |
| 728 | + if( 0 ){ | |
| 729 | + this.#isWaiting = false; | |
| 730 | + return; | |
| 731 | + } | |
| 732 | + const resp = window.fetch(F.repoUrl('forumajax_save'), { | |
| 733 | + method: 'POST', | |
| 734 | + body: fd | |
| 735 | + }).then(r=>r.json()) | |
| 736 | + .then(j=>{ | |
| 737 | + j = F.nu(j); | |
| 738 | + console.debug("forum post editor response:",j); | |
| 739 | + if( j.error ){ | |
| 740 | + throw new Error(j.error); | |
| 741 | + }else if( j.message ){ | |
| 742 | + /* This is only for use in debugging during | |
| 743 | + * development. */ | |
| 744 | + this.reportError(j.message); | |
| 745 | + return; | |
| 746 | + } | |
| 747 | + if( 1 ){ | |
| 748 | + this.#clearDraft(); | |
| 749 | + if( this.#opt.onsubmit instanceof Function ){ | |
| 750 | + try{this.#opt.onsubmit(this, j);} | |
| 751 | + catch(e){ | |
| 752 | + console.error("ForumPostEditor.onsubmit() threw: ", e); | |
| 753 | + } | |
| 754 | + } | |
| 755 | + /* | |
| 756 | + if( this.#opt.edit?.uuid === j.uuid ) then we know the | |
| 757 | + content did not change, but it's possible that attachments | |
| 758 | + and/or a status tag did. Ergo, we need to unconditionally | |
| 759 | + reload to render those changes (if any). The other option | |
| 760 | + is to tell the user "nothing changed" and leave them in | |
| 761 | + the editor, but that could be a lie because we don't know | |
| 762 | + if any attachments or tags were changed. | |
| 763 | + */ | |
| 764 | + else if( 0 ){ | |
| 765 | + if( this.#opt.edit.uuid === j.uuid | |
| 766 | + && !j.statusModified && 0===j.attachedCount ){ | |
| 767 | + this.reportError("No changes made."); | |
| 768 | + }else{ | |
| 769 | + window.location = F.repoUrl('forumpost/'+j.uuid); | |
| 770 | + setTimeout(()=>this.close(), 500/*just in case not redirected*/); | |
| 771 | + } | |
| 772 | + } | |
| 773 | + }else{ | |
| 774 | + this.reportError( | |
| 775 | + "Saving worked but we're ignoring it and staying here." | |
| 776 | + ); | |
| 777 | + } | |
| 778 | + }) | |
| 779 | + .catch((e)=>this.reportError(e.message)) | |
| 780 | + .finally(()=>this.#isWaiting = false); | |
| 781 | + } | |
| 782 | + | |
| 783 | + #storeDraft(){ | |
| 784 | + if( this.#draft ){ | |
| 785 | + this.#draft.mtime = Date.now(); | |
| 786 | + F.storage.setJSON(this.#opt.draftKey, this.#draft); | |
| 787 | + } | |
| 788 | + } | |
| 789 | + | |
| 790 | + /** Clears any persistent draft state. Does not clear the UI | |
| 791 | + widgets. */ | |
| 792 | + #clearDraft(){ | |
| 793 | + if( this.#draft ){ | |
| 794 | + F.storage.remove(this.#opt.draftKey); | |
| 795 | + this.#draft = F.nu(); | |
| 796 | + } | |
| 797 | + } | |
| 798 | + | |
| 799 | + /** | |
| 800 | + Looks for editing draft keys matching either a fixed key or a | |
| 801 | + regex, and removes each matching one which is older than the | |
| 802 | + given number of days. Pass days=0 to purge all entries | |
| 803 | + immediately. | |
| 804 | + */ | |
| 805 | + static purgeOldDrafts(key, days=10){ | |
| 806 | + const age = (3600 * 24 * days) * 1000/*ms*/; | |
| 807 | + const now = Date.now(); | |
| 808 | + const check = (k)=>{ | |
| 809 | + const o = F.storage.getJSON(k); | |
| 810 | + if( o && o.mtime && (!days || (o.mtime+age <= now)) ){ | |
| 811 | + F.storage.remove(k); | |
| 812 | + } | |
| 813 | + }; | |
| 814 | + if( key instanceof RegExp ){ | |
| 815 | + for(const k of F.storage.keys(false).filter(v=>key.test(v))){ | |
| 816 | + check(k); | |
| 817 | + } | |
| 818 | + }else{ | |
| 819 | + check(key); | |
| 820 | + } | |
| 821 | + } | |
| 822 | + | |
| 823 | + async #fetchPost(){ | |
| 824 | + /* | |
| 825 | + TODO: when editing an existing post, fetch the raw body of the | |
| 826 | + post and populate this.e. | |
| 827 | + */ | |
| 828 | + } | |
| 829 | + }/*ForumPostEditor*/; | |
| 830 | + F.ForumPostEditor = ForumPostEditor; | |
| 831 | + | |
| 7 | 832 | /** |
| 8 | 833 | When the page is loaded, this handler does the following: |
| 9 | 834 | |
| 10 | - - Installs expand/collapse UI elements on "long" posts and collapses | |
| 835 | + 1. Installs expand/collapse UI elements on "long" posts and collapses | |
| 11 | 836 | them. |
| 12 | 837 | |
| 13 | - - Any pikchr-generated SVGs get a source-toggle button added to them | |
| 838 | + 2. Any pikchr-generated SVGs get a source-toggle button added to them | |
| 14 | 839 | which activates when the mouse is over the image or it is tapped. |
| 840 | + | |
| 841 | + 3. Plugs in a new edit/reply widget to forum posts. | |
| 15 | 842 | |
| 16 | 843 | This is a harmless no-op if the current page has neither forum |
| 17 | - post constructs for (1) nor any pikchr images for (2), nor will | |
| 18 | - NOT running this code cause any breakage for clients with no JS | |
| 19 | - support: this is all "nice-to-have", not required functionality. | |
| 844 | + post constructs for (1) and (3) nor any pikchr images for (2), | |
| 845 | + nor will NOT running this code cause any breakage for clients | |
| 846 | + with no JS support: this is all "nice-to-have", not required | |
| 847 | + functionality. | |
| 20 | 848 | */ |
| 21 | 849 | F.onPageLoad(function(){ |
| 22 | 850 | const scrollbarIsVisible = (e)=>e.scrollHeight > e.clientHeight; |
| 23 | 851 | /* Returns an event handler which implements the post expand/collapse toggle |
| 24 | 852 | on contentElem when the given widget is activated. */ |
| @@ -105,15 +933,20 @@ | ||
| 105 | 933 | const eStatus = document.querySelector( |
| 106 | 934 | 'form div.submenu select.submenuctrl[name="status"]' |
| 107 | 935 | ); |
| 108 | 936 | if( eStatus ){ |
| 109 | 937 | /* Main /forum list. Remove the 'x' form element when eStatus |
| 110 | - ** changes, to avoid propagating x when changing the filter. */ | |
| 938 | + changes, to avoid propagating x when changing the filter. | |
| 939 | + The problem this solves: we're browsed to page 3 of status X. | |
| 940 | + We change the status filter selection to Y. We're redirected | |
| 941 | + to page x, but Y only has 2 posts with that status, so we see | |
| 942 | + an empty list. When changing the filter, we need to ensure | |
| 943 | + that we start back and that beginning. */ | |
| 111 | 944 | const pForm = eStatus.parentElement?.parentElement; |
| 112 | 945 | if( pForm ){ |
| 113 | 946 | eStatus.addEventListener('change', ()=>{ |
| 114 | - pForm.querySelector('input[type="hidden"][name="x"]')?.remove(); | |
| 947 | + pForm.querySelector('input[type="hidden"][name="x"]')?.remove?.(); | |
| 115 | 948 | }, true); |
| 116 | 949 | } |
| 117 | 950 | }else{ |
| 118 | 951 | /* One of the single-post edit/view pages. Handle various UI |
| 119 | 952 | controls and attempt to keep stray double-clicks from |
| @@ -126,16 +959,21 @@ | ||
| 126 | 959 | return; |
| 127 | 960 | } |
| 128 | 961 | form.dataset.submitted = '1'; |
| 129 | 962 | /** If the user is left waiting "a long time," disable the |
| 130 | 963 | resubmit protection. If we don't do this and they tap the |
| 131 | - browser's cancel button while waiting, they'll be stuck with | |
| 132 | - an unsubmittable form. */ | |
| 964 | + browser's cancel button while waiting, they'll be stuck | |
| 965 | + with an unsubmittable form. It can apparently also happen, | |
| 966 | + via browser-back, that the form gets left in a submitted | |
| 967 | + state. */ | |
| 133 | 968 | setTimeout(()=>{delete form.dataset.submitted}, 7000); |
| 134 | 969 | return; |
| 135 | 970 | }; |
| 971 | + | |
| 136 | 972 | document.querySelectorAll("form").forEach(function(form){ |
| 973 | + /* Set up controls for closing posts and setting thread | |
| 974 | + status. */ | |
| 137 | 975 | form.addEventListener('submit', formSubmitted); |
| 138 | 976 | form |
| 139 | 977 | .querySelectorAll("input.action-close, input.action-reopen") |
| 140 | 978 | .forEach(function(e){ |
| 141 | 979 | e.classList.remove('hidden'); |
| @@ -149,11 +987,13 @@ | ||
| 149 | 987 | form |
| 150 | 988 | .querySelectorAll("input[type='button'].action-status") |
| 151 | 989 | .forEach(function(btn){ |
| 152 | 990 | btn.classList.remove('hidden'); |
| 153 | 991 | const sel = btn.previousElementSibling; |
| 154 | - const updateAble = ()=>{ | |
| 992 | + const updateButton = ()=>{ | |
| 993 | + /* Enable btn only when the status has been locally | |
| 994 | + modified. */ | |
| 155 | 995 | if( sel.dataset.initialValue ){ |
| 156 | 996 | if( sel.dataset.initialValue===sel.value ){ |
| 157 | 997 | btn.setAttribute('disabled',''); |
| 158 | 998 | }else{ |
| 159 | 999 | btn.removeAttribute('disabled'); |
| @@ -164,17 +1004,392 @@ | ||
| 164 | 1004 | }else{ |
| 165 | 1005 | btn.removeAttribute('disabled'); |
| 166 | 1006 | } |
| 167 | 1007 | } |
| 168 | 1008 | }; |
| 169 | - sel.addEventListener('change', updateAble, true); | |
| 170 | - updateAble(); | |
| 1009 | + sel.addEventListener('change', updateButton, true); | |
| 1010 | + updateButton(); | |
| 171 | 1011 | F.confirmer(btn, { |
| 172 | 1012 | confirmText: "Confirm status change", |
| 173 | 1013 | onconfirm: ()=>form.submit() |
| 174 | 1014 | }); |
| 175 | 1015 | }); |
| 176 | 1016 | }); |
| 177 | 1017 | } |
| 178 | 1018 | |
| 1019 | + /* Apply page-specific tweaks for ForumPostEditor instance fpe | |
| 1020 | + then plug it into the UI at the end of ePost. */ | |
| 1021 | + const initFPEWidget = (fpe, ePost)=>{ | |
| 1022 | + ePost.eUnhideThenWhenDone = [ | |
| 1023 | + /* List of elements to hide while editing/replying and reveal | |
| 1024 | + when discarding or saving. */ | |
| 1025 | + ]; | |
| 1026 | + for( const ee of ePost.querySelectorAll( | |
| 1027 | + '.forumpost-single-controls, fieldset.forum-status-selection' | |
| 1028 | + ) ){ | |
| 1029 | + ee.hidden = true; | |
| 1030 | + ePost.eUnhideThenWhenDone.push(ee); | |
| 1031 | + } | |
| 1032 | + const w = fpe.widget; | |
| 1033 | + w.classList.add('animate-entrance'); | |
| 1034 | + ePost.append(w); | |
| 1035 | + requestAnimationFrame(() => { | |
| 1036 | + w.scrollIntoView({ | |
| 1037 | + behavior: 'smooth', | |
| 1038 | + block: 'nearest', | |
| 1039 | + inline: 'nearest' | |
| 1040 | + }); | |
| 1041 | + }); | |
| 1042 | + }; | |
| 1043 | + | |
| 1044 | + const plugInEditor = | |
| 1045 | + (new URL(window.location).searchParams).get('nojs')===null; | |
| 1046 | + | |
| 1047 | + const eForumNew = ( | |
| 1048 | + plugInEditor | |
| 1049 | + && ( | |
| 1050 | + document.body.classList.contains('cpage-forumnew') | |
| 1051 | + || document.body.classList.contains('cpage-forume1') | |
| 1052 | + )) | |
| 1053 | + ? document.body.querySelector('#forumnew-placeholder') | |
| 1054 | + : null; | |
| 1055 | + if( plugInEditor && eForumNew ){ | |
| 1056 | + /* /forumnew and /forume1 */ | |
| 1057 | + const fpe = new F.ForumPostEditor({ | |
| 1058 | + draftKey: 'draft-forumnew', | |
| 1059 | + hiddenFields: eForumNew.querySelectorAll('input[type=hidden]'), | |
| 1060 | + hideStash: true, | |
| 1061 | + ondiscard: ()=>{ | |
| 1062 | + window.location = F.repoUrl('forum'); | |
| 1063 | + }, | |
| 1064 | + onsubmit: (fpe, response)=>{ | |
| 1065 | + window.location = F.repoUrl('forumpost/'+response.uuid); | |
| 1066 | + } | |
| 1067 | + }); | |
| 1068 | + eForumNew.parentElement.insertBefore(fpe.widget, eForumNew); | |
| 1069 | + eForumNew.remove(); | |
| 1070 | + fossil.page.fpe = fpe /* for testing via the console */; | |
| 1071 | + }/*eForumNew*/ | |
| 1072 | + else if( plugInEditor | |
| 1073 | + && (document.body.classList.contains('cpage-forumpost') | |
| 1074 | + || document.body.classList.contains('cpage-forumthread')) ){ | |
| 1075 | + /* /forumpost and /forumthread. Take over the Edit/Reply buttons | |
| 1076 | + to use a ForumPostEditor. */ | |
| 1077 | + | |
| 1078 | + const fetchPost = async (fpid)=>{ | |
| 1079 | + return window.fetch(F.repoUrl('ajax/artifact.json?uuid='+fpid)) | |
| 1080 | + .then(r=>r.json()) | |
| 1081 | + .then(j=>{ | |
| 1082 | + j = F.nu(j); | |
| 1083 | + if( j.error ) throw new Error(j.error); | |
| 1084 | + return j; | |
| 1085 | + }); | |
| 1086 | + }; | |
| 1087 | + | |
| 1088 | + const makeDraftKey = (prefix,uuid)=>{ | |
| 1089 | + return prefix+'-'+uuid.substr(0,12); | |
| 1090 | + }; | |
| 1091 | + | |
| 1092 | + /** | |
| 1093 | + Perform some init common to both Reply and Edit. ePost = the | |
| 1094 | + forum post DOM element. eButton = the Reply or Edit | |
| 1095 | + button. | |
| 1096 | + */ | |
| 1097 | + const setupEditReplyElement = (ePost, eButton)=>{ | |
| 1098 | + /* Forum posts are indented in the main forum view to | |
| 1099 | + represent their place in the hierarchy. In order to gain | |
| 1100 | + some screen space, we shift the post to the left margin and | |
| 1101 | + arrange to shift it back when the editor is closed. We also | |
| 1102 | + record the original button label so that it can be | |
| 1103 | + restored on close. */ | |
| 1104 | + ePost.dataset.originalMarginLeft = ePost.style.marginLeft; | |
| 1105 | + ePost.style.marginLeft = 'initial'; | |
| 1106 | + eButton.dataset.originalLabel = eButton.innerText; | |
| 1107 | + }; | |
| 1108 | + | |
| 1109 | + /** Undoes the damage done by setupEditReplyElement(). */ | |
| 1110 | + const restoreEditReplyElement = (ePost, eButton)=>{ | |
| 1111 | + if( ePost.dataset.originalMarginLeft ){ | |
| 1112 | + ePost.style.marginLeft = ePost.dataset.originalMarginLeft; | |
| 1113 | + delete ePost.dataset.originalMarginLeft; | |
| 1114 | + } | |
| 1115 | + if( eButton.dataset.originalLabel ){ | |
| 1116 | + eButton.innerText = eButton.dataset.originalLabel; | |
| 1117 | + delete eButton.dataset.originalLabel; | |
| 1118 | + } | |
| 1119 | + for(const ee of (ePost.eUnhideThenWhenDone || [])){ | |
| 1120 | + ee.removeAttribute('hidden'); | |
| 1121 | + } | |
| 1122 | + ePost.eUnhideThenWhenDone = undefined; | |
| 1123 | + }; | |
| 1124 | + | |
| 1125 | + /** | |
| 1126 | + Reports an error regarding the forum post element | |
| 1127 | + ePost, appending each entry in msg to a wrapper | |
| 1128 | + element with the class | |
| 1129 | + */ | |
| 1130 | + const reportFPEError = (ePost,...msg)=>{ | |
| 1131 | + const e = D.addClass(D.p(), 'error'); | |
| 1132 | + e.append( | |
| 1133 | + ...msg, | |
| 1134 | + D.br(), | |
| 1135 | + D.button("Clear error", ()=>e.remove()) | |
| 1136 | + ); | |
| 1137 | + ePost.append(e); | |
| 1138 | + }; | |
| 1139 | + | |
| 1140 | + /** | |
| 1141 | + Plug in an editor widget representing a reply to a post. | |
| 1142 | + form = a (.forum-post-single-controls > form) element. The | |
| 1143 | + final 3 arguments are as documented for | |
| 1144 | + setupEditReplyElement(). | |
| 1145 | + */ | |
| 1146 | + const replyClicked = async (form, ePost, eBtnReply)=>{ | |
| 1147 | + const fpid = ePost.dataset.fpid; | |
| 1148 | + const fEditHead = ePost.dataset.fedithead; | |
| 1149 | + const draftKey = makeDraftKey( | |
| 1150 | + 'draft-reply', fEditHead | |
| 1151 | + /* The problem with firt as a key is that firt is not | |
| 1152 | + necessarily the root edit of that post, which is what we | |
| 1153 | + really want as a draft key so that the draft does not | |
| 1154 | + disappear if firt is later edited (giving us a new firt | |
| 1155 | + value here). */ | |
| 1156 | + || fpid | |
| 1157 | + ); | |
| 1158 | + let releaseLock; | |
| 1159 | + if( window.navigator.locks ){ | |
| 1160 | + releaseLock = await new Promise((resolve)=>{ | |
| 1161 | + window.navigator.locks.request( | |
| 1162 | + 'fossil-'+draftKey, | |
| 1163 | + {ifAvailable: true}, | |
| 1164 | + async (lock) => { | |
| 1165 | + if( !lock ){ | |
| 1166 | + /*lock contention*/ | |
| 1167 | + resolve(null); | |
| 1168 | + return; | |
| 1169 | + } | |
| 1170 | + let release; | |
| 1171 | + const lockReleased = new Promise(res=>release=res); | |
| 1172 | + resolve(release); | |
| 1173 | + await lockReleased/*hold the lock open*/; | |
| 1174 | + }); | |
| 1175 | + }); | |
| 1176 | + if( !releaseLock ){ | |
| 1177 | + reportFPEError( | |
| 1178 | + ePost, | |
| 1179 | + "This post is actively being replied to ", | |
| 1180 | + "in another tab. To avoid losing edits, ", | |
| 1181 | + "it cannot be opened here until the locking ", | |
| 1182 | + "tab is closed." | |
| 1183 | + ); | |
| 1184 | + return; | |
| 1185 | + } | |
| 1186 | + } | |
| 1187 | + | |
| 1188 | + setupEditReplyElement(ePost, eBtnReply); | |
| 1189 | + eBtnReply.innerText = "Replying..."; | |
| 1190 | + const ondone = (fpe, response)=>{ | |
| 1191 | + /* onsubmit() and ondiscard() callback */ | |
| 1192 | + restoreEditReplyElement(ePost, eBtnReply); | |
| 1193 | + //console.debug("ondiscard/onsubmit", fpe, artifact); | |
| 1194 | + if( response/*onsubmit()*/ ){ | |
| 1195 | + window.location = F.repoUrl('forumpost/'+response.uuid); | |
| 1196 | + setTimeout(()=>fpe.close(), 500/*just in case not redirected*/); | |
| 1197 | + }else{/*ondiscard() or onclose()*/ | |
| 1198 | + } | |
| 1199 | + }; | |
| 1200 | + const fpe = new F.ForumPostEditor(F.nu({ | |
| 1201 | + hiddenFields: form.querySelectorAll( | |
| 1202 | + 'input[type=hidden][name=csrf]' | |
| 1203 | + /* Do not inherit the fpid field, else this will become | |
| 1204 | + an edit to that post rather than a response. */ | |
| 1205 | + ), | |
| 1206 | + ondiscard: ()=>{/*need a noop here. Will call onclose()*/}, | |
| 1207 | + onsubmit: ondone, | |
| 1208 | + onclose: ()=>{ | |
| 1209 | + if( releaseLock ){ | |
| 1210 | + releaseLock(); | |
| 1211 | + releaseLock = null; | |
| 1212 | + } | |
| 1213 | + ondone(); | |
| 1214 | + }, | |
| 1215 | + inReplyTo: fpid, | |
| 1216 | + draftKey | |
| 1217 | + })); | |
| 1218 | + initFPEWidget(fpe, ePost); | |
| 1219 | + }/*replyClicked()*/; | |
| 1220 | + | |
| 1221 | + /** | |
| 1222 | + Plug in an editor widget representing an edit to a post. | |
| 1223 | + form = a (.forum-post-single-controls > form) element. The | |
| 1224 | + final 3 arguments are as documented for | |
| 1225 | + setupEditReplyElement(). | |
| 1226 | + */ | |
| 1227 | + const editClicked = async (form, ePost, eBtnEdit)=>{ | |
| 1228 | + const fpid = ePost.dataset.fpid; | |
| 1229 | + const firt = ePost.dataset.firt; | |
| 1230 | + const fEditHead = ePost.dataset.fedithead; | |
| 1231 | + const draftKey = makeDraftKey('draft-forumedit', fEditHead || fpid); | |
| 1232 | + let releaseLock; | |
| 1233 | + if( navigator.locks ){ | |
| 1234 | + releaseLock = await new Promise((resolve) => { | |
| 1235 | + navigator.locks.request( | |
| 1236 | + 'fossil-'+draftKey, | |
| 1237 | + {ifAvailable: true}, | |
| 1238 | + async (lock)=>{ | |
| 1239 | + if( !lock ){ | |
| 1240 | + resolve(null); | |
| 1241 | + return; | |
| 1242 | + } | |
| 1243 | + let release; | |
| 1244 | + const lockReleased = new Promise(res=>release=res); | |
| 1245 | + resolve(release); | |
| 1246 | + await lockReleased; | |
| 1247 | + }); | |
| 1248 | + }); | |
| 1249 | + | |
| 1250 | + if( !releaseLock ){ | |
| 1251 | + reportFPEError( | |
| 1252 | + ePost, | |
| 1253 | + "This post is actively being edited ", | |
| 1254 | + "in another tab. To avoid losing edits, ", | |
| 1255 | + "it cannot be opened here until the locking ", | |
| 1256 | + "tab is closed." | |
| 1257 | + ); | |
| 1258 | + return; | |
| 1259 | + } | |
| 1260 | + } | |
| 1261 | + setupEditReplyElement(ePost, eBtnEdit); | |
| 1262 | + eBtnEdit.innerText = "Editing..."; | |
| 1263 | + fetchPost(fpid) | |
| 1264 | + .then(artifact=>{ | |
| 1265 | + const ondone = (fpe, response)=>{ | |
| 1266 | + /* onsubmit() and ondiscard() callback */ | |
| 1267 | + if( response/*onsubmit()*/ ){ | |
| 1268 | + if( fpid === response.uuid | |
| 1269 | + && !response.statusModified | |
| 1270 | + && 0===response.attachedCount ){ | |
| 1271 | + fpe.reportError("No changes made."); | |
| 1272 | + }else{ | |
| 1273 | + restoreEditReplyElement(ePost, eBtnEdit); | |
| 1274 | + window.location = F.repoUrl('forumpost/'+response.uuid); | |
| 1275 | + setTimeout(()=>fpe.close(), 500/*just in case not redirected*/); | |
| 1276 | + } | |
| 1277 | + }else{ | |
| 1278 | + /*ondiscard() or onclose()*/ | |
| 1279 | + restoreEditReplyElement(ePost, eBtnEdit); | |
| 1280 | + } | |
| 1281 | + }; | |
| 1282 | + const eStatusSelect = ePost.querySelector( | |
| 1283 | + ':scope > fieldset.forum-status-selection select[name=status]' | |
| 1284 | + ); | |
| 1285 | + | |
| 1286 | + const fpe = new F.ForumPostEditor(F.nu({ | |
| 1287 | + hiddenFields: form.querySelectorAll('input[type=hidden]'), | |
| 1288 | + ondiscard: ()=>{/*need a noop here. Will call onclose()*/}, | |
| 1289 | + onsubmit: ondone, | |
| 1290 | + onclose: ()=>{ | |
| 1291 | + if(releaseLock){ | |
| 1292 | + releaseLock(); | |
| 1293 | + releaseLock = null; | |
| 1294 | + } | |
| 1295 | + ondone(); | |
| 1296 | + }, | |
| 1297 | + draftKey, | |
| 1298 | + edit: artifact, | |
| 1299 | + status: eStatusSelect?.value, | |
| 1300 | + inReplyTo: firt | |
| 1301 | + })); | |
| 1302 | + initFPEWidget(fpe, ePost); | |
| 1303 | + }) | |
| 1304 | + .catch(err=>{ | |
| 1305 | + if( releaseLock ){ | |
| 1306 | + releaseLock(); | |
| 1307 | + releaseLock = null; | |
| 1308 | + } | |
| 1309 | + restoreEditReplyElement(ePost, eBtnEdit); | |
| 1310 | + console.error("Error fetching post:", err); | |
| 1311 | + reportFPEError(ePost, "Error fetching post: ", err.message); | |
| 1312 | + }); | |
| 1313 | + }/*editClicked()*/; | |
| 1314 | + | |
| 1315 | + document.body.querySelectorAll( | |
| 1316 | + '.forumpost-single-controls > form' | |
| 1317 | + ).forEach(form=>{ | |
| 1318 | + /* For each forum post... */ | |
| 1319 | + const eThePost = form.parentElement.parentElement/*main post DOM element*/; | |
| 1320 | + if( !eThePost?.dataset?.fpid ){ | |
| 1321 | + /* The server injects these dataset values. */ | |
| 1322 | + console.warn("Unexpected missing fpid", eThePost); | |
| 1323 | + return; | |
| 1324 | + } | |
| 1325 | + const checkButtonForDraft = (draftKeyPrefix, eBtn)=>{ | |
| 1326 | + /* If a draft is found associated with eThePost, mark eBtn | |
| 1327 | + as a draft and set up storage event listeners to update | |
| 1328 | + the button as new drafts come and go. */ | |
| 1329 | + const fpid = eThePost.dataset.fpid; | |
| 1330 | + const fEditHead = eThePost.dataset.fedithead; | |
| 1331 | + const draftKey = makeDraftKey(draftKeyPrefix, fEditHead || fpid); | |
| 1332 | + if( F.storage.contains(draftKey) ){ | |
| 1333 | + eBtn.classList.add('draft'); | |
| 1334 | + } | |
| 1335 | + F.storage.addEventListener('set', ({detail})=>{ | |
| 1336 | + if( draftKey === detail.key ){ | |
| 1337 | + eBtn.classList.add('draft'); | |
| 1338 | + } | |
| 1339 | + }); | |
| 1340 | + F.storage.addEventListener('remove', ({detail})=>{ | |
| 1341 | + if( draftKey === detail.key ){ | |
| 1342 | + eBtn.classList.remove('draft'); | |
| 1343 | + } | |
| 1344 | + }); | |
| 1345 | + }; | |
| 1346 | + /* Replace the Reply and Edit buttons with ones which will activate | |
| 1347 | + a ForumPostEditor. */ | |
| 1348 | + const btnReply = form.querySelector('input[type=submit][name=reply]'); | |
| 1349 | + if( btnReply ){ | |
| 1350 | + const b = D.button("Reply", ()=>replyClicked(form, eThePost, b)); | |
| 1351 | + b.type = 'button'/*keep container form from submitting*/; | |
| 1352 | + checkButtonForDraft('draft-reply',b); | |
| 1353 | + btnReply.parentElement.insertBefore(b, btnReply); | |
| 1354 | + btnReply.remove(); | |
| 1355 | + } | |
| 1356 | + const btnEdit = form.querySelector('input[type=submit][name=edit]'); | |
| 1357 | + if( btnEdit ){ | |
| 1358 | + const b = D.button("Edit", ()=>editClicked(form, eThePost, b)); | |
| 1359 | + b.type = 'button'/*keep container form from submitting*/; | |
| 1360 | + checkButtonForDraft('draft-forumedit',b); | |
| 1361 | + btnEdit.parentElement.insertBefore(b, btnEdit); | |
| 1362 | + btnEdit.remove(); | |
| 1363 | + } | |
| 1364 | + })/*for-each form*/; | |
| 1365 | + | |
| 1366 | + }/* /forumpost and /forumthread */ | |
| 1367 | + | |
| 1368 | + document.body.querySelectorAll('.remove-on-load').forEach(e=>e.remove()); | |
| 1369 | + document.body.querySelectorAll('.initially-hidden').forEach(e=>{ | |
| 1370 | + /* This is a workaround for a span.help-buttonlet which we need | |
| 1371 | + to start hidden so that it does not show up for no-JS | |
| 1372 | + clients. */ | |
| 1373 | + e.classList.remove('initially-hidden'); | |
| 1374 | + }); | |
| 1375 | + | |
| 1376 | + if( plugInEditor ){ | |
| 1377 | + document.body.querySelectorAll('.remove-if-replaced').forEach( | |
| 1378 | + /* Remove remaining legacy UI elements. */ e=>e.remove() | |
| 1379 | + ); | |
| 1380 | + /* Purge old drafts only every now and then. */ | |
| 1381 | + const now = Date.now(); | |
| 1382 | + const lastPurge = +F.storage.get('forum-drafts-last-purge', 0); | |
| 1383 | + if( now - lastPurge > (24 * 60 * 60 * 1000 /*1 day ms*/) ){ | |
| 1384 | + F.storage.set('forum-drafts-last-purge', now); | |
| 1385 | + setTimeout(()=>{ | |
| 1386 | + /* Don't block the UI while we're doing I/O */ | |
| 1387 | + F.ForumPostEditor.purgeOldDrafts( | |
| 1388 | + /^draft-(reply|forumedit)-.*/ | |
| 1389 | + /* Intentionally leaving draft-forumnew in place. */ | |
| 1390 | + ); | |
| 1391 | + }, 50); | |
| 1392 | + } | |
| 1393 | + } | |
| 179 | 1394 | })/*F.onPageLoad callback*/; |
| 180 | 1395 | })(window.fossil); |
| 181 | 1396 |
| --- src/fossil.page.forumpost.js | |
| +++ src/fossil.page.forumpost.js | |
| @@ -1,24 +1,852 @@ | |
| 1 | (function(F/*the fossil object*/){ |
| 2 | "use strict"; |
| 3 | /* JS code for /forumpost and friends. Requires fossil.dom |
| 4 | and can optionally use fossil.pikchr. */ |
| 5 | const P = F.page, D = F.dom; |
| 6 | |
| 7 | /** |
| 8 | When the page is loaded, this handler does the following: |
| 9 | |
| 10 | - Installs expand/collapse UI elements on "long" posts and collapses |
| 11 | them. |
| 12 | |
| 13 | - Any pikchr-generated SVGs get a source-toggle button added to them |
| 14 | which activates when the mouse is over the image or it is tapped. |
| 15 | |
| 16 | This is a harmless no-op if the current page has neither forum |
| 17 | post constructs for (1) nor any pikchr images for (2), nor will |
| 18 | NOT running this code cause any breakage for clients with no JS |
| 19 | support: this is all "nice-to-have", not required functionality. |
| 20 | */ |
| 21 | F.onPageLoad(function(){ |
| 22 | const scrollbarIsVisible = (e)=>e.scrollHeight > e.clientHeight; |
| 23 | /* Returns an event handler which implements the post expand/collapse toggle |
| 24 | on contentElem when the given widget is activated. */ |
| @@ -105,15 +933,20 @@ | |
| 105 | const eStatus = document.querySelector( |
| 106 | 'form div.submenu select.submenuctrl[name="status"]' |
| 107 | ); |
| 108 | if( eStatus ){ |
| 109 | /* Main /forum list. Remove the 'x' form element when eStatus |
| 110 | ** changes, to avoid propagating x when changing the filter. */ |
| 111 | const pForm = eStatus.parentElement?.parentElement; |
| 112 | if( pForm ){ |
| 113 | eStatus.addEventListener('change', ()=>{ |
| 114 | pForm.querySelector('input[type="hidden"][name="x"]')?.remove(); |
| 115 | }, true); |
| 116 | } |
| 117 | }else{ |
| 118 | /* One of the single-post edit/view pages. Handle various UI |
| 119 | controls and attempt to keep stray double-clicks from |
| @@ -126,16 +959,21 @@ | |
| 126 | return; |
| 127 | } |
| 128 | form.dataset.submitted = '1'; |
| 129 | /** If the user is left waiting "a long time," disable the |
| 130 | resubmit protection. If we don't do this and they tap the |
| 131 | browser's cancel button while waiting, they'll be stuck with |
| 132 | an unsubmittable form. */ |
| 133 | setTimeout(()=>{delete form.dataset.submitted}, 7000); |
| 134 | return; |
| 135 | }; |
| 136 | document.querySelectorAll("form").forEach(function(form){ |
| 137 | form.addEventListener('submit', formSubmitted); |
| 138 | form |
| 139 | .querySelectorAll("input.action-close, input.action-reopen") |
| 140 | .forEach(function(e){ |
| 141 | e.classList.remove('hidden'); |
| @@ -149,11 +987,13 @@ | |
| 149 | form |
| 150 | .querySelectorAll("input[type='button'].action-status") |
| 151 | .forEach(function(btn){ |
| 152 | btn.classList.remove('hidden'); |
| 153 | const sel = btn.previousElementSibling; |
| 154 | const updateAble = ()=>{ |
| 155 | if( sel.dataset.initialValue ){ |
| 156 | if( sel.dataset.initialValue===sel.value ){ |
| 157 | btn.setAttribute('disabled',''); |
| 158 | }else{ |
| 159 | btn.removeAttribute('disabled'); |
| @@ -164,17 +1004,392 @@ | |
| 164 | }else{ |
| 165 | btn.removeAttribute('disabled'); |
| 166 | } |
| 167 | } |
| 168 | }; |
| 169 | sel.addEventListener('change', updateAble, true); |
| 170 | updateAble(); |
| 171 | F.confirmer(btn, { |
| 172 | confirmText: "Confirm status change", |
| 173 | onconfirm: ()=>form.submit() |
| 174 | }); |
| 175 | }); |
| 176 | }); |
| 177 | } |
| 178 | |
| 179 | })/*F.onPageLoad callback*/; |
| 180 | })(window.fossil); |
| 181 |
| --- src/fossil.page.forumpost.js | |
| +++ src/fossil.page.forumpost.js | |
| @@ -1,24 +1,852 @@ | |
| 1 | /** |
| 2 | Code for the forum family of pages. Requires fossil.X where X is |
| 3 | (copybutton, pikchr, confirmer, attach, tabs, storage). |
| 4 | */ |
| 5 | (function(F/*the fossil object*/){ |
| 6 | "use strict"; |
| 7 | /* JS code for /forumpost and friends. Requires fossil.dom |
| 8 | and can optionally use fossil.pikchr. */ |
| 9 | const P = F.page, D = F.dom; |
| 10 | |
| 11 | let idCounter = 0; |
| 12 | |
| 13 | /* |
| 14 | The problem: when previewing the bottom-most post of a thread, the |
| 15 | preview widget's size changes cause the page to scroll |
| 16 | unpredictably as the bottom boundary of the page moves. A weird |
| 17 | workaround (not invented here) is to add dummy blank padding to |
| 18 | the page to allow the preview widget to grow and shrink without |
| 19 | (usually) scrolling, but whether it does so really depends on its |
| 20 | size. |
| 21 | |
| 22 | We could maybe get the same effect by adding this size as |
| 23 | padding-bottom to document.body instead of as a new element. |
| 24 | */ |
| 25 | const dummyPadding = D.div(); |
| 26 | dummyPadding.style.height = '75em'; |
| 27 | /* Keep track of ForumPostEditor instances so we can remove this |
| 28 | padding when none are active. */ |
| 29 | dummyPadding.refs = new Set(); |
| 30 | F.dummyPadding = dummyPadding /* only for debugging */; |
| 31 | |
| 32 | /** |
| 33 | A forum post editor widget for new posts and responses. |
| 34 | */ |
| 35 | class ForumPostEditor { |
| 36 | /* Options */ |
| 37 | #opt; |
| 38 | /* Dom elements */ |
| 39 | #e; |
| 40 | /* F.Attacher instance */ |
| 41 | #att; |
| 42 | /* Is waiting on a pending remote response. */ |
| 43 | #isWaiting = false; |
| 44 | /* F.TabManager */ |
| 45 | #tabs; |
| 46 | /* Elements to disable while an XHR is pending. */ |
| 47 | #toDisable = []; |
| 48 | /* DOM element of the current active tab. */ |
| 49 | #activeTab; |
| 50 | /* Extra input[type=hidden] fields imported from fossil's |
| 51 | static page generation. */ |
| 52 | #extraFields; |
| 53 | /* Persistent draft message object. */ |
| 54 | #draft; |
| 55 | |
| 56 | /** |
| 57 | Options: |
| 58 | |
| 59 | opt.draftKey[string=undefined]: if set then this object's state |
| 60 | will be stored in fossil.storage when the relevant input fields |
| 61 | lose focus. If old state is found, the form is pre-populated |
| 62 | from it. The state is cleared on a discard() or successful |
| 63 | submit. |
| 64 | |
| 65 | opt.ondiscard[=function]: if set, a Discard button is added |
| 66 | which, when activated, clears the current draft and removes |
| 67 | this object's widget from the DOM. After doing so, |
| 68 | opt.ondiscard() is called and passed this object. Exceptions |
| 69 | thrown by ondiscard() are ignored but may be logged. |
| 70 | |
| 71 | opt.onsubmit[=function]: if set, this function is called |
| 72 | immediately after the post has been successfully saved, and |
| 73 | passed this object and a JSON-format response object from the |
| 74 | save request. It is generally then up to the caller to close() |
| 75 | this object and/or redirect to /forumpost/${arguments[1].uuid}. |
| 76 | |
| 77 | opt.onclose[=function]: like opt.onsubmit, this function is |
| 78 | called when this.close() is called, and passed no arguments. |
| 79 | onclose() is called before the widget is removed from the dom |
| 80 | and _does not_ fire if it is not in the DOM. |
| 81 | |
| 82 | opt.hiddenFields: an optional list of input elements to |
| 83 | incorporate into the form for requests which request the |
| 84 | preview or save the post. |
| 85 | |
| 86 | opt.inReplyTo=uuid: if this is a response to a post, this |
| 87 | is the full forum post uuid of the being-replied-to post. |
| 88 | |
| 89 | opt.edit=artifactObject: if this is an edit of an existing |
| 90 | post, this is the full JSON-format artifact of the forum post |
| 91 | the being-edited post, as returned by /ajax/artifact.json. |
| 92 | |
| 93 | opt.status: optional current status tag value for opt.edit, |
| 94 | if known. This is used for pre-selecting a status value. |
| 95 | |
| 96 | opt.hideStash[bool=false]: if true, the "Stash" button does not |
| 97 | get added. Intended for use with /forumnew. |
| 98 | */ |
| 99 | constructor(opt){ |
| 100 | opt = this.#opt = F.nu({ |
| 101 | draftKey: undefined, |
| 102 | hideStash: false |
| 103 | }, opt); |
| 104 | opt.isNewThread = !opt.inReplyTo && !opt.edit; |
| 105 | if( opt.draftKey ){ |
| 106 | this.#draft = F.nu(F.storage.getJSON(opt.draftKey, {})); |
| 107 | } |
| 108 | const e = this.#e = F.nu({ |
| 109 | mimetype: F.nu(), |
| 110 | button: F.nu() |
| 111 | }); |
| 112 | //console.debug("Setting up FPE opt =",opt); |
| 113 | const wrapper = e.widget = D.addClass(D.div(), 'ForumPostEditor'); |
| 114 | D.clearElement(wrapper); |
| 115 | |
| 116 | if( !opt.inReplyTo ){ |
| 117 | /* Title... */ |
| 118 | e.titleBar = D.addClass(D.div(),'titlebar'); |
| 119 | e.title = D.attr( |
| 120 | D.addClass(D.input('text'), 'title'), |
| 121 | 'placeholder', |
| 122 | 'Thread title (required)' |
| 123 | ); |
| 124 | e.title.setAttribute('maxlength', 125); |
| 125 | e.titleBar.append( |
| 126 | D.append(D.span(), "Title:"), |
| 127 | e.title |
| 128 | ); |
| 129 | if( this.#draft ){ |
| 130 | e.title.addEventListener('blur', ()=>{ |
| 131 | this.#draft.title = e.title.value; |
| 132 | this.#storeDraft(); |
| 133 | }); |
| 134 | e.title.value = this.#draft.title || opt.edit?.H || ''; |
| 135 | }else if( opt.edit?.H ){ |
| 136 | e.title.value = opt.edit.H; |
| 137 | } |
| 138 | wrapper.append(e.titleBar); |
| 139 | } |
| 140 | |
| 141 | { /* Mimetype... */ |
| 142 | e.mimetype.wrapper = D.addClass(D.div(), 'mimetype-wrapper'); |
| 143 | const sel = e.mimetype.select = D.addClass(D.select(), 'mimetype-select'); |
| 144 | sel.setAttribute('title', 'Markup format for this post.'); |
| 145 | this.#toDisable.push(sel); |
| 146 | let i = 0; |
| 147 | D.option(sel, '', '- Markup format -').disabled = true; |
| 148 | for(const [k,v] of Object.entries({ |
| 149 | 'text/x-markdown': 'Markdown', |
| 150 | 'text/x-fossil-wiki': 'Fossil Wiki', |
| 151 | 'text/plain': 'Plain text' |
| 152 | })) { |
| 153 | D.option(sel, k, v); |
| 154 | } |
| 155 | sel.value = opt.mimetype |
| 156 | || this.#draft?.mimetype |
| 157 | || F.storage.get('forum-mimetype', sel.options[1].value); |
| 158 | sel.addEventListener('change',ev=>{ |
| 159 | if( this.#draft && this.#draft.mimetype!==ev.target.value ){ |
| 160 | this.#draft.mimetype = ev.target.value; |
| 161 | this.#storeDraft(); |
| 162 | } |
| 163 | F.storage.set('forum-mimetype', ev.target.value); |
| 164 | }); |
| 165 | e.mimetype.wrapper.append(sel); |
| 166 | } |
| 167 | |
| 168 | e.buttons = D.addClass(D.div(), 'buttons'); |
| 169 | { /* Preview/submit buttons... */ |
| 170 | e.button.preview = D.attr( |
| 171 | D.button("Preview", e=>this.#preview()), |
| 172 | 'title', |
| 173 | 'Preview your edits.' |
| 174 | ); |
| 175 | e.button.submit = D.attr( |
| 176 | D.button("Submit"), |
| 177 | 'title', |
| 178 | 'Save any edits to the server. Not permitted until Preview has been used.' |
| 179 | ); |
| 180 | if( this.#draft && !opt.hideStash ){ |
| 181 | e.button.stash = D.attr( |
| 182 | D.button( |
| 183 | "Stash", e=>this.close() |
| 184 | /* This could be called Close, but that would semantically |
| 185 | collide with the Close [this post] button. All "Stash" |
| 186 | does is close the widget. */ |
| 187 | ), |
| 188 | 'title', "Close this editor and stash any edits locally." |
| 189 | ); |
| 190 | } |
| 191 | if( opt.ondiscard instanceof Function ){ |
| 192 | e.button.discard = D.attr( |
| 193 | D.button('Discard'), |
| 194 | 'title', |
| 195 | 'Close the editor and discard all local edits.' |
| 196 | ); |
| 197 | } |
| 198 | if( 1 ){ |
| 199 | F.confirmer(e.button.submit, { |
| 200 | confirmText: "Confirm submit...", |
| 201 | onconfirm: ()=>this.#submit() |
| 202 | }); |
| 203 | if( e.button.discard ){ |
| 204 | F.confirmer(e.button.discard, { |
| 205 | confirmText: "Really discard?", |
| 206 | onconfirm: ()=>this.discard() |
| 207 | }); |
| 208 | } |
| 209 | }else{ |
| 210 | e.button.submit.addEventListener('click', ()=>this.#submit()); |
| 211 | if( e.button.discard ){ |
| 212 | e.button.submit.addEventListener('click', ()=>this.discard()); |
| 213 | } |
| 214 | } |
| 215 | e.button.submit.setAttribute('disabled', ''); |
| 216 | wrapper.append(e.buttons); |
| 217 | |
| 218 | e.error = D.addClass(D.div(), 'error', 'hidden'); |
| 219 | wrapper.append(e.error); |
| 220 | e.error.addEventListener('dblclick',()=>this.reportError()); |
| 221 | } |
| 222 | |
| 223 | const idPrefix = 'FormPostEditor'+(++idCounter)/* TabManager requires IDs */; |
| 224 | { /* Main tabs... */ |
| 225 | e.tabs = D.attr( |
| 226 | D.addClass(D.div(), 'tab-container'), |
| 227 | 'id', idPrefix+'-tabs' |
| 228 | ); |
| 229 | this.#tabs = new F.TabManager(e.tabs); |
| 230 | this.#tabs.addEventListener('before-switch-to', (ev)=>{ |
| 231 | //console.debug("Switching to tab",ev.detail); |
| 232 | switch( (this.#activeTab = ev.detail) ){ |
| 233 | case e.preview: |
| 234 | this.#e.button.preview.click(); |
| 235 | break; |
| 236 | case e.help: |
| 237 | if( e.help.$needsInit ){ |
| 238 | delete e.help.$needsInit; |
| 239 | this.#initHelpTab(); |
| 240 | } |
| 241 | break; |
| 242 | case e.tabAttach: |
| 243 | if( !this.#att ) this.#initAttacherTab(); |
| 244 | break; |
| 245 | } |
| 246 | }); |
| 247 | wrapper.append( e.tabs ); |
| 248 | |
| 249 | e.tabEdit = D.div(); |
| 250 | e.tabEdit.classList.add('editor-wrapper'); |
| 251 | e.editor = D.attr( |
| 252 | D.addClass(D.textarea(), 'editor'), |
| 253 | 'placeholder', |
| 254 | 'Your message to other forum-goers...' |
| 255 | ); |
| 256 | e.tabEdit.append(e.editor); |
| 257 | e.tabEdit.dataset.tabLabel = (opt.edit || !opt.inReplyTo) |
| 258 | ? 'Edit' : 'Reply'; |
| 259 | this.#tabs.addTab( e.tabEdit ); |
| 260 | this.#tabs.switchToTab( e.tabEdit ); |
| 261 | if( this.#draft ){ |
| 262 | this.editorContent = this.#draft.content || opt.edit?.W || ''; |
| 263 | e.editor.addEventListener( |
| 264 | 'blur', ()=>{ |
| 265 | this.#draft.content = this.editorContent; |
| 266 | this.#storeDraft(); |
| 267 | } |
| 268 | ); |
| 269 | }else if( opt.edit?.W ){ |
| 270 | this.editorContent = opt.artifact.W; |
| 271 | } |
| 272 | e.preview = D.addClass(D.div(), 'preview'); |
| 273 | e.preview.dataset.tabLabel = 'Preview'; |
| 274 | this.#toDisable.push(e.button.preview); |
| 275 | this.#tabs.addTab( e.preview ); |
| 276 | } |
| 277 | |
| 278 | if( F.user.enableDebug ){ |
| 279 | e.debug = D.addClass(D.div(), 'debug'); |
| 280 | e.debug.dataset.tabLabel = 'Debug'; |
| 281 | e.debug.setAttribute('id', idPrefix+'-debug'); |
| 282 | for(const [k,v] of Object.entries({ |
| 283 | dryrun: 'Dry run', |
| 284 | domod: 'Require moderation approval', |
| 285 | //showqp: 'Show query parameters', |
| 286 | fpsilent: 'Do not send notification emails' |
| 287 | })){ |
| 288 | const lbl = D.label(false, v); |
| 289 | lbl.prepend(D.checkbox(k)); |
| 290 | e.debug.append(lbl); |
| 291 | } |
| 292 | this.#tabs.addTab(e.debug); |
| 293 | } |
| 294 | e.buttons.append(e.mimetype.wrapper); |
| 295 | |
| 296 | if( opt.edit |
| 297 | && !opt.inReplyTo |
| 298 | && F.config.forumStatuses?.length>0 ){ |
| 299 | const sel = e.status = D.select(); |
| 300 | sel.setAttribute('title', 'The status tag value for this post.'); |
| 301 | D.option(sel, "", "- Status -").disabled = true; |
| 302 | for( const status of F.config.forumStatuses ){ |
| 303 | D.option(sel, status.value, status.label); |
| 304 | } |
| 305 | e.buttons.append(sel); |
| 306 | if( opt.status ){ |
| 307 | sel.value = opt.status; |
| 308 | }else if( this.#draft ){ |
| 309 | if( this.#draft.status ){ |
| 310 | sel.value = this.#draft.status; |
| 311 | }else{ |
| 312 | this.#draft.status = sel.value = F.config.forumStatuses[0].value; |
| 313 | } |
| 314 | sel.addEventListener('change',ev=>{ |
| 315 | const v = sel.value; |
| 316 | if( this.#draft.status !== v ){ |
| 317 | this.#draft.status = v; |
| 318 | this.#storeDraft(); |
| 319 | } |
| 320 | }); |
| 321 | } |
| 322 | }/*e.status*/ |
| 323 | |
| 324 | if( F.user.mayAttachForum ){ |
| 325 | //e.buttons.append( e.button.addAttach = this.#att.takeAddButton() ); |
| 326 | e.tabAttach = D.div(); |
| 327 | e.tabAttach.setAttribute('id', idPrefix+'-attach'); |
| 328 | e.tabAttach.dataset.tabLabel = 'Attachments'; |
| 329 | this.#tabs.addTab(e.tabAttach); |
| 330 | /* Reminder: we don't currently have a way to disable/enable |
| 331 | an Attacher's controls during ajax traffic. */ |
| 332 | } |
| 333 | e.buttons.append(e.button.preview, e.button.submit); |
| 334 | if( e.button.stash ){ |
| 335 | e.buttons.append(e.button.stash); |
| 336 | this.#toDisable.push(e.button.stash); |
| 337 | } |
| 338 | if( e.button.discard ){ |
| 339 | e.buttons.append(e.button.discard); |
| 340 | this.#toDisable.push(e.button.discard); |
| 341 | } |
| 342 | |
| 343 | e.help = D.attr(D.div(), 'id', idPrefix+'-help'); |
| 344 | e.help.$needsInit = true; |
| 345 | e.help.dataset.tabLabel = 'Help'; |
| 346 | this.#tabs.addTab(e.help); |
| 347 | |
| 348 | if( opt.hiddenFields ){ |
| 349 | this.addHiddenFields( opt.hiddenFields ); |
| 350 | delete opt.hiddenFields; |
| 351 | } |
| 352 | |
| 353 | { /* Shift-enter pieces... */ |
| 354 | const eCb = D.checkbox(1); |
| 355 | const eLbl = D.label(); |
| 356 | const eHelp = D.append( |
| 357 | D.span(), [ |
| 358 | 'When checked, shift-enter will toggle between preview ', |
| 359 | 'and edit modes, which is generally useful but some ', |
| 360 | 'software keyboards misinteract with it. If the preview ', |
| 361 | 'starts when tapping Enter, turn this setting off.' |
| 362 | ].join('') |
| 363 | ); |
| 364 | eCb.checked = F.storage.getBool( |
| 365 | 'edit-shift-enter-preview', |
| 366 | true |
| 367 | /* Maintenance reminder: this setting is shared across |
| 368 | several apps, like /chat, /wikiedit, and /fileedit. */ |
| 369 | ); |
| 370 | eCb.addEventListener('change', (ev)=>{ |
| 371 | F.storage.set('edit-shift-enter-preview', eCb.checked); |
| 372 | }); |
| 373 | F.helpButtonlets.setup(eHelp); |
| 374 | eLbl.append("Shift-enter toggles preview?", eCb, eHelp); |
| 375 | e.tabEdit.append(eLbl); |
| 376 | const isShiftEnter = (ev)=>eCb.checked && ev.shiftKey && 13===ev.keyCode; |
| 377 | e.editor.addEventListener('keydown',(ev)=>{ |
| 378 | /** |
| 379 | If eCb.checked is true, a keyboard combo of shift-enter |
| 380 | (from the editor) toggles between preview and edit modes. |
| 381 | This is normally desired but at least one software |
| 382 | keyboard is known to misinteract with this, treating an |
| 383 | Enter after automatically-capitalized letters as a |
| 384 | shift-enter: |
| 385 | |
| 386 | https://fossil-scm.org/forum/forumpost/dbd5b68366147ce8 |
| 387 | */ |
| 388 | if(!isShiftEnter(ev)) return; |
| 389 | ev.preventDefault(); |
| 390 | ev.stopPropagation(); |
| 391 | e.editor.blur(/*force draft update if needed*/); |
| 392 | this.#tabs.switchToTab(e.preview); |
| 393 | }, false); |
| 394 | // If we're in the preview tab, have ctrl-enter switch back to the editor. |
| 395 | document.body.addEventListener('keydown',(ev)=>{ |
| 396 | if(!isShiftEnter(ev)) return; |
| 397 | if(this.#activeTab !== e.tabEdit){ |
| 398 | ev.preventDefault(); |
| 399 | ev.stopPropagation(); |
| 400 | this.#tabs.switchToTab(e.tabEdit); |
| 401 | e.editor.focus(/*slow as molasses for long docs, as focus() |
| 402 | forces a document reflow. */); |
| 403 | return false; |
| 404 | } |
| 405 | }, true); |
| 406 | }/*shift-enter preview bits*/ |
| 407 | |
| 408 | if(0){ /* Needs to be optional */ |
| 409 | const elemsToToggle = document.body.querySelectorAll( |
| 410 | ':scope > header, :scope > nav' |
| 411 | ); |
| 412 | e.button.toggleHeader = |
| 413 | D.button('Toggle header', e=>{ |
| 414 | for(const et of elemsToToggle){ |
| 415 | et.classList.toggle('hidden'); |
| 416 | } |
| 417 | }); |
| 418 | e.buttons.append(e.button.toggleHeader); |
| 419 | } |
| 420 | |
| 421 | { |
| 422 | const eLbl = D.label(false, "Posting as "+F.user.name) |
| 423 | eLbl.classList.add('logged-in-as'); |
| 424 | e.buttons.append(eLbl); |
| 425 | } |
| 426 | |
| 427 | }/*constructor*/ |
| 428 | |
| 429 | /* |
| 430 | ** Removes this object from the DOM. It has no side effects if |
| 431 | ** it's not in the DOM. |
| 432 | */ |
| 433 | close(){ |
| 434 | const e = this.#e.widget; |
| 435 | if( e?.parentNode ){ |
| 436 | if( this.#opt.onclose instanceof Function ){ |
| 437 | try{this.#opt.onclose();} |
| 438 | catch(e){ |
| 439 | console.error("ForumPostEditor.onclose() threw:",e); |
| 440 | } |
| 441 | } |
| 442 | //console.debug("FPE discarding", this); |
| 443 | e.classList.add('animate-exit'); |
| 444 | e.addEventListener('animationend', ()=>e.remove(), {once: true}); |
| 445 | dummyPadding.refs.delete(this); |
| 446 | if( 0===dummyPadding.refs.size ){ |
| 447 | dummyPadding.remove(); |
| 448 | } |
| 449 | } |
| 450 | } |
| 451 | |
| 452 | /* |
| 453 | ** Discards any draft edits then calls close(). If an ondiscard |
| 454 | ** callback was provided to the constructor then it is called |
| 455 | ** before the drafts are cleared and any exceptions it throws are |
| 456 | ** ignored (but may be logged). |
| 457 | */ |
| 458 | discard(){ |
| 459 | if( this.#opt.ondiscard instanceof Function ){ |
| 460 | try{this.#opt.ondiscard(this);} |
| 461 | catch(e){ |
| 462 | console.error("ForumPostEditor.ondiscard() threw:",e); |
| 463 | } |
| 464 | } |
| 465 | this.#clearDraft(); |
| 466 | this.close(); |
| 467 | } |
| 468 | |
| 469 | /** This widget's top-most DOM element. */ |
| 470 | get widget(){ |
| 471 | if( !dummyPadding.parentElement ){ |
| 472 | document.body.append(dummyPadding); |
| 473 | } |
| 474 | dummyPadding.refs.add(this); |
| 475 | return this.#e.widget; |
| 476 | } |
| 477 | |
| 478 | get editorContent(){ |
| 479 | /* We wrap access to the editor's contents in a getter/setter so |
| 480 | that we can eventually add optional use of a contenteditable |
| 481 | edit field, as those are generally more comfortable. The code |
| 482 | for that is in fossil.page.chat.js. */ |
| 483 | return this.#e.editor.value; |
| 484 | } |
| 485 | |
| 486 | set editorContent(v){ |
| 487 | this.#e.editor.value = v; |
| 488 | } |
| 489 | |
| 490 | /** |
| 491 | Reports an error by appending each argument to the error widget |
| 492 | and unhiding it. If passed no arugments, it clears and hides |
| 493 | the error widget. |
| 494 | */ |
| 495 | reportError(...msg){ |
| 496 | const e = this.#e.error; |
| 497 | D.clearElement(e); |
| 498 | if( msg.length ){ |
| 499 | console.error('ForumPostEditor:',...msg); |
| 500 | e.classList.remove('hidden'); |
| 501 | e.append( |
| 502 | ...msg, D.br(), |
| 503 | D.button("Clear", ()=>this.reportError()) |
| 504 | /* Looks horrid in the Blitz skin */ |
| 505 | ); |
| 506 | }else{ |
| 507 | e.classList.add('hidden'); |
| 508 | } |
| 509 | } |
| 510 | |
| 511 | /** |
| 512 | Adds a list of input[type=hidden] form fields to this object, |
| 513 | imported from the server-generated HTML. This is used for |
| 514 | collecting, e.g., the CSRF token and an initial page title. |
| 515 | */ |
| 516 | addHiddenFields(list){ |
| 517 | this.#extraFields ??= []; |
| 518 | for( const f of list ){ |
| 519 | if( !f ) continue; |
| 520 | if( 'title'===f.name && this.#e.title ){ |
| 521 | if( f.value && this.#opt.isNewThread && !this.#e.title.value ){ |
| 522 | this.#e.title.value = f.value; |
| 523 | } |
| 524 | }else{ |
| 525 | this.#extraFields.push(f); |
| 526 | } |
| 527 | } |
| 528 | } |
| 529 | |
| 530 | get mimetype(){ |
| 531 | return this.#e.mimetype.select.value; |
| 532 | } |
| 533 | |
| 534 | get title(){ |
| 535 | return this.#e.title?.value || this.#opt.edit?.H; |
| 536 | } |
| 537 | |
| 538 | #initHelpTab(){ |
| 539 | const eh = this.#e.help; |
| 540 | const list = D.ul(); |
| 541 | D.append( |
| 542 | D.li(list), |
| 543 | D.attr(D.a(F.repoUrl('markup_help'), 'Markup styles'), |
| 544 | 'target', '_new') |
| 545 | ); |
| 546 | D.append( |
| 547 | D.li(list), |
| 548 | "WARNING: draft edits are keyed on the ID of the message they ", |
| 549 | "are editing or responding to. Attempting to edit or reply to ", |
| 550 | "the same post from multiple tabs will cause the most-recently-edited ", |
| 551 | "one to overwrite the draft slot for that post. In browsers which support ", |
| 552 | "Web Locks, a second attempt to edit or reply to a post will be blocked ", |
| 553 | "and an error will be shown explaining the problem." |
| 554 | ); |
| 555 | if( this.#e.status ){ |
| 556 | D.append( |
| 557 | D.li(list), |
| 558 | "Tip: changing just the status in the editor will change only that, ", |
| 559 | "not a whole new (but unedited) copy of the post." |
| 560 | ); |
| 561 | } |
| 562 | eh.append(list); |
| 563 | } |
| 564 | |
| 565 | #initAttacherTab(){ |
| 566 | this.#att = new F.Attacher({ |
| 567 | reverse: true |
| 568 | }); |
| 569 | if( this.#opt.edit ){ |
| 570 | const eNote = D.append( |
| 571 | D.div(), |
| 572 | "Tip: attachments can be added to posts without editing them ", |
| 573 | "by visiting ", |
| 574 | D.attr( |
| 575 | D.a(F.repoUrl('attachadd?target='+this.#opt.edit.uuid), '/attachadd'), |
| 576 | 'target', |
| 577 | '_new' |
| 578 | ), |
| 579 | ".", |
| 580 | ); |
| 581 | this.#e.tabAttach.append(eNote); |
| 582 | } |
| 583 | this.#e.tabAttach.append(this.#att.widget); |
| 584 | } |
| 585 | |
| 586 | #newFormData(addThisContent){ |
| 587 | const fd = new FormData; |
| 588 | for(const f of this.#extraFields){ |
| 589 | fd.append(f.name, f.value); |
| 590 | } |
| 591 | let v; |
| 592 | if( this.#opt.inReplyTo ){ |
| 593 | fd.append( 'firt', this.#opt.inReplyTo ); |
| 594 | }else if( (v = (this.#e.title?.value?.trim?.() || this.#opt.edit?.H)) ){ |
| 595 | fd.append('title', v); |
| 596 | } |
| 597 | fd.append('mimetype', this.mimetype); |
| 598 | fd.append('content', addThisContent || this.editorContent.trim()); |
| 599 | return fd; |
| 600 | } |
| 601 | |
| 602 | async #fetchPreview(content){ |
| 603 | /* TODO: fetch preview */ |
| 604 | const e = this.#e; |
| 605 | const fd = /*no: this.#newFormData(content); */ |
| 606 | new FormData; |
| 607 | let ext; |
| 608 | switch(this.mimetype){ |
| 609 | case 'text/x-markdown': ext = 'md'; break; |
| 610 | case 'text/x-fossil-wiki': ext = 'wiki'; break; |
| 611 | default: ext = 'txt'; break; |
| 612 | } |
| 613 | fd.append('filename', 'x.'+ext/*for mimetype determination*/); |
| 614 | fd.append('content', this.editorContent.trim()); |
| 615 | return window |
| 616 | .fetch(F.repoUrl('ajax/preview-text'),{ |
| 617 | method: 'POST', |
| 618 | body: fd |
| 619 | }) |
| 620 | .then(r=>r.text()) |
| 621 | .then(t=>{ |
| 622 | if( /^\{.*}$/.test(t) ){ |
| 623 | const o = JSON.parse(t); |
| 624 | throw new Error(o.error); |
| 625 | } |
| 626 | return t; |
| 627 | }); |
| 628 | } |
| 629 | |
| 630 | #setPreviewContent(rawHtml){ |
| 631 | /** |
| 632 | Append the new content then remove the old, to help reduce |
| 633 | jumping-around of the UI if the preview is cleared then |
| 634 | repopulated. |
| 635 | */ |
| 636 | const preview = this.#e.preview; |
| 637 | const childs = [...preview.childNodes]; |
| 638 | D.parseHtml(preview, rawHtml); |
| 639 | D.remove(childs); |
| 640 | //preview.style.removeProperty('height'); |
| 641 | if(F.pikchr && 'text/x-markdown'===this.mimetype){ |
| 642 | F.pikchr.addSrcView( |
| 643 | preview.querySelectorAll('svg.pikchr') |
| 644 | ); |
| 645 | } |
| 646 | } |
| 647 | |
| 648 | async #preview(){ |
| 649 | if( this.#isWaiting ) return; |
| 650 | const e = this.#e; |
| 651 | if( e.preview !== this.#activeTab ){ |
| 652 | this.#tabs.switchToTab(e.preview); |
| 653 | /* Will recurse into here */ |
| 654 | return; |
| 655 | } |
| 656 | const content = this.editorContent.trim(); |
| 657 | //console.debug("content to preview", content); |
| 658 | if( !content ){ |
| 659 | return; |
| 660 | } |
| 661 | if( 0 |
| 662 | && !e.preview.firstElementChild ){ |
| 663 | /* On an initial first preview, inherit the editor's height to |
| 664 | reduce jumping-around of the UI. */ |
| 665 | if( 0 /* does not work: height of the editor is "auto" */ ){ |
| 666 | const c = window.getComputedStyle(e.editor/*tabEdit*/); |
| 667 | e.preview.style.height = c.height; |
| 668 | }else{ |
| 669 | e.preview.style.height = '20em'; |
| 670 | } |
| 671 | } |
| 672 | this.#isWaiting = true; |
| 673 | D.disable(this.#toDisable, e.button.submit); |
| 674 | this.#fetchPreview(content) |
| 675 | .then((c)=>{ |
| 676 | this.#setPreviewContent(c); |
| 677 | D.enable(e.button.submit); |
| 678 | }) |
| 679 | .catch(err=>{ |
| 680 | e.preview.textContent = "Error fetching preview: "+err.message; |
| 681 | console.error("Error fetching preview:",err); |
| 682 | this.reportError(err.message); |
| 683 | }) |
| 684 | .finally(()=>{ |
| 685 | this.#isWaiting = false; |
| 686 | D.enable(this.#toDisable); |
| 687 | }); |
| 688 | } |
| 689 | |
| 690 | #validate(tgt){ |
| 691 | if( this.#e.title ){ |
| 692 | const v = this.#e.title.value.trim(); |
| 693 | if( !v ){ |
| 694 | this.reportError("A non-empty title is required."); |
| 695 | return; |
| 696 | } |
| 697 | } |
| 698 | return true; |
| 699 | } |
| 700 | |
| 701 | #submit(){ |
| 702 | if( this.#isWaiting ) return; |
| 703 | if( !this.#validate() ) return; |
| 704 | this.#isWaiting = true; |
| 705 | const e = this.#e; |
| 706 | D.disable(e.button.submit); |
| 707 | const fd = this.#newFormData(); |
| 708 | if( this.#e.status ){ |
| 709 | /* Send the status only if it was modified, otherwise we may |
| 710 | add a superfluous tag. */ |
| 711 | const v = this.#e.status.value; |
| 712 | if( this.#e.status.dataset.originalValue !== v ){ |
| 713 | fd.append("status", v); |
| 714 | } |
| 715 | } |
| 716 | if( e.debug ){ |
| 717 | e.debug.querySelectorAll('input[type=checkbox]').forEach(cb=>{ |
| 718 | if( cb.checked ){ |
| 719 | fd.append(cb.value, 1); |
| 720 | //console.debug("Forum post debug option:",cb); |
| 721 | } |
| 722 | }); |
| 723 | } |
| 724 | if( this.#att ){ |
| 725 | this.#att.populateFormData(fd); |
| 726 | } |
| 727 | //console.warn("Ready to submit",fd); |
| 728 | if( 0 ){ |
| 729 | this.#isWaiting = false; |
| 730 | return; |
| 731 | } |
| 732 | const resp = window.fetch(F.repoUrl('forumajax_save'), { |
| 733 | method: 'POST', |
| 734 | body: fd |
| 735 | }).then(r=>r.json()) |
| 736 | .then(j=>{ |
| 737 | j = F.nu(j); |
| 738 | console.debug("forum post editor response:",j); |
| 739 | if( j.error ){ |
| 740 | throw new Error(j.error); |
| 741 | }else if( j.message ){ |
| 742 | /* This is only for use in debugging during |
| 743 | * development. */ |
| 744 | this.reportError(j.message); |
| 745 | return; |
| 746 | } |
| 747 | if( 1 ){ |
| 748 | this.#clearDraft(); |
| 749 | if( this.#opt.onsubmit instanceof Function ){ |
| 750 | try{this.#opt.onsubmit(this, j);} |
| 751 | catch(e){ |
| 752 | console.error("ForumPostEditor.onsubmit() threw: ", e); |
| 753 | } |
| 754 | } |
| 755 | /* |
| 756 | if( this.#opt.edit?.uuid === j.uuid ) then we know the |
| 757 | content did not change, but it's possible that attachments |
| 758 | and/or a status tag did. Ergo, we need to unconditionally |
| 759 | reload to render those changes (if any). The other option |
| 760 | is to tell the user "nothing changed" and leave them in |
| 761 | the editor, but that could be a lie because we don't know |
| 762 | if any attachments or tags were changed. |
| 763 | */ |
| 764 | else if( 0 ){ |
| 765 | if( this.#opt.edit.uuid === j.uuid |
| 766 | && !j.statusModified && 0===j.attachedCount ){ |
| 767 | this.reportError("No changes made."); |
| 768 | }else{ |
| 769 | window.location = F.repoUrl('forumpost/'+j.uuid); |
| 770 | setTimeout(()=>this.close(), 500/*just in case not redirected*/); |
| 771 | } |
| 772 | } |
| 773 | }else{ |
| 774 | this.reportError( |
| 775 | "Saving worked but we're ignoring it and staying here." |
| 776 | ); |
| 777 | } |
| 778 | }) |
| 779 | .catch((e)=>this.reportError(e.message)) |
| 780 | .finally(()=>this.#isWaiting = false); |
| 781 | } |
| 782 | |
| 783 | #storeDraft(){ |
| 784 | if( this.#draft ){ |
| 785 | this.#draft.mtime = Date.now(); |
| 786 | F.storage.setJSON(this.#opt.draftKey, this.#draft); |
| 787 | } |
| 788 | } |
| 789 | |
| 790 | /** Clears any persistent draft state. Does not clear the UI |
| 791 | widgets. */ |
| 792 | #clearDraft(){ |
| 793 | if( this.#draft ){ |
| 794 | F.storage.remove(this.#opt.draftKey); |
| 795 | this.#draft = F.nu(); |
| 796 | } |
| 797 | } |
| 798 | |
| 799 | /** |
| 800 | Looks for editing draft keys matching either a fixed key or a |
| 801 | regex, and removes each matching one which is older than the |
| 802 | given number of days. Pass days=0 to purge all entries |
| 803 | immediately. |
| 804 | */ |
| 805 | static purgeOldDrafts(key, days=10){ |
| 806 | const age = (3600 * 24 * days) * 1000/*ms*/; |
| 807 | const now = Date.now(); |
| 808 | const check = (k)=>{ |
| 809 | const o = F.storage.getJSON(k); |
| 810 | if( o && o.mtime && (!days || (o.mtime+age <= now)) ){ |
| 811 | F.storage.remove(k); |
| 812 | } |
| 813 | }; |
| 814 | if( key instanceof RegExp ){ |
| 815 | for(const k of F.storage.keys(false).filter(v=>key.test(v))){ |
| 816 | check(k); |
| 817 | } |
| 818 | }else{ |
| 819 | check(key); |
| 820 | } |
| 821 | } |
| 822 | |
| 823 | async #fetchPost(){ |
| 824 | /* |
| 825 | TODO: when editing an existing post, fetch the raw body of the |
| 826 | post and populate this.e. |
| 827 | */ |
| 828 | } |
| 829 | }/*ForumPostEditor*/; |
| 830 | F.ForumPostEditor = ForumPostEditor; |
| 831 | |
| 832 | /** |
| 833 | When the page is loaded, this handler does the following: |
| 834 | |
| 835 | 1. Installs expand/collapse UI elements on "long" posts and collapses |
| 836 | them. |
| 837 | |
| 838 | 2. Any pikchr-generated SVGs get a source-toggle button added to them |
| 839 | which activates when the mouse is over the image or it is tapped. |
| 840 | |
| 841 | 3. Plugs in a new edit/reply widget to forum posts. |
| 842 | |
| 843 | This is a harmless no-op if the current page has neither forum |
| 844 | post constructs for (1) and (3) nor any pikchr images for (2), |
| 845 | nor will NOT running this code cause any breakage for clients |
| 846 | with no JS support: this is all "nice-to-have", not required |
| 847 | functionality. |
| 848 | */ |
| 849 | F.onPageLoad(function(){ |
| 850 | const scrollbarIsVisible = (e)=>e.scrollHeight > e.clientHeight; |
| 851 | /* Returns an event handler which implements the post expand/collapse toggle |
| 852 | on contentElem when the given widget is activated. */ |
| @@ -105,15 +933,20 @@ | |
| 933 | const eStatus = document.querySelector( |
| 934 | 'form div.submenu select.submenuctrl[name="status"]' |
| 935 | ); |
| 936 | if( eStatus ){ |
| 937 | /* Main /forum list. Remove the 'x' form element when eStatus |
| 938 | changes, to avoid propagating x when changing the filter. |
| 939 | The problem this solves: we're browsed to page 3 of status X. |
| 940 | We change the status filter selection to Y. We're redirected |
| 941 | to page x, but Y only has 2 posts with that status, so we see |
| 942 | an empty list. When changing the filter, we need to ensure |
| 943 | that we start back and that beginning. */ |
| 944 | const pForm = eStatus.parentElement?.parentElement; |
| 945 | if( pForm ){ |
| 946 | eStatus.addEventListener('change', ()=>{ |
| 947 | pForm.querySelector('input[type="hidden"][name="x"]')?.remove?.(); |
| 948 | }, true); |
| 949 | } |
| 950 | }else{ |
| 951 | /* One of the single-post edit/view pages. Handle various UI |
| 952 | controls and attempt to keep stray double-clicks from |
| @@ -126,16 +959,21 @@ | |
| 959 | return; |
| 960 | } |
| 961 | form.dataset.submitted = '1'; |
| 962 | /** If the user is left waiting "a long time," disable the |
| 963 | resubmit protection. If we don't do this and they tap the |
| 964 | browser's cancel button while waiting, they'll be stuck |
| 965 | with an unsubmittable form. It can apparently also happen, |
| 966 | via browser-back, that the form gets left in a submitted |
| 967 | state. */ |
| 968 | setTimeout(()=>{delete form.dataset.submitted}, 7000); |
| 969 | return; |
| 970 | }; |
| 971 | |
| 972 | document.querySelectorAll("form").forEach(function(form){ |
| 973 | /* Set up controls for closing posts and setting thread |
| 974 | status. */ |
| 975 | form.addEventListener('submit', formSubmitted); |
| 976 | form |
| 977 | .querySelectorAll("input.action-close, input.action-reopen") |
| 978 | .forEach(function(e){ |
| 979 | e.classList.remove('hidden'); |
| @@ -149,11 +987,13 @@ | |
| 987 | form |
| 988 | .querySelectorAll("input[type='button'].action-status") |
| 989 | .forEach(function(btn){ |
| 990 | btn.classList.remove('hidden'); |
| 991 | const sel = btn.previousElementSibling; |
| 992 | const updateButton = ()=>{ |
| 993 | /* Enable btn only when the status has been locally |
| 994 | modified. */ |
| 995 | if( sel.dataset.initialValue ){ |
| 996 | if( sel.dataset.initialValue===sel.value ){ |
| 997 | btn.setAttribute('disabled',''); |
| 998 | }else{ |
| 999 | btn.removeAttribute('disabled'); |
| @@ -164,17 +1004,392 @@ | |
| 1004 | }else{ |
| 1005 | btn.removeAttribute('disabled'); |
| 1006 | } |
| 1007 | } |
| 1008 | }; |
| 1009 | sel.addEventListener('change', updateButton, true); |
| 1010 | updateButton(); |
| 1011 | F.confirmer(btn, { |
| 1012 | confirmText: "Confirm status change", |
| 1013 | onconfirm: ()=>form.submit() |
| 1014 | }); |
| 1015 | }); |
| 1016 | }); |
| 1017 | } |
| 1018 | |
| 1019 | /* Apply page-specific tweaks for ForumPostEditor instance fpe |
| 1020 | then plug it into the UI at the end of ePost. */ |
| 1021 | const initFPEWidget = (fpe, ePost)=>{ |
| 1022 | ePost.eUnhideThenWhenDone = [ |
| 1023 | /* List of elements to hide while editing/replying and reveal |
| 1024 | when discarding or saving. */ |
| 1025 | ]; |
| 1026 | for( const ee of ePost.querySelectorAll( |
| 1027 | '.forumpost-single-controls, fieldset.forum-status-selection' |
| 1028 | ) ){ |
| 1029 | ee.hidden = true; |
| 1030 | ePost.eUnhideThenWhenDone.push(ee); |
| 1031 | } |
| 1032 | const w = fpe.widget; |
| 1033 | w.classList.add('animate-entrance'); |
| 1034 | ePost.append(w); |
| 1035 | requestAnimationFrame(() => { |
| 1036 | w.scrollIntoView({ |
| 1037 | behavior: 'smooth', |
| 1038 | block: 'nearest', |
| 1039 | inline: 'nearest' |
| 1040 | }); |
| 1041 | }); |
| 1042 | }; |
| 1043 | |
| 1044 | const plugInEditor = |
| 1045 | (new URL(window.location).searchParams).get('nojs')===null; |
| 1046 | |
| 1047 | const eForumNew = ( |
| 1048 | plugInEditor |
| 1049 | && ( |
| 1050 | document.body.classList.contains('cpage-forumnew') |
| 1051 | || document.body.classList.contains('cpage-forume1') |
| 1052 | )) |
| 1053 | ? document.body.querySelector('#forumnew-placeholder') |
| 1054 | : null; |
| 1055 | if( plugInEditor && eForumNew ){ |
| 1056 | /* /forumnew and /forume1 */ |
| 1057 | const fpe = new F.ForumPostEditor({ |
| 1058 | draftKey: 'draft-forumnew', |
| 1059 | hiddenFields: eForumNew.querySelectorAll('input[type=hidden]'), |
| 1060 | hideStash: true, |
| 1061 | ondiscard: ()=>{ |
| 1062 | window.location = F.repoUrl('forum'); |
| 1063 | }, |
| 1064 | onsubmit: (fpe, response)=>{ |
| 1065 | window.location = F.repoUrl('forumpost/'+response.uuid); |
| 1066 | } |
| 1067 | }); |
| 1068 | eForumNew.parentElement.insertBefore(fpe.widget, eForumNew); |
| 1069 | eForumNew.remove(); |
| 1070 | fossil.page.fpe = fpe /* for testing via the console */; |
| 1071 | }/*eForumNew*/ |
| 1072 | else if( plugInEditor |
| 1073 | && (document.body.classList.contains('cpage-forumpost') |
| 1074 | || document.body.classList.contains('cpage-forumthread')) ){ |
| 1075 | /* /forumpost and /forumthread. Take over the Edit/Reply buttons |
| 1076 | to use a ForumPostEditor. */ |
| 1077 | |
| 1078 | const fetchPost = async (fpid)=>{ |
| 1079 | return window.fetch(F.repoUrl('ajax/artifact.json?uuid='+fpid)) |
| 1080 | .then(r=>r.json()) |
| 1081 | .then(j=>{ |
| 1082 | j = F.nu(j); |
| 1083 | if( j.error ) throw new Error(j.error); |
| 1084 | return j; |
| 1085 | }); |
| 1086 | }; |
| 1087 | |
| 1088 | const makeDraftKey = (prefix,uuid)=>{ |
| 1089 | return prefix+'-'+uuid.substr(0,12); |
| 1090 | }; |
| 1091 | |
| 1092 | /** |
| 1093 | Perform some init common to both Reply and Edit. ePost = the |
| 1094 | forum post DOM element. eButton = the Reply or Edit |
| 1095 | button. |
| 1096 | */ |
| 1097 | const setupEditReplyElement = (ePost, eButton)=>{ |
| 1098 | /* Forum posts are indented in the main forum view to |
| 1099 | represent their place in the hierarchy. In order to gain |
| 1100 | some screen space, we shift the post to the left margin and |
| 1101 | arrange to shift it back when the editor is closed. We also |
| 1102 | record the original button label so that it can be |
| 1103 | restored on close. */ |
| 1104 | ePost.dataset.originalMarginLeft = ePost.style.marginLeft; |
| 1105 | ePost.style.marginLeft = 'initial'; |
| 1106 | eButton.dataset.originalLabel = eButton.innerText; |
| 1107 | }; |
| 1108 | |
| 1109 | /** Undoes the damage done by setupEditReplyElement(). */ |
| 1110 | const restoreEditReplyElement = (ePost, eButton)=>{ |
| 1111 | if( ePost.dataset.originalMarginLeft ){ |
| 1112 | ePost.style.marginLeft = ePost.dataset.originalMarginLeft; |
| 1113 | delete ePost.dataset.originalMarginLeft; |
| 1114 | } |
| 1115 | if( eButton.dataset.originalLabel ){ |
| 1116 | eButton.innerText = eButton.dataset.originalLabel; |
| 1117 | delete eButton.dataset.originalLabel; |
| 1118 | } |
| 1119 | for(const ee of (ePost.eUnhideThenWhenDone || [])){ |
| 1120 | ee.removeAttribute('hidden'); |
| 1121 | } |
| 1122 | ePost.eUnhideThenWhenDone = undefined; |
| 1123 | }; |
| 1124 | |
| 1125 | /** |
| 1126 | Reports an error regarding the forum post element |
| 1127 | ePost, appending each entry in msg to a wrapper |
| 1128 | element with the class |
| 1129 | */ |
| 1130 | const reportFPEError = (ePost,...msg)=>{ |
| 1131 | const e = D.addClass(D.p(), 'error'); |
| 1132 | e.append( |
| 1133 | ...msg, |
| 1134 | D.br(), |
| 1135 | D.button("Clear error", ()=>e.remove()) |
| 1136 | ); |
| 1137 | ePost.append(e); |
| 1138 | }; |
| 1139 | |
| 1140 | /** |
| 1141 | Plug in an editor widget representing a reply to a post. |
| 1142 | form = a (.forum-post-single-controls > form) element. The |
| 1143 | final 3 arguments are as documented for |
| 1144 | setupEditReplyElement(). |
| 1145 | */ |
| 1146 | const replyClicked = async (form, ePost, eBtnReply)=>{ |
| 1147 | const fpid = ePost.dataset.fpid; |
| 1148 | const fEditHead = ePost.dataset.fedithead; |
| 1149 | const draftKey = makeDraftKey( |
| 1150 | 'draft-reply', fEditHead |
| 1151 | /* The problem with firt as a key is that firt is not |
| 1152 | necessarily the root edit of that post, which is what we |
| 1153 | really want as a draft key so that the draft does not |
| 1154 | disappear if firt is later edited (giving us a new firt |
| 1155 | value here). */ |
| 1156 | || fpid |
| 1157 | ); |
| 1158 | let releaseLock; |
| 1159 | if( window.navigator.locks ){ |
| 1160 | releaseLock = await new Promise((resolve)=>{ |
| 1161 | window.navigator.locks.request( |
| 1162 | 'fossil-'+draftKey, |
| 1163 | {ifAvailable: true}, |
| 1164 | async (lock) => { |
| 1165 | if( !lock ){ |
| 1166 | /*lock contention*/ |
| 1167 | resolve(null); |
| 1168 | return; |
| 1169 | } |
| 1170 | let release; |
| 1171 | const lockReleased = new Promise(res=>release=res); |
| 1172 | resolve(release); |
| 1173 | await lockReleased/*hold the lock open*/; |
| 1174 | }); |
| 1175 | }); |
| 1176 | if( !releaseLock ){ |
| 1177 | reportFPEError( |
| 1178 | ePost, |
| 1179 | "This post is actively being replied to ", |
| 1180 | "in another tab. To avoid losing edits, ", |
| 1181 | "it cannot be opened here until the locking ", |
| 1182 | "tab is closed." |
| 1183 | ); |
| 1184 | return; |
| 1185 | } |
| 1186 | } |
| 1187 | |
| 1188 | setupEditReplyElement(ePost, eBtnReply); |
| 1189 | eBtnReply.innerText = "Replying..."; |
| 1190 | const ondone = (fpe, response)=>{ |
| 1191 | /* onsubmit() and ondiscard() callback */ |
| 1192 | restoreEditReplyElement(ePost, eBtnReply); |
| 1193 | //console.debug("ondiscard/onsubmit", fpe, artifact); |
| 1194 | if( response/*onsubmit()*/ ){ |
| 1195 | window.location = F.repoUrl('forumpost/'+response.uuid); |
| 1196 | setTimeout(()=>fpe.close(), 500/*just in case not redirected*/); |
| 1197 | }else{/*ondiscard() or onclose()*/ |
| 1198 | } |
| 1199 | }; |
| 1200 | const fpe = new F.ForumPostEditor(F.nu({ |
| 1201 | hiddenFields: form.querySelectorAll( |
| 1202 | 'input[type=hidden][name=csrf]' |
| 1203 | /* Do not inherit the fpid field, else this will become |
| 1204 | an edit to that post rather than a response. */ |
| 1205 | ), |
| 1206 | ondiscard: ()=>{/*need a noop here. Will call onclose()*/}, |
| 1207 | onsubmit: ondone, |
| 1208 | onclose: ()=>{ |
| 1209 | if( releaseLock ){ |
| 1210 | releaseLock(); |
| 1211 | releaseLock = null; |
| 1212 | } |
| 1213 | ondone(); |
| 1214 | }, |
| 1215 | inReplyTo: fpid, |
| 1216 | draftKey |
| 1217 | })); |
| 1218 | initFPEWidget(fpe, ePost); |
| 1219 | }/*replyClicked()*/; |
| 1220 | |
| 1221 | /** |
| 1222 | Plug in an editor widget representing an edit to a post. |
| 1223 | form = a (.forum-post-single-controls > form) element. The |
| 1224 | final 3 arguments are as documented for |
| 1225 | setupEditReplyElement(). |
| 1226 | */ |
| 1227 | const editClicked = async (form, ePost, eBtnEdit)=>{ |
| 1228 | const fpid = ePost.dataset.fpid; |
| 1229 | const firt = ePost.dataset.firt; |
| 1230 | const fEditHead = ePost.dataset.fedithead; |
| 1231 | const draftKey = makeDraftKey('draft-forumedit', fEditHead || fpid); |
| 1232 | let releaseLock; |
| 1233 | if( navigator.locks ){ |
| 1234 | releaseLock = await new Promise((resolve) => { |
| 1235 | navigator.locks.request( |
| 1236 | 'fossil-'+draftKey, |
| 1237 | {ifAvailable: true}, |
| 1238 | async (lock)=>{ |
| 1239 | if( !lock ){ |
| 1240 | resolve(null); |
| 1241 | return; |
| 1242 | } |
| 1243 | let release; |
| 1244 | const lockReleased = new Promise(res=>release=res); |
| 1245 | resolve(release); |
| 1246 | await lockReleased; |
| 1247 | }); |
| 1248 | }); |
| 1249 | |
| 1250 | if( !releaseLock ){ |
| 1251 | reportFPEError( |
| 1252 | ePost, |
| 1253 | "This post is actively being edited ", |
| 1254 | "in another tab. To avoid losing edits, ", |
| 1255 | "it cannot be opened here until the locking ", |
| 1256 | "tab is closed." |
| 1257 | ); |
| 1258 | return; |
| 1259 | } |
| 1260 | } |
| 1261 | setupEditReplyElement(ePost, eBtnEdit); |
| 1262 | eBtnEdit.innerText = "Editing..."; |
| 1263 | fetchPost(fpid) |
| 1264 | .then(artifact=>{ |
| 1265 | const ondone = (fpe, response)=>{ |
| 1266 | /* onsubmit() and ondiscard() callback */ |
| 1267 | if( response/*onsubmit()*/ ){ |
| 1268 | if( fpid === response.uuid |
| 1269 | && !response.statusModified |
| 1270 | && 0===response.attachedCount ){ |
| 1271 | fpe.reportError("No changes made."); |
| 1272 | }else{ |
| 1273 | restoreEditReplyElement(ePost, eBtnEdit); |
| 1274 | window.location = F.repoUrl('forumpost/'+response.uuid); |
| 1275 | setTimeout(()=>fpe.close(), 500/*just in case not redirected*/); |
| 1276 | } |
| 1277 | }else{ |
| 1278 | /*ondiscard() or onclose()*/ |
| 1279 | restoreEditReplyElement(ePost, eBtnEdit); |
| 1280 | } |
| 1281 | }; |
| 1282 | const eStatusSelect = ePost.querySelector( |
| 1283 | ':scope > fieldset.forum-status-selection select[name=status]' |
| 1284 | ); |
| 1285 | |
| 1286 | const fpe = new F.ForumPostEditor(F.nu({ |
| 1287 | hiddenFields: form.querySelectorAll('input[type=hidden]'), |
| 1288 | ondiscard: ()=>{/*need a noop here. Will call onclose()*/}, |
| 1289 | onsubmit: ondone, |
| 1290 | onclose: ()=>{ |
| 1291 | if(releaseLock){ |
| 1292 | releaseLock(); |
| 1293 | releaseLock = null; |
| 1294 | } |
| 1295 | ondone(); |
| 1296 | }, |
| 1297 | draftKey, |
| 1298 | edit: artifact, |
| 1299 | status: eStatusSelect?.value, |
| 1300 | inReplyTo: firt |
| 1301 | })); |
| 1302 | initFPEWidget(fpe, ePost); |
| 1303 | }) |
| 1304 | .catch(err=>{ |
| 1305 | if( releaseLock ){ |
| 1306 | releaseLock(); |
| 1307 | releaseLock = null; |
| 1308 | } |
| 1309 | restoreEditReplyElement(ePost, eBtnEdit); |
| 1310 | console.error("Error fetching post:", err); |
| 1311 | reportFPEError(ePost, "Error fetching post: ", err.message); |
| 1312 | }); |
| 1313 | }/*editClicked()*/; |
| 1314 | |
| 1315 | document.body.querySelectorAll( |
| 1316 | '.forumpost-single-controls > form' |
| 1317 | ).forEach(form=>{ |
| 1318 | /* For each forum post... */ |
| 1319 | const eThePost = form.parentElement.parentElement/*main post DOM element*/; |
| 1320 | if( !eThePost?.dataset?.fpid ){ |
| 1321 | /* The server injects these dataset values. */ |
| 1322 | console.warn("Unexpected missing fpid", eThePost); |
| 1323 | return; |
| 1324 | } |
| 1325 | const checkButtonForDraft = (draftKeyPrefix, eBtn)=>{ |
| 1326 | /* If a draft is found associated with eThePost, mark eBtn |
| 1327 | as a draft and set up storage event listeners to update |
| 1328 | the button as new drafts come and go. */ |
| 1329 | const fpid = eThePost.dataset.fpid; |
| 1330 | const fEditHead = eThePost.dataset.fedithead; |
| 1331 | const draftKey = makeDraftKey(draftKeyPrefix, fEditHead || fpid); |
| 1332 | if( F.storage.contains(draftKey) ){ |
| 1333 | eBtn.classList.add('draft'); |
| 1334 | } |
| 1335 | F.storage.addEventListener('set', ({detail})=>{ |
| 1336 | if( draftKey === detail.key ){ |
| 1337 | eBtn.classList.add('draft'); |
| 1338 | } |
| 1339 | }); |
| 1340 | F.storage.addEventListener('remove', ({detail})=>{ |
| 1341 | if( draftKey === detail.key ){ |
| 1342 | eBtn.classList.remove('draft'); |
| 1343 | } |
| 1344 | }); |
| 1345 | }; |
| 1346 | /* Replace the Reply and Edit buttons with ones which will activate |
| 1347 | a ForumPostEditor. */ |
| 1348 | const btnReply = form.querySelector('input[type=submit][name=reply]'); |
| 1349 | if( btnReply ){ |
| 1350 | const b = D.button("Reply", ()=>replyClicked(form, eThePost, b)); |
| 1351 | b.type = 'button'/*keep container form from submitting*/; |
| 1352 | checkButtonForDraft('draft-reply',b); |
| 1353 | btnReply.parentElement.insertBefore(b, btnReply); |
| 1354 | btnReply.remove(); |
| 1355 | } |
| 1356 | const btnEdit = form.querySelector('input[type=submit][name=edit]'); |
| 1357 | if( btnEdit ){ |
| 1358 | const b = D.button("Edit", ()=>editClicked(form, eThePost, b)); |
| 1359 | b.type = 'button'/*keep container form from submitting*/; |
| 1360 | checkButtonForDraft('draft-forumedit',b); |
| 1361 | btnEdit.parentElement.insertBefore(b, btnEdit); |
| 1362 | btnEdit.remove(); |
| 1363 | } |
| 1364 | })/*for-each form*/; |
| 1365 | |
| 1366 | }/* /forumpost and /forumthread */ |
| 1367 | |
| 1368 | document.body.querySelectorAll('.remove-on-load').forEach(e=>e.remove()); |
| 1369 | document.body.querySelectorAll('.initially-hidden').forEach(e=>{ |
| 1370 | /* This is a workaround for a span.help-buttonlet which we need |
| 1371 | to start hidden so that it does not show up for no-JS |
| 1372 | clients. */ |
| 1373 | e.classList.remove('initially-hidden'); |
| 1374 | }); |
| 1375 | |
| 1376 | if( plugInEditor ){ |
| 1377 | document.body.querySelectorAll('.remove-if-replaced').forEach( |
| 1378 | /* Remove remaining legacy UI elements. */ e=>e.remove() |
| 1379 | ); |
| 1380 | /* Purge old drafts only every now and then. */ |
| 1381 | const now = Date.now(); |
| 1382 | const lastPurge = +F.storage.get('forum-drafts-last-purge', 0); |
| 1383 | if( now - lastPurge > (24 * 60 * 60 * 1000 /*1 day ms*/) ){ |
| 1384 | F.storage.set('forum-drafts-last-purge', now); |
| 1385 | setTimeout(()=>{ |
| 1386 | /* Don't block the UI while we're doing I/O */ |
| 1387 | F.ForumPostEditor.purgeOldDrafts( |
| 1388 | /^draft-(reply|forumedit)-.*/ |
| 1389 | /* Intentionally leaving draft-forumnew in place. */ |
| 1390 | ); |
| 1391 | }, 50); |
| 1392 | } |
| 1393 | } |
| 1394 | })/*F.onPageLoad callback*/; |
| 1395 | })(window.fossil); |
| 1396 |
+5
-6
| --- src/fossil.page.wikiedit.js | ||
| +++ src/fossil.page.wikiedit.js | ||
| @@ -1187,11 +1187,11 @@ | ||
| 1187 | 1187 | if(!wi.attachments || !wi.attachments.length){ |
| 1188 | 1188 | D.append(f.eAttach, |
| 1189 | 1189 | btnReload, |
| 1190 | 1190 | " No attachments found for page ["+wi.name+"]. ", |
| 1191 | 1191 | D.a(F.repoUrl('attachadd',{ |
| 1192 | - page: wi.name, | |
| 1192 | + target: wi.name, | |
| 1193 | 1193 | from: F.repoUrl('wikiedit',{name: wi.name})}), |
| 1194 | 1194 | "Add attachments..." ) |
| 1195 | 1195 | ); |
| 1196 | 1196 | return this; |
| 1197 | 1197 | } |
| @@ -1198,14 +1198,14 @@ | ||
| 1198 | 1198 | D.append( |
| 1199 | 1199 | f.eAttach, |
| 1200 | 1200 | D.append(D.p(), |
| 1201 | 1201 | btnReload," ", |
| 1202 | 1202 | D.a(F.repoUrl('attachlist',{page:wi.name}), |
| 1203 | - "Attachments for page ["+wi.name+"]."), | |
| 1204 | - " ", | |
| 1203 | + "Attachments for page ["+wi.name+"]"), | |
| 1204 | + ". ", | |
| 1205 | 1205 | D.a(F.repoUrl('attachadd',{ |
| 1206 | - page:wi.name, | |
| 1206 | + target:wi.name, | |
| 1207 | 1207 | from: F.repoUrl('wikiedit',{name: wi.name})}), |
| 1208 | 1208 | "Add attachments..." ) |
| 1209 | 1209 | ) |
| 1210 | 1210 | ); |
| 1211 | 1211 | wi.attachments.forEach(function(a){ |
| @@ -1388,11 +1388,10 @@ | ||
| 1388 | 1388 | setting. |
| 1389 | 1389 | */ |
| 1390 | 1390 | P.baseHrefRestore = function(){ |
| 1391 | 1391 | this.base.tag.href = this.base.originalHref; |
| 1392 | 1392 | }; |
| 1393 | - | |
| 1394 | 1393 | |
| 1395 | 1394 | /** |
| 1396 | 1395 | loadPage() loads the given wiki page and updates the relevant |
| 1397 | 1396 | UI elements to reflect the loaded state. If passed no arguments |
| 1398 | 1397 | then it re-uses the values from the currently-loaded page, reloading |
| @@ -1454,11 +1453,11 @@ | ||
| 1454 | 1453 | onload(r); |
| 1455 | 1454 | } |
| 1456 | 1455 | }); |
| 1457 | 1456 | return this; |
| 1458 | 1457 | }; |
| 1459 | - | |
| 1458 | + | |
| 1460 | 1459 | /** |
| 1461 | 1460 | Fetches the page preview based on the contents and settings of |
| 1462 | 1461 | this page's input fields, and updates the UI with the |
| 1463 | 1462 | preview. |
| 1464 | 1463 | |
| 1465 | 1464 |
| --- src/fossil.page.wikiedit.js | |
| +++ src/fossil.page.wikiedit.js | |
| @@ -1187,11 +1187,11 @@ | |
| 1187 | if(!wi.attachments || !wi.attachments.length){ |
| 1188 | D.append(f.eAttach, |
| 1189 | btnReload, |
| 1190 | " No attachments found for page ["+wi.name+"]. ", |
| 1191 | D.a(F.repoUrl('attachadd',{ |
| 1192 | page: wi.name, |
| 1193 | from: F.repoUrl('wikiedit',{name: wi.name})}), |
| 1194 | "Add attachments..." ) |
| 1195 | ); |
| 1196 | return this; |
| 1197 | } |
| @@ -1198,14 +1198,14 @@ | |
| 1198 | D.append( |
| 1199 | f.eAttach, |
| 1200 | D.append(D.p(), |
| 1201 | btnReload," ", |
| 1202 | D.a(F.repoUrl('attachlist',{page:wi.name}), |
| 1203 | "Attachments for page ["+wi.name+"]."), |
| 1204 | " ", |
| 1205 | D.a(F.repoUrl('attachadd',{ |
| 1206 | page:wi.name, |
| 1207 | from: F.repoUrl('wikiedit',{name: wi.name})}), |
| 1208 | "Add attachments..." ) |
| 1209 | ) |
| 1210 | ); |
| 1211 | wi.attachments.forEach(function(a){ |
| @@ -1388,11 +1388,10 @@ | |
| 1388 | setting. |
| 1389 | */ |
| 1390 | P.baseHrefRestore = function(){ |
| 1391 | this.base.tag.href = this.base.originalHref; |
| 1392 | }; |
| 1393 | |
| 1394 | |
| 1395 | /** |
| 1396 | loadPage() loads the given wiki page and updates the relevant |
| 1397 | UI elements to reflect the loaded state. If passed no arguments |
| 1398 | then it re-uses the values from the currently-loaded page, reloading |
| @@ -1454,11 +1453,11 @@ | |
| 1454 | onload(r); |
| 1455 | } |
| 1456 | }); |
| 1457 | return this; |
| 1458 | }; |
| 1459 | |
| 1460 | /** |
| 1461 | Fetches the page preview based on the contents and settings of |
| 1462 | this page's input fields, and updates the UI with the |
| 1463 | preview. |
| 1464 | |
| 1465 |
| --- src/fossil.page.wikiedit.js | |
| +++ src/fossil.page.wikiedit.js | |
| @@ -1187,11 +1187,11 @@ | |
| 1187 | if(!wi.attachments || !wi.attachments.length){ |
| 1188 | D.append(f.eAttach, |
| 1189 | btnReload, |
| 1190 | " No attachments found for page ["+wi.name+"]. ", |
| 1191 | D.a(F.repoUrl('attachadd',{ |
| 1192 | target: wi.name, |
| 1193 | from: F.repoUrl('wikiedit',{name: wi.name})}), |
| 1194 | "Add attachments..." ) |
| 1195 | ); |
| 1196 | return this; |
| 1197 | } |
| @@ -1198,14 +1198,14 @@ | |
| 1198 | D.append( |
| 1199 | f.eAttach, |
| 1200 | D.append(D.p(), |
| 1201 | btnReload," ", |
| 1202 | D.a(F.repoUrl('attachlist',{page:wi.name}), |
| 1203 | "Attachments for page ["+wi.name+"]"), |
| 1204 | ". ", |
| 1205 | D.a(F.repoUrl('attachadd',{ |
| 1206 | target:wi.name, |
| 1207 | from: F.repoUrl('wikiedit',{name: wi.name})}), |
| 1208 | "Add attachments..." ) |
| 1209 | ) |
| 1210 | ); |
| 1211 | wi.attachments.forEach(function(a){ |
| @@ -1388,11 +1388,10 @@ | |
| 1388 | setting. |
| 1389 | */ |
| 1390 | P.baseHrefRestore = function(){ |
| 1391 | this.base.tag.href = this.base.originalHref; |
| 1392 | }; |
| 1393 | |
| 1394 | /** |
| 1395 | loadPage() loads the given wiki page and updates the relevant |
| 1396 | UI elements to reflect the loaded state. If passed no arguments |
| 1397 | then it re-uses the values from the currently-loaded page, reloading |
| @@ -1454,11 +1453,11 @@ | |
| 1453 | onload(r); |
| 1454 | } |
| 1455 | }); |
| 1456 | return this; |
| 1457 | }; |
| 1458 | |
| 1459 | /** |
| 1460 | Fetches the page preview based on the contents and settings of |
| 1461 | this page's input fields, and updates the UI with the |
| 1462 | preview. |
| 1463 | |
| 1464 |
+10
-10
| --- src/fossil.popupwidget.js | ||
| +++ src/fossil.popupwidget.js | ||
| @@ -55,11 +55,11 @@ | ||
| 55 | 55 | from the default), the class "fossil-PopupWidget" is always set |
| 56 | 56 | in order to allow certain app-internal CSS to account for popup |
| 57 | 57 | windows in special cases. |
| 58 | 58 | |
| 59 | 59 | .style: optional object of properties to copy directly into |
| 60 | - the element's style object. | |
| 60 | + the element's style object. | |
| 61 | 61 | |
| 62 | 62 | The options passed to this constructor get normalized into a |
| 63 | 63 | separate object which includes any default values for options not |
| 64 | 64 | provided by the caller. That object is available this the |
| 65 | 65 | resulting PopupWidget's options property. Default values for any |
| @@ -161,11 +161,11 @@ | ||
| 161 | 161 | Sidebar: showing/hiding the widget is, as is conventional for |
| 162 | 162 | this framework, done by removing/adding the 'hidden' CSS class |
| 163 | 163 | to it, so that class must be defined appropriately. |
| 164 | 164 | */ |
| 165 | 165 | show: function(){ |
| 166 | - var x = undefined, y = undefined, showIt, | |
| 166 | + let x = undefined, y = undefined, showIt, | |
| 167 | 167 | wasShown = !this.e.classList.contains('hidden'); |
| 168 | 168 | if(2===arguments.length){ |
| 169 | 169 | x = arguments[0]; |
| 170 | 170 | y = arguments[1]; |
| 171 | 171 | showIt = true; |
| @@ -344,11 +344,12 @@ | ||
| 344 | 344 | - No arguments, which is equivalent to passing the string |
| 345 | 345 | ".help-buttonlet:not(.processed)". |
| 346 | 346 | |
| 347 | 347 | Passing the same element(s) more than once is a no-op: during |
| 348 | 348 | initialization, each elements get the class'processed' added to |
| 349 | - it, and any elements with that class are skipped. | |
| 349 | + it, and any elements with that class are skipped. Each element | |
| 350 | + gets the 'help-buttonlet' CSS class added to it. | |
| 350 | 351 | |
| 351 | 352 | All child nodes of a help buttonlet are removed from the button |
| 352 | 353 | during initialization and stashed away for use in a PopupWidget |
| 353 | 354 | when the botton is clicked. |
| 354 | 355 | |
| @@ -374,24 +375,24 @@ | ||
| 374 | 375 | calculate the resulting size, then move and/or resize it. |
| 375 | 376 | |
| 376 | 377 | This algorithm/these heuristics can certainly be improved |
| 377 | 378 | upon. |
| 378 | 379 | */ |
| 379 | - var popupRect, rectElem = ev.target; | |
| 380 | + let popupRect, rectElem = ev.target; | |
| 380 | 381 | while(rectElem){ |
| 381 | 382 | popupRect = rectElem.getClientRects()[0]/*undefined if off-screen!*/; |
| 382 | 383 | if(popupRect) break; |
| 383 | 384 | rectElem = rectElem.parentNode; |
| 384 | 385 | } |
| 385 | 386 | if(!popupRect) popupRect = {x:0, y:0, left:0, right:0}; |
| 386 | - var x = popupRect.left, y = popupRect.top; | |
| 387 | + let x = popupRect.left, y = popupRect.top; | |
| 387 | 388 | if(x<0) x = 0; |
| 388 | 389 | if(y<0) y = 0; |
| 389 | 390 | if(rectElem){ |
| 390 | 391 | /* Try to ensure that the popup's z-level is higher than this element's */ |
| 391 | 392 | const rz = window.getComputedStyle(rectElem).zIndex; |
| 392 | - var myZ; | |
| 393 | + let myZ; | |
| 393 | 394 | if(rz && !isNaN(+rz)){ |
| 394 | 395 | myZ = +rz + 1; |
| 395 | 396 | }else{ |
| 396 | 397 | myZ = 10000/*guess!*/; |
| 397 | 398 | } |
| @@ -416,11 +417,11 @@ | ||
| 416 | 417 | fch.popup.show(x, y); |
| 417 | 418 | return false; |
| 418 | 419 | }; |
| 419 | 420 | f.foreachElement = function(e){ |
| 420 | 421 | if(e.classList.contains('processed')) return; |
| 421 | - e.classList.add('processed'); | |
| 422 | + e.classList.add('processed', 'help-buttonlet'); | |
| 422 | 423 | e.$helpContent = []; |
| 423 | 424 | /* We have to move all child nodes out of the way because we |
| 424 | 425 | cannot hide TEXT nodes via CSS (which cannot select TEXT |
| 425 | 426 | nodes). We have to do it in two steps to avoid invaliding |
| 426 | 427 | the list during traversal. */ |
| @@ -427,11 +428,11 @@ | ||
| 427 | 428 | e.childNodes.forEach((ch)=>e.$helpContent.push(ch)); |
| 428 | 429 | e.$helpContent.forEach((ch)=>ch.remove()); |
| 429 | 430 | e.addEventListener('click', f.clickHandler, false); |
| 430 | 431 | }; |
| 431 | 432 | }/*static init*/ |
| 432 | - var elems; | |
| 433 | + let elems; | |
| 433 | 434 | if(!arguments.length){ |
| 434 | 435 | arguments[0] = '.help-buttonlet:not(.processed)'; |
| 435 | 436 | arguments.length = 1; |
| 436 | 437 | } |
| 437 | 438 | if(arguments.length){ |
| @@ -443,11 +444,11 @@ | ||
| 443 | 444 | elems = arguments[0]; |
| 444 | 445 | } |
| 445 | 446 | } |
| 446 | 447 | if(elems) elems.forEach(f.foreachElement); |
| 447 | 448 | }, |
| 448 | - | |
| 449 | + | |
| 449 | 450 | /** |
| 450 | 451 | Sets up the given element as a "help buttonlet", adding the CSS |
| 451 | 452 | class help-buttonlet to it. Any (optional) arguments after the |
| 452 | 453 | first are appended to the element using fossil.dom.append(), so |
| 453 | 454 | that they become the content for the buttonlet's popup help. |
| @@ -465,7 +466,6 @@ | ||
| 465 | 466 | return elem; |
| 466 | 467 | } |
| 467 | 468 | }/*helpButtonlets*/; |
| 468 | 469 | |
| 469 | 470 | F.onDOMContentLoaded( ()=>F.helpButtonlets.setup() ); |
| 470 | - | |
| 471 | 471 | })(window.fossil); |
| 472 | 472 |
| --- src/fossil.popupwidget.js | |
| +++ src/fossil.popupwidget.js | |
| @@ -55,11 +55,11 @@ | |
| 55 | from the default), the class "fossil-PopupWidget" is always set |
| 56 | in order to allow certain app-internal CSS to account for popup |
| 57 | windows in special cases. |
| 58 | |
| 59 | .style: optional object of properties to copy directly into |
| 60 | the element's style object. |
| 61 | |
| 62 | The options passed to this constructor get normalized into a |
| 63 | separate object which includes any default values for options not |
| 64 | provided by the caller. That object is available this the |
| 65 | resulting PopupWidget's options property. Default values for any |
| @@ -161,11 +161,11 @@ | |
| 161 | Sidebar: showing/hiding the widget is, as is conventional for |
| 162 | this framework, done by removing/adding the 'hidden' CSS class |
| 163 | to it, so that class must be defined appropriately. |
| 164 | */ |
| 165 | show: function(){ |
| 166 | var x = undefined, y = undefined, showIt, |
| 167 | wasShown = !this.e.classList.contains('hidden'); |
| 168 | if(2===arguments.length){ |
| 169 | x = arguments[0]; |
| 170 | y = arguments[1]; |
| 171 | showIt = true; |
| @@ -344,11 +344,12 @@ | |
| 344 | - No arguments, which is equivalent to passing the string |
| 345 | ".help-buttonlet:not(.processed)". |
| 346 | |
| 347 | Passing the same element(s) more than once is a no-op: during |
| 348 | initialization, each elements get the class'processed' added to |
| 349 | it, and any elements with that class are skipped. |
| 350 | |
| 351 | All child nodes of a help buttonlet are removed from the button |
| 352 | during initialization and stashed away for use in a PopupWidget |
| 353 | when the botton is clicked. |
| 354 | |
| @@ -374,24 +375,24 @@ | |
| 374 | calculate the resulting size, then move and/or resize it. |
| 375 | |
| 376 | This algorithm/these heuristics can certainly be improved |
| 377 | upon. |
| 378 | */ |
| 379 | var popupRect, rectElem = ev.target; |
| 380 | while(rectElem){ |
| 381 | popupRect = rectElem.getClientRects()[0]/*undefined if off-screen!*/; |
| 382 | if(popupRect) break; |
| 383 | rectElem = rectElem.parentNode; |
| 384 | } |
| 385 | if(!popupRect) popupRect = {x:0, y:0, left:0, right:0}; |
| 386 | var x = popupRect.left, y = popupRect.top; |
| 387 | if(x<0) x = 0; |
| 388 | if(y<0) y = 0; |
| 389 | if(rectElem){ |
| 390 | /* Try to ensure that the popup's z-level is higher than this element's */ |
| 391 | const rz = window.getComputedStyle(rectElem).zIndex; |
| 392 | var myZ; |
| 393 | if(rz && !isNaN(+rz)){ |
| 394 | myZ = +rz + 1; |
| 395 | }else{ |
| 396 | myZ = 10000/*guess!*/; |
| 397 | } |
| @@ -416,11 +417,11 @@ | |
| 416 | fch.popup.show(x, y); |
| 417 | return false; |
| 418 | }; |
| 419 | f.foreachElement = function(e){ |
| 420 | if(e.classList.contains('processed')) return; |
| 421 | e.classList.add('processed'); |
| 422 | e.$helpContent = []; |
| 423 | /* We have to move all child nodes out of the way because we |
| 424 | cannot hide TEXT nodes via CSS (which cannot select TEXT |
| 425 | nodes). We have to do it in two steps to avoid invaliding |
| 426 | the list during traversal. */ |
| @@ -427,11 +428,11 @@ | |
| 427 | e.childNodes.forEach((ch)=>e.$helpContent.push(ch)); |
| 428 | e.$helpContent.forEach((ch)=>ch.remove()); |
| 429 | e.addEventListener('click', f.clickHandler, false); |
| 430 | }; |
| 431 | }/*static init*/ |
| 432 | var elems; |
| 433 | if(!arguments.length){ |
| 434 | arguments[0] = '.help-buttonlet:not(.processed)'; |
| 435 | arguments.length = 1; |
| 436 | } |
| 437 | if(arguments.length){ |
| @@ -443,11 +444,11 @@ | |
| 443 | elems = arguments[0]; |
| 444 | } |
| 445 | } |
| 446 | if(elems) elems.forEach(f.foreachElement); |
| 447 | }, |
| 448 | |
| 449 | /** |
| 450 | Sets up the given element as a "help buttonlet", adding the CSS |
| 451 | class help-buttonlet to it. Any (optional) arguments after the |
| 452 | first are appended to the element using fossil.dom.append(), so |
| 453 | that they become the content for the buttonlet's popup help. |
| @@ -465,7 +466,6 @@ | |
| 465 | return elem; |
| 466 | } |
| 467 | }/*helpButtonlets*/; |
| 468 | |
| 469 | F.onDOMContentLoaded( ()=>F.helpButtonlets.setup() ); |
| 470 | |
| 471 | })(window.fossil); |
| 472 |
| --- src/fossil.popupwidget.js | |
| +++ src/fossil.popupwidget.js | |
| @@ -55,11 +55,11 @@ | |
| 55 | from the default), the class "fossil-PopupWidget" is always set |
| 56 | in order to allow certain app-internal CSS to account for popup |
| 57 | windows in special cases. |
| 58 | |
| 59 | .style: optional object of properties to copy directly into |
| 60 | the element's style object. |
| 61 | |
| 62 | The options passed to this constructor get normalized into a |
| 63 | separate object which includes any default values for options not |
| 64 | provided by the caller. That object is available this the |
| 65 | resulting PopupWidget's options property. Default values for any |
| @@ -161,11 +161,11 @@ | |
| 161 | Sidebar: showing/hiding the widget is, as is conventional for |
| 162 | this framework, done by removing/adding the 'hidden' CSS class |
| 163 | to it, so that class must be defined appropriately. |
| 164 | */ |
| 165 | show: function(){ |
| 166 | let x = undefined, y = undefined, showIt, |
| 167 | wasShown = !this.e.classList.contains('hidden'); |
| 168 | if(2===arguments.length){ |
| 169 | x = arguments[0]; |
| 170 | y = arguments[1]; |
| 171 | showIt = true; |
| @@ -344,11 +344,12 @@ | |
| 344 | - No arguments, which is equivalent to passing the string |
| 345 | ".help-buttonlet:not(.processed)". |
| 346 | |
| 347 | Passing the same element(s) more than once is a no-op: during |
| 348 | initialization, each elements get the class'processed' added to |
| 349 | it, and any elements with that class are skipped. Each element |
| 350 | gets the 'help-buttonlet' CSS class added to it. |
| 351 | |
| 352 | All child nodes of a help buttonlet are removed from the button |
| 353 | during initialization and stashed away for use in a PopupWidget |
| 354 | when the botton is clicked. |
| 355 | |
| @@ -374,24 +375,24 @@ | |
| 375 | calculate the resulting size, then move and/or resize it. |
| 376 | |
| 377 | This algorithm/these heuristics can certainly be improved |
| 378 | upon. |
| 379 | */ |
| 380 | let popupRect, rectElem = ev.target; |
| 381 | while(rectElem){ |
| 382 | popupRect = rectElem.getClientRects()[0]/*undefined if off-screen!*/; |
| 383 | if(popupRect) break; |
| 384 | rectElem = rectElem.parentNode; |
| 385 | } |
| 386 | if(!popupRect) popupRect = {x:0, y:0, left:0, right:0}; |
| 387 | let x = popupRect.left, y = popupRect.top; |
| 388 | if(x<0) x = 0; |
| 389 | if(y<0) y = 0; |
| 390 | if(rectElem){ |
| 391 | /* Try to ensure that the popup's z-level is higher than this element's */ |
| 392 | const rz = window.getComputedStyle(rectElem).zIndex; |
| 393 | let myZ; |
| 394 | if(rz && !isNaN(+rz)){ |
| 395 | myZ = +rz + 1; |
| 396 | }else{ |
| 397 | myZ = 10000/*guess!*/; |
| 398 | } |
| @@ -416,11 +417,11 @@ | |
| 417 | fch.popup.show(x, y); |
| 418 | return false; |
| 419 | }; |
| 420 | f.foreachElement = function(e){ |
| 421 | if(e.classList.contains('processed')) return; |
| 422 | e.classList.add('processed', 'help-buttonlet'); |
| 423 | e.$helpContent = []; |
| 424 | /* We have to move all child nodes out of the way because we |
| 425 | cannot hide TEXT nodes via CSS (which cannot select TEXT |
| 426 | nodes). We have to do it in two steps to avoid invaliding |
| 427 | the list during traversal. */ |
| @@ -427,11 +428,11 @@ | |
| 428 | e.childNodes.forEach((ch)=>e.$helpContent.push(ch)); |
| 429 | e.$helpContent.forEach((ch)=>ch.remove()); |
| 430 | e.addEventListener('click', f.clickHandler, false); |
| 431 | }; |
| 432 | }/*static init*/ |
| 433 | let elems; |
| 434 | if(!arguments.length){ |
| 435 | arguments[0] = '.help-buttonlet:not(.processed)'; |
| 436 | arguments.length = 1; |
| 437 | } |
| 438 | if(arguments.length){ |
| @@ -443,11 +444,11 @@ | |
| 444 | elems = arguments[0]; |
| 445 | } |
| 446 | } |
| 447 | if(elems) elems.forEach(f.foreachElement); |
| 448 | }, |
| 449 | |
| 450 | /** |
| 451 | Sets up the given element as a "help buttonlet", adding the CSS |
| 452 | class help-buttonlet to it. Any (optional) arguments after the |
| 453 | first are appended to the element using fossil.dom.append(), so |
| 454 | that they become the content for the buttonlet's popup help. |
| @@ -465,7 +466,6 @@ | |
| 466 | return elem; |
| 467 | } |
| 468 | }/*helpButtonlets*/; |
| 469 | |
| 470 | F.onDOMContentLoaded( ()=>F.helpButtonlets.setup() ); |
| 471 | })(window.fossil); |
| 472 |
+62
-7
| --- src/fossil.storage.js | ||
| +++ src/fossil.storage.js | ||
| @@ -82,24 +82,53 @@ | ||
| 82 | 82 | )+'::' : ( |
| 83 | 83 | '' /* transient storage */ |
| 84 | 84 | ) |
| 85 | 85 | ); |
| 86 | 86 | |
| 87 | + /** | |
| 88 | + Proxy for custom events. Created on demand. | |
| 89 | + */ | |
| 90 | + let events; | |
| 87 | 91 | /** |
| 88 | 92 | A proxy for localStorage or sessionStorage or a |
| 89 | 93 | page-instance-local proxy, if neither one is availble. |
| 90 | 94 | |
| 91 | 95 | Which exact storage implementation is uses is unspecified, and |
| 92 | 96 | apps must not rely on it. |
| 93 | 97 | */ |
| 94 | 98 | F.storage = { |
| 95 | 99 | storageKeyPrefix: storageKeyPrefix, |
| 100 | + addEventListener(...args){ | |
| 101 | + events ??= new EventTarget() | |
| 102 | + return events.addEventListener(...args); | |
| 103 | + }, | |
| 104 | + removeEventListener(...args){ | |
| 105 | + events ??= new EventTarget() | |
| 106 | + return events.removeEventListener(...args); | |
| 107 | + }, | |
| 96 | 108 | /** Sets the storage key k to value v, implicitly converting |
| 97 | - it to a string. */ | |
| 98 | - set: (k,v)=>$storage.setItem(storageKeyPrefix+k,v), | |
| 109 | + it to a string. | |
| 110 | + | |
| 111 | + Fires a 'set' CustomEvent with a detail value in the form | |
| 112 | + {key, value} with the new value. | |
| 113 | + */ | |
| 114 | + set: (k,v)=>{ | |
| 115 | + $storage.setItem(storageKeyPrefix+k,v); | |
| 116 | + if( events ){ | |
| 117 | + events.dispatchEvent( | |
| 118 | + new CustomEvent('set',{ | |
| 119 | + detail: F.nu({ | |
| 120 | + key: k, value: v | |
| 121 | + }) | |
| 122 | + }) | |
| 123 | + ); | |
| 124 | + } | |
| 125 | + }, | |
| 99 | 126 | /** Sets storage key k to JSON.stringify(v). */ |
| 100 | - setJSON: (k,v)=>$storage.setItem(storageKeyPrefix+k,JSON.stringify(v)), | |
| 127 | + setJSON: function(k,v){ | |
| 128 | + return this.set(k,JSON.stringify(v)); | |
| 129 | + }, | |
| 101 | 130 | /** Returns the value for the given storage key, or |
| 102 | 131 | dflt if the key is not found in the storage. */ |
| 103 | 132 | get: (k,dflt)=>$storageHolder.hasOwnProperty( |
| 104 | 133 | storageKeyPrefix+k |
| 105 | 134 | ) ? $storage.getItem(storageKeyPrefix+k) : dflt, |
| @@ -121,22 +150,48 @@ | ||
| 121 | 150 | catch(e){return dflt} |
| 122 | 151 | }, |
| 123 | 152 | /** Returns true if the storage contains the given key, |
| 124 | 153 | else false. */ |
| 125 | 154 | contains: (k)=>$storageHolder.hasOwnProperty(storageKeyPrefix+k), |
| 126 | - /** Removes the given key from the storage. Returns this. */ | |
| 155 | + /** | |
| 156 | + Removes the given key from the storage. Returns this. | |
| 157 | + | |
| 158 | + Fires a 'remove' CustomEvent with a detail value in the form | |
| 159 | + {key}. | |
| 160 | + */ | |
| 127 | 161 | remove: function(k){ |
| 128 | - $storage.removeItem(storageKeyPrefix+k); | |
| 162 | + const kk = storageKeyPrefix+k; | |
| 163 | + if( events ){ | |
| 164 | + const had = $storageHolder.hasOwnProperty(kk) | |
| 165 | + $storage.removeItem(kk); | |
| 166 | + if( had ){ | |
| 167 | + events.dispatchEvent( | |
| 168 | + new CustomEvent('remove',{ | |
| 169 | + detail: F.nu({key: k}) | |
| 170 | + }) | |
| 171 | + ); | |
| 172 | + } | |
| 173 | + }else{ | |
| 174 | + $storage.removeItem(kk); | |
| 175 | + } | |
| 129 | 176 | return this; |
| 130 | 177 | }, |
| 131 | 178 | /** Clears ALL keys from the storage. Returns this. */ |
| 132 | 179 | clear: function(){ |
| 133 | 180 | this.keys().forEach((k)=>$storage.removeItem(/*w/o prefix*/k)); |
| 134 | 181 | return this; |
| 135 | 182 | }, |
| 136 | - /** Returns an array of all keys currently in the storage. */ | |
| 137 | - keys: ()=>Object.keys($storageHolder).filter((v)=>(v||'').startsWith(storageKeyPrefix)), | |
| 183 | + /** Returns an array of all keys currently in the storage. If full | |
| 184 | + is true then the keys include the storage key prefix, else | |
| 185 | + they don't. It should default to false but does not for | |
| 186 | + historical compatibility. */ | |
| 187 | + keys: function(full=true){ | |
| 188 | + const li = Object.keys($storageHolder).filter((v)=>(v||'').startsWith(storageKeyPrefix)); | |
| 189 | + if( full ) return li; | |
| 190 | + const n = this.storageKeyPrefix.length; | |
| 191 | + return li.map(v=>v.substring(n)); | |
| 192 | + }, | |
| 138 | 193 | /** Returns true if this storage is transient (only available |
| 139 | 194 | until the page is reloaded), indicating that fileStorage |
| 140 | 195 | and sessionStorage are unavailable. */ |
| 141 | 196 | isTransient: ()=>$storageHolder!==$storage, |
| 142 | 197 | /** Returns a symbolic name for the current storage mechanism. */ |
| 143 | 198 |
| --- src/fossil.storage.js | |
| +++ src/fossil.storage.js | |
| @@ -82,24 +82,53 @@ | |
| 82 | )+'::' : ( |
| 83 | '' /* transient storage */ |
| 84 | ) |
| 85 | ); |
| 86 | |
| 87 | /** |
| 88 | A proxy for localStorage or sessionStorage or a |
| 89 | page-instance-local proxy, if neither one is availble. |
| 90 | |
| 91 | Which exact storage implementation is uses is unspecified, and |
| 92 | apps must not rely on it. |
| 93 | */ |
| 94 | F.storage = { |
| 95 | storageKeyPrefix: storageKeyPrefix, |
| 96 | /** Sets the storage key k to value v, implicitly converting |
| 97 | it to a string. */ |
| 98 | set: (k,v)=>$storage.setItem(storageKeyPrefix+k,v), |
| 99 | /** Sets storage key k to JSON.stringify(v). */ |
| 100 | setJSON: (k,v)=>$storage.setItem(storageKeyPrefix+k,JSON.stringify(v)), |
| 101 | /** Returns the value for the given storage key, or |
| 102 | dflt if the key is not found in the storage. */ |
| 103 | get: (k,dflt)=>$storageHolder.hasOwnProperty( |
| 104 | storageKeyPrefix+k |
| 105 | ) ? $storage.getItem(storageKeyPrefix+k) : dflt, |
| @@ -121,22 +150,48 @@ | |
| 121 | catch(e){return dflt} |
| 122 | }, |
| 123 | /** Returns true if the storage contains the given key, |
| 124 | else false. */ |
| 125 | contains: (k)=>$storageHolder.hasOwnProperty(storageKeyPrefix+k), |
| 126 | /** Removes the given key from the storage. Returns this. */ |
| 127 | remove: function(k){ |
| 128 | $storage.removeItem(storageKeyPrefix+k); |
| 129 | return this; |
| 130 | }, |
| 131 | /** Clears ALL keys from the storage. Returns this. */ |
| 132 | clear: function(){ |
| 133 | this.keys().forEach((k)=>$storage.removeItem(/*w/o prefix*/k)); |
| 134 | return this; |
| 135 | }, |
| 136 | /** Returns an array of all keys currently in the storage. */ |
| 137 | keys: ()=>Object.keys($storageHolder).filter((v)=>(v||'').startsWith(storageKeyPrefix)), |
| 138 | /** Returns true if this storage is transient (only available |
| 139 | until the page is reloaded), indicating that fileStorage |
| 140 | and sessionStorage are unavailable. */ |
| 141 | isTransient: ()=>$storageHolder!==$storage, |
| 142 | /** Returns a symbolic name for the current storage mechanism. */ |
| 143 |
| --- src/fossil.storage.js | |
| +++ src/fossil.storage.js | |
| @@ -82,24 +82,53 @@ | |
| 82 | )+'::' : ( |
| 83 | '' /* transient storage */ |
| 84 | ) |
| 85 | ); |
| 86 | |
| 87 | /** |
| 88 | Proxy for custom events. Created on demand. |
| 89 | */ |
| 90 | let events; |
| 91 | /** |
| 92 | A proxy for localStorage or sessionStorage or a |
| 93 | page-instance-local proxy, if neither one is availble. |
| 94 | |
| 95 | Which exact storage implementation is uses is unspecified, and |
| 96 | apps must not rely on it. |
| 97 | */ |
| 98 | F.storage = { |
| 99 | storageKeyPrefix: storageKeyPrefix, |
| 100 | addEventListener(...args){ |
| 101 | events ??= new EventTarget() |
| 102 | return events.addEventListener(...args); |
| 103 | }, |
| 104 | removeEventListener(...args){ |
| 105 | events ??= new EventTarget() |
| 106 | return events.removeEventListener(...args); |
| 107 | }, |
| 108 | /** Sets the storage key k to value v, implicitly converting |
| 109 | it to a string. |
| 110 | |
| 111 | Fires a 'set' CustomEvent with a detail value in the form |
| 112 | {key, value} with the new value. |
| 113 | */ |
| 114 | set: (k,v)=>{ |
| 115 | $storage.setItem(storageKeyPrefix+k,v); |
| 116 | if( events ){ |
| 117 | events.dispatchEvent( |
| 118 | new CustomEvent('set',{ |
| 119 | detail: F.nu({ |
| 120 | key: k, value: v |
| 121 | }) |
| 122 | }) |
| 123 | ); |
| 124 | } |
| 125 | }, |
| 126 | /** Sets storage key k to JSON.stringify(v). */ |
| 127 | setJSON: function(k,v){ |
| 128 | return this.set(k,JSON.stringify(v)); |
| 129 | }, |
| 130 | /** Returns the value for the given storage key, or |
| 131 | dflt if the key is not found in the storage. */ |
| 132 | get: (k,dflt)=>$storageHolder.hasOwnProperty( |
| 133 | storageKeyPrefix+k |
| 134 | ) ? $storage.getItem(storageKeyPrefix+k) : dflt, |
| @@ -121,22 +150,48 @@ | |
| 150 | catch(e){return dflt} |
| 151 | }, |
| 152 | /** Returns true if the storage contains the given key, |
| 153 | else false. */ |
| 154 | contains: (k)=>$storageHolder.hasOwnProperty(storageKeyPrefix+k), |
| 155 | /** |
| 156 | Removes the given key from the storage. Returns this. |
| 157 | |
| 158 | Fires a 'remove' CustomEvent with a detail value in the form |
| 159 | {key}. |
| 160 | */ |
| 161 | remove: function(k){ |
| 162 | const kk = storageKeyPrefix+k; |
| 163 | if( events ){ |
| 164 | const had = $storageHolder.hasOwnProperty(kk) |
| 165 | $storage.removeItem(kk); |
| 166 | if( had ){ |
| 167 | events.dispatchEvent( |
| 168 | new CustomEvent('remove',{ |
| 169 | detail: F.nu({key: k}) |
| 170 | }) |
| 171 | ); |
| 172 | } |
| 173 | }else{ |
| 174 | $storage.removeItem(kk); |
| 175 | } |
| 176 | return this; |
| 177 | }, |
| 178 | /** Clears ALL keys from the storage. Returns this. */ |
| 179 | clear: function(){ |
| 180 | this.keys().forEach((k)=>$storage.removeItem(/*w/o prefix*/k)); |
| 181 | return this; |
| 182 | }, |
| 183 | /** Returns an array of all keys currently in the storage. If full |
| 184 | is true then the keys include the storage key prefix, else |
| 185 | they don't. It should default to false but does not for |
| 186 | historical compatibility. */ |
| 187 | keys: function(full=true){ |
| 188 | const li = Object.keys($storageHolder).filter((v)=>(v||'').startsWith(storageKeyPrefix)); |
| 189 | if( full ) return li; |
| 190 | const n = this.storageKeyPrefix.length; |
| 191 | return li.map(v=>v.substring(n)); |
| 192 | }, |
| 193 | /** Returns true if this storage is transient (only available |
| 194 | until the page is reloaded), indicating that fileStorage |
| 195 | and sessionStorage are unavailable. */ |
| 196 | isTransient: ()=>$storageHolder!==$storage, |
| 197 | /** Returns a symbolic name for the current storage mechanism. */ |
| 198 |
+3
-2
| --- src/fossil.tabs.js | ||
| +++ src/fossil.tabs.js | ||
| @@ -7,11 +7,11 @@ | ||
| 7 | 7 | /** |
| 8 | 8 | Creates a TabManager. If passed a truthy first argument, it is |
| 9 | 9 | passed to init(). If passed a truthy second argument, it must be |
| 10 | 10 | an Object holding configuration options: |
| 11 | 11 | |
| 12 | - { | |
| 12 | + { | |
| 13 | 13 | tabAccessKeys: boolean (=true) |
| 14 | 14 | If true, tab buttons are assigned "accesskey" values |
| 15 | 15 | equal to their 1-based tab number. |
| 16 | 16 | } |
| 17 | 17 | */ |
| @@ -56,11 +56,11 @@ | ||
| 56 | 56 | /** |
| 57 | 57 | Initializes the tabs associated with the given tab container |
| 58 | 58 | (DOM element or selector for a single element). This must be |
| 59 | 59 | called once before using any other member functions of a given |
| 60 | 60 | instance, noting that the constructor will call this if it is |
| 61 | - passed an argument. | |
| 61 | + passed an argument. | |
| 62 | 62 | |
| 63 | 63 | The tab container must have an 'id' attribute. This function |
| 64 | 64 | looks through the DOM for all elements which have |
| 65 | 65 | data-tab-parent=thatId. For each one it creates a button to |
| 66 | 66 | switch to that tab and moves the element into this.e.tabs, |
| @@ -150,10 +150,11 @@ | ||
| 150 | 150 | e.target.$manager.switchToTab(e.target.$tab); |
| 151 | 151 | }; |
| 152 | 152 | } |
| 153 | 153 | tab = tabArg(tab); |
| 154 | 154 | tab.remove(); |
| 155 | + tab.classList.add('hidden'); | |
| 155 | 156 | D.append(this.e.tabs, D.addClass(tab,'tab-panel')); |
| 156 | 157 | const tabCount = this.e.tabBar.childNodes.length+1; |
| 157 | 158 | const lbl = tab.dataset.tabLabel || 'Tab #'+tabCount; |
| 158 | 159 | const btn = D.addClass(D.append(D.span(), lbl), 'tab-button'); |
| 159 | 160 | D.append(this.e.tabBar,btn); |
| 160 | 161 |
| --- src/fossil.tabs.js | |
| +++ src/fossil.tabs.js | |
| @@ -7,11 +7,11 @@ | |
| 7 | /** |
| 8 | Creates a TabManager. If passed a truthy first argument, it is |
| 9 | passed to init(). If passed a truthy second argument, it must be |
| 10 | an Object holding configuration options: |
| 11 | |
| 12 | { |
| 13 | tabAccessKeys: boolean (=true) |
| 14 | If true, tab buttons are assigned "accesskey" values |
| 15 | equal to their 1-based tab number. |
| 16 | } |
| 17 | */ |
| @@ -56,11 +56,11 @@ | |
| 56 | /** |
| 57 | Initializes the tabs associated with the given tab container |
| 58 | (DOM element or selector for a single element). This must be |
| 59 | called once before using any other member functions of a given |
| 60 | instance, noting that the constructor will call this if it is |
| 61 | passed an argument. |
| 62 | |
| 63 | The tab container must have an 'id' attribute. This function |
| 64 | looks through the DOM for all elements which have |
| 65 | data-tab-parent=thatId. For each one it creates a button to |
| 66 | switch to that tab and moves the element into this.e.tabs, |
| @@ -150,10 +150,11 @@ | |
| 150 | e.target.$manager.switchToTab(e.target.$tab); |
| 151 | }; |
| 152 | } |
| 153 | tab = tabArg(tab); |
| 154 | tab.remove(); |
| 155 | D.append(this.e.tabs, D.addClass(tab,'tab-panel')); |
| 156 | const tabCount = this.e.tabBar.childNodes.length+1; |
| 157 | const lbl = tab.dataset.tabLabel || 'Tab #'+tabCount; |
| 158 | const btn = D.addClass(D.append(D.span(), lbl), 'tab-button'); |
| 159 | D.append(this.e.tabBar,btn); |
| 160 |
| --- src/fossil.tabs.js | |
| +++ src/fossil.tabs.js | |
| @@ -7,11 +7,11 @@ | |
| 7 | /** |
| 8 | Creates a TabManager. If passed a truthy first argument, it is |
| 9 | passed to init(). If passed a truthy second argument, it must be |
| 10 | an Object holding configuration options: |
| 11 | |
| 12 | { |
| 13 | tabAccessKeys: boolean (=true) |
| 14 | If true, tab buttons are assigned "accesskey" values |
| 15 | equal to their 1-based tab number. |
| 16 | } |
| 17 | */ |
| @@ -56,11 +56,11 @@ | |
| 56 | /** |
| 57 | Initializes the tabs associated with the given tab container |
| 58 | (DOM element or selector for a single element). This must be |
| 59 | called once before using any other member functions of a given |
| 60 | instance, noting that the constructor will call this if it is |
| 61 | passed an argument. |
| 62 | |
| 63 | The tab container must have an 'id' attribute. This function |
| 64 | looks through the DOM for all elements which have |
| 65 | data-tab-parent=thatId. For each one it creates a button to |
| 66 | switch to that tab and moves the element into this.e.tabs, |
| @@ -150,10 +150,11 @@ | |
| 150 | e.target.$manager.switchToTab(e.target.$tab); |
| 151 | }; |
| 152 | } |
| 153 | tab = tabArg(tab); |
| 154 | tab.remove(); |
| 155 | tab.classList.add('hidden'); |
| 156 | D.append(this.e.tabs, D.addClass(tab,'tab-panel')); |
| 157 | const tabCount = this.e.tabBar.childNodes.length+1; |
| 158 | const lbl = tab.dataset.tabLabel || 'Tab #'+tabCount; |
| 159 | const btn = D.addClass(D.append(D.span(), lbl), 'tab-button'); |
| 160 | D.append(this.e.tabBar,btn); |
| 161 |
+6
-2
| --- src/info.c | ||
| +++ src/info.c | ||
| @@ -1931,11 +1931,11 @@ | ||
| 1931 | 1931 | @ Also attachment "%h(zFilename)" to |
| 1932 | 1932 | }else{ |
| 1933 | 1933 | @ Attachment "%h(zFilename)" to |
| 1934 | 1934 | } |
| 1935 | 1935 | objType |= OBJTYPE_ATTACHMENT; |
| 1936 | - switch( attachment_target_type(zTarget) ){ | |
| 1936 | + switch( attachment_target_type(zTarget, 1) ){ | |
| 1937 | 1937 | case CFTYPE_FORUM: |
| 1938 | 1938 | if( g.perm.Hyperlink && g.anon.RdForum ){ |
| 1939 | 1939 | @ forum post [%z(href("%R/forumpost/%!S",zTarget))%S(zTarget)</a>] |
| 1940 | 1940 | }else{ |
| 1941 | 1941 | @ forum post [%S(zTarget)] |
| @@ -1954,16 +1954,20 @@ | ||
| 1954 | 1954 | }else{ |
| 1955 | 1955 | @ tech note [%S(zTarget)] |
| 1956 | 1956 | } |
| 1957 | 1957 | break; |
| 1958 | 1958 | case CFTYPE_WIKI: |
| 1959 | - default /* historical behavior - assume wiki */: | |
| 1960 | 1959 | if( g.perm.Hyperlink && g.anon.RdWiki ){ |
| 1961 | 1960 | @ wiki page [%z(href("%R/wiki?name=%t",zTarget))%h(zTarget)</a>] |
| 1962 | 1961 | }else{ |
| 1963 | 1962 | @ wiki page [%h(zTarget)] |
| 1964 | 1963 | } |
| 1964 | + break; | |
| 1965 | + default: | |
| 1966 | + /* historical behavior is to assume wiki, but we can end up | |
| 1967 | + ** showing bogus links that way to stale attachments. */ | |
| 1968 | + @ unknown artifact %h(zTarget) | |
| 1965 | 1969 | } |
| 1966 | 1970 | @ added by |
| 1967 | 1971 | hyperlink_to_user(zUser,zDate," on"); |
| 1968 | 1972 | hyperlink_to_date(zDate,"."); |
| 1969 | 1973 | cnt++; |
| 1970 | 1974 |
| --- src/info.c | |
| +++ src/info.c | |
| @@ -1931,11 +1931,11 @@ | |
| 1931 | @ Also attachment "%h(zFilename)" to |
| 1932 | }else{ |
| 1933 | @ Attachment "%h(zFilename)" to |
| 1934 | } |
| 1935 | objType |= OBJTYPE_ATTACHMENT; |
| 1936 | switch( attachment_target_type(zTarget) ){ |
| 1937 | case CFTYPE_FORUM: |
| 1938 | if( g.perm.Hyperlink && g.anon.RdForum ){ |
| 1939 | @ forum post [%z(href("%R/forumpost/%!S",zTarget))%S(zTarget)</a>] |
| 1940 | }else{ |
| 1941 | @ forum post [%S(zTarget)] |
| @@ -1954,16 +1954,20 @@ | |
| 1954 | }else{ |
| 1955 | @ tech note [%S(zTarget)] |
| 1956 | } |
| 1957 | break; |
| 1958 | case CFTYPE_WIKI: |
| 1959 | default /* historical behavior - assume wiki */: |
| 1960 | if( g.perm.Hyperlink && g.anon.RdWiki ){ |
| 1961 | @ wiki page [%z(href("%R/wiki?name=%t",zTarget))%h(zTarget)</a>] |
| 1962 | }else{ |
| 1963 | @ wiki page [%h(zTarget)] |
| 1964 | } |
| 1965 | } |
| 1966 | @ added by |
| 1967 | hyperlink_to_user(zUser,zDate," on"); |
| 1968 | hyperlink_to_date(zDate,"."); |
| 1969 | cnt++; |
| 1970 |
| --- src/info.c | |
| +++ src/info.c | |
| @@ -1931,11 +1931,11 @@ | |
| 1931 | @ Also attachment "%h(zFilename)" to |
| 1932 | }else{ |
| 1933 | @ Attachment "%h(zFilename)" to |
| 1934 | } |
| 1935 | objType |= OBJTYPE_ATTACHMENT; |
| 1936 | switch( attachment_target_type(zTarget, 1) ){ |
| 1937 | case CFTYPE_FORUM: |
| 1938 | if( g.perm.Hyperlink && g.anon.RdForum ){ |
| 1939 | @ forum post [%z(href("%R/forumpost/%!S",zTarget))%S(zTarget)</a>] |
| 1940 | }else{ |
| 1941 | @ forum post [%S(zTarget)] |
| @@ -1954,16 +1954,20 @@ | |
| 1954 | }else{ |
| 1955 | @ tech note [%S(zTarget)] |
| 1956 | } |
| 1957 | break; |
| 1958 | case CFTYPE_WIKI: |
| 1959 | if( g.perm.Hyperlink && g.anon.RdWiki ){ |
| 1960 | @ wiki page [%z(href("%R/wiki?name=%t",zTarget))%h(zTarget)</a>] |
| 1961 | }else{ |
| 1962 | @ wiki page [%h(zTarget)] |
| 1963 | } |
| 1964 | break; |
| 1965 | default: |
| 1966 | /* historical behavior is to assume wiki, but we can end up |
| 1967 | ** showing bogus links that way to stale attachments. */ |
| 1968 | @ unknown artifact %h(zTarget) |
| 1969 | } |
| 1970 | @ added by |
| 1971 | hyperlink_to_user(zUser,zDate," on"); |
| 1972 | hyperlink_to_date(zDate,"."); |
| 1973 | cnt++; |
| 1974 |
+2
| --- src/main.mk | ||
| +++ src/main.mk | ||
| @@ -225,10 +225,11 @@ | ||
| 225 | 225 | $(SRCDIR)/copybtn.js \ |
| 226 | 226 | $(SRCDIR)/default.css \ |
| 227 | 227 | $(SRCDIR)/diff.js \ |
| 228 | 228 | $(SRCDIR)/diff.tcl \ |
| 229 | 229 | $(SRCDIR)/forum.js \ |
| 230 | + $(SRCDIR)/fossil.attach.js \ | |
| 230 | 231 | $(SRCDIR)/fossil.bootstrap.js \ |
| 231 | 232 | $(SRCDIR)/fossil.confirmer.js \ |
| 232 | 233 | $(SRCDIR)/fossil.copybutton.js \ |
| 233 | 234 | $(SRCDIR)/fossil.diff.js \ |
| 234 | 235 | $(SRCDIR)/fossil.dom.js \ |
| @@ -275,10 +276,11 @@ | ||
| 275 | 276 | $(SRCDIR)/sounds/e.wav \ |
| 276 | 277 | $(SRCDIR)/sounds/f.wav \ |
| 277 | 278 | $(SRCDIR)/style.admin_log.css \ |
| 278 | 279 | $(SRCDIR)/style.chat.css \ |
| 279 | 280 | $(SRCDIR)/style.fileedit.css \ |
| 281 | + $(SRCDIR)/style.forum.css \ | |
| 280 | 282 | $(SRCDIR)/style.pikchrshow.css \ |
| 281 | 283 | $(SRCDIR)/style.uvlist.css \ |
| 282 | 284 | $(SRCDIR)/style.wikiedit.css \ |
| 283 | 285 | $(SRCDIR)/tree.js \ |
| 284 | 286 | $(SRCDIR)/useredit.js \ |
| 285 | 287 |
| --- src/main.mk | |
| +++ src/main.mk | |
| @@ -225,10 +225,11 @@ | |
| 225 | $(SRCDIR)/copybtn.js \ |
| 226 | $(SRCDIR)/default.css \ |
| 227 | $(SRCDIR)/diff.js \ |
| 228 | $(SRCDIR)/diff.tcl \ |
| 229 | $(SRCDIR)/forum.js \ |
| 230 | $(SRCDIR)/fossil.bootstrap.js \ |
| 231 | $(SRCDIR)/fossil.confirmer.js \ |
| 232 | $(SRCDIR)/fossil.copybutton.js \ |
| 233 | $(SRCDIR)/fossil.diff.js \ |
| 234 | $(SRCDIR)/fossil.dom.js \ |
| @@ -275,10 +276,11 @@ | |
| 275 | $(SRCDIR)/sounds/e.wav \ |
| 276 | $(SRCDIR)/sounds/f.wav \ |
| 277 | $(SRCDIR)/style.admin_log.css \ |
| 278 | $(SRCDIR)/style.chat.css \ |
| 279 | $(SRCDIR)/style.fileedit.css \ |
| 280 | $(SRCDIR)/style.pikchrshow.css \ |
| 281 | $(SRCDIR)/style.uvlist.css \ |
| 282 | $(SRCDIR)/style.wikiedit.css \ |
| 283 | $(SRCDIR)/tree.js \ |
| 284 | $(SRCDIR)/useredit.js \ |
| 285 |
| --- src/main.mk | |
| +++ src/main.mk | |
| @@ -225,10 +225,11 @@ | |
| 225 | $(SRCDIR)/copybtn.js \ |
| 226 | $(SRCDIR)/default.css \ |
| 227 | $(SRCDIR)/diff.js \ |
| 228 | $(SRCDIR)/diff.tcl \ |
| 229 | $(SRCDIR)/forum.js \ |
| 230 | $(SRCDIR)/fossil.attach.js \ |
| 231 | $(SRCDIR)/fossil.bootstrap.js \ |
| 232 | $(SRCDIR)/fossil.confirmer.js \ |
| 233 | $(SRCDIR)/fossil.copybutton.js \ |
| 234 | $(SRCDIR)/fossil.diff.js \ |
| 235 | $(SRCDIR)/fossil.dom.js \ |
| @@ -275,10 +276,11 @@ | |
| 276 | $(SRCDIR)/sounds/e.wav \ |
| 277 | $(SRCDIR)/sounds/f.wav \ |
| 278 | $(SRCDIR)/style.admin_log.css \ |
| 279 | $(SRCDIR)/style.chat.css \ |
| 280 | $(SRCDIR)/style.fileedit.css \ |
| 281 | $(SRCDIR)/style.forum.css \ |
| 282 | $(SRCDIR)/style.pikchrshow.css \ |
| 283 | $(SRCDIR)/style.uvlist.css \ |
| 284 | $(SRCDIR)/style.wikiedit.css \ |
| 285 | $(SRCDIR)/tree.js \ |
| 286 | $(SRCDIR)/useredit.js \ |
| 287 |
+4
-8
| --- src/manifest.c | ||
| +++ src/manifest.c | ||
| @@ -1128,20 +1128,16 @@ | ||
| 1128 | 1128 | md5sum_init(); |
| 1129 | 1129 | if( !isRepeat ) g.parseCnt[p->type]++; |
| 1130 | 1130 | return p; |
| 1131 | 1131 | |
| 1132 | 1132 | manifest_syntax_error: |
| 1133 | - { | |
| 1134 | - char *zUuid = rid_to_uuid(rid); | |
| 1133 | + if(pErr!=0){ | |
| 1134 | + char *zUuid = rid>0 ? rid_to_uuid(rid) : 0; | |
| 1135 | 1135 | if( zUuid ){ |
| 1136 | - if(pErr!=0){ | |
| 1137 | - blob_appendf(pErr, "artifact [%s] ", zUuid); | |
| 1138 | - } | |
| 1136 | + blob_appendf(pErr, "artifact [%s] ", zUuid); | |
| 1139 | 1137 | fossil_free(zUuid); |
| 1140 | 1138 | } |
| 1141 | - } | |
| 1142 | - if(pErr!=0){ | |
| 1143 | 1139 | if( zErr ){ |
| 1144 | 1140 | blob_appendf(pErr, "line %d: %s", lineNo, zErr); |
| 1145 | 1141 | }else{ |
| 1146 | 1142 | blob_appendf(pErr, "unknown error on line %d", lineNo); |
| 1147 | 1143 | } |
| @@ -2646,11 +2642,11 @@ | ||
| 2646 | 2642 | " WHERE target=%Q AND filename=%Q))" |
| 2647 | 2643 | " WHERE target=%Q AND filename=%Q", |
| 2648 | 2644 | p->zAttachTarget, p->zAttachName, |
| 2649 | 2645 | p->zAttachTarget, p->zAttachName |
| 2650 | 2646 | ); |
| 2651 | - switch( attachment_target_type(p->zAttachTarget) ){ | |
| 2647 | + switch( attachment_target_type(p->zAttachTarget, 1) ){ | |
| 2652 | 2648 | case 0: |
| 2653 | 2649 | /* It is possible that p->zAttachTarget is not yet in this |
| 2654 | 2650 | ** copy of the repository. If we cannot identify it yet, |
| 2655 | 2651 | ** generate a generic /artifact link to it instead of a |
| 2656 | 2652 | ** type-specific link or an error message. */ |
| 2657 | 2653 |
| --- src/manifest.c | |
| +++ src/manifest.c | |
| @@ -1128,20 +1128,16 @@ | |
| 1128 | md5sum_init(); |
| 1129 | if( !isRepeat ) g.parseCnt[p->type]++; |
| 1130 | return p; |
| 1131 | |
| 1132 | manifest_syntax_error: |
| 1133 | { |
| 1134 | char *zUuid = rid_to_uuid(rid); |
| 1135 | if( zUuid ){ |
| 1136 | if(pErr!=0){ |
| 1137 | blob_appendf(pErr, "artifact [%s] ", zUuid); |
| 1138 | } |
| 1139 | fossil_free(zUuid); |
| 1140 | } |
| 1141 | } |
| 1142 | if(pErr!=0){ |
| 1143 | if( zErr ){ |
| 1144 | blob_appendf(pErr, "line %d: %s", lineNo, zErr); |
| 1145 | }else{ |
| 1146 | blob_appendf(pErr, "unknown error on line %d", lineNo); |
| 1147 | } |
| @@ -2646,11 +2642,11 @@ | |
| 2646 | " WHERE target=%Q AND filename=%Q))" |
| 2647 | " WHERE target=%Q AND filename=%Q", |
| 2648 | p->zAttachTarget, p->zAttachName, |
| 2649 | p->zAttachTarget, p->zAttachName |
| 2650 | ); |
| 2651 | switch( attachment_target_type(p->zAttachTarget) ){ |
| 2652 | case 0: |
| 2653 | /* It is possible that p->zAttachTarget is not yet in this |
| 2654 | ** copy of the repository. If we cannot identify it yet, |
| 2655 | ** generate a generic /artifact link to it instead of a |
| 2656 | ** type-specific link or an error message. */ |
| 2657 |
| --- src/manifest.c | |
| +++ src/manifest.c | |
| @@ -1128,20 +1128,16 @@ | |
| 1128 | md5sum_init(); |
| 1129 | if( !isRepeat ) g.parseCnt[p->type]++; |
| 1130 | return p; |
| 1131 | |
| 1132 | manifest_syntax_error: |
| 1133 | if(pErr!=0){ |
| 1134 | char *zUuid = rid>0 ? rid_to_uuid(rid) : 0; |
| 1135 | if( zUuid ){ |
| 1136 | blob_appendf(pErr, "artifact [%s] ", zUuid); |
| 1137 | fossil_free(zUuid); |
| 1138 | } |
| 1139 | if( zErr ){ |
| 1140 | blob_appendf(pErr, "line %d: %s", lineNo, zErr); |
| 1141 | }else{ |
| 1142 | blob_appendf(pErr, "unknown error on line %d", lineNo); |
| 1143 | } |
| @@ -2646,11 +2642,11 @@ | |
| 2642 | " WHERE target=%Q AND filename=%Q))" |
| 2643 | " WHERE target=%Q AND filename=%Q", |
| 2644 | p->zAttachTarget, p->zAttachName, |
| 2645 | p->zAttachTarget, p->zAttachName |
| 2646 | ); |
| 2647 | switch( attachment_target_type(p->zAttachTarget, 1) ){ |
| 2648 | case 0: |
| 2649 | /* It is possible that p->zAttachTarget is not yet in this |
| 2650 | ** copy of the repository. If we cannot identify it yet, |
| 2651 | ** generate a generic /artifact link to it instead of a |
| 2652 | ** type-specific link or an error message. */ |
| 2653 |
+39
-3
| --- src/markdown.md | ||
| +++ src/markdown.md | ||
| @@ -184,16 +184,52 @@ | ||
| 184 | 184 | >``` |
| 185 | 185 | > Character **^** is not part of a label, it is part of the syntax. |
| 186 | 186 | > Both a footnote's text and a fragment to which a footnote applies |
| 187 | 187 | > are subject to further interpretation as Markdown sources. |
| 188 | 188 | |
| 189 | +## Safe HTML ## | |
| 190 | + | |
| 191 | +> Markdown documents may contain raw HTML, filtered to allow the following: | |
| 192 | +> | |
| 193 | +> <a> <abbr> <address> <article> <aside> | |
| 194 | +> <b> <big> <blockquote> <br> <caption> | |
| 195 | +> <center> <cite> <code> <col> <colgroup> | |
| 196 | +> <dd> <del> <details> <dfn> <div> <dl> | |
| 197 | +> <dt> <em> <figcaption> <figure> <font> | |
| 198 | +> <footer> <h1> <h2> <h3> <h4> <h5> | |
| 199 | +> <h6> <header> <hr> <i> <img> <ins> | |
| 200 | +> <kbd> <label> <li> <mark> <meter> <nav> | |
| 201 | +> <nobr> <nowiki> <ol> <p> <picture> <pre> | |
| 202 | +> <progress> <q> <s> <samp> <section> <small> | |
| 203 | +> <source> <span> <strike> <strong> <sub> | |
| 204 | +> <summary> <sup> <table> <tbody> <td> | |
| 205 | +> <tfoot> <th> <thead> <time> <title> <tr> | |
| 206 | +> <tt> <u> <ul> <var> <wbr> <verbatim> | |
| 207 | +> | |
| 208 | +> Any other tag is sent through to the output in a way that causes it to be | |
| 209 | +> visibly called out as disallowed. This is the same whitelist as for | |
| 210 | +> [Fossil Wiki](/wiki_rules) markup. | |
| 211 | +> | |
| 212 | +> Fossil-specific elements **<nowiki>** and **<verbatim>** disable | |
| 213 | +> wiki and Markdown processing within their spans; see | |
| 214 | +> [Wiki formatting rules](/wiki_rules) for details. | |
| 215 | +> | |
| 216 | +> Only a fixed set of attributes is permitted (for example **id**, | |
| 217 | +> **class**, **style**, **href**, **src**, **srcset**, **alt**, **type**, | |
| 218 | +> **datetime**, and **value**). Event handlers, **javascript:** URLs, and | |
| 219 | +> similar constructs are stripped. | |
| 220 | +> | |
| 221 | +> Repository administrators may allow additional unsafe HTML (such as | |
| 222 | +> **<script>** and **<form>**) in some contexts using the | |
| 223 | +> [safe-html setting](/help/safe-html) on the Admin/Wiki page. | |
| 224 | + | |
| 189 | 225 | ## Miscellaneous ## |
| 190 | 226 | |
| 191 | 227 | > * In-line images are made using **\!\[alt-text\]\(image-URL\)**. |
| 192 | -> * Use HTML for advanced formatting such as forms, noting that certain | |
| 193 | -> tags are [disallowed in some contexts](/help/safe-html). | |
| 194 | -> * **\<!--** HTML-style comments **-->** are supported. | |
| 228 | +> * Use HTML for advanced formatting; see [Safe HTML](#safe-html) above. | |
| 229 | +> * **\<!--** HTML-style comments **-->** are allowed through only when | |
| 230 | +> the safe-HTML rules are bypassed. | |
| 195 | 231 | > * Escape special characters (ex: **\[** **\(** **\|** **\***) |
| 196 | 232 | > using backslash (ex: **\\\[** **\\\(** **\\\|** **\\\***). |
| 197 | 233 | > * A line consisting of **---**, **\*\*\***, or **\_\_\_** is a horizontal |
| 198 | 234 | > rule. Spaces and extra **-**/**\***/**_** are allowed. |
| 199 | 235 | > * Paragraphs enclosed in **\<html\>...\</html\>** is passed through unchanged. |
| 200 | 236 |
| --- src/markdown.md | |
| +++ src/markdown.md | |
| @@ -184,16 +184,52 @@ | |
| 184 | >``` |
| 185 | > Character **^** is not part of a label, it is part of the syntax. |
| 186 | > Both a footnote's text and a fragment to which a footnote applies |
| 187 | > are subject to further interpretation as Markdown sources. |
| 188 | |
| 189 | ## Miscellaneous ## |
| 190 | |
| 191 | > * In-line images are made using **\!\[alt-text\]\(image-URL\)**. |
| 192 | > * Use HTML for advanced formatting such as forms, noting that certain |
| 193 | > tags are [disallowed in some contexts](/help/safe-html). |
| 194 | > * **\<!--** HTML-style comments **-->** are supported. |
| 195 | > * Escape special characters (ex: **\[** **\(** **\|** **\***) |
| 196 | > using backslash (ex: **\\\[** **\\\(** **\\\|** **\\\***). |
| 197 | > * A line consisting of **---**, **\*\*\***, or **\_\_\_** is a horizontal |
| 198 | > rule. Spaces and extra **-**/**\***/**_** are allowed. |
| 199 | > * Paragraphs enclosed in **\<html\>...\</html\>** is passed through unchanged. |
| 200 |
| --- src/markdown.md | |
| +++ src/markdown.md | |
| @@ -184,16 +184,52 @@ | |
| 184 | >``` |
| 185 | > Character **^** is not part of a label, it is part of the syntax. |
| 186 | > Both a footnote's text and a fragment to which a footnote applies |
| 187 | > are subject to further interpretation as Markdown sources. |
| 188 | |
| 189 | ## Safe HTML ## |
| 190 | |
| 191 | > Markdown documents may contain raw HTML, filtered to allow the following: |
| 192 | > |
| 193 | > <a> <abbr> <address> <article> <aside> |
| 194 | > <b> <big> <blockquote> <br> <caption> |
| 195 | > <center> <cite> <code> <col> <colgroup> |
| 196 | > <dd> <del> <details> <dfn> <div> <dl> |
| 197 | > <dt> <em> <figcaption> <figure> <font> |
| 198 | > <footer> <h1> <h2> <h3> <h4> <h5> |
| 199 | > <h6> <header> <hr> <i> <img> <ins> |
| 200 | > <kbd> <label> <li> <mark> <meter> <nav> |
| 201 | > <nobr> <nowiki> <ol> <p> <picture> <pre> |
| 202 | > <progress> <q> <s> <samp> <section> <small> |
| 203 | > <source> <span> <strike> <strong> <sub> |
| 204 | > <summary> <sup> <table> <tbody> <td> |
| 205 | > <tfoot> <th> <thead> <time> <title> <tr> |
| 206 | > <tt> <u> <ul> <var> <wbr> <verbatim> |
| 207 | > |
| 208 | > Any other tag is sent through to the output in a way that causes it to be |
| 209 | > visibly called out as disallowed. This is the same whitelist as for |
| 210 | > [Fossil Wiki](/wiki_rules) markup. |
| 211 | > |
| 212 | > Fossil-specific elements **<nowiki>** and **<verbatim>** disable |
| 213 | > wiki and Markdown processing within their spans; see |
| 214 | > [Wiki formatting rules](/wiki_rules) for details. |
| 215 | > |
| 216 | > Only a fixed set of attributes is permitted (for example **id**, |
| 217 | > **class**, **style**, **href**, **src**, **srcset**, **alt**, **type**, |
| 218 | > **datetime**, and **value**). Event handlers, **javascript:** URLs, and |
| 219 | > similar constructs are stripped. |
| 220 | > |
| 221 | > Repository administrators may allow additional unsafe HTML (such as |
| 222 | > **<script>** and **<form>**) in some contexts using the |
| 223 | > [safe-html setting](/help/safe-html) on the Admin/Wiki page. |
| 224 | |
| 225 | ## Miscellaneous ## |
| 226 | |
| 227 | > * In-line images are made using **\!\[alt-text\]\(image-URL\)**. |
| 228 | > * Use HTML for advanced formatting; see [Safe HTML](#safe-html) above. |
| 229 | > * **\<!--** HTML-style comments **-->** are allowed through only when |
| 230 | > the safe-HTML rules are bypassed. |
| 231 | > * Escape special characters (ex: **\[** **\(** **\|** **\***) |
| 232 | > using backslash (ex: **\\\[** **\\\(** **\\\|** **\\\***). |
| 233 | > * A line consisting of **---**, **\*\*\***, or **\_\_\_** is a horizontal |
| 234 | > rule. Spaces and extra **-**/**\***/**_** are allowed. |
| 235 | > * Paragraphs enclosed in **\<html\>...\</html\>** is passed through unchanged. |
| 236 |
+13
-3
| --- src/stash.c | ||
| +++ src/stash.c | ||
| @@ -447,24 +447,34 @@ | ||
| 447 | 447 | } |
| 448 | 448 | }else{ |
| 449 | 449 | Blob delta; |
| 450 | 450 | int isOrigLink = file_islink(zOPath); |
| 451 | 451 | db_ephemeral_blob(&q, 6, &delta); |
| 452 | - if( !bWebpage ) fossil_print("CHANGED %s\n", zNew); | |
| 453 | 452 | if( !isOrigLink != !isLink ){ |
| 453 | + if( !bWebpage ) fossil_print("CHANGED %s\n", zNew); | |
| 454 | 454 | diff_print_index(zNew, pCfg, 0); |
| 455 | 455 | diff_print_filenames(zOrig, zNew, pCfg, 0); |
| 456 | 456 | printf(DIFF_CANNOT_COMPUTE_SYMLINK); |
| 457 | 457 | }else{ |
| 458 | + int isChanged = 0; | |
| 458 | 459 | content_get(rid, &a); |
| 459 | 460 | blob_delta_apply(&a, &delta, &b); |
| 460 | 461 | if( fBaseline ){ |
| 461 | - diff_file_mem(&a, &b, zNew, pCfg); | |
| 462 | - }else{ | |
| 462 | + if( blob_compare(&a, &b) ){ | |
| 463 | + isChanged = 1; | |
| 464 | + if( !bWebpage ) fossil_print("CHANGED %s\n", zNew); | |
| 465 | + diff_file_mem(&a, &b, zNew, pCfg); | |
| 466 | + } | |
| 467 | + }else if( !file_same_as_blob(&b, zOPath) ){ | |
| 468 | + isChanged = 1; | |
| 469 | + if( !bWebpage ) fossil_print("CHANGED %s\n", zNew); | |
| 463 | 470 | pCfg->diffFlags ^= DIFF_INVERT; |
| 464 | 471 | diff_file(&b, zOPath, zNew, pCfg, 0); |
| 465 | 472 | pCfg->diffFlags ^= DIFF_INVERT; |
| 473 | + } | |
| 474 | + if( !bWebpage && !isChanged && pCfg->diffFlags & DIFF_VERBOSE ){ | |
| 475 | + fossil_print("UNCHANGED %s\n", zNew); | |
| 466 | 476 | } |
| 467 | 477 | blob_reset(&a); |
| 468 | 478 | blob_reset(&b); |
| 469 | 479 | } |
| 470 | 480 | blob_reset(&delta); |
| 471 | 481 |
| --- src/stash.c | |
| +++ src/stash.c | |
| @@ -447,24 +447,34 @@ | |
| 447 | } |
| 448 | }else{ |
| 449 | Blob delta; |
| 450 | int isOrigLink = file_islink(zOPath); |
| 451 | db_ephemeral_blob(&q, 6, &delta); |
| 452 | if( !bWebpage ) fossil_print("CHANGED %s\n", zNew); |
| 453 | if( !isOrigLink != !isLink ){ |
| 454 | diff_print_index(zNew, pCfg, 0); |
| 455 | diff_print_filenames(zOrig, zNew, pCfg, 0); |
| 456 | printf(DIFF_CANNOT_COMPUTE_SYMLINK); |
| 457 | }else{ |
| 458 | content_get(rid, &a); |
| 459 | blob_delta_apply(&a, &delta, &b); |
| 460 | if( fBaseline ){ |
| 461 | diff_file_mem(&a, &b, zNew, pCfg); |
| 462 | }else{ |
| 463 | pCfg->diffFlags ^= DIFF_INVERT; |
| 464 | diff_file(&b, zOPath, zNew, pCfg, 0); |
| 465 | pCfg->diffFlags ^= DIFF_INVERT; |
| 466 | } |
| 467 | blob_reset(&a); |
| 468 | blob_reset(&b); |
| 469 | } |
| 470 | blob_reset(&delta); |
| 471 |
| --- src/stash.c | |
| +++ src/stash.c | |
| @@ -447,24 +447,34 @@ | |
| 447 | } |
| 448 | }else{ |
| 449 | Blob delta; |
| 450 | int isOrigLink = file_islink(zOPath); |
| 451 | db_ephemeral_blob(&q, 6, &delta); |
| 452 | if( !isOrigLink != !isLink ){ |
| 453 | if( !bWebpage ) fossil_print("CHANGED %s\n", zNew); |
| 454 | diff_print_index(zNew, pCfg, 0); |
| 455 | diff_print_filenames(zOrig, zNew, pCfg, 0); |
| 456 | printf(DIFF_CANNOT_COMPUTE_SYMLINK); |
| 457 | }else{ |
| 458 | int isChanged = 0; |
| 459 | content_get(rid, &a); |
| 460 | blob_delta_apply(&a, &delta, &b); |
| 461 | if( fBaseline ){ |
| 462 | if( blob_compare(&a, &b) ){ |
| 463 | isChanged = 1; |
| 464 | if( !bWebpage ) fossil_print("CHANGED %s\n", zNew); |
| 465 | diff_file_mem(&a, &b, zNew, pCfg); |
| 466 | } |
| 467 | }else if( !file_same_as_blob(&b, zOPath) ){ |
| 468 | isChanged = 1; |
| 469 | if( !bWebpage ) fossil_print("CHANGED %s\n", zNew); |
| 470 | pCfg->diffFlags ^= DIFF_INVERT; |
| 471 | diff_file(&b, zOPath, zNew, pCfg, 0); |
| 472 | pCfg->diffFlags ^= DIFF_INVERT; |
| 473 | } |
| 474 | if( !bWebpage && !isChanged && pCfg->diffFlags & DIFF_VERBOSE ){ |
| 475 | fossil_print("UNCHANGED %s\n", zNew); |
| 476 | } |
| 477 | blob_reset(&a); |
| 478 | blob_reset(&b); |
| 479 | } |
| 480 | blob_reset(&delta); |
| 481 |
+17
-2
| --- src/statrep.c | ||
| +++ src/statrep.c | ||
| @@ -76,10 +76,15 @@ | ||
| 76 | 76 | case 'f': |
| 77 | 77 | case 'F': |
| 78 | 78 | zRealType = "f"; |
| 79 | 79 | rc = *zRealType; |
| 80 | 80 | break; |
| 81 | + case 'h': | |
| 82 | + case 'H': | |
| 83 | + zRealType = "h"; | |
| 84 | + rc = *zRealType; | |
| 85 | + break; | |
| 81 | 86 | case 'g': |
| 82 | 87 | case 'G': |
| 83 | 88 | zRealType = "g"; |
| 84 | 89 | rc = *zRealType; |
| 85 | 90 | break; |
| @@ -117,10 +122,16 @@ | ||
| 117 | 122 | } |
| 118 | 123 | if( zRealType==0 ){ |
| 119 | 124 | statsReportTimelineYFlag = "a"; |
| 120 | 125 | db_multi_exec("CREATE TEMP VIEW v_reports AS " |
| 121 | 126 | "SELECT * FROM event WHERE %s", zTimeSpan/*safe-for-%s*/); |
| 127 | + }else if( rc=='h' ){ | |
| 128 | + statsReportTimelineYFlag = zRealType; | |
| 129 | + db_multi_exec("CREATE TEMP VIEW v_reports AS " | |
| 130 | + "SELECT * FROM event WHERE (type='f') " | |
| 131 | + " AND (comment GLOB 'Post:*') AND %s", | |
| 132 | + zTimeSpan/*safe-for-%s*/); | |
| 122 | 133 | }else if( rc!='n' && rc!='m' ){ |
| 123 | 134 | statsReportTimelineYFlag = zRealType; |
| 124 | 135 | db_multi_exec("CREATE TEMP VIEW v_reports AS " |
| 125 | 136 | "SELECT * FROM event WHERE (type GLOB %Q) AND %s", |
| 126 | 137 | zRealType, zTimeSpan/*safe-for-%s*/); |
| @@ -154,10 +165,12 @@ | ||
| 154 | 165 | return "non-merge check-ins"; |
| 155 | 166 | case 'e': |
| 156 | 167 | return "technotes"; |
| 157 | 168 | case 'f': |
| 158 | 169 | return "forum posts"; |
| 170 | + case 'h': | |
| 171 | + return "forum threads"; | |
| 159 | 172 | case 'w': |
| 160 | 173 | return "wiki changes"; |
| 161 | 174 | case 't': |
| 162 | 175 | return "ticket changes"; |
| 163 | 176 | case 'g': |
| @@ -855,16 +868,17 @@ | ||
| 855 | 868 | ** * all (everything), |
| 856 | 869 | ** * ci (check-in) |
| 857 | 870 | ** * m (merge check-in), |
| 858 | 871 | ** * n (non-merge check-in) |
| 859 | 872 | ** * f (forum post) |
| 873 | +** * h (forum thread) | |
| 860 | 874 | ** * w (wiki page change) |
| 861 | 875 | ** * t (ticket change) |
| 862 | 876 | ** * g (tag added or removed) |
| 863 | 877 | ** Defaulting to all event types. |
| 864 | -** from=DATETIME Consider only events after this timestamp (requires to) | |
| 865 | -** to=DATETIME Consider only events before this timestamp (requires from) | |
| 878 | +** from=DATETIME Consider only events after this time (requires to) | |
| 879 | +** to=DATETIME Consider only events before this time (requires from) | |
| 866 | 880 | ** |
| 867 | 881 | ** |
| 868 | 882 | ** The view-specific query parameters include: |
| 869 | 883 | ** |
| 870 | 884 | ** view=byweek: |
| @@ -894,10 +908,11 @@ | ||
| 894 | 908 | }; |
| 895 | 909 | static const char *const azType[] = { |
| 896 | 910 | "a", "All Changes", |
| 897 | 911 | "ci", "Check-ins", |
| 898 | 912 | "f", "Forum Posts", |
| 913 | + "h", "Forum Threads", | |
| 899 | 914 | "m", "Merge check-ins", |
| 900 | 915 | "n", "Non-merge check-ins", |
| 901 | 916 | "g", "Tags", |
| 902 | 917 | "e", "Tech Notes", |
| 903 | 918 | "t", "Tickets", |
| 904 | 919 |
| --- src/statrep.c | |
| +++ src/statrep.c | |
| @@ -76,10 +76,15 @@ | |
| 76 | case 'f': |
| 77 | case 'F': |
| 78 | zRealType = "f"; |
| 79 | rc = *zRealType; |
| 80 | break; |
| 81 | case 'g': |
| 82 | case 'G': |
| 83 | zRealType = "g"; |
| 84 | rc = *zRealType; |
| 85 | break; |
| @@ -117,10 +122,16 @@ | |
| 117 | } |
| 118 | if( zRealType==0 ){ |
| 119 | statsReportTimelineYFlag = "a"; |
| 120 | db_multi_exec("CREATE TEMP VIEW v_reports AS " |
| 121 | "SELECT * FROM event WHERE %s", zTimeSpan/*safe-for-%s*/); |
| 122 | }else if( rc!='n' && rc!='m' ){ |
| 123 | statsReportTimelineYFlag = zRealType; |
| 124 | db_multi_exec("CREATE TEMP VIEW v_reports AS " |
| 125 | "SELECT * FROM event WHERE (type GLOB %Q) AND %s", |
| 126 | zRealType, zTimeSpan/*safe-for-%s*/); |
| @@ -154,10 +165,12 @@ | |
| 154 | return "non-merge check-ins"; |
| 155 | case 'e': |
| 156 | return "technotes"; |
| 157 | case 'f': |
| 158 | return "forum posts"; |
| 159 | case 'w': |
| 160 | return "wiki changes"; |
| 161 | case 't': |
| 162 | return "ticket changes"; |
| 163 | case 'g': |
| @@ -855,16 +868,17 @@ | |
| 855 | ** * all (everything), |
| 856 | ** * ci (check-in) |
| 857 | ** * m (merge check-in), |
| 858 | ** * n (non-merge check-in) |
| 859 | ** * f (forum post) |
| 860 | ** * w (wiki page change) |
| 861 | ** * t (ticket change) |
| 862 | ** * g (tag added or removed) |
| 863 | ** Defaulting to all event types. |
| 864 | ** from=DATETIME Consider only events after this timestamp (requires to) |
| 865 | ** to=DATETIME Consider only events before this timestamp (requires from) |
| 866 | ** |
| 867 | ** |
| 868 | ** The view-specific query parameters include: |
| 869 | ** |
| 870 | ** view=byweek: |
| @@ -894,10 +908,11 @@ | |
| 894 | }; |
| 895 | static const char *const azType[] = { |
| 896 | "a", "All Changes", |
| 897 | "ci", "Check-ins", |
| 898 | "f", "Forum Posts", |
| 899 | "m", "Merge check-ins", |
| 900 | "n", "Non-merge check-ins", |
| 901 | "g", "Tags", |
| 902 | "e", "Tech Notes", |
| 903 | "t", "Tickets", |
| 904 |
| --- src/statrep.c | |
| +++ src/statrep.c | |
| @@ -76,10 +76,15 @@ | |
| 76 | case 'f': |
| 77 | case 'F': |
| 78 | zRealType = "f"; |
| 79 | rc = *zRealType; |
| 80 | break; |
| 81 | case 'h': |
| 82 | case 'H': |
| 83 | zRealType = "h"; |
| 84 | rc = *zRealType; |
| 85 | break; |
| 86 | case 'g': |
| 87 | case 'G': |
| 88 | zRealType = "g"; |
| 89 | rc = *zRealType; |
| 90 | break; |
| @@ -117,10 +122,16 @@ | |
| 122 | } |
| 123 | if( zRealType==0 ){ |
| 124 | statsReportTimelineYFlag = "a"; |
| 125 | db_multi_exec("CREATE TEMP VIEW v_reports AS " |
| 126 | "SELECT * FROM event WHERE %s", zTimeSpan/*safe-for-%s*/); |
| 127 | }else if( rc=='h' ){ |
| 128 | statsReportTimelineYFlag = zRealType; |
| 129 | db_multi_exec("CREATE TEMP VIEW v_reports AS " |
| 130 | "SELECT * FROM event WHERE (type='f') " |
| 131 | " AND (comment GLOB 'Post:*') AND %s", |
| 132 | zTimeSpan/*safe-for-%s*/); |
| 133 | }else if( rc!='n' && rc!='m' ){ |
| 134 | statsReportTimelineYFlag = zRealType; |
| 135 | db_multi_exec("CREATE TEMP VIEW v_reports AS " |
| 136 | "SELECT * FROM event WHERE (type GLOB %Q) AND %s", |
| 137 | zRealType, zTimeSpan/*safe-for-%s*/); |
| @@ -154,10 +165,12 @@ | |
| 165 | return "non-merge check-ins"; |
| 166 | case 'e': |
| 167 | return "technotes"; |
| 168 | case 'f': |
| 169 | return "forum posts"; |
| 170 | case 'h': |
| 171 | return "forum threads"; |
| 172 | case 'w': |
| 173 | return "wiki changes"; |
| 174 | case 't': |
| 175 | return "ticket changes"; |
| 176 | case 'g': |
| @@ -855,16 +868,17 @@ | |
| 868 | ** * all (everything), |
| 869 | ** * ci (check-in) |
| 870 | ** * m (merge check-in), |
| 871 | ** * n (non-merge check-in) |
| 872 | ** * f (forum post) |
| 873 | ** * h (forum thread) |
| 874 | ** * w (wiki page change) |
| 875 | ** * t (ticket change) |
| 876 | ** * g (tag added or removed) |
| 877 | ** Defaulting to all event types. |
| 878 | ** from=DATETIME Consider only events after this time (requires to) |
| 879 | ** to=DATETIME Consider only events before this time (requires from) |
| 880 | ** |
| 881 | ** |
| 882 | ** The view-specific query parameters include: |
| 883 | ** |
| 884 | ** view=byweek: |
| @@ -894,10 +908,11 @@ | |
| 908 | }; |
| 909 | static const char *const azType[] = { |
| 910 | "a", "All Changes", |
| 911 | "ci", "Check-ins", |
| 912 | "f", "Forum Posts", |
| 913 | "h", "Forum Threads", |
| 914 | "m", "Merge check-ins", |
| 915 | "n", "Non-merge check-ins", |
| 916 | "g", "Tags", |
| 917 | "e", "Tech Notes", |
| 918 | "t", "Tickets", |
| 919 |
+22
-3
| --- src/style.c | ||
| +++ src/style.c | ||
| @@ -384,10 +384,11 @@ | ||
| 384 | 384 | |
| 385 | 385 | /* Use this for the $current_page variable if it is not NULL. If it |
| 386 | 386 | ** is NULL then use g.zPath. |
| 387 | 387 | */ |
| 388 | 388 | static char *local_zCurrentPage = 0; |
| 389 | +static char *local_zCurrentFeature = 0; | |
| 389 | 390 | |
| 390 | 391 | /* |
| 391 | 392 | ** Set the desired $current_page to something other than g.zPath |
| 392 | 393 | */ |
| 393 | 394 | void style_set_current_page(const char *zFormat, ...){ |
| @@ -419,12 +420,13 @@ | ||
| 419 | 420 | |
| 420 | 421 | /* Initialize the URL to its baseline */ |
| 421 | 422 | url = empty_blob; |
| 422 | 423 | blob_appendf(&url, "%R/style.css"); |
| 423 | 424 | |
| 424 | - /* If page-specific CSS exists for the current page, then append | |
| 425 | - ** the pathname for the page-specific CSS. The default CSS is | |
| 425 | + /* If page- or feature-specific CSS exists for the current page, | |
| 426 | + ** then append the pathname for the page-specific CSS. The default | |
| 427 | + ** CSS is | |
| 426 | 428 | ** |
| 427 | 429 | ** /style.css |
| 428 | 430 | ** |
| 429 | 431 | ** But for the "/wikiedit" page (to name but one example), we |
| 430 | 432 | ** append a path as follows: |
| @@ -432,14 +434,25 @@ | ||
| 432 | 434 | ** /style.css/wikiedit |
| 433 | 435 | ** |
| 434 | 436 | ** The /style.css page (implemented below) will detect this extra "wikiedit" |
| 435 | 437 | ** path information and include the page-specific CSS along with the |
| 436 | 438 | ** default CSS when it delivers the page. |
| 439 | + ** | |
| 440 | + ** Prior to 2026-06-06, this only looked at zPage but /forum and | |
| 441 | + ** friends need a per-feature style, so it now falls back to | |
| 442 | + ** local_zCurrentFeature. The current mechanism cannot support both | |
| 443 | + ** concurrently in a single request. | |
| 437 | 444 | */ |
| 438 | 445 | zBuiltin = mprintf("style.%s.css", zPage); |
| 439 | 446 | if( builtin_file(zBuiltin,0)!=0 ){ |
| 440 | - blob_appendf(&url, "/%s", zPage); | |
| 447 | + blob_appendf(&url, "/%t", zPage); | |
| 448 | + }else if( local_zCurrentFeature ){ | |
| 449 | + fossil_free(zBuiltin); | |
| 450 | + zBuiltin = mprintf("style.%s.css", local_zCurrentFeature); | |
| 451 | + if( builtin_file(zBuiltin,0)!=0 ){ | |
| 452 | + blob_appendf(&url, "/%t", local_zCurrentFeature); | |
| 453 | + } | |
| 441 | 454 | } |
| 442 | 455 | fossil_free(zBuiltin); |
| 443 | 456 | |
| 444 | 457 | /* Add query parameters that will change whenever the skin changes |
| 445 | 458 | ** or after any updates to the CSS files |
| @@ -727,10 +740,12 @@ | ||
| 727 | 740 | ** style_init_th1_vars() because that uses Th_MaybeStore() instead to |
| 728 | 741 | ** allow webpage implementations to call this before style_header() |
| 729 | 742 | ** to override that "maybe" default with something better. |
| 730 | 743 | */ |
| 731 | 744 | void style_set_current_feature(const char* zFeature){ |
| 745 | + fossil_free( local_zCurrentFeature ); | |
| 746 | + local_zCurrentFeature = fossil_strdup(zFeature); | |
| 732 | 747 | Th_Store("current_feature", zFeature); |
| 733 | 748 | } |
| 734 | 749 | |
| 735 | 750 | /* |
| 736 | 751 | ** Returns the current mainmenu value from either the --mainmenu flag |
| @@ -1258,10 +1273,11 @@ | ||
| 1258 | 1273 | "** Page-specific CSS for \"%s\"\n" |
| 1259 | 1274 | "***********************************************************/\n", |
| 1260 | 1275 | zPage); |
| 1261 | 1276 | blob_append(pOut, zBuiltin, nFile); |
| 1262 | 1277 | fossil_free(zFile); |
| 1278 | + zFile = 0; | |
| 1263 | 1279 | return; |
| 1264 | 1280 | } |
| 1265 | 1281 | /* Potential TODO: check for aliases/page groups. e.g. group all |
| 1266 | 1282 | ** /forumXYZ CSS into one file, all /setupXYZ into another, etc. As |
| 1267 | 1283 | ** of this writing, doing so would only shave a few kb from |
| @@ -1418,10 +1434,13 @@ | ||
| 1418 | 1434 | ** For administators, or if the test_env_enable setting is true, then |
| 1419 | 1435 | ** details of the request environment are displayed. Otherwise, just |
| 1420 | 1436 | ** the error message is shown. |
| 1421 | 1437 | ** |
| 1422 | 1438 | ** If zFormat is an empty string, then this is the /test-env page. |
| 1439 | +** | |
| 1440 | +** If the resulting formatted error message is not empty then this | |
| 1441 | +** function does not return. | |
| 1423 | 1442 | */ |
| 1424 | 1443 | void webpage_error(const char *zFormat, ...){ |
| 1425 | 1444 | int showAll = 0; |
| 1426 | 1445 | char *zErr = 0; |
| 1427 | 1446 | int isAuth = 0; |
| 1428 | 1447 | |
| 1429 | 1448 | ADDED src/style.forum.css |
| --- src/style.c | |
| +++ src/style.c | |
| @@ -384,10 +384,11 @@ | |
| 384 | |
| 385 | /* Use this for the $current_page variable if it is not NULL. If it |
| 386 | ** is NULL then use g.zPath. |
| 387 | */ |
| 388 | static char *local_zCurrentPage = 0; |
| 389 | |
| 390 | /* |
| 391 | ** Set the desired $current_page to something other than g.zPath |
| 392 | */ |
| 393 | void style_set_current_page(const char *zFormat, ...){ |
| @@ -419,12 +420,13 @@ | |
| 419 | |
| 420 | /* Initialize the URL to its baseline */ |
| 421 | url = empty_blob; |
| 422 | blob_appendf(&url, "%R/style.css"); |
| 423 | |
| 424 | /* If page-specific CSS exists for the current page, then append |
| 425 | ** the pathname for the page-specific CSS. The default CSS is |
| 426 | ** |
| 427 | ** /style.css |
| 428 | ** |
| 429 | ** But for the "/wikiedit" page (to name but one example), we |
| 430 | ** append a path as follows: |
| @@ -432,14 +434,25 @@ | |
| 432 | ** /style.css/wikiedit |
| 433 | ** |
| 434 | ** The /style.css page (implemented below) will detect this extra "wikiedit" |
| 435 | ** path information and include the page-specific CSS along with the |
| 436 | ** default CSS when it delivers the page. |
| 437 | */ |
| 438 | zBuiltin = mprintf("style.%s.css", zPage); |
| 439 | if( builtin_file(zBuiltin,0)!=0 ){ |
| 440 | blob_appendf(&url, "/%s", zPage); |
| 441 | } |
| 442 | fossil_free(zBuiltin); |
| 443 | |
| 444 | /* Add query parameters that will change whenever the skin changes |
| 445 | ** or after any updates to the CSS files |
| @@ -727,10 +740,12 @@ | |
| 727 | ** style_init_th1_vars() because that uses Th_MaybeStore() instead to |
| 728 | ** allow webpage implementations to call this before style_header() |
| 729 | ** to override that "maybe" default with something better. |
| 730 | */ |
| 731 | void style_set_current_feature(const char* zFeature){ |
| 732 | Th_Store("current_feature", zFeature); |
| 733 | } |
| 734 | |
| 735 | /* |
| 736 | ** Returns the current mainmenu value from either the --mainmenu flag |
| @@ -1258,10 +1273,11 @@ | |
| 1258 | "** Page-specific CSS for \"%s\"\n" |
| 1259 | "***********************************************************/\n", |
| 1260 | zPage); |
| 1261 | blob_append(pOut, zBuiltin, nFile); |
| 1262 | fossil_free(zFile); |
| 1263 | return; |
| 1264 | } |
| 1265 | /* Potential TODO: check for aliases/page groups. e.g. group all |
| 1266 | ** /forumXYZ CSS into one file, all /setupXYZ into another, etc. As |
| 1267 | ** of this writing, doing so would only shave a few kb from |
| @@ -1418,10 +1434,13 @@ | |
| 1418 | ** For administators, or if the test_env_enable setting is true, then |
| 1419 | ** details of the request environment are displayed. Otherwise, just |
| 1420 | ** the error message is shown. |
| 1421 | ** |
| 1422 | ** If zFormat is an empty string, then this is the /test-env page. |
| 1423 | */ |
| 1424 | void webpage_error(const char *zFormat, ...){ |
| 1425 | int showAll = 0; |
| 1426 | char *zErr = 0; |
| 1427 | int isAuth = 0; |
| 1428 | |
| 1429 | DDED src/style.forum.css |
| --- src/style.c | |
| +++ src/style.c | |
| @@ -384,10 +384,11 @@ | |
| 384 | |
| 385 | /* Use this for the $current_page variable if it is not NULL. If it |
| 386 | ** is NULL then use g.zPath. |
| 387 | */ |
| 388 | static char *local_zCurrentPage = 0; |
| 389 | static char *local_zCurrentFeature = 0; |
| 390 | |
| 391 | /* |
| 392 | ** Set the desired $current_page to something other than g.zPath |
| 393 | */ |
| 394 | void style_set_current_page(const char *zFormat, ...){ |
| @@ -419,12 +420,13 @@ | |
| 420 | |
| 421 | /* Initialize the URL to its baseline */ |
| 422 | url = empty_blob; |
| 423 | blob_appendf(&url, "%R/style.css"); |
| 424 | |
| 425 | /* If page- or feature-specific CSS exists for the current page, |
| 426 | ** then append the pathname for the page-specific CSS. The default |
| 427 | ** CSS is |
| 428 | ** |
| 429 | ** /style.css |
| 430 | ** |
| 431 | ** But for the "/wikiedit" page (to name but one example), we |
| 432 | ** append a path as follows: |
| @@ -432,14 +434,25 @@ | |
| 434 | ** /style.css/wikiedit |
| 435 | ** |
| 436 | ** The /style.css page (implemented below) will detect this extra "wikiedit" |
| 437 | ** path information and include the page-specific CSS along with the |
| 438 | ** default CSS when it delivers the page. |
| 439 | ** |
| 440 | ** Prior to 2026-06-06, this only looked at zPage but /forum and |
| 441 | ** friends need a per-feature style, so it now falls back to |
| 442 | ** local_zCurrentFeature. The current mechanism cannot support both |
| 443 | ** concurrently in a single request. |
| 444 | */ |
| 445 | zBuiltin = mprintf("style.%s.css", zPage); |
| 446 | if( builtin_file(zBuiltin,0)!=0 ){ |
| 447 | blob_appendf(&url, "/%t", zPage); |
| 448 | }else if( local_zCurrentFeature ){ |
| 449 | fossil_free(zBuiltin); |
| 450 | zBuiltin = mprintf("style.%s.css", local_zCurrentFeature); |
| 451 | if( builtin_file(zBuiltin,0)!=0 ){ |
| 452 | blob_appendf(&url, "/%t", local_zCurrentFeature); |
| 453 | } |
| 454 | } |
| 455 | fossil_free(zBuiltin); |
| 456 | |
| 457 | /* Add query parameters that will change whenever the skin changes |
| 458 | ** or after any updates to the CSS files |
| @@ -727,10 +740,12 @@ | |
| 740 | ** style_init_th1_vars() because that uses Th_MaybeStore() instead to |
| 741 | ** allow webpage implementations to call this before style_header() |
| 742 | ** to override that "maybe" default with something better. |
| 743 | */ |
| 744 | void style_set_current_feature(const char* zFeature){ |
| 745 | fossil_free( local_zCurrentFeature ); |
| 746 | local_zCurrentFeature = fossil_strdup(zFeature); |
| 747 | Th_Store("current_feature", zFeature); |
| 748 | } |
| 749 | |
| 750 | /* |
| 751 | ** Returns the current mainmenu value from either the --mainmenu flag |
| @@ -1258,10 +1273,11 @@ | |
| 1273 | "** Page-specific CSS for \"%s\"\n" |
| 1274 | "***********************************************************/\n", |
| 1275 | zPage); |
| 1276 | blob_append(pOut, zBuiltin, nFile); |
| 1277 | fossil_free(zFile); |
| 1278 | zFile = 0; |
| 1279 | return; |
| 1280 | } |
| 1281 | /* Potential TODO: check for aliases/page groups. e.g. group all |
| 1282 | ** /forumXYZ CSS into one file, all /setupXYZ into another, etc. As |
| 1283 | ** of this writing, doing so would only shave a few kb from |
| @@ -1418,10 +1434,13 @@ | |
| 1434 | ** For administators, or if the test_env_enable setting is true, then |
| 1435 | ** details of the request environment are displayed. Otherwise, just |
| 1436 | ** the error message is shown. |
| 1437 | ** |
| 1438 | ** If zFormat is an empty string, then this is the /test-env page. |
| 1439 | ** |
| 1440 | ** If the resulting formatted error message is not empty then this |
| 1441 | ** function does not return. |
| 1442 | */ |
| 1443 | void webpage_error(const char *zFormat, ...){ |
| 1444 | int showAll = 0; |
| 1445 | char *zErr = 0; |
| 1446 | int isAuth = 0; |
| 1447 | |
| 1448 | DDED src/style.forum.css |
+33
| --- a/src/style.forum.css | ||
| +++ b/src/style.forum.css | ||
| @@ -0,0 +1,33 @@ | ||
| 1 | +/* Styles specific to the forum family of pages */ | |
| 2 | + | |
| 3 | +fieldset.forum-status-selection { | |
| 4 | + max-width: max-content; | |
| 5 | + border-radius: 0.5em; | |
| 6 | + padding: 0 0.5em; | |
| 7 | + margin-bottom: 0.35em; | |
| 8 | +} | |
| 9 | + | |
| 10 | +body.forum .forumpost-single-controls button.draft:after { | |
| 11 | + /* Reply/Edit buttons on posts which have local draft edits. */ | |
| 12 | + content: " [draft]"; | |
| 13 | +} | |
| 14 | + | |
| 15 | +/* .ForumPostEditor is the top container element used by the JS | |
| 16 | + ForumPostEditor class in fossil.page.forumpost.js. */ | |
| 17 | +.ForumPostEditor { | |
| 18 | + display: flex; | |
| 19 | + flex-direction: column; | |
| 20 | + gap: 1em; | |
| 21 | + padding: 0.5em; | |
| 22 | +} | |
| 23 | +.ForumPostEditor > .tab-bar{ | |
| 24 | +} | |
| 25 | +.ForumPostEditor > .tab-container { | |
| 26 | +} | |
| 27 | +.ForumPostEditor > .tab-container > .tabs { | |
| 28 | + /*min-height: 10em;*/ | |
| 29 | +} | |
| 30 | +.ForumPostEditor > .tab-container > .tabs > .tab-panel.debug { | |
| 31 | + display: flex; | |
| 32 | + flex-direction: column; | |
| 33 | + gap: 0 |
| --- a/src/style.forum.css | |
| +++ b/src/style.forum.css | |
| @@ -0,0 +1,33 @@ | |
| --- a/src/style.forum.css | |
| +++ b/src/style.forum.css | |
| @@ -0,0 +1,33 @@ | |
| 1 | /* Styles specific to the forum family of pages */ |
| 2 | |
| 3 | fieldset.forum-status-selection { |
| 4 | max-width: max-content; |
| 5 | border-radius: 0.5em; |
| 6 | padding: 0 0.5em; |
| 7 | margin-bottom: 0.35em; |
| 8 | } |
| 9 | |
| 10 | body.forum .forumpost-single-controls button.draft:after { |
| 11 | /* Reply/Edit buttons on posts which have local draft edits. */ |
| 12 | content: " [draft]"; |
| 13 | } |
| 14 | |
| 15 | /* .ForumPostEditor is the top container element used by the JS |
| 16 | ForumPostEditor class in fossil.page.forumpost.js. */ |
| 17 | .ForumPostEditor { |
| 18 | display: flex; |
| 19 | flex-direction: column; |
| 20 | gap: 1em; |
| 21 | padding: 0.5em; |
| 22 | } |
| 23 | .ForumPostEditor > .tab-bar{ |
| 24 | } |
| 25 | .ForumPostEditor > .tab-container { |
| 26 | } |
| 27 | .ForumPostEditor > .tab-container > .tabs { |
| 28 | /*min-height: 10em;*/ |
| 29 | } |
| 30 | .ForumPostEditor > .tab-container > .tabs > .tab-panel.debug { |
| 31 | display: flex; |
| 32 | flex-direction: column; |
| 33 | gap: 0 |
+18
| --- src/timeline.c | ||
| +++ src/timeline.c | ||
| @@ -3538,10 +3538,21 @@ | ||
| 3538 | 3538 | zFree = mprintf("[%S] Edit to wiki page \"%s\" (user: %s)", |
| 3539 | 3539 | zId, zComShort+1, zUserShort); |
| 3540 | 3540 | } |
| 3541 | 3541 | }else{ |
| 3542 | 3542 | zFree = mprintf("[%S] %s%s", zId, zPrefix, zCom); |
| 3543 | + | |
| 3544 | + if( get_comment_format() & COMMENT_PRINT_SUMMARY ){ | |
| 3545 | + char *z, *t; | |
| 3546 | + for(z=zFree; *z!='\0'; z++){ | |
| 3547 | + if( *z=='\n' ){ | |
| 3548 | + for(t=z+1; *t!='\0' && *t!='\n' && fossil_isspace(*t); t++){} | |
| 3549 | + if( *t=='\n' ) break; | |
| 3550 | + } | |
| 3551 | + } | |
| 3552 | + *z = '\0'; | |
| 3553 | + } | |
| 3543 | 3554 | } |
| 3544 | 3555 | |
| 3545 | 3556 | if( zFormat ){ |
| 3546 | 3557 | char *zEntry; |
| 3547 | 3558 | int nEntryLine = 0; |
| @@ -3766,10 +3777,11 @@ | ||
| 3766 | 3777 | ** PATH can be a file or a subdirectory. |
| 3767 | 3778 | ** -q|--quiet Do not print notifications at the end of the timeline. |
| 3768 | 3779 | ** -r|--reverse Show items in chronological order. |
| 3769 | 3780 | ** -R REPO_FILE Specifies the repository db to use. Default is |
| 3770 | 3781 | ** the current check-out's repository. |
| 3782 | +** -s|--summmary Truncate comments after the first blank line. | |
| 3771 | 3783 | ** --sql Show the SQL used to generate the timeline |
| 3772 | 3784 | ** -t|--type TYPE Output items from the given types only, such as: |
| 3773 | 3785 | ** ci = file commits only |
| 3774 | 3786 | ** e = technical notes only |
| 3775 | 3787 | ** f = forum posts only |
| @@ -3804,10 +3816,12 @@ | ||
| 3804 | 3816 | const char *zFilePattern = 0; |
| 3805 | 3817 | const char *zFormat = 0; |
| 3806 | 3818 | const char *zBr = 0; |
| 3807 | 3819 | Blob treeName; |
| 3808 | 3820 | int showSql = 0; |
| 3821 | + int bCommentGitStyle = 0; | |
| 3822 | + int oldComFmtFlags = get_comment_format(); | |
| 3809 | 3823 | |
| 3810 | 3824 | verboseFlag = find_option("verbose","v", 0)!=0; |
| 3811 | 3825 | if( !verboseFlag){ |
| 3812 | 3826 | verboseFlag = find_option("showfiles","f", 0)!=0; /* deprecated */ |
| 3813 | 3827 | } |
| @@ -3816,10 +3830,13 @@ | ||
| 3816 | 3830 | zWidth = find_option("width","W",1); |
| 3817 | 3831 | zType = find_option("type","t",1); |
| 3818 | 3832 | zUser = find_option("for-user","u",1); |
| 3819 | 3833 | zFilePattern = find_option("path","p",1); |
| 3820 | 3834 | zFormat = find_option("format","F",1); |
| 3835 | + if( (bCommentGitStyle = (find_option("summary","s",0))!=0) ){ | |
| 3836 | + g.comFmtFlags |= COMMENT_PRINT_SUMMARY; | |
| 3837 | + } | |
| 3821 | 3838 | zBr = find_option("branch","b",1); |
| 3822 | 3839 | if( find_option("current-branch","c",0)!=0 ){ |
| 3823 | 3840 | if( !g.localOpen ){ |
| 3824 | 3841 | fossil_fatal("not within an open check-out"); |
| 3825 | 3842 | }else{ |
| @@ -4030,10 +4047,11 @@ | ||
| 4030 | 4047 | } |
| 4031 | 4048 | db_prepare_blob(&q, &sql); |
| 4032 | 4049 | blob_reset(&sql); |
| 4033 | 4050 | print_timeline(&q, n, width, zFormat, verboseFlag); |
| 4034 | 4051 | db_finalize(&q); |
| 4052 | + g.comFmtFlags = oldComFmtFlags; | |
| 4035 | 4053 | } |
| 4036 | 4054 | |
| 4037 | 4055 | /* |
| 4038 | 4056 | ** WEBPAGE: thisdayinhistory |
| 4039 | 4057 | ** |
| 4040 | 4058 |
| --- src/timeline.c | |
| +++ src/timeline.c | |
| @@ -3538,10 +3538,21 @@ | |
| 3538 | zFree = mprintf("[%S] Edit to wiki page \"%s\" (user: %s)", |
| 3539 | zId, zComShort+1, zUserShort); |
| 3540 | } |
| 3541 | }else{ |
| 3542 | zFree = mprintf("[%S] %s%s", zId, zPrefix, zCom); |
| 3543 | } |
| 3544 | |
| 3545 | if( zFormat ){ |
| 3546 | char *zEntry; |
| 3547 | int nEntryLine = 0; |
| @@ -3766,10 +3777,11 @@ | |
| 3766 | ** PATH can be a file or a subdirectory. |
| 3767 | ** -q|--quiet Do not print notifications at the end of the timeline. |
| 3768 | ** -r|--reverse Show items in chronological order. |
| 3769 | ** -R REPO_FILE Specifies the repository db to use. Default is |
| 3770 | ** the current check-out's repository. |
| 3771 | ** --sql Show the SQL used to generate the timeline |
| 3772 | ** -t|--type TYPE Output items from the given types only, such as: |
| 3773 | ** ci = file commits only |
| 3774 | ** e = technical notes only |
| 3775 | ** f = forum posts only |
| @@ -3804,10 +3816,12 @@ | |
| 3804 | const char *zFilePattern = 0; |
| 3805 | const char *zFormat = 0; |
| 3806 | const char *zBr = 0; |
| 3807 | Blob treeName; |
| 3808 | int showSql = 0; |
| 3809 | |
| 3810 | verboseFlag = find_option("verbose","v", 0)!=0; |
| 3811 | if( !verboseFlag){ |
| 3812 | verboseFlag = find_option("showfiles","f", 0)!=0; /* deprecated */ |
| 3813 | } |
| @@ -3816,10 +3830,13 @@ | |
| 3816 | zWidth = find_option("width","W",1); |
| 3817 | zType = find_option("type","t",1); |
| 3818 | zUser = find_option("for-user","u",1); |
| 3819 | zFilePattern = find_option("path","p",1); |
| 3820 | zFormat = find_option("format","F",1); |
| 3821 | zBr = find_option("branch","b",1); |
| 3822 | if( find_option("current-branch","c",0)!=0 ){ |
| 3823 | if( !g.localOpen ){ |
| 3824 | fossil_fatal("not within an open check-out"); |
| 3825 | }else{ |
| @@ -4030,10 +4047,11 @@ | |
| 4030 | } |
| 4031 | db_prepare_blob(&q, &sql); |
| 4032 | blob_reset(&sql); |
| 4033 | print_timeline(&q, n, width, zFormat, verboseFlag); |
| 4034 | db_finalize(&q); |
| 4035 | } |
| 4036 | |
| 4037 | /* |
| 4038 | ** WEBPAGE: thisdayinhistory |
| 4039 | ** |
| 4040 |
| --- src/timeline.c | |
| +++ src/timeline.c | |
| @@ -3538,10 +3538,21 @@ | |
| 3538 | zFree = mprintf("[%S] Edit to wiki page \"%s\" (user: %s)", |
| 3539 | zId, zComShort+1, zUserShort); |
| 3540 | } |
| 3541 | }else{ |
| 3542 | zFree = mprintf("[%S] %s%s", zId, zPrefix, zCom); |
| 3543 | |
| 3544 | if( get_comment_format() & COMMENT_PRINT_SUMMARY ){ |
| 3545 | char *z, *t; |
| 3546 | for(z=zFree; *z!='\0'; z++){ |
| 3547 | if( *z=='\n' ){ |
| 3548 | for(t=z+1; *t!='\0' && *t!='\n' && fossil_isspace(*t); t++){} |
| 3549 | if( *t=='\n' ) break; |
| 3550 | } |
| 3551 | } |
| 3552 | *z = '\0'; |
| 3553 | } |
| 3554 | } |
| 3555 | |
| 3556 | if( zFormat ){ |
| 3557 | char *zEntry; |
| 3558 | int nEntryLine = 0; |
| @@ -3766,10 +3777,11 @@ | |
| 3777 | ** PATH can be a file or a subdirectory. |
| 3778 | ** -q|--quiet Do not print notifications at the end of the timeline. |
| 3779 | ** -r|--reverse Show items in chronological order. |
| 3780 | ** -R REPO_FILE Specifies the repository db to use. Default is |
| 3781 | ** the current check-out's repository. |
| 3782 | ** -s|--summmary Truncate comments after the first blank line. |
| 3783 | ** --sql Show the SQL used to generate the timeline |
| 3784 | ** -t|--type TYPE Output items from the given types only, such as: |
| 3785 | ** ci = file commits only |
| 3786 | ** e = technical notes only |
| 3787 | ** f = forum posts only |
| @@ -3804,10 +3816,12 @@ | |
| 3816 | const char *zFilePattern = 0; |
| 3817 | const char *zFormat = 0; |
| 3818 | const char *zBr = 0; |
| 3819 | Blob treeName; |
| 3820 | int showSql = 0; |
| 3821 | int bCommentGitStyle = 0; |
| 3822 | int oldComFmtFlags = get_comment_format(); |
| 3823 | |
| 3824 | verboseFlag = find_option("verbose","v", 0)!=0; |
| 3825 | if( !verboseFlag){ |
| 3826 | verboseFlag = find_option("showfiles","f", 0)!=0; /* deprecated */ |
| 3827 | } |
| @@ -3816,10 +3830,13 @@ | |
| 3830 | zWidth = find_option("width","W",1); |
| 3831 | zType = find_option("type","t",1); |
| 3832 | zUser = find_option("for-user","u",1); |
| 3833 | zFilePattern = find_option("path","p",1); |
| 3834 | zFormat = find_option("format","F",1); |
| 3835 | if( (bCommentGitStyle = (find_option("summary","s",0))!=0) ){ |
| 3836 | g.comFmtFlags |= COMMENT_PRINT_SUMMARY; |
| 3837 | } |
| 3838 | zBr = find_option("branch","b",1); |
| 3839 | if( find_option("current-branch","c",0)!=0 ){ |
| 3840 | if( !g.localOpen ){ |
| 3841 | fossil_fatal("not within an open check-out"); |
| 3842 | }else{ |
| @@ -4030,10 +4047,11 @@ | |
| 4047 | } |
| 4048 | db_prepare_blob(&q, &sql); |
| 4049 | blob_reset(&sql); |
| 4050 | print_timeline(&q, n, width, zFormat, verboseFlag); |
| 4051 | db_finalize(&q); |
| 4052 | g.comFmtFlags = oldComFmtFlags; |
| 4053 | } |
| 4054 | |
| 4055 | /* |
| 4056 | ** WEBPAGE: thisdayinhistory |
| 4057 | ** |
| 4058 |
+11
-6
| --- src/tkt.c | ||
| +++ src/tkt.c | ||
| @@ -749,13 +749,16 @@ | ||
| 749 | 749 | } |
| 750 | 750 | } |
| 751 | 751 | if( g.anon.NewTkt ){ |
| 752 | 752 | style_submenu_element("New Ticket", "%R/tktnew"); |
| 753 | 753 | } |
| 754 | + zFullName = db_text(0, | |
| 755 | + "SELECT tkt_uuid FROM ticket" | |
| 756 | + " WHERE tkt_uuid GLOB '%q*'", zUuid); | |
| 754 | 757 | if( g.anon.ApndTkt && g.anon.Attach ){ |
| 755 | - style_submenu_element("Attach", "%R/attachadd?tkt=%T&from=%R/tktview/%t", | |
| 756 | - zUuid, zUuid); | |
| 758 | + style_submenu_element("Attach", "%R/attachadd?target=%T&from=%R/tktview/%t", | |
| 759 | + zFullName, zUuid); | |
| 757 | 760 | } |
| 758 | 761 | if( P("plaintext") ){ |
| 759 | 762 | style_submenu_element("Formatted", "%R/tktview/%s", zUuid); |
| 760 | 763 | }else{ |
| 761 | 764 | style_submenu_element("Plaintext", "%R/tktview/%s?plaintext", zUuid); |
| @@ -773,13 +776,10 @@ | ||
| 773 | 776 | } |
| 774 | 777 | } |
| 775 | 778 | if( !showTimeline && g.perm.Hyperlink ){ |
| 776 | 779 | style_submenu_element("Timeline", "%R/info/%T", zUuid); |
| 777 | 780 | } |
| 778 | - zFullName = db_text(0, | |
| 779 | - "SELECT tkt_uuid FROM ticket" | |
| 780 | - " WHERE tkt_uuid GLOB '%q*'", zUuid); | |
| 781 | 781 | if( g.thTrace ) Th_Trace("BEGIN_TKTVIEW<br>\n", -1); |
| 782 | 782 | ticket_init(); |
| 783 | 783 | initializeVariablesFromCGI(); |
| 784 | 784 | getAllTicketFields(); |
| 785 | 785 | initializeVariablesFromDb(); |
| @@ -789,11 +789,16 @@ | ||
| 789 | 789 | safe_html_context(DOCSRC_TICKET); |
| 790 | 790 | Th_Render(zScript); |
| 791 | 791 | if( g.thTrace ) Th_Trace("END_TKTVIEW<br>\n", -1); |
| 792 | 792 | |
| 793 | 793 | if( zFullName ){ |
| 794 | - attachment_list(zFullName, "<h2>Attachments:</h2>", 1); | |
| 794 | + char * z = mprintf( | |
| 795 | + "<h2><a href='%R/attachlist?tkt=%t'>Attachments</a>:</h2>", | |
| 796 | + zFullName | |
| 797 | + ); | |
| 798 | + attachment_list(zFullName, z, 1); | |
| 799 | + fossil_free(z); | |
| 795 | 800 | } |
| 796 | 801 | |
| 797 | 802 | builtin_fossil_js_bundle_or("dom", "storage", NULL); |
| 798 | 803 | builtin_request_js("fossil.page.ticket.js"); |
| 799 | 804 | builtin_fulfill_js_requests(); |
| 800 | 805 |
| --- src/tkt.c | |
| +++ src/tkt.c | |
| @@ -749,13 +749,16 @@ | |
| 749 | } |
| 750 | } |
| 751 | if( g.anon.NewTkt ){ |
| 752 | style_submenu_element("New Ticket", "%R/tktnew"); |
| 753 | } |
| 754 | if( g.anon.ApndTkt && g.anon.Attach ){ |
| 755 | style_submenu_element("Attach", "%R/attachadd?tkt=%T&from=%R/tktview/%t", |
| 756 | zUuid, zUuid); |
| 757 | } |
| 758 | if( P("plaintext") ){ |
| 759 | style_submenu_element("Formatted", "%R/tktview/%s", zUuid); |
| 760 | }else{ |
| 761 | style_submenu_element("Plaintext", "%R/tktview/%s?plaintext", zUuid); |
| @@ -773,13 +776,10 @@ | |
| 773 | } |
| 774 | } |
| 775 | if( !showTimeline && g.perm.Hyperlink ){ |
| 776 | style_submenu_element("Timeline", "%R/info/%T", zUuid); |
| 777 | } |
| 778 | zFullName = db_text(0, |
| 779 | "SELECT tkt_uuid FROM ticket" |
| 780 | " WHERE tkt_uuid GLOB '%q*'", zUuid); |
| 781 | if( g.thTrace ) Th_Trace("BEGIN_TKTVIEW<br>\n", -1); |
| 782 | ticket_init(); |
| 783 | initializeVariablesFromCGI(); |
| 784 | getAllTicketFields(); |
| 785 | initializeVariablesFromDb(); |
| @@ -789,11 +789,16 @@ | |
| 789 | safe_html_context(DOCSRC_TICKET); |
| 790 | Th_Render(zScript); |
| 791 | if( g.thTrace ) Th_Trace("END_TKTVIEW<br>\n", -1); |
| 792 | |
| 793 | if( zFullName ){ |
| 794 | attachment_list(zFullName, "<h2>Attachments:</h2>", 1); |
| 795 | } |
| 796 | |
| 797 | builtin_fossil_js_bundle_or("dom", "storage", NULL); |
| 798 | builtin_request_js("fossil.page.ticket.js"); |
| 799 | builtin_fulfill_js_requests(); |
| 800 |
| --- src/tkt.c | |
| +++ src/tkt.c | |
| @@ -749,13 +749,16 @@ | |
| 749 | } |
| 750 | } |
| 751 | if( g.anon.NewTkt ){ |
| 752 | style_submenu_element("New Ticket", "%R/tktnew"); |
| 753 | } |
| 754 | zFullName = db_text(0, |
| 755 | "SELECT tkt_uuid FROM ticket" |
| 756 | " WHERE tkt_uuid GLOB '%q*'", zUuid); |
| 757 | if( g.anon.ApndTkt && g.anon.Attach ){ |
| 758 | style_submenu_element("Attach", "%R/attachadd?target=%T&from=%R/tktview/%t", |
| 759 | zFullName, zUuid); |
| 760 | } |
| 761 | if( P("plaintext") ){ |
| 762 | style_submenu_element("Formatted", "%R/tktview/%s", zUuid); |
| 763 | }else{ |
| 764 | style_submenu_element("Plaintext", "%R/tktview/%s?plaintext", zUuid); |
| @@ -773,13 +776,10 @@ | |
| 776 | } |
| 777 | } |
| 778 | if( !showTimeline && g.perm.Hyperlink ){ |
| 779 | style_submenu_element("Timeline", "%R/info/%T", zUuid); |
| 780 | } |
| 781 | if( g.thTrace ) Th_Trace("BEGIN_TKTVIEW<br>\n", -1); |
| 782 | ticket_init(); |
| 783 | initializeVariablesFromCGI(); |
| 784 | getAllTicketFields(); |
| 785 | initializeVariablesFromDb(); |
| @@ -789,11 +789,16 @@ | |
| 789 | safe_html_context(DOCSRC_TICKET); |
| 790 | Th_Render(zScript); |
| 791 | if( g.thTrace ) Th_Trace("END_TKTVIEW<br>\n", -1); |
| 792 | |
| 793 | if( zFullName ){ |
| 794 | char * z = mprintf( |
| 795 | "<h2><a href='%R/attachlist?tkt=%t'>Attachments</a>:</h2>", |
| 796 | zFullName |
| 797 | ); |
| 798 | attachment_list(zFullName, z, 1); |
| 799 | fossil_free(z); |
| 800 | } |
| 801 | |
| 802 | builtin_fossil_js_bundle_or("dom", "storage", NULL); |
| 803 | builtin_request_js("fossil.page.ticket.js"); |
| 804 | builtin_fulfill_js_requests(); |
| 805 |
+2
-2
| --- src/util.c | ||
| +++ src/util.c | ||
| @@ -917,12 +917,12 @@ | ||
| 917 | 917 | unsigned char zStr[37]; |
| 918 | 918 | unsigned char *p = zStr; |
| 919 | 919 | int i, k; |
| 920 | 920 | |
| 921 | 921 | sqlite3_randomness(16, aBlob); |
| 922 | - aBlob[6] = (aBlob[6]&0x0f) + 0x40; /* Version byte: 0100 xxxx */ | |
| 923 | - aBlob[8] = (aBlob[8]&0x3f) + 0x80; /* Variant byte: 10xx xxxx */ | |
| 922 | + aBlob[6] = (aBlob[6]&0x0f) | 0x40; /* Version byte: 0100 xxxx */ | |
| 923 | + aBlob[8] = (aBlob[8]&0x3f) | 0x80; /* Variant byte: 10xx xxxx */ | |
| 924 | 924 | |
| 925 | 925 | for(i=0, k=0x550; i<16; i++, k=k>>1){ |
| 926 | 926 | if( k&1 ){ |
| 927 | 927 | *p++ = '-'; /* Add a dash after byte 4, 6, 8, and 12 */ |
| 928 | 928 | } |
| 929 | 929 |
| --- src/util.c | |
| +++ src/util.c | |
| @@ -917,12 +917,12 @@ | |
| 917 | unsigned char zStr[37]; |
| 918 | unsigned char *p = zStr; |
| 919 | int i, k; |
| 920 | |
| 921 | sqlite3_randomness(16, aBlob); |
| 922 | aBlob[6] = (aBlob[6]&0x0f) + 0x40; /* Version byte: 0100 xxxx */ |
| 923 | aBlob[8] = (aBlob[8]&0x3f) + 0x80; /* Variant byte: 10xx xxxx */ |
| 924 | |
| 925 | for(i=0, k=0x550; i<16; i++, k=k>>1){ |
| 926 | if( k&1 ){ |
| 927 | *p++ = '-'; /* Add a dash after byte 4, 6, 8, and 12 */ |
| 928 | } |
| 929 |
| --- src/util.c | |
| +++ src/util.c | |
| @@ -917,12 +917,12 @@ | |
| 917 | unsigned char zStr[37]; |
| 918 | unsigned char *p = zStr; |
| 919 | int i, k; |
| 920 | |
| 921 | sqlite3_randomness(16, aBlob); |
| 922 | aBlob[6] = (aBlob[6]&0x0f) | 0x40; /* Version byte: 0100 xxxx */ |
| 923 | aBlob[8] = (aBlob[8]&0x3f) | 0x80; /* Variant byte: 10xx xxxx */ |
| 924 | |
| 925 | for(i=0, k=0x550; i<16; i++, k=k>>1){ |
| 926 | if( k&1 ){ |
| 927 | *p++ = '-'; /* Add a dash after byte 4, 6, 8, and 12 */ |
| 928 | } |
| 929 |
+11
-39
| --- src/wiki.c | ||
| +++ src/wiki.c | ||
| @@ -626,10 +626,14 @@ | ||
| 626 | 626 | style_submenu_element("Edit", "%R/wikiappend?name=%T", zPageName); |
| 627 | 627 | } |
| 628 | 628 | if( g.perm.Hyperlink ){ |
| 629 | 629 | style_submenu_element("History", "%R/whistory?name=%T", zPageName); |
| 630 | 630 | } |
| 631 | + if( rid>0 && attach_user_may(rid, CFTYPE_WIKI) ){ | |
| 632 | + style_submenu_element("Attach", "%R/attachadd?target=%T", | |
| 633 | + zPageName); | |
| 634 | + } | |
| 631 | 635 | } |
| 632 | 636 | if( !isPopup ){ |
| 633 | 637 | style_set_current_page("%T?name=%T", g.zPath, zPageName); |
| 634 | 638 | wiki_page_header(WIKITYPE_UNKNOWN, zPageName, ""); |
| 635 | 639 | if( !noSubmenu ){ |
| @@ -826,49 +830,15 @@ | ||
| 826 | 830 | ** mtime order. |
| 827 | 831 | */ |
| 828 | 832 | static void wiki_ajax_emit_page_attachments(Manifest * pWiki, |
| 829 | 833 | int latestOnly, |
| 830 | 834 | int nullIfEmpty){ |
| 831 | - int i = 0; | |
| 832 | - Stmt q = empty_Stmt; | |
| 833 | - db_prepare(&q, | |
| 834 | - "SELECT datetime(mtime), src, target, filename, isLatest," | |
| 835 | - " (SELECT uuid FROM blob WHERE rid=attachid) uuid" | |
| 836 | - " FROM attachment" | |
| 837 | - " WHERE target=%Q" | |
| 838 | - " AND (isLatest OR %d)" | |
| 839 | - " ORDER BY target, isLatest DESC, mtime DESC", | |
| 840 | - pWiki->zWikiTitle, !latestOnly | |
| 841 | - ); | |
| 842 | - while(SQLITE_ROW == db_step(&q)){ | |
| 843 | - const char * zTime = db_column_text(&q, 0); | |
| 844 | - const char * zSrc = db_column_text(&q, 1); | |
| 845 | - const char * zTarget = db_column_text(&q, 2); | |
| 846 | - const char * zName = db_column_text(&q, 3); | |
| 847 | - const int isLatest = db_column_int(&q, 4); | |
| 848 | - const char * zUuid = db_column_text(&q, 5); | |
| 849 | - if(!i++){ | |
| 850 | - CX("["); | |
| 851 | - }else{ | |
| 852 | - CX(","); | |
| 853 | - } | |
| 854 | - CX("{"); | |
| 855 | - CX("\"uuid\": %!j, \"src\": %!j, \"target\": %!j, " | |
| 856 | - "\"filename\": %!j, \"mtime\": %!j, \"isLatest\": %s}", | |
| 857 | - zUuid, zSrc, zTarget, | |
| 858 | - zName, zTime, isLatest ? "true" : "false"); | |
| 859 | - } | |
| 860 | - db_finalize(&q); | |
| 861 | - if(!i){ | |
| 862 | - if(nullIfEmpty){ | |
| 863 | - CX("null"); | |
| 864 | - }else{ | |
| 865 | - CX("[]"); | |
| 866 | - } | |
| 867 | - }else{ | |
| 868 | - CX("]"); | |
| 869 | - } | |
| 835 | + Blob b = BLOB_INITIALIZER; | |
| 836 | + attachments_to_json(pWiki, &b, latestOnly, | |
| 837 | + nullIfEmpty ? -1 : 1); | |
| 838 | + CX("%b", &b); | |
| 839 | + blob_reset(&b); | |
| 870 | 840 | } |
| 871 | 841 | |
| 872 | 842 | /* |
| 873 | 843 | ** Proxy for wiki_ajax_emit_page_attachments() which attempts to load |
| 874 | 844 | ** the given wiki page artifact. Returns true if it can load the given |
| @@ -1139,10 +1109,12 @@ | ||
| 1139 | 1109 | ** |
| 1140 | 1110 | ** URL params: |
| 1141 | 1111 | ** |
| 1142 | 1112 | ** mimetype = the wiki page mimetype (determines rendering style) |
| 1143 | 1113 | ** content = the wiki page content |
| 1114 | +** | |
| 1115 | +** Responds with a partial HTML document. | |
| 1144 | 1116 | */ |
| 1145 | 1117 | static void wiki_ajax_route_preview(void){ |
| 1146 | 1118 | const char * zContent = P("content"); |
| 1147 | 1119 | |
| 1148 | 1120 | if( zContent==0 ){ |
| 1149 | 1121 |
| --- src/wiki.c | |
| +++ src/wiki.c | |
| @@ -626,10 +626,14 @@ | |
| 626 | style_submenu_element("Edit", "%R/wikiappend?name=%T", zPageName); |
| 627 | } |
| 628 | if( g.perm.Hyperlink ){ |
| 629 | style_submenu_element("History", "%R/whistory?name=%T", zPageName); |
| 630 | } |
| 631 | } |
| 632 | if( !isPopup ){ |
| 633 | style_set_current_page("%T?name=%T", g.zPath, zPageName); |
| 634 | wiki_page_header(WIKITYPE_UNKNOWN, zPageName, ""); |
| 635 | if( !noSubmenu ){ |
| @@ -826,49 +830,15 @@ | |
| 826 | ** mtime order. |
| 827 | */ |
| 828 | static void wiki_ajax_emit_page_attachments(Manifest * pWiki, |
| 829 | int latestOnly, |
| 830 | int nullIfEmpty){ |
| 831 | int i = 0; |
| 832 | Stmt q = empty_Stmt; |
| 833 | db_prepare(&q, |
| 834 | "SELECT datetime(mtime), src, target, filename, isLatest," |
| 835 | " (SELECT uuid FROM blob WHERE rid=attachid) uuid" |
| 836 | " FROM attachment" |
| 837 | " WHERE target=%Q" |
| 838 | " AND (isLatest OR %d)" |
| 839 | " ORDER BY target, isLatest DESC, mtime DESC", |
| 840 | pWiki->zWikiTitle, !latestOnly |
| 841 | ); |
| 842 | while(SQLITE_ROW == db_step(&q)){ |
| 843 | const char * zTime = db_column_text(&q, 0); |
| 844 | const char * zSrc = db_column_text(&q, 1); |
| 845 | const char * zTarget = db_column_text(&q, 2); |
| 846 | const char * zName = db_column_text(&q, 3); |
| 847 | const int isLatest = db_column_int(&q, 4); |
| 848 | const char * zUuid = db_column_text(&q, 5); |
| 849 | if(!i++){ |
| 850 | CX("["); |
| 851 | }else{ |
| 852 | CX(","); |
| 853 | } |
| 854 | CX("{"); |
| 855 | CX("\"uuid\": %!j, \"src\": %!j, \"target\": %!j, " |
| 856 | "\"filename\": %!j, \"mtime\": %!j, \"isLatest\": %s}", |
| 857 | zUuid, zSrc, zTarget, |
| 858 | zName, zTime, isLatest ? "true" : "false"); |
| 859 | } |
| 860 | db_finalize(&q); |
| 861 | if(!i){ |
| 862 | if(nullIfEmpty){ |
| 863 | CX("null"); |
| 864 | }else{ |
| 865 | CX("[]"); |
| 866 | } |
| 867 | }else{ |
| 868 | CX("]"); |
| 869 | } |
| 870 | } |
| 871 | |
| 872 | /* |
| 873 | ** Proxy for wiki_ajax_emit_page_attachments() which attempts to load |
| 874 | ** the given wiki page artifact. Returns true if it can load the given |
| @@ -1139,10 +1109,12 @@ | |
| 1139 | ** |
| 1140 | ** URL params: |
| 1141 | ** |
| 1142 | ** mimetype = the wiki page mimetype (determines rendering style) |
| 1143 | ** content = the wiki page content |
| 1144 | */ |
| 1145 | static void wiki_ajax_route_preview(void){ |
| 1146 | const char * zContent = P("content"); |
| 1147 | |
| 1148 | if( zContent==0 ){ |
| 1149 |
| --- src/wiki.c | |
| +++ src/wiki.c | |
| @@ -626,10 +626,14 @@ | |
| 626 | style_submenu_element("Edit", "%R/wikiappend?name=%T", zPageName); |
| 627 | } |
| 628 | if( g.perm.Hyperlink ){ |
| 629 | style_submenu_element("History", "%R/whistory?name=%T", zPageName); |
| 630 | } |
| 631 | if( rid>0 && attach_user_may(rid, CFTYPE_WIKI) ){ |
| 632 | style_submenu_element("Attach", "%R/attachadd?target=%T", |
| 633 | zPageName); |
| 634 | } |
| 635 | } |
| 636 | if( !isPopup ){ |
| 637 | style_set_current_page("%T?name=%T", g.zPath, zPageName); |
| 638 | wiki_page_header(WIKITYPE_UNKNOWN, zPageName, ""); |
| 639 | if( !noSubmenu ){ |
| @@ -826,49 +830,15 @@ | |
| 830 | ** mtime order. |
| 831 | */ |
| 832 | static void wiki_ajax_emit_page_attachments(Manifest * pWiki, |
| 833 | int latestOnly, |
| 834 | int nullIfEmpty){ |
| 835 | Blob b = BLOB_INITIALIZER; |
| 836 | attachments_to_json(pWiki, &b, latestOnly, |
| 837 | nullIfEmpty ? -1 : 1); |
| 838 | CX("%b", &b); |
| 839 | blob_reset(&b); |
| 840 | } |
| 841 | |
| 842 | /* |
| 843 | ** Proxy for wiki_ajax_emit_page_attachments() which attempts to load |
| 844 | ** the given wiki page artifact. Returns true if it can load the given |
| @@ -1139,10 +1109,12 @@ | |
| 1109 | ** |
| 1110 | ** URL params: |
| 1111 | ** |
| 1112 | ** mimetype = the wiki page mimetype (determines rendering style) |
| 1113 | ** content = the wiki page content |
| 1114 | ** |
| 1115 | ** Responds with a partial HTML document. |
| 1116 | */ |
| 1117 | static void wiki_ajax_route_preview(void){ |
| 1118 | const char * zContent = P("content"); |
| 1119 | |
| 1120 | if( zContent==0 ){ |
| 1121 |
+16
-15
| --- src/wiki.wiki | ||
| +++ src/wiki.wiki | ||
| @@ -55,25 +55,26 @@ | ||
| 55 | 55 | <nowiki>[#anchor-name],</nowiki> providing you have added the necessary |
| 56 | 56 | "<a name='anchor-name'></a>" tag to your wiki page. |
| 57 | 57 | |
| 58 | 58 | 6. <b>HTML.</b> |
| 59 | 59 | The following standard HTML elements may be used: |
| 60 | - <a> <address> <article> <aside> <b> | |
| 61 | - <big> <blockquote> <br> <center> <cite> | |
| 62 | - <code> <col> <colgroup> <dd> | |
| 63 | - <del> <dfn> | |
| 64 | - <div> <dl> <dt> <em> <font> <footer> | |
| 65 | - <ins> | |
| 66 | - <h1> <h2> <h3> <h4> <h5> <h6> | |
| 67 | - <header> <hr> <i> <img> <kbd> <li> | |
| 68 | - <nav> <nobr> <nowiki> <ol> <p> <pre> | |
| 69 | - <s> <samp> <section> <small> <span> | |
| 70 | - <strike> <strong> <sub> <sup> <table> | |
| 71 | - <tbody> <td> <tfoot> <th> <thead> | |
| 72 | - <title> <tr> <tt> <u> <ul> <var> | |
| 73 | - <verbatim>. There are two non-standard elements available: | |
| 74 | - <verbatim> and <nowiki>. No other elements are allowed. | |
| 60 | + <a> <abbr> <address> <article> <aside> | |
| 61 | + <b> <big> <blockquote> <br> <caption> | |
| 62 | + <center> <cite> <code> <col> <colgroup> | |
| 63 | + <dd> <del> <details> <dfn> <div> <dl> | |
| 64 | + <dt> <em> <figcaption> <figure> <font> | |
| 65 | + <footer> <h1> <h2> <h3> <h4> <h5> | |
| 66 | + <h6> <header> <hr> <i> <img> <ins> | |
| 67 | + <kbd> <label> <li> <mark> <meter> <nav> | |
| 68 | + <nobr> <nowiki> <ol> <p> <picture> <pre> | |
| 69 | + <progress> <q> <s> <samp> <section> <small> | |
| 70 | + <source> <span> <strike> <strong> <sub> | |
| 71 | + <summary> <sup> <table> <tbody> <td> | |
| 72 | + <tfoot> <th> <thead> <time> <title> <tr> | |
| 73 | + <tt> <u> <ul> <var> <wbr> <verbatim>. | |
| 74 | + There are two non-standard elements available: <verbatim> and | |
| 75 | + <nowiki>. No other elements are allowed. | |
| 75 | 76 | All attributes are checked and only a few benign attributes are |
| 76 | 77 | allowed on each element. In particular, any attributes that specify |
| 77 | 78 | javascript or CSS are elided. |
| 78 | 79 | |
| 79 | 80 | 7. <b>Special Markup.</b> |
| 80 | 81 |
| --- src/wiki.wiki | |
| +++ src/wiki.wiki | |
| @@ -55,25 +55,26 @@ | |
| 55 | <nowiki>[#anchor-name],</nowiki> providing you have added the necessary |
| 56 | "<a name='anchor-name'></a>" tag to your wiki page. |
| 57 | |
| 58 | 6. <b>HTML.</b> |
| 59 | The following standard HTML elements may be used: |
| 60 | <a> <address> <article> <aside> <b> |
| 61 | <big> <blockquote> <br> <center> <cite> |
| 62 | <code> <col> <colgroup> <dd> |
| 63 | <del> <dfn> |
| 64 | <div> <dl> <dt> <em> <font> <footer> |
| 65 | <ins> |
| 66 | <h1> <h2> <h3> <h4> <h5> <h6> |
| 67 | <header> <hr> <i> <img> <kbd> <li> |
| 68 | <nav> <nobr> <nowiki> <ol> <p> <pre> |
| 69 | <s> <samp> <section> <small> <span> |
| 70 | <strike> <strong> <sub> <sup> <table> |
| 71 | <tbody> <td> <tfoot> <th> <thead> |
| 72 | <title> <tr> <tt> <u> <ul> <var> |
| 73 | <verbatim>. There are two non-standard elements available: |
| 74 | <verbatim> and <nowiki>. No other elements are allowed. |
| 75 | All attributes are checked and only a few benign attributes are |
| 76 | allowed on each element. In particular, any attributes that specify |
| 77 | javascript or CSS are elided. |
| 78 | |
| 79 | 7. <b>Special Markup.</b> |
| 80 |
| --- src/wiki.wiki | |
| +++ src/wiki.wiki | |
| @@ -55,25 +55,26 @@ | |
| 55 | <nowiki>[#anchor-name],</nowiki> providing you have added the necessary |
| 56 | "<a name='anchor-name'></a>" tag to your wiki page. |
| 57 | |
| 58 | 6. <b>HTML.</b> |
| 59 | The following standard HTML elements may be used: |
| 60 | <a> <abbr> <address> <article> <aside> |
| 61 | <b> <big> <blockquote> <br> <caption> |
| 62 | <center> <cite> <code> <col> <colgroup> |
| 63 | <dd> <del> <details> <dfn> <div> <dl> |
| 64 | <dt> <em> <figcaption> <figure> <font> |
| 65 | <footer> <h1> <h2> <h3> <h4> <h5> |
| 66 | <h6> <header> <hr> <i> <img> <ins> |
| 67 | <kbd> <label> <li> <mark> <meter> <nav> |
| 68 | <nobr> <nowiki> <ol> <p> <picture> <pre> |
| 69 | <progress> <q> <s> <samp> <section> <small> |
| 70 | <source> <span> <strike> <strong> <sub> |
| 71 | <summary> <sup> <table> <tbody> <td> |
| 72 | <tfoot> <th> <thead> <time> <title> <tr> |
| 73 | <tt> <u> <ul> <var> <wbr> <verbatim>. |
| 74 | There are two non-standard elements available: <verbatim> and |
| 75 | <nowiki>. No other elements are allowed. |
| 76 | All attributes are checked and only a few benign attributes are |
| 77 | allowed on each element. In particular, any attributes that specify |
| 78 | javascript or CSS are elided. |
| 79 | |
| 80 | 7. <b>Special Markup.</b> |
| 81 |
+115
-43
| --- src/wikiformat.c | ||
| +++ src/wikiformat.c | ||
| @@ -68,25 +68,37 @@ | ||
| 68 | 68 | ATTR_ALT, |
| 69 | 69 | ATTR_BGCOLOR, |
| 70 | 70 | ATTR_BORDER, |
| 71 | 71 | ATTR_CELLPADDING, |
| 72 | 72 | ATTR_CELLSPACING, |
| 73 | + ATTR_CITE, | |
| 73 | 74 | ATTR_CLASS, |
| 74 | 75 | ATTR_CLEAR, |
| 75 | 76 | ATTR_COLOR, |
| 76 | 77 | ATTR_COLSPAN, |
| 77 | 78 | ATTR_COMPACT, |
| 79 | + ATTR_DATETIME, | |
| 78 | 80 | ATTR_FACE, |
| 81 | + ATTR_FOR, | |
| 79 | 82 | ATTR_HEIGHT, |
| 83 | + ATTR_HIGH, | |
| 80 | 84 | ATTR_HREF, |
| 81 | 85 | ATTR_HSPACE, |
| 82 | 86 | ATTR_ID, |
| 83 | 87 | ATTR_LINKS, |
| 88 | + ATTR_LOW, | |
| 89 | + ATTR_MAX, | |
| 90 | + ATTR_MEDIA, | |
| 91 | + ATTR_MIN, | |
| 84 | 92 | ATTR_NAME, |
| 93 | + ATTR_OPEN, | |
| 94 | + ATTR_OPTIMUM, | |
| 85 | 95 | ATTR_ROWSPAN, |
| 86 | 96 | ATTR_SIZE, |
| 97 | + ATTR_SIZES, | |
| 87 | 98 | ATTR_SRC, |
| 99 | + ATTR_SRCSET, | |
| 88 | 100 | ATTR_START, |
| 89 | 101 | ATTR_STYLE, |
| 90 | 102 | ATTR_TARGET, |
| 91 | 103 | ATTR_TITLE, |
| 92 | 104 | ATTR_TYPE, |
| @@ -94,46 +106,48 @@ | ||
| 94 | 106 | ATTR_VALUE, |
| 95 | 107 | ATTR_VSPACE, |
| 96 | 108 | ATTR_WIDTH |
| 97 | 109 | }; |
| 98 | 110 | |
| 99 | -enum amsk_t { | |
| 100 | - AMSK_ALIGN = 0x00000001, | |
| 101 | - AMSK_ALT = 0x00000002, | |
| 102 | - AMSK_BGCOLOR = 0x00000004, | |
| 103 | - AMSK_BORDER = 0x00000008, | |
| 104 | - AMSK_CELLPADDING = 0x00000010, | |
| 105 | - AMSK_CELLSPACING = 0x00000020, | |
| 106 | - AMSK_CLASS = 0x00000040, | |
| 107 | - AMSK_CLEAR = 0x00000080, | |
| 108 | - AMSK_COLOR = 0x00000100, | |
| 109 | - AMSK_COLSPAN = 0x00000200, | |
| 110 | - AMSK_COMPACT = 0x00000400, | |
| 111 | - AMSK_FACE = 0x00000800, | |
| 112 | - AMSK_HEIGHT = 0x00001000, | |
| 113 | - AMSK_HREF = 0x00002000, | |
| 114 | - AMSK_HSPACE = 0x00004000, | |
| 115 | - AMSK_ID = 0x00008000, | |
| 116 | - AMSK_LINKS = 0x00010000, | |
| 117 | - AMSK_NAME = 0x00020000, | |
| 118 | - AMSK_ROWSPAN = 0x00040000, | |
| 119 | - AMSK_SIZE = 0x00080000, | |
| 120 | - AMSK_SRC = 0x00100000, | |
| 121 | - AMSK_START = 0x00200000, | |
| 122 | - AMSK_STYLE = 0x00400000, | |
| 123 | - AMSK_TARGET = 0x00800000, | |
| 124 | - AMSK_TITLE = 0x01000000, | |
| 125 | - AMSK_TYPE = 0x02000000, | |
| 126 | - AMSK_VALIGN = 0x04000000, | |
| 127 | - AMSK_VALUE = 0x08000000, | |
| 128 | - AMSK_VSPACE = 0x10000000, | |
| 129 | - AMSK_WIDTH = 0x20000000 | |
| 130 | -}; | |
| 111 | +typedef uint64_t amsk_t; | |
| 112 | +#define AMSK_ALIGN ((amsk_t)1 << 0) | |
| 113 | +#define AMSK_ALT ((amsk_t)1 << 1) | |
| 114 | +#define AMSK_BGCOLOR ((amsk_t)1 << 2) | |
| 115 | +#define AMSK_BORDER ((amsk_t)1 << 3) | |
| 116 | +#define AMSK_CELLPADDING ((amsk_t)1 << 4) | |
| 117 | +#define AMSK_CELLSPACING ((amsk_t)1 << 5) | |
| 118 | +#define AMSK_CLASS ((amsk_t)1 << 6) | |
| 119 | +#define AMSK_CLEAR ((amsk_t)1 << 7) | |
| 120 | +#define AMSK_COLOR ((amsk_t)1 << 8) | |
| 121 | +#define AMSK_COLSPAN ((amsk_t)1 << 9) | |
| 122 | +#define AMSK_COMPACT ((amsk_t)1 << 10) | |
| 123 | +#define AMSK_FACE ((amsk_t)1 << 11) | |
| 124 | +#define AMSK_HEIGHT ((amsk_t)1 << 12) | |
| 125 | +#define AMSK_HREF ((amsk_t)1 << 13) | |
| 126 | +#define AMSK_HSPACE ((amsk_t)1 << 14) | |
| 127 | +#define AMSK_ID ((amsk_t)1 << 15) | |
| 128 | +#define AMSK_LINKS ((amsk_t)1 << 16) | |
| 129 | +#define AMSK_NAME ((amsk_t)1 << 17) | |
| 130 | +#define AMSK_OPEN ((amsk_t)1 << 18) | |
| 131 | +#define AMSK_ROWSPAN ((amsk_t)1 << 19) | |
| 132 | +#define AMSK_SIZE ((amsk_t)1 << 20) | |
| 133 | +#define AMSK_SRC ((amsk_t)1 << 21) | |
| 134 | +#define AMSK_START ((amsk_t)1 << 22) | |
| 135 | +#define AMSK_STYLE ((amsk_t)1 << 23) | |
| 136 | +#define AMSK_TARGET ((amsk_t)1 << 24) | |
| 137 | +#define AMSK_TITLE ((amsk_t)1 << 25) | |
| 138 | +#define AMSK_TYPE ((amsk_t)1 << 26) | |
| 139 | +#define AMSK_VALIGN ((amsk_t)1 << 27) | |
| 140 | +#define AMSK_VALUE ((amsk_t)1 << 28) | |
| 141 | +#define AMSK_VSPACE ((amsk_t)1 << 29) | |
| 142 | +#define AMSK_WIDTH ((amsk_t)1 << 30) | |
| 143 | +#define AMSK_CITE ((amsk_t)1 << 31) | |
| 144 | +#define AMSK_DATETIME ((amsk_t)1 << 32) | |
| 131 | 145 | |
| 132 | 146 | static const struct AllowedAttribute { |
| 133 | 147 | const char *zName; |
| 134 | - unsigned int iMask; | |
| 148 | + amsk_t iMask; | |
| 135 | 149 | } aAttribute[] = { |
| 136 | 150 | /* These indexes MUST line up with their |
| 137 | 151 | corresponding allowed_attr_t enum values. |
| 138 | 152 | */ |
| 139 | 153 | { 0, 0 }, |
| @@ -141,25 +155,37 @@ | ||
| 141 | 155 | { "alt", AMSK_ALT }, |
| 142 | 156 | { "bgcolor", AMSK_BGCOLOR }, |
| 143 | 157 | { "border", AMSK_BORDER }, |
| 144 | 158 | { "cellpadding", AMSK_CELLPADDING }, |
| 145 | 159 | { "cellspacing", AMSK_CELLSPACING }, |
| 160 | + { "cite", AMSK_CITE }, | |
| 146 | 161 | { "class", AMSK_CLASS }, |
| 147 | 162 | { "clear", AMSK_CLEAR }, |
| 148 | 163 | { "color", AMSK_COLOR }, |
| 149 | 164 | { "colspan", AMSK_COLSPAN }, |
| 150 | 165 | { "compact", AMSK_COMPACT }, |
| 166 | + { "datetime", AMSK_DATETIME }, | |
| 151 | 167 | { "face", AMSK_FACE }, |
| 168 | + { "for", 0 }, | |
| 152 | 169 | { "height", AMSK_HEIGHT }, |
| 170 | + { "high", 0 }, | |
| 153 | 171 | { "href", AMSK_HREF }, |
| 154 | 172 | { "hspace", AMSK_HSPACE }, |
| 155 | 173 | { "id", AMSK_ID }, |
| 156 | 174 | { "links", AMSK_LINKS }, |
| 175 | + { "low", 0 }, | |
| 176 | + { "max", 0 }, | |
| 177 | + { "media", 0 }, | |
| 178 | + { "min", 0 }, | |
| 157 | 179 | { "name", AMSK_NAME }, |
| 180 | + { "open", AMSK_OPEN }, | |
| 181 | + { "optimum", 0 }, | |
| 158 | 182 | { "rowspan", AMSK_ROWSPAN }, |
| 159 | 183 | { "size", AMSK_SIZE }, |
| 184 | + { "sizes", 0 }, | |
| 160 | 185 | { "src", AMSK_SRC }, |
| 186 | + { "srcset", 0 }, | |
| 161 | 187 | { "start", AMSK_START }, |
| 162 | 188 | { "style", AMSK_STYLE }, |
| 163 | 189 | { "target", AMSK_TARGET }, |
| 164 | 190 | { "title", AMSK_TITLE }, |
| 165 | 191 | { "type", AMSK_TYPE }, |
| @@ -209,10 +235,11 @@ | ||
| 209 | 235 | MARKUP_HTML5_ASIDE, |
| 210 | 236 | MARKUP_B, |
| 211 | 237 | MARKUP_BIG, |
| 212 | 238 | MARKUP_BLOCKQUOTE, |
| 213 | 239 | MARKUP_BR, |
| 240 | + MARKUP_CAPTION, | |
| 214 | 241 | MARKUP_CENTER, |
| 215 | 242 | MARKUP_CITE, |
| 216 | 243 | MARKUP_CODE, |
| 217 | 244 | MARKUP_COL, |
| 218 | 245 | MARKUP_COLGROUP, |
| @@ -222,10 +249,12 @@ | ||
| 222 | 249 | MARKUP_DFN, |
| 223 | 250 | MARKUP_DIV, |
| 224 | 251 | MARKUP_DL, |
| 225 | 252 | MARKUP_DT, |
| 226 | 253 | MARKUP_EM, |
| 254 | + MARKUP_FIGCAPTION, | |
| 255 | + MARKUP_FIGURE, | |
| 227 | 256 | MARKUP_FONT, |
| 228 | 257 | MARKUP_HTML5_FOOTER, |
| 229 | 258 | MARKUP_H1, |
| 230 | 259 | MARKUP_H2, |
| 231 | 260 | MARKUP_H3, |
| @@ -236,21 +265,28 @@ | ||
| 236 | 265 | MARKUP_HR, |
| 237 | 266 | MARKUP_I, |
| 238 | 267 | MARKUP_IMG, |
| 239 | 268 | MARKUP_INS, |
| 240 | 269 | MARKUP_KBD, |
| 270 | + MARKUP_LABEL, | |
| 241 | 271 | MARKUP_LI, |
| 272 | + MARKUP_MARK, | |
| 273 | + MARKUP_METER, | |
| 242 | 274 | MARKUP_HTML5_NAV, |
| 243 | 275 | MARKUP_NOBR, |
| 244 | 276 | MARKUP_NOWIKI, |
| 245 | 277 | MARKUP_OL, |
| 246 | 278 | MARKUP_P, |
| 279 | + MARKUP_PICTURE, | |
| 247 | 280 | MARKUP_PRE, |
| 281 | + MARKUP_PROGRESS, | |
| 282 | + MARKUP_Q, | |
| 248 | 283 | MARKUP_S, |
| 249 | 284 | MARKUP_SAMP, |
| 250 | 285 | MARKUP_HTML5_SECTION, |
| 251 | 286 | MARKUP_SMALL, |
| 287 | + MARKUP_SOURCE, | |
| 252 | 288 | MARKUP_SPAN, |
| 253 | 289 | MARKUP_STRIKE, |
| 254 | 290 | MARKUP_STRONG, |
| 255 | 291 | MARKUP_SUB, |
| 256 | 292 | MARKUP_SUMMARY, |
| @@ -259,16 +295,18 @@ | ||
| 259 | 295 | MARKUP_TBODY, |
| 260 | 296 | MARKUP_TD, |
| 261 | 297 | MARKUP_TFOOT, |
| 262 | 298 | MARKUP_TH, |
| 263 | 299 | MARKUP_THEAD, |
| 300 | + MARKUP_TIME, | |
| 264 | 301 | MARKUP_TITLE, |
| 265 | 302 | MARKUP_TR, |
| 266 | 303 | MARKUP_TT, |
| 267 | 304 | MARKUP_U, |
| 268 | 305 | MARKUP_UL, |
| 269 | 306 | MARKUP_VAR, |
| 307 | + MARKUP_WBR, | |
| 270 | 308 | MARKUP_VERBATIM |
| 271 | 309 | }; |
| 272 | 310 | |
| 273 | 311 | /* |
| 274 | 312 | ** The various markup is divided into the following types: |
| @@ -299,11 +337,11 @@ | ||
| 299 | 337 | |
| 300 | 338 | static const struct AllowedMarkup { |
| 301 | 339 | const char *zName; /* Name of the markup */ |
| 302 | 340 | char iCode; /* The MARKUP_* code */ |
| 303 | 341 | short int iType; /* The MUTYPE_* code */ |
| 304 | - int allowedAttr; /* Allowed attributes on this markup */ | |
| 342 | + amsk_t allowedAttr; /* Allowed attributes on this markup */ | |
| 305 | 343 | } aMarkup[] = { |
| 306 | 344 | { 0, MARKUP_INVALID, 0, 0 }, |
| 307 | 345 | { "a", MARKUP_A, MUTYPE_HYPERLINK, |
| 308 | 346 | AMSK_HREF|AMSK_NAME|AMSK_CLASS|AMSK_TARGET|AMSK_STYLE| |
| 309 | 347 | AMSK_TITLE}, |
| @@ -316,10 +354,12 @@ | ||
| 316 | 354 | AMSK_ID|AMSK_CLASS|AMSK_STYLE }, |
| 317 | 355 | { "b", MARKUP_B, MUTYPE_FONT, AMSK_STYLE }, |
| 318 | 356 | { "big", MARKUP_BIG, MUTYPE_FONT, AMSK_STYLE }, |
| 319 | 357 | { "blockquote", MARKUP_BLOCKQUOTE, MUTYPE_BLOCK, AMSK_STYLE }, |
| 320 | 358 | { "br", MARKUP_BR, MUTYPE_SINGLE, AMSK_CLEAR }, |
| 359 | + { "caption", MARKUP_CAPTION, MUTYPE_BLOCK, | |
| 360 | + AMSK_ID|AMSK_CLASS|AMSK_STYLE }, | |
| 321 | 361 | { "center", MARKUP_CENTER, MUTYPE_BLOCK, AMSK_STYLE }, |
| 322 | 362 | { "cite", MARKUP_CITE, MUTYPE_FONT, AMSK_STYLE }, |
| 323 | 363 | { "code", MARKUP_CODE, MUTYPE_FONT, AMSK_STYLE }, |
| 324 | 364 | { "col", MARKUP_COL, MUTYPE_SINGLE, |
| 325 | 365 | AMSK_ALIGN|AMSK_CLASS|AMSK_COLSPAN|AMSK_WIDTH|AMSK_STYLE }, |
| @@ -326,18 +366,22 @@ | ||
| 326 | 366 | { "colgroup", MARKUP_COLGROUP, MUTYPE_BLOCK, |
| 327 | 367 | AMSK_ALIGN|AMSK_CLASS|AMSK_COLSPAN|AMSK_WIDTH|AMSK_STYLE}, |
| 328 | 368 | { "dd", MARKUP_DD, MUTYPE_LI, AMSK_STYLE }, |
| 329 | 369 | { "del", MARKUP_DEL, MUTYPE_FONT, AMSK_STYLE }, |
| 330 | 370 | { "details", MARKUP_DETAILS, MUTYPE_BLOCK, |
| 331 | - AMSK_ID|AMSK_CLASS|AMSK_STYLE }, | |
| 371 | + AMSK_ID|AMSK_CLASS|AMSK_STYLE|AMSK_OPEN }, | |
| 332 | 372 | { "dfn", MARKUP_DFN, MUTYPE_FONT, AMSK_STYLE }, |
| 333 | 373 | { "div", MARKUP_DIV, MUTYPE_BLOCK, |
| 334 | 374 | AMSK_ID|AMSK_CLASS|AMSK_STYLE }, |
| 335 | 375 | { "dl", MARKUP_DL, MUTYPE_LIST, |
| 336 | 376 | AMSK_COMPACT|AMSK_STYLE }, |
| 337 | 377 | { "dt", MARKUP_DT, MUTYPE_LI, AMSK_STYLE }, |
| 338 | 378 | { "em", MARKUP_EM, MUTYPE_FONT, AMSK_STYLE }, |
| 379 | + { "figcaption", MARKUP_FIGCAPTION, MUTYPE_BLOCK, | |
| 380 | + AMSK_ID|AMSK_CLASS|AMSK_STYLE }, | |
| 381 | + { "figure", MARKUP_FIGURE, MUTYPE_BLOCK, | |
| 382 | + AMSK_ID|AMSK_CLASS|AMSK_STYLE }, | |
| 339 | 383 | { "font", MARKUP_FONT, MUTYPE_FONT, |
| 340 | 384 | AMSK_COLOR|AMSK_FACE|AMSK_SIZE|AMSK_STYLE }, |
| 341 | 385 | { "footer", MARKUP_HTML5_FOOTER, MUTYPE_BLOCK, |
| 342 | 386 | AMSK_ID|AMSK_CLASS|AMSK_STYLE }, |
| 343 | 387 | { "h1", MARKUP_H1, MUTYPE_BLOCK, |
| @@ -358,29 +402,43 @@ | ||
| 358 | 402 | AMSK_ALIGN|AMSK_COLOR|AMSK_SIZE|AMSK_WIDTH| |
| 359 | 403 | AMSK_STYLE|AMSK_CLASS }, |
| 360 | 404 | { "i", MARKUP_I, MUTYPE_FONT, AMSK_STYLE }, |
| 361 | 405 | { "img", MARKUP_IMG, MUTYPE_SINGLE, |
| 362 | 406 | AMSK_ALIGN|AMSK_ALT|AMSK_BORDER|AMSK_HEIGHT| |
| 363 | - AMSK_HSPACE|AMSK_SRC|AMSK_VSPACE|AMSK_WIDTH|AMSK_STYLE }, | |
| 407 | + AMSK_HSPACE|AMSK_SRC|AMSK_VSPACE|AMSK_WIDTH|AMSK_STYLE }, | |
| 364 | 408 | { "ins", MARKUP_INS, MUTYPE_FONT, AMSK_STYLE }, |
| 365 | 409 | { "kbd", MARKUP_KBD, MUTYPE_FONT, AMSK_STYLE }, |
| 410 | + { "label", MARKUP_LABEL, MUTYPE_FONT, | |
| 411 | + AMSK_ID|AMSK_CLASS|AMSK_STYLE }, | |
| 366 | 412 | { "li", MARKUP_LI, MUTYPE_LI, |
| 367 | 413 | AMSK_TYPE|AMSK_VALUE|AMSK_STYLE }, |
| 414 | + { "mark", MARKUP_MARK, MUTYPE_FONT, | |
| 415 | + AMSK_ID|AMSK_CLASS|AMSK_STYLE }, | |
| 416 | + { "meter", MARKUP_METER, MUTYPE_FONT, | |
| 417 | + AMSK_ID|AMSK_CLASS|AMSK_STYLE|AMSK_VALUE }, | |
| 368 | 418 | { "nav", MARKUP_HTML5_NAV, MUTYPE_BLOCK, |
| 369 | 419 | AMSK_ID|AMSK_CLASS|AMSK_STYLE }, |
| 370 | 420 | { "nobr", MARKUP_NOBR, MUTYPE_FONT, 0 }, |
| 371 | 421 | { "nowiki", MARKUP_NOWIKI, MUTYPE_SPECIAL, 0 }, |
| 372 | 422 | { "ol", MARKUP_OL, MUTYPE_LIST, |
| 373 | 423 | AMSK_START|AMSK_TYPE|AMSK_COMPACT|AMSK_STYLE }, |
| 374 | 424 | { "p", MARKUP_P, MUTYPE_BLOCK, |
| 375 | 425 | AMSK_ALIGN|AMSK_CLASS|AMSK_STYLE }, |
| 426 | + { "picture", MARKUP_PICTURE, MUTYPE_BLOCK, | |
| 427 | + AMSK_ID|AMSK_CLASS|AMSK_STYLE }, | |
| 376 | 428 | { "pre", MARKUP_PRE, MUTYPE_BLOCK, AMSK_STYLE }, |
| 429 | + { "progress", MARKUP_PROGRESS, MUTYPE_FONT, | |
| 430 | + AMSK_ID|AMSK_CLASS|AMSK_STYLE|AMSK_VALUE }, | |
| 431 | + { "q", MARKUP_Q, MUTYPE_FONT, | |
| 432 | + AMSK_CLASS|AMSK_STYLE|AMSK_CITE }, | |
| 377 | 433 | { "s", MARKUP_S, MUTYPE_FONT, AMSK_STYLE }, |
| 378 | 434 | { "samp", MARKUP_SAMP, MUTYPE_FONT, AMSK_STYLE }, |
| 379 | 435 | { "section", MARKUP_HTML5_SECTION, MUTYPE_BLOCK, |
| 380 | 436 | AMSK_ID|AMSK_CLASS|AMSK_STYLE }, |
| 381 | 437 | { "small", MARKUP_SMALL, MUTYPE_FONT, AMSK_STYLE }, |
| 438 | + { "source", MARKUP_SOURCE, MUTYPE_SINGLE, | |
| 439 | + AMSK_TYPE|AMSK_ID|AMSK_CLASS|AMSK_STYLE }, | |
| 382 | 440 | { "span", MARKUP_SPAN, MUTYPE_BLOCK, |
| 383 | 441 | AMSK_ALIGN|AMSK_CLASS|AMSK_STYLE }, |
| 384 | 442 | { "strike", MARKUP_STRIKE, MUTYPE_FONT, AMSK_STYLE }, |
| 385 | 443 | { "strong", MARKUP_STRONG, MUTYPE_FONT, AMSK_STYLE }, |
| 386 | 444 | { "sub", MARKUP_SUB, MUTYPE_FONT, AMSK_STYLE }, |
| @@ -401,18 +459,21 @@ | ||
| 401 | 459 | { "th", MARKUP_TH, MUTYPE_TD, |
| 402 | 460 | AMSK_ALIGN|AMSK_BGCOLOR|AMSK_COLSPAN| |
| 403 | 461 | AMSK_ROWSPAN|AMSK_VALIGN|AMSK_CLASS|AMSK_STYLE }, |
| 404 | 462 | { "thead", MARKUP_THEAD, MUTYPE_BLOCK, |
| 405 | 463 | AMSK_ALIGN|AMSK_CLASS|AMSK_STYLE }, |
| 464 | + { "time", MARKUP_TIME, MUTYPE_FONT, | |
| 465 | + AMSK_DATETIME|AMSK_ID|AMSK_CLASS|AMSK_STYLE|AMSK_TITLE }, | |
| 406 | 466 | { "title", MARKUP_TITLE, MUTYPE_BLOCK, 0 }, |
| 407 | 467 | { "tr", MARKUP_TR, MUTYPE_TR, |
| 408 | 468 | AMSK_ALIGN|AMSK_BGCOLOR|AMSK_VALIGN|AMSK_CLASS|AMSK_STYLE }, |
| 409 | 469 | { "tt", MARKUP_TT, MUTYPE_FONT, AMSK_STYLE }, |
| 410 | 470 | { "u", MARKUP_U, MUTYPE_FONT, AMSK_STYLE }, |
| 411 | 471 | { "ul", MARKUP_UL, MUTYPE_LIST, |
| 412 | 472 | AMSK_TYPE|AMSK_COMPACT|AMSK_STYLE }, |
| 413 | 473 | { "var", MARKUP_VAR, MUTYPE_FONT, AMSK_STYLE }, |
| 474 | + { "wbr", MARKUP_WBR, MUTYPE_SINGLE, 0 }, | |
| 414 | 475 | { "verbatim", MARKUP_VERBATIM, MUTYPE_SPECIAL, |
| 415 | 476 | AMSK_ID|AMSK_TYPE }, |
| 416 | 477 | }; |
| 417 | 478 | |
| 418 | 479 | void show_allowed_wiki_markup( void ){ |
| @@ -816,23 +877,34 @@ | ||
| 816 | 877 | unsigned char iACode; /* ATTR_* */ |
| 817 | 878 | char *zValue; /* Argument to this attribute. Might be NULL */ |
| 818 | 879 | char cTerm; /* Original argument termination character */ |
| 819 | 880 | } aAttr[10]; |
| 820 | 881 | }; |
| 882 | + | |
| 883 | +/* | |
| 884 | +** Return true if attribute iACode has already been parsed into p. | |
| 885 | +*/ | |
| 886 | +static int markupHasAttr(const ParsedMarkup *p, int iACode){ | |
| 887 | + int i; | |
| 888 | + for(i=0; i<p->nAttr; i++){ | |
| 889 | + if( p->aAttr[i].iACode==iACode ) return 1; | |
| 890 | + } | |
| 891 | + return 0; | |
| 892 | +} | |
| 821 | 893 | |
| 822 | 894 | /* |
| 823 | 895 | ** z[] is an HTML markup element - something that begins with '<'. |
| 824 | 896 | ** Parse this element into the p structure. |
| 825 | 897 | ** |
| 826 | 898 | ** The content of z[] might be modified by converting characters |
| 827 | 899 | ** to lowercase and by inserting some "\000" characters. |
| 828 | 900 | */ |
| 829 | -static int parseMarkup(ParsedMarkup *p, char *z){ | |
| 901 | +static amsk_t parseMarkup(ParsedMarkup *p, char *z){ | |
| 830 | 902 | int i, j, c; |
| 831 | 903 | int iACode; |
| 832 | 904 | char *zValue; |
| 833 | - int seen = 0; | |
| 905 | + amsk_t seen = 0; | |
| 834 | 906 | char zTag[100]; |
| 835 | 907 | |
| 836 | 908 | if( z[1]=='/' ){ |
| 837 | 909 | p->endTag = 1; |
| 838 | 910 | i = 2; |
| @@ -868,11 +940,11 @@ | ||
| 868 | 940 | if( j<(int)sizeof(zTag)-1 ) zTag[j++] = fossil_tolower(z[i]); |
| 869 | 941 | i++; |
| 870 | 942 | } |
| 871 | 943 | zTag[j] = 0; |
| 872 | 944 | p->aAttr[p->nAttr].iACode = iACode = findAttr(zTag); |
| 873 | - attrOk = iACode!=0 && (seen & aAttribute[iACode].iMask)==0; | |
| 945 | + attrOk = iACode!=0 && !markupHasAttr(p, iACode); | |
| 874 | 946 | while( fossil_isspace(z[i]) ){ z++; } |
| 875 | 947 | if( z[i]!='=' ){ |
| 876 | 948 | p->aAttr[p->nAttr].zValue = 0; |
| 877 | 949 | p->aAttr[p->nAttr].cTerm = 0; |
| 878 | 950 | c = 0; |
| @@ -904,11 +976,11 @@ | ||
| 904 | 976 | } |
| 905 | 977 | } |
| 906 | 978 | i++; |
| 907 | 979 | } |
| 908 | 980 | if( attrOk ){ |
| 909 | - seen |= aAttribute[iACode].iMask; | |
| 981 | + if( aAttribute[iACode].iMask ) seen |= aAttribute[iACode].iMask; | |
| 910 | 982 | p->nAttr++; |
| 911 | 983 | } |
| 912 | 984 | while( fossil_isspace(z[i]) ){ i++; } |
| 913 | 985 | if( z[i]==0 || z[i]=='>' || (z[i]=='/' && z[i+1]=='>') ) break; |
| 914 | 986 | } |
| @@ -1712,17 +1784,17 @@ | ||
| 1712 | 1784 | break; |
| 1713 | 1785 | } |
| 1714 | 1786 | case TOKEN_MARKUP: { |
| 1715 | 1787 | const char *zId; |
| 1716 | 1788 | int iDiv; |
| 1717 | - int mAttr = parseMarkup(&markup, z); | |
| 1789 | + (void)parseMarkup(&markup, z); | |
| 1718 | 1790 | |
| 1719 | 1791 | /* Convert <title> to <h1 align='center'> */ |
| 1720 | 1792 | if( markup.iCode==MARKUP_TITLE && !p->inVerbatim ){ |
| 1721 | 1793 | markup.iCode = MARKUP_H1; |
| 1722 | 1794 | markup.nAttr = 1; |
| 1723 | - markup.aAttr[0].iACode = AMSK_ALIGN; | |
| 1795 | + markup.aAttr[0].iACode = ATTR_ALIGN; | |
| 1724 | 1796 | markup.aAttr[0].zValue = "center"; |
| 1725 | 1797 | markup.aAttr[0].cTerm = 0; |
| 1726 | 1798 | } |
| 1727 | 1799 | |
| 1728 | 1800 | /* Markup of the form </div id=ID> where there is a matching |
| @@ -1801,11 +1873,11 @@ | ||
| 1801 | 1873 | popStackToTag(p, markup.iCode); |
| 1802 | 1874 | }else |
| 1803 | 1875 | |
| 1804 | 1876 | /* Push <div> markup onto the stack together with the id=ID attribute. |
| 1805 | 1877 | */ |
| 1806 | - if( markup.iCode==MARKUP_DIV && (mAttr & ATTR_ID)!=0 ){ | |
| 1878 | + if( markup.iCode==MARKUP_DIV && attributeValue(&markup, ATTR_ID)!=0 ){ | |
| 1807 | 1879 | pushStackWithId(p, markup.iCode, markupId(&markup), |
| 1808 | 1880 | (p->state & ALLOW_WIKI)!=0); |
| 1809 | 1881 | }else |
| 1810 | 1882 | |
| 1811 | 1883 | /* Enter <verbatim> processing. With verbatim enabled, all other |
| 1812 | 1884 |
| --- src/wikiformat.c | |
| +++ src/wikiformat.c | |
| @@ -68,25 +68,37 @@ | |
| 68 | ATTR_ALT, |
| 69 | ATTR_BGCOLOR, |
| 70 | ATTR_BORDER, |
| 71 | ATTR_CELLPADDING, |
| 72 | ATTR_CELLSPACING, |
| 73 | ATTR_CLASS, |
| 74 | ATTR_CLEAR, |
| 75 | ATTR_COLOR, |
| 76 | ATTR_COLSPAN, |
| 77 | ATTR_COMPACT, |
| 78 | ATTR_FACE, |
| 79 | ATTR_HEIGHT, |
| 80 | ATTR_HREF, |
| 81 | ATTR_HSPACE, |
| 82 | ATTR_ID, |
| 83 | ATTR_LINKS, |
| 84 | ATTR_NAME, |
| 85 | ATTR_ROWSPAN, |
| 86 | ATTR_SIZE, |
| 87 | ATTR_SRC, |
| 88 | ATTR_START, |
| 89 | ATTR_STYLE, |
| 90 | ATTR_TARGET, |
| 91 | ATTR_TITLE, |
| 92 | ATTR_TYPE, |
| @@ -94,46 +106,48 @@ | |
| 94 | ATTR_VALUE, |
| 95 | ATTR_VSPACE, |
| 96 | ATTR_WIDTH |
| 97 | }; |
| 98 | |
| 99 | enum amsk_t { |
| 100 | AMSK_ALIGN = 0x00000001, |
| 101 | AMSK_ALT = 0x00000002, |
| 102 | AMSK_BGCOLOR = 0x00000004, |
| 103 | AMSK_BORDER = 0x00000008, |
| 104 | AMSK_CELLPADDING = 0x00000010, |
| 105 | AMSK_CELLSPACING = 0x00000020, |
| 106 | AMSK_CLASS = 0x00000040, |
| 107 | AMSK_CLEAR = 0x00000080, |
| 108 | AMSK_COLOR = 0x00000100, |
| 109 | AMSK_COLSPAN = 0x00000200, |
| 110 | AMSK_COMPACT = 0x00000400, |
| 111 | AMSK_FACE = 0x00000800, |
| 112 | AMSK_HEIGHT = 0x00001000, |
| 113 | AMSK_HREF = 0x00002000, |
| 114 | AMSK_HSPACE = 0x00004000, |
| 115 | AMSK_ID = 0x00008000, |
| 116 | AMSK_LINKS = 0x00010000, |
| 117 | AMSK_NAME = 0x00020000, |
| 118 | AMSK_ROWSPAN = 0x00040000, |
| 119 | AMSK_SIZE = 0x00080000, |
| 120 | AMSK_SRC = 0x00100000, |
| 121 | AMSK_START = 0x00200000, |
| 122 | AMSK_STYLE = 0x00400000, |
| 123 | AMSK_TARGET = 0x00800000, |
| 124 | AMSK_TITLE = 0x01000000, |
| 125 | AMSK_TYPE = 0x02000000, |
| 126 | AMSK_VALIGN = 0x04000000, |
| 127 | AMSK_VALUE = 0x08000000, |
| 128 | AMSK_VSPACE = 0x10000000, |
| 129 | AMSK_WIDTH = 0x20000000 |
| 130 | }; |
| 131 | |
| 132 | static const struct AllowedAttribute { |
| 133 | const char *zName; |
| 134 | unsigned int iMask; |
| 135 | } aAttribute[] = { |
| 136 | /* These indexes MUST line up with their |
| 137 | corresponding allowed_attr_t enum values. |
| 138 | */ |
| 139 | { 0, 0 }, |
| @@ -141,25 +155,37 @@ | |
| 141 | { "alt", AMSK_ALT }, |
| 142 | { "bgcolor", AMSK_BGCOLOR }, |
| 143 | { "border", AMSK_BORDER }, |
| 144 | { "cellpadding", AMSK_CELLPADDING }, |
| 145 | { "cellspacing", AMSK_CELLSPACING }, |
| 146 | { "class", AMSK_CLASS }, |
| 147 | { "clear", AMSK_CLEAR }, |
| 148 | { "color", AMSK_COLOR }, |
| 149 | { "colspan", AMSK_COLSPAN }, |
| 150 | { "compact", AMSK_COMPACT }, |
| 151 | { "face", AMSK_FACE }, |
| 152 | { "height", AMSK_HEIGHT }, |
| 153 | { "href", AMSK_HREF }, |
| 154 | { "hspace", AMSK_HSPACE }, |
| 155 | { "id", AMSK_ID }, |
| 156 | { "links", AMSK_LINKS }, |
| 157 | { "name", AMSK_NAME }, |
| 158 | { "rowspan", AMSK_ROWSPAN }, |
| 159 | { "size", AMSK_SIZE }, |
| 160 | { "src", AMSK_SRC }, |
| 161 | { "start", AMSK_START }, |
| 162 | { "style", AMSK_STYLE }, |
| 163 | { "target", AMSK_TARGET }, |
| 164 | { "title", AMSK_TITLE }, |
| 165 | { "type", AMSK_TYPE }, |
| @@ -209,10 +235,11 @@ | |
| 209 | MARKUP_HTML5_ASIDE, |
| 210 | MARKUP_B, |
| 211 | MARKUP_BIG, |
| 212 | MARKUP_BLOCKQUOTE, |
| 213 | MARKUP_BR, |
| 214 | MARKUP_CENTER, |
| 215 | MARKUP_CITE, |
| 216 | MARKUP_CODE, |
| 217 | MARKUP_COL, |
| 218 | MARKUP_COLGROUP, |
| @@ -222,10 +249,12 @@ | |
| 222 | MARKUP_DFN, |
| 223 | MARKUP_DIV, |
| 224 | MARKUP_DL, |
| 225 | MARKUP_DT, |
| 226 | MARKUP_EM, |
| 227 | MARKUP_FONT, |
| 228 | MARKUP_HTML5_FOOTER, |
| 229 | MARKUP_H1, |
| 230 | MARKUP_H2, |
| 231 | MARKUP_H3, |
| @@ -236,21 +265,28 @@ | |
| 236 | MARKUP_HR, |
| 237 | MARKUP_I, |
| 238 | MARKUP_IMG, |
| 239 | MARKUP_INS, |
| 240 | MARKUP_KBD, |
| 241 | MARKUP_LI, |
| 242 | MARKUP_HTML5_NAV, |
| 243 | MARKUP_NOBR, |
| 244 | MARKUP_NOWIKI, |
| 245 | MARKUP_OL, |
| 246 | MARKUP_P, |
| 247 | MARKUP_PRE, |
| 248 | MARKUP_S, |
| 249 | MARKUP_SAMP, |
| 250 | MARKUP_HTML5_SECTION, |
| 251 | MARKUP_SMALL, |
| 252 | MARKUP_SPAN, |
| 253 | MARKUP_STRIKE, |
| 254 | MARKUP_STRONG, |
| 255 | MARKUP_SUB, |
| 256 | MARKUP_SUMMARY, |
| @@ -259,16 +295,18 @@ | |
| 259 | MARKUP_TBODY, |
| 260 | MARKUP_TD, |
| 261 | MARKUP_TFOOT, |
| 262 | MARKUP_TH, |
| 263 | MARKUP_THEAD, |
| 264 | MARKUP_TITLE, |
| 265 | MARKUP_TR, |
| 266 | MARKUP_TT, |
| 267 | MARKUP_U, |
| 268 | MARKUP_UL, |
| 269 | MARKUP_VAR, |
| 270 | MARKUP_VERBATIM |
| 271 | }; |
| 272 | |
| 273 | /* |
| 274 | ** The various markup is divided into the following types: |
| @@ -299,11 +337,11 @@ | |
| 299 | |
| 300 | static const struct AllowedMarkup { |
| 301 | const char *zName; /* Name of the markup */ |
| 302 | char iCode; /* The MARKUP_* code */ |
| 303 | short int iType; /* The MUTYPE_* code */ |
| 304 | int allowedAttr; /* Allowed attributes on this markup */ |
| 305 | } aMarkup[] = { |
| 306 | { 0, MARKUP_INVALID, 0, 0 }, |
| 307 | { "a", MARKUP_A, MUTYPE_HYPERLINK, |
| 308 | AMSK_HREF|AMSK_NAME|AMSK_CLASS|AMSK_TARGET|AMSK_STYLE| |
| 309 | AMSK_TITLE}, |
| @@ -316,10 +354,12 @@ | |
| 316 | AMSK_ID|AMSK_CLASS|AMSK_STYLE }, |
| 317 | { "b", MARKUP_B, MUTYPE_FONT, AMSK_STYLE }, |
| 318 | { "big", MARKUP_BIG, MUTYPE_FONT, AMSK_STYLE }, |
| 319 | { "blockquote", MARKUP_BLOCKQUOTE, MUTYPE_BLOCK, AMSK_STYLE }, |
| 320 | { "br", MARKUP_BR, MUTYPE_SINGLE, AMSK_CLEAR }, |
| 321 | { "center", MARKUP_CENTER, MUTYPE_BLOCK, AMSK_STYLE }, |
| 322 | { "cite", MARKUP_CITE, MUTYPE_FONT, AMSK_STYLE }, |
| 323 | { "code", MARKUP_CODE, MUTYPE_FONT, AMSK_STYLE }, |
| 324 | { "col", MARKUP_COL, MUTYPE_SINGLE, |
| 325 | AMSK_ALIGN|AMSK_CLASS|AMSK_COLSPAN|AMSK_WIDTH|AMSK_STYLE }, |
| @@ -326,18 +366,22 @@ | |
| 326 | { "colgroup", MARKUP_COLGROUP, MUTYPE_BLOCK, |
| 327 | AMSK_ALIGN|AMSK_CLASS|AMSK_COLSPAN|AMSK_WIDTH|AMSK_STYLE}, |
| 328 | { "dd", MARKUP_DD, MUTYPE_LI, AMSK_STYLE }, |
| 329 | { "del", MARKUP_DEL, MUTYPE_FONT, AMSK_STYLE }, |
| 330 | { "details", MARKUP_DETAILS, MUTYPE_BLOCK, |
| 331 | AMSK_ID|AMSK_CLASS|AMSK_STYLE }, |
| 332 | { "dfn", MARKUP_DFN, MUTYPE_FONT, AMSK_STYLE }, |
| 333 | { "div", MARKUP_DIV, MUTYPE_BLOCK, |
| 334 | AMSK_ID|AMSK_CLASS|AMSK_STYLE }, |
| 335 | { "dl", MARKUP_DL, MUTYPE_LIST, |
| 336 | AMSK_COMPACT|AMSK_STYLE }, |
| 337 | { "dt", MARKUP_DT, MUTYPE_LI, AMSK_STYLE }, |
| 338 | { "em", MARKUP_EM, MUTYPE_FONT, AMSK_STYLE }, |
| 339 | { "font", MARKUP_FONT, MUTYPE_FONT, |
| 340 | AMSK_COLOR|AMSK_FACE|AMSK_SIZE|AMSK_STYLE }, |
| 341 | { "footer", MARKUP_HTML5_FOOTER, MUTYPE_BLOCK, |
| 342 | AMSK_ID|AMSK_CLASS|AMSK_STYLE }, |
| 343 | { "h1", MARKUP_H1, MUTYPE_BLOCK, |
| @@ -358,29 +402,43 @@ | |
| 358 | AMSK_ALIGN|AMSK_COLOR|AMSK_SIZE|AMSK_WIDTH| |
| 359 | AMSK_STYLE|AMSK_CLASS }, |
| 360 | { "i", MARKUP_I, MUTYPE_FONT, AMSK_STYLE }, |
| 361 | { "img", MARKUP_IMG, MUTYPE_SINGLE, |
| 362 | AMSK_ALIGN|AMSK_ALT|AMSK_BORDER|AMSK_HEIGHT| |
| 363 | AMSK_HSPACE|AMSK_SRC|AMSK_VSPACE|AMSK_WIDTH|AMSK_STYLE }, |
| 364 | { "ins", MARKUP_INS, MUTYPE_FONT, AMSK_STYLE }, |
| 365 | { "kbd", MARKUP_KBD, MUTYPE_FONT, AMSK_STYLE }, |
| 366 | { "li", MARKUP_LI, MUTYPE_LI, |
| 367 | AMSK_TYPE|AMSK_VALUE|AMSK_STYLE }, |
| 368 | { "nav", MARKUP_HTML5_NAV, MUTYPE_BLOCK, |
| 369 | AMSK_ID|AMSK_CLASS|AMSK_STYLE }, |
| 370 | { "nobr", MARKUP_NOBR, MUTYPE_FONT, 0 }, |
| 371 | { "nowiki", MARKUP_NOWIKI, MUTYPE_SPECIAL, 0 }, |
| 372 | { "ol", MARKUP_OL, MUTYPE_LIST, |
| 373 | AMSK_START|AMSK_TYPE|AMSK_COMPACT|AMSK_STYLE }, |
| 374 | { "p", MARKUP_P, MUTYPE_BLOCK, |
| 375 | AMSK_ALIGN|AMSK_CLASS|AMSK_STYLE }, |
| 376 | { "pre", MARKUP_PRE, MUTYPE_BLOCK, AMSK_STYLE }, |
| 377 | { "s", MARKUP_S, MUTYPE_FONT, AMSK_STYLE }, |
| 378 | { "samp", MARKUP_SAMP, MUTYPE_FONT, AMSK_STYLE }, |
| 379 | { "section", MARKUP_HTML5_SECTION, MUTYPE_BLOCK, |
| 380 | AMSK_ID|AMSK_CLASS|AMSK_STYLE }, |
| 381 | { "small", MARKUP_SMALL, MUTYPE_FONT, AMSK_STYLE }, |
| 382 | { "span", MARKUP_SPAN, MUTYPE_BLOCK, |
| 383 | AMSK_ALIGN|AMSK_CLASS|AMSK_STYLE }, |
| 384 | { "strike", MARKUP_STRIKE, MUTYPE_FONT, AMSK_STYLE }, |
| 385 | { "strong", MARKUP_STRONG, MUTYPE_FONT, AMSK_STYLE }, |
| 386 | { "sub", MARKUP_SUB, MUTYPE_FONT, AMSK_STYLE }, |
| @@ -401,18 +459,21 @@ | |
| 401 | { "th", MARKUP_TH, MUTYPE_TD, |
| 402 | AMSK_ALIGN|AMSK_BGCOLOR|AMSK_COLSPAN| |
| 403 | AMSK_ROWSPAN|AMSK_VALIGN|AMSK_CLASS|AMSK_STYLE }, |
| 404 | { "thead", MARKUP_THEAD, MUTYPE_BLOCK, |
| 405 | AMSK_ALIGN|AMSK_CLASS|AMSK_STYLE }, |
| 406 | { "title", MARKUP_TITLE, MUTYPE_BLOCK, 0 }, |
| 407 | { "tr", MARKUP_TR, MUTYPE_TR, |
| 408 | AMSK_ALIGN|AMSK_BGCOLOR|AMSK_VALIGN|AMSK_CLASS|AMSK_STYLE }, |
| 409 | { "tt", MARKUP_TT, MUTYPE_FONT, AMSK_STYLE }, |
| 410 | { "u", MARKUP_U, MUTYPE_FONT, AMSK_STYLE }, |
| 411 | { "ul", MARKUP_UL, MUTYPE_LIST, |
| 412 | AMSK_TYPE|AMSK_COMPACT|AMSK_STYLE }, |
| 413 | { "var", MARKUP_VAR, MUTYPE_FONT, AMSK_STYLE }, |
| 414 | { "verbatim", MARKUP_VERBATIM, MUTYPE_SPECIAL, |
| 415 | AMSK_ID|AMSK_TYPE }, |
| 416 | }; |
| 417 | |
| 418 | void show_allowed_wiki_markup( void ){ |
| @@ -816,23 +877,34 @@ | |
| 816 | unsigned char iACode; /* ATTR_* */ |
| 817 | char *zValue; /* Argument to this attribute. Might be NULL */ |
| 818 | char cTerm; /* Original argument termination character */ |
| 819 | } aAttr[10]; |
| 820 | }; |
| 821 | |
| 822 | /* |
| 823 | ** z[] is an HTML markup element - something that begins with '<'. |
| 824 | ** Parse this element into the p structure. |
| 825 | ** |
| 826 | ** The content of z[] might be modified by converting characters |
| 827 | ** to lowercase and by inserting some "\000" characters. |
| 828 | */ |
| 829 | static int parseMarkup(ParsedMarkup *p, char *z){ |
| 830 | int i, j, c; |
| 831 | int iACode; |
| 832 | char *zValue; |
| 833 | int seen = 0; |
| 834 | char zTag[100]; |
| 835 | |
| 836 | if( z[1]=='/' ){ |
| 837 | p->endTag = 1; |
| 838 | i = 2; |
| @@ -868,11 +940,11 @@ | |
| 868 | if( j<(int)sizeof(zTag)-1 ) zTag[j++] = fossil_tolower(z[i]); |
| 869 | i++; |
| 870 | } |
| 871 | zTag[j] = 0; |
| 872 | p->aAttr[p->nAttr].iACode = iACode = findAttr(zTag); |
| 873 | attrOk = iACode!=0 && (seen & aAttribute[iACode].iMask)==0; |
| 874 | while( fossil_isspace(z[i]) ){ z++; } |
| 875 | if( z[i]!='=' ){ |
| 876 | p->aAttr[p->nAttr].zValue = 0; |
| 877 | p->aAttr[p->nAttr].cTerm = 0; |
| 878 | c = 0; |
| @@ -904,11 +976,11 @@ | |
| 904 | } |
| 905 | } |
| 906 | i++; |
| 907 | } |
| 908 | if( attrOk ){ |
| 909 | seen |= aAttribute[iACode].iMask; |
| 910 | p->nAttr++; |
| 911 | } |
| 912 | while( fossil_isspace(z[i]) ){ i++; } |
| 913 | if( z[i]==0 || z[i]=='>' || (z[i]=='/' && z[i+1]=='>') ) break; |
| 914 | } |
| @@ -1712,17 +1784,17 @@ | |
| 1712 | break; |
| 1713 | } |
| 1714 | case TOKEN_MARKUP: { |
| 1715 | const char *zId; |
| 1716 | int iDiv; |
| 1717 | int mAttr = parseMarkup(&markup, z); |
| 1718 | |
| 1719 | /* Convert <title> to <h1 align='center'> */ |
| 1720 | if( markup.iCode==MARKUP_TITLE && !p->inVerbatim ){ |
| 1721 | markup.iCode = MARKUP_H1; |
| 1722 | markup.nAttr = 1; |
| 1723 | markup.aAttr[0].iACode = AMSK_ALIGN; |
| 1724 | markup.aAttr[0].zValue = "center"; |
| 1725 | markup.aAttr[0].cTerm = 0; |
| 1726 | } |
| 1727 | |
| 1728 | /* Markup of the form </div id=ID> where there is a matching |
| @@ -1801,11 +1873,11 @@ | |
| 1801 | popStackToTag(p, markup.iCode); |
| 1802 | }else |
| 1803 | |
| 1804 | /* Push <div> markup onto the stack together with the id=ID attribute. |
| 1805 | */ |
| 1806 | if( markup.iCode==MARKUP_DIV && (mAttr & ATTR_ID)!=0 ){ |
| 1807 | pushStackWithId(p, markup.iCode, markupId(&markup), |
| 1808 | (p->state & ALLOW_WIKI)!=0); |
| 1809 | }else |
| 1810 | |
| 1811 | /* Enter <verbatim> processing. With verbatim enabled, all other |
| 1812 |
| --- src/wikiformat.c | |
| +++ src/wikiformat.c | |
| @@ -68,25 +68,37 @@ | |
| 68 | ATTR_ALT, |
| 69 | ATTR_BGCOLOR, |
| 70 | ATTR_BORDER, |
| 71 | ATTR_CELLPADDING, |
| 72 | ATTR_CELLSPACING, |
| 73 | ATTR_CITE, |
| 74 | ATTR_CLASS, |
| 75 | ATTR_CLEAR, |
| 76 | ATTR_COLOR, |
| 77 | ATTR_COLSPAN, |
| 78 | ATTR_COMPACT, |
| 79 | ATTR_DATETIME, |
| 80 | ATTR_FACE, |
| 81 | ATTR_FOR, |
| 82 | ATTR_HEIGHT, |
| 83 | ATTR_HIGH, |
| 84 | ATTR_HREF, |
| 85 | ATTR_HSPACE, |
| 86 | ATTR_ID, |
| 87 | ATTR_LINKS, |
| 88 | ATTR_LOW, |
| 89 | ATTR_MAX, |
| 90 | ATTR_MEDIA, |
| 91 | ATTR_MIN, |
| 92 | ATTR_NAME, |
| 93 | ATTR_OPEN, |
| 94 | ATTR_OPTIMUM, |
| 95 | ATTR_ROWSPAN, |
| 96 | ATTR_SIZE, |
| 97 | ATTR_SIZES, |
| 98 | ATTR_SRC, |
| 99 | ATTR_SRCSET, |
| 100 | ATTR_START, |
| 101 | ATTR_STYLE, |
| 102 | ATTR_TARGET, |
| 103 | ATTR_TITLE, |
| 104 | ATTR_TYPE, |
| @@ -94,46 +106,48 @@ | |
| 106 | ATTR_VALUE, |
| 107 | ATTR_VSPACE, |
| 108 | ATTR_WIDTH |
| 109 | }; |
| 110 | |
| 111 | typedef uint64_t amsk_t; |
| 112 | #define AMSK_ALIGN ((amsk_t)1 << 0) |
| 113 | #define AMSK_ALT ((amsk_t)1 << 1) |
| 114 | #define AMSK_BGCOLOR ((amsk_t)1 << 2) |
| 115 | #define AMSK_BORDER ((amsk_t)1 << 3) |
| 116 | #define AMSK_CELLPADDING ((amsk_t)1 << 4) |
| 117 | #define AMSK_CELLSPACING ((amsk_t)1 << 5) |
| 118 | #define AMSK_CLASS ((amsk_t)1 << 6) |
| 119 | #define AMSK_CLEAR ((amsk_t)1 << 7) |
| 120 | #define AMSK_COLOR ((amsk_t)1 << 8) |
| 121 | #define AMSK_COLSPAN ((amsk_t)1 << 9) |
| 122 | #define AMSK_COMPACT ((amsk_t)1 << 10) |
| 123 | #define AMSK_FACE ((amsk_t)1 << 11) |
| 124 | #define AMSK_HEIGHT ((amsk_t)1 << 12) |
| 125 | #define AMSK_HREF ((amsk_t)1 << 13) |
| 126 | #define AMSK_HSPACE ((amsk_t)1 << 14) |
| 127 | #define AMSK_ID ((amsk_t)1 << 15) |
| 128 | #define AMSK_LINKS ((amsk_t)1 << 16) |
| 129 | #define AMSK_NAME ((amsk_t)1 << 17) |
| 130 | #define AMSK_OPEN ((amsk_t)1 << 18) |
| 131 | #define AMSK_ROWSPAN ((amsk_t)1 << 19) |
| 132 | #define AMSK_SIZE ((amsk_t)1 << 20) |
| 133 | #define AMSK_SRC ((amsk_t)1 << 21) |
| 134 | #define AMSK_START ((amsk_t)1 << 22) |
| 135 | #define AMSK_STYLE ((amsk_t)1 << 23) |
| 136 | #define AMSK_TARGET ((amsk_t)1 << 24) |
| 137 | #define AMSK_TITLE ((amsk_t)1 << 25) |
| 138 | #define AMSK_TYPE ((amsk_t)1 << 26) |
| 139 | #define AMSK_VALIGN ((amsk_t)1 << 27) |
| 140 | #define AMSK_VALUE ((amsk_t)1 << 28) |
| 141 | #define AMSK_VSPACE ((amsk_t)1 << 29) |
| 142 | #define AMSK_WIDTH ((amsk_t)1 << 30) |
| 143 | #define AMSK_CITE ((amsk_t)1 << 31) |
| 144 | #define AMSK_DATETIME ((amsk_t)1 << 32) |
| 145 | |
| 146 | static const struct AllowedAttribute { |
| 147 | const char *zName; |
| 148 | amsk_t iMask; |
| 149 | } aAttribute[] = { |
| 150 | /* These indexes MUST line up with their |
| 151 | corresponding allowed_attr_t enum values. |
| 152 | */ |
| 153 | { 0, 0 }, |
| @@ -141,25 +155,37 @@ | |
| 155 | { "alt", AMSK_ALT }, |
| 156 | { "bgcolor", AMSK_BGCOLOR }, |
| 157 | { "border", AMSK_BORDER }, |
| 158 | { "cellpadding", AMSK_CELLPADDING }, |
| 159 | { "cellspacing", AMSK_CELLSPACING }, |
| 160 | { "cite", AMSK_CITE }, |
| 161 | { "class", AMSK_CLASS }, |
| 162 | { "clear", AMSK_CLEAR }, |
| 163 | { "color", AMSK_COLOR }, |
| 164 | { "colspan", AMSK_COLSPAN }, |
| 165 | { "compact", AMSK_COMPACT }, |
| 166 | { "datetime", AMSK_DATETIME }, |
| 167 | { "face", AMSK_FACE }, |
| 168 | { "for", 0 }, |
| 169 | { "height", AMSK_HEIGHT }, |
| 170 | { "high", 0 }, |
| 171 | { "href", AMSK_HREF }, |
| 172 | { "hspace", AMSK_HSPACE }, |
| 173 | { "id", AMSK_ID }, |
| 174 | { "links", AMSK_LINKS }, |
| 175 | { "low", 0 }, |
| 176 | { "max", 0 }, |
| 177 | { "media", 0 }, |
| 178 | { "min", 0 }, |
| 179 | { "name", AMSK_NAME }, |
| 180 | { "open", AMSK_OPEN }, |
| 181 | { "optimum", 0 }, |
| 182 | { "rowspan", AMSK_ROWSPAN }, |
| 183 | { "size", AMSK_SIZE }, |
| 184 | { "sizes", 0 }, |
| 185 | { "src", AMSK_SRC }, |
| 186 | { "srcset", 0 }, |
| 187 | { "start", AMSK_START }, |
| 188 | { "style", AMSK_STYLE }, |
| 189 | { "target", AMSK_TARGET }, |
| 190 | { "title", AMSK_TITLE }, |
| 191 | { "type", AMSK_TYPE }, |
| @@ -209,10 +235,11 @@ | |
| 235 | MARKUP_HTML5_ASIDE, |
| 236 | MARKUP_B, |
| 237 | MARKUP_BIG, |
| 238 | MARKUP_BLOCKQUOTE, |
| 239 | MARKUP_BR, |
| 240 | MARKUP_CAPTION, |
| 241 | MARKUP_CENTER, |
| 242 | MARKUP_CITE, |
| 243 | MARKUP_CODE, |
| 244 | MARKUP_COL, |
| 245 | MARKUP_COLGROUP, |
| @@ -222,10 +249,12 @@ | |
| 249 | MARKUP_DFN, |
| 250 | MARKUP_DIV, |
| 251 | MARKUP_DL, |
| 252 | MARKUP_DT, |
| 253 | MARKUP_EM, |
| 254 | MARKUP_FIGCAPTION, |
| 255 | MARKUP_FIGURE, |
| 256 | MARKUP_FONT, |
| 257 | MARKUP_HTML5_FOOTER, |
| 258 | MARKUP_H1, |
| 259 | MARKUP_H2, |
| 260 | MARKUP_H3, |
| @@ -236,21 +265,28 @@ | |
| 265 | MARKUP_HR, |
| 266 | MARKUP_I, |
| 267 | MARKUP_IMG, |
| 268 | MARKUP_INS, |
| 269 | MARKUP_KBD, |
| 270 | MARKUP_LABEL, |
| 271 | MARKUP_LI, |
| 272 | MARKUP_MARK, |
| 273 | MARKUP_METER, |
| 274 | MARKUP_HTML5_NAV, |
| 275 | MARKUP_NOBR, |
| 276 | MARKUP_NOWIKI, |
| 277 | MARKUP_OL, |
| 278 | MARKUP_P, |
| 279 | MARKUP_PICTURE, |
| 280 | MARKUP_PRE, |
| 281 | MARKUP_PROGRESS, |
| 282 | MARKUP_Q, |
| 283 | MARKUP_S, |
| 284 | MARKUP_SAMP, |
| 285 | MARKUP_HTML5_SECTION, |
| 286 | MARKUP_SMALL, |
| 287 | MARKUP_SOURCE, |
| 288 | MARKUP_SPAN, |
| 289 | MARKUP_STRIKE, |
| 290 | MARKUP_STRONG, |
| 291 | MARKUP_SUB, |
| 292 | MARKUP_SUMMARY, |
| @@ -259,16 +295,18 @@ | |
| 295 | MARKUP_TBODY, |
| 296 | MARKUP_TD, |
| 297 | MARKUP_TFOOT, |
| 298 | MARKUP_TH, |
| 299 | MARKUP_THEAD, |
| 300 | MARKUP_TIME, |
| 301 | MARKUP_TITLE, |
| 302 | MARKUP_TR, |
| 303 | MARKUP_TT, |
| 304 | MARKUP_U, |
| 305 | MARKUP_UL, |
| 306 | MARKUP_VAR, |
| 307 | MARKUP_WBR, |
| 308 | MARKUP_VERBATIM |
| 309 | }; |
| 310 | |
| 311 | /* |
| 312 | ** The various markup is divided into the following types: |
| @@ -299,11 +337,11 @@ | |
| 337 | |
| 338 | static const struct AllowedMarkup { |
| 339 | const char *zName; /* Name of the markup */ |
| 340 | char iCode; /* The MARKUP_* code */ |
| 341 | short int iType; /* The MUTYPE_* code */ |
| 342 | amsk_t allowedAttr; /* Allowed attributes on this markup */ |
| 343 | } aMarkup[] = { |
| 344 | { 0, MARKUP_INVALID, 0, 0 }, |
| 345 | { "a", MARKUP_A, MUTYPE_HYPERLINK, |
| 346 | AMSK_HREF|AMSK_NAME|AMSK_CLASS|AMSK_TARGET|AMSK_STYLE| |
| 347 | AMSK_TITLE}, |
| @@ -316,10 +354,12 @@ | |
| 354 | AMSK_ID|AMSK_CLASS|AMSK_STYLE }, |
| 355 | { "b", MARKUP_B, MUTYPE_FONT, AMSK_STYLE }, |
| 356 | { "big", MARKUP_BIG, MUTYPE_FONT, AMSK_STYLE }, |
| 357 | { "blockquote", MARKUP_BLOCKQUOTE, MUTYPE_BLOCK, AMSK_STYLE }, |
| 358 | { "br", MARKUP_BR, MUTYPE_SINGLE, AMSK_CLEAR }, |
| 359 | { "caption", MARKUP_CAPTION, MUTYPE_BLOCK, |
| 360 | AMSK_ID|AMSK_CLASS|AMSK_STYLE }, |
| 361 | { "center", MARKUP_CENTER, MUTYPE_BLOCK, AMSK_STYLE }, |
| 362 | { "cite", MARKUP_CITE, MUTYPE_FONT, AMSK_STYLE }, |
| 363 | { "code", MARKUP_CODE, MUTYPE_FONT, AMSK_STYLE }, |
| 364 | { "col", MARKUP_COL, MUTYPE_SINGLE, |
| 365 | AMSK_ALIGN|AMSK_CLASS|AMSK_COLSPAN|AMSK_WIDTH|AMSK_STYLE }, |
| @@ -326,18 +366,22 @@ | |
| 366 | { "colgroup", MARKUP_COLGROUP, MUTYPE_BLOCK, |
| 367 | AMSK_ALIGN|AMSK_CLASS|AMSK_COLSPAN|AMSK_WIDTH|AMSK_STYLE}, |
| 368 | { "dd", MARKUP_DD, MUTYPE_LI, AMSK_STYLE }, |
| 369 | { "del", MARKUP_DEL, MUTYPE_FONT, AMSK_STYLE }, |
| 370 | { "details", MARKUP_DETAILS, MUTYPE_BLOCK, |
| 371 | AMSK_ID|AMSK_CLASS|AMSK_STYLE|AMSK_OPEN }, |
| 372 | { "dfn", MARKUP_DFN, MUTYPE_FONT, AMSK_STYLE }, |
| 373 | { "div", MARKUP_DIV, MUTYPE_BLOCK, |
| 374 | AMSK_ID|AMSK_CLASS|AMSK_STYLE }, |
| 375 | { "dl", MARKUP_DL, MUTYPE_LIST, |
| 376 | AMSK_COMPACT|AMSK_STYLE }, |
| 377 | { "dt", MARKUP_DT, MUTYPE_LI, AMSK_STYLE }, |
| 378 | { "em", MARKUP_EM, MUTYPE_FONT, AMSK_STYLE }, |
| 379 | { "figcaption", MARKUP_FIGCAPTION, MUTYPE_BLOCK, |
| 380 | AMSK_ID|AMSK_CLASS|AMSK_STYLE }, |
| 381 | { "figure", MARKUP_FIGURE, MUTYPE_BLOCK, |
| 382 | AMSK_ID|AMSK_CLASS|AMSK_STYLE }, |
| 383 | { "font", MARKUP_FONT, MUTYPE_FONT, |
| 384 | AMSK_COLOR|AMSK_FACE|AMSK_SIZE|AMSK_STYLE }, |
| 385 | { "footer", MARKUP_HTML5_FOOTER, MUTYPE_BLOCK, |
| 386 | AMSK_ID|AMSK_CLASS|AMSK_STYLE }, |
| 387 | { "h1", MARKUP_H1, MUTYPE_BLOCK, |
| @@ -358,29 +402,43 @@ | |
| 402 | AMSK_ALIGN|AMSK_COLOR|AMSK_SIZE|AMSK_WIDTH| |
| 403 | AMSK_STYLE|AMSK_CLASS }, |
| 404 | { "i", MARKUP_I, MUTYPE_FONT, AMSK_STYLE }, |
| 405 | { "img", MARKUP_IMG, MUTYPE_SINGLE, |
| 406 | AMSK_ALIGN|AMSK_ALT|AMSK_BORDER|AMSK_HEIGHT| |
| 407 | AMSK_HSPACE|AMSK_SRC|AMSK_VSPACE|AMSK_WIDTH|AMSK_STYLE }, |
| 408 | { "ins", MARKUP_INS, MUTYPE_FONT, AMSK_STYLE }, |
| 409 | { "kbd", MARKUP_KBD, MUTYPE_FONT, AMSK_STYLE }, |
| 410 | { "label", MARKUP_LABEL, MUTYPE_FONT, |
| 411 | AMSK_ID|AMSK_CLASS|AMSK_STYLE }, |
| 412 | { "li", MARKUP_LI, MUTYPE_LI, |
| 413 | AMSK_TYPE|AMSK_VALUE|AMSK_STYLE }, |
| 414 | { "mark", MARKUP_MARK, MUTYPE_FONT, |
| 415 | AMSK_ID|AMSK_CLASS|AMSK_STYLE }, |
| 416 | { "meter", MARKUP_METER, MUTYPE_FONT, |
| 417 | AMSK_ID|AMSK_CLASS|AMSK_STYLE|AMSK_VALUE }, |
| 418 | { "nav", MARKUP_HTML5_NAV, MUTYPE_BLOCK, |
| 419 | AMSK_ID|AMSK_CLASS|AMSK_STYLE }, |
| 420 | { "nobr", MARKUP_NOBR, MUTYPE_FONT, 0 }, |
| 421 | { "nowiki", MARKUP_NOWIKI, MUTYPE_SPECIAL, 0 }, |
| 422 | { "ol", MARKUP_OL, MUTYPE_LIST, |
| 423 | AMSK_START|AMSK_TYPE|AMSK_COMPACT|AMSK_STYLE }, |
| 424 | { "p", MARKUP_P, MUTYPE_BLOCK, |
| 425 | AMSK_ALIGN|AMSK_CLASS|AMSK_STYLE }, |
| 426 | { "picture", MARKUP_PICTURE, MUTYPE_BLOCK, |
| 427 | AMSK_ID|AMSK_CLASS|AMSK_STYLE }, |
| 428 | { "pre", MARKUP_PRE, MUTYPE_BLOCK, AMSK_STYLE }, |
| 429 | { "progress", MARKUP_PROGRESS, MUTYPE_FONT, |
| 430 | AMSK_ID|AMSK_CLASS|AMSK_STYLE|AMSK_VALUE }, |
| 431 | { "q", MARKUP_Q, MUTYPE_FONT, |
| 432 | AMSK_CLASS|AMSK_STYLE|AMSK_CITE }, |
| 433 | { "s", MARKUP_S, MUTYPE_FONT, AMSK_STYLE }, |
| 434 | { "samp", MARKUP_SAMP, MUTYPE_FONT, AMSK_STYLE }, |
| 435 | { "section", MARKUP_HTML5_SECTION, MUTYPE_BLOCK, |
| 436 | AMSK_ID|AMSK_CLASS|AMSK_STYLE }, |
| 437 | { "small", MARKUP_SMALL, MUTYPE_FONT, AMSK_STYLE }, |
| 438 | { "source", MARKUP_SOURCE, MUTYPE_SINGLE, |
| 439 | AMSK_TYPE|AMSK_ID|AMSK_CLASS|AMSK_STYLE }, |
| 440 | { "span", MARKUP_SPAN, MUTYPE_BLOCK, |
| 441 | AMSK_ALIGN|AMSK_CLASS|AMSK_STYLE }, |
| 442 | { "strike", MARKUP_STRIKE, MUTYPE_FONT, AMSK_STYLE }, |
| 443 | { "strong", MARKUP_STRONG, MUTYPE_FONT, AMSK_STYLE }, |
| 444 | { "sub", MARKUP_SUB, MUTYPE_FONT, AMSK_STYLE }, |
| @@ -401,18 +459,21 @@ | |
| 459 | { "th", MARKUP_TH, MUTYPE_TD, |
| 460 | AMSK_ALIGN|AMSK_BGCOLOR|AMSK_COLSPAN| |
| 461 | AMSK_ROWSPAN|AMSK_VALIGN|AMSK_CLASS|AMSK_STYLE }, |
| 462 | { "thead", MARKUP_THEAD, MUTYPE_BLOCK, |
| 463 | AMSK_ALIGN|AMSK_CLASS|AMSK_STYLE }, |
| 464 | { "time", MARKUP_TIME, MUTYPE_FONT, |
| 465 | AMSK_DATETIME|AMSK_ID|AMSK_CLASS|AMSK_STYLE|AMSK_TITLE }, |
| 466 | { "title", MARKUP_TITLE, MUTYPE_BLOCK, 0 }, |
| 467 | { "tr", MARKUP_TR, MUTYPE_TR, |
| 468 | AMSK_ALIGN|AMSK_BGCOLOR|AMSK_VALIGN|AMSK_CLASS|AMSK_STYLE }, |
| 469 | { "tt", MARKUP_TT, MUTYPE_FONT, AMSK_STYLE }, |
| 470 | { "u", MARKUP_U, MUTYPE_FONT, AMSK_STYLE }, |
| 471 | { "ul", MARKUP_UL, MUTYPE_LIST, |
| 472 | AMSK_TYPE|AMSK_COMPACT|AMSK_STYLE }, |
| 473 | { "var", MARKUP_VAR, MUTYPE_FONT, AMSK_STYLE }, |
| 474 | { "wbr", MARKUP_WBR, MUTYPE_SINGLE, 0 }, |
| 475 | { "verbatim", MARKUP_VERBATIM, MUTYPE_SPECIAL, |
| 476 | AMSK_ID|AMSK_TYPE }, |
| 477 | }; |
| 478 | |
| 479 | void show_allowed_wiki_markup( void ){ |
| @@ -816,23 +877,34 @@ | |
| 877 | unsigned char iACode; /* ATTR_* */ |
| 878 | char *zValue; /* Argument to this attribute. Might be NULL */ |
| 879 | char cTerm; /* Original argument termination character */ |
| 880 | } aAttr[10]; |
| 881 | }; |
| 882 | |
| 883 | /* |
| 884 | ** Return true if attribute iACode has already been parsed into p. |
| 885 | */ |
| 886 | static int markupHasAttr(const ParsedMarkup *p, int iACode){ |
| 887 | int i; |
| 888 | for(i=0; i<p->nAttr; i++){ |
| 889 | if( p->aAttr[i].iACode==iACode ) return 1; |
| 890 | } |
| 891 | return 0; |
| 892 | } |
| 893 | |
| 894 | /* |
| 895 | ** z[] is an HTML markup element - something that begins with '<'. |
| 896 | ** Parse this element into the p structure. |
| 897 | ** |
| 898 | ** The content of z[] might be modified by converting characters |
| 899 | ** to lowercase and by inserting some "\000" characters. |
| 900 | */ |
| 901 | static amsk_t parseMarkup(ParsedMarkup *p, char *z){ |
| 902 | int i, j, c; |
| 903 | int iACode; |
| 904 | char *zValue; |
| 905 | amsk_t seen = 0; |
| 906 | char zTag[100]; |
| 907 | |
| 908 | if( z[1]=='/' ){ |
| 909 | p->endTag = 1; |
| 910 | i = 2; |
| @@ -868,11 +940,11 @@ | |
| 940 | if( j<(int)sizeof(zTag)-1 ) zTag[j++] = fossil_tolower(z[i]); |
| 941 | i++; |
| 942 | } |
| 943 | zTag[j] = 0; |
| 944 | p->aAttr[p->nAttr].iACode = iACode = findAttr(zTag); |
| 945 | attrOk = iACode!=0 && !markupHasAttr(p, iACode); |
| 946 | while( fossil_isspace(z[i]) ){ z++; } |
| 947 | if( z[i]!='=' ){ |
| 948 | p->aAttr[p->nAttr].zValue = 0; |
| 949 | p->aAttr[p->nAttr].cTerm = 0; |
| 950 | c = 0; |
| @@ -904,11 +976,11 @@ | |
| 976 | } |
| 977 | } |
| 978 | i++; |
| 979 | } |
| 980 | if( attrOk ){ |
| 981 | if( aAttribute[iACode].iMask ) seen |= aAttribute[iACode].iMask; |
| 982 | p->nAttr++; |
| 983 | } |
| 984 | while( fossil_isspace(z[i]) ){ i++; } |
| 985 | if( z[i]==0 || z[i]=='>' || (z[i]=='/' && z[i+1]=='>') ) break; |
| 986 | } |
| @@ -1712,17 +1784,17 @@ | |
| 1784 | break; |
| 1785 | } |
| 1786 | case TOKEN_MARKUP: { |
| 1787 | const char *zId; |
| 1788 | int iDiv; |
| 1789 | (void)parseMarkup(&markup, z); |
| 1790 | |
| 1791 | /* Convert <title> to <h1 align='center'> */ |
| 1792 | if( markup.iCode==MARKUP_TITLE && !p->inVerbatim ){ |
| 1793 | markup.iCode = MARKUP_H1; |
| 1794 | markup.nAttr = 1; |
| 1795 | markup.aAttr[0].iACode = ATTR_ALIGN; |
| 1796 | markup.aAttr[0].zValue = "center"; |
| 1797 | markup.aAttr[0].cTerm = 0; |
| 1798 | } |
| 1799 | |
| 1800 | /* Markup of the form </div id=ID> where there is a matching |
| @@ -1801,11 +1873,11 @@ | |
| 1873 | popStackToTag(p, markup.iCode); |
| 1874 | }else |
| 1875 | |
| 1876 | /* Push <div> markup onto the stack together with the id=ID attribute. |
| 1877 | */ |
| 1878 | if( markup.iCode==MARKUP_DIV && attributeValue(&markup, ATTR_ID)!=0 ){ |
| 1879 | pushStackWithId(p, markup.iCode, markupId(&markup), |
| 1880 | (p->state & ALLOW_WIKI)!=0); |
| 1881 | }else |
| 1882 | |
| 1883 | /* Enter <verbatim> processing. With verbatim enabled, all other |
| 1884 |
+2
| --- win/Makefile.mingw | ||
| +++ win/Makefile.mingw | ||
| @@ -611,10 +611,11 @@ | ||
| 611 | 611 | $(SRCDIR)/copybtn.js \ |
| 612 | 612 | $(SRCDIR)/default.css \ |
| 613 | 613 | $(SRCDIR)/diff.js \ |
| 614 | 614 | $(SRCDIR)/diff.tcl \ |
| 615 | 615 | $(SRCDIR)/forum.js \ |
| 616 | + $(SRCDIR)/fossil.attach.js \ | |
| 616 | 617 | $(SRCDIR)/fossil.bootstrap.js \ |
| 617 | 618 | $(SRCDIR)/fossil.confirmer.js \ |
| 618 | 619 | $(SRCDIR)/fossil.copybutton.js \ |
| 619 | 620 | $(SRCDIR)/fossil.diff.js \ |
| 620 | 621 | $(SRCDIR)/fossil.dom.js \ |
| @@ -661,10 +662,11 @@ | ||
| 661 | 662 | $(SRCDIR)/sounds/e.wav \ |
| 662 | 663 | $(SRCDIR)/sounds/f.wav \ |
| 663 | 664 | $(SRCDIR)/style.admin_log.css \ |
| 664 | 665 | $(SRCDIR)/style.chat.css \ |
| 665 | 666 | $(SRCDIR)/style.fileedit.css \ |
| 667 | + $(SRCDIR)/style.forum.css \ | |
| 666 | 668 | $(SRCDIR)/style.pikchrshow.css \ |
| 667 | 669 | $(SRCDIR)/style.uvlist.css \ |
| 668 | 670 | $(SRCDIR)/style.wikiedit.css \ |
| 669 | 671 | $(SRCDIR)/tree.js \ |
| 670 | 672 | $(SRCDIR)/useredit.js \ |
| 671 | 673 |
| --- win/Makefile.mingw | |
| +++ win/Makefile.mingw | |
| @@ -611,10 +611,11 @@ | |
| 611 | $(SRCDIR)/copybtn.js \ |
| 612 | $(SRCDIR)/default.css \ |
| 613 | $(SRCDIR)/diff.js \ |
| 614 | $(SRCDIR)/diff.tcl \ |
| 615 | $(SRCDIR)/forum.js \ |
| 616 | $(SRCDIR)/fossil.bootstrap.js \ |
| 617 | $(SRCDIR)/fossil.confirmer.js \ |
| 618 | $(SRCDIR)/fossil.copybutton.js \ |
| 619 | $(SRCDIR)/fossil.diff.js \ |
| 620 | $(SRCDIR)/fossil.dom.js \ |
| @@ -661,10 +662,11 @@ | |
| 661 | $(SRCDIR)/sounds/e.wav \ |
| 662 | $(SRCDIR)/sounds/f.wav \ |
| 663 | $(SRCDIR)/style.admin_log.css \ |
| 664 | $(SRCDIR)/style.chat.css \ |
| 665 | $(SRCDIR)/style.fileedit.css \ |
| 666 | $(SRCDIR)/style.pikchrshow.css \ |
| 667 | $(SRCDIR)/style.uvlist.css \ |
| 668 | $(SRCDIR)/style.wikiedit.css \ |
| 669 | $(SRCDIR)/tree.js \ |
| 670 | $(SRCDIR)/useredit.js \ |
| 671 |
| --- win/Makefile.mingw | |
| +++ win/Makefile.mingw | |
| @@ -611,10 +611,11 @@ | |
| 611 | $(SRCDIR)/copybtn.js \ |
| 612 | $(SRCDIR)/default.css \ |
| 613 | $(SRCDIR)/diff.js \ |
| 614 | $(SRCDIR)/diff.tcl \ |
| 615 | $(SRCDIR)/forum.js \ |
| 616 | $(SRCDIR)/fossil.attach.js \ |
| 617 | $(SRCDIR)/fossil.bootstrap.js \ |
| 618 | $(SRCDIR)/fossil.confirmer.js \ |
| 619 | $(SRCDIR)/fossil.copybutton.js \ |
| 620 | $(SRCDIR)/fossil.diff.js \ |
| 621 | $(SRCDIR)/fossil.dom.js \ |
| @@ -661,10 +662,11 @@ | |
| 662 | $(SRCDIR)/sounds/e.wav \ |
| 663 | $(SRCDIR)/sounds/f.wav \ |
| 664 | $(SRCDIR)/style.admin_log.css \ |
| 665 | $(SRCDIR)/style.chat.css \ |
| 666 | $(SRCDIR)/style.fileedit.css \ |
| 667 | $(SRCDIR)/style.forum.css \ |
| 668 | $(SRCDIR)/style.pikchrshow.css \ |
| 669 | $(SRCDIR)/style.uvlist.css \ |
| 670 | $(SRCDIR)/style.wikiedit.css \ |
| 671 | $(SRCDIR)/tree.js \ |
| 672 | $(SRCDIR)/useredit.js \ |
| 673 |
+4
| --- win/Makefile.msc | ||
| +++ win/Makefile.msc | ||
| @@ -576,10 +576,11 @@ | ||
| 576 | 576 | "$(SRCDIR)\copybtn.js" \ |
| 577 | 577 | "$(SRCDIR)\default.css" \ |
| 578 | 578 | "$(SRCDIR)\diff.js" \ |
| 579 | 579 | "$(SRCDIR)\diff.tcl" \ |
| 580 | 580 | "$(SRCDIR)\forum.js" \ |
| 581 | + "$(SRCDIR)\fossil.attach.js" \ | |
| 581 | 582 | "$(SRCDIR)\fossil.bootstrap.js" \ |
| 582 | 583 | "$(SRCDIR)\fossil.confirmer.js" \ |
| 583 | 584 | "$(SRCDIR)\fossil.copybutton.js" \ |
| 584 | 585 | "$(SRCDIR)\fossil.diff.js" \ |
| 585 | 586 | "$(SRCDIR)\fossil.dom.js" \ |
| @@ -626,10 +627,11 @@ | ||
| 626 | 627 | "$(SRCDIR)\sounds\e.wav" \ |
| 627 | 628 | "$(SRCDIR)\sounds\f.wav" \ |
| 628 | 629 | "$(SRCDIR)\style.admin_log.css" \ |
| 629 | 630 | "$(SRCDIR)\style.chat.css" \ |
| 630 | 631 | "$(SRCDIR)\style.fileedit.css" \ |
| 632 | + "$(SRCDIR)\style.forum.css" \ | |
| 631 | 633 | "$(SRCDIR)\style.pikchrshow.css" \ |
| 632 | 634 | "$(SRCDIR)\style.uvlist.css" \ |
| 633 | 635 | "$(SRCDIR)\style.wikiedit.css" \ |
| 634 | 636 | "$(SRCDIR)\tree.js" \ |
| 635 | 637 | "$(SRCDIR)\useredit.js" \ |
| @@ -1214,10 +1216,11 @@ | ||
| 1214 | 1216 | echo "$(SRCDIR)\copybtn.js" >> $@ |
| 1215 | 1217 | echo "$(SRCDIR)\default.css" >> $@ |
| 1216 | 1218 | echo "$(SRCDIR)\diff.js" >> $@ |
| 1217 | 1219 | echo "$(SRCDIR)\diff.tcl" >> $@ |
| 1218 | 1220 | echo "$(SRCDIR)\forum.js" >> $@ |
| 1221 | + echo "$(SRCDIR)\fossil.attach.js" >> $@ | |
| 1219 | 1222 | echo "$(SRCDIR)\fossil.bootstrap.js" >> $@ |
| 1220 | 1223 | echo "$(SRCDIR)\fossil.confirmer.js" >> $@ |
| 1221 | 1224 | echo "$(SRCDIR)\fossil.copybutton.js" >> $@ |
| 1222 | 1225 | echo "$(SRCDIR)\fossil.diff.js" >> $@ |
| 1223 | 1226 | echo "$(SRCDIR)\fossil.dom.js" >> $@ |
| @@ -1264,10 +1267,11 @@ | ||
| 1264 | 1267 | echo "$(SRCDIR)\sounds/e.wav" >> $@ |
| 1265 | 1268 | echo "$(SRCDIR)\sounds/f.wav" >> $@ |
| 1266 | 1269 | echo "$(SRCDIR)\style.admin_log.css" >> $@ |
| 1267 | 1270 | echo "$(SRCDIR)\style.chat.css" >> $@ |
| 1268 | 1271 | echo "$(SRCDIR)\style.fileedit.css" >> $@ |
| 1272 | + echo "$(SRCDIR)\style.forum.css" >> $@ | |
| 1269 | 1273 | echo "$(SRCDIR)\style.pikchrshow.css" >> $@ |
| 1270 | 1274 | echo "$(SRCDIR)\style.uvlist.css" >> $@ |
| 1271 | 1275 | echo "$(SRCDIR)\style.wikiedit.css" >> $@ |
| 1272 | 1276 | echo "$(SRCDIR)\tree.js" >> $@ |
| 1273 | 1277 | echo "$(SRCDIR)\useredit.js" >> $@ |
| 1274 | 1278 |
| --- win/Makefile.msc | |
| +++ win/Makefile.msc | |
| @@ -576,10 +576,11 @@ | |
| 576 | "$(SRCDIR)\copybtn.js" \ |
| 577 | "$(SRCDIR)\default.css" \ |
| 578 | "$(SRCDIR)\diff.js" \ |
| 579 | "$(SRCDIR)\diff.tcl" \ |
| 580 | "$(SRCDIR)\forum.js" \ |
| 581 | "$(SRCDIR)\fossil.bootstrap.js" \ |
| 582 | "$(SRCDIR)\fossil.confirmer.js" \ |
| 583 | "$(SRCDIR)\fossil.copybutton.js" \ |
| 584 | "$(SRCDIR)\fossil.diff.js" \ |
| 585 | "$(SRCDIR)\fossil.dom.js" \ |
| @@ -626,10 +627,11 @@ | |
| 626 | "$(SRCDIR)\sounds\e.wav" \ |
| 627 | "$(SRCDIR)\sounds\f.wav" \ |
| 628 | "$(SRCDIR)\style.admin_log.css" \ |
| 629 | "$(SRCDIR)\style.chat.css" \ |
| 630 | "$(SRCDIR)\style.fileedit.css" \ |
| 631 | "$(SRCDIR)\style.pikchrshow.css" \ |
| 632 | "$(SRCDIR)\style.uvlist.css" \ |
| 633 | "$(SRCDIR)\style.wikiedit.css" \ |
| 634 | "$(SRCDIR)\tree.js" \ |
| 635 | "$(SRCDIR)\useredit.js" \ |
| @@ -1214,10 +1216,11 @@ | |
| 1214 | echo "$(SRCDIR)\copybtn.js" >> $@ |
| 1215 | echo "$(SRCDIR)\default.css" >> $@ |
| 1216 | echo "$(SRCDIR)\diff.js" >> $@ |
| 1217 | echo "$(SRCDIR)\diff.tcl" >> $@ |
| 1218 | echo "$(SRCDIR)\forum.js" >> $@ |
| 1219 | echo "$(SRCDIR)\fossil.bootstrap.js" >> $@ |
| 1220 | echo "$(SRCDIR)\fossil.confirmer.js" >> $@ |
| 1221 | echo "$(SRCDIR)\fossil.copybutton.js" >> $@ |
| 1222 | echo "$(SRCDIR)\fossil.diff.js" >> $@ |
| 1223 | echo "$(SRCDIR)\fossil.dom.js" >> $@ |
| @@ -1264,10 +1267,11 @@ | |
| 1264 | echo "$(SRCDIR)\sounds/e.wav" >> $@ |
| 1265 | echo "$(SRCDIR)\sounds/f.wav" >> $@ |
| 1266 | echo "$(SRCDIR)\style.admin_log.css" >> $@ |
| 1267 | echo "$(SRCDIR)\style.chat.css" >> $@ |
| 1268 | echo "$(SRCDIR)\style.fileedit.css" >> $@ |
| 1269 | echo "$(SRCDIR)\style.pikchrshow.css" >> $@ |
| 1270 | echo "$(SRCDIR)\style.uvlist.css" >> $@ |
| 1271 | echo "$(SRCDIR)\style.wikiedit.css" >> $@ |
| 1272 | echo "$(SRCDIR)\tree.js" >> $@ |
| 1273 | echo "$(SRCDIR)\useredit.js" >> $@ |
| 1274 |
| --- win/Makefile.msc | |
| +++ win/Makefile.msc | |
| @@ -576,10 +576,11 @@ | |
| 576 | "$(SRCDIR)\copybtn.js" \ |
| 577 | "$(SRCDIR)\default.css" \ |
| 578 | "$(SRCDIR)\diff.js" \ |
| 579 | "$(SRCDIR)\diff.tcl" \ |
| 580 | "$(SRCDIR)\forum.js" \ |
| 581 | "$(SRCDIR)\fossil.attach.js" \ |
| 582 | "$(SRCDIR)\fossil.bootstrap.js" \ |
| 583 | "$(SRCDIR)\fossil.confirmer.js" \ |
| 584 | "$(SRCDIR)\fossil.copybutton.js" \ |
| 585 | "$(SRCDIR)\fossil.diff.js" \ |
| 586 | "$(SRCDIR)\fossil.dom.js" \ |
| @@ -626,10 +627,11 @@ | |
| 627 | "$(SRCDIR)\sounds\e.wav" \ |
| 628 | "$(SRCDIR)\sounds\f.wav" \ |
| 629 | "$(SRCDIR)\style.admin_log.css" \ |
| 630 | "$(SRCDIR)\style.chat.css" \ |
| 631 | "$(SRCDIR)\style.fileedit.css" \ |
| 632 | "$(SRCDIR)\style.forum.css" \ |
| 633 | "$(SRCDIR)\style.pikchrshow.css" \ |
| 634 | "$(SRCDIR)\style.uvlist.css" \ |
| 635 | "$(SRCDIR)\style.wikiedit.css" \ |
| 636 | "$(SRCDIR)\tree.js" \ |
| 637 | "$(SRCDIR)\useredit.js" \ |
| @@ -1214,10 +1216,11 @@ | |
| 1216 | echo "$(SRCDIR)\copybtn.js" >> $@ |
| 1217 | echo "$(SRCDIR)\default.css" >> $@ |
| 1218 | echo "$(SRCDIR)\diff.js" >> $@ |
| 1219 | echo "$(SRCDIR)\diff.tcl" >> $@ |
| 1220 | echo "$(SRCDIR)\forum.js" >> $@ |
| 1221 | echo "$(SRCDIR)\fossil.attach.js" >> $@ |
| 1222 | echo "$(SRCDIR)\fossil.bootstrap.js" >> $@ |
| 1223 | echo "$(SRCDIR)\fossil.confirmer.js" >> $@ |
| 1224 | echo "$(SRCDIR)\fossil.copybutton.js" >> $@ |
| 1225 | echo "$(SRCDIR)\fossil.diff.js" >> $@ |
| 1226 | echo "$(SRCDIR)\fossil.dom.js" >> $@ |
| @@ -1264,10 +1267,11 @@ | |
| 1267 | echo "$(SRCDIR)\sounds/e.wav" >> $@ |
| 1268 | echo "$(SRCDIR)\sounds/f.wav" >> $@ |
| 1269 | echo "$(SRCDIR)\style.admin_log.css" >> $@ |
| 1270 | echo "$(SRCDIR)\style.chat.css" >> $@ |
| 1271 | echo "$(SRCDIR)\style.fileedit.css" >> $@ |
| 1272 | echo "$(SRCDIR)\style.forum.css" >> $@ |
| 1273 | echo "$(SRCDIR)\style.pikchrshow.css" >> $@ |
| 1274 | echo "$(SRCDIR)\style.uvlist.css" >> $@ |
| 1275 | echo "$(SRCDIR)\style.wikiedit.css" >> $@ |
| 1276 | echo "$(SRCDIR)\tree.js" >> $@ |
| 1277 | echo "$(SRCDIR)\useredit.js" >> $@ |
| 1278 |
+5
-2
| --- www/changes.wiki | ||
| +++ www/changes.wiki | ||
| @@ -5,24 +5,27 @@ | ||
| 5 | 5 | context of the check-in being edited. |
| 6 | 6 | <li> Honor the NO_COLOR environment variable in the |
| 7 | 7 | "[/help/system|fossil sys ls]" command. |
| 8 | 8 | <li> On the [/help/www/info|/info webpage] (and similar) put a |
| 9 | 9 | "copy" button before the date. |
| 10 | - <li> Add the ".m4a" and ".oft" mimetypes. | |
| 10 | + <li> Add the ".m4a" and ".otf" mimetypes. | |
| 11 | 11 | <li> When doing a "fossil update", if a file under management needs to |
| 12 | 12 | overwrite an unmanaged file, display the name of the backup that |
| 13 | 13 | is made of the unmanaged file, and use file_delete() to delete |
| 14 | 14 | the unmanaged file, even if that unmanaged file is read-only. |
| 15 | 15 | <li> Improve the default prompts used by the |
| 16 | 16 | "[/help/sqlite3|fossil sql]" command. |
| 17 | 17 | <li> The captcha now uses light-gray boxes as the background, instead of |
| 18 | 18 | spaces, to work around width inconsistencies in some fonts. |
| 19 | + <li> Forum post editing and replying was overhauled with a new UI. Clients | |
| 20 | + with JavaScript disabled will still see the older forms. | |
| 19 | 21 | <li> Forum posts may now have attachments if their poster has the new "B" |
| 20 | - capability.</li> | |
| 22 | + capability. | |
| 21 | 23 | <li> Add the "[/help/attachment-size-limit|attachment-size-limit]" setting |
| 22 | 24 | to limit the size of file attachments to wiki pages, tech notes, |
| 23 | 25 | tickets, and forum posts. |
| 26 | + <li> Addded several HTML5 tags deemed harmless to the MD/wiki allowlists. | |
| 24 | 27 | </ol> |
| 25 | 28 | |
| 26 | 29 | <h2 id='v2_28'>Changes for version 2.28 (2026-03-11)</h2><ol> |
| 27 | 30 | <li> Improvements to [./antibot.wiki|anti-robot defenses]:<ol type="a"> |
| 28 | 31 | <li> The default configuration now allows robots to download any tarball |
| 29 | 32 |
| --- www/changes.wiki | |
| +++ www/changes.wiki | |
| @@ -5,24 +5,27 @@ | |
| 5 | context of the check-in being edited. |
| 6 | <li> Honor the NO_COLOR environment variable in the |
| 7 | "[/help/system|fossil sys ls]" command. |
| 8 | <li> On the [/help/www/info|/info webpage] (and similar) put a |
| 9 | "copy" button before the date. |
| 10 | <li> Add the ".m4a" and ".oft" mimetypes. |
| 11 | <li> When doing a "fossil update", if a file under management needs to |
| 12 | overwrite an unmanaged file, display the name of the backup that |
| 13 | is made of the unmanaged file, and use file_delete() to delete |
| 14 | the unmanaged file, even if that unmanaged file is read-only. |
| 15 | <li> Improve the default prompts used by the |
| 16 | "[/help/sqlite3|fossil sql]" command. |
| 17 | <li> The captcha now uses light-gray boxes as the background, instead of |
| 18 | spaces, to work around width inconsistencies in some fonts. |
| 19 | <li> Forum posts may now have attachments if their poster has the new "B" |
| 20 | capability.</li> |
| 21 | <li> Add the "[/help/attachment-size-limit|attachment-size-limit]" setting |
| 22 | to limit the size of file attachments to wiki pages, tech notes, |
| 23 | tickets, and forum posts. |
| 24 | </ol> |
| 25 | |
| 26 | <h2 id='v2_28'>Changes for version 2.28 (2026-03-11)</h2><ol> |
| 27 | <li> Improvements to [./antibot.wiki|anti-robot defenses]:<ol type="a"> |
| 28 | <li> The default configuration now allows robots to download any tarball |
| 29 |
| --- www/changes.wiki | |
| +++ www/changes.wiki | |
| @@ -5,24 +5,27 @@ | |
| 5 | context of the check-in being edited. |
| 6 | <li> Honor the NO_COLOR environment variable in the |
| 7 | "[/help/system|fossil sys ls]" command. |
| 8 | <li> On the [/help/www/info|/info webpage] (and similar) put a |
| 9 | "copy" button before the date. |
| 10 | <li> Add the ".m4a" and ".otf" mimetypes. |
| 11 | <li> When doing a "fossil update", if a file under management needs to |
| 12 | overwrite an unmanaged file, display the name of the backup that |
| 13 | is made of the unmanaged file, and use file_delete() to delete |
| 14 | the unmanaged file, even if that unmanaged file is read-only. |
| 15 | <li> Improve the default prompts used by the |
| 16 | "[/help/sqlite3|fossil sql]" command. |
| 17 | <li> The captcha now uses light-gray boxes as the background, instead of |
| 18 | spaces, to work around width inconsistencies in some fonts. |
| 19 | <li> Forum post editing and replying was overhauled with a new UI. Clients |
| 20 | with JavaScript disabled will still see the older forms. |
| 21 | <li> Forum posts may now have attachments if their poster has the new "B" |
| 22 | capability. |
| 23 | <li> Add the "[/help/attachment-size-limit|attachment-size-limit]" setting |
| 24 | to limit the size of file attachments to wiki pages, tech notes, |
| 25 | tickets, and forum posts. |
| 26 | <li> Addded several HTML5 tags deemed harmless to the MD/wiki allowlists. |
| 27 | </ol> |
| 28 | |
| 29 | <h2 id='v2_28'>Changes for version 2.28 (2026-03-11)</h2><ol> |
| 30 | <li> Improvements to [./antibot.wiki|anti-robot defenses]:<ol type="a"> |
| 31 | <li> The default configuration now allows robots to download any tarball |
| 32 |
+2
-2
| --- www/customskin.md | ||
| +++ www/customskin.md | ||
| @@ -342,16 +342,16 @@ | ||
| 342 | 342 | section for more information on the two possible variable formats. |
| 343 | 343 | |
| 344 | 344 | For example, first few lines of a typical Skin Header will look |
| 345 | 345 | like this: |
| 346 | 346 | |
| 347 | - <div class="header"> | |
| 347 | + <header> | |
| 348 | 348 | <div class="title"><h1>$<project_name></h1>$<title>/div> |
| 349 | 349 | |
| 350 | 350 | After variables are substituted by TH1, that will look more like this: |
| 351 | 351 | |
| 352 | - <div class="header"> | |
| 352 | + <header> | |
| 353 | 353 | <div class="title"><h1>Project Name</h1>Page Title</div> |
| 354 | 354 | |
| 355 | 355 | As you can see, two TH1 variable substitutions were done. |
| 356 | 356 | |
| 357 | 357 | The same TH1 interpreter is used for both the header and the footer |
| 358 | 358 |
| --- www/customskin.md | |
| +++ www/customskin.md | |
| @@ -342,16 +342,16 @@ | |
| 342 | section for more information on the two possible variable formats. |
| 343 | |
| 344 | For example, first few lines of a typical Skin Header will look |
| 345 | like this: |
| 346 | |
| 347 | <div class="header"> |
| 348 | <div class="title"><h1>$<project_name></h1>$<title>/div> |
| 349 | |
| 350 | After variables are substituted by TH1, that will look more like this: |
| 351 | |
| 352 | <div class="header"> |
| 353 | <div class="title"><h1>Project Name</h1>Page Title</div> |
| 354 | |
| 355 | As you can see, two TH1 variable substitutions were done. |
| 356 | |
| 357 | The same TH1 interpreter is used for both the header and the footer |
| 358 |
| --- www/customskin.md | |
| +++ www/customskin.md | |
| @@ -342,16 +342,16 @@ | |
| 342 | section for more information on the two possible variable formats. |
| 343 | |
| 344 | For example, first few lines of a typical Skin Header will look |
| 345 | like this: |
| 346 | |
| 347 | <header> |
| 348 | <div class="title"><h1>$<project_name></h1>$<title>/div> |
| 349 | |
| 350 | After variables are substituted by TH1, that will look more like this: |
| 351 | |
| 352 | <header> |
| 353 | <div class="title"><h1>Project Name</h1>Page Title</div> |
| 354 | |
| 355 | As you can see, two TH1 variable substitutions were done. |
| 356 | |
| 357 | The same TH1 interpreter is used for both the header and the footer |
| 358 |
+96
-10
| --- www/forum.wiki | ||
| +++ www/forum.wiki | ||
| @@ -408,11 +408,11 @@ | ||
| 408 | 408 | posts. This is intentional, as closing one's own post can be used to |
| 409 | 409 | antagonize other forum users. For example, by posting something |
| 410 | 410 | trollish or highly controversial in nature and closing the post to |
| 411 | 411 | further responses. |
| 412 | 412 | |
| 413 | -<h2 name="status">Setting Post Statuses</h2> | |
| 413 | +<h2 id="status">Setting Post Statuses</h2> | |
| 414 | 414 | |
| 415 | 415 | As of version 2.29 forum posts may be tagged with a status. The |
| 416 | 416 | setting <tt>forum-statuses</tt> controls whether this feature is is |
| 417 | 417 | enabled. If it is not set, is not valid JSON5, or has only a single |
| 418 | 418 | entry, then status are not shown in the forum. |
| @@ -474,22 +474,23 @@ | ||
| 474 | 474 | rendering a newer version. |
| 475 | 475 | |
| 476 | 476 | Caveat: a "closed" status is not recommended because it's easy to confuse with |
| 477 | 477 | the <a href='#close-post'>"closed" tag feature</a>, which behaves considerably |
| 478 | 478 | differently and predates that "status" tag support by about three years. The |
| 479 | -"closed" semantics cannot be trivially consolidated with those of "status". | |
| 480 | - | |
| 481 | -<h2 name="attachments">Attachments</h2> | |
| 482 | - | |
| 483 | -As of version 2.29 users with the [./caps/index.md|'B' capability] | |
| 484 | -may attach files to forum posts. Files may not be attached until a | |
| 485 | -forum post is saved for the first time, after which an "Attach" button | |
| 486 | -will appear in the post when it is selected. Attached files undergo | |
| 479 | +"closed" semantics cannot be trivially consolidated with those of "status" | |
| 480 | +but we reserve the right to do so at some point. | |
| 481 | + | |
| 482 | +<h2 id="attachments">Attachments</h2> | |
| 483 | + | |
| 484 | +As of version 2.29 users with the [./caps/index.md|'B' capability] may | |
| 485 | +attach files to forum posts. Files may not be attached until a forum | |
| 486 | +post is saved for the first time, after which an "Attach" button will | |
| 487 | +appear in the post when it is selected. Attached files undergo | |
| 487 | 488 | moderation exactly like forum posts do. When a moderator accepts a |
| 488 | 489 | pending-moderation posts, all files attached to it which are also |
| 489 | 490 | pending approval are also approved. Similarly, when a moderator |
| 490 | -rejects a pending-moderation post, all files attached to it when are | |
| 491 | +rejects a pending-moderation post, all files attached to it which are | |
| 491 | 492 | also pending approval are rejected. |
| 492 | 493 | |
| 493 | 494 | Developer notes regarding the save-before-attach limitation: |
| 494 | 495 | |
| 495 | 496 | * We cannot add the attachment form to the current post editor |
| @@ -513,5 +514,90 @@ | ||
| 513 | 514 | notified, admin reads post, user attaches a file at that time, |
| 514 | 515 | admin approves post. |
| 515 | 516 | |
| 516 | 517 | TBD is whether to accept that case or to remove automatic approval of |
| 517 | 518 | attached files. |
| 519 | + | |
| 520 | +<h2 id="drafts">Draft Edits</h2> | |
| 521 | + | |
| 522 | +The forum uses, if available, JavaScript's <code>localStorage</code> | |
| 523 | +or <code>sessionStorage</code> to store local drafts of new | |
| 524 | +posts, replies, and edits to existing posts. Each time an editor | |
| 525 | +input field loses focus, the draft is saved. When viewing a forum | |
| 526 | +thread, the Edit and Reply buttons get clearly marked if they | |
| 527 | +have an associated local draft. | |
| 528 | + | |
| 529 | +Local drafts for replies and responses (but not new posts) are | |
| 530 | +automatically purged at semi-random intervals. Every Nth visit to a | |
| 531 | +forum page has a small chance to run the cleanup, and drafts older | |
| 532 | +than 10 days old are purged. | |
| 533 | + | |
| 534 | +If neither <code>localStorage</code> nor <code>sessionStorage</code> | |
| 535 | +are available, it uses a transient storage object which is recreated | |
| 536 | +on each page-load, so it cannot store drafts outside of a single | |
| 537 | +thread at a time. | |
| 538 | + | |
| 539 | +Beware: Each draft is keyed to the post it is editing or replied to, | |
| 540 | +which means that editing or replying to the same post from different | |
| 541 | +tabs will cause collisions. The editor to most recently lose the focus | |
| 542 | +will save the draft, overwriting the other. | |
| 543 | + | |
| 544 | +In browsers which support Web Locks, attempts to edit resp. reply to a | |
| 545 | +post which is already being edited resp. replied to in another tab | |
| 546 | +will show an error for the second and subsequent tabs. The first tab | |
| 547 | +to use the edit/reply buttons will hold a separate Web Lock for each | |
| 548 | +and forbid edit/reply access to the post in other tabs until the | |
| 549 | +locking tab is closed. | |
| 550 | + | |
| 551 | + | |
| 552 | +<h2 id="padding">Curious Extra Page Padding</h2> | |
| 553 | + | |
| 554 | +Users may notice that the forum pages get a bunch of padding added to | |
| 555 | +the bottom of the page when an editor widget is opened. This is not a | |
| 556 | +bug, but a workaround to help avoid the UI jumping around when | |
| 557 | +rendering a preview wildly resizes the widget. The effect is | |
| 558 | +especially helpful when working on posts while the bottom footer of | |
| 559 | +the page is in the viewport, as the footer is a hard boundary against | |
| 560 | +scrolling. With the extra padding, the editor widget's top edge shifts | |
| 561 | +around less when the preview is shown and hidden. | |
| 562 | + | |
| 563 | +<h2 id="fork">Forked Edits</h2> | |
| 564 | + | |
| 565 | +It is possible to "fork" forum messages, in the same way that any | |
| 566 | +given check-in may cause a fork. This can happen when a message is | |
| 567 | +edited concurrently from two or more browser tabs or via two or more | |
| 568 | +fossil instances which subsequently sync with each other. | |
| 569 | + | |
| 570 | +When a forum post is forked, the UI currently behaves as it does for | |
| 571 | +concurrent wiki page edits, showing only the most recent | |
| 572 | +edit. Currently (2026-06) the UI is incapable of showing the older | |
| 573 | +forked copies but (A) the data are still part of the SCM history and | |
| 574 | +(B) there are plans to improve the UI to show such forks. | |
| 575 | + | |
| 576 | +For trivia's sake, here's a query which finds posts which have | |
| 577 | +forks: | |
| 578 | + | |
| 579 | +<pre><verbatim> | |
| 580 | +SELECT bp.rid AS parentRid, bp.uuid parentUuid, count(f.fprev) forkCount | |
| 581 | +FROM blob b, blob bp, forumpost f | |
| 582 | +WHERE b.rid=f.fpid | |
| 583 | +AND f.fprev=bp.rid | |
| 584 | +GROUP BY f.fprev | |
| 585 | +HAVING forkCount>1; | |
| 586 | +</verbatim></pre> | |
| 587 | + | |
| 588 | +And here's one which lists forked posts: | |
| 589 | + | |
| 590 | +<pre><verbatim> | |
| 591 | +WITH multiparent(mpid) AS ( | |
| 592 | + SELECT fprev FROM forumpost f | |
| 593 | + GROUP BY fprev | |
| 594 | + HAVING count(fprev)>1 | |
| 595 | +) | |
| 596 | +SELECT datetime(fmtime), forumpost.* | |
| 597 | +FROM forumpost, multiparent | |
| 598 | +WHERE fprev=mpid | |
| 599 | +ORDER BY mpid, fpid, fmtime; | |
| 600 | +</verbatim></pre> | |
| 601 | + | |
| 602 | +The telling part is the <code>fprev</code> column: those with matching | |
| 603 | +values forked from the post referred to by <code>fprev</code>. | |
| 518 | 604 |
| --- www/forum.wiki | |
| +++ www/forum.wiki | |
| @@ -408,11 +408,11 @@ | |
| 408 | posts. This is intentional, as closing one's own post can be used to |
| 409 | antagonize other forum users. For example, by posting something |
| 410 | trollish or highly controversial in nature and closing the post to |
| 411 | further responses. |
| 412 | |
| 413 | <h2 name="status">Setting Post Statuses</h2> |
| 414 | |
| 415 | As of version 2.29 forum posts may be tagged with a status. The |
| 416 | setting <tt>forum-statuses</tt> controls whether this feature is is |
| 417 | enabled. If it is not set, is not valid JSON5, or has only a single |
| 418 | entry, then status are not shown in the forum. |
| @@ -474,22 +474,23 @@ | |
| 474 | rendering a newer version. |
| 475 | |
| 476 | Caveat: a "closed" status is not recommended because it's easy to confuse with |
| 477 | the <a href='#close-post'>"closed" tag feature</a>, which behaves considerably |
| 478 | differently and predates that "status" tag support by about three years. The |
| 479 | "closed" semantics cannot be trivially consolidated with those of "status". |
| 480 | |
| 481 | <h2 name="attachments">Attachments</h2> |
| 482 | |
| 483 | As of version 2.29 users with the [./caps/index.md|'B' capability] |
| 484 | may attach files to forum posts. Files may not be attached until a |
| 485 | forum post is saved for the first time, after which an "Attach" button |
| 486 | will appear in the post when it is selected. Attached files undergo |
| 487 | moderation exactly like forum posts do. When a moderator accepts a |
| 488 | pending-moderation posts, all files attached to it which are also |
| 489 | pending approval are also approved. Similarly, when a moderator |
| 490 | rejects a pending-moderation post, all files attached to it when are |
| 491 | also pending approval are rejected. |
| 492 | |
| 493 | Developer notes regarding the save-before-attach limitation: |
| 494 | |
| 495 | * We cannot add the attachment form to the current post editor |
| @@ -513,5 +514,90 @@ | |
| 513 | notified, admin reads post, user attaches a file at that time, |
| 514 | admin approves post. |
| 515 | |
| 516 | TBD is whether to accept that case or to remove automatic approval of |
| 517 | attached files. |
| 518 |
| --- www/forum.wiki | |
| +++ www/forum.wiki | |
| @@ -408,11 +408,11 @@ | |
| 408 | posts. This is intentional, as closing one's own post can be used to |
| 409 | antagonize other forum users. For example, by posting something |
| 410 | trollish or highly controversial in nature and closing the post to |
| 411 | further responses. |
| 412 | |
| 413 | <h2 id="status">Setting Post Statuses</h2> |
| 414 | |
| 415 | As of version 2.29 forum posts may be tagged with a status. The |
| 416 | setting <tt>forum-statuses</tt> controls whether this feature is is |
| 417 | enabled. If it is not set, is not valid JSON5, or has only a single |
| 418 | entry, then status are not shown in the forum. |
| @@ -474,22 +474,23 @@ | |
| 474 | rendering a newer version. |
| 475 | |
| 476 | Caveat: a "closed" status is not recommended because it's easy to confuse with |
| 477 | the <a href='#close-post'>"closed" tag feature</a>, which behaves considerably |
| 478 | differently and predates that "status" tag support by about three years. The |
| 479 | "closed" semantics cannot be trivially consolidated with those of "status" |
| 480 | but we reserve the right to do so at some point. |
| 481 | |
| 482 | <h2 id="attachments">Attachments</h2> |
| 483 | |
| 484 | As of version 2.29 users with the [./caps/index.md|'B' capability] may |
| 485 | attach files to forum posts. Files may not be attached until a forum |
| 486 | post is saved for the first time, after which an "Attach" button will |
| 487 | appear in the post when it is selected. Attached files undergo |
| 488 | moderation exactly like forum posts do. When a moderator accepts a |
| 489 | pending-moderation posts, all files attached to it which are also |
| 490 | pending approval are also approved. Similarly, when a moderator |
| 491 | rejects a pending-moderation post, all files attached to it which are |
| 492 | also pending approval are rejected. |
| 493 | |
| 494 | Developer notes regarding the save-before-attach limitation: |
| 495 | |
| 496 | * We cannot add the attachment form to the current post editor |
| @@ -513,5 +514,90 @@ | |
| 514 | notified, admin reads post, user attaches a file at that time, |
| 515 | admin approves post. |
| 516 | |
| 517 | TBD is whether to accept that case or to remove automatic approval of |
| 518 | attached files. |
| 519 | |
| 520 | <h2 id="drafts">Draft Edits</h2> |
| 521 | |
| 522 | The forum uses, if available, JavaScript's <code>localStorage</code> |
| 523 | or <code>sessionStorage</code> to store local drafts of new |
| 524 | posts, replies, and edits to existing posts. Each time an editor |
| 525 | input field loses focus, the draft is saved. When viewing a forum |
| 526 | thread, the Edit and Reply buttons get clearly marked if they |
| 527 | have an associated local draft. |
| 528 | |
| 529 | Local drafts for replies and responses (but not new posts) are |
| 530 | automatically purged at semi-random intervals. Every Nth visit to a |
| 531 | forum page has a small chance to run the cleanup, and drafts older |
| 532 | than 10 days old are purged. |
| 533 | |
| 534 | If neither <code>localStorage</code> nor <code>sessionStorage</code> |
| 535 | are available, it uses a transient storage object which is recreated |
| 536 | on each page-load, so it cannot store drafts outside of a single |
| 537 | thread at a time. |
| 538 | |
| 539 | Beware: Each draft is keyed to the post it is editing or replied to, |
| 540 | which means that editing or replying to the same post from different |
| 541 | tabs will cause collisions. The editor to most recently lose the focus |
| 542 | will save the draft, overwriting the other. |
| 543 | |
| 544 | In browsers which support Web Locks, attempts to edit resp. reply to a |
| 545 | post which is already being edited resp. replied to in another tab |
| 546 | will show an error for the second and subsequent tabs. The first tab |
| 547 | to use the edit/reply buttons will hold a separate Web Lock for each |
| 548 | and forbid edit/reply access to the post in other tabs until the |
| 549 | locking tab is closed. |
| 550 | |
| 551 | |
| 552 | <h2 id="padding">Curious Extra Page Padding</h2> |
| 553 | |
| 554 | Users may notice that the forum pages get a bunch of padding added to |
| 555 | the bottom of the page when an editor widget is opened. This is not a |
| 556 | bug, but a workaround to help avoid the UI jumping around when |
| 557 | rendering a preview wildly resizes the widget. The effect is |
| 558 | especially helpful when working on posts while the bottom footer of |
| 559 | the page is in the viewport, as the footer is a hard boundary against |
| 560 | scrolling. With the extra padding, the editor widget's top edge shifts |
| 561 | around less when the preview is shown and hidden. |
| 562 | |
| 563 | <h2 id="fork">Forked Edits</h2> |
| 564 | |
| 565 | It is possible to "fork" forum messages, in the same way that any |
| 566 | given check-in may cause a fork. This can happen when a message is |
| 567 | edited concurrently from two or more browser tabs or via two or more |
| 568 | fossil instances which subsequently sync with each other. |
| 569 | |
| 570 | When a forum post is forked, the UI currently behaves as it does for |
| 571 | concurrent wiki page edits, showing only the most recent |
| 572 | edit. Currently (2026-06) the UI is incapable of showing the older |
| 573 | forked copies but (A) the data are still part of the SCM history and |
| 574 | (B) there are plans to improve the UI to show such forks. |
| 575 | |
| 576 | For trivia's sake, here's a query which finds posts which have |
| 577 | forks: |
| 578 | |
| 579 | <pre><verbatim> |
| 580 | SELECT bp.rid AS parentRid, bp.uuid parentUuid, count(f.fprev) forkCount |
| 581 | FROM blob b, blob bp, forumpost f |
| 582 | WHERE b.rid=f.fpid |
| 583 | AND f.fprev=bp.rid |
| 584 | GROUP BY f.fprev |
| 585 | HAVING forkCount>1; |
| 586 | </verbatim></pre> |
| 587 | |
| 588 | And here's one which lists forked posts: |
| 589 | |
| 590 | <pre><verbatim> |
| 591 | WITH multiparent(mpid) AS ( |
| 592 | SELECT fprev FROM forumpost f |
| 593 | GROUP BY fprev |
| 594 | HAVING count(fprev)>1 |
| 595 | ) |
| 596 | SELECT datetime(fmtime), forumpost.* |
| 597 | FROM forumpost, multiparent |
| 598 | WHERE fprev=mpid |
| 599 | ORDER BY mpid, fpid, fmtime; |
| 600 | </verbatim></pre> |
| 601 | |
| 602 | The telling part is the <code>fprev</code> column: those with matching |
| 603 | values forked from the post referred to by <code>fprev</code>. |
| 604 |
+142
-119
| --- www/fossil-v-git.wiki | ||
| +++ www/fossil-v-git.wiki | ||
| @@ -20,12 +20,12 @@ | ||
| 20 | 20 | In this document, we set all of that similarity and interoperability |
| 21 | 21 | aside and focus on the important differences between the two, especially |
| 22 | 22 | those that impact the user experience. |
| 23 | 23 | |
| 24 | 24 | Keep in mind that you are reading this on a Fossil website, and though |
| 25 | -we try to be fair, the information here | |
| 26 | -might be biased in favor of Fossil, if only because we spend most of our | |
| 25 | +we try to be fair, the information here will inevitably | |
| 26 | +be biased in favor of Fossil purely because we spend most of our | |
| 27 | 27 | time using Fossil, not Git. Ask around for second opinions from |
| 28 | 28 | people who have used <em>both</em> Fossil and Git. |
| 29 | 29 | |
| 30 | 30 | If you want a more practical, less philosophical guide to moving from |
| 31 | 31 | Git to Fossil, see our [./gitusers.md | Git to Fossil Translation Guide]. |
| @@ -178,36 +178,40 @@ | ||
| 178 | 178 | This policy is particularly useful when running Fossil inside a |
| 179 | 179 | restrictive container, anything from [./chroot.md | classic chroot |
| 180 | 180 | jails] to modern [https://en.wikipedia.org/wiki/OS-level_virtualization |
| 181 | 181 | | OS-level virtualization mechanisms] such as |
| 182 | 182 | [https://en.wikipedia.org/wiki/Docker_(software) | Docker]. |
| 183 | -Our [./containers.md | stock container image] is under 8 MB when | |
| 184 | -uncompressed and running. It contains nothing but a single | |
| 183 | +Our [./containers.md | stock container image] is under 11 MB when | |
| 184 | +uncompressed and running because it contains nothing but a single | |
| 185 | 185 | statically-linked binary. |
| 186 | 186 | |
| 187 | -If you build a dynamically linked binary instead, Fossil's on-disk size | |
| 188 | -drops to around 6 MB, and it's dependent only on widespread | |
| 187 | +If you build a dynamically linked binary instead, a Linux | |
| 188 | +x86_64 build of Fossil drops to under 5 MB on-disk, stripped. | |
| 189 | +It will depend only on widespread | |
| 189 | 190 | platform libraries with stable ABIs such as glibc, zlib, and openssl. |
| 190 | 191 | |
| 191 | -Full static linking is easier on Windows, so our precompiled Windows | |
| 192 | -binaries are just a ZIP archive | |
| 193 | -containing only "<tt>fossil.exe</tt>". There is no "<tt>setup.exe</tt>" | |
| 194 | -to run. | |
| 192 | +Much the same is true on Windows, where our precompiled static binaries | |
| 193 | +are distributed inside a ZIP archive containing "<tt>fossil.exe</tt>" | |
| 194 | +and not a thing else, with a size on-par that of the Linux container | |
| 195 | +build. There is no "<tt>setup.exe</tt>" to run; just copy it into your | |
| 196 | +<tt>%PATH%</tt>. | |
| 195 | 197 | |
| 196 | 198 | Fossil is easy to build from sources. Just run |
| 197 | 199 | "<tt>./configure && make</tt>" on POSIX systems and |
| 198 | 200 | "<tt>nmake /f Makefile.msc</tt>" on Windows. |
| 199 | 201 | |
| 200 | -Contrast a basic installation of Git, which takes up about | |
| 201 | -15 MiB on Debian 10 across 230 files, not counting the contents of | |
| 202 | -<tt>/usr/share/doc</tt> or <tt>/usr/share/locale</tt>. If you need to | |
| 203 | -deploy to any platform where you cannot count on facilities like the POSIX | |
| 204 | -shell, Perl interpreter, and Tcl/Tk platform needed to fully use Git | |
| 205 | -as part of the base platform, the full footprint of a Git installation | |
| 206 | -extends to more like 45 MiB and thousands of files. This complicates | |
| 207 | -several common scenarios: Git for Windows, chrooted Git servers, | |
| 208 | -Docker images... | |
| 202 | +Git, by contrast, takes 25 MiB on Ubuntu 26.04 across 940 files. | |
| 203 | +That doesn't count platform facilities like the POSIX shell and script | |
| 204 | +interpreters its full feature set requires. A fairer comparison to | |
| 205 | +Fossil's single static binary container is the Docker Hardened Image for | |
| 206 | +[https://hub.docker.com/hardened-images/catalog/dhi/git |Git 2.x on | |
| 207 | +Alpine], which presently weighs in at 36.3 megs unpacked and 607 files, | |
| 208 | +if you count all the <tt>busybox</tt> symlinks which would otherwise be | |
| 209 | +separate binaries on a more traditional Linux system. Worst of all are | |
| 210 | +the "Git for Windows" packages where they end up needing to ship a large | |
| 211 | +yet nerfed Linux-like userland to support Git's many loosely coupled | |
| 212 | +pieces, approaching a hundred megs and thousands of files. | |
| 209 | 213 | |
| 210 | 214 | Some say that Git more closely adheres to the Unix philosophy, |
| 211 | 215 | summarized as "many small tools, loosely joined," but we have many |
| 212 | 216 | examples of other successful Unix software that violates that principle |
| 213 | 217 | to good effect, from Apache to Python to ZFS. We can infer from that |
| @@ -216,26 +220,47 @@ | ||
| 216 | 220 | matters is effectiveness and efficiency. We believe Fossil achieves |
| 217 | 221 | this. |
| 218 | 222 | |
| 219 | 223 | The above size comparisons aren't apples-to-apples anyway. We've |
| 220 | 224 | compared the size of Fossil with all of its [#features | many built-in |
| 221 | -features] to a fairly minimal Git installation. You must add a lot of | |
| 222 | -third-party software to Git to give it a Fossil-equivalent feature set. | |
| 223 | -Consider [https://about.gitlab.com/|GitLab], a third-party extension to | |
| 224 | -Git wrapping it in many features, making it roughly Fossil-equivalent, | |
| 225 | -though [https://docs.gitlab.com/ee/install/requirements.html|much more | |
| 226 | -resource hungry] and hence more costly to run than the equivalent Fossil | |
| 227 | -setup. [https://hub.docker.com/r/gitlab/gitlab-ce/ | The official GitLab | |
| 228 | -Community Edition container] currently clocks in at 2.66 GiB! | |
| 229 | - | |
| 230 | -GitLab's requirements are easy to accept when you're dedicating | |
| 231 | -a local rack server or blade to it, since its minimum requirements are | |
| 232 | -more or less a description of the smallest | |
| 233 | -thing you could call a "server" these days, but when you go to host that | |
| 234 | -in the cloud, you can expect to pay about 8 times as much to comfortably host | |
| 235 | -GitLab as for Fossil.³ This difference is largely due to basic | |
| 236 | -technology choices: Ruby and PostgreSQL vs C and SQLite. | |
| 225 | +features] to a relatively bare-bones Git installation. You must add a | |
| 226 | +lot of third-party software to Git to give it a Fossil-equivalent | |
| 227 | +feature set. | |
| 228 | + | |
| 229 | +Consider [https://about.gitlab.com/|GitLab], which wraps Git in enough | |
| 230 | +features to bring it to a rough (very rough) parity with Fossil. While | |
| 231 | +it is certainly ahead in some areas — automation comes to mind — it must | |
| 232 | +be noted that GitLab's own forums are on Discourse, not self-hosted as | |
| 233 | +with Fossil. You may then argue that Discourse is superior to the Fossil | |
| 234 | +forum feature, but this merely brings us back to another of Fossil's | |
| 235 | +advantages: when the posts are part of the repo, you can migrate your | |
| 236 | +entire project to another host merely by standing that repo back up on | |
| 237 | +other hardware. Choosing to assemble your project hosting from multiple | |
| 238 | +pieces requires each to have their own backups, their own management | |
| 239 | +processes, and their own migration strategies. | |
| 240 | + | |
| 241 | +Even then, these disparate services do not cooperate at the same level | |
| 242 | +as in Fossil, where the ability to have a forum post linking to a wiki | |
| 243 | +article linking to a trouble ticket linking to a commit falls out of the | |
| 244 | +model nearly for free. These are all internal links, mind, potentially | |
| 245 | +using nothing but repository artifact hashes, all backed by SQLite's | |
| 246 | +referential integrity. With something like GitLab, its "internal | |
| 247 | +references" are tacked on after the fact, not Git repo hashes at all, | |
| 248 | +and when it comes to external services like Discourse, history tells us | |
| 249 | +you're storing up tech debt which will come due at some future point in | |
| 250 | +the form of piles of broken links when one piece or the other needs | |
| 251 | +changing out. | |
| 252 | + | |
| 253 | +Furthermore, GitLab is far more resource hungry even in its | |
| 254 | +[https://docs.gitlab.com/omnibus/settings/memory_constrained_envs/ | | |
| 255 | +minimal configuration], hence more costly to run than the equivalent | |
| 256 | +Fossil setup. [https://hub.docker.com/r/gitlab/gitlab-ce/ | The | |
| 257 | +official GitLab Community Edition container] currently clocks in at | |
| 258 | +3.44 GiB, independent of add-ons like Discourse. You can expect it to | |
| 259 | +cost around 8× as much to host it on a cloud service. Even pared down | |
| 260 | +to the minimum, there remain the consequences from the difference in | |
| 261 | +basic technology choices: Ruby and PostgreSQL vs C and SQLite. | |
| 237 | 262 | |
| 238 | 263 | The Fossil project itself is [./selfhost.wiki|hosted on a small and |
| 239 | 264 | inexpensive VPS]. A bare-bones $5/month VPS or a |
| 240 | 265 | spare Raspberry Pi is sufficient to run a full-up project |
| 241 | 266 | site, complete with tickets, wiki, chat, and forum, in addition to |
| @@ -255,33 +280,34 @@ | ||
| 255 | 280 | <tt>.git</tt> folder or compressed into bespoke key/value |
| 256 | 281 | [https://git-scm.com/book/en/v2/Git-Internals-Packfiles|pack-files], |
| 257 | 282 | whereas Fossil stores its objects in a [https://www.sqlite.org/|SQLite] |
| 258 | 283 | database file which provides ACID transactions and a high-level query |
| 259 | 284 | language. |
| 260 | -This difference is more than an implementation detail. It has important | |
| 285 | + | |
| 286 | +This difference is more than an implementation detail; it has important | |
| 261 | 287 | practical consequences. |
| 262 | 288 | |
| 263 | -One notable consequence is that it is difficult to find the descendants | |
| 289 | +One notable example is that it is difficult to find the descendants | |
| 264 | 290 | of check-ins in Git. |
| 265 | 291 | One can easily locate the ancestors of a particular Git check-in |
| 266 | -by following the pointers embedded in the check-in object, but it is | |
| 267 | -difficult to go the other direction and locate the descendants of a | |
| 268 | -check-in. It is so difficult, in fact, that neither native Git nor | |
| 269 | -GitHub provide this capability short of crawling the | |
| 270 | -[https://www.git-scm.com/docs/git-log|commit log]. With Fossil, | |
| 271 | -on the other hand, finding descendants is a simple SQL query. | |
| 272 | -It is common in Fossil to ask to see | |
| 273 | -[/timeline?df=release&y=ci|all check-ins since the last release]. | |
| 274 | -Git lets you see "what came before". Fossil makes it just as | |
| 275 | -easy to also see "what came after". | |
| 292 | +by following the pointers embedded in the check-in object, but | |
| 293 | +going the other direction is difficult enough | |
| 294 | +that neither native Git nor the big "forge" facilities | |
| 295 | +like GitHub and GitLab provide this capability short of crawling the | |
| 296 | +[https://www.git-scm.com/docs/git-log|commit log]. In Fossil, | |
| 297 | +we can find descendants using a simple SQL query, which then allows | |
| 298 | +us to see [/timeline?df=release&y=ci|all check-ins since the last release], | |
| 299 | +as but one example. | |
| 300 | +Git lets you see "what came before," but Fossil makes it just as | |
| 301 | +easy to also see "what came after." | |
| 276 | 302 | |
| 277 | 303 | Leaf check-ins in Git that lack a "ref" become "detached," making them |
| 278 | 304 | difficult to locate and subject to garbage collection. This |
| 279 | 305 | [https://stackoverflow.com/q/3965676 | detached head |
| 280 | 306 | state] problem has caused grief for |
| 281 | -[https://www.google.com/search?q=git+detached+head+state | many | |
| 282 | -Git users]. With | |
| 307 | +[https://www.google.com/search?q=git+detached+head+state | | |
| 308 | +untold millions of Git users]. With | |
| 283 | 309 | Fossil, detached heads are simply impossible because we can always find |
| 284 | 310 | our way back into the Merkle tree using one or more of the relations |
| 285 | 311 | in the SQL database. |
| 286 | 312 | |
| 287 | 313 | The SQL query capabilities of Fossil make it easier to track the |
| @@ -288,34 +314,27 @@ | ||
| 288 | 314 | changes for one particular file within a project. For example, |
| 289 | 315 | you can easily find |
| 290 | 316 | [/finfo/www/fossil-v-git.wiki|the complete edit history of this one document], |
| 291 | 317 | or even |
| 292 | 318 | [/finfo/www/fossil-v-git.wiki?ubg|the same history color-coded by committer], |
| 293 | -Both questions are simple SQL query in Fossil, with procedural code | |
| 319 | +Both come down to simple SQL queries in Fossil, with procedural code | |
| 294 | 320 | only being used to format the result for display. |
| 295 | 321 | The same result could be obtained from Git, but because the data is |
| 296 | 322 | in a key/value store, much more procedural code has to be written to |
| 297 | -walk the data and compute the result. And since that is a lot more | |
| 298 | -work, the question is seldom asked. | |
| 323 | +walk the data and compute the result. | |
| 299 | 324 | |
| 300 | 325 | The ease of querying Fossil data using SQL means that status or |
| 301 | 326 | history information about the project under management is easier |
| 302 | -to obtain. Being easier means that it is more likely to happen. | |
| 327 | +to obtain, hence more likely to happen, giving its developers better | |
| 328 | +situational awareness. | |
| 303 | 329 | Fossil reports tend to be more detailed and useful. |
| 304 | 330 | Compare [/timeline?c=6df7a853ec16865b|this Fossil timeline] |
| 305 | 331 | to |
| 306 | 332 | [https://github.com/drhsqlite/fossil-mirror/commits/master?after=f720c106d297ca1f61bccb30c5c191b88a626d01+34 | |
| 307 | -its closest equivalent in GitHub]. Judge for yourself: which of those | |
| 333 | +its closest equivalent in the GitHub mirror]. Judge for yourself: which of those | |
| 308 | 334 | reports is more useful to a developer trying to understand what happened? |
| 309 | 335 | |
| 310 | -The bottom line is that even though Fossil and Git are built around | |
| 311 | -the same low-level data structure, the use of SQL | |
| 312 | -to query this data makes the data more accessible in Fossil, resulting | |
| 313 | -in more detailed information being available to the user. This | |
| 314 | -improves situational awareness and makes working on the project | |
| 315 | -easier. | |
| 316 | - | |
| 317 | 336 | <h3 id="portable">2.4 Portable</h3> |
| 318 | 337 | |
| 319 | 338 | Fossil is largely written in ISO C, almost purely conforming to the |
| 320 | 339 | original 1989 standard. We make very little use of |
| 321 | 340 | [https://en.wikipedia.org/wiki/C99|C99], and we do not knowingly make |
| @@ -326,11 +345,11 @@ | ||
| 326 | 345 | facilities Fossil needs to do its thing. (Network sockets, file locking, |
| 327 | 346 | etc.) There are certainly well-known platforms Fossil hasn't been ported |
| 328 | 347 | to yet, but that's most likely due to lack of interest rather than |
| 329 | 348 | inherent difficulties in doing the port. We believe the most stringent |
| 330 | 349 | limit on its portability is that it assumes at least a 32-bit CPU and |
| 331 | -several megs of flat-addressed memory.⁴ Fossil isn't quite as | |
| 350 | +several megs of flat-addressed memory.³ Fossil isn't quite as | |
| 332 | 351 | [https://www.sqlite.org/custombuild.html|portable as SQLite], but it's |
| 333 | 352 | close. |
| 334 | 353 | |
| 335 | 354 | Over half of the C code in Fossil is actually an embedded copy of the |
| 336 | 355 | current version of SQLite. Much of what is Fossil-specific after you set |
| @@ -344,20 +363,20 @@ | ||
| 344 | 363 | necessary]. The server-side |
| 345 | 364 | UI scripting uses a custom minimal |
| 346 | 365 | [https://en.wikipedia.org/wiki/Tcl|Tcl] dialect called |
| 347 | 366 | [./th1.md|TH1], which is |
| 348 | 367 | embedded into Fossil itself. Fossil's build system and test suite are |
| 349 | -largely based on Tcl.⁵ All of this is quite portable. | |
| 368 | +largely based on Tcl.⁴ All of this is quite portable. | |
| 350 | 369 | |
| 351 | 370 | About half of Git's code is POSIX C, and about a third is POSIX shell |
| 352 | 371 | code. This is largely why the so-called "Git for Windows" distributions |
| 353 | 372 | (both [https://git-scm.com/download/win|first-party] and |
| 354 | 373 | [https://gitforwindows.org/|third-party]) are actually an |
| 355 | 374 | [https://www.msys2.org/wiki/Home/|MSYS POSIX portability environment] bundled |
| 356 | 375 | with all of the Git stuff, because it would be too painful to port Git |
| 357 | 376 | natively to Windows. Git is a foreign citizen on Windows, speaking to it |
| 358 | -only through a translator.⁶ | |
| 377 | +only through a translator.⁵ | |
| 359 | 378 | |
| 360 | 379 | While Fossil does lean toward POSIX norms when given a choice — LF-only |
| 361 | 380 | line endings are treated as first-class citizens over CR+LF, for example |
| 362 | 381 | — the Windows build of Fossil is truly native. |
| 363 | 382 | |
| @@ -444,11 +463,11 @@ | ||
| 444 | 463 | [https://www.git-scm.com/docs/git-request-pull|pull requests] offer |
| 445 | 464 | a low-friction path to accepting |
| 446 | 465 | [https://www.jonobacon.com/2012/07/25/building-strong-community-structural-integrity/|drive-by |
| 447 | 466 | contributions]. Fossil's closest equivalents are its unique |
| 448 | 467 | [/help/bundle|bundle] and [/help/patch|patch] features, which require higher engagement |
| 449 | - than firing off a PR.⁷ This difference comes directly from the | |
| 468 | + than firing off a PR.⁶ This difference comes directly from the | |
| 450 | 469 | initial designed purpose for each tool: the SQLite project doesn't |
| 451 | 470 | accept outside contributions from previously-unknown developers, but |
| 452 | 471 | the Linux kernel does. |
| 453 | 472 | |
| 454 | 473 | * <b>No rebasing:</b> When your local repo clone syncs changes |
| @@ -500,16 +519,16 @@ | ||
| 500 | 519 | that everyone — especially the project leader — can maintain a better |
| 501 | 520 | mental picture of what is happening, leading to better situational |
| 502 | 521 | awareness. |
| 503 | 522 | |
| 504 | 523 | By contrast, "…[https://docs.github.com/en/get-started/quickstart/contributing-to-projects|forking is |
| 505 | -at the core of social coding at GitHub]". As of January 2022, | |
| 506 | -[https://github.com/search?q=is:public|Github hosts 47 million distinct | |
| 524 | +at the core of social coding at GitHub]". As of June 2026, | |
| 525 | +[https://github.com/search?q=is:public|Github hosts 324 million distinct | |
| 507 | 526 | software projects], most of which were created by forking a |
| 508 | 527 | previously-existing project. Since this is |
| 509 | -[https://evansdata.com/reports/viewRelease.php?reportID=9 | roughly | |
| 510 | -twice the number of developers in the world], it beggars belief that | |
| 528 | +[https://www.griddynamics.com/blog/number-software-developers-world | | |
| 529 | +~11× the number of developers in the world], it beggars belief that | |
| 511 | 530 | most of these forks are still under active development. The vast bulk |
| 512 | 531 | of these must be abandoned one-off efforts. This is part of the nature |
| 513 | 532 | of bazaar style development. |
| 514 | 533 | |
| 515 | 534 | You can think about this difference in terms of |
| @@ -533,18 +552,18 @@ | ||
| 533 | 552 | <h4 id="scale">2.5.2 Scale</h4> |
| 534 | 553 | |
| 535 | 554 | The Linux kernel has a far bigger developer community than that of |
| 536 | 555 | SQLite: there are thousands and thousands of contributors to Linux, most |
| 537 | 556 | of whom do not know each other's names. These thousands are responsible |
| 538 | -for producing roughly 89× more code than is in SQLite. (10.7 | |
| 539 | -[https://en.wikipedia.org/wiki/Source_lines_of_code|MLOC] vs. 0.12 MLOC | |
| 540 | -according to [https://dwheeler.com/sloccount/|SLOCCount].) The Linux | |
| 557 | +for producing roughly 73× more code than is in SQLite. (32.0 | |
| 558 | +[https://en.wikipedia.org/wiki/Source_lines_of_code|MLOC] vs. 0.44 MLOC | |
| 559 | +according to [https://github.com/boyter/scc | scc].) The Linux | |
| 541 | 560 | kernel and its development process were already uncommonly large back in |
| 542 | 561 | 2005 when Git was designed, specifically to support the consequences of |
| 543 | 562 | having such a large set of developers working on such a large code base. |
| 544 | 563 | |
| 545 | -95% of the code in SQLite comes from just four programmers, and 64% of | |
| 564 | +95% of the code in SQLite comes from just six programmers, and 62% of | |
| 546 | 565 | it is from the lead developer alone. The SQLite developers know each |
| 547 | 566 | other well and interact daily. Fossil was designed for this development |
| 548 | 567 | model. |
| 549 | 568 | |
| 550 | 569 | When choosing your DVCS, we think you should ask yourself whether the |
| @@ -564,35 +583,33 @@ | ||
| 564 | 583 | |
| 565 | 584 | Both Fossil and Git store history as a directed acyclic graph (DAG) |
| 566 | 585 | of changes, but Git tends to focus more on individual branches of |
| 567 | 586 | the DAG, whereas Fossil puts more emphasis on the entire DAG. |
| 568 | 587 | |
| 569 | -For example, the default behavior in Git is to only synchronize | |
| 570 | -a single branch, whereas with Fossil the only sync option is to | |
| 571 | -sync the entire DAG. Git commands, | |
| 588 | +While a common usage pattern in Git is to only synchronize | |
| 589 | +a single branch — <tt>git pull upstream feature/branch</tt> — instead | |
| 590 | +of all refs, Fossil does not give you a choice; it | |
| 591 | +syncs the entire DAG or nothing. Git commands, | |
| 572 | 592 | GitHub, and GitLab tend to show only a single branch at |
| 573 | 593 | a time, whereas Fossil usually shows all parallel branches at |
| 574 | 594 | once. Git has commands like "rebase" that help keep all relevant |
| 575 | 595 | changes on a single branch, whereas Fossil encourages a style of |
| 576 | 596 | many concurrent branches constantly springing into existence, |
| 577 | 597 | undergoing active development in parallel for a few days or weeks, then |
| 578 | 598 | merging back into the main line and disappearing. |
| 579 | 599 | |
| 580 | 600 | This difference in emphasis arises from the different purposes of |
| 581 | -the two systems. Git focuses on individual branches, because that | |
| 601 | +the two systems. Git's focus on individual branches | |
| 582 | 602 | is exactly what you want for a highly-distributed bazaar-style project |
| 583 | 603 | such as Linux. Linus Torvalds does not want to see every check-in |
| 584 | 604 | by every contributor to Linux: such extreme visibility does not scale |
| 585 | 605 | well. Contrast Fossil, which was written for the cathedral-style SQLite project |
| 586 | 606 | and its handful of active committers. Seeing all |
| 587 | 607 | changes on all branches all at once helps keep the whole team |
| 588 | 608 | up-to-date with what everybody else is doing, resulting in a more |
| 589 | 609 | tightly focused and cohesive implementation. |
| 590 | 610 | |
| 591 | -Parts of this section are [https://fossil-scm.org/forum/forumpost/5961e969fa|disputed] | |
| 592 | -by [https://github.com/olorin37|Jakub A. G.]. | |
| 593 | - | |
| 594 | 611 | |
| 595 | 612 | <h3 id="checkouts">2.6 One vs. Many Check-outs per Repository</h3> |
| 596 | 613 | |
| 597 | 614 | Because Git commingles the repository data with the initial checkout of |
| 598 | 615 | that repository, the default mode of operation in Git is to stick to that |
| @@ -612,20 +629,28 @@ | ||
| 612 | 629 | standard advice is to use a switch-in-place workflow in Fossil when |
| 613 | 630 | the disturbance from switching branches is small, and to use multiple |
| 614 | 631 | checkouts when you have long-lived working branches that are different |
| 615 | 632 | enough that switching in place is disruptive. |
| 616 | 633 | |
| 617 | -While you can [./gitusers.md#worktree | use Git in the Fossil style], | |
| 618 | -Git's default tie between working directory and | |
| 619 | -repository means the standard method for working with a Git repo is to | |
| 620 | -have one working directory only. Most Git tutorials teach this style, so | |
| 621 | -it is how most people learn to use Git. Because relatively few people | |
| 622 | -use Git with multiple working directories per repository, there are | |
| 634 | +While you can [./gitusers.md#worktree | use Git in the Fossil style] via | |
| 635 | +its worktree feature, tutorials continue to teach the style of having | |
| 636 | +one working directory only. Git can even fight you on this, as when | |
| 637 | +working with a forked repository; it is best to have independent clones | |
| 638 | +of the upstream and your fork, to allow separate "remote" lists and | |
| 639 | +such. This can result in two working directories but each having a | |
| 640 | +captive repo clone each, which isn't in the spirit of <tt>git | |
| 641 | +worktree</tt> at all. Yet, it beats the alternative, which then | |
| 642 | +highlights a gap in the model of diverging and re-converging forks. | |
| 643 | +Ideally, Git would let you create one of these forks as a worktree while | |
| 644 | +maintaining a strong separation between your fork and the upstream repo, | |
| 645 | +but it ends up being too much hassle to bother with. | |
| 646 | + | |
| 647 | +There are | |
| 623 | 648 | [https://duckduckgo.com/?q=git+worktree+problem | several known |
| 624 | -problems] with that way of working, problems which don't happen in Fossil because of | |
| 625 | -the clear [./ckout-workflows.md | separation] between a Fossil repository and | |
| 626 | -each working directory. | |
| 649 | +problems] with the single worktree style, ones which don't happen in | |
| 650 | +Fossil because of the clear [./ckout-workflows.md | separation] between | |
| 651 | +a Fossil repository and each working directory. | |
| 627 | 652 | |
| 628 | 653 | This distinction matters because switching branches inside a single working directory loses local context |
| 629 | 654 | on each switch. |
| 630 | 655 | |
| 631 | 656 | For instance, in any software project where the runnable program must be |
| @@ -658,13 +683,10 @@ | ||
| 658 | 683 | |
| 659 | 684 | Plus, |
| 660 | 685 | <tt>cd</tt> is faster to type than <tt>git checkout</tt> or <tt>fossil |
| 661 | 686 | update</tt>. |
| 662 | 687 | |
| 663 | -Parts of this section are [https://fossil-scm.org/forum/forumpost/5961e969fa|disputed] | |
| 664 | -by [https://github.com/olorin37|Jakub A. G.]. | |
| 665 | - | |
| 666 | 688 | <h3 id="history">2.7 What you should have done vs. What you actually did</h3> |
| 667 | 689 | |
| 668 | 690 | Git puts a lot of emphasis on maintaining |
| 669 | 691 | a "clean" check-in history. Extraneous and experimental branches by |
| 670 | 692 | individual developers often never make it into the main repository. |
| @@ -822,14 +844,14 @@ | ||
| 822 | 844 | concepts to keep track of in your mental model of Fossil's internal |
| 823 | 845 | operation. |
| 824 | 846 | |
| 825 | 847 | Fossil's implementation of the feature is also simpler to describe. The |
| 826 | 848 | brief online help for <tt>[/help/merge | fossil merge]</tt> is |
| 827 | -currently 41 lines long, to which you want to add the 600 lines of | |
| 849 | +currently 50 lines long, to which you want to add the ~800 lines of | |
| 828 | 850 | [./branching.wiki | the branching document]. The equivalent |
| 829 | 851 | documentation in Git is the aggregation of the man pages for the above |
| 830 | -three commands, which is over 1000 lines, much of it mutually redundant. | |
| 852 | +three commands, which is approaching 1400 lines as of this writing, much of it mutually redundant. | |
| 831 | 853 | (e.g. Git's <tt>--edit</tt> and <tt>--no-commit</tt> options get |
| 832 | 854 | described three times, each time differently.) Fossil's |
| 833 | 855 | documentation is not only more concise, it gives a nice split of brief |
| 834 | 856 | online help and full online documentation. |
| 835 | 857 | |
| @@ -852,35 +874,42 @@ | ||
| 852 | 874 | This not |
| 853 | 875 | only solves the SHAttered problem, it should prevent a reoccurrence of |
| 854 | 876 | similar problems for the foreseeable future. |
| 855 | 877 | |
| 856 | 878 | Meanwhile, the Git community took until August 2018 to publish |
| 857 | -[https://git-scm.com/docs/hash-function-transition/|their first plan] | |
| 858 | -for solving the same problem by moving to SHA-256, a variant of the | |
| 859 | -[https://en.wikipedia.org/wiki/SHA-2 | older SHA-2 algorithm]. As of | |
| 860 | -this writing in February 2020, that plan hasn't been implemented, as far | |
| 861 | -as this author is aware, but there is now | |
| 862 | -[https://lwn.net/ml/git/[email protected]/ | |
| 863 | -| a competing SHA-256 based plan] which requires complete repository | |
| 864 | -conversion from SHA-1 to SHA-256, breaking all public hashes in the | |
| 865 | -repo. One way to characterize such a massive upheaval in Git terms is a | |
| 866 | -whole-project rebase, which violates the | |
| 867 | -[https://www.atlassian.com/git/tutorials/merging-vs-rebasing#the-golden-rule-of-rebasing|Golden Rule of Rebasing]. | |
| 868 | - | |
| 869 | -Regardless of the eventual implementation details, we fully expect Git | |
| 870 | -to move off SHA-1 eventually and for the changes to take years more to | |
| 871 | -percolate through the community. | |
| 872 | - | |
| 879 | +[https://git-scm.com/docs/hash-function-transition/ | their plan] for | |
| 880 | +solving the same problem by moving to SHA-256, a variant of the | |
| 881 | +[https://en.wikipedia.org/wiki/SHA-2 | older SHA-2 algorithm]. That is | |
| 882 | +now technically implemented in the sense that <tt>git init | |
| 883 | +--object-format=sha256</tt> exists, but note well: this is not only an | |
| 884 | +optional setting, Git forge support is mixed, most notably | |
| 885 | +[https://github.com/GitoxideLabs/gitoxide/issues/281 | lacking in | |
| 886 | +GitHub], plus also BitBucket and others. This is doubtless because of | |
| 887 | +this warning in the latest ([https://git-scm.com/docs/git-init/2.54.0 | | |
| 888 | +as of this writing]) <tt>git init</tt> docs: | |
| 889 | + | |
| 890 | +<blockquote>Note: At present, there is no interoperability between | |
| 891 | +SHA-256 repositories and SHA-1 repositories.</blockquote> | |
| 892 | + | |
| 893 | +Although we are now in the <i>tenth year</i> of this situation, there | |
| 894 | +remains hope that Git will manage to make the transition without taking | |
| 895 | +the full decade: the | |
| 896 | +[https://www.deployhq.com/blog/git-3-0-on-the-horizon-what-git-users-need-to-know-about-the-next-major-release | |
| 897 | +| latest plan] is that Git 3.0 will finally <i>(finally!)</i> switch to | |
| 898 | +SHA256 by default, forcing the issue. Given the track record, we are | |
| 899 | +taking a "show me" stance on this claim. | |
| 900 | + | |
| 901 | +Always remember, attacks only get better, never worse. | |
| 873 | 902 | Almost three years after Fossil solved this problem, the |
| 874 | 903 | [https://sha-mbles.github.io/ | SHAmbles attack] was published, further |
| 875 | 904 | weakening the case for continuing to use SHA-1. |
| 876 | 905 | |
| 877 | 906 | The practical impact of attacks like SHAttered and SHAmbles on the |
| 878 | 907 | Git and Fossil Merkle trees isn't clear, but you want to have your repositories |
| 879 | -moved over to a stronger hash algorithm before someone figures out how | |
| 880 | -to make use of the weaknesses in the old one. Fossil has had this covered | |
| 881 | -for years now, so that the solution is now almost universally deployed. | |
| 908 | +moved over to a stronger hash algorithm <i>before</i> someone figures out how | |
| 909 | +to make use of the weaknesses in the old one. Fossil's solution is long | |
| 910 | +since [https://repology.org/project/fossil/versions | universally deployed]. | |
| 882 | 911 | |
| 883 | 912 | <hr/> |
| 884 | 913 | |
| 885 | 914 | <h3>Asides and Digressions</h3> |
| 886 | 915 | |
| @@ -910,16 +939,10 @@ | ||
| 910 | 939 | lightweight web server, |
| 911 | 940 | <tt>[https://sqlite.org/althttpd/|althttpd]</tt>, |
| 912 | 941 | which is configured as a front end to Fossil running in CGI mode on |
| 913 | 942 | these sites. |
| 914 | 943 | |
| 915 | - <li><p>That estimate is based on pricing at Digital Ocean in | |
| 916 | - mid-2019: Fossil will run just fine on the smallest instance they | |
| 917 | - offer, at US $5/month, but the closest match to GitLab's minimum | |
| 918 | - requirements among Digital Ocean's offerings currently costs | |
| 919 | - $40/month. | |
| 920 | - | |
| 921 | 944 | <li><p>This means you can give up waiting for Fossil to be ported to |
| 922 | 945 | the PDP-11, but we remain hopeful that someone may eventually port |
| 923 | 946 | it to [https://en.wikipedia.org/wiki/Z/OS|z/OS]. |
| 924 | 947 | |
| 925 | 948 | <li><p>"Why is there all this Tcl in and around Fossil?" you may |
| 926 | 949 |
| --- www/fossil-v-git.wiki | |
| +++ www/fossil-v-git.wiki | |
| @@ -20,12 +20,12 @@ | |
| 20 | In this document, we set all of that similarity and interoperability |
| 21 | aside and focus on the important differences between the two, especially |
| 22 | those that impact the user experience. |
| 23 | |
| 24 | Keep in mind that you are reading this on a Fossil website, and though |
| 25 | we try to be fair, the information here |
| 26 | might be biased in favor of Fossil, if only because we spend most of our |
| 27 | time using Fossil, not Git. Ask around for second opinions from |
| 28 | people who have used <em>both</em> Fossil and Git. |
| 29 | |
| 30 | If you want a more practical, less philosophical guide to moving from |
| 31 | Git to Fossil, see our [./gitusers.md | Git to Fossil Translation Guide]. |
| @@ -178,36 +178,40 @@ | |
| 178 | This policy is particularly useful when running Fossil inside a |
| 179 | restrictive container, anything from [./chroot.md | classic chroot |
| 180 | jails] to modern [https://en.wikipedia.org/wiki/OS-level_virtualization |
| 181 | | OS-level virtualization mechanisms] such as |
| 182 | [https://en.wikipedia.org/wiki/Docker_(software) | Docker]. |
| 183 | Our [./containers.md | stock container image] is under 8 MB when |
| 184 | uncompressed and running. It contains nothing but a single |
| 185 | statically-linked binary. |
| 186 | |
| 187 | If you build a dynamically linked binary instead, Fossil's on-disk size |
| 188 | drops to around 6 MB, and it's dependent only on widespread |
| 189 | platform libraries with stable ABIs such as glibc, zlib, and openssl. |
| 190 | |
| 191 | Full static linking is easier on Windows, so our precompiled Windows |
| 192 | binaries are just a ZIP archive |
| 193 | containing only "<tt>fossil.exe</tt>". There is no "<tt>setup.exe</tt>" |
| 194 | to run. |
| 195 | |
| 196 | Fossil is easy to build from sources. Just run |
| 197 | "<tt>./configure && make</tt>" on POSIX systems and |
| 198 | "<tt>nmake /f Makefile.msc</tt>" on Windows. |
| 199 | |
| 200 | Contrast a basic installation of Git, which takes up about |
| 201 | 15 MiB on Debian 10 across 230 files, not counting the contents of |
| 202 | <tt>/usr/share/doc</tt> or <tt>/usr/share/locale</tt>. If you need to |
| 203 | deploy to any platform where you cannot count on facilities like the POSIX |
| 204 | shell, Perl interpreter, and Tcl/Tk platform needed to fully use Git |
| 205 | as part of the base platform, the full footprint of a Git installation |
| 206 | extends to more like 45 MiB and thousands of files. This complicates |
| 207 | several common scenarios: Git for Windows, chrooted Git servers, |
| 208 | Docker images... |
| 209 | |
| 210 | Some say that Git more closely adheres to the Unix philosophy, |
| 211 | summarized as "many small tools, loosely joined," but we have many |
| 212 | examples of other successful Unix software that violates that principle |
| 213 | to good effect, from Apache to Python to ZFS. We can infer from that |
| @@ -216,26 +220,47 @@ | |
| 216 | matters is effectiveness and efficiency. We believe Fossil achieves |
| 217 | this. |
| 218 | |
| 219 | The above size comparisons aren't apples-to-apples anyway. We've |
| 220 | compared the size of Fossil with all of its [#features | many built-in |
| 221 | features] to a fairly minimal Git installation. You must add a lot of |
| 222 | third-party software to Git to give it a Fossil-equivalent feature set. |
| 223 | Consider [https://about.gitlab.com/|GitLab], a third-party extension to |
| 224 | Git wrapping it in many features, making it roughly Fossil-equivalent, |
| 225 | though [https://docs.gitlab.com/ee/install/requirements.html|much more |
| 226 | resource hungry] and hence more costly to run than the equivalent Fossil |
| 227 | setup. [https://hub.docker.com/r/gitlab/gitlab-ce/ | The official GitLab |
| 228 | Community Edition container] currently clocks in at 2.66 GiB! |
| 229 | |
| 230 | GitLab's requirements are easy to accept when you're dedicating |
| 231 | a local rack server or blade to it, since its minimum requirements are |
| 232 | more or less a description of the smallest |
| 233 | thing you could call a "server" these days, but when you go to host that |
| 234 | in the cloud, you can expect to pay about 8 times as much to comfortably host |
| 235 | GitLab as for Fossil.³ This difference is largely due to basic |
| 236 | technology choices: Ruby and PostgreSQL vs C and SQLite. |
| 237 | |
| 238 | The Fossil project itself is [./selfhost.wiki|hosted on a small and |
| 239 | inexpensive VPS]. A bare-bones $5/month VPS or a |
| 240 | spare Raspberry Pi is sufficient to run a full-up project |
| 241 | site, complete with tickets, wiki, chat, and forum, in addition to |
| @@ -255,33 +280,34 @@ | |
| 255 | <tt>.git</tt> folder or compressed into bespoke key/value |
| 256 | [https://git-scm.com/book/en/v2/Git-Internals-Packfiles|pack-files], |
| 257 | whereas Fossil stores its objects in a [https://www.sqlite.org/|SQLite] |
| 258 | database file which provides ACID transactions and a high-level query |
| 259 | language. |
| 260 | This difference is more than an implementation detail. It has important |
| 261 | practical consequences. |
| 262 | |
| 263 | One notable consequence is that it is difficult to find the descendants |
| 264 | of check-ins in Git. |
| 265 | One can easily locate the ancestors of a particular Git check-in |
| 266 | by following the pointers embedded in the check-in object, but it is |
| 267 | difficult to go the other direction and locate the descendants of a |
| 268 | check-in. It is so difficult, in fact, that neither native Git nor |
| 269 | GitHub provide this capability short of crawling the |
| 270 | [https://www.git-scm.com/docs/git-log|commit log]. With Fossil, |
| 271 | on the other hand, finding descendants is a simple SQL query. |
| 272 | It is common in Fossil to ask to see |
| 273 | [/timeline?df=release&y=ci|all check-ins since the last release]. |
| 274 | Git lets you see "what came before". Fossil makes it just as |
| 275 | easy to also see "what came after". |
| 276 | |
| 277 | Leaf check-ins in Git that lack a "ref" become "detached," making them |
| 278 | difficult to locate and subject to garbage collection. This |
| 279 | [https://stackoverflow.com/q/3965676 | detached head |
| 280 | state] problem has caused grief for |
| 281 | [https://www.google.com/search?q=git+detached+head+state | many |
| 282 | Git users]. With |
| 283 | Fossil, detached heads are simply impossible because we can always find |
| 284 | our way back into the Merkle tree using one or more of the relations |
| 285 | in the SQL database. |
| 286 | |
| 287 | The SQL query capabilities of Fossil make it easier to track the |
| @@ -288,34 +314,27 @@ | |
| 288 | changes for one particular file within a project. For example, |
| 289 | you can easily find |
| 290 | [/finfo/www/fossil-v-git.wiki|the complete edit history of this one document], |
| 291 | or even |
| 292 | [/finfo/www/fossil-v-git.wiki?ubg|the same history color-coded by committer], |
| 293 | Both questions are simple SQL query in Fossil, with procedural code |
| 294 | only being used to format the result for display. |
| 295 | The same result could be obtained from Git, but because the data is |
| 296 | in a key/value store, much more procedural code has to be written to |
| 297 | walk the data and compute the result. And since that is a lot more |
| 298 | work, the question is seldom asked. |
| 299 | |
| 300 | The ease of querying Fossil data using SQL means that status or |
| 301 | history information about the project under management is easier |
| 302 | to obtain. Being easier means that it is more likely to happen. |
| 303 | Fossil reports tend to be more detailed and useful. |
| 304 | Compare [/timeline?c=6df7a853ec16865b|this Fossil timeline] |
| 305 | to |
| 306 | [https://github.com/drhsqlite/fossil-mirror/commits/master?after=f720c106d297ca1f61bccb30c5c191b88a626d01+34 | |
| 307 | its closest equivalent in GitHub]. Judge for yourself: which of those |
| 308 | reports is more useful to a developer trying to understand what happened? |
| 309 | |
| 310 | The bottom line is that even though Fossil and Git are built around |
| 311 | the same low-level data structure, the use of SQL |
| 312 | to query this data makes the data more accessible in Fossil, resulting |
| 313 | in more detailed information being available to the user. This |
| 314 | improves situational awareness and makes working on the project |
| 315 | easier. |
| 316 | |
| 317 | <h3 id="portable">2.4 Portable</h3> |
| 318 | |
| 319 | Fossil is largely written in ISO C, almost purely conforming to the |
| 320 | original 1989 standard. We make very little use of |
| 321 | [https://en.wikipedia.org/wiki/C99|C99], and we do not knowingly make |
| @@ -326,11 +345,11 @@ | |
| 326 | facilities Fossil needs to do its thing. (Network sockets, file locking, |
| 327 | etc.) There are certainly well-known platforms Fossil hasn't been ported |
| 328 | to yet, but that's most likely due to lack of interest rather than |
| 329 | inherent difficulties in doing the port. We believe the most stringent |
| 330 | limit on its portability is that it assumes at least a 32-bit CPU and |
| 331 | several megs of flat-addressed memory.⁴ Fossil isn't quite as |
| 332 | [https://www.sqlite.org/custombuild.html|portable as SQLite], but it's |
| 333 | close. |
| 334 | |
| 335 | Over half of the C code in Fossil is actually an embedded copy of the |
| 336 | current version of SQLite. Much of what is Fossil-specific after you set |
| @@ -344,20 +363,20 @@ | |
| 344 | necessary]. The server-side |
| 345 | UI scripting uses a custom minimal |
| 346 | [https://en.wikipedia.org/wiki/Tcl|Tcl] dialect called |
| 347 | [./th1.md|TH1], which is |
| 348 | embedded into Fossil itself. Fossil's build system and test suite are |
| 349 | largely based on Tcl.⁵ All of this is quite portable. |
| 350 | |
| 351 | About half of Git's code is POSIX C, and about a third is POSIX shell |
| 352 | code. This is largely why the so-called "Git for Windows" distributions |
| 353 | (both [https://git-scm.com/download/win|first-party] and |
| 354 | [https://gitforwindows.org/|third-party]) are actually an |
| 355 | [https://www.msys2.org/wiki/Home/|MSYS POSIX portability environment] bundled |
| 356 | with all of the Git stuff, because it would be too painful to port Git |
| 357 | natively to Windows. Git is a foreign citizen on Windows, speaking to it |
| 358 | only through a translator.⁶ |
| 359 | |
| 360 | While Fossil does lean toward POSIX norms when given a choice — LF-only |
| 361 | line endings are treated as first-class citizens over CR+LF, for example |
| 362 | — the Windows build of Fossil is truly native. |
| 363 | |
| @@ -444,11 +463,11 @@ | |
| 444 | [https://www.git-scm.com/docs/git-request-pull|pull requests] offer |
| 445 | a low-friction path to accepting |
| 446 | [https://www.jonobacon.com/2012/07/25/building-strong-community-structural-integrity/|drive-by |
| 447 | contributions]. Fossil's closest equivalents are its unique |
| 448 | [/help/bundle|bundle] and [/help/patch|patch] features, which require higher engagement |
| 449 | than firing off a PR.⁷ This difference comes directly from the |
| 450 | initial designed purpose for each tool: the SQLite project doesn't |
| 451 | accept outside contributions from previously-unknown developers, but |
| 452 | the Linux kernel does. |
| 453 | |
| 454 | * <b>No rebasing:</b> When your local repo clone syncs changes |
| @@ -500,16 +519,16 @@ | |
| 500 | that everyone — especially the project leader — can maintain a better |
| 501 | mental picture of what is happening, leading to better situational |
| 502 | awareness. |
| 503 | |
| 504 | By contrast, "…[https://docs.github.com/en/get-started/quickstart/contributing-to-projects|forking is |
| 505 | at the core of social coding at GitHub]". As of January 2022, |
| 506 | [https://github.com/search?q=is:public|Github hosts 47 million distinct |
| 507 | software projects], most of which were created by forking a |
| 508 | previously-existing project. Since this is |
| 509 | [https://evansdata.com/reports/viewRelease.php?reportID=9 | roughly |
| 510 | twice the number of developers in the world], it beggars belief that |
| 511 | most of these forks are still under active development. The vast bulk |
| 512 | of these must be abandoned one-off efforts. This is part of the nature |
| 513 | of bazaar style development. |
| 514 | |
| 515 | You can think about this difference in terms of |
| @@ -533,18 +552,18 @@ | |
| 533 | <h4 id="scale">2.5.2 Scale</h4> |
| 534 | |
| 535 | The Linux kernel has a far bigger developer community than that of |
| 536 | SQLite: there are thousands and thousands of contributors to Linux, most |
| 537 | of whom do not know each other's names. These thousands are responsible |
| 538 | for producing roughly 89× more code than is in SQLite. (10.7 |
| 539 | [https://en.wikipedia.org/wiki/Source_lines_of_code|MLOC] vs. 0.12 MLOC |
| 540 | according to [https://dwheeler.com/sloccount/|SLOCCount].) The Linux |
| 541 | kernel and its development process were already uncommonly large back in |
| 542 | 2005 when Git was designed, specifically to support the consequences of |
| 543 | having such a large set of developers working on such a large code base. |
| 544 | |
| 545 | 95% of the code in SQLite comes from just four programmers, and 64% of |
| 546 | it is from the lead developer alone. The SQLite developers know each |
| 547 | other well and interact daily. Fossil was designed for this development |
| 548 | model. |
| 549 | |
| 550 | When choosing your DVCS, we think you should ask yourself whether the |
| @@ -564,35 +583,33 @@ | |
| 564 | |
| 565 | Both Fossil and Git store history as a directed acyclic graph (DAG) |
| 566 | of changes, but Git tends to focus more on individual branches of |
| 567 | the DAG, whereas Fossil puts more emphasis on the entire DAG. |
| 568 | |
| 569 | For example, the default behavior in Git is to only synchronize |
| 570 | a single branch, whereas with Fossil the only sync option is to |
| 571 | sync the entire DAG. Git commands, |
| 572 | GitHub, and GitLab tend to show only a single branch at |
| 573 | a time, whereas Fossil usually shows all parallel branches at |
| 574 | once. Git has commands like "rebase" that help keep all relevant |
| 575 | changes on a single branch, whereas Fossil encourages a style of |
| 576 | many concurrent branches constantly springing into existence, |
| 577 | undergoing active development in parallel for a few days or weeks, then |
| 578 | merging back into the main line and disappearing. |
| 579 | |
| 580 | This difference in emphasis arises from the different purposes of |
| 581 | the two systems. Git focuses on individual branches, because that |
| 582 | is exactly what you want for a highly-distributed bazaar-style project |
| 583 | such as Linux. Linus Torvalds does not want to see every check-in |
| 584 | by every contributor to Linux: such extreme visibility does not scale |
| 585 | well. Contrast Fossil, which was written for the cathedral-style SQLite project |
| 586 | and its handful of active committers. Seeing all |
| 587 | changes on all branches all at once helps keep the whole team |
| 588 | up-to-date with what everybody else is doing, resulting in a more |
| 589 | tightly focused and cohesive implementation. |
| 590 | |
| 591 | Parts of this section are [https://fossil-scm.org/forum/forumpost/5961e969fa|disputed] |
| 592 | by [https://github.com/olorin37|Jakub A. G.]. |
| 593 | |
| 594 | |
| 595 | <h3 id="checkouts">2.6 One vs. Many Check-outs per Repository</h3> |
| 596 | |
| 597 | Because Git commingles the repository data with the initial checkout of |
| 598 | that repository, the default mode of operation in Git is to stick to that |
| @@ -612,20 +629,28 @@ | |
| 612 | standard advice is to use a switch-in-place workflow in Fossil when |
| 613 | the disturbance from switching branches is small, and to use multiple |
| 614 | checkouts when you have long-lived working branches that are different |
| 615 | enough that switching in place is disruptive. |
| 616 | |
| 617 | While you can [./gitusers.md#worktree | use Git in the Fossil style], |
| 618 | Git's default tie between working directory and |
| 619 | repository means the standard method for working with a Git repo is to |
| 620 | have one working directory only. Most Git tutorials teach this style, so |
| 621 | it is how most people learn to use Git. Because relatively few people |
| 622 | use Git with multiple working directories per repository, there are |
| 623 | [https://duckduckgo.com/?q=git+worktree+problem | several known |
| 624 | problems] with that way of working, problems which don't happen in Fossil because of |
| 625 | the clear [./ckout-workflows.md | separation] between a Fossil repository and |
| 626 | each working directory. |
| 627 | |
| 628 | This distinction matters because switching branches inside a single working directory loses local context |
| 629 | on each switch. |
| 630 | |
| 631 | For instance, in any software project where the runnable program must be |
| @@ -658,13 +683,10 @@ | |
| 658 | |
| 659 | Plus, |
| 660 | <tt>cd</tt> is faster to type than <tt>git checkout</tt> or <tt>fossil |
| 661 | update</tt>. |
| 662 | |
| 663 | Parts of this section are [https://fossil-scm.org/forum/forumpost/5961e969fa|disputed] |
| 664 | by [https://github.com/olorin37|Jakub A. G.]. |
| 665 | |
| 666 | <h3 id="history">2.7 What you should have done vs. What you actually did</h3> |
| 667 | |
| 668 | Git puts a lot of emphasis on maintaining |
| 669 | a "clean" check-in history. Extraneous and experimental branches by |
| 670 | individual developers often never make it into the main repository. |
| @@ -822,14 +844,14 @@ | |
| 822 | concepts to keep track of in your mental model of Fossil's internal |
| 823 | operation. |
| 824 | |
| 825 | Fossil's implementation of the feature is also simpler to describe. The |
| 826 | brief online help for <tt>[/help/merge | fossil merge]</tt> is |
| 827 | currently 41 lines long, to which you want to add the 600 lines of |
| 828 | [./branching.wiki | the branching document]. The equivalent |
| 829 | documentation in Git is the aggregation of the man pages for the above |
| 830 | three commands, which is over 1000 lines, much of it mutually redundant. |
| 831 | (e.g. Git's <tt>--edit</tt> and <tt>--no-commit</tt> options get |
| 832 | described three times, each time differently.) Fossil's |
| 833 | documentation is not only more concise, it gives a nice split of brief |
| 834 | online help and full online documentation. |
| 835 | |
| @@ -852,35 +874,42 @@ | |
| 852 | This not |
| 853 | only solves the SHAttered problem, it should prevent a reoccurrence of |
| 854 | similar problems for the foreseeable future. |
| 855 | |
| 856 | Meanwhile, the Git community took until August 2018 to publish |
| 857 | [https://git-scm.com/docs/hash-function-transition/|their first plan] |
| 858 | for solving the same problem by moving to SHA-256, a variant of the |
| 859 | [https://en.wikipedia.org/wiki/SHA-2 | older SHA-2 algorithm]. As of |
| 860 | this writing in February 2020, that plan hasn't been implemented, as far |
| 861 | as this author is aware, but there is now |
| 862 | [https://lwn.net/ml/git/[email protected]/ |
| 863 | | a competing SHA-256 based plan] which requires complete repository |
| 864 | conversion from SHA-1 to SHA-256, breaking all public hashes in the |
| 865 | repo. One way to characterize such a massive upheaval in Git terms is a |
| 866 | whole-project rebase, which violates the |
| 867 | [https://www.atlassian.com/git/tutorials/merging-vs-rebasing#the-golden-rule-of-rebasing|Golden Rule of Rebasing]. |
| 868 | |
| 869 | Regardless of the eventual implementation details, we fully expect Git |
| 870 | to move off SHA-1 eventually and for the changes to take years more to |
| 871 | percolate through the community. |
| 872 | |
| 873 | Almost three years after Fossil solved this problem, the |
| 874 | [https://sha-mbles.github.io/ | SHAmbles attack] was published, further |
| 875 | weakening the case for continuing to use SHA-1. |
| 876 | |
| 877 | The practical impact of attacks like SHAttered and SHAmbles on the |
| 878 | Git and Fossil Merkle trees isn't clear, but you want to have your repositories |
| 879 | moved over to a stronger hash algorithm before someone figures out how |
| 880 | to make use of the weaknesses in the old one. Fossil has had this covered |
| 881 | for years now, so that the solution is now almost universally deployed. |
| 882 | |
| 883 | <hr/> |
| 884 | |
| 885 | <h3>Asides and Digressions</h3> |
| 886 | |
| @@ -910,16 +939,10 @@ | |
| 910 | lightweight web server, |
| 911 | <tt>[https://sqlite.org/althttpd/|althttpd]</tt>, |
| 912 | which is configured as a front end to Fossil running in CGI mode on |
| 913 | these sites. |
| 914 | |
| 915 | <li><p>That estimate is based on pricing at Digital Ocean in |
| 916 | mid-2019: Fossil will run just fine on the smallest instance they |
| 917 | offer, at US $5/month, but the closest match to GitLab's minimum |
| 918 | requirements among Digital Ocean's offerings currently costs |
| 919 | $40/month. |
| 920 | |
| 921 | <li><p>This means you can give up waiting for Fossil to be ported to |
| 922 | the PDP-11, but we remain hopeful that someone may eventually port |
| 923 | it to [https://en.wikipedia.org/wiki/Z/OS|z/OS]. |
| 924 | |
| 925 | <li><p>"Why is there all this Tcl in and around Fossil?" you may |
| 926 |
| --- www/fossil-v-git.wiki | |
| +++ www/fossil-v-git.wiki | |
| @@ -20,12 +20,12 @@ | |
| 20 | In this document, we set all of that similarity and interoperability |
| 21 | aside and focus on the important differences between the two, especially |
| 22 | those that impact the user experience. |
| 23 | |
| 24 | Keep in mind that you are reading this on a Fossil website, and though |
| 25 | we try to be fair, the information here will inevitably |
| 26 | be biased in favor of Fossil purely because we spend most of our |
| 27 | time using Fossil, not Git. Ask around for second opinions from |
| 28 | people who have used <em>both</em> Fossil and Git. |
| 29 | |
| 30 | If you want a more practical, less philosophical guide to moving from |
| 31 | Git to Fossil, see our [./gitusers.md | Git to Fossil Translation Guide]. |
| @@ -178,36 +178,40 @@ | |
| 178 | This policy is particularly useful when running Fossil inside a |
| 179 | restrictive container, anything from [./chroot.md | classic chroot |
| 180 | jails] to modern [https://en.wikipedia.org/wiki/OS-level_virtualization |
| 181 | | OS-level virtualization mechanisms] such as |
| 182 | [https://en.wikipedia.org/wiki/Docker_(software) | Docker]. |
| 183 | Our [./containers.md | stock container image] is under 11 MB when |
| 184 | uncompressed and running because it contains nothing but a single |
| 185 | statically-linked binary. |
| 186 | |
| 187 | If you build a dynamically linked binary instead, a Linux |
| 188 | x86_64 build of Fossil drops to under 5 MB on-disk, stripped. |
| 189 | It will depend only on widespread |
| 190 | platform libraries with stable ABIs such as glibc, zlib, and openssl. |
| 191 | |
| 192 | Much the same is true on Windows, where our precompiled static binaries |
| 193 | are distributed inside a ZIP archive containing "<tt>fossil.exe</tt>" |
| 194 | and not a thing else, with a size on-par that of the Linux container |
| 195 | build. There is no "<tt>setup.exe</tt>" to run; just copy it into your |
| 196 | <tt>%PATH%</tt>. |
| 197 | |
| 198 | Fossil is easy to build from sources. Just run |
| 199 | "<tt>./configure && make</tt>" on POSIX systems and |
| 200 | "<tt>nmake /f Makefile.msc</tt>" on Windows. |
| 201 | |
| 202 | Git, by contrast, takes 25 MiB on Ubuntu 26.04 across 940 files. |
| 203 | That doesn't count platform facilities like the POSIX shell and script |
| 204 | interpreters its full feature set requires. A fairer comparison to |
| 205 | Fossil's single static binary container is the Docker Hardened Image for |
| 206 | [https://hub.docker.com/hardened-images/catalog/dhi/git |Git 2.x on |
| 207 | Alpine], which presently weighs in at 36.3 megs unpacked and 607 files, |
| 208 | if you count all the <tt>busybox</tt> symlinks which would otherwise be |
| 209 | separate binaries on a more traditional Linux system. Worst of all are |
| 210 | the "Git for Windows" packages where they end up needing to ship a large |
| 211 | yet nerfed Linux-like userland to support Git's many loosely coupled |
| 212 | pieces, approaching a hundred megs and thousands of files. |
| 213 | |
| 214 | Some say that Git more closely adheres to the Unix philosophy, |
| 215 | summarized as "many small tools, loosely joined," but we have many |
| 216 | examples of other successful Unix software that violates that principle |
| 217 | to good effect, from Apache to Python to ZFS. We can infer from that |
| @@ -216,26 +220,47 @@ | |
| 220 | matters is effectiveness and efficiency. We believe Fossil achieves |
| 221 | this. |
| 222 | |
| 223 | The above size comparisons aren't apples-to-apples anyway. We've |
| 224 | compared the size of Fossil with all of its [#features | many built-in |
| 225 | features] to a relatively bare-bones Git installation. You must add a |
| 226 | lot of third-party software to Git to give it a Fossil-equivalent |
| 227 | feature set. |
| 228 | |
| 229 | Consider [https://about.gitlab.com/|GitLab], which wraps Git in enough |
| 230 | features to bring it to a rough (very rough) parity with Fossil. While |
| 231 | it is certainly ahead in some areas — automation comes to mind — it must |
| 232 | be noted that GitLab's own forums are on Discourse, not self-hosted as |
| 233 | with Fossil. You may then argue that Discourse is superior to the Fossil |
| 234 | forum feature, but this merely brings us back to another of Fossil's |
| 235 | advantages: when the posts are part of the repo, you can migrate your |
| 236 | entire project to another host merely by standing that repo back up on |
| 237 | other hardware. Choosing to assemble your project hosting from multiple |
| 238 | pieces requires each to have their own backups, their own management |
| 239 | processes, and their own migration strategies. |
| 240 | |
| 241 | Even then, these disparate services do not cooperate at the same level |
| 242 | as in Fossil, where the ability to have a forum post linking to a wiki |
| 243 | article linking to a trouble ticket linking to a commit falls out of the |
| 244 | model nearly for free. These are all internal links, mind, potentially |
| 245 | using nothing but repository artifact hashes, all backed by SQLite's |
| 246 | referential integrity. With something like GitLab, its "internal |
| 247 | references" are tacked on after the fact, not Git repo hashes at all, |
| 248 | and when it comes to external services like Discourse, history tells us |
| 249 | you're storing up tech debt which will come due at some future point in |
| 250 | the form of piles of broken links when one piece or the other needs |
| 251 | changing out. |
| 252 | |
| 253 | Furthermore, GitLab is far more resource hungry even in its |
| 254 | [https://docs.gitlab.com/omnibus/settings/memory_constrained_envs/ | |
| 255 | minimal configuration], hence more costly to run than the equivalent |
| 256 | Fossil setup. [https://hub.docker.com/r/gitlab/gitlab-ce/ | The |
| 257 | official GitLab Community Edition container] currently clocks in at |
| 258 | 3.44 GiB, independent of add-ons like Discourse. You can expect it to |
| 259 | cost around 8× as much to host it on a cloud service. Even pared down |
| 260 | to the minimum, there remain the consequences from the difference in |
| 261 | basic technology choices: Ruby and PostgreSQL vs C and SQLite. |
| 262 | |
| 263 | The Fossil project itself is [./selfhost.wiki|hosted on a small and |
| 264 | inexpensive VPS]. A bare-bones $5/month VPS or a |
| 265 | spare Raspberry Pi is sufficient to run a full-up project |
| 266 | site, complete with tickets, wiki, chat, and forum, in addition to |
| @@ -255,33 +280,34 @@ | |
| 280 | <tt>.git</tt> folder or compressed into bespoke key/value |
| 281 | [https://git-scm.com/book/en/v2/Git-Internals-Packfiles|pack-files], |
| 282 | whereas Fossil stores its objects in a [https://www.sqlite.org/|SQLite] |
| 283 | database file which provides ACID transactions and a high-level query |
| 284 | language. |
| 285 | |
| 286 | This difference is more than an implementation detail; it has important |
| 287 | practical consequences. |
| 288 | |
| 289 | One notable example is that it is difficult to find the descendants |
| 290 | of check-ins in Git. |
| 291 | One can easily locate the ancestors of a particular Git check-in |
| 292 | by following the pointers embedded in the check-in object, but |
| 293 | going the other direction is difficult enough |
| 294 | that neither native Git nor the big "forge" facilities |
| 295 | like GitHub and GitLab provide this capability short of crawling the |
| 296 | [https://www.git-scm.com/docs/git-log|commit log]. In Fossil, |
| 297 | we can find descendants using a simple SQL query, which then allows |
| 298 | us to see [/timeline?df=release&y=ci|all check-ins since the last release], |
| 299 | as but one example. |
| 300 | Git lets you see "what came before," but Fossil makes it just as |
| 301 | easy to also see "what came after." |
| 302 | |
| 303 | Leaf check-ins in Git that lack a "ref" become "detached," making them |
| 304 | difficult to locate and subject to garbage collection. This |
| 305 | [https://stackoverflow.com/q/3965676 | detached head |
| 306 | state] problem has caused grief for |
| 307 | [https://www.google.com/search?q=git+detached+head+state | |
| 308 | untold millions of Git users]. With |
| 309 | Fossil, detached heads are simply impossible because we can always find |
| 310 | our way back into the Merkle tree using one or more of the relations |
| 311 | in the SQL database. |
| 312 | |
| 313 | The SQL query capabilities of Fossil make it easier to track the |
| @@ -288,34 +314,27 @@ | |
| 314 | changes for one particular file within a project. For example, |
| 315 | you can easily find |
| 316 | [/finfo/www/fossil-v-git.wiki|the complete edit history of this one document], |
| 317 | or even |
| 318 | [/finfo/www/fossil-v-git.wiki?ubg|the same history color-coded by committer], |
| 319 | Both come down to simple SQL queries in Fossil, with procedural code |
| 320 | only being used to format the result for display. |
| 321 | The same result could be obtained from Git, but because the data is |
| 322 | in a key/value store, much more procedural code has to be written to |
| 323 | walk the data and compute the result. |
| 324 | |
| 325 | The ease of querying Fossil data using SQL means that status or |
| 326 | history information about the project under management is easier |
| 327 | to obtain, hence more likely to happen, giving its developers better |
| 328 | situational awareness. |
| 329 | Fossil reports tend to be more detailed and useful. |
| 330 | Compare [/timeline?c=6df7a853ec16865b|this Fossil timeline] |
| 331 | to |
| 332 | [https://github.com/drhsqlite/fossil-mirror/commits/master?after=f720c106d297ca1f61bccb30c5c191b88a626d01+34 | |
| 333 | its closest equivalent in the GitHub mirror]. Judge for yourself: which of those |
| 334 | reports is more useful to a developer trying to understand what happened? |
| 335 | |
| 336 | <h3 id="portable">2.4 Portable</h3> |
| 337 | |
| 338 | Fossil is largely written in ISO C, almost purely conforming to the |
| 339 | original 1989 standard. We make very little use of |
| 340 | [https://en.wikipedia.org/wiki/C99|C99], and we do not knowingly make |
| @@ -326,11 +345,11 @@ | |
| 345 | facilities Fossil needs to do its thing. (Network sockets, file locking, |
| 346 | etc.) There are certainly well-known platforms Fossil hasn't been ported |
| 347 | to yet, but that's most likely due to lack of interest rather than |
| 348 | inherent difficulties in doing the port. We believe the most stringent |
| 349 | limit on its portability is that it assumes at least a 32-bit CPU and |
| 350 | several megs of flat-addressed memory.³ Fossil isn't quite as |
| 351 | [https://www.sqlite.org/custombuild.html|portable as SQLite], but it's |
| 352 | close. |
| 353 | |
| 354 | Over half of the C code in Fossil is actually an embedded copy of the |
| 355 | current version of SQLite. Much of what is Fossil-specific after you set |
| @@ -344,20 +363,20 @@ | |
| 363 | necessary]. The server-side |
| 364 | UI scripting uses a custom minimal |
| 365 | [https://en.wikipedia.org/wiki/Tcl|Tcl] dialect called |
| 366 | [./th1.md|TH1], which is |
| 367 | embedded into Fossil itself. Fossil's build system and test suite are |
| 368 | largely based on Tcl.⁴ All of this is quite portable. |
| 369 | |
| 370 | About half of Git's code is POSIX C, and about a third is POSIX shell |
| 371 | code. This is largely why the so-called "Git for Windows" distributions |
| 372 | (both [https://git-scm.com/download/win|first-party] and |
| 373 | [https://gitforwindows.org/|third-party]) are actually an |
| 374 | [https://www.msys2.org/wiki/Home/|MSYS POSIX portability environment] bundled |
| 375 | with all of the Git stuff, because it would be too painful to port Git |
| 376 | natively to Windows. Git is a foreign citizen on Windows, speaking to it |
| 377 | only through a translator.⁵ |
| 378 | |
| 379 | While Fossil does lean toward POSIX norms when given a choice — LF-only |
| 380 | line endings are treated as first-class citizens over CR+LF, for example |
| 381 | — the Windows build of Fossil is truly native. |
| 382 | |
| @@ -444,11 +463,11 @@ | |
| 463 | [https://www.git-scm.com/docs/git-request-pull|pull requests] offer |
| 464 | a low-friction path to accepting |
| 465 | [https://www.jonobacon.com/2012/07/25/building-strong-community-structural-integrity/|drive-by |
| 466 | contributions]. Fossil's closest equivalents are its unique |
| 467 | [/help/bundle|bundle] and [/help/patch|patch] features, which require higher engagement |
| 468 | than firing off a PR.⁶ This difference comes directly from the |
| 469 | initial designed purpose for each tool: the SQLite project doesn't |
| 470 | accept outside contributions from previously-unknown developers, but |
| 471 | the Linux kernel does. |
| 472 | |
| 473 | * <b>No rebasing:</b> When your local repo clone syncs changes |
| @@ -500,16 +519,16 @@ | |
| 519 | that everyone — especially the project leader — can maintain a better |
| 520 | mental picture of what is happening, leading to better situational |
| 521 | awareness. |
| 522 | |
| 523 | By contrast, "…[https://docs.github.com/en/get-started/quickstart/contributing-to-projects|forking is |
| 524 | at the core of social coding at GitHub]". As of June 2026, |
| 525 | [https://github.com/search?q=is:public|Github hosts 324 million distinct |
| 526 | software projects], most of which were created by forking a |
| 527 | previously-existing project. Since this is |
| 528 | [https://www.griddynamics.com/blog/number-software-developers-world | |
| 529 | ~11× the number of developers in the world], it beggars belief that |
| 530 | most of these forks are still under active development. The vast bulk |
| 531 | of these must be abandoned one-off efforts. This is part of the nature |
| 532 | of bazaar style development. |
| 533 | |
| 534 | You can think about this difference in terms of |
| @@ -533,18 +552,18 @@ | |
| 552 | <h4 id="scale">2.5.2 Scale</h4> |
| 553 | |
| 554 | The Linux kernel has a far bigger developer community than that of |
| 555 | SQLite: there are thousands and thousands of contributors to Linux, most |
| 556 | of whom do not know each other's names. These thousands are responsible |
| 557 | for producing roughly 73× more code than is in SQLite. (32.0 |
| 558 | [https://en.wikipedia.org/wiki/Source_lines_of_code|MLOC] vs. 0.44 MLOC |
| 559 | according to [https://github.com/boyter/scc | scc].) The Linux |
| 560 | kernel and its development process were already uncommonly large back in |
| 561 | 2005 when Git was designed, specifically to support the consequences of |
| 562 | having such a large set of developers working on such a large code base. |
| 563 | |
| 564 | 95% of the code in SQLite comes from just six programmers, and 62% of |
| 565 | it is from the lead developer alone. The SQLite developers know each |
| 566 | other well and interact daily. Fossil was designed for this development |
| 567 | model. |
| 568 | |
| 569 | When choosing your DVCS, we think you should ask yourself whether the |
| @@ -564,35 +583,33 @@ | |
| 583 | |
| 584 | Both Fossil and Git store history as a directed acyclic graph (DAG) |
| 585 | of changes, but Git tends to focus more on individual branches of |
| 586 | the DAG, whereas Fossil puts more emphasis on the entire DAG. |
| 587 | |
| 588 | While a common usage pattern in Git is to only synchronize |
| 589 | a single branch — <tt>git pull upstream feature/branch</tt> — instead |
| 590 | of all refs, Fossil does not give you a choice; it |
| 591 | syncs the entire DAG or nothing. Git commands, |
| 592 | GitHub, and GitLab tend to show only a single branch at |
| 593 | a time, whereas Fossil usually shows all parallel branches at |
| 594 | once. Git has commands like "rebase" that help keep all relevant |
| 595 | changes on a single branch, whereas Fossil encourages a style of |
| 596 | many concurrent branches constantly springing into existence, |
| 597 | undergoing active development in parallel for a few days or weeks, then |
| 598 | merging back into the main line and disappearing. |
| 599 | |
| 600 | This difference in emphasis arises from the different purposes of |
| 601 | the two systems. Git's focus on individual branches |
| 602 | is exactly what you want for a highly-distributed bazaar-style project |
| 603 | such as Linux. Linus Torvalds does not want to see every check-in |
| 604 | by every contributor to Linux: such extreme visibility does not scale |
| 605 | well. Contrast Fossil, which was written for the cathedral-style SQLite project |
| 606 | and its handful of active committers. Seeing all |
| 607 | changes on all branches all at once helps keep the whole team |
| 608 | up-to-date with what everybody else is doing, resulting in a more |
| 609 | tightly focused and cohesive implementation. |
| 610 | |
| 611 | |
| 612 | <h3 id="checkouts">2.6 One vs. Many Check-outs per Repository</h3> |
| 613 | |
| 614 | Because Git commingles the repository data with the initial checkout of |
| 615 | that repository, the default mode of operation in Git is to stick to that |
| @@ -612,20 +629,28 @@ | |
| 629 | standard advice is to use a switch-in-place workflow in Fossil when |
| 630 | the disturbance from switching branches is small, and to use multiple |
| 631 | checkouts when you have long-lived working branches that are different |
| 632 | enough that switching in place is disruptive. |
| 633 | |
| 634 | While you can [./gitusers.md#worktree | use Git in the Fossil style] via |
| 635 | its worktree feature, tutorials continue to teach the style of having |
| 636 | one working directory only. Git can even fight you on this, as when |
| 637 | working with a forked repository; it is best to have independent clones |
| 638 | of the upstream and your fork, to allow separate "remote" lists and |
| 639 | such. This can result in two working directories but each having a |
| 640 | captive repo clone each, which isn't in the spirit of <tt>git |
| 641 | worktree</tt> at all. Yet, it beats the alternative, which then |
| 642 | highlights a gap in the model of diverging and re-converging forks. |
| 643 | Ideally, Git would let you create one of these forks as a worktree while |
| 644 | maintaining a strong separation between your fork and the upstream repo, |
| 645 | but it ends up being too much hassle to bother with. |
| 646 | |
| 647 | There are |
| 648 | [https://duckduckgo.com/?q=git+worktree+problem | several known |
| 649 | problems] with the single worktree style, ones which don't happen in |
| 650 | Fossil because of the clear [./ckout-workflows.md | separation] between |
| 651 | a Fossil repository and each working directory. |
| 652 | |
| 653 | This distinction matters because switching branches inside a single working directory loses local context |
| 654 | on each switch. |
| 655 | |
| 656 | For instance, in any software project where the runnable program must be |
| @@ -658,13 +683,10 @@ | |
| 683 | |
| 684 | Plus, |
| 685 | <tt>cd</tt> is faster to type than <tt>git checkout</tt> or <tt>fossil |
| 686 | update</tt>. |
| 687 | |
| 688 | <h3 id="history">2.7 What you should have done vs. What you actually did</h3> |
| 689 | |
| 690 | Git puts a lot of emphasis on maintaining |
| 691 | a "clean" check-in history. Extraneous and experimental branches by |
| 692 | individual developers often never make it into the main repository. |
| @@ -822,14 +844,14 @@ | |
| 844 | concepts to keep track of in your mental model of Fossil's internal |
| 845 | operation. |
| 846 | |
| 847 | Fossil's implementation of the feature is also simpler to describe. The |
| 848 | brief online help for <tt>[/help/merge | fossil merge]</tt> is |
| 849 | currently 50 lines long, to which you want to add the ~800 lines of |
| 850 | [./branching.wiki | the branching document]. The equivalent |
| 851 | documentation in Git is the aggregation of the man pages for the above |
| 852 | three commands, which is approaching 1400 lines as of this writing, much of it mutually redundant. |
| 853 | (e.g. Git's <tt>--edit</tt> and <tt>--no-commit</tt> options get |
| 854 | described three times, each time differently.) Fossil's |
| 855 | documentation is not only more concise, it gives a nice split of brief |
| 856 | online help and full online documentation. |
| 857 | |
| @@ -852,35 +874,42 @@ | |
| 874 | This not |
| 875 | only solves the SHAttered problem, it should prevent a reoccurrence of |
| 876 | similar problems for the foreseeable future. |
| 877 | |
| 878 | Meanwhile, the Git community took until August 2018 to publish |
| 879 | [https://git-scm.com/docs/hash-function-transition/ | their plan] for |
| 880 | solving the same problem by moving to SHA-256, a variant of the |
| 881 | [https://en.wikipedia.org/wiki/SHA-2 | older SHA-2 algorithm]. That is |
| 882 | now technically implemented in the sense that <tt>git init |
| 883 | --object-format=sha256</tt> exists, but note well: this is not only an |
| 884 | optional setting, Git forge support is mixed, most notably |
| 885 | [https://github.com/GitoxideLabs/gitoxide/issues/281 | lacking in |
| 886 | GitHub], plus also BitBucket and others. This is doubtless because of |
| 887 | this warning in the latest ([https://git-scm.com/docs/git-init/2.54.0 | |
| 888 | as of this writing]) <tt>git init</tt> docs: |
| 889 | |
| 890 | <blockquote>Note: At present, there is no interoperability between |
| 891 | SHA-256 repositories and SHA-1 repositories.</blockquote> |
| 892 | |
| 893 | Although we are now in the <i>tenth year</i> of this situation, there |
| 894 | remains hope that Git will manage to make the transition without taking |
| 895 | the full decade: the |
| 896 | [https://www.deployhq.com/blog/git-3-0-on-the-horizon-what-git-users-need-to-know-about-the-next-major-release |
| 897 | | latest plan] is that Git 3.0 will finally <i>(finally!)</i> switch to |
| 898 | SHA256 by default, forcing the issue. Given the track record, we are |
| 899 | taking a "show me" stance on this claim. |
| 900 | |
| 901 | Always remember, attacks only get better, never worse. |
| 902 | Almost three years after Fossil solved this problem, the |
| 903 | [https://sha-mbles.github.io/ | SHAmbles attack] was published, further |
| 904 | weakening the case for continuing to use SHA-1. |
| 905 | |
| 906 | The practical impact of attacks like SHAttered and SHAmbles on the |
| 907 | Git and Fossil Merkle trees isn't clear, but you want to have your repositories |
| 908 | moved over to a stronger hash algorithm <i>before</i> someone figures out how |
| 909 | to make use of the weaknesses in the old one. Fossil's solution is long |
| 910 | since [https://repology.org/project/fossil/versions | universally deployed]. |
| 911 | |
| 912 | <hr/> |
| 913 | |
| 914 | <h3>Asides and Digressions</h3> |
| 915 | |
| @@ -910,16 +939,10 @@ | |
| 939 | lightweight web server, |
| 940 | <tt>[https://sqlite.org/althttpd/|althttpd]</tt>, |
| 941 | which is configured as a front end to Fossil running in CGI mode on |
| 942 | these sites. |
| 943 | |
| 944 | <li><p>This means you can give up waiting for Fossil to be ported to |
| 945 | the PDP-11, but we remain hopeful that someone may eventually port |
| 946 | it to [https://en.wikipedia.org/wiki/Z/OS|z/OS]. |
| 947 | |
| 948 | <li><p>"Why is there all this Tcl in and around Fossil?" you may |
| 949 |
+6
-6
| --- www/javascript.md | ||
| +++ www/javascript.md | ||
| @@ -66,11 +66,11 @@ | ||
| 66 | 66 | Most JavaScript-based Fossil pages use less code than that. |
| 67 | 67 | |
| 68 | 68 | Atop that, Fossil sends HTTP headers to the browser that allow it |
| 69 | 69 | to perform aggressive caching so that typical page loads will skip |
| 70 | 70 | re-loading this content on subsequent loads. These features are |
| 71 | - currently optional: you must either set the new | |
| 71 | + currently optional: you must either set the | |
| 72 | 72 | [`fossil server --jsmode bundle` option][fsrv] or the corresponding |
| 73 | 73 | `jsmode` control line |
| 74 | 74 | in your [`fossil cgi`][fcgi] script when setting up your |
| 75 | 75 | [Fossil server][fshome]. That done, Fossil’s JavaScript files will |
| 76 | 76 | load almost instantly from the browser’s cache after the initial |
| @@ -100,11 +100,11 @@ | ||
| 100 | 100 | Ajax partial page updates are faster than |
| 101 | 101 | the no-JS alternative, a full HTTP POST round-trip to submit new |
| 102 | 102 | data to the remote server, retrieve an entire new HTML document, |
| 103 | 103 | and re-render the whole thing client-side. |
| 104 | 104 | |
| 105 | -3. <a id="3pjs"></a>“**Third-party JavaScript cannot be trusted.**” | |
| 105 | +3. “<a id="3pjs"></a>**Third-party JavaScript cannot be trusted.**” | |
| 106 | 106 | |
| 107 | 107 | Fossil does not use any third-party JavaScript libraries, not even |
| 108 | 108 | very common ones like jQuery. Every bit of JavaScript served by the |
| 109 | 109 | stock version of Fossil was written specifically for the Fossil |
| 110 | 110 | project and is stored [in its code repository][fsrc]. |
| @@ -113,11 +113,11 @@ | ||
| 113 | 113 | Fossil and mechanisms like [skin editing][cskin] don’t suffice for your |
| 114 | 114 | purposes, you can hack on the JavaScript in your local instance |
| 115 | 115 | directly, just as you can hack on its C, SQL, and Tcl code. Fossil |
| 116 | 116 | is free and open source software, under [a single license][2cbsd]. |
| 117 | 117 | |
| 118 | -4. <a id="snoop"></a>“**JavaScript and cookies are used to snoop on web users.**” | |
| 118 | +4. “<a id="snoop"></a>**JavaScript and cookies are used to snoop on web users.**” | |
| 119 | 119 | |
| 120 | 120 | There is no tracking or other snooping technology in Fossil other than |
| 121 | 121 | that necessary for basic security, such as IP address logging on |
| 122 | 122 | check-ins. (This is in part why we have no [comprehensive user |
| 123 | 123 | statistics](#stats)!) |
| @@ -184,11 +184,11 @@ | ||
| 184 | 184 | The no-JS case is a [minority position](#stats), so those that want |
| 185 | 185 | Fossil to have no-JS alternatives and graceful fallbacks will need |
| 186 | 186 | to get involved with the development if they want this state of |
| 187 | 187 | affairs to continue. |
| 188 | 188 | |
| 189 | -8. <a id="stats"></a>“**A large number of users run without JavaScript enabled.**” | |
| 189 | +8. “<a id="stats"></a>**A large number of users run without JavaScript enabled.**” | |
| 190 | 190 | |
| 191 | 191 | That’s not what web audience measurements say: |
| 192 | 192 | |
| 193 | 193 | * [What percentage of browsers with javascript disabled?][s1] |
| 194 | 194 | * [How many people are missing out on JavaScript enhancement?][s2] |
| @@ -207,11 +207,11 @@ | ||
| 207 | 207 | run [powerful conditional blocking plugins](#block) in their |
| 208 | 208 | browsers, rather than block JavaScript entirely. We suspect that |
| 209 | 209 | between these two forces, the number of no-JS purists among Fossil’s |
| 210 | 210 | user base is still a tiny minority. |
| 211 | 211 | |
| 212 | -9. <a id="block"></a>“**I block JavaScript entirely in my browser. That breaks Fossil.**” | |
| 212 | +9. “<a id="block"></a>**I block JavaScript entirely in my browser. That breaks Fossil.**” | |
| 213 | 213 | |
| 214 | 214 | First, see our philosophy statements above. Briefly, we intend that |
| 215 | 215 | there always be some other way to get any given result without using |
| 216 | 216 | JavaScript, developer interest willing. |
| 217 | 217 | |
| @@ -237,11 +237,11 @@ | ||
| 237 | 237 | a few of these part-timers are responsible for the bulk of the code |
| 238 | 238 | in Fossil. If you want Fossil to support such niche use cases, then |
| 239 | 239 | you will have to [get involved with its development][cg]: it’s |
| 240 | 240 | *your* uncommon itch. |
| 241 | 241 | |
| 242 | -11. <a id="compat"></a>“**Fossil’s JavaScript code isn’t compatible with my browser.**” | |
| 242 | +11. “<a id="compat"></a>**Fossil’s JavaScript code isn’t compatible with my browser.**” | |
| 243 | 243 | |
| 244 | 244 | The Fossil project’s developers aim to remain compatible with |
| 245 | 245 | the largest portions of the client-side browser base. We use only |
| 246 | 246 | standards-defined JavaScript features which are known to work in the |
| 247 | 247 | overwhelmingly vast majority of browsers going back approximately 5 |
| 248 | 248 |
| --- www/javascript.md | |
| +++ www/javascript.md | |
| @@ -66,11 +66,11 @@ | |
| 66 | Most JavaScript-based Fossil pages use less code than that. |
| 67 | |
| 68 | Atop that, Fossil sends HTTP headers to the browser that allow it |
| 69 | to perform aggressive caching so that typical page loads will skip |
| 70 | re-loading this content on subsequent loads. These features are |
| 71 | currently optional: you must either set the new |
| 72 | [`fossil server --jsmode bundle` option][fsrv] or the corresponding |
| 73 | `jsmode` control line |
| 74 | in your [`fossil cgi`][fcgi] script when setting up your |
| 75 | [Fossil server][fshome]. That done, Fossil’s JavaScript files will |
| 76 | load almost instantly from the browser’s cache after the initial |
| @@ -100,11 +100,11 @@ | |
| 100 | Ajax partial page updates are faster than |
| 101 | the no-JS alternative, a full HTTP POST round-trip to submit new |
| 102 | data to the remote server, retrieve an entire new HTML document, |
| 103 | and re-render the whole thing client-side. |
| 104 | |
| 105 | 3. <a id="3pjs"></a>“**Third-party JavaScript cannot be trusted.**” |
| 106 | |
| 107 | Fossil does not use any third-party JavaScript libraries, not even |
| 108 | very common ones like jQuery. Every bit of JavaScript served by the |
| 109 | stock version of Fossil was written specifically for the Fossil |
| 110 | project and is stored [in its code repository][fsrc]. |
| @@ -113,11 +113,11 @@ | |
| 113 | Fossil and mechanisms like [skin editing][cskin] don’t suffice for your |
| 114 | purposes, you can hack on the JavaScript in your local instance |
| 115 | directly, just as you can hack on its C, SQL, and Tcl code. Fossil |
| 116 | is free and open source software, under [a single license][2cbsd]. |
| 117 | |
| 118 | 4. <a id="snoop"></a>“**JavaScript and cookies are used to snoop on web users.**” |
| 119 | |
| 120 | There is no tracking or other snooping technology in Fossil other than |
| 121 | that necessary for basic security, such as IP address logging on |
| 122 | check-ins. (This is in part why we have no [comprehensive user |
| 123 | statistics](#stats)!) |
| @@ -184,11 +184,11 @@ | |
| 184 | The no-JS case is a [minority position](#stats), so those that want |
| 185 | Fossil to have no-JS alternatives and graceful fallbacks will need |
| 186 | to get involved with the development if they want this state of |
| 187 | affairs to continue. |
| 188 | |
| 189 | 8. <a id="stats"></a>“**A large number of users run without JavaScript enabled.**” |
| 190 | |
| 191 | That’s not what web audience measurements say: |
| 192 | |
| 193 | * [What percentage of browsers with javascript disabled?][s1] |
| 194 | * [How many people are missing out on JavaScript enhancement?][s2] |
| @@ -207,11 +207,11 @@ | |
| 207 | run [powerful conditional blocking plugins](#block) in their |
| 208 | browsers, rather than block JavaScript entirely. We suspect that |
| 209 | between these two forces, the number of no-JS purists among Fossil’s |
| 210 | user base is still a tiny minority. |
| 211 | |
| 212 | 9. <a id="block"></a>“**I block JavaScript entirely in my browser. That breaks Fossil.**” |
| 213 | |
| 214 | First, see our philosophy statements above. Briefly, we intend that |
| 215 | there always be some other way to get any given result without using |
| 216 | JavaScript, developer interest willing. |
| 217 | |
| @@ -237,11 +237,11 @@ | |
| 237 | a few of these part-timers are responsible for the bulk of the code |
| 238 | in Fossil. If you want Fossil to support such niche use cases, then |
| 239 | you will have to [get involved with its development][cg]: it’s |
| 240 | *your* uncommon itch. |
| 241 | |
| 242 | 11. <a id="compat"></a>“**Fossil’s JavaScript code isn’t compatible with my browser.**” |
| 243 | |
| 244 | The Fossil project’s developers aim to remain compatible with |
| 245 | the largest portions of the client-side browser base. We use only |
| 246 | standards-defined JavaScript features which are known to work in the |
| 247 | overwhelmingly vast majority of browsers going back approximately 5 |
| 248 |
| --- www/javascript.md | |
| +++ www/javascript.md | |
| @@ -66,11 +66,11 @@ | |
| 66 | Most JavaScript-based Fossil pages use less code than that. |
| 67 | |
| 68 | Atop that, Fossil sends HTTP headers to the browser that allow it |
| 69 | to perform aggressive caching so that typical page loads will skip |
| 70 | re-loading this content on subsequent loads. These features are |
| 71 | currently optional: you must either set the |
| 72 | [`fossil server --jsmode bundle` option][fsrv] or the corresponding |
| 73 | `jsmode` control line |
| 74 | in your [`fossil cgi`][fcgi] script when setting up your |
| 75 | [Fossil server][fshome]. That done, Fossil’s JavaScript files will |
| 76 | load almost instantly from the browser’s cache after the initial |
| @@ -100,11 +100,11 @@ | |
| 100 | Ajax partial page updates are faster than |
| 101 | the no-JS alternative, a full HTTP POST round-trip to submit new |
| 102 | data to the remote server, retrieve an entire new HTML document, |
| 103 | and re-render the whole thing client-side. |
| 104 | |
| 105 | 3. “<a id="3pjs"></a>**Third-party JavaScript cannot be trusted.**” |
| 106 | |
| 107 | Fossil does not use any third-party JavaScript libraries, not even |
| 108 | very common ones like jQuery. Every bit of JavaScript served by the |
| 109 | stock version of Fossil was written specifically for the Fossil |
| 110 | project and is stored [in its code repository][fsrc]. |
| @@ -113,11 +113,11 @@ | |
| 113 | Fossil and mechanisms like [skin editing][cskin] don’t suffice for your |
| 114 | purposes, you can hack on the JavaScript in your local instance |
| 115 | directly, just as you can hack on its C, SQL, and Tcl code. Fossil |
| 116 | is free and open source software, under [a single license][2cbsd]. |
| 117 | |
| 118 | 4. “<a id="snoop"></a>**JavaScript and cookies are used to snoop on web users.**” |
| 119 | |
| 120 | There is no tracking or other snooping technology in Fossil other than |
| 121 | that necessary for basic security, such as IP address logging on |
| 122 | check-ins. (This is in part why we have no [comprehensive user |
| 123 | statistics](#stats)!) |
| @@ -184,11 +184,11 @@ | |
| 184 | The no-JS case is a [minority position](#stats), so those that want |
| 185 | Fossil to have no-JS alternatives and graceful fallbacks will need |
| 186 | to get involved with the development if they want this state of |
| 187 | affairs to continue. |
| 188 | |
| 189 | 8. “<a id="stats"></a>**A large number of users run without JavaScript enabled.**” |
| 190 | |
| 191 | That’s not what web audience measurements say: |
| 192 | |
| 193 | * [What percentage of browsers with javascript disabled?][s1] |
| 194 | * [How many people are missing out on JavaScript enhancement?][s2] |
| @@ -207,11 +207,11 @@ | |
| 207 | run [powerful conditional blocking plugins](#block) in their |
| 208 | browsers, rather than block JavaScript entirely. We suspect that |
| 209 | between these two forces, the number of no-JS purists among Fossil’s |
| 210 | user base is still a tiny minority. |
| 211 | |
| 212 | 9. “<a id="block"></a>**I block JavaScript entirely in my browser. That breaks Fossil.**” |
| 213 | |
| 214 | First, see our philosophy statements above. Briefly, we intend that |
| 215 | there always be some other way to get any given result without using |
| 216 | JavaScript, developer interest willing. |
| 217 | |
| @@ -237,11 +237,11 @@ | |
| 237 | a few of these part-timers are responsible for the bulk of the code |
| 238 | in Fossil. If you want Fossil to support such niche use cases, then |
| 239 | you will have to [get involved with its development][cg]: it’s |
| 240 | *your* uncommon itch. |
| 241 | |
| 242 | 11. “<a id="compat"></a>**Fossil’s JavaScript code isn’t compatible with my browser.**” |
| 243 | |
| 244 | The Fossil project’s developers aim to remain compatible with |
| 245 | the largest portions of the client-side browser base. We use only |
| 246 | standards-defined JavaScript features which are known to work in the |
| 247 | overwhelmingly vast majority of browsers going back approximately 5 |
| 248 |
+3
-1
| --- www/server/any/cgi.md | ||
| +++ www/server/any/cgi.md | ||
| @@ -38,11 +38,13 @@ | ||
| 38 | 38 | (This might differ from the user the web server normally runs |
| 39 | 39 | under.) The directory holding the repository file(s) needs to be |
| 40 | 40 | writable so that SQLite can write its journal files. When using |
| 41 | 41 | another access control system, such as AppArmor or SELinux, it may |
| 42 | 42 | be necessary to explicitly permit that account to read and write |
| 43 | - the necessary files. | |
| 43 | + the necessary files. Also verify a possible _systemd_ sandboxing of | |
| 44 | + the web server service, especially the combination of _ProtectSystem_, | |
| 45 | + _ProtectHome_, and _ReadWriteDirectories_/_ReadWritePaths_. | |
| 44 | 46 | |
| 45 | 47 | * Fossil must be able to create temporary files in a |
| 46 | 48 | [directory that varies by host OS](../../env-opts.md#temp). When the |
| 47 | 49 | CGI process is operating [within a chroot](../../chroot.md), |
| 48 | 50 | ensure that this directory exists and is readable/writeable by the |
| 49 | 51 |
| --- www/server/any/cgi.md | |
| +++ www/server/any/cgi.md | |
| @@ -38,11 +38,13 @@ | |
| 38 | (This might differ from the user the web server normally runs |
| 39 | under.) The directory holding the repository file(s) needs to be |
| 40 | writable so that SQLite can write its journal files. When using |
| 41 | another access control system, such as AppArmor or SELinux, it may |
| 42 | be necessary to explicitly permit that account to read and write |
| 43 | the necessary files. |
| 44 | |
| 45 | * Fossil must be able to create temporary files in a |
| 46 | [directory that varies by host OS](../../env-opts.md#temp). When the |
| 47 | CGI process is operating [within a chroot](../../chroot.md), |
| 48 | ensure that this directory exists and is readable/writeable by the |
| 49 |
| --- www/server/any/cgi.md | |
| +++ www/server/any/cgi.md | |
| @@ -38,11 +38,13 @@ | |
| 38 | (This might differ from the user the web server normally runs |
| 39 | under.) The directory holding the repository file(s) needs to be |
| 40 | writable so that SQLite can write its journal files. When using |
| 41 | another access control system, such as AppArmor or SELinux, it may |
| 42 | be necessary to explicitly permit that account to read and write |
| 43 | the necessary files. Also verify a possible _systemd_ sandboxing of |
| 44 | the web server service, especially the combination of _ProtectSystem_, |
| 45 | _ProtectHome_, and _ReadWriteDirectories_/_ReadWritePaths_. |
| 46 | |
| 47 | * Fossil must be able to create temporary files in a |
| 48 | [directory that varies by host OS](../../env-opts.md#temp). When the |
| 49 | CGI process is operating [within a chroot](../../chroot.md), |
| 50 | ensure that this directory exists and is readable/writeable by the |
| 51 |
+3
-3
| --- www/sync.wiki | ||
| +++ www/sync.wiki | ||
| @@ -242,15 +242,15 @@ | ||
| 242 | 242 | |
| 243 | 243 | As of version 2.27, Fossil supports transfering of the login card |
| 244 | 244 | externally to the request payload via a Cookie HTTP header: |
| 245 | 245 | |
| 246 | 246 | <verbatim> |
| 247 | - Cookie: x-f-x-l=... | |
| 247 | + Cookie: x-f-l-c=... | |
| 248 | 248 | </verbatim> |
| 249 | 249 | |
| 250 | -Where "..." is the URL-encoded login cookie. <code>x-f-x-l</code> is | |
| 251 | -short for X-Fossil-Xfer-Login. | |
| 250 | +Where "..." is the URL-encoded login cookie. <code>x-f-l-c</code> is | |
| 251 | +short for X-Fossil-Login-Cookie. | |
| 252 | 252 | |
| 253 | 253 | |
| 254 | 254 | <h3 id="file">3.3 File Cards</h3> |
| 255 | 255 | |
| 256 | 256 | Artifacts are transferred using either "file" cards, or "cfile" |
| 257 | 257 |
| --- www/sync.wiki | |
| +++ www/sync.wiki | |
| @@ -242,15 +242,15 @@ | |
| 242 | |
| 243 | As of version 2.27, Fossil supports transfering of the login card |
| 244 | externally to the request payload via a Cookie HTTP header: |
| 245 | |
| 246 | <verbatim> |
| 247 | Cookie: x-f-x-l=... |
| 248 | </verbatim> |
| 249 | |
| 250 | Where "..." is the URL-encoded login cookie. <code>x-f-x-l</code> is |
| 251 | short for X-Fossil-Xfer-Login. |
| 252 | |
| 253 | |
| 254 | <h3 id="file">3.3 File Cards</h3> |
| 255 | |
| 256 | Artifacts are transferred using either "file" cards, or "cfile" |
| 257 |
| --- www/sync.wiki | |
| +++ www/sync.wiki | |
| @@ -242,15 +242,15 @@ | |
| 242 | |
| 243 | As of version 2.27, Fossil supports transfering of the login card |
| 244 | externally to the request payload via a Cookie HTTP header: |
| 245 | |
| 246 | <verbatim> |
| 247 | Cookie: x-f-l-c=... |
| 248 | </verbatim> |
| 249 | |
| 250 | Where "..." is the URL-encoded login cookie. <code>x-f-l-c</code> is |
| 251 | short for X-Fossil-Login-Cookie. |
| 252 | |
| 253 | |
| 254 | <h3 id="file">3.3 File Cards</h3> |
| 255 | |
| 256 | Artifacts are transferred using either "file" cards, or "cfile" |
| 257 |