Fossil SCM
Merge the latest trunk enhancements into the quickfilter branch.
Commit
e14c75676c3eb42afc4af2408c8dce2594ec3e1a3032db611d57a11fd1f573f1
Parent
fa31d18bf49185e…
95 files changed
+101
-30
+588
-151
+5
-4
+1
-1
+1
-1
+1
-1
+1
-1
+1
-1
+1
-1
+1
-1
+101
+1
-1
+9
-2
+148
-64
+12
-5
+8
-7
+2
-2
+54
-28
+289
-165
+2
-1
+4
+353
+1
-1
+1
+1
-1
+75
-3
+2
+32
-5
+3
-3
+95
-22
+376
-57
+1
-1
+1
-1
-1
+12
-12
+55
-25
+21
-8
+18
-2
+33
-21
+62
-16
+28
-21
+65
-6
+8
-3
+113
-23
+113
-23
+2
-4
+2
-4
+3
-2
+66
-16
+15
-5
+85
-39
+4
-3
+2
-1
+98
-48
+5
-1
+8
-2
+1
-1
+3
+9
-7
+9
-7
+17
-7
+95
-56
+53
-14
+92
-53
+207
-39
+27
-89
+49
-22
+27
-15
+131
-15
+1
-1
+5
-3
+79
-41
+16
-6
+2
-2
+20
-8
+1
-1
+9
+84
+36
-36
+84
+141
+1
-1
+1
-1
+6
+53
-22
+18
+4
-3
+4
-3
+2
-2
+106
-74
+3
-3
+1
-1
+1
-1
+128
-5
~
Makefile.in
~
extsrc/shell.c
~
extsrc/sqlite3.c
~
extsrc/sqlite3.h
~
skins/blitz/footer.txt
~
skins/darkmode/footer.txt
~
skins/default/header.txt
~
skins/eagle/footer.txt
~
skins/eagle/header.txt
~
skins/original/footer.txt
~
skins/original/header.txt
~
skins/xekri/css.txt
~
skins/xekri/header.txt
~
src/add.c
~
src/alerts.c
~
src/backoffice.c
~
src/branch.c
~
src/browse.c
~
src/cache.c
~
src/cgi.c
~
src/chat.c
~
src/checkin.c
~
src/color.c
~
src/db.c
~
src/default.css
~
src/doc.c
~
src/encode.c
~
src/finfo.c
~
src/forum.c
~
src/fossil.dom.js
~
src/fossil.fetch.js
~
src/fossil.page.chat.js
~
src/fossil.popupwidget.js
~
src/graph.c
~
src/graph.js
~
src/http_ssl.c
~
src/info.c
~
src/loadctrl.c
~
src/login.c
~
src/lookslike.c
~
src/main.c
~
src/manifest.c
~
src/name.c
~
src/printf.c
~
src/repolist.c
~
src/repolist.c
~
src/report.c
~
src/report.c
~
src/search.c
~
src/security_audit.c
~
src/setup.c
~
src/setupuser.c
~
src/shun.c
~
src/sitemap.c
~
src/smtp.c
~
src/sorttable.js
~
src/stash.c
~
src/stat.c
~
src/statrep.c
~
src/style.c
~
src/style.c
~
src/style.chat.css
~
src/th.c
~
src/th.h
~
src/th_lang.c
~
src/th_main.c
~
src/th_tcl.c
~
src/timeline.c
~
src/tkt.c
~
src/tktsetup.c
~
src/undo.c
~
src/unversioned.c
~
src/user.c
~
src/util.c
~
src/winfile.c
~
src/xfer.c
~
test/many-www.tcl
~
test/tester.tcl
~
test/th1-taint.test
~
test/th1.test
~
tools/fake-smtpd.tcl
~
tools/find-fossil-cgis.tcl
~
tools/fossil-stress.tcl
~
www/aboutcgi.wiki
~
www/cgi.wiki
~
www/changes.wiki
~
www/chat.md
~
www/env-opts.md
~
www/env-opts.md
~
www/loadmgmt.md
~
www/quickstart.wiki
~
www/server/debian/service.md
~
www/server/windows/service.md
~
www/serverext.wiki
~
www/th1.md
No diff available
+101
-30
| --- extsrc/shell.c | ||
| +++ extsrc/shell.c | ||
| @@ -1162,10 +1162,27 @@ | ||
| 1162 | 1162 | } |
| 1163 | 1163 | } |
| 1164 | 1164 | return n; |
| 1165 | 1165 | } |
| 1166 | 1166 | #endif |
| 1167 | + | |
| 1168 | +/* | |
| 1169 | +** Check to see if z[] is a valid VT100 escape. If it is, then | |
| 1170 | +** return the number of bytes in the escape sequence. Return 0 if | |
| 1171 | +** z[] is not a VT100 escape. | |
| 1172 | +** | |
| 1173 | +** This routine assumes that z[0] is \033 (ESC). | |
| 1174 | +*/ | |
| 1175 | +static int isVt100(const unsigned char *z){ | |
| 1176 | + int i; | |
| 1177 | + if( z[1]!='[' ) return 0; | |
| 1178 | + i = 2; | |
| 1179 | + while( z[i]>=0x30 && z[i]<=0x3f ){ i++; } | |
| 1180 | + while( z[i]>=0x20 && z[i]<=0x2f ){ i++; } | |
| 1181 | + if( z[i]<0x40 || z[i]>0x7e ) return 0; | |
| 1182 | + return i+1; | |
| 1183 | +} | |
| 1167 | 1184 | |
| 1168 | 1185 | /* |
| 1169 | 1186 | ** Output string zUtf to stdout as w characters. If w is negative, |
| 1170 | 1187 | ** then right-justify the text. W is the width in UTF-8 characters, not |
| 1171 | 1188 | ** in bytes. This is different from the %*.*s specification in printf |
| @@ -1178,10 +1195,11 @@ | ||
| 1178 | 1195 | static void utf8_width_print(FILE *out, int w, const char *zUtf){ |
| 1179 | 1196 | const unsigned char *a = (const unsigned char*)zUtf; |
| 1180 | 1197 | unsigned char c; |
| 1181 | 1198 | int i = 0; |
| 1182 | 1199 | int n = 0; |
| 1200 | + int k; | |
| 1183 | 1201 | int aw = w<0 ? -w : w; |
| 1184 | 1202 | if( zUtf==0 ) zUtf = ""; |
| 1185 | 1203 | while( (c = a[i])!=0 ){ |
| 1186 | 1204 | if( (c&0xc0)==0xc0 ){ |
| 1187 | 1205 | int u; |
| @@ -1190,10 +1208,12 @@ | ||
| 1190 | 1208 | if( x+n>aw ){ |
| 1191 | 1209 | break; |
| 1192 | 1210 | } |
| 1193 | 1211 | i += len; |
| 1194 | 1212 | n += x; |
| 1213 | + }else if( c==0x1b && (k = isVt100(&a[i]))>0 ){ | |
| 1214 | + i += k; | |
| 1195 | 1215 | }else if( n>=aw ){ |
| 1196 | 1216 | break; |
| 1197 | 1217 | }else{ |
| 1198 | 1218 | n++; |
| 1199 | 1219 | i++; |
| @@ -6270,12 +6290,11 @@ | ||
| 6270 | 6290 | ** start HIDDEN, |
| 6271 | 6291 | ** stop HIDDEN, |
| 6272 | 6292 | ** step HIDDEN |
| 6273 | 6293 | ** ); |
| 6274 | 6294 | ** |
| 6275 | -** The virtual table also has a rowid, logically equivalent to n+1 where | |
| 6276 | -** "n" is the ascending integer in the aforesaid production definition. | |
| 6295 | +** The virtual table also has a rowid which is an alias for the value. | |
| 6277 | 6296 | ** |
| 6278 | 6297 | ** Function arguments in queries against this virtual table are translated |
| 6279 | 6298 | ** into equality constraints against successive hidden columns. In other |
| 6280 | 6299 | ** words, the following pairs of queries are equivalent to each other: |
| 6281 | 6300 | ** |
| @@ -6326,10 +6345,11 @@ | ||
| 6326 | 6345 | /* #include "sqlite3ext.h" */ |
| 6327 | 6346 | SQLITE_EXTENSION_INIT1 |
| 6328 | 6347 | #include <assert.h> |
| 6329 | 6348 | #include <string.h> |
| 6330 | 6349 | #include <limits.h> |
| 6350 | +#include <math.h> | |
| 6331 | 6351 | |
| 6332 | 6352 | #ifndef SQLITE_OMIT_VIRTUALTABLE |
| 6333 | 6353 | /* |
| 6334 | 6354 | ** Return that member of a generate_series(...) sequence whose 0-based |
| 6335 | 6355 | ** index is ix. The 0th member is given by smBase. The sequence members |
| @@ -6486,10 +6506,11 @@ | ||
| 6486 | 6506 | ){ |
| 6487 | 6507 | sqlite3_vtab *pNew; |
| 6488 | 6508 | int rc; |
| 6489 | 6509 | |
| 6490 | 6510 | /* Column numbers */ |
| 6511 | +#define SERIES_COLUMN_ROWID (-1) | |
| 6491 | 6512 | #define SERIES_COLUMN_VALUE 0 |
| 6492 | 6513 | #define SERIES_COLUMN_START 1 |
| 6493 | 6514 | #define SERIES_COLUMN_STOP 2 |
| 6494 | 6515 | #define SERIES_COLUMN_STEP 3 |
| 6495 | 6516 | |
| @@ -6573,17 +6594,15 @@ | ||
| 6573 | 6594 | #define LARGEST_UINT64 (0xffffffff|(((sqlite3_uint64)0xffffffff)<<32)) |
| 6574 | 6595 | #define SMALLEST_INT64 (((sqlite3_int64)-1) - LARGEST_INT64) |
| 6575 | 6596 | #endif |
| 6576 | 6597 | |
| 6577 | 6598 | /* |
| 6578 | -** Return the rowid for the current row, logically equivalent to n+1 where | |
| 6579 | -** "n" is the ascending integer in the aforesaid production definition. | |
| 6599 | +** The rowid is the same as the value. | |
| 6580 | 6600 | */ |
| 6581 | 6601 | static int seriesRowid(sqlite3_vtab_cursor *cur, sqlite_int64 *pRowid){ |
| 6582 | 6602 | series_cursor *pCur = (series_cursor*)cur; |
| 6583 | - sqlite3_uint64 n = pCur->ss.uSeqIndexNow; | |
| 6584 | - *pRowid = (sqlite3_int64)((n<LARGEST_UINT64)? n+1 : 0); | |
| 6603 | + *pRowid = pCur->ss.iValueNow; | |
| 6585 | 6604 | return SQLITE_OK; |
| 6586 | 6605 | } |
| 6587 | 6606 | |
| 6588 | 6607 | /* |
| 6589 | 6608 | ** Return TRUE if the cursor has been moved off of the last |
| @@ -6692,29 +6711,56 @@ | ||
| 6692 | 6711 | if( idxNum & 0x3380 ){ |
| 6693 | 6712 | /* Extract the maximum range of output values determined by |
| 6694 | 6713 | ** constraints on the "value" column. |
| 6695 | 6714 | */ |
| 6696 | 6715 | if( idxNum & 0x0080 ){ |
| 6697 | - iMin = iMax = sqlite3_value_int64(argv[i++]); | |
| 6716 | + if( sqlite3_value_numeric_type(argv[i])==SQLITE_FLOAT ){ | |
| 6717 | + double r = sqlite3_value_double(argv[i++]); | |
| 6718 | + if( r==ceil(r) ){ | |
| 6719 | + iMin = iMax = (sqlite3_int64)r; | |
| 6720 | + }else{ | |
| 6721 | + returnNoRows = 1; | |
| 6722 | + } | |
| 6723 | + }else{ | |
| 6724 | + iMin = iMax = sqlite3_value_int64(argv[i++]); | |
| 6725 | + } | |
| 6698 | 6726 | }else{ |
| 6699 | 6727 | if( idxNum & 0x0300 ){ |
| 6700 | - iMin = sqlite3_value_int64(argv[i++]); | |
| 6701 | - if( idxNum & 0x0200 ){ | |
| 6702 | - if( iMin==LARGEST_INT64 ){ | |
| 6703 | - returnNoRows = 1; | |
| 6728 | + if( sqlite3_value_numeric_type(argv[i])==SQLITE_FLOAT ){ | |
| 6729 | + double r = sqlite3_value_double(argv[i++]); | |
| 6730 | + if( idxNum & 0x0200 && r==ceil(r) ){ | |
| 6731 | + iMin = (sqlite3_int64)ceil(r+1.0); | |
| 6704 | 6732 | }else{ |
| 6705 | - iMin++; | |
| 6733 | + iMin = (sqlite3_int64)ceil(r); | |
| 6734 | + } | |
| 6735 | + }else{ | |
| 6736 | + iMin = sqlite3_value_int64(argv[i++]); | |
| 6737 | + if( idxNum & 0x0200 ){ | |
| 6738 | + if( iMin==LARGEST_INT64 ){ | |
| 6739 | + returnNoRows = 1; | |
| 6740 | + }else{ | |
| 6741 | + iMin++; | |
| 6742 | + } | |
| 6706 | 6743 | } |
| 6707 | 6744 | } |
| 6708 | 6745 | } |
| 6709 | 6746 | if( idxNum & 0x3000 ){ |
| 6710 | - iMax = sqlite3_value_int64(argv[i++]); | |
| 6711 | - if( idxNum & 0x2000 ){ | |
| 6712 | - if( iMax==SMALLEST_INT64 ){ | |
| 6713 | - returnNoRows = 1; | |
| 6747 | + if( sqlite3_value_numeric_type(argv[i])==SQLITE_FLOAT ){ | |
| 6748 | + double r = sqlite3_value_double(argv[i++]); | |
| 6749 | + if( (idxNum & 0x2000)!=0 && r==floor(r) ){ | |
| 6750 | + iMax = (sqlite3_int64)(r-1.0); | |
| 6714 | 6751 | }else{ |
| 6715 | - iMax--; | |
| 6752 | + iMax = (sqlite3_int64)floor(r); | |
| 6753 | + } | |
| 6754 | + }else{ | |
| 6755 | + iMax = sqlite3_value_int64(argv[i++]); | |
| 6756 | + if( idxNum & 0x2000 ){ | |
| 6757 | + if( iMax==SMALLEST_INT64 ){ | |
| 6758 | + returnNoRows = 1; | |
| 6759 | + }else{ | |
| 6760 | + iMax--; | |
| 6761 | + } | |
| 6716 | 6762 | } |
| 6717 | 6763 | } |
| 6718 | 6764 | } |
| 6719 | 6765 | if( iMin>iMax ){ |
| 6720 | 6766 | returnNoRows = 1; |
| @@ -6764,11 +6810,11 @@ | ||
| 6764 | 6810 | |
| 6765 | 6811 | |
| 6766 | 6812 | for(i=0; i<argc; i++){ |
| 6767 | 6813 | if( sqlite3_value_type(argv[i])==SQLITE_NULL ){ |
| 6768 | 6814 | /* If any of the constraints have a NULL value, then return no rows. |
| 6769 | - ** See ticket https://www.sqlite.org/src/info/fac496b61722daf2 */ | |
| 6815 | + ** See ticket https://sqlite.org/src/info/fac496b61722daf2 */ | |
| 6770 | 6816 | returnNoRows = 1; |
| 6771 | 6817 | break; |
| 6772 | 6818 | } |
| 6773 | 6819 | } |
| 6774 | 6820 | if( returnNoRows ){ |
| @@ -6867,11 +6913,14 @@ | ||
| 6867 | 6913 | idxNum |= 0x40; |
| 6868 | 6914 | } |
| 6869 | 6915 | continue; |
| 6870 | 6916 | } |
| 6871 | 6917 | if( pConstraint->iColumn<SERIES_COLUMN_START ){ |
| 6872 | - if( pConstraint->iColumn==SERIES_COLUMN_VALUE && pConstraint->usable ){ | |
| 6918 | + if( (pConstraint->iColumn==SERIES_COLUMN_VALUE || | |
| 6919 | + pConstraint->iColumn==SERIES_COLUMN_ROWID) | |
| 6920 | + && pConstraint->usable | |
| 6921 | + ){ | |
| 6873 | 6922 | switch( op ){ |
| 6874 | 6923 | case SQLITE_INDEX_CONSTRAINT_EQ: |
| 6875 | 6924 | case SQLITE_INDEX_CONSTRAINT_IS: { |
| 6876 | 6925 | idxNum |= 0x0080; |
| 6877 | 6926 | idxNum &= ~0x3300; |
| @@ -24197,13 +24246,18 @@ | ||
| 24197 | 24246 | j++; |
| 24198 | 24247 | }while( (n&7)!=0 && n<mxWidth ); |
| 24199 | 24248 | i++; |
| 24200 | 24249 | continue; |
| 24201 | 24250 | } |
| 24202 | - n++; | |
| 24203 | - j += 3; | |
| 24204 | - i++; | |
| 24251 | + if( c==0x1b && p->eEscMode==SHELL_ESC_OFF && (k = isVt100(&z[i]))>0 ){ | |
| 24252 | + i += k; | |
| 24253 | + j += k; | |
| 24254 | + }else{ | |
| 24255 | + n++; | |
| 24256 | + j += 3; | |
| 24257 | + i++; | |
| 24258 | + } | |
| 24205 | 24259 | } |
| 24206 | 24260 | if( n>=mxWidth && bWordWrap ){ |
| 24207 | 24261 | /* Perhaps try to back up to a better place to break the line */ |
| 24208 | 24262 | for(k=i; k>i/2; k--){ |
| 24209 | 24263 | if( IsSpace(z[k-1]) ) break; |
| @@ -24265,13 +24319,21 @@ | ||
| 24265 | 24319 | break; |
| 24266 | 24320 | case SHELL_ESC_ASCII: |
| 24267 | 24321 | zOut[j++] = '^'; |
| 24268 | 24322 | zOut[j++] = 0x40 + c; |
| 24269 | 24323 | break; |
| 24270 | - case SHELL_ESC_OFF: | |
| 24271 | - zOut[j++] = c; | |
| 24324 | + case SHELL_ESC_OFF: { | |
| 24325 | + int nn; | |
| 24326 | + if( c==0x1b && (nn = isVt100(&z[i]))>0 ){ | |
| 24327 | + memcpy(&zOut[j], &z[i], nn); | |
| 24328 | + j += nn; | |
| 24329 | + i += nn - 1; | |
| 24330 | + }else{ | |
| 24331 | + zOut[j++] = c; | |
| 24332 | + } | |
| 24272 | 24333 | break; |
| 24334 | + } | |
| 24273 | 24335 | } |
| 24274 | 24336 | i++; |
| 24275 | 24337 | } |
| 24276 | 24338 | zOut[j] = 0; |
| 24277 | 24339 | return (char*)zOut; |
| @@ -25404,10 +25466,11 @@ | ||
| 25404 | 25466 | " --readonly Open FILE readonly", |
| 25405 | 25467 | " --zip FILE is a ZIP archive", |
| 25406 | 25468 | #ifndef SQLITE_SHELL_FIDDLE |
| 25407 | 25469 | ".output ?FILE? Send output to FILE or stdout if FILE is omitted", |
| 25408 | 25470 | " If FILE begins with '|' then open it as a pipe.", |
| 25471 | + " If FILE is 'off' then output is disabled.", | |
| 25409 | 25472 | " Options:", |
| 25410 | 25473 | " --bom Prefix output with a UTF8 byte-order mark", |
| 25411 | 25474 | " -e Send output to the system text editor", |
| 25412 | 25475 | " --plain Use text/plain for -w option", |
| 25413 | 25476 | " -w Send output to a web browser", |
| @@ -27021,11 +27084,11 @@ | ||
| 27021 | 27084 | for(i=0; i<pgSz; i+=16){ |
| 27022 | 27085 | const u8 *aLine = aData+i; |
| 27023 | 27086 | for(j=0; j<16 && aLine[j]==0; j++){} |
| 27024 | 27087 | if( j==16 ) continue; |
| 27025 | 27088 | if( !seenPageLabel ){ |
| 27026 | - sqlite3_fprintf(p->out, "| page %lld offset %lld\n", pgno, pgno*pgSz); | |
| 27089 | + sqlite3_fprintf(p->out, "| page %lld offset %lld\n",pgno,(pgno-1)*pgSz); | |
| 27027 | 27090 | seenPageLabel = 1; |
| 27028 | 27091 | } |
| 27029 | 27092 | sqlite3_fprintf(p->out, "| %5d:", i); |
| 27030 | 27093 | for(j=0; j<16; j++) sqlite3_fprintf(p->out, " %02x", aLine[j]); |
| 27031 | 27094 | sqlite3_fprintf(p->out, " "); |
| @@ -30399,13 +30462,13 @@ | ||
| 30399 | 30462 | || (c=='e' && n==5 && cli_strcmp(azArg[0],"excel")==0) |
| 30400 | 30463 | || (c=='w' && n==3 && cli_strcmp(azArg[0],"www")==0) |
| 30401 | 30464 | ){ |
| 30402 | 30465 | char *zFile = 0; |
| 30403 | 30466 | int i; |
| 30404 | - int eMode = 0; | |
| 30405 | - int bOnce = 0; /* 0: .output, 1: .once, 2: .excel/.www */ | |
| 30406 | - int bPlain = 0; /* --plain option */ | |
| 30467 | + int eMode = 0; /* 0: .outout/.once, 'x'=.excel, 'w'=.www */ | |
| 30468 | + int bOnce = 0; /* 0: .output, 1: .once, 2: .excel/.www */ | |
| 30469 | + int bPlain = 0; /* --plain option */ | |
| 30407 | 30470 | static const char *zBomUtf8 = "\357\273\277"; |
| 30408 | 30471 | const char *zBom = 0; |
| 30409 | 30472 | |
| 30410 | 30473 | failIfSafeMode(p, "cannot run .%s in safe mode", azArg[0]); |
| 30411 | 30474 | if( c=='e' ){ |
| @@ -30430,18 +30493,26 @@ | ||
| 30430 | 30493 | }else if( c=='o' && cli_strcmp(z,"-e")==0 ){ |
| 30431 | 30494 | eMode = 'e'; /* text editor */ |
| 30432 | 30495 | }else if( c=='o' && cli_strcmp(z,"-w")==0 ){ |
| 30433 | 30496 | eMode = 'w'; /* Web browser */ |
| 30434 | 30497 | }else{ |
| 30435 | - sqlite3_fprintf(p->out, | |
| 30498 | + sqlite3_fprintf(p->out, | |
| 30436 | 30499 | "ERROR: unknown option: \"%s\". Usage:\n", azArg[i]); |
| 30437 | 30500 | showHelp(p->out, azArg[0]); |
| 30438 | 30501 | rc = 1; |
| 30439 | 30502 | goto meta_command_exit; |
| 30440 | 30503 | } |
| 30441 | 30504 | }else if( zFile==0 && eMode==0 ){ |
| 30442 | - zFile = sqlite3_mprintf("%s", z); | |
| 30505 | + if( cli_strcmp(z, "off")==0 ){ | |
| 30506 | +#ifdef _WIN32 | |
| 30507 | + zFile = sqlite3_mprintf("nul"); | |
| 30508 | +#else | |
| 30509 | + zFile = sqlite3_mprintf("/dev/null"); | |
| 30510 | +#endif | |
| 30511 | + }else{ | |
| 30512 | + zFile = sqlite3_mprintf("%s", z); | |
| 30513 | + } | |
| 30443 | 30514 | if( zFile && zFile[0]=='|' ){ |
| 30444 | 30515 | while( i+1<nArg ) zFile = sqlite3_mprintf("%z %s", zFile, azArg[++i]); |
| 30445 | 30516 | break; |
| 30446 | 30517 | } |
| 30447 | 30518 | }else{ |
| 30448 | 30519 |
| --- extsrc/shell.c | |
| +++ extsrc/shell.c | |
| @@ -1162,10 +1162,27 @@ | |
| 1162 | } |
| 1163 | } |
| 1164 | return n; |
| 1165 | } |
| 1166 | #endif |
| 1167 | |
| 1168 | /* |
| 1169 | ** Output string zUtf to stdout as w characters. If w is negative, |
| 1170 | ** then right-justify the text. W is the width in UTF-8 characters, not |
| 1171 | ** in bytes. This is different from the %*.*s specification in printf |
| @@ -1178,10 +1195,11 @@ | |
| 1178 | static void utf8_width_print(FILE *out, int w, const char *zUtf){ |
| 1179 | const unsigned char *a = (const unsigned char*)zUtf; |
| 1180 | unsigned char c; |
| 1181 | int i = 0; |
| 1182 | int n = 0; |
| 1183 | int aw = w<0 ? -w : w; |
| 1184 | if( zUtf==0 ) zUtf = ""; |
| 1185 | while( (c = a[i])!=0 ){ |
| 1186 | if( (c&0xc0)==0xc0 ){ |
| 1187 | int u; |
| @@ -1190,10 +1208,12 @@ | |
| 1190 | if( x+n>aw ){ |
| 1191 | break; |
| 1192 | } |
| 1193 | i += len; |
| 1194 | n += x; |
| 1195 | }else if( n>=aw ){ |
| 1196 | break; |
| 1197 | }else{ |
| 1198 | n++; |
| 1199 | i++; |
| @@ -6270,12 +6290,11 @@ | |
| 6270 | ** start HIDDEN, |
| 6271 | ** stop HIDDEN, |
| 6272 | ** step HIDDEN |
| 6273 | ** ); |
| 6274 | ** |
| 6275 | ** The virtual table also has a rowid, logically equivalent to n+1 where |
| 6276 | ** "n" is the ascending integer in the aforesaid production definition. |
| 6277 | ** |
| 6278 | ** Function arguments in queries against this virtual table are translated |
| 6279 | ** into equality constraints against successive hidden columns. In other |
| 6280 | ** words, the following pairs of queries are equivalent to each other: |
| 6281 | ** |
| @@ -6326,10 +6345,11 @@ | |
| 6326 | /* #include "sqlite3ext.h" */ |
| 6327 | SQLITE_EXTENSION_INIT1 |
| 6328 | #include <assert.h> |
| 6329 | #include <string.h> |
| 6330 | #include <limits.h> |
| 6331 | |
| 6332 | #ifndef SQLITE_OMIT_VIRTUALTABLE |
| 6333 | /* |
| 6334 | ** Return that member of a generate_series(...) sequence whose 0-based |
| 6335 | ** index is ix. The 0th member is given by smBase. The sequence members |
| @@ -6486,10 +6506,11 @@ | |
| 6486 | ){ |
| 6487 | sqlite3_vtab *pNew; |
| 6488 | int rc; |
| 6489 | |
| 6490 | /* Column numbers */ |
| 6491 | #define SERIES_COLUMN_VALUE 0 |
| 6492 | #define SERIES_COLUMN_START 1 |
| 6493 | #define SERIES_COLUMN_STOP 2 |
| 6494 | #define SERIES_COLUMN_STEP 3 |
| 6495 | |
| @@ -6573,17 +6594,15 @@ | |
| 6573 | #define LARGEST_UINT64 (0xffffffff|(((sqlite3_uint64)0xffffffff)<<32)) |
| 6574 | #define SMALLEST_INT64 (((sqlite3_int64)-1) - LARGEST_INT64) |
| 6575 | #endif |
| 6576 | |
| 6577 | /* |
| 6578 | ** Return the rowid for the current row, logically equivalent to n+1 where |
| 6579 | ** "n" is the ascending integer in the aforesaid production definition. |
| 6580 | */ |
| 6581 | static int seriesRowid(sqlite3_vtab_cursor *cur, sqlite_int64 *pRowid){ |
| 6582 | series_cursor *pCur = (series_cursor*)cur; |
| 6583 | sqlite3_uint64 n = pCur->ss.uSeqIndexNow; |
| 6584 | *pRowid = (sqlite3_int64)((n<LARGEST_UINT64)? n+1 : 0); |
| 6585 | return SQLITE_OK; |
| 6586 | } |
| 6587 | |
| 6588 | /* |
| 6589 | ** Return TRUE if the cursor has been moved off of the last |
| @@ -6692,29 +6711,56 @@ | |
| 6692 | if( idxNum & 0x3380 ){ |
| 6693 | /* Extract the maximum range of output values determined by |
| 6694 | ** constraints on the "value" column. |
| 6695 | */ |
| 6696 | if( idxNum & 0x0080 ){ |
| 6697 | iMin = iMax = sqlite3_value_int64(argv[i++]); |
| 6698 | }else{ |
| 6699 | if( idxNum & 0x0300 ){ |
| 6700 | iMin = sqlite3_value_int64(argv[i++]); |
| 6701 | if( idxNum & 0x0200 ){ |
| 6702 | if( iMin==LARGEST_INT64 ){ |
| 6703 | returnNoRows = 1; |
| 6704 | }else{ |
| 6705 | iMin++; |
| 6706 | } |
| 6707 | } |
| 6708 | } |
| 6709 | if( idxNum & 0x3000 ){ |
| 6710 | iMax = sqlite3_value_int64(argv[i++]); |
| 6711 | if( idxNum & 0x2000 ){ |
| 6712 | if( iMax==SMALLEST_INT64 ){ |
| 6713 | returnNoRows = 1; |
| 6714 | }else{ |
| 6715 | iMax--; |
| 6716 | } |
| 6717 | } |
| 6718 | } |
| 6719 | if( iMin>iMax ){ |
| 6720 | returnNoRows = 1; |
| @@ -6764,11 +6810,11 @@ | |
| 6764 | |
| 6765 | |
| 6766 | for(i=0; i<argc; i++){ |
| 6767 | if( sqlite3_value_type(argv[i])==SQLITE_NULL ){ |
| 6768 | /* If any of the constraints have a NULL value, then return no rows. |
| 6769 | ** See ticket https://www.sqlite.org/src/info/fac496b61722daf2 */ |
| 6770 | returnNoRows = 1; |
| 6771 | break; |
| 6772 | } |
| 6773 | } |
| 6774 | if( returnNoRows ){ |
| @@ -6867,11 +6913,14 @@ | |
| 6867 | idxNum |= 0x40; |
| 6868 | } |
| 6869 | continue; |
| 6870 | } |
| 6871 | if( pConstraint->iColumn<SERIES_COLUMN_START ){ |
| 6872 | if( pConstraint->iColumn==SERIES_COLUMN_VALUE && pConstraint->usable ){ |
| 6873 | switch( op ){ |
| 6874 | case SQLITE_INDEX_CONSTRAINT_EQ: |
| 6875 | case SQLITE_INDEX_CONSTRAINT_IS: { |
| 6876 | idxNum |= 0x0080; |
| 6877 | idxNum &= ~0x3300; |
| @@ -24197,13 +24246,18 @@ | |
| 24197 | j++; |
| 24198 | }while( (n&7)!=0 && n<mxWidth ); |
| 24199 | i++; |
| 24200 | continue; |
| 24201 | } |
| 24202 | n++; |
| 24203 | j += 3; |
| 24204 | i++; |
| 24205 | } |
| 24206 | if( n>=mxWidth && bWordWrap ){ |
| 24207 | /* Perhaps try to back up to a better place to break the line */ |
| 24208 | for(k=i; k>i/2; k--){ |
| 24209 | if( IsSpace(z[k-1]) ) break; |
| @@ -24265,13 +24319,21 @@ | |
| 24265 | break; |
| 24266 | case SHELL_ESC_ASCII: |
| 24267 | zOut[j++] = '^'; |
| 24268 | zOut[j++] = 0x40 + c; |
| 24269 | break; |
| 24270 | case SHELL_ESC_OFF: |
| 24271 | zOut[j++] = c; |
| 24272 | break; |
| 24273 | } |
| 24274 | i++; |
| 24275 | } |
| 24276 | zOut[j] = 0; |
| 24277 | return (char*)zOut; |
| @@ -25404,10 +25466,11 @@ | |
| 25404 | " --readonly Open FILE readonly", |
| 25405 | " --zip FILE is a ZIP archive", |
| 25406 | #ifndef SQLITE_SHELL_FIDDLE |
| 25407 | ".output ?FILE? Send output to FILE or stdout if FILE is omitted", |
| 25408 | " If FILE begins with '|' then open it as a pipe.", |
| 25409 | " Options:", |
| 25410 | " --bom Prefix output with a UTF8 byte-order mark", |
| 25411 | " -e Send output to the system text editor", |
| 25412 | " --plain Use text/plain for -w option", |
| 25413 | " -w Send output to a web browser", |
| @@ -27021,11 +27084,11 @@ | |
| 27021 | for(i=0; i<pgSz; i+=16){ |
| 27022 | const u8 *aLine = aData+i; |
| 27023 | for(j=0; j<16 && aLine[j]==0; j++){} |
| 27024 | if( j==16 ) continue; |
| 27025 | if( !seenPageLabel ){ |
| 27026 | sqlite3_fprintf(p->out, "| page %lld offset %lld\n", pgno, pgno*pgSz); |
| 27027 | seenPageLabel = 1; |
| 27028 | } |
| 27029 | sqlite3_fprintf(p->out, "| %5d:", i); |
| 27030 | for(j=0; j<16; j++) sqlite3_fprintf(p->out, " %02x", aLine[j]); |
| 27031 | sqlite3_fprintf(p->out, " "); |
| @@ -30399,13 +30462,13 @@ | |
| 30399 | || (c=='e' && n==5 && cli_strcmp(azArg[0],"excel")==0) |
| 30400 | || (c=='w' && n==3 && cli_strcmp(azArg[0],"www")==0) |
| 30401 | ){ |
| 30402 | char *zFile = 0; |
| 30403 | int i; |
| 30404 | int eMode = 0; |
| 30405 | int bOnce = 0; /* 0: .output, 1: .once, 2: .excel/.www */ |
| 30406 | int bPlain = 0; /* --plain option */ |
| 30407 | static const char *zBomUtf8 = "\357\273\277"; |
| 30408 | const char *zBom = 0; |
| 30409 | |
| 30410 | failIfSafeMode(p, "cannot run .%s in safe mode", azArg[0]); |
| 30411 | if( c=='e' ){ |
| @@ -30430,18 +30493,26 @@ | |
| 30430 | }else if( c=='o' && cli_strcmp(z,"-e")==0 ){ |
| 30431 | eMode = 'e'; /* text editor */ |
| 30432 | }else if( c=='o' && cli_strcmp(z,"-w")==0 ){ |
| 30433 | eMode = 'w'; /* Web browser */ |
| 30434 | }else{ |
| 30435 | sqlite3_fprintf(p->out, |
| 30436 | "ERROR: unknown option: \"%s\". Usage:\n", azArg[i]); |
| 30437 | showHelp(p->out, azArg[0]); |
| 30438 | rc = 1; |
| 30439 | goto meta_command_exit; |
| 30440 | } |
| 30441 | }else if( zFile==0 && eMode==0 ){ |
| 30442 | zFile = sqlite3_mprintf("%s", z); |
| 30443 | if( zFile && zFile[0]=='|' ){ |
| 30444 | while( i+1<nArg ) zFile = sqlite3_mprintf("%z %s", zFile, azArg[++i]); |
| 30445 | break; |
| 30446 | } |
| 30447 | }else{ |
| 30448 |
| --- extsrc/shell.c | |
| +++ extsrc/shell.c | |
| @@ -1162,10 +1162,27 @@ | |
| 1162 | } |
| 1163 | } |
| 1164 | return n; |
| 1165 | } |
| 1166 | #endif |
| 1167 | |
| 1168 | /* |
| 1169 | ** Check to see if z[] is a valid VT100 escape. If it is, then |
| 1170 | ** return the number of bytes in the escape sequence. Return 0 if |
| 1171 | ** z[] is not a VT100 escape. |
| 1172 | ** |
| 1173 | ** This routine assumes that z[0] is \033 (ESC). |
| 1174 | */ |
| 1175 | static int isVt100(const unsigned char *z){ |
| 1176 | int i; |
| 1177 | if( z[1]!='[' ) return 0; |
| 1178 | i = 2; |
| 1179 | while( z[i]>=0x30 && z[i]<=0x3f ){ i++; } |
| 1180 | while( z[i]>=0x20 && z[i]<=0x2f ){ i++; } |
| 1181 | if( z[i]<0x40 || z[i]>0x7e ) return 0; |
| 1182 | return i+1; |
| 1183 | } |
| 1184 | |
| 1185 | /* |
| 1186 | ** Output string zUtf to stdout as w characters. If w is negative, |
| 1187 | ** then right-justify the text. W is the width in UTF-8 characters, not |
| 1188 | ** in bytes. This is different from the %*.*s specification in printf |
| @@ -1178,10 +1195,11 @@ | |
| 1195 | static void utf8_width_print(FILE *out, int w, const char *zUtf){ |
| 1196 | const unsigned char *a = (const unsigned char*)zUtf; |
| 1197 | unsigned char c; |
| 1198 | int i = 0; |
| 1199 | int n = 0; |
| 1200 | int k; |
| 1201 | int aw = w<0 ? -w : w; |
| 1202 | if( zUtf==0 ) zUtf = ""; |
| 1203 | while( (c = a[i])!=0 ){ |
| 1204 | if( (c&0xc0)==0xc0 ){ |
| 1205 | int u; |
| @@ -1190,10 +1208,12 @@ | |
| 1208 | if( x+n>aw ){ |
| 1209 | break; |
| 1210 | } |
| 1211 | i += len; |
| 1212 | n += x; |
| 1213 | }else if( c==0x1b && (k = isVt100(&a[i]))>0 ){ |
| 1214 | i += k; |
| 1215 | }else if( n>=aw ){ |
| 1216 | break; |
| 1217 | }else{ |
| 1218 | n++; |
| 1219 | i++; |
| @@ -6270,12 +6290,11 @@ | |
| 6290 | ** start HIDDEN, |
| 6291 | ** stop HIDDEN, |
| 6292 | ** step HIDDEN |
| 6293 | ** ); |
| 6294 | ** |
| 6295 | ** The virtual table also has a rowid which is an alias for the value. |
| 6296 | ** |
| 6297 | ** Function arguments in queries against this virtual table are translated |
| 6298 | ** into equality constraints against successive hidden columns. In other |
| 6299 | ** words, the following pairs of queries are equivalent to each other: |
| 6300 | ** |
| @@ -6326,10 +6345,11 @@ | |
| 6345 | /* #include "sqlite3ext.h" */ |
| 6346 | SQLITE_EXTENSION_INIT1 |
| 6347 | #include <assert.h> |
| 6348 | #include <string.h> |
| 6349 | #include <limits.h> |
| 6350 | #include <math.h> |
| 6351 | |
| 6352 | #ifndef SQLITE_OMIT_VIRTUALTABLE |
| 6353 | /* |
| 6354 | ** Return that member of a generate_series(...) sequence whose 0-based |
| 6355 | ** index is ix. The 0th member is given by smBase. The sequence members |
| @@ -6486,10 +6506,11 @@ | |
| 6506 | ){ |
| 6507 | sqlite3_vtab *pNew; |
| 6508 | int rc; |
| 6509 | |
| 6510 | /* Column numbers */ |
| 6511 | #define SERIES_COLUMN_ROWID (-1) |
| 6512 | #define SERIES_COLUMN_VALUE 0 |
| 6513 | #define SERIES_COLUMN_START 1 |
| 6514 | #define SERIES_COLUMN_STOP 2 |
| 6515 | #define SERIES_COLUMN_STEP 3 |
| 6516 | |
| @@ -6573,17 +6594,15 @@ | |
| 6594 | #define LARGEST_UINT64 (0xffffffff|(((sqlite3_uint64)0xffffffff)<<32)) |
| 6595 | #define SMALLEST_INT64 (((sqlite3_int64)-1) - LARGEST_INT64) |
| 6596 | #endif |
| 6597 | |
| 6598 | /* |
| 6599 | ** The rowid is the same as the value. |
| 6600 | */ |
| 6601 | static int seriesRowid(sqlite3_vtab_cursor *cur, sqlite_int64 *pRowid){ |
| 6602 | series_cursor *pCur = (series_cursor*)cur; |
| 6603 | *pRowid = pCur->ss.iValueNow; |
| 6604 | return SQLITE_OK; |
| 6605 | } |
| 6606 | |
| 6607 | /* |
| 6608 | ** Return TRUE if the cursor has been moved off of the last |
| @@ -6692,29 +6711,56 @@ | |
| 6711 | if( idxNum & 0x3380 ){ |
| 6712 | /* Extract the maximum range of output values determined by |
| 6713 | ** constraints on the "value" column. |
| 6714 | */ |
| 6715 | if( idxNum & 0x0080 ){ |
| 6716 | if( sqlite3_value_numeric_type(argv[i])==SQLITE_FLOAT ){ |
| 6717 | double r = sqlite3_value_double(argv[i++]); |
| 6718 | if( r==ceil(r) ){ |
| 6719 | iMin = iMax = (sqlite3_int64)r; |
| 6720 | }else{ |
| 6721 | returnNoRows = 1; |
| 6722 | } |
| 6723 | }else{ |
| 6724 | iMin = iMax = sqlite3_value_int64(argv[i++]); |
| 6725 | } |
| 6726 | }else{ |
| 6727 | if( idxNum & 0x0300 ){ |
| 6728 | if( sqlite3_value_numeric_type(argv[i])==SQLITE_FLOAT ){ |
| 6729 | double r = sqlite3_value_double(argv[i++]); |
| 6730 | if( idxNum & 0x0200 && r==ceil(r) ){ |
| 6731 | iMin = (sqlite3_int64)ceil(r+1.0); |
| 6732 | }else{ |
| 6733 | iMin = (sqlite3_int64)ceil(r); |
| 6734 | } |
| 6735 | }else{ |
| 6736 | iMin = sqlite3_value_int64(argv[i++]); |
| 6737 | if( idxNum & 0x0200 ){ |
| 6738 | if( iMin==LARGEST_INT64 ){ |
| 6739 | returnNoRows = 1; |
| 6740 | }else{ |
| 6741 | iMin++; |
| 6742 | } |
| 6743 | } |
| 6744 | } |
| 6745 | } |
| 6746 | if( idxNum & 0x3000 ){ |
| 6747 | if( sqlite3_value_numeric_type(argv[i])==SQLITE_FLOAT ){ |
| 6748 | double r = sqlite3_value_double(argv[i++]); |
| 6749 | if( (idxNum & 0x2000)!=0 && r==floor(r) ){ |
| 6750 | iMax = (sqlite3_int64)(r-1.0); |
| 6751 | }else{ |
| 6752 | iMax = (sqlite3_int64)floor(r); |
| 6753 | } |
| 6754 | }else{ |
| 6755 | iMax = sqlite3_value_int64(argv[i++]); |
| 6756 | if( idxNum & 0x2000 ){ |
| 6757 | if( iMax==SMALLEST_INT64 ){ |
| 6758 | returnNoRows = 1; |
| 6759 | }else{ |
| 6760 | iMax--; |
| 6761 | } |
| 6762 | } |
| 6763 | } |
| 6764 | } |
| 6765 | if( iMin>iMax ){ |
| 6766 | returnNoRows = 1; |
| @@ -6764,11 +6810,11 @@ | |
| 6810 | |
| 6811 | |
| 6812 | for(i=0; i<argc; i++){ |
| 6813 | if( sqlite3_value_type(argv[i])==SQLITE_NULL ){ |
| 6814 | /* If any of the constraints have a NULL value, then return no rows. |
| 6815 | ** See ticket https://sqlite.org/src/info/fac496b61722daf2 */ |
| 6816 | returnNoRows = 1; |
| 6817 | break; |
| 6818 | } |
| 6819 | } |
| 6820 | if( returnNoRows ){ |
| @@ -6867,11 +6913,14 @@ | |
| 6913 | idxNum |= 0x40; |
| 6914 | } |
| 6915 | continue; |
| 6916 | } |
| 6917 | if( pConstraint->iColumn<SERIES_COLUMN_START ){ |
| 6918 | if( (pConstraint->iColumn==SERIES_COLUMN_VALUE || |
| 6919 | pConstraint->iColumn==SERIES_COLUMN_ROWID) |
| 6920 | && pConstraint->usable |
| 6921 | ){ |
| 6922 | switch( op ){ |
| 6923 | case SQLITE_INDEX_CONSTRAINT_EQ: |
| 6924 | case SQLITE_INDEX_CONSTRAINT_IS: { |
| 6925 | idxNum |= 0x0080; |
| 6926 | idxNum &= ~0x3300; |
| @@ -24197,13 +24246,18 @@ | |
| 24246 | j++; |
| 24247 | }while( (n&7)!=0 && n<mxWidth ); |
| 24248 | i++; |
| 24249 | continue; |
| 24250 | } |
| 24251 | if( c==0x1b && p->eEscMode==SHELL_ESC_OFF && (k = isVt100(&z[i]))>0 ){ |
| 24252 | i += k; |
| 24253 | j += k; |
| 24254 | }else{ |
| 24255 | n++; |
| 24256 | j += 3; |
| 24257 | i++; |
| 24258 | } |
| 24259 | } |
| 24260 | if( n>=mxWidth && bWordWrap ){ |
| 24261 | /* Perhaps try to back up to a better place to break the line */ |
| 24262 | for(k=i; k>i/2; k--){ |
| 24263 | if( IsSpace(z[k-1]) ) break; |
| @@ -24265,13 +24319,21 @@ | |
| 24319 | break; |
| 24320 | case SHELL_ESC_ASCII: |
| 24321 | zOut[j++] = '^'; |
| 24322 | zOut[j++] = 0x40 + c; |
| 24323 | break; |
| 24324 | case SHELL_ESC_OFF: { |
| 24325 | int nn; |
| 24326 | if( c==0x1b && (nn = isVt100(&z[i]))>0 ){ |
| 24327 | memcpy(&zOut[j], &z[i], nn); |
| 24328 | j += nn; |
| 24329 | i += nn - 1; |
| 24330 | }else{ |
| 24331 | zOut[j++] = c; |
| 24332 | } |
| 24333 | break; |
| 24334 | } |
| 24335 | } |
| 24336 | i++; |
| 24337 | } |
| 24338 | zOut[j] = 0; |
| 24339 | return (char*)zOut; |
| @@ -25404,10 +25466,11 @@ | |
| 25466 | " --readonly Open FILE readonly", |
| 25467 | " --zip FILE is a ZIP archive", |
| 25468 | #ifndef SQLITE_SHELL_FIDDLE |
| 25469 | ".output ?FILE? Send output to FILE or stdout if FILE is omitted", |
| 25470 | " If FILE begins with '|' then open it as a pipe.", |
| 25471 | " If FILE is 'off' then output is disabled.", |
| 25472 | " Options:", |
| 25473 | " --bom Prefix output with a UTF8 byte-order mark", |
| 25474 | " -e Send output to the system text editor", |
| 25475 | " --plain Use text/plain for -w option", |
| 25476 | " -w Send output to a web browser", |
| @@ -27021,11 +27084,11 @@ | |
| 27084 | for(i=0; i<pgSz; i+=16){ |
| 27085 | const u8 *aLine = aData+i; |
| 27086 | for(j=0; j<16 && aLine[j]==0; j++){} |
| 27087 | if( j==16 ) continue; |
| 27088 | if( !seenPageLabel ){ |
| 27089 | sqlite3_fprintf(p->out, "| page %lld offset %lld\n",pgno,(pgno-1)*pgSz); |
| 27090 | seenPageLabel = 1; |
| 27091 | } |
| 27092 | sqlite3_fprintf(p->out, "| %5d:", i); |
| 27093 | for(j=0; j<16; j++) sqlite3_fprintf(p->out, " %02x", aLine[j]); |
| 27094 | sqlite3_fprintf(p->out, " "); |
| @@ -30399,13 +30462,13 @@ | |
| 30462 | || (c=='e' && n==5 && cli_strcmp(azArg[0],"excel")==0) |
| 30463 | || (c=='w' && n==3 && cli_strcmp(azArg[0],"www")==0) |
| 30464 | ){ |
| 30465 | char *zFile = 0; |
| 30466 | int i; |
| 30467 | int eMode = 0; /* 0: .outout/.once, 'x'=.excel, 'w'=.www */ |
| 30468 | int bOnce = 0; /* 0: .output, 1: .once, 2: .excel/.www */ |
| 30469 | int bPlain = 0; /* --plain option */ |
| 30470 | static const char *zBomUtf8 = "\357\273\277"; |
| 30471 | const char *zBom = 0; |
| 30472 | |
| 30473 | failIfSafeMode(p, "cannot run .%s in safe mode", azArg[0]); |
| 30474 | if( c=='e' ){ |
| @@ -30430,18 +30493,26 @@ | |
| 30493 | }else if( c=='o' && cli_strcmp(z,"-e")==0 ){ |
| 30494 | eMode = 'e'; /* text editor */ |
| 30495 | }else if( c=='o' && cli_strcmp(z,"-w")==0 ){ |
| 30496 | eMode = 'w'; /* Web browser */ |
| 30497 | }else{ |
| 30498 | sqlite3_fprintf(p->out, |
| 30499 | "ERROR: unknown option: \"%s\". Usage:\n", azArg[i]); |
| 30500 | showHelp(p->out, azArg[0]); |
| 30501 | rc = 1; |
| 30502 | goto meta_command_exit; |
| 30503 | } |
| 30504 | }else if( zFile==0 && eMode==0 ){ |
| 30505 | if( cli_strcmp(z, "off")==0 ){ |
| 30506 | #ifdef _WIN32 |
| 30507 | zFile = sqlite3_mprintf("nul"); |
| 30508 | #else |
| 30509 | zFile = sqlite3_mprintf("/dev/null"); |
| 30510 | #endif |
| 30511 | }else{ |
| 30512 | zFile = sqlite3_mprintf("%s", z); |
| 30513 | } |
| 30514 | if( zFile && zFile[0]=='|' ){ |
| 30515 | while( i+1<nArg ) zFile = sqlite3_mprintf("%z %s", zFile, azArg[++i]); |
| 30516 | break; |
| 30517 | } |
| 30518 | }else{ |
| 30519 |
+588
-151
| --- extsrc/sqlite3.c | ||
| +++ extsrc/sqlite3.c | ||
| @@ -16,11 +16,11 @@ | ||
| 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 | -** 18bda13e197e4b4ec7464b3e70012f71edc0 with changes in files: | |
| 21 | +** d22475b81c4e26ccc50f3b5626d43b32f7a2 with changes in files: | |
| 22 | 22 | ** |
| 23 | 23 | ** |
| 24 | 24 | */ |
| 25 | 25 | #ifndef SQLITE_AMALGAMATION |
| 26 | 26 | #define SQLITE_CORE 1 |
| @@ -450,11 +450,11 @@ | ||
| 450 | 450 | ** be held constant and Z will be incremented or else Y will be incremented |
| 451 | 451 | ** and Z will be reset to zero. |
| 452 | 452 | ** |
| 453 | 453 | ** Since [version 3.6.18] ([dateof:3.6.18]), |
| 454 | 454 | ** SQLite source code has been stored in the |
| 455 | -** <a href="http://www.fossil-scm.org/">Fossil configuration management | |
| 455 | +** <a href="http://fossil-scm.org/">Fossil configuration management | |
| 456 | 456 | ** system</a>. ^The SQLITE_SOURCE_ID macro evaluates to |
| 457 | 457 | ** a string which identifies a particular check-in of SQLite |
| 458 | 458 | ** within its configuration management system. ^The SQLITE_SOURCE_ID |
| 459 | 459 | ** string contains the date and time of the check-in (UTC) and a SHA1 |
| 460 | 460 | ** or SHA3-256 hash of the entire source tree. If the source code has |
| @@ -465,11 +465,11 @@ | ||
| 465 | 465 | ** [sqlite3_libversion_number()], [sqlite3_sourceid()], |
| 466 | 466 | ** [sqlite_version()] and [sqlite_source_id()]. |
| 467 | 467 | */ |
| 468 | 468 | #define SQLITE_VERSION "3.50.0" |
| 469 | 469 | #define SQLITE_VERSION_NUMBER 3050000 |
| 470 | -#define SQLITE_SOURCE_ID "2025-03-16 00:13:29 18bda13e197e4b4ec7464b3e70012f71edc05f73d8b14bb48bad452f81c7e185" | |
| 470 | +#define SQLITE_SOURCE_ID "2025-04-15 21:59:38 d22475b81c4e26ccc50f3b5626d43b32f7a2de34e5a764539554665bdda735d5" | |
| 471 | 471 | |
| 472 | 472 | /* |
| 473 | 473 | ** CAPI3REF: Run-Time Library Version Numbers |
| 474 | 474 | ** KEYWORDS: sqlite3_version sqlite3_sourceid |
| 475 | 475 | ** |
| @@ -11946,12 +11946,13 @@ | ||
| 11946 | 11946 | ** To clarify, if this function is called and then a changeset constructed |
| 11947 | 11947 | ** using [sqlite3session_changeset()], then after applying that changeset to |
| 11948 | 11948 | ** database zFrom the contents of the two compatible tables would be |
| 11949 | 11949 | ** identical. |
| 11950 | 11950 | ** |
| 11951 | -** It an error if database zFrom does not exist or does not contain the | |
| 11952 | -** required compatible table. | |
| 11951 | +** Unless the call to this function is a no-op as described above, it is an | |
| 11952 | +** error if database zFrom does not exist or does not contain the required | |
| 11953 | +** compatible table. | |
| 11953 | 11954 | ** |
| 11954 | 11955 | ** If the operation is successful, SQLITE_OK is returned. Otherwise, an SQLite |
| 11955 | 11956 | ** error code. In this case, if argument pzErrMsg is not NULL, *pzErrMsg |
| 11956 | 11957 | ** may be set to point to a buffer containing an English language error |
| 11957 | 11958 | ** message. It is the responsibility of the caller to free this buffer using |
| @@ -19162,10 +19163,11 @@ | ||
| 19162 | 19163 | unsigned noSkipScan:1; /* Do not try to use skip-scan if true */ |
| 19163 | 19164 | unsigned hasStat1:1; /* aiRowLogEst values come from sqlite_stat1 */ |
| 19164 | 19165 | unsigned bLowQual:1; /* sqlite_stat1 says this is a low-quality index */ |
| 19165 | 19166 | unsigned bNoQuery:1; /* Do not use this index to optimize queries */ |
| 19166 | 19167 | unsigned bAscKeyBug:1; /* True if the bba7b69f9849b5bf bug applies */ |
| 19168 | + unsigned bIdxRowid:1; /* One or more of the index keys is the ROWID */ | |
| 19167 | 19169 | unsigned bHasVCol:1; /* Index references one or more VIRTUAL columns */ |
| 19168 | 19170 | unsigned bHasExpr:1; /* Index contains an expression, either a literal |
| 19169 | 19171 | ** expression, or a reference to a VIRTUAL column */ |
| 19170 | 19172 | #ifdef SQLITE_ENABLE_STAT4 |
| 19171 | 19173 | int nSample; /* Number of elements in aSample[] */ |
| @@ -30270,10 +30272,12 @@ | ||
| 30270 | 30272 | */ |
| 30271 | 30273 | #include "windows.h" |
| 30272 | 30274 | |
| 30273 | 30275 | #ifdef __CYGWIN__ |
| 30274 | 30276 | # include <sys/cygwin.h> |
| 30277 | +# include <sys/stat.h> /* amalgamator: dontcache */ | |
| 30278 | +# include <unistd.h> /* amalgamator: dontcache */ | |
| 30275 | 30279 | # include <errno.h> /* amalgamator: dontcache */ |
| 30276 | 30280 | #endif |
| 30277 | 30281 | |
| 30278 | 30282 | /* |
| 30279 | 30283 | ** Determine if we are dealing with Windows NT. |
| @@ -35444,11 +35448,11 @@ | ||
| 35444 | 35448 | unsigned char const *z = zIn; |
| 35445 | 35449 | unsigned char const *zEnd = &z[nByte-1]; |
| 35446 | 35450 | int n = 0; |
| 35447 | 35451 | |
| 35448 | 35452 | if( SQLITE_UTF16NATIVE==SQLITE_UTF16LE ) z++; |
| 35449 | - while( n<nChar && ALWAYS(z<=zEnd) ){ | |
| 35453 | + while( n<nChar && z<=zEnd ){ | |
| 35450 | 35454 | c = z[0]; |
| 35451 | 35455 | z += 2; |
| 35452 | 35456 | if( c>=0xd8 && c<0xdc && z<=zEnd && z[0]>=0xdc && z[0]<0xe0 ) z += 2; |
| 35453 | 35457 | n++; |
| 35454 | 35458 | } |
| @@ -47726,20 +47730,20 @@ | ||
| 47726 | 47730 | { "FileTimeToLocalFileTime", (SYSCALL)FileTimeToLocalFileTime, 0 }, |
| 47727 | 47731 | #else |
| 47728 | 47732 | { "FileTimeToLocalFileTime", (SYSCALL)0, 0 }, |
| 47729 | 47733 | #endif |
| 47730 | 47734 | |
| 47731 | -#define osFileTimeToLocalFileTime ((BOOL(WINAPI*)(CONST FILETIME*, \ | |
| 47735 | +#define osFileTimeToLocalFileTime ((BOOL(WINAPI*)(const FILETIME*, \ | |
| 47732 | 47736 | LPFILETIME))aSyscall[11].pCurrent) |
| 47733 | 47737 | |
| 47734 | 47738 | #if SQLITE_OS_WINCE |
| 47735 | 47739 | { "FileTimeToSystemTime", (SYSCALL)FileTimeToSystemTime, 0 }, |
| 47736 | 47740 | #else |
| 47737 | 47741 | { "FileTimeToSystemTime", (SYSCALL)0, 0 }, |
| 47738 | 47742 | #endif |
| 47739 | 47743 | |
| 47740 | -#define osFileTimeToSystemTime ((BOOL(WINAPI*)(CONST FILETIME*, \ | |
| 47744 | +#define osFileTimeToSystemTime ((BOOL(WINAPI*)(const FILETIME*, \ | |
| 47741 | 47745 | LPSYSTEMTIME))aSyscall[12].pCurrent) |
| 47742 | 47746 | |
| 47743 | 47747 | { "FlushFileBuffers", (SYSCALL)FlushFileBuffers, 0 }, |
| 47744 | 47748 | |
| 47745 | 47749 | #define osFlushFileBuffers ((BOOL(WINAPI*)(HANDLE))aSyscall[13].pCurrent) |
| @@ -48015,11 +48019,11 @@ | ||
| 48015 | 48019 | { "LockFile", (SYSCALL)LockFile, 0 }, |
| 48016 | 48020 | #else |
| 48017 | 48021 | { "LockFile", (SYSCALL)0, 0 }, |
| 48018 | 48022 | #endif |
| 48019 | 48023 | |
| 48020 | -#ifndef osLockFile | |
| 48024 | +#if !defined(osLockFile) && defined(SQLITE_WIN32_HAS_ANSI) | |
| 48021 | 48025 | #define osLockFile ((BOOL(WINAPI*)(HANDLE,DWORD,DWORD,DWORD, \ |
| 48022 | 48026 | DWORD))aSyscall[47].pCurrent) |
| 48023 | 48027 | #endif |
| 48024 | 48028 | |
| 48025 | 48029 | #if !SQLITE_OS_WINCE |
| @@ -48079,20 +48083,20 @@ | ||
| 48079 | 48083 | |
| 48080 | 48084 | #define osSleep ((VOID(WINAPI*)(DWORD))aSyscall[55].pCurrent) |
| 48081 | 48085 | |
| 48082 | 48086 | { "SystemTimeToFileTime", (SYSCALL)SystemTimeToFileTime, 0 }, |
| 48083 | 48087 | |
| 48084 | -#define osSystemTimeToFileTime ((BOOL(WINAPI*)(CONST SYSTEMTIME*, \ | |
| 48088 | +#define osSystemTimeToFileTime ((BOOL(WINAPI*)(const SYSTEMTIME*, \ | |
| 48085 | 48089 | LPFILETIME))aSyscall[56].pCurrent) |
| 48086 | 48090 | |
| 48087 | 48091 | #if !SQLITE_OS_WINCE && !SQLITE_OS_WINRT |
| 48088 | 48092 | { "UnlockFile", (SYSCALL)UnlockFile, 0 }, |
| 48089 | 48093 | #else |
| 48090 | 48094 | { "UnlockFile", (SYSCALL)0, 0 }, |
| 48091 | 48095 | #endif |
| 48092 | 48096 | |
| 48093 | -#ifndef osUnlockFile | |
| 48097 | +#if !defined(osUnlockFile) && defined(SQLITE_WIN32_HAS_ANSI) | |
| 48094 | 48098 | #define osUnlockFile ((BOOL(WINAPI*)(HANDLE,DWORD,DWORD,DWORD, \ |
| 48095 | 48099 | DWORD))aSyscall[57].pCurrent) |
| 48096 | 48100 | #endif |
| 48097 | 48101 | |
| 48098 | 48102 | #if !SQLITE_OS_WINCE |
| @@ -48316,10 +48320,67 @@ | ||
| 48316 | 48320 | { "CancelIo", (SYSCALL)0, 0 }, |
| 48317 | 48321 | #endif |
| 48318 | 48322 | |
| 48319 | 48323 | #define osCancelIo ((BOOL(WINAPI*)(HANDLE))aSyscall[81].pCurrent) |
| 48320 | 48324 | |
| 48325 | +#if defined(SQLITE_WIN32_HAS_WIDE) && defined(_WIN32) | |
| 48326 | + { "GetModuleHandleW", (SYSCALL)GetModuleHandleW, 0 }, | |
| 48327 | +#else | |
| 48328 | + { "GetModuleHandleW", (SYSCALL)0, 0 }, | |
| 48329 | +#endif | |
| 48330 | + | |
| 48331 | +#define osGetModuleHandleW ((HMODULE(WINAPI*)(LPCWSTR))aSyscall[82].pCurrent) | |
| 48332 | + | |
| 48333 | +#ifndef _WIN32 | |
| 48334 | + { "getenv", (SYSCALL)getenv, 0 }, | |
| 48335 | +#else | |
| 48336 | + { "getenv", (SYSCALL)0, 0 }, | |
| 48337 | +#endif | |
| 48338 | + | |
| 48339 | +#define osGetenv ((const char *(*)(const char *))aSyscall[83].pCurrent) | |
| 48340 | + | |
| 48341 | +#ifndef _WIN32 | |
| 48342 | + { "getcwd", (SYSCALL)getcwd, 0 }, | |
| 48343 | +#else | |
| 48344 | + { "getcwd", (SYSCALL)0, 0 }, | |
| 48345 | +#endif | |
| 48346 | + | |
| 48347 | +#define osGetcwd ((char*(*)(char*,size_t))aSyscall[84].pCurrent) | |
| 48348 | + | |
| 48349 | +#ifndef _WIN32 | |
| 48350 | + { "readlink", (SYSCALL)readlink, 0 }, | |
| 48351 | +#else | |
| 48352 | + { "readlink", (SYSCALL)0, 0 }, | |
| 48353 | +#endif | |
| 48354 | + | |
| 48355 | +#define osReadlink ((ssize_t(*)(const char*,char*,size_t))aSyscall[85].pCurrent) | |
| 48356 | + | |
| 48357 | +#ifndef _WIN32 | |
| 48358 | + { "lstat", (SYSCALL)lstat, 0 }, | |
| 48359 | +#else | |
| 48360 | + { "lstat", (SYSCALL)0, 0 }, | |
| 48361 | +#endif | |
| 48362 | + | |
| 48363 | +#define osLstat ((int(*)(const char*,struct stat*))aSyscall[86].pCurrent) | |
| 48364 | + | |
| 48365 | +#ifndef _WIN32 | |
| 48366 | + { "__errno", (SYSCALL)__errno, 0 }, | |
| 48367 | +#else | |
| 48368 | + { "__errno", (SYSCALL)0, 0 }, | |
| 48369 | +#endif | |
| 48370 | + | |
| 48371 | +#define osErrno (*((int*(*)(void))aSyscall[87].pCurrent)()) | |
| 48372 | + | |
| 48373 | +#ifndef _WIN32 | |
| 48374 | + { "cygwin_conv_path", (SYSCALL)cygwin_conv_path, 0 }, | |
| 48375 | +#else | |
| 48376 | + { "cygwin_conv_path", (SYSCALL)0, 0 }, | |
| 48377 | +#endif | |
| 48378 | + | |
| 48379 | +#define osCygwin_conv_path ((size_t(*)(unsigned int, \ | |
| 48380 | + const void *, void *, size_t))aSyscall[88].pCurrent) | |
| 48381 | + | |
| 48321 | 48382 | }; /* End of the overrideable system calls */ |
| 48322 | 48383 | |
| 48323 | 48384 | /* |
| 48324 | 48385 | ** This is the xSetSystemCall() method of sqlite3_vfs for all of the |
| 48325 | 48386 | ** "win32" VFSes. Return SQLITE_OK upon successfully updating the |
| @@ -48489,10 +48550,11 @@ | ||
| 48489 | 48550 | sqlite3_mutex_leave(pMainMtx); |
| 48490 | 48551 | return rc; |
| 48491 | 48552 | } |
| 48492 | 48553 | #endif /* SQLITE_WIN32_MALLOC */ |
| 48493 | 48554 | |
| 48555 | +#ifdef _WIN32 | |
| 48494 | 48556 | /* |
| 48495 | 48557 | ** This function outputs the specified (ANSI) string to the Win32 debugger |
| 48496 | 48558 | ** (if available). |
| 48497 | 48559 | */ |
| 48498 | 48560 | |
| @@ -48531,10 +48593,11 @@ | ||
| 48531 | 48593 | }else{ |
| 48532 | 48594 | fprintf(stderr, "%s", zBuf); |
| 48533 | 48595 | } |
| 48534 | 48596 | #endif |
| 48535 | 48597 | } |
| 48598 | +#endif /* _WIN32 */ | |
| 48536 | 48599 | |
| 48537 | 48600 | /* |
| 48538 | 48601 | ** The following routine suspends the current thread for at least ms |
| 48539 | 48602 | ** milliseconds. This is equivalent to the Win32 Sleep() interface. |
| 48540 | 48603 | */ |
| @@ -48831,10 +48894,11 @@ | ||
| 48831 | 48894 | SQLITE_PRIVATE void sqlite3MemSetDefault(void){ |
| 48832 | 48895 | sqlite3_config(SQLITE_CONFIG_MALLOC, sqlite3MemGetWin32()); |
| 48833 | 48896 | } |
| 48834 | 48897 | #endif /* SQLITE_WIN32_MALLOC */ |
| 48835 | 48898 | |
| 48899 | +#ifdef _WIN32 | |
| 48836 | 48900 | /* |
| 48837 | 48901 | ** Convert a UTF-8 string to Microsoft Unicode. |
| 48838 | 48902 | ** |
| 48839 | 48903 | ** Space to hold the returned string is obtained from sqlite3_malloc(). |
| 48840 | 48904 | */ |
| @@ -48856,10 +48920,11 @@ | ||
| 48856 | 48920 | sqlite3_free(zWideText); |
| 48857 | 48921 | zWideText = 0; |
| 48858 | 48922 | } |
| 48859 | 48923 | return zWideText; |
| 48860 | 48924 | } |
| 48925 | +#endif /* _WIN32 */ | |
| 48861 | 48926 | |
| 48862 | 48927 | /* |
| 48863 | 48928 | ** Convert a Microsoft Unicode string to UTF-8. |
| 48864 | 48929 | ** |
| 48865 | 48930 | ** Space to hold the returned string is obtained from sqlite3_malloc(). |
| @@ -48890,32 +48955,33 @@ | ||
| 48890 | 48955 | ** code page. |
| 48891 | 48956 | ** |
| 48892 | 48957 | ** Space to hold the returned string is obtained from sqlite3_malloc(). |
| 48893 | 48958 | */ |
| 48894 | 48959 | static LPWSTR winMbcsToUnicode(const char *zText, int useAnsi){ |
| 48895 | - int nByte; | |
| 48960 | + int nWideChar; | |
| 48896 | 48961 | LPWSTR zMbcsText; |
| 48897 | 48962 | int codepage = useAnsi ? CP_ACP : CP_OEMCP; |
| 48898 | 48963 | |
| 48899 | - nByte = osMultiByteToWideChar(codepage, 0, zText, -1, NULL, | |
| 48900 | - 0)*sizeof(WCHAR); | |
| 48901 | - if( nByte==0 ){ | |
| 48964 | + nWideChar = osMultiByteToWideChar(codepage, 0, zText, -1, NULL, | |
| 48965 | + 0); | |
| 48966 | + if( nWideChar==0 ){ | |
| 48902 | 48967 | return 0; |
| 48903 | 48968 | } |
| 48904 | - zMbcsText = sqlite3MallocZero( nByte*sizeof(WCHAR) ); | |
| 48969 | + zMbcsText = sqlite3MallocZero( nWideChar*sizeof(WCHAR) ); | |
| 48905 | 48970 | if( zMbcsText==0 ){ |
| 48906 | 48971 | return 0; |
| 48907 | 48972 | } |
| 48908 | - nByte = osMultiByteToWideChar(codepage, 0, zText, -1, zMbcsText, | |
| 48909 | - nByte); | |
| 48910 | - if( nByte==0 ){ | |
| 48973 | + nWideChar = osMultiByteToWideChar(codepage, 0, zText, -1, zMbcsText, | |
| 48974 | + nWideChar); | |
| 48975 | + if( nWideChar==0 ){ | |
| 48911 | 48976 | sqlite3_free(zMbcsText); |
| 48912 | 48977 | zMbcsText = 0; |
| 48913 | 48978 | } |
| 48914 | 48979 | return zMbcsText; |
| 48915 | 48980 | } |
| 48916 | 48981 | |
| 48982 | +#ifdef _WIN32 | |
| 48917 | 48983 | /* |
| 48918 | 48984 | ** Convert a Microsoft Unicode string to a multi-byte character string, |
| 48919 | 48985 | ** using the ANSI or OEM code page. |
| 48920 | 48986 | ** |
| 48921 | 48987 | ** Space to hold the returned string is obtained from sqlite3_malloc(). |
| @@ -48939,10 +49005,11 @@ | ||
| 48939 | 49005 | sqlite3_free(zText); |
| 48940 | 49006 | zText = 0; |
| 48941 | 49007 | } |
| 48942 | 49008 | return zText; |
| 48943 | 49009 | } |
| 49010 | +#endif /* _WIN32 */ | |
| 48944 | 49011 | |
| 48945 | 49012 | /* |
| 48946 | 49013 | ** Convert a multi-byte character string to UTF-8. |
| 48947 | 49014 | ** |
| 48948 | 49015 | ** Space to hold the returned string is obtained from sqlite3_malloc(). |
| @@ -48958,10 +49025,11 @@ | ||
| 48958 | 49025 | zTextUtf8 = winUnicodeToUtf8(zTmpWide); |
| 48959 | 49026 | sqlite3_free(zTmpWide); |
| 48960 | 49027 | return zTextUtf8; |
| 48961 | 49028 | } |
| 48962 | 49029 | |
| 49030 | +#ifdef _WIN32 | |
| 48963 | 49031 | /* |
| 48964 | 49032 | ** Convert a UTF-8 string to a multi-byte character string. |
| 48965 | 49033 | ** |
| 48966 | 49034 | ** Space to hold the returned string is obtained from sqlite3_malloc(). |
| 48967 | 49035 | */ |
| @@ -49007,10 +49075,11 @@ | ||
| 49007 | 49075 | #ifndef SQLITE_OMIT_AUTOINIT |
| 49008 | 49076 | if( sqlite3_initialize() ) return 0; |
| 49009 | 49077 | #endif |
| 49010 | 49078 | return winUnicodeToUtf8(zWideText); |
| 49011 | 49079 | } |
| 49080 | +#endif /* _WIN32 */ | |
| 49012 | 49081 | |
| 49013 | 49082 | /* |
| 49014 | 49083 | ** This is a public wrapper for the winMbcsToUtf8() function. |
| 49015 | 49084 | */ |
| 49016 | 49085 | SQLITE_API char *sqlite3_win32_mbcs_to_utf8(const char *zText){ |
| @@ -49024,10 +49093,11 @@ | ||
| 49024 | 49093 | if( sqlite3_initialize() ) return 0; |
| 49025 | 49094 | #endif |
| 49026 | 49095 | return winMbcsToUtf8(zText, osAreFileApisANSI()); |
| 49027 | 49096 | } |
| 49028 | 49097 | |
| 49098 | +#ifdef _WIN32 | |
| 49029 | 49099 | /* |
| 49030 | 49100 | ** This is a public wrapper for the winMbcsToUtf8() function. |
| 49031 | 49101 | */ |
| 49032 | 49102 | SQLITE_API char *sqlite3_win32_mbcs_to_utf8_v2(const char *zText, int useAnsi){ |
| 49033 | 49103 | #ifdef SQLITE_ENABLE_API_ARMOR |
| @@ -49148,10 +49218,11 @@ | ||
| 49148 | 49218 | unsigned long type, /* Identifier for directory being set or reset */ |
| 49149 | 49219 | void *zValue /* New value for directory being set or reset */ |
| 49150 | 49220 | ){ |
| 49151 | 49221 | return sqlite3_win32_set_directory16(type, zValue); |
| 49152 | 49222 | } |
| 49223 | +#endif /* _WIN32 */ | |
| 49153 | 49224 | |
| 49154 | 49225 | /* |
| 49155 | 49226 | ** The return value of winGetLastErrorMsg |
| 49156 | 49227 | ** is zero if the error message fits in the buffer, or non-zero |
| 49157 | 49228 | ** otherwise (if the message was truncated). |
| @@ -49696,13 +49767,15 @@ | ||
| 49696 | 49767 | OVERLAPPED ovlp; |
| 49697 | 49768 | memset(&ovlp, 0, sizeof(OVERLAPPED)); |
| 49698 | 49769 | ovlp.Offset = offsetLow; |
| 49699 | 49770 | ovlp.OffsetHigh = offsetHigh; |
| 49700 | 49771 | return osLockFileEx(*phFile, flags, 0, numBytesLow, numBytesHigh, &ovlp); |
| 49772 | +#ifdef SQLITE_WIN32_HAS_ANSI | |
| 49701 | 49773 | }else{ |
| 49702 | 49774 | return osLockFile(*phFile, offsetLow, offsetHigh, numBytesLow, |
| 49703 | 49775 | numBytesHigh); |
| 49776 | +#endif | |
| 49704 | 49777 | } |
| 49705 | 49778 | #endif |
| 49706 | 49779 | } |
| 49707 | 49780 | |
| 49708 | 49781 | /* |
| @@ -49806,13 +49879,15 @@ | ||
| 49806 | 49879 | OVERLAPPED ovlp; |
| 49807 | 49880 | memset(&ovlp, 0, sizeof(OVERLAPPED)); |
| 49808 | 49881 | ovlp.Offset = offsetLow; |
| 49809 | 49882 | ovlp.OffsetHigh = offsetHigh; |
| 49810 | 49883 | return osUnlockFileEx(*phFile, 0, numBytesLow, numBytesHigh, &ovlp); |
| 49884 | +#ifdef SQLITE_WIN32_HAS_ANSI | |
| 49811 | 49885 | }else{ |
| 49812 | 49886 | return osUnlockFile(*phFile, offsetLow, offsetHigh, numBytesLow, |
| 49813 | 49887 | numBytesHigh); |
| 49888 | +#endif | |
| 49814 | 49889 | } |
| 49815 | 49890 | #endif |
| 49816 | 49891 | } |
| 49817 | 49892 | |
| 49818 | 49893 | /* |
| @@ -51222,18 +51297,95 @@ | ||
| 51222 | 51297 | |
| 51223 | 51298 | /* |
| 51224 | 51299 | ** Convert a UTF-8 filename into whatever form the underlying |
| 51225 | 51300 | ** operating system wants filenames in. Space to hold the result |
| 51226 | 51301 | ** is obtained from malloc and must be freed by the calling |
| 51227 | -** function. | |
| 51302 | +** function | |
| 51303 | +** | |
| 51304 | +** On Cygwin, 3 possible input forms are accepted: | |
| 51305 | +** - If the filename starts with "<drive>:/" or "<drive>:\", | |
| 51306 | +** it is converted to UTF-16 as-is. | |
| 51307 | +** - If the filename contains '/', it is assumed to be a | |
| 51308 | +** Cygwin absolute path, it is converted to a win32 | |
| 51309 | +** absolute path in UTF-16. | |
| 51310 | +** - Otherwise it must be a filename only, the win32 filename | |
| 51311 | +** is returned in UTF-16. | |
| 51312 | +** Note: If the function cygwin_conv_path() fails, only | |
| 51313 | +** UTF-8 -> UTF-16 conversion will be done. This can only | |
| 51314 | +** happen when the file path >32k, in which case winUtf8ToUnicode() | |
| 51315 | +** will fail too. | |
| 51228 | 51316 | */ |
| 51229 | 51317 | static void *winConvertFromUtf8Filename(const char *zFilename){ |
| 51230 | 51318 | void *zConverted = 0; |
| 51231 | 51319 | if( osIsNT() ){ |
| 51320 | +#ifdef __CYGWIN__ | |
| 51321 | + int nChar; | |
| 51322 | + LPWSTR zWideFilename; | |
| 51323 | + | |
| 51324 | + if( osCygwin_conv_path && !(winIsDriveLetterAndColon(zFilename) | |
| 51325 | + && winIsDirSep(zFilename[2])) ){ | |
| 51326 | + int nByte; | |
| 51327 | + int convertflag = CCP_POSIX_TO_WIN_W; | |
| 51328 | + if( !strchr(zFilename, '/') ) convertflag |= CCP_RELATIVE; | |
| 51329 | + nByte = (int)osCygwin_conv_path(convertflag, | |
| 51330 | + zFilename, 0, 0); | |
| 51331 | + if( nByte>0 ){ | |
| 51332 | + zConverted = sqlite3MallocZero(nByte+12); | |
| 51333 | + if ( zConverted==0 ){ | |
| 51334 | + return zConverted; | |
| 51335 | + } | |
| 51336 | + zWideFilename = zConverted; | |
| 51337 | + /* Filenames should be prefixed, except when converted | |
| 51338 | + * full path already starts with "\\?\". */ | |
| 51339 | + if( osCygwin_conv_path(convertflag, zFilename, | |
| 51340 | + zWideFilename+4, nByte)==0 ){ | |
| 51341 | + if( (convertflag&CCP_RELATIVE) ){ | |
| 51342 | + memmove(zWideFilename, zWideFilename+4, nByte); | |
| 51343 | + }else if( memcmp(zWideFilename+4, L"\\\\", 4) ){ | |
| 51344 | + memcpy(zWideFilename, L"\\\\?\\", 8); | |
| 51345 | + }else if( zWideFilename[6]!='?' ){ | |
| 51346 | + memmove(zWideFilename+6, zWideFilename+4, nByte); | |
| 51347 | + memcpy(zWideFilename, L"\\\\?\\UNC", 14); | |
| 51348 | + }else{ | |
| 51349 | + memmove(zWideFilename, zWideFilename+4, nByte); | |
| 51350 | + } | |
| 51351 | + return zConverted; | |
| 51352 | + } | |
| 51353 | + sqlite3_free(zConverted); | |
| 51354 | + } | |
| 51355 | + } | |
| 51356 | + nChar = osMultiByteToWideChar(CP_UTF8, 0, zFilename, -1, NULL, 0); | |
| 51357 | + if( nChar==0 ){ | |
| 51358 | + return 0; | |
| 51359 | + } | |
| 51360 | + zWideFilename = sqlite3MallocZero( nChar*sizeof(WCHAR)+12 ); | |
| 51361 | + if( zWideFilename==0 ){ | |
| 51362 | + return 0; | |
| 51363 | + } | |
| 51364 | + nChar = osMultiByteToWideChar(CP_UTF8, 0, zFilename, -1, | |
| 51365 | + zWideFilename, nChar); | |
| 51366 | + if( nChar==0 ){ | |
| 51367 | + sqlite3_free(zWideFilename); | |
| 51368 | + zWideFilename = 0; | |
| 51369 | + }else if( nChar>MAX_PATH | |
| 51370 | + && winIsDriveLetterAndColon(zFilename) | |
| 51371 | + && winIsDirSep(zFilename[2]) ){ | |
| 51372 | + memmove(zWideFilename+4, zWideFilename, nChar*sizeof(WCHAR)); | |
| 51373 | + zWideFilename[2] = '\\'; | |
| 51374 | + memcpy(zWideFilename, L"\\\\?\\", 8); | |
| 51375 | + }else if( nChar>MAX_PATH | |
| 51376 | + && winIsDirSep(zFilename[0]) && winIsDirSep(zFilename[1]) | |
| 51377 | + && zFilename[2] != '?' ){ | |
| 51378 | + memmove(zWideFilename+6, zWideFilename, nChar*sizeof(WCHAR)); | |
| 51379 | + memcpy(zWideFilename, L"\\\\?\\UNC", 14); | |
| 51380 | + } | |
| 51381 | + zConverted = zWideFilename; | |
| 51382 | +#else | |
| 51232 | 51383 | zConverted = winUtf8ToUnicode(zFilename); |
| 51384 | +#endif /* __CYGWIN__ */ | |
| 51233 | 51385 | } |
| 51234 | -#ifdef SQLITE_WIN32_HAS_ANSI | |
| 51386 | +#if defined(SQLITE_WIN32_HAS_ANSI) && defined(_WIN32) | |
| 51235 | 51387 | else{ |
| 51236 | 51388 | zConverted = winUtf8ToMbcs(zFilename, osAreFileApisANSI()); |
| 51237 | 51389 | } |
| 51238 | 51390 | #endif |
| 51239 | 51391 | /* caller will handle out of memory */ |
| @@ -52058,11 +52210,11 @@ | ||
| 52058 | 52210 | ** |
| 52059 | 52211 | ** This division contains the implementation of methods on the |
| 52060 | 52212 | ** sqlite3_vfs object. |
| 52061 | 52213 | */ |
| 52062 | 52214 | |
| 52063 | -#if defined(__CYGWIN__) | |
| 52215 | +#if 0 /* No longer necessary */ | |
| 52064 | 52216 | /* |
| 52065 | 52217 | ** Convert a filename from whatever the underlying operating system |
| 52066 | 52218 | ** supports for filenames into UTF-8. Space to hold the result is |
| 52067 | 52219 | ** obtained from malloc and must be freed by the calling function. |
| 52068 | 52220 | */ |
| @@ -52091,11 +52243,18 @@ | ||
| 52091 | 52243 | int nLen = sqlite3Strlen30(zBuf); |
| 52092 | 52244 | if( nLen>0 ){ |
| 52093 | 52245 | if( winIsDirSep(zBuf[nLen-1]) ){ |
| 52094 | 52246 | return 1; |
| 52095 | 52247 | }else if( nLen+1<nBuf ){ |
| 52096 | - zBuf[nLen] = winGetDirSep(); | |
| 52248 | + if( !osGetenv ){ | |
| 52249 | + zBuf[nLen] = winGetDirSep(); | |
| 52250 | + }else if( winIsDriveLetterAndColon(zBuf) && winIsDirSep(zBuf[2]) ){ | |
| 52251 | + zBuf[nLen] = '\\'; | |
| 52252 | + zBuf[2]='\\'; | |
| 52253 | + }else{ | |
| 52254 | + zBuf[nLen] = '/'; | |
| 52255 | + } | |
| 52097 | 52256 | zBuf[nLen+1] = '\0'; |
| 52098 | 52257 | return 1; |
| 52099 | 52258 | } |
| 52100 | 52259 | } |
| 52101 | 52260 | } |
| @@ -52118,11 +52277,11 @@ | ||
| 52118 | 52277 | /* |
| 52119 | 52278 | ** Create a temporary file name and store the resulting pointer into pzBuf. |
| 52120 | 52279 | ** The pointer returned in pzBuf must be freed via sqlite3_free(). |
| 52121 | 52280 | */ |
| 52122 | 52281 | static int winGetTempname(sqlite3_vfs *pVfs, char **pzBuf){ |
| 52123 | - static char zChars[] = | |
| 52282 | + static const char zChars[] = | |
| 52124 | 52283 | "abcdefghijklmnopqrstuvwxyz" |
| 52125 | 52284 | "ABCDEFGHIJKLMNOPQRSTUVWXYZ" |
| 52126 | 52285 | "0123456789"; |
| 52127 | 52286 | size_t i, j; |
| 52128 | 52287 | DWORD pid; |
| @@ -52169,11 +52328,11 @@ | ||
| 52169 | 52328 | } |
| 52170 | 52329 | sqlite3_mutex_leave(sqlite3MutexAlloc(SQLITE_MUTEX_STATIC_TEMPDIR)); |
| 52171 | 52330 | } |
| 52172 | 52331 | |
| 52173 | 52332 | #if defined(__CYGWIN__) |
| 52174 | - else{ | |
| 52333 | + else if( osGetenv!=NULL ){ | |
| 52175 | 52334 | static const char *azDirs[] = { |
| 52176 | 52335 | 0, /* getenv("SQLITE_TMPDIR") */ |
| 52177 | 52336 | 0, /* getenv("TMPDIR") */ |
| 52178 | 52337 | 0, /* getenv("TMP") */ |
| 52179 | 52338 | 0, /* getenv("TEMP") */ |
| @@ -52185,24 +52344,24 @@ | ||
| 52185 | 52344 | 0 /* List terminator */ |
| 52186 | 52345 | }; |
| 52187 | 52346 | unsigned int i; |
| 52188 | 52347 | const char *zDir = 0; |
| 52189 | 52348 | |
| 52190 | - if( !azDirs[0] ) azDirs[0] = getenv("SQLITE_TMPDIR"); | |
| 52191 | - if( !azDirs[1] ) azDirs[1] = getenv("TMPDIR"); | |
| 52192 | - if( !azDirs[2] ) azDirs[2] = getenv("TMP"); | |
| 52193 | - if( !azDirs[3] ) azDirs[3] = getenv("TEMP"); | |
| 52194 | - if( !azDirs[4] ) azDirs[4] = getenv("USERPROFILE"); | |
| 52349 | + if( !azDirs[0] ) azDirs[0] = osGetenv("SQLITE_TMPDIR"); | |
| 52350 | + if( !azDirs[1] ) azDirs[1] = osGetenv("TMPDIR"); | |
| 52351 | + if( !azDirs[2] ) azDirs[2] = osGetenv("TMP"); | |
| 52352 | + if( !azDirs[3] ) azDirs[3] = osGetenv("TEMP"); | |
| 52353 | + if( !azDirs[4] ) azDirs[4] = osGetenv("USERPROFILE"); | |
| 52195 | 52354 | for(i=0; i<sizeof(azDirs)/sizeof(azDirs[0]); zDir=azDirs[i++]){ |
| 52196 | 52355 | void *zConverted; |
| 52197 | 52356 | if( zDir==0 ) continue; |
| 52198 | 52357 | /* If the path starts with a drive letter followed by the colon |
| 52199 | 52358 | ** character, assume it is already a native Win32 path; otherwise, |
| 52200 | 52359 | ** it must be converted to a native Win32 path via the Cygwin API |
| 52201 | 52360 | ** prior to using it. |
| 52202 | 52361 | */ |
| 52203 | - if( winIsDriveLetterAndColon(zDir) ){ | |
| 52362 | + { | |
| 52204 | 52363 | zConverted = winConvertFromUtf8Filename(zDir); |
| 52205 | 52364 | if( !zConverted ){ |
| 52206 | 52365 | sqlite3_free(zBuf); |
| 52207 | 52366 | OSTRACE(("TEMP-FILENAME rc=SQLITE_IOERR_NOMEM\n")); |
| 52208 | 52367 | return SQLITE_IOERR_NOMEM_BKPT; |
| @@ -52211,19 +52370,20 @@ | ||
| 52211 | 52370 | sqlite3_snprintf(nMax, zBuf, "%s", zDir); |
| 52212 | 52371 | sqlite3_free(zConverted); |
| 52213 | 52372 | break; |
| 52214 | 52373 | } |
| 52215 | 52374 | sqlite3_free(zConverted); |
| 52375 | +#if 0 /* No longer necessary */ | |
| 52216 | 52376 | }else{ |
| 52217 | 52377 | zConverted = sqlite3MallocZero( nMax+1 ); |
| 52218 | 52378 | if( !zConverted ){ |
| 52219 | 52379 | sqlite3_free(zBuf); |
| 52220 | 52380 | OSTRACE(("TEMP-FILENAME rc=SQLITE_IOERR_NOMEM\n")); |
| 52221 | 52381 | return SQLITE_IOERR_NOMEM_BKPT; |
| 52222 | 52382 | } |
| 52223 | - if( cygwin_conv_path( | |
| 52224 | - osIsNT() ? CCP_POSIX_TO_WIN_W : CCP_POSIX_TO_WIN_A, zDir, | |
| 52383 | + if( osCygwin_conv_path( | |
| 52384 | + CCP_POSIX_TO_WIN_W, zDir, | |
| 52225 | 52385 | zConverted, nMax+1)<0 ){ |
| 52226 | 52386 | sqlite3_free(zConverted); |
| 52227 | 52387 | sqlite3_free(zBuf); |
| 52228 | 52388 | OSTRACE(("TEMP-FILENAME rc=SQLITE_IOERR_CONVPATH\n")); |
| 52229 | 52389 | return winLogError(SQLITE_IOERR_CONVPATH, (DWORD)errno, |
| @@ -52245,14 +52405,17 @@ | ||
| 52245 | 52405 | sqlite3_free(zUtf8); |
| 52246 | 52406 | sqlite3_free(zConverted); |
| 52247 | 52407 | break; |
| 52248 | 52408 | } |
| 52249 | 52409 | sqlite3_free(zConverted); |
| 52410 | +#endif /* No longer necessary */ | |
| 52250 | 52411 | } |
| 52251 | 52412 | } |
| 52252 | 52413 | } |
| 52253 | -#elif !SQLITE_OS_WINRT && !defined(__CYGWIN__) | |
| 52414 | +#endif | |
| 52415 | + | |
| 52416 | +#if !SQLITE_OS_WINRT && defined(_WIN32) | |
| 52254 | 52417 | else if( osIsNT() ){ |
| 52255 | 52418 | char *zMulti; |
| 52256 | 52419 | LPWSTR zWidePath = sqlite3MallocZero( nMax*sizeof(WCHAR) ); |
| 52257 | 52420 | if( !zWidePath ){ |
| 52258 | 52421 | sqlite3_free(zBuf); |
| @@ -52372,11 +52535,11 @@ | ||
| 52372 | 52535 | &sAttrData)) && winRetryIoerr(&cnt, &lastErrno) ){} |
| 52373 | 52536 | if( !rc ){ |
| 52374 | 52537 | return 0; /* Invalid name? */ |
| 52375 | 52538 | } |
| 52376 | 52539 | attr = sAttrData.dwFileAttributes; |
| 52377 | -#if SQLITE_OS_WINCE==0 | |
| 52540 | +#if SQLITE_OS_WINCE==0 && defined(SQLITE_WIN32_HAS_ANSI) | |
| 52378 | 52541 | }else{ |
| 52379 | 52542 | attr = osGetFileAttributesA((char*)zConverted); |
| 52380 | 52543 | #endif |
| 52381 | 52544 | } |
| 52382 | 52545 | return (attr!=INVALID_FILE_ATTRIBUTES) && (attr&FILE_ATTRIBUTE_DIRECTORY); |
| @@ -52388,10 +52551,16 @@ | ||
| 52388 | 52551 | const char *zFilename, /* Name of file to check */ |
| 52389 | 52552 | int flags, /* Type of test to make on this file */ |
| 52390 | 52553 | int *pResOut /* OUT: Result */ |
| 52391 | 52554 | ); |
| 52392 | 52555 | |
| 52556 | +/* | |
| 52557 | +** The Windows version of xAccess() accepts an extra bit in the flags | |
| 52558 | +** parameter that prevents an anti-virus retry loop. | |
| 52559 | +*/ | |
| 52560 | +#define NORETRY 0x4000 | |
| 52561 | + | |
| 52393 | 52562 | /* |
| 52394 | 52563 | ** Open a file. |
| 52395 | 52564 | */ |
| 52396 | 52565 | static int winOpen( |
| 52397 | 52566 | sqlite3_vfs *pVfs, /* Used to get maximum path length and AppData */ |
| @@ -52412,10 +52581,11 @@ | ||
| 52412 | 52581 | winVfsAppData *pAppData; |
| 52413 | 52582 | winFile *pFile = (winFile*)id; |
| 52414 | 52583 | void *zConverted; /* Filename in OS encoding */ |
| 52415 | 52584 | const char *zUtf8Name = zName; /* Filename in UTF-8 encoding */ |
| 52416 | 52585 | int cnt = 0; |
| 52586 | + int isRO = 0; /* file is known to be accessible readonly */ | |
| 52417 | 52587 | |
| 52418 | 52588 | /* If argument zPath is a NULL pointer, this function is required to open |
| 52419 | 52589 | ** a temporary file. Use this buffer to store the file name in. |
| 52420 | 52590 | */ |
| 52421 | 52591 | char *zTmpname = 0; /* For temporary filename, if necessary. */ |
| @@ -52576,13 +52746,13 @@ | ||
| 52576 | 52746 | dwShareMode, |
| 52577 | 52747 | dwCreationDisposition, |
| 52578 | 52748 | &extendedParameters); |
| 52579 | 52749 | if( h!=INVALID_HANDLE_VALUE ) break; |
| 52580 | 52750 | if( isReadWrite ){ |
| 52581 | - int rc2, isRO = 0; | |
| 52751 | + int rc2; | |
| 52582 | 52752 | sqlite3BeginBenignMalloc(); |
| 52583 | - rc2 = winAccess(pVfs, zUtf8Name, SQLITE_ACCESS_READ, &isRO); | |
| 52753 | + rc2 = winAccess(pVfs, zUtf8Name, SQLITE_ACCESS_READ|NORETRY, &isRO); | |
| 52584 | 52754 | sqlite3EndBenignMalloc(); |
| 52585 | 52755 | if( rc2==SQLITE_OK && isRO ) break; |
| 52586 | 52756 | } |
| 52587 | 52757 | }while( winRetryIoerr(&cnt, &lastErrno) ); |
| 52588 | 52758 | #else |
| @@ -52593,13 +52763,13 @@ | ||
| 52593 | 52763 | dwCreationDisposition, |
| 52594 | 52764 | dwFlagsAndAttributes, |
| 52595 | 52765 | NULL); |
| 52596 | 52766 | if( h!=INVALID_HANDLE_VALUE ) break; |
| 52597 | 52767 | if( isReadWrite ){ |
| 52598 | - int rc2, isRO = 0; | |
| 52768 | + int rc2; | |
| 52599 | 52769 | sqlite3BeginBenignMalloc(); |
| 52600 | - rc2 = winAccess(pVfs, zUtf8Name, SQLITE_ACCESS_READ, &isRO); | |
| 52770 | + rc2 = winAccess(pVfs, zUtf8Name, SQLITE_ACCESS_READ|NORETRY, &isRO); | |
| 52601 | 52771 | sqlite3EndBenignMalloc(); |
| 52602 | 52772 | if( rc2==SQLITE_OK && isRO ) break; |
| 52603 | 52773 | } |
| 52604 | 52774 | }while( winRetryIoerr(&cnt, &lastErrno) ); |
| 52605 | 52775 | #endif |
| @@ -52613,13 +52783,13 @@ | ||
| 52613 | 52783 | dwCreationDisposition, |
| 52614 | 52784 | dwFlagsAndAttributes, |
| 52615 | 52785 | NULL); |
| 52616 | 52786 | if( h!=INVALID_HANDLE_VALUE ) break; |
| 52617 | 52787 | if( isReadWrite ){ |
| 52618 | - int rc2, isRO = 0; | |
| 52788 | + int rc2; | |
| 52619 | 52789 | sqlite3BeginBenignMalloc(); |
| 52620 | - rc2 = winAccess(pVfs, zUtf8Name, SQLITE_ACCESS_READ, &isRO); | |
| 52790 | + rc2 = winAccess(pVfs, zUtf8Name, SQLITE_ACCESS_READ|NORETRY, &isRO); | |
| 52621 | 52791 | sqlite3EndBenignMalloc(); |
| 52622 | 52792 | if( rc2==SQLITE_OK && isRO ) break; |
| 52623 | 52793 | } |
| 52624 | 52794 | }while( winRetryIoerr(&cnt, &lastErrno) ); |
| 52625 | 52795 | } |
| @@ -52630,11 +52800,11 @@ | ||
| 52630 | 52800 | dwDesiredAccess, (h==INVALID_HANDLE_VALUE) ? "failed" : "ok")); |
| 52631 | 52801 | |
| 52632 | 52802 | if( h==INVALID_HANDLE_VALUE ){ |
| 52633 | 52803 | sqlite3_free(zConverted); |
| 52634 | 52804 | sqlite3_free(zTmpname); |
| 52635 | - if( isReadWrite && !isExclusive ){ | |
| 52805 | + if( isReadWrite && isRO && !isExclusive ){ | |
| 52636 | 52806 | return winOpen(pVfs, zName, id, |
| 52637 | 52807 | ((flags|SQLITE_OPEN_READONLY) & |
| 52638 | 52808 | ~(SQLITE_OPEN_CREATE|SQLITE_OPEN_READWRITE)), |
| 52639 | 52809 | pOutFlags); |
| 52640 | 52810 | }else{ |
| @@ -52832,11 +53002,17 @@ | ||
| 52832 | 53002 | ){ |
| 52833 | 53003 | DWORD attr; |
| 52834 | 53004 | int rc = 0; |
| 52835 | 53005 | DWORD lastErrno = 0; |
| 52836 | 53006 | void *zConverted; |
| 53007 | + int noRetry = 0; /* Do not use winRetryIoerr() */ | |
| 52837 | 53008 | UNUSED_PARAMETER(pVfs); |
| 53009 | + | |
| 53010 | + if( (flags & NORETRY)!=0 ){ | |
| 53011 | + noRetry = 1; | |
| 53012 | + flags &= ~NORETRY; | |
| 53013 | + } | |
| 52838 | 53014 | |
| 52839 | 53015 | SimulateIOError( return SQLITE_IOERR_ACCESS; ); |
| 52840 | 53016 | OSTRACE(("ACCESS name=%s, flags=%x, pResOut=%p\n", |
| 52841 | 53017 | zFilename, flags, pResOut)); |
| 52842 | 53018 | |
| @@ -52856,11 +53032,14 @@ | ||
| 52856 | 53032 | int cnt = 0; |
| 52857 | 53033 | WIN32_FILE_ATTRIBUTE_DATA sAttrData; |
| 52858 | 53034 | memset(&sAttrData, 0, sizeof(sAttrData)); |
| 52859 | 53035 | while( !(rc = osGetFileAttributesExW((LPCWSTR)zConverted, |
| 52860 | 53036 | GetFileExInfoStandard, |
| 52861 | - &sAttrData)) && winRetryIoerr(&cnt, &lastErrno) ){} | |
| 53037 | + &sAttrData)) | |
| 53038 | + && !noRetry | |
| 53039 | + && winRetryIoerr(&cnt, &lastErrno) | |
| 53040 | + ){ /* Loop until true */} | |
| 52862 | 53041 | if( rc ){ |
| 52863 | 53042 | /* For an SQLITE_ACCESS_EXISTS query, treat a zero-length file |
| 52864 | 53043 | ** as if it does not exist. |
| 52865 | 53044 | */ |
| 52866 | 53045 | if( flags==SQLITE_ACCESS_EXISTS |
| @@ -52924,10 +53103,11 @@ | ||
| 52924 | 53103 | const char *zPathname |
| 52925 | 53104 | ){ |
| 52926 | 53105 | return ( sqlite3Isalpha(zPathname[0]) && zPathname[1]==':' ); |
| 52927 | 53106 | } |
| 52928 | 53107 | |
| 53108 | +#ifdef _WIN32 | |
| 52929 | 53109 | /* |
| 52930 | 53110 | ** Returns non-zero if the specified path name should be used verbatim. If |
| 52931 | 53111 | ** non-zero is returned from this function, the calling function must simply |
| 52932 | 53112 | ** use the provided path name verbatim -OR- resolve it into a full path name |
| 52933 | 53113 | ** using the GetFullPathName Win32 API function (if available). |
| @@ -52960,10 +53140,74 @@ | ||
| 52960 | 53140 | ** If we get to this point, the path name should almost certainly be a purely |
| 52961 | 53141 | ** relative one (i.e. not a UNC name, not absolute, and not volume relative). |
| 52962 | 53142 | */ |
| 52963 | 53143 | return FALSE; |
| 52964 | 53144 | } |
| 53145 | +#endif /* _WIN32 */ | |
| 53146 | + | |
| 53147 | +#ifdef __CYGWIN__ | |
| 53148 | +/* | |
| 53149 | +** Simplify a filename into its canonical form | |
| 53150 | +** by making the following changes: | |
| 53151 | +** | |
| 53152 | +** * convert any '/' to '\' (win32) or reverse (Cygwin) | |
| 53153 | +** * removing any trailing and duplicate / (except for UNC paths) | |
| 53154 | +** * convert /./ into just / | |
| 53155 | +** | |
| 53156 | +** Changes are made in-place. Return the new name length. | |
| 53157 | +** | |
| 53158 | +** The original filename is in z[0..]. If the path is shortened, | |
| 53159 | +** no-longer used bytes will be written by '\0'. | |
| 53160 | +*/ | |
| 53161 | +static void winSimplifyName(char *z){ | |
| 53162 | + int i, j; | |
| 53163 | + for(i=j=0; z[i]; ++i){ | |
| 53164 | + if( winIsDirSep(z[i]) ){ | |
| 53165 | +#if !defined(SQLITE_TEST) | |
| 53166 | + /* Some test-cases assume that "./foo" and "foo" are different */ | |
| 53167 | + if( z[i+1]=='.' && winIsDirSep(z[i+2]) ){ | |
| 53168 | + ++i; | |
| 53169 | + continue; | |
| 53170 | + } | |
| 53171 | +#endif | |
| 53172 | + if( !z[i+1] || (winIsDirSep(z[i+1]) && (i!=0)) ){ | |
| 53173 | + continue; | |
| 53174 | + } | |
| 53175 | + z[j++] = osGetenv?'/':'\\'; | |
| 53176 | + }else{ | |
| 53177 | + z[j++] = z[i]; | |
| 53178 | + } | |
| 53179 | + } | |
| 53180 | + while(j<i) z[j++] = '\0'; | |
| 53181 | +} | |
| 53182 | + | |
| 53183 | +#define SQLITE_MAX_SYMLINKS 100 | |
| 53184 | + | |
| 53185 | +static int mkFullPathname( | |
| 53186 | + const char *zPath, /* Input path */ | |
| 53187 | + char *zOut, /* Output buffer */ | |
| 53188 | + int nOut /* Allocated size of buffer zOut */ | |
| 53189 | +){ | |
| 53190 | + int nPath = sqlite3Strlen30(zPath); | |
| 53191 | + int iOff = 0; | |
| 53192 | + if( zPath[0]!='/' ){ | |
| 53193 | + if( osGetcwd(zOut, nOut-2)==0 ){ | |
| 53194 | + return winLogError(SQLITE_CANTOPEN_BKPT, (DWORD)osErrno, "getcwd", zPath); | |
| 53195 | + } | |
| 53196 | + iOff = sqlite3Strlen30(zOut); | |
| 53197 | + zOut[iOff++] = '/'; | |
| 53198 | + } | |
| 53199 | + if( (iOff+nPath+1)>nOut ){ | |
| 53200 | + /* SQLite assumes that xFullPathname() nul-terminates the output buffer | |
| 53201 | + ** even if it returns an error. */ | |
| 53202 | + zOut[iOff] = '\0'; | |
| 53203 | + return SQLITE_CANTOPEN_BKPT; | |
| 53204 | + } | |
| 53205 | + sqlite3_snprintf(nOut-iOff, &zOut[iOff], "%s", zPath); | |
| 53206 | + return SQLITE_OK; | |
| 53207 | +} | |
| 53208 | +#endif /* __CYGWIN__ */ | |
| 52965 | 53209 | |
| 52966 | 53210 | /* |
| 52967 | 53211 | ** Turn a relative pathname into a full pathname. Write the full |
| 52968 | 53212 | ** pathname into zOut[]. zOut[] will be at least pVfs->mxPathname |
| 52969 | 53213 | ** bytes in size. |
| @@ -52972,12 +53216,12 @@ | ||
| 52972 | 53216 | sqlite3_vfs *pVfs, /* Pointer to vfs object */ |
| 52973 | 53217 | const char *zRelative, /* Possibly relative input path */ |
| 52974 | 53218 | int nFull, /* Size of output buffer in bytes */ |
| 52975 | 53219 | char *zFull /* Output buffer */ |
| 52976 | 53220 | ){ |
| 52977 | -#if !SQLITE_OS_WINCE && !SQLITE_OS_WINRT && !defined(__CYGWIN__) | |
| 52978 | - DWORD nByte; | |
| 53221 | +#if !SQLITE_OS_WINCE && !SQLITE_OS_WINRT | |
| 53222 | + int nByte; | |
| 52979 | 53223 | void *zConverted; |
| 52980 | 53224 | char *zOut; |
| 52981 | 53225 | #endif |
| 52982 | 53226 | |
| 52983 | 53227 | /* If this path name begins with "/X:" or "\\?\", where "X" is any |
| @@ -52986,68 +53230,114 @@ | ||
| 52986 | 53230 | if( zRelative[0]=='/' && (winIsDriveLetterAndColon(zRelative+1) |
| 52987 | 53231 | || winIsLongPathPrefix(zRelative+1)) ){ |
| 52988 | 53232 | zRelative++; |
| 52989 | 53233 | } |
| 52990 | 53234 | |
| 52991 | -#if defined(__CYGWIN__) | |
| 53235 | + SimulateIOError( return SQLITE_ERROR ); | |
| 53236 | + | |
| 53237 | +#ifdef __CYGWIN__ | |
| 53238 | + if( osGetcwd ){ | |
| 53239 | + zFull[nFull-1] = '\0'; | |
| 53240 | + if( !winIsDriveLetterAndColon(zRelative) || !winIsDirSep(zRelative[2]) ){ | |
| 53241 | + int rc = SQLITE_OK; | |
| 53242 | + int nLink = 1; /* Number of symbolic links followed so far */ | |
| 53243 | + const char *zIn = zRelative; /* Input path for each iteration of loop */ | |
| 53244 | + char *zDel = 0; | |
| 53245 | + struct stat buf; | |
| 53246 | + | |
| 53247 | + UNUSED_PARAMETER(pVfs); | |
| 53248 | + | |
| 53249 | + do { | |
| 53250 | + /* Call lstat() on path zIn. Set bLink to true if the path is a symbolic | |
| 53251 | + ** link, or false otherwise. */ | |
| 53252 | + int bLink = 0; | |
| 53253 | + if( osLstat && osReadlink ) { | |
| 53254 | + if( osLstat(zIn, &buf)!=0 ){ | |
| 53255 | + int myErrno = osErrno; | |
| 53256 | + if( myErrno!=ENOENT ){ | |
| 53257 | + rc = winLogError(SQLITE_CANTOPEN_BKPT, (DWORD)myErrno, "lstat", zIn); | |
| 53258 | + } | |
| 53259 | + }else{ | |
| 53260 | + bLink = ((buf.st_mode & 0170000) == 0120000); | |
| 53261 | + } | |
| 53262 | + | |
| 53263 | + if( bLink ){ | |
| 53264 | + if( zDel==0 ){ | |
| 53265 | + zDel = sqlite3MallocZero(nFull); | |
| 53266 | + if( zDel==0 ) rc = SQLITE_NOMEM; | |
| 53267 | + }else if( ++nLink>SQLITE_MAX_SYMLINKS ){ | |
| 53268 | + rc = SQLITE_CANTOPEN_BKPT; | |
| 53269 | + } | |
| 53270 | + | |
| 53271 | + if( rc==SQLITE_OK ){ | |
| 53272 | + nByte = osReadlink(zIn, zDel, nFull-1); | |
| 53273 | + if( nByte ==(DWORD)-1 ){ | |
| 53274 | + rc = winLogError(SQLITE_CANTOPEN_BKPT, (DWORD)osErrno, "readlink", zIn); | |
| 53275 | + }else{ | |
| 53276 | + if( zDel[0]!='/' ){ | |
| 53277 | + int n; | |
| 53278 | + for(n = sqlite3Strlen30(zIn); n>0 && zIn[n-1]!='/'; n--); | |
| 53279 | + if( nByte+n+1>nFull ){ | |
| 53280 | + rc = SQLITE_CANTOPEN_BKPT; | |
| 53281 | + }else{ | |
| 53282 | + memmove(&zDel[n], zDel, nByte+1); | |
| 53283 | + memcpy(zDel, zIn, n); | |
| 53284 | + nByte += n; | |
| 53285 | + } | |
| 53286 | + } | |
| 53287 | + zDel[nByte] = '\0'; | |
| 53288 | + } | |
| 53289 | + } | |
| 53290 | + | |
| 53291 | + zIn = zDel; | |
| 53292 | + } | |
| 53293 | + } | |
| 53294 | + | |
| 53295 | + assert( rc!=SQLITE_OK || zIn!=zFull || zIn[0]=='/' ); | |
| 53296 | + if( rc==SQLITE_OK && zIn!=zFull ){ | |
| 53297 | + rc = mkFullPathname(zIn, zFull, nFull); | |
| 53298 | + } | |
| 53299 | + if( bLink==0 ) break; | |
| 53300 | + zIn = zFull; | |
| 53301 | + }while( rc==SQLITE_OK ); | |
| 53302 | + | |
| 53303 | + sqlite3_free(zDel); | |
| 53304 | + winSimplifyName(zFull); | |
| 53305 | + return rc; | |
| 53306 | + } | |
| 53307 | + } | |
| 53308 | +#endif /* __CYGWIN__ */ | |
| 53309 | +#if 0 /* This doesn't work correctly at all! See: | |
| 53310 | + <https://marc.info/?l=sqlite-users&m=139299149416314&w=2> | |
| 53311 | +*/ | |
| 52992 | 53312 | SimulateIOError( return SQLITE_ERROR ); |
| 52993 | 53313 | UNUSED_PARAMETER(nFull); |
| 52994 | 53314 | assert( nFull>=pVfs->mxPathname ); |
| 52995 | - if ( sqlite3_data_directory && !winIsVerbatimPathname(zRelative) ){ | |
| 52996 | - /* | |
| 52997 | - ** NOTE: We are dealing with a relative path name and the data | |
| 52998 | - ** directory has been set. Therefore, use it as the basis | |
| 52999 | - ** for converting the relative path name to an absolute | |
| 53000 | - ** one by prepending the data directory and a slash. | |
| 53001 | - */ | |
| 53002 | - char *zOut = sqlite3MallocZero( 1+(u64)pVfs->mxPathname ); | |
| 53003 | - if( !zOut ){ | |
| 53004 | - return SQLITE_IOERR_NOMEM_BKPT; | |
| 53005 | - } | |
| 53006 | - if( cygwin_conv_path( | |
| 53007 | - (osIsNT() ? CCP_POSIX_TO_WIN_W : CCP_POSIX_TO_WIN_A) | | |
| 53008 | - CCP_RELATIVE, zRelative, zOut, pVfs->mxPathname+1)<0 ){ | |
| 53009 | - sqlite3_free(zOut); | |
| 53010 | - return winLogError(SQLITE_CANTOPEN_CONVPATH, (DWORD)errno, | |
| 53011 | - "winFullPathname1", zRelative); | |
| 53012 | - }else{ | |
| 53013 | - char *zUtf8 = winConvertToUtf8Filename(zOut); | |
| 53014 | - if( !zUtf8 ){ | |
| 53015 | - sqlite3_free(zOut); | |
| 53016 | - return SQLITE_IOERR_NOMEM_BKPT; | |
| 53017 | - } | |
| 53018 | - sqlite3_snprintf(MIN(nFull, pVfs->mxPathname), zFull, "%s%c%s", | |
| 53019 | - sqlite3_data_directory, winGetDirSep(), zUtf8); | |
| 53020 | - sqlite3_free(zUtf8); | |
| 53021 | - sqlite3_free(zOut); | |
| 53022 | - } | |
| 53023 | - }else{ | |
| 53024 | - char *zOut = sqlite3MallocZero( pVfs->mxPathname+1 ); | |
| 53025 | - if( !zOut ){ | |
| 53026 | - return SQLITE_IOERR_NOMEM_BKPT; | |
| 53027 | - } | |
| 53028 | - if( cygwin_conv_path( | |
| 53029 | - (osIsNT() ? CCP_POSIX_TO_WIN_W : CCP_POSIX_TO_WIN_A), | |
| 53030 | - zRelative, zOut, pVfs->mxPathname+1)<0 ){ | |
| 53031 | - sqlite3_free(zOut); | |
| 53032 | - return winLogError(SQLITE_CANTOPEN_CONVPATH, (DWORD)errno, | |
| 53033 | - "winFullPathname2", zRelative); | |
| 53034 | - }else{ | |
| 53035 | - char *zUtf8 = winConvertToUtf8Filename(zOut); | |
| 53036 | - if( !zUtf8 ){ | |
| 53037 | - sqlite3_free(zOut); | |
| 53038 | - return SQLITE_IOERR_NOMEM_BKPT; | |
| 53039 | - } | |
| 53040 | - sqlite3_snprintf(MIN(nFull, pVfs->mxPathname), zFull, "%s", zUtf8); | |
| 53041 | - sqlite3_free(zUtf8); | |
| 53042 | - sqlite3_free(zOut); | |
| 53043 | - } | |
| 53044 | - } | |
| 53045 | - return SQLITE_OK; | |
| 53046 | -#endif | |
| 53047 | - | |
| 53048 | -#if (SQLITE_OS_WINCE || SQLITE_OS_WINRT) && !defined(__CYGWIN__) | |
| 53315 | + char *zOut = sqlite3MallocZero( pVfs->mxPathname+1 ); | |
| 53316 | + if( !zOut ){ | |
| 53317 | + return SQLITE_IOERR_NOMEM_BKPT; | |
| 53318 | + } | |
| 53319 | + if( osCygwin_conv_path( | |
| 53320 | + CCP_POSIX_TO_WIN_W, | |
| 53321 | + zRelative, zOut, pVfs->mxPathname+1)<0 ){ | |
| 53322 | + sqlite3_free(zOut); | |
| 53323 | + return winLogError(SQLITE_CANTOPEN_CONVPATH, (DWORD)errno, | |
| 53324 | + "winFullPathname2", zRelative); | |
| 53325 | + }else{ | |
| 53326 | + char *zUtf8 = winConvertToUtf8Filename(zOut); | |
| 53327 | + if( !zUtf8 ){ | |
| 53328 | + sqlite3_free(zOut); | |
| 53329 | + return SQLITE_IOERR_NOMEM_BKPT; | |
| 53330 | + } | |
| 53331 | + sqlite3_snprintf(MIN(nFull, pVfs->mxPathname), zFull, "%s", zUtf8); | |
| 53332 | + sqlite3_free(zUtf8); | |
| 53333 | + sqlite3_free(zOut); | |
| 53334 | + } | |
| 53335 | + return SQLITE_OK; | |
| 53336 | +#endif | |
| 53337 | + | |
| 53338 | +#if (SQLITE_OS_WINCE || SQLITE_OS_WINRT) && defined(_WIN32) | |
| 53049 | 53339 | SimulateIOError( return SQLITE_ERROR ); |
| 53050 | 53340 | /* WinCE has no concept of a relative pathname, or so I am told. */ |
| 53051 | 53341 | /* WinRT has no way to convert a relative path to an absolute one. */ |
| 53052 | 53342 | if ( sqlite3_data_directory && !winIsVerbatimPathname(zRelative) ){ |
| 53053 | 53343 | /* |
| @@ -53062,11 +53352,12 @@ | ||
| 53062 | 53352 | sqlite3_snprintf(MIN(nFull, pVfs->mxPathname), zFull, "%s", zRelative); |
| 53063 | 53353 | } |
| 53064 | 53354 | return SQLITE_OK; |
| 53065 | 53355 | #endif |
| 53066 | 53356 | |
| 53067 | -#if !SQLITE_OS_WINCE && !SQLITE_OS_WINRT && !defined(__CYGWIN__) | |
| 53357 | +#if !SQLITE_OS_WINCE && !SQLITE_OS_WINRT | |
| 53358 | +#if defined(_WIN32) | |
| 53068 | 53359 | /* It's odd to simulate an io-error here, but really this is just |
| 53069 | 53360 | ** using the io-error infrastructure to test that SQLite handles this |
| 53070 | 53361 | ** function failing. This function could fail if, for example, the |
| 53071 | 53362 | ** current working directory has been unlinked. |
| 53072 | 53363 | */ |
| @@ -53080,10 +53371,11 @@ | ||
| 53080 | 53371 | */ |
| 53081 | 53372 | sqlite3_snprintf(MIN(nFull, pVfs->mxPathname), zFull, "%s%c%s", |
| 53082 | 53373 | sqlite3_data_directory, winGetDirSep(), zRelative); |
| 53083 | 53374 | return SQLITE_OK; |
| 53084 | 53375 | } |
| 53376 | +#endif | |
| 53085 | 53377 | zConverted = winConvertFromUtf8Filename(zRelative); |
| 53086 | 53378 | if( zConverted==0 ){ |
| 53087 | 53379 | return SQLITE_IOERR_NOMEM_BKPT; |
| 53088 | 53380 | } |
| 53089 | 53381 | if( osIsNT() ){ |
| @@ -53092,16 +53384,17 @@ | ||
| 53092 | 53384 | if( nByte==0 ){ |
| 53093 | 53385 | sqlite3_free(zConverted); |
| 53094 | 53386 | return winLogError(SQLITE_CANTOPEN_FULLPATH, osGetLastError(), |
| 53095 | 53387 | "winFullPathname1", zRelative); |
| 53096 | 53388 | } |
| 53097 | - zTemp = sqlite3MallocZero( nByte*sizeof(zTemp[0]) + 3*sizeof(zTemp[0]) ); | |
| 53389 | + nByte += 3; | |
| 53390 | + zTemp = sqlite3MallocZero( nByte*sizeof(zTemp[0]) ); | |
| 53098 | 53391 | if( zTemp==0 ){ |
| 53099 | 53392 | sqlite3_free(zConverted); |
| 53100 | 53393 | return SQLITE_IOERR_NOMEM_BKPT; |
| 53101 | 53394 | } |
| 53102 | - nByte = osGetFullPathNameW((LPCWSTR)zConverted, nByte+3, zTemp, 0); | |
| 53395 | + nByte = osGetFullPathNameW((LPCWSTR)zConverted, nByte, zTemp, 0); | |
| 53103 | 53396 | if( nByte==0 ){ |
| 53104 | 53397 | sqlite3_free(zConverted); |
| 53105 | 53398 | sqlite3_free(zTemp); |
| 53106 | 53399 | return winLogError(SQLITE_CANTOPEN_FULLPATH, osGetLastError(), |
| 53107 | 53400 | "winFullPathname2", zRelative); |
| @@ -53135,11 +53428,30 @@ | ||
| 53135 | 53428 | zOut = winMbcsToUtf8(zTemp, osAreFileApisANSI()); |
| 53136 | 53429 | sqlite3_free(zTemp); |
| 53137 | 53430 | } |
| 53138 | 53431 | #endif |
| 53139 | 53432 | if( zOut ){ |
| 53433 | +#ifdef __CYGWIN__ | |
| 53434 | + if( memcmp(zOut, "\\\\?\\", 4) ){ | |
| 53435 | + sqlite3_snprintf(MIN(nFull, pVfs->mxPathname), zFull, "%s", zOut); | |
| 53436 | + }else if( memcmp(zOut+4, "UNC\\", 4) ){ | |
| 53437 | + sqlite3_snprintf(MIN(nFull, pVfs->mxPathname), zFull, "%s", zOut+4); | |
| 53438 | + }else{ | |
| 53439 | + char *p = zOut+6; | |
| 53440 | + *p = '\\'; | |
| 53441 | + if( osGetcwd ){ | |
| 53442 | + /* On Cygwin, UNC paths use forward slashes */ | |
| 53443 | + while( *p ){ | |
| 53444 | + if( *p=='\\' ) *p = '/'; | |
| 53445 | + ++p; | |
| 53446 | + } | |
| 53447 | + } | |
| 53448 | + sqlite3_snprintf(MIN(nFull, pVfs->mxPathname), zFull, "%s", zOut+6); | |
| 53449 | + } | |
| 53450 | +#else | |
| 53140 | 53451 | sqlite3_snprintf(MIN(nFull, pVfs->mxPathname), zFull, "%s", zOut); |
| 53452 | +#endif /* __CYGWIN__ */ | |
| 53141 | 53453 | sqlite3_free(zOut); |
| 53142 | 53454 | return SQLITE_OK; |
| 53143 | 53455 | }else{ |
| 53144 | 53456 | return SQLITE_IOERR_NOMEM_BKPT; |
| 53145 | 53457 | } |
| @@ -53165,11 +53477,13 @@ | ||
| 53165 | 53477 | ** Interfaces for opening a shared library, finding entry points |
| 53166 | 53478 | ** within the shared library, and closing the shared library. |
| 53167 | 53479 | */ |
| 53168 | 53480 | static void *winDlOpen(sqlite3_vfs *pVfs, const char *zFilename){ |
| 53169 | 53481 | HANDLE h; |
| 53170 | -#if defined(__CYGWIN__) | |
| 53482 | +#if 0 /* This doesn't work correctly at all! See: | |
| 53483 | + <https://marc.info/?l=sqlite-users&m=139299149416314&w=2> | |
| 53484 | +*/ | |
| 53171 | 53485 | int nFull = pVfs->mxPathname+1; |
| 53172 | 53486 | char *zFull = sqlite3MallocZero( nFull ); |
| 53173 | 53487 | void *zConverted = 0; |
| 53174 | 53488 | if( zFull==0 ){ |
| 53175 | 53489 | OSTRACE(("DLOPEN name=%s, handle=%p\n", zFilename, (void*)0)); |
| @@ -53532,11 +53846,11 @@ | ||
| 53532 | 53846 | }; |
| 53533 | 53847 | #endif |
| 53534 | 53848 | |
| 53535 | 53849 | /* Double-check that the aSyscall[] array has been constructed |
| 53536 | 53850 | ** correctly. See ticket [bb3a86e890c8e96ab] */ |
| 53537 | - assert( ArraySize(aSyscall)==82 ); | |
| 53851 | + assert( ArraySize(aSyscall)==89 ); | |
| 53538 | 53852 | |
| 53539 | 53853 | /* get memory map allocation granularity */ |
| 53540 | 53854 | memset(&winSysInfo, 0, sizeof(SYSTEM_INFO)); |
| 53541 | 53855 | #if SQLITE_OS_WINRT |
| 53542 | 53856 | osGetNativeSystemInfo(&winSysInfo); |
| @@ -66508,14 +66822,12 @@ | ||
| 66508 | 66822 | s2 = aIn[1]; |
| 66509 | 66823 | }else{ |
| 66510 | 66824 | s1 = s2 = 0; |
| 66511 | 66825 | } |
| 66512 | 66826 | |
| 66513 | - assert( nByte>=8 ); | |
| 66514 | - assert( (nByte&0x00000007)==0 ); | |
| 66515 | - assert( nByte<=65536 ); | |
| 66516 | - assert( nByte%4==0 ); | |
| 66827 | + /* nByte is a multiple of 8 between 8 and 65536 */ | |
| 66828 | + assert( nByte>=8 && (nByte&7)==0 && nByte<=65536 ); | |
| 66517 | 66829 | |
| 66518 | 66830 | if( !nativeCksum ){ |
| 66519 | 66831 | do { |
| 66520 | 66832 | s1 += BYTESWAP32(aData[0]) + s2; |
| 66521 | 66833 | s2 += BYTESWAP32(aData[1]) + s1; |
| @@ -83726,11 +84038,11 @@ | ||
| 83726 | 84038 | ** many different strings can be converted into the same int or real. |
| 83727 | 84039 | ** If a table contains a numeric value and an index is based on the |
| 83728 | 84040 | ** corresponding string value, then it is important that the string be |
| 83729 | 84041 | ** derived from the numeric value, not the other way around, to ensure |
| 83730 | 84042 | ** that the index and table are consistent. See ticket |
| 83731 | -** https://www.sqlite.org/src/info/343634942dd54ab (2018-01-31) for | |
| 84043 | +** https://sqlite.org/src/info/343634942dd54ab (2018-01-31) for | |
| 83732 | 84044 | ** an example. |
| 83733 | 84045 | ** |
| 83734 | 84046 | ** This routine looks at pMem to verify that if it has both a numeric |
| 83735 | 84047 | ** representation and a string representation then the string rep has |
| 83736 | 84048 | ** been derived from the numeric and not the other way around. It returns |
| @@ -93014,11 +93326,11 @@ | ||
| 93014 | 93326 | unsigned char enc |
| 93015 | 93327 | ){ |
| 93016 | 93328 | assert( xDel!=SQLITE_DYNAMIC ); |
| 93017 | 93329 | if( enc!=SQLITE_UTF8 ){ |
| 93018 | 93330 | if( enc==SQLITE_UTF16 ) enc = SQLITE_UTF16NATIVE; |
| 93019 | - nData &= ~(u16)1; | |
| 93331 | + nData &= ~(u64)1; | |
| 93020 | 93332 | } |
| 93021 | 93333 | return bindText(pStmt, i, zData, nData, xDel, enc); |
| 93022 | 93334 | } |
| 93023 | 93335 | #ifndef SQLITE_OMIT_UTF16 |
| 93024 | 93336 | SQLITE_API int sqlite3_bind_text16( |
| @@ -125166,11 +125478,11 @@ | ||
| 125166 | 125478 | if( !isDupColumn(pIdx, pIdx->nKeyCol, pPk, i) ){ |
| 125167 | 125479 | testcase( hasColumn(pIdx->aiColumn, pIdx->nKeyCol, pPk->aiColumn[i]) ); |
| 125168 | 125480 | pIdx->aiColumn[j] = pPk->aiColumn[i]; |
| 125169 | 125481 | pIdx->azColl[j] = pPk->azColl[i]; |
| 125170 | 125482 | if( pPk->aSortOrder[i] ){ |
| 125171 | - /* See ticket https://www.sqlite.org/src/info/bba7b69f9849b5bf */ | |
| 125483 | + /* See ticket https://sqlite.org/src/info/bba7b69f9849b5bf */ | |
| 125172 | 125484 | pIdx->bAscKeyBug = 1; |
| 125173 | 125485 | } |
| 125174 | 125486 | j++; |
| 125175 | 125487 | } |
| 125176 | 125488 | } |
| @@ -126543,11 +126855,11 @@ | ||
| 126543 | 126855 | /* This OP_SeekEnd opcode makes index insert for a REINDEX go much |
| 126544 | 126856 | ** faster by avoiding unnecessary seeks. But the optimization does |
| 126545 | 126857 | ** not work for UNIQUE constraint indexes on WITHOUT ROWID tables |
| 126546 | 126858 | ** with DESC primary keys, since those indexes have there keys in |
| 126547 | 126859 | ** a different order from the main table. |
| 126548 | - ** See ticket: https://www.sqlite.org/src/info/bba7b69f9849b5bf | |
| 126860 | + ** See ticket: https://sqlite.org/src/info/bba7b69f9849b5bf | |
| 126549 | 126861 | */ |
| 126550 | 126862 | sqlite3VdbeAddOp1(v, OP_SeekEnd, iIdx); |
| 126551 | 126863 | } |
| 126552 | 126864 | sqlite3VdbeAddOp2(v, OP_IdxInsert, iIdx, regRecord); |
| 126553 | 126865 | sqlite3VdbeChangeP5(v, OPFLAG_USESEEKRESULT); |
| @@ -126927,10 +127239,11 @@ | ||
| 126927 | 127239 | }else{ |
| 126928 | 127240 | j = pCExpr->iColumn; |
| 126929 | 127241 | assert( j<=0x7fff ); |
| 126930 | 127242 | if( j<0 ){ |
| 126931 | 127243 | j = pTab->iPKey; |
| 127244 | + pIndex->bIdxRowid = 1; | |
| 126932 | 127245 | }else{ |
| 126933 | 127246 | if( pTab->aCol[j].notNull==0 ){ |
| 126934 | 127247 | pIndex->uniqNotNull = 0; |
| 126935 | 127248 | } |
| 126936 | 127249 | if( pTab->aCol[j].colFlags & COLFLAG_VIRTUAL ){ |
| @@ -136632,11 +136945,11 @@ | ||
| 136632 | 136945 | ** OE_Update guarantees that only a single row will change, so it |
| 136633 | 136946 | ** must happen before OE_Replace. Technically, OE_Abort and OE_Rollback |
| 136634 | 136947 | ** could happen in any order, but they are grouped up front for |
| 136635 | 136948 | ** convenience. |
| 136636 | 136949 | ** |
| 136637 | - ** 2018-08-14: Ticket https://www.sqlite.org/src/info/908f001483982c43 | |
| 136950 | + ** 2018-08-14: Ticket https://sqlite.org/src/info/908f001483982c43 | |
| 136638 | 136951 | ** The order of constraints used to have OE_Update as (2) and OE_Abort |
| 136639 | 136952 | ** and so forth as (1). But apparently PostgreSQL checks the OE_Update |
| 136640 | 136953 | ** constraint before any others, so it had to be moved. |
| 136641 | 136954 | ** |
| 136642 | 136955 | ** Constraint checking code is generated in this order: |
| @@ -147785,10 +148098,11 @@ | ||
| 147785 | 148098 | } |
| 147786 | 148099 | |
| 147787 | 148100 | multi_select_end: |
| 147788 | 148101 | pDest->iSdst = dest.iSdst; |
| 147789 | 148102 | pDest->nSdst = dest.nSdst; |
| 148103 | + pDest->iSDParm2 = dest.iSDParm2; | |
| 147790 | 148104 | if( pDelete ){ |
| 147791 | 148105 | sqlite3ParserAddCleanup(pParse, sqlite3SelectDeleteGeneric, pDelete); |
| 147792 | 148106 | } |
| 147793 | 148107 | return rc; |
| 147794 | 148108 | } |
| @@ -149395,11 +149709,12 @@ | ||
| 149395 | 149709 | && pE2->iColumn==pColumn->iColumn |
| 149396 | 149710 | ){ |
| 149397 | 149711 | return; /* Already present. Return without doing anything. */ |
| 149398 | 149712 | } |
| 149399 | 149713 | } |
| 149400 | - if( sqlite3ExprAffinity(pColumn)==SQLITE_AFF_BLOB ){ | |
| 149714 | + assert( SQLITE_AFF_NONE<SQLITE_AFF_BLOB ); | |
| 149715 | + if( sqlite3ExprAffinity(pColumn)<=SQLITE_AFF_BLOB ){ | |
| 149401 | 149716 | pConst->bHasAffBlob = 1; |
| 149402 | 149717 | } |
| 149403 | 149718 | |
| 149404 | 149719 | pConst->nConst++; |
| 149405 | 149720 | pConst->apExpr = sqlite3DbReallocOrFree(pConst->pParse->db, pConst->apExpr, |
| @@ -149470,11 +149785,12 @@ | ||
| 149470 | 149785 | for(i=0; i<pConst->nConst; i++){ |
| 149471 | 149786 | Expr *pColumn = pConst->apExpr[i*2]; |
| 149472 | 149787 | if( pColumn==pExpr ) continue; |
| 149473 | 149788 | if( pColumn->iTable!=pExpr->iTable ) continue; |
| 149474 | 149789 | if( pColumn->iColumn!=pExpr->iColumn ) continue; |
| 149475 | - if( bIgnoreAffBlob && sqlite3ExprAffinity(pColumn)==SQLITE_AFF_BLOB ){ | |
| 149790 | + assert( SQLITE_AFF_NONE<SQLITE_AFF_BLOB ); | |
| 149791 | + if( bIgnoreAffBlob && sqlite3ExprAffinity(pColumn)<=SQLITE_AFF_BLOB ){ | |
| 149476 | 149792 | break; |
| 149477 | 149793 | } |
| 149478 | 149794 | /* A match is found. Add the EP_FixedCol property */ |
| 149479 | 149795 | pConst->nChng++; |
| 149480 | 149796 | ExprClearProperty(pExpr, EP_Leaf); |
| @@ -150123,11 +150439,11 @@ | ||
| 150123 | 150439 | ** |
| 150124 | 150440 | ** This transformation is necessary because the multiSelectOrderBy() routine |
| 150125 | 150441 | ** above that generates the code for a compound SELECT with an ORDER BY clause |
| 150126 | 150442 | ** uses a merge algorithm that requires the same collating sequence on the |
| 150127 | 150443 | ** result columns as on the ORDER BY clause. See ticket |
| 150128 | -** http://www.sqlite.org/src/info/6709574d2a | |
| 150444 | +** http://sqlite.org/src/info/6709574d2a | |
| 150129 | 150445 | ** |
| 150130 | 150446 | ** This transformation is only needed for EXCEPT, INTERSECT, and UNION. |
| 150131 | 150447 | ** The UNION ALL operator works fine with multiSelectOrderBy() even when |
| 150132 | 150448 | ** there are COLLATE terms in the ORDER BY. |
| 150133 | 150449 | */ |
| @@ -152654,10 +152970,16 @@ | ||
| 152654 | 152970 | pWInfo = sqlite3WhereBegin(pParse, pTabList, pWhere, sSort.pOrderBy, |
| 152655 | 152971 | p->pEList, p, wctrlFlags, p->nSelectRow); |
| 152656 | 152972 | if( pWInfo==0 ) goto select_end; |
| 152657 | 152973 | if( sqlite3WhereOutputRowCount(pWInfo) < p->nSelectRow ){ |
| 152658 | 152974 | p->nSelectRow = sqlite3WhereOutputRowCount(pWInfo); |
| 152975 | + if( pDest->eDest<=SRT_DistQueue && pDest->eDest>=SRT_DistFifo ){ | |
| 152976 | + /* TUNING: For a UNION CTE, because UNION is implies DISTINCT, | |
| 152977 | + ** reduce the estimated output row count by 8 (LogEst 30). | |
| 152978 | + ** Search for tag-20250414a to see other cases */ | |
| 152979 | + p->nSelectRow -= 30; | |
| 152980 | + } | |
| 152659 | 152981 | } |
| 152660 | 152982 | if( sDistinct.isTnct && sqlite3WhereIsDistinct(pWInfo) ){ |
| 152661 | 152983 | sDistinct.eTnctType = sqlite3WhereIsDistinct(pWInfo); |
| 152662 | 152984 | } |
| 152663 | 152985 | if( sSort.pOrderBy ){ |
| @@ -156925,11 +157247,11 @@ | ||
| 156925 | 157247 | iDb = sqlite3TwoPartName(pParse, pNm, pNm, &pNm); |
| 156926 | 157248 | if( iDb<0 ) goto build_vacuum_end; |
| 156927 | 157249 | #else |
| 156928 | 157250 | /* When SQLITE_BUG_COMPATIBLE_20160819 is defined, unrecognized arguments |
| 156929 | 157251 | ** to VACUUM are silently ignored. This is a back-out of a bug fix that |
| 156930 | - ** occurred on 2016-08-19 (https://www.sqlite.org/src/info/083f9e6270). | |
| 157252 | + ** occurred on 2016-08-19 (https://sqlite.org/src/info/083f9e6270). | |
| 156931 | 157253 | ** The buggy behavior is required for binary compatibility with some |
| 156932 | 157254 | ** legacy applications. */ |
| 156933 | 157255 | iDb = sqlite3FindDb(pParse->db, pNm); |
| 156934 | 157256 | if( iDb<0 ) iDb = 0; |
| 156935 | 157257 | #endif |
| @@ -159820,11 +160142,11 @@ | ||
| 159820 | 160142 | |
| 159821 | 160143 | |
| 159822 | 160144 | /* |
| 159823 | 160145 | ** pX is an expression of the form: (vector) IN (SELECT ...) |
| 159824 | 160146 | ** In other words, it is a vector IN operator with a SELECT clause on the |
| 159825 | -** LHS. But not all terms in the vector are indexable and the terms might | |
| 160147 | +** RHS. But not all terms in the vector are indexable and the terms might | |
| 159826 | 160148 | ** not be in the correct order for indexing. |
| 159827 | 160149 | ** |
| 159828 | 160150 | ** This routine makes a copy of the input pX expression and then adjusts |
| 159829 | 160151 | ** the vector on the LHS with corresponding changes to the SELECT so that |
| 159830 | 160152 | ** the vector contains only index terms and those terms are in the correct |
| @@ -161642,11 +161964,11 @@ | ||
| 161642 | 161964 | ** ON or USING clause of a LEFT JOIN, and terms that are usable as |
| 161643 | 161965 | ** indices. |
| 161644 | 161966 | ** |
| 161645 | 161967 | ** This optimization also only applies if the (x1 OR x2 OR ...) term |
| 161646 | 161968 | ** is not contained in the ON clause of a LEFT JOIN. |
| 161647 | - ** See ticket http://www.sqlite.org/src/info/f2369304e4 | |
| 161969 | + ** See ticket http://sqlite.org/src/info/f2369304e4 | |
| 161648 | 161970 | ** |
| 161649 | 161971 | ** 2022-02-04: Do not push down slices of a row-value comparison. |
| 161650 | 161972 | ** In other words, "w" or "y" may not be a slice of a vector. Otherwise, |
| 161651 | 161973 | ** the initialization of the right-hand operand of the vector comparison |
| 161652 | 161974 | ** might not occur, or might occur only in an OR branch that is not |
| @@ -167615,11 +167937,11 @@ | ||
| 167615 | 167937 | } |
| 167616 | 167938 | |
| 167617 | 167939 | if( (pNew->wsFlags & WHERE_TOP_LIMIT)==0 |
| 167618 | 167940 | && pNew->u.btree.nEq<pProbe->nColumn |
| 167619 | 167941 | && (pNew->u.btree.nEq<pProbe->nKeyCol || |
| 167620 | - pProbe->idxType!=SQLITE_IDXTYPE_PRIMARYKEY) | |
| 167942 | + (pProbe->idxType!=SQLITE_IDXTYPE_PRIMARYKEY && !pProbe->bIdxRowid)) | |
| 167621 | 167943 | ){ |
| 167622 | 167944 | if( pNew->u.btree.nEq>3 ){ |
| 167623 | 167945 | sqlite3ProgressCheck(pParse); |
| 167624 | 167946 | } |
| 167625 | 167947 | whereLoopAddBtreeIndex(pBuilder, pSrc, pProbe, nInMul+nIn); |
| @@ -171073,11 +171395,12 @@ | ||
| 171073 | 171395 | wherePathSolver(pWInfo, pWInfo->nRowOut<0 ? 1 : pWInfo->nRowOut+1); |
| 171074 | 171396 | if( db->mallocFailed ) goto whereBeginError; |
| 171075 | 171397 | } |
| 171076 | 171398 | |
| 171077 | 171399 | /* TUNING: Assume that a DISTINCT clause on a subquery reduces |
| 171078 | - ** the output size by a factor of 8 (LogEst -30). | |
| 171400 | + ** the output size by a factor of 8 (LogEst -30). Search for | |
| 171401 | + ** tag-20250414a to see other cases. | |
| 171079 | 171402 | */ |
| 171080 | 171403 | if( (pWInfo->wctrlFlags & WHERE_WANT_DISTINCT)!=0 ){ |
| 171081 | 171404 | WHERETRACE(0x0080,("nRowOut reduced from %d to %d due to DISTINCT\n", |
| 171082 | 171405 | pWInfo->nRowOut, pWInfo->nRowOut-30)); |
| 171083 | 171406 | pWInfo->nRowOut -= 30; |
| @@ -188065,10 +188388,17 @@ | ||
| 188065 | 188388 | ****************************************************************************** |
| 188066 | 188389 | ** |
| 188067 | 188390 | */ |
| 188068 | 188391 | #ifndef _FTSINT_H |
| 188069 | 188392 | #define _FTSINT_H |
| 188393 | + | |
| 188394 | +/* #include <assert.h> */ | |
| 188395 | +/* #include <stdlib.h> */ | |
| 188396 | +/* #include <stddef.h> */ | |
| 188397 | +/* #include <stdio.h> */ | |
| 188398 | +/* #include <string.h> */ | |
| 188399 | +/* #include <stdarg.h> */ | |
| 188070 | 188400 | |
| 188071 | 188401 | #if !defined(NDEBUG) && !defined(SQLITE_DEBUG) |
| 188072 | 188402 | # define NDEBUG 1 |
| 188073 | 188403 | #endif |
| 188074 | 188404 | |
| @@ -189017,16 +189347,10 @@ | ||
| 189017 | 189347 | |
| 189018 | 189348 | #if defined(SQLITE_ENABLE_FTS3) && !defined(SQLITE_CORE) |
| 189019 | 189349 | # define SQLITE_CORE 1 |
| 189020 | 189350 | #endif |
| 189021 | 189351 | |
| 189022 | -/* #include <assert.h> */ | |
| 189023 | -/* #include <stdlib.h> */ | |
| 189024 | -/* #include <stddef.h> */ | |
| 189025 | -/* #include <stdio.h> */ | |
| 189026 | -/* #include <string.h> */ | |
| 189027 | -/* #include <stdarg.h> */ | |
| 189028 | 189352 | |
| 189029 | 189353 | /* #include "fts3.h" */ |
| 189030 | 189354 | #ifndef SQLITE_CORE |
| 189031 | 189355 | /* # include "sqlite3ext.h" */ |
| 189032 | 189356 | SQLITE_EXTENSION_INIT1 |
| @@ -198969,11 +199293,11 @@ | ||
| 198969 | 199293 | UNUSED_PARAMETER(nVal); |
| 198970 | 199294 | |
| 198971 | 199295 | fts3tokResetCursor(pCsr); |
| 198972 | 199296 | if( idxNum==1 ){ |
| 198973 | 199297 | const char *zByte = (const char *)sqlite3_value_text(apVal[0]); |
| 198974 | - int nByte = sqlite3_value_bytes(apVal[0]); | |
| 199298 | + sqlite3_int64 nByte = sqlite3_value_bytes(apVal[0]); | |
| 198975 | 199299 | pCsr->zInput = sqlite3_malloc64(nByte+1); |
| 198976 | 199300 | if( pCsr->zInput==0 ){ |
| 198977 | 199301 | rc = SQLITE_NOMEM; |
| 198978 | 199302 | }else{ |
| 198979 | 199303 | if( nByte>0 ) memcpy(pCsr->zInput, zByte, nByte); |
| @@ -205949,20 +206273,20 @@ | ||
| 205949 | 206273 | case FTS3_MATCHINFO_LCS: |
| 205950 | 206274 | nVal = pInfo->nCol; |
| 205951 | 206275 | break; |
| 205952 | 206276 | |
| 205953 | 206277 | case FTS3_MATCHINFO_LHITS: |
| 205954 | - nVal = pInfo->nCol * pInfo->nPhrase; | |
| 206278 | + nVal = (size_t)pInfo->nCol * pInfo->nPhrase; | |
| 205955 | 206279 | break; |
| 205956 | 206280 | |
| 205957 | 206281 | case FTS3_MATCHINFO_LHITS_BM: |
| 205958 | - nVal = pInfo->nPhrase * ((pInfo->nCol + 31) / 32); | |
| 206282 | + nVal = (size_t)pInfo->nPhrase * ((pInfo->nCol + 31) / 32); | |
| 205959 | 206283 | break; |
| 205960 | 206284 | |
| 205961 | 206285 | default: |
| 205962 | 206286 | assert( cArg==FTS3_MATCHINFO_HITS ); |
| 205963 | - nVal = pInfo->nCol * pInfo->nPhrase * 3; | |
| 206287 | + nVal = (size_t)pInfo->nCol * pInfo->nPhrase * 3; | |
| 205964 | 206288 | break; |
| 205965 | 206289 | } |
| 205966 | 206290 | |
| 205967 | 206291 | return nVal; |
| 205968 | 206292 | } |
| @@ -207516,12 +207840,12 @@ | ||
| 207516 | 207840 | ** with JSON-5 extensions is accepted as input. |
| 207517 | 207841 | ** |
| 207518 | 207842 | ** Beginning with version 3.45.0 (circa 2024-01-01), these routines also |
| 207519 | 207843 | ** accept BLOB values that have JSON encoded using a binary representation |
| 207520 | 207844 | ** called "JSONB". The name JSONB comes from PostgreSQL, however the on-disk |
| 207521 | -** format SQLite JSONB is completely different and incompatible with | |
| 207522 | -** PostgreSQL JSONB. | |
| 207845 | +** format for SQLite-JSONB is completely different and incompatible with | |
| 207846 | +** PostgreSQL-JSONB. | |
| 207523 | 207847 | ** |
| 207524 | 207848 | ** Decoding and interpreting JSONB is still O(N) where N is the size of |
| 207525 | 207849 | ** the input, the same as text JSON. However, the constant of proportionality |
| 207526 | 207850 | ** for JSONB is much smaller due to faster parsing. The size of each |
| 207527 | 207851 | ** element in JSONB is encoded in its header, so there is no need to search |
| @@ -207574,21 +207898,21 @@ | ||
| 207574 | 207898 | ** 14 4 byte (0-4294967295) 5 |
| 207575 | 207899 | ** 15 8 byte (0-1.8e19) 9 |
| 207576 | 207900 | ** |
| 207577 | 207901 | ** The payload size need not be expressed in its minimal form. For example, |
| 207578 | 207902 | ** if the payload size is 10, the size can be expressed in any of 5 different |
| 207579 | -** ways: (1) (X>>4)==10, (2) (X>>4)==12 following by on 0x0a byte, | |
| 207903 | +** ways: (1) (X>>4)==10, (2) (X>>4)==12 following by one 0x0a byte, | |
| 207580 | 207904 | ** (3) (X>>4)==13 followed by 0x00 and 0x0a, (4) (X>>4)==14 followed by |
| 207581 | 207905 | ** 0x00 0x00 0x00 0x0a, or (5) (X>>4)==15 followed by 7 bytes of 0x00 and |
| 207582 | 207906 | ** a single byte of 0x0a. The shorter forms are preferred, of course, but |
| 207583 | 207907 | ** sometimes when generating JSONB, the payload size is not known in advance |
| 207584 | 207908 | ** and it is convenient to reserve sufficient header space to cover the |
| 207585 | 207909 | ** largest possible payload size and then come back later and patch up |
| 207586 | 207910 | ** the size when it becomes known, resulting in a non-minimal encoding. |
| 207587 | 207911 | ** |
| 207588 | 207912 | ** The value (X>>4)==15 is not actually used in the current implementation |
| 207589 | -** (as SQLite is currently unable handle BLOBs larger than about 2GB) | |
| 207913 | +** (as SQLite is currently unable to handle BLOBs larger than about 2GB) | |
| 207590 | 207914 | ** but is included in the design to allow for future enhancements. |
| 207591 | 207915 | ** |
| 207592 | 207916 | ** The payload follows the header. NULL, TRUE, and FALSE have no payload and |
| 207593 | 207917 | ** their payload size must always be zero. The payload for INT, INT5, |
| 207594 | 207918 | ** FLOAT, FLOAT5, TEXT, TEXTJ, TEXT5, and TEXTROW is text. Note that the |
| @@ -208658,11 +208982,11 @@ | ||
| 208658 | 208982 | if( jsonBlobExpand(pParse, pParse->nBlob+szPayload+9) ) return; |
| 208659 | 208983 | jsonBlobAppendNode(pParse, eType, szPayload, aPayload); |
| 208660 | 208984 | } |
| 208661 | 208985 | |
| 208662 | 208986 | |
| 208663 | -/* Append an node type byte together with the payload size and | |
| 208987 | +/* Append a node type byte together with the payload size and | |
| 208664 | 208988 | ** possibly also the payload. |
| 208665 | 208989 | ** |
| 208666 | 208990 | ** If aPayload is not NULL, then it is a pointer to the payload which |
| 208667 | 208991 | ** is also appended. If aPayload is NULL, the pParse->aBlob[] array |
| 208668 | 208992 | ** is resized (if necessary) so that it is big enough to hold the |
| @@ -209992,10 +210316,86 @@ | ||
| 209992 | 210316 | (void)jsonbPayloadSize(pParse, iRoot, &sz); |
| 209993 | 210317 | pParse->nBlob = nBlob; |
| 209994 | 210318 | sz += pParse->delta; |
| 209995 | 210319 | pParse->delta += jsonBlobChangePayloadSize(pParse, iRoot, sz); |
| 209996 | 210320 | } |
| 210321 | + | |
| 210322 | +/* | |
| 210323 | +** If the JSONB at aIns[0..nIns-1] can be expanded (by denormalizing the | |
| 210324 | +** size field) by d bytes, then write the expansion into aOut[] and | |
| 210325 | +** return true. In this way, an overwrite happens without changing the | |
| 210326 | +** size of the JSONB, which reduces memcpy() operations and also make it | |
| 210327 | +** faster and easier to update the B-Tree entry that contains the JSONB | |
| 210328 | +** in the database. | |
| 210329 | +** | |
| 210330 | +** If the expansion of aIns[] by d bytes cannot be (easily) accomplished | |
| 210331 | +** then return false. | |
| 210332 | +** | |
| 210333 | +** The d parameter is guaranteed to be between 1 and 8. | |
| 210334 | +** | |
| 210335 | +** This routine is an optimization. A correct answer is obtained if it | |
| 210336 | +** always leaves the output unchanged and returns false. | |
| 210337 | +*/ | |
| 210338 | +static int jsonBlobOverwrite( | |
| 210339 | + u8 *aOut, /* Overwrite here */ | |
| 210340 | + const u8 *aIns, /* New content */ | |
| 210341 | + u32 nIns, /* Bytes of new content */ | |
| 210342 | + u32 d /* Need to expand new content by this much */ | |
| 210343 | +){ | |
| 210344 | + u32 szPayload; /* Bytes of payload */ | |
| 210345 | + u32 i; /* New header size, after expansion & a loop counter */ | |
| 210346 | + u8 szHdr; /* Size of header before expansion */ | |
| 210347 | + | |
| 210348 | + /* Lookup table for finding the upper 4 bits of the first byte of the | |
| 210349 | + ** expanded aIns[], based on the size of the expanded aIns[] header: | |
| 210350 | + ** | |
| 210351 | + ** 2 3 4 5 6 7 8 9 */ | |
| 210352 | + static const u8 aType[] = { 0xc0, 0xd0, 0, 0xe0, 0, 0, 0, 0xf0 }; | |
| 210353 | + | |
| 210354 | + if( (aIns[0]&0x0f)<=2 ) return 0; /* Cannot enlarge NULL, true, false */ | |
| 210355 | + switch( aIns[0]>>4 ){ | |
| 210356 | + default: { /* aIns[] header size 1 */ | |
| 210357 | + if( ((1<<d)&0x116)==0 ) return 0; /* d must be 1, 2, 4, or 8 */ | |
| 210358 | + i = d + 1; /* New hdr sz: 2, 3, 5, or 9 */ | |
| 210359 | + szHdr = 1; | |
| 210360 | + break; | |
| 210361 | + } | |
| 210362 | + case 12: { /* aIns[] header size is 2 */ | |
| 210363 | + if( ((1<<d)&0x8a)==0) return 0; /* d must be 1, 3, or 7 */ | |
| 210364 | + i = d + 2; /* New hdr sz: 2, 5, or 9 */ | |
| 210365 | + szHdr = 2; | |
| 210366 | + break; | |
| 210367 | + } | |
| 210368 | + case 13: { /* aIns[] header size is 3 */ | |
| 210369 | + if( d!=2 && d!=6 ) return 0; /* d must be 2 or 6 */ | |
| 210370 | + i = d + 3; /* New hdr sz: 5 or 9 */ | |
| 210371 | + szHdr = 3; | |
| 210372 | + break; | |
| 210373 | + } | |
| 210374 | + case 14: { /* aIns[] header size is 5 */ | |
| 210375 | + if( d!=4 ) return 0; /* d must be 4 */ | |
| 210376 | + i = 9; /* New hdr sz: 9 */ | |
| 210377 | + szHdr = 5; | |
| 210378 | + break; | |
| 210379 | + } | |
| 210380 | + case 15: { /* aIns[] header size is 9 */ | |
| 210381 | + return 0; /* No solution */ | |
| 210382 | + } | |
| 210383 | + } | |
| 210384 | + assert( i>=2 && i<=9 && aType[i-2]!=0 ); | |
| 210385 | + aOut[0] = (aIns[0] & 0x0f) | aType[i-2]; | |
| 210386 | + memcpy(&aOut[i], &aIns[szHdr], nIns-szHdr); | |
| 210387 | + szPayload = nIns - szHdr; | |
| 210388 | + while( 1/*edit-by-break*/ ){ | |
| 210389 | + i--; | |
| 210390 | + aOut[i] = szPayload & 0xff; | |
| 210391 | + if( i==1 ) break; | |
| 210392 | + szPayload >>= 8; | |
| 210393 | + } | |
| 210394 | + assert( (szPayload>>8)==0 ); | |
| 210395 | + return 1; | |
| 210396 | +} | |
| 209997 | 210397 | |
| 209998 | 210398 | /* |
| 209999 | 210399 | ** Modify the JSONB blob at pParse->aBlob by removing nDel bytes of |
| 210000 | 210400 | ** content beginning at iDel, and replacing them with nIns bytes of |
| 210001 | 210401 | ** content given by aIns. |
| @@ -210014,10 +210414,15 @@ | ||
| 210014 | 210414 | u32 nDel, /* Number of bytes to remove */ |
| 210015 | 210415 | const u8 *aIns, /* Content to insert */ |
| 210016 | 210416 | u32 nIns /* Bytes of content to insert */ |
| 210017 | 210417 | ){ |
| 210018 | 210418 | i64 d = (i64)nIns - (i64)nDel; |
| 210419 | + if( d<0 && d>=(-8) && aIns!=0 | |
| 210420 | + && jsonBlobOverwrite(&pParse->aBlob[iDel], aIns, nIns, (int)-d) | |
| 210421 | + ){ | |
| 210422 | + return; | |
| 210423 | + } | |
| 210019 | 210424 | if( d!=0 ){ |
| 210020 | 210425 | if( pParse->nBlob + d > pParse->nBlobAlloc ){ |
| 210021 | 210426 | jsonBlobExpand(pParse, pParse->nBlob+d); |
| 210022 | 210427 | if( pParse->oom ) return; |
| 210023 | 210428 | } |
| @@ -210025,11 +210430,13 @@ | ||
| 210025 | 210430 | &pParse->aBlob[iDel+nDel], |
| 210026 | 210431 | pParse->nBlob - (iDel+nDel)); |
| 210027 | 210432 | pParse->nBlob += d; |
| 210028 | 210433 | pParse->delta += d; |
| 210029 | 210434 | } |
| 210030 | - if( nIns && aIns ) memcpy(&pParse->aBlob[iDel], aIns, nIns); | |
| 210435 | + if( nIns && aIns ){ | |
| 210436 | + memcpy(&pParse->aBlob[iDel], aIns, nIns); | |
| 210437 | + } | |
| 210031 | 210438 | } |
| 210032 | 210439 | |
| 210033 | 210440 | /* |
| 210034 | 210441 | ** Return the number of escaped newlines to be ignored. |
| 210035 | 210442 | ** An escaped newline is a one of the following byte sequences: |
| @@ -210788,11 +211195,11 @@ | ||
| 210788 | 211195 | } |
| 210789 | 211196 | return 0; |
| 210790 | 211197 | } |
| 210791 | 211198 | |
| 210792 | 211199 | /* argv[0] is a BLOB that seems likely to be a JSONB. Subsequent |
| 210793 | -** arguments come in parse where each pair contains a JSON path and | |
| 211200 | +** arguments come in pairs where each pair contains a JSON path and | |
| 210794 | 211201 | ** content to insert or set at that patch. Do the updates |
| 210795 | 211202 | ** and return the result. |
| 210796 | 211203 | ** |
| 210797 | 211204 | ** The specific operation is determined by eEdit, which can be one |
| 210798 | 211205 | ** of JEDIT_INS, JEDIT_REPL, or JEDIT_SET. |
| @@ -227533,12 +227940,12 @@ | ||
| 227533 | 227940 | ){ |
| 227534 | 227941 | if( sqlite3_value_type(argv[3])==SQLITE_NULL && isInsert && pgno>1 ){ |
| 227535 | 227942 | /* "INSERT INTO dbpage($PGNO,NULL)" causes page number $PGNO and |
| 227536 | 227943 | ** all subsequent pages to be deleted. */ |
| 227537 | 227944 | pTab->iDbTrunc = iDb; |
| 227538 | - pgno--; | |
| 227539 | - pTab->pgnoTrunc = pgno; | |
| 227945 | + pTab->pgnoTrunc = pgno-1; | |
| 227946 | + pgno = 1; | |
| 227540 | 227947 | }else{ |
| 227541 | 227948 | zErr = "bad page value"; |
| 227542 | 227949 | goto update_fail; |
| 227543 | 227950 | } |
| 227544 | 227951 | } |
| @@ -228850,10 +229257,12 @@ | ||
| 228850 | 229257 | int rc = SQLITE_OK; |
| 228851 | 229258 | |
| 228852 | 229259 | if( pTab->nCol==0 ){ |
| 228853 | 229260 | u8 *abPK; |
| 228854 | 229261 | assert( pTab->azCol==0 || pTab->abPK==0 ); |
| 229262 | + sqlite3_free(pTab->azCol); | |
| 229263 | + pTab->abPK = 0; | |
| 228855 | 229264 | rc = sessionTableInfo(pSession, db, zDb, |
| 228856 | 229265 | pTab->zName, &pTab->nCol, &pTab->nTotalCol, 0, &pTab->azCol, |
| 228857 | 229266 | &pTab->azDflt, &pTab->aiIdx, &abPK, |
| 228858 | 229267 | ((pSession==0 || pSession->bImplicitPK) ? &pTab->bRowid : 0) |
| 228859 | 229268 | ); |
| @@ -229857,11 +230266,13 @@ | ||
| 229857 | 230266 | char *zExpr = 0; |
| 229858 | 230267 | sqlite3 *db = pSession->db; |
| 229859 | 230268 | SessionTable *pTo; /* Table zTbl */ |
| 229860 | 230269 | |
| 229861 | 230270 | /* Locate and if necessary initialize the target table object */ |
| 230271 | + pSession->bAutoAttach++; | |
| 229862 | 230272 | rc = sessionFindTable(pSession, zTbl, &pTo); |
| 230273 | + pSession->bAutoAttach--; | |
| 229863 | 230274 | if( pTo==0 ) goto diff_out; |
| 229864 | 230275 | if( sessionInitTable(pSession, pTo, pSession->db, pSession->zDb) ){ |
| 229865 | 230276 | rc = pSession->rc; |
| 229866 | 230277 | goto diff_out; |
| 229867 | 230278 | } |
| @@ -229868,21 +230279,47 @@ | ||
| 229868 | 230279 | |
| 229869 | 230280 | /* Check the table schemas match */ |
| 229870 | 230281 | if( rc==SQLITE_OK ){ |
| 229871 | 230282 | int bHasPk = 0; |
| 229872 | 230283 | int bMismatch = 0; |
| 229873 | - int nCol; /* Columns in zFrom.zTbl */ | |
| 230284 | + int nCol = 0; /* Columns in zFrom.zTbl */ | |
| 229874 | 230285 | int bRowid = 0; |
| 229875 | - u8 *abPK; | |
| 230286 | + u8 *abPK = 0; | |
| 229876 | 230287 | const char **azCol = 0; |
| 229877 | - rc = sessionTableInfo(0, db, zFrom, zTbl, | |
| 229878 | - &nCol, 0, 0, &azCol, 0, 0, &abPK, | |
| 229879 | - pSession->bImplicitPK ? &bRowid : 0 | |
| 229880 | - ); | |
| 230288 | + char *zDbExists = 0; | |
| 230289 | + | |
| 230290 | + /* Check that database zFrom is attached. */ | |
| 230291 | + zDbExists = sqlite3_mprintf("SELECT * FROM %Q.sqlite_schema", zFrom); | |
| 230292 | + if( zDbExists==0 ){ | |
| 230293 | + rc = SQLITE_NOMEM; | |
| 230294 | + }else{ | |
| 230295 | + sqlite3_stmt *pDbExists = 0; | |
| 230296 | + rc = sqlite3_prepare_v2(db, zDbExists, -1, &pDbExists, 0); | |
| 230297 | + if( rc==SQLITE_ERROR ){ | |
| 230298 | + rc = SQLITE_OK; | |
| 230299 | + nCol = -1; | |
| 230300 | + } | |
| 230301 | + sqlite3_finalize(pDbExists); | |
| 230302 | + sqlite3_free(zDbExists); | |
| 230303 | + } | |
| 230304 | + | |
| 230305 | + if( rc==SQLITE_OK && nCol==0 ){ | |
| 230306 | + rc = sessionTableInfo(0, db, zFrom, zTbl, | |
| 230307 | + &nCol, 0, 0, &azCol, 0, 0, &abPK, | |
| 230308 | + pSession->bImplicitPK ? &bRowid : 0 | |
| 230309 | + ); | |
| 230310 | + } | |
| 229881 | 230311 | if( rc==SQLITE_OK ){ |
| 229882 | 230312 | if( pTo->nCol!=nCol ){ |
| 229883 | - bMismatch = 1; | |
| 230313 | + if( nCol<=0 ){ | |
| 230314 | + rc = SQLITE_SCHEMA; | |
| 230315 | + if( pzErrMsg ){ | |
| 230316 | + *pzErrMsg = sqlite3_mprintf("no such table: %s.%s", zFrom, zTbl); | |
| 230317 | + } | |
| 230318 | + }else{ | |
| 230319 | + bMismatch = 1; | |
| 230320 | + } | |
| 229884 | 230321 | }else{ |
| 229885 | 230322 | int i; |
| 229886 | 230323 | for(i=0; i<nCol; i++){ |
| 229887 | 230324 | if( pTo->abPK[i]!=abPK[i] ) bMismatch = 1; |
| 229888 | 230325 | if( sqlite3_stricmp(azCol[i], pTo->azCol[i]) ) bMismatch = 1; |
| @@ -241869,11 +242306,12 @@ | ||
| 241869 | 242306 | sqlite3Fts5ParseError( |
| 241870 | 242307 | pParse, "expected integer, got \"%.*s\"", p->n, p->p |
| 241871 | 242308 | ); |
| 241872 | 242309 | return; |
| 241873 | 242310 | } |
| 241874 | - nNear = nNear * 10 + (p->p[i] - '0'); | |
| 242311 | + if( nNear<214748363 ) nNear = nNear * 10 + (p->p[i] - '0'); | |
| 242312 | + /* ^^^^^^^^^^^^^^^--- Prevent integer overflow */ | |
| 241875 | 242313 | } |
| 241876 | 242314 | }else{ |
| 241877 | 242315 | nNear = FTS5_DEFAULT_NEARDIST; |
| 241878 | 242316 | } |
| 241879 | 242317 | pNear->nNear = nNear; |
| @@ -256775,11 +257213,11 @@ | ||
| 256775 | 257213 | int nArg, /* Number of args */ |
| 256776 | 257214 | sqlite3_value **apUnused /* Function arguments */ |
| 256777 | 257215 | ){ |
| 256778 | 257216 | assert( nArg==0 ); |
| 256779 | 257217 | UNUSED_PARAM2(nArg, apUnused); |
| 256780 | - sqlite3_result_text(pCtx, "fts5: 2025-03-16 00:13:29 18bda13e197e4b4ec7464b3e70012f71edc05f73d8b14bb48bad452f81c7e185", -1, SQLITE_TRANSIENT); | |
| 257218 | + sqlite3_result_text(pCtx, "fts5: 2025-04-15 21:59:38 d22475b81c4e26ccc50f3b5626d43b32f7a2de34e5a764539554665bdda735d5", -1, SQLITE_TRANSIENT); | |
| 256781 | 257219 | } |
| 256782 | 257220 | |
| 256783 | 257221 | /* |
| 256784 | 257222 | ** Implementation of fts5_locale(LOCALE, TEXT) function. |
| 256785 | 257223 | ** |
| @@ -260838,11 +261276,10 @@ | ||
| 260838 | 261276 | } |
| 260839 | 261277 | iTbl++; |
| 260840 | 261278 | } |
| 260841 | 261279 | aAscii[0] = 0; /* 0x00 is never a token character */ |
| 260842 | 261280 | } |
| 260843 | - | |
| 260844 | 261281 | |
| 260845 | 261282 | /* |
| 260846 | 261283 | ** 2015 May 30 |
| 260847 | 261284 | ** |
| 260848 | 261285 | ** The author disclaims copyright to this source code. In place of |
| 260849 | 261286 |
| --- extsrc/sqlite3.c | |
| +++ extsrc/sqlite3.c | |
| @@ -16,11 +16,11 @@ | |
| 16 | ** if you want a wrapper to interface SQLite with your choice of programming |
| 17 | ** language. The code for the "sqlite3" command-line shell is also in a |
| 18 | ** separate file. This file contains only code for the core SQLite library. |
| 19 | ** |
| 20 | ** The content in this amalgamation comes from Fossil check-in |
| 21 | ** 18bda13e197e4b4ec7464b3e70012f71edc0 with changes in files: |
| 22 | ** |
| 23 | ** |
| 24 | */ |
| 25 | #ifndef SQLITE_AMALGAMATION |
| 26 | #define SQLITE_CORE 1 |
| @@ -450,11 +450,11 @@ | |
| 450 | ** be held constant and Z will be incremented or else Y will be incremented |
| 451 | ** and Z will be reset to zero. |
| 452 | ** |
| 453 | ** Since [version 3.6.18] ([dateof:3.6.18]), |
| 454 | ** SQLite source code has been stored in the |
| 455 | ** <a href="http://www.fossil-scm.org/">Fossil configuration management |
| 456 | ** system</a>. ^The SQLITE_SOURCE_ID macro evaluates to |
| 457 | ** a string which identifies a particular check-in of SQLite |
| 458 | ** within its configuration management system. ^The SQLITE_SOURCE_ID |
| 459 | ** string contains the date and time of the check-in (UTC) and a SHA1 |
| 460 | ** or SHA3-256 hash of the entire source tree. If the source code has |
| @@ -465,11 +465,11 @@ | |
| 465 | ** [sqlite3_libversion_number()], [sqlite3_sourceid()], |
| 466 | ** [sqlite_version()] and [sqlite_source_id()]. |
| 467 | */ |
| 468 | #define SQLITE_VERSION "3.50.0" |
| 469 | #define SQLITE_VERSION_NUMBER 3050000 |
| 470 | #define SQLITE_SOURCE_ID "2025-03-16 00:13:29 18bda13e197e4b4ec7464b3e70012f71edc05f73d8b14bb48bad452f81c7e185" |
| 471 | |
| 472 | /* |
| 473 | ** CAPI3REF: Run-Time Library Version Numbers |
| 474 | ** KEYWORDS: sqlite3_version sqlite3_sourceid |
| 475 | ** |
| @@ -11946,12 +11946,13 @@ | |
| 11946 | ** To clarify, if this function is called and then a changeset constructed |
| 11947 | ** using [sqlite3session_changeset()], then after applying that changeset to |
| 11948 | ** database zFrom the contents of the two compatible tables would be |
| 11949 | ** identical. |
| 11950 | ** |
| 11951 | ** It an error if database zFrom does not exist or does not contain the |
| 11952 | ** required compatible table. |
| 11953 | ** |
| 11954 | ** If the operation is successful, SQLITE_OK is returned. Otherwise, an SQLite |
| 11955 | ** error code. In this case, if argument pzErrMsg is not NULL, *pzErrMsg |
| 11956 | ** may be set to point to a buffer containing an English language error |
| 11957 | ** message. It is the responsibility of the caller to free this buffer using |
| @@ -19162,10 +19163,11 @@ | |
| 19162 | unsigned noSkipScan:1; /* Do not try to use skip-scan if true */ |
| 19163 | unsigned hasStat1:1; /* aiRowLogEst values come from sqlite_stat1 */ |
| 19164 | unsigned bLowQual:1; /* sqlite_stat1 says this is a low-quality index */ |
| 19165 | unsigned bNoQuery:1; /* Do not use this index to optimize queries */ |
| 19166 | unsigned bAscKeyBug:1; /* True if the bba7b69f9849b5bf bug applies */ |
| 19167 | unsigned bHasVCol:1; /* Index references one or more VIRTUAL columns */ |
| 19168 | unsigned bHasExpr:1; /* Index contains an expression, either a literal |
| 19169 | ** expression, or a reference to a VIRTUAL column */ |
| 19170 | #ifdef SQLITE_ENABLE_STAT4 |
| 19171 | int nSample; /* Number of elements in aSample[] */ |
| @@ -30270,10 +30272,12 @@ | |
| 30270 | */ |
| 30271 | #include "windows.h" |
| 30272 | |
| 30273 | #ifdef __CYGWIN__ |
| 30274 | # include <sys/cygwin.h> |
| 30275 | # include <errno.h> /* amalgamator: dontcache */ |
| 30276 | #endif |
| 30277 | |
| 30278 | /* |
| 30279 | ** Determine if we are dealing with Windows NT. |
| @@ -35444,11 +35448,11 @@ | |
| 35444 | unsigned char const *z = zIn; |
| 35445 | unsigned char const *zEnd = &z[nByte-1]; |
| 35446 | int n = 0; |
| 35447 | |
| 35448 | if( SQLITE_UTF16NATIVE==SQLITE_UTF16LE ) z++; |
| 35449 | while( n<nChar && ALWAYS(z<=zEnd) ){ |
| 35450 | c = z[0]; |
| 35451 | z += 2; |
| 35452 | if( c>=0xd8 && c<0xdc && z<=zEnd && z[0]>=0xdc && z[0]<0xe0 ) z += 2; |
| 35453 | n++; |
| 35454 | } |
| @@ -47726,20 +47730,20 @@ | |
| 47726 | { "FileTimeToLocalFileTime", (SYSCALL)FileTimeToLocalFileTime, 0 }, |
| 47727 | #else |
| 47728 | { "FileTimeToLocalFileTime", (SYSCALL)0, 0 }, |
| 47729 | #endif |
| 47730 | |
| 47731 | #define osFileTimeToLocalFileTime ((BOOL(WINAPI*)(CONST FILETIME*, \ |
| 47732 | LPFILETIME))aSyscall[11].pCurrent) |
| 47733 | |
| 47734 | #if SQLITE_OS_WINCE |
| 47735 | { "FileTimeToSystemTime", (SYSCALL)FileTimeToSystemTime, 0 }, |
| 47736 | #else |
| 47737 | { "FileTimeToSystemTime", (SYSCALL)0, 0 }, |
| 47738 | #endif |
| 47739 | |
| 47740 | #define osFileTimeToSystemTime ((BOOL(WINAPI*)(CONST FILETIME*, \ |
| 47741 | LPSYSTEMTIME))aSyscall[12].pCurrent) |
| 47742 | |
| 47743 | { "FlushFileBuffers", (SYSCALL)FlushFileBuffers, 0 }, |
| 47744 | |
| 47745 | #define osFlushFileBuffers ((BOOL(WINAPI*)(HANDLE))aSyscall[13].pCurrent) |
| @@ -48015,11 +48019,11 @@ | |
| 48015 | { "LockFile", (SYSCALL)LockFile, 0 }, |
| 48016 | #else |
| 48017 | { "LockFile", (SYSCALL)0, 0 }, |
| 48018 | #endif |
| 48019 | |
| 48020 | #ifndef osLockFile |
| 48021 | #define osLockFile ((BOOL(WINAPI*)(HANDLE,DWORD,DWORD,DWORD, \ |
| 48022 | DWORD))aSyscall[47].pCurrent) |
| 48023 | #endif |
| 48024 | |
| 48025 | #if !SQLITE_OS_WINCE |
| @@ -48079,20 +48083,20 @@ | |
| 48079 | |
| 48080 | #define osSleep ((VOID(WINAPI*)(DWORD))aSyscall[55].pCurrent) |
| 48081 | |
| 48082 | { "SystemTimeToFileTime", (SYSCALL)SystemTimeToFileTime, 0 }, |
| 48083 | |
| 48084 | #define osSystemTimeToFileTime ((BOOL(WINAPI*)(CONST SYSTEMTIME*, \ |
| 48085 | LPFILETIME))aSyscall[56].pCurrent) |
| 48086 | |
| 48087 | #if !SQLITE_OS_WINCE && !SQLITE_OS_WINRT |
| 48088 | { "UnlockFile", (SYSCALL)UnlockFile, 0 }, |
| 48089 | #else |
| 48090 | { "UnlockFile", (SYSCALL)0, 0 }, |
| 48091 | #endif |
| 48092 | |
| 48093 | #ifndef osUnlockFile |
| 48094 | #define osUnlockFile ((BOOL(WINAPI*)(HANDLE,DWORD,DWORD,DWORD, \ |
| 48095 | DWORD))aSyscall[57].pCurrent) |
| 48096 | #endif |
| 48097 | |
| 48098 | #if !SQLITE_OS_WINCE |
| @@ -48316,10 +48320,67 @@ | |
| 48316 | { "CancelIo", (SYSCALL)0, 0 }, |
| 48317 | #endif |
| 48318 | |
| 48319 | #define osCancelIo ((BOOL(WINAPI*)(HANDLE))aSyscall[81].pCurrent) |
| 48320 | |
| 48321 | }; /* End of the overrideable system calls */ |
| 48322 | |
| 48323 | /* |
| 48324 | ** This is the xSetSystemCall() method of sqlite3_vfs for all of the |
| 48325 | ** "win32" VFSes. Return SQLITE_OK upon successfully updating the |
| @@ -48489,10 +48550,11 @@ | |
| 48489 | sqlite3_mutex_leave(pMainMtx); |
| 48490 | return rc; |
| 48491 | } |
| 48492 | #endif /* SQLITE_WIN32_MALLOC */ |
| 48493 | |
| 48494 | /* |
| 48495 | ** This function outputs the specified (ANSI) string to the Win32 debugger |
| 48496 | ** (if available). |
| 48497 | */ |
| 48498 | |
| @@ -48531,10 +48593,11 @@ | |
| 48531 | }else{ |
| 48532 | fprintf(stderr, "%s", zBuf); |
| 48533 | } |
| 48534 | #endif |
| 48535 | } |
| 48536 | |
| 48537 | /* |
| 48538 | ** The following routine suspends the current thread for at least ms |
| 48539 | ** milliseconds. This is equivalent to the Win32 Sleep() interface. |
| 48540 | */ |
| @@ -48831,10 +48894,11 @@ | |
| 48831 | SQLITE_PRIVATE void sqlite3MemSetDefault(void){ |
| 48832 | sqlite3_config(SQLITE_CONFIG_MALLOC, sqlite3MemGetWin32()); |
| 48833 | } |
| 48834 | #endif /* SQLITE_WIN32_MALLOC */ |
| 48835 | |
| 48836 | /* |
| 48837 | ** Convert a UTF-8 string to Microsoft Unicode. |
| 48838 | ** |
| 48839 | ** Space to hold the returned string is obtained from sqlite3_malloc(). |
| 48840 | */ |
| @@ -48856,10 +48920,11 @@ | |
| 48856 | sqlite3_free(zWideText); |
| 48857 | zWideText = 0; |
| 48858 | } |
| 48859 | return zWideText; |
| 48860 | } |
| 48861 | |
| 48862 | /* |
| 48863 | ** Convert a Microsoft Unicode string to UTF-8. |
| 48864 | ** |
| 48865 | ** Space to hold the returned string is obtained from sqlite3_malloc(). |
| @@ -48890,32 +48955,33 @@ | |
| 48890 | ** code page. |
| 48891 | ** |
| 48892 | ** Space to hold the returned string is obtained from sqlite3_malloc(). |
| 48893 | */ |
| 48894 | static LPWSTR winMbcsToUnicode(const char *zText, int useAnsi){ |
| 48895 | int nByte; |
| 48896 | LPWSTR zMbcsText; |
| 48897 | int codepage = useAnsi ? CP_ACP : CP_OEMCP; |
| 48898 | |
| 48899 | nByte = osMultiByteToWideChar(codepage, 0, zText, -1, NULL, |
| 48900 | 0)*sizeof(WCHAR); |
| 48901 | if( nByte==0 ){ |
| 48902 | return 0; |
| 48903 | } |
| 48904 | zMbcsText = sqlite3MallocZero( nByte*sizeof(WCHAR) ); |
| 48905 | if( zMbcsText==0 ){ |
| 48906 | return 0; |
| 48907 | } |
| 48908 | nByte = osMultiByteToWideChar(codepage, 0, zText, -1, zMbcsText, |
| 48909 | nByte); |
| 48910 | if( nByte==0 ){ |
| 48911 | sqlite3_free(zMbcsText); |
| 48912 | zMbcsText = 0; |
| 48913 | } |
| 48914 | return zMbcsText; |
| 48915 | } |
| 48916 | |
| 48917 | /* |
| 48918 | ** Convert a Microsoft Unicode string to a multi-byte character string, |
| 48919 | ** using the ANSI or OEM code page. |
| 48920 | ** |
| 48921 | ** Space to hold the returned string is obtained from sqlite3_malloc(). |
| @@ -48939,10 +49005,11 @@ | |
| 48939 | sqlite3_free(zText); |
| 48940 | zText = 0; |
| 48941 | } |
| 48942 | return zText; |
| 48943 | } |
| 48944 | |
| 48945 | /* |
| 48946 | ** Convert a multi-byte character string to UTF-8. |
| 48947 | ** |
| 48948 | ** Space to hold the returned string is obtained from sqlite3_malloc(). |
| @@ -48958,10 +49025,11 @@ | |
| 48958 | zTextUtf8 = winUnicodeToUtf8(zTmpWide); |
| 48959 | sqlite3_free(zTmpWide); |
| 48960 | return zTextUtf8; |
| 48961 | } |
| 48962 | |
| 48963 | /* |
| 48964 | ** Convert a UTF-8 string to a multi-byte character string. |
| 48965 | ** |
| 48966 | ** Space to hold the returned string is obtained from sqlite3_malloc(). |
| 48967 | */ |
| @@ -49007,10 +49075,11 @@ | |
| 49007 | #ifndef SQLITE_OMIT_AUTOINIT |
| 49008 | if( sqlite3_initialize() ) return 0; |
| 49009 | #endif |
| 49010 | return winUnicodeToUtf8(zWideText); |
| 49011 | } |
| 49012 | |
| 49013 | /* |
| 49014 | ** This is a public wrapper for the winMbcsToUtf8() function. |
| 49015 | */ |
| 49016 | SQLITE_API char *sqlite3_win32_mbcs_to_utf8(const char *zText){ |
| @@ -49024,10 +49093,11 @@ | |
| 49024 | if( sqlite3_initialize() ) return 0; |
| 49025 | #endif |
| 49026 | return winMbcsToUtf8(zText, osAreFileApisANSI()); |
| 49027 | } |
| 49028 | |
| 49029 | /* |
| 49030 | ** This is a public wrapper for the winMbcsToUtf8() function. |
| 49031 | */ |
| 49032 | SQLITE_API char *sqlite3_win32_mbcs_to_utf8_v2(const char *zText, int useAnsi){ |
| 49033 | #ifdef SQLITE_ENABLE_API_ARMOR |
| @@ -49148,10 +49218,11 @@ | |
| 49148 | unsigned long type, /* Identifier for directory being set or reset */ |
| 49149 | void *zValue /* New value for directory being set or reset */ |
| 49150 | ){ |
| 49151 | return sqlite3_win32_set_directory16(type, zValue); |
| 49152 | } |
| 49153 | |
| 49154 | /* |
| 49155 | ** The return value of winGetLastErrorMsg |
| 49156 | ** is zero if the error message fits in the buffer, or non-zero |
| 49157 | ** otherwise (if the message was truncated). |
| @@ -49696,13 +49767,15 @@ | |
| 49696 | OVERLAPPED ovlp; |
| 49697 | memset(&ovlp, 0, sizeof(OVERLAPPED)); |
| 49698 | ovlp.Offset = offsetLow; |
| 49699 | ovlp.OffsetHigh = offsetHigh; |
| 49700 | return osLockFileEx(*phFile, flags, 0, numBytesLow, numBytesHigh, &ovlp); |
| 49701 | }else{ |
| 49702 | return osLockFile(*phFile, offsetLow, offsetHigh, numBytesLow, |
| 49703 | numBytesHigh); |
| 49704 | } |
| 49705 | #endif |
| 49706 | } |
| 49707 | |
| 49708 | /* |
| @@ -49806,13 +49879,15 @@ | |
| 49806 | OVERLAPPED ovlp; |
| 49807 | memset(&ovlp, 0, sizeof(OVERLAPPED)); |
| 49808 | ovlp.Offset = offsetLow; |
| 49809 | ovlp.OffsetHigh = offsetHigh; |
| 49810 | return osUnlockFileEx(*phFile, 0, numBytesLow, numBytesHigh, &ovlp); |
| 49811 | }else{ |
| 49812 | return osUnlockFile(*phFile, offsetLow, offsetHigh, numBytesLow, |
| 49813 | numBytesHigh); |
| 49814 | } |
| 49815 | #endif |
| 49816 | } |
| 49817 | |
| 49818 | /* |
| @@ -51222,18 +51297,95 @@ | |
| 51222 | |
| 51223 | /* |
| 51224 | ** Convert a UTF-8 filename into whatever form the underlying |
| 51225 | ** operating system wants filenames in. Space to hold the result |
| 51226 | ** is obtained from malloc and must be freed by the calling |
| 51227 | ** function. |
| 51228 | */ |
| 51229 | static void *winConvertFromUtf8Filename(const char *zFilename){ |
| 51230 | void *zConverted = 0; |
| 51231 | if( osIsNT() ){ |
| 51232 | zConverted = winUtf8ToUnicode(zFilename); |
| 51233 | } |
| 51234 | #ifdef SQLITE_WIN32_HAS_ANSI |
| 51235 | else{ |
| 51236 | zConverted = winUtf8ToMbcs(zFilename, osAreFileApisANSI()); |
| 51237 | } |
| 51238 | #endif |
| 51239 | /* caller will handle out of memory */ |
| @@ -52058,11 +52210,11 @@ | |
| 52058 | ** |
| 52059 | ** This division contains the implementation of methods on the |
| 52060 | ** sqlite3_vfs object. |
| 52061 | */ |
| 52062 | |
| 52063 | #if defined(__CYGWIN__) |
| 52064 | /* |
| 52065 | ** Convert a filename from whatever the underlying operating system |
| 52066 | ** supports for filenames into UTF-8. Space to hold the result is |
| 52067 | ** obtained from malloc and must be freed by the calling function. |
| 52068 | */ |
| @@ -52091,11 +52243,18 @@ | |
| 52091 | int nLen = sqlite3Strlen30(zBuf); |
| 52092 | if( nLen>0 ){ |
| 52093 | if( winIsDirSep(zBuf[nLen-1]) ){ |
| 52094 | return 1; |
| 52095 | }else if( nLen+1<nBuf ){ |
| 52096 | zBuf[nLen] = winGetDirSep(); |
| 52097 | zBuf[nLen+1] = '\0'; |
| 52098 | return 1; |
| 52099 | } |
| 52100 | } |
| 52101 | } |
| @@ -52118,11 +52277,11 @@ | |
| 52118 | /* |
| 52119 | ** Create a temporary file name and store the resulting pointer into pzBuf. |
| 52120 | ** The pointer returned in pzBuf must be freed via sqlite3_free(). |
| 52121 | */ |
| 52122 | static int winGetTempname(sqlite3_vfs *pVfs, char **pzBuf){ |
| 52123 | static char zChars[] = |
| 52124 | "abcdefghijklmnopqrstuvwxyz" |
| 52125 | "ABCDEFGHIJKLMNOPQRSTUVWXYZ" |
| 52126 | "0123456789"; |
| 52127 | size_t i, j; |
| 52128 | DWORD pid; |
| @@ -52169,11 +52328,11 @@ | |
| 52169 | } |
| 52170 | sqlite3_mutex_leave(sqlite3MutexAlloc(SQLITE_MUTEX_STATIC_TEMPDIR)); |
| 52171 | } |
| 52172 | |
| 52173 | #if defined(__CYGWIN__) |
| 52174 | else{ |
| 52175 | static const char *azDirs[] = { |
| 52176 | 0, /* getenv("SQLITE_TMPDIR") */ |
| 52177 | 0, /* getenv("TMPDIR") */ |
| 52178 | 0, /* getenv("TMP") */ |
| 52179 | 0, /* getenv("TEMP") */ |
| @@ -52185,24 +52344,24 @@ | |
| 52185 | 0 /* List terminator */ |
| 52186 | }; |
| 52187 | unsigned int i; |
| 52188 | const char *zDir = 0; |
| 52189 | |
| 52190 | if( !azDirs[0] ) azDirs[0] = getenv("SQLITE_TMPDIR"); |
| 52191 | if( !azDirs[1] ) azDirs[1] = getenv("TMPDIR"); |
| 52192 | if( !azDirs[2] ) azDirs[2] = getenv("TMP"); |
| 52193 | if( !azDirs[3] ) azDirs[3] = getenv("TEMP"); |
| 52194 | if( !azDirs[4] ) azDirs[4] = getenv("USERPROFILE"); |
| 52195 | for(i=0; i<sizeof(azDirs)/sizeof(azDirs[0]); zDir=azDirs[i++]){ |
| 52196 | void *zConverted; |
| 52197 | if( zDir==0 ) continue; |
| 52198 | /* If the path starts with a drive letter followed by the colon |
| 52199 | ** character, assume it is already a native Win32 path; otherwise, |
| 52200 | ** it must be converted to a native Win32 path via the Cygwin API |
| 52201 | ** prior to using it. |
| 52202 | */ |
| 52203 | if( winIsDriveLetterAndColon(zDir) ){ |
| 52204 | zConverted = winConvertFromUtf8Filename(zDir); |
| 52205 | if( !zConverted ){ |
| 52206 | sqlite3_free(zBuf); |
| 52207 | OSTRACE(("TEMP-FILENAME rc=SQLITE_IOERR_NOMEM\n")); |
| 52208 | return SQLITE_IOERR_NOMEM_BKPT; |
| @@ -52211,19 +52370,20 @@ | |
| 52211 | sqlite3_snprintf(nMax, zBuf, "%s", zDir); |
| 52212 | sqlite3_free(zConverted); |
| 52213 | break; |
| 52214 | } |
| 52215 | sqlite3_free(zConverted); |
| 52216 | }else{ |
| 52217 | zConverted = sqlite3MallocZero( nMax+1 ); |
| 52218 | if( !zConverted ){ |
| 52219 | sqlite3_free(zBuf); |
| 52220 | OSTRACE(("TEMP-FILENAME rc=SQLITE_IOERR_NOMEM\n")); |
| 52221 | return SQLITE_IOERR_NOMEM_BKPT; |
| 52222 | } |
| 52223 | if( cygwin_conv_path( |
| 52224 | osIsNT() ? CCP_POSIX_TO_WIN_W : CCP_POSIX_TO_WIN_A, zDir, |
| 52225 | zConverted, nMax+1)<0 ){ |
| 52226 | sqlite3_free(zConverted); |
| 52227 | sqlite3_free(zBuf); |
| 52228 | OSTRACE(("TEMP-FILENAME rc=SQLITE_IOERR_CONVPATH\n")); |
| 52229 | return winLogError(SQLITE_IOERR_CONVPATH, (DWORD)errno, |
| @@ -52245,14 +52405,17 @@ | |
| 52245 | sqlite3_free(zUtf8); |
| 52246 | sqlite3_free(zConverted); |
| 52247 | break; |
| 52248 | } |
| 52249 | sqlite3_free(zConverted); |
| 52250 | } |
| 52251 | } |
| 52252 | } |
| 52253 | #elif !SQLITE_OS_WINRT && !defined(__CYGWIN__) |
| 52254 | else if( osIsNT() ){ |
| 52255 | char *zMulti; |
| 52256 | LPWSTR zWidePath = sqlite3MallocZero( nMax*sizeof(WCHAR) ); |
| 52257 | if( !zWidePath ){ |
| 52258 | sqlite3_free(zBuf); |
| @@ -52372,11 +52535,11 @@ | |
| 52372 | &sAttrData)) && winRetryIoerr(&cnt, &lastErrno) ){} |
| 52373 | if( !rc ){ |
| 52374 | return 0; /* Invalid name? */ |
| 52375 | } |
| 52376 | attr = sAttrData.dwFileAttributes; |
| 52377 | #if SQLITE_OS_WINCE==0 |
| 52378 | }else{ |
| 52379 | attr = osGetFileAttributesA((char*)zConverted); |
| 52380 | #endif |
| 52381 | } |
| 52382 | return (attr!=INVALID_FILE_ATTRIBUTES) && (attr&FILE_ATTRIBUTE_DIRECTORY); |
| @@ -52388,10 +52551,16 @@ | |
| 52388 | const char *zFilename, /* Name of file to check */ |
| 52389 | int flags, /* Type of test to make on this file */ |
| 52390 | int *pResOut /* OUT: Result */ |
| 52391 | ); |
| 52392 | |
| 52393 | /* |
| 52394 | ** Open a file. |
| 52395 | */ |
| 52396 | static int winOpen( |
| 52397 | sqlite3_vfs *pVfs, /* Used to get maximum path length and AppData */ |
| @@ -52412,10 +52581,11 @@ | |
| 52412 | winVfsAppData *pAppData; |
| 52413 | winFile *pFile = (winFile*)id; |
| 52414 | void *zConverted; /* Filename in OS encoding */ |
| 52415 | const char *zUtf8Name = zName; /* Filename in UTF-8 encoding */ |
| 52416 | int cnt = 0; |
| 52417 | |
| 52418 | /* If argument zPath is a NULL pointer, this function is required to open |
| 52419 | ** a temporary file. Use this buffer to store the file name in. |
| 52420 | */ |
| 52421 | char *zTmpname = 0; /* For temporary filename, if necessary. */ |
| @@ -52576,13 +52746,13 @@ | |
| 52576 | dwShareMode, |
| 52577 | dwCreationDisposition, |
| 52578 | &extendedParameters); |
| 52579 | if( h!=INVALID_HANDLE_VALUE ) break; |
| 52580 | if( isReadWrite ){ |
| 52581 | int rc2, isRO = 0; |
| 52582 | sqlite3BeginBenignMalloc(); |
| 52583 | rc2 = winAccess(pVfs, zUtf8Name, SQLITE_ACCESS_READ, &isRO); |
| 52584 | sqlite3EndBenignMalloc(); |
| 52585 | if( rc2==SQLITE_OK && isRO ) break; |
| 52586 | } |
| 52587 | }while( winRetryIoerr(&cnt, &lastErrno) ); |
| 52588 | #else |
| @@ -52593,13 +52763,13 @@ | |
| 52593 | dwCreationDisposition, |
| 52594 | dwFlagsAndAttributes, |
| 52595 | NULL); |
| 52596 | if( h!=INVALID_HANDLE_VALUE ) break; |
| 52597 | if( isReadWrite ){ |
| 52598 | int rc2, isRO = 0; |
| 52599 | sqlite3BeginBenignMalloc(); |
| 52600 | rc2 = winAccess(pVfs, zUtf8Name, SQLITE_ACCESS_READ, &isRO); |
| 52601 | sqlite3EndBenignMalloc(); |
| 52602 | if( rc2==SQLITE_OK && isRO ) break; |
| 52603 | } |
| 52604 | }while( winRetryIoerr(&cnt, &lastErrno) ); |
| 52605 | #endif |
| @@ -52613,13 +52783,13 @@ | |
| 52613 | dwCreationDisposition, |
| 52614 | dwFlagsAndAttributes, |
| 52615 | NULL); |
| 52616 | if( h!=INVALID_HANDLE_VALUE ) break; |
| 52617 | if( isReadWrite ){ |
| 52618 | int rc2, isRO = 0; |
| 52619 | sqlite3BeginBenignMalloc(); |
| 52620 | rc2 = winAccess(pVfs, zUtf8Name, SQLITE_ACCESS_READ, &isRO); |
| 52621 | sqlite3EndBenignMalloc(); |
| 52622 | if( rc2==SQLITE_OK && isRO ) break; |
| 52623 | } |
| 52624 | }while( winRetryIoerr(&cnt, &lastErrno) ); |
| 52625 | } |
| @@ -52630,11 +52800,11 @@ | |
| 52630 | dwDesiredAccess, (h==INVALID_HANDLE_VALUE) ? "failed" : "ok")); |
| 52631 | |
| 52632 | if( h==INVALID_HANDLE_VALUE ){ |
| 52633 | sqlite3_free(zConverted); |
| 52634 | sqlite3_free(zTmpname); |
| 52635 | if( isReadWrite && !isExclusive ){ |
| 52636 | return winOpen(pVfs, zName, id, |
| 52637 | ((flags|SQLITE_OPEN_READONLY) & |
| 52638 | ~(SQLITE_OPEN_CREATE|SQLITE_OPEN_READWRITE)), |
| 52639 | pOutFlags); |
| 52640 | }else{ |
| @@ -52832,11 +53002,17 @@ | |
| 52832 | ){ |
| 52833 | DWORD attr; |
| 52834 | int rc = 0; |
| 52835 | DWORD lastErrno = 0; |
| 52836 | void *zConverted; |
| 52837 | UNUSED_PARAMETER(pVfs); |
| 52838 | |
| 52839 | SimulateIOError( return SQLITE_IOERR_ACCESS; ); |
| 52840 | OSTRACE(("ACCESS name=%s, flags=%x, pResOut=%p\n", |
| 52841 | zFilename, flags, pResOut)); |
| 52842 | |
| @@ -52856,11 +53032,14 @@ | |
| 52856 | int cnt = 0; |
| 52857 | WIN32_FILE_ATTRIBUTE_DATA sAttrData; |
| 52858 | memset(&sAttrData, 0, sizeof(sAttrData)); |
| 52859 | while( !(rc = osGetFileAttributesExW((LPCWSTR)zConverted, |
| 52860 | GetFileExInfoStandard, |
| 52861 | &sAttrData)) && winRetryIoerr(&cnt, &lastErrno) ){} |
| 52862 | if( rc ){ |
| 52863 | /* For an SQLITE_ACCESS_EXISTS query, treat a zero-length file |
| 52864 | ** as if it does not exist. |
| 52865 | */ |
| 52866 | if( flags==SQLITE_ACCESS_EXISTS |
| @@ -52924,10 +53103,11 @@ | |
| 52924 | const char *zPathname |
| 52925 | ){ |
| 52926 | return ( sqlite3Isalpha(zPathname[0]) && zPathname[1]==':' ); |
| 52927 | } |
| 52928 | |
| 52929 | /* |
| 52930 | ** Returns non-zero if the specified path name should be used verbatim. If |
| 52931 | ** non-zero is returned from this function, the calling function must simply |
| 52932 | ** use the provided path name verbatim -OR- resolve it into a full path name |
| 52933 | ** using the GetFullPathName Win32 API function (if available). |
| @@ -52960,10 +53140,74 @@ | |
| 52960 | ** If we get to this point, the path name should almost certainly be a purely |
| 52961 | ** relative one (i.e. not a UNC name, not absolute, and not volume relative). |
| 52962 | */ |
| 52963 | return FALSE; |
| 52964 | } |
| 52965 | |
| 52966 | /* |
| 52967 | ** Turn a relative pathname into a full pathname. Write the full |
| 52968 | ** pathname into zOut[]. zOut[] will be at least pVfs->mxPathname |
| 52969 | ** bytes in size. |
| @@ -52972,12 +53216,12 @@ | |
| 52972 | sqlite3_vfs *pVfs, /* Pointer to vfs object */ |
| 52973 | const char *zRelative, /* Possibly relative input path */ |
| 52974 | int nFull, /* Size of output buffer in bytes */ |
| 52975 | char *zFull /* Output buffer */ |
| 52976 | ){ |
| 52977 | #if !SQLITE_OS_WINCE && !SQLITE_OS_WINRT && !defined(__CYGWIN__) |
| 52978 | DWORD nByte; |
| 52979 | void *zConverted; |
| 52980 | char *zOut; |
| 52981 | #endif |
| 52982 | |
| 52983 | /* If this path name begins with "/X:" or "\\?\", where "X" is any |
| @@ -52986,68 +53230,114 @@ | |
| 52986 | if( zRelative[0]=='/' && (winIsDriveLetterAndColon(zRelative+1) |
| 52987 | || winIsLongPathPrefix(zRelative+1)) ){ |
| 52988 | zRelative++; |
| 52989 | } |
| 52990 | |
| 52991 | #if defined(__CYGWIN__) |
| 52992 | SimulateIOError( return SQLITE_ERROR ); |
| 52993 | UNUSED_PARAMETER(nFull); |
| 52994 | assert( nFull>=pVfs->mxPathname ); |
| 52995 | if ( sqlite3_data_directory && !winIsVerbatimPathname(zRelative) ){ |
| 52996 | /* |
| 52997 | ** NOTE: We are dealing with a relative path name and the data |
| 52998 | ** directory has been set. Therefore, use it as the basis |
| 52999 | ** for converting the relative path name to an absolute |
| 53000 | ** one by prepending the data directory and a slash. |
| 53001 | */ |
| 53002 | char *zOut = sqlite3MallocZero( 1+(u64)pVfs->mxPathname ); |
| 53003 | if( !zOut ){ |
| 53004 | return SQLITE_IOERR_NOMEM_BKPT; |
| 53005 | } |
| 53006 | if( cygwin_conv_path( |
| 53007 | (osIsNT() ? CCP_POSIX_TO_WIN_W : CCP_POSIX_TO_WIN_A) | |
| 53008 | CCP_RELATIVE, zRelative, zOut, pVfs->mxPathname+1)<0 ){ |
| 53009 | sqlite3_free(zOut); |
| 53010 | return winLogError(SQLITE_CANTOPEN_CONVPATH, (DWORD)errno, |
| 53011 | "winFullPathname1", zRelative); |
| 53012 | }else{ |
| 53013 | char *zUtf8 = winConvertToUtf8Filename(zOut); |
| 53014 | if( !zUtf8 ){ |
| 53015 | sqlite3_free(zOut); |
| 53016 | return SQLITE_IOERR_NOMEM_BKPT; |
| 53017 | } |
| 53018 | sqlite3_snprintf(MIN(nFull, pVfs->mxPathname), zFull, "%s%c%s", |
| 53019 | sqlite3_data_directory, winGetDirSep(), zUtf8); |
| 53020 | sqlite3_free(zUtf8); |
| 53021 | sqlite3_free(zOut); |
| 53022 | } |
| 53023 | }else{ |
| 53024 | char *zOut = sqlite3MallocZero( pVfs->mxPathname+1 ); |
| 53025 | if( !zOut ){ |
| 53026 | return SQLITE_IOERR_NOMEM_BKPT; |
| 53027 | } |
| 53028 | if( cygwin_conv_path( |
| 53029 | (osIsNT() ? CCP_POSIX_TO_WIN_W : CCP_POSIX_TO_WIN_A), |
| 53030 | zRelative, zOut, pVfs->mxPathname+1)<0 ){ |
| 53031 | sqlite3_free(zOut); |
| 53032 | return winLogError(SQLITE_CANTOPEN_CONVPATH, (DWORD)errno, |
| 53033 | "winFullPathname2", zRelative); |
| 53034 | }else{ |
| 53035 | char *zUtf8 = winConvertToUtf8Filename(zOut); |
| 53036 | if( !zUtf8 ){ |
| 53037 | sqlite3_free(zOut); |
| 53038 | return SQLITE_IOERR_NOMEM_BKPT; |
| 53039 | } |
| 53040 | sqlite3_snprintf(MIN(nFull, pVfs->mxPathname), zFull, "%s", zUtf8); |
| 53041 | sqlite3_free(zUtf8); |
| 53042 | sqlite3_free(zOut); |
| 53043 | } |
| 53044 | } |
| 53045 | return SQLITE_OK; |
| 53046 | #endif |
| 53047 | |
| 53048 | #if (SQLITE_OS_WINCE || SQLITE_OS_WINRT) && !defined(__CYGWIN__) |
| 53049 | SimulateIOError( return SQLITE_ERROR ); |
| 53050 | /* WinCE has no concept of a relative pathname, or so I am told. */ |
| 53051 | /* WinRT has no way to convert a relative path to an absolute one. */ |
| 53052 | if ( sqlite3_data_directory && !winIsVerbatimPathname(zRelative) ){ |
| 53053 | /* |
| @@ -53062,11 +53352,12 @@ | |
| 53062 | sqlite3_snprintf(MIN(nFull, pVfs->mxPathname), zFull, "%s", zRelative); |
| 53063 | } |
| 53064 | return SQLITE_OK; |
| 53065 | #endif |
| 53066 | |
| 53067 | #if !SQLITE_OS_WINCE && !SQLITE_OS_WINRT && !defined(__CYGWIN__) |
| 53068 | /* It's odd to simulate an io-error here, but really this is just |
| 53069 | ** using the io-error infrastructure to test that SQLite handles this |
| 53070 | ** function failing. This function could fail if, for example, the |
| 53071 | ** current working directory has been unlinked. |
| 53072 | */ |
| @@ -53080,10 +53371,11 @@ | |
| 53080 | */ |
| 53081 | sqlite3_snprintf(MIN(nFull, pVfs->mxPathname), zFull, "%s%c%s", |
| 53082 | sqlite3_data_directory, winGetDirSep(), zRelative); |
| 53083 | return SQLITE_OK; |
| 53084 | } |
| 53085 | zConverted = winConvertFromUtf8Filename(zRelative); |
| 53086 | if( zConverted==0 ){ |
| 53087 | return SQLITE_IOERR_NOMEM_BKPT; |
| 53088 | } |
| 53089 | if( osIsNT() ){ |
| @@ -53092,16 +53384,17 @@ | |
| 53092 | if( nByte==0 ){ |
| 53093 | sqlite3_free(zConverted); |
| 53094 | return winLogError(SQLITE_CANTOPEN_FULLPATH, osGetLastError(), |
| 53095 | "winFullPathname1", zRelative); |
| 53096 | } |
| 53097 | zTemp = sqlite3MallocZero( nByte*sizeof(zTemp[0]) + 3*sizeof(zTemp[0]) ); |
| 53098 | if( zTemp==0 ){ |
| 53099 | sqlite3_free(zConverted); |
| 53100 | return SQLITE_IOERR_NOMEM_BKPT; |
| 53101 | } |
| 53102 | nByte = osGetFullPathNameW((LPCWSTR)zConverted, nByte+3, zTemp, 0); |
| 53103 | if( nByte==0 ){ |
| 53104 | sqlite3_free(zConverted); |
| 53105 | sqlite3_free(zTemp); |
| 53106 | return winLogError(SQLITE_CANTOPEN_FULLPATH, osGetLastError(), |
| 53107 | "winFullPathname2", zRelative); |
| @@ -53135,11 +53428,30 @@ | |
| 53135 | zOut = winMbcsToUtf8(zTemp, osAreFileApisANSI()); |
| 53136 | sqlite3_free(zTemp); |
| 53137 | } |
| 53138 | #endif |
| 53139 | if( zOut ){ |
| 53140 | sqlite3_snprintf(MIN(nFull, pVfs->mxPathname), zFull, "%s", zOut); |
| 53141 | sqlite3_free(zOut); |
| 53142 | return SQLITE_OK; |
| 53143 | }else{ |
| 53144 | return SQLITE_IOERR_NOMEM_BKPT; |
| 53145 | } |
| @@ -53165,11 +53477,13 @@ | |
| 53165 | ** Interfaces for opening a shared library, finding entry points |
| 53166 | ** within the shared library, and closing the shared library. |
| 53167 | */ |
| 53168 | static void *winDlOpen(sqlite3_vfs *pVfs, const char *zFilename){ |
| 53169 | HANDLE h; |
| 53170 | #if defined(__CYGWIN__) |
| 53171 | int nFull = pVfs->mxPathname+1; |
| 53172 | char *zFull = sqlite3MallocZero( nFull ); |
| 53173 | void *zConverted = 0; |
| 53174 | if( zFull==0 ){ |
| 53175 | OSTRACE(("DLOPEN name=%s, handle=%p\n", zFilename, (void*)0)); |
| @@ -53532,11 +53846,11 @@ | |
| 53532 | }; |
| 53533 | #endif |
| 53534 | |
| 53535 | /* Double-check that the aSyscall[] array has been constructed |
| 53536 | ** correctly. See ticket [bb3a86e890c8e96ab] */ |
| 53537 | assert( ArraySize(aSyscall)==82 ); |
| 53538 | |
| 53539 | /* get memory map allocation granularity */ |
| 53540 | memset(&winSysInfo, 0, sizeof(SYSTEM_INFO)); |
| 53541 | #if SQLITE_OS_WINRT |
| 53542 | osGetNativeSystemInfo(&winSysInfo); |
| @@ -66508,14 +66822,12 @@ | |
| 66508 | s2 = aIn[1]; |
| 66509 | }else{ |
| 66510 | s1 = s2 = 0; |
| 66511 | } |
| 66512 | |
| 66513 | assert( nByte>=8 ); |
| 66514 | assert( (nByte&0x00000007)==0 ); |
| 66515 | assert( nByte<=65536 ); |
| 66516 | assert( nByte%4==0 ); |
| 66517 | |
| 66518 | if( !nativeCksum ){ |
| 66519 | do { |
| 66520 | s1 += BYTESWAP32(aData[0]) + s2; |
| 66521 | s2 += BYTESWAP32(aData[1]) + s1; |
| @@ -83726,11 +84038,11 @@ | |
| 83726 | ** many different strings can be converted into the same int or real. |
| 83727 | ** If a table contains a numeric value and an index is based on the |
| 83728 | ** corresponding string value, then it is important that the string be |
| 83729 | ** derived from the numeric value, not the other way around, to ensure |
| 83730 | ** that the index and table are consistent. See ticket |
| 83731 | ** https://www.sqlite.org/src/info/343634942dd54ab (2018-01-31) for |
| 83732 | ** an example. |
| 83733 | ** |
| 83734 | ** This routine looks at pMem to verify that if it has both a numeric |
| 83735 | ** representation and a string representation then the string rep has |
| 83736 | ** been derived from the numeric and not the other way around. It returns |
| @@ -93014,11 +93326,11 @@ | |
| 93014 | unsigned char enc |
| 93015 | ){ |
| 93016 | assert( xDel!=SQLITE_DYNAMIC ); |
| 93017 | if( enc!=SQLITE_UTF8 ){ |
| 93018 | if( enc==SQLITE_UTF16 ) enc = SQLITE_UTF16NATIVE; |
| 93019 | nData &= ~(u16)1; |
| 93020 | } |
| 93021 | return bindText(pStmt, i, zData, nData, xDel, enc); |
| 93022 | } |
| 93023 | #ifndef SQLITE_OMIT_UTF16 |
| 93024 | SQLITE_API int sqlite3_bind_text16( |
| @@ -125166,11 +125478,11 @@ | |
| 125166 | if( !isDupColumn(pIdx, pIdx->nKeyCol, pPk, i) ){ |
| 125167 | testcase( hasColumn(pIdx->aiColumn, pIdx->nKeyCol, pPk->aiColumn[i]) ); |
| 125168 | pIdx->aiColumn[j] = pPk->aiColumn[i]; |
| 125169 | pIdx->azColl[j] = pPk->azColl[i]; |
| 125170 | if( pPk->aSortOrder[i] ){ |
| 125171 | /* See ticket https://www.sqlite.org/src/info/bba7b69f9849b5bf */ |
| 125172 | pIdx->bAscKeyBug = 1; |
| 125173 | } |
| 125174 | j++; |
| 125175 | } |
| 125176 | } |
| @@ -126543,11 +126855,11 @@ | |
| 126543 | /* This OP_SeekEnd opcode makes index insert for a REINDEX go much |
| 126544 | ** faster by avoiding unnecessary seeks. But the optimization does |
| 126545 | ** not work for UNIQUE constraint indexes on WITHOUT ROWID tables |
| 126546 | ** with DESC primary keys, since those indexes have there keys in |
| 126547 | ** a different order from the main table. |
| 126548 | ** See ticket: https://www.sqlite.org/src/info/bba7b69f9849b5bf |
| 126549 | */ |
| 126550 | sqlite3VdbeAddOp1(v, OP_SeekEnd, iIdx); |
| 126551 | } |
| 126552 | sqlite3VdbeAddOp2(v, OP_IdxInsert, iIdx, regRecord); |
| 126553 | sqlite3VdbeChangeP5(v, OPFLAG_USESEEKRESULT); |
| @@ -126927,10 +127239,11 @@ | |
| 126927 | }else{ |
| 126928 | j = pCExpr->iColumn; |
| 126929 | assert( j<=0x7fff ); |
| 126930 | if( j<0 ){ |
| 126931 | j = pTab->iPKey; |
| 126932 | }else{ |
| 126933 | if( pTab->aCol[j].notNull==0 ){ |
| 126934 | pIndex->uniqNotNull = 0; |
| 126935 | } |
| 126936 | if( pTab->aCol[j].colFlags & COLFLAG_VIRTUAL ){ |
| @@ -136632,11 +136945,11 @@ | |
| 136632 | ** OE_Update guarantees that only a single row will change, so it |
| 136633 | ** must happen before OE_Replace. Technically, OE_Abort and OE_Rollback |
| 136634 | ** could happen in any order, but they are grouped up front for |
| 136635 | ** convenience. |
| 136636 | ** |
| 136637 | ** 2018-08-14: Ticket https://www.sqlite.org/src/info/908f001483982c43 |
| 136638 | ** The order of constraints used to have OE_Update as (2) and OE_Abort |
| 136639 | ** and so forth as (1). But apparently PostgreSQL checks the OE_Update |
| 136640 | ** constraint before any others, so it had to be moved. |
| 136641 | ** |
| 136642 | ** Constraint checking code is generated in this order: |
| @@ -147785,10 +148098,11 @@ | |
| 147785 | } |
| 147786 | |
| 147787 | multi_select_end: |
| 147788 | pDest->iSdst = dest.iSdst; |
| 147789 | pDest->nSdst = dest.nSdst; |
| 147790 | if( pDelete ){ |
| 147791 | sqlite3ParserAddCleanup(pParse, sqlite3SelectDeleteGeneric, pDelete); |
| 147792 | } |
| 147793 | return rc; |
| 147794 | } |
| @@ -149395,11 +149709,12 @@ | |
| 149395 | && pE2->iColumn==pColumn->iColumn |
| 149396 | ){ |
| 149397 | return; /* Already present. Return without doing anything. */ |
| 149398 | } |
| 149399 | } |
| 149400 | if( sqlite3ExprAffinity(pColumn)==SQLITE_AFF_BLOB ){ |
| 149401 | pConst->bHasAffBlob = 1; |
| 149402 | } |
| 149403 | |
| 149404 | pConst->nConst++; |
| 149405 | pConst->apExpr = sqlite3DbReallocOrFree(pConst->pParse->db, pConst->apExpr, |
| @@ -149470,11 +149785,12 @@ | |
| 149470 | for(i=0; i<pConst->nConst; i++){ |
| 149471 | Expr *pColumn = pConst->apExpr[i*2]; |
| 149472 | if( pColumn==pExpr ) continue; |
| 149473 | if( pColumn->iTable!=pExpr->iTable ) continue; |
| 149474 | if( pColumn->iColumn!=pExpr->iColumn ) continue; |
| 149475 | if( bIgnoreAffBlob && sqlite3ExprAffinity(pColumn)==SQLITE_AFF_BLOB ){ |
| 149476 | break; |
| 149477 | } |
| 149478 | /* A match is found. Add the EP_FixedCol property */ |
| 149479 | pConst->nChng++; |
| 149480 | ExprClearProperty(pExpr, EP_Leaf); |
| @@ -150123,11 +150439,11 @@ | |
| 150123 | ** |
| 150124 | ** This transformation is necessary because the multiSelectOrderBy() routine |
| 150125 | ** above that generates the code for a compound SELECT with an ORDER BY clause |
| 150126 | ** uses a merge algorithm that requires the same collating sequence on the |
| 150127 | ** result columns as on the ORDER BY clause. See ticket |
| 150128 | ** http://www.sqlite.org/src/info/6709574d2a |
| 150129 | ** |
| 150130 | ** This transformation is only needed for EXCEPT, INTERSECT, and UNION. |
| 150131 | ** The UNION ALL operator works fine with multiSelectOrderBy() even when |
| 150132 | ** there are COLLATE terms in the ORDER BY. |
| 150133 | */ |
| @@ -152654,10 +152970,16 @@ | |
| 152654 | pWInfo = sqlite3WhereBegin(pParse, pTabList, pWhere, sSort.pOrderBy, |
| 152655 | p->pEList, p, wctrlFlags, p->nSelectRow); |
| 152656 | if( pWInfo==0 ) goto select_end; |
| 152657 | if( sqlite3WhereOutputRowCount(pWInfo) < p->nSelectRow ){ |
| 152658 | p->nSelectRow = sqlite3WhereOutputRowCount(pWInfo); |
| 152659 | } |
| 152660 | if( sDistinct.isTnct && sqlite3WhereIsDistinct(pWInfo) ){ |
| 152661 | sDistinct.eTnctType = sqlite3WhereIsDistinct(pWInfo); |
| 152662 | } |
| 152663 | if( sSort.pOrderBy ){ |
| @@ -156925,11 +157247,11 @@ | |
| 156925 | iDb = sqlite3TwoPartName(pParse, pNm, pNm, &pNm); |
| 156926 | if( iDb<0 ) goto build_vacuum_end; |
| 156927 | #else |
| 156928 | /* When SQLITE_BUG_COMPATIBLE_20160819 is defined, unrecognized arguments |
| 156929 | ** to VACUUM are silently ignored. This is a back-out of a bug fix that |
| 156930 | ** occurred on 2016-08-19 (https://www.sqlite.org/src/info/083f9e6270). |
| 156931 | ** The buggy behavior is required for binary compatibility with some |
| 156932 | ** legacy applications. */ |
| 156933 | iDb = sqlite3FindDb(pParse->db, pNm); |
| 156934 | if( iDb<0 ) iDb = 0; |
| 156935 | #endif |
| @@ -159820,11 +160142,11 @@ | |
| 159820 | |
| 159821 | |
| 159822 | /* |
| 159823 | ** pX is an expression of the form: (vector) IN (SELECT ...) |
| 159824 | ** In other words, it is a vector IN operator with a SELECT clause on the |
| 159825 | ** LHS. But not all terms in the vector are indexable and the terms might |
| 159826 | ** not be in the correct order for indexing. |
| 159827 | ** |
| 159828 | ** This routine makes a copy of the input pX expression and then adjusts |
| 159829 | ** the vector on the LHS with corresponding changes to the SELECT so that |
| 159830 | ** the vector contains only index terms and those terms are in the correct |
| @@ -161642,11 +161964,11 @@ | |
| 161642 | ** ON or USING clause of a LEFT JOIN, and terms that are usable as |
| 161643 | ** indices. |
| 161644 | ** |
| 161645 | ** This optimization also only applies if the (x1 OR x2 OR ...) term |
| 161646 | ** is not contained in the ON clause of a LEFT JOIN. |
| 161647 | ** See ticket http://www.sqlite.org/src/info/f2369304e4 |
| 161648 | ** |
| 161649 | ** 2022-02-04: Do not push down slices of a row-value comparison. |
| 161650 | ** In other words, "w" or "y" may not be a slice of a vector. Otherwise, |
| 161651 | ** the initialization of the right-hand operand of the vector comparison |
| 161652 | ** might not occur, or might occur only in an OR branch that is not |
| @@ -167615,11 +167937,11 @@ | |
| 167615 | } |
| 167616 | |
| 167617 | if( (pNew->wsFlags & WHERE_TOP_LIMIT)==0 |
| 167618 | && pNew->u.btree.nEq<pProbe->nColumn |
| 167619 | && (pNew->u.btree.nEq<pProbe->nKeyCol || |
| 167620 | pProbe->idxType!=SQLITE_IDXTYPE_PRIMARYKEY) |
| 167621 | ){ |
| 167622 | if( pNew->u.btree.nEq>3 ){ |
| 167623 | sqlite3ProgressCheck(pParse); |
| 167624 | } |
| 167625 | whereLoopAddBtreeIndex(pBuilder, pSrc, pProbe, nInMul+nIn); |
| @@ -171073,11 +171395,12 @@ | |
| 171073 | wherePathSolver(pWInfo, pWInfo->nRowOut<0 ? 1 : pWInfo->nRowOut+1); |
| 171074 | if( db->mallocFailed ) goto whereBeginError; |
| 171075 | } |
| 171076 | |
| 171077 | /* TUNING: Assume that a DISTINCT clause on a subquery reduces |
| 171078 | ** the output size by a factor of 8 (LogEst -30). |
| 171079 | */ |
| 171080 | if( (pWInfo->wctrlFlags & WHERE_WANT_DISTINCT)!=0 ){ |
| 171081 | WHERETRACE(0x0080,("nRowOut reduced from %d to %d due to DISTINCT\n", |
| 171082 | pWInfo->nRowOut, pWInfo->nRowOut-30)); |
| 171083 | pWInfo->nRowOut -= 30; |
| @@ -188065,10 +188388,17 @@ | |
| 188065 | ****************************************************************************** |
| 188066 | ** |
| 188067 | */ |
| 188068 | #ifndef _FTSINT_H |
| 188069 | #define _FTSINT_H |
| 188070 | |
| 188071 | #if !defined(NDEBUG) && !defined(SQLITE_DEBUG) |
| 188072 | # define NDEBUG 1 |
| 188073 | #endif |
| 188074 | |
| @@ -189017,16 +189347,10 @@ | |
| 189017 | |
| 189018 | #if defined(SQLITE_ENABLE_FTS3) && !defined(SQLITE_CORE) |
| 189019 | # define SQLITE_CORE 1 |
| 189020 | #endif |
| 189021 | |
| 189022 | /* #include <assert.h> */ |
| 189023 | /* #include <stdlib.h> */ |
| 189024 | /* #include <stddef.h> */ |
| 189025 | /* #include <stdio.h> */ |
| 189026 | /* #include <string.h> */ |
| 189027 | /* #include <stdarg.h> */ |
| 189028 | |
| 189029 | /* #include "fts3.h" */ |
| 189030 | #ifndef SQLITE_CORE |
| 189031 | /* # include "sqlite3ext.h" */ |
| 189032 | SQLITE_EXTENSION_INIT1 |
| @@ -198969,11 +199293,11 @@ | |
| 198969 | UNUSED_PARAMETER(nVal); |
| 198970 | |
| 198971 | fts3tokResetCursor(pCsr); |
| 198972 | if( idxNum==1 ){ |
| 198973 | const char *zByte = (const char *)sqlite3_value_text(apVal[0]); |
| 198974 | int nByte = sqlite3_value_bytes(apVal[0]); |
| 198975 | pCsr->zInput = sqlite3_malloc64(nByte+1); |
| 198976 | if( pCsr->zInput==0 ){ |
| 198977 | rc = SQLITE_NOMEM; |
| 198978 | }else{ |
| 198979 | if( nByte>0 ) memcpy(pCsr->zInput, zByte, nByte); |
| @@ -205949,20 +206273,20 @@ | |
| 205949 | case FTS3_MATCHINFO_LCS: |
| 205950 | nVal = pInfo->nCol; |
| 205951 | break; |
| 205952 | |
| 205953 | case FTS3_MATCHINFO_LHITS: |
| 205954 | nVal = pInfo->nCol * pInfo->nPhrase; |
| 205955 | break; |
| 205956 | |
| 205957 | case FTS3_MATCHINFO_LHITS_BM: |
| 205958 | nVal = pInfo->nPhrase * ((pInfo->nCol + 31) / 32); |
| 205959 | break; |
| 205960 | |
| 205961 | default: |
| 205962 | assert( cArg==FTS3_MATCHINFO_HITS ); |
| 205963 | nVal = pInfo->nCol * pInfo->nPhrase * 3; |
| 205964 | break; |
| 205965 | } |
| 205966 | |
| 205967 | return nVal; |
| 205968 | } |
| @@ -207516,12 +207840,12 @@ | |
| 207516 | ** with JSON-5 extensions is accepted as input. |
| 207517 | ** |
| 207518 | ** Beginning with version 3.45.0 (circa 2024-01-01), these routines also |
| 207519 | ** accept BLOB values that have JSON encoded using a binary representation |
| 207520 | ** called "JSONB". The name JSONB comes from PostgreSQL, however the on-disk |
| 207521 | ** format SQLite JSONB is completely different and incompatible with |
| 207522 | ** PostgreSQL JSONB. |
| 207523 | ** |
| 207524 | ** Decoding and interpreting JSONB is still O(N) where N is the size of |
| 207525 | ** the input, the same as text JSON. However, the constant of proportionality |
| 207526 | ** for JSONB is much smaller due to faster parsing. The size of each |
| 207527 | ** element in JSONB is encoded in its header, so there is no need to search |
| @@ -207574,21 +207898,21 @@ | |
| 207574 | ** 14 4 byte (0-4294967295) 5 |
| 207575 | ** 15 8 byte (0-1.8e19) 9 |
| 207576 | ** |
| 207577 | ** The payload size need not be expressed in its minimal form. For example, |
| 207578 | ** if the payload size is 10, the size can be expressed in any of 5 different |
| 207579 | ** ways: (1) (X>>4)==10, (2) (X>>4)==12 following by on 0x0a byte, |
| 207580 | ** (3) (X>>4)==13 followed by 0x00 and 0x0a, (4) (X>>4)==14 followed by |
| 207581 | ** 0x00 0x00 0x00 0x0a, or (5) (X>>4)==15 followed by 7 bytes of 0x00 and |
| 207582 | ** a single byte of 0x0a. The shorter forms are preferred, of course, but |
| 207583 | ** sometimes when generating JSONB, the payload size is not known in advance |
| 207584 | ** and it is convenient to reserve sufficient header space to cover the |
| 207585 | ** largest possible payload size and then come back later and patch up |
| 207586 | ** the size when it becomes known, resulting in a non-minimal encoding. |
| 207587 | ** |
| 207588 | ** The value (X>>4)==15 is not actually used in the current implementation |
| 207589 | ** (as SQLite is currently unable handle BLOBs larger than about 2GB) |
| 207590 | ** but is included in the design to allow for future enhancements. |
| 207591 | ** |
| 207592 | ** The payload follows the header. NULL, TRUE, and FALSE have no payload and |
| 207593 | ** their payload size must always be zero. The payload for INT, INT5, |
| 207594 | ** FLOAT, FLOAT5, TEXT, TEXTJ, TEXT5, and TEXTROW is text. Note that the |
| @@ -208658,11 +208982,11 @@ | |
| 208658 | if( jsonBlobExpand(pParse, pParse->nBlob+szPayload+9) ) return; |
| 208659 | jsonBlobAppendNode(pParse, eType, szPayload, aPayload); |
| 208660 | } |
| 208661 | |
| 208662 | |
| 208663 | /* Append an node type byte together with the payload size and |
| 208664 | ** possibly also the payload. |
| 208665 | ** |
| 208666 | ** If aPayload is not NULL, then it is a pointer to the payload which |
| 208667 | ** is also appended. If aPayload is NULL, the pParse->aBlob[] array |
| 208668 | ** is resized (if necessary) so that it is big enough to hold the |
| @@ -209992,10 +210316,86 @@ | |
| 209992 | (void)jsonbPayloadSize(pParse, iRoot, &sz); |
| 209993 | pParse->nBlob = nBlob; |
| 209994 | sz += pParse->delta; |
| 209995 | pParse->delta += jsonBlobChangePayloadSize(pParse, iRoot, sz); |
| 209996 | } |
| 209997 | |
| 209998 | /* |
| 209999 | ** Modify the JSONB blob at pParse->aBlob by removing nDel bytes of |
| 210000 | ** content beginning at iDel, and replacing them with nIns bytes of |
| 210001 | ** content given by aIns. |
| @@ -210014,10 +210414,15 @@ | |
| 210014 | u32 nDel, /* Number of bytes to remove */ |
| 210015 | const u8 *aIns, /* Content to insert */ |
| 210016 | u32 nIns /* Bytes of content to insert */ |
| 210017 | ){ |
| 210018 | i64 d = (i64)nIns - (i64)nDel; |
| 210019 | if( d!=0 ){ |
| 210020 | if( pParse->nBlob + d > pParse->nBlobAlloc ){ |
| 210021 | jsonBlobExpand(pParse, pParse->nBlob+d); |
| 210022 | if( pParse->oom ) return; |
| 210023 | } |
| @@ -210025,11 +210430,13 @@ | |
| 210025 | &pParse->aBlob[iDel+nDel], |
| 210026 | pParse->nBlob - (iDel+nDel)); |
| 210027 | pParse->nBlob += d; |
| 210028 | pParse->delta += d; |
| 210029 | } |
| 210030 | if( nIns && aIns ) memcpy(&pParse->aBlob[iDel], aIns, nIns); |
| 210031 | } |
| 210032 | |
| 210033 | /* |
| 210034 | ** Return the number of escaped newlines to be ignored. |
| 210035 | ** An escaped newline is a one of the following byte sequences: |
| @@ -210788,11 +211195,11 @@ | |
| 210788 | } |
| 210789 | return 0; |
| 210790 | } |
| 210791 | |
| 210792 | /* argv[0] is a BLOB that seems likely to be a JSONB. Subsequent |
| 210793 | ** arguments come in parse where each pair contains a JSON path and |
| 210794 | ** content to insert or set at that patch. Do the updates |
| 210795 | ** and return the result. |
| 210796 | ** |
| 210797 | ** The specific operation is determined by eEdit, which can be one |
| 210798 | ** of JEDIT_INS, JEDIT_REPL, or JEDIT_SET. |
| @@ -227533,12 +227940,12 @@ | |
| 227533 | ){ |
| 227534 | if( sqlite3_value_type(argv[3])==SQLITE_NULL && isInsert && pgno>1 ){ |
| 227535 | /* "INSERT INTO dbpage($PGNO,NULL)" causes page number $PGNO and |
| 227536 | ** all subsequent pages to be deleted. */ |
| 227537 | pTab->iDbTrunc = iDb; |
| 227538 | pgno--; |
| 227539 | pTab->pgnoTrunc = pgno; |
| 227540 | }else{ |
| 227541 | zErr = "bad page value"; |
| 227542 | goto update_fail; |
| 227543 | } |
| 227544 | } |
| @@ -228850,10 +229257,12 @@ | |
| 228850 | int rc = SQLITE_OK; |
| 228851 | |
| 228852 | if( pTab->nCol==0 ){ |
| 228853 | u8 *abPK; |
| 228854 | assert( pTab->azCol==0 || pTab->abPK==0 ); |
| 228855 | rc = sessionTableInfo(pSession, db, zDb, |
| 228856 | pTab->zName, &pTab->nCol, &pTab->nTotalCol, 0, &pTab->azCol, |
| 228857 | &pTab->azDflt, &pTab->aiIdx, &abPK, |
| 228858 | ((pSession==0 || pSession->bImplicitPK) ? &pTab->bRowid : 0) |
| 228859 | ); |
| @@ -229857,11 +230266,13 @@ | |
| 229857 | char *zExpr = 0; |
| 229858 | sqlite3 *db = pSession->db; |
| 229859 | SessionTable *pTo; /* Table zTbl */ |
| 229860 | |
| 229861 | /* Locate and if necessary initialize the target table object */ |
| 229862 | rc = sessionFindTable(pSession, zTbl, &pTo); |
| 229863 | if( pTo==0 ) goto diff_out; |
| 229864 | if( sessionInitTable(pSession, pTo, pSession->db, pSession->zDb) ){ |
| 229865 | rc = pSession->rc; |
| 229866 | goto diff_out; |
| 229867 | } |
| @@ -229868,21 +230279,47 @@ | |
| 229868 | |
| 229869 | /* Check the table schemas match */ |
| 229870 | if( rc==SQLITE_OK ){ |
| 229871 | int bHasPk = 0; |
| 229872 | int bMismatch = 0; |
| 229873 | int nCol; /* Columns in zFrom.zTbl */ |
| 229874 | int bRowid = 0; |
| 229875 | u8 *abPK; |
| 229876 | const char **azCol = 0; |
| 229877 | rc = sessionTableInfo(0, db, zFrom, zTbl, |
| 229878 | &nCol, 0, 0, &azCol, 0, 0, &abPK, |
| 229879 | pSession->bImplicitPK ? &bRowid : 0 |
| 229880 | ); |
| 229881 | if( rc==SQLITE_OK ){ |
| 229882 | if( pTo->nCol!=nCol ){ |
| 229883 | bMismatch = 1; |
| 229884 | }else{ |
| 229885 | int i; |
| 229886 | for(i=0; i<nCol; i++){ |
| 229887 | if( pTo->abPK[i]!=abPK[i] ) bMismatch = 1; |
| 229888 | if( sqlite3_stricmp(azCol[i], pTo->azCol[i]) ) bMismatch = 1; |
| @@ -241869,11 +242306,12 @@ | |
| 241869 | sqlite3Fts5ParseError( |
| 241870 | pParse, "expected integer, got \"%.*s\"", p->n, p->p |
| 241871 | ); |
| 241872 | return; |
| 241873 | } |
| 241874 | nNear = nNear * 10 + (p->p[i] - '0'); |
| 241875 | } |
| 241876 | }else{ |
| 241877 | nNear = FTS5_DEFAULT_NEARDIST; |
| 241878 | } |
| 241879 | pNear->nNear = nNear; |
| @@ -256775,11 +257213,11 @@ | |
| 256775 | int nArg, /* Number of args */ |
| 256776 | sqlite3_value **apUnused /* Function arguments */ |
| 256777 | ){ |
| 256778 | assert( nArg==0 ); |
| 256779 | UNUSED_PARAM2(nArg, apUnused); |
| 256780 | sqlite3_result_text(pCtx, "fts5: 2025-03-16 00:13:29 18bda13e197e4b4ec7464b3e70012f71edc05f73d8b14bb48bad452f81c7e185", -1, SQLITE_TRANSIENT); |
| 256781 | } |
| 256782 | |
| 256783 | /* |
| 256784 | ** Implementation of fts5_locale(LOCALE, TEXT) function. |
| 256785 | ** |
| @@ -260838,11 +261276,10 @@ | |
| 260838 | } |
| 260839 | iTbl++; |
| 260840 | } |
| 260841 | aAscii[0] = 0; /* 0x00 is never a token character */ |
| 260842 | } |
| 260843 | |
| 260844 | |
| 260845 | /* |
| 260846 | ** 2015 May 30 |
| 260847 | ** |
| 260848 | ** The author disclaims copyright to this source code. In place of |
| 260849 |
| --- extsrc/sqlite3.c | |
| +++ extsrc/sqlite3.c | |
| @@ -16,11 +16,11 @@ | |
| 16 | ** if you want a wrapper to interface SQLite with your choice of programming |
| 17 | ** language. The code for the "sqlite3" command-line shell is also in a |
| 18 | ** separate file. This file contains only code for the core SQLite library. |
| 19 | ** |
| 20 | ** The content in this amalgamation comes from Fossil check-in |
| 21 | ** d22475b81c4e26ccc50f3b5626d43b32f7a2 with changes in files: |
| 22 | ** |
| 23 | ** |
| 24 | */ |
| 25 | #ifndef SQLITE_AMALGAMATION |
| 26 | #define SQLITE_CORE 1 |
| @@ -450,11 +450,11 @@ | |
| 450 | ** be held constant and Z will be incremented or else Y will be incremented |
| 451 | ** and Z will be reset to zero. |
| 452 | ** |
| 453 | ** Since [version 3.6.18] ([dateof:3.6.18]), |
| 454 | ** SQLite source code has been stored in the |
| 455 | ** <a href="http://fossil-scm.org/">Fossil configuration management |
| 456 | ** system</a>. ^The SQLITE_SOURCE_ID macro evaluates to |
| 457 | ** a string which identifies a particular check-in of SQLite |
| 458 | ** within its configuration management system. ^The SQLITE_SOURCE_ID |
| 459 | ** string contains the date and time of the check-in (UTC) and a SHA1 |
| 460 | ** or SHA3-256 hash of the entire source tree. If the source code has |
| @@ -465,11 +465,11 @@ | |
| 465 | ** [sqlite3_libversion_number()], [sqlite3_sourceid()], |
| 466 | ** [sqlite_version()] and [sqlite_source_id()]. |
| 467 | */ |
| 468 | #define SQLITE_VERSION "3.50.0" |
| 469 | #define SQLITE_VERSION_NUMBER 3050000 |
| 470 | #define SQLITE_SOURCE_ID "2025-04-15 21:59:38 d22475b81c4e26ccc50f3b5626d43b32f7a2de34e5a764539554665bdda735d5" |
| 471 | |
| 472 | /* |
| 473 | ** CAPI3REF: Run-Time Library Version Numbers |
| 474 | ** KEYWORDS: sqlite3_version sqlite3_sourceid |
| 475 | ** |
| @@ -11946,12 +11946,13 @@ | |
| 11946 | ** To clarify, if this function is called and then a changeset constructed |
| 11947 | ** using [sqlite3session_changeset()], then after applying that changeset to |
| 11948 | ** database zFrom the contents of the two compatible tables would be |
| 11949 | ** identical. |
| 11950 | ** |
| 11951 | ** Unless the call to this function is a no-op as described above, it is an |
| 11952 | ** error if database zFrom does not exist or does not contain the required |
| 11953 | ** compatible table. |
| 11954 | ** |
| 11955 | ** If the operation is successful, SQLITE_OK is returned. Otherwise, an SQLite |
| 11956 | ** error code. In this case, if argument pzErrMsg is not NULL, *pzErrMsg |
| 11957 | ** may be set to point to a buffer containing an English language error |
| 11958 | ** message. It is the responsibility of the caller to free this buffer using |
| @@ -19162,10 +19163,11 @@ | |
| 19163 | unsigned noSkipScan:1; /* Do not try to use skip-scan if true */ |
| 19164 | unsigned hasStat1:1; /* aiRowLogEst values come from sqlite_stat1 */ |
| 19165 | unsigned bLowQual:1; /* sqlite_stat1 says this is a low-quality index */ |
| 19166 | unsigned bNoQuery:1; /* Do not use this index to optimize queries */ |
| 19167 | unsigned bAscKeyBug:1; /* True if the bba7b69f9849b5bf bug applies */ |
| 19168 | unsigned bIdxRowid:1; /* One or more of the index keys is the ROWID */ |
| 19169 | unsigned bHasVCol:1; /* Index references one or more VIRTUAL columns */ |
| 19170 | unsigned bHasExpr:1; /* Index contains an expression, either a literal |
| 19171 | ** expression, or a reference to a VIRTUAL column */ |
| 19172 | #ifdef SQLITE_ENABLE_STAT4 |
| 19173 | int nSample; /* Number of elements in aSample[] */ |
| @@ -30270,10 +30272,12 @@ | |
| 30272 | */ |
| 30273 | #include "windows.h" |
| 30274 | |
| 30275 | #ifdef __CYGWIN__ |
| 30276 | # include <sys/cygwin.h> |
| 30277 | # include <sys/stat.h> /* amalgamator: dontcache */ |
| 30278 | # include <unistd.h> /* amalgamator: dontcache */ |
| 30279 | # include <errno.h> /* amalgamator: dontcache */ |
| 30280 | #endif |
| 30281 | |
| 30282 | /* |
| 30283 | ** Determine if we are dealing with Windows NT. |
| @@ -35444,11 +35448,11 @@ | |
| 35448 | unsigned char const *z = zIn; |
| 35449 | unsigned char const *zEnd = &z[nByte-1]; |
| 35450 | int n = 0; |
| 35451 | |
| 35452 | if( SQLITE_UTF16NATIVE==SQLITE_UTF16LE ) z++; |
| 35453 | while( n<nChar && z<=zEnd ){ |
| 35454 | c = z[0]; |
| 35455 | z += 2; |
| 35456 | if( c>=0xd8 && c<0xdc && z<=zEnd && z[0]>=0xdc && z[0]<0xe0 ) z += 2; |
| 35457 | n++; |
| 35458 | } |
| @@ -47726,20 +47730,20 @@ | |
| 47730 | { "FileTimeToLocalFileTime", (SYSCALL)FileTimeToLocalFileTime, 0 }, |
| 47731 | #else |
| 47732 | { "FileTimeToLocalFileTime", (SYSCALL)0, 0 }, |
| 47733 | #endif |
| 47734 | |
| 47735 | #define osFileTimeToLocalFileTime ((BOOL(WINAPI*)(const FILETIME*, \ |
| 47736 | LPFILETIME))aSyscall[11].pCurrent) |
| 47737 | |
| 47738 | #if SQLITE_OS_WINCE |
| 47739 | { "FileTimeToSystemTime", (SYSCALL)FileTimeToSystemTime, 0 }, |
| 47740 | #else |
| 47741 | { "FileTimeToSystemTime", (SYSCALL)0, 0 }, |
| 47742 | #endif |
| 47743 | |
| 47744 | #define osFileTimeToSystemTime ((BOOL(WINAPI*)(const FILETIME*, \ |
| 47745 | LPSYSTEMTIME))aSyscall[12].pCurrent) |
| 47746 | |
| 47747 | { "FlushFileBuffers", (SYSCALL)FlushFileBuffers, 0 }, |
| 47748 | |
| 47749 | #define osFlushFileBuffers ((BOOL(WINAPI*)(HANDLE))aSyscall[13].pCurrent) |
| @@ -48015,11 +48019,11 @@ | |
| 48019 | { "LockFile", (SYSCALL)LockFile, 0 }, |
| 48020 | #else |
| 48021 | { "LockFile", (SYSCALL)0, 0 }, |
| 48022 | #endif |
| 48023 | |
| 48024 | #if !defined(osLockFile) && defined(SQLITE_WIN32_HAS_ANSI) |
| 48025 | #define osLockFile ((BOOL(WINAPI*)(HANDLE,DWORD,DWORD,DWORD, \ |
| 48026 | DWORD))aSyscall[47].pCurrent) |
| 48027 | #endif |
| 48028 | |
| 48029 | #if !SQLITE_OS_WINCE |
| @@ -48079,20 +48083,20 @@ | |
| 48083 | |
| 48084 | #define osSleep ((VOID(WINAPI*)(DWORD))aSyscall[55].pCurrent) |
| 48085 | |
| 48086 | { "SystemTimeToFileTime", (SYSCALL)SystemTimeToFileTime, 0 }, |
| 48087 | |
| 48088 | #define osSystemTimeToFileTime ((BOOL(WINAPI*)(const SYSTEMTIME*, \ |
| 48089 | LPFILETIME))aSyscall[56].pCurrent) |
| 48090 | |
| 48091 | #if !SQLITE_OS_WINCE && !SQLITE_OS_WINRT |
| 48092 | { "UnlockFile", (SYSCALL)UnlockFile, 0 }, |
| 48093 | #else |
| 48094 | { "UnlockFile", (SYSCALL)0, 0 }, |
| 48095 | #endif |
| 48096 | |
| 48097 | #if !defined(osUnlockFile) && defined(SQLITE_WIN32_HAS_ANSI) |
| 48098 | #define osUnlockFile ((BOOL(WINAPI*)(HANDLE,DWORD,DWORD,DWORD, \ |
| 48099 | DWORD))aSyscall[57].pCurrent) |
| 48100 | #endif |
| 48101 | |
| 48102 | #if !SQLITE_OS_WINCE |
| @@ -48316,10 +48320,67 @@ | |
| 48320 | { "CancelIo", (SYSCALL)0, 0 }, |
| 48321 | #endif |
| 48322 | |
| 48323 | #define osCancelIo ((BOOL(WINAPI*)(HANDLE))aSyscall[81].pCurrent) |
| 48324 | |
| 48325 | #if defined(SQLITE_WIN32_HAS_WIDE) && defined(_WIN32) |
| 48326 | { "GetModuleHandleW", (SYSCALL)GetModuleHandleW, 0 }, |
| 48327 | #else |
| 48328 | { "GetModuleHandleW", (SYSCALL)0, 0 }, |
| 48329 | #endif |
| 48330 | |
| 48331 | #define osGetModuleHandleW ((HMODULE(WINAPI*)(LPCWSTR))aSyscall[82].pCurrent) |
| 48332 | |
| 48333 | #ifndef _WIN32 |
| 48334 | { "getenv", (SYSCALL)getenv, 0 }, |
| 48335 | #else |
| 48336 | { "getenv", (SYSCALL)0, 0 }, |
| 48337 | #endif |
| 48338 | |
| 48339 | #define osGetenv ((const char *(*)(const char *))aSyscall[83].pCurrent) |
| 48340 | |
| 48341 | #ifndef _WIN32 |
| 48342 | { "getcwd", (SYSCALL)getcwd, 0 }, |
| 48343 | #else |
| 48344 | { "getcwd", (SYSCALL)0, 0 }, |
| 48345 | #endif |
| 48346 | |
| 48347 | #define osGetcwd ((char*(*)(char*,size_t))aSyscall[84].pCurrent) |
| 48348 | |
| 48349 | #ifndef _WIN32 |
| 48350 | { "readlink", (SYSCALL)readlink, 0 }, |
| 48351 | #else |
| 48352 | { "readlink", (SYSCALL)0, 0 }, |
| 48353 | #endif |
| 48354 | |
| 48355 | #define osReadlink ((ssize_t(*)(const char*,char*,size_t))aSyscall[85].pCurrent) |
| 48356 | |
| 48357 | #ifndef _WIN32 |
| 48358 | { "lstat", (SYSCALL)lstat, 0 }, |
| 48359 | #else |
| 48360 | { "lstat", (SYSCALL)0, 0 }, |
| 48361 | #endif |
| 48362 | |
| 48363 | #define osLstat ((int(*)(const char*,struct stat*))aSyscall[86].pCurrent) |
| 48364 | |
| 48365 | #ifndef _WIN32 |
| 48366 | { "__errno", (SYSCALL)__errno, 0 }, |
| 48367 | #else |
| 48368 | { "__errno", (SYSCALL)0, 0 }, |
| 48369 | #endif |
| 48370 | |
| 48371 | #define osErrno (*((int*(*)(void))aSyscall[87].pCurrent)()) |
| 48372 | |
| 48373 | #ifndef _WIN32 |
| 48374 | { "cygwin_conv_path", (SYSCALL)cygwin_conv_path, 0 }, |
| 48375 | #else |
| 48376 | { "cygwin_conv_path", (SYSCALL)0, 0 }, |
| 48377 | #endif |
| 48378 | |
| 48379 | #define osCygwin_conv_path ((size_t(*)(unsigned int, \ |
| 48380 | const void *, void *, size_t))aSyscall[88].pCurrent) |
| 48381 | |
| 48382 | }; /* End of the overrideable system calls */ |
| 48383 | |
| 48384 | /* |
| 48385 | ** This is the xSetSystemCall() method of sqlite3_vfs for all of the |
| 48386 | ** "win32" VFSes. Return SQLITE_OK upon successfully updating the |
| @@ -48489,10 +48550,11 @@ | |
| 48550 | sqlite3_mutex_leave(pMainMtx); |
| 48551 | return rc; |
| 48552 | } |
| 48553 | #endif /* SQLITE_WIN32_MALLOC */ |
| 48554 | |
| 48555 | #ifdef _WIN32 |
| 48556 | /* |
| 48557 | ** This function outputs the specified (ANSI) string to the Win32 debugger |
| 48558 | ** (if available). |
| 48559 | */ |
| 48560 | |
| @@ -48531,10 +48593,11 @@ | |
| 48593 | }else{ |
| 48594 | fprintf(stderr, "%s", zBuf); |
| 48595 | } |
| 48596 | #endif |
| 48597 | } |
| 48598 | #endif /* _WIN32 */ |
| 48599 | |
| 48600 | /* |
| 48601 | ** The following routine suspends the current thread for at least ms |
| 48602 | ** milliseconds. This is equivalent to the Win32 Sleep() interface. |
| 48603 | */ |
| @@ -48831,10 +48894,11 @@ | |
| 48894 | SQLITE_PRIVATE void sqlite3MemSetDefault(void){ |
| 48895 | sqlite3_config(SQLITE_CONFIG_MALLOC, sqlite3MemGetWin32()); |
| 48896 | } |
| 48897 | #endif /* SQLITE_WIN32_MALLOC */ |
| 48898 | |
| 48899 | #ifdef _WIN32 |
| 48900 | /* |
| 48901 | ** Convert a UTF-8 string to Microsoft Unicode. |
| 48902 | ** |
| 48903 | ** Space to hold the returned string is obtained from sqlite3_malloc(). |
| 48904 | */ |
| @@ -48856,10 +48920,11 @@ | |
| 48920 | sqlite3_free(zWideText); |
| 48921 | zWideText = 0; |
| 48922 | } |
| 48923 | return zWideText; |
| 48924 | } |
| 48925 | #endif /* _WIN32 */ |
| 48926 | |
| 48927 | /* |
| 48928 | ** Convert a Microsoft Unicode string to UTF-8. |
| 48929 | ** |
| 48930 | ** Space to hold the returned string is obtained from sqlite3_malloc(). |
| @@ -48890,32 +48955,33 @@ | |
| 48955 | ** code page. |
| 48956 | ** |
| 48957 | ** Space to hold the returned string is obtained from sqlite3_malloc(). |
| 48958 | */ |
| 48959 | static LPWSTR winMbcsToUnicode(const char *zText, int useAnsi){ |
| 48960 | int nWideChar; |
| 48961 | LPWSTR zMbcsText; |
| 48962 | int codepage = useAnsi ? CP_ACP : CP_OEMCP; |
| 48963 | |
| 48964 | nWideChar = osMultiByteToWideChar(codepage, 0, zText, -1, NULL, |
| 48965 | 0); |
| 48966 | if( nWideChar==0 ){ |
| 48967 | return 0; |
| 48968 | } |
| 48969 | zMbcsText = sqlite3MallocZero( nWideChar*sizeof(WCHAR) ); |
| 48970 | if( zMbcsText==0 ){ |
| 48971 | return 0; |
| 48972 | } |
| 48973 | nWideChar = osMultiByteToWideChar(codepage, 0, zText, -1, zMbcsText, |
| 48974 | nWideChar); |
| 48975 | if( nWideChar==0 ){ |
| 48976 | sqlite3_free(zMbcsText); |
| 48977 | zMbcsText = 0; |
| 48978 | } |
| 48979 | return zMbcsText; |
| 48980 | } |
| 48981 | |
| 48982 | #ifdef _WIN32 |
| 48983 | /* |
| 48984 | ** Convert a Microsoft Unicode string to a multi-byte character string, |
| 48985 | ** using the ANSI or OEM code page. |
| 48986 | ** |
| 48987 | ** Space to hold the returned string is obtained from sqlite3_malloc(). |
| @@ -48939,10 +49005,11 @@ | |
| 49005 | sqlite3_free(zText); |
| 49006 | zText = 0; |
| 49007 | } |
| 49008 | return zText; |
| 49009 | } |
| 49010 | #endif /* _WIN32 */ |
| 49011 | |
| 49012 | /* |
| 49013 | ** Convert a multi-byte character string to UTF-8. |
| 49014 | ** |
| 49015 | ** Space to hold the returned string is obtained from sqlite3_malloc(). |
| @@ -48958,10 +49025,11 @@ | |
| 49025 | zTextUtf8 = winUnicodeToUtf8(zTmpWide); |
| 49026 | sqlite3_free(zTmpWide); |
| 49027 | return zTextUtf8; |
| 49028 | } |
| 49029 | |
| 49030 | #ifdef _WIN32 |
| 49031 | /* |
| 49032 | ** Convert a UTF-8 string to a multi-byte character string. |
| 49033 | ** |
| 49034 | ** Space to hold the returned string is obtained from sqlite3_malloc(). |
| 49035 | */ |
| @@ -49007,10 +49075,11 @@ | |
| 49075 | #ifndef SQLITE_OMIT_AUTOINIT |
| 49076 | if( sqlite3_initialize() ) return 0; |
| 49077 | #endif |
| 49078 | return winUnicodeToUtf8(zWideText); |
| 49079 | } |
| 49080 | #endif /* _WIN32 */ |
| 49081 | |
| 49082 | /* |
| 49083 | ** This is a public wrapper for the winMbcsToUtf8() function. |
| 49084 | */ |
| 49085 | SQLITE_API char *sqlite3_win32_mbcs_to_utf8(const char *zText){ |
| @@ -49024,10 +49093,11 @@ | |
| 49093 | if( sqlite3_initialize() ) return 0; |
| 49094 | #endif |
| 49095 | return winMbcsToUtf8(zText, osAreFileApisANSI()); |
| 49096 | } |
| 49097 | |
| 49098 | #ifdef _WIN32 |
| 49099 | /* |
| 49100 | ** This is a public wrapper for the winMbcsToUtf8() function. |
| 49101 | */ |
| 49102 | SQLITE_API char *sqlite3_win32_mbcs_to_utf8_v2(const char *zText, int useAnsi){ |
| 49103 | #ifdef SQLITE_ENABLE_API_ARMOR |
| @@ -49148,10 +49218,11 @@ | |
| 49218 | unsigned long type, /* Identifier for directory being set or reset */ |
| 49219 | void *zValue /* New value for directory being set or reset */ |
| 49220 | ){ |
| 49221 | return sqlite3_win32_set_directory16(type, zValue); |
| 49222 | } |
| 49223 | #endif /* _WIN32 */ |
| 49224 | |
| 49225 | /* |
| 49226 | ** The return value of winGetLastErrorMsg |
| 49227 | ** is zero if the error message fits in the buffer, or non-zero |
| 49228 | ** otherwise (if the message was truncated). |
| @@ -49696,13 +49767,15 @@ | |
| 49767 | OVERLAPPED ovlp; |
| 49768 | memset(&ovlp, 0, sizeof(OVERLAPPED)); |
| 49769 | ovlp.Offset = offsetLow; |
| 49770 | ovlp.OffsetHigh = offsetHigh; |
| 49771 | return osLockFileEx(*phFile, flags, 0, numBytesLow, numBytesHigh, &ovlp); |
| 49772 | #ifdef SQLITE_WIN32_HAS_ANSI |
| 49773 | }else{ |
| 49774 | return osLockFile(*phFile, offsetLow, offsetHigh, numBytesLow, |
| 49775 | numBytesHigh); |
| 49776 | #endif |
| 49777 | } |
| 49778 | #endif |
| 49779 | } |
| 49780 | |
| 49781 | /* |
| @@ -49806,13 +49879,15 @@ | |
| 49879 | OVERLAPPED ovlp; |
| 49880 | memset(&ovlp, 0, sizeof(OVERLAPPED)); |
| 49881 | ovlp.Offset = offsetLow; |
| 49882 | ovlp.OffsetHigh = offsetHigh; |
| 49883 | return osUnlockFileEx(*phFile, 0, numBytesLow, numBytesHigh, &ovlp); |
| 49884 | #ifdef SQLITE_WIN32_HAS_ANSI |
| 49885 | }else{ |
| 49886 | return osUnlockFile(*phFile, offsetLow, offsetHigh, numBytesLow, |
| 49887 | numBytesHigh); |
| 49888 | #endif |
| 49889 | } |
| 49890 | #endif |
| 49891 | } |
| 49892 | |
| 49893 | /* |
| @@ -51222,18 +51297,95 @@ | |
| 51297 | |
| 51298 | /* |
| 51299 | ** Convert a UTF-8 filename into whatever form the underlying |
| 51300 | ** operating system wants filenames in. Space to hold the result |
| 51301 | ** is obtained from malloc and must be freed by the calling |
| 51302 | ** function |
| 51303 | ** |
| 51304 | ** On Cygwin, 3 possible input forms are accepted: |
| 51305 | ** - If the filename starts with "<drive>:/" or "<drive>:\", |
| 51306 | ** it is converted to UTF-16 as-is. |
| 51307 | ** - If the filename contains '/', it is assumed to be a |
| 51308 | ** Cygwin absolute path, it is converted to a win32 |
| 51309 | ** absolute path in UTF-16. |
| 51310 | ** - Otherwise it must be a filename only, the win32 filename |
| 51311 | ** is returned in UTF-16. |
| 51312 | ** Note: If the function cygwin_conv_path() fails, only |
| 51313 | ** UTF-8 -> UTF-16 conversion will be done. This can only |
| 51314 | ** happen when the file path >32k, in which case winUtf8ToUnicode() |
| 51315 | ** will fail too. |
| 51316 | */ |
| 51317 | static void *winConvertFromUtf8Filename(const char *zFilename){ |
| 51318 | void *zConverted = 0; |
| 51319 | if( osIsNT() ){ |
| 51320 | #ifdef __CYGWIN__ |
| 51321 | int nChar; |
| 51322 | LPWSTR zWideFilename; |
| 51323 | |
| 51324 | if( osCygwin_conv_path && !(winIsDriveLetterAndColon(zFilename) |
| 51325 | && winIsDirSep(zFilename[2])) ){ |
| 51326 | int nByte; |
| 51327 | int convertflag = CCP_POSIX_TO_WIN_W; |
| 51328 | if( !strchr(zFilename, '/') ) convertflag |= CCP_RELATIVE; |
| 51329 | nByte = (int)osCygwin_conv_path(convertflag, |
| 51330 | zFilename, 0, 0); |
| 51331 | if( nByte>0 ){ |
| 51332 | zConverted = sqlite3MallocZero(nByte+12); |
| 51333 | if ( zConverted==0 ){ |
| 51334 | return zConverted; |
| 51335 | } |
| 51336 | zWideFilename = zConverted; |
| 51337 | /* Filenames should be prefixed, except when converted |
| 51338 | * full path already starts with "\\?\". */ |
| 51339 | if( osCygwin_conv_path(convertflag, zFilename, |
| 51340 | zWideFilename+4, nByte)==0 ){ |
| 51341 | if( (convertflag&CCP_RELATIVE) ){ |
| 51342 | memmove(zWideFilename, zWideFilename+4, nByte); |
| 51343 | }else if( memcmp(zWideFilename+4, L"\\\\", 4) ){ |
| 51344 | memcpy(zWideFilename, L"\\\\?\\", 8); |
| 51345 | }else if( zWideFilename[6]!='?' ){ |
| 51346 | memmove(zWideFilename+6, zWideFilename+4, nByte); |
| 51347 | memcpy(zWideFilename, L"\\\\?\\UNC", 14); |
| 51348 | }else{ |
| 51349 | memmove(zWideFilename, zWideFilename+4, nByte); |
| 51350 | } |
| 51351 | return zConverted; |
| 51352 | } |
| 51353 | sqlite3_free(zConverted); |
| 51354 | } |
| 51355 | } |
| 51356 | nChar = osMultiByteToWideChar(CP_UTF8, 0, zFilename, -1, NULL, 0); |
| 51357 | if( nChar==0 ){ |
| 51358 | return 0; |
| 51359 | } |
| 51360 | zWideFilename = sqlite3MallocZero( nChar*sizeof(WCHAR)+12 ); |
| 51361 | if( zWideFilename==0 ){ |
| 51362 | return 0; |
| 51363 | } |
| 51364 | nChar = osMultiByteToWideChar(CP_UTF8, 0, zFilename, -1, |
| 51365 | zWideFilename, nChar); |
| 51366 | if( nChar==0 ){ |
| 51367 | sqlite3_free(zWideFilename); |
| 51368 | zWideFilename = 0; |
| 51369 | }else if( nChar>MAX_PATH |
| 51370 | && winIsDriveLetterAndColon(zFilename) |
| 51371 | && winIsDirSep(zFilename[2]) ){ |
| 51372 | memmove(zWideFilename+4, zWideFilename, nChar*sizeof(WCHAR)); |
| 51373 | zWideFilename[2] = '\\'; |
| 51374 | memcpy(zWideFilename, L"\\\\?\\", 8); |
| 51375 | }else if( nChar>MAX_PATH |
| 51376 | && winIsDirSep(zFilename[0]) && winIsDirSep(zFilename[1]) |
| 51377 | && zFilename[2] != '?' ){ |
| 51378 | memmove(zWideFilename+6, zWideFilename, nChar*sizeof(WCHAR)); |
| 51379 | memcpy(zWideFilename, L"\\\\?\\UNC", 14); |
| 51380 | } |
| 51381 | zConverted = zWideFilename; |
| 51382 | #else |
| 51383 | zConverted = winUtf8ToUnicode(zFilename); |
| 51384 | #endif /* __CYGWIN__ */ |
| 51385 | } |
| 51386 | #if defined(SQLITE_WIN32_HAS_ANSI) && defined(_WIN32) |
| 51387 | else{ |
| 51388 | zConverted = winUtf8ToMbcs(zFilename, osAreFileApisANSI()); |
| 51389 | } |
| 51390 | #endif |
| 51391 | /* caller will handle out of memory */ |
| @@ -52058,11 +52210,11 @@ | |
| 52210 | ** |
| 52211 | ** This division contains the implementation of methods on the |
| 52212 | ** sqlite3_vfs object. |
| 52213 | */ |
| 52214 | |
| 52215 | #if 0 /* No longer necessary */ |
| 52216 | /* |
| 52217 | ** Convert a filename from whatever the underlying operating system |
| 52218 | ** supports for filenames into UTF-8. Space to hold the result is |
| 52219 | ** obtained from malloc and must be freed by the calling function. |
| 52220 | */ |
| @@ -52091,11 +52243,18 @@ | |
| 52243 | int nLen = sqlite3Strlen30(zBuf); |
| 52244 | if( nLen>0 ){ |
| 52245 | if( winIsDirSep(zBuf[nLen-1]) ){ |
| 52246 | return 1; |
| 52247 | }else if( nLen+1<nBuf ){ |
| 52248 | if( !osGetenv ){ |
| 52249 | zBuf[nLen] = winGetDirSep(); |
| 52250 | }else if( winIsDriveLetterAndColon(zBuf) && winIsDirSep(zBuf[2]) ){ |
| 52251 | zBuf[nLen] = '\\'; |
| 52252 | zBuf[2]='\\'; |
| 52253 | }else{ |
| 52254 | zBuf[nLen] = '/'; |
| 52255 | } |
| 52256 | zBuf[nLen+1] = '\0'; |
| 52257 | return 1; |
| 52258 | } |
| 52259 | } |
| 52260 | } |
| @@ -52118,11 +52277,11 @@ | |
| 52277 | /* |
| 52278 | ** Create a temporary file name and store the resulting pointer into pzBuf. |
| 52279 | ** The pointer returned in pzBuf must be freed via sqlite3_free(). |
| 52280 | */ |
| 52281 | static int winGetTempname(sqlite3_vfs *pVfs, char **pzBuf){ |
| 52282 | static const char zChars[] = |
| 52283 | "abcdefghijklmnopqrstuvwxyz" |
| 52284 | "ABCDEFGHIJKLMNOPQRSTUVWXYZ" |
| 52285 | "0123456789"; |
| 52286 | size_t i, j; |
| 52287 | DWORD pid; |
| @@ -52169,11 +52328,11 @@ | |
| 52328 | } |
| 52329 | sqlite3_mutex_leave(sqlite3MutexAlloc(SQLITE_MUTEX_STATIC_TEMPDIR)); |
| 52330 | } |
| 52331 | |
| 52332 | #if defined(__CYGWIN__) |
| 52333 | else if( osGetenv!=NULL ){ |
| 52334 | static const char *azDirs[] = { |
| 52335 | 0, /* getenv("SQLITE_TMPDIR") */ |
| 52336 | 0, /* getenv("TMPDIR") */ |
| 52337 | 0, /* getenv("TMP") */ |
| 52338 | 0, /* getenv("TEMP") */ |
| @@ -52185,24 +52344,24 @@ | |
| 52344 | 0 /* List terminator */ |
| 52345 | }; |
| 52346 | unsigned int i; |
| 52347 | const char *zDir = 0; |
| 52348 | |
| 52349 | if( !azDirs[0] ) azDirs[0] = osGetenv("SQLITE_TMPDIR"); |
| 52350 | if( !azDirs[1] ) azDirs[1] = osGetenv("TMPDIR"); |
| 52351 | if( !azDirs[2] ) azDirs[2] = osGetenv("TMP"); |
| 52352 | if( !azDirs[3] ) azDirs[3] = osGetenv("TEMP"); |
| 52353 | if( !azDirs[4] ) azDirs[4] = osGetenv("USERPROFILE"); |
| 52354 | for(i=0; i<sizeof(azDirs)/sizeof(azDirs[0]); zDir=azDirs[i++]){ |
| 52355 | void *zConverted; |
| 52356 | if( zDir==0 ) continue; |
| 52357 | /* If the path starts with a drive letter followed by the colon |
| 52358 | ** character, assume it is already a native Win32 path; otherwise, |
| 52359 | ** it must be converted to a native Win32 path via the Cygwin API |
| 52360 | ** prior to using it. |
| 52361 | */ |
| 52362 | { |
| 52363 | zConverted = winConvertFromUtf8Filename(zDir); |
| 52364 | if( !zConverted ){ |
| 52365 | sqlite3_free(zBuf); |
| 52366 | OSTRACE(("TEMP-FILENAME rc=SQLITE_IOERR_NOMEM\n")); |
| 52367 | return SQLITE_IOERR_NOMEM_BKPT; |
| @@ -52211,19 +52370,20 @@ | |
| 52370 | sqlite3_snprintf(nMax, zBuf, "%s", zDir); |
| 52371 | sqlite3_free(zConverted); |
| 52372 | break; |
| 52373 | } |
| 52374 | sqlite3_free(zConverted); |
| 52375 | #if 0 /* No longer necessary */ |
| 52376 | }else{ |
| 52377 | zConverted = sqlite3MallocZero( nMax+1 ); |
| 52378 | if( !zConverted ){ |
| 52379 | sqlite3_free(zBuf); |
| 52380 | OSTRACE(("TEMP-FILENAME rc=SQLITE_IOERR_NOMEM\n")); |
| 52381 | return SQLITE_IOERR_NOMEM_BKPT; |
| 52382 | } |
| 52383 | if( osCygwin_conv_path( |
| 52384 | CCP_POSIX_TO_WIN_W, zDir, |
| 52385 | zConverted, nMax+1)<0 ){ |
| 52386 | sqlite3_free(zConverted); |
| 52387 | sqlite3_free(zBuf); |
| 52388 | OSTRACE(("TEMP-FILENAME rc=SQLITE_IOERR_CONVPATH\n")); |
| 52389 | return winLogError(SQLITE_IOERR_CONVPATH, (DWORD)errno, |
| @@ -52245,14 +52405,17 @@ | |
| 52405 | sqlite3_free(zUtf8); |
| 52406 | sqlite3_free(zConverted); |
| 52407 | break; |
| 52408 | } |
| 52409 | sqlite3_free(zConverted); |
| 52410 | #endif /* No longer necessary */ |
| 52411 | } |
| 52412 | } |
| 52413 | } |
| 52414 | #endif |
| 52415 | |
| 52416 | #if !SQLITE_OS_WINRT && defined(_WIN32) |
| 52417 | else if( osIsNT() ){ |
| 52418 | char *zMulti; |
| 52419 | LPWSTR zWidePath = sqlite3MallocZero( nMax*sizeof(WCHAR) ); |
| 52420 | if( !zWidePath ){ |
| 52421 | sqlite3_free(zBuf); |
| @@ -52372,11 +52535,11 @@ | |
| 52535 | &sAttrData)) && winRetryIoerr(&cnt, &lastErrno) ){} |
| 52536 | if( !rc ){ |
| 52537 | return 0; /* Invalid name? */ |
| 52538 | } |
| 52539 | attr = sAttrData.dwFileAttributes; |
| 52540 | #if SQLITE_OS_WINCE==0 && defined(SQLITE_WIN32_HAS_ANSI) |
| 52541 | }else{ |
| 52542 | attr = osGetFileAttributesA((char*)zConverted); |
| 52543 | #endif |
| 52544 | } |
| 52545 | return (attr!=INVALID_FILE_ATTRIBUTES) && (attr&FILE_ATTRIBUTE_DIRECTORY); |
| @@ -52388,10 +52551,16 @@ | |
| 52551 | const char *zFilename, /* Name of file to check */ |
| 52552 | int flags, /* Type of test to make on this file */ |
| 52553 | int *pResOut /* OUT: Result */ |
| 52554 | ); |
| 52555 | |
| 52556 | /* |
| 52557 | ** The Windows version of xAccess() accepts an extra bit in the flags |
| 52558 | ** parameter that prevents an anti-virus retry loop. |
| 52559 | */ |
| 52560 | #define NORETRY 0x4000 |
| 52561 | |
| 52562 | /* |
| 52563 | ** Open a file. |
| 52564 | */ |
| 52565 | static int winOpen( |
| 52566 | sqlite3_vfs *pVfs, /* Used to get maximum path length and AppData */ |
| @@ -52412,10 +52581,11 @@ | |
| 52581 | winVfsAppData *pAppData; |
| 52582 | winFile *pFile = (winFile*)id; |
| 52583 | void *zConverted; /* Filename in OS encoding */ |
| 52584 | const char *zUtf8Name = zName; /* Filename in UTF-8 encoding */ |
| 52585 | int cnt = 0; |
| 52586 | int isRO = 0; /* file is known to be accessible readonly */ |
| 52587 | |
| 52588 | /* If argument zPath is a NULL pointer, this function is required to open |
| 52589 | ** a temporary file. Use this buffer to store the file name in. |
| 52590 | */ |
| 52591 | char *zTmpname = 0; /* For temporary filename, if necessary. */ |
| @@ -52576,13 +52746,13 @@ | |
| 52746 | dwShareMode, |
| 52747 | dwCreationDisposition, |
| 52748 | &extendedParameters); |
| 52749 | if( h!=INVALID_HANDLE_VALUE ) break; |
| 52750 | if( isReadWrite ){ |
| 52751 | int rc2; |
| 52752 | sqlite3BeginBenignMalloc(); |
| 52753 | rc2 = winAccess(pVfs, zUtf8Name, SQLITE_ACCESS_READ|NORETRY, &isRO); |
| 52754 | sqlite3EndBenignMalloc(); |
| 52755 | if( rc2==SQLITE_OK && isRO ) break; |
| 52756 | } |
| 52757 | }while( winRetryIoerr(&cnt, &lastErrno) ); |
| 52758 | #else |
| @@ -52593,13 +52763,13 @@ | |
| 52763 | dwCreationDisposition, |
| 52764 | dwFlagsAndAttributes, |
| 52765 | NULL); |
| 52766 | if( h!=INVALID_HANDLE_VALUE ) break; |
| 52767 | if( isReadWrite ){ |
| 52768 | int rc2; |
| 52769 | sqlite3BeginBenignMalloc(); |
| 52770 | rc2 = winAccess(pVfs, zUtf8Name, SQLITE_ACCESS_READ|NORETRY, &isRO); |
| 52771 | sqlite3EndBenignMalloc(); |
| 52772 | if( rc2==SQLITE_OK && isRO ) break; |
| 52773 | } |
| 52774 | }while( winRetryIoerr(&cnt, &lastErrno) ); |
| 52775 | #endif |
| @@ -52613,13 +52783,13 @@ | |
| 52783 | dwCreationDisposition, |
| 52784 | dwFlagsAndAttributes, |
| 52785 | NULL); |
| 52786 | if( h!=INVALID_HANDLE_VALUE ) break; |
| 52787 | if( isReadWrite ){ |
| 52788 | int rc2; |
| 52789 | sqlite3BeginBenignMalloc(); |
| 52790 | rc2 = winAccess(pVfs, zUtf8Name, SQLITE_ACCESS_READ|NORETRY, &isRO); |
| 52791 | sqlite3EndBenignMalloc(); |
| 52792 | if( rc2==SQLITE_OK && isRO ) break; |
| 52793 | } |
| 52794 | }while( winRetryIoerr(&cnt, &lastErrno) ); |
| 52795 | } |
| @@ -52630,11 +52800,11 @@ | |
| 52800 | dwDesiredAccess, (h==INVALID_HANDLE_VALUE) ? "failed" : "ok")); |
| 52801 | |
| 52802 | if( h==INVALID_HANDLE_VALUE ){ |
| 52803 | sqlite3_free(zConverted); |
| 52804 | sqlite3_free(zTmpname); |
| 52805 | if( isReadWrite && isRO && !isExclusive ){ |
| 52806 | return winOpen(pVfs, zName, id, |
| 52807 | ((flags|SQLITE_OPEN_READONLY) & |
| 52808 | ~(SQLITE_OPEN_CREATE|SQLITE_OPEN_READWRITE)), |
| 52809 | pOutFlags); |
| 52810 | }else{ |
| @@ -52832,11 +53002,17 @@ | |
| 53002 | ){ |
| 53003 | DWORD attr; |
| 53004 | int rc = 0; |
| 53005 | DWORD lastErrno = 0; |
| 53006 | void *zConverted; |
| 53007 | int noRetry = 0; /* Do not use winRetryIoerr() */ |
| 53008 | UNUSED_PARAMETER(pVfs); |
| 53009 | |
| 53010 | if( (flags & NORETRY)!=0 ){ |
| 53011 | noRetry = 1; |
| 53012 | flags &= ~NORETRY; |
| 53013 | } |
| 53014 | |
| 53015 | SimulateIOError( return SQLITE_IOERR_ACCESS; ); |
| 53016 | OSTRACE(("ACCESS name=%s, flags=%x, pResOut=%p\n", |
| 53017 | zFilename, flags, pResOut)); |
| 53018 | |
| @@ -52856,11 +53032,14 @@ | |
| 53032 | int cnt = 0; |
| 53033 | WIN32_FILE_ATTRIBUTE_DATA sAttrData; |
| 53034 | memset(&sAttrData, 0, sizeof(sAttrData)); |
| 53035 | while( !(rc = osGetFileAttributesExW((LPCWSTR)zConverted, |
| 53036 | GetFileExInfoStandard, |
| 53037 | &sAttrData)) |
| 53038 | && !noRetry |
| 53039 | && winRetryIoerr(&cnt, &lastErrno) |
| 53040 | ){ /* Loop until true */} |
| 53041 | if( rc ){ |
| 53042 | /* For an SQLITE_ACCESS_EXISTS query, treat a zero-length file |
| 53043 | ** as if it does not exist. |
| 53044 | */ |
| 53045 | if( flags==SQLITE_ACCESS_EXISTS |
| @@ -52924,10 +53103,11 @@ | |
| 53103 | const char *zPathname |
| 53104 | ){ |
| 53105 | return ( sqlite3Isalpha(zPathname[0]) && zPathname[1]==':' ); |
| 53106 | } |
| 53107 | |
| 53108 | #ifdef _WIN32 |
| 53109 | /* |
| 53110 | ** Returns non-zero if the specified path name should be used verbatim. If |
| 53111 | ** non-zero is returned from this function, the calling function must simply |
| 53112 | ** use the provided path name verbatim -OR- resolve it into a full path name |
| 53113 | ** using the GetFullPathName Win32 API function (if available). |
| @@ -52960,10 +53140,74 @@ | |
| 53140 | ** If we get to this point, the path name should almost certainly be a purely |
| 53141 | ** relative one (i.e. not a UNC name, not absolute, and not volume relative). |
| 53142 | */ |
| 53143 | return FALSE; |
| 53144 | } |
| 53145 | #endif /* _WIN32 */ |
| 53146 | |
| 53147 | #ifdef __CYGWIN__ |
| 53148 | /* |
| 53149 | ** Simplify a filename into its canonical form |
| 53150 | ** by making the following changes: |
| 53151 | ** |
| 53152 | ** * convert any '/' to '\' (win32) or reverse (Cygwin) |
| 53153 | ** * removing any trailing and duplicate / (except for UNC paths) |
| 53154 | ** * convert /./ into just / |
| 53155 | ** |
| 53156 | ** Changes are made in-place. Return the new name length. |
| 53157 | ** |
| 53158 | ** The original filename is in z[0..]. If the path is shortened, |
| 53159 | ** no-longer used bytes will be written by '\0'. |
| 53160 | */ |
| 53161 | static void winSimplifyName(char *z){ |
| 53162 | int i, j; |
| 53163 | for(i=j=0; z[i]; ++i){ |
| 53164 | if( winIsDirSep(z[i]) ){ |
| 53165 | #if !defined(SQLITE_TEST) |
| 53166 | /* Some test-cases assume that "./foo" and "foo" are different */ |
| 53167 | if( z[i+1]=='.' && winIsDirSep(z[i+2]) ){ |
| 53168 | ++i; |
| 53169 | continue; |
| 53170 | } |
| 53171 | #endif |
| 53172 | if( !z[i+1] || (winIsDirSep(z[i+1]) && (i!=0)) ){ |
| 53173 | continue; |
| 53174 | } |
| 53175 | z[j++] = osGetenv?'/':'\\'; |
| 53176 | }else{ |
| 53177 | z[j++] = z[i]; |
| 53178 | } |
| 53179 | } |
| 53180 | while(j<i) z[j++] = '\0'; |
| 53181 | } |
| 53182 | |
| 53183 | #define SQLITE_MAX_SYMLINKS 100 |
| 53184 | |
| 53185 | static int mkFullPathname( |
| 53186 | const char *zPath, /* Input path */ |
| 53187 | char *zOut, /* Output buffer */ |
| 53188 | int nOut /* Allocated size of buffer zOut */ |
| 53189 | ){ |
| 53190 | int nPath = sqlite3Strlen30(zPath); |
| 53191 | int iOff = 0; |
| 53192 | if( zPath[0]!='/' ){ |
| 53193 | if( osGetcwd(zOut, nOut-2)==0 ){ |
| 53194 | return winLogError(SQLITE_CANTOPEN_BKPT, (DWORD)osErrno, "getcwd", zPath); |
| 53195 | } |
| 53196 | iOff = sqlite3Strlen30(zOut); |
| 53197 | zOut[iOff++] = '/'; |
| 53198 | } |
| 53199 | if( (iOff+nPath+1)>nOut ){ |
| 53200 | /* SQLite assumes that xFullPathname() nul-terminates the output buffer |
| 53201 | ** even if it returns an error. */ |
| 53202 | zOut[iOff] = '\0'; |
| 53203 | return SQLITE_CANTOPEN_BKPT; |
| 53204 | } |
| 53205 | sqlite3_snprintf(nOut-iOff, &zOut[iOff], "%s", zPath); |
| 53206 | return SQLITE_OK; |
| 53207 | } |
| 53208 | #endif /* __CYGWIN__ */ |
| 53209 | |
| 53210 | /* |
| 53211 | ** Turn a relative pathname into a full pathname. Write the full |
| 53212 | ** pathname into zOut[]. zOut[] will be at least pVfs->mxPathname |
| 53213 | ** bytes in size. |
| @@ -52972,12 +53216,12 @@ | |
| 53216 | sqlite3_vfs *pVfs, /* Pointer to vfs object */ |
| 53217 | const char *zRelative, /* Possibly relative input path */ |
| 53218 | int nFull, /* Size of output buffer in bytes */ |
| 53219 | char *zFull /* Output buffer */ |
| 53220 | ){ |
| 53221 | #if !SQLITE_OS_WINCE && !SQLITE_OS_WINRT |
| 53222 | int nByte; |
| 53223 | void *zConverted; |
| 53224 | char *zOut; |
| 53225 | #endif |
| 53226 | |
| 53227 | /* If this path name begins with "/X:" or "\\?\", where "X" is any |
| @@ -52986,68 +53230,114 @@ | |
| 53230 | if( zRelative[0]=='/' && (winIsDriveLetterAndColon(zRelative+1) |
| 53231 | || winIsLongPathPrefix(zRelative+1)) ){ |
| 53232 | zRelative++; |
| 53233 | } |
| 53234 | |
| 53235 | SimulateIOError( return SQLITE_ERROR ); |
| 53236 | |
| 53237 | #ifdef __CYGWIN__ |
| 53238 | if( osGetcwd ){ |
| 53239 | zFull[nFull-1] = '\0'; |
| 53240 | if( !winIsDriveLetterAndColon(zRelative) || !winIsDirSep(zRelative[2]) ){ |
| 53241 | int rc = SQLITE_OK; |
| 53242 | int nLink = 1; /* Number of symbolic links followed so far */ |
| 53243 | const char *zIn = zRelative; /* Input path for each iteration of loop */ |
| 53244 | char *zDel = 0; |
| 53245 | struct stat buf; |
| 53246 | |
| 53247 | UNUSED_PARAMETER(pVfs); |
| 53248 | |
| 53249 | do { |
| 53250 | /* Call lstat() on path zIn. Set bLink to true if the path is a symbolic |
| 53251 | ** link, or false otherwise. */ |
| 53252 | int bLink = 0; |
| 53253 | if( osLstat && osReadlink ) { |
| 53254 | if( osLstat(zIn, &buf)!=0 ){ |
| 53255 | int myErrno = osErrno; |
| 53256 | if( myErrno!=ENOENT ){ |
| 53257 | rc = winLogError(SQLITE_CANTOPEN_BKPT, (DWORD)myErrno, "lstat", zIn); |
| 53258 | } |
| 53259 | }else{ |
| 53260 | bLink = ((buf.st_mode & 0170000) == 0120000); |
| 53261 | } |
| 53262 | |
| 53263 | if( bLink ){ |
| 53264 | if( zDel==0 ){ |
| 53265 | zDel = sqlite3MallocZero(nFull); |
| 53266 | if( zDel==0 ) rc = SQLITE_NOMEM; |
| 53267 | }else if( ++nLink>SQLITE_MAX_SYMLINKS ){ |
| 53268 | rc = SQLITE_CANTOPEN_BKPT; |
| 53269 | } |
| 53270 | |
| 53271 | if( rc==SQLITE_OK ){ |
| 53272 | nByte = osReadlink(zIn, zDel, nFull-1); |
| 53273 | if( nByte ==(DWORD)-1 ){ |
| 53274 | rc = winLogError(SQLITE_CANTOPEN_BKPT, (DWORD)osErrno, "readlink", zIn); |
| 53275 | }else{ |
| 53276 | if( zDel[0]!='/' ){ |
| 53277 | int n; |
| 53278 | for(n = sqlite3Strlen30(zIn); n>0 && zIn[n-1]!='/'; n--); |
| 53279 | if( nByte+n+1>nFull ){ |
| 53280 | rc = SQLITE_CANTOPEN_BKPT; |
| 53281 | }else{ |
| 53282 | memmove(&zDel[n], zDel, nByte+1); |
| 53283 | memcpy(zDel, zIn, n); |
| 53284 | nByte += n; |
| 53285 | } |
| 53286 | } |
| 53287 | zDel[nByte] = '\0'; |
| 53288 | } |
| 53289 | } |
| 53290 | |
| 53291 | zIn = zDel; |
| 53292 | } |
| 53293 | } |
| 53294 | |
| 53295 | assert( rc!=SQLITE_OK || zIn!=zFull || zIn[0]=='/' ); |
| 53296 | if( rc==SQLITE_OK && zIn!=zFull ){ |
| 53297 | rc = mkFullPathname(zIn, zFull, nFull); |
| 53298 | } |
| 53299 | if( bLink==0 ) break; |
| 53300 | zIn = zFull; |
| 53301 | }while( rc==SQLITE_OK ); |
| 53302 | |
| 53303 | sqlite3_free(zDel); |
| 53304 | winSimplifyName(zFull); |
| 53305 | return rc; |
| 53306 | } |
| 53307 | } |
| 53308 | #endif /* __CYGWIN__ */ |
| 53309 | #if 0 /* This doesn't work correctly at all! See: |
| 53310 | <https://marc.info/?l=sqlite-users&m=139299149416314&w=2> |
| 53311 | */ |
| 53312 | SimulateIOError( return SQLITE_ERROR ); |
| 53313 | UNUSED_PARAMETER(nFull); |
| 53314 | assert( nFull>=pVfs->mxPathname ); |
| 53315 | char *zOut = sqlite3MallocZero( pVfs->mxPathname+1 ); |
| 53316 | if( !zOut ){ |
| 53317 | return SQLITE_IOERR_NOMEM_BKPT; |
| 53318 | } |
| 53319 | if( osCygwin_conv_path( |
| 53320 | CCP_POSIX_TO_WIN_W, |
| 53321 | zRelative, zOut, pVfs->mxPathname+1)<0 ){ |
| 53322 | sqlite3_free(zOut); |
| 53323 | return winLogError(SQLITE_CANTOPEN_CONVPATH, (DWORD)errno, |
| 53324 | "winFullPathname2", zRelative); |
| 53325 | }else{ |
| 53326 | char *zUtf8 = winConvertToUtf8Filename(zOut); |
| 53327 | if( !zUtf8 ){ |
| 53328 | sqlite3_free(zOut); |
| 53329 | return SQLITE_IOERR_NOMEM_BKPT; |
| 53330 | } |
| 53331 | sqlite3_snprintf(MIN(nFull, pVfs->mxPathname), zFull, "%s", zUtf8); |
| 53332 | sqlite3_free(zUtf8); |
| 53333 | sqlite3_free(zOut); |
| 53334 | } |
| 53335 | return SQLITE_OK; |
| 53336 | #endif |
| 53337 | |
| 53338 | #if (SQLITE_OS_WINCE || SQLITE_OS_WINRT) && defined(_WIN32) |
| 53339 | SimulateIOError( return SQLITE_ERROR ); |
| 53340 | /* WinCE has no concept of a relative pathname, or so I am told. */ |
| 53341 | /* WinRT has no way to convert a relative path to an absolute one. */ |
| 53342 | if ( sqlite3_data_directory && !winIsVerbatimPathname(zRelative) ){ |
| 53343 | /* |
| @@ -53062,11 +53352,12 @@ | |
| 53352 | sqlite3_snprintf(MIN(nFull, pVfs->mxPathname), zFull, "%s", zRelative); |
| 53353 | } |
| 53354 | return SQLITE_OK; |
| 53355 | #endif |
| 53356 | |
| 53357 | #if !SQLITE_OS_WINCE && !SQLITE_OS_WINRT |
| 53358 | #if defined(_WIN32) |
| 53359 | /* It's odd to simulate an io-error here, but really this is just |
| 53360 | ** using the io-error infrastructure to test that SQLite handles this |
| 53361 | ** function failing. This function could fail if, for example, the |
| 53362 | ** current working directory has been unlinked. |
| 53363 | */ |
| @@ -53080,10 +53371,11 @@ | |
| 53371 | */ |
| 53372 | sqlite3_snprintf(MIN(nFull, pVfs->mxPathname), zFull, "%s%c%s", |
| 53373 | sqlite3_data_directory, winGetDirSep(), zRelative); |
| 53374 | return SQLITE_OK; |
| 53375 | } |
| 53376 | #endif |
| 53377 | zConverted = winConvertFromUtf8Filename(zRelative); |
| 53378 | if( zConverted==0 ){ |
| 53379 | return SQLITE_IOERR_NOMEM_BKPT; |
| 53380 | } |
| 53381 | if( osIsNT() ){ |
| @@ -53092,16 +53384,17 @@ | |
| 53384 | if( nByte==0 ){ |
| 53385 | sqlite3_free(zConverted); |
| 53386 | return winLogError(SQLITE_CANTOPEN_FULLPATH, osGetLastError(), |
| 53387 | "winFullPathname1", zRelative); |
| 53388 | } |
| 53389 | nByte += 3; |
| 53390 | zTemp = sqlite3MallocZero( nByte*sizeof(zTemp[0]) ); |
| 53391 | if( zTemp==0 ){ |
| 53392 | sqlite3_free(zConverted); |
| 53393 | return SQLITE_IOERR_NOMEM_BKPT; |
| 53394 | } |
| 53395 | nByte = osGetFullPathNameW((LPCWSTR)zConverted, nByte, zTemp, 0); |
| 53396 | if( nByte==0 ){ |
| 53397 | sqlite3_free(zConverted); |
| 53398 | sqlite3_free(zTemp); |
| 53399 | return winLogError(SQLITE_CANTOPEN_FULLPATH, osGetLastError(), |
| 53400 | "winFullPathname2", zRelative); |
| @@ -53135,11 +53428,30 @@ | |
| 53428 | zOut = winMbcsToUtf8(zTemp, osAreFileApisANSI()); |
| 53429 | sqlite3_free(zTemp); |
| 53430 | } |
| 53431 | #endif |
| 53432 | if( zOut ){ |
| 53433 | #ifdef __CYGWIN__ |
| 53434 | if( memcmp(zOut, "\\\\?\\", 4) ){ |
| 53435 | sqlite3_snprintf(MIN(nFull, pVfs->mxPathname), zFull, "%s", zOut); |
| 53436 | }else if( memcmp(zOut+4, "UNC\\", 4) ){ |
| 53437 | sqlite3_snprintf(MIN(nFull, pVfs->mxPathname), zFull, "%s", zOut+4); |
| 53438 | }else{ |
| 53439 | char *p = zOut+6; |
| 53440 | *p = '\\'; |
| 53441 | if( osGetcwd ){ |
| 53442 | /* On Cygwin, UNC paths use forward slashes */ |
| 53443 | while( *p ){ |
| 53444 | if( *p=='\\' ) *p = '/'; |
| 53445 | ++p; |
| 53446 | } |
| 53447 | } |
| 53448 | sqlite3_snprintf(MIN(nFull, pVfs->mxPathname), zFull, "%s", zOut+6); |
| 53449 | } |
| 53450 | #else |
| 53451 | sqlite3_snprintf(MIN(nFull, pVfs->mxPathname), zFull, "%s", zOut); |
| 53452 | #endif /* __CYGWIN__ */ |
| 53453 | sqlite3_free(zOut); |
| 53454 | return SQLITE_OK; |
| 53455 | }else{ |
| 53456 | return SQLITE_IOERR_NOMEM_BKPT; |
| 53457 | } |
| @@ -53165,11 +53477,13 @@ | |
| 53477 | ** Interfaces for opening a shared library, finding entry points |
| 53478 | ** within the shared library, and closing the shared library. |
| 53479 | */ |
| 53480 | static void *winDlOpen(sqlite3_vfs *pVfs, const char *zFilename){ |
| 53481 | HANDLE h; |
| 53482 | #if 0 /* This doesn't work correctly at all! See: |
| 53483 | <https://marc.info/?l=sqlite-users&m=139299149416314&w=2> |
| 53484 | */ |
| 53485 | int nFull = pVfs->mxPathname+1; |
| 53486 | char *zFull = sqlite3MallocZero( nFull ); |
| 53487 | void *zConverted = 0; |
| 53488 | if( zFull==0 ){ |
| 53489 | OSTRACE(("DLOPEN name=%s, handle=%p\n", zFilename, (void*)0)); |
| @@ -53532,11 +53846,11 @@ | |
| 53846 | }; |
| 53847 | #endif |
| 53848 | |
| 53849 | /* Double-check that the aSyscall[] array has been constructed |
| 53850 | ** correctly. See ticket [bb3a86e890c8e96ab] */ |
| 53851 | assert( ArraySize(aSyscall)==89 ); |
| 53852 | |
| 53853 | /* get memory map allocation granularity */ |
| 53854 | memset(&winSysInfo, 0, sizeof(SYSTEM_INFO)); |
| 53855 | #if SQLITE_OS_WINRT |
| 53856 | osGetNativeSystemInfo(&winSysInfo); |
| @@ -66508,14 +66822,12 @@ | |
| 66822 | s2 = aIn[1]; |
| 66823 | }else{ |
| 66824 | s1 = s2 = 0; |
| 66825 | } |
| 66826 | |
| 66827 | /* nByte is a multiple of 8 between 8 and 65536 */ |
| 66828 | assert( nByte>=8 && (nByte&7)==0 && nByte<=65536 ); |
| 66829 | |
| 66830 | if( !nativeCksum ){ |
| 66831 | do { |
| 66832 | s1 += BYTESWAP32(aData[0]) + s2; |
| 66833 | s2 += BYTESWAP32(aData[1]) + s1; |
| @@ -83726,11 +84038,11 @@ | |
| 84038 | ** many different strings can be converted into the same int or real. |
| 84039 | ** If a table contains a numeric value and an index is based on the |
| 84040 | ** corresponding string value, then it is important that the string be |
| 84041 | ** derived from the numeric value, not the other way around, to ensure |
| 84042 | ** that the index and table are consistent. See ticket |
| 84043 | ** https://sqlite.org/src/info/343634942dd54ab (2018-01-31) for |
| 84044 | ** an example. |
| 84045 | ** |
| 84046 | ** This routine looks at pMem to verify that if it has both a numeric |
| 84047 | ** representation and a string representation then the string rep has |
| 84048 | ** been derived from the numeric and not the other way around. It returns |
| @@ -93014,11 +93326,11 @@ | |
| 93326 | unsigned char enc |
| 93327 | ){ |
| 93328 | assert( xDel!=SQLITE_DYNAMIC ); |
| 93329 | if( enc!=SQLITE_UTF8 ){ |
| 93330 | if( enc==SQLITE_UTF16 ) enc = SQLITE_UTF16NATIVE; |
| 93331 | nData &= ~(u64)1; |
| 93332 | } |
| 93333 | return bindText(pStmt, i, zData, nData, xDel, enc); |
| 93334 | } |
| 93335 | #ifndef SQLITE_OMIT_UTF16 |
| 93336 | SQLITE_API int sqlite3_bind_text16( |
| @@ -125166,11 +125478,11 @@ | |
| 125478 | if( !isDupColumn(pIdx, pIdx->nKeyCol, pPk, i) ){ |
| 125479 | testcase( hasColumn(pIdx->aiColumn, pIdx->nKeyCol, pPk->aiColumn[i]) ); |
| 125480 | pIdx->aiColumn[j] = pPk->aiColumn[i]; |
| 125481 | pIdx->azColl[j] = pPk->azColl[i]; |
| 125482 | if( pPk->aSortOrder[i] ){ |
| 125483 | /* See ticket https://sqlite.org/src/info/bba7b69f9849b5bf */ |
| 125484 | pIdx->bAscKeyBug = 1; |
| 125485 | } |
| 125486 | j++; |
| 125487 | } |
| 125488 | } |
| @@ -126543,11 +126855,11 @@ | |
| 126855 | /* This OP_SeekEnd opcode makes index insert for a REINDEX go much |
| 126856 | ** faster by avoiding unnecessary seeks. But the optimization does |
| 126857 | ** not work for UNIQUE constraint indexes on WITHOUT ROWID tables |
| 126858 | ** with DESC primary keys, since those indexes have there keys in |
| 126859 | ** a different order from the main table. |
| 126860 | ** See ticket: https://sqlite.org/src/info/bba7b69f9849b5bf |
| 126861 | */ |
| 126862 | sqlite3VdbeAddOp1(v, OP_SeekEnd, iIdx); |
| 126863 | } |
| 126864 | sqlite3VdbeAddOp2(v, OP_IdxInsert, iIdx, regRecord); |
| 126865 | sqlite3VdbeChangeP5(v, OPFLAG_USESEEKRESULT); |
| @@ -126927,10 +127239,11 @@ | |
| 127239 | }else{ |
| 127240 | j = pCExpr->iColumn; |
| 127241 | assert( j<=0x7fff ); |
| 127242 | if( j<0 ){ |
| 127243 | j = pTab->iPKey; |
| 127244 | pIndex->bIdxRowid = 1; |
| 127245 | }else{ |
| 127246 | if( pTab->aCol[j].notNull==0 ){ |
| 127247 | pIndex->uniqNotNull = 0; |
| 127248 | } |
| 127249 | if( pTab->aCol[j].colFlags & COLFLAG_VIRTUAL ){ |
| @@ -136632,11 +136945,11 @@ | |
| 136945 | ** OE_Update guarantees that only a single row will change, so it |
| 136946 | ** must happen before OE_Replace. Technically, OE_Abort and OE_Rollback |
| 136947 | ** could happen in any order, but they are grouped up front for |
| 136948 | ** convenience. |
| 136949 | ** |
| 136950 | ** 2018-08-14: Ticket https://sqlite.org/src/info/908f001483982c43 |
| 136951 | ** The order of constraints used to have OE_Update as (2) and OE_Abort |
| 136952 | ** and so forth as (1). But apparently PostgreSQL checks the OE_Update |
| 136953 | ** constraint before any others, so it had to be moved. |
| 136954 | ** |
| 136955 | ** Constraint checking code is generated in this order: |
| @@ -147785,10 +148098,11 @@ | |
| 148098 | } |
| 148099 | |
| 148100 | multi_select_end: |
| 148101 | pDest->iSdst = dest.iSdst; |
| 148102 | pDest->nSdst = dest.nSdst; |
| 148103 | pDest->iSDParm2 = dest.iSDParm2; |
| 148104 | if( pDelete ){ |
| 148105 | sqlite3ParserAddCleanup(pParse, sqlite3SelectDeleteGeneric, pDelete); |
| 148106 | } |
| 148107 | return rc; |
| 148108 | } |
| @@ -149395,11 +149709,12 @@ | |
| 149709 | && pE2->iColumn==pColumn->iColumn |
| 149710 | ){ |
| 149711 | return; /* Already present. Return without doing anything. */ |
| 149712 | } |
| 149713 | } |
| 149714 | assert( SQLITE_AFF_NONE<SQLITE_AFF_BLOB ); |
| 149715 | if( sqlite3ExprAffinity(pColumn)<=SQLITE_AFF_BLOB ){ |
| 149716 | pConst->bHasAffBlob = 1; |
| 149717 | } |
| 149718 | |
| 149719 | pConst->nConst++; |
| 149720 | pConst->apExpr = sqlite3DbReallocOrFree(pConst->pParse->db, pConst->apExpr, |
| @@ -149470,11 +149785,12 @@ | |
| 149785 | for(i=0; i<pConst->nConst; i++){ |
| 149786 | Expr *pColumn = pConst->apExpr[i*2]; |
| 149787 | if( pColumn==pExpr ) continue; |
| 149788 | if( pColumn->iTable!=pExpr->iTable ) continue; |
| 149789 | if( pColumn->iColumn!=pExpr->iColumn ) continue; |
| 149790 | assert( SQLITE_AFF_NONE<SQLITE_AFF_BLOB ); |
| 149791 | if( bIgnoreAffBlob && sqlite3ExprAffinity(pColumn)<=SQLITE_AFF_BLOB ){ |
| 149792 | break; |
| 149793 | } |
| 149794 | /* A match is found. Add the EP_FixedCol property */ |
| 149795 | pConst->nChng++; |
| 149796 | ExprClearProperty(pExpr, EP_Leaf); |
| @@ -150123,11 +150439,11 @@ | |
| 150439 | ** |
| 150440 | ** This transformation is necessary because the multiSelectOrderBy() routine |
| 150441 | ** above that generates the code for a compound SELECT with an ORDER BY clause |
| 150442 | ** uses a merge algorithm that requires the same collating sequence on the |
| 150443 | ** result columns as on the ORDER BY clause. See ticket |
| 150444 | ** http://sqlite.org/src/info/6709574d2a |
| 150445 | ** |
| 150446 | ** This transformation is only needed for EXCEPT, INTERSECT, and UNION. |
| 150447 | ** The UNION ALL operator works fine with multiSelectOrderBy() even when |
| 150448 | ** there are COLLATE terms in the ORDER BY. |
| 150449 | */ |
| @@ -152654,10 +152970,16 @@ | |
| 152970 | pWInfo = sqlite3WhereBegin(pParse, pTabList, pWhere, sSort.pOrderBy, |
| 152971 | p->pEList, p, wctrlFlags, p->nSelectRow); |
| 152972 | if( pWInfo==0 ) goto select_end; |
| 152973 | if( sqlite3WhereOutputRowCount(pWInfo) < p->nSelectRow ){ |
| 152974 | p->nSelectRow = sqlite3WhereOutputRowCount(pWInfo); |
| 152975 | if( pDest->eDest<=SRT_DistQueue && pDest->eDest>=SRT_DistFifo ){ |
| 152976 | /* TUNING: For a UNION CTE, because UNION is implies DISTINCT, |
| 152977 | ** reduce the estimated output row count by 8 (LogEst 30). |
| 152978 | ** Search for tag-20250414a to see other cases */ |
| 152979 | p->nSelectRow -= 30; |
| 152980 | } |
| 152981 | } |
| 152982 | if( sDistinct.isTnct && sqlite3WhereIsDistinct(pWInfo) ){ |
| 152983 | sDistinct.eTnctType = sqlite3WhereIsDistinct(pWInfo); |
| 152984 | } |
| 152985 | if( sSort.pOrderBy ){ |
| @@ -156925,11 +157247,11 @@ | |
| 157247 | iDb = sqlite3TwoPartName(pParse, pNm, pNm, &pNm); |
| 157248 | if( iDb<0 ) goto build_vacuum_end; |
| 157249 | #else |
| 157250 | /* When SQLITE_BUG_COMPATIBLE_20160819 is defined, unrecognized arguments |
| 157251 | ** to VACUUM are silently ignored. This is a back-out of a bug fix that |
| 157252 | ** occurred on 2016-08-19 (https://sqlite.org/src/info/083f9e6270). |
| 157253 | ** The buggy behavior is required for binary compatibility with some |
| 157254 | ** legacy applications. */ |
| 157255 | iDb = sqlite3FindDb(pParse->db, pNm); |
| 157256 | if( iDb<0 ) iDb = 0; |
| 157257 | #endif |
| @@ -159820,11 +160142,11 @@ | |
| 160142 | |
| 160143 | |
| 160144 | /* |
| 160145 | ** pX is an expression of the form: (vector) IN (SELECT ...) |
| 160146 | ** In other words, it is a vector IN operator with a SELECT clause on the |
| 160147 | ** RHS. But not all terms in the vector are indexable and the terms might |
| 160148 | ** not be in the correct order for indexing. |
| 160149 | ** |
| 160150 | ** This routine makes a copy of the input pX expression and then adjusts |
| 160151 | ** the vector on the LHS with corresponding changes to the SELECT so that |
| 160152 | ** the vector contains only index terms and those terms are in the correct |
| @@ -161642,11 +161964,11 @@ | |
| 161964 | ** ON or USING clause of a LEFT JOIN, and terms that are usable as |
| 161965 | ** indices. |
| 161966 | ** |
| 161967 | ** This optimization also only applies if the (x1 OR x2 OR ...) term |
| 161968 | ** is not contained in the ON clause of a LEFT JOIN. |
| 161969 | ** See ticket http://sqlite.org/src/info/f2369304e4 |
| 161970 | ** |
| 161971 | ** 2022-02-04: Do not push down slices of a row-value comparison. |
| 161972 | ** In other words, "w" or "y" may not be a slice of a vector. Otherwise, |
| 161973 | ** the initialization of the right-hand operand of the vector comparison |
| 161974 | ** might not occur, or might occur only in an OR branch that is not |
| @@ -167615,11 +167937,11 @@ | |
| 167937 | } |
| 167938 | |
| 167939 | if( (pNew->wsFlags & WHERE_TOP_LIMIT)==0 |
| 167940 | && pNew->u.btree.nEq<pProbe->nColumn |
| 167941 | && (pNew->u.btree.nEq<pProbe->nKeyCol || |
| 167942 | (pProbe->idxType!=SQLITE_IDXTYPE_PRIMARYKEY && !pProbe->bIdxRowid)) |
| 167943 | ){ |
| 167944 | if( pNew->u.btree.nEq>3 ){ |
| 167945 | sqlite3ProgressCheck(pParse); |
| 167946 | } |
| 167947 | whereLoopAddBtreeIndex(pBuilder, pSrc, pProbe, nInMul+nIn); |
| @@ -171073,11 +171395,12 @@ | |
| 171395 | wherePathSolver(pWInfo, pWInfo->nRowOut<0 ? 1 : pWInfo->nRowOut+1); |
| 171396 | if( db->mallocFailed ) goto whereBeginError; |
| 171397 | } |
| 171398 | |
| 171399 | /* TUNING: Assume that a DISTINCT clause on a subquery reduces |
| 171400 | ** the output size by a factor of 8 (LogEst -30). Search for |
| 171401 | ** tag-20250414a to see other cases. |
| 171402 | */ |
| 171403 | if( (pWInfo->wctrlFlags & WHERE_WANT_DISTINCT)!=0 ){ |
| 171404 | WHERETRACE(0x0080,("nRowOut reduced from %d to %d due to DISTINCT\n", |
| 171405 | pWInfo->nRowOut, pWInfo->nRowOut-30)); |
| 171406 | pWInfo->nRowOut -= 30; |
| @@ -188065,10 +188388,17 @@ | |
| 188388 | ****************************************************************************** |
| 188389 | ** |
| 188390 | */ |
| 188391 | #ifndef _FTSINT_H |
| 188392 | #define _FTSINT_H |
| 188393 | |
| 188394 | /* #include <assert.h> */ |
| 188395 | /* #include <stdlib.h> */ |
| 188396 | /* #include <stddef.h> */ |
| 188397 | /* #include <stdio.h> */ |
| 188398 | /* #include <string.h> */ |
| 188399 | /* #include <stdarg.h> */ |
| 188400 | |
| 188401 | #if !defined(NDEBUG) && !defined(SQLITE_DEBUG) |
| 188402 | # define NDEBUG 1 |
| 188403 | #endif |
| 188404 | |
| @@ -189017,16 +189347,10 @@ | |
| 189347 | |
| 189348 | #if defined(SQLITE_ENABLE_FTS3) && !defined(SQLITE_CORE) |
| 189349 | # define SQLITE_CORE 1 |
| 189350 | #endif |
| 189351 | |
| 189352 | |
| 189353 | /* #include "fts3.h" */ |
| 189354 | #ifndef SQLITE_CORE |
| 189355 | /* # include "sqlite3ext.h" */ |
| 189356 | SQLITE_EXTENSION_INIT1 |
| @@ -198969,11 +199293,11 @@ | |
| 199293 | UNUSED_PARAMETER(nVal); |
| 199294 | |
| 199295 | fts3tokResetCursor(pCsr); |
| 199296 | if( idxNum==1 ){ |
| 199297 | const char *zByte = (const char *)sqlite3_value_text(apVal[0]); |
| 199298 | sqlite3_int64 nByte = sqlite3_value_bytes(apVal[0]); |
| 199299 | pCsr->zInput = sqlite3_malloc64(nByte+1); |
| 199300 | if( pCsr->zInput==0 ){ |
| 199301 | rc = SQLITE_NOMEM; |
| 199302 | }else{ |
| 199303 | if( nByte>0 ) memcpy(pCsr->zInput, zByte, nByte); |
| @@ -205949,20 +206273,20 @@ | |
| 206273 | case FTS3_MATCHINFO_LCS: |
| 206274 | nVal = pInfo->nCol; |
| 206275 | break; |
| 206276 | |
| 206277 | case FTS3_MATCHINFO_LHITS: |
| 206278 | nVal = (size_t)pInfo->nCol * pInfo->nPhrase; |
| 206279 | break; |
| 206280 | |
| 206281 | case FTS3_MATCHINFO_LHITS_BM: |
| 206282 | nVal = (size_t)pInfo->nPhrase * ((pInfo->nCol + 31) / 32); |
| 206283 | break; |
| 206284 | |
| 206285 | default: |
| 206286 | assert( cArg==FTS3_MATCHINFO_HITS ); |
| 206287 | nVal = (size_t)pInfo->nCol * pInfo->nPhrase * 3; |
| 206288 | break; |
| 206289 | } |
| 206290 | |
| 206291 | return nVal; |
| 206292 | } |
| @@ -207516,12 +207840,12 @@ | |
| 207840 | ** with JSON-5 extensions is accepted as input. |
| 207841 | ** |
| 207842 | ** Beginning with version 3.45.0 (circa 2024-01-01), these routines also |
| 207843 | ** accept BLOB values that have JSON encoded using a binary representation |
| 207844 | ** called "JSONB". The name JSONB comes from PostgreSQL, however the on-disk |
| 207845 | ** format for SQLite-JSONB is completely different and incompatible with |
| 207846 | ** PostgreSQL-JSONB. |
| 207847 | ** |
| 207848 | ** Decoding and interpreting JSONB is still O(N) where N is the size of |
| 207849 | ** the input, the same as text JSON. However, the constant of proportionality |
| 207850 | ** for JSONB is much smaller due to faster parsing. The size of each |
| 207851 | ** element in JSONB is encoded in its header, so there is no need to search |
| @@ -207574,21 +207898,21 @@ | |
| 207898 | ** 14 4 byte (0-4294967295) 5 |
| 207899 | ** 15 8 byte (0-1.8e19) 9 |
| 207900 | ** |
| 207901 | ** The payload size need not be expressed in its minimal form. For example, |
| 207902 | ** if the payload size is 10, the size can be expressed in any of 5 different |
| 207903 | ** ways: (1) (X>>4)==10, (2) (X>>4)==12 following by one 0x0a byte, |
| 207904 | ** (3) (X>>4)==13 followed by 0x00 and 0x0a, (4) (X>>4)==14 followed by |
| 207905 | ** 0x00 0x00 0x00 0x0a, or (5) (X>>4)==15 followed by 7 bytes of 0x00 and |
| 207906 | ** a single byte of 0x0a. The shorter forms are preferred, of course, but |
| 207907 | ** sometimes when generating JSONB, the payload size is not known in advance |
| 207908 | ** and it is convenient to reserve sufficient header space to cover the |
| 207909 | ** largest possible payload size and then come back later and patch up |
| 207910 | ** the size when it becomes known, resulting in a non-minimal encoding. |
| 207911 | ** |
| 207912 | ** The value (X>>4)==15 is not actually used in the current implementation |
| 207913 | ** (as SQLite is currently unable to handle BLOBs larger than about 2GB) |
| 207914 | ** but is included in the design to allow for future enhancements. |
| 207915 | ** |
| 207916 | ** The payload follows the header. NULL, TRUE, and FALSE have no payload and |
| 207917 | ** their payload size must always be zero. The payload for INT, INT5, |
| 207918 | ** FLOAT, FLOAT5, TEXT, TEXTJ, TEXT5, and TEXTROW is text. Note that the |
| @@ -208658,11 +208982,11 @@ | |
| 208982 | if( jsonBlobExpand(pParse, pParse->nBlob+szPayload+9) ) return; |
| 208983 | jsonBlobAppendNode(pParse, eType, szPayload, aPayload); |
| 208984 | } |
| 208985 | |
| 208986 | |
| 208987 | /* Append a node type byte together with the payload size and |
| 208988 | ** possibly also the payload. |
| 208989 | ** |
| 208990 | ** If aPayload is not NULL, then it is a pointer to the payload which |
| 208991 | ** is also appended. If aPayload is NULL, the pParse->aBlob[] array |
| 208992 | ** is resized (if necessary) so that it is big enough to hold the |
| @@ -209992,10 +210316,86 @@ | |
| 210316 | (void)jsonbPayloadSize(pParse, iRoot, &sz); |
| 210317 | pParse->nBlob = nBlob; |
| 210318 | sz += pParse->delta; |
| 210319 | pParse->delta += jsonBlobChangePayloadSize(pParse, iRoot, sz); |
| 210320 | } |
| 210321 | |
| 210322 | /* |
| 210323 | ** If the JSONB at aIns[0..nIns-1] can be expanded (by denormalizing the |
| 210324 | ** size field) by d bytes, then write the expansion into aOut[] and |
| 210325 | ** return true. In this way, an overwrite happens without changing the |
| 210326 | ** size of the JSONB, which reduces memcpy() operations and also make it |
| 210327 | ** faster and easier to update the B-Tree entry that contains the JSONB |
| 210328 | ** in the database. |
| 210329 | ** |
| 210330 | ** If the expansion of aIns[] by d bytes cannot be (easily) accomplished |
| 210331 | ** then return false. |
| 210332 | ** |
| 210333 | ** The d parameter is guaranteed to be between 1 and 8. |
| 210334 | ** |
| 210335 | ** This routine is an optimization. A correct answer is obtained if it |
| 210336 | ** always leaves the output unchanged and returns false. |
| 210337 | */ |
| 210338 | static int jsonBlobOverwrite( |
| 210339 | u8 *aOut, /* Overwrite here */ |
| 210340 | const u8 *aIns, /* New content */ |
| 210341 | u32 nIns, /* Bytes of new content */ |
| 210342 | u32 d /* Need to expand new content by this much */ |
| 210343 | ){ |
| 210344 | u32 szPayload; /* Bytes of payload */ |
| 210345 | u32 i; /* New header size, after expansion & a loop counter */ |
| 210346 | u8 szHdr; /* Size of header before expansion */ |
| 210347 | |
| 210348 | /* Lookup table for finding the upper 4 bits of the first byte of the |
| 210349 | ** expanded aIns[], based on the size of the expanded aIns[] header: |
| 210350 | ** |
| 210351 | ** 2 3 4 5 6 7 8 9 */ |
| 210352 | static const u8 aType[] = { 0xc0, 0xd0, 0, 0xe0, 0, 0, 0, 0xf0 }; |
| 210353 | |
| 210354 | if( (aIns[0]&0x0f)<=2 ) return 0; /* Cannot enlarge NULL, true, false */ |
| 210355 | switch( aIns[0]>>4 ){ |
| 210356 | default: { /* aIns[] header size 1 */ |
| 210357 | if( ((1<<d)&0x116)==0 ) return 0; /* d must be 1, 2, 4, or 8 */ |
| 210358 | i = d + 1; /* New hdr sz: 2, 3, 5, or 9 */ |
| 210359 | szHdr = 1; |
| 210360 | break; |
| 210361 | } |
| 210362 | case 12: { /* aIns[] header size is 2 */ |
| 210363 | if( ((1<<d)&0x8a)==0) return 0; /* d must be 1, 3, or 7 */ |
| 210364 | i = d + 2; /* New hdr sz: 2, 5, or 9 */ |
| 210365 | szHdr = 2; |
| 210366 | break; |
| 210367 | } |
| 210368 | case 13: { /* aIns[] header size is 3 */ |
| 210369 | if( d!=2 && d!=6 ) return 0; /* d must be 2 or 6 */ |
| 210370 | i = d + 3; /* New hdr sz: 5 or 9 */ |
| 210371 | szHdr = 3; |
| 210372 | break; |
| 210373 | } |
| 210374 | case 14: { /* aIns[] header size is 5 */ |
| 210375 | if( d!=4 ) return 0; /* d must be 4 */ |
| 210376 | i = 9; /* New hdr sz: 9 */ |
| 210377 | szHdr = 5; |
| 210378 | break; |
| 210379 | } |
| 210380 | case 15: { /* aIns[] header size is 9 */ |
| 210381 | return 0; /* No solution */ |
| 210382 | } |
| 210383 | } |
| 210384 | assert( i>=2 && i<=9 && aType[i-2]!=0 ); |
| 210385 | aOut[0] = (aIns[0] & 0x0f) | aType[i-2]; |
| 210386 | memcpy(&aOut[i], &aIns[szHdr], nIns-szHdr); |
| 210387 | szPayload = nIns - szHdr; |
| 210388 | while( 1/*edit-by-break*/ ){ |
| 210389 | i--; |
| 210390 | aOut[i] = szPayload & 0xff; |
| 210391 | if( i==1 ) break; |
| 210392 | szPayload >>= 8; |
| 210393 | } |
| 210394 | assert( (szPayload>>8)==0 ); |
| 210395 | return 1; |
| 210396 | } |
| 210397 | |
| 210398 | /* |
| 210399 | ** Modify the JSONB blob at pParse->aBlob by removing nDel bytes of |
| 210400 | ** content beginning at iDel, and replacing them with nIns bytes of |
| 210401 | ** content given by aIns. |
| @@ -210014,10 +210414,15 @@ | |
| 210414 | u32 nDel, /* Number of bytes to remove */ |
| 210415 | const u8 *aIns, /* Content to insert */ |
| 210416 | u32 nIns /* Bytes of content to insert */ |
| 210417 | ){ |
| 210418 | i64 d = (i64)nIns - (i64)nDel; |
| 210419 | if( d<0 && d>=(-8) && aIns!=0 |
| 210420 | && jsonBlobOverwrite(&pParse->aBlob[iDel], aIns, nIns, (int)-d) |
| 210421 | ){ |
| 210422 | return; |
| 210423 | } |
| 210424 | if( d!=0 ){ |
| 210425 | if( pParse->nBlob + d > pParse->nBlobAlloc ){ |
| 210426 | jsonBlobExpand(pParse, pParse->nBlob+d); |
| 210427 | if( pParse->oom ) return; |
| 210428 | } |
| @@ -210025,11 +210430,13 @@ | |
| 210430 | &pParse->aBlob[iDel+nDel], |
| 210431 | pParse->nBlob - (iDel+nDel)); |
| 210432 | pParse->nBlob += d; |
| 210433 | pParse->delta += d; |
| 210434 | } |
| 210435 | if( nIns && aIns ){ |
| 210436 | memcpy(&pParse->aBlob[iDel], aIns, nIns); |
| 210437 | } |
| 210438 | } |
| 210439 | |
| 210440 | /* |
| 210441 | ** Return the number of escaped newlines to be ignored. |
| 210442 | ** An escaped newline is a one of the following byte sequences: |
| @@ -210788,11 +211195,11 @@ | |
| 211195 | } |
| 211196 | return 0; |
| 211197 | } |
| 211198 | |
| 211199 | /* argv[0] is a BLOB that seems likely to be a JSONB. Subsequent |
| 211200 | ** arguments come in pairs where each pair contains a JSON path and |
| 211201 | ** content to insert or set at that patch. Do the updates |
| 211202 | ** and return the result. |
| 211203 | ** |
| 211204 | ** The specific operation is determined by eEdit, which can be one |
| 211205 | ** of JEDIT_INS, JEDIT_REPL, or JEDIT_SET. |
| @@ -227533,12 +227940,12 @@ | |
| 227940 | ){ |
| 227941 | if( sqlite3_value_type(argv[3])==SQLITE_NULL && isInsert && pgno>1 ){ |
| 227942 | /* "INSERT INTO dbpage($PGNO,NULL)" causes page number $PGNO and |
| 227943 | ** all subsequent pages to be deleted. */ |
| 227944 | pTab->iDbTrunc = iDb; |
| 227945 | pTab->pgnoTrunc = pgno-1; |
| 227946 | pgno = 1; |
| 227947 | }else{ |
| 227948 | zErr = "bad page value"; |
| 227949 | goto update_fail; |
| 227950 | } |
| 227951 | } |
| @@ -228850,10 +229257,12 @@ | |
| 229257 | int rc = SQLITE_OK; |
| 229258 | |
| 229259 | if( pTab->nCol==0 ){ |
| 229260 | u8 *abPK; |
| 229261 | assert( pTab->azCol==0 || pTab->abPK==0 ); |
| 229262 | sqlite3_free(pTab->azCol); |
| 229263 | pTab->abPK = 0; |
| 229264 | rc = sessionTableInfo(pSession, db, zDb, |
| 229265 | pTab->zName, &pTab->nCol, &pTab->nTotalCol, 0, &pTab->azCol, |
| 229266 | &pTab->azDflt, &pTab->aiIdx, &abPK, |
| 229267 | ((pSession==0 || pSession->bImplicitPK) ? &pTab->bRowid : 0) |
| 229268 | ); |
| @@ -229857,11 +230266,13 @@ | |
| 230266 | char *zExpr = 0; |
| 230267 | sqlite3 *db = pSession->db; |
| 230268 | SessionTable *pTo; /* Table zTbl */ |
| 230269 | |
| 230270 | /* Locate and if necessary initialize the target table object */ |
| 230271 | pSession->bAutoAttach++; |
| 230272 | rc = sessionFindTable(pSession, zTbl, &pTo); |
| 230273 | pSession->bAutoAttach--; |
| 230274 | if( pTo==0 ) goto diff_out; |
| 230275 | if( sessionInitTable(pSession, pTo, pSession->db, pSession->zDb) ){ |
| 230276 | rc = pSession->rc; |
| 230277 | goto diff_out; |
| 230278 | } |
| @@ -229868,21 +230279,47 @@ | |
| 230279 | |
| 230280 | /* Check the table schemas match */ |
| 230281 | if( rc==SQLITE_OK ){ |
| 230282 | int bHasPk = 0; |
| 230283 | int bMismatch = 0; |
| 230284 | int nCol = 0; /* Columns in zFrom.zTbl */ |
| 230285 | int bRowid = 0; |
| 230286 | u8 *abPK = 0; |
| 230287 | const char **azCol = 0; |
| 230288 | char *zDbExists = 0; |
| 230289 | |
| 230290 | /* Check that database zFrom is attached. */ |
| 230291 | zDbExists = sqlite3_mprintf("SELECT * FROM %Q.sqlite_schema", zFrom); |
| 230292 | if( zDbExists==0 ){ |
| 230293 | rc = SQLITE_NOMEM; |
| 230294 | }else{ |
| 230295 | sqlite3_stmt *pDbExists = 0; |
| 230296 | rc = sqlite3_prepare_v2(db, zDbExists, -1, &pDbExists, 0); |
| 230297 | if( rc==SQLITE_ERROR ){ |
| 230298 | rc = SQLITE_OK; |
| 230299 | nCol = -1; |
| 230300 | } |
| 230301 | sqlite3_finalize(pDbExists); |
| 230302 | sqlite3_free(zDbExists); |
| 230303 | } |
| 230304 | |
| 230305 | if( rc==SQLITE_OK && nCol==0 ){ |
| 230306 | rc = sessionTableInfo(0, db, zFrom, zTbl, |
| 230307 | &nCol, 0, 0, &azCol, 0, 0, &abPK, |
| 230308 | pSession->bImplicitPK ? &bRowid : 0 |
| 230309 | ); |
| 230310 | } |
| 230311 | if( rc==SQLITE_OK ){ |
| 230312 | if( pTo->nCol!=nCol ){ |
| 230313 | if( nCol<=0 ){ |
| 230314 | rc = SQLITE_SCHEMA; |
| 230315 | if( pzErrMsg ){ |
| 230316 | *pzErrMsg = sqlite3_mprintf("no such table: %s.%s", zFrom, zTbl); |
| 230317 | } |
| 230318 | }else{ |
| 230319 | bMismatch = 1; |
| 230320 | } |
| 230321 | }else{ |
| 230322 | int i; |
| 230323 | for(i=0; i<nCol; i++){ |
| 230324 | if( pTo->abPK[i]!=abPK[i] ) bMismatch = 1; |
| 230325 | if( sqlite3_stricmp(azCol[i], pTo->azCol[i]) ) bMismatch = 1; |
| @@ -241869,11 +242306,12 @@ | |
| 242306 | sqlite3Fts5ParseError( |
| 242307 | pParse, "expected integer, got \"%.*s\"", p->n, p->p |
| 242308 | ); |
| 242309 | return; |
| 242310 | } |
| 242311 | if( nNear<214748363 ) nNear = nNear * 10 + (p->p[i] - '0'); |
| 242312 | /* ^^^^^^^^^^^^^^^--- Prevent integer overflow */ |
| 242313 | } |
| 242314 | }else{ |
| 242315 | nNear = FTS5_DEFAULT_NEARDIST; |
| 242316 | } |
| 242317 | pNear->nNear = nNear; |
| @@ -256775,11 +257213,11 @@ | |
| 257213 | int nArg, /* Number of args */ |
| 257214 | sqlite3_value **apUnused /* Function arguments */ |
| 257215 | ){ |
| 257216 | assert( nArg==0 ); |
| 257217 | UNUSED_PARAM2(nArg, apUnused); |
| 257218 | sqlite3_result_text(pCtx, "fts5: 2025-04-15 21:59:38 d22475b81c4e26ccc50f3b5626d43b32f7a2de34e5a764539554665bdda735d5", -1, SQLITE_TRANSIENT); |
| 257219 | } |
| 257220 | |
| 257221 | /* |
| 257222 | ** Implementation of fts5_locale(LOCALE, TEXT) function. |
| 257223 | ** |
| @@ -260838,11 +261276,10 @@ | |
| 261276 | } |
| 261277 | iTbl++; |
| 261278 | } |
| 261279 | aAscii[0] = 0; /* 0x00 is never a token character */ |
| 261280 | } |
| 261281 | |
| 261282 | /* |
| 261283 | ** 2015 May 30 |
| 261284 | ** |
| 261285 | ** The author disclaims copyright to this source code. In place of |
| 261286 |
+5
-4
| --- extsrc/sqlite3.h | ||
| +++ extsrc/sqlite3.h | ||
| @@ -131,11 +131,11 @@ | ||
| 131 | 131 | ** be held constant and Z will be incremented or else Y will be incremented |
| 132 | 132 | ** and Z will be reset to zero. |
| 133 | 133 | ** |
| 134 | 134 | ** Since [version 3.6.18] ([dateof:3.6.18]), |
| 135 | 135 | ** SQLite source code has been stored in the |
| 136 | -** <a href="http://www.fossil-scm.org/">Fossil configuration management | |
| 136 | +** <a href="http://fossil-scm.org/">Fossil configuration management | |
| 137 | 137 | ** system</a>. ^The SQLITE_SOURCE_ID macro evaluates to |
| 138 | 138 | ** a string which identifies a particular check-in of SQLite |
| 139 | 139 | ** within its configuration management system. ^The SQLITE_SOURCE_ID |
| 140 | 140 | ** string contains the date and time of the check-in (UTC) and a SHA1 |
| 141 | 141 | ** or SHA3-256 hash of the entire source tree. If the source code has |
| @@ -146,11 +146,11 @@ | ||
| 146 | 146 | ** [sqlite3_libversion_number()], [sqlite3_sourceid()], |
| 147 | 147 | ** [sqlite_version()] and [sqlite_source_id()]. |
| 148 | 148 | */ |
| 149 | 149 | #define SQLITE_VERSION "3.50.0" |
| 150 | 150 | #define SQLITE_VERSION_NUMBER 3050000 |
| 151 | -#define SQLITE_SOURCE_ID "2025-03-16 00:13:29 18bda13e197e4b4ec7464b3e70012f71edc05f73d8b14bb48bad452f81c7e185" | |
| 151 | +#define SQLITE_SOURCE_ID "2025-04-15 21:59:38 d22475b81c4e26ccc50f3b5626d43b32f7a2de34e5a764539554665bdda735d5" | |
| 152 | 152 | |
| 153 | 153 | /* |
| 154 | 154 | ** CAPI3REF: Run-Time Library Version Numbers |
| 155 | 155 | ** KEYWORDS: sqlite3_version sqlite3_sourceid |
| 156 | 156 | ** |
| @@ -11627,12 +11627,13 @@ | ||
| 11627 | 11627 | ** To clarify, if this function is called and then a changeset constructed |
| 11628 | 11628 | ** using [sqlite3session_changeset()], then after applying that changeset to |
| 11629 | 11629 | ** database zFrom the contents of the two compatible tables would be |
| 11630 | 11630 | ** identical. |
| 11631 | 11631 | ** |
| 11632 | -** It an error if database zFrom does not exist or does not contain the | |
| 11633 | -** required compatible table. | |
| 11632 | +** Unless the call to this function is a no-op as described above, it is an | |
| 11633 | +** error if database zFrom does not exist or does not contain the required | |
| 11634 | +** compatible table. | |
| 11634 | 11635 | ** |
| 11635 | 11636 | ** If the operation is successful, SQLITE_OK is returned. Otherwise, an SQLite |
| 11636 | 11637 | ** error code. In this case, if argument pzErrMsg is not NULL, *pzErrMsg |
| 11637 | 11638 | ** may be set to point to a buffer containing an English language error |
| 11638 | 11639 | ** message. It is the responsibility of the caller to free this buffer using |
| 11639 | 11640 |
| --- extsrc/sqlite3.h | |
| +++ extsrc/sqlite3.h | |
| @@ -131,11 +131,11 @@ | |
| 131 | ** be held constant and Z will be incremented or else Y will be incremented |
| 132 | ** and Z will be reset to zero. |
| 133 | ** |
| 134 | ** Since [version 3.6.18] ([dateof:3.6.18]), |
| 135 | ** SQLite source code has been stored in the |
| 136 | ** <a href="http://www.fossil-scm.org/">Fossil configuration management |
| 137 | ** system</a>. ^The SQLITE_SOURCE_ID macro evaluates to |
| 138 | ** a string which identifies a particular check-in of SQLite |
| 139 | ** within its configuration management system. ^The SQLITE_SOURCE_ID |
| 140 | ** string contains the date and time of the check-in (UTC) and a SHA1 |
| 141 | ** or SHA3-256 hash of the entire source tree. If the source code has |
| @@ -146,11 +146,11 @@ | |
| 146 | ** [sqlite3_libversion_number()], [sqlite3_sourceid()], |
| 147 | ** [sqlite_version()] and [sqlite_source_id()]. |
| 148 | */ |
| 149 | #define SQLITE_VERSION "3.50.0" |
| 150 | #define SQLITE_VERSION_NUMBER 3050000 |
| 151 | #define SQLITE_SOURCE_ID "2025-03-16 00:13:29 18bda13e197e4b4ec7464b3e70012f71edc05f73d8b14bb48bad452f81c7e185" |
| 152 | |
| 153 | /* |
| 154 | ** CAPI3REF: Run-Time Library Version Numbers |
| 155 | ** KEYWORDS: sqlite3_version sqlite3_sourceid |
| 156 | ** |
| @@ -11627,12 +11627,13 @@ | |
| 11627 | ** To clarify, if this function is called and then a changeset constructed |
| 11628 | ** using [sqlite3session_changeset()], then after applying that changeset to |
| 11629 | ** database zFrom the contents of the two compatible tables would be |
| 11630 | ** identical. |
| 11631 | ** |
| 11632 | ** It an error if database zFrom does not exist or does not contain the |
| 11633 | ** required compatible table. |
| 11634 | ** |
| 11635 | ** If the operation is successful, SQLITE_OK is returned. Otherwise, an SQLite |
| 11636 | ** error code. In this case, if argument pzErrMsg is not NULL, *pzErrMsg |
| 11637 | ** may be set to point to a buffer containing an English language error |
| 11638 | ** message. It is the responsibility of the caller to free this buffer using |
| 11639 |
| --- extsrc/sqlite3.h | |
| +++ extsrc/sqlite3.h | |
| @@ -131,11 +131,11 @@ | |
| 131 | ** be held constant and Z will be incremented or else Y will be incremented |
| 132 | ** and Z will be reset to zero. |
| 133 | ** |
| 134 | ** Since [version 3.6.18] ([dateof:3.6.18]), |
| 135 | ** SQLite source code has been stored in the |
| 136 | ** <a href="http://fossil-scm.org/">Fossil configuration management |
| 137 | ** system</a>. ^The SQLITE_SOURCE_ID macro evaluates to |
| 138 | ** a string which identifies a particular check-in of SQLite |
| 139 | ** within its configuration management system. ^The SQLITE_SOURCE_ID |
| 140 | ** string contains the date and time of the check-in (UTC) and a SHA1 |
| 141 | ** or SHA3-256 hash of the entire source tree. If the source code has |
| @@ -146,11 +146,11 @@ | |
| 146 | ** [sqlite3_libversion_number()], [sqlite3_sourceid()], |
| 147 | ** [sqlite_version()] and [sqlite_source_id()]. |
| 148 | */ |
| 149 | #define SQLITE_VERSION "3.50.0" |
| 150 | #define SQLITE_VERSION_NUMBER 3050000 |
| 151 | #define SQLITE_SOURCE_ID "2025-04-15 21:59:38 d22475b81c4e26ccc50f3b5626d43b32f7a2de34e5a764539554665bdda735d5" |
| 152 | |
| 153 | /* |
| 154 | ** CAPI3REF: Run-Time Library Version Numbers |
| 155 | ** KEYWORDS: sqlite3_version sqlite3_sourceid |
| 156 | ** |
| @@ -11627,12 +11627,13 @@ | |
| 11627 | ** To clarify, if this function is called and then a changeset constructed |
| 11628 | ** using [sqlite3session_changeset()], then after applying that changeset to |
| 11629 | ** database zFrom the contents of the two compatible tables would be |
| 11630 | ** identical. |
| 11631 | ** |
| 11632 | ** Unless the call to this function is a no-op as described above, it is an |
| 11633 | ** error if database zFrom does not exist or does not contain the required |
| 11634 | ** compatible table. |
| 11635 | ** |
| 11636 | ** If the operation is successful, SQLITE_OK is returned. Otherwise, an SQLite |
| 11637 | ** error code. In this case, if argument pzErrMsg is not NULL, *pzErrMsg |
| 11638 | ** may be set to point to a buffer containing an English language error |
| 11639 | ** message. It is the responsibility of the caller to free this buffer using |
| 11640 |
+1
-1
| --- skins/blitz/footer.txt | ||
| +++ skins/blitz/footer.txt | ||
| @@ -1,10 +1,10 @@ | ||
| 1 | 1 | </div> <!-- end div container --> |
| 2 | 2 | </div> <!-- end div middle max-full-width --> |
| 3 | 3 | <footer> |
| 4 | 4 | <div class="container"> |
| 5 | 5 | <div class="pull-right"> |
| 6 | - <a href="https://www.fossil-scm.org/">Fossil $release_version $manifest_version $manifest_date</a> | |
| 6 | + <a href="https://fossil-scm.org/">Fossil $release_version $manifest_version $manifest_date</a> | |
| 7 | 7 | </div> |
| 8 | 8 | This page was generated in about <th1>puts [expr {([utime]+[stime]+1000)/1000*0.001}]</th1>s |
| 9 | 9 | </div> |
| 10 | 10 | </footer> |
| 11 | 11 |
| --- skins/blitz/footer.txt | |
| +++ skins/blitz/footer.txt | |
| @@ -1,10 +1,10 @@ | |
| 1 | </div> <!-- end div container --> |
| 2 | </div> <!-- end div middle max-full-width --> |
| 3 | <footer> |
| 4 | <div class="container"> |
| 5 | <div class="pull-right"> |
| 6 | <a href="https://www.fossil-scm.org/">Fossil $release_version $manifest_version $manifest_date</a> |
| 7 | </div> |
| 8 | This page was generated in about <th1>puts [expr {([utime]+[stime]+1000)/1000*0.001}]</th1>s |
| 9 | </div> |
| 10 | </footer> |
| 11 |
| --- skins/blitz/footer.txt | |
| +++ skins/blitz/footer.txt | |
| @@ -1,10 +1,10 @@ | |
| 1 | </div> <!-- end div container --> |
| 2 | </div> <!-- end div middle max-full-width --> |
| 3 | <footer> |
| 4 | <div class="container"> |
| 5 | <div class="pull-right"> |
| 6 | <a href="https://fossil-scm.org/">Fossil $release_version $manifest_version $manifest_date</a> |
| 7 | </div> |
| 8 | This page was generated in about <th1>puts [expr {([utime]+[stime]+1000)/1000*0.001}]</th1>s |
| 9 | </div> |
| 10 | </footer> |
| 11 |
+1
-1
| --- skins/darkmode/footer.txt | ||
| +++ skins/darkmode/footer.txt | ||
| @@ -1,8 +1,8 @@ | ||
| 1 | 1 | <footer> |
| 2 | 2 | <div class="container"> |
| 3 | 3 | <div class="pull-right"> |
| 4 | - <a href="https://www.fossil-scm.org/">Fossil $release_version $manifest_version $manifest_date</a> | |
| 4 | + <a href="https://fossil-scm.org/">Fossil $release_version $manifest_version $manifest_date</a> | |
| 5 | 5 | </div> |
| 6 | 6 | This page was generated in about <th1>puts [expr {([utime]+[stime]+1000)/1000*0.001}]</th1>s |
| 7 | 7 | </div> |
| 8 | 8 | </footer> |
| 9 | 9 |
| --- skins/darkmode/footer.txt | |
| +++ skins/darkmode/footer.txt | |
| @@ -1,8 +1,8 @@ | |
| 1 | <footer> |
| 2 | <div class="container"> |
| 3 | <div class="pull-right"> |
| 4 | <a href="https://www.fossil-scm.org/">Fossil $release_version $manifest_version $manifest_date</a> |
| 5 | </div> |
| 6 | This page was generated in about <th1>puts [expr {([utime]+[stime]+1000)/1000*0.001}]</th1>s |
| 7 | </div> |
| 8 | </footer> |
| 9 |
| --- skins/darkmode/footer.txt | |
| +++ skins/darkmode/footer.txt | |
| @@ -1,8 +1,8 @@ | |
| 1 | <footer> |
| 2 | <div class="container"> |
| 3 | <div class="pull-right"> |
| 4 | <a href="https://fossil-scm.org/">Fossil $release_version $manifest_version $manifest_date</a> |
| 5 | </div> |
| 6 | This page was generated in about <th1>puts [expr {([utime]+[stime]+1000)/1000*0.001}]</th1>s |
| 7 | </div> |
| 8 | </footer> |
| 9 |
+1
-1
| --- skins/default/header.txt | ||
| +++ skins/default/header.txt | ||
| @@ -28,11 +28,11 @@ | ||
| 28 | 28 | return $logourl |
| 29 | 29 | } |
| 30 | 30 | set logourl [getLogoUrl $baseurl] |
| 31 | 31 | </th1> |
| 32 | 32 | <a href="$logourl"> |
| 33 | - <img src="$logo_image_url" border="0" alt="$project_name"> | |
| 33 | + <img src="$logo_image_url" border="0" alt="$<project_name>"> | |
| 34 | 34 | </a> |
| 35 | 35 | </div> |
| 36 | 36 | <div class="title"> |
| 37 | 37 | <h1>$<project_name></h1> |
| 38 | 38 | <span class="page-title">$<title></span> |
| 39 | 39 |
| --- skins/default/header.txt | |
| +++ skins/default/header.txt | |
| @@ -28,11 +28,11 @@ | |
| 28 | return $logourl |
| 29 | } |
| 30 | set logourl [getLogoUrl $baseurl] |
| 31 | </th1> |
| 32 | <a href="$logourl"> |
| 33 | <img src="$logo_image_url" border="0" alt="$project_name"> |
| 34 | </a> |
| 35 | </div> |
| 36 | <div class="title"> |
| 37 | <h1>$<project_name></h1> |
| 38 | <span class="page-title">$<title></span> |
| 39 |
| --- skins/default/header.txt | |
| +++ skins/default/header.txt | |
| @@ -28,11 +28,11 @@ | |
| 28 | return $logourl |
| 29 | } |
| 30 | set logourl [getLogoUrl $baseurl] |
| 31 | </th1> |
| 32 | <a href="$logourl"> |
| 33 | <img src="$logo_image_url" border="0" alt="$<project_name>"> |
| 34 | </a> |
| 35 | </div> |
| 36 | <div class="title"> |
| 37 | <h1>$<project_name></h1> |
| 38 | <span class="page-title">$<title></span> |
| 39 |
+1
-1
| --- skins/eagle/footer.txt | ||
| +++ skins/eagle/footer.txt | ||
| @@ -10,11 +10,11 @@ | ||
| 10 | 10 | set length [string length $version] |
| 11 | 11 | return [string range $version 1 [expr {$length - 2}]] |
| 12 | 12 | } |
| 13 | 13 | set version [getVersion $manifest_version] |
| 14 | 14 | set tclVersion [getTclVersion] |
| 15 | - set fossilUrl https://www.fossil-scm.org | |
| 15 | + set fossilUrl https://fossil-scm.org | |
| 16 | 16 | set fossilDate [string range $manifest_date 0 9]T[string range $manifest_date 11 end] |
| 17 | 17 | </th1> |
| 18 | 18 | This page was generated in about |
| 19 | 19 | <th1>puts [expr {([utime]+[stime]+1000)/1000*0.001}]</th1>s by |
| 20 | 20 | <a href="$fossilUrl/">Fossil</a> |
| 21 | 21 |
| --- skins/eagle/footer.txt | |
| +++ skins/eagle/footer.txt | |
| @@ -10,11 +10,11 @@ | |
| 10 | set length [string length $version] |
| 11 | return [string range $version 1 [expr {$length - 2}]] |
| 12 | } |
| 13 | set version [getVersion $manifest_version] |
| 14 | set tclVersion [getTclVersion] |
| 15 | set fossilUrl https://www.fossil-scm.org |
| 16 | set fossilDate [string range $manifest_date 0 9]T[string range $manifest_date 11 end] |
| 17 | </th1> |
| 18 | This page was generated in about |
| 19 | <th1>puts [expr {([utime]+[stime]+1000)/1000*0.001}]</th1>s by |
| 20 | <a href="$fossilUrl/">Fossil</a> |
| 21 |
| --- skins/eagle/footer.txt | |
| +++ skins/eagle/footer.txt | |
| @@ -10,11 +10,11 @@ | |
| 10 | set length [string length $version] |
| 11 | return [string range $version 1 [expr {$length - 2}]] |
| 12 | } |
| 13 | set version [getVersion $manifest_version] |
| 14 | set tclVersion [getTclVersion] |
| 15 | set fossilUrl https://fossil-scm.org |
| 16 | set fossilDate [string range $manifest_date 0 9]T[string range $manifest_date 11 end] |
| 17 | </th1> |
| 18 | This page was generated in about |
| 19 | <th1>puts [expr {([utime]+[stime]+1000)/1000*0.001}]</th1>s by |
| 20 | <a href="$fossilUrl/">Fossil</a> |
| 21 |
+1
-1
| --- skins/eagle/header.txt | ||
| +++ skins/eagle/header.txt | ||
| @@ -65,11 +65,11 @@ | ||
| 65 | 65 | # Link logo to the top of the current repo |
| 66 | 66 | set logourl $baseurl |
| 67 | 67 | } |
| 68 | 68 | </th1> |
| 69 | 69 | <a href="$logourl"> |
| 70 | - <img src="$logo_image_url" border="0" alt="$project_name"> | |
| 70 | + <img src="$logo_image_url" border="0" alt="$<project_name>"> | |
| 71 | 71 | </a> |
| 72 | 72 | </div> |
| 73 | 73 | <div class="title">$<title></div> |
| 74 | 74 | <div class="status"><nobr><th1> |
| 75 | 75 | if {[info exists login]} { |
| 76 | 76 |
| --- skins/eagle/header.txt | |
| +++ skins/eagle/header.txt | |
| @@ -65,11 +65,11 @@ | |
| 65 | # Link logo to the top of the current repo |
| 66 | set logourl $baseurl |
| 67 | } |
| 68 | </th1> |
| 69 | <a href="$logourl"> |
| 70 | <img src="$logo_image_url" border="0" alt="$project_name"> |
| 71 | </a> |
| 72 | </div> |
| 73 | <div class="title">$<title></div> |
| 74 | <div class="status"><nobr><th1> |
| 75 | if {[info exists login]} { |
| 76 |
| --- skins/eagle/header.txt | |
| +++ skins/eagle/header.txt | |
| @@ -65,11 +65,11 @@ | |
| 65 | # Link logo to the top of the current repo |
| 66 | set logourl $baseurl |
| 67 | } |
| 68 | </th1> |
| 69 | <a href="$logourl"> |
| 70 | <img src="$logo_image_url" border="0" alt="$<project_name>"> |
| 71 | </a> |
| 72 | </div> |
| 73 | <div class="title">$<title></div> |
| 74 | <div class="status"><nobr><th1> |
| 75 | if {[info exists login]} { |
| 76 |
+1
-1
| --- skins/original/footer.txt | ||
| +++ skins/original/footer.txt | ||
| @@ -10,11 +10,11 @@ | ||
| 10 | 10 | set length [string length $version] |
| 11 | 11 | return [string range $version 1 [expr {$length - 2}]] |
| 12 | 12 | } |
| 13 | 13 | set version [getVersion $manifest_version] |
| 14 | 14 | set tclVersion [getTclVersion] |
| 15 | - set fossilUrl https://www.fossil-scm.org | |
| 15 | + set fossilUrl https://fossil-scm.org | |
| 16 | 16 | set fossilDate [string range $manifest_date 0 9]T[string range $manifest_date 11 end] |
| 17 | 17 | </th1> |
| 18 | 18 | This page was generated in about |
| 19 | 19 | <th1>puts [expr {([utime]+[stime]+1000)/1000*0.001}]</th1>s by |
| 20 | 20 | <a href="$fossilUrl/">Fossil</a> |
| 21 | 21 |
| --- skins/original/footer.txt | |
| +++ skins/original/footer.txt | |
| @@ -10,11 +10,11 @@ | |
| 10 | set length [string length $version] |
| 11 | return [string range $version 1 [expr {$length - 2}]] |
| 12 | } |
| 13 | set version [getVersion $manifest_version] |
| 14 | set tclVersion [getTclVersion] |
| 15 | set fossilUrl https://www.fossil-scm.org |
| 16 | set fossilDate [string range $manifest_date 0 9]T[string range $manifest_date 11 end] |
| 17 | </th1> |
| 18 | This page was generated in about |
| 19 | <th1>puts [expr {([utime]+[stime]+1000)/1000*0.001}]</th1>s by |
| 20 | <a href="$fossilUrl/">Fossil</a> |
| 21 |
| --- skins/original/footer.txt | |
| +++ skins/original/footer.txt | |
| @@ -10,11 +10,11 @@ | |
| 10 | set length [string length $version] |
| 11 | return [string range $version 1 [expr {$length - 2}]] |
| 12 | } |
| 13 | set version [getVersion $manifest_version] |
| 14 | set tclVersion [getTclVersion] |
| 15 | set fossilUrl https://fossil-scm.org |
| 16 | set fossilDate [string range $manifest_date 0 9]T[string range $manifest_date 11 end] |
| 17 | </th1> |
| 18 | This page was generated in about |
| 19 | <th1>puts [expr {([utime]+[stime]+1000)/1000*0.001}]</th1>s by |
| 20 | <a href="$fossilUrl/">Fossil</a> |
| 21 |
+1
-1
| --- skins/original/header.txt | ||
| +++ skins/original/header.txt | ||
| @@ -59,11 +59,11 @@ | ||
| 59 | 59 | return $logourl |
| 60 | 60 | } |
| 61 | 61 | set logourl [getLogoUrl $baseurl] |
| 62 | 62 | </th1> |
| 63 | 63 | <a href="$logourl"> |
| 64 | - <img src="$logo_image_url" border="0" alt="$project_name"> | |
| 64 | + <img src="$logo_image_url" border="0" alt="$<project_name>"> | |
| 65 | 65 | </a> |
| 66 | 66 | </div> |
| 67 | 67 | <div class="title">$<title></div> |
| 68 | 68 | <div class="status"><nobr><th1> |
| 69 | 69 | if {[info exists login]} { |
| 70 | 70 |
| --- skins/original/header.txt | |
| +++ skins/original/header.txt | |
| @@ -59,11 +59,11 @@ | |
| 59 | return $logourl |
| 60 | } |
| 61 | set logourl [getLogoUrl $baseurl] |
| 62 | </th1> |
| 63 | <a href="$logourl"> |
| 64 | <img src="$logo_image_url" border="0" alt="$project_name"> |
| 65 | </a> |
| 66 | </div> |
| 67 | <div class="title">$<title></div> |
| 68 | <div class="status"><nobr><th1> |
| 69 | if {[info exists login]} { |
| 70 |
| --- skins/original/header.txt | |
| +++ skins/original/header.txt | |
| @@ -59,11 +59,11 @@ | |
| 59 | return $logourl |
| 60 | } |
| 61 | set logourl [getLogoUrl $baseurl] |
| 62 | </th1> |
| 63 | <a href="$logourl"> |
| 64 | <img src="$logo_image_url" border="0" alt="$<project_name>"> |
| 65 | </a> |
| 66 | </div> |
| 67 | <div class="title">$<title></div> |
| 68 | <div class="status"><nobr><th1> |
| 69 | if {[info exists login]} { |
| 70 |
+101
| --- skins/xekri/css.txt | ||
| +++ skins/xekri/css.txt | ||
| @@ -20,10 +20,11 @@ | ||
| 20 | 20 | font-size: 1em; |
| 21 | 21 | min-height: 100%; |
| 22 | 22 | } |
| 23 | 23 | |
| 24 | 24 | body { |
| 25 | + background-color: #333; | |
| 25 | 26 | margin: 0; |
| 26 | 27 | padding: 0; |
| 27 | 28 | text-size-adjust: none; |
| 28 | 29 | } |
| 29 | 30 | |
| @@ -882,10 +883,110 @@ | ||
| 882 | 883 | |
| 883 | 884 | /* the format for the timeline version links */ |
| 884 | 885 | a.timelineHistLink { |
| 885 | 886 | } |
| 886 | 887 | |
| 888 | +/* Timeline graph style taken from Ardoise, with | |
| 889 | +** minor adjustments (2025-03-28) */ | |
| 890 | +.tl-canvas { | |
| 891 | + margin: 0 6px 0 10px | |
| 892 | +} | |
| 893 | +.tl-rail { | |
| 894 | + width: 18px | |
| 895 | +} | |
| 896 | +.tl-mergeoffset { | |
| 897 | + width: 2px | |
| 898 | +} | |
| 899 | +.tl-nodemark { | |
| 900 | + margin-top: .8em | |
| 901 | +} | |
| 902 | +.tl-node { | |
| 903 | + width: 10px; | |
| 904 | + height: 10px; | |
| 905 | + border: 1px solid #bbb; | |
| 906 | + background: #111; | |
| 907 | + cursor: pointer | |
| 908 | +} | |
| 909 | +.tl-node.leaf:after { | |
| 910 | + content: ''; | |
| 911 | + position: absolute; | |
| 912 | + top: 3px; | |
| 913 | + left: 3px; | |
| 914 | + width: 4px; | |
| 915 | + height: 4px; | |
| 916 | + background: #bbb | |
| 917 | +} | |
| 918 | +.tl-node.closed-leaf svg { | |
| 919 | + position: absolute; | |
| 920 | + top: 0px; | |
| 921 | + left: 0px; | |
| 922 | + width: 10px; | |
| 923 | + height: 10px; | |
| 924 | + color: #bbb; | |
| 925 | +} | |
| 926 | +.tl-node.sel:after { | |
| 927 | + content: ''; | |
| 928 | + position: absolute; | |
| 929 | + top: 1px; | |
| 930 | + left: 1px; | |
| 931 | + width: 8px; | |
| 932 | + height: 8px; | |
| 933 | + background: #ff8000 | |
| 934 | +} | |
| 935 | +.tl-arrow { | |
| 936 | + width: 0; | |
| 937 | + height: 0; | |
| 938 | + transform: scale(.999); | |
| 939 | + border: 0 solid transparent | |
| 940 | +} | |
| 941 | +.tl-arrow.u { | |
| 942 | + margin-top: -1px; | |
| 943 | + border-width: 0 3px; | |
| 944 | + border-bottom: 7px solid | |
| 945 | +} | |
| 946 | +.tl-arrow.u.sm { | |
| 947 | + border-bottom: 5px solid #bbb | |
| 948 | +} | |
| 949 | +.tl-line { | |
| 950 | + background: #bbb; | |
| 951 | + width: 2px | |
| 952 | +} | |
| 953 | +.tl-arrow.merge { | |
| 954 | + height: 1px; | |
| 955 | + border-width: 2px 0 | |
| 956 | +} | |
| 957 | +.tl-arrow.merge.l { | |
| 958 | + border-right: 3px solid #bbb | |
| 959 | +} | |
| 960 | +.tl-arrow.merge.r { | |
| 961 | + border-left: 3px solid #bbb | |
| 962 | +} | |
| 963 | +.tl-line.merge { | |
| 964 | + width: 1px | |
| 965 | +} | |
| 966 | +.tl-arrow.cherrypick { | |
| 967 | + height: 1px; | |
| 968 | + border-width: 2px 0; | |
| 969 | +} | |
| 970 | +.tl-arrow.cherrypick.l { | |
| 971 | + border-right: 3px solid #bbb; | |
| 972 | +} | |
| 973 | +.tl-arrow.cherrypick.r { | |
| 974 | + border-left: 3px solid #bbb; | |
| 975 | +} | |
| 976 | +.tl-line.cherrypick.h { | |
| 977 | + width: 0px; | |
| 978 | + border-top: 1px dashed #bbb; | |
| 979 | + border-left: 0px dashed #bbb; | |
| 980 | + background: rgba(255,255,255,0); | |
| 981 | +} | |
| 982 | +.tl-line.cherrypick.v { | |
| 983 | + width: 0px; | |
| 984 | + border-top: 0px dashed #bbb; | |
| 985 | + border-left: 1px dashed #bbb; | |
| 986 | + background: rgba(255,255,255,0); | |
| 987 | +} | |
| 887 | 988 | |
| 888 | 989 | /************************************** |
| 889 | 990 | * User Edit |
| 890 | 991 | */ |
| 891 | 992 | |
| 892 | 993 |
| --- skins/xekri/css.txt | |
| +++ skins/xekri/css.txt | |
| @@ -20,10 +20,11 @@ | |
| 20 | font-size: 1em; |
| 21 | min-height: 100%; |
| 22 | } |
| 23 | |
| 24 | body { |
| 25 | margin: 0; |
| 26 | padding: 0; |
| 27 | text-size-adjust: none; |
| 28 | } |
| 29 | |
| @@ -882,10 +883,110 @@ | |
| 882 | |
| 883 | /* the format for the timeline version links */ |
| 884 | a.timelineHistLink { |
| 885 | } |
| 886 | |
| 887 | |
| 888 | /************************************** |
| 889 | * User Edit |
| 890 | */ |
| 891 | |
| 892 |
| --- skins/xekri/css.txt | |
| +++ skins/xekri/css.txt | |
| @@ -20,10 +20,11 @@ | |
| 20 | font-size: 1em; |
| 21 | min-height: 100%; |
| 22 | } |
| 23 | |
| 24 | body { |
| 25 | background-color: #333; |
| 26 | margin: 0; |
| 27 | padding: 0; |
| 28 | text-size-adjust: none; |
| 29 | } |
| 30 | |
| @@ -882,10 +883,110 @@ | |
| 883 | |
| 884 | /* the format for the timeline version links */ |
| 885 | a.timelineHistLink { |
| 886 | } |
| 887 | |
| 888 | /* Timeline graph style taken from Ardoise, with |
| 889 | ** minor adjustments (2025-03-28) */ |
| 890 | .tl-canvas { |
| 891 | margin: 0 6px 0 10px |
| 892 | } |
| 893 | .tl-rail { |
| 894 | width: 18px |
| 895 | } |
| 896 | .tl-mergeoffset { |
| 897 | width: 2px |
| 898 | } |
| 899 | .tl-nodemark { |
| 900 | margin-top: .8em |
| 901 | } |
| 902 | .tl-node { |
| 903 | width: 10px; |
| 904 | height: 10px; |
| 905 | border: 1px solid #bbb; |
| 906 | background: #111; |
| 907 | cursor: pointer |
| 908 | } |
| 909 | .tl-node.leaf:after { |
| 910 | content: ''; |
| 911 | position: absolute; |
| 912 | top: 3px; |
| 913 | left: 3px; |
| 914 | width: 4px; |
| 915 | height: 4px; |
| 916 | background: #bbb |
| 917 | } |
| 918 | .tl-node.closed-leaf svg { |
| 919 | position: absolute; |
| 920 | top: 0px; |
| 921 | left: 0px; |
| 922 | width: 10px; |
| 923 | height: 10px; |
| 924 | color: #bbb; |
| 925 | } |
| 926 | .tl-node.sel:after { |
| 927 | content: ''; |
| 928 | position: absolute; |
| 929 | top: 1px; |
| 930 | left: 1px; |
| 931 | width: 8px; |
| 932 | height: 8px; |
| 933 | background: #ff8000 |
| 934 | } |
| 935 | .tl-arrow { |
| 936 | width: 0; |
| 937 | height: 0; |
| 938 | transform: scale(.999); |
| 939 | border: 0 solid transparent |
| 940 | } |
| 941 | .tl-arrow.u { |
| 942 | margin-top: -1px; |
| 943 | border-width: 0 3px; |
| 944 | border-bottom: 7px solid |
| 945 | } |
| 946 | .tl-arrow.u.sm { |
| 947 | border-bottom: 5px solid #bbb |
| 948 | } |
| 949 | .tl-line { |
| 950 | background: #bbb; |
| 951 | width: 2px |
| 952 | } |
| 953 | .tl-arrow.merge { |
| 954 | height: 1px; |
| 955 | border-width: 2px 0 |
| 956 | } |
| 957 | .tl-arrow.merge.l { |
| 958 | border-right: 3px solid #bbb |
| 959 | } |
| 960 | .tl-arrow.merge.r { |
| 961 | border-left: 3px solid #bbb |
| 962 | } |
| 963 | .tl-line.merge { |
| 964 | width: 1px |
| 965 | } |
| 966 | .tl-arrow.cherrypick { |
| 967 | height: 1px; |
| 968 | border-width: 2px 0; |
| 969 | } |
| 970 | .tl-arrow.cherrypick.l { |
| 971 | border-right: 3px solid #bbb; |
| 972 | } |
| 973 | .tl-arrow.cherrypick.r { |
| 974 | border-left: 3px solid #bbb; |
| 975 | } |
| 976 | .tl-line.cherrypick.h { |
| 977 | width: 0px; |
| 978 | border-top: 1px dashed #bbb; |
| 979 | border-left: 0px dashed #bbb; |
| 980 | background: rgba(255,255,255,0); |
| 981 | } |
| 982 | .tl-line.cherrypick.v { |
| 983 | width: 0px; |
| 984 | border-top: 0px dashed #bbb; |
| 985 | border-left: 1px dashed #bbb; |
| 986 | background: rgba(255,255,255,0); |
| 987 | } |
| 988 | |
| 989 | /************************************** |
| 990 | * User Edit |
| 991 | */ |
| 992 | |
| 993 |
+1
-1
| --- skins/xekri/header.txt | ||
| +++ skins/xekri/header.txt | ||
| @@ -65,11 +65,11 @@ | ||
| 65 | 65 | # Link logo to the top of the current repo |
| 66 | 66 | set logourl $baseurl |
| 67 | 67 | } |
| 68 | 68 | </th1> |
| 69 | 69 | <a href="$logourl"> |
| 70 | - <img src="$logo_image_url" border="0" alt="$project_name"> | |
| 70 | + <img src="$logo_image_url" border="0" alt="$<project_name>"> | |
| 71 | 71 | </a> |
| 72 | 72 | </div> |
| 73 | 73 | <div class="title">$<title></div> |
| 74 | 74 | <div class="status"><nobr> |
| 75 | 75 | <th1> |
| 76 | 76 |
| --- skins/xekri/header.txt | |
| +++ skins/xekri/header.txt | |
| @@ -65,11 +65,11 @@ | |
| 65 | # Link logo to the top of the current repo |
| 66 | set logourl $baseurl |
| 67 | } |
| 68 | </th1> |
| 69 | <a href="$logourl"> |
| 70 | <img src="$logo_image_url" border="0" alt="$project_name"> |
| 71 | </a> |
| 72 | </div> |
| 73 | <div class="title">$<title></div> |
| 74 | <div class="status"><nobr> |
| 75 | <th1> |
| 76 |
| --- skins/xekri/header.txt | |
| +++ skins/xekri/header.txt | |
| @@ -65,11 +65,11 @@ | |
| 65 | # Link logo to the top of the current repo |
| 66 | set logourl $baseurl |
| 67 | } |
| 68 | </th1> |
| 69 | <a href="$logourl"> |
| 70 | <img src="$logo_image_url" border="0" alt="$<project_name>"> |
| 71 | </a> |
| 72 | </div> |
| 73 | <div class="title">$<title></div> |
| 74 | <div class="status"><nobr> |
| 75 | <th1> |
| 76 |
+9
-2
| --- src/add.c | ||
| +++ src/add.c | ||
| @@ -896,11 +896,12 @@ | ||
| 896 | 896 | */ |
| 897 | 897 | static void mv_one_file( |
| 898 | 898 | int vid, |
| 899 | 899 | const char *zOrig, |
| 900 | 900 | const char *zNew, |
| 901 | - int dryRunFlag | |
| 901 | + int dryRunFlag, | |
| 902 | + int moveFiles | |
| 902 | 903 | ){ |
| 903 | 904 | int x = db_int(-1, "SELECT deleted FROM vfile WHERE pathname=%Q %s", |
| 904 | 905 | zNew, filename_collation()); |
| 905 | 906 | if( x>=0 ){ |
| 906 | 907 | if( x==0 ){ |
| @@ -912,10 +913,16 @@ | ||
| 912 | 913 | } |
| 913 | 914 | }else{ |
| 914 | 915 | fossil_fatal("cannot rename '%s' to '%s' since the delete of '%s' has " |
| 915 | 916 | "not yet been committed", zOrig, zNew, zNew); |
| 916 | 917 | } |
| 918 | + } | |
| 919 | + if( moveFiles ){ | |
| 920 | + if( file_size(zNew, ExtFILE) != -1 ){ | |
| 921 | + fossil_fatal("cannot rename '%s' to '%s' on disk since another file" | |
| 922 | + " named '%s' already exists", zOrig, zNew, zNew); | |
| 923 | + } | |
| 917 | 924 | } |
| 918 | 925 | fossil_print("RENAME %s %s\n", zOrig, zNew); |
| 919 | 926 | if( !dryRunFlag ){ |
| 920 | 927 | db_multi_exec( |
| 921 | 928 | "UPDATE vfile SET pathname='%q' WHERE pathname='%q' %s AND vid=%d", |
| @@ -1135,11 +1142,11 @@ | ||
| 1135 | 1142 | } |
| 1136 | 1143 | db_prepare(&q, "SELECT f, t FROM mv ORDER BY f"); |
| 1137 | 1144 | while( db_step(&q)==SQLITE_ROW ){ |
| 1138 | 1145 | const char *zFrom = db_column_text(&q, 0); |
| 1139 | 1146 | const char *zTo = db_column_text(&q, 1); |
| 1140 | - mv_one_file(vid, zFrom, zTo, dryRunFlag); | |
| 1147 | + mv_one_file(vid, zFrom, zTo, dryRunFlag, moveFiles); | |
| 1141 | 1148 | if( moveFiles ) add_file_to_move(zFrom, zTo); |
| 1142 | 1149 | } |
| 1143 | 1150 | db_finalize(&q); |
| 1144 | 1151 | undo_reset(); |
| 1145 | 1152 | db_end_transaction(0); |
| 1146 | 1153 |
| --- src/add.c | |
| +++ src/add.c | |
| @@ -896,11 +896,12 @@ | |
| 896 | */ |
| 897 | static void mv_one_file( |
| 898 | int vid, |
| 899 | const char *zOrig, |
| 900 | const char *zNew, |
| 901 | int dryRunFlag |
| 902 | ){ |
| 903 | int x = db_int(-1, "SELECT deleted FROM vfile WHERE pathname=%Q %s", |
| 904 | zNew, filename_collation()); |
| 905 | if( x>=0 ){ |
| 906 | if( x==0 ){ |
| @@ -912,10 +913,16 @@ | |
| 912 | } |
| 913 | }else{ |
| 914 | fossil_fatal("cannot rename '%s' to '%s' since the delete of '%s' has " |
| 915 | "not yet been committed", zOrig, zNew, zNew); |
| 916 | } |
| 917 | } |
| 918 | fossil_print("RENAME %s %s\n", zOrig, zNew); |
| 919 | if( !dryRunFlag ){ |
| 920 | db_multi_exec( |
| 921 | "UPDATE vfile SET pathname='%q' WHERE pathname='%q' %s AND vid=%d", |
| @@ -1135,11 +1142,11 @@ | |
| 1135 | } |
| 1136 | db_prepare(&q, "SELECT f, t FROM mv ORDER BY f"); |
| 1137 | while( db_step(&q)==SQLITE_ROW ){ |
| 1138 | const char *zFrom = db_column_text(&q, 0); |
| 1139 | const char *zTo = db_column_text(&q, 1); |
| 1140 | mv_one_file(vid, zFrom, zTo, dryRunFlag); |
| 1141 | if( moveFiles ) add_file_to_move(zFrom, zTo); |
| 1142 | } |
| 1143 | db_finalize(&q); |
| 1144 | undo_reset(); |
| 1145 | db_end_transaction(0); |
| 1146 |
| --- src/add.c | |
| +++ src/add.c | |
| @@ -896,11 +896,12 @@ | |
| 896 | */ |
| 897 | static void mv_one_file( |
| 898 | int vid, |
| 899 | const char *zOrig, |
| 900 | const char *zNew, |
| 901 | int dryRunFlag, |
| 902 | int moveFiles |
| 903 | ){ |
| 904 | int x = db_int(-1, "SELECT deleted FROM vfile WHERE pathname=%Q %s", |
| 905 | zNew, filename_collation()); |
| 906 | if( x>=0 ){ |
| 907 | if( x==0 ){ |
| @@ -912,10 +913,16 @@ | |
| 913 | } |
| 914 | }else{ |
| 915 | fossil_fatal("cannot rename '%s' to '%s' since the delete of '%s' has " |
| 916 | "not yet been committed", zOrig, zNew, zNew); |
| 917 | } |
| 918 | } |
| 919 | if( moveFiles ){ |
| 920 | if( file_size(zNew, ExtFILE) != -1 ){ |
| 921 | fossil_fatal("cannot rename '%s' to '%s' on disk since another file" |
| 922 | " named '%s' already exists", zOrig, zNew, zNew); |
| 923 | } |
| 924 | } |
| 925 | fossil_print("RENAME %s %s\n", zOrig, zNew); |
| 926 | if( !dryRunFlag ){ |
| 927 | db_multi_exec( |
| 928 | "UPDATE vfile SET pathname='%q' WHERE pathname='%q' %s AND vid=%d", |
| @@ -1135,11 +1142,11 @@ | |
| 1142 | } |
| 1143 | db_prepare(&q, "SELECT f, t FROM mv ORDER BY f"); |
| 1144 | while( db_step(&q)==SQLITE_ROW ){ |
| 1145 | const char *zFrom = db_column_text(&q, 0); |
| 1146 | const char *zTo = db_column_text(&q, 1); |
| 1147 | mv_one_file(vid, zFrom, zTo, dryRunFlag, moveFiles); |
| 1148 | if( moveFiles ) add_file_to_move(zFrom, zTo); |
| 1149 | } |
| 1150 | db_finalize(&q); |
| 1151 | undo_reset(); |
| 1152 | db_end_transaction(0); |
| 1153 |
+148
-64
| --- src/alerts.c | ||
| +++ src/alerts.c | ||
| @@ -51,11 +51,11 @@ | ||
| 51 | 51 | @ -- f - Forum posts |
| 52 | 52 | @ -- k - ** Special: Unsubscribed using /oneclickunsub |
| 53 | 53 | @ -- n - New forum threads |
| 54 | 54 | @ -- r - Replies to my own forum posts |
| 55 | 55 | @ -- t - Ticket changes |
| 56 | -@ -- u - Elevation of users' permissions (admins only) | |
| 56 | +@ -- u - Changes of users' permissions (admins only) | |
| 57 | 57 | @ -- w - Wiki changes |
| 58 | 58 | @ -- x - Edits to forum posts |
| 59 | 59 | @ -- Probably different codes will be added in the future. In the future |
| 60 | 60 | @ -- we might also add a separate table that allows subscribing to email |
| 61 | 61 | @ -- notifications for specific branches or tags or tickets. |
| @@ -282,10 +282,13 @@ | ||
| 282 | 282 | style_submenu_element("Subscribers","%R/subscribers"); |
| 283 | 283 | } |
| 284 | 284 | if( fossil_strcmp(g.zPath,"subscribe") ){ |
| 285 | 285 | style_submenu_element("Add New Subscriber","%R/subscribe"); |
| 286 | 286 | } |
| 287 | + if( fossil_strcmp(g.zPath,"setup_notification") ){ | |
| 288 | + style_submenu_element("Notification Setup","%R/setup_notification"); | |
| 289 | + } | |
| 287 | 290 | } |
| 288 | 291 | } |
| 289 | 292 | |
| 290 | 293 | |
| 291 | 294 | /* |
| @@ -295,14 +298,14 @@ | ||
| 295 | 298 | ** Normally accessible via the /Admin/Notification menu. |
| 296 | 299 | */ |
| 297 | 300 | void setup_notification(void){ |
| 298 | 301 | static const char *const azSendMethods[] = { |
| 299 | 302 | "off", "Disabled", |
| 300 | - "pipe", "Pipe to a command", | |
| 303 | + "relay", "SMTP relay", | |
| 301 | 304 | "db", "Store in a database", |
| 302 | 305 | "dir", "Store in a directory", |
| 303 | - "relay", "SMTP relay" | |
| 306 | + "pipe", "Pipe to a command", | |
| 304 | 307 | }; |
| 305 | 308 | login_check_credentials(); |
| 306 | 309 | if( !g.perm.Setup ){ |
| 307 | 310 | login_needed(0); |
| 308 | 311 | return; |
| @@ -311,22 +314,25 @@ | ||
| 311 | 314 | |
| 312 | 315 | alert_submenu_common(); |
| 313 | 316 | style_submenu_element("Send Announcement","%R/announce"); |
| 314 | 317 | style_set_current_feature("alerts"); |
| 315 | 318 | style_header("Email Notification Setup"); |
| 316 | - @ <h1>Status</h1> | |
| 319 | + @ <form action="%R/setup_notification" method="post"><div> | |
| 320 | + @ <h1>Status   <input type="submit" name="submit" value="Refresh"></h1> | |
| 321 | + @ </form> | |
| 317 | 322 | @ <table class="label-value"> |
| 318 | 323 | if( alert_enabled() ){ |
| 319 | 324 | stats_for_email(); |
| 320 | 325 | }else{ |
| 321 | 326 | @ <th>Disabled</th> |
| 322 | 327 | } |
| 323 | 328 | @ </table> |
| 324 | 329 | @ <hr> |
| 325 | - @ <h1> Configuration </h1> | |
| 326 | 330 | @ <form action="%R/setup_notification" method="post"><div> |
| 327 | - @ <input type="submit" name="submit" value="Apply Changes"><hr> | |
| 331 | + @ <h1> Configuration </h1> | |
| 332 | + @ <p><input type="submit" name="submit" value="Apply Changes"></p> | |
| 333 | + @ <hr> | |
| 328 | 334 | login_insert_csrf_secret(); |
| 329 | 335 | |
| 330 | 336 | entry_attribute("Canonical Server URL", 40, "email-url", |
| 331 | 337 | "eurl", "", 0); |
| 332 | 338 | @ <p><b>Required.</b> |
| @@ -391,38 +397,40 @@ | ||
| 391 | 397 | @ <p>How to send email. Requires auxiliary information from the fields |
| 392 | 398 | @ that follow. Hint: Use the <a href="%R/announce">/announce</a> page |
| 393 | 399 | @ to send test message to debug this setting. |
| 394 | 400 | @ (Property: "email-send-method")</p> |
| 395 | 401 | alert_schema(1); |
| 402 | + entry_attribute("SMTP Relay Host", 60, "email-send-relayhost", | |
| 403 | + "esrh", "localhost", 0); | |
| 404 | + @ <p>When the send method is "SMTP relay", each email message is | |
| 405 | + @ transmitted via the SMTP protocol (rfc5321) to a "Mail Submission | |
| 406 | + @ Agent" or "MSA" (rfc4409) at the hostname shown here. Optionally | |
| 407 | + @ append a colon and TCP port number (ex: smtp.example.com:587). | |
| 408 | + @ The default TCP port number is 25. | |
| 409 | + @ Usage Hint: If Fossil is running inside of a chroot jail, then it might | |
| 410 | + @ not be able to resolve hostnames. Work around this by using a raw IP | |
| 411 | + @ address or create a "/etc/hosts" file inside the chroot jail. | |
| 412 | + @ (Property: "email-send-relayhost")</p> | |
| 413 | + @ | |
| 414 | + entry_attribute("Store Emails In This Database", 60, "email-send-db", | |
| 415 | + "esdb", "", 0); | |
| 416 | + @ <p>When the send method is "store in a database", each email message is | |
| 417 | + @ stored in an SQLite database file with the name given here. | |
| 418 | + @ (Property: "email-send-db")</p> | |
| 396 | 419 | entry_attribute("Pipe Email Text Into This Command", 60, "email-send-command", |
| 397 | 420 | "ecmd", "sendmail -ti", 0); |
| 398 | 421 | @ <p>When the send method is "pipe to a command", this is the command |
| 399 | 422 | @ that is run. Email messages are piped into the standard input of this |
| 400 | 423 | @ command. The command is expected to extract the sender address, |
| 401 | 424 | @ recipient addresses, and subject from the header of the piped email |
| 402 | 425 | @ text. (Property: "email-send-command")</p> |
| 403 | - | |
| 404 | - entry_attribute("Store Emails In This Database", 60, "email-send-db", | |
| 405 | - "esdb", "", 0); | |
| 406 | - @ <p>When the send method is "store in a database", each email message is | |
| 407 | - @ stored in an SQLite database file with the name given here. | |
| 408 | - @ (Property: "email-send-db")</p> | |
| 409 | - | |
| 410 | 426 | entry_attribute("Store Emails In This Directory", 60, "email-send-dir", |
| 411 | 427 | "esdir", "", 0); |
| 412 | 428 | @ <p>When the send method is "store in a directory", each email message is |
| 413 | 429 | @ stored as a separate file in the directory shown here. |
| 414 | 430 | @ (Property: "email-send-dir")</p> |
| 415 | 431 | |
| 416 | - entry_attribute("SMTP Relay Host", 60, "email-send-relayhost", | |
| 417 | - "esrh", "", 0); | |
| 418 | - @ <p>When the send method is "SMTP relay", each email message is | |
| 419 | - @ transmitted via the SMTP protocol (rfc5321) to a "Mail Submission | |
| 420 | - @ Agent" or "MSA" (rfc4409) at the hostname shown here. Optionally | |
| 421 | - @ append a colon and TCP port number (ex: smtp.example.com:587). | |
| 422 | - @ The default TCP port number is 25. | |
| 423 | - @ (Property: "email-send-relayhost")</p> | |
| 424 | 432 | @ <hr> |
| 425 | 433 | |
| 426 | 434 | @ <p><input type="submit" name="submit" value="Apply Changes"></p> |
| 427 | 435 | @ </div></form> |
| 428 | 436 | db_end_transaction(0); |
| @@ -630,19 +638,27 @@ | ||
| 630 | 638 | emailerGetSetting(p, &p->zCmd, "email-send-command"); |
| 631 | 639 | }else if( fossil_strcmp(p->zDest, "dir")==0 ){ |
| 632 | 640 | emailerGetSetting(p, &p->zDir, "email-send-dir"); |
| 633 | 641 | }else if( fossil_strcmp(p->zDest, "blob")==0 ){ |
| 634 | 642 | blob_init(&p->out, 0, 0); |
| 635 | - }else if( fossil_strcmp(p->zDest, "relay")==0 ){ | |
| 643 | + }else if( fossil_strcmp(p->zDest, "relay")==0 | |
| 644 | + || fossil_strcmp(p->zDest, "debug-relay")==0 | |
| 645 | + ){ | |
| 636 | 646 | const char *zRelay = 0; |
| 637 | 647 | emailerGetSetting(p, &zRelay, "email-send-relayhost"); |
| 638 | 648 | if( zRelay ){ |
| 639 | 649 | u32 smtpFlags = SMTP_DIRECT; |
| 640 | 650 | if( mFlags & ALERT_TRACE ) smtpFlags |= SMTP_TRACE_STDOUT; |
| 651 | + blob_init(&p->out, 0, 0); | |
| 641 | 652 | p->pSmtp = smtp_session_new(domain_of_addr(p->zFrom), zRelay, |
| 642 | - smtpFlags); | |
| 643 | - smtp_client_startup(p->pSmtp); | |
| 653 | + smtpFlags, 0); | |
| 654 | + if( p->pSmtp==0 || p->pSmtp->zErr ){ | |
| 655 | + emailerError(p, "Could not start SMTP session: %s", | |
| 656 | + p->pSmtp ? p->pSmtp->zErr : "reason unknown"); | |
| 657 | + }else if( p->zDest[0]=='d' ){ | |
| 658 | + smtp_session_config(p->pSmtp, SMTP_TRACE_BLOB, &p->out); | |
| 659 | + } | |
| 644 | 660 | } |
| 645 | 661 | } |
| 646 | 662 | return p; |
| 647 | 663 | } |
| 648 | 664 | |
| @@ -968,13 +984,10 @@ | ||
| 968 | 984 | blob_appendf(pOut, "Sender: <%s>\r\n", p->zFrom); |
| 969 | 985 | }else{ |
| 970 | 986 | blob_appendf(pOut, "From: <%s>\r\n", p->zFrom); |
| 971 | 987 | } |
| 972 | 988 | blob_appendf(pOut, "Date: %z\r\n", cgi_rfc822_datestamp(time(0))); |
| 973 | - if( p->zListId && p->zListId[0] ){ | |
| 974 | - blob_appendf(pOut, "List-Id: %s\r\n", p->zListId); | |
| 975 | - } | |
| 976 | 989 | if( strstr(blob_str(pHdr), "\r\nMessage-Id:")==0 ){ |
| 977 | 990 | /* Message-id format: "<$(date)x$(random)@$(from-host)>" where $(date) is |
| 978 | 991 | ** the current unix-time in hex, $(random) is a 64-bit random number, |
| 979 | 992 | ** and $(from) is the domain part of the email-self setting. */ |
| 980 | 993 | sqlite3_randomness(sizeof(r1), &r1); |
| @@ -1016,13 +1029,21 @@ | ||
| 1016 | 1029 | blob_write_to_file(&all, zFile); |
| 1017 | 1030 | fossil_free(zFile); |
| 1018 | 1031 | }else if( p->pSmtp ){ |
| 1019 | 1032 | char **azTo = 0; |
| 1020 | 1033 | int nTo = 0; |
| 1034 | + SmtpSession *pSmtp = p->pSmtp; | |
| 1021 | 1035 | email_header_to(pHdr, &nTo, &azTo); |
| 1022 | - if( nTo>0 ){ | |
| 1023 | - smtp_send_msg(p->pSmtp, p->zFrom, nTo, (const char**)azTo,blob_str(&all)); | |
| 1036 | + if( nTo>0 && !pSmtp->bFatal ){ | |
| 1037 | + smtp_send_msg(pSmtp,p->zFrom,nTo,(const char**)azTo,blob_str(&all)); | |
| 1038 | + if( pSmtp->zErr && !pSmtp->bFatal ){ | |
| 1039 | + smtp_send_msg(pSmtp,p->zFrom,nTo,(const char**)azTo,blob_str(&all)); | |
| 1040 | + } | |
| 1041 | + if( pSmtp->zErr ){ | |
| 1042 | + fossil_errorlog("SMTP: (%s) %s", pSmtp->bFatal ? "fatal" : "retry", | |
| 1043 | + pSmtp->zErr); | |
| 1044 | + } | |
| 1024 | 1045 | email_header_to_free(nTo, azTo); |
| 1025 | 1046 | } |
| 1026 | 1047 | }else if( strcmp(p->zDest, "stdout")==0 ){ |
| 1027 | 1048 | char **azTo = 0; |
| 1028 | 1049 | int nTo = 0; |
| @@ -1125,11 +1146,11 @@ | ||
| 1125 | 1146 | ** SETTING: email-listid width=40 |
| 1126 | 1147 | ** If this setting is not an empty string, then it becomes the argument to |
| 1127 | 1148 | ** a "List-ID:" header that is added to all out-bound notification emails. |
| 1128 | 1149 | */ |
| 1129 | 1150 | /* |
| 1130 | -** SETTING: email-send-relayhost width=40 sensitive | |
| 1151 | +** SETTING: email-send-relayhost width=40 sensitive default=127.0.0.1 | |
| 1131 | 1152 | ** This is the hostname and TCP port to which output email messages |
| 1132 | 1153 | ** are sent when email-send-method is "relay". There should be an |
| 1133 | 1154 | ** SMTP server configured as a Mail Submission Agent listening on the |
| 1134 | 1155 | ** designated host and port and all times. |
| 1135 | 1156 | */ |
| @@ -1704,11 +1725,11 @@ | ||
| 1704 | 1725 | @ <label><input type="checkbox" name="sw" %s(PCK("sw"))> \ |
| 1705 | 1726 | @ Wiki</label><br> |
| 1706 | 1727 | } |
| 1707 | 1728 | if( g.perm.Admin ){ |
| 1708 | 1729 | @ <label><input type="checkbox" name="su" %s(PCK("su"))> \ |
| 1709 | - @ User permission elevation</label> | |
| 1730 | + @ User permission changes</label> | |
| 1710 | 1731 | } |
| 1711 | 1732 | di = PB("di"); |
| 1712 | 1733 | @ </td></tr> |
| 1713 | 1734 | @ <tr> |
| 1714 | 1735 | @ <td class="form_label">Delivery:</td> |
| @@ -2114,11 +2135,11 @@ | ||
| 2114 | 2135 | /* Corner-case bug: if an admin assigns 'u' to a non-admin, that |
| 2115 | 2136 | ** subscription will get removed if the user later edits their |
| 2116 | 2137 | ** subscriptions, as non-admins are not permitted to add that |
| 2117 | 2138 | ** subscription. */ |
| 2118 | 2139 | @ <label><input type="checkbox" name="su" %s(su?"checked":"")>\ |
| 2119 | - @ User permission elevation</label> | |
| 2140 | + @ User permission changes</label> | |
| 2120 | 2141 | } |
| 2121 | 2142 | @ </td></tr> |
| 2122 | 2143 | if( strchr(ssub,'k')!=0 ){ |
| 2123 | 2144 | @ <tr><td></td><td> ↑ |
| 2124 | 2145 | @ Note: User did a one-click unsubscribe</td></tr> |
| @@ -3191,18 +3212,21 @@ | ||
| 3191 | 3212 | Blob fhdr, fbody; |
| 3192 | 3213 | blob_init(&fhdr, 0, 0); |
| 3193 | 3214 | blob_appendf(&fhdr, "To: <%s>\r\n", zEmail); |
| 3194 | 3215 | blob_append(&fhdr, blob_buffer(&p->hdr), blob_size(&p->hdr)); |
| 3195 | 3216 | blob_init(&fbody, blob_buffer(&p->txt), blob_size(&p->txt)); |
| 3196 | - blob_appendf(&fhdr, "List-Unsubscribe: <%s/oneclickunsub/%s>\r\n", | |
| 3197 | - zUrl, zCode); | |
| 3198 | - blob_appendf(&fhdr, | |
| 3199 | - "List-Unsubscribe-Post: List-Unsubscribe=One-Click\r\n"); | |
| 3200 | - blob_appendf(&fbody, "\n-- \nUnsubscribe: %s/unsubscribe/%s\n", | |
| 3201 | - zUrl, zCode); | |
| 3202 | - /* blob_appendf(&fbody, "Subscription settings: %s/alerts/%s\n", | |
| 3203 | - ** zUrl, zCode); */ | |
| 3217 | + if( pSender->zListId && pSender->zListId[0] ){ | |
| 3218 | + blob_appendf(&fhdr, "List-Id: %s\r\n", pSender->zListId); | |
| 3219 | + blob_appendf(&fhdr, "List-Unsubscribe: <%s/oneclickunsub/%s>\r\n", | |
| 3220 | + zUrl, zCode); | |
| 3221 | + blob_appendf(&fhdr, | |
| 3222 | + "List-Unsubscribe-Post: List-Unsubscribe=One-Click\r\n"); | |
| 3223 | + blob_appendf(&fbody, "\n-- \nUnsubscribe: %s/unsubscribe/%s\n", | |
| 3224 | + zUrl, zCode); | |
| 3225 | + /* blob_appendf(&fbody, "Subscription settings: %s/alerts/%s\n", | |
| 3226 | + ** zUrl, zCode); */ | |
| 3227 | + } | |
| 3204 | 3228 | alert_send(pSender,&fhdr,&fbody,p->zFromName); |
| 3205 | 3229 | nSent++; |
| 3206 | 3230 | blob_reset(&fhdr); |
| 3207 | 3231 | blob_reset(&fbody); |
| 3208 | 3232 | }else{ |
| @@ -3221,15 +3245,19 @@ | ||
| 3221 | 3245 | blob_append(&body, "\n", 1); |
| 3222 | 3246 | blob_append(&body, blob_buffer(&p->txt), blob_size(&p->txt)); |
| 3223 | 3247 | } |
| 3224 | 3248 | } |
| 3225 | 3249 | if( nHit==0 ) continue; |
| 3226 | - blob_appendf(&hdr, "List-Unsubscribe: <%s/oneclickunsub/%s>\r\n", | |
| 3227 | - zUrl, zCode); | |
| 3228 | - blob_appendf(&hdr, "List-Unsubscribe-Post: List-Unsubscribe=One-Click\r\n"); | |
| 3229 | - blob_appendf(&body,"\n-- \nSubscription info: %s/alerts/%s\n", | |
| 3230 | - zUrl, zCode); | |
| 3250 | + if( pSender->zListId && pSender->zListId[0] ){ | |
| 3251 | + blob_appendf(&hdr, "List-Id: %s\r\n", pSender->zListId); | |
| 3252 | + blob_appendf(&hdr, "List-Unsubscribe: <%s/oneclickunsub/%s>\r\n", | |
| 3253 | + zUrl, zCode); | |
| 3254 | + blob_appendf(&hdr, | |
| 3255 | + "List-Unsubscribe-Post: List-Unsubscribe=One-Click\r\n"); | |
| 3256 | + blob_appendf(&body,"\n-- \nSubscription info: %s/alerts/%s\n", | |
| 3257 | + zUrl, zCode); | |
| 3258 | + } | |
| 3231 | 3259 | alert_send(pSender,&hdr,&body,0); |
| 3232 | 3260 | nSent++; |
| 3233 | 3261 | blob_truncate(&hdr, 0); |
| 3234 | 3262 | blob_truncate(&body, 0); |
| 3235 | 3263 | } |
| @@ -3271,18 +3299,28 @@ | ||
| 3271 | 3299 | " AND length(sdigest)>0", |
| 3272 | 3300 | iNewWarn, iOldWarn |
| 3273 | 3301 | ); |
| 3274 | 3302 | while( db_step(&q)==SQLITE_ROW ){ |
| 3275 | 3303 | Blob hdr, body; |
| 3304 | + const char *zCode = db_column_text(&q,0); | |
| 3276 | 3305 | blob_init(&hdr, 0, 0); |
| 3277 | 3306 | blob_init(&body, 0, 0); |
| 3278 | 3307 | alert_renewal_msg(&hdr, &body, |
| 3279 | - db_column_text(&q,0), | |
| 3308 | + zCode, | |
| 3280 | 3309 | db_column_int(&q,1), |
| 3281 | 3310 | db_column_text(&q,2), |
| 3282 | 3311 | db_column_text(&q,3), |
| 3283 | 3312 | zRepoName, zUrl); |
| 3313 | + if( pSender->zListId && pSender->zListId[0] ){ | |
| 3314 | + blob_appendf(&hdr, "List-Id: %s\r\n", pSender->zListId); | |
| 3315 | + blob_appendf(&hdr, "List-Unsubscribe: <%s/oneclickunsub/%s>\r\n", | |
| 3316 | + zUrl, zCode); | |
| 3317 | + blob_appendf(&hdr, | |
| 3318 | + "List-Unsubscribe-Post: List-Unsubscribe=One-Click\r\n"); | |
| 3319 | + blob_appendf(&body, "\n-- \nUnsubscribe: %s/unsubscribe/%s\n", | |
| 3320 | + zUrl, zCode); | |
| 3321 | + } | |
| 3284 | 3322 | alert_send(pSender,&hdr,&body,0); |
| 3285 | 3323 | blob_reset(&hdr); |
| 3286 | 3324 | blob_reset(&body); |
| 3287 | 3325 | } |
| 3288 | 3326 | db_finalize(&q); |
| @@ -3436,16 +3474,28 @@ | ||
| 3436 | 3474 | char *zSubject = PT("subject"); |
| 3437 | 3475 | int bAll = PB("all"); |
| 3438 | 3476 | int bAA = PB("aa"); |
| 3439 | 3477 | int bMods = PB("mods"); |
| 3440 | 3478 | const char *zSub = db_get("email-subname", "[Fossil Repo]"); |
| 3441 | - int bTest2 = fossil_strcmp(P("name"),"test2")==0; | |
| 3479 | + const char *zName = P("name"); /* Debugging options */ | |
| 3480 | + const char *zDest = 0; /* How to send the announcement */ | |
| 3481 | + int bTest = 0; | |
| 3442 | 3482 | Blob hdr, body; |
| 3483 | + | |
| 3484 | + if( fossil_strcmp(zName, "test2")==0 ){ | |
| 3485 | + bTest = 2; | |
| 3486 | + zDest = "blob"; | |
| 3487 | + }else if( fossil_strcmp(zName, "test3")==0 ){ | |
| 3488 | + bTest = 3; | |
| 3489 | + if( fossil_strcmp(db_get("email-send-method",""),"relay")==0 ){ | |
| 3490 | + zDest = "debug-relay"; | |
| 3491 | + } | |
| 3492 | + } | |
| 3443 | 3493 | blob_init(&body, 0, 0); |
| 3444 | 3494 | blob_init(&hdr, 0, 0); |
| 3445 | 3495 | blob_appendf(&body, "%s", PT("msg")/*safe-for-%s*/); |
| 3446 | - pSender = alert_sender_new(bTest2 ? "blob" : 0, 0); | |
| 3496 | + pSender = alert_sender_new(zDest, 0); | |
| 3447 | 3497 | if( zTo[0] ){ |
| 3448 | 3498 | blob_appendf(&hdr, "To: <%s>\r\nSubject: %s %s\r\n", zTo, zSub, zSubject); |
| 3449 | 3499 | alert_send(pSender, &hdr, &body, 0); |
| 3450 | 3500 | } |
| 3451 | 3501 | if( bAll || bAA || bMods ){ |
| @@ -3479,17 +3529,24 @@ | ||
| 3479 | 3529 | } |
| 3480 | 3530 | alert_send(pSender, &hdr, &body, 0); |
| 3481 | 3531 | } |
| 3482 | 3532 | db_finalize(&q); |
| 3483 | 3533 | } |
| 3484 | - if( bTest2 ){ | |
| 3485 | - /* If the URL is /announce/test2 instead of just /announce, then no | |
| 3486 | - ** email is actually sent. Instead, the text of the email that would | |
| 3487 | - ** have been sent is displayed in the result window. */ | |
| 3488 | - @ <pre style='border: 2px solid blue; padding: 1ex'> | |
| 3534 | + if( bTest && blob_size(&pSender->out) ){ | |
| 3535 | + /* If the URL is "/announce/test2" then no email is actually sent. | |
| 3536 | + ** Instead, the text of the email that would have been sent is | |
| 3537 | + ** displayed in the result window. | |
| 3538 | + ** | |
| 3539 | + ** If the URL is "/announce/test3" and the email-send-method is "relay" | |
| 3540 | + ** then the announcement is sent as it normally would be, but a | |
| 3541 | + ** transcript of the SMTP conversation with the MTA is shown here. | |
| 3542 | + */ | |
| 3543 | + blob_trim(&pSender->out); | |
| 3544 | + @ <pre style='border: 2px solid blue; padding: 1ex;'> | |
| 3489 | 3545 | @ %h(blob_str(&pSender->out)) |
| 3490 | 3546 | @ </pre> |
| 3547 | + blob_reset(&pSender->out); | |
| 3491 | 3548 | } |
| 3492 | 3549 | zErr = pSender->zErr; |
| 3493 | 3550 | pSender->zErr = 0; |
| 3494 | 3551 | alert_sender_free(pSender); |
| 3495 | 3552 | return zErr; |
| @@ -3505,35 +3562,43 @@ | ||
| 3505 | 3562 | ** also send a message to an arbitrary email address and/or to all |
| 3506 | 3563 | ** subscribers regardless of whether or not they have elected to |
| 3507 | 3564 | ** receive announcements. |
| 3508 | 3565 | */ |
| 3509 | 3566 | void announce_page(void){ |
| 3510 | - const char *zAction = "announce" | |
| 3511 | - /* Maintenance reminder: we need an explicit action=THIS_PAGE on the | |
| 3512 | - ** form element to avoid that a URL arg of to=... passed to this | |
| 3513 | - ** page ends up overwriting the form-posted "to" value. This | |
| 3514 | - ** action value differs for the test1 request path. | |
| 3515 | - */; | |
| 3516 | - | |
| 3567 | + const char *zAction = "announce"; | |
| 3568 | + const char *zName = PD("name",""); | |
| 3569 | + /* | |
| 3570 | + ** Debugging Notes: | |
| 3571 | + ** | |
| 3572 | + ** /announce/test1 -> Shows query parameter values | |
| 3573 | + ** /announce/test2 -> Shows the formatted message but does | |
| 3574 | + ** not send it. | |
| 3575 | + ** /announce/test3 -> Sends the message, but also shows | |
| 3576 | + ** the SMTP transcript. | |
| 3577 | + */ | |
| 3517 | 3578 | login_check_credentials(); |
| 3518 | 3579 | if( !g.perm.Announce ){ |
| 3519 | 3580 | login_needed(0); |
| 3520 | 3581 | return; |
| 3521 | 3582 | } |
| 3583 | + if( !g.perm.Setup ){ | |
| 3584 | + zName = 0; /* Disable debugging feature for non-admin users */ | |
| 3585 | + } | |
| 3522 | 3586 | style_set_current_feature("alerts"); |
| 3523 | - if( fossil_strcmp(P("name"),"test1")==0 ){ | |
| 3587 | + if( fossil_strcmp(zName,"test1")==0 ){ | |
| 3524 | 3588 | /* Visit the /announce/test1 page to see the CGI variables */ |
| 3525 | 3589 | zAction = "announce/test1"; |
| 3526 | 3590 | @ <p style='border: 1px solid black; padding: 1ex;'> |
| 3527 | 3591 | cgi_print_all(0, 0, 0); |
| 3528 | 3592 | @ </p> |
| 3529 | 3593 | }else if( P("submit")!=0 && cgi_csrf_safe(2) ){ |
| 3530 | 3594 | char *zErr = alert_send_announcement(); |
| 3531 | 3595 | style_header("Announcement Sent"); |
| 3532 | 3596 | if( zErr ){ |
| 3533 | - @ <h1>Internal Error</h1> | |
| 3534 | - @ <p>The following error was reported by the system: | |
| 3597 | + @ <h1>Error</h1> | |
| 3598 | + @ <p>The following error was reported by the | |
| 3599 | + @ announcement-sending subsystem: | |
| 3535 | 3600 | @ <blockquote><pre> |
| 3536 | 3601 | @ %h(zErr) |
| 3537 | 3602 | @ </pre></blockquote> |
| 3538 | 3603 | }else{ |
| 3539 | 3604 | @ <p>The announcement has been sent. |
| @@ -3548,10 +3613,16 @@ | ||
| 3548 | 3613 | @ for this repository.</p> |
| 3549 | 3614 | return; |
| 3550 | 3615 | } |
| 3551 | 3616 | |
| 3552 | 3617 | style_header("Send Announcement"); |
| 3618 | + alert_submenu_common(); | |
| 3619 | + if( fossil_strcmp(zName,"test2")==0 ){ | |
| 3620 | + zAction = "announce/test2"; | |
| 3621 | + }else if( fossil_strcmp(zName,"test3")==0 ){ | |
| 3622 | + zAction = "announce/test3"; | |
| 3623 | + } | |
| 3553 | 3624 | @ <form method="POST" action="%R/%s(zAction)"> |
| 3554 | 3625 | login_insert_csrf_secret(); |
| 3555 | 3626 | @ <table class="subscribe"> |
| 3556 | 3627 | if( g.perm.Admin ){ |
| 3557 | 3628 | int aa = PB("aa"); |
| @@ -3584,15 +3655,28 @@ | ||
| 3584 | 3655 | @ <td><textarea name="msg" cols="80" rows="10" wrap="virtual">\ |
| 3585 | 3656 | @ %h(PT("msg"))</textarea> |
| 3586 | 3657 | @ </tr> |
| 3587 | 3658 | @ <tr> |
| 3588 | 3659 | @ <td></td> |
| 3589 | - if( fossil_strcmp(P("name"),"test2")==0 ){ | |
| 3660 | + if( fossil_strcmp(zName,"test2")==0 ){ | |
| 3590 | 3661 | @ <td><input type="submit" name="submit" value="Dry Run"> |
| 3591 | 3662 | }else{ |
| 3592 | 3663 | @ <td><input type="submit" name="submit" value="Send Message"> |
| 3593 | 3664 | } |
| 3594 | 3665 | @ </tr> |
| 3595 | 3666 | @ </table> |
| 3596 | 3667 | @ </form> |
| 3668 | + if( g.perm.Setup ){ | |
| 3669 | + @ <hr> | |
| 3670 | + @ <p>Trouble-shooting Options:</p> | |
| 3671 | + @ <ol> | |
| 3672 | + @ <li> <a href="%R/announce">Normal Processing</a> | |
| 3673 | + @ <li> Only <a href="%R/announce/test1">show POST parameters</a> | |
| 3674 | + @ - Do not send the announcement. | |
| 3675 | + @ <li> <a href="%R/announce/test2">Show the email text</a> but do | |
| 3676 | + @ not actually send it. | |
| 3677 | + @ <li> Send the message and also <a href="%R/announce/test3">show the | |
| 3678 | + @ SMTP traffic</a> when using "relay" mode. | |
| 3679 | + @ </ol> | |
| 3680 | + } | |
| 3597 | 3681 | style_finish_page(); |
| 3598 | 3682 | } |
| 3599 | 3683 |
| --- src/alerts.c | |
| +++ src/alerts.c | |
| @@ -51,11 +51,11 @@ | |
| 51 | @ -- f - Forum posts |
| 52 | @ -- k - ** Special: Unsubscribed using /oneclickunsub |
| 53 | @ -- n - New forum threads |
| 54 | @ -- r - Replies to my own forum posts |
| 55 | @ -- t - Ticket changes |
| 56 | @ -- u - Elevation of users' permissions (admins only) |
| 57 | @ -- w - Wiki changes |
| 58 | @ -- x - Edits to forum posts |
| 59 | @ -- Probably different codes will be added in the future. In the future |
| 60 | @ -- we might also add a separate table that allows subscribing to email |
| 61 | @ -- notifications for specific branches or tags or tickets. |
| @@ -282,10 +282,13 @@ | |
| 282 | style_submenu_element("Subscribers","%R/subscribers"); |
| 283 | } |
| 284 | if( fossil_strcmp(g.zPath,"subscribe") ){ |
| 285 | style_submenu_element("Add New Subscriber","%R/subscribe"); |
| 286 | } |
| 287 | } |
| 288 | } |
| 289 | |
| 290 | |
| 291 | /* |
| @@ -295,14 +298,14 @@ | |
| 295 | ** Normally accessible via the /Admin/Notification menu. |
| 296 | */ |
| 297 | void setup_notification(void){ |
| 298 | static const char *const azSendMethods[] = { |
| 299 | "off", "Disabled", |
| 300 | "pipe", "Pipe to a command", |
| 301 | "db", "Store in a database", |
| 302 | "dir", "Store in a directory", |
| 303 | "relay", "SMTP relay" |
| 304 | }; |
| 305 | login_check_credentials(); |
| 306 | if( !g.perm.Setup ){ |
| 307 | login_needed(0); |
| 308 | return; |
| @@ -311,22 +314,25 @@ | |
| 311 | |
| 312 | alert_submenu_common(); |
| 313 | style_submenu_element("Send Announcement","%R/announce"); |
| 314 | style_set_current_feature("alerts"); |
| 315 | style_header("Email Notification Setup"); |
| 316 | @ <h1>Status</h1> |
| 317 | @ <table class="label-value"> |
| 318 | if( alert_enabled() ){ |
| 319 | stats_for_email(); |
| 320 | }else{ |
| 321 | @ <th>Disabled</th> |
| 322 | } |
| 323 | @ </table> |
| 324 | @ <hr> |
| 325 | @ <h1> Configuration </h1> |
| 326 | @ <form action="%R/setup_notification" method="post"><div> |
| 327 | @ <input type="submit" name="submit" value="Apply Changes"><hr> |
| 328 | login_insert_csrf_secret(); |
| 329 | |
| 330 | entry_attribute("Canonical Server URL", 40, "email-url", |
| 331 | "eurl", "", 0); |
| 332 | @ <p><b>Required.</b> |
| @@ -391,38 +397,40 @@ | |
| 391 | @ <p>How to send email. Requires auxiliary information from the fields |
| 392 | @ that follow. Hint: Use the <a href="%R/announce">/announce</a> page |
| 393 | @ to send test message to debug this setting. |
| 394 | @ (Property: "email-send-method")</p> |
| 395 | alert_schema(1); |
| 396 | entry_attribute("Pipe Email Text Into This Command", 60, "email-send-command", |
| 397 | "ecmd", "sendmail -ti", 0); |
| 398 | @ <p>When the send method is "pipe to a command", this is the command |
| 399 | @ that is run. Email messages are piped into the standard input of this |
| 400 | @ command. The command is expected to extract the sender address, |
| 401 | @ recipient addresses, and subject from the header of the piped email |
| 402 | @ text. (Property: "email-send-command")</p> |
| 403 | |
| 404 | entry_attribute("Store Emails In This Database", 60, "email-send-db", |
| 405 | "esdb", "", 0); |
| 406 | @ <p>When the send method is "store in a database", each email message is |
| 407 | @ stored in an SQLite database file with the name given here. |
| 408 | @ (Property: "email-send-db")</p> |
| 409 | |
| 410 | entry_attribute("Store Emails In This Directory", 60, "email-send-dir", |
| 411 | "esdir", "", 0); |
| 412 | @ <p>When the send method is "store in a directory", each email message is |
| 413 | @ stored as a separate file in the directory shown here. |
| 414 | @ (Property: "email-send-dir")</p> |
| 415 | |
| 416 | entry_attribute("SMTP Relay Host", 60, "email-send-relayhost", |
| 417 | "esrh", "", 0); |
| 418 | @ <p>When the send method is "SMTP relay", each email message is |
| 419 | @ transmitted via the SMTP protocol (rfc5321) to a "Mail Submission |
| 420 | @ Agent" or "MSA" (rfc4409) at the hostname shown here. Optionally |
| 421 | @ append a colon and TCP port number (ex: smtp.example.com:587). |
| 422 | @ The default TCP port number is 25. |
| 423 | @ (Property: "email-send-relayhost")</p> |
| 424 | @ <hr> |
| 425 | |
| 426 | @ <p><input type="submit" name="submit" value="Apply Changes"></p> |
| 427 | @ </div></form> |
| 428 | db_end_transaction(0); |
| @@ -630,19 +638,27 @@ | |
| 630 | emailerGetSetting(p, &p->zCmd, "email-send-command"); |
| 631 | }else if( fossil_strcmp(p->zDest, "dir")==0 ){ |
| 632 | emailerGetSetting(p, &p->zDir, "email-send-dir"); |
| 633 | }else if( fossil_strcmp(p->zDest, "blob")==0 ){ |
| 634 | blob_init(&p->out, 0, 0); |
| 635 | }else if( fossil_strcmp(p->zDest, "relay")==0 ){ |
| 636 | const char *zRelay = 0; |
| 637 | emailerGetSetting(p, &zRelay, "email-send-relayhost"); |
| 638 | if( zRelay ){ |
| 639 | u32 smtpFlags = SMTP_DIRECT; |
| 640 | if( mFlags & ALERT_TRACE ) smtpFlags |= SMTP_TRACE_STDOUT; |
| 641 | p->pSmtp = smtp_session_new(domain_of_addr(p->zFrom), zRelay, |
| 642 | smtpFlags); |
| 643 | smtp_client_startup(p->pSmtp); |
| 644 | } |
| 645 | } |
| 646 | return p; |
| 647 | } |
| 648 | |
| @@ -968,13 +984,10 @@ | |
| 968 | blob_appendf(pOut, "Sender: <%s>\r\n", p->zFrom); |
| 969 | }else{ |
| 970 | blob_appendf(pOut, "From: <%s>\r\n", p->zFrom); |
| 971 | } |
| 972 | blob_appendf(pOut, "Date: %z\r\n", cgi_rfc822_datestamp(time(0))); |
| 973 | if( p->zListId && p->zListId[0] ){ |
| 974 | blob_appendf(pOut, "List-Id: %s\r\n", p->zListId); |
| 975 | } |
| 976 | if( strstr(blob_str(pHdr), "\r\nMessage-Id:")==0 ){ |
| 977 | /* Message-id format: "<$(date)x$(random)@$(from-host)>" where $(date) is |
| 978 | ** the current unix-time in hex, $(random) is a 64-bit random number, |
| 979 | ** and $(from) is the domain part of the email-self setting. */ |
| 980 | sqlite3_randomness(sizeof(r1), &r1); |
| @@ -1016,13 +1029,21 @@ | |
| 1016 | blob_write_to_file(&all, zFile); |
| 1017 | fossil_free(zFile); |
| 1018 | }else if( p->pSmtp ){ |
| 1019 | char **azTo = 0; |
| 1020 | int nTo = 0; |
| 1021 | email_header_to(pHdr, &nTo, &azTo); |
| 1022 | if( nTo>0 ){ |
| 1023 | smtp_send_msg(p->pSmtp, p->zFrom, nTo, (const char**)azTo,blob_str(&all)); |
| 1024 | email_header_to_free(nTo, azTo); |
| 1025 | } |
| 1026 | }else if( strcmp(p->zDest, "stdout")==0 ){ |
| 1027 | char **azTo = 0; |
| 1028 | int nTo = 0; |
| @@ -1125,11 +1146,11 @@ | |
| 1125 | ** SETTING: email-listid width=40 |
| 1126 | ** If this setting is not an empty string, then it becomes the argument to |
| 1127 | ** a "List-ID:" header that is added to all out-bound notification emails. |
| 1128 | */ |
| 1129 | /* |
| 1130 | ** SETTING: email-send-relayhost width=40 sensitive |
| 1131 | ** This is the hostname and TCP port to which output email messages |
| 1132 | ** are sent when email-send-method is "relay". There should be an |
| 1133 | ** SMTP server configured as a Mail Submission Agent listening on the |
| 1134 | ** designated host and port and all times. |
| 1135 | */ |
| @@ -1704,11 +1725,11 @@ | |
| 1704 | @ <label><input type="checkbox" name="sw" %s(PCK("sw"))> \ |
| 1705 | @ Wiki</label><br> |
| 1706 | } |
| 1707 | if( g.perm.Admin ){ |
| 1708 | @ <label><input type="checkbox" name="su" %s(PCK("su"))> \ |
| 1709 | @ User permission elevation</label> |
| 1710 | } |
| 1711 | di = PB("di"); |
| 1712 | @ </td></tr> |
| 1713 | @ <tr> |
| 1714 | @ <td class="form_label">Delivery:</td> |
| @@ -2114,11 +2135,11 @@ | |
| 2114 | /* Corner-case bug: if an admin assigns 'u' to a non-admin, that |
| 2115 | ** subscription will get removed if the user later edits their |
| 2116 | ** subscriptions, as non-admins are not permitted to add that |
| 2117 | ** subscription. */ |
| 2118 | @ <label><input type="checkbox" name="su" %s(su?"checked":"")>\ |
| 2119 | @ User permission elevation</label> |
| 2120 | } |
| 2121 | @ </td></tr> |
| 2122 | if( strchr(ssub,'k')!=0 ){ |
| 2123 | @ <tr><td></td><td> ↑ |
| 2124 | @ Note: User did a one-click unsubscribe</td></tr> |
| @@ -3191,18 +3212,21 @@ | |
| 3191 | Blob fhdr, fbody; |
| 3192 | blob_init(&fhdr, 0, 0); |
| 3193 | blob_appendf(&fhdr, "To: <%s>\r\n", zEmail); |
| 3194 | blob_append(&fhdr, blob_buffer(&p->hdr), blob_size(&p->hdr)); |
| 3195 | blob_init(&fbody, blob_buffer(&p->txt), blob_size(&p->txt)); |
| 3196 | blob_appendf(&fhdr, "List-Unsubscribe: <%s/oneclickunsub/%s>\r\n", |
| 3197 | zUrl, zCode); |
| 3198 | blob_appendf(&fhdr, |
| 3199 | "List-Unsubscribe-Post: List-Unsubscribe=One-Click\r\n"); |
| 3200 | blob_appendf(&fbody, "\n-- \nUnsubscribe: %s/unsubscribe/%s\n", |
| 3201 | zUrl, zCode); |
| 3202 | /* blob_appendf(&fbody, "Subscription settings: %s/alerts/%s\n", |
| 3203 | ** zUrl, zCode); */ |
| 3204 | alert_send(pSender,&fhdr,&fbody,p->zFromName); |
| 3205 | nSent++; |
| 3206 | blob_reset(&fhdr); |
| 3207 | blob_reset(&fbody); |
| 3208 | }else{ |
| @@ -3221,15 +3245,19 @@ | |
| 3221 | blob_append(&body, "\n", 1); |
| 3222 | blob_append(&body, blob_buffer(&p->txt), blob_size(&p->txt)); |
| 3223 | } |
| 3224 | } |
| 3225 | if( nHit==0 ) continue; |
| 3226 | blob_appendf(&hdr, "List-Unsubscribe: <%s/oneclickunsub/%s>\r\n", |
| 3227 | zUrl, zCode); |
| 3228 | blob_appendf(&hdr, "List-Unsubscribe-Post: List-Unsubscribe=One-Click\r\n"); |
| 3229 | blob_appendf(&body,"\n-- \nSubscription info: %s/alerts/%s\n", |
| 3230 | zUrl, zCode); |
| 3231 | alert_send(pSender,&hdr,&body,0); |
| 3232 | nSent++; |
| 3233 | blob_truncate(&hdr, 0); |
| 3234 | blob_truncate(&body, 0); |
| 3235 | } |
| @@ -3271,18 +3299,28 @@ | |
| 3271 | " AND length(sdigest)>0", |
| 3272 | iNewWarn, iOldWarn |
| 3273 | ); |
| 3274 | while( db_step(&q)==SQLITE_ROW ){ |
| 3275 | Blob hdr, body; |
| 3276 | blob_init(&hdr, 0, 0); |
| 3277 | blob_init(&body, 0, 0); |
| 3278 | alert_renewal_msg(&hdr, &body, |
| 3279 | db_column_text(&q,0), |
| 3280 | db_column_int(&q,1), |
| 3281 | db_column_text(&q,2), |
| 3282 | db_column_text(&q,3), |
| 3283 | zRepoName, zUrl); |
| 3284 | alert_send(pSender,&hdr,&body,0); |
| 3285 | blob_reset(&hdr); |
| 3286 | blob_reset(&body); |
| 3287 | } |
| 3288 | db_finalize(&q); |
| @@ -3436,16 +3474,28 @@ | |
| 3436 | char *zSubject = PT("subject"); |
| 3437 | int bAll = PB("all"); |
| 3438 | int bAA = PB("aa"); |
| 3439 | int bMods = PB("mods"); |
| 3440 | const char *zSub = db_get("email-subname", "[Fossil Repo]"); |
| 3441 | int bTest2 = fossil_strcmp(P("name"),"test2")==0; |
| 3442 | Blob hdr, body; |
| 3443 | blob_init(&body, 0, 0); |
| 3444 | blob_init(&hdr, 0, 0); |
| 3445 | blob_appendf(&body, "%s", PT("msg")/*safe-for-%s*/); |
| 3446 | pSender = alert_sender_new(bTest2 ? "blob" : 0, 0); |
| 3447 | if( zTo[0] ){ |
| 3448 | blob_appendf(&hdr, "To: <%s>\r\nSubject: %s %s\r\n", zTo, zSub, zSubject); |
| 3449 | alert_send(pSender, &hdr, &body, 0); |
| 3450 | } |
| 3451 | if( bAll || bAA || bMods ){ |
| @@ -3479,17 +3529,24 @@ | |
| 3479 | } |
| 3480 | alert_send(pSender, &hdr, &body, 0); |
| 3481 | } |
| 3482 | db_finalize(&q); |
| 3483 | } |
| 3484 | if( bTest2 ){ |
| 3485 | /* If the URL is /announce/test2 instead of just /announce, then no |
| 3486 | ** email is actually sent. Instead, the text of the email that would |
| 3487 | ** have been sent is displayed in the result window. */ |
| 3488 | @ <pre style='border: 2px solid blue; padding: 1ex'> |
| 3489 | @ %h(blob_str(&pSender->out)) |
| 3490 | @ </pre> |
| 3491 | } |
| 3492 | zErr = pSender->zErr; |
| 3493 | pSender->zErr = 0; |
| 3494 | alert_sender_free(pSender); |
| 3495 | return zErr; |
| @@ -3505,35 +3562,43 @@ | |
| 3505 | ** also send a message to an arbitrary email address and/or to all |
| 3506 | ** subscribers regardless of whether or not they have elected to |
| 3507 | ** receive announcements. |
| 3508 | */ |
| 3509 | void announce_page(void){ |
| 3510 | const char *zAction = "announce" |
| 3511 | /* Maintenance reminder: we need an explicit action=THIS_PAGE on the |
| 3512 | ** form element to avoid that a URL arg of to=... passed to this |
| 3513 | ** page ends up overwriting the form-posted "to" value. This |
| 3514 | ** action value differs for the test1 request path. |
| 3515 | */; |
| 3516 | |
| 3517 | login_check_credentials(); |
| 3518 | if( !g.perm.Announce ){ |
| 3519 | login_needed(0); |
| 3520 | return; |
| 3521 | } |
| 3522 | style_set_current_feature("alerts"); |
| 3523 | if( fossil_strcmp(P("name"),"test1")==0 ){ |
| 3524 | /* Visit the /announce/test1 page to see the CGI variables */ |
| 3525 | zAction = "announce/test1"; |
| 3526 | @ <p style='border: 1px solid black; padding: 1ex;'> |
| 3527 | cgi_print_all(0, 0, 0); |
| 3528 | @ </p> |
| 3529 | }else if( P("submit")!=0 && cgi_csrf_safe(2) ){ |
| 3530 | char *zErr = alert_send_announcement(); |
| 3531 | style_header("Announcement Sent"); |
| 3532 | if( zErr ){ |
| 3533 | @ <h1>Internal Error</h1> |
| 3534 | @ <p>The following error was reported by the system: |
| 3535 | @ <blockquote><pre> |
| 3536 | @ %h(zErr) |
| 3537 | @ </pre></blockquote> |
| 3538 | }else{ |
| 3539 | @ <p>The announcement has been sent. |
| @@ -3548,10 +3613,16 @@ | |
| 3548 | @ for this repository.</p> |
| 3549 | return; |
| 3550 | } |
| 3551 | |
| 3552 | style_header("Send Announcement"); |
| 3553 | @ <form method="POST" action="%R/%s(zAction)"> |
| 3554 | login_insert_csrf_secret(); |
| 3555 | @ <table class="subscribe"> |
| 3556 | if( g.perm.Admin ){ |
| 3557 | int aa = PB("aa"); |
| @@ -3584,15 +3655,28 @@ | |
| 3584 | @ <td><textarea name="msg" cols="80" rows="10" wrap="virtual">\ |
| 3585 | @ %h(PT("msg"))</textarea> |
| 3586 | @ </tr> |
| 3587 | @ <tr> |
| 3588 | @ <td></td> |
| 3589 | if( fossil_strcmp(P("name"),"test2")==0 ){ |
| 3590 | @ <td><input type="submit" name="submit" value="Dry Run"> |
| 3591 | }else{ |
| 3592 | @ <td><input type="submit" name="submit" value="Send Message"> |
| 3593 | } |
| 3594 | @ </tr> |
| 3595 | @ </table> |
| 3596 | @ </form> |
| 3597 | style_finish_page(); |
| 3598 | } |
| 3599 |
| --- src/alerts.c | |
| +++ src/alerts.c | |
| @@ -51,11 +51,11 @@ | |
| 51 | @ -- f - Forum posts |
| 52 | @ -- k - ** Special: Unsubscribed using /oneclickunsub |
| 53 | @ -- n - New forum threads |
| 54 | @ -- r - Replies to my own forum posts |
| 55 | @ -- t - Ticket changes |
| 56 | @ -- u - Changes of users' permissions (admins only) |
| 57 | @ -- w - Wiki changes |
| 58 | @ -- x - Edits to forum posts |
| 59 | @ -- Probably different codes will be added in the future. In the future |
| 60 | @ -- we might also add a separate table that allows subscribing to email |
| 61 | @ -- notifications for specific branches or tags or tickets. |
| @@ -282,10 +282,13 @@ | |
| 282 | style_submenu_element("Subscribers","%R/subscribers"); |
| 283 | } |
| 284 | if( fossil_strcmp(g.zPath,"subscribe") ){ |
| 285 | style_submenu_element("Add New Subscriber","%R/subscribe"); |
| 286 | } |
| 287 | if( fossil_strcmp(g.zPath,"setup_notification") ){ |
| 288 | style_submenu_element("Notification Setup","%R/setup_notification"); |
| 289 | } |
| 290 | } |
| 291 | } |
| 292 | |
| 293 | |
| 294 | /* |
| @@ -295,14 +298,14 @@ | |
| 298 | ** Normally accessible via the /Admin/Notification menu. |
| 299 | */ |
| 300 | void setup_notification(void){ |
| 301 | static const char *const azSendMethods[] = { |
| 302 | "off", "Disabled", |
| 303 | "relay", "SMTP relay", |
| 304 | "db", "Store in a database", |
| 305 | "dir", "Store in a directory", |
| 306 | "pipe", "Pipe to a command", |
| 307 | }; |
| 308 | login_check_credentials(); |
| 309 | if( !g.perm.Setup ){ |
| 310 | login_needed(0); |
| 311 | return; |
| @@ -311,22 +314,25 @@ | |
| 314 | |
| 315 | alert_submenu_common(); |
| 316 | style_submenu_element("Send Announcement","%R/announce"); |
| 317 | style_set_current_feature("alerts"); |
| 318 | style_header("Email Notification Setup"); |
| 319 | @ <form action="%R/setup_notification" method="post"><div> |
| 320 | @ <h1>Status   <input type="submit" name="submit" value="Refresh"></h1> |
| 321 | @ </form> |
| 322 | @ <table class="label-value"> |
| 323 | if( alert_enabled() ){ |
| 324 | stats_for_email(); |
| 325 | }else{ |
| 326 | @ <th>Disabled</th> |
| 327 | } |
| 328 | @ </table> |
| 329 | @ <hr> |
| 330 | @ <form action="%R/setup_notification" method="post"><div> |
| 331 | @ <h1> Configuration </h1> |
| 332 | @ <p><input type="submit" name="submit" value="Apply Changes"></p> |
| 333 | @ <hr> |
| 334 | login_insert_csrf_secret(); |
| 335 | |
| 336 | entry_attribute("Canonical Server URL", 40, "email-url", |
| 337 | "eurl", "", 0); |
| 338 | @ <p><b>Required.</b> |
| @@ -391,38 +397,40 @@ | |
| 397 | @ <p>How to send email. Requires auxiliary information from the fields |
| 398 | @ that follow. Hint: Use the <a href="%R/announce">/announce</a> page |
| 399 | @ to send test message to debug this setting. |
| 400 | @ (Property: "email-send-method")</p> |
| 401 | alert_schema(1); |
| 402 | entry_attribute("SMTP Relay Host", 60, "email-send-relayhost", |
| 403 | "esrh", "localhost", 0); |
| 404 | @ <p>When the send method is "SMTP relay", each email message is |
| 405 | @ transmitted via the SMTP protocol (rfc5321) to a "Mail Submission |
| 406 | @ Agent" or "MSA" (rfc4409) at the hostname shown here. Optionally |
| 407 | @ append a colon and TCP port number (ex: smtp.example.com:587). |
| 408 | @ The default TCP port number is 25. |
| 409 | @ Usage Hint: If Fossil is running inside of a chroot jail, then it might |
| 410 | @ not be able to resolve hostnames. Work around this by using a raw IP |
| 411 | @ address or create a "/etc/hosts" file inside the chroot jail. |
| 412 | @ (Property: "email-send-relayhost")</p> |
| 413 | @ |
| 414 | entry_attribute("Store Emails In This Database", 60, "email-send-db", |
| 415 | "esdb", "", 0); |
| 416 | @ <p>When the send method is "store in a database", each email message is |
| 417 | @ stored in an SQLite database file with the name given here. |
| 418 | @ (Property: "email-send-db")</p> |
| 419 | entry_attribute("Pipe Email Text Into This Command", 60, "email-send-command", |
| 420 | "ecmd", "sendmail -ti", 0); |
| 421 | @ <p>When the send method is "pipe to a command", this is the command |
| 422 | @ that is run. Email messages are piped into the standard input of this |
| 423 | @ command. The command is expected to extract the sender address, |
| 424 | @ recipient addresses, and subject from the header of the piped email |
| 425 | @ text. (Property: "email-send-command")</p> |
| 426 | entry_attribute("Store Emails In This Directory", 60, "email-send-dir", |
| 427 | "esdir", "", 0); |
| 428 | @ <p>When the send method is "store in a directory", each email message is |
| 429 | @ stored as a separate file in the directory shown here. |
| 430 | @ (Property: "email-send-dir")</p> |
| 431 | |
| 432 | @ <hr> |
| 433 | |
| 434 | @ <p><input type="submit" name="submit" value="Apply Changes"></p> |
| 435 | @ </div></form> |
| 436 | db_end_transaction(0); |
| @@ -630,19 +638,27 @@ | |
| 638 | emailerGetSetting(p, &p->zCmd, "email-send-command"); |
| 639 | }else if( fossil_strcmp(p->zDest, "dir")==0 ){ |
| 640 | emailerGetSetting(p, &p->zDir, "email-send-dir"); |
| 641 | }else if( fossil_strcmp(p->zDest, "blob")==0 ){ |
| 642 | blob_init(&p->out, 0, 0); |
| 643 | }else if( fossil_strcmp(p->zDest, "relay")==0 |
| 644 | || fossil_strcmp(p->zDest, "debug-relay")==0 |
| 645 | ){ |
| 646 | const char *zRelay = 0; |
| 647 | emailerGetSetting(p, &zRelay, "email-send-relayhost"); |
| 648 | if( zRelay ){ |
| 649 | u32 smtpFlags = SMTP_DIRECT; |
| 650 | if( mFlags & ALERT_TRACE ) smtpFlags |= SMTP_TRACE_STDOUT; |
| 651 | blob_init(&p->out, 0, 0); |
| 652 | p->pSmtp = smtp_session_new(domain_of_addr(p->zFrom), zRelay, |
| 653 | smtpFlags, 0); |
| 654 | if( p->pSmtp==0 || p->pSmtp->zErr ){ |
| 655 | emailerError(p, "Could not start SMTP session: %s", |
| 656 | p->pSmtp ? p->pSmtp->zErr : "reason unknown"); |
| 657 | }else if( p->zDest[0]=='d' ){ |
| 658 | smtp_session_config(p->pSmtp, SMTP_TRACE_BLOB, &p->out); |
| 659 | } |
| 660 | } |
| 661 | } |
| 662 | return p; |
| 663 | } |
| 664 | |
| @@ -968,13 +984,10 @@ | |
| 984 | blob_appendf(pOut, "Sender: <%s>\r\n", p->zFrom); |
| 985 | }else{ |
| 986 | blob_appendf(pOut, "From: <%s>\r\n", p->zFrom); |
| 987 | } |
| 988 | blob_appendf(pOut, "Date: %z\r\n", cgi_rfc822_datestamp(time(0))); |
| 989 | if( strstr(blob_str(pHdr), "\r\nMessage-Id:")==0 ){ |
| 990 | /* Message-id format: "<$(date)x$(random)@$(from-host)>" where $(date) is |
| 991 | ** the current unix-time in hex, $(random) is a 64-bit random number, |
| 992 | ** and $(from) is the domain part of the email-self setting. */ |
| 993 | sqlite3_randomness(sizeof(r1), &r1); |
| @@ -1016,13 +1029,21 @@ | |
| 1029 | blob_write_to_file(&all, zFile); |
| 1030 | fossil_free(zFile); |
| 1031 | }else if( p->pSmtp ){ |
| 1032 | char **azTo = 0; |
| 1033 | int nTo = 0; |
| 1034 | SmtpSession *pSmtp = p->pSmtp; |
| 1035 | email_header_to(pHdr, &nTo, &azTo); |
| 1036 | if( nTo>0 && !pSmtp->bFatal ){ |
| 1037 | smtp_send_msg(pSmtp,p->zFrom,nTo,(const char**)azTo,blob_str(&all)); |
| 1038 | if( pSmtp->zErr && !pSmtp->bFatal ){ |
| 1039 | smtp_send_msg(pSmtp,p->zFrom,nTo,(const char**)azTo,blob_str(&all)); |
| 1040 | } |
| 1041 | if( pSmtp->zErr ){ |
| 1042 | fossil_errorlog("SMTP: (%s) %s", pSmtp->bFatal ? "fatal" : "retry", |
| 1043 | pSmtp->zErr); |
| 1044 | } |
| 1045 | email_header_to_free(nTo, azTo); |
| 1046 | } |
| 1047 | }else if( strcmp(p->zDest, "stdout")==0 ){ |
| 1048 | char **azTo = 0; |
| 1049 | int nTo = 0; |
| @@ -1125,11 +1146,11 @@ | |
| 1146 | ** SETTING: email-listid width=40 |
| 1147 | ** If this setting is not an empty string, then it becomes the argument to |
| 1148 | ** a "List-ID:" header that is added to all out-bound notification emails. |
| 1149 | */ |
| 1150 | /* |
| 1151 | ** SETTING: email-send-relayhost width=40 sensitive default=127.0.0.1 |
| 1152 | ** This is the hostname and TCP port to which output email messages |
| 1153 | ** are sent when email-send-method is "relay". There should be an |
| 1154 | ** SMTP server configured as a Mail Submission Agent listening on the |
| 1155 | ** designated host and port and all times. |
| 1156 | */ |
| @@ -1704,11 +1725,11 @@ | |
| 1725 | @ <label><input type="checkbox" name="sw" %s(PCK("sw"))> \ |
| 1726 | @ Wiki</label><br> |
| 1727 | } |
| 1728 | if( g.perm.Admin ){ |
| 1729 | @ <label><input type="checkbox" name="su" %s(PCK("su"))> \ |
| 1730 | @ User permission changes</label> |
| 1731 | } |
| 1732 | di = PB("di"); |
| 1733 | @ </td></tr> |
| 1734 | @ <tr> |
| 1735 | @ <td class="form_label">Delivery:</td> |
| @@ -2114,11 +2135,11 @@ | |
| 2135 | /* Corner-case bug: if an admin assigns 'u' to a non-admin, that |
| 2136 | ** subscription will get removed if the user later edits their |
| 2137 | ** subscriptions, as non-admins are not permitted to add that |
| 2138 | ** subscription. */ |
| 2139 | @ <label><input type="checkbox" name="su" %s(su?"checked":"")>\ |
| 2140 | @ User permission changes</label> |
| 2141 | } |
| 2142 | @ </td></tr> |
| 2143 | if( strchr(ssub,'k')!=0 ){ |
| 2144 | @ <tr><td></td><td> ↑ |
| 2145 | @ Note: User did a one-click unsubscribe</td></tr> |
| @@ -3191,18 +3212,21 @@ | |
| 3212 | Blob fhdr, fbody; |
| 3213 | blob_init(&fhdr, 0, 0); |
| 3214 | blob_appendf(&fhdr, "To: <%s>\r\n", zEmail); |
| 3215 | blob_append(&fhdr, blob_buffer(&p->hdr), blob_size(&p->hdr)); |
| 3216 | blob_init(&fbody, blob_buffer(&p->txt), blob_size(&p->txt)); |
| 3217 | if( pSender->zListId && pSender->zListId[0] ){ |
| 3218 | blob_appendf(&fhdr, "List-Id: %s\r\n", pSender->zListId); |
| 3219 | blob_appendf(&fhdr, "List-Unsubscribe: <%s/oneclickunsub/%s>\r\n", |
| 3220 | zUrl, zCode); |
| 3221 | blob_appendf(&fhdr, |
| 3222 | "List-Unsubscribe-Post: List-Unsubscribe=One-Click\r\n"); |
| 3223 | blob_appendf(&fbody, "\n-- \nUnsubscribe: %s/unsubscribe/%s\n", |
| 3224 | zUrl, zCode); |
| 3225 | /* blob_appendf(&fbody, "Subscription settings: %s/alerts/%s\n", |
| 3226 | ** zUrl, zCode); */ |
| 3227 | } |
| 3228 | alert_send(pSender,&fhdr,&fbody,p->zFromName); |
| 3229 | nSent++; |
| 3230 | blob_reset(&fhdr); |
| 3231 | blob_reset(&fbody); |
| 3232 | }else{ |
| @@ -3221,15 +3245,19 @@ | |
| 3245 | blob_append(&body, "\n", 1); |
| 3246 | blob_append(&body, blob_buffer(&p->txt), blob_size(&p->txt)); |
| 3247 | } |
| 3248 | } |
| 3249 | if( nHit==0 ) continue; |
| 3250 | if( pSender->zListId && pSender->zListId[0] ){ |
| 3251 | blob_appendf(&hdr, "List-Id: %s\r\n", pSender->zListId); |
| 3252 | blob_appendf(&hdr, "List-Unsubscribe: <%s/oneclickunsub/%s>\r\n", |
| 3253 | zUrl, zCode); |
| 3254 | blob_appendf(&hdr, |
| 3255 | "List-Unsubscribe-Post: List-Unsubscribe=One-Click\r\n"); |
| 3256 | blob_appendf(&body,"\n-- \nSubscription info: %s/alerts/%s\n", |
| 3257 | zUrl, zCode); |
| 3258 | } |
| 3259 | alert_send(pSender,&hdr,&body,0); |
| 3260 | nSent++; |
| 3261 | blob_truncate(&hdr, 0); |
| 3262 | blob_truncate(&body, 0); |
| 3263 | } |
| @@ -3271,18 +3299,28 @@ | |
| 3299 | " AND length(sdigest)>0", |
| 3300 | iNewWarn, iOldWarn |
| 3301 | ); |
| 3302 | while( db_step(&q)==SQLITE_ROW ){ |
| 3303 | Blob hdr, body; |
| 3304 | const char *zCode = db_column_text(&q,0); |
| 3305 | blob_init(&hdr, 0, 0); |
| 3306 | blob_init(&body, 0, 0); |
| 3307 | alert_renewal_msg(&hdr, &body, |
| 3308 | zCode, |
| 3309 | db_column_int(&q,1), |
| 3310 | db_column_text(&q,2), |
| 3311 | db_column_text(&q,3), |
| 3312 | zRepoName, zUrl); |
| 3313 | if( pSender->zListId && pSender->zListId[0] ){ |
| 3314 | blob_appendf(&hdr, "List-Id: %s\r\n", pSender->zListId); |
| 3315 | blob_appendf(&hdr, "List-Unsubscribe: <%s/oneclickunsub/%s>\r\n", |
| 3316 | zUrl, zCode); |
| 3317 | blob_appendf(&hdr, |
| 3318 | "List-Unsubscribe-Post: List-Unsubscribe=One-Click\r\n"); |
| 3319 | blob_appendf(&body, "\n-- \nUnsubscribe: %s/unsubscribe/%s\n", |
| 3320 | zUrl, zCode); |
| 3321 | } |
| 3322 | alert_send(pSender,&hdr,&body,0); |
| 3323 | blob_reset(&hdr); |
| 3324 | blob_reset(&body); |
| 3325 | } |
| 3326 | db_finalize(&q); |
| @@ -3436,16 +3474,28 @@ | |
| 3474 | char *zSubject = PT("subject"); |
| 3475 | int bAll = PB("all"); |
| 3476 | int bAA = PB("aa"); |
| 3477 | int bMods = PB("mods"); |
| 3478 | const char *zSub = db_get("email-subname", "[Fossil Repo]"); |
| 3479 | const char *zName = P("name"); /* Debugging options */ |
| 3480 | const char *zDest = 0; /* How to send the announcement */ |
| 3481 | int bTest = 0; |
| 3482 | Blob hdr, body; |
| 3483 | |
| 3484 | if( fossil_strcmp(zName, "test2")==0 ){ |
| 3485 | bTest = 2; |
| 3486 | zDest = "blob"; |
| 3487 | }else if( fossil_strcmp(zName, "test3")==0 ){ |
| 3488 | bTest = 3; |
| 3489 | if( fossil_strcmp(db_get("email-send-method",""),"relay")==0 ){ |
| 3490 | zDest = "debug-relay"; |
| 3491 | } |
| 3492 | } |
| 3493 | blob_init(&body, 0, 0); |
| 3494 | blob_init(&hdr, 0, 0); |
| 3495 | blob_appendf(&body, "%s", PT("msg")/*safe-for-%s*/); |
| 3496 | pSender = alert_sender_new(zDest, 0); |
| 3497 | if( zTo[0] ){ |
| 3498 | blob_appendf(&hdr, "To: <%s>\r\nSubject: %s %s\r\n", zTo, zSub, zSubject); |
| 3499 | alert_send(pSender, &hdr, &body, 0); |
| 3500 | } |
| 3501 | if( bAll || bAA || bMods ){ |
| @@ -3479,17 +3529,24 @@ | |
| 3529 | } |
| 3530 | alert_send(pSender, &hdr, &body, 0); |
| 3531 | } |
| 3532 | db_finalize(&q); |
| 3533 | } |
| 3534 | if( bTest && blob_size(&pSender->out) ){ |
| 3535 | /* If the URL is "/announce/test2" then no email is actually sent. |
| 3536 | ** Instead, the text of the email that would have been sent is |
| 3537 | ** displayed in the result window. |
| 3538 | ** |
| 3539 | ** If the URL is "/announce/test3" and the email-send-method is "relay" |
| 3540 | ** then the announcement is sent as it normally would be, but a |
| 3541 | ** transcript of the SMTP conversation with the MTA is shown here. |
| 3542 | */ |
| 3543 | blob_trim(&pSender->out); |
| 3544 | @ <pre style='border: 2px solid blue; padding: 1ex;'> |
| 3545 | @ %h(blob_str(&pSender->out)) |
| 3546 | @ </pre> |
| 3547 | blob_reset(&pSender->out); |
| 3548 | } |
| 3549 | zErr = pSender->zErr; |
| 3550 | pSender->zErr = 0; |
| 3551 | alert_sender_free(pSender); |
| 3552 | return zErr; |
| @@ -3505,35 +3562,43 @@ | |
| 3562 | ** also send a message to an arbitrary email address and/or to all |
| 3563 | ** subscribers regardless of whether or not they have elected to |
| 3564 | ** receive announcements. |
| 3565 | */ |
| 3566 | void announce_page(void){ |
| 3567 | const char *zAction = "announce"; |
| 3568 | const char *zName = PD("name",""); |
| 3569 | /* |
| 3570 | ** Debugging Notes: |
| 3571 | ** |
| 3572 | ** /announce/test1 -> Shows query parameter values |
| 3573 | ** /announce/test2 -> Shows the formatted message but does |
| 3574 | ** not send it. |
| 3575 | ** /announce/test3 -> Sends the message, but also shows |
| 3576 | ** the SMTP transcript. |
| 3577 | */ |
| 3578 | login_check_credentials(); |
| 3579 | if( !g.perm.Announce ){ |
| 3580 | login_needed(0); |
| 3581 | return; |
| 3582 | } |
| 3583 | if( !g.perm.Setup ){ |
| 3584 | zName = 0; /* Disable debugging feature for non-admin users */ |
| 3585 | } |
| 3586 | style_set_current_feature("alerts"); |
| 3587 | if( fossil_strcmp(zName,"test1")==0 ){ |
| 3588 | /* Visit the /announce/test1 page to see the CGI variables */ |
| 3589 | zAction = "announce/test1"; |
| 3590 | @ <p style='border: 1px solid black; padding: 1ex;'> |
| 3591 | cgi_print_all(0, 0, 0); |
| 3592 | @ </p> |
| 3593 | }else if( P("submit")!=0 && cgi_csrf_safe(2) ){ |
| 3594 | char *zErr = alert_send_announcement(); |
| 3595 | style_header("Announcement Sent"); |
| 3596 | if( zErr ){ |
| 3597 | @ <h1>Error</h1> |
| 3598 | @ <p>The following error was reported by the |
| 3599 | @ announcement-sending subsystem: |
| 3600 | @ <blockquote><pre> |
| 3601 | @ %h(zErr) |
| 3602 | @ </pre></blockquote> |
| 3603 | }else{ |
| 3604 | @ <p>The announcement has been sent. |
| @@ -3548,10 +3613,16 @@ | |
| 3613 | @ for this repository.</p> |
| 3614 | return; |
| 3615 | } |
| 3616 | |
| 3617 | style_header("Send Announcement"); |
| 3618 | alert_submenu_common(); |
| 3619 | if( fossil_strcmp(zName,"test2")==0 ){ |
| 3620 | zAction = "announce/test2"; |
| 3621 | }else if( fossil_strcmp(zName,"test3")==0 ){ |
| 3622 | zAction = "announce/test3"; |
| 3623 | } |
| 3624 | @ <form method="POST" action="%R/%s(zAction)"> |
| 3625 | login_insert_csrf_secret(); |
| 3626 | @ <table class="subscribe"> |
| 3627 | if( g.perm.Admin ){ |
| 3628 | int aa = PB("aa"); |
| @@ -3584,15 +3655,28 @@ | |
| 3655 | @ <td><textarea name="msg" cols="80" rows="10" wrap="virtual">\ |
| 3656 | @ %h(PT("msg"))</textarea> |
| 3657 | @ </tr> |
| 3658 | @ <tr> |
| 3659 | @ <td></td> |
| 3660 | if( fossil_strcmp(zName,"test2")==0 ){ |
| 3661 | @ <td><input type="submit" name="submit" value="Dry Run"> |
| 3662 | }else{ |
| 3663 | @ <td><input type="submit" name="submit" value="Send Message"> |
| 3664 | } |
| 3665 | @ </tr> |
| 3666 | @ </table> |
| 3667 | @ </form> |
| 3668 | if( g.perm.Setup ){ |
| 3669 | @ <hr> |
| 3670 | @ <p>Trouble-shooting Options:</p> |
| 3671 | @ <ol> |
| 3672 | @ <li> <a href="%R/announce">Normal Processing</a> |
| 3673 | @ <li> Only <a href="%R/announce/test1">show POST parameters</a> |
| 3674 | @ - Do not send the announcement. |
| 3675 | @ <li> <a href="%R/announce/test2">Show the email text</a> but do |
| 3676 | @ not actually send it. |
| 3677 | @ <li> Send the message and also <a href="%R/announce/test3">show the |
| 3678 | @ SMTP traffic</a> when using "relay" mode. |
| 3679 | @ </ol> |
| 3680 | } |
| 3681 | style_finish_page(); |
| 3682 | } |
| 3683 |
+12
-5
| --- src/backoffice.c | ||
| +++ src/backoffice.c | ||
| @@ -38,11 +38,11 @@ | ||
| 38 | 38 | ** process table, doing nothing on rarely accessed repositories, and |
| 39 | 39 | ** if the Fossil binary is updated on a system, the backoffice processes |
| 40 | 40 | ** will restart using the new binary automatically. |
| 41 | 41 | ** |
| 42 | 42 | ** At any point in time there should be at most two backoffice processes. |
| 43 | -** There is a main process that is doing the actually work, and there is | |
| 43 | +** There is a main process that is doing the actual work, and there is | |
| 44 | 44 | ** a second stand-by process that is waiting for the main process to finish |
| 45 | 45 | ** and that will become the main process after a delay. |
| 46 | 46 | ** |
| 47 | 47 | ** After any successful web page reply, the backoffice_check_if_needed() |
| 48 | 48 | ** routine is called. That routine checks to see if both one or both of |
| @@ -53,11 +53,11 @@ | ||
| 53 | 53 | ** backoffice_run_if_needed() routine is called. If the prior call |
| 54 | 54 | ** to backoffice_check_if_needed() indicated that backoffice processing |
| 55 | 55 | ** might be required, the run_if_needed() attempts to kick off a backoffice |
| 56 | 56 | ** process. |
| 57 | 57 | ** |
| 58 | -** All work performance by the backoffice is in the backoffice_work() | |
| 58 | +** All work performed by the backoffice is in the backoffice_work() | |
| 59 | 59 | ** routine. |
| 60 | 60 | */ |
| 61 | 61 | #if defined(_WIN32) |
| 62 | 62 | # if defined(_WIN32_WINNT) |
| 63 | 63 | # undef _WIN32_WINNT |
| @@ -485,11 +485,11 @@ | ||
| 485 | 485 | int warningDelay = 30; |
| 486 | 486 | static int once = 0; |
| 487 | 487 | |
| 488 | 488 | if( sqlite3_db_readonly(g.db, 0) ) return; |
| 489 | 489 | if( db_is_protected(PROTECT_READONLY) ) return; |
| 490 | - g.zPhase = "backoffice"; | |
| 490 | + g.zPhase = "backoffice-pending"; | |
| 491 | 491 | backoffice_error_check_one(&once); |
| 492 | 492 | idSelf = backofficeProcessId(); |
| 493 | 493 | while(1){ |
| 494 | 494 | tmNow = time(0); |
| 495 | 495 | db_begin_write(); |
| @@ -510,10 +510,11 @@ | ||
| 510 | 510 | /* This process can start doing backoffice work immediately */ |
| 511 | 511 | x.idCurrent = idSelf; |
| 512 | 512 | x.tmCurrent = tmNow + BKOFCE_LEASE_TIME; |
| 513 | 513 | x.idNext = 0; |
| 514 | 514 | x.tmNext = 0; |
| 515 | + g.zPhase = "backoffice-work"; | |
| 515 | 516 | backofficeWriteLease(&x); |
| 516 | 517 | db_end_transaction(0); |
| 517 | 518 | backofficeTrace("/***** Begin Backoffice Processing %d *****/\n", |
| 518 | 519 | GETPID()); |
| 519 | 520 | backoffice_work(); |
| @@ -543,13 +544,16 @@ | ||
| 543 | 544 | db_end_transaction(0); |
| 544 | 545 | break; |
| 545 | 546 | } |
| 546 | 547 | }else{ |
| 547 | 548 | if( (sqlite3_uint64)(lastWarning+warningDelay) < tmNow ){ |
| 548 | - fossil_warning( | |
| 549 | + sqlite3_int64 runningFor = BKOFCE_LEASE_TIME + tmNow - x.tmCurrent; | |
| 550 | + if( warningDelay>=240 && runningFor<1800 ){ | |
| 551 | + fossil_warning( | |
| 549 | 552 | "backoffice process %lld still running after %d seconds", |
| 550 | - x.idCurrent, (int)(BKOFCE_LEASE_TIME + tmNow - x.tmCurrent)); | |
| 553 | + x.idCurrent, runningFor); | |
| 554 | + } | |
| 551 | 555 | lastWarning = tmNow; |
| 552 | 556 | warningDelay *= 2; |
| 553 | 557 | } |
| 554 | 558 | if( backofficeSleep(1000) ){ |
| 555 | 559 | /* The sleep was interrupted by a signal from another thread. */ |
| @@ -642,14 +646,17 @@ | ||
| 642 | 646 | backofficeBlob = &log; |
| 643 | 647 | blob_appendf(&log, "%s %s", db_text(0, "SELECT datetime('now')"), zName); |
| 644 | 648 | } |
| 645 | 649 | |
| 646 | 650 | /* Here is where the actual work of the backoffice happens */ |
| 651 | + g.zPhase = "backoffice-alerts"; | |
| 647 | 652 | nThis = alert_backoffice(0); |
| 648 | 653 | if( nThis ){ backoffice_log("%d alerts", nThis); nTotal += nThis; } |
| 654 | + g.zPhase = "backoffice-hooks"; | |
| 649 | 655 | nThis = hook_backoffice(); |
| 650 | 656 | if( nThis ){ backoffice_log("%d hooks", nThis); nTotal += nThis; } |
| 657 | + g.zPhase = "backoffice-close"; | |
| 651 | 658 | |
| 652 | 659 | /* Close the log */ |
| 653 | 660 | if( backofficeFILE ){ |
| 654 | 661 | if( nTotal || backofficeLogDetail ){ |
| 655 | 662 | if( nTotal==0 ) backoffice_log("no-op"); |
| 656 | 663 |
| --- src/backoffice.c | |
| +++ src/backoffice.c | |
| @@ -38,11 +38,11 @@ | |
| 38 | ** process table, doing nothing on rarely accessed repositories, and |
| 39 | ** if the Fossil binary is updated on a system, the backoffice processes |
| 40 | ** will restart using the new binary automatically. |
| 41 | ** |
| 42 | ** At any point in time there should be at most two backoffice processes. |
| 43 | ** There is a main process that is doing the actually work, and there is |
| 44 | ** a second stand-by process that is waiting for the main process to finish |
| 45 | ** and that will become the main process after a delay. |
| 46 | ** |
| 47 | ** After any successful web page reply, the backoffice_check_if_needed() |
| 48 | ** routine is called. That routine checks to see if both one or both of |
| @@ -53,11 +53,11 @@ | |
| 53 | ** backoffice_run_if_needed() routine is called. If the prior call |
| 54 | ** to backoffice_check_if_needed() indicated that backoffice processing |
| 55 | ** might be required, the run_if_needed() attempts to kick off a backoffice |
| 56 | ** process. |
| 57 | ** |
| 58 | ** All work performance by the backoffice is in the backoffice_work() |
| 59 | ** routine. |
| 60 | */ |
| 61 | #if defined(_WIN32) |
| 62 | # if defined(_WIN32_WINNT) |
| 63 | # undef _WIN32_WINNT |
| @@ -485,11 +485,11 @@ | |
| 485 | int warningDelay = 30; |
| 486 | static int once = 0; |
| 487 | |
| 488 | if( sqlite3_db_readonly(g.db, 0) ) return; |
| 489 | if( db_is_protected(PROTECT_READONLY) ) return; |
| 490 | g.zPhase = "backoffice"; |
| 491 | backoffice_error_check_one(&once); |
| 492 | idSelf = backofficeProcessId(); |
| 493 | while(1){ |
| 494 | tmNow = time(0); |
| 495 | db_begin_write(); |
| @@ -510,10 +510,11 @@ | |
| 510 | /* This process can start doing backoffice work immediately */ |
| 511 | x.idCurrent = idSelf; |
| 512 | x.tmCurrent = tmNow + BKOFCE_LEASE_TIME; |
| 513 | x.idNext = 0; |
| 514 | x.tmNext = 0; |
| 515 | backofficeWriteLease(&x); |
| 516 | db_end_transaction(0); |
| 517 | backofficeTrace("/***** Begin Backoffice Processing %d *****/\n", |
| 518 | GETPID()); |
| 519 | backoffice_work(); |
| @@ -543,13 +544,16 @@ | |
| 543 | db_end_transaction(0); |
| 544 | break; |
| 545 | } |
| 546 | }else{ |
| 547 | if( (sqlite3_uint64)(lastWarning+warningDelay) < tmNow ){ |
| 548 | fossil_warning( |
| 549 | "backoffice process %lld still running after %d seconds", |
| 550 | x.idCurrent, (int)(BKOFCE_LEASE_TIME + tmNow - x.tmCurrent)); |
| 551 | lastWarning = tmNow; |
| 552 | warningDelay *= 2; |
| 553 | } |
| 554 | if( backofficeSleep(1000) ){ |
| 555 | /* The sleep was interrupted by a signal from another thread. */ |
| @@ -642,14 +646,17 @@ | |
| 642 | backofficeBlob = &log; |
| 643 | blob_appendf(&log, "%s %s", db_text(0, "SELECT datetime('now')"), zName); |
| 644 | } |
| 645 | |
| 646 | /* Here is where the actual work of the backoffice happens */ |
| 647 | nThis = alert_backoffice(0); |
| 648 | if( nThis ){ backoffice_log("%d alerts", nThis); nTotal += nThis; } |
| 649 | nThis = hook_backoffice(); |
| 650 | if( nThis ){ backoffice_log("%d hooks", nThis); nTotal += nThis; } |
| 651 | |
| 652 | /* Close the log */ |
| 653 | if( backofficeFILE ){ |
| 654 | if( nTotal || backofficeLogDetail ){ |
| 655 | if( nTotal==0 ) backoffice_log("no-op"); |
| 656 |
| --- src/backoffice.c | |
| +++ src/backoffice.c | |
| @@ -38,11 +38,11 @@ | |
| 38 | ** process table, doing nothing on rarely accessed repositories, and |
| 39 | ** if the Fossil binary is updated on a system, the backoffice processes |
| 40 | ** will restart using the new binary automatically. |
| 41 | ** |
| 42 | ** At any point in time there should be at most two backoffice processes. |
| 43 | ** There is a main process that is doing the actual work, and there is |
| 44 | ** a second stand-by process that is waiting for the main process to finish |
| 45 | ** and that will become the main process after a delay. |
| 46 | ** |
| 47 | ** After any successful web page reply, the backoffice_check_if_needed() |
| 48 | ** routine is called. That routine checks to see if both one or both of |
| @@ -53,11 +53,11 @@ | |
| 53 | ** backoffice_run_if_needed() routine is called. If the prior call |
| 54 | ** to backoffice_check_if_needed() indicated that backoffice processing |
| 55 | ** might be required, the run_if_needed() attempts to kick off a backoffice |
| 56 | ** process. |
| 57 | ** |
| 58 | ** All work performed by the backoffice is in the backoffice_work() |
| 59 | ** routine. |
| 60 | */ |
| 61 | #if defined(_WIN32) |
| 62 | # if defined(_WIN32_WINNT) |
| 63 | # undef _WIN32_WINNT |
| @@ -485,11 +485,11 @@ | |
| 485 | int warningDelay = 30; |
| 486 | static int once = 0; |
| 487 | |
| 488 | if( sqlite3_db_readonly(g.db, 0) ) return; |
| 489 | if( db_is_protected(PROTECT_READONLY) ) return; |
| 490 | g.zPhase = "backoffice-pending"; |
| 491 | backoffice_error_check_one(&once); |
| 492 | idSelf = backofficeProcessId(); |
| 493 | while(1){ |
| 494 | tmNow = time(0); |
| 495 | db_begin_write(); |
| @@ -510,10 +510,11 @@ | |
| 510 | /* This process can start doing backoffice work immediately */ |
| 511 | x.idCurrent = idSelf; |
| 512 | x.tmCurrent = tmNow + BKOFCE_LEASE_TIME; |
| 513 | x.idNext = 0; |
| 514 | x.tmNext = 0; |
| 515 | g.zPhase = "backoffice-work"; |
| 516 | backofficeWriteLease(&x); |
| 517 | db_end_transaction(0); |
| 518 | backofficeTrace("/***** Begin Backoffice Processing %d *****/\n", |
| 519 | GETPID()); |
| 520 | backoffice_work(); |
| @@ -543,13 +544,16 @@ | |
| 544 | db_end_transaction(0); |
| 545 | break; |
| 546 | } |
| 547 | }else{ |
| 548 | if( (sqlite3_uint64)(lastWarning+warningDelay) < tmNow ){ |
| 549 | sqlite3_int64 runningFor = BKOFCE_LEASE_TIME + tmNow - x.tmCurrent; |
| 550 | if( warningDelay>=240 && runningFor<1800 ){ |
| 551 | fossil_warning( |
| 552 | "backoffice process %lld still running after %d seconds", |
| 553 | x.idCurrent, runningFor); |
| 554 | } |
| 555 | lastWarning = tmNow; |
| 556 | warningDelay *= 2; |
| 557 | } |
| 558 | if( backofficeSleep(1000) ){ |
| 559 | /* The sleep was interrupted by a signal from another thread. */ |
| @@ -642,14 +646,17 @@ | |
| 646 | backofficeBlob = &log; |
| 647 | blob_appendf(&log, "%s %s", db_text(0, "SELECT datetime('now')"), zName); |
| 648 | } |
| 649 | |
| 650 | /* Here is where the actual work of the backoffice happens */ |
| 651 | g.zPhase = "backoffice-alerts"; |
| 652 | nThis = alert_backoffice(0); |
| 653 | if( nThis ){ backoffice_log("%d alerts", nThis); nTotal += nThis; } |
| 654 | g.zPhase = "backoffice-hooks"; |
| 655 | nThis = hook_backoffice(); |
| 656 | if( nThis ){ backoffice_log("%d hooks", nThis); nTotal += nThis; } |
| 657 | g.zPhase = "backoffice-close"; |
| 658 | |
| 659 | /* Close the log */ |
| 660 | if( backofficeFILE ){ |
| 661 | if( nTotal || backofficeLogDetail ){ |
| 662 | if( nTotal==0 ) backoffice_log("no-op"); |
| 663 |
+8
-7
| --- src/branch.c | ||
| +++ src/branch.c | ||
| @@ -657,25 +657,25 @@ | ||
| 657 | 657 | ** |
| 658 | 658 | ** > fossil branch new BRANCH-NAME BASIS ?OPTIONS? |
| 659 | 659 | ** |
| 660 | 660 | ** Create a new branch BRANCH-NAME off of check-in BASIS. |
| 661 | 661 | ** |
| 662 | +** This command is available for people who want to create a branch | |
| 663 | +** in advance. But the use of this command is discouraged. The | |
| 664 | +** preferred idiom in Fossil is to create new branches at the point | |
| 665 | +** of need, using the "--branch NAME" option to the "fossil commit" | |
| 666 | +** command. | |
| 667 | +** | |
| 662 | 668 | ** Options: |
| 663 | 669 | ** --private Branch is private (i.e., remains local) |
| 664 | 670 | ** --bgcolor COLOR Use COLOR instead of automatic background |
| 665 | 671 | ** --nosign Do not sign the manifest for the check-in |
| 666 | 672 | ** that creates this branch |
| 667 | 673 | ** --nosync Do not auto-sync prior to creating the branch |
| 668 | 674 | ** --date-override DATE DATE to use instead of 'now' |
| 669 | 675 | ** --user-override USER USER to use instead of the current default |
| 670 | 676 | ** |
| 671 | -** DATE may be "now" or "YYYY-MM-DDTHH:MM:SS.SSS". If in | |
| 672 | -** year-month-day form, it may be truncated, the "T" may be | |
| 673 | -** replaced by a space, and it may also name a timezone offset | |
| 674 | -** from UTC as "-HH:MM" (westward) or "+HH:MM" (eastward). | |
| 675 | -** Either no timezone suffix or "Z" means UTC. | |
| 676 | -** | |
| 677 | 677 | ** Options: |
| 678 | 678 | ** -R|--repository REPO Run commands on repository REPO |
| 679 | 679 | */ |
| 680 | 680 | void branch_cmd(void){ |
| 681 | 681 | int n; |
| @@ -870,11 +870,12 @@ | ||
| 870 | 870 | const char *zLastCkin = db_column_text(&q, 5); |
| 871 | 871 | const char *zBgClr = db_column_text(&q, 6); |
| 872 | 872 | char *zAge = human_readable_age(rNow - rMtime); |
| 873 | 873 | sqlite3_int64 iMtime = (sqlite3_int64)(rMtime*86400.0); |
| 874 | 874 | if( zMergeTo && zMergeTo[0]==0 ) zMergeTo = 0; |
| 875 | - if( zBgClr == 0 ){ | |
| 875 | + if( zBgClr ) zBgClr = reasonable_bg_color(zBgClr, 0); | |
| 876 | + if( zBgClr==0 ){ | |
| 876 | 877 | if( zBranch==0 || strcmp(zBranch,"trunk")==0 ){ |
| 877 | 878 | zBgClr = 0; |
| 878 | 879 | }else{ |
| 879 | 880 | zBgClr = hash_color(zBranch); |
| 880 | 881 | } |
| 881 | 882 |
| --- src/branch.c | |
| +++ src/branch.c | |
| @@ -657,25 +657,25 @@ | |
| 657 | ** |
| 658 | ** > fossil branch new BRANCH-NAME BASIS ?OPTIONS? |
| 659 | ** |
| 660 | ** Create a new branch BRANCH-NAME off of check-in BASIS. |
| 661 | ** |
| 662 | ** Options: |
| 663 | ** --private Branch is private (i.e., remains local) |
| 664 | ** --bgcolor COLOR Use COLOR instead of automatic background |
| 665 | ** --nosign Do not sign the manifest for the check-in |
| 666 | ** that creates this branch |
| 667 | ** --nosync Do not auto-sync prior to creating the branch |
| 668 | ** --date-override DATE DATE to use instead of 'now' |
| 669 | ** --user-override USER USER to use instead of the current default |
| 670 | ** |
| 671 | ** DATE may be "now" or "YYYY-MM-DDTHH:MM:SS.SSS". If in |
| 672 | ** year-month-day form, it may be truncated, the "T" may be |
| 673 | ** replaced by a space, and it may also name a timezone offset |
| 674 | ** from UTC as "-HH:MM" (westward) or "+HH:MM" (eastward). |
| 675 | ** Either no timezone suffix or "Z" means UTC. |
| 676 | ** |
| 677 | ** Options: |
| 678 | ** -R|--repository REPO Run commands on repository REPO |
| 679 | */ |
| 680 | void branch_cmd(void){ |
| 681 | int n; |
| @@ -870,11 +870,12 @@ | |
| 870 | const char *zLastCkin = db_column_text(&q, 5); |
| 871 | const char *zBgClr = db_column_text(&q, 6); |
| 872 | char *zAge = human_readable_age(rNow - rMtime); |
| 873 | sqlite3_int64 iMtime = (sqlite3_int64)(rMtime*86400.0); |
| 874 | if( zMergeTo && zMergeTo[0]==0 ) zMergeTo = 0; |
| 875 | if( zBgClr == 0 ){ |
| 876 | if( zBranch==0 || strcmp(zBranch,"trunk")==0 ){ |
| 877 | zBgClr = 0; |
| 878 | }else{ |
| 879 | zBgClr = hash_color(zBranch); |
| 880 | } |
| 881 |
| --- src/branch.c | |
| +++ src/branch.c | |
| @@ -657,25 +657,25 @@ | |
| 657 | ** |
| 658 | ** > fossil branch new BRANCH-NAME BASIS ?OPTIONS? |
| 659 | ** |
| 660 | ** Create a new branch BRANCH-NAME off of check-in BASIS. |
| 661 | ** |
| 662 | ** This command is available for people who want to create a branch |
| 663 | ** in advance. But the use of this command is discouraged. The |
| 664 | ** preferred idiom in Fossil is to create new branches at the point |
| 665 | ** of need, using the "--branch NAME" option to the "fossil commit" |
| 666 | ** command. |
| 667 | ** |
| 668 | ** Options: |
| 669 | ** --private Branch is private (i.e., remains local) |
| 670 | ** --bgcolor COLOR Use COLOR instead of automatic background |
| 671 | ** --nosign Do not sign the manifest for the check-in |
| 672 | ** that creates this branch |
| 673 | ** --nosync Do not auto-sync prior to creating the branch |
| 674 | ** --date-override DATE DATE to use instead of 'now' |
| 675 | ** --user-override USER USER to use instead of the current default |
| 676 | ** |
| 677 | ** Options: |
| 678 | ** -R|--repository REPO Run commands on repository REPO |
| 679 | */ |
| 680 | void branch_cmd(void){ |
| 681 | int n; |
| @@ -870,11 +870,12 @@ | |
| 870 | const char *zLastCkin = db_column_text(&q, 5); |
| 871 | const char *zBgClr = db_column_text(&q, 6); |
| 872 | char *zAge = human_readable_age(rNow - rMtime); |
| 873 | sqlite3_int64 iMtime = (sqlite3_int64)(rMtime*86400.0); |
| 874 | if( zMergeTo && zMergeTo[0]==0 ) zMergeTo = 0; |
| 875 | if( zBgClr ) zBgClr = reasonable_bg_color(zBgClr, 0); |
| 876 | if( zBgClr==0 ){ |
| 877 | if( zBranch==0 || strcmp(zBranch,"trunk")==0 ){ |
| 878 | zBgClr = 0; |
| 879 | }else{ |
| 880 | zBgClr = hash_color(zBranch); |
| 881 | } |
| 882 |
+2
-2
| --- src/browse.c | ||
| +++ src/browse.c | ||
| @@ -205,11 +205,11 @@ | ||
| 205 | 205 | linkTip = rid != symbolic_name_to_rid("tip", "ci"); |
| 206 | 206 | zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid); |
| 207 | 207 | isSymbolicCI = (sqlite3_strnicmp(zUuid, zCI, strlen(zCI))!=0); |
| 208 | 208 | isBranchCI = branch_includes_uuid(zCI, zUuid); |
| 209 | 209 | if( bDocDir ) zCI = mprintf("%S", zUuid); |
| 210 | - Th_Store("current_checkin", zCI); | |
| 210 | + Th_StoreUnsafe("current_checkin", zCI); | |
| 211 | 211 | }else{ |
| 212 | 212 | zCI = 0; |
| 213 | 213 | } |
| 214 | 214 | } |
| 215 | 215 | |
| @@ -771,11 +771,11 @@ | ||
| 771 | 771 | rNow = db_double(0.0, "SELECT mtime FROM event WHERE objid=%d", rid); |
| 772 | 772 | zNow = db_text("", "SELECT datetime(mtime,toLocal())" |
| 773 | 773 | " FROM event WHERE objid=%d", rid); |
| 774 | 774 | isSymbolicCI = (sqlite3_strnicmp(zUuid, zCI, strlen(zCI)) != 0); |
| 775 | 775 | isBranchCI = branch_includes_uuid(zCI, zUuid); |
| 776 | - Th_Store("current_checkin", zCI); | |
| 776 | + Th_StoreUnsafe("current_checkin", zCI); | |
| 777 | 777 | }else{ |
| 778 | 778 | zCI = 0; |
| 779 | 779 | } |
| 780 | 780 | } |
| 781 | 781 | if( zCI==0 ){ |
| 782 | 782 |
| --- src/browse.c | |
| +++ src/browse.c | |
| @@ -205,11 +205,11 @@ | |
| 205 | linkTip = rid != symbolic_name_to_rid("tip", "ci"); |
| 206 | zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid); |
| 207 | isSymbolicCI = (sqlite3_strnicmp(zUuid, zCI, strlen(zCI))!=0); |
| 208 | isBranchCI = branch_includes_uuid(zCI, zUuid); |
| 209 | if( bDocDir ) zCI = mprintf("%S", zUuid); |
| 210 | Th_Store("current_checkin", zCI); |
| 211 | }else{ |
| 212 | zCI = 0; |
| 213 | } |
| 214 | } |
| 215 | |
| @@ -771,11 +771,11 @@ | |
| 771 | rNow = db_double(0.0, "SELECT mtime FROM event WHERE objid=%d", rid); |
| 772 | zNow = db_text("", "SELECT datetime(mtime,toLocal())" |
| 773 | " FROM event WHERE objid=%d", rid); |
| 774 | isSymbolicCI = (sqlite3_strnicmp(zUuid, zCI, strlen(zCI)) != 0); |
| 775 | isBranchCI = branch_includes_uuid(zCI, zUuid); |
| 776 | Th_Store("current_checkin", zCI); |
| 777 | }else{ |
| 778 | zCI = 0; |
| 779 | } |
| 780 | } |
| 781 | if( zCI==0 ){ |
| 782 |
| --- src/browse.c | |
| +++ src/browse.c | |
| @@ -205,11 +205,11 @@ | |
| 205 | linkTip = rid != symbolic_name_to_rid("tip", "ci"); |
| 206 | zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid); |
| 207 | isSymbolicCI = (sqlite3_strnicmp(zUuid, zCI, strlen(zCI))!=0); |
| 208 | isBranchCI = branch_includes_uuid(zCI, zUuid); |
| 209 | if( bDocDir ) zCI = mprintf("%S", zUuid); |
| 210 | Th_StoreUnsafe("current_checkin", zCI); |
| 211 | }else{ |
| 212 | zCI = 0; |
| 213 | } |
| 214 | } |
| 215 | |
| @@ -771,11 +771,11 @@ | |
| 771 | rNow = db_double(0.0, "SELECT mtime FROM event WHERE objid=%d", rid); |
| 772 | zNow = db_text("", "SELECT datetime(mtime,toLocal())" |
| 773 | " FROM event WHERE objid=%d", rid); |
| 774 | isSymbolicCI = (sqlite3_strnicmp(zUuid, zCI, strlen(zCI)) != 0); |
| 775 | isBranchCI = branch_includes_uuid(zCI, zUuid); |
| 776 | Th_StoreUnsafe("current_checkin", zCI); |
| 777 | }else{ |
| 778 | zCI = 0; |
| 779 | } |
| 780 | } |
| 781 | if( zCI==0 ){ |
| 782 |
+54
-28
| --- src/cache.c | ||
| +++ src/cache.c | ||
| @@ -399,62 +399,88 @@ | ||
| 399 | 399 | ** WEBPAGE: cachestat |
| 400 | 400 | ** |
| 401 | 401 | ** Show information about the webpage cache. Requires Setup privilege. |
| 402 | 402 | */ |
| 403 | 403 | void cache_page(void){ |
| 404 | - sqlite3 *db; | |
| 404 | + sqlite3 *db = 0; | |
| 405 | 405 | sqlite3_stmt *pStmt; |
| 406 | + int doInit; | |
| 407 | + char *zDbName = cacheName(); | |
| 408 | + int nEntry = 0; | |
| 409 | + int mxEntry = 0; | |
| 406 | 410 | char zBuf[100]; |
| 407 | 411 | |
| 408 | 412 | login_check_credentials(); |
| 409 | 413 | if( !g.perm.Setup ){ login_needed(0); return; } |
| 410 | 414 | style_set_current_feature("cache"); |
| 411 | 415 | style_header("Web Cache Status"); |
| 412 | - db = cacheOpen(0); | |
| 413 | - if( db==0 ){ | |
| 414 | - @ The web-page cache is disabled for this repository | |
| 415 | - }else{ | |
| 416 | - char *zDbName = cacheName(); | |
| 416 | + style_submenu_element("Refresh","%R/cachestat"); | |
| 417 | + doInit = P("init")!=0 && cgi_csrf_safe(2); | |
| 418 | + db = cacheOpen(doInit); | |
| 419 | + if( db!=0 ){ | |
| 420 | + if( P("clear")!=0 && cgi_csrf_safe(2) ){ | |
| 421 | + sqlite3_exec(db, "DELETE FROM cache; DELETE FROM blob; VACUUM;",0,0,0); | |
| 422 | + } | |
| 417 | 423 | cache_register_sizename(db); |
| 418 | 424 | pStmt = cacheStmt(db, |
| 419 | 425 | "SELECT key, sz, nRef, datetime(tm,'unixepoch')" |
| 420 | 426 | " FROM cache" |
| 421 | 427 | " ORDER BY (tm + 3600*min(nRef,48)) DESC" |
| 422 | 428 | ); |
| 423 | 429 | if( pStmt ){ |
| 424 | - @ <ol> | |
| 425 | 430 | while( sqlite3_step(pStmt)==SQLITE_ROW ){ |
| 426 | 431 | const unsigned char *zName = sqlite3_column_text(pStmt,0); |
| 427 | 432 | char *zHash = cache_hash_of_key((const char*)zName); |
| 433 | + if( nEntry==0 ){ | |
| 434 | + @ <h2>Current Cache Entries:</h2> | |
| 435 | + @ <ol> | |
| 436 | + } | |
| 428 | 437 | @ <li><p>%z(href("%R/cacheget?key=%T",zName))%h(zName)</a><br> |
| 429 | - @ size: %,lld(sqlite3_column_int64(pStmt,1)) | |
| 430 | - @ hit-count: %d(sqlite3_column_int(pStmt,2)) | |
| 431 | - @ last-access: %s(sqlite3_column_text(pStmt,3)) \ | |
| 438 | + @ size: %,lld(sqlite3_column_int64(pStmt,1)), | |
| 439 | + @ hit-count: %d(sqlite3_column_int(pStmt,2)), | |
| 440 | + @ last-access: %s(sqlite3_column_text(pStmt,3))Z \ | |
| 432 | 441 | if( zHash ){ |
| 433 | - @ %z(href("%R/timeline?c=%S",zHash))check-in</a>\ | |
| 442 | + @ → %z(href("%R/timeline?c=%S",zHash))checkin info</a>\ | |
| 434 | 443 | fossil_free(zHash); |
| 435 | 444 | } |
| 436 | 445 | @ </p></li> |
| 437 | - | |
| 446 | + nEntry++; | |
| 438 | 447 | } |
| 439 | 448 | sqlite3_finalize(pStmt); |
| 440 | - @ </ol> | |
| 441 | - } | |
| 442 | - zDbName = cacheName(); | |
| 443 | - bigSizeName(sizeof(zBuf), zBuf, file_size(zDbName, ExtFILE)); | |
| 444 | - @ <p> | |
| 445 | - @ cache-file name: %h(zDbName)<br> | |
| 446 | - @ cache-file size: %s(zBuf)<br> | |
| 447 | - @ max-cache-entry: %d(db_get_int("max-cache-entry",10)) | |
| 448 | - @ </p> | |
| 449 | - @ <p> | |
| 450 | - @ Use the "<a href="%R/help?cmd=cache">fossil cache</a>" command | |
| 451 | - @ on the command-line to create and configure the web-cache. | |
| 452 | - @ </p> | |
| 453 | - fossil_free(zDbName); | |
| 454 | - sqlite3_close(db); | |
| 455 | - } | |
| 449 | + if( nEntry ){ | |
| 450 | + @ </ol> | |
| 451 | + } | |
| 452 | + } | |
| 453 | + } | |
| 454 | + @ <h2>About The Web-Cache</h2> | |
| 455 | + @ <p> | |
| 456 | + @ The web-cache is a separate database file that holds cached copies | |
| 457 | + @ tarballs, ZIP archives, and other pages that are expensive to compute | |
| 458 | + @ and are likely to be reused. | |
| 459 | + @ <form method="post"> | |
| 460 | + login_insert_csrf_secret(); | |
| 461 | + @ <ul> | |
| 462 | + if( db==0 ){ | |
| 463 | + @ <li> Web-cache is currently disabled. | |
| 464 | + @ <input type="submit" name="init" value="Enable"> | |
| 465 | + }else{ | |
| 466 | + bigSizeName(sizeof(zBuf), zBuf, file_size(zDbName, ExtFILE)); | |
| 467 | + mxEntry = db_get_int("max-cache-entry",10); | |
| 468 | + @ <li> Filename of the cache database: <b>%h(zDbName)</b> | |
| 469 | + @ <li> Size of the cache database: %s(zBuf) | |
| 470 | + @ <li> Maximum number of entries: %d(mxEntry) | |
| 471 | + @ <li> Number of cache entries used: %d(nEntry) | |
| 472 | + @ <li> Change the max-cache-entry setting on the | |
| 473 | + @ <a href="%R/setup_settings">Settings</a> page to adjust the | |
| 474 | + @ maximum number of entries in the cache. | |
| 475 | + @ <li><input type="submit" name="clear" value="Clear the cache"> | |
| 476 | + @ <li> Disable the cache by manually deleting the cache database file. | |
| 477 | + } | |
| 478 | + @ </ul> | |
| 479 | + @ </form> | |
| 480 | + fossil_free(zDbName); | |
| 481 | + if( db ) sqlite3_close(db); | |
| 456 | 482 | style_finish_page(); |
| 457 | 483 | } |
| 458 | 484 | |
| 459 | 485 | /* |
| 460 | 486 | ** WEBPAGE: cacheget |
| 461 | 487 |
| --- src/cache.c | |
| +++ src/cache.c | |
| @@ -399,62 +399,88 @@ | |
| 399 | ** WEBPAGE: cachestat |
| 400 | ** |
| 401 | ** Show information about the webpage cache. Requires Setup privilege. |
| 402 | */ |
| 403 | void cache_page(void){ |
| 404 | sqlite3 *db; |
| 405 | sqlite3_stmt *pStmt; |
| 406 | char zBuf[100]; |
| 407 | |
| 408 | login_check_credentials(); |
| 409 | if( !g.perm.Setup ){ login_needed(0); return; } |
| 410 | style_set_current_feature("cache"); |
| 411 | style_header("Web Cache Status"); |
| 412 | db = cacheOpen(0); |
| 413 | if( db==0 ){ |
| 414 | @ The web-page cache is disabled for this repository |
| 415 | }else{ |
| 416 | char *zDbName = cacheName(); |
| 417 | cache_register_sizename(db); |
| 418 | pStmt = cacheStmt(db, |
| 419 | "SELECT key, sz, nRef, datetime(tm,'unixepoch')" |
| 420 | " FROM cache" |
| 421 | " ORDER BY (tm + 3600*min(nRef,48)) DESC" |
| 422 | ); |
| 423 | if( pStmt ){ |
| 424 | @ <ol> |
| 425 | while( sqlite3_step(pStmt)==SQLITE_ROW ){ |
| 426 | const unsigned char *zName = sqlite3_column_text(pStmt,0); |
| 427 | char *zHash = cache_hash_of_key((const char*)zName); |
| 428 | @ <li><p>%z(href("%R/cacheget?key=%T",zName))%h(zName)</a><br> |
| 429 | @ size: %,lld(sqlite3_column_int64(pStmt,1)) |
| 430 | @ hit-count: %d(sqlite3_column_int(pStmt,2)) |
| 431 | @ last-access: %s(sqlite3_column_text(pStmt,3)) \ |
| 432 | if( zHash ){ |
| 433 | @ %z(href("%R/timeline?c=%S",zHash))check-in</a>\ |
| 434 | fossil_free(zHash); |
| 435 | } |
| 436 | @ </p></li> |
| 437 | |
| 438 | } |
| 439 | sqlite3_finalize(pStmt); |
| 440 | @ </ol> |
| 441 | } |
| 442 | zDbName = cacheName(); |
| 443 | bigSizeName(sizeof(zBuf), zBuf, file_size(zDbName, ExtFILE)); |
| 444 | @ <p> |
| 445 | @ cache-file name: %h(zDbName)<br> |
| 446 | @ cache-file size: %s(zBuf)<br> |
| 447 | @ max-cache-entry: %d(db_get_int("max-cache-entry",10)) |
| 448 | @ </p> |
| 449 | @ <p> |
| 450 | @ Use the "<a href="%R/help?cmd=cache">fossil cache</a>" command |
| 451 | @ on the command-line to create and configure the web-cache. |
| 452 | @ </p> |
| 453 | fossil_free(zDbName); |
| 454 | sqlite3_close(db); |
| 455 | } |
| 456 | style_finish_page(); |
| 457 | } |
| 458 | |
| 459 | /* |
| 460 | ** WEBPAGE: cacheget |
| 461 |
| --- src/cache.c | |
| +++ src/cache.c | |
| @@ -399,62 +399,88 @@ | |
| 399 | ** WEBPAGE: cachestat |
| 400 | ** |
| 401 | ** Show information about the webpage cache. Requires Setup privilege. |
| 402 | */ |
| 403 | void cache_page(void){ |
| 404 | sqlite3 *db = 0; |
| 405 | sqlite3_stmt *pStmt; |
| 406 | int doInit; |
| 407 | char *zDbName = cacheName(); |
| 408 | int nEntry = 0; |
| 409 | int mxEntry = 0; |
| 410 | char zBuf[100]; |
| 411 | |
| 412 | login_check_credentials(); |
| 413 | if( !g.perm.Setup ){ login_needed(0); return; } |
| 414 | style_set_current_feature("cache"); |
| 415 | style_header("Web Cache Status"); |
| 416 | style_submenu_element("Refresh","%R/cachestat"); |
| 417 | doInit = P("init")!=0 && cgi_csrf_safe(2); |
| 418 | db = cacheOpen(doInit); |
| 419 | if( db!=0 ){ |
| 420 | if( P("clear")!=0 && cgi_csrf_safe(2) ){ |
| 421 | sqlite3_exec(db, "DELETE FROM cache; DELETE FROM blob; VACUUM;",0,0,0); |
| 422 | } |
| 423 | cache_register_sizename(db); |
| 424 | pStmt = cacheStmt(db, |
| 425 | "SELECT key, sz, nRef, datetime(tm,'unixepoch')" |
| 426 | " FROM cache" |
| 427 | " ORDER BY (tm + 3600*min(nRef,48)) DESC" |
| 428 | ); |
| 429 | if( pStmt ){ |
| 430 | while( sqlite3_step(pStmt)==SQLITE_ROW ){ |
| 431 | const unsigned char *zName = sqlite3_column_text(pStmt,0); |
| 432 | char *zHash = cache_hash_of_key((const char*)zName); |
| 433 | if( nEntry==0 ){ |
| 434 | @ <h2>Current Cache Entries:</h2> |
| 435 | @ <ol> |
| 436 | } |
| 437 | @ <li><p>%z(href("%R/cacheget?key=%T",zName))%h(zName)</a><br> |
| 438 | @ size: %,lld(sqlite3_column_int64(pStmt,1)), |
| 439 | @ hit-count: %d(sqlite3_column_int(pStmt,2)), |
| 440 | @ last-access: %s(sqlite3_column_text(pStmt,3))Z \ |
| 441 | if( zHash ){ |
| 442 | @ → %z(href("%R/timeline?c=%S",zHash))checkin info</a>\ |
| 443 | fossil_free(zHash); |
| 444 | } |
| 445 | @ </p></li> |
| 446 | nEntry++; |
| 447 | } |
| 448 | sqlite3_finalize(pStmt); |
| 449 | if( nEntry ){ |
| 450 | @ </ol> |
| 451 | } |
| 452 | } |
| 453 | } |
| 454 | @ <h2>About The Web-Cache</h2> |
| 455 | @ <p> |
| 456 | @ The web-cache is a separate database file that holds cached copies |
| 457 | @ tarballs, ZIP archives, and other pages that are expensive to compute |
| 458 | @ and are likely to be reused. |
| 459 | @ <form method="post"> |
| 460 | login_insert_csrf_secret(); |
| 461 | @ <ul> |
| 462 | if( db==0 ){ |
| 463 | @ <li> Web-cache is currently disabled. |
| 464 | @ <input type="submit" name="init" value="Enable"> |
| 465 | }else{ |
| 466 | bigSizeName(sizeof(zBuf), zBuf, file_size(zDbName, ExtFILE)); |
| 467 | mxEntry = db_get_int("max-cache-entry",10); |
| 468 | @ <li> Filename of the cache database: <b>%h(zDbName)</b> |
| 469 | @ <li> Size of the cache database: %s(zBuf) |
| 470 | @ <li> Maximum number of entries: %d(mxEntry) |
| 471 | @ <li> Number of cache entries used: %d(nEntry) |
| 472 | @ <li> Change the max-cache-entry setting on the |
| 473 | @ <a href="%R/setup_settings">Settings</a> page to adjust the |
| 474 | @ maximum number of entries in the cache. |
| 475 | @ <li><input type="submit" name="clear" value="Clear the cache"> |
| 476 | @ <li> Disable the cache by manually deleting the cache database file. |
| 477 | } |
| 478 | @ </ul> |
| 479 | @ </form> |
| 480 | fossil_free(zDbName); |
| 481 | if( db ) sqlite3_close(db); |
| 482 | style_finish_page(); |
| 483 | } |
| 484 | |
| 485 | /* |
| 486 | ** WEBPAGE: cacheget |
| 487 |
+289
-165
| --- src/cgi.c | ||
| +++ src/cgi.c | ||
| @@ -72,10 +72,11 @@ | ||
| 72 | 72 | # include <ws2tcpip.h> |
| 73 | 73 | #else |
| 74 | 74 | # include <sys/socket.h> |
| 75 | 75 | # include <sys/un.h> |
| 76 | 76 | # include <netinet/in.h> |
| 77 | +# include <netdb.h> | |
| 77 | 78 | # include <arpa/inet.h> |
| 78 | 79 | # include <sys/times.h> |
| 79 | 80 | # include <sys/time.h> |
| 80 | 81 | # include <sys/wait.h> |
| 81 | 82 | # include <sys/select.h> |
| @@ -103,12 +104,12 @@ | ||
| 103 | 104 | #define PT(x) cgi_parameter_trimmed((x),0) |
| 104 | 105 | #define PDT(x,y) cgi_parameter_trimmed((x),(y)) |
| 105 | 106 | #define PB(x) cgi_parameter_boolean(x) |
| 106 | 107 | #define PCK(x) cgi_parameter_checked(x,1) |
| 107 | 108 | #define PIF(x,y) cgi_parameter_checked(x,y) |
| 108 | -#define P_NoBot(x) cgi_parameter_nosql((x),0) | |
| 109 | -#define PD_NoBot(x,y) cgi_parameter_nosql((x),(y)) | |
| 109 | +#define P_NoBot(x) cgi_parameter_no_attack((x),0) | |
| 110 | +#define PD_NoBot(x,y) cgi_parameter_no_attack((x),(y)) | |
| 110 | 111 | |
| 111 | 112 | /* |
| 112 | 113 | ** Shortcut for the cgi_printf() routine. Instead of using the |
| 113 | 114 | ** |
| 114 | 115 | ** @ ... |
| @@ -637,10 +638,13 @@ | ||
| 637 | 638 | cgi_set_status(iStat, zStat); |
| 638 | 639 | free(zLocation); |
| 639 | 640 | cgi_reply(); |
| 640 | 641 | fossil_exit(0); |
| 641 | 642 | } |
| 643 | +NORETURN void cgi_redirect_perm(const char *zURL){ | |
| 644 | + cgi_redirect_with_status(zURL, 301, "Moved Permanently"); | |
| 645 | +} | |
| 642 | 646 | NORETURN void cgi_redirect(const char *zURL){ |
| 643 | 647 | cgi_redirect_with_status(zURL, 302, "Moved Temporarily"); |
| 644 | 648 | } |
| 645 | 649 | NORETURN void cgi_redirect_with_method(const char *zURL){ |
| 646 | 650 | cgi_redirect_with_status(zURL, 307, "Temporary Redirect"); |
| @@ -1620,37 +1624,39 @@ | ||
| 1620 | 1624 | fossil_errorlog("Xpossible hack attempt - 418 response on \"%s\"", zName); |
| 1621 | 1625 | exit(0); |
| 1622 | 1626 | } |
| 1623 | 1627 | |
| 1624 | 1628 | /* |
| 1625 | -** If looks_like_sql_injection() returns true for the given string, calls | |
| 1629 | +** If looks_like_attack() returns true for the given string, call | |
| 1626 | 1630 | ** cgi_begone_spider() and does not return, else this function has no |
| 1627 | 1631 | ** side effects. The range of checks performed by this function may |
| 1628 | 1632 | ** be extended in the future. |
| 1629 | 1633 | ** |
| 1630 | 1634 | ** Checks are omitted for any logged-in user. |
| 1631 | 1635 | ** |
| 1632 | -** This is NOT a defense against SQL injection. Fossil should easily be | |
| 1633 | -** proof against SQL injection without this routine. Rather, this is an | |
| 1634 | -** attempt to avoid denial-of-service caused by persistent spiders that hammer | |
| 1635 | -** the server with dozens or hundreds of SQL injection attempts per second | |
| 1636 | -** against pages (such as /vdiff) that are expensive to compute. In other | |
| 1636 | +** This is the primary defense against attack. Fossil should easily be | |
| 1637 | +** proof against SQL injection and XSS attacks even without without this | |
| 1638 | +** routine. Rather, this is an attempt to avoid denial-of-service caused | |
| 1639 | +** by persistent spiders that hammer the server with dozens or hundreds of | |
| 1640 | +** probes per seconds as they look for vulnerabilities. In other | |
| 1637 | 1641 | ** words, this is an effort to reduce the CPU load imposed by malicious |
| 1638 | -** spiders. It is not an effect defense against SQL injection vulnerabilities. | |
| 1642 | +** spiders. Though those routine might help make attacks harder, it is | |
| 1643 | +** not itself an impenetrably barrier against attack and should not be | |
| 1644 | +** relied upon as the only defense. | |
| 1639 | 1645 | */ |
| 1640 | 1646 | void cgi_value_spider_check(const char *zTxt, const char *zName){ |
| 1641 | - if( g.zLogin==0 && looks_like_sql_injection(zTxt) ){ | |
| 1647 | + if( g.zLogin==0 && looks_like_attack(zTxt) ){ | |
| 1642 | 1648 | cgi_begone_spider(zName); |
| 1643 | 1649 | } |
| 1644 | 1650 | } |
| 1645 | 1651 | |
| 1646 | 1652 | /* |
| 1647 | 1653 | ** A variant of cgi_parameter() with the same semantics except that if |
| 1648 | 1654 | ** cgi_parameter(zName,zDefault) returns a value other than zDefault |
| 1649 | 1655 | ** then it passes that value to cgi_value_spider_check(). |
| 1650 | 1656 | */ |
| 1651 | -const char *cgi_parameter_nosql(const char *zName, const char *zDefault){ | |
| 1657 | +const char *cgi_parameter_no_attack(const char *zName, const char *zDefault){ | |
| 1652 | 1658 | const char *zTxt = cgi_parameter(zName, zDefault); |
| 1653 | 1659 | |
| 1654 | 1660 | if( zTxt!=zDefault ){ |
| 1655 | 1661 | cgi_value_spider_check(zTxt, zName); |
| 1656 | 1662 | } |
| @@ -2070,34 +2076,40 @@ | ||
| 2070 | 2076 | } |
| 2071 | 2077 | if( zLeftOver ){ *zLeftOver = zInput; } |
| 2072 | 2078 | return zResult; |
| 2073 | 2079 | } |
| 2074 | 2080 | |
| 2081 | +/* | |
| 2082 | +** All possible forms of an IP address. Needed to work around GCC strict | |
| 2083 | +** aliasing rules. | |
| 2084 | +*/ | |
| 2085 | +typedef union { | |
| 2086 | + struct sockaddr sa; /* Abstract superclass */ | |
| 2087 | + struct sockaddr_in sa4; /* IPv4 */ | |
| 2088 | + struct sockaddr_in6 sa6; /* IPv6 */ | |
| 2089 | + struct sockaddr_storage sas; /* Should be the maximum of the above 3 */ | |
| 2090 | +} address; | |
| 2091 | + | |
| 2075 | 2092 | /* |
| 2076 | 2093 | ** Determine the IP address on the other side of a connection. |
| 2077 | 2094 | ** Return a pointer to a string. Or return 0 if unable. |
| 2078 | 2095 | ** |
| 2079 | 2096 | ** The string is held in a static buffer that is overwritten on |
| 2080 | 2097 | ** each call. |
| 2081 | 2098 | */ |
| 2082 | 2099 | char *cgi_remote_ip(int fd){ |
| 2083 | -#if 0 | |
| 2084 | - static char zIp[100]; | |
| 2085 | - struct sockaddr_in6 addr; | |
| 2086 | - socklen_t sz = sizeof(addr); | |
| 2087 | - if( getpeername(fd, &addr, &sz) ) return 0; | |
| 2088 | - zIp[0] = 0; | |
| 2089 | - if( inet_ntop(AF_INET6, &addr, zIp, sizeof(zIp))==0 ){ | |
| 2100 | + address remoteAddr; | |
| 2101 | + socklen_t size = sizeof(remoteAddr); | |
| 2102 | + static char zHost[NI_MAXHOST]; | |
| 2103 | + if( getpeername(0, &remoteAddr.sa, &size) ){ | |
| 2104 | + return 0; | |
| 2105 | + } | |
| 2106 | + if( getnameinfo(&remoteAddr.sa, size, zHost, sizeof(zHost), 0, 0, | |
| 2107 | + NI_NUMERICHOST) ){ | |
| 2090 | 2108 | return 0; |
| 2091 | 2109 | } |
| 2092 | - return zIp; | |
| 2093 | -#else | |
| 2094 | - struct sockaddr_in remoteName; | |
| 2095 | - socklen_t size = sizeof(struct sockaddr_in); | |
| 2096 | - if( getpeername(fd, (struct sockaddr*)&remoteName, &size) ) return 0; | |
| 2097 | - return inet_ntoa(remoteName.sin_addr); | |
| 2098 | -#endif | |
| 2110 | + return zHost; | |
| 2099 | 2111 | } |
| 2100 | 2112 | |
| 2101 | 2113 | /* |
| 2102 | 2114 | ** This routine handles a single HTTP request which is coming in on |
| 2103 | 2115 | ** g.httpIn and which replies on g.httpOut |
| @@ -2486,11 +2498,10 @@ | ||
| 2486 | 2498 | fossil_free(zToFree); |
| 2487 | 2499 | fgetc(g.httpIn); /* Read past the "," separating header from content */ |
| 2488 | 2500 | cgi_init(); |
| 2489 | 2501 | } |
| 2490 | 2502 | |
| 2491 | - | |
| 2492 | 2503 | #if INTERFACE |
| 2493 | 2504 | /* |
| 2494 | 2505 | ** Bitmap values for the flags parameter to cgi_http_server(). |
| 2495 | 2506 | */ |
| 2496 | 2507 | #define HTTP_SERVER_LOCALHOST 0x0001 /* Bind to 127.0.0.1 only */ |
| @@ -2529,122 +2540,222 @@ | ||
| 2529 | 2540 | ){ |
| 2530 | 2541 | #if defined(_WIN32) |
| 2531 | 2542 | /* Use win32_http_server() instead */ |
| 2532 | 2543 | fossil_exit(1); |
| 2533 | 2544 | #else |
| 2534 | - int listener = -1; /* The server socket */ | |
| 2535 | - int connection; /* A socket for each individual connection */ | |
| 2545 | + int listen4 = -1; /* Main socket; IPv4 or unix-domain */ | |
| 2546 | + int listen6 = -1; /* Aux socket for corresponding IPv6 */ | |
| 2547 | + int mxListen = -1; /* Maximum of listen4 and listen6 */ | |
| 2548 | + int connection; /* An incoming connection */ | |
| 2536 | 2549 | int nRequest = 0; /* Number of requests handled so far */ |
| 2537 | 2550 | fd_set readfds; /* Set of file descriptors for select() */ |
| 2538 | 2551 | socklen_t lenaddr; /* Length of the inaddr structure */ |
| 2539 | 2552 | int child; /* PID of the child process */ |
| 2540 | 2553 | int nchildren = 0; /* Number of child processes */ |
| 2541 | 2554 | struct timeval delay; /* How long to wait inside select() */ |
| 2542 | - struct sockaddr_in inaddr; /* The socket address */ | |
| 2555 | + struct sockaddr_in6 inaddr6; /* Address for IPv6 */ | |
| 2556 | + struct sockaddr_in inaddr4; /* Address for IPv4 */ | |
| 2543 | 2557 | struct sockaddr_un uxaddr; /* The address for unix-domain sockets */ |
| 2544 | 2558 | int opt = 1; /* setsockopt flag */ |
| 2545 | 2559 | int rc; /* Result code from system calls */ |
| 2546 | 2560 | int iPort = mnPort; /* Port to try to use */ |
| 2547 | - | |
| 2548 | - while( iPort<=mxPort ){ | |
| 2549 | - if( flags & HTTP_SERVER_UNIXSOCKET ){ | |
| 2550 | - /* Initialize a Unix socket named g.zSockName */ | |
| 2551 | - assert( g.zSockName!=0 ); | |
| 2552 | - memset(&uxaddr, 0, sizeof(uxaddr)); | |
| 2553 | - if( strlen(g.zSockName)>sizeof(uxaddr.sun_path) ){ | |
| 2554 | - fossil_fatal("name of unix socket too big: %s\nmax size: %d\n", | |
| 2555 | - g.zSockName, (int)sizeof(uxaddr.sun_path)); | |
| 2556 | - } | |
| 2557 | - if( file_isdir(g.zSockName, ExtFILE)!=0 ){ | |
| 2558 | - if( !file_issocket(g.zSockName) ){ | |
| 2559 | - fossil_fatal("cannot name socket \"%s\" because another object" | |
| 2560 | - " with that name already exists", g.zSockName); | |
| 2561 | - }else{ | |
| 2562 | - unlink(g.zSockName); | |
| 2563 | - } | |
| 2564 | - } | |
| 2565 | - uxaddr.sun_family = AF_UNIX; | |
| 2566 | - strncpy(uxaddr.sun_path, g.zSockName, sizeof(uxaddr.sun_path)-1); | |
| 2567 | - listener = socket(AF_UNIX, SOCK_STREAM, 0); | |
| 2568 | - if( listener<0 ){ | |
| 2569 | - fossil_fatal("unable to create a unix socket named %s", | |
| 2570 | - g.zSockName); | |
| 2571 | - } | |
| 2572 | - /* Set the access permission for the new socket. Default to 0660. | |
| 2573 | - ** But use an alternative specified by --socket-mode if available. | |
| 2574 | - ** Do this before bind() to avoid a race condition. */ | |
| 2575 | - if( g.zSockMode ){ | |
| 2576 | - file_set_mode(g.zSockName, listener, g.zSockMode, 0); | |
| 2577 | - }else{ | |
| 2578 | - file_set_mode(g.zSockName, listener, "0660", 1); | |
| 2579 | - } | |
| 2580 | - }else{ | |
| 2581 | - /* Initialize a TCP/IP socket on port iPort */ | |
| 2582 | - memset(&inaddr, 0, sizeof(inaddr)); | |
| 2583 | - inaddr.sin_family = AF_INET; | |
| 2584 | - if( zIpAddr ){ | |
| 2585 | - inaddr.sin_addr.s_addr = inet_addr(zIpAddr); | |
| 2586 | - if( inaddr.sin_addr.s_addr == INADDR_NONE ){ | |
| 2587 | - fossil_fatal("not a valid IP address: %s", zIpAddr); | |
| 2588 | - } | |
| 2589 | - }else if( flags & HTTP_SERVER_LOCALHOST ){ | |
| 2590 | - inaddr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); | |
| 2591 | - }else{ | |
| 2592 | - inaddr.sin_addr.s_addr = htonl(INADDR_ANY); | |
| 2593 | - } | |
| 2594 | - inaddr.sin_port = htons(iPort); | |
| 2595 | - listener = socket(AF_INET, SOCK_STREAM, 0); | |
| 2596 | - if( listener<0 ){ | |
| 2597 | - iPort++; | |
| 2598 | - continue; | |
| 2599 | - } | |
| 2600 | - } | |
| 2601 | - | |
| 2602 | - /* if we can't terminate nicely, at least allow the socket to be reused */ | |
| 2603 | - setsockopt(listener,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt)); | |
| 2604 | - | |
| 2605 | - if( flags & HTTP_SERVER_UNIXSOCKET ){ | |
| 2606 | - rc = bind(listener, (struct sockaddr*)&uxaddr, sizeof(uxaddr)); | |
| 2607 | - /* Set the owner of the socket if requested by --socket-owner. This | |
| 2608 | - ** must wait until after bind(), after the filesystem object has been | |
| 2609 | - ** created. See https://lkml.org/lkml/2004/11/1/84 and | |
| 2610 | - ** https://fossil-scm.org/forum/forumpost/7517680ef9684c57 */ | |
| 2611 | - if( g.zSockOwner ){ | |
| 2612 | - file_set_owner(g.zSockName, listener, g.zSockOwner); | |
| 2613 | - } | |
| 2614 | - }else{ | |
| 2615 | - rc = bind(listener, (struct sockaddr*)&inaddr, sizeof(inaddr)); | |
| 2616 | - } | |
| 2617 | - if( rc<0 ){ | |
| 2618 | - close(listener); | |
| 2619 | - iPort++; | |
| 2620 | - continue; | |
| 2621 | - } | |
| 2622 | - break; | |
| 2623 | - } | |
| 2624 | - if( iPort>mxPort ){ | |
| 2625 | - if( flags & HTTP_SERVER_UNIXSOCKET ){ | |
| 2626 | - fossil_fatal("unable to listen on unix socket %s", zIpAddr); | |
| 2627 | - }else if( mnPort==mxPort ){ | |
| 2628 | - fossil_fatal("unable to open listening socket on port %d", mnPort); | |
| 2629 | - }else{ | |
| 2630 | - fossil_fatal("unable to open listening socket on any" | |
| 2631 | - " port in the range %d..%d", mnPort, mxPort); | |
| 2632 | - } | |
| 2633 | - } | |
| 2634 | - if( iPort>mxPort ) return 1; | |
| 2635 | - listen(listener,10); | |
| 2636 | - if( flags & HTTP_SERVER_UNIXSOCKET ){ | |
| 2637 | - fossil_print("Listening for %s requests on unix socket %s\n", | |
| 2638 | - (flags & HTTP_SERVER_SCGI)!=0 ? "SCGI" : | |
| 2639 | - g.httpUseSSL?"TLS-encrypted HTTPS":"HTTP", g.zSockName); | |
| 2640 | - }else{ | |
| 2641 | - fossil_print("Listening for %s requests on TCP port %d\n", | |
| 2642 | - (flags & HTTP_SERVER_SCGI)!=0 ? "SCGI" : | |
| 2643 | - g.httpUseSSL?"TLS-encrypted HTTPS":"HTTP", iPort); | |
| 2644 | - } | |
| 2645 | - fflush(stdout); | |
| 2561 | + const char *zRequestType; /* Type of requests to listen for */ | |
| 2562 | + | |
| 2563 | + | |
| 2564 | + if( flags & HTTP_SERVER_SCGI ){ | |
| 2565 | + zRequestType = "SCGI"; | |
| 2566 | + }else if( g.httpUseSSL ){ | |
| 2567 | + zRequestType = "TLS-encrypted HTTPS"; | |
| 2568 | + }else{ | |
| 2569 | + zRequestType = "HTTP"; | |
| 2570 | + } | |
| 2571 | + | |
| 2572 | + if( flags & HTTP_SERVER_UNIXSOCKET ){ | |
| 2573 | + /* CASE 1: A unix socket named g.zSockName. After creation, set the | |
| 2574 | + ** permissions on the new socket to g.zSockMode and make the | |
| 2575 | + ** owner of the socket be g.zSockOwner. | |
| 2576 | + */ | |
| 2577 | + assert( g.zSockName!=0 ); | |
| 2578 | + memset(&uxaddr, 0, sizeof(uxaddr)); | |
| 2579 | + if( strlen(g.zSockName)>sizeof(uxaddr.sun_path) ){ | |
| 2580 | + fossil_fatal("name of unix socket too big: %s\nmax size: %d\n", | |
| 2581 | + g.zSockName, (int)sizeof(uxaddr.sun_path)); | |
| 2582 | + } | |
| 2583 | + if( file_isdir(g.zSockName, ExtFILE)!=0 ){ | |
| 2584 | + if( !file_issocket(g.zSockName) ){ | |
| 2585 | + fossil_fatal("cannot name socket \"%s\" because another object" | |
| 2586 | + " with that name already exists", g.zSockName); | |
| 2587 | + }else{ | |
| 2588 | + unlink(g.zSockName); | |
| 2589 | + } | |
| 2590 | + } | |
| 2591 | + uxaddr.sun_family = AF_UNIX; | |
| 2592 | + strncpy(uxaddr.sun_path, g.zSockName, sizeof(uxaddr.sun_path)-1); | |
| 2593 | + listen4 = socket(AF_UNIX, SOCK_STREAM, 0); | |
| 2594 | + if( listen4<0 ){ | |
| 2595 | + fossil_fatal("unable to create a unix socket named %s", | |
| 2596 | + g.zSockName); | |
| 2597 | + } | |
| 2598 | + mxListen = listen4; | |
| 2599 | + listen6 = -1; | |
| 2600 | + | |
| 2601 | + /* Set the access permission for the new socket. Default to 0660. | |
| 2602 | + ** But use an alternative specified by --socket-mode if available. | |
| 2603 | + ** Do this before bind() to avoid a race condition. */ | |
| 2604 | + if( g.zSockMode ){ | |
| 2605 | + file_set_mode(g.zSockName, listen4, g.zSockMode, 0); | |
| 2606 | + }else{ | |
| 2607 | + file_set_mode(g.zSockName, listen4, "0660", 1); | |
| 2608 | + } | |
| 2609 | + rc = bind(listen4, (struct sockaddr*)&uxaddr, sizeof(uxaddr)); | |
| 2610 | + /* Set the owner of the socket if requested by --socket-owner. This | |
| 2611 | + ** must wait until after bind(), after the filesystem object has been | |
| 2612 | + ** created. See https://lkml.org/lkml/2004/11/1/84 and | |
| 2613 | + ** https://fossil-scm.org/forum/forumpost/7517680ef9684c57 */ | |
| 2614 | + if( g.zSockOwner ){ | |
| 2615 | + file_set_owner(g.zSockName, listen4, g.zSockOwner); | |
| 2616 | + } | |
| 2617 | + fossil_print("Listening for %s requests on unix socket %s\n", | |
| 2618 | + zRequestType, g.zSockName); | |
| 2619 | + fflush(stdout); | |
| 2620 | + }else if( zIpAddr && strchr(zIpAddr,':')!=0 ){ | |
| 2621 | + /* CASE 2: TCP on IPv6 IP address specified by zIpAddr and on port iPort. | |
| 2622 | + */ | |
| 2623 | + assert( mnPort==mxPort ); | |
| 2624 | + memset(&inaddr6, 0, sizeof(inaddr6)); | |
| 2625 | + inaddr6.sin6_family = AF_INET6; | |
| 2626 | + inaddr6.sin6_port = htons(iPort); | |
| 2627 | + if( inet_pton(AF_INET6, zIpAddr, &inaddr6.sin6_addr)==0 ){ | |
| 2628 | + fossil_fatal("not a valid IPv6 address: %s", zIpAddr); | |
| 2629 | + } | |
| 2630 | + listen6 = socket(AF_INET6, SOCK_STREAM, 0); | |
| 2631 | + if( listen6>0 ){ | |
| 2632 | + opt = 1; | |
| 2633 | + setsockopt(listen6, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); | |
| 2634 | + rc = bind(listen6, (struct sockaddr*)&inaddr6, sizeof(inaddr6)); | |
| 2635 | + if( rc<0 ){ | |
| 2636 | + close(listen6); | |
| 2637 | + listen6 = -1; | |
| 2638 | + } | |
| 2639 | + } | |
| 2640 | + if( listen6<0 ){ | |
| 2641 | + fossil_fatal("cannot open a listening socket on [%s]:%d", | |
| 2642 | + zIpAddr, mnPort); | |
| 2643 | + } | |
| 2644 | + mxListen = listen6; | |
| 2645 | + listen4 = -1; | |
| 2646 | + fossil_print("Listening for %s requests on [%s]:%d\n", | |
| 2647 | + zRequestType, zIpAddr, iPort); | |
| 2648 | + fflush(stdout); | |
| 2649 | + }else if( zIpAddr && zIpAddr[0] ){ | |
| 2650 | + /* CASE 3: TCP on IPv4 IP address specified by zIpAddr and on port iPort. | |
| 2651 | + */ | |
| 2652 | + assert( mnPort==mxPort ); | |
| 2653 | + memset(&inaddr4, 0, sizeof(inaddr4)); | |
| 2654 | + inaddr4.sin_family = AF_INET; | |
| 2655 | + inaddr4.sin_port = htons(iPort); | |
| 2656 | + if( strcmp(zIpAddr, "localhost")==0 ) zIpAddr = "127.0.0.1"; | |
| 2657 | + inaddr4.sin_addr.s_addr = inet_addr(zIpAddr); | |
| 2658 | + if( inaddr4.sin_addr.s_addr == INADDR_NONE ){ | |
| 2659 | + fossil_fatal("not a valid IPv4 address: %s", zIpAddr); | |
| 2660 | + } | |
| 2661 | + listen4 = socket(AF_INET, SOCK_STREAM, 0); | |
| 2662 | + if( listen4>0 ){ | |
| 2663 | + setsockopt(listen4, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); | |
| 2664 | + rc = bind(listen4, (struct sockaddr*)&inaddr4, sizeof(inaddr4)); | |
| 2665 | + if( rc<0 ){ | |
| 2666 | + close(listen6); | |
| 2667 | + listen4 = -1; | |
| 2668 | + } | |
| 2669 | + } | |
| 2670 | + if( listen4<0 ){ | |
| 2671 | + fossil_fatal("cannot open a listening socket on %s:%d", | |
| 2672 | + zIpAddr, mnPort); | |
| 2673 | + } | |
| 2674 | + mxListen = listen4; | |
| 2675 | + listen6 = -1; | |
| 2676 | + fossil_print("Listening for %s requests on TCP port %s:%d\n", | |
| 2677 | + zRequestType, zIpAddr, iPort); | |
| 2678 | + fflush(stdout); | |
| 2679 | + }else{ | |
| 2680 | + /* CASE 4: Listen on all available IP addresses, or on only loopback | |
| 2681 | + ** addresses (if HTTP_SERVER_LOCALHOST). The TCP port is the | |
| 2682 | + ** first available in the range of mnPort..mxPort. Listen | |
| 2683 | + ** on both IPv4 and IPv6, if possible. The TCP port scan is done | |
| 2684 | + ** on IPv4. | |
| 2685 | + */ | |
| 2686 | + while( iPort<=mxPort ){ | |
| 2687 | + const char *zProto; | |
| 2688 | + memset(&inaddr4, 0, sizeof(inaddr4)); | |
| 2689 | + inaddr4.sin_family = AF_INET; | |
| 2690 | + inaddr4.sin_port = htons(iPort); | |
| 2691 | + if( flags & HTTP_SERVER_LOCALHOST ){ | |
| 2692 | + inaddr4.sin_addr.s_addr = htonl(INADDR_LOOPBACK); | |
| 2693 | + }else{ | |
| 2694 | + inaddr4.sin_addr.s_addr = htonl(INADDR_ANY); | |
| 2695 | + } | |
| 2696 | + listen4 = socket(AF_INET, SOCK_STREAM, 0); | |
| 2697 | + if( listen4>0 ){ | |
| 2698 | + setsockopt(listen4, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); | |
| 2699 | + rc = bind(listen4, (struct sockaddr*)&inaddr4, sizeof(inaddr4)); | |
| 2700 | + if( rc<0 ){ | |
| 2701 | + close(listen4); | |
| 2702 | + listen4 = -1; | |
| 2703 | + } | |
| 2704 | + } | |
| 2705 | + if( listen4<0 ){ | |
| 2706 | + iPort++; | |
| 2707 | + continue; | |
| 2708 | + } | |
| 2709 | + mxListen = listen4; | |
| 2710 | + | |
| 2711 | + /* If we get here, that means we found an open TCP port at iPort for | |
| 2712 | + ** IPv4. Try to set up a corresponding IPv6 socket on the same port. | |
| 2713 | + */ | |
| 2714 | + memset(&inaddr6, 0, sizeof(inaddr6)); | |
| 2715 | + inaddr6.sin6_family = AF_INET6; | |
| 2716 | + inaddr6.sin6_port = htons(iPort); | |
| 2717 | + if( flags & HTTP_SERVER_LOCALHOST ){ | |
| 2718 | + inaddr6.sin6_addr = in6addr_loopback; | |
| 2719 | + }else{ | |
| 2720 | + inaddr6.sin6_addr = in6addr_any; | |
| 2721 | + } | |
| 2722 | + listen6 = socket(AF_INET6, SOCK_STREAM, 0); | |
| 2723 | + if( listen6>0 ){ | |
| 2724 | + setsockopt(listen6, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); | |
| 2725 | + setsockopt(listen6, IPPROTO_IPV6, IPV6_V6ONLY, &opt, sizeof(opt)); | |
| 2726 | + rc = bind(listen6, (struct sockaddr*)&inaddr6, sizeof(inaddr6)); | |
| 2727 | + if( rc<0 ){ | |
| 2728 | + close(listen6); | |
| 2729 | + listen6 = -1; | |
| 2730 | + } | |
| 2731 | + } | |
| 2732 | + if( listen6<0 ){ | |
| 2733 | + zProto = "IPv4 only"; | |
| 2734 | + }else{ | |
| 2735 | + zProto = "IPv4 and IPv6"; | |
| 2736 | + if( listen6>listen4 ) mxListen = listen6; | |
| 2737 | + } | |
| 2738 | + | |
| 2739 | + fossil_print("Listening for %s requests on TCP port %s%d, %s\n", | |
| 2740 | + zRequestType, | |
| 2741 | + (flags & HTTP_SERVER_LOCALHOST)!=0 ? "localhost:" : "", | |
| 2742 | + iPort, zProto); | |
| 2743 | + fflush(stdout); | |
| 2744 | + break; | |
| 2745 | + } | |
| 2746 | + if( iPort>mxPort ){ | |
| 2747 | + fossil_fatal("no available TCP ports in the range %d..%d", | |
| 2748 | + mnPort, mxPort); | |
| 2749 | + } | |
| 2750 | + } | |
| 2751 | + | |
| 2752 | + /* If we get to this point, that means there is at least one listening | |
| 2753 | + ** socket on either listen4 or listen6 and perhaps on both. */ | |
| 2754 | + assert( listen4>0 || listen6>0 ); | |
| 2755 | + if( listen4>0 ) listen(listen4,10); | |
| 2756 | + if( listen6>0 ) listen(listen6,10); | |
| 2646 | 2757 | if( zBrowser && (flags & HTTP_SERVER_UNIXSOCKET)==0 ){ |
| 2647 | 2758 | assert( strstr(zBrowser,"%d")!=0 ); |
| 2648 | 2759 | zBrowser = mprintf(zBrowser /*works-like:"%d"*/, iPort); |
| 2649 | 2760 | #if defined(__CYGWIN__) |
| 2650 | 2761 | /* On Cygwin, we can do better than "echo" */ |
| @@ -2658,56 +2769,69 @@ | ||
| 2658 | 2769 | #endif |
| 2659 | 2770 | if( fossil_system(zBrowser)<0 ){ |
| 2660 | 2771 | fossil_warning("cannot start browser: %s\n", zBrowser); |
| 2661 | 2772 | } |
| 2662 | 2773 | } |
| 2774 | + | |
| 2775 | + /* What for incomming requests. For each request, fork() a child process | |
| 2776 | + ** to deal with that request. The child process returns. The parent | |
| 2777 | + ** keeps on listening and never returns. | |
| 2778 | + */ | |
| 2663 | 2779 | while( 1 ){ |
| 2664 | 2780 | #if FOSSIL_MAX_CONNECTIONS>0 |
| 2665 | 2781 | while( nchildren>=FOSSIL_MAX_CONNECTIONS ){ |
| 2666 | 2782 | if( wait(0)>=0 ) nchildren--; |
| 2667 | 2783 | } |
| 2668 | 2784 | #endif |
| 2669 | 2785 | delay.tv_sec = 0; |
| 2670 | 2786 | delay.tv_usec = 100000; |
| 2671 | 2787 | FD_ZERO(&readfds); |
| 2672 | - assert( listener>=0 ); | |
| 2673 | - FD_SET( listener, &readfds); | |
| 2674 | - select( listener+1, &readfds, 0, 0, &delay); | |
| 2675 | - if( FD_ISSET(listener, &readfds) ){ | |
| 2676 | - lenaddr = sizeof(inaddr); | |
| 2677 | - connection = accept(listener, (struct sockaddr*)&inaddr, &lenaddr); | |
| 2678 | - if( connection>=0 ){ | |
| 2679 | - if( flags & HTTP_SERVER_NOFORK ){ | |
| 2680 | - child = 0; | |
| 2681 | - }else{ | |
| 2682 | - child = fork(); | |
| 2683 | - } | |
| 2684 | - if( child!=0 ){ | |
| 2685 | - if( child>0 ){ | |
| 2686 | - nchildren++; | |
| 2687 | - nRequest++; | |
| 2688 | - } | |
| 2689 | - close(connection); | |
| 2690 | - }else{ | |
| 2691 | - int nErr = 0, fd; | |
| 2692 | - g.zSockName = 0 /* avoid deleting the socket via atexit() */; | |
| 2693 | - close(0); | |
| 2694 | - fd = dup(connection); | |
| 2695 | - if( fd!=0 ) nErr++; | |
| 2696 | - close(1); | |
| 2697 | - fd = dup(connection); | |
| 2698 | - if( fd!=1 ) nErr++; | |
| 2699 | - if( 0 && !g.fAnyTrace ){ | |
| 2700 | - close(2); | |
| 2701 | - fd = dup(connection); | |
| 2702 | - if( fd!=2 ) nErr++; | |
| 2703 | - } | |
| 2704 | - close(connection); | |
| 2705 | - g.nPendingRequest = nchildren+1; | |
| 2706 | - g.nRequest = nRequest+1; | |
| 2707 | - return nErr; | |
| 2708 | - } | |
| 2788 | + assert( listen4>0 || listen6>0 ); | |
| 2789 | + if( listen4>0 ) FD_SET( listen4, &readfds); | |
| 2790 | + if( listen6>0 ) FD_SET( listen6, &readfds); | |
| 2791 | + select( mxListen+1, &readfds, 0, 0, &delay); | |
| 2792 | + if( listen4>0 && FD_ISSET(listen4, &readfds) ){ | |
| 2793 | + lenaddr = sizeof(inaddr4); | |
| 2794 | + connection = accept(listen4, (struct sockaddr*)&inaddr4, &lenaddr); | |
| 2795 | + }else if( listen6>0 && FD_ISSET(listen6, &readfds) ){ | |
| 2796 | + lenaddr = sizeof(inaddr6); | |
| 2797 | + connection = accept(listen6, (struct sockaddr*)&inaddr6, &lenaddr); | |
| 2798 | + }else{ | |
| 2799 | + connection = -1; | |
| 2800 | + } | |
| 2801 | + if( connection>=0 ){ | |
| 2802 | + if( flags & HTTP_SERVER_NOFORK ){ | |
| 2803 | + child = 0; | |
| 2804 | + }else{ | |
| 2805 | + child = fork(); | |
| 2806 | + } | |
| 2807 | + if( child!=0 ){ | |
| 2808 | + if( child>0 ){ | |
| 2809 | + nchildren++; | |
| 2810 | + nRequest++; | |
| 2811 | + } | |
| 2812 | + close(connection); | |
| 2813 | + }else{ | |
| 2814 | + int nErr = 0, fd; | |
| 2815 | + g.zSockName = 0 /* avoid deleting the socket via atexit() */; | |
| 2816 | + close(0); | |
| 2817 | + fd = dup(connection); | |
| 2818 | + if( fd!=0 ) nErr++; | |
| 2819 | + close(1); | |
| 2820 | + fd = dup(connection); | |
| 2821 | + if( fd!=1 ) nErr++; | |
| 2822 | + if( 0 && !g.fAnyTrace ){ | |
| 2823 | + close(2); | |
| 2824 | + fd = dup(connection); | |
| 2825 | + if( fd!=2 ) nErr++; | |
| 2826 | + } | |
| 2827 | + close(connection); | |
| 2828 | + if( listen4>0 ) close(listen4); | |
| 2829 | + if( listen6>0 ) close(listen6); | |
| 2830 | + g.nPendingRequest = nchildren+1; | |
| 2831 | + g.nRequest = nRequest+1; | |
| 2832 | + return nErr; | |
| 2709 | 2833 | } |
| 2710 | 2834 | } |
| 2711 | 2835 | /* Bury dead children */ |
| 2712 | 2836 | if( nchildren ){ |
| 2713 | 2837 | while(1){ |
| 2714 | 2838 |
| --- src/cgi.c | |
| +++ src/cgi.c | |
| @@ -72,10 +72,11 @@ | |
| 72 | # include <ws2tcpip.h> |
| 73 | #else |
| 74 | # include <sys/socket.h> |
| 75 | # include <sys/un.h> |
| 76 | # include <netinet/in.h> |
| 77 | # include <arpa/inet.h> |
| 78 | # include <sys/times.h> |
| 79 | # include <sys/time.h> |
| 80 | # include <sys/wait.h> |
| 81 | # include <sys/select.h> |
| @@ -103,12 +104,12 @@ | |
| 103 | #define PT(x) cgi_parameter_trimmed((x),0) |
| 104 | #define PDT(x,y) cgi_parameter_trimmed((x),(y)) |
| 105 | #define PB(x) cgi_parameter_boolean(x) |
| 106 | #define PCK(x) cgi_parameter_checked(x,1) |
| 107 | #define PIF(x,y) cgi_parameter_checked(x,y) |
| 108 | #define P_NoBot(x) cgi_parameter_nosql((x),0) |
| 109 | #define PD_NoBot(x,y) cgi_parameter_nosql((x),(y)) |
| 110 | |
| 111 | /* |
| 112 | ** Shortcut for the cgi_printf() routine. Instead of using the |
| 113 | ** |
| 114 | ** @ ... |
| @@ -637,10 +638,13 @@ | |
| 637 | cgi_set_status(iStat, zStat); |
| 638 | free(zLocation); |
| 639 | cgi_reply(); |
| 640 | fossil_exit(0); |
| 641 | } |
| 642 | NORETURN void cgi_redirect(const char *zURL){ |
| 643 | cgi_redirect_with_status(zURL, 302, "Moved Temporarily"); |
| 644 | } |
| 645 | NORETURN void cgi_redirect_with_method(const char *zURL){ |
| 646 | cgi_redirect_with_status(zURL, 307, "Temporary Redirect"); |
| @@ -1620,37 +1624,39 @@ | |
| 1620 | fossil_errorlog("Xpossible hack attempt - 418 response on \"%s\"", zName); |
| 1621 | exit(0); |
| 1622 | } |
| 1623 | |
| 1624 | /* |
| 1625 | ** If looks_like_sql_injection() returns true for the given string, calls |
| 1626 | ** cgi_begone_spider() and does not return, else this function has no |
| 1627 | ** side effects. The range of checks performed by this function may |
| 1628 | ** be extended in the future. |
| 1629 | ** |
| 1630 | ** Checks are omitted for any logged-in user. |
| 1631 | ** |
| 1632 | ** This is NOT a defense against SQL injection. Fossil should easily be |
| 1633 | ** proof against SQL injection without this routine. Rather, this is an |
| 1634 | ** attempt to avoid denial-of-service caused by persistent spiders that hammer |
| 1635 | ** the server with dozens or hundreds of SQL injection attempts per second |
| 1636 | ** against pages (such as /vdiff) that are expensive to compute. In other |
| 1637 | ** words, this is an effort to reduce the CPU load imposed by malicious |
| 1638 | ** spiders. It is not an effect defense against SQL injection vulnerabilities. |
| 1639 | */ |
| 1640 | void cgi_value_spider_check(const char *zTxt, const char *zName){ |
| 1641 | if( g.zLogin==0 && looks_like_sql_injection(zTxt) ){ |
| 1642 | cgi_begone_spider(zName); |
| 1643 | } |
| 1644 | } |
| 1645 | |
| 1646 | /* |
| 1647 | ** A variant of cgi_parameter() with the same semantics except that if |
| 1648 | ** cgi_parameter(zName,zDefault) returns a value other than zDefault |
| 1649 | ** then it passes that value to cgi_value_spider_check(). |
| 1650 | */ |
| 1651 | const char *cgi_parameter_nosql(const char *zName, const char *zDefault){ |
| 1652 | const char *zTxt = cgi_parameter(zName, zDefault); |
| 1653 | |
| 1654 | if( zTxt!=zDefault ){ |
| 1655 | cgi_value_spider_check(zTxt, zName); |
| 1656 | } |
| @@ -2070,34 +2076,40 @@ | |
| 2070 | } |
| 2071 | if( zLeftOver ){ *zLeftOver = zInput; } |
| 2072 | return zResult; |
| 2073 | } |
| 2074 | |
| 2075 | /* |
| 2076 | ** Determine the IP address on the other side of a connection. |
| 2077 | ** Return a pointer to a string. Or return 0 if unable. |
| 2078 | ** |
| 2079 | ** The string is held in a static buffer that is overwritten on |
| 2080 | ** each call. |
| 2081 | */ |
| 2082 | char *cgi_remote_ip(int fd){ |
| 2083 | #if 0 |
| 2084 | static char zIp[100]; |
| 2085 | struct sockaddr_in6 addr; |
| 2086 | socklen_t sz = sizeof(addr); |
| 2087 | if( getpeername(fd, &addr, &sz) ) return 0; |
| 2088 | zIp[0] = 0; |
| 2089 | if( inet_ntop(AF_INET6, &addr, zIp, sizeof(zIp))==0 ){ |
| 2090 | return 0; |
| 2091 | } |
| 2092 | return zIp; |
| 2093 | #else |
| 2094 | struct sockaddr_in remoteName; |
| 2095 | socklen_t size = sizeof(struct sockaddr_in); |
| 2096 | if( getpeername(fd, (struct sockaddr*)&remoteName, &size) ) return 0; |
| 2097 | return inet_ntoa(remoteName.sin_addr); |
| 2098 | #endif |
| 2099 | } |
| 2100 | |
| 2101 | /* |
| 2102 | ** This routine handles a single HTTP request which is coming in on |
| 2103 | ** g.httpIn and which replies on g.httpOut |
| @@ -2486,11 +2498,10 @@ | |
| 2486 | fossil_free(zToFree); |
| 2487 | fgetc(g.httpIn); /* Read past the "," separating header from content */ |
| 2488 | cgi_init(); |
| 2489 | } |
| 2490 | |
| 2491 | |
| 2492 | #if INTERFACE |
| 2493 | /* |
| 2494 | ** Bitmap values for the flags parameter to cgi_http_server(). |
| 2495 | */ |
| 2496 | #define HTTP_SERVER_LOCALHOST 0x0001 /* Bind to 127.0.0.1 only */ |
| @@ -2529,122 +2540,222 @@ | |
| 2529 | ){ |
| 2530 | #if defined(_WIN32) |
| 2531 | /* Use win32_http_server() instead */ |
| 2532 | fossil_exit(1); |
| 2533 | #else |
| 2534 | int listener = -1; /* The server socket */ |
| 2535 | int connection; /* A socket for each individual connection */ |
| 2536 | int nRequest = 0; /* Number of requests handled so far */ |
| 2537 | fd_set readfds; /* Set of file descriptors for select() */ |
| 2538 | socklen_t lenaddr; /* Length of the inaddr structure */ |
| 2539 | int child; /* PID of the child process */ |
| 2540 | int nchildren = 0; /* Number of child processes */ |
| 2541 | struct timeval delay; /* How long to wait inside select() */ |
| 2542 | struct sockaddr_in inaddr; /* The socket address */ |
| 2543 | struct sockaddr_un uxaddr; /* The address for unix-domain sockets */ |
| 2544 | int opt = 1; /* setsockopt flag */ |
| 2545 | int rc; /* Result code from system calls */ |
| 2546 | int iPort = mnPort; /* Port to try to use */ |
| 2547 | |
| 2548 | while( iPort<=mxPort ){ |
| 2549 | if( flags & HTTP_SERVER_UNIXSOCKET ){ |
| 2550 | /* Initialize a Unix socket named g.zSockName */ |
| 2551 | assert( g.zSockName!=0 ); |
| 2552 | memset(&uxaddr, 0, sizeof(uxaddr)); |
| 2553 | if( strlen(g.zSockName)>sizeof(uxaddr.sun_path) ){ |
| 2554 | fossil_fatal("name of unix socket too big: %s\nmax size: %d\n", |
| 2555 | g.zSockName, (int)sizeof(uxaddr.sun_path)); |
| 2556 | } |
| 2557 | if( file_isdir(g.zSockName, ExtFILE)!=0 ){ |
| 2558 | if( !file_issocket(g.zSockName) ){ |
| 2559 | fossil_fatal("cannot name socket \"%s\" because another object" |
| 2560 | " with that name already exists", g.zSockName); |
| 2561 | }else{ |
| 2562 | unlink(g.zSockName); |
| 2563 | } |
| 2564 | } |
| 2565 | uxaddr.sun_family = AF_UNIX; |
| 2566 | strncpy(uxaddr.sun_path, g.zSockName, sizeof(uxaddr.sun_path)-1); |
| 2567 | listener = socket(AF_UNIX, SOCK_STREAM, 0); |
| 2568 | if( listener<0 ){ |
| 2569 | fossil_fatal("unable to create a unix socket named %s", |
| 2570 | g.zSockName); |
| 2571 | } |
| 2572 | /* Set the access permission for the new socket. Default to 0660. |
| 2573 | ** But use an alternative specified by --socket-mode if available. |
| 2574 | ** Do this before bind() to avoid a race condition. */ |
| 2575 | if( g.zSockMode ){ |
| 2576 | file_set_mode(g.zSockName, listener, g.zSockMode, 0); |
| 2577 | }else{ |
| 2578 | file_set_mode(g.zSockName, listener, "0660", 1); |
| 2579 | } |
| 2580 | }else{ |
| 2581 | /* Initialize a TCP/IP socket on port iPort */ |
| 2582 | memset(&inaddr, 0, sizeof(inaddr)); |
| 2583 | inaddr.sin_family = AF_INET; |
| 2584 | if( zIpAddr ){ |
| 2585 | inaddr.sin_addr.s_addr = inet_addr(zIpAddr); |
| 2586 | if( inaddr.sin_addr.s_addr == INADDR_NONE ){ |
| 2587 | fossil_fatal("not a valid IP address: %s", zIpAddr); |
| 2588 | } |
| 2589 | }else if( flags & HTTP_SERVER_LOCALHOST ){ |
| 2590 | inaddr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); |
| 2591 | }else{ |
| 2592 | inaddr.sin_addr.s_addr = htonl(INADDR_ANY); |
| 2593 | } |
| 2594 | inaddr.sin_port = htons(iPort); |
| 2595 | listener = socket(AF_INET, SOCK_STREAM, 0); |
| 2596 | if( listener<0 ){ |
| 2597 | iPort++; |
| 2598 | continue; |
| 2599 | } |
| 2600 | } |
| 2601 | |
| 2602 | /* if we can't terminate nicely, at least allow the socket to be reused */ |
| 2603 | setsockopt(listener,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt)); |
| 2604 | |
| 2605 | if( flags & HTTP_SERVER_UNIXSOCKET ){ |
| 2606 | rc = bind(listener, (struct sockaddr*)&uxaddr, sizeof(uxaddr)); |
| 2607 | /* Set the owner of the socket if requested by --socket-owner. This |
| 2608 | ** must wait until after bind(), after the filesystem object has been |
| 2609 | ** created. See https://lkml.org/lkml/2004/11/1/84 and |
| 2610 | ** https://fossil-scm.org/forum/forumpost/7517680ef9684c57 */ |
| 2611 | if( g.zSockOwner ){ |
| 2612 | file_set_owner(g.zSockName, listener, g.zSockOwner); |
| 2613 | } |
| 2614 | }else{ |
| 2615 | rc = bind(listener, (struct sockaddr*)&inaddr, sizeof(inaddr)); |
| 2616 | } |
| 2617 | if( rc<0 ){ |
| 2618 | close(listener); |
| 2619 | iPort++; |
| 2620 | continue; |
| 2621 | } |
| 2622 | break; |
| 2623 | } |
| 2624 | if( iPort>mxPort ){ |
| 2625 | if( flags & HTTP_SERVER_UNIXSOCKET ){ |
| 2626 | fossil_fatal("unable to listen on unix socket %s", zIpAddr); |
| 2627 | }else if( mnPort==mxPort ){ |
| 2628 | fossil_fatal("unable to open listening socket on port %d", mnPort); |
| 2629 | }else{ |
| 2630 | fossil_fatal("unable to open listening socket on any" |
| 2631 | " port in the range %d..%d", mnPort, mxPort); |
| 2632 | } |
| 2633 | } |
| 2634 | if( iPort>mxPort ) return 1; |
| 2635 | listen(listener,10); |
| 2636 | if( flags & HTTP_SERVER_UNIXSOCKET ){ |
| 2637 | fossil_print("Listening for %s requests on unix socket %s\n", |
| 2638 | (flags & HTTP_SERVER_SCGI)!=0 ? "SCGI" : |
| 2639 | g.httpUseSSL?"TLS-encrypted HTTPS":"HTTP", g.zSockName); |
| 2640 | }else{ |
| 2641 | fossil_print("Listening for %s requests on TCP port %d\n", |
| 2642 | (flags & HTTP_SERVER_SCGI)!=0 ? "SCGI" : |
| 2643 | g.httpUseSSL?"TLS-encrypted HTTPS":"HTTP", iPort); |
| 2644 | } |
| 2645 | fflush(stdout); |
| 2646 | if( zBrowser && (flags & HTTP_SERVER_UNIXSOCKET)==0 ){ |
| 2647 | assert( strstr(zBrowser,"%d")!=0 ); |
| 2648 | zBrowser = mprintf(zBrowser /*works-like:"%d"*/, iPort); |
| 2649 | #if defined(__CYGWIN__) |
| 2650 | /* On Cygwin, we can do better than "echo" */ |
| @@ -2658,56 +2769,69 @@ | |
| 2658 | #endif |
| 2659 | if( fossil_system(zBrowser)<0 ){ |
| 2660 | fossil_warning("cannot start browser: %s\n", zBrowser); |
| 2661 | } |
| 2662 | } |
| 2663 | while( 1 ){ |
| 2664 | #if FOSSIL_MAX_CONNECTIONS>0 |
| 2665 | while( nchildren>=FOSSIL_MAX_CONNECTIONS ){ |
| 2666 | if( wait(0)>=0 ) nchildren--; |
| 2667 | } |
| 2668 | #endif |
| 2669 | delay.tv_sec = 0; |
| 2670 | delay.tv_usec = 100000; |
| 2671 | FD_ZERO(&readfds); |
| 2672 | assert( listener>=0 ); |
| 2673 | FD_SET( listener, &readfds); |
| 2674 | select( listener+1, &readfds, 0, 0, &delay); |
| 2675 | if( FD_ISSET(listener, &readfds) ){ |
| 2676 | lenaddr = sizeof(inaddr); |
| 2677 | connection = accept(listener, (struct sockaddr*)&inaddr, &lenaddr); |
| 2678 | if( connection>=0 ){ |
| 2679 | if( flags & HTTP_SERVER_NOFORK ){ |
| 2680 | child = 0; |
| 2681 | }else{ |
| 2682 | child = fork(); |
| 2683 | } |
| 2684 | if( child!=0 ){ |
| 2685 | if( child>0 ){ |
| 2686 | nchildren++; |
| 2687 | nRequest++; |
| 2688 | } |
| 2689 | close(connection); |
| 2690 | }else{ |
| 2691 | int nErr = 0, fd; |
| 2692 | g.zSockName = 0 /* avoid deleting the socket via atexit() */; |
| 2693 | close(0); |
| 2694 | fd = dup(connection); |
| 2695 | if( fd!=0 ) nErr++; |
| 2696 | close(1); |
| 2697 | fd = dup(connection); |
| 2698 | if( fd!=1 ) nErr++; |
| 2699 | if( 0 && !g.fAnyTrace ){ |
| 2700 | close(2); |
| 2701 | fd = dup(connection); |
| 2702 | if( fd!=2 ) nErr++; |
| 2703 | } |
| 2704 | close(connection); |
| 2705 | g.nPendingRequest = nchildren+1; |
| 2706 | g.nRequest = nRequest+1; |
| 2707 | return nErr; |
| 2708 | } |
| 2709 | } |
| 2710 | } |
| 2711 | /* Bury dead children */ |
| 2712 | if( nchildren ){ |
| 2713 | while(1){ |
| 2714 |
| --- src/cgi.c | |
| +++ src/cgi.c | |
| @@ -72,10 +72,11 @@ | |
| 72 | # include <ws2tcpip.h> |
| 73 | #else |
| 74 | # include <sys/socket.h> |
| 75 | # include <sys/un.h> |
| 76 | # include <netinet/in.h> |
| 77 | # include <netdb.h> |
| 78 | # include <arpa/inet.h> |
| 79 | # include <sys/times.h> |
| 80 | # include <sys/time.h> |
| 81 | # include <sys/wait.h> |
| 82 | # include <sys/select.h> |
| @@ -103,12 +104,12 @@ | |
| 104 | #define PT(x) cgi_parameter_trimmed((x),0) |
| 105 | #define PDT(x,y) cgi_parameter_trimmed((x),(y)) |
| 106 | #define PB(x) cgi_parameter_boolean(x) |
| 107 | #define PCK(x) cgi_parameter_checked(x,1) |
| 108 | #define PIF(x,y) cgi_parameter_checked(x,y) |
| 109 | #define P_NoBot(x) cgi_parameter_no_attack((x),0) |
| 110 | #define PD_NoBot(x,y) cgi_parameter_no_attack((x),(y)) |
| 111 | |
| 112 | /* |
| 113 | ** Shortcut for the cgi_printf() routine. Instead of using the |
| 114 | ** |
| 115 | ** @ ... |
| @@ -637,10 +638,13 @@ | |
| 638 | cgi_set_status(iStat, zStat); |
| 639 | free(zLocation); |
| 640 | cgi_reply(); |
| 641 | fossil_exit(0); |
| 642 | } |
| 643 | NORETURN void cgi_redirect_perm(const char *zURL){ |
| 644 | cgi_redirect_with_status(zURL, 301, "Moved Permanently"); |
| 645 | } |
| 646 | NORETURN void cgi_redirect(const char *zURL){ |
| 647 | cgi_redirect_with_status(zURL, 302, "Moved Temporarily"); |
| 648 | } |
| 649 | NORETURN void cgi_redirect_with_method(const char *zURL){ |
| 650 | cgi_redirect_with_status(zURL, 307, "Temporary Redirect"); |
| @@ -1620,37 +1624,39 @@ | |
| 1624 | fossil_errorlog("Xpossible hack attempt - 418 response on \"%s\"", zName); |
| 1625 | exit(0); |
| 1626 | } |
| 1627 | |
| 1628 | /* |
| 1629 | ** If looks_like_attack() returns true for the given string, call |
| 1630 | ** cgi_begone_spider() and does not return, else this function has no |
| 1631 | ** side effects. The range of checks performed by this function may |
| 1632 | ** be extended in the future. |
| 1633 | ** |
| 1634 | ** Checks are omitted for any logged-in user. |
| 1635 | ** |
| 1636 | ** This is the primary defense against attack. Fossil should easily be |
| 1637 | ** proof against SQL injection and XSS attacks even without without this |
| 1638 | ** routine. Rather, this is an attempt to avoid denial-of-service caused |
| 1639 | ** by persistent spiders that hammer the server with dozens or hundreds of |
| 1640 | ** probes per seconds as they look for vulnerabilities. In other |
| 1641 | ** words, this is an effort to reduce the CPU load imposed by malicious |
| 1642 | ** spiders. Though those routine might help make attacks harder, it is |
| 1643 | ** not itself an impenetrably barrier against attack and should not be |
| 1644 | ** relied upon as the only defense. |
| 1645 | */ |
| 1646 | void cgi_value_spider_check(const char *zTxt, const char *zName){ |
| 1647 | if( g.zLogin==0 && looks_like_attack(zTxt) ){ |
| 1648 | cgi_begone_spider(zName); |
| 1649 | } |
| 1650 | } |
| 1651 | |
| 1652 | /* |
| 1653 | ** A variant of cgi_parameter() with the same semantics except that if |
| 1654 | ** cgi_parameter(zName,zDefault) returns a value other than zDefault |
| 1655 | ** then it passes that value to cgi_value_spider_check(). |
| 1656 | */ |
| 1657 | const char *cgi_parameter_no_attack(const char *zName, const char *zDefault){ |
| 1658 | const char *zTxt = cgi_parameter(zName, zDefault); |
| 1659 | |
| 1660 | if( zTxt!=zDefault ){ |
| 1661 | cgi_value_spider_check(zTxt, zName); |
| 1662 | } |
| @@ -2070,34 +2076,40 @@ | |
| 2076 | } |
| 2077 | if( zLeftOver ){ *zLeftOver = zInput; } |
| 2078 | return zResult; |
| 2079 | } |
| 2080 | |
| 2081 | /* |
| 2082 | ** All possible forms of an IP address. Needed to work around GCC strict |
| 2083 | ** aliasing rules. |
| 2084 | */ |
| 2085 | typedef union { |
| 2086 | struct sockaddr sa; /* Abstract superclass */ |
| 2087 | struct sockaddr_in sa4; /* IPv4 */ |
| 2088 | struct sockaddr_in6 sa6; /* IPv6 */ |
| 2089 | struct sockaddr_storage sas; /* Should be the maximum of the above 3 */ |
| 2090 | } address; |
| 2091 | |
| 2092 | /* |
| 2093 | ** Determine the IP address on the other side of a connection. |
| 2094 | ** Return a pointer to a string. Or return 0 if unable. |
| 2095 | ** |
| 2096 | ** The string is held in a static buffer that is overwritten on |
| 2097 | ** each call. |
| 2098 | */ |
| 2099 | char *cgi_remote_ip(int fd){ |
| 2100 | address remoteAddr; |
| 2101 | socklen_t size = sizeof(remoteAddr); |
| 2102 | static char zHost[NI_MAXHOST]; |
| 2103 | if( getpeername(0, &remoteAddr.sa, &size) ){ |
| 2104 | return 0; |
| 2105 | } |
| 2106 | if( getnameinfo(&remoteAddr.sa, size, zHost, sizeof(zHost), 0, 0, |
| 2107 | NI_NUMERICHOST) ){ |
| 2108 | return 0; |
| 2109 | } |
| 2110 | return zHost; |
| 2111 | } |
| 2112 | |
| 2113 | /* |
| 2114 | ** This routine handles a single HTTP request which is coming in on |
| 2115 | ** g.httpIn and which replies on g.httpOut |
| @@ -2486,11 +2498,10 @@ | |
| 2498 | fossil_free(zToFree); |
| 2499 | fgetc(g.httpIn); /* Read past the "," separating header from content */ |
| 2500 | cgi_init(); |
| 2501 | } |
| 2502 | |
| 2503 | #if INTERFACE |
| 2504 | /* |
| 2505 | ** Bitmap values for the flags parameter to cgi_http_server(). |
| 2506 | */ |
| 2507 | #define HTTP_SERVER_LOCALHOST 0x0001 /* Bind to 127.0.0.1 only */ |
| @@ -2529,122 +2540,222 @@ | |
| 2540 | ){ |
| 2541 | #if defined(_WIN32) |
| 2542 | /* Use win32_http_server() instead */ |
| 2543 | fossil_exit(1); |
| 2544 | #else |
| 2545 | int listen4 = -1; /* Main socket; IPv4 or unix-domain */ |
| 2546 | int listen6 = -1; /* Aux socket for corresponding IPv6 */ |
| 2547 | int mxListen = -1; /* Maximum of listen4 and listen6 */ |
| 2548 | int connection; /* An incoming connection */ |
| 2549 | int nRequest = 0; /* Number of requests handled so far */ |
| 2550 | fd_set readfds; /* Set of file descriptors for select() */ |
| 2551 | socklen_t lenaddr; /* Length of the inaddr structure */ |
| 2552 | int child; /* PID of the child process */ |
| 2553 | int nchildren = 0; /* Number of child processes */ |
| 2554 | struct timeval delay; /* How long to wait inside select() */ |
| 2555 | struct sockaddr_in6 inaddr6; /* Address for IPv6 */ |
| 2556 | struct sockaddr_in inaddr4; /* Address for IPv4 */ |
| 2557 | struct sockaddr_un uxaddr; /* The address for unix-domain sockets */ |
| 2558 | int opt = 1; /* setsockopt flag */ |
| 2559 | int rc; /* Result code from system calls */ |
| 2560 | int iPort = mnPort; /* Port to try to use */ |
| 2561 | const char *zRequestType; /* Type of requests to listen for */ |
| 2562 | |
| 2563 | |
| 2564 | if( flags & HTTP_SERVER_SCGI ){ |
| 2565 | zRequestType = "SCGI"; |
| 2566 | }else if( g.httpUseSSL ){ |
| 2567 | zRequestType = "TLS-encrypted HTTPS"; |
| 2568 | }else{ |
| 2569 | zRequestType = "HTTP"; |
| 2570 | } |
| 2571 | |
| 2572 | if( flags & HTTP_SERVER_UNIXSOCKET ){ |
| 2573 | /* CASE 1: A unix socket named g.zSockName. After creation, set the |
| 2574 | ** permissions on the new socket to g.zSockMode and make the |
| 2575 | ** owner of the socket be g.zSockOwner. |
| 2576 | */ |
| 2577 | assert( g.zSockName!=0 ); |
| 2578 | memset(&uxaddr, 0, sizeof(uxaddr)); |
| 2579 | if( strlen(g.zSockName)>sizeof(uxaddr.sun_path) ){ |
| 2580 | fossil_fatal("name of unix socket too big: %s\nmax size: %d\n", |
| 2581 | g.zSockName, (int)sizeof(uxaddr.sun_path)); |
| 2582 | } |
| 2583 | if( file_isdir(g.zSockName, ExtFILE)!=0 ){ |
| 2584 | if( !file_issocket(g.zSockName) ){ |
| 2585 | fossil_fatal("cannot name socket \"%s\" because another object" |
| 2586 | " with that name already exists", g.zSockName); |
| 2587 | }else{ |
| 2588 | unlink(g.zSockName); |
| 2589 | } |
| 2590 | } |
| 2591 | uxaddr.sun_family = AF_UNIX; |
| 2592 | strncpy(uxaddr.sun_path, g.zSockName, sizeof(uxaddr.sun_path)-1); |
| 2593 | listen4 = socket(AF_UNIX, SOCK_STREAM, 0); |
| 2594 | if( listen4<0 ){ |
| 2595 | fossil_fatal("unable to create a unix socket named %s", |
| 2596 | g.zSockName); |
| 2597 | } |
| 2598 | mxListen = listen4; |
| 2599 | listen6 = -1; |
| 2600 | |
| 2601 | /* Set the access permission for the new socket. Default to 0660. |
| 2602 | ** But use an alternative specified by --socket-mode if available. |
| 2603 | ** Do this before bind() to avoid a race condition. */ |
| 2604 | if( g.zSockMode ){ |
| 2605 | file_set_mode(g.zSockName, listen4, g.zSockMode, 0); |
| 2606 | }else{ |
| 2607 | file_set_mode(g.zSockName, listen4, "0660", 1); |
| 2608 | } |
| 2609 | rc = bind(listen4, (struct sockaddr*)&uxaddr, sizeof(uxaddr)); |
| 2610 | /* Set the owner of the socket if requested by --socket-owner. This |
| 2611 | ** must wait until after bind(), after the filesystem object has been |
| 2612 | ** created. See https://lkml.org/lkml/2004/11/1/84 and |
| 2613 | ** https://fossil-scm.org/forum/forumpost/7517680ef9684c57 */ |
| 2614 | if( g.zSockOwner ){ |
| 2615 | file_set_owner(g.zSockName, listen4, g.zSockOwner); |
| 2616 | } |
| 2617 | fossil_print("Listening for %s requests on unix socket %s\n", |
| 2618 | zRequestType, g.zSockName); |
| 2619 | fflush(stdout); |
| 2620 | }else if( zIpAddr && strchr(zIpAddr,':')!=0 ){ |
| 2621 | /* CASE 2: TCP on IPv6 IP address specified by zIpAddr and on port iPort. |
| 2622 | */ |
| 2623 | assert( mnPort==mxPort ); |
| 2624 | memset(&inaddr6, 0, sizeof(inaddr6)); |
| 2625 | inaddr6.sin6_family = AF_INET6; |
| 2626 | inaddr6.sin6_port = htons(iPort); |
| 2627 | if( inet_pton(AF_INET6, zIpAddr, &inaddr6.sin6_addr)==0 ){ |
| 2628 | fossil_fatal("not a valid IPv6 address: %s", zIpAddr); |
| 2629 | } |
| 2630 | listen6 = socket(AF_INET6, SOCK_STREAM, 0); |
| 2631 | if( listen6>0 ){ |
| 2632 | opt = 1; |
| 2633 | setsockopt(listen6, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); |
| 2634 | rc = bind(listen6, (struct sockaddr*)&inaddr6, sizeof(inaddr6)); |
| 2635 | if( rc<0 ){ |
| 2636 | close(listen6); |
| 2637 | listen6 = -1; |
| 2638 | } |
| 2639 | } |
| 2640 | if( listen6<0 ){ |
| 2641 | fossil_fatal("cannot open a listening socket on [%s]:%d", |
| 2642 | zIpAddr, mnPort); |
| 2643 | } |
| 2644 | mxListen = listen6; |
| 2645 | listen4 = -1; |
| 2646 | fossil_print("Listening for %s requests on [%s]:%d\n", |
| 2647 | zRequestType, zIpAddr, iPort); |
| 2648 | fflush(stdout); |
| 2649 | }else if( zIpAddr && zIpAddr[0] ){ |
| 2650 | /* CASE 3: TCP on IPv4 IP address specified by zIpAddr and on port iPort. |
| 2651 | */ |
| 2652 | assert( mnPort==mxPort ); |
| 2653 | memset(&inaddr4, 0, sizeof(inaddr4)); |
| 2654 | inaddr4.sin_family = AF_INET; |
| 2655 | inaddr4.sin_port = htons(iPort); |
| 2656 | if( strcmp(zIpAddr, "localhost")==0 ) zIpAddr = "127.0.0.1"; |
| 2657 | inaddr4.sin_addr.s_addr = inet_addr(zIpAddr); |
| 2658 | if( inaddr4.sin_addr.s_addr == INADDR_NONE ){ |
| 2659 | fossil_fatal("not a valid IPv4 address: %s", zIpAddr); |
| 2660 | } |
| 2661 | listen4 = socket(AF_INET, SOCK_STREAM, 0); |
| 2662 | if( listen4>0 ){ |
| 2663 | setsockopt(listen4, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); |
| 2664 | rc = bind(listen4, (struct sockaddr*)&inaddr4, sizeof(inaddr4)); |
| 2665 | if( rc<0 ){ |
| 2666 | close(listen6); |
| 2667 | listen4 = -1; |
| 2668 | } |
| 2669 | } |
| 2670 | if( listen4<0 ){ |
| 2671 | fossil_fatal("cannot open a listening socket on %s:%d", |
| 2672 | zIpAddr, mnPort); |
| 2673 | } |
| 2674 | mxListen = listen4; |
| 2675 | listen6 = -1; |
| 2676 | fossil_print("Listening for %s requests on TCP port %s:%d\n", |
| 2677 | zRequestType, zIpAddr, iPort); |
| 2678 | fflush(stdout); |
| 2679 | }else{ |
| 2680 | /* CASE 4: Listen on all available IP addresses, or on only loopback |
| 2681 | ** addresses (if HTTP_SERVER_LOCALHOST). The TCP port is the |
| 2682 | ** first available in the range of mnPort..mxPort. Listen |
| 2683 | ** on both IPv4 and IPv6, if possible. The TCP port scan is done |
| 2684 | ** on IPv4. |
| 2685 | */ |
| 2686 | while( iPort<=mxPort ){ |
| 2687 | const char *zProto; |
| 2688 | memset(&inaddr4, 0, sizeof(inaddr4)); |
| 2689 | inaddr4.sin_family = AF_INET; |
| 2690 | inaddr4.sin_port = htons(iPort); |
| 2691 | if( flags & HTTP_SERVER_LOCALHOST ){ |
| 2692 | inaddr4.sin_addr.s_addr = htonl(INADDR_LOOPBACK); |
| 2693 | }else{ |
| 2694 | inaddr4.sin_addr.s_addr = htonl(INADDR_ANY); |
| 2695 | } |
| 2696 | listen4 = socket(AF_INET, SOCK_STREAM, 0); |
| 2697 | if( listen4>0 ){ |
| 2698 | setsockopt(listen4, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); |
| 2699 | rc = bind(listen4, (struct sockaddr*)&inaddr4, sizeof(inaddr4)); |
| 2700 | if( rc<0 ){ |
| 2701 | close(listen4); |
| 2702 | listen4 = -1; |
| 2703 | } |
| 2704 | } |
| 2705 | if( listen4<0 ){ |
| 2706 | iPort++; |
| 2707 | continue; |
| 2708 | } |
| 2709 | mxListen = listen4; |
| 2710 | |
| 2711 | /* If we get here, that means we found an open TCP port at iPort for |
| 2712 | ** IPv4. Try to set up a corresponding IPv6 socket on the same port. |
| 2713 | */ |
| 2714 | memset(&inaddr6, 0, sizeof(inaddr6)); |
| 2715 | inaddr6.sin6_family = AF_INET6; |
| 2716 | inaddr6.sin6_port = htons(iPort); |
| 2717 | if( flags & HTTP_SERVER_LOCALHOST ){ |
| 2718 | inaddr6.sin6_addr = in6addr_loopback; |
| 2719 | }else{ |
| 2720 | inaddr6.sin6_addr = in6addr_any; |
| 2721 | } |
| 2722 | listen6 = socket(AF_INET6, SOCK_STREAM, 0); |
| 2723 | if( listen6>0 ){ |
| 2724 | setsockopt(listen6, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); |
| 2725 | setsockopt(listen6, IPPROTO_IPV6, IPV6_V6ONLY, &opt, sizeof(opt)); |
| 2726 | rc = bind(listen6, (struct sockaddr*)&inaddr6, sizeof(inaddr6)); |
| 2727 | if( rc<0 ){ |
| 2728 | close(listen6); |
| 2729 | listen6 = -1; |
| 2730 | } |
| 2731 | } |
| 2732 | if( listen6<0 ){ |
| 2733 | zProto = "IPv4 only"; |
| 2734 | }else{ |
| 2735 | zProto = "IPv4 and IPv6"; |
| 2736 | if( listen6>listen4 ) mxListen = listen6; |
| 2737 | } |
| 2738 | |
| 2739 | fossil_print("Listening for %s requests on TCP port %s%d, %s\n", |
| 2740 | zRequestType, |
| 2741 | (flags & HTTP_SERVER_LOCALHOST)!=0 ? "localhost:" : "", |
| 2742 | iPort, zProto); |
| 2743 | fflush(stdout); |
| 2744 | break; |
| 2745 | } |
| 2746 | if( iPort>mxPort ){ |
| 2747 | fossil_fatal("no available TCP ports in the range %d..%d", |
| 2748 | mnPort, mxPort); |
| 2749 | } |
| 2750 | } |
| 2751 | |
| 2752 | /* If we get to this point, that means there is at least one listening |
| 2753 | ** socket on either listen4 or listen6 and perhaps on both. */ |
| 2754 | assert( listen4>0 || listen6>0 ); |
| 2755 | if( listen4>0 ) listen(listen4,10); |
| 2756 | if( listen6>0 ) listen(listen6,10); |
| 2757 | if( zBrowser && (flags & HTTP_SERVER_UNIXSOCKET)==0 ){ |
| 2758 | assert( strstr(zBrowser,"%d")!=0 ); |
| 2759 | zBrowser = mprintf(zBrowser /*works-like:"%d"*/, iPort); |
| 2760 | #if defined(__CYGWIN__) |
| 2761 | /* On Cygwin, we can do better than "echo" */ |
| @@ -2658,56 +2769,69 @@ | |
| 2769 | #endif |
| 2770 | if( fossil_system(zBrowser)<0 ){ |
| 2771 | fossil_warning("cannot start browser: %s\n", zBrowser); |
| 2772 | } |
| 2773 | } |
| 2774 | |
| 2775 | /* What for incomming requests. For each request, fork() a child process |
| 2776 | ** to deal with that request. The child process returns. The parent |
| 2777 | ** keeps on listening and never returns. |
| 2778 | */ |
| 2779 | while( 1 ){ |
| 2780 | #if FOSSIL_MAX_CONNECTIONS>0 |
| 2781 | while( nchildren>=FOSSIL_MAX_CONNECTIONS ){ |
| 2782 | if( wait(0)>=0 ) nchildren--; |
| 2783 | } |
| 2784 | #endif |
| 2785 | delay.tv_sec = 0; |
| 2786 | delay.tv_usec = 100000; |
| 2787 | FD_ZERO(&readfds); |
| 2788 | assert( listen4>0 || listen6>0 ); |
| 2789 | if( listen4>0 ) FD_SET( listen4, &readfds); |
| 2790 | if( listen6>0 ) FD_SET( listen6, &readfds); |
| 2791 | select( mxListen+1, &readfds, 0, 0, &delay); |
| 2792 | if( listen4>0 && FD_ISSET(listen4, &readfds) ){ |
| 2793 | lenaddr = sizeof(inaddr4); |
| 2794 | connection = accept(listen4, (struct sockaddr*)&inaddr4, &lenaddr); |
| 2795 | }else if( listen6>0 && FD_ISSET(listen6, &readfds) ){ |
| 2796 | lenaddr = sizeof(inaddr6); |
| 2797 | connection = accept(listen6, (struct sockaddr*)&inaddr6, &lenaddr); |
| 2798 | }else{ |
| 2799 | connection = -1; |
| 2800 | } |
| 2801 | if( connection>=0 ){ |
| 2802 | if( flags & HTTP_SERVER_NOFORK ){ |
| 2803 | child = 0; |
| 2804 | }else{ |
| 2805 | child = fork(); |
| 2806 | } |
| 2807 | if( child!=0 ){ |
| 2808 | if( child>0 ){ |
| 2809 | nchildren++; |
| 2810 | nRequest++; |
| 2811 | } |
| 2812 | close(connection); |
| 2813 | }else{ |
| 2814 | int nErr = 0, fd; |
| 2815 | g.zSockName = 0 /* avoid deleting the socket via atexit() */; |
| 2816 | close(0); |
| 2817 | fd = dup(connection); |
| 2818 | if( fd!=0 ) nErr++; |
| 2819 | close(1); |
| 2820 | fd = dup(connection); |
| 2821 | if( fd!=1 ) nErr++; |
| 2822 | if( 0 && !g.fAnyTrace ){ |
| 2823 | close(2); |
| 2824 | fd = dup(connection); |
| 2825 | if( fd!=2 ) nErr++; |
| 2826 | } |
| 2827 | close(connection); |
| 2828 | if( listen4>0 ) close(listen4); |
| 2829 | if( listen6>0 ) close(listen6); |
| 2830 | g.nPendingRequest = nchildren+1; |
| 2831 | g.nRequest = nRequest+1; |
| 2832 | return nErr; |
| 2833 | } |
| 2834 | } |
| 2835 | /* Bury dead children */ |
| 2836 | if( nchildren ){ |
| 2837 | while(1){ |
| 2838 |
+2
-1
| --- src/chat.c | ||
| +++ src/chat.c | ||
| @@ -254,11 +254,12 @@ | ||
| 254 | 254 | @ /*^^^for skins which add their own BODY tag */; |
| 255 | 255 | @ window.fossil.config.chat = { |
| 256 | 256 | @ fromcli: %h(PB("cli")?"true":"false"), |
| 257 | 257 | @ alertSound: "%h(zAlert)", |
| 258 | 258 | @ initSize: %d(db_get_int("chat-initial-history",50)), |
| 259 | - @ imagesInline: !!%d(db_get_boolean("chat-inline-images",1)) | |
| 259 | + @ imagesInline: !!%d(db_get_boolean("chat-inline-images",1)), | |
| 260 | + @ pollTimeout: %d(db_get_int("chat-poll-timeout",420)) | |
| 260 | 261 | @ }; |
| 261 | 262 | ajax_emit_js_preview_modes(0); |
| 262 | 263 | chat_emit_alert_list(); |
| 263 | 264 | @ }, false); |
| 264 | 265 | @ </script> |
| 265 | 266 |
| --- src/chat.c | |
| +++ src/chat.c | |
| @@ -254,11 +254,12 @@ | |
| 254 | @ /*^^^for skins which add their own BODY tag */; |
| 255 | @ window.fossil.config.chat = { |
| 256 | @ fromcli: %h(PB("cli")?"true":"false"), |
| 257 | @ alertSound: "%h(zAlert)", |
| 258 | @ initSize: %d(db_get_int("chat-initial-history",50)), |
| 259 | @ imagesInline: !!%d(db_get_boolean("chat-inline-images",1)) |
| 260 | @ }; |
| 261 | ajax_emit_js_preview_modes(0); |
| 262 | chat_emit_alert_list(); |
| 263 | @ }, false); |
| 264 | @ </script> |
| 265 |
| --- src/chat.c | |
| +++ src/chat.c | |
| @@ -254,11 +254,12 @@ | |
| 254 | @ /*^^^for skins which add their own BODY tag */; |
| 255 | @ window.fossil.config.chat = { |
| 256 | @ fromcli: %h(PB("cli")?"true":"false"), |
| 257 | @ alertSound: "%h(zAlert)", |
| 258 | @ initSize: %d(db_get_int("chat-initial-history",50)), |
| 259 | @ imagesInline: !!%d(db_get_boolean("chat-inline-images",1)), |
| 260 | @ pollTimeout: %d(db_get_int("chat-poll-timeout",420)) |
| 261 | @ }; |
| 262 | ajax_emit_js_preview_modes(0); |
| 263 | chat_emit_alert_list(); |
| 264 | @ }, false); |
| 265 | @ </script> |
| 266 |
+4
| --- src/checkin.c | ||
| +++ src/checkin.c | ||
| @@ -2436,16 +2436,19 @@ | ||
| 2436 | 2436 | ** --allow-conflict Allow unresolved merge conflicts |
| 2437 | 2437 | ** --allow-empty Allow a commit with no changes |
| 2438 | 2438 | ** --allow-fork Allow the commit to fork |
| 2439 | 2439 | ** --allow-older Allow a commit older than its ancestor |
| 2440 | 2440 | ** --baseline Use a baseline manifest in the commit process |
| 2441 | +** --bgcolor COLOR Apply COLOR to this one check-in only | |
| 2441 | 2442 | ** --branch NEW-BRANCH-NAME Check in to this new branch |
| 2443 | +** --branchcolor COLOR Apply given COLOR to the branch | |
| 2442 | 2444 | ** --close Close the branch being committed |
| 2443 | 2445 | ** --date-override DATETIME Make DATETIME the time of the check-in. |
| 2444 | 2446 | ** Useful when importing historical check-ins |
| 2445 | 2447 | ** from another version control system. |
| 2446 | 2448 | ** --delta Use a delta manifest in the commit process |
| 2449 | +** --editor NAME Text editor to use for check-in comment. | |
| 2447 | 2450 | ** --hash Verify file status using hashing rather |
| 2448 | 2451 | ** than relying on filesystem mtimes |
| 2449 | 2452 | ** --if-changes Make this command a silent no-op if there |
| 2450 | 2453 | ** are no changes |
| 2451 | 2454 | ** --ignore-clock-skew If a clock skew is detected, ignore it and |
| @@ -2597,10 +2600,11 @@ | ||
| 2597 | 2600 | useCksum = db_get_boolean("repo-cksum", 1); |
| 2598 | 2601 | bIgnoreSkew = find_option("ignore-clock-skew",0,0)!=0; |
| 2599 | 2602 | outputManifest = db_get_manifest_setting(0); |
| 2600 | 2603 | mxSize = db_large_file_size(); |
| 2601 | 2604 | if( find_option("ignore-oversize",0,0)!=0 ) mxSize = 0; |
| 2605 | + (void)fossil_text_editor(); | |
| 2602 | 2606 | verify_all_options(); |
| 2603 | 2607 | |
| 2604 | 2608 | /* The --no-warnings flag and the --force flag each imply |
| 2605 | 2609 | ** the --no-verify-comment flag */ |
| 2606 | 2610 | if( noWarningFlag || forceFlag ){ |
| 2607 | 2611 |
| --- src/checkin.c | |
| +++ src/checkin.c | |
| @@ -2436,16 +2436,19 @@ | |
| 2436 | ** --allow-conflict Allow unresolved merge conflicts |
| 2437 | ** --allow-empty Allow a commit with no changes |
| 2438 | ** --allow-fork Allow the commit to fork |
| 2439 | ** --allow-older Allow a commit older than its ancestor |
| 2440 | ** --baseline Use a baseline manifest in the commit process |
| 2441 | ** --branch NEW-BRANCH-NAME Check in to this new branch |
| 2442 | ** --close Close the branch being committed |
| 2443 | ** --date-override DATETIME Make DATETIME the time of the check-in. |
| 2444 | ** Useful when importing historical check-ins |
| 2445 | ** from another version control system. |
| 2446 | ** --delta Use a delta manifest in the commit process |
| 2447 | ** --hash Verify file status using hashing rather |
| 2448 | ** than relying on filesystem mtimes |
| 2449 | ** --if-changes Make this command a silent no-op if there |
| 2450 | ** are no changes |
| 2451 | ** --ignore-clock-skew If a clock skew is detected, ignore it and |
| @@ -2597,10 +2600,11 @@ | |
| 2597 | useCksum = db_get_boolean("repo-cksum", 1); |
| 2598 | bIgnoreSkew = find_option("ignore-clock-skew",0,0)!=0; |
| 2599 | outputManifest = db_get_manifest_setting(0); |
| 2600 | mxSize = db_large_file_size(); |
| 2601 | if( find_option("ignore-oversize",0,0)!=0 ) mxSize = 0; |
| 2602 | verify_all_options(); |
| 2603 | |
| 2604 | /* The --no-warnings flag and the --force flag each imply |
| 2605 | ** the --no-verify-comment flag */ |
| 2606 | if( noWarningFlag || forceFlag ){ |
| 2607 |
| --- src/checkin.c | |
| +++ src/checkin.c | |
| @@ -2436,16 +2436,19 @@ | |
| 2436 | ** --allow-conflict Allow unresolved merge conflicts |
| 2437 | ** --allow-empty Allow a commit with no changes |
| 2438 | ** --allow-fork Allow the commit to fork |
| 2439 | ** --allow-older Allow a commit older than its ancestor |
| 2440 | ** --baseline Use a baseline manifest in the commit process |
| 2441 | ** --bgcolor COLOR Apply COLOR to this one check-in only |
| 2442 | ** --branch NEW-BRANCH-NAME Check in to this new branch |
| 2443 | ** --branchcolor COLOR Apply given COLOR to the branch |
| 2444 | ** --close Close the branch being committed |
| 2445 | ** --date-override DATETIME Make DATETIME the time of the check-in. |
| 2446 | ** Useful when importing historical check-ins |
| 2447 | ** from another version control system. |
| 2448 | ** --delta Use a delta manifest in the commit process |
| 2449 | ** --editor NAME Text editor to use for check-in comment. |
| 2450 | ** --hash Verify file status using hashing rather |
| 2451 | ** than relying on filesystem mtimes |
| 2452 | ** --if-changes Make this command a silent no-op if there |
| 2453 | ** are no changes |
| 2454 | ** --ignore-clock-skew If a clock skew is detected, ignore it and |
| @@ -2597,10 +2600,11 @@ | |
| 2600 | useCksum = db_get_boolean("repo-cksum", 1); |
| 2601 | bIgnoreSkew = find_option("ignore-clock-skew",0,0)!=0; |
| 2602 | outputManifest = db_get_manifest_setting(0); |
| 2603 | mxSize = db_large_file_size(); |
| 2604 | if( find_option("ignore-oversize",0,0)!=0 ) mxSize = 0; |
| 2605 | (void)fossil_text_editor(); |
| 2606 | verify_all_options(); |
| 2607 | |
| 2608 | /* The --no-warnings flag and the --force flag each imply |
| 2609 | ** the --no-verify-comment flag */ |
| 2610 | if( noWarningFlag || forceFlag ){ |
| 2611 |
+353
| --- src/color.c | ||
| +++ src/color.c | ||
| @@ -20,10 +20,281 @@ | ||
| 20 | 20 | ** |
| 21 | 21 | */ |
| 22 | 22 | #include "config.h" |
| 23 | 23 | #include <string.h> |
| 24 | 24 | #include "color.h" |
| 25 | + | |
| 26 | +/* | |
| 27 | +** 140 standard CSS color names and their corresponding RGB values, | |
| 28 | +** in alphabetical order by name so that we can do a binary search | |
| 29 | +** for lookup. | |
| 30 | +*/ | |
| 31 | +static const struct CssColors { | |
| 32 | + const char *zName; /* CSS Color name, lower case */ | |
| 33 | + unsigned int iRGB; /* Corresponding RGB value */ | |
| 34 | +} aCssColors[] = { | |
| 35 | + { "aliceblue", 0xf0f8ff }, | |
| 36 | + { "antiquewhite", 0xfaebd7 }, | |
| 37 | + { "aqua", 0x00ffff }, | |
| 38 | + { "aquamarine", 0x7fffd4 }, | |
| 39 | + { "azure", 0xf0ffff }, | |
| 40 | + { "beige", 0xf5f5dc }, | |
| 41 | + { "bisque", 0xffe4c4 }, | |
| 42 | + { "black", 0x000000 }, | |
| 43 | + { "blanchedalmond", 0xffebcd }, | |
| 44 | + { "blue", 0x0000ff }, | |
| 45 | + { "blueviolet", 0x8a2be2 }, | |
| 46 | + { "brown", 0xa52a2a }, | |
| 47 | + { "burlywood", 0xdeb887 }, | |
| 48 | + { "cadetblue", 0x5f9ea0 }, | |
| 49 | + { "chartreuse", 0x7fff00 }, | |
| 50 | + { "chocolate", 0xd2691e }, | |
| 51 | + { "coral", 0xff7f50 }, | |
| 52 | + { "cornflowerblue", 0x6495ed }, | |
| 53 | + { "cornsilk", 0xfff8dc }, | |
| 54 | + { "crimson", 0xdc143c }, | |
| 55 | + { "cyan", 0x00ffff }, | |
| 56 | + { "darkblue", 0x00008b }, | |
| 57 | + { "darkcyan", 0x008b8b }, | |
| 58 | + { "darkgoldenrod", 0xb8860b }, | |
| 59 | + { "darkgray", 0xa9a9a9 }, | |
| 60 | + { "darkgreen", 0x006400 }, | |
| 61 | + { "darkkhaki", 0xbdb76b }, | |
| 62 | + { "darkmagenta", 0x8b008b }, | |
| 63 | + { "darkolivegreen", 0x556b2f }, | |
| 64 | + { "darkorange", 0xff8c00 }, | |
| 65 | + { "darkorchid", 0x9932cc }, | |
| 66 | + { "darkred", 0x8b0000 }, | |
| 67 | + { "darksalmon", 0xe9967a }, | |
| 68 | + { "darkseagreen", 0x8fbc8f }, | |
| 69 | + { "darkslateblue", 0x483d8b }, | |
| 70 | + { "darkslategray", 0x2f4f4f }, | |
| 71 | + { "darkturquoise", 0x00ced1 }, | |
| 72 | + { "darkviolet", 0x9400d3 }, | |
| 73 | + { "deeppink", 0xff1493 }, | |
| 74 | + { "deepskyblue", 0x00bfff }, | |
| 75 | + { "dimgray", 0x696969 }, | |
| 76 | + { "dodgerblue", 0x1e90ff }, | |
| 77 | + { "firebrick", 0xb22222 }, | |
| 78 | + { "floralwhite", 0xfffaf0 }, | |
| 79 | + { "forestgreen", 0x228b22 }, | |
| 80 | + { "fuchsia", 0xff00ff }, | |
| 81 | + { "gainsboro", 0xdcdcdc }, | |
| 82 | + { "ghostwhite", 0xf8f8ff }, | |
| 83 | + { "gold", 0xffd700 }, | |
| 84 | + { "goldenrod", 0xdaa520 }, | |
| 85 | + { "gray", 0x808080 }, | |
| 86 | + { "green", 0x008000 }, | |
| 87 | + { "greenyellow", 0xadff2f }, | |
| 88 | + { "honeydew", 0xf0fff0 }, | |
| 89 | + { "hotpink", 0xff69b4 }, | |
| 90 | + { "indianred", 0xcd5c5c }, | |
| 91 | + { "indigo", 0x4b0082 }, | |
| 92 | + { "ivory", 0xfffff0 }, | |
| 93 | + { "khaki", 0xf0e68c }, | |
| 94 | + { "lavender", 0xe6e6fa }, | |
| 95 | + { "lavenderblush", 0xfff0f5 }, | |
| 96 | + { "lawngreen", 0x7cfc00 }, | |
| 97 | + { "lemonchiffon", 0xfffacd }, | |
| 98 | + { "lightblue", 0xadd8e6 }, | |
| 99 | + { "lightcoral", 0xf08080 }, | |
| 100 | + { "lightcyan", 0xe0ffff }, | |
| 101 | + { "lightgoldenrodyellow", 0xfafad2 }, | |
| 102 | + { "lightgrey", 0xd3d3d3 }, | |
| 103 | + { "lightgreen", 0x90ee90 }, | |
| 104 | + { "lightpink", 0xffb6c1 }, | |
| 105 | + { "lightsalmon", 0xffa07a }, | |
| 106 | + { "lightseagreen", 0x20b2aa }, | |
| 107 | + { "lightskyblue", 0x87cefa }, | |
| 108 | + { "lightslategray", 0x778899 }, | |
| 109 | + { "lightsteelblue", 0xb0c4de }, | |
| 110 | + { "lightyellow", 0xffffe0 }, | |
| 111 | + { "lime", 0x00ff00 }, | |
| 112 | + { "limegreen", 0x32cd32 }, | |
| 113 | + { "linen", 0xfaf0e6 }, | |
| 114 | + { "magenta", 0xff00ff }, | |
| 115 | + { "maroon", 0x800000 }, | |
| 116 | + { "mediumaquamarine", 0x66cdaa }, | |
| 117 | + { "mediumblue", 0x0000cd }, | |
| 118 | + { "mediumorchid", 0xba55d3 }, | |
| 119 | + { "mediumpurple", 0x9370d8 }, | |
| 120 | + { "mediumseagreen", 0x3cb371 }, | |
| 121 | + { "mediumslateblue", 0x7b68ee }, | |
| 122 | + { "mediumspringgreen", 0x00fa9a }, | |
| 123 | + { "mediumturquoise", 0x48d1cc }, | |
| 124 | + { "mediumvioletred", 0xc71585 }, | |
| 125 | + { "midnightblue", 0x191970 }, | |
| 126 | + { "mintcream", 0xf5fffa }, | |
| 127 | + { "mistyrose", 0xffe4e1 }, | |
| 128 | + { "moccasin", 0xffe4b5 }, | |
| 129 | + { "navajowhite", 0xffdead }, | |
| 130 | + { "navy", 0x000080 }, | |
| 131 | + { "oldlace", 0xfdf5e6 }, | |
| 132 | + { "olive", 0x808000 }, | |
| 133 | + { "olivedrab", 0x6b8e23 }, | |
| 134 | + { "orange", 0xffa500 }, | |
| 135 | + { "orangered", 0xff4500 }, | |
| 136 | + { "orchid", 0xda70d6 }, | |
| 137 | + { "palegoldenrod", 0xeee8aa }, | |
| 138 | + { "palegreen", 0x98fb98 }, | |
| 139 | + { "paleturquoise", 0xafeeee }, | |
| 140 | + { "palevioletred", 0xd87093 }, | |
| 141 | + { "papayawhip", 0xffefd5 }, | |
| 142 | + { "peachpuff", 0xffdab9 }, | |
| 143 | + { "peru", 0xcd853f }, | |
| 144 | + { "pink", 0xffc0cb }, | |
| 145 | + { "plum", 0xdda0dd }, | |
| 146 | + { "powderblue", 0xb0e0e6 }, | |
| 147 | + { "purple", 0x800080 }, | |
| 148 | + { "red", 0xff0000 }, | |
| 149 | + { "rosybrown", 0xbc8f8f }, | |
| 150 | + { "royalblue", 0x4169e1 }, | |
| 151 | + { "saddlebrown", 0x8b4513 }, | |
| 152 | + { "salmon", 0xfa8072 }, | |
| 153 | + { "sandybrown", 0xf4a460 }, | |
| 154 | + { "seagreen", 0x2e8b57 }, | |
| 155 | + { "seashell", 0xfff5ee }, | |
| 156 | + { "sienna", 0xa0522d }, | |
| 157 | + { "silver", 0xc0c0c0 }, | |
| 158 | + { "skyblue", 0x87ceeb }, | |
| 159 | + { "slateblue", 0x6a5acd }, | |
| 160 | + { "slategray", 0x708090 }, | |
| 161 | + { "snow", 0xfffafa }, | |
| 162 | + { "springgreen", 0x00ff7f }, | |
| 163 | + { "steelblue", 0x4682b4 }, | |
| 164 | + { "tan", 0xd2b48c }, | |
| 165 | + { "teal", 0x008080 }, | |
| 166 | + { "thistle", 0xd8bfd8 }, | |
| 167 | + { "tomato", 0xff6347 }, | |
| 168 | + { "turquoise", 0x40e0d0 }, | |
| 169 | + { "violet", 0xee82ee }, | |
| 170 | + { "wheat", 0xf5deb3 }, | |
| 171 | + { "white", 0xffffff }, | |
| 172 | + { "whitesmoke", 0xf5f5f5 }, | |
| 173 | + { "yellow", 0xffff00 }, | |
| 174 | + { "yellowgreen", 0x9acd32 }, | |
| 175 | +}; | |
| 176 | + | |
| 177 | +/* | |
| 178 | +** Attempt to translate a CSS color name into an integer that | |
| 179 | +** represents the equivalent RGB value. Ignore alpha if provided. | |
| 180 | +** If the name cannot be translated, return -1. | |
| 181 | +*/ | |
| 182 | +int color_name_to_rgb(const char *zName){ | |
| 183 | + if( zName==0 || zName[0]==0 ) return -1; | |
| 184 | + if( zName[0]=='#' ){ | |
| 185 | + int i, v = 0; | |
| 186 | + for(i=1; i<=6 && fossil_isxdigit(zName[i]); i++){ | |
| 187 | + v = v*16 + fossil_hexvalue(zName[i]); | |
| 188 | + } | |
| 189 | + if( i==4 ){ | |
| 190 | + v = fossil_hexvalue(zName[1])*0x110000 + | |
| 191 | + fossil_hexvalue(zName[2])*0x1100 + | |
| 192 | + fossil_hexvalue(zName[3])*0x11; | |
| 193 | + return v; | |
| 194 | + } | |
| 195 | + if( i==7 ){ | |
| 196 | + return v; | |
| 197 | + } | |
| 198 | + return -1; | |
| 199 | + }else{ | |
| 200 | + int iMin = 0; | |
| 201 | + int iMax = count(aCssColors)-1; | |
| 202 | + while( iMin<=iMax ){ | |
| 203 | + int iMid = (iMin+iMax)/2; | |
| 204 | + int c = sqlite3_stricmp(aCssColors[iMid].zName, zName); | |
| 205 | + if( c==0 ) return aCssColors[iMid].iRGB; | |
| 206 | + if( c<0 ){ | |
| 207 | + iMin = iMid+1; | |
| 208 | + }else{ | |
| 209 | + iMax = iMid-1; | |
| 210 | + } | |
| 211 | + } | |
| 212 | + return -1; | |
| 213 | + } | |
| 214 | +} | |
| 215 | + | |
| 216 | +/* | |
| 217 | +** SETTING: raw-bgcolor boolean default=off | |
| 218 | +** | |
| 219 | +** Fossil usually tries to adjust user-specified background colors | |
| 220 | +** for checkins so that the text is readable and so that the color | |
| 221 | +** is not too garish. This setting disables that filter. When | |
| 222 | +** this setting is on, the user-selected background colors are shown | |
| 223 | +** exactly as requested. | |
| 224 | +*/ | |
| 225 | + | |
| 226 | +/* | |
| 227 | +** Shift a color provided by the user so that it is suitable | |
| 228 | +** for use as a background color in the current skin. | |
| 229 | +** | |
| 230 | +** The return value is a #HHHHHH color name contained in | |
| 231 | +** static space that is overwritten on the next call. | |
| 232 | +** | |
| 233 | +** If we cannot make sense of the background color recommendation | |
| 234 | +** that is the input, then return NULL. | |
| 235 | +** | |
| 236 | +** The iFgClr parameter is normally 0. But for testing purposes, set | |
| 237 | +** it to 1 for a black foregrounds and 2 for a white foreground. | |
| 238 | +*/ | |
| 239 | +const char *reasonable_bg_color(const char *zRequested, int iFgClr){ | |
| 240 | + int iRGB = color_name_to_rgb(zRequested); | |
| 241 | + int r, g, b; /* RGB components of requested color */ | |
| 242 | + static int systemFg = 0; /* 1==black-foreground 2==white-foreground */ | |
| 243 | + int fg; /* Foreground color to actually use */ | |
| 244 | + static char zColor[10]; /* Return value */ | |
| 245 | + | |
| 246 | + if( iFgClr ){ | |
| 247 | + fg = iFgClr; | |
| 248 | + }else if( systemFg==0 ){ | |
| 249 | + if( db_get_boolean("raw-bgcolor",0) ){ | |
| 250 | + fg = systemFg = 3; | |
| 251 | + }else{ | |
| 252 | + fg = systemFg = skin_detail_boolean("white-foreground") ? 2 : 1; | |
| 253 | + } | |
| 254 | + }else{ | |
| 255 | + fg = systemFg; | |
| 256 | + } | |
| 257 | + if( fg>=3 ) return zRequested; | |
| 258 | + | |
| 259 | + if( iRGB<0 ) return 0; | |
| 260 | + r = (iRGB>>16) & 0xff; | |
| 261 | + g = (iRGB>>8) & 0xff; | |
| 262 | + b = iRGB & 0xff; | |
| 263 | + if( fg==1 ){ | |
| 264 | + /* Dark text on a light background. Adjust so that | |
| 265 | + ** no color component is less than 255-K, resulting in | |
| 266 | + ** a pastel background color. Color adjustment is quadratic | |
| 267 | + ** so that colors that are further out of range have a greater | |
| 268 | + ** adjustment. */ | |
| 269 | + const int K = 79; | |
| 270 | + int k, x, m; | |
| 271 | + m = r<g ? r : g; | |
| 272 | + if( m>b ) m = b; | |
| 273 | + k = (m*m)/255 + K; | |
| 274 | + x = 255 - k; | |
| 275 | + r = (k*r)/255 + x; | |
| 276 | + g = (k*g)/255 + x; | |
| 277 | + b = (k*b)/255 + x; | |
| 278 | + }else{ | |
| 279 | + /* Light text on a dark background. Adjust so that | |
| 280 | + ** no color component is greater than K, resulting in | |
| 281 | + ** a low-intensity, low-saturation background color. | |
| 282 | + ** The color adjustment is quadratic so that colors that | |
| 283 | + ** are further out of range have a greater adjustment. */ | |
| 284 | + const int K = 112; | |
| 285 | + int k, m; | |
| 286 | + m = r>g ? r : g; | |
| 287 | + if( m<b ) m = b; | |
| 288 | + k = 255 - (255-K)*(m*m)/65025; | |
| 289 | + r = (k*r)/255; | |
| 290 | + g = (k*g)/255; | |
| 291 | + b = (k*b)/255; | |
| 292 | + } | |
| 293 | + sqlite3_snprintf(8, zColor, "#%02x%02x%02x", r,g,b); | |
| 294 | + return zColor; | |
| 295 | +} | |
| 25 | 296 | |
| 26 | 297 | /* |
| 27 | 298 | ** Compute a hash on a branch or user name |
| 28 | 299 | */ |
| 29 | 300 | static unsigned int hash_of_name(const char *z){ |
| @@ -185,5 +456,87 @@ | ||
| 185 | 456 | @ <input type="submit" value="Submit"> |
| 186 | 457 | @ <input type="submit" name="rand" value="Random"> |
| 187 | 458 | @ </form> |
| 188 | 459 | style_finish_page(); |
| 189 | 460 | } |
| 461 | + | |
| 462 | +/* | |
| 463 | +** WEBPAGE: test-bgcolor | |
| 464 | +** | |
| 465 | +** Show how user-specified background colors will be rendered | |
| 466 | +** using the reasonable_bg_color() algorithm. | |
| 467 | +*/ | |
| 468 | +void test_bgcolor_page(void){ | |
| 469 | + const char *zReq; /* Requested color name */ | |
| 470 | + const char *zBG; /* Actual color provided */ | |
| 471 | + const char *zBg1; | |
| 472 | + char zNm[10]; | |
| 473 | + static const char *azDflt[] = { | |
| 474 | + "red", "orange", "yellow", "green", "blue", "indigo", "violet", | |
| 475 | + "tan", "brown", "gray", | |
| 476 | + }; | |
| 477 | + const int N = count(azDflt); | |
| 478 | + int i, cnt, iClr, r, g, b; | |
| 479 | + char *zFg; | |
| 480 | + login_check_credentials(); | |
| 481 | + style_set_current_feature("test"); | |
| 482 | + style_header("Background Color Test"); | |
| 483 | + for(i=cnt=0; i<N; i++){ | |
| 484 | + sqlite3_snprintf(sizeof(zNm),zNm,"b%c",'a'+i); | |
| 485 | + zReq = PD(zNm,azDflt[i]); | |
| 486 | + if( zReq==0 || zReq[0]==0 ) continue; | |
| 487 | + if( cnt==0 ){ | |
| 488 | + @ <table border="1" cellspacing="0" cellpadding="10"> | |
| 489 | + @ <tr> | |
| 490 | + @ <th>Requested Background | |
| 491 | + @ <th>Light mode | |
| 492 | + @ <th>Dark mode | |
| 493 | + @ </tr> | |
| 494 | + } | |
| 495 | + cnt++; | |
| 496 | + zBG = reasonable_bg_color(zReq, 0); | |
| 497 | + if( zBG==0 ){ | |
| 498 | + @ <tr><td colspan="3" align="center">\ | |
| 499 | + @ "%h(zReq)" is not a recognized color name</td></tr> | |
| 500 | + continue; | |
| 501 | + } | |
| 502 | + iClr = color_name_to_rgb(zReq); | |
| 503 | + r = (iClr>>16) & 0xff; | |
| 504 | + g = (iClr>>8) & 0xff; | |
| 505 | + b = iClr & 0xff; | |
| 506 | + if( 3*r + 7*g + b > 6*255 ){ | |
| 507 | + zFg = "black"; | |
| 508 | + }else{ | |
| 509 | + zFg = "white"; | |
| 510 | + } | |
| 511 | + if( zReq[0]!='#' ){ | |
| 512 | + char zReqRGB[12]; | |
| 513 | + sqlite3_snprintf(sizeof(zReqRGB),zReqRGB,"#%06x",color_name_to_rgb(zReq)); | |
| 514 | + @ <tr><td style='color:%h(zFg);background-color:%h(zReq);'>\ | |
| 515 | + @ Requested color "%h(zReq)" (%h(zReqRGB))</td> | |
| 516 | + }else{ | |
| 517 | + @ <tr><td style='color:%h(zFg);background-color:%s(zReq);'>\ | |
| 518 | + @ Requested color "%h(zReq)"</td> | |
| 519 | + } | |
| 520 | + zBg1 = reasonable_bg_color(zReq,1); | |
| 521 | + @ <td style='color:black;background-color:%h(zBg1);'>\ | |
| 522 | + @ Background color for dark text: %h(zBg1)</td> | |
| 523 | + zBg1 = reasonable_bg_color(zReq,2); | |
| 524 | + @ <td style='color:white;background-color:%h(zBg1);'>\ | |
| 525 | + @ Background color for light text: %h(zBg1)</td></tr> | |
| 526 | + } | |
| 527 | + if( cnt ){ | |
| 528 | + @ </table> | |
| 529 | + @ <hr> | |
| 530 | + } | |
| 531 | + @ <form method="POST"> | |
| 532 | + @ <p>Enter CSS color names below and see them shifted into corresponding | |
| 533 | + @ background colors above.</p> | |
| 534 | + for(i=0; i<N; i++){ | |
| 535 | + sqlite3_snprintf(sizeof(zNm),zNm,"b%c",'a'+i); | |
| 536 | + @ <input type="text" size="30" name='%s(zNm)' \ | |
| 537 | + @ value='%h(PD(zNm,azDflt[i]))'><br> | |
| 538 | + } | |
| 539 | + @ <input type="submit" value="Submit"> | |
| 540 | + @ </form> | |
| 541 | + style_finish_page(); | |
| 542 | +} | |
| 190 | 543 |
| --- src/color.c | |
| +++ src/color.c | |
| @@ -20,10 +20,281 @@ | |
| 20 | ** |
| 21 | */ |
| 22 | #include "config.h" |
| 23 | #include <string.h> |
| 24 | #include "color.h" |
| 25 | |
| 26 | /* |
| 27 | ** Compute a hash on a branch or user name |
| 28 | */ |
| 29 | static unsigned int hash_of_name(const char *z){ |
| @@ -185,5 +456,87 @@ | |
| 185 | @ <input type="submit" value="Submit"> |
| 186 | @ <input type="submit" name="rand" value="Random"> |
| 187 | @ </form> |
| 188 | style_finish_page(); |
| 189 | } |
| 190 |
| --- src/color.c | |
| +++ src/color.c | |
| @@ -20,10 +20,281 @@ | |
| 20 | ** |
| 21 | */ |
| 22 | #include "config.h" |
| 23 | #include <string.h> |
| 24 | #include "color.h" |
| 25 | |
| 26 | /* |
| 27 | ** 140 standard CSS color names and their corresponding RGB values, |
| 28 | ** in alphabetical order by name so that we can do a binary search |
| 29 | ** for lookup. |
| 30 | */ |
| 31 | static const struct CssColors { |
| 32 | const char *zName; /* CSS Color name, lower case */ |
| 33 | unsigned int iRGB; /* Corresponding RGB value */ |
| 34 | } aCssColors[] = { |
| 35 | { "aliceblue", 0xf0f8ff }, |
| 36 | { "antiquewhite", 0xfaebd7 }, |
| 37 | { "aqua", 0x00ffff }, |
| 38 | { "aquamarine", 0x7fffd4 }, |
| 39 | { "azure", 0xf0ffff }, |
| 40 | { "beige", 0xf5f5dc }, |
| 41 | { "bisque", 0xffe4c4 }, |
| 42 | { "black", 0x000000 }, |
| 43 | { "blanchedalmond", 0xffebcd }, |
| 44 | { "blue", 0x0000ff }, |
| 45 | { "blueviolet", 0x8a2be2 }, |
| 46 | { "brown", 0xa52a2a }, |
| 47 | { "burlywood", 0xdeb887 }, |
| 48 | { "cadetblue", 0x5f9ea0 }, |
| 49 | { "chartreuse", 0x7fff00 }, |
| 50 | { "chocolate", 0xd2691e }, |
| 51 | { "coral", 0xff7f50 }, |
| 52 | { "cornflowerblue", 0x6495ed }, |
| 53 | { "cornsilk", 0xfff8dc }, |
| 54 | { "crimson", 0xdc143c }, |
| 55 | { "cyan", 0x00ffff }, |
| 56 | { "darkblue", 0x00008b }, |
| 57 | { "darkcyan", 0x008b8b }, |
| 58 | { "darkgoldenrod", 0xb8860b }, |
| 59 | { "darkgray", 0xa9a9a9 }, |
| 60 | { "darkgreen", 0x006400 }, |
| 61 | { "darkkhaki", 0xbdb76b }, |
| 62 | { "darkmagenta", 0x8b008b }, |
| 63 | { "darkolivegreen", 0x556b2f }, |
| 64 | { "darkorange", 0xff8c00 }, |
| 65 | { "darkorchid", 0x9932cc }, |
| 66 | { "darkred", 0x8b0000 }, |
| 67 | { "darksalmon", 0xe9967a }, |
| 68 | { "darkseagreen", 0x8fbc8f }, |
| 69 | { "darkslateblue", 0x483d8b }, |
| 70 | { "darkslategray", 0x2f4f4f }, |
| 71 | { "darkturquoise", 0x00ced1 }, |
| 72 | { "darkviolet", 0x9400d3 }, |
| 73 | { "deeppink", 0xff1493 }, |
| 74 | { "deepskyblue", 0x00bfff }, |
| 75 | { "dimgray", 0x696969 }, |
| 76 | { "dodgerblue", 0x1e90ff }, |
| 77 | { "firebrick", 0xb22222 }, |
| 78 | { "floralwhite", 0xfffaf0 }, |
| 79 | { "forestgreen", 0x228b22 }, |
| 80 | { "fuchsia", 0xff00ff }, |
| 81 | { "gainsboro", 0xdcdcdc }, |
| 82 | { "ghostwhite", 0xf8f8ff }, |
| 83 | { "gold", 0xffd700 }, |
| 84 | { "goldenrod", 0xdaa520 }, |
| 85 | { "gray", 0x808080 }, |
| 86 | { "green", 0x008000 }, |
| 87 | { "greenyellow", 0xadff2f }, |
| 88 | { "honeydew", 0xf0fff0 }, |
| 89 | { "hotpink", 0xff69b4 }, |
| 90 | { "indianred", 0xcd5c5c }, |
| 91 | { "indigo", 0x4b0082 }, |
| 92 | { "ivory", 0xfffff0 }, |
| 93 | { "khaki", 0xf0e68c }, |
| 94 | { "lavender", 0xe6e6fa }, |
| 95 | { "lavenderblush", 0xfff0f5 }, |
| 96 | { "lawngreen", 0x7cfc00 }, |
| 97 | { "lemonchiffon", 0xfffacd }, |
| 98 | { "lightblue", 0xadd8e6 }, |
| 99 | { "lightcoral", 0xf08080 }, |
| 100 | { "lightcyan", 0xe0ffff }, |
| 101 | { "lightgoldenrodyellow", 0xfafad2 }, |
| 102 | { "lightgrey", 0xd3d3d3 }, |
| 103 | { "lightgreen", 0x90ee90 }, |
| 104 | { "lightpink", 0xffb6c1 }, |
| 105 | { "lightsalmon", 0xffa07a }, |
| 106 | { "lightseagreen", 0x20b2aa }, |
| 107 | { "lightskyblue", 0x87cefa }, |
| 108 | { "lightslategray", 0x778899 }, |
| 109 | { "lightsteelblue", 0xb0c4de }, |
| 110 | { "lightyellow", 0xffffe0 }, |
| 111 | { "lime", 0x00ff00 }, |
| 112 | { "limegreen", 0x32cd32 }, |
| 113 | { "linen", 0xfaf0e6 }, |
| 114 | { "magenta", 0xff00ff }, |
| 115 | { "maroon", 0x800000 }, |
| 116 | { "mediumaquamarine", 0x66cdaa }, |
| 117 | { "mediumblue", 0x0000cd }, |
| 118 | { "mediumorchid", 0xba55d3 }, |
| 119 | { "mediumpurple", 0x9370d8 }, |
| 120 | { "mediumseagreen", 0x3cb371 }, |
| 121 | { "mediumslateblue", 0x7b68ee }, |
| 122 | { "mediumspringgreen", 0x00fa9a }, |
| 123 | { "mediumturquoise", 0x48d1cc }, |
| 124 | { "mediumvioletred", 0xc71585 }, |
| 125 | { "midnightblue", 0x191970 }, |
| 126 | { "mintcream", 0xf5fffa }, |
| 127 | { "mistyrose", 0xffe4e1 }, |
| 128 | { "moccasin", 0xffe4b5 }, |
| 129 | { "navajowhite", 0xffdead }, |
| 130 | { "navy", 0x000080 }, |
| 131 | { "oldlace", 0xfdf5e6 }, |
| 132 | { "olive", 0x808000 }, |
| 133 | { "olivedrab", 0x6b8e23 }, |
| 134 | { "orange", 0xffa500 }, |
| 135 | { "orangered", 0xff4500 }, |
| 136 | { "orchid", 0xda70d6 }, |
| 137 | { "palegoldenrod", 0xeee8aa }, |
| 138 | { "palegreen", 0x98fb98 }, |
| 139 | { "paleturquoise", 0xafeeee }, |
| 140 | { "palevioletred", 0xd87093 }, |
| 141 | { "papayawhip", 0xffefd5 }, |
| 142 | { "peachpuff", 0xffdab9 }, |
| 143 | { "peru", 0xcd853f }, |
| 144 | { "pink", 0xffc0cb }, |
| 145 | { "plum", 0xdda0dd }, |
| 146 | { "powderblue", 0xb0e0e6 }, |
| 147 | { "purple", 0x800080 }, |
| 148 | { "red", 0xff0000 }, |
| 149 | { "rosybrown", 0xbc8f8f }, |
| 150 | { "royalblue", 0x4169e1 }, |
| 151 | { "saddlebrown", 0x8b4513 }, |
| 152 | { "salmon", 0xfa8072 }, |
| 153 | { "sandybrown", 0xf4a460 }, |
| 154 | { "seagreen", 0x2e8b57 }, |
| 155 | { "seashell", 0xfff5ee }, |
| 156 | { "sienna", 0xa0522d }, |
| 157 | { "silver", 0xc0c0c0 }, |
| 158 | { "skyblue", 0x87ceeb }, |
| 159 | { "slateblue", 0x6a5acd }, |
| 160 | { "slategray", 0x708090 }, |
| 161 | { "snow", 0xfffafa }, |
| 162 | { "springgreen", 0x00ff7f }, |
| 163 | { "steelblue", 0x4682b4 }, |
| 164 | { "tan", 0xd2b48c }, |
| 165 | { "teal", 0x008080 }, |
| 166 | { "thistle", 0xd8bfd8 }, |
| 167 | { "tomato", 0xff6347 }, |
| 168 | { "turquoise", 0x40e0d0 }, |
| 169 | { "violet", 0xee82ee }, |
| 170 | { "wheat", 0xf5deb3 }, |
| 171 | { "white", 0xffffff }, |
| 172 | { "whitesmoke", 0xf5f5f5 }, |
| 173 | { "yellow", 0xffff00 }, |
| 174 | { "yellowgreen", 0x9acd32 }, |
| 175 | }; |
| 176 | |
| 177 | /* |
| 178 | ** Attempt to translate a CSS color name into an integer that |
| 179 | ** represents the equivalent RGB value. Ignore alpha if provided. |
| 180 | ** If the name cannot be translated, return -1. |
| 181 | */ |
| 182 | int color_name_to_rgb(const char *zName){ |
| 183 | if( zName==0 || zName[0]==0 ) return -1; |
| 184 | if( zName[0]=='#' ){ |
| 185 | int i, v = 0; |
| 186 | for(i=1; i<=6 && fossil_isxdigit(zName[i]); i++){ |
| 187 | v = v*16 + fossil_hexvalue(zName[i]); |
| 188 | } |
| 189 | if( i==4 ){ |
| 190 | v = fossil_hexvalue(zName[1])*0x110000 + |
| 191 | fossil_hexvalue(zName[2])*0x1100 + |
| 192 | fossil_hexvalue(zName[3])*0x11; |
| 193 | return v; |
| 194 | } |
| 195 | if( i==7 ){ |
| 196 | return v; |
| 197 | } |
| 198 | return -1; |
| 199 | }else{ |
| 200 | int iMin = 0; |
| 201 | int iMax = count(aCssColors)-1; |
| 202 | while( iMin<=iMax ){ |
| 203 | int iMid = (iMin+iMax)/2; |
| 204 | int c = sqlite3_stricmp(aCssColors[iMid].zName, zName); |
| 205 | if( c==0 ) return aCssColors[iMid].iRGB; |
| 206 | if( c<0 ){ |
| 207 | iMin = iMid+1; |
| 208 | }else{ |
| 209 | iMax = iMid-1; |
| 210 | } |
| 211 | } |
| 212 | return -1; |
| 213 | } |
| 214 | } |
| 215 | |
| 216 | /* |
| 217 | ** SETTING: raw-bgcolor boolean default=off |
| 218 | ** |
| 219 | ** Fossil usually tries to adjust user-specified background colors |
| 220 | ** for checkins so that the text is readable and so that the color |
| 221 | ** is not too garish. This setting disables that filter. When |
| 222 | ** this setting is on, the user-selected background colors are shown |
| 223 | ** exactly as requested. |
| 224 | */ |
| 225 | |
| 226 | /* |
| 227 | ** Shift a color provided by the user so that it is suitable |
| 228 | ** for use as a background color in the current skin. |
| 229 | ** |
| 230 | ** The return value is a #HHHHHH color name contained in |
| 231 | ** static space that is overwritten on the next call. |
| 232 | ** |
| 233 | ** If we cannot make sense of the background color recommendation |
| 234 | ** that is the input, then return NULL. |
| 235 | ** |
| 236 | ** The iFgClr parameter is normally 0. But for testing purposes, set |
| 237 | ** it to 1 for a black foregrounds and 2 for a white foreground. |
| 238 | */ |
| 239 | const char *reasonable_bg_color(const char *zRequested, int iFgClr){ |
| 240 | int iRGB = color_name_to_rgb(zRequested); |
| 241 | int r, g, b; /* RGB components of requested color */ |
| 242 | static int systemFg = 0; /* 1==black-foreground 2==white-foreground */ |
| 243 | int fg; /* Foreground color to actually use */ |
| 244 | static char zColor[10]; /* Return value */ |
| 245 | |
| 246 | if( iFgClr ){ |
| 247 | fg = iFgClr; |
| 248 | }else if( systemFg==0 ){ |
| 249 | if( db_get_boolean("raw-bgcolor",0) ){ |
| 250 | fg = systemFg = 3; |
| 251 | }else{ |
| 252 | fg = systemFg = skin_detail_boolean("white-foreground") ? 2 : 1; |
| 253 | } |
| 254 | }else{ |
| 255 | fg = systemFg; |
| 256 | } |
| 257 | if( fg>=3 ) return zRequested; |
| 258 | |
| 259 | if( iRGB<0 ) return 0; |
| 260 | r = (iRGB>>16) & 0xff; |
| 261 | g = (iRGB>>8) & 0xff; |
| 262 | b = iRGB & 0xff; |
| 263 | if( fg==1 ){ |
| 264 | /* Dark text on a light background. Adjust so that |
| 265 | ** no color component is less than 255-K, resulting in |
| 266 | ** a pastel background color. Color adjustment is quadratic |
| 267 | ** so that colors that are further out of range have a greater |
| 268 | ** adjustment. */ |
| 269 | const int K = 79; |
| 270 | int k, x, m; |
| 271 | m = r<g ? r : g; |
| 272 | if( m>b ) m = b; |
| 273 | k = (m*m)/255 + K; |
| 274 | x = 255 - k; |
| 275 | r = (k*r)/255 + x; |
| 276 | g = (k*g)/255 + x; |
| 277 | b = (k*b)/255 + x; |
| 278 | }else{ |
| 279 | /* Light text on a dark background. Adjust so that |
| 280 | ** no color component is greater than K, resulting in |
| 281 | ** a low-intensity, low-saturation background color. |
| 282 | ** The color adjustment is quadratic so that colors that |
| 283 | ** are further out of range have a greater adjustment. */ |
| 284 | const int K = 112; |
| 285 | int k, m; |
| 286 | m = r>g ? r : g; |
| 287 | if( m<b ) m = b; |
| 288 | k = 255 - (255-K)*(m*m)/65025; |
| 289 | r = (k*r)/255; |
| 290 | g = (k*g)/255; |
| 291 | b = (k*b)/255; |
| 292 | } |
| 293 | sqlite3_snprintf(8, zColor, "#%02x%02x%02x", r,g,b); |
| 294 | return zColor; |
| 295 | } |
| 296 | |
| 297 | /* |
| 298 | ** Compute a hash on a branch or user name |
| 299 | */ |
| 300 | static unsigned int hash_of_name(const char *z){ |
| @@ -185,5 +456,87 @@ | |
| 456 | @ <input type="submit" value="Submit"> |
| 457 | @ <input type="submit" name="rand" value="Random"> |
| 458 | @ </form> |
| 459 | style_finish_page(); |
| 460 | } |
| 461 | |
| 462 | /* |
| 463 | ** WEBPAGE: test-bgcolor |
| 464 | ** |
| 465 | ** Show how user-specified background colors will be rendered |
| 466 | ** using the reasonable_bg_color() algorithm. |
| 467 | */ |
| 468 | void test_bgcolor_page(void){ |
| 469 | const char *zReq; /* Requested color name */ |
| 470 | const char *zBG; /* Actual color provided */ |
| 471 | const char *zBg1; |
| 472 | char zNm[10]; |
| 473 | static const char *azDflt[] = { |
| 474 | "red", "orange", "yellow", "green", "blue", "indigo", "violet", |
| 475 | "tan", "brown", "gray", |
| 476 | }; |
| 477 | const int N = count(azDflt); |
| 478 | int i, cnt, iClr, r, g, b; |
| 479 | char *zFg; |
| 480 | login_check_credentials(); |
| 481 | style_set_current_feature("test"); |
| 482 | style_header("Background Color Test"); |
| 483 | for(i=cnt=0; i<N; i++){ |
| 484 | sqlite3_snprintf(sizeof(zNm),zNm,"b%c",'a'+i); |
| 485 | zReq = PD(zNm,azDflt[i]); |
| 486 | if( zReq==0 || zReq[0]==0 ) continue; |
| 487 | if( cnt==0 ){ |
| 488 | @ <table border="1" cellspacing="0" cellpadding="10"> |
| 489 | @ <tr> |
| 490 | @ <th>Requested Background |
| 491 | @ <th>Light mode |
| 492 | @ <th>Dark mode |
| 493 | @ </tr> |
| 494 | } |
| 495 | cnt++; |
| 496 | zBG = reasonable_bg_color(zReq, 0); |
| 497 | if( zBG==0 ){ |
| 498 | @ <tr><td colspan="3" align="center">\ |
| 499 | @ "%h(zReq)" is not a recognized color name</td></tr> |
| 500 | continue; |
| 501 | } |
| 502 | iClr = color_name_to_rgb(zReq); |
| 503 | r = (iClr>>16) & 0xff; |
| 504 | g = (iClr>>8) & 0xff; |
| 505 | b = iClr & 0xff; |
| 506 | if( 3*r + 7*g + b > 6*255 ){ |
| 507 | zFg = "black"; |
| 508 | }else{ |
| 509 | zFg = "white"; |
| 510 | } |
| 511 | if( zReq[0]!='#' ){ |
| 512 | char zReqRGB[12]; |
| 513 | sqlite3_snprintf(sizeof(zReqRGB),zReqRGB,"#%06x",color_name_to_rgb(zReq)); |
| 514 | @ <tr><td style='color:%h(zFg);background-color:%h(zReq);'>\ |
| 515 | @ Requested color "%h(zReq)" (%h(zReqRGB))</td> |
| 516 | }else{ |
| 517 | @ <tr><td style='color:%h(zFg);background-color:%s(zReq);'>\ |
| 518 | @ Requested color "%h(zReq)"</td> |
| 519 | } |
| 520 | zBg1 = reasonable_bg_color(zReq,1); |
| 521 | @ <td style='color:black;background-color:%h(zBg1);'>\ |
| 522 | @ Background color for dark text: %h(zBg1)</td> |
| 523 | zBg1 = reasonable_bg_color(zReq,2); |
| 524 | @ <td style='color:white;background-color:%h(zBg1);'>\ |
| 525 | @ Background color for light text: %h(zBg1)</td></tr> |
| 526 | } |
| 527 | if( cnt ){ |
| 528 | @ </table> |
| 529 | @ <hr> |
| 530 | } |
| 531 | @ <form method="POST"> |
| 532 | @ <p>Enter CSS color names below and see them shifted into corresponding |
| 533 | @ background colors above.</p> |
| 534 | for(i=0; i<N; i++){ |
| 535 | sqlite3_snprintf(sizeof(zNm),zNm,"b%c",'a'+i); |
| 536 | @ <input type="text" size="30" name='%s(zNm)' \ |
| 537 | @ value='%h(PD(zNm,azDflt[i]))'><br> |
| 538 | } |
| 539 | @ <input type="submit" value="Submit"> |
| 540 | @ </form> |
| 541 | style_finish_page(); |
| 542 | } |
| 543 |
M
src/db.c
+1
-1
| --- src/db.c | ||
| +++ src/db.c | ||
| @@ -3369,11 +3369,11 @@ | ||
| 3369 | 3369 | if( zProjectName ) fossil_print("project-name: %s\n", zProjectName); |
| 3370 | 3370 | if( zProjectDesc ) fossil_print("project-description: %s\n", zProjectDesc); |
| 3371 | 3371 | fossil_print("project-id: %s\n", db_get("project-code", 0)); |
| 3372 | 3372 | fossil_print("server-id: %s\n", db_get("server-code", 0)); |
| 3373 | 3373 | zPassword = db_text(0, "SELECT pw FROM user WHERE login=%Q", g.zLogin); |
| 3374 | - fossil_print("admin-user: %s (initial password is \"%s\")\n", | |
| 3374 | + fossil_print("admin-user: %s (initial remote-access password is \"%s\")\n", | |
| 3375 | 3375 | g.zLogin, zPassword); |
| 3376 | 3376 | hash_user_password(g.zLogin); |
| 3377 | 3377 | } |
| 3378 | 3378 | |
| 3379 | 3379 | /* |
| 3380 | 3380 |
| --- src/db.c | |
| +++ src/db.c | |
| @@ -3369,11 +3369,11 @@ | |
| 3369 | if( zProjectName ) fossil_print("project-name: %s\n", zProjectName); |
| 3370 | if( zProjectDesc ) fossil_print("project-description: %s\n", zProjectDesc); |
| 3371 | fossil_print("project-id: %s\n", db_get("project-code", 0)); |
| 3372 | fossil_print("server-id: %s\n", db_get("server-code", 0)); |
| 3373 | zPassword = db_text(0, "SELECT pw FROM user WHERE login=%Q", g.zLogin); |
| 3374 | fossil_print("admin-user: %s (initial password is \"%s\")\n", |
| 3375 | g.zLogin, zPassword); |
| 3376 | hash_user_password(g.zLogin); |
| 3377 | } |
| 3378 | |
| 3379 | /* |
| 3380 |
| --- src/db.c | |
| +++ src/db.c | |
| @@ -3369,11 +3369,11 @@ | |
| 3369 | if( zProjectName ) fossil_print("project-name: %s\n", zProjectName); |
| 3370 | if( zProjectDesc ) fossil_print("project-description: %s\n", zProjectDesc); |
| 3371 | fossil_print("project-id: %s\n", db_get("project-code", 0)); |
| 3372 | fossil_print("server-id: %s\n", db_get("server-code", 0)); |
| 3373 | zPassword = db_text(0, "SELECT pw FROM user WHERE login=%Q", g.zLogin); |
| 3374 | fossil_print("admin-user: %s (initial remote-access password is \"%s\")\n", |
| 3375 | g.zLogin, zPassword); |
| 3376 | hash_user_password(g.zLogin); |
| 3377 | } |
| 3378 | |
| 3379 | /* |
| 3380 |
+1
| --- src/default.css | ||
| +++ src/default.css | ||
| @@ -751,10 +751,11 @@ | ||
| 751 | 751 | border-left: 1px solid gold; |
| 752 | 752 | } |
| 753 | 753 | body.cpage-ckout .file-change-line, |
| 754 | 754 | body.cpage-info .file-change-line, |
| 755 | 755 | body.cpage-vinfo .file-change-line, |
| 756 | +body.cpage-ci .file-change-line, | |
| 756 | 757 | body.cpage-vdiff .file-change-line { |
| 757 | 758 | margin-top: 16px; |
| 758 | 759 | margin-bottom: 16px; |
| 759 | 760 | margin-right: 1em /* keep it from nudging right up against the scrollbar-reveal zone */; |
| 760 | 761 | display: flex; |
| 761 | 762 |
| --- src/default.css | |
| +++ src/default.css | |
| @@ -751,10 +751,11 @@ | |
| 751 | border-left: 1px solid gold; |
| 752 | } |
| 753 | body.cpage-ckout .file-change-line, |
| 754 | body.cpage-info .file-change-line, |
| 755 | body.cpage-vinfo .file-change-line, |
| 756 | body.cpage-vdiff .file-change-line { |
| 757 | margin-top: 16px; |
| 758 | margin-bottom: 16px; |
| 759 | margin-right: 1em /* keep it from nudging right up against the scrollbar-reveal zone */; |
| 760 | display: flex; |
| 761 |
| --- src/default.css | |
| +++ src/default.css | |
| @@ -751,10 +751,11 @@ | |
| 751 | border-left: 1px solid gold; |
| 752 | } |
| 753 | body.cpage-ckout .file-change-line, |
| 754 | body.cpage-info .file-change-line, |
| 755 | body.cpage-vinfo .file-change-line, |
| 756 | body.cpage-ci .file-change-line, |
| 757 | body.cpage-vdiff .file-change-line { |
| 758 | margin-top: 16px; |
| 759 | margin-bottom: 16px; |
| 760 | margin-right: 1em /* keep it from nudging right up against the scrollbar-reveal zone */; |
| 761 | display: flex; |
| 762 |
+1
-1
| --- src/doc.c | ||
| +++ src/doc.c | ||
| @@ -1052,11 +1052,11 @@ | ||
| 1052 | 1052 | */ |
| 1053 | 1053 | zMime = nMiss==0 ? P("mimetype") : 0; |
| 1054 | 1054 | if( zMime==0 ){ |
| 1055 | 1055 | zMime = mimetype_from_name(zName); |
| 1056 | 1056 | } |
| 1057 | - Th_Store("doc_name", zName); | |
| 1057 | + Th_StoreUnsafe("doc_name", zName); | |
| 1058 | 1058 | if( vid ){ |
| 1059 | 1059 | Th_Store("doc_version", db_text(0, "SELECT '[' || substr(uuid,1,10) || ']'" |
| 1060 | 1060 | " FROM blob WHERE rid=%d", vid)); |
| 1061 | 1061 | Th_Store("doc_date", db_text(0, "SELECT datetime(mtime) FROM event" |
| 1062 | 1062 | " WHERE objid=%d AND type='ci'", vid)); |
| 1063 | 1063 |
| --- src/doc.c | |
| +++ src/doc.c | |
| @@ -1052,11 +1052,11 @@ | |
| 1052 | */ |
| 1053 | zMime = nMiss==0 ? P("mimetype") : 0; |
| 1054 | if( zMime==0 ){ |
| 1055 | zMime = mimetype_from_name(zName); |
| 1056 | } |
| 1057 | Th_Store("doc_name", zName); |
| 1058 | if( vid ){ |
| 1059 | Th_Store("doc_version", db_text(0, "SELECT '[' || substr(uuid,1,10) || ']'" |
| 1060 | " FROM blob WHERE rid=%d", vid)); |
| 1061 | Th_Store("doc_date", db_text(0, "SELECT datetime(mtime) FROM event" |
| 1062 | " WHERE objid=%d AND type='ci'", vid)); |
| 1063 |
| --- src/doc.c | |
| +++ src/doc.c | |
| @@ -1052,11 +1052,11 @@ | |
| 1052 | */ |
| 1053 | zMime = nMiss==0 ? P("mimetype") : 0; |
| 1054 | if( zMime==0 ){ |
| 1055 | zMime = mimetype_from_name(zName); |
| 1056 | } |
| 1057 | Th_StoreUnsafe("doc_name", zName); |
| 1058 | if( vid ){ |
| 1059 | Th_Store("doc_version", db_text(0, "SELECT '[' || substr(uuid,1,10) || ']'" |
| 1060 | " FROM blob WHERE rid=%d", vid)); |
| 1061 | Th_Store("doc_date", db_text(0, "SELECT datetime(mtime) FROM event" |
| 1062 | " WHERE objid=%d AND type='ci'", vid)); |
| 1063 |
+75
-3
| --- src/encode.c | ||
| +++ src/encode.c | ||
| @@ -142,10 +142,82 @@ | ||
| 142 | 142 | } |
| 143 | 143 | } |
| 144 | 144 | if( j<i ) blob_append(p, zIn+j, i-j); |
| 145 | 145 | } |
| 146 | 146 | |
| 147 | +/* | |
| 148 | +** Make the given string safe for HTML by converting syntax characters | |
| 149 | +** into alternatives that do not have the special syntactic meaning. | |
| 150 | +** | |
| 151 | +** < --> U+227a | |
| 152 | +** > --> U+227b | |
| 153 | +** & --> + | |
| 154 | +** " --> U+201d | |
| 155 | +** ' --> U+2019 | |
| 156 | +** | |
| 157 | +** Return a pointer to a new string obtained from fossil_malloc(). | |
| 158 | +*/ | |
| 159 | +char *html_lookalike(const char *z, int n){ | |
| 160 | + unsigned char c; | |
| 161 | + int i = 0; | |
| 162 | + int count = 0; | |
| 163 | + unsigned char *zOut; | |
| 164 | + const unsigned char *zIn = (const unsigned char*)z; | |
| 165 | + | |
| 166 | + if( n<0 ) n = strlen(z); | |
| 167 | + while( i<n ){ | |
| 168 | + switch( zIn[i] ){ | |
| 169 | + case '<': count += 3; break; | |
| 170 | + case '>': count += 3; break; | |
| 171 | + case '"': count += 3; break; | |
| 172 | + case '\'': count += 3; break; | |
| 173 | + case 0: n = i; break; | |
| 174 | + } | |
| 175 | + i++; | |
| 176 | + } | |
| 177 | + i = 0; | |
| 178 | + zOut = fossil_malloc( count+n+1 ); | |
| 179 | + if( count==0 ){ | |
| 180 | + memcpy(zOut, zIn, n); | |
| 181 | + zOut[n] = 0; | |
| 182 | + return (char*)zOut; | |
| 183 | + } | |
| 184 | + while( n-->0 ){ | |
| 185 | + c = *(zIn++); | |
| 186 | + switch( c ){ | |
| 187 | + case '<': | |
| 188 | + zOut[i++] = 0xe2; | |
| 189 | + zOut[i++] = 0x89; | |
| 190 | + zOut[i++] = 0xba; | |
| 191 | + break; | |
| 192 | + case '>': | |
| 193 | + zOut[i++] = 0xe2; | |
| 194 | + zOut[i++] = 0x89; | |
| 195 | + zOut[i++] = 0xbb; | |
| 196 | + break; | |
| 197 | + case '&': | |
| 198 | + zOut[i++] = '+'; | |
| 199 | + break; | |
| 200 | + case '"': | |
| 201 | + zOut[i++] = 0xe2; | |
| 202 | + zOut[i++] = 0x80; | |
| 203 | + zOut[i++] = 0x9d; | |
| 204 | + break; | |
| 205 | + case '\'': | |
| 206 | + zOut[i++] = 0xe2; | |
| 207 | + zOut[i++] = 0x80; | |
| 208 | + zOut[i++] = 0x99; | |
| 209 | + break; | |
| 210 | + default: | |
| 211 | + zOut[i++] = c; | |
| 212 | + break; | |
| 213 | + } | |
| 214 | + } | |
| 215 | + zOut[i] = 0; | |
| 216 | + return (char*)zOut; | |
| 217 | +} | |
| 218 | + | |
| 147 | 219 | |
| 148 | 220 | /* |
| 149 | 221 | ** Encode a string for HTTP. This means converting lots of |
| 150 | 222 | ** characters into the "%HH" where H is a hex digit. It also |
| 151 | 223 | ** means converting spaces to "+". |
| @@ -244,11 +316,11 @@ | ||
| 244 | 316 | } |
| 245 | 317 | |
| 246 | 318 | /* |
| 247 | 319 | ** Convert a single HEX digit to an integer |
| 248 | 320 | */ |
| 249 | -static int AsciiToHex(int c){ | |
| 321 | +int fossil_hexvalue(int c){ | |
| 250 | 322 | if( c>='a' && c<='f' ){ |
| 251 | 323 | c += 10 - 'a'; |
| 252 | 324 | }else if( c>='A' && c<='F' ){ |
| 253 | 325 | c += 10 - 'A'; |
| 254 | 326 | }else if( c>='0' && c<='9' ){ |
| @@ -272,12 +344,12 @@ | ||
| 272 | 344 | i = j = 0; |
| 273 | 345 | while( z[i] ){ |
| 274 | 346 | switch( z[i] ){ |
| 275 | 347 | case '%': |
| 276 | 348 | if( z[i+1] && z[i+2] ){ |
| 277 | - z[j] = AsciiToHex(z[i+1]) << 4; | |
| 278 | - z[j] |= AsciiToHex(z[i+2]); | |
| 349 | + z[j] = fossil_hexvalue(z[i+1]) << 4; | |
| 350 | + z[j] |= fossil_hexvalue(z[i+2]); | |
| 279 | 351 | i += 2; |
| 280 | 352 | } |
| 281 | 353 | break; |
| 282 | 354 | case '+': |
| 283 | 355 | z[j] = ' '; |
| 284 | 356 |
| --- src/encode.c | |
| +++ src/encode.c | |
| @@ -142,10 +142,82 @@ | |
| 142 | } |
| 143 | } |
| 144 | if( j<i ) blob_append(p, zIn+j, i-j); |
| 145 | } |
| 146 | |
| 147 | |
| 148 | /* |
| 149 | ** Encode a string for HTTP. This means converting lots of |
| 150 | ** characters into the "%HH" where H is a hex digit. It also |
| 151 | ** means converting spaces to "+". |
| @@ -244,11 +316,11 @@ | |
| 244 | } |
| 245 | |
| 246 | /* |
| 247 | ** Convert a single HEX digit to an integer |
| 248 | */ |
| 249 | static int AsciiToHex(int c){ |
| 250 | if( c>='a' && c<='f' ){ |
| 251 | c += 10 - 'a'; |
| 252 | }else if( c>='A' && c<='F' ){ |
| 253 | c += 10 - 'A'; |
| 254 | }else if( c>='0' && c<='9' ){ |
| @@ -272,12 +344,12 @@ | |
| 272 | i = j = 0; |
| 273 | while( z[i] ){ |
| 274 | switch( z[i] ){ |
| 275 | case '%': |
| 276 | if( z[i+1] && z[i+2] ){ |
| 277 | z[j] = AsciiToHex(z[i+1]) << 4; |
| 278 | z[j] |= AsciiToHex(z[i+2]); |
| 279 | i += 2; |
| 280 | } |
| 281 | break; |
| 282 | case '+': |
| 283 | z[j] = ' '; |
| 284 |
| --- src/encode.c | |
| +++ src/encode.c | |
| @@ -142,10 +142,82 @@ | |
| 142 | } |
| 143 | } |
| 144 | if( j<i ) blob_append(p, zIn+j, i-j); |
| 145 | } |
| 146 | |
| 147 | /* |
| 148 | ** Make the given string safe for HTML by converting syntax characters |
| 149 | ** into alternatives that do not have the special syntactic meaning. |
| 150 | ** |
| 151 | ** < --> U+227a |
| 152 | ** > --> U+227b |
| 153 | ** & --> + |
| 154 | ** " --> U+201d |
| 155 | ** ' --> U+2019 |
| 156 | ** |
| 157 | ** Return a pointer to a new string obtained from fossil_malloc(). |
| 158 | */ |
| 159 | char *html_lookalike(const char *z, int n){ |
| 160 | unsigned char c; |
| 161 | int i = 0; |
| 162 | int count = 0; |
| 163 | unsigned char *zOut; |
| 164 | const unsigned char *zIn = (const unsigned char*)z; |
| 165 | |
| 166 | if( n<0 ) n = strlen(z); |
| 167 | while( i<n ){ |
| 168 | switch( zIn[i] ){ |
| 169 | case '<': count += 3; break; |
| 170 | case '>': count += 3; break; |
| 171 | case '"': count += 3; break; |
| 172 | case '\'': count += 3; break; |
| 173 | case 0: n = i; break; |
| 174 | } |
| 175 | i++; |
| 176 | } |
| 177 | i = 0; |
| 178 | zOut = fossil_malloc( count+n+1 ); |
| 179 | if( count==0 ){ |
| 180 | memcpy(zOut, zIn, n); |
| 181 | zOut[n] = 0; |
| 182 | return (char*)zOut; |
| 183 | } |
| 184 | while( n-->0 ){ |
| 185 | c = *(zIn++); |
| 186 | switch( c ){ |
| 187 | case '<': |
| 188 | zOut[i++] = 0xe2; |
| 189 | zOut[i++] = 0x89; |
| 190 | zOut[i++] = 0xba; |
| 191 | break; |
| 192 | case '>': |
| 193 | zOut[i++] = 0xe2; |
| 194 | zOut[i++] = 0x89; |
| 195 | zOut[i++] = 0xbb; |
| 196 | break; |
| 197 | case '&': |
| 198 | zOut[i++] = '+'; |
| 199 | break; |
| 200 | case '"': |
| 201 | zOut[i++] = 0xe2; |
| 202 | zOut[i++] = 0x80; |
| 203 | zOut[i++] = 0x9d; |
| 204 | break; |
| 205 | case '\'': |
| 206 | zOut[i++] = 0xe2; |
| 207 | zOut[i++] = 0x80; |
| 208 | zOut[i++] = 0x99; |
| 209 | break; |
| 210 | default: |
| 211 | zOut[i++] = c; |
| 212 | break; |
| 213 | } |
| 214 | } |
| 215 | zOut[i] = 0; |
| 216 | return (char*)zOut; |
| 217 | } |
| 218 | |
| 219 | |
| 220 | /* |
| 221 | ** Encode a string for HTTP. This means converting lots of |
| 222 | ** characters into the "%HH" where H is a hex digit. It also |
| 223 | ** means converting spaces to "+". |
| @@ -244,11 +316,11 @@ | |
| 316 | } |
| 317 | |
| 318 | /* |
| 319 | ** Convert a single HEX digit to an integer |
| 320 | */ |
| 321 | int fossil_hexvalue(int c){ |
| 322 | if( c>='a' && c<='f' ){ |
| 323 | c += 10 - 'a'; |
| 324 | }else if( c>='A' && c<='F' ){ |
| 325 | c += 10 - 'A'; |
| 326 | }else if( c>='0' && c<='9' ){ |
| @@ -272,12 +344,12 @@ | |
| 344 | i = j = 0; |
| 345 | while( z[i] ){ |
| 346 | switch( z[i] ){ |
| 347 | case '%': |
| 348 | if( z[i+1] && z[i+2] ){ |
| 349 | z[j] = fossil_hexvalue(z[i+1]) << 4; |
| 350 | z[j] |= fossil_hexvalue(z[i+2]); |
| 351 | i += 2; |
| 352 | } |
| 353 | break; |
| 354 | case '+': |
| 355 | z[j] = ' '; |
| 356 |
+2
| --- src/finfo.c | ||
| +++ src/finfo.c | ||
| @@ -636,10 +636,12 @@ | ||
| 636 | 636 | if( zBr==0 ) zBr = "trunk"; |
| 637 | 637 | if( uBg ){ |
| 638 | 638 | zBgClr = user_color(zUser); |
| 639 | 639 | }else if( brBg || zBgClr==0 || zBgClr[0]==0 ){ |
| 640 | 640 | zBgClr = strcmp(zBr,"trunk")==0 ? "" : hash_color(zBr); |
| 641 | + }else if( zBgClr ){ | |
| 642 | + zBgClr = reasonable_bg_color(zBgClr,0); | |
| 641 | 643 | } |
| 642 | 644 | gidx = graph_add_row(pGraph, |
| 643 | 645 | frid>0 ? (GraphRowId)frid*(mxfnid+1)+fnid : fpid+1000000000, |
| 644 | 646 | nParent, 0, aParent, zBr, zBgClr, |
| 645 | 647 | zUuid, 0); |
| 646 | 648 |
| --- src/finfo.c | |
| +++ src/finfo.c | |
| @@ -636,10 +636,12 @@ | |
| 636 | if( zBr==0 ) zBr = "trunk"; |
| 637 | if( uBg ){ |
| 638 | zBgClr = user_color(zUser); |
| 639 | }else if( brBg || zBgClr==0 || zBgClr[0]==0 ){ |
| 640 | zBgClr = strcmp(zBr,"trunk")==0 ? "" : hash_color(zBr); |
| 641 | } |
| 642 | gidx = graph_add_row(pGraph, |
| 643 | frid>0 ? (GraphRowId)frid*(mxfnid+1)+fnid : fpid+1000000000, |
| 644 | nParent, 0, aParent, zBr, zBgClr, |
| 645 | zUuid, 0); |
| 646 |
| --- src/finfo.c | |
| +++ src/finfo.c | |
| @@ -636,10 +636,12 @@ | |
| 636 | if( zBr==0 ) zBr = "trunk"; |
| 637 | if( uBg ){ |
| 638 | zBgClr = user_color(zUser); |
| 639 | }else if( brBg || zBgClr==0 || zBgClr[0]==0 ){ |
| 640 | zBgClr = strcmp(zBr,"trunk")==0 ? "" : hash_color(zBr); |
| 641 | }else if( zBgClr ){ |
| 642 | zBgClr = reasonable_bg_color(zBgClr,0); |
| 643 | } |
| 644 | gidx = graph_add_row(pGraph, |
| 645 | frid>0 ? (GraphRowId)frid*(mxfnid+1)+fnid : fpid+1000000000, |
| 646 | nParent, 0, aParent, zBr, zBgClr, |
| 647 | zUuid, 0); |
| 648 |
+32
-5
| --- src/forum.c | ||
| +++ src/forum.c | ||
| @@ -59,10 +59,11 @@ | ||
| 59 | 59 | ForumPost *pFirst; /* First post in chronological order */ |
| 60 | 60 | ForumPost *pLast; /* Last post in chronological order */ |
| 61 | 61 | ForumPost *pDisplay; /* Entries in display order */ |
| 62 | 62 | ForumPost *pTail; /* Last on the display list */ |
| 63 | 63 | int mxIndent; /* Maximum indentation level */ |
| 64 | + int nArtifact; /* Number of forum artifacts in this thread */ | |
| 64 | 65 | }; |
| 65 | 66 | #endif /* INTERFACE */ |
| 66 | 67 | |
| 67 | 68 | /* |
| 68 | 69 | ** Return true if the forum post with the given rid has been |
| @@ -109,12 +110,17 @@ | ||
| 109 | 110 | ** the post. |
| 110 | 111 | ** |
| 111 | 112 | ** If bCheckIrt is true then p's thread in-response-to parents are |
| 112 | 113 | ** checked (recursively) for closure, else only p is checked. |
| 113 | 114 | */ |
| 114 | -static int forumpost_is_closed(ForumPost *p, int bCheckIrt){ | |
| 115 | - while(p){ | |
| 115 | +static int forumpost_is_closed( | |
| 116 | + ForumThread *pThread, /* Thread that the post is a member of */ | |
| 117 | + ForumPost *p, /* the forum post */ | |
| 118 | + int bCheckIrt /* True to check In-Reply-To posts */ | |
| 119 | +){ | |
| 120 | + int mx = pThread->nArtifact+1; | |
| 121 | + while( p && (mx--)>0 ){ | |
| 116 | 122 | if( p->pEditHead ) p = p->pEditHead; |
| 117 | 123 | if( p->iClosed || !bCheckIrt ) return p->iClosed; |
| 118 | 124 | p = p->pIrt; |
| 119 | 125 | } |
| 120 | 126 | return 0; |
| @@ -409,10 +415,11 @@ | ||
| 409 | 415 | pThread->pFirst = pPost; |
| 410 | 416 | }else{ |
| 411 | 417 | pThread->pLast->pNext = pPost; |
| 412 | 418 | } |
| 413 | 419 | pThread->pLast = pPost; |
| 420 | + pThread->nArtifact++; | |
| 414 | 421 | |
| 415 | 422 | /* Find the in-reply-to post. Default to the topic post if the replied-to |
| 416 | 423 | ** post cannot be found. */ |
| 417 | 424 | if( firt ){ |
| 418 | 425 | pPost->pIrt = pThread->pFirst; |
| @@ -520,10 +527,11 @@ | ||
| 520 | 527 | fossil_fatal("Not a forum post: \"%s\"", zName); |
| 521 | 528 | } |
| 522 | 529 | fossil_print("fpid = %d\n", fpid); |
| 523 | 530 | fossil_print("froot = %d\n", froot); |
| 524 | 531 | pThread = forumthread_create(froot, 1); |
| 532 | + fossil_print("count = %d\n", pThread->nArtifact); | |
| 525 | 533 | fossil_print("Chronological:\n"); |
| 526 | 534 | fossil_print( |
| 527 | 535 | /* 0 1 2 3 4 5 6 7 */ |
| 528 | 536 | /* 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123 */ |
| 529 | 537 | " sid rev closed fpid pIrt pEditPrev pEditTail hash\n"); |
| @@ -565,10 +573,11 @@ | ||
| 565 | 573 | int froot; |
| 566 | 574 | const char *zName = P("name"); |
| 567 | 575 | ForumThread *pThread; |
| 568 | 576 | ForumPost *p; |
| 569 | 577 | char *fuuid; |
| 578 | + Stmt q; | |
| 570 | 579 | |
| 571 | 580 | login_check_credentials(); |
| 572 | 581 | if( !g.perm.Admin ){ |
| 573 | 582 | return; |
| 574 | 583 | } |
| @@ -599,10 +608,27 @@ | ||
| 599 | 608 | for(p=pThread->pFirst; p; p=p->pNext){ |
| 600 | 609 | @ %h(p->zUuid) |
| 601 | 610 | } |
| 602 | 611 | forumthread_delete(pThread); |
| 603 | 612 | @ </pre> |
| 613 | + @ <hr> | |
| 614 | + @ <h2>Related FORUMPOST Table Content</h2> | |
| 615 | + @ <table border="1" cellpadding="4" cellspacing="0"> | |
| 616 | + @ <tr><th>fpid<th>froot<th>fprev<th>firt<th>fmtime | |
| 617 | + db_prepare(&q, "SELECT fpid, froot, fprev, firt, datetime(fmtime)" | |
| 618 | + " FROM forumpost" | |
| 619 | + " WHERE froot=%d" | |
| 620 | + " ORDER BY fmtime", froot); | |
| 621 | + while( db_step(&q)==SQLITE_ROW ){ | |
| 622 | + @ <tr><td>%d(db_column_int(&q,0))\ | |
| 623 | + @ <td>%d(db_column_int(&q,1))\ | |
| 624 | + @ <td>%d(db_column_int(&q,2))\ | |
| 625 | + @ <td>%d(db_column_int(&q,3))\ | |
| 626 | + @ <td>%h(db_column_text(&q,4))</tr> | |
| 627 | + } | |
| 628 | + @ </table> | |
| 629 | + db_finalize(&q); | |
| 604 | 630 | style_finish_page(); |
| 605 | 631 | } |
| 606 | 632 | |
| 607 | 633 | /* |
| 608 | 634 | ** Render a forum post for display |
| @@ -725,10 +751,11 @@ | ||
| 725 | 751 | |
| 726 | 752 | /* |
| 727 | 753 | ** Display a single post in a forum thread. |
| 728 | 754 | */ |
| 729 | 755 | static void forum_display_post( |
| 756 | + ForumThread *pThread, /* The thread that this post is a member of */ | |
| 730 | 757 | ForumPost *p, /* Forum post to display */ |
| 731 | 758 | int iIndentScale, /* Indent scale factor */ |
| 732 | 759 | int bRaw, /* True to omit the border */ |
| 733 | 760 | int bUnf, /* True to leave the post unformatted */ |
| 734 | 761 | int bHist, /* True if showing edit history */ |
| @@ -747,14 +774,14 @@ | ||
| 747 | 774 | const char *zMimetype;/* Formatting MIME type */ |
| 748 | 775 | |
| 749 | 776 | /* Get the manifest for the post. Abort if not found (e.g. shunned). */ |
| 750 | 777 | pManifest = manifest_get(p->fpid, CFTYPE_FORUM, 0); |
| 751 | 778 | if( !pManifest ) return; |
| 752 | - iClosed = forumpost_is_closed(p, 1); | |
| 779 | + iClosed = forumpost_is_closed(pThread, p, 1); | |
| 753 | 780 | /* When not in raw mode, create the border around the post. */ |
| 754 | 781 | if( !bRaw ){ |
| 755 | - /* Open the <div> enclosing the post. Set the class string to mark the post | |
| 782 | + /* Open the <div> enclosing the post. Set the class string to mark the post | |
| 756 | 783 | ** as selected and/or obsolete. */ |
| 757 | 784 | iIndent = (p->pEditHead ? p->pEditHead->nIndent : p->nIndent)-1; |
| 758 | 785 | @ <div id='forum%d(p->fpid)' class='forumTime\ |
| 759 | 786 | @ %s(bSelect ? " forumSel" : "")\ |
| 760 | 787 | @ %s(iClosed ? " forumClosed" : "")\ |
| @@ -1027,11 +1054,11 @@ | ||
| 1027 | 1054 | } |
| 1028 | 1055 | |
| 1029 | 1056 | /* Display the appropriate subset of posts in sequence. */ |
| 1030 | 1057 | while( p ){ |
| 1031 | 1058 | /* Display the post. */ |
| 1032 | - forum_display_post(p, iIndentScale, mode==FD_RAW, | |
| 1059 | + forum_display_post(pThread, p, iIndentScale, mode==FD_RAW, | |
| 1033 | 1060 | bUnf, bHist, p==pSelect, zQuery); |
| 1034 | 1061 | |
| 1035 | 1062 | /* Advance to the next post in the thread. */ |
| 1036 | 1063 | if( mode==FD_CHRONO ){ |
| 1037 | 1064 | /* Chronological mode: display posts (optionally including edits) in their |
| 1038 | 1065 |
| --- src/forum.c | |
| +++ src/forum.c | |
| @@ -59,10 +59,11 @@ | |
| 59 | ForumPost *pFirst; /* First post in chronological order */ |
| 60 | ForumPost *pLast; /* Last post in chronological order */ |
| 61 | ForumPost *pDisplay; /* Entries in display order */ |
| 62 | ForumPost *pTail; /* Last on the display list */ |
| 63 | int mxIndent; /* Maximum indentation level */ |
| 64 | }; |
| 65 | #endif /* INTERFACE */ |
| 66 | |
| 67 | /* |
| 68 | ** Return true if the forum post with the given rid has been |
| @@ -109,12 +110,17 @@ | |
| 109 | ** the post. |
| 110 | ** |
| 111 | ** If bCheckIrt is true then p's thread in-response-to parents are |
| 112 | ** checked (recursively) for closure, else only p is checked. |
| 113 | */ |
| 114 | static int forumpost_is_closed(ForumPost *p, int bCheckIrt){ |
| 115 | while(p){ |
| 116 | if( p->pEditHead ) p = p->pEditHead; |
| 117 | if( p->iClosed || !bCheckIrt ) return p->iClosed; |
| 118 | p = p->pIrt; |
| 119 | } |
| 120 | return 0; |
| @@ -409,10 +415,11 @@ | |
| 409 | pThread->pFirst = pPost; |
| 410 | }else{ |
| 411 | pThread->pLast->pNext = pPost; |
| 412 | } |
| 413 | pThread->pLast = pPost; |
| 414 | |
| 415 | /* Find the in-reply-to post. Default to the topic post if the replied-to |
| 416 | ** post cannot be found. */ |
| 417 | if( firt ){ |
| 418 | pPost->pIrt = pThread->pFirst; |
| @@ -520,10 +527,11 @@ | |
| 520 | fossil_fatal("Not a forum post: \"%s\"", zName); |
| 521 | } |
| 522 | fossil_print("fpid = %d\n", fpid); |
| 523 | fossil_print("froot = %d\n", froot); |
| 524 | pThread = forumthread_create(froot, 1); |
| 525 | fossil_print("Chronological:\n"); |
| 526 | fossil_print( |
| 527 | /* 0 1 2 3 4 5 6 7 */ |
| 528 | /* 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123 */ |
| 529 | " sid rev closed fpid pIrt pEditPrev pEditTail hash\n"); |
| @@ -565,10 +573,11 @@ | |
| 565 | int froot; |
| 566 | const char *zName = P("name"); |
| 567 | ForumThread *pThread; |
| 568 | ForumPost *p; |
| 569 | char *fuuid; |
| 570 | |
| 571 | login_check_credentials(); |
| 572 | if( !g.perm.Admin ){ |
| 573 | return; |
| 574 | } |
| @@ -599,10 +608,27 @@ | |
| 599 | for(p=pThread->pFirst; p; p=p->pNext){ |
| 600 | @ %h(p->zUuid) |
| 601 | } |
| 602 | forumthread_delete(pThread); |
| 603 | @ </pre> |
| 604 | style_finish_page(); |
| 605 | } |
| 606 | |
| 607 | /* |
| 608 | ** Render a forum post for display |
| @@ -725,10 +751,11 @@ | |
| 725 | |
| 726 | /* |
| 727 | ** Display a single post in a forum thread. |
| 728 | */ |
| 729 | static void forum_display_post( |
| 730 | ForumPost *p, /* Forum post to display */ |
| 731 | int iIndentScale, /* Indent scale factor */ |
| 732 | int bRaw, /* True to omit the border */ |
| 733 | int bUnf, /* True to leave the post unformatted */ |
| 734 | int bHist, /* True if showing edit history */ |
| @@ -747,14 +774,14 @@ | |
| 747 | const char *zMimetype;/* Formatting MIME type */ |
| 748 | |
| 749 | /* Get the manifest for the post. Abort if not found (e.g. shunned). */ |
| 750 | pManifest = manifest_get(p->fpid, CFTYPE_FORUM, 0); |
| 751 | if( !pManifest ) return; |
| 752 | iClosed = forumpost_is_closed(p, 1); |
| 753 | /* When not in raw mode, create the border around the post. */ |
| 754 | if( !bRaw ){ |
| 755 | /* Open the <div> enclosing the post. Set the class string to mark the post |
| 756 | ** as selected and/or obsolete. */ |
| 757 | iIndent = (p->pEditHead ? p->pEditHead->nIndent : p->nIndent)-1; |
| 758 | @ <div id='forum%d(p->fpid)' class='forumTime\ |
| 759 | @ %s(bSelect ? " forumSel" : "")\ |
| 760 | @ %s(iClosed ? " forumClosed" : "")\ |
| @@ -1027,11 +1054,11 @@ | |
| 1027 | } |
| 1028 | |
| 1029 | /* Display the appropriate subset of posts in sequence. */ |
| 1030 | while( p ){ |
| 1031 | /* Display the post. */ |
| 1032 | forum_display_post(p, iIndentScale, mode==FD_RAW, |
| 1033 | bUnf, bHist, p==pSelect, zQuery); |
| 1034 | |
| 1035 | /* Advance to the next post in the thread. */ |
| 1036 | if( mode==FD_CHRONO ){ |
| 1037 | /* Chronological mode: display posts (optionally including edits) in their |
| 1038 |
| --- src/forum.c | |
| +++ src/forum.c | |
| @@ -59,10 +59,11 @@ | |
| 59 | ForumPost *pFirst; /* First post in chronological order */ |
| 60 | ForumPost *pLast; /* Last post in chronological order */ |
| 61 | ForumPost *pDisplay; /* Entries in display order */ |
| 62 | ForumPost *pTail; /* Last on the display list */ |
| 63 | int mxIndent; /* Maximum indentation level */ |
| 64 | int nArtifact; /* Number of forum artifacts in this thread */ |
| 65 | }; |
| 66 | #endif /* INTERFACE */ |
| 67 | |
| 68 | /* |
| 69 | ** Return true if the forum post with the given rid has been |
| @@ -109,12 +110,17 @@ | |
| 110 | ** the post. |
| 111 | ** |
| 112 | ** If bCheckIrt is true then p's thread in-response-to parents are |
| 113 | ** checked (recursively) for closure, else only p is checked. |
| 114 | */ |
| 115 | static int forumpost_is_closed( |
| 116 | ForumThread *pThread, /* Thread that the post is a member of */ |
| 117 | ForumPost *p, /* the forum post */ |
| 118 | int bCheckIrt /* True to check In-Reply-To posts */ |
| 119 | ){ |
| 120 | int mx = pThread->nArtifact+1; |
| 121 | while( p && (mx--)>0 ){ |
| 122 | if( p->pEditHead ) p = p->pEditHead; |
| 123 | if( p->iClosed || !bCheckIrt ) return p->iClosed; |
| 124 | p = p->pIrt; |
| 125 | } |
| 126 | return 0; |
| @@ -409,10 +415,11 @@ | |
| 415 | pThread->pFirst = pPost; |
| 416 | }else{ |
| 417 | pThread->pLast->pNext = pPost; |
| 418 | } |
| 419 | pThread->pLast = pPost; |
| 420 | pThread->nArtifact++; |
| 421 | |
| 422 | /* Find the in-reply-to post. Default to the topic post if the replied-to |
| 423 | ** post cannot be found. */ |
| 424 | if( firt ){ |
| 425 | pPost->pIrt = pThread->pFirst; |
| @@ -520,10 +527,11 @@ | |
| 527 | fossil_fatal("Not a forum post: \"%s\"", zName); |
| 528 | } |
| 529 | fossil_print("fpid = %d\n", fpid); |
| 530 | fossil_print("froot = %d\n", froot); |
| 531 | pThread = forumthread_create(froot, 1); |
| 532 | fossil_print("count = %d\n", pThread->nArtifact); |
| 533 | fossil_print("Chronological:\n"); |
| 534 | fossil_print( |
| 535 | /* 0 1 2 3 4 5 6 7 */ |
| 536 | /* 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123 */ |
| 537 | " sid rev closed fpid pIrt pEditPrev pEditTail hash\n"); |
| @@ -565,10 +573,11 @@ | |
| 573 | int froot; |
| 574 | const char *zName = P("name"); |
| 575 | ForumThread *pThread; |
| 576 | ForumPost *p; |
| 577 | char *fuuid; |
| 578 | Stmt q; |
| 579 | |
| 580 | login_check_credentials(); |
| 581 | if( !g.perm.Admin ){ |
| 582 | return; |
| 583 | } |
| @@ -599,10 +608,27 @@ | |
| 608 | for(p=pThread->pFirst; p; p=p->pNext){ |
| 609 | @ %h(p->zUuid) |
| 610 | } |
| 611 | forumthread_delete(pThread); |
| 612 | @ </pre> |
| 613 | @ <hr> |
| 614 | @ <h2>Related FORUMPOST Table Content</h2> |
| 615 | @ <table border="1" cellpadding="4" cellspacing="0"> |
| 616 | @ <tr><th>fpid<th>froot<th>fprev<th>firt<th>fmtime |
| 617 | db_prepare(&q, "SELECT fpid, froot, fprev, firt, datetime(fmtime)" |
| 618 | " FROM forumpost" |
| 619 | " WHERE froot=%d" |
| 620 | " ORDER BY fmtime", froot); |
| 621 | while( db_step(&q)==SQLITE_ROW ){ |
| 622 | @ <tr><td>%d(db_column_int(&q,0))\ |
| 623 | @ <td>%d(db_column_int(&q,1))\ |
| 624 | @ <td>%d(db_column_int(&q,2))\ |
| 625 | @ <td>%d(db_column_int(&q,3))\ |
| 626 | @ <td>%h(db_column_text(&q,4))</tr> |
| 627 | } |
| 628 | @ </table> |
| 629 | db_finalize(&q); |
| 630 | style_finish_page(); |
| 631 | } |
| 632 | |
| 633 | /* |
| 634 | ** Render a forum post for display |
| @@ -725,10 +751,11 @@ | |
| 751 | |
| 752 | /* |
| 753 | ** Display a single post in a forum thread. |
| 754 | */ |
| 755 | static void forum_display_post( |
| 756 | ForumThread *pThread, /* The thread that this post is a member of */ |
| 757 | ForumPost *p, /* Forum post to display */ |
| 758 | int iIndentScale, /* Indent scale factor */ |
| 759 | int bRaw, /* True to omit the border */ |
| 760 | int bUnf, /* True to leave the post unformatted */ |
| 761 | int bHist, /* True if showing edit history */ |
| @@ -747,14 +774,14 @@ | |
| 774 | const char *zMimetype;/* Formatting MIME type */ |
| 775 | |
| 776 | /* Get the manifest for the post. Abort if not found (e.g. shunned). */ |
| 777 | pManifest = manifest_get(p->fpid, CFTYPE_FORUM, 0); |
| 778 | if( !pManifest ) return; |
| 779 | iClosed = forumpost_is_closed(pThread, p, 1); |
| 780 | /* When not in raw mode, create the border around the post. */ |
| 781 | if( !bRaw ){ |
| 782 | /* Open the <div> enclosing the post. Set the class string to mark the post |
| 783 | ** as selected and/or obsolete. */ |
| 784 | iIndent = (p->pEditHead ? p->pEditHead->nIndent : p->nIndent)-1; |
| 785 | @ <div id='forum%d(p->fpid)' class='forumTime\ |
| 786 | @ %s(bSelect ? " forumSel" : "")\ |
| 787 | @ %s(iClosed ? " forumClosed" : "")\ |
| @@ -1027,11 +1054,11 @@ | |
| 1054 | } |
| 1055 | |
| 1056 | /* Display the appropriate subset of posts in sequence. */ |
| 1057 | while( p ){ |
| 1058 | /* Display the post. */ |
| 1059 | forum_display_post(pThread, p, iIndentScale, mode==FD_RAW, |
| 1060 | bUnf, bHist, p==pSelect, zQuery); |
| 1061 | |
| 1062 | /* Advance to the next post in the thread. */ |
| 1063 | if( mode==FD_CHRONO ){ |
| 1064 | /* Chronological mode: display posts (optionally including edits) in their |
| 1065 |
+3
-3
| --- src/fossil.dom.js | ||
| +++ src/fossil.dom.js | ||
| @@ -17,16 +17,16 @@ | ||
| 17 | 17 | return function(){ |
| 18 | 18 | return document.createElement(eType); |
| 19 | 19 | }; |
| 20 | 20 | }, |
| 21 | 21 | remove: function(e){ |
| 22 | - if(e.forEach){ | |
| 22 | + if(e?.forEach){ | |
| 23 | 23 | e.forEach( |
| 24 | - (x)=>x.parentNode.removeChild(x) | |
| 24 | + (x)=>x?.parentNode?.removeChild(x) | |
| 25 | 25 | ); |
| 26 | 26 | }else{ |
| 27 | - e.parentNode.removeChild(e); | |
| 27 | + e?.parentNode?.removeChild(e); | |
| 28 | 28 | } |
| 29 | 29 | return e; |
| 30 | 30 | }, |
| 31 | 31 | /** |
| 32 | 32 | Removes all child DOM elements from the given element |
| 33 | 33 |
| --- src/fossil.dom.js | |
| +++ src/fossil.dom.js | |
| @@ -17,16 +17,16 @@ | |
| 17 | return function(){ |
| 18 | return document.createElement(eType); |
| 19 | }; |
| 20 | }, |
| 21 | remove: function(e){ |
| 22 | if(e.forEach){ |
| 23 | e.forEach( |
| 24 | (x)=>x.parentNode.removeChild(x) |
| 25 | ); |
| 26 | }else{ |
| 27 | e.parentNode.removeChild(e); |
| 28 | } |
| 29 | return e; |
| 30 | }, |
| 31 | /** |
| 32 | Removes all child DOM elements from the given element |
| 33 |
| --- src/fossil.dom.js | |
| +++ src/fossil.dom.js | |
| @@ -17,16 +17,16 @@ | |
| 17 | return function(){ |
| 18 | return document.createElement(eType); |
| 19 | }; |
| 20 | }, |
| 21 | remove: function(e){ |
| 22 | if(e?.forEach){ |
| 23 | e.forEach( |
| 24 | (x)=>x?.parentNode?.removeChild(x) |
| 25 | ); |
| 26 | }else{ |
| 27 | e?.parentNode?.removeChild(e); |
| 28 | } |
| 29 | return e; |
| 30 | }, |
| 31 | /** |
| 32 | Removes all child DOM elements from the given element |
| 33 |
+95
-22
| --- src/fossil.fetch.js | ||
| +++ src/fossil.fetch.js | ||
| @@ -27,18 +27,51 @@ | ||
| 27 | 27 | "this", noting that this call may have amended the options object |
| 28 | 28 | with state other than what the caller provided. |
| 29 | 29 | |
| 30 | 30 | - onerror: callback(Error object) (default = output error message |
| 31 | 31 | to console.error() and fossil.error()). Triggered if the request |
| 32 | - generates any response other than HTTP 200, suffers a connection | |
| 33 | - error or timeout while awaiting a response, or if the onload() | |
| 34 | - handler throws an exception. In the context of the callback, the | |
| 35 | - options object is "this". Note that this function is intended to be | |
| 36 | - used solely for error reporting, not error recovery. Because | |
| 37 | - onerror() may be called if onload() throws, it is up to the caller | |
| 38 | - to ensure that their onerror() callback references only state which | |
| 39 | - is valid in such a case. | |
| 32 | + generates any response other than HTTP 200, or if the beforesend() | |
| 33 | + or onload() handler throws an exception. In the context of the | |
| 34 | + callback, the options object is "this". This function is intended | |
| 35 | + to be used solely for error reporting, not error recovery. Special | |
| 36 | + cases for the Error object: | |
| 37 | + | |
| 38 | + 1. Timeouts unfortunately show up as a series of 2 events: an | |
| 39 | + HTTP 0 followed immediately by an XHR.ontimeout(). The former | |
| 40 | + cannot(?) be unambiguously identified as the trigger for the | |
| 41 | + pending timeout, so we have no option but to pass it on as-is | |
| 42 | + instead of flagging it as a timeout response. The latter will | |
| 43 | + trigger the client-provided ontimeout() if it's available (see | |
| 44 | + below), else it calls the onerror() callback. An error object | |
| 45 | + passed to ontimeout() by fetch() will have (.name='timeout', | |
| 46 | + .status=XHR.status). | |
| 47 | + | |
| 48 | + 2. Else if the response contains a JSON-format exception on the | |
| 49 | + server, it will have (.name='json-error', | |
| 50 | + status=XHR.status). Any JSON-format result object which has a | |
| 51 | + property named "error" is considered to be a server-generated | |
| 52 | + error. | |
| 53 | + | |
| 54 | + 3. Else if it gets a non 2xx HTTP code then it will have | |
| 55 | + (.name='http',.status=XHR.status). | |
| 56 | + | |
| 57 | + 4. If onerror() throws, the exception is suppressed but may | |
| 58 | + generate a console error message. | |
| 59 | + | |
| 60 | + - ontimeout: callback(Error object). If set, timeout errors are | |
| 61 | + reported here, else they are reported through onerror(). | |
| 62 | + Unfortunately, XHR fires two events for a timeout: an | |
| 63 | + onreadystatechange() and an ontimeout(), in that order. From the | |
| 64 | + former, however, we cannot unambiguously identify the error as | |
| 65 | + having been caused by a timeout, so clients which set ontimeout() | |
| 66 | + will get _two_ callback calls: one with with an HTTP error response | |
| 67 | + followed immediately by an ontimeout() response. Error objects | |
| 68 | + passed to this will have (.name='timeout', .status=xhr.HttpStatus). | |
| 69 | + In the context of the callback, the options object is "this", Like | |
| 70 | + onerror(), any exceptions thrown by the ontimeout() handler are | |
| 71 | + suppressed, but may generate a console error message. The onerror() | |
| 72 | + handler is _not_ called in this case. | |
| 40 | 73 | |
| 41 | 74 | - method: 'POST' | 'GET' (default = 'GET'). CASE SENSITIVE! |
| 42 | 75 | |
| 43 | 76 | - payload: anything acceptable by XHR2.send(ARG) (DOMString, |
| 44 | 77 | Document, FormData, Blob, File, ArrayBuffer), or a plain object or |
| @@ -46,11 +79,12 @@ | ||
| 46 | 79 | then the method is automatically set to 'POST'. By default XHR2 |
| 47 | 80 | will set the content type based on the payload type. If an |
| 48 | 81 | object/array is converted to JSON, the contentType option is |
| 49 | 82 | automatically set to 'application/json', and if JSON.stringify() of |
| 50 | 83 | that value fails then the exception is propagated to this |
| 51 | - function's caller. | |
| 84 | + function's caller. (beforesend(), aftersend(), and onerror() are | |
| 85 | + NOT triggered in that case.) | |
| 52 | 86 | |
| 53 | 87 | - contentType: Optional request content type when POSTing. Ignored |
| 54 | 88 | if the method is not 'POST'. |
| 55 | 89 | |
| 56 | 90 | - responseType: optional string. One of ("text", "arraybuffer", |
| @@ -58,10 +92,13 @@ | ||
| 58 | 92 | As an extension, it supports "json", which tells it that the |
| 59 | 93 | response is expected to be text and that it should be JSON.parse()d |
| 60 | 94 | before passing it on to the onload() callback. If parsing of such |
| 61 | 95 | an object fails, the onload callback is not called, and the |
| 62 | 96 | onerror() callback is passed the exception from the parsing error. |
| 97 | + If the parsed JSON object has an "error" property, it is assumed to | |
| 98 | + be an error string, which is used to populate a new Error object, | |
| 99 | + which will gets (.name="json") set on it. | |
| 63 | 100 | |
| 64 | 101 | - urlParams: string|object. If a string, it is assumed to be a |
| 65 | 102 | URI-encoded list of params in the form "key1=val1&key2=val2...", |
| 66 | 103 | with NO leading '?'. If it is an object, all of its properties get |
| 67 | 104 | converted to that form. Either way, the parameters get appended to |
| @@ -73,11 +110,11 @@ | ||
| 73 | 110 | value of that single header. If it's an array, it's treated as a |
| 74 | 111 | list of headers to return, and the 2nd argument is a map of those |
| 75 | 112 | header values. When a map is passed on, all of its keys are |
| 76 | 113 | lower-cased. When a given header is requested and that header is |
| 77 | 114 | set multiple times, their values are (per the XHR docs) |
| 78 | - concatenated together with ", " between them. | |
| 115 | + concatenated together with "," between them. | |
| 79 | 116 | |
| 80 | 117 | - beforesend/aftersend: optional callbacks which are called |
| 81 | 118 | without arguments immediately before the request is submitted |
| 82 | 119 | and immediately after it is received, regardless of success or |
| 83 | 120 | error. In the context of the callback, the options object is |
| @@ -133,11 +170,11 @@ | ||
| 133 | 170 | }); |
| 134 | 171 | return rc; |
| 135 | 172 | }; |
| 136 | 173 | } |
| 137 | 174 | if('/'===uri[0]) uri = uri.substr(1); |
| 138 | - if(!opt) opt = {}; | |
| 175 | + if(!opt) opt = {}/* should arguably be Object.create(null) */; | |
| 139 | 176 | else if('function'===typeof opt) opt={onload:opt}; |
| 140 | 177 | if(!opt.onload) opt.onload = f.onload; |
| 141 | 178 | if(!opt.onerror) opt.onerror = f.onerror; |
| 142 | 179 | if(!opt.beforesend) opt.beforesend = f.beforesend; |
| 143 | 180 | if(!opt.aftersend) opt.aftersend = f.aftersend; |
| @@ -164,15 +201,34 @@ | ||
| 164 | 201 | jsonResponse = true; |
| 165 | 202 | x.responseType = 'text'; |
| 166 | 203 | }else{ |
| 167 | 204 | x.responseType = opt.responseType||'text'; |
| 168 | 205 | } |
| 169 | - x.ontimeout = function(){ | |
| 206 | + x.ontimeout = function(ev){ | |
| 170 | 207 | try{opt.aftersend()}catch(e){/*ignore*/} |
| 171 | - opt.onerror(new Error("XHR timeout of "+x.timeout+"ms expired.")); | |
| 208 | + const err = new Error("XHR timeout of "+x.timeout+"ms expired."); | |
| 209 | + err.status = x.status; | |
| 210 | + err.name = 'timeout'; | |
| 211 | + //console.warn("fetch.ontimeout",ev); | |
| 212 | + try{ | |
| 213 | + (opt.ontimeout || opt.onerror)(err); | |
| 214 | + }catch(e){ | |
| 215 | + /*ignore*/ | |
| 216 | + console.error("fossil.fetch()'s ontimeout() handler threw",e); | |
| 217 | + } | |
| 218 | + }; | |
| 219 | + /* Ensure that if onerror() throws, it's ignored. */ | |
| 220 | + const origOnError = opt.onerror; | |
| 221 | + opt.onerror = (arg)=>{ | |
| 222 | + try{ origOnError.call(this, arg) } | |
| 223 | + catch(e){ | |
| 224 | + /*ignored*/ | |
| 225 | + console.error("fossil.fetch()'s onerror() threw",e); | |
| 226 | + } | |
| 172 | 227 | }; |
| 173 | - x.onreadystatechange = function(){ | |
| 228 | + x.onreadystatechange = function(ev){ | |
| 229 | + //console.warn("onreadystatechange", x.readyState, ev.target.responseText); | |
| 174 | 230 | if(XMLHttpRequest.DONE !== x.readyState) return; |
| 175 | 231 | try{opt.aftersend()}catch(e){/*ignore*/} |
| 176 | 232 | if(false && 0===x.status){ |
| 177 | 233 | /* For reasons unknown, we _sometimes_ trigger x.status==0 in FF |
| 178 | 234 | when the /chat page starts up, but not in Chrome nor in other |
| @@ -180,20 +236,37 @@ | ||
| 180 | 236 | request is actually sent and it appears to have no |
| 181 | 237 | side-effects on the app other than to generate an error |
| 182 | 238 | (i.e. no requests/responses are missing). This is a silly |
| 183 | 239 | workaround which may or may not bite us later. If so, it can |
| 184 | 240 | be removed at the cost of an unsightly console error message |
| 185 | - in FF. */ | |
| 241 | + in FF. | |
| 242 | + | |
| 243 | + 2025-04-10: that behavior is now also in Chrome and enabling | |
| 244 | + this workaround causes our timeout errors to never arrive. | |
| 245 | + */ | |
| 186 | 246 | return; |
| 187 | 247 | } |
| 188 | 248 | if(200!==x.status){ |
| 249 | + //console.warn("Error response",ev.target); | |
| 189 | 250 | let err; |
| 190 | 251 | try{ |
| 191 | 252 | const j = JSON.parse(x.response); |
| 192 | - if(j.error) err = new Error(j.error); | |
| 253 | + if(j.error){ | |
| 254 | + err = new Error(j.error); | |
| 255 | + err.name = 'json.error'; | |
| 256 | + } | |
| 193 | 257 | }catch(ex){/*ignore*/} |
| 194 | - opt.onerror(err || new Error("HTTP response status "+x.status+".")); | |
| 258 | + if( !err ){ | |
| 259 | + /* We can't tell from here whether this was a timeout-capable | |
| 260 | + request which timed out on our end or was one which is a | |
| 261 | + genuine error. We also don't know whether the server timed | |
| 262 | + out the connection before we did. */ | |
| 263 | + err = new Error("HTTP response status "+x.status+".") | |
| 264 | + err.name = 'http'; | |
| 265 | + } | |
| 266 | + err.status = x.status; | |
| 267 | + opt.onerror(err); | |
| 195 | 268 | return; |
| 196 | 269 | } |
| 197 | 270 | const orh = opt.responseHeaders; |
| 198 | 271 | let head; |
| 199 | 272 | if(true===orh){ |
| @@ -209,17 +282,17 @@ | ||
| 209 | 282 | try{ |
| 210 | 283 | const args = [(jsonResponse && x.response) |
| 211 | 284 | ? JSON.parse(x.response) : x.response]; |
| 212 | 285 | if(head) args.push(head); |
| 213 | 286 | opt.onload.apply(opt, args); |
| 214 | - }catch(e){ | |
| 215 | - opt.onerror(e); | |
| 287 | + }catch(err){ | |
| 288 | + opt.onerror(err); | |
| 216 | 289 | } |
| 217 | - }; | |
| 290 | + }/*onreadystatechange()*/; | |
| 218 | 291 | try{opt.beforesend()} |
| 219 | - catch(e){ | |
| 220 | - opt.onerror(e); | |
| 292 | + catch(err){ | |
| 293 | + opt.onerror(err); | |
| 221 | 294 | return; |
| 222 | 295 | } |
| 223 | 296 | x.open(opt.method||'GET', url.join(''), true); |
| 224 | 297 | if('POST'===opt.method && 'string'===typeof opt.contentType){ |
| 225 | 298 | x.setRequestHeader('Content-Type',opt.contentType); |
| 226 | 299 |
| --- src/fossil.fetch.js | |
| +++ src/fossil.fetch.js | |
| @@ -27,18 +27,51 @@ | |
| 27 | "this", noting that this call may have amended the options object |
| 28 | with state other than what the caller provided. |
| 29 | |
| 30 | - onerror: callback(Error object) (default = output error message |
| 31 | to console.error() and fossil.error()). Triggered if the request |
| 32 | generates any response other than HTTP 200, suffers a connection |
| 33 | error or timeout while awaiting a response, or if the onload() |
| 34 | handler throws an exception. In the context of the callback, the |
| 35 | options object is "this". Note that this function is intended to be |
| 36 | used solely for error reporting, not error recovery. Because |
| 37 | onerror() may be called if onload() throws, it is up to the caller |
| 38 | to ensure that their onerror() callback references only state which |
| 39 | is valid in such a case. |
| 40 | |
| 41 | - method: 'POST' | 'GET' (default = 'GET'). CASE SENSITIVE! |
| 42 | |
| 43 | - payload: anything acceptable by XHR2.send(ARG) (DOMString, |
| 44 | Document, FormData, Blob, File, ArrayBuffer), or a plain object or |
| @@ -46,11 +79,12 @@ | |
| 46 | then the method is automatically set to 'POST'. By default XHR2 |
| 47 | will set the content type based on the payload type. If an |
| 48 | object/array is converted to JSON, the contentType option is |
| 49 | automatically set to 'application/json', and if JSON.stringify() of |
| 50 | that value fails then the exception is propagated to this |
| 51 | function's caller. |
| 52 | |
| 53 | - contentType: Optional request content type when POSTing. Ignored |
| 54 | if the method is not 'POST'. |
| 55 | |
| 56 | - responseType: optional string. One of ("text", "arraybuffer", |
| @@ -58,10 +92,13 @@ | |
| 58 | As an extension, it supports "json", which tells it that the |
| 59 | response is expected to be text and that it should be JSON.parse()d |
| 60 | before passing it on to the onload() callback. If parsing of such |
| 61 | an object fails, the onload callback is not called, and the |
| 62 | onerror() callback is passed the exception from the parsing error. |
| 63 | |
| 64 | - urlParams: string|object. If a string, it is assumed to be a |
| 65 | URI-encoded list of params in the form "key1=val1&key2=val2...", |
| 66 | with NO leading '?'. If it is an object, all of its properties get |
| 67 | converted to that form. Either way, the parameters get appended to |
| @@ -73,11 +110,11 @@ | |
| 73 | value of that single header. If it's an array, it's treated as a |
| 74 | list of headers to return, and the 2nd argument is a map of those |
| 75 | header values. When a map is passed on, all of its keys are |
| 76 | lower-cased. When a given header is requested and that header is |
| 77 | set multiple times, their values are (per the XHR docs) |
| 78 | concatenated together with ", " between them. |
| 79 | |
| 80 | - beforesend/aftersend: optional callbacks which are called |
| 81 | without arguments immediately before the request is submitted |
| 82 | and immediately after it is received, regardless of success or |
| 83 | error. In the context of the callback, the options object is |
| @@ -133,11 +170,11 @@ | |
| 133 | }); |
| 134 | return rc; |
| 135 | }; |
| 136 | } |
| 137 | if('/'===uri[0]) uri = uri.substr(1); |
| 138 | if(!opt) opt = {}; |
| 139 | else if('function'===typeof opt) opt={onload:opt}; |
| 140 | if(!opt.onload) opt.onload = f.onload; |
| 141 | if(!opt.onerror) opt.onerror = f.onerror; |
| 142 | if(!opt.beforesend) opt.beforesend = f.beforesend; |
| 143 | if(!opt.aftersend) opt.aftersend = f.aftersend; |
| @@ -164,15 +201,34 @@ | |
| 164 | jsonResponse = true; |
| 165 | x.responseType = 'text'; |
| 166 | }else{ |
| 167 | x.responseType = opt.responseType||'text'; |
| 168 | } |
| 169 | x.ontimeout = function(){ |
| 170 | try{opt.aftersend()}catch(e){/*ignore*/} |
| 171 | opt.onerror(new Error("XHR timeout of "+x.timeout+"ms expired.")); |
| 172 | }; |
| 173 | x.onreadystatechange = function(){ |
| 174 | if(XMLHttpRequest.DONE !== x.readyState) return; |
| 175 | try{opt.aftersend()}catch(e){/*ignore*/} |
| 176 | if(false && 0===x.status){ |
| 177 | /* For reasons unknown, we _sometimes_ trigger x.status==0 in FF |
| 178 | when the /chat page starts up, but not in Chrome nor in other |
| @@ -180,20 +236,37 @@ | |
| 180 | request is actually sent and it appears to have no |
| 181 | side-effects on the app other than to generate an error |
| 182 | (i.e. no requests/responses are missing). This is a silly |
| 183 | workaround which may or may not bite us later. If so, it can |
| 184 | be removed at the cost of an unsightly console error message |
| 185 | in FF. */ |
| 186 | return; |
| 187 | } |
| 188 | if(200!==x.status){ |
| 189 | let err; |
| 190 | try{ |
| 191 | const j = JSON.parse(x.response); |
| 192 | if(j.error) err = new Error(j.error); |
| 193 | }catch(ex){/*ignore*/} |
| 194 | opt.onerror(err || new Error("HTTP response status "+x.status+".")); |
| 195 | return; |
| 196 | } |
| 197 | const orh = opt.responseHeaders; |
| 198 | let head; |
| 199 | if(true===orh){ |
| @@ -209,17 +282,17 @@ | |
| 209 | try{ |
| 210 | const args = [(jsonResponse && x.response) |
| 211 | ? JSON.parse(x.response) : x.response]; |
| 212 | if(head) args.push(head); |
| 213 | opt.onload.apply(opt, args); |
| 214 | }catch(e){ |
| 215 | opt.onerror(e); |
| 216 | } |
| 217 | }; |
| 218 | try{opt.beforesend()} |
| 219 | catch(e){ |
| 220 | opt.onerror(e); |
| 221 | return; |
| 222 | } |
| 223 | x.open(opt.method||'GET', url.join(''), true); |
| 224 | if('POST'===opt.method && 'string'===typeof opt.contentType){ |
| 225 | x.setRequestHeader('Content-Type',opt.contentType); |
| 226 |
| --- src/fossil.fetch.js | |
| +++ src/fossil.fetch.js | |
| @@ -27,18 +27,51 @@ | |
| 27 | "this", noting that this call may have amended the options object |
| 28 | with state other than what the caller provided. |
| 29 | |
| 30 | - onerror: callback(Error object) (default = output error message |
| 31 | to console.error() and fossil.error()). Triggered if the request |
| 32 | generates any response other than HTTP 200, or if the beforesend() |
| 33 | or onload() handler throws an exception. In the context of the |
| 34 | callback, the options object is "this". This function is intended |
| 35 | to be used solely for error reporting, not error recovery. Special |
| 36 | cases for the Error object: |
| 37 | |
| 38 | 1. Timeouts unfortunately show up as a series of 2 events: an |
| 39 | HTTP 0 followed immediately by an XHR.ontimeout(). The former |
| 40 | cannot(?) be unambiguously identified as the trigger for the |
| 41 | pending timeout, so we have no option but to pass it on as-is |
| 42 | instead of flagging it as a timeout response. The latter will |
| 43 | trigger the client-provided ontimeout() if it's available (see |
| 44 | below), else it calls the onerror() callback. An error object |
| 45 | passed to ontimeout() by fetch() will have (.name='timeout', |
| 46 | .status=XHR.status). |
| 47 | |
| 48 | 2. Else if the response contains a JSON-format exception on the |
| 49 | server, it will have (.name='json-error', |
| 50 | status=XHR.status). Any JSON-format result object which has a |
| 51 | property named "error" is considered to be a server-generated |
| 52 | error. |
| 53 | |
| 54 | 3. Else if it gets a non 2xx HTTP code then it will have |
| 55 | (.name='http',.status=XHR.status). |
| 56 | |
| 57 | 4. If onerror() throws, the exception is suppressed but may |
| 58 | generate a console error message. |
| 59 | |
| 60 | - ontimeout: callback(Error object). If set, timeout errors are |
| 61 | reported here, else they are reported through onerror(). |
| 62 | Unfortunately, XHR fires two events for a timeout: an |
| 63 | onreadystatechange() and an ontimeout(), in that order. From the |
| 64 | former, however, we cannot unambiguously identify the error as |
| 65 | having been caused by a timeout, so clients which set ontimeout() |
| 66 | will get _two_ callback calls: one with with an HTTP error response |
| 67 | followed immediately by an ontimeout() response. Error objects |
| 68 | passed to this will have (.name='timeout', .status=xhr.HttpStatus). |
| 69 | In the context of the callback, the options object is "this", Like |
| 70 | onerror(), any exceptions thrown by the ontimeout() handler are |
| 71 | suppressed, but may generate a console error message. The onerror() |
| 72 | handler is _not_ called in this case. |
| 73 | |
| 74 | - method: 'POST' | 'GET' (default = 'GET'). CASE SENSITIVE! |
| 75 | |
| 76 | - payload: anything acceptable by XHR2.send(ARG) (DOMString, |
| 77 | Document, FormData, Blob, File, ArrayBuffer), or a plain object or |
| @@ -46,11 +79,12 @@ | |
| 79 | then the method is automatically set to 'POST'. By default XHR2 |
| 80 | will set the content type based on the payload type. If an |
| 81 | object/array is converted to JSON, the contentType option is |
| 82 | automatically set to 'application/json', and if JSON.stringify() of |
| 83 | that value fails then the exception is propagated to this |
| 84 | function's caller. (beforesend(), aftersend(), and onerror() are |
| 85 | NOT triggered in that case.) |
| 86 | |
| 87 | - contentType: Optional request content type when POSTing. Ignored |
| 88 | if the method is not 'POST'. |
| 89 | |
| 90 | - responseType: optional string. One of ("text", "arraybuffer", |
| @@ -58,10 +92,13 @@ | |
| 92 | As an extension, it supports "json", which tells it that the |
| 93 | response is expected to be text and that it should be JSON.parse()d |
| 94 | before passing it on to the onload() callback. If parsing of such |
| 95 | an object fails, the onload callback is not called, and the |
| 96 | onerror() callback is passed the exception from the parsing error. |
| 97 | If the parsed JSON object has an "error" property, it is assumed to |
| 98 | be an error string, which is used to populate a new Error object, |
| 99 | which will gets (.name="json") set on it. |
| 100 | |
| 101 | - urlParams: string|object. If a string, it is assumed to be a |
| 102 | URI-encoded list of params in the form "key1=val1&key2=val2...", |
| 103 | with NO leading '?'. If it is an object, all of its properties get |
| 104 | converted to that form. Either way, the parameters get appended to |
| @@ -73,11 +110,11 @@ | |
| 110 | value of that single header. If it's an array, it's treated as a |
| 111 | list of headers to return, and the 2nd argument is a map of those |
| 112 | header values. When a map is passed on, all of its keys are |
| 113 | lower-cased. When a given header is requested and that header is |
| 114 | set multiple times, their values are (per the XHR docs) |
| 115 | concatenated together with "," between them. |
| 116 | |
| 117 | - beforesend/aftersend: optional callbacks which are called |
| 118 | without arguments immediately before the request is submitted |
| 119 | and immediately after it is received, regardless of success or |
| 120 | error. In the context of the callback, the options object is |
| @@ -133,11 +170,11 @@ | |
| 170 | }); |
| 171 | return rc; |
| 172 | }; |
| 173 | } |
| 174 | if('/'===uri[0]) uri = uri.substr(1); |
| 175 | if(!opt) opt = {}/* should arguably be Object.create(null) */; |
| 176 | else if('function'===typeof opt) opt={onload:opt}; |
| 177 | if(!opt.onload) opt.onload = f.onload; |
| 178 | if(!opt.onerror) opt.onerror = f.onerror; |
| 179 | if(!opt.beforesend) opt.beforesend = f.beforesend; |
| 180 | if(!opt.aftersend) opt.aftersend = f.aftersend; |
| @@ -164,15 +201,34 @@ | |
| 201 | jsonResponse = true; |
| 202 | x.responseType = 'text'; |
| 203 | }else{ |
| 204 | x.responseType = opt.responseType||'text'; |
| 205 | } |
| 206 | x.ontimeout = function(ev){ |
| 207 | try{opt.aftersend()}catch(e){/*ignore*/} |
| 208 | const err = new Error("XHR timeout of "+x.timeout+"ms expired."); |
| 209 | err.status = x.status; |
| 210 | err.name = 'timeout'; |
| 211 | //console.warn("fetch.ontimeout",ev); |
| 212 | try{ |
| 213 | (opt.ontimeout || opt.onerror)(err); |
| 214 | }catch(e){ |
| 215 | /*ignore*/ |
| 216 | console.error("fossil.fetch()'s ontimeout() handler threw",e); |
| 217 | } |
| 218 | }; |
| 219 | /* Ensure that if onerror() throws, it's ignored. */ |
| 220 | const origOnError = opt.onerror; |
| 221 | opt.onerror = (arg)=>{ |
| 222 | try{ origOnError.call(this, arg) } |
| 223 | catch(e){ |
| 224 | /*ignored*/ |
| 225 | console.error("fossil.fetch()'s onerror() threw",e); |
| 226 | } |
| 227 | }; |
| 228 | x.onreadystatechange = function(ev){ |
| 229 | //console.warn("onreadystatechange", x.readyState, ev.target.responseText); |
| 230 | if(XMLHttpRequest.DONE !== x.readyState) return; |
| 231 | try{opt.aftersend()}catch(e){/*ignore*/} |
| 232 | if(false && 0===x.status){ |
| 233 | /* For reasons unknown, we _sometimes_ trigger x.status==0 in FF |
| 234 | when the /chat page starts up, but not in Chrome nor in other |
| @@ -180,20 +236,37 @@ | |
| 236 | request is actually sent and it appears to have no |
| 237 | side-effects on the app other than to generate an error |
| 238 | (i.e. no requests/responses are missing). This is a silly |
| 239 | workaround which may or may not bite us later. If so, it can |
| 240 | be removed at the cost of an unsightly console error message |
| 241 | in FF. |
| 242 | |
| 243 | 2025-04-10: that behavior is now also in Chrome and enabling |
| 244 | this workaround causes our timeout errors to never arrive. |
| 245 | */ |
| 246 | return; |
| 247 | } |
| 248 | if(200!==x.status){ |
| 249 | //console.warn("Error response",ev.target); |
| 250 | let err; |
| 251 | try{ |
| 252 | const j = JSON.parse(x.response); |
| 253 | if(j.error){ |
| 254 | err = new Error(j.error); |
| 255 | err.name = 'json.error'; |
| 256 | } |
| 257 | }catch(ex){/*ignore*/} |
| 258 | if( !err ){ |
| 259 | /* We can't tell from here whether this was a timeout-capable |
| 260 | request which timed out on our end or was one which is a |
| 261 | genuine error. We also don't know whether the server timed |
| 262 | out the connection before we did. */ |
| 263 | err = new Error("HTTP response status "+x.status+".") |
| 264 | err.name = 'http'; |
| 265 | } |
| 266 | err.status = x.status; |
| 267 | opt.onerror(err); |
| 268 | return; |
| 269 | } |
| 270 | const orh = opt.responseHeaders; |
| 271 | let head; |
| 272 | if(true===orh){ |
| @@ -209,17 +282,17 @@ | |
| 282 | try{ |
| 283 | const args = [(jsonResponse && x.response) |
| 284 | ? JSON.parse(x.response) : x.response]; |
| 285 | if(head) args.push(head); |
| 286 | opt.onload.apply(opt, args); |
| 287 | }catch(err){ |
| 288 | opt.onerror(err); |
| 289 | } |
| 290 | }/*onreadystatechange()*/; |
| 291 | try{opt.beforesend()} |
| 292 | catch(err){ |
| 293 | opt.onerror(err); |
| 294 | return; |
| 295 | } |
| 296 | x.open(opt.method||'GET', url.join(''), true); |
| 297 | if('POST'===opt.method && 'string'===typeof opt.contentType){ |
| 298 | x.setRequestHeader('Content-Type',opt.contentType); |
| 299 |
+376
-57
| --- src/fossil.page.chat.js | ||
| +++ src/fossil.page.chat.js | ||
| @@ -1,6 +1,6 @@ | ||
| 1 | -/** | |
| 1 | +-/** | |
| 2 | 2 | This file contains the client-side implementation of fossil's /chat |
| 3 | 3 | application. |
| 4 | 4 | */ |
| 5 | 5 | window.fossil.onPageLoad(function(){ |
| 6 | 6 | const F = window.fossil, D = F.dom; |
| @@ -129,19 +129,21 @@ | ||
| 129 | 129 | return resized; |
| 130 | 130 | })(); |
| 131 | 131 | fossil.FRK = ForceResizeKludge/*for debugging*/; |
| 132 | 132 | const Chat = ForceResizeKludge.chat = (function(){ |
| 133 | 133 | const cs = { // the "Chat" object (result of this function) |
| 134 | - verboseErrors: false /* if true then certain, mostly extraneous, | |
| 135 | - error messages may be sent to the console. */, | |
| 134 | + beVerbose: false | |
| 135 | + //!!window.location.hostname.match("localhost") | |
| 136 | + /* if true then certain, mostly extraneous, error messages and | |
| 137 | + log messages may be sent to the console. */, | |
| 136 | 138 | playedBeep: false /* used for the beep-once setting */, |
| 137 | 139 | e:{/*map of certain DOM elements.*/ |
| 138 | 140 | messageInjectPoint: E1('#message-inject-point'), |
| 139 | 141 | pageTitle: E1('head title'), |
| 140 | 142 | loadOlderToolbar: undefined /* the load-posts toolbar (dynamically created) */, |
| 141 | - inputWrapper: E1("#chat-input-area"), | |
| 142 | - inputElementWrapper: E1('#chat-input-line-wrapper'), | |
| 143 | + inputArea: E1("#chat-input-area"), | |
| 144 | + inputLineWrapper: E1('#chat-input-line-wrapper'), | |
| 143 | 145 | fileSelectWrapper: E1('#chat-input-file-area'), |
| 144 | 146 | viewMessages: E1('#chat-messages-wrapper'), |
| 145 | 147 | btnSubmit: E1('#chat-button-submit'), |
| 146 | 148 | btnAttach: E1('#chat-button-attach'), |
| 147 | 149 | inputX: E1('#chat-input-field-x'), |
| @@ -155,11 +157,13 @@ | ||
| 155 | 157 | viewSearch: E1('#chat-search'), |
| 156 | 158 | searchContent: E1('#chat-search-content'), |
| 157 | 159 | btnPreview: E1('#chat-button-preview'), |
| 158 | 160 | views: document.querySelectorAll('.chat-view'), |
| 159 | 161 | activeUserListWrapper: E1('#chat-user-list-wrapper'), |
| 160 | - activeUserList: E1('#chat-user-list') | |
| 162 | + activeUserList: E1('#chat-user-list'), | |
| 163 | + eMsgPollError: undefined /* current connection error MessageMidget */, | |
| 164 | + pollErrorMarker: document.body /* element to toggle 'connection-error' CSS class on */ | |
| 161 | 165 | }, |
| 162 | 166 | me: F.user.name, |
| 163 | 167 | mxMsg: F.config.chat.initSize ? -F.config.chat.initSize : -50, |
| 164 | 168 | mnMsg: undefined/*lowest message ID we've seen so far (for history loading)*/, |
| 165 | 169 | pageIsActive: 'visible'===document.visibilityState, |
| @@ -179,10 +183,105 @@ | ||
| 179 | 183 | filterState:{ |
| 180 | 184 | activeUser: undefined, |
| 181 | 185 | match: function(uname){ |
| 182 | 186 | return this.activeUser===uname || !this.activeUser; |
| 183 | 187 | } |
| 188 | + }, | |
| 189 | + /** | |
| 190 | + The timer object is used to control connection throttling | |
| 191 | + when connection errors arrise. It starts off with a polling | |
| 192 | + delay of $initialDelay ms. If there's a connection error, | |
| 193 | + that gets bumped by some value for each subsequent error, up | |
| 194 | + to some max value. | |
| 195 | + | |
| 196 | + The timing of resetting the delay when service returns is, | |
| 197 | + because of the long-poll connection and our lack of low-level | |
| 198 | + insight into the connection at this level, a bit wonky. | |
| 199 | + */ | |
| 200 | + timer:{ | |
| 201 | + /* setTimeout() ID for (delayed) starting a Chat.poll(), so | |
| 202 | + that it runs at controlled intervals (which change when a | |
| 203 | + connection drops and recovers). */ | |
| 204 | + tidPendingPoll: undefined, | |
| 205 | + tidClearPollErr: undefined /*setTimeout() timer id for | |
| 206 | + reconnection determination. See | |
| 207 | + clearPollErrOnWait(). */, | |
| 208 | + $initialDelay: 1000 /* initial polling interval (ms) */, | |
| 209 | + currentDelay: 1000 /* current polling interval */, | |
| 210 | + maxDelay: 60000 * 5 /* max interval when backing off for | |
| 211 | + connection errors */, | |
| 212 | + minDelay: 5000 /* minimum delay time for a back-off/retry | |
| 213 | + attempt. */, | |
| 214 | + errCount: 0 /* Current poller connection error count */, | |
| 215 | + minErrForNotify: 4 /* Don't warn for connection errors until this | |
| 216 | + many have occurred */, | |
| 217 | + pollTimeout: (1 && window.location.hostname.match( | |
| 218 | + "localhost" /*presumably local dev mode*/ | |
| 219 | + )) ? 15000 | |
| 220 | + : (+F.config.chat.pollTimeout>0 | |
| 221 | + ? (1000 * (F.config.chat.pollTimeout - Math.floor(F.config.chat.pollTimeout * 0.1))) | |
| 222 | + /* ^^^^^^^^^^^^ we want our timeouts to be slightly shorter | |
| 223 | + than the server's so that we can distingished timed-out | |
| 224 | + polls on our end from HTTP errors (if the server times | |
| 225 | + out). */ | |
| 226 | + : 30000), | |
| 227 | + /** Returns a random fudge value for reconnect attempt times, | |
| 228 | + intended to keep the /chat server from getting hammered if | |
| 229 | + all clients which were just disconnected all reconnect at | |
| 230 | + the same instant. */ | |
| 231 | + randomInterval: function(factor){ | |
| 232 | + return Math.floor(Math.random() * factor); | |
| 233 | + }, | |
| 234 | + /** Increments the reconnection delay, within some min/max range. */ | |
| 235 | + incrDelay: function(){ | |
| 236 | + if( this.maxDelay > this.currentDelay ){ | |
| 237 | + if(this.currentDelay < this.minDelay){ | |
| 238 | + this.currentDelay = this.minDelay + this.randomInterval(this.minDelay); | |
| 239 | + }else{ | |
| 240 | + this.currentDelay = this.currentDelay*2 + this.randomInterval(this.currentDelay); | |
| 241 | + } | |
| 242 | + } | |
| 243 | + return this.currentDelay; | |
| 244 | + }, | |
| 245 | + /** Resets the delay counter to v || its initial value. */ | |
| 246 | + resetDelay: function(ms=0){ | |
| 247 | + return this.currentDelay = ms || this.$initialDelay; | |
| 248 | + }, | |
| 249 | + /** Returns true if the timer is set to delayed mode. */ | |
| 250 | + isDelayed: function(){ | |
| 251 | + return (this.currentDelay > this.$initialDelay) ? this.currentDelay : 0; | |
| 252 | + }, | |
| 253 | + /** | |
| 254 | + Cancels any in-progress pending-poll timer and starts a new | |
| 255 | + one with the given delay, defaulting to this.resetDelay(). | |
| 256 | + */ | |
| 257 | + startPendingPollTimer: function(delay){ | |
| 258 | + this.cancelPendingPollTimer().tidPendingPoll | |
| 259 | + = setTimeout( Chat.poll, delay || Chat.timer.resetDelay() ); | |
| 260 | + return this; | |
| 261 | + }, | |
| 262 | + /** | |
| 263 | + Cancels any still-active timer set to trigger the next | |
| 264 | + Chat.poll(). | |
| 265 | + */ | |
| 266 | + cancelPendingPollTimer: function(){ | |
| 267 | + if( this.tidPendingPoll ){ | |
| 268 | + clearTimeout(this.tidPendingPoll); | |
| 269 | + this.tidPendingPoll = 0; | |
| 270 | + } | |
| 271 | + return this; | |
| 272 | + }, | |
| 273 | + /** | |
| 274 | + Cancels any pending reconnection attempt back-off timer.. | |
| 275 | + */ | |
| 276 | + cancelReconnectCheckTimer: function(){ | |
| 277 | + if( this.tidClearPollErr ){ | |
| 278 | + clearTimeout(this.tidClearPollErr); | |
| 279 | + this.tidClearPollErr = 0; | |
| 280 | + } | |
| 281 | + return this; | |
| 282 | + } | |
| 184 | 283 | }, |
| 185 | 284 | /** |
| 186 | 285 | Gets (no args) or sets (1 arg) the current input text field |
| 187 | 286 | value, taking into account single- vs multi-line input. The |
| 188 | 287 | getter returns a trim()'d string and the setter returns this |
| @@ -606,19 +705,19 @@ | ||
| 606 | 705 | |
| 607 | 706 | /** |
| 608 | 707 | If animations are enabled, passes its arguments |
| 609 | 708 | to D.addClassBriefly(), else this is a no-op. |
| 610 | 709 | If cb is a function, it is called after the |
| 611 | - CSS class is removed. Returns this object; | |
| 710 | + CSS class is removed. Returns this object; | |
| 612 | 711 | */ |
| 613 | 712 | animate: function f(e,a,cb){ |
| 614 | 713 | if(!f.$disabled){ |
| 615 | 714 | D.addClassBriefly(e, a, 0, cb); |
| 616 | 715 | } |
| 617 | 716 | return this; |
| 618 | 717 | } |
| 619 | - }; | |
| 718 | + }/*Chat object*/; | |
| 620 | 719 | cs.e.inputFields = [ cs.e.input1, cs.e.inputM, cs.e.inputX ]; |
| 621 | 720 | cs.e.inputFields.$currentIndex = 0; |
| 622 | 721 | cs.e.inputFields.forEach(function(e,ndx){ |
| 623 | 722 | if(ndx===cs.e.inputFields.$currentIndex) D.removeClass(e,'hidden'); |
| 624 | 723 | else D.addClass(e,'hidden'); |
| @@ -645,33 +744,59 @@ | ||
| 645 | 744 | cs.reportError = function(/*msg args*/){ |
| 646 | 745 | const args = argsToArray(arguments); |
| 647 | 746 | console.error("chat error:",args); |
| 648 | 747 | F.toast.error.apply(F.toast, args); |
| 649 | 748 | }; |
| 749 | + | |
| 750 | + let InternalMsgId = 0; | |
| 650 | 751 | /** |
| 651 | 752 | Reports an error in the form of a new message in the chat |
| 652 | 753 | feed. All arguments are appended to the message's content area |
| 653 | 754 | using fossil.dom.append(), so may be of any type supported by |
| 654 | 755 | that function. |
| 655 | 756 | */ |
| 656 | 757 | cs.reportErrorAsMessage = function f(/*msg args*/){ |
| 657 | - if(undefined === f.$msgid) f.$msgid=0; | |
| 658 | 758 | const args = argsToArray(arguments).map(function(v){ |
| 659 | 759 | return (v instanceof Error) ? v.message : v; |
| 660 | 760 | }); |
| 661 | - console.error("chat error:",args); | |
| 761 | + if(Chat.beVerbose){ | |
| 762 | + console.error("chat error:",args); | |
| 763 | + } | |
| 662 | 764 | const d = new Date().toISOString(), |
| 663 | 765 | mw = new this.MessageWidget({ |
| 664 | 766 | isError: true, |
| 665 | - xfrom: null, | |
| 666 | - msgid: "error-"+(++f.$msgid), | |
| 767 | + xfrom: undefined, | |
| 768 | + msgid: "error-"+(++InternalMsgId), | |
| 769 | + mtime: d, | |
| 770 | + lmtime: d, | |
| 771 | + xmsg: args | |
| 772 | + }); | |
| 773 | + this.injectMessageElem(mw.e.body); | |
| 774 | + mw.scrollIntoView(); | |
| 775 | + return mw; | |
| 776 | + }; | |
| 777 | + | |
| 778 | + /** | |
| 779 | + For use by the connection poller to send a "connection | |
| 780 | + restored" message. | |
| 781 | + */ | |
| 782 | + cs.reportReconnection = function f(/*msg args*/){ | |
| 783 | + const args = argsToArray(arguments).map(function(v){ | |
| 784 | + return (v instanceof Error) ? v.message : v; | |
| 785 | + }); | |
| 786 | + const d = new Date().toISOString(), | |
| 787 | + mw = new this.MessageWidget({ | |
| 788 | + isError: false, | |
| 789 | + xfrom: undefined, | |
| 790 | + msgid: "reconnect-"+(++InternalMsgId), | |
| 667 | 791 | mtime: d, |
| 668 | 792 | lmtime: d, |
| 669 | 793 | xmsg: args |
| 670 | 794 | }); |
| 671 | 795 | this.injectMessageElem(mw.e.body); |
| 672 | 796 | mw.scrollIntoView(); |
| 797 | + return mw; | |
| 673 | 798 | }; |
| 674 | 799 | |
| 675 | 800 | cs.getMessageElemById = function(id){ |
| 676 | 801 | return qs('[data-msgid="'+id+'"]'); |
| 677 | 802 | }; |
| @@ -690,24 +815,40 @@ | ||
| 690 | 815 | /** |
| 691 | 816 | LOCALLY deletes a message element by the message ID or passing |
| 692 | 817 | the .message-row element. Returns true if it removes an element, |
| 693 | 818 | else false. |
| 694 | 819 | */ |
| 695 | - cs.deleteMessageElem = function(id){ | |
| 820 | + cs.deleteMessageElem = function(id, silent){ | |
| 696 | 821 | var e; |
| 697 | 822 | if(id instanceof HTMLElement){ |
| 698 | 823 | e = id; |
| 699 | 824 | id = e.dataset.msgid; |
| 700 | - }else{ | |
| 825 | + delete e.dataset.msgid; | |
| 826 | + if( e?.dataset?.alsoRemove ){ | |
| 827 | + const xId = e.dataset.alsoRemove; | |
| 828 | + delete e.dataset.alsoRemove; | |
| 829 | + this.deleteMessageElem( xId ); | |
| 830 | + } | |
| 831 | + }else if(id instanceof Chat.MessageWidget) { | |
| 832 | + if( this.e.eMsgPollError === e ){ | |
| 833 | + this.e.eMsgPollError = undefined; | |
| 834 | + } | |
| 835 | + if(id.e?.body){ | |
| 836 | + this.deleteMessageElem(id.e.body); | |
| 837 | + } | |
| 838 | + return; | |
| 839 | + } else{ | |
| 701 | 840 | e = this.getMessageElemById(id); |
| 702 | 841 | } |
| 703 | 842 | if(e && id){ |
| 704 | 843 | D.remove(e); |
| 705 | 844 | if(e===this.e.newestMessage){ |
| 706 | 845 | this.fetchLastMessageElem(); |
| 707 | 846 | } |
| 708 | - F.toast.message("Deleted message "+id+"."); | |
| 847 | + if( !silent ){ | |
| 848 | + F.toast.message("Deleted message "+id+"."); | |
| 849 | + } | |
| 709 | 850 | } |
| 710 | 851 | return !!e; |
| 711 | 852 | }; |
| 712 | 853 | |
| 713 | 854 | /** |
| @@ -776,10 +917,11 @@ | ||
| 776 | 917 | const self = this; |
| 777 | 918 | F.fetch('chat-fetch-one',{ |
| 778 | 919 | urlParams:{ name: id, raw: true}, |
| 779 | 920 | responseType: 'json', |
| 780 | 921 | onload: function(msg){ |
| 922 | + reportConnectionOkay('chat-fetch-one'); | |
| 781 | 923 | content.$elems[1] = D.append(D.pre(),msg.xmsg); |
| 782 | 924 | content.$elems[1]._xmsgRaw = msg.xmsg/*used for copy-to-clipboard feature*/; |
| 783 | 925 | self.toggleTextMode(e); |
| 784 | 926 | }, |
| 785 | 927 | aftersend:function(){ |
| @@ -834,14 +976,16 @@ | ||
| 834 | 976 | }else{ |
| 835 | 977 | e = this.getMessageElemById(id); |
| 836 | 978 | } |
| 837 | 979 | if(!(e instanceof HTMLElement)) return; |
| 838 | 980 | if(this.userMayDelete(e)){ |
| 839 | - this.ajaxStart(); | |
| 840 | 981 | F.fetch("chat-delete/" + id, { |
| 841 | 982 | responseType: 'json', |
| 842 | - onload:(r)=>this.deleteMessageElem(r), | |
| 983 | + onload:(r)=>{ | |
| 984 | + reportConnectionOkay('chat-delete'); | |
| 985 | + this.deleteMessageElem(r); | |
| 986 | + }, | |
| 843 | 987 | onerror:(err)=>this.reportErrorAsMessage(err) |
| 844 | 988 | }); |
| 845 | 989 | }else{ |
| 846 | 990 | this.deleteMessageElem(id); |
| 847 | 991 | } |
| @@ -1035,10 +1179,11 @@ | ||
| 1035 | 1179 | |
| 1036 | 1180 | ctor.prototype = { |
| 1037 | 1181 | scrollIntoView: function(){ |
| 1038 | 1182 | this.e.content.scrollIntoView(); |
| 1039 | 1183 | }, |
| 1184 | + //remove: function(silent){Chat.deleteMessageElem(this, silent);}, | |
| 1040 | 1185 | setMessage: function(m){ |
| 1041 | 1186 | const ds = this.e.body.dataset; |
| 1042 | 1187 | ds.timestamp = m.mtime; |
| 1043 | 1188 | ds.lmtime = m.lmtime; |
| 1044 | 1189 | ds.msgid = m.msgid; |
| @@ -1212,12 +1357,22 @@ | ||
| 1212 | 1357 | const btnDeleteLocal = D.button("Delete locally"); |
| 1213 | 1358 | D.append(toolbar, btnDeleteLocal); |
| 1214 | 1359 | const self = this; |
| 1215 | 1360 | btnDeleteLocal.addEventListener('click', function(){ |
| 1216 | 1361 | self.hide(); |
| 1217 | - Chat.deleteMessageElem(eMsg); | |
| 1362 | + Chat.deleteMessageElem(eMsg) | |
| 1218 | 1363 | }); |
| 1364 | + if( eMsg.classList.contains('notification') ){ | |
| 1365 | + const btnDeletePoll = D.button("Delete /chat notifications?"); | |
| 1366 | + D.append(toolbar, btnDeletePoll); | |
| 1367 | + btnDeletePoll.addEventListener('click', function(){ | |
| 1368 | + self.hide(); | |
| 1369 | + Chat.e.viewMessages.querySelectorAll( | |
| 1370 | + '.message-widget.notification:not(.resend-message)' | |
| 1371 | + ).forEach(e=>Chat.deleteMessageElem(e, true)); | |
| 1372 | + }); | |
| 1373 | + } | |
| 1219 | 1374 | if(Chat.userMayDelete(eMsg)){ |
| 1220 | 1375 | const btnDeleteGlobal = D.button("Delete globally"); |
| 1221 | 1376 | D.append(toolbar, btnDeleteGlobal); |
| 1222 | 1377 | F.confirmer(btnDeleteGlobal,{ |
| 1223 | 1378 | pinSize: true, |
| @@ -1457,10 +1612,11 @@ | ||
| 1457 | 1612 | n: nFetch, |
| 1458 | 1613 | i: iFirst |
| 1459 | 1614 | }, |
| 1460 | 1615 | responseType: "json", |
| 1461 | 1616 | onload:function(jx){ |
| 1617 | + reportConnectionOkay('chat-query'); | |
| 1462 | 1618 | if( bDown ) jx.msgs.reverse(); |
| 1463 | 1619 | jx.msgs.forEach((m) => { |
| 1464 | 1620 | m.isSearchResult = true; |
| 1465 | 1621 | var mw = new Chat.MessageWidget(m); |
| 1466 | 1622 | if( bDown ){ |
| @@ -1524,11 +1680,11 @@ | ||
| 1524 | 1680 | reader.onload = (e)=>img.setAttribute('src', e.target.result); |
| 1525 | 1681 | reader.readAsDataURL(blob); |
| 1526 | 1682 | } |
| 1527 | 1683 | }; |
| 1528 | 1684 | Chat.e.inputFile.addEventListener('change', function(ev){ |
| 1529 | - updateDropZoneContent(this.files && this.files[0] ? this.files[0] : undefined) | |
| 1685 | + updateDropZoneContent(this?.files[0]) | |
| 1530 | 1686 | }); |
| 1531 | 1687 | /* Handle image paste from clipboard. TODO: figure out how we can |
| 1532 | 1688 | paste non-image binary data as if it had been selected via the |
| 1533 | 1689 | file selection element. */ |
| 1534 | 1690 | const pasteListener = function(event){ |
| @@ -1604,10 +1760,11 @@ | ||
| 1604 | 1760 | D.span(),"This message was not successfully sent to the server:" |
| 1605 | 1761 | )); |
| 1606 | 1762 | if(state.msg){ |
| 1607 | 1763 | const ta = D.textarea(); |
| 1608 | 1764 | ta.value = state.msg; |
| 1765 | + ta.setAttribute('readonly','true'); | |
| 1609 | 1766 | D.append(w,ta); |
| 1610 | 1767 | } |
| 1611 | 1768 | if(state.blob){ |
| 1612 | 1769 | D.append(w,D.append(D.span(),"Attachment: ",(state.blob.name||"unnamed"))); |
| 1613 | 1770 | //console.debug("blob = ",state.blob); |
| @@ -1622,11 +1779,46 @@ | ||
| 1622 | 1779 | if(state.msg) Chat.inputValue(state.msg); |
| 1623 | 1780 | if(state.blob) BlobXferState.updateDropZoneContent(state.blob); |
| 1624 | 1781 | const theMsg = findMessageWidgetParent(w); |
| 1625 | 1782 | if(theMsg) Chat.deleteMessageElem(theMsg); |
| 1626 | 1783 | })); |
| 1627 | - Chat.reportErrorAsMessage(w); | |
| 1784 | + D.addClass(Chat.reportErrorAsMessage(w).e.body, "resend-message"); | |
| 1785 | + }; | |
| 1786 | + | |
| 1787 | + /* Assume the connection has been established, reset the | |
| 1788 | + Chat.timer.tidClearPollErr, and (if showMsg and | |
| 1789 | + !!Chat.e.eMsgPollError) alert the user that the outage appears to | |
| 1790 | + be over. Also schedule Chat.poll() to run in the very near | |
| 1791 | + future. */ | |
| 1792 | + const reportConnectionOkay = function(dbgContext, showMsg = true){ | |
| 1793 | + if(Chat.beVerbose){ | |
| 1794 | + console.warn('reportConnectionOkay', dbgContext, | |
| 1795 | + 'Chat.e.pollErrorMarker classes =', | |
| 1796 | + Chat.e.pollErrorMarker.classList, | |
| 1797 | + 'Chat.timer.tidClearPollErr =',Chat.timer.tidClearPollErr, | |
| 1798 | + 'Chat.timer =',Chat.timer); | |
| 1799 | + } | |
| 1800 | + if( Chat.timer.errCount ){ | |
| 1801 | + D.removeClass(Chat.e.pollErrorMarker, 'connection-error'); | |
| 1802 | + Chat.timer.errCount = 0; | |
| 1803 | + } | |
| 1804 | + Chat.timer.cancelReconnectCheckTimer().startPendingPollTimer(); | |
| 1805 | + if( Chat.e.eMsgPollError ) { | |
| 1806 | + const oldErrMsg = Chat.e.eMsgPollError; | |
| 1807 | + Chat.e.eMsgPollError = undefined; | |
| 1808 | + if( showMsg ){ | |
| 1809 | + if(Chat.beVerbose){ | |
| 1810 | + console.log("Poller Connection restored."); | |
| 1811 | + } | |
| 1812 | + const m = Chat.reportReconnection("Poller connection restored."); | |
| 1813 | + if( oldErrMsg ){ | |
| 1814 | + D.remove(oldErrMsg.e?.body.querySelector('button.retry-now')); | |
| 1815 | + } | |
| 1816 | + m.e.body.dataset.alsoRemove = oldErrMsg?.e?.body?.dataset?.msgid; | |
| 1817 | + D.addClass(m.e.body,'poller-connection'); | |
| 1818 | + } | |
| 1819 | + } | |
| 1628 | 1820 | }; |
| 1629 | 1821 | |
| 1630 | 1822 | /** |
| 1631 | 1823 | Submits the contents of the message input field (if not empty) |
| 1632 | 1824 | and/or the file attachment field to the server. If both are |
| @@ -1686,10 +1878,11 @@ | ||
| 1686 | 1878 | onerror:function(err){ |
| 1687 | 1879 | self.reportErrorAsMessage(err); |
| 1688 | 1880 | recoverFailedMessage(fallback); |
| 1689 | 1881 | }, |
| 1690 | 1882 | onload:function(txt){ |
| 1883 | + reportConnectionOkay('chat-send'); | |
| 1691 | 1884 | if(!txt) return/*success response*/; |
| 1692 | 1885 | try{ |
| 1693 | 1886 | const json = JSON.parse(txt); |
| 1694 | 1887 | self.newContent({msgs:[json]}); |
| 1695 | 1888 | }catch(e){ |
| @@ -2126,11 +2319,11 @@ | ||
| 2126 | 2319 | Chat.e.inputFields.$currentIndex = a[2]; |
| 2127 | 2320 | Chat.inputValue(v); |
| 2128 | 2321 | D.removeClass(a[0], 'hidden'); |
| 2129 | 2322 | D.addClass(a[1], 'hidden'); |
| 2130 | 2323 | } |
| 2131 | - Chat.e.inputElementWrapper.classList[ | |
| 2324 | + Chat.e.inputLineWrapper.classList[ | |
| 2132 | 2325 | s.value ? 'add' : 'remove' |
| 2133 | 2326 | ]('compact'); |
| 2134 | 2327 | Chat.e.inputFields[Chat.e.inputFields.$currentIndex].focus(); |
| 2135 | 2328 | }); |
| 2136 | 2329 | Chat.settings.addListener('edit-ctrl-send',function(s){ |
| @@ -2185,10 +2378,11 @@ | ||
| 2185 | 2378 | /*filename needed for mimetype determination*/); |
| 2186 | 2379 | fd.append('render_mode',F.page.previewModes.wiki); |
| 2187 | 2380 | F.fetch('ajax/preview-text',{ |
| 2188 | 2381 | payload: fd, |
| 2189 | 2382 | onload: function(html){ |
| 2383 | + reportConnectionOkay('ajax/preview-text'); | |
| 2190 | 2384 | Chat.setPreviewText(html); |
| 2191 | 2385 | F.pikchr.addSrcView(Chat.e.viewPreview.querySelectorAll('svg.pikchr')); |
| 2192 | 2386 | }, |
| 2193 | 2387 | onerror: function(e){ |
| 2194 | 2388 | F.fetch.onerror(e); |
| @@ -2322,10 +2516,11 @@ | ||
| 2322 | 2516 | onerror:function(err){ |
| 2323 | 2517 | Chat.reportErrorAsMessage(err); |
| 2324 | 2518 | Chat._isBatchLoading = false; |
| 2325 | 2519 | }, |
| 2326 | 2520 | onload:function(x){ |
| 2521 | + reportConnectionOkay('loadOldMessages()'); | |
| 2327 | 2522 | let gotMessages = x.msgs.length; |
| 2328 | 2523 | newcontent(x,true); |
| 2329 | 2524 | Chat._isBatchLoading = false; |
| 2330 | 2525 | Chat.updateActiveUserList(); |
| 2331 | 2526 | if(Chat._gotServerError){ |
| @@ -2411,10 +2606,11 @@ | ||
| 2411 | 2606 | onerror:function(err){ |
| 2412 | 2607 | Chat.setCurrentView(Chat.e.viewMessages); |
| 2413 | 2608 | Chat.reportErrorAsMessage(err); |
| 2414 | 2609 | }, |
| 2415 | 2610 | onload:function(jx){ |
| 2611 | + reportConnectionOkay('submitSearch()'); | |
| 2416 | 2612 | let previd = 0; |
| 2417 | 2613 | D.clearElement(eMsgTgt); |
| 2418 | 2614 | jx.msgs.forEach((m)=>{ |
| 2419 | 2615 | m.isSearchResult = true; |
| 2420 | 2616 | const mw = new Chat.MessageWidget(m); |
| @@ -2444,87 +2640,210 @@ | ||
| 2444 | 2640 | } |
| 2445 | 2641 | } |
| 2446 | 2642 | ); |
| 2447 | 2643 | }/*Chat.submitSearch()*/; |
| 2448 | 2644 | |
| 2449 | - const afterFetch = function f(){ | |
| 2645 | + /* | |
| 2646 | + To be called from F.fetch('chat-poll') beforesend() handler. If | |
| 2647 | + we're currently in delayed-retry mode and a connection is | |
| 2648 | + started, try to reset the delay after N time waiting on that | |
| 2649 | + connection. The fact that the connection is waiting to respond, | |
| 2650 | + rather than outright failing, is a good hint that the outage is | |
| 2651 | + over and we can reset the back-off timer. | |
| 2652 | + | |
| 2653 | + Without this, recovery of a connection error won't be reported | |
| 2654 | + until after the long-poll completes by either receiving new | |
| 2655 | + messages or timing out. Once a long-poll is in progress, though, | |
| 2656 | + we "know" that it's up and running again, so can update the UI and | |
| 2657 | + connection timer to reflect that. That's the job this function | |
| 2658 | + does. | |
| 2659 | + | |
| 2660 | + Only one of these asynchronous checks will ever be active | |
| 2661 | + concurrently and only if Chat.timer.isDelayed() is true. i.e. if | |
| 2662 | + this timer is active or Chat.timer.isDelayed() is false, this is a | |
| 2663 | + no-op. | |
| 2664 | + */ | |
| 2665 | + const chatPollBeforeSend = function(){ | |
| 2666 | + //console.warn('chatPollBeforeSend outer', Chat.timer.tidClearPollErr, Chat.timer.currentDelay); | |
| 2667 | + if( !Chat.timer.tidClearPollErr && Chat.timer.isDelayed() ){ | |
| 2668 | + Chat.timer.tidClearPollErr = setTimeout(()=>{ | |
| 2669 | + //console.warn('chatPollBeforeSend inner'); | |
| 2670 | + Chat.timer.tidClearPollErr = 0; | |
| 2671 | + if( poll.running ){ | |
| 2672 | + /* This chat-poll F.fetch() is still underway, so let's | |
| 2673 | + assume the connection is back up until/unless it times | |
| 2674 | + out or breaks again. */ | |
| 2675 | + reportConnectionOkay('chatPollBeforeSend', true); | |
| 2676 | + } | |
| 2677 | + }, Chat.timer.$initialDelay * 4/*kinda arbitrary: not too long for UI wait and | |
| 2678 | + not too short as to make connection unlikely. */ ); | |
| 2679 | + } | |
| 2680 | + }; | |
| 2681 | + | |
| 2682 | + /** | |
| 2683 | + Deal with the last poll() response and maybe re-start poll(). | |
| 2684 | + */ | |
| 2685 | + const afterPollFetch = function f(err){ | |
| 2450 | 2686 | if(true===f.isFirstCall){ |
| 2451 | 2687 | f.isFirstCall = false; |
| 2452 | 2688 | Chat.ajaxEnd(); |
| 2453 | 2689 | Chat.e.viewMessages.classList.remove('loading'); |
| 2454 | 2690 | setTimeout(function(){ |
| 2455 | 2691 | Chat.scrollMessagesTo(1); |
| 2456 | 2692 | }, 250); |
| 2457 | 2693 | } |
| 2458 | - if(Chat._gotServerError && Chat.intervalTimer){ | |
| 2459 | - clearInterval(Chat.intervalTimer); | |
| 2694 | + Chat.timer.cancelPendingPollTimer(); | |
| 2695 | + if(Chat._gotServerError){ | |
| 2460 | 2696 | Chat.reportErrorAsMessage( |
| 2461 | 2697 | "Shutting down chat poller due to server-side error. ", |
| 2462 | - "Reload this page to reactivate it."); | |
| 2463 | - delete Chat.intervalTimer; | |
| 2464 | - } | |
| 2465 | - poll.running = false; | |
| 2466 | - }; | |
| 2467 | - afterFetch.isFirstCall = true; | |
| 2468 | - /** | |
| 2469 | - FIXME: when polling fails because the remote server is | |
| 2470 | - reachable but it's not accepting HTTP requests, we should back | |
| 2471 | - off on polling for a while. e.g. if the remote web server process | |
| 2472 | - is killed, the poll fails quickly and immediately retries, | |
| 2473 | - hammering the remote server until the httpd is back up. That | |
| 2474 | - happens often during development of this application. | |
| 2475 | - | |
| 2476 | - XHR does not offer a direct way of distinguishing between | |
| 2477 | - HTTP/connection errors, but we can hypothetically use the | |
| 2478 | - xhrRequest.status value to do so, with status==0 being a | |
| 2479 | - connection error. We do not currently have a clean way of passing | |
| 2480 | - that info back to the fossil.fetch() client, so we'll need to | |
| 2481 | - hammer on that API a bit to get this working. | |
| 2482 | - */ | |
| 2483 | - const poll = async function f(){ | |
| 2698 | + "Reload this page to reactivate it." | |
| 2699 | + ); | |
| 2700 | + } else { | |
| 2701 | + if( err && Chat.beVerbose ){ | |
| 2702 | + console.error("afterPollFetch:",err.name,err.status,err.message); | |
| 2703 | + } | |
| 2704 | + if( !err || 'timeout'===err.name/*(probably) long-poll expired*/ ){ | |
| 2705 | + /* Restart the poller immediately. */ | |
| 2706 | + reportConnectionOkay('afterPollFetch '+err, false); | |
| 2707 | + }else{ | |
| 2708 | + /* Delay a while before trying again, noting that other Chat | |
| 2709 | + APIs may try and succeed at connections before this timer | |
| 2710 | + resolves, in which case they'll clear this timeout and the | |
| 2711 | + UI message about the outage. */ | |
| 2712 | + let delay; | |
| 2713 | + D.addClass(Chat.e.pollErrorMarker, 'connection-error'); | |
| 2714 | + if( ++Chat.timer.errCount < Chat.timer.minErrForNotify ){ | |
| 2715 | + delay = Chat.timer.resetDelay( | |
| 2716 | + (Chat.timer.minDelay * Chat.timer.errCount) | |
| 2717 | + + Chat.timer.randomInterval(Chat.timer.minDelay) | |
| 2718 | + ); | |
| 2719 | + if(Chat.beVerbose){ | |
| 2720 | + console.warn("Ignoring polling error #",Chat.timer.errCount, | |
| 2721 | + "for another",delay,"ms" ); | |
| 2722 | + } | |
| 2723 | + } else { | |
| 2724 | + delay = Chat.timer.incrDelay(); | |
| 2725 | + //console.warn("afterPollFetch Chat.e.eMsgPollError",Chat.e.eMsgPollError); | |
| 2726 | + const msg = "Poller connection error. Retrying in "+delay+ " ms."; | |
| 2727 | + /* Replace the current/newest connection error widget. We could also | |
| 2728 | + just update its body with the new message, but then its timestamp | |
| 2729 | + never updates. OTOH, if we replace the message, we lose the | |
| 2730 | + start time of the outage in the log. It seems more useful to | |
| 2731 | + update the timestamp so that it doesn't look like it's hung. */ | |
| 2732 | + if( Chat.e.eMsgPollError ){ | |
| 2733 | + Chat.deleteMessageElem(Chat.e.eMsgPollError, false); | |
| 2734 | + } | |
| 2735 | + const theMsg = Chat.e.eMsgPollError = Chat.reportErrorAsMessage(msg); | |
| 2736 | + D.addClass(Chat.e.eMsgPollError.e.body,'poller-connection'); | |
| 2737 | + /* Add a "retry now" button */ | |
| 2738 | + const btnDel = D.addClass(D.button("Retry now"), 'retry-now'); | |
| 2739 | + const eParent = Chat.e.eMsgPollError.e.content; | |
| 2740 | + D.append(eParent, " ", btnDel); | |
| 2741 | + btnDel.addEventListener('click', function(){ | |
| 2742 | + D.remove(btnDel); | |
| 2743 | + D.append(eParent, D.text("retrying...")); | |
| 2744 | + Chat.timer.cancelPendingPollTimer().currentDelay = | |
| 2745 | + Chat.timer.resetDelay() + | |
| 2746 | + 1 /*workaround for showing the "connection restored" | |
| 2747 | + message, as the +1 will cause | |
| 2748 | + Chat.timer.isDelayed() to be true.*/; | |
| 2749 | + poll(); | |
| 2750 | + }); | |
| 2751 | + //Chat.playNewMessageSound();// browser complains b/c this wasn't via human interaction | |
| 2752 | + } | |
| 2753 | + Chat.timer.startPendingPollTimer(delay); | |
| 2754 | + } | |
| 2755 | + } | |
| 2756 | + }; | |
| 2757 | + afterPollFetch.isFirstCall = true; | |
| 2758 | + | |
| 2759 | + /** | |
| 2760 | + Initiates, if it's not already running, a single long-poll | |
| 2761 | + request to the /chat-poll endpoint. In the handling of that | |
| 2762 | + response, it end up will psuedo-recursively calling itself via | |
| 2763 | + the response-handling process. Despite being async, the implied | |
| 2764 | + returned Promise is meaningless. | |
| 2765 | + */ | |
| 2766 | + const poll = Chat.poll = async function f(){ | |
| 2484 | 2767 | if(f.running) return; |
| 2485 | 2768 | f.running = true; |
| 2486 | 2769 | Chat._isBatchLoading = f.isFirstCall; |
| 2487 | 2770 | if(true===f.isFirstCall){ |
| 2488 | 2771 | f.isFirstCall = false; |
| 2772 | + f.pendingOnError = undefined; | |
| 2489 | 2773 | Chat.ajaxStart(); |
| 2490 | 2774 | Chat.e.viewMessages.classList.add('loading'); |
| 2775 | + /* | |
| 2776 | + We manager onerror() results in poll() in a roundabout | |
| 2777 | + manner: when an onerror() arrives, we stash it aside | |
| 2778 | + for a moment before processing it. | |
| 2779 | + | |
| 2780 | + This level of indirection is necessary to be able to | |
| 2781 | + unambiguously identify client-timeout-specific polling errors | |
| 2782 | + from other errors. Timeouts are always announced in pairs of | |
| 2783 | + an HTTP 0 and something we can unambiguously identify as a | |
| 2784 | + timeout (in that order). When we receive an HTTP error we put | |
| 2785 | + it into this queue. If an ontimeout() call arrives before | |
| 2786 | + this error is handled, this error is ignored. If, however, an | |
| 2787 | + HTTP error is seen without an accompanying timeout, we handle | |
| 2788 | + it from here. | |
| 2789 | + | |
| 2790 | + It's kinda like in the curses C API, where you to match | |
| 2791 | + ALT-X by first getting an ESC event, then an X event, but | |
| 2792 | + this one is a lot less explicable. (It's almost certainly a | |
| 2793 | + mis-handling bug in F.fetch(), but it has so far eluded my | |
| 2794 | + eyes.) | |
| 2795 | + */ | |
| 2796 | + f.delayPendingOnError = function(err){ | |
| 2797 | + if( f.pendingOnError ){ | |
| 2798 | + const x = f.pendingOnError; | |
| 2799 | + f.pendingOnError = undefined; | |
| 2800 | + afterPollFetch(x); | |
| 2801 | + } | |
| 2802 | + }; | |
| 2491 | 2803 | } |
| 2492 | 2804 | F.fetch("chat-poll",{ |
| 2493 | - timeout: 420 * 1000/*FIXME: get the value from the server*/, | |
| 2805 | + timeout: Chat.timer.pollTimeout, | |
| 2494 | 2806 | urlParams:{ |
| 2495 | 2807 | name: Chat.mxMsg |
| 2496 | 2808 | }, |
| 2497 | 2809 | responseType: "json", |
| 2498 | 2810 | // Disable the ajax start/end handling for this long-polling op: |
| 2499 | - beforesend: function(){}, | |
| 2500 | - aftersend: function(){}, | |
| 2811 | + beforesend: chatPollBeforeSend, | |
| 2812 | + aftersend: function(){ | |
| 2813 | + poll.running = false; | |
| 2814 | + }, | |
| 2815 | + ontimeout: function(err){ | |
| 2816 | + f.pendingOnError = undefined /*strip preceeding non-timeout error, if any*/; | |
| 2817 | + afterPollFetch(err); | |
| 2818 | + }, | |
| 2501 | 2819 | onerror:function(err){ |
| 2502 | 2820 | Chat._isBatchLoading = false; |
| 2503 | - if(Chat.verboseErrors) console.error(err); | |
| 2504 | - /* ^^^ we don't use Chat.reportError() here b/c the polling | |
| 2505 | - fails exepectedly when it times out, but is then immediately | |
| 2506 | - resumed, and reportError() produces a loud error message. */ | |
| 2507 | - afterFetch(); | |
| 2821 | + if(Chat.beVerbose){ | |
| 2822 | + console.error("poll.onerror:",err.name,err.status,JSON.stringify(err)); | |
| 2823 | + } | |
| 2824 | + f.pendingOnError = err; | |
| 2825 | + setTimeout(f.delayPendingOnError, 100); | |
| 2508 | 2826 | }, |
| 2509 | 2827 | onload:function(y){ |
| 2828 | + reportConnectionOkay('poll.onload', true); | |
| 2510 | 2829 | newcontent(y); |
| 2511 | 2830 | if(Chat._isBatchLoading){ |
| 2512 | 2831 | Chat._isBatchLoading = false; |
| 2513 | 2832 | Chat.updateActiveUserList(); |
| 2514 | 2833 | } |
| 2515 | - afterFetch(); | |
| 2834 | + afterPollFetch(); | |
| 2516 | 2835 | } |
| 2517 | 2836 | }); |
| 2518 | - }; | |
| 2837 | + }/*poll()*/; | |
| 2519 | 2838 | poll.isFirstCall = true; |
| 2520 | 2839 | Chat._gotServerError = poll.running = false; |
| 2521 | 2840 | if( window.fossil.config.chat.fromcli ){ |
| 2522 | 2841 | Chat.chatOnlyMode(true); |
| 2523 | 2842 | } |
| 2524 | - Chat.intervalTimer = setInterval(poll, 1000); | |
| 2843 | + Chat.timer.startPendingPollTimer(); | |
| 2525 | 2844 | delete ForceResizeKludge.$disabled; |
| 2526 | 2845 | ForceResizeKludge(); |
| 2527 | 2846 | Chat.animate.$disabled = false; |
| 2528 | 2847 | setTimeout( ()=>Chat.inputFocus(), 0 ); |
| 2529 | 2848 | F.page.chat = Chat/* enables testing the APIs via the dev tools */; |
| 2530 | 2849 | }); |
| 2531 | 2850 |
| --- src/fossil.page.chat.js | |
| +++ src/fossil.page.chat.js | |
| @@ -1,6 +1,6 @@ | |
| 1 | /** |
| 2 | This file contains the client-side implementation of fossil's /chat |
| 3 | application. |
| 4 | */ |
| 5 | window.fossil.onPageLoad(function(){ |
| 6 | const F = window.fossil, D = F.dom; |
| @@ -129,19 +129,21 @@ | |
| 129 | return resized; |
| 130 | })(); |
| 131 | fossil.FRK = ForceResizeKludge/*for debugging*/; |
| 132 | const Chat = ForceResizeKludge.chat = (function(){ |
| 133 | const cs = { // the "Chat" object (result of this function) |
| 134 | verboseErrors: false /* if true then certain, mostly extraneous, |
| 135 | error messages may be sent to the console. */, |
| 136 | playedBeep: false /* used for the beep-once setting */, |
| 137 | e:{/*map of certain DOM elements.*/ |
| 138 | messageInjectPoint: E1('#message-inject-point'), |
| 139 | pageTitle: E1('head title'), |
| 140 | loadOlderToolbar: undefined /* the load-posts toolbar (dynamically created) */, |
| 141 | inputWrapper: E1("#chat-input-area"), |
| 142 | inputElementWrapper: E1('#chat-input-line-wrapper'), |
| 143 | fileSelectWrapper: E1('#chat-input-file-area'), |
| 144 | viewMessages: E1('#chat-messages-wrapper'), |
| 145 | btnSubmit: E1('#chat-button-submit'), |
| 146 | btnAttach: E1('#chat-button-attach'), |
| 147 | inputX: E1('#chat-input-field-x'), |
| @@ -155,11 +157,13 @@ | |
| 155 | viewSearch: E1('#chat-search'), |
| 156 | searchContent: E1('#chat-search-content'), |
| 157 | btnPreview: E1('#chat-button-preview'), |
| 158 | views: document.querySelectorAll('.chat-view'), |
| 159 | activeUserListWrapper: E1('#chat-user-list-wrapper'), |
| 160 | activeUserList: E1('#chat-user-list') |
| 161 | }, |
| 162 | me: F.user.name, |
| 163 | mxMsg: F.config.chat.initSize ? -F.config.chat.initSize : -50, |
| 164 | mnMsg: undefined/*lowest message ID we've seen so far (for history loading)*/, |
| 165 | pageIsActive: 'visible'===document.visibilityState, |
| @@ -179,10 +183,105 @@ | |
| 179 | filterState:{ |
| 180 | activeUser: undefined, |
| 181 | match: function(uname){ |
| 182 | return this.activeUser===uname || !this.activeUser; |
| 183 | } |
| 184 | }, |
| 185 | /** |
| 186 | Gets (no args) or sets (1 arg) the current input text field |
| 187 | value, taking into account single- vs multi-line input. The |
| 188 | getter returns a trim()'d string and the setter returns this |
| @@ -606,19 +705,19 @@ | |
| 606 | |
| 607 | /** |
| 608 | If animations are enabled, passes its arguments |
| 609 | to D.addClassBriefly(), else this is a no-op. |
| 610 | If cb is a function, it is called after the |
| 611 | CSS class is removed. Returns this object; |
| 612 | */ |
| 613 | animate: function f(e,a,cb){ |
| 614 | if(!f.$disabled){ |
| 615 | D.addClassBriefly(e, a, 0, cb); |
| 616 | } |
| 617 | return this; |
| 618 | } |
| 619 | }; |
| 620 | cs.e.inputFields = [ cs.e.input1, cs.e.inputM, cs.e.inputX ]; |
| 621 | cs.e.inputFields.$currentIndex = 0; |
| 622 | cs.e.inputFields.forEach(function(e,ndx){ |
| 623 | if(ndx===cs.e.inputFields.$currentIndex) D.removeClass(e,'hidden'); |
| 624 | else D.addClass(e,'hidden'); |
| @@ -645,33 +744,59 @@ | |
| 645 | cs.reportError = function(/*msg args*/){ |
| 646 | const args = argsToArray(arguments); |
| 647 | console.error("chat error:",args); |
| 648 | F.toast.error.apply(F.toast, args); |
| 649 | }; |
| 650 | /** |
| 651 | Reports an error in the form of a new message in the chat |
| 652 | feed. All arguments are appended to the message's content area |
| 653 | using fossil.dom.append(), so may be of any type supported by |
| 654 | that function. |
| 655 | */ |
| 656 | cs.reportErrorAsMessage = function f(/*msg args*/){ |
| 657 | if(undefined === f.$msgid) f.$msgid=0; |
| 658 | const args = argsToArray(arguments).map(function(v){ |
| 659 | return (v instanceof Error) ? v.message : v; |
| 660 | }); |
| 661 | console.error("chat error:",args); |
| 662 | const d = new Date().toISOString(), |
| 663 | mw = new this.MessageWidget({ |
| 664 | isError: true, |
| 665 | xfrom: null, |
| 666 | msgid: "error-"+(++f.$msgid), |
| 667 | mtime: d, |
| 668 | lmtime: d, |
| 669 | xmsg: args |
| 670 | }); |
| 671 | this.injectMessageElem(mw.e.body); |
| 672 | mw.scrollIntoView(); |
| 673 | }; |
| 674 | |
| 675 | cs.getMessageElemById = function(id){ |
| 676 | return qs('[data-msgid="'+id+'"]'); |
| 677 | }; |
| @@ -690,24 +815,40 @@ | |
| 690 | /** |
| 691 | LOCALLY deletes a message element by the message ID or passing |
| 692 | the .message-row element. Returns true if it removes an element, |
| 693 | else false. |
| 694 | */ |
| 695 | cs.deleteMessageElem = function(id){ |
| 696 | var e; |
| 697 | if(id instanceof HTMLElement){ |
| 698 | e = id; |
| 699 | id = e.dataset.msgid; |
| 700 | }else{ |
| 701 | e = this.getMessageElemById(id); |
| 702 | } |
| 703 | if(e && id){ |
| 704 | D.remove(e); |
| 705 | if(e===this.e.newestMessage){ |
| 706 | this.fetchLastMessageElem(); |
| 707 | } |
| 708 | F.toast.message("Deleted message "+id+"."); |
| 709 | } |
| 710 | return !!e; |
| 711 | }; |
| 712 | |
| 713 | /** |
| @@ -776,10 +917,11 @@ | |
| 776 | const self = this; |
| 777 | F.fetch('chat-fetch-one',{ |
| 778 | urlParams:{ name: id, raw: true}, |
| 779 | responseType: 'json', |
| 780 | onload: function(msg){ |
| 781 | content.$elems[1] = D.append(D.pre(),msg.xmsg); |
| 782 | content.$elems[1]._xmsgRaw = msg.xmsg/*used for copy-to-clipboard feature*/; |
| 783 | self.toggleTextMode(e); |
| 784 | }, |
| 785 | aftersend:function(){ |
| @@ -834,14 +976,16 @@ | |
| 834 | }else{ |
| 835 | e = this.getMessageElemById(id); |
| 836 | } |
| 837 | if(!(e instanceof HTMLElement)) return; |
| 838 | if(this.userMayDelete(e)){ |
| 839 | this.ajaxStart(); |
| 840 | F.fetch("chat-delete/" + id, { |
| 841 | responseType: 'json', |
| 842 | onload:(r)=>this.deleteMessageElem(r), |
| 843 | onerror:(err)=>this.reportErrorAsMessage(err) |
| 844 | }); |
| 845 | }else{ |
| 846 | this.deleteMessageElem(id); |
| 847 | } |
| @@ -1035,10 +1179,11 @@ | |
| 1035 | |
| 1036 | ctor.prototype = { |
| 1037 | scrollIntoView: function(){ |
| 1038 | this.e.content.scrollIntoView(); |
| 1039 | }, |
| 1040 | setMessage: function(m){ |
| 1041 | const ds = this.e.body.dataset; |
| 1042 | ds.timestamp = m.mtime; |
| 1043 | ds.lmtime = m.lmtime; |
| 1044 | ds.msgid = m.msgid; |
| @@ -1212,12 +1357,22 @@ | |
| 1212 | const btnDeleteLocal = D.button("Delete locally"); |
| 1213 | D.append(toolbar, btnDeleteLocal); |
| 1214 | const self = this; |
| 1215 | btnDeleteLocal.addEventListener('click', function(){ |
| 1216 | self.hide(); |
| 1217 | Chat.deleteMessageElem(eMsg); |
| 1218 | }); |
| 1219 | if(Chat.userMayDelete(eMsg)){ |
| 1220 | const btnDeleteGlobal = D.button("Delete globally"); |
| 1221 | D.append(toolbar, btnDeleteGlobal); |
| 1222 | F.confirmer(btnDeleteGlobal,{ |
| 1223 | pinSize: true, |
| @@ -1457,10 +1612,11 @@ | |
| 1457 | n: nFetch, |
| 1458 | i: iFirst |
| 1459 | }, |
| 1460 | responseType: "json", |
| 1461 | onload:function(jx){ |
| 1462 | if( bDown ) jx.msgs.reverse(); |
| 1463 | jx.msgs.forEach((m) => { |
| 1464 | m.isSearchResult = true; |
| 1465 | var mw = new Chat.MessageWidget(m); |
| 1466 | if( bDown ){ |
| @@ -1524,11 +1680,11 @@ | |
| 1524 | reader.onload = (e)=>img.setAttribute('src', e.target.result); |
| 1525 | reader.readAsDataURL(blob); |
| 1526 | } |
| 1527 | }; |
| 1528 | Chat.e.inputFile.addEventListener('change', function(ev){ |
| 1529 | updateDropZoneContent(this.files && this.files[0] ? this.files[0] : undefined) |
| 1530 | }); |
| 1531 | /* Handle image paste from clipboard. TODO: figure out how we can |
| 1532 | paste non-image binary data as if it had been selected via the |
| 1533 | file selection element. */ |
| 1534 | const pasteListener = function(event){ |
| @@ -1604,10 +1760,11 @@ | |
| 1604 | D.span(),"This message was not successfully sent to the server:" |
| 1605 | )); |
| 1606 | if(state.msg){ |
| 1607 | const ta = D.textarea(); |
| 1608 | ta.value = state.msg; |
| 1609 | D.append(w,ta); |
| 1610 | } |
| 1611 | if(state.blob){ |
| 1612 | D.append(w,D.append(D.span(),"Attachment: ",(state.blob.name||"unnamed"))); |
| 1613 | //console.debug("blob = ",state.blob); |
| @@ -1622,11 +1779,46 @@ | |
| 1622 | if(state.msg) Chat.inputValue(state.msg); |
| 1623 | if(state.blob) BlobXferState.updateDropZoneContent(state.blob); |
| 1624 | const theMsg = findMessageWidgetParent(w); |
| 1625 | if(theMsg) Chat.deleteMessageElem(theMsg); |
| 1626 | })); |
| 1627 | Chat.reportErrorAsMessage(w); |
| 1628 | }; |
| 1629 | |
| 1630 | /** |
| 1631 | Submits the contents of the message input field (if not empty) |
| 1632 | and/or the file attachment field to the server. If both are |
| @@ -1686,10 +1878,11 @@ | |
| 1686 | onerror:function(err){ |
| 1687 | self.reportErrorAsMessage(err); |
| 1688 | recoverFailedMessage(fallback); |
| 1689 | }, |
| 1690 | onload:function(txt){ |
| 1691 | if(!txt) return/*success response*/; |
| 1692 | try{ |
| 1693 | const json = JSON.parse(txt); |
| 1694 | self.newContent({msgs:[json]}); |
| 1695 | }catch(e){ |
| @@ -2126,11 +2319,11 @@ | |
| 2126 | Chat.e.inputFields.$currentIndex = a[2]; |
| 2127 | Chat.inputValue(v); |
| 2128 | D.removeClass(a[0], 'hidden'); |
| 2129 | D.addClass(a[1], 'hidden'); |
| 2130 | } |
| 2131 | Chat.e.inputElementWrapper.classList[ |
| 2132 | s.value ? 'add' : 'remove' |
| 2133 | ]('compact'); |
| 2134 | Chat.e.inputFields[Chat.e.inputFields.$currentIndex].focus(); |
| 2135 | }); |
| 2136 | Chat.settings.addListener('edit-ctrl-send',function(s){ |
| @@ -2185,10 +2378,11 @@ | |
| 2185 | /*filename needed for mimetype determination*/); |
| 2186 | fd.append('render_mode',F.page.previewModes.wiki); |
| 2187 | F.fetch('ajax/preview-text',{ |
| 2188 | payload: fd, |
| 2189 | onload: function(html){ |
| 2190 | Chat.setPreviewText(html); |
| 2191 | F.pikchr.addSrcView(Chat.e.viewPreview.querySelectorAll('svg.pikchr')); |
| 2192 | }, |
| 2193 | onerror: function(e){ |
| 2194 | F.fetch.onerror(e); |
| @@ -2322,10 +2516,11 @@ | |
| 2322 | onerror:function(err){ |
| 2323 | Chat.reportErrorAsMessage(err); |
| 2324 | Chat._isBatchLoading = false; |
| 2325 | }, |
| 2326 | onload:function(x){ |
| 2327 | let gotMessages = x.msgs.length; |
| 2328 | newcontent(x,true); |
| 2329 | Chat._isBatchLoading = false; |
| 2330 | Chat.updateActiveUserList(); |
| 2331 | if(Chat._gotServerError){ |
| @@ -2411,10 +2606,11 @@ | |
| 2411 | onerror:function(err){ |
| 2412 | Chat.setCurrentView(Chat.e.viewMessages); |
| 2413 | Chat.reportErrorAsMessage(err); |
| 2414 | }, |
| 2415 | onload:function(jx){ |
| 2416 | let previd = 0; |
| 2417 | D.clearElement(eMsgTgt); |
| 2418 | jx.msgs.forEach((m)=>{ |
| 2419 | m.isSearchResult = true; |
| 2420 | const mw = new Chat.MessageWidget(m); |
| @@ -2444,87 +2640,210 @@ | |
| 2444 | } |
| 2445 | } |
| 2446 | ); |
| 2447 | }/*Chat.submitSearch()*/; |
| 2448 | |
| 2449 | const afterFetch = function f(){ |
| 2450 | if(true===f.isFirstCall){ |
| 2451 | f.isFirstCall = false; |
| 2452 | Chat.ajaxEnd(); |
| 2453 | Chat.e.viewMessages.classList.remove('loading'); |
| 2454 | setTimeout(function(){ |
| 2455 | Chat.scrollMessagesTo(1); |
| 2456 | }, 250); |
| 2457 | } |
| 2458 | if(Chat._gotServerError && Chat.intervalTimer){ |
| 2459 | clearInterval(Chat.intervalTimer); |
| 2460 | Chat.reportErrorAsMessage( |
| 2461 | "Shutting down chat poller due to server-side error. ", |
| 2462 | "Reload this page to reactivate it."); |
| 2463 | delete Chat.intervalTimer; |
| 2464 | } |
| 2465 | poll.running = false; |
| 2466 | }; |
| 2467 | afterFetch.isFirstCall = true; |
| 2468 | /** |
| 2469 | FIXME: when polling fails because the remote server is |
| 2470 | reachable but it's not accepting HTTP requests, we should back |
| 2471 | off on polling for a while. e.g. if the remote web server process |
| 2472 | is killed, the poll fails quickly and immediately retries, |
| 2473 | hammering the remote server until the httpd is back up. That |
| 2474 | happens often during development of this application. |
| 2475 | |
| 2476 | XHR does not offer a direct way of distinguishing between |
| 2477 | HTTP/connection errors, but we can hypothetically use the |
| 2478 | xhrRequest.status value to do so, with status==0 being a |
| 2479 | connection error. We do not currently have a clean way of passing |
| 2480 | that info back to the fossil.fetch() client, so we'll need to |
| 2481 | hammer on that API a bit to get this working. |
| 2482 | */ |
| 2483 | const poll = async function f(){ |
| 2484 | if(f.running) return; |
| 2485 | f.running = true; |
| 2486 | Chat._isBatchLoading = f.isFirstCall; |
| 2487 | if(true===f.isFirstCall){ |
| 2488 | f.isFirstCall = false; |
| 2489 | Chat.ajaxStart(); |
| 2490 | Chat.e.viewMessages.classList.add('loading'); |
| 2491 | } |
| 2492 | F.fetch("chat-poll",{ |
| 2493 | timeout: 420 * 1000/*FIXME: get the value from the server*/, |
| 2494 | urlParams:{ |
| 2495 | name: Chat.mxMsg |
| 2496 | }, |
| 2497 | responseType: "json", |
| 2498 | // Disable the ajax start/end handling for this long-polling op: |
| 2499 | beforesend: function(){}, |
| 2500 | aftersend: function(){}, |
| 2501 | onerror:function(err){ |
| 2502 | Chat._isBatchLoading = false; |
| 2503 | if(Chat.verboseErrors) console.error(err); |
| 2504 | /* ^^^ we don't use Chat.reportError() here b/c the polling |
| 2505 | fails exepectedly when it times out, but is then immediately |
| 2506 | resumed, and reportError() produces a loud error message. */ |
| 2507 | afterFetch(); |
| 2508 | }, |
| 2509 | onload:function(y){ |
| 2510 | newcontent(y); |
| 2511 | if(Chat._isBatchLoading){ |
| 2512 | Chat._isBatchLoading = false; |
| 2513 | Chat.updateActiveUserList(); |
| 2514 | } |
| 2515 | afterFetch(); |
| 2516 | } |
| 2517 | }); |
| 2518 | }; |
| 2519 | poll.isFirstCall = true; |
| 2520 | Chat._gotServerError = poll.running = false; |
| 2521 | if( window.fossil.config.chat.fromcli ){ |
| 2522 | Chat.chatOnlyMode(true); |
| 2523 | } |
| 2524 | Chat.intervalTimer = setInterval(poll, 1000); |
| 2525 | delete ForceResizeKludge.$disabled; |
| 2526 | ForceResizeKludge(); |
| 2527 | Chat.animate.$disabled = false; |
| 2528 | setTimeout( ()=>Chat.inputFocus(), 0 ); |
| 2529 | F.page.chat = Chat/* enables testing the APIs via the dev tools */; |
| 2530 | }); |
| 2531 |
| --- src/fossil.page.chat.js | |
| +++ src/fossil.page.chat.js | |
| @@ -1,6 +1,6 @@ | |
| 1 | -/** |
| 2 | This file contains the client-side implementation of fossil's /chat |
| 3 | application. |
| 4 | */ |
| 5 | window.fossil.onPageLoad(function(){ |
| 6 | const F = window.fossil, D = F.dom; |
| @@ -129,19 +129,21 @@ | |
| 129 | return resized; |
| 130 | })(); |
| 131 | fossil.FRK = ForceResizeKludge/*for debugging*/; |
| 132 | const Chat = ForceResizeKludge.chat = (function(){ |
| 133 | const cs = { // the "Chat" object (result of this function) |
| 134 | beVerbose: false |
| 135 | //!!window.location.hostname.match("localhost") |
| 136 | /* if true then certain, mostly extraneous, error messages and |
| 137 | log messages may be sent to the console. */, |
| 138 | playedBeep: false /* used for the beep-once setting */, |
| 139 | e:{/*map of certain DOM elements.*/ |
| 140 | messageInjectPoint: E1('#message-inject-point'), |
| 141 | pageTitle: E1('head title'), |
| 142 | loadOlderToolbar: undefined /* the load-posts toolbar (dynamically created) */, |
| 143 | inputArea: E1("#chat-input-area"), |
| 144 | inputLineWrapper: E1('#chat-input-line-wrapper'), |
| 145 | fileSelectWrapper: E1('#chat-input-file-area'), |
| 146 | viewMessages: E1('#chat-messages-wrapper'), |
| 147 | btnSubmit: E1('#chat-button-submit'), |
| 148 | btnAttach: E1('#chat-button-attach'), |
| 149 | inputX: E1('#chat-input-field-x'), |
| @@ -155,11 +157,13 @@ | |
| 157 | viewSearch: E1('#chat-search'), |
| 158 | searchContent: E1('#chat-search-content'), |
| 159 | btnPreview: E1('#chat-button-preview'), |
| 160 | views: document.querySelectorAll('.chat-view'), |
| 161 | activeUserListWrapper: E1('#chat-user-list-wrapper'), |
| 162 | activeUserList: E1('#chat-user-list'), |
| 163 | eMsgPollError: undefined /* current connection error MessageMidget */, |
| 164 | pollErrorMarker: document.body /* element to toggle 'connection-error' CSS class on */ |
| 165 | }, |
| 166 | me: F.user.name, |
| 167 | mxMsg: F.config.chat.initSize ? -F.config.chat.initSize : -50, |
| 168 | mnMsg: undefined/*lowest message ID we've seen so far (for history loading)*/, |
| 169 | pageIsActive: 'visible'===document.visibilityState, |
| @@ -179,10 +183,105 @@ | |
| 183 | filterState:{ |
| 184 | activeUser: undefined, |
| 185 | match: function(uname){ |
| 186 | return this.activeUser===uname || !this.activeUser; |
| 187 | } |
| 188 | }, |
| 189 | /** |
| 190 | The timer object is used to control connection throttling |
| 191 | when connection errors arrise. It starts off with a polling |
| 192 | delay of $initialDelay ms. If there's a connection error, |
| 193 | that gets bumped by some value for each subsequent error, up |
| 194 | to some max value. |
| 195 | |
| 196 | The timing of resetting the delay when service returns is, |
| 197 | because of the long-poll connection and our lack of low-level |
| 198 | insight into the connection at this level, a bit wonky. |
| 199 | */ |
| 200 | timer:{ |
| 201 | /* setTimeout() ID for (delayed) starting a Chat.poll(), so |
| 202 | that it runs at controlled intervals (which change when a |
| 203 | connection drops and recovers). */ |
| 204 | tidPendingPoll: undefined, |
| 205 | tidClearPollErr: undefined /*setTimeout() timer id for |
| 206 | reconnection determination. See |
| 207 | clearPollErrOnWait(). */, |
| 208 | $initialDelay: 1000 /* initial polling interval (ms) */, |
| 209 | currentDelay: 1000 /* current polling interval */, |
| 210 | maxDelay: 60000 * 5 /* max interval when backing off for |
| 211 | connection errors */, |
| 212 | minDelay: 5000 /* minimum delay time for a back-off/retry |
| 213 | attempt. */, |
| 214 | errCount: 0 /* Current poller connection error count */, |
| 215 | minErrForNotify: 4 /* Don't warn for connection errors until this |
| 216 | many have occurred */, |
| 217 | pollTimeout: (1 && window.location.hostname.match( |
| 218 | "localhost" /*presumably local dev mode*/ |
| 219 | )) ? 15000 |
| 220 | : (+F.config.chat.pollTimeout>0 |
| 221 | ? (1000 * (F.config.chat.pollTimeout - Math.floor(F.config.chat.pollTimeout * 0.1))) |
| 222 | /* ^^^^^^^^^^^^ we want our timeouts to be slightly shorter |
| 223 | than the server's so that we can distingished timed-out |
| 224 | polls on our end from HTTP errors (if the server times |
| 225 | out). */ |
| 226 | : 30000), |
| 227 | /** Returns a random fudge value for reconnect attempt times, |
| 228 | intended to keep the /chat server from getting hammered if |
| 229 | all clients which were just disconnected all reconnect at |
| 230 | the same instant. */ |
| 231 | randomInterval: function(factor){ |
| 232 | return Math.floor(Math.random() * factor); |
| 233 | }, |
| 234 | /** Increments the reconnection delay, within some min/max range. */ |
| 235 | incrDelay: function(){ |
| 236 | if( this.maxDelay > this.currentDelay ){ |
| 237 | if(this.currentDelay < this.minDelay){ |
| 238 | this.currentDelay = this.minDelay + this.randomInterval(this.minDelay); |
| 239 | }else{ |
| 240 | this.currentDelay = this.currentDelay*2 + this.randomInterval(this.currentDelay); |
| 241 | } |
| 242 | } |
| 243 | return this.currentDelay; |
| 244 | }, |
| 245 | /** Resets the delay counter to v || its initial value. */ |
| 246 | resetDelay: function(ms=0){ |
| 247 | return this.currentDelay = ms || this.$initialDelay; |
| 248 | }, |
| 249 | /** Returns true if the timer is set to delayed mode. */ |
| 250 | isDelayed: function(){ |
| 251 | return (this.currentDelay > this.$initialDelay) ? this.currentDelay : 0; |
| 252 | }, |
| 253 | /** |
| 254 | Cancels any in-progress pending-poll timer and starts a new |
| 255 | one with the given delay, defaulting to this.resetDelay(). |
| 256 | */ |
| 257 | startPendingPollTimer: function(delay){ |
| 258 | this.cancelPendingPollTimer().tidPendingPoll |
| 259 | = setTimeout( Chat.poll, delay || Chat.timer.resetDelay() ); |
| 260 | return this; |
| 261 | }, |
| 262 | /** |
| 263 | Cancels any still-active timer set to trigger the next |
| 264 | Chat.poll(). |
| 265 | */ |
| 266 | cancelPendingPollTimer: function(){ |
| 267 | if( this.tidPendingPoll ){ |
| 268 | clearTimeout(this.tidPendingPoll); |
| 269 | this.tidPendingPoll = 0; |
| 270 | } |
| 271 | return this; |
| 272 | }, |
| 273 | /** |
| 274 | Cancels any pending reconnection attempt back-off timer.. |
| 275 | */ |
| 276 | cancelReconnectCheckTimer: function(){ |
| 277 | if( this.tidClearPollErr ){ |
| 278 | clearTimeout(this.tidClearPollErr); |
| 279 | this.tidClearPollErr = 0; |
| 280 | } |
| 281 | return this; |
| 282 | } |
| 283 | }, |
| 284 | /** |
| 285 | Gets (no args) or sets (1 arg) the current input text field |
| 286 | value, taking into account single- vs multi-line input. The |
| 287 | getter returns a trim()'d string and the setter returns this |
| @@ -606,19 +705,19 @@ | |
| 705 | |
| 706 | /** |
| 707 | If animations are enabled, passes its arguments |
| 708 | to D.addClassBriefly(), else this is a no-op. |
| 709 | If cb is a function, it is called after the |
| 710 | CSS class is removed. Returns this object; |
| 711 | */ |
| 712 | animate: function f(e,a,cb){ |
| 713 | if(!f.$disabled){ |
| 714 | D.addClassBriefly(e, a, 0, cb); |
| 715 | } |
| 716 | return this; |
| 717 | } |
| 718 | }/*Chat object*/; |
| 719 | cs.e.inputFields = [ cs.e.input1, cs.e.inputM, cs.e.inputX ]; |
| 720 | cs.e.inputFields.$currentIndex = 0; |
| 721 | cs.e.inputFields.forEach(function(e,ndx){ |
| 722 | if(ndx===cs.e.inputFields.$currentIndex) D.removeClass(e,'hidden'); |
| 723 | else D.addClass(e,'hidden'); |
| @@ -645,33 +744,59 @@ | |
| 744 | cs.reportError = function(/*msg args*/){ |
| 745 | const args = argsToArray(arguments); |
| 746 | console.error("chat error:",args); |
| 747 | F.toast.error.apply(F.toast, args); |
| 748 | }; |
| 749 | |
| 750 | let InternalMsgId = 0; |
| 751 | /** |
| 752 | Reports an error in the form of a new message in the chat |
| 753 | feed. All arguments are appended to the message's content area |
| 754 | using fossil.dom.append(), so may be of any type supported by |
| 755 | that function. |
| 756 | */ |
| 757 | cs.reportErrorAsMessage = function f(/*msg args*/){ |
| 758 | const args = argsToArray(arguments).map(function(v){ |
| 759 | return (v instanceof Error) ? v.message : v; |
| 760 | }); |
| 761 | if(Chat.beVerbose){ |
| 762 | console.error("chat error:",args); |
| 763 | } |
| 764 | const d = new Date().toISOString(), |
| 765 | mw = new this.MessageWidget({ |
| 766 | isError: true, |
| 767 | xfrom: undefined, |
| 768 | msgid: "error-"+(++InternalMsgId), |
| 769 | mtime: d, |
| 770 | lmtime: d, |
| 771 | xmsg: args |
| 772 | }); |
| 773 | this.injectMessageElem(mw.e.body); |
| 774 | mw.scrollIntoView(); |
| 775 | return mw; |
| 776 | }; |
| 777 | |
| 778 | /** |
| 779 | For use by the connection poller to send a "connection |
| 780 | restored" message. |
| 781 | */ |
| 782 | cs.reportReconnection = function f(/*msg args*/){ |
| 783 | const args = argsToArray(arguments).map(function(v){ |
| 784 | return (v instanceof Error) ? v.message : v; |
| 785 | }); |
| 786 | const d = new Date().toISOString(), |
| 787 | mw = new this.MessageWidget({ |
| 788 | isError: false, |
| 789 | xfrom: undefined, |
| 790 | msgid: "reconnect-"+(++InternalMsgId), |
| 791 | mtime: d, |
| 792 | lmtime: d, |
| 793 | xmsg: args |
| 794 | }); |
| 795 | this.injectMessageElem(mw.e.body); |
| 796 | mw.scrollIntoView(); |
| 797 | return mw; |
| 798 | }; |
| 799 | |
| 800 | cs.getMessageElemById = function(id){ |
| 801 | return qs('[data-msgid="'+id+'"]'); |
| 802 | }; |
| @@ -690,24 +815,40 @@ | |
| 815 | /** |
| 816 | LOCALLY deletes a message element by the message ID or passing |
| 817 | the .message-row element. Returns true if it removes an element, |
| 818 | else false. |
| 819 | */ |
| 820 | cs.deleteMessageElem = function(id, silent){ |
| 821 | var e; |
| 822 | if(id instanceof HTMLElement){ |
| 823 | e = id; |
| 824 | id = e.dataset.msgid; |
| 825 | delete e.dataset.msgid; |
| 826 | if( e?.dataset?.alsoRemove ){ |
| 827 | const xId = e.dataset.alsoRemove; |
| 828 | delete e.dataset.alsoRemove; |
| 829 | this.deleteMessageElem( xId ); |
| 830 | } |
| 831 | }else if(id instanceof Chat.MessageWidget) { |
| 832 | if( this.e.eMsgPollError === e ){ |
| 833 | this.e.eMsgPollError = undefined; |
| 834 | } |
| 835 | if(id.e?.body){ |
| 836 | this.deleteMessageElem(id.e.body); |
| 837 | } |
| 838 | return; |
| 839 | } else{ |
| 840 | e = this.getMessageElemById(id); |
| 841 | } |
| 842 | if(e && id){ |
| 843 | D.remove(e); |
| 844 | if(e===this.e.newestMessage){ |
| 845 | this.fetchLastMessageElem(); |
| 846 | } |
| 847 | if( !silent ){ |
| 848 | F.toast.message("Deleted message "+id+"."); |
| 849 | } |
| 850 | } |
| 851 | return !!e; |
| 852 | }; |
| 853 | |
| 854 | /** |
| @@ -776,10 +917,11 @@ | |
| 917 | const self = this; |
| 918 | F.fetch('chat-fetch-one',{ |
| 919 | urlParams:{ name: id, raw: true}, |
| 920 | responseType: 'json', |
| 921 | onload: function(msg){ |
| 922 | reportConnectionOkay('chat-fetch-one'); |
| 923 | content.$elems[1] = D.append(D.pre(),msg.xmsg); |
| 924 | content.$elems[1]._xmsgRaw = msg.xmsg/*used for copy-to-clipboard feature*/; |
| 925 | self.toggleTextMode(e); |
| 926 | }, |
| 927 | aftersend:function(){ |
| @@ -834,14 +976,16 @@ | |
| 976 | }else{ |
| 977 | e = this.getMessageElemById(id); |
| 978 | } |
| 979 | if(!(e instanceof HTMLElement)) return; |
| 980 | if(this.userMayDelete(e)){ |
| 981 | F.fetch("chat-delete/" + id, { |
| 982 | responseType: 'json', |
| 983 | onload:(r)=>{ |
| 984 | reportConnectionOkay('chat-delete'); |
| 985 | this.deleteMessageElem(r); |
| 986 | }, |
| 987 | onerror:(err)=>this.reportErrorAsMessage(err) |
| 988 | }); |
| 989 | }else{ |
| 990 | this.deleteMessageElem(id); |
| 991 | } |
| @@ -1035,10 +1179,11 @@ | |
| 1179 | |
| 1180 | ctor.prototype = { |
| 1181 | scrollIntoView: function(){ |
| 1182 | this.e.content.scrollIntoView(); |
| 1183 | }, |
| 1184 | //remove: function(silent){Chat.deleteMessageElem(this, silent);}, |
| 1185 | setMessage: function(m){ |
| 1186 | const ds = this.e.body.dataset; |
| 1187 | ds.timestamp = m.mtime; |
| 1188 | ds.lmtime = m.lmtime; |
| 1189 | ds.msgid = m.msgid; |
| @@ -1212,12 +1357,22 @@ | |
| 1357 | const btnDeleteLocal = D.button("Delete locally"); |
| 1358 | D.append(toolbar, btnDeleteLocal); |
| 1359 | const self = this; |
| 1360 | btnDeleteLocal.addEventListener('click', function(){ |
| 1361 | self.hide(); |
| 1362 | Chat.deleteMessageElem(eMsg) |
| 1363 | }); |
| 1364 | if( eMsg.classList.contains('notification') ){ |
| 1365 | const btnDeletePoll = D.button("Delete /chat notifications?"); |
| 1366 | D.append(toolbar, btnDeletePoll); |
| 1367 | btnDeletePoll.addEventListener('click', function(){ |
| 1368 | self.hide(); |
| 1369 | Chat.e.viewMessages.querySelectorAll( |
| 1370 | '.message-widget.notification:not(.resend-message)' |
| 1371 | ).forEach(e=>Chat.deleteMessageElem(e, true)); |
| 1372 | }); |
| 1373 | } |
| 1374 | if(Chat.userMayDelete(eMsg)){ |
| 1375 | const btnDeleteGlobal = D.button("Delete globally"); |
| 1376 | D.append(toolbar, btnDeleteGlobal); |
| 1377 | F.confirmer(btnDeleteGlobal,{ |
| 1378 | pinSize: true, |
| @@ -1457,10 +1612,11 @@ | |
| 1612 | n: nFetch, |
| 1613 | i: iFirst |
| 1614 | }, |
| 1615 | responseType: "json", |
| 1616 | onload:function(jx){ |
| 1617 | reportConnectionOkay('chat-query'); |
| 1618 | if( bDown ) jx.msgs.reverse(); |
| 1619 | jx.msgs.forEach((m) => { |
| 1620 | m.isSearchResult = true; |
| 1621 | var mw = new Chat.MessageWidget(m); |
| 1622 | if( bDown ){ |
| @@ -1524,11 +1680,11 @@ | |
| 1680 | reader.onload = (e)=>img.setAttribute('src', e.target.result); |
| 1681 | reader.readAsDataURL(blob); |
| 1682 | } |
| 1683 | }; |
| 1684 | Chat.e.inputFile.addEventListener('change', function(ev){ |
| 1685 | updateDropZoneContent(this?.files[0]) |
| 1686 | }); |
| 1687 | /* Handle image paste from clipboard. TODO: figure out how we can |
| 1688 | paste non-image binary data as if it had been selected via the |
| 1689 | file selection element. */ |
| 1690 | const pasteListener = function(event){ |
| @@ -1604,10 +1760,11 @@ | |
| 1760 | D.span(),"This message was not successfully sent to the server:" |
| 1761 | )); |
| 1762 | if(state.msg){ |
| 1763 | const ta = D.textarea(); |
| 1764 | ta.value = state.msg; |
| 1765 | ta.setAttribute('readonly','true'); |
| 1766 | D.append(w,ta); |
| 1767 | } |
| 1768 | if(state.blob){ |
| 1769 | D.append(w,D.append(D.span(),"Attachment: ",(state.blob.name||"unnamed"))); |
| 1770 | //console.debug("blob = ",state.blob); |
| @@ -1622,11 +1779,46 @@ | |
| 1779 | if(state.msg) Chat.inputValue(state.msg); |
| 1780 | if(state.blob) BlobXferState.updateDropZoneContent(state.blob); |
| 1781 | const theMsg = findMessageWidgetParent(w); |
| 1782 | if(theMsg) Chat.deleteMessageElem(theMsg); |
| 1783 | })); |
| 1784 | D.addClass(Chat.reportErrorAsMessage(w).e.body, "resend-message"); |
| 1785 | }; |
| 1786 | |
| 1787 | /* Assume the connection has been established, reset the |
| 1788 | Chat.timer.tidClearPollErr, and (if showMsg and |
| 1789 | !!Chat.e.eMsgPollError) alert the user that the outage appears to |
| 1790 | be over. Also schedule Chat.poll() to run in the very near |
| 1791 | future. */ |
| 1792 | const reportConnectionOkay = function(dbgContext, showMsg = true){ |
| 1793 | if(Chat.beVerbose){ |
| 1794 | console.warn('reportConnectionOkay', dbgContext, |
| 1795 | 'Chat.e.pollErrorMarker classes =', |
| 1796 | Chat.e.pollErrorMarker.classList, |
| 1797 | 'Chat.timer.tidClearPollErr =',Chat.timer.tidClearPollErr, |
| 1798 | 'Chat.timer =',Chat.timer); |
| 1799 | } |
| 1800 | if( Chat.timer.errCount ){ |
| 1801 | D.removeClass(Chat.e.pollErrorMarker, 'connection-error'); |
| 1802 | Chat.timer.errCount = 0; |
| 1803 | } |
| 1804 | Chat.timer.cancelReconnectCheckTimer().startPendingPollTimer(); |
| 1805 | if( Chat.e.eMsgPollError ) { |
| 1806 | const oldErrMsg = Chat.e.eMsgPollError; |
| 1807 | Chat.e.eMsgPollError = undefined; |
| 1808 | if( showMsg ){ |
| 1809 | if(Chat.beVerbose){ |
| 1810 | console.log("Poller Connection restored."); |
| 1811 | } |
| 1812 | const m = Chat.reportReconnection("Poller connection restored."); |
| 1813 | if( oldErrMsg ){ |
| 1814 | D.remove(oldErrMsg.e?.body.querySelector('button.retry-now')); |
| 1815 | } |
| 1816 | m.e.body.dataset.alsoRemove = oldErrMsg?.e?.body?.dataset?.msgid; |
| 1817 | D.addClass(m.e.body,'poller-connection'); |
| 1818 | } |
| 1819 | } |
| 1820 | }; |
| 1821 | |
| 1822 | /** |
| 1823 | Submits the contents of the message input field (if not empty) |
| 1824 | and/or the file attachment field to the server. If both are |
| @@ -1686,10 +1878,11 @@ | |
| 1878 | onerror:function(err){ |
| 1879 | self.reportErrorAsMessage(err); |
| 1880 | recoverFailedMessage(fallback); |
| 1881 | }, |
| 1882 | onload:function(txt){ |
| 1883 | reportConnectionOkay('chat-send'); |
| 1884 | if(!txt) return/*success response*/; |
| 1885 | try{ |
| 1886 | const json = JSON.parse(txt); |
| 1887 | self.newContent({msgs:[json]}); |
| 1888 | }catch(e){ |
| @@ -2126,11 +2319,11 @@ | |
| 2319 | Chat.e.inputFields.$currentIndex = a[2]; |
| 2320 | Chat.inputValue(v); |
| 2321 | D.removeClass(a[0], 'hidden'); |
| 2322 | D.addClass(a[1], 'hidden'); |
| 2323 | } |
| 2324 | Chat.e.inputLineWrapper.classList[ |
| 2325 | s.value ? 'add' : 'remove' |
| 2326 | ]('compact'); |
| 2327 | Chat.e.inputFields[Chat.e.inputFields.$currentIndex].focus(); |
| 2328 | }); |
| 2329 | Chat.settings.addListener('edit-ctrl-send',function(s){ |
| @@ -2185,10 +2378,11 @@ | |
| 2378 | /*filename needed for mimetype determination*/); |
| 2379 | fd.append('render_mode',F.page.previewModes.wiki); |
| 2380 | F.fetch('ajax/preview-text',{ |
| 2381 | payload: fd, |
| 2382 | onload: function(html){ |
| 2383 | reportConnectionOkay('ajax/preview-text'); |
| 2384 | Chat.setPreviewText(html); |
| 2385 | F.pikchr.addSrcView(Chat.e.viewPreview.querySelectorAll('svg.pikchr')); |
| 2386 | }, |
| 2387 | onerror: function(e){ |
| 2388 | F.fetch.onerror(e); |
| @@ -2322,10 +2516,11 @@ | |
| 2516 | onerror:function(err){ |
| 2517 | Chat.reportErrorAsMessage(err); |
| 2518 | Chat._isBatchLoading = false; |
| 2519 | }, |
| 2520 | onload:function(x){ |
| 2521 | reportConnectionOkay('loadOldMessages()'); |
| 2522 | let gotMessages = x.msgs.length; |
| 2523 | newcontent(x,true); |
| 2524 | Chat._isBatchLoading = false; |
| 2525 | Chat.updateActiveUserList(); |
| 2526 | if(Chat._gotServerError){ |
| @@ -2411,10 +2606,11 @@ | |
| 2606 | onerror:function(err){ |
| 2607 | Chat.setCurrentView(Chat.e.viewMessages); |
| 2608 | Chat.reportErrorAsMessage(err); |
| 2609 | }, |
| 2610 | onload:function(jx){ |
| 2611 | reportConnectionOkay('submitSearch()'); |
| 2612 | let previd = 0; |
| 2613 | D.clearElement(eMsgTgt); |
| 2614 | jx.msgs.forEach((m)=>{ |
| 2615 | m.isSearchResult = true; |
| 2616 | const mw = new Chat.MessageWidget(m); |
| @@ -2444,87 +2640,210 @@ | |
| 2640 | } |
| 2641 | } |
| 2642 | ); |
| 2643 | }/*Chat.submitSearch()*/; |
| 2644 | |
| 2645 | /* |
| 2646 | To be called from F.fetch('chat-poll') beforesend() handler. If |
| 2647 | we're currently in delayed-retry mode and a connection is |
| 2648 | started, try to reset the delay after N time waiting on that |
| 2649 | connection. The fact that the connection is waiting to respond, |
| 2650 | rather than outright failing, is a good hint that the outage is |
| 2651 | over and we can reset the back-off timer. |
| 2652 | |
| 2653 | Without this, recovery of a connection error won't be reported |
| 2654 | until after the long-poll completes by either receiving new |
| 2655 | messages or timing out. Once a long-poll is in progress, though, |
| 2656 | we "know" that it's up and running again, so can update the UI and |
| 2657 | connection timer to reflect that. That's the job this function |
| 2658 | does. |
| 2659 | |
| 2660 | Only one of these asynchronous checks will ever be active |
| 2661 | concurrently and only if Chat.timer.isDelayed() is true. i.e. if |
| 2662 | this timer is active or Chat.timer.isDelayed() is false, this is a |
| 2663 | no-op. |
| 2664 | */ |
| 2665 | const chatPollBeforeSend = function(){ |
| 2666 | //console.warn('chatPollBeforeSend outer', Chat.timer.tidClearPollErr, Chat.timer.currentDelay); |
| 2667 | if( !Chat.timer.tidClearPollErr && Chat.timer.isDelayed() ){ |
| 2668 | Chat.timer.tidClearPollErr = setTimeout(()=>{ |
| 2669 | //console.warn('chatPollBeforeSend inner'); |
| 2670 | Chat.timer.tidClearPollErr = 0; |
| 2671 | if( poll.running ){ |
| 2672 | /* This chat-poll F.fetch() is still underway, so let's |
| 2673 | assume the connection is back up until/unless it times |
| 2674 | out or breaks again. */ |
| 2675 | reportConnectionOkay('chatPollBeforeSend', true); |
| 2676 | } |
| 2677 | }, Chat.timer.$initialDelay * 4/*kinda arbitrary: not too long for UI wait and |
| 2678 | not too short as to make connection unlikely. */ ); |
| 2679 | } |
| 2680 | }; |
| 2681 | |
| 2682 | /** |
| 2683 | Deal with the last poll() response and maybe re-start poll(). |
| 2684 | */ |
| 2685 | const afterPollFetch = function f(err){ |
| 2686 | if(true===f.isFirstCall){ |
| 2687 | f.isFirstCall = false; |
| 2688 | Chat.ajaxEnd(); |
| 2689 | Chat.e.viewMessages.classList.remove('loading'); |
| 2690 | setTimeout(function(){ |
| 2691 | Chat.scrollMessagesTo(1); |
| 2692 | }, 250); |
| 2693 | } |
| 2694 | Chat.timer.cancelPendingPollTimer(); |
| 2695 | if(Chat._gotServerError){ |
| 2696 | Chat.reportErrorAsMessage( |
| 2697 | "Shutting down chat poller due to server-side error. ", |
| 2698 | "Reload this page to reactivate it." |
| 2699 | ); |
| 2700 | } else { |
| 2701 | if( err && Chat.beVerbose ){ |
| 2702 | console.error("afterPollFetch:",err.name,err.status,err.message); |
| 2703 | } |
| 2704 | if( !err || 'timeout'===err.name/*(probably) long-poll expired*/ ){ |
| 2705 | /* Restart the poller immediately. */ |
| 2706 | reportConnectionOkay('afterPollFetch '+err, false); |
| 2707 | }else{ |
| 2708 | /* Delay a while before trying again, noting that other Chat |
| 2709 | APIs may try and succeed at connections before this timer |
| 2710 | resolves, in which case they'll clear this timeout and the |
| 2711 | UI message about the outage. */ |
| 2712 | let delay; |
| 2713 | D.addClass(Chat.e.pollErrorMarker, 'connection-error'); |
| 2714 | if( ++Chat.timer.errCount < Chat.timer.minErrForNotify ){ |
| 2715 | delay = Chat.timer.resetDelay( |
| 2716 | (Chat.timer.minDelay * Chat.timer.errCount) |
| 2717 | + Chat.timer.randomInterval(Chat.timer.minDelay) |
| 2718 | ); |
| 2719 | if(Chat.beVerbose){ |
| 2720 | console.warn("Ignoring polling error #",Chat.timer.errCount, |
| 2721 | "for another",delay,"ms" ); |
| 2722 | } |
| 2723 | } else { |
| 2724 | delay = Chat.timer.incrDelay(); |
| 2725 | //console.warn("afterPollFetch Chat.e.eMsgPollError",Chat.e.eMsgPollError); |
| 2726 | const msg = "Poller connection error. Retrying in "+delay+ " ms."; |
| 2727 | /* Replace the current/newest connection error widget. We could also |
| 2728 | just update its body with the new message, but then its timestamp |
| 2729 | never updates. OTOH, if we replace the message, we lose the |
| 2730 | start time of the outage in the log. It seems more useful to |
| 2731 | update the timestamp so that it doesn't look like it's hung. */ |
| 2732 | if( Chat.e.eMsgPollError ){ |
| 2733 | Chat.deleteMessageElem(Chat.e.eMsgPollError, false); |
| 2734 | } |
| 2735 | const theMsg = Chat.e.eMsgPollError = Chat.reportErrorAsMessage(msg); |
| 2736 | D.addClass(Chat.e.eMsgPollError.e.body,'poller-connection'); |
| 2737 | /* Add a "retry now" button */ |
| 2738 | const btnDel = D.addClass(D.button("Retry now"), 'retry-now'); |
| 2739 | const eParent = Chat.e.eMsgPollError.e.content; |
| 2740 | D.append(eParent, " ", btnDel); |
| 2741 | btnDel.addEventListener('click', function(){ |
| 2742 | D.remove(btnDel); |
| 2743 | D.append(eParent, D.text("retrying...")); |
| 2744 | Chat.timer.cancelPendingPollTimer().currentDelay = |
| 2745 | Chat.timer.resetDelay() + |
| 2746 | 1 /*workaround for showing the "connection restored" |
| 2747 | message, as the +1 will cause |
| 2748 | Chat.timer.isDelayed() to be true.*/; |
| 2749 | poll(); |
| 2750 | }); |
| 2751 | //Chat.playNewMessageSound();// browser complains b/c this wasn't via human interaction |
| 2752 | } |
| 2753 | Chat.timer.startPendingPollTimer(delay); |
| 2754 | } |
| 2755 | } |
| 2756 | }; |
| 2757 | afterPollFetch.isFirstCall = true; |
| 2758 | |
| 2759 | /** |
| 2760 | Initiates, if it's not already running, a single long-poll |
| 2761 | request to the /chat-poll endpoint. In the handling of that |
| 2762 | response, it end up will psuedo-recursively calling itself via |
| 2763 | the response-handling process. Despite being async, the implied |
| 2764 | returned Promise is meaningless. |
| 2765 | */ |
| 2766 | const poll = Chat.poll = async function f(){ |
| 2767 | if(f.running) return; |
| 2768 | f.running = true; |
| 2769 | Chat._isBatchLoading = f.isFirstCall; |
| 2770 | if(true===f.isFirstCall){ |
| 2771 | f.isFirstCall = false; |
| 2772 | f.pendingOnError = undefined; |
| 2773 | Chat.ajaxStart(); |
| 2774 | Chat.e.viewMessages.classList.add('loading'); |
| 2775 | /* |
| 2776 | We manager onerror() results in poll() in a roundabout |
| 2777 | manner: when an onerror() arrives, we stash it aside |
| 2778 | for a moment before processing it. |
| 2779 | |
| 2780 | This level of indirection is necessary to be able to |
| 2781 | unambiguously identify client-timeout-specific polling errors |
| 2782 | from other errors. Timeouts are always announced in pairs of |
| 2783 | an HTTP 0 and something we can unambiguously identify as a |
| 2784 | timeout (in that order). When we receive an HTTP error we put |
| 2785 | it into this queue. If an ontimeout() call arrives before |
| 2786 | this error is handled, this error is ignored. If, however, an |
| 2787 | HTTP error is seen without an accompanying timeout, we handle |
| 2788 | it from here. |
| 2789 | |
| 2790 | It's kinda like in the curses C API, where you to match |
| 2791 | ALT-X by first getting an ESC event, then an X event, but |
| 2792 | this one is a lot less explicable. (It's almost certainly a |
| 2793 | mis-handling bug in F.fetch(), but it has so far eluded my |
| 2794 | eyes.) |
| 2795 | */ |
| 2796 | f.delayPendingOnError = function(err){ |
| 2797 | if( f.pendingOnError ){ |
| 2798 | const x = f.pendingOnError; |
| 2799 | f.pendingOnError = undefined; |
| 2800 | afterPollFetch(x); |
| 2801 | } |
| 2802 | }; |
| 2803 | } |
| 2804 | F.fetch("chat-poll",{ |
| 2805 | timeout: Chat.timer.pollTimeout, |
| 2806 | urlParams:{ |
| 2807 | name: Chat.mxMsg |
| 2808 | }, |
| 2809 | responseType: "json", |
| 2810 | // Disable the ajax start/end handling for this long-polling op: |
| 2811 | beforesend: chatPollBeforeSend, |
| 2812 | aftersend: function(){ |
| 2813 | poll.running = false; |
| 2814 | }, |
| 2815 | ontimeout: function(err){ |
| 2816 | f.pendingOnError = undefined /*strip preceeding non-timeout error, if any*/; |
| 2817 | afterPollFetch(err); |
| 2818 | }, |
| 2819 | onerror:function(err){ |
| 2820 | Chat._isBatchLoading = false; |
| 2821 | if(Chat.beVerbose){ |
| 2822 | console.error("poll.onerror:",err.name,err.status,JSON.stringify(err)); |
| 2823 | } |
| 2824 | f.pendingOnError = err; |
| 2825 | setTimeout(f.delayPendingOnError, 100); |
| 2826 | }, |
| 2827 | onload:function(y){ |
| 2828 | reportConnectionOkay('poll.onload', true); |
| 2829 | newcontent(y); |
| 2830 | if(Chat._isBatchLoading){ |
| 2831 | Chat._isBatchLoading = false; |
| 2832 | Chat.updateActiveUserList(); |
| 2833 | } |
| 2834 | afterPollFetch(); |
| 2835 | } |
| 2836 | }); |
| 2837 | }/*poll()*/; |
| 2838 | poll.isFirstCall = true; |
| 2839 | Chat._gotServerError = poll.running = false; |
| 2840 | if( window.fossil.config.chat.fromcli ){ |
| 2841 | Chat.chatOnlyMode(true); |
| 2842 | } |
| 2843 | Chat.timer.startPendingPollTimer(); |
| 2844 | delete ForceResizeKludge.$disabled; |
| 2845 | ForceResizeKludge(); |
| 2846 | Chat.animate.$disabled = false; |
| 2847 | setTimeout( ()=>Chat.inputFocus(), 0 ); |
| 2848 | F.page.chat = Chat/* enables testing the APIs via the dev tools */; |
| 2849 | }); |
| 2850 |
+1
-1
| --- src/fossil.popupwidget.js | ||
| +++ src/fossil.popupwidget.js | ||
| @@ -286,11 +286,11 @@ | ||
| 286 | 286 | }; |
| 287 | 287 | |
| 288 | 288 | F.toast = { |
| 289 | 289 | config: { |
| 290 | 290 | position: { x: 5, y: 5 /*viewport-relative, pixels*/ }, |
| 291 | - displayTimeMs: 3000 | |
| 291 | + displayTimeMs: 5000 | |
| 292 | 292 | }, |
| 293 | 293 | /** |
| 294 | 294 | Convenience wrapper around a PopupWidget which pops up a shared |
| 295 | 295 | PopupWidget instance to show toast-style messages (commonly |
| 296 | 296 | seen on Android). Its arguments may be anything suitable for |
| 297 | 297 |
| --- src/fossil.popupwidget.js | |
| +++ src/fossil.popupwidget.js | |
| @@ -286,11 +286,11 @@ | |
| 286 | }; |
| 287 | |
| 288 | F.toast = { |
| 289 | config: { |
| 290 | position: { x: 5, y: 5 /*viewport-relative, pixels*/ }, |
| 291 | displayTimeMs: 3000 |
| 292 | }, |
| 293 | /** |
| 294 | Convenience wrapper around a PopupWidget which pops up a shared |
| 295 | PopupWidget instance to show toast-style messages (commonly |
| 296 | seen on Android). Its arguments may be anything suitable for |
| 297 |
| --- src/fossil.popupwidget.js | |
| +++ src/fossil.popupwidget.js | |
| @@ -286,11 +286,11 @@ | |
| 286 | }; |
| 287 | |
| 288 | F.toast = { |
| 289 | config: { |
| 290 | position: { x: 5, y: 5 /*viewport-relative, pixels*/ }, |
| 291 | displayTimeMs: 5000 |
| 292 | }, |
| 293 | /** |
| 294 | Convenience wrapper around a PopupWidget which pops up a shared |
| 295 | PopupWidget instance to show toast-style messages (commonly |
| 296 | seen on Android). Its arguments may be anything suitable for |
| 297 |
+1
-1
| --- src/graph.c | ||
| +++ src/graph.c | ||
| @@ -121,11 +121,11 @@ | ||
| 121 | 121 | u8 hasOffsetMergeRiser; /* Merge arrow from leaf goes up on a different |
| 122 | 122 | ** rail that the node */ |
| 123 | 123 | u8 bOverfull; /* Unable to allocate sufficient rails */ |
| 124 | 124 | u64 mergeRail; /* Rails used for merge lines */ |
| 125 | 125 | GraphRow **apHash; /* Hash table of GraphRow objects. Key: rid */ |
| 126 | - u8 aiRailMap[GR_MAX_RAIL]; /* Mapping of rails to actually columns */ | |
| 126 | + u8 aiRailMap[GR_MAX_RAIL+1]; /* Mapping of rails to actually columns */ | |
| 127 | 127 | }; |
| 128 | 128 | |
| 129 | 129 | #endif |
| 130 | 130 | |
| 131 | 131 | /* The N-th bit */ |
| 132 | 132 |
| --- src/graph.c | |
| +++ src/graph.c | |
| @@ -121,11 +121,11 @@ | |
| 121 | u8 hasOffsetMergeRiser; /* Merge arrow from leaf goes up on a different |
| 122 | ** rail that the node */ |
| 123 | u8 bOverfull; /* Unable to allocate sufficient rails */ |
| 124 | u64 mergeRail; /* Rails used for merge lines */ |
| 125 | GraphRow **apHash; /* Hash table of GraphRow objects. Key: rid */ |
| 126 | u8 aiRailMap[GR_MAX_RAIL]; /* Mapping of rails to actually columns */ |
| 127 | }; |
| 128 | |
| 129 | #endif |
| 130 | |
| 131 | /* The N-th bit */ |
| 132 |
| --- src/graph.c | |
| +++ src/graph.c | |
| @@ -121,11 +121,11 @@ | |
| 121 | u8 hasOffsetMergeRiser; /* Merge arrow from leaf goes up on a different |
| 122 | ** rail that the node */ |
| 123 | u8 bOverfull; /* Unable to allocate sufficient rails */ |
| 124 | u64 mergeRail; /* Rails used for merge lines */ |
| 125 | GraphRow **apHash; /* Hash table of GraphRow objects. Key: rid */ |
| 126 | u8 aiRailMap[GR_MAX_RAIL+1]; /* Mapping of rails to actually columns */ |
| 127 | }; |
| 128 | |
| 129 | #endif |
| 130 | |
| 131 | /* The N-th bit */ |
| 132 |
-1
| --- src/graph.js | ||
| +++ src/graph.js | ||
| @@ -9,11 +9,10 @@ | ||
| 9 | 9 | ** |
| 10 | 10 | ** { "iTableId": INTEGER, // Table sequence number (NN) |
| 11 | 11 | ** "circleNodes": BOOLEAN, // True for circle nodes. False for squares |
| 12 | 12 | ** "showArrowheads": BOOLEAN, // True for arrowheads. False to omit |
| 13 | 13 | ** "iRailPitch": INTEGER, // Spacing between vertical lines (px) |
| 14 | -** "colorGraph": BOOLEAN, // True to put color on graph lines | |
| 15 | 14 | ** "nomo": BOOLEAN, // True to join merge lines with rails |
| 16 | 15 | ** "iTopRow": INTEGER, // Index of top-most row in the graph |
| 17 | 16 | ** "omitDescenders": BOOLEAN, // Omit ancestor lines off bottom of screen |
| 18 | 17 | ** "fileDiff": BOOLEAN, // True for file diff. False for check-in |
| 19 | 18 | ** "scrollToSelect": BOOLEAN, // Scroll to selection on first render |
| 20 | 19 |
| --- src/graph.js | |
| +++ src/graph.js | |
| @@ -9,11 +9,10 @@ | |
| 9 | ** |
| 10 | ** { "iTableId": INTEGER, // Table sequence number (NN) |
| 11 | ** "circleNodes": BOOLEAN, // True for circle nodes. False for squares |
| 12 | ** "showArrowheads": BOOLEAN, // True for arrowheads. False to omit |
| 13 | ** "iRailPitch": INTEGER, // Spacing between vertical lines (px) |
| 14 | ** "colorGraph": BOOLEAN, // True to put color on graph lines |
| 15 | ** "nomo": BOOLEAN, // True to join merge lines with rails |
| 16 | ** "iTopRow": INTEGER, // Index of top-most row in the graph |
| 17 | ** "omitDescenders": BOOLEAN, // Omit ancestor lines off bottom of screen |
| 18 | ** "fileDiff": BOOLEAN, // True for file diff. False for check-in |
| 19 | ** "scrollToSelect": BOOLEAN, // Scroll to selection on first render |
| 20 |
| --- src/graph.js | |
| +++ src/graph.js | |
| @@ -9,11 +9,10 @@ | |
| 9 | ** |
| 10 | ** { "iTableId": INTEGER, // Table sequence number (NN) |
| 11 | ** "circleNodes": BOOLEAN, // True for circle nodes. False for squares |
| 12 | ** "showArrowheads": BOOLEAN, // True for arrowheads. False to omit |
| 13 | ** "iRailPitch": INTEGER, // Spacing between vertical lines (px) |
| 14 | ** "nomo": BOOLEAN, // True to join merge lines with rails |
| 15 | ** "iTopRow": INTEGER, // Index of top-most row in the graph |
| 16 | ** "omitDescenders": BOOLEAN, // Omit ancestor lines off bottom of screen |
| 17 | ** "fileDiff": BOOLEAN, // True for file diff. False for check-in |
| 18 | ** "scrollToSelect": BOOLEAN, // Scroll to selection on first render |
| 19 |
+12
-12
| --- src/http_ssl.c | ||
| +++ src/http_ssl.c | ||
| @@ -319,11 +319,13 @@ | ||
| 319 | 319 | ** The following OpenSSL configuration options must not be used for this feature |
| 320 | 320 | ** to be available: `no-autoalginit', `no-winstore'. The Fossil makefiles do not |
| 321 | 321 | ** currently set these options when building OpenSSL for Windows. */ |
| 322 | 322 | #if defined(_WIN32) |
| 323 | 323 | #if OPENSSL_VERSION_NUMBER >= 0x030200000 |
| 324 | - if( SSL_CTX_load_verify_store(sslCtx, "org.openssl.winstore:")==0 ){ | |
| 324 | + if( SSLeay()!=0x30500000 /* Don't use for 3.5.0 due to a bug */ | |
| 325 | + && SSL_CTX_load_verify_store(sslCtx, "org.openssl.winstore:")==0 | |
| 326 | + ){ | |
| 325 | 327 | fossil_print("NOTICE: Failed to load the Windows root certificates.\n"); |
| 326 | 328 | } |
| 327 | 329 | #endif /* OPENSSL_VERSION_NUMBER >= 0x030200000 */ |
| 328 | 330 | #endif /* _WIN32 */ |
| 329 | 331 | |
| @@ -999,12 +1001,12 @@ | ||
| 999 | 1001 | fossil_print("\n" |
| 1000 | 1002 | " The OpenSSL library is not used by this build of Fossil\n\n" |
| 1001 | 1003 | ); |
| 1002 | 1004 | } |
| 1003 | 1005 | #else |
| 1004 | - fossil_print("OpenSSL-version: %s (0x%09x)\n", | |
| 1005 | - SSLeay_version(SSLEAY_VERSION), OPENSSL_VERSION_NUMBER); | |
| 1006 | + fossil_print("OpenSSL-version: %s (0x%09llx)\n", | |
| 1007 | + SSLeay_version(SSLEAY_VERSION), (unsigned long long)SSLeay()); | |
| 1006 | 1008 | if( verbose ){ |
| 1007 | 1009 | fossil_print("\n" |
| 1008 | 1010 | " The version of the OpenSSL library being used\n" |
| 1009 | 1011 | " by this instance of Fossil. Version 3.0.0 or\n" |
| 1010 | 1012 | " later is recommended.\n\n" |
| @@ -1061,20 +1063,18 @@ | ||
| 1061 | 1063 | " values are built into your OpenSSL library.\n\n" |
| 1062 | 1064 | ); |
| 1063 | 1065 | } |
| 1064 | 1066 | |
| 1065 | 1067 | #if defined(_WIN32) |
| 1066 | -#if OPENSSL_VERSION_NUMBER >= 0x030200000 | |
| 1067 | - fossil_print(" OpenSSL-winstore: Yes\n"); | |
| 1068 | -#else /* OPENSSL_VERSION_NUMBER >= 0x030200000 */ | |
| 1069 | - fossil_print(" OpenSSL-winstore: No\n"); | |
| 1070 | -#endif /* OPENSSL_VERSION_NUMBER >= 0x030200000 */ | |
| 1068 | + fossil_print(" OpenSSL-winstore: %s\n", | |
| 1069 | + (SSLeay()>=0x30200000 && SSLeay()!=0x30500000) ? "Yes" : "No"); | |
| 1071 | 1070 | if( verbose ){ |
| 1072 | 1071 | fossil_print("\n" |
| 1073 | - " OpenSSL 3.2.0, or newer, use the root certificates managed by\n" | |
| 1074 | - " the Windows operating system. The installed root certificates\n" | |
| 1075 | - " are listed by the command:\n\n" | |
| 1072 | + " OpenSSL 3.2.0, or newer, but not version 3.5.0 due to a bug,\n" | |
| 1073 | + " are able to use the root certificates managed by the Windows\n" | |
| 1074 | + " operating system. The installed root certificates are listed\n" | |
| 1075 | + " by the command:\n\n" | |
| 1076 | 1076 | " certutil -store \"ROOT\"\n\n" |
| 1077 | 1077 | ); |
| 1078 | 1078 | } |
| 1079 | 1079 | #endif /* _WIN32 */ |
| 1080 | 1080 | |
| @@ -1232,10 +1232,10 @@ | ||
| 1232 | 1232 | ** freed by the caller. |
| 1233 | 1233 | */ |
| 1234 | 1234 | char *fossil_openssl_version(void){ |
| 1235 | 1235 | #if defined(FOSSIL_ENABLE_SSL) |
| 1236 | 1236 | return mprintf("%s (0x%09x)\n", |
| 1237 | - SSLeay_version(SSLEAY_VERSION), OPENSSL_VERSION_NUMBER); | |
| 1237 | + SSLeay_version(SSLEAY_VERSION), (sqlite3_uint64)SSLeay()); | |
| 1238 | 1238 | #else |
| 1239 | 1239 | return mprintf("none"); |
| 1240 | 1240 | #endif |
| 1241 | 1241 | } |
| 1242 | 1242 |
| --- src/http_ssl.c | |
| +++ src/http_ssl.c | |
| @@ -319,11 +319,13 @@ | |
| 319 | ** The following OpenSSL configuration options must not be used for this feature |
| 320 | ** to be available: `no-autoalginit', `no-winstore'. The Fossil makefiles do not |
| 321 | ** currently set these options when building OpenSSL for Windows. */ |
| 322 | #if defined(_WIN32) |
| 323 | #if OPENSSL_VERSION_NUMBER >= 0x030200000 |
| 324 | if( SSL_CTX_load_verify_store(sslCtx, "org.openssl.winstore:")==0 ){ |
| 325 | fossil_print("NOTICE: Failed to load the Windows root certificates.\n"); |
| 326 | } |
| 327 | #endif /* OPENSSL_VERSION_NUMBER >= 0x030200000 */ |
| 328 | #endif /* _WIN32 */ |
| 329 | |
| @@ -999,12 +1001,12 @@ | |
| 999 | fossil_print("\n" |
| 1000 | " The OpenSSL library is not used by this build of Fossil\n\n" |
| 1001 | ); |
| 1002 | } |
| 1003 | #else |
| 1004 | fossil_print("OpenSSL-version: %s (0x%09x)\n", |
| 1005 | SSLeay_version(SSLEAY_VERSION), OPENSSL_VERSION_NUMBER); |
| 1006 | if( verbose ){ |
| 1007 | fossil_print("\n" |
| 1008 | " The version of the OpenSSL library being used\n" |
| 1009 | " by this instance of Fossil. Version 3.0.0 or\n" |
| 1010 | " later is recommended.\n\n" |
| @@ -1061,20 +1063,18 @@ | |
| 1061 | " values are built into your OpenSSL library.\n\n" |
| 1062 | ); |
| 1063 | } |
| 1064 | |
| 1065 | #if defined(_WIN32) |
| 1066 | #if OPENSSL_VERSION_NUMBER >= 0x030200000 |
| 1067 | fossil_print(" OpenSSL-winstore: Yes\n"); |
| 1068 | #else /* OPENSSL_VERSION_NUMBER >= 0x030200000 */ |
| 1069 | fossil_print(" OpenSSL-winstore: No\n"); |
| 1070 | #endif /* OPENSSL_VERSION_NUMBER >= 0x030200000 */ |
| 1071 | if( verbose ){ |
| 1072 | fossil_print("\n" |
| 1073 | " OpenSSL 3.2.0, or newer, use the root certificates managed by\n" |
| 1074 | " the Windows operating system. The installed root certificates\n" |
| 1075 | " are listed by the command:\n\n" |
| 1076 | " certutil -store \"ROOT\"\n\n" |
| 1077 | ); |
| 1078 | } |
| 1079 | #endif /* _WIN32 */ |
| 1080 | |
| @@ -1232,10 +1232,10 @@ | |
| 1232 | ** freed by the caller. |
| 1233 | */ |
| 1234 | char *fossil_openssl_version(void){ |
| 1235 | #if defined(FOSSIL_ENABLE_SSL) |
| 1236 | return mprintf("%s (0x%09x)\n", |
| 1237 | SSLeay_version(SSLEAY_VERSION), OPENSSL_VERSION_NUMBER); |
| 1238 | #else |
| 1239 | return mprintf("none"); |
| 1240 | #endif |
| 1241 | } |
| 1242 |
| --- src/http_ssl.c | |
| +++ src/http_ssl.c | |
| @@ -319,11 +319,13 @@ | |
| 319 | ** The following OpenSSL configuration options must not be used for this feature |
| 320 | ** to be available: `no-autoalginit', `no-winstore'. The Fossil makefiles do not |
| 321 | ** currently set these options when building OpenSSL for Windows. */ |
| 322 | #if defined(_WIN32) |
| 323 | #if OPENSSL_VERSION_NUMBER >= 0x030200000 |
| 324 | if( SSLeay()!=0x30500000 /* Don't use for 3.5.0 due to a bug */ |
| 325 | && SSL_CTX_load_verify_store(sslCtx, "org.openssl.winstore:")==0 |
| 326 | ){ |
| 327 | fossil_print("NOTICE: Failed to load the Windows root certificates.\n"); |
| 328 | } |
| 329 | #endif /* OPENSSL_VERSION_NUMBER >= 0x030200000 */ |
| 330 | #endif /* _WIN32 */ |
| 331 | |
| @@ -999,12 +1001,12 @@ | |
| 1001 | fossil_print("\n" |
| 1002 | " The OpenSSL library is not used by this build of Fossil\n\n" |
| 1003 | ); |
| 1004 | } |
| 1005 | #else |
| 1006 | fossil_print("OpenSSL-version: %s (0x%09llx)\n", |
| 1007 | SSLeay_version(SSLEAY_VERSION), (unsigned long long)SSLeay()); |
| 1008 | if( verbose ){ |
| 1009 | fossil_print("\n" |
| 1010 | " The version of the OpenSSL library being used\n" |
| 1011 | " by this instance of Fossil. Version 3.0.0 or\n" |
| 1012 | " later is recommended.\n\n" |
| @@ -1061,20 +1063,18 @@ | |
| 1063 | " values are built into your OpenSSL library.\n\n" |
| 1064 | ); |
| 1065 | } |
| 1066 | |
| 1067 | #if defined(_WIN32) |
| 1068 | fossil_print(" OpenSSL-winstore: %s\n", |
| 1069 | (SSLeay()>=0x30200000 && SSLeay()!=0x30500000) ? "Yes" : "No"); |
| 1070 | if( verbose ){ |
| 1071 | fossil_print("\n" |
| 1072 | " OpenSSL 3.2.0, or newer, but not version 3.5.0 due to a bug,\n" |
| 1073 | " are able to use the root certificates managed by the Windows\n" |
| 1074 | " operating system. The installed root certificates are listed\n" |
| 1075 | " by the command:\n\n" |
| 1076 | " certutil -store \"ROOT\"\n\n" |
| 1077 | ); |
| 1078 | } |
| 1079 | #endif /* _WIN32 */ |
| 1080 | |
| @@ -1232,10 +1232,10 @@ | |
| 1232 | ** freed by the caller. |
| 1233 | */ |
| 1234 | char *fossil_openssl_version(void){ |
| 1235 | #if defined(FOSSIL_ENABLE_SSL) |
| 1236 | return mprintf("%s (0x%09x)\n", |
| 1237 | SSLeay_version(SSLEAY_VERSION), (sqlite3_uint64)SSLeay()); |
| 1238 | #else |
| 1239 | return mprintf("none"); |
| 1240 | #endif |
| 1241 | } |
| 1242 |
+55
-25
| --- src/info.c | ||
| +++ src/info.c | ||
| @@ -951,11 +951,11 @@ | ||
| 951 | 951 | const char *zOrigDate; |
| 952 | 952 | int okWiki = 0; |
| 953 | 953 | Blob wiki_read_links = BLOB_INITIALIZER; |
| 954 | 954 | Blob wiki_add_links = BLOB_INITIALIZER; |
| 955 | 955 | |
| 956 | - Th_Store("current_checkin", zName); | |
| 956 | + Th_StoreUnsafe("current_checkin", zName); | |
| 957 | 957 | style_header("Check-in [%S]", zUuid); |
| 958 | 958 | login_anonymous_available(); |
| 959 | 959 | zEUser = db_text(0, |
| 960 | 960 | "SELECT value FROM tagxref" |
| 961 | 961 | " WHERE tagid=%d AND rid=%d AND tagtype>0", |
| @@ -1182,14 +1182,14 @@ | ||
| 1182 | 1182 | @ %z(chref("button","%R/%s/%T?diff=2%s",zPage,zName,zW))\ |
| 1183 | 1183 | @ Side-by-Side Diff</a> |
| 1184 | 1184 | } |
| 1185 | 1185 | if( diffType!=0 ){ |
| 1186 | 1186 | if( *zW ){ |
| 1187 | - @ %z(chref("button","%R/%s/%T",zPage,zName)) | |
| 1187 | + @ %z(chref("button","%R/%s/%T?diff=%d",zPage,zName,diffType)) | |
| 1188 | 1188 | @ Show Whitespace Changes</a> |
| 1189 | 1189 | }else{ |
| 1190 | - @ %z(chref("button","%R/%s/%T?w",zPage,zName)) | |
| 1190 | + @ %z(chref("button","%R/%s/%T?diff=%d&w",zPage,zName,diffType)) | |
| 1191 | 1191 | @ Ignore Whitespace</a> |
| 1192 | 1192 | } |
| 1193 | 1193 | } |
| 1194 | 1194 | if( zParent ){ |
| 1195 | 1195 | @ %z(chref("button","%R/vpatch?from=%!S&to=%!S",zParent,zUuid)) |
| @@ -1970,14 +1970,16 @@ | ||
| 1970 | 1970 | diff_config_init(&DCfg, 0); |
| 1971 | 1971 | diffType = preferred_diff_type(); |
| 1972 | 1972 | if( P("from") && P("to") ){ |
| 1973 | 1973 | v1 = artifact_from_ci_and_filename("from"); |
| 1974 | 1974 | v2 = artifact_from_ci_and_filename("to"); |
| 1975 | + if( v1==0 || v2==0 ) fossil_redirect_home(); | |
| 1975 | 1976 | }else{ |
| 1976 | 1977 | Stmt q; |
| 1977 | 1978 | v1 = name_to_rid_www("v1"); |
| 1978 | 1979 | v2 = name_to_rid_www("v2"); |
| 1980 | + if( v1==0 || v2==0 ) fossil_redirect_home(); | |
| 1979 | 1981 | |
| 1980 | 1982 | /* If the two file versions being compared both have the same |
| 1981 | 1983 | ** filename, then offer an "Annotate" link that constructs an |
| 1982 | 1984 | ** annotation between those version. */ |
| 1983 | 1985 | db_prepare(&q, |
| @@ -2003,11 +2005,10 @@ | ||
| 2003 | 2005 | "%R/annotate?origin=%s&checkin=%s&filename=%T", |
| 2004 | 2006 | zOrig, zCkin, zFN); |
| 2005 | 2007 | } |
| 2006 | 2008 | db_finalize(&q); |
| 2007 | 2009 | } |
| 2008 | - if( v1==0 || v2==0 ) fossil_redirect_home(); | |
| 2009 | 2010 | zRe = P("regex"); |
| 2010 | 2011 | cgi_check_for_malice(); |
| 2011 | 2012 | if( zRe ) re_compile(&pRe, zRe, 0); |
| 2012 | 2013 | if( verbose ) objdescFlags |= OBJDESC_DETAIL; |
| 2013 | 2014 | if( isPatch ){ |
| @@ -2622,24 +2623,28 @@ | ||
| 2622 | 2623 | |
| 2623 | 2624 | /* |
| 2624 | 2625 | ** WEBPAGE: artifact |
| 2625 | 2626 | ** WEBPAGE: file |
| 2626 | 2627 | ** WEBPAGE: whatis |
| 2628 | +** WEBPAGE: docfile | |
| 2627 | 2629 | ** |
| 2628 | 2630 | ** Typical usage: |
| 2629 | 2631 | ** |
| 2630 | 2632 | ** /artifact/HASH |
| 2631 | 2633 | ** /whatis/HASH |
| 2632 | 2634 | ** /file/NAME |
| 2635 | +** /docfile/NAME | |
| 2633 | 2636 | ** |
| 2634 | 2637 | ** Additional query parameters: |
| 2635 | 2638 | ** |
| 2636 | 2639 | ** ln - show line numbers |
| 2637 | 2640 | ** ln=N - highlight line number N |
| 2638 | 2641 | ** ln=M-N - highlight lines M through N inclusive |
| 2639 | 2642 | ** ln=M-N+Y-Z - highlight lines M through N and Y through Z (inclusive) |
| 2640 | 2643 | ** verbose - show more detail in the description |
| 2644 | +** brief - show just the document, not the metadata. The | |
| 2645 | +** /docfile page is an alias for /file?brief | |
| 2641 | 2646 | ** download - redirect to the download (artifact page only) |
| 2642 | 2647 | ** name=NAME - filename or hash as a query parameter |
| 2643 | 2648 | ** filename=NAME - alternative spelling for "name=" |
| 2644 | 2649 | ** fn=NAME - alternative spelling for "name=" |
| 2645 | 2650 | ** ci=VERSION - The specific check-in to use with "name=" to |
| @@ -2676,10 +2681,11 @@ | ||
| 2676 | 2681 | int asText; |
| 2677 | 2682 | const char *zUuid = 0; |
| 2678 | 2683 | u32 objdescFlags = OBJDESC_BASE; |
| 2679 | 2684 | int descOnly = fossil_strcmp(g.zPath,"whatis")==0; |
| 2680 | 2685 | int hashOnly = P("hash")!=0; |
| 2686 | + int docOnly = P("brief")!=0; | |
| 2681 | 2687 | int isFile = fossil_strcmp(g.zPath,"file")==0; |
| 2682 | 2688 | const char *zLn = P("ln"); |
| 2683 | 2689 | const char *zName = P("name"); |
| 2684 | 2690 | const char *zCI = P("ci"); |
| 2685 | 2691 | HQuery url; |
| @@ -2690,10 +2696,14 @@ | ||
| 2690 | 2696 | |
| 2691 | 2697 | login_check_credentials(); |
| 2692 | 2698 | if( !g.perm.Read ){ login_needed(g.anon.Read); return; } |
| 2693 | 2699 | cgi_check_for_malice(); |
| 2694 | 2700 | style_set_current_feature("artifact"); |
| 2701 | + if( fossil_strcmp(g.zPath, "docfile")==0 ){ | |
| 2702 | + isFile = 1; | |
| 2703 | + docOnly = 1; | |
| 2704 | + } | |
| 2695 | 2705 | |
| 2696 | 2706 | /* Capture and normalize the name= and ci= query parameters */ |
| 2697 | 2707 | if( zName==0 ){ |
| 2698 | 2708 | zName = P("filename"); |
| 2699 | 2709 | if( zName==0 ){ |
| @@ -2802,11 +2812,13 @@ | ||
| 2802 | 2812 | return; |
| 2803 | 2813 | } |
| 2804 | 2814 | |
| 2805 | 2815 | asText = P("txt")!=0; |
| 2806 | 2816 | if( isFile ){ |
| 2807 | - if( zCI==0 || fossil_strcmp(zCI,"tip")==0 ){ | |
| 2817 | + if( docOnly ){ | |
| 2818 | + /* No header */ | |
| 2819 | + }else if( zCI==0 || fossil_strcmp(zCI,"tip")==0 ){ | |
| 2808 | 2820 | zCI = "tip"; |
| 2809 | 2821 | @ <h2>File %z(href("%R/finfo?name=%T&m&ci=tip",zName))%h(zName)</a> |
| 2810 | 2822 | @ from the %z(href("%R/info/tip"))latest check-in</a></h2> |
| 2811 | 2823 | }else{ |
| 2812 | 2824 | const char *zPath; |
| @@ -2824,17 +2836,19 @@ | ||
| 2824 | 2836 | }else{ |
| 2825 | 2837 | @ part of check-in %z(href("%R/info/%!S",zCIUuid))%S(zCIUuid)</a></h2> |
| 2826 | 2838 | } |
| 2827 | 2839 | blob_reset(&path); |
| 2828 | 2840 | } |
| 2829 | - style_submenu_element("Artifact", "%R/artifact/%S", zUuid); | |
| 2830 | 2841 | zMime = mimetype_from_name(zName); |
| 2831 | - style_submenu_element("Annotate", "%R/annotate?filename=%T&checkin=%T", | |
| 2832 | - zName, zCI); | |
| 2833 | - style_submenu_element("Blame", "%R/blame?filename=%T&checkin=%T", | |
| 2834 | - zName, zCI); | |
| 2835 | - style_submenu_element("Doc", "%R/doc/%T/%T", zCI, zName); | |
| 2842 | + if( !docOnly ){ | |
| 2843 | + style_submenu_element("Artifact", "%R/artifact/%S", zUuid); | |
| 2844 | + style_submenu_element("Annotate", "%R/annotate?filename=%T&checkin=%T", | |
| 2845 | + zName, zCI); | |
| 2846 | + style_submenu_element("Blame", "%R/blame?filename=%T&checkin=%T", | |
| 2847 | + zName, zCI); | |
| 2848 | + style_submenu_element("Doc", "%R/doc/%T/%T", zCI, zName); | |
| 2849 | + } | |
| 2836 | 2850 | blob_init(&downloadName, zName, -1); |
| 2837 | 2851 | objType = OBJTYPE_CONTENT; |
| 2838 | 2852 | }else{ |
| 2839 | 2853 | @ <h2>Artifact |
| 2840 | 2854 | style_copy_button(1, "hash-ar", 0, 2, "%s", zUuid); |
| @@ -2853,11 +2867,11 @@ | ||
| 2853 | 2867 | cgi_redirectf("%R/raw/%s?at=%T", |
| 2854 | 2868 | db_text("x", "SELECT uuid FROM blob WHERE rid=%d", rid), |
| 2855 | 2869 | file_tail(blob_str(&downloadName))); |
| 2856 | 2870 | /*NOTREACHED*/ |
| 2857 | 2871 | } |
| 2858 | - if( g.perm.Admin ){ | |
| 2872 | + if( g.perm.Admin && !docOnly ){ | |
| 2859 | 2873 | const char *zUuid = db_text("", "SELECT uuid FROM blob WHERE rid=%d", rid); |
| 2860 | 2874 | if( db_exists("SELECT 1 FROM shun WHERE uuid=%Q", zUuid) ){ |
| 2861 | 2875 | style_submenu_element("Unshun", "%R/shun?accept=%s&sub=1#accshun", zUuid); |
| 2862 | 2876 | }else{ |
| 2863 | 2877 | style_submenu_element("Shun", "%R/shun?shun=%s#addshun",zUuid); |
| @@ -2898,41 +2912,49 @@ | ||
| 2898 | 2912 | const char *zIp = db_column_text(&q,2); |
| 2899 | 2913 | @ <p>Received on %s(zDate) from %h(zUser) at %h(zIp).</p> |
| 2900 | 2914 | } |
| 2901 | 2915 | db_finalize(&q); |
| 2902 | 2916 | } |
| 2903 | - style_submenu_element("Download", "%R/raw/%s?at=%T", zUuid, file_tail(zName)); | |
| 2904 | - if( db_exists("SELECT 1 FROM mlink WHERE fid=%d", rid) ){ | |
| 2905 | - style_submenu_element("Check-ins Using", "%R/timeline?uf=%s", zUuid); | |
| 2917 | + if( !docOnly ){ | |
| 2918 | + style_submenu_element("Download", "%R/raw/%s?at=%T",zUuid,file_tail(zName)); | |
| 2919 | + if( db_exists("SELECT 1 FROM mlink WHERE fid=%d", rid) ){ | |
| 2920 | + style_submenu_element("Check-ins Using", "%R/timeline?uf=%s", zUuid); | |
| 2921 | + } | |
| 2906 | 2922 | } |
| 2907 | 2923 | if( zMime ){ |
| 2908 | 2924 | if( fossil_strcmp(zMime, "text/html")==0 ){ |
| 2909 | 2925 | if( asText ){ |
| 2910 | 2926 | style_submenu_element("Html", "%s", url_render(&url, "txt", 0, 0, 0)); |
| 2911 | 2927 | }else{ |
| 2912 | 2928 | renderAsHtml = 1; |
| 2913 | - style_submenu_element("Text", "%s", url_render(&url, "txt", "1", 0, 0)); | |
| 2929 | + if( !docOnly ){ | |
| 2930 | + style_submenu_element("Text", "%s", url_render(&url, "txt","1",0,0)); | |
| 2931 | + } | |
| 2914 | 2932 | } |
| 2915 | 2933 | }else if( fossil_strcmp(zMime, "text/x-fossil-wiki")==0 |
| 2916 | 2934 | || fossil_strcmp(zMime, "text/x-markdown")==0 |
| 2917 | 2935 | || fossil_strcmp(zMime, "text/x-pikchr")==0 ){ |
| 2918 | 2936 | if( asText ){ |
| 2919 | 2937 | style_submenu_element(zMime[7]=='p' ? "Pikchr" : "Wiki", |
| 2920 | 2938 | "%s", url_render(&url, "txt", 0, 0, 0)); |
| 2921 | 2939 | }else{ |
| 2922 | 2940 | renderAsWiki = 1; |
| 2923 | - style_submenu_element("Text", "%s", url_render(&url, "txt", "1", 0, 0)); | |
| 2941 | + if( !docOnly ){ | |
| 2942 | + style_submenu_element("Text", "%s", url_render(&url, "txt","1",0,0)); | |
| 2943 | + } | |
| 2924 | 2944 | } |
| 2925 | 2945 | }else if( fossil_strcmp(zMime, "image/svg+xml")==0 ){ |
| 2926 | 2946 | if( asText ){ |
| 2927 | 2947 | style_submenu_element("Svg", "%s", url_render(&url, "txt", 0, 0, 0)); |
| 2928 | 2948 | }else{ |
| 2929 | 2949 | renderAsSvg = 1; |
| 2930 | - style_submenu_element("Text", "%s", url_render(&url, "txt", "1", 0, 0)); | |
| 2950 | + if( !docOnly ){ | |
| 2951 | + style_submenu_element("Text", "%s", url_render(&url, "txt","1",0,0)); | |
| 2952 | + } | |
| 2931 | 2953 | } |
| 2932 | 2954 | } |
| 2933 | - if( fileedit_is_editable(zName) ){ | |
| 2955 | + if( !docOnly && fileedit_is_editable(zName) ){ | |
| 2934 | 2956 | style_submenu_element("Edit", |
| 2935 | 2957 | "%R/fileedit?filename=%T&checkin=%!S", |
| 2936 | 2958 | zName, zCI); |
| 2937 | 2959 | } |
| 2938 | 2960 | } |
| @@ -2940,11 +2962,13 @@ | ||
| 2940 | 2962 | style_submenu_element("Parsed", "%R/info/%s", zUuid); |
| 2941 | 2963 | } |
| 2942 | 2964 | if( descOnly ){ |
| 2943 | 2965 | style_submenu_element("Content", "%R/artifact/%s", zUuid); |
| 2944 | 2966 | }else{ |
| 2945 | - @ <hr> | |
| 2967 | + if( !docOnly || !isFile ){ | |
| 2968 | + @ <hr> | |
| 2969 | + } | |
| 2946 | 2970 | content_get(rid, &content); |
| 2947 | 2971 | if( renderAsWiki ){ |
| 2948 | 2972 | safe_html_context(DOCSRC_FILE); |
| 2949 | 2973 | wiki_render_by_mimetype(&content, zMime); |
| 2950 | 2974 | document_emit_js(); |
| @@ -3610,10 +3634,11 @@ | ||
| 3610 | 3634 | zNewColorFlag = P("newclr") ? " checked" : ""; |
| 3611 | 3635 | zNewTagFlag = P("newtag") ? " checked" : ""; |
| 3612 | 3636 | zNewTag = PDT("tagname",""); |
| 3613 | 3637 | zNewBrFlag = P("newbr") ? " checked" : ""; |
| 3614 | 3638 | zNewBranch = PDT("brname",""); |
| 3639 | + zBranchName = branch_of_rid(rid); | |
| 3615 | 3640 | zCloseFlag = P("close") ? " checked" : ""; |
| 3616 | 3641 | zHideFlag = P("hide") ? " checked" : ""; |
| 3617 | 3642 | if( P("apply") && cgi_csrf_safe(2) ){ |
| 3618 | 3643 | Blob ctrl; |
| 3619 | 3644 | char *zNow; |
| @@ -3657,17 +3682,25 @@ | ||
| 3657 | 3682 | zUuid[10] = 0; |
| 3658 | 3683 | style_header("Edit Check-in [%s]", zUuid); |
| 3659 | 3684 | if( P("preview") ){ |
| 3660 | 3685 | Blob suffix; |
| 3661 | 3686 | int nTag = 0; |
| 3687 | + const char *zDplyBr; /* Branch name used to determine BG color */ | |
| 3688 | + if( zNewBrFlag[0] && zNewBranch[0] ){ | |
| 3689 | + zDplyBr = zNewBranch; | |
| 3690 | + }else{ | |
| 3691 | + zDplyBr = zBranchName; | |
| 3692 | + } | |
| 3662 | 3693 | @ <b>Preview:</b> |
| 3663 | 3694 | @ <blockquote> |
| 3664 | 3695 | @ <table border=0> |
| 3665 | 3696 | if( zNewColorFlag[0] && zNewColor && zNewColor[0] ){ |
| 3666 | - @ <tr><td style="background-color: %h(zNewColor);"> | |
| 3697 | + @ <tr><td style="background-color:%h(reasonable_bg_color(zNewColor,0));"> | |
| 3667 | 3698 | }else if( zColor[0] ){ |
| 3668 | - @ <tr><td style="background-color: %h(zColor);"> | |
| 3699 | + @ <tr><td style="background-color:%h(reasonable_bg_color(zColor,0));"> | |
| 3700 | + }else if( zDplyBr && fossil_strcmp(zDplyBr,"trunk")!=0 ){ | |
| 3701 | + @ <tr><td style="background-color:%h(hash_color(zDplyBr));"> | |
| 3669 | 3702 | }else{ |
| 3670 | 3703 | @ <tr><td> |
| 3671 | 3704 | } |
| 3672 | 3705 | @ %!W(blob_str(&comment)) |
| 3673 | 3706 | blob_zero(&suffix); |
| @@ -3748,13 +3781,10 @@ | ||
| 3748 | 3781 | @ <tr><th align="right" valign="top">Tags:</th> |
| 3749 | 3782 | @ <td valign="top"> |
| 3750 | 3783 | @ <label><input type="checkbox" id="newtag" name="newtag"%s(zNewTagFlag)> |
| 3751 | 3784 | @ Add the following new tag name to this check-in:</label> |
| 3752 | 3785 | @ <input size="15" name="tagname" id="tagname" value="%h(zNewTag)"> |
| 3753 | - zBranchName = db_text(0, "SELECT value FROM tagxref, tag" | |
| 3754 | - " WHERE tagxref.rid=%d AND tagtype>0 AND tagxref.tagid=tag.tagid" | |
| 3755 | - " AND tagxref.tagid=%d", rid, TAG_BRANCH); | |
| 3756 | 3786 | db_prepare(&q, |
| 3757 | 3787 | "SELECT tag.tagid, tagname, tagxref.value FROM tagxref, tag" |
| 3758 | 3788 | " WHERE tagxref.rid=%d AND tagtype>0 AND tagxref.tagid=tag.tagid" |
| 3759 | 3789 | " ORDER BY CASE WHEN tagname GLOB 'sym-*' THEN substr(tagname,5)" |
| 3760 | 3790 | " ELSE tagname END /*sort*/", |
| 3761 | 3791 |
| --- src/info.c | |
| +++ src/info.c | |
| @@ -951,11 +951,11 @@ | |
| 951 | const char *zOrigDate; |
| 952 | int okWiki = 0; |
| 953 | Blob wiki_read_links = BLOB_INITIALIZER; |
| 954 | Blob wiki_add_links = BLOB_INITIALIZER; |
| 955 | |
| 956 | Th_Store("current_checkin", zName); |
| 957 | style_header("Check-in [%S]", zUuid); |
| 958 | login_anonymous_available(); |
| 959 | zEUser = db_text(0, |
| 960 | "SELECT value FROM tagxref" |
| 961 | " WHERE tagid=%d AND rid=%d AND tagtype>0", |
| @@ -1182,14 +1182,14 @@ | |
| 1182 | @ %z(chref("button","%R/%s/%T?diff=2%s",zPage,zName,zW))\ |
| 1183 | @ Side-by-Side Diff</a> |
| 1184 | } |
| 1185 | if( diffType!=0 ){ |
| 1186 | if( *zW ){ |
| 1187 | @ %z(chref("button","%R/%s/%T",zPage,zName)) |
| 1188 | @ Show Whitespace Changes</a> |
| 1189 | }else{ |
| 1190 | @ %z(chref("button","%R/%s/%T?w",zPage,zName)) |
| 1191 | @ Ignore Whitespace</a> |
| 1192 | } |
| 1193 | } |
| 1194 | if( zParent ){ |
| 1195 | @ %z(chref("button","%R/vpatch?from=%!S&to=%!S",zParent,zUuid)) |
| @@ -1970,14 +1970,16 @@ | |
| 1970 | diff_config_init(&DCfg, 0); |
| 1971 | diffType = preferred_diff_type(); |
| 1972 | if( P("from") && P("to") ){ |
| 1973 | v1 = artifact_from_ci_and_filename("from"); |
| 1974 | v2 = artifact_from_ci_and_filename("to"); |
| 1975 | }else{ |
| 1976 | Stmt q; |
| 1977 | v1 = name_to_rid_www("v1"); |
| 1978 | v2 = name_to_rid_www("v2"); |
| 1979 | |
| 1980 | /* If the two file versions being compared both have the same |
| 1981 | ** filename, then offer an "Annotate" link that constructs an |
| 1982 | ** annotation between those version. */ |
| 1983 | db_prepare(&q, |
| @@ -2003,11 +2005,10 @@ | |
| 2003 | "%R/annotate?origin=%s&checkin=%s&filename=%T", |
| 2004 | zOrig, zCkin, zFN); |
| 2005 | } |
| 2006 | db_finalize(&q); |
| 2007 | } |
| 2008 | if( v1==0 || v2==0 ) fossil_redirect_home(); |
| 2009 | zRe = P("regex"); |
| 2010 | cgi_check_for_malice(); |
| 2011 | if( zRe ) re_compile(&pRe, zRe, 0); |
| 2012 | if( verbose ) objdescFlags |= OBJDESC_DETAIL; |
| 2013 | if( isPatch ){ |
| @@ -2622,24 +2623,28 @@ | |
| 2622 | |
| 2623 | /* |
| 2624 | ** WEBPAGE: artifact |
| 2625 | ** WEBPAGE: file |
| 2626 | ** WEBPAGE: whatis |
| 2627 | ** |
| 2628 | ** Typical usage: |
| 2629 | ** |
| 2630 | ** /artifact/HASH |
| 2631 | ** /whatis/HASH |
| 2632 | ** /file/NAME |
| 2633 | ** |
| 2634 | ** Additional query parameters: |
| 2635 | ** |
| 2636 | ** ln - show line numbers |
| 2637 | ** ln=N - highlight line number N |
| 2638 | ** ln=M-N - highlight lines M through N inclusive |
| 2639 | ** ln=M-N+Y-Z - highlight lines M through N and Y through Z (inclusive) |
| 2640 | ** verbose - show more detail in the description |
| 2641 | ** download - redirect to the download (artifact page only) |
| 2642 | ** name=NAME - filename or hash as a query parameter |
| 2643 | ** filename=NAME - alternative spelling for "name=" |
| 2644 | ** fn=NAME - alternative spelling for "name=" |
| 2645 | ** ci=VERSION - The specific check-in to use with "name=" to |
| @@ -2676,10 +2681,11 @@ | |
| 2676 | int asText; |
| 2677 | const char *zUuid = 0; |
| 2678 | u32 objdescFlags = OBJDESC_BASE; |
| 2679 | int descOnly = fossil_strcmp(g.zPath,"whatis")==0; |
| 2680 | int hashOnly = P("hash")!=0; |
| 2681 | int isFile = fossil_strcmp(g.zPath,"file")==0; |
| 2682 | const char *zLn = P("ln"); |
| 2683 | const char *zName = P("name"); |
| 2684 | const char *zCI = P("ci"); |
| 2685 | HQuery url; |
| @@ -2690,10 +2696,14 @@ | |
| 2690 | |
| 2691 | login_check_credentials(); |
| 2692 | if( !g.perm.Read ){ login_needed(g.anon.Read); return; } |
| 2693 | cgi_check_for_malice(); |
| 2694 | style_set_current_feature("artifact"); |
| 2695 | |
| 2696 | /* Capture and normalize the name= and ci= query parameters */ |
| 2697 | if( zName==0 ){ |
| 2698 | zName = P("filename"); |
| 2699 | if( zName==0 ){ |
| @@ -2802,11 +2812,13 @@ | |
| 2802 | return; |
| 2803 | } |
| 2804 | |
| 2805 | asText = P("txt")!=0; |
| 2806 | if( isFile ){ |
| 2807 | if( zCI==0 || fossil_strcmp(zCI,"tip")==0 ){ |
| 2808 | zCI = "tip"; |
| 2809 | @ <h2>File %z(href("%R/finfo?name=%T&m&ci=tip",zName))%h(zName)</a> |
| 2810 | @ from the %z(href("%R/info/tip"))latest check-in</a></h2> |
| 2811 | }else{ |
| 2812 | const char *zPath; |
| @@ -2824,17 +2836,19 @@ | |
| 2824 | }else{ |
| 2825 | @ part of check-in %z(href("%R/info/%!S",zCIUuid))%S(zCIUuid)</a></h2> |
| 2826 | } |
| 2827 | blob_reset(&path); |
| 2828 | } |
| 2829 | style_submenu_element("Artifact", "%R/artifact/%S", zUuid); |
| 2830 | zMime = mimetype_from_name(zName); |
| 2831 | style_submenu_element("Annotate", "%R/annotate?filename=%T&checkin=%T", |
| 2832 | zName, zCI); |
| 2833 | style_submenu_element("Blame", "%R/blame?filename=%T&checkin=%T", |
| 2834 | zName, zCI); |
| 2835 | style_submenu_element("Doc", "%R/doc/%T/%T", zCI, zName); |
| 2836 | blob_init(&downloadName, zName, -1); |
| 2837 | objType = OBJTYPE_CONTENT; |
| 2838 | }else{ |
| 2839 | @ <h2>Artifact |
| 2840 | style_copy_button(1, "hash-ar", 0, 2, "%s", zUuid); |
| @@ -2853,11 +2867,11 @@ | |
| 2853 | cgi_redirectf("%R/raw/%s?at=%T", |
| 2854 | db_text("x", "SELECT uuid FROM blob WHERE rid=%d", rid), |
| 2855 | file_tail(blob_str(&downloadName))); |
| 2856 | /*NOTREACHED*/ |
| 2857 | } |
| 2858 | if( g.perm.Admin ){ |
| 2859 | const char *zUuid = db_text("", "SELECT uuid FROM blob WHERE rid=%d", rid); |
| 2860 | if( db_exists("SELECT 1 FROM shun WHERE uuid=%Q", zUuid) ){ |
| 2861 | style_submenu_element("Unshun", "%R/shun?accept=%s&sub=1#accshun", zUuid); |
| 2862 | }else{ |
| 2863 | style_submenu_element("Shun", "%R/shun?shun=%s#addshun",zUuid); |
| @@ -2898,41 +2912,49 @@ | |
| 2898 | const char *zIp = db_column_text(&q,2); |
| 2899 | @ <p>Received on %s(zDate) from %h(zUser) at %h(zIp).</p> |
| 2900 | } |
| 2901 | db_finalize(&q); |
| 2902 | } |
| 2903 | style_submenu_element("Download", "%R/raw/%s?at=%T", zUuid, file_tail(zName)); |
| 2904 | if( db_exists("SELECT 1 FROM mlink WHERE fid=%d", rid) ){ |
| 2905 | style_submenu_element("Check-ins Using", "%R/timeline?uf=%s", zUuid); |
| 2906 | } |
| 2907 | if( zMime ){ |
| 2908 | if( fossil_strcmp(zMime, "text/html")==0 ){ |
| 2909 | if( asText ){ |
| 2910 | style_submenu_element("Html", "%s", url_render(&url, "txt", 0, 0, 0)); |
| 2911 | }else{ |
| 2912 | renderAsHtml = 1; |
| 2913 | style_submenu_element("Text", "%s", url_render(&url, "txt", "1", 0, 0)); |
| 2914 | } |
| 2915 | }else if( fossil_strcmp(zMime, "text/x-fossil-wiki")==0 |
| 2916 | || fossil_strcmp(zMime, "text/x-markdown")==0 |
| 2917 | || fossil_strcmp(zMime, "text/x-pikchr")==0 ){ |
| 2918 | if( asText ){ |
| 2919 | style_submenu_element(zMime[7]=='p' ? "Pikchr" : "Wiki", |
| 2920 | "%s", url_render(&url, "txt", 0, 0, 0)); |
| 2921 | }else{ |
| 2922 | renderAsWiki = 1; |
| 2923 | style_submenu_element("Text", "%s", url_render(&url, "txt", "1", 0, 0)); |
| 2924 | } |
| 2925 | }else if( fossil_strcmp(zMime, "image/svg+xml")==0 ){ |
| 2926 | if( asText ){ |
| 2927 | style_submenu_element("Svg", "%s", url_render(&url, "txt", 0, 0, 0)); |
| 2928 | }else{ |
| 2929 | renderAsSvg = 1; |
| 2930 | style_submenu_element("Text", "%s", url_render(&url, "txt", "1", 0, 0)); |
| 2931 | } |
| 2932 | } |
| 2933 | if( fileedit_is_editable(zName) ){ |
| 2934 | style_submenu_element("Edit", |
| 2935 | "%R/fileedit?filename=%T&checkin=%!S", |
| 2936 | zName, zCI); |
| 2937 | } |
| 2938 | } |
| @@ -2940,11 +2962,13 @@ | |
| 2940 | style_submenu_element("Parsed", "%R/info/%s", zUuid); |
| 2941 | } |
| 2942 | if( descOnly ){ |
| 2943 | style_submenu_element("Content", "%R/artifact/%s", zUuid); |
| 2944 | }else{ |
| 2945 | @ <hr> |
| 2946 | content_get(rid, &content); |
| 2947 | if( renderAsWiki ){ |
| 2948 | safe_html_context(DOCSRC_FILE); |
| 2949 | wiki_render_by_mimetype(&content, zMime); |
| 2950 | document_emit_js(); |
| @@ -3610,10 +3634,11 @@ | |
| 3610 | zNewColorFlag = P("newclr") ? " checked" : ""; |
| 3611 | zNewTagFlag = P("newtag") ? " checked" : ""; |
| 3612 | zNewTag = PDT("tagname",""); |
| 3613 | zNewBrFlag = P("newbr") ? " checked" : ""; |
| 3614 | zNewBranch = PDT("brname",""); |
| 3615 | zCloseFlag = P("close") ? " checked" : ""; |
| 3616 | zHideFlag = P("hide") ? " checked" : ""; |
| 3617 | if( P("apply") && cgi_csrf_safe(2) ){ |
| 3618 | Blob ctrl; |
| 3619 | char *zNow; |
| @@ -3657,17 +3682,25 @@ | |
| 3657 | zUuid[10] = 0; |
| 3658 | style_header("Edit Check-in [%s]", zUuid); |
| 3659 | if( P("preview") ){ |
| 3660 | Blob suffix; |
| 3661 | int nTag = 0; |
| 3662 | @ <b>Preview:</b> |
| 3663 | @ <blockquote> |
| 3664 | @ <table border=0> |
| 3665 | if( zNewColorFlag[0] && zNewColor && zNewColor[0] ){ |
| 3666 | @ <tr><td style="background-color: %h(zNewColor);"> |
| 3667 | }else if( zColor[0] ){ |
| 3668 | @ <tr><td style="background-color: %h(zColor);"> |
| 3669 | }else{ |
| 3670 | @ <tr><td> |
| 3671 | } |
| 3672 | @ %!W(blob_str(&comment)) |
| 3673 | blob_zero(&suffix); |
| @@ -3748,13 +3781,10 @@ | |
| 3748 | @ <tr><th align="right" valign="top">Tags:</th> |
| 3749 | @ <td valign="top"> |
| 3750 | @ <label><input type="checkbox" id="newtag" name="newtag"%s(zNewTagFlag)> |
| 3751 | @ Add the following new tag name to this check-in:</label> |
| 3752 | @ <input size="15" name="tagname" id="tagname" value="%h(zNewTag)"> |
| 3753 | zBranchName = db_text(0, "SELECT value FROM tagxref, tag" |
| 3754 | " WHERE tagxref.rid=%d AND tagtype>0 AND tagxref.tagid=tag.tagid" |
| 3755 | " AND tagxref.tagid=%d", rid, TAG_BRANCH); |
| 3756 | db_prepare(&q, |
| 3757 | "SELECT tag.tagid, tagname, tagxref.value FROM tagxref, tag" |
| 3758 | " WHERE tagxref.rid=%d AND tagtype>0 AND tagxref.tagid=tag.tagid" |
| 3759 | " ORDER BY CASE WHEN tagname GLOB 'sym-*' THEN substr(tagname,5)" |
| 3760 | " ELSE tagname END /*sort*/", |
| 3761 |
| --- src/info.c | |
| +++ src/info.c | |
| @@ -951,11 +951,11 @@ | |
| 951 | const char *zOrigDate; |
| 952 | int okWiki = 0; |
| 953 | Blob wiki_read_links = BLOB_INITIALIZER; |
| 954 | Blob wiki_add_links = BLOB_INITIALIZER; |
| 955 | |
| 956 | Th_StoreUnsafe("current_checkin", zName); |
| 957 | style_header("Check-in [%S]", zUuid); |
| 958 | login_anonymous_available(); |
| 959 | zEUser = db_text(0, |
| 960 | "SELECT value FROM tagxref" |
| 961 | " WHERE tagid=%d AND rid=%d AND tagtype>0", |
| @@ -1182,14 +1182,14 @@ | |
| 1182 | @ %z(chref("button","%R/%s/%T?diff=2%s",zPage,zName,zW))\ |
| 1183 | @ Side-by-Side Diff</a> |
| 1184 | } |
| 1185 | if( diffType!=0 ){ |
| 1186 | if( *zW ){ |
| 1187 | @ %z(chref("button","%R/%s/%T?diff=%d",zPage,zName,diffType)) |
| 1188 | @ Show Whitespace Changes</a> |
| 1189 | }else{ |
| 1190 | @ %z(chref("button","%R/%s/%T?diff=%d&w",zPage,zName,diffType)) |
| 1191 | @ Ignore Whitespace</a> |
| 1192 | } |
| 1193 | } |
| 1194 | if( zParent ){ |
| 1195 | @ %z(chref("button","%R/vpatch?from=%!S&to=%!S",zParent,zUuid)) |
| @@ -1970,14 +1970,16 @@ | |
| 1970 | diff_config_init(&DCfg, 0); |
| 1971 | diffType = preferred_diff_type(); |
| 1972 | if( P("from") && P("to") ){ |
| 1973 | v1 = artifact_from_ci_and_filename("from"); |
| 1974 | v2 = artifact_from_ci_and_filename("to"); |
| 1975 | if( v1==0 || v2==0 ) fossil_redirect_home(); |
| 1976 | }else{ |
| 1977 | Stmt q; |
| 1978 | v1 = name_to_rid_www("v1"); |
| 1979 | v2 = name_to_rid_www("v2"); |
| 1980 | if( v1==0 || v2==0 ) fossil_redirect_home(); |
| 1981 | |
| 1982 | /* If the two file versions being compared both have the same |
| 1983 | ** filename, then offer an "Annotate" link that constructs an |
| 1984 | ** annotation between those version. */ |
| 1985 | db_prepare(&q, |
| @@ -2003,11 +2005,10 @@ | |
| 2005 | "%R/annotate?origin=%s&checkin=%s&filename=%T", |
| 2006 | zOrig, zCkin, zFN); |
| 2007 | } |
| 2008 | db_finalize(&q); |
| 2009 | } |
| 2010 | zRe = P("regex"); |
| 2011 | cgi_check_for_malice(); |
| 2012 | if( zRe ) re_compile(&pRe, zRe, 0); |
| 2013 | if( verbose ) objdescFlags |= OBJDESC_DETAIL; |
| 2014 | if( isPatch ){ |
| @@ -2622,24 +2623,28 @@ | |
| 2623 | |
| 2624 | /* |
| 2625 | ** WEBPAGE: artifact |
| 2626 | ** WEBPAGE: file |
| 2627 | ** WEBPAGE: whatis |
| 2628 | ** WEBPAGE: docfile |
| 2629 | ** |
| 2630 | ** Typical usage: |
| 2631 | ** |
| 2632 | ** /artifact/HASH |
| 2633 | ** /whatis/HASH |
| 2634 | ** /file/NAME |
| 2635 | ** /docfile/NAME |
| 2636 | ** |
| 2637 | ** Additional query parameters: |
| 2638 | ** |
| 2639 | ** ln - show line numbers |
| 2640 | ** ln=N - highlight line number N |
| 2641 | ** ln=M-N - highlight lines M through N inclusive |
| 2642 | ** ln=M-N+Y-Z - highlight lines M through N and Y through Z (inclusive) |
| 2643 | ** verbose - show more detail in the description |
| 2644 | ** brief - show just the document, not the metadata. The |
| 2645 | ** /docfile page is an alias for /file?brief |
| 2646 | ** download - redirect to the download (artifact page only) |
| 2647 | ** name=NAME - filename or hash as a query parameter |
| 2648 | ** filename=NAME - alternative spelling for "name=" |
| 2649 | ** fn=NAME - alternative spelling for "name=" |
| 2650 | ** ci=VERSION - The specific check-in to use with "name=" to |
| @@ -2676,10 +2681,11 @@ | |
| 2681 | int asText; |
| 2682 | const char *zUuid = 0; |
| 2683 | u32 objdescFlags = OBJDESC_BASE; |
| 2684 | int descOnly = fossil_strcmp(g.zPath,"whatis")==0; |
| 2685 | int hashOnly = P("hash")!=0; |
| 2686 | int docOnly = P("brief")!=0; |
| 2687 | int isFile = fossil_strcmp(g.zPath,"file")==0; |
| 2688 | const char *zLn = P("ln"); |
| 2689 | const char *zName = P("name"); |
| 2690 | const char *zCI = P("ci"); |
| 2691 | HQuery url; |
| @@ -2690,10 +2696,14 @@ | |
| 2696 | |
| 2697 | login_check_credentials(); |
| 2698 | if( !g.perm.Read ){ login_needed(g.anon.Read); return; } |
| 2699 | cgi_check_for_malice(); |
| 2700 | style_set_current_feature("artifact"); |
| 2701 | if( fossil_strcmp(g.zPath, "docfile")==0 ){ |
| 2702 | isFile = 1; |
| 2703 | docOnly = 1; |
| 2704 | } |
| 2705 | |
| 2706 | /* Capture and normalize the name= and ci= query parameters */ |
| 2707 | if( zName==0 ){ |
| 2708 | zName = P("filename"); |
| 2709 | if( zName==0 ){ |
| @@ -2802,11 +2812,13 @@ | |
| 2812 | return; |
| 2813 | } |
| 2814 | |
| 2815 | asText = P("txt")!=0; |
| 2816 | if( isFile ){ |
| 2817 | if( docOnly ){ |
| 2818 | /* No header */ |
| 2819 | }else if( zCI==0 || fossil_strcmp(zCI,"tip")==0 ){ |
| 2820 | zCI = "tip"; |
| 2821 | @ <h2>File %z(href("%R/finfo?name=%T&m&ci=tip",zName))%h(zName)</a> |
| 2822 | @ from the %z(href("%R/info/tip"))latest check-in</a></h2> |
| 2823 | }else{ |
| 2824 | const char *zPath; |
| @@ -2824,17 +2836,19 @@ | |
| 2836 | }else{ |
| 2837 | @ part of check-in %z(href("%R/info/%!S",zCIUuid))%S(zCIUuid)</a></h2> |
| 2838 | } |
| 2839 | blob_reset(&path); |
| 2840 | } |
| 2841 | zMime = mimetype_from_name(zName); |
| 2842 | if( !docOnly ){ |
| 2843 | style_submenu_element("Artifact", "%R/artifact/%S", zUuid); |
| 2844 | style_submenu_element("Annotate", "%R/annotate?filename=%T&checkin=%T", |
| 2845 | zName, zCI); |
| 2846 | style_submenu_element("Blame", "%R/blame?filename=%T&checkin=%T", |
| 2847 | zName, zCI); |
| 2848 | style_submenu_element("Doc", "%R/doc/%T/%T", zCI, zName); |
| 2849 | } |
| 2850 | blob_init(&downloadName, zName, -1); |
| 2851 | objType = OBJTYPE_CONTENT; |
| 2852 | }else{ |
| 2853 | @ <h2>Artifact |
| 2854 | style_copy_button(1, "hash-ar", 0, 2, "%s", zUuid); |
| @@ -2853,11 +2867,11 @@ | |
| 2867 | cgi_redirectf("%R/raw/%s?at=%T", |
| 2868 | db_text("x", "SELECT uuid FROM blob WHERE rid=%d", rid), |
| 2869 | file_tail(blob_str(&downloadName))); |
| 2870 | /*NOTREACHED*/ |
| 2871 | } |
| 2872 | if( g.perm.Admin && !docOnly ){ |
| 2873 | const char *zUuid = db_text("", "SELECT uuid FROM blob WHERE rid=%d", rid); |
| 2874 | if( db_exists("SELECT 1 FROM shun WHERE uuid=%Q", zUuid) ){ |
| 2875 | style_submenu_element("Unshun", "%R/shun?accept=%s&sub=1#accshun", zUuid); |
| 2876 | }else{ |
| 2877 | style_submenu_element("Shun", "%R/shun?shun=%s#addshun",zUuid); |
| @@ -2898,41 +2912,49 @@ | |
| 2912 | const char *zIp = db_column_text(&q,2); |
| 2913 | @ <p>Received on %s(zDate) from %h(zUser) at %h(zIp).</p> |
| 2914 | } |
| 2915 | db_finalize(&q); |
| 2916 | } |
| 2917 | if( !docOnly ){ |
| 2918 | style_submenu_element("Download", "%R/raw/%s?at=%T",zUuid,file_tail(zName)); |
| 2919 | if( db_exists("SELECT 1 FROM mlink WHERE fid=%d", rid) ){ |
| 2920 | style_submenu_element("Check-ins Using", "%R/timeline?uf=%s", zUuid); |
| 2921 | } |
| 2922 | } |
| 2923 | if( zMime ){ |
| 2924 | if( fossil_strcmp(zMime, "text/html")==0 ){ |
| 2925 | if( asText ){ |
| 2926 | style_submenu_element("Html", "%s", url_render(&url, "txt", 0, 0, 0)); |
| 2927 | }else{ |
| 2928 | renderAsHtml = 1; |
| 2929 | if( !docOnly ){ |
| 2930 | style_submenu_element("Text", "%s", url_render(&url, "txt","1",0,0)); |
| 2931 | } |
| 2932 | } |
| 2933 | }else if( fossil_strcmp(zMime, "text/x-fossil-wiki")==0 |
| 2934 | || fossil_strcmp(zMime, "text/x-markdown")==0 |
| 2935 | || fossil_strcmp(zMime, "text/x-pikchr")==0 ){ |
| 2936 | if( asText ){ |
| 2937 | style_submenu_element(zMime[7]=='p' ? "Pikchr" : "Wiki", |
| 2938 | "%s", url_render(&url, "txt", 0, 0, 0)); |
| 2939 | }else{ |
| 2940 | renderAsWiki = 1; |
| 2941 | if( !docOnly ){ |
| 2942 | style_submenu_element("Text", "%s", url_render(&url, "txt","1",0,0)); |
| 2943 | } |
| 2944 | } |
| 2945 | }else if( fossil_strcmp(zMime, "image/svg+xml")==0 ){ |
| 2946 | if( asText ){ |
| 2947 | style_submenu_element("Svg", "%s", url_render(&url, "txt", 0, 0, 0)); |
| 2948 | }else{ |
| 2949 | renderAsSvg = 1; |
| 2950 | if( !docOnly ){ |
| 2951 | style_submenu_element("Text", "%s", url_render(&url, "txt","1",0,0)); |
| 2952 | } |
| 2953 | } |
| 2954 | } |
| 2955 | if( !docOnly && fileedit_is_editable(zName) ){ |
| 2956 | style_submenu_element("Edit", |
| 2957 | "%R/fileedit?filename=%T&checkin=%!S", |
| 2958 | zName, zCI); |
| 2959 | } |
| 2960 | } |
| @@ -2940,11 +2962,13 @@ | |
| 2962 | style_submenu_element("Parsed", "%R/info/%s", zUuid); |
| 2963 | } |
| 2964 | if( descOnly ){ |
| 2965 | style_submenu_element("Content", "%R/artifact/%s", zUuid); |
| 2966 | }else{ |
| 2967 | if( !docOnly || !isFile ){ |
| 2968 | @ <hr> |
| 2969 | } |
| 2970 | content_get(rid, &content); |
| 2971 | if( renderAsWiki ){ |
| 2972 | safe_html_context(DOCSRC_FILE); |
| 2973 | wiki_render_by_mimetype(&content, zMime); |
| 2974 | document_emit_js(); |
| @@ -3610,10 +3634,11 @@ | |
| 3634 | zNewColorFlag = P("newclr") ? " checked" : ""; |
| 3635 | zNewTagFlag = P("newtag") ? " checked" : ""; |
| 3636 | zNewTag = PDT("tagname",""); |
| 3637 | zNewBrFlag = P("newbr") ? " checked" : ""; |
| 3638 | zNewBranch = PDT("brname",""); |
| 3639 | zBranchName = branch_of_rid(rid); |
| 3640 | zCloseFlag = P("close") ? " checked" : ""; |
| 3641 | zHideFlag = P("hide") ? " checked" : ""; |
| 3642 | if( P("apply") && cgi_csrf_safe(2) ){ |
| 3643 | Blob ctrl; |
| 3644 | char *zNow; |
| @@ -3657,17 +3682,25 @@ | |
| 3682 | zUuid[10] = 0; |
| 3683 | style_header("Edit Check-in [%s]", zUuid); |
| 3684 | if( P("preview") ){ |
| 3685 | Blob suffix; |
| 3686 | int nTag = 0; |
| 3687 | const char *zDplyBr; /* Branch name used to determine BG color */ |
| 3688 | if( zNewBrFlag[0] && zNewBranch[0] ){ |
| 3689 | zDplyBr = zNewBranch; |
| 3690 | }else{ |
| 3691 | zDplyBr = zBranchName; |
| 3692 | } |
| 3693 | @ <b>Preview:</b> |
| 3694 | @ <blockquote> |
| 3695 | @ <table border=0> |
| 3696 | if( zNewColorFlag[0] && zNewColor && zNewColor[0] ){ |
| 3697 | @ <tr><td style="background-color:%h(reasonable_bg_color(zNewColor,0));"> |
| 3698 | }else if( zColor[0] ){ |
| 3699 | @ <tr><td style="background-color:%h(reasonable_bg_color(zColor,0));"> |
| 3700 | }else if( zDplyBr && fossil_strcmp(zDplyBr,"trunk")!=0 ){ |
| 3701 | @ <tr><td style="background-color:%h(hash_color(zDplyBr));"> |
| 3702 | }else{ |
| 3703 | @ <tr><td> |
| 3704 | } |
| 3705 | @ %!W(blob_str(&comment)) |
| 3706 | blob_zero(&suffix); |
| @@ -3748,13 +3781,10 @@ | |
| 3781 | @ <tr><th align="right" valign="top">Tags:</th> |
| 3782 | @ <td valign="top"> |
| 3783 | @ <label><input type="checkbox" id="newtag" name="newtag"%s(zNewTagFlag)> |
| 3784 | @ Add the following new tag name to this check-in:</label> |
| 3785 | @ <input size="15" name="tagname" id="tagname" value="%h(zNewTag)"> |
| 3786 | db_prepare(&q, |
| 3787 | "SELECT tag.tagid, tagname, tagxref.value FROM tagxref, tag" |
| 3788 | " WHERE tagxref.rid=%d AND tagtype>0 AND tagxref.tagid=tag.tagid" |
| 3789 | " ORDER BY CASE WHEN tagname GLOB 'sym-*' THEN substr(tagname,5)" |
| 3790 | " ELSE tagname END /*sort*/", |
| 3791 |
+21
-8
| --- src/loadctrl.c | ||
| +++ src/loadctrl.c | ||
| @@ -43,10 +43,30 @@ | ||
| 43 | 43 | ** Print the load average on the host machine. |
| 44 | 44 | */ |
| 45 | 45 | void loadavg_test_cmd(void){ |
| 46 | 46 | fossil_print("load-average: %f\n", load_average()); |
| 47 | 47 | } |
| 48 | + | |
| 49 | +/* | |
| 50 | +** WEBPAGE: test-overload | |
| 51 | +** | |
| 52 | +** Generate the response that would normally be shown only when | |
| 53 | +** service is denied due to an overload condition. This is for | |
| 54 | +** testing of the overload warning page. | |
| 55 | +*/ | |
| 56 | +void overload_page(void){ | |
| 57 | + double mxLoad = atof(db_get("max-loadavg", "0.0")); | |
| 58 | + style_set_current_feature("test"); | |
| 59 | + style_header("Server Overload"); | |
| 60 | + @ <h2>The server load is currently too high. | |
| 61 | + @ Please try again later.</h2> | |
| 62 | + @ <p>Current load average: %f(load_average())<br> | |
| 63 | + @ Load average limit: %f(mxLoad)<br> | |
| 64 | + @ URL: %h(g.zBaseURL)%h(P("PATH_INFO"))<br> | |
| 65 | + @ Timestamp: %h(db_text("","SELECT datetime()"))Z</p> | |
| 66 | + style_finish_page(); | |
| 67 | +} | |
| 48 | 68 | |
| 49 | 69 | /* |
| 50 | 70 | ** Abort the current page request if the load average of the host |
| 51 | 71 | ** computer is too high. Admin and Setup users are exempt from this |
| 52 | 72 | ** restriction. |
| @@ -60,17 +80,10 @@ | ||
| 60 | 80 | login_check_credentials(); |
| 61 | 81 | if(g.perm.Admin || g.perm.Setup){ |
| 62 | 82 | return; |
| 63 | 83 | } |
| 64 | 84 | #endif |
| 65 | - | |
| 66 | - style_set_current_feature("test"); | |
| 67 | - style_header("Server Overload"); | |
| 68 | - @ <h2>The server load is currently too high. | |
| 69 | - @ Please try again later.</h2> | |
| 70 | - @ <p>Current load average: %f(load_average()).<br> | |
| 71 | - @ Load average limit: %f(mxLoad)</p> | |
| 72 | - style_finish_page(); | |
| 85 | + overload_page(); | |
| 73 | 86 | cgi_set_status(503,"Server Overload"); |
| 74 | 87 | cgi_reply(); |
| 75 | 88 | exit(0); |
| 76 | 89 | } |
| 77 | 90 |
| --- src/loadctrl.c | |
| +++ src/loadctrl.c | |
| @@ -43,10 +43,30 @@ | |
| 43 | ** Print the load average on the host machine. |
| 44 | */ |
| 45 | void loadavg_test_cmd(void){ |
| 46 | fossil_print("load-average: %f\n", load_average()); |
| 47 | } |
| 48 | |
| 49 | /* |
| 50 | ** Abort the current page request if the load average of the host |
| 51 | ** computer is too high. Admin and Setup users are exempt from this |
| 52 | ** restriction. |
| @@ -60,17 +80,10 @@ | |
| 60 | login_check_credentials(); |
| 61 | if(g.perm.Admin || g.perm.Setup){ |
| 62 | return; |
| 63 | } |
| 64 | #endif |
| 65 | |
| 66 | style_set_current_feature("test"); |
| 67 | style_header("Server Overload"); |
| 68 | @ <h2>The server load is currently too high. |
| 69 | @ Please try again later.</h2> |
| 70 | @ <p>Current load average: %f(load_average()).<br> |
| 71 | @ Load average limit: %f(mxLoad)</p> |
| 72 | style_finish_page(); |
| 73 | cgi_set_status(503,"Server Overload"); |
| 74 | cgi_reply(); |
| 75 | exit(0); |
| 76 | } |
| 77 |
| --- src/loadctrl.c | |
| +++ src/loadctrl.c | |
| @@ -43,10 +43,30 @@ | |
| 43 | ** Print the load average on the host machine. |
| 44 | */ |
| 45 | void loadavg_test_cmd(void){ |
| 46 | fossil_print("load-average: %f\n", load_average()); |
| 47 | } |
| 48 | |
| 49 | /* |
| 50 | ** WEBPAGE: test-overload |
| 51 | ** |
| 52 | ** Generate the response that would normally be shown only when |
| 53 | ** service is denied due to an overload condition. This is for |
| 54 | ** testing of the overload warning page. |
| 55 | */ |
| 56 | void overload_page(void){ |
| 57 | double mxLoad = atof(db_get("max-loadavg", "0.0")); |
| 58 | style_set_current_feature("test"); |
| 59 | style_header("Server Overload"); |
| 60 | @ <h2>The server load is currently too high. |
| 61 | @ Please try again later.</h2> |
| 62 | @ <p>Current load average: %f(load_average())<br> |
| 63 | @ Load average limit: %f(mxLoad)<br> |
| 64 | @ URL: %h(g.zBaseURL)%h(P("PATH_INFO"))<br> |
| 65 | @ Timestamp: %h(db_text("","SELECT datetime()"))Z</p> |
| 66 | style_finish_page(); |
| 67 | } |
| 68 | |
| 69 | /* |
| 70 | ** Abort the current page request if the load average of the host |
| 71 | ** computer is too high. Admin and Setup users are exempt from this |
| 72 | ** restriction. |
| @@ -60,17 +80,10 @@ | |
| 80 | login_check_credentials(); |
| 81 | if(g.perm.Admin || g.perm.Setup){ |
| 82 | return; |
| 83 | } |
| 84 | #endif |
| 85 | overload_page(); |
| 86 | cgi_set_status(503,"Server Overload"); |
| 87 | cgi_reply(); |
| 88 | exit(0); |
| 89 | } |
| 90 |
+18
-2
| --- src/login.c | ||
| +++ src/login.c | ||
| @@ -1299,20 +1299,36 @@ | ||
| 1299 | 1299 | ** that should restrict robot access. No restrictions |
| 1300 | 1300 | ** are applied if this setting is undefined or is |
| 1301 | 1301 | ** an empty string. |
| 1302 | 1302 | */ |
| 1303 | 1303 | void login_restrict_robot_access(void){ |
| 1304 | - const char *zReferer; | |
| 1305 | 1304 | const char *zGlob; |
| 1306 | 1305 | int isMatch = 1; |
| 1307 | 1306 | int nQP; /* Number of query parameters other than name= */ |
| 1308 | 1307 | if( g.zLogin!=0 ) return; |
| 1309 | 1308 | zGlob = db_get("robot-restrict",0); |
| 1310 | 1309 | if( zGlob==0 || zGlob[0]==0 ) return; |
| 1311 | 1310 | if( g.isHuman ){ |
| 1311 | + const char *zReferer; | |
| 1312 | + const char *zAccept; | |
| 1313 | + const char *zBr; | |
| 1312 | 1314 | zReferer = P("HTTP_REFERER"); |
| 1313 | 1315 | if( zReferer && zReferer[0]!=0 ) return; |
| 1316 | + | |
| 1317 | + /* Robots typically do not accept the brotli encoding, at least not | |
| 1318 | + ** at the time of this writing (2025-04-01), but standard web-browser | |
| 1319 | + ** all generally do accept brotli. So if brotli is accepted, | |
| 1320 | + ** assume we are not talking to a robot. We might want to revisit this | |
| 1321 | + ** heuristic in the future... | |
| 1322 | + */ | |
| 1323 | + if( (zAccept = P("HTTP_ACCEPT_ENCODING"))!=0 | |
| 1324 | + && (zBr = strstr(zAccept,"br"))!=0 | |
| 1325 | + && !fossil_isalnum(zBr[2]) | |
| 1326 | + && (zBr==zAccept || !fossil_isalnum(zBr[-1])) | |
| 1327 | + ){ | |
| 1328 | + return; | |
| 1329 | + } | |
| 1314 | 1330 | } |
| 1315 | 1331 | nQP = cgi_qp_count(); |
| 1316 | 1332 | if( nQP<1 ) return; |
| 1317 | 1333 | isMatch = glob_multi_match(zGlob, g.zPath); |
| 1318 | 1334 | if( !isMatch ) return; |
| @@ -1400,11 +1416,11 @@ | ||
| 1400 | 1416 | */ |
| 1401 | 1417 | zIpAddr = PD("REMOTE_ADDR","nil"); |
| 1402 | 1418 | if( ( cgi_is_loopback(zIpAddr) |
| 1403 | 1419 | || (g.fSshClient & CGI_SSH_CLIENT)!=0 ) |
| 1404 | 1420 | && g.useLocalauth |
| 1405 | - && db_get_int("localauth",0)==0 | |
| 1421 | + && db_get_boolean("localauth",0)==0 | |
| 1406 | 1422 | && P("HTTPS")==0 |
| 1407 | 1423 | ){ |
| 1408 | 1424 | char *zSeed; |
| 1409 | 1425 | if( g.localOpen ) zLogin = db_lget("default-user",0); |
| 1410 | 1426 | if( zLogin!=0 ){ |
| 1411 | 1427 |
| --- src/login.c | |
| +++ src/login.c | |
| @@ -1299,20 +1299,36 @@ | |
| 1299 | ** that should restrict robot access. No restrictions |
| 1300 | ** are applied if this setting is undefined or is |
| 1301 | ** an empty string. |
| 1302 | */ |
| 1303 | void login_restrict_robot_access(void){ |
| 1304 | const char *zReferer; |
| 1305 | const char *zGlob; |
| 1306 | int isMatch = 1; |
| 1307 | int nQP; /* Number of query parameters other than name= */ |
| 1308 | if( g.zLogin!=0 ) return; |
| 1309 | zGlob = db_get("robot-restrict",0); |
| 1310 | if( zGlob==0 || zGlob[0]==0 ) return; |
| 1311 | if( g.isHuman ){ |
| 1312 | zReferer = P("HTTP_REFERER"); |
| 1313 | if( zReferer && zReferer[0]!=0 ) return; |
| 1314 | } |
| 1315 | nQP = cgi_qp_count(); |
| 1316 | if( nQP<1 ) return; |
| 1317 | isMatch = glob_multi_match(zGlob, g.zPath); |
| 1318 | if( !isMatch ) return; |
| @@ -1400,11 +1416,11 @@ | |
| 1400 | */ |
| 1401 | zIpAddr = PD("REMOTE_ADDR","nil"); |
| 1402 | if( ( cgi_is_loopback(zIpAddr) |
| 1403 | || (g.fSshClient & CGI_SSH_CLIENT)!=0 ) |
| 1404 | && g.useLocalauth |
| 1405 | && db_get_int("localauth",0)==0 |
| 1406 | && P("HTTPS")==0 |
| 1407 | ){ |
| 1408 | char *zSeed; |
| 1409 | if( g.localOpen ) zLogin = db_lget("default-user",0); |
| 1410 | if( zLogin!=0 ){ |
| 1411 |
| --- src/login.c | |
| +++ src/login.c | |
| @@ -1299,20 +1299,36 @@ | |
| 1299 | ** that should restrict robot access. No restrictions |
| 1300 | ** are applied if this setting is undefined or is |
| 1301 | ** an empty string. |
| 1302 | */ |
| 1303 | void login_restrict_robot_access(void){ |
| 1304 | const char *zGlob; |
| 1305 | int isMatch = 1; |
| 1306 | int nQP; /* Number of query parameters other than name= */ |
| 1307 | if( g.zLogin!=0 ) return; |
| 1308 | zGlob = db_get("robot-restrict",0); |
| 1309 | if( zGlob==0 || zGlob[0]==0 ) return; |
| 1310 | if( g.isHuman ){ |
| 1311 | const char *zReferer; |
| 1312 | const char *zAccept; |
| 1313 | const char *zBr; |
| 1314 | zReferer = P("HTTP_REFERER"); |
| 1315 | if( zReferer && zReferer[0]!=0 ) return; |
| 1316 | |
| 1317 | /* Robots typically do not accept the brotli encoding, at least not |
| 1318 | ** at the time of this writing (2025-04-01), but standard web-browser |
| 1319 | ** all generally do accept brotli. So if brotli is accepted, |
| 1320 | ** assume we are not talking to a robot. We might want to revisit this |
| 1321 | ** heuristic in the future... |
| 1322 | */ |
| 1323 | if( (zAccept = P("HTTP_ACCEPT_ENCODING"))!=0 |
| 1324 | && (zBr = strstr(zAccept,"br"))!=0 |
| 1325 | && !fossil_isalnum(zBr[2]) |
| 1326 | && (zBr==zAccept || !fossil_isalnum(zBr[-1])) |
| 1327 | ){ |
| 1328 | return; |
| 1329 | } |
| 1330 | } |
| 1331 | nQP = cgi_qp_count(); |
| 1332 | if( nQP<1 ) return; |
| 1333 | isMatch = glob_multi_match(zGlob, g.zPath); |
| 1334 | if( !isMatch ) return; |
| @@ -1400,11 +1416,11 @@ | |
| 1416 | */ |
| 1417 | zIpAddr = PD("REMOTE_ADDR","nil"); |
| 1418 | if( ( cgi_is_loopback(zIpAddr) |
| 1419 | || (g.fSshClient & CGI_SSH_CLIENT)!=0 ) |
| 1420 | && g.useLocalauth |
| 1421 | && db_get_boolean("localauth",0)==0 |
| 1422 | && P("HTTPS")==0 |
| 1423 | ){ |
| 1424 | char *zSeed; |
| 1425 | if( g.localOpen ) zLogin = db_lget("default-user",0); |
| 1426 | if( zLogin!=0 ){ |
| 1427 |
+33
-21
| --- src/lookslike.c | ||
| +++ src/lookslike.c | ||
| @@ -478,54 +478,66 @@ | ||
| 478 | 478 | } |
| 479 | 479 | |
| 480 | 480 | /* |
| 481 | 481 | ** Returns true if the given text contains certain keywords or |
| 482 | 482 | ** punctuation which indicate that it might be an SQL injection attempt |
| 483 | -** or some other kind of mischief. | |
| 483 | +** or Cross-site scripting attempt or some other kind of mischief. | |
| 484 | 484 | ** |
| 485 | -** This is not a defense against vulnerabilities in the Fossil code. | |
| 486 | -** Rather, this is part of an effort to do early detection of malicious | |
| 487 | -** spiders to avoid them using up too many CPU cycles. | |
| 485 | +** This is not a primary defense against vulnerabilities in the Fossil | |
| 486 | +** code. Rather, this is part of an effort to do early detection of malicious | |
| 487 | +** spiders to avoid them using up too many CPU cycles. Or, this routine | |
| 488 | +** can also be thought of as a secondary layer of defense against attacks. | |
| 488 | 489 | */ |
| 489 | -int looks_like_sql_injection(const char *zTxt){ | |
| 490 | +int looks_like_attack(const char *zTxt){ | |
| 490 | 491 | unsigned int i; |
| 492 | + int rc = 0; | |
| 491 | 493 | if( zTxt==0 ) return 0; |
| 492 | 494 | for(i=0; zTxt[i]; i++){ |
| 493 | 495 | switch( zTxt[i] ){ |
| 496 | + case '<': | |
| 494 | 497 | case ';': |
| 495 | 498 | case '\'': |
| 496 | 499 | return 1; |
| 497 | 500 | case '/': /* 0123456789 123456789 */ |
| 498 | - if( strncmp(zTxt+i+1, "/wp-content/plugins/", 20)==0 ) return 1; | |
| 499 | - if( strncmp(zTxt+i+1, "/wp-admin/admin-ajax", 20)==0 ) return 1; | |
| 501 | + if( strncmp(zTxt+i+1, "/wp-content/plugins/", 20)==0 ) rc = 1; | |
| 502 | + if( strncmp(zTxt+i+1, "/wp-admin/admin-ajax", 20)==0 ) rc = 1; | |
| 500 | 503 | break; |
| 501 | 504 | case 'a': |
| 502 | 505 | case 'A': |
| 503 | - if( isWholeWord(zTxt, i, "and", 3) ) return 1; | |
| 506 | + if( isWholeWord(zTxt, i, "and", 3) ) rc = 1; | |
| 504 | 507 | break; |
| 505 | 508 | case 'n': |
| 506 | 509 | case 'N': |
| 507 | - if( isWholeWord(zTxt, i, "null", 4) ) return 1; | |
| 510 | + if( isWholeWord(zTxt, i, "null", 4) ) rc = 1; | |
| 508 | 511 | break; |
| 509 | 512 | case 'o': |
| 510 | 513 | case 'O': |
| 511 | 514 | if( isWholeWord(zTxt, i, "order", 5) && fossil_isspace(zTxt[i+5]) ){ |
| 512 | - return 1; | |
| 515 | + rc = 1; | |
| 513 | 516 | } |
| 514 | - if( isWholeWord(zTxt, i, "or", 2) ) return 1; | |
| 517 | + if( isWholeWord(zTxt, i, "or", 2) ) rc = 1; | |
| 515 | 518 | break; |
| 516 | 519 | case 's': |
| 517 | 520 | case 'S': |
| 518 | - if( isWholeWord(zTxt, i, "select", 6) ) return 1; | |
| 521 | + if( isWholeWord(zTxt, i, "select", 6) ) rc = 1; | |
| 519 | 522 | break; |
| 520 | 523 | case 'w': |
| 521 | 524 | case 'W': |
| 522 | - if( isWholeWord(zTxt, i, "waitfor", 7) ) return 1; | |
| 525 | + if( isWholeWord(zTxt, i, "waitfor", 7) ) rc = 1; | |
| 523 | 526 | break; |
| 524 | 527 | } |
| 525 | 528 | } |
| 526 | - return 0; | |
| 529 | + if( rc ){ | |
| 530 | + /* The test/markdown-test3.md document which is part of the Fossil source | |
| 531 | + ** tree intentionally tries to fake an attack. Do not report such | |
| 532 | + ** errors. */ | |
| 533 | + const char *zPathInfo = P("PATH_INFO"); | |
| 534 | + if( sqlite3_strglob("/doc/*/test/markdown-test3.md", zPathInfo)==0 ){ | |
| 535 | + rc = 0; | |
| 536 | + } | |
| 537 | + } | |
| 538 | + return rc; | |
| 527 | 539 | } |
| 528 | 540 | |
| 529 | 541 | /* |
| 530 | 542 | ** This is a utility routine associated with the test-looks-like-sql-injection |
| 531 | 543 | ** command. |
| @@ -534,11 +546,11 @@ | ||
| 534 | 546 | ** might be SQL injection. |
| 535 | 547 | ** |
| 536 | 548 | ** Or if bInvert is true, then show the opposite - those lines that do NOT |
| 537 | 549 | ** look like SQL injection. |
| 538 | 550 | */ |
| 539 | -static void show_sql_injection_lines( | |
| 551 | +static void show_attack_lines( | |
| 540 | 552 | const char *zInFile, /* Name of input file */ |
| 541 | 553 | int bInvert, /* Invert the sense of the output (-v) */ |
| 542 | 554 | int bDeHttpize /* De-httpize the inputs. (-d) */ |
| 543 | 555 | ){ |
| 544 | 556 | FILE *in; |
| @@ -551,34 +563,34 @@ | ||
| 551 | 563 | fossil_fatal("cannot open \"%s\" for reading\n", zInFile); |
| 552 | 564 | } |
| 553 | 565 | } |
| 554 | 566 | while( fgets(zLine, sizeof(zLine), in) ){ |
| 555 | 567 | dehttpize(zLine); |
| 556 | - if( (looks_like_sql_injection(zLine)!=0) ^ bInvert ){ | |
| 568 | + if( (looks_like_attack(zLine)!=0) ^ bInvert ){ | |
| 557 | 569 | fossil_print("%s", zLine); |
| 558 | 570 | } |
| 559 | 571 | } |
| 560 | 572 | if( in!=stdin ) fclose(in); |
| 561 | 573 | } |
| 562 | 574 | |
| 563 | 575 | /* |
| 564 | -** COMMAND: test-looks-like-sql-injection | |
| 576 | +** COMMAND: test-looks-like-attack | |
| 565 | 577 | ** |
| 566 | 578 | ** Read lines of input from files named as arguments (or from standard |
| 567 | 579 | ** input if no arguments are provided) and print those that look like they |
| 568 | 580 | ** might be part of an SQL injection attack. |
| 569 | 581 | ** |
| 570 | -** Used to test the looks_lide_sql_injection() utility subroutine, possibly | |
| 582 | +** Used to test the looks_lile_attack() utility subroutine, possibly | |
| 571 | 583 | ** by piping in actual server log data. |
| 572 | 584 | */ |
| 573 | -void test_looks_like_sql_injection(void){ | |
| 585 | +void test_looks_like_attack(void){ | |
| 574 | 586 | int i; |
| 575 | 587 | int bInvert = find_option("invert","v",0)!=0; |
| 576 | 588 | int bDeHttpize = find_option("dehttpize","d",0)!=0; |
| 577 | 589 | verify_all_options(); |
| 578 | 590 | if( g.argc==2 ){ |
| 579 | - show_sql_injection_lines(0, bInvert, bDeHttpize); | |
| 591 | + show_attack_lines(0, bInvert, bDeHttpize); | |
| 580 | 592 | } |
| 581 | 593 | for(i=2; i<g.argc; i++){ |
| 582 | - show_sql_injection_lines(g.argv[i], bInvert, bDeHttpize); | |
| 594 | + show_attack_lines(g.argv[i], bInvert, bDeHttpize); | |
| 583 | 595 | } |
| 584 | 596 | } |
| 585 | 597 |
| --- src/lookslike.c | |
| +++ src/lookslike.c | |
| @@ -478,54 +478,66 @@ | |
| 478 | } |
| 479 | |
| 480 | /* |
| 481 | ** Returns true if the given text contains certain keywords or |
| 482 | ** punctuation which indicate that it might be an SQL injection attempt |
| 483 | ** or some other kind of mischief. |
| 484 | ** |
| 485 | ** This is not a defense against vulnerabilities in the Fossil code. |
| 486 | ** Rather, this is part of an effort to do early detection of malicious |
| 487 | ** spiders to avoid them using up too many CPU cycles. |
| 488 | */ |
| 489 | int looks_like_sql_injection(const char *zTxt){ |
| 490 | unsigned int i; |
| 491 | if( zTxt==0 ) return 0; |
| 492 | for(i=0; zTxt[i]; i++){ |
| 493 | switch( zTxt[i] ){ |
| 494 | case ';': |
| 495 | case '\'': |
| 496 | return 1; |
| 497 | case '/': /* 0123456789 123456789 */ |
| 498 | if( strncmp(zTxt+i+1, "/wp-content/plugins/", 20)==0 ) return 1; |
| 499 | if( strncmp(zTxt+i+1, "/wp-admin/admin-ajax", 20)==0 ) return 1; |
| 500 | break; |
| 501 | case 'a': |
| 502 | case 'A': |
| 503 | if( isWholeWord(zTxt, i, "and", 3) ) return 1; |
| 504 | break; |
| 505 | case 'n': |
| 506 | case 'N': |
| 507 | if( isWholeWord(zTxt, i, "null", 4) ) return 1; |
| 508 | break; |
| 509 | case 'o': |
| 510 | case 'O': |
| 511 | if( isWholeWord(zTxt, i, "order", 5) && fossil_isspace(zTxt[i+5]) ){ |
| 512 | return 1; |
| 513 | } |
| 514 | if( isWholeWord(zTxt, i, "or", 2) ) return 1; |
| 515 | break; |
| 516 | case 's': |
| 517 | case 'S': |
| 518 | if( isWholeWord(zTxt, i, "select", 6) ) return 1; |
| 519 | break; |
| 520 | case 'w': |
| 521 | case 'W': |
| 522 | if( isWholeWord(zTxt, i, "waitfor", 7) ) return 1; |
| 523 | break; |
| 524 | } |
| 525 | } |
| 526 | return 0; |
| 527 | } |
| 528 | |
| 529 | /* |
| 530 | ** This is a utility routine associated with the test-looks-like-sql-injection |
| 531 | ** command. |
| @@ -534,11 +546,11 @@ | |
| 534 | ** might be SQL injection. |
| 535 | ** |
| 536 | ** Or if bInvert is true, then show the opposite - those lines that do NOT |
| 537 | ** look like SQL injection. |
| 538 | */ |
| 539 | static void show_sql_injection_lines( |
| 540 | const char *zInFile, /* Name of input file */ |
| 541 | int bInvert, /* Invert the sense of the output (-v) */ |
| 542 | int bDeHttpize /* De-httpize the inputs. (-d) */ |
| 543 | ){ |
| 544 | FILE *in; |
| @@ -551,34 +563,34 @@ | |
| 551 | fossil_fatal("cannot open \"%s\" for reading\n", zInFile); |
| 552 | } |
| 553 | } |
| 554 | while( fgets(zLine, sizeof(zLine), in) ){ |
| 555 | dehttpize(zLine); |
| 556 | if( (looks_like_sql_injection(zLine)!=0) ^ bInvert ){ |
| 557 | fossil_print("%s", zLine); |
| 558 | } |
| 559 | } |
| 560 | if( in!=stdin ) fclose(in); |
| 561 | } |
| 562 | |
| 563 | /* |
| 564 | ** COMMAND: test-looks-like-sql-injection |
| 565 | ** |
| 566 | ** Read lines of input from files named as arguments (or from standard |
| 567 | ** input if no arguments are provided) and print those that look like they |
| 568 | ** might be part of an SQL injection attack. |
| 569 | ** |
| 570 | ** Used to test the looks_lide_sql_injection() utility subroutine, possibly |
| 571 | ** by piping in actual server log data. |
| 572 | */ |
| 573 | void test_looks_like_sql_injection(void){ |
| 574 | int i; |
| 575 | int bInvert = find_option("invert","v",0)!=0; |
| 576 | int bDeHttpize = find_option("dehttpize","d",0)!=0; |
| 577 | verify_all_options(); |
| 578 | if( g.argc==2 ){ |
| 579 | show_sql_injection_lines(0, bInvert, bDeHttpize); |
| 580 | } |
| 581 | for(i=2; i<g.argc; i++){ |
| 582 | show_sql_injection_lines(g.argv[i], bInvert, bDeHttpize); |
| 583 | } |
| 584 | } |
| 585 |
| --- src/lookslike.c | |
| +++ src/lookslike.c | |
| @@ -478,54 +478,66 @@ | |
| 478 | } |
| 479 | |
| 480 | /* |
| 481 | ** Returns true if the given text contains certain keywords or |
| 482 | ** punctuation which indicate that it might be an SQL injection attempt |
| 483 | ** or Cross-site scripting attempt or some other kind of mischief. |
| 484 | ** |
| 485 | ** This is not a primary defense against vulnerabilities in the Fossil |
| 486 | ** code. Rather, this is part of an effort to do early detection of malicious |
| 487 | ** spiders to avoid them using up too many CPU cycles. Or, this routine |
| 488 | ** can also be thought of as a secondary layer of defense against attacks. |
| 489 | */ |
| 490 | int looks_like_attack(const char *zTxt){ |
| 491 | unsigned int i; |
| 492 | int rc = 0; |
| 493 | if( zTxt==0 ) return 0; |
| 494 | for(i=0; zTxt[i]; i++){ |
| 495 | switch( zTxt[i] ){ |
| 496 | case '<': |
| 497 | case ';': |
| 498 | case '\'': |
| 499 | return 1; |
| 500 | case '/': /* 0123456789 123456789 */ |
| 501 | if( strncmp(zTxt+i+1, "/wp-content/plugins/", 20)==0 ) rc = 1; |
| 502 | if( strncmp(zTxt+i+1, "/wp-admin/admin-ajax", 20)==0 ) rc = 1; |
| 503 | break; |
| 504 | case 'a': |
| 505 | case 'A': |
| 506 | if( isWholeWord(zTxt, i, "and", 3) ) rc = 1; |
| 507 | break; |
| 508 | case 'n': |
| 509 | case 'N': |
| 510 | if( isWholeWord(zTxt, i, "null", 4) ) rc = 1; |
| 511 | break; |
| 512 | case 'o': |
| 513 | case 'O': |
| 514 | if( isWholeWord(zTxt, i, "order", 5) && fossil_isspace(zTxt[i+5]) ){ |
| 515 | rc = 1; |
| 516 | } |
| 517 | if( isWholeWord(zTxt, i, "or", 2) ) rc = 1; |
| 518 | break; |
| 519 | case 's': |
| 520 | case 'S': |
| 521 | if( isWholeWord(zTxt, i, "select", 6) ) rc = 1; |
| 522 | break; |
| 523 | case 'w': |
| 524 | case 'W': |
| 525 | if( isWholeWord(zTxt, i, "waitfor", 7) ) rc = 1; |
| 526 | break; |
| 527 | } |
| 528 | } |
| 529 | if( rc ){ |
| 530 | /* The test/markdown-test3.md document which is part of the Fossil source |
| 531 | ** tree intentionally tries to fake an attack. Do not report such |
| 532 | ** errors. */ |
| 533 | const char *zPathInfo = P("PATH_INFO"); |
| 534 | if( sqlite3_strglob("/doc/*/test/markdown-test3.md", zPathInfo)==0 ){ |
| 535 | rc = 0; |
| 536 | } |
| 537 | } |
| 538 | return rc; |
| 539 | } |
| 540 | |
| 541 | /* |
| 542 | ** This is a utility routine associated with the test-looks-like-sql-injection |
| 543 | ** command. |
| @@ -534,11 +546,11 @@ | |
| 546 | ** might be SQL injection. |
| 547 | ** |
| 548 | ** Or if bInvert is true, then show the opposite - those lines that do NOT |
| 549 | ** look like SQL injection. |
| 550 | */ |
| 551 | static void show_attack_lines( |
| 552 | const char *zInFile, /* Name of input file */ |
| 553 | int bInvert, /* Invert the sense of the output (-v) */ |
| 554 | int bDeHttpize /* De-httpize the inputs. (-d) */ |
| 555 | ){ |
| 556 | FILE *in; |
| @@ -551,34 +563,34 @@ | |
| 563 | fossil_fatal("cannot open \"%s\" for reading\n", zInFile); |
| 564 | } |
| 565 | } |
| 566 | while( fgets(zLine, sizeof(zLine), in) ){ |
| 567 | dehttpize(zLine); |
| 568 | if( (looks_like_attack(zLine)!=0) ^ bInvert ){ |
| 569 | fossil_print("%s", zLine); |
| 570 | } |
| 571 | } |
| 572 | if( in!=stdin ) fclose(in); |
| 573 | } |
| 574 | |
| 575 | /* |
| 576 | ** COMMAND: test-looks-like-attack |
| 577 | ** |
| 578 | ** Read lines of input from files named as arguments (or from standard |
| 579 | ** input if no arguments are provided) and print those that look like they |
| 580 | ** might be part of an SQL injection attack. |
| 581 | ** |
| 582 | ** Used to test the looks_lile_attack() utility subroutine, possibly |
| 583 | ** by piping in actual server log data. |
| 584 | */ |
| 585 | void test_looks_like_attack(void){ |
| 586 | int i; |
| 587 | int bInvert = find_option("invert","v",0)!=0; |
| 588 | int bDeHttpize = find_option("dehttpize","d",0)!=0; |
| 589 | verify_all_options(); |
| 590 | if( g.argc==2 ){ |
| 591 | show_attack_lines(0, bInvert, bDeHttpize); |
| 592 | } |
| 593 | for(i=2; i<g.argc; i++){ |
| 594 | show_attack_lines(g.argv[i], bInvert, bDeHttpize); |
| 595 | } |
| 596 | } |
| 597 |
+62
-16
| --- src/main.c | ||
| +++ src/main.c | ||
| @@ -2053,11 +2053,12 @@ | ||
| 2053 | 2053 | */ |
| 2054 | 2054 | set_base_url(0); |
| 2055 | 2055 | if( fossil_redirect_to_https_if_needed(2) ) return; |
| 2056 | 2056 | if( zPathInfo==0 || zPathInfo[0]==0 |
| 2057 | 2057 | || (zPathInfo[0]=='/' && zPathInfo[1]==0) ){ |
| 2058 | - /* Second special case: If the PATH_INFO is blank, issue a redirect: | |
| 2058 | + /* Second special case: If the PATH_INFO is blank, issue a | |
| 2059 | + ** temporary 302 redirect: | |
| 2059 | 2060 | ** (1) to "/ckout" if g.useLocalauth and g.localOpen are both set. |
| 2060 | 2061 | ** (2) to the home page identified by the "index-page" setting |
| 2061 | 2062 | ** in the repository CONFIG table |
| 2062 | 2063 | ** (3) to "/index" if there no "index-page" setting in CONFIG |
| 2063 | 2064 | */ |
| @@ -2267,10 +2268,21 @@ | ||
| 2267 | 2268 | ** |
| 2268 | 2269 | ** #!/usr/bin/fossil |
| 2269 | 2270 | ** redirect: * https://fossil-scm.org/home |
| 2270 | 2271 | ** |
| 2271 | 2272 | ** Thus requests to the .com website redirect to the .org website. |
| 2273 | +** This form uses a 301 Permanent redirect. | |
| 2274 | +** | |
| 2275 | +** On a "*" redirect, the PATH_INFO and QUERY_STRING of the query | |
| 2276 | +** that provoked the redirect are appended to the target. So, for | |
| 2277 | +** example, if the input URL for the redirect above were | |
| 2278 | +** "http://www.fossil.com/index.html/timeline?c=20250404", then | |
| 2279 | +** the redirect would be to: | |
| 2280 | +** | |
| 2281 | +** https://fossil-scm.org/home/timeline?c=20250404 | |
| 2282 | +** ^^^^^^^^^^^^^^^^^^^^ | |
| 2283 | +** Copied from input URL | |
| 2272 | 2284 | */ |
| 2273 | 2285 | static void redirect_web_page(int nRedirect, char **azRedirect){ |
| 2274 | 2286 | int i; /* Loop counter */ |
| 2275 | 2287 | const char *zNotFound = 0; /* Not found URL */ |
| 2276 | 2288 | const char *zName = P("name"); |
| @@ -2297,21 +2309,22 @@ | ||
| 2297 | 2309 | } |
| 2298 | 2310 | if( zNotFound ){ |
| 2299 | 2311 | Blob to; |
| 2300 | 2312 | const char *z; |
| 2301 | 2313 | if( strstr(zNotFound, "%s") ){ |
| 2302 | - cgi_redirectf(zNotFound /*works-like:"%s"*/, zName); | |
| 2314 | + char *zTarget = mprintf(zNotFound /*works-like:"%s"*/, zName); | |
| 2315 | + cgi_redirect_perm(zTarget); | |
| 2303 | 2316 | } |
| 2304 | 2317 | if( strchr(zNotFound, '?') ){ |
| 2305 | - cgi_redirect(zNotFound); | |
| 2318 | + cgi_redirect_perm(zNotFound); | |
| 2306 | 2319 | } |
| 2307 | 2320 | blob_init(&to, zNotFound, -1); |
| 2308 | 2321 | z = P("PATH_INFO"); |
| 2309 | 2322 | if( z && z[0]=='/' ) blob_append(&to, z, -1); |
| 2310 | 2323 | z = P("QUERY_STRING"); |
| 2311 | 2324 | if( z && z[0]!=0 ) blob_appendf(&to, "?%s", z); |
| 2312 | - cgi_redirect(blob_str(&to)); | |
| 2325 | + cgi_redirect_perm(blob_str(&to)); | |
| 2313 | 2326 | }else{ |
| 2314 | 2327 | @ <html> |
| 2315 | 2328 | @ <head><title>No Such Object</title></head> |
| 2316 | 2329 | @ <body> |
| 2317 | 2330 | @ <p>No such object: <b>%h(zName)</b></p> |
| @@ -2352,11 +2365,18 @@ | ||
| 2352 | 2365 | ** notfound: URL When in "directory:" mode, redirect to |
| 2353 | 2366 | ** URL if no suitable repository is found. |
| 2354 | 2367 | ** |
| 2355 | 2368 | ** repolist When in "directory:" mode, display a page |
| 2356 | 2369 | ** showing a list of available repositories if |
| 2357 | -** the URL is "/". | |
| 2370 | +** the URL is "/". Some control over the display | |
| 2371 | +** is accomplished using environment variables. | |
| 2372 | +** FOSSIL_REPOLIST_TITLE is the tital of the page. | |
| 2373 | +** FOSSIL_REPOLIST_SHOW cause the "Description" | |
| 2374 | +** column to display if it contains "description" as | |
| 2375 | +** as a substring, and causes the Login-Group column | |
| 2376 | +** to display if it contains the "login-group" | |
| 2377 | +** substring. | |
| 2358 | 2378 | ** |
| 2359 | 2379 | ** localauth Grant administrator privileges to connections |
| 2360 | 2380 | ** from 127.0.0.1 or ::1. |
| 2361 | 2381 | ** |
| 2362 | 2382 | ** nossl Signal that no SSL connections are available. |
| @@ -2394,10 +2414,13 @@ | ||
| 2394 | 2414 | ** REPO for a check-in or ticket that matches the |
| 2395 | 2415 | ** value of "name", then redirect to URL. There |
| 2396 | 2416 | ** can be multiple "redirect:" lines that are |
| 2397 | 2417 | ** processed in order. If the REPO is "*", then |
| 2398 | 2418 | ** an unconditional redirect to URL is taken. |
| 2419 | +** When "*" is used a 301 permanent redirect is | |
| 2420 | +** issued and the tail and query string from the | |
| 2421 | +** original query are appeneded onto URL. | |
| 2399 | 2422 | ** |
| 2400 | 2423 | ** jsmode: VALUE Specifies the delivery mode for JavaScript |
| 2401 | 2424 | ** files. See the help text for the --jsmode |
| 2402 | 2425 | ** flag of the http command. |
| 2403 | 2426 | ** |
| @@ -3052,23 +3075,25 @@ | ||
| 3052 | 3075 | ** using this command interactively over SSH. A better solution would be |
| 3053 | 3076 | ** to use a different command for "ssh" sync, but we cannot do that without |
| 3054 | 3077 | ** breaking legacy. |
| 3055 | 3078 | ** |
| 3056 | 3079 | ** Options: |
| 3080 | +** --csrf-safe N Set cgi_csrf_safe() to to return N | |
| 3057 | 3081 | ** --nobody Pretend to be user "nobody" |
| 3058 | 3082 | ** --test Do not do special "sync" processing when operating |
| 3059 | 3083 | ** over an SSH link |
| 3060 | 3084 | ** --th-trace Trace TH1 execution (for debugging purposes) |
| 3061 | 3085 | ** --usercap CAP User capability string (Default: "sxy") |
| 3062 | -** | |
| 3063 | 3086 | */ |
| 3064 | 3087 | void cmd_test_http(void){ |
| 3065 | 3088 | const char *zIpAddr; /* IP address of remote client */ |
| 3066 | 3089 | const char *zUserCap; |
| 3067 | 3090 | int bTest = 0; |
| 3091 | + const char *zCsrfSafe = find_option("csrf-safe",0,1); | |
| 3068 | 3092 | |
| 3069 | 3093 | Th_InitTraceLog(); |
| 3094 | + if( zCsrfSafe ) g.okCsrf = atoi(zCsrfSafe); | |
| 3070 | 3095 | zUserCap = find_option("usercap",0,1); |
| 3071 | 3096 | if( !find_option("nobody",0,0) ){ |
| 3072 | 3097 | if( zUserCap==0 ){ |
| 3073 | 3098 | g.useLocalauth = 1; |
| 3074 | 3099 | zUserCap = "sxy"; |
| @@ -3505,11 +3530,12 @@ | ||
| 3505 | 3530 | } |
| 3506 | 3531 | blob_append_escaped_arg(&ssh, "fossil", 1); |
| 3507 | 3532 | }else{ |
| 3508 | 3533 | blob_appendf(&ssh, " %$", zFossilCmd); |
| 3509 | 3534 | } |
| 3510 | - blob_appendf(&ssh, " ui --nobrowser --localauth --port %d", iPort); | |
| 3535 | + blob_appendf(&ssh, " ui --nobrowser --localauth --port 127.0.0.1:%d", | |
| 3536 | + iPort); | |
| 3511 | 3537 | if( zNotFound ) blob_appendf(&ssh, " --notfound %!$", zNotFound); |
| 3512 | 3538 | if( zFileGlob ) blob_appendf(&ssh, " --files-urlenc %T", zFileGlob); |
| 3513 | 3539 | if( g.zCkoutAlias ) blob_appendf(&ssh," --ckout-alias %!$",g.zCkoutAlias); |
| 3514 | 3540 | if( zExtPage ){ |
| 3515 | 3541 | if( !file_is_absolute_path(zExtPage) ){ |
| @@ -3692,10 +3718,13 @@ | ||
| 3692 | 3718 | ** case=3 Extra db_end_transaction() |
| 3693 | 3719 | ** case=4 Error during SQL processing |
| 3694 | 3720 | ** case=5 Call the segfault handler |
| 3695 | 3721 | ** case=6 Call webpage_assert() |
| 3696 | 3722 | ** case=7 Call webpage_error() |
| 3723 | +** case=8 Simulate a timeout | |
| 3724 | +** case=9 Simulate a TH1 XSS vulnerability | |
| 3725 | +** case=10 Simulate a TH1 SQL-injection vulnerability | |
| 3697 | 3726 | */ |
| 3698 | 3727 | void test_warning_page(void){ |
| 3699 | 3728 | int iCase = atoi(PD("case","0")); |
| 3700 | 3729 | int i; |
| 3701 | 3730 | login_check_credentials(); |
| @@ -3704,17 +3733,15 @@ | ||
| 3704 | 3733 | return; |
| 3705 | 3734 | } |
| 3706 | 3735 | style_set_current_feature("test"); |
| 3707 | 3736 | style_header("Warning Test Page"); |
| 3708 | 3737 | style_submenu_element("Error Log","%R/errorlog"); |
| 3709 | - if( iCase<1 || iCase>4 ){ | |
| 3710 | - @ <p>Generate a message to the <a href="%R/errorlog">error log</a> | |
| 3711 | - @ by clicking on one of the following cases: | |
| 3712 | - }else{ | |
| 3713 | - @ <p>This is the test page for case=%d(iCase). All possible cases: | |
| 3714 | - } | |
| 3715 | - for(i=1; i<=8; i++){ | |
| 3738 | + @ <p>This page will generate various kinds of errors to test Fossil's | |
| 3739 | + @ reaction. Depending on settings, a message might be written | |
| 3740 | + @ into the <a href="%R/errorlog">error log</a>. Click on | |
| 3741 | + @ one of the following hyperlinks to generate a simulated error: | |
| 3742 | + for(i=1; i<=10; i++){ | |
| 3716 | 3743 | @ <a href='./test-warning?case=%d(i)'>[%d(i)]</a> |
| 3717 | 3744 | } |
| 3718 | 3745 | @ </p> |
| 3719 | 3746 | @ <p><ol> |
| 3720 | 3747 | @ <li value='1'> Call fossil_warning() |
| @@ -3743,20 +3770,39 @@ | ||
| 3743 | 3770 | } |
| 3744 | 3771 | @ <li value='6'> call webpage_assert(0) |
| 3745 | 3772 | if( iCase==6 ){ |
| 3746 | 3773 | webpage_assert( 5==7 ); |
| 3747 | 3774 | } |
| 3748 | - @ <li value='7'> call webpage_error()" | |
| 3775 | + @ <li value='7'> call webpage_error() | |
| 3749 | 3776 | if( iCase==7 ){ |
| 3750 | 3777 | cgi_reset_content(); |
| 3751 | 3778 | webpage_error("Case 7 from /test-warning"); |
| 3752 | 3779 | } |
| 3753 | - @ <li value='8'> simulated timeout" | |
| 3780 | + @ <li value='8'> simulated timeout | |
| 3754 | 3781 | if( iCase==8 ){ |
| 3755 | 3782 | fossil_set_timeout(1); |
| 3756 | 3783 | cgi_reset_content(); |
| 3757 | 3784 | sqlite3_sleep(1100); |
| 3785 | + } | |
| 3786 | + @ <li value='9'> simulated TH1 XSS vulnerability | |
| 3787 | + @ <li value='10'> simulated TH1 SQL-injection vulnerability | |
| 3788 | + if( iCase==9 || iCase==10 ){ | |
| 3789 | + const char *zR; | |
| 3790 | + int n, rc; | |
| 3791 | + static const char *zTH1[] = { | |
| 3792 | + /* case 9 */ "html [taint {<b>XSS</b>}]", | |
| 3793 | + /* case 10 */ "query [taint {SELECT 'SQL-injection' AS msg}] {\n" | |
| 3794 | + " html \"<b>[htmlize $msg]</b>\"\n" | |
| 3795 | + "}" | |
| 3796 | + }; | |
| 3797 | + rc = Th_Eval(g.interp, 0, zTH1[iCase==10], -1); | |
| 3798 | + zR = Th_GetResult(g.interp, &n); | |
| 3799 | + if( rc==TH_OK ){ | |
| 3800 | + @ <pre class="th1result">%h(zR)</pre> | |
| 3801 | + }else{ | |
| 3802 | + @ <pre class="th1error">%h(zR)</pre> | |
| 3803 | + } | |
| 3758 | 3804 | } |
| 3759 | 3805 | @ </ol> |
| 3760 | 3806 | @ <p>End of test</p> |
| 3761 | 3807 | style_finish_page(); |
| 3762 | 3808 | } |
| 3763 | 3809 |
| --- src/main.c | |
| +++ src/main.c | |
| @@ -2053,11 +2053,12 @@ | |
| 2053 | */ |
| 2054 | set_base_url(0); |
| 2055 | if( fossil_redirect_to_https_if_needed(2) ) return; |
| 2056 | if( zPathInfo==0 || zPathInfo[0]==0 |
| 2057 | || (zPathInfo[0]=='/' && zPathInfo[1]==0) ){ |
| 2058 | /* Second special case: If the PATH_INFO is blank, issue a redirect: |
| 2059 | ** (1) to "/ckout" if g.useLocalauth and g.localOpen are both set. |
| 2060 | ** (2) to the home page identified by the "index-page" setting |
| 2061 | ** in the repository CONFIG table |
| 2062 | ** (3) to "/index" if there no "index-page" setting in CONFIG |
| 2063 | */ |
| @@ -2267,10 +2268,21 @@ | |
| 2267 | ** |
| 2268 | ** #!/usr/bin/fossil |
| 2269 | ** redirect: * https://fossil-scm.org/home |
| 2270 | ** |
| 2271 | ** Thus requests to the .com website redirect to the .org website. |
| 2272 | */ |
| 2273 | static void redirect_web_page(int nRedirect, char **azRedirect){ |
| 2274 | int i; /* Loop counter */ |
| 2275 | const char *zNotFound = 0; /* Not found URL */ |
| 2276 | const char *zName = P("name"); |
| @@ -2297,21 +2309,22 @@ | |
| 2297 | } |
| 2298 | if( zNotFound ){ |
| 2299 | Blob to; |
| 2300 | const char *z; |
| 2301 | if( strstr(zNotFound, "%s") ){ |
| 2302 | cgi_redirectf(zNotFound /*works-like:"%s"*/, zName); |
| 2303 | } |
| 2304 | if( strchr(zNotFound, '?') ){ |
| 2305 | cgi_redirect(zNotFound); |
| 2306 | } |
| 2307 | blob_init(&to, zNotFound, -1); |
| 2308 | z = P("PATH_INFO"); |
| 2309 | if( z && z[0]=='/' ) blob_append(&to, z, -1); |
| 2310 | z = P("QUERY_STRING"); |
| 2311 | if( z && z[0]!=0 ) blob_appendf(&to, "?%s", z); |
| 2312 | cgi_redirect(blob_str(&to)); |
| 2313 | }else{ |
| 2314 | @ <html> |
| 2315 | @ <head><title>No Such Object</title></head> |
| 2316 | @ <body> |
| 2317 | @ <p>No such object: <b>%h(zName)</b></p> |
| @@ -2352,11 +2365,18 @@ | |
| 2352 | ** notfound: URL When in "directory:" mode, redirect to |
| 2353 | ** URL if no suitable repository is found. |
| 2354 | ** |
| 2355 | ** repolist When in "directory:" mode, display a page |
| 2356 | ** showing a list of available repositories if |
| 2357 | ** the URL is "/". |
| 2358 | ** |
| 2359 | ** localauth Grant administrator privileges to connections |
| 2360 | ** from 127.0.0.1 or ::1. |
| 2361 | ** |
| 2362 | ** nossl Signal that no SSL connections are available. |
| @@ -2394,10 +2414,13 @@ | |
| 2394 | ** REPO for a check-in or ticket that matches the |
| 2395 | ** value of "name", then redirect to URL. There |
| 2396 | ** can be multiple "redirect:" lines that are |
| 2397 | ** processed in order. If the REPO is "*", then |
| 2398 | ** an unconditional redirect to URL is taken. |
| 2399 | ** |
| 2400 | ** jsmode: VALUE Specifies the delivery mode for JavaScript |
| 2401 | ** files. See the help text for the --jsmode |
| 2402 | ** flag of the http command. |
| 2403 | ** |
| @@ -3052,23 +3075,25 @@ | |
| 3052 | ** using this command interactively over SSH. A better solution would be |
| 3053 | ** to use a different command for "ssh" sync, but we cannot do that without |
| 3054 | ** breaking legacy. |
| 3055 | ** |
| 3056 | ** Options: |
| 3057 | ** --nobody Pretend to be user "nobody" |
| 3058 | ** --test Do not do special "sync" processing when operating |
| 3059 | ** over an SSH link |
| 3060 | ** --th-trace Trace TH1 execution (for debugging purposes) |
| 3061 | ** --usercap CAP User capability string (Default: "sxy") |
| 3062 | ** |
| 3063 | */ |
| 3064 | void cmd_test_http(void){ |
| 3065 | const char *zIpAddr; /* IP address of remote client */ |
| 3066 | const char *zUserCap; |
| 3067 | int bTest = 0; |
| 3068 | |
| 3069 | Th_InitTraceLog(); |
| 3070 | zUserCap = find_option("usercap",0,1); |
| 3071 | if( !find_option("nobody",0,0) ){ |
| 3072 | if( zUserCap==0 ){ |
| 3073 | g.useLocalauth = 1; |
| 3074 | zUserCap = "sxy"; |
| @@ -3505,11 +3530,12 @@ | |
| 3505 | } |
| 3506 | blob_append_escaped_arg(&ssh, "fossil", 1); |
| 3507 | }else{ |
| 3508 | blob_appendf(&ssh, " %$", zFossilCmd); |
| 3509 | } |
| 3510 | blob_appendf(&ssh, " ui --nobrowser --localauth --port %d", iPort); |
| 3511 | if( zNotFound ) blob_appendf(&ssh, " --notfound %!$", zNotFound); |
| 3512 | if( zFileGlob ) blob_appendf(&ssh, " --files-urlenc %T", zFileGlob); |
| 3513 | if( g.zCkoutAlias ) blob_appendf(&ssh," --ckout-alias %!$",g.zCkoutAlias); |
| 3514 | if( zExtPage ){ |
| 3515 | if( !file_is_absolute_path(zExtPage) ){ |
| @@ -3692,10 +3718,13 @@ | |
| 3692 | ** case=3 Extra db_end_transaction() |
| 3693 | ** case=4 Error during SQL processing |
| 3694 | ** case=5 Call the segfault handler |
| 3695 | ** case=6 Call webpage_assert() |
| 3696 | ** case=7 Call webpage_error() |
| 3697 | */ |
| 3698 | void test_warning_page(void){ |
| 3699 | int iCase = atoi(PD("case","0")); |
| 3700 | int i; |
| 3701 | login_check_credentials(); |
| @@ -3704,17 +3733,15 @@ | |
| 3704 | return; |
| 3705 | } |
| 3706 | style_set_current_feature("test"); |
| 3707 | style_header("Warning Test Page"); |
| 3708 | style_submenu_element("Error Log","%R/errorlog"); |
| 3709 | if( iCase<1 || iCase>4 ){ |
| 3710 | @ <p>Generate a message to the <a href="%R/errorlog">error log</a> |
| 3711 | @ by clicking on one of the following cases: |
| 3712 | }else{ |
| 3713 | @ <p>This is the test page for case=%d(iCase). All possible cases: |
| 3714 | } |
| 3715 | for(i=1; i<=8; i++){ |
| 3716 | @ <a href='./test-warning?case=%d(i)'>[%d(i)]</a> |
| 3717 | } |
| 3718 | @ </p> |
| 3719 | @ <p><ol> |
| 3720 | @ <li value='1'> Call fossil_warning() |
| @@ -3743,20 +3770,39 @@ | |
| 3743 | } |
| 3744 | @ <li value='6'> call webpage_assert(0) |
| 3745 | if( iCase==6 ){ |
| 3746 | webpage_assert( 5==7 ); |
| 3747 | } |
| 3748 | @ <li value='7'> call webpage_error()" |
| 3749 | if( iCase==7 ){ |
| 3750 | cgi_reset_content(); |
| 3751 | webpage_error("Case 7 from /test-warning"); |
| 3752 | } |
| 3753 | @ <li value='8'> simulated timeout" |
| 3754 | if( iCase==8 ){ |
| 3755 | fossil_set_timeout(1); |
| 3756 | cgi_reset_content(); |
| 3757 | sqlite3_sleep(1100); |
| 3758 | } |
| 3759 | @ </ol> |
| 3760 | @ <p>End of test</p> |
| 3761 | style_finish_page(); |
| 3762 | } |
| 3763 |
| --- src/main.c | |
| +++ src/main.c | |
| @@ -2053,11 +2053,12 @@ | |
| 2053 | */ |
| 2054 | set_base_url(0); |
| 2055 | if( fossil_redirect_to_https_if_needed(2) ) return; |
| 2056 | if( zPathInfo==0 || zPathInfo[0]==0 |
| 2057 | || (zPathInfo[0]=='/' && zPathInfo[1]==0) ){ |
| 2058 | /* Second special case: If the PATH_INFO is blank, issue a |
| 2059 | ** temporary 302 redirect: |
| 2060 | ** (1) to "/ckout" if g.useLocalauth and g.localOpen are both set. |
| 2061 | ** (2) to the home page identified by the "index-page" setting |
| 2062 | ** in the repository CONFIG table |
| 2063 | ** (3) to "/index" if there no "index-page" setting in CONFIG |
| 2064 | */ |
| @@ -2267,10 +2268,21 @@ | |
| 2268 | ** |
| 2269 | ** #!/usr/bin/fossil |
| 2270 | ** redirect: * https://fossil-scm.org/home |
| 2271 | ** |
| 2272 | ** Thus requests to the .com website redirect to the .org website. |
| 2273 | ** This form uses a 301 Permanent redirect. |
| 2274 | ** |
| 2275 | ** On a "*" redirect, the PATH_INFO and QUERY_STRING of the query |
| 2276 | ** that provoked the redirect are appended to the target. So, for |
| 2277 | ** example, if the input URL for the redirect above were |
| 2278 | ** "http://www.fossil.com/index.html/timeline?c=20250404", then |
| 2279 | ** the redirect would be to: |
| 2280 | ** |
| 2281 | ** https://fossil-scm.org/home/timeline?c=20250404 |
| 2282 | ** ^^^^^^^^^^^^^^^^^^^^ |
| 2283 | ** Copied from input URL |
| 2284 | */ |
| 2285 | static void redirect_web_page(int nRedirect, char **azRedirect){ |
| 2286 | int i; /* Loop counter */ |
| 2287 | const char *zNotFound = 0; /* Not found URL */ |
| 2288 | const char *zName = P("name"); |
| @@ -2297,21 +2309,22 @@ | |
| 2309 | } |
| 2310 | if( zNotFound ){ |
| 2311 | Blob to; |
| 2312 | const char *z; |
| 2313 | if( strstr(zNotFound, "%s") ){ |
| 2314 | char *zTarget = mprintf(zNotFound /*works-like:"%s"*/, zName); |
| 2315 | cgi_redirect_perm(zTarget); |
| 2316 | } |
| 2317 | if( strchr(zNotFound, '?') ){ |
| 2318 | cgi_redirect_perm(zNotFound); |
| 2319 | } |
| 2320 | blob_init(&to, zNotFound, -1); |
| 2321 | z = P("PATH_INFO"); |
| 2322 | if( z && z[0]=='/' ) blob_append(&to, z, -1); |
| 2323 | z = P("QUERY_STRING"); |
| 2324 | if( z && z[0]!=0 ) blob_appendf(&to, "?%s", z); |
| 2325 | cgi_redirect_perm(blob_str(&to)); |
| 2326 | }else{ |
| 2327 | @ <html> |
| 2328 | @ <head><title>No Such Object</title></head> |
| 2329 | @ <body> |
| 2330 | @ <p>No such object: <b>%h(zName)</b></p> |
| @@ -2352,11 +2365,18 @@ | |
| 2365 | ** notfound: URL When in "directory:" mode, redirect to |
| 2366 | ** URL if no suitable repository is found. |
| 2367 | ** |
| 2368 | ** repolist When in "directory:" mode, display a page |
| 2369 | ** showing a list of available repositories if |
| 2370 | ** the URL is "/". Some control over the display |
| 2371 | ** is accomplished using environment variables. |
| 2372 | ** FOSSIL_REPOLIST_TITLE is the tital of the page. |
| 2373 | ** FOSSIL_REPOLIST_SHOW cause the "Description" |
| 2374 | ** column to display if it contains "description" as |
| 2375 | ** as a substring, and causes the Login-Group column |
| 2376 | ** to display if it contains the "login-group" |
| 2377 | ** substring. |
| 2378 | ** |
| 2379 | ** localauth Grant administrator privileges to connections |
| 2380 | ** from 127.0.0.1 or ::1. |
| 2381 | ** |
| 2382 | ** nossl Signal that no SSL connections are available. |
| @@ -2394,10 +2414,13 @@ | |
| 2414 | ** REPO for a check-in or ticket that matches the |
| 2415 | ** value of "name", then redirect to URL. There |
| 2416 | ** can be multiple "redirect:" lines that are |
| 2417 | ** processed in order. If the REPO is "*", then |
| 2418 | ** an unconditional redirect to URL is taken. |
| 2419 | ** When "*" is used a 301 permanent redirect is |
| 2420 | ** issued and the tail and query string from the |
| 2421 | ** original query are appeneded onto URL. |
| 2422 | ** |
| 2423 | ** jsmode: VALUE Specifies the delivery mode for JavaScript |
| 2424 | ** files. See the help text for the --jsmode |
| 2425 | ** flag of the http command. |
| 2426 | ** |
| @@ -3052,23 +3075,25 @@ | |
| 3075 | ** using this command interactively over SSH. A better solution would be |
| 3076 | ** to use a different command for "ssh" sync, but we cannot do that without |
| 3077 | ** breaking legacy. |
| 3078 | ** |
| 3079 | ** Options: |
| 3080 | ** --csrf-safe N Set cgi_csrf_safe() to to return N |
| 3081 | ** --nobody Pretend to be user "nobody" |
| 3082 | ** --test Do not do special "sync" processing when operating |
| 3083 | ** over an SSH link |
| 3084 | ** --th-trace Trace TH1 execution (for debugging purposes) |
| 3085 | ** --usercap CAP User capability string (Default: "sxy") |
| 3086 | */ |
| 3087 | void cmd_test_http(void){ |
| 3088 | const char *zIpAddr; /* IP address of remote client */ |
| 3089 | const char *zUserCap; |
| 3090 | int bTest = 0; |
| 3091 | const char *zCsrfSafe = find_option("csrf-safe",0,1); |
| 3092 | |
| 3093 | Th_InitTraceLog(); |
| 3094 | if( zCsrfSafe ) g.okCsrf = atoi(zCsrfSafe); |
| 3095 | zUserCap = find_option("usercap",0,1); |
| 3096 | if( !find_option("nobody",0,0) ){ |
| 3097 | if( zUserCap==0 ){ |
| 3098 | g.useLocalauth = 1; |
| 3099 | zUserCap = "sxy"; |
| @@ -3505,11 +3530,12 @@ | |
| 3530 | } |
| 3531 | blob_append_escaped_arg(&ssh, "fossil", 1); |
| 3532 | }else{ |
| 3533 | blob_appendf(&ssh, " %$", zFossilCmd); |
| 3534 | } |
| 3535 | blob_appendf(&ssh, " ui --nobrowser --localauth --port 127.0.0.1:%d", |
| 3536 | iPort); |
| 3537 | if( zNotFound ) blob_appendf(&ssh, " --notfound %!$", zNotFound); |
| 3538 | if( zFileGlob ) blob_appendf(&ssh, " --files-urlenc %T", zFileGlob); |
| 3539 | if( g.zCkoutAlias ) blob_appendf(&ssh," --ckout-alias %!$",g.zCkoutAlias); |
| 3540 | if( zExtPage ){ |
| 3541 | if( !file_is_absolute_path(zExtPage) ){ |
| @@ -3692,10 +3718,13 @@ | |
| 3718 | ** case=3 Extra db_end_transaction() |
| 3719 | ** case=4 Error during SQL processing |
| 3720 | ** case=5 Call the segfault handler |
| 3721 | ** case=6 Call webpage_assert() |
| 3722 | ** case=7 Call webpage_error() |
| 3723 | ** case=8 Simulate a timeout |
| 3724 | ** case=9 Simulate a TH1 XSS vulnerability |
| 3725 | ** case=10 Simulate a TH1 SQL-injection vulnerability |
| 3726 | */ |
| 3727 | void test_warning_page(void){ |
| 3728 | int iCase = atoi(PD("case","0")); |
| 3729 | int i; |
| 3730 | login_check_credentials(); |
| @@ -3704,17 +3733,15 @@ | |
| 3733 | return; |
| 3734 | } |
| 3735 | style_set_current_feature("test"); |
| 3736 | style_header("Warning Test Page"); |
| 3737 | style_submenu_element("Error Log","%R/errorlog"); |
| 3738 | @ <p>This page will generate various kinds of errors to test Fossil's |
| 3739 | @ reaction. Depending on settings, a message might be written |
| 3740 | @ into the <a href="%R/errorlog">error log</a>. Click on |
| 3741 | @ one of the following hyperlinks to generate a simulated error: |
| 3742 | for(i=1; i<=10; i++){ |
| 3743 | @ <a href='./test-warning?case=%d(i)'>[%d(i)]</a> |
| 3744 | } |
| 3745 | @ </p> |
| 3746 | @ <p><ol> |
| 3747 | @ <li value='1'> Call fossil_warning() |
| @@ -3743,20 +3770,39 @@ | |
| 3770 | } |
| 3771 | @ <li value='6'> call webpage_assert(0) |
| 3772 | if( iCase==6 ){ |
| 3773 | webpage_assert( 5==7 ); |
| 3774 | } |
| 3775 | @ <li value='7'> call webpage_error() |
| 3776 | if( iCase==7 ){ |
| 3777 | cgi_reset_content(); |
| 3778 | webpage_error("Case 7 from /test-warning"); |
| 3779 | } |
| 3780 | @ <li value='8'> simulated timeout |
| 3781 | if( iCase==8 ){ |
| 3782 | fossil_set_timeout(1); |
| 3783 | cgi_reset_content(); |
| 3784 | sqlite3_sleep(1100); |
| 3785 | } |
| 3786 | @ <li value='9'> simulated TH1 XSS vulnerability |
| 3787 | @ <li value='10'> simulated TH1 SQL-injection vulnerability |
| 3788 | if( iCase==9 || iCase==10 ){ |
| 3789 | const char *zR; |
| 3790 | int n, rc; |
| 3791 | static const char *zTH1[] = { |
| 3792 | /* case 9 */ "html [taint {<b>XSS</b>}]", |
| 3793 | /* case 10 */ "query [taint {SELECT 'SQL-injection' AS msg}] {\n" |
| 3794 | " html \"<b>[htmlize $msg]</b>\"\n" |
| 3795 | "}" |
| 3796 | }; |
| 3797 | rc = Th_Eval(g.interp, 0, zTH1[iCase==10], -1); |
| 3798 | zR = Th_GetResult(g.interp, &n); |
| 3799 | if( rc==TH_OK ){ |
| 3800 | @ <pre class="th1result">%h(zR)</pre> |
| 3801 | }else{ |
| 3802 | @ <pre class="th1error">%h(zR)</pre> |
| 3803 | } |
| 3804 | } |
| 3805 | @ </ol> |
| 3806 | @ <p>End of test</p> |
| 3807 | style_finish_page(); |
| 3808 | } |
| 3809 |
+28
-21
| --- src/manifest.c | ||
| +++ src/manifest.c | ||
| @@ -2927,11 +2927,11 @@ | ||
| 2927 | 2927 | case CFTYPE_CLUSTER: return "cluster"; |
| 2928 | 2928 | case CFTYPE_CONTROL: return "tag"; |
| 2929 | 2929 | case CFTYPE_WIKI: return "wiki"; |
| 2930 | 2930 | case CFTYPE_TICKET: return "ticket"; |
| 2931 | 2931 | case CFTYPE_ATTACHMENT: return "attachment"; |
| 2932 | - case CFTYPE_EVENT: return "event"; | |
| 2932 | + case CFTYPE_EVENT: return "technote"; | |
| 2933 | 2933 | case CFTYPE_FORUM: return "forumpost"; |
| 2934 | 2934 | } |
| 2935 | 2935 | return NULL; |
| 2936 | 2936 | } |
| 2937 | 2937 | |
| @@ -2948,26 +2948,26 @@ | ||
| 2948 | 2948 | */ |
| 2949 | 2949 | void artifact_to_json(Manifest const *p, Blob *b){ |
| 2950 | 2950 | int i; |
| 2951 | 2951 | |
| 2952 | 2952 | blob_append_literal(b, "{"); |
| 2953 | - blob_appendf(b, "\"uuid\": \"%z\"", rid_to_uuid(p->rid)); | |
| 2953 | + blob_appendf(b, "\"uuid\":\"%z\"", rid_to_uuid(p->rid)); | |
| 2954 | 2954 | /*blob_appendf(b, ", \"rid\": %d", p->rid); not portable across repos*/ |
| 2955 | - blob_appendf(b, ", \"type\": %!j", artifact_type_to_name(p->type)); | |
| 2955 | + blob_appendf(b, ",\"type\":%!j", artifact_type_to_name(p->type)); | |
| 2956 | 2956 | #define ISA(TYPE) if( p->type==TYPE ) |
| 2957 | 2957 | #define CARD_LETTER(LETTER) \ |
| 2958 | - blob_append_literal(b, ",\"" #LETTER "\": ") | |
| 2958 | + blob_append_literal(b, ",\"" #LETTER "\":") | |
| 2959 | 2959 | #define CARD_STR(LETTER, VAL) \ |
| 2960 | 2960 | assert( VAL ); CARD_LETTER(LETTER); blob_appendf(b, "%!j", VAL) |
| 2961 | 2961 | #define CARD_STR2(LETTER, VAL) \ |
| 2962 | 2962 | if( VAL ) { CARD_STR(LETTER, VAL); } (void)0 |
| 2963 | 2963 | #define STR_OR_NULL(VAL) \ |
| 2964 | 2964 | if( VAL ) blob_appendf(b, "%!j", VAL); \ |
| 2965 | 2965 | else blob_append(b, "null", 4) |
| 2966 | 2966 | #define KVP_STR(ADDCOMMA, KEY,VAL) \ |
| 2967 | 2967 | if(ADDCOMMA) blob_append_char(b, ','); \ |
| 2968 | - blob_appendf(b, "%!j: ", #KEY); \ | |
| 2968 | + blob_appendf(b, "%!j:", #KEY); \ | |
| 2969 | 2969 | STR_OR_NULL(VAL) |
| 2970 | 2970 | |
| 2971 | 2971 | ISA( CFTYPE_ATTACHMENT ){ |
| 2972 | 2972 | CARD_LETTER(A); |
| 2973 | 2973 | blob_append_char(b, '{'); |
| @@ -2978,11 +2978,11 @@ | ||
| 2978 | 2978 | } |
| 2979 | 2979 | CARD_STR2(B, p->zBaseline); |
| 2980 | 2980 | CARD_STR2(C, p->zComment); |
| 2981 | 2981 | CARD_LETTER(D); blob_appendf(b, "%f", p->rDate); |
| 2982 | 2982 | ISA( CFTYPE_EVENT ){ |
| 2983 | - blob_appendf(b, ", \"E\": {\"time\": %f, \"id\": %!j}", | |
| 2983 | + blob_appendf(b, ", \"E\":{\"time\":%f,\"id\":%!j}", | |
| 2984 | 2984 | p->rEventDate, p->zEventId); |
| 2985 | 2985 | } |
| 2986 | 2986 | ISA( CFTYPE_MANIFEST ){ |
| 2987 | 2987 | CARD_LETTER(F); |
| 2988 | 2988 | blob_append_char(b, '['); |
| @@ -2991,29 +2991,34 @@ | ||
| 2991 | 2991 | if( i>0 ) blob_append_char(b, ','); |
| 2992 | 2992 | blob_append_char(b, '{'); |
| 2993 | 2993 | KVP_STR(0, name, pF->zName); |
| 2994 | 2994 | KVP_STR(1, uuid, pF->zUuid); |
| 2995 | 2995 | KVP_STR(1, perm, pF->zPerm); |
| 2996 | - KVP_STR(1, oldName, pF->zPrior); | |
| 2996 | + KVP_STR(1, rename, pF->zPrior); | |
| 2997 | 2997 | blob_append_char(b, '}'); |
| 2998 | 2998 | } |
| 2999 | 2999 | /* Special case: model checkins with no F-card as having an empty |
| 3000 | 3000 | ** array, rather than no F-cards, to hypothetically simplify |
| 3001 | 3001 | ** handling in JSON queries. */ |
| 3002 | 3002 | blob_append_char(b, ']'); |
| 3003 | 3003 | } |
| 3004 | 3004 | CARD_STR2(G, p->zThreadRoot); |
| 3005 | - CARD_STR2(H, p->zThreadTitle); | |
| 3006 | - CARD_STR2(I, p->zInReplyTo); | |
| 3005 | + ISA( CFTYPE_FORUM ){ | |
| 3006 | + CARD_LETTER(H); | |
| 3007 | + STR_OR_NULL( (p->zThreadTitle && *p->zThreadTitle) ? p->zThreadTitle : NULL); | |
| 3008 | + CARD_STR2(I, p->zInReplyTo); | |
| 3009 | + } | |
| 3007 | 3010 | if( p->nField ){ |
| 3008 | 3011 | CARD_LETTER(J); |
| 3009 | 3012 | blob_append_char(b, '['); |
| 3010 | 3013 | for( i = 0; i < p->nField; ++i ){ |
| 3014 | + const char * zName = p->aField[i].zName; | |
| 3011 | 3015 | if( i>0 ) blob_append_char(b, ','); |
| 3012 | 3016 | blob_append_char(b, '{'); |
| 3013 | - KVP_STR(0, name, p->aField[i].zName); | |
| 3017 | + KVP_STR(0, name, '+'==*zName ? &zName[1] : zName); | |
| 3014 | 3018 | KVP_STR(1, value, p->aField[i].zValue); |
| 3019 | + blob_appendf(b, ",\"append\":%s", '+'==*zName ? "true" : "false"); | |
| 3015 | 3020 | blob_append_char(b, '}'); |
| 3016 | 3021 | } |
| 3017 | 3022 | blob_append_char(b, ']'); |
| 3018 | 3023 | } |
| 3019 | 3024 | CARD_STR2(K, p->zTicketUuid); |
| @@ -3026,18 +3031,16 @@ | ||
| 3026 | 3031 | blob_appendf(b, "%!j", p->azCChild[i]); |
| 3027 | 3032 | } |
| 3028 | 3033 | blob_append_char(b, ']'); |
| 3029 | 3034 | } |
| 3030 | 3035 | CARD_STR2(N, p->zMimetype); |
| 3031 | - ISA( CFTYPE_MANIFEST ){ | |
| 3036 | + ISA( CFTYPE_MANIFEST || p->nParent>0 ){ | |
| 3032 | 3037 | CARD_LETTER(P); |
| 3033 | 3038 | blob_append_char(b, '['); |
| 3034 | - if( p->nParent ){ | |
| 3035 | - for( i = 0; i < p->nParent; ++i ){ | |
| 3036 | - if( i>0 ) blob_append_char(b, ','); | |
| 3037 | - blob_appendf(b, "%!j", p->azParent[i]); | |
| 3038 | - } | |
| 3039 | + for( i = 0; i < p->nParent; ++i ){ | |
| 3040 | + if( i>0 ) blob_append_char(b, ','); | |
| 3041 | + blob_appendf(b, "%!j", p->azParent[i]); | |
| 3039 | 3042 | } |
| 3040 | 3043 | /* Special case: model checkins with no P-card as having an empty |
| 3041 | 3044 | ** array, as per F-cards. */ |
| 3042 | 3045 | blob_append_char(b, ']'); |
| 3043 | 3046 | } |
| @@ -3045,11 +3048,11 @@ | ||
| 3045 | 3048 | CARD_LETTER(Q); |
| 3046 | 3049 | blob_append_char(b, '['); |
| 3047 | 3050 | for( i = 0; i < p->nCherrypick; ++i ){ |
| 3048 | 3051 | if( i>0 ) blob_append_char(b, ','); |
| 3049 | 3052 | blob_append_char(b, '{'); |
| 3050 | - blob_appendf(b, "\"type\": \"%c\"", p->aCherrypick[i].zCPTarget[0]); | |
| 3053 | + blob_appendf(b, "\"type\":\"%c\"", p->aCherrypick[i].zCPTarget[0]); | |
| 3051 | 3054 | KVP_STR(1, target, &p->aCherrypick[i].zCPTarget[1]); |
| 3052 | 3055 | KVP_STR(1, base, p->aCherrypick[i].zCPBase); |
| 3053 | 3056 | blob_append_char(b, '}'); |
| 3054 | 3057 | } |
| 3055 | 3058 | blob_append_char(b, ']'); |
| @@ -3060,21 +3063,25 @@ | ||
| 3060 | 3063 | blob_append_char(b, '['); |
| 3061 | 3064 | for( int i = 0; i < p->nTag; ++i ){ |
| 3062 | 3065 | const char *zName = p->aTag[i].zName; |
| 3063 | 3066 | if( i>0 ) blob_append_char(b, ','); |
| 3064 | 3067 | blob_append_char(b, '{'); |
| 3065 | - blob_appendf(b, "\"type\": \"%c\"", *zName); | |
| 3068 | + blob_appendf(b, "\"type\":\"%c\"", *zName); | |
| 3066 | 3069 | KVP_STR(1, name, &zName[1]); |
| 3067 | 3070 | KVP_STR(1, target, p->aTag[i].zUuid ? p->aTag[i].zUuid : "*") |
| 3068 | 3071 | /* We could arguably resolve the "*" as null or p's uuid. */; |
| 3069 | 3072 | KVP_STR(1, value, p->aTag[i].zValue); |
| 3070 | 3073 | blob_append_char(b, '}'); |
| 3071 | 3074 | } |
| 3072 | 3075 | blob_append_char(b, ']'); |
| 3073 | 3076 | } |
| 3074 | 3077 | CARD_STR2(U, p->zUser); |
| 3075 | - CARD_STR2(W, p->zWiki); | |
| 3078 | + if( p->zWiki || CFTYPE_WIKI==p->type || CFTYPE_FORUM==p->type | |
| 3079 | + || CFTYPE_EVENT==p->type ){ | |
| 3080 | + CARD_LETTER(W); | |
| 3081 | + STR_OR_NULL((p->zWiki && *p->zWiki) ? p->zWiki : NULL); | |
| 3082 | + } | |
| 3076 | 3083 | blob_append_literal(b, "}"); |
| 3077 | 3084 | #undef CARD_FMT |
| 3078 | 3085 | #undef CARD_LETTER |
| 3079 | 3086 | #undef CARD_STR |
| 3080 | 3087 | #undef CARD_STR2 |
| @@ -3164,19 +3171,19 @@ | ||
| 3164 | 3171 | |
| 3165 | 3172 | |
| 3166 | 3173 | /* |
| 3167 | 3174 | ** COMMAND: test-artifact-to-json |
| 3168 | 3175 | ** |
| 3169 | -** Usage: %fossil test-artifact-to-json ?-pretty? symbolic-name [...names] | |
| 3176 | +** Usage: %fossil test-artifact-to-json ?-pretty|-p? symbolic-name [...names] | |
| 3170 | 3177 | ** |
| 3171 | 3178 | ** Tests the artifact_to_json() and artifact_to_json_by_name() APIs. |
| 3172 | 3179 | */ |
| 3173 | 3180 | void test_manifest_to_json(void){ |
| 3174 | 3181 | int i; |
| 3175 | 3182 | Blob b = empty_blob; |
| 3176 | 3183 | Stmt q; |
| 3177 | - const int bPretty = find_option("pretty",0,0)!=0; | |
| 3184 | + const int bPretty = find_option("pretty","p",0)!=0; | |
| 3178 | 3185 | int nErr = 0; |
| 3179 | 3186 | |
| 3180 | 3187 | db_find_and_open_repository(0,0); |
| 3181 | 3188 | db_prepare(&q, "select json_pretty(:json)"); |
| 3182 | 3189 | for( i=2; i<g.argc; ++i ){ |
| 3183 | 3190 |
| --- src/manifest.c | |
| +++ src/manifest.c | |
| @@ -2927,11 +2927,11 @@ | |
| 2927 | case CFTYPE_CLUSTER: return "cluster"; |
| 2928 | case CFTYPE_CONTROL: return "tag"; |
| 2929 | case CFTYPE_WIKI: return "wiki"; |
| 2930 | case CFTYPE_TICKET: return "ticket"; |
| 2931 | case CFTYPE_ATTACHMENT: return "attachment"; |
| 2932 | case CFTYPE_EVENT: return "event"; |
| 2933 | case CFTYPE_FORUM: return "forumpost"; |
| 2934 | } |
| 2935 | return NULL; |
| 2936 | } |
| 2937 | |
| @@ -2948,26 +2948,26 @@ | |
| 2948 | */ |
| 2949 | void artifact_to_json(Manifest const *p, Blob *b){ |
| 2950 | int i; |
| 2951 | |
| 2952 | blob_append_literal(b, "{"); |
| 2953 | blob_appendf(b, "\"uuid\": \"%z\"", rid_to_uuid(p->rid)); |
| 2954 | /*blob_appendf(b, ", \"rid\": %d", p->rid); not portable across repos*/ |
| 2955 | blob_appendf(b, ", \"type\": %!j", artifact_type_to_name(p->type)); |
| 2956 | #define ISA(TYPE) if( p->type==TYPE ) |
| 2957 | #define CARD_LETTER(LETTER) \ |
| 2958 | blob_append_literal(b, ",\"" #LETTER "\": ") |
| 2959 | #define CARD_STR(LETTER, VAL) \ |
| 2960 | assert( VAL ); CARD_LETTER(LETTER); blob_appendf(b, "%!j", VAL) |
| 2961 | #define CARD_STR2(LETTER, VAL) \ |
| 2962 | if( VAL ) { CARD_STR(LETTER, VAL); } (void)0 |
| 2963 | #define STR_OR_NULL(VAL) \ |
| 2964 | if( VAL ) blob_appendf(b, "%!j", VAL); \ |
| 2965 | else blob_append(b, "null", 4) |
| 2966 | #define KVP_STR(ADDCOMMA, KEY,VAL) \ |
| 2967 | if(ADDCOMMA) blob_append_char(b, ','); \ |
| 2968 | blob_appendf(b, "%!j: ", #KEY); \ |
| 2969 | STR_OR_NULL(VAL) |
| 2970 | |
| 2971 | ISA( CFTYPE_ATTACHMENT ){ |
| 2972 | CARD_LETTER(A); |
| 2973 | blob_append_char(b, '{'); |
| @@ -2978,11 +2978,11 @@ | |
| 2978 | } |
| 2979 | CARD_STR2(B, p->zBaseline); |
| 2980 | CARD_STR2(C, p->zComment); |
| 2981 | CARD_LETTER(D); blob_appendf(b, "%f", p->rDate); |
| 2982 | ISA( CFTYPE_EVENT ){ |
| 2983 | blob_appendf(b, ", \"E\": {\"time\": %f, \"id\": %!j}", |
| 2984 | p->rEventDate, p->zEventId); |
| 2985 | } |
| 2986 | ISA( CFTYPE_MANIFEST ){ |
| 2987 | CARD_LETTER(F); |
| 2988 | blob_append_char(b, '['); |
| @@ -2991,29 +2991,34 @@ | |
| 2991 | if( i>0 ) blob_append_char(b, ','); |
| 2992 | blob_append_char(b, '{'); |
| 2993 | KVP_STR(0, name, pF->zName); |
| 2994 | KVP_STR(1, uuid, pF->zUuid); |
| 2995 | KVP_STR(1, perm, pF->zPerm); |
| 2996 | KVP_STR(1, oldName, pF->zPrior); |
| 2997 | blob_append_char(b, '}'); |
| 2998 | } |
| 2999 | /* Special case: model checkins with no F-card as having an empty |
| 3000 | ** array, rather than no F-cards, to hypothetically simplify |
| 3001 | ** handling in JSON queries. */ |
| 3002 | blob_append_char(b, ']'); |
| 3003 | } |
| 3004 | CARD_STR2(G, p->zThreadRoot); |
| 3005 | CARD_STR2(H, p->zThreadTitle); |
| 3006 | CARD_STR2(I, p->zInReplyTo); |
| 3007 | if( p->nField ){ |
| 3008 | CARD_LETTER(J); |
| 3009 | blob_append_char(b, '['); |
| 3010 | for( i = 0; i < p->nField; ++i ){ |
| 3011 | if( i>0 ) blob_append_char(b, ','); |
| 3012 | blob_append_char(b, '{'); |
| 3013 | KVP_STR(0, name, p->aField[i].zName); |
| 3014 | KVP_STR(1, value, p->aField[i].zValue); |
| 3015 | blob_append_char(b, '}'); |
| 3016 | } |
| 3017 | blob_append_char(b, ']'); |
| 3018 | } |
| 3019 | CARD_STR2(K, p->zTicketUuid); |
| @@ -3026,18 +3031,16 @@ | |
| 3026 | blob_appendf(b, "%!j", p->azCChild[i]); |
| 3027 | } |
| 3028 | blob_append_char(b, ']'); |
| 3029 | } |
| 3030 | CARD_STR2(N, p->zMimetype); |
| 3031 | ISA( CFTYPE_MANIFEST ){ |
| 3032 | CARD_LETTER(P); |
| 3033 | blob_append_char(b, '['); |
| 3034 | if( p->nParent ){ |
| 3035 | for( i = 0; i < p->nParent; ++i ){ |
| 3036 | if( i>0 ) blob_append_char(b, ','); |
| 3037 | blob_appendf(b, "%!j", p->azParent[i]); |
| 3038 | } |
| 3039 | } |
| 3040 | /* Special case: model checkins with no P-card as having an empty |
| 3041 | ** array, as per F-cards. */ |
| 3042 | blob_append_char(b, ']'); |
| 3043 | } |
| @@ -3045,11 +3048,11 @@ | |
| 3045 | CARD_LETTER(Q); |
| 3046 | blob_append_char(b, '['); |
| 3047 | for( i = 0; i < p->nCherrypick; ++i ){ |
| 3048 | if( i>0 ) blob_append_char(b, ','); |
| 3049 | blob_append_char(b, '{'); |
| 3050 | blob_appendf(b, "\"type\": \"%c\"", p->aCherrypick[i].zCPTarget[0]); |
| 3051 | KVP_STR(1, target, &p->aCherrypick[i].zCPTarget[1]); |
| 3052 | KVP_STR(1, base, p->aCherrypick[i].zCPBase); |
| 3053 | blob_append_char(b, '}'); |
| 3054 | } |
| 3055 | blob_append_char(b, ']'); |
| @@ -3060,21 +3063,25 @@ | |
| 3060 | blob_append_char(b, '['); |
| 3061 | for( int i = 0; i < p->nTag; ++i ){ |
| 3062 | const char *zName = p->aTag[i].zName; |
| 3063 | if( i>0 ) blob_append_char(b, ','); |
| 3064 | blob_append_char(b, '{'); |
| 3065 | blob_appendf(b, "\"type\": \"%c\"", *zName); |
| 3066 | KVP_STR(1, name, &zName[1]); |
| 3067 | KVP_STR(1, target, p->aTag[i].zUuid ? p->aTag[i].zUuid : "*") |
| 3068 | /* We could arguably resolve the "*" as null or p's uuid. */; |
| 3069 | KVP_STR(1, value, p->aTag[i].zValue); |
| 3070 | blob_append_char(b, '}'); |
| 3071 | } |
| 3072 | blob_append_char(b, ']'); |
| 3073 | } |
| 3074 | CARD_STR2(U, p->zUser); |
| 3075 | CARD_STR2(W, p->zWiki); |
| 3076 | blob_append_literal(b, "}"); |
| 3077 | #undef CARD_FMT |
| 3078 | #undef CARD_LETTER |
| 3079 | #undef CARD_STR |
| 3080 | #undef CARD_STR2 |
| @@ -3164,19 +3171,19 @@ | |
| 3164 | |
| 3165 | |
| 3166 | /* |
| 3167 | ** COMMAND: test-artifact-to-json |
| 3168 | ** |
| 3169 | ** Usage: %fossil test-artifact-to-json ?-pretty? symbolic-name [...names] |
| 3170 | ** |
| 3171 | ** Tests the artifact_to_json() and artifact_to_json_by_name() APIs. |
| 3172 | */ |
| 3173 | void test_manifest_to_json(void){ |
| 3174 | int i; |
| 3175 | Blob b = empty_blob; |
| 3176 | Stmt q; |
| 3177 | const int bPretty = find_option("pretty",0,0)!=0; |
| 3178 | int nErr = 0; |
| 3179 | |
| 3180 | db_find_and_open_repository(0,0); |
| 3181 | db_prepare(&q, "select json_pretty(:json)"); |
| 3182 | for( i=2; i<g.argc; ++i ){ |
| 3183 |
| --- src/manifest.c | |
| +++ src/manifest.c | |
| @@ -2927,11 +2927,11 @@ | |
| 2927 | case CFTYPE_CLUSTER: return "cluster"; |
| 2928 | case CFTYPE_CONTROL: return "tag"; |
| 2929 | case CFTYPE_WIKI: return "wiki"; |
| 2930 | case CFTYPE_TICKET: return "ticket"; |
| 2931 | case CFTYPE_ATTACHMENT: return "attachment"; |
| 2932 | case CFTYPE_EVENT: return "technote"; |
| 2933 | case CFTYPE_FORUM: return "forumpost"; |
| 2934 | } |
| 2935 | return NULL; |
| 2936 | } |
| 2937 | |
| @@ -2948,26 +2948,26 @@ | |
| 2948 | */ |
| 2949 | void artifact_to_json(Manifest const *p, Blob *b){ |
| 2950 | int i; |
| 2951 | |
| 2952 | blob_append_literal(b, "{"); |
| 2953 | blob_appendf(b, "\"uuid\":\"%z\"", rid_to_uuid(p->rid)); |
| 2954 | /*blob_appendf(b, ", \"rid\": %d", p->rid); not portable across repos*/ |
| 2955 | blob_appendf(b, ",\"type\":%!j", artifact_type_to_name(p->type)); |
| 2956 | #define ISA(TYPE) if( p->type==TYPE ) |
| 2957 | #define CARD_LETTER(LETTER) \ |
| 2958 | blob_append_literal(b, ",\"" #LETTER "\":") |
| 2959 | #define CARD_STR(LETTER, VAL) \ |
| 2960 | assert( VAL ); CARD_LETTER(LETTER); blob_appendf(b, "%!j", VAL) |
| 2961 | #define CARD_STR2(LETTER, VAL) \ |
| 2962 | if( VAL ) { CARD_STR(LETTER, VAL); } (void)0 |
| 2963 | #define STR_OR_NULL(VAL) \ |
| 2964 | if( VAL ) blob_appendf(b, "%!j", VAL); \ |
| 2965 | else blob_append(b, "null", 4) |
| 2966 | #define KVP_STR(ADDCOMMA, KEY,VAL) \ |
| 2967 | if(ADDCOMMA) blob_append_char(b, ','); \ |
| 2968 | blob_appendf(b, "%!j:", #KEY); \ |
| 2969 | STR_OR_NULL(VAL) |
| 2970 | |
| 2971 | ISA( CFTYPE_ATTACHMENT ){ |
| 2972 | CARD_LETTER(A); |
| 2973 | blob_append_char(b, '{'); |
| @@ -2978,11 +2978,11 @@ | |
| 2978 | } |
| 2979 | CARD_STR2(B, p->zBaseline); |
| 2980 | CARD_STR2(C, p->zComment); |
| 2981 | CARD_LETTER(D); blob_appendf(b, "%f", p->rDate); |
| 2982 | ISA( CFTYPE_EVENT ){ |
| 2983 | blob_appendf(b, ", \"E\":{\"time\":%f,\"id\":%!j}", |
| 2984 | p->rEventDate, p->zEventId); |
| 2985 | } |
| 2986 | ISA( CFTYPE_MANIFEST ){ |
| 2987 | CARD_LETTER(F); |
| 2988 | blob_append_char(b, '['); |
| @@ -2991,29 +2991,34 @@ | |
| 2991 | if( i>0 ) blob_append_char(b, ','); |
| 2992 | blob_append_char(b, '{'); |
| 2993 | KVP_STR(0, name, pF->zName); |
| 2994 | KVP_STR(1, uuid, pF->zUuid); |
| 2995 | KVP_STR(1, perm, pF->zPerm); |
| 2996 | KVP_STR(1, rename, pF->zPrior); |
| 2997 | blob_append_char(b, '}'); |
| 2998 | } |
| 2999 | /* Special case: model checkins with no F-card as having an empty |
| 3000 | ** array, rather than no F-cards, to hypothetically simplify |
| 3001 | ** handling in JSON queries. */ |
| 3002 | blob_append_char(b, ']'); |
| 3003 | } |
| 3004 | CARD_STR2(G, p->zThreadRoot); |
| 3005 | ISA( CFTYPE_FORUM ){ |
| 3006 | CARD_LETTER(H); |
| 3007 | STR_OR_NULL( (p->zThreadTitle && *p->zThreadTitle) ? p->zThreadTitle : NULL); |
| 3008 | CARD_STR2(I, p->zInReplyTo); |
| 3009 | } |
| 3010 | if( p->nField ){ |
| 3011 | CARD_LETTER(J); |
| 3012 | blob_append_char(b, '['); |
| 3013 | for( i = 0; i < p->nField; ++i ){ |
| 3014 | const char * zName = p->aField[i].zName; |
| 3015 | if( i>0 ) blob_append_char(b, ','); |
| 3016 | blob_append_char(b, '{'); |
| 3017 | KVP_STR(0, name, '+'==*zName ? &zName[1] : zName); |
| 3018 | KVP_STR(1, value, p->aField[i].zValue); |
| 3019 | blob_appendf(b, ",\"append\":%s", '+'==*zName ? "true" : "false"); |
| 3020 | blob_append_char(b, '}'); |
| 3021 | } |
| 3022 | blob_append_char(b, ']'); |
| 3023 | } |
| 3024 | CARD_STR2(K, p->zTicketUuid); |
| @@ -3026,18 +3031,16 @@ | |
| 3031 | blob_appendf(b, "%!j", p->azCChild[i]); |
| 3032 | } |
| 3033 | blob_append_char(b, ']'); |
| 3034 | } |
| 3035 | CARD_STR2(N, p->zMimetype); |
| 3036 | ISA( CFTYPE_MANIFEST || p->nParent>0 ){ |
| 3037 | CARD_LETTER(P); |
| 3038 | blob_append_char(b, '['); |
| 3039 | for( i = 0; i < p->nParent; ++i ){ |
| 3040 | if( i>0 ) blob_append_char(b, ','); |
| 3041 | blob_appendf(b, "%!j", p->azParent[i]); |
| 3042 | } |
| 3043 | /* Special case: model checkins with no P-card as having an empty |
| 3044 | ** array, as per F-cards. */ |
| 3045 | blob_append_char(b, ']'); |
| 3046 | } |
| @@ -3045,11 +3048,11 @@ | |
| 3048 | CARD_LETTER(Q); |
| 3049 | blob_append_char(b, '['); |
| 3050 | for( i = 0; i < p->nCherrypick; ++i ){ |
| 3051 | if( i>0 ) blob_append_char(b, ','); |
| 3052 | blob_append_char(b, '{'); |
| 3053 | blob_appendf(b, "\"type\":\"%c\"", p->aCherrypick[i].zCPTarget[0]); |
| 3054 | KVP_STR(1, target, &p->aCherrypick[i].zCPTarget[1]); |
| 3055 | KVP_STR(1, base, p->aCherrypick[i].zCPBase); |
| 3056 | blob_append_char(b, '}'); |
| 3057 | } |
| 3058 | blob_append_char(b, ']'); |
| @@ -3060,21 +3063,25 @@ | |
| 3063 | blob_append_char(b, '['); |
| 3064 | for( int i = 0; i < p->nTag; ++i ){ |
| 3065 | const char *zName = p->aTag[i].zName; |
| 3066 | if( i>0 ) blob_append_char(b, ','); |
| 3067 | blob_append_char(b, '{'); |
| 3068 | blob_appendf(b, "\"type\":\"%c\"", *zName); |
| 3069 | KVP_STR(1, name, &zName[1]); |
| 3070 | KVP_STR(1, target, p->aTag[i].zUuid ? p->aTag[i].zUuid : "*") |
| 3071 | /* We could arguably resolve the "*" as null or p's uuid. */; |
| 3072 | KVP_STR(1, value, p->aTag[i].zValue); |
| 3073 | blob_append_char(b, '}'); |
| 3074 | } |
| 3075 | blob_append_char(b, ']'); |
| 3076 | } |
| 3077 | CARD_STR2(U, p->zUser); |
| 3078 | if( p->zWiki || CFTYPE_WIKI==p->type || CFTYPE_FORUM==p->type |
| 3079 | || CFTYPE_EVENT==p->type ){ |
| 3080 | CARD_LETTER(W); |
| 3081 | STR_OR_NULL((p->zWiki && *p->zWiki) ? p->zWiki : NULL); |
| 3082 | } |
| 3083 | blob_append_literal(b, "}"); |
| 3084 | #undef CARD_FMT |
| 3085 | #undef CARD_LETTER |
| 3086 | #undef CARD_STR |
| 3087 | #undef CARD_STR2 |
| @@ -3164,19 +3171,19 @@ | |
| 3171 | |
| 3172 | |
| 3173 | /* |
| 3174 | ** COMMAND: test-artifact-to-json |
| 3175 | ** |
| 3176 | ** Usage: %fossil test-artifact-to-json ?-pretty|-p? symbolic-name [...names] |
| 3177 | ** |
| 3178 | ** Tests the artifact_to_json() and artifact_to_json_by_name() APIs. |
| 3179 | */ |
| 3180 | void test_manifest_to_json(void){ |
| 3181 | int i; |
| 3182 | Blob b = empty_blob; |
| 3183 | Stmt q; |
| 3184 | const int bPretty = find_option("pretty","p",0)!=0; |
| 3185 | int nErr = 0; |
| 3186 | |
| 3187 | db_find_and_open_repository(0,0); |
| 3188 | db_prepare(&q, "select json_pretty(:json)"); |
| 3189 | for( i=2; i<g.argc; ++i ){ |
| 3190 |
+65
-6
| --- src/name.c | ||
| +++ src/name.c | ||
| @@ -1816,11 +1816,11 @@ | ||
| 1816 | 1816 | } |
| 1817 | 1817 | if( bUnclst==0 ){ |
| 1818 | 1818 | style_submenu_element("Unclustered","bloblist?unclustered"); |
| 1819 | 1819 | } |
| 1820 | 1820 | if( g.perm.Admin ){ |
| 1821 | - style_submenu_element("Artifact Log", "rcvfromlist"); | |
| 1821 | + style_submenu_element("Xfer Log", "rcvfromlist"); | |
| 1822 | 1822 | } |
| 1823 | 1823 | if( !phantomOnly ){ |
| 1824 | 1824 | style_submenu_element("Phantoms", "bloblist?phan"); |
| 1825 | 1825 | } |
| 1826 | 1826 | style_submenu_element("Clusters","clusterlist"); |
| @@ -2005,11 +2005,11 @@ | ||
| 2005 | 2005 | void phantom_list_page(void){ |
| 2006 | 2006 | login_check_credentials(); |
| 2007 | 2007 | if( !g.perm.Read ){ login_needed(g.anon.Read); return; } |
| 2008 | 2008 | style_header("Public Phantom Artifacts"); |
| 2009 | 2009 | if( g.perm.Admin ){ |
| 2010 | - style_submenu_element("Artifact Log", "rcvfromlist"); | |
| 2010 | + style_submenu_element("Xfer Log", "rcvfromlist"); | |
| 2011 | 2011 | style_submenu_element("Artifact List", "bloblist"); |
| 2012 | 2012 | } |
| 2013 | 2013 | if( g.perm.Write ){ |
| 2014 | 2014 | style_submenu_element("Artifact Stats", "artifact_stats"); |
| 2015 | 2015 | } |
| @@ -2030,11 +2030,11 @@ | ||
| 2030 | 2030 | int n = atoi(PD("n","250")); |
| 2031 | 2031 | |
| 2032 | 2032 | login_check_credentials(); |
| 2033 | 2033 | if( !g.perm.Read ){ login_needed(g.anon.Read); return; } |
| 2034 | 2034 | if( g.perm.Admin ){ |
| 2035 | - style_submenu_element("Artifact Log", "rcvfromlist"); | |
| 2035 | + style_submenu_element("Xfer Log", "rcvfromlist"); | |
| 2036 | 2036 | } |
| 2037 | 2037 | if( g.perm.Write ){ |
| 2038 | 2038 | style_submenu_element("Artifact Stats", "artifact_stats"); |
| 2039 | 2039 | } |
| 2040 | 2040 | style_submenu_element("All Artifacts", "bloblist"); |
| @@ -2203,15 +2203,74 @@ | ||
| 2203 | 2203 | /* |
| 2204 | 2204 | ** COMMAND: test-phantoms |
| 2205 | 2205 | ** |
| 2206 | 2206 | ** Usage: %fossil test-phantoms |
| 2207 | 2207 | ** |
| 2208 | -** Show all phantom artifacts | |
| 2208 | +** Show all phantom artifacts. A phantom artifact is one for which there | |
| 2209 | +** is no content. Options: | |
| 2210 | +** | |
| 2211 | +** --count Show only a count of the number of phantoms. | |
| 2212 | +** --delta Show all delta-phantoms. A delta-phantom is a | |
| 2213 | +** artifact for which there is a delta but the delta | |
| 2214 | +** source is a phantom. | |
| 2215 | +** --list Just list the phantoms. Do not try to describe them. | |
| 2209 | 2216 | */ |
| 2210 | 2217 | void test_phatoms_cmd(void){ |
| 2218 | + int bDelta; | |
| 2219 | + int bList; | |
| 2220 | + int bCount; | |
| 2221 | + unsigned nPhantom = 0; | |
| 2222 | + unsigned nDeltaPhantom = 0; | |
| 2211 | 2223 | db_find_and_open_repository(0,0); |
| 2212 | - describe_artifacts_to_stdout("IN (SELECT rid FROM blob WHERE size<0)", 0); | |
| 2224 | + bDelta = find_option("delta", 0, 0)!=0; | |
| 2225 | + bList = find_option("list", 0, 0)!=0; | |
| 2226 | + bCount = find_option("count", 0, 0)!=0; | |
| 2227 | + verify_all_options(); | |
| 2228 | + if( bList || bCount ){ | |
| 2229 | + Stmt q1, q2; | |
| 2230 | + db_prepare(&q1, "SELECT rid, uuid FROM blob WHERE size<0"); | |
| 2231 | + while( db_step(&q1)==SQLITE_ROW ){ | |
| 2232 | + int rid = db_column_int(&q1, 0); | |
| 2233 | + nPhantom++; | |
| 2234 | + if( !bCount ){ | |
| 2235 | + fossil_print("%S (%d)\n", db_column_text(&q1,1), rid); | |
| 2236 | + } | |
| 2237 | + db_prepare(&q2, | |
| 2238 | + "WITH RECURSIVE deltasof(rid) AS (" | |
| 2239 | + " SELECT rid FROM delta WHERE srcid=%d" | |
| 2240 | + " UNION" | |
| 2241 | + " SELECT delta.rid FROM deltasof, delta" | |
| 2242 | + " WHERE delta.srcid=deltasof.rid)" | |
| 2243 | + "SELECT deltasof.rid, blob.uuid FROM deltasof LEFT JOIN blob" | |
| 2244 | + " ON blob.rid=deltasof.rid", rid | |
| 2245 | + ); | |
| 2246 | + while( db_step(&q2)==SQLITE_ROW ){ | |
| 2247 | + nDeltaPhantom++; | |
| 2248 | + if( !bCount ){ | |
| 2249 | + fossil_print(" %S (%d)\n", db_column_text(&q2,1), | |
| 2250 | + db_column_int(&q2,0)); | |
| 2251 | + } | |
| 2252 | + } | |
| 2253 | + db_finalize(&q2); | |
| 2254 | + } | |
| 2255 | + db_finalize(&q1); | |
| 2256 | + if( nPhantom ){ | |
| 2257 | + fossil_print("Phantoms: %u Delta-phantoms: %u\n", | |
| 2258 | + nPhantom, nDeltaPhantom); | |
| 2259 | + } | |
| 2260 | + }else if( bDelta ){ | |
| 2261 | + describe_artifacts_to_stdout( | |
| 2262 | + "IN (WITH RECURSIVE delta_phantom(rid) AS (\n" | |
| 2263 | + " SELECT delta.rid FROM blob, delta\n" | |
| 2264 | + " WHERE blob.size<0 AND delta.srcid=blob.rid\n" | |
| 2265 | + " UNION\n" | |
| 2266 | + " SELECT delta.rid FROM delta_phantom, delta\n" | |
| 2267 | + " WHERE delta.srcid=delta_phantom.rid)\n" | |
| 2268 | + " SELECT rid FROM delta_phantom)", 0); | |
| 2269 | + }else{ | |
| 2270 | + describe_artifacts_to_stdout("IN (SELECT rid FROM blob WHERE size<0)", 0); | |
| 2271 | + } | |
| 2213 | 2272 | } |
| 2214 | 2273 | |
| 2215 | 2274 | /* Maximum number of collision examples to remember */ |
| 2216 | 2275 | #define MAX_COLLIDE 25 |
| 2217 | 2276 | |
| @@ -2311,11 +2370,11 @@ | ||
| 2311 | 2370 | login_check_credentials(); |
| 2312 | 2371 | if( !g.perm.Read ){ login_needed(g.anon.Read); return; } |
| 2313 | 2372 | style_header("All Cluster Artifacts"); |
| 2314 | 2373 | style_submenu_element("All Artifactst", "bloblist"); |
| 2315 | 2374 | if( g.perm.Admin ){ |
| 2316 | - style_submenu_element("Artifact Log", "rcvfromlist"); | |
| 2375 | + style_submenu_element("Xfer Log", "rcvfromlist"); | |
| 2317 | 2376 | } |
| 2318 | 2377 | style_submenu_element("Phantoms", "bloblist?phan"); |
| 2319 | 2378 | if( g.perm.Write ){ |
| 2320 | 2379 | style_submenu_element("Artifact Stats", "artifact_stats"); |
| 2321 | 2380 | } |
| 2322 | 2381 |
| --- src/name.c | |
| +++ src/name.c | |
| @@ -1816,11 +1816,11 @@ | |
| 1816 | } |
| 1817 | if( bUnclst==0 ){ |
| 1818 | style_submenu_element("Unclustered","bloblist?unclustered"); |
| 1819 | } |
| 1820 | if( g.perm.Admin ){ |
| 1821 | style_submenu_element("Artifact Log", "rcvfromlist"); |
| 1822 | } |
| 1823 | if( !phantomOnly ){ |
| 1824 | style_submenu_element("Phantoms", "bloblist?phan"); |
| 1825 | } |
| 1826 | style_submenu_element("Clusters","clusterlist"); |
| @@ -2005,11 +2005,11 @@ | |
| 2005 | void phantom_list_page(void){ |
| 2006 | login_check_credentials(); |
| 2007 | if( !g.perm.Read ){ login_needed(g.anon.Read); return; } |
| 2008 | style_header("Public Phantom Artifacts"); |
| 2009 | if( g.perm.Admin ){ |
| 2010 | style_submenu_element("Artifact Log", "rcvfromlist"); |
| 2011 | style_submenu_element("Artifact List", "bloblist"); |
| 2012 | } |
| 2013 | if( g.perm.Write ){ |
| 2014 | style_submenu_element("Artifact Stats", "artifact_stats"); |
| 2015 | } |
| @@ -2030,11 +2030,11 @@ | |
| 2030 | int n = atoi(PD("n","250")); |
| 2031 | |
| 2032 | login_check_credentials(); |
| 2033 | if( !g.perm.Read ){ login_needed(g.anon.Read); return; } |
| 2034 | if( g.perm.Admin ){ |
| 2035 | style_submenu_element("Artifact Log", "rcvfromlist"); |
| 2036 | } |
| 2037 | if( g.perm.Write ){ |
| 2038 | style_submenu_element("Artifact Stats", "artifact_stats"); |
| 2039 | } |
| 2040 | style_submenu_element("All Artifacts", "bloblist"); |
| @@ -2203,15 +2203,74 @@ | |
| 2203 | /* |
| 2204 | ** COMMAND: test-phantoms |
| 2205 | ** |
| 2206 | ** Usage: %fossil test-phantoms |
| 2207 | ** |
| 2208 | ** Show all phantom artifacts |
| 2209 | */ |
| 2210 | void test_phatoms_cmd(void){ |
| 2211 | db_find_and_open_repository(0,0); |
| 2212 | describe_artifacts_to_stdout("IN (SELECT rid FROM blob WHERE size<0)", 0); |
| 2213 | } |
| 2214 | |
| 2215 | /* Maximum number of collision examples to remember */ |
| 2216 | #define MAX_COLLIDE 25 |
| 2217 | |
| @@ -2311,11 +2370,11 @@ | |
| 2311 | login_check_credentials(); |
| 2312 | if( !g.perm.Read ){ login_needed(g.anon.Read); return; } |
| 2313 | style_header("All Cluster Artifacts"); |
| 2314 | style_submenu_element("All Artifactst", "bloblist"); |
| 2315 | if( g.perm.Admin ){ |
| 2316 | style_submenu_element("Artifact Log", "rcvfromlist"); |
| 2317 | } |
| 2318 | style_submenu_element("Phantoms", "bloblist?phan"); |
| 2319 | if( g.perm.Write ){ |
| 2320 | style_submenu_element("Artifact Stats", "artifact_stats"); |
| 2321 | } |
| 2322 |
| --- src/name.c | |
| +++ src/name.c | |
| @@ -1816,11 +1816,11 @@ | |
| 1816 | } |
| 1817 | if( bUnclst==0 ){ |
| 1818 | style_submenu_element("Unclustered","bloblist?unclustered"); |
| 1819 | } |
| 1820 | if( g.perm.Admin ){ |
| 1821 | style_submenu_element("Xfer Log", "rcvfromlist"); |
| 1822 | } |
| 1823 | if( !phantomOnly ){ |
| 1824 | style_submenu_element("Phantoms", "bloblist?phan"); |
| 1825 | } |
| 1826 | style_submenu_element("Clusters","clusterlist"); |
| @@ -2005,11 +2005,11 @@ | |
| 2005 | void phantom_list_page(void){ |
| 2006 | login_check_credentials(); |
| 2007 | if( !g.perm.Read ){ login_needed(g.anon.Read); return; } |
| 2008 | style_header("Public Phantom Artifacts"); |
| 2009 | if( g.perm.Admin ){ |
| 2010 | style_submenu_element("Xfer Log", "rcvfromlist"); |
| 2011 | style_submenu_element("Artifact List", "bloblist"); |
| 2012 | } |
| 2013 | if( g.perm.Write ){ |
| 2014 | style_submenu_element("Artifact Stats", "artifact_stats"); |
| 2015 | } |
| @@ -2030,11 +2030,11 @@ | |
| 2030 | int n = atoi(PD("n","250")); |
| 2031 | |
| 2032 | login_check_credentials(); |
| 2033 | if( !g.perm.Read ){ login_needed(g.anon.Read); return; } |
| 2034 | if( g.perm.Admin ){ |
| 2035 | style_submenu_element("Xfer Log", "rcvfromlist"); |
| 2036 | } |
| 2037 | if( g.perm.Write ){ |
| 2038 | style_submenu_element("Artifact Stats", "artifact_stats"); |
| 2039 | } |
| 2040 | style_submenu_element("All Artifacts", "bloblist"); |
| @@ -2203,15 +2203,74 @@ | |
| 2203 | /* |
| 2204 | ** COMMAND: test-phantoms |
| 2205 | ** |
| 2206 | ** Usage: %fossil test-phantoms |
| 2207 | ** |
| 2208 | ** Show all phantom artifacts. A phantom artifact is one for which there |
| 2209 | ** is no content. Options: |
| 2210 | ** |
| 2211 | ** --count Show only a count of the number of phantoms. |
| 2212 | ** --delta Show all delta-phantoms. A delta-phantom is a |
| 2213 | ** artifact for which there is a delta but the delta |
| 2214 | ** source is a phantom. |
| 2215 | ** --list Just list the phantoms. Do not try to describe them. |
| 2216 | */ |
| 2217 | void test_phatoms_cmd(void){ |
| 2218 | int bDelta; |
| 2219 | int bList; |
| 2220 | int bCount; |
| 2221 | unsigned nPhantom = 0; |
| 2222 | unsigned nDeltaPhantom = 0; |
| 2223 | db_find_and_open_repository(0,0); |
| 2224 | bDelta = find_option("delta", 0, 0)!=0; |
| 2225 | bList = find_option("list", 0, 0)!=0; |
| 2226 | bCount = find_option("count", 0, 0)!=0; |
| 2227 | verify_all_options(); |
| 2228 | if( bList || bCount ){ |
| 2229 | Stmt q1, q2; |
| 2230 | db_prepare(&q1, "SELECT rid, uuid FROM blob WHERE size<0"); |
| 2231 | while( db_step(&q1)==SQLITE_ROW ){ |
| 2232 | int rid = db_column_int(&q1, 0); |
| 2233 | nPhantom++; |
| 2234 | if( !bCount ){ |
| 2235 | fossil_print("%S (%d)\n", db_column_text(&q1,1), rid); |
| 2236 | } |
| 2237 | db_prepare(&q2, |
| 2238 | "WITH RECURSIVE deltasof(rid) AS (" |
| 2239 | " SELECT rid FROM delta WHERE srcid=%d" |
| 2240 | " UNION" |
| 2241 | " SELECT delta.rid FROM deltasof, delta" |
| 2242 | " WHERE delta.srcid=deltasof.rid)" |
| 2243 | "SELECT deltasof.rid, blob.uuid FROM deltasof LEFT JOIN blob" |
| 2244 | " ON blob.rid=deltasof.rid", rid |
| 2245 | ); |
| 2246 | while( db_step(&q2)==SQLITE_ROW ){ |
| 2247 | nDeltaPhantom++; |
| 2248 | if( !bCount ){ |
| 2249 | fossil_print(" %S (%d)\n", db_column_text(&q2,1), |
| 2250 | db_column_int(&q2,0)); |
| 2251 | } |
| 2252 | } |
| 2253 | db_finalize(&q2); |
| 2254 | } |
| 2255 | db_finalize(&q1); |
| 2256 | if( nPhantom ){ |
| 2257 | fossil_print("Phantoms: %u Delta-phantoms: %u\n", |
| 2258 | nPhantom, nDeltaPhantom); |
| 2259 | } |
| 2260 | }else if( bDelta ){ |
| 2261 | describe_artifacts_to_stdout( |
| 2262 | "IN (WITH RECURSIVE delta_phantom(rid) AS (\n" |
| 2263 | " SELECT delta.rid FROM blob, delta\n" |
| 2264 | " WHERE blob.size<0 AND delta.srcid=blob.rid\n" |
| 2265 | " UNION\n" |
| 2266 | " SELECT delta.rid FROM delta_phantom, delta\n" |
| 2267 | " WHERE delta.srcid=delta_phantom.rid)\n" |
| 2268 | " SELECT rid FROM delta_phantom)", 0); |
| 2269 | }else{ |
| 2270 | describe_artifacts_to_stdout("IN (SELECT rid FROM blob WHERE size<0)", 0); |
| 2271 | } |
| 2272 | } |
| 2273 | |
| 2274 | /* Maximum number of collision examples to remember */ |
| 2275 | #define MAX_COLLIDE 25 |
| 2276 | |
| @@ -2311,11 +2370,11 @@ | |
| 2370 | login_check_credentials(); |
| 2371 | if( !g.perm.Read ){ login_needed(g.anon.Read); return; } |
| 2372 | style_header("All Cluster Artifacts"); |
| 2373 | style_submenu_element("All Artifactst", "bloblist"); |
| 2374 | if( g.perm.Admin ){ |
| 2375 | style_submenu_element("Xfer Log", "rcvfromlist"); |
| 2376 | } |
| 2377 | style_submenu_element("Phantoms", "bloblist?phan"); |
| 2378 | if( g.perm.Write ){ |
| 2379 | style_submenu_element("Artifact Stats", "artifact_stats"); |
| 2380 | } |
| 2381 |
+8
-3
| --- src/printf.c | ||
| +++ src/printf.c | ||
| @@ -1077,10 +1077,11 @@ | ||
| 1077 | 1077 | time_t now; |
| 1078 | 1078 | FILE *out; |
| 1079 | 1079 | const char *z; |
| 1080 | 1080 | int i; |
| 1081 | 1081 | int bDetail = 0; |
| 1082 | + int bBrief = 0; | |
| 1082 | 1083 | va_list ap; |
| 1083 | 1084 | static const char *const azEnv[] = { "HTTP_HOST", "HTTP_REFERER", |
| 1084 | 1085 | "HTTP_USER_AGENT", |
| 1085 | 1086 | "PATH_INFO", "QUERY_STRING", "REMOTE_ADDR", "REQUEST_METHOD", |
| 1086 | 1087 | "REQUEST_URI", "SCRIPT_NAME" }; |
| @@ -1098,16 +1099,20 @@ | ||
| 1098 | 1099 | pNow->tm_hour, pNow->tm_min, pNow->tm_sec); |
| 1099 | 1100 | va_start(ap, zFormat); |
| 1100 | 1101 | if( zFormat[0]=='X' ){ |
| 1101 | 1102 | bDetail = 1; |
| 1102 | 1103 | zFormat++; |
| 1104 | + }else if( strncmp(zFormat,"SMTP:",5)==0 ){ | |
| 1105 | + bBrief = 1; | |
| 1103 | 1106 | } |
| 1104 | 1107 | vfprintf(out, zFormat, ap); |
| 1105 | - fprintf(out, "\n"); | |
| 1108 | + fprintf(out, " (pid %d)\n", (int)getpid()); | |
| 1106 | 1109 | va_end(ap); |
| 1107 | 1110 | if( g.zPhase!=0 ) fprintf(out, "while in %s\n", g.zPhase); |
| 1108 | - if( bDetail ){ | |
| 1111 | + if( bBrief ){ | |
| 1112 | + /* Say nothing more */ | |
| 1113 | + }else if( bDetail ){ | |
| 1109 | 1114 | cgi_print_all(1,3,out); |
| 1110 | 1115 | }else{ |
| 1111 | 1116 | for(i=0; i<count(azEnv); i++){ |
| 1112 | 1117 | char *p; |
| 1113 | 1118 | if( (p = fossil_getenv(azEnv[i]))!=0 && p[0]!=0 ){ |
| @@ -1116,11 +1121,11 @@ | ||
| 1116 | 1121 | }else if( (z = P(azEnv[i]))!=0 && z[0]!=0 ){ |
| 1117 | 1122 | fprintf(out, "%s=%s\n", azEnv[i], z); |
| 1118 | 1123 | } |
| 1119 | 1124 | } |
| 1120 | 1125 | } |
| 1121 | - fclose(out); | |
| 1126 | + if( out!=stderr ) fclose(out); | |
| 1122 | 1127 | } |
| 1123 | 1128 | |
| 1124 | 1129 | /* |
| 1125 | 1130 | ** The following variable becomes true while processing a fatal error |
| 1126 | 1131 | ** or a panic. If additional "recursive-fatal" errors occur while |
| 1127 | 1132 |
| --- src/printf.c | |
| +++ src/printf.c | |
| @@ -1077,10 +1077,11 @@ | |
| 1077 | time_t now; |
| 1078 | FILE *out; |
| 1079 | const char *z; |
| 1080 | int i; |
| 1081 | int bDetail = 0; |
| 1082 | va_list ap; |
| 1083 | static const char *const azEnv[] = { "HTTP_HOST", "HTTP_REFERER", |
| 1084 | "HTTP_USER_AGENT", |
| 1085 | "PATH_INFO", "QUERY_STRING", "REMOTE_ADDR", "REQUEST_METHOD", |
| 1086 | "REQUEST_URI", "SCRIPT_NAME" }; |
| @@ -1098,16 +1099,20 @@ | |
| 1098 | pNow->tm_hour, pNow->tm_min, pNow->tm_sec); |
| 1099 | va_start(ap, zFormat); |
| 1100 | if( zFormat[0]=='X' ){ |
| 1101 | bDetail = 1; |
| 1102 | zFormat++; |
| 1103 | } |
| 1104 | vfprintf(out, zFormat, ap); |
| 1105 | fprintf(out, "\n"); |
| 1106 | va_end(ap); |
| 1107 | if( g.zPhase!=0 ) fprintf(out, "while in %s\n", g.zPhase); |
| 1108 | if( bDetail ){ |
| 1109 | cgi_print_all(1,3,out); |
| 1110 | }else{ |
| 1111 | for(i=0; i<count(azEnv); i++){ |
| 1112 | char *p; |
| 1113 | if( (p = fossil_getenv(azEnv[i]))!=0 && p[0]!=0 ){ |
| @@ -1116,11 +1121,11 @@ | |
| 1116 | }else if( (z = P(azEnv[i]))!=0 && z[0]!=0 ){ |
| 1117 | fprintf(out, "%s=%s\n", azEnv[i], z); |
| 1118 | } |
| 1119 | } |
| 1120 | } |
| 1121 | fclose(out); |
| 1122 | } |
| 1123 | |
| 1124 | /* |
| 1125 | ** The following variable becomes true while processing a fatal error |
| 1126 | ** or a panic. If additional "recursive-fatal" errors occur while |
| 1127 |
| --- src/printf.c | |
| +++ src/printf.c | |
| @@ -1077,10 +1077,11 @@ | |
| 1077 | time_t now; |
| 1078 | FILE *out; |
| 1079 | const char *z; |
| 1080 | int i; |
| 1081 | int bDetail = 0; |
| 1082 | int bBrief = 0; |
| 1083 | va_list ap; |
| 1084 | static const char *const azEnv[] = { "HTTP_HOST", "HTTP_REFERER", |
| 1085 | "HTTP_USER_AGENT", |
| 1086 | "PATH_INFO", "QUERY_STRING", "REMOTE_ADDR", "REQUEST_METHOD", |
| 1087 | "REQUEST_URI", "SCRIPT_NAME" }; |
| @@ -1098,16 +1099,20 @@ | |
| 1099 | pNow->tm_hour, pNow->tm_min, pNow->tm_sec); |
| 1100 | va_start(ap, zFormat); |
| 1101 | if( zFormat[0]=='X' ){ |
| 1102 | bDetail = 1; |
| 1103 | zFormat++; |
| 1104 | }else if( strncmp(zFormat,"SMTP:",5)==0 ){ |
| 1105 | bBrief = 1; |
| 1106 | } |
| 1107 | vfprintf(out, zFormat, ap); |
| 1108 | fprintf(out, " (pid %d)\n", (int)getpid()); |
| 1109 | va_end(ap); |
| 1110 | if( g.zPhase!=0 ) fprintf(out, "while in %s\n", g.zPhase); |
| 1111 | if( bBrief ){ |
| 1112 | /* Say nothing more */ |
| 1113 | }else if( bDetail ){ |
| 1114 | cgi_print_all(1,3,out); |
| 1115 | }else{ |
| 1116 | for(i=0; i<count(azEnv); i++){ |
| 1117 | char *p; |
| 1118 | if( (p = fossil_getenv(azEnv[i]))!=0 && p[0]!=0 ){ |
| @@ -1116,11 +1121,11 @@ | |
| 1121 | }else if( (z = P(azEnv[i]))!=0 && z[0]!=0 ){ |
| 1122 | fprintf(out, "%s=%s\n", azEnv[i], z); |
| 1123 | } |
| 1124 | } |
| 1125 | } |
| 1126 | if( out!=stderr ) fclose(out); |
| 1127 | } |
| 1128 | |
| 1129 | /* |
| 1130 | ** The following variable becomes true while processing a fatal error |
| 1131 | ** or a panic. If additional "recursive-fatal" errors occur while |
| 1132 |
+113
-23
| --- src/repolist.c | ||
| +++ src/repolist.c | ||
| @@ -31,10 +31,11 @@ | ||
| 31 | 31 | int isValid; /* True if zRepoName is a valid Fossil repository */ |
| 32 | 32 | int isRepolistSkin; /* 1 or 2 if this repository wants to be the skin |
| 33 | 33 | ** for the repository list. 2 means do use this |
| 34 | 34 | ** repository but do not display it in the list. */ |
| 35 | 35 | char *zProjName; /* Project Name. Memory from fossil_malloc() */ |
| 36 | + char *zProjDesc; /* Project Description. Memory from fossil_malloc() */ | |
| 36 | 37 | char *zLoginGroup; /* Name of login group, or NULL. Malloced() */ |
| 37 | 38 | double rMTime; /* Last update. Julian day number */ |
| 38 | 39 | }; |
| 39 | 40 | #endif |
| 40 | 41 | |
| @@ -49,10 +50,11 @@ | ||
| 49 | 50 | int rc; |
| 50 | 51 | |
| 51 | 52 | pRepo->isRepolistSkin = 0; |
| 52 | 53 | pRepo->isValid = 0; |
| 53 | 54 | pRepo->zProjName = 0; |
| 55 | + pRepo->zProjDesc = 0; | |
| 54 | 56 | pRepo->zLoginGroup = 0; |
| 55 | 57 | pRepo->rMTime = 0.0; |
| 56 | 58 | |
| 57 | 59 | g.dbIgnoreErrors++; |
| 58 | 60 | rc = sqlite3_open_v2(pRepo->zRepoName, &db, SQLITE_OPEN_READWRITE, 0); |
| @@ -71,10 +73,19 @@ | ||
| 71 | 73 | -1, &pStmt, 0); |
| 72 | 74 | if( rc ) goto finish_repo_list; |
| 73 | 75 | if( sqlite3_step(pStmt)==SQLITE_ROW ){ |
| 74 | 76 | pRepo->zProjName = fossil_strdup((char*)sqlite3_column_text(pStmt,0)); |
| 75 | 77 | } |
| 78 | + sqlite3_finalize(pStmt); | |
| 79 | + if( rc ) goto finish_repo_list; | |
| 80 | + rc = sqlite3_prepare_v2(db, "SELECT value FROM config" | |
| 81 | + " WHERE name='project-description'", | |
| 82 | + -1, &pStmt, 0); | |
| 83 | + if( rc ) goto finish_repo_list; | |
| 84 | + if( sqlite3_step(pStmt)==SQLITE_ROW ){ | |
| 85 | + pRepo->zProjDesc = fossil_strdup((char*)sqlite3_column_text(pStmt,0)); | |
| 86 | + } | |
| 76 | 87 | sqlite3_finalize(pStmt); |
| 77 | 88 | rc = sqlite3_prepare_v2(db, "SELECT value FROM config" |
| 78 | 89 | " WHERE name='login-group-name'", |
| 79 | 90 | -1, &pStmt, 0); |
| 80 | 91 | if( rc==SQLITE_OK && sqlite3_step(pStmt)==SQLITE_ROW ){ |
| @@ -89,10 +100,25 @@ | ||
| 89 | 100 | sqlite3_finalize(pStmt); |
| 90 | 101 | finish_repo_list: |
| 91 | 102 | g.dbIgnoreErrors--; |
| 92 | 103 | sqlite3_close(db); |
| 93 | 104 | } |
| 105 | + | |
| 106 | +/* | |
| 107 | +** SETTING: show-repolist-desc boolean default=off | |
| 108 | +** | |
| 109 | +** If the value of this setting is "1" globally, then the repository-list | |
| 110 | +** page will show the description of each repository. This setting only | |
| 111 | +** has effect when it is in the global setting database. | |
| 112 | +*/ | |
| 113 | +/* | |
| 114 | +** SETTING: show-repolist-lg boolean default=off | |
| 115 | +** | |
| 116 | +** If the value of this setting is "1" globally, then the repository-list | |
| 117 | +** page will show the login-group for each repository. This setting only | |
| 118 | +** has effect when it is in the global setting database. | |
| 119 | +*/ | |
| 94 | 120 | |
| 95 | 121 | /* |
| 96 | 122 | ** Generate a web-page that lists all repositories located under the |
| 97 | 123 | ** g.zRepositoryName directory and return non-zero. |
| 98 | 124 | ** |
| @@ -116,12 +142,27 @@ | ||
| 116 | 142 | ** False if a directory scan of base for repos */ |
| 117 | 143 | Blob html; /* Html for the body of the repository list */ |
| 118 | 144 | char *zSkinRepo = 0; /* Name of the repository database used for skins */ |
| 119 | 145 | char *zSkinUrl = 0; /* URL for the skin database */ |
| 120 | 146 | int quickfilter = 0; /* Is quickfilter is enabled? */ |
| 147 | + const char *zShow; /* Value of FOSSIL_REPOLIST_SHOW environment variable */ | |
| 148 | + int bShowDesc = 0; /* True to show the description column */ | |
| 149 | + int bShowLg = 0; /* True to show the login-group column */ | |
| 121 | 150 | |
| 122 | 151 | assert( g.db==0 ); |
| 152 | + zShow = P("FOSSIL_REPOLIST_SHOW"); | |
| 153 | + if( zShow ){ | |
| 154 | + bShowDesc = strstr(zShow,"description")!=0; | |
| 155 | + bShowLg = strstr(zShow,"login-group")!=0; | |
| 156 | + }else if( db_open_config(1, 1) | |
| 157 | + && db_table_exists("configdb", "global_config") | |
| 158 | + ){ | |
| 159 | + bShowDesc = db_int(bShowDesc, "SELECT value FROM global_config" | |
| 160 | + " WHERE name='show-repolist-desc'"); | |
| 161 | + bShowLg = db_int(bShowLg, "SELECT value FROM global_config" | |
| 162 | + " WHERE name='show-repolist-lg'"); | |
| 163 | + } | |
| 123 | 164 | blob_init(&html, 0, 0); |
| 124 | 165 | if( fossil_strcmp(g.zRepositoryName,"/")==0 && !g.fJail ){ |
| 125 | 166 | /* For the special case of the "repository directory" being "/", |
| 126 | 167 | ** show all of the repositories named in the ~/.fossil database. |
| 127 | 168 | ** |
| @@ -128,11 +169,10 @@ | ||
| 128 | 169 | ** On unix systems, then entries are of the form "repo:/home/..." |
| 129 | 170 | ** and on Windows systems they are like on unix, starting with a "/" |
| 130 | 171 | ** or they can begin with a drive letter: "repo:C:/Users/...". In either |
| 131 | 172 | ** case, we want returned path to omit any initial "/". |
| 132 | 173 | */ |
| 133 | - db_open_config(1, 0); | |
| 134 | 174 | db_multi_exec( |
| 135 | 175 | "CREATE TEMP VIEW sfile AS" |
| 136 | 176 | " SELECT ltrim(substr(name,6),'/') AS 'pathname' FROM global_config" |
| 137 | 177 | " WHERE name GLOB 'repo:*'" |
| 138 | 178 | ); |
| @@ -140,10 +180,12 @@ | ||
| 140 | 180 | }else{ |
| 141 | 181 | /* The default case: All repositories under the g.zRepositoryName |
| 142 | 182 | ** directory. |
| 143 | 183 | */ |
| 144 | 184 | blob_init(&base, g.zRepositoryName, -1); |
| 185 | + db_close(0); | |
| 186 | + assert( g.db==0 ); | |
| 145 | 187 | sqlite3_open(":memory:", &g.db); |
| 146 | 188 | db_multi_exec("CREATE TABLE sfile(pathname TEXT);"); |
| 147 | 189 | db_multi_exec("CREATE TABLE vfile(pathname);"); |
| 148 | 190 | vfile_scan(&base, blob_size(&base), 0, 0, 0, ExtFILE); |
| 149 | 191 | db_multi_exec("DELETE FROM sfile WHERE pathname NOT GLOB '*[^/].fossil'" |
| @@ -161,18 +203,50 @@ | ||
| 161 | 203 | g.localOpen = 0; |
| 162 | 204 | return 0; |
| 163 | 205 | }else{ |
| 164 | 206 | Stmt q; |
| 165 | 207 | double rNow; |
| 166 | - blob_append_sql(&html, | |
| 208 | + char zType[16]; /* Column type letters for class "sortable" */ | |
| 209 | + int nType; | |
| 210 | + zType[0] = 't'; /* Repo name */ | |
| 211 | + zType[1] = 'x'; /* Space between repo-name and project-name */ | |
| 212 | + zType[2] = 't'; /* Project name */ | |
| 213 | + nType = 3; | |
| 214 | + if( bShowDesc ){ | |
| 215 | + zType[nType++] = 'x'; /* Space between name and description */ | |
| 216 | + zType[nType++] = 't'; /* Project description */ | |
| 217 | + } | |
| 218 | + zType[nType++] = 'x'; /* space before age */ | |
| 219 | + zType[nType++] = 'k'; /* Project age */ | |
| 220 | + if( bShowLg ){ | |
| 221 | + zType[nType++] = 'x'; /* space before login-group */ | |
| 222 | + zType[nType++] = 't'; /* Login Group */ | |
| 223 | + } | |
| 224 | + zType[nType] = 0; | |
| 225 | + blob_appendf(&html, | |
| 167 | 226 | "<table border='0' class='sortable filterlist' data-init-sort='1'" |
| 168 | - " data-column-types='txtxkxt'><thead>\n" | |
| 169 | - "<tr><th>Filename<th width='20'>" | |
| 170 | - "<th>Project Name<th width='20'>" | |
| 171 | - "<th>Last Modified<th width='20'>" | |
| 172 | - "<th>Login Group</tr>\n" | |
| 173 | - "</thead><tbody>\n"); | |
| 227 | + " data-column-types='%s' cellspacing='0' cellpadding='0'><thead>\n" | |
| 228 | + "<tr><th>Filename</th><th> </th>\n" | |
| 229 | + "<th%s><nobr>Project Name</nobr></th>\n", | |
| 230 | + zType, (bShowDesc ? " width='25%'" : "")); | |
| 231 | + if( bShowDesc ){ | |
| 232 | + blob_appendf(&html, | |
| 233 | + "<th> </th>\n" | |
| 234 | + "<th width='25%%'><nobr>Project Description</nobr></th>\n" | |
| 235 | + ); | |
| 236 | + } | |
| 237 | + blob_appendf(&html, | |
| 238 | + "<th> </th>" | |
| 239 | + "<th><nobr>Last Modified</nobr></th>\n" | |
| 240 | + ); | |
| 241 | + if( bShowLg ){ | |
| 242 | + blob_appendf(&html, | |
| 243 | + "<th> </th>" | |
| 244 | + "<th><nobr>Login Group</nobr></th></tr>\n" | |
| 245 | + ); | |
| 246 | + } | |
| 247 | + blob_appendf(&html,"</thead><tbody>\n"); | |
| 174 | 248 | db_prepare(&q, "SELECT pathname" |
| 175 | 249 | " FROM sfile ORDER BY pathname COLLATE nocase;"); |
| 176 | 250 | rNow = db_double(0, "SELECT julianday('now')"); |
| 177 | 251 | while( db_step(&q)==SQLITE_ROW ){ |
| 178 | 252 | const char *zName = db_column_text(&q, 0); |
| @@ -231,21 +305,21 @@ | ||
| 231 | 305 | if( x.rMTime==0.0 ){ |
| 232 | 306 | /* This repository has no entry in the "event" table. |
| 233 | 307 | ** Its age will still be maximum, so data-sortkey will work. */ |
| 234 | 308 | zAge = mprintf("unknown"); |
| 235 | 309 | } |
| 236 | - blob_append_sql(&html, "<tr><td valign='top'>"); | |
| 310 | + blob_appendf(&html, "<tr><td valign='top'><nobr>"); | |
| 237 | 311 | if( !file_ends_with_repository_extension(zName,0) ){ |
| 238 | 312 | /* The "fossil server DIRECTORY" and "fossil ui DIRECTORY" commands |
| 239 | 313 | ** do not work for repositories whose names do not end in ".fossil". |
| 240 | 314 | ** So do not hyperlink those cases. */ |
| 241 | - blob_append_sql(&html,"%h",zName); | |
| 315 | + blob_appendf(&html,"%h",zName); | |
| 242 | 316 | } else if( sqlite3_strglob("*/.*", zName)==0 ){ |
| 243 | 317 | /* Do not show hyperlinks for hidden repos */ |
| 244 | - blob_append_sql(&html, "%h (hidden)", zName); | |
| 318 | + blob_appendf(&html, "%h (hidden)", zName); | |
| 245 | 319 | } else if( allRepo && sqlite3_strglob("[a-zA-Z]:/?*", zName)!=0 ){ |
| 246 | - blob_append_sql(&html, | |
| 320 | + blob_appendf(&html, | |
| 247 | 321 | "<a href='%R/%T/home' target='_blank'>/%h</a>\n", |
| 248 | 322 | zUrl, zName); |
| 249 | 323 | }else if( file_ends_with_repository_extension(zName,1) ){ |
| 250 | 324 | /* As described in |
| 251 | 325 | ** https://fossil-scm.org/forum/info/f50f647c97c72fc1: if |
| @@ -262,44 +336,60 @@ | ||
| 262 | 336 | , zDirPart |
| 263 | 337 | #if USE_SEE |
| 264 | 338 | , zDirPart |
| 265 | 339 | #endif |
| 266 | 340 | ) ){ |
| 267 | - blob_append_sql(&html, | |
| 341 | + blob_appendf(&html, | |
| 268 | 342 | "<s>%h</s> (directory/repo name collision)\n", |
| 269 | 343 | zName); |
| 270 | 344 | }else{ |
| 271 | - blob_append_sql(&html, | |
| 345 | + blob_appendf(&html, | |
| 272 | 346 | "<a href='%R/%T/home' target='_blank'>%h</a>\n", |
| 273 | 347 | zUrl, zName); |
| 274 | 348 | } |
| 275 | 349 | fossil_free(zDirPart); |
| 276 | 350 | }else{ |
| 277 | - blob_append_sql(&html, | |
| 351 | + blob_appendf(&html, | |
| 278 | 352 | "<a href='%R/%T/home' target='_blank'>%h</a>\n", |
| 279 | 353 | zUrl, zName); |
| 280 | 354 | } |
| 355 | + blob_appendf(&html,"</nobr></td>\n"); | |
| 281 | 356 | if( x.zProjName ){ |
| 282 | - blob_append_sql(&html, "<td></td><td>%h</td>\n", x.zProjName); | |
| 357 | + blob_appendf(&html, "<td> </td><td valign='top'>%h</td>\n", | |
| 358 | + x.zProjName); | |
| 283 | 359 | fossil_free(x.zProjName); |
| 284 | 360 | }else{ |
| 285 | - blob_append_sql(&html, "<td></td><td></td>\n"); | |
| 361 | + blob_appendf(&html, "<td> </td><td></td>\n"); | |
| 362 | + } | |
| 363 | + if( !bShowDesc ){ | |
| 364 | + /* Do nothing */ | |
| 365 | + }else if( x.zProjDesc ){ | |
| 366 | + blob_appendf(&html, "<td> </td><td valign='top'>%h</td>\n", | |
| 367 | + x.zProjDesc); | |
| 368 | + fossil_free(x.zProjDesc); | |
| 369 | + }else{ | |
| 370 | + blob_appendf(&html, "<td> </td><td></td>\n"); | |
| 286 | 371 | } |
| 287 | - blob_append_sql(&html, | |
| 288 | - "<td></td><td data-sortkey='%08x'>%h</td>\n", | |
| 372 | + blob_appendf(&html, | |
| 373 | + "<td> </td><td data-sortkey='%08x' align='center' valign='top'>" | |
| 374 | + "<nobr>%h</nobr></td>\n", | |
| 289 | 375 | (int)iAge, zAge); |
| 290 | 376 | fossil_free(zAge); |
| 291 | - if( x.zLoginGroup ){ | |
| 292 | - blob_append_sql(&html, "<td></td><td>%h</td></tr>\n", x.zLoginGroup); | |
| 377 | + if( !bShowLg ){ | |
| 378 | + blob_appendf(&html, "</tr>\n"); | |
| 379 | + }else if( x.zLoginGroup ){ | |
| 380 | + blob_appendf(&html, "<td> </td><td valign='top'>" | |
| 381 | + "<nobr>%h</nobr></td></tr>\n", | |
| 382 | + x.zLoginGroup); | |
| 293 | 383 | fossil_free(x.zLoginGroup); |
| 294 | 384 | }else{ |
| 295 | - blob_append_sql(&html, "<td></td><td></td></tr>\n"); | |
| 385 | + blob_appendf(&html, "<td> </td><td></td></tr>\n"); | |
| 296 | 386 | } |
| 297 | 387 | sqlite3_free(zUrl); |
| 298 | 388 | } |
| 299 | 389 | db_finalize(&q); |
| 300 | - blob_append_sql(&html,"</tbody></table>\n"); | |
| 390 | + blob_appendf(&html,"</tbody></table>\n"); | |
| 301 | 391 | } |
| 302 | 392 | if( zSkinRepo ){ |
| 303 | 393 | char *zNewBase = mprintf("%s/%s", g.zBaseURL, zSkinUrl); |
| 304 | 394 | g.zBaseURL = 0; |
| 305 | 395 | set_base_url(zNewBase); |
| 306 | 396 |
| --- src/repolist.c | |
| +++ src/repolist.c | |
| @@ -31,10 +31,11 @@ | |
| 31 | int isValid; /* True if zRepoName is a valid Fossil repository */ |
| 32 | int isRepolistSkin; /* 1 or 2 if this repository wants to be the skin |
| 33 | ** for the repository list. 2 means do use this |
| 34 | ** repository but do not display it in the list. */ |
| 35 | char *zProjName; /* Project Name. Memory from fossil_malloc() */ |
| 36 | char *zLoginGroup; /* Name of login group, or NULL. Malloced() */ |
| 37 | double rMTime; /* Last update. Julian day number */ |
| 38 | }; |
| 39 | #endif |
| 40 | |
| @@ -49,10 +50,11 @@ | |
| 49 | int rc; |
| 50 | |
| 51 | pRepo->isRepolistSkin = 0; |
| 52 | pRepo->isValid = 0; |
| 53 | pRepo->zProjName = 0; |
| 54 | pRepo->zLoginGroup = 0; |
| 55 | pRepo->rMTime = 0.0; |
| 56 | |
| 57 | g.dbIgnoreErrors++; |
| 58 | rc = sqlite3_open_v2(pRepo->zRepoName, &db, SQLITE_OPEN_READWRITE, 0); |
| @@ -71,10 +73,19 @@ | |
| 71 | -1, &pStmt, 0); |
| 72 | if( rc ) goto finish_repo_list; |
| 73 | if( sqlite3_step(pStmt)==SQLITE_ROW ){ |
| 74 | pRepo->zProjName = fossil_strdup((char*)sqlite3_column_text(pStmt,0)); |
| 75 | } |
| 76 | sqlite3_finalize(pStmt); |
| 77 | rc = sqlite3_prepare_v2(db, "SELECT value FROM config" |
| 78 | " WHERE name='login-group-name'", |
| 79 | -1, &pStmt, 0); |
| 80 | if( rc==SQLITE_OK && sqlite3_step(pStmt)==SQLITE_ROW ){ |
| @@ -89,10 +100,25 @@ | |
| 89 | sqlite3_finalize(pStmt); |
| 90 | finish_repo_list: |
| 91 | g.dbIgnoreErrors--; |
| 92 | sqlite3_close(db); |
| 93 | } |
| 94 | |
| 95 | /* |
| 96 | ** Generate a web-page that lists all repositories located under the |
| 97 | ** g.zRepositoryName directory and return non-zero. |
| 98 | ** |
| @@ -116,12 +142,27 @@ | |
| 116 | ** False if a directory scan of base for repos */ |
| 117 | Blob html; /* Html for the body of the repository list */ |
| 118 | char *zSkinRepo = 0; /* Name of the repository database used for skins */ |
| 119 | char *zSkinUrl = 0; /* URL for the skin database */ |
| 120 | int quickfilter = 0; /* Is quickfilter is enabled? */ |
| 121 | |
| 122 | assert( g.db==0 ); |
| 123 | blob_init(&html, 0, 0); |
| 124 | if( fossil_strcmp(g.zRepositoryName,"/")==0 && !g.fJail ){ |
| 125 | /* For the special case of the "repository directory" being "/", |
| 126 | ** show all of the repositories named in the ~/.fossil database. |
| 127 | ** |
| @@ -128,11 +169,10 @@ | |
| 128 | ** On unix systems, then entries are of the form "repo:/home/..." |
| 129 | ** and on Windows systems they are like on unix, starting with a "/" |
| 130 | ** or they can begin with a drive letter: "repo:C:/Users/...". In either |
| 131 | ** case, we want returned path to omit any initial "/". |
| 132 | */ |
| 133 | db_open_config(1, 0); |
| 134 | db_multi_exec( |
| 135 | "CREATE TEMP VIEW sfile AS" |
| 136 | " SELECT ltrim(substr(name,6),'/') AS 'pathname' FROM global_config" |
| 137 | " WHERE name GLOB 'repo:*'" |
| 138 | ); |
| @@ -140,10 +180,12 @@ | |
| 140 | }else{ |
| 141 | /* The default case: All repositories under the g.zRepositoryName |
| 142 | ** directory. |
| 143 | */ |
| 144 | blob_init(&base, g.zRepositoryName, -1); |
| 145 | sqlite3_open(":memory:", &g.db); |
| 146 | db_multi_exec("CREATE TABLE sfile(pathname TEXT);"); |
| 147 | db_multi_exec("CREATE TABLE vfile(pathname);"); |
| 148 | vfile_scan(&base, blob_size(&base), 0, 0, 0, ExtFILE); |
| 149 | db_multi_exec("DELETE FROM sfile WHERE pathname NOT GLOB '*[^/].fossil'" |
| @@ -161,18 +203,50 @@ | |
| 161 | g.localOpen = 0; |
| 162 | return 0; |
| 163 | }else{ |
| 164 | Stmt q; |
| 165 | double rNow; |
| 166 | blob_append_sql(&html, |
| 167 | "<table border='0' class='sortable filterlist' data-init-sort='1'" |
| 168 | " data-column-types='txtxkxt'><thead>\n" |
| 169 | "<tr><th>Filename<th width='20'>" |
| 170 | "<th>Project Name<th width='20'>" |
| 171 | "<th>Last Modified<th width='20'>" |
| 172 | "<th>Login Group</tr>\n" |
| 173 | "</thead><tbody>\n"); |
| 174 | db_prepare(&q, "SELECT pathname" |
| 175 | " FROM sfile ORDER BY pathname COLLATE nocase;"); |
| 176 | rNow = db_double(0, "SELECT julianday('now')"); |
| 177 | while( db_step(&q)==SQLITE_ROW ){ |
| 178 | const char *zName = db_column_text(&q, 0); |
| @@ -231,21 +305,21 @@ | |
| 231 | if( x.rMTime==0.0 ){ |
| 232 | /* This repository has no entry in the "event" table. |
| 233 | ** Its age will still be maximum, so data-sortkey will work. */ |
| 234 | zAge = mprintf("unknown"); |
| 235 | } |
| 236 | blob_append_sql(&html, "<tr><td valign='top'>"); |
| 237 | if( !file_ends_with_repository_extension(zName,0) ){ |
| 238 | /* The "fossil server DIRECTORY" and "fossil ui DIRECTORY" commands |
| 239 | ** do not work for repositories whose names do not end in ".fossil". |
| 240 | ** So do not hyperlink those cases. */ |
| 241 | blob_append_sql(&html,"%h",zName); |
| 242 | } else if( sqlite3_strglob("*/.*", zName)==0 ){ |
| 243 | /* Do not show hyperlinks for hidden repos */ |
| 244 | blob_append_sql(&html, "%h (hidden)", zName); |
| 245 | } else if( allRepo && sqlite3_strglob("[a-zA-Z]:/?*", zName)!=0 ){ |
| 246 | blob_append_sql(&html, |
| 247 | "<a href='%R/%T/home' target='_blank'>/%h</a>\n", |
| 248 | zUrl, zName); |
| 249 | }else if( file_ends_with_repository_extension(zName,1) ){ |
| 250 | /* As described in |
| 251 | ** https://fossil-scm.org/forum/info/f50f647c97c72fc1: if |
| @@ -262,44 +336,60 @@ | |
| 262 | , zDirPart |
| 263 | #if USE_SEE |
| 264 | , zDirPart |
| 265 | #endif |
| 266 | ) ){ |
| 267 | blob_append_sql(&html, |
| 268 | "<s>%h</s> (directory/repo name collision)\n", |
| 269 | zName); |
| 270 | }else{ |
| 271 | blob_append_sql(&html, |
| 272 | "<a href='%R/%T/home' target='_blank'>%h</a>\n", |
| 273 | zUrl, zName); |
| 274 | } |
| 275 | fossil_free(zDirPart); |
| 276 | }else{ |
| 277 | blob_append_sql(&html, |
| 278 | "<a href='%R/%T/home' target='_blank'>%h</a>\n", |
| 279 | zUrl, zName); |
| 280 | } |
| 281 | if( x.zProjName ){ |
| 282 | blob_append_sql(&html, "<td></td><td>%h</td>\n", x.zProjName); |
| 283 | fossil_free(x.zProjName); |
| 284 | }else{ |
| 285 | blob_append_sql(&html, "<td></td><td></td>\n"); |
| 286 | } |
| 287 | blob_append_sql(&html, |
| 288 | "<td></td><td data-sortkey='%08x'>%h</td>\n", |
| 289 | (int)iAge, zAge); |
| 290 | fossil_free(zAge); |
| 291 | if( x.zLoginGroup ){ |
| 292 | blob_append_sql(&html, "<td></td><td>%h</td></tr>\n", x.zLoginGroup); |
| 293 | fossil_free(x.zLoginGroup); |
| 294 | }else{ |
| 295 | blob_append_sql(&html, "<td></td><td></td></tr>\n"); |
| 296 | } |
| 297 | sqlite3_free(zUrl); |
| 298 | } |
| 299 | db_finalize(&q); |
| 300 | blob_append_sql(&html,"</tbody></table>\n"); |
| 301 | } |
| 302 | if( zSkinRepo ){ |
| 303 | char *zNewBase = mprintf("%s/%s", g.zBaseURL, zSkinUrl); |
| 304 | g.zBaseURL = 0; |
| 305 | set_base_url(zNewBase); |
| 306 |
| --- src/repolist.c | |
| +++ src/repolist.c | |
| @@ -31,10 +31,11 @@ | |
| 31 | int isValid; /* True if zRepoName is a valid Fossil repository */ |
| 32 | int isRepolistSkin; /* 1 or 2 if this repository wants to be the skin |
| 33 | ** for the repository list. 2 means do use this |
| 34 | ** repository but do not display it in the list. */ |
| 35 | char *zProjName; /* Project Name. Memory from fossil_malloc() */ |
| 36 | char *zProjDesc; /* Project Description. Memory from fossil_malloc() */ |
| 37 | char *zLoginGroup; /* Name of login group, or NULL. Malloced() */ |
| 38 | double rMTime; /* Last update. Julian day number */ |
| 39 | }; |
| 40 | #endif |
| 41 | |
| @@ -49,10 +50,11 @@ | |
| 50 | int rc; |
| 51 | |
| 52 | pRepo->isRepolistSkin = 0; |
| 53 | pRepo->isValid = 0; |
| 54 | pRepo->zProjName = 0; |
| 55 | pRepo->zProjDesc = 0; |
| 56 | pRepo->zLoginGroup = 0; |
| 57 | pRepo->rMTime = 0.0; |
| 58 | |
| 59 | g.dbIgnoreErrors++; |
| 60 | rc = sqlite3_open_v2(pRepo->zRepoName, &db, SQLITE_OPEN_READWRITE, 0); |
| @@ -71,10 +73,19 @@ | |
| 73 | -1, &pStmt, 0); |
| 74 | if( rc ) goto finish_repo_list; |
| 75 | if( sqlite3_step(pStmt)==SQLITE_ROW ){ |
| 76 | pRepo->zProjName = fossil_strdup((char*)sqlite3_column_text(pStmt,0)); |
| 77 | } |
| 78 | sqlite3_finalize(pStmt); |
| 79 | if( rc ) goto finish_repo_list; |
| 80 | rc = sqlite3_prepare_v2(db, "SELECT value FROM config" |
| 81 | " WHERE name='project-description'", |
| 82 | -1, &pStmt, 0); |
| 83 | if( rc ) goto finish_repo_list; |
| 84 | if( sqlite3_step(pStmt)==SQLITE_ROW ){ |
| 85 | pRepo->zProjDesc = fossil_strdup((char*)sqlite3_column_text(pStmt,0)); |
| 86 | } |
| 87 | sqlite3_finalize(pStmt); |
| 88 | rc = sqlite3_prepare_v2(db, "SELECT value FROM config" |
| 89 | " WHERE name='login-group-name'", |
| 90 | -1, &pStmt, 0); |
| 91 | if( rc==SQLITE_OK && sqlite3_step(pStmt)==SQLITE_ROW ){ |
| @@ -89,10 +100,25 @@ | |
| 100 | sqlite3_finalize(pStmt); |
| 101 | finish_repo_list: |
| 102 | g.dbIgnoreErrors--; |
| 103 | sqlite3_close(db); |
| 104 | } |
| 105 | |
| 106 | /* |
| 107 | ** SETTING: show-repolist-desc boolean default=off |
| 108 | ** |
| 109 | ** If the value of this setting is "1" globally, then the repository-list |
| 110 | ** page will show the description of each repository. This setting only |
| 111 | ** has effect when it is in the global setting database. |
| 112 | */ |
| 113 | /* |
| 114 | ** SETTING: show-repolist-lg boolean default=off |
| 115 | ** |
| 116 | ** If the value of this setting is "1" globally, then the repository-list |
| 117 | ** page will show the login-group for each repository. This setting only |
| 118 | ** has effect when it is in the global setting database. |
| 119 | */ |
| 120 | |
| 121 | /* |
| 122 | ** Generate a web-page that lists all repositories located under the |
| 123 | ** g.zRepositoryName directory and return non-zero. |
| 124 | ** |
| @@ -116,12 +142,27 @@ | |
| 142 | ** False if a directory scan of base for repos */ |
| 143 | Blob html; /* Html for the body of the repository list */ |
| 144 | char *zSkinRepo = 0; /* Name of the repository database used for skins */ |
| 145 | char *zSkinUrl = 0; /* URL for the skin database */ |
| 146 | int quickfilter = 0; /* Is quickfilter is enabled? */ |
| 147 | const char *zShow; /* Value of FOSSIL_REPOLIST_SHOW environment variable */ |
| 148 | int bShowDesc = 0; /* True to show the description column */ |
| 149 | int bShowLg = 0; /* True to show the login-group column */ |
| 150 | |
| 151 | assert( g.db==0 ); |
| 152 | zShow = P("FOSSIL_REPOLIST_SHOW"); |
| 153 | if( zShow ){ |
| 154 | bShowDesc = strstr(zShow,"description")!=0; |
| 155 | bShowLg = strstr(zShow,"login-group")!=0; |
| 156 | }else if( db_open_config(1, 1) |
| 157 | && db_table_exists("configdb", "global_config") |
| 158 | ){ |
| 159 | bShowDesc = db_int(bShowDesc, "SELECT value FROM global_config" |
| 160 | " WHERE name='show-repolist-desc'"); |
| 161 | bShowLg = db_int(bShowLg, "SELECT value FROM global_config" |
| 162 | " WHERE name='show-repolist-lg'"); |
| 163 | } |
| 164 | blob_init(&html, 0, 0); |
| 165 | if( fossil_strcmp(g.zRepositoryName,"/")==0 && !g.fJail ){ |
| 166 | /* For the special case of the "repository directory" being "/", |
| 167 | ** show all of the repositories named in the ~/.fossil database. |
| 168 | ** |
| @@ -128,11 +169,10 @@ | |
| 169 | ** On unix systems, then entries are of the form "repo:/home/..." |
| 170 | ** and on Windows systems they are like on unix, starting with a "/" |
| 171 | ** or they can begin with a drive letter: "repo:C:/Users/...". In either |
| 172 | ** case, we want returned path to omit any initial "/". |
| 173 | */ |
| 174 | db_multi_exec( |
| 175 | "CREATE TEMP VIEW sfile AS" |
| 176 | " SELECT ltrim(substr(name,6),'/') AS 'pathname' FROM global_config" |
| 177 | " WHERE name GLOB 'repo:*'" |
| 178 | ); |
| @@ -140,10 +180,12 @@ | |
| 180 | }else{ |
| 181 | /* The default case: All repositories under the g.zRepositoryName |
| 182 | ** directory. |
| 183 | */ |
| 184 | blob_init(&base, g.zRepositoryName, -1); |
| 185 | db_close(0); |
| 186 | assert( g.db==0 ); |
| 187 | sqlite3_open(":memory:", &g.db); |
| 188 | db_multi_exec("CREATE TABLE sfile(pathname TEXT);"); |
| 189 | db_multi_exec("CREATE TABLE vfile(pathname);"); |
| 190 | vfile_scan(&base, blob_size(&base), 0, 0, 0, ExtFILE); |
| 191 | db_multi_exec("DELETE FROM sfile WHERE pathname NOT GLOB '*[^/].fossil'" |
| @@ -161,18 +203,50 @@ | |
| 203 | g.localOpen = 0; |
| 204 | return 0; |
| 205 | }else{ |
| 206 | Stmt q; |
| 207 | double rNow; |
| 208 | char zType[16]; /* Column type letters for class "sortable" */ |
| 209 | int nType; |
| 210 | zType[0] = 't'; /* Repo name */ |
| 211 | zType[1] = 'x'; /* Space between repo-name and project-name */ |
| 212 | zType[2] = 't'; /* Project name */ |
| 213 | nType = 3; |
| 214 | if( bShowDesc ){ |
| 215 | zType[nType++] = 'x'; /* Space between name and description */ |
| 216 | zType[nType++] = 't'; /* Project description */ |
| 217 | } |
| 218 | zType[nType++] = 'x'; /* space before age */ |
| 219 | zType[nType++] = 'k'; /* Project age */ |
| 220 | if( bShowLg ){ |
| 221 | zType[nType++] = 'x'; /* space before login-group */ |
| 222 | zType[nType++] = 't'; /* Login Group */ |
| 223 | } |
| 224 | zType[nType] = 0; |
| 225 | blob_appendf(&html, |
| 226 | "<table border='0' class='sortable filterlist' data-init-sort='1'" |
| 227 | " data-column-types='%s' cellspacing='0' cellpadding='0'><thead>\n" |
| 228 | "<tr><th>Filename</th><th> </th>\n" |
| 229 | "<th%s><nobr>Project Name</nobr></th>\n", |
| 230 | zType, (bShowDesc ? " width='25%'" : "")); |
| 231 | if( bShowDesc ){ |
| 232 | blob_appendf(&html, |
| 233 | "<th> </th>\n" |
| 234 | "<th width='25%%'><nobr>Project Description</nobr></th>\n" |
| 235 | ); |
| 236 | } |
| 237 | blob_appendf(&html, |
| 238 | "<th> </th>" |
| 239 | "<th><nobr>Last Modified</nobr></th>\n" |
| 240 | ); |
| 241 | if( bShowLg ){ |
| 242 | blob_appendf(&html, |
| 243 | "<th> </th>" |
| 244 | "<th><nobr>Login Group</nobr></th></tr>\n" |
| 245 | ); |
| 246 | } |
| 247 | blob_appendf(&html,"</thead><tbody>\n"); |
| 248 | db_prepare(&q, "SELECT pathname" |
| 249 | " FROM sfile ORDER BY pathname COLLATE nocase;"); |
| 250 | rNow = db_double(0, "SELECT julianday('now')"); |
| 251 | while( db_step(&q)==SQLITE_ROW ){ |
| 252 | const char *zName = db_column_text(&q, 0); |
| @@ -231,21 +305,21 @@ | |
| 305 | if( x.rMTime==0.0 ){ |
| 306 | /* This repository has no entry in the "event" table. |
| 307 | ** Its age will still be maximum, so data-sortkey will work. */ |
| 308 | zAge = mprintf("unknown"); |
| 309 | } |
| 310 | blob_appendf(&html, "<tr><td valign='top'><nobr>"); |
| 311 | if( !file_ends_with_repository_extension(zName,0) ){ |
| 312 | /* The "fossil server DIRECTORY" and "fossil ui DIRECTORY" commands |
| 313 | ** do not work for repositories whose names do not end in ".fossil". |
| 314 | ** So do not hyperlink those cases. */ |
| 315 | blob_appendf(&html,"%h",zName); |
| 316 | } else if( sqlite3_strglob("*/.*", zName)==0 ){ |
| 317 | /* Do not show hyperlinks for hidden repos */ |
| 318 | blob_appendf(&html, "%h (hidden)", zName); |
| 319 | } else if( allRepo && sqlite3_strglob("[a-zA-Z]:/?*", zName)!=0 ){ |
| 320 | blob_appendf(&html, |
| 321 | "<a href='%R/%T/home' target='_blank'>/%h</a>\n", |
| 322 | zUrl, zName); |
| 323 | }else if( file_ends_with_repository_extension(zName,1) ){ |
| 324 | /* As described in |
| 325 | ** https://fossil-scm.org/forum/info/f50f647c97c72fc1: if |
| @@ -262,44 +336,60 @@ | |
| 336 | , zDirPart |
| 337 | #if USE_SEE |
| 338 | , zDirPart |
| 339 | #endif |
| 340 | ) ){ |
| 341 | blob_appendf(&html, |
| 342 | "<s>%h</s> (directory/repo name collision)\n", |
| 343 | zName); |
| 344 | }else{ |
| 345 | blob_appendf(&html, |
| 346 | "<a href='%R/%T/home' target='_blank'>%h</a>\n", |
| 347 | zUrl, zName); |
| 348 | } |
| 349 | fossil_free(zDirPart); |
| 350 | }else{ |
| 351 | blob_appendf(&html, |
| 352 | "<a href='%R/%T/home' target='_blank'>%h</a>\n", |
| 353 | zUrl, zName); |
| 354 | } |
| 355 | blob_appendf(&html,"</nobr></td>\n"); |
| 356 | if( x.zProjName ){ |
| 357 | blob_appendf(&html, "<td> </td><td valign='top'>%h</td>\n", |
| 358 | x.zProjName); |
| 359 | fossil_free(x.zProjName); |
| 360 | }else{ |
| 361 | blob_appendf(&html, "<td> </td><td></td>\n"); |
| 362 | } |
| 363 | if( !bShowDesc ){ |
| 364 | /* Do nothing */ |
| 365 | }else if( x.zProjDesc ){ |
| 366 | blob_appendf(&html, "<td> </td><td valign='top'>%h</td>\n", |
| 367 | x.zProjDesc); |
| 368 | fossil_free(x.zProjDesc); |
| 369 | }else{ |
| 370 | blob_appendf(&html, "<td> </td><td></td>\n"); |
| 371 | } |
| 372 | blob_appendf(&html, |
| 373 | "<td> </td><td data-sortkey='%08x' align='center' valign='top'>" |
| 374 | "<nobr>%h</nobr></td>\n", |
| 375 | (int)iAge, zAge); |
| 376 | fossil_free(zAge); |
| 377 | if( !bShowLg ){ |
| 378 | blob_appendf(&html, "</tr>\n"); |
| 379 | }else if( x.zLoginGroup ){ |
| 380 | blob_appendf(&html, "<td> </td><td valign='top'>" |
| 381 | "<nobr>%h</nobr></td></tr>\n", |
| 382 | x.zLoginGroup); |
| 383 | fossil_free(x.zLoginGroup); |
| 384 | }else{ |
| 385 | blob_appendf(&html, "<td> </td><td></td></tr>\n"); |
| 386 | } |
| 387 | sqlite3_free(zUrl); |
| 388 | } |
| 389 | db_finalize(&q); |
| 390 | blob_appendf(&html,"</tbody></table>\n"); |
| 391 | } |
| 392 | if( zSkinRepo ){ |
| 393 | char *zNewBase = mprintf("%s/%s", g.zBaseURL, zSkinUrl); |
| 394 | g.zBaseURL = 0; |
| 395 | set_base_url(zNewBase); |
| 396 |
+113
-23
| --- src/repolist.c | ||
| +++ src/repolist.c | ||
| @@ -31,10 +31,11 @@ | ||
| 31 | 31 | int isValid; /* True if zRepoName is a valid Fossil repository */ |
| 32 | 32 | int isRepolistSkin; /* 1 or 2 if this repository wants to be the skin |
| 33 | 33 | ** for the repository list. 2 means do use this |
| 34 | 34 | ** repository but do not display it in the list. */ |
| 35 | 35 | char *zProjName; /* Project Name. Memory from fossil_malloc() */ |
| 36 | + char *zProjDesc; /* Project Description. Memory from fossil_malloc() */ | |
| 36 | 37 | char *zLoginGroup; /* Name of login group, or NULL. Malloced() */ |
| 37 | 38 | double rMTime; /* Last update. Julian day number */ |
| 38 | 39 | }; |
| 39 | 40 | #endif |
| 40 | 41 | |
| @@ -49,10 +50,11 @@ | ||
| 49 | 50 | int rc; |
| 50 | 51 | |
| 51 | 52 | pRepo->isRepolistSkin = 0; |
| 52 | 53 | pRepo->isValid = 0; |
| 53 | 54 | pRepo->zProjName = 0; |
| 55 | + pRepo->zProjDesc = 0; | |
| 54 | 56 | pRepo->zLoginGroup = 0; |
| 55 | 57 | pRepo->rMTime = 0.0; |
| 56 | 58 | |
| 57 | 59 | g.dbIgnoreErrors++; |
| 58 | 60 | rc = sqlite3_open_v2(pRepo->zRepoName, &db, SQLITE_OPEN_READWRITE, 0); |
| @@ -71,10 +73,19 @@ | ||
| 71 | 73 | -1, &pStmt, 0); |
| 72 | 74 | if( rc ) goto finish_repo_list; |
| 73 | 75 | if( sqlite3_step(pStmt)==SQLITE_ROW ){ |
| 74 | 76 | pRepo->zProjName = fossil_strdup((char*)sqlite3_column_text(pStmt,0)); |
| 75 | 77 | } |
| 78 | + sqlite3_finalize(pStmt); | |
| 79 | + if( rc ) goto finish_repo_list; | |
| 80 | + rc = sqlite3_prepare_v2(db, "SELECT value FROM config" | |
| 81 | + " WHERE name='project-description'", | |
| 82 | + -1, &pStmt, 0); | |
| 83 | + if( rc ) goto finish_repo_list; | |
| 84 | + if( sqlite3_step(pStmt)==SQLITE_ROW ){ | |
| 85 | + pRepo->zProjDesc = fossil_strdup((char*)sqlite3_column_text(pStmt,0)); | |
| 86 | + } | |
| 76 | 87 | sqlite3_finalize(pStmt); |
| 77 | 88 | rc = sqlite3_prepare_v2(db, "SELECT value FROM config" |
| 78 | 89 | " WHERE name='login-group-name'", |
| 79 | 90 | -1, &pStmt, 0); |
| 80 | 91 | if( rc==SQLITE_OK && sqlite3_step(pStmt)==SQLITE_ROW ){ |
| @@ -89,10 +100,25 @@ | ||
| 89 | 100 | sqlite3_finalize(pStmt); |
| 90 | 101 | finish_repo_list: |
| 91 | 102 | g.dbIgnoreErrors--; |
| 92 | 103 | sqlite3_close(db); |
| 93 | 104 | } |
| 105 | + | |
| 106 | +/* | |
| 107 | +** SETTING: show-repolist-desc boolean default=off | |
| 108 | +** | |
| 109 | +** If the value of this setting is "1" globally, then the repository-list | |
| 110 | +** page will show the description of each repository. This setting only | |
| 111 | +** has effect when it is in the global setting database. | |
| 112 | +*/ | |
| 113 | +/* | |
| 114 | +** SETTING: show-repolist-lg boolean default=off | |
| 115 | +** | |
| 116 | +** If the value of this setting is "1" globally, then the repository-list | |
| 117 | +** page will show the login-group for each repository. This setting only | |
| 118 | +** has effect when it is in the global setting database. | |
| 119 | +*/ | |
| 94 | 120 | |
| 95 | 121 | /* |
| 96 | 122 | ** Generate a web-page that lists all repositories located under the |
| 97 | 123 | ** g.zRepositoryName directory and return non-zero. |
| 98 | 124 | ** |
| @@ -116,12 +142,27 @@ | ||
| 116 | 142 | ** False if a directory scan of base for repos */ |
| 117 | 143 | Blob html; /* Html for the body of the repository list */ |
| 118 | 144 | char *zSkinRepo = 0; /* Name of the repository database used for skins */ |
| 119 | 145 | char *zSkinUrl = 0; /* URL for the skin database */ |
| 120 | 146 | int quickfilter = 0; /* Is quickfilter is enabled? */ |
| 147 | + const char *zShow; /* Value of FOSSIL_REPOLIST_SHOW environment variable */ | |
| 148 | + int bShowDesc = 0; /* True to show the description column */ | |
| 149 | + int bShowLg = 0; /* True to show the login-group column */ | |
| 121 | 150 | |
| 122 | 151 | assert( g.db==0 ); |
| 152 | + zShow = P("FOSSIL_REPOLIST_SHOW"); | |
| 153 | + if( zShow ){ | |
| 154 | + bShowDesc = strstr(zShow,"description")!=0; | |
| 155 | + bShowLg = strstr(zShow,"login-group")!=0; | |
| 156 | + }else if( db_open_config(1, 1) | |
| 157 | + && db_table_exists("configdb", "global_config") | |
| 158 | + ){ | |
| 159 | + bShowDesc = db_int(bShowDesc, "SELECT value FROM global_config" | |
| 160 | + " WHERE name='show-repolist-desc'"); | |
| 161 | + bShowLg = db_int(bShowLg, "SELECT value FROM global_config" | |
| 162 | + " WHERE name='show-repolist-lg'"); | |
| 163 | + } | |
| 123 | 164 | blob_init(&html, 0, 0); |
| 124 | 165 | if( fossil_strcmp(g.zRepositoryName,"/")==0 && !g.fJail ){ |
| 125 | 166 | /* For the special case of the "repository directory" being "/", |
| 126 | 167 | ** show all of the repositories named in the ~/.fossil database. |
| 127 | 168 | ** |
| @@ -128,11 +169,10 @@ | ||
| 128 | 169 | ** On unix systems, then entries are of the form "repo:/home/..." |
| 129 | 170 | ** and on Windows systems they are like on unix, starting with a "/" |
| 130 | 171 | ** or they can begin with a drive letter: "repo:C:/Users/...". In either |
| 131 | 172 | ** case, we want returned path to omit any initial "/". |
| 132 | 173 | */ |
| 133 | - db_open_config(1, 0); | |
| 134 | 174 | db_multi_exec( |
| 135 | 175 | "CREATE TEMP VIEW sfile AS" |
| 136 | 176 | " SELECT ltrim(substr(name,6),'/') AS 'pathname' FROM global_config" |
| 137 | 177 | " WHERE name GLOB 'repo:*'" |
| 138 | 178 | ); |
| @@ -140,10 +180,12 @@ | ||
| 140 | 180 | }else{ |
| 141 | 181 | /* The default case: All repositories under the g.zRepositoryName |
| 142 | 182 | ** directory. |
| 143 | 183 | */ |
| 144 | 184 | blob_init(&base, g.zRepositoryName, -1); |
| 185 | + db_close(0); | |
| 186 | + assert( g.db==0 ); | |
| 145 | 187 | sqlite3_open(":memory:", &g.db); |
| 146 | 188 | db_multi_exec("CREATE TABLE sfile(pathname TEXT);"); |
| 147 | 189 | db_multi_exec("CREATE TABLE vfile(pathname);"); |
| 148 | 190 | vfile_scan(&base, blob_size(&base), 0, 0, 0, ExtFILE); |
| 149 | 191 | db_multi_exec("DELETE FROM sfile WHERE pathname NOT GLOB '*[^/].fossil'" |
| @@ -161,18 +203,50 @@ | ||
| 161 | 203 | g.localOpen = 0; |
| 162 | 204 | return 0; |
| 163 | 205 | }else{ |
| 164 | 206 | Stmt q; |
| 165 | 207 | double rNow; |
| 166 | - blob_append_sql(&html, | |
| 208 | + char zType[16]; /* Column type letters for class "sortable" */ | |
| 209 | + int nType; | |
| 210 | + zType[0] = 't'; /* Repo name */ | |
| 211 | + zType[1] = 'x'; /* Space between repo-name and project-name */ | |
| 212 | + zType[2] = 't'; /* Project name */ | |
| 213 | + nType = 3; | |
| 214 | + if( bShowDesc ){ | |
| 215 | + zType[nType++] = 'x'; /* Space between name and description */ | |
| 216 | + zType[nType++] = 't'; /* Project description */ | |
| 217 | + } | |
| 218 | + zType[nType++] = 'x'; /* space before age */ | |
| 219 | + zType[nType++] = 'k'; /* Project age */ | |
| 220 | + if( bShowLg ){ | |
| 221 | + zType[nType++] = 'x'; /* space before login-group */ | |
| 222 | + zType[nType++] = 't'; /* Login Group */ | |
| 223 | + } | |
| 224 | + zType[nType] = 0; | |
| 225 | + blob_appendf(&html, | |
| 167 | 226 | "<table border='0' class='sortable filterlist' data-init-sort='1'" |
| 168 | - " data-column-types='txtxkxt'><thead>\n" | |
| 169 | - "<tr><th>Filename<th width='20'>" | |
| 170 | - "<th>Project Name<th width='20'>" | |
| 171 | - "<th>Last Modified<th width='20'>" | |
| 172 | - "<th>Login Group</tr>\n" | |
| 173 | - "</thead><tbody>\n"); | |
| 227 | + " data-column-types='%s' cellspacing='0' cellpadding='0'><thead>\n" | |
| 228 | + "<tr><th>Filename</th><th> </th>\n" | |
| 229 | + "<th%s><nobr>Project Name</nobr></th>\n", | |
| 230 | + zType, (bShowDesc ? " width='25%'" : "")); | |
| 231 | + if( bShowDesc ){ | |
| 232 | + blob_appendf(&html, | |
| 233 | + "<th> </th>\n" | |
| 234 | + "<th width='25%%'><nobr>Project Description</nobr></th>\n" | |
| 235 | + ); | |
| 236 | + } | |
| 237 | + blob_appendf(&html, | |
| 238 | + "<th> </th>" | |
| 239 | + "<th><nobr>Last Modified</nobr></th>\n" | |
| 240 | + ); | |
| 241 | + if( bShowLg ){ | |
| 242 | + blob_appendf(&html, | |
| 243 | + "<th> </th>" | |
| 244 | + "<th><nobr>Login Group</nobr></th></tr>\n" | |
| 245 | + ); | |
| 246 | + } | |
| 247 | + blob_appendf(&html,"</thead><tbody>\n"); | |
| 174 | 248 | db_prepare(&q, "SELECT pathname" |
| 175 | 249 | " FROM sfile ORDER BY pathname COLLATE nocase;"); |
| 176 | 250 | rNow = db_double(0, "SELECT julianday('now')"); |
| 177 | 251 | while( db_step(&q)==SQLITE_ROW ){ |
| 178 | 252 | const char *zName = db_column_text(&q, 0); |
| @@ -231,21 +305,21 @@ | ||
| 231 | 305 | if( x.rMTime==0.0 ){ |
| 232 | 306 | /* This repository has no entry in the "event" table. |
| 233 | 307 | ** Its age will still be maximum, so data-sortkey will work. */ |
| 234 | 308 | zAge = mprintf("unknown"); |
| 235 | 309 | } |
| 236 | - blob_append_sql(&html, "<tr><td valign='top'>"); | |
| 310 | + blob_appendf(&html, "<tr><td valign='top'><nobr>"); | |
| 237 | 311 | if( !file_ends_with_repository_extension(zName,0) ){ |
| 238 | 312 | /* The "fossil server DIRECTORY" and "fossil ui DIRECTORY" commands |
| 239 | 313 | ** do not work for repositories whose names do not end in ".fossil". |
| 240 | 314 | ** So do not hyperlink those cases. */ |
| 241 | - blob_append_sql(&html,"%h",zName); | |
| 315 | + blob_appendf(&html,"%h",zName); | |
| 242 | 316 | } else if( sqlite3_strglob("*/.*", zName)==0 ){ |
| 243 | 317 | /* Do not show hyperlinks for hidden repos */ |
| 244 | - blob_append_sql(&html, "%h (hidden)", zName); | |
| 318 | + blob_appendf(&html, "%h (hidden)", zName); | |
| 245 | 319 | } else if( allRepo && sqlite3_strglob("[a-zA-Z]:/?*", zName)!=0 ){ |
| 246 | - blob_append_sql(&html, | |
| 320 | + blob_appendf(&html, | |
| 247 | 321 | "<a href='%R/%T/home' target='_blank'>/%h</a>\n", |
| 248 | 322 | zUrl, zName); |
| 249 | 323 | }else if( file_ends_with_repository_extension(zName,1) ){ |
| 250 | 324 | /* As described in |
| 251 | 325 | ** https://fossil-scm.org/forum/info/f50f647c97c72fc1: if |
| @@ -262,44 +336,60 @@ | ||
| 262 | 336 | , zDirPart |
| 263 | 337 | #if USE_SEE |
| 264 | 338 | , zDirPart |
| 265 | 339 | #endif |
| 266 | 340 | ) ){ |
| 267 | - blob_append_sql(&html, | |
| 341 | + blob_appendf(&html, | |
| 268 | 342 | "<s>%h</s> (directory/repo name collision)\n", |
| 269 | 343 | zName); |
| 270 | 344 | }else{ |
| 271 | - blob_append_sql(&html, | |
| 345 | + blob_appendf(&html, | |
| 272 | 346 | "<a href='%R/%T/home' target='_blank'>%h</a>\n", |
| 273 | 347 | zUrl, zName); |
| 274 | 348 | } |
| 275 | 349 | fossil_free(zDirPart); |
| 276 | 350 | }else{ |
| 277 | - blob_append_sql(&html, | |
| 351 | + blob_appendf(&html, | |
| 278 | 352 | "<a href='%R/%T/home' target='_blank'>%h</a>\n", |
| 279 | 353 | zUrl, zName); |
| 280 | 354 | } |
| 355 | + blob_appendf(&html,"</nobr></td>\n"); | |
| 281 | 356 | if( x.zProjName ){ |
| 282 | - blob_append_sql(&html, "<td></td><td>%h</td>\n", x.zProjName); | |
| 357 | + blob_appendf(&html, "<td> </td><td valign='top'>%h</td>\n", | |
| 358 | + x.zProjName); | |
| 283 | 359 | fossil_free(x.zProjName); |
| 284 | 360 | }else{ |
| 285 | - blob_append_sql(&html, "<td></td><td></td>\n"); | |
| 361 | + blob_appendf(&html, "<td> </td><td></td>\n"); | |
| 362 | + } | |
| 363 | + if( !bShowDesc ){ | |
| 364 | + /* Do nothing */ | |
| 365 | + }else if( x.zProjDesc ){ | |
| 366 | + blob_appendf(&html, "<td> </td><td valign='top'>%h</td>\n", | |
| 367 | + x.zProjDesc); | |
| 368 | + fossil_free(x.zProjDesc); | |
| 369 | + }else{ | |
| 370 | + blob_appendf(&html, "<td> </td><td></td>\n"); | |
| 286 | 371 | } |
| 287 | - blob_append_sql(&html, | |
| 288 | - "<td></td><td data-sortkey='%08x'>%h</td>\n", | |
| 372 | + blob_appendf(&html, | |
| 373 | + "<td> </td><td data-sortkey='%08x' align='center' valign='top'>" | |
| 374 | + "<nobr>%h</nobr></td>\n", | |
| 289 | 375 | (int)iAge, zAge); |
| 290 | 376 | fossil_free(zAge); |
| 291 | - if( x.zLoginGroup ){ | |
| 292 | - blob_append_sql(&html, "<td></td><td>%h</td></tr>\n", x.zLoginGroup); | |
| 377 | + if( !bShowLg ){ | |
| 378 | + blob_appendf(&html, "</tr>\n"); | |
| 379 | + }else if( x.zLoginGroup ){ | |
| 380 | + blob_appendf(&html, "<td> </td><td valign='top'>" | |
| 381 | + "<nobr>%h</nobr></td></tr>\n", | |
| 382 | + x.zLoginGroup); | |
| 293 | 383 | fossil_free(x.zLoginGroup); |
| 294 | 384 | }else{ |
| 295 | - blob_append_sql(&html, "<td></td><td></td></tr>\n"); | |
| 385 | + blob_appendf(&html, "<td> </td><td></td></tr>\n"); | |
| 296 | 386 | } |
| 297 | 387 | sqlite3_free(zUrl); |
| 298 | 388 | } |
| 299 | 389 | db_finalize(&q); |
| 300 | - blob_append_sql(&html,"</tbody></table>\n"); | |
| 390 | + blob_appendf(&html,"</tbody></table>\n"); | |
| 301 | 391 | } |
| 302 | 392 | if( zSkinRepo ){ |
| 303 | 393 | char *zNewBase = mprintf("%s/%s", g.zBaseURL, zSkinUrl); |
| 304 | 394 | g.zBaseURL = 0; |
| 305 | 395 | set_base_url(zNewBase); |
| 306 | 396 |
| --- src/repolist.c | |
| +++ src/repolist.c | |
| @@ -31,10 +31,11 @@ | |
| 31 | int isValid; /* True if zRepoName is a valid Fossil repository */ |
| 32 | int isRepolistSkin; /* 1 or 2 if this repository wants to be the skin |
| 33 | ** for the repository list. 2 means do use this |
| 34 | ** repository but do not display it in the list. */ |
| 35 | char *zProjName; /* Project Name. Memory from fossil_malloc() */ |
| 36 | char *zLoginGroup; /* Name of login group, or NULL. Malloced() */ |
| 37 | double rMTime; /* Last update. Julian day number */ |
| 38 | }; |
| 39 | #endif |
| 40 | |
| @@ -49,10 +50,11 @@ | |
| 49 | int rc; |
| 50 | |
| 51 | pRepo->isRepolistSkin = 0; |
| 52 | pRepo->isValid = 0; |
| 53 | pRepo->zProjName = 0; |
| 54 | pRepo->zLoginGroup = 0; |
| 55 | pRepo->rMTime = 0.0; |
| 56 | |
| 57 | g.dbIgnoreErrors++; |
| 58 | rc = sqlite3_open_v2(pRepo->zRepoName, &db, SQLITE_OPEN_READWRITE, 0); |
| @@ -71,10 +73,19 @@ | |
| 71 | -1, &pStmt, 0); |
| 72 | if( rc ) goto finish_repo_list; |
| 73 | if( sqlite3_step(pStmt)==SQLITE_ROW ){ |
| 74 | pRepo->zProjName = fossil_strdup((char*)sqlite3_column_text(pStmt,0)); |
| 75 | } |
| 76 | sqlite3_finalize(pStmt); |
| 77 | rc = sqlite3_prepare_v2(db, "SELECT value FROM config" |
| 78 | " WHERE name='login-group-name'", |
| 79 | -1, &pStmt, 0); |
| 80 | if( rc==SQLITE_OK && sqlite3_step(pStmt)==SQLITE_ROW ){ |
| @@ -89,10 +100,25 @@ | |
| 89 | sqlite3_finalize(pStmt); |
| 90 | finish_repo_list: |
| 91 | g.dbIgnoreErrors--; |
| 92 | sqlite3_close(db); |
| 93 | } |
| 94 | |
| 95 | /* |
| 96 | ** Generate a web-page that lists all repositories located under the |
| 97 | ** g.zRepositoryName directory and return non-zero. |
| 98 | ** |
| @@ -116,12 +142,27 @@ | |
| 116 | ** False if a directory scan of base for repos */ |
| 117 | Blob html; /* Html for the body of the repository list */ |
| 118 | char *zSkinRepo = 0; /* Name of the repository database used for skins */ |
| 119 | char *zSkinUrl = 0; /* URL for the skin database */ |
| 120 | int quickfilter = 0; /* Is quickfilter is enabled? */ |
| 121 | |
| 122 | assert( g.db==0 ); |
| 123 | blob_init(&html, 0, 0); |
| 124 | if( fossil_strcmp(g.zRepositoryName,"/")==0 && !g.fJail ){ |
| 125 | /* For the special case of the "repository directory" being "/", |
| 126 | ** show all of the repositories named in the ~/.fossil database. |
| 127 | ** |
| @@ -128,11 +169,10 @@ | |
| 128 | ** On unix systems, then entries are of the form "repo:/home/..." |
| 129 | ** and on Windows systems they are like on unix, starting with a "/" |
| 130 | ** or they can begin with a drive letter: "repo:C:/Users/...". In either |
| 131 | ** case, we want returned path to omit any initial "/". |
| 132 | */ |
| 133 | db_open_config(1, 0); |
| 134 | db_multi_exec( |
| 135 | "CREATE TEMP VIEW sfile AS" |
| 136 | " SELECT ltrim(substr(name,6),'/') AS 'pathname' FROM global_config" |
| 137 | " WHERE name GLOB 'repo:*'" |
| 138 | ); |
| @@ -140,10 +180,12 @@ | |
| 140 | }else{ |
| 141 | /* The default case: All repositories under the g.zRepositoryName |
| 142 | ** directory. |
| 143 | */ |
| 144 | blob_init(&base, g.zRepositoryName, -1); |
| 145 | sqlite3_open(":memory:", &g.db); |
| 146 | db_multi_exec("CREATE TABLE sfile(pathname TEXT);"); |
| 147 | db_multi_exec("CREATE TABLE vfile(pathname);"); |
| 148 | vfile_scan(&base, blob_size(&base), 0, 0, 0, ExtFILE); |
| 149 | db_multi_exec("DELETE FROM sfile WHERE pathname NOT GLOB '*[^/].fossil'" |
| @@ -161,18 +203,50 @@ | |
| 161 | g.localOpen = 0; |
| 162 | return 0; |
| 163 | }else{ |
| 164 | Stmt q; |
| 165 | double rNow; |
| 166 | blob_append_sql(&html, |
| 167 | "<table border='0' class='sortable filterlist' data-init-sort='1'" |
| 168 | " data-column-types='txtxkxt'><thead>\n" |
| 169 | "<tr><th>Filename<th width='20'>" |
| 170 | "<th>Project Name<th width='20'>" |
| 171 | "<th>Last Modified<th width='20'>" |
| 172 | "<th>Login Group</tr>\n" |
| 173 | "</thead><tbody>\n"); |
| 174 | db_prepare(&q, "SELECT pathname" |
| 175 | " FROM sfile ORDER BY pathname COLLATE nocase;"); |
| 176 | rNow = db_double(0, "SELECT julianday('now')"); |
| 177 | while( db_step(&q)==SQLITE_ROW ){ |
| 178 | const char *zName = db_column_text(&q, 0); |
| @@ -231,21 +305,21 @@ | |
| 231 | if( x.rMTime==0.0 ){ |
| 232 | /* This repository has no entry in the "event" table. |
| 233 | ** Its age will still be maximum, so data-sortkey will work. */ |
| 234 | zAge = mprintf("unknown"); |
| 235 | } |
| 236 | blob_append_sql(&html, "<tr><td valign='top'>"); |
| 237 | if( !file_ends_with_repository_extension(zName,0) ){ |
| 238 | /* The "fossil server DIRECTORY" and "fossil ui DIRECTORY" commands |
| 239 | ** do not work for repositories whose names do not end in ".fossil". |
| 240 | ** So do not hyperlink those cases. */ |
| 241 | blob_append_sql(&html,"%h",zName); |
| 242 | } else if( sqlite3_strglob("*/.*", zName)==0 ){ |
| 243 | /* Do not show hyperlinks for hidden repos */ |
| 244 | blob_append_sql(&html, "%h (hidden)", zName); |
| 245 | } else if( allRepo && sqlite3_strglob("[a-zA-Z]:/?*", zName)!=0 ){ |
| 246 | blob_append_sql(&html, |
| 247 | "<a href='%R/%T/home' target='_blank'>/%h</a>\n", |
| 248 | zUrl, zName); |
| 249 | }else if( file_ends_with_repository_extension(zName,1) ){ |
| 250 | /* As described in |
| 251 | ** https://fossil-scm.org/forum/info/f50f647c97c72fc1: if |
| @@ -262,44 +336,60 @@ | |
| 262 | , zDirPart |
| 263 | #if USE_SEE |
| 264 | , zDirPart |
| 265 | #endif |
| 266 | ) ){ |
| 267 | blob_append_sql(&html, |
| 268 | "<s>%h</s> (directory/repo name collision)\n", |
| 269 | zName); |
| 270 | }else{ |
| 271 | blob_append_sql(&html, |
| 272 | "<a href='%R/%T/home' target='_blank'>%h</a>\n", |
| 273 | zUrl, zName); |
| 274 | } |
| 275 | fossil_free(zDirPart); |
| 276 | }else{ |
| 277 | blob_append_sql(&html, |
| 278 | "<a href='%R/%T/home' target='_blank'>%h</a>\n", |
| 279 | zUrl, zName); |
| 280 | } |
| 281 | if( x.zProjName ){ |
| 282 | blob_append_sql(&html, "<td></td><td>%h</td>\n", x.zProjName); |
| 283 | fossil_free(x.zProjName); |
| 284 | }else{ |
| 285 | blob_append_sql(&html, "<td></td><td></td>\n"); |
| 286 | } |
| 287 | blob_append_sql(&html, |
| 288 | "<td></td><td data-sortkey='%08x'>%h</td>\n", |
| 289 | (int)iAge, zAge); |
| 290 | fossil_free(zAge); |
| 291 | if( x.zLoginGroup ){ |
| 292 | blob_append_sql(&html, "<td></td><td>%h</td></tr>\n", x.zLoginGroup); |
| 293 | fossil_free(x.zLoginGroup); |
| 294 | }else{ |
| 295 | blob_append_sql(&html, "<td></td><td></td></tr>\n"); |
| 296 | } |
| 297 | sqlite3_free(zUrl); |
| 298 | } |
| 299 | db_finalize(&q); |
| 300 | blob_append_sql(&html,"</tbody></table>\n"); |
| 301 | } |
| 302 | if( zSkinRepo ){ |
| 303 | char *zNewBase = mprintf("%s/%s", g.zBaseURL, zSkinUrl); |
| 304 | g.zBaseURL = 0; |
| 305 | set_base_url(zNewBase); |
| 306 |
| --- src/repolist.c | |
| +++ src/repolist.c | |
| @@ -31,10 +31,11 @@ | |
| 31 | int isValid; /* True if zRepoName is a valid Fossil repository */ |
| 32 | int isRepolistSkin; /* 1 or 2 if this repository wants to be the skin |
| 33 | ** for the repository list. 2 means do use this |
| 34 | ** repository but do not display it in the list. */ |
| 35 | char *zProjName; /* Project Name. Memory from fossil_malloc() */ |
| 36 | char *zProjDesc; /* Project Description. Memory from fossil_malloc() */ |
| 37 | char *zLoginGroup; /* Name of login group, or NULL. Malloced() */ |
| 38 | double rMTime; /* Last update. Julian day number */ |
| 39 | }; |
| 40 | #endif |
| 41 | |
| @@ -49,10 +50,11 @@ | |
| 50 | int rc; |
| 51 | |
| 52 | pRepo->isRepolistSkin = 0; |
| 53 | pRepo->isValid = 0; |
| 54 | pRepo->zProjName = 0; |
| 55 | pRepo->zProjDesc = 0; |
| 56 | pRepo->zLoginGroup = 0; |
| 57 | pRepo->rMTime = 0.0; |
| 58 | |
| 59 | g.dbIgnoreErrors++; |
| 60 | rc = sqlite3_open_v2(pRepo->zRepoName, &db, SQLITE_OPEN_READWRITE, 0); |
| @@ -71,10 +73,19 @@ | |
| 73 | -1, &pStmt, 0); |
| 74 | if( rc ) goto finish_repo_list; |
| 75 | if( sqlite3_step(pStmt)==SQLITE_ROW ){ |
| 76 | pRepo->zProjName = fossil_strdup((char*)sqlite3_column_text(pStmt,0)); |
| 77 | } |
| 78 | sqlite3_finalize(pStmt); |
| 79 | if( rc ) goto finish_repo_list; |
| 80 | rc = sqlite3_prepare_v2(db, "SELECT value FROM config" |
| 81 | " WHERE name='project-description'", |
| 82 | -1, &pStmt, 0); |
| 83 | if( rc ) goto finish_repo_list; |
| 84 | if( sqlite3_step(pStmt)==SQLITE_ROW ){ |
| 85 | pRepo->zProjDesc = fossil_strdup((char*)sqlite3_column_text(pStmt,0)); |
| 86 | } |
| 87 | sqlite3_finalize(pStmt); |
| 88 | rc = sqlite3_prepare_v2(db, "SELECT value FROM config" |
| 89 | " WHERE name='login-group-name'", |
| 90 | -1, &pStmt, 0); |
| 91 | if( rc==SQLITE_OK && sqlite3_step(pStmt)==SQLITE_ROW ){ |
| @@ -89,10 +100,25 @@ | |
| 100 | sqlite3_finalize(pStmt); |
| 101 | finish_repo_list: |
| 102 | g.dbIgnoreErrors--; |
| 103 | sqlite3_close(db); |
| 104 | } |
| 105 | |
| 106 | /* |
| 107 | ** SETTING: show-repolist-desc boolean default=off |
| 108 | ** |
| 109 | ** If the value of this setting is "1" globally, then the repository-list |
| 110 | ** page will show the description of each repository. This setting only |
| 111 | ** has effect when it is in the global setting database. |
| 112 | */ |
| 113 | /* |
| 114 | ** SETTING: show-repolist-lg boolean default=off |
| 115 | ** |
| 116 | ** If the value of this setting is "1" globally, then the repository-list |
| 117 | ** page will show the login-group for each repository. This setting only |
| 118 | ** has effect when it is in the global setting database. |
| 119 | */ |
| 120 | |
| 121 | /* |
| 122 | ** Generate a web-page that lists all repositories located under the |
| 123 | ** g.zRepositoryName directory and return non-zero. |
| 124 | ** |
| @@ -116,12 +142,27 @@ | |
| 142 | ** False if a directory scan of base for repos */ |
| 143 | Blob html; /* Html for the body of the repository list */ |
| 144 | char *zSkinRepo = 0; /* Name of the repository database used for skins */ |
| 145 | char *zSkinUrl = 0; /* URL for the skin database */ |
| 146 | int quickfilter = 0; /* Is quickfilter is enabled? */ |
| 147 | const char *zShow; /* Value of FOSSIL_REPOLIST_SHOW environment variable */ |
| 148 | int bShowDesc = 0; /* True to show the description column */ |
| 149 | int bShowLg = 0; /* True to show the login-group column */ |
| 150 | |
| 151 | assert( g.db==0 ); |
| 152 | zShow = P("FOSSIL_REPOLIST_SHOW"); |
| 153 | if( zShow ){ |
| 154 | bShowDesc = strstr(zShow,"description")!=0; |
| 155 | bShowLg = strstr(zShow,"login-group")!=0; |
| 156 | }else if( db_open_config(1, 1) |
| 157 | && db_table_exists("configdb", "global_config") |
| 158 | ){ |
| 159 | bShowDesc = db_int(bShowDesc, "SELECT value FROM global_config" |
| 160 | " WHERE name='show-repolist-desc'"); |
| 161 | bShowLg = db_int(bShowLg, "SELECT value FROM global_config" |
| 162 | " WHERE name='show-repolist-lg'"); |
| 163 | } |
| 164 | blob_init(&html, 0, 0); |
| 165 | if( fossil_strcmp(g.zRepositoryName,"/")==0 && !g.fJail ){ |
| 166 | /* For the special case of the "repository directory" being "/", |
| 167 | ** show all of the repositories named in the ~/.fossil database. |
| 168 | ** |
| @@ -128,11 +169,10 @@ | |
| 169 | ** On unix systems, then entries are of the form "repo:/home/..." |
| 170 | ** and on Windows systems they are like on unix, starting with a "/" |
| 171 | ** or they can begin with a drive letter: "repo:C:/Users/...". In either |
| 172 | ** case, we want returned path to omit any initial "/". |
| 173 | */ |
| 174 | db_multi_exec( |
| 175 | "CREATE TEMP VIEW sfile AS" |
| 176 | " SELECT ltrim(substr(name,6),'/') AS 'pathname' FROM global_config" |
| 177 | " WHERE name GLOB 'repo:*'" |
| 178 | ); |
| @@ -140,10 +180,12 @@ | |
| 180 | }else{ |
| 181 | /* The default case: All repositories under the g.zRepositoryName |
| 182 | ** directory. |
| 183 | */ |
| 184 | blob_init(&base, g.zRepositoryName, -1); |
| 185 | db_close(0); |
| 186 | assert( g.db==0 ); |
| 187 | sqlite3_open(":memory:", &g.db); |
| 188 | db_multi_exec("CREATE TABLE sfile(pathname TEXT);"); |
| 189 | db_multi_exec("CREATE TABLE vfile(pathname);"); |
| 190 | vfile_scan(&base, blob_size(&base), 0, 0, 0, ExtFILE); |
| 191 | db_multi_exec("DELETE FROM sfile WHERE pathname NOT GLOB '*[^/].fossil'" |
| @@ -161,18 +203,50 @@ | |
| 203 | g.localOpen = 0; |
| 204 | return 0; |
| 205 | }else{ |
| 206 | Stmt q; |
| 207 | double rNow; |
| 208 | char zType[16]; /* Column type letters for class "sortable" */ |
| 209 | int nType; |
| 210 | zType[0] = 't'; /* Repo name */ |
| 211 | zType[1] = 'x'; /* Space between repo-name and project-name */ |
| 212 | zType[2] = 't'; /* Project name */ |
| 213 | nType = 3; |
| 214 | if( bShowDesc ){ |
| 215 | zType[nType++] = 'x'; /* Space between name and description */ |
| 216 | zType[nType++] = 't'; /* Project description */ |
| 217 | } |
| 218 | zType[nType++] = 'x'; /* space before age */ |
| 219 | zType[nType++] = 'k'; /* Project age */ |
| 220 | if( bShowLg ){ |
| 221 | zType[nType++] = 'x'; /* space before login-group */ |
| 222 | zType[nType++] = 't'; /* Login Group */ |
| 223 | } |
| 224 | zType[nType] = 0; |
| 225 | blob_appendf(&html, |
| 226 | "<table border='0' class='sortable filterlist' data-init-sort='1'" |
| 227 | " data-column-types='%s' cellspacing='0' cellpadding='0'><thead>\n" |
| 228 | "<tr><th>Filename</th><th> </th>\n" |
| 229 | "<th%s><nobr>Project Name</nobr></th>\n", |
| 230 | zType, (bShowDesc ? " width='25%'" : "")); |
| 231 | if( bShowDesc ){ |
| 232 | blob_appendf(&html, |
| 233 | "<th> </th>\n" |
| 234 | "<th width='25%%'><nobr>Project Description</nobr></th>\n" |
| 235 | ); |
| 236 | } |
| 237 | blob_appendf(&html, |
| 238 | "<th> </th>" |
| 239 | "<th><nobr>Last Modified</nobr></th>\n" |
| 240 | ); |
| 241 | if( bShowLg ){ |
| 242 | blob_appendf(&html, |
| 243 | "<th> </th>" |
| 244 | "<th><nobr>Login Group</nobr></th></tr>\n" |
| 245 | ); |
| 246 | } |
| 247 | blob_appendf(&html,"</thead><tbody>\n"); |
| 248 | db_prepare(&q, "SELECT pathname" |
| 249 | " FROM sfile ORDER BY pathname COLLATE nocase;"); |
| 250 | rNow = db_double(0, "SELECT julianday('now')"); |
| 251 | while( db_step(&q)==SQLITE_ROW ){ |
| 252 | const char *zName = db_column_text(&q, 0); |
| @@ -231,21 +305,21 @@ | |
| 305 | if( x.rMTime==0.0 ){ |
| 306 | /* This repository has no entry in the "event" table. |
| 307 | ** Its age will still be maximum, so data-sortkey will work. */ |
| 308 | zAge = mprintf("unknown"); |
| 309 | } |
| 310 | blob_appendf(&html, "<tr><td valign='top'><nobr>"); |
| 311 | if( !file_ends_with_repository_extension(zName,0) ){ |
| 312 | /* The "fossil server DIRECTORY" and "fossil ui DIRECTORY" commands |
| 313 | ** do not work for repositories whose names do not end in ".fossil". |
| 314 | ** So do not hyperlink those cases. */ |
| 315 | blob_appendf(&html,"%h",zName); |
| 316 | } else if( sqlite3_strglob("*/.*", zName)==0 ){ |
| 317 | /* Do not show hyperlinks for hidden repos */ |
| 318 | blob_appendf(&html, "%h (hidden)", zName); |
| 319 | } else if( allRepo && sqlite3_strglob("[a-zA-Z]:/?*", zName)!=0 ){ |
| 320 | blob_appendf(&html, |
| 321 | "<a href='%R/%T/home' target='_blank'>/%h</a>\n", |
| 322 | zUrl, zName); |
| 323 | }else if( file_ends_with_repository_extension(zName,1) ){ |
| 324 | /* As described in |
| 325 | ** https://fossil-scm.org/forum/info/f50f647c97c72fc1: if |
| @@ -262,44 +336,60 @@ | |
| 336 | , zDirPart |
| 337 | #if USE_SEE |
| 338 | , zDirPart |
| 339 | #endif |
| 340 | ) ){ |
| 341 | blob_appendf(&html, |
| 342 | "<s>%h</s> (directory/repo name collision)\n", |
| 343 | zName); |
| 344 | }else{ |
| 345 | blob_appendf(&html, |
| 346 | "<a href='%R/%T/home' target='_blank'>%h</a>\n", |
| 347 | zUrl, zName); |
| 348 | } |
| 349 | fossil_free(zDirPart); |
| 350 | }else{ |
| 351 | blob_appendf(&html, |
| 352 | "<a href='%R/%T/home' target='_blank'>%h</a>\n", |
| 353 | zUrl, zName); |
| 354 | } |
| 355 | blob_appendf(&html,"</nobr></td>\n"); |
| 356 | if( x.zProjName ){ |
| 357 | blob_appendf(&html, "<td> </td><td valign='top'>%h</td>\n", |
| 358 | x.zProjName); |
| 359 | fossil_free(x.zProjName); |
| 360 | }else{ |
| 361 | blob_appendf(&html, "<td> </td><td></td>\n"); |
| 362 | } |
| 363 | if( !bShowDesc ){ |
| 364 | /* Do nothing */ |
| 365 | }else if( x.zProjDesc ){ |
| 366 | blob_appendf(&html, "<td> </td><td valign='top'>%h</td>\n", |
| 367 | x.zProjDesc); |
| 368 | fossil_free(x.zProjDesc); |
| 369 | }else{ |
| 370 | blob_appendf(&html, "<td> </td><td></td>\n"); |
| 371 | } |
| 372 | blob_appendf(&html, |
| 373 | "<td> </td><td data-sortkey='%08x' align='center' valign='top'>" |
| 374 | "<nobr>%h</nobr></td>\n", |
| 375 | (int)iAge, zAge); |
| 376 | fossil_free(zAge); |
| 377 | if( !bShowLg ){ |
| 378 | blob_appendf(&html, "</tr>\n"); |
| 379 | }else if( x.zLoginGroup ){ |
| 380 | blob_appendf(&html, "<td> </td><td valign='top'>" |
| 381 | "<nobr>%h</nobr></td></tr>\n", |
| 382 | x.zLoginGroup); |
| 383 | fossil_free(x.zLoginGroup); |
| 384 | }else{ |
| 385 | blob_appendf(&html, "<td> </td><td></td></tr>\n"); |
| 386 | } |
| 387 | sqlite3_free(zUrl); |
| 388 | } |
| 389 | db_finalize(&q); |
| 390 | blob_appendf(&html,"</tbody></table>\n"); |
| 391 | } |
| 392 | if( zSkinRepo ){ |
| 393 | char *zNewBase = mprintf("%s/%s", g.zBaseURL, zSkinUrl); |
| 394 | g.zBaseURL = 0; |
| 395 | set_base_url(zNewBase); |
| 396 |
+2
-4
| --- src/report.c | ||
| +++ src/report.c | ||
| @@ -588,13 +588,10 @@ | ||
| 588 | 588 | zOwner = g.zLogin; |
| 589 | 589 | } |
| 590 | 590 | } |
| 591 | 591 | if( zOwner==0 ) zOwner = g.zLogin; |
| 592 | 592 | style_submenu_element("Cancel", "%R/reportlist"); |
| 593 | - if( rn>0 ){ | |
| 594 | - style_submenu_element("Delete", "%R/rptedit/%d?del1=1", rn); | |
| 595 | - } | |
| 596 | 593 | style_header("%s", rn>0 ? "Edit Report Format":"Create New Report Format"); |
| 597 | 594 | if( zErr ){ |
| 598 | 595 | @ <blockquote class="reportError">%h(zErr)</blockquote> |
| 599 | 596 | } |
| 600 | 597 | @ <form action="rptedit" method="post"><div> |
| @@ -906,11 +903,12 @@ | ||
| 906 | 903 | |
| 907 | 904 | /* Output the separator above each entry in a table which has multiple lines |
| 908 | 905 | ** per database entry. |
| 909 | 906 | */ |
| 910 | 907 | if( pState->iNewRow>=0 ){ |
| 911 | - @ <tr><td colspan=%d(pState->nCol)><font size=1> </font></td></tr> | |
| 908 | + @ <tr><td colspan="%d(pState->nCol)" style="padding:0px"> | |
| 909 | + @ <hr style="margin:0px"></td></tr> | |
| 912 | 910 | } |
| 913 | 911 | |
| 914 | 912 | /* Output the data for this entry from the database |
| 915 | 913 | */ |
| 916 | 914 | zBg = pState->iBg>=0 ? azArg[pState->iBg] : 0; |
| 917 | 915 |
| --- src/report.c | |
| +++ src/report.c | |
| @@ -588,13 +588,10 @@ | |
| 588 | zOwner = g.zLogin; |
| 589 | } |
| 590 | } |
| 591 | if( zOwner==0 ) zOwner = g.zLogin; |
| 592 | style_submenu_element("Cancel", "%R/reportlist"); |
| 593 | if( rn>0 ){ |
| 594 | style_submenu_element("Delete", "%R/rptedit/%d?del1=1", rn); |
| 595 | } |
| 596 | style_header("%s", rn>0 ? "Edit Report Format":"Create New Report Format"); |
| 597 | if( zErr ){ |
| 598 | @ <blockquote class="reportError">%h(zErr)</blockquote> |
| 599 | } |
| 600 | @ <form action="rptedit" method="post"><div> |
| @@ -906,11 +903,12 @@ | |
| 906 | |
| 907 | /* Output the separator above each entry in a table which has multiple lines |
| 908 | ** per database entry. |
| 909 | */ |
| 910 | if( pState->iNewRow>=0 ){ |
| 911 | @ <tr><td colspan=%d(pState->nCol)><font size=1> </font></td></tr> |
| 912 | } |
| 913 | |
| 914 | /* Output the data for this entry from the database |
| 915 | */ |
| 916 | zBg = pState->iBg>=0 ? azArg[pState->iBg] : 0; |
| 917 |
| --- src/report.c | |
| +++ src/report.c | |
| @@ -588,13 +588,10 @@ | |
| 588 | zOwner = g.zLogin; |
| 589 | } |
| 590 | } |
| 591 | if( zOwner==0 ) zOwner = g.zLogin; |
| 592 | style_submenu_element("Cancel", "%R/reportlist"); |
| 593 | style_header("%s", rn>0 ? "Edit Report Format":"Create New Report Format"); |
| 594 | if( zErr ){ |
| 595 | @ <blockquote class="reportError">%h(zErr)</blockquote> |
| 596 | } |
| 597 | @ <form action="rptedit" method="post"><div> |
| @@ -906,11 +903,12 @@ | |
| 903 | |
| 904 | /* Output the separator above each entry in a table which has multiple lines |
| 905 | ** per database entry. |
| 906 | */ |
| 907 | if( pState->iNewRow>=0 ){ |
| 908 | @ <tr><td colspan="%d(pState->nCol)" style="padding:0px"> |
| 909 | @ <hr style="margin:0px"></td></tr> |
| 910 | } |
| 911 | |
| 912 | /* Output the data for this entry from the database |
| 913 | */ |
| 914 | zBg = pState->iBg>=0 ? azArg[pState->iBg] : 0; |
| 915 |
+2
-4
| --- src/report.c | ||
| +++ src/report.c | ||
| @@ -588,13 +588,10 @@ | ||
| 588 | 588 | zOwner = g.zLogin; |
| 589 | 589 | } |
| 590 | 590 | } |
| 591 | 591 | if( zOwner==0 ) zOwner = g.zLogin; |
| 592 | 592 | style_submenu_element("Cancel", "%R/reportlist"); |
| 593 | - if( rn>0 ){ | |
| 594 | - style_submenu_element("Delete", "%R/rptedit/%d?del1=1", rn); | |
| 595 | - } | |
| 596 | 593 | style_header("%s", rn>0 ? "Edit Report Format":"Create New Report Format"); |
| 597 | 594 | if( zErr ){ |
| 598 | 595 | @ <blockquote class="reportError">%h(zErr)</blockquote> |
| 599 | 596 | } |
| 600 | 597 | @ <form action="rptedit" method="post"><div> |
| @@ -906,11 +903,12 @@ | ||
| 906 | 903 | |
| 907 | 904 | /* Output the separator above each entry in a table which has multiple lines |
| 908 | 905 | ** per database entry. |
| 909 | 906 | */ |
| 910 | 907 | if( pState->iNewRow>=0 ){ |
| 911 | - @ <tr><td colspan=%d(pState->nCol)><font size=1> </font></td></tr> | |
| 908 | + @ <tr><td colspan="%d(pState->nCol)" style="padding:0px"> | |
| 909 | + @ <hr style="margin:0px"></td></tr> | |
| 912 | 910 | } |
| 913 | 911 | |
| 914 | 912 | /* Output the data for this entry from the database |
| 915 | 913 | */ |
| 916 | 914 | zBg = pState->iBg>=0 ? azArg[pState->iBg] : 0; |
| 917 | 915 |
| --- src/report.c | |
| +++ src/report.c | |
| @@ -588,13 +588,10 @@ | |
| 588 | zOwner = g.zLogin; |
| 589 | } |
| 590 | } |
| 591 | if( zOwner==0 ) zOwner = g.zLogin; |
| 592 | style_submenu_element("Cancel", "%R/reportlist"); |
| 593 | if( rn>0 ){ |
| 594 | style_submenu_element("Delete", "%R/rptedit/%d?del1=1", rn); |
| 595 | } |
| 596 | style_header("%s", rn>0 ? "Edit Report Format":"Create New Report Format"); |
| 597 | if( zErr ){ |
| 598 | @ <blockquote class="reportError">%h(zErr)</blockquote> |
| 599 | } |
| 600 | @ <form action="rptedit" method="post"><div> |
| @@ -906,11 +903,12 @@ | |
| 906 | |
| 907 | /* Output the separator above each entry in a table which has multiple lines |
| 908 | ** per database entry. |
| 909 | */ |
| 910 | if( pState->iNewRow>=0 ){ |
| 911 | @ <tr><td colspan=%d(pState->nCol)><font size=1> </font></td></tr> |
| 912 | } |
| 913 | |
| 914 | /* Output the data for this entry from the database |
| 915 | */ |
| 916 | zBg = pState->iBg>=0 ? azArg[pState->iBg] : 0; |
| 917 |
| --- src/report.c | |
| +++ src/report.c | |
| @@ -588,13 +588,10 @@ | |
| 588 | zOwner = g.zLogin; |
| 589 | } |
| 590 | } |
| 591 | if( zOwner==0 ) zOwner = g.zLogin; |
| 592 | style_submenu_element("Cancel", "%R/reportlist"); |
| 593 | style_header("%s", rn>0 ? "Edit Report Format":"Create New Report Format"); |
| 594 | if( zErr ){ |
| 595 | @ <blockquote class="reportError">%h(zErr)</blockquote> |
| 596 | } |
| 597 | @ <form action="rptedit" method="post"><div> |
| @@ -906,11 +903,12 @@ | |
| 903 | |
| 904 | /* Output the separator above each entry in a table which has multiple lines |
| 905 | ** per database entry. |
| 906 | */ |
| 907 | if( pState->iNewRow>=0 ){ |
| 908 | @ <tr><td colspan="%d(pState->nCol)" style="padding:0px"> |
| 909 | @ <hr style="margin:0px"></td></tr> |
| 910 | } |
| 911 | |
| 912 | /* Output the data for this entry from the database |
| 913 | */ |
| 914 | zBg = pState->iBg>=0 ? azArg[pState->iBg] : 0; |
| 915 |
+3
-2
| --- src/search.c | ||
| +++ src/search.c | ||
| @@ -618,10 +618,11 @@ | ||
| 618 | 618 | int bDebug = find_option("debug",0,0)!=0; /* Undocumented */ |
| 619 | 619 | int nLimit = zLimit ? atoi(zLimit) : -1000; |
| 620 | 620 | int width; |
| 621 | 621 | int nTty = 0; /* VT100 highlight color for matching text */ |
| 622 | 622 | const char *zHighlight = 0; |
| 623 | + int bFlags = 0; /* DB open flags */ | |
| 623 | 624 | |
| 624 | 625 | nTty = terminal_is_vt100(); |
| 625 | 626 | |
| 626 | 627 | /* Undocumented option to change highlight color */ |
| 627 | 628 | zHighlight = find_option("highlight",0,1); |
| @@ -666,12 +667,12 @@ | ||
| 666 | 667 | if( find_option("wiki",0,0) ){ srchFlags |= SRCH_WIKI; bFts = 1; } |
| 667 | 668 | |
| 668 | 669 | /* If no search objects are specified, default to "check-in comments" */ |
| 669 | 670 | if( srchFlags==0 ) srchFlags = SRCH_CKIN; |
| 670 | 671 | |
| 671 | - | |
| 672 | - db_find_and_open_repository(0, 0); | |
| 672 | + if( srchFlags==SRCH_HELP ) bFlags = OPEN_OK_NOT_FOUND|OPEN_SUBSTITUTE; | |
| 673 | + db_find_and_open_repository(bFlags, 0); | |
| 673 | 674 | verify_all_options(); |
| 674 | 675 | if( g.argc<3 ) return; |
| 675 | 676 | login_set_capabilities("s", 0); |
| 676 | 677 | if( search_restrict(srchFlags)==0 && (srchFlags & SRCH_HELP)==0 ){ |
| 677 | 678 | const char *zC1 = 0, *zPlural = "s"; |
| 678 | 679 |
| --- src/search.c | |
| +++ src/search.c | |
| @@ -618,10 +618,11 @@ | |
| 618 | int bDebug = find_option("debug",0,0)!=0; /* Undocumented */ |
| 619 | int nLimit = zLimit ? atoi(zLimit) : -1000; |
| 620 | int width; |
| 621 | int nTty = 0; /* VT100 highlight color for matching text */ |
| 622 | const char *zHighlight = 0; |
| 623 | |
| 624 | nTty = terminal_is_vt100(); |
| 625 | |
| 626 | /* Undocumented option to change highlight color */ |
| 627 | zHighlight = find_option("highlight",0,1); |
| @@ -666,12 +667,12 @@ | |
| 666 | if( find_option("wiki",0,0) ){ srchFlags |= SRCH_WIKI; bFts = 1; } |
| 667 | |
| 668 | /* If no search objects are specified, default to "check-in comments" */ |
| 669 | if( srchFlags==0 ) srchFlags = SRCH_CKIN; |
| 670 | |
| 671 | |
| 672 | db_find_and_open_repository(0, 0); |
| 673 | verify_all_options(); |
| 674 | if( g.argc<3 ) return; |
| 675 | login_set_capabilities("s", 0); |
| 676 | if( search_restrict(srchFlags)==0 && (srchFlags & SRCH_HELP)==0 ){ |
| 677 | const char *zC1 = 0, *zPlural = "s"; |
| 678 |
| --- src/search.c | |
| +++ src/search.c | |
| @@ -618,10 +618,11 @@ | |
| 618 | int bDebug = find_option("debug",0,0)!=0; /* Undocumented */ |
| 619 | int nLimit = zLimit ? atoi(zLimit) : -1000; |
| 620 | int width; |
| 621 | int nTty = 0; /* VT100 highlight color for matching text */ |
| 622 | const char *zHighlight = 0; |
| 623 | int bFlags = 0; /* DB open flags */ |
| 624 | |
| 625 | nTty = terminal_is_vt100(); |
| 626 | |
| 627 | /* Undocumented option to change highlight color */ |
| 628 | zHighlight = find_option("highlight",0,1); |
| @@ -666,12 +667,12 @@ | |
| 667 | if( find_option("wiki",0,0) ){ srchFlags |= SRCH_WIKI; bFts = 1; } |
| 668 | |
| 669 | /* If no search objects are specified, default to "check-in comments" */ |
| 670 | if( srchFlags==0 ) srchFlags = SRCH_CKIN; |
| 671 | |
| 672 | if( srchFlags==SRCH_HELP ) bFlags = OPEN_OK_NOT_FOUND|OPEN_SUBSTITUTE; |
| 673 | db_find_and_open_repository(bFlags, 0); |
| 674 | verify_all_options(); |
| 675 | if( g.argc<3 ) return; |
| 676 | login_set_capabilities("s", 0); |
| 677 | if( search_restrict(srchFlags)==0 && (srchFlags & SRCH_HELP)==0 ){ |
| 678 | const char *zC1 = 0, *zPlural = "s"; |
| 679 |
+66
-16
| --- src/security_audit.c | ||
| +++ src/security_audit.c | ||
| @@ -100,10 +100,11 @@ | ||
| 100 | 100 | const char *zReadCap; /* Capabilities of user group "reader" */ |
| 101 | 101 | const char *zPubPages; /* GLOB pattern for public pages */ |
| 102 | 102 | const char *zSelfCap; /* Capabilities of self-registered users */ |
| 103 | 103 | int hasSelfReg = 0; /* True if able to self-register */ |
| 104 | 104 | const char *zPublicUrl; /* Canonical access URL */ |
| 105 | + const char *zVulnReport; /* The vuln-report setting */ | |
| 105 | 106 | Blob cmd; |
| 106 | 107 | char *z; |
| 107 | 108 | int n, i; |
| 108 | 109 | CapabilityString *pCap; |
| 109 | 110 | char **azCSP; /* Parsed content security policy */ |
| @@ -362,10 +363,22 @@ | ||
| 362 | 363 | @ <li><p><b>WARNING:</b> |
| 363 | 364 | @ The "strict-manifest-syntax" flag is off. This is a security |
| 364 | 365 | @ risk. Turn this setting on (its default) to protect the users |
| 365 | 366 | @ of this repository. |
| 366 | 367 | } |
| 368 | + | |
| 369 | + zVulnReport = db_get("vuln-report","log"); | |
| 370 | + if( fossil_strcmp(zVulnReport,"block")!=0 | |
| 371 | + && fossil_strcmp(zVulnReport,"fatal")!=0 | |
| 372 | + ){ | |
| 373 | + @ <li><p><b>WARNING:</b> | |
| 374 | + @ The <a href="%R/help?cmd=vuln-report">vuln-report setting</a> | |
| 375 | + @ has a value of "%h(zVulnReport)". This disables defenses against | |
| 376 | + @ XSS or SQL-injection vulnerabilities caused by coding errors in | |
| 377 | + @ custom TH1 scripts. For the best security, change | |
| 378 | + @ the value of the vuln-report setting to "block" or "fatal". | |
| 379 | + } | |
| 367 | 380 | |
| 368 | 381 | /* Obsolete: */ |
| 369 | 382 | if( hasAnyCap(zAnonCap, "d") || |
| 370 | 383 | hasAnyCap(zDevCap, "d") || |
| 371 | 384 | hasAnyCap(zReadCap, "d") ){ |
| @@ -810,33 +823,39 @@ | ||
| 810 | 823 | ** WEBPAGE: errorlog |
| 811 | 824 | ** |
| 812 | 825 | ** Show the content of the error log. Only the administrator can view |
| 813 | 826 | ** this page. |
| 814 | 827 | ** |
| 815 | -** y=0x01 Show only hack attempts | |
| 816 | -** y=0x02 Show only panics and assertion faults | |
| 817 | -** y=0x04 Show hung backoffice processes | |
| 818 | -** y=0x08 Show POST requests from a different origin | |
| 819 | -** y=0x40 Show other uncategorized messages | |
| 828 | +** y=0x001 Show only hack attempts | |
| 829 | +** y=0x002 Show only panics and assertion faults | |
| 830 | +** y=0x004 Show hung backoffice processes | |
| 831 | +** y=0x008 Show POST requests from a different origin | |
| 832 | +** y=0x010 Show SQLITE_AUTH and similar | |
| 833 | +** y=0x020 Show SMTP error reports | |
| 834 | +** y=0x040 Show TH1 vulnerability reports | |
| 835 | +** y=0x800 Show other uncategorized messages | |
| 820 | 836 | ** |
| 821 | 837 | ** If y is omitted or is zero, a count of the various message types is |
| 822 | 838 | ** shown. |
| 823 | 839 | */ |
| 824 | 840 | void errorlog_page(void){ |
| 825 | 841 | i64 szFile; |
| 826 | 842 | FILE *in; |
| 827 | 843 | char *zLog; |
| 828 | 844 | const char *zType = P("y"); |
| 829 | - static const int eAllTypes = 0x4f; | |
| 845 | + static const int eAllTypes = 0x87f; | |
| 830 | 846 | long eType = 0; |
| 831 | 847 | int bOutput = 0; |
| 832 | 848 | int prevWasTime = 0; |
| 833 | 849 | int nHack = 0; |
| 834 | 850 | int nPanic = 0; |
| 835 | 851 | int nOther = 0; |
| 836 | 852 | int nHang = 0; |
| 837 | 853 | int nXPost = 0; |
| 854 | + int nAuth = 0; | |
| 855 | + int nSmtp = 0; | |
| 856 | + int nVuln = 0; | |
| 838 | 857 | char z[10000]; |
| 839 | 858 | char zTime[10000]; |
| 840 | 859 | |
| 841 | 860 | login_check_credentials(); |
| 842 | 861 | if( !g.perm.Admin ){ |
| @@ -906,11 +925,20 @@ | ||
| 906 | 925 | @ <li>Hung backoffice processes |
| 907 | 926 | } |
| 908 | 927 | if( eType & 0x08 ){ |
| 909 | 928 | @ <li>POST requests from different origin |
| 910 | 929 | } |
| 930 | + if( eType & 0x10 ){ | |
| 931 | + @ <li>SQLITE_AUTH and similar errors | |
| 932 | + } | |
| 933 | + if( eType & 0x20 ){ | |
| 934 | + @ <li>SMTP malfunctions | |
| 935 | + } | |
| 911 | 936 | if( eType & 0x40 ){ |
| 937 | + @ <li>TH1 vulnerabilities | |
| 938 | + } | |
| 939 | + if( eType & 0x800 ){ | |
| 912 | 940 | @ <li>Other uncategorized messages |
| 913 | 941 | } |
| 914 | 942 | @ </ul> |
| 915 | 943 | } |
| 916 | 944 | @ <hr> |
| @@ -924,21 +952,35 @@ | ||
| 924 | 952 | nHack++; |
| 925 | 953 | }else |
| 926 | 954 | if( (strncmp(z,"panic: ", 7)==0 || strstr(z," assertion fault ")!=0) ){ |
| 927 | 955 | bOutput = (eType & 0x02)!=0; |
| 928 | 956 | nPanic++; |
| 957 | + }else | |
| 958 | + if( strncmp(z,"SMTP:", 5)==0 ){ | |
| 959 | + bOutput = (eType & 0x20)!=0; | |
| 960 | + nSmtp++; | |
| 929 | 961 | }else |
| 930 | 962 | if( sqlite3_strglob("warning: backoffice process * still *",z)==0 ){ |
| 931 | 963 | bOutput = (eType & 0x04)!=0; |
| 932 | 964 | nHang++; |
| 933 | 965 | }else |
| 934 | 966 | if( sqlite3_strglob("warning: POST from different origin*",z)==0 ){ |
| 935 | 967 | bOutput = (eType & 0x08)!=0; |
| 936 | 968 | nXPost++; |
| 937 | 969 | }else |
| 938 | - { | |
| 970 | + if( sqlite3_strglob("SECURITY: authorizer blocks*",z)==0 | |
| 971 | + || sqlite3_strglob("warning: SQLITE_AUTH*",z)==0 | |
| 972 | + ){ | |
| 973 | + bOutput = (eType & 0x10)!=0; | |
| 974 | + nAuth++; | |
| 975 | + }else | |
| 976 | + if( strncmp(z,"possible", 8)==0 && strstr(z,"tainted")!=0 ){ | |
| 939 | 977 | bOutput = (eType & 0x40)!=0; |
| 978 | + nVuln++; | |
| 979 | + }else | |
| 980 | + { | |
| 981 | + bOutput = (eType & 0x800)!=0; | |
| 940 | 982 | nOther++; |
| 941 | 983 | } |
| 942 | 984 | if( bOutput ){ |
| 943 | 985 | @ %h(zTime)\ |
| 944 | 986 | } |
| @@ -958,42 +1000,50 @@ | ||
| 958 | 1000 | fclose(in); |
| 959 | 1001 | if( eType ){ |
| 960 | 1002 | @ </pre> |
| 961 | 1003 | } |
| 962 | 1004 | if( eType==0 ){ |
| 963 | - int nNonHack = nPanic + nHang + nOther; | |
| 1005 | + int nNonHack = nPanic + nHang + nAuth + nSmtp + nVuln + nOther; | |
| 964 | 1006 | int nTotal = nNonHack + nHack + nXPost; |
| 965 | 1007 | @ <p><table border="a" cellspacing="0" cellpadding="5"> |
| 966 | 1008 | if( nPanic>0 ){ |
| 967 | 1009 | @ <tr><td align="right">%d(nPanic)</td> |
| 968 | 1010 | @ <td><a href="./errorlog?y=2">Panics</a></td> |
| 969 | 1011 | } |
| 1012 | + if( nVuln>0 ){ | |
| 1013 | + @ <tr><td align="right">%d(nVuln)</td> | |
| 1014 | + @ <td><a href="./errorlog?y=64">TH1 Vulnerabilities</a></td> | |
| 1015 | + } | |
| 970 | 1016 | if( nHack>0 ){ |
| 971 | 1017 | @ <tr><td align="right">%d(nHack)</td> |
| 972 | 1018 | @ <td><a href="./errorlog?y=1">Hack Attempts</a></td> |
| 973 | 1019 | } |
| 974 | 1020 | if( nHang>0 ){ |
| 975 | 1021 | @ <tr><td align="right">%d(nHang)</td> |
| 976 | - @ <td><a href="./errorlog?y=4/">Hung Backoffice</a></td> | |
| 1022 | + @ <td><a href="./errorlog?y=4">Hung Backoffice</a></td> | |
| 977 | 1023 | } |
| 978 | 1024 | if( nXPost>0 ){ |
| 979 | 1025 | @ <tr><td align="right">%d(nXPost)</td> |
| 980 | - @ <td><a href="./errorlog?y=8/">POSTs from different origin</a></td> | |
| 1026 | + @ <td><a href="./errorlog?y=8">POSTs from different origin</a></td> | |
| 1027 | + } | |
| 1028 | + if( nAuth>0 ){ | |
| 1029 | + @ <tr><td align="right">%d(nAuth)</td> | |
| 1030 | + @ <td><a href="./errorlog?y=16">SQLITE_AUTH and similar</a></td> | |
| 1031 | + } | |
| 1032 | + if( nSmtp>0 ){ | |
| 1033 | + @ <tr><td align="right">%d(nSmtp)</td> | |
| 1034 | + @ <td><a href="./errorlog?y=32">SMTP faults</a></td> | |
| 981 | 1035 | } |
| 982 | 1036 | if( nOther>0 ){ |
| 983 | 1037 | @ <tr><td align="right">%d(nOther)</td> |
| 984 | - @ <td><a href="./errorlog?y=64/">Other</a></td> | |
| 985 | - } | |
| 986 | - if( nHack+nXPost>0 && nNonHack>0 ){ | |
| 987 | - @ <tr><td align="right">%d(nNonHack)</td> | |
| 988 | - @ <td><a href="%R/errorlog?y=70">Other than hack attempts</a></td> | |
| 1038 | + @ <td><a href="./errorlog?y=2048">Other</a></td> | |
| 989 | 1039 | } |
| 990 | 1040 | @ <tr><td align="right">%d(nTotal)</td> |
| 991 | 1041 | if( nTotal>0 ){ |
| 992 | - @ <td><a href="./errorlog?y=255">All Messages</a></td> | |
| 1042 | + @ <td><a href="./errorlog?y=4095">All Messages</a></td> | |
| 993 | 1043 | }else{ |
| 994 | 1044 | @ <td>All Messages</td> |
| 995 | 1045 | } |
| 996 | 1046 | @ </table> |
| 997 | 1047 | } |
| 998 | 1048 | style_finish_page(); |
| 999 | 1049 | } |
| 1000 | 1050 |
| --- src/security_audit.c | |
| +++ src/security_audit.c | |
| @@ -100,10 +100,11 @@ | |
| 100 | const char *zReadCap; /* Capabilities of user group "reader" */ |
| 101 | const char *zPubPages; /* GLOB pattern for public pages */ |
| 102 | const char *zSelfCap; /* Capabilities of self-registered users */ |
| 103 | int hasSelfReg = 0; /* True if able to self-register */ |
| 104 | const char *zPublicUrl; /* Canonical access URL */ |
| 105 | Blob cmd; |
| 106 | char *z; |
| 107 | int n, i; |
| 108 | CapabilityString *pCap; |
| 109 | char **azCSP; /* Parsed content security policy */ |
| @@ -362,10 +363,22 @@ | |
| 362 | @ <li><p><b>WARNING:</b> |
| 363 | @ The "strict-manifest-syntax" flag is off. This is a security |
| 364 | @ risk. Turn this setting on (its default) to protect the users |
| 365 | @ of this repository. |
| 366 | } |
| 367 | |
| 368 | /* Obsolete: */ |
| 369 | if( hasAnyCap(zAnonCap, "d") || |
| 370 | hasAnyCap(zDevCap, "d") || |
| 371 | hasAnyCap(zReadCap, "d") ){ |
| @@ -810,33 +823,39 @@ | |
| 810 | ** WEBPAGE: errorlog |
| 811 | ** |
| 812 | ** Show the content of the error log. Only the administrator can view |
| 813 | ** this page. |
| 814 | ** |
| 815 | ** y=0x01 Show only hack attempts |
| 816 | ** y=0x02 Show only panics and assertion faults |
| 817 | ** y=0x04 Show hung backoffice processes |
| 818 | ** y=0x08 Show POST requests from a different origin |
| 819 | ** y=0x40 Show other uncategorized messages |
| 820 | ** |
| 821 | ** If y is omitted or is zero, a count of the various message types is |
| 822 | ** shown. |
| 823 | */ |
| 824 | void errorlog_page(void){ |
| 825 | i64 szFile; |
| 826 | FILE *in; |
| 827 | char *zLog; |
| 828 | const char *zType = P("y"); |
| 829 | static const int eAllTypes = 0x4f; |
| 830 | long eType = 0; |
| 831 | int bOutput = 0; |
| 832 | int prevWasTime = 0; |
| 833 | int nHack = 0; |
| 834 | int nPanic = 0; |
| 835 | int nOther = 0; |
| 836 | int nHang = 0; |
| 837 | int nXPost = 0; |
| 838 | char z[10000]; |
| 839 | char zTime[10000]; |
| 840 | |
| 841 | login_check_credentials(); |
| 842 | if( !g.perm.Admin ){ |
| @@ -906,11 +925,20 @@ | |
| 906 | @ <li>Hung backoffice processes |
| 907 | } |
| 908 | if( eType & 0x08 ){ |
| 909 | @ <li>POST requests from different origin |
| 910 | } |
| 911 | if( eType & 0x40 ){ |
| 912 | @ <li>Other uncategorized messages |
| 913 | } |
| 914 | @ </ul> |
| 915 | } |
| 916 | @ <hr> |
| @@ -924,21 +952,35 @@ | |
| 924 | nHack++; |
| 925 | }else |
| 926 | if( (strncmp(z,"panic: ", 7)==0 || strstr(z," assertion fault ")!=0) ){ |
| 927 | bOutput = (eType & 0x02)!=0; |
| 928 | nPanic++; |
| 929 | }else |
| 930 | if( sqlite3_strglob("warning: backoffice process * still *",z)==0 ){ |
| 931 | bOutput = (eType & 0x04)!=0; |
| 932 | nHang++; |
| 933 | }else |
| 934 | if( sqlite3_strglob("warning: POST from different origin*",z)==0 ){ |
| 935 | bOutput = (eType & 0x08)!=0; |
| 936 | nXPost++; |
| 937 | }else |
| 938 | { |
| 939 | bOutput = (eType & 0x40)!=0; |
| 940 | nOther++; |
| 941 | } |
| 942 | if( bOutput ){ |
| 943 | @ %h(zTime)\ |
| 944 | } |
| @@ -958,42 +1000,50 @@ | |
| 958 | fclose(in); |
| 959 | if( eType ){ |
| 960 | @ </pre> |
| 961 | } |
| 962 | if( eType==0 ){ |
| 963 | int nNonHack = nPanic + nHang + nOther; |
| 964 | int nTotal = nNonHack + nHack + nXPost; |
| 965 | @ <p><table border="a" cellspacing="0" cellpadding="5"> |
| 966 | if( nPanic>0 ){ |
| 967 | @ <tr><td align="right">%d(nPanic)</td> |
| 968 | @ <td><a href="./errorlog?y=2">Panics</a></td> |
| 969 | } |
| 970 | if( nHack>0 ){ |
| 971 | @ <tr><td align="right">%d(nHack)</td> |
| 972 | @ <td><a href="./errorlog?y=1">Hack Attempts</a></td> |
| 973 | } |
| 974 | if( nHang>0 ){ |
| 975 | @ <tr><td align="right">%d(nHang)</td> |
| 976 | @ <td><a href="./errorlog?y=4/">Hung Backoffice</a></td> |
| 977 | } |
| 978 | if( nXPost>0 ){ |
| 979 | @ <tr><td align="right">%d(nXPost)</td> |
| 980 | @ <td><a href="./errorlog?y=8/">POSTs from different origin</a></td> |
| 981 | } |
| 982 | if( nOther>0 ){ |
| 983 | @ <tr><td align="right">%d(nOther)</td> |
| 984 | @ <td><a href="./errorlog?y=64/">Other</a></td> |
| 985 | } |
| 986 | if( nHack+nXPost>0 && nNonHack>0 ){ |
| 987 | @ <tr><td align="right">%d(nNonHack)</td> |
| 988 | @ <td><a href="%R/errorlog?y=70">Other than hack attempts</a></td> |
| 989 | } |
| 990 | @ <tr><td align="right">%d(nTotal)</td> |
| 991 | if( nTotal>0 ){ |
| 992 | @ <td><a href="./errorlog?y=255">All Messages</a></td> |
| 993 | }else{ |
| 994 | @ <td>All Messages</td> |
| 995 | } |
| 996 | @ </table> |
| 997 | } |
| 998 | style_finish_page(); |
| 999 | } |
| 1000 |
| --- src/security_audit.c | |
| +++ src/security_audit.c | |
| @@ -100,10 +100,11 @@ | |
| 100 | const char *zReadCap; /* Capabilities of user group "reader" */ |
| 101 | const char *zPubPages; /* GLOB pattern for public pages */ |
| 102 | const char *zSelfCap; /* Capabilities of self-registered users */ |
| 103 | int hasSelfReg = 0; /* True if able to self-register */ |
| 104 | const char *zPublicUrl; /* Canonical access URL */ |
| 105 | const char *zVulnReport; /* The vuln-report setting */ |
| 106 | Blob cmd; |
| 107 | char *z; |
| 108 | int n, i; |
| 109 | CapabilityString *pCap; |
| 110 | char **azCSP; /* Parsed content security policy */ |
| @@ -362,10 +363,22 @@ | |
| 363 | @ <li><p><b>WARNING:</b> |
| 364 | @ The "strict-manifest-syntax" flag is off. This is a security |
| 365 | @ risk. Turn this setting on (its default) to protect the users |
| 366 | @ of this repository. |
| 367 | } |
| 368 | |
| 369 | zVulnReport = db_get("vuln-report","log"); |
| 370 | if( fossil_strcmp(zVulnReport,"block")!=0 |
| 371 | && fossil_strcmp(zVulnReport,"fatal")!=0 |
| 372 | ){ |
| 373 | @ <li><p><b>WARNING:</b> |
| 374 | @ The <a href="%R/help?cmd=vuln-report">vuln-report setting</a> |
| 375 | @ has a value of "%h(zVulnReport)". This disables defenses against |
| 376 | @ XSS or SQL-injection vulnerabilities caused by coding errors in |
| 377 | @ custom TH1 scripts. For the best security, change |
| 378 | @ the value of the vuln-report setting to "block" or "fatal". |
| 379 | } |
| 380 | |
| 381 | /* Obsolete: */ |
| 382 | if( hasAnyCap(zAnonCap, "d") || |
| 383 | hasAnyCap(zDevCap, "d") || |
| 384 | hasAnyCap(zReadCap, "d") ){ |
| @@ -810,33 +823,39 @@ | |
| 823 | ** WEBPAGE: errorlog |
| 824 | ** |
| 825 | ** Show the content of the error log. Only the administrator can view |
| 826 | ** this page. |
| 827 | ** |
| 828 | ** y=0x001 Show only hack attempts |
| 829 | ** y=0x002 Show only panics and assertion faults |
| 830 | ** y=0x004 Show hung backoffice processes |
| 831 | ** y=0x008 Show POST requests from a different origin |
| 832 | ** y=0x010 Show SQLITE_AUTH and similar |
| 833 | ** y=0x020 Show SMTP error reports |
| 834 | ** y=0x040 Show TH1 vulnerability reports |
| 835 | ** y=0x800 Show other uncategorized messages |
| 836 | ** |
| 837 | ** If y is omitted or is zero, a count of the various message types is |
| 838 | ** shown. |
| 839 | */ |
| 840 | void errorlog_page(void){ |
| 841 | i64 szFile; |
| 842 | FILE *in; |
| 843 | char *zLog; |
| 844 | const char *zType = P("y"); |
| 845 | static const int eAllTypes = 0x87f; |
| 846 | long eType = 0; |
| 847 | int bOutput = 0; |
| 848 | int prevWasTime = 0; |
| 849 | int nHack = 0; |
| 850 | int nPanic = 0; |
| 851 | int nOther = 0; |
| 852 | int nHang = 0; |
| 853 | int nXPost = 0; |
| 854 | int nAuth = 0; |
| 855 | int nSmtp = 0; |
| 856 | int nVuln = 0; |
| 857 | char z[10000]; |
| 858 | char zTime[10000]; |
| 859 | |
| 860 | login_check_credentials(); |
| 861 | if( !g.perm.Admin ){ |
| @@ -906,11 +925,20 @@ | |
| 925 | @ <li>Hung backoffice processes |
| 926 | } |
| 927 | if( eType & 0x08 ){ |
| 928 | @ <li>POST requests from different origin |
| 929 | } |
| 930 | if( eType & 0x10 ){ |
| 931 | @ <li>SQLITE_AUTH and similar errors |
| 932 | } |
| 933 | if( eType & 0x20 ){ |
| 934 | @ <li>SMTP malfunctions |
| 935 | } |
| 936 | if( eType & 0x40 ){ |
| 937 | @ <li>TH1 vulnerabilities |
| 938 | } |
| 939 | if( eType & 0x800 ){ |
| 940 | @ <li>Other uncategorized messages |
| 941 | } |
| 942 | @ </ul> |
| 943 | } |
| 944 | @ <hr> |
| @@ -924,21 +952,35 @@ | |
| 952 | nHack++; |
| 953 | }else |
| 954 | if( (strncmp(z,"panic: ", 7)==0 || strstr(z," assertion fault ")!=0) ){ |
| 955 | bOutput = (eType & 0x02)!=0; |
| 956 | nPanic++; |
| 957 | }else |
| 958 | if( strncmp(z,"SMTP:", 5)==0 ){ |
| 959 | bOutput = (eType & 0x20)!=0; |
| 960 | nSmtp++; |
| 961 | }else |
| 962 | if( sqlite3_strglob("warning: backoffice process * still *",z)==0 ){ |
| 963 | bOutput = (eType & 0x04)!=0; |
| 964 | nHang++; |
| 965 | }else |
| 966 | if( sqlite3_strglob("warning: POST from different origin*",z)==0 ){ |
| 967 | bOutput = (eType & 0x08)!=0; |
| 968 | nXPost++; |
| 969 | }else |
| 970 | if( sqlite3_strglob("SECURITY: authorizer blocks*",z)==0 |
| 971 | || sqlite3_strglob("warning: SQLITE_AUTH*",z)==0 |
| 972 | ){ |
| 973 | bOutput = (eType & 0x10)!=0; |
| 974 | nAuth++; |
| 975 | }else |
| 976 | if( strncmp(z,"possible", 8)==0 && strstr(z,"tainted")!=0 ){ |
| 977 | bOutput = (eType & 0x40)!=0; |
| 978 | nVuln++; |
| 979 | }else |
| 980 | { |
| 981 | bOutput = (eType & 0x800)!=0; |
| 982 | nOther++; |
| 983 | } |
| 984 | if( bOutput ){ |
| 985 | @ %h(zTime)\ |
| 986 | } |
| @@ -958,42 +1000,50 @@ | |
| 1000 | fclose(in); |
| 1001 | if( eType ){ |
| 1002 | @ </pre> |
| 1003 | } |
| 1004 | if( eType==0 ){ |
| 1005 | int nNonHack = nPanic + nHang + nAuth + nSmtp + nVuln + nOther; |
| 1006 | int nTotal = nNonHack + nHack + nXPost; |
| 1007 | @ <p><table border="a" cellspacing="0" cellpadding="5"> |
| 1008 | if( nPanic>0 ){ |
| 1009 | @ <tr><td align="right">%d(nPanic)</td> |
| 1010 | @ <td><a href="./errorlog?y=2">Panics</a></td> |
| 1011 | } |
| 1012 | if( nVuln>0 ){ |
| 1013 | @ <tr><td align="right">%d(nVuln)</td> |
| 1014 | @ <td><a href="./errorlog?y=64">TH1 Vulnerabilities</a></td> |
| 1015 | } |
| 1016 | if( nHack>0 ){ |
| 1017 | @ <tr><td align="right">%d(nHack)</td> |
| 1018 | @ <td><a href="./errorlog?y=1">Hack Attempts</a></td> |
| 1019 | } |
| 1020 | if( nHang>0 ){ |
| 1021 | @ <tr><td align="right">%d(nHang)</td> |
| 1022 | @ <td><a href="./errorlog?y=4">Hung Backoffice</a></td> |
| 1023 | } |
| 1024 | if( nXPost>0 ){ |
| 1025 | @ <tr><td align="right">%d(nXPost)</td> |
| 1026 | @ <td><a href="./errorlog?y=8">POSTs from different origin</a></td> |
| 1027 | } |
| 1028 | if( nAuth>0 ){ |
| 1029 | @ <tr><td align="right">%d(nAuth)</td> |
| 1030 | @ <td><a href="./errorlog?y=16">SQLITE_AUTH and similar</a></td> |
| 1031 | } |
| 1032 | if( nSmtp>0 ){ |
| 1033 | @ <tr><td align="right">%d(nSmtp)</td> |
| 1034 | @ <td><a href="./errorlog?y=32">SMTP faults</a></td> |
| 1035 | } |
| 1036 | if( nOther>0 ){ |
| 1037 | @ <tr><td align="right">%d(nOther)</td> |
| 1038 | @ <td><a href="./errorlog?y=2048">Other</a></td> |
| 1039 | } |
| 1040 | @ <tr><td align="right">%d(nTotal)</td> |
| 1041 | if( nTotal>0 ){ |
| 1042 | @ <td><a href="./errorlog?y=4095">All Messages</a></td> |
| 1043 | }else{ |
| 1044 | @ <td>All Messages</td> |
| 1045 | } |
| 1046 | @ </table> |
| 1047 | } |
| 1048 | style_finish_page(); |
| 1049 | } |
| 1050 |
+15
-5
| --- src/setup.c | ||
| +++ src/setup.c | ||
| @@ -201,11 +201,11 @@ | ||
| 201 | 201 | login_needed(0); |
| 202 | 202 | return; |
| 203 | 203 | } |
| 204 | 204 | style_header("Log Menu"); |
| 205 | 205 | @ <table border="0" cellspacing="3"> |
| 206 | - | |
| 206 | + | |
| 207 | 207 | if( db_get_boolean("admin-log",1)==0 ){ |
| 208 | 208 | blob_appendf(&desc, |
| 209 | 209 | "The admin log records configuration changes to the repository.\n" |
| 210 | 210 | "<b>Disabled</b>: Turn on the " |
| 211 | 211 | " <a href='%R/setup_settings'>admin-log setting</a> to enable." |
| @@ -216,11 +216,11 @@ | ||
| 216 | 216 | setup_menu_entry("Admin Log", "admin_log", |
| 217 | 217 | "The admin log records configuration changes to the repository\n" |
| 218 | 218 | "in the \"admin_log\" table.\n" |
| 219 | 219 | ); |
| 220 | 220 | } |
| 221 | - setup_menu_entry("Artifact Log", "rcvfromlist", | |
| 221 | + setup_menu_entry("Xfer Log", "rcvfromlist", | |
| 222 | 222 | "The artifact log records when new content is added in the\n" |
| 223 | 223 | "\"rcvfrom\" table.\n" |
| 224 | 224 | ); |
| 225 | 225 | if( db_get_boolean("access-log",1) ){ |
| 226 | 226 | setup_menu_entry("User Log", "user_log", |
| @@ -462,11 +462,11 @@ | ||
| 462 | 462 | @ and "require a mouse event" should be turned on. These values only come |
| 463 | 463 | @ into play when the main auto-hyperlink settings is 2 ("UserAgent and |
| 464 | 464 | @ Javascript").</p> |
| 465 | 465 | @ |
| 466 | 466 | @ <p>To see if Javascript-base hyperlink enabling mechanism is working, |
| 467 | - @ visit the <a href="%R/test_env">/test_env</a> page (from a separate | |
| 467 | + @ visit the <a href="%R/test-env">/test-env</a> page (from a separate | |
| 468 | 468 | @ web browser that is not logged in, even as "anonymous") and verify |
| 469 | 469 | @ that the "g.jsHref" value is "1".</p> |
| 470 | 470 | @ <p>(Properties: "auto-hyperlink", "auto-hyperlink-delay", and |
| 471 | 471 | @ "auto-hyperlink-mouseover"")</p> |
| 472 | 472 | } |
| @@ -604,11 +604,11 @@ | ||
| 604 | 604 | @ in the CGI script. |
| 605 | 605 | @ </ol> |
| 606 | 606 | @ (Property: "localauth") |
| 607 | 607 | @ |
| 608 | 608 | @ <hr> |
| 609 | - onoff_attribute("Enable /test_env", | |
| 609 | + onoff_attribute("Enable /test-env", | |
| 610 | 610 | "test_env_enable", "test_env_enable", 0, 0); |
| 611 | 611 | @ <p>When enabled, the %h(g.zBaseURL)/test_env URL is available to all |
| 612 | 612 | @ users. When disabled (the default) only users Admin and Setup can visit |
| 613 | 613 | @ the /test_env page. |
| 614 | 614 | @ (Property: "test_env_enable") |
| @@ -1003,10 +1003,20 @@ | ||
| 1003 | 1003 | "timeline-hard-newlines", "thnl", 0, 0); |
| 1004 | 1004 | @ <p>In timeline displays, newline characters in check-in comments force |
| 1005 | 1005 | @ a line break on the display. |
| 1006 | 1006 | @ (Property: "timeline-hard-newlines")</p> |
| 1007 | 1007 | |
| 1008 | + @ <hr> | |
| 1009 | + onoff_attribute("Do not adjust user-selected background colors", | |
| 1010 | + "raw-bgcolor", "rbgc", 0, 0); | |
| 1011 | + @ <p>Fossil normally attempts to adjust the saturation and intensity of | |
| 1012 | + @ user-specified background colors on check-ins and branches so that the | |
| 1013 | + @ foreground text is easily readable on all skins. Enable this setting | |
| 1014 | + @ to omit that adjustment and use exactly the background color specified | |
| 1015 | + @ by users. | |
| 1016 | + @ (Property: "raw-bgcolor")</p> | |
| 1017 | + | |
| 1008 | 1018 | @ <hr> |
| 1009 | 1019 | onoff_attribute("Use Universal Coordinated Time (UTC)", |
| 1010 | 1020 | "timeline-utc", "utc", 1, 0); |
| 1011 | 1021 | @ <p>Show times as UTC (also sometimes called Greenwich Mean Time (GMT) or |
| 1012 | 1022 | @ Zulu) instead of in local time. On this server, local time is currently |
| @@ -1286,11 +1296,11 @@ | ||
| 1286 | 1296 | @ </p> |
| 1287 | 1297 | @ <hr> |
| 1288 | 1298 | textarea_attribute("Project Description", 3, 80, |
| 1289 | 1299 | "project-description", "pd", "", 0); |
| 1290 | 1300 | @ <p>Describe your project. This will be used in page headers for search |
| 1291 | - @ engines as well as a short RSS description. | |
| 1301 | + @ engines, the repository listing and a short RSS description. | |
| 1292 | 1302 | @ (Property: "project-description")</p> |
| 1293 | 1303 | @ <hr> |
| 1294 | 1304 | entry_attribute("Canonical Server URL", 40, "email-url", |
| 1295 | 1305 | "eurl", "", 0); |
| 1296 | 1306 | @ <p>This is the URL used to access this repository as a server. |
| 1297 | 1307 |
| --- src/setup.c | |
| +++ src/setup.c | |
| @@ -201,11 +201,11 @@ | |
| 201 | login_needed(0); |
| 202 | return; |
| 203 | } |
| 204 | style_header("Log Menu"); |
| 205 | @ <table border="0" cellspacing="3"> |
| 206 | |
| 207 | if( db_get_boolean("admin-log",1)==0 ){ |
| 208 | blob_appendf(&desc, |
| 209 | "The admin log records configuration changes to the repository.\n" |
| 210 | "<b>Disabled</b>: Turn on the " |
| 211 | " <a href='%R/setup_settings'>admin-log setting</a> to enable." |
| @@ -216,11 +216,11 @@ | |
| 216 | setup_menu_entry("Admin Log", "admin_log", |
| 217 | "The admin log records configuration changes to the repository\n" |
| 218 | "in the \"admin_log\" table.\n" |
| 219 | ); |
| 220 | } |
| 221 | setup_menu_entry("Artifact Log", "rcvfromlist", |
| 222 | "The artifact log records when new content is added in the\n" |
| 223 | "\"rcvfrom\" table.\n" |
| 224 | ); |
| 225 | if( db_get_boolean("access-log",1) ){ |
| 226 | setup_menu_entry("User Log", "user_log", |
| @@ -462,11 +462,11 @@ | |
| 462 | @ and "require a mouse event" should be turned on. These values only come |
| 463 | @ into play when the main auto-hyperlink settings is 2 ("UserAgent and |
| 464 | @ Javascript").</p> |
| 465 | @ |
| 466 | @ <p>To see if Javascript-base hyperlink enabling mechanism is working, |
| 467 | @ visit the <a href="%R/test_env">/test_env</a> page (from a separate |
| 468 | @ web browser that is not logged in, even as "anonymous") and verify |
| 469 | @ that the "g.jsHref" value is "1".</p> |
| 470 | @ <p>(Properties: "auto-hyperlink", "auto-hyperlink-delay", and |
| 471 | @ "auto-hyperlink-mouseover"")</p> |
| 472 | } |
| @@ -604,11 +604,11 @@ | |
| 604 | @ in the CGI script. |
| 605 | @ </ol> |
| 606 | @ (Property: "localauth") |
| 607 | @ |
| 608 | @ <hr> |
| 609 | onoff_attribute("Enable /test_env", |
| 610 | "test_env_enable", "test_env_enable", 0, 0); |
| 611 | @ <p>When enabled, the %h(g.zBaseURL)/test_env URL is available to all |
| 612 | @ users. When disabled (the default) only users Admin and Setup can visit |
| 613 | @ the /test_env page. |
| 614 | @ (Property: "test_env_enable") |
| @@ -1003,10 +1003,20 @@ | |
| 1003 | "timeline-hard-newlines", "thnl", 0, 0); |
| 1004 | @ <p>In timeline displays, newline characters in check-in comments force |
| 1005 | @ a line break on the display. |
| 1006 | @ (Property: "timeline-hard-newlines")</p> |
| 1007 | |
| 1008 | @ <hr> |
| 1009 | onoff_attribute("Use Universal Coordinated Time (UTC)", |
| 1010 | "timeline-utc", "utc", 1, 0); |
| 1011 | @ <p>Show times as UTC (also sometimes called Greenwich Mean Time (GMT) or |
| 1012 | @ Zulu) instead of in local time. On this server, local time is currently |
| @@ -1286,11 +1296,11 @@ | |
| 1286 | @ </p> |
| 1287 | @ <hr> |
| 1288 | textarea_attribute("Project Description", 3, 80, |
| 1289 | "project-description", "pd", "", 0); |
| 1290 | @ <p>Describe your project. This will be used in page headers for search |
| 1291 | @ engines as well as a short RSS description. |
| 1292 | @ (Property: "project-description")</p> |
| 1293 | @ <hr> |
| 1294 | entry_attribute("Canonical Server URL", 40, "email-url", |
| 1295 | "eurl", "", 0); |
| 1296 | @ <p>This is the URL used to access this repository as a server. |
| 1297 |
| --- src/setup.c | |
| +++ src/setup.c | |
| @@ -201,11 +201,11 @@ | |
| 201 | login_needed(0); |
| 202 | return; |
| 203 | } |
| 204 | style_header("Log Menu"); |
| 205 | @ <table border="0" cellspacing="3"> |
| 206 | |
| 207 | if( db_get_boolean("admin-log",1)==0 ){ |
| 208 | blob_appendf(&desc, |
| 209 | "The admin log records configuration changes to the repository.\n" |
| 210 | "<b>Disabled</b>: Turn on the " |
| 211 | " <a href='%R/setup_settings'>admin-log setting</a> to enable." |
| @@ -216,11 +216,11 @@ | |
| 216 | setup_menu_entry("Admin Log", "admin_log", |
| 217 | "The admin log records configuration changes to the repository\n" |
| 218 | "in the \"admin_log\" table.\n" |
| 219 | ); |
| 220 | } |
| 221 | setup_menu_entry("Xfer Log", "rcvfromlist", |
| 222 | "The artifact log records when new content is added in the\n" |
| 223 | "\"rcvfrom\" table.\n" |
| 224 | ); |
| 225 | if( db_get_boolean("access-log",1) ){ |
| 226 | setup_menu_entry("User Log", "user_log", |
| @@ -462,11 +462,11 @@ | |
| 462 | @ and "require a mouse event" should be turned on. These values only come |
| 463 | @ into play when the main auto-hyperlink settings is 2 ("UserAgent and |
| 464 | @ Javascript").</p> |
| 465 | @ |
| 466 | @ <p>To see if Javascript-base hyperlink enabling mechanism is working, |
| 467 | @ visit the <a href="%R/test-env">/test-env</a> page (from a separate |
| 468 | @ web browser that is not logged in, even as "anonymous") and verify |
| 469 | @ that the "g.jsHref" value is "1".</p> |
| 470 | @ <p>(Properties: "auto-hyperlink", "auto-hyperlink-delay", and |
| 471 | @ "auto-hyperlink-mouseover"")</p> |
| 472 | } |
| @@ -604,11 +604,11 @@ | |
| 604 | @ in the CGI script. |
| 605 | @ </ol> |
| 606 | @ (Property: "localauth") |
| 607 | @ |
| 608 | @ <hr> |
| 609 | onoff_attribute("Enable /test-env", |
| 610 | "test_env_enable", "test_env_enable", 0, 0); |
| 611 | @ <p>When enabled, the %h(g.zBaseURL)/test_env URL is available to all |
| 612 | @ users. When disabled (the default) only users Admin and Setup can visit |
| 613 | @ the /test_env page. |
| 614 | @ (Property: "test_env_enable") |
| @@ -1003,10 +1003,20 @@ | |
| 1003 | "timeline-hard-newlines", "thnl", 0, 0); |
| 1004 | @ <p>In timeline displays, newline characters in check-in comments force |
| 1005 | @ a line break on the display. |
| 1006 | @ (Property: "timeline-hard-newlines")</p> |
| 1007 | |
| 1008 | @ <hr> |
| 1009 | onoff_attribute("Do not adjust user-selected background colors", |
| 1010 | "raw-bgcolor", "rbgc", 0, 0); |
| 1011 | @ <p>Fossil normally attempts to adjust the saturation and intensity of |
| 1012 | @ user-specified background colors on check-ins and branches so that the |
| 1013 | @ foreground text is easily readable on all skins. Enable this setting |
| 1014 | @ to omit that adjustment and use exactly the background color specified |
| 1015 | @ by users. |
| 1016 | @ (Property: "raw-bgcolor")</p> |
| 1017 | |
| 1018 | @ <hr> |
| 1019 | onoff_attribute("Use Universal Coordinated Time (UTC)", |
| 1020 | "timeline-utc", "utc", 1, 0); |
| 1021 | @ <p>Show times as UTC (also sometimes called Greenwich Mean Time (GMT) or |
| 1022 | @ Zulu) instead of in local time. On this server, local time is currently |
| @@ -1286,11 +1296,11 @@ | |
| 1296 | @ </p> |
| 1297 | @ <hr> |
| 1298 | textarea_attribute("Project Description", 3, 80, |
| 1299 | "project-description", "pd", "", 0); |
| 1300 | @ <p>Describe your project. This will be used in page headers for search |
| 1301 | @ engines, the repository listing and a short RSS description. |
| 1302 | @ (Property: "project-description")</p> |
| 1303 | @ <hr> |
| 1304 | entry_attribute("Canonical Server URL", 40, "email-url", |
| 1305 | "eurl", "", 0); |
| 1306 | @ <p>This is the URL used to access this repository as a server. |
| 1307 |
+85
-39
| --- src/setupuser.c | ||
| +++ src/setupuser.c | ||
| @@ -41,21 +41,22 @@ | ||
| 41 | 41 | Stmt s; |
| 42 | 42 | double rNow; |
| 43 | 43 | const char *zWith = P("with"); |
| 44 | 44 | int bUnusedOnly = P("unused")!=0; |
| 45 | 45 | int bUbg = P("ubg")!=0; |
| 46 | + int bHaveAlerts; | |
| 46 | 47 | |
| 47 | 48 | login_check_credentials(); |
| 48 | 49 | if( !g.perm.Admin ){ |
| 49 | 50 | login_needed(0); |
| 50 | 51 | return; |
| 51 | 52 | } |
| 52 | - | |
| 53 | + bHaveAlerts = alert_tables_exist(); | |
| 53 | 54 | style_submenu_element("Add", "setup_uedit"); |
| 54 | 55 | style_submenu_element("Log", "access_log"); |
| 55 | 56 | style_submenu_element("Help", "setup_ulist_notes"); |
| 56 | - if( alert_tables_exist() ){ | |
| 57 | + if( bHaveAlerts ){ | |
| 57 | 58 | style_submenu_element("Subscribers", "subscribers"); |
| 58 | 59 | } |
| 59 | 60 | style_set_current_feature("setup"); |
| 60 | 61 | style_header("User List"); |
| 61 | 62 | if( (zWith==0 || zWith[0]==0) && !bUnusedOnly ){ |
| @@ -147,30 +148,34 @@ | ||
| 147 | 148 | zWith = mprintf( |
| 148 | 149 | " AND login NOT IN (" |
| 149 | 150 | "SELECT user FROM event WHERE user NOT NULL " |
| 150 | 151 | "UNION ALL SELECT euser FROM event WHERE euser NOT NULL%s)" |
| 151 | 152 | " AND uid NOT IN (SELECT uid FROM rcvfrom)", |
| 152 | - alert_tables_exist() ? | |
| 153 | + bHaveAlerts ? | |
| 153 | 154 | " UNION ALL SELECT suname FROM subscriber WHERE suname NOT NULL":""); |
| 154 | 155 | }else if( zWith && zWith[0] ){ |
| 155 | 156 | zWith = mprintf(" AND fullcap(cap) GLOB '*[%q]*'", zWith); |
| 156 | 157 | }else{ |
| 157 | 158 | zWith = ""; |
| 158 | 159 | } |
| 159 | 160 | db_prepare(&s, |
| 160 | - "SELECT uid, login, cap, info, date(user.mtime,'unixepoch')," | |
| 161 | - " lower(login) AS sortkey, " | |
| 162 | - " CASE WHEN info LIKE '%%expires 20%%'" | |
| 161 | + /*0-4*/"SELECT uid, login, cap, info, date(user.mtime,'unixepoch')," | |
| 162 | + /* 5 */"lower(login) AS sortkey, " | |
| 163 | + /* 6 */"CASE WHEN info LIKE '%%expires 20%%'" | |
| 163 | 164 | " THEN substr(info,instr(lower(info),'expires')+8,10)" |
| 164 | 165 | " END AS exp," |
| 165 | - "atime," | |
| 166 | - " subscriber.ssub, subscriber.subscriberId," | |
| 167 | - " user.mtime AS sorttime" | |
| 168 | - " FROM user LEFT JOIN lastAccess ON login=uname" | |
| 169 | - " LEFT JOIN subscriber ON login=suname" | |
| 170 | - " WHERE login NOT IN ('anonymous','nobody','developer','reader') %s" | |
| 171 | - " ORDER BY sorttime DESC", zWith/*safe-for-%s*/ | |
| 166 | + /* 7 */"atime," | |
| 167 | + /* 8 */"user.mtime AS sorttime," | |
| 168 | + /*9-11*/"%s" | |
| 169 | + " FROM user LEFT JOIN lastAccess ON login=uname" | |
| 170 | + " LEFT JOIN subscriber ON login=suname" | |
| 171 | + " WHERE login NOT IN ('anonymous','nobody','developer','reader') %s" | |
| 172 | + " ORDER BY sorttime DESC", | |
| 173 | + bHaveAlerts | |
| 174 | + ? "subscriber.ssub, subscriber.subscriberId, subscriber.semail" | |
| 175 | + : "null, null, null", | |
| 176 | + zWith/*safe-for-%s*/ | |
| 172 | 177 | ); |
| 173 | 178 | rNow = db_double(0.0, "SELECT julianday('now');"); |
| 174 | 179 | while( db_step(&s)==SQLITE_ROW ){ |
| 175 | 180 | int uid = db_column_int(&s, 0); |
| 176 | 181 | const char *zLogin = db_column_text(&s, 1); |
| @@ -180,12 +185,12 @@ | ||
| 180 | 185 | const char *zSortKey = db_column_text(&s,5); |
| 181 | 186 | const char *zExp = db_column_text(&s,6); |
| 182 | 187 | double rATime = db_column_double(&s,7); |
| 183 | 188 | char *zAge = 0; |
| 184 | 189 | const char *zSub; |
| 185 | - int sid = db_column_int(&s,9); | |
| 186 | - sqlite3_int64 sorttime = db_column_int64(&s, 10); | |
| 190 | + int sid = db_column_int(&s,10); | |
| 191 | + sqlite3_int64 sorttime = db_column_int64(&s, 8); | |
| 187 | 192 | if( rATime>0.0 ){ |
| 188 | 193 | zAge = human_readable_age(rNow - rATime); |
| 189 | 194 | } |
| 190 | 195 | if( bUbg ){ |
| 191 | 196 | @ <tr style='background-color: %h(user_color(zLogin));'> |
| @@ -197,16 +202,18 @@ | ||
| 197 | 202 | @ <td>%h(zCap) |
| 198 | 203 | @ <td>%h(zInfo) |
| 199 | 204 | @ <td data-sortkey='%09llx(sorttime)'>%h(zDate?zDate:"") |
| 200 | 205 | @ <td>%h(zExp?zExp:"") |
| 201 | 206 | @ <td data-sortkey='%f(rATime)' style='white-space:nowrap'>%s(zAge?zAge:"") |
| 202 | - if( db_column_type(&s,8)==SQLITE_NULL ){ | |
| 207 | + if( db_column_type(&s,9)==SQLITE_NULL ){ | |
| 203 | 208 | @ <td> |
| 204 | - }else if( (zSub = db_column_text(&s,8))==0 || zSub[0]==0 ){ | |
| 209 | + }else if( (zSub = db_column_text(&s,9))==0 || zSub[0]==0 ){ | |
| 205 | 210 | @ <td><a href="%R/alerts?sid=%d(sid)"><i>off</i></a> |
| 206 | 211 | }else{ |
| 207 | - @ <td><a href="%R/alerts?sid=%d(sid)">%h(zSub)</a> | |
| 212 | + const char *zEmail = db_column_text(&s, 11); | |
| 213 | + char * zAt = zEmail ? mprintf(" → %h", zEmail) : mprintf(""); | |
| 214 | + @ <td><a href="%R/alerts?sid=%d(sid)">%h(zSub)</a> %z(zAt) | |
| 208 | 215 | } |
| 209 | 216 | |
| 210 | 217 | @ </tr> |
| 211 | 218 | fossil_free(zAge); |
| 212 | 219 | } |
| @@ -304,22 +311,59 @@ | ||
| 304 | 311 | while( zPw[0]=='*' ){ zPw++; } |
| 305 | 312 | return zPw[0]!=0; |
| 306 | 313 | } |
| 307 | 314 | |
| 308 | 315 | /* |
| 309 | -** Return true if user capability string zNew contains any capability | |
| 310 | -** letter which is not in user capability string zOrig, else 0. This | |
| 311 | -** does not take inherited permissions into account. Either argument | |
| 312 | -** may be NULL. | |
| 316 | +** Return true if user capability strings zOrig and zNew materially | |
| 317 | +** differ, taking into account that they may be sorted in an arbitary | |
| 318 | +** order. This does not take inherited permissions into | |
| 319 | +** account. Either argument may be NULL. A NULL and an empty string | |
| 320 | +** are considered equivalent here. e.g. "abc" and "cab" are equivalent | |
| 321 | +** for this purpose, but "aCb" and "acb" are not. | |
| 322 | +*/ | |
| 323 | +static int userCapsChanged(const char *zOrig, const char *zNew){ | |
| 324 | + if( !zOrig ){ | |
| 325 | + return zNew ? (0!=*zNew) : 0; | |
| 326 | + }else if( !zNew ){ | |
| 327 | + return 0!=*zOrig; | |
| 328 | + }else if( 0==fossil_strcmp(zOrig, zNew) ){ | |
| 329 | + return 0; | |
| 330 | + }else{ | |
| 331 | + /* We don't know that zOrig and zNew are sorted equivalently. The | |
| 332 | + ** following steps will compare strings which contain all the same | |
| 333 | + ** capabilities letters as equivalent, regardless of the letters' | |
| 334 | + ** order in their strings. */ | |
| 335 | + char aOrig[128]; /* table of zOrig bytes */ | |
| 336 | + int nOrig = 0, nNew = 0; | |
| 337 | + | |
| 338 | + memset( &aOrig[0], 0, sizeof(aOrig) ); | |
| 339 | + for( ; *zOrig; ++zOrig, ++nOrig ){ | |
| 340 | + if( 0==(*zOrig & 0x80) ){ | |
| 341 | + aOrig[(int)*zOrig] = 1; | |
| 342 | + } | |
| 343 | + } | |
| 344 | + for( ; *zNew; ++zNew, ++nNew ){ | |
| 345 | + if( 0==(*zNew & 0x80) && !aOrig[(int)*zNew] ){ | |
| 346 | + return 1; | |
| 347 | + } | |
| 348 | + } | |
| 349 | + return nOrig!=nNew; | |
| 350 | + } | |
| 351 | +} | |
| 352 | + | |
| 353 | +/* | |
| 354 | +** COMMAND: test-user-caps-changed | |
| 355 | +** | |
| 356 | +** Usage: %fossil test-user-caps-changed caps1 caps2 | |
| 357 | +** | |
| 313 | 358 | */ |
| 314 | -static int userHasNewCaps(const char *zOrig, const char *zNew){ | |
| 315 | - for( ; zNew && *zNew; ++zNew ){ | |
| 316 | - if( !zOrig || strchr(zOrig,*zNew)==0 ){ | |
| 317 | - return *zNew; | |
| 318 | - } | |
| 319 | - } | |
| 320 | - return 0; | |
| 359 | +void test_user_caps_changed(void){ | |
| 360 | + | |
| 361 | + char const * zOld = g.argc>2 ? g.argv[2] : NULL; | |
| 362 | + char const * zNew = g.argc>3 ? g.argv[3] : NULL; | |
| 363 | + fossil_print("Has changes? = %d\n", | |
| 364 | + userCapsChanged( zOld, zNew )); | |
| 321 | 365 | } |
| 322 | 366 | |
| 323 | 367 | /* |
| 324 | 368 | ** Sends notification of user permission elevation changes to all |
| 325 | 369 | ** subscribers with a "u" subscription. This is a no-op if alerts are |
| @@ -333,11 +377,11 @@ | ||
| 333 | 377 | ** edits their subscriptions after an admin assigns them this one, |
| 334 | 378 | ** this particular one will be lost. "Feature or bug?" is unclear, |
| 335 | 379 | ** but it would be odd for a non-admin to be assigned this |
| 336 | 380 | ** capability. |
| 337 | 381 | */ |
| 338 | -static void alert_user_elevation(const char *zLogin, /*Affected user*/ | |
| 382 | +static void alert_user_cap_change(const char *zLogin, /*Affected user*/ | |
| 339 | 383 | int uid, /*[user].uid*/ |
| 340 | 384 | int bIsNew, /*true if new user*/ |
| 341 | 385 | const char *zOrigCaps,/*Old caps*/ |
| 342 | 386 | const char *zNewCaps /*New caps*/){ |
| 343 | 387 | Blob hdr, body; |
| @@ -349,21 +393,21 @@ | ||
| 349 | 393 | char * zSubject; |
| 350 | 394 | |
| 351 | 395 | if( !alert_enabled() ) return; |
| 352 | 396 | zSubject = bIsNew |
| 353 | 397 | ? mprintf("New user created: [%q]", zLogin) |
| 354 | - : mprintf("User [%q] permissions elevated", zLogin); | |
| 398 | + : mprintf("User [%q] capabilities changed", zLogin); | |
| 355 | 399 | zURL = db_get("email-url",0); |
| 356 | 400 | zSubname = db_get("email-subname", "[Fossil Repo]"); |
| 357 | 401 | blob_init(&body, 0, 0); |
| 358 | 402 | blob_init(&hdr, 0, 0); |
| 359 | 403 | if( bIsNew ){ |
| 360 | - blob_appendf(&body, "User [%q] was created by with " | |
| 404 | + blob_appendf(&body, "User [%q] was created with " | |
| 361 | 405 | "permissions [%q] by user [%q].\n", |
| 362 | 406 | zLogin, zNewCaps, g.zLogin); |
| 363 | 407 | } else { |
| 364 | - blob_appendf(&body, "Permissions for user [%q] where elevated " | |
| 408 | + blob_appendf(&body, "Permissions for user [%q] where changed " | |
| 365 | 409 | "from [%q] to [%q] by user [%q].\n", |
| 366 | 410 | zLogin, zOrigCaps, zNewCaps, g.zLogin); |
| 367 | 411 | } |
| 368 | 412 | if( zURL ){ |
| 369 | 413 | blob_appendf(&body, "\nUser editor: %s/setup_uedit?uid=%d\n", zURL, uid); |
| @@ -486,11 +530,11 @@ | ||
| 486 | 530 | }else if( !cgi_csrf_safe(2) ){ |
| 487 | 531 | /* This might be a cross-site request forgery, so ignore it */ |
| 488 | 532 | }else{ |
| 489 | 533 | /* We have all the information we need to make the change to the user */ |
| 490 | 534 | char c; |
| 491 | - int bHasNewCaps = 0 /* 1 if user's permissions are increased */; | |
| 535 | + int bCapsChanged = 0 /* 1 if user's permissions changed */; | |
| 492 | 536 | const int bIsNew = uid<=0; |
| 493 | 537 | char aCap[70], zNm[4]; |
| 494 | 538 | zNm[0] = 'a'; |
| 495 | 539 | zNm[2] = 0; |
| 496 | 540 | for(i=0, c='a'; c<='z'; c++){ |
| @@ -508,11 +552,11 @@ | ||
| 508 | 552 | a[c&0x7f] = P(zNm)!=0; |
| 509 | 553 | if( a[c&0x7f] ) aCap[i++] = c; |
| 510 | 554 | } |
| 511 | 555 | |
| 512 | 556 | aCap[i] = 0; |
| 513 | - bHasNewCaps = bIsNew || userHasNewCaps(zOldCaps, &aCap[0]); | |
| 557 | + bCapsChanged = bIsNew || userCapsChanged(zOldCaps, &aCap[0]); | |
| 514 | 558 | zPw = P("pw"); |
| 515 | 559 | zLogin = P("login"); |
| 516 | 560 | if( strlen(zLogin)==0 ){ |
| 517 | 561 | const char *zRef = cgi_referer("setup_ulist"); |
| 518 | 562 | style_header("User Creation Error"); |
| @@ -613,18 +657,20 @@ | ||
| 613 | 657 | @ <span class="loginError">%h(zErr)</span> |
| 614 | 658 | @ |
| 615 | 659 | @ <p><a href="setup_uedit?id=%d(uid)&referer=%T(zRef)"> |
| 616 | 660 | @ [Bummer]</a></p> |
| 617 | 661 | style_finish_page(); |
| 618 | - if( bHasNewCaps ){ | |
| 619 | - alert_user_elevation(zLogin, uid, bIsNew, zOldCaps, &aCap[0]); | |
| 662 | + if( bCapsChanged ){ | |
| 663 | + /* It's possible that caps were updated locally even if | |
| 664 | + ** login group updates failed. */ | |
| 665 | + alert_user_cap_change(zLogin, uid, bIsNew, zOldCaps, &aCap[0]); | |
| 620 | 666 | } |
| 621 | 667 | return; |
| 622 | 668 | } |
| 623 | 669 | } |
| 624 | - if( bHasNewCaps ){ | |
| 625 | - alert_user_elevation(zLogin, uid, bIsNew, zOldCaps, &aCap[0]); | |
| 670 | + if( bCapsChanged ){ | |
| 671 | + alert_user_cap_change(zLogin, uid, bIsNew, zOldCaps, &aCap[0]); | |
| 626 | 672 | } |
| 627 | 673 | cgi_redirect(cgi_referer("setup_ulist")); |
| 628 | 674 | return; |
| 629 | 675 | } |
| 630 | 676 | |
| 631 | 677 |
| --- src/setupuser.c | |
| +++ src/setupuser.c | |
| @@ -41,21 +41,22 @@ | |
| 41 | Stmt s; |
| 42 | double rNow; |
| 43 | const char *zWith = P("with"); |
| 44 | int bUnusedOnly = P("unused")!=0; |
| 45 | int bUbg = P("ubg")!=0; |
| 46 | |
| 47 | login_check_credentials(); |
| 48 | if( !g.perm.Admin ){ |
| 49 | login_needed(0); |
| 50 | return; |
| 51 | } |
| 52 | |
| 53 | style_submenu_element("Add", "setup_uedit"); |
| 54 | style_submenu_element("Log", "access_log"); |
| 55 | style_submenu_element("Help", "setup_ulist_notes"); |
| 56 | if( alert_tables_exist() ){ |
| 57 | style_submenu_element("Subscribers", "subscribers"); |
| 58 | } |
| 59 | style_set_current_feature("setup"); |
| 60 | style_header("User List"); |
| 61 | if( (zWith==0 || zWith[0]==0) && !bUnusedOnly ){ |
| @@ -147,30 +148,34 @@ | |
| 147 | zWith = mprintf( |
| 148 | " AND login NOT IN (" |
| 149 | "SELECT user FROM event WHERE user NOT NULL " |
| 150 | "UNION ALL SELECT euser FROM event WHERE euser NOT NULL%s)" |
| 151 | " AND uid NOT IN (SELECT uid FROM rcvfrom)", |
| 152 | alert_tables_exist() ? |
| 153 | " UNION ALL SELECT suname FROM subscriber WHERE suname NOT NULL":""); |
| 154 | }else if( zWith && zWith[0] ){ |
| 155 | zWith = mprintf(" AND fullcap(cap) GLOB '*[%q]*'", zWith); |
| 156 | }else{ |
| 157 | zWith = ""; |
| 158 | } |
| 159 | db_prepare(&s, |
| 160 | "SELECT uid, login, cap, info, date(user.mtime,'unixepoch')," |
| 161 | " lower(login) AS sortkey, " |
| 162 | " CASE WHEN info LIKE '%%expires 20%%'" |
| 163 | " THEN substr(info,instr(lower(info),'expires')+8,10)" |
| 164 | " END AS exp," |
| 165 | "atime," |
| 166 | " subscriber.ssub, subscriber.subscriberId," |
| 167 | " user.mtime AS sorttime" |
| 168 | " FROM user LEFT JOIN lastAccess ON login=uname" |
| 169 | " LEFT JOIN subscriber ON login=suname" |
| 170 | " WHERE login NOT IN ('anonymous','nobody','developer','reader') %s" |
| 171 | " ORDER BY sorttime DESC", zWith/*safe-for-%s*/ |
| 172 | ); |
| 173 | rNow = db_double(0.0, "SELECT julianday('now');"); |
| 174 | while( db_step(&s)==SQLITE_ROW ){ |
| 175 | int uid = db_column_int(&s, 0); |
| 176 | const char *zLogin = db_column_text(&s, 1); |
| @@ -180,12 +185,12 @@ | |
| 180 | const char *zSortKey = db_column_text(&s,5); |
| 181 | const char *zExp = db_column_text(&s,6); |
| 182 | double rATime = db_column_double(&s,7); |
| 183 | char *zAge = 0; |
| 184 | const char *zSub; |
| 185 | int sid = db_column_int(&s,9); |
| 186 | sqlite3_int64 sorttime = db_column_int64(&s, 10); |
| 187 | if( rATime>0.0 ){ |
| 188 | zAge = human_readable_age(rNow - rATime); |
| 189 | } |
| 190 | if( bUbg ){ |
| 191 | @ <tr style='background-color: %h(user_color(zLogin));'> |
| @@ -197,16 +202,18 @@ | |
| 197 | @ <td>%h(zCap) |
| 198 | @ <td>%h(zInfo) |
| 199 | @ <td data-sortkey='%09llx(sorttime)'>%h(zDate?zDate:"") |
| 200 | @ <td>%h(zExp?zExp:"") |
| 201 | @ <td data-sortkey='%f(rATime)' style='white-space:nowrap'>%s(zAge?zAge:"") |
| 202 | if( db_column_type(&s,8)==SQLITE_NULL ){ |
| 203 | @ <td> |
| 204 | }else if( (zSub = db_column_text(&s,8))==0 || zSub[0]==0 ){ |
| 205 | @ <td><a href="%R/alerts?sid=%d(sid)"><i>off</i></a> |
| 206 | }else{ |
| 207 | @ <td><a href="%R/alerts?sid=%d(sid)">%h(zSub)</a> |
| 208 | } |
| 209 | |
| 210 | @ </tr> |
| 211 | fossil_free(zAge); |
| 212 | } |
| @@ -304,22 +311,59 @@ | |
| 304 | while( zPw[0]=='*' ){ zPw++; } |
| 305 | return zPw[0]!=0; |
| 306 | } |
| 307 | |
| 308 | /* |
| 309 | ** Return true if user capability string zNew contains any capability |
| 310 | ** letter which is not in user capability string zOrig, else 0. This |
| 311 | ** does not take inherited permissions into account. Either argument |
| 312 | ** may be NULL. |
| 313 | */ |
| 314 | static int userHasNewCaps(const char *zOrig, const char *zNew){ |
| 315 | for( ; zNew && *zNew; ++zNew ){ |
| 316 | if( !zOrig || strchr(zOrig,*zNew)==0 ){ |
| 317 | return *zNew; |
| 318 | } |
| 319 | } |
| 320 | return 0; |
| 321 | } |
| 322 | |
| 323 | /* |
| 324 | ** Sends notification of user permission elevation changes to all |
| 325 | ** subscribers with a "u" subscription. This is a no-op if alerts are |
| @@ -333,11 +377,11 @@ | |
| 333 | ** edits their subscriptions after an admin assigns them this one, |
| 334 | ** this particular one will be lost. "Feature or bug?" is unclear, |
| 335 | ** but it would be odd for a non-admin to be assigned this |
| 336 | ** capability. |
| 337 | */ |
| 338 | static void alert_user_elevation(const char *zLogin, /*Affected user*/ |
| 339 | int uid, /*[user].uid*/ |
| 340 | int bIsNew, /*true if new user*/ |
| 341 | const char *zOrigCaps,/*Old caps*/ |
| 342 | const char *zNewCaps /*New caps*/){ |
| 343 | Blob hdr, body; |
| @@ -349,21 +393,21 @@ | |
| 349 | char * zSubject; |
| 350 | |
| 351 | if( !alert_enabled() ) return; |
| 352 | zSubject = bIsNew |
| 353 | ? mprintf("New user created: [%q]", zLogin) |
| 354 | : mprintf("User [%q] permissions elevated", zLogin); |
| 355 | zURL = db_get("email-url",0); |
| 356 | zSubname = db_get("email-subname", "[Fossil Repo]"); |
| 357 | blob_init(&body, 0, 0); |
| 358 | blob_init(&hdr, 0, 0); |
| 359 | if( bIsNew ){ |
| 360 | blob_appendf(&body, "User [%q] was created by with " |
| 361 | "permissions [%q] by user [%q].\n", |
| 362 | zLogin, zNewCaps, g.zLogin); |
| 363 | } else { |
| 364 | blob_appendf(&body, "Permissions for user [%q] where elevated " |
| 365 | "from [%q] to [%q] by user [%q].\n", |
| 366 | zLogin, zOrigCaps, zNewCaps, g.zLogin); |
| 367 | } |
| 368 | if( zURL ){ |
| 369 | blob_appendf(&body, "\nUser editor: %s/setup_uedit?uid=%d\n", zURL, uid); |
| @@ -486,11 +530,11 @@ | |
| 486 | }else if( !cgi_csrf_safe(2) ){ |
| 487 | /* This might be a cross-site request forgery, so ignore it */ |
| 488 | }else{ |
| 489 | /* We have all the information we need to make the change to the user */ |
| 490 | char c; |
| 491 | int bHasNewCaps = 0 /* 1 if user's permissions are increased */; |
| 492 | const int bIsNew = uid<=0; |
| 493 | char aCap[70], zNm[4]; |
| 494 | zNm[0] = 'a'; |
| 495 | zNm[2] = 0; |
| 496 | for(i=0, c='a'; c<='z'; c++){ |
| @@ -508,11 +552,11 @@ | |
| 508 | a[c&0x7f] = P(zNm)!=0; |
| 509 | if( a[c&0x7f] ) aCap[i++] = c; |
| 510 | } |
| 511 | |
| 512 | aCap[i] = 0; |
| 513 | bHasNewCaps = bIsNew || userHasNewCaps(zOldCaps, &aCap[0]); |
| 514 | zPw = P("pw"); |
| 515 | zLogin = P("login"); |
| 516 | if( strlen(zLogin)==0 ){ |
| 517 | const char *zRef = cgi_referer("setup_ulist"); |
| 518 | style_header("User Creation Error"); |
| @@ -613,18 +657,20 @@ | |
| 613 | @ <span class="loginError">%h(zErr)</span> |
| 614 | @ |
| 615 | @ <p><a href="setup_uedit?id=%d(uid)&referer=%T(zRef)"> |
| 616 | @ [Bummer]</a></p> |
| 617 | style_finish_page(); |
| 618 | if( bHasNewCaps ){ |
| 619 | alert_user_elevation(zLogin, uid, bIsNew, zOldCaps, &aCap[0]); |
| 620 | } |
| 621 | return; |
| 622 | } |
| 623 | } |
| 624 | if( bHasNewCaps ){ |
| 625 | alert_user_elevation(zLogin, uid, bIsNew, zOldCaps, &aCap[0]); |
| 626 | } |
| 627 | cgi_redirect(cgi_referer("setup_ulist")); |
| 628 | return; |
| 629 | } |
| 630 | |
| 631 |
| --- src/setupuser.c | |
| +++ src/setupuser.c | |
| @@ -41,21 +41,22 @@ | |
| 41 | Stmt s; |
| 42 | double rNow; |
| 43 | const char *zWith = P("with"); |
| 44 | int bUnusedOnly = P("unused")!=0; |
| 45 | int bUbg = P("ubg")!=0; |
| 46 | int bHaveAlerts; |
| 47 | |
| 48 | login_check_credentials(); |
| 49 | if( !g.perm.Admin ){ |
| 50 | login_needed(0); |
| 51 | return; |
| 52 | } |
| 53 | bHaveAlerts = alert_tables_exist(); |
| 54 | style_submenu_element("Add", "setup_uedit"); |
| 55 | style_submenu_element("Log", "access_log"); |
| 56 | style_submenu_element("Help", "setup_ulist_notes"); |
| 57 | if( bHaveAlerts ){ |
| 58 | style_submenu_element("Subscribers", "subscribers"); |
| 59 | } |
| 60 | style_set_current_feature("setup"); |
| 61 | style_header("User List"); |
| 62 | if( (zWith==0 || zWith[0]==0) && !bUnusedOnly ){ |
| @@ -147,30 +148,34 @@ | |
| 148 | zWith = mprintf( |
| 149 | " AND login NOT IN (" |
| 150 | "SELECT user FROM event WHERE user NOT NULL " |
| 151 | "UNION ALL SELECT euser FROM event WHERE euser NOT NULL%s)" |
| 152 | " AND uid NOT IN (SELECT uid FROM rcvfrom)", |
| 153 | bHaveAlerts ? |
| 154 | " UNION ALL SELECT suname FROM subscriber WHERE suname NOT NULL":""); |
| 155 | }else if( zWith && zWith[0] ){ |
| 156 | zWith = mprintf(" AND fullcap(cap) GLOB '*[%q]*'", zWith); |
| 157 | }else{ |
| 158 | zWith = ""; |
| 159 | } |
| 160 | db_prepare(&s, |
| 161 | /*0-4*/"SELECT uid, login, cap, info, date(user.mtime,'unixepoch')," |
| 162 | /* 5 */"lower(login) AS sortkey, " |
| 163 | /* 6 */"CASE WHEN info LIKE '%%expires 20%%'" |
| 164 | " THEN substr(info,instr(lower(info),'expires')+8,10)" |
| 165 | " END AS exp," |
| 166 | /* 7 */"atime," |
| 167 | /* 8 */"user.mtime AS sorttime," |
| 168 | /*9-11*/"%s" |
| 169 | " FROM user LEFT JOIN lastAccess ON login=uname" |
| 170 | " LEFT JOIN subscriber ON login=suname" |
| 171 | " WHERE login NOT IN ('anonymous','nobody','developer','reader') %s" |
| 172 | " ORDER BY sorttime DESC", |
| 173 | bHaveAlerts |
| 174 | ? "subscriber.ssub, subscriber.subscriberId, subscriber.semail" |
| 175 | : "null, null, null", |
| 176 | zWith/*safe-for-%s*/ |
| 177 | ); |
| 178 | rNow = db_double(0.0, "SELECT julianday('now');"); |
| 179 | while( db_step(&s)==SQLITE_ROW ){ |
| 180 | int uid = db_column_int(&s, 0); |
| 181 | const char *zLogin = db_column_text(&s, 1); |
| @@ -180,12 +185,12 @@ | |
| 185 | const char *zSortKey = db_column_text(&s,5); |
| 186 | const char *zExp = db_column_text(&s,6); |
| 187 | double rATime = db_column_double(&s,7); |
| 188 | char *zAge = 0; |
| 189 | const char *zSub; |
| 190 | int sid = db_column_int(&s,10); |
| 191 | sqlite3_int64 sorttime = db_column_int64(&s, 8); |
| 192 | if( rATime>0.0 ){ |
| 193 | zAge = human_readable_age(rNow - rATime); |
| 194 | } |
| 195 | if( bUbg ){ |
| 196 | @ <tr style='background-color: %h(user_color(zLogin));'> |
| @@ -197,16 +202,18 @@ | |
| 202 | @ <td>%h(zCap) |
| 203 | @ <td>%h(zInfo) |
| 204 | @ <td data-sortkey='%09llx(sorttime)'>%h(zDate?zDate:"") |
| 205 | @ <td>%h(zExp?zExp:"") |
| 206 | @ <td data-sortkey='%f(rATime)' style='white-space:nowrap'>%s(zAge?zAge:"") |
| 207 | if( db_column_type(&s,9)==SQLITE_NULL ){ |
| 208 | @ <td> |
| 209 | }else if( (zSub = db_column_text(&s,9))==0 || zSub[0]==0 ){ |
| 210 | @ <td><a href="%R/alerts?sid=%d(sid)"><i>off</i></a> |
| 211 | }else{ |
| 212 | const char *zEmail = db_column_text(&s, 11); |
| 213 | char * zAt = zEmail ? mprintf(" → %h", zEmail) : mprintf(""); |
| 214 | @ <td><a href="%R/alerts?sid=%d(sid)">%h(zSub)</a> %z(zAt) |
| 215 | } |
| 216 | |
| 217 | @ </tr> |
| 218 | fossil_free(zAge); |
| 219 | } |
| @@ -304,22 +311,59 @@ | |
| 311 | while( zPw[0]=='*' ){ zPw++; } |
| 312 | return zPw[0]!=0; |
| 313 | } |
| 314 | |
| 315 | /* |
| 316 | ** Return true if user capability strings zOrig and zNew materially |
| 317 | ** differ, taking into account that they may be sorted in an arbitary |
| 318 | ** order. This does not take inherited permissions into |
| 319 | ** account. Either argument may be NULL. A NULL and an empty string |
| 320 | ** are considered equivalent here. e.g. "abc" and "cab" are equivalent |
| 321 | ** for this purpose, but "aCb" and "acb" are not. |
| 322 | */ |
| 323 | static int userCapsChanged(const char *zOrig, const char *zNew){ |
| 324 | if( !zOrig ){ |
| 325 | return zNew ? (0!=*zNew) : 0; |
| 326 | }else if( !zNew ){ |
| 327 | return 0!=*zOrig; |
| 328 | }else if( 0==fossil_strcmp(zOrig, zNew) ){ |
| 329 | return 0; |
| 330 | }else{ |
| 331 | /* We don't know that zOrig and zNew are sorted equivalently. The |
| 332 | ** following steps will compare strings which contain all the same |
| 333 | ** capabilities letters as equivalent, regardless of the letters' |
| 334 | ** order in their strings. */ |
| 335 | char aOrig[128]; /* table of zOrig bytes */ |
| 336 | int nOrig = 0, nNew = 0; |
| 337 | |
| 338 | memset( &aOrig[0], 0, sizeof(aOrig) ); |
| 339 | for( ; *zOrig; ++zOrig, ++nOrig ){ |
| 340 | if( 0==(*zOrig & 0x80) ){ |
| 341 | aOrig[(int)*zOrig] = 1; |
| 342 | } |
| 343 | } |
| 344 | for( ; *zNew; ++zNew, ++nNew ){ |
| 345 | if( 0==(*zNew & 0x80) && !aOrig[(int)*zNew] ){ |
| 346 | return 1; |
| 347 | } |
| 348 | } |
| 349 | return nOrig!=nNew; |
| 350 | } |
| 351 | } |
| 352 | |
| 353 | /* |
| 354 | ** COMMAND: test-user-caps-changed |
| 355 | ** |
| 356 | ** Usage: %fossil test-user-caps-changed caps1 caps2 |
| 357 | ** |
| 358 | */ |
| 359 | void test_user_caps_changed(void){ |
| 360 | |
| 361 | char const * zOld = g.argc>2 ? g.argv[2] : NULL; |
| 362 | char const * zNew = g.argc>3 ? g.argv[3] : NULL; |
| 363 | fossil_print("Has changes? = %d\n", |
| 364 | userCapsChanged( zOld, zNew )); |
| 365 | } |
| 366 | |
| 367 | /* |
| 368 | ** Sends notification of user permission elevation changes to all |
| 369 | ** subscribers with a "u" subscription. This is a no-op if alerts are |
| @@ -333,11 +377,11 @@ | |
| 377 | ** edits their subscriptions after an admin assigns them this one, |
| 378 | ** this particular one will be lost. "Feature or bug?" is unclear, |
| 379 | ** but it would be odd for a non-admin to be assigned this |
| 380 | ** capability. |
| 381 | */ |
| 382 | static void alert_user_cap_change(const char *zLogin, /*Affected user*/ |
| 383 | int uid, /*[user].uid*/ |
| 384 | int bIsNew, /*true if new user*/ |
| 385 | const char *zOrigCaps,/*Old caps*/ |
| 386 | const char *zNewCaps /*New caps*/){ |
| 387 | Blob hdr, body; |
| @@ -349,21 +393,21 @@ | |
| 393 | char * zSubject; |
| 394 | |
| 395 | if( !alert_enabled() ) return; |
| 396 | zSubject = bIsNew |
| 397 | ? mprintf("New user created: [%q]", zLogin) |
| 398 | : mprintf("User [%q] capabilities changed", zLogin); |
| 399 | zURL = db_get("email-url",0); |
| 400 | zSubname = db_get("email-subname", "[Fossil Repo]"); |
| 401 | blob_init(&body, 0, 0); |
| 402 | blob_init(&hdr, 0, 0); |
| 403 | if( bIsNew ){ |
| 404 | blob_appendf(&body, "User [%q] was created with " |
| 405 | "permissions [%q] by user [%q].\n", |
| 406 | zLogin, zNewCaps, g.zLogin); |
| 407 | } else { |
| 408 | blob_appendf(&body, "Permissions for user [%q] where changed " |
| 409 | "from [%q] to [%q] by user [%q].\n", |
| 410 | zLogin, zOrigCaps, zNewCaps, g.zLogin); |
| 411 | } |
| 412 | if( zURL ){ |
| 413 | blob_appendf(&body, "\nUser editor: %s/setup_uedit?uid=%d\n", zURL, uid); |
| @@ -486,11 +530,11 @@ | |
| 530 | }else if( !cgi_csrf_safe(2) ){ |
| 531 | /* This might be a cross-site request forgery, so ignore it */ |
| 532 | }else{ |
| 533 | /* We have all the information we need to make the change to the user */ |
| 534 | char c; |
| 535 | int bCapsChanged = 0 /* 1 if user's permissions changed */; |
| 536 | const int bIsNew = uid<=0; |
| 537 | char aCap[70], zNm[4]; |
| 538 | zNm[0] = 'a'; |
| 539 | zNm[2] = 0; |
| 540 | for(i=0, c='a'; c<='z'; c++){ |
| @@ -508,11 +552,11 @@ | |
| 552 | a[c&0x7f] = P(zNm)!=0; |
| 553 | if( a[c&0x7f] ) aCap[i++] = c; |
| 554 | } |
| 555 | |
| 556 | aCap[i] = 0; |
| 557 | bCapsChanged = bIsNew || userCapsChanged(zOldCaps, &aCap[0]); |
| 558 | zPw = P("pw"); |
| 559 | zLogin = P("login"); |
| 560 | if( strlen(zLogin)==0 ){ |
| 561 | const char *zRef = cgi_referer("setup_ulist"); |
| 562 | style_header("User Creation Error"); |
| @@ -613,18 +657,20 @@ | |
| 657 | @ <span class="loginError">%h(zErr)</span> |
| 658 | @ |
| 659 | @ <p><a href="setup_uedit?id=%d(uid)&referer=%T(zRef)"> |
| 660 | @ [Bummer]</a></p> |
| 661 | style_finish_page(); |
| 662 | if( bCapsChanged ){ |
| 663 | /* It's possible that caps were updated locally even if |
| 664 | ** login group updates failed. */ |
| 665 | alert_user_cap_change(zLogin, uid, bIsNew, zOldCaps, &aCap[0]); |
| 666 | } |
| 667 | return; |
| 668 | } |
| 669 | } |
| 670 | if( bCapsChanged ){ |
| 671 | alert_user_cap_change(zLogin, uid, bIsNew, zOldCaps, &aCap[0]); |
| 672 | } |
| 673 | cgi_redirect(cgi_referer("setup_ulist")); |
| 674 | return; |
| 675 | } |
| 676 | |
| 677 |
+4
-3
| --- src/shun.c | ||
| +++ src/shun.c | ||
| @@ -373,11 +373,11 @@ | ||
| 373 | 373 | login_check_credentials(); |
| 374 | 374 | if( !g.perm.Admin ){ |
| 375 | 375 | login_needed(0); |
| 376 | 376 | return; |
| 377 | 377 | } |
| 378 | - style_header("Artifact Receipts"); | |
| 378 | + style_header("Xfer Log"); | |
| 379 | 379 | style_submenu_element("Log-Menu", "setup-logmenu"); |
| 380 | 380 | if( showAll ){ |
| 381 | 381 | ofst = 0; |
| 382 | 382 | }else{ |
| 383 | 383 | style_submenu_element("All", "rcvfromlist?all=1"); |
| @@ -415,12 +415,13 @@ | ||
| 415 | 415 | " FROM rcvfrom LEFT JOIN user USING(uid)" |
| 416 | 416 | " ORDER BY rcvid DESC LIMIT %d OFFSET %d", |
| 417 | 417 | showAll ? -1 : perScreen+1, ofst |
| 418 | 418 | ); |
| 419 | 419 | @ <p>Whenever new artifacts are added to the repository, either by |
| 420 | - @ push or using the web interface, an entry is made in the RCVFROM table | |
| 421 | - @ to record the source of that artifact. This log facilitates | |
| 420 | + @ push or using the web interface or by "fossil commit" or similar, | |
| 421 | + @ an entry is made in the RCVFROM table | |
| 422 | + @ to record the source of those artifacts. This log facilitates | |
| 422 | 423 | @ finding and fixing attempts to inject illicit content into the |
| 423 | 424 | @ repository.</p> |
| 424 | 425 | @ |
| 425 | 426 | @ <p>Click on the "rcvid" to show a list of specific artifacts received |
| 426 | 427 | @ by a transaction. After identifying illicit artifacts, remove them |
| 427 | 428 |
| --- src/shun.c | |
| +++ src/shun.c | |
| @@ -373,11 +373,11 @@ | |
| 373 | login_check_credentials(); |
| 374 | if( !g.perm.Admin ){ |
| 375 | login_needed(0); |
| 376 | return; |
| 377 | } |
| 378 | style_header("Artifact Receipts"); |
| 379 | style_submenu_element("Log-Menu", "setup-logmenu"); |
| 380 | if( showAll ){ |
| 381 | ofst = 0; |
| 382 | }else{ |
| 383 | style_submenu_element("All", "rcvfromlist?all=1"); |
| @@ -415,12 +415,13 @@ | |
| 415 | " FROM rcvfrom LEFT JOIN user USING(uid)" |
| 416 | " ORDER BY rcvid DESC LIMIT %d OFFSET %d", |
| 417 | showAll ? -1 : perScreen+1, ofst |
| 418 | ); |
| 419 | @ <p>Whenever new artifacts are added to the repository, either by |
| 420 | @ push or using the web interface, an entry is made in the RCVFROM table |
| 421 | @ to record the source of that artifact. This log facilitates |
| 422 | @ finding and fixing attempts to inject illicit content into the |
| 423 | @ repository.</p> |
| 424 | @ |
| 425 | @ <p>Click on the "rcvid" to show a list of specific artifacts received |
| 426 | @ by a transaction. After identifying illicit artifacts, remove them |
| 427 |
| --- src/shun.c | |
| +++ src/shun.c | |
| @@ -373,11 +373,11 @@ | |
| 373 | login_check_credentials(); |
| 374 | if( !g.perm.Admin ){ |
| 375 | login_needed(0); |
| 376 | return; |
| 377 | } |
| 378 | style_header("Xfer Log"); |
| 379 | style_submenu_element("Log-Menu", "setup-logmenu"); |
| 380 | if( showAll ){ |
| 381 | ofst = 0; |
| 382 | }else{ |
| 383 | style_submenu_element("All", "rcvfromlist?all=1"); |
| @@ -415,12 +415,13 @@ | |
| 415 | " FROM rcvfrom LEFT JOIN user USING(uid)" |
| 416 | " ORDER BY rcvid DESC LIMIT %d OFFSET %d", |
| 417 | showAll ? -1 : perScreen+1, ofst |
| 418 | ); |
| 419 | @ <p>Whenever new artifacts are added to the repository, either by |
| 420 | @ push or using the web interface or by "fossil commit" or similar, |
| 421 | @ an entry is made in the RCVFROM table |
| 422 | @ to record the source of those artifacts. This log facilitates |
| 423 | @ finding and fixing attempts to inject illicit content into the |
| 424 | @ repository.</p> |
| 425 | @ |
| 426 | @ <p>Click on the "rcvid" to show a list of specific artifacts received |
| 427 | @ by a transaction. After identifying illicit artifacts, remove them |
| 428 |
+2
-1
| --- src/sitemap.c | ||
| +++ src/sitemap.c | ||
| @@ -285,18 +285,19 @@ | ||
| 285 | 285 | style_header("Test Page Map"); |
| 286 | 286 | style_adunit_config(ADUNIT_RIGHT_OK); |
| 287 | 287 | } |
| 288 | 288 | @ <ul id="sitemap" class="columns" style="column-width:20em"> |
| 289 | 289 | if( g.perm.Admin || db_get_boolean("test_env_enable",0) ){ |
| 290 | - @ <li>%z(href("%R/test_env"))CGI Environment Test</a></li> | |
| 290 | + @ <li>%z(href("%R/test-env"))CGI Environment Test</a></li> | |
| 291 | 291 | } |
| 292 | 292 | if( g.perm.Read ){ |
| 293 | 293 | @ <li>%z(href("%R/test-rename-list"))List of file renames</a></li> |
| 294 | 294 | } |
| 295 | 295 | @ <li>%z(href("%R/test-builtin-files"))List of built-in files</a></li> |
| 296 | 296 | @ <li>%z(href("%R/mimetype_list"))List of MIME types</a></li> |
| 297 | 297 | @ <li>%z(href("%R/hash-color-test"))Hash color test</a> |
| 298 | + @ <li>%z(href("%R/test-bgcolor"))Background color test</a> | |
| 298 | 299 | if( g.perm.Admin ){ |
| 299 | 300 | @ <li>%z(href("%R/test-backlinks"))List of backlinks</a></li> |
| 300 | 301 | @ <li>%z(href("%R/test-backlink-timeline"))Backlink timeline</a></li> |
| 301 | 302 | @ <li>%z(href("%R/phantoms"))List of phantom artifacts</a></li> |
| 302 | 303 | @ <li>%z(href("%R/test-warning"))Error Log test page</a></li> |
| 303 | 304 |
| --- src/sitemap.c | |
| +++ src/sitemap.c | |
| @@ -285,18 +285,19 @@ | |
| 285 | style_header("Test Page Map"); |
| 286 | style_adunit_config(ADUNIT_RIGHT_OK); |
| 287 | } |
| 288 | @ <ul id="sitemap" class="columns" style="column-width:20em"> |
| 289 | if( g.perm.Admin || db_get_boolean("test_env_enable",0) ){ |
| 290 | @ <li>%z(href("%R/test_env"))CGI Environment Test</a></li> |
| 291 | } |
| 292 | if( g.perm.Read ){ |
| 293 | @ <li>%z(href("%R/test-rename-list"))List of file renames</a></li> |
| 294 | } |
| 295 | @ <li>%z(href("%R/test-builtin-files"))List of built-in files</a></li> |
| 296 | @ <li>%z(href("%R/mimetype_list"))List of MIME types</a></li> |
| 297 | @ <li>%z(href("%R/hash-color-test"))Hash color test</a> |
| 298 | if( g.perm.Admin ){ |
| 299 | @ <li>%z(href("%R/test-backlinks"))List of backlinks</a></li> |
| 300 | @ <li>%z(href("%R/test-backlink-timeline"))Backlink timeline</a></li> |
| 301 | @ <li>%z(href("%R/phantoms"))List of phantom artifacts</a></li> |
| 302 | @ <li>%z(href("%R/test-warning"))Error Log test page</a></li> |
| 303 |
| --- src/sitemap.c | |
| +++ src/sitemap.c | |
| @@ -285,18 +285,19 @@ | |
| 285 | style_header("Test Page Map"); |
| 286 | style_adunit_config(ADUNIT_RIGHT_OK); |
| 287 | } |
| 288 | @ <ul id="sitemap" class="columns" style="column-width:20em"> |
| 289 | if( g.perm.Admin || db_get_boolean("test_env_enable",0) ){ |
| 290 | @ <li>%z(href("%R/test-env"))CGI Environment Test</a></li> |
| 291 | } |
| 292 | if( g.perm.Read ){ |
| 293 | @ <li>%z(href("%R/test-rename-list"))List of file renames</a></li> |
| 294 | } |
| 295 | @ <li>%z(href("%R/test-builtin-files"))List of built-in files</a></li> |
| 296 | @ <li>%z(href("%R/mimetype_list"))List of MIME types</a></li> |
| 297 | @ <li>%z(href("%R/hash-color-test"))Hash color test</a> |
| 298 | @ <li>%z(href("%R/test-bgcolor"))Background color test</a> |
| 299 | if( g.perm.Admin ){ |
| 300 | @ <li>%z(href("%R/test-backlinks"))List of backlinks</a></li> |
| 301 | @ <li>%z(href("%R/test-backlink-timeline"))Backlink timeline</a></li> |
| 302 | @ <li>%z(href("%R/phantoms"))List of phantom artifacts</a></li> |
| 303 | @ <li>%z(href("%R/test-warning"))Error Log test page</a></li> |
| 304 |
+98
-48
| --- src/smtp.c | ||
| +++ src/smtp.c | ||
| @@ -156,13 +156,15 @@ | ||
| 156 | 156 | const char *zDest; /* Domain that will receive the email */ |
| 157 | 157 | char *zHostname; /* Hostname of SMTP server for zDest */ |
| 158 | 158 | u32 smtpFlags; /* Flags changing the operation */ |
| 159 | 159 | FILE *logFile; /* Write session transcript to this log file */ |
| 160 | 160 | Blob *pTranscript; /* Record session transcript here */ |
| 161 | - int atEof; /* True after connection closes */ | |
| 161 | + int bOpen; /* True if connection is Open */ | |
| 162 | + int bFatal; /* Error is fatal. Do not retry */ | |
| 162 | 163 | char *zErr; /* Error message */ |
| 163 | 164 | Blob inbuf; /* Input buffer */ |
| 165 | + UrlData url; /* Address of the server */ | |
| 164 | 166 | }; |
| 165 | 167 | |
| 166 | 168 | /* Allowed values for SmtpSession.smtpFlags */ |
| 167 | 169 | #define SMTP_TRACE_STDOUT 0x00001 /* Debugging info to console */ |
| 168 | 170 | #define SMTP_TRACE_FILE 0x00002 /* Debugging info to logFile */ |
| @@ -180,86 +182,107 @@ | ||
| 180 | 182 | blob_reset(&pSession->inbuf); |
| 181 | 183 | fossil_free(pSession->zHostname); |
| 182 | 184 | fossil_free(pSession->zErr); |
| 183 | 185 | fossil_free(pSession); |
| 184 | 186 | } |
| 187 | + | |
| 188 | +/* | |
| 189 | +** Set an error message on the SmtpSession | |
| 190 | +*/ | |
| 191 | +static void smtp_set_error( | |
| 192 | + SmtpSession *p, /* The SMTP context */ | |
| 193 | + int bFatal, /* Fatal error. Reset and retry is pointless */ | |
| 194 | + const char *zFormat, /* Error message. */ | |
| 195 | + ... | |
| 196 | +){ | |
| 197 | + if( bFatal ) p->bFatal = 1; | |
| 198 | + if( p->zErr==0 ){ | |
| 199 | + va_list ap; | |
| 200 | + va_start(ap, zFormat); | |
| 201 | + p->zErr = vmprintf(zFormat, ap); | |
| 202 | + va_end(ap); | |
| 203 | + } | |
| 204 | + if( p->bOpen ){ | |
| 205 | + socket_close(); | |
| 206 | + p->bOpen = 0; | |
| 207 | + } | |
| 208 | +} | |
| 185 | 209 | |
| 186 | 210 | /* |
| 187 | 211 | ** Allocate a new SmtpSession object. |
| 188 | 212 | ** |
| 189 | -** Both zFrom and zDest must be specified. | |
| 190 | -** | |
| 191 | -** The ... arguments are in this order: | |
| 213 | +** Both zFrom and zDest must be specified. smtpFlags may not contain | |
| 214 | +** either SMTP_TRACE_FILE or SMTP_TRACE_BLOB as those settings must be | |
| 215 | +** added by a subsequent call to smtp_session_config(). | |
| 192 | 216 | ** |
| 193 | -** SMTP_PORT: int | |
| 194 | -** SMTP_TRACE_FILE: FILE* | |
| 195 | -** SMTP_TRACE_BLOB: Blob* | |
| 217 | +** The iPort option is ignored unless SMTP_PORT is set in smtpFlags | |
| 196 | 218 | */ |
| 197 | 219 | SmtpSession *smtp_session_new( |
| 198 | 220 | const char *zFrom, /* Domain for the client */ |
| 199 | 221 | const char *zDest, /* Domain of the server */ |
| 200 | 222 | u32 smtpFlags, /* Flags */ |
| 201 | - ... /* Arguments depending on the flags */ | |
| 223 | + int iPort /* TCP port if the SMTP_PORT flags is present */ | |
| 202 | 224 | ){ |
| 203 | 225 | SmtpSession *p; |
| 204 | - va_list ap; | |
| 205 | - UrlData url; | |
| 206 | 226 | |
| 207 | 227 | p = fossil_malloc( sizeof(*p) ); |
| 208 | 228 | memset(p, 0, sizeof(*p)); |
| 209 | 229 | p->zFrom = zFrom; |
| 210 | 230 | p->zDest = zDest; |
| 211 | 231 | p->smtpFlags = smtpFlags; |
| 212 | - memset(&url, 0, sizeof(url)); | |
| 213 | - url.port = 25; | |
| 232 | + p->url.port = 25; | |
| 214 | 233 | blob_init(&p->inbuf, 0, 0); |
| 215 | - va_start(ap, smtpFlags); | |
| 216 | 234 | if( smtpFlags & SMTP_PORT ){ |
| 217 | - url.port = va_arg(ap, int); | |
| 218 | - } | |
| 219 | - if( smtpFlags & SMTP_TRACE_FILE ){ | |
| 220 | - p->logFile = va_arg(ap, FILE*); | |
| 221 | - } | |
| 222 | - if( smtpFlags & SMTP_TRACE_BLOB ){ | |
| 223 | - p->pTranscript = va_arg(ap, Blob*); | |
| 224 | - } | |
| 225 | - va_end(ap); | |
| 235 | + p->url.port = iPort; | |
| 236 | + } | |
| 226 | 237 | if( (smtpFlags & SMTP_DIRECT)!=0 ){ |
| 227 | 238 | int i; |
| 228 | 239 | p->zHostname = fossil_strdup(zDest); |
| 229 | 240 | for(i=0; p->zHostname[i] && p->zHostname[i]!=':'; i++){} |
| 230 | 241 | if( p->zHostname[i]==':' ){ |
| 231 | 242 | p->zHostname[i] = 0; |
| 232 | - url.port = atoi(&p->zHostname[i+1]); | |
| 243 | + p->url.port = atoi(&p->zHostname[i+1]); | |
| 233 | 244 | } |
| 234 | 245 | }else{ |
| 235 | 246 | p->zHostname = smtp_mx_host(zDest); |
| 236 | 247 | } |
| 237 | 248 | if( p->zHostname==0 ){ |
| 238 | - p->atEof = 1; | |
| 239 | - p->zErr = mprintf("cannot locate SMTP server for \"%s\"", zDest); | |
| 249 | + smtp_set_error(p, 1, "cannot locate SMTP server for \"%s\"", zDest); | |
| 240 | 250 | return p; |
| 241 | 251 | } |
| 242 | - url.name = p->zHostname; | |
| 252 | + p->url.name = p->zHostname; | |
| 243 | 253 | socket_global_init(); |
| 244 | - if( socket_open(&url) ){ | |
| 245 | - p->atEof = 1; | |
| 246 | - p->zErr = socket_errmsg(); | |
| 247 | - socket_close(); | |
| 248 | - } | |
| 254 | + p->bOpen = 0; | |
| 249 | 255 | return p; |
| 250 | 256 | } |
| 257 | + | |
| 258 | +/* | |
| 259 | +** Configure debugging options on SmtpSession. Add all bits in | |
| 260 | +** smtpFlags to the settings. The following bits can be added: | |
| 261 | +** | |
| 262 | +** SMTP_FLAG_FILE: In which case pArg is the FILE* pointer to use | |
| 263 | +** | |
| 264 | +** SMTP_FLAG_BLOB: In which case pArg is the Blob* poitner to use. | |
| 265 | +*/ | |
| 266 | +void smtp_session_config(SmtpSession *p, u32 smtpFlags, void *pArg){ | |
| 267 | + p->smtpFlags = smtpFlags; | |
| 268 | + if( smtpFlags & SMTP_TRACE_FILE ){ | |
| 269 | + p->logFile = (FILE*)pArg; | |
| 270 | + }else if( smtpFlags & SMTP_TRACE_BLOB ){ | |
| 271 | + p->pTranscript = (Blob*)pArg; | |
| 272 | + } | |
| 273 | +} | |
| 251 | 274 | |
| 252 | 275 | /* |
| 253 | 276 | ** Send a single line of output the SMTP client to the server. |
| 254 | 277 | */ |
| 255 | 278 | static void smtp_send_line(SmtpSession *p, const char *zFormat, ...){ |
| 256 | 279 | Blob b = empty_blob; |
| 257 | 280 | va_list ap; |
| 258 | 281 | char *z; |
| 259 | 282 | int n; |
| 260 | - if( p->atEof ) return; | |
| 283 | + if( !p->bOpen ) return; | |
| 261 | 284 | va_start(ap, zFormat); |
| 262 | 285 | blob_vappendf(&b, zFormat, ap); |
| 263 | 286 | va_end(ap); |
| 264 | 287 | z = blob_buffer(&b); |
| 265 | 288 | n = blob_size(&b); |
| @@ -291,11 +314,11 @@ | ||
| 291 | 314 | char *z = blob_buffer(&p->inbuf); |
| 292 | 315 | int i = blob_tell(&p->inbuf); |
| 293 | 316 | int nDelay = 0; |
| 294 | 317 | if( i<n && z[n-1]=='\n' ){ |
| 295 | 318 | blob_line(&p->inbuf, in); |
| 296 | - }else if( p->atEof ){ | |
| 319 | + }else if( !p->bOpen ){ | |
| 297 | 320 | blob_init(in, 0, 0); |
| 298 | 321 | }else{ |
| 299 | 322 | if( n>0 && i>=n ){ |
| 300 | 323 | blob_truncate(&p->inbuf, 0); |
| 301 | 324 | blob_rewind(&p->inbuf); |
| @@ -314,13 +337,11 @@ | ||
| 314 | 337 | if( got==1000 ) continue; |
| 315 | 338 | } |
| 316 | 339 | nDelay++; |
| 317 | 340 | if( nDelay>100 ){ |
| 318 | 341 | blob_init(in, 0, 0); |
| 319 | - p->zErr = mprintf("timeout"); | |
| 320 | - socket_close(); | |
| 321 | - p->atEof = 1; | |
| 342 | + smtp_set_error(p, 1, "client times out waiting on server response"); | |
| 322 | 343 | return; |
| 323 | 344 | }else{ |
| 324 | 345 | sqlite3_sleep(100); |
| 325 | 346 | } |
| 326 | 347 | }while( n<1 || z[n-1]!='\n' ); |
| @@ -354,10 +375,11 @@ | ||
| 354 | 375 | ){ |
| 355 | 376 | int n; |
| 356 | 377 | char *z; |
| 357 | 378 | blob_truncate(in, 0); |
| 358 | 379 | smtp_recv_line(p, in); |
| 380 | + blob_trim(in); | |
| 359 | 381 | z = blob_str(in); |
| 360 | 382 | n = blob_size(in); |
| 361 | 383 | if( z[0]=='#' ){ |
| 362 | 384 | *piCode = 0; |
| 363 | 385 | *pbMore = 1; |
| @@ -375,45 +397,57 @@ | ||
| 375 | 397 | int smtp_client_quit(SmtpSession *p){ |
| 376 | 398 | Blob in = BLOB_INITIALIZER; |
| 377 | 399 | int iCode = 0; |
| 378 | 400 | int bMore = 0; |
| 379 | 401 | char *zArg = 0; |
| 380 | - smtp_send_line(p, "QUIT\r\n"); | |
| 381 | - do{ | |
| 382 | - smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg); | |
| 383 | - }while( bMore ); | |
| 384 | - p->atEof = 1; | |
| 385 | - socket_close(); | |
| 402 | + if( p->bOpen ){ | |
| 403 | + smtp_send_line(p, "QUIT\r\n"); | |
| 404 | + do{ | |
| 405 | + smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg); | |
| 406 | + }while( bMore ); | |
| 407 | + p->bOpen = 0; | |
| 408 | + socket_close(); | |
| 409 | + } | |
| 386 | 410 | return 0; |
| 387 | 411 | } |
| 388 | 412 | |
| 389 | 413 | /* |
| 390 | 414 | ** Begin a client SMTP session. Wait for the initial 220 then send |
| 391 | 415 | ** the EHLO and wait for a 250. |
| 392 | 416 | ** |
| 393 | 417 | ** Return 0 on success and non-zero for a failure. |
| 394 | 418 | */ |
| 395 | -int smtp_client_startup(SmtpSession *p){ | |
| 419 | +static int smtp_client_startup(SmtpSession *p){ | |
| 396 | 420 | Blob in = BLOB_INITIALIZER; |
| 397 | 421 | int iCode = 0; |
| 398 | 422 | int bMore = 0; |
| 399 | 423 | char *zArg = 0; |
| 424 | + if( p==0 || p->bFatal ) return 1; | |
| 425 | + if( socket_open(&p->url) ){ | |
| 426 | + smtp_set_error(p, 1, "can't open socket: %z", socket_errmsg()); | |
| 427 | + return 1; | |
| 428 | + } | |
| 429 | + p->bOpen = 1; | |
| 400 | 430 | do{ |
| 401 | 431 | smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg); |
| 402 | 432 | }while( bMore ); |
| 403 | 433 | if( iCode!=220 ){ |
| 434 | + smtp_set_error(p, 1, "conversation begins with: \"%d %s\"",iCode,zArg); | |
| 404 | 435 | smtp_client_quit(p); |
| 405 | 436 | return 1; |
| 406 | 437 | } |
| 407 | 438 | smtp_send_line(p, "EHLO %s\r\n", p->zFrom); |
| 408 | 439 | do{ |
| 409 | 440 | smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg); |
| 410 | 441 | }while( bMore ); |
| 411 | 442 | if( iCode!=250 ){ |
| 443 | + smtp_set_error(p, 1, "reply to EHLO with: \"%d %s\"",iCode, zArg); | |
| 412 | 444 | smtp_client_quit(p); |
| 413 | 445 | return 1; |
| 414 | 446 | } |
| 447 | + fossil_free(p->zErr); | |
| 448 | + p->zErr = 0; | |
| 415 | 449 | return 0; |
| 416 | 450 | } |
| 417 | 451 | |
| 418 | 452 | /* |
| 419 | 453 | ** COMMAND: test-smtp-probe |
| @@ -541,27 +575,40 @@ | ||
| 541 | 575 | int iCode = 0; |
| 542 | 576 | int bMore = 0; |
| 543 | 577 | char *zArg = 0; |
| 544 | 578 | Blob in; |
| 545 | 579 | blob_init(&in, 0, 0); |
| 580 | + if( !p->bOpen ){ | |
| 581 | + if( !p->bFatal ) smtp_client_startup(p); | |
| 582 | + if( !p->bOpen ) return 1; | |
| 583 | + } | |
| 546 | 584 | smtp_send_line(p, "MAIL FROM:<%s>\r\n", zFrom); |
| 547 | 585 | do{ |
| 548 | 586 | smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg); |
| 549 | 587 | }while( bMore ); |
| 550 | - if( iCode!=250 ) return 1; | |
| 588 | + if( iCode!=250 ){ | |
| 589 | + smtp_set_error(p, 0,"reply to MAIL FROM: \"%d %s\"",iCode,zArg); | |
| 590 | + return 1; | |
| 591 | + } | |
| 551 | 592 | for(i=0; i<nTo; i++){ |
| 552 | 593 | smtp_send_line(p, "RCPT TO:<%s>\r\n", azTo[i]); |
| 553 | 594 | do{ |
| 554 | 595 | smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg); |
| 555 | 596 | }while( bMore ); |
| 556 | - if( iCode!=250 ) return 1; | |
| 597 | + if( iCode!=250 ){ | |
| 598 | + smtp_set_error(p, 0,"reply to RCPT TO: \"%d %s\"",iCode,zArg); | |
| 599 | + return 1; | |
| 600 | + } | |
| 557 | 601 | } |
| 558 | 602 | smtp_send_line(p, "DATA\r\n"); |
| 559 | 603 | do{ |
| 560 | 604 | smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg); |
| 561 | 605 | }while( bMore ); |
| 562 | - if( iCode!=354 ) return 1; | |
| 606 | + if( iCode!=354 ){ | |
| 607 | + smtp_set_error(p, 0, "reply to DATA with: \"%d %s\"",iCode,zArg); | |
| 608 | + return 1; | |
| 609 | + } | |
| 563 | 610 | smtp_send_email_body(zMsg, socket_send, 0); |
| 564 | 611 | if( p->smtpFlags & SMTP_TRACE_STDOUT ){ |
| 565 | 612 | fossil_print("C: # message content\nC: .\n"); |
| 566 | 613 | } |
| 567 | 614 | if( p->smtpFlags & SMTP_TRACE_FILE ){ |
| @@ -571,11 +618,15 @@ | ||
| 571 | 618 | blob_appendf(p->pTranscript, "C: # message content\nC: .\n"); |
| 572 | 619 | } |
| 573 | 620 | do{ |
| 574 | 621 | smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg); |
| 575 | 622 | }while( bMore ); |
| 576 | - if( iCode!=250 ) return 1; | |
| 623 | + if( iCode!=250 ){ | |
| 624 | + smtp_set_error(p, 0, "reply to end-of-DATA with: \"%d %s\"", | |
| 625 | + iCode, zArg); | |
| 626 | + return 1; | |
| 627 | + } | |
| 577 | 628 | return 0; |
| 578 | 629 | } |
| 579 | 630 | |
| 580 | 631 | /* |
| 581 | 632 | ** The input is a base email address of the form "local@domain". |
| @@ -636,14 +687,13 @@ | ||
| 636 | 687 | p = smtp_session_new(zFromDomain, zToDomain, smtpFlags, smtpPort); |
| 637 | 688 | if( p->zErr ){ |
| 638 | 689 | fossil_fatal("%s", p->zErr); |
| 639 | 690 | } |
| 640 | 691 | fossil_print("Connection to \"%s\"\n", p->zHostname); |
| 641 | - smtp_client_startup(p); | |
| 642 | 692 | smtp_send_msg(p, zFrom, nTo, azTo, blob_str(&body)); |
| 643 | 693 | smtp_client_quit(p); |
| 644 | 694 | if( p->zErr ){ |
| 645 | 695 | fossil_fatal("ERROR: %s\n", p->zErr); |
| 646 | 696 | } |
| 647 | 697 | smtp_session_free(p); |
| 648 | 698 | blob_reset(&body); |
| 649 | 699 | } |
| 650 | 700 |
| --- src/smtp.c | |
| +++ src/smtp.c | |
| @@ -156,13 +156,15 @@ | |
| 156 | const char *zDest; /* Domain that will receive the email */ |
| 157 | char *zHostname; /* Hostname of SMTP server for zDest */ |
| 158 | u32 smtpFlags; /* Flags changing the operation */ |
| 159 | FILE *logFile; /* Write session transcript to this log file */ |
| 160 | Blob *pTranscript; /* Record session transcript here */ |
| 161 | int atEof; /* True after connection closes */ |
| 162 | char *zErr; /* Error message */ |
| 163 | Blob inbuf; /* Input buffer */ |
| 164 | }; |
| 165 | |
| 166 | /* Allowed values for SmtpSession.smtpFlags */ |
| 167 | #define SMTP_TRACE_STDOUT 0x00001 /* Debugging info to console */ |
| 168 | #define SMTP_TRACE_FILE 0x00002 /* Debugging info to logFile */ |
| @@ -180,86 +182,107 @@ | |
| 180 | blob_reset(&pSession->inbuf); |
| 181 | fossil_free(pSession->zHostname); |
| 182 | fossil_free(pSession->zErr); |
| 183 | fossil_free(pSession); |
| 184 | } |
| 185 | |
| 186 | /* |
| 187 | ** Allocate a new SmtpSession object. |
| 188 | ** |
| 189 | ** Both zFrom and zDest must be specified. |
| 190 | ** |
| 191 | ** The ... arguments are in this order: |
| 192 | ** |
| 193 | ** SMTP_PORT: int |
| 194 | ** SMTP_TRACE_FILE: FILE* |
| 195 | ** SMTP_TRACE_BLOB: Blob* |
| 196 | */ |
| 197 | SmtpSession *smtp_session_new( |
| 198 | const char *zFrom, /* Domain for the client */ |
| 199 | const char *zDest, /* Domain of the server */ |
| 200 | u32 smtpFlags, /* Flags */ |
| 201 | ... /* Arguments depending on the flags */ |
| 202 | ){ |
| 203 | SmtpSession *p; |
| 204 | va_list ap; |
| 205 | UrlData url; |
| 206 | |
| 207 | p = fossil_malloc( sizeof(*p) ); |
| 208 | memset(p, 0, sizeof(*p)); |
| 209 | p->zFrom = zFrom; |
| 210 | p->zDest = zDest; |
| 211 | p->smtpFlags = smtpFlags; |
| 212 | memset(&url, 0, sizeof(url)); |
| 213 | url.port = 25; |
| 214 | blob_init(&p->inbuf, 0, 0); |
| 215 | va_start(ap, smtpFlags); |
| 216 | if( smtpFlags & SMTP_PORT ){ |
| 217 | url.port = va_arg(ap, int); |
| 218 | } |
| 219 | if( smtpFlags & SMTP_TRACE_FILE ){ |
| 220 | p->logFile = va_arg(ap, FILE*); |
| 221 | } |
| 222 | if( smtpFlags & SMTP_TRACE_BLOB ){ |
| 223 | p->pTranscript = va_arg(ap, Blob*); |
| 224 | } |
| 225 | va_end(ap); |
| 226 | if( (smtpFlags & SMTP_DIRECT)!=0 ){ |
| 227 | int i; |
| 228 | p->zHostname = fossil_strdup(zDest); |
| 229 | for(i=0; p->zHostname[i] && p->zHostname[i]!=':'; i++){} |
| 230 | if( p->zHostname[i]==':' ){ |
| 231 | p->zHostname[i] = 0; |
| 232 | url.port = atoi(&p->zHostname[i+1]); |
| 233 | } |
| 234 | }else{ |
| 235 | p->zHostname = smtp_mx_host(zDest); |
| 236 | } |
| 237 | if( p->zHostname==0 ){ |
| 238 | p->atEof = 1; |
| 239 | p->zErr = mprintf("cannot locate SMTP server for \"%s\"", zDest); |
| 240 | return p; |
| 241 | } |
| 242 | url.name = p->zHostname; |
| 243 | socket_global_init(); |
| 244 | if( socket_open(&url) ){ |
| 245 | p->atEof = 1; |
| 246 | p->zErr = socket_errmsg(); |
| 247 | socket_close(); |
| 248 | } |
| 249 | return p; |
| 250 | } |
| 251 | |
| 252 | /* |
| 253 | ** Send a single line of output the SMTP client to the server. |
| 254 | */ |
| 255 | static void smtp_send_line(SmtpSession *p, const char *zFormat, ...){ |
| 256 | Blob b = empty_blob; |
| 257 | va_list ap; |
| 258 | char *z; |
| 259 | int n; |
| 260 | if( p->atEof ) return; |
| 261 | va_start(ap, zFormat); |
| 262 | blob_vappendf(&b, zFormat, ap); |
| 263 | va_end(ap); |
| 264 | z = blob_buffer(&b); |
| 265 | n = blob_size(&b); |
| @@ -291,11 +314,11 @@ | |
| 291 | char *z = blob_buffer(&p->inbuf); |
| 292 | int i = blob_tell(&p->inbuf); |
| 293 | int nDelay = 0; |
| 294 | if( i<n && z[n-1]=='\n' ){ |
| 295 | blob_line(&p->inbuf, in); |
| 296 | }else if( p->atEof ){ |
| 297 | blob_init(in, 0, 0); |
| 298 | }else{ |
| 299 | if( n>0 && i>=n ){ |
| 300 | blob_truncate(&p->inbuf, 0); |
| 301 | blob_rewind(&p->inbuf); |
| @@ -314,13 +337,11 @@ | |
| 314 | if( got==1000 ) continue; |
| 315 | } |
| 316 | nDelay++; |
| 317 | if( nDelay>100 ){ |
| 318 | blob_init(in, 0, 0); |
| 319 | p->zErr = mprintf("timeout"); |
| 320 | socket_close(); |
| 321 | p->atEof = 1; |
| 322 | return; |
| 323 | }else{ |
| 324 | sqlite3_sleep(100); |
| 325 | } |
| 326 | }while( n<1 || z[n-1]!='\n' ); |
| @@ -354,10 +375,11 @@ | |
| 354 | ){ |
| 355 | int n; |
| 356 | char *z; |
| 357 | blob_truncate(in, 0); |
| 358 | smtp_recv_line(p, in); |
| 359 | z = blob_str(in); |
| 360 | n = blob_size(in); |
| 361 | if( z[0]=='#' ){ |
| 362 | *piCode = 0; |
| 363 | *pbMore = 1; |
| @@ -375,45 +397,57 @@ | |
| 375 | int smtp_client_quit(SmtpSession *p){ |
| 376 | Blob in = BLOB_INITIALIZER; |
| 377 | int iCode = 0; |
| 378 | int bMore = 0; |
| 379 | char *zArg = 0; |
| 380 | smtp_send_line(p, "QUIT\r\n"); |
| 381 | do{ |
| 382 | smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg); |
| 383 | }while( bMore ); |
| 384 | p->atEof = 1; |
| 385 | socket_close(); |
| 386 | return 0; |
| 387 | } |
| 388 | |
| 389 | /* |
| 390 | ** Begin a client SMTP session. Wait for the initial 220 then send |
| 391 | ** the EHLO and wait for a 250. |
| 392 | ** |
| 393 | ** Return 0 on success and non-zero for a failure. |
| 394 | */ |
| 395 | int smtp_client_startup(SmtpSession *p){ |
| 396 | Blob in = BLOB_INITIALIZER; |
| 397 | int iCode = 0; |
| 398 | int bMore = 0; |
| 399 | char *zArg = 0; |
| 400 | do{ |
| 401 | smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg); |
| 402 | }while( bMore ); |
| 403 | if( iCode!=220 ){ |
| 404 | smtp_client_quit(p); |
| 405 | return 1; |
| 406 | } |
| 407 | smtp_send_line(p, "EHLO %s\r\n", p->zFrom); |
| 408 | do{ |
| 409 | smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg); |
| 410 | }while( bMore ); |
| 411 | if( iCode!=250 ){ |
| 412 | smtp_client_quit(p); |
| 413 | return 1; |
| 414 | } |
| 415 | return 0; |
| 416 | } |
| 417 | |
| 418 | /* |
| 419 | ** COMMAND: test-smtp-probe |
| @@ -541,27 +575,40 @@ | |
| 541 | int iCode = 0; |
| 542 | int bMore = 0; |
| 543 | char *zArg = 0; |
| 544 | Blob in; |
| 545 | blob_init(&in, 0, 0); |
| 546 | smtp_send_line(p, "MAIL FROM:<%s>\r\n", zFrom); |
| 547 | do{ |
| 548 | smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg); |
| 549 | }while( bMore ); |
| 550 | if( iCode!=250 ) return 1; |
| 551 | for(i=0; i<nTo; i++){ |
| 552 | smtp_send_line(p, "RCPT TO:<%s>\r\n", azTo[i]); |
| 553 | do{ |
| 554 | smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg); |
| 555 | }while( bMore ); |
| 556 | if( iCode!=250 ) return 1; |
| 557 | } |
| 558 | smtp_send_line(p, "DATA\r\n"); |
| 559 | do{ |
| 560 | smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg); |
| 561 | }while( bMore ); |
| 562 | if( iCode!=354 ) return 1; |
| 563 | smtp_send_email_body(zMsg, socket_send, 0); |
| 564 | if( p->smtpFlags & SMTP_TRACE_STDOUT ){ |
| 565 | fossil_print("C: # message content\nC: .\n"); |
| 566 | } |
| 567 | if( p->smtpFlags & SMTP_TRACE_FILE ){ |
| @@ -571,11 +618,15 @@ | |
| 571 | blob_appendf(p->pTranscript, "C: # message content\nC: .\n"); |
| 572 | } |
| 573 | do{ |
| 574 | smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg); |
| 575 | }while( bMore ); |
| 576 | if( iCode!=250 ) return 1; |
| 577 | return 0; |
| 578 | } |
| 579 | |
| 580 | /* |
| 581 | ** The input is a base email address of the form "local@domain". |
| @@ -636,14 +687,13 @@ | |
| 636 | p = smtp_session_new(zFromDomain, zToDomain, smtpFlags, smtpPort); |
| 637 | if( p->zErr ){ |
| 638 | fossil_fatal("%s", p->zErr); |
| 639 | } |
| 640 | fossil_print("Connection to \"%s\"\n", p->zHostname); |
| 641 | smtp_client_startup(p); |
| 642 | smtp_send_msg(p, zFrom, nTo, azTo, blob_str(&body)); |
| 643 | smtp_client_quit(p); |
| 644 | if( p->zErr ){ |
| 645 | fossil_fatal("ERROR: %s\n", p->zErr); |
| 646 | } |
| 647 | smtp_session_free(p); |
| 648 | blob_reset(&body); |
| 649 | } |
| 650 |
| --- src/smtp.c | |
| +++ src/smtp.c | |
| @@ -156,13 +156,15 @@ | |
| 156 | const char *zDest; /* Domain that will receive the email */ |
| 157 | char *zHostname; /* Hostname of SMTP server for zDest */ |
| 158 | u32 smtpFlags; /* Flags changing the operation */ |
| 159 | FILE *logFile; /* Write session transcript to this log file */ |
| 160 | Blob *pTranscript; /* Record session transcript here */ |
| 161 | int bOpen; /* True if connection is Open */ |
| 162 | int bFatal; /* Error is fatal. Do not retry */ |
| 163 | char *zErr; /* Error message */ |
| 164 | Blob inbuf; /* Input buffer */ |
| 165 | UrlData url; /* Address of the server */ |
| 166 | }; |
| 167 | |
| 168 | /* Allowed values for SmtpSession.smtpFlags */ |
| 169 | #define SMTP_TRACE_STDOUT 0x00001 /* Debugging info to console */ |
| 170 | #define SMTP_TRACE_FILE 0x00002 /* Debugging info to logFile */ |
| @@ -180,86 +182,107 @@ | |
| 182 | blob_reset(&pSession->inbuf); |
| 183 | fossil_free(pSession->zHostname); |
| 184 | fossil_free(pSession->zErr); |
| 185 | fossil_free(pSession); |
| 186 | } |
| 187 | |
| 188 | /* |
| 189 | ** Set an error message on the SmtpSession |
| 190 | */ |
| 191 | static void smtp_set_error( |
| 192 | SmtpSession *p, /* The SMTP context */ |
| 193 | int bFatal, /* Fatal error. Reset and retry is pointless */ |
| 194 | const char *zFormat, /* Error message. */ |
| 195 | ... |
| 196 | ){ |
| 197 | if( bFatal ) p->bFatal = 1; |
| 198 | if( p->zErr==0 ){ |
| 199 | va_list ap; |
| 200 | va_start(ap, zFormat); |
| 201 | p->zErr = vmprintf(zFormat, ap); |
| 202 | va_end(ap); |
| 203 | } |
| 204 | if( p->bOpen ){ |
| 205 | socket_close(); |
| 206 | p->bOpen = 0; |
| 207 | } |
| 208 | } |
| 209 | |
| 210 | /* |
| 211 | ** Allocate a new SmtpSession object. |
| 212 | ** |
| 213 | ** Both zFrom and zDest must be specified. smtpFlags may not contain |
| 214 | ** either SMTP_TRACE_FILE or SMTP_TRACE_BLOB as those settings must be |
| 215 | ** added by a subsequent call to smtp_session_config(). |
| 216 | ** |
| 217 | ** The iPort option is ignored unless SMTP_PORT is set in smtpFlags |
| 218 | */ |
| 219 | SmtpSession *smtp_session_new( |
| 220 | const char *zFrom, /* Domain for the client */ |
| 221 | const char *zDest, /* Domain of the server */ |
| 222 | u32 smtpFlags, /* Flags */ |
| 223 | int iPort /* TCP port if the SMTP_PORT flags is present */ |
| 224 | ){ |
| 225 | SmtpSession *p; |
| 226 | |
| 227 | p = fossil_malloc( sizeof(*p) ); |
| 228 | memset(p, 0, sizeof(*p)); |
| 229 | p->zFrom = zFrom; |
| 230 | p->zDest = zDest; |
| 231 | p->smtpFlags = smtpFlags; |
| 232 | p->url.port = 25; |
| 233 | blob_init(&p->inbuf, 0, 0); |
| 234 | if( smtpFlags & SMTP_PORT ){ |
| 235 | p->url.port = iPort; |
| 236 | } |
| 237 | if( (smtpFlags & SMTP_DIRECT)!=0 ){ |
| 238 | int i; |
| 239 | p->zHostname = fossil_strdup(zDest); |
| 240 | for(i=0; p->zHostname[i] && p->zHostname[i]!=':'; i++){} |
| 241 | if( p->zHostname[i]==':' ){ |
| 242 | p->zHostname[i] = 0; |
| 243 | p->url.port = atoi(&p->zHostname[i+1]); |
| 244 | } |
| 245 | }else{ |
| 246 | p->zHostname = smtp_mx_host(zDest); |
| 247 | } |
| 248 | if( p->zHostname==0 ){ |
| 249 | smtp_set_error(p, 1, "cannot locate SMTP server for \"%s\"", zDest); |
| 250 | return p; |
| 251 | } |
| 252 | p->url.name = p->zHostname; |
| 253 | socket_global_init(); |
| 254 | p->bOpen = 0; |
| 255 | return p; |
| 256 | } |
| 257 | |
| 258 | /* |
| 259 | ** Configure debugging options on SmtpSession. Add all bits in |
| 260 | ** smtpFlags to the settings. The following bits can be added: |
| 261 | ** |
| 262 | ** SMTP_FLAG_FILE: In which case pArg is the FILE* pointer to use |
| 263 | ** |
| 264 | ** SMTP_FLAG_BLOB: In which case pArg is the Blob* poitner to use. |
| 265 | */ |
| 266 | void smtp_session_config(SmtpSession *p, u32 smtpFlags, void *pArg){ |
| 267 | p->smtpFlags = smtpFlags; |
| 268 | if( smtpFlags & SMTP_TRACE_FILE ){ |
| 269 | p->logFile = (FILE*)pArg; |
| 270 | }else if( smtpFlags & SMTP_TRACE_BLOB ){ |
| 271 | p->pTranscript = (Blob*)pArg; |
| 272 | } |
| 273 | } |
| 274 | |
| 275 | /* |
| 276 | ** Send a single line of output the SMTP client to the server. |
| 277 | */ |
| 278 | static void smtp_send_line(SmtpSession *p, const char *zFormat, ...){ |
| 279 | Blob b = empty_blob; |
| 280 | va_list ap; |
| 281 | char *z; |
| 282 | int n; |
| 283 | if( !p->bOpen ) return; |
| 284 | va_start(ap, zFormat); |
| 285 | blob_vappendf(&b, zFormat, ap); |
| 286 | va_end(ap); |
| 287 | z = blob_buffer(&b); |
| 288 | n = blob_size(&b); |
| @@ -291,11 +314,11 @@ | |
| 314 | char *z = blob_buffer(&p->inbuf); |
| 315 | int i = blob_tell(&p->inbuf); |
| 316 | int nDelay = 0; |
| 317 | if( i<n && z[n-1]=='\n' ){ |
| 318 | blob_line(&p->inbuf, in); |
| 319 | }else if( !p->bOpen ){ |
| 320 | blob_init(in, 0, 0); |
| 321 | }else{ |
| 322 | if( n>0 && i>=n ){ |
| 323 | blob_truncate(&p->inbuf, 0); |
| 324 | blob_rewind(&p->inbuf); |
| @@ -314,13 +337,11 @@ | |
| 337 | if( got==1000 ) continue; |
| 338 | } |
| 339 | nDelay++; |
| 340 | if( nDelay>100 ){ |
| 341 | blob_init(in, 0, 0); |
| 342 | smtp_set_error(p, 1, "client times out waiting on server response"); |
| 343 | return; |
| 344 | }else{ |
| 345 | sqlite3_sleep(100); |
| 346 | } |
| 347 | }while( n<1 || z[n-1]!='\n' ); |
| @@ -354,10 +375,11 @@ | |
| 375 | ){ |
| 376 | int n; |
| 377 | char *z; |
| 378 | blob_truncate(in, 0); |
| 379 | smtp_recv_line(p, in); |
| 380 | blob_trim(in); |
| 381 | z = blob_str(in); |
| 382 | n = blob_size(in); |
| 383 | if( z[0]=='#' ){ |
| 384 | *piCode = 0; |
| 385 | *pbMore = 1; |
| @@ -375,45 +397,57 @@ | |
| 397 | int smtp_client_quit(SmtpSession *p){ |
| 398 | Blob in = BLOB_INITIALIZER; |
| 399 | int iCode = 0; |
| 400 | int bMore = 0; |
| 401 | char *zArg = 0; |
| 402 | if( p->bOpen ){ |
| 403 | smtp_send_line(p, "QUIT\r\n"); |
| 404 | do{ |
| 405 | smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg); |
| 406 | }while( bMore ); |
| 407 | p->bOpen = 0; |
| 408 | socket_close(); |
| 409 | } |
| 410 | return 0; |
| 411 | } |
| 412 | |
| 413 | /* |
| 414 | ** Begin a client SMTP session. Wait for the initial 220 then send |
| 415 | ** the EHLO and wait for a 250. |
| 416 | ** |
| 417 | ** Return 0 on success and non-zero for a failure. |
| 418 | */ |
| 419 | static int smtp_client_startup(SmtpSession *p){ |
| 420 | Blob in = BLOB_INITIALIZER; |
| 421 | int iCode = 0; |
| 422 | int bMore = 0; |
| 423 | char *zArg = 0; |
| 424 | if( p==0 || p->bFatal ) return 1; |
| 425 | if( socket_open(&p->url) ){ |
| 426 | smtp_set_error(p, 1, "can't open socket: %z", socket_errmsg()); |
| 427 | return 1; |
| 428 | } |
| 429 | p->bOpen = 1; |
| 430 | do{ |
| 431 | smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg); |
| 432 | }while( bMore ); |
| 433 | if( iCode!=220 ){ |
| 434 | smtp_set_error(p, 1, "conversation begins with: \"%d %s\"",iCode,zArg); |
| 435 | smtp_client_quit(p); |
| 436 | return 1; |
| 437 | } |
| 438 | smtp_send_line(p, "EHLO %s\r\n", p->zFrom); |
| 439 | do{ |
| 440 | smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg); |
| 441 | }while( bMore ); |
| 442 | if( iCode!=250 ){ |
| 443 | smtp_set_error(p, 1, "reply to EHLO with: \"%d %s\"",iCode, zArg); |
| 444 | smtp_client_quit(p); |
| 445 | return 1; |
| 446 | } |
| 447 | fossil_free(p->zErr); |
| 448 | p->zErr = 0; |
| 449 | return 0; |
| 450 | } |
| 451 | |
| 452 | /* |
| 453 | ** COMMAND: test-smtp-probe |
| @@ -541,27 +575,40 @@ | |
| 575 | int iCode = 0; |
| 576 | int bMore = 0; |
| 577 | char *zArg = 0; |
| 578 | Blob in; |
| 579 | blob_init(&in, 0, 0); |
| 580 | if( !p->bOpen ){ |
| 581 | if( !p->bFatal ) smtp_client_startup(p); |
| 582 | if( !p->bOpen ) return 1; |
| 583 | } |
| 584 | smtp_send_line(p, "MAIL FROM:<%s>\r\n", zFrom); |
| 585 | do{ |
| 586 | smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg); |
| 587 | }while( bMore ); |
| 588 | if( iCode!=250 ){ |
| 589 | smtp_set_error(p, 0,"reply to MAIL FROM: \"%d %s\"",iCode,zArg); |
| 590 | return 1; |
| 591 | } |
| 592 | for(i=0; i<nTo; i++){ |
| 593 | smtp_send_line(p, "RCPT TO:<%s>\r\n", azTo[i]); |
| 594 | do{ |
| 595 | smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg); |
| 596 | }while( bMore ); |
| 597 | if( iCode!=250 ){ |
| 598 | smtp_set_error(p, 0,"reply to RCPT TO: \"%d %s\"",iCode,zArg); |
| 599 | return 1; |
| 600 | } |
| 601 | } |
| 602 | smtp_send_line(p, "DATA\r\n"); |
| 603 | do{ |
| 604 | smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg); |
| 605 | }while( bMore ); |
| 606 | if( iCode!=354 ){ |
| 607 | smtp_set_error(p, 0, "reply to DATA with: \"%d %s\"",iCode,zArg); |
| 608 | return 1; |
| 609 | } |
| 610 | smtp_send_email_body(zMsg, socket_send, 0); |
| 611 | if( p->smtpFlags & SMTP_TRACE_STDOUT ){ |
| 612 | fossil_print("C: # message content\nC: .\n"); |
| 613 | } |
| 614 | if( p->smtpFlags & SMTP_TRACE_FILE ){ |
| @@ -571,11 +618,15 @@ | |
| 618 | blob_appendf(p->pTranscript, "C: # message content\nC: .\n"); |
| 619 | } |
| 620 | do{ |
| 621 | smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg); |
| 622 | }while( bMore ); |
| 623 | if( iCode!=250 ){ |
| 624 | smtp_set_error(p, 0, "reply to end-of-DATA with: \"%d %s\"", |
| 625 | iCode, zArg); |
| 626 | return 1; |
| 627 | } |
| 628 | return 0; |
| 629 | } |
| 630 | |
| 631 | /* |
| 632 | ** The input is a base email address of the form "local@domain". |
| @@ -636,14 +687,13 @@ | |
| 687 | p = smtp_session_new(zFromDomain, zToDomain, smtpFlags, smtpPort); |
| 688 | if( p->zErr ){ |
| 689 | fossil_fatal("%s", p->zErr); |
| 690 | } |
| 691 | fossil_print("Connection to \"%s\"\n", p->zHostname); |
| 692 | smtp_send_msg(p, zFrom, nTo, azTo, blob_str(&body)); |
| 693 | smtp_client_quit(p); |
| 694 | if( p->zErr ){ |
| 695 | fossil_fatal("ERROR: %s\n", p->zErr); |
| 696 | } |
| 697 | smtp_session_free(p); |
| 698 | blob_reset(&body); |
| 699 | } |
| 700 |
+5
-1
| --- src/sorttable.js | ||
| +++ src/sorttable.js | ||
| @@ -9,11 +9,11 @@ | ||
| 9 | 9 | ** function. Example: |
| 10 | 10 | ** |
| 11 | 11 | ** <table class='sortable' data-column-types='tnkx' data-init-sort='2'> |
| 12 | 12 | ** |
| 13 | 13 | ** Column data types are determined by the data-column-types attribute of |
| 14 | -** the table. The value of data-column-types is a string where each | |
| 14 | +** the table. The value of data-column-types is a string where each | |
| 15 | 15 | ** character of the string represents a datatype for one column in the |
| 16 | 16 | ** table. |
| 17 | 17 | ** |
| 18 | 18 | ** t Sort by text |
| 19 | 19 | ** n Sort numerically |
| @@ -86,18 +86,22 @@ | ||
| 86 | 86 | hdrCell.className = clsName; |
| 87 | 87 | } |
| 88 | 88 | } |
| 89 | 89 | this.sortText = function(a,b) { |
| 90 | 90 | var i = thisObject.sortIndex; |
| 91 | + if (a.cells.length<=i) return -1; /* see ticket 59d699710b1ab5d4 */ | |
| 92 | + if (b.cells.length<=i) return 1; | |
| 91 | 93 | aa = a.cells[i].textContent.replace(/^\W+/,'').toLowerCase(); |
| 92 | 94 | bb = b.cells[i].textContent.replace(/^\W+/,'').toLowerCase(); |
| 93 | 95 | if(aa<bb) return -1; |
| 94 | 96 | if(aa==bb) return a.rowIndex-b.rowIndex; |
| 95 | 97 | return 1; |
| 96 | 98 | } |
| 97 | 99 | this.sortReverseText = function(a,b) { |
| 98 | 100 | var i = thisObject.sortIndex; |
| 101 | + if (a.cells.length<=i) return 1; /* see ticket 59d699710b1ab5d4 */ | |
| 102 | + if (b.cells.length<=i) return -1; | |
| 99 | 103 | aa = a.cells[i].textContent.replace(/^\W+/,'').toLowerCase(); |
| 100 | 104 | bb = b.cells[i].textContent.replace(/^\W+/,'').toLowerCase(); |
| 101 | 105 | if(aa<bb) return +1; |
| 102 | 106 | if(aa==bb) return a.rowIndex-b.rowIndex; |
| 103 | 107 | return -1; |
| 104 | 108 |
| --- src/sorttable.js | |
| +++ src/sorttable.js | |
| @@ -9,11 +9,11 @@ | |
| 9 | ** function. Example: |
| 10 | ** |
| 11 | ** <table class='sortable' data-column-types='tnkx' data-init-sort='2'> |
| 12 | ** |
| 13 | ** Column data types are determined by the data-column-types attribute of |
| 14 | ** the table. The value of data-column-types is a string where each |
| 15 | ** character of the string represents a datatype for one column in the |
| 16 | ** table. |
| 17 | ** |
| 18 | ** t Sort by text |
| 19 | ** n Sort numerically |
| @@ -86,18 +86,22 @@ | |
| 86 | hdrCell.className = clsName; |
| 87 | } |
| 88 | } |
| 89 | this.sortText = function(a,b) { |
| 90 | var i = thisObject.sortIndex; |
| 91 | aa = a.cells[i].textContent.replace(/^\W+/,'').toLowerCase(); |
| 92 | bb = b.cells[i].textContent.replace(/^\W+/,'').toLowerCase(); |
| 93 | if(aa<bb) return -1; |
| 94 | if(aa==bb) return a.rowIndex-b.rowIndex; |
| 95 | return 1; |
| 96 | } |
| 97 | this.sortReverseText = function(a,b) { |
| 98 | var i = thisObject.sortIndex; |
| 99 | aa = a.cells[i].textContent.replace(/^\W+/,'').toLowerCase(); |
| 100 | bb = b.cells[i].textContent.replace(/^\W+/,'').toLowerCase(); |
| 101 | if(aa<bb) return +1; |
| 102 | if(aa==bb) return a.rowIndex-b.rowIndex; |
| 103 | return -1; |
| 104 |
| --- src/sorttable.js | |
| +++ src/sorttable.js | |
| @@ -9,11 +9,11 @@ | |
| 9 | ** function. Example: |
| 10 | ** |
| 11 | ** <table class='sortable' data-column-types='tnkx' data-init-sort='2'> |
| 12 | ** |
| 13 | ** Column data types are determined by the data-column-types attribute of |
| 14 | ** the table. The value of data-column-types is a string where each |
| 15 | ** character of the string represents a datatype for one column in the |
| 16 | ** table. |
| 17 | ** |
| 18 | ** t Sort by text |
| 19 | ** n Sort numerically |
| @@ -86,18 +86,22 @@ | |
| 86 | hdrCell.className = clsName; |
| 87 | } |
| 88 | } |
| 89 | this.sortText = function(a,b) { |
| 90 | var i = thisObject.sortIndex; |
| 91 | if (a.cells.length<=i) return -1; /* see ticket 59d699710b1ab5d4 */ |
| 92 | if (b.cells.length<=i) return 1; |
| 93 | aa = a.cells[i].textContent.replace(/^\W+/,'').toLowerCase(); |
| 94 | bb = b.cells[i].textContent.replace(/^\W+/,'').toLowerCase(); |
| 95 | if(aa<bb) return -1; |
| 96 | if(aa==bb) return a.rowIndex-b.rowIndex; |
| 97 | return 1; |
| 98 | } |
| 99 | this.sortReverseText = function(a,b) { |
| 100 | var i = thisObject.sortIndex; |
| 101 | if (a.cells.length<=i) return 1; /* see ticket 59d699710b1ab5d4 */ |
| 102 | if (b.cells.length<=i) return -1; |
| 103 | aa = a.cells[i].textContent.replace(/^\W+/,'').toLowerCase(); |
| 104 | bb = b.cells[i].textContent.replace(/^\W+/,'').toLowerCase(); |
| 105 | if(aa<bb) return +1; |
| 106 | if(aa==bb) return a.rowIndex-b.rowIndex; |
| 107 | return -1; |
| 108 |
+8
-2
| --- src/stash.c | ||
| +++ src/stash.c | ||
| @@ -259,10 +259,11 @@ | ||
| 259 | 259 | const char *zComment; /* Comment to add to the stash */ |
| 260 | 260 | int stashid; /* ID of the new stash */ |
| 261 | 261 | int vid; /* Current check-out */ |
| 262 | 262 | |
| 263 | 263 | zComment = find_option("comment", "m", 1); |
| 264 | + (void)fossil_text_editor(); | |
| 264 | 265 | verify_all_options(); |
| 265 | 266 | if( zComment==0 ){ |
| 266 | 267 | Blob prompt; /* Prompt for stash comment */ |
| 267 | 268 | Blob comment; /* User comment reply */ |
| 268 | 269 | #if defined(_WIN32) || defined(__CYGWIN__) |
| @@ -508,19 +509,24 @@ | ||
| 508 | 509 | ** COMMAND: stash |
| 509 | 510 | ** |
| 510 | 511 | ** Usage: %fossil stash SUBCOMMAND ARGS... |
| 511 | 512 | ** |
| 512 | 513 | ** > fossil stash |
| 513 | -** > fossil stash save ?-m|--comment COMMENT? ?FILES...? | |
| 514 | -** > fossil stash snapshot ?-m|--comment COMMENT? ?FILES...? | |
| 514 | +** > fossil stash save ?FILES...? | |
| 515 | +** > fossil stash snapshot ?FILES...? | |
| 515 | 516 | ** |
| 516 | 517 | ** Save the current changes in the working tree as a new stash. |
| 517 | 518 | ** Then revert the changes back to the last check-in. If FILES |
| 518 | 519 | ** are listed, then only stash and revert the named files. The |
| 519 | 520 | ** "save" verb can be omitted if and only if there are no other |
| 520 | 521 | ** arguments. The "snapshot" verb works the same as "save" but |
| 521 | 522 | ** omits the revert, keeping the check-out unchanged. |
| 523 | +** | |
| 524 | +** Options: | |
| 525 | +** --editor NAME Use the NAME editor to enter comment | |
| 526 | +** -m|--comment COMMENT Comment text for the new stash | |
| 527 | +** | |
| 522 | 528 | ** |
| 523 | 529 | ** > fossil stash list|ls ?-v|--verbose? ?-W|--width NUM? |
| 524 | 530 | ** |
| 525 | 531 | ** List all changes sets currently stashed. Show information about |
| 526 | 532 | ** individual files in each changeset if -v or --verbose is used. |
| 527 | 533 |
| --- src/stash.c | |
| +++ src/stash.c | |
| @@ -259,10 +259,11 @@ | |
| 259 | const char *zComment; /* Comment to add to the stash */ |
| 260 | int stashid; /* ID of the new stash */ |
| 261 | int vid; /* Current check-out */ |
| 262 | |
| 263 | zComment = find_option("comment", "m", 1); |
| 264 | verify_all_options(); |
| 265 | if( zComment==0 ){ |
| 266 | Blob prompt; /* Prompt for stash comment */ |
| 267 | Blob comment; /* User comment reply */ |
| 268 | #if defined(_WIN32) || defined(__CYGWIN__) |
| @@ -508,19 +509,24 @@ | |
| 508 | ** COMMAND: stash |
| 509 | ** |
| 510 | ** Usage: %fossil stash SUBCOMMAND ARGS... |
| 511 | ** |
| 512 | ** > fossil stash |
| 513 | ** > fossil stash save ?-m|--comment COMMENT? ?FILES...? |
| 514 | ** > fossil stash snapshot ?-m|--comment COMMENT? ?FILES...? |
| 515 | ** |
| 516 | ** Save the current changes in the working tree as a new stash. |
| 517 | ** Then revert the changes back to the last check-in. If FILES |
| 518 | ** are listed, then only stash and revert the named files. The |
| 519 | ** "save" verb can be omitted if and only if there are no other |
| 520 | ** arguments. The "snapshot" verb works the same as "save" but |
| 521 | ** omits the revert, keeping the check-out unchanged. |
| 522 | ** |
| 523 | ** > fossil stash list|ls ?-v|--verbose? ?-W|--width NUM? |
| 524 | ** |
| 525 | ** List all changes sets currently stashed. Show information about |
| 526 | ** individual files in each changeset if -v or --verbose is used. |
| 527 |
| --- src/stash.c | |
| +++ src/stash.c | |
| @@ -259,10 +259,11 @@ | |
| 259 | const char *zComment; /* Comment to add to the stash */ |
| 260 | int stashid; /* ID of the new stash */ |
| 261 | int vid; /* Current check-out */ |
| 262 | |
| 263 | zComment = find_option("comment", "m", 1); |
| 264 | (void)fossil_text_editor(); |
| 265 | verify_all_options(); |
| 266 | if( zComment==0 ){ |
| 267 | Blob prompt; /* Prompt for stash comment */ |
| 268 | Blob comment; /* User comment reply */ |
| 269 | #if defined(_WIN32) || defined(__CYGWIN__) |
| @@ -508,19 +509,24 @@ | |
| 509 | ** COMMAND: stash |
| 510 | ** |
| 511 | ** Usage: %fossil stash SUBCOMMAND ARGS... |
| 512 | ** |
| 513 | ** > fossil stash |
| 514 | ** > fossil stash save ?FILES...? |
| 515 | ** > fossil stash snapshot ?FILES...? |
| 516 | ** |
| 517 | ** Save the current changes in the working tree as a new stash. |
| 518 | ** Then revert the changes back to the last check-in. If FILES |
| 519 | ** are listed, then only stash and revert the named files. The |
| 520 | ** "save" verb can be omitted if and only if there are no other |
| 521 | ** arguments. The "snapshot" verb works the same as "save" but |
| 522 | ** omits the revert, keeping the check-out unchanged. |
| 523 | ** |
| 524 | ** Options: |
| 525 | ** --editor NAME Use the NAME editor to enter comment |
| 526 | ** -m|--comment COMMENT Comment text for the new stash |
| 527 | ** |
| 528 | ** |
| 529 | ** > fossil stash list|ls ?-v|--verbose? ?-W|--width NUM? |
| 530 | ** |
| 531 | ** List all changes sets currently stashed. Show information about |
| 532 | ** individual files in each changeset if -v or --verbose is used. |
| 533 |
+1
-1
| --- src/stat.c | ||
| +++ src/stat.c | ||
| @@ -166,11 +166,11 @@ | ||
| 166 | 166 | style_submenu_element("Artifacts", "bloblist"); |
| 167 | 167 | if( sqlite3_compileoption_used("ENABLE_DBSTAT_VTAB") ){ |
| 168 | 168 | style_submenu_element("Table Sizes", "repo-tabsize"); |
| 169 | 169 | } |
| 170 | 170 | if( g.perm.Admin || g.perm.Setup || db_get_boolean("test_env_enable",0) ){ |
| 171 | - style_submenu_element("Environment", "test_env"); | |
| 171 | + style_submenu_element("Environment", "test-env"); | |
| 172 | 172 | } |
| 173 | 173 | @ <table class="label-value"> |
| 174 | 174 | fsize = file_size(g.zRepositoryName, ExtFILE); |
| 175 | 175 | @ <tr><th>Repository Size:</th><td>%,lld(fsize) bytes</td> |
| 176 | 176 | @ </td></tr> |
| 177 | 177 |
| --- src/stat.c | |
| +++ src/stat.c | |
| @@ -166,11 +166,11 @@ | |
| 166 | style_submenu_element("Artifacts", "bloblist"); |
| 167 | if( sqlite3_compileoption_used("ENABLE_DBSTAT_VTAB") ){ |
| 168 | style_submenu_element("Table Sizes", "repo-tabsize"); |
| 169 | } |
| 170 | if( g.perm.Admin || g.perm.Setup || db_get_boolean("test_env_enable",0) ){ |
| 171 | style_submenu_element("Environment", "test_env"); |
| 172 | } |
| 173 | @ <table class="label-value"> |
| 174 | fsize = file_size(g.zRepositoryName, ExtFILE); |
| 175 | @ <tr><th>Repository Size:</th><td>%,lld(fsize) bytes</td> |
| 176 | @ </td></tr> |
| 177 |
| --- src/stat.c | |
| +++ src/stat.c | |
| @@ -166,11 +166,11 @@ | |
| 166 | style_submenu_element("Artifacts", "bloblist"); |
| 167 | if( sqlite3_compileoption_used("ENABLE_DBSTAT_VTAB") ){ |
| 168 | style_submenu_element("Table Sizes", "repo-tabsize"); |
| 169 | } |
| 170 | if( g.perm.Admin || g.perm.Setup || db_get_boolean("test_env_enable",0) ){ |
| 171 | style_submenu_element("Environment", "test-env"); |
| 172 | } |
| 173 | @ <table class="label-value"> |
| 174 | fsize = file_size(g.zRepositoryName, ExtFILE); |
| 175 | @ <tr><th>Repository Size:</th><td>%,lld(fsize) bytes</td> |
| 176 | @ </td></tr> |
| 177 |
+3
| --- src/statrep.c | ||
| +++ src/statrep.c | ||
| @@ -859,10 +859,13 @@ | ||
| 859 | 859 | ** * f (forum post) |
| 860 | 860 | ** * w (wiki page change) |
| 861 | 861 | ** * t (ticket change) |
| 862 | 862 | ** * g (tag added or removed) |
| 863 | 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 | +** | |
| 864 | 867 | ** |
| 865 | 868 | ** The view-specific query parameters include: |
| 866 | 869 | ** |
| 867 | 870 | ** view=byweek: |
| 868 | 871 | ** |
| 869 | 872 |
| --- src/statrep.c | |
| +++ src/statrep.c | |
| @@ -859,10 +859,13 @@ | |
| 859 | ** * f (forum post) |
| 860 | ** * w (wiki page change) |
| 861 | ** * t (ticket change) |
| 862 | ** * g (tag added or removed) |
| 863 | ** Defaulting to all event types. |
| 864 | ** |
| 865 | ** The view-specific query parameters include: |
| 866 | ** |
| 867 | ** view=byweek: |
| 868 | ** |
| 869 |
| --- src/statrep.c | |
| +++ src/statrep.c | |
| @@ -859,10 +859,13 @@ | |
| 859 | ** * f (forum post) |
| 860 | ** * w (wiki page change) |
| 861 | ** * t (ticket change) |
| 862 | ** * g (tag added or removed) |
| 863 | ** Defaulting to all event types. |
| 864 | ** from=DATETIME Consider only events after this timestamp (requires to) |
| 865 | ** to=DATETIME Consider only events before this timestamp (requires from) |
| 866 | ** |
| 867 | ** |
| 868 | ** The view-specific query parameters include: |
| 869 | ** |
| 870 | ** view=byweek: |
| 871 | ** |
| 872 |
+9
-7
| --- src/style.c | ||
| +++ src/style.c | ||
| @@ -744,13 +744,14 @@ | ||
| 744 | 744 | ** is evaluated before the header is rendered). |
| 745 | 745 | */ |
| 746 | 746 | Th_MaybeStore("default_csp", zDfltCsp); |
| 747 | 747 | fossil_free(zDfltCsp); |
| 748 | 748 | Th_Store("nonce", zNonce); |
| 749 | - Th_Store("project_name", db_get("project-name","Unnamed Fossil Project")); | |
| 750 | - Th_Store("project_description", db_get("project-description","")); | |
| 751 | - if( zTitle ) Th_Store("title", zTitle); | |
| 749 | + Th_StoreUnsafe("project_name", | |
| 750 | + db_get("project-name","Unnamed Fossil Project")); | |
| 751 | + Th_StoreUnsafe("project_description", db_get("project-description","")); | |
| 752 | + if( zTitle ) Th_Store("title", html_lookalike(zTitle,-1)); | |
| 752 | 753 | Th_Store("baseurl", g.zBaseURL); |
| 753 | 754 | Th_Store("secureurl", fossil_wants_https(1)? g.zHttpsURL: g.zBaseURL); |
| 754 | 755 | Th_Store("home", g.zTop); |
| 755 | 756 | Th_Store("index_page", db_get("index-page","/home")); |
| 756 | 757 | if( local_zCurrentPage==0 ) style_set_current_page("%T", g.zPath); |
| @@ -772,11 +773,11 @@ | ||
| 772 | 773 | Th_Store("mainmenu", style_get_mainmenu()); |
| 773 | 774 | stylesheet_url_var(); |
| 774 | 775 | image_url_var("logo"); |
| 775 | 776 | image_url_var("background"); |
| 776 | 777 | if( !login_is_nobody() ){ |
| 777 | - Th_Store("login", g.zLogin); | |
| 778 | + Th_Store("login", html_lookalike(g.zLogin,-1)); | |
| 778 | 779 | } |
| 779 | 780 | Th_MaybeStore("current_feature", feature_from_page_path(local_zCurrentPage) ); |
| 780 | 781 | if( g.ftntsIssues[0] || g.ftntsIssues[1] || |
| 781 | 782 | g.ftntsIssues[2] || g.ftntsIssues[3] ){ |
| 782 | 783 | char buf[80]; |
| @@ -1382,11 +1383,12 @@ | ||
| 1382 | 1383 | @ </form> |
| 1383 | 1384 | style_finish_page(); |
| 1384 | 1385 | } |
| 1385 | 1386 | |
| 1386 | 1387 | /* |
| 1387 | -** WEBPAGE: test_env | |
| 1388 | +** WEBPAGE: test-env | |
| 1389 | +** WEBPAGE: test_env alias | |
| 1388 | 1390 | ** |
| 1389 | 1391 | ** Display CGI-variables and other aspects of the run-time |
| 1390 | 1392 | ** environment, for debugging and trouble-shooting purposes. |
| 1391 | 1393 | */ |
| 1392 | 1394 | void page_test_env(void){ |
| @@ -1445,11 +1447,11 @@ | ||
| 1445 | 1447 | ** |
| 1446 | 1448 | ** For administators, or if the test_env_enable setting is true, then |
| 1447 | 1449 | ** details of the request environment are displayed. Otherwise, just |
| 1448 | 1450 | ** the error message is shown. |
| 1449 | 1451 | ** |
| 1450 | -** If zFormat is an empty string, then this is the /test_env page. | |
| 1452 | +** If zFormat is an empty string, then this is the /test-env page. | |
| 1451 | 1453 | */ |
| 1452 | 1454 | void webpage_error(const char *zFormat, ...){ |
| 1453 | 1455 | int showAll = 0; |
| 1454 | 1456 | char *zErr = 0; |
| 1455 | 1457 | int isAuth = 0; |
| @@ -1545,11 +1547,11 @@ | ||
| 1545 | 1547 | } |
| 1546 | 1548 | @ <hr> |
| 1547 | 1549 | P("HTTP_USER_AGENT"); |
| 1548 | 1550 | P("SERVER_SOFTWARE"); |
| 1549 | 1551 | cgi_print_all(showAll, 0, 0); |
| 1550 | - @ <p><form method="POST" action="%R/test_env"> | |
| 1552 | + @ <p><form method="POST" action="%R/test-env"> | |
| 1551 | 1553 | @ <input type="hidden" name="showall" value="%d(showAll)"> |
| 1552 | 1554 | @ <input type="submit" name="post-test-button" value="POST Test"> |
| 1553 | 1555 | @ </form> |
| 1554 | 1556 | if( showAll && blob_size(&g.httpHeader)>0 ){ |
| 1555 | 1557 | @ <hr> |
| 1556 | 1558 |
| --- src/style.c | |
| +++ src/style.c | |
| @@ -744,13 +744,14 @@ | |
| 744 | ** is evaluated before the header is rendered). |
| 745 | */ |
| 746 | Th_MaybeStore("default_csp", zDfltCsp); |
| 747 | fossil_free(zDfltCsp); |
| 748 | Th_Store("nonce", zNonce); |
| 749 | Th_Store("project_name", db_get("project-name","Unnamed Fossil Project")); |
| 750 | Th_Store("project_description", db_get("project-description","")); |
| 751 | if( zTitle ) Th_Store("title", zTitle); |
| 752 | Th_Store("baseurl", g.zBaseURL); |
| 753 | Th_Store("secureurl", fossil_wants_https(1)? g.zHttpsURL: g.zBaseURL); |
| 754 | Th_Store("home", g.zTop); |
| 755 | Th_Store("index_page", db_get("index-page","/home")); |
| 756 | if( local_zCurrentPage==0 ) style_set_current_page("%T", g.zPath); |
| @@ -772,11 +773,11 @@ | |
| 772 | Th_Store("mainmenu", style_get_mainmenu()); |
| 773 | stylesheet_url_var(); |
| 774 | image_url_var("logo"); |
| 775 | image_url_var("background"); |
| 776 | if( !login_is_nobody() ){ |
| 777 | Th_Store("login", g.zLogin); |
| 778 | } |
| 779 | Th_MaybeStore("current_feature", feature_from_page_path(local_zCurrentPage) ); |
| 780 | if( g.ftntsIssues[0] || g.ftntsIssues[1] || |
| 781 | g.ftntsIssues[2] || g.ftntsIssues[3] ){ |
| 782 | char buf[80]; |
| @@ -1382,11 +1383,12 @@ | |
| 1382 | @ </form> |
| 1383 | style_finish_page(); |
| 1384 | } |
| 1385 | |
| 1386 | /* |
| 1387 | ** WEBPAGE: test_env |
| 1388 | ** |
| 1389 | ** Display CGI-variables and other aspects of the run-time |
| 1390 | ** environment, for debugging and trouble-shooting purposes. |
| 1391 | */ |
| 1392 | void page_test_env(void){ |
| @@ -1445,11 +1447,11 @@ | |
| 1445 | ** |
| 1446 | ** For administators, or if the test_env_enable setting is true, then |
| 1447 | ** details of the request environment are displayed. Otherwise, just |
| 1448 | ** the error message is shown. |
| 1449 | ** |
| 1450 | ** If zFormat is an empty string, then this is the /test_env page. |
| 1451 | */ |
| 1452 | void webpage_error(const char *zFormat, ...){ |
| 1453 | int showAll = 0; |
| 1454 | char *zErr = 0; |
| 1455 | int isAuth = 0; |
| @@ -1545,11 +1547,11 @@ | |
| 1545 | } |
| 1546 | @ <hr> |
| 1547 | P("HTTP_USER_AGENT"); |
| 1548 | P("SERVER_SOFTWARE"); |
| 1549 | cgi_print_all(showAll, 0, 0); |
| 1550 | @ <p><form method="POST" action="%R/test_env"> |
| 1551 | @ <input type="hidden" name="showall" value="%d(showAll)"> |
| 1552 | @ <input type="submit" name="post-test-button" value="POST Test"> |
| 1553 | @ </form> |
| 1554 | if( showAll && blob_size(&g.httpHeader)>0 ){ |
| 1555 | @ <hr> |
| 1556 |
| --- src/style.c | |
| +++ src/style.c | |
| @@ -744,13 +744,14 @@ | |
| 744 | ** is evaluated before the header is rendered). |
| 745 | */ |
| 746 | Th_MaybeStore("default_csp", zDfltCsp); |
| 747 | fossil_free(zDfltCsp); |
| 748 | Th_Store("nonce", zNonce); |
| 749 | Th_StoreUnsafe("project_name", |
| 750 | db_get("project-name","Unnamed Fossil Project")); |
| 751 | Th_StoreUnsafe("project_description", db_get("project-description","")); |
| 752 | if( zTitle ) Th_Store("title", html_lookalike(zTitle,-1)); |
| 753 | Th_Store("baseurl", g.zBaseURL); |
| 754 | Th_Store("secureurl", fossil_wants_https(1)? g.zHttpsURL: g.zBaseURL); |
| 755 | Th_Store("home", g.zTop); |
| 756 | Th_Store("index_page", db_get("index-page","/home")); |
| 757 | if( local_zCurrentPage==0 ) style_set_current_page("%T", g.zPath); |
| @@ -772,11 +773,11 @@ | |
| 773 | Th_Store("mainmenu", style_get_mainmenu()); |
| 774 | stylesheet_url_var(); |
| 775 | image_url_var("logo"); |
| 776 | image_url_var("background"); |
| 777 | if( !login_is_nobody() ){ |
| 778 | Th_Store("login", html_lookalike(g.zLogin,-1)); |
| 779 | } |
| 780 | Th_MaybeStore("current_feature", feature_from_page_path(local_zCurrentPage) ); |
| 781 | if( g.ftntsIssues[0] || g.ftntsIssues[1] || |
| 782 | g.ftntsIssues[2] || g.ftntsIssues[3] ){ |
| 783 | char buf[80]; |
| @@ -1382,11 +1383,12 @@ | |
| 1383 | @ </form> |
| 1384 | style_finish_page(); |
| 1385 | } |
| 1386 | |
| 1387 | /* |
| 1388 | ** WEBPAGE: test-env |
| 1389 | ** WEBPAGE: test_env alias |
| 1390 | ** |
| 1391 | ** Display CGI-variables and other aspects of the run-time |
| 1392 | ** environment, for debugging and trouble-shooting purposes. |
| 1393 | */ |
| 1394 | void page_test_env(void){ |
| @@ -1445,11 +1447,11 @@ | |
| 1447 | ** |
| 1448 | ** For administators, or if the test_env_enable setting is true, then |
| 1449 | ** details of the request environment are displayed. Otherwise, just |
| 1450 | ** the error message is shown. |
| 1451 | ** |
| 1452 | ** If zFormat is an empty string, then this is the /test-env page. |
| 1453 | */ |
| 1454 | void webpage_error(const char *zFormat, ...){ |
| 1455 | int showAll = 0; |
| 1456 | char *zErr = 0; |
| 1457 | int isAuth = 0; |
| @@ -1545,11 +1547,11 @@ | |
| 1547 | } |
| 1548 | @ <hr> |
| 1549 | P("HTTP_USER_AGENT"); |
| 1550 | P("SERVER_SOFTWARE"); |
| 1551 | cgi_print_all(showAll, 0, 0); |
| 1552 | @ <p><form method="POST" action="%R/test-env"> |
| 1553 | @ <input type="hidden" name="showall" value="%d(showAll)"> |
| 1554 | @ <input type="submit" name="post-test-button" value="POST Test"> |
| 1555 | @ </form> |
| 1556 | if( showAll && blob_size(&g.httpHeader)>0 ){ |
| 1557 | @ <hr> |
| 1558 |
+9
-7
| --- src/style.c | ||
| +++ src/style.c | ||
| @@ -744,13 +744,14 @@ | ||
| 744 | 744 | ** is evaluated before the header is rendered). |
| 745 | 745 | */ |
| 746 | 746 | Th_MaybeStore("default_csp", zDfltCsp); |
| 747 | 747 | fossil_free(zDfltCsp); |
| 748 | 748 | Th_Store("nonce", zNonce); |
| 749 | - Th_Store("project_name", db_get("project-name","Unnamed Fossil Project")); | |
| 750 | - Th_Store("project_description", db_get("project-description","")); | |
| 751 | - if( zTitle ) Th_Store("title", zTitle); | |
| 749 | + Th_StoreUnsafe("project_name", | |
| 750 | + db_get("project-name","Unnamed Fossil Project")); | |
| 751 | + Th_StoreUnsafe("project_description", db_get("project-description","")); | |
| 752 | + if( zTitle ) Th_Store("title", html_lookalike(zTitle,-1)); | |
| 752 | 753 | Th_Store("baseurl", g.zBaseURL); |
| 753 | 754 | Th_Store("secureurl", fossil_wants_https(1)? g.zHttpsURL: g.zBaseURL); |
| 754 | 755 | Th_Store("home", g.zTop); |
| 755 | 756 | Th_Store("index_page", db_get("index-page","/home")); |
| 756 | 757 | if( local_zCurrentPage==0 ) style_set_current_page("%T", g.zPath); |
| @@ -772,11 +773,11 @@ | ||
| 772 | 773 | Th_Store("mainmenu", style_get_mainmenu()); |
| 773 | 774 | stylesheet_url_var(); |
| 774 | 775 | image_url_var("logo"); |
| 775 | 776 | image_url_var("background"); |
| 776 | 777 | if( !login_is_nobody() ){ |
| 777 | - Th_Store("login", g.zLogin); | |
| 778 | + Th_Store("login", html_lookalike(g.zLogin,-1)); | |
| 778 | 779 | } |
| 779 | 780 | Th_MaybeStore("current_feature", feature_from_page_path(local_zCurrentPage) ); |
| 780 | 781 | if( g.ftntsIssues[0] || g.ftntsIssues[1] || |
| 781 | 782 | g.ftntsIssues[2] || g.ftntsIssues[3] ){ |
| 782 | 783 | char buf[80]; |
| @@ -1382,11 +1383,12 @@ | ||
| 1382 | 1383 | @ </form> |
| 1383 | 1384 | style_finish_page(); |
| 1384 | 1385 | } |
| 1385 | 1386 | |
| 1386 | 1387 | /* |
| 1387 | -** WEBPAGE: test_env | |
| 1388 | +** WEBPAGE: test-env | |
| 1389 | +** WEBPAGE: test_env alias | |
| 1388 | 1390 | ** |
| 1389 | 1391 | ** Display CGI-variables and other aspects of the run-time |
| 1390 | 1392 | ** environment, for debugging and trouble-shooting purposes. |
| 1391 | 1393 | */ |
| 1392 | 1394 | void page_test_env(void){ |
| @@ -1445,11 +1447,11 @@ | ||
| 1445 | 1447 | ** |
| 1446 | 1448 | ** For administators, or if the test_env_enable setting is true, then |
| 1447 | 1449 | ** details of the request environment are displayed. Otherwise, just |
| 1448 | 1450 | ** the error message is shown. |
| 1449 | 1451 | ** |
| 1450 | -** If zFormat is an empty string, then this is the /test_env page. | |
| 1452 | +** If zFormat is an empty string, then this is the /test-env page. | |
| 1451 | 1453 | */ |
| 1452 | 1454 | void webpage_error(const char *zFormat, ...){ |
| 1453 | 1455 | int showAll = 0; |
| 1454 | 1456 | char *zErr = 0; |
| 1455 | 1457 | int isAuth = 0; |
| @@ -1545,11 +1547,11 @@ | ||
| 1545 | 1547 | } |
| 1546 | 1548 | @ <hr> |
| 1547 | 1549 | P("HTTP_USER_AGENT"); |
| 1548 | 1550 | P("SERVER_SOFTWARE"); |
| 1549 | 1551 | cgi_print_all(showAll, 0, 0); |
| 1550 | - @ <p><form method="POST" action="%R/test_env"> | |
| 1552 | + @ <p><form method="POST" action="%R/test-env"> | |
| 1551 | 1553 | @ <input type="hidden" name="showall" value="%d(showAll)"> |
| 1552 | 1554 | @ <input type="submit" name="post-test-button" value="POST Test"> |
| 1553 | 1555 | @ </form> |
| 1554 | 1556 | if( showAll && blob_size(&g.httpHeader)>0 ){ |
| 1555 | 1557 | @ <hr> |
| 1556 | 1558 |
| --- src/style.c | |
| +++ src/style.c | |
| @@ -744,13 +744,14 @@ | |
| 744 | ** is evaluated before the header is rendered). |
| 745 | */ |
| 746 | Th_MaybeStore("default_csp", zDfltCsp); |
| 747 | fossil_free(zDfltCsp); |
| 748 | Th_Store("nonce", zNonce); |
| 749 | Th_Store("project_name", db_get("project-name","Unnamed Fossil Project")); |
| 750 | Th_Store("project_description", db_get("project-description","")); |
| 751 | if( zTitle ) Th_Store("title", zTitle); |
| 752 | Th_Store("baseurl", g.zBaseURL); |
| 753 | Th_Store("secureurl", fossil_wants_https(1)? g.zHttpsURL: g.zBaseURL); |
| 754 | Th_Store("home", g.zTop); |
| 755 | Th_Store("index_page", db_get("index-page","/home")); |
| 756 | if( local_zCurrentPage==0 ) style_set_current_page("%T", g.zPath); |
| @@ -772,11 +773,11 @@ | |
| 772 | Th_Store("mainmenu", style_get_mainmenu()); |
| 773 | stylesheet_url_var(); |
| 774 | image_url_var("logo"); |
| 775 | image_url_var("background"); |
| 776 | if( !login_is_nobody() ){ |
| 777 | Th_Store("login", g.zLogin); |
| 778 | } |
| 779 | Th_MaybeStore("current_feature", feature_from_page_path(local_zCurrentPage) ); |
| 780 | if( g.ftntsIssues[0] || g.ftntsIssues[1] || |
| 781 | g.ftntsIssues[2] || g.ftntsIssues[3] ){ |
| 782 | char buf[80]; |
| @@ -1382,11 +1383,12 @@ | |
| 1382 | @ </form> |
| 1383 | style_finish_page(); |
| 1384 | } |
| 1385 | |
| 1386 | /* |
| 1387 | ** WEBPAGE: test_env |
| 1388 | ** |
| 1389 | ** Display CGI-variables and other aspects of the run-time |
| 1390 | ** environment, for debugging and trouble-shooting purposes. |
| 1391 | */ |
| 1392 | void page_test_env(void){ |
| @@ -1445,11 +1447,11 @@ | |
| 1445 | ** |
| 1446 | ** For administators, or if the test_env_enable setting is true, then |
| 1447 | ** details of the request environment are displayed. Otherwise, just |
| 1448 | ** the error message is shown. |
| 1449 | ** |
| 1450 | ** If zFormat is an empty string, then this is the /test_env page. |
| 1451 | */ |
| 1452 | void webpage_error(const char *zFormat, ...){ |
| 1453 | int showAll = 0; |
| 1454 | char *zErr = 0; |
| 1455 | int isAuth = 0; |
| @@ -1545,11 +1547,11 @@ | |
| 1545 | } |
| 1546 | @ <hr> |
| 1547 | P("HTTP_USER_AGENT"); |
| 1548 | P("SERVER_SOFTWARE"); |
| 1549 | cgi_print_all(showAll, 0, 0); |
| 1550 | @ <p><form method="POST" action="%R/test_env"> |
| 1551 | @ <input type="hidden" name="showall" value="%d(showAll)"> |
| 1552 | @ <input type="submit" name="post-test-button" value="POST Test"> |
| 1553 | @ </form> |
| 1554 | if( showAll && blob_size(&g.httpHeader)>0 ){ |
| 1555 | @ <hr> |
| 1556 |
| --- src/style.c | |
| +++ src/style.c | |
| @@ -744,13 +744,14 @@ | |
| 744 | ** is evaluated before the header is rendered). |
| 745 | */ |
| 746 | Th_MaybeStore("default_csp", zDfltCsp); |
| 747 | fossil_free(zDfltCsp); |
| 748 | Th_Store("nonce", zNonce); |
| 749 | Th_StoreUnsafe("project_name", |
| 750 | db_get("project-name","Unnamed Fossil Project")); |
| 751 | Th_StoreUnsafe("project_description", db_get("project-description","")); |
| 752 | if( zTitle ) Th_Store("title", html_lookalike(zTitle,-1)); |
| 753 | Th_Store("baseurl", g.zBaseURL); |
| 754 | Th_Store("secureurl", fossil_wants_https(1)? g.zHttpsURL: g.zBaseURL); |
| 755 | Th_Store("home", g.zTop); |
| 756 | Th_Store("index_page", db_get("index-page","/home")); |
| 757 | if( local_zCurrentPage==0 ) style_set_current_page("%T", g.zPath); |
| @@ -772,11 +773,11 @@ | |
| 773 | Th_Store("mainmenu", style_get_mainmenu()); |
| 774 | stylesheet_url_var(); |
| 775 | image_url_var("logo"); |
| 776 | image_url_var("background"); |
| 777 | if( !login_is_nobody() ){ |
| 778 | Th_Store("login", html_lookalike(g.zLogin,-1)); |
| 779 | } |
| 780 | Th_MaybeStore("current_feature", feature_from_page_path(local_zCurrentPage) ); |
| 781 | if( g.ftntsIssues[0] || g.ftntsIssues[1] || |
| 782 | g.ftntsIssues[2] || g.ftntsIssues[3] ){ |
| 783 | char buf[80]; |
| @@ -1382,11 +1383,12 @@ | |
| 1383 | @ </form> |
| 1384 | style_finish_page(); |
| 1385 | } |
| 1386 | |
| 1387 | /* |
| 1388 | ** WEBPAGE: test-env |
| 1389 | ** WEBPAGE: test_env alias |
| 1390 | ** |
| 1391 | ** Display CGI-variables and other aspects of the run-time |
| 1392 | ** environment, for debugging and trouble-shooting purposes. |
| 1393 | */ |
| 1394 | void page_test_env(void){ |
| @@ -1445,11 +1447,11 @@ | |
| 1447 | ** |
| 1448 | ** For administators, or if the test_env_enable setting is true, then |
| 1449 | ** details of the request environment are displayed. Otherwise, just |
| 1450 | ** the error message is shown. |
| 1451 | ** |
| 1452 | ** If zFormat is an empty string, then this is the /test-env page. |
| 1453 | */ |
| 1454 | void webpage_error(const char *zFormat, ...){ |
| 1455 | int showAll = 0; |
| 1456 | char *zErr = 0; |
| 1457 | int isAuth = 0; |
| @@ -1545,11 +1547,11 @@ | |
| 1547 | } |
| 1548 | @ <hr> |
| 1549 | P("HTTP_USER_AGENT"); |
| 1550 | P("SERVER_SOFTWARE"); |
| 1551 | cgi_print_all(showAll, 0, 0); |
| 1552 | @ <p><form method="POST" action="%R/test-env"> |
| 1553 | @ <input type="hidden" name="showall" value="%d(showAll)"> |
| 1554 | @ <input type="submit" name="post-test-button" value="POST Test"> |
| 1555 | @ </form> |
| 1556 | if( showAll && blob_size(&g.httpHeader)>0 ){ |
| 1557 | @ <hr> |
| 1558 |
+17
-7
| --- src/style.chat.css | ||
| +++ src/style.chat.css | ||
| @@ -213,10 +213,19 @@ | ||
| 213 | 213 | } |
| 214 | 214 | body.chat #chat-messages-wrapper.loading > * { |
| 215 | 215 | /* An attempt at reducing flicker when loading lots of messages. */ |
| 216 | 216 | visibility: hidden; |
| 217 | 217 | } |
| 218 | + | |
| 219 | +/* Provide a visual cue when polling is offline. */ | |
| 220 | +body.chat.connection-error #chat-input-line-wrapper { | |
| 221 | + border-top: medium dotted red; | |
| 222 | +} | |
| 223 | +body.chat.fossil-dark-style.connection-error #chat-input-line-wrapper { | |
| 224 | + border-color: yellow; | |
| 225 | +} | |
| 226 | + | |
| 218 | 227 | body.chat div.content { |
| 219 | 228 | margin: 0; |
| 220 | 229 | padding: 0; |
| 221 | 230 | display: flex; |
| 222 | 231 | flex-direction: column-reverse; |
| @@ -241,20 +250,21 @@ | ||
| 241 | 250 | /* Safari user reports that 2em is necessary to keep the file selection |
| 242 | 251 | widget from overlapping the page footer, whereas a margin of 0 is fine |
| 243 | 252 | for FF/Chrome (and 2em is a *huge* waste of space for those). */ |
| 244 | 253 | margin-bottom: 0; |
| 245 | 254 | } |
| 246 | -.chat-input-field { | |
| 255 | + | |
| 256 | +body.chat .chat-input-field { | |
| 247 | 257 | flex: 10 1 auto; |
| 248 | 258 | margin: 0; |
| 249 | 259 | } |
| 250 | -#chat-input-field-x, | |
| 251 | -#chat-input-field-multi { | |
| 260 | +body.chat #chat-input-field-x, | |
| 261 | +body.chat #chat-input-field-multi { | |
| 252 | 262 | overflow: auto; |
| 253 | 263 | resize: vertical; |
| 254 | 264 | } |
| 255 | -#chat-input-field-x { | |
| 265 | +body.chat #chat-input-field-x { | |
| 256 | 266 | display: inline-block/*supposed workaround for Chrome weirdness*/; |
| 257 | 267 | padding: 0.2em; |
| 258 | 268 | background-color: rgba(156,156,156,0.3); |
| 259 | 269 | white-space: pre-wrap; |
| 260 | 270 | /* ^^^ Firefox, when pasting plain text into a contenteditable field, |
| @@ -261,20 +271,20 @@ | ||
| 261 | 271 | loses all newlines unless we explicitly set this. Chrome does not. */ |
| 262 | 272 | cursor: text; |
| 263 | 273 | /* ^^^ In some browsers the cursor may not change for a contenteditable |
| 264 | 274 | element until it has focus, causing potential confusion. */ |
| 265 | 275 | } |
| 266 | -#chat-input-field-x:empty::before { | |
| 276 | +body.chat #chat-input-field-x:empty::before { | |
| 267 | 277 | content: attr(data-placeholder); |
| 268 | 278 | opacity: 0.6; |
| 269 | 279 | } |
| 270 | -.chat-input-field:not(:focus){ | |
| 280 | +body.chat .chat-input-field:not(:focus){ | |
| 271 | 281 | border-width: 1px; |
| 272 | 282 | border-style: solid; |
| 273 | 283 | border-radius: 0.25em; |
| 274 | 284 | } |
| 275 | -.chat-input-field:focus{ | |
| 285 | +body.chat .chat-input-field:focus{ | |
| 276 | 286 | /* This transparent border helps avoid the text shifting around |
| 277 | 287 | when the contenteditable attribute causes a border (which we |
| 278 | 288 | apparently cannot style) to be added. */ |
| 279 | 289 | border-width: 1px; |
| 280 | 290 | border-style: solid; |
| 281 | 291 |
| --- src/style.chat.css | |
| +++ src/style.chat.css | |
| @@ -213,10 +213,19 @@ | |
| 213 | } |
| 214 | body.chat #chat-messages-wrapper.loading > * { |
| 215 | /* An attempt at reducing flicker when loading lots of messages. */ |
| 216 | visibility: hidden; |
| 217 | } |
| 218 | body.chat div.content { |
| 219 | margin: 0; |
| 220 | padding: 0; |
| 221 | display: flex; |
| 222 | flex-direction: column-reverse; |
| @@ -241,20 +250,21 @@ | |
| 241 | /* Safari user reports that 2em is necessary to keep the file selection |
| 242 | widget from overlapping the page footer, whereas a margin of 0 is fine |
| 243 | for FF/Chrome (and 2em is a *huge* waste of space for those). */ |
| 244 | margin-bottom: 0; |
| 245 | } |
| 246 | .chat-input-field { |
| 247 | flex: 10 1 auto; |
| 248 | margin: 0; |
| 249 | } |
| 250 | #chat-input-field-x, |
| 251 | #chat-input-field-multi { |
| 252 | overflow: auto; |
| 253 | resize: vertical; |
| 254 | } |
| 255 | #chat-input-field-x { |
| 256 | display: inline-block/*supposed workaround for Chrome weirdness*/; |
| 257 | padding: 0.2em; |
| 258 | background-color: rgba(156,156,156,0.3); |
| 259 | white-space: pre-wrap; |
| 260 | /* ^^^ Firefox, when pasting plain text into a contenteditable field, |
| @@ -261,20 +271,20 @@ | |
| 261 | loses all newlines unless we explicitly set this. Chrome does not. */ |
| 262 | cursor: text; |
| 263 | /* ^^^ In some browsers the cursor may not change for a contenteditable |
| 264 | element until it has focus, causing potential confusion. */ |
| 265 | } |
| 266 | #chat-input-field-x:empty::before { |
| 267 | content: attr(data-placeholder); |
| 268 | opacity: 0.6; |
| 269 | } |
| 270 | .chat-input-field:not(:focus){ |
| 271 | border-width: 1px; |
| 272 | border-style: solid; |
| 273 | border-radius: 0.25em; |
| 274 | } |
| 275 | .chat-input-field:focus{ |
| 276 | /* This transparent border helps avoid the text shifting around |
| 277 | when the contenteditable attribute causes a border (which we |
| 278 | apparently cannot style) to be added. */ |
| 279 | border-width: 1px; |
| 280 | border-style: solid; |
| 281 |
| --- src/style.chat.css | |
| +++ src/style.chat.css | |
| @@ -213,10 +213,19 @@ | |
| 213 | } |
| 214 | body.chat #chat-messages-wrapper.loading > * { |
| 215 | /* An attempt at reducing flicker when loading lots of messages. */ |
| 216 | visibility: hidden; |
| 217 | } |
| 218 | |
| 219 | /* Provide a visual cue when polling is offline. */ |
| 220 | body.chat.connection-error #chat-input-line-wrapper { |
| 221 | border-top: medium dotted red; |
| 222 | } |
| 223 | body.chat.fossil-dark-style.connection-error #chat-input-line-wrapper { |
| 224 | border-color: yellow; |
| 225 | } |
| 226 | |
| 227 | body.chat div.content { |
| 228 | margin: 0; |
| 229 | padding: 0; |
| 230 | display: flex; |
| 231 | flex-direction: column-reverse; |
| @@ -241,20 +250,21 @@ | |
| 250 | /* Safari user reports that 2em is necessary to keep the file selection |
| 251 | widget from overlapping the page footer, whereas a margin of 0 is fine |
| 252 | for FF/Chrome (and 2em is a *huge* waste of space for those). */ |
| 253 | margin-bottom: 0; |
| 254 | } |
| 255 | |
| 256 | body.chat .chat-input-field { |
| 257 | flex: 10 1 auto; |
| 258 | margin: 0; |
| 259 | } |
| 260 | body.chat #chat-input-field-x, |
| 261 | body.chat #chat-input-field-multi { |
| 262 | overflow: auto; |
| 263 | resize: vertical; |
| 264 | } |
| 265 | body.chat #chat-input-field-x { |
| 266 | display: inline-block/*supposed workaround for Chrome weirdness*/; |
| 267 | padding: 0.2em; |
| 268 | background-color: rgba(156,156,156,0.3); |
| 269 | white-space: pre-wrap; |
| 270 | /* ^^^ Firefox, when pasting plain text into a contenteditable field, |
| @@ -261,20 +271,20 @@ | |
| 271 | loses all newlines unless we explicitly set this. Chrome does not. */ |
| 272 | cursor: text; |
| 273 | /* ^^^ In some browsers the cursor may not change for a contenteditable |
| 274 | element until it has focus, causing potential confusion. */ |
| 275 | } |
| 276 | body.chat #chat-input-field-x:empty::before { |
| 277 | content: attr(data-placeholder); |
| 278 | opacity: 0.6; |
| 279 | } |
| 280 | body.chat .chat-input-field:not(:focus){ |
| 281 | border-width: 1px; |
| 282 | border-style: solid; |
| 283 | border-radius: 0.25em; |
| 284 | } |
| 285 | body.chat .chat-input-field:focus{ |
| 286 | /* This transparent border helps avoid the text shifting around |
| 287 | when the contenteditable attribute causes a border (which we |
| 288 | apparently cannot style) to be added. */ |
| 289 | border-width: 1px; |
| 290 | border-style: solid; |
| 291 |
M
src/th.c
+95
-56
| --- src/th.c | ||
| +++ src/th.c | ||
| @@ -7,10 +7,16 @@ | ||
| 7 | 7 | #include "config.h" |
| 8 | 8 | #include "th.h" |
| 9 | 9 | #include <string.h> |
| 10 | 10 | #include <assert.h> |
| 11 | 11 | |
| 12 | +/* | |
| 13 | +** External routines | |
| 14 | +*/ | |
| 15 | +void fossil_panic(const char*,...); | |
| 16 | +void fossil_errorlog(const char*,...); | |
| 17 | + | |
| 12 | 18 | /* |
| 13 | 19 | ** Values used for element values in the tcl_platform array. |
| 14 | 20 | */ |
| 15 | 21 | |
| 16 | 22 | #if !defined(TH_ENGINE) |
| @@ -197,10 +203,11 @@ | ||
| 197 | 203 | */ |
| 198 | 204 | struct Buffer { |
| 199 | 205 | char *zBuf; |
| 200 | 206 | int nBuf; |
| 201 | 207 | int nBufAlloc; |
| 208 | + int bTaint; | |
| 202 | 209 | }; |
| 203 | 210 | typedef struct Buffer Buffer; |
| 204 | 211 | static void thBufferInit(Buffer *); |
| 205 | 212 | static void thBufferFree(Th_Interp *interp, Buffer *); |
| 206 | 213 | |
| @@ -209,10 +216,18 @@ | ||
| 209 | 216 | ** be NULL as long as the number of bytes to copy is zero. |
| 210 | 217 | */ |
| 211 | 218 | static void th_memcpy(void *dest, const void *src, size_t n){ |
| 212 | 219 | if( n>0 ) memcpy(dest,src,n); |
| 213 | 220 | } |
| 221 | + | |
| 222 | +/* | |
| 223 | +** An oversized string has been encountered. Do not try to recover. | |
| 224 | +** Panic the process. | |
| 225 | +*/ | |
| 226 | +void Th_OversizeString(void){ | |
| 227 | + fossil_panic("string too large. maximum size 286MB."); | |
| 228 | +} | |
| 214 | 229 | |
| 215 | 230 | /* |
| 216 | 231 | ** Append nAdd bytes of content copied from zAdd to the end of buffer |
| 217 | 232 | ** pBuffer. If there is not enough space currently allocated, resize |
| 218 | 233 | ** the allocation to make space. |
| @@ -219,40 +234,46 @@ | ||
| 219 | 234 | */ |
| 220 | 235 | static void thBufferWriteResize( |
| 221 | 236 | Th_Interp *interp, |
| 222 | 237 | Buffer *pBuffer, |
| 223 | 238 | const char *zAdd, |
| 224 | - int nAdd | |
| 239 | + int nAddX | |
| 225 | 240 | ){ |
| 241 | + int nAdd = TH1_LEN(nAddX); | |
| 226 | 242 | int nNew = (pBuffer->nBuf+nAdd)*2+32; |
| 227 | 243 | #if defined(TH_MEMDEBUG) |
| 228 | 244 | char *zNew = (char *)Th_Malloc(interp, nNew); |
| 245 | + TH1_SIZECHECK(nNew); | |
| 229 | 246 | th_memcpy(zNew, pBuffer->zBuf, pBuffer->nBuf); |
| 230 | 247 | Th_Free(interp, pBuffer->zBuf); |
| 231 | 248 | pBuffer->zBuf = zNew; |
| 232 | 249 | #else |
| 233 | 250 | int nOld = pBuffer->nBufAlloc; |
| 251 | + TH1_SIZECHECK(nNew); | |
| 234 | 252 | pBuffer->zBuf = Th_Realloc(interp, pBuffer->zBuf, nNew); |
| 235 | 253 | memset(pBuffer->zBuf+nOld, 0, nNew-nOld); |
| 236 | 254 | #endif |
| 237 | 255 | pBuffer->nBufAlloc = nNew; |
| 238 | 256 | th_memcpy(&pBuffer->zBuf[pBuffer->nBuf], zAdd, nAdd); |
| 239 | 257 | pBuffer->nBuf += nAdd; |
| 258 | + TH1_XFER_TAINT(pBuffer->bTaint, nAddX); | |
| 240 | 259 | } |
| 241 | 260 | static void thBufferWriteFast( |
| 242 | 261 | Th_Interp *interp, |
| 243 | 262 | Buffer *pBuffer, |
| 244 | 263 | const char *zAdd, |
| 245 | - int nAdd | |
| 264 | + int nAddX | |
| 246 | 265 | ){ |
| 266 | + int nAdd = TH1_LEN(nAddX); | |
| 247 | 267 | if( pBuffer->nBuf+nAdd > pBuffer->nBufAlloc ){ |
| 248 | - thBufferWriteResize(interp, pBuffer, zAdd, nAdd); | |
| 268 | + thBufferWriteResize(interp, pBuffer, zAdd, nAddX); | |
| 249 | 269 | }else{ |
| 250 | 270 | if( pBuffer->zBuf ){ |
| 251 | 271 | memcpy(pBuffer->zBuf + pBuffer->nBuf, zAdd, nAdd); |
| 252 | 272 | } |
| 253 | 273 | pBuffer->nBuf += nAdd; |
| 274 | + TH1_XFER_TAINT(pBuffer->bTaint, nAddX); | |
| 254 | 275 | } |
| 255 | 276 | } |
| 256 | 277 | #define thBufferWrite(a,b,c,d) thBufferWriteFast(a,b,(const char *)c,d) |
| 257 | 278 | |
| 258 | 279 | /* |
| @@ -704,24 +725,25 @@ | ||
| 704 | 725 | int nWord |
| 705 | 726 | ){ |
| 706 | 727 | int rc = TH_OK; |
| 707 | 728 | Buffer output; |
| 708 | 729 | int i; |
| 730 | + int nn = TH1_LEN(nWord); | |
| 709 | 731 | |
| 710 | 732 | thBufferInit(&output); |
| 711 | 733 | |
| 712 | - if( nWord>1 && (zWord[0]=='{' && zWord[nWord-1]=='}') ){ | |
| 713 | - thBufferWrite(interp, &output, &zWord[1], nWord-2); | |
| 734 | + if( nn>1 && (zWord[0]=='{' && zWord[nn-1]=='}') ){ | |
| 735 | + thBufferWrite(interp, &output, &zWord[1], nn-2); | |
| 714 | 736 | }else{ |
| 715 | 737 | |
| 716 | 738 | /* If the word is surrounded by double-quotes strip these away. */ |
| 717 | - if( nWord>1 && (zWord[0]=='"' && zWord[nWord-1]=='"') ){ | |
| 739 | + if( nn>1 && (zWord[0]=='"' && zWord[nn-1]=='"') ){ | |
| 718 | 740 | zWord++; |
| 719 | - nWord -= 2; | |
| 741 | + nn -= 2; | |
| 720 | 742 | } |
| 721 | 743 | |
| 722 | - for(i=0; rc==TH_OK && i<nWord; i++){ | |
| 744 | + for(i=0; rc==TH_OK && i<nn; i++){ | |
| 723 | 745 | int nGet; |
| 724 | 746 | |
| 725 | 747 | int (*xGet)(Th_Interp *, const char*, int, int *) = 0; |
| 726 | 748 | int (*xSubst)(Th_Interp *, const char*, int) = 0; |
| 727 | 749 | |
| @@ -743,11 +765,11 @@ | ||
| 743 | 765 | thBufferAddChar(interp, &output, zWord[i]); |
| 744 | 766 | continue; /* Go to the next iteration of the for(...) loop */ |
| 745 | 767 | } |
| 746 | 768 | } |
| 747 | 769 | |
| 748 | - rc = xGet(interp, &zWord[i], nWord-i, &nGet); | |
| 770 | + rc = xGet(interp, &zWord[i], nn-i, &nGet); | |
| 749 | 771 | if( rc==TH_OK ){ |
| 750 | 772 | rc = xSubst(interp, &zWord[i], nGet); |
| 751 | 773 | } |
| 752 | 774 | if( rc==TH_OK ){ |
| 753 | 775 | const char *zRes; |
| @@ -758,11 +780,11 @@ | ||
| 758 | 780 | } |
| 759 | 781 | } |
| 760 | 782 | } |
| 761 | 783 | |
| 762 | 784 | if( rc==TH_OK ){ |
| 763 | - Th_SetResult(interp, output.zBuf, output.nBuf); | |
| 785 | + Th_SetResult(interp, output.zBuf, output.nBuf|output.bTaint); | |
| 764 | 786 | } |
| 765 | 787 | thBufferFree(interp, &output); |
| 766 | 788 | return rc; |
| 767 | 789 | } |
| 768 | 790 | |
| @@ -826,11 +848,11 @@ | ||
| 826 | 848 | Buffer strbuf; |
| 827 | 849 | Buffer lenbuf; |
| 828 | 850 | int nCount = 0; |
| 829 | 851 | |
| 830 | 852 | const char *zInput = zList; |
| 831 | - int nInput = nList; | |
| 853 | + int nInput = TH1_LEN(nList); | |
| 832 | 854 | |
| 833 | 855 | thBufferInit(&strbuf); |
| 834 | 856 | thBufferInit(&lenbuf); |
| 835 | 857 | |
| 836 | 858 | while( nInput>0 ){ |
| @@ -837,19 +859,19 @@ | ||
| 837 | 859 | const char *zWord; |
| 838 | 860 | int nWord; |
| 839 | 861 | |
| 840 | 862 | thNextSpace(interp, zInput, nInput, &nWord); |
| 841 | 863 | zInput += nWord; |
| 842 | - nInput = nList-(zInput-zList); | |
| 864 | + nInput = TH1_LEN(nList)-(zInput-zList); | |
| 843 | 865 | |
| 844 | 866 | if( TH_OK!=(rc = thNextWord(interp, zInput, nInput, &nWord, 0)) |
| 845 | 867 | || TH_OK!=(rc = thSubstWord(interp, zInput, nWord)) |
| 846 | 868 | ){ |
| 847 | 869 | goto finish; |
| 848 | 870 | } |
| 849 | - zInput = &zInput[nWord]; | |
| 850 | - nInput = nList-(zInput-zList); | |
| 871 | + zInput = &zInput[TH1_LEN(nWord)]; | |
| 872 | + nInput = TH1_LEN(nList)-(zInput-zList); | |
| 851 | 873 | if( nWord>0 ){ |
| 852 | 874 | zWord = Th_GetResult(interp, &nWord); |
| 853 | 875 | thBufferWrite(interp, &strbuf, zWord, nWord); |
| 854 | 876 | thBufferAddChar(interp, &strbuf, 0); |
| 855 | 877 | thBufferWrite(interp, &lenbuf, &nWord, sizeof(int)); |
| @@ -872,11 +894,11 @@ | ||
| 872 | 894 | zElem = (char *)&anElem[nCount]; |
| 873 | 895 | th_memcpy(anElem, lenbuf.zBuf, lenbuf.nBuf); |
| 874 | 896 | th_memcpy(zElem, strbuf.zBuf, strbuf.nBuf); |
| 875 | 897 | for(i=0; i<nCount;i++){ |
| 876 | 898 | azElem[i] = zElem; |
| 877 | - zElem += (anElem[i] + 1); | |
| 899 | + zElem += (TH1_LEN(anElem[i]) + 1); | |
| 878 | 900 | } |
| 879 | 901 | *pazElem = azElem; |
| 880 | 902 | *panElem = anElem; |
| 881 | 903 | } |
| 882 | 904 | if( pnCount ){ |
| @@ -894,12 +916,17 @@ | ||
| 894 | 916 | ** in the current stack frame. |
| 895 | 917 | */ |
| 896 | 918 | static int thEvalLocal(Th_Interp *interp, const char *zProgram, int nProgram){ |
| 897 | 919 | int rc = TH_OK; |
| 898 | 920 | const char *zInput = zProgram; |
| 899 | - int nInput = nProgram; | |
| 921 | + int nInput = TH1_LEN(nProgram); | |
| 900 | 922 | |
| 923 | + if( TH1_TAINTED(nProgram) | |
| 924 | + && Th_ReportTaint(interp, "script", zProgram, nProgram) | |
| 925 | + ){ | |
| 926 | + return TH_ERROR; | |
| 927 | + } | |
| 901 | 928 | while( rc==TH_OK && nInput ){ |
| 902 | 929 | Th_HashEntry *pEntry; |
| 903 | 930 | int nSpace; |
| 904 | 931 | const char *zFirst; |
| 905 | 932 | |
| @@ -949,13 +976,13 @@ | ||
| 949 | 976 | if( rc!=TH_OK ) continue; |
| 950 | 977 | |
| 951 | 978 | if( argc>0 ){ |
| 952 | 979 | |
| 953 | 980 | /* Look up the command name in the command hash-table. */ |
| 954 | - pEntry = Th_HashFind(interp, interp->paCmd, argv[0], argl[0], 0); | |
| 981 | + pEntry = Th_HashFind(interp, interp->paCmd, argv[0], TH1_LEN(argl[0]),0); | |
| 955 | 982 | if( !pEntry ){ |
| 956 | - Th_ErrorMessage(interp, "no such command: ", argv[0], argl[0]); | |
| 983 | + Th_ErrorMessage(interp, "no such command: ", argv[0], TH1_LEN(argl[0])); | |
| 957 | 984 | rc = TH_ERROR; |
| 958 | 985 | } |
| 959 | 986 | |
| 960 | 987 | /* Call the command procedure. */ |
| 961 | 988 | if( rc==TH_OK ){ |
| @@ -1053,10 +1080,12 @@ | ||
| 1053 | 1080 | }else{ |
| 1054 | 1081 | int nInput = nProgram; |
| 1055 | 1082 | |
| 1056 | 1083 | if( nInput<0 ){ |
| 1057 | 1084 | nInput = th_strlen(zProgram); |
| 1085 | + }else{ | |
| 1086 | + nInput = TH1_LEN(nInput); | |
| 1058 | 1087 | } |
| 1059 | 1088 | rc = thEvalLocal(interp, zProgram, nInput); |
| 1060 | 1089 | } |
| 1061 | 1090 | |
| 1062 | 1091 | interp->pFrame = pSavedFrame; |
| @@ -1095,10 +1124,12 @@ | ||
| 1095 | 1124 | int isGlobal = 0; |
| 1096 | 1125 | int i; |
| 1097 | 1126 | |
| 1098 | 1127 | if( nVarname<0 ){ |
| 1099 | 1128 | nVarname = th_strlen(zVarname); |
| 1129 | + }else{ | |
| 1130 | + nVarname = TH1_LEN(nVarname); | |
| 1100 | 1131 | } |
| 1101 | 1132 | nOuter = nVarname; |
| 1102 | 1133 | |
| 1103 | 1134 | /* If the variable name starts with "::", then do the lookup is in the |
| 1104 | 1135 | ** uppermost (global) frame. |
| @@ -1271,31 +1302,10 @@ | ||
| 1271 | 1302 | } |
| 1272 | 1303 | |
| 1273 | 1304 | return Th_SetResult(interp, pValue->zData, pValue->nData); |
| 1274 | 1305 | } |
| 1275 | 1306 | |
| 1276 | -/* | |
| 1277 | -** If interp has a variable with the given name, its value is returned | |
| 1278 | -** and its length is returned via *nOut if nOut is not NULL. If | |
| 1279 | -** interp has no such var then NULL is returned without setting any | |
| 1280 | -** error state and *nOut, if not NULL, is set to -1. The returned value | |
| 1281 | -** is owned by the interpreter and may be invalidated the next time | |
| 1282 | -** the interpreter is modified. | |
| 1283 | -*/ | |
| 1284 | -const char * Th_MaybeGetVar(Th_Interp *interp, const char *zVarName, | |
| 1285 | - int *nOut){ | |
| 1286 | - Th_Variable *pValue; | |
| 1287 | - | |
| 1288 | - pValue = thFindValue(interp, zVarName, -1, 0, 0, 1, 0); | |
| 1289 | - if( !pValue || !pValue->zData ){ | |
| 1290 | - if( nOut!=0 ) *nOut = -1; | |
| 1291 | - return NULL; | |
| 1292 | - } | |
| 1293 | - if( nOut!=0 ) *nOut = pValue->nData; | |
| 1294 | - return pValue->zData; | |
| 1295 | -} | |
| 1296 | - | |
| 1297 | 1307 | /* |
| 1298 | 1308 | ** Return true if variable (zVar, nVar) exists. |
| 1299 | 1309 | */ |
| 1300 | 1310 | int Th_ExistsVar(Th_Interp *interp, const char *zVar, int nVar){ |
| 1301 | 1311 | Th_Variable *pValue = thFindValue(interp, zVar, nVar, 0, 1, 1, 0); |
| @@ -1324,28 +1334,32 @@ | ||
| 1324 | 1334 | int nVar, |
| 1325 | 1335 | const char *zValue, |
| 1326 | 1336 | int nValue |
| 1327 | 1337 | ){ |
| 1328 | 1338 | Th_Variable *pValue; |
| 1339 | + int nn; | |
| 1329 | 1340 | |
| 1341 | + nVar = TH1_LEN(nVar); | |
| 1330 | 1342 | pValue = thFindValue(interp, zVar, nVar, 1, 0, 0, 0); |
| 1331 | 1343 | if( !pValue ){ |
| 1332 | 1344 | return TH_ERROR; |
| 1333 | 1345 | } |
| 1334 | 1346 | |
| 1335 | 1347 | if( nValue<0 ){ |
| 1336 | - nValue = th_strlen(zValue); | |
| 1348 | + nn = th_strlen(zValue); | |
| 1349 | + }else{ | |
| 1350 | + nn = TH1_LEN(nValue); | |
| 1337 | 1351 | } |
| 1338 | 1352 | if( pValue->zData ){ |
| 1339 | 1353 | Th_Free(interp, pValue->zData); |
| 1340 | 1354 | pValue->zData = 0; |
| 1341 | 1355 | } |
| 1342 | 1356 | |
| 1343 | - assert(zValue || nValue==0); | |
| 1344 | - pValue->zData = Th_Malloc(interp, nValue+1); | |
| 1345 | - pValue->zData[nValue] = '\0'; | |
| 1346 | - th_memcpy(pValue->zData, zValue, nValue); | |
| 1357 | + assert(zValue || nn==0); | |
| 1358 | + pValue->zData = Th_Malloc(interp, nn+1); | |
| 1359 | + pValue->zData[nn] = '\0'; | |
| 1360 | + th_memcpy(pValue->zData, zValue, nn); | |
| 1347 | 1361 | pValue->nData = nValue; |
| 1348 | 1362 | |
| 1349 | 1363 | return TH_OK; |
| 1350 | 1364 | } |
| 1351 | 1365 | |
| @@ -1458,10 +1472,12 @@ | ||
| 1458 | 1472 | */ |
| 1459 | 1473 | char *th_strdup(Th_Interp *interp, const char *z, int n){ |
| 1460 | 1474 | char *zRes; |
| 1461 | 1475 | if( n<0 ){ |
| 1462 | 1476 | n = th_strlen(z); |
| 1477 | + }else{ | |
| 1478 | + n = TH1_LEN(n); | |
| 1463 | 1479 | } |
| 1464 | 1480 | zRes = Th_Malloc(interp, n+1); |
| 1465 | 1481 | th_memcpy(zRes, z, n); |
| 1466 | 1482 | zRes[n] = '\0'; |
| 1467 | 1483 | return zRes; |
| @@ -1519,13 +1535,14 @@ | ||
| 1519 | 1535 | n = th_strlen(z); |
| 1520 | 1536 | } |
| 1521 | 1537 | |
| 1522 | 1538 | if( z && n>0 ){ |
| 1523 | 1539 | char *zResult; |
| 1524 | - zResult = Th_Malloc(pInterp, n+1); | |
| 1525 | - th_memcpy(zResult, z, n); | |
| 1526 | - zResult[n] = '\0'; | |
| 1540 | + int nn = TH1_LEN(n); | |
| 1541 | + zResult = Th_Malloc(pInterp, nn+1); | |
| 1542 | + th_memcpy(zResult, z, nn); | |
| 1543 | + zResult[nn] = '\0'; | |
| 1527 | 1544 | pInterp->zResult = zResult; |
| 1528 | 1545 | pInterp->nResult = n; |
| 1529 | 1546 | } |
| 1530 | 1547 | |
| 1531 | 1548 | return TH_OK; |
| @@ -1777,15 +1794,19 @@ | ||
| 1777 | 1794 | int hasSpecialChar = 0; /* Whitespace or {}[]'" */ |
| 1778 | 1795 | int hasEscapeChar = 0; /* '}' without matching '{' to the left or a '\\' */ |
| 1779 | 1796 | int nBrace = 0; |
| 1780 | 1797 | |
| 1781 | 1798 | output.zBuf = *pzList; |
| 1782 | - output.nBuf = *pnList; | |
| 1799 | + output.nBuf = TH1_LEN(*pnList); | |
| 1783 | 1800 | output.nBufAlloc = output.nBuf; |
| 1801 | + output.bTaint = 0; | |
| 1802 | + TH1_XFER_TAINT(output.bTaint, *pnList); | |
| 1784 | 1803 | |
| 1785 | 1804 | if( nElem<0 ){ |
| 1786 | 1805 | nElem = th_strlen(zElem); |
| 1806 | + }else{ | |
| 1807 | + nElem = TH1_LEN(nElem); | |
| 1787 | 1808 | } |
| 1788 | 1809 | if( output.nBuf>0 ){ |
| 1789 | 1810 | thBufferAddChar(interp, &output, ' '); |
| 1790 | 1811 | } |
| 1791 | 1812 | |
| @@ -1834,24 +1855,28 @@ | ||
| 1834 | 1855 | int *pnStr, /* IN/OUT: Current length of *pzStr */ |
| 1835 | 1856 | const char *zElem, /* Data to append */ |
| 1836 | 1857 | int nElem /* Length of nElem */ |
| 1837 | 1858 | ){ |
| 1838 | 1859 | char *zNew; |
| 1839 | - int nNew; | |
| 1860 | + long long int nNew; | |
| 1861 | + int nn; | |
| 1840 | 1862 | |
| 1841 | 1863 | if( nElem<0 ){ |
| 1842 | - nElem = th_strlen(zElem); | |
| 1864 | + nn = th_strlen(zElem); | |
| 1865 | + }else{ | |
| 1866 | + nn = TH1_LEN(nElem); | |
| 1843 | 1867 | } |
| 1844 | 1868 | |
| 1845 | - nNew = *pnStr + nElem; | |
| 1869 | + nNew = TH1_LEN(*pnStr) + nn; | |
| 1870 | + TH1_SIZECHECK(nNew); | |
| 1846 | 1871 | zNew = Th_Malloc(interp, nNew); |
| 1847 | 1872 | th_memcpy(zNew, *pzStr, *pnStr); |
| 1848 | - th_memcpy(&zNew[*pnStr], zElem, nElem); | |
| 1873 | + th_memcpy(&zNew[TH1_LEN(*pnStr)], zElem, nn); | |
| 1849 | 1874 | |
| 1850 | 1875 | Th_Free(interp, *pzStr); |
| 1851 | 1876 | *pzStr = zNew; |
| 1852 | - *pnStr = nNew; | |
| 1877 | + *pnStr = (int)nNew; | |
| 1853 | 1878 | |
| 1854 | 1879 | return TH_OK; |
| 1855 | 1880 | } |
| 1856 | 1881 | |
| 1857 | 1882 | /* |
| @@ -2106,16 +2131,18 @@ | ||
| 2106 | 2131 | /* Evaluate left and right arguments, if they exist. */ |
| 2107 | 2132 | if( pExpr->pLeft ){ |
| 2108 | 2133 | rc = exprEval(interp, pExpr->pLeft); |
| 2109 | 2134 | if( rc==TH_OK ){ |
| 2110 | 2135 | zLeft = Th_TakeResult(interp, &nLeft); |
| 2136 | + nLeft = TH1_LEN(nLeft); | |
| 2111 | 2137 | } |
| 2112 | 2138 | } |
| 2113 | 2139 | if( rc==TH_OK && pExpr->pRight ){ |
| 2114 | 2140 | rc = exprEval(interp, pExpr->pRight); |
| 2115 | 2141 | if( rc==TH_OK ){ |
| 2116 | 2142 | zRight = Th_TakeResult(interp, &nRight); |
| 2143 | + nRight = TH1_LEN(nRight); | |
| 2117 | 2144 | } |
| 2118 | 2145 | } |
| 2119 | 2146 | |
| 2120 | 2147 | /* Convert arguments to their required forms. */ |
| 2121 | 2148 | if( rc==TH_OK ){ |
| @@ -2160,12 +2187,15 @@ | ||
| 2160 | 2187 | } |
| 2161 | 2188 | iRes = iLeft%iRight; |
| 2162 | 2189 | break; |
| 2163 | 2190 | case OP_ADD: iRes = iLeft+iRight; break; |
| 2164 | 2191 | case OP_SUBTRACT: iRes = iLeft-iRight; break; |
| 2165 | - case OP_LEFTSHIFT: iRes = iLeft<<iRight; break; | |
| 2166 | - case OP_RIGHTSHIFT: iRes = iLeft>>iRight; break; | |
| 2192 | + case OP_LEFTSHIFT: { | |
| 2193 | + iRes = (int)(((unsigned int)iLeft)<<(iRight&0x1f)); | |
| 2194 | + break; | |
| 2195 | + } | |
| 2196 | + case OP_RIGHTSHIFT: iRes = iLeft>>(iRight&0x1f); break; | |
| 2167 | 2197 | case OP_LT: iRes = iLeft<iRight; break; |
| 2168 | 2198 | case OP_GT: iRes = iLeft>iRight; break; |
| 2169 | 2199 | case OP_LE: iRes = iLeft<=iRight; break; |
| 2170 | 2200 | case OP_GE: iRes = iLeft>=iRight; break; |
| 2171 | 2201 | case OP_EQ: iRes = iLeft==iRight; break; |
| @@ -2453,10 +2483,12 @@ | ||
| 2453 | 2483 | int nToken = 0; |
| 2454 | 2484 | Expr **apToken = 0; |
| 2455 | 2485 | |
| 2456 | 2486 | if( nExpr<0 ){ |
| 2457 | 2487 | nExpr = th_strlen(zExpr); |
| 2488 | + }else{ | |
| 2489 | + nExpr = TH1_LEN(nExpr); | |
| 2458 | 2490 | } |
| 2459 | 2491 | |
| 2460 | 2492 | /* Parse the expression to a list of tokens. */ |
| 2461 | 2493 | rc = exprParse(interp, zExpr, nExpr, &apToken, &nToken); |
| 2462 | 2494 | |
| @@ -2564,10 +2596,12 @@ | ||
| 2564 | 2596 | Th_HashEntry *pRet; |
| 2565 | 2597 | Th_HashEntry **ppRet; |
| 2566 | 2598 | |
| 2567 | 2599 | if( nKey<0 ){ |
| 2568 | 2600 | nKey = th_strlen(zKey); |
| 2601 | + }else{ | |
| 2602 | + nKey = TH1_LEN(nKey); | |
| 2569 | 2603 | } |
| 2570 | 2604 | |
| 2571 | 2605 | for(i=0; i<nKey; i++){ |
| 2572 | 2606 | iKey = (iKey<<3) ^ iKey ^ zKey[i]; |
| 2573 | 2607 | } |
| @@ -2797,10 +2831,12 @@ | ||
| 2797 | 2831 | int base = 10; |
| 2798 | 2832 | int (*isdigit)(char) = th_isdigit; |
| 2799 | 2833 | |
| 2800 | 2834 | if( n<0 ){ |
| 2801 | 2835 | n = th_strlen(z); |
| 2836 | + }else{ | |
| 2837 | + n = TH1_LEN(n); | |
| 2802 | 2838 | } |
| 2803 | 2839 | |
| 2804 | 2840 | if( n>1 && (z[0]=='-' || z[0]=='+') ){ |
| 2805 | 2841 | i = 1; |
| 2806 | 2842 | } |
| @@ -2856,11 +2892,11 @@ | ||
| 2856 | 2892 | const char *z, |
| 2857 | 2893 | int n, |
| 2858 | 2894 | double *pfOut |
| 2859 | 2895 | ){ |
| 2860 | 2896 | if( !sqlite3IsNumber((const char *)z, 0) ){ |
| 2861 | - Th_ErrorMessage(interp, "expected number, got: \"", z, n); | |
| 2897 | + Th_ErrorMessage(interp, "expected number, got: \"", z, TH1_LEN(n)); | |
| 2862 | 2898 | return TH_ERROR; |
| 2863 | 2899 | } |
| 2864 | 2900 | |
| 2865 | 2901 | sqlite3AtoF((const char *)z, pfOut); |
| 2866 | 2902 | return TH_OK; |
| @@ -2875,10 +2911,13 @@ | ||
| 2875 | 2911 | unsigned int uVal = iVal; |
| 2876 | 2912 | char zBuf[32]; |
| 2877 | 2913 | char *z = &zBuf[32]; |
| 2878 | 2914 | |
| 2879 | 2915 | if( iVal<0 ){ |
| 2916 | + if( iVal==0x80000000 ){ | |
| 2917 | + return Th_SetResult(interp, "-2147483648", -1); | |
| 2918 | + } | |
| 2880 | 2919 | isNegative = 1; |
| 2881 | 2920 | uVal = iVal * -1; |
| 2882 | 2921 | } |
| 2883 | 2922 | *(--z) = '\0'; |
| 2884 | 2923 | *(--z) = (char)(48+(uVal%10)); |
| 2885 | 2924 |
| --- src/th.c | |
| +++ src/th.c | |
| @@ -7,10 +7,16 @@ | |
| 7 | #include "config.h" |
| 8 | #include "th.h" |
| 9 | #include <string.h> |
| 10 | #include <assert.h> |
| 11 | |
| 12 | /* |
| 13 | ** Values used for element values in the tcl_platform array. |
| 14 | */ |
| 15 | |
| 16 | #if !defined(TH_ENGINE) |
| @@ -197,10 +203,11 @@ | |
| 197 | */ |
| 198 | struct Buffer { |
| 199 | char *zBuf; |
| 200 | int nBuf; |
| 201 | int nBufAlloc; |
| 202 | }; |
| 203 | typedef struct Buffer Buffer; |
| 204 | static void thBufferInit(Buffer *); |
| 205 | static void thBufferFree(Th_Interp *interp, Buffer *); |
| 206 | |
| @@ -209,10 +216,18 @@ | |
| 209 | ** be NULL as long as the number of bytes to copy is zero. |
| 210 | */ |
| 211 | static void th_memcpy(void *dest, const void *src, size_t n){ |
| 212 | if( n>0 ) memcpy(dest,src,n); |
| 213 | } |
| 214 | |
| 215 | /* |
| 216 | ** Append nAdd bytes of content copied from zAdd to the end of buffer |
| 217 | ** pBuffer. If there is not enough space currently allocated, resize |
| 218 | ** the allocation to make space. |
| @@ -219,40 +234,46 @@ | |
| 219 | */ |
| 220 | static void thBufferWriteResize( |
| 221 | Th_Interp *interp, |
| 222 | Buffer *pBuffer, |
| 223 | const char *zAdd, |
| 224 | int nAdd |
| 225 | ){ |
| 226 | int nNew = (pBuffer->nBuf+nAdd)*2+32; |
| 227 | #if defined(TH_MEMDEBUG) |
| 228 | char *zNew = (char *)Th_Malloc(interp, nNew); |
| 229 | th_memcpy(zNew, pBuffer->zBuf, pBuffer->nBuf); |
| 230 | Th_Free(interp, pBuffer->zBuf); |
| 231 | pBuffer->zBuf = zNew; |
| 232 | #else |
| 233 | int nOld = pBuffer->nBufAlloc; |
| 234 | pBuffer->zBuf = Th_Realloc(interp, pBuffer->zBuf, nNew); |
| 235 | memset(pBuffer->zBuf+nOld, 0, nNew-nOld); |
| 236 | #endif |
| 237 | pBuffer->nBufAlloc = nNew; |
| 238 | th_memcpy(&pBuffer->zBuf[pBuffer->nBuf], zAdd, nAdd); |
| 239 | pBuffer->nBuf += nAdd; |
| 240 | } |
| 241 | static void thBufferWriteFast( |
| 242 | Th_Interp *interp, |
| 243 | Buffer *pBuffer, |
| 244 | const char *zAdd, |
| 245 | int nAdd |
| 246 | ){ |
| 247 | if( pBuffer->nBuf+nAdd > pBuffer->nBufAlloc ){ |
| 248 | thBufferWriteResize(interp, pBuffer, zAdd, nAdd); |
| 249 | }else{ |
| 250 | if( pBuffer->zBuf ){ |
| 251 | memcpy(pBuffer->zBuf + pBuffer->nBuf, zAdd, nAdd); |
| 252 | } |
| 253 | pBuffer->nBuf += nAdd; |
| 254 | } |
| 255 | } |
| 256 | #define thBufferWrite(a,b,c,d) thBufferWriteFast(a,b,(const char *)c,d) |
| 257 | |
| 258 | /* |
| @@ -704,24 +725,25 @@ | |
| 704 | int nWord |
| 705 | ){ |
| 706 | int rc = TH_OK; |
| 707 | Buffer output; |
| 708 | int i; |
| 709 | |
| 710 | thBufferInit(&output); |
| 711 | |
| 712 | if( nWord>1 && (zWord[0]=='{' && zWord[nWord-1]=='}') ){ |
| 713 | thBufferWrite(interp, &output, &zWord[1], nWord-2); |
| 714 | }else{ |
| 715 | |
| 716 | /* If the word is surrounded by double-quotes strip these away. */ |
| 717 | if( nWord>1 && (zWord[0]=='"' && zWord[nWord-1]=='"') ){ |
| 718 | zWord++; |
| 719 | nWord -= 2; |
| 720 | } |
| 721 | |
| 722 | for(i=0; rc==TH_OK && i<nWord; i++){ |
| 723 | int nGet; |
| 724 | |
| 725 | int (*xGet)(Th_Interp *, const char*, int, int *) = 0; |
| 726 | int (*xSubst)(Th_Interp *, const char*, int) = 0; |
| 727 | |
| @@ -743,11 +765,11 @@ | |
| 743 | thBufferAddChar(interp, &output, zWord[i]); |
| 744 | continue; /* Go to the next iteration of the for(...) loop */ |
| 745 | } |
| 746 | } |
| 747 | |
| 748 | rc = xGet(interp, &zWord[i], nWord-i, &nGet); |
| 749 | if( rc==TH_OK ){ |
| 750 | rc = xSubst(interp, &zWord[i], nGet); |
| 751 | } |
| 752 | if( rc==TH_OK ){ |
| 753 | const char *zRes; |
| @@ -758,11 +780,11 @@ | |
| 758 | } |
| 759 | } |
| 760 | } |
| 761 | |
| 762 | if( rc==TH_OK ){ |
| 763 | Th_SetResult(interp, output.zBuf, output.nBuf); |
| 764 | } |
| 765 | thBufferFree(interp, &output); |
| 766 | return rc; |
| 767 | } |
| 768 | |
| @@ -826,11 +848,11 @@ | |
| 826 | Buffer strbuf; |
| 827 | Buffer lenbuf; |
| 828 | int nCount = 0; |
| 829 | |
| 830 | const char *zInput = zList; |
| 831 | int nInput = nList; |
| 832 | |
| 833 | thBufferInit(&strbuf); |
| 834 | thBufferInit(&lenbuf); |
| 835 | |
| 836 | while( nInput>0 ){ |
| @@ -837,19 +859,19 @@ | |
| 837 | const char *zWord; |
| 838 | int nWord; |
| 839 | |
| 840 | thNextSpace(interp, zInput, nInput, &nWord); |
| 841 | zInput += nWord; |
| 842 | nInput = nList-(zInput-zList); |
| 843 | |
| 844 | if( TH_OK!=(rc = thNextWord(interp, zInput, nInput, &nWord, 0)) |
| 845 | || TH_OK!=(rc = thSubstWord(interp, zInput, nWord)) |
| 846 | ){ |
| 847 | goto finish; |
| 848 | } |
| 849 | zInput = &zInput[nWord]; |
| 850 | nInput = nList-(zInput-zList); |
| 851 | if( nWord>0 ){ |
| 852 | zWord = Th_GetResult(interp, &nWord); |
| 853 | thBufferWrite(interp, &strbuf, zWord, nWord); |
| 854 | thBufferAddChar(interp, &strbuf, 0); |
| 855 | thBufferWrite(interp, &lenbuf, &nWord, sizeof(int)); |
| @@ -872,11 +894,11 @@ | |
| 872 | zElem = (char *)&anElem[nCount]; |
| 873 | th_memcpy(anElem, lenbuf.zBuf, lenbuf.nBuf); |
| 874 | th_memcpy(zElem, strbuf.zBuf, strbuf.nBuf); |
| 875 | for(i=0; i<nCount;i++){ |
| 876 | azElem[i] = zElem; |
| 877 | zElem += (anElem[i] + 1); |
| 878 | } |
| 879 | *pazElem = azElem; |
| 880 | *panElem = anElem; |
| 881 | } |
| 882 | if( pnCount ){ |
| @@ -894,12 +916,17 @@ | |
| 894 | ** in the current stack frame. |
| 895 | */ |
| 896 | static int thEvalLocal(Th_Interp *interp, const char *zProgram, int nProgram){ |
| 897 | int rc = TH_OK; |
| 898 | const char *zInput = zProgram; |
| 899 | int nInput = nProgram; |
| 900 | |
| 901 | while( rc==TH_OK && nInput ){ |
| 902 | Th_HashEntry *pEntry; |
| 903 | int nSpace; |
| 904 | const char *zFirst; |
| 905 | |
| @@ -949,13 +976,13 @@ | |
| 949 | if( rc!=TH_OK ) continue; |
| 950 | |
| 951 | if( argc>0 ){ |
| 952 | |
| 953 | /* Look up the command name in the command hash-table. */ |
| 954 | pEntry = Th_HashFind(interp, interp->paCmd, argv[0], argl[0], 0); |
| 955 | if( !pEntry ){ |
| 956 | Th_ErrorMessage(interp, "no such command: ", argv[0], argl[0]); |
| 957 | rc = TH_ERROR; |
| 958 | } |
| 959 | |
| 960 | /* Call the command procedure. */ |
| 961 | if( rc==TH_OK ){ |
| @@ -1053,10 +1080,12 @@ | |
| 1053 | }else{ |
| 1054 | int nInput = nProgram; |
| 1055 | |
| 1056 | if( nInput<0 ){ |
| 1057 | nInput = th_strlen(zProgram); |
| 1058 | } |
| 1059 | rc = thEvalLocal(interp, zProgram, nInput); |
| 1060 | } |
| 1061 | |
| 1062 | interp->pFrame = pSavedFrame; |
| @@ -1095,10 +1124,12 @@ | |
| 1095 | int isGlobal = 0; |
| 1096 | int i; |
| 1097 | |
| 1098 | if( nVarname<0 ){ |
| 1099 | nVarname = th_strlen(zVarname); |
| 1100 | } |
| 1101 | nOuter = nVarname; |
| 1102 | |
| 1103 | /* If the variable name starts with "::", then do the lookup is in the |
| 1104 | ** uppermost (global) frame. |
| @@ -1271,31 +1302,10 @@ | |
| 1271 | } |
| 1272 | |
| 1273 | return Th_SetResult(interp, pValue->zData, pValue->nData); |
| 1274 | } |
| 1275 | |
| 1276 | /* |
| 1277 | ** If interp has a variable with the given name, its value is returned |
| 1278 | ** and its length is returned via *nOut if nOut is not NULL. If |
| 1279 | ** interp has no such var then NULL is returned without setting any |
| 1280 | ** error state and *nOut, if not NULL, is set to -1. The returned value |
| 1281 | ** is owned by the interpreter and may be invalidated the next time |
| 1282 | ** the interpreter is modified. |
| 1283 | */ |
| 1284 | const char * Th_MaybeGetVar(Th_Interp *interp, const char *zVarName, |
| 1285 | int *nOut){ |
| 1286 | Th_Variable *pValue; |
| 1287 | |
| 1288 | pValue = thFindValue(interp, zVarName, -1, 0, 0, 1, 0); |
| 1289 | if( !pValue || !pValue->zData ){ |
| 1290 | if( nOut!=0 ) *nOut = -1; |
| 1291 | return NULL; |
| 1292 | } |
| 1293 | if( nOut!=0 ) *nOut = pValue->nData; |
| 1294 | return pValue->zData; |
| 1295 | } |
| 1296 | |
| 1297 | /* |
| 1298 | ** Return true if variable (zVar, nVar) exists. |
| 1299 | */ |
| 1300 | int Th_ExistsVar(Th_Interp *interp, const char *zVar, int nVar){ |
| 1301 | Th_Variable *pValue = thFindValue(interp, zVar, nVar, 0, 1, 1, 0); |
| @@ -1324,28 +1334,32 @@ | |
| 1324 | int nVar, |
| 1325 | const char *zValue, |
| 1326 | int nValue |
| 1327 | ){ |
| 1328 | Th_Variable *pValue; |
| 1329 | |
| 1330 | pValue = thFindValue(interp, zVar, nVar, 1, 0, 0, 0); |
| 1331 | if( !pValue ){ |
| 1332 | return TH_ERROR; |
| 1333 | } |
| 1334 | |
| 1335 | if( nValue<0 ){ |
| 1336 | nValue = th_strlen(zValue); |
| 1337 | } |
| 1338 | if( pValue->zData ){ |
| 1339 | Th_Free(interp, pValue->zData); |
| 1340 | pValue->zData = 0; |
| 1341 | } |
| 1342 | |
| 1343 | assert(zValue || nValue==0); |
| 1344 | pValue->zData = Th_Malloc(interp, nValue+1); |
| 1345 | pValue->zData[nValue] = '\0'; |
| 1346 | th_memcpy(pValue->zData, zValue, nValue); |
| 1347 | pValue->nData = nValue; |
| 1348 | |
| 1349 | return TH_OK; |
| 1350 | } |
| 1351 | |
| @@ -1458,10 +1472,12 @@ | |
| 1458 | */ |
| 1459 | char *th_strdup(Th_Interp *interp, const char *z, int n){ |
| 1460 | char *zRes; |
| 1461 | if( n<0 ){ |
| 1462 | n = th_strlen(z); |
| 1463 | } |
| 1464 | zRes = Th_Malloc(interp, n+1); |
| 1465 | th_memcpy(zRes, z, n); |
| 1466 | zRes[n] = '\0'; |
| 1467 | return zRes; |
| @@ -1519,13 +1535,14 @@ | |
| 1519 | n = th_strlen(z); |
| 1520 | } |
| 1521 | |
| 1522 | if( z && n>0 ){ |
| 1523 | char *zResult; |
| 1524 | zResult = Th_Malloc(pInterp, n+1); |
| 1525 | th_memcpy(zResult, z, n); |
| 1526 | zResult[n] = '\0'; |
| 1527 | pInterp->zResult = zResult; |
| 1528 | pInterp->nResult = n; |
| 1529 | } |
| 1530 | |
| 1531 | return TH_OK; |
| @@ -1777,15 +1794,19 @@ | |
| 1777 | int hasSpecialChar = 0; /* Whitespace or {}[]'" */ |
| 1778 | int hasEscapeChar = 0; /* '}' without matching '{' to the left or a '\\' */ |
| 1779 | int nBrace = 0; |
| 1780 | |
| 1781 | output.zBuf = *pzList; |
| 1782 | output.nBuf = *pnList; |
| 1783 | output.nBufAlloc = output.nBuf; |
| 1784 | |
| 1785 | if( nElem<0 ){ |
| 1786 | nElem = th_strlen(zElem); |
| 1787 | } |
| 1788 | if( output.nBuf>0 ){ |
| 1789 | thBufferAddChar(interp, &output, ' '); |
| 1790 | } |
| 1791 | |
| @@ -1834,24 +1855,28 @@ | |
| 1834 | int *pnStr, /* IN/OUT: Current length of *pzStr */ |
| 1835 | const char *zElem, /* Data to append */ |
| 1836 | int nElem /* Length of nElem */ |
| 1837 | ){ |
| 1838 | char *zNew; |
| 1839 | int nNew; |
| 1840 | |
| 1841 | if( nElem<0 ){ |
| 1842 | nElem = th_strlen(zElem); |
| 1843 | } |
| 1844 | |
| 1845 | nNew = *pnStr + nElem; |
| 1846 | zNew = Th_Malloc(interp, nNew); |
| 1847 | th_memcpy(zNew, *pzStr, *pnStr); |
| 1848 | th_memcpy(&zNew[*pnStr], zElem, nElem); |
| 1849 | |
| 1850 | Th_Free(interp, *pzStr); |
| 1851 | *pzStr = zNew; |
| 1852 | *pnStr = nNew; |
| 1853 | |
| 1854 | return TH_OK; |
| 1855 | } |
| 1856 | |
| 1857 | /* |
| @@ -2106,16 +2131,18 @@ | |
| 2106 | /* Evaluate left and right arguments, if they exist. */ |
| 2107 | if( pExpr->pLeft ){ |
| 2108 | rc = exprEval(interp, pExpr->pLeft); |
| 2109 | if( rc==TH_OK ){ |
| 2110 | zLeft = Th_TakeResult(interp, &nLeft); |
| 2111 | } |
| 2112 | } |
| 2113 | if( rc==TH_OK && pExpr->pRight ){ |
| 2114 | rc = exprEval(interp, pExpr->pRight); |
| 2115 | if( rc==TH_OK ){ |
| 2116 | zRight = Th_TakeResult(interp, &nRight); |
| 2117 | } |
| 2118 | } |
| 2119 | |
| 2120 | /* Convert arguments to their required forms. */ |
| 2121 | if( rc==TH_OK ){ |
| @@ -2160,12 +2187,15 @@ | |
| 2160 | } |
| 2161 | iRes = iLeft%iRight; |
| 2162 | break; |
| 2163 | case OP_ADD: iRes = iLeft+iRight; break; |
| 2164 | case OP_SUBTRACT: iRes = iLeft-iRight; break; |
| 2165 | case OP_LEFTSHIFT: iRes = iLeft<<iRight; break; |
| 2166 | case OP_RIGHTSHIFT: iRes = iLeft>>iRight; break; |
| 2167 | case OP_LT: iRes = iLeft<iRight; break; |
| 2168 | case OP_GT: iRes = iLeft>iRight; break; |
| 2169 | case OP_LE: iRes = iLeft<=iRight; break; |
| 2170 | case OP_GE: iRes = iLeft>=iRight; break; |
| 2171 | case OP_EQ: iRes = iLeft==iRight; break; |
| @@ -2453,10 +2483,12 @@ | |
| 2453 | int nToken = 0; |
| 2454 | Expr **apToken = 0; |
| 2455 | |
| 2456 | if( nExpr<0 ){ |
| 2457 | nExpr = th_strlen(zExpr); |
| 2458 | } |
| 2459 | |
| 2460 | /* Parse the expression to a list of tokens. */ |
| 2461 | rc = exprParse(interp, zExpr, nExpr, &apToken, &nToken); |
| 2462 | |
| @@ -2564,10 +2596,12 @@ | |
| 2564 | Th_HashEntry *pRet; |
| 2565 | Th_HashEntry **ppRet; |
| 2566 | |
| 2567 | if( nKey<0 ){ |
| 2568 | nKey = th_strlen(zKey); |
| 2569 | } |
| 2570 | |
| 2571 | for(i=0; i<nKey; i++){ |
| 2572 | iKey = (iKey<<3) ^ iKey ^ zKey[i]; |
| 2573 | } |
| @@ -2797,10 +2831,12 @@ | |
| 2797 | int base = 10; |
| 2798 | int (*isdigit)(char) = th_isdigit; |
| 2799 | |
| 2800 | if( n<0 ){ |
| 2801 | n = th_strlen(z); |
| 2802 | } |
| 2803 | |
| 2804 | if( n>1 && (z[0]=='-' || z[0]=='+') ){ |
| 2805 | i = 1; |
| 2806 | } |
| @@ -2856,11 +2892,11 @@ | |
| 2856 | const char *z, |
| 2857 | int n, |
| 2858 | double *pfOut |
| 2859 | ){ |
| 2860 | if( !sqlite3IsNumber((const char *)z, 0) ){ |
| 2861 | Th_ErrorMessage(interp, "expected number, got: \"", z, n); |
| 2862 | return TH_ERROR; |
| 2863 | } |
| 2864 | |
| 2865 | sqlite3AtoF((const char *)z, pfOut); |
| 2866 | return TH_OK; |
| @@ -2875,10 +2911,13 @@ | |
| 2875 | unsigned int uVal = iVal; |
| 2876 | char zBuf[32]; |
| 2877 | char *z = &zBuf[32]; |
| 2878 | |
| 2879 | if( iVal<0 ){ |
| 2880 | isNegative = 1; |
| 2881 | uVal = iVal * -1; |
| 2882 | } |
| 2883 | *(--z) = '\0'; |
| 2884 | *(--z) = (char)(48+(uVal%10)); |
| 2885 |
| --- src/th.c | |
| +++ src/th.c | |
| @@ -7,10 +7,16 @@ | |
| 7 | #include "config.h" |
| 8 | #include "th.h" |
| 9 | #include <string.h> |
| 10 | #include <assert.h> |
| 11 | |
| 12 | /* |
| 13 | ** External routines |
| 14 | */ |
| 15 | void fossil_panic(const char*,...); |
| 16 | void fossil_errorlog(const char*,...); |
| 17 | |
| 18 | /* |
| 19 | ** Values used for element values in the tcl_platform array. |
| 20 | */ |
| 21 | |
| 22 | #if !defined(TH_ENGINE) |
| @@ -197,10 +203,11 @@ | |
| 203 | */ |
| 204 | struct Buffer { |
| 205 | char *zBuf; |
| 206 | int nBuf; |
| 207 | int nBufAlloc; |
| 208 | int bTaint; |
| 209 | }; |
| 210 | typedef struct Buffer Buffer; |
| 211 | static void thBufferInit(Buffer *); |
| 212 | static void thBufferFree(Th_Interp *interp, Buffer *); |
| 213 | |
| @@ -209,10 +216,18 @@ | |
| 216 | ** be NULL as long as the number of bytes to copy is zero. |
| 217 | */ |
| 218 | static void th_memcpy(void *dest, const void *src, size_t n){ |
| 219 | if( n>0 ) memcpy(dest,src,n); |
| 220 | } |
| 221 | |
| 222 | /* |
| 223 | ** An oversized string has been encountered. Do not try to recover. |
| 224 | ** Panic the process. |
| 225 | */ |
| 226 | void Th_OversizeString(void){ |
| 227 | fossil_panic("string too large. maximum size 286MB."); |
| 228 | } |
| 229 | |
| 230 | /* |
| 231 | ** Append nAdd bytes of content copied from zAdd to the end of buffer |
| 232 | ** pBuffer. If there is not enough space currently allocated, resize |
| 233 | ** the allocation to make space. |
| @@ -219,40 +234,46 @@ | |
| 234 | */ |
| 235 | static void thBufferWriteResize( |
| 236 | Th_Interp *interp, |
| 237 | Buffer *pBuffer, |
| 238 | const char *zAdd, |
| 239 | int nAddX |
| 240 | ){ |
| 241 | int nAdd = TH1_LEN(nAddX); |
| 242 | int nNew = (pBuffer->nBuf+nAdd)*2+32; |
| 243 | #if defined(TH_MEMDEBUG) |
| 244 | char *zNew = (char *)Th_Malloc(interp, nNew); |
| 245 | TH1_SIZECHECK(nNew); |
| 246 | th_memcpy(zNew, pBuffer->zBuf, pBuffer->nBuf); |
| 247 | Th_Free(interp, pBuffer->zBuf); |
| 248 | pBuffer->zBuf = zNew; |
| 249 | #else |
| 250 | int nOld = pBuffer->nBufAlloc; |
| 251 | TH1_SIZECHECK(nNew); |
| 252 | pBuffer->zBuf = Th_Realloc(interp, pBuffer->zBuf, nNew); |
| 253 | memset(pBuffer->zBuf+nOld, 0, nNew-nOld); |
| 254 | #endif |
| 255 | pBuffer->nBufAlloc = nNew; |
| 256 | th_memcpy(&pBuffer->zBuf[pBuffer->nBuf], zAdd, nAdd); |
| 257 | pBuffer->nBuf += nAdd; |
| 258 | TH1_XFER_TAINT(pBuffer->bTaint, nAddX); |
| 259 | } |
| 260 | static void thBufferWriteFast( |
| 261 | Th_Interp *interp, |
| 262 | Buffer *pBuffer, |
| 263 | const char *zAdd, |
| 264 | int nAddX |
| 265 | ){ |
| 266 | int nAdd = TH1_LEN(nAddX); |
| 267 | if( pBuffer->nBuf+nAdd > pBuffer->nBufAlloc ){ |
| 268 | thBufferWriteResize(interp, pBuffer, zAdd, nAddX); |
| 269 | }else{ |
| 270 | if( pBuffer->zBuf ){ |
| 271 | memcpy(pBuffer->zBuf + pBuffer->nBuf, zAdd, nAdd); |
| 272 | } |
| 273 | pBuffer->nBuf += nAdd; |
| 274 | TH1_XFER_TAINT(pBuffer->bTaint, nAddX); |
| 275 | } |
| 276 | } |
| 277 | #define thBufferWrite(a,b,c,d) thBufferWriteFast(a,b,(const char *)c,d) |
| 278 | |
| 279 | /* |
| @@ -704,24 +725,25 @@ | |
| 725 | int nWord |
| 726 | ){ |
| 727 | int rc = TH_OK; |
| 728 | Buffer output; |
| 729 | int i; |
| 730 | int nn = TH1_LEN(nWord); |
| 731 | |
| 732 | thBufferInit(&output); |
| 733 | |
| 734 | if( nn>1 && (zWord[0]=='{' && zWord[nn-1]=='}') ){ |
| 735 | thBufferWrite(interp, &output, &zWord[1], nn-2); |
| 736 | }else{ |
| 737 | |
| 738 | /* If the word is surrounded by double-quotes strip these away. */ |
| 739 | if( nn>1 && (zWord[0]=='"' && zWord[nn-1]=='"') ){ |
| 740 | zWord++; |
| 741 | nn -= 2; |
| 742 | } |
| 743 | |
| 744 | for(i=0; rc==TH_OK && i<nn; i++){ |
| 745 | int nGet; |
| 746 | |
| 747 | int (*xGet)(Th_Interp *, const char*, int, int *) = 0; |
| 748 | int (*xSubst)(Th_Interp *, const char*, int) = 0; |
| 749 | |
| @@ -743,11 +765,11 @@ | |
| 765 | thBufferAddChar(interp, &output, zWord[i]); |
| 766 | continue; /* Go to the next iteration of the for(...) loop */ |
| 767 | } |
| 768 | } |
| 769 | |
| 770 | rc = xGet(interp, &zWord[i], nn-i, &nGet); |
| 771 | if( rc==TH_OK ){ |
| 772 | rc = xSubst(interp, &zWord[i], nGet); |
| 773 | } |
| 774 | if( rc==TH_OK ){ |
| 775 | const char *zRes; |
| @@ -758,11 +780,11 @@ | |
| 780 | } |
| 781 | } |
| 782 | } |
| 783 | |
| 784 | if( rc==TH_OK ){ |
| 785 | Th_SetResult(interp, output.zBuf, output.nBuf|output.bTaint); |
| 786 | } |
| 787 | thBufferFree(interp, &output); |
| 788 | return rc; |
| 789 | } |
| 790 | |
| @@ -826,11 +848,11 @@ | |
| 848 | Buffer strbuf; |
| 849 | Buffer lenbuf; |
| 850 | int nCount = 0; |
| 851 | |
| 852 | const char *zInput = zList; |
| 853 | int nInput = TH1_LEN(nList); |
| 854 | |
| 855 | thBufferInit(&strbuf); |
| 856 | thBufferInit(&lenbuf); |
| 857 | |
| 858 | while( nInput>0 ){ |
| @@ -837,19 +859,19 @@ | |
| 859 | const char *zWord; |
| 860 | int nWord; |
| 861 | |
| 862 | thNextSpace(interp, zInput, nInput, &nWord); |
| 863 | zInput += nWord; |
| 864 | nInput = TH1_LEN(nList)-(zInput-zList); |
| 865 | |
| 866 | if( TH_OK!=(rc = thNextWord(interp, zInput, nInput, &nWord, 0)) |
| 867 | || TH_OK!=(rc = thSubstWord(interp, zInput, nWord)) |
| 868 | ){ |
| 869 | goto finish; |
| 870 | } |
| 871 | zInput = &zInput[TH1_LEN(nWord)]; |
| 872 | nInput = TH1_LEN(nList)-(zInput-zList); |
| 873 | if( nWord>0 ){ |
| 874 | zWord = Th_GetResult(interp, &nWord); |
| 875 | thBufferWrite(interp, &strbuf, zWord, nWord); |
| 876 | thBufferAddChar(interp, &strbuf, 0); |
| 877 | thBufferWrite(interp, &lenbuf, &nWord, sizeof(int)); |
| @@ -872,11 +894,11 @@ | |
| 894 | zElem = (char *)&anElem[nCount]; |
| 895 | th_memcpy(anElem, lenbuf.zBuf, lenbuf.nBuf); |
| 896 | th_memcpy(zElem, strbuf.zBuf, strbuf.nBuf); |
| 897 | for(i=0; i<nCount;i++){ |
| 898 | azElem[i] = zElem; |
| 899 | zElem += (TH1_LEN(anElem[i]) + 1); |
| 900 | } |
| 901 | *pazElem = azElem; |
| 902 | *panElem = anElem; |
| 903 | } |
| 904 | if( pnCount ){ |
| @@ -894,12 +916,17 @@ | |
| 916 | ** in the current stack frame. |
| 917 | */ |
| 918 | static int thEvalLocal(Th_Interp *interp, const char *zProgram, int nProgram){ |
| 919 | int rc = TH_OK; |
| 920 | const char *zInput = zProgram; |
| 921 | int nInput = TH1_LEN(nProgram); |
| 922 | |
| 923 | if( TH1_TAINTED(nProgram) |
| 924 | && Th_ReportTaint(interp, "script", zProgram, nProgram) |
| 925 | ){ |
| 926 | return TH_ERROR; |
| 927 | } |
| 928 | while( rc==TH_OK && nInput ){ |
| 929 | Th_HashEntry *pEntry; |
| 930 | int nSpace; |
| 931 | const char *zFirst; |
| 932 | |
| @@ -949,13 +976,13 @@ | |
| 976 | if( rc!=TH_OK ) continue; |
| 977 | |
| 978 | if( argc>0 ){ |
| 979 | |
| 980 | /* Look up the command name in the command hash-table. */ |
| 981 | pEntry = Th_HashFind(interp, interp->paCmd, argv[0], TH1_LEN(argl[0]),0); |
| 982 | if( !pEntry ){ |
| 983 | Th_ErrorMessage(interp, "no such command: ", argv[0], TH1_LEN(argl[0])); |
| 984 | rc = TH_ERROR; |
| 985 | } |
| 986 | |
| 987 | /* Call the command procedure. */ |
| 988 | if( rc==TH_OK ){ |
| @@ -1053,10 +1080,12 @@ | |
| 1080 | }else{ |
| 1081 | int nInput = nProgram; |
| 1082 | |
| 1083 | if( nInput<0 ){ |
| 1084 | nInput = th_strlen(zProgram); |
| 1085 | }else{ |
| 1086 | nInput = TH1_LEN(nInput); |
| 1087 | } |
| 1088 | rc = thEvalLocal(interp, zProgram, nInput); |
| 1089 | } |
| 1090 | |
| 1091 | interp->pFrame = pSavedFrame; |
| @@ -1095,10 +1124,12 @@ | |
| 1124 | int isGlobal = 0; |
| 1125 | int i; |
| 1126 | |
| 1127 | if( nVarname<0 ){ |
| 1128 | nVarname = th_strlen(zVarname); |
| 1129 | }else{ |
| 1130 | nVarname = TH1_LEN(nVarname); |
| 1131 | } |
| 1132 | nOuter = nVarname; |
| 1133 | |
| 1134 | /* If the variable name starts with "::", then do the lookup is in the |
| 1135 | ** uppermost (global) frame. |
| @@ -1271,31 +1302,10 @@ | |
| 1302 | } |
| 1303 | |
| 1304 | return Th_SetResult(interp, pValue->zData, pValue->nData); |
| 1305 | } |
| 1306 | |
| 1307 | /* |
| 1308 | ** Return true if variable (zVar, nVar) exists. |
| 1309 | */ |
| 1310 | int Th_ExistsVar(Th_Interp *interp, const char *zVar, int nVar){ |
| 1311 | Th_Variable *pValue = thFindValue(interp, zVar, nVar, 0, 1, 1, 0); |
| @@ -1324,28 +1334,32 @@ | |
| 1334 | int nVar, |
| 1335 | const char *zValue, |
| 1336 | int nValue |
| 1337 | ){ |
| 1338 | Th_Variable *pValue; |
| 1339 | int nn; |
| 1340 | |
| 1341 | nVar = TH1_LEN(nVar); |
| 1342 | pValue = thFindValue(interp, zVar, nVar, 1, 0, 0, 0); |
| 1343 | if( !pValue ){ |
| 1344 | return TH_ERROR; |
| 1345 | } |
| 1346 | |
| 1347 | if( nValue<0 ){ |
| 1348 | nn = th_strlen(zValue); |
| 1349 | }else{ |
| 1350 | nn = TH1_LEN(nValue); |
| 1351 | } |
| 1352 | if( pValue->zData ){ |
| 1353 | Th_Free(interp, pValue->zData); |
| 1354 | pValue->zData = 0; |
| 1355 | } |
| 1356 | |
| 1357 | assert(zValue || nn==0); |
| 1358 | pValue->zData = Th_Malloc(interp, nn+1); |
| 1359 | pValue->zData[nn] = '\0'; |
| 1360 | th_memcpy(pValue->zData, zValue, nn); |
| 1361 | pValue->nData = nValue; |
| 1362 | |
| 1363 | return TH_OK; |
| 1364 | } |
| 1365 | |
| @@ -1458,10 +1472,12 @@ | |
| 1472 | */ |
| 1473 | char *th_strdup(Th_Interp *interp, const char *z, int n){ |
| 1474 | char *zRes; |
| 1475 | if( n<0 ){ |
| 1476 | n = th_strlen(z); |
| 1477 | }else{ |
| 1478 | n = TH1_LEN(n); |
| 1479 | } |
| 1480 | zRes = Th_Malloc(interp, n+1); |
| 1481 | th_memcpy(zRes, z, n); |
| 1482 | zRes[n] = '\0'; |
| 1483 | return zRes; |
| @@ -1519,13 +1535,14 @@ | |
| 1535 | n = th_strlen(z); |
| 1536 | } |
| 1537 | |
| 1538 | if( z && n>0 ){ |
| 1539 | char *zResult; |
| 1540 | int nn = TH1_LEN(n); |
| 1541 | zResult = Th_Malloc(pInterp, nn+1); |
| 1542 | th_memcpy(zResult, z, nn); |
| 1543 | zResult[nn] = '\0'; |
| 1544 | pInterp->zResult = zResult; |
| 1545 | pInterp->nResult = n; |
| 1546 | } |
| 1547 | |
| 1548 | return TH_OK; |
| @@ -1777,15 +1794,19 @@ | |
| 1794 | int hasSpecialChar = 0; /* Whitespace or {}[]'" */ |
| 1795 | int hasEscapeChar = 0; /* '}' without matching '{' to the left or a '\\' */ |
| 1796 | int nBrace = 0; |
| 1797 | |
| 1798 | output.zBuf = *pzList; |
| 1799 | output.nBuf = TH1_LEN(*pnList); |
| 1800 | output.nBufAlloc = output.nBuf; |
| 1801 | output.bTaint = 0; |
| 1802 | TH1_XFER_TAINT(output.bTaint, *pnList); |
| 1803 | |
| 1804 | if( nElem<0 ){ |
| 1805 | nElem = th_strlen(zElem); |
| 1806 | }else{ |
| 1807 | nElem = TH1_LEN(nElem); |
| 1808 | } |
| 1809 | if( output.nBuf>0 ){ |
| 1810 | thBufferAddChar(interp, &output, ' '); |
| 1811 | } |
| 1812 | |
| @@ -1834,24 +1855,28 @@ | |
| 1855 | int *pnStr, /* IN/OUT: Current length of *pzStr */ |
| 1856 | const char *zElem, /* Data to append */ |
| 1857 | int nElem /* Length of nElem */ |
| 1858 | ){ |
| 1859 | char *zNew; |
| 1860 | long long int nNew; |
| 1861 | int nn; |
| 1862 | |
| 1863 | if( nElem<0 ){ |
| 1864 | nn = th_strlen(zElem); |
| 1865 | }else{ |
| 1866 | nn = TH1_LEN(nElem); |
| 1867 | } |
| 1868 | |
| 1869 | nNew = TH1_LEN(*pnStr) + nn; |
| 1870 | TH1_SIZECHECK(nNew); |
| 1871 | zNew = Th_Malloc(interp, nNew); |
| 1872 | th_memcpy(zNew, *pzStr, *pnStr); |
| 1873 | th_memcpy(&zNew[TH1_LEN(*pnStr)], zElem, nn); |
| 1874 | |
| 1875 | Th_Free(interp, *pzStr); |
| 1876 | *pzStr = zNew; |
| 1877 | *pnStr = (int)nNew; |
| 1878 | |
| 1879 | return TH_OK; |
| 1880 | } |
| 1881 | |
| 1882 | /* |
| @@ -2106,16 +2131,18 @@ | |
| 2131 | /* Evaluate left and right arguments, if they exist. */ |
| 2132 | if( pExpr->pLeft ){ |
| 2133 | rc = exprEval(interp, pExpr->pLeft); |
| 2134 | if( rc==TH_OK ){ |
| 2135 | zLeft = Th_TakeResult(interp, &nLeft); |
| 2136 | nLeft = TH1_LEN(nLeft); |
| 2137 | } |
| 2138 | } |
| 2139 | if( rc==TH_OK && pExpr->pRight ){ |
| 2140 | rc = exprEval(interp, pExpr->pRight); |
| 2141 | if( rc==TH_OK ){ |
| 2142 | zRight = Th_TakeResult(interp, &nRight); |
| 2143 | nRight = TH1_LEN(nRight); |
| 2144 | } |
| 2145 | } |
| 2146 | |
| 2147 | /* Convert arguments to their required forms. */ |
| 2148 | if( rc==TH_OK ){ |
| @@ -2160,12 +2187,15 @@ | |
| 2187 | } |
| 2188 | iRes = iLeft%iRight; |
| 2189 | break; |
| 2190 | case OP_ADD: iRes = iLeft+iRight; break; |
| 2191 | case OP_SUBTRACT: iRes = iLeft-iRight; break; |
| 2192 | case OP_LEFTSHIFT: { |
| 2193 | iRes = (int)(((unsigned int)iLeft)<<(iRight&0x1f)); |
| 2194 | break; |
| 2195 | } |
| 2196 | case OP_RIGHTSHIFT: iRes = iLeft>>(iRight&0x1f); break; |
| 2197 | case OP_LT: iRes = iLeft<iRight; break; |
| 2198 | case OP_GT: iRes = iLeft>iRight; break; |
| 2199 | case OP_LE: iRes = iLeft<=iRight; break; |
| 2200 | case OP_GE: iRes = iLeft>=iRight; break; |
| 2201 | case OP_EQ: iRes = iLeft==iRight; break; |
| @@ -2453,10 +2483,12 @@ | |
| 2483 | int nToken = 0; |
| 2484 | Expr **apToken = 0; |
| 2485 | |
| 2486 | if( nExpr<0 ){ |
| 2487 | nExpr = th_strlen(zExpr); |
| 2488 | }else{ |
| 2489 | nExpr = TH1_LEN(nExpr); |
| 2490 | } |
| 2491 | |
| 2492 | /* Parse the expression to a list of tokens. */ |
| 2493 | rc = exprParse(interp, zExpr, nExpr, &apToken, &nToken); |
| 2494 | |
| @@ -2564,10 +2596,12 @@ | |
| 2596 | Th_HashEntry *pRet; |
| 2597 | Th_HashEntry **ppRet; |
| 2598 | |
| 2599 | if( nKey<0 ){ |
| 2600 | nKey = th_strlen(zKey); |
| 2601 | }else{ |
| 2602 | nKey = TH1_LEN(nKey); |
| 2603 | } |
| 2604 | |
| 2605 | for(i=0; i<nKey; i++){ |
| 2606 | iKey = (iKey<<3) ^ iKey ^ zKey[i]; |
| 2607 | } |
| @@ -2797,10 +2831,12 @@ | |
| 2831 | int base = 10; |
| 2832 | int (*isdigit)(char) = th_isdigit; |
| 2833 | |
| 2834 | if( n<0 ){ |
| 2835 | n = th_strlen(z); |
| 2836 | }else{ |
| 2837 | n = TH1_LEN(n); |
| 2838 | } |
| 2839 | |
| 2840 | if( n>1 && (z[0]=='-' || z[0]=='+') ){ |
| 2841 | i = 1; |
| 2842 | } |
| @@ -2856,11 +2892,11 @@ | |
| 2892 | const char *z, |
| 2893 | int n, |
| 2894 | double *pfOut |
| 2895 | ){ |
| 2896 | if( !sqlite3IsNumber((const char *)z, 0) ){ |
| 2897 | Th_ErrorMessage(interp, "expected number, got: \"", z, TH1_LEN(n)); |
| 2898 | return TH_ERROR; |
| 2899 | } |
| 2900 | |
| 2901 | sqlite3AtoF((const char *)z, pfOut); |
| 2902 | return TH_OK; |
| @@ -2875,10 +2911,13 @@ | |
| 2911 | unsigned int uVal = iVal; |
| 2912 | char zBuf[32]; |
| 2913 | char *z = &zBuf[32]; |
| 2914 | |
| 2915 | if( iVal<0 ){ |
| 2916 | if( iVal==0x80000000 ){ |
| 2917 | return Th_SetResult(interp, "-2147483648", -1); |
| 2918 | } |
| 2919 | isNegative = 1; |
| 2920 | uVal = iVal * -1; |
| 2921 | } |
| 2922 | *(--z) = '\0'; |
| 2923 | *(--z) = (char)(48+(uVal%10)); |
| 2924 |
M
src/th.h
+53
-14
| --- src/th.h | ||
| +++ src/th.h | ||
| @@ -1,10 +1,56 @@ | ||
| 1 | - | |
| 2 | 1 | /* This header file defines the external interface to the custom Scripting |
| 3 | 2 | ** Language (TH) interpreter. TH is very similar to Tcl but is not an |
| 4 | 3 | ** exact clone. |
| 4 | +** | |
| 5 | +** TH1 was original developed to run SQLite tests on SymbianOS. This version | |
| 6 | +** of TH1 was repurposed as a scripted language for Fossil, and was heavily | |
| 7 | +** modified for that purpose, beginning in early 2008. | |
| 8 | +** | |
| 9 | +** More recently, TH1 has been enhanced to distinguish between regular text | |
| 10 | +** and "tainted" text. "Tainted" text is text that might have originated | |
| 11 | +** from an outside source and hence might not be trustworthy. To prevent | |
| 12 | +** cross-site scripting (XSS) and SQL-injections and similar attacks, | |
| 13 | +** tainted text should not be used for the following purposes: | |
| 14 | +** | |
| 15 | +** * executed as TH1 script or expression. | |
| 16 | +** * output as HTML or Javascript | |
| 17 | +** * used as part of an SQL query | |
| 18 | +** | |
| 19 | +** Tainted text can be converted into a safe form using commands like | |
| 20 | +** "htmlize". And some commands ("query" and "expr") know how to use | |
| 21 | +** potentially tainted variable values directly, and thus can bypass | |
| 22 | +** the restrictions above. | |
| 23 | +** | |
| 24 | +** Whether a string is clean or tainted is determined by its length integer. | |
| 25 | +** TH1 limits strings to be no more than 0x0fffffff bytes bytes in length | |
| 26 | +** (about 268MB - more than sufficient for the purposes of Fossil). The top | |
| 27 | +** bit of the length integer is the sign bit, of course. The next three bits | |
| 28 | +** are reserved. One of those, the 0x10000000 bit, marks tainted strings. | |
| 5 | 29 | */ |
| 30 | +#define TH1_MX_STRLEN 0x0fffffff /* Maximum length of a TH1-C string */ | |
| 31 | +#define TH1_TAINT_BIT 0x10000000 /* The taint bit */ | |
| 32 | +#define TH1_SIGN 0x80000000 | |
| 33 | + | |
| 34 | +/* Convert an integer into a string length. Negative values remain negative */ | |
| 35 | +#define TH1_LEN(X) ((TH1_SIGN|TH1_MX_STRLEN)&(X)) | |
| 36 | + | |
| 37 | +/* Return true if the string is tainted */ | |
| 38 | +#define TH1_TAINTED(X) (((X)&TH1_TAINT_BIT)!=0) | |
| 39 | + | |
| 40 | +/* Remove taint from a string */ | |
| 41 | +#define TH1_RM_TAINT(X) ((X)&~TH1_TAINT_BIT) | |
| 42 | + | |
| 43 | +/* Add taint to a string */ | |
| 44 | +#define TH1_ADD_TAINT(X) ((X)|TH1_TAINT_BIT) | |
| 45 | + | |
| 46 | +/* If B is tainted, make A tainted too */ | |
| 47 | +#define TH1_XFER_TAINT(A,B) (A)|=(TH1_TAINT_BIT&(B)) | |
| 48 | + | |
| 49 | +/* Check to see if a string is too big for TH1 */ | |
| 50 | +#define TH1_SIZECHECK(N) if((N)>TH1_MX_STRLEN){Th_OversizeString();} | |
| 51 | +void Th_OversizeString(void); | |
| 6 | 52 | |
| 7 | 53 | /* |
| 8 | 54 | ** Before creating an interpreter, the application must allocate and |
| 9 | 55 | ** populate an instance of the following structure. It must remain valid |
| 10 | 56 | ** for the lifetime of the interpreter. |
| @@ -24,10 +70,16 @@ | ||
| 24 | 70 | ** Create and delete interpreters. |
| 25 | 71 | */ |
| 26 | 72 | Th_Interp * Th_CreateInterp(Th_Vtab *); |
| 27 | 73 | void Th_DeleteInterp(Th_Interp *); |
| 28 | 74 | |
| 75 | +/* | |
| 76 | +** Report taint in the string zStr,nStr. That string represents "zTitle" | |
| 77 | +** If non-zero is returned error out of the caller. | |
| 78 | +*/ | |
| 79 | +int Th_ReportTaint(Th_Interp*,const char*,const char*zStr,int nStr); | |
| 80 | + | |
| 29 | 81 | /* |
| 30 | 82 | ** Evaluate an TH program in the stack frame identified by parameter |
| 31 | 83 | ** iFrame, according to the following rules: |
| 32 | 84 | ** |
| 33 | 85 | ** * If iFrame is 0, this means the current frame. |
| @@ -56,23 +108,10 @@ | ||
| 56 | 108 | int Th_GetVar(Th_Interp *, const char *, int); |
| 57 | 109 | int Th_SetVar(Th_Interp *, const char *, int, const char *, int); |
| 58 | 110 | int Th_LinkVar(Th_Interp *, const char *, int, int, const char *, int); |
| 59 | 111 | int Th_UnsetVar(Th_Interp *, const char *, int); |
| 60 | 112 | |
| 61 | -/* | |
| 62 | -** If interp has a variable with the given name, its value is returned | |
| 63 | -** and its length is returned via *nOut if nOut is not NULL. If | |
| 64 | -** interp has no such var then NULL is returned without setting any | |
| 65 | -** error state and *nOut, if not NULL, is set to 0. The returned value | |
| 66 | -** is owned by the interpreter and may be invalidated the next time | |
| 67 | -** the interpreter is modified. | |
| 68 | -** | |
| 69 | -** zVarName must be NUL-terminated. | |
| 70 | -*/ | |
| 71 | -const char * Th_MaybeGetVar(Th_Interp *interp, const char *zVarName, | |
| 72 | - int *nOut); | |
| 73 | - | |
| 74 | 113 | typedef int (*Th_CommandProc)(Th_Interp *, void *, int, const char **, int *); |
| 75 | 114 | |
| 76 | 115 | /* |
| 77 | 116 | ** Register new commands. |
| 78 | 117 | */ |
| 79 | 118 |
| --- src/th.h | |
| +++ src/th.h | |
| @@ -1,10 +1,56 @@ | |
| 1 | |
| 2 | /* This header file defines the external interface to the custom Scripting |
| 3 | ** Language (TH) interpreter. TH is very similar to Tcl but is not an |
| 4 | ** exact clone. |
| 5 | */ |
| 6 | |
| 7 | /* |
| 8 | ** Before creating an interpreter, the application must allocate and |
| 9 | ** populate an instance of the following structure. It must remain valid |
| 10 | ** for the lifetime of the interpreter. |
| @@ -24,10 +70,16 @@ | |
| 24 | ** Create and delete interpreters. |
| 25 | */ |
| 26 | Th_Interp * Th_CreateInterp(Th_Vtab *); |
| 27 | void Th_DeleteInterp(Th_Interp *); |
| 28 | |
| 29 | /* |
| 30 | ** Evaluate an TH program in the stack frame identified by parameter |
| 31 | ** iFrame, according to the following rules: |
| 32 | ** |
| 33 | ** * If iFrame is 0, this means the current frame. |
| @@ -56,23 +108,10 @@ | |
| 56 | int Th_GetVar(Th_Interp *, const char *, int); |
| 57 | int Th_SetVar(Th_Interp *, const char *, int, const char *, int); |
| 58 | int Th_LinkVar(Th_Interp *, const char *, int, int, const char *, int); |
| 59 | int Th_UnsetVar(Th_Interp *, const char *, int); |
| 60 | |
| 61 | /* |
| 62 | ** If interp has a variable with the given name, its value is returned |
| 63 | ** and its length is returned via *nOut if nOut is not NULL. If |
| 64 | ** interp has no such var then NULL is returned without setting any |
| 65 | ** error state and *nOut, if not NULL, is set to 0. The returned value |
| 66 | ** is owned by the interpreter and may be invalidated the next time |
| 67 | ** the interpreter is modified. |
| 68 | ** |
| 69 | ** zVarName must be NUL-terminated. |
| 70 | */ |
| 71 | const char * Th_MaybeGetVar(Th_Interp *interp, const char *zVarName, |
| 72 | int *nOut); |
| 73 | |
| 74 | typedef int (*Th_CommandProc)(Th_Interp *, void *, int, const char **, int *); |
| 75 | |
| 76 | /* |
| 77 | ** Register new commands. |
| 78 | */ |
| 79 |
| --- src/th.h | |
| +++ src/th.h | |
| @@ -1,10 +1,56 @@ | |
| 1 | /* This header file defines the external interface to the custom Scripting |
| 2 | ** Language (TH) interpreter. TH is very similar to Tcl but is not an |
| 3 | ** exact clone. |
| 4 | ** |
| 5 | ** TH1 was original developed to run SQLite tests on SymbianOS. This version |
| 6 | ** of TH1 was repurposed as a scripted language for Fossil, and was heavily |
| 7 | ** modified for that purpose, beginning in early 2008. |
| 8 | ** |
| 9 | ** More recently, TH1 has been enhanced to distinguish between regular text |
| 10 | ** and "tainted" text. "Tainted" text is text that might have originated |
| 11 | ** from an outside source and hence might not be trustworthy. To prevent |
| 12 | ** cross-site scripting (XSS) and SQL-injections and similar attacks, |
| 13 | ** tainted text should not be used for the following purposes: |
| 14 | ** |
| 15 | ** * executed as TH1 script or expression. |
| 16 | ** * output as HTML or Javascript |
| 17 | ** * used as part of an SQL query |
| 18 | ** |
| 19 | ** Tainted text can be converted into a safe form using commands like |
| 20 | ** "htmlize". And some commands ("query" and "expr") know how to use |
| 21 | ** potentially tainted variable values directly, and thus can bypass |
| 22 | ** the restrictions above. |
| 23 | ** |
| 24 | ** Whether a string is clean or tainted is determined by its length integer. |
| 25 | ** TH1 limits strings to be no more than 0x0fffffff bytes bytes in length |
| 26 | ** (about 268MB - more than sufficient for the purposes of Fossil). The top |
| 27 | ** bit of the length integer is the sign bit, of course. The next three bits |
| 28 | ** are reserved. One of those, the 0x10000000 bit, marks tainted strings. |
| 29 | */ |
| 30 | #define TH1_MX_STRLEN 0x0fffffff /* Maximum length of a TH1-C string */ |
| 31 | #define TH1_TAINT_BIT 0x10000000 /* The taint bit */ |
| 32 | #define TH1_SIGN 0x80000000 |
| 33 | |
| 34 | /* Convert an integer into a string length. Negative values remain negative */ |
| 35 | #define TH1_LEN(X) ((TH1_SIGN|TH1_MX_STRLEN)&(X)) |
| 36 | |
| 37 | /* Return true if the string is tainted */ |
| 38 | #define TH1_TAINTED(X) (((X)&TH1_TAINT_BIT)!=0) |
| 39 | |
| 40 | /* Remove taint from a string */ |
| 41 | #define TH1_RM_TAINT(X) ((X)&~TH1_TAINT_BIT) |
| 42 | |
| 43 | /* Add taint to a string */ |
| 44 | #define TH1_ADD_TAINT(X) ((X)|TH1_TAINT_BIT) |
| 45 | |
| 46 | /* If B is tainted, make A tainted too */ |
| 47 | #define TH1_XFER_TAINT(A,B) (A)|=(TH1_TAINT_BIT&(B)) |
| 48 | |
| 49 | /* Check to see if a string is too big for TH1 */ |
| 50 | #define TH1_SIZECHECK(N) if((N)>TH1_MX_STRLEN){Th_OversizeString();} |
| 51 | void Th_OversizeString(void); |
| 52 | |
| 53 | /* |
| 54 | ** Before creating an interpreter, the application must allocate and |
| 55 | ** populate an instance of the following structure. It must remain valid |
| 56 | ** for the lifetime of the interpreter. |
| @@ -24,10 +70,16 @@ | |
| 70 | ** Create and delete interpreters. |
| 71 | */ |
| 72 | Th_Interp * Th_CreateInterp(Th_Vtab *); |
| 73 | void Th_DeleteInterp(Th_Interp *); |
| 74 | |
| 75 | /* |
| 76 | ** Report taint in the string zStr,nStr. That string represents "zTitle" |
| 77 | ** If non-zero is returned error out of the caller. |
| 78 | */ |
| 79 | int Th_ReportTaint(Th_Interp*,const char*,const char*zStr,int nStr); |
| 80 | |
| 81 | /* |
| 82 | ** Evaluate an TH program in the stack frame identified by parameter |
| 83 | ** iFrame, according to the following rules: |
| 84 | ** |
| 85 | ** * If iFrame is 0, this means the current frame. |
| @@ -56,23 +108,10 @@ | |
| 108 | int Th_GetVar(Th_Interp *, const char *, int); |
| 109 | int Th_SetVar(Th_Interp *, const char *, int, const char *, int); |
| 110 | int Th_LinkVar(Th_Interp *, const char *, int, int, const char *, int); |
| 111 | int Th_UnsetVar(Th_Interp *, const char *, int); |
| 112 | |
| 113 | typedef int (*Th_CommandProc)(Th_Interp *, void *, int, const char **, int *); |
| 114 | |
| 115 | /* |
| 116 | ** Register new commands. |
| 117 | */ |
| 118 |
+92
-53
| --- src/th_lang.c | ||
| +++ src/th_lang.c | ||
| @@ -39,11 +39,11 @@ | ||
| 39 | 39 | |
| 40 | 40 | rc = Th_Eval(interp, 0, argv[1], -1); |
| 41 | 41 | if( argc==3 ){ |
| 42 | 42 | int nResult; |
| 43 | 43 | const char *zResult = Th_GetResult(interp, &nResult); |
| 44 | - Th_SetVar(interp, argv[2], argl[2], zResult, nResult); | |
| 44 | + Th_SetVar(interp, argv[2], TH1_LEN(argl[2]), zResult, nResult); | |
| 45 | 45 | } |
| 46 | 46 | |
| 47 | 47 | Th_SetResultInt(interp, rc); |
| 48 | 48 | return TH_OK; |
| 49 | 49 | } |
| @@ -180,20 +180,24 @@ | ||
| 180 | 180 | int nVar; |
| 181 | 181 | char **azValue = 0; |
| 182 | 182 | int *anValue; |
| 183 | 183 | int nValue; |
| 184 | 184 | int ii, jj; |
| 185 | + int bTaint = 0; | |
| 185 | 186 | |
| 186 | 187 | if( argc!=4 ){ |
| 187 | 188 | return Th_WrongNumArgs(interp, "foreach varlist list script"); |
| 188 | 189 | } |
| 189 | 190 | rc = Th_SplitList(interp, argv[1], argl[1], &azVar, &anVar, &nVar); |
| 190 | 191 | if( rc ) return rc; |
| 192 | + TH1_XFER_TAINT(bTaint, argl[2]); | |
| 191 | 193 | rc = Th_SplitList(interp, argv[2], argl[2], &azValue, &anValue, &nValue); |
| 192 | 194 | for(ii=0; rc==TH_OK && ii<=nValue-nVar; ii+=nVar){ |
| 193 | 195 | for(jj=0; jj<nVar; jj++){ |
| 194 | - Th_SetVar(interp, azVar[jj], anVar[jj], azValue[ii+jj], anValue[ii+jj]); | |
| 196 | + int x = anValue[ii+jj]; | |
| 197 | + TH1_XFER_TAINT(x, bTaint); | |
| 198 | + Th_SetVar(interp, azVar[jj], anVar[jj], azValue[ii+jj], x); | |
| 195 | 199 | } |
| 196 | 200 | rc = eval_loopbody(interp, argv[3], argl[3]); |
| 197 | 201 | } |
| 198 | 202 | if( rc==TH_BREAK ) rc = TH_OK; |
| 199 | 203 | Th_Free(interp, azVar); |
| @@ -215,15 +219,18 @@ | ||
| 215 | 219 | int *argl |
| 216 | 220 | ){ |
| 217 | 221 | char *zList = 0; |
| 218 | 222 | int nList = 0; |
| 219 | 223 | int i; |
| 224 | + int bTaint = 0; | |
| 220 | 225 | |
| 221 | 226 | for(i=1; i<argc; i++){ |
| 227 | + TH1_XFER_TAINT(bTaint,argl[i]); | |
| 222 | 228 | Th_ListAppend(interp, &zList, &nList, argv[i], argl[i]); |
| 223 | 229 | } |
| 224 | 230 | |
| 231 | + TH1_XFER_TAINT(nList, bTaint); | |
| 225 | 232 | Th_SetResult(interp, zList, nList); |
| 226 | 233 | Th_Free(interp, zList); |
| 227 | 234 | |
| 228 | 235 | return TH_OK; |
| 229 | 236 | } |
| @@ -244,23 +251,27 @@ | ||
| 244 | 251 | int *argl |
| 245 | 252 | ){ |
| 246 | 253 | char *zList = 0; |
| 247 | 254 | int nList = 0; |
| 248 | 255 | int i, rc; |
| 256 | + int bTaint = 0; | |
| 249 | 257 | |
| 250 | 258 | if( argc<2 ){ |
| 251 | 259 | return Th_WrongNumArgs(interp, "lappend var ..."); |
| 252 | 260 | } |
| 253 | 261 | rc = Th_GetVar(interp, argv[1], argl[1]); |
| 254 | 262 | if( rc==TH_OK ){ |
| 255 | 263 | zList = Th_TakeResult(interp, &nList); |
| 256 | 264 | } |
| 257 | 265 | |
| 266 | + TH1_XFER_TAINT(bTaint, nList); | |
| 258 | 267 | for(i=2; i<argc; i++){ |
| 268 | + TH1_XFER_TAINT(bTaint, argl[i]); | |
| 259 | 269 | Th_ListAppend(interp, &zList, &nList, argv[i], argl[i]); |
| 260 | 270 | } |
| 261 | 271 | |
| 272 | + TH1_XFER_TAINT(nList, bTaint); | |
| 262 | 273 | Th_SetVar(interp, argv[1], argl[1], zList, nList); |
| 263 | 274 | Th_SetResult(interp, zList, nList); |
| 264 | 275 | Th_Free(interp, zList); |
| 265 | 276 | |
| 266 | 277 | return TH_OK; |
| @@ -283,23 +294,27 @@ | ||
| 283 | 294 | int rc; |
| 284 | 295 | |
| 285 | 296 | char **azElem; |
| 286 | 297 | int *anElem; |
| 287 | 298 | int nCount; |
| 299 | + int bTaint = 0; | |
| 288 | 300 | |
| 289 | 301 | if( argc!=3 ){ |
| 290 | 302 | return Th_WrongNumArgs(interp, "lindex list index"); |
| 291 | 303 | } |
| 292 | 304 | |
| 293 | 305 | if( TH_OK!=Th_ToInt(interp, argv[2], argl[2], &iElem) ){ |
| 294 | 306 | return TH_ERROR; |
| 295 | 307 | } |
| 296 | 308 | |
| 309 | + TH1_XFER_TAINT(bTaint, argl[1]); | |
| 297 | 310 | rc = Th_SplitList(interp, argv[1], argl[1], &azElem, &anElem, &nCount); |
| 298 | 311 | if( rc==TH_OK ){ |
| 299 | 312 | if( iElem<nCount && iElem>=0 ){ |
| 300 | - Th_SetResult(interp, azElem[iElem], anElem[iElem]); | |
| 313 | + int sz = anElem[iElem]; | |
| 314 | + TH1_XFER_TAINT(sz, bTaint); | |
| 315 | + Th_SetResult(interp, azElem[iElem], sz); | |
| 301 | 316 | }else{ |
| 302 | 317 | Th_SetResult(interp, 0, 0); |
| 303 | 318 | } |
| 304 | 319 | Th_Free(interp, azElem); |
| 305 | 320 | } |
| @@ -356,13 +371,14 @@ | ||
| 356 | 371 | return Th_WrongNumArgs(interp, "lsearch list string"); |
| 357 | 372 | } |
| 358 | 373 | |
| 359 | 374 | rc = Th_SplitList(interp, argv[1], argl[1], &azElem, &anElem, &nCount); |
| 360 | 375 | if( rc==TH_OK ){ |
| 376 | + int nn = TH1_LEN(argl[2]); | |
| 361 | 377 | Th_SetResultInt(interp, -1); |
| 362 | 378 | for(i=0; i<nCount; i++){ |
| 363 | - if( anElem[i]==argl[2] && 0==memcmp(azElem[i], argv[2], argl[2]) ){ | |
| 379 | + if( TH1_LEN(anElem[i])==nn && 0==memcmp(azElem[i], argv[2], nn) ){ | |
| 364 | 380 | Th_SetResultInt(interp, i); |
| 365 | 381 | break; |
| 366 | 382 | } |
| 367 | 383 | } |
| 368 | 384 | Th_Free(interp, azElem); |
| @@ -561,28 +577,31 @@ | ||
| 561 | 577 | int nUsage = 0; /* Number of bytes at zUsage */ |
| 562 | 578 | |
| 563 | 579 | if( argc!=4 ){ |
| 564 | 580 | return Th_WrongNumArgs(interp, "proc name arglist code"); |
| 565 | 581 | } |
| 566 | - if( Th_SplitList(interp, argv[2], argl[2], &azParam, &anParam, &nParam) ){ | |
| 582 | + if( Th_SplitList(interp, argv[2], TH1_LEN(argl[2]), | |
| 583 | + &azParam, &anParam, &nParam) ){ | |
| 567 | 584 | return TH_ERROR; |
| 568 | 585 | } |
| 569 | 586 | |
| 570 | 587 | /* Allocate the new ProcDefn structure. */ |
| 571 | 588 | nByte = sizeof(ProcDefn) + /* ProcDefn structure */ |
| 572 | 589 | (sizeof(char *) + sizeof(int)) * nParam + /* azParam, anParam */ |
| 573 | 590 | (sizeof(char *) + sizeof(int)) * nParam + /* azDefault, anDefault */ |
| 574 | - argl[3] + /* zProgram */ | |
| 575 | - argl[2]; /* Space for copies of parameter names and default values */ | |
| 591 | + TH1_LEN(argl[3]) + /* zProgram */ | |
| 592 | + TH1_LEN(argl[2]); /* Space for copies of param names and dflt values */ | |
| 576 | 593 | p = (ProcDefn *)Th_Malloc(interp, nByte); |
| 577 | 594 | |
| 578 | 595 | /* If the last parameter in the parameter list is "args", then set the |
| 579 | 596 | ** ProcDefn.hasArgs flag. The "args" parameter does not require an |
| 580 | 597 | ** entry in the ProcDefn.azParam[] or ProcDefn.azDefault[] arrays. |
| 581 | 598 | */ |
| 582 | 599 | if( nParam>0 ){ |
| 583 | - if( anParam[nParam-1]==4 && 0==memcmp(azParam[nParam-1], "args", 4) ){ | |
| 600 | + if( TH1_LEN(anParam[nParam-1])==4 | |
| 601 | + && 0==memcmp(azParam[nParam-1], "args", 4) | |
| 602 | + ){ | |
| 584 | 603 | p->hasArgs = 1; |
| 585 | 604 | nParam--; |
| 586 | 605 | } |
| 587 | 606 | } |
| 588 | 607 | |
| @@ -590,12 +609,12 @@ | ||
| 590 | 609 | p->azParam = (char **)&p[1]; |
| 591 | 610 | p->anParam = (int *)&p->azParam[nParam]; |
| 592 | 611 | p->azDefault = (char **)&p->anParam[nParam]; |
| 593 | 612 | p->anDefault = (int *)&p->azDefault[nParam]; |
| 594 | 613 | p->zProgram = (char *)&p->anDefault[nParam]; |
| 595 | - memcpy(p->zProgram, argv[3], argl[3]); | |
| 596 | - p->nProgram = argl[3]; | |
| 614 | + memcpy(p->zProgram, argv[3], TH1_LEN(argl[3])); | |
| 615 | + p->nProgram = TH1_LEN(argl[3]); | |
| 597 | 616 | zSpace = &p->zProgram[p->nProgram]; |
| 598 | 617 | |
| 599 | 618 | for(i=0; i<nParam; i++){ |
| 600 | 619 | char **az; |
| 601 | 620 | int *an; |
| @@ -672,11 +691,12 @@ | ||
| 672 | 691 | int *argl |
| 673 | 692 | ){ |
| 674 | 693 | if( argc!=3 ){ |
| 675 | 694 | return Th_WrongNumArgs(interp, "rename oldcmd newcmd"); |
| 676 | 695 | } |
| 677 | - return Th_RenameCommand(interp, argv[1], argl[1], argv[2], argl[2]); | |
| 696 | + return Th_RenameCommand(interp, argv[1], TH1_LEN(argl[1]), | |
| 697 | + argv[2], TH1_LEN(argl[2])); | |
| 678 | 698 | } |
| 679 | 699 | |
| 680 | 700 | /* |
| 681 | 701 | ** TH Syntax: |
| 682 | 702 | ** |
| @@ -746,13 +766,13 @@ | ||
| 746 | 766 | if( argc!=4 ){ |
| 747 | 767 | return Th_WrongNumArgs(interp, "string compare str1 str2"); |
| 748 | 768 | } |
| 749 | 769 | |
| 750 | 770 | zLeft = argv[2]; |
| 751 | - nLeft = argl[2]; | |
| 771 | + nLeft = TH1_LEN(argl[2]); | |
| 752 | 772 | zRight = argv[3]; |
| 753 | - nRight = argl[3]; | |
| 773 | + nRight = TH1_LEN(argl[3]); | |
| 754 | 774 | |
| 755 | 775 | for(i=0; iRes==0 && i<nLeft && i<nRight; i++){ |
| 756 | 776 | iRes = zLeft[i]-zRight[i]; |
| 757 | 777 | } |
| 758 | 778 | if( iRes==0 ){ |
| @@ -779,12 +799,12 @@ | ||
| 779 | 799 | |
| 780 | 800 | if( argc!=4 ){ |
| 781 | 801 | return Th_WrongNumArgs(interp, "string first needle haystack"); |
| 782 | 802 | } |
| 783 | 803 | |
| 784 | - nNeedle = argl[2]; | |
| 785 | - nHaystack = argl[3]; | |
| 804 | + nNeedle = TH1_LEN(argl[2]); | |
| 805 | + nHaystack = TH1_LEN(argl[3]); | |
| 786 | 806 | |
| 787 | 807 | if( nNeedle && nHaystack && nNeedle<=nHaystack ){ |
| 788 | 808 | const char *zNeedle = argv[2]; |
| 789 | 809 | const char *zHaystack = argv[3]; |
| 790 | 810 | int i; |
| @@ -812,20 +832,22 @@ | ||
| 812 | 832 | |
| 813 | 833 | if( argc!=4 ){ |
| 814 | 834 | return Th_WrongNumArgs(interp, "string index string index"); |
| 815 | 835 | } |
| 816 | 836 | |
| 817 | - if( argl[3]==3 && 0==memcmp("end", argv[3], 3) ){ | |
| 818 | - iIndex = argl[2]-1; | |
| 837 | + if( TH1_LEN(argl[3])==3 && 0==memcmp("end", argv[3], 3) ){ | |
| 838 | + iIndex = TH1_LEN(argl[2])-1; | |
| 819 | 839 | }else if( Th_ToInt(interp, argv[3], argl[3], &iIndex) ){ |
| 820 | 840 | Th_ErrorMessage( |
| 821 | 841 | interp, "Expected \"end\" or integer, got:", argv[3], argl[3]); |
| 822 | 842 | return TH_ERROR; |
| 823 | 843 | } |
| 824 | 844 | |
| 825 | - if( iIndex>=0 && iIndex<argl[2] ){ | |
| 826 | - return Th_SetResult(interp, &argv[2][iIndex], 1); | |
| 845 | + if( iIndex>=0 && iIndex<TH1_LEN(argl[2]) ){ | |
| 846 | + int sz = 1; | |
| 847 | + TH1_XFER_TAINT(sz, argl[2]); | |
| 848 | + return Th_SetResult(interp, &argv[2][iIndex], sz); | |
| 827 | 849 | }else{ |
| 828 | 850 | return Th_SetResult(interp, 0, 0); |
| 829 | 851 | } |
| 830 | 852 | } |
| 831 | 853 | |
| @@ -838,41 +860,44 @@ | ||
| 838 | 860 | Th_Interp *interp, void *ctx, int argc, const char **argv, int *argl |
| 839 | 861 | ){ |
| 840 | 862 | if( argc!=4 ){ |
| 841 | 863 | return Th_WrongNumArgs(interp, "string is class string"); |
| 842 | 864 | } |
| 843 | - if( argl[2]==5 && 0==memcmp(argv[2], "alnum", 5) ){ | |
| 865 | + if( TH1_LEN(argl[2])==5 && 0==memcmp(argv[2], "alnum", 5) ){ | |
| 844 | 866 | int i; |
| 845 | 867 | int iRes = 1; |
| 846 | 868 | |
| 847 | - for(i=0; i<argl[3]; i++){ | |
| 869 | + for(i=0; i<TH1_LEN(argl[3]); i++){ | |
| 848 | 870 | if( !th_isalnum(argv[3][i]) ){ |
| 849 | 871 | iRes = 0; |
| 850 | 872 | } |
| 851 | 873 | } |
| 852 | 874 | |
| 853 | 875 | return Th_SetResultInt(interp, iRes); |
| 854 | - }else if( argl[2]==6 && 0==memcmp(argv[2], "double", 6) ){ | |
| 876 | + }else if( TH1_LEN(argl[2])==6 && 0==memcmp(argv[2], "double", 6) ){ | |
| 855 | 877 | double fVal; |
| 856 | 878 | if( Th_ToDouble(interp, argv[3], argl[3], &fVal)==TH_OK ){ |
| 857 | 879 | return Th_SetResultInt(interp, 1); |
| 858 | 880 | } |
| 859 | 881 | return Th_SetResultInt(interp, 0); |
| 860 | - }else if( argl[2]==7 && 0==memcmp(argv[2], "integer", 7) ){ | |
| 882 | + }else if( TH1_LEN(argl[2])==7 && 0==memcmp(argv[2], "integer", 7) ){ | |
| 861 | 883 | int iVal; |
| 862 | 884 | if( Th_ToInt(interp, argv[3], argl[3], &iVal)==TH_OK ){ |
| 863 | 885 | return Th_SetResultInt(interp, 1); |
| 864 | 886 | } |
| 865 | 887 | return Th_SetResultInt(interp, 0); |
| 866 | - }else if( argl[2]==4 && 0==memcmp(argv[2], "list", 4) ){ | |
| 888 | + }else if( TH1_LEN(argl[2])==4 && 0==memcmp(argv[2], "list", 4) ){ | |
| 867 | 889 | if( Th_SplitList(interp, argv[3], argl[3], 0, 0, 0)==TH_OK ){ |
| 868 | 890 | return Th_SetResultInt(interp, 1); |
| 869 | 891 | } |
| 870 | 892 | return Th_SetResultInt(interp, 0); |
| 893 | + }else if( TH1_LEN(argl[2])==7 && 0==memcmp(argv[2], "tainted", 7) ){ | |
| 894 | + return Th_SetResultInt(interp, TH1_TAINTED(argl[3])); | |
| 871 | 895 | }else{ |
| 872 | 896 | Th_ErrorMessage(interp, |
| 873 | - "Expected alnum, double, integer, or list, got:", argv[2], argl[2]); | |
| 897 | + "Expected alnum, double, integer, list, or tainted, got:", | |
| 898 | + argv[2], TH1_LEN(argl[2])); | |
| 874 | 899 | return TH_ERROR; |
| 875 | 900 | } |
| 876 | 901 | } |
| 877 | 902 | |
| 878 | 903 | /* |
| @@ -889,12 +914,12 @@ | ||
| 889 | 914 | |
| 890 | 915 | if( argc!=4 ){ |
| 891 | 916 | return Th_WrongNumArgs(interp, "string last needle haystack"); |
| 892 | 917 | } |
| 893 | 918 | |
| 894 | - nNeedle = argl[2]; | |
| 895 | - nHaystack = argl[3]; | |
| 919 | + nNeedle = TH1_LEN(argl[2]); | |
| 920 | + nHaystack = TH1_LEN(argl[3]); | |
| 896 | 921 | |
| 897 | 922 | if( nNeedle && nHaystack && nNeedle<=nHaystack ){ |
| 898 | 923 | const char *zNeedle = argv[2]; |
| 899 | 924 | const char *zHaystack = argv[3]; |
| 900 | 925 | int i; |
| @@ -919,11 +944,11 @@ | ||
| 919 | 944 | Th_Interp *interp, void *ctx, int argc, const char **argv, int *argl |
| 920 | 945 | ){ |
| 921 | 946 | if( argc!=3 ){ |
| 922 | 947 | return Th_WrongNumArgs(interp, "string length string"); |
| 923 | 948 | } |
| 924 | - return Th_SetResultInt(interp, argl[2]); | |
| 949 | + return Th_SetResultInt(interp, TH1_LEN(argl[2])); | |
| 925 | 950 | } |
| 926 | 951 | |
| 927 | 952 | /* |
| 928 | 953 | ** TH Syntax: |
| 929 | 954 | ** |
| @@ -938,12 +963,12 @@ | ||
| 938 | 963 | char *zPat, *zStr; |
| 939 | 964 | int rc; |
| 940 | 965 | if( argc!=4 ){ |
| 941 | 966 | return Th_WrongNumArgs(interp, "string match pattern string"); |
| 942 | 967 | } |
| 943 | - zPat = fossil_strndup(argv[2],argl[2]); | |
| 944 | - zStr = fossil_strndup(argv[3],argl[3]); | |
| 968 | + zPat = fossil_strndup(argv[2],TH1_LEN(argl[2])); | |
| 969 | + zStr = fossil_strndup(argv[3],TH1_LEN(argl[3])); | |
| 945 | 970 | rc = sqlite3_strglob(zPat,zStr); |
| 946 | 971 | fossil_free(zPat); |
| 947 | 972 | fossil_free(zStr); |
| 948 | 973 | return Th_SetResultInt(interp, !rc); |
| 949 | 974 | } |
| @@ -956,31 +981,34 @@ | ||
| 956 | 981 | static int string_range_command( |
| 957 | 982 | Th_Interp *interp, void *ctx, int argc, const char **argv, int *argl |
| 958 | 983 | ){ |
| 959 | 984 | int iStart; |
| 960 | 985 | int iEnd; |
| 986 | + int sz; | |
| 961 | 987 | |
| 962 | 988 | if( argc!=5 ){ |
| 963 | 989 | return Th_WrongNumArgs(interp, "string range string first last"); |
| 964 | 990 | } |
| 965 | 991 | |
| 966 | - if( argl[4]==3 && 0==memcmp("end", argv[4], 3) ){ | |
| 967 | - iEnd = argl[2]; | |
| 992 | + if( TH1_LEN(argl[4])==3 && 0==memcmp("end", argv[4], 3) ){ | |
| 993 | + iEnd = TH1_LEN(argl[2]); | |
| 968 | 994 | }else if( Th_ToInt(interp, argv[4], argl[4], &iEnd) ){ |
| 969 | 995 | Th_ErrorMessage( |
| 970 | - interp, "Expected \"end\" or integer, got:", argv[4], argl[4]); | |
| 996 | + interp, "Expected \"end\" or integer, got:", argv[4], TH1_LEN(argl[4])); | |
| 971 | 997 | return TH_ERROR; |
| 972 | 998 | } |
| 973 | 999 | if( Th_ToInt(interp, argv[3], argl[3], &iStart) ){ |
| 974 | 1000 | return TH_ERROR; |
| 975 | 1001 | } |
| 976 | 1002 | |
| 977 | 1003 | if( iStart<0 ) iStart = 0; |
| 978 | - if( iEnd>=argl[2] ) iEnd = argl[2]-1; | |
| 1004 | + if( iEnd>=TH1_LEN(argl[2]) ) iEnd = TH1_LEN(argl[2])-1; | |
| 979 | 1005 | if( iStart>iEnd ) iEnd = iStart-1; |
| 1006 | + sz = iEnd - iStart + 1; | |
| 1007 | + TH1_XFER_TAINT(sz, argl[2]); | |
| 980 | 1008 | |
| 981 | - return Th_SetResult(interp, &argv[2][iStart], iEnd-iStart+1); | |
| 1009 | + return Th_SetResult(interp, &argv[2][iStart], sz); | |
| 982 | 1010 | } |
| 983 | 1011 | |
| 984 | 1012 | /* |
| 985 | 1013 | ** TH Syntax: |
| 986 | 1014 | ** |
| @@ -989,27 +1017,33 @@ | ||
| 989 | 1017 | static int string_repeat_command( |
| 990 | 1018 | Th_Interp *interp, void *ctx, int argc, const char **argv, int *argl |
| 991 | 1019 | ){ |
| 992 | 1020 | int n; |
| 993 | 1021 | int i; |
| 994 | - int nByte; | |
| 1022 | + int sz; | |
| 1023 | + long long int nByte; | |
| 995 | 1024 | char *zByte; |
| 996 | 1025 | |
| 997 | 1026 | if( argc!=4 ){ |
| 998 | 1027 | return Th_WrongNumArgs(interp, "string repeat string n"); |
| 999 | 1028 | } |
| 1000 | 1029 | if( Th_ToInt(interp, argv[3], argl[3], &n) ){ |
| 1001 | 1030 | return TH_ERROR; |
| 1002 | 1031 | } |
| 1003 | 1032 | |
| 1004 | - nByte = argl[2] * n; | |
| 1033 | + nByte = n; | |
| 1034 | + sz = TH1_LEN(argl[2]); | |
| 1035 | + nByte *= sz; | |
| 1036 | + TH1_SIZECHECK(nByte+1); | |
| 1005 | 1037 | zByte = Th_Malloc(interp, nByte+1); |
| 1006 | - for(i=0; i<nByte; i+=argl[2]){ | |
| 1007 | - memcpy(&zByte[i], argv[2], argl[2]); | |
| 1038 | + for(i=0; i<nByte; i+=sz){ | |
| 1039 | + memcpy(&zByte[i], argv[2], sz); | |
| 1008 | 1040 | } |
| 1009 | 1041 | |
| 1010 | - Th_SetResult(interp, zByte, nByte); | |
| 1042 | + n = nByte; | |
| 1043 | + TH1_XFER_TAINT(n, argl[2]); | |
| 1044 | + Th_SetResult(interp, zByte, n); | |
| 1011 | 1045 | Th_Free(interp, zByte); |
| 1012 | 1046 | return TH_OK; |
| 1013 | 1047 | } |
| 1014 | 1048 | |
| 1015 | 1049 | /* |
| @@ -1027,17 +1061,18 @@ | ||
| 1027 | 1061 | |
| 1028 | 1062 | if( argc!=3 ){ |
| 1029 | 1063 | return Th_WrongNumArgs(interp, "string trim string"); |
| 1030 | 1064 | } |
| 1031 | 1065 | z = argv[2]; |
| 1032 | - n = argl[2]; | |
| 1033 | - if( argl[1]<5 || argv[1][4]=='l' ){ | |
| 1066 | + n = TH1_LEN(argl[2]); | |
| 1067 | + if( TH1_LEN(argl[1])<5 || argv[1][4]=='l' ){ | |
| 1034 | 1068 | while( n && th_isspace(z[0]) ){ z++; n--; } |
| 1035 | 1069 | } |
| 1036 | - if( argl[1]<5 || argv[1][4]=='r' ){ | |
| 1070 | + if( TH1_LEN(argl[1])<5 || argv[1][4]=='r' ){ | |
| 1037 | 1071 | while( n && th_isspace(z[n-1]) ){ n--; } |
| 1038 | 1072 | } |
| 1073 | + TH1_XFER_TAINT(n, argl[2]); | |
| 1039 | 1074 | Th_SetResult(interp, z, n); |
| 1040 | 1075 | return TH_OK; |
| 1041 | 1076 | } |
| 1042 | 1077 | |
| 1043 | 1078 | /* |
| @@ -1051,11 +1086,11 @@ | ||
| 1051 | 1086 | int rc; |
| 1052 | 1087 | |
| 1053 | 1088 | if( argc!=3 ){ |
| 1054 | 1089 | return Th_WrongNumArgs(interp, "info exists var"); |
| 1055 | 1090 | } |
| 1056 | - rc = Th_ExistsVar(interp, argv[2], argl[2]); | |
| 1091 | + rc = Th_ExistsVar(interp, argv[2], TH1_LEN(argl[2])); | |
| 1057 | 1092 | Th_SetResultInt(interp, rc); |
| 1058 | 1093 | return TH_OK; |
| 1059 | 1094 | } |
| 1060 | 1095 | |
| 1061 | 1096 | /* |
| @@ -1117,11 +1152,11 @@ | ||
| 1117 | 1152 | int rc; |
| 1118 | 1153 | |
| 1119 | 1154 | if( argc!=3 ){ |
| 1120 | 1155 | return Th_WrongNumArgs(interp, "array exists var"); |
| 1121 | 1156 | } |
| 1122 | - rc = Th_ExistsArrayVar(interp, argv[2], argl[2]); | |
| 1157 | + rc = Th_ExistsArrayVar(interp, argv[2], TH1_LEN(argl[2])); | |
| 1123 | 1158 | Th_SetResultInt(interp, rc); |
| 1124 | 1159 | return TH_OK; |
| 1125 | 1160 | } |
| 1126 | 1161 | |
| 1127 | 1162 | /* |
| @@ -1137,11 +1172,11 @@ | ||
| 1137 | 1172 | int nElem = 0; |
| 1138 | 1173 | |
| 1139 | 1174 | if( argc!=3 ){ |
| 1140 | 1175 | return Th_WrongNumArgs(interp, "array names varname"); |
| 1141 | 1176 | } |
| 1142 | - rc = Th_ListAppendArray(interp, argv[2], argl[2], &zElem, &nElem); | |
| 1177 | + rc = Th_ListAppendArray(interp, argv[2], TH1_LEN(argl[2]), &zElem, &nElem); | |
| 1143 | 1178 | if( rc!=TH_OK ){ |
| 1144 | 1179 | return rc; |
| 1145 | 1180 | } |
| 1146 | 1181 | Th_SetResult(interp, zElem, nElem); |
| 1147 | 1182 | if( zElem ) Th_Free(interp, zElem); |
| @@ -1161,11 +1196,11 @@ | ||
| 1161 | 1196 | int *argl |
| 1162 | 1197 | ){ |
| 1163 | 1198 | if( argc!=2 ){ |
| 1164 | 1199 | return Th_WrongNumArgs(interp, "unset var"); |
| 1165 | 1200 | } |
| 1166 | - return Th_UnsetVar(interp, argv[1], argl[1]); | |
| 1201 | + return Th_UnsetVar(interp, argv[1], TH1_LEN(argl[1])); | |
| 1167 | 1202 | } |
| 1168 | 1203 | |
| 1169 | 1204 | int Th_CallSubCommand( |
| 1170 | 1205 | Th_Interp *interp, |
| 1171 | 1206 | void *ctx, |
| @@ -1176,19 +1211,22 @@ | ||
| 1176 | 1211 | ){ |
| 1177 | 1212 | if( argc>1 ){ |
| 1178 | 1213 | int i; |
| 1179 | 1214 | for(i=0; aSub[i].zName; i++){ |
| 1180 | 1215 | const char *zName = aSub[i].zName; |
| 1181 | - if( th_strlen(zName)==argl[1] && 0==memcmp(zName, argv[1], argl[1]) ){ | |
| 1216 | + if( th_strlen(zName)==TH1_LEN(argl[1]) | |
| 1217 | + && 0==memcmp(zName, argv[1], TH1_LEN(argl[1])) ){ | |
| 1182 | 1218 | return aSub[i].xProc(interp, ctx, argc, argv, argl); |
| 1183 | 1219 | } |
| 1184 | 1220 | } |
| 1185 | 1221 | } |
| 1186 | 1222 | if(argc<2){ |
| 1187 | - Th_ErrorMessage(interp, "Expected sub-command for", argv[0], argl[0]); | |
| 1223 | + Th_ErrorMessage(interp, "Expected sub-command for", | |
| 1224 | + argv[0], TH1_LEN(argl[0])); | |
| 1188 | 1225 | }else{ |
| 1189 | - Th_ErrorMessage(interp, "Expected sub-command, got:", argv[1], argl[1]); | |
| 1226 | + Th_ErrorMessage(interp, "Expected sub-command, got:", | |
| 1227 | + argv[1], TH1_LEN(argl[1])); | |
| 1190 | 1228 | } |
| 1191 | 1229 | return TH_ERROR; |
| 1192 | 1230 | } |
| 1193 | 1231 | |
| 1194 | 1232 | /* |
| @@ -1319,11 +1357,11 @@ | ||
| 1319 | 1357 | int iFrame = -1; |
| 1320 | 1358 | |
| 1321 | 1359 | if( argc!=2 && argc!=3 ){ |
| 1322 | 1360 | return Th_WrongNumArgs(interp, "uplevel ?level? script..."); |
| 1323 | 1361 | } |
| 1324 | - if( argc==3 && TH_OK!=thToFrame(interp, argv[1], argl[1], &iFrame) ){ | |
| 1362 | + if( argc==3 && TH_OK!=thToFrame(interp, argv[1], TH1_LEN(argl[1]), &iFrame) ){ | |
| 1325 | 1363 | return TH_ERROR; |
| 1326 | 1364 | } |
| 1327 | 1365 | return Th_Eval(interp, iFrame, argv[argc-1], -1); |
| 1328 | 1366 | } |
| 1329 | 1367 | |
| @@ -1342,19 +1380,20 @@ | ||
| 1342 | 1380 | int iVar = 1; |
| 1343 | 1381 | int iFrame = -1; |
| 1344 | 1382 | int rc = TH_OK; |
| 1345 | 1383 | int i; |
| 1346 | 1384 | |
| 1347 | - if( TH_OK==thToFrame(0, argv[1], argl[1], &iFrame) ){ | |
| 1385 | + if( TH_OK==thToFrame(0, argv[1], TH1_LEN(argl[1]), &iFrame) ){ | |
| 1348 | 1386 | iVar++; |
| 1349 | 1387 | } |
| 1350 | 1388 | if( argc==iVar || (argc-iVar)%2 ){ |
| 1351 | 1389 | return Th_WrongNumArgs(interp, |
| 1352 | 1390 | "upvar frame othervar myvar ?othervar myvar...?"); |
| 1353 | 1391 | } |
| 1354 | 1392 | for(i=iVar; rc==TH_OK && i<argc; i=i+2){ |
| 1355 | - rc = Th_LinkVar(interp, argv[i+1], argl[i+1], iFrame, argv[i], argl[i]); | |
| 1393 | + rc = Th_LinkVar(interp, argv[i+1], TH1_LEN(argl[i+1]), | |
| 1394 | + iFrame, argv[i], TH1_LEN(argl[i])); | |
| 1356 | 1395 | } |
| 1357 | 1396 | return rc; |
| 1358 | 1397 | } |
| 1359 | 1398 | |
| 1360 | 1399 | /* |
| 1361 | 1400 |
| --- src/th_lang.c | |
| +++ src/th_lang.c | |
| @@ -39,11 +39,11 @@ | |
| 39 | |
| 40 | rc = Th_Eval(interp, 0, argv[1], -1); |
| 41 | if( argc==3 ){ |
| 42 | int nResult; |
| 43 | const char *zResult = Th_GetResult(interp, &nResult); |
| 44 | Th_SetVar(interp, argv[2], argl[2], zResult, nResult); |
| 45 | } |
| 46 | |
| 47 | Th_SetResultInt(interp, rc); |
| 48 | return TH_OK; |
| 49 | } |
| @@ -180,20 +180,24 @@ | |
| 180 | int nVar; |
| 181 | char **azValue = 0; |
| 182 | int *anValue; |
| 183 | int nValue; |
| 184 | int ii, jj; |
| 185 | |
| 186 | if( argc!=4 ){ |
| 187 | return Th_WrongNumArgs(interp, "foreach varlist list script"); |
| 188 | } |
| 189 | rc = Th_SplitList(interp, argv[1], argl[1], &azVar, &anVar, &nVar); |
| 190 | if( rc ) return rc; |
| 191 | rc = Th_SplitList(interp, argv[2], argl[2], &azValue, &anValue, &nValue); |
| 192 | for(ii=0; rc==TH_OK && ii<=nValue-nVar; ii+=nVar){ |
| 193 | for(jj=0; jj<nVar; jj++){ |
| 194 | Th_SetVar(interp, azVar[jj], anVar[jj], azValue[ii+jj], anValue[ii+jj]); |
| 195 | } |
| 196 | rc = eval_loopbody(interp, argv[3], argl[3]); |
| 197 | } |
| 198 | if( rc==TH_BREAK ) rc = TH_OK; |
| 199 | Th_Free(interp, azVar); |
| @@ -215,15 +219,18 @@ | |
| 215 | int *argl |
| 216 | ){ |
| 217 | char *zList = 0; |
| 218 | int nList = 0; |
| 219 | int i; |
| 220 | |
| 221 | for(i=1; i<argc; i++){ |
| 222 | Th_ListAppend(interp, &zList, &nList, argv[i], argl[i]); |
| 223 | } |
| 224 | |
| 225 | Th_SetResult(interp, zList, nList); |
| 226 | Th_Free(interp, zList); |
| 227 | |
| 228 | return TH_OK; |
| 229 | } |
| @@ -244,23 +251,27 @@ | |
| 244 | int *argl |
| 245 | ){ |
| 246 | char *zList = 0; |
| 247 | int nList = 0; |
| 248 | int i, rc; |
| 249 | |
| 250 | if( argc<2 ){ |
| 251 | return Th_WrongNumArgs(interp, "lappend var ..."); |
| 252 | } |
| 253 | rc = Th_GetVar(interp, argv[1], argl[1]); |
| 254 | if( rc==TH_OK ){ |
| 255 | zList = Th_TakeResult(interp, &nList); |
| 256 | } |
| 257 | |
| 258 | for(i=2; i<argc; i++){ |
| 259 | Th_ListAppend(interp, &zList, &nList, argv[i], argl[i]); |
| 260 | } |
| 261 | |
| 262 | Th_SetVar(interp, argv[1], argl[1], zList, nList); |
| 263 | Th_SetResult(interp, zList, nList); |
| 264 | Th_Free(interp, zList); |
| 265 | |
| 266 | return TH_OK; |
| @@ -283,23 +294,27 @@ | |
| 283 | int rc; |
| 284 | |
| 285 | char **azElem; |
| 286 | int *anElem; |
| 287 | int nCount; |
| 288 | |
| 289 | if( argc!=3 ){ |
| 290 | return Th_WrongNumArgs(interp, "lindex list index"); |
| 291 | } |
| 292 | |
| 293 | if( TH_OK!=Th_ToInt(interp, argv[2], argl[2], &iElem) ){ |
| 294 | return TH_ERROR; |
| 295 | } |
| 296 | |
| 297 | rc = Th_SplitList(interp, argv[1], argl[1], &azElem, &anElem, &nCount); |
| 298 | if( rc==TH_OK ){ |
| 299 | if( iElem<nCount && iElem>=0 ){ |
| 300 | Th_SetResult(interp, azElem[iElem], anElem[iElem]); |
| 301 | }else{ |
| 302 | Th_SetResult(interp, 0, 0); |
| 303 | } |
| 304 | Th_Free(interp, azElem); |
| 305 | } |
| @@ -356,13 +371,14 @@ | |
| 356 | return Th_WrongNumArgs(interp, "lsearch list string"); |
| 357 | } |
| 358 | |
| 359 | rc = Th_SplitList(interp, argv[1], argl[1], &azElem, &anElem, &nCount); |
| 360 | if( rc==TH_OK ){ |
| 361 | Th_SetResultInt(interp, -1); |
| 362 | for(i=0; i<nCount; i++){ |
| 363 | if( anElem[i]==argl[2] && 0==memcmp(azElem[i], argv[2], argl[2]) ){ |
| 364 | Th_SetResultInt(interp, i); |
| 365 | break; |
| 366 | } |
| 367 | } |
| 368 | Th_Free(interp, azElem); |
| @@ -561,28 +577,31 @@ | |
| 561 | int nUsage = 0; /* Number of bytes at zUsage */ |
| 562 | |
| 563 | if( argc!=4 ){ |
| 564 | return Th_WrongNumArgs(interp, "proc name arglist code"); |
| 565 | } |
| 566 | if( Th_SplitList(interp, argv[2], argl[2], &azParam, &anParam, &nParam) ){ |
| 567 | return TH_ERROR; |
| 568 | } |
| 569 | |
| 570 | /* Allocate the new ProcDefn structure. */ |
| 571 | nByte = sizeof(ProcDefn) + /* ProcDefn structure */ |
| 572 | (sizeof(char *) + sizeof(int)) * nParam + /* azParam, anParam */ |
| 573 | (sizeof(char *) + sizeof(int)) * nParam + /* azDefault, anDefault */ |
| 574 | argl[3] + /* zProgram */ |
| 575 | argl[2]; /* Space for copies of parameter names and default values */ |
| 576 | p = (ProcDefn *)Th_Malloc(interp, nByte); |
| 577 | |
| 578 | /* If the last parameter in the parameter list is "args", then set the |
| 579 | ** ProcDefn.hasArgs flag. The "args" parameter does not require an |
| 580 | ** entry in the ProcDefn.azParam[] or ProcDefn.azDefault[] arrays. |
| 581 | */ |
| 582 | if( nParam>0 ){ |
| 583 | if( anParam[nParam-1]==4 && 0==memcmp(azParam[nParam-1], "args", 4) ){ |
| 584 | p->hasArgs = 1; |
| 585 | nParam--; |
| 586 | } |
| 587 | } |
| 588 | |
| @@ -590,12 +609,12 @@ | |
| 590 | p->azParam = (char **)&p[1]; |
| 591 | p->anParam = (int *)&p->azParam[nParam]; |
| 592 | p->azDefault = (char **)&p->anParam[nParam]; |
| 593 | p->anDefault = (int *)&p->azDefault[nParam]; |
| 594 | p->zProgram = (char *)&p->anDefault[nParam]; |
| 595 | memcpy(p->zProgram, argv[3], argl[3]); |
| 596 | p->nProgram = argl[3]; |
| 597 | zSpace = &p->zProgram[p->nProgram]; |
| 598 | |
| 599 | for(i=0; i<nParam; i++){ |
| 600 | char **az; |
| 601 | int *an; |
| @@ -672,11 +691,12 @@ | |
| 672 | int *argl |
| 673 | ){ |
| 674 | if( argc!=3 ){ |
| 675 | return Th_WrongNumArgs(interp, "rename oldcmd newcmd"); |
| 676 | } |
| 677 | return Th_RenameCommand(interp, argv[1], argl[1], argv[2], argl[2]); |
| 678 | } |
| 679 | |
| 680 | /* |
| 681 | ** TH Syntax: |
| 682 | ** |
| @@ -746,13 +766,13 @@ | |
| 746 | if( argc!=4 ){ |
| 747 | return Th_WrongNumArgs(interp, "string compare str1 str2"); |
| 748 | } |
| 749 | |
| 750 | zLeft = argv[2]; |
| 751 | nLeft = argl[2]; |
| 752 | zRight = argv[3]; |
| 753 | nRight = argl[3]; |
| 754 | |
| 755 | for(i=0; iRes==0 && i<nLeft && i<nRight; i++){ |
| 756 | iRes = zLeft[i]-zRight[i]; |
| 757 | } |
| 758 | if( iRes==0 ){ |
| @@ -779,12 +799,12 @@ | |
| 779 | |
| 780 | if( argc!=4 ){ |
| 781 | return Th_WrongNumArgs(interp, "string first needle haystack"); |
| 782 | } |
| 783 | |
| 784 | nNeedle = argl[2]; |
| 785 | nHaystack = argl[3]; |
| 786 | |
| 787 | if( nNeedle && nHaystack && nNeedle<=nHaystack ){ |
| 788 | const char *zNeedle = argv[2]; |
| 789 | const char *zHaystack = argv[3]; |
| 790 | int i; |
| @@ -812,20 +832,22 @@ | |
| 812 | |
| 813 | if( argc!=4 ){ |
| 814 | return Th_WrongNumArgs(interp, "string index string index"); |
| 815 | } |
| 816 | |
| 817 | if( argl[3]==3 && 0==memcmp("end", argv[3], 3) ){ |
| 818 | iIndex = argl[2]-1; |
| 819 | }else if( Th_ToInt(interp, argv[3], argl[3], &iIndex) ){ |
| 820 | Th_ErrorMessage( |
| 821 | interp, "Expected \"end\" or integer, got:", argv[3], argl[3]); |
| 822 | return TH_ERROR; |
| 823 | } |
| 824 | |
| 825 | if( iIndex>=0 && iIndex<argl[2] ){ |
| 826 | return Th_SetResult(interp, &argv[2][iIndex], 1); |
| 827 | }else{ |
| 828 | return Th_SetResult(interp, 0, 0); |
| 829 | } |
| 830 | } |
| 831 | |
| @@ -838,41 +860,44 @@ | |
| 838 | Th_Interp *interp, void *ctx, int argc, const char **argv, int *argl |
| 839 | ){ |
| 840 | if( argc!=4 ){ |
| 841 | return Th_WrongNumArgs(interp, "string is class string"); |
| 842 | } |
| 843 | if( argl[2]==5 && 0==memcmp(argv[2], "alnum", 5) ){ |
| 844 | int i; |
| 845 | int iRes = 1; |
| 846 | |
| 847 | for(i=0; i<argl[3]; i++){ |
| 848 | if( !th_isalnum(argv[3][i]) ){ |
| 849 | iRes = 0; |
| 850 | } |
| 851 | } |
| 852 | |
| 853 | return Th_SetResultInt(interp, iRes); |
| 854 | }else if( argl[2]==6 && 0==memcmp(argv[2], "double", 6) ){ |
| 855 | double fVal; |
| 856 | if( Th_ToDouble(interp, argv[3], argl[3], &fVal)==TH_OK ){ |
| 857 | return Th_SetResultInt(interp, 1); |
| 858 | } |
| 859 | return Th_SetResultInt(interp, 0); |
| 860 | }else if( argl[2]==7 && 0==memcmp(argv[2], "integer", 7) ){ |
| 861 | int iVal; |
| 862 | if( Th_ToInt(interp, argv[3], argl[3], &iVal)==TH_OK ){ |
| 863 | return Th_SetResultInt(interp, 1); |
| 864 | } |
| 865 | return Th_SetResultInt(interp, 0); |
| 866 | }else if( argl[2]==4 && 0==memcmp(argv[2], "list", 4) ){ |
| 867 | if( Th_SplitList(interp, argv[3], argl[3], 0, 0, 0)==TH_OK ){ |
| 868 | return Th_SetResultInt(interp, 1); |
| 869 | } |
| 870 | return Th_SetResultInt(interp, 0); |
| 871 | }else{ |
| 872 | Th_ErrorMessage(interp, |
| 873 | "Expected alnum, double, integer, or list, got:", argv[2], argl[2]); |
| 874 | return TH_ERROR; |
| 875 | } |
| 876 | } |
| 877 | |
| 878 | /* |
| @@ -889,12 +914,12 @@ | |
| 889 | |
| 890 | if( argc!=4 ){ |
| 891 | return Th_WrongNumArgs(interp, "string last needle haystack"); |
| 892 | } |
| 893 | |
| 894 | nNeedle = argl[2]; |
| 895 | nHaystack = argl[3]; |
| 896 | |
| 897 | if( nNeedle && nHaystack && nNeedle<=nHaystack ){ |
| 898 | const char *zNeedle = argv[2]; |
| 899 | const char *zHaystack = argv[3]; |
| 900 | int i; |
| @@ -919,11 +944,11 @@ | |
| 919 | Th_Interp *interp, void *ctx, int argc, const char **argv, int *argl |
| 920 | ){ |
| 921 | if( argc!=3 ){ |
| 922 | return Th_WrongNumArgs(interp, "string length string"); |
| 923 | } |
| 924 | return Th_SetResultInt(interp, argl[2]); |
| 925 | } |
| 926 | |
| 927 | /* |
| 928 | ** TH Syntax: |
| 929 | ** |
| @@ -938,12 +963,12 @@ | |
| 938 | char *zPat, *zStr; |
| 939 | int rc; |
| 940 | if( argc!=4 ){ |
| 941 | return Th_WrongNumArgs(interp, "string match pattern string"); |
| 942 | } |
| 943 | zPat = fossil_strndup(argv[2],argl[2]); |
| 944 | zStr = fossil_strndup(argv[3],argl[3]); |
| 945 | rc = sqlite3_strglob(zPat,zStr); |
| 946 | fossil_free(zPat); |
| 947 | fossil_free(zStr); |
| 948 | return Th_SetResultInt(interp, !rc); |
| 949 | } |
| @@ -956,31 +981,34 @@ | |
| 956 | static int string_range_command( |
| 957 | Th_Interp *interp, void *ctx, int argc, const char **argv, int *argl |
| 958 | ){ |
| 959 | int iStart; |
| 960 | int iEnd; |
| 961 | |
| 962 | if( argc!=5 ){ |
| 963 | return Th_WrongNumArgs(interp, "string range string first last"); |
| 964 | } |
| 965 | |
| 966 | if( argl[4]==3 && 0==memcmp("end", argv[4], 3) ){ |
| 967 | iEnd = argl[2]; |
| 968 | }else if( Th_ToInt(interp, argv[4], argl[4], &iEnd) ){ |
| 969 | Th_ErrorMessage( |
| 970 | interp, "Expected \"end\" or integer, got:", argv[4], argl[4]); |
| 971 | return TH_ERROR; |
| 972 | } |
| 973 | if( Th_ToInt(interp, argv[3], argl[3], &iStart) ){ |
| 974 | return TH_ERROR; |
| 975 | } |
| 976 | |
| 977 | if( iStart<0 ) iStart = 0; |
| 978 | if( iEnd>=argl[2] ) iEnd = argl[2]-1; |
| 979 | if( iStart>iEnd ) iEnd = iStart-1; |
| 980 | |
| 981 | return Th_SetResult(interp, &argv[2][iStart], iEnd-iStart+1); |
| 982 | } |
| 983 | |
| 984 | /* |
| 985 | ** TH Syntax: |
| 986 | ** |
| @@ -989,27 +1017,33 @@ | |
| 989 | static int string_repeat_command( |
| 990 | Th_Interp *interp, void *ctx, int argc, const char **argv, int *argl |
| 991 | ){ |
| 992 | int n; |
| 993 | int i; |
| 994 | int nByte; |
| 995 | char *zByte; |
| 996 | |
| 997 | if( argc!=4 ){ |
| 998 | return Th_WrongNumArgs(interp, "string repeat string n"); |
| 999 | } |
| 1000 | if( Th_ToInt(interp, argv[3], argl[3], &n) ){ |
| 1001 | return TH_ERROR; |
| 1002 | } |
| 1003 | |
| 1004 | nByte = argl[2] * n; |
| 1005 | zByte = Th_Malloc(interp, nByte+1); |
| 1006 | for(i=0; i<nByte; i+=argl[2]){ |
| 1007 | memcpy(&zByte[i], argv[2], argl[2]); |
| 1008 | } |
| 1009 | |
| 1010 | Th_SetResult(interp, zByte, nByte); |
| 1011 | Th_Free(interp, zByte); |
| 1012 | return TH_OK; |
| 1013 | } |
| 1014 | |
| 1015 | /* |
| @@ -1027,17 +1061,18 @@ | |
| 1027 | |
| 1028 | if( argc!=3 ){ |
| 1029 | return Th_WrongNumArgs(interp, "string trim string"); |
| 1030 | } |
| 1031 | z = argv[2]; |
| 1032 | n = argl[2]; |
| 1033 | if( argl[1]<5 || argv[1][4]=='l' ){ |
| 1034 | while( n && th_isspace(z[0]) ){ z++; n--; } |
| 1035 | } |
| 1036 | if( argl[1]<5 || argv[1][4]=='r' ){ |
| 1037 | while( n && th_isspace(z[n-1]) ){ n--; } |
| 1038 | } |
| 1039 | Th_SetResult(interp, z, n); |
| 1040 | return TH_OK; |
| 1041 | } |
| 1042 | |
| 1043 | /* |
| @@ -1051,11 +1086,11 @@ | |
| 1051 | int rc; |
| 1052 | |
| 1053 | if( argc!=3 ){ |
| 1054 | return Th_WrongNumArgs(interp, "info exists var"); |
| 1055 | } |
| 1056 | rc = Th_ExistsVar(interp, argv[2], argl[2]); |
| 1057 | Th_SetResultInt(interp, rc); |
| 1058 | return TH_OK; |
| 1059 | } |
| 1060 | |
| 1061 | /* |
| @@ -1117,11 +1152,11 @@ | |
| 1117 | int rc; |
| 1118 | |
| 1119 | if( argc!=3 ){ |
| 1120 | return Th_WrongNumArgs(interp, "array exists var"); |
| 1121 | } |
| 1122 | rc = Th_ExistsArrayVar(interp, argv[2], argl[2]); |
| 1123 | Th_SetResultInt(interp, rc); |
| 1124 | return TH_OK; |
| 1125 | } |
| 1126 | |
| 1127 | /* |
| @@ -1137,11 +1172,11 @@ | |
| 1137 | int nElem = 0; |
| 1138 | |
| 1139 | if( argc!=3 ){ |
| 1140 | return Th_WrongNumArgs(interp, "array names varname"); |
| 1141 | } |
| 1142 | rc = Th_ListAppendArray(interp, argv[2], argl[2], &zElem, &nElem); |
| 1143 | if( rc!=TH_OK ){ |
| 1144 | return rc; |
| 1145 | } |
| 1146 | Th_SetResult(interp, zElem, nElem); |
| 1147 | if( zElem ) Th_Free(interp, zElem); |
| @@ -1161,11 +1196,11 @@ | |
| 1161 | int *argl |
| 1162 | ){ |
| 1163 | if( argc!=2 ){ |
| 1164 | return Th_WrongNumArgs(interp, "unset var"); |
| 1165 | } |
| 1166 | return Th_UnsetVar(interp, argv[1], argl[1]); |
| 1167 | } |
| 1168 | |
| 1169 | int Th_CallSubCommand( |
| 1170 | Th_Interp *interp, |
| 1171 | void *ctx, |
| @@ -1176,19 +1211,22 @@ | |
| 1176 | ){ |
| 1177 | if( argc>1 ){ |
| 1178 | int i; |
| 1179 | for(i=0; aSub[i].zName; i++){ |
| 1180 | const char *zName = aSub[i].zName; |
| 1181 | if( th_strlen(zName)==argl[1] && 0==memcmp(zName, argv[1], argl[1]) ){ |
| 1182 | return aSub[i].xProc(interp, ctx, argc, argv, argl); |
| 1183 | } |
| 1184 | } |
| 1185 | } |
| 1186 | if(argc<2){ |
| 1187 | Th_ErrorMessage(interp, "Expected sub-command for", argv[0], argl[0]); |
| 1188 | }else{ |
| 1189 | Th_ErrorMessage(interp, "Expected sub-command, got:", argv[1], argl[1]); |
| 1190 | } |
| 1191 | return TH_ERROR; |
| 1192 | } |
| 1193 | |
| 1194 | /* |
| @@ -1319,11 +1357,11 @@ | |
| 1319 | int iFrame = -1; |
| 1320 | |
| 1321 | if( argc!=2 && argc!=3 ){ |
| 1322 | return Th_WrongNumArgs(interp, "uplevel ?level? script..."); |
| 1323 | } |
| 1324 | if( argc==3 && TH_OK!=thToFrame(interp, argv[1], argl[1], &iFrame) ){ |
| 1325 | return TH_ERROR; |
| 1326 | } |
| 1327 | return Th_Eval(interp, iFrame, argv[argc-1], -1); |
| 1328 | } |
| 1329 | |
| @@ -1342,19 +1380,20 @@ | |
| 1342 | int iVar = 1; |
| 1343 | int iFrame = -1; |
| 1344 | int rc = TH_OK; |
| 1345 | int i; |
| 1346 | |
| 1347 | if( TH_OK==thToFrame(0, argv[1], argl[1], &iFrame) ){ |
| 1348 | iVar++; |
| 1349 | } |
| 1350 | if( argc==iVar || (argc-iVar)%2 ){ |
| 1351 | return Th_WrongNumArgs(interp, |
| 1352 | "upvar frame othervar myvar ?othervar myvar...?"); |
| 1353 | } |
| 1354 | for(i=iVar; rc==TH_OK && i<argc; i=i+2){ |
| 1355 | rc = Th_LinkVar(interp, argv[i+1], argl[i+1], iFrame, argv[i], argl[i]); |
| 1356 | } |
| 1357 | return rc; |
| 1358 | } |
| 1359 | |
| 1360 | /* |
| 1361 |
| --- src/th_lang.c | |
| +++ src/th_lang.c | |
| @@ -39,11 +39,11 @@ | |
| 39 | |
| 40 | rc = Th_Eval(interp, 0, argv[1], -1); |
| 41 | if( argc==3 ){ |
| 42 | int nResult; |
| 43 | const char *zResult = Th_GetResult(interp, &nResult); |
| 44 | Th_SetVar(interp, argv[2], TH1_LEN(argl[2]), zResult, nResult); |
| 45 | } |
| 46 | |
| 47 | Th_SetResultInt(interp, rc); |
| 48 | return TH_OK; |
| 49 | } |
| @@ -180,20 +180,24 @@ | |
| 180 | int nVar; |
| 181 | char **azValue = 0; |
| 182 | int *anValue; |
| 183 | int nValue; |
| 184 | int ii, jj; |
| 185 | int bTaint = 0; |
| 186 | |
| 187 | if( argc!=4 ){ |
| 188 | return Th_WrongNumArgs(interp, "foreach varlist list script"); |
| 189 | } |
| 190 | rc = Th_SplitList(interp, argv[1], argl[1], &azVar, &anVar, &nVar); |
| 191 | if( rc ) return rc; |
| 192 | TH1_XFER_TAINT(bTaint, argl[2]); |
| 193 | rc = Th_SplitList(interp, argv[2], argl[2], &azValue, &anValue, &nValue); |
| 194 | for(ii=0; rc==TH_OK && ii<=nValue-nVar; ii+=nVar){ |
| 195 | for(jj=0; jj<nVar; jj++){ |
| 196 | int x = anValue[ii+jj]; |
| 197 | TH1_XFER_TAINT(x, bTaint); |
| 198 | Th_SetVar(interp, azVar[jj], anVar[jj], azValue[ii+jj], x); |
| 199 | } |
| 200 | rc = eval_loopbody(interp, argv[3], argl[3]); |
| 201 | } |
| 202 | if( rc==TH_BREAK ) rc = TH_OK; |
| 203 | Th_Free(interp, azVar); |
| @@ -215,15 +219,18 @@ | |
| 219 | int *argl |
| 220 | ){ |
| 221 | char *zList = 0; |
| 222 | int nList = 0; |
| 223 | int i; |
| 224 | int bTaint = 0; |
| 225 | |
| 226 | for(i=1; i<argc; i++){ |
| 227 | TH1_XFER_TAINT(bTaint,argl[i]); |
| 228 | Th_ListAppend(interp, &zList, &nList, argv[i], argl[i]); |
| 229 | } |
| 230 | |
| 231 | TH1_XFER_TAINT(nList, bTaint); |
| 232 | Th_SetResult(interp, zList, nList); |
| 233 | Th_Free(interp, zList); |
| 234 | |
| 235 | return TH_OK; |
| 236 | } |
| @@ -244,23 +251,27 @@ | |
| 251 | int *argl |
| 252 | ){ |
| 253 | char *zList = 0; |
| 254 | int nList = 0; |
| 255 | int i, rc; |
| 256 | int bTaint = 0; |
| 257 | |
| 258 | if( argc<2 ){ |
| 259 | return Th_WrongNumArgs(interp, "lappend var ..."); |
| 260 | } |
| 261 | rc = Th_GetVar(interp, argv[1], argl[1]); |
| 262 | if( rc==TH_OK ){ |
| 263 | zList = Th_TakeResult(interp, &nList); |
| 264 | } |
| 265 | |
| 266 | TH1_XFER_TAINT(bTaint, nList); |
| 267 | for(i=2; i<argc; i++){ |
| 268 | TH1_XFER_TAINT(bTaint, argl[i]); |
| 269 | Th_ListAppend(interp, &zList, &nList, argv[i], argl[i]); |
| 270 | } |
| 271 | |
| 272 | TH1_XFER_TAINT(nList, bTaint); |
| 273 | Th_SetVar(interp, argv[1], argl[1], zList, nList); |
| 274 | Th_SetResult(interp, zList, nList); |
| 275 | Th_Free(interp, zList); |
| 276 | |
| 277 | return TH_OK; |
| @@ -283,23 +294,27 @@ | |
| 294 | int rc; |
| 295 | |
| 296 | char **azElem; |
| 297 | int *anElem; |
| 298 | int nCount; |
| 299 | int bTaint = 0; |
| 300 | |
| 301 | if( argc!=3 ){ |
| 302 | return Th_WrongNumArgs(interp, "lindex list index"); |
| 303 | } |
| 304 | |
| 305 | if( TH_OK!=Th_ToInt(interp, argv[2], argl[2], &iElem) ){ |
| 306 | return TH_ERROR; |
| 307 | } |
| 308 | |
| 309 | TH1_XFER_TAINT(bTaint, argl[1]); |
| 310 | rc = Th_SplitList(interp, argv[1], argl[1], &azElem, &anElem, &nCount); |
| 311 | if( rc==TH_OK ){ |
| 312 | if( iElem<nCount && iElem>=0 ){ |
| 313 | int sz = anElem[iElem]; |
| 314 | TH1_XFER_TAINT(sz, bTaint); |
| 315 | Th_SetResult(interp, azElem[iElem], sz); |
| 316 | }else{ |
| 317 | Th_SetResult(interp, 0, 0); |
| 318 | } |
| 319 | Th_Free(interp, azElem); |
| 320 | } |
| @@ -356,13 +371,14 @@ | |
| 371 | return Th_WrongNumArgs(interp, "lsearch list string"); |
| 372 | } |
| 373 | |
| 374 | rc = Th_SplitList(interp, argv[1], argl[1], &azElem, &anElem, &nCount); |
| 375 | if( rc==TH_OK ){ |
| 376 | int nn = TH1_LEN(argl[2]); |
| 377 | Th_SetResultInt(interp, -1); |
| 378 | for(i=0; i<nCount; i++){ |
| 379 | if( TH1_LEN(anElem[i])==nn && 0==memcmp(azElem[i], argv[2], nn) ){ |
| 380 | Th_SetResultInt(interp, i); |
| 381 | break; |
| 382 | } |
| 383 | } |
| 384 | Th_Free(interp, azElem); |
| @@ -561,28 +577,31 @@ | |
| 577 | int nUsage = 0; /* Number of bytes at zUsage */ |
| 578 | |
| 579 | if( argc!=4 ){ |
| 580 | return Th_WrongNumArgs(interp, "proc name arglist code"); |
| 581 | } |
| 582 | if( Th_SplitList(interp, argv[2], TH1_LEN(argl[2]), |
| 583 | &azParam, &anParam, &nParam) ){ |
| 584 | return TH_ERROR; |
| 585 | } |
| 586 | |
| 587 | /* Allocate the new ProcDefn structure. */ |
| 588 | nByte = sizeof(ProcDefn) + /* ProcDefn structure */ |
| 589 | (sizeof(char *) + sizeof(int)) * nParam + /* azParam, anParam */ |
| 590 | (sizeof(char *) + sizeof(int)) * nParam + /* azDefault, anDefault */ |
| 591 | TH1_LEN(argl[3]) + /* zProgram */ |
| 592 | TH1_LEN(argl[2]); /* Space for copies of param names and dflt values */ |
| 593 | p = (ProcDefn *)Th_Malloc(interp, nByte); |
| 594 | |
| 595 | /* If the last parameter in the parameter list is "args", then set the |
| 596 | ** ProcDefn.hasArgs flag. The "args" parameter does not require an |
| 597 | ** entry in the ProcDefn.azParam[] or ProcDefn.azDefault[] arrays. |
| 598 | */ |
| 599 | if( nParam>0 ){ |
| 600 | if( TH1_LEN(anParam[nParam-1])==4 |
| 601 | && 0==memcmp(azParam[nParam-1], "args", 4) |
| 602 | ){ |
| 603 | p->hasArgs = 1; |
| 604 | nParam--; |
| 605 | } |
| 606 | } |
| 607 | |
| @@ -590,12 +609,12 @@ | |
| 609 | p->azParam = (char **)&p[1]; |
| 610 | p->anParam = (int *)&p->azParam[nParam]; |
| 611 | p->azDefault = (char **)&p->anParam[nParam]; |
| 612 | p->anDefault = (int *)&p->azDefault[nParam]; |
| 613 | p->zProgram = (char *)&p->anDefault[nParam]; |
| 614 | memcpy(p->zProgram, argv[3], TH1_LEN(argl[3])); |
| 615 | p->nProgram = TH1_LEN(argl[3]); |
| 616 | zSpace = &p->zProgram[p->nProgram]; |
| 617 | |
| 618 | for(i=0; i<nParam; i++){ |
| 619 | char **az; |
| 620 | int *an; |
| @@ -672,11 +691,12 @@ | |
| 691 | int *argl |
| 692 | ){ |
| 693 | if( argc!=3 ){ |
| 694 | return Th_WrongNumArgs(interp, "rename oldcmd newcmd"); |
| 695 | } |
| 696 | return Th_RenameCommand(interp, argv[1], TH1_LEN(argl[1]), |
| 697 | argv[2], TH1_LEN(argl[2])); |
| 698 | } |
| 699 | |
| 700 | /* |
| 701 | ** TH Syntax: |
| 702 | ** |
| @@ -746,13 +766,13 @@ | |
| 766 | if( argc!=4 ){ |
| 767 | return Th_WrongNumArgs(interp, "string compare str1 str2"); |
| 768 | } |
| 769 | |
| 770 | zLeft = argv[2]; |
| 771 | nLeft = TH1_LEN(argl[2]); |
| 772 | zRight = argv[3]; |
| 773 | nRight = TH1_LEN(argl[3]); |
| 774 | |
| 775 | for(i=0; iRes==0 && i<nLeft && i<nRight; i++){ |
| 776 | iRes = zLeft[i]-zRight[i]; |
| 777 | } |
| 778 | if( iRes==0 ){ |
| @@ -779,12 +799,12 @@ | |
| 799 | |
| 800 | if( argc!=4 ){ |
| 801 | return Th_WrongNumArgs(interp, "string first needle haystack"); |
| 802 | } |
| 803 | |
| 804 | nNeedle = TH1_LEN(argl[2]); |
| 805 | nHaystack = TH1_LEN(argl[3]); |
| 806 | |
| 807 | if( nNeedle && nHaystack && nNeedle<=nHaystack ){ |
| 808 | const char *zNeedle = argv[2]; |
| 809 | const char *zHaystack = argv[3]; |
| 810 | int i; |
| @@ -812,20 +832,22 @@ | |
| 832 | |
| 833 | if( argc!=4 ){ |
| 834 | return Th_WrongNumArgs(interp, "string index string index"); |
| 835 | } |
| 836 | |
| 837 | if( TH1_LEN(argl[3])==3 && 0==memcmp("end", argv[3], 3) ){ |
| 838 | iIndex = TH1_LEN(argl[2])-1; |
| 839 | }else if( Th_ToInt(interp, argv[3], argl[3], &iIndex) ){ |
| 840 | Th_ErrorMessage( |
| 841 | interp, "Expected \"end\" or integer, got:", argv[3], argl[3]); |
| 842 | return TH_ERROR; |
| 843 | } |
| 844 | |
| 845 | if( iIndex>=0 && iIndex<TH1_LEN(argl[2]) ){ |
| 846 | int sz = 1; |
| 847 | TH1_XFER_TAINT(sz, argl[2]); |
| 848 | return Th_SetResult(interp, &argv[2][iIndex], sz); |
| 849 | }else{ |
| 850 | return Th_SetResult(interp, 0, 0); |
| 851 | } |
| 852 | } |
| 853 | |
| @@ -838,41 +860,44 @@ | |
| 860 | Th_Interp *interp, void *ctx, int argc, const char **argv, int *argl |
| 861 | ){ |
| 862 | if( argc!=4 ){ |
| 863 | return Th_WrongNumArgs(interp, "string is class string"); |
| 864 | } |
| 865 | if( TH1_LEN(argl[2])==5 && 0==memcmp(argv[2], "alnum", 5) ){ |
| 866 | int i; |
| 867 | int iRes = 1; |
| 868 | |
| 869 | for(i=0; i<TH1_LEN(argl[3]); i++){ |
| 870 | if( !th_isalnum(argv[3][i]) ){ |
| 871 | iRes = 0; |
| 872 | } |
| 873 | } |
| 874 | |
| 875 | return Th_SetResultInt(interp, iRes); |
| 876 | }else if( TH1_LEN(argl[2])==6 && 0==memcmp(argv[2], "double", 6) ){ |
| 877 | double fVal; |
| 878 | if( Th_ToDouble(interp, argv[3], argl[3], &fVal)==TH_OK ){ |
| 879 | return Th_SetResultInt(interp, 1); |
| 880 | } |
| 881 | return Th_SetResultInt(interp, 0); |
| 882 | }else if( TH1_LEN(argl[2])==7 && 0==memcmp(argv[2], "integer", 7) ){ |
| 883 | int iVal; |
| 884 | if( Th_ToInt(interp, argv[3], argl[3], &iVal)==TH_OK ){ |
| 885 | return Th_SetResultInt(interp, 1); |
| 886 | } |
| 887 | return Th_SetResultInt(interp, 0); |
| 888 | }else if( TH1_LEN(argl[2])==4 && 0==memcmp(argv[2], "list", 4) ){ |
| 889 | if( Th_SplitList(interp, argv[3], argl[3], 0, 0, 0)==TH_OK ){ |
| 890 | return Th_SetResultInt(interp, 1); |
| 891 | } |
| 892 | return Th_SetResultInt(interp, 0); |
| 893 | }else if( TH1_LEN(argl[2])==7 && 0==memcmp(argv[2], "tainted", 7) ){ |
| 894 | return Th_SetResultInt(interp, TH1_TAINTED(argl[3])); |
| 895 | }else{ |
| 896 | Th_ErrorMessage(interp, |
| 897 | "Expected alnum, double, integer, list, or tainted, got:", |
| 898 | argv[2], TH1_LEN(argl[2])); |
| 899 | return TH_ERROR; |
| 900 | } |
| 901 | } |
| 902 | |
| 903 | /* |
| @@ -889,12 +914,12 @@ | |
| 914 | |
| 915 | if( argc!=4 ){ |
| 916 | return Th_WrongNumArgs(interp, "string last needle haystack"); |
| 917 | } |
| 918 | |
| 919 | nNeedle = TH1_LEN(argl[2]); |
| 920 | nHaystack = TH1_LEN(argl[3]); |
| 921 | |
| 922 | if( nNeedle && nHaystack && nNeedle<=nHaystack ){ |
| 923 | const char *zNeedle = argv[2]; |
| 924 | const char *zHaystack = argv[3]; |
| 925 | int i; |
| @@ -919,11 +944,11 @@ | |
| 944 | Th_Interp *interp, void *ctx, int argc, const char **argv, int *argl |
| 945 | ){ |
| 946 | if( argc!=3 ){ |
| 947 | return Th_WrongNumArgs(interp, "string length string"); |
| 948 | } |
| 949 | return Th_SetResultInt(interp, TH1_LEN(argl[2])); |
| 950 | } |
| 951 | |
| 952 | /* |
| 953 | ** TH Syntax: |
| 954 | ** |
| @@ -938,12 +963,12 @@ | |
| 963 | char *zPat, *zStr; |
| 964 | int rc; |
| 965 | if( argc!=4 ){ |
| 966 | return Th_WrongNumArgs(interp, "string match pattern string"); |
| 967 | } |
| 968 | zPat = fossil_strndup(argv[2],TH1_LEN(argl[2])); |
| 969 | zStr = fossil_strndup(argv[3],TH1_LEN(argl[3])); |
| 970 | rc = sqlite3_strglob(zPat,zStr); |
| 971 | fossil_free(zPat); |
| 972 | fossil_free(zStr); |
| 973 | return Th_SetResultInt(interp, !rc); |
| 974 | } |
| @@ -956,31 +981,34 @@ | |
| 981 | static int string_range_command( |
| 982 | Th_Interp *interp, void *ctx, int argc, const char **argv, int *argl |
| 983 | ){ |
| 984 | int iStart; |
| 985 | int iEnd; |
| 986 | int sz; |
| 987 | |
| 988 | if( argc!=5 ){ |
| 989 | return Th_WrongNumArgs(interp, "string range string first last"); |
| 990 | } |
| 991 | |
| 992 | if( TH1_LEN(argl[4])==3 && 0==memcmp("end", argv[4], 3) ){ |
| 993 | iEnd = TH1_LEN(argl[2]); |
| 994 | }else if( Th_ToInt(interp, argv[4], argl[4], &iEnd) ){ |
| 995 | Th_ErrorMessage( |
| 996 | interp, "Expected \"end\" or integer, got:", argv[4], TH1_LEN(argl[4])); |
| 997 | return TH_ERROR; |
| 998 | } |
| 999 | if( Th_ToInt(interp, argv[3], argl[3], &iStart) ){ |
| 1000 | return TH_ERROR; |
| 1001 | } |
| 1002 | |
| 1003 | if( iStart<0 ) iStart = 0; |
| 1004 | if( iEnd>=TH1_LEN(argl[2]) ) iEnd = TH1_LEN(argl[2])-1; |
| 1005 | if( iStart>iEnd ) iEnd = iStart-1; |
| 1006 | sz = iEnd - iStart + 1; |
| 1007 | TH1_XFER_TAINT(sz, argl[2]); |
| 1008 | |
| 1009 | return Th_SetResult(interp, &argv[2][iStart], sz); |
| 1010 | } |
| 1011 | |
| 1012 | /* |
| 1013 | ** TH Syntax: |
| 1014 | ** |
| @@ -989,27 +1017,33 @@ | |
| 1017 | static int string_repeat_command( |
| 1018 | Th_Interp *interp, void *ctx, int argc, const char **argv, int *argl |
| 1019 | ){ |
| 1020 | int n; |
| 1021 | int i; |
| 1022 | int sz; |
| 1023 | long long int nByte; |
| 1024 | char *zByte; |
| 1025 | |
| 1026 | if( argc!=4 ){ |
| 1027 | return Th_WrongNumArgs(interp, "string repeat string n"); |
| 1028 | } |
| 1029 | if( Th_ToInt(interp, argv[3], argl[3], &n) ){ |
| 1030 | return TH_ERROR; |
| 1031 | } |
| 1032 | |
| 1033 | nByte = n; |
| 1034 | sz = TH1_LEN(argl[2]); |
| 1035 | nByte *= sz; |
| 1036 | TH1_SIZECHECK(nByte+1); |
| 1037 | zByte = Th_Malloc(interp, nByte+1); |
| 1038 | for(i=0; i<nByte; i+=sz){ |
| 1039 | memcpy(&zByte[i], argv[2], sz); |
| 1040 | } |
| 1041 | |
| 1042 | n = nByte; |
| 1043 | TH1_XFER_TAINT(n, argl[2]); |
| 1044 | Th_SetResult(interp, zByte, n); |
| 1045 | Th_Free(interp, zByte); |
| 1046 | return TH_OK; |
| 1047 | } |
| 1048 | |
| 1049 | /* |
| @@ -1027,17 +1061,18 @@ | |
| 1061 | |
| 1062 | if( argc!=3 ){ |
| 1063 | return Th_WrongNumArgs(interp, "string trim string"); |
| 1064 | } |
| 1065 | z = argv[2]; |
| 1066 | n = TH1_LEN(argl[2]); |
| 1067 | if( TH1_LEN(argl[1])<5 || argv[1][4]=='l' ){ |
| 1068 | while( n && th_isspace(z[0]) ){ z++; n--; } |
| 1069 | } |
| 1070 | if( TH1_LEN(argl[1])<5 || argv[1][4]=='r' ){ |
| 1071 | while( n && th_isspace(z[n-1]) ){ n--; } |
| 1072 | } |
| 1073 | TH1_XFER_TAINT(n, argl[2]); |
| 1074 | Th_SetResult(interp, z, n); |
| 1075 | return TH_OK; |
| 1076 | } |
| 1077 | |
| 1078 | /* |
| @@ -1051,11 +1086,11 @@ | |
| 1086 | int rc; |
| 1087 | |
| 1088 | if( argc!=3 ){ |
| 1089 | return Th_WrongNumArgs(interp, "info exists var"); |
| 1090 | } |
| 1091 | rc = Th_ExistsVar(interp, argv[2], TH1_LEN(argl[2])); |
| 1092 | Th_SetResultInt(interp, rc); |
| 1093 | return TH_OK; |
| 1094 | } |
| 1095 | |
| 1096 | /* |
| @@ -1117,11 +1152,11 @@ | |
| 1152 | int rc; |
| 1153 | |
| 1154 | if( argc!=3 ){ |
| 1155 | return Th_WrongNumArgs(interp, "array exists var"); |
| 1156 | } |
| 1157 | rc = Th_ExistsArrayVar(interp, argv[2], TH1_LEN(argl[2])); |
| 1158 | Th_SetResultInt(interp, rc); |
| 1159 | return TH_OK; |
| 1160 | } |
| 1161 | |
| 1162 | /* |
| @@ -1137,11 +1172,11 @@ | |
| 1172 | int nElem = 0; |
| 1173 | |
| 1174 | if( argc!=3 ){ |
| 1175 | return Th_WrongNumArgs(interp, "array names varname"); |
| 1176 | } |
| 1177 | rc = Th_ListAppendArray(interp, argv[2], TH1_LEN(argl[2]), &zElem, &nElem); |
| 1178 | if( rc!=TH_OK ){ |
| 1179 | return rc; |
| 1180 | } |
| 1181 | Th_SetResult(interp, zElem, nElem); |
| 1182 | if( zElem ) Th_Free(interp, zElem); |
| @@ -1161,11 +1196,11 @@ | |
| 1196 | int *argl |
| 1197 | ){ |
| 1198 | if( argc!=2 ){ |
| 1199 | return Th_WrongNumArgs(interp, "unset var"); |
| 1200 | } |
| 1201 | return Th_UnsetVar(interp, argv[1], TH1_LEN(argl[1])); |
| 1202 | } |
| 1203 | |
| 1204 | int Th_CallSubCommand( |
| 1205 | Th_Interp *interp, |
| 1206 | void *ctx, |
| @@ -1176,19 +1211,22 @@ | |
| 1211 | ){ |
| 1212 | if( argc>1 ){ |
| 1213 | int i; |
| 1214 | for(i=0; aSub[i].zName; i++){ |
| 1215 | const char *zName = aSub[i].zName; |
| 1216 | if( th_strlen(zName)==TH1_LEN(argl[1]) |
| 1217 | && 0==memcmp(zName, argv[1], TH1_LEN(argl[1])) ){ |
| 1218 | return aSub[i].xProc(interp, ctx, argc, argv, argl); |
| 1219 | } |
| 1220 | } |
| 1221 | } |
| 1222 | if(argc<2){ |
| 1223 | Th_ErrorMessage(interp, "Expected sub-command for", |
| 1224 | argv[0], TH1_LEN(argl[0])); |
| 1225 | }else{ |
| 1226 | Th_ErrorMessage(interp, "Expected sub-command, got:", |
| 1227 | argv[1], TH1_LEN(argl[1])); |
| 1228 | } |
| 1229 | return TH_ERROR; |
| 1230 | } |
| 1231 | |
| 1232 | /* |
| @@ -1319,11 +1357,11 @@ | |
| 1357 | int iFrame = -1; |
| 1358 | |
| 1359 | if( argc!=2 && argc!=3 ){ |
| 1360 | return Th_WrongNumArgs(interp, "uplevel ?level? script..."); |
| 1361 | } |
| 1362 | if( argc==3 && TH_OK!=thToFrame(interp, argv[1], TH1_LEN(argl[1]), &iFrame) ){ |
| 1363 | return TH_ERROR; |
| 1364 | } |
| 1365 | return Th_Eval(interp, iFrame, argv[argc-1], -1); |
| 1366 | } |
| 1367 | |
| @@ -1342,19 +1380,20 @@ | |
| 1380 | int iVar = 1; |
| 1381 | int iFrame = -1; |
| 1382 | int rc = TH_OK; |
| 1383 | int i; |
| 1384 | |
| 1385 | if( TH_OK==thToFrame(0, argv[1], TH1_LEN(argl[1]), &iFrame) ){ |
| 1386 | iVar++; |
| 1387 | } |
| 1388 | if( argc==iVar || (argc-iVar)%2 ){ |
| 1389 | return Th_WrongNumArgs(interp, |
| 1390 | "upvar frame othervar myvar ?othervar myvar...?"); |
| 1391 | } |
| 1392 | for(i=iVar; rc==TH_OK && i<argc; i=i+2){ |
| 1393 | rc = Th_LinkVar(interp, argv[i+1], TH1_LEN(argl[i+1]), |
| 1394 | iFrame, argv[i], TH1_LEN(argl[i])); |
| 1395 | } |
| 1396 | return rc; |
| 1397 | } |
| 1398 | |
| 1399 | /* |
| 1400 |
+207
-39
| --- src/th_main.c | ||
| +++ src/th_main.c | ||
| @@ -262,11 +262,11 @@ | ||
| 262 | 262 | ){ |
| 263 | 263 | char *zOut; |
| 264 | 264 | if( argc!=2 ){ |
| 265 | 265 | return Th_WrongNumArgs(interp, "httpize STRING"); |
| 266 | 266 | } |
| 267 | - zOut = httpize((char*)argv[1], argl[1]); | |
| 267 | + zOut = httpize((char*)argv[1], TH1_LEN(argl[1])); | |
| 268 | 268 | Th_SetResult(interp, zOut, -1); |
| 269 | 269 | free(zOut); |
| 270 | 270 | return TH_OK; |
| 271 | 271 | } |
| 272 | 272 | |
| @@ -291,11 +291,12 @@ | ||
| 291 | 291 | if( argc<2 || argc>3 ){ |
| 292 | 292 | return Th_WrongNumArgs(interp, "enable_output [LABEL] BOOLEAN"); |
| 293 | 293 | } |
| 294 | 294 | rc = Th_ToInt(interp, argv[argc-1], argl[argc-1], &enableOutput); |
| 295 | 295 | if( g.thTrace ){ |
| 296 | - Th_Trace("enable_output {%.*s} -> %d<br>\n", argl[1],argv[1],enableOutput); | |
| 296 | + Th_Trace("enable_output {%.*s} -> %d<br>\n", | |
| 297 | + TH1_LEN(argl[1]),argv[1],enableOutput); | |
| 297 | 298 | } |
| 298 | 299 | return rc; |
| 299 | 300 | } |
| 300 | 301 | |
| 301 | 302 | /* |
| @@ -322,11 +323,11 @@ | ||
| 322 | 323 | buul = (TH_INIT_NO_ENCODE & g.th1Flags) ? 0 : 1; |
| 323 | 324 | Th_SetResultInt(g.interp, buul); |
| 324 | 325 | if(argc>1){ |
| 325 | 326 | if( g.thTrace ){ |
| 326 | 327 | Th_Trace("enable_htmlify {%.*s} -> %d<br>\n", |
| 327 | - argl[1],argv[1],buul); | |
| 328 | + TH1_LEN(argl[1]),argv[1],buul); | |
| 328 | 329 | } |
| 329 | 330 | rc = Th_ToInt(interp, argv[argc-1], argl[argc-1], &buul); |
| 330 | 331 | if(!rc){ |
| 331 | 332 | if(buul){ |
| 332 | 333 | g.th1Flags &= ~TH_INIT_NO_ENCODE; |
| @@ -381,19 +382,23 @@ | ||
| 381 | 382 | ** g.th1Flags has the TH_INIT_NO_ENCODE flag. |
| 382 | 383 | ** |
| 383 | 384 | ** If pOut is NULL and the global pThOut is not then that blob |
| 384 | 385 | ** is used for output. |
| 385 | 386 | */ |
| 386 | -static void sendText(Blob * pOut, const char *z, int n, int encode){ | |
| 387 | +static void sendText(Blob *pOut, const char *z, int n, int encode){ | |
| 387 | 388 | if(0==pOut && pThOut!=0){ |
| 388 | 389 | pOut = pThOut; |
| 389 | 390 | } |
| 390 | 391 | if(TH_INIT_NO_ENCODE & g.th1Flags){ |
| 391 | 392 | encode = 0; |
| 392 | 393 | } |
| 393 | 394 | if( enableOutput && n ){ |
| 394 | - if( n<0 ) n = strlen(z); | |
| 395 | + if( n<0 ){ | |
| 396 | + n = strlen(z); | |
| 397 | + }else{ | |
| 398 | + n = TH1_LEN(n); | |
| 399 | + } | |
| 395 | 400 | if( encode ){ |
| 396 | 401 | z = htmlize(z, n); |
| 397 | 402 | n = strlen(z); |
| 398 | 403 | } |
| 399 | 404 | if(pOut!=0){ |
| @@ -525,14 +530,22 @@ | ||
| 525 | 530 | void *pConvert, |
| 526 | 531 | int argc, |
| 527 | 532 | const char **argv, |
| 528 | 533 | int *argl |
| 529 | 534 | ){ |
| 535 | + int encode = *(unsigned int*)pConvert; | |
| 536 | + int n; | |
| 530 | 537 | if( argc!=2 ){ |
| 531 | 538 | return Th_WrongNumArgs(interp, "puts STRING"); |
| 532 | 539 | } |
| 533 | - sendText(0,(char*)argv[1], argl[1], *(unsigned int*)pConvert); | |
| 540 | + n = argl[1]; | |
| 541 | + if( encode==0 && n>0 && TH1_TAINTED(n) ){ | |
| 542 | + if( Th_ReportTaint(interp, "output string", argv[1], n) ){ | |
| 543 | + return TH_ERROR; | |
| 544 | + } | |
| 545 | + } | |
| 546 | + sendText(0,(char*)argv[1], TH1_LEN(n), encode); | |
| 534 | 547 | return TH_OK; |
| 535 | 548 | } |
| 536 | 549 | |
| 537 | 550 | /* |
| 538 | 551 | ** TH1 command: redirect URL ?withMethod? |
| @@ -557,10 +570,15 @@ | ||
| 557 | 570 | } |
| 558 | 571 | if( argc==3 ){ |
| 559 | 572 | if( Th_ToInt(interp, argv[2], argl[2], &withMethod) ){ |
| 560 | 573 | return TH_ERROR; |
| 561 | 574 | } |
| 575 | + } | |
| 576 | + if( TH1_TAINTED(argl[1]) | |
| 577 | + && Th_ReportTaint(interp,"redirect URL",argv[1],argl[1]) | |
| 578 | + ){ | |
| 579 | + return TH_ERROR; | |
| 562 | 580 | } |
| 563 | 581 | if( withMethod ){ |
| 564 | 582 | cgi_redirect_with_method(argv[1]); |
| 565 | 583 | }else{ |
| 566 | 584 | cgi_redirect(argv[1]); |
| @@ -660,11 +678,11 @@ | ||
| 660 | 678 | int nValue = 0; |
| 661 | 679 | if( argc!=2 ){ |
| 662 | 680 | return Th_WrongNumArgs(interp, "markdown STRING"); |
| 663 | 681 | } |
| 664 | 682 | blob_zero(&src); |
| 665 | - blob_init(&src, (char*)argv[1], argl[1]); | |
| 683 | + blob_init(&src, (char*)argv[1], TH1_LEN(argl[1])); | |
| 666 | 684 | blob_zero(&title); blob_zero(&body); |
| 667 | 685 | markdown_to_html(&src, &title, &body); |
| 668 | 686 | Th_ListAppend(interp, &zValue, &nValue, blob_str(&title), blob_size(&title)); |
| 669 | 687 | Th_ListAppend(interp, &zValue, &nValue, blob_str(&body), blob_size(&body)); |
| 670 | 688 | Th_SetResult(interp, zValue, nValue); |
| @@ -690,11 +708,11 @@ | ||
| 690 | 708 | if( argc!=2 ){ |
| 691 | 709 | return Th_WrongNumArgs(interp, "wiki STRING"); |
| 692 | 710 | } |
| 693 | 711 | if( enableOutput ){ |
| 694 | 712 | Blob src; |
| 695 | - blob_init(&src, (char*)argv[1], argl[1]); | |
| 713 | + blob_init(&src, (char*)argv[1], TH1_LEN(argl[1])); | |
| 696 | 714 | wiki_convert(&src, 0, flags); |
| 697 | 715 | blob_reset(&src); |
| 698 | 716 | } |
| 699 | 717 | return TH_OK; |
| 700 | 718 | } |
| @@ -735,11 +753,11 @@ | ||
| 735 | 753 | ){ |
| 736 | 754 | char *zOut; |
| 737 | 755 | if( argc!=2 ){ |
| 738 | 756 | return Th_WrongNumArgs(interp, "htmlize STRING"); |
| 739 | 757 | } |
| 740 | - zOut = htmlize((char*)argv[1], argl[1]); | |
| 758 | + zOut = htmlize((char*)argv[1], TH1_LEN(argl[1])); | |
| 741 | 759 | Th_SetResult(interp, zOut, -1); |
| 742 | 760 | free(zOut); |
| 743 | 761 | return TH_OK; |
| 744 | 762 | } |
| 745 | 763 | |
| @@ -757,11 +775,11 @@ | ||
| 757 | 775 | ){ |
| 758 | 776 | char *zOut; |
| 759 | 777 | if( argc!=2 ){ |
| 760 | 778 | return Th_WrongNumArgs(interp, "encode64 STRING"); |
| 761 | 779 | } |
| 762 | - zOut = encode64((char*)argv[1], argl[1]); | |
| 780 | + zOut = encode64((char*)argv[1], TH1_LEN(argl[1])); | |
| 763 | 781 | Th_SetResult(interp, zOut, -1); |
| 764 | 782 | free(zOut); |
| 765 | 783 | return TH_OK; |
| 766 | 784 | } |
| 767 | 785 | |
| @@ -778,11 +796,11 @@ | ||
| 778 | 796 | int argc, |
| 779 | 797 | const char **argv, |
| 780 | 798 | int *argl |
| 781 | 799 | ){ |
| 782 | 800 | char *zOut; |
| 783 | - if( argc>=2 && argl[1]==6 && memcmp(argv[1],"-local",6)==0 ){ | |
| 801 | + if( argc>=2 && TH1_LEN(argl[1])==6 && memcmp(argv[1],"-local",6)==0 ){ | |
| 784 | 802 | zOut = db_text("??", "SELECT datetime('now',toLocal())"); |
| 785 | 803 | }else{ |
| 786 | 804 | zOut = db_text("??", "SELECT datetime('now')"); |
| 787 | 805 | } |
| 788 | 806 | Th_SetResult(interp, zOut, -1); |
| @@ -810,13 +828,13 @@ | ||
| 810 | 828 | if( argc<2 ){ |
| 811 | 829 | return Th_WrongNumArgs(interp, "hascap STRING ..."); |
| 812 | 830 | } |
| 813 | 831 | for(i=1; rc==1 && i<argc; i++){ |
| 814 | 832 | if( g.thTrace ){ |
| 815 | - Th_ListAppend(interp, &zCapList, &nCapList, argv[i], argl[i]); | |
| 833 | + Th_ListAppend(interp, &zCapList, &nCapList, argv[i], TH1_LEN(argl[i])); | |
| 816 | 834 | } |
| 817 | - rc = login_has_capability((char*)argv[i],argl[i],*(int*)p); | |
| 835 | + rc = login_has_capability((char*)argv[i],TH1_LEN(argl[i]),*(int*)p); | |
| 818 | 836 | } |
| 819 | 837 | if( g.thTrace ){ |
| 820 | 838 | Th_Trace("[%s %#h] => %d<br>\n", argv[0], nCapList, zCapList, rc); |
| 821 | 839 | Th_Free(interp, zCapList); |
| 822 | 840 | } |
| @@ -858,11 +876,11 @@ | ||
| 858 | 876 | int i; |
| 859 | 877 | |
| 860 | 878 | if( argc!=2 ){ |
| 861 | 879 | return Th_WrongNumArgs(interp, "capexpr EXPR"); |
| 862 | 880 | } |
| 863 | - rc = Th_SplitList(interp, argv[1], argl[1], &azCap, &anCap, &nCap); | |
| 881 | + rc = Th_SplitList(interp, argv[1], TH1_LEN(argl[1]), &azCap, &anCap, &nCap); | |
| 864 | 882 | if( rc ) return rc; |
| 865 | 883 | rc = 0; |
| 866 | 884 | for(i=0; i<nCap; i++){ |
| 867 | 885 | if( azCap[i][0]=='!' ){ |
| 868 | 886 | rc = !login_has_capability(azCap[i]+1, anCap[i]-1, 0); |
| @@ -921,11 +939,12 @@ | ||
| 921 | 939 | if( argc<2 ){ |
| 922 | 940 | return Th_WrongNumArgs(interp, "hascap STRING ..."); |
| 923 | 941 | } |
| 924 | 942 | for(i=1; i<argc && rc; i++){ |
| 925 | 943 | int match = 0; |
| 926 | - for(j=0; j<argl[i]; j++){ | |
| 944 | + int nn = TH1_LEN(argl[i]); | |
| 945 | + for(j=0; j<nn; j++){ | |
| 927 | 946 | switch( argv[i][j] ){ |
| 928 | 947 | case 'c': match |= searchCap & SRCH_CKIN; break; |
| 929 | 948 | case 'd': match |= searchCap & SRCH_DOC; break; |
| 930 | 949 | case 't': match |= searchCap & SRCH_TKT; break; |
| 931 | 950 | case 'w': match |= searchCap & SRCH_WIKI; break; |
| @@ -932,11 +951,11 @@ | ||
| 932 | 951 | } |
| 933 | 952 | } |
| 934 | 953 | if( !match ) rc = 0; |
| 935 | 954 | } |
| 936 | 955 | if( g.thTrace ){ |
| 937 | - Th_Trace("[searchable %#h] => %d<br>\n", argl[1], argv[1], rc); | |
| 956 | + Th_Trace("[searchable %#h] => %d<br>\n", TH1_LEN(argl[1]), argv[1], rc); | |
| 938 | 957 | } |
| 939 | 958 | Th_SetResultInt(interp, rc); |
| 940 | 959 | return TH_OK; |
| 941 | 960 | } |
| 942 | 961 | |
| @@ -1051,11 +1070,11 @@ | ||
| 1051 | 1070 | #endif |
| 1052 | 1071 | else if( 0 == fossil_strnicmp( zArg, "markdown\0", 9 ) ){ |
| 1053 | 1072 | rc = 1; |
| 1054 | 1073 | } |
| 1055 | 1074 | if( g.thTrace ){ |
| 1056 | - Th_Trace("[hasfeature %#h] => %d<br>\n", argl[1], zArg, rc); | |
| 1075 | + Th_Trace("[hasfeature %#h] => %d<br>\n", TH1_LEN(argl[1]), zArg, rc); | |
| 1057 | 1076 | } |
| 1058 | 1077 | Th_SetResultInt(interp, rc); |
| 1059 | 1078 | return TH_OK; |
| 1060 | 1079 | } |
| 1061 | 1080 | |
| @@ -1104,18 +1123,20 @@ | ||
| 1104 | 1123 | const char **argv, |
| 1105 | 1124 | int *argl |
| 1106 | 1125 | ){ |
| 1107 | 1126 | int rc = 0; |
| 1108 | 1127 | int i; |
| 1128 | + int nn; | |
| 1109 | 1129 | if( argc!=2 ){ |
| 1110 | 1130 | return Th_WrongNumArgs(interp, "anycap STRING"); |
| 1111 | 1131 | } |
| 1112 | - for(i=0; rc==0 && i<argl[1]; i++){ | |
| 1132 | + nn = TH1_LEN(argl[1]); | |
| 1133 | + for(i=0; rc==0 && i<nn; i++){ | |
| 1113 | 1134 | rc = login_has_capability((char*)&argv[1][i],1,0); |
| 1114 | 1135 | } |
| 1115 | 1136 | if( g.thTrace ){ |
| 1116 | - Th_Trace("[anycap %#h] => %d<br>\n", argl[1], argv[1], rc); | |
| 1137 | + Th_Trace("[anycap %#h] => %d<br>\n", TH1_LEN(argl[1]), argv[1], rc); | |
| 1117 | 1138 | } |
| 1118 | 1139 | Th_SetResultInt(interp, rc); |
| 1119 | 1140 | return TH_OK; |
| 1120 | 1141 | } |
| 1121 | 1142 | |
| @@ -1140,22 +1161,23 @@ | ||
| 1140 | 1161 | return Th_WrongNumArgs(interp, "combobox NAME TEXT-LIST NUMLINES"); |
| 1141 | 1162 | } |
| 1142 | 1163 | if( enableOutput ){ |
| 1143 | 1164 | int height; |
| 1144 | 1165 | Blob name; |
| 1145 | - int nValue; | |
| 1166 | + int nValue = 0; | |
| 1146 | 1167 | const char *zValue; |
| 1147 | 1168 | char *z, *zH; |
| 1148 | 1169 | int nElem; |
| 1149 | 1170 | int *aszElem; |
| 1150 | 1171 | char **azElem; |
| 1151 | 1172 | int i; |
| 1152 | 1173 | |
| 1153 | 1174 | if( Th_ToInt(interp, argv[3], argl[3], &height) ) return TH_ERROR; |
| 1154 | - Th_SplitList(interp, argv[2], argl[2], &azElem, &aszElem, &nElem); | |
| 1155 | - blob_init(&name, (char*)argv[1], argl[1]); | |
| 1175 | + Th_SplitList(interp, argv[2], TH1_LEN(argl[2]), &azElem, &aszElem, &nElem); | |
| 1176 | + blob_init(&name, (char*)argv[1], TH1_LEN(argl[1])); | |
| 1156 | 1177 | zValue = Th_Fetch(blob_str(&name), &nValue); |
| 1178 | + nValue = TH1_LEN(nValue); | |
| 1157 | 1179 | zH = htmlize(blob_buffer(&name), blob_size(&name)); |
| 1158 | 1180 | z = mprintf("<select id=\"%s\" name=\"%s\" size=\"%d\">", zH, zH, height); |
| 1159 | 1181 | free(zH); |
| 1160 | 1182 | sendText(0,z, -1, 0); |
| 1161 | 1183 | free(z); |
| @@ -1247,11 +1269,11 @@ | ||
| 1247 | 1269 | return Th_WrongNumArgs(interp, "linecount STRING MAX MIN"); |
| 1248 | 1270 | } |
| 1249 | 1271 | if( Th_ToInt(interp, argv[2], argl[2], &iMax) ) return TH_ERROR; |
| 1250 | 1272 | if( Th_ToInt(interp, argv[3], argl[3], &iMin) ) return TH_ERROR; |
| 1251 | 1273 | z = argv[1]; |
| 1252 | - size = argl[1]; | |
| 1274 | + size = TH1_LEN(argl[1]); | |
| 1253 | 1275 | for(n=1, i=0; i<size; i++){ |
| 1254 | 1276 | if( z[i]=='\n' ){ |
| 1255 | 1277 | n++; |
| 1256 | 1278 | if( n>=iMax ) break; |
| 1257 | 1279 | } |
| @@ -1407,11 +1429,12 @@ | ||
| 1407 | 1429 | return TH_OK; |
| 1408 | 1430 | }else if( fossil_strnicmp(argv[1], "vfs\0", 4)==0 ){ |
| 1409 | 1431 | Th_SetResult(interp, g.zVfsName ? g.zVfsName : zDefault, -1); |
| 1410 | 1432 | return TH_OK; |
| 1411 | 1433 | }else{ |
| 1412 | - Th_ErrorMessage(interp, "unsupported global state:", argv[1], argl[1]); | |
| 1434 | + Th_ErrorMessage(interp, "unsupported global state:", | |
| 1435 | + argv[1], TH1_LEN(argl[1])); | |
| 1413 | 1436 | return TH_ERROR; |
| 1414 | 1437 | } |
| 1415 | 1438 | } |
| 1416 | 1439 | |
| 1417 | 1440 | /* |
| @@ -1426,17 +1449,21 @@ | ||
| 1426 | 1449 | int argc, |
| 1427 | 1450 | const char **argv, |
| 1428 | 1451 | int *argl |
| 1429 | 1452 | ){ |
| 1430 | 1453 | const char *zDefault = 0; |
| 1454 | + const char *zVal; | |
| 1455 | + int sz; | |
| 1431 | 1456 | if( argc!=2 && argc!=3 ){ |
| 1432 | 1457 | return Th_WrongNumArgs(interp, "getParameter NAME ?DEFAULT?"); |
| 1433 | 1458 | } |
| 1434 | 1459 | if( argc==3 ){ |
| 1435 | 1460 | zDefault = argv[2]; |
| 1436 | 1461 | } |
| 1437 | - Th_SetResult(interp, cgi_parameter(argv[1], zDefault), -1); | |
| 1462 | + zVal = cgi_parameter(argv[1], zDefault); | |
| 1463 | + sz = th_strlen(zVal); | |
| 1464 | + Th_SetResult(interp, zVal, TH1_ADD_TAINT(sz)); | |
| 1438 | 1465 | return TH_OK; |
| 1439 | 1466 | } |
| 1440 | 1467 | |
| 1441 | 1468 | /* |
| 1442 | 1469 | ** TH1 command: setParameter NAME VALUE |
| @@ -1848,10 +1875,47 @@ | ||
| 1848 | 1875 | sqlite3_snprintf(sizeof(zUTime), zUTime, "%llu", x); |
| 1849 | 1876 | Th_SetResult(interp, zUTime, -1); |
| 1850 | 1877 | return TH_OK; |
| 1851 | 1878 | } |
| 1852 | 1879 | |
| 1880 | +/* | |
| 1881 | +** TH1 command: taint STRING | |
| 1882 | +** | |
| 1883 | +** Return a copy of STRING that is marked as tainted. | |
| 1884 | +*/ | |
| 1885 | +static int taintCmd( | |
| 1886 | + Th_Interp *interp, | |
| 1887 | + void *p, | |
| 1888 | + int argc, | |
| 1889 | + const char **argv, | |
| 1890 | + int *argl | |
| 1891 | +){ | |
| 1892 | + if( argc!=2 ){ | |
| 1893 | + return Th_WrongNumArgs(interp, "STRING"); | |
| 1894 | + } | |
| 1895 | + Th_SetResult(interp, argv[1], TH1_ADD_TAINT(argl[1])); | |
| 1896 | + return TH_OK; | |
| 1897 | +} | |
| 1898 | + | |
| 1899 | +/* | |
| 1900 | +** TH1 command: untaint STRING | |
| 1901 | +** | |
| 1902 | +** Return a copy of STRING that is marked as untainted. | |
| 1903 | +*/ | |
| 1904 | +static int untaintCmd( | |
| 1905 | + Th_Interp *interp, | |
| 1906 | + void *p, | |
| 1907 | + int argc, | |
| 1908 | + const char **argv, | |
| 1909 | + int *argl | |
| 1910 | +){ | |
| 1911 | + if( argc!=2 ){ | |
| 1912 | + return Th_WrongNumArgs(interp, "STRING"); | |
| 1913 | + } | |
| 1914 | + Th_SetResult(interp, argv[1], TH1_LEN(argl[1])); | |
| 1915 | + return TH_OK; | |
| 1916 | +} | |
| 1853 | 1917 | |
| 1854 | 1918 | /* |
| 1855 | 1919 | ** TH1 command: randhex N |
| 1856 | 1920 | ** |
| 1857 | 1921 | ** Return N*2 random hexadecimal digits with N<50. If N is omitted, |
| @@ -1923,11 +1987,13 @@ | ||
| 1923 | 1987 | int res = TH_OK; |
| 1924 | 1988 | int nVar; |
| 1925 | 1989 | char *zErr = 0; |
| 1926 | 1990 | int noComplain = 0; |
| 1927 | 1991 | |
| 1928 | - if( argc>3 && argl[1]==11 && strncmp(argv[1], "-nocomplain", 11)==0 ){ | |
| 1992 | + if( argc>3 && TH1_LEN(argl[1])==11 | |
| 1993 | + && strncmp(argv[1], "-nocomplain", 11)==0 | |
| 1994 | + ){ | |
| 1929 | 1995 | argc--; |
| 1930 | 1996 | argv++; |
| 1931 | 1997 | argl++; |
| 1932 | 1998 | noComplain = 1; |
| 1933 | 1999 | } |
| @@ -1939,15 +2005,22 @@ | ||
| 1939 | 2005 | Th_ErrorMessage(interp, "database is not open", 0, 0); |
| 1940 | 2006 | return TH_ERROR; |
| 1941 | 2007 | } |
| 1942 | 2008 | zSql = argv[1]; |
| 1943 | 2009 | nSql = argl[1]; |
| 2010 | + if( TH1_TAINTED(nSql) ){ | |
| 2011 | + if( Th_ReportTaint(interp,"query SQL",zSql,nSql) ){ | |
| 2012 | + return TH_ERROR; | |
| 2013 | + } | |
| 2014 | + nSql = TH1_LEN(nSql); | |
| 2015 | + } | |
| 2016 | + | |
| 1944 | 2017 | while( res==TH_OK && nSql>0 ){ |
| 1945 | 2018 | zErr = 0; |
| 1946 | 2019 | report_restrict_sql(&zErr); |
| 1947 | 2020 | g.dbIgnoreErrors++; |
| 1948 | - rc = sqlite3_prepare_v2(g.db, argv[1], argl[1], &pStmt, &zTail); | |
| 2021 | + rc = sqlite3_prepare_v2(g.db, argv[1], TH1_LEN(argl[1]), &pStmt, &zTail); | |
| 1949 | 2022 | g.dbIgnoreErrors--; |
| 1950 | 2023 | report_unrestrict_sql(); |
| 1951 | 2024 | if( rc!=0 || zErr!=0 ){ |
| 1952 | 2025 | if( noComplain ) return TH_OK; |
| 1953 | 2026 | Th_ErrorMessage(interp, "SQL error: ", |
| @@ -1964,31 +2037,31 @@ | ||
| 1964 | 2037 | int szVar = zVar ? th_strlen(zVar) : 0; |
| 1965 | 2038 | if( szVar>1 && zVar[0]=='$' |
| 1966 | 2039 | && Th_GetVar(interp, zVar+1, szVar-1)==TH_OK ){ |
| 1967 | 2040 | int nVal; |
| 1968 | 2041 | const char *zVal = Th_GetResult(interp, &nVal); |
| 1969 | - sqlite3_bind_text(pStmt, i, zVal, nVal, SQLITE_TRANSIENT); | |
| 2042 | + sqlite3_bind_text(pStmt, i, zVal, TH1_LEN(nVal), SQLITE_TRANSIENT); | |
| 1970 | 2043 | } |
| 1971 | 2044 | } |
| 1972 | 2045 | while( res==TH_OK && ignore_errors_step(pStmt)==SQLITE_ROW ){ |
| 1973 | 2046 | int nCol = sqlite3_column_count(pStmt); |
| 1974 | 2047 | for(i=0; i<nCol; i++){ |
| 1975 | 2048 | const char *zCol = sqlite3_column_name(pStmt, i); |
| 1976 | 2049 | int szCol = th_strlen(zCol); |
| 1977 | 2050 | const char *zVal = (const char*)sqlite3_column_text(pStmt, i); |
| 1978 | 2051 | int szVal = sqlite3_column_bytes(pStmt, i); |
| 1979 | - Th_SetVar(interp, zCol, szCol, zVal, szVal); | |
| 2052 | + Th_SetVar(interp, zCol, szCol, zVal, TH1_ADD_TAINT(szVal)); | |
| 1980 | 2053 | } |
| 1981 | 2054 | if( g.thTrace ){ |
| 1982 | - Th_Trace("query_eval {<pre>%#h</pre>}<br>\n", argl[2], argv[2]); | |
| 2055 | + Th_Trace("query_eval {<pre>%#h</pre>}<br>\n",TH1_LEN(argl[2]),argv[2]); | |
| 1983 | 2056 | } |
| 1984 | - res = Th_Eval(interp, 0, argv[2], argl[2]); | |
| 2057 | + res = Th_Eval(interp, 0, argv[2], TH1_LEN(argl[2])); | |
| 1985 | 2058 | if( g.thTrace ){ |
| 1986 | 2059 | int nTrRes; |
| 1987 | 2060 | char *zTrRes = (char*)Th_GetResult(g.interp, &nTrRes); |
| 1988 | 2061 | Th_Trace("[query_eval] => %h {%#h}<br>\n", |
| 1989 | - Th_ReturnCodeName(res, 0), nTrRes, zTrRes); | |
| 2062 | + Th_ReturnCodeName(res, 0), TH1_LEN(nTrRes), zTrRes); | |
| 1990 | 2063 | } |
| 1991 | 2064 | if( res==TH_BREAK || res==TH_CONTINUE ) res = TH_OK; |
| 1992 | 2065 | } |
| 1993 | 2066 | rc = sqlite3_finalize(pStmt); |
| 1994 | 2067 | if( rc!=SQLITE_OK ){ |
| @@ -2038,11 +2111,11 @@ | ||
| 2038 | 2111 | Th_SetResult(interp, 0, 0); |
| 2039 | 2112 | rc = TH_OK; |
| 2040 | 2113 | } |
| 2041 | 2114 | if( g.thTrace ){ |
| 2042 | 2115 | Th_Trace("[setting %s%#h] => %d<br>\n", strict ? "strict " : "", |
| 2043 | - argl[nArg], argv[nArg], rc); | |
| 2116 | + TH1_LEN(argl[nArg]), argv[nArg], rc); | |
| 2044 | 2117 | } |
| 2045 | 2118 | return rc; |
| 2046 | 2119 | } |
| 2047 | 2120 | |
| 2048 | 2121 | /* |
| @@ -2121,11 +2194,11 @@ | ||
| 2121 | 2194 | return Th_WrongNumArgs(interp, REGEXP_WRONGNUMARGS); |
| 2122 | 2195 | } |
| 2123 | 2196 | zErr = re_compile(&pRe, argv[nArg], noCase); |
| 2124 | 2197 | if( !zErr ){ |
| 2125 | 2198 | Th_SetResultInt(interp, re_match(pRe, |
| 2126 | - (const unsigned char *)argv[nArg+1], argl[nArg+1])); | |
| 2199 | + (const unsigned char *)argv[nArg+1], TH1_LEN(argl[nArg+1]))); | |
| 2127 | 2200 | rc = TH_OK; |
| 2128 | 2201 | }else{ |
| 2129 | 2202 | Th_SetResult(interp, zErr, -1); |
| 2130 | 2203 | rc = TH_ERROR; |
| 2131 | 2204 | } |
| @@ -2160,11 +2233,11 @@ | ||
| 2160 | 2233 | UrlData urlData; |
| 2161 | 2234 | |
| 2162 | 2235 | if( argc<2 || argc>5 ){ |
| 2163 | 2236 | return Th_WrongNumArgs(interp, HTTP_WRONGNUMARGS); |
| 2164 | 2237 | } |
| 2165 | - if( fossil_strnicmp(argv[nArg], "-asynchronous", argl[nArg])==0 ){ | |
| 2238 | + if( fossil_strnicmp(argv[nArg], "-asynchronous", TH1_LEN(argl[nArg]))==0 ){ | |
| 2166 | 2239 | fAsynchronous = 1; nArg++; |
| 2167 | 2240 | } |
| 2168 | 2241 | if( fossil_strcmp(argv[nArg], "--")==0 ) nArg++; |
| 2169 | 2242 | if( nArg+1!=argc && nArg+2!=argc ){ |
| 2170 | 2243 | return Th_WrongNumArgs(interp, REGEXP_WRONGNUMARGS); |
| @@ -2189,11 +2262,11 @@ | ||
| 2189 | 2262 | return TH_ERROR; |
| 2190 | 2263 | } |
| 2191 | 2264 | re_free(pRe); |
| 2192 | 2265 | blob_zero(&payload); |
| 2193 | 2266 | if( nArg+2==argc ){ |
| 2194 | - blob_append(&payload, argv[nArg+1], argl[nArg+1]); | |
| 2267 | + blob_append(&payload, argv[nArg+1], TH1_LEN(argl[nArg+1])); | |
| 2195 | 2268 | zType = "POST"; |
| 2196 | 2269 | }else{ |
| 2197 | 2270 | zType = "GET"; |
| 2198 | 2271 | } |
| 2199 | 2272 | if( fAsynchronous ){ |
| @@ -2268,11 +2341,11 @@ | ||
| 2268 | 2341 | if( argc!=2 ){ |
| 2269 | 2342 | return Th_WrongNumArgs(interp, "captureTh1 STRING"); |
| 2270 | 2343 | } |
| 2271 | 2344 | pOrig = Th_SetOutputBlob(&out); |
| 2272 | 2345 | zStr = argv[1]; |
| 2273 | - nStr = argl[1]; | |
| 2346 | + nStr = TH1_LEN(argl[1]); | |
| 2274 | 2347 | rc = Th_Eval(g.interp, 0, zStr, nStr); |
| 2275 | 2348 | Th_SetOutputBlob(pOrig); |
| 2276 | 2349 | if(0==rc){ |
| 2277 | 2350 | Th_SetResult(g.interp, blob_str(&out), blob_size(&out)); |
| 2278 | 2351 | } |
| @@ -2387,13 +2460,15 @@ | ||
| 2387 | 2460 | {"setting", settingCmd, 0}, |
| 2388 | 2461 | {"styleFooter", styleFooterCmd, 0}, |
| 2389 | 2462 | {"styleHeader", styleHeaderCmd, 0}, |
| 2390 | 2463 | {"styleScript", styleScriptCmd, 0}, |
| 2391 | 2464 | {"submenu", submenuCmd, 0}, |
| 2465 | + {"taint", taintCmd, 0}, | |
| 2392 | 2466 | {"tclReady", tclReadyCmd, 0}, |
| 2393 | 2467 | {"trace", traceCmd, 0}, |
| 2394 | 2468 | {"stime", stimeCmd, 0}, |
| 2469 | + {"untaint", untaintCmd, 0}, | |
| 2395 | 2470 | {"unversioned", unversionedCmd, 0}, |
| 2396 | 2471 | {"utime", utimeCmd, 0}, |
| 2397 | 2472 | {"verifyCsrf", verifyCsrfCmd, 0}, |
| 2398 | 2473 | {"verifyLogin", verifyLoginCmd, 0}, |
| 2399 | 2474 | {"wiki", wikiCmd, (void*)&aFlags[0]}, |
| @@ -2494,10 +2569,26 @@ | ||
| 2494 | 2569 | Th_Trace("set %h {%h}<br>\n", zName, zValue); |
| 2495 | 2570 | } |
| 2496 | 2571 | Th_SetVar(g.interp, zName, -1, zValue, strlen(zValue)); |
| 2497 | 2572 | } |
| 2498 | 2573 | } |
| 2574 | + | |
| 2575 | +/* | |
| 2576 | +** Store a string value in a variable in the interpreter | |
| 2577 | +** with the "taint" marking, so that TH1 knows that this | |
| 2578 | +** variable contains content under the control of the remote | |
| 2579 | +** user and presents a risk of XSS or SQL-injection attacks. | |
| 2580 | +*/ | |
| 2581 | +void Th_StoreUnsafe(const char *zName, const char *zValue){ | |
| 2582 | + Th_FossilInit(TH_INIT_DEFAULT); | |
| 2583 | + if( zValue ){ | |
| 2584 | + if( g.thTrace ){ | |
| 2585 | + Th_Trace("set %h [taint {%h}]<br>\n", zName, zValue); | |
| 2586 | + } | |
| 2587 | + Th_SetVar(g.interp, zName, -1, zValue, TH1_ADD_TAINT(strlen(zValue))); | |
| 2588 | + } | |
| 2589 | +} | |
| 2499 | 2590 | |
| 2500 | 2591 | /* |
| 2501 | 2592 | ** Appends an element to a TH1 list value. This function is called by the |
| 2502 | 2593 | ** transfer subsystem; therefore, it must be very careful to avoid doing |
| 2503 | 2594 | ** any unnecessary work. To that end, the TH1 subsystem will not be called |
| @@ -2680,10 +2771,11 @@ | ||
| 2680 | 2771 | char *zResult = (char*)Th_GetResult(g.interp, &nResult); |
| 2681 | 2772 | /* |
| 2682 | 2773 | ** Make sure that the TH1 script error was not caused by a "missing" |
| 2683 | 2774 | ** command hook handler as that is not actually an error condition. |
| 2684 | 2775 | */ |
| 2776 | + nResult = TH1_LEN(nResult); | |
| 2685 | 2777 | if( memcmp(zResult, NO_COMMAND_HOOK_ERROR, nResult)!=0 ){ |
| 2686 | 2778 | sendError(0,zResult, nResult, 0); |
| 2687 | 2779 | }else{ |
| 2688 | 2780 | /* |
| 2689 | 2781 | ** There is no command hook handler "installed". This situation |
| @@ -2767,10 +2859,11 @@ | ||
| 2767 | 2859 | char *zResult = (char*)Th_GetResult(g.interp, &nResult); |
| 2768 | 2860 | /* |
| 2769 | 2861 | ** Make sure that the TH1 script error was not caused by a "missing" |
| 2770 | 2862 | ** webpage hook handler as that is not actually an error condition. |
| 2771 | 2863 | */ |
| 2864 | + nResult = TH1_LEN(nResult); | |
| 2772 | 2865 | if( memcmp(zResult, NO_WEBPAGE_HOOK_ERROR, nResult)!=0 ){ |
| 2773 | 2866 | sendError(0,zResult, nResult, 1); |
| 2774 | 2867 | }else{ |
| 2775 | 2868 | /* |
| 2776 | 2869 | ** There is no webpage hook handler "installed". This situation |
| @@ -2894,11 +2987,16 @@ | ||
| 2894 | 2987 | } |
| 2895 | 2988 | rc = Th_GetVar(g.interp, (char*)zVar, nVar); |
| 2896 | 2989 | z += i+1+n; |
| 2897 | 2990 | i = 0; |
| 2898 | 2991 | zResult = (char*)Th_GetResult(g.interp, &n); |
| 2899 | - sendText(pOut,(char*)zResult, n, encode); | |
| 2992 | + if( !TH1_TAINTED(n) | |
| 2993 | + || encode | |
| 2994 | + || Th_ReportTaint(g.interp, "inline variable", zVar, nVar)==TH_OK | |
| 2995 | + ){ | |
| 2996 | + sendText(pOut,(char*)zResult, n, encode); | |
| 2997 | + } | |
| 2900 | 2998 | }else if( z[i]=='<' && isBeginScriptTag(&z[i]) ){ |
| 2901 | 2999 | sendText(pOut,z, i, 0); |
| 2902 | 3000 | z += i+5; |
| 2903 | 3001 | for(i=0; z[i] && (z[i]!='<' || !isEndScriptTag(&z[i])); i++){} |
| 2904 | 3002 | if( g.thTrace ){ |
| @@ -2907,11 +3005,11 @@ | ||
| 2907 | 3005 | rc = Th_Eval(g.interp, 0, (const char*)z, i); |
| 2908 | 3006 | if( g.thTrace ){ |
| 2909 | 3007 | int nTrRes; |
| 2910 | 3008 | char *zTrRes = (char*)Th_GetResult(g.interp, &nTrRes); |
| 2911 | 3009 | Th_Trace("[render_eval] => %h {%#h}<br>\n", |
| 2912 | - Th_ReturnCodeName(rc, 0), nTrRes, zTrRes); | |
| 3010 | + Th_ReturnCodeName(rc, 0), TH1_LEN(nTrRes), zTrRes); | |
| 2913 | 3011 | } |
| 2914 | 3012 | if( rc!=TH_OK ) break; |
| 2915 | 3013 | z += i; |
| 2916 | 3014 | if( z[0] ){ z += 6; } |
| 2917 | 3015 | i = 0; |
| @@ -2953,10 +3051,78 @@ | ||
| 2953 | 3051 | ** as appropriate. We need to pass on g.th1Flags for the case of |
| 2954 | 3052 | ** recursive calls, so that, e.g., TH_INIT_NO_ENCODE does not get |
| 2955 | 3053 | ** inadvertently toggled off by a recursive call. |
| 2956 | 3054 | */; |
| 2957 | 3055 | } |
| 3056 | + | |
| 3057 | +/* | |
| 3058 | +** SETTING: vuln-report width=8 default=log | |
| 3059 | +** | |
| 3060 | +** This setting controls Fossil's behavior when it encounters a potential | |
| 3061 | +** XSS or SQL-injection vulnerability due to misuse of TH1 configuration | |
| 3062 | +** scripts. Choices are: | |
| 3063 | +** | |
| 3064 | +** off Do nothing. Ignore the vulnerability. | |
| 3065 | +** | |
| 3066 | +** log Write a report of the problem into the error log. | |
| 3067 | +** | |
| 3068 | +** block Like "log" but also prevent the offending TH1 command | |
| 3069 | +** from running. | |
| 3070 | +** | |
| 3071 | +** fatal Render an error message page instead of the requested | |
| 3072 | +** page. | |
| 3073 | +*/ | |
| 3074 | + | |
| 3075 | +/* | |
| 3076 | +** Report misuse of a tainted string in TH1. | |
| 3077 | +** | |
| 3078 | +** The behavior depends on the vuln-report setting. If "off", this routine | |
| 3079 | +** is a no-op. Otherwise, right a message into the error log. If | |
| 3080 | +** vuln-report is "log", that is all that happens. But for any other | |
| 3081 | +** value of vuln-report, a fatal error is raised. | |
| 3082 | +*/ | |
| 3083 | +int Th_ReportTaint( | |
| 3084 | + Th_Interp *interp, /* Report error here, if an error is reported */ | |
| 3085 | + const char *zWhere, /* Where the tainted string appears */ | |
| 3086 | + const char *zStr, /* The tainted string */ | |
| 3087 | + int nStr /* Length of the tainted string */ | |
| 3088 | +){ | |
| 3089 | + static const char *zDisp = 0; /* Dispensation; what to do with the error */ | |
| 3090 | + const char *zVulnType; /* Type of vulnerability */ | |
| 3091 | + | |
| 3092 | + if( zDisp==0 ) zDisp = db_get("vuln-report","log"); | |
| 3093 | + if( is_false(zDisp) ) return 0; | |
| 3094 | + if( strstr(zWhere,"SQL")!=0 ){ | |
| 3095 | + zVulnType = "SQL-injection"; | |
| 3096 | + }else{ | |
| 3097 | + zVulnType = "XSS"; | |
| 3098 | + } | |
| 3099 | + nStr = TH1_LEN(nStr); | |
| 3100 | + fossil_errorlog("possible TH1 %s vulnerability due to tainted %s: \"%.*s\"", | |
| 3101 | + zVulnType, zWhere, nStr, zStr); | |
| 3102 | + if( strcmp(zDisp,"log")==0 ){ | |
| 3103 | + return 0; | |
| 3104 | + } | |
| 3105 | + if( strcmp(zDisp,"block")==0 ){ | |
| 3106 | + char *z = mprintf("tainted %s: \"", zWhere); | |
| 3107 | + Th_ErrorMessage(interp, z, zStr, nStr); | |
| 3108 | + fossil_free(z); | |
| 3109 | + }else{ | |
| 3110 | + char *z = mprintf("%#h", nStr, zStr); | |
| 3111 | + zDisp = "off"; | |
| 3112 | + cgi_reset_content(); | |
| 3113 | + style_submenu_enable(0); | |
| 3114 | + style_set_current_feature("error"); | |
| 3115 | + style_header("Configuration Error"); | |
| 3116 | + @ <p>Error in a TH1 configuration script: | |
| 3117 | + @ tainted %h(zWhere): "%z(z)" | |
| 3118 | + style_finish_page(); | |
| 3119 | + cgi_reply(); | |
| 3120 | + fossil_exit(1); | |
| 3121 | + } | |
| 3122 | + return 1; | |
| 3123 | +} | |
| 2958 | 3124 | |
| 2959 | 3125 | /* |
| 2960 | 3126 | ** COMMAND: test-th-render |
| 2961 | 3127 | ** |
| 2962 | 3128 | ** Usage: %fossil test-th-render FILE |
| @@ -2992,10 +3158,11 @@ | ||
| 2992 | 3158 | if( find_option("set-user-caps", 0, 0)!=0 ){ |
| 2993 | 3159 | const char *zCap = fossil_getenv("TH1_TEST_USER_CAPS"); |
| 2994 | 3160 | login_set_capabilities(zCap ? zCap : "sx", 0); |
| 2995 | 3161 | g.useLocalauth = 1; |
| 2996 | 3162 | } |
| 3163 | + db_find_and_open_repository(OPEN_OK_NOT_FOUND|OPEN_SUBSTITUTE,0); | |
| 2997 | 3164 | verify_all_options(); |
| 2998 | 3165 | if( g.argc<3 ){ |
| 2999 | 3166 | usage("FILE"); |
| 3000 | 3167 | } |
| 3001 | 3168 | blob_zero(&in); |
| @@ -3044,10 +3211,11 @@ | ||
| 3044 | 3211 | if( find_option("set-user-caps", 0, 0)!=0 ){ |
| 3045 | 3212 | const char *zCap = fossil_getenv("TH1_TEST_USER_CAPS"); |
| 3046 | 3213 | login_set_capabilities(zCap ? zCap : "sx", 0); |
| 3047 | 3214 | g.useLocalauth = 1; |
| 3048 | 3215 | } |
| 3216 | + db_find_and_open_repository(OPEN_OK_NOT_FOUND|OPEN_SUBSTITUTE,0); | |
| 3049 | 3217 | verify_all_options(); |
| 3050 | 3218 | if( g.argc!=3 ){ |
| 3051 | 3219 | usage("script"); |
| 3052 | 3220 | } |
| 3053 | 3221 | if(file_isfile(g.argv[2], ExtFILE)){ |
| 3054 | 3222 |
| --- src/th_main.c | |
| +++ src/th_main.c | |
| @@ -262,11 +262,11 @@ | |
| 262 | ){ |
| 263 | char *zOut; |
| 264 | if( argc!=2 ){ |
| 265 | return Th_WrongNumArgs(interp, "httpize STRING"); |
| 266 | } |
| 267 | zOut = httpize((char*)argv[1], argl[1]); |
| 268 | Th_SetResult(interp, zOut, -1); |
| 269 | free(zOut); |
| 270 | return TH_OK; |
| 271 | } |
| 272 | |
| @@ -291,11 +291,12 @@ | |
| 291 | if( argc<2 || argc>3 ){ |
| 292 | return Th_WrongNumArgs(interp, "enable_output [LABEL] BOOLEAN"); |
| 293 | } |
| 294 | rc = Th_ToInt(interp, argv[argc-1], argl[argc-1], &enableOutput); |
| 295 | if( g.thTrace ){ |
| 296 | Th_Trace("enable_output {%.*s} -> %d<br>\n", argl[1],argv[1],enableOutput); |
| 297 | } |
| 298 | return rc; |
| 299 | } |
| 300 | |
| 301 | /* |
| @@ -322,11 +323,11 @@ | |
| 322 | buul = (TH_INIT_NO_ENCODE & g.th1Flags) ? 0 : 1; |
| 323 | Th_SetResultInt(g.interp, buul); |
| 324 | if(argc>1){ |
| 325 | if( g.thTrace ){ |
| 326 | Th_Trace("enable_htmlify {%.*s} -> %d<br>\n", |
| 327 | argl[1],argv[1],buul); |
| 328 | } |
| 329 | rc = Th_ToInt(interp, argv[argc-1], argl[argc-1], &buul); |
| 330 | if(!rc){ |
| 331 | if(buul){ |
| 332 | g.th1Flags &= ~TH_INIT_NO_ENCODE; |
| @@ -381,19 +382,23 @@ | |
| 381 | ** g.th1Flags has the TH_INIT_NO_ENCODE flag. |
| 382 | ** |
| 383 | ** If pOut is NULL and the global pThOut is not then that blob |
| 384 | ** is used for output. |
| 385 | */ |
| 386 | static void sendText(Blob * pOut, const char *z, int n, int encode){ |
| 387 | if(0==pOut && pThOut!=0){ |
| 388 | pOut = pThOut; |
| 389 | } |
| 390 | if(TH_INIT_NO_ENCODE & g.th1Flags){ |
| 391 | encode = 0; |
| 392 | } |
| 393 | if( enableOutput && n ){ |
| 394 | if( n<0 ) n = strlen(z); |
| 395 | if( encode ){ |
| 396 | z = htmlize(z, n); |
| 397 | n = strlen(z); |
| 398 | } |
| 399 | if(pOut!=0){ |
| @@ -525,14 +530,22 @@ | |
| 525 | void *pConvert, |
| 526 | int argc, |
| 527 | const char **argv, |
| 528 | int *argl |
| 529 | ){ |
| 530 | if( argc!=2 ){ |
| 531 | return Th_WrongNumArgs(interp, "puts STRING"); |
| 532 | } |
| 533 | sendText(0,(char*)argv[1], argl[1], *(unsigned int*)pConvert); |
| 534 | return TH_OK; |
| 535 | } |
| 536 | |
| 537 | /* |
| 538 | ** TH1 command: redirect URL ?withMethod? |
| @@ -557,10 +570,15 @@ | |
| 557 | } |
| 558 | if( argc==3 ){ |
| 559 | if( Th_ToInt(interp, argv[2], argl[2], &withMethod) ){ |
| 560 | return TH_ERROR; |
| 561 | } |
| 562 | } |
| 563 | if( withMethod ){ |
| 564 | cgi_redirect_with_method(argv[1]); |
| 565 | }else{ |
| 566 | cgi_redirect(argv[1]); |
| @@ -660,11 +678,11 @@ | |
| 660 | int nValue = 0; |
| 661 | if( argc!=2 ){ |
| 662 | return Th_WrongNumArgs(interp, "markdown STRING"); |
| 663 | } |
| 664 | blob_zero(&src); |
| 665 | blob_init(&src, (char*)argv[1], argl[1]); |
| 666 | blob_zero(&title); blob_zero(&body); |
| 667 | markdown_to_html(&src, &title, &body); |
| 668 | Th_ListAppend(interp, &zValue, &nValue, blob_str(&title), blob_size(&title)); |
| 669 | Th_ListAppend(interp, &zValue, &nValue, blob_str(&body), blob_size(&body)); |
| 670 | Th_SetResult(interp, zValue, nValue); |
| @@ -690,11 +708,11 @@ | |
| 690 | if( argc!=2 ){ |
| 691 | return Th_WrongNumArgs(interp, "wiki STRING"); |
| 692 | } |
| 693 | if( enableOutput ){ |
| 694 | Blob src; |
| 695 | blob_init(&src, (char*)argv[1], argl[1]); |
| 696 | wiki_convert(&src, 0, flags); |
| 697 | blob_reset(&src); |
| 698 | } |
| 699 | return TH_OK; |
| 700 | } |
| @@ -735,11 +753,11 @@ | |
| 735 | ){ |
| 736 | char *zOut; |
| 737 | if( argc!=2 ){ |
| 738 | return Th_WrongNumArgs(interp, "htmlize STRING"); |
| 739 | } |
| 740 | zOut = htmlize((char*)argv[1], argl[1]); |
| 741 | Th_SetResult(interp, zOut, -1); |
| 742 | free(zOut); |
| 743 | return TH_OK; |
| 744 | } |
| 745 | |
| @@ -757,11 +775,11 @@ | |
| 757 | ){ |
| 758 | char *zOut; |
| 759 | if( argc!=2 ){ |
| 760 | return Th_WrongNumArgs(interp, "encode64 STRING"); |
| 761 | } |
| 762 | zOut = encode64((char*)argv[1], argl[1]); |
| 763 | Th_SetResult(interp, zOut, -1); |
| 764 | free(zOut); |
| 765 | return TH_OK; |
| 766 | } |
| 767 | |
| @@ -778,11 +796,11 @@ | |
| 778 | int argc, |
| 779 | const char **argv, |
| 780 | int *argl |
| 781 | ){ |
| 782 | char *zOut; |
| 783 | if( argc>=2 && argl[1]==6 && memcmp(argv[1],"-local",6)==0 ){ |
| 784 | zOut = db_text("??", "SELECT datetime('now',toLocal())"); |
| 785 | }else{ |
| 786 | zOut = db_text("??", "SELECT datetime('now')"); |
| 787 | } |
| 788 | Th_SetResult(interp, zOut, -1); |
| @@ -810,13 +828,13 @@ | |
| 810 | if( argc<2 ){ |
| 811 | return Th_WrongNumArgs(interp, "hascap STRING ..."); |
| 812 | } |
| 813 | for(i=1; rc==1 && i<argc; i++){ |
| 814 | if( g.thTrace ){ |
| 815 | Th_ListAppend(interp, &zCapList, &nCapList, argv[i], argl[i]); |
| 816 | } |
| 817 | rc = login_has_capability((char*)argv[i],argl[i],*(int*)p); |
| 818 | } |
| 819 | if( g.thTrace ){ |
| 820 | Th_Trace("[%s %#h] => %d<br>\n", argv[0], nCapList, zCapList, rc); |
| 821 | Th_Free(interp, zCapList); |
| 822 | } |
| @@ -858,11 +876,11 @@ | |
| 858 | int i; |
| 859 | |
| 860 | if( argc!=2 ){ |
| 861 | return Th_WrongNumArgs(interp, "capexpr EXPR"); |
| 862 | } |
| 863 | rc = Th_SplitList(interp, argv[1], argl[1], &azCap, &anCap, &nCap); |
| 864 | if( rc ) return rc; |
| 865 | rc = 0; |
| 866 | for(i=0; i<nCap; i++){ |
| 867 | if( azCap[i][0]=='!' ){ |
| 868 | rc = !login_has_capability(azCap[i]+1, anCap[i]-1, 0); |
| @@ -921,11 +939,12 @@ | |
| 921 | if( argc<2 ){ |
| 922 | return Th_WrongNumArgs(interp, "hascap STRING ..."); |
| 923 | } |
| 924 | for(i=1; i<argc && rc; i++){ |
| 925 | int match = 0; |
| 926 | for(j=0; j<argl[i]; j++){ |
| 927 | switch( argv[i][j] ){ |
| 928 | case 'c': match |= searchCap & SRCH_CKIN; break; |
| 929 | case 'd': match |= searchCap & SRCH_DOC; break; |
| 930 | case 't': match |= searchCap & SRCH_TKT; break; |
| 931 | case 'w': match |= searchCap & SRCH_WIKI; break; |
| @@ -932,11 +951,11 @@ | |
| 932 | } |
| 933 | } |
| 934 | if( !match ) rc = 0; |
| 935 | } |
| 936 | if( g.thTrace ){ |
| 937 | Th_Trace("[searchable %#h] => %d<br>\n", argl[1], argv[1], rc); |
| 938 | } |
| 939 | Th_SetResultInt(interp, rc); |
| 940 | return TH_OK; |
| 941 | } |
| 942 | |
| @@ -1051,11 +1070,11 @@ | |
| 1051 | #endif |
| 1052 | else if( 0 == fossil_strnicmp( zArg, "markdown\0", 9 ) ){ |
| 1053 | rc = 1; |
| 1054 | } |
| 1055 | if( g.thTrace ){ |
| 1056 | Th_Trace("[hasfeature %#h] => %d<br>\n", argl[1], zArg, rc); |
| 1057 | } |
| 1058 | Th_SetResultInt(interp, rc); |
| 1059 | return TH_OK; |
| 1060 | } |
| 1061 | |
| @@ -1104,18 +1123,20 @@ | |
| 1104 | const char **argv, |
| 1105 | int *argl |
| 1106 | ){ |
| 1107 | int rc = 0; |
| 1108 | int i; |
| 1109 | if( argc!=2 ){ |
| 1110 | return Th_WrongNumArgs(interp, "anycap STRING"); |
| 1111 | } |
| 1112 | for(i=0; rc==0 && i<argl[1]; i++){ |
| 1113 | rc = login_has_capability((char*)&argv[1][i],1,0); |
| 1114 | } |
| 1115 | if( g.thTrace ){ |
| 1116 | Th_Trace("[anycap %#h] => %d<br>\n", argl[1], argv[1], rc); |
| 1117 | } |
| 1118 | Th_SetResultInt(interp, rc); |
| 1119 | return TH_OK; |
| 1120 | } |
| 1121 | |
| @@ -1140,22 +1161,23 @@ | |
| 1140 | return Th_WrongNumArgs(interp, "combobox NAME TEXT-LIST NUMLINES"); |
| 1141 | } |
| 1142 | if( enableOutput ){ |
| 1143 | int height; |
| 1144 | Blob name; |
| 1145 | int nValue; |
| 1146 | const char *zValue; |
| 1147 | char *z, *zH; |
| 1148 | int nElem; |
| 1149 | int *aszElem; |
| 1150 | char **azElem; |
| 1151 | int i; |
| 1152 | |
| 1153 | if( Th_ToInt(interp, argv[3], argl[3], &height) ) return TH_ERROR; |
| 1154 | Th_SplitList(interp, argv[2], argl[2], &azElem, &aszElem, &nElem); |
| 1155 | blob_init(&name, (char*)argv[1], argl[1]); |
| 1156 | zValue = Th_Fetch(blob_str(&name), &nValue); |
| 1157 | zH = htmlize(blob_buffer(&name), blob_size(&name)); |
| 1158 | z = mprintf("<select id=\"%s\" name=\"%s\" size=\"%d\">", zH, zH, height); |
| 1159 | free(zH); |
| 1160 | sendText(0,z, -1, 0); |
| 1161 | free(z); |
| @@ -1247,11 +1269,11 @@ | |
| 1247 | return Th_WrongNumArgs(interp, "linecount STRING MAX MIN"); |
| 1248 | } |
| 1249 | if( Th_ToInt(interp, argv[2], argl[2], &iMax) ) return TH_ERROR; |
| 1250 | if( Th_ToInt(interp, argv[3], argl[3], &iMin) ) return TH_ERROR; |
| 1251 | z = argv[1]; |
| 1252 | size = argl[1]; |
| 1253 | for(n=1, i=0; i<size; i++){ |
| 1254 | if( z[i]=='\n' ){ |
| 1255 | n++; |
| 1256 | if( n>=iMax ) break; |
| 1257 | } |
| @@ -1407,11 +1429,12 @@ | |
| 1407 | return TH_OK; |
| 1408 | }else if( fossil_strnicmp(argv[1], "vfs\0", 4)==0 ){ |
| 1409 | Th_SetResult(interp, g.zVfsName ? g.zVfsName : zDefault, -1); |
| 1410 | return TH_OK; |
| 1411 | }else{ |
| 1412 | Th_ErrorMessage(interp, "unsupported global state:", argv[1], argl[1]); |
| 1413 | return TH_ERROR; |
| 1414 | } |
| 1415 | } |
| 1416 | |
| 1417 | /* |
| @@ -1426,17 +1449,21 @@ | |
| 1426 | int argc, |
| 1427 | const char **argv, |
| 1428 | int *argl |
| 1429 | ){ |
| 1430 | const char *zDefault = 0; |
| 1431 | if( argc!=2 && argc!=3 ){ |
| 1432 | return Th_WrongNumArgs(interp, "getParameter NAME ?DEFAULT?"); |
| 1433 | } |
| 1434 | if( argc==3 ){ |
| 1435 | zDefault = argv[2]; |
| 1436 | } |
| 1437 | Th_SetResult(interp, cgi_parameter(argv[1], zDefault), -1); |
| 1438 | return TH_OK; |
| 1439 | } |
| 1440 | |
| 1441 | /* |
| 1442 | ** TH1 command: setParameter NAME VALUE |
| @@ -1848,10 +1875,47 @@ | |
| 1848 | sqlite3_snprintf(sizeof(zUTime), zUTime, "%llu", x); |
| 1849 | Th_SetResult(interp, zUTime, -1); |
| 1850 | return TH_OK; |
| 1851 | } |
| 1852 | |
| 1853 | |
| 1854 | /* |
| 1855 | ** TH1 command: randhex N |
| 1856 | ** |
| 1857 | ** Return N*2 random hexadecimal digits with N<50. If N is omitted, |
| @@ -1923,11 +1987,13 @@ | |
| 1923 | int res = TH_OK; |
| 1924 | int nVar; |
| 1925 | char *zErr = 0; |
| 1926 | int noComplain = 0; |
| 1927 | |
| 1928 | if( argc>3 && argl[1]==11 && strncmp(argv[1], "-nocomplain", 11)==0 ){ |
| 1929 | argc--; |
| 1930 | argv++; |
| 1931 | argl++; |
| 1932 | noComplain = 1; |
| 1933 | } |
| @@ -1939,15 +2005,22 @@ | |
| 1939 | Th_ErrorMessage(interp, "database is not open", 0, 0); |
| 1940 | return TH_ERROR; |
| 1941 | } |
| 1942 | zSql = argv[1]; |
| 1943 | nSql = argl[1]; |
| 1944 | while( res==TH_OK && nSql>0 ){ |
| 1945 | zErr = 0; |
| 1946 | report_restrict_sql(&zErr); |
| 1947 | g.dbIgnoreErrors++; |
| 1948 | rc = sqlite3_prepare_v2(g.db, argv[1], argl[1], &pStmt, &zTail); |
| 1949 | g.dbIgnoreErrors--; |
| 1950 | report_unrestrict_sql(); |
| 1951 | if( rc!=0 || zErr!=0 ){ |
| 1952 | if( noComplain ) return TH_OK; |
| 1953 | Th_ErrorMessage(interp, "SQL error: ", |
| @@ -1964,31 +2037,31 @@ | |
| 1964 | int szVar = zVar ? th_strlen(zVar) : 0; |
| 1965 | if( szVar>1 && zVar[0]=='$' |
| 1966 | && Th_GetVar(interp, zVar+1, szVar-1)==TH_OK ){ |
| 1967 | int nVal; |
| 1968 | const char *zVal = Th_GetResult(interp, &nVal); |
| 1969 | sqlite3_bind_text(pStmt, i, zVal, nVal, SQLITE_TRANSIENT); |
| 1970 | } |
| 1971 | } |
| 1972 | while( res==TH_OK && ignore_errors_step(pStmt)==SQLITE_ROW ){ |
| 1973 | int nCol = sqlite3_column_count(pStmt); |
| 1974 | for(i=0; i<nCol; i++){ |
| 1975 | const char *zCol = sqlite3_column_name(pStmt, i); |
| 1976 | int szCol = th_strlen(zCol); |
| 1977 | const char *zVal = (const char*)sqlite3_column_text(pStmt, i); |
| 1978 | int szVal = sqlite3_column_bytes(pStmt, i); |
| 1979 | Th_SetVar(interp, zCol, szCol, zVal, szVal); |
| 1980 | } |
| 1981 | if( g.thTrace ){ |
| 1982 | Th_Trace("query_eval {<pre>%#h</pre>}<br>\n", argl[2], argv[2]); |
| 1983 | } |
| 1984 | res = Th_Eval(interp, 0, argv[2], argl[2]); |
| 1985 | if( g.thTrace ){ |
| 1986 | int nTrRes; |
| 1987 | char *zTrRes = (char*)Th_GetResult(g.interp, &nTrRes); |
| 1988 | Th_Trace("[query_eval] => %h {%#h}<br>\n", |
| 1989 | Th_ReturnCodeName(res, 0), nTrRes, zTrRes); |
| 1990 | } |
| 1991 | if( res==TH_BREAK || res==TH_CONTINUE ) res = TH_OK; |
| 1992 | } |
| 1993 | rc = sqlite3_finalize(pStmt); |
| 1994 | if( rc!=SQLITE_OK ){ |
| @@ -2038,11 +2111,11 @@ | |
| 2038 | Th_SetResult(interp, 0, 0); |
| 2039 | rc = TH_OK; |
| 2040 | } |
| 2041 | if( g.thTrace ){ |
| 2042 | Th_Trace("[setting %s%#h] => %d<br>\n", strict ? "strict " : "", |
| 2043 | argl[nArg], argv[nArg], rc); |
| 2044 | } |
| 2045 | return rc; |
| 2046 | } |
| 2047 | |
| 2048 | /* |
| @@ -2121,11 +2194,11 @@ | |
| 2121 | return Th_WrongNumArgs(interp, REGEXP_WRONGNUMARGS); |
| 2122 | } |
| 2123 | zErr = re_compile(&pRe, argv[nArg], noCase); |
| 2124 | if( !zErr ){ |
| 2125 | Th_SetResultInt(interp, re_match(pRe, |
| 2126 | (const unsigned char *)argv[nArg+1], argl[nArg+1])); |
| 2127 | rc = TH_OK; |
| 2128 | }else{ |
| 2129 | Th_SetResult(interp, zErr, -1); |
| 2130 | rc = TH_ERROR; |
| 2131 | } |
| @@ -2160,11 +2233,11 @@ | |
| 2160 | UrlData urlData; |
| 2161 | |
| 2162 | if( argc<2 || argc>5 ){ |
| 2163 | return Th_WrongNumArgs(interp, HTTP_WRONGNUMARGS); |
| 2164 | } |
| 2165 | if( fossil_strnicmp(argv[nArg], "-asynchronous", argl[nArg])==0 ){ |
| 2166 | fAsynchronous = 1; nArg++; |
| 2167 | } |
| 2168 | if( fossil_strcmp(argv[nArg], "--")==0 ) nArg++; |
| 2169 | if( nArg+1!=argc && nArg+2!=argc ){ |
| 2170 | return Th_WrongNumArgs(interp, REGEXP_WRONGNUMARGS); |
| @@ -2189,11 +2262,11 @@ | |
| 2189 | return TH_ERROR; |
| 2190 | } |
| 2191 | re_free(pRe); |
| 2192 | blob_zero(&payload); |
| 2193 | if( nArg+2==argc ){ |
| 2194 | blob_append(&payload, argv[nArg+1], argl[nArg+1]); |
| 2195 | zType = "POST"; |
| 2196 | }else{ |
| 2197 | zType = "GET"; |
| 2198 | } |
| 2199 | if( fAsynchronous ){ |
| @@ -2268,11 +2341,11 @@ | |
| 2268 | if( argc!=2 ){ |
| 2269 | return Th_WrongNumArgs(interp, "captureTh1 STRING"); |
| 2270 | } |
| 2271 | pOrig = Th_SetOutputBlob(&out); |
| 2272 | zStr = argv[1]; |
| 2273 | nStr = argl[1]; |
| 2274 | rc = Th_Eval(g.interp, 0, zStr, nStr); |
| 2275 | Th_SetOutputBlob(pOrig); |
| 2276 | if(0==rc){ |
| 2277 | Th_SetResult(g.interp, blob_str(&out), blob_size(&out)); |
| 2278 | } |
| @@ -2387,13 +2460,15 @@ | |
| 2387 | {"setting", settingCmd, 0}, |
| 2388 | {"styleFooter", styleFooterCmd, 0}, |
| 2389 | {"styleHeader", styleHeaderCmd, 0}, |
| 2390 | {"styleScript", styleScriptCmd, 0}, |
| 2391 | {"submenu", submenuCmd, 0}, |
| 2392 | {"tclReady", tclReadyCmd, 0}, |
| 2393 | {"trace", traceCmd, 0}, |
| 2394 | {"stime", stimeCmd, 0}, |
| 2395 | {"unversioned", unversionedCmd, 0}, |
| 2396 | {"utime", utimeCmd, 0}, |
| 2397 | {"verifyCsrf", verifyCsrfCmd, 0}, |
| 2398 | {"verifyLogin", verifyLoginCmd, 0}, |
| 2399 | {"wiki", wikiCmd, (void*)&aFlags[0]}, |
| @@ -2494,10 +2569,26 @@ | |
| 2494 | Th_Trace("set %h {%h}<br>\n", zName, zValue); |
| 2495 | } |
| 2496 | Th_SetVar(g.interp, zName, -1, zValue, strlen(zValue)); |
| 2497 | } |
| 2498 | } |
| 2499 | |
| 2500 | /* |
| 2501 | ** Appends an element to a TH1 list value. This function is called by the |
| 2502 | ** transfer subsystem; therefore, it must be very careful to avoid doing |
| 2503 | ** any unnecessary work. To that end, the TH1 subsystem will not be called |
| @@ -2680,10 +2771,11 @@ | |
| 2680 | char *zResult = (char*)Th_GetResult(g.interp, &nResult); |
| 2681 | /* |
| 2682 | ** Make sure that the TH1 script error was not caused by a "missing" |
| 2683 | ** command hook handler as that is not actually an error condition. |
| 2684 | */ |
| 2685 | if( memcmp(zResult, NO_COMMAND_HOOK_ERROR, nResult)!=0 ){ |
| 2686 | sendError(0,zResult, nResult, 0); |
| 2687 | }else{ |
| 2688 | /* |
| 2689 | ** There is no command hook handler "installed". This situation |
| @@ -2767,10 +2859,11 @@ | |
| 2767 | char *zResult = (char*)Th_GetResult(g.interp, &nResult); |
| 2768 | /* |
| 2769 | ** Make sure that the TH1 script error was not caused by a "missing" |
| 2770 | ** webpage hook handler as that is not actually an error condition. |
| 2771 | */ |
| 2772 | if( memcmp(zResult, NO_WEBPAGE_HOOK_ERROR, nResult)!=0 ){ |
| 2773 | sendError(0,zResult, nResult, 1); |
| 2774 | }else{ |
| 2775 | /* |
| 2776 | ** There is no webpage hook handler "installed". This situation |
| @@ -2894,11 +2987,16 @@ | |
| 2894 | } |
| 2895 | rc = Th_GetVar(g.interp, (char*)zVar, nVar); |
| 2896 | z += i+1+n; |
| 2897 | i = 0; |
| 2898 | zResult = (char*)Th_GetResult(g.interp, &n); |
| 2899 | sendText(pOut,(char*)zResult, n, encode); |
| 2900 | }else if( z[i]=='<' && isBeginScriptTag(&z[i]) ){ |
| 2901 | sendText(pOut,z, i, 0); |
| 2902 | z += i+5; |
| 2903 | for(i=0; z[i] && (z[i]!='<' || !isEndScriptTag(&z[i])); i++){} |
| 2904 | if( g.thTrace ){ |
| @@ -2907,11 +3005,11 @@ | |
| 2907 | rc = Th_Eval(g.interp, 0, (const char*)z, i); |
| 2908 | if( g.thTrace ){ |
| 2909 | int nTrRes; |
| 2910 | char *zTrRes = (char*)Th_GetResult(g.interp, &nTrRes); |
| 2911 | Th_Trace("[render_eval] => %h {%#h}<br>\n", |
| 2912 | Th_ReturnCodeName(rc, 0), nTrRes, zTrRes); |
| 2913 | } |
| 2914 | if( rc!=TH_OK ) break; |
| 2915 | z += i; |
| 2916 | if( z[0] ){ z += 6; } |
| 2917 | i = 0; |
| @@ -2953,10 +3051,78 @@ | |
| 2953 | ** as appropriate. We need to pass on g.th1Flags for the case of |
| 2954 | ** recursive calls, so that, e.g., TH_INIT_NO_ENCODE does not get |
| 2955 | ** inadvertently toggled off by a recursive call. |
| 2956 | */; |
| 2957 | } |
| 2958 | |
| 2959 | /* |
| 2960 | ** COMMAND: test-th-render |
| 2961 | ** |
| 2962 | ** Usage: %fossil test-th-render FILE |
| @@ -2992,10 +3158,11 @@ | |
| 2992 | if( find_option("set-user-caps", 0, 0)!=0 ){ |
| 2993 | const char *zCap = fossil_getenv("TH1_TEST_USER_CAPS"); |
| 2994 | login_set_capabilities(zCap ? zCap : "sx", 0); |
| 2995 | g.useLocalauth = 1; |
| 2996 | } |
| 2997 | verify_all_options(); |
| 2998 | if( g.argc<3 ){ |
| 2999 | usage("FILE"); |
| 3000 | } |
| 3001 | blob_zero(&in); |
| @@ -3044,10 +3211,11 @@ | |
| 3044 | if( find_option("set-user-caps", 0, 0)!=0 ){ |
| 3045 | const char *zCap = fossil_getenv("TH1_TEST_USER_CAPS"); |
| 3046 | login_set_capabilities(zCap ? zCap : "sx", 0); |
| 3047 | g.useLocalauth = 1; |
| 3048 | } |
| 3049 | verify_all_options(); |
| 3050 | if( g.argc!=3 ){ |
| 3051 | usage("script"); |
| 3052 | } |
| 3053 | if(file_isfile(g.argv[2], ExtFILE)){ |
| 3054 |
| --- src/th_main.c | |
| +++ src/th_main.c | |
| @@ -262,11 +262,11 @@ | |
| 262 | ){ |
| 263 | char *zOut; |
| 264 | if( argc!=2 ){ |
| 265 | return Th_WrongNumArgs(interp, "httpize STRING"); |
| 266 | } |
| 267 | zOut = httpize((char*)argv[1], TH1_LEN(argl[1])); |
| 268 | Th_SetResult(interp, zOut, -1); |
| 269 | free(zOut); |
| 270 | return TH_OK; |
| 271 | } |
| 272 | |
| @@ -291,11 +291,12 @@ | |
| 291 | if( argc<2 || argc>3 ){ |
| 292 | return Th_WrongNumArgs(interp, "enable_output [LABEL] BOOLEAN"); |
| 293 | } |
| 294 | rc = Th_ToInt(interp, argv[argc-1], argl[argc-1], &enableOutput); |
| 295 | if( g.thTrace ){ |
| 296 | Th_Trace("enable_output {%.*s} -> %d<br>\n", |
| 297 | TH1_LEN(argl[1]),argv[1],enableOutput); |
| 298 | } |
| 299 | return rc; |
| 300 | } |
| 301 | |
| 302 | /* |
| @@ -322,11 +323,11 @@ | |
| 323 | buul = (TH_INIT_NO_ENCODE & g.th1Flags) ? 0 : 1; |
| 324 | Th_SetResultInt(g.interp, buul); |
| 325 | if(argc>1){ |
| 326 | if( g.thTrace ){ |
| 327 | Th_Trace("enable_htmlify {%.*s} -> %d<br>\n", |
| 328 | TH1_LEN(argl[1]),argv[1],buul); |
| 329 | } |
| 330 | rc = Th_ToInt(interp, argv[argc-1], argl[argc-1], &buul); |
| 331 | if(!rc){ |
| 332 | if(buul){ |
| 333 | g.th1Flags &= ~TH_INIT_NO_ENCODE; |
| @@ -381,19 +382,23 @@ | |
| 382 | ** g.th1Flags has the TH_INIT_NO_ENCODE flag. |
| 383 | ** |
| 384 | ** If pOut is NULL and the global pThOut is not then that blob |
| 385 | ** is used for output. |
| 386 | */ |
| 387 | static void sendText(Blob *pOut, const char *z, int n, int encode){ |
| 388 | if(0==pOut && pThOut!=0){ |
| 389 | pOut = pThOut; |
| 390 | } |
| 391 | if(TH_INIT_NO_ENCODE & g.th1Flags){ |
| 392 | encode = 0; |
| 393 | } |
| 394 | if( enableOutput && n ){ |
| 395 | if( n<0 ){ |
| 396 | n = strlen(z); |
| 397 | }else{ |
| 398 | n = TH1_LEN(n); |
| 399 | } |
| 400 | if( encode ){ |
| 401 | z = htmlize(z, n); |
| 402 | n = strlen(z); |
| 403 | } |
| 404 | if(pOut!=0){ |
| @@ -525,14 +530,22 @@ | |
| 530 | void *pConvert, |
| 531 | int argc, |
| 532 | const char **argv, |
| 533 | int *argl |
| 534 | ){ |
| 535 | int encode = *(unsigned int*)pConvert; |
| 536 | int n; |
| 537 | if( argc!=2 ){ |
| 538 | return Th_WrongNumArgs(interp, "puts STRING"); |
| 539 | } |
| 540 | n = argl[1]; |
| 541 | if( encode==0 && n>0 && TH1_TAINTED(n) ){ |
| 542 | if( Th_ReportTaint(interp, "output string", argv[1], n) ){ |
| 543 | return TH_ERROR; |
| 544 | } |
| 545 | } |
| 546 | sendText(0,(char*)argv[1], TH1_LEN(n), encode); |
| 547 | return TH_OK; |
| 548 | } |
| 549 | |
| 550 | /* |
| 551 | ** TH1 command: redirect URL ?withMethod? |
| @@ -557,10 +570,15 @@ | |
| 570 | } |
| 571 | if( argc==3 ){ |
| 572 | if( Th_ToInt(interp, argv[2], argl[2], &withMethod) ){ |
| 573 | return TH_ERROR; |
| 574 | } |
| 575 | } |
| 576 | if( TH1_TAINTED(argl[1]) |
| 577 | && Th_ReportTaint(interp,"redirect URL",argv[1],argl[1]) |
| 578 | ){ |
| 579 | return TH_ERROR; |
| 580 | } |
| 581 | if( withMethod ){ |
| 582 | cgi_redirect_with_method(argv[1]); |
| 583 | }else{ |
| 584 | cgi_redirect(argv[1]); |
| @@ -660,11 +678,11 @@ | |
| 678 | int nValue = 0; |
| 679 | if( argc!=2 ){ |
| 680 | return Th_WrongNumArgs(interp, "markdown STRING"); |
| 681 | } |
| 682 | blob_zero(&src); |
| 683 | blob_init(&src, (char*)argv[1], TH1_LEN(argl[1])); |
| 684 | blob_zero(&title); blob_zero(&body); |
| 685 | markdown_to_html(&src, &title, &body); |
| 686 | Th_ListAppend(interp, &zValue, &nValue, blob_str(&title), blob_size(&title)); |
| 687 | Th_ListAppend(interp, &zValue, &nValue, blob_str(&body), blob_size(&body)); |
| 688 | Th_SetResult(interp, zValue, nValue); |
| @@ -690,11 +708,11 @@ | |
| 708 | if( argc!=2 ){ |
| 709 | return Th_WrongNumArgs(interp, "wiki STRING"); |
| 710 | } |
| 711 | if( enableOutput ){ |
| 712 | Blob src; |
| 713 | blob_init(&src, (char*)argv[1], TH1_LEN(argl[1])); |
| 714 | wiki_convert(&src, 0, flags); |
| 715 | blob_reset(&src); |
| 716 | } |
| 717 | return TH_OK; |
| 718 | } |
| @@ -735,11 +753,11 @@ | |
| 753 | ){ |
| 754 | char *zOut; |
| 755 | if( argc!=2 ){ |
| 756 | return Th_WrongNumArgs(interp, "htmlize STRING"); |
| 757 | } |
| 758 | zOut = htmlize((char*)argv[1], TH1_LEN(argl[1])); |
| 759 | Th_SetResult(interp, zOut, -1); |
| 760 | free(zOut); |
| 761 | return TH_OK; |
| 762 | } |
| 763 | |
| @@ -757,11 +775,11 @@ | |
| 775 | ){ |
| 776 | char *zOut; |
| 777 | if( argc!=2 ){ |
| 778 | return Th_WrongNumArgs(interp, "encode64 STRING"); |
| 779 | } |
| 780 | zOut = encode64((char*)argv[1], TH1_LEN(argl[1])); |
| 781 | Th_SetResult(interp, zOut, -1); |
| 782 | free(zOut); |
| 783 | return TH_OK; |
| 784 | } |
| 785 | |
| @@ -778,11 +796,11 @@ | |
| 796 | int argc, |
| 797 | const char **argv, |
| 798 | int *argl |
| 799 | ){ |
| 800 | char *zOut; |
| 801 | if( argc>=2 && TH1_LEN(argl[1])==6 && memcmp(argv[1],"-local",6)==0 ){ |
| 802 | zOut = db_text("??", "SELECT datetime('now',toLocal())"); |
| 803 | }else{ |
| 804 | zOut = db_text("??", "SELECT datetime('now')"); |
| 805 | } |
| 806 | Th_SetResult(interp, zOut, -1); |
| @@ -810,13 +828,13 @@ | |
| 828 | if( argc<2 ){ |
| 829 | return Th_WrongNumArgs(interp, "hascap STRING ..."); |
| 830 | } |
| 831 | for(i=1; rc==1 && i<argc; i++){ |
| 832 | if( g.thTrace ){ |
| 833 | Th_ListAppend(interp, &zCapList, &nCapList, argv[i], TH1_LEN(argl[i])); |
| 834 | } |
| 835 | rc = login_has_capability((char*)argv[i],TH1_LEN(argl[i]),*(int*)p); |
| 836 | } |
| 837 | if( g.thTrace ){ |
| 838 | Th_Trace("[%s %#h] => %d<br>\n", argv[0], nCapList, zCapList, rc); |
| 839 | Th_Free(interp, zCapList); |
| 840 | } |
| @@ -858,11 +876,11 @@ | |
| 876 | int i; |
| 877 | |
| 878 | if( argc!=2 ){ |
| 879 | return Th_WrongNumArgs(interp, "capexpr EXPR"); |
| 880 | } |
| 881 | rc = Th_SplitList(interp, argv[1], TH1_LEN(argl[1]), &azCap, &anCap, &nCap); |
| 882 | if( rc ) return rc; |
| 883 | rc = 0; |
| 884 | for(i=0; i<nCap; i++){ |
| 885 | if( azCap[i][0]=='!' ){ |
| 886 | rc = !login_has_capability(azCap[i]+1, anCap[i]-1, 0); |
| @@ -921,11 +939,12 @@ | |
| 939 | if( argc<2 ){ |
| 940 | return Th_WrongNumArgs(interp, "hascap STRING ..."); |
| 941 | } |
| 942 | for(i=1; i<argc && rc; i++){ |
| 943 | int match = 0; |
| 944 | int nn = TH1_LEN(argl[i]); |
| 945 | for(j=0; j<nn; j++){ |
| 946 | switch( argv[i][j] ){ |
| 947 | case 'c': match |= searchCap & SRCH_CKIN; break; |
| 948 | case 'd': match |= searchCap & SRCH_DOC; break; |
| 949 | case 't': match |= searchCap & SRCH_TKT; break; |
| 950 | case 'w': match |= searchCap & SRCH_WIKI; break; |
| @@ -932,11 +951,11 @@ | |
| 951 | } |
| 952 | } |
| 953 | if( !match ) rc = 0; |
| 954 | } |
| 955 | if( g.thTrace ){ |
| 956 | Th_Trace("[searchable %#h] => %d<br>\n", TH1_LEN(argl[1]), argv[1], rc); |
| 957 | } |
| 958 | Th_SetResultInt(interp, rc); |
| 959 | return TH_OK; |
| 960 | } |
| 961 | |
| @@ -1051,11 +1070,11 @@ | |
| 1070 | #endif |
| 1071 | else if( 0 == fossil_strnicmp( zArg, "markdown\0", 9 ) ){ |
| 1072 | rc = 1; |
| 1073 | } |
| 1074 | if( g.thTrace ){ |
| 1075 | Th_Trace("[hasfeature %#h] => %d<br>\n", TH1_LEN(argl[1]), zArg, rc); |
| 1076 | } |
| 1077 | Th_SetResultInt(interp, rc); |
| 1078 | return TH_OK; |
| 1079 | } |
| 1080 | |
| @@ -1104,18 +1123,20 @@ | |
| 1123 | const char **argv, |
| 1124 | int *argl |
| 1125 | ){ |
| 1126 | int rc = 0; |
| 1127 | int i; |
| 1128 | int nn; |
| 1129 | if( argc!=2 ){ |
| 1130 | return Th_WrongNumArgs(interp, "anycap STRING"); |
| 1131 | } |
| 1132 | nn = TH1_LEN(argl[1]); |
| 1133 | for(i=0; rc==0 && i<nn; i++){ |
| 1134 | rc = login_has_capability((char*)&argv[1][i],1,0); |
| 1135 | } |
| 1136 | if( g.thTrace ){ |
| 1137 | Th_Trace("[anycap %#h] => %d<br>\n", TH1_LEN(argl[1]), argv[1], rc); |
| 1138 | } |
| 1139 | Th_SetResultInt(interp, rc); |
| 1140 | return TH_OK; |
| 1141 | } |
| 1142 | |
| @@ -1140,22 +1161,23 @@ | |
| 1161 | return Th_WrongNumArgs(interp, "combobox NAME TEXT-LIST NUMLINES"); |
| 1162 | } |
| 1163 | if( enableOutput ){ |
| 1164 | int height; |
| 1165 | Blob name; |
| 1166 | int nValue = 0; |
| 1167 | const char *zValue; |
| 1168 | char *z, *zH; |
| 1169 | int nElem; |
| 1170 | int *aszElem; |
| 1171 | char **azElem; |
| 1172 | int i; |
| 1173 | |
| 1174 | if( Th_ToInt(interp, argv[3], argl[3], &height) ) return TH_ERROR; |
| 1175 | Th_SplitList(interp, argv[2], TH1_LEN(argl[2]), &azElem, &aszElem, &nElem); |
| 1176 | blob_init(&name, (char*)argv[1], TH1_LEN(argl[1])); |
| 1177 | zValue = Th_Fetch(blob_str(&name), &nValue); |
| 1178 | nValue = TH1_LEN(nValue); |
| 1179 | zH = htmlize(blob_buffer(&name), blob_size(&name)); |
| 1180 | z = mprintf("<select id=\"%s\" name=\"%s\" size=\"%d\">", zH, zH, height); |
| 1181 | free(zH); |
| 1182 | sendText(0,z, -1, 0); |
| 1183 | free(z); |
| @@ -1247,11 +1269,11 @@ | |
| 1269 | return Th_WrongNumArgs(interp, "linecount STRING MAX MIN"); |
| 1270 | } |
| 1271 | if( Th_ToInt(interp, argv[2], argl[2], &iMax) ) return TH_ERROR; |
| 1272 | if( Th_ToInt(interp, argv[3], argl[3], &iMin) ) return TH_ERROR; |
| 1273 | z = argv[1]; |
| 1274 | size = TH1_LEN(argl[1]); |
| 1275 | for(n=1, i=0; i<size; i++){ |
| 1276 | if( z[i]=='\n' ){ |
| 1277 | n++; |
| 1278 | if( n>=iMax ) break; |
| 1279 | } |
| @@ -1407,11 +1429,12 @@ | |
| 1429 | return TH_OK; |
| 1430 | }else if( fossil_strnicmp(argv[1], "vfs\0", 4)==0 ){ |
| 1431 | Th_SetResult(interp, g.zVfsName ? g.zVfsName : zDefault, -1); |
| 1432 | return TH_OK; |
| 1433 | }else{ |
| 1434 | Th_ErrorMessage(interp, "unsupported global state:", |
| 1435 | argv[1], TH1_LEN(argl[1])); |
| 1436 | return TH_ERROR; |
| 1437 | } |
| 1438 | } |
| 1439 | |
| 1440 | /* |
| @@ -1426,17 +1449,21 @@ | |
| 1449 | int argc, |
| 1450 | const char **argv, |
| 1451 | int *argl |
| 1452 | ){ |
| 1453 | const char *zDefault = 0; |
| 1454 | const char *zVal; |
| 1455 | int sz; |
| 1456 | if( argc!=2 && argc!=3 ){ |
| 1457 | return Th_WrongNumArgs(interp, "getParameter NAME ?DEFAULT?"); |
| 1458 | } |
| 1459 | if( argc==3 ){ |
| 1460 | zDefault = argv[2]; |
| 1461 | } |
| 1462 | zVal = cgi_parameter(argv[1], zDefault); |
| 1463 | sz = th_strlen(zVal); |
| 1464 | Th_SetResult(interp, zVal, TH1_ADD_TAINT(sz)); |
| 1465 | return TH_OK; |
| 1466 | } |
| 1467 | |
| 1468 | /* |
| 1469 | ** TH1 command: setParameter NAME VALUE |
| @@ -1848,10 +1875,47 @@ | |
| 1875 | sqlite3_snprintf(sizeof(zUTime), zUTime, "%llu", x); |
| 1876 | Th_SetResult(interp, zUTime, -1); |
| 1877 | return TH_OK; |
| 1878 | } |
| 1879 | |
| 1880 | /* |
| 1881 | ** TH1 command: taint STRING |
| 1882 | ** |
| 1883 | ** Return a copy of STRING that is marked as tainted. |
| 1884 | */ |
| 1885 | static int taintCmd( |
| 1886 | Th_Interp *interp, |
| 1887 | void *p, |
| 1888 | int argc, |
| 1889 | const char **argv, |
| 1890 | int *argl |
| 1891 | ){ |
| 1892 | if( argc!=2 ){ |
| 1893 | return Th_WrongNumArgs(interp, "STRING"); |
| 1894 | } |
| 1895 | Th_SetResult(interp, argv[1], TH1_ADD_TAINT(argl[1])); |
| 1896 | return TH_OK; |
| 1897 | } |
| 1898 | |
| 1899 | /* |
| 1900 | ** TH1 command: untaint STRING |
| 1901 | ** |
| 1902 | ** Return a copy of STRING that is marked as untainted. |
| 1903 | */ |
| 1904 | static int untaintCmd( |
| 1905 | Th_Interp *interp, |
| 1906 | void *p, |
| 1907 | int argc, |
| 1908 | const char **argv, |
| 1909 | int *argl |
| 1910 | ){ |
| 1911 | if( argc!=2 ){ |
| 1912 | return Th_WrongNumArgs(interp, "STRING"); |
| 1913 | } |
| 1914 | Th_SetResult(interp, argv[1], TH1_LEN(argl[1])); |
| 1915 | return TH_OK; |
| 1916 | } |
| 1917 | |
| 1918 | /* |
| 1919 | ** TH1 command: randhex N |
| 1920 | ** |
| 1921 | ** Return N*2 random hexadecimal digits with N<50. If N is omitted, |
| @@ -1923,11 +1987,13 @@ | |
| 1987 | int res = TH_OK; |
| 1988 | int nVar; |
| 1989 | char *zErr = 0; |
| 1990 | int noComplain = 0; |
| 1991 | |
| 1992 | if( argc>3 && TH1_LEN(argl[1])==11 |
| 1993 | && strncmp(argv[1], "-nocomplain", 11)==0 |
| 1994 | ){ |
| 1995 | argc--; |
| 1996 | argv++; |
| 1997 | argl++; |
| 1998 | noComplain = 1; |
| 1999 | } |
| @@ -1939,15 +2005,22 @@ | |
| 2005 | Th_ErrorMessage(interp, "database is not open", 0, 0); |
| 2006 | return TH_ERROR; |
| 2007 | } |
| 2008 | zSql = argv[1]; |
| 2009 | nSql = argl[1]; |
| 2010 | if( TH1_TAINTED(nSql) ){ |
| 2011 | if( Th_ReportTaint(interp,"query SQL",zSql,nSql) ){ |
| 2012 | return TH_ERROR; |
| 2013 | } |
| 2014 | nSql = TH1_LEN(nSql); |
| 2015 | } |
| 2016 | |
| 2017 | while( res==TH_OK && nSql>0 ){ |
| 2018 | zErr = 0; |
| 2019 | report_restrict_sql(&zErr); |
| 2020 | g.dbIgnoreErrors++; |
| 2021 | rc = sqlite3_prepare_v2(g.db, argv[1], TH1_LEN(argl[1]), &pStmt, &zTail); |
| 2022 | g.dbIgnoreErrors--; |
| 2023 | report_unrestrict_sql(); |
| 2024 | if( rc!=0 || zErr!=0 ){ |
| 2025 | if( noComplain ) return TH_OK; |
| 2026 | Th_ErrorMessage(interp, "SQL error: ", |
| @@ -1964,31 +2037,31 @@ | |
| 2037 | int szVar = zVar ? th_strlen(zVar) : 0; |
| 2038 | if( szVar>1 && zVar[0]=='$' |
| 2039 | && Th_GetVar(interp, zVar+1, szVar-1)==TH_OK ){ |
| 2040 | int nVal; |
| 2041 | const char *zVal = Th_GetResult(interp, &nVal); |
| 2042 | sqlite3_bind_text(pStmt, i, zVal, TH1_LEN(nVal), SQLITE_TRANSIENT); |
| 2043 | } |
| 2044 | } |
| 2045 | while( res==TH_OK && ignore_errors_step(pStmt)==SQLITE_ROW ){ |
| 2046 | int nCol = sqlite3_column_count(pStmt); |
| 2047 | for(i=0; i<nCol; i++){ |
| 2048 | const char *zCol = sqlite3_column_name(pStmt, i); |
| 2049 | int szCol = th_strlen(zCol); |
| 2050 | const char *zVal = (const char*)sqlite3_column_text(pStmt, i); |
| 2051 | int szVal = sqlite3_column_bytes(pStmt, i); |
| 2052 | Th_SetVar(interp, zCol, szCol, zVal, TH1_ADD_TAINT(szVal)); |
| 2053 | } |
| 2054 | if( g.thTrace ){ |
| 2055 | Th_Trace("query_eval {<pre>%#h</pre>}<br>\n",TH1_LEN(argl[2]),argv[2]); |
| 2056 | } |
| 2057 | res = Th_Eval(interp, 0, argv[2], TH1_LEN(argl[2])); |
| 2058 | if( g.thTrace ){ |
| 2059 | int nTrRes; |
| 2060 | char *zTrRes = (char*)Th_GetResult(g.interp, &nTrRes); |
| 2061 | Th_Trace("[query_eval] => %h {%#h}<br>\n", |
| 2062 | Th_ReturnCodeName(res, 0), TH1_LEN(nTrRes), zTrRes); |
| 2063 | } |
| 2064 | if( res==TH_BREAK || res==TH_CONTINUE ) res = TH_OK; |
| 2065 | } |
| 2066 | rc = sqlite3_finalize(pStmt); |
| 2067 | if( rc!=SQLITE_OK ){ |
| @@ -2038,11 +2111,11 @@ | |
| 2111 | Th_SetResult(interp, 0, 0); |
| 2112 | rc = TH_OK; |
| 2113 | } |
| 2114 | if( g.thTrace ){ |
| 2115 | Th_Trace("[setting %s%#h] => %d<br>\n", strict ? "strict " : "", |
| 2116 | TH1_LEN(argl[nArg]), argv[nArg], rc); |
| 2117 | } |
| 2118 | return rc; |
| 2119 | } |
| 2120 | |
| 2121 | /* |
| @@ -2121,11 +2194,11 @@ | |
| 2194 | return Th_WrongNumArgs(interp, REGEXP_WRONGNUMARGS); |
| 2195 | } |
| 2196 | zErr = re_compile(&pRe, argv[nArg], noCase); |
| 2197 | if( !zErr ){ |
| 2198 | Th_SetResultInt(interp, re_match(pRe, |
| 2199 | (const unsigned char *)argv[nArg+1], TH1_LEN(argl[nArg+1]))); |
| 2200 | rc = TH_OK; |
| 2201 | }else{ |
| 2202 | Th_SetResult(interp, zErr, -1); |
| 2203 | rc = TH_ERROR; |
| 2204 | } |
| @@ -2160,11 +2233,11 @@ | |
| 2233 | UrlData urlData; |
| 2234 | |
| 2235 | if( argc<2 || argc>5 ){ |
| 2236 | return Th_WrongNumArgs(interp, HTTP_WRONGNUMARGS); |
| 2237 | } |
| 2238 | if( fossil_strnicmp(argv[nArg], "-asynchronous", TH1_LEN(argl[nArg]))==0 ){ |
| 2239 | fAsynchronous = 1; nArg++; |
| 2240 | } |
| 2241 | if( fossil_strcmp(argv[nArg], "--")==0 ) nArg++; |
| 2242 | if( nArg+1!=argc && nArg+2!=argc ){ |
| 2243 | return Th_WrongNumArgs(interp, REGEXP_WRONGNUMARGS); |
| @@ -2189,11 +2262,11 @@ | |
| 2262 | return TH_ERROR; |
| 2263 | } |
| 2264 | re_free(pRe); |
| 2265 | blob_zero(&payload); |
| 2266 | if( nArg+2==argc ){ |
| 2267 | blob_append(&payload, argv[nArg+1], TH1_LEN(argl[nArg+1])); |
| 2268 | zType = "POST"; |
| 2269 | }else{ |
| 2270 | zType = "GET"; |
| 2271 | } |
| 2272 | if( fAsynchronous ){ |
| @@ -2268,11 +2341,11 @@ | |
| 2341 | if( argc!=2 ){ |
| 2342 | return Th_WrongNumArgs(interp, "captureTh1 STRING"); |
| 2343 | } |
| 2344 | pOrig = Th_SetOutputBlob(&out); |
| 2345 | zStr = argv[1]; |
| 2346 | nStr = TH1_LEN(argl[1]); |
| 2347 | rc = Th_Eval(g.interp, 0, zStr, nStr); |
| 2348 | Th_SetOutputBlob(pOrig); |
| 2349 | if(0==rc){ |
| 2350 | Th_SetResult(g.interp, blob_str(&out), blob_size(&out)); |
| 2351 | } |
| @@ -2387,13 +2460,15 @@ | |
| 2460 | {"setting", settingCmd, 0}, |
| 2461 | {"styleFooter", styleFooterCmd, 0}, |
| 2462 | {"styleHeader", styleHeaderCmd, 0}, |
| 2463 | {"styleScript", styleScriptCmd, 0}, |
| 2464 | {"submenu", submenuCmd, 0}, |
| 2465 | {"taint", taintCmd, 0}, |
| 2466 | {"tclReady", tclReadyCmd, 0}, |
| 2467 | {"trace", traceCmd, 0}, |
| 2468 | {"stime", stimeCmd, 0}, |
| 2469 | {"untaint", untaintCmd, 0}, |
| 2470 | {"unversioned", unversionedCmd, 0}, |
| 2471 | {"utime", utimeCmd, 0}, |
| 2472 | {"verifyCsrf", verifyCsrfCmd, 0}, |
| 2473 | {"verifyLogin", verifyLoginCmd, 0}, |
| 2474 | {"wiki", wikiCmd, (void*)&aFlags[0]}, |
| @@ -2494,10 +2569,26 @@ | |
| 2569 | Th_Trace("set %h {%h}<br>\n", zName, zValue); |
| 2570 | } |
| 2571 | Th_SetVar(g.interp, zName, -1, zValue, strlen(zValue)); |
| 2572 | } |
| 2573 | } |
| 2574 | |
| 2575 | /* |
| 2576 | ** Store a string value in a variable in the interpreter |
| 2577 | ** with the "taint" marking, so that TH1 knows that this |
| 2578 | ** variable contains content under the control of the remote |
| 2579 | ** user and presents a risk of XSS or SQL-injection attacks. |
| 2580 | */ |
| 2581 | void Th_StoreUnsafe(const char *zName, const char *zValue){ |
| 2582 | Th_FossilInit(TH_INIT_DEFAULT); |
| 2583 | if( zValue ){ |
| 2584 | if( g.thTrace ){ |
| 2585 | Th_Trace("set %h [taint {%h}]<br>\n", zName, zValue); |
| 2586 | } |
| 2587 | Th_SetVar(g.interp, zName, -1, zValue, TH1_ADD_TAINT(strlen(zValue))); |
| 2588 | } |
| 2589 | } |
| 2590 | |
| 2591 | /* |
| 2592 | ** Appends an element to a TH1 list value. This function is called by the |
| 2593 | ** transfer subsystem; therefore, it must be very careful to avoid doing |
| 2594 | ** any unnecessary work. To that end, the TH1 subsystem will not be called |
| @@ -2680,10 +2771,11 @@ | |
| 2771 | char *zResult = (char*)Th_GetResult(g.interp, &nResult); |
| 2772 | /* |
| 2773 | ** Make sure that the TH1 script error was not caused by a "missing" |
| 2774 | ** command hook handler as that is not actually an error condition. |
| 2775 | */ |
| 2776 | nResult = TH1_LEN(nResult); |
| 2777 | if( memcmp(zResult, NO_COMMAND_HOOK_ERROR, nResult)!=0 ){ |
| 2778 | sendError(0,zResult, nResult, 0); |
| 2779 | }else{ |
| 2780 | /* |
| 2781 | ** There is no command hook handler "installed". This situation |
| @@ -2767,10 +2859,11 @@ | |
| 2859 | char *zResult = (char*)Th_GetResult(g.interp, &nResult); |
| 2860 | /* |
| 2861 | ** Make sure that the TH1 script error was not caused by a "missing" |
| 2862 | ** webpage hook handler as that is not actually an error condition. |
| 2863 | */ |
| 2864 | nResult = TH1_LEN(nResult); |
| 2865 | if( memcmp(zResult, NO_WEBPAGE_HOOK_ERROR, nResult)!=0 ){ |
| 2866 | sendError(0,zResult, nResult, 1); |
| 2867 | }else{ |
| 2868 | /* |
| 2869 | ** There is no webpage hook handler "installed". This situation |
| @@ -2894,11 +2987,16 @@ | |
| 2987 | } |
| 2988 | rc = Th_GetVar(g.interp, (char*)zVar, nVar); |
| 2989 | z += i+1+n; |
| 2990 | i = 0; |
| 2991 | zResult = (char*)Th_GetResult(g.interp, &n); |
| 2992 | if( !TH1_TAINTED(n) |
| 2993 | || encode |
| 2994 | || Th_ReportTaint(g.interp, "inline variable", zVar, nVar)==TH_OK |
| 2995 | ){ |
| 2996 | sendText(pOut,(char*)zResult, n, encode); |
| 2997 | } |
| 2998 | }else if( z[i]=='<' && isBeginScriptTag(&z[i]) ){ |
| 2999 | sendText(pOut,z, i, 0); |
| 3000 | z += i+5; |
| 3001 | for(i=0; z[i] && (z[i]!='<' || !isEndScriptTag(&z[i])); i++){} |
| 3002 | if( g.thTrace ){ |
| @@ -2907,11 +3005,11 @@ | |
| 3005 | rc = Th_Eval(g.interp, 0, (const char*)z, i); |
| 3006 | if( g.thTrace ){ |
| 3007 | int nTrRes; |
| 3008 | char *zTrRes = (char*)Th_GetResult(g.interp, &nTrRes); |
| 3009 | Th_Trace("[render_eval] => %h {%#h}<br>\n", |
| 3010 | Th_ReturnCodeName(rc, 0), TH1_LEN(nTrRes), zTrRes); |
| 3011 | } |
| 3012 | if( rc!=TH_OK ) break; |
| 3013 | z += i; |
| 3014 | if( z[0] ){ z += 6; } |
| 3015 | i = 0; |
| @@ -2953,10 +3051,78 @@ | |
| 3051 | ** as appropriate. We need to pass on g.th1Flags for the case of |
| 3052 | ** recursive calls, so that, e.g., TH_INIT_NO_ENCODE does not get |
| 3053 | ** inadvertently toggled off by a recursive call. |
| 3054 | */; |
| 3055 | } |
| 3056 | |
| 3057 | /* |
| 3058 | ** SETTING: vuln-report width=8 default=log |
| 3059 | ** |
| 3060 | ** This setting controls Fossil's behavior when it encounters a potential |
| 3061 | ** XSS or SQL-injection vulnerability due to misuse of TH1 configuration |
| 3062 | ** scripts. Choices are: |
| 3063 | ** |
| 3064 | ** off Do nothing. Ignore the vulnerability. |
| 3065 | ** |
| 3066 | ** log Write a report of the problem into the error log. |
| 3067 | ** |
| 3068 | ** block Like "log" but also prevent the offending TH1 command |
| 3069 | ** from running. |
| 3070 | ** |
| 3071 | ** fatal Render an error message page instead of the requested |
| 3072 | ** page. |
| 3073 | */ |
| 3074 | |
| 3075 | /* |
| 3076 | ** Report misuse of a tainted string in TH1. |
| 3077 | ** |
| 3078 | ** The behavior depends on the vuln-report setting. If "off", this routine |
| 3079 | ** is a no-op. Otherwise, right a message into the error log. If |
| 3080 | ** vuln-report is "log", that is all that happens. But for any other |
| 3081 | ** value of vuln-report, a fatal error is raised. |
| 3082 | */ |
| 3083 | int Th_ReportTaint( |
| 3084 | Th_Interp *interp, /* Report error here, if an error is reported */ |
| 3085 | const char *zWhere, /* Where the tainted string appears */ |
| 3086 | const char *zStr, /* The tainted string */ |
| 3087 | int nStr /* Length of the tainted string */ |
| 3088 | ){ |
| 3089 | static const char *zDisp = 0; /* Dispensation; what to do with the error */ |
| 3090 | const char *zVulnType; /* Type of vulnerability */ |
| 3091 | |
| 3092 | if( zDisp==0 ) zDisp = db_get("vuln-report","log"); |
| 3093 | if( is_false(zDisp) ) return 0; |
| 3094 | if( strstr(zWhere,"SQL")!=0 ){ |
| 3095 | zVulnType = "SQL-injection"; |
| 3096 | }else{ |
| 3097 | zVulnType = "XSS"; |
| 3098 | } |
| 3099 | nStr = TH1_LEN(nStr); |
| 3100 | fossil_errorlog("possible TH1 %s vulnerability due to tainted %s: \"%.*s\"", |
| 3101 | zVulnType, zWhere, nStr, zStr); |
| 3102 | if( strcmp(zDisp,"log")==0 ){ |
| 3103 | return 0; |
| 3104 | } |
| 3105 | if( strcmp(zDisp,"block")==0 ){ |
| 3106 | char *z = mprintf("tainted %s: \"", zWhere); |
| 3107 | Th_ErrorMessage(interp, z, zStr, nStr); |
| 3108 | fossil_free(z); |
| 3109 | }else{ |
| 3110 | char *z = mprintf("%#h", nStr, zStr); |
| 3111 | zDisp = "off"; |
| 3112 | cgi_reset_content(); |
| 3113 | style_submenu_enable(0); |
| 3114 | style_set_current_feature("error"); |
| 3115 | style_header("Configuration Error"); |
| 3116 | @ <p>Error in a TH1 configuration script: |
| 3117 | @ tainted %h(zWhere): "%z(z)" |
| 3118 | style_finish_page(); |
| 3119 | cgi_reply(); |
| 3120 | fossil_exit(1); |
| 3121 | } |
| 3122 | return 1; |
| 3123 | } |
| 3124 | |
| 3125 | /* |
| 3126 | ** COMMAND: test-th-render |
| 3127 | ** |
| 3128 | ** Usage: %fossil test-th-render FILE |
| @@ -2992,10 +3158,11 @@ | |
| 3158 | if( find_option("set-user-caps", 0, 0)!=0 ){ |
| 3159 | const char *zCap = fossil_getenv("TH1_TEST_USER_CAPS"); |
| 3160 | login_set_capabilities(zCap ? zCap : "sx", 0); |
| 3161 | g.useLocalauth = 1; |
| 3162 | } |
| 3163 | db_find_and_open_repository(OPEN_OK_NOT_FOUND|OPEN_SUBSTITUTE,0); |
| 3164 | verify_all_options(); |
| 3165 | if( g.argc<3 ){ |
| 3166 | usage("FILE"); |
| 3167 | } |
| 3168 | blob_zero(&in); |
| @@ -3044,10 +3211,11 @@ | |
| 3211 | if( find_option("set-user-caps", 0, 0)!=0 ){ |
| 3212 | const char *zCap = fossil_getenv("TH1_TEST_USER_CAPS"); |
| 3213 | login_set_capabilities(zCap ? zCap : "sx", 0); |
| 3214 | g.useLocalauth = 1; |
| 3215 | } |
| 3216 | db_find_and_open_repository(OPEN_OK_NOT_FOUND|OPEN_SUBSTITUTE,0); |
| 3217 | verify_all_options(); |
| 3218 | if( g.argc!=3 ){ |
| 3219 | usage("script"); |
| 3220 | } |
| 3221 | if(file_isfile(g.argv[2], ExtFILE)){ |
| 3222 |
+27
-89
| --- src/th_tcl.c | ||
| +++ src/th_tcl.c | ||
| @@ -41,16 +41,16 @@ | ||
| 41 | 41 | #define USE_ARGV_TO_OBJV() \ |
| 42 | 42 | int objc; \ |
| 43 | 43 | Tcl_Obj **objv; \ |
| 44 | 44 | int obji; |
| 45 | 45 | |
| 46 | -#define COPY_ARGV_TO_OBJV() \ | |
| 47 | - objc = argc-1; \ | |
| 48 | - objv = (Tcl_Obj **)ckalloc((unsigned)(objc * sizeof(Tcl_Obj *))); \ | |
| 49 | - for(obji=1; obji<argc; obji++){ \ | |
| 50 | - objv[obji-1] = Tcl_NewStringObj(argv[obji], argl[obji]); \ | |
| 51 | - Tcl_IncrRefCount(objv[obji-1]); \ | |
| 46 | +#define COPY_ARGV_TO_OBJV() \ | |
| 47 | + objc = argc-1; \ | |
| 48 | + objv = (Tcl_Obj **)ckalloc((unsigned)(objc * sizeof(Tcl_Obj *))); \ | |
| 49 | + for(obji=1; obji<argc; obji++){ \ | |
| 50 | + objv[obji-1] = Tcl_NewStringObj(argv[obji], TH1_LEN(argl[obji])); \ | |
| 51 | + Tcl_IncrRefCount(objv[obji-1]); \ | |
| 52 | 52 | } |
| 53 | 53 | |
| 54 | 54 | #define FREE_ARGV_TO_OBJV() \ |
| 55 | 55 | for(obji=1; obji<argc; obji++){ \ |
| 56 | 56 | Tcl_DecrRefCount(objv[obji-1]); \ |
| @@ -183,11 +183,11 @@ | ||
| 183 | 183 | ** the only Tcl API functions that MUST be called prior to being able to call |
| 184 | 184 | ** Tcl_InitStubs (i.e. because it requires a Tcl interpreter). For complete |
| 185 | 185 | ** cleanup if the Tcl stubs initialization fails somehow, the Tcl_DeleteInterp |
| 186 | 186 | ** and Tcl_Finalize function types are also required. |
| 187 | 187 | */ |
| 188 | -typedef void (tcl_FindExecutableProc) (const char *); | |
| 188 | +typedef const char *(tcl_FindExecutableProc) (const char *); | |
| 189 | 189 | typedef Tcl_Interp *(tcl_CreateInterpProc) (void); |
| 190 | 190 | typedef void (tcl_DeleteInterpProc) (Tcl_Interp *); |
| 191 | 191 | typedef void (tcl_FinalizeProc) (void); |
| 192 | 192 | |
| 193 | 193 | /* |
| @@ -321,27 +321,10 @@ | ||
| 321 | 321 | ** by the caller. This must be declared here because quite a few functions in |
| 322 | 322 | ** this file need to use it before it can be defined. |
| 323 | 323 | */ |
| 324 | 324 | static int createTclInterp(Th_Interp *interp, void *pContext); |
| 325 | 325 | |
| 326 | -/* | |
| 327 | -** Returns the TH1 return code corresponding to the specified Tcl | |
| 328 | -** return code. | |
| 329 | -*/ | |
| 330 | -static int getTh1ReturnCode( | |
| 331 | - int rc /* The Tcl return code value to convert. */ | |
| 332 | -){ | |
| 333 | - switch( rc ){ | |
| 334 | - case /*0*/ TCL_OK: return /*0*/ TH_OK; | |
| 335 | - case /*1*/ TCL_ERROR: return /*1*/ TH_ERROR; | |
| 336 | - case /*2*/ TCL_RETURN: return /*3*/ TH_RETURN; | |
| 337 | - case /*3*/ TCL_BREAK: return /*2*/ TH_BREAK; | |
| 338 | - case /*4*/ TCL_CONTINUE: return /*4*/ TH_CONTINUE; | |
| 339 | - default /*?*/: return /*?*/ rc; | |
| 340 | - } | |
| 341 | -} | |
| 342 | - | |
| 343 | 326 | /* |
| 344 | 327 | ** Returns the Tcl return code corresponding to the specified TH1 |
| 345 | 328 | ** return code. |
| 346 | 329 | */ |
| 347 | 330 | static int getTclReturnCode( |
| @@ -387,10 +370,12 @@ | ||
| 387 | 370 | static char *getTclResult( |
| 388 | 371 | Tcl_Interp *pInterp, |
| 389 | 372 | int *pN |
| 390 | 373 | ){ |
| 391 | 374 | Tcl_Obj *resultPtr; |
| 375 | + Tcl_Size n; | |
| 376 | + char *zRes; | |
| 392 | 377 | |
| 393 | 378 | if( !pInterp ){ /* This should not happen. */ |
| 394 | 379 | if( pN ) *pN = 0; |
| 395 | 380 | return 0; |
| 396 | 381 | } |
| @@ -397,11 +382,13 @@ | ||
| 397 | 382 | resultPtr = Tcl_GetObjResult(pInterp); |
| 398 | 383 | if( !resultPtr ){ /* This should not happen either? */ |
| 399 | 384 | if( pN ) *pN = 0; |
| 400 | 385 | return 0; |
| 401 | 386 | } |
| 402 | - return Tcl_GetStringFromObj(resultPtr, pN); | |
| 387 | + zRes = Tcl_GetStringFromObj(resultPtr, &n); | |
| 388 | + *pN = (int)n; | |
| 389 | + return zRes; | |
| 403 | 390 | } |
| 404 | 391 | |
| 405 | 392 | /* |
| 406 | 393 | ** Tcl context information used by TH1. This structure definition has been |
| 407 | 394 | ** copied from and should be kept in sync with the one in "main.c". |
| @@ -416,48 +403,12 @@ | ||
| 416 | 403 | tcl_FinalizeProc *xFinalize; /* Tcl_Finalize() pointer. */ |
| 417 | 404 | Tcl_Interp *interp; /* The on-demand created Tcl interpreter. */ |
| 418 | 405 | int useObjProc; /* Non-zero if an objProc can be called directly. */ |
| 419 | 406 | int useTip285; /* Non-zero if TIP #285 is available. */ |
| 420 | 407 | const char *setup; /* The optional Tcl setup script. */ |
| 421 | - tcl_NotifyProc *xPreEval; /* Optional, called before Tcl_Eval*(). */ | |
| 422 | - void *pPreContext; /* Optional, provided to xPreEval(). */ | |
| 423 | - tcl_NotifyProc *xPostEval; /* Optional, called after Tcl_Eval*(). */ | |
| 424 | - void *pPostContext; /* Optional, provided to xPostEval(). */ | |
| 425 | 408 | }; |
| 426 | 409 | |
| 427 | -/* | |
| 428 | -** This function calls the configured xPreEval or xPostEval functions, if any. | |
| 429 | -** May have arbitrary side-effects. This function returns the result of the | |
| 430 | -** called notification function or the value of the rc argument if there is no | |
| 431 | -** notification function configured. | |
| 432 | -*/ | |
| 433 | -static int notifyPreOrPostEval( | |
| 434 | - int bIsPost, | |
| 435 | - Th_Interp *interp, | |
| 436 | - void *ctx, | |
| 437 | - int argc, | |
| 438 | - const char **argv, | |
| 439 | - int *argl, | |
| 440 | - int rc | |
| 441 | -){ | |
| 442 | - struct TclContext *tclContext = (struct TclContext *)ctx; | |
| 443 | - tcl_NotifyProc *xNotifyProc; | |
| 444 | - | |
| 445 | - if( !tclContext ){ | |
| 446 | - Th_ErrorMessage(interp, | |
| 447 | - "invalid Tcl context", (const char *)"", 0); | |
| 448 | - return TH_ERROR; | |
| 449 | - } | |
| 450 | - xNotifyProc = bIsPost ? tclContext->xPostEval : tclContext->xPreEval; | |
| 451 | - if( xNotifyProc ){ | |
| 452 | - rc = xNotifyProc(bIsPost ? | |
| 453 | - tclContext->pPostContext : tclContext->pPreContext, | |
| 454 | - interp, ctx, argc, argv, argl, rc); | |
| 455 | - } | |
| 456 | - return rc; | |
| 457 | -} | |
| 458 | - | |
| 459 | 410 | /* |
| 460 | 411 | ** TH1 command: tclEval arg ?arg ...? |
| 461 | 412 | ** |
| 462 | 413 | ** Evaluates the Tcl script and returns its result verbatim. If a Tcl script |
| 463 | 414 | ** error is generated, it will be transformed into a TH1 script error. The |
| @@ -485,17 +436,13 @@ | ||
| 485 | 436 | tclInterp = GET_CTX_TCL_INTERP(ctx); |
| 486 | 437 | if( !tclInterp || Tcl_InterpDeleted(tclInterp) ){ |
| 487 | 438 | Th_ErrorMessage(interp, "invalid Tcl interpreter", (const char *)"", 0); |
| 488 | 439 | return TH_ERROR; |
| 489 | 440 | } |
| 490 | - rc = notifyPreOrPostEval(0, interp, ctx, argc, argv, argl, rc); | |
| 491 | - if( rc!=TH_OK ){ | |
| 492 | - return rc; | |
| 493 | - } | |
| 494 | 441 | Tcl_Preserve((ClientData)tclInterp); |
| 495 | 442 | if( argc==2 ){ |
| 496 | - objPtr = Tcl_NewStringObj(argv[1], argl[1]); | |
| 443 | + objPtr = Tcl_NewStringObj(argv[1], TH1_LEN(argl[1])); | |
| 497 | 444 | Tcl_IncrRefCount(objPtr); |
| 498 | 445 | rc = Tcl_EvalObjEx(tclInterp, objPtr, 0); |
| 499 | 446 | Tcl_DecrRefCount(objPtr); objPtr = 0; |
| 500 | 447 | }else{ |
| 501 | 448 | USE_ARGV_TO_OBJV(); |
| @@ -507,12 +454,10 @@ | ||
| 507 | 454 | FREE_ARGV_TO_OBJV(); |
| 508 | 455 | } |
| 509 | 456 | zResult = getTclResult(tclInterp, &nResult); |
| 510 | 457 | Th_SetResult(interp, zResult, nResult); |
| 511 | 458 | Tcl_Release((ClientData)tclInterp); |
| 512 | - rc = notifyPreOrPostEval(1, interp, ctx, argc, argv, argl, | |
| 513 | - getTh1ReturnCode(rc)); | |
| 514 | 459 | return rc; |
| 515 | 460 | } |
| 516 | 461 | |
| 517 | 462 | /* |
| 518 | 463 | ** TH1 command: tclExpr arg ?arg ...? |
| @@ -545,17 +490,13 @@ | ||
| 545 | 490 | tclInterp = GET_CTX_TCL_INTERP(ctx); |
| 546 | 491 | if( !tclInterp || Tcl_InterpDeleted(tclInterp) ){ |
| 547 | 492 | Th_ErrorMessage(interp, "invalid Tcl interpreter", (const char *)"", 0); |
| 548 | 493 | return TH_ERROR; |
| 549 | 494 | } |
| 550 | - rc = notifyPreOrPostEval(0, interp, ctx, argc, argv, argl, rc); | |
| 551 | - if( rc!=TH_OK ){ | |
| 552 | - return rc; | |
| 553 | - } | |
| 554 | 495 | Tcl_Preserve((ClientData)tclInterp); |
| 555 | 496 | if( argc==2 ){ |
| 556 | - objPtr = Tcl_NewStringObj(argv[1], argl[1]); | |
| 497 | + objPtr = Tcl_NewStringObj(argv[1], TH1_LEN(argl[1])); | |
| 557 | 498 | Tcl_IncrRefCount(objPtr); |
| 558 | 499 | rc = Tcl_ExprObj(tclInterp, objPtr, &resultObjPtr); |
| 559 | 500 | Tcl_DecrRefCount(objPtr); objPtr = 0; |
| 560 | 501 | }else{ |
| 561 | 502 | USE_ARGV_TO_OBJV(); |
| @@ -565,21 +506,21 @@ | ||
| 565 | 506 | rc = Tcl_ExprObj(tclInterp, objPtr, &resultObjPtr); |
| 566 | 507 | Tcl_DecrRefCount(objPtr); objPtr = 0; |
| 567 | 508 | FREE_ARGV_TO_OBJV(); |
| 568 | 509 | } |
| 569 | 510 | if( rc==TCL_OK ){ |
| 570 | - zResult = Tcl_GetStringFromObj(resultObjPtr, &nResult); | |
| 511 | + Tcl_Size szResult = 0; | |
| 512 | + zResult = Tcl_GetStringFromObj(resultObjPtr, &szResult); | |
| 513 | + nResult = (int)szResult; | |
| 571 | 514 | }else{ |
| 572 | 515 | zResult = getTclResult(tclInterp, &nResult); |
| 573 | 516 | } |
| 574 | - Th_SetResult(interp, zResult, nResult); | |
| 517 | + Th_SetResult(interp, zResult, (int)nResult); | |
| 575 | 518 | if( rc==TCL_OK ){ |
| 576 | 519 | Tcl_DecrRefCount(resultObjPtr); resultObjPtr = 0; |
| 577 | 520 | } |
| 578 | 521 | Tcl_Release((ClientData)tclInterp); |
| 579 | - rc = notifyPreOrPostEval(1, interp, ctx, argc, argv, argl, | |
| 580 | - getTh1ReturnCode(rc)); | |
| 581 | 522 | return rc; |
| 582 | 523 | } |
| 583 | 524 | |
| 584 | 525 | /* |
| 585 | 526 | ** TH1 command: tclInvoke command ?arg ...? |
| @@ -610,20 +551,16 @@ | ||
| 610 | 551 | tclInterp = GET_CTX_TCL_INTERP(ctx); |
| 611 | 552 | if( !tclInterp || Tcl_InterpDeleted(tclInterp) ){ |
| 612 | 553 | Th_ErrorMessage(interp, "invalid Tcl interpreter", (const char *)"", 0); |
| 613 | 554 | return TH_ERROR; |
| 614 | 555 | } |
| 615 | - rc = notifyPreOrPostEval(0, interp, ctx, argc, argv, argl, rc); | |
| 616 | - if( rc!=TH_OK ){ | |
| 617 | - return rc; | |
| 618 | - } | |
| 619 | 556 | Tcl_Preserve((ClientData)tclInterp); |
| 620 | 557 | #if !defined(USE_TCL_EVALOBJV) || !USE_TCL_EVALOBJV |
| 621 | 558 | if( GET_CTX_TCL_USEOBJPROC(ctx) ){ |
| 622 | 559 | Tcl_Command command; |
| 623 | 560 | Tcl_CmdInfo cmdInfo; |
| 624 | - Tcl_Obj *objPtr = Tcl_NewStringObj(argv[1], argl[1]); | |
| 561 | + Tcl_Obj *objPtr = Tcl_NewStringObj(argv[1], TH1_LEN(argl[1])); | |
| 625 | 562 | Tcl_IncrRefCount(objPtr); |
| 626 | 563 | command = Tcl_GetCommandFromObj(tclInterp, objPtr); |
| 627 | 564 | if( !command || Tcl_GetCommandInfoFromToken(command, &cmdInfo)==0 ){ |
| 628 | 565 | Th_ErrorMessage(interp, "Tcl command not found:", argv[1], argl[1]); |
| 629 | 566 | Tcl_DecrRefCount(objPtr); objPtr = 0; |
| @@ -649,12 +586,10 @@ | ||
| 649 | 586 | FREE_ARGV_TO_OBJV(); |
| 650 | 587 | } |
| 651 | 588 | zResult = getTclResult(tclInterp, &nResult); |
| 652 | 589 | Th_SetResult(interp, zResult, nResult); |
| 653 | 590 | Tcl_Release((ClientData)tclInterp); |
| 654 | - rc = notifyPreOrPostEval(1, interp, ctx, argc, argv, argl, | |
| 655 | - getTh1ReturnCode(rc)); | |
| 656 | 591 | return rc; |
| 657 | 592 | } |
| 658 | 593 | |
| 659 | 594 | /* |
| 660 | 595 | ** TH1 command: tclIsSafe |
| @@ -767,10 +702,11 @@ | ||
| 767 | 702 | int objc, |
| 768 | 703 | Tcl_Obj *const objv[] |
| 769 | 704 | ){ |
| 770 | 705 | Th_Interp *th1Interp; |
| 771 | 706 | int nArg; |
| 707 | + Tcl_Size szArg; | |
| 772 | 708 | const char *arg; |
| 773 | 709 | int rc; |
| 774 | 710 | |
| 775 | 711 | if( objc!=2 ){ |
| 776 | 712 | Tcl_WrongNumArgs(interp, 1, objv, "arg"); |
| @@ -779,14 +715,15 @@ | ||
| 779 | 715 | th1Interp = (Th_Interp *)clientData; |
| 780 | 716 | if( !th1Interp ){ |
| 781 | 717 | Tcl_AppendResult(interp, "invalid TH1 interpreter", NULL); |
| 782 | 718 | return TCL_ERROR; |
| 783 | 719 | } |
| 784 | - arg = Tcl_GetStringFromObj(objv[1], &nArg); | |
| 720 | + arg = Tcl_GetStringFromObj(objv[1], &szArg); | |
| 721 | + nArg = (int)szArg; | |
| 785 | 722 | rc = Th_Eval(th1Interp, 0, arg, nArg); |
| 786 | 723 | arg = Th_GetResult(th1Interp, &nArg); |
| 787 | - Tcl_SetObjResult(interp, Tcl_NewStringObj(arg, nArg)); | |
| 724 | + Tcl_SetObjResult(interp, Tcl_NewStringObj(arg, TH1_LEN(nArg))); | |
| 788 | 725 | return getTclReturnCode(rc); |
| 789 | 726 | } |
| 790 | 727 | |
| 791 | 728 | /* |
| 792 | 729 | ** Tcl command: th1Expr arg |
| @@ -800,10 +737,11 @@ | ||
| 800 | 737 | int objc, |
| 801 | 738 | Tcl_Obj *const objv[] |
| 802 | 739 | ){ |
| 803 | 740 | Th_Interp *th1Interp; |
| 804 | 741 | int nArg; |
| 742 | + Tcl_Size szArg; | |
| 805 | 743 | const char *arg; |
| 806 | 744 | int rc; |
| 807 | 745 | |
| 808 | 746 | if( objc!=2 ){ |
| 809 | 747 | Tcl_WrongNumArgs(interp, 1, objv, "arg"); |
| @@ -812,14 +750,14 @@ | ||
| 812 | 750 | th1Interp = (Th_Interp *)clientData; |
| 813 | 751 | if( !th1Interp ){ |
| 814 | 752 | Tcl_AppendResult(interp, "invalid TH1 interpreter", NULL); |
| 815 | 753 | return TCL_ERROR; |
| 816 | 754 | } |
| 817 | - arg = Tcl_GetStringFromObj(objv[1], &nArg); | |
| 818 | - rc = Th_Expr(th1Interp, arg, nArg); | |
| 755 | + arg = Tcl_GetStringFromObj(objv[1], &szArg); | |
| 756 | + rc = Th_Expr(th1Interp, arg, (int)szArg); | |
| 819 | 757 | arg = Th_GetResult(th1Interp, &nArg); |
| 820 | - Tcl_SetObjResult(interp, Tcl_NewStringObj(arg, nArg)); | |
| 758 | + Tcl_SetObjResult(interp, Tcl_NewStringObj(arg, TH1_LEN(nArg))); | |
| 821 | 759 | return getTclReturnCode(rc); |
| 822 | 760 | } |
| 823 | 761 | |
| 824 | 762 | /* |
| 825 | 763 | ** Array of Tcl integration commands. Used when adding or removing the Tcl |
| 826 | 764 |
| --- src/th_tcl.c | |
| +++ src/th_tcl.c | |
| @@ -41,16 +41,16 @@ | |
| 41 | #define USE_ARGV_TO_OBJV() \ |
| 42 | int objc; \ |
| 43 | Tcl_Obj **objv; \ |
| 44 | int obji; |
| 45 | |
| 46 | #define COPY_ARGV_TO_OBJV() \ |
| 47 | objc = argc-1; \ |
| 48 | objv = (Tcl_Obj **)ckalloc((unsigned)(objc * sizeof(Tcl_Obj *))); \ |
| 49 | for(obji=1; obji<argc; obji++){ \ |
| 50 | objv[obji-1] = Tcl_NewStringObj(argv[obji], argl[obji]); \ |
| 51 | Tcl_IncrRefCount(objv[obji-1]); \ |
| 52 | } |
| 53 | |
| 54 | #define FREE_ARGV_TO_OBJV() \ |
| 55 | for(obji=1; obji<argc; obji++){ \ |
| 56 | Tcl_DecrRefCount(objv[obji-1]); \ |
| @@ -183,11 +183,11 @@ | |
| 183 | ** the only Tcl API functions that MUST be called prior to being able to call |
| 184 | ** Tcl_InitStubs (i.e. because it requires a Tcl interpreter). For complete |
| 185 | ** cleanup if the Tcl stubs initialization fails somehow, the Tcl_DeleteInterp |
| 186 | ** and Tcl_Finalize function types are also required. |
| 187 | */ |
| 188 | typedef void (tcl_FindExecutableProc) (const char *); |
| 189 | typedef Tcl_Interp *(tcl_CreateInterpProc) (void); |
| 190 | typedef void (tcl_DeleteInterpProc) (Tcl_Interp *); |
| 191 | typedef void (tcl_FinalizeProc) (void); |
| 192 | |
| 193 | /* |
| @@ -321,27 +321,10 @@ | |
| 321 | ** by the caller. This must be declared here because quite a few functions in |
| 322 | ** this file need to use it before it can be defined. |
| 323 | */ |
| 324 | static int createTclInterp(Th_Interp *interp, void *pContext); |
| 325 | |
| 326 | /* |
| 327 | ** Returns the TH1 return code corresponding to the specified Tcl |
| 328 | ** return code. |
| 329 | */ |
| 330 | static int getTh1ReturnCode( |
| 331 | int rc /* The Tcl return code value to convert. */ |
| 332 | ){ |
| 333 | switch( rc ){ |
| 334 | case /*0*/ TCL_OK: return /*0*/ TH_OK; |
| 335 | case /*1*/ TCL_ERROR: return /*1*/ TH_ERROR; |
| 336 | case /*2*/ TCL_RETURN: return /*3*/ TH_RETURN; |
| 337 | case /*3*/ TCL_BREAK: return /*2*/ TH_BREAK; |
| 338 | case /*4*/ TCL_CONTINUE: return /*4*/ TH_CONTINUE; |
| 339 | default /*?*/: return /*?*/ rc; |
| 340 | } |
| 341 | } |
| 342 | |
| 343 | /* |
| 344 | ** Returns the Tcl return code corresponding to the specified TH1 |
| 345 | ** return code. |
| 346 | */ |
| 347 | static int getTclReturnCode( |
| @@ -387,10 +370,12 @@ | |
| 387 | static char *getTclResult( |
| 388 | Tcl_Interp *pInterp, |
| 389 | int *pN |
| 390 | ){ |
| 391 | Tcl_Obj *resultPtr; |
| 392 | |
| 393 | if( !pInterp ){ /* This should not happen. */ |
| 394 | if( pN ) *pN = 0; |
| 395 | return 0; |
| 396 | } |
| @@ -397,11 +382,13 @@ | |
| 397 | resultPtr = Tcl_GetObjResult(pInterp); |
| 398 | if( !resultPtr ){ /* This should not happen either? */ |
| 399 | if( pN ) *pN = 0; |
| 400 | return 0; |
| 401 | } |
| 402 | return Tcl_GetStringFromObj(resultPtr, pN); |
| 403 | } |
| 404 | |
| 405 | /* |
| 406 | ** Tcl context information used by TH1. This structure definition has been |
| 407 | ** copied from and should be kept in sync with the one in "main.c". |
| @@ -416,48 +403,12 @@ | |
| 416 | tcl_FinalizeProc *xFinalize; /* Tcl_Finalize() pointer. */ |
| 417 | Tcl_Interp *interp; /* The on-demand created Tcl interpreter. */ |
| 418 | int useObjProc; /* Non-zero if an objProc can be called directly. */ |
| 419 | int useTip285; /* Non-zero if TIP #285 is available. */ |
| 420 | const char *setup; /* The optional Tcl setup script. */ |
| 421 | tcl_NotifyProc *xPreEval; /* Optional, called before Tcl_Eval*(). */ |
| 422 | void *pPreContext; /* Optional, provided to xPreEval(). */ |
| 423 | tcl_NotifyProc *xPostEval; /* Optional, called after Tcl_Eval*(). */ |
| 424 | void *pPostContext; /* Optional, provided to xPostEval(). */ |
| 425 | }; |
| 426 | |
| 427 | /* |
| 428 | ** This function calls the configured xPreEval or xPostEval functions, if any. |
| 429 | ** May have arbitrary side-effects. This function returns the result of the |
| 430 | ** called notification function or the value of the rc argument if there is no |
| 431 | ** notification function configured. |
| 432 | */ |
| 433 | static int notifyPreOrPostEval( |
| 434 | int bIsPost, |
| 435 | Th_Interp *interp, |
| 436 | void *ctx, |
| 437 | int argc, |
| 438 | const char **argv, |
| 439 | int *argl, |
| 440 | int rc |
| 441 | ){ |
| 442 | struct TclContext *tclContext = (struct TclContext *)ctx; |
| 443 | tcl_NotifyProc *xNotifyProc; |
| 444 | |
| 445 | if( !tclContext ){ |
| 446 | Th_ErrorMessage(interp, |
| 447 | "invalid Tcl context", (const char *)"", 0); |
| 448 | return TH_ERROR; |
| 449 | } |
| 450 | xNotifyProc = bIsPost ? tclContext->xPostEval : tclContext->xPreEval; |
| 451 | if( xNotifyProc ){ |
| 452 | rc = xNotifyProc(bIsPost ? |
| 453 | tclContext->pPostContext : tclContext->pPreContext, |
| 454 | interp, ctx, argc, argv, argl, rc); |
| 455 | } |
| 456 | return rc; |
| 457 | } |
| 458 | |
| 459 | /* |
| 460 | ** TH1 command: tclEval arg ?arg ...? |
| 461 | ** |
| 462 | ** Evaluates the Tcl script and returns its result verbatim. If a Tcl script |
| 463 | ** error is generated, it will be transformed into a TH1 script error. The |
| @@ -485,17 +436,13 @@ | |
| 485 | tclInterp = GET_CTX_TCL_INTERP(ctx); |
| 486 | if( !tclInterp || Tcl_InterpDeleted(tclInterp) ){ |
| 487 | Th_ErrorMessage(interp, "invalid Tcl interpreter", (const char *)"", 0); |
| 488 | return TH_ERROR; |
| 489 | } |
| 490 | rc = notifyPreOrPostEval(0, interp, ctx, argc, argv, argl, rc); |
| 491 | if( rc!=TH_OK ){ |
| 492 | return rc; |
| 493 | } |
| 494 | Tcl_Preserve((ClientData)tclInterp); |
| 495 | if( argc==2 ){ |
| 496 | objPtr = Tcl_NewStringObj(argv[1], argl[1]); |
| 497 | Tcl_IncrRefCount(objPtr); |
| 498 | rc = Tcl_EvalObjEx(tclInterp, objPtr, 0); |
| 499 | Tcl_DecrRefCount(objPtr); objPtr = 0; |
| 500 | }else{ |
| 501 | USE_ARGV_TO_OBJV(); |
| @@ -507,12 +454,10 @@ | |
| 507 | FREE_ARGV_TO_OBJV(); |
| 508 | } |
| 509 | zResult = getTclResult(tclInterp, &nResult); |
| 510 | Th_SetResult(interp, zResult, nResult); |
| 511 | Tcl_Release((ClientData)tclInterp); |
| 512 | rc = notifyPreOrPostEval(1, interp, ctx, argc, argv, argl, |
| 513 | getTh1ReturnCode(rc)); |
| 514 | return rc; |
| 515 | } |
| 516 | |
| 517 | /* |
| 518 | ** TH1 command: tclExpr arg ?arg ...? |
| @@ -545,17 +490,13 @@ | |
| 545 | tclInterp = GET_CTX_TCL_INTERP(ctx); |
| 546 | if( !tclInterp || Tcl_InterpDeleted(tclInterp) ){ |
| 547 | Th_ErrorMessage(interp, "invalid Tcl interpreter", (const char *)"", 0); |
| 548 | return TH_ERROR; |
| 549 | } |
| 550 | rc = notifyPreOrPostEval(0, interp, ctx, argc, argv, argl, rc); |
| 551 | if( rc!=TH_OK ){ |
| 552 | return rc; |
| 553 | } |
| 554 | Tcl_Preserve((ClientData)tclInterp); |
| 555 | if( argc==2 ){ |
| 556 | objPtr = Tcl_NewStringObj(argv[1], argl[1]); |
| 557 | Tcl_IncrRefCount(objPtr); |
| 558 | rc = Tcl_ExprObj(tclInterp, objPtr, &resultObjPtr); |
| 559 | Tcl_DecrRefCount(objPtr); objPtr = 0; |
| 560 | }else{ |
| 561 | USE_ARGV_TO_OBJV(); |
| @@ -565,21 +506,21 @@ | |
| 565 | rc = Tcl_ExprObj(tclInterp, objPtr, &resultObjPtr); |
| 566 | Tcl_DecrRefCount(objPtr); objPtr = 0; |
| 567 | FREE_ARGV_TO_OBJV(); |
| 568 | } |
| 569 | if( rc==TCL_OK ){ |
| 570 | zResult = Tcl_GetStringFromObj(resultObjPtr, &nResult); |
| 571 | }else{ |
| 572 | zResult = getTclResult(tclInterp, &nResult); |
| 573 | } |
| 574 | Th_SetResult(interp, zResult, nResult); |
| 575 | if( rc==TCL_OK ){ |
| 576 | Tcl_DecrRefCount(resultObjPtr); resultObjPtr = 0; |
| 577 | } |
| 578 | Tcl_Release((ClientData)tclInterp); |
| 579 | rc = notifyPreOrPostEval(1, interp, ctx, argc, argv, argl, |
| 580 | getTh1ReturnCode(rc)); |
| 581 | return rc; |
| 582 | } |
| 583 | |
| 584 | /* |
| 585 | ** TH1 command: tclInvoke command ?arg ...? |
| @@ -610,20 +551,16 @@ | |
| 610 | tclInterp = GET_CTX_TCL_INTERP(ctx); |
| 611 | if( !tclInterp || Tcl_InterpDeleted(tclInterp) ){ |
| 612 | Th_ErrorMessage(interp, "invalid Tcl interpreter", (const char *)"", 0); |
| 613 | return TH_ERROR; |
| 614 | } |
| 615 | rc = notifyPreOrPostEval(0, interp, ctx, argc, argv, argl, rc); |
| 616 | if( rc!=TH_OK ){ |
| 617 | return rc; |
| 618 | } |
| 619 | Tcl_Preserve((ClientData)tclInterp); |
| 620 | #if !defined(USE_TCL_EVALOBJV) || !USE_TCL_EVALOBJV |
| 621 | if( GET_CTX_TCL_USEOBJPROC(ctx) ){ |
| 622 | Tcl_Command command; |
| 623 | Tcl_CmdInfo cmdInfo; |
| 624 | Tcl_Obj *objPtr = Tcl_NewStringObj(argv[1], argl[1]); |
| 625 | Tcl_IncrRefCount(objPtr); |
| 626 | command = Tcl_GetCommandFromObj(tclInterp, objPtr); |
| 627 | if( !command || Tcl_GetCommandInfoFromToken(command, &cmdInfo)==0 ){ |
| 628 | Th_ErrorMessage(interp, "Tcl command not found:", argv[1], argl[1]); |
| 629 | Tcl_DecrRefCount(objPtr); objPtr = 0; |
| @@ -649,12 +586,10 @@ | |
| 649 | FREE_ARGV_TO_OBJV(); |
| 650 | } |
| 651 | zResult = getTclResult(tclInterp, &nResult); |
| 652 | Th_SetResult(interp, zResult, nResult); |
| 653 | Tcl_Release((ClientData)tclInterp); |
| 654 | rc = notifyPreOrPostEval(1, interp, ctx, argc, argv, argl, |
| 655 | getTh1ReturnCode(rc)); |
| 656 | return rc; |
| 657 | } |
| 658 | |
| 659 | /* |
| 660 | ** TH1 command: tclIsSafe |
| @@ -767,10 +702,11 @@ | |
| 767 | int objc, |
| 768 | Tcl_Obj *const objv[] |
| 769 | ){ |
| 770 | Th_Interp *th1Interp; |
| 771 | int nArg; |
| 772 | const char *arg; |
| 773 | int rc; |
| 774 | |
| 775 | if( objc!=2 ){ |
| 776 | Tcl_WrongNumArgs(interp, 1, objv, "arg"); |
| @@ -779,14 +715,15 @@ | |
| 779 | th1Interp = (Th_Interp *)clientData; |
| 780 | if( !th1Interp ){ |
| 781 | Tcl_AppendResult(interp, "invalid TH1 interpreter", NULL); |
| 782 | return TCL_ERROR; |
| 783 | } |
| 784 | arg = Tcl_GetStringFromObj(objv[1], &nArg); |
| 785 | rc = Th_Eval(th1Interp, 0, arg, nArg); |
| 786 | arg = Th_GetResult(th1Interp, &nArg); |
| 787 | Tcl_SetObjResult(interp, Tcl_NewStringObj(arg, nArg)); |
| 788 | return getTclReturnCode(rc); |
| 789 | } |
| 790 | |
| 791 | /* |
| 792 | ** Tcl command: th1Expr arg |
| @@ -800,10 +737,11 @@ | |
| 800 | int objc, |
| 801 | Tcl_Obj *const objv[] |
| 802 | ){ |
| 803 | Th_Interp *th1Interp; |
| 804 | int nArg; |
| 805 | const char *arg; |
| 806 | int rc; |
| 807 | |
| 808 | if( objc!=2 ){ |
| 809 | Tcl_WrongNumArgs(interp, 1, objv, "arg"); |
| @@ -812,14 +750,14 @@ | |
| 812 | th1Interp = (Th_Interp *)clientData; |
| 813 | if( !th1Interp ){ |
| 814 | Tcl_AppendResult(interp, "invalid TH1 interpreter", NULL); |
| 815 | return TCL_ERROR; |
| 816 | } |
| 817 | arg = Tcl_GetStringFromObj(objv[1], &nArg); |
| 818 | rc = Th_Expr(th1Interp, arg, nArg); |
| 819 | arg = Th_GetResult(th1Interp, &nArg); |
| 820 | Tcl_SetObjResult(interp, Tcl_NewStringObj(arg, nArg)); |
| 821 | return getTclReturnCode(rc); |
| 822 | } |
| 823 | |
| 824 | /* |
| 825 | ** Array of Tcl integration commands. Used when adding or removing the Tcl |
| 826 |
| --- src/th_tcl.c | |
| +++ src/th_tcl.c | |
| @@ -41,16 +41,16 @@ | |
| 41 | #define USE_ARGV_TO_OBJV() \ |
| 42 | int objc; \ |
| 43 | Tcl_Obj **objv; \ |
| 44 | int obji; |
| 45 | |
| 46 | #define COPY_ARGV_TO_OBJV() \ |
| 47 | objc = argc-1; \ |
| 48 | objv = (Tcl_Obj **)ckalloc((unsigned)(objc * sizeof(Tcl_Obj *))); \ |
| 49 | for(obji=1; obji<argc; obji++){ \ |
| 50 | objv[obji-1] = Tcl_NewStringObj(argv[obji], TH1_LEN(argl[obji])); \ |
| 51 | Tcl_IncrRefCount(objv[obji-1]); \ |
| 52 | } |
| 53 | |
| 54 | #define FREE_ARGV_TO_OBJV() \ |
| 55 | for(obji=1; obji<argc; obji++){ \ |
| 56 | Tcl_DecrRefCount(objv[obji-1]); \ |
| @@ -183,11 +183,11 @@ | |
| 183 | ** the only Tcl API functions that MUST be called prior to being able to call |
| 184 | ** Tcl_InitStubs (i.e. because it requires a Tcl interpreter). For complete |
| 185 | ** cleanup if the Tcl stubs initialization fails somehow, the Tcl_DeleteInterp |
| 186 | ** and Tcl_Finalize function types are also required. |
| 187 | */ |
| 188 | typedef const char *(tcl_FindExecutableProc) (const char *); |
| 189 | typedef Tcl_Interp *(tcl_CreateInterpProc) (void); |
| 190 | typedef void (tcl_DeleteInterpProc) (Tcl_Interp *); |
| 191 | typedef void (tcl_FinalizeProc) (void); |
| 192 | |
| 193 | /* |
| @@ -321,27 +321,10 @@ | |
| 321 | ** by the caller. This must be declared here because quite a few functions in |
| 322 | ** this file need to use it before it can be defined. |
| 323 | */ |
| 324 | static int createTclInterp(Th_Interp *interp, void *pContext); |
| 325 | |
| 326 | /* |
| 327 | ** Returns the Tcl return code corresponding to the specified TH1 |
| 328 | ** return code. |
| 329 | */ |
| 330 | static int getTclReturnCode( |
| @@ -387,10 +370,12 @@ | |
| 370 | static char *getTclResult( |
| 371 | Tcl_Interp *pInterp, |
| 372 | int *pN |
| 373 | ){ |
| 374 | Tcl_Obj *resultPtr; |
| 375 | Tcl_Size n; |
| 376 | char *zRes; |
| 377 | |
| 378 | if( !pInterp ){ /* This should not happen. */ |
| 379 | if( pN ) *pN = 0; |
| 380 | return 0; |
| 381 | } |
| @@ -397,11 +382,13 @@ | |
| 382 | resultPtr = Tcl_GetObjResult(pInterp); |
| 383 | if( !resultPtr ){ /* This should not happen either? */ |
| 384 | if( pN ) *pN = 0; |
| 385 | return 0; |
| 386 | } |
| 387 | zRes = Tcl_GetStringFromObj(resultPtr, &n); |
| 388 | *pN = (int)n; |
| 389 | return zRes; |
| 390 | } |
| 391 | |
| 392 | /* |
| 393 | ** Tcl context information used by TH1. This structure definition has been |
| 394 | ** copied from and should be kept in sync with the one in "main.c". |
| @@ -416,48 +403,12 @@ | |
| 403 | tcl_FinalizeProc *xFinalize; /* Tcl_Finalize() pointer. */ |
| 404 | Tcl_Interp *interp; /* The on-demand created Tcl interpreter. */ |
| 405 | int useObjProc; /* Non-zero if an objProc can be called directly. */ |
| 406 | int useTip285; /* Non-zero if TIP #285 is available. */ |
| 407 | const char *setup; /* The optional Tcl setup script. */ |
| 408 | }; |
| 409 | |
| 410 | /* |
| 411 | ** TH1 command: tclEval arg ?arg ...? |
| 412 | ** |
| 413 | ** Evaluates the Tcl script and returns its result verbatim. If a Tcl script |
| 414 | ** error is generated, it will be transformed into a TH1 script error. The |
| @@ -485,17 +436,13 @@ | |
| 436 | tclInterp = GET_CTX_TCL_INTERP(ctx); |
| 437 | if( !tclInterp || Tcl_InterpDeleted(tclInterp) ){ |
| 438 | Th_ErrorMessage(interp, "invalid Tcl interpreter", (const char *)"", 0); |
| 439 | return TH_ERROR; |
| 440 | } |
| 441 | Tcl_Preserve((ClientData)tclInterp); |
| 442 | if( argc==2 ){ |
| 443 | objPtr = Tcl_NewStringObj(argv[1], TH1_LEN(argl[1])); |
| 444 | Tcl_IncrRefCount(objPtr); |
| 445 | rc = Tcl_EvalObjEx(tclInterp, objPtr, 0); |
| 446 | Tcl_DecrRefCount(objPtr); objPtr = 0; |
| 447 | }else{ |
| 448 | USE_ARGV_TO_OBJV(); |
| @@ -507,12 +454,10 @@ | |
| 454 | FREE_ARGV_TO_OBJV(); |
| 455 | } |
| 456 | zResult = getTclResult(tclInterp, &nResult); |
| 457 | Th_SetResult(interp, zResult, nResult); |
| 458 | Tcl_Release((ClientData)tclInterp); |
| 459 | return rc; |
| 460 | } |
| 461 | |
| 462 | /* |
| 463 | ** TH1 command: tclExpr arg ?arg ...? |
| @@ -545,17 +490,13 @@ | |
| 490 | tclInterp = GET_CTX_TCL_INTERP(ctx); |
| 491 | if( !tclInterp || Tcl_InterpDeleted(tclInterp) ){ |
| 492 | Th_ErrorMessage(interp, "invalid Tcl interpreter", (const char *)"", 0); |
| 493 | return TH_ERROR; |
| 494 | } |
| 495 | Tcl_Preserve((ClientData)tclInterp); |
| 496 | if( argc==2 ){ |
| 497 | objPtr = Tcl_NewStringObj(argv[1], TH1_LEN(argl[1])); |
| 498 | Tcl_IncrRefCount(objPtr); |
| 499 | rc = Tcl_ExprObj(tclInterp, objPtr, &resultObjPtr); |
| 500 | Tcl_DecrRefCount(objPtr); objPtr = 0; |
| 501 | }else{ |
| 502 | USE_ARGV_TO_OBJV(); |
| @@ -565,21 +506,21 @@ | |
| 506 | rc = Tcl_ExprObj(tclInterp, objPtr, &resultObjPtr); |
| 507 | Tcl_DecrRefCount(objPtr); objPtr = 0; |
| 508 | FREE_ARGV_TO_OBJV(); |
| 509 | } |
| 510 | if( rc==TCL_OK ){ |
| 511 | Tcl_Size szResult = 0; |
| 512 | zResult = Tcl_GetStringFromObj(resultObjPtr, &szResult); |
| 513 | nResult = (int)szResult; |
| 514 | }else{ |
| 515 | zResult = getTclResult(tclInterp, &nResult); |
| 516 | } |
| 517 | Th_SetResult(interp, zResult, (int)nResult); |
| 518 | if( rc==TCL_OK ){ |
| 519 | Tcl_DecrRefCount(resultObjPtr); resultObjPtr = 0; |
| 520 | } |
| 521 | Tcl_Release((ClientData)tclInterp); |
| 522 | return rc; |
| 523 | } |
| 524 | |
| 525 | /* |
| 526 | ** TH1 command: tclInvoke command ?arg ...? |
| @@ -610,20 +551,16 @@ | |
| 551 | tclInterp = GET_CTX_TCL_INTERP(ctx); |
| 552 | if( !tclInterp || Tcl_InterpDeleted(tclInterp) ){ |
| 553 | Th_ErrorMessage(interp, "invalid Tcl interpreter", (const char *)"", 0); |
| 554 | return TH_ERROR; |
| 555 | } |
| 556 | Tcl_Preserve((ClientData)tclInterp); |
| 557 | #if !defined(USE_TCL_EVALOBJV) || !USE_TCL_EVALOBJV |
| 558 | if( GET_CTX_TCL_USEOBJPROC(ctx) ){ |
| 559 | Tcl_Command command; |
| 560 | Tcl_CmdInfo cmdInfo; |
| 561 | Tcl_Obj *objPtr = Tcl_NewStringObj(argv[1], TH1_LEN(argl[1])); |
| 562 | Tcl_IncrRefCount(objPtr); |
| 563 | command = Tcl_GetCommandFromObj(tclInterp, objPtr); |
| 564 | if( !command || Tcl_GetCommandInfoFromToken(command, &cmdInfo)==0 ){ |
| 565 | Th_ErrorMessage(interp, "Tcl command not found:", argv[1], argl[1]); |
| 566 | Tcl_DecrRefCount(objPtr); objPtr = 0; |
| @@ -649,12 +586,10 @@ | |
| 586 | FREE_ARGV_TO_OBJV(); |
| 587 | } |
| 588 | zResult = getTclResult(tclInterp, &nResult); |
| 589 | Th_SetResult(interp, zResult, nResult); |
| 590 | Tcl_Release((ClientData)tclInterp); |
| 591 | return rc; |
| 592 | } |
| 593 | |
| 594 | /* |
| 595 | ** TH1 command: tclIsSafe |
| @@ -767,10 +702,11 @@ | |
| 702 | int objc, |
| 703 | Tcl_Obj *const objv[] |
| 704 | ){ |
| 705 | Th_Interp *th1Interp; |
| 706 | int nArg; |
| 707 | Tcl_Size szArg; |
| 708 | const char *arg; |
| 709 | int rc; |
| 710 | |
| 711 | if( objc!=2 ){ |
| 712 | Tcl_WrongNumArgs(interp, 1, objv, "arg"); |
| @@ -779,14 +715,15 @@ | |
| 715 | th1Interp = (Th_Interp *)clientData; |
| 716 | if( !th1Interp ){ |
| 717 | Tcl_AppendResult(interp, "invalid TH1 interpreter", NULL); |
| 718 | return TCL_ERROR; |
| 719 | } |
| 720 | arg = Tcl_GetStringFromObj(objv[1], &szArg); |
| 721 | nArg = (int)szArg; |
| 722 | rc = Th_Eval(th1Interp, 0, arg, nArg); |
| 723 | arg = Th_GetResult(th1Interp, &nArg); |
| 724 | Tcl_SetObjResult(interp, Tcl_NewStringObj(arg, TH1_LEN(nArg))); |
| 725 | return getTclReturnCode(rc); |
| 726 | } |
| 727 | |
| 728 | /* |
| 729 | ** Tcl command: th1Expr arg |
| @@ -800,10 +737,11 @@ | |
| 737 | int objc, |
| 738 | Tcl_Obj *const objv[] |
| 739 | ){ |
| 740 | Th_Interp *th1Interp; |
| 741 | int nArg; |
| 742 | Tcl_Size szArg; |
| 743 | const char *arg; |
| 744 | int rc; |
| 745 | |
| 746 | if( objc!=2 ){ |
| 747 | Tcl_WrongNumArgs(interp, 1, objv, "arg"); |
| @@ -812,14 +750,14 @@ | |
| 750 | th1Interp = (Th_Interp *)clientData; |
| 751 | if( !th1Interp ){ |
| 752 | Tcl_AppendResult(interp, "invalid TH1 interpreter", NULL); |
| 753 | return TCL_ERROR; |
| 754 | } |
| 755 | arg = Tcl_GetStringFromObj(objv[1], &szArg); |
| 756 | rc = Th_Expr(th1Interp, arg, (int)szArg); |
| 757 | arg = Th_GetResult(th1Interp, &nArg); |
| 758 | Tcl_SetObjResult(interp, Tcl_NewStringObj(arg, TH1_LEN(nArg))); |
| 759 | return getTclReturnCode(rc); |
| 760 | } |
| 761 | |
| 762 | /* |
| 763 | ** Array of Tcl integration commands. Used when adding or removing the Tcl |
| 764 |
+49
-22
| --- src/timeline.c | ||
| +++ src/timeline.c | ||
| @@ -221,28 +221,28 @@ | ||
| 221 | 221 | vid = db_lget_int("checkout", 0); |
| 222 | 222 | } |
| 223 | 223 | zPrevDate[0] = 0; |
| 224 | 224 | mxWikiLen = db_get_int("timeline-max-comment", 0); |
| 225 | 225 | dateFormat = db_get_int("timeline-date-format", 0); |
| 226 | - /* | |
| 227 | - ** SETTING: timeline-truncate-at-blank boolean default=off | |
| 228 | - ** | |
| 229 | - ** If enabled, check-in comments displayed on the timeline are truncated | |
| 230 | - ** at the first blank line of the comment text. The comment text after | |
| 231 | - ** the first blank line is only seen in the /info or similar pages that | |
| 232 | - ** show details about the check-in. | |
| 233 | - */ | |
| 226 | +/* | |
| 227 | +** SETTING: timeline-truncate-at-blank boolean default=off | |
| 228 | +** | |
| 229 | +** If enabled, check-in comments displayed on the timeline are truncated | |
| 230 | +** at the first blank line of the comment text. The comment text after | |
| 231 | +** the first blank line is only seen in the /info or similar pages that | |
| 232 | +** show details about the check-in. | |
| 233 | +*/ | |
| 234 | 234 | bCommentGitStyle = db_get_int("timeline-truncate-at-blank", 0); |
| 235 | - /* | |
| 236 | - ** SETTING: timeline-tslink-info boolean default=off | |
| 237 | - ** | |
| 238 | - ** The hyperlink on the timestamp associated with each timeline entry, | |
| 239 | - ** on the far left-hand side of the screen, normally targets another | |
| 240 | - ** /timeline page that shows the entry in context. However, if this | |
| 241 | - ** option is turned on, that hyperlink targets the /info page showing | |
| 242 | - ** the details of the entry. | |
| 243 | - */ | |
| 235 | +/* | |
| 236 | +** SETTING: timeline-tslink-info boolean default=off | |
| 237 | +** | |
| 238 | +** The hyperlink on the timestamp associated with each timeline entry, | |
| 239 | +** on the far left-hand side of the screen, normally targets another | |
| 240 | +** /timeline page that shows the entry in context. However, if this | |
| 241 | +** option is turned on, that hyperlink targets the /info page showing | |
| 242 | +** the details of the entry. | |
| 243 | +*/ | |
| 244 | 244 | bTimestampLinksToInfo = db_get_boolean("timeline-tslink-info", 0); |
| 245 | 245 | if( (tmFlags & TIMELINE_VIEWS)==0 ){ |
| 246 | 246 | tmFlags |= timeline_ss_cookie(); |
| 247 | 247 | } |
| 248 | 248 | if( tmFlags & TIMELINE_COLUMNAR ){ |
| @@ -402,10 +402,12 @@ | ||
| 402 | 402 | zDateLink = mprintf("<a>"); |
| 403 | 403 | } |
| 404 | 404 | @ <td class="timelineTime">%z(zDateLink)%s(zTime)</a></td> |
| 405 | 405 | @ <td class="timelineGraph"> |
| 406 | 406 | if( tmFlags & (TIMELINE_UCOLOR|TIMELINE_DELTA|TIMELINE_NOCOLOR) ){ |
| 407 | + /* Don't use the requested background color. Use the background color | |
| 408 | + ** override from query parameters instead. */ | |
| 407 | 409 | if( tmFlags & TIMELINE_UCOLOR ){ |
| 408 | 410 | zBgClr = zUser ? user_color(zUser) : 0; |
| 409 | 411 | }else if( tmFlags & TIMELINE_NOCOLOR ){ |
| 410 | 412 | zBgClr = 0; |
| 411 | 413 | }else if( zType[0]=='c' ){ |
| @@ -420,16 +422,21 @@ | ||
| 420 | 422 | }else{ |
| 421 | 423 | zBgClr = hash_color("f"); /* delta manifest */ |
| 422 | 424 | } |
| 423 | 425 | db_reset(&qdelta); |
| 424 | 426 | } |
| 427 | + }else{ | |
| 428 | + /* Make sure the user-specified background color is reasonable */ | |
| 429 | + zBgClr = reasonable_bg_color(zBgClr, 0); | |
| 425 | 430 | } |
| 426 | 431 | if( zType[0]=='c' |
| 427 | 432 | && (pGraph || zBgClr==0 || (tmFlags & (TIMELINE_BRCOLOR|TIMELINE_DELTA))!=0) |
| 428 | 433 | ){ |
| 429 | 434 | zBr = branch_of_rid(rid); |
| 430 | 435 | if( zBgClr==0 || (tmFlags & TIMELINE_BRCOLOR)!=0 ){ |
| 436 | + /* If no background color is specified, use a color based on the | |
| 437 | + ** branch name */ | |
| 431 | 438 | if( tmFlags & (TIMELINE_DELTA|TIMELINE_NOCOLOR) ){ |
| 432 | 439 | }else if( zBr==0 || strcmp(zBr,"trunk")==0 ){ |
| 433 | 440 | zBgClr = 0; |
| 434 | 441 | }else{ |
| 435 | 442 | zBgClr = hash_color(zBr); |
| @@ -591,10 +598,19 @@ | ||
| 591 | 598 | drawDetailEllipsis = 0; |
| 592 | 599 | }else{ |
| 593 | 600 | cgi_printf("%W",blob_str(&comment)); |
| 594 | 601 | } |
| 595 | 602 | } |
| 603 | + | |
| 604 | + if( zType[0]=='c' && strcmp(zUuid, MANIFEST_UUID)==0 ){ | |
| 605 | + /* This will only ever happen when Fossil is drawing a timeline for | |
| 606 | + ** its own self-host repository. If the timeline shows the specific | |
| 607 | + ** check-in corresponding to the current executable, then tag that | |
| 608 | + ** check-in with "This is me!". */ | |
| 609 | + @ <b>← This is me!</b> | |
| 610 | + } | |
| 611 | + | |
| 596 | 612 | @ </span> |
| 597 | 613 | blob_reset(&comment); |
| 598 | 614 | |
| 599 | 615 | /* Generate extra information and hyperlinks to follow the comment. |
| 600 | 616 | ** Example: "(check-in: [abcdefg], user: drh, tags: trunk)" |
| @@ -1872,11 +1888,11 @@ | ||
| 1872 | 1888 | if( zTagName ){ |
| 1873 | 1889 | zType = "ci"; |
| 1874 | 1890 | if( matchStyle==MS_EXACT ){ |
| 1875 | 1891 | /* For exact maching, inhibit links to the selected tag. */ |
| 1876 | 1892 | zThisTag = zTagName; |
| 1877 | - Th_Store("current_checkin", zTagName); | |
| 1893 | + Th_StoreUnsafe("current_checkin", zTagName); | |
| 1878 | 1894 | } |
| 1879 | 1895 | |
| 1880 | 1896 | /* Display a checkbox to enable/disable display of related check-ins. */ |
| 1881 | 1897 | if( advancedMenu ){ |
| 1882 | 1898 | style_submenu_checkbox("rel", "Related", 0, 0); |
| @@ -3733,15 +3749,22 @@ | ||
| 3733 | 3749 | } |
| 3734 | 3750 | } |
| 3735 | 3751 | |
| 3736 | 3752 | if( mode==TIMELINE_MODE_NONE ) mode = TIMELINE_MODE_BEFORE; |
| 3737 | 3753 | blob_zero(&sql); |
| 3754 | + if( mode==TIMELINE_MODE_AFTER ){ | |
| 3755 | + /* Extra outer select to get older rows in reverse order */ | |
| 3756 | + blob_append(&sql, "SELECT *\nFROM (", -1); | |
| 3757 | + } | |
| 3738 | 3758 | blob_append(&sql, timeline_query_for_tty(), -1); |
| 3739 | 3759 | blob_append_sql(&sql, "\n AND event.mtime %s %s", |
| 3740 | 3760 | ( mode==TIMELINE_MODE_BEFORE || |
| 3741 | 3761 | mode==TIMELINE_MODE_PARENTS ) ? "<=" : ">=", zDate /*safe-for-%s*/ |
| 3742 | 3762 | ); |
| 3763 | + if( zType && (zType[0]!='a') ){ | |
| 3764 | + blob_append_sql(&sql, "\n AND event.type=%Q ", zType); | |
| 3765 | + } | |
| 3743 | 3766 | |
| 3744 | 3767 | /* When zFilePattern is specified, compute complete ancestry; |
| 3745 | 3768 | * limit later at print_timeline() */ |
| 3746 | 3769 | if( mode==TIMELINE_MODE_CHILDREN || mode==TIMELINE_MODE_PARENTS ){ |
| 3747 | 3770 | db_multi_exec("CREATE TEMP TABLE ok(rid INTEGER PRIMARY KEY)"); |
| @@ -3750,13 +3773,10 @@ | ||
| 3750 | 3773 | }else{ |
| 3751 | 3774 | compute_ancestors(objid, (zFilePattern ? 0 : n), 0, 0); |
| 3752 | 3775 | } |
| 3753 | 3776 | blob_append_sql(&sql, "\n AND blob.rid IN ok"); |
| 3754 | 3777 | } |
| 3755 | - if( zType && (zType[0]!='a') ){ | |
| 3756 | - blob_append_sql(&sql, "\n AND event.type=%Q ", zType); | |
| 3757 | - } | |
| 3758 | 3778 | if( zFilePattern ){ |
| 3759 | 3779 | blob_append(&sql, |
| 3760 | 3780 | "\n AND EXISTS(SELECT 1 FROM mlink\n" |
| 3761 | 3781 | " WHERE mlink.mid=event.objid\n" |
| 3762 | 3782 | " AND mlink.fnid IN ", -1); |
| @@ -3794,11 +3814,18 @@ | ||
| 3794 | 3814 | " WHERE tx.value='%q'\n" |
| 3795 | 3815 | ")\n" /* No merge closures */ |
| 3796 | 3816 | " AND (tagxref.value IS NULL OR tagxref.value='%q')", |
| 3797 | 3817 | zBr, zBr, zBr, TAG_BRANCH, zBr, zBr); |
| 3798 | 3818 | } |
| 3799 | - blob_append_sql(&sql, "\nORDER BY event.mtime DESC"); | |
| 3819 | + | |
| 3820 | + if( mode==TIMELINE_MODE_AFTER ){ | |
| 3821 | + /* Complete the above outer select. */ | |
| 3822 | + blob_append_sql(&sql, | |
| 3823 | + "\nORDER BY event.mtime LIMIT abs(%d)) t ORDER BY t.mDateTime DESC;", n); | |
| 3824 | + }else{ | |
| 3825 | + blob_append_sql(&sql, "\nORDER BY event.mtime DESC"); | |
| 3826 | + } | |
| 3800 | 3827 | if( iOffset>0 ){ |
| 3801 | 3828 | /* Don't handle LIMIT here, otherwise print_timeline() |
| 3802 | 3829 | * will not determine the end-marker correctly! */ |
| 3803 | 3830 | blob_append_sql(&sql, "\n LIMIT -1 OFFSET %d", iOffset); |
| 3804 | 3831 | } |
| 3805 | 3832 |
| --- src/timeline.c | |
| +++ src/timeline.c | |
| @@ -221,28 +221,28 @@ | |
| 221 | vid = db_lget_int("checkout", 0); |
| 222 | } |
| 223 | zPrevDate[0] = 0; |
| 224 | mxWikiLen = db_get_int("timeline-max-comment", 0); |
| 225 | dateFormat = db_get_int("timeline-date-format", 0); |
| 226 | /* |
| 227 | ** SETTING: timeline-truncate-at-blank boolean default=off |
| 228 | ** |
| 229 | ** If enabled, check-in comments displayed on the timeline are truncated |
| 230 | ** at the first blank line of the comment text. The comment text after |
| 231 | ** the first blank line is only seen in the /info or similar pages that |
| 232 | ** show details about the check-in. |
| 233 | */ |
| 234 | bCommentGitStyle = db_get_int("timeline-truncate-at-blank", 0); |
| 235 | /* |
| 236 | ** SETTING: timeline-tslink-info boolean default=off |
| 237 | ** |
| 238 | ** The hyperlink on the timestamp associated with each timeline entry, |
| 239 | ** on the far left-hand side of the screen, normally targets another |
| 240 | ** /timeline page that shows the entry in context. However, if this |
| 241 | ** option is turned on, that hyperlink targets the /info page showing |
| 242 | ** the details of the entry. |
| 243 | */ |
| 244 | bTimestampLinksToInfo = db_get_boolean("timeline-tslink-info", 0); |
| 245 | if( (tmFlags & TIMELINE_VIEWS)==0 ){ |
| 246 | tmFlags |= timeline_ss_cookie(); |
| 247 | } |
| 248 | if( tmFlags & TIMELINE_COLUMNAR ){ |
| @@ -402,10 +402,12 @@ | |
| 402 | zDateLink = mprintf("<a>"); |
| 403 | } |
| 404 | @ <td class="timelineTime">%z(zDateLink)%s(zTime)</a></td> |
| 405 | @ <td class="timelineGraph"> |
| 406 | if( tmFlags & (TIMELINE_UCOLOR|TIMELINE_DELTA|TIMELINE_NOCOLOR) ){ |
| 407 | if( tmFlags & TIMELINE_UCOLOR ){ |
| 408 | zBgClr = zUser ? user_color(zUser) : 0; |
| 409 | }else if( tmFlags & TIMELINE_NOCOLOR ){ |
| 410 | zBgClr = 0; |
| 411 | }else if( zType[0]=='c' ){ |
| @@ -420,16 +422,21 @@ | |
| 420 | }else{ |
| 421 | zBgClr = hash_color("f"); /* delta manifest */ |
| 422 | } |
| 423 | db_reset(&qdelta); |
| 424 | } |
| 425 | } |
| 426 | if( zType[0]=='c' |
| 427 | && (pGraph || zBgClr==0 || (tmFlags & (TIMELINE_BRCOLOR|TIMELINE_DELTA))!=0) |
| 428 | ){ |
| 429 | zBr = branch_of_rid(rid); |
| 430 | if( zBgClr==0 || (tmFlags & TIMELINE_BRCOLOR)!=0 ){ |
| 431 | if( tmFlags & (TIMELINE_DELTA|TIMELINE_NOCOLOR) ){ |
| 432 | }else if( zBr==0 || strcmp(zBr,"trunk")==0 ){ |
| 433 | zBgClr = 0; |
| 434 | }else{ |
| 435 | zBgClr = hash_color(zBr); |
| @@ -591,10 +598,19 @@ | |
| 591 | drawDetailEllipsis = 0; |
| 592 | }else{ |
| 593 | cgi_printf("%W",blob_str(&comment)); |
| 594 | } |
| 595 | } |
| 596 | @ </span> |
| 597 | blob_reset(&comment); |
| 598 | |
| 599 | /* Generate extra information and hyperlinks to follow the comment. |
| 600 | ** Example: "(check-in: [abcdefg], user: drh, tags: trunk)" |
| @@ -1872,11 +1888,11 @@ | |
| 1872 | if( zTagName ){ |
| 1873 | zType = "ci"; |
| 1874 | if( matchStyle==MS_EXACT ){ |
| 1875 | /* For exact maching, inhibit links to the selected tag. */ |
| 1876 | zThisTag = zTagName; |
| 1877 | Th_Store("current_checkin", zTagName); |
| 1878 | } |
| 1879 | |
| 1880 | /* Display a checkbox to enable/disable display of related check-ins. */ |
| 1881 | if( advancedMenu ){ |
| 1882 | style_submenu_checkbox("rel", "Related", 0, 0); |
| @@ -3733,15 +3749,22 @@ | |
| 3733 | } |
| 3734 | } |
| 3735 | |
| 3736 | if( mode==TIMELINE_MODE_NONE ) mode = TIMELINE_MODE_BEFORE; |
| 3737 | blob_zero(&sql); |
| 3738 | blob_append(&sql, timeline_query_for_tty(), -1); |
| 3739 | blob_append_sql(&sql, "\n AND event.mtime %s %s", |
| 3740 | ( mode==TIMELINE_MODE_BEFORE || |
| 3741 | mode==TIMELINE_MODE_PARENTS ) ? "<=" : ">=", zDate /*safe-for-%s*/ |
| 3742 | ); |
| 3743 | |
| 3744 | /* When zFilePattern is specified, compute complete ancestry; |
| 3745 | * limit later at print_timeline() */ |
| 3746 | if( mode==TIMELINE_MODE_CHILDREN || mode==TIMELINE_MODE_PARENTS ){ |
| 3747 | db_multi_exec("CREATE TEMP TABLE ok(rid INTEGER PRIMARY KEY)"); |
| @@ -3750,13 +3773,10 @@ | |
| 3750 | }else{ |
| 3751 | compute_ancestors(objid, (zFilePattern ? 0 : n), 0, 0); |
| 3752 | } |
| 3753 | blob_append_sql(&sql, "\n AND blob.rid IN ok"); |
| 3754 | } |
| 3755 | if( zType && (zType[0]!='a') ){ |
| 3756 | blob_append_sql(&sql, "\n AND event.type=%Q ", zType); |
| 3757 | } |
| 3758 | if( zFilePattern ){ |
| 3759 | blob_append(&sql, |
| 3760 | "\n AND EXISTS(SELECT 1 FROM mlink\n" |
| 3761 | " WHERE mlink.mid=event.objid\n" |
| 3762 | " AND mlink.fnid IN ", -1); |
| @@ -3794,11 +3814,18 @@ | |
| 3794 | " WHERE tx.value='%q'\n" |
| 3795 | ")\n" /* No merge closures */ |
| 3796 | " AND (tagxref.value IS NULL OR tagxref.value='%q')", |
| 3797 | zBr, zBr, zBr, TAG_BRANCH, zBr, zBr); |
| 3798 | } |
| 3799 | blob_append_sql(&sql, "\nORDER BY event.mtime DESC"); |
| 3800 | if( iOffset>0 ){ |
| 3801 | /* Don't handle LIMIT here, otherwise print_timeline() |
| 3802 | * will not determine the end-marker correctly! */ |
| 3803 | blob_append_sql(&sql, "\n LIMIT -1 OFFSET %d", iOffset); |
| 3804 | } |
| 3805 |
| --- src/timeline.c | |
| +++ src/timeline.c | |
| @@ -221,28 +221,28 @@ | |
| 221 | vid = db_lget_int("checkout", 0); |
| 222 | } |
| 223 | zPrevDate[0] = 0; |
| 224 | mxWikiLen = db_get_int("timeline-max-comment", 0); |
| 225 | dateFormat = db_get_int("timeline-date-format", 0); |
| 226 | /* |
| 227 | ** SETTING: timeline-truncate-at-blank boolean default=off |
| 228 | ** |
| 229 | ** If enabled, check-in comments displayed on the timeline are truncated |
| 230 | ** at the first blank line of the comment text. The comment text after |
| 231 | ** the first blank line is only seen in the /info or similar pages that |
| 232 | ** show details about the check-in. |
| 233 | */ |
| 234 | bCommentGitStyle = db_get_int("timeline-truncate-at-blank", 0); |
| 235 | /* |
| 236 | ** SETTING: timeline-tslink-info boolean default=off |
| 237 | ** |
| 238 | ** The hyperlink on the timestamp associated with each timeline entry, |
| 239 | ** on the far left-hand side of the screen, normally targets another |
| 240 | ** /timeline page that shows the entry in context. However, if this |
| 241 | ** option is turned on, that hyperlink targets the /info page showing |
| 242 | ** the details of the entry. |
| 243 | */ |
| 244 | bTimestampLinksToInfo = db_get_boolean("timeline-tslink-info", 0); |
| 245 | if( (tmFlags & TIMELINE_VIEWS)==0 ){ |
| 246 | tmFlags |= timeline_ss_cookie(); |
| 247 | } |
| 248 | if( tmFlags & TIMELINE_COLUMNAR ){ |
| @@ -402,10 +402,12 @@ | |
| 402 | zDateLink = mprintf("<a>"); |
| 403 | } |
| 404 | @ <td class="timelineTime">%z(zDateLink)%s(zTime)</a></td> |
| 405 | @ <td class="timelineGraph"> |
| 406 | if( tmFlags & (TIMELINE_UCOLOR|TIMELINE_DELTA|TIMELINE_NOCOLOR) ){ |
| 407 | /* Don't use the requested background color. Use the background color |
| 408 | ** override from query parameters instead. */ |
| 409 | if( tmFlags & TIMELINE_UCOLOR ){ |
| 410 | zBgClr = zUser ? user_color(zUser) : 0; |
| 411 | }else if( tmFlags & TIMELINE_NOCOLOR ){ |
| 412 | zBgClr = 0; |
| 413 | }else if( zType[0]=='c' ){ |
| @@ -420,16 +422,21 @@ | |
| 422 | }else{ |
| 423 | zBgClr = hash_color("f"); /* delta manifest */ |
| 424 | } |
| 425 | db_reset(&qdelta); |
| 426 | } |
| 427 | }else{ |
| 428 | /* Make sure the user-specified background color is reasonable */ |
| 429 | zBgClr = reasonable_bg_color(zBgClr, 0); |
| 430 | } |
| 431 | if( zType[0]=='c' |
| 432 | && (pGraph || zBgClr==0 || (tmFlags & (TIMELINE_BRCOLOR|TIMELINE_DELTA))!=0) |
| 433 | ){ |
| 434 | zBr = branch_of_rid(rid); |
| 435 | if( zBgClr==0 || (tmFlags & TIMELINE_BRCOLOR)!=0 ){ |
| 436 | /* If no background color is specified, use a color based on the |
| 437 | ** branch name */ |
| 438 | if( tmFlags & (TIMELINE_DELTA|TIMELINE_NOCOLOR) ){ |
| 439 | }else if( zBr==0 || strcmp(zBr,"trunk")==0 ){ |
| 440 | zBgClr = 0; |
| 441 | }else{ |
| 442 | zBgClr = hash_color(zBr); |
| @@ -591,10 +598,19 @@ | |
| 598 | drawDetailEllipsis = 0; |
| 599 | }else{ |
| 600 | cgi_printf("%W",blob_str(&comment)); |
| 601 | } |
| 602 | } |
| 603 | |
| 604 | if( zType[0]=='c' && strcmp(zUuid, MANIFEST_UUID)==0 ){ |
| 605 | /* This will only ever happen when Fossil is drawing a timeline for |
| 606 | ** its own self-host repository. If the timeline shows the specific |
| 607 | ** check-in corresponding to the current executable, then tag that |
| 608 | ** check-in with "This is me!". */ |
| 609 | @ <b>← This is me!</b> |
| 610 | } |
| 611 | |
| 612 | @ </span> |
| 613 | blob_reset(&comment); |
| 614 | |
| 615 | /* Generate extra information and hyperlinks to follow the comment. |
| 616 | ** Example: "(check-in: [abcdefg], user: drh, tags: trunk)" |
| @@ -1872,11 +1888,11 @@ | |
| 1888 | if( zTagName ){ |
| 1889 | zType = "ci"; |
| 1890 | if( matchStyle==MS_EXACT ){ |
| 1891 | /* For exact maching, inhibit links to the selected tag. */ |
| 1892 | zThisTag = zTagName; |
| 1893 | Th_StoreUnsafe("current_checkin", zTagName); |
| 1894 | } |
| 1895 | |
| 1896 | /* Display a checkbox to enable/disable display of related check-ins. */ |
| 1897 | if( advancedMenu ){ |
| 1898 | style_submenu_checkbox("rel", "Related", 0, 0); |
| @@ -3733,15 +3749,22 @@ | |
| 3749 | } |
| 3750 | } |
| 3751 | |
| 3752 | if( mode==TIMELINE_MODE_NONE ) mode = TIMELINE_MODE_BEFORE; |
| 3753 | blob_zero(&sql); |
| 3754 | if( mode==TIMELINE_MODE_AFTER ){ |
| 3755 | /* Extra outer select to get older rows in reverse order */ |
| 3756 | blob_append(&sql, "SELECT *\nFROM (", -1); |
| 3757 | } |
| 3758 | blob_append(&sql, timeline_query_for_tty(), -1); |
| 3759 | blob_append_sql(&sql, "\n AND event.mtime %s %s", |
| 3760 | ( mode==TIMELINE_MODE_BEFORE || |
| 3761 | mode==TIMELINE_MODE_PARENTS ) ? "<=" : ">=", zDate /*safe-for-%s*/ |
| 3762 | ); |
| 3763 | if( zType && (zType[0]!='a') ){ |
| 3764 | blob_append_sql(&sql, "\n AND event.type=%Q ", zType); |
| 3765 | } |
| 3766 | |
| 3767 | /* When zFilePattern is specified, compute complete ancestry; |
| 3768 | * limit later at print_timeline() */ |
| 3769 | if( mode==TIMELINE_MODE_CHILDREN || mode==TIMELINE_MODE_PARENTS ){ |
| 3770 | db_multi_exec("CREATE TEMP TABLE ok(rid INTEGER PRIMARY KEY)"); |
| @@ -3750,13 +3773,10 @@ | |
| 3773 | }else{ |
| 3774 | compute_ancestors(objid, (zFilePattern ? 0 : n), 0, 0); |
| 3775 | } |
| 3776 | blob_append_sql(&sql, "\n AND blob.rid IN ok"); |
| 3777 | } |
| 3778 | if( zFilePattern ){ |
| 3779 | blob_append(&sql, |
| 3780 | "\n AND EXISTS(SELECT 1 FROM mlink\n" |
| 3781 | " WHERE mlink.mid=event.objid\n" |
| 3782 | " AND mlink.fnid IN ", -1); |
| @@ -3794,11 +3814,18 @@ | |
| 3814 | " WHERE tx.value='%q'\n" |
| 3815 | ")\n" /* No merge closures */ |
| 3816 | " AND (tagxref.value IS NULL OR tagxref.value='%q')", |
| 3817 | zBr, zBr, zBr, TAG_BRANCH, zBr, zBr); |
| 3818 | } |
| 3819 | |
| 3820 | if( mode==TIMELINE_MODE_AFTER ){ |
| 3821 | /* Complete the above outer select. */ |
| 3822 | blob_append_sql(&sql, |
| 3823 | "\nORDER BY event.mtime LIMIT abs(%d)) t ORDER BY t.mDateTime DESC;", n); |
| 3824 | }else{ |
| 3825 | blob_append_sql(&sql, "\nORDER BY event.mtime DESC"); |
| 3826 | } |
| 3827 | if( iOffset>0 ){ |
| 3828 | /* Don't handle LIMIT here, otherwise print_timeline() |
| 3829 | * will not determine the end-marker correctly! */ |
| 3830 | blob_append_sql(&sql, "\n LIMIT -1 OFFSET %d", iOffset); |
| 3831 | } |
| 3832 |
+27
-15
| --- src/tkt.c | ||
| +++ src/tkt.c | ||
| @@ -188,14 +188,19 @@ | ||
| 188 | 188 | */ |
| 189 | 189 | static void initializeVariablesFromDb(void){ |
| 190 | 190 | const char *zName; |
| 191 | 191 | Stmt q; |
| 192 | 192 | int i, n, size, j; |
| 193 | + const char *zCTimeColumn = haveTicketCTime ? "tkt_ctime" : "tkt_mtime"; | |
| 193 | 194 | |
| 194 | 195 | zName = PD("name","-none-"); |
| 195 | - db_prepare(&q, "SELECT datetime(tkt_mtime,toLocal()) AS tkt_datetime, *" | |
| 196 | + db_prepare(&q, "SELECT datetime(tkt_mtime,toLocal()) AS tkt_datetime, " | |
| 197 | + "datetime(%s,toLocal()) AS tkt_datetime_creation, " | |
| 198 | + "julianday('now') - tkt_mtime, " | |
| 199 | + "julianday('now') - %s, *" | |
| 196 | 200 | " FROM ticket WHERE tkt_uuid GLOB '%q*'", |
| 201 | + zCTimeColumn/*safe-for-%s*/, zCTimeColumn/*safe-for-%s*/, | |
| 197 | 202 | zName); |
| 198 | 203 | if( db_step(&q)==SQLITE_ROW ){ |
| 199 | 204 | n = db_column_count(&q); |
| 200 | 205 | for(i=0; i<n; i++){ |
| 201 | 206 | const char *zVal = db_column_text(&q, i); |
| @@ -207,19 +212,22 @@ | ||
| 207 | 212 | zVal = zRevealed = db_reveal(zVal); |
| 208 | 213 | } |
| 209 | 214 | if( (j = fieldId(zName))>=0 ){ |
| 210 | 215 | aField[j].zValue = mprintf("%s", zVal); |
| 211 | 216 | }else if( memcmp(zName, "tkt_", 4)==0 && Th_Fetch(zName, &size)==0 ){ |
| 217 | + /* TICKET table columns that begin with "tkt_" are always safe */ | |
| 212 | 218 | Th_Store(zName, zVal); |
| 213 | 219 | } |
| 214 | 220 | free(zRevealed); |
| 215 | 221 | } |
| 222 | + Th_Store("tkt_mage", human_readable_age(db_column_double(&q, 2))); | |
| 223 | + Th_Store("tkt_cage", human_readable_age(db_column_double(&q, 3))); | |
| 216 | 224 | } |
| 217 | 225 | db_finalize(&q); |
| 218 | 226 | for(i=0; i<nField; i++){ |
| 219 | 227 | if( Th_Fetch(aField[i].zName, &size)==0 ){ |
| 220 | - Th_Store(aField[i].zName, aField[i].zValue); | |
| 228 | + Th_StoreUnsafe(aField[i].zName, aField[i].zValue); | |
| 221 | 229 | } |
| 222 | 230 | } |
| 223 | 231 | } |
| 224 | 232 | |
| 225 | 233 | /* |
| @@ -228,11 +236,11 @@ | ||
| 228 | 236 | static void initializeVariablesFromCGI(void){ |
| 229 | 237 | int i; |
| 230 | 238 | const char *z; |
| 231 | 239 | |
| 232 | 240 | for(i=0; (z = cgi_parameter_name(i))!=0; i++){ |
| 233 | - Th_Store(z, P(z)); | |
| 241 | + Th_StoreUnsafe(z, P(z)); | |
| 234 | 242 | } |
| 235 | 243 | } |
| 236 | 244 | |
| 237 | 245 | /* |
| 238 | 246 | ** Information about a single J-card |
| @@ -769,11 +777,12 @@ | ||
| 769 | 777 | } |
| 770 | 778 | zFullName = db_text(0, |
| 771 | 779 | "SELECT tkt_uuid FROM ticket" |
| 772 | 780 | " WHERE tkt_uuid GLOB '%q*'", zUuid); |
| 773 | 781 | if( g.perm.WrWiki && g.perm.WrTkt ){ |
| 774 | - style_submenu_element("Edit Description", "%R/wikiedit?name=ticket/%T", zFullName); | |
| 782 | + style_submenu_element("Edit Description", | |
| 783 | + "%R/wikiedit?name=ticket/%T", zFullName); | |
| 775 | 784 | } |
| 776 | 785 | if( g.thTrace ) Th_Trace("BEGIN_TKTVIEW<br>\n", -1); |
| 777 | 786 | ticket_init(); |
| 778 | 787 | initializeVariablesFromCGI(); |
| 779 | 788 | getAllTicketFields(); |
| @@ -812,15 +821,15 @@ | ||
| 812 | 821 | if( argc!=3 ){ |
| 813 | 822 | return Th_WrongNumArgs(interp, "append_field FIELD STRING"); |
| 814 | 823 | } |
| 815 | 824 | if( g.thTrace ){ |
| 816 | 825 | Th_Trace("append_field %#h {%#h}<br>\n", |
| 817 | - argl[1], argv[1], argl[2], argv[2]); | |
| 826 | + TH1_LEN(argl[1]), argv[1], TH1_LEN(argl[2]), argv[2]); | |
| 818 | 827 | } |
| 819 | 828 | for(idx=0; idx<nField; idx++){ |
| 820 | - if( memcmp(aField[idx].zName, argv[1], argl[1])==0 | |
| 821 | - && aField[idx].zName[argl[1]]==0 ){ | |
| 829 | + if( memcmp(aField[idx].zName, argv[1], TH1_LEN(argl[1]))==0 | |
| 830 | + && aField[idx].zName[TH1_LEN(argl[1])]==0 ){ | |
| 822 | 831 | break; |
| 823 | 832 | } |
| 824 | 833 | } |
| 825 | 834 | if( idx>=nField ){ |
| 826 | 835 | Th_ErrorMessage(g.interp, "no such TICKET column: ", argv[1], argl[1]); |
| @@ -932,10 +941,11 @@ | ||
| 932 | 941 | const char *zValue; |
| 933 | 942 | int nValue; |
| 934 | 943 | if( aField[i].zAppend ) continue; |
| 935 | 944 | zValue = Th_Fetch(aField[i].zName, &nValue); |
| 936 | 945 | if( zValue ){ |
| 946 | + nValue = TH1_LEN(nValue); | |
| 937 | 947 | while( nValue>0 && fossil_isspace(zValue[nValue-1]) ){ nValue--; } |
| 938 | 948 | if( ((aField[i].mUsed & USEDBY_TICKETCHNG)!=0 && nValue>0) |
| 939 | 949 | || memcmp(zValue, aField[i].zValue, nValue)!=0 |
| 940 | 950 | ||(int)strlen(aField[i].zValue)!=nValue |
| 941 | 951 | ){ |
| @@ -1026,32 +1036,34 @@ | ||
| 1026 | 1036 | form_begin(0, "%R/%s", g.zPath); |
| 1027 | 1037 | if( P("date_override") && g.perm.Setup ){ |
| 1028 | 1038 | @ <input type="hidden" name="date_override" value="%h(P("date_override"))"> |
| 1029 | 1039 | } |
| 1030 | 1040 | zScript = ticket_newpage_code(); |
| 1041 | + Th_Store("private_contact", ""); | |
| 1031 | 1042 | if( g.zLogin && g.zLogin[0] ){ |
| 1032 | - int nEmail = 0; | |
| 1033 | - (void)Th_MaybeGetVar(g.interp, "private_contact", &nEmail); | |
| 1034 | - uid = nEmail>0 | |
| 1035 | - ? 0 : db_int(0, "SELECT uid FROM user WHERE login=%Q", g.zLogin); | |
| 1043 | + uid = db_int(0, "SELECT uid FROM user WHERE login=%Q", g.zLogin); | |
| 1036 | 1044 | if( uid ){ |
| 1037 | 1045 | char * zEmail = |
| 1038 | 1046 | db_text(0, "SELECT find_emailaddr(info) FROM user WHERE uid=%d", |
| 1039 | 1047 | uid); |
| 1040 | 1048 | if( zEmail ){ |
| 1041 | - Th_Store("private_contact", zEmail); | |
| 1049 | + Th_StoreUnsafe("private_contact", zEmail); | |
| 1042 | 1050 | fossil_free(zEmail); |
| 1043 | 1051 | } |
| 1044 | 1052 | } |
| 1045 | 1053 | } |
| 1046 | - Th_Store("login", login_name()); | |
| 1054 | + Th_StoreUnsafe("login", login_name()); | |
| 1047 | 1055 | Th_Store("date", db_text(0, "SELECT datetime('now')")); |
| 1048 | 1056 | Th_CreateCommand(g.interp, "submit_ticket", submitTicketCmd, |
| 1049 | 1057 | (void*)&zNewUuid, 0); |
| 1050 | 1058 | if( g.thTrace ) Th_Trace("BEGIN_TKTNEW_SCRIPT<br>\n", -1); |
| 1051 | 1059 | if( Th_Render(zScript)==TH_RETURN && !g.thTrace && zNewUuid ){ |
| 1052 | - cgi_redirect(mprintf("%R/tktview/%s", zNewUuid)); | |
| 1060 | + if( P("submitandnew") ){ | |
| 1061 | + cgi_redirect(mprintf("%R/tktnew/%s", zNewUuid)); | |
| 1062 | + }else{ | |
| 1063 | + cgi_redirect(mprintf("%R/tktview/%s", zNewUuid)); | |
| 1064 | + } | |
| 1053 | 1065 | return; |
| 1054 | 1066 | } |
| 1055 | 1067 | captcha_generate(0); |
| 1056 | 1068 | @ </form> |
| 1057 | 1069 | if( g.thTrace ) Th_Trace("END_TKTVIEW<br>\n", -1); |
| @@ -1112,11 +1124,11 @@ | ||
| 1112 | 1124 | initializeVariablesFromDb(); |
| 1113 | 1125 | if( g.zPath[0]=='d' ) showAllFields(); |
| 1114 | 1126 | form_begin(0, "%R/%s", g.zPath); |
| 1115 | 1127 | @ <input type="hidden" name="name" value="%s(zName)"> |
| 1116 | 1128 | zScript = ticket_editpage_code(); |
| 1117 | - Th_Store("login", login_name()); | |
| 1129 | + Th_StoreUnsafe("login", login_name()); | |
| 1118 | 1130 | Th_Store("date", db_text(0, "SELECT datetime('now')")); |
| 1119 | 1131 | Th_CreateCommand(g.interp, "append_field", appendRemarkCmd, 0, 0); |
| 1120 | 1132 | Th_CreateCommand(g.interp, "submit_ticket", submitTicketCmd, (void*)&zName,0); |
| 1121 | 1133 | if( g.thTrace ) Th_Trace("BEGIN_TKTEDIT_SCRIPT<br>\n", -1); |
| 1122 | 1134 | if( Th_Render(zScript)==TH_RETURN && !g.thTrace && zName ){ |
| 1123 | 1135 |
| --- src/tkt.c | |
| +++ src/tkt.c | |
| @@ -188,14 +188,19 @@ | |
| 188 | */ |
| 189 | static void initializeVariablesFromDb(void){ |
| 190 | const char *zName; |
| 191 | Stmt q; |
| 192 | int i, n, size, j; |
| 193 | |
| 194 | zName = PD("name","-none-"); |
| 195 | db_prepare(&q, "SELECT datetime(tkt_mtime,toLocal()) AS tkt_datetime, *" |
| 196 | " FROM ticket WHERE tkt_uuid GLOB '%q*'", |
| 197 | zName); |
| 198 | if( db_step(&q)==SQLITE_ROW ){ |
| 199 | n = db_column_count(&q); |
| 200 | for(i=0; i<n; i++){ |
| 201 | const char *zVal = db_column_text(&q, i); |
| @@ -207,19 +212,22 @@ | |
| 207 | zVal = zRevealed = db_reveal(zVal); |
| 208 | } |
| 209 | if( (j = fieldId(zName))>=0 ){ |
| 210 | aField[j].zValue = mprintf("%s", zVal); |
| 211 | }else if( memcmp(zName, "tkt_", 4)==0 && Th_Fetch(zName, &size)==0 ){ |
| 212 | Th_Store(zName, zVal); |
| 213 | } |
| 214 | free(zRevealed); |
| 215 | } |
| 216 | } |
| 217 | db_finalize(&q); |
| 218 | for(i=0; i<nField; i++){ |
| 219 | if( Th_Fetch(aField[i].zName, &size)==0 ){ |
| 220 | Th_Store(aField[i].zName, aField[i].zValue); |
| 221 | } |
| 222 | } |
| 223 | } |
| 224 | |
| 225 | /* |
| @@ -228,11 +236,11 @@ | |
| 228 | static void initializeVariablesFromCGI(void){ |
| 229 | int i; |
| 230 | const char *z; |
| 231 | |
| 232 | for(i=0; (z = cgi_parameter_name(i))!=0; i++){ |
| 233 | Th_Store(z, P(z)); |
| 234 | } |
| 235 | } |
| 236 | |
| 237 | /* |
| 238 | ** Information about a single J-card |
| @@ -769,11 +777,12 @@ | |
| 769 | } |
| 770 | zFullName = db_text(0, |
| 771 | "SELECT tkt_uuid FROM ticket" |
| 772 | " WHERE tkt_uuid GLOB '%q*'", zUuid); |
| 773 | if( g.perm.WrWiki && g.perm.WrTkt ){ |
| 774 | style_submenu_element("Edit Description", "%R/wikiedit?name=ticket/%T", zFullName); |
| 775 | } |
| 776 | if( g.thTrace ) Th_Trace("BEGIN_TKTVIEW<br>\n", -1); |
| 777 | ticket_init(); |
| 778 | initializeVariablesFromCGI(); |
| 779 | getAllTicketFields(); |
| @@ -812,15 +821,15 @@ | |
| 812 | if( argc!=3 ){ |
| 813 | return Th_WrongNumArgs(interp, "append_field FIELD STRING"); |
| 814 | } |
| 815 | if( g.thTrace ){ |
| 816 | Th_Trace("append_field %#h {%#h}<br>\n", |
| 817 | argl[1], argv[1], argl[2], argv[2]); |
| 818 | } |
| 819 | for(idx=0; idx<nField; idx++){ |
| 820 | if( memcmp(aField[idx].zName, argv[1], argl[1])==0 |
| 821 | && aField[idx].zName[argl[1]]==0 ){ |
| 822 | break; |
| 823 | } |
| 824 | } |
| 825 | if( idx>=nField ){ |
| 826 | Th_ErrorMessage(g.interp, "no such TICKET column: ", argv[1], argl[1]); |
| @@ -932,10 +941,11 @@ | |
| 932 | const char *zValue; |
| 933 | int nValue; |
| 934 | if( aField[i].zAppend ) continue; |
| 935 | zValue = Th_Fetch(aField[i].zName, &nValue); |
| 936 | if( zValue ){ |
| 937 | while( nValue>0 && fossil_isspace(zValue[nValue-1]) ){ nValue--; } |
| 938 | if( ((aField[i].mUsed & USEDBY_TICKETCHNG)!=0 && nValue>0) |
| 939 | || memcmp(zValue, aField[i].zValue, nValue)!=0 |
| 940 | ||(int)strlen(aField[i].zValue)!=nValue |
| 941 | ){ |
| @@ -1026,32 +1036,34 @@ | |
| 1026 | form_begin(0, "%R/%s", g.zPath); |
| 1027 | if( P("date_override") && g.perm.Setup ){ |
| 1028 | @ <input type="hidden" name="date_override" value="%h(P("date_override"))"> |
| 1029 | } |
| 1030 | zScript = ticket_newpage_code(); |
| 1031 | if( g.zLogin && g.zLogin[0] ){ |
| 1032 | int nEmail = 0; |
| 1033 | (void)Th_MaybeGetVar(g.interp, "private_contact", &nEmail); |
| 1034 | uid = nEmail>0 |
| 1035 | ? 0 : db_int(0, "SELECT uid FROM user WHERE login=%Q", g.zLogin); |
| 1036 | if( uid ){ |
| 1037 | char * zEmail = |
| 1038 | db_text(0, "SELECT find_emailaddr(info) FROM user WHERE uid=%d", |
| 1039 | uid); |
| 1040 | if( zEmail ){ |
| 1041 | Th_Store("private_contact", zEmail); |
| 1042 | fossil_free(zEmail); |
| 1043 | } |
| 1044 | } |
| 1045 | } |
| 1046 | Th_Store("login", login_name()); |
| 1047 | Th_Store("date", db_text(0, "SELECT datetime('now')")); |
| 1048 | Th_CreateCommand(g.interp, "submit_ticket", submitTicketCmd, |
| 1049 | (void*)&zNewUuid, 0); |
| 1050 | if( g.thTrace ) Th_Trace("BEGIN_TKTNEW_SCRIPT<br>\n", -1); |
| 1051 | if( Th_Render(zScript)==TH_RETURN && !g.thTrace && zNewUuid ){ |
| 1052 | cgi_redirect(mprintf("%R/tktview/%s", zNewUuid)); |
| 1053 | return; |
| 1054 | } |
| 1055 | captcha_generate(0); |
| 1056 | @ </form> |
| 1057 | if( g.thTrace ) Th_Trace("END_TKTVIEW<br>\n", -1); |
| @@ -1112,11 +1124,11 @@ | |
| 1112 | initializeVariablesFromDb(); |
| 1113 | if( g.zPath[0]=='d' ) showAllFields(); |
| 1114 | form_begin(0, "%R/%s", g.zPath); |
| 1115 | @ <input type="hidden" name="name" value="%s(zName)"> |
| 1116 | zScript = ticket_editpage_code(); |
| 1117 | Th_Store("login", login_name()); |
| 1118 | Th_Store("date", db_text(0, "SELECT datetime('now')")); |
| 1119 | Th_CreateCommand(g.interp, "append_field", appendRemarkCmd, 0, 0); |
| 1120 | Th_CreateCommand(g.interp, "submit_ticket", submitTicketCmd, (void*)&zName,0); |
| 1121 | if( g.thTrace ) Th_Trace("BEGIN_TKTEDIT_SCRIPT<br>\n", -1); |
| 1122 | if( Th_Render(zScript)==TH_RETURN && !g.thTrace && zName ){ |
| 1123 |
| --- src/tkt.c | |
| +++ src/tkt.c | |
| @@ -188,14 +188,19 @@ | |
| 188 | */ |
| 189 | static void initializeVariablesFromDb(void){ |
| 190 | const char *zName; |
| 191 | Stmt q; |
| 192 | int i, n, size, j; |
| 193 | const char *zCTimeColumn = haveTicketCTime ? "tkt_ctime" : "tkt_mtime"; |
| 194 | |
| 195 | zName = PD("name","-none-"); |
| 196 | db_prepare(&q, "SELECT datetime(tkt_mtime,toLocal()) AS tkt_datetime, " |
| 197 | "datetime(%s,toLocal()) AS tkt_datetime_creation, " |
| 198 | "julianday('now') - tkt_mtime, " |
| 199 | "julianday('now') - %s, *" |
| 200 | " FROM ticket WHERE tkt_uuid GLOB '%q*'", |
| 201 | zCTimeColumn/*safe-for-%s*/, zCTimeColumn/*safe-for-%s*/, |
| 202 | zName); |
| 203 | if( db_step(&q)==SQLITE_ROW ){ |
| 204 | n = db_column_count(&q); |
| 205 | for(i=0; i<n; i++){ |
| 206 | const char *zVal = db_column_text(&q, i); |
| @@ -207,19 +212,22 @@ | |
| 212 | zVal = zRevealed = db_reveal(zVal); |
| 213 | } |
| 214 | if( (j = fieldId(zName))>=0 ){ |
| 215 | aField[j].zValue = mprintf("%s", zVal); |
| 216 | }else if( memcmp(zName, "tkt_", 4)==0 && Th_Fetch(zName, &size)==0 ){ |
| 217 | /* TICKET table columns that begin with "tkt_" are always safe */ |
| 218 | Th_Store(zName, zVal); |
| 219 | } |
| 220 | free(zRevealed); |
| 221 | } |
| 222 | Th_Store("tkt_mage", human_readable_age(db_column_double(&q, 2))); |
| 223 | Th_Store("tkt_cage", human_readable_age(db_column_double(&q, 3))); |
| 224 | } |
| 225 | db_finalize(&q); |
| 226 | for(i=0; i<nField; i++){ |
| 227 | if( Th_Fetch(aField[i].zName, &size)==0 ){ |
| 228 | Th_StoreUnsafe(aField[i].zName, aField[i].zValue); |
| 229 | } |
| 230 | } |
| 231 | } |
| 232 | |
| 233 | /* |
| @@ -228,11 +236,11 @@ | |
| 236 | static void initializeVariablesFromCGI(void){ |
| 237 | int i; |
| 238 | const char *z; |
| 239 | |
| 240 | for(i=0; (z = cgi_parameter_name(i))!=0; i++){ |
| 241 | Th_StoreUnsafe(z, P(z)); |
| 242 | } |
| 243 | } |
| 244 | |
| 245 | /* |
| 246 | ** Information about a single J-card |
| @@ -769,11 +777,12 @@ | |
| 777 | } |
| 778 | zFullName = db_text(0, |
| 779 | "SELECT tkt_uuid FROM ticket" |
| 780 | " WHERE tkt_uuid GLOB '%q*'", zUuid); |
| 781 | if( g.perm.WrWiki && g.perm.WrTkt ){ |
| 782 | style_submenu_element("Edit Description", |
| 783 | "%R/wikiedit?name=ticket/%T", zFullName); |
| 784 | } |
| 785 | if( g.thTrace ) Th_Trace("BEGIN_TKTVIEW<br>\n", -1); |
| 786 | ticket_init(); |
| 787 | initializeVariablesFromCGI(); |
| 788 | getAllTicketFields(); |
| @@ -812,15 +821,15 @@ | |
| 821 | if( argc!=3 ){ |
| 822 | return Th_WrongNumArgs(interp, "append_field FIELD STRING"); |
| 823 | } |
| 824 | if( g.thTrace ){ |
| 825 | Th_Trace("append_field %#h {%#h}<br>\n", |
| 826 | TH1_LEN(argl[1]), argv[1], TH1_LEN(argl[2]), argv[2]); |
| 827 | } |
| 828 | for(idx=0; idx<nField; idx++){ |
| 829 | if( memcmp(aField[idx].zName, argv[1], TH1_LEN(argl[1]))==0 |
| 830 | && aField[idx].zName[TH1_LEN(argl[1])]==0 ){ |
| 831 | break; |
| 832 | } |
| 833 | } |
| 834 | if( idx>=nField ){ |
| 835 | Th_ErrorMessage(g.interp, "no such TICKET column: ", argv[1], argl[1]); |
| @@ -932,10 +941,11 @@ | |
| 941 | const char *zValue; |
| 942 | int nValue; |
| 943 | if( aField[i].zAppend ) continue; |
| 944 | zValue = Th_Fetch(aField[i].zName, &nValue); |
| 945 | if( zValue ){ |
| 946 | nValue = TH1_LEN(nValue); |
| 947 | while( nValue>0 && fossil_isspace(zValue[nValue-1]) ){ nValue--; } |
| 948 | if( ((aField[i].mUsed & USEDBY_TICKETCHNG)!=0 && nValue>0) |
| 949 | || memcmp(zValue, aField[i].zValue, nValue)!=0 |
| 950 | ||(int)strlen(aField[i].zValue)!=nValue |
| 951 | ){ |
| @@ -1026,32 +1036,34 @@ | |
| 1036 | form_begin(0, "%R/%s", g.zPath); |
| 1037 | if( P("date_override") && g.perm.Setup ){ |
| 1038 | @ <input type="hidden" name="date_override" value="%h(P("date_override"))"> |
| 1039 | } |
| 1040 | zScript = ticket_newpage_code(); |
| 1041 | Th_Store("private_contact", ""); |
| 1042 | if( g.zLogin && g.zLogin[0] ){ |
| 1043 | uid = db_int(0, "SELECT uid FROM user WHERE login=%Q", g.zLogin); |
| 1044 | if( uid ){ |
| 1045 | char * zEmail = |
| 1046 | db_text(0, "SELECT find_emailaddr(info) FROM user WHERE uid=%d", |
| 1047 | uid); |
| 1048 | if( zEmail ){ |
| 1049 | Th_StoreUnsafe("private_contact", zEmail); |
| 1050 | fossil_free(zEmail); |
| 1051 | } |
| 1052 | } |
| 1053 | } |
| 1054 | Th_StoreUnsafe("login", login_name()); |
| 1055 | Th_Store("date", db_text(0, "SELECT datetime('now')")); |
| 1056 | Th_CreateCommand(g.interp, "submit_ticket", submitTicketCmd, |
| 1057 | (void*)&zNewUuid, 0); |
| 1058 | if( g.thTrace ) Th_Trace("BEGIN_TKTNEW_SCRIPT<br>\n", -1); |
| 1059 | if( Th_Render(zScript)==TH_RETURN && !g.thTrace && zNewUuid ){ |
| 1060 | if( P("submitandnew") ){ |
| 1061 | cgi_redirect(mprintf("%R/tktnew/%s", zNewUuid)); |
| 1062 | }else{ |
| 1063 | cgi_redirect(mprintf("%R/tktview/%s", zNewUuid)); |
| 1064 | } |
| 1065 | return; |
| 1066 | } |
| 1067 | captcha_generate(0); |
| 1068 | @ </form> |
| 1069 | if( g.thTrace ) Th_Trace("END_TKTVIEW<br>\n", -1); |
| @@ -1112,11 +1124,11 @@ | |
| 1124 | initializeVariablesFromDb(); |
| 1125 | if( g.zPath[0]=='d' ) showAllFields(); |
| 1126 | form_begin(0, "%R/%s", g.zPath); |
| 1127 | @ <input type="hidden" name="name" value="%s(zName)"> |
| 1128 | zScript = ticket_editpage_code(); |
| 1129 | Th_StoreUnsafe("login", login_name()); |
| 1130 | Th_Store("date", db_text(0, "SELECT datetime('now')")); |
| 1131 | Th_CreateCommand(g.interp, "append_field", appendRemarkCmd, 0, 0); |
| 1132 | Th_CreateCommand(g.interp, "submit_ticket", submitTicketCmd, (void*)&zName,0); |
| 1133 | if( g.thTrace ) Th_Trace("BEGIN_TKTEDIT_SCRIPT<br>\n", -1); |
| 1134 | if( Th_Render(zScript)==TH_RETURN && !g.thTrace && zName ){ |
| 1135 |
+131
-15
| --- src/tktsetup.c | ||
| +++ src/tktsetup.c | ||
| @@ -125,11 +125,11 @@ | ||
| 125 | 125 | if( !g.perm.Setup ){ |
| 126 | 126 | login_needed(0); |
| 127 | 127 | return; |
| 128 | 128 | } |
| 129 | 129 | style_set_current_feature("tktsetup"); |
| 130 | - if( PB("setup") ){ | |
| 130 | + if( P("setup") ){ | |
| 131 | 131 | cgi_redirect("tktsetup"); |
| 132 | 132 | } |
| 133 | 133 | isSubmit = P("submit")!=0; |
| 134 | 134 | z = P("x"); |
| 135 | 135 | if( z==0 ){ |
| @@ -164,10 +164,11 @@ | ||
| 164 | 164 | @ <hr> |
| 165 | 165 | @ <h2>Default %s(zTitle)</h2> |
| 166 | 166 | @ <blockquote><pre> |
| 167 | 167 | @ %h(zDfltValue) |
| 168 | 168 | @ </pre></blockquote> |
| 169 | + style_submenu_element("Back", "%R/tktsetup"); | |
| 169 | 170 | style_finish_page(); |
| 170 | 171 | } |
| 171 | 172 | |
| 172 | 173 | /* |
| 173 | 174 | ** WEBPAGE: tktsetup_tab |
| @@ -301,11 +302,11 @@ | ||
| 301 | 302 | } |
| 302 | 303 | |
| 303 | 304 | static const char zDefaultNew[] = |
| 304 | 305 | @ <th1> |
| 305 | 306 | @ if {![info exists mutype]} {set mutype Markdown} |
| 306 | -@ if {[info exists submit]} { | |
| 307 | +@ if {[info exists submit] || [info exists submitandnew]} { | |
| 307 | 308 | @ set status Open |
| 308 | 309 | @ if {$mutype eq "HTML"} { |
| 309 | 310 | @ set mimetype "text/html" |
| 310 | 311 | @ } elseif {$mutype eq "Wiki"} { |
| 311 | 312 | @ set mimetype "text/x-fossil-wiki" |
| @@ -349,10 +350,28 @@ | ||
| 349 | 350 | @ <td align="left"><th1>combobox severity $severity_choices 1</th1></td> |
| 350 | 351 | @ <td align="left">How debilitating is the problem? How badly does the problem |
| 351 | 352 | @ affect the operation of the product?</td> |
| 352 | 353 | @ </tr> |
| 353 | 354 | @ |
| 355 | +@ <th1> | |
| 356 | +@ if {[capexpr {w}]} { | |
| 357 | +@ html {<tr><td class="tktDspLabel">Priority:</td><td>} | |
| 358 | +@ combobox priority $priority_choices 1 | |
| 359 | +@ html { | |
| 360 | +@ <td align="left">How important is the affected functionality?</td> | |
| 361 | +@ </td></tr> | |
| 362 | +@ } | |
| 363 | +@ | |
| 364 | +@ html {<tr><td class="tktDspLabel">Subsystem:</td><td>} | |
| 365 | +@ combobox subsystem $subsystem_choices 1 | |
| 366 | +@ html { | |
| 367 | +@ <td align="left">Which subsystem is affected?</td> | |
| 368 | +@ </td></tr> | |
| 369 | +@ } | |
| 370 | +@ } | |
| 371 | +@ </th1> | |
| 372 | +@ | |
| 354 | 373 | @ <tr> |
| 355 | 374 | @ <td align="right">EMail:</td> |
| 356 | 375 | @ <td align="left"> |
| 357 | 376 | @ <input name="private_contact" value="$<private_contact>" size="30"> |
| 358 | 377 | @ </td> |
| @@ -405,19 +424,27 @@ | ||
| 405 | 424 | @ <tr> |
| 406 | 425 | @ <td><td align="left"> |
| 407 | 426 | @ <input type="submit" name="submit" value="Submit"> |
| 408 | 427 | @ </td> |
| 409 | 428 | @ <td align="left">After filling in the information above, press this |
| 410 | -@ button to create the new ticket</td> | |
| 429 | +@ button to create the new ticket.</td> | |
| 430 | +@ </tr> | |
| 431 | +@ | |
| 432 | +@ <tr> | |
| 433 | +@ <td><td align="left"> | |
| 434 | +@ <input type="submit" name="submitandnew" value="Submit and New"> | |
| 435 | +@ </td> | |
| 436 | +@ <td align="left">Create the new ticket and start another | |
| 437 | +@ ticket form with the inputs.</td> | |
| 411 | 438 | @ </tr> |
| 412 | 439 | @ <th1>enable_output 1</th1> |
| 413 | 440 | @ |
| 414 | 441 | @ <tr> |
| 415 | 442 | @ <td><td align="left"> |
| 416 | 443 | @ <input type="submit" name="cancel" value="Cancel"> |
| 417 | 444 | @ </td> |
| 418 | -@ <td>Abandon and forget this ticket</td> | |
| 445 | +@ <td>Abandon and forget this ticket.</td> | |
| 419 | 446 | @ </tr> |
| 420 | 447 | @ </table> |
| 421 | 448 | ; |
| 422 | 449 | |
| 423 | 450 | /* |
| @@ -454,11 +481,11 @@ | ||
| 454 | 481 | @ <th1> |
| 455 | 482 | @ if {[info exists tkt_uuid]} { |
| 456 | 483 | @ html "<td class='tktDspValue' colspan='3'>" |
| 457 | 484 | @ copybtn hash-tk 0 $tkt_uuid 2 |
| 458 | 485 | @ if {[hascap s]} { |
| 459 | -@ html " ($tkt_id)" | |
| 486 | +@ puts " ($tkt_id)" | |
| 460 | 487 | @ } |
| 461 | 488 | @ html "</td></tr>\n" |
| 462 | 489 | @ } else { |
| 463 | 490 | @ if {[hascap s]} { |
| 464 | 491 | @ html "<td class='tktDspValue' colspan='3'>Deleted " |
| @@ -465,10 +492,14 @@ | ||
| 465 | 492 | @ html "(0)</td></tr>\n" |
| 466 | 493 | @ } else { |
| 467 | 494 | @ html "<td class='tktDspValue' colspan='3'>Deleted</td></tr>\n" |
| 468 | 495 | @ } |
| 469 | 496 | @ } |
| 497 | +@ | |
| 498 | +@ if {[capexpr {n}]} { | |
| 499 | +@ submenu link "Copy Ticket" /tktnew/$tkt_uuid | |
| 500 | +@ } | |
| 470 | 501 | @ </th1> |
| 471 | 502 | @ <tr><td class="tktDspLabel">Title:</td> |
| 472 | 503 | @ <td class="tktDspValue" colspan="3"> |
| 473 | 504 | @ $<title> |
| 474 | 505 | @ </td></tr> |
| @@ -491,23 +522,63 @@ | ||
| 491 | 522 | @ $<resolution> |
| 492 | 523 | @ </td></tr> |
| 493 | 524 | @ <tr><td class="tktDspLabel">Last Modified:</td><td class="tktDspValue"> |
| 494 | 525 | @ <th1> |
| 495 | 526 | @ if {[info exists tkt_datetime]} { |
| 496 | -@ html $tkt_datetime | |
| 527 | +@ puts $tkt_datetime | |
| 528 | +@ } | |
| 529 | +@ if {[info exists tkt_mage]} { | |
| 530 | +@ html "<br>[htmlize $tkt_mage] ago" | |
| 497 | 531 | @ } |
| 498 | 532 | @ </th1> |
| 499 | 533 | @ </td> |
| 534 | +@ <td class="tktDspLabel">Created:</td><td class="tktDspValue"> | |
| 535 | +@ <th1> | |
| 536 | +@ if {[info exists tkt_datetime_creation]} { | |
| 537 | +@ puts $tkt_datetime_creation | |
| 538 | +@ } | |
| 539 | +@ if {[info exists tkt_cage]} { | |
| 540 | +@ html "<br>[htmlize $tkt_cage] ago" | |
| 541 | +@ } | |
| 542 | +@ </th1> | |
| 543 | +@ </td></tr> | |
| 500 | 544 | @ <th1>enable_output [hascap e]</th1> |
| 501 | -@ <td class="tktDspLabel">Contact:</td><td class="tktDspValue"> | |
| 545 | +@ <tr> | |
| 546 | +@ <td class="tktDspLabel">Contact:</td><td class="tktDspValue" colspan="3"> | |
| 502 | 547 | @ $<private_contact> |
| 503 | 548 | @ </td> |
| 549 | +@ </tr> | |
| 504 | 550 | @ <th1>enable_output 1</th1> |
| 505 | -@ </tr> | |
| 506 | 551 | @ <tr><td class="tktDspLabel">Version Found In:</td> |
| 507 | 552 | @ <td colspan="3" valign="top" class="tktDspValue"> |
| 508 | -@ $<foundin> | |
| 553 | +@ <th1> | |
| 554 | +@ set versionlink "" | |
| 555 | +@ set urlfoundin [httpize $foundin] | |
| 556 | +@ set tagpattern {^[-0-9A-Za-z_\\.]+$} | |
| 557 | +@ if [regexp $tagpattern $foundin] { | |
| 558 | +@ query {SELECT count(*) AS match FROM tag | |
| 559 | +@ WHERE tagname=concat('sym-',$foundin)} { | |
| 560 | +@ if {$match} {set versionlink "timeline?t=$urlfoundin"} | |
| 561 | +@ } | |
| 562 | +@ } | |
| 563 | +@ set hashpattern {^[0-9a-f]+$} | |
| 564 | +@ if [regexp $hashpattern $foundin] { | |
| 565 | +@ set pattern $foundin* | |
| 566 | +@ query {SELECT count(*) AS match FROM blob WHERE uuid GLOB $pattern} { | |
| 567 | +@ if {$match} {set versionlink "info/$urlfoundin"} | |
| 568 | +@ } | |
| 569 | +@ } | |
| 570 | +@ if {$versionlink eq ""} { | |
| 571 | +@ puts $foundin | |
| 572 | +@ } else { | |
| 573 | +@ html "<a href=\"" | |
| 574 | +@ puts $versionlink | |
| 575 | +@ html "\">" | |
| 576 | +@ puts $foundin | |
| 577 | +@ html "</a>" | |
| 578 | +@ } | |
| 579 | +@ </th1> | |
| 509 | 580 | @ </td></tr> |
| 510 | 581 | @ </table> |
| 511 | 582 | @ |
| 512 | 583 | @ <th1> |
| 513 | 584 | @ wiki_assoc "ticket" $tkt_uuid |
| @@ -537,24 +608,25 @@ | ||
| 537 | 608 | @ FROM ticketchng |
| 538 | 609 | @ WHERE tkt_id=$tkt_id AND length(icomment)>0} { |
| 539 | 610 | @ if {$seenRow} { |
| 540 | 611 | @ html "<hr>\n" |
| 541 | 612 | @ } else { |
| 542 | -@ html "<tr><td class='tktDspLabel' style='text-align:left'>User Comments:</td></tr>\n" | |
| 613 | +@ html "<tr><td class='tktDspLabel' style='text-align:left'>\n" | |
| 614 | +@ html "User Comments:</td></tr>\n" | |
| 543 | 615 | @ html "<tr><td colspan='5' class='tktDspValue'>\n" |
| 544 | 616 | @ set seenRow 1 |
| 545 | 617 | @ } |
| 546 | 618 | @ html "<span class='tktDspCommenter'>" |
| 547 | -@ html "[htmlize $xlogin]" | |
| 619 | +@ puts $xlogin | |
| 548 | 620 | @ if {$xlogin ne $xusername && [string length $xusername]>0} { |
| 549 | -@ html " (claiming to be [htmlize $xusername])" | |
| 621 | +@ puts " (claiming to be $xusername)" | |
| 550 | 622 | @ } |
| 551 | -@ html " added on $xdate:" | |
| 623 | +@ puts " added on $xdate:" | |
| 552 | 624 | @ html "</span>\n" |
| 553 | 625 | @ if {$alwaysPlaintext || $xmimetype eq "text/plain"} { |
| 554 | 626 | @ set r [randhex] |
| 555 | -@ if {$xmimetype ne "text/plain"} {html "([htmlize $xmimetype])\n"} | |
| 627 | +@ if {$xmimetype ne "text/plain"} {puts "($xmimetype)\n"} | |
| 556 | 628 | @ wiki "<verbatim-$r>[string trimright $xcomment]</verbatim-$r>\n" |
| 557 | 629 | @ } elseif {$xmimetype eq "text/x-fossil-wiki"} { |
| 558 | 630 | @ wiki "<p>\n[string trimright $xcomment]\n</p>\n" |
| 559 | 631 | @ } elseif {$xmimetype eq "text/x-markdown"} { |
| 560 | 632 | @ html [lindex [markdown $xcomment] 1] |
| @@ -711,10 +783,52 @@ | ||
| 711 | 783 | @ <input type="submit" name="cancel" value="Cancel"> |
| 712 | 784 | @ </td> |
| 713 | 785 | @ <td>Abandon this edit</td> |
| 714 | 786 | @ </tr> |
| 715 | 787 | @ |
| 788 | +@ <th1> | |
| 789 | +@ set seenRow 0 | |
| 790 | +@ set alwaysPlaintext [info exists plaintext] | |
| 791 | +@ query {SELECT datetime(tkt_mtime) AS xdate, login AS xlogin, | |
| 792 | +@ mimetype as xmimetype, icomment AS xcomment, | |
| 793 | +@ username AS xusername | |
| 794 | +@ FROM ticketchng | |
| 795 | +@ WHERE tkt_id=$tkt_id AND length(icomment)>0} { | |
| 796 | +@ if {$seenRow} { | |
| 797 | +@ html "<hr>\n" | |
| 798 | +@ } else { | |
| 799 | +@ html "<tr><td colspan='2'><hr></td></tr>\n" | |
| 800 | +@ html "<tr><td colspan='2' class='tktDspLabel' style='text-align:left'>\n" | |
| 801 | +@ html "Previous User Comments:</td></tr>\n" | |
| 802 | +@ html "<tr><td colspan='2' class='tktDspValue'>\n" | |
| 803 | +@ set seenRow 1 | |
| 804 | +@ } | |
| 805 | +@ html "<span class='tktDspCommenter'>" | |
| 806 | +@ puts $xlogin | |
| 807 | +@ if {$xlogin ne $xusername && [string length $xusername]>0} { | |
| 808 | +@ puts " (claiming to be $xusername)" | |
| 809 | +@ } | |
| 810 | +@ puts " added on $xdate:" | |
| 811 | +@ html "</span>\n" | |
| 812 | +@ if {$alwaysPlaintext || $xmimetype eq "text/plain"} { | |
| 813 | +@ set r [randhex] | |
| 814 | +@ if {$xmimetype ne "text/plain"} {puts "($xmimetype)\n"} | |
| 815 | +@ wiki "<verbatim-$r>[string trimright $xcomment]</verbatim-$r>\n" | |
| 816 | +@ } elseif {$xmimetype eq "text/x-fossil-wiki"} { | |
| 817 | +@ wiki "<p>\n[string trimright $xcomment]\n</p>\n" | |
| 818 | +@ } elseif {$xmimetype eq "text/x-markdown"} { | |
| 819 | +@ html [lindex [markdown $xcomment] 1] | |
| 820 | +@ } elseif {$xmimetype eq "text/html"} { | |
| 821 | +@ wiki "<p><nowiki>\n[string trimright $xcomment]\n</nowiki>\n" | |
| 822 | +@ } else { | |
| 823 | +@ set r [randhex] | |
| 824 | +@ wiki "<verbatim-$r links>[string trimright $xcomment]</verbatim-$r>\n" | |
| 825 | +@ } | |
| 826 | +@ } | |
| 827 | +@ if {$seenRow} {html "</td></tr>\n"} | |
| 828 | +@ </th1> | |
| 829 | +@ | |
| 716 | 830 | @ </table> |
| 717 | 831 | ; |
| 718 | 832 | |
| 719 | 833 | /* |
| 720 | 834 | ** Return the code used to generate the edit ticket page |
| @@ -809,11 +923,12 @@ | ||
| 809 | 923 | @ WHEN status='Fixed' THEN '#cfe8bd' |
| 810 | 924 | @ WHEN status='Tested' THEN '#bde5d6' |
| 811 | 925 | @ WHEN status='Deferred' THEN '#cacae5' |
| 812 | 926 | @ ELSE '#c8c8c8' END AS 'bgcolor', |
| 813 | 927 | @ substr(tkt_uuid,1,10) AS '#', |
| 814 | -@ datetime(tkt_mtime) AS 'mtime', | |
| 928 | +@ datetime(tkt_ctime) AS 'created', | |
| 929 | +@ datetime(tkt_mtime) AS 'modified', | |
| 815 | 930 | @ type, |
| 816 | 931 | @ status, |
| 817 | 932 | @ subsystem, |
| 818 | 933 | @ title, |
| 819 | 934 | @ comment AS '_comments' |
| @@ -943,8 +1058,9 @@ | ||
| 943 | 1058 | @ <input type="submit" name="submit" value="Apply Changes"> |
| 944 | 1059 | @ <input type="submit" name="setup" value="Cancel"> |
| 945 | 1060 | @ </p> |
| 946 | 1061 | @ </div></form> |
| 947 | 1062 | db_end_transaction(0); |
| 1063 | + style_submenu_element("Back", "%R/tktsetup"); | |
| 948 | 1064 | style_finish_page(); |
| 949 | 1065 | |
| 950 | 1066 | } |
| 951 | 1067 |
| --- src/tktsetup.c | |
| +++ src/tktsetup.c | |
| @@ -125,11 +125,11 @@ | |
| 125 | if( !g.perm.Setup ){ |
| 126 | login_needed(0); |
| 127 | return; |
| 128 | } |
| 129 | style_set_current_feature("tktsetup"); |
| 130 | if( PB("setup") ){ |
| 131 | cgi_redirect("tktsetup"); |
| 132 | } |
| 133 | isSubmit = P("submit")!=0; |
| 134 | z = P("x"); |
| 135 | if( z==0 ){ |
| @@ -164,10 +164,11 @@ | |
| 164 | @ <hr> |
| 165 | @ <h2>Default %s(zTitle)</h2> |
| 166 | @ <blockquote><pre> |
| 167 | @ %h(zDfltValue) |
| 168 | @ </pre></blockquote> |
| 169 | style_finish_page(); |
| 170 | } |
| 171 | |
| 172 | /* |
| 173 | ** WEBPAGE: tktsetup_tab |
| @@ -301,11 +302,11 @@ | |
| 301 | } |
| 302 | |
| 303 | static const char zDefaultNew[] = |
| 304 | @ <th1> |
| 305 | @ if {![info exists mutype]} {set mutype Markdown} |
| 306 | @ if {[info exists submit]} { |
| 307 | @ set status Open |
| 308 | @ if {$mutype eq "HTML"} { |
| 309 | @ set mimetype "text/html" |
| 310 | @ } elseif {$mutype eq "Wiki"} { |
| 311 | @ set mimetype "text/x-fossil-wiki" |
| @@ -349,10 +350,28 @@ | |
| 349 | @ <td align="left"><th1>combobox severity $severity_choices 1</th1></td> |
| 350 | @ <td align="left">How debilitating is the problem? How badly does the problem |
| 351 | @ affect the operation of the product?</td> |
| 352 | @ </tr> |
| 353 | @ |
| 354 | @ <tr> |
| 355 | @ <td align="right">EMail:</td> |
| 356 | @ <td align="left"> |
| 357 | @ <input name="private_contact" value="$<private_contact>" size="30"> |
| 358 | @ </td> |
| @@ -405,19 +424,27 @@ | |
| 405 | @ <tr> |
| 406 | @ <td><td align="left"> |
| 407 | @ <input type="submit" name="submit" value="Submit"> |
| 408 | @ </td> |
| 409 | @ <td align="left">After filling in the information above, press this |
| 410 | @ button to create the new ticket</td> |
| 411 | @ </tr> |
| 412 | @ <th1>enable_output 1</th1> |
| 413 | @ |
| 414 | @ <tr> |
| 415 | @ <td><td align="left"> |
| 416 | @ <input type="submit" name="cancel" value="Cancel"> |
| 417 | @ </td> |
| 418 | @ <td>Abandon and forget this ticket</td> |
| 419 | @ </tr> |
| 420 | @ </table> |
| 421 | ; |
| 422 | |
| 423 | /* |
| @@ -454,11 +481,11 @@ | |
| 454 | @ <th1> |
| 455 | @ if {[info exists tkt_uuid]} { |
| 456 | @ html "<td class='tktDspValue' colspan='3'>" |
| 457 | @ copybtn hash-tk 0 $tkt_uuid 2 |
| 458 | @ if {[hascap s]} { |
| 459 | @ html " ($tkt_id)" |
| 460 | @ } |
| 461 | @ html "</td></tr>\n" |
| 462 | @ } else { |
| 463 | @ if {[hascap s]} { |
| 464 | @ html "<td class='tktDspValue' colspan='3'>Deleted " |
| @@ -465,10 +492,14 @@ | |
| 465 | @ html "(0)</td></tr>\n" |
| 466 | @ } else { |
| 467 | @ html "<td class='tktDspValue' colspan='3'>Deleted</td></tr>\n" |
| 468 | @ } |
| 469 | @ } |
| 470 | @ </th1> |
| 471 | @ <tr><td class="tktDspLabel">Title:</td> |
| 472 | @ <td class="tktDspValue" colspan="3"> |
| 473 | @ $<title> |
| 474 | @ </td></tr> |
| @@ -491,23 +522,63 @@ | |
| 491 | @ $<resolution> |
| 492 | @ </td></tr> |
| 493 | @ <tr><td class="tktDspLabel">Last Modified:</td><td class="tktDspValue"> |
| 494 | @ <th1> |
| 495 | @ if {[info exists tkt_datetime]} { |
| 496 | @ html $tkt_datetime |
| 497 | @ } |
| 498 | @ </th1> |
| 499 | @ </td> |
| 500 | @ <th1>enable_output [hascap e]</th1> |
| 501 | @ <td class="tktDspLabel">Contact:</td><td class="tktDspValue"> |
| 502 | @ $<private_contact> |
| 503 | @ </td> |
| 504 | @ <th1>enable_output 1</th1> |
| 505 | @ </tr> |
| 506 | @ <tr><td class="tktDspLabel">Version Found In:</td> |
| 507 | @ <td colspan="3" valign="top" class="tktDspValue"> |
| 508 | @ $<foundin> |
| 509 | @ </td></tr> |
| 510 | @ </table> |
| 511 | @ |
| 512 | @ <th1> |
| 513 | @ wiki_assoc "ticket" $tkt_uuid |
| @@ -537,24 +608,25 @@ | |
| 537 | @ FROM ticketchng |
| 538 | @ WHERE tkt_id=$tkt_id AND length(icomment)>0} { |
| 539 | @ if {$seenRow} { |
| 540 | @ html "<hr>\n" |
| 541 | @ } else { |
| 542 | @ html "<tr><td class='tktDspLabel' style='text-align:left'>User Comments:</td></tr>\n" |
| 543 | @ html "<tr><td colspan='5' class='tktDspValue'>\n" |
| 544 | @ set seenRow 1 |
| 545 | @ } |
| 546 | @ html "<span class='tktDspCommenter'>" |
| 547 | @ html "[htmlize $xlogin]" |
| 548 | @ if {$xlogin ne $xusername && [string length $xusername]>0} { |
| 549 | @ html " (claiming to be [htmlize $xusername])" |
| 550 | @ } |
| 551 | @ html " added on $xdate:" |
| 552 | @ html "</span>\n" |
| 553 | @ if {$alwaysPlaintext || $xmimetype eq "text/plain"} { |
| 554 | @ set r [randhex] |
| 555 | @ if {$xmimetype ne "text/plain"} {html "([htmlize $xmimetype])\n"} |
| 556 | @ wiki "<verbatim-$r>[string trimright $xcomment]</verbatim-$r>\n" |
| 557 | @ } elseif {$xmimetype eq "text/x-fossil-wiki"} { |
| 558 | @ wiki "<p>\n[string trimright $xcomment]\n</p>\n" |
| 559 | @ } elseif {$xmimetype eq "text/x-markdown"} { |
| 560 | @ html [lindex [markdown $xcomment] 1] |
| @@ -711,10 +783,52 @@ | |
| 711 | @ <input type="submit" name="cancel" value="Cancel"> |
| 712 | @ </td> |
| 713 | @ <td>Abandon this edit</td> |
| 714 | @ </tr> |
| 715 | @ |
| 716 | @ </table> |
| 717 | ; |
| 718 | |
| 719 | /* |
| 720 | ** Return the code used to generate the edit ticket page |
| @@ -809,11 +923,12 @@ | |
| 809 | @ WHEN status='Fixed' THEN '#cfe8bd' |
| 810 | @ WHEN status='Tested' THEN '#bde5d6' |
| 811 | @ WHEN status='Deferred' THEN '#cacae5' |
| 812 | @ ELSE '#c8c8c8' END AS 'bgcolor', |
| 813 | @ substr(tkt_uuid,1,10) AS '#', |
| 814 | @ datetime(tkt_mtime) AS 'mtime', |
| 815 | @ type, |
| 816 | @ status, |
| 817 | @ subsystem, |
| 818 | @ title, |
| 819 | @ comment AS '_comments' |
| @@ -943,8 +1058,9 @@ | |
| 943 | @ <input type="submit" name="submit" value="Apply Changes"> |
| 944 | @ <input type="submit" name="setup" value="Cancel"> |
| 945 | @ </p> |
| 946 | @ </div></form> |
| 947 | db_end_transaction(0); |
| 948 | style_finish_page(); |
| 949 | |
| 950 | } |
| 951 |
| --- src/tktsetup.c | |
| +++ src/tktsetup.c | |
| @@ -125,11 +125,11 @@ | |
| 125 | if( !g.perm.Setup ){ |
| 126 | login_needed(0); |
| 127 | return; |
| 128 | } |
| 129 | style_set_current_feature("tktsetup"); |
| 130 | if( P("setup") ){ |
| 131 | cgi_redirect("tktsetup"); |
| 132 | } |
| 133 | isSubmit = P("submit")!=0; |
| 134 | z = P("x"); |
| 135 | if( z==0 ){ |
| @@ -164,10 +164,11 @@ | |
| 164 | @ <hr> |
| 165 | @ <h2>Default %s(zTitle)</h2> |
| 166 | @ <blockquote><pre> |
| 167 | @ %h(zDfltValue) |
| 168 | @ </pre></blockquote> |
| 169 | style_submenu_element("Back", "%R/tktsetup"); |
| 170 | style_finish_page(); |
| 171 | } |
| 172 | |
| 173 | /* |
| 174 | ** WEBPAGE: tktsetup_tab |
| @@ -301,11 +302,11 @@ | |
| 302 | } |
| 303 | |
| 304 | static const char zDefaultNew[] = |
| 305 | @ <th1> |
| 306 | @ if {![info exists mutype]} {set mutype Markdown} |
| 307 | @ if {[info exists submit] || [info exists submitandnew]} { |
| 308 | @ set status Open |
| 309 | @ if {$mutype eq "HTML"} { |
| 310 | @ set mimetype "text/html" |
| 311 | @ } elseif {$mutype eq "Wiki"} { |
| 312 | @ set mimetype "text/x-fossil-wiki" |
| @@ -349,10 +350,28 @@ | |
| 350 | @ <td align="left"><th1>combobox severity $severity_choices 1</th1></td> |
| 351 | @ <td align="left">How debilitating is the problem? How badly does the problem |
| 352 | @ affect the operation of the product?</td> |
| 353 | @ </tr> |
| 354 | @ |
| 355 | @ <th1> |
| 356 | @ if {[capexpr {w}]} { |
| 357 | @ html {<tr><td class="tktDspLabel">Priority:</td><td>} |
| 358 | @ combobox priority $priority_choices 1 |
| 359 | @ html { |
| 360 | @ <td align="left">How important is the affected functionality?</td> |
| 361 | @ </td></tr> |
| 362 | @ } |
| 363 | @ |
| 364 | @ html {<tr><td class="tktDspLabel">Subsystem:</td><td>} |
| 365 | @ combobox subsystem $subsystem_choices 1 |
| 366 | @ html { |
| 367 | @ <td align="left">Which subsystem is affected?</td> |
| 368 | @ </td></tr> |
| 369 | @ } |
| 370 | @ } |
| 371 | @ </th1> |
| 372 | @ |
| 373 | @ <tr> |
| 374 | @ <td align="right">EMail:</td> |
| 375 | @ <td align="left"> |
| 376 | @ <input name="private_contact" value="$<private_contact>" size="30"> |
| 377 | @ </td> |
| @@ -405,19 +424,27 @@ | |
| 424 | @ <tr> |
| 425 | @ <td><td align="left"> |
| 426 | @ <input type="submit" name="submit" value="Submit"> |
| 427 | @ </td> |
| 428 | @ <td align="left">After filling in the information above, press this |
| 429 | @ button to create the new ticket.</td> |
| 430 | @ </tr> |
| 431 | @ |
| 432 | @ <tr> |
| 433 | @ <td><td align="left"> |
| 434 | @ <input type="submit" name="submitandnew" value="Submit and New"> |
| 435 | @ </td> |
| 436 | @ <td align="left">Create the new ticket and start another |
| 437 | @ ticket form with the inputs.</td> |
| 438 | @ </tr> |
| 439 | @ <th1>enable_output 1</th1> |
| 440 | @ |
| 441 | @ <tr> |
| 442 | @ <td><td align="left"> |
| 443 | @ <input type="submit" name="cancel" value="Cancel"> |
| 444 | @ </td> |
| 445 | @ <td>Abandon and forget this ticket.</td> |
| 446 | @ </tr> |
| 447 | @ </table> |
| 448 | ; |
| 449 | |
| 450 | /* |
| @@ -454,11 +481,11 @@ | |
| 481 | @ <th1> |
| 482 | @ if {[info exists tkt_uuid]} { |
| 483 | @ html "<td class='tktDspValue' colspan='3'>" |
| 484 | @ copybtn hash-tk 0 $tkt_uuid 2 |
| 485 | @ if {[hascap s]} { |
| 486 | @ puts " ($tkt_id)" |
| 487 | @ } |
| 488 | @ html "</td></tr>\n" |
| 489 | @ } else { |
| 490 | @ if {[hascap s]} { |
| 491 | @ html "<td class='tktDspValue' colspan='3'>Deleted " |
| @@ -465,10 +492,14 @@ | |
| 492 | @ html "(0)</td></tr>\n" |
| 493 | @ } else { |
| 494 | @ html "<td class='tktDspValue' colspan='3'>Deleted</td></tr>\n" |
| 495 | @ } |
| 496 | @ } |
| 497 | @ |
| 498 | @ if {[capexpr {n}]} { |
| 499 | @ submenu link "Copy Ticket" /tktnew/$tkt_uuid |
| 500 | @ } |
| 501 | @ </th1> |
| 502 | @ <tr><td class="tktDspLabel">Title:</td> |
| 503 | @ <td class="tktDspValue" colspan="3"> |
| 504 | @ $<title> |
| 505 | @ </td></tr> |
| @@ -491,23 +522,63 @@ | |
| 522 | @ $<resolution> |
| 523 | @ </td></tr> |
| 524 | @ <tr><td class="tktDspLabel">Last Modified:</td><td class="tktDspValue"> |
| 525 | @ <th1> |
| 526 | @ if {[info exists tkt_datetime]} { |
| 527 | @ puts $tkt_datetime |
| 528 | @ } |
| 529 | @ if {[info exists tkt_mage]} { |
| 530 | @ html "<br>[htmlize $tkt_mage] ago" |
| 531 | @ } |
| 532 | @ </th1> |
| 533 | @ </td> |
| 534 | @ <td class="tktDspLabel">Created:</td><td class="tktDspValue"> |
| 535 | @ <th1> |
| 536 | @ if {[info exists tkt_datetime_creation]} { |
| 537 | @ puts $tkt_datetime_creation |
| 538 | @ } |
| 539 | @ if {[info exists tkt_cage]} { |
| 540 | @ html "<br>[htmlize $tkt_cage] ago" |
| 541 | @ } |
| 542 | @ </th1> |
| 543 | @ </td></tr> |
| 544 | @ <th1>enable_output [hascap e]</th1> |
| 545 | @ <tr> |
| 546 | @ <td class="tktDspLabel">Contact:</td><td class="tktDspValue" colspan="3"> |
| 547 | @ $<private_contact> |
| 548 | @ </td> |
| 549 | @ </tr> |
| 550 | @ <th1>enable_output 1</th1> |
| 551 | @ <tr><td class="tktDspLabel">Version Found In:</td> |
| 552 | @ <td colspan="3" valign="top" class="tktDspValue"> |
| 553 | @ <th1> |
| 554 | @ set versionlink "" |
| 555 | @ set urlfoundin [httpize $foundin] |
| 556 | @ set tagpattern {^[-0-9A-Za-z_\\.]+$} |
| 557 | @ if [regexp $tagpattern $foundin] { |
| 558 | @ query {SELECT count(*) AS match FROM tag |
| 559 | @ WHERE tagname=concat('sym-',$foundin)} { |
| 560 | @ if {$match} {set versionlink "timeline?t=$urlfoundin"} |
| 561 | @ } |
| 562 | @ } |
| 563 | @ set hashpattern {^[0-9a-f]+$} |
| 564 | @ if [regexp $hashpattern $foundin] { |
| 565 | @ set pattern $foundin* |
| 566 | @ query {SELECT count(*) AS match FROM blob WHERE uuid GLOB $pattern} { |
| 567 | @ if {$match} {set versionlink "info/$urlfoundin"} |
| 568 | @ } |
| 569 | @ } |
| 570 | @ if {$versionlink eq ""} { |
| 571 | @ puts $foundin |
| 572 | @ } else { |
| 573 | @ html "<a href=\"" |
| 574 | @ puts $versionlink |
| 575 | @ html "\">" |
| 576 | @ puts $foundin |
| 577 | @ html "</a>" |
| 578 | @ } |
| 579 | @ </th1> |
| 580 | @ </td></tr> |
| 581 | @ </table> |
| 582 | @ |
| 583 | @ <th1> |
| 584 | @ wiki_assoc "ticket" $tkt_uuid |
| @@ -537,24 +608,25 @@ | |
| 608 | @ FROM ticketchng |
| 609 | @ WHERE tkt_id=$tkt_id AND length(icomment)>0} { |
| 610 | @ if {$seenRow} { |
| 611 | @ html "<hr>\n" |
| 612 | @ } else { |
| 613 | @ html "<tr><td class='tktDspLabel' style='text-align:left'>\n" |
| 614 | @ html "User Comments:</td></tr>\n" |
| 615 | @ html "<tr><td colspan='5' class='tktDspValue'>\n" |
| 616 | @ set seenRow 1 |
| 617 | @ } |
| 618 | @ html "<span class='tktDspCommenter'>" |
| 619 | @ puts $xlogin |
| 620 | @ if {$xlogin ne $xusername && [string length $xusername]>0} { |
| 621 | @ puts " (claiming to be $xusername)" |
| 622 | @ } |
| 623 | @ puts " added on $xdate:" |
| 624 | @ html "</span>\n" |
| 625 | @ if {$alwaysPlaintext || $xmimetype eq "text/plain"} { |
| 626 | @ set r [randhex] |
| 627 | @ if {$xmimetype ne "text/plain"} {puts "($xmimetype)\n"} |
| 628 | @ wiki "<verbatim-$r>[string trimright $xcomment]</verbatim-$r>\n" |
| 629 | @ } elseif {$xmimetype eq "text/x-fossil-wiki"} { |
| 630 | @ wiki "<p>\n[string trimright $xcomment]\n</p>\n" |
| 631 | @ } elseif {$xmimetype eq "text/x-markdown"} { |
| 632 | @ html [lindex [markdown $xcomment] 1] |
| @@ -711,10 +783,52 @@ | |
| 783 | @ <input type="submit" name="cancel" value="Cancel"> |
| 784 | @ </td> |
| 785 | @ <td>Abandon this edit</td> |
| 786 | @ </tr> |
| 787 | @ |
| 788 | @ <th1> |
| 789 | @ set seenRow 0 |
| 790 | @ set alwaysPlaintext [info exists plaintext] |
| 791 | @ query {SELECT datetime(tkt_mtime) AS xdate, login AS xlogin, |
| 792 | @ mimetype as xmimetype, icomment AS xcomment, |
| 793 | @ username AS xusername |
| 794 | @ FROM ticketchng |
| 795 | @ WHERE tkt_id=$tkt_id AND length(icomment)>0} { |
| 796 | @ if {$seenRow} { |
| 797 | @ html "<hr>\n" |
| 798 | @ } else { |
| 799 | @ html "<tr><td colspan='2'><hr></td></tr>\n" |
| 800 | @ html "<tr><td colspan='2' class='tktDspLabel' style='text-align:left'>\n" |
| 801 | @ html "Previous User Comments:</td></tr>\n" |
| 802 | @ html "<tr><td colspan='2' class='tktDspValue'>\n" |
| 803 | @ set seenRow 1 |
| 804 | @ } |
| 805 | @ html "<span class='tktDspCommenter'>" |
| 806 | @ puts $xlogin |
| 807 | @ if {$xlogin ne $xusername && [string length $xusername]>0} { |
| 808 | @ puts " (claiming to be $xusername)" |
| 809 | @ } |
| 810 | @ puts " added on $xdate:" |
| 811 | @ html "</span>\n" |
| 812 | @ if {$alwaysPlaintext || $xmimetype eq "text/plain"} { |
| 813 | @ set r [randhex] |
| 814 | @ if {$xmimetype ne "text/plain"} {puts "($xmimetype)\n"} |
| 815 | @ wiki "<verbatim-$r>[string trimright $xcomment]</verbatim-$r>\n" |
| 816 | @ } elseif {$xmimetype eq "text/x-fossil-wiki"} { |
| 817 | @ wiki "<p>\n[string trimright $xcomment]\n</p>\n" |
| 818 | @ } elseif {$xmimetype eq "text/x-markdown"} { |
| 819 | @ html [lindex [markdown $xcomment] 1] |
| 820 | @ } elseif {$xmimetype eq "text/html"} { |
| 821 | @ wiki "<p><nowiki>\n[string trimright $xcomment]\n</nowiki>\n" |
| 822 | @ } else { |
| 823 | @ set r [randhex] |
| 824 | @ wiki "<verbatim-$r links>[string trimright $xcomment]</verbatim-$r>\n" |
| 825 | @ } |
| 826 | @ } |
| 827 | @ if {$seenRow} {html "</td></tr>\n"} |
| 828 | @ </th1> |
| 829 | @ |
| 830 | @ </table> |
| 831 | ; |
| 832 | |
| 833 | /* |
| 834 | ** Return the code used to generate the edit ticket page |
| @@ -809,11 +923,12 @@ | |
| 923 | @ WHEN status='Fixed' THEN '#cfe8bd' |
| 924 | @ WHEN status='Tested' THEN '#bde5d6' |
| 925 | @ WHEN status='Deferred' THEN '#cacae5' |
| 926 | @ ELSE '#c8c8c8' END AS 'bgcolor', |
| 927 | @ substr(tkt_uuid,1,10) AS '#', |
| 928 | @ datetime(tkt_ctime) AS 'created', |
| 929 | @ datetime(tkt_mtime) AS 'modified', |
| 930 | @ type, |
| 931 | @ status, |
| 932 | @ subsystem, |
| 933 | @ title, |
| 934 | @ comment AS '_comments' |
| @@ -943,8 +1058,9 @@ | |
| 1058 | @ <input type="submit" name="submit" value="Apply Changes"> |
| 1059 | @ <input type="submit" name="setup" value="Cancel"> |
| 1060 | @ </p> |
| 1061 | @ </div></form> |
| 1062 | db_end_transaction(0); |
| 1063 | style_submenu_element("Back", "%R/tktsetup"); |
| 1064 | style_finish_page(); |
| 1065 | |
| 1066 | } |
| 1067 |
+1
-1
| --- src/undo.c | ||
| +++ src/undo.c | ||
| @@ -70,11 +70,11 @@ | ||
| 70 | 70 | old_exe = db_column_int(&q, 2); |
| 71 | 71 | if( old_exists ){ |
| 72 | 72 | db_ephemeral_blob(&q, 0, &new); |
| 73 | 73 | } |
| 74 | 74 | if( file_unsafe_in_tree_path(zFullname) ){ |
| 75 | - /* do nothign with this unsafe file */ | |
| 75 | + /* do nothing with this unsafe file */ | |
| 76 | 76 | }else if( old_exists ){ |
| 77 | 77 | if( new_exists ){ |
| 78 | 78 | fossil_print("%s %s\n", redoFlag ? "REDO" : "UNDO", zPathname); |
| 79 | 79 | }else{ |
| 80 | 80 | fossil_print("NEW %s\n", zPathname); |
| 81 | 81 |
| --- src/undo.c | |
| +++ src/undo.c | |
| @@ -70,11 +70,11 @@ | |
| 70 | old_exe = db_column_int(&q, 2); |
| 71 | if( old_exists ){ |
| 72 | db_ephemeral_blob(&q, 0, &new); |
| 73 | } |
| 74 | if( file_unsafe_in_tree_path(zFullname) ){ |
| 75 | /* do nothign with this unsafe file */ |
| 76 | }else if( old_exists ){ |
| 77 | if( new_exists ){ |
| 78 | fossil_print("%s %s\n", redoFlag ? "REDO" : "UNDO", zPathname); |
| 79 | }else{ |
| 80 | fossil_print("NEW %s\n", zPathname); |
| 81 |
| --- src/undo.c | |
| +++ src/undo.c | |
| @@ -70,11 +70,11 @@ | |
| 70 | old_exe = db_column_int(&q, 2); |
| 71 | if( old_exists ){ |
| 72 | db_ephemeral_blob(&q, 0, &new); |
| 73 | } |
| 74 | if( file_unsafe_in_tree_path(zFullname) ){ |
| 75 | /* do nothing with this unsafe file */ |
| 76 | }else if( old_exists ){ |
| 77 | if( new_exists ){ |
| 78 | fossil_print("%s %s\n", redoFlag ? "REDO" : "UNDO", zPathname); |
| 79 | }else{ |
| 80 | fossil_print("NEW %s\n", zPathname); |
| 81 |
+5
-3
| --- src/unversioned.c | ||
| +++ src/unversioned.c | ||
| @@ -246,10 +246,12 @@ | ||
| 246 | 246 | ** a single file at a time. |
| 247 | 247 | ** |
| 248 | 248 | ** cat FILE ... Concatenate the content of FILEs to stdout. |
| 249 | 249 | ** |
| 250 | 250 | ** edit FILE Bring up FILE in a text editor for modification. |
| 251 | +** Options: | |
| 252 | +** --editor NAME Name of the text editor to use | |
| 251 | 253 | ** |
| 252 | 254 | ** export FILE OUTPUT Write the content of FILE into OUTPUT on disk |
| 253 | 255 | ** |
| 254 | 256 | ** list | ls Show all unversioned files held in the local |
| 255 | 257 | ** repository. |
| @@ -361,17 +363,17 @@ | ||
| 361 | 363 | const char *zTFile; /* Temporary file */ |
| 362 | 364 | const char *zUVFile; /* Name of the unversioned file */ |
| 363 | 365 | char *zCmd; /* Command to run the text editor */ |
| 364 | 366 | Blob content; /* Content of the unversioned file */ |
| 365 | 367 | |
| 366 | - verify_all_options(); | |
| 367 | - if( g.argc!=4) usage("edit UVFILE"); | |
| 368 | - zUVFile = g.argv[3]; | |
| 369 | 368 | zEditor = fossil_text_editor(); |
| 370 | 369 | if( zEditor==0 ){ |
| 371 | 370 | fossil_fatal("no text editor - set the VISUAL env variable"); |
| 372 | 371 | } |
| 372 | + verify_all_options(); | |
| 373 | + if( g.argc!=4) usage("edit UVFILE"); | |
| 374 | + zUVFile = g.argv[3]; | |
| 373 | 375 | zTFile = fossil_temp_filename(); |
| 374 | 376 | if( zTFile==0 ) fossil_fatal("cannot find a temporary filename"); |
| 375 | 377 | db_begin_transaction(); |
| 376 | 378 | content_rcvid_init("#!fossil unversioned edit"); |
| 377 | 379 | if( unversioned_content(zUVFile, &content)==0 ){ |
| 378 | 380 |
| --- src/unversioned.c | |
| +++ src/unversioned.c | |
| @@ -246,10 +246,12 @@ | |
| 246 | ** a single file at a time. |
| 247 | ** |
| 248 | ** cat FILE ... Concatenate the content of FILEs to stdout. |
| 249 | ** |
| 250 | ** edit FILE Bring up FILE in a text editor for modification. |
| 251 | ** |
| 252 | ** export FILE OUTPUT Write the content of FILE into OUTPUT on disk |
| 253 | ** |
| 254 | ** list | ls Show all unversioned files held in the local |
| 255 | ** repository. |
| @@ -361,17 +363,17 @@ | |
| 361 | const char *zTFile; /* Temporary file */ |
| 362 | const char *zUVFile; /* Name of the unversioned file */ |
| 363 | char *zCmd; /* Command to run the text editor */ |
| 364 | Blob content; /* Content of the unversioned file */ |
| 365 | |
| 366 | verify_all_options(); |
| 367 | if( g.argc!=4) usage("edit UVFILE"); |
| 368 | zUVFile = g.argv[3]; |
| 369 | zEditor = fossil_text_editor(); |
| 370 | if( zEditor==0 ){ |
| 371 | fossil_fatal("no text editor - set the VISUAL env variable"); |
| 372 | } |
| 373 | zTFile = fossil_temp_filename(); |
| 374 | if( zTFile==0 ) fossil_fatal("cannot find a temporary filename"); |
| 375 | db_begin_transaction(); |
| 376 | content_rcvid_init("#!fossil unversioned edit"); |
| 377 | if( unversioned_content(zUVFile, &content)==0 ){ |
| 378 |
| --- src/unversioned.c | |
| +++ src/unversioned.c | |
| @@ -246,10 +246,12 @@ | |
| 246 | ** a single file at a time. |
| 247 | ** |
| 248 | ** cat FILE ... Concatenate the content of FILEs to stdout. |
| 249 | ** |
| 250 | ** edit FILE Bring up FILE in a text editor for modification. |
| 251 | ** Options: |
| 252 | ** --editor NAME Name of the text editor to use |
| 253 | ** |
| 254 | ** export FILE OUTPUT Write the content of FILE into OUTPUT on disk |
| 255 | ** |
| 256 | ** list | ls Show all unversioned files held in the local |
| 257 | ** repository. |
| @@ -361,17 +363,17 @@ | |
| 363 | const char *zTFile; /* Temporary file */ |
| 364 | const char *zUVFile; /* Name of the unversioned file */ |
| 365 | char *zCmd; /* Command to run the text editor */ |
| 366 | Blob content; /* Content of the unversioned file */ |
| 367 | |
| 368 | zEditor = fossil_text_editor(); |
| 369 | if( zEditor==0 ){ |
| 370 | fossil_fatal("no text editor - set the VISUAL env variable"); |
| 371 | } |
| 372 | verify_all_options(); |
| 373 | if( g.argc!=4) usage("edit UVFILE"); |
| 374 | zUVFile = g.argv[3]; |
| 375 | zTFile = fossil_temp_filename(); |
| 376 | if( zTFile==0 ) fossil_fatal("cannot find a temporary filename"); |
| 377 | db_begin_transaction(); |
| 378 | content_rcvid_init("#!fossil unversioned edit"); |
| 379 | if( unversioned_content(zUVFile, &content)==0 ){ |
| 380 |
+79
-41
| --- src/user.c | ||
| +++ src/user.c | ||
| @@ -326,14 +326,21 @@ | ||
| 326 | 326 | ** |
| 327 | 327 | ** > fossil user contact USERNAME ?CONTACT-INFO? |
| 328 | 328 | ** |
| 329 | 329 | ** Query or set contact information for user USERNAME |
| 330 | 330 | ** |
| 331 | -** > fossil user default ?USERNAME? | |
| 331 | +** > fossil user default ?OPTIONS? ?USERNAME? | |
| 332 | 332 | ** |
| 333 | 333 | ** Query or set the default user. The default user is the |
| 334 | -** user for command-line interaction. | |
| 334 | +** user for command-line interaction. If USERNAME is an | |
| 335 | +** empty string, then the default user is unset from the | |
| 336 | +** repository and will subsequently be determined by the -U | |
| 337 | +** command-line option or by environment variables | |
| 338 | +** FOSSIL_USER, USER, LOGNAME, or USERNAME, in that order. | |
| 339 | +** OPTIONS: | |
| 340 | +** | |
| 341 | +** -v|--verbose Show how the default user is computed | |
| 335 | 342 | ** |
| 336 | 343 | ** > fossil user list | ls |
| 337 | 344 | ** |
| 338 | 345 | ** List all users known to the repository |
| 339 | 346 | ** |
| @@ -385,21 +392,50 @@ | ||
| 385 | 392 | &login, zPw, &caps, &contact |
| 386 | 393 | ); |
| 387 | 394 | db_protect_pop(); |
| 388 | 395 | free(zPw); |
| 389 | 396 | }else if( n>=2 && strncmp(g.argv[2],"default",n)==0 ){ |
| 390 | - if( g.argc==3 ){ | |
| 391 | - user_select(); | |
| 392 | - fossil_print("%s\n", g.zLogin); | |
| 393 | - }else{ | |
| 394 | - if( !db_exists("SELECT 1 FROM user WHERE login=%Q", g.argv[3]) ){ | |
| 395 | - fossil_fatal("no such user: %s", g.argv[3]); | |
| 396 | - } | |
| 397 | - if( g.localOpen ){ | |
| 398 | - db_lset("default-user", g.argv[3]); | |
| 399 | - }else{ | |
| 400 | - db_set("default-user", g.argv[3], 0); | |
| 397 | + int eVerbose = find_option("verbose","v",0)!=0; | |
| 398 | + verify_all_options(); | |
| 399 | + if( g.argc>3 ){ | |
| 400 | + const char *zUser = g.argv[3]; | |
| 401 | + if( fossil_strcmp(zUser,"")==0 || fossil_stricmp(zUser,"nobody")==0 ){ | |
| 402 | + db_begin_transaction(); | |
| 403 | + if( g.localOpen ){ | |
| 404 | + db_multi_exec("DELETE FROM vvar WHERE name='default-user'"); | |
| 405 | + } | |
| 406 | + db_unset("default-user",0); | |
| 407 | + db_commit_transaction(); | |
| 408 | + }else{ | |
| 409 | + if( !db_exists("SELECT 1 FROM user WHERE login=%Q", g.argv[3]) ){ | |
| 410 | + fossil_fatal("no such user: %s", g.argv[3]); | |
| 411 | + } | |
| 412 | + if( g.localOpen ){ | |
| 413 | + db_lset("default-user", g.argv[3]); | |
| 414 | + }else{ | |
| 415 | + db_set("default-user", g.argv[3], 0); | |
| 416 | + } | |
| 417 | + } | |
| 418 | + } | |
| 419 | + if( g.argc==3 || eVerbose ){ | |
| 420 | + int eHow = user_select(); | |
| 421 | + const char *zHow = "???"; | |
| 422 | + switch( eHow ){ | |
| 423 | + case 1: zHow = "-U option"; break; | |
| 424 | + case 2: zHow = "previously set"; break; | |
| 425 | + case 3: zHow = "local check-out"; break; | |
| 426 | + case 4: zHow = "repository"; break; | |
| 427 | + case 5: zHow = "FOSSIL_USER"; break; | |
| 428 | + case 6: zHow = "USER"; break; | |
| 429 | + case 7: zHow = "LOGNAME"; break; | |
| 430 | + case 8: zHow = "USERNAME"; break; | |
| 431 | + case 9: zHow = "URL"; break; | |
| 432 | + } | |
| 433 | + if( eVerbose ){ | |
| 434 | + fossil_print("%s (determined by %s)\n", g.zLogin, zHow); | |
| 435 | + }else{ | |
| 436 | + fossil_print("%s\n", g.zLogin); | |
| 401 | 437 | } |
| 402 | 438 | } |
| 403 | 439 | }else if(( n>=2 && strncmp(g.argv[2],"list",n)==0 ) || |
| 404 | 440 | ( n>=2 && strncmp(g.argv[2],"ls",n)==0 )){ |
| 405 | 441 | Stmt q; |
| @@ -496,52 +532,54 @@ | ||
| 496 | 532 | /* |
| 497 | 533 | ** Figure out what user is at the controls. |
| 498 | 534 | ** |
| 499 | 535 | ** (1) Use the --user and -U command-line options. |
| 500 | 536 | ** |
| 501 | -** (2) If the local database is open, check in VVAR. | |
| 502 | -** | |
| 503 | -** (3) Check the default user in the repository | |
| 504 | -** | |
| 505 | -** (4) Try the FOSSIL_USER environment variable. | |
| 506 | -** | |
| 507 | -** (5) Try the USER environment variable. | |
| 508 | -** | |
| 509 | -** (6) Try the LOGNAME environment variable. | |
| 510 | -** | |
| 511 | -** (7) Try the USERNAME environment variable. | |
| 512 | -** | |
| 513 | -** (8) Check if the user can be extracted from the remote URL. | |
| 537 | +** (2) The name used for login (if there was a login). | |
| 538 | +** | |
| 539 | +** (3) If the local database is open, check in VVAR. | |
| 540 | +** | |
| 541 | +** (4) Check the default-user in the repository | |
| 542 | +** | |
| 543 | +** (5) Try the FOSSIL_USER environment variable. | |
| 544 | +** | |
| 545 | +** (6) Try the USER environment variable. | |
| 546 | +** | |
| 547 | +** (7) Try the LOGNAME environment variable. | |
| 548 | +** | |
| 549 | +** (8) Try the USERNAME environment variable. | |
| 550 | +** | |
| 551 | +** (9) Check if the user can be extracted from the remote URL. | |
| 514 | 552 | ** |
| 515 | 553 | ** The user name is stored in g.zLogin. The uid is in g.userUid. |
| 516 | 554 | */ |
| 517 | -void user_select(void){ | |
| 555 | +int user_select(void){ | |
| 518 | 556 | UrlData url; |
| 519 | - if( g.userUid ) return; | |
| 557 | + if( g.userUid ) return 1; | |
| 520 | 558 | if( g.zLogin ){ |
| 521 | 559 | if( attempt_user(g.zLogin)==0 ){ |
| 522 | 560 | fossil_fatal("no such user: %s", g.zLogin); |
| 523 | 561 | }else{ |
| 524 | - return; | |
| 562 | + return 2; | |
| 525 | 563 | } |
| 526 | 564 | } |
| 527 | 565 | |
| 528 | - if( g.localOpen && attempt_user(db_lget("default-user",0)) ) return; | |
| 529 | - | |
| 530 | - if( attempt_user(db_get("default-user", 0)) ) return; | |
| 531 | - | |
| 532 | - if( attempt_user(fossil_getenv("FOSSIL_USER")) ) return; | |
| 533 | - | |
| 534 | - if( attempt_user(fossil_getenv("USER")) ) return; | |
| 535 | - | |
| 536 | - if( attempt_user(fossil_getenv("LOGNAME")) ) return; | |
| 537 | - | |
| 538 | - if( attempt_user(fossil_getenv("USERNAME")) ) return; | |
| 566 | + if( g.localOpen && attempt_user(db_lget("default-user",0)) ) return 3; | |
| 567 | + | |
| 568 | + if( attempt_user(db_get("default-user", 0)) ) return 4; | |
| 569 | + | |
| 570 | + if( attempt_user(fossil_getenv("FOSSIL_USER")) ) return 5; | |
| 571 | + | |
| 572 | + if( attempt_user(fossil_getenv("USER")) ) return 6; | |
| 573 | + | |
| 574 | + if( attempt_user(fossil_getenv("LOGNAME")) ) return 7; | |
| 575 | + | |
| 576 | + if( attempt_user(fossil_getenv("USERNAME")) ) return 8; | |
| 539 | 577 | |
| 540 | 578 | memset(&url, 0, sizeof(url)); |
| 541 | 579 | url_parse_local(0, URL_USE_CONFIG, &url); |
| 542 | - if( url.user && attempt_user(url.user) ) return; | |
| 580 | + if( url.user && attempt_user(url.user) ) return 9; | |
| 543 | 581 | |
| 544 | 582 | fossil_print( |
| 545 | 583 | "Cannot figure out who you are! Consider using the --user\n" |
| 546 | 584 | "command line option, setting your USER environment variable,\n" |
| 547 | 585 | "or setting a default user with \"fossil user default USER\".\n" |
| 548 | 586 |
| --- src/user.c | |
| +++ src/user.c | |
| @@ -326,14 +326,21 @@ | |
| 326 | ** |
| 327 | ** > fossil user contact USERNAME ?CONTACT-INFO? |
| 328 | ** |
| 329 | ** Query or set contact information for user USERNAME |
| 330 | ** |
| 331 | ** > fossil user default ?USERNAME? |
| 332 | ** |
| 333 | ** Query or set the default user. The default user is the |
| 334 | ** user for command-line interaction. |
| 335 | ** |
| 336 | ** > fossil user list | ls |
| 337 | ** |
| 338 | ** List all users known to the repository |
| 339 | ** |
| @@ -385,21 +392,50 @@ | |
| 385 | &login, zPw, &caps, &contact |
| 386 | ); |
| 387 | db_protect_pop(); |
| 388 | free(zPw); |
| 389 | }else if( n>=2 && strncmp(g.argv[2],"default",n)==0 ){ |
| 390 | if( g.argc==3 ){ |
| 391 | user_select(); |
| 392 | fossil_print("%s\n", g.zLogin); |
| 393 | }else{ |
| 394 | if( !db_exists("SELECT 1 FROM user WHERE login=%Q", g.argv[3]) ){ |
| 395 | fossil_fatal("no such user: %s", g.argv[3]); |
| 396 | } |
| 397 | if( g.localOpen ){ |
| 398 | db_lset("default-user", g.argv[3]); |
| 399 | }else{ |
| 400 | db_set("default-user", g.argv[3], 0); |
| 401 | } |
| 402 | } |
| 403 | }else if(( n>=2 && strncmp(g.argv[2],"list",n)==0 ) || |
| 404 | ( n>=2 && strncmp(g.argv[2],"ls",n)==0 )){ |
| 405 | Stmt q; |
| @@ -496,52 +532,54 @@ | |
| 496 | /* |
| 497 | ** Figure out what user is at the controls. |
| 498 | ** |
| 499 | ** (1) Use the --user and -U command-line options. |
| 500 | ** |
| 501 | ** (2) If the local database is open, check in VVAR. |
| 502 | ** |
| 503 | ** (3) Check the default user in the repository |
| 504 | ** |
| 505 | ** (4) Try the FOSSIL_USER environment variable. |
| 506 | ** |
| 507 | ** (5) Try the USER environment variable. |
| 508 | ** |
| 509 | ** (6) Try the LOGNAME environment variable. |
| 510 | ** |
| 511 | ** (7) Try the USERNAME environment variable. |
| 512 | ** |
| 513 | ** (8) Check if the user can be extracted from the remote URL. |
| 514 | ** |
| 515 | ** The user name is stored in g.zLogin. The uid is in g.userUid. |
| 516 | */ |
| 517 | void user_select(void){ |
| 518 | UrlData url; |
| 519 | if( g.userUid ) return; |
| 520 | if( g.zLogin ){ |
| 521 | if( attempt_user(g.zLogin)==0 ){ |
| 522 | fossil_fatal("no such user: %s", g.zLogin); |
| 523 | }else{ |
| 524 | return; |
| 525 | } |
| 526 | } |
| 527 | |
| 528 | if( g.localOpen && attempt_user(db_lget("default-user",0)) ) return; |
| 529 | |
| 530 | if( attempt_user(db_get("default-user", 0)) ) return; |
| 531 | |
| 532 | if( attempt_user(fossil_getenv("FOSSIL_USER")) ) return; |
| 533 | |
| 534 | if( attempt_user(fossil_getenv("USER")) ) return; |
| 535 | |
| 536 | if( attempt_user(fossil_getenv("LOGNAME")) ) return; |
| 537 | |
| 538 | if( attempt_user(fossil_getenv("USERNAME")) ) return; |
| 539 | |
| 540 | memset(&url, 0, sizeof(url)); |
| 541 | url_parse_local(0, URL_USE_CONFIG, &url); |
| 542 | if( url.user && attempt_user(url.user) ) return; |
| 543 | |
| 544 | fossil_print( |
| 545 | "Cannot figure out who you are! Consider using the --user\n" |
| 546 | "command line option, setting your USER environment variable,\n" |
| 547 | "or setting a default user with \"fossil user default USER\".\n" |
| 548 |
| --- src/user.c | |
| +++ src/user.c | |
| @@ -326,14 +326,21 @@ | |
| 326 | ** |
| 327 | ** > fossil user contact USERNAME ?CONTACT-INFO? |
| 328 | ** |
| 329 | ** Query or set contact information for user USERNAME |
| 330 | ** |
| 331 | ** > fossil user default ?OPTIONS? ?USERNAME? |
| 332 | ** |
| 333 | ** Query or set the default user. The default user is the |
| 334 | ** user for command-line interaction. If USERNAME is an |
| 335 | ** empty string, then the default user is unset from the |
| 336 | ** repository and will subsequently be determined by the -U |
| 337 | ** command-line option or by environment variables |
| 338 | ** FOSSIL_USER, USER, LOGNAME, or USERNAME, in that order. |
| 339 | ** OPTIONS: |
| 340 | ** |
| 341 | ** -v|--verbose Show how the default user is computed |
| 342 | ** |
| 343 | ** > fossil user list | ls |
| 344 | ** |
| 345 | ** List all users known to the repository |
| 346 | ** |
| @@ -385,21 +392,50 @@ | |
| 392 | &login, zPw, &caps, &contact |
| 393 | ); |
| 394 | db_protect_pop(); |
| 395 | free(zPw); |
| 396 | }else if( n>=2 && strncmp(g.argv[2],"default",n)==0 ){ |
| 397 | int eVerbose = find_option("verbose","v",0)!=0; |
| 398 | verify_all_options(); |
| 399 | if( g.argc>3 ){ |
| 400 | const char *zUser = g.argv[3]; |
| 401 | if( fossil_strcmp(zUser,"")==0 || fossil_stricmp(zUser,"nobody")==0 ){ |
| 402 | db_begin_transaction(); |
| 403 | if( g.localOpen ){ |
| 404 | db_multi_exec("DELETE FROM vvar WHERE name='default-user'"); |
| 405 | } |
| 406 | db_unset("default-user",0); |
| 407 | db_commit_transaction(); |
| 408 | }else{ |
| 409 | if( !db_exists("SELECT 1 FROM user WHERE login=%Q", g.argv[3]) ){ |
| 410 | fossil_fatal("no such user: %s", g.argv[3]); |
| 411 | } |
| 412 | if( g.localOpen ){ |
| 413 | db_lset("default-user", g.argv[3]); |
| 414 | }else{ |
| 415 | db_set("default-user", g.argv[3], 0); |
| 416 | } |
| 417 | } |
| 418 | } |
| 419 | if( g.argc==3 || eVerbose ){ |
| 420 | int eHow = user_select(); |
| 421 | const char *zHow = "???"; |
| 422 | switch( eHow ){ |
| 423 | case 1: zHow = "-U option"; break; |
| 424 | case 2: zHow = "previously set"; break; |
| 425 | case 3: zHow = "local check-out"; break; |
| 426 | case 4: zHow = "repository"; break; |
| 427 | case 5: zHow = "FOSSIL_USER"; break; |
| 428 | case 6: zHow = "USER"; break; |
| 429 | case 7: zHow = "LOGNAME"; break; |
| 430 | case 8: zHow = "USERNAME"; break; |
| 431 | case 9: zHow = "URL"; break; |
| 432 | } |
| 433 | if( eVerbose ){ |
| 434 | fossil_print("%s (determined by %s)\n", g.zLogin, zHow); |
| 435 | }else{ |
| 436 | fossil_print("%s\n", g.zLogin); |
| 437 | } |
| 438 | } |
| 439 | }else if(( n>=2 && strncmp(g.argv[2],"list",n)==0 ) || |
| 440 | ( n>=2 && strncmp(g.argv[2],"ls",n)==0 )){ |
| 441 | Stmt q; |
| @@ -496,52 +532,54 @@ | |
| 532 | /* |
| 533 | ** Figure out what user is at the controls. |
| 534 | ** |
| 535 | ** (1) Use the --user and -U command-line options. |
| 536 | ** |
| 537 | ** (2) The name used for login (if there was a login). |
| 538 | ** |
| 539 | ** (3) If the local database is open, check in VVAR. |
| 540 | ** |
| 541 | ** (4) Check the default-user in the repository |
| 542 | ** |
| 543 | ** (5) Try the FOSSIL_USER environment variable. |
| 544 | ** |
| 545 | ** (6) Try the USER environment variable. |
| 546 | ** |
| 547 | ** (7) Try the LOGNAME environment variable. |
| 548 | ** |
| 549 | ** (8) Try the USERNAME environment variable. |
| 550 | ** |
| 551 | ** (9) Check if the user can be extracted from the remote URL. |
| 552 | ** |
| 553 | ** The user name is stored in g.zLogin. The uid is in g.userUid. |
| 554 | */ |
| 555 | int user_select(void){ |
| 556 | UrlData url; |
| 557 | if( g.userUid ) return 1; |
| 558 | if( g.zLogin ){ |
| 559 | if( attempt_user(g.zLogin)==0 ){ |
| 560 | fossil_fatal("no such user: %s", g.zLogin); |
| 561 | }else{ |
| 562 | return 2; |
| 563 | } |
| 564 | } |
| 565 | |
| 566 | if( g.localOpen && attempt_user(db_lget("default-user",0)) ) return 3; |
| 567 | |
| 568 | if( attempt_user(db_get("default-user", 0)) ) return 4; |
| 569 | |
| 570 | if( attempt_user(fossil_getenv("FOSSIL_USER")) ) return 5; |
| 571 | |
| 572 | if( attempt_user(fossil_getenv("USER")) ) return 6; |
| 573 | |
| 574 | if( attempt_user(fossil_getenv("LOGNAME")) ) return 7; |
| 575 | |
| 576 | if( attempt_user(fossil_getenv("USERNAME")) ) return 8; |
| 577 | |
| 578 | memset(&url, 0, sizeof(url)); |
| 579 | url_parse_local(0, URL_USE_CONFIG, &url); |
| 580 | if( url.user && attempt_user(url.user) ) return 9; |
| 581 | |
| 582 | fossil_print( |
| 583 | "Cannot figure out who you are! Consider using the --user\n" |
| 584 | "command line option, setting your USER environment variable,\n" |
| 585 | "or setting a default user with \"fossil user default USER\".\n" |
| 586 |
+16
-6
| --- src/util.c | ||
| +++ src/util.c | ||
| @@ -666,23 +666,33 @@ | ||
| 666 | 666 | /* |
| 667 | 667 | ** Return the name of the users preferred text editor. Return NULL if |
| 668 | 668 | ** not found. |
| 669 | 669 | ** |
| 670 | 670 | ** Search algorithm: |
| 671 | -** (1) The local "editor" setting | |
| 672 | -** (2) The global "editor" setting | |
| 673 | -** (3) The VISUAL environment variable | |
| 674 | -** (4) The EDITOR environment variable | |
| 675 | -** (5) Any of the following programs that are available: | |
| 671 | +** (1) The value of the --editor command-line argument | |
| 672 | +** (2) The local "editor" setting | |
| 673 | +** (3) The global "editor" setting | |
| 674 | +** (4) The VISUAL environment variable | |
| 675 | +** (5) The EDITOR environment variable | |
| 676 | +** (6) Any of the following programs that are available: | |
| 676 | 677 | ** notepad, nano, pico, jove, edit, vi, vim, ed, |
| 678 | +** | |
| 679 | +** The search only occurs once, the first time this routine is called. | |
| 680 | +** Second and subsequent invocations always return the same value. | |
| 677 | 681 | */ |
| 678 | 682 | const char *fossil_text_editor(void){ |
| 679 | - const char *zEditor = db_get("editor", 0); | |
| 683 | + static const char *zEditor = 0; | |
| 680 | 684 | const char *azStdEd[] = { |
| 681 | 685 | "notepad", "nano", "pico", "jove", "edit", "vi", "vim", "ed" |
| 682 | 686 | }; |
| 683 | 687 | int i = 0; |
| 688 | + if( zEditor==0 ){ | |
| 689 | + zEditor = find_option("editor",0,1); | |
| 690 | + } | |
| 691 | + if( zEditor==0 ){ | |
| 692 | + zEditor = db_get("editor", 0); | |
| 693 | + } | |
| 684 | 694 | if( zEditor==0 ){ |
| 685 | 695 | zEditor = fossil_getenv("VISUAL"); |
| 686 | 696 | } |
| 687 | 697 | if( zEditor==0 ){ |
| 688 | 698 | zEditor = fossil_getenv("EDITOR"); |
| 689 | 699 |
| --- src/util.c | |
| +++ src/util.c | |
| @@ -666,23 +666,33 @@ | |
| 666 | /* |
| 667 | ** Return the name of the users preferred text editor. Return NULL if |
| 668 | ** not found. |
| 669 | ** |
| 670 | ** Search algorithm: |
| 671 | ** (1) The local "editor" setting |
| 672 | ** (2) The global "editor" setting |
| 673 | ** (3) The VISUAL environment variable |
| 674 | ** (4) The EDITOR environment variable |
| 675 | ** (5) Any of the following programs that are available: |
| 676 | ** notepad, nano, pico, jove, edit, vi, vim, ed, |
| 677 | */ |
| 678 | const char *fossil_text_editor(void){ |
| 679 | const char *zEditor = db_get("editor", 0); |
| 680 | const char *azStdEd[] = { |
| 681 | "notepad", "nano", "pico", "jove", "edit", "vi", "vim", "ed" |
| 682 | }; |
| 683 | int i = 0; |
| 684 | if( zEditor==0 ){ |
| 685 | zEditor = fossil_getenv("VISUAL"); |
| 686 | } |
| 687 | if( zEditor==0 ){ |
| 688 | zEditor = fossil_getenv("EDITOR"); |
| 689 |
| --- src/util.c | |
| +++ src/util.c | |
| @@ -666,23 +666,33 @@ | |
| 666 | /* |
| 667 | ** Return the name of the users preferred text editor. Return NULL if |
| 668 | ** not found. |
| 669 | ** |
| 670 | ** Search algorithm: |
| 671 | ** (1) The value of the --editor command-line argument |
| 672 | ** (2) The local "editor" setting |
| 673 | ** (3) The global "editor" setting |
| 674 | ** (4) The VISUAL environment variable |
| 675 | ** (5) The EDITOR environment variable |
| 676 | ** (6) Any of the following programs that are available: |
| 677 | ** notepad, nano, pico, jove, edit, vi, vim, ed, |
| 678 | ** |
| 679 | ** The search only occurs once, the first time this routine is called. |
| 680 | ** Second and subsequent invocations always return the same value. |
| 681 | */ |
| 682 | const char *fossil_text_editor(void){ |
| 683 | static const char *zEditor = 0; |
| 684 | const char *azStdEd[] = { |
| 685 | "notepad", "nano", "pico", "jove", "edit", "vi", "vim", "ed" |
| 686 | }; |
| 687 | int i = 0; |
| 688 | if( zEditor==0 ){ |
| 689 | zEditor = find_option("editor",0,1); |
| 690 | } |
| 691 | if( zEditor==0 ){ |
| 692 | zEditor = db_get("editor", 0); |
| 693 | } |
| 694 | if( zEditor==0 ){ |
| 695 | zEditor = fossil_getenv("VISUAL"); |
| 696 | } |
| 697 | if( zEditor==0 ){ |
| 698 | zEditor = fossil_getenv("EDITOR"); |
| 699 |
+2
-2
| --- src/winfile.c | ||
| +++ src/winfile.c | ||
| @@ -505,14 +505,14 @@ | ||
| 505 | 505 | fi2.FileId[1], fi2.FileId[0]); |
| 506 | 506 | } |
| 507 | 507 | } |
| 508 | 508 | if( zFileId==0 ){ |
| 509 | 509 | if( GetFileInformationByHandle(hFile,&fi) ){ |
| 510 | - ULARGE_INTEGER FileId = { | |
| 510 | + ULARGE_INTEGER FileId = {{ | |
| 511 | 511 | /*.LowPart = */ fi.nFileIndexLow, |
| 512 | 512 | /*.HighPart = */ fi.nFileIndexHigh |
| 513 | - }; | |
| 513 | + }}; | |
| 514 | 514 | zFileId = mprintf( |
| 515 | 515 | "%08x/%016llx", |
| 516 | 516 | fi.dwVolumeSerialNumber,(u64)FileId.QuadPart); |
| 517 | 517 | } |
| 518 | 518 | } |
| 519 | 519 |
| --- src/winfile.c | |
| +++ src/winfile.c | |
| @@ -505,14 +505,14 @@ | |
| 505 | fi2.FileId[1], fi2.FileId[0]); |
| 506 | } |
| 507 | } |
| 508 | if( zFileId==0 ){ |
| 509 | if( GetFileInformationByHandle(hFile,&fi) ){ |
| 510 | ULARGE_INTEGER FileId = { |
| 511 | /*.LowPart = */ fi.nFileIndexLow, |
| 512 | /*.HighPart = */ fi.nFileIndexHigh |
| 513 | }; |
| 514 | zFileId = mprintf( |
| 515 | "%08x/%016llx", |
| 516 | fi.dwVolumeSerialNumber,(u64)FileId.QuadPart); |
| 517 | } |
| 518 | } |
| 519 |
| --- src/winfile.c | |
| +++ src/winfile.c | |
| @@ -505,14 +505,14 @@ | |
| 505 | fi2.FileId[1], fi2.FileId[0]); |
| 506 | } |
| 507 | } |
| 508 | if( zFileId==0 ){ |
| 509 | if( GetFileInformationByHandle(hFile,&fi) ){ |
| 510 | ULARGE_INTEGER FileId = {{ |
| 511 | /*.LowPart = */ fi.nFileIndexLow, |
| 512 | /*.HighPart = */ fi.nFileIndexHigh |
| 513 | }}; |
| 514 | zFileId = mprintf( |
| 515 | "%08x/%016llx", |
| 516 | fi.dwVolumeSerialNumber,(u64)FileId.QuadPart); |
| 517 | } |
| 518 | } |
| 519 |
+20
-8
| --- src/xfer.c | ||
| +++ src/xfer.c | ||
| @@ -1116,17 +1116,29 @@ | ||
| 1116 | 1116 | blob_appendf(pXfer->pOut, "uvigot %s %lld %s %d\n", |
| 1117 | 1117 | zName, mtime, zHash, sz); |
| 1118 | 1118 | } |
| 1119 | 1119 | db_finalize(&uvq); |
| 1120 | 1120 | } |
| 1121 | + | |
| 1122 | +/* | |
| 1123 | +** Return a string that contains supplemental information about a | |
| 1124 | +** "not authorized" error. The string might be empty if no additional | |
| 1125 | +** information is available. | |
| 1126 | +*/ | |
| 1127 | +static char *whyNotAuth(void){ | |
| 1128 | + if( g.useLocalauth && db_get_boolean("localauth",0)!=0 ){ | |
| 1129 | + return "\\sbecause\\sthe\\s'localauth'\\ssetting\\sis\\senabled"; | |
| 1130 | + } | |
| 1131 | + return ""; | |
| 1132 | +} | |
| 1121 | 1133 | |
| 1122 | 1134 | /* |
| 1123 | 1135 | ** Called when there is an attempt to transfer private content to and |
| 1124 | 1136 | ** from a server without authorization. |
| 1125 | 1137 | */ |
| 1126 | 1138 | static void server_private_xfer_not_authorized(void){ |
| 1127 | - @ error not\sauthorized\sto\ssync\sprivate\scontent | |
| 1139 | + @ error not\sauthorized\sto\ssync\sprivate\scontent%s(whyNotAuth()) | |
| 1128 | 1140 | } |
| 1129 | 1141 | |
| 1130 | 1142 | /* |
| 1131 | 1143 | ** Return the common TH1 code to evaluate prior to evaluating any other |
| 1132 | 1144 | ** TH1 transfer notification scripts. |
| @@ -1316,11 +1328,11 @@ | ||
| 1316 | 1328 | ** Server accepts a file from the client. |
| 1317 | 1329 | */ |
| 1318 | 1330 | if( blob_eq(&xfer.aToken[0], "file") ){ |
| 1319 | 1331 | if( !isPush ){ |
| 1320 | 1332 | cgi_reset_content(); |
| 1321 | - @ error not\sauthorized\sto\swrite | |
| 1333 | + @ error not\sauthorized\sto\swrite%s(whyNotAuth()) | |
| 1322 | 1334 | nErr++; |
| 1323 | 1335 | break; |
| 1324 | 1336 | } |
| 1325 | 1337 | xfer_accept_file(&xfer, 0, pzUuidList, pnUuidList); |
| 1326 | 1338 | if( blob_size(&xfer.err) ){ |
| @@ -1337,11 +1349,11 @@ | ||
| 1337 | 1349 | ** Server accepts a compressed file from the client. |
| 1338 | 1350 | */ |
| 1339 | 1351 | if( blob_eq(&xfer.aToken[0], "cfile") ){ |
| 1340 | 1352 | if( !isPush ){ |
| 1341 | 1353 | cgi_reset_content(); |
| 1342 | - @ error not\sauthorized\sto\swrite | |
| 1354 | + @ error not\sauthorized\sto\swrite%s(whyNotAuth()) | |
| 1343 | 1355 | nErr++; |
| 1344 | 1356 | break; |
| 1345 | 1357 | } |
| 1346 | 1358 | xfer_accept_compressed_file(&xfer, pzUuidList, pnUuidList); |
| 1347 | 1359 | if( blob_size(&xfer.err) ){ |
| @@ -1461,23 +1473,23 @@ | ||
| 1461 | 1473 | } |
| 1462 | 1474 | login_check_credentials(); |
| 1463 | 1475 | if( blob_eq(&xfer.aToken[0], "pull") ){ |
| 1464 | 1476 | if( !g.perm.Read ){ |
| 1465 | 1477 | cgi_reset_content(); |
| 1466 | - @ error not\sauthorized\sto\sread | |
| 1478 | + @ error not\sauthorized\sto\sread%s(whyNotAuth()) | |
| 1467 | 1479 | nErr++; |
| 1468 | 1480 | break; |
| 1469 | 1481 | } |
| 1470 | 1482 | isPull = 1; |
| 1471 | 1483 | }else{ |
| 1472 | 1484 | if( !g.perm.Write ){ |
| 1473 | 1485 | if( !isPull ){ |
| 1474 | 1486 | cgi_reset_content(); |
| 1475 | - @ error not\sauthorized\sto\swrite | |
| 1487 | + @ error not\sauthorized\sto\swrite%s(whyNotAuth()) | |
| 1476 | 1488 | nErr++; |
| 1477 | 1489 | }else{ |
| 1478 | - @ message pull\sonly\s-\snot\sauthorized\sto\spush | |
| 1490 | + @ message pull\sonly\s-\snot\sauthorized\sto\spush%s(whyNotAuth()) | |
| 1479 | 1491 | } |
| 1480 | 1492 | }else{ |
| 1481 | 1493 | isPush = 1; |
| 1482 | 1494 | } |
| 1483 | 1495 | } |
| @@ -1491,11 +1503,11 @@ | ||
| 1491 | 1503 | int iVers; |
| 1492 | 1504 | login_check_credentials(); |
| 1493 | 1505 | if( !g.perm.Clone ){ |
| 1494 | 1506 | cgi_reset_content(); |
| 1495 | 1507 | @ push %s(db_get("server-code", "x")) %s(db_get("project-code", "x")) |
| 1496 | - @ error not\sauthorized\sto\sclone | |
| 1508 | + @ error not\sauthorized\sto\sclone%s(whyNotAuth()) | |
| 1497 | 1509 | nErr++; |
| 1498 | 1510 | break; |
| 1499 | 1511 | } |
| 1500 | 1512 | if( db_get_boolean("uv-sync",0) && !uvCatalogSent ){ |
| 1501 | 1513 | @ pragma uv-pull-only |
| @@ -1592,11 +1604,11 @@ | ||
| 1592 | 1604 | } |
| 1593 | 1605 | blob_zero(&content); |
| 1594 | 1606 | blob_extract(xfer.pIn, size, &content); |
| 1595 | 1607 | if( !g.perm.Admin ){ |
| 1596 | 1608 | cgi_reset_content(); |
| 1597 | - @ error not\sauthorized\sto\spush\sconfiguration | |
| 1609 | + @ error not\sauthorized\sto\spush\sconfiguration%s(whyNotAuth()) | |
| 1598 | 1610 | nErr++; |
| 1599 | 1611 | break; |
| 1600 | 1612 | } |
| 1601 | 1613 | configure_receive(zName, &content, CONFIGSET_ALL); |
| 1602 | 1614 | blob_reset(&content); |
| 1603 | 1615 |
| --- src/xfer.c | |
| +++ src/xfer.c | |
| @@ -1116,17 +1116,29 @@ | |
| 1116 | blob_appendf(pXfer->pOut, "uvigot %s %lld %s %d\n", |
| 1117 | zName, mtime, zHash, sz); |
| 1118 | } |
| 1119 | db_finalize(&uvq); |
| 1120 | } |
| 1121 | |
| 1122 | /* |
| 1123 | ** Called when there is an attempt to transfer private content to and |
| 1124 | ** from a server without authorization. |
| 1125 | */ |
| 1126 | static void server_private_xfer_not_authorized(void){ |
| 1127 | @ error not\sauthorized\sto\ssync\sprivate\scontent |
| 1128 | } |
| 1129 | |
| 1130 | /* |
| 1131 | ** Return the common TH1 code to evaluate prior to evaluating any other |
| 1132 | ** TH1 transfer notification scripts. |
| @@ -1316,11 +1328,11 @@ | |
| 1316 | ** Server accepts a file from the client. |
| 1317 | */ |
| 1318 | if( blob_eq(&xfer.aToken[0], "file") ){ |
| 1319 | if( !isPush ){ |
| 1320 | cgi_reset_content(); |
| 1321 | @ error not\sauthorized\sto\swrite |
| 1322 | nErr++; |
| 1323 | break; |
| 1324 | } |
| 1325 | xfer_accept_file(&xfer, 0, pzUuidList, pnUuidList); |
| 1326 | if( blob_size(&xfer.err) ){ |
| @@ -1337,11 +1349,11 @@ | |
| 1337 | ** Server accepts a compressed file from the client. |
| 1338 | */ |
| 1339 | if( blob_eq(&xfer.aToken[0], "cfile") ){ |
| 1340 | if( !isPush ){ |
| 1341 | cgi_reset_content(); |
| 1342 | @ error not\sauthorized\sto\swrite |
| 1343 | nErr++; |
| 1344 | break; |
| 1345 | } |
| 1346 | xfer_accept_compressed_file(&xfer, pzUuidList, pnUuidList); |
| 1347 | if( blob_size(&xfer.err) ){ |
| @@ -1461,23 +1473,23 @@ | |
| 1461 | } |
| 1462 | login_check_credentials(); |
| 1463 | if( blob_eq(&xfer.aToken[0], "pull") ){ |
| 1464 | if( !g.perm.Read ){ |
| 1465 | cgi_reset_content(); |
| 1466 | @ error not\sauthorized\sto\sread |
| 1467 | nErr++; |
| 1468 | break; |
| 1469 | } |
| 1470 | isPull = 1; |
| 1471 | }else{ |
| 1472 | if( !g.perm.Write ){ |
| 1473 | if( !isPull ){ |
| 1474 | cgi_reset_content(); |
| 1475 | @ error not\sauthorized\sto\swrite |
| 1476 | nErr++; |
| 1477 | }else{ |
| 1478 | @ message pull\sonly\s-\snot\sauthorized\sto\spush |
| 1479 | } |
| 1480 | }else{ |
| 1481 | isPush = 1; |
| 1482 | } |
| 1483 | } |
| @@ -1491,11 +1503,11 @@ | |
| 1491 | int iVers; |
| 1492 | login_check_credentials(); |
| 1493 | if( !g.perm.Clone ){ |
| 1494 | cgi_reset_content(); |
| 1495 | @ push %s(db_get("server-code", "x")) %s(db_get("project-code", "x")) |
| 1496 | @ error not\sauthorized\sto\sclone |
| 1497 | nErr++; |
| 1498 | break; |
| 1499 | } |
| 1500 | if( db_get_boolean("uv-sync",0) && !uvCatalogSent ){ |
| 1501 | @ pragma uv-pull-only |
| @@ -1592,11 +1604,11 @@ | |
| 1592 | } |
| 1593 | blob_zero(&content); |
| 1594 | blob_extract(xfer.pIn, size, &content); |
| 1595 | if( !g.perm.Admin ){ |
| 1596 | cgi_reset_content(); |
| 1597 | @ error not\sauthorized\sto\spush\sconfiguration |
| 1598 | nErr++; |
| 1599 | break; |
| 1600 | } |
| 1601 | configure_receive(zName, &content, CONFIGSET_ALL); |
| 1602 | blob_reset(&content); |
| 1603 |
| --- src/xfer.c | |
| +++ src/xfer.c | |
| @@ -1116,17 +1116,29 @@ | |
| 1116 | blob_appendf(pXfer->pOut, "uvigot %s %lld %s %d\n", |
| 1117 | zName, mtime, zHash, sz); |
| 1118 | } |
| 1119 | db_finalize(&uvq); |
| 1120 | } |
| 1121 | |
| 1122 | /* |
| 1123 | ** Return a string that contains supplemental information about a |
| 1124 | ** "not authorized" error. The string might be empty if no additional |
| 1125 | ** information is available. |
| 1126 | */ |
| 1127 | static char *whyNotAuth(void){ |
| 1128 | if( g.useLocalauth && db_get_boolean("localauth",0)!=0 ){ |
| 1129 | return "\\sbecause\\sthe\\s'localauth'\\ssetting\\sis\\senabled"; |
| 1130 | } |
| 1131 | return ""; |
| 1132 | } |
| 1133 | |
| 1134 | /* |
| 1135 | ** Called when there is an attempt to transfer private content to and |
| 1136 | ** from a server without authorization. |
| 1137 | */ |
| 1138 | static void server_private_xfer_not_authorized(void){ |
| 1139 | @ error not\sauthorized\sto\ssync\sprivate\scontent%s(whyNotAuth()) |
| 1140 | } |
| 1141 | |
| 1142 | /* |
| 1143 | ** Return the common TH1 code to evaluate prior to evaluating any other |
| 1144 | ** TH1 transfer notification scripts. |
| @@ -1316,11 +1328,11 @@ | |
| 1328 | ** Server accepts a file from the client. |
| 1329 | */ |
| 1330 | if( blob_eq(&xfer.aToken[0], "file") ){ |
| 1331 | if( !isPush ){ |
| 1332 | cgi_reset_content(); |
| 1333 | @ error not\sauthorized\sto\swrite%s(whyNotAuth()) |
| 1334 | nErr++; |
| 1335 | break; |
| 1336 | } |
| 1337 | xfer_accept_file(&xfer, 0, pzUuidList, pnUuidList); |
| 1338 | if( blob_size(&xfer.err) ){ |
| @@ -1337,11 +1349,11 @@ | |
| 1349 | ** Server accepts a compressed file from the client. |
| 1350 | */ |
| 1351 | if( blob_eq(&xfer.aToken[0], "cfile") ){ |
| 1352 | if( !isPush ){ |
| 1353 | cgi_reset_content(); |
| 1354 | @ error not\sauthorized\sto\swrite%s(whyNotAuth()) |
| 1355 | nErr++; |
| 1356 | break; |
| 1357 | } |
| 1358 | xfer_accept_compressed_file(&xfer, pzUuidList, pnUuidList); |
| 1359 | if( blob_size(&xfer.err) ){ |
| @@ -1461,23 +1473,23 @@ | |
| 1473 | } |
| 1474 | login_check_credentials(); |
| 1475 | if( blob_eq(&xfer.aToken[0], "pull") ){ |
| 1476 | if( !g.perm.Read ){ |
| 1477 | cgi_reset_content(); |
| 1478 | @ error not\sauthorized\sto\sread%s(whyNotAuth()) |
| 1479 | nErr++; |
| 1480 | break; |
| 1481 | } |
| 1482 | isPull = 1; |
| 1483 | }else{ |
| 1484 | if( !g.perm.Write ){ |
| 1485 | if( !isPull ){ |
| 1486 | cgi_reset_content(); |
| 1487 | @ error not\sauthorized\sto\swrite%s(whyNotAuth()) |
| 1488 | nErr++; |
| 1489 | }else{ |
| 1490 | @ message pull\sonly\s-\snot\sauthorized\sto\spush%s(whyNotAuth()) |
| 1491 | } |
| 1492 | }else{ |
| 1493 | isPush = 1; |
| 1494 | } |
| 1495 | } |
| @@ -1491,11 +1503,11 @@ | |
| 1503 | int iVers; |
| 1504 | login_check_credentials(); |
| 1505 | if( !g.perm.Clone ){ |
| 1506 | cgi_reset_content(); |
| 1507 | @ push %s(db_get("server-code", "x")) %s(db_get("project-code", "x")) |
| 1508 | @ error not\sauthorized\sto\sclone%s(whyNotAuth()) |
| 1509 | nErr++; |
| 1510 | break; |
| 1511 | } |
| 1512 | if( db_get_boolean("uv-sync",0) && !uvCatalogSent ){ |
| 1513 | @ pragma uv-pull-only |
| @@ -1592,11 +1604,11 @@ | |
| 1604 | } |
| 1605 | blob_zero(&content); |
| 1606 | blob_extract(xfer.pIn, size, &content); |
| 1607 | if( !g.perm.Admin ){ |
| 1608 | cgi_reset_content(); |
| 1609 | @ error not\sauthorized\sto\spush\sconfiguration%s(whyNotAuth()) |
| 1610 | nErr++; |
| 1611 | break; |
| 1612 | } |
| 1613 | configure_receive(zName, &content, CONFIGSET_ALL); |
| 1614 | blob_reset(&content); |
| 1615 |
+1
-1
| --- test/many-www.tcl | ||
| +++ test/many-www.tcl | ||
| @@ -24,11 +24,11 @@ | ||
| 24 | 24 | /setup |
| 25 | 25 | /dir |
| 26 | 26 | /wcontent |
| 27 | 27 | /attachlist |
| 28 | 28 | /taglist |
| 29 | - /test_env | |
| 29 | + /test-env | |
| 30 | 30 | /stat |
| 31 | 31 | /rcvfromlist |
| 32 | 32 | /urllist |
| 33 | 33 | /modreq |
| 34 | 34 | /info/d5c4 |
| 35 | 35 |
| --- test/many-www.tcl | |
| +++ test/many-www.tcl | |
| @@ -24,11 +24,11 @@ | |
| 24 | /setup |
| 25 | /dir |
| 26 | /wcontent |
| 27 | /attachlist |
| 28 | /taglist |
| 29 | /test_env |
| 30 | /stat |
| 31 | /rcvfromlist |
| 32 | /urllist |
| 33 | /modreq |
| 34 | /info/d5c4 |
| 35 |
| --- test/many-www.tcl | |
| +++ test/many-www.tcl | |
| @@ -24,11 +24,11 @@ | |
| 24 | /setup |
| 25 | /dir |
| 26 | /wcontent |
| 27 | /attachlist |
| 28 | /taglist |
| 29 | /test-env |
| 30 | /stat |
| 31 | /rcvfromlist |
| 32 | /urllist |
| 33 | /modreq |
| 34 | /info/d5c4 |
| 35 |
+9
| --- test/tester.tcl | ||
| +++ test/tester.tcl | ||
| @@ -356,30 +356,39 @@ | ||
| 356 | 356 | mtime-changes \ |
| 357 | 357 | mv-rm-files \ |
| 358 | 358 | pgp-command \ |
| 359 | 359 | preferred-diff-type \ |
| 360 | 360 | proxy \ |
| 361 | + raw-bgcolor \ | |
| 361 | 362 | redirect-to-https \ |
| 362 | 363 | relative-paths \ |
| 363 | 364 | repo-cksum \ |
| 364 | 365 | repolist-skin \ |
| 365 | 366 | robot-restrict \ |
| 366 | 367 | robots-txt \ |
| 367 | 368 | safe-html \ |
| 368 | 369 | self-pw-reset \ |
| 369 | 370 | self-register \ |
| 371 | + show-repolist-desc \ | |
| 372 | + show-repolist-lg \ | |
| 370 | 373 | sitemap-extra \ |
| 371 | 374 | ssh-command \ |
| 372 | 375 | ssl-ca-location \ |
| 373 | 376 | ssl-identity \ |
| 374 | 377 | tclsh \ |
| 375 | 378 | th1-setup \ |
| 376 | 379 | th1-uri-regexp \ |
| 377 | 380 | ticket-default-report \ |
| 381 | + timeline-hard-newlines \ | |
| 382 | + timeline-plaintext \ | |
| 383 | + timeline-truncate-at-blank \ | |
| 384 | + timeline-tslink-info \ | |
| 378 | 385 | timeline-utc \ |
| 379 | 386 | user-color-map \ |
| 387 | + verify-comments \ | |
| 380 | 388 | uv-sync \ |
| 389 | + vuln-report \ | |
| 381 | 390 | web-browser] |
| 382 | 391 | |
| 383 | 392 | fossil test-th-eval "hasfeature legacyMvRm" |
| 384 | 393 | |
| 385 | 394 | if {[normalize_result] eq "1"} { |
| 386 | 395 | |
| 387 | 396 | ADDED test/th1-taint.test |
| --- test/tester.tcl | |
| +++ test/tester.tcl | |
| @@ -356,30 +356,39 @@ | |
| 356 | mtime-changes \ |
| 357 | mv-rm-files \ |
| 358 | pgp-command \ |
| 359 | preferred-diff-type \ |
| 360 | proxy \ |
| 361 | redirect-to-https \ |
| 362 | relative-paths \ |
| 363 | repo-cksum \ |
| 364 | repolist-skin \ |
| 365 | robot-restrict \ |
| 366 | robots-txt \ |
| 367 | safe-html \ |
| 368 | self-pw-reset \ |
| 369 | self-register \ |
| 370 | sitemap-extra \ |
| 371 | ssh-command \ |
| 372 | ssl-ca-location \ |
| 373 | ssl-identity \ |
| 374 | tclsh \ |
| 375 | th1-setup \ |
| 376 | th1-uri-regexp \ |
| 377 | ticket-default-report \ |
| 378 | timeline-utc \ |
| 379 | user-color-map \ |
| 380 | uv-sync \ |
| 381 | web-browser] |
| 382 | |
| 383 | fossil test-th-eval "hasfeature legacyMvRm" |
| 384 | |
| 385 | if {[normalize_result] eq "1"} { |
| 386 | |
| 387 | DDED test/th1-taint.test |
| --- test/tester.tcl | |
| +++ test/tester.tcl | |
| @@ -356,30 +356,39 @@ | |
| 356 | mtime-changes \ |
| 357 | mv-rm-files \ |
| 358 | pgp-command \ |
| 359 | preferred-diff-type \ |
| 360 | proxy \ |
| 361 | raw-bgcolor \ |
| 362 | redirect-to-https \ |
| 363 | relative-paths \ |
| 364 | repo-cksum \ |
| 365 | repolist-skin \ |
| 366 | robot-restrict \ |
| 367 | robots-txt \ |
| 368 | safe-html \ |
| 369 | self-pw-reset \ |
| 370 | self-register \ |
| 371 | show-repolist-desc \ |
| 372 | show-repolist-lg \ |
| 373 | sitemap-extra \ |
| 374 | ssh-command \ |
| 375 | ssl-ca-location \ |
| 376 | ssl-identity \ |
| 377 | tclsh \ |
| 378 | th1-setup \ |
| 379 | th1-uri-regexp \ |
| 380 | ticket-default-report \ |
| 381 | timeline-hard-newlines \ |
| 382 | timeline-plaintext \ |
| 383 | timeline-truncate-at-blank \ |
| 384 | timeline-tslink-info \ |
| 385 | timeline-utc \ |
| 386 | user-color-map \ |
| 387 | verify-comments \ |
| 388 | uv-sync \ |
| 389 | vuln-report \ |
| 390 | web-browser] |
| 391 | |
| 392 | fossil test-th-eval "hasfeature legacyMvRm" |
| 393 | |
| 394 | if {[normalize_result] eq "1"} { |
| 395 | |
| 396 | DDED test/th1-taint.test |
+84
| --- a/test/th1-taint.test | ||
| +++ b/test/th1-taint.test | ||
| @@ -0,0 +1,84 @@ | ||
| 1 | +# | |
| 2 | +# Copyright (c) 2025 D. Richard Hipp | |
| 3 | +# | |
| 4 | +# This program is free software; you can redistribute it and/or | |
| 5 | +# modify it under the terms of the Simplified BSD License (also | |
| 6 | +# known as the "2-Clause License" or "FreeBSD License".) | |
| 7 | +# | |
| 8 | +# This program is distributed in the hope that it will be useful, | |
| 9 | +# but without any warranty; without even the implied warranty of | |
| 10 | +# merchantability or fitness for a particular purpose. | |
| 11 | +# | |
| 12 | +# Author contact information: | |
| 13 | +# [email protected] | |
| 14 | +# http://www.hwaci.com/drh/ | |
| 15 | +# | |
| 16 | +############################################################################ | |
| 17 | +# | |
| 18 | +# TH1 Commands | |
| 19 | +# | |
| 20 | + | |
| 21 | +set path [file dirname [info script]]; test_setup | |
| 22 | + | |
| 23 | +proc taint-test {testnum th1script expected} { | |
| 24 | + global fossilexe | |
| 25 | + set rc [catch {exec $fossilexe test-th-eval $th1script} got] | |
| 26 | + if {$rc} { | |
| 27 | + test th1-taint-$testnum 0 | |
| 28 | + puts $got | |
| 29 | + return | |
| 30 | + } | |
| 31 | + if {$got ne $expected} { | |
| 32 | + test th1-taint-$testnum 0 | |
| 33 | + puts " Expected: $expected" | |
| 34 | + puts " Got: $got" | |
| 35 | + } else { | |
| 36 | + test th1-taint-$testnum 1 | |
| 37 | + } | |
| 38 | +} | |
| 39 | + | |
| 40 | +taint-test 10 {string is tainted abcd} 0 | |
| 41 | +taint-test 20 {string is tainted [taint abcd]} 1 | |
| 42 | +taint-test 30 {string is tainted [untaint [taint abcd]]} 0 | |
| 43 | +taint-test 40 {string is tainted [untaint abcde]} 0 | |
| 44 | +taint-test 50 {string is tainted "abc[taint def]ghi"} 1 | |
| 45 | +taint-test 60 {set t1 [taint abc]; string is tainted "123 $t1 456"} 1 | |
| 46 | + | |
| 47 | +taint-test 100 {set t1 [taint abc]; lappend t1 def; string is tainted $t1} 1 | |
| 48 | +taint-test 110 {set t1 abc; lappend t1 [taint def]; string is tainted $t1} 1 | |
| 49 | + | |
| 50 | +taint-test 200 {string is tainted [list abc def ghi]} 0 | |
| 51 | +taint-test 210 {string is tainted [list [taint abc] def ghi]} 1 | |
| 52 | +taint-test 220 {string is tainted [list abc [taint def] ghi]} 1 | |
| 53 | +taint-test 230 {string is tainted [list abc def [taint ghi]]} 1 | |
| 54 | + | |
| 55 | +taint-test 300 { | |
| 56 | + set res {} | |
| 57 | + foreach x [list abc [taint def] ghi] { | |
| 58 | + lappend res [string is tainted $x] | |
| 59 | + } | |
| 60 | + set res | |
| 61 | +} {1 1 1} | |
| 62 | +taint-test 310 { | |
| 63 | + set res {} | |
| 64 | + foreach {x y} [list abc [taint def] ghi jkl] { | |
| 65 | + lappend res [string is tainted $x] [string is tainted $y] | |
| 66 | + } | |
| 67 | + set res | |
| 68 | +} {1 1 1 1} | |
| 69 | + | |
| 70 | +taint-test 400 {string is tainted [lindex "abc [taint def] ghi" 0]} 1 | |
| 71 | +taint-test 410 {string is tainted [lindex "abc [taint def] ghi" 1]} 1 | |
| 72 | +taint-test 420 {string is tainted [lindex "abc [taint def] ghi" 2]} 1 | |
| 73 | +taint-test 430 {string is tainted [lindex "abc [taint def] ghi" 3]} 0 | |
| 74 | + | |
| 75 | +taint-test 500 {string is tainted [string index [taint abcdefg] 3]} 1 | |
| 76 | + | |
| 77 | +taint-test 600 {string is tainted [string range [taint abcdefg] 3 5]} 1 | |
| 78 | + | |
| 79 | +taint-test 700 {string is tainted [string trim [taint " abcdefg "]]} 1 | |
| 80 | +taint-test 710 {string is tainted [string trimright [taint " abcdefg "]]} 1 | |
| 81 | +taint-test 720 {string is tainted [string trimleft [taint " abcdefg "]]} 1 | |
| 82 | + | |
| 83 | + | |
| 84 | +test_cleanup |
| --- a/test/th1-taint.test | |
| +++ b/test/th1-taint.test | |
| @@ -0,0 +1,84 @@ | |
| --- a/test/th1-taint.test | |
| +++ b/test/th1-taint.test | |
| @@ -0,0 +1,84 @@ | |
| 1 | # |
| 2 | # Copyright (c) 2025 D. Richard Hipp |
| 3 | # |
| 4 | # This program is free software; you can redistribute it and/or |
| 5 | # modify it under the terms of the Simplified BSD License (also |
| 6 | # known as the "2-Clause License" or "FreeBSD License".) |
| 7 | # |
| 8 | # This program is distributed in the hope that it will be useful, |
| 9 | # but without any warranty; without even the implied warranty of |
| 10 | # merchantability or fitness for a particular purpose. |
| 11 | # |
| 12 | # Author contact information: |
| 13 | # [email protected] |
| 14 | # http://www.hwaci.com/drh/ |
| 15 | # |
| 16 | ############################################################################ |
| 17 | # |
| 18 | # TH1 Commands |
| 19 | # |
| 20 | |
| 21 | set path [file dirname [info script]]; test_setup |
| 22 | |
| 23 | proc taint-test {testnum th1script expected} { |
| 24 | global fossilexe |
| 25 | set rc [catch {exec $fossilexe test-th-eval $th1script} got] |
| 26 | if {$rc} { |
| 27 | test th1-taint-$testnum 0 |
| 28 | puts $got |
| 29 | return |
| 30 | } |
| 31 | if {$got ne $expected} { |
| 32 | test th1-taint-$testnum 0 |
| 33 | puts " Expected: $expected" |
| 34 | puts " Got: $got" |
| 35 | } else { |
| 36 | test th1-taint-$testnum 1 |
| 37 | } |
| 38 | } |
| 39 | |
| 40 | taint-test 10 {string is tainted abcd} 0 |
| 41 | taint-test 20 {string is tainted [taint abcd]} 1 |
| 42 | taint-test 30 {string is tainted [untaint [taint abcd]]} 0 |
| 43 | taint-test 40 {string is tainted [untaint abcde]} 0 |
| 44 | taint-test 50 {string is tainted "abc[taint def]ghi"} 1 |
| 45 | taint-test 60 {set t1 [taint abc]; string is tainted "123 $t1 456"} 1 |
| 46 | |
| 47 | taint-test 100 {set t1 [taint abc]; lappend t1 def; string is tainted $t1} 1 |
| 48 | taint-test 110 {set t1 abc; lappend t1 [taint def]; string is tainted $t1} 1 |
| 49 | |
| 50 | taint-test 200 {string is tainted [list abc def ghi]} 0 |
| 51 | taint-test 210 {string is tainted [list [taint abc] def ghi]} 1 |
| 52 | taint-test 220 {string is tainted [list abc [taint def] ghi]} 1 |
| 53 | taint-test 230 {string is tainted [list abc def [taint ghi]]} 1 |
| 54 | |
| 55 | taint-test 300 { |
| 56 | set res {} |
| 57 | foreach x [list abc [taint def] ghi] { |
| 58 | lappend res [string is tainted $x] |
| 59 | } |
| 60 | set res |
| 61 | } {1 1 1} |
| 62 | taint-test 310 { |
| 63 | set res {} |
| 64 | foreach {x y} [list abc [taint def] ghi jkl] { |
| 65 | lappend res [string is tainted $x] [string is tainted $y] |
| 66 | } |
| 67 | set res |
| 68 | } {1 1 1 1} |
| 69 | |
| 70 | taint-test 400 {string is tainted [lindex "abc [taint def] ghi" 0]} 1 |
| 71 | taint-test 410 {string is tainted [lindex "abc [taint def] ghi" 1]} 1 |
| 72 | taint-test 420 {string is tainted [lindex "abc [taint def] ghi" 2]} 1 |
| 73 | taint-test 430 {string is tainted [lindex "abc [taint def] ghi" 3]} 0 |
| 74 | |
| 75 | taint-test 500 {string is tainted [string index [taint abcdefg] 3]} 1 |
| 76 | |
| 77 | taint-test 600 {string is tainted [string range [taint abcdefg] 3 5]} 1 |
| 78 | |
| 79 | taint-test 700 {string is tainted [string trim [taint " abcdefg "]]} 1 |
| 80 | taint-test 710 {string is tainted [string trimright [taint " abcdefg "]]} 1 |
| 81 | taint-test 720 {string is tainted [string trimleft [taint " abcdefg "]]} 1 |
| 82 | |
| 83 | |
| 84 | test_cleanup |
+36
-36
| --- test/th1.test | ||
| +++ test/th1.test | ||
| @@ -795,23 +795,23 @@ | ||
| 795 | 795 | rpage-\$requested_page\ |
| 796 | 796 | cpage-\$canonical_page\">" [normalize_result]]} |
| 797 | 797 | |
| 798 | 798 | ############################################################################### |
| 799 | 799 | |
| 800 | -fossil test-th-eval "styleHeader {Page Title Here}" | |
| 801 | -test th1-header-1 {$RESULT eq {TH_ERROR: repository unavailable}} | |
| 800 | +#fossil test-th-eval "styleHeader {Page Title Here}" | |
| 801 | +#test th1-header-1 {$RESULT eq {TH_ERROR: repository unavailable}} | |
| 802 | 802 | |
| 803 | 803 | ############################################################################### |
| 804 | 804 | |
| 805 | 805 | test_in_checkout th1-header-2 { |
| 806 | 806 | fossil test-th-eval --open-config "styleHeader {Page Title Here}" |
| 807 | 807 | } {[regexp -- {<title>Fossil: Page Title Here</title>} $RESULT]} |
| 808 | 808 | |
| 809 | 809 | ############################################################################### |
| 810 | 810 | |
| 811 | -fossil test-th-eval "styleFooter" | |
| 812 | -test th1-footer-1 {$RESULT eq {TH_ERROR: repository unavailable}} | |
| 811 | +#fossil test-th-eval "styleFooter" | |
| 812 | +#test th1-footer-1 {$RESULT eq {TH_ERROR: repository unavailable}} | |
| 813 | 813 | |
| 814 | 814 | ############################################################################### |
| 815 | 815 | |
| 816 | 816 | fossil test-th-eval --open-config "styleFooter" |
| 817 | 817 | test th1-footer-2 {$RESULT eq {}} |
| @@ -879,44 +879,44 @@ | ||
| 879 | 879 | test th1-artifact-1 {$RESULT eq \ |
| 880 | 880 | {TH_ERROR: wrong # args: should be "artifact ID ?FILENAME?"}} |
| 881 | 881 | |
| 882 | 882 | ############################################################################### |
| 883 | 883 | |
| 884 | -fossil test-th-eval "artifact tip" | |
| 885 | -test th1-artifact-2 {$RESULT eq {TH_ERROR: repository unavailable}} | |
| 884 | +#fossil test-th-eval "artifact tip" | |
| 885 | +#test th1-artifact-2 {$RESULT eq {TH_ERROR: repository unavailable}} | |
| 886 | 886 | |
| 887 | 887 | ############################################################################### |
| 888 | 888 | |
| 889 | 889 | test_in_checkout th1-artifact-3 { |
| 890 | 890 | fossil test-th-eval --open-config "artifact tip" |
| 891 | 891 | } {[regexp -- {F test/th1\.test [0-9a-f]{40,64}} $RESULT]} |
| 892 | 892 | |
| 893 | 893 | ############################################################################### |
| 894 | 894 | |
| 895 | -fossil test-th-eval "artifact 0000000000" | |
| 896 | -test th1-artifact-4 {$RESULT eq {TH_ERROR: repository unavailable}} | |
| 895 | +#fossil test-th-eval "artifact 0000000000" | |
| 896 | +#test th1-artifact-4 {$RESULT eq {TH_ERROR: repository unavailable}} | |
| 897 | 897 | |
| 898 | 898 | ############################################################################### |
| 899 | 899 | |
| 900 | 900 | fossil test-th-eval --open-config "artifact 0000000000" |
| 901 | 901 | test th1-artifact-5 {$RESULT eq {TH_ERROR: name not found}} |
| 902 | 902 | |
| 903 | 903 | ############################################################################### |
| 904 | 904 | |
| 905 | -fossil test-th-eval "artifact tip test/th1.test" | |
| 906 | -test th1-artifact-6 {$RESULT eq {TH_ERROR: repository unavailable}} | |
| 905 | +#fossil test-th-eval "artifact tip test/th1.test" | |
| 906 | +#test th1-artifact-6 {$RESULT eq {TH_ERROR: repository unavailable}} | |
| 907 | 907 | |
| 908 | 908 | ############################################################################### |
| 909 | 909 | |
| 910 | 910 | test_in_checkout th1-artifact-7 { |
| 911 | 911 | fossil test-th-eval --open-config "artifact tip test/th1.test" |
| 912 | 912 | } {[regexp -- {th1-artifact-7} $RESULT]} |
| 913 | 913 | |
| 914 | 914 | ############################################################################### |
| 915 | 915 | |
| 916 | -fossil test-th-eval "artifact 0000000000 test/th1.test" | |
| 917 | -test th1-artifact-8 {$RESULT eq {TH_ERROR: repository unavailable}} | |
| 916 | +#fossil test-th-eval "artifact 0000000000 test/th1.test" | |
| 917 | +#test th1-artifact-8 {$RESULT eq {TH_ERROR: repository unavailable}} | |
| 918 | 918 | |
| 919 | 919 | ############################################################################### |
| 920 | 920 | |
| 921 | 921 | fossil test-th-eval --open-config "artifact 0000000000 test/th1.test" |
| 922 | 922 | test th1-artifact-9 {$RESULT eq {TH_ERROR: manifest not found}} |
| @@ -947,12 +947,12 @@ | ||
| 947 | 947 | } |
| 948 | 948 | } |
| 949 | 949 | |
| 950 | 950 | ############################################################################### |
| 951 | 951 | |
| 952 | -fossil test-th-eval "globalState configuration" | |
| 953 | -test th1-globalState-3 {[string length $RESULT] == 0} | |
| 952 | +#fossil test-th-eval "globalState configuration" | |
| 953 | +#test th1-globalState-3 {[string length $RESULT] == 0} | |
| 954 | 954 | |
| 955 | 955 | ############################################################################### |
| 956 | 956 | |
| 957 | 957 | fossil test-th-eval --open-config "globalState configuration" |
| 958 | 958 | test th1-globalState-4 {[string length $RESULT] > 0} |
| @@ -1041,12 +1041,12 @@ | ||
| 1041 | 1041 | fossil test-th-eval "globalState flags" |
| 1042 | 1042 | test th1-globalState-16 {$RESULT eq "0"} |
| 1043 | 1043 | |
| 1044 | 1044 | ############################################################################### |
| 1045 | 1045 | |
| 1046 | -fossil test-th-eval "reinitialize; globalState configuration" | |
| 1047 | -test th1-reinitialize-1 {$RESULT eq ""} | |
| 1046 | +#fossil test-th-eval "reinitialize; globalState configuration" | |
| 1047 | +#test th1-reinitialize-1 {$RESULT eq ""} | |
| 1048 | 1048 | |
| 1049 | 1049 | ############################################################################### |
| 1050 | 1050 | |
| 1051 | 1051 | fossil test-th-eval "reinitialize 1; globalState configuration" |
| 1052 | 1052 | test th1-reinitialize-2 {$RESULT ne ""} |
| @@ -1056,29 +1056,29 @@ | ||
| 1056 | 1056 | # |
| 1057 | 1057 | # NOTE: This test will fail if the command names are added to TH1, or |
| 1058 | 1058 | # moved from Tcl builds to plain or the reverse. Sorting the |
| 1059 | 1059 | # command lists eliminates a dependence on order. |
| 1060 | 1060 | # |
| 1061 | -fossil test-th-eval "info commands" | |
| 1062 | -set sorted_result [lsort $RESULT] | |
| 1063 | -protOut "Sorted: $sorted_result" | |
| 1064 | -set base_commands {anoncap anycap array artifact break breakpoint \ | |
| 1065 | - builtin_request_js capexpr captureTh1 catch cgiHeaderLine checkout \ | |
| 1066 | - combobox continue copybtn date decorate defHeader dir enable_htmlify \ | |
| 1067 | - enable_output encode64 error expr for foreach getParameter glob_match \ | |
| 1068 | - globalState hascap hasfeature html htmlize http httpize if info \ | |
| 1069 | - insertCsrf lappend lindex linecount list llength lsearch markdown nonce \ | |
| 1070 | - proc puts query randhex redirect regexp reinitialize rename render \ | |
| 1071 | - repository return searchable set setParameter setting stime string \ | |
| 1072 | - styleFooter styleHeader styleScript submenu tclReady trace unset \ | |
| 1073 | - unversioned uplevel upvar utime verifyCsrf verifyLogin wiki} | |
| 1074 | -set tcl_commands {tclEval tclExpr tclInvoke tclIsSafe tclMakeSafe} | |
| 1075 | -if {$th1Tcl} { | |
| 1076 | - test th1-info-commands-1 {$sorted_result eq [lsort "$base_commands $tcl_commands"]} | |
| 1077 | -} else { | |
| 1078 | - test th1-info-commands-1 {$sorted_result eq [lsort "$base_commands"]} | |
| 1079 | -} | |
| 1061 | +#fossil test-th-eval "info commands" | |
| 1062 | +#set sorted_result [lsort $RESULT] | |
| 1063 | +#protOut "Sorted: $sorted_result" | |
| 1064 | +#set base_commands {anoncap anycap array artifact break breakpoint \ | |
| 1065 | +# builtin_request_js capexpr captureTh1 catch cgiHeaderLine checkout \ | |
| 1066 | +# combobox continue copybtn date decorate defHeader dir enable_htmlify \ | |
| 1067 | +# enable_output encode64 error expr for foreach getParameter glob_match \ | |
| 1068 | +# globalState hascap hasfeature html htmlize http httpize if info \ | |
| 1069 | +# insertCsrf lappend lindex linecount list llength lsearch markdown nonce \ | |
| 1070 | +# proc puts query randhex redirect regexp reinitialize rename render \ | |
| 1071 | +# repository return searchable set setParameter setting stime string \ | |
| 1072 | +# styleFooter styleHeader styleScript submenu tclReady trace unset \ | |
| 1073 | +# unversioned uplevel upvar utime verifyCsrf verifyLogin wiki} | |
| 1074 | +#set tcl_commands {tclEval tclExpr tclInvoke tclIsSafe tclMakeSafe} | |
| 1075 | +#if {$th1Tcl} { | |
| 1076 | +# test th1-info-commands-1 {$sorted_result eq [lsort "$base_commands $tcl_commands"]} | |
| 1077 | +#} else { | |
| 1078 | +# test th1-info-commands-1 {$sorted_result eq [lsort "$base_commands"]} | |
| 1079 | +#} | |
| 1080 | 1080 | |
| 1081 | 1081 | ############################################################################### |
| 1082 | 1082 | |
| 1083 | 1083 | fossil test-th-eval "info vars" |
| 1084 | 1084 | |
| @@ -1326,11 +1326,11 @@ | ||
| 1326 | 1326 | |
| 1327 | 1327 | ############################################################################### |
| 1328 | 1328 | |
| 1329 | 1329 | fossil test-th-eval {string is other 123} |
| 1330 | 1330 | test th1-string-is-4 {$RESULT eq \ |
| 1331 | -"TH_ERROR: Expected alnum, double, integer, or list, got: other"} | |
| 1331 | +"TH_ERROR: Expected alnum, double, integer, list, or tainted, got: other"} | |
| 1332 | 1332 | |
| 1333 | 1333 | ############################################################################### |
| 1334 | 1334 | |
| 1335 | 1335 | fossil test-th-eval {string is alnum 123} |
| 1336 | 1336 | test th1-string-is-5 {$RESULT eq "1"} |
| 1337 | 1337 | |
| 1338 | 1338 | ADDED tools/fake-smtpd.tcl |
| 1339 | 1339 | ADDED tools/find-fossil-cgis.tcl |
| --- test/th1.test | |
| +++ test/th1.test | |
| @@ -795,23 +795,23 @@ | |
| 795 | rpage-\$requested_page\ |
| 796 | cpage-\$canonical_page\">" [normalize_result]]} |
| 797 | |
| 798 | ############################################################################### |
| 799 | |
| 800 | fossil test-th-eval "styleHeader {Page Title Here}" |
| 801 | test th1-header-1 {$RESULT eq {TH_ERROR: repository unavailable}} |
| 802 | |
| 803 | ############################################################################### |
| 804 | |
| 805 | test_in_checkout th1-header-2 { |
| 806 | fossil test-th-eval --open-config "styleHeader {Page Title Here}" |
| 807 | } {[regexp -- {<title>Fossil: Page Title Here</title>} $RESULT]} |
| 808 | |
| 809 | ############################################################################### |
| 810 | |
| 811 | fossil test-th-eval "styleFooter" |
| 812 | test th1-footer-1 {$RESULT eq {TH_ERROR: repository unavailable}} |
| 813 | |
| 814 | ############################################################################### |
| 815 | |
| 816 | fossil test-th-eval --open-config "styleFooter" |
| 817 | test th1-footer-2 {$RESULT eq {}} |
| @@ -879,44 +879,44 @@ | |
| 879 | test th1-artifact-1 {$RESULT eq \ |
| 880 | {TH_ERROR: wrong # args: should be "artifact ID ?FILENAME?"}} |
| 881 | |
| 882 | ############################################################################### |
| 883 | |
| 884 | fossil test-th-eval "artifact tip" |
| 885 | test th1-artifact-2 {$RESULT eq {TH_ERROR: repository unavailable}} |
| 886 | |
| 887 | ############################################################################### |
| 888 | |
| 889 | test_in_checkout th1-artifact-3 { |
| 890 | fossil test-th-eval --open-config "artifact tip" |
| 891 | } {[regexp -- {F test/th1\.test [0-9a-f]{40,64}} $RESULT]} |
| 892 | |
| 893 | ############################################################################### |
| 894 | |
| 895 | fossil test-th-eval "artifact 0000000000" |
| 896 | test th1-artifact-4 {$RESULT eq {TH_ERROR: repository unavailable}} |
| 897 | |
| 898 | ############################################################################### |
| 899 | |
| 900 | fossil test-th-eval --open-config "artifact 0000000000" |
| 901 | test th1-artifact-5 {$RESULT eq {TH_ERROR: name not found}} |
| 902 | |
| 903 | ############################################################################### |
| 904 | |
| 905 | fossil test-th-eval "artifact tip test/th1.test" |
| 906 | test th1-artifact-6 {$RESULT eq {TH_ERROR: repository unavailable}} |
| 907 | |
| 908 | ############################################################################### |
| 909 | |
| 910 | test_in_checkout th1-artifact-7 { |
| 911 | fossil test-th-eval --open-config "artifact tip test/th1.test" |
| 912 | } {[regexp -- {th1-artifact-7} $RESULT]} |
| 913 | |
| 914 | ############################################################################### |
| 915 | |
| 916 | fossil test-th-eval "artifact 0000000000 test/th1.test" |
| 917 | test th1-artifact-8 {$RESULT eq {TH_ERROR: repository unavailable}} |
| 918 | |
| 919 | ############################################################################### |
| 920 | |
| 921 | fossil test-th-eval --open-config "artifact 0000000000 test/th1.test" |
| 922 | test th1-artifact-9 {$RESULT eq {TH_ERROR: manifest not found}} |
| @@ -947,12 +947,12 @@ | |
| 947 | } |
| 948 | } |
| 949 | |
| 950 | ############################################################################### |
| 951 | |
| 952 | fossil test-th-eval "globalState configuration" |
| 953 | test th1-globalState-3 {[string length $RESULT] == 0} |
| 954 | |
| 955 | ############################################################################### |
| 956 | |
| 957 | fossil test-th-eval --open-config "globalState configuration" |
| 958 | test th1-globalState-4 {[string length $RESULT] > 0} |
| @@ -1041,12 +1041,12 @@ | |
| 1041 | fossil test-th-eval "globalState flags" |
| 1042 | test th1-globalState-16 {$RESULT eq "0"} |
| 1043 | |
| 1044 | ############################################################################### |
| 1045 | |
| 1046 | fossil test-th-eval "reinitialize; globalState configuration" |
| 1047 | test th1-reinitialize-1 {$RESULT eq ""} |
| 1048 | |
| 1049 | ############################################################################### |
| 1050 | |
| 1051 | fossil test-th-eval "reinitialize 1; globalState configuration" |
| 1052 | test th1-reinitialize-2 {$RESULT ne ""} |
| @@ -1056,29 +1056,29 @@ | |
| 1056 | # |
| 1057 | # NOTE: This test will fail if the command names are added to TH1, or |
| 1058 | # moved from Tcl builds to plain or the reverse. Sorting the |
| 1059 | # command lists eliminates a dependence on order. |
| 1060 | # |
| 1061 | fossil test-th-eval "info commands" |
| 1062 | set sorted_result [lsort $RESULT] |
| 1063 | protOut "Sorted: $sorted_result" |
| 1064 | set base_commands {anoncap anycap array artifact break breakpoint \ |
| 1065 | builtin_request_js capexpr captureTh1 catch cgiHeaderLine checkout \ |
| 1066 | combobox continue copybtn date decorate defHeader dir enable_htmlify \ |
| 1067 | enable_output encode64 error expr for foreach getParameter glob_match \ |
| 1068 | globalState hascap hasfeature html htmlize http httpize if info \ |
| 1069 | insertCsrf lappend lindex linecount list llength lsearch markdown nonce \ |
| 1070 | proc puts query randhex redirect regexp reinitialize rename render \ |
| 1071 | repository return searchable set setParameter setting stime string \ |
| 1072 | styleFooter styleHeader styleScript submenu tclReady trace unset \ |
| 1073 | unversioned uplevel upvar utime verifyCsrf verifyLogin wiki} |
| 1074 | set tcl_commands {tclEval tclExpr tclInvoke tclIsSafe tclMakeSafe} |
| 1075 | if {$th1Tcl} { |
| 1076 | test th1-info-commands-1 {$sorted_result eq [lsort "$base_commands $tcl_commands"]} |
| 1077 | } else { |
| 1078 | test th1-info-commands-1 {$sorted_result eq [lsort "$base_commands"]} |
| 1079 | } |
| 1080 | |
| 1081 | ############################################################################### |
| 1082 | |
| 1083 | fossil test-th-eval "info vars" |
| 1084 | |
| @@ -1326,11 +1326,11 @@ | |
| 1326 | |
| 1327 | ############################################################################### |
| 1328 | |
| 1329 | fossil test-th-eval {string is other 123} |
| 1330 | test th1-string-is-4 {$RESULT eq \ |
| 1331 | "TH_ERROR: Expected alnum, double, integer, or list, got: other"} |
| 1332 | |
| 1333 | ############################################################################### |
| 1334 | |
| 1335 | fossil test-th-eval {string is alnum 123} |
| 1336 | test th1-string-is-5 {$RESULT eq "1"} |
| 1337 | |
| 1338 | DDED tools/fake-smtpd.tcl |
| 1339 | DDED tools/find-fossil-cgis.tcl |
| --- test/th1.test | |
| +++ test/th1.test | |
| @@ -795,23 +795,23 @@ | |
| 795 | rpage-\$requested_page\ |
| 796 | cpage-\$canonical_page\">" [normalize_result]]} |
| 797 | |
| 798 | ############################################################################### |
| 799 | |
| 800 | #fossil test-th-eval "styleHeader {Page Title Here}" |
| 801 | #test th1-header-1 {$RESULT eq {TH_ERROR: repository unavailable}} |
| 802 | |
| 803 | ############################################################################### |
| 804 | |
| 805 | test_in_checkout th1-header-2 { |
| 806 | fossil test-th-eval --open-config "styleHeader {Page Title Here}" |
| 807 | } {[regexp -- {<title>Fossil: Page Title Here</title>} $RESULT]} |
| 808 | |
| 809 | ############################################################################### |
| 810 | |
| 811 | #fossil test-th-eval "styleFooter" |
| 812 | #test th1-footer-1 {$RESULT eq {TH_ERROR: repository unavailable}} |
| 813 | |
| 814 | ############################################################################### |
| 815 | |
| 816 | fossil test-th-eval --open-config "styleFooter" |
| 817 | test th1-footer-2 {$RESULT eq {}} |
| @@ -879,44 +879,44 @@ | |
| 879 | test th1-artifact-1 {$RESULT eq \ |
| 880 | {TH_ERROR: wrong # args: should be "artifact ID ?FILENAME?"}} |
| 881 | |
| 882 | ############################################################################### |
| 883 | |
| 884 | #fossil test-th-eval "artifact tip" |
| 885 | #test th1-artifact-2 {$RESULT eq {TH_ERROR: repository unavailable}} |
| 886 | |
| 887 | ############################################################################### |
| 888 | |
| 889 | test_in_checkout th1-artifact-3 { |
| 890 | fossil test-th-eval --open-config "artifact tip" |
| 891 | } {[regexp -- {F test/th1\.test [0-9a-f]{40,64}} $RESULT]} |
| 892 | |
| 893 | ############################################################################### |
| 894 | |
| 895 | #fossil test-th-eval "artifact 0000000000" |
| 896 | #test th1-artifact-4 {$RESULT eq {TH_ERROR: repository unavailable}} |
| 897 | |
| 898 | ############################################################################### |
| 899 | |
| 900 | fossil test-th-eval --open-config "artifact 0000000000" |
| 901 | test th1-artifact-5 {$RESULT eq {TH_ERROR: name not found}} |
| 902 | |
| 903 | ############################################################################### |
| 904 | |
| 905 | #fossil test-th-eval "artifact tip test/th1.test" |
| 906 | #test th1-artifact-6 {$RESULT eq {TH_ERROR: repository unavailable}} |
| 907 | |
| 908 | ############################################################################### |
| 909 | |
| 910 | test_in_checkout th1-artifact-7 { |
| 911 | fossil test-th-eval --open-config "artifact tip test/th1.test" |
| 912 | } {[regexp -- {th1-artifact-7} $RESULT]} |
| 913 | |
| 914 | ############################################################################### |
| 915 | |
| 916 | #fossil test-th-eval "artifact 0000000000 test/th1.test" |
| 917 | #test th1-artifact-8 {$RESULT eq {TH_ERROR: repository unavailable}} |
| 918 | |
| 919 | ############################################################################### |
| 920 | |
| 921 | fossil test-th-eval --open-config "artifact 0000000000 test/th1.test" |
| 922 | test th1-artifact-9 {$RESULT eq {TH_ERROR: manifest not found}} |
| @@ -947,12 +947,12 @@ | |
| 947 | } |
| 948 | } |
| 949 | |
| 950 | ############################################################################### |
| 951 | |
| 952 | #fossil test-th-eval "globalState configuration" |
| 953 | #test th1-globalState-3 {[string length $RESULT] == 0} |
| 954 | |
| 955 | ############################################################################### |
| 956 | |
| 957 | fossil test-th-eval --open-config "globalState configuration" |
| 958 | test th1-globalState-4 {[string length $RESULT] > 0} |
| @@ -1041,12 +1041,12 @@ | |
| 1041 | fossil test-th-eval "globalState flags" |
| 1042 | test th1-globalState-16 {$RESULT eq "0"} |
| 1043 | |
| 1044 | ############################################################################### |
| 1045 | |
| 1046 | #fossil test-th-eval "reinitialize; globalState configuration" |
| 1047 | #test th1-reinitialize-1 {$RESULT eq ""} |
| 1048 | |
| 1049 | ############################################################################### |
| 1050 | |
| 1051 | fossil test-th-eval "reinitialize 1; globalState configuration" |
| 1052 | test th1-reinitialize-2 {$RESULT ne ""} |
| @@ -1056,29 +1056,29 @@ | |
| 1056 | # |
| 1057 | # NOTE: This test will fail if the command names are added to TH1, or |
| 1058 | # moved from Tcl builds to plain or the reverse. Sorting the |
| 1059 | # command lists eliminates a dependence on order. |
| 1060 | # |
| 1061 | #fossil test-th-eval "info commands" |
| 1062 | #set sorted_result [lsort $RESULT] |
| 1063 | #protOut "Sorted: $sorted_result" |
| 1064 | #set base_commands {anoncap anycap array artifact break breakpoint \ |
| 1065 | # builtin_request_js capexpr captureTh1 catch cgiHeaderLine checkout \ |
| 1066 | # combobox continue copybtn date decorate defHeader dir enable_htmlify \ |
| 1067 | # enable_output encode64 error expr for foreach getParameter glob_match \ |
| 1068 | # globalState hascap hasfeature html htmlize http httpize if info \ |
| 1069 | # insertCsrf lappend lindex linecount list llength lsearch markdown nonce \ |
| 1070 | # proc puts query randhex redirect regexp reinitialize rename render \ |
| 1071 | # repository return searchable set setParameter setting stime string \ |
| 1072 | # styleFooter styleHeader styleScript submenu tclReady trace unset \ |
| 1073 | # unversioned uplevel upvar utime verifyCsrf verifyLogin wiki} |
| 1074 | #set tcl_commands {tclEval tclExpr tclInvoke tclIsSafe tclMakeSafe} |
| 1075 | #if {$th1Tcl} { |
| 1076 | # test th1-info-commands-1 {$sorted_result eq [lsort "$base_commands $tcl_commands"]} |
| 1077 | #} else { |
| 1078 | # test th1-info-commands-1 {$sorted_result eq [lsort "$base_commands"]} |
| 1079 | #} |
| 1080 | |
| 1081 | ############################################################################### |
| 1082 | |
| 1083 | fossil test-th-eval "info vars" |
| 1084 | |
| @@ -1326,11 +1326,11 @@ | |
| 1326 | |
| 1327 | ############################################################################### |
| 1328 | |
| 1329 | fossil test-th-eval {string is other 123} |
| 1330 | test th1-string-is-4 {$RESULT eq \ |
| 1331 | "TH_ERROR: Expected alnum, double, integer, list, or tainted, got: other"} |
| 1332 | |
| 1333 | ############################################################################### |
| 1334 | |
| 1335 | fossil test-th-eval {string is alnum 123} |
| 1336 | test th1-string-is-5 {$RESULT eq "1"} |
| 1337 | |
| 1338 | DDED tools/fake-smtpd.tcl |
| 1339 | DDED tools/find-fossil-cgis.tcl |
+84
| --- a/tools/fake-smtpd.tcl | ||
| +++ b/tools/fake-smtpd.tcl | ||
| @@ -0,0 +1,84 @@ | ||
| 1 | +#!/usr/bin/tclsh | |
| 2 | +# | |
| 3 | +# This script is a testing aid for working on the Relay notification method | |
| 4 | +# in Fossil. | |
| 5 | +# | |
| 6 | +# This script listens for connections on port 25 or probably some other TCP | |
| 7 | +# port specified by the "--port N" option. It pretend to be an SMTP server, | |
| 8 | +# though it does not actually relay any email. Instead, it just prints the | |
| 9 | +# SMTP conversation on stdout. | |
| 10 | +# | |
| 11 | +# If the "--max N" option is used, then the fake SMTP server shuts down | |
| 12 | +# with an error after receiving N messages from the client. This can be | |
| 13 | +# used to test retry capabilities in the client. | |
| 14 | +# | |
| 15 | +# Suggested Test Procedure For Fossil Relay Notifications | |
| 16 | +# | |
| 17 | +# 1. Bring up "fossil ui" | |
| 18 | +# 2. Configure notification for relay to localhost:8025 | |
| 19 | +# 3. Start this script in a separate window. Something like: | |
| 20 | +# tclsh fake-smtpd.tcl -port 8025 -max 100 | |
| 21 | +# 4. Send test messages using Fossil | |
| 22 | +# | |
| 23 | +proc conn_puts {chan txt} { | |
| 24 | + puts "S: $txt" | |
| 25 | + puts $chan $txt | |
| 26 | + flush $chan | |
| 27 | +} | |
| 28 | +set mxMsg 100000000 | |
| 29 | +proc connection {chan ip port} { | |
| 30 | + global mxMsg | |
| 31 | + set nMsg 0 | |
| 32 | + puts "*** begin connection from $ip:$port ***" | |
| 33 | + conn_puts $chan "220 localhost fake-SMTPD" | |
| 34 | + set inData 0 | |
| 35 | + while {1} { | |
| 36 | + set line [string trimright [gets $chan]] | |
| 37 | + if {$line eq ""} { | |
| 38 | + if {[eof $chan]} break | |
| 39 | + } | |
| 40 | + puts "C: $line" | |
| 41 | + incr nMsg | |
| 42 | + if {$inData} { | |
| 43 | + if {$line eq "."} { | |
| 44 | + set inData 0 | |
| 45 | + conn_puts $chan "250 Ok" | |
| 46 | + } | |
| 47 | + } elseif {$nMsg>$mxMsg} { | |
| 48 | + conn_puts $chan "999 I'm done!" | |
| 49 | + break | |
| 50 | + } elseif {[string match "HELO *" $line]} { | |
| 51 | + conn_puts $chan "250 Ok" | |
| 52 | + } elseif {[string match "EHLO *" $line]} { | |
| 53 | + conn_puts $chan "250-SIZE 100000" | |
| 54 | + conn_puts $chan "250 HELP" | |
| 55 | + } elseif {[string match "DATA*" $line]} { | |
| 56 | + conn_puts $chan "354 End data with <CR><LF>.<CR><LF>" | |
| 57 | + set inData 1 | |
| 58 | + } elseif {[string match "QUIT*" $line]} { | |
| 59 | + conn_puts $chan "221 Bye" | |
| 60 | + break | |
| 61 | + } else { | |
| 62 | + conn_puts $chan "250 Ok" | |
| 63 | + } | |
| 64 | + } | |
| 65 | + puts "*** connection closed ($nMsg messages) ***" | |
| 66 | + close $chan | |
| 67 | +} | |
| 68 | +set port 25 | |
| 69 | +set argc [llength $argv] | |
| 70 | +for {set i 0} {$i<$argc-1} {incr i} { | |
| 71 | + set arg [lindex $argv $i] | |
| 72 | + if {$arg eq "-port" || $arg eq "--port"} { | |
| 73 | + incr i | |
| 74 | + set port [lindex $argv $i] | |
| 75 | + } | |
| 76 | + if {$arg eq "-max" || $arg eq "--max"} { | |
| 77 | + incr i | |
| 78 | + set mxMsg [lindex $argv $i] | |
| 79 | + } | |
| 80 | +} | |
| 81 | +puts "listening on localhost:$port" | |
| 82 | +socket -server connection $port | |
| 83 | +set forever 0 | |
| 84 | +vwait forever |
| --- a/tools/fake-smtpd.tcl | |
| +++ b/tools/fake-smtpd.tcl | |
| @@ -0,0 +1,84 @@ | |
| --- a/tools/fake-smtpd.tcl | |
| +++ b/tools/fake-smtpd.tcl | |
| @@ -0,0 +1,84 @@ | |
| 1 | #!/usr/bin/tclsh |
| 2 | # |
| 3 | # This script is a testing aid for working on the Relay notification method |
| 4 | # in Fossil. |
| 5 | # |
| 6 | # This script listens for connections on port 25 or probably some other TCP |
| 7 | # port specified by the "--port N" option. It pretend to be an SMTP server, |
| 8 | # though it does not actually relay any email. Instead, it just prints the |
| 9 | # SMTP conversation on stdout. |
| 10 | # |
| 11 | # If the "--max N" option is used, then the fake SMTP server shuts down |
| 12 | # with an error after receiving N messages from the client. This can be |
| 13 | # used to test retry capabilities in the client. |
| 14 | # |
| 15 | # Suggested Test Procedure For Fossil Relay Notifications |
| 16 | # |
| 17 | # 1. Bring up "fossil ui" |
| 18 | # 2. Configure notification for relay to localhost:8025 |
| 19 | # 3. Start this script in a separate window. Something like: |
| 20 | # tclsh fake-smtpd.tcl -port 8025 -max 100 |
| 21 | # 4. Send test messages using Fossil |
| 22 | # |
| 23 | proc conn_puts {chan txt} { |
| 24 | puts "S: $txt" |
| 25 | puts $chan $txt |
| 26 | flush $chan |
| 27 | } |
| 28 | set mxMsg 100000000 |
| 29 | proc connection {chan ip port} { |
| 30 | global mxMsg |
| 31 | set nMsg 0 |
| 32 | puts "*** begin connection from $ip:$port ***" |
| 33 | conn_puts $chan "220 localhost fake-SMTPD" |
| 34 | set inData 0 |
| 35 | while {1} { |
| 36 | set line [string trimright [gets $chan]] |
| 37 | if {$line eq ""} { |
| 38 | if {[eof $chan]} break |
| 39 | } |
| 40 | puts "C: $line" |
| 41 | incr nMsg |
| 42 | if {$inData} { |
| 43 | if {$line eq "."} { |
| 44 | set inData 0 |
| 45 | conn_puts $chan "250 Ok" |
| 46 | } |
| 47 | } elseif {$nMsg>$mxMsg} { |
| 48 | conn_puts $chan "999 I'm done!" |
| 49 | break |
| 50 | } elseif {[string match "HELO *" $line]} { |
| 51 | conn_puts $chan "250 Ok" |
| 52 | } elseif {[string match "EHLO *" $line]} { |
| 53 | conn_puts $chan "250-SIZE 100000" |
| 54 | conn_puts $chan "250 HELP" |
| 55 | } elseif {[string match "DATA*" $line]} { |
| 56 | conn_puts $chan "354 End data with <CR><LF>.<CR><LF>" |
| 57 | set inData 1 |
| 58 | } elseif {[string match "QUIT*" $line]} { |
| 59 | conn_puts $chan "221 Bye" |
| 60 | break |
| 61 | } else { |
| 62 | conn_puts $chan "250 Ok" |
| 63 | } |
| 64 | } |
| 65 | puts "*** connection closed ($nMsg messages) ***" |
| 66 | close $chan |
| 67 | } |
| 68 | set port 25 |
| 69 | set argc [llength $argv] |
| 70 | for {set i 0} {$i<$argc-1} {incr i} { |
| 71 | set arg [lindex $argv $i] |
| 72 | if {$arg eq "-port" || $arg eq "--port"} { |
| 73 | incr i |
| 74 | set port [lindex $argv $i] |
| 75 | } |
| 76 | if {$arg eq "-max" || $arg eq "--max"} { |
| 77 | incr i |
| 78 | set mxMsg [lindex $argv $i] |
| 79 | } |
| 80 | } |
| 81 | puts "listening on localhost:$port" |
| 82 | socket -server connection $port |
| 83 | set forever 0 |
| 84 | vwait forever |
+141
| --- a/tools/find-fossil-cgis.tcl | ||
| +++ b/tools/find-fossil-cgis.tcl | ||
| @@ -0,0 +1,141 @@ | ||
| 1 | +#!/usr/bin/tclsh | |
| 2 | +# | |
| 3 | +# This script scans a directory hierarchy looking for Fossil CGI files - | |
| 4 | +# the files that are used to launch Fossil as a CGI program. For each | |
| 5 | +# such file found, in prints the name of the file and also the file | |
| 6 | +# content, indented, if the --print option is used. | |
| 7 | +# | |
| 8 | +# tclsh find-fossil-cgis.tcl [OPTIONS] DIRECTORY | |
| 9 | +# | |
| 10 | +# The argument is the directory from which to begin the search. | |
| 11 | +# | |
| 12 | +# OPTIONS can be zero or more of the following: | |
| 13 | +# | |
| 14 | +# --has REGEXP Only show the CGI if the body matches REGEXP. | |
| 15 | +# May be repeated multiple times, in which case | |
| 16 | +# all must match. | |
| 17 | +# | |
| 18 | +# --hasnot REGEXP Only show the CGI if it does NOT match the | |
| 19 | +# REGEXP. | |
| 20 | +# | |
| 21 | +# --print Show the content of the CGI, indented by | |
| 22 | +# three spaces | |
| 23 | +# | |
| 24 | +# --symlink Process DIRECTORY arguments that are symlinks. | |
| 25 | +# Normally symlinks are silently ignored. | |
| 26 | +# | |
| 27 | +# -v Show progress information for debugging | |
| 28 | +# | |
| 29 | +# EXAMPLE USE CASES: | |
| 30 | +# | |
| 31 | +# Find all CGIs that do not have the "errorlog:" property set | |
| 32 | +# | |
| 33 | +# find-fossil-cgis.tcl *.website --has '\nrepository:' \ | |
| 34 | +# --hasnot '\nerrorlog:' | |
| 35 | +# | |
| 36 | +# Add the errorlog: property to any CGI that does not have it: | |
| 37 | +# | |
| 38 | +# find-fossil-cgis.tcl *.website --has '\nrepository:' \ | |
| 39 | +# --hasnot '\nerrorlog:' | while read x | |
| 40 | +# do | |
| 41 | +# echo 'errorlog: /logs/errors.txt' >>$x | |
| 42 | +# done | |
| 43 | +# | |
| 44 | +# Find and print all CGIs that do redirects | |
| 45 | +# | |
| 46 | +# find-fossil-cgis.tcl *.website --has '\nredirect:' --print | |
| 47 | +# | |
| 48 | + | |
| 49 | + | |
| 50 | +# Find the CGIs in directory $dir. Invoke recursively to | |
| 51 | +# scan subdirectories. | |
| 52 | +# | |
| 53 | +proc find_in_one_dir {dir} { | |
| 54 | + global HAS HASNOT PRINT V | |
| 55 | + if {$V>0} { | |
| 56 | + puts "# $dir" | |
| 57 | + } | |
| 58 | + foreach obj [lsort [glob -nocomplain -directory $dir *]] { | |
| 59 | + if {[file isdir $obj]} { | |
| 60 | + find_in_one_dir $obj | |
| 61 | + continue | |
| 62 | + } | |
| 63 | + if {![file isfile $obj]} continue | |
| 64 | + if {[file size $obj]>5000} continue | |
| 65 | + if {![file exec $obj]} continue | |
| 66 | + if {![file readable $obj]} continue | |
| 67 | + set fd [open $obj rb] | |
| 68 | + set txt [read $fd] | |
| 69 | + close $fd | |
| 70 | + if {![string match #!* $txt]} continue | |
| 71 | + if {![regexp {fossil} $txt]} continue | |
| 72 | + if {![regexp {\nrepository: } $txt] && | |
| 73 | + ![regexp {\ndirectory: } $txt] && | |
| 74 | + ![regexp {\nredirect: } $txt]} continue | |
| 75 | + set ok 1 | |
| 76 | + foreach re $HAS { | |
| 77 | + if {![regexp $re $txt]} {set ok 0; break;} | |
| 78 | + } | |
| 79 | + if {!$ok} continue | |
| 80 | + foreach re $HASNOT { | |
| 81 | + if {[regexp $re $txt]} {set ok 0; break;} | |
| 82 | + } | |
| 83 | + if {!$ok} continue | |
| 84 | + # | |
| 85 | + # At this point assume we have found a CGI file. | |
| 86 | + # | |
| 87 | + puts $obj | |
| 88 | + if {$PRINT} { | |
| 89 | + regsub -all {\n} [string trim $txt] "\n " out | |
| 90 | + puts " $out" | |
| 91 | + } | |
| 92 | + } | |
| 93 | +} | |
| 94 | +set HAS [list] | |
| 95 | +set HASNOT [list] | |
| 96 | +set PRINT 0 | |
| 97 | +set SYMLINK 0 | |
| 98 | +set V 0 | |
| 99 | +set N [llength $argv] | |
| 100 | +set DIRLIST [list] | |
| 101 | + | |
| 102 | +# First pass: Gather all the command-line arguments but do no | |
| 103 | +# processing. | |
| 104 | +# | |
| 105 | +for {set i 0} {$i<$N} {incr i} { | |
| 106 | + set dir [lindex $argv $i] | |
| 107 | + if {($dir eq "-has" || $dir eq "--has") && $i<[expr {$N-1}]} { | |
| 108 | + incr i | |
| 109 | + lappend HAS [lindex $argv $i] | |
| 110 | + continue | |
| 111 | + } | |
| 112 | + if {($dir eq "-hasnot" || $dir eq "--hasnot") && $i<[expr {$N-1}]} { | |
| 113 | + incr i | |
| 114 | + lappend HASNOT [lindex $argv $i] | |
| 115 | + continue | |
| 116 | + } | |
| 117 | + if {$dir eq "-print" || $dir eq "--print"} { | |
| 118 | + set PRINT 1 | |
| 119 | + continue | |
| 120 | + } | |
| 121 | + if {$dir eq "-symlink" || $dir eq "--symlink"} { | |
| 122 | + set SYMLINK 1 | |
| 123 | + continue | |
| 124 | + } | |
| 125 | + if {$dir eq "-v"} { | |
| 126 | + set V 1 | |
| 127 | + continue | |
| 128 | + } | |
| 129 | + if {[file type $dir]=="directory"} { | |
| 130 | + lappend DIRLIST $dir | |
| 131 | + } | |
| 132 | +} | |
| 133 | + | |
| 134 | +# Second pass: Process the non-option arguments. | |
| 135 | +# | |
| 136 | +foreach dir $DIRLIST { | |
| 137 | + set type [file type $dir] | |
| 138 | + if {$type eq "directory" || ($SYMLINK && $type eq "link")} { | |
| 139 | + find_in_one_dir $dir | |
| 140 | + } | |
| 141 | +} |
| --- a/tools/find-fossil-cgis.tcl | |
| +++ b/tools/find-fossil-cgis.tcl | |
| @@ -0,0 +1,141 @@ | |
| --- a/tools/find-fossil-cgis.tcl | |
| +++ b/tools/find-fossil-cgis.tcl | |
| @@ -0,0 +1,141 @@ | |
| 1 | #!/usr/bin/tclsh |
| 2 | # |
| 3 | # This script scans a directory hierarchy looking for Fossil CGI files - |
| 4 | # the files that are used to launch Fossil as a CGI program. For each |
| 5 | # such file found, in prints the name of the file and also the file |
| 6 | # content, indented, if the --print option is used. |
| 7 | # |
| 8 | # tclsh find-fossil-cgis.tcl [OPTIONS] DIRECTORY |
| 9 | # |
| 10 | # The argument is the directory from which to begin the search. |
| 11 | # |
| 12 | # OPTIONS can be zero or more of the following: |
| 13 | # |
| 14 | # --has REGEXP Only show the CGI if the body matches REGEXP. |
| 15 | # May be repeated multiple times, in which case |
| 16 | # all must match. |
| 17 | # |
| 18 | # --hasnot REGEXP Only show the CGI if it does NOT match the |
| 19 | # REGEXP. |
| 20 | # |
| 21 | # --print Show the content of the CGI, indented by |
| 22 | # three spaces |
| 23 | # |
| 24 | # --symlink Process DIRECTORY arguments that are symlinks. |
| 25 | # Normally symlinks are silently ignored. |
| 26 | # |
| 27 | # -v Show progress information for debugging |
| 28 | # |
| 29 | # EXAMPLE USE CASES: |
| 30 | # |
| 31 | # Find all CGIs that do not have the "errorlog:" property set |
| 32 | # |
| 33 | # find-fossil-cgis.tcl *.website --has '\nrepository:' \ |
| 34 | # --hasnot '\nerrorlog:' |
| 35 | # |
| 36 | # Add the errorlog: property to any CGI that does not have it: |
| 37 | # |
| 38 | # find-fossil-cgis.tcl *.website --has '\nrepository:' \ |
| 39 | # --hasnot '\nerrorlog:' | while read x |
| 40 | # do |
| 41 | # echo 'errorlog: /logs/errors.txt' >>$x |
| 42 | # done |
| 43 | # |
| 44 | # Find and print all CGIs that do redirects |
| 45 | # |
| 46 | # find-fossil-cgis.tcl *.website --has '\nredirect:' --print |
| 47 | # |
| 48 | |
| 49 | |
| 50 | # Find the CGIs in directory $dir. Invoke recursively to |
| 51 | # scan subdirectories. |
| 52 | # |
| 53 | proc find_in_one_dir {dir} { |
| 54 | global HAS HASNOT PRINT V |
| 55 | if {$V>0} { |
| 56 | puts "# $dir" |
| 57 | } |
| 58 | foreach obj [lsort [glob -nocomplain -directory $dir *]] { |
| 59 | if {[file isdir $obj]} { |
| 60 | find_in_one_dir $obj |
| 61 | continue |
| 62 | } |
| 63 | if {![file isfile $obj]} continue |
| 64 | if {[file size $obj]>5000} continue |
| 65 | if {![file exec $obj]} continue |
| 66 | if {![file readable $obj]} continue |
| 67 | set fd [open $obj rb] |
| 68 | set txt [read $fd] |
| 69 | close $fd |
| 70 | if {![string match #!* $txt]} continue |
| 71 | if {![regexp {fossil} $txt]} continue |
| 72 | if {![regexp {\nrepository: } $txt] && |
| 73 | ![regexp {\ndirectory: } $txt] && |
| 74 | ![regexp {\nredirect: } $txt]} continue |
| 75 | set ok 1 |
| 76 | foreach re $HAS { |
| 77 | if {![regexp $re $txt]} {set ok 0; break;} |
| 78 | } |
| 79 | if {!$ok} continue |
| 80 | foreach re $HASNOT { |
| 81 | if {[regexp $re $txt]} {set ok 0; break;} |
| 82 | } |
| 83 | if {!$ok} continue |
| 84 | # |
| 85 | # At this point assume we have found a CGI file. |
| 86 | # |
| 87 | puts $obj |
| 88 | if {$PRINT} { |
| 89 | regsub -all {\n} [string trim $txt] "\n " out |
| 90 | puts " $out" |
| 91 | } |
| 92 | } |
| 93 | } |
| 94 | set HAS [list] |
| 95 | set HASNOT [list] |
| 96 | set PRINT 0 |
| 97 | set SYMLINK 0 |
| 98 | set V 0 |
| 99 | set N [llength $argv] |
| 100 | set DIRLIST [list] |
| 101 | |
| 102 | # First pass: Gather all the command-line arguments but do no |
| 103 | # processing. |
| 104 | # |
| 105 | for {set i 0} {$i<$N} {incr i} { |
| 106 | set dir [lindex $argv $i] |
| 107 | if {($dir eq "-has" || $dir eq "--has") && $i<[expr {$N-1}]} { |
| 108 | incr i |
| 109 | lappend HAS [lindex $argv $i] |
| 110 | continue |
| 111 | } |
| 112 | if {($dir eq "-hasnot" || $dir eq "--hasnot") && $i<[expr {$N-1}]} { |
| 113 | incr i |
| 114 | lappend HASNOT [lindex $argv $i] |
| 115 | continue |
| 116 | } |
| 117 | if {$dir eq "-print" || $dir eq "--print"} { |
| 118 | set PRINT 1 |
| 119 | continue |
| 120 | } |
| 121 | if {$dir eq "-symlink" || $dir eq "--symlink"} { |
| 122 | set SYMLINK 1 |
| 123 | continue |
| 124 | } |
| 125 | if {$dir eq "-v"} { |
| 126 | set V 1 |
| 127 | continue |
| 128 | } |
| 129 | if {[file type $dir]=="directory"} { |
| 130 | lappend DIRLIST $dir |
| 131 | } |
| 132 | } |
| 133 | |
| 134 | # Second pass: Process the non-option arguments. |
| 135 | # |
| 136 | foreach dir $DIRLIST { |
| 137 | set type [file type $dir] |
| 138 | if {$type eq "directory" || ($SYMLINK && $type eq "link")} { |
| 139 | find_in_one_dir $dir |
| 140 | } |
| 141 | } |
+1
-1
| --- tools/fossil-stress.tcl | ||
| +++ tools/fossil-stress.tcl | ||
| @@ -93,11 +93,11 @@ | ||
| 93 | 93 | /fileage |
| 94 | 94 | /dir |
| 95 | 95 | /tree |
| 96 | 96 | /uvlist |
| 97 | 97 | /stat |
| 98 | - /test_env | |
| 98 | + /test-env | |
| 99 | 99 | /sitemap |
| 100 | 100 | /hash-collisions |
| 101 | 101 | /artifact_stats |
| 102 | 102 | /bloblist |
| 103 | 103 | /bigbloblist |
| 104 | 104 |
| --- tools/fossil-stress.tcl | |
| +++ tools/fossil-stress.tcl | |
| @@ -93,11 +93,11 @@ | |
| 93 | /fileage |
| 94 | /dir |
| 95 | /tree |
| 96 | /uvlist |
| 97 | /stat |
| 98 | /test_env |
| 99 | /sitemap |
| 100 | /hash-collisions |
| 101 | /artifact_stats |
| 102 | /bloblist |
| 103 | /bigbloblist |
| 104 |
| --- tools/fossil-stress.tcl | |
| +++ tools/fossil-stress.tcl | |
| @@ -93,11 +93,11 @@ | |
| 93 | /fileage |
| 94 | /dir |
| 95 | /tree |
| 96 | /uvlist |
| 97 | /stat |
| 98 | /test-env |
| 99 | /sitemap |
| 100 | /hash-collisions |
| 101 | /artifact_stats |
| 102 | /bloblist |
| 103 | /bigbloblist |
| 104 |
+1
-1
| --- www/aboutcgi.wiki | ||
| +++ www/aboutcgi.wiki | ||
| @@ -67,11 +67,11 @@ | ||
| 67 | 67 | <td>The query string that follows the "?" in the URL, if there is one. |
| 68 | 68 | </table> |
| 69 | 69 | |
| 70 | 70 | There are other CGI environment variables beyond those listed above. |
| 71 | 71 | Many Fossil servers implement the |
| 72 | -[https://fossil-scm.org/home/test_env/two/three?abc=xyz|test_env] | |
| 72 | +[https://fossil-scm.org/home/test-env/two/three?abc=xyz|test-env] | |
| 73 | 73 | webpage that shows some of the CGI environment |
| 74 | 74 | variables that Fossil pays attention to. |
| 75 | 75 | |
| 76 | 76 | In addition to setting various CGI environment variables, if the HTTP |
| 77 | 77 | request contains POST content, then the web server relays the POST content |
| 78 | 78 |
| --- www/aboutcgi.wiki | |
| +++ www/aboutcgi.wiki | |
| @@ -67,11 +67,11 @@ | |
| 67 | <td>The query string that follows the "?" in the URL, if there is one. |
| 68 | </table> |
| 69 | |
| 70 | There are other CGI environment variables beyond those listed above. |
| 71 | Many Fossil servers implement the |
| 72 | [https://fossil-scm.org/home/test_env/two/three?abc=xyz|test_env] |
| 73 | webpage that shows some of the CGI environment |
| 74 | variables that Fossil pays attention to. |
| 75 | |
| 76 | In addition to setting various CGI environment variables, if the HTTP |
| 77 | request contains POST content, then the web server relays the POST content |
| 78 |
| --- www/aboutcgi.wiki | |
| +++ www/aboutcgi.wiki | |
| @@ -67,11 +67,11 @@ | |
| 67 | <td>The query string that follows the "?" in the URL, if there is one. |
| 68 | </table> |
| 69 | |
| 70 | There are other CGI environment variables beyond those listed above. |
| 71 | Many Fossil servers implement the |
| 72 | [https://fossil-scm.org/home/test-env/two/three?abc=xyz|test-env] |
| 73 | webpage that shows some of the CGI environment |
| 74 | variables that Fossil pays attention to. |
| 75 | |
| 76 | In addition to setting various CGI environment variables, if the HTTP |
| 77 | request contains POST content, then the web server relays the POST content |
| 78 |
+6
| --- www/cgi.wiki | ||
| +++ www/cgi.wiki | ||
| @@ -79,10 +79,16 @@ | ||
| 79 | 79 | If no repository has such a non-zero repolist-skin setting, then |
| 80 | 80 | the repository list is generic HTML without any decoration, with |
| 81 | 81 | the page title taken from the <tt>FOSSIL_REPOLIST_TITLE</tt> |
| 82 | 82 | environment variable. The variable can be defined in the CGI |
| 83 | 83 | control file using the [#setenv|<tt>setenv:</tt>] statement. |
| 84 | + | |
| 85 | +The "Project Description" and "Login-Group" columns on the repolist page | |
| 86 | +are optional. They are hidden by default. Show them by putting value "1" | |
| 87 | +in the global settings "show-repolist-desc" and "show-repolist-lg", or | |
| 88 | +by setting the <tt>FOSSIL_REPOLIST_SHOW</tt> environment variable to | |
| 89 | +a string that contains substrings "description" and/or "login-group". | |
| 84 | 90 | |
| 85 | 91 | The repolist-generated page recurses into subdirectories and will list |
| 86 | 92 | all <tt>*.fossil</tt> files found, with the following exceptions: |
| 87 | 93 | |
| 88 | 94 | * Filenames starting with a period are treated as "hidden" and skipped. |
| 89 | 95 |
| --- www/cgi.wiki | |
| +++ www/cgi.wiki | |
| @@ -79,10 +79,16 @@ | |
| 79 | If no repository has such a non-zero repolist-skin setting, then |
| 80 | the repository list is generic HTML without any decoration, with |
| 81 | the page title taken from the <tt>FOSSIL_REPOLIST_TITLE</tt> |
| 82 | environment variable. The variable can be defined in the CGI |
| 83 | control file using the [#setenv|<tt>setenv:</tt>] statement. |
| 84 | |
| 85 | The repolist-generated page recurses into subdirectories and will list |
| 86 | all <tt>*.fossil</tt> files found, with the following exceptions: |
| 87 | |
| 88 | * Filenames starting with a period are treated as "hidden" and skipped. |
| 89 |
| --- www/cgi.wiki | |
| +++ www/cgi.wiki | |
| @@ -79,10 +79,16 @@ | |
| 79 | If no repository has such a non-zero repolist-skin setting, then |
| 80 | the repository list is generic HTML without any decoration, with |
| 81 | the page title taken from the <tt>FOSSIL_REPOLIST_TITLE</tt> |
| 82 | environment variable. The variable can be defined in the CGI |
| 83 | control file using the [#setenv|<tt>setenv:</tt>] statement. |
| 84 | |
| 85 | The "Project Description" and "Login-Group" columns on the repolist page |
| 86 | are optional. They are hidden by default. Show them by putting value "1" |
| 87 | in the global settings "show-repolist-desc" and "show-repolist-lg", or |
| 88 | by setting the <tt>FOSSIL_REPOLIST_SHOW</tt> environment variable to |
| 89 | a string that contains substrings "description" and/or "login-group". |
| 90 | |
| 91 | The repolist-generated page recurses into subdirectories and will list |
| 92 | all <tt>*.fossil</tt> files found, with the following exceptions: |
| 93 | |
| 94 | * Filenames starting with a period are treated as "hidden" and skipped. |
| 95 |
+53
-22
| --- www/changes.wiki | ||
| +++ www/changes.wiki | ||
| @@ -1,43 +1,45 @@ | ||
| 1 | 1 | <title>Change Log</title> |
| 2 | 2 | |
| 3 | -<h2 id='v2_26'>Changes for version 2.26 (pending)</h2> | |
| 4 | - | |
| 5 | - * Enhancements to [/help?cmd=diff|fossil diff] and similar: | |
| 3 | +<h2 id='v2_26'>Changes for version 2.26 (pending)</h2><ol> | |
| 4 | + <li>Enhancements to [/help?cmd=diff|fossil diff] and similar: | |
| 6 | 5 | <ol type="a"> |
| 7 | - <li> The --from can optionally accepts a directory name as its argument, | |
| 6 | + <li> The --from can optionally accept a directory name as its argument, | |
| 8 | 7 | and uses files under that directory as the baseline for the diff. |
| 9 | 8 | <li> For "gdiff", if no [/help?cmd=gdiff-command|gdiff-command setting] |
| 10 | 9 | is defined, Fossil tries to do a --tk diff if "tclsh" and "wish" |
| 11 | 10 | are available, or a --by diff if not. |
| 12 | 11 | <li> The "Reload" button is added to --tk diffs, to bring the displayed |
| 13 | 12 | diff up to date with the latest changes on disk. |
| 14 | 13 | <li> Add the "Hide diffs/Show diffs" toggle to web-UI diff pages that show |
| 15 | 14 | diffs of multiple files. |
| 16 | 15 | </ol> |
| 17 | - * Added the [/help?cmd=/ckout|/ckout web page] to provide information | |
| 16 | + <li>Added the [/help?cmd=/ckout|/ckout web page] to provide information | |
| 18 | 17 | about pending changes in a working check-out |
| 19 | - * Enhancements to the [/help?cmd=ui|fossil ui] command: | |
| 18 | + <li>Enhancements to the [/help?cmd=ui|fossil ui] command: | |
| 20 | 19 | <ol type="a"> |
| 21 | 20 | <li> Defaults to using the new [/help?cmd=/ckout|/ckout page] as its |
| 22 | 21 | start page. Or, if the new "--from PATH" option is present, the |
| 23 | 22 | default start page becomes "/ckout?exbase=PATH". |
| 24 | 23 | <li> The new "--extpage FILENAME" option opens the named file as if it |
| 25 | 24 | where in a [./serverext.wiki|CGI extension]. Example usage: the |
| 26 | - person editing this change log has | |
| 25 | + person editing this change log has | |
| 27 | 26 | "fossil ui --extpage www/changes.wiki" running and hence can |
| 28 | 27 | press "Reload" on the web browser to view edits. |
| 28 | + <li> Accept both IPv4 and IPv6 connections on all platforms, including | |
| 29 | + Windows and OpenBSD. This also applies to the "fossil server" | |
| 30 | + command. | |
| 29 | 31 | </ol> |
| 30 | - * Enhancements to [/help?cmd=merge|fossil merge]: | |
| 32 | + <li>Enhancements to [/help?cmd=merge|fossil merge]: | |
| 31 | 33 | <ol type="a"> |
| 32 | 34 | <li> Added the [/help?cmd=merge-info|fossil merge-info] command and |
| 33 | 35 | especially the --tk option to that command, to provide analysis |
| 34 | 36 | of the most recent merge or update operation. |
| 35 | 37 | <li> When a merge conflict occurs, a new section is added to the conflict |
| 36 | 38 | text that shows Fossil's suggested resolution to the conflict. |
| 37 | 39 | </ol> |
| 38 | - * Enhancements to [/help?cmd=commit|fossil commit]: | |
| 40 | + <li>Enhancements to [/help?cmd=commit|fossil commit]: | |
| 39 | 41 | <ol type="a"> |
| 40 | 42 | <li> If Fossil sees potential formatting mistakes (ex: bad hyperlinks) |
| 41 | 43 | in the check-in comment, it will alert the developer and give |
| 42 | 44 | him or her the opportunity to edit the comment before continuing. |
| 43 | 45 | This feature is controllable by the |
| @@ -47,18 +49,19 @@ | ||
| 47 | 49 | <li> Added the ability to sign check-ins with SSH keys. |
| 48 | 50 | <li> Issue a warning if a user tries to commit on a check-in where the |
| 49 | 51 | branch has been changed. |
| 50 | 52 | <li> The interactive checkin comment prompt shows the formatting rules |
| 51 | 53 | set for that repository. |
| 54 | + <li> Add the "--editor" option. | |
| 52 | 55 | </ol> |
| 53 | - * Deprecate the --comfmtflags and --comment-format global options and | |
| 56 | + <li>Deprecate the --comfmtflags and --comment-format global options and | |
| 54 | 57 | no longer list them in the built-in help, but keep them working for |
| 55 | 58 | backwards compatibility. |
| 56 | 59 | Alternative TTY comment formatting can still be specified using the |
| 57 | 60 | [/help?cmd=comment-format|comment-format setting], if desired. The |
| 58 | 61 | default comment format is now called "canonical", not "legacy". |
| 59 | - * Enhancements to the [/help?cmd=/timeline|/timeline page]: | |
| 62 | + <li>Enhancements to the [/help?cmd=/timeline|/timeline page]: | |
| 60 | 63 | <ol type="a"> |
| 61 | 64 | <li> Added the "ml=" ("Merge-in List") query parameter that works |
| 62 | 65 | like "rl=" ("Related List") but adds "mionly" style related |
| 63 | 66 | check-ins instead of the full "rel" style. |
| 64 | 67 | <li> For "tl=", "rl=", and "ml=", the order of the branches in the |
| @@ -84,25 +87,32 @@ | ||
| 84 | 87 | collapses long runs of check-ins on the same branch into just |
| 85 | 88 | end-points. |
| 86 | 89 | <li> The p= and d= parameters an reference different check-ins, which |
| 87 | 90 | case the timeline shows those check-ins that are both ancestors |
| 88 | 91 | of p= and descendants of d=. |
| 92 | + <li> The saturation and intensity of user-specified checkin and branch | |
| 93 | + background colors are automatically adjusted to keep the colors | |
| 94 | + compatible with the current skin, unless the | |
| 95 | + [/help?cmd=raw-bgcolor|raw-bgcolor setting] is turned on. | |
| 89 | 96 | </ol> |
| 90 | - * Added the [/help?cmd=/clusterlist|/clusterlist page] for analysis | |
| 97 | + <li>The [/help?cmd=/docfile|/docfile webpage] was added. It works like | |
| 98 | + /doc but keeps the title of markdown documents with the document rather | |
| 99 | + that moving it up to the page title. | |
| 100 | + <li>Added the [/help?cmd=/clusterlist|/clusterlist page] for analysis | |
| 91 | 101 | and debugging |
| 92 | - * Added the "artifact_to_json(NAME)" SQL function that returns a JSON | |
| 102 | + <li>Added the "artifact_to_json(NAME)" SQL function that returns a JSON | |
| 93 | 103 | decoding of the artifact described by NAME. |
| 94 | - * Improvements to the [/help?cmd=patch|fossil patch] command: | |
| 104 | + <li>Improvements to the [/help?cmd=patch|fossil patch] command: | |
| 95 | 105 | <ol type="a"> |
| 96 | 106 | <li> Fix a bug in "fossil patch create" that causes |
| 97 | 107 | [/help?cmd=revert|fossil revert] operations that happened |
| 98 | 108 | on individualfiles after a [/help?cmd=merge|fossil merge] |
| 99 | 109 | to be omitted from the patch. |
| 100 | 110 | <li> Added the [/help?cmd=patch|patch alias] command for managing |
| 101 | 111 | aliases for remote checkout names. |
| 102 | 112 | </ol> |
| 103 | - * Enhancements to on-line help and the [/help?cmd=help|fossil help] command: | |
| 113 | + <li>Enhancements to on-line help and the [/help?cmd=help|fossil help] command: | |
| 104 | 114 | <ol type="a"> |
| 105 | 115 | <li> Add the ability to search the help text, either in the UI |
| 106 | 116 | (on the [/help?cmd=/search|/search page]) or from the command-line |
| 107 | 117 | (using the "[/help?cmd=search|fossil search -h PATTERN]" command.) |
| 108 | 118 | <li> Accepts an optional SUBCOMMAND argument following the |
| @@ -109,19 +119,40 @@ | ||
| 109 | 119 | COMMAND argument and only shows results for the specified |
| 110 | 120 | subcommand, not the entire command. |
| 111 | 121 | <li> The -u (--usage) option shows only the command-line syntax |
| 112 | 122 | <li> The -o (--options) option shows only the command-line options |
| 113 | 123 | </ol> |
| 114 | - * Added the ability to attach wiki pages to a ticket for extended | |
| 115 | - descriptions. | |
| 116 | - * Added the "hash" query parameter to the | |
| 124 | + <li>Enhancements to the ticket system: | |
| 125 | + <ol type="a"> | |
| 126 | + <li> Added the ability to attach wiki pages to a ticket for extended | |
| 127 | + descriptions. | |
| 128 | + <li> Added submenu to the 'View Ticket' page, to use it as | |
| 129 | + template for a new ticket. | |
| 130 | + <li> Added button 'Submit and New' to create multiple tickets | |
| 131 | + in a row. | |
| 132 | + <li> Link the version field in ticket view to a matching checkin or tag. | |
| 133 | + <li> Show creation time in report and ticket view. | |
| 134 | + <li> Show previous comments in edit ticket as reference. | |
| 135 | + </ol> | |
| 136 | + <li>Added the "hash" query parameter to the | |
| 117 | 137 | [/help?cmd=/whatis|/whatis webpage]. |
| 118 | - * Add a "user elevation" [/doc/trunk/www/alerts.md|subscription] | |
| 138 | + <li>Add a "user permissions changes" [/doc/trunk/www/alerts.md|subscription] | |
| 119 | 139 | which alerts subscribers when an admin creates a new user or |
| 120 | - adds new permissions to one. | |
| 121 | - * Diverse minor fixes and additions. | |
| 122 | - | |
| 140 | + when a user's permissions change. | |
| 141 | + <li>Show project description on repository list. | |
| 142 | + <li>Make [/help?cmd=/chat|/chat] better-behaved during server outages, reducing | |
| 143 | + the frequency of reconnection attempts over time and providing feedback | |
| 144 | + to the user when the connection is down. | |
| 145 | + <li>The [/doc/trunk/www/th1.md|TH1 script language] now makes a distinction | |
| 146 | + between [/doc/trunk/www/th1.md#taint|tainted and untainted string values]. | |
| 147 | + This is a security enhancement that makes it more difficult to write | |
| 148 | + custom TH1 scripts that contain XSS or SQL-injection bugs. As part of | |
| 149 | + this enhancement, the [/help?cmd=vuln-report|vuln-report] setting was | |
| 150 | + added to control what Fossil does when it encounters a potential TH1 | |
| 151 | + security problem. | |
| 152 | + <li>Diverse minor fixes and additions. | |
| 153 | +</ol> | |
| 123 | 154 | |
| 124 | 155 | <h2 id='v2_25'>Changes for version 2.25 (2024-11-06)</h2> |
| 125 | 156 | |
| 126 | 157 | * The "[/help?cmd=ui|fossil ui /]" command now works even for repositories |
| 127 | 158 | that have non-ASCII filenames |
| 128 | 159 |
| --- www/changes.wiki | |
| +++ www/changes.wiki | |
| @@ -1,43 +1,45 @@ | |
| 1 | <title>Change Log</title> |
| 2 | |
| 3 | <h2 id='v2_26'>Changes for version 2.26 (pending)</h2> |
| 4 | |
| 5 | * Enhancements to [/help?cmd=diff|fossil diff] and similar: |
| 6 | <ol type="a"> |
| 7 | <li> The --from can optionally accepts a directory name as its argument, |
| 8 | and uses files under that directory as the baseline for the diff. |
| 9 | <li> For "gdiff", if no [/help?cmd=gdiff-command|gdiff-command setting] |
| 10 | is defined, Fossil tries to do a --tk diff if "tclsh" and "wish" |
| 11 | are available, or a --by diff if not. |
| 12 | <li> The "Reload" button is added to --tk diffs, to bring the displayed |
| 13 | diff up to date with the latest changes on disk. |
| 14 | <li> Add the "Hide diffs/Show diffs" toggle to web-UI diff pages that show |
| 15 | diffs of multiple files. |
| 16 | </ol> |
| 17 | * Added the [/help?cmd=/ckout|/ckout web page] to provide information |
| 18 | about pending changes in a working check-out |
| 19 | * Enhancements to the [/help?cmd=ui|fossil ui] command: |
| 20 | <ol type="a"> |
| 21 | <li> Defaults to using the new [/help?cmd=/ckout|/ckout page] as its |
| 22 | start page. Or, if the new "--from PATH" option is present, the |
| 23 | default start page becomes "/ckout?exbase=PATH". |
| 24 | <li> The new "--extpage FILENAME" option opens the named file as if it |
| 25 | where in a [./serverext.wiki|CGI extension]. Example usage: the |
| 26 | person editing this change log has |
| 27 | "fossil ui --extpage www/changes.wiki" running and hence can |
| 28 | press "Reload" on the web browser to view edits. |
| 29 | </ol> |
| 30 | * Enhancements to [/help?cmd=merge|fossil merge]: |
| 31 | <ol type="a"> |
| 32 | <li> Added the [/help?cmd=merge-info|fossil merge-info] command and |
| 33 | especially the --tk option to that command, to provide analysis |
| 34 | of the most recent merge or update operation. |
| 35 | <li> When a merge conflict occurs, a new section is added to the conflict |
| 36 | text that shows Fossil's suggested resolution to the conflict. |
| 37 | </ol> |
| 38 | * Enhancements to [/help?cmd=commit|fossil commit]: |
| 39 | <ol type="a"> |
| 40 | <li> If Fossil sees potential formatting mistakes (ex: bad hyperlinks) |
| 41 | in the check-in comment, it will alert the developer and give |
| 42 | him or her the opportunity to edit the comment before continuing. |
| 43 | This feature is controllable by the |
| @@ -47,18 +49,19 @@ | |
| 47 | <li> Added the ability to sign check-ins with SSH keys. |
| 48 | <li> Issue a warning if a user tries to commit on a check-in where the |
| 49 | branch has been changed. |
| 50 | <li> The interactive checkin comment prompt shows the formatting rules |
| 51 | set for that repository. |
| 52 | </ol> |
| 53 | * Deprecate the --comfmtflags and --comment-format global options and |
| 54 | no longer list them in the built-in help, but keep them working for |
| 55 | backwards compatibility. |
| 56 | Alternative TTY comment formatting can still be specified using the |
| 57 | [/help?cmd=comment-format|comment-format setting], if desired. The |
| 58 | default comment format is now called "canonical", not "legacy". |
| 59 | * Enhancements to the [/help?cmd=/timeline|/timeline page]: |
| 60 | <ol type="a"> |
| 61 | <li> Added the "ml=" ("Merge-in List") query parameter that works |
| 62 | like "rl=" ("Related List") but adds "mionly" style related |
| 63 | check-ins instead of the full "rel" style. |
| 64 | <li> For "tl=", "rl=", and "ml=", the order of the branches in the |
| @@ -84,25 +87,32 @@ | |
| 84 | collapses long runs of check-ins on the same branch into just |
| 85 | end-points. |
| 86 | <li> The p= and d= parameters an reference different check-ins, which |
| 87 | case the timeline shows those check-ins that are both ancestors |
| 88 | of p= and descendants of d=. |
| 89 | </ol> |
| 90 | * Added the [/help?cmd=/clusterlist|/clusterlist page] for analysis |
| 91 | and debugging |
| 92 | * Added the "artifact_to_json(NAME)" SQL function that returns a JSON |
| 93 | decoding of the artifact described by NAME. |
| 94 | * Improvements to the [/help?cmd=patch|fossil patch] command: |
| 95 | <ol type="a"> |
| 96 | <li> Fix a bug in "fossil patch create" that causes |
| 97 | [/help?cmd=revert|fossil revert] operations that happened |
| 98 | on individualfiles after a [/help?cmd=merge|fossil merge] |
| 99 | to be omitted from the patch. |
| 100 | <li> Added the [/help?cmd=patch|patch alias] command for managing |
| 101 | aliases for remote checkout names. |
| 102 | </ol> |
| 103 | * Enhancements to on-line help and the [/help?cmd=help|fossil help] command: |
| 104 | <ol type="a"> |
| 105 | <li> Add the ability to search the help text, either in the UI |
| 106 | (on the [/help?cmd=/search|/search page]) or from the command-line |
| 107 | (using the "[/help?cmd=search|fossil search -h PATTERN]" command.) |
| 108 | <li> Accepts an optional SUBCOMMAND argument following the |
| @@ -109,19 +119,40 @@ | |
| 109 | COMMAND argument and only shows results for the specified |
| 110 | subcommand, not the entire command. |
| 111 | <li> The -u (--usage) option shows only the command-line syntax |
| 112 | <li> The -o (--options) option shows only the command-line options |
| 113 | </ol> |
| 114 | * Added the ability to attach wiki pages to a ticket for extended |
| 115 | descriptions. |
| 116 | * Added the "hash" query parameter to the |
| 117 | [/help?cmd=/whatis|/whatis webpage]. |
| 118 | * Add a "user elevation" [/doc/trunk/www/alerts.md|subscription] |
| 119 | which alerts subscribers when an admin creates a new user or |
| 120 | adds new permissions to one. |
| 121 | * Diverse minor fixes and additions. |
| 122 | |
| 123 | |
| 124 | <h2 id='v2_25'>Changes for version 2.25 (2024-11-06)</h2> |
| 125 | |
| 126 | * The "[/help?cmd=ui|fossil ui /]" command now works even for repositories |
| 127 | that have non-ASCII filenames |
| 128 |
| --- www/changes.wiki | |
| +++ www/changes.wiki | |
| @@ -1,43 +1,45 @@ | |
| 1 | <title>Change Log</title> |
| 2 | |
| 3 | <h2 id='v2_26'>Changes for version 2.26 (pending)</h2><ol> |
| 4 | <li>Enhancements to [/help?cmd=diff|fossil diff] and similar: |
| 5 | <ol type="a"> |
| 6 | <li> The --from can optionally accept a directory name as its argument, |
| 7 | and uses files under that directory as the baseline for the diff. |
| 8 | <li> For "gdiff", if no [/help?cmd=gdiff-command|gdiff-command setting] |
| 9 | is defined, Fossil tries to do a --tk diff if "tclsh" and "wish" |
| 10 | are available, or a --by diff if not. |
| 11 | <li> The "Reload" button is added to --tk diffs, to bring the displayed |
| 12 | diff up to date with the latest changes on disk. |
| 13 | <li> Add the "Hide diffs/Show diffs" toggle to web-UI diff pages that show |
| 14 | diffs of multiple files. |
| 15 | </ol> |
| 16 | <li>Added the [/help?cmd=/ckout|/ckout web page] to provide information |
| 17 | about pending changes in a working check-out |
| 18 | <li>Enhancements to the [/help?cmd=ui|fossil ui] command: |
| 19 | <ol type="a"> |
| 20 | <li> Defaults to using the new [/help?cmd=/ckout|/ckout page] as its |
| 21 | start page. Or, if the new "--from PATH" option is present, the |
| 22 | default start page becomes "/ckout?exbase=PATH". |
| 23 | <li> The new "--extpage FILENAME" option opens the named file as if it |
| 24 | where in a [./serverext.wiki|CGI extension]. Example usage: the |
| 25 | person editing this change log has |
| 26 | "fossil ui --extpage www/changes.wiki" running and hence can |
| 27 | press "Reload" on the web browser to view edits. |
| 28 | <li> Accept both IPv4 and IPv6 connections on all platforms, including |
| 29 | Windows and OpenBSD. This also applies to the "fossil server" |
| 30 | command. |
| 31 | </ol> |
| 32 | <li>Enhancements to [/help?cmd=merge|fossil merge]: |
| 33 | <ol type="a"> |
| 34 | <li> Added the [/help?cmd=merge-info|fossil merge-info] command and |
| 35 | especially the --tk option to that command, to provide analysis |
| 36 | of the most recent merge or update operation. |
| 37 | <li> When a merge conflict occurs, a new section is added to the conflict |
| 38 | text that shows Fossil's suggested resolution to the conflict. |
| 39 | </ol> |
| 40 | <li>Enhancements to [/help?cmd=commit|fossil commit]: |
| 41 | <ol type="a"> |
| 42 | <li> If Fossil sees potential formatting mistakes (ex: bad hyperlinks) |
| 43 | in the check-in comment, it will alert the developer and give |
| 44 | him or her the opportunity to edit the comment before continuing. |
| 45 | This feature is controllable by the |
| @@ -47,18 +49,19 @@ | |
| 49 | <li> Added the ability to sign check-ins with SSH keys. |
| 50 | <li> Issue a warning if a user tries to commit on a check-in where the |
| 51 | branch has been changed. |
| 52 | <li> The interactive checkin comment prompt shows the formatting rules |
| 53 | set for that repository. |
| 54 | <li> Add the "--editor" option. |
| 55 | </ol> |
| 56 | <li>Deprecate the --comfmtflags and --comment-format global options and |
| 57 | no longer list them in the built-in help, but keep them working for |
| 58 | backwards compatibility. |
| 59 | Alternative TTY comment formatting can still be specified using the |
| 60 | [/help?cmd=comment-format|comment-format setting], if desired. The |
| 61 | default comment format is now called "canonical", not "legacy". |
| 62 | <li>Enhancements to the [/help?cmd=/timeline|/timeline page]: |
| 63 | <ol type="a"> |
| 64 | <li> Added the "ml=" ("Merge-in List") query parameter that works |
| 65 | like "rl=" ("Related List") but adds "mionly" style related |
| 66 | check-ins instead of the full "rel" style. |
| 67 | <li> For "tl=", "rl=", and "ml=", the order of the branches in the |
| @@ -84,25 +87,32 @@ | |
| 87 | collapses long runs of check-ins on the same branch into just |
| 88 | end-points. |
| 89 | <li> The p= and d= parameters an reference different check-ins, which |
| 90 | case the timeline shows those check-ins that are both ancestors |
| 91 | of p= and descendants of d=. |
| 92 | <li> The saturation and intensity of user-specified checkin and branch |
| 93 | background colors are automatically adjusted to keep the colors |
| 94 | compatible with the current skin, unless the |
| 95 | [/help?cmd=raw-bgcolor|raw-bgcolor setting] is turned on. |
| 96 | </ol> |
| 97 | <li>The [/help?cmd=/docfile|/docfile webpage] was added. It works like |
| 98 | /doc but keeps the title of markdown documents with the document rather |
| 99 | that moving it up to the page title. |
| 100 | <li>Added the [/help?cmd=/clusterlist|/clusterlist page] for analysis |
| 101 | and debugging |
| 102 | <li>Added the "artifact_to_json(NAME)" SQL function that returns a JSON |
| 103 | decoding of the artifact described by NAME. |
| 104 | <li>Improvements to the [/help?cmd=patch|fossil patch] command: |
| 105 | <ol type="a"> |
| 106 | <li> Fix a bug in "fossil patch create" that causes |
| 107 | [/help?cmd=revert|fossil revert] operations that happened |
| 108 | on individualfiles after a [/help?cmd=merge|fossil merge] |
| 109 | to be omitted from the patch. |
| 110 | <li> Added the [/help?cmd=patch|patch alias] command for managing |
| 111 | aliases for remote checkout names. |
| 112 | </ol> |
| 113 | <li>Enhancements to on-line help and the [/help?cmd=help|fossil help] command: |
| 114 | <ol type="a"> |
| 115 | <li> Add the ability to search the help text, either in the UI |
| 116 | (on the [/help?cmd=/search|/search page]) or from the command-line |
| 117 | (using the "[/help?cmd=search|fossil search -h PATTERN]" command.) |
| 118 | <li> Accepts an optional SUBCOMMAND argument following the |
| @@ -109,19 +119,40 @@ | |
| 119 | COMMAND argument and only shows results for the specified |
| 120 | subcommand, not the entire command. |
| 121 | <li> The -u (--usage) option shows only the command-line syntax |
| 122 | <li> The -o (--options) option shows only the command-line options |
| 123 | </ol> |
| 124 | <li>Enhancements to the ticket system: |
| 125 | <ol type="a"> |
| 126 | <li> Added the ability to attach wiki pages to a ticket for extended |
| 127 | descriptions. |
| 128 | <li> Added submenu to the 'View Ticket' page, to use it as |
| 129 | template for a new ticket. |
| 130 | <li> Added button 'Submit and New' to create multiple tickets |
| 131 | in a row. |
| 132 | <li> Link the version field in ticket view to a matching checkin or tag. |
| 133 | <li> Show creation time in report and ticket view. |
| 134 | <li> Show previous comments in edit ticket as reference. |
| 135 | </ol> |
| 136 | <li>Added the "hash" query parameter to the |
| 137 | [/help?cmd=/whatis|/whatis webpage]. |
| 138 | <li>Add a "user permissions changes" [/doc/trunk/www/alerts.md|subscription] |
| 139 | which alerts subscribers when an admin creates a new user or |
| 140 | when a user's permissions change. |
| 141 | <li>Show project description on repository list. |
| 142 | <li>Make [/help?cmd=/chat|/chat] better-behaved during server outages, reducing |
| 143 | the frequency of reconnection attempts over time and providing feedback |
| 144 | to the user when the connection is down. |
| 145 | <li>The [/doc/trunk/www/th1.md|TH1 script language] now makes a distinction |
| 146 | between [/doc/trunk/www/th1.md#taint|tainted and untainted string values]. |
| 147 | This is a security enhancement that makes it more difficult to write |
| 148 | custom TH1 scripts that contain XSS or SQL-injection bugs. As part of |
| 149 | this enhancement, the [/help?cmd=vuln-report|vuln-report] setting was |
| 150 | added to control what Fossil does when it encounters a potential TH1 |
| 151 | security problem. |
| 152 | <li>Diverse minor fixes and additions. |
| 153 | </ol> |
| 154 | |
| 155 | <h2 id='v2_25'>Changes for version 2.25 (2024-11-06)</h2> |
| 156 | |
| 157 | * The "[/help?cmd=ui|fossil ui /]" command now works even for repositories |
| 158 | that have non-ASCII filenames |
| 159 |
+18
| --- www/chat.md | ||
| +++ www/chat.md | ||
| @@ -164,10 +164,28 @@ | ||
| 164 | 164 | setting. |
| 165 | 165 | |
| 166 | 166 | This mechanism is similar to [email notification](./alerts.md) except that |
| 167 | 167 | the notification is sent via chat instead of via email. |
| 168 | 168 | |
| 169 | +## Quirks | |
| 170 | + | |
| 171 | + - There is no message-editing capability. This is by design and | |
| 172 | + desire of `/chat`'s developers. | |
| 173 | + | |
| 174 | + - When `/chat` has problems connecting to the message poller (see | |
| 175 | + the next section) it will provide a subtle visual indicator of the | |
| 176 | + connection problem - a dotted line along the top of the input | |
| 177 | + field. If the connection recovers within a short time, that | |
| 178 | + indicator will go away, otherwise it will pop up a loud message | |
| 179 | + signifying that the connection to the poller is down and how long | |
| 180 | + it will wait to retry (progressively longer, up to some | |
| 181 | + maximum). `/chat` will recover automatically when the server is | |
| 182 | + reachable. Trying to send messages while the poller connection is | |
| 183 | + down is permitted, and the poller will attempt to recover | |
| 184 | + immediately if sending of a message succeeds. That applies to any | |
| 185 | + operations which send traffic, e.g. if the per-message "toggle | |
| 186 | + text mode" button is activated or a message is globally deleted. | |
| 169 | 187 | |
| 170 | 188 | ## Implementation Details |
| 171 | 189 | |
| 172 | 190 | *You do not need to understand how Fossil chat works in order to use it. |
| 173 | 191 | But many developers prefer to know how their tools work. |
| 174 | 192 |
| --- www/chat.md | |
| +++ www/chat.md | |
| @@ -164,10 +164,28 @@ | |
| 164 | setting. |
| 165 | |
| 166 | This mechanism is similar to [email notification](./alerts.md) except that |
| 167 | the notification is sent via chat instead of via email. |
| 168 | |
| 169 | |
| 170 | ## Implementation Details |
| 171 | |
| 172 | *You do not need to understand how Fossil chat works in order to use it. |
| 173 | But many developers prefer to know how their tools work. |
| 174 |
| --- www/chat.md | |
| +++ www/chat.md | |
| @@ -164,10 +164,28 @@ | |
| 164 | setting. |
| 165 | |
| 166 | This mechanism is similar to [email notification](./alerts.md) except that |
| 167 | the notification is sent via chat instead of via email. |
| 168 | |
| 169 | ## Quirks |
| 170 | |
| 171 | - There is no message-editing capability. This is by design and |
| 172 | desire of `/chat`'s developers. |
| 173 | |
| 174 | - When `/chat` has problems connecting to the message poller (see |
| 175 | the next section) it will provide a subtle visual indicator of the |
| 176 | connection problem - a dotted line along the top of the input |
| 177 | field. If the connection recovers within a short time, that |
| 178 | indicator will go away, otherwise it will pop up a loud message |
| 179 | signifying that the connection to the poller is down and how long |
| 180 | it will wait to retry (progressively longer, up to some |
| 181 | maximum). `/chat` will recover automatically when the server is |
| 182 | reachable. Trying to send messages while the poller connection is |
| 183 | down is permitted, and the poller will attempt to recover |
| 184 | immediately if sending of a message succeeds. That applies to any |
| 185 | operations which send traffic, e.g. if the per-message "toggle |
| 186 | text mode" button is activated or a message is globally deleted. |
| 187 | |
| 188 | ## Implementation Details |
| 189 | |
| 190 | *You do not need to understand how Fossil chat works in order to use it. |
| 191 | But many developers prefer to know how their tools work. |
| 192 |
+4
-3
| --- www/env-opts.md | ||
| +++ www/env-opts.md | ||
| @@ -154,13 +154,14 @@ | ||
| 154 | 154 | `FOSSIL_REPOLIST_TITLE`: The page title of the "Repository List" page |
| 155 | 155 | loaded by the `fossil all ui` or `fossil ui /` commands. Only used if |
| 156 | 156 | none of the listed repositories has the `repolist_skin` property set. |
| 157 | 157 | Can be set from the [CGI control file][cgictlfile]. |
| 158 | 158 | |
| 159 | -`FOSSIL_REPOLIST_QUICKFILTER`: Enable or disable the quickfilter on | |
| 160 | -repository listings, which allows for simple filtering of the listed | |
| 161 | -repositories. | |
| 159 | +`FOSSIL_REPOLIST_SHOW`: If this variable exists and has a text value | |
| 160 | +that contains the substring "description", then the "Project Description" | |
| 161 | +column appears on the repolist page. If it contains the substring | |
| 162 | +"login-group", then the Login-Group column appears on the repolist page. | |
| 162 | 163 | Can be set from the [CGI control file][cgictlfile]. |
| 163 | 164 | |
| 164 | 165 | `FOSSIL_USE_SEE_TEXTKEY`: If set, treat the encryption key string for |
| 165 | 166 | SEE as text to be hashed into the actual encryption key. This has no |
| 166 | 167 | effect if Fossil was not compiled with SEE support enabled. |
| 167 | 168 |
| --- www/env-opts.md | |
| +++ www/env-opts.md | |
| @@ -154,13 +154,14 @@ | |
| 154 | `FOSSIL_REPOLIST_TITLE`: The page title of the "Repository List" page |
| 155 | loaded by the `fossil all ui` or `fossil ui /` commands. Only used if |
| 156 | none of the listed repositories has the `repolist_skin` property set. |
| 157 | Can be set from the [CGI control file][cgictlfile]. |
| 158 | |
| 159 | `FOSSIL_REPOLIST_QUICKFILTER`: Enable or disable the quickfilter on |
| 160 | repository listings, which allows for simple filtering of the listed |
| 161 | repositories. |
| 162 | Can be set from the [CGI control file][cgictlfile]. |
| 163 | |
| 164 | `FOSSIL_USE_SEE_TEXTKEY`: If set, treat the encryption key string for |
| 165 | SEE as text to be hashed into the actual encryption key. This has no |
| 166 | effect if Fossil was not compiled with SEE support enabled. |
| 167 |
| --- www/env-opts.md | |
| +++ www/env-opts.md | |
| @@ -154,13 +154,14 @@ | |
| 154 | `FOSSIL_REPOLIST_TITLE`: The page title of the "Repository List" page |
| 155 | loaded by the `fossil all ui` or `fossil ui /` commands. Only used if |
| 156 | none of the listed repositories has the `repolist_skin` property set. |
| 157 | Can be set from the [CGI control file][cgictlfile]. |
| 158 | |
| 159 | `FOSSIL_REPOLIST_SHOW`: If this variable exists and has a text value |
| 160 | that contains the substring "description", then the "Project Description" |
| 161 | column appears on the repolist page. If it contains the substring |
| 162 | "login-group", then the Login-Group column appears on the repolist page. |
| 163 | Can be set from the [CGI control file][cgictlfile]. |
| 164 | |
| 165 | `FOSSIL_USE_SEE_TEXTKEY`: If set, treat the encryption key string for |
| 166 | SEE as text to be hashed into the actual encryption key. This has no |
| 167 | effect if Fossil was not compiled with SEE support enabled. |
| 168 |
+4
-3
| --- www/env-opts.md | ||
| +++ www/env-opts.md | ||
| @@ -154,13 +154,14 @@ | ||
| 154 | 154 | `FOSSIL_REPOLIST_TITLE`: The page title of the "Repository List" page |
| 155 | 155 | loaded by the `fossil all ui` or `fossil ui /` commands. Only used if |
| 156 | 156 | none of the listed repositories has the `repolist_skin` property set. |
| 157 | 157 | Can be set from the [CGI control file][cgictlfile]. |
| 158 | 158 | |
| 159 | -`FOSSIL_REPOLIST_QUICKFILTER`: Enable or disable the quickfilter on | |
| 160 | -repository listings, which allows for simple filtering of the listed | |
| 161 | -repositories. | |
| 159 | +`FOSSIL_REPOLIST_SHOW`: If this variable exists and has a text value | |
| 160 | +that contains the substring "description", then the "Project Description" | |
| 161 | +column appears on the repolist page. If it contains the substring | |
| 162 | +"login-group", then the Login-Group column appears on the repolist page. | |
| 162 | 163 | Can be set from the [CGI control file][cgictlfile]. |
| 163 | 164 | |
| 164 | 165 | `FOSSIL_USE_SEE_TEXTKEY`: If set, treat the encryption key string for |
| 165 | 166 | SEE as text to be hashed into the actual encryption key. This has no |
| 166 | 167 | effect if Fossil was not compiled with SEE support enabled. |
| 167 | 168 |
| --- www/env-opts.md | |
| +++ www/env-opts.md | |
| @@ -154,13 +154,14 @@ | |
| 154 | `FOSSIL_REPOLIST_TITLE`: The page title of the "Repository List" page |
| 155 | loaded by the `fossil all ui` or `fossil ui /` commands. Only used if |
| 156 | none of the listed repositories has the `repolist_skin` property set. |
| 157 | Can be set from the [CGI control file][cgictlfile]. |
| 158 | |
| 159 | `FOSSIL_REPOLIST_QUICKFILTER`: Enable or disable the quickfilter on |
| 160 | repository listings, which allows for simple filtering of the listed |
| 161 | repositories. |
| 162 | Can be set from the [CGI control file][cgictlfile]. |
| 163 | |
| 164 | `FOSSIL_USE_SEE_TEXTKEY`: If set, treat the encryption key string for |
| 165 | SEE as text to be hashed into the actual encryption key. This has no |
| 166 | effect if Fossil was not compiled with SEE support enabled. |
| 167 |
| --- www/env-opts.md | |
| +++ www/env-opts.md | |
| @@ -154,13 +154,14 @@ | |
| 154 | `FOSSIL_REPOLIST_TITLE`: The page title of the "Repository List" page |
| 155 | loaded by the `fossil all ui` or `fossil ui /` commands. Only used if |
| 156 | none of the listed repositories has the `repolist_skin` property set. |
| 157 | Can be set from the [CGI control file][cgictlfile]. |
| 158 | |
| 159 | `FOSSIL_REPOLIST_SHOW`: If this variable exists and has a text value |
| 160 | that contains the substring "description", then the "Project Description" |
| 161 | column appears on the repolist page. If it contains the substring |
| 162 | "login-group", then the Login-Group column appears on the repolist page. |
| 163 | Can be set from the [CGI control file][cgictlfile]. |
| 164 | |
| 165 | `FOSSIL_USE_SEE_TEXTKEY`: If set, treat the encryption key string for |
| 166 | SEE as text to be hashed into the actual encryption key. This has no |
| 167 | effect if Fossil was not compiled with SEE support enabled. |
| 168 |
+2
-2
| --- www/loadmgmt.md | ||
| +++ www/loadmgmt.md | ||
| @@ -77,11 +77,11 @@ | ||
| 77 | 77 | |
| 78 | 78 | The `/home/www/proc` pathname should be adjusted so that the `/proc` |
| 79 | 79 | component is at the root of the chroot jail, of course. |
| 80 | 80 | |
| 81 | 81 | To see if the load-average limiter is functional, visit the |
| 82 | -[`/test_env`][hte] page of the server to view the current load average. | |
| 82 | +[`/test-env`][hte] page of the server to view the current load average. | |
| 83 | 83 | If the value for the load average is greater than zero, that means that |
| 84 | 84 | it is possible to activate the load-average limiter on that repository. |
| 85 | 85 | If the load average shows exactly "0.0", then that means that Fossil is |
| 86 | 86 | unable to find the load average. This can either be because it is in a |
| 87 | 87 | `chroot(2)` jail without `/proc` access, or because it is running on a |
| @@ -88,9 +88,9 @@ | ||
| 88 | 88 | system that does not support `getloadavg()` and so the load-average |
| 89 | 89 | limiter will not function. |
| 90 | 90 | |
| 91 | 91 | |
| 92 | 92 | [503]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.5.4 |
| 93 | -[hte]: /help?cmd=/test_env | |
| 93 | +[hte]: /help?cmd=/test-env | |
| 94 | 94 | [gla]: https://linux.die.net/man/3/getloadavg |
| 95 | 95 | [lin]: http://www.linode.com |
| 96 | 96 | [sh]: ./selfhost.wiki |
| 97 | 97 |
| --- www/loadmgmt.md | |
| +++ www/loadmgmt.md | |
| @@ -77,11 +77,11 @@ | |
| 77 | |
| 78 | The `/home/www/proc` pathname should be adjusted so that the `/proc` |
| 79 | component is at the root of the chroot jail, of course. |
| 80 | |
| 81 | To see if the load-average limiter is functional, visit the |
| 82 | [`/test_env`][hte] page of the server to view the current load average. |
| 83 | If the value for the load average is greater than zero, that means that |
| 84 | it is possible to activate the load-average limiter on that repository. |
| 85 | If the load average shows exactly "0.0", then that means that Fossil is |
| 86 | unable to find the load average. This can either be because it is in a |
| 87 | `chroot(2)` jail without `/proc` access, or because it is running on a |
| @@ -88,9 +88,9 @@ | |
| 88 | system that does not support `getloadavg()` and so the load-average |
| 89 | limiter will not function. |
| 90 | |
| 91 | |
| 92 | [503]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.5.4 |
| 93 | [hte]: /help?cmd=/test_env |
| 94 | [gla]: https://linux.die.net/man/3/getloadavg |
| 95 | [lin]: http://www.linode.com |
| 96 | [sh]: ./selfhost.wiki |
| 97 |
| --- www/loadmgmt.md | |
| +++ www/loadmgmt.md | |
| @@ -77,11 +77,11 @@ | |
| 77 | |
| 78 | The `/home/www/proc` pathname should be adjusted so that the `/proc` |
| 79 | component is at the root of the chroot jail, of course. |
| 80 | |
| 81 | To see if the load-average limiter is functional, visit the |
| 82 | [`/test-env`][hte] page of the server to view the current load average. |
| 83 | If the value for the load average is greater than zero, that means that |
| 84 | it is possible to activate the load-average limiter on that repository. |
| 85 | If the load average shows exactly "0.0", then that means that Fossil is |
| 86 | unable to find the load average. This can either be because it is in a |
| 87 | `chroot(2)` jail without `/proc` access, or because it is running on a |
| @@ -88,9 +88,9 @@ | |
| 88 | system that does not support `getloadavg()` and so the load-average |
| 89 | limiter will not function. |
| 90 | |
| 91 | |
| 92 | [503]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.5.4 |
| 93 | [hte]: /help?cmd=/test-env |
| 94 | [gla]: https://linux.die.net/man/3/getloadavg |
| 95 | [lin]: http://www.linode.com |
| 96 | [sh]: ./selfhost.wiki |
| 97 |
+106
-74
| --- www/quickstart.wiki | ||
| +++ www/quickstart.wiki | ||
| @@ -14,20 +14,18 @@ | ||
| 14 | 14 | someplace on your $PATH. |
| 15 | 15 | |
| 16 | 16 | You can test that Fossil is present and working like this: |
| 17 | 17 | |
| 18 | 18 | <pre><b>fossil version |
| 19 | -This is fossil version 2.13 [309af345ab] 2020-09-28 04:02:55 UTC | |
| 19 | +This is fossil version 2.25 [8f798279d5] 2024-11-06 12:59:09 UTC | |
| 20 | 20 | </b></pre> |
| 21 | 21 | |
| 22 | 22 | <h2 id="workflow" name="fslclone">General Work Flow</h2> |
| 23 | 23 | |
| 24 | -Fossil works with repository files (a database in a single file with the project's | |
| 25 | -complete history) and with checked-out local trees (the working directory | |
| 26 | -you use to do your work). | |
| 27 | -(See [./glossary.md | the glossary] for more background.) | |
| 28 | -The workflow looks like this: | |
| 24 | +Fossil works with [./glossary.md#repository | repository files] | |
| 25 | +and [./glossary.md#check-out | check-out directories] using a | |
| 26 | +workflow like this: | |
| 29 | 27 | |
| 30 | 28 | <ul> |
| 31 | 29 | <li>Create or clone a repository file. ([/help/init|fossil init] or |
| 32 | 30 | [/help/clone | fossil clone]) |
| 33 | 31 | <li>Check out a local tree. ([/help/open | fossil open]) |
| @@ -41,29 +39,54 @@ | ||
| 41 | 39 | The following sections give a brief overview of these |
| 42 | 40 | operations. |
| 43 | 41 | |
| 44 | 42 | <h2 id="new">Starting A New Project</h2> |
| 45 | 43 | |
| 46 | -To start a new project with fossil create a new empty repository | |
| 47 | -this way: ([/help/init | more info]) | |
| 44 | +To start a new project with Fossil, [/help/init | create a new empty repository]: | |
| 48 | 45 | |
| 49 | 46 | <pre><b>fossil init</b> <i>repository-filename</i> |
| 50 | 47 | </pre> |
| 51 | 48 | |
| 52 | 49 | You can name the database anything you like, and you can place it anywhere in the filesystem. |
| 53 | -The <tt>.fossil</tt> extension is traditional but only required if you are going to use the | |
| 54 | -<tt>[/help/server | fossil server DIRECTORY]</tt> feature.” | |
| 50 | +The <tt>.fossil</tt> extension is traditional, but it is only required if you are going to use the | |
| 51 | +<tt>[/help/server | fossil server DIRECTORY]</tt> feature. | |
| 52 | + | |
| 53 | +Next, do something along the lines of: | |
| 54 | + | |
| 55 | +<pre> | |
| 56 | +<b>mkdir -p ~/src/project/trunk</b> | |
| 57 | +<b>cd ~/src/project/trunk</b> | |
| 58 | +<b>fossil open</b> <i>repository-filename</i> | |
| 59 | +<b>fossil add</b> foo.c bar.h qux.md | |
| 60 | +<b>fossil commit</b> | |
| 61 | +</pre> | |
| 62 | + | |
| 63 | +If your project directory already exists, obviating the <b>mkdir</b> | |
| 64 | +step, you will instead need to add the <tt>--force</tt> flag to the | |
| 65 | +<b>open</b> command to authorize Fossil to open the repo into a | |
| 66 | +non-empty checkout directory. (This is to avoid accidental opens into, | |
| 67 | +for example, your home directory.) | |
| 68 | + | |
| 69 | +The convention of naming your checkout directory after a long-lived | |
| 70 | +branch name like "trunk" is in support of Fossil's ability to have as | |
| 71 | +many open checkouts as you like. This author frequently has additional | |
| 72 | +checkout directories named <tt>../release</tt>, <tt>../scratch</tt>, | |
| 73 | +etc. The release directory is open to the branch of the same name, while | |
| 74 | +the scratch directory is used when disturbing one of the other | |
| 75 | +long-lived checkout directories is undesireable, as when performing a | |
| 76 | +[/help/bisect | bisect] operation. | |
| 77 | + | |
| 55 | 78 | |
| 56 | 79 | <h2 id="clone">Cloning An Existing Repository</h2> |
| 57 | 80 | |
| 58 | 81 | Most fossil operations interact with a repository that is on the |
| 59 | 82 | local disk drive, not on a remote system. Hence, before accessing |
| 60 | 83 | a remote repository it is necessary to make a local copy of that |
| 61 | -repository. Making a local copy of a remote repository is called | |
| 62 | -"cloning". | |
| 84 | +repository, a process called | |
| 85 | +"[/help/clone | cloning]". | |
| 63 | 86 | |
| 64 | -Clone a remote repository as follows: ([/help/clone | more info]) | |
| 87 | +This is done as follows: | |
| 65 | 88 | |
| 66 | 89 | <pre><b>fossil clone</b> <i>URL repository-filename</i> |
| 67 | 90 | </pre> |
| 68 | 91 | |
| 69 | 92 | The <i>URL</i> specifies the fossil repository |
| @@ -81,12 +104,20 @@ | ||
| 81 | 104 | 100% complete... |
| 82 | 105 | Extra delta compression... |
| 83 | 106 | Vacuuming the database... |
| 84 | 107 | project-id: 94259BB9F186226D80E49D1FA2DB29F935CCA0333 |
| 85 | 108 | server-id: 016595e9043054038a9ea9bc526d7f33f7ac0e42 |
| 86 | -admin-user: exampleuser (password is "yoWgDR42iv")> | |
| 109 | +admin-user: exampleuser (intial remote-access password is "yoWgDR42iv")> | |
| 87 | 110 | </b></pre> |
| 111 | + | |
| 112 | +This <i>exampleuser</i> will be used by Fossil as the author of commits when | |
| 113 | +you checkin changes to the repository. It is also used by Fossil when you | |
| 114 | +make your repository available to others using the built-in server mode by | |
| 115 | +running <tt>[/help/server | fossil server]</tt> and will also be used when | |
| 116 | +running <tt>[/help/ui | fossil ui]</tt> to view the repository through | |
| 117 | +the Fossil UI. See the quick start topic for setting up a | |
| 118 | +<a href="#server">server</a> for more details. | |
| 88 | 119 | |
| 89 | 120 | If the remote repository requires a login, include a |
| 90 | 121 | userid in the URL like this: |
| 91 | 122 | |
| 92 | 123 | <pre><b>fossil clone https://</b><i>remoteuserid</i><b>@www.example.org/ myclone.fossil</b></pre> |
| @@ -127,26 +158,23 @@ | ||
| 127 | 158 | |
| 128 | 159 | <h2 id="checkout">Checking Out A Local Tree</h2> |
| 129 | 160 | |
| 130 | 161 | To work on a project in fossil, you need to check out a local |
| 131 | 162 | copy of the source tree. Create the directory you want to be |
| 132 | -the root of your tree and cd into that directory. Then | |
| 133 | -do this: ([/help/open | more info]) | |
| 163 | +the root of your tree, <tt>cd</tt> into that directory, and then: | |
| 134 | 164 | |
| 135 | 165 | <pre><b>fossil open</b> <i>repository-filename</i></pre> |
| 136 | 166 | |
| 137 | -for example: | |
| 167 | +For example: | |
| 138 | 168 | |
| 139 | 169 | <pre><b>fossil open ../myclone.fossil |
| 140 | 170 | BUILD.txt |
| 141 | 171 | COPYRIGHT-BSD2.txt |
| 142 | 172 | README.md |
| 143 | 173 | ︙ |
| 144 | 174 | </tt></b></pre> |
| 145 | 175 | |
| 146 | -(or "fossil open ..\myclone.fossil" on Windows). | |
| 147 | - | |
| 148 | 176 | This leaves you with the newest version of the tree |
| 149 | 177 | checked out. |
| 150 | 178 | From anywhere underneath the root of your local tree, you |
| 151 | 179 | can type commands like the following to find out the status of |
| 152 | 180 | your local tree: |
| @@ -294,41 +322,60 @@ | ||
| 294 | 322 | |
| 295 | 323 | This will get you started on identifying checkins. The |
| 296 | 324 | <a href="./checkin_names.wiki">Checkin Names document</a> is a complete reference, including |
| 297 | 325 | how timestamps can also be used. |
| 298 | 326 | |
| 299 | -<h2 id="config">Configuring Your Local Repository</h2> | |
| 327 | +<h2 id="config">Accessing Your Local Repository's Web User Interface</h2> | |
| 300 | 328 | |
| 301 | -When you create a new repository, either by cloning an existing | |
| 302 | -project or create a new project of your own, you usually want to do some | |
| 303 | -local configuration. This is easily accomplished using the web-server | |
| 304 | -that is built into fossil. Start the fossil web server like this: | |
| 305 | -([/help/ui | more info]) | |
| 329 | +After you create a new repository, you usually want to do some local | |
| 330 | +configuration. This is most easily accomplished by firing up the Fossil | |
| 331 | +UI: | |
| 306 | 332 | |
| 307 | 333 | <pre> |
| 308 | 334 | <b>fossil ui</b> <i>repository-filename</i> |
| 309 | 335 | </pre> |
| 310 | 336 | |
| 311 | -You can omit the <i>repository-filename</i> from the command above | |
| 337 | +You can shorten that to just [/help/ui | <b>fossil ui</b>] | |
| 312 | 338 | if you are inside a checked-out local tree. |
| 313 | 339 | |
| 314 | -This starts a web server then automatically launches your | |
| 315 | -web browser and makes it point to this web server. If your system | |
| 316 | -has an unusual configuration, fossil might not be able to figure out | |
| 317 | -how to start your web browser. In that case, first tell fossil | |
| 318 | -where to find your web browser using a command like this: | |
| 340 | +This command starts an internal web server, after which Fossil | |
| 341 | +automatically launches your default browser, pointed at itself, | |
| 342 | +presenting a special view of the repository, its web user interface. | |
| 343 | + | |
| 344 | +You may override Fossil's logic for selecting the default browser so: | |
| 319 | 345 | |
| 320 | 346 | <pre> |
| 321 | 347 | <b>fossil setting web-browser</b> <i>path-to-web-browser</i> |
| 322 | 348 | </pre> |
| 323 | 349 | |
| 324 | -By default, fossil does not require a login for HTTP connections | |
| 325 | -coming in from the IP loopback address 127.0.0.1. You can, and perhaps | |
| 326 | -should, change this after you create a few users. | |
| 350 | +When launched this way, Fossil binds its internal web server to the IP | |
| 351 | +loopback address, 127.0.0.1, which it treats specially, bypassing all | |
| 352 | +user controls, effectively giving visitors the | |
| 353 | +[./caps/admin-v-setup.md#apsu | all-powerful Setup capabliity]. | |
| 354 | + | |
| 355 | +Why is that a good idea, you ask? Because it is a safe | |
| 356 | +presumption that only someone with direct file access to the repository | |
| 357 | +database file could be using the resulting web interface. Anyone who can | |
| 358 | +modify the repo DB directly could give themselves any and all access | |
| 359 | +with a SQL query, or even by direct file manipulation; no amount of | |
| 360 | +access control matters to such a user. | |
| 361 | + | |
| 362 | +(Contrast the [#server | many <i>other</i> ways] of setting Fossil up | |
| 363 | +as an HTTP server, where the repo DB is on the other side of the HTTP | |
| 364 | +server wall, inaccessible by all means other than Fossil's own | |
| 365 | +mediation. For this reason, the "localhost bypasses access control" | |
| 366 | +policy does <i>not</i> apply to these other interfaces. That is a very | |
| 367 | +good thing, since without this difference in policy, it would be unsafe | |
| 368 | +to bind a [/help?cmd=server | <b>fossil server</b>] instance to | |
| 369 | +localhost on a high-numbered port and then reverse-proxy it out to the | |
| 370 | +world via HTTPS, a practice this author does engage in, with confidence.) | |
| 327 | 371 | |
| 328 | -When you are finished configuring, just press Control-C or use | |
| 329 | -the <b>kill</b> command to shut down the mini-server. | |
| 372 | +Once you are finished configuring Fossil, you may safely Control-C out | |
| 373 | +of the <b>fossil ui</b> command to shut down this privileged | |
| 374 | +built-in web server. Moreover, you may by grace of SQLite do this <i>at | |
| 375 | +any time</i>: all changes are either committed durably to the repo DB or | |
| 376 | +rolled back, in their totality. This includes configuration changes. | |
| 330 | 377 | |
| 331 | 378 | <h2 id="sharing">Sharing Changes</h2> |
| 332 | 379 | |
| 333 | 380 | When [./concepts.wiki#workflow|autosync] is turned off, |
| 334 | 381 | the changes you [/help/commit | commit] are only |
| @@ -384,16 +431,12 @@ | ||
| 384 | 431 | them and fails if local changes exist unless the <tt>--force</tt> |
| 385 | 432 | flag is used. |
| 386 | 433 | |
| 387 | 434 | <h2 id="branch" name="merge">Branching And Merging</h2> |
| 388 | 435 | |
| 389 | -Use the --branch option to the [/help/commit | commit] command | |
| 390 | -to start a new branch. Note that in Fossil, branches are normally | |
| 391 | -created when you commit, not before you start editing. You can | |
| 392 | -use the [/help/branch | branch new] command to create a new branch | |
| 393 | -before you start editing, if you want, but most people just wait | |
| 394 | -until they are ready to commit. | |
| 436 | +Use the --branch option to the [/help/commit | commit] command to start | |
| 437 | +a new branch at the point of need. ([./gitusers.md#bneed | Contrast git].) | |
| 395 | 438 | |
| 396 | 439 | To merge two branches back together, first |
| 397 | 440 | [/help/update | update] to the branch you want to merge into. |
| 398 | 441 | Then do a [/help/merge|merge] of the other branch that you want to incorporate |
| 399 | 442 | the changes from. For example, to merge "featureX" changes into "trunk" |
| @@ -442,55 +485,44 @@ | ||
| 442 | 485 | level of undo/redo. |
| 443 | 486 | |
| 444 | 487 | |
| 445 | 488 | <h2 id="server">Setting Up A Server</h2> |
| 446 | 489 | |
| 447 | -Fossil can act as a stand-alone web server using one of these | |
| 448 | -commands: | |
| 490 | +In addition to the inward-facing <b>fossil ui</b> mode covered [#config | |
| 491 | +| above], Fossil can also act as an outward-facing web server: | |
| 449 | 492 | |
| 450 | 493 | <pre> |
| 451 | 494 | <b>[/help/server | fossil server]</b> <i>repository-filename</i> |
| 452 | -<b>[/help/ui | fossil ui]</b> <i>repository-filename</i> | |
| 453 | 495 | </pre> |
| 454 | 496 | |
| 455 | -The <i>repository-filename</i> can be omitted when these commands | |
| 456 | -are run from within an open check-out, which is a particularly useful | |
| 457 | -shortcut with the <b>fossil ui</b> command. | |
| 458 | - | |
| 459 | -The <b>ui</b> command is intended for accessing the web user interface | |
| 460 | -from a local desktop. (We sometimes call this mode "Fossil UI.") | |
| 461 | -The <b>ui</b> command differs from the | |
| 462 | -<b>server</b> command by binding to the loopback IP | |
| 463 | -address only (thus making the web UI visible only on the | |
| 464 | -local machine) and by automatically starting your default web browser, | |
| 465 | -pointing it at the running UI | |
| 466 | -server. The localhost restriction exists because it also gives anyone | |
| 467 | -who can access the resulting web UI full control over the | |
| 468 | -repository. (This is the [./caps/admin-v-setup.md#apsu | all-powerful | |
| 469 | -Setup capabliity].) | |
| 470 | - | |
| 471 | -For cross-machine collaboration, use the <b>server</b> command instead, | |
| 472 | -which binds on all IP addresses, does not try to start a web browser, | |
| 473 | -and enforces [./caps/ | Fossil's role-based access control system]. | |
| 474 | - | |
| 475 | -Servers are also easily configured as: | |
| 497 | +Just as with <b>fossil ui</b>, you may omit the | |
| 498 | +<i>repository-filename</i> parameter when running this from within an open | |
| 499 | +check-out. | |
| 500 | + | |
| 501 | +<i>Unlike</i> <b>fossil ui</b> mode, Fossil binds to all network | |
| 502 | +interfaces by default in this mode, and it enforces the configured | |
| 503 | +[./caps/ | role-based access controls]. Further, because it is meant to | |
| 504 | +provide external web service, it doesn't try to launch a local web | |
| 505 | +browser pointing to a "Fossil UI" presentation; external visitors see | |
| 506 | +your repository's configured home page instead. | |
| 507 | + | |
| 508 | +To serve varying needs, there are additional ways to serve a Fossil repo | |
| 509 | +to external users: | |
| 476 | 510 | |
| 477 | 511 | <ul> |
| 512 | +<li>[./server/any/cgi.md|CGI], as used by Fossil's [./selfhost.wiki | | |
| 513 | + self-hosting repositories] | |
| 514 | +<li>[./server/any/scgi.md|SCGI] | |
| 478 | 515 | <li>[./server/any/inetd.md|inetd] |
| 479 | 516 | <li>[./server/debian/service.md|systemd] |
| 480 | -<li>[./server/any/cgi.md|CGI] | |
| 481 | -<li>[./server/any/scgi.md|SCGI] | |
| 482 | 517 | </ul> |
| 483 | 518 | |
| 484 | 519 | …along with [./server/#matrix | several other options]. |
| 485 | 520 | |
| 486 | -The [./selfhost.wiki | self-hosting fossil repositories] use | |
| 487 | -CGI. | |
| 488 | - | |
| 489 | -You might <i>need</i> to set up a server, whether you know it yet or | |
| 490 | -not. See the [./server/whyuseaserver.wiki | Benefits of a Fossil Server] | |
| 491 | -article for details. | |
| 521 | +We recommend that you read the [./server/whyuseaserver.wiki | Benefits | |
| 522 | +of a Fossil Server] article, because you might <i>need</i> to do this | |
| 523 | +and not yet know it. | |
| 492 | 524 | |
| 493 | 525 | <h2 id="proxy">HTTP Proxies</h2> |
| 494 | 526 | |
| 495 | 527 | If you are behind a restrictive firewall that requires you to use |
| 496 | 528 | an HTTP proxy to reach the internet, then you can configure the proxy |
| 497 | 529 |
| --- www/quickstart.wiki | |
| +++ www/quickstart.wiki | |
| @@ -14,20 +14,18 @@ | |
| 14 | someplace on your $PATH. |
| 15 | |
| 16 | You can test that Fossil is present and working like this: |
| 17 | |
| 18 | <pre><b>fossil version |
| 19 | This is fossil version 2.13 [309af345ab] 2020-09-28 04:02:55 UTC |
| 20 | </b></pre> |
| 21 | |
| 22 | <h2 id="workflow" name="fslclone">General Work Flow</h2> |
| 23 | |
| 24 | Fossil works with repository files (a database in a single file with the project's |
| 25 | complete history) and with checked-out local trees (the working directory |
| 26 | you use to do your work). |
| 27 | (See [./glossary.md | the glossary] for more background.) |
| 28 | The workflow looks like this: |
| 29 | |
| 30 | <ul> |
| 31 | <li>Create or clone a repository file. ([/help/init|fossil init] or |
| 32 | [/help/clone | fossil clone]) |
| 33 | <li>Check out a local tree. ([/help/open | fossil open]) |
| @@ -41,29 +39,54 @@ | |
| 41 | The following sections give a brief overview of these |
| 42 | operations. |
| 43 | |
| 44 | <h2 id="new">Starting A New Project</h2> |
| 45 | |
| 46 | To start a new project with fossil create a new empty repository |
| 47 | this way: ([/help/init | more info]) |
| 48 | |
| 49 | <pre><b>fossil init</b> <i>repository-filename</i> |
| 50 | </pre> |
| 51 | |
| 52 | You can name the database anything you like, and you can place it anywhere in the filesystem. |
| 53 | The <tt>.fossil</tt> extension is traditional but only required if you are going to use the |
| 54 | <tt>[/help/server | fossil server DIRECTORY]</tt> feature.” |
| 55 | |
| 56 | <h2 id="clone">Cloning An Existing Repository</h2> |
| 57 | |
| 58 | Most fossil operations interact with a repository that is on the |
| 59 | local disk drive, not on a remote system. Hence, before accessing |
| 60 | a remote repository it is necessary to make a local copy of that |
| 61 | repository. Making a local copy of a remote repository is called |
| 62 | "cloning". |
| 63 | |
| 64 | Clone a remote repository as follows: ([/help/clone | more info]) |
| 65 | |
| 66 | <pre><b>fossil clone</b> <i>URL repository-filename</i> |
| 67 | </pre> |
| 68 | |
| 69 | The <i>URL</i> specifies the fossil repository |
| @@ -81,12 +104,20 @@ | |
| 81 | 100% complete... |
| 82 | Extra delta compression... |
| 83 | Vacuuming the database... |
| 84 | project-id: 94259BB9F186226D80E49D1FA2DB29F935CCA0333 |
| 85 | server-id: 016595e9043054038a9ea9bc526d7f33f7ac0e42 |
| 86 | admin-user: exampleuser (password is "yoWgDR42iv")> |
| 87 | </b></pre> |
| 88 | |
| 89 | If the remote repository requires a login, include a |
| 90 | userid in the URL like this: |
| 91 | |
| 92 | <pre><b>fossil clone https://</b><i>remoteuserid</i><b>@www.example.org/ myclone.fossil</b></pre> |
| @@ -127,26 +158,23 @@ | |
| 127 | |
| 128 | <h2 id="checkout">Checking Out A Local Tree</h2> |
| 129 | |
| 130 | To work on a project in fossil, you need to check out a local |
| 131 | copy of the source tree. Create the directory you want to be |
| 132 | the root of your tree and cd into that directory. Then |
| 133 | do this: ([/help/open | more info]) |
| 134 | |
| 135 | <pre><b>fossil open</b> <i>repository-filename</i></pre> |
| 136 | |
| 137 | for example: |
| 138 | |
| 139 | <pre><b>fossil open ../myclone.fossil |
| 140 | BUILD.txt |
| 141 | COPYRIGHT-BSD2.txt |
| 142 | README.md |
| 143 | ︙ |
| 144 | </tt></b></pre> |
| 145 | |
| 146 | (or "fossil open ..\myclone.fossil" on Windows). |
| 147 | |
| 148 | This leaves you with the newest version of the tree |
| 149 | checked out. |
| 150 | From anywhere underneath the root of your local tree, you |
| 151 | can type commands like the following to find out the status of |
| 152 | your local tree: |
| @@ -294,41 +322,60 @@ | |
| 294 | |
| 295 | This will get you started on identifying checkins. The |
| 296 | <a href="./checkin_names.wiki">Checkin Names document</a> is a complete reference, including |
| 297 | how timestamps can also be used. |
| 298 | |
| 299 | <h2 id="config">Configuring Your Local Repository</h2> |
| 300 | |
| 301 | When you create a new repository, either by cloning an existing |
| 302 | project or create a new project of your own, you usually want to do some |
| 303 | local configuration. This is easily accomplished using the web-server |
| 304 | that is built into fossil. Start the fossil web server like this: |
| 305 | ([/help/ui | more info]) |
| 306 | |
| 307 | <pre> |
| 308 | <b>fossil ui</b> <i>repository-filename</i> |
| 309 | </pre> |
| 310 | |
| 311 | You can omit the <i>repository-filename</i> from the command above |
| 312 | if you are inside a checked-out local tree. |
| 313 | |
| 314 | This starts a web server then automatically launches your |
| 315 | web browser and makes it point to this web server. If your system |
| 316 | has an unusual configuration, fossil might not be able to figure out |
| 317 | how to start your web browser. In that case, first tell fossil |
| 318 | where to find your web browser using a command like this: |
| 319 | |
| 320 | <pre> |
| 321 | <b>fossil setting web-browser</b> <i>path-to-web-browser</i> |
| 322 | </pre> |
| 323 | |
| 324 | By default, fossil does not require a login for HTTP connections |
| 325 | coming in from the IP loopback address 127.0.0.1. You can, and perhaps |
| 326 | should, change this after you create a few users. |
| 327 | |
| 328 | When you are finished configuring, just press Control-C or use |
| 329 | the <b>kill</b> command to shut down the mini-server. |
| 330 | |
| 331 | <h2 id="sharing">Sharing Changes</h2> |
| 332 | |
| 333 | When [./concepts.wiki#workflow|autosync] is turned off, |
| 334 | the changes you [/help/commit | commit] are only |
| @@ -384,16 +431,12 @@ | |
| 384 | them and fails if local changes exist unless the <tt>--force</tt> |
| 385 | flag is used. |
| 386 | |
| 387 | <h2 id="branch" name="merge">Branching And Merging</h2> |
| 388 | |
| 389 | Use the --branch option to the [/help/commit | commit] command |
| 390 | to start a new branch. Note that in Fossil, branches are normally |
| 391 | created when you commit, not before you start editing. You can |
| 392 | use the [/help/branch | branch new] command to create a new branch |
| 393 | before you start editing, if you want, but most people just wait |
| 394 | until they are ready to commit. |
| 395 | |
| 396 | To merge two branches back together, first |
| 397 | [/help/update | update] to the branch you want to merge into. |
| 398 | Then do a [/help/merge|merge] of the other branch that you want to incorporate |
| 399 | the changes from. For example, to merge "featureX" changes into "trunk" |
| @@ -442,55 +485,44 @@ | |
| 442 | level of undo/redo. |
| 443 | |
| 444 | |
| 445 | <h2 id="server">Setting Up A Server</h2> |
| 446 | |
| 447 | Fossil can act as a stand-alone web server using one of these |
| 448 | commands: |
| 449 | |
| 450 | <pre> |
| 451 | <b>[/help/server | fossil server]</b> <i>repository-filename</i> |
| 452 | <b>[/help/ui | fossil ui]</b> <i>repository-filename</i> |
| 453 | </pre> |
| 454 | |
| 455 | The <i>repository-filename</i> can be omitted when these commands |
| 456 | are run from within an open check-out, which is a particularly useful |
| 457 | shortcut with the <b>fossil ui</b> command. |
| 458 | |
| 459 | The <b>ui</b> command is intended for accessing the web user interface |
| 460 | from a local desktop. (We sometimes call this mode "Fossil UI.") |
| 461 | The <b>ui</b> command differs from the |
| 462 | <b>server</b> command by binding to the loopback IP |
| 463 | address only (thus making the web UI visible only on the |
| 464 | local machine) and by automatically starting your default web browser, |
| 465 | pointing it at the running UI |
| 466 | server. The localhost restriction exists because it also gives anyone |
| 467 | who can access the resulting web UI full control over the |
| 468 | repository. (This is the [./caps/admin-v-setup.md#apsu | all-powerful |
| 469 | Setup capabliity].) |
| 470 | |
| 471 | For cross-machine collaboration, use the <b>server</b> command instead, |
| 472 | which binds on all IP addresses, does not try to start a web browser, |
| 473 | and enforces [./caps/ | Fossil's role-based access control system]. |
| 474 | |
| 475 | Servers are also easily configured as: |
| 476 | |
| 477 | <ul> |
| 478 | <li>[./server/any/inetd.md|inetd] |
| 479 | <li>[./server/debian/service.md|systemd] |
| 480 | <li>[./server/any/cgi.md|CGI] |
| 481 | <li>[./server/any/scgi.md|SCGI] |
| 482 | </ul> |
| 483 | |
| 484 | …along with [./server/#matrix | several other options]. |
| 485 | |
| 486 | The [./selfhost.wiki | self-hosting fossil repositories] use |
| 487 | CGI. |
| 488 | |
| 489 | You might <i>need</i> to set up a server, whether you know it yet or |
| 490 | not. See the [./server/whyuseaserver.wiki | Benefits of a Fossil Server] |
| 491 | article for details. |
| 492 | |
| 493 | <h2 id="proxy">HTTP Proxies</h2> |
| 494 | |
| 495 | If you are behind a restrictive firewall that requires you to use |
| 496 | an HTTP proxy to reach the internet, then you can configure the proxy |
| 497 |
| --- www/quickstart.wiki | |
| +++ www/quickstart.wiki | |
| @@ -14,20 +14,18 @@ | |
| 14 | someplace on your $PATH. |
| 15 | |
| 16 | You can test that Fossil is present and working like this: |
| 17 | |
| 18 | <pre><b>fossil version |
| 19 | This is fossil version 2.25 [8f798279d5] 2024-11-06 12:59:09 UTC |
| 20 | </b></pre> |
| 21 | |
| 22 | <h2 id="workflow" name="fslclone">General Work Flow</h2> |
| 23 | |
| 24 | Fossil works with [./glossary.md#repository | repository files] |
| 25 | and [./glossary.md#check-out | check-out directories] using a |
| 26 | workflow like this: |
| 27 | |
| 28 | <ul> |
| 29 | <li>Create or clone a repository file. ([/help/init|fossil init] or |
| 30 | [/help/clone | fossil clone]) |
| 31 | <li>Check out a local tree. ([/help/open | fossil open]) |
| @@ -41,29 +39,54 @@ | |
| 39 | The following sections give a brief overview of these |
| 40 | operations. |
| 41 | |
| 42 | <h2 id="new">Starting A New Project</h2> |
| 43 | |
| 44 | To start a new project with Fossil, [/help/init | create a new empty repository]: |
| 45 | |
| 46 | <pre><b>fossil init</b> <i>repository-filename</i> |
| 47 | </pre> |
| 48 | |
| 49 | You can name the database anything you like, and you can place it anywhere in the filesystem. |
| 50 | The <tt>.fossil</tt> extension is traditional, but it is only required if you are going to use the |
| 51 | <tt>[/help/server | fossil server DIRECTORY]</tt> feature. |
| 52 | |
| 53 | Next, do something along the lines of: |
| 54 | |
| 55 | <pre> |
| 56 | <b>mkdir -p ~/src/project/trunk</b> |
| 57 | <b>cd ~/src/project/trunk</b> |
| 58 | <b>fossil open</b> <i>repository-filename</i> |
| 59 | <b>fossil add</b> foo.c bar.h qux.md |
| 60 | <b>fossil commit</b> |
| 61 | </pre> |
| 62 | |
| 63 | If your project directory already exists, obviating the <b>mkdir</b> |
| 64 | step, you will instead need to add the <tt>--force</tt> flag to the |
| 65 | <b>open</b> command to authorize Fossil to open the repo into a |
| 66 | non-empty checkout directory. (This is to avoid accidental opens into, |
| 67 | for example, your home directory.) |
| 68 | |
| 69 | The convention of naming your checkout directory after a long-lived |
| 70 | branch name like "trunk" is in support of Fossil's ability to have as |
| 71 | many open checkouts as you like. This author frequently has additional |
| 72 | checkout directories named <tt>../release</tt>, <tt>../scratch</tt>, |
| 73 | etc. The release directory is open to the branch of the same name, while |
| 74 | the scratch directory is used when disturbing one of the other |
| 75 | long-lived checkout directories is undesireable, as when performing a |
| 76 | [/help/bisect | bisect] operation. |
| 77 | |
| 78 | |
| 79 | <h2 id="clone">Cloning An Existing Repository</h2> |
| 80 | |
| 81 | Most fossil operations interact with a repository that is on the |
| 82 | local disk drive, not on a remote system. Hence, before accessing |
| 83 | a remote repository it is necessary to make a local copy of that |
| 84 | repository, a process called |
| 85 | "[/help/clone | cloning]". |
| 86 | |
| 87 | This is done as follows: |
| 88 | |
| 89 | <pre><b>fossil clone</b> <i>URL repository-filename</i> |
| 90 | </pre> |
| 91 | |
| 92 | The <i>URL</i> specifies the fossil repository |
| @@ -81,12 +104,20 @@ | |
| 104 | 100% complete... |
| 105 | Extra delta compression... |
| 106 | Vacuuming the database... |
| 107 | project-id: 94259BB9F186226D80E49D1FA2DB29F935CCA0333 |
| 108 | server-id: 016595e9043054038a9ea9bc526d7f33f7ac0e42 |
| 109 | admin-user: exampleuser (intial remote-access password is "yoWgDR42iv")> |
| 110 | </b></pre> |
| 111 | |
| 112 | This <i>exampleuser</i> will be used by Fossil as the author of commits when |
| 113 | you checkin changes to the repository. It is also used by Fossil when you |
| 114 | make your repository available to others using the built-in server mode by |
| 115 | running <tt>[/help/server | fossil server]</tt> and will also be used when |
| 116 | running <tt>[/help/ui | fossil ui]</tt> to view the repository through |
| 117 | the Fossil UI. See the quick start topic for setting up a |
| 118 | <a href="#server">server</a> for more details. |
| 119 | |
| 120 | If the remote repository requires a login, include a |
| 121 | userid in the URL like this: |
| 122 | |
| 123 | <pre><b>fossil clone https://</b><i>remoteuserid</i><b>@www.example.org/ myclone.fossil</b></pre> |
| @@ -127,26 +158,23 @@ | |
| 158 | |
| 159 | <h2 id="checkout">Checking Out A Local Tree</h2> |
| 160 | |
| 161 | To work on a project in fossil, you need to check out a local |
| 162 | copy of the source tree. Create the directory you want to be |
| 163 | the root of your tree, <tt>cd</tt> into that directory, and then: |
| 164 | |
| 165 | <pre><b>fossil open</b> <i>repository-filename</i></pre> |
| 166 | |
| 167 | For example: |
| 168 | |
| 169 | <pre><b>fossil open ../myclone.fossil |
| 170 | BUILD.txt |
| 171 | COPYRIGHT-BSD2.txt |
| 172 | README.md |
| 173 | ︙ |
| 174 | </tt></b></pre> |
| 175 | |
| 176 | This leaves you with the newest version of the tree |
| 177 | checked out. |
| 178 | From anywhere underneath the root of your local tree, you |
| 179 | can type commands like the following to find out the status of |
| 180 | your local tree: |
| @@ -294,41 +322,60 @@ | |
| 322 | |
| 323 | This will get you started on identifying checkins. The |
| 324 | <a href="./checkin_names.wiki">Checkin Names document</a> is a complete reference, including |
| 325 | how timestamps can also be used. |
| 326 | |
| 327 | <h2 id="config">Accessing Your Local Repository's Web User Interface</h2> |
| 328 | |
| 329 | After you create a new repository, you usually want to do some local |
| 330 | configuration. This is most easily accomplished by firing up the Fossil |
| 331 | UI: |
| 332 | |
| 333 | <pre> |
| 334 | <b>fossil ui</b> <i>repository-filename</i> |
| 335 | </pre> |
| 336 | |
| 337 | You can shorten that to just [/help/ui | <b>fossil ui</b>] |
| 338 | if you are inside a checked-out local tree. |
| 339 | |
| 340 | This command starts an internal web server, after which Fossil |
| 341 | automatically launches your default browser, pointed at itself, |
| 342 | presenting a special view of the repository, its web user interface. |
| 343 | |
| 344 | You may override Fossil's logic for selecting the default browser so: |
| 345 | |
| 346 | <pre> |
| 347 | <b>fossil setting web-browser</b> <i>path-to-web-browser</i> |
| 348 | </pre> |
| 349 | |
| 350 | When launched this way, Fossil binds its internal web server to the IP |
| 351 | loopback address, 127.0.0.1, which it treats specially, bypassing all |
| 352 | user controls, effectively giving visitors the |
| 353 | [./caps/admin-v-setup.md#apsu | all-powerful Setup capabliity]. |
| 354 | |
| 355 | Why is that a good idea, you ask? Because it is a safe |
| 356 | presumption that only someone with direct file access to the repository |
| 357 | database file could be using the resulting web interface. Anyone who can |
| 358 | modify the repo DB directly could give themselves any and all access |
| 359 | with a SQL query, or even by direct file manipulation; no amount of |
| 360 | access control matters to such a user. |
| 361 | |
| 362 | (Contrast the [#server | many <i>other</i> ways] of setting Fossil up |
| 363 | as an HTTP server, where the repo DB is on the other side of the HTTP |
| 364 | server wall, inaccessible by all means other than Fossil's own |
| 365 | mediation. For this reason, the "localhost bypasses access control" |
| 366 | policy does <i>not</i> apply to these other interfaces. That is a very |
| 367 | good thing, since without this difference in policy, it would be unsafe |
| 368 | to bind a [/help?cmd=server | <b>fossil server</b>] instance to |
| 369 | localhost on a high-numbered port and then reverse-proxy it out to the |
| 370 | world via HTTPS, a practice this author does engage in, with confidence.) |
| 371 | |
| 372 | Once you are finished configuring Fossil, you may safely Control-C out |
| 373 | of the <b>fossil ui</b> command to shut down this privileged |
| 374 | built-in web server. Moreover, you may by grace of SQLite do this <i>at |
| 375 | any time</i>: all changes are either committed durably to the repo DB or |
| 376 | rolled back, in their totality. This includes configuration changes. |
| 377 | |
| 378 | <h2 id="sharing">Sharing Changes</h2> |
| 379 | |
| 380 | When [./concepts.wiki#workflow|autosync] is turned off, |
| 381 | the changes you [/help/commit | commit] are only |
| @@ -384,16 +431,12 @@ | |
| 431 | them and fails if local changes exist unless the <tt>--force</tt> |
| 432 | flag is used. |
| 433 | |
| 434 | <h2 id="branch" name="merge">Branching And Merging</h2> |
| 435 | |
| 436 | Use the --branch option to the [/help/commit | commit] command to start |
| 437 | a new branch at the point of need. ([./gitusers.md#bneed | Contrast git].) |
| 438 | |
| 439 | To merge two branches back together, first |
| 440 | [/help/update | update] to the branch you want to merge into. |
| 441 | Then do a [/help/merge|merge] of the other branch that you want to incorporate |
| 442 | the changes from. For example, to merge "featureX" changes into "trunk" |
| @@ -442,55 +485,44 @@ | |
| 485 | level of undo/redo. |
| 486 | |
| 487 | |
| 488 | <h2 id="server">Setting Up A Server</h2> |
| 489 | |
| 490 | In addition to the inward-facing <b>fossil ui</b> mode covered [#config |
| 491 | | above], Fossil can also act as an outward-facing web server: |
| 492 | |
| 493 | <pre> |
| 494 | <b>[/help/server | fossil server]</b> <i>repository-filename</i> |
| 495 | </pre> |
| 496 | |
| 497 | Just as with <b>fossil ui</b>, you may omit the |
| 498 | <i>repository-filename</i> parameter when running this from within an open |
| 499 | check-out. |
| 500 | |
| 501 | <i>Unlike</i> <b>fossil ui</b> mode, Fossil binds to all network |
| 502 | interfaces by default in this mode, and it enforces the configured |
| 503 | [./caps/ | role-based access controls]. Further, because it is meant to |
| 504 | provide external web service, it doesn't try to launch a local web |
| 505 | browser pointing to a "Fossil UI" presentation; external visitors see |
| 506 | your repository's configured home page instead. |
| 507 | |
| 508 | To serve varying needs, there are additional ways to serve a Fossil repo |
| 509 | to external users: |
| 510 | |
| 511 | <ul> |
| 512 | <li>[./server/any/cgi.md|CGI], as used by Fossil's [./selfhost.wiki | |
| 513 | self-hosting repositories] |
| 514 | <li>[./server/any/scgi.md|SCGI] |
| 515 | <li>[./server/any/inetd.md|inetd] |
| 516 | <li>[./server/debian/service.md|systemd] |
| 517 | </ul> |
| 518 | |
| 519 | …along with [./server/#matrix | several other options]. |
| 520 | |
| 521 | We recommend that you read the [./server/whyuseaserver.wiki | Benefits |
| 522 | of a Fossil Server] article, because you might <i>need</i> to do this |
| 523 | and not yet know it. |
| 524 | |
| 525 | <h2 id="proxy">HTTP Proxies</h2> |
| 526 | |
| 527 | If you are behind a restrictive firewall that requires you to use |
| 528 | an HTTP proxy to reach the internet, then you can configure the proxy |
| 529 |
+3
-3
| --- www/server/debian/service.md | ||
| +++ www/server/debian/service.md | ||
| @@ -52,11 +52,11 @@ | ||
| 52 | 52 | suitable for sharing a Fossil repo to a workgroup on a private LAN. |
| 53 | 53 | |
| 54 | 54 | To do this, write the following in |
| 55 | 55 | `~/.local/share/systemd/user/fossil.service`: |
| 56 | 56 | |
| 57 | -```dosini | |
| 57 | +> ```dosini | |
| 58 | 58 | [Unit] |
| 59 | 59 | Description=Fossil user server |
| 60 | 60 | After=network-online.target |
| 61 | 61 | |
| 62 | 62 | [Service] |
| @@ -164,11 +164,11 @@ | ||
| 164 | 164 | It’s more complicated, but it has some nice properties. |
| 165 | 165 | |
| 166 | 166 | We first need to define the privileged socket listener by writing |
| 167 | 167 | `/etc/systemd/system/fossil.socket`: |
| 168 | 168 | |
| 169 | -```dosini | |
| 169 | +> ```dosini | |
| 170 | 170 | [Unit] |
| 171 | 171 | Description=Fossil socket |
| 172 | 172 | |
| 173 | 173 | [Socket] |
| 174 | 174 | Accept=yes |
| @@ -189,11 +189,11 @@ | ||
| 189 | 189 | documentation](../any/inetd.md). |
| 190 | 190 | |
| 191 | 191 | Next, create the service definition file in that same directory as |
| 192 | 192 | `[email protected]`: |
| 193 | 193 | |
| 194 | -```dosini | |
| 194 | +> ```dosini | |
| 195 | 195 | [Unit] |
| 196 | 196 | Description=Fossil socket server |
| 197 | 197 | After=network-online.target |
| 198 | 198 | |
| 199 | 199 | [Service] |
| 200 | 200 |
| --- www/server/debian/service.md | |
| +++ www/server/debian/service.md | |
| @@ -52,11 +52,11 @@ | |
| 52 | suitable for sharing a Fossil repo to a workgroup on a private LAN. |
| 53 | |
| 54 | To do this, write the following in |
| 55 | `~/.local/share/systemd/user/fossil.service`: |
| 56 | |
| 57 | ```dosini |
| 58 | [Unit] |
| 59 | Description=Fossil user server |
| 60 | After=network-online.target |
| 61 | |
| 62 | [Service] |
| @@ -164,11 +164,11 @@ | |
| 164 | It’s more complicated, but it has some nice properties. |
| 165 | |
| 166 | We first need to define the privileged socket listener by writing |
| 167 | `/etc/systemd/system/fossil.socket`: |
| 168 | |
| 169 | ```dosini |
| 170 | [Unit] |
| 171 | Description=Fossil socket |
| 172 | |
| 173 | [Socket] |
| 174 | Accept=yes |
| @@ -189,11 +189,11 @@ | |
| 189 | documentation](../any/inetd.md). |
| 190 | |
| 191 | Next, create the service definition file in that same directory as |
| 192 | `[email protected]`: |
| 193 | |
| 194 | ```dosini |
| 195 | [Unit] |
| 196 | Description=Fossil socket server |
| 197 | After=network-online.target |
| 198 | |
| 199 | [Service] |
| 200 |
| --- www/server/debian/service.md | |
| +++ www/server/debian/service.md | |
| @@ -52,11 +52,11 @@ | |
| 52 | suitable for sharing a Fossil repo to a workgroup on a private LAN. |
| 53 | |
| 54 | To do this, write the following in |
| 55 | `~/.local/share/systemd/user/fossil.service`: |
| 56 | |
| 57 | > ```dosini |
| 58 | [Unit] |
| 59 | Description=Fossil user server |
| 60 | After=network-online.target |
| 61 | |
| 62 | [Service] |
| @@ -164,11 +164,11 @@ | |
| 164 | It’s more complicated, but it has some nice properties. |
| 165 | |
| 166 | We first need to define the privileged socket listener by writing |
| 167 | `/etc/systemd/system/fossil.socket`: |
| 168 | |
| 169 | > ```dosini |
| 170 | [Unit] |
| 171 | Description=Fossil socket |
| 172 | |
| 173 | [Socket] |
| 174 | Accept=yes |
| @@ -189,11 +189,11 @@ | |
| 189 | documentation](../any/inetd.md). |
| 190 | |
| 191 | Next, create the service definition file in that same directory as |
| 192 | `[email protected]`: |
| 193 | |
| 194 | > ```dosini |
| 195 | [Unit] |
| 196 | Description=Fossil socket server |
| 197 | After=network-online.target |
| 198 | |
| 199 | [Service] |
| 200 |
+1
-1
| --- www/server/windows/service.md | ||
| +++ www/server/windows/service.md | ||
| @@ -55,11 +55,11 @@ | ||
| 55 | 55 | for temporary files is exempted from such scanning. Ordinarily, this |
| 56 | 56 | will be a subdirectory named "fossil" in the temporary directory given |
| 57 | 57 | by the Windows GetTempPath(...) API, [namely](https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-gettemppathw#remarks) |
| 58 | 58 | the value of the first existing environment variable from `%TMP%`, `%TEMP%`, |
| 59 | 59 | `%USERPROFILE%`, and `%SystemRoot%`; you can look for their actual values in |
| 60 | -your system by accessing the `/test_env` webpage. | |
| 60 | +your system by accessing the `/test-env` webpage. | |
| 61 | 61 | Excluding this subdirectory will avoid certain rare failures where the |
| 62 | 62 | fossil.exe process is unable to use the directory normally during a scan. |
| 63 | 63 | |
| 64 | 64 | ### <a id='PowerShell'></a>Advanced service installation using PowerShell |
| 65 | 65 | |
| 66 | 66 |
| --- www/server/windows/service.md | |
| +++ www/server/windows/service.md | |
| @@ -55,11 +55,11 @@ | |
| 55 | for temporary files is exempted from such scanning. Ordinarily, this |
| 56 | will be a subdirectory named "fossil" in the temporary directory given |
| 57 | by the Windows GetTempPath(...) API, [namely](https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-gettemppathw#remarks) |
| 58 | the value of the first existing environment variable from `%TMP%`, `%TEMP%`, |
| 59 | `%USERPROFILE%`, and `%SystemRoot%`; you can look for their actual values in |
| 60 | your system by accessing the `/test_env` webpage. |
| 61 | Excluding this subdirectory will avoid certain rare failures where the |
| 62 | fossil.exe process is unable to use the directory normally during a scan. |
| 63 | |
| 64 | ### <a id='PowerShell'></a>Advanced service installation using PowerShell |
| 65 | |
| 66 |
| --- www/server/windows/service.md | |
| +++ www/server/windows/service.md | |
| @@ -55,11 +55,11 @@ | |
| 55 | for temporary files is exempted from such scanning. Ordinarily, this |
| 56 | will be a subdirectory named "fossil" in the temporary directory given |
| 57 | by the Windows GetTempPath(...) API, [namely](https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-gettemppathw#remarks) |
| 58 | the value of the first existing environment variable from `%TMP%`, `%TEMP%`, |
| 59 | `%USERPROFILE%`, and `%SystemRoot%`; you can look for their actual values in |
| 60 | your system by accessing the `/test-env` webpage. |
| 61 | Excluding this subdirectory will avoid certain rare failures where the |
| 62 | fossil.exe process is unable to use the directory normally during a scan. |
| 63 | |
| 64 | ### <a id='PowerShell'></a>Advanced service installation using PowerShell |
| 65 | |
| 66 |
+1
-1
| --- www/serverext.wiki | ||
| +++ www/serverext.wiki | ||
| @@ -189,11 +189,11 @@ | ||
| 189 | 189 | to find more detail about what each of the above variables mean and how |
| 190 | 190 | they are used. |
| 191 | 191 | Live listings of the values of some or all of these environment variables |
| 192 | 192 | can be found at links like these: |
| 193 | 193 | |
| 194 | - * [https://fossil-scm.org/home/test_env] | |
| 194 | + * [https://fossil-scm.org/home/test-env] | |
| 195 | 195 | * [https://sqlite.org/src/ext/checklist/top/env] |
| 196 | 196 | |
| 197 | 197 | In addition to the standard CGI environment variables listed above, |
| 198 | 198 | Fossil adds the following: |
| 199 | 199 | |
| 200 | 200 |
| --- www/serverext.wiki | |
| +++ www/serverext.wiki | |
| @@ -189,11 +189,11 @@ | |
| 189 | to find more detail about what each of the above variables mean and how |
| 190 | they are used. |
| 191 | Live listings of the values of some or all of these environment variables |
| 192 | can be found at links like these: |
| 193 | |
| 194 | * [https://fossil-scm.org/home/test_env] |
| 195 | * [https://sqlite.org/src/ext/checklist/top/env] |
| 196 | |
| 197 | In addition to the standard CGI environment variables listed above, |
| 198 | Fossil adds the following: |
| 199 | |
| 200 |
| --- www/serverext.wiki | |
| +++ www/serverext.wiki | |
| @@ -189,11 +189,11 @@ | |
| 189 | to find more detail about what each of the above variables mean and how |
| 190 | they are used. |
| 191 | Live listings of the values of some or all of these environment variables |
| 192 | can be found at links like these: |
| 193 | |
| 194 | * [https://fossil-scm.org/home/test-env] |
| 195 | * [https://sqlite.org/src/ext/checklist/top/env] |
| 196 | |
| 197 | In addition to the standard CGI environment variables listed above, |
| 198 | Fossil adds the following: |
| 199 | |
| 200 |
+128
-5
| --- www/th1.md | ||
| +++ www/th1.md | ||
| @@ -68,11 +68,11 @@ | ||
| 68 | 68 | are removed from each token by the command parser.) The third token |
| 69 | 69 | is the `puts "hello"`, with its whitespace and newlines. The fourth token |
| 70 | 70 | is `else` and the fifth and last token is `puts "world"`. |
| 71 | 71 | |
| 72 | 72 | The `if` command evaluates its first argument (the second token) |
| 73 | -as an expression, and if that expression is true, evaluates its | |
| 73 | +as an expression, and if that expression is true, it evaluates its | |
| 74 | 74 | second argument (the third token) as a TH1 script. |
| 75 | 75 | If the expression is false and the third argument is `else`, then |
| 76 | 76 | the fourth argument is evaluated as a TH1 expression. |
| 77 | 77 | |
| 78 | 78 | So, you see, even though the example above spans five lines, it is really |
| @@ -106,10 +106,49 @@ | ||
| 106 | 106 | $repository "" info trunk]]] end] |
| 107 | 107 | |
| 108 | 108 | Those backslashes allow the command to wrap nicely within a standard |
| 109 | 109 | terminal width while telling the interpreter to consider those three |
| 110 | 110 | lines as a single command. |
| 111 | + | |
| 112 | +<a id="taint"></a>Tainted And Untainted Strings | |
| 113 | +----------------------------------------------- | |
| 114 | + | |
| 115 | +Beginning with Fossil version 2.26 (circa 2025), TH1 distinguishes between | |
| 116 | +"tainted" and "untainted" strings. Tainted strings are strings that are | |
| 117 | +derived from user inputs that might contain text that is designed to subvert | |
| 118 | +the script. Untainted strings are known to come from secure sources and | |
| 119 | +are assumed to contain no malicious content. | |
| 120 | + | |
| 121 | +Beginning with Fossil version 2.26, and depending on the value of the | |
| 122 | +[vuln-report setting](/help?cmd=vuln-report), TH1 will prevent tainted | |
| 123 | +strings from being used in ways that might lead to XSS or SQL-injection | |
| 124 | +attacks. This feature helps to ensure that XSS and SQL-injection | |
| 125 | +vulnerabilities are not *accidentally* added to Fossil when | |
| 126 | +custom TH1 scripts for headers or footers or tickets are added to a | |
| 127 | +repository. Note that the tainted/untainted distinction in strings does | |
| 128 | +not make it impossible to introduce XSS and SQL-injections vulnerabilities | |
| 129 | +using poorly-written TH1 scripts; it just makes it more difficult and | |
| 130 | +less likely to happen by accident. Developers must still consider the | |
| 131 | +security implications TH1 customizations they add to Fossil, and take | |
| 132 | +appropriate precautions when writing custom TH1. Peer review of TH1 | |
| 133 | +script changes is encouraged. | |
| 134 | + | |
| 135 | +In Fossil version 2.26, if the vuln-report setting is set to "block" | |
| 136 | +or "fatal", the [html](#html) and [query](#query) TH1 commands will | |
| 137 | +fail with an error if their argument is a tainted string. This helps | |
| 138 | +to prevent XSS and SQL-injection attacks, respectively. Note that | |
| 139 | +the default value of the vuln-report setting is "log", which allows those | |
| 140 | +commands to continue working and only writes a warning message into the | |
| 141 | +error log. <b>Future versions of Fossil may change the default value | |
| 142 | +of the vuln-report setting to "block" or "fatal".</b> Fossil users | |
| 143 | +with customized TH1 scripts are encouraged to audit their customizations | |
| 144 | +and fix any potential vulnerabilities soon, so as to avoid breakage | |
| 145 | +caused by future upgrades. <b>Future versions of Fossil might also | |
| 146 | +place additional restrictions on the use of tainted strings.</b> | |
| 147 | +For example, it is likely that future versions of Fossil will disallow | |
| 148 | +using tainted strings as script, for example as the body of a "for" | |
| 149 | +loop or of a "proc". | |
| 111 | 150 | |
| 112 | 151 | |
| 113 | 152 | Summary of Core TH1 Commands |
| 114 | 153 | ---------------------------- |
| 115 | 154 | |
| @@ -147,10 +186,13 @@ | ||
| 147 | 186 | * string last NEEDLE HAYSTACK ?START-INDEX? |
| 148 | 187 | * string match PATTERN STRING |
| 149 | 188 | * string length STRING |
| 150 | 189 | * string range STRING FIRST LAST |
| 151 | 190 | * string repeat STRING COUNT |
| 191 | + * string trim STRING | |
| 192 | + * string trimleft STRING | |
| 193 | + * string trimright STRING | |
| 152 | 194 | * unset VARNAME |
| 153 | 195 | * uplevel ?LEVEL? SCRIPT |
| 154 | 196 | * upvar ?FRAME? OTHERVAR MYVAR ?OTHERVAR MYVAR? |
| 155 | 197 | |
| 156 | 198 | All of the above commands work as in the original Tcl. Refer to the |
| @@ -214,17 +256,19 @@ | ||
| 214 | 256 | * [stime](#stime) |
| 215 | 257 | * [styleHeader](#styleHeader) |
| 216 | 258 | * [styleFooter](#styleFooter) |
| 217 | 259 | * [styleScript](#styleScript) |
| 218 | 260 | * [submenu](#submenu) |
| 261 | + * [taint](#taintCmd) | |
| 219 | 262 | * [tclEval](#tclEval) |
| 220 | 263 | * [tclExpr](#tclExpr) |
| 221 | 264 | * [tclInvoke](#tclInvoke) |
| 222 | 265 | * [tclIsSafe](#tclIsSafe) |
| 223 | 266 | * [tclMakeSafe](#tclMakeSafe) |
| 224 | 267 | * [tclReady](#tclReady) |
| 225 | 268 | * [trace](#trace) |
| 269 | + * [untaint](#untaintCmd) | |
| 226 | 270 | * [unversioned content](#unversioned_content) |
| 227 | 271 | * [unversioned list](#unversioned_list) |
| 228 | 272 | * [utime](#utime) |
| 229 | 273 | * [verifyCsrf](#verifyCsrf) |
| 230 | 274 | * [verifyLogin](#verifyLogin) |
| @@ -527,11 +571,25 @@ | ||
| 527 | 571 | <a id="html"></a>TH1 html Command |
| 528 | 572 | ----------------------------------- |
| 529 | 573 | |
| 530 | 574 | * html STRING |
| 531 | 575 | |
| 532 | -Outputs the STRING escaped for HTML. | |
| 576 | +Outputs the STRING literally. It is assumed that STRING contains | |
| 577 | +valid HTML, or that if STRING contains any characters that are | |
| 578 | +significant to HTML (such as `<`, `>`, `'`, or `&`) have already | |
| 579 | +been escaped, perhaps by the [htmlize](#htmlize) command. Use the | |
| 580 | +[puts](#puts) command to output text that might contain unescaped | |
| 581 | +HTML markup. | |
| 582 | + | |
| 583 | +**Beware of XSS attacks!** If the STRING value to the html command | |
| 584 | +can be controlled by a hostile user, then he might be able to sneak | |
| 585 | +in malicious HTML or Javascript which could result in a | |
| 586 | +cross-site scripting (XSS) attack. Be careful that all text that | |
| 587 | +in STRING that might come from user input has been sanitized by the | |
| 588 | +[htmlize](#htmlize) command or similar. In recent versions of Fossil, | |
| 589 | +the STRING value must be [untainted](#taint) or else the "html" command | |
| 590 | +will fail. | |
| 533 | 591 | |
| 534 | 592 | <a id="htmlize"></a>TH1 htmlize Command |
| 535 | 593 | ----------------------------------------- |
| 536 | 594 | |
| 537 | 595 | * htmlize STRING |
| @@ -595,12 +653,16 @@ | ||
| 595 | 653 | <a id="puts"></a>TH1 puts Command |
| 596 | 654 | ----------------------------------- |
| 597 | 655 | |
| 598 | 656 | * puts STRING |
| 599 | 657 | |
| 600 | -Outputs the STRING unchanged, where "unchanged" might, depending on | |
| 601 | -the context, mean "with some characters escaped for HTML." | |
| 658 | +Outputs STRING. Characters within STRING that have special meaning | |
| 659 | +in HTML are escaped prior to being output. Thus is it safe for STRING | |
| 660 | +to be derived from user inputs. See also the [html](#html) command | |
| 661 | +which behaves similarly except does not escape HTML markup. This | |
| 662 | +command ("puts") is safe to use on [tainted strings](#taint), but the "html" | |
| 663 | +command is not. | |
| 602 | 664 | |
| 603 | 665 | <a id="query"></a>TH1 query Command |
| 604 | 666 | ------------------------------------- |
| 605 | 667 | |
| 606 | 668 | * query ?-nocomplain? SQL CODE |
| @@ -608,11 +670,44 @@ | ||
| 608 | 670 | Runs the SQL query given by the SQL argument. For each row in the result |
| 609 | 671 | set, run CODE. |
| 610 | 672 | |
| 611 | 673 | In SQL, parameters such as $var are filled in using the value of variable |
| 612 | 674 | "var". Result values are stored in variables with the column name prior |
| 613 | -to each invocation of CODE. | |
| 675 | +to each invocation of CODE. The names of the variables in which results | |
| 676 | +are stored can be controlled using "AS name" clauses in the SQL. As | |
| 677 | +the database will often contain content that originates from untrusted | |
| 678 | +users, all result values are marked as [tainted](#taint). | |
| 679 | + | |
| 680 | +**Beware of SQL injections in the `query` command!** | |
| 681 | +The SQL argument to the query command should always be literal SQL | |
| 682 | +text enclosed in {...}. The SQL argument should never be a double-quoted | |
| 683 | +string or the value of a \$variable, as those constructs can lead to | |
| 684 | +an SQL Injection attack. If you need to include the values of one or | |
| 685 | +more TH1 variables as part of the SQL, then put \$variable inside the | |
| 686 | +{...}. The \$variable keyword will then get passed down into the SQLite | |
| 687 | +parser which knows to look up the value of \$variable in the TH1 symbol | |
| 688 | +table. For example: | |
| 689 | + | |
| 690 | +~~~ | |
| 691 | + query {SELECT res FROM tab1 WHERE key=$mykey} {...} | |
| 692 | +~~~ | |
| 693 | + | |
| 694 | +SQLite will see the \$mykey token in the SQL and will know to resolve it | |
| 695 | +to the value of the "mykey" TH1 variable, safely and without the possibility | |
| 696 | +of SQL injection. The following is unsafe: | |
| 697 | + | |
| 698 | +~~~ | |
| 699 | + query "SELECT res FROM tab1 WHERE key='$mykey'" {...} ;# <-- UNSAFE! | |
| 700 | +~~~ | |
| 701 | + | |
| 702 | +In this second example, TH1 does the expansion of `$mykey` prior to passing | |
| 703 | +the text down into SQLite. So if `$mykey` contains a single-quote character, | |
| 704 | +followed by additional hostile text, that will result in an SQL injection. | |
| 705 | + | |
| 706 | +To help guard against SQL-injections, recent versions of Fossil require | |
| 707 | +that the SQL argument be [untainted](#taint) or else the "query" command | |
| 708 | +will fail. | |
| 614 | 709 | |
| 615 | 710 | <a id="randhex"></a>TH1 randhex Command |
| 616 | 711 | ----------------------------------------- |
| 617 | 712 | |
| 618 | 713 | * randhex N |
| @@ -638,10 +733,12 @@ | ||
| 638 | 733 | * regexp ?-nocase? ?--? exp string |
| 639 | 734 | |
| 640 | 735 | Checks the string against the specified regular expression and returns |
| 641 | 736 | non-zero if it matches. If the regular expression is invalid or cannot |
| 642 | 737 | be compiled, an error will be generated. |
| 738 | + | |
| 739 | +See [fossil grep](./grep.md) for details on the regexp syntax. | |
| 643 | 740 | |
| 644 | 741 | <a id="reinitialize"></a>TH1 reinitialize Command |
| 645 | 742 | --------------------------------------------------- |
| 646 | 743 | |
| 647 | 744 | * reinitialize ?FLAGS? |
| @@ -741,10 +838,24 @@ | ||
| 741 | 838 | |
| 742 | 839 | * submenu link LABEL URL |
| 743 | 840 | |
| 744 | 841 | Add hyperlink to the submenu of the current page. |
| 745 | 842 | |
| 843 | +<a id="taintCmd"></a>TH1 taint Command | |
| 844 | +----------------------------------------- | |
| 845 | + | |
| 846 | + * taint STRING | |
| 847 | + | |
| 848 | +This command returns a copy of STRING that has been marked as | |
| 849 | +[tainted](#taint). Tainted strings are strings which might be | |
| 850 | +controlled by an attacker and might contain hostile inputs and | |
| 851 | +are thus unsafe to use in certain contexts. For example, tainted | |
| 852 | +strings should not be output as part of a webpage as they might | |
| 853 | +contain rogue HTML or Javascript that could lead to an XSS | |
| 854 | +vulnerability. Similarly, tainted strings should not be run as | |
| 855 | +SQL since they might contain an SQL-injection vulerability. | |
| 856 | + | |
| 746 | 857 | <a id="tclEval"></a>TH1 tclEval Command |
| 747 | 858 | ----------------------------------------- |
| 748 | 859 | |
| 749 | 860 | **This command requires the Tcl integration feature.** |
| 750 | 861 | |
| @@ -812,10 +923,22 @@ | ||
| 812 | 923 | |
| 813 | 924 | * trace STRING |
| 814 | 925 | |
| 815 | 926 | Generates a TH1 trace message if TH1 tracing is enabled. |
| 816 | 927 | |
| 928 | +<a id="untaintCmd"></a>TH1 taint Command | |
| 929 | +----------------------------------------- | |
| 930 | + | |
| 931 | + * untaint STRING | |
| 932 | + | |
| 933 | +This command returns a copy of STRING that has been marked as | |
| 934 | +[untainted](#taint). Untainted strings are strings which are | |
| 935 | +believed to be free of potentially hostile content. Use this | |
| 936 | +command with caution, as it overwrites the tainted-string protection | |
| 937 | +mechanisms that are built into TH1. If you do not understand all | |
| 938 | +the implications of executing this command, then do not use it. | |
| 939 | + | |
| 817 | 940 | <a id="unversioned_content"></a>TH1 unversioned content Command |
| 818 | 941 | ----------------------------------------------------------------- |
| 819 | 942 | |
| 820 | 943 | * unversioned content FILENAME |
| 821 | 944 | |
| 822 | 945 |
| --- www/th1.md | |
| +++ www/th1.md | |
| @@ -68,11 +68,11 @@ | |
| 68 | are removed from each token by the command parser.) The third token |
| 69 | is the `puts "hello"`, with its whitespace and newlines. The fourth token |
| 70 | is `else` and the fifth and last token is `puts "world"`. |
| 71 | |
| 72 | The `if` command evaluates its first argument (the second token) |
| 73 | as an expression, and if that expression is true, evaluates its |
| 74 | second argument (the third token) as a TH1 script. |
| 75 | If the expression is false and the third argument is `else`, then |
| 76 | the fourth argument is evaluated as a TH1 expression. |
| 77 | |
| 78 | So, you see, even though the example above spans five lines, it is really |
| @@ -106,10 +106,49 @@ | |
| 106 | $repository "" info trunk]]] end] |
| 107 | |
| 108 | Those backslashes allow the command to wrap nicely within a standard |
| 109 | terminal width while telling the interpreter to consider those three |
| 110 | lines as a single command. |
| 111 | |
| 112 | |
| 113 | Summary of Core TH1 Commands |
| 114 | ---------------------------- |
| 115 | |
| @@ -147,10 +186,13 @@ | |
| 147 | * string last NEEDLE HAYSTACK ?START-INDEX? |
| 148 | * string match PATTERN STRING |
| 149 | * string length STRING |
| 150 | * string range STRING FIRST LAST |
| 151 | * string repeat STRING COUNT |
| 152 | * unset VARNAME |
| 153 | * uplevel ?LEVEL? SCRIPT |
| 154 | * upvar ?FRAME? OTHERVAR MYVAR ?OTHERVAR MYVAR? |
| 155 | |
| 156 | All of the above commands work as in the original Tcl. Refer to the |
| @@ -214,17 +256,19 @@ | |
| 214 | * [stime](#stime) |
| 215 | * [styleHeader](#styleHeader) |
| 216 | * [styleFooter](#styleFooter) |
| 217 | * [styleScript](#styleScript) |
| 218 | * [submenu](#submenu) |
| 219 | * [tclEval](#tclEval) |
| 220 | * [tclExpr](#tclExpr) |
| 221 | * [tclInvoke](#tclInvoke) |
| 222 | * [tclIsSafe](#tclIsSafe) |
| 223 | * [tclMakeSafe](#tclMakeSafe) |
| 224 | * [tclReady](#tclReady) |
| 225 | * [trace](#trace) |
| 226 | * [unversioned content](#unversioned_content) |
| 227 | * [unversioned list](#unversioned_list) |
| 228 | * [utime](#utime) |
| 229 | * [verifyCsrf](#verifyCsrf) |
| 230 | * [verifyLogin](#verifyLogin) |
| @@ -527,11 +571,25 @@ | |
| 527 | <a id="html"></a>TH1 html Command |
| 528 | ----------------------------------- |
| 529 | |
| 530 | * html STRING |
| 531 | |
| 532 | Outputs the STRING escaped for HTML. |
| 533 | |
| 534 | <a id="htmlize"></a>TH1 htmlize Command |
| 535 | ----------------------------------------- |
| 536 | |
| 537 | * htmlize STRING |
| @@ -595,12 +653,16 @@ | |
| 595 | <a id="puts"></a>TH1 puts Command |
| 596 | ----------------------------------- |
| 597 | |
| 598 | * puts STRING |
| 599 | |
| 600 | Outputs the STRING unchanged, where "unchanged" might, depending on |
| 601 | the context, mean "with some characters escaped for HTML." |
| 602 | |
| 603 | <a id="query"></a>TH1 query Command |
| 604 | ------------------------------------- |
| 605 | |
| 606 | * query ?-nocomplain? SQL CODE |
| @@ -608,11 +670,44 @@ | |
| 608 | Runs the SQL query given by the SQL argument. For each row in the result |
| 609 | set, run CODE. |
| 610 | |
| 611 | In SQL, parameters such as $var are filled in using the value of variable |
| 612 | "var". Result values are stored in variables with the column name prior |
| 613 | to each invocation of CODE. |
| 614 | |
| 615 | <a id="randhex"></a>TH1 randhex Command |
| 616 | ----------------------------------------- |
| 617 | |
| 618 | * randhex N |
| @@ -638,10 +733,12 @@ | |
| 638 | * regexp ?-nocase? ?--? exp string |
| 639 | |
| 640 | Checks the string against the specified regular expression and returns |
| 641 | non-zero if it matches. If the regular expression is invalid or cannot |
| 642 | be compiled, an error will be generated. |
| 643 | |
| 644 | <a id="reinitialize"></a>TH1 reinitialize Command |
| 645 | --------------------------------------------------- |
| 646 | |
| 647 | * reinitialize ?FLAGS? |
| @@ -741,10 +838,24 @@ | |
| 741 | |
| 742 | * submenu link LABEL URL |
| 743 | |
| 744 | Add hyperlink to the submenu of the current page. |
| 745 | |
| 746 | <a id="tclEval"></a>TH1 tclEval Command |
| 747 | ----------------------------------------- |
| 748 | |
| 749 | **This command requires the Tcl integration feature.** |
| 750 | |
| @@ -812,10 +923,22 @@ | |
| 812 | |
| 813 | * trace STRING |
| 814 | |
| 815 | Generates a TH1 trace message if TH1 tracing is enabled. |
| 816 | |
| 817 | <a id="unversioned_content"></a>TH1 unversioned content Command |
| 818 | ----------------------------------------------------------------- |
| 819 | |
| 820 | * unversioned content FILENAME |
| 821 | |
| 822 |
| --- www/th1.md | |
| +++ www/th1.md | |
| @@ -68,11 +68,11 @@ | |
| 68 | are removed from each token by the command parser.) The third token |
| 69 | is the `puts "hello"`, with its whitespace and newlines. The fourth token |
| 70 | is `else` and the fifth and last token is `puts "world"`. |
| 71 | |
| 72 | The `if` command evaluates its first argument (the second token) |
| 73 | as an expression, and if that expression is true, it evaluates its |
| 74 | second argument (the third token) as a TH1 script. |
| 75 | If the expression is false and the third argument is `else`, then |
| 76 | the fourth argument is evaluated as a TH1 expression. |
| 77 | |
| 78 | So, you see, even though the example above spans five lines, it is really |
| @@ -106,10 +106,49 @@ | |
| 106 | $repository "" info trunk]]] end] |
| 107 | |
| 108 | Those backslashes allow the command to wrap nicely within a standard |
| 109 | terminal width while telling the interpreter to consider those three |
| 110 | lines as a single command. |
| 111 | |
| 112 | <a id="taint"></a>Tainted And Untainted Strings |
| 113 | ----------------------------------------------- |
| 114 | |
| 115 | Beginning with Fossil version 2.26 (circa 2025), TH1 distinguishes between |
| 116 | "tainted" and "untainted" strings. Tainted strings are strings that are |
| 117 | derived from user inputs that might contain text that is designed to subvert |
| 118 | the script. Untainted strings are known to come from secure sources and |
| 119 | are assumed to contain no malicious content. |
| 120 | |
| 121 | Beginning with Fossil version 2.26, and depending on the value of the |
| 122 | [vuln-report setting](/help?cmd=vuln-report), TH1 will prevent tainted |
| 123 | strings from being used in ways that might lead to XSS or SQL-injection |
| 124 | attacks. This feature helps to ensure that XSS and SQL-injection |
| 125 | vulnerabilities are not *accidentally* added to Fossil when |
| 126 | custom TH1 scripts for headers or footers or tickets are added to a |
| 127 | repository. Note that the tainted/untainted distinction in strings does |
| 128 | not make it impossible to introduce XSS and SQL-injections vulnerabilities |
| 129 | using poorly-written TH1 scripts; it just makes it more difficult and |
| 130 | less likely to happen by accident. Developers must still consider the |
| 131 | security implications TH1 customizations they add to Fossil, and take |
| 132 | appropriate precautions when writing custom TH1. Peer review of TH1 |
| 133 | script changes is encouraged. |
| 134 | |
| 135 | In Fossil version 2.26, if the vuln-report setting is set to "block" |
| 136 | or "fatal", the [html](#html) and [query](#query) TH1 commands will |
| 137 | fail with an error if their argument is a tainted string. This helps |
| 138 | to prevent XSS and SQL-injection attacks, respectively. Note that |
| 139 | the default value of the vuln-report setting is "log", which allows those |
| 140 | commands to continue working and only writes a warning message into the |
| 141 | error log. <b>Future versions of Fossil may change the default value |
| 142 | of the vuln-report setting to "block" or "fatal".</b> Fossil users |
| 143 | with customized TH1 scripts are encouraged to audit their customizations |
| 144 | and fix any potential vulnerabilities soon, so as to avoid breakage |
| 145 | caused by future upgrades. <b>Future versions of Fossil might also |
| 146 | place additional restrictions on the use of tainted strings.</b> |
| 147 | For example, it is likely that future versions of Fossil will disallow |
| 148 | using tainted strings as script, for example as the body of a "for" |
| 149 | loop or of a "proc". |
| 150 | |
| 151 | |
| 152 | Summary of Core TH1 Commands |
| 153 | ---------------------------- |
| 154 | |
| @@ -147,10 +186,13 @@ | |
| 186 | * string last NEEDLE HAYSTACK ?START-INDEX? |
| 187 | * string match PATTERN STRING |
| 188 | * string length STRING |
| 189 | * string range STRING FIRST LAST |
| 190 | * string repeat STRING COUNT |
| 191 | * string trim STRING |
| 192 | * string trimleft STRING |
| 193 | * string trimright STRING |
| 194 | * unset VARNAME |
| 195 | * uplevel ?LEVEL? SCRIPT |
| 196 | * upvar ?FRAME? OTHERVAR MYVAR ?OTHERVAR MYVAR? |
| 197 | |
| 198 | All of the above commands work as in the original Tcl. Refer to the |
| @@ -214,17 +256,19 @@ | |
| 256 | * [stime](#stime) |
| 257 | * [styleHeader](#styleHeader) |
| 258 | * [styleFooter](#styleFooter) |
| 259 | * [styleScript](#styleScript) |
| 260 | * [submenu](#submenu) |
| 261 | * [taint](#taintCmd) |
| 262 | * [tclEval](#tclEval) |
| 263 | * [tclExpr](#tclExpr) |
| 264 | * [tclInvoke](#tclInvoke) |
| 265 | * [tclIsSafe](#tclIsSafe) |
| 266 | * [tclMakeSafe](#tclMakeSafe) |
| 267 | * [tclReady](#tclReady) |
| 268 | * [trace](#trace) |
| 269 | * [untaint](#untaintCmd) |
| 270 | * [unversioned content](#unversioned_content) |
| 271 | * [unversioned list](#unversioned_list) |
| 272 | * [utime](#utime) |
| 273 | * [verifyCsrf](#verifyCsrf) |
| 274 | * [verifyLogin](#verifyLogin) |
| @@ -527,11 +571,25 @@ | |
| 571 | <a id="html"></a>TH1 html Command |
| 572 | ----------------------------------- |
| 573 | |
| 574 | * html STRING |
| 575 | |
| 576 | Outputs the STRING literally. It is assumed that STRING contains |
| 577 | valid HTML, or that if STRING contains any characters that are |
| 578 | significant to HTML (such as `<`, `>`, `'`, or `&`) have already |
| 579 | been escaped, perhaps by the [htmlize](#htmlize) command. Use the |
| 580 | [puts](#puts) command to output text that might contain unescaped |
| 581 | HTML markup. |
| 582 | |
| 583 | **Beware of XSS attacks!** If the STRING value to the html command |
| 584 | can be controlled by a hostile user, then he might be able to sneak |
| 585 | in malicious HTML or Javascript which could result in a |
| 586 | cross-site scripting (XSS) attack. Be careful that all text that |
| 587 | in STRING that might come from user input has been sanitized by the |
| 588 | [htmlize](#htmlize) command or similar. In recent versions of Fossil, |
| 589 | the STRING value must be [untainted](#taint) or else the "html" command |
| 590 | will fail. |
| 591 | |
| 592 | <a id="htmlize"></a>TH1 htmlize Command |
| 593 | ----------------------------------------- |
| 594 | |
| 595 | * htmlize STRING |
| @@ -595,12 +653,16 @@ | |
| 653 | <a id="puts"></a>TH1 puts Command |
| 654 | ----------------------------------- |
| 655 | |
| 656 | * puts STRING |
| 657 | |
| 658 | Outputs STRING. Characters within STRING that have special meaning |
| 659 | in HTML are escaped prior to being output. Thus is it safe for STRING |
| 660 | to be derived from user inputs. See also the [html](#html) command |
| 661 | which behaves similarly except does not escape HTML markup. This |
| 662 | command ("puts") is safe to use on [tainted strings](#taint), but the "html" |
| 663 | command is not. |
| 664 | |
| 665 | <a id="query"></a>TH1 query Command |
| 666 | ------------------------------------- |
| 667 | |
| 668 | * query ?-nocomplain? SQL CODE |
| @@ -608,11 +670,44 @@ | |
| 670 | Runs the SQL query given by the SQL argument. For each row in the result |
| 671 | set, run CODE. |
| 672 | |
| 673 | In SQL, parameters such as $var are filled in using the value of variable |
| 674 | "var". Result values are stored in variables with the column name prior |
| 675 | to each invocation of CODE. The names of the variables in which results |
| 676 | are stored can be controlled using "AS name" clauses in the SQL. As |
| 677 | the database will often contain content that originates from untrusted |
| 678 | users, all result values are marked as [tainted](#taint). |
| 679 | |
| 680 | **Beware of SQL injections in the `query` command!** |
| 681 | The SQL argument to the query command should always be literal SQL |
| 682 | text enclosed in {...}. The SQL argument should never be a double-quoted |
| 683 | string or the value of a \$variable, as those constructs can lead to |
| 684 | an SQL Injection attack. If you need to include the values of one or |
| 685 | more TH1 variables as part of the SQL, then put \$variable inside the |
| 686 | {...}. The \$variable keyword will then get passed down into the SQLite |
| 687 | parser which knows to look up the value of \$variable in the TH1 symbol |
| 688 | table. For example: |
| 689 | |
| 690 | ~~~ |
| 691 | query {SELECT res FROM tab1 WHERE key=$mykey} {...} |
| 692 | ~~~ |
| 693 | |
| 694 | SQLite will see the \$mykey token in the SQL and will know to resolve it |
| 695 | to the value of the "mykey" TH1 variable, safely and without the possibility |
| 696 | of SQL injection. The following is unsafe: |
| 697 | |
| 698 | ~~~ |
| 699 | query "SELECT res FROM tab1 WHERE key='$mykey'" {...} ;# <-- UNSAFE! |
| 700 | ~~~ |
| 701 | |
| 702 | In this second example, TH1 does the expansion of `$mykey` prior to passing |
| 703 | the text down into SQLite. So if `$mykey` contains a single-quote character, |
| 704 | followed by additional hostile text, that will result in an SQL injection. |
| 705 | |
| 706 | To help guard against SQL-injections, recent versions of Fossil require |
| 707 | that the SQL argument be [untainted](#taint) or else the "query" command |
| 708 | will fail. |
| 709 | |
| 710 | <a id="randhex"></a>TH1 randhex Command |
| 711 | ----------------------------------------- |
| 712 | |
| 713 | * randhex N |
| @@ -638,10 +733,12 @@ | |
| 733 | * regexp ?-nocase? ?--? exp string |
| 734 | |
| 735 | Checks the string against the specified regular expression and returns |
| 736 | non-zero if it matches. If the regular expression is invalid or cannot |
| 737 | be compiled, an error will be generated. |
| 738 | |
| 739 | See [fossil grep](./grep.md) for details on the regexp syntax. |
| 740 | |
| 741 | <a id="reinitialize"></a>TH1 reinitialize Command |
| 742 | --------------------------------------------------- |
| 743 | |
| 744 | * reinitialize ?FLAGS? |
| @@ -741,10 +838,24 @@ | |
| 838 | |
| 839 | * submenu link LABEL URL |
| 840 | |
| 841 | Add hyperlink to the submenu of the current page. |
| 842 | |
| 843 | <a id="taintCmd"></a>TH1 taint Command |
| 844 | ----------------------------------------- |
| 845 | |
| 846 | * taint STRING |
| 847 | |
| 848 | This command returns a copy of STRING that has been marked as |
| 849 | [tainted](#taint). Tainted strings are strings which might be |
| 850 | controlled by an attacker and might contain hostile inputs and |
| 851 | are thus unsafe to use in certain contexts. For example, tainted |
| 852 | strings should not be output as part of a webpage as they might |
| 853 | contain rogue HTML or Javascript that could lead to an XSS |
| 854 | vulnerability. Similarly, tainted strings should not be run as |
| 855 | SQL since they might contain an SQL-injection vulerability. |
| 856 | |
| 857 | <a id="tclEval"></a>TH1 tclEval Command |
| 858 | ----------------------------------------- |
| 859 | |
| 860 | **This command requires the Tcl integration feature.** |
| 861 | |
| @@ -812,10 +923,22 @@ | |
| 923 | |
| 924 | * trace STRING |
| 925 | |
| 926 | Generates a TH1 trace message if TH1 tracing is enabled. |
| 927 | |
| 928 | <a id="untaintCmd"></a>TH1 taint Command |
| 929 | ----------------------------------------- |
| 930 | |
| 931 | * untaint STRING |
| 932 | |
| 933 | This command returns a copy of STRING that has been marked as |
| 934 | [untainted](#taint). Untainted strings are strings which are |
| 935 | believed to be free of potentially hostile content. Use this |
| 936 | command with caution, as it overwrites the tainted-string protection |
| 937 | mechanisms that are built into TH1. If you do not understand all |
| 938 | the implications of executing this command, then do not use it. |
| 939 | |
| 940 | <a id="unversioned_content"></a>TH1 unversioned content Command |
| 941 | ----------------------------------------------------------------- |
| 942 | |
| 943 | * unversioned content FILENAME |
| 944 | |
| 945 |