Fossil SCM
A new implementation for "Forum" in which each forum post is an artifact. This merge includes lots of enhancements to email notification, backoffice, configuration, and other subsystems, all in support of the new forum artifacts. The forum feature is not complete nor bug-free but at this point it seems good enough to continue development on trunk.
Commit
99fcc43f5d57ee530dd94aabf799ce0d3dc74ce8edd97182b54bb7feb85a3e7b
Parent
030bf2002f64c3f…
38 files changed
+3
-8
+1
-3
+12
+10
-6
+24
-25
+45
+1
-1
+385
-170
+921
-326
+17
+11
-23
+128
-77
+24
-13
+13
+1
+281
-86
+29
-2
+19
-3
+1
-1
+4
+31
-2
+77
-8
+56
-11
+6
-83
+20
+1
-1
+2
-2
+142
-83
+28
-27
+163
-60
+15
-4
+2
-1
+4
-3
+1
-1
+10
-4
+13
+11
+130
-4
~
src/attach.c
~
src/backoffice.c
~
src/capabilities.c
~
src/cgi.c
~
src/db.c
~
src/default_css.txt
~
src/dispatch.c
~
src/email.c
~
src/forum.c
~
src/forum.js
~
src/info.c
~
src/login.c
~
src/main.c
~
src/main.mk
~
src/makemake.tcl
~
src/manifest.c
~
src/moderate.c
~
src/name.c
~
src/popen.c
~
src/printf.c
~
src/schema.c
~
src/search.c
~
src/security_audit.c
~
src/setup.c
~
src/shell.c
~
src/skins.c
~
src/smtp.c
~
src/sqlite3.c
~
src/sqlite3.h
~
src/style.c
~
src/timeline.c
~
src/webmail.c
~
src/wiki.c
~
src/xfer.c
~
win/Makefile.dmc
~
win/Makefile.mingw
~
win/Makefile.msc
~
www/fileformat.wiki
+3
-8
| --- src/attach.c | ||
| +++ src/attach.c | ||
| @@ -106,15 +106,13 @@ | ||
| 106 | 106 | }else{ |
| 107 | 107 | zUrlTail = mprintf("page=%t&file=%t", zTarget, zFilename); |
| 108 | 108 | } |
| 109 | 109 | @ <li><p> |
| 110 | 110 | @ Attachment %z(href("%R/ainfo/%!S",zUuid))%S(zUuid)</a> |
| 111 | - if( moderation_pending(attachid) ){ | |
| 112 | - @ <span class="modpending">*** Awaiting Moderator Approval ***</span> | |
| 113 | - } | |
| 111 | + moderation_pending_www(attachid); | |
| 114 | 112 | @ <br /><a href="%R/attachview?%s(zUrlTail)">%h(zFilename)</a> |
| 115 | - @ [<a href="%R/attachdownload/%t(zFilename)?%s(zUrlTail)">download</a>]<br /> | |
| 113 | + @ [<a href="%R/attachdownload/%t(zFilename)?%s(zUrlTail)">download</a>]<br> | |
| 116 | 114 | if( zComment ) while( fossil_isspace(zComment[0]) ) zComment++; |
| 117 | 115 | if( zComment && zComment[0] ){ |
| 118 | 116 | @ %!W(zComment)<br /> |
| 119 | 117 | } |
| 120 | 118 | if( zPage==0 && zTkt==0 && zTechNote==0 ){ |
| @@ -564,14 +562,11 @@ | ||
| 564 | 562 | @ <tr><th>Artifact ID:</th> |
| 565 | 563 | @ <td>%z(href("%R/artifact/%!S",zUuid))%s(zUuid)</a> |
| 566 | 564 | if( g.perm.Setup ){ |
| 567 | 565 | @ (%d(rid)) |
| 568 | 566 | } |
| 569 | - modPending = moderation_pending(rid); | |
| 570 | - if( modPending ){ | |
| 571 | - @ <span class="modpending">*** Awaiting Moderator Approval ***</span> | |
| 572 | - } | |
| 567 | + modPending = moderation_pending_www(rid); | |
| 573 | 568 | if( zTktUuid ){ |
| 574 | 569 | @ <tr><th>Ticket:</th> |
| 575 | 570 | @ <td>%z(href("%R/tktview/%s",zTktUuid))%s(zTktUuid)</a></td></tr> |
| 576 | 571 | } |
| 577 | 572 | if( zTNUuid ){ |
| 578 | 573 |
| --- src/attach.c | |
| +++ src/attach.c | |
| @@ -106,15 +106,13 @@ | |
| 106 | }else{ |
| 107 | zUrlTail = mprintf("page=%t&file=%t", zTarget, zFilename); |
| 108 | } |
| 109 | @ <li><p> |
| 110 | @ Attachment %z(href("%R/ainfo/%!S",zUuid))%S(zUuid)</a> |
| 111 | if( moderation_pending(attachid) ){ |
| 112 | @ <span class="modpending">*** Awaiting Moderator Approval ***</span> |
| 113 | } |
| 114 | @ <br /><a href="%R/attachview?%s(zUrlTail)">%h(zFilename)</a> |
| 115 | @ [<a href="%R/attachdownload/%t(zFilename)?%s(zUrlTail)">download</a>]<br /> |
| 116 | if( zComment ) while( fossil_isspace(zComment[0]) ) zComment++; |
| 117 | if( zComment && zComment[0] ){ |
| 118 | @ %!W(zComment)<br /> |
| 119 | } |
| 120 | if( zPage==0 && zTkt==0 && zTechNote==0 ){ |
| @@ -564,14 +562,11 @@ | |
| 564 | @ <tr><th>Artifact ID:</th> |
| 565 | @ <td>%z(href("%R/artifact/%!S",zUuid))%s(zUuid)</a> |
| 566 | if( g.perm.Setup ){ |
| 567 | @ (%d(rid)) |
| 568 | } |
| 569 | modPending = moderation_pending(rid); |
| 570 | if( modPending ){ |
| 571 | @ <span class="modpending">*** Awaiting Moderator Approval ***</span> |
| 572 | } |
| 573 | if( zTktUuid ){ |
| 574 | @ <tr><th>Ticket:</th> |
| 575 | @ <td>%z(href("%R/tktview/%s",zTktUuid))%s(zTktUuid)</a></td></tr> |
| 576 | } |
| 577 | if( zTNUuid ){ |
| 578 |
| --- src/attach.c | |
| +++ src/attach.c | |
| @@ -106,15 +106,13 @@ | |
| 106 | }else{ |
| 107 | zUrlTail = mprintf("page=%t&file=%t", zTarget, zFilename); |
| 108 | } |
| 109 | @ <li><p> |
| 110 | @ Attachment %z(href("%R/ainfo/%!S",zUuid))%S(zUuid)</a> |
| 111 | moderation_pending_www(attachid); |
| 112 | @ <br /><a href="%R/attachview?%s(zUrlTail)">%h(zFilename)</a> |
| 113 | @ [<a href="%R/attachdownload/%t(zFilename)?%s(zUrlTail)">download</a>]<br> |
| 114 | if( zComment ) while( fossil_isspace(zComment[0]) ) zComment++; |
| 115 | if( zComment && zComment[0] ){ |
| 116 | @ %!W(zComment)<br /> |
| 117 | } |
| 118 | if( zPage==0 && zTkt==0 && zTechNote==0 ){ |
| @@ -564,14 +562,11 @@ | |
| 562 | @ <tr><th>Artifact ID:</th> |
| 563 | @ <td>%z(href("%R/artifact/%!S",zUuid))%s(zUuid)</a> |
| 564 | if( g.perm.Setup ){ |
| 565 | @ (%d(rid)) |
| 566 | } |
| 567 | modPending = moderation_pending_www(rid); |
| 568 | if( zTktUuid ){ |
| 569 | @ <tr><th>Ticket:</th> |
| 570 | @ <td>%z(href("%R/tktview/%s",zTktUuid))%s(zTktUuid)</a></td></tr> |
| 571 | } |
| 572 | if( zTNUuid ){ |
| 573 |
+1
-3
| --- src/backoffice.c | ||
| +++ src/backoffice.c | ||
| @@ -78,18 +78,16 @@ | ||
| 78 | 78 | ** otherwise taking a long time to complete. Set this when a user-visible |
| 79 | 79 | ** process might need to wait for backoffice to complete. |
| 80 | 80 | */ |
| 81 | 81 | static int backofficeNoDelay = 0; |
| 82 | 82 | |
| 83 | - | |
| 84 | 83 | /* |
| 85 | 84 | ** Disable the backoffice |
| 86 | 85 | */ |
| 87 | 86 | void backoffice_no_delay(void){ |
| 88 | 87 | backofficeNoDelay = 1; |
| 89 | 88 | } |
| 90 | - | |
| 91 | 89 | |
| 92 | 90 | /* |
| 93 | 91 | ** Parse a unsigned 64-bit integer from a string. Return a pointer |
| 94 | 92 | ** to the character of z[] that occurs after the integer. |
| 95 | 93 | */ |
| @@ -264,11 +262,11 @@ | ||
| 264 | 262 | getpid()); |
| 265 | 263 | } |
| 266 | 264 | backoffice_work(); |
| 267 | 265 | break; |
| 268 | 266 | } |
| 269 | - if( backofficeNoDelay ){ | |
| 267 | + if( backofficeNoDelay || db_get_boolean("backoffice-nodelay",1) ){ | |
| 270 | 268 | /* If the no-delay flag is set, exit immediately rather than queuing |
| 271 | 269 | ** up. Assume that some future request will come along and handle any |
| 272 | 270 | ** necessary backoffice work. */ |
| 273 | 271 | db_end_transaction(0); |
| 274 | 272 | break; |
| 275 | 273 | |
| 276 | 274 | ADDED src/capabilities.c |
| --- src/backoffice.c | |
| +++ src/backoffice.c | |
| @@ -78,18 +78,16 @@ | |
| 78 | ** otherwise taking a long time to complete. Set this when a user-visible |
| 79 | ** process might need to wait for backoffice to complete. |
| 80 | */ |
| 81 | static int backofficeNoDelay = 0; |
| 82 | |
| 83 | |
| 84 | /* |
| 85 | ** Disable the backoffice |
| 86 | */ |
| 87 | void backoffice_no_delay(void){ |
| 88 | backofficeNoDelay = 1; |
| 89 | } |
| 90 | |
| 91 | |
| 92 | /* |
| 93 | ** Parse a unsigned 64-bit integer from a string. Return a pointer |
| 94 | ** to the character of z[] that occurs after the integer. |
| 95 | */ |
| @@ -264,11 +262,11 @@ | |
| 264 | getpid()); |
| 265 | } |
| 266 | backoffice_work(); |
| 267 | break; |
| 268 | } |
| 269 | if( backofficeNoDelay ){ |
| 270 | /* If the no-delay flag is set, exit immediately rather than queuing |
| 271 | ** up. Assume that some future request will come along and handle any |
| 272 | ** necessary backoffice work. */ |
| 273 | db_end_transaction(0); |
| 274 | break; |
| 275 | |
| 276 | DDED src/capabilities.c |
| --- src/backoffice.c | |
| +++ src/backoffice.c | |
| @@ -78,18 +78,16 @@ | |
| 78 | ** otherwise taking a long time to complete. Set this when a user-visible |
| 79 | ** process might need to wait for backoffice to complete. |
| 80 | */ |
| 81 | static int backofficeNoDelay = 0; |
| 82 | |
| 83 | /* |
| 84 | ** Disable the backoffice |
| 85 | */ |
| 86 | void backoffice_no_delay(void){ |
| 87 | backofficeNoDelay = 1; |
| 88 | } |
| 89 | |
| 90 | /* |
| 91 | ** Parse a unsigned 64-bit integer from a string. Return a pointer |
| 92 | ** to the character of z[] that occurs after the integer. |
| 93 | */ |
| @@ -264,11 +262,11 @@ | |
| 262 | getpid()); |
| 263 | } |
| 264 | backoffice_work(); |
| 265 | break; |
| 266 | } |
| 267 | if( backofficeNoDelay || db_get_boolean("backoffice-nodelay",1) ){ |
| 268 | /* If the no-delay flag is set, exit immediately rather than queuing |
| 269 | ** up. Assume that some future request will come along and handle any |
| 270 | ** necessary backoffice work. */ |
| 271 | db_end_transaction(0); |
| 272 | break; |
| 273 | |
| 274 | DDED src/capabilities.c |
+12
| --- a/src/capabilities.c | ||
| +++ b/src/capabilities.c | ||
| @@ -0,0 +1,12 @@ | ||
| 1 | + Set or removSet or removSet or removSet or removemovSet or remov@ <table> | |
| 2 | +<i:</i> </tr> | |
| 3 | + }void){ | |
| 4 | + @ <table> | |
| 5 | + a</th> | |
| 6 | + @ <td><i>Admin:</i> </td></tr> | |
| 7 | + } | |
| 8 | +if( zReader==0 ){ | |
| 9 | + } | |
| 10 | +Reader)} | |
| 11 | +Anon); | |
| 12 | +Nobody); |
| --- a/src/capabilities.c | |
| +++ b/src/capabilities.c | |
| @@ -0,0 +1,12 @@ | |
| --- a/src/capabilities.c | |
| +++ b/src/capabilities.c | |
| @@ -0,0 +1,12 @@ | |
| 1 | Set or removSet or removSet or removSet or removemovSet or remov@ <table> |
| 2 | <i:</i> </tr> |
| 3 | }void){ |
| 4 | @ <table> |
| 5 | a</th> |
| 6 | @ <td><i>Admin:</i> </td></tr> |
| 7 | } |
| 8 | if( zReader==0 ){ |
| 9 | } |
| 10 | Reader)} |
| 11 | Anon); |
| 12 | Nobody); |
+10
-6
| --- src/cgi.c | ||
| +++ src/cgi.c | ||
| @@ -344,11 +344,11 @@ | ||
| 344 | 344 | |
| 345 | 345 | /* After the webpage has been sent, do any useful background |
| 346 | 346 | ** processing. |
| 347 | 347 | */ |
| 348 | 348 | g.cgiOutput = 2; |
| 349 | - if( g.db!=0 && iReplyStatus==200 ){ | |
| 349 | + if( g.db!=0 && iReplyStatus==200 && !g.fSshClient ){ | |
| 350 | 350 | fclose(g.httpOut); |
| 351 | 351 | #ifdef _WIN32 |
| 352 | 352 | g.httpOut = fossil_fopen("NUL", "wb"); |
| 353 | 353 | #else |
| 354 | 354 | g.httpOut = fossil_fopen("/dev/null", "wb"); |
| @@ -1120,23 +1120,27 @@ | ||
| 1120 | 1120 | return zDefault; |
| 1121 | 1121 | } |
| 1122 | 1122 | |
| 1123 | 1123 | /* |
| 1124 | 1124 | ** Return the value of a CGI parameter with leading and trailing |
| 1125 | -** spaces removed. | |
| 1125 | +** spaces removed and with internal \r\n changed to just \n | |
| 1126 | 1126 | */ |
| 1127 | 1127 | char *cgi_parameter_trimmed(const char *zName, const char *zDefault){ |
| 1128 | 1128 | const char *zIn; |
| 1129 | - char *zOut; | |
| 1130 | - int i; | |
| 1129 | + char *zOut, c; | |
| 1130 | + int i, j; | |
| 1131 | 1131 | zIn = cgi_parameter(zName, 0); |
| 1132 | 1132 | if( zIn==0 ) zIn = zDefault; |
| 1133 | 1133 | if( zIn==0 ) return 0; |
| 1134 | 1134 | while( fossil_isspace(zIn[0]) ) zIn++; |
| 1135 | 1135 | zOut = fossil_strdup(zIn); |
| 1136 | - for(i=0; zOut[i]; i++){} | |
| 1137 | - while( i>0 && fossil_isspace(zOut[i-1]) ) zOut[--i] = 0; | |
| 1136 | + for(i=j=0; (c = zOut[i])!=0; i++){ | |
| 1137 | + if( c=='\r' && zOut[i+1]=='\n' ) continue; | |
| 1138 | + zOut[j++] = c; | |
| 1139 | + } | |
| 1140 | + zOut[j] = 0; | |
| 1141 | + while( j>0 && fossil_isspace(zOut[j-1]) ) zOut[--j] = 0; | |
| 1138 | 1142 | return zOut; |
| 1139 | 1143 | } |
| 1140 | 1144 | |
| 1141 | 1145 | /* |
| 1142 | 1146 | ** Return true if the CGI parameter zName exists and is not equal to 0, |
| 1143 | 1147 |
| --- src/cgi.c | |
| +++ src/cgi.c | |
| @@ -344,11 +344,11 @@ | |
| 344 | |
| 345 | /* After the webpage has been sent, do any useful background |
| 346 | ** processing. |
| 347 | */ |
| 348 | g.cgiOutput = 2; |
| 349 | if( g.db!=0 && iReplyStatus==200 ){ |
| 350 | fclose(g.httpOut); |
| 351 | #ifdef _WIN32 |
| 352 | g.httpOut = fossil_fopen("NUL", "wb"); |
| 353 | #else |
| 354 | g.httpOut = fossil_fopen("/dev/null", "wb"); |
| @@ -1120,23 +1120,27 @@ | |
| 1120 | return zDefault; |
| 1121 | } |
| 1122 | |
| 1123 | /* |
| 1124 | ** Return the value of a CGI parameter with leading and trailing |
| 1125 | ** spaces removed. |
| 1126 | */ |
| 1127 | char *cgi_parameter_trimmed(const char *zName, const char *zDefault){ |
| 1128 | const char *zIn; |
| 1129 | char *zOut; |
| 1130 | int i; |
| 1131 | zIn = cgi_parameter(zName, 0); |
| 1132 | if( zIn==0 ) zIn = zDefault; |
| 1133 | if( zIn==0 ) return 0; |
| 1134 | while( fossil_isspace(zIn[0]) ) zIn++; |
| 1135 | zOut = fossil_strdup(zIn); |
| 1136 | for(i=0; zOut[i]; i++){} |
| 1137 | while( i>0 && fossil_isspace(zOut[i-1]) ) zOut[--i] = 0; |
| 1138 | return zOut; |
| 1139 | } |
| 1140 | |
| 1141 | /* |
| 1142 | ** Return true if the CGI parameter zName exists and is not equal to 0, |
| 1143 |
| --- src/cgi.c | |
| +++ src/cgi.c | |
| @@ -344,11 +344,11 @@ | |
| 344 | |
| 345 | /* After the webpage has been sent, do any useful background |
| 346 | ** processing. |
| 347 | */ |
| 348 | g.cgiOutput = 2; |
| 349 | if( g.db!=0 && iReplyStatus==200 && !g.fSshClient ){ |
| 350 | fclose(g.httpOut); |
| 351 | #ifdef _WIN32 |
| 352 | g.httpOut = fossil_fopen("NUL", "wb"); |
| 353 | #else |
| 354 | g.httpOut = fossil_fopen("/dev/null", "wb"); |
| @@ -1120,23 +1120,27 @@ | |
| 1120 | return zDefault; |
| 1121 | } |
| 1122 | |
| 1123 | /* |
| 1124 | ** Return the value of a CGI parameter with leading and trailing |
| 1125 | ** spaces removed and with internal \r\n changed to just \n |
| 1126 | */ |
| 1127 | char *cgi_parameter_trimmed(const char *zName, const char *zDefault){ |
| 1128 | const char *zIn; |
| 1129 | char *zOut, c; |
| 1130 | int i, j; |
| 1131 | zIn = cgi_parameter(zName, 0); |
| 1132 | if( zIn==0 ) zIn = zDefault; |
| 1133 | if( zIn==0 ) return 0; |
| 1134 | while( fossil_isspace(zIn[0]) ) zIn++; |
| 1135 | zOut = fossil_strdup(zIn); |
| 1136 | for(i=j=0; (c = zOut[i])!=0; i++){ |
| 1137 | if( c=='\r' && zOut[i+1]=='\n' ) continue; |
| 1138 | zOut[j++] = c; |
| 1139 | } |
| 1140 | zOut[j] = 0; |
| 1141 | while( j>0 && fossil_isspace(zOut[j-1]) ) zOut[--j] = 0; |
| 1142 | return zOut; |
| 1143 | } |
| 1144 | |
| 1145 | /* |
| 1146 | ** Return true if the CGI parameter zName exists and is not equal to 0, |
| 1147 |
M
src/db.c
+24
-25
| --- src/db.c | ||
| +++ src/db.c | ||
| @@ -73,11 +73,10 @@ | ||
| 73 | 73 | */ |
| 74 | 74 | static void db_err(const char *zFormat, ...){ |
| 75 | 75 | static int rcLooping = 0; |
| 76 | 76 | va_list ap; |
| 77 | 77 | char *z; |
| 78 | - int rc = 1; | |
| 79 | 78 | if( rcLooping ) exit(rcLooping); |
| 80 | 79 | va_start(ap, zFormat); |
| 81 | 80 | z = vmprintf(zFormat, ap); |
| 82 | 81 | va_end(ap); |
| 83 | 82 | #ifdef FOSSIL_ENABLE_JSON |
| @@ -87,26 +86,16 @@ | ||
| 87 | 86 | rc = 0 /* avoid HTTP 500 */; |
| 88 | 87 | } |
| 89 | 88 | } |
| 90 | 89 | else |
| 91 | 90 | #endif /* FOSSIL_ENABLE_JSON */ |
| 92 | - if( g.xferPanic ){ | |
| 91 | + if( g.xferPanic && g.cgiOutput==1 ){ | |
| 93 | 92 | cgi_reset_content(); |
| 94 | 93 | @ error Database\serror:\s%F(z) |
| 95 | - cgi_reply(); | |
| 96 | - } | |
| 97 | - else if( g.cgiOutput ){ | |
| 98 | - g.cgiOutput = 0; | |
| 99 | - cgi_printf("<h1>Database Error</h1>\n<p>%h</p>\n", z); | |
| 100 | - cgi_reply(); | |
| 101 | - }else{ | |
| 102 | - fprintf(stderr, "%s: %s\n", g.argv[0], z); | |
| 103 | - } | |
| 104 | - free(z); | |
| 105 | - rcLooping = rc; | |
| 106 | - db_force_rollback(); | |
| 107 | - fossil_exit(rc); | |
| 94 | + cgi_reply(); | |
| 95 | + } | |
| 96 | + fossil_panic("Database error: %s", z); | |
| 108 | 97 | } |
| 109 | 98 | |
| 110 | 99 | /* |
| 111 | 100 | ** All static variable that a used by only this file are gathered into |
| 112 | 101 | ** the following structure. |
| @@ -492,15 +481,10 @@ | ||
| 492 | 481 | db_check_result(rc); |
| 493 | 482 | return rc; |
| 494 | 483 | } |
| 495 | 484 | int db_finalize(Stmt *pStmt){ |
| 496 | 485 | int rc; |
| 497 | - db_stats(pStmt); | |
| 498 | - blob_reset(&pStmt->sql); | |
| 499 | - rc = sqlite3_finalize(pStmt->pStmt); | |
| 500 | - db_check_result(rc); | |
| 501 | - pStmt->pStmt = 0; | |
| 502 | 486 | if( pStmt->pNext ){ |
| 503 | 487 | pStmt->pNext->pPrev = pStmt->pPrev; |
| 504 | 488 | } |
| 505 | 489 | if( pStmt->pPrev ){ |
| 506 | 490 | pStmt->pPrev->pNext = pStmt->pNext; |
| @@ -507,10 +491,15 @@ | ||
| 507 | 491 | }else if( db.pAllStmt==pStmt ){ |
| 508 | 492 | db.pAllStmt = pStmt->pNext; |
| 509 | 493 | } |
| 510 | 494 | pStmt->pNext = 0; |
| 511 | 495 | pStmt->pPrev = 0; |
| 496 | + db_stats(pStmt); | |
| 497 | + blob_reset(&pStmt->sql); | |
| 498 | + rc = sqlite3_finalize(pStmt->pStmt); | |
| 499 | + db_check_result(rc); | |
| 500 | + pStmt->pStmt = 0; | |
| 512 | 501 | return rc; |
| 513 | 502 | } |
| 514 | 503 | |
| 515 | 504 | /* |
| 516 | 505 | ** Return the rowid of the most recent insert |
| @@ -1012,10 +1001,14 @@ | ||
| 1012 | 1001 | db_tolocal_function, 0, 0); |
| 1013 | 1002 | sqlite3_create_function(db, "fromLocal", 0, SQLITE_UTF8, 0, |
| 1014 | 1003 | db_fromlocal_function, 0, 0); |
| 1015 | 1004 | sqlite3_create_function(db, "hextoblob", 1, SQLITE_UTF8, 0, |
| 1016 | 1005 | db_hextoblob, 0, 0); |
| 1006 | + sqlite3_create_function(db, "capunion", 1, SQLITE_UTF8, 0, | |
| 1007 | + 0, capability_union_step, capability_union_finalize); | |
| 1008 | + sqlite3_create_function(db, "fullcap", 1, SQLITE_UTF8, 0, | |
| 1009 | + capability_fullcap, 0, 0); | |
| 1017 | 1010 | } |
| 1018 | 1011 | |
| 1019 | 1012 | #if USE_SEE |
| 1020 | 1013 | /* |
| 1021 | 1014 | ** This is a pointer to the saved database encryption key string. |
| @@ -1632,22 +1625,22 @@ | ||
| 1632 | 1625 | if( file_access(zDbName, R_OK) || file_size(zDbName, ExtFILE)<1024 ){ |
| 1633 | 1626 | if( file_access(zDbName, F_OK) ){ |
| 1634 | 1627 | #ifdef FOSSIL_ENABLE_JSON |
| 1635 | 1628 | g.json.resultCode = FSL_JSON_E_DB_NOT_FOUND; |
| 1636 | 1629 | #endif |
| 1637 | - fossil_panic("repository does not exist or" | |
| 1630 | + fossil_fatal("repository does not exist or" | |
| 1638 | 1631 | " is in an unreadable directory: %s", zDbName); |
| 1639 | 1632 | }else if( file_access(zDbName, R_OK) ){ |
| 1640 | 1633 | #ifdef FOSSIL_ENABLE_JSON |
| 1641 | 1634 | g.json.resultCode = FSL_JSON_E_DENIED; |
| 1642 | 1635 | #endif |
| 1643 | - fossil_panic("read permission denied for repository %s", zDbName); | |
| 1636 | + fossil_fatal("read permission denied for repository %s", zDbName); | |
| 1644 | 1637 | }else{ |
| 1645 | 1638 | #ifdef FOSSIL_ENABLE_JSON |
| 1646 | 1639 | g.json.resultCode = FSL_JSON_E_DB_NOT_VALID; |
| 1647 | 1640 | #endif |
| 1648 | - fossil_panic("not a valid repository: %s", zDbName); | |
| 1641 | + fossil_fatal("not a valid repository: %s", zDbName); | |
| 1649 | 1642 | } |
| 1650 | 1643 | } |
| 1651 | 1644 | g.zRepositoryName = mprintf("%s", zDbName); |
| 1652 | 1645 | db_open_or_attach(g.zRepositoryName, "repository"); |
| 1653 | 1646 | g.repositoryOpen = 1; |
| @@ -1725,13 +1718,13 @@ | ||
| 1725 | 1718 | if( (bFlags & OPEN_OK_NOT_FOUND)==0 ){ |
| 1726 | 1719 | #ifdef FOSSIL_ENABLE_JSON |
| 1727 | 1720 | g.json.resultCode = FSL_JSON_E_DB_NOT_FOUND; |
| 1728 | 1721 | #endif |
| 1729 | 1722 | if( nArgUsed==0 ){ |
| 1730 | - fossil_panic("use --repository or -R to specify the repository database"); | |
| 1723 | + fossil_fatal("use --repository or -R to specify the repository database"); | |
| 1731 | 1724 | }else{ |
| 1732 | - fossil_panic("specify the repository name as a command-line argument"); | |
| 1725 | + fossil_fatal("specify the repository name as a command-line argument"); | |
| 1733 | 1726 | } |
| 1734 | 1727 | } |
| 1735 | 1728 | } |
| 1736 | 1729 | |
| 1737 | 1730 | /* |
| @@ -3033,10 +3026,16 @@ | ||
| 3033 | 3026 | ** SETTING: autosync-tries width=16 default=1 |
| 3034 | 3027 | ** If autosync is enabled setting this to a value greater |
| 3035 | 3028 | ** than zero will cause autosync to try no more than this |
| 3036 | 3029 | ** number of attempts if there is a sync failure. |
| 3037 | 3030 | */ |
| 3031 | +/* | |
| 3032 | +** SETTING: backoffice-nodelay boolean default=on | |
| 3033 | +** If backoffice-nodelay is true, then the backoffice processing | |
| 3034 | +** will never invoke sleep(). If it has nothing useful to do, | |
| 3035 | +** it simply exits. | |
| 3036 | +*/ | |
| 3038 | 3037 | /* |
| 3039 | 3038 | ** SETTING: binary-glob width=40 versionable block-text |
| 3040 | 3039 | ** The VALUE of this setting is a comma or newline-separated list of |
| 3041 | 3040 | ** GLOB patterns that should be treated as binary files |
| 3042 | 3041 | ** for committing and merging purposes. Example: *.jpg |
| 3043 | 3042 |
| --- src/db.c | |
| +++ src/db.c | |
| @@ -73,11 +73,10 @@ | |
| 73 | */ |
| 74 | static void db_err(const char *zFormat, ...){ |
| 75 | static int rcLooping = 0; |
| 76 | va_list ap; |
| 77 | char *z; |
| 78 | int rc = 1; |
| 79 | if( rcLooping ) exit(rcLooping); |
| 80 | va_start(ap, zFormat); |
| 81 | z = vmprintf(zFormat, ap); |
| 82 | va_end(ap); |
| 83 | #ifdef FOSSIL_ENABLE_JSON |
| @@ -87,26 +86,16 @@ | |
| 87 | rc = 0 /* avoid HTTP 500 */; |
| 88 | } |
| 89 | } |
| 90 | else |
| 91 | #endif /* FOSSIL_ENABLE_JSON */ |
| 92 | if( g.xferPanic ){ |
| 93 | cgi_reset_content(); |
| 94 | @ error Database\serror:\s%F(z) |
| 95 | cgi_reply(); |
| 96 | } |
| 97 | else if( g.cgiOutput ){ |
| 98 | g.cgiOutput = 0; |
| 99 | cgi_printf("<h1>Database Error</h1>\n<p>%h</p>\n", z); |
| 100 | cgi_reply(); |
| 101 | }else{ |
| 102 | fprintf(stderr, "%s: %s\n", g.argv[0], z); |
| 103 | } |
| 104 | free(z); |
| 105 | rcLooping = rc; |
| 106 | db_force_rollback(); |
| 107 | fossil_exit(rc); |
| 108 | } |
| 109 | |
| 110 | /* |
| 111 | ** All static variable that a used by only this file are gathered into |
| 112 | ** the following structure. |
| @@ -492,15 +481,10 @@ | |
| 492 | db_check_result(rc); |
| 493 | return rc; |
| 494 | } |
| 495 | int db_finalize(Stmt *pStmt){ |
| 496 | int rc; |
| 497 | db_stats(pStmt); |
| 498 | blob_reset(&pStmt->sql); |
| 499 | rc = sqlite3_finalize(pStmt->pStmt); |
| 500 | db_check_result(rc); |
| 501 | pStmt->pStmt = 0; |
| 502 | if( pStmt->pNext ){ |
| 503 | pStmt->pNext->pPrev = pStmt->pPrev; |
| 504 | } |
| 505 | if( pStmt->pPrev ){ |
| 506 | pStmt->pPrev->pNext = pStmt->pNext; |
| @@ -507,10 +491,15 @@ | |
| 507 | }else if( db.pAllStmt==pStmt ){ |
| 508 | db.pAllStmt = pStmt->pNext; |
| 509 | } |
| 510 | pStmt->pNext = 0; |
| 511 | pStmt->pPrev = 0; |
| 512 | return rc; |
| 513 | } |
| 514 | |
| 515 | /* |
| 516 | ** Return the rowid of the most recent insert |
| @@ -1012,10 +1001,14 @@ | |
| 1012 | db_tolocal_function, 0, 0); |
| 1013 | sqlite3_create_function(db, "fromLocal", 0, SQLITE_UTF8, 0, |
| 1014 | db_fromlocal_function, 0, 0); |
| 1015 | sqlite3_create_function(db, "hextoblob", 1, SQLITE_UTF8, 0, |
| 1016 | db_hextoblob, 0, 0); |
| 1017 | } |
| 1018 | |
| 1019 | #if USE_SEE |
| 1020 | /* |
| 1021 | ** This is a pointer to the saved database encryption key string. |
| @@ -1632,22 +1625,22 @@ | |
| 1632 | if( file_access(zDbName, R_OK) || file_size(zDbName, ExtFILE)<1024 ){ |
| 1633 | if( file_access(zDbName, F_OK) ){ |
| 1634 | #ifdef FOSSIL_ENABLE_JSON |
| 1635 | g.json.resultCode = FSL_JSON_E_DB_NOT_FOUND; |
| 1636 | #endif |
| 1637 | fossil_panic("repository does not exist or" |
| 1638 | " is in an unreadable directory: %s", zDbName); |
| 1639 | }else if( file_access(zDbName, R_OK) ){ |
| 1640 | #ifdef FOSSIL_ENABLE_JSON |
| 1641 | g.json.resultCode = FSL_JSON_E_DENIED; |
| 1642 | #endif |
| 1643 | fossil_panic("read permission denied for repository %s", zDbName); |
| 1644 | }else{ |
| 1645 | #ifdef FOSSIL_ENABLE_JSON |
| 1646 | g.json.resultCode = FSL_JSON_E_DB_NOT_VALID; |
| 1647 | #endif |
| 1648 | fossil_panic("not a valid repository: %s", zDbName); |
| 1649 | } |
| 1650 | } |
| 1651 | g.zRepositoryName = mprintf("%s", zDbName); |
| 1652 | db_open_or_attach(g.zRepositoryName, "repository"); |
| 1653 | g.repositoryOpen = 1; |
| @@ -1725,13 +1718,13 @@ | |
| 1725 | if( (bFlags & OPEN_OK_NOT_FOUND)==0 ){ |
| 1726 | #ifdef FOSSIL_ENABLE_JSON |
| 1727 | g.json.resultCode = FSL_JSON_E_DB_NOT_FOUND; |
| 1728 | #endif |
| 1729 | if( nArgUsed==0 ){ |
| 1730 | fossil_panic("use --repository or -R to specify the repository database"); |
| 1731 | }else{ |
| 1732 | fossil_panic("specify the repository name as a command-line argument"); |
| 1733 | } |
| 1734 | } |
| 1735 | } |
| 1736 | |
| 1737 | /* |
| @@ -3033,10 +3026,16 @@ | |
| 3033 | ** SETTING: autosync-tries width=16 default=1 |
| 3034 | ** If autosync is enabled setting this to a value greater |
| 3035 | ** than zero will cause autosync to try no more than this |
| 3036 | ** number of attempts if there is a sync failure. |
| 3037 | */ |
| 3038 | /* |
| 3039 | ** SETTING: binary-glob width=40 versionable block-text |
| 3040 | ** The VALUE of this setting is a comma or newline-separated list of |
| 3041 | ** GLOB patterns that should be treated as binary files |
| 3042 | ** for committing and merging purposes. Example: *.jpg |
| 3043 |
| --- src/db.c | |
| +++ src/db.c | |
| @@ -73,11 +73,10 @@ | |
| 73 | */ |
| 74 | static void db_err(const char *zFormat, ...){ |
| 75 | static int rcLooping = 0; |
| 76 | va_list ap; |
| 77 | char *z; |
| 78 | if( rcLooping ) exit(rcLooping); |
| 79 | va_start(ap, zFormat); |
| 80 | z = vmprintf(zFormat, ap); |
| 81 | va_end(ap); |
| 82 | #ifdef FOSSIL_ENABLE_JSON |
| @@ -87,26 +86,16 @@ | |
| 86 | rc = 0 /* avoid HTTP 500 */; |
| 87 | } |
| 88 | } |
| 89 | else |
| 90 | #endif /* FOSSIL_ENABLE_JSON */ |
| 91 | if( g.xferPanic && g.cgiOutput==1 ){ |
| 92 | cgi_reset_content(); |
| 93 | @ error Database\serror:\s%F(z) |
| 94 | cgi_reply(); |
| 95 | } |
| 96 | fossil_panic("Database error: %s", z); |
| 97 | } |
| 98 | |
| 99 | /* |
| 100 | ** All static variable that a used by only this file are gathered into |
| 101 | ** the following structure. |
| @@ -492,15 +481,10 @@ | |
| 481 | db_check_result(rc); |
| 482 | return rc; |
| 483 | } |
| 484 | int db_finalize(Stmt *pStmt){ |
| 485 | int rc; |
| 486 | if( pStmt->pNext ){ |
| 487 | pStmt->pNext->pPrev = pStmt->pPrev; |
| 488 | } |
| 489 | if( pStmt->pPrev ){ |
| 490 | pStmt->pPrev->pNext = pStmt->pNext; |
| @@ -507,10 +491,15 @@ | |
| 491 | }else if( db.pAllStmt==pStmt ){ |
| 492 | db.pAllStmt = pStmt->pNext; |
| 493 | } |
| 494 | pStmt->pNext = 0; |
| 495 | pStmt->pPrev = 0; |
| 496 | db_stats(pStmt); |
| 497 | blob_reset(&pStmt->sql); |
| 498 | rc = sqlite3_finalize(pStmt->pStmt); |
| 499 | db_check_result(rc); |
| 500 | pStmt->pStmt = 0; |
| 501 | return rc; |
| 502 | } |
| 503 | |
| 504 | /* |
| 505 | ** Return the rowid of the most recent insert |
| @@ -1012,10 +1001,14 @@ | |
| 1001 | db_tolocal_function, 0, 0); |
| 1002 | sqlite3_create_function(db, "fromLocal", 0, SQLITE_UTF8, 0, |
| 1003 | db_fromlocal_function, 0, 0); |
| 1004 | sqlite3_create_function(db, "hextoblob", 1, SQLITE_UTF8, 0, |
| 1005 | db_hextoblob, 0, 0); |
| 1006 | sqlite3_create_function(db, "capunion", 1, SQLITE_UTF8, 0, |
| 1007 | 0, capability_union_step, capability_union_finalize); |
| 1008 | sqlite3_create_function(db, "fullcap", 1, SQLITE_UTF8, 0, |
| 1009 | capability_fullcap, 0, 0); |
| 1010 | } |
| 1011 | |
| 1012 | #if USE_SEE |
| 1013 | /* |
| 1014 | ** This is a pointer to the saved database encryption key string. |
| @@ -1632,22 +1625,22 @@ | |
| 1625 | if( file_access(zDbName, R_OK) || file_size(zDbName, ExtFILE)<1024 ){ |
| 1626 | if( file_access(zDbName, F_OK) ){ |
| 1627 | #ifdef FOSSIL_ENABLE_JSON |
| 1628 | g.json.resultCode = FSL_JSON_E_DB_NOT_FOUND; |
| 1629 | #endif |
| 1630 | fossil_fatal("repository does not exist or" |
| 1631 | " is in an unreadable directory: %s", zDbName); |
| 1632 | }else if( file_access(zDbName, R_OK) ){ |
| 1633 | #ifdef FOSSIL_ENABLE_JSON |
| 1634 | g.json.resultCode = FSL_JSON_E_DENIED; |
| 1635 | #endif |
| 1636 | fossil_fatal("read permission denied for repository %s", zDbName); |
| 1637 | }else{ |
| 1638 | #ifdef FOSSIL_ENABLE_JSON |
| 1639 | g.json.resultCode = FSL_JSON_E_DB_NOT_VALID; |
| 1640 | #endif |
| 1641 | fossil_fatal("not a valid repository: %s", zDbName); |
| 1642 | } |
| 1643 | } |
| 1644 | g.zRepositoryName = mprintf("%s", zDbName); |
| 1645 | db_open_or_attach(g.zRepositoryName, "repository"); |
| 1646 | g.repositoryOpen = 1; |
| @@ -1725,13 +1718,13 @@ | |
| 1718 | if( (bFlags & OPEN_OK_NOT_FOUND)==0 ){ |
| 1719 | #ifdef FOSSIL_ENABLE_JSON |
| 1720 | g.json.resultCode = FSL_JSON_E_DB_NOT_FOUND; |
| 1721 | #endif |
| 1722 | if( nArgUsed==0 ){ |
| 1723 | fossil_fatal("use --repository or -R to specify the repository database"); |
| 1724 | }else{ |
| 1725 | fossil_fatal("specify the repository name as a command-line argument"); |
| 1726 | } |
| 1727 | } |
| 1728 | } |
| 1729 | |
| 1730 | /* |
| @@ -3033,10 +3026,16 @@ | |
| 3026 | ** SETTING: autosync-tries width=16 default=1 |
| 3027 | ** If autosync is enabled setting this to a value greater |
| 3028 | ** than zero will cause autosync to try no more than this |
| 3029 | ** number of attempts if there is a sync failure. |
| 3030 | */ |
| 3031 | /* |
| 3032 | ** SETTING: backoffice-nodelay boolean default=on |
| 3033 | ** If backoffice-nodelay is true, then the backoffice processing |
| 3034 | ** will never invoke sleep(). If it has nothing useful to do, |
| 3035 | ** it simply exits. |
| 3036 | */ |
| 3037 | /* |
| 3038 | ** SETTING: binary-glob width=40 versionable block-text |
| 3039 | ** The VALUE of this setting is a comma or newline-separated list of |
| 3040 | ** GLOB patterns that should be treated as binary files |
| 3041 | ** for committing and merging purposes. Example: *.jpg |
| 3042 |
+45
| --- src/default_css.txt | ||
| +++ src/default_css.txt | ||
| @@ -534,10 +534,14 @@ | ||
| 534 | 534 | pre.th1error { |
| 535 | 535 | white-space: pre-wrap; |
| 536 | 536 | word-wrap: break-word; |
| 537 | 537 | color: red; |
| 538 | 538 | } |
| 539 | +pre.textPlain { | |
| 540 | + white-space: pre-wrap; | |
| 541 | + word-wrap: break-word; | |
| 542 | +} | |
| 539 | 543 | .statistics-report-graph-line { |
| 540 | 544 | background-color: #446979; |
| 541 | 545 | } |
| 542 | 546 | .statistics-report-table-events th { |
| 543 | 547 | padding: 0 1em 0 1em; |
| @@ -672,5 +676,46 @@ | ||
| 672 | 676 | } |
| 673 | 677 | td.form_label { |
| 674 | 678 | vertical-align: top; |
| 675 | 679 | text-align: right; |
| 676 | 680 | } |
| 681 | +.debug { | |
| 682 | + background-color: #ffc; | |
| 683 | + border: 2px solid #ff0; | |
| 684 | +} | |
| 685 | +div.forumEdit { | |
| 686 | + border: 1px solid black; | |
| 687 | + padding-left: 1ex; | |
| 688 | + padding-right: 1ex; | |
| 689 | +} | |
| 690 | +div.forumHier, div.forumTime { | |
| 691 | + border: 1px solid black; | |
| 692 | + padding-left: 1ex; | |
| 693 | + padding-right: 1ex; | |
| 694 | + margin-top: 1ex; | |
| 695 | +} | |
| 696 | +div.forumSel { | |
| 697 | + background-color: #cef; | |
| 698 | +} | |
| 699 | +div.forumObs { | |
| 700 | + color: #bbb; | |
| 701 | +} | |
| 702 | +#capabilitySummary { | |
| 703 | + text-align: center; | |
| 704 | +} | |
| 705 | +#capabilitySummary td { | |
| 706 | + padding-left: 3ex; | |
| 707 | + padding-right: 3ex; | |
| 708 | +} | |
| 709 | +#capabilitySummary th { | |
| 710 | + padding-left: 1ex; | |
| 711 | + padding-right: 1ex; | |
| 712 | +} | |
| 713 | +.capsumOff { | |
| 714 | + background-color: #bbb; | |
| 715 | +} | |
| 716 | +.capsumRead { | |
| 717 | + background-color: #bfb; | |
| 718 | +} | |
| 719 | +.capsumWrite { | |
| 720 | + background-color: #ffb; | |
| 721 | +} | |
| 677 | 722 |
| --- src/default_css.txt | |
| +++ src/default_css.txt | |
| @@ -534,10 +534,14 @@ | |
| 534 | pre.th1error { |
| 535 | white-space: pre-wrap; |
| 536 | word-wrap: break-word; |
| 537 | color: red; |
| 538 | } |
| 539 | .statistics-report-graph-line { |
| 540 | background-color: #446979; |
| 541 | } |
| 542 | .statistics-report-table-events th { |
| 543 | padding: 0 1em 0 1em; |
| @@ -672,5 +676,46 @@ | |
| 672 | } |
| 673 | td.form_label { |
| 674 | vertical-align: top; |
| 675 | text-align: right; |
| 676 | } |
| 677 |
| --- src/default_css.txt | |
| +++ src/default_css.txt | |
| @@ -534,10 +534,14 @@ | |
| 534 | pre.th1error { |
| 535 | white-space: pre-wrap; |
| 536 | word-wrap: break-word; |
| 537 | color: red; |
| 538 | } |
| 539 | pre.textPlain { |
| 540 | white-space: pre-wrap; |
| 541 | word-wrap: break-word; |
| 542 | } |
| 543 | .statistics-report-graph-line { |
| 544 | background-color: #446979; |
| 545 | } |
| 546 | .statistics-report-table-events th { |
| 547 | padding: 0 1em 0 1em; |
| @@ -672,5 +676,46 @@ | |
| 676 | } |
| 677 | td.form_label { |
| 678 | vertical-align: top; |
| 679 | text-align: right; |
| 680 | } |
| 681 | .debug { |
| 682 | background-color: #ffc; |
| 683 | border: 2px solid #ff0; |
| 684 | } |
| 685 | div.forumEdit { |
| 686 | border: 1px solid black; |
| 687 | padding-left: 1ex; |
| 688 | padding-right: 1ex; |
| 689 | } |
| 690 | div.forumHier, div.forumTime { |
| 691 | border: 1px solid black; |
| 692 | padding-left: 1ex; |
| 693 | padding-right: 1ex; |
| 694 | margin-top: 1ex; |
| 695 | } |
| 696 | div.forumSel { |
| 697 | background-color: #cef; |
| 698 | } |
| 699 | div.forumObs { |
| 700 | color: #bbb; |
| 701 | } |
| 702 | #capabilitySummary { |
| 703 | text-align: center; |
| 704 | } |
| 705 | #capabilitySummary td { |
| 706 | padding-left: 3ex; |
| 707 | padding-right: 3ex; |
| 708 | } |
| 709 | #capabilitySummary th { |
| 710 | padding-left: 1ex; |
| 711 | padding-right: 1ex; |
| 712 | } |
| 713 | .capsumOff { |
| 714 | background-color: #bbb; |
| 715 | } |
| 716 | .capsumRead { |
| 717 | background-color: #bfb; |
| 718 | } |
| 719 | .capsumWrite { |
| 720 | background-color: #ffb; |
| 721 | } |
| 722 |
+1
-1
| --- src/dispatch.c | ||
| +++ src/dispatch.c | ||
| @@ -167,11 +167,11 @@ | ||
| 167 | 167 | zQ = &z[i+1]; |
| 168 | 168 | }else{ |
| 169 | 169 | zQ = &z[i]; |
| 170 | 170 | } |
| 171 | 171 | if( dispatch_name_search(z, CMDFLAG_WEBPAGE, ppCmd) ){ |
| 172 | - fossil_panic("\"%s\" aliased to \"%s\" but \"%s\" does not exist", | |
| 172 | + fossil_fatal("\"%s\" aliased to \"%s\" but \"%s\" does not exist", | |
| 173 | 173 | zName, z, z); |
| 174 | 174 | } |
| 175 | 175 | z = zQ; |
| 176 | 176 | while( *z ){ |
| 177 | 177 | char *zName = z; |
| 178 | 178 |
| --- src/dispatch.c | |
| +++ src/dispatch.c | |
| @@ -167,11 +167,11 @@ | |
| 167 | zQ = &z[i+1]; |
| 168 | }else{ |
| 169 | zQ = &z[i]; |
| 170 | } |
| 171 | if( dispatch_name_search(z, CMDFLAG_WEBPAGE, ppCmd) ){ |
| 172 | fossil_panic("\"%s\" aliased to \"%s\" but \"%s\" does not exist", |
| 173 | zName, z, z); |
| 174 | } |
| 175 | z = zQ; |
| 176 | while( *z ){ |
| 177 | char *zName = z; |
| 178 |
| --- src/dispatch.c | |
| +++ src/dispatch.c | |
| @@ -167,11 +167,11 @@ | |
| 167 | zQ = &z[i+1]; |
| 168 | }else{ |
| 169 | zQ = &z[i]; |
| 170 | } |
| 171 | if( dispatch_name_search(z, CMDFLAG_WEBPAGE, ppCmd) ){ |
| 172 | fossil_fatal("\"%s\" aliased to \"%s\" but \"%s\" does not exist", |
| 173 | zName, z, z); |
| 174 | } |
| 175 | z = zQ; |
| 176 | while( *z ){ |
| 177 | char *zName = z; |
| 178 |
+385
-170
| --- src/email.c | ||
| +++ src/email.c | ||
| @@ -1,7 +1,7 @@ | ||
| 1 | 1 | /* |
| 2 | -** Copyright (c) 2007 D. Richard Hipp | |
| 2 | +** Copyright (c) 2018 D. Richard Hipp | |
| 3 | 3 | ** |
| 4 | 4 | ** This program is free software; you can redistribute it and/or |
| 5 | 5 | ** modify it under the terms of the Simplified BSD License (also |
| 6 | 6 | ** known as the "2-Clause License" or "FreeBSD License".) |
| 7 | 7 | ** |
| @@ -14,11 +14,16 @@ | ||
| 14 | 14 | ** http://www.hwaci.com/drh/ |
| 15 | 15 | ** |
| 16 | 16 | ******************************************************************************* |
| 17 | 17 | ** |
| 18 | 18 | ** Logic for email notification, also known as "alerts". |
| 19 | -*/ | |
| 19 | +** | |
| 20 | +** Are you looking for the code that reads and writes the internet | |
| 21 | +** email protocol? That is not here. See the "smtp.c" file instead. | |
| 22 | +** Yes, the choice of source code filenames is not the greatest, but | |
| 23 | +** it is not so bad that changing them seems justified. | |
| 24 | +*/ | |
| 20 | 25 | #include "config.h" |
| 21 | 26 | #include "email.h" |
| 22 | 27 | #include <assert.h> |
| 23 | 28 | #include <time.h> |
| 24 | 29 | |
| @@ -72,12 +77,13 @@ | ||
| 72 | 77 | @ -- Remaining characters determine the specific event. For example, |
| 73 | 78 | @ -- 'c4413' means check-in with rid=4413. |
| 74 | 79 | @ -- |
| 75 | 80 | @ CREATE TABLE repository.pending_alert( |
| 76 | 81 | @ eventid TEXT PRIMARY KEY, -- Object that changed |
| 77 | -@ sentSep BOOLEAN DEFAULT false, -- individual emails sent | |
| 78 | -@ sentDigest BOOLEAN DEFAULT false -- digest emails sent | |
| 82 | +@ sentSep BOOLEAN DEFAULT false, -- individual alert sent | |
| 83 | +@ sentDigest BOOLEAN DEFAULT false, -- digest alert sent | |
| 84 | +@ sentMod BOOLEAN DEFAULT false -- pending moderation alert sent | |
| 79 | 85 | @ ) WITHOUT ROWID; |
| 80 | 86 | @ |
| 81 | 87 | @ DROP TABLE IF EXISTS repository.email_bounce; |
| 82 | 88 | @ -- Record bounced emails. If too many bounces are received within |
| 83 | 89 | @ -- some defined time range, then cancel the subscription. Older |
| @@ -110,10 +116,15 @@ | ||
| 110 | 116 | ){ |
| 111 | 117 | return; /* Don't create table for disabled email */ |
| 112 | 118 | } |
| 113 | 119 | db_multi_exec(zEmailInit/*works-like:""*/); |
| 114 | 120 | email_triggers_enable(); |
| 121 | + }else if( !db_table_has_column("repository","pending_alert","sentMod") ){ | |
| 122 | + db_multi_exec( | |
| 123 | + "ALTER TABLE repository.pending_alert" | |
| 124 | + " ADD COLUMN sentMod BOOLEAN DEFAULT false;" | |
| 125 | + ); | |
| 115 | 126 | } |
| 116 | 127 | } |
| 117 | 128 | |
| 118 | 129 | /* |
| 119 | 130 | ** Enable triggers that automatically populate the pending_alert |
| @@ -289,18 +300,10 @@ | ||
| 289 | 300 | @ <p>This is the email for the human administrator for the system. |
| 290 | 301 | @ Abuse and trouble reports are send here. |
| 291 | 302 | @ (Property: "email-admin")</p> |
| 292 | 303 | @ <hr> |
| 293 | 304 | |
| 294 | - entry_attribute("Inbound email directory", 40, "email-receive-dir", | |
| 295 | - "erdir", "", 0); | |
| 296 | - @ <p>Inbound emails can be stored in a directory for analysis as | |
| 297 | - @ a debugging aid. Put the name of that directory in this entry box. | |
| 298 | - @ Disable saving of inbound email by making this an empty string. | |
| 299 | - @ Abuse and trouble reports are send here. | |
| 300 | - @ (Property: "email-receive-dir")</p> | |
| 301 | - @ <hr> | |
| 302 | 305 | @ <p><input type="submit" name="submit" value="Apply Changes" /></p> |
| 303 | 306 | @ </div></form> |
| 304 | 307 | db_end_transaction(0); |
| 305 | 308 | style_footer(); |
| 306 | 309 | } |
| @@ -568,27 +571,27 @@ | ||
| 568 | 571 | return 0; |
| 569 | 572 | } |
| 570 | 573 | |
| 571 | 574 | /* |
| 572 | 575 | ** Make a copy of the input string up to but not including the |
| 573 | -** first ">" character. | |
| 576 | +** first cTerm character. | |
| 574 | 577 | ** |
| 575 | 578 | ** Verify that the string really that is to be copied really is a |
| 576 | 579 | ** valid email address. If it is not, then return NULL. |
| 577 | 580 | ** |
| 578 | 581 | ** This routine is more restrictive than necessary. It does not |
| 579 | 582 | ** allow comments, IP address, quoted strings, or certain uncommon |
| 580 | 583 | ** characters. The only non-alphanumerics allowed in the local |
| 581 | 584 | ** part are "_", "+", "-" and "+". |
| 582 | 585 | */ |
| 583 | -char *email_copy_addr(const char *z){ | |
| 586 | +char *email_copy_addr(const char *z, char cTerm ){ | |
| 584 | 587 | int i; |
| 585 | 588 | int nAt = 0; |
| 586 | 589 | int nDot = 0; |
| 587 | 590 | char c; |
| 588 | 591 | if( z[0]=='.' ) return 0; /* Local part cannot begin with "." */ |
| 589 | - for(i=0; (c = z[i])!=0 && c!='>'; i++){ | |
| 592 | + for(i=0; (c = z[i])!=0 && c!=cTerm; i++){ | |
| 590 | 593 | if( fossil_isalnum(c) ){ |
| 591 | 594 | /* Alphanumerics are always ok */ |
| 592 | 595 | }else if( c=='@' ){ |
| 593 | 596 | if( nAt ) return 0; /* Only a single "@" allowed */ |
| 594 | 597 | if( i>64 ) return 0; /* Local part too big */ |
| @@ -598,22 +601,22 @@ | ||
| 598 | 601 | if( z[i-1]=='.' ) return 0; /* Last char of local cannot be "." */ |
| 599 | 602 | if( z[i+1]=='.' || z[i+1]=='-' ){ |
| 600 | 603 | return 0; /* Domain cannot begin with "." or "-" */ |
| 601 | 604 | } |
| 602 | 605 | }else if( c=='-' ){ |
| 603 | - if( z[i+1]=='>' ) return 0; /* Last character cannot be "-" */ | |
| 606 | + if( z[i+1]==cTerm ) return 0; /* Last character cannot be "-" */ | |
| 604 | 607 | }else if( c=='.' ){ |
| 605 | 608 | if( z[i+1]=='.' ) return 0; /* Do not allow ".." */ |
| 606 | - if( z[i+1]=='>' ) return 0; /* Domain may not end with . */ | |
| 609 | + if( z[i+1]==cTerm ) return 0; /* Domain may not end with . */ | |
| 607 | 610 | nDot++; |
| 608 | 611 | }else if( (c=='_' || c=='+') && nAt==0 ){ |
| 609 | 612 | /* _ and + are ok in the local part */ |
| 610 | 613 | }else{ |
| 611 | 614 | return 0; /* Anything else is an error */ |
| 612 | 615 | } |
| 613 | 616 | } |
| 614 | - if( c!='>' ) return 0; /* Missing final ">" */ | |
| 617 | + if( c!=cTerm ) return 0; /* Missing terminator */ | |
| 615 | 618 | if( nAt==0 ) return 0; /* No "@" found anywhere */ |
| 616 | 619 | if( nDot==0 ) return 0; /* No "." in the domain */ |
| 617 | 620 | |
| 618 | 621 | /* If we reach this point, the email address is valid */ |
| 619 | 622 | return mprintf("%.*s", i, z); |
| @@ -631,11 +634,11 @@ | ||
| 631 | 634 | int i; |
| 632 | 635 | |
| 633 | 636 | email_header_value(pMsg, "to", &v); |
| 634 | 637 | z = blob_str(&v); |
| 635 | 638 | for(i=0; z[i]; i++){ |
| 636 | - if( z[i]=='<' && (zAddr = email_copy_addr(&z[i+1]))!=0 ){ | |
| 639 | + if( z[i]=='<' && (zAddr = email_copy_addr(&z[i+1],'>'))!=0 ){ | |
| 637 | 640 | azTo = fossil_realloc(azTo, sizeof(azTo[0])*(nTo+1) ); |
| 638 | 641 | azTo[nTo++] = zAddr; |
| 639 | 642 | } |
| 640 | 643 | } |
| 641 | 644 | *pnTo = nTo; |
| @@ -691,16 +694,18 @@ | ||
| 691 | 694 | pOut = &all; |
| 692 | 695 | } |
| 693 | 696 | blob_append(pOut, blob_buffer(pHdr), blob_size(pHdr)); |
| 694 | 697 | blob_appendf(pOut, "From: <%s>\r\n", p->zFrom); |
| 695 | 698 | blob_appendf(pOut, "Date: %z\r\n", cgi_rfc822_datestamp(time(0))); |
| 696 | - /* Message-id format: "<$(date)x$(random).$(from)>" where $(date) is | |
| 697 | - ** the current unix-time in hex, $(random) is a 64-bit random number, | |
| 698 | - ** and $(from) is the sender. */ | |
| 699 | - sqlite3_randomness(sizeof(r1), &r1); | |
| 700 | - r2 = time(0); | |
| 701 | - blob_appendf(pOut, "Message-Id: <%llxx%016llx.%s>\r\n", r2, r1, p->zFrom); | |
| 699 | + if( strstr(blob_str(pHdr), "\r\nMessage-Id:")==0 ){ | |
| 700 | + /* Message-id format: "<$(date)x$(random).$(from)>" where $(date) is | |
| 701 | + ** the current unix-time in hex, $(random) is a 64-bit random number, | |
| 702 | + ** and $(from) is the sender. */ | |
| 703 | + sqlite3_randomness(sizeof(r1), &r1); | |
| 704 | + r2 = time(0); | |
| 705 | + blob_appendf(pOut, "Message-Id: <%llxx%016llx.%s>\r\n", r2, r1, p->zFrom); | |
| 706 | + } | |
| 702 | 707 | blob_add_final_newline(pBody); |
| 703 | 708 | blob_appendf(pOut,"Content-Type: text/plain\r\n"); |
| 704 | 709 | #if 0 |
| 705 | 710 | blob_appendf(pOut, "Content-Transfer-Encoding: base64\r\n\r\n"); |
| 706 | 711 | append_base64(pOut, pBody); |
| @@ -752,24 +757,10 @@ | ||
| 752 | 757 | fossil_print("%s", blob_str(&all)); |
| 753 | 758 | } |
| 754 | 759 | blob_reset(&all); |
| 755 | 760 | } |
| 756 | 761 | |
| 757 | -/* | |
| 758 | -** Analyze and act on a received email. | |
| 759 | -** | |
| 760 | -** This routine takes ownership of the Blob parameter and is responsible | |
| 761 | -** for freeing that blob when it is done with it. | |
| 762 | -** | |
| 763 | -** This routine acts on all email messages received from the | |
| 764 | -** "fossil email inbound" command. | |
| 765 | -*/ | |
| 766 | -void email_receive(Blob *pMsg){ | |
| 767 | - /* To Do: Look for bounce messages and possibly disable subscriptions */ | |
| 768 | - blob_reset(pMsg); | |
| 769 | -} | |
| 770 | - | |
| 771 | 762 | /* |
| 772 | 763 | ** SETTING: email-send-method width=5 default=off |
| 773 | 764 | ** Determine the method used to send email. Allowed values are |
| 774 | 765 | ** "off", "relay", "pipe", "dir", "db", and "stdout". The "off" value |
| 775 | 766 | ** means no email is ever sent. The "relay" value means emails are sent |
| @@ -801,16 +792,10 @@ | ||
| 801 | 792 | /* |
| 802 | 793 | ** SETTING: email-self width=40 |
| 803 | 794 | ** This is the email address for the repository. Outbound emails add |
| 804 | 795 | ** this email address as the "From:" field. |
| 805 | 796 | */ |
| 806 | -/* | |
| 807 | -** SETTING: email-receive-dir width=40 | |
| 808 | -** Inbound email messages are saved as separate files in this directory, | |
| 809 | -** for debugging analysis. Disable saving of inbound emails omitting | |
| 810 | -** this setting, or making it an empty string. | |
| 811 | -*/ | |
| 812 | 797 | /* |
| 813 | 798 | ** SETTING: email-send-relayhost width=40 |
| 814 | 799 | ** This is the hostname and TCP port to which output email messages |
| 815 | 800 | ** are sent when email-send-method is "relay". There should be an |
| 816 | 801 | ** SMTP server configured as a Mail Submission Agent listening on the |
| @@ -817,80 +802,72 @@ | ||
| 817 | 802 | ** designated host and port and all times. |
| 818 | 803 | */ |
| 819 | 804 | |
| 820 | 805 | |
| 821 | 806 | /* |
| 822 | -** COMMAND: email | |
| 807 | +** COMMAND: alerts | |
| 823 | 808 | ** |
| 824 | -** Usage: %fossil email SUBCOMMAND ARGS... | |
| 809 | +** Usage: %fossil alerts SUBCOMMAND ARGS... | |
| 825 | 810 | ** |
| 826 | 811 | ** Subcommands: |
| 827 | 812 | ** |
| 828 | -** exec Compose and send pending email alerts. | |
| 813 | +** pending Show all pending alerts. Useful for debugging. | |
| 814 | +** | |
| 815 | +** reset Hard reset of all email notification tables | |
| 816 | +** in the repository. This erases all subscription | |
| 817 | +** information. ** Use with extreme care ** | |
| 818 | +** | |
| 819 | +** send Compose and send pending email alerts. | |
| 829 | 820 | ** Some installations may want to do this via |
| 830 | 821 | ** a cron-job to make sure alerts are sent |
| 831 | 822 | ** in a timely manner. |
| 832 | 823 | ** Options: |
| 833 | 824 | ** |
| 834 | 825 | ** --digest Send digests |
| 835 | -** --test Resets to standard output | |
| 836 | -** | |
| 837 | -** inbound [FILE] Receive an inbound email message. This message | |
| 838 | -** is analyzed to see if it is a bounce, and if | |
| 839 | -** necessary, subscribers may be disabled. | |
| 840 | -** | |
| 841 | -** reset Hard reset of all email notification tables | |
| 842 | -** in the repository. This erases all subscription | |
| 843 | -** information. Use with extreme care. | |
| 844 | -** | |
| 845 | -** send TO [OPTIONS] Send a single email message using whatever | |
| 826 | +** --test Write to standard output | |
| 827 | +** | |
| 828 | +** settings [NAME VALUE] With no arguments, list all email settings. | |
| 829 | +** Or change the value of a single email setting. | |
| 830 | +** | |
| 831 | +** status Report on the status of the email alert | |
| 832 | +** subsystem | |
| 833 | +** | |
| 834 | +** subscribers [PATTERN] List all subscribers matching PATTERN. | |
| 835 | +** | |
| 836 | +** test-message TO [OPTS] Send a single email message using whatever | |
| 846 | 837 | ** email sending mechanism is currently configured. |
| 847 | -** Use this for testing the email configuration. | |
| 848 | -** Options: | |
| 838 | +** Use this for testing the email notification | |
| 839 | +** configuration. Options: | |
| 849 | 840 | ** |
| 850 | 841 | ** --body FILENAME |
| 851 | 842 | ** --smtp-trace |
| 852 | 843 | ** --stdout |
| 853 | 844 | ** --subject|-S SUBJECT |
| 854 | 845 | ** |
| 855 | -** settings [NAME VALUE] With no arguments, list all email settings. | |
| 856 | -** Or change the value of a single email setting. | |
| 857 | -** | |
| 858 | -** subscribers [PATTERN] List all subscribers matching PATTERN. | |
| 859 | -** | |
| 860 | 846 | ** unsubscribe EMAIL Remove a single subscriber with the given EMAIL. |
| 861 | 847 | */ |
| 862 | 848 | void email_cmd(void){ |
| 863 | 849 | const char *zCmd; |
| 864 | 850 | int nCmd; |
| 865 | 851 | db_find_and_open_repository(0, 0); |
| 866 | 852 | email_schema(0); |
| 867 | 853 | zCmd = g.argc>=3 ? g.argv[2] : "x"; |
| 868 | 854 | nCmd = (int)strlen(zCmd); |
| 869 | - if( strncmp(zCmd, "exec", nCmd)==0 ){ | |
| 870 | - u32 eFlags = 0; | |
| 871 | - if( find_option("digest",0,0)!=0 ) eFlags |= SENDALERT_DIGEST; | |
| 872 | - if( find_option("test",0,0)!=0 ){ | |
| 873 | - eFlags |= SENDALERT_PRESERVE|SENDALERT_STDOUT; | |
| 874 | - } | |
| 875 | - verify_all_options(); | |
| 876 | - email_send_alerts(eFlags); | |
| 877 | - }else | |
| 878 | - if( strncmp(zCmd, "inbound", nCmd)==0 ){ | |
| 879 | - Blob email; | |
| 880 | - const char *zInboundDir = db_get("email-receive-dir",""); | |
| 881 | - verify_all_options(); | |
| 882 | - if( g.argc!=3 && g.argc!=4 ){ | |
| 883 | - usage("inbound [FILE]"); | |
| 884 | - } | |
| 885 | - blob_read_from_file(&email, g.argc==3 ? "-" : g.argv[3], ExtFILE); | |
| 886 | - if( zInboundDir[0] ){ | |
| 887 | - char *zFN = file_time_tempname(zInboundDir,".email"); | |
| 888 | - blob_write_to_file(&email, zFN); | |
| 889 | - fossil_free(zFN); | |
| 890 | - } | |
| 891 | - email_receive(&email); | |
| 855 | + if( strncmp(zCmd, "pending", nCmd)==0 ){ | |
| 856 | + Stmt q; | |
| 857 | + verify_all_options(); | |
| 858 | + if( g.argc!=3 ) usage("pending"); | |
| 859 | + db_prepare(&q,"SELECT eventid, sentSep, sentDigest, sentMod" | |
| 860 | + " FROM pending_alert"); | |
| 861 | + while( db_step(&q)==SQLITE_ROW ){ | |
| 862 | + fossil_print("%10s %7s %10s %7s\n", | |
| 863 | + db_column_text(&q,0), | |
| 864 | + db_column_int(&q,1) ? "sentSep" : "", | |
| 865 | + db_column_int(&q,2) ? "sentDigest" : "", | |
| 866 | + db_column_int(&q,3) ? "sentMod" : ""); | |
| 867 | + } | |
| 868 | + db_finalize(&q); | |
| 892 | 869 | }else |
| 893 | 870 | if( strncmp(zCmd, "reset", nCmd)==0 ){ |
| 894 | 871 | int c; |
| 895 | 872 | int bForce = find_option("force","f",0)!=0; |
| 896 | 873 | verify_all_options(); |
| @@ -918,43 +895,17 @@ | ||
| 918 | 895 | ); |
| 919 | 896 | email_schema(0); |
| 920 | 897 | } |
| 921 | 898 | }else |
| 922 | 899 | if( strncmp(zCmd, "send", nCmd)==0 ){ |
| 923 | - Blob prompt, body, hdr; | |
| 924 | - const char *zDest = find_option("stdout",0,0)!=0 ? "stdout" : 0; | |
| 925 | - int i; | |
| 926 | - u32 mFlags = EMAIL_IMMEDIATE_FAIL; | |
| 927 | - const char *zSubject = find_option("subject", "S", 1); | |
| 928 | - const char *zSource = find_option("body", 0, 1); | |
| 929 | - EmailSender *pSender; | |
| 930 | - if( find_option("smtp-trace",0,0)!=0 ) mFlags |= EMAIL_TRACE; | |
| 900 | + u32 eFlags = 0; | |
| 901 | + if( find_option("digest",0,0)!=0 ) eFlags |= SENDALERT_DIGEST; | |
| 902 | + if( find_option("test",0,0)!=0 ){ | |
| 903 | + eFlags |= SENDALERT_PRESERVE|SENDALERT_STDOUT; | |
| 904 | + } | |
| 931 | 905 | verify_all_options(); |
| 932 | - blob_init(&prompt, 0, 0); | |
| 933 | - blob_init(&body, 0, 0); | |
| 934 | - blob_init(&hdr, 0, 0); | |
| 935 | - blob_appendf(&hdr,"To: "); | |
| 936 | - for(i=3; i<g.argc; i++){ | |
| 937 | - if( i>3 ) blob_append(&hdr, ", ", 2); | |
| 938 | - blob_appendf(&hdr, "<%s>", g.argv[i]); | |
| 939 | - } | |
| 940 | - blob_append(&hdr,"\r\n",2); | |
| 941 | - if( zSubject ){ | |
| 942 | - blob_appendf(&hdr, "Subject: %s\r\n", zSubject); | |
| 943 | - } | |
| 944 | - if( zSource ){ | |
| 945 | - blob_read_from_file(&body, zSource, ExtFILE); | |
| 946 | - }else{ | |
| 947 | - prompt_for_user_comment(&body, &prompt); | |
| 948 | - } | |
| 949 | - blob_add_final_newline(&body); | |
| 950 | - pSender = email_sender_new(zDest, mFlags); | |
| 951 | - email_send(pSender, &hdr, &body); | |
| 952 | - email_sender_free(pSender); | |
| 953 | - blob_reset(&hdr); | |
| 954 | - blob_reset(&body); | |
| 955 | - blob_reset(&prompt); | |
| 906 | + email_send_alerts(eFlags); | |
| 956 | 907 | }else |
| 957 | 908 | if( strncmp(zCmd, "settings", nCmd)==0 ){ |
| 958 | 909 | int isGlobal = find_option("global",0,0)!=0; |
| 959 | 910 | int nSetting; |
| 960 | 911 | const Setting *pSetting = setting_info(&nSetting); |
| @@ -974,10 +925,32 @@ | ||
| 974 | 925 | for(; nSetting>0; nSetting--, pSetting++ ){ |
| 975 | 926 | if( strncmp(pSetting->name,"email-",6)!=0 ) continue; |
| 976 | 927 | print_setting(pSetting); |
| 977 | 928 | } |
| 978 | 929 | }else |
| 930 | + if( strncmp(zCmd, "status", nCmd)==0 ){ | |
| 931 | + int nSetting, n; | |
| 932 | + static const char *zFmt = "%-29s %d\n"; | |
| 933 | + const Setting *pSetting = setting_info(&nSetting); | |
| 934 | + db_open_config(1, 0); | |
| 935 | + verify_all_options(); | |
| 936 | + if( g.argc!=3 ) usage("status"); | |
| 937 | + pSetting = setting_info(&nSetting); | |
| 938 | + for(; nSetting>0; nSetting--, pSetting++ ){ | |
| 939 | + if( strncmp(pSetting->name,"email-",6)!=0 ) continue; | |
| 940 | + print_setting(pSetting); | |
| 941 | + } | |
| 942 | + n = db_int(0,"SELECT count(*) FROM pending_alert WHERE NOT sentSep"); | |
| 943 | + fossil_print(zFmt/*works-like:"%s%d"*/, "pending-alerts", n); | |
| 944 | + n = db_int(0,"SELECT count(*) FROM pending_alert WHERE NOT sentDigest"); | |
| 945 | + fossil_print(zFmt/*works-like:"%s%d"*/, "pending-digest-alerts", n); | |
| 946 | + n = db_int(0,"SELECT count(*) FROM subscriber"); | |
| 947 | + fossil_print(zFmt/*works-like:"%s%d"*/, "total-subscribers", n); | |
| 948 | + n = db_int(0, "SELECT count(*) FROM subscriber WHERE sverified" | |
| 949 | + " AND NOT sdonotcall AND length(ssub)>1"); | |
| 950 | + fossil_print(zFmt/*works-like:"%s%d"*/, "active-subscribers", n); | |
| 951 | + }else | |
| 979 | 952 | if( strncmp(zCmd, "subscribers", nCmd)==0 ){ |
| 980 | 953 | Stmt q; |
| 981 | 954 | verify_all_options(); |
| 982 | 955 | if( g.argc!=3 && g.argc!=4 ) usage("subscribers [PATTERN]"); |
| 983 | 956 | if( g.argc==4 ){ |
| @@ -995,19 +968,54 @@ | ||
| 995 | 968 | } |
| 996 | 969 | while( db_step(&q)==SQLITE_ROW ){ |
| 997 | 970 | fossil_print("%s\n", db_column_text(&q, 0)); |
| 998 | 971 | } |
| 999 | 972 | db_finalize(&q); |
| 973 | + }else | |
| 974 | + if( strncmp(zCmd, "test-message", nCmd)==0 ){ | |
| 975 | + Blob prompt, body, hdr; | |
| 976 | + const char *zDest = find_option("stdout",0,0)!=0 ? "stdout" : 0; | |
| 977 | + int i; | |
| 978 | + u32 mFlags = EMAIL_IMMEDIATE_FAIL; | |
| 979 | + const char *zSubject = find_option("subject", "S", 1); | |
| 980 | + const char *zSource = find_option("body", 0, 1); | |
| 981 | + EmailSender *pSender; | |
| 982 | + if( find_option("smtp-trace",0,0)!=0 ) mFlags |= EMAIL_TRACE; | |
| 983 | + verify_all_options(); | |
| 984 | + blob_init(&prompt, 0, 0); | |
| 985 | + blob_init(&body, 0, 0); | |
| 986 | + blob_init(&hdr, 0, 0); | |
| 987 | + blob_appendf(&hdr,"To: "); | |
| 988 | + for(i=3; i<g.argc; i++){ | |
| 989 | + if( i>3 ) blob_append(&hdr, ", ", 2); | |
| 990 | + blob_appendf(&hdr, "<%s>", g.argv[i]); | |
| 991 | + } | |
| 992 | + blob_append(&hdr,"\r\n",2); | |
| 993 | + if( zSubject==0 ) zSubject = "fossil alerts test-message"; | |
| 994 | + blob_appendf(&hdr, "Subject: %s\r\n", zSubject); | |
| 995 | + if( zSource ){ | |
| 996 | + blob_read_from_file(&body, zSource, ExtFILE); | |
| 997 | + }else{ | |
| 998 | + prompt_for_user_comment(&body, &prompt); | |
| 999 | + } | |
| 1000 | + blob_add_final_newline(&body); | |
| 1001 | + pSender = email_sender_new(zDest, mFlags); | |
| 1002 | + email_send(pSender, &hdr, &body); | |
| 1003 | + email_sender_free(pSender); | |
| 1004 | + blob_reset(&hdr); | |
| 1005 | + blob_reset(&body); | |
| 1006 | + blob_reset(&prompt); | |
| 1000 | 1007 | }else |
| 1001 | 1008 | if( strncmp(zCmd, "unsubscribe", nCmd)==0 ){ |
| 1002 | 1009 | verify_all_options(); |
| 1003 | 1010 | if( g.argc!=4 ) usage("unsubscribe EMAIL"); |
| 1004 | 1011 | db_multi_exec( |
| 1005 | 1012 | "DELETE FROM subscriber WHERE semail=%Q", g.argv[3]); |
| 1006 | 1013 | }else |
| 1007 | 1014 | { |
| 1008 | - usage("exec|inbound|reset|send|setting|subscribers|unsubscribe"); | |
| 1015 | + usage("pending|reset|send|setting|status|" | |
| 1016 | + "subscribers|test-message|unsubscribe"); | |
| 1009 | 1017 | } |
| 1010 | 1018 | } |
| 1011 | 1019 | |
| 1012 | 1020 | /* |
| 1013 | 1021 | ** Do error checking on a submitted subscription form. Return TRUE |
| @@ -1784,11 +1792,13 @@ | ||
| 1784 | 1792 | /* |
| 1785 | 1793 | ** A single event that might appear in an alert is recorded as an |
| 1786 | 1794 | ** instance of the following object. |
| 1787 | 1795 | */ |
| 1788 | 1796 | struct EmailEvent { |
| 1789 | - int type; /* 'c', 't', 'w', etc. */ | |
| 1797 | + int type; /* 'c', 'f', 'm', 't', 'w' */ | |
| 1798 | + int needMod; /* Pending moderator approval */ | |
| 1799 | + Blob hdr; /* Header content, for forum entries */ | |
| 1790 | 1800 | Blob txt; /* Text description to appear in an alert */ |
| 1791 | 1801 | EmailEvent *pNext; /* Next in chronological order */ |
| 1792 | 1802 | }; |
| 1793 | 1803 | #endif |
| 1794 | 1804 | |
| @@ -1797,10 +1807,11 @@ | ||
| 1797 | 1807 | */ |
| 1798 | 1808 | void email_free_eventlist(EmailEvent *p){ |
| 1799 | 1809 | while( p ){ |
| 1800 | 1810 | EmailEvent *pNext = p->pNext; |
| 1801 | 1811 | blob_reset(&p->txt); |
| 1812 | + blob_reset(&p->hdr); | |
| 1802 | 1813 | fossil_free(p); |
| 1803 | 1814 | p = pNext; |
| 1804 | 1815 | } |
| 1805 | 1816 | } |
| 1806 | 1817 | |
| @@ -1807,68 +1818,152 @@ | ||
| 1807 | 1818 | /* |
| 1808 | 1819 | ** Compute and return a linked list of EmailEvent objects |
| 1809 | 1820 | ** corresponding to the current content of the temp.wantalert |
| 1810 | 1821 | ** table which should be defined as follows: |
| 1811 | 1822 | ** |
| 1812 | -** CREATE TEMP TABLE wantalert(eventId TEXT); | |
| 1823 | +** CREATE TEMP TABLE wantalert(eventId TEXT, needMod BOOLEAN); | |
| 1813 | 1824 | */ |
| 1814 | -EmailEvent *email_compute_event_text(int *pnEvent){ | |
| 1825 | +EmailEvent *email_compute_event_text(int *pnEvent, int doDigest){ | |
| 1815 | 1826 | Stmt q; |
| 1816 | 1827 | EmailEvent *p; |
| 1817 | 1828 | EmailEvent anchor; |
| 1818 | 1829 | EmailEvent *pLast; |
| 1819 | 1830 | const char *zUrl = db_get("email-url","http://localhost:8080"); |
| 1831 | + const char *zFrom; | |
| 1832 | + const char *zSub; | |
| 1833 | + | |
| 1820 | 1834 | |
| 1835 | + /* First do non-forum post events */ | |
| 1821 | 1836 | db_prepare(&q, |
| 1822 | 1837 | "SELECT" |
| 1823 | - " blob.uuid," /* 0 */ | |
| 1824 | - " datetime(event.mtime)," /* 1 */ | |
| 1838 | + " blob.uuid," /* 0 */ | |
| 1839 | + " datetime(event.mtime)," /* 1 */ | |
| 1825 | 1840 | " coalesce(ecomment,comment)" |
| 1826 | 1841 | " || ' (user: ' || coalesce(euser,user,'?')" |
| 1827 | 1842 | " || (SELECT case when length(x)>0 then ' tags: ' || x else '' end" |
| 1828 | 1843 | " FROM (SELECT group_concat(substr(tagname,5), ', ') AS x" |
| 1829 | 1844 | " FROM tag, tagxref" |
| 1830 | 1845 | " WHERE tagname GLOB 'sym-*' AND tag.tagid=tagxref.tagid" |
| 1831 | 1846 | " AND tagxref.rid=blob.rid AND tagxref.tagtype>0))" |
| 1832 | - " || ')' as comment," /* 2 */ | |
| 1833 | - " tagxref.value AS branch," /* 3 */ | |
| 1834 | - " wantalert.eventId" /* 4 */ | |
| 1835 | - " FROM temp.wantalert JOIN tag CROSS JOIN event CROSS JOIN blob" | |
| 1836 | - " LEFT JOIN tagxref ON tagxref.tagid=tag.tagid" | |
| 1837 | - " AND tagxref.tagtype>0" | |
| 1838 | - " AND tagxref.rid=blob.rid" | |
| 1847 | + " || ')' as comment," /* 2 */ | |
| 1848 | + " wantalert.eventId," /* 3 */ | |
| 1849 | + " wantalert.needMod" /* 4 */ | |
| 1850 | + " FROM temp.wantalert CROSS JOIN event CROSS JOIN blob" | |
| 1839 | 1851 | " WHERE blob.rid=event.objid" |
| 1840 | - " AND tag.tagname='branch'" | |
| 1841 | 1852 | " AND event.objid=substr(wantalert.eventId,2)+0" |
| 1842 | - " ORDER BY event.mtime" | |
| 1853 | + " AND (%d OR eventId NOT GLOB 'f*')" | |
| 1854 | + " ORDER BY event.mtime", | |
| 1855 | + doDigest | |
| 1843 | 1856 | ); |
| 1844 | 1857 | memset(&anchor, 0, sizeof(anchor)); |
| 1845 | 1858 | pLast = &anchor; |
| 1846 | 1859 | *pnEvent = 0; |
| 1847 | 1860 | while( db_step(&q)==SQLITE_ROW ){ |
| 1848 | 1861 | const char *zType = ""; |
| 1849 | 1862 | p = fossil_malloc( sizeof(EmailEvent) ); |
| 1850 | 1863 | pLast->pNext = p; |
| 1851 | 1864 | pLast = p; |
| 1852 | - p->type = db_column_text(&q, 4)[0]; | |
| 1865 | + p->type = db_column_text(&q, 3)[0]; | |
| 1866 | + p->needMod = db_column_int(&q, 4); | |
| 1853 | 1867 | p->pNext = 0; |
| 1854 | 1868 | switch( p->type ){ |
| 1855 | 1869 | case 'c': zType = "Check-In"; break; |
| 1870 | + case 'f': zType = "Forum post"; break; | |
| 1856 | 1871 | case 't': zType = "Wiki Edit"; break; |
| 1857 | 1872 | case 'w': zType = "Ticket Change"; break; |
| 1858 | 1873 | } |
| 1874 | + blob_init(&p->hdr, 0, 0); | |
| 1859 | 1875 | blob_init(&p->txt, 0, 0); |
| 1860 | 1876 | blob_appendf(&p->txt,"== %s %s ==\n%s\n%s/info/%.20s\n", |
| 1861 | 1877 | db_column_text(&q,1), |
| 1862 | 1878 | zType, |
| 1863 | 1879 | db_column_text(&q,2), |
| 1864 | 1880 | zUrl, |
| 1865 | 1881 | db_column_text(&q,0) |
| 1866 | 1882 | ); |
| 1883 | + if( p->needMod ){ | |
| 1884 | + blob_appendf(&p->txt, | |
| 1885 | + "** Pending moderator approval (%s/modreq) **\n", | |
| 1886 | + zUrl | |
| 1887 | + ); | |
| 1888 | + } | |
| 1889 | + (*pnEvent)++; | |
| 1890 | + } | |
| 1891 | + db_finalize(&q); | |
| 1892 | + | |
| 1893 | + /* Early-out if forumpost is not a table in this repository */ | |
| 1894 | + if( !db_table_exists("repository","forumpost") ){ | |
| 1895 | + return anchor.pNext; | |
| 1896 | + } | |
| 1897 | + | |
| 1898 | + /* For digests, the previous loop also handled forumposts already */ | |
| 1899 | + if( doDigest ){ | |
| 1900 | + return anchor.pNext; | |
| 1901 | + } | |
| 1902 | + | |
| 1903 | + /* If we reach this point, it means that forumposts exist and this | |
| 1904 | + ** is a normal email alert. Construct full-text forum post alerts | |
| 1905 | + ** using a format that enables them to be sent as separate emails. | |
| 1906 | + */ | |
| 1907 | + db_prepare(&q, | |
| 1908 | + "SELECT" | |
| 1909 | + " forumpost.fpid," /* 0 */ | |
| 1910 | + " (SELECT uuid FROM blob WHERE rid=forumpost.fpid)," /* 1 */ | |
| 1911 | + " datetime(event.mtime)," /* 2 */ | |
| 1912 | + " substr(comment,instr(comment,':')+2)," /* 3 */ | |
| 1913 | + " (SELECT uuid FROM blob WHERE rid=forumpost.firt)," /* 4 */ | |
| 1914 | + " wantalert.needMod" /* 5 */ | |
| 1915 | + " FROM temp.wantalert, event, forumpost" | |
| 1916 | + " WHERE event.objid=substr(wantalert.eventId,2)+0" | |
| 1917 | + " AND eventId GLOB 'f*'" | |
| 1918 | + " AND forumpost.fpid=event.objid" | |
| 1919 | + ); | |
| 1920 | + zFrom = db_get("email-self",0); | |
| 1921 | + zSub = db_get("email-subname",""); | |
| 1922 | + while( db_step(&q)==SQLITE_ROW ){ | |
| 1923 | + Manifest *pPost = manifest_get(db_column_int(&q,0), CFTYPE_FORUM, 0); | |
| 1924 | + const char *zIrt; | |
| 1925 | + const char *zUuid; | |
| 1926 | + const char *zTitle; | |
| 1927 | + if( pPost==0 ) continue; | |
| 1928 | + p = fossil_malloc( sizeof(EmailEvent) ); | |
| 1929 | + pLast->pNext = p; | |
| 1930 | + pLast = p; | |
| 1931 | + p->type = 'f'; | |
| 1932 | + p->needMod = db_column_int(&q, 5); | |
| 1933 | + p->pNext = 0; | |
| 1934 | + blob_init(&p->hdr, 0, 0); | |
| 1935 | + zUuid = db_column_text(&q, 1); | |
| 1936 | + zTitle = db_column_text(&q, 3); | |
| 1937 | + if( p->needMod ){ | |
| 1938 | + blob_appendf(&p->hdr, "Subject: %s Pending Moderation: %s\r\n", | |
| 1939 | + zSub, zTitle); | |
| 1940 | + }else{ | |
| 1941 | + blob_appendf(&p->hdr, "Subject: %s %s\r\n", zSub, zTitle); | |
| 1942 | + blob_appendf(&p->hdr, "Message-Id: <%s.%s>\r\n", zUuid, zFrom); | |
| 1943 | + zIrt = db_column_text(&q, 4); | |
| 1944 | + if( zIrt && zIrt[0] ){ | |
| 1945 | + blob_appendf(&p->hdr, "In-Reply-To: <%s.%s>\r\n", zIrt, zFrom); | |
| 1946 | + } | |
| 1947 | + } | |
| 1948 | + blob_init(&p->txt, 0, 0); | |
| 1949 | + if( p->needMod ){ | |
| 1950 | + blob_appendf(&p->txt, | |
| 1951 | + "** Pending moderator approval (%s/modreq) **\n", | |
| 1952 | + zUrl | |
| 1953 | + ); | |
| 1954 | + } | |
| 1955 | + blob_appendf(&p->txt, | |
| 1956 | + "Forum post by %s on %s\n", | |
| 1957 | + pPost->zUser, db_column_text(&q, 2)); | |
| 1958 | + blob_appendf(&p->txt, "%s/forumpost/%S\n\n", zUrl, zUuid); | |
| 1959 | + blob_append(&p->txt, pPost->zWiki, -1); | |
| 1960 | + manifest_destroy(pPost); | |
| 1867 | 1961 | (*pnEvent)++; |
| 1868 | 1962 | } |
| 1869 | 1963 | db_finalize(&q); |
| 1964 | + | |
| 1870 | 1965 | return anchor.pNext; |
| 1871 | 1966 | } |
| 1872 | 1967 | |
| 1873 | 1968 | /* |
| 1874 | 1969 | ** Put a header on an alert email |
| @@ -1900,34 +1995,50 @@ | ||
| 1900 | 1995 | ** command line, generate text for all events named in the |
| 1901 | 1996 | ** pending_alert table. |
| 1902 | 1997 | ** |
| 1903 | 1998 | ** This command is intended for testing and debugging the logic |
| 1904 | 1999 | ** that generates email alert text. |
| 2000 | +** | |
| 2001 | +** Options: | |
| 2002 | +** | |
| 2003 | +** --digest Generate digest alert text | |
| 2004 | +** --needmod Assume all events are pending moderator approval | |
| 1905 | 2005 | */ |
| 1906 | 2006 | void test_alert_cmd(void){ |
| 1907 | 2007 | Blob out; |
| 1908 | 2008 | int nEvent; |
| 2009 | + int needMod; | |
| 2010 | + int doDigest; | |
| 1909 | 2011 | EmailEvent *pEvent, *p; |
| 1910 | 2012 | |
| 2013 | + doDigest = find_option("digest",0,0)!=0; | |
| 2014 | + needMod = find_option("needmod",0,0)!=0; | |
| 1911 | 2015 | db_find_and_open_repository(0, 0); |
| 1912 | 2016 | verify_all_options(); |
| 1913 | 2017 | db_begin_transaction(); |
| 1914 | 2018 | email_schema(0); |
| 1915 | - db_multi_exec("CREATE TEMP TABLE wantalert(eventid TEXT)"); | |
| 2019 | + db_multi_exec("CREATE TEMP TABLE wantalert(eventid TEXT, needMod BOOLEAN)"); | |
| 1916 | 2020 | if( g.argc==2 ){ |
| 1917 | - db_multi_exec("INSERT INTO wantalert SELECT eventid FROM pending_alert"); | |
| 2021 | + db_multi_exec( | |
| 2022 | + "INSERT INTO wantalert(eventId,needMod)" | |
| 2023 | + " SELECT eventid, %d FROM pending_alert", needMod); | |
| 1918 | 2024 | }else{ |
| 1919 | 2025 | int i; |
| 1920 | 2026 | for(i=2; i<g.argc; i++){ |
| 1921 | - db_multi_exec("INSERT INTO wantalert VALUES(%Q)", g.argv[i]); | |
| 2027 | + db_multi_exec("INSERT INTO wantalert(eventId,needMod) VALUES(%Q,%d)", | |
| 2028 | + g.argv[i], needMod); | |
| 1922 | 2029 | } |
| 1923 | 2030 | } |
| 1924 | 2031 | blob_init(&out, 0, 0); |
| 1925 | 2032 | email_header(&out); |
| 1926 | - pEvent = email_compute_event_text(&nEvent); | |
| 2033 | + pEvent = email_compute_event_text(&nEvent, doDigest); | |
| 1927 | 2034 | for(p=pEvent; p; p=p->pNext){ |
| 1928 | 2035 | blob_append(&out, "\n", 1); |
| 2036 | + if( blob_size(&p->hdr) ){ | |
| 2037 | + blob_append(&out, blob_buffer(&p->hdr), blob_size(&p->hdr)); | |
| 2038 | + blob_append(&out, "\n", 1); | |
| 2039 | + } | |
| 1929 | 2040 | blob_append(&out, blob_buffer(&p->txt), blob_size(&p->txt)); |
| 1930 | 2041 | } |
| 1931 | 2042 | email_free_eventlist(pEvent); |
| 1932 | 2043 | email_footer(&out); |
| 1933 | 2044 | fossil_print("%s", blob_str(&out)); |
| @@ -1936,38 +2047,54 @@ | ||
| 1936 | 2047 | } |
| 1937 | 2048 | |
| 1938 | 2049 | /* |
| 1939 | 2050 | ** COMMAND: test-add-alerts |
| 1940 | 2051 | ** |
| 1941 | -** Usage: %fossil test-add-alerts [--backoffice] EVENTID ... | |
| 2052 | +** Usage: %fossil test-add-alerts [OPTIONS] EVENTID ... | |
| 1942 | 2053 | ** |
| 1943 | 2054 | ** Add one or more events to the pending_alert queue. Use this |
| 1944 | 2055 | ** command during testing to force email notifications for specific |
| 1945 | 2056 | ** events. |
| 1946 | 2057 | ** |
| 1947 | -** EVENTIDs are text. The first character is 'c', 'w', or 't' | |
| 1948 | -** for check-in, wiki, or ticket. The remaining text is a | |
| 2058 | +** EVENTIDs are text. The first character is 'c', 'f', 't', or 'w' | |
| 2059 | +** for check-in, forum, ticket, or wiki. The remaining text is a | |
| 1949 | 2060 | ** integer that references the EVENT.OBJID value for the event. |
| 1950 | 2061 | ** Run /timeline?showid to see these OBJID values. |
| 1951 | 2062 | ** |
| 1952 | -** If the --backoffice option is included, then email_backoffice() is run | |
| 1953 | -** after all alerts have been added. This will cause the alerts to | |
| 1954 | -** be sent out with the SENDALERT_TRACE option. | |
| 2063 | +** Options: | |
| 2064 | +** | |
| 2065 | +** --backoffice Run email_backoffice() after all alerts have | |
| 2066 | +** been added. This will cause the alerts to be | |
| 2067 | +** sent out with the SENDALERT_TRACE option. | |
| 2068 | +** | |
| 2069 | +** --debug Like --backoffice, but add the SENDALERT_STDOUT | |
| 2070 | +** so that emails are printed to standard output | |
| 2071 | +** rather than being sent. | |
| 2072 | +** | |
| 2073 | +** --digest Process emails using SENDALERT_DIGEST | |
| 1955 | 2074 | */ |
| 1956 | 2075 | void test_add_alert_cmd(void){ |
| 1957 | 2076 | int i; |
| 1958 | 2077 | int doAuto = find_option("backoffice",0,0)!=0; |
| 2078 | + unsigned mFlags = 0; | |
| 2079 | + if( find_option("debug",0,0)!=0 ){ | |
| 2080 | + doAuto = 1; | |
| 2081 | + mFlags = SENDALERT_STDOUT; | |
| 2082 | + } | |
| 2083 | + if( find_option("digest",0,0)!=0 ){ | |
| 2084 | + mFlags |= SENDALERT_DIGEST; | |
| 2085 | + } | |
| 1959 | 2086 | db_find_and_open_repository(0, 0); |
| 1960 | 2087 | verify_all_options(); |
| 1961 | 2088 | db_begin_write(); |
| 1962 | 2089 | email_schema(0); |
| 1963 | 2090 | for(i=2; i<g.argc; i++){ |
| 1964 | 2091 | db_multi_exec("REPLACE INTO pending_alert(eventId) VALUES(%Q)", g.argv[i]); |
| 1965 | 2092 | } |
| 1966 | 2093 | db_end_transaction(0); |
| 1967 | 2094 | if( doAuto ){ |
| 1968 | - email_backoffice(SENDALERT_TRACE); | |
| 2095 | + email_backoffice(SENDALERT_TRACE|mFlags); | |
| 1969 | 2096 | } |
| 1970 | 2097 | } |
| 1971 | 2098 | |
| 1972 | 2099 | #if INTERFACE |
| 1973 | 2100 | /* |
| @@ -1979,11 +2106,35 @@ | ||
| 1979 | 2106 | #define SENDALERT_TRACE 0x0008 /* Trace operation for debugging */ |
| 1980 | 2107 | |
| 1981 | 2108 | #endif /* INTERFACE */ |
| 1982 | 2109 | |
| 1983 | 2110 | /* |
| 1984 | -** Send alert emails to all subscribers. | |
| 2111 | +** Send alert emails to subscribers. | |
| 2112 | +** | |
| 2113 | +** This procedure is run by either the backoffice, or in response to the | |
| 2114 | +** "fossil alerts send" command. Details of operation are controlled by | |
| 2115 | +** the flags parameter. | |
| 2116 | +** | |
| 2117 | +** Here is a summary of what happens: | |
| 2118 | +** | |
| 2119 | +** (1) Create a TEMP table wantalert(eventId,needMod) and fill it with | |
| 2120 | +** all the events that we want to send alerts about. The needMod | |
| 2121 | +** flags is set if and only if the event is still awaiting | |
| 2122 | +** moderator approval. Events with the needMod flag are only | |
| 2123 | +** shown to users that have moderator privileges. | |
| 2124 | +** | |
| 2125 | +** (2) Call email_compute_event_text() to compute a list of EmailEvent | |
| 2126 | +** objects that describe all events about which we want to send | |
| 2127 | +** alerts. | |
| 2128 | +** | |
| 2129 | +** (3) Loop over all subscribers. Compose and send one or more email | |
| 2130 | +** messages to each subscriber that describe the events for | |
| 2131 | +** which the subscriber has expressed interest and has | |
| 2132 | +** appropriate privileges. | |
| 2133 | +** | |
| 2134 | +** (4) Update the pending_alerts table to indicate that alerts have been | |
| 2135 | +** sent. | |
| 1985 | 2136 | */ |
| 1986 | 2137 | void email_send_alerts(u32 flags){ |
| 1987 | 2138 | EmailEvent *pEvents, *p; |
| 1988 | 2139 | int nEvent = 0; |
| 1989 | 2140 | Stmt q; |
| @@ -1996,10 +2147,11 @@ | ||
| 1996 | 2147 | EmailSender *pSender = 0; |
| 1997 | 2148 | u32 senderFlags = 0; |
| 1998 | 2149 | |
| 1999 | 2150 | if( g.fSqlTrace ) fossil_trace("-- BEGIN email_send_alerts(%u)\n", flags); |
| 2000 | 2151 | db_begin_transaction(); |
| 2152 | + email_schema(0); | |
| 2001 | 2153 | if( !email_enabled() ) goto send_alerts_done; |
| 2002 | 2154 | zUrl = db_get("email-url",0); |
| 2003 | 2155 | if( zUrl==0 ) goto send_alerts_done; |
| 2004 | 2156 | zRepoName = db_get("email-subname",0); |
| 2005 | 2157 | if( zRepoName==0 ) goto send_alerts_done; |
| @@ -2009,57 +2161,111 @@ | ||
| 2009 | 2161 | senderFlags |= EMAIL_TRACE; |
| 2010 | 2162 | } |
| 2011 | 2163 | pSender = email_sender_new(zDest, senderFlags); |
| 2012 | 2164 | db_multi_exec( |
| 2013 | 2165 | "DROP TABLE IF EXISTS temp.wantalert;" |
| 2014 | - "CREATE TEMP TABLE wantalert(eventId TEXT);" | |
| 2166 | + "CREATE TEMP TABLE wantalert(eventId TEXT, needMod BOOLEAN, sentMod);" | |
| 2015 | 2167 | ); |
| 2016 | 2168 | if( flags & SENDALERT_DIGEST ){ |
| 2169 | + /* Unmoderated changes are never sent as part of a digest */ | |
| 2017 | 2170 | db_multi_exec( |
| 2018 | - "INSERT INTO wantalert SELECT eventid FROM pending_alert" | |
| 2171 | + "INSERT INTO wantalert(eventId,needMod)" | |
| 2172 | + " SELECT eventid, 0" | |
| 2173 | + " FROM pending_alert" | |
| 2019 | 2174 | " WHERE sentDigest IS FALSE" |
| 2175 | + " AND NOT EXISTS(SELECT 1 FROM private WHERE rid=substr(eventid,2));" | |
| 2020 | 2176 | ); |
| 2021 | 2177 | zDigest = "true"; |
| 2022 | 2178 | }else{ |
| 2179 | + /* Immediate alerts might include events that are subject to | |
| 2180 | + ** moderator approval */ | |
| 2023 | 2181 | db_multi_exec( |
| 2024 | - "INSERT INTO wantalert SELECT eventid FROM pending_alert" | |
| 2025 | - " WHERE sentSep IS FALSE" | |
| 2182 | + "INSERT INTO wantalert(eventId,needMod,sentMod)" | |
| 2183 | + " SELECT eventid," | |
| 2184 | + " EXISTS(SELECT 1 FROM private WHERE rid=substr(eventid,2))," | |
| 2185 | + " sentMod" | |
| 2186 | + " FROM pending_alert" | |
| 2187 | + " WHERE sentSep IS FALSE;" | |
| 2188 | + "DELETE FROM wantalert WHERE needMod AND sentMod;" | |
| 2026 | 2189 | ); |
| 2027 | 2190 | } |
| 2028 | - pEvents = email_compute_event_text(&nEvent); | |
| 2191 | + pEvents = email_compute_event_text(&nEvent, (flags & SENDALERT_DIGEST)!=0); | |
| 2029 | 2192 | if( nEvent==0 ) goto send_alerts_done; |
| 2030 | 2193 | blob_init(&hdr, 0, 0); |
| 2031 | 2194 | blob_init(&body, 0, 0); |
| 2032 | 2195 | db_prepare(&q, |
| 2033 | 2196 | "SELECT" |
| 2034 | 2197 | " hex(subscriberCode)," /* 0 */ |
| 2035 | 2198 | " semail," /* 1 */ |
| 2036 | - " ssub" /* 2 */ | |
| 2199 | + " ssub," /* 2 */ | |
| 2200 | + " fullcap((SELECT cap FROM user WHERE login=suname))" /* 3 */ | |
| 2037 | 2201 | " FROM subscriber" |
| 2038 | 2202 | " WHERE sverified AND NOT sdonotcall" |
| 2039 | 2203 | " AND sdigest IS %s", |
| 2040 | 2204 | zDigest/*safe-for-%s*/ |
| 2041 | 2205 | ); |
| 2042 | 2206 | while( db_step(&q)==SQLITE_ROW ){ |
| 2043 | 2207 | const char *zCode = db_column_text(&q, 0); |
| 2044 | 2208 | const char *zSub = db_column_text(&q, 2); |
| 2045 | 2209 | const char *zEmail = db_column_text(&q, 1); |
| 2210 | + const char *zCap = db_column_text(&q, 3); | |
| 2046 | 2211 | int nHit = 0; |
| 2047 | 2212 | for(p=pEvents; p; p=p->pNext){ |
| 2048 | 2213 | if( strchr(zSub,p->type)==0 ) continue; |
| 2049 | - if( nHit==0 ){ | |
| 2050 | - blob_appendf(&hdr,"To: <%s>\r\n", zEmail); | |
| 2051 | - blob_appendf(&hdr,"Subject: %s activity alert\r\n", zRepoName); | |
| 2052 | - blob_appendf(&body, | |
| 2053 | - "This is an automated email sent by the Fossil repository " | |
| 2054 | - "at %s to report changes.\n", | |
| 2055 | - zUrl | |
| 2056 | - ); | |
| 2057 | - } | |
| 2058 | - nHit++; | |
| 2059 | - blob_append(&body, "\n", 1); | |
| 2060 | - blob_append(&body, blob_buffer(&p->txt), blob_size(&p->txt)); | |
| 2214 | + if( p->needMod ){ | |
| 2215 | + /* For events that require moderator approval, only send an alert | |
| 2216 | + ** if the recipient is a moderator for that type of event */ | |
| 2217 | + char xType = '*'; | |
| 2218 | + switch( p->type ){ | |
| 2219 | + case 'f': xType = '5'; break; | |
| 2220 | + case 't': xType = 'q'; break; | |
| 2221 | + case 'w': xType = 'l'; break; | |
| 2222 | + } | |
| 2223 | + if( strchr(zCap,xType)==0 ) continue; | |
| 2224 | + }else if( strchr(zCap,'s')!=0 || strchr(zCap,'a')!=0 ){ | |
| 2225 | + /* Setup and admin users can get any notification that does not | |
| 2226 | + ** require moderation */ | |
| 2227 | + }else{ | |
| 2228 | + /* Other users only see the alert if they have sufficient | |
| 2229 | + ** privilege to view the event itself */ | |
| 2230 | + char xType = '*'; | |
| 2231 | + switch( p->type ){ | |
| 2232 | + case 'c': xType = 'o'; break; | |
| 2233 | + case 'f': xType = '2'; break; | |
| 2234 | + case 't': xType = 'r'; break; | |
| 2235 | + case 'w': xType = 'j'; break; | |
| 2236 | + } | |
| 2237 | + if( strchr(zCap,xType)==0 ) continue; | |
| 2238 | + } | |
| 2239 | + if( blob_size(&p->hdr)>0 ){ | |
| 2240 | + /* This alert should be sent as a separate email */ | |
| 2241 | + Blob fhdr, fbody; | |
| 2242 | + blob_init(&fhdr, 0, 0); | |
| 2243 | + blob_appendf(&fhdr, "To: <%s>\r\n", zEmail); | |
| 2244 | + blob_append(&fhdr, blob_buffer(&p->hdr), blob_size(&p->hdr)); | |
| 2245 | + blob_init(&fbody, blob_buffer(&p->txt), blob_size(&p->txt)); | |
| 2246 | + blob_appendf(&fbody, "\n-- \nSubscription info: %s/alerts/%s\n", | |
| 2247 | + zUrl, zCode); | |
| 2248 | + email_send(pSender,&fhdr,&fbody); | |
| 2249 | + blob_reset(&fhdr); | |
| 2250 | + blob_reset(&fbody); | |
| 2251 | + }else{ | |
| 2252 | + /* Events other than forum posts are gathered together into | |
| 2253 | + ** a single email message */ | |
| 2254 | + if( nHit==0 ){ | |
| 2255 | + blob_appendf(&hdr,"To: <%s>\r\n", zEmail); | |
| 2256 | + blob_appendf(&hdr,"Subject: %s activity alert\r\n", zRepoName); | |
| 2257 | + blob_appendf(&body, | |
| 2258 | + "This is an automated email sent by the Fossil repository " | |
| 2259 | + "at %s to report changes.\n", | |
| 2260 | + zUrl | |
| 2261 | + ); | |
| 2262 | + } | |
| 2263 | + nHit++; | |
| 2264 | + blob_append(&body, "\n", 1); | |
| 2265 | + blob_append(&body, blob_buffer(&p->txt), blob_size(&p->txt)); | |
| 2266 | + } | |
| 2061 | 2267 | } |
| 2062 | 2268 | if( nHit==0 ) continue; |
| 2063 | 2269 | blob_appendf(&body,"\n-- \nSubscription info: %s/alerts/%s\n", |
| 2064 | 2270 | zUrl, zCode); |
| 2065 | 2271 | email_send(pSender,&hdr,&body); |
| @@ -2070,15 +2276,24 @@ | ||
| 2070 | 2276 | blob_reset(&body); |
| 2071 | 2277 | db_finalize(&q); |
| 2072 | 2278 | email_free_eventlist(pEvents); |
| 2073 | 2279 | if( (flags & SENDALERT_PRESERVE)==0 ){ |
| 2074 | 2280 | if( flags & SENDALERT_DIGEST ){ |
| 2075 | - db_multi_exec("UPDATE pending_alert SET sentDigest=true"); | |
| 2281 | + db_multi_exec( | |
| 2282 | + "UPDATE pending_alert SET sentDigest=true" | |
| 2283 | + " WHERE eventid IN (SELECT eventid FROM wantalert);" | |
| 2284 | + "DELETE FROM pending_alert WHERE sentDigest AND sentSep;" | |
| 2285 | + ); | |
| 2076 | 2286 | }else{ |
| 2077 | - db_multi_exec("UPDATE pending_alert SET sentSep=true"); | |
| 2287 | + db_multi_exec( | |
| 2288 | + "UPDATE pending_alert SET sentSep=true" | |
| 2289 | + " WHERE eventid IN (SELECT eventid FROM wantalert WHERE NOT needMod);" | |
| 2290 | + "UPDATE pending_alert SET sentMod=true" | |
| 2291 | + " WHERE eventid IN (SELECT eventid FROM wantalert WHERE needMod);" | |
| 2292 | + "DELETE FROM pending_alert WHERE sentDigest AND sentSep;" | |
| 2293 | + ); | |
| 2078 | 2294 | } |
| 2079 | - db_multi_exec("DELETE FROM pending_alert WHERE sentDigest AND sentSep"); | |
| 2080 | 2295 | } |
| 2081 | 2296 | send_alerts_done: |
| 2082 | 2297 | email_sender_free(pSender); |
| 2083 | 2298 | if( g.fSqlTrace ) fossil_trace("-- END email_send_alerts(%u)\n", flags); |
| 2084 | 2299 | db_end_transaction(0); |
| 2085 | 2300 |
| --- src/email.c | |
| +++ src/email.c | |
| @@ -1,7 +1,7 @@ | |
| 1 | /* |
| 2 | ** Copyright (c) 2007 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 | ** |
| @@ -14,11 +14,16 @@ | |
| 14 | ** http://www.hwaci.com/drh/ |
| 15 | ** |
| 16 | ******************************************************************************* |
| 17 | ** |
| 18 | ** Logic for email notification, also known as "alerts". |
| 19 | */ |
| 20 | #include "config.h" |
| 21 | #include "email.h" |
| 22 | #include <assert.h> |
| 23 | #include <time.h> |
| 24 | |
| @@ -72,12 +77,13 @@ | |
| 72 | @ -- Remaining characters determine the specific event. For example, |
| 73 | @ -- 'c4413' means check-in with rid=4413. |
| 74 | @ -- |
| 75 | @ CREATE TABLE repository.pending_alert( |
| 76 | @ eventid TEXT PRIMARY KEY, -- Object that changed |
| 77 | @ sentSep BOOLEAN DEFAULT false, -- individual emails sent |
| 78 | @ sentDigest BOOLEAN DEFAULT false -- digest emails sent |
| 79 | @ ) WITHOUT ROWID; |
| 80 | @ |
| 81 | @ DROP TABLE IF EXISTS repository.email_bounce; |
| 82 | @ -- Record bounced emails. If too many bounces are received within |
| 83 | @ -- some defined time range, then cancel the subscription. Older |
| @@ -110,10 +116,15 @@ | |
| 110 | ){ |
| 111 | return; /* Don't create table for disabled email */ |
| 112 | } |
| 113 | db_multi_exec(zEmailInit/*works-like:""*/); |
| 114 | email_triggers_enable(); |
| 115 | } |
| 116 | } |
| 117 | |
| 118 | /* |
| 119 | ** Enable triggers that automatically populate the pending_alert |
| @@ -289,18 +300,10 @@ | |
| 289 | @ <p>This is the email for the human administrator for the system. |
| 290 | @ Abuse and trouble reports are send here. |
| 291 | @ (Property: "email-admin")</p> |
| 292 | @ <hr> |
| 293 | |
| 294 | entry_attribute("Inbound email directory", 40, "email-receive-dir", |
| 295 | "erdir", "", 0); |
| 296 | @ <p>Inbound emails can be stored in a directory for analysis as |
| 297 | @ a debugging aid. Put the name of that directory in this entry box. |
| 298 | @ Disable saving of inbound email by making this an empty string. |
| 299 | @ Abuse and trouble reports are send here. |
| 300 | @ (Property: "email-receive-dir")</p> |
| 301 | @ <hr> |
| 302 | @ <p><input type="submit" name="submit" value="Apply Changes" /></p> |
| 303 | @ </div></form> |
| 304 | db_end_transaction(0); |
| 305 | style_footer(); |
| 306 | } |
| @@ -568,27 +571,27 @@ | |
| 568 | return 0; |
| 569 | } |
| 570 | |
| 571 | /* |
| 572 | ** Make a copy of the input string up to but not including the |
| 573 | ** first ">" character. |
| 574 | ** |
| 575 | ** Verify that the string really that is to be copied really is a |
| 576 | ** valid email address. If it is not, then return NULL. |
| 577 | ** |
| 578 | ** This routine is more restrictive than necessary. It does not |
| 579 | ** allow comments, IP address, quoted strings, or certain uncommon |
| 580 | ** characters. The only non-alphanumerics allowed in the local |
| 581 | ** part are "_", "+", "-" and "+". |
| 582 | */ |
| 583 | char *email_copy_addr(const char *z){ |
| 584 | int i; |
| 585 | int nAt = 0; |
| 586 | int nDot = 0; |
| 587 | char c; |
| 588 | if( z[0]=='.' ) return 0; /* Local part cannot begin with "." */ |
| 589 | for(i=0; (c = z[i])!=0 && c!='>'; i++){ |
| 590 | if( fossil_isalnum(c) ){ |
| 591 | /* Alphanumerics are always ok */ |
| 592 | }else if( c=='@' ){ |
| 593 | if( nAt ) return 0; /* Only a single "@" allowed */ |
| 594 | if( i>64 ) return 0; /* Local part too big */ |
| @@ -598,22 +601,22 @@ | |
| 598 | if( z[i-1]=='.' ) return 0; /* Last char of local cannot be "." */ |
| 599 | if( z[i+1]=='.' || z[i+1]=='-' ){ |
| 600 | return 0; /* Domain cannot begin with "." or "-" */ |
| 601 | } |
| 602 | }else if( c=='-' ){ |
| 603 | if( z[i+1]=='>' ) return 0; /* Last character cannot be "-" */ |
| 604 | }else if( c=='.' ){ |
| 605 | if( z[i+1]=='.' ) return 0; /* Do not allow ".." */ |
| 606 | if( z[i+1]=='>' ) return 0; /* Domain may not end with . */ |
| 607 | nDot++; |
| 608 | }else if( (c=='_' || c=='+') && nAt==0 ){ |
| 609 | /* _ and + are ok in the local part */ |
| 610 | }else{ |
| 611 | return 0; /* Anything else is an error */ |
| 612 | } |
| 613 | } |
| 614 | if( c!='>' ) return 0; /* Missing final ">" */ |
| 615 | if( nAt==0 ) return 0; /* No "@" found anywhere */ |
| 616 | if( nDot==0 ) return 0; /* No "." in the domain */ |
| 617 | |
| 618 | /* If we reach this point, the email address is valid */ |
| 619 | return mprintf("%.*s", i, z); |
| @@ -631,11 +634,11 @@ | |
| 631 | int i; |
| 632 | |
| 633 | email_header_value(pMsg, "to", &v); |
| 634 | z = blob_str(&v); |
| 635 | for(i=0; z[i]; i++){ |
| 636 | if( z[i]=='<' && (zAddr = email_copy_addr(&z[i+1]))!=0 ){ |
| 637 | azTo = fossil_realloc(azTo, sizeof(azTo[0])*(nTo+1) ); |
| 638 | azTo[nTo++] = zAddr; |
| 639 | } |
| 640 | } |
| 641 | *pnTo = nTo; |
| @@ -691,16 +694,18 @@ | |
| 691 | pOut = &all; |
| 692 | } |
| 693 | blob_append(pOut, blob_buffer(pHdr), blob_size(pHdr)); |
| 694 | blob_appendf(pOut, "From: <%s>\r\n", p->zFrom); |
| 695 | blob_appendf(pOut, "Date: %z\r\n", cgi_rfc822_datestamp(time(0))); |
| 696 | /* Message-id format: "<$(date)x$(random).$(from)>" where $(date) is |
| 697 | ** the current unix-time in hex, $(random) is a 64-bit random number, |
| 698 | ** and $(from) is the sender. */ |
| 699 | sqlite3_randomness(sizeof(r1), &r1); |
| 700 | r2 = time(0); |
| 701 | blob_appendf(pOut, "Message-Id: <%llxx%016llx.%s>\r\n", r2, r1, p->zFrom); |
| 702 | blob_add_final_newline(pBody); |
| 703 | blob_appendf(pOut,"Content-Type: text/plain\r\n"); |
| 704 | #if 0 |
| 705 | blob_appendf(pOut, "Content-Transfer-Encoding: base64\r\n\r\n"); |
| 706 | append_base64(pOut, pBody); |
| @@ -752,24 +757,10 @@ | |
| 752 | fossil_print("%s", blob_str(&all)); |
| 753 | } |
| 754 | blob_reset(&all); |
| 755 | } |
| 756 | |
| 757 | /* |
| 758 | ** Analyze and act on a received email. |
| 759 | ** |
| 760 | ** This routine takes ownership of the Blob parameter and is responsible |
| 761 | ** for freeing that blob when it is done with it. |
| 762 | ** |
| 763 | ** This routine acts on all email messages received from the |
| 764 | ** "fossil email inbound" command. |
| 765 | */ |
| 766 | void email_receive(Blob *pMsg){ |
| 767 | /* To Do: Look for bounce messages and possibly disable subscriptions */ |
| 768 | blob_reset(pMsg); |
| 769 | } |
| 770 | |
| 771 | /* |
| 772 | ** SETTING: email-send-method width=5 default=off |
| 773 | ** Determine the method used to send email. Allowed values are |
| 774 | ** "off", "relay", "pipe", "dir", "db", and "stdout". The "off" value |
| 775 | ** means no email is ever sent. The "relay" value means emails are sent |
| @@ -801,16 +792,10 @@ | |
| 801 | /* |
| 802 | ** SETTING: email-self width=40 |
| 803 | ** This is the email address for the repository. Outbound emails add |
| 804 | ** this email address as the "From:" field. |
| 805 | */ |
| 806 | /* |
| 807 | ** SETTING: email-receive-dir width=40 |
| 808 | ** Inbound email messages are saved as separate files in this directory, |
| 809 | ** for debugging analysis. Disable saving of inbound emails omitting |
| 810 | ** this setting, or making it an empty string. |
| 811 | */ |
| 812 | /* |
| 813 | ** SETTING: email-send-relayhost width=40 |
| 814 | ** This is the hostname and TCP port to which output email messages |
| 815 | ** are sent when email-send-method is "relay". There should be an |
| 816 | ** SMTP server configured as a Mail Submission Agent listening on the |
| @@ -817,80 +802,72 @@ | |
| 817 | ** designated host and port and all times. |
| 818 | */ |
| 819 | |
| 820 | |
| 821 | /* |
| 822 | ** COMMAND: email |
| 823 | ** |
| 824 | ** Usage: %fossil email SUBCOMMAND ARGS... |
| 825 | ** |
| 826 | ** Subcommands: |
| 827 | ** |
| 828 | ** exec Compose and send pending email alerts. |
| 829 | ** Some installations may want to do this via |
| 830 | ** a cron-job to make sure alerts are sent |
| 831 | ** in a timely manner. |
| 832 | ** Options: |
| 833 | ** |
| 834 | ** --digest Send digests |
| 835 | ** --test Resets to standard output |
| 836 | ** |
| 837 | ** inbound [FILE] Receive an inbound email message. This message |
| 838 | ** is analyzed to see if it is a bounce, and if |
| 839 | ** necessary, subscribers may be disabled. |
| 840 | ** |
| 841 | ** reset Hard reset of all email notification tables |
| 842 | ** in the repository. This erases all subscription |
| 843 | ** information. Use with extreme care. |
| 844 | ** |
| 845 | ** send TO [OPTIONS] Send a single email message using whatever |
| 846 | ** email sending mechanism is currently configured. |
| 847 | ** Use this for testing the email configuration. |
| 848 | ** Options: |
| 849 | ** |
| 850 | ** --body FILENAME |
| 851 | ** --smtp-trace |
| 852 | ** --stdout |
| 853 | ** --subject|-S SUBJECT |
| 854 | ** |
| 855 | ** settings [NAME VALUE] With no arguments, list all email settings. |
| 856 | ** Or change the value of a single email setting. |
| 857 | ** |
| 858 | ** subscribers [PATTERN] List all subscribers matching PATTERN. |
| 859 | ** |
| 860 | ** unsubscribe EMAIL Remove a single subscriber with the given EMAIL. |
| 861 | */ |
| 862 | void email_cmd(void){ |
| 863 | const char *zCmd; |
| 864 | int nCmd; |
| 865 | db_find_and_open_repository(0, 0); |
| 866 | email_schema(0); |
| 867 | zCmd = g.argc>=3 ? g.argv[2] : "x"; |
| 868 | nCmd = (int)strlen(zCmd); |
| 869 | if( strncmp(zCmd, "exec", nCmd)==0 ){ |
| 870 | u32 eFlags = 0; |
| 871 | if( find_option("digest",0,0)!=0 ) eFlags |= SENDALERT_DIGEST; |
| 872 | if( find_option("test",0,0)!=0 ){ |
| 873 | eFlags |= SENDALERT_PRESERVE|SENDALERT_STDOUT; |
| 874 | } |
| 875 | verify_all_options(); |
| 876 | email_send_alerts(eFlags); |
| 877 | }else |
| 878 | if( strncmp(zCmd, "inbound", nCmd)==0 ){ |
| 879 | Blob email; |
| 880 | const char *zInboundDir = db_get("email-receive-dir",""); |
| 881 | verify_all_options(); |
| 882 | if( g.argc!=3 && g.argc!=4 ){ |
| 883 | usage("inbound [FILE]"); |
| 884 | } |
| 885 | blob_read_from_file(&email, g.argc==3 ? "-" : g.argv[3], ExtFILE); |
| 886 | if( zInboundDir[0] ){ |
| 887 | char *zFN = file_time_tempname(zInboundDir,".email"); |
| 888 | blob_write_to_file(&email, zFN); |
| 889 | fossil_free(zFN); |
| 890 | } |
| 891 | email_receive(&email); |
| 892 | }else |
| 893 | if( strncmp(zCmd, "reset", nCmd)==0 ){ |
| 894 | int c; |
| 895 | int bForce = find_option("force","f",0)!=0; |
| 896 | verify_all_options(); |
| @@ -918,43 +895,17 @@ | |
| 918 | ); |
| 919 | email_schema(0); |
| 920 | } |
| 921 | }else |
| 922 | if( strncmp(zCmd, "send", nCmd)==0 ){ |
| 923 | Blob prompt, body, hdr; |
| 924 | const char *zDest = find_option("stdout",0,0)!=0 ? "stdout" : 0; |
| 925 | int i; |
| 926 | u32 mFlags = EMAIL_IMMEDIATE_FAIL; |
| 927 | const char *zSubject = find_option("subject", "S", 1); |
| 928 | const char *zSource = find_option("body", 0, 1); |
| 929 | EmailSender *pSender; |
| 930 | if( find_option("smtp-trace",0,0)!=0 ) mFlags |= EMAIL_TRACE; |
| 931 | verify_all_options(); |
| 932 | blob_init(&prompt, 0, 0); |
| 933 | blob_init(&body, 0, 0); |
| 934 | blob_init(&hdr, 0, 0); |
| 935 | blob_appendf(&hdr,"To: "); |
| 936 | for(i=3; i<g.argc; i++){ |
| 937 | if( i>3 ) blob_append(&hdr, ", ", 2); |
| 938 | blob_appendf(&hdr, "<%s>", g.argv[i]); |
| 939 | } |
| 940 | blob_append(&hdr,"\r\n",2); |
| 941 | if( zSubject ){ |
| 942 | blob_appendf(&hdr, "Subject: %s\r\n", zSubject); |
| 943 | } |
| 944 | if( zSource ){ |
| 945 | blob_read_from_file(&body, zSource, ExtFILE); |
| 946 | }else{ |
| 947 | prompt_for_user_comment(&body, &prompt); |
| 948 | } |
| 949 | blob_add_final_newline(&body); |
| 950 | pSender = email_sender_new(zDest, mFlags); |
| 951 | email_send(pSender, &hdr, &body); |
| 952 | email_sender_free(pSender); |
| 953 | blob_reset(&hdr); |
| 954 | blob_reset(&body); |
| 955 | blob_reset(&prompt); |
| 956 | }else |
| 957 | if( strncmp(zCmd, "settings", nCmd)==0 ){ |
| 958 | int isGlobal = find_option("global",0,0)!=0; |
| 959 | int nSetting; |
| 960 | const Setting *pSetting = setting_info(&nSetting); |
| @@ -974,10 +925,32 @@ | |
| 974 | for(; nSetting>0; nSetting--, pSetting++ ){ |
| 975 | if( strncmp(pSetting->name,"email-",6)!=0 ) continue; |
| 976 | print_setting(pSetting); |
| 977 | } |
| 978 | }else |
| 979 | if( strncmp(zCmd, "subscribers", nCmd)==0 ){ |
| 980 | Stmt q; |
| 981 | verify_all_options(); |
| 982 | if( g.argc!=3 && g.argc!=4 ) usage("subscribers [PATTERN]"); |
| 983 | if( g.argc==4 ){ |
| @@ -995,19 +968,54 @@ | |
| 995 | } |
| 996 | while( db_step(&q)==SQLITE_ROW ){ |
| 997 | fossil_print("%s\n", db_column_text(&q, 0)); |
| 998 | } |
| 999 | db_finalize(&q); |
| 1000 | }else |
| 1001 | if( strncmp(zCmd, "unsubscribe", nCmd)==0 ){ |
| 1002 | verify_all_options(); |
| 1003 | if( g.argc!=4 ) usage("unsubscribe EMAIL"); |
| 1004 | db_multi_exec( |
| 1005 | "DELETE FROM subscriber WHERE semail=%Q", g.argv[3]); |
| 1006 | }else |
| 1007 | { |
| 1008 | usage("exec|inbound|reset|send|setting|subscribers|unsubscribe"); |
| 1009 | } |
| 1010 | } |
| 1011 | |
| 1012 | /* |
| 1013 | ** Do error checking on a submitted subscription form. Return TRUE |
| @@ -1784,11 +1792,13 @@ | |
| 1784 | /* |
| 1785 | ** A single event that might appear in an alert is recorded as an |
| 1786 | ** instance of the following object. |
| 1787 | */ |
| 1788 | struct EmailEvent { |
| 1789 | int type; /* 'c', 't', 'w', etc. */ |
| 1790 | Blob txt; /* Text description to appear in an alert */ |
| 1791 | EmailEvent *pNext; /* Next in chronological order */ |
| 1792 | }; |
| 1793 | #endif |
| 1794 | |
| @@ -1797,10 +1807,11 @@ | |
| 1797 | */ |
| 1798 | void email_free_eventlist(EmailEvent *p){ |
| 1799 | while( p ){ |
| 1800 | EmailEvent *pNext = p->pNext; |
| 1801 | blob_reset(&p->txt); |
| 1802 | fossil_free(p); |
| 1803 | p = pNext; |
| 1804 | } |
| 1805 | } |
| 1806 | |
| @@ -1807,68 +1818,152 @@ | |
| 1807 | /* |
| 1808 | ** Compute and return a linked list of EmailEvent objects |
| 1809 | ** corresponding to the current content of the temp.wantalert |
| 1810 | ** table which should be defined as follows: |
| 1811 | ** |
| 1812 | ** CREATE TEMP TABLE wantalert(eventId TEXT); |
| 1813 | */ |
| 1814 | EmailEvent *email_compute_event_text(int *pnEvent){ |
| 1815 | Stmt q; |
| 1816 | EmailEvent *p; |
| 1817 | EmailEvent anchor; |
| 1818 | EmailEvent *pLast; |
| 1819 | const char *zUrl = db_get("email-url","http://localhost:8080"); |
| 1820 | |
| 1821 | db_prepare(&q, |
| 1822 | "SELECT" |
| 1823 | " blob.uuid," /* 0 */ |
| 1824 | " datetime(event.mtime)," /* 1 */ |
| 1825 | " coalesce(ecomment,comment)" |
| 1826 | " || ' (user: ' || coalesce(euser,user,'?')" |
| 1827 | " || (SELECT case when length(x)>0 then ' tags: ' || x else '' end" |
| 1828 | " FROM (SELECT group_concat(substr(tagname,5), ', ') AS x" |
| 1829 | " FROM tag, tagxref" |
| 1830 | " WHERE tagname GLOB 'sym-*' AND tag.tagid=tagxref.tagid" |
| 1831 | " AND tagxref.rid=blob.rid AND tagxref.tagtype>0))" |
| 1832 | " || ')' as comment," /* 2 */ |
| 1833 | " tagxref.value AS branch," /* 3 */ |
| 1834 | " wantalert.eventId" /* 4 */ |
| 1835 | " FROM temp.wantalert JOIN tag CROSS JOIN event CROSS JOIN blob" |
| 1836 | " LEFT JOIN tagxref ON tagxref.tagid=tag.tagid" |
| 1837 | " AND tagxref.tagtype>0" |
| 1838 | " AND tagxref.rid=blob.rid" |
| 1839 | " WHERE blob.rid=event.objid" |
| 1840 | " AND tag.tagname='branch'" |
| 1841 | " AND event.objid=substr(wantalert.eventId,2)+0" |
| 1842 | " ORDER BY event.mtime" |
| 1843 | ); |
| 1844 | memset(&anchor, 0, sizeof(anchor)); |
| 1845 | pLast = &anchor; |
| 1846 | *pnEvent = 0; |
| 1847 | while( db_step(&q)==SQLITE_ROW ){ |
| 1848 | const char *zType = ""; |
| 1849 | p = fossil_malloc( sizeof(EmailEvent) ); |
| 1850 | pLast->pNext = p; |
| 1851 | pLast = p; |
| 1852 | p->type = db_column_text(&q, 4)[0]; |
| 1853 | p->pNext = 0; |
| 1854 | switch( p->type ){ |
| 1855 | case 'c': zType = "Check-In"; break; |
| 1856 | case 't': zType = "Wiki Edit"; break; |
| 1857 | case 'w': zType = "Ticket Change"; break; |
| 1858 | } |
| 1859 | blob_init(&p->txt, 0, 0); |
| 1860 | blob_appendf(&p->txt,"== %s %s ==\n%s\n%s/info/%.20s\n", |
| 1861 | db_column_text(&q,1), |
| 1862 | zType, |
| 1863 | db_column_text(&q,2), |
| 1864 | zUrl, |
| 1865 | db_column_text(&q,0) |
| 1866 | ); |
| 1867 | (*pnEvent)++; |
| 1868 | } |
| 1869 | db_finalize(&q); |
| 1870 | return anchor.pNext; |
| 1871 | } |
| 1872 | |
| 1873 | /* |
| 1874 | ** Put a header on an alert email |
| @@ -1900,34 +1995,50 @@ | |
| 1900 | ** command line, generate text for all events named in the |
| 1901 | ** pending_alert table. |
| 1902 | ** |
| 1903 | ** This command is intended for testing and debugging the logic |
| 1904 | ** that generates email alert text. |
| 1905 | */ |
| 1906 | void test_alert_cmd(void){ |
| 1907 | Blob out; |
| 1908 | int nEvent; |
| 1909 | EmailEvent *pEvent, *p; |
| 1910 | |
| 1911 | db_find_and_open_repository(0, 0); |
| 1912 | verify_all_options(); |
| 1913 | db_begin_transaction(); |
| 1914 | email_schema(0); |
| 1915 | db_multi_exec("CREATE TEMP TABLE wantalert(eventid TEXT)"); |
| 1916 | if( g.argc==2 ){ |
| 1917 | db_multi_exec("INSERT INTO wantalert SELECT eventid FROM pending_alert"); |
| 1918 | }else{ |
| 1919 | int i; |
| 1920 | for(i=2; i<g.argc; i++){ |
| 1921 | db_multi_exec("INSERT INTO wantalert VALUES(%Q)", g.argv[i]); |
| 1922 | } |
| 1923 | } |
| 1924 | blob_init(&out, 0, 0); |
| 1925 | email_header(&out); |
| 1926 | pEvent = email_compute_event_text(&nEvent); |
| 1927 | for(p=pEvent; p; p=p->pNext){ |
| 1928 | blob_append(&out, "\n", 1); |
| 1929 | blob_append(&out, blob_buffer(&p->txt), blob_size(&p->txt)); |
| 1930 | } |
| 1931 | email_free_eventlist(pEvent); |
| 1932 | email_footer(&out); |
| 1933 | fossil_print("%s", blob_str(&out)); |
| @@ -1936,38 +2047,54 @@ | |
| 1936 | } |
| 1937 | |
| 1938 | /* |
| 1939 | ** COMMAND: test-add-alerts |
| 1940 | ** |
| 1941 | ** Usage: %fossil test-add-alerts [--backoffice] EVENTID ... |
| 1942 | ** |
| 1943 | ** Add one or more events to the pending_alert queue. Use this |
| 1944 | ** command during testing to force email notifications for specific |
| 1945 | ** events. |
| 1946 | ** |
| 1947 | ** EVENTIDs are text. The first character is 'c', 'w', or 't' |
| 1948 | ** for check-in, wiki, or ticket. The remaining text is a |
| 1949 | ** integer that references the EVENT.OBJID value for the event. |
| 1950 | ** Run /timeline?showid to see these OBJID values. |
| 1951 | ** |
| 1952 | ** If the --backoffice option is included, then email_backoffice() is run |
| 1953 | ** after all alerts have been added. This will cause the alerts to |
| 1954 | ** be sent out with the SENDALERT_TRACE option. |
| 1955 | */ |
| 1956 | void test_add_alert_cmd(void){ |
| 1957 | int i; |
| 1958 | int doAuto = find_option("backoffice",0,0)!=0; |
| 1959 | db_find_and_open_repository(0, 0); |
| 1960 | verify_all_options(); |
| 1961 | db_begin_write(); |
| 1962 | email_schema(0); |
| 1963 | for(i=2; i<g.argc; i++){ |
| 1964 | db_multi_exec("REPLACE INTO pending_alert(eventId) VALUES(%Q)", g.argv[i]); |
| 1965 | } |
| 1966 | db_end_transaction(0); |
| 1967 | if( doAuto ){ |
| 1968 | email_backoffice(SENDALERT_TRACE); |
| 1969 | } |
| 1970 | } |
| 1971 | |
| 1972 | #if INTERFACE |
| 1973 | /* |
| @@ -1979,11 +2106,35 @@ | |
| 1979 | #define SENDALERT_TRACE 0x0008 /* Trace operation for debugging */ |
| 1980 | |
| 1981 | #endif /* INTERFACE */ |
| 1982 | |
| 1983 | /* |
| 1984 | ** Send alert emails to all subscribers. |
| 1985 | */ |
| 1986 | void email_send_alerts(u32 flags){ |
| 1987 | EmailEvent *pEvents, *p; |
| 1988 | int nEvent = 0; |
| 1989 | Stmt q; |
| @@ -1996,10 +2147,11 @@ | |
| 1996 | EmailSender *pSender = 0; |
| 1997 | u32 senderFlags = 0; |
| 1998 | |
| 1999 | if( g.fSqlTrace ) fossil_trace("-- BEGIN email_send_alerts(%u)\n", flags); |
| 2000 | db_begin_transaction(); |
| 2001 | if( !email_enabled() ) goto send_alerts_done; |
| 2002 | zUrl = db_get("email-url",0); |
| 2003 | if( zUrl==0 ) goto send_alerts_done; |
| 2004 | zRepoName = db_get("email-subname",0); |
| 2005 | if( zRepoName==0 ) goto send_alerts_done; |
| @@ -2009,57 +2161,111 @@ | |
| 2009 | senderFlags |= EMAIL_TRACE; |
| 2010 | } |
| 2011 | pSender = email_sender_new(zDest, senderFlags); |
| 2012 | db_multi_exec( |
| 2013 | "DROP TABLE IF EXISTS temp.wantalert;" |
| 2014 | "CREATE TEMP TABLE wantalert(eventId TEXT);" |
| 2015 | ); |
| 2016 | if( flags & SENDALERT_DIGEST ){ |
| 2017 | db_multi_exec( |
| 2018 | "INSERT INTO wantalert SELECT eventid FROM pending_alert" |
| 2019 | " WHERE sentDigest IS FALSE" |
| 2020 | ); |
| 2021 | zDigest = "true"; |
| 2022 | }else{ |
| 2023 | db_multi_exec( |
| 2024 | "INSERT INTO wantalert SELECT eventid FROM pending_alert" |
| 2025 | " WHERE sentSep IS FALSE" |
| 2026 | ); |
| 2027 | } |
| 2028 | pEvents = email_compute_event_text(&nEvent); |
| 2029 | if( nEvent==0 ) goto send_alerts_done; |
| 2030 | blob_init(&hdr, 0, 0); |
| 2031 | blob_init(&body, 0, 0); |
| 2032 | db_prepare(&q, |
| 2033 | "SELECT" |
| 2034 | " hex(subscriberCode)," /* 0 */ |
| 2035 | " semail," /* 1 */ |
| 2036 | " ssub" /* 2 */ |
| 2037 | " FROM subscriber" |
| 2038 | " WHERE sverified AND NOT sdonotcall" |
| 2039 | " AND sdigest IS %s", |
| 2040 | zDigest/*safe-for-%s*/ |
| 2041 | ); |
| 2042 | while( db_step(&q)==SQLITE_ROW ){ |
| 2043 | const char *zCode = db_column_text(&q, 0); |
| 2044 | const char *zSub = db_column_text(&q, 2); |
| 2045 | const char *zEmail = db_column_text(&q, 1); |
| 2046 | int nHit = 0; |
| 2047 | for(p=pEvents; p; p=p->pNext){ |
| 2048 | if( strchr(zSub,p->type)==0 ) continue; |
| 2049 | if( nHit==0 ){ |
| 2050 | blob_appendf(&hdr,"To: <%s>\r\n", zEmail); |
| 2051 | blob_appendf(&hdr,"Subject: %s activity alert\r\n", zRepoName); |
| 2052 | blob_appendf(&body, |
| 2053 | "This is an automated email sent by the Fossil repository " |
| 2054 | "at %s to report changes.\n", |
| 2055 | zUrl |
| 2056 | ); |
| 2057 | } |
| 2058 | nHit++; |
| 2059 | blob_append(&body, "\n", 1); |
| 2060 | blob_append(&body, blob_buffer(&p->txt), blob_size(&p->txt)); |
| 2061 | } |
| 2062 | if( nHit==0 ) continue; |
| 2063 | blob_appendf(&body,"\n-- \nSubscription info: %s/alerts/%s\n", |
| 2064 | zUrl, zCode); |
| 2065 | email_send(pSender,&hdr,&body); |
| @@ -2070,15 +2276,24 @@ | |
| 2070 | blob_reset(&body); |
| 2071 | db_finalize(&q); |
| 2072 | email_free_eventlist(pEvents); |
| 2073 | if( (flags & SENDALERT_PRESERVE)==0 ){ |
| 2074 | if( flags & SENDALERT_DIGEST ){ |
| 2075 | db_multi_exec("UPDATE pending_alert SET sentDigest=true"); |
| 2076 | }else{ |
| 2077 | db_multi_exec("UPDATE pending_alert SET sentSep=true"); |
| 2078 | } |
| 2079 | db_multi_exec("DELETE FROM pending_alert WHERE sentDigest AND sentSep"); |
| 2080 | } |
| 2081 | send_alerts_done: |
| 2082 | email_sender_free(pSender); |
| 2083 | if( g.fSqlTrace ) fossil_trace("-- END email_send_alerts(%u)\n", flags); |
| 2084 | db_end_transaction(0); |
| 2085 |
| --- src/email.c | |
| +++ src/email.c | |
| @@ -1,7 +1,7 @@ | |
| 1 | /* |
| 2 | ** Copyright (c) 2018 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 | ** |
| @@ -14,11 +14,16 @@ | |
| 14 | ** http://www.hwaci.com/drh/ |
| 15 | ** |
| 16 | ******************************************************************************* |
| 17 | ** |
| 18 | ** Logic for email notification, also known as "alerts". |
| 19 | ** |
| 20 | ** Are you looking for the code that reads and writes the internet |
| 21 | ** email protocol? That is not here. See the "smtp.c" file instead. |
| 22 | ** Yes, the choice of source code filenames is not the greatest, but |
| 23 | ** it is not so bad that changing them seems justified. |
| 24 | */ |
| 25 | #include "config.h" |
| 26 | #include "email.h" |
| 27 | #include <assert.h> |
| 28 | #include <time.h> |
| 29 | |
| @@ -72,12 +77,13 @@ | |
| 77 | @ -- Remaining characters determine the specific event. For example, |
| 78 | @ -- 'c4413' means check-in with rid=4413. |
| 79 | @ -- |
| 80 | @ CREATE TABLE repository.pending_alert( |
| 81 | @ eventid TEXT PRIMARY KEY, -- Object that changed |
| 82 | @ sentSep BOOLEAN DEFAULT false, -- individual alert sent |
| 83 | @ sentDigest BOOLEAN DEFAULT false, -- digest alert sent |
| 84 | @ sentMod BOOLEAN DEFAULT false -- pending moderation alert sent |
| 85 | @ ) WITHOUT ROWID; |
| 86 | @ |
| 87 | @ DROP TABLE IF EXISTS repository.email_bounce; |
| 88 | @ -- Record bounced emails. If too many bounces are received within |
| 89 | @ -- some defined time range, then cancel the subscription. Older |
| @@ -110,10 +116,15 @@ | |
| 116 | ){ |
| 117 | return; /* Don't create table for disabled email */ |
| 118 | } |
| 119 | db_multi_exec(zEmailInit/*works-like:""*/); |
| 120 | email_triggers_enable(); |
| 121 | }else if( !db_table_has_column("repository","pending_alert","sentMod") ){ |
| 122 | db_multi_exec( |
| 123 | "ALTER TABLE repository.pending_alert" |
| 124 | " ADD COLUMN sentMod BOOLEAN DEFAULT false;" |
| 125 | ); |
| 126 | } |
| 127 | } |
| 128 | |
| 129 | /* |
| 130 | ** Enable triggers that automatically populate the pending_alert |
| @@ -289,18 +300,10 @@ | |
| 300 | @ <p>This is the email for the human administrator for the system. |
| 301 | @ Abuse and trouble reports are send here. |
| 302 | @ (Property: "email-admin")</p> |
| 303 | @ <hr> |
| 304 | |
| 305 | @ <p><input type="submit" name="submit" value="Apply Changes" /></p> |
| 306 | @ </div></form> |
| 307 | db_end_transaction(0); |
| 308 | style_footer(); |
| 309 | } |
| @@ -568,27 +571,27 @@ | |
| 571 | return 0; |
| 572 | } |
| 573 | |
| 574 | /* |
| 575 | ** Make a copy of the input string up to but not including the |
| 576 | ** first cTerm character. |
| 577 | ** |
| 578 | ** Verify that the string really that is to be copied really is a |
| 579 | ** valid email address. If it is not, then return NULL. |
| 580 | ** |
| 581 | ** This routine is more restrictive than necessary. It does not |
| 582 | ** allow comments, IP address, quoted strings, or certain uncommon |
| 583 | ** characters. The only non-alphanumerics allowed in the local |
| 584 | ** part are "_", "+", "-" and "+". |
| 585 | */ |
| 586 | char *email_copy_addr(const char *z, char cTerm ){ |
| 587 | int i; |
| 588 | int nAt = 0; |
| 589 | int nDot = 0; |
| 590 | char c; |
| 591 | if( z[0]=='.' ) return 0; /* Local part cannot begin with "." */ |
| 592 | for(i=0; (c = z[i])!=0 && c!=cTerm; i++){ |
| 593 | if( fossil_isalnum(c) ){ |
| 594 | /* Alphanumerics are always ok */ |
| 595 | }else if( c=='@' ){ |
| 596 | if( nAt ) return 0; /* Only a single "@" allowed */ |
| 597 | if( i>64 ) return 0; /* Local part too big */ |
| @@ -598,22 +601,22 @@ | |
| 601 | if( z[i-1]=='.' ) return 0; /* Last char of local cannot be "." */ |
| 602 | if( z[i+1]=='.' || z[i+1]=='-' ){ |
| 603 | return 0; /* Domain cannot begin with "." or "-" */ |
| 604 | } |
| 605 | }else if( c=='-' ){ |
| 606 | if( z[i+1]==cTerm ) return 0; /* Last character cannot be "-" */ |
| 607 | }else if( c=='.' ){ |
| 608 | if( z[i+1]=='.' ) return 0; /* Do not allow ".." */ |
| 609 | if( z[i+1]==cTerm ) return 0; /* Domain may not end with . */ |
| 610 | nDot++; |
| 611 | }else if( (c=='_' || c=='+') && nAt==0 ){ |
| 612 | /* _ and + are ok in the local part */ |
| 613 | }else{ |
| 614 | return 0; /* Anything else is an error */ |
| 615 | } |
| 616 | } |
| 617 | if( c!=cTerm ) return 0; /* Missing terminator */ |
| 618 | if( nAt==0 ) return 0; /* No "@" found anywhere */ |
| 619 | if( nDot==0 ) return 0; /* No "." in the domain */ |
| 620 | |
| 621 | /* If we reach this point, the email address is valid */ |
| 622 | return mprintf("%.*s", i, z); |
| @@ -631,11 +634,11 @@ | |
| 634 | int i; |
| 635 | |
| 636 | email_header_value(pMsg, "to", &v); |
| 637 | z = blob_str(&v); |
| 638 | for(i=0; z[i]; i++){ |
| 639 | if( z[i]=='<' && (zAddr = email_copy_addr(&z[i+1],'>'))!=0 ){ |
| 640 | azTo = fossil_realloc(azTo, sizeof(azTo[0])*(nTo+1) ); |
| 641 | azTo[nTo++] = zAddr; |
| 642 | } |
| 643 | } |
| 644 | *pnTo = nTo; |
| @@ -691,16 +694,18 @@ | |
| 694 | pOut = &all; |
| 695 | } |
| 696 | blob_append(pOut, blob_buffer(pHdr), blob_size(pHdr)); |
| 697 | blob_appendf(pOut, "From: <%s>\r\n", p->zFrom); |
| 698 | blob_appendf(pOut, "Date: %z\r\n", cgi_rfc822_datestamp(time(0))); |
| 699 | if( strstr(blob_str(pHdr), "\r\nMessage-Id:")==0 ){ |
| 700 | /* Message-id format: "<$(date)x$(random).$(from)>" where $(date) is |
| 701 | ** the current unix-time in hex, $(random) is a 64-bit random number, |
| 702 | ** and $(from) is the sender. */ |
| 703 | sqlite3_randomness(sizeof(r1), &r1); |
| 704 | r2 = time(0); |
| 705 | blob_appendf(pOut, "Message-Id: <%llxx%016llx.%s>\r\n", r2, r1, p->zFrom); |
| 706 | } |
| 707 | blob_add_final_newline(pBody); |
| 708 | blob_appendf(pOut,"Content-Type: text/plain\r\n"); |
| 709 | #if 0 |
| 710 | blob_appendf(pOut, "Content-Transfer-Encoding: base64\r\n\r\n"); |
| 711 | append_base64(pOut, pBody); |
| @@ -752,24 +757,10 @@ | |
| 757 | fossil_print("%s", blob_str(&all)); |
| 758 | } |
| 759 | blob_reset(&all); |
| 760 | } |
| 761 | |
| 762 | /* |
| 763 | ** SETTING: email-send-method width=5 default=off |
| 764 | ** Determine the method used to send email. Allowed values are |
| 765 | ** "off", "relay", "pipe", "dir", "db", and "stdout". The "off" value |
| 766 | ** means no email is ever sent. The "relay" value means emails are sent |
| @@ -801,16 +792,10 @@ | |
| 792 | /* |
| 793 | ** SETTING: email-self width=40 |
| 794 | ** This is the email address for the repository. Outbound emails add |
| 795 | ** this email address as the "From:" field. |
| 796 | */ |
| 797 | /* |
| 798 | ** SETTING: email-send-relayhost width=40 |
| 799 | ** This is the hostname and TCP port to which output email messages |
| 800 | ** are sent when email-send-method is "relay". There should be an |
| 801 | ** SMTP server configured as a Mail Submission Agent listening on the |
| @@ -817,80 +802,72 @@ | |
| 802 | ** designated host and port and all times. |
| 803 | */ |
| 804 | |
| 805 | |
| 806 | /* |
| 807 | ** COMMAND: alerts |
| 808 | ** |
| 809 | ** Usage: %fossil alerts SUBCOMMAND ARGS... |
| 810 | ** |
| 811 | ** Subcommands: |
| 812 | ** |
| 813 | ** pending Show all pending alerts. Useful for debugging. |
| 814 | ** |
| 815 | ** reset Hard reset of all email notification tables |
| 816 | ** in the repository. This erases all subscription |
| 817 | ** information. ** Use with extreme care ** |
| 818 | ** |
| 819 | ** send Compose and send pending email alerts. |
| 820 | ** Some installations may want to do this via |
| 821 | ** a cron-job to make sure alerts are sent |
| 822 | ** in a timely manner. |
| 823 | ** Options: |
| 824 | ** |
| 825 | ** --digest Send digests |
| 826 | ** --test Write to standard output |
| 827 | ** |
| 828 | ** settings [NAME VALUE] With no arguments, list all email settings. |
| 829 | ** Or change the value of a single email setting. |
| 830 | ** |
| 831 | ** status Report on the status of the email alert |
| 832 | ** subsystem |
| 833 | ** |
| 834 | ** subscribers [PATTERN] List all subscribers matching PATTERN. |
| 835 | ** |
| 836 | ** test-message TO [OPTS] Send a single email message using whatever |
| 837 | ** email sending mechanism is currently configured. |
| 838 | ** Use this for testing the email notification |
| 839 | ** configuration. Options: |
| 840 | ** |
| 841 | ** --body FILENAME |
| 842 | ** --smtp-trace |
| 843 | ** --stdout |
| 844 | ** --subject|-S SUBJECT |
| 845 | ** |
| 846 | ** unsubscribe EMAIL Remove a single subscriber with the given EMAIL. |
| 847 | */ |
| 848 | void email_cmd(void){ |
| 849 | const char *zCmd; |
| 850 | int nCmd; |
| 851 | db_find_and_open_repository(0, 0); |
| 852 | email_schema(0); |
| 853 | zCmd = g.argc>=3 ? g.argv[2] : "x"; |
| 854 | nCmd = (int)strlen(zCmd); |
| 855 | if( strncmp(zCmd, "pending", nCmd)==0 ){ |
| 856 | Stmt q; |
| 857 | verify_all_options(); |
| 858 | if( g.argc!=3 ) usage("pending"); |
| 859 | db_prepare(&q,"SELECT eventid, sentSep, sentDigest, sentMod" |
| 860 | " FROM pending_alert"); |
| 861 | while( db_step(&q)==SQLITE_ROW ){ |
| 862 | fossil_print("%10s %7s %10s %7s\n", |
| 863 | db_column_text(&q,0), |
| 864 | db_column_int(&q,1) ? "sentSep" : "", |
| 865 | db_column_int(&q,2) ? "sentDigest" : "", |
| 866 | db_column_int(&q,3) ? "sentMod" : ""); |
| 867 | } |
| 868 | db_finalize(&q); |
| 869 | }else |
| 870 | if( strncmp(zCmd, "reset", nCmd)==0 ){ |
| 871 | int c; |
| 872 | int bForce = find_option("force","f",0)!=0; |
| 873 | verify_all_options(); |
| @@ -918,43 +895,17 @@ | |
| 895 | ); |
| 896 | email_schema(0); |
| 897 | } |
| 898 | }else |
| 899 | if( strncmp(zCmd, "send", nCmd)==0 ){ |
| 900 | u32 eFlags = 0; |
| 901 | if( find_option("digest",0,0)!=0 ) eFlags |= SENDALERT_DIGEST; |
| 902 | if( find_option("test",0,0)!=0 ){ |
| 903 | eFlags |= SENDALERT_PRESERVE|SENDALERT_STDOUT; |
| 904 | } |
| 905 | verify_all_options(); |
| 906 | email_send_alerts(eFlags); |
| 907 | }else |
| 908 | if( strncmp(zCmd, "settings", nCmd)==0 ){ |
| 909 | int isGlobal = find_option("global",0,0)!=0; |
| 910 | int nSetting; |
| 911 | const Setting *pSetting = setting_info(&nSetting); |
| @@ -974,10 +925,32 @@ | |
| 925 | for(; nSetting>0; nSetting--, pSetting++ ){ |
| 926 | if( strncmp(pSetting->name,"email-",6)!=0 ) continue; |
| 927 | print_setting(pSetting); |
| 928 | } |
| 929 | }else |
| 930 | if( strncmp(zCmd, "status", nCmd)==0 ){ |
| 931 | int nSetting, n; |
| 932 | static const char *zFmt = "%-29s %d\n"; |
| 933 | const Setting *pSetting = setting_info(&nSetting); |
| 934 | db_open_config(1, 0); |
| 935 | verify_all_options(); |
| 936 | if( g.argc!=3 ) usage("status"); |
| 937 | pSetting = setting_info(&nSetting); |
| 938 | for(; nSetting>0; nSetting--, pSetting++ ){ |
| 939 | if( strncmp(pSetting->name,"email-",6)!=0 ) continue; |
| 940 | print_setting(pSetting); |
| 941 | } |
| 942 | n = db_int(0,"SELECT count(*) FROM pending_alert WHERE NOT sentSep"); |
| 943 | fossil_print(zFmt/*works-like:"%s%d"*/, "pending-alerts", n); |
| 944 | n = db_int(0,"SELECT count(*) FROM pending_alert WHERE NOT sentDigest"); |
| 945 | fossil_print(zFmt/*works-like:"%s%d"*/, "pending-digest-alerts", n); |
| 946 | n = db_int(0,"SELECT count(*) FROM subscriber"); |
| 947 | fossil_print(zFmt/*works-like:"%s%d"*/, "total-subscribers", n); |
| 948 | n = db_int(0, "SELECT count(*) FROM subscriber WHERE sverified" |
| 949 | " AND NOT sdonotcall AND length(ssub)>1"); |
| 950 | fossil_print(zFmt/*works-like:"%s%d"*/, "active-subscribers", n); |
| 951 | }else |
| 952 | if( strncmp(zCmd, "subscribers", nCmd)==0 ){ |
| 953 | Stmt q; |
| 954 | verify_all_options(); |
| 955 | if( g.argc!=3 && g.argc!=4 ) usage("subscribers [PATTERN]"); |
| 956 | if( g.argc==4 ){ |
| @@ -995,19 +968,54 @@ | |
| 968 | } |
| 969 | while( db_step(&q)==SQLITE_ROW ){ |
| 970 | fossil_print("%s\n", db_column_text(&q, 0)); |
| 971 | } |
| 972 | db_finalize(&q); |
| 973 | }else |
| 974 | if( strncmp(zCmd, "test-message", nCmd)==0 ){ |
| 975 | Blob prompt, body, hdr; |
| 976 | const char *zDest = find_option("stdout",0,0)!=0 ? "stdout" : 0; |
| 977 | int i; |
| 978 | u32 mFlags = EMAIL_IMMEDIATE_FAIL; |
| 979 | const char *zSubject = find_option("subject", "S", 1); |
| 980 | const char *zSource = find_option("body", 0, 1); |
| 981 | EmailSender *pSender; |
| 982 | if( find_option("smtp-trace",0,0)!=0 ) mFlags |= EMAIL_TRACE; |
| 983 | verify_all_options(); |
| 984 | blob_init(&prompt, 0, 0); |
| 985 | blob_init(&body, 0, 0); |
| 986 | blob_init(&hdr, 0, 0); |
| 987 | blob_appendf(&hdr,"To: "); |
| 988 | for(i=3; i<g.argc; i++){ |
| 989 | if( i>3 ) blob_append(&hdr, ", ", 2); |
| 990 | blob_appendf(&hdr, "<%s>", g.argv[i]); |
| 991 | } |
| 992 | blob_append(&hdr,"\r\n",2); |
| 993 | if( zSubject==0 ) zSubject = "fossil alerts test-message"; |
| 994 | blob_appendf(&hdr, "Subject: %s\r\n", zSubject); |
| 995 | if( zSource ){ |
| 996 | blob_read_from_file(&body, zSource, ExtFILE); |
| 997 | }else{ |
| 998 | prompt_for_user_comment(&body, &prompt); |
| 999 | } |
| 1000 | blob_add_final_newline(&body); |
| 1001 | pSender = email_sender_new(zDest, mFlags); |
| 1002 | email_send(pSender, &hdr, &body); |
| 1003 | email_sender_free(pSender); |
| 1004 | blob_reset(&hdr); |
| 1005 | blob_reset(&body); |
| 1006 | blob_reset(&prompt); |
| 1007 | }else |
| 1008 | if( strncmp(zCmd, "unsubscribe", nCmd)==0 ){ |
| 1009 | verify_all_options(); |
| 1010 | if( g.argc!=4 ) usage("unsubscribe EMAIL"); |
| 1011 | db_multi_exec( |
| 1012 | "DELETE FROM subscriber WHERE semail=%Q", g.argv[3]); |
| 1013 | }else |
| 1014 | { |
| 1015 | usage("pending|reset|send|setting|status|" |
| 1016 | "subscribers|test-message|unsubscribe"); |
| 1017 | } |
| 1018 | } |
| 1019 | |
| 1020 | /* |
| 1021 | ** Do error checking on a submitted subscription form. Return TRUE |
| @@ -1784,11 +1792,13 @@ | |
| 1792 | /* |
| 1793 | ** A single event that might appear in an alert is recorded as an |
| 1794 | ** instance of the following object. |
| 1795 | */ |
| 1796 | struct EmailEvent { |
| 1797 | int type; /* 'c', 'f', 'm', 't', 'w' */ |
| 1798 | int needMod; /* Pending moderator approval */ |
| 1799 | Blob hdr; /* Header content, for forum entries */ |
| 1800 | Blob txt; /* Text description to appear in an alert */ |
| 1801 | EmailEvent *pNext; /* Next in chronological order */ |
| 1802 | }; |
| 1803 | #endif |
| 1804 | |
| @@ -1797,10 +1807,11 @@ | |
| 1807 | */ |
| 1808 | void email_free_eventlist(EmailEvent *p){ |
| 1809 | while( p ){ |
| 1810 | EmailEvent *pNext = p->pNext; |
| 1811 | blob_reset(&p->txt); |
| 1812 | blob_reset(&p->hdr); |
| 1813 | fossil_free(p); |
| 1814 | p = pNext; |
| 1815 | } |
| 1816 | } |
| 1817 | |
| @@ -1807,68 +1818,152 @@ | |
| 1818 | /* |
| 1819 | ** Compute and return a linked list of EmailEvent objects |
| 1820 | ** corresponding to the current content of the temp.wantalert |
| 1821 | ** table which should be defined as follows: |
| 1822 | ** |
| 1823 | ** CREATE TEMP TABLE wantalert(eventId TEXT, needMod BOOLEAN); |
| 1824 | */ |
| 1825 | EmailEvent *email_compute_event_text(int *pnEvent, int doDigest){ |
| 1826 | Stmt q; |
| 1827 | EmailEvent *p; |
| 1828 | EmailEvent anchor; |
| 1829 | EmailEvent *pLast; |
| 1830 | const char *zUrl = db_get("email-url","http://localhost:8080"); |
| 1831 | const char *zFrom; |
| 1832 | const char *zSub; |
| 1833 | |
| 1834 | |
| 1835 | /* First do non-forum post events */ |
| 1836 | db_prepare(&q, |
| 1837 | "SELECT" |
| 1838 | " blob.uuid," /* 0 */ |
| 1839 | " datetime(event.mtime)," /* 1 */ |
| 1840 | " coalesce(ecomment,comment)" |
| 1841 | " || ' (user: ' || coalesce(euser,user,'?')" |
| 1842 | " || (SELECT case when length(x)>0 then ' tags: ' || x else '' end" |
| 1843 | " FROM (SELECT group_concat(substr(tagname,5), ', ') AS x" |
| 1844 | " FROM tag, tagxref" |
| 1845 | " WHERE tagname GLOB 'sym-*' AND tag.tagid=tagxref.tagid" |
| 1846 | " AND tagxref.rid=blob.rid AND tagxref.tagtype>0))" |
| 1847 | " || ')' as comment," /* 2 */ |
| 1848 | " wantalert.eventId," /* 3 */ |
| 1849 | " wantalert.needMod" /* 4 */ |
| 1850 | " FROM temp.wantalert CROSS JOIN event CROSS JOIN blob" |
| 1851 | " WHERE blob.rid=event.objid" |
| 1852 | " AND event.objid=substr(wantalert.eventId,2)+0" |
| 1853 | " AND (%d OR eventId NOT GLOB 'f*')" |
| 1854 | " ORDER BY event.mtime", |
| 1855 | doDigest |
| 1856 | ); |
| 1857 | memset(&anchor, 0, sizeof(anchor)); |
| 1858 | pLast = &anchor; |
| 1859 | *pnEvent = 0; |
| 1860 | while( db_step(&q)==SQLITE_ROW ){ |
| 1861 | const char *zType = ""; |
| 1862 | p = fossil_malloc( sizeof(EmailEvent) ); |
| 1863 | pLast->pNext = p; |
| 1864 | pLast = p; |
| 1865 | p->type = db_column_text(&q, 3)[0]; |
| 1866 | p->needMod = db_column_int(&q, 4); |
| 1867 | p->pNext = 0; |
| 1868 | switch( p->type ){ |
| 1869 | case 'c': zType = "Check-In"; break; |
| 1870 | case 'f': zType = "Forum post"; break; |
| 1871 | case 't': zType = "Wiki Edit"; break; |
| 1872 | case 'w': zType = "Ticket Change"; break; |
| 1873 | } |
| 1874 | blob_init(&p->hdr, 0, 0); |
| 1875 | blob_init(&p->txt, 0, 0); |
| 1876 | blob_appendf(&p->txt,"== %s %s ==\n%s\n%s/info/%.20s\n", |
| 1877 | db_column_text(&q,1), |
| 1878 | zType, |
| 1879 | db_column_text(&q,2), |
| 1880 | zUrl, |
| 1881 | db_column_text(&q,0) |
| 1882 | ); |
| 1883 | if( p->needMod ){ |
| 1884 | blob_appendf(&p->txt, |
| 1885 | "** Pending moderator approval (%s/modreq) **\n", |
| 1886 | zUrl |
| 1887 | ); |
| 1888 | } |
| 1889 | (*pnEvent)++; |
| 1890 | } |
| 1891 | db_finalize(&q); |
| 1892 | |
| 1893 | /* Early-out if forumpost is not a table in this repository */ |
| 1894 | if( !db_table_exists("repository","forumpost") ){ |
| 1895 | return anchor.pNext; |
| 1896 | } |
| 1897 | |
| 1898 | /* For digests, the previous loop also handled forumposts already */ |
| 1899 | if( doDigest ){ |
| 1900 | return anchor.pNext; |
| 1901 | } |
| 1902 | |
| 1903 | /* If we reach this point, it means that forumposts exist and this |
| 1904 | ** is a normal email alert. Construct full-text forum post alerts |
| 1905 | ** using a format that enables them to be sent as separate emails. |
| 1906 | */ |
| 1907 | db_prepare(&q, |
| 1908 | "SELECT" |
| 1909 | " forumpost.fpid," /* 0 */ |
| 1910 | " (SELECT uuid FROM blob WHERE rid=forumpost.fpid)," /* 1 */ |
| 1911 | " datetime(event.mtime)," /* 2 */ |
| 1912 | " substr(comment,instr(comment,':')+2)," /* 3 */ |
| 1913 | " (SELECT uuid FROM blob WHERE rid=forumpost.firt)," /* 4 */ |
| 1914 | " wantalert.needMod" /* 5 */ |
| 1915 | " FROM temp.wantalert, event, forumpost" |
| 1916 | " WHERE event.objid=substr(wantalert.eventId,2)+0" |
| 1917 | " AND eventId GLOB 'f*'" |
| 1918 | " AND forumpost.fpid=event.objid" |
| 1919 | ); |
| 1920 | zFrom = db_get("email-self",0); |
| 1921 | zSub = db_get("email-subname",""); |
| 1922 | while( db_step(&q)==SQLITE_ROW ){ |
| 1923 | Manifest *pPost = manifest_get(db_column_int(&q,0), CFTYPE_FORUM, 0); |
| 1924 | const char *zIrt; |
| 1925 | const char *zUuid; |
| 1926 | const char *zTitle; |
| 1927 | if( pPost==0 ) continue; |
| 1928 | p = fossil_malloc( sizeof(EmailEvent) ); |
| 1929 | pLast->pNext = p; |
| 1930 | pLast = p; |
| 1931 | p->type = 'f'; |
| 1932 | p->needMod = db_column_int(&q, 5); |
| 1933 | p->pNext = 0; |
| 1934 | blob_init(&p->hdr, 0, 0); |
| 1935 | zUuid = db_column_text(&q, 1); |
| 1936 | zTitle = db_column_text(&q, 3); |
| 1937 | if( p->needMod ){ |
| 1938 | blob_appendf(&p->hdr, "Subject: %s Pending Moderation: %s\r\n", |
| 1939 | zSub, zTitle); |
| 1940 | }else{ |
| 1941 | blob_appendf(&p->hdr, "Subject: %s %s\r\n", zSub, zTitle); |
| 1942 | blob_appendf(&p->hdr, "Message-Id: <%s.%s>\r\n", zUuid, zFrom); |
| 1943 | zIrt = db_column_text(&q, 4); |
| 1944 | if( zIrt && zIrt[0] ){ |
| 1945 | blob_appendf(&p->hdr, "In-Reply-To: <%s.%s>\r\n", zIrt, zFrom); |
| 1946 | } |
| 1947 | } |
| 1948 | blob_init(&p->txt, 0, 0); |
| 1949 | if( p->needMod ){ |
| 1950 | blob_appendf(&p->txt, |
| 1951 | "** Pending moderator approval (%s/modreq) **\n", |
| 1952 | zUrl |
| 1953 | ); |
| 1954 | } |
| 1955 | blob_appendf(&p->txt, |
| 1956 | "Forum post by %s on %s\n", |
| 1957 | pPost->zUser, db_column_text(&q, 2)); |
| 1958 | blob_appendf(&p->txt, "%s/forumpost/%S\n\n", zUrl, zUuid); |
| 1959 | blob_append(&p->txt, pPost->zWiki, -1); |
| 1960 | manifest_destroy(pPost); |
| 1961 | (*pnEvent)++; |
| 1962 | } |
| 1963 | db_finalize(&q); |
| 1964 | |
| 1965 | return anchor.pNext; |
| 1966 | } |
| 1967 | |
| 1968 | /* |
| 1969 | ** Put a header on an alert email |
| @@ -1900,34 +1995,50 @@ | |
| 1995 | ** command line, generate text for all events named in the |
| 1996 | ** pending_alert table. |
| 1997 | ** |
| 1998 | ** This command is intended for testing and debugging the logic |
| 1999 | ** that generates email alert text. |
| 2000 | ** |
| 2001 | ** Options: |
| 2002 | ** |
| 2003 | ** --digest Generate digest alert text |
| 2004 | ** --needmod Assume all events are pending moderator approval |
| 2005 | */ |
| 2006 | void test_alert_cmd(void){ |
| 2007 | Blob out; |
| 2008 | int nEvent; |
| 2009 | int needMod; |
| 2010 | int doDigest; |
| 2011 | EmailEvent *pEvent, *p; |
| 2012 | |
| 2013 | doDigest = find_option("digest",0,0)!=0; |
| 2014 | needMod = find_option("needmod",0,0)!=0; |
| 2015 | db_find_and_open_repository(0, 0); |
| 2016 | verify_all_options(); |
| 2017 | db_begin_transaction(); |
| 2018 | email_schema(0); |
| 2019 | db_multi_exec("CREATE TEMP TABLE wantalert(eventid TEXT, needMod BOOLEAN)"); |
| 2020 | if( g.argc==2 ){ |
| 2021 | db_multi_exec( |
| 2022 | "INSERT INTO wantalert(eventId,needMod)" |
| 2023 | " SELECT eventid, %d FROM pending_alert", needMod); |
| 2024 | }else{ |
| 2025 | int i; |
| 2026 | for(i=2; i<g.argc; i++){ |
| 2027 | db_multi_exec("INSERT INTO wantalert(eventId,needMod) VALUES(%Q,%d)", |
| 2028 | g.argv[i], needMod); |
| 2029 | } |
| 2030 | } |
| 2031 | blob_init(&out, 0, 0); |
| 2032 | email_header(&out); |
| 2033 | pEvent = email_compute_event_text(&nEvent, doDigest); |
| 2034 | for(p=pEvent; p; p=p->pNext){ |
| 2035 | blob_append(&out, "\n", 1); |
| 2036 | if( blob_size(&p->hdr) ){ |
| 2037 | blob_append(&out, blob_buffer(&p->hdr), blob_size(&p->hdr)); |
| 2038 | blob_append(&out, "\n", 1); |
| 2039 | } |
| 2040 | blob_append(&out, blob_buffer(&p->txt), blob_size(&p->txt)); |
| 2041 | } |
| 2042 | email_free_eventlist(pEvent); |
| 2043 | email_footer(&out); |
| 2044 | fossil_print("%s", blob_str(&out)); |
| @@ -1936,38 +2047,54 @@ | |
| 2047 | } |
| 2048 | |
| 2049 | /* |
| 2050 | ** COMMAND: test-add-alerts |
| 2051 | ** |
| 2052 | ** Usage: %fossil test-add-alerts [OPTIONS] EVENTID ... |
| 2053 | ** |
| 2054 | ** Add one or more events to the pending_alert queue. Use this |
| 2055 | ** command during testing to force email notifications for specific |
| 2056 | ** events. |
| 2057 | ** |
| 2058 | ** EVENTIDs are text. The first character is 'c', 'f', 't', or 'w' |
| 2059 | ** for check-in, forum, ticket, or wiki. The remaining text is a |
| 2060 | ** integer that references the EVENT.OBJID value for the event. |
| 2061 | ** Run /timeline?showid to see these OBJID values. |
| 2062 | ** |
| 2063 | ** Options: |
| 2064 | ** |
| 2065 | ** --backoffice Run email_backoffice() after all alerts have |
| 2066 | ** been added. This will cause the alerts to be |
| 2067 | ** sent out with the SENDALERT_TRACE option. |
| 2068 | ** |
| 2069 | ** --debug Like --backoffice, but add the SENDALERT_STDOUT |
| 2070 | ** so that emails are printed to standard output |
| 2071 | ** rather than being sent. |
| 2072 | ** |
| 2073 | ** --digest Process emails using SENDALERT_DIGEST |
| 2074 | */ |
| 2075 | void test_add_alert_cmd(void){ |
| 2076 | int i; |
| 2077 | int doAuto = find_option("backoffice",0,0)!=0; |
| 2078 | unsigned mFlags = 0; |
| 2079 | if( find_option("debug",0,0)!=0 ){ |
| 2080 | doAuto = 1; |
| 2081 | mFlags = SENDALERT_STDOUT; |
| 2082 | } |
| 2083 | if( find_option("digest",0,0)!=0 ){ |
| 2084 | mFlags |= SENDALERT_DIGEST; |
| 2085 | } |
| 2086 | db_find_and_open_repository(0, 0); |
| 2087 | verify_all_options(); |
| 2088 | db_begin_write(); |
| 2089 | email_schema(0); |
| 2090 | for(i=2; i<g.argc; i++){ |
| 2091 | db_multi_exec("REPLACE INTO pending_alert(eventId) VALUES(%Q)", g.argv[i]); |
| 2092 | } |
| 2093 | db_end_transaction(0); |
| 2094 | if( doAuto ){ |
| 2095 | email_backoffice(SENDALERT_TRACE|mFlags); |
| 2096 | } |
| 2097 | } |
| 2098 | |
| 2099 | #if INTERFACE |
| 2100 | /* |
| @@ -1979,11 +2106,35 @@ | |
| 2106 | #define SENDALERT_TRACE 0x0008 /* Trace operation for debugging */ |
| 2107 | |
| 2108 | #endif /* INTERFACE */ |
| 2109 | |
| 2110 | /* |
| 2111 | ** Send alert emails to subscribers. |
| 2112 | ** |
| 2113 | ** This procedure is run by either the backoffice, or in response to the |
| 2114 | ** "fossil alerts send" command. Details of operation are controlled by |
| 2115 | ** the flags parameter. |
| 2116 | ** |
| 2117 | ** Here is a summary of what happens: |
| 2118 | ** |
| 2119 | ** (1) Create a TEMP table wantalert(eventId,needMod) and fill it with |
| 2120 | ** all the events that we want to send alerts about. The needMod |
| 2121 | ** flags is set if and only if the event is still awaiting |
| 2122 | ** moderator approval. Events with the needMod flag are only |
| 2123 | ** shown to users that have moderator privileges. |
| 2124 | ** |
| 2125 | ** (2) Call email_compute_event_text() to compute a list of EmailEvent |
| 2126 | ** objects that describe all events about which we want to send |
| 2127 | ** alerts. |
| 2128 | ** |
| 2129 | ** (3) Loop over all subscribers. Compose and send one or more email |
| 2130 | ** messages to each subscriber that describe the events for |
| 2131 | ** which the subscriber has expressed interest and has |
| 2132 | ** appropriate privileges. |
| 2133 | ** |
| 2134 | ** (4) Update the pending_alerts table to indicate that alerts have been |
| 2135 | ** sent. |
| 2136 | */ |
| 2137 | void email_send_alerts(u32 flags){ |
| 2138 | EmailEvent *pEvents, *p; |
| 2139 | int nEvent = 0; |
| 2140 | Stmt q; |
| @@ -1996,10 +2147,11 @@ | |
| 2147 | EmailSender *pSender = 0; |
| 2148 | u32 senderFlags = 0; |
| 2149 | |
| 2150 | if( g.fSqlTrace ) fossil_trace("-- BEGIN email_send_alerts(%u)\n", flags); |
| 2151 | db_begin_transaction(); |
| 2152 | email_schema(0); |
| 2153 | if( !email_enabled() ) goto send_alerts_done; |
| 2154 | zUrl = db_get("email-url",0); |
| 2155 | if( zUrl==0 ) goto send_alerts_done; |
| 2156 | zRepoName = db_get("email-subname",0); |
| 2157 | if( zRepoName==0 ) goto send_alerts_done; |
| @@ -2009,57 +2161,111 @@ | |
| 2161 | senderFlags |= EMAIL_TRACE; |
| 2162 | } |
| 2163 | pSender = email_sender_new(zDest, senderFlags); |
| 2164 | db_multi_exec( |
| 2165 | "DROP TABLE IF EXISTS temp.wantalert;" |
| 2166 | "CREATE TEMP TABLE wantalert(eventId TEXT, needMod BOOLEAN, sentMod);" |
| 2167 | ); |
| 2168 | if( flags & SENDALERT_DIGEST ){ |
| 2169 | /* Unmoderated changes are never sent as part of a digest */ |
| 2170 | db_multi_exec( |
| 2171 | "INSERT INTO wantalert(eventId,needMod)" |
| 2172 | " SELECT eventid, 0" |
| 2173 | " FROM pending_alert" |
| 2174 | " WHERE sentDigest IS FALSE" |
| 2175 | " AND NOT EXISTS(SELECT 1 FROM private WHERE rid=substr(eventid,2));" |
| 2176 | ); |
| 2177 | zDigest = "true"; |
| 2178 | }else{ |
| 2179 | /* Immediate alerts might include events that are subject to |
| 2180 | ** moderator approval */ |
| 2181 | db_multi_exec( |
| 2182 | "INSERT INTO wantalert(eventId,needMod,sentMod)" |
| 2183 | " SELECT eventid," |
| 2184 | " EXISTS(SELECT 1 FROM private WHERE rid=substr(eventid,2))," |
| 2185 | " sentMod" |
| 2186 | " FROM pending_alert" |
| 2187 | " WHERE sentSep IS FALSE;" |
| 2188 | "DELETE FROM wantalert WHERE needMod AND sentMod;" |
| 2189 | ); |
| 2190 | } |
| 2191 | pEvents = email_compute_event_text(&nEvent, (flags & SENDALERT_DIGEST)!=0); |
| 2192 | if( nEvent==0 ) goto send_alerts_done; |
| 2193 | blob_init(&hdr, 0, 0); |
| 2194 | blob_init(&body, 0, 0); |
| 2195 | db_prepare(&q, |
| 2196 | "SELECT" |
| 2197 | " hex(subscriberCode)," /* 0 */ |
| 2198 | " semail," /* 1 */ |
| 2199 | " ssub," /* 2 */ |
| 2200 | " fullcap((SELECT cap FROM user WHERE login=suname))" /* 3 */ |
| 2201 | " FROM subscriber" |
| 2202 | " WHERE sverified AND NOT sdonotcall" |
| 2203 | " AND sdigest IS %s", |
| 2204 | zDigest/*safe-for-%s*/ |
| 2205 | ); |
| 2206 | while( db_step(&q)==SQLITE_ROW ){ |
| 2207 | const char *zCode = db_column_text(&q, 0); |
| 2208 | const char *zSub = db_column_text(&q, 2); |
| 2209 | const char *zEmail = db_column_text(&q, 1); |
| 2210 | const char *zCap = db_column_text(&q, 3); |
| 2211 | int nHit = 0; |
| 2212 | for(p=pEvents; p; p=p->pNext){ |
| 2213 | if( strchr(zSub,p->type)==0 ) continue; |
| 2214 | if( p->needMod ){ |
| 2215 | /* For events that require moderator approval, only send an alert |
| 2216 | ** if the recipient is a moderator for that type of event */ |
| 2217 | char xType = '*'; |
| 2218 | switch( p->type ){ |
| 2219 | case 'f': xType = '5'; break; |
| 2220 | case 't': xType = 'q'; break; |
| 2221 | case 'w': xType = 'l'; break; |
| 2222 | } |
| 2223 | if( strchr(zCap,xType)==0 ) continue; |
| 2224 | }else if( strchr(zCap,'s')!=0 || strchr(zCap,'a')!=0 ){ |
| 2225 | /* Setup and admin users can get any notification that does not |
| 2226 | ** require moderation */ |
| 2227 | }else{ |
| 2228 | /* Other users only see the alert if they have sufficient |
| 2229 | ** privilege to view the event itself */ |
| 2230 | char xType = '*'; |
| 2231 | switch( p->type ){ |
| 2232 | case 'c': xType = 'o'; break; |
| 2233 | case 'f': xType = '2'; break; |
| 2234 | case 't': xType = 'r'; break; |
| 2235 | case 'w': xType = 'j'; break; |
| 2236 | } |
| 2237 | if( strchr(zCap,xType)==0 ) continue; |
| 2238 | } |
| 2239 | if( blob_size(&p->hdr)>0 ){ |
| 2240 | /* This alert should be sent as a separate email */ |
| 2241 | Blob fhdr, fbody; |
| 2242 | blob_init(&fhdr, 0, 0); |
| 2243 | blob_appendf(&fhdr, "To: <%s>\r\n", zEmail); |
| 2244 | blob_append(&fhdr, blob_buffer(&p->hdr), blob_size(&p->hdr)); |
| 2245 | blob_init(&fbody, blob_buffer(&p->txt), blob_size(&p->txt)); |
| 2246 | blob_appendf(&fbody, "\n-- \nSubscription info: %s/alerts/%s\n", |
| 2247 | zUrl, zCode); |
| 2248 | email_send(pSender,&fhdr,&fbody); |
| 2249 | blob_reset(&fhdr); |
| 2250 | blob_reset(&fbody); |
| 2251 | }else{ |
| 2252 | /* Events other than forum posts are gathered together into |
| 2253 | ** a single email message */ |
| 2254 | if( nHit==0 ){ |
| 2255 | blob_appendf(&hdr,"To: <%s>\r\n", zEmail); |
| 2256 | blob_appendf(&hdr,"Subject: %s activity alert\r\n", zRepoName); |
| 2257 | blob_appendf(&body, |
| 2258 | "This is an automated email sent by the Fossil repository " |
| 2259 | "at %s to report changes.\n", |
| 2260 | zUrl |
| 2261 | ); |
| 2262 | } |
| 2263 | nHit++; |
| 2264 | blob_append(&body, "\n", 1); |
| 2265 | blob_append(&body, blob_buffer(&p->txt), blob_size(&p->txt)); |
| 2266 | } |
| 2267 | } |
| 2268 | if( nHit==0 ) continue; |
| 2269 | blob_appendf(&body,"\n-- \nSubscription info: %s/alerts/%s\n", |
| 2270 | zUrl, zCode); |
| 2271 | email_send(pSender,&hdr,&body); |
| @@ -2070,15 +2276,24 @@ | |
| 2276 | blob_reset(&body); |
| 2277 | db_finalize(&q); |
| 2278 | email_free_eventlist(pEvents); |
| 2279 | if( (flags & SENDALERT_PRESERVE)==0 ){ |
| 2280 | if( flags & SENDALERT_DIGEST ){ |
| 2281 | db_multi_exec( |
| 2282 | "UPDATE pending_alert SET sentDigest=true" |
| 2283 | " WHERE eventid IN (SELECT eventid FROM wantalert);" |
| 2284 | "DELETE FROM pending_alert WHERE sentDigest AND sentSep;" |
| 2285 | ); |
| 2286 | }else{ |
| 2287 | db_multi_exec( |
| 2288 | "UPDATE pending_alert SET sentSep=true" |
| 2289 | " WHERE eventid IN (SELECT eventid FROM wantalert WHERE NOT needMod);" |
| 2290 | "UPDATE pending_alert SET sentMod=true" |
| 2291 | " WHERE eventid IN (SELECT eventid FROM wantalert WHERE needMod);" |
| 2292 | "DELETE FROM pending_alert WHERE sentDigest AND sentSep;" |
| 2293 | ); |
| 2294 | } |
| 2295 | } |
| 2296 | send_alerts_done: |
| 2297 | email_sender_free(pSender); |
| 2298 | if( g.fSqlTrace ) fossil_trace("-- END email_send_alerts(%u)\n", flags); |
| 2299 | db_end_transaction(0); |
| 2300 |
+921
-326
| --- src/forum.c | ||
| +++ src/forum.c | ||
| @@ -19,337 +19,932 @@ | ||
| 19 | 19 | */ |
| 20 | 20 | #include "config.h" |
| 21 | 21 | #include <assert.h> |
| 22 | 22 | #include "forum.h" |
| 23 | 23 | |
| 24 | -/* | |
| 25 | -** The schema for the tables that manage the forum, if forum is | |
| 26 | -** enabled. | |
| 27 | -*/ | |
| 28 | -static const char zForumInit[] = | |
| 29 | -@ CREATE TABLE repository.forumpost( | |
| 30 | -@ mpostid INTEGER PRIMARY KEY, -- unique id for each post (local) | |
| 31 | -@ mposthash TEXT, -- uuid for this post | |
| 32 | -@ mthreadid INTEGER, -- thread to which this post belongs | |
| 33 | -@ uname TEXT, -- name of user | |
| 34 | -@ mtime REAL, -- julian day number | |
| 35 | -@ mstatus TEXT, -- status. NULL=ok. 'mod'=pending moderation | |
| 36 | -@ mimetype TEXT, -- Mimetype for mbody | |
| 37 | -@ ipaddr TEXT, -- IP address of post origin | |
| 38 | -@ inreplyto INT, -- Parent posting | |
| 39 | -@ mbody TEXT -- Content of the post | |
| 40 | -@ ); | |
| 41 | -@ CREATE INDEX repository.forumpost_x1 ON | |
| 42 | -@ forumpost(inreplyto,mtime); | |
| 43 | -@ CREATE TABLE repository.forumthread( | |
| 44 | -@ mthreadid INTEGER PRIMARY KEY, | |
| 45 | -@ mthreadhash TEXT, -- uuid for this thread | |
| 46 | -@ mtitle TEXT, -- Title or subject line | |
| 47 | -@ mtime REAL, -- Most recent update | |
| 48 | -@ npost INT -- Number of posts on this thread | |
| 49 | -@ ); | |
| 50 | -; | |
| 51 | - | |
| 52 | -/* | |
| 53 | -** Create the forum tables in the schema if they do not already | |
| 54 | -** exist. | |
| 55 | -*/ | |
| 56 | -static void forum_verify_schema(void){ | |
| 57 | - if( !db_table_exists("repository","forumpost") ){ | |
| 58 | - db_multi_exec(zForumInit /*works-like:""*/); | |
| 59 | - } | |
| 60 | -} | |
| 61 | - | |
| 62 | -/* | |
| 63 | -** WEBPAGE: forum | |
| 64 | -** URL: /forum | |
| 65 | -** Query parameters: | |
| 66 | -** | |
| 67 | -** item=N Show post N and its replies | |
| 68 | -** | |
| 69 | -*/ | |
| 70 | -void forum_page(void){ | |
| 71 | - int itemId; | |
| 72 | - Stmt q; | |
| 73 | - int i; | |
| 74 | - | |
| 75 | - login_check_credentials(); | |
| 76 | - if( !g.perm.RdForum ){ login_needed(g.anon.RdForum); return; } | |
| 77 | - forum_verify_schema(); | |
| 78 | - style_header("Forum"); | |
| 79 | - itemId = atoi(PD("item","0")); | |
| 80 | - if( itemId>0 ){ | |
| 81 | - int iUp; | |
| 82 | - double rNow; | |
| 83 | - style_submenu_element("Topics", "%R/forum"); | |
| 84 | - iUp = db_int(0, "SELECT inreplyto FROM forumpost WHERE mpostid=%d", itemId); | |
| 85 | - if( iUp ){ | |
| 86 | - style_submenu_element("Parent", "%R/forum?item=%d", iUp); | |
| 87 | - } | |
| 88 | - rNow = db_double(0.0, "SELECT julianday('now')"); | |
| 89 | - /* Show the post given by itemId and all its descendents */ | |
| 90 | - db_prepare(&q, | |
| 91 | - "WITH RECURSIVE" | |
| 92 | - " post(id,uname,mstat,mime,ipaddr,parent,mbody,depth,mtime) AS (" | |
| 93 | - " SELECT mpostid, uname, mstatus, mimetype, ipaddr, inreplyto, mbody," | |
| 94 | - " 0, mtime FROM forumpost WHERE mpostid=%d" | |
| 95 | - " UNION" | |
| 96 | - " SELECT f.mpostid, f.uname, f.mstatus, f.mimetype, f.ipaddr," | |
| 97 | - " f.inreplyto, f.mbody, p.depth+1 AS xdepth, f.mtime AS xtime" | |
| 98 | - " FROM forumpost AS f, post AS p" | |
| 99 | - " WHERE f.inreplyto=p.id" | |
| 100 | - " ORDER BY xdepth DESC, xtime ASC" | |
| 101 | - ") SELECT * FROM post;", | |
| 102 | - itemId | |
| 103 | - ); | |
| 104 | - while( db_step(&q)==SQLITE_ROW ){ | |
| 105 | - int id = db_column_int(&q, 0); | |
| 106 | - const char *zUser = db_column_text(&q, 1); | |
| 107 | - const char *zMime = db_column_text(&q, 3); | |
| 108 | - int iDepth = db_column_int(&q, 7); | |
| 109 | - double rMTime = db_column_double(&q, 8); | |
| 110 | - char *zAge = db_timespan_name(rNow - rMTime); | |
| 111 | - Blob body; | |
| 112 | - @ <!-- Forum post %d(id) --> | |
| 113 | - @ <table class="forum_post"> | |
| 114 | - @ <tr> | |
| 115 | - @ <td class="forum_margin" width="%d(iDepth*40)" rowspan="2"> | |
| 116 | - @ <td><span class="forum_author">%h(zUser)</span> | |
| 117 | - @ <span class="forum_age">%s(zAge) ago</span> | |
| 118 | - sqlite3_free(zAge); | |
| 119 | - if( g.perm.WrForum ){ | |
| 120 | - @ <span class="forum_buttons"> | |
| 121 | - if( g.perm.AdminForum || fossil_strcmp(g.zLogin, zUser)==0 ){ | |
| 122 | - @ <a href='%R/forumedit?item=%d(id)'>Edit</a> | |
| 123 | - } | |
| 124 | - @ <a href='%R/forumedit?replyto=%d(id)'>Reply</a> | |
| 125 | - @ </span> | |
| 126 | - } | |
| 127 | - @ </tr> | |
| 128 | - @ <tr><td><div class="forum_body"> | |
| 129 | - blob_init(&body, db_column_text(&q,6), db_column_bytes(&q,6)); | |
| 130 | - wiki_render_by_mimetype(&body, zMime); | |
| 131 | - blob_reset(&body); | |
| 132 | - @ </div></td></tr> | |
| 133 | - @ </table> | |
| 134 | - } | |
| 135 | - }else{ | |
| 136 | - /* If we reach this point, that means the users wants a list of | |
| 137 | - ** recent threads. | |
| 138 | - */ | |
| 139 | - i = 0; | |
| 140 | - db_prepare(&q, | |
| 141 | - "SELECT a.mtitle, a.npost, b.mpostid" | |
| 142 | - " FROM forumthread AS a, forumpost AS b " | |
| 143 | - " WHERE a.mthreadid=b.mthreadid" | |
| 144 | - " AND b.inreplyto IS NULL" | |
| 145 | - " ORDER BY a.mtime DESC LIMIT 40" | |
| 146 | - ); | |
| 147 | - if( g.perm.WrForum ){ | |
| 148 | - style_submenu_element("New", "%R/forumedit"); | |
| 149 | - } | |
| 150 | - @ <h1>Recent Forum Threads</h1> | |
| 151 | - while( db_step(&q)==SQLITE_ROW ){ | |
| 152 | - int n = db_column_int(&q,1); | |
| 153 | - int itemid = db_column_int(&q,2); | |
| 154 | - const char *zTitle = db_column_text(&q,0); | |
| 155 | - if( (i++)==0 ){ | |
| 156 | - @ <ol> | |
| 157 | - } | |
| 158 | - @ <li><span class="forum_title"> | |
| 159 | - @ %z(href("%R/forum?item=%d",itemid))%h(zTitle)</a></span> | |
| 160 | - @ <span class="forum_npost">%d(n) post%s(n==1?"":"s")</span></li> | |
| 161 | - } | |
| 162 | - if( i ){ | |
| 163 | - @ </ol> | |
| 164 | - } | |
| 165 | - } | |
| 24 | +#if INTERFACE | |
| 25 | +/* | |
| 26 | +** Each instance of the following object represents a single message - | |
| 27 | +** either the initial post, an edit to a post, a reply, or an edit to | |
| 28 | +** a reply. | |
| 29 | +*/ | |
| 30 | +struct ForumEntry { | |
| 31 | + int fpid; /* rid for this entry */ | |
| 32 | + int fprev; /* zero if initial entry. non-zero if an edit */ | |
| 33 | + int firt; /* This entry replies to firt */ | |
| 34 | + int mfirt; /* Root in-reply-to */ | |
| 35 | + char *zUuid; /* Artifact hash */ | |
| 36 | + ForumEntry *pLeaf; /* Most recent edit for this entry */ | |
| 37 | + ForumEntry *pEdit; /* This entry is an edit of pEditee */ | |
| 38 | + ForumEntry *pNext; /* Next in chronological order */ | |
| 39 | + ForumEntry *pPrev; /* Previous in chronological order */ | |
| 40 | + ForumEntry *pDisplay; /* Next in display order */ | |
| 41 | + int nIndent; /* Number of levels of indentation for this entry */ | |
| 42 | +}; | |
| 43 | + | |
| 44 | +/* | |
| 45 | +** A single instance of the following tracks all entries for a thread. | |
| 46 | +*/ | |
| 47 | +struct ForumThread { | |
| 48 | + ForumEntry *pFirst; /* First entry in chronological order */ | |
| 49 | + ForumEntry *pLast; /* Last entry in chronological order */ | |
| 50 | + ForumEntry *pDisplay; /* Entries in display order */ | |
| 51 | + ForumEntry *pTail; /* Last on the display list */ | |
| 52 | +}; | |
| 53 | +#endif /* INTERFACE */ | |
| 54 | + | |
| 55 | +/* | |
| 56 | +** Delete a complete ForumThread and all its entries. | |
| 57 | +*/ | |
| 58 | +static void forumthread_delete(ForumThread *pThread){ | |
| 59 | + ForumEntry *pEntry, *pNext; | |
| 60 | + for(pEntry=pThread->pFirst; pEntry; pEntry = pNext){ | |
| 61 | + pNext = pEntry->pNext; | |
| 62 | + fossil_free(pEntry->zUuid); | |
| 63 | + fossil_free(pEntry); | |
| 64 | + } | |
| 65 | + fossil_free(pThread); | |
| 66 | +} | |
| 67 | + | |
| 68 | +#if 0 /* not used */ | |
| 69 | +/* | |
| 70 | +** Search a ForumEntry list forwards looking for the entry with fpid | |
| 71 | +*/ | |
| 72 | +static ForumEntry *forumentry_forward(ForumEntry *p, int fpid){ | |
| 73 | + while( p && p->fpid!=fpid ) p = p->pNext; | |
| 74 | + return p; | |
| 75 | +} | |
| 76 | +#endif | |
| 77 | + | |
| 78 | +/* | |
| 79 | +** Search backwards for a ForumEntry | |
| 80 | +*/ | |
| 81 | +static ForumEntry *forumentry_backward(ForumEntry *p, int fpid){ | |
| 82 | + while( p && p->fpid!=fpid ) p = p->pPrev; | |
| 83 | + return p; | |
| 84 | +} | |
| 85 | + | |
| 86 | +/* | |
| 87 | +** Add an entry to the display list | |
| 88 | +*/ | |
| 89 | +static void forumentry_add_to_display(ForumThread *pThread, ForumEntry *p){ | |
| 90 | + if( pThread->pDisplay==0 ){ | |
| 91 | + pThread->pDisplay = p; | |
| 92 | + }else{ | |
| 93 | + pThread->pTail->pDisplay = p; | |
| 94 | + } | |
| 95 | + pThread->pTail = p; | |
| 96 | +} | |
| 97 | + | |
| 98 | +/* | |
| 99 | +** Extend the display list for pThread by adding all entries that | |
| 100 | +** reference fpid. The first such entry will be no earlier then | |
| 101 | +** entry "p". | |
| 102 | +*/ | |
| 103 | +static void forumthread_display_order( | |
| 104 | + ForumThread *pThread, | |
| 105 | + ForumEntry *p, | |
| 106 | + int fpid, | |
| 107 | + int nIndent | |
| 108 | +){ | |
| 109 | + while( p ){ | |
| 110 | + if( p->fprev==0 && p->mfirt==fpid ){ | |
| 111 | + p->nIndent = nIndent; | |
| 112 | + forumentry_add_to_display(pThread, p); | |
| 113 | + forumthread_display_order(pThread, p->pNext, p->fpid, nIndent+1); | |
| 114 | + } | |
| 115 | + p = p->pNext; | |
| 116 | + } | |
| 117 | +} | |
| 118 | + | |
| 119 | +/* | |
| 120 | +** Construct a ForumThread object given the root record id. | |
| 121 | +*/ | |
| 122 | +static ForumThread *forumthread_create(int froot, int computeHierarchy){ | |
| 123 | + ForumThread *pThread; | |
| 124 | + ForumEntry *pEntry; | |
| 125 | + Stmt q; | |
| 126 | + pThread = fossil_malloc( sizeof(*pThread) ); | |
| 127 | + memset(pThread, 0, sizeof(*pThread)); | |
| 128 | + db_prepare(&q, | |
| 129 | + "SELECT fpid, firt, fprev, (SELECT uuid FROM blob WHERE rid=fpid)" | |
| 130 | + " FROM forumpost" | |
| 131 | + " WHERE froot=%d ORDER BY fmtime", | |
| 132 | + froot | |
| 133 | + ); | |
| 134 | + while( db_step(&q)==SQLITE_ROW ){ | |
| 135 | + pEntry = fossil_malloc( sizeof(*pEntry) ); | |
| 136 | + memset(pEntry, 0, sizeof(*pEntry)); | |
| 137 | + pEntry->fpid = db_column_int(&q, 0); | |
| 138 | + pEntry->firt = db_column_int(&q, 1); | |
| 139 | + pEntry->fprev = db_column_int(&q, 2); | |
| 140 | + pEntry->zUuid = fossil_strdup(db_column_text(&q,3)); | |
| 141 | + pEntry->mfirt = pEntry->firt; | |
| 142 | + pEntry->pPrev = pThread->pLast; | |
| 143 | + pEntry->pNext = 0; | |
| 144 | + if( pThread->pLast==0 ){ | |
| 145 | + pThread->pFirst = pEntry; | |
| 146 | + }else{ | |
| 147 | + pThread->pLast->pNext = pEntry; | |
| 148 | + } | |
| 149 | + pThread->pLast = pEntry; | |
| 150 | + } | |
| 151 | + db_finalize(&q); | |
| 152 | + | |
| 153 | + /* Establish which entries are the latest edit. After this loop | |
| 154 | + ** completes, entries that have non-NULL pLeaf should not be | |
| 155 | + ** displayed. | |
| 156 | + */ | |
| 157 | + for(pEntry=pThread->pFirst; pEntry; pEntry=pEntry->pNext){ | |
| 158 | + if( pEntry->fprev ){ | |
| 159 | + ForumEntry *pBase, *p; | |
| 160 | + p = forumentry_backward(pEntry->pPrev, pEntry->fprev); | |
| 161 | + pEntry->pEdit = p; | |
| 162 | + while( p ){ | |
| 163 | + pBase = p; | |
| 164 | + p->pLeaf = pEntry; | |
| 165 | + p = pBase->pEdit; | |
| 166 | + } | |
| 167 | + for(p=pEntry->pNext; p; p=p->pNext){ | |
| 168 | + if( p->mfirt==pEntry->fpid ) p->mfirt = pBase->fpid; | |
| 169 | + } | |
| 170 | + } | |
| 171 | + } | |
| 172 | + | |
| 173 | + if( computeHierarchy ){ | |
| 174 | + /* Compute the hierarchical display order */ | |
| 175 | + pEntry = pThread->pFirst; | |
| 176 | + pEntry->nIndent = 1; | |
| 177 | + forumentry_add_to_display(pThread, pEntry); | |
| 178 | + forumthread_display_order(pThread, pEntry, pEntry->fpid, 2); | |
| 179 | + } | |
| 180 | + | |
| 181 | + /* Return the result */ | |
| 182 | + return pThread; | |
| 183 | +} | |
| 184 | + | |
| 185 | +/* | |
| 186 | +** COMMAND: test-forumthread | |
| 187 | +** | |
| 188 | +** Usage: %fossil test-forumthread THREADID | |
| 189 | +** | |
| 190 | +** Display a summary of all messages on a thread. | |
| 191 | +*/ | |
| 192 | +void forumthread_cmd(void){ | |
| 193 | + int fpid; | |
| 194 | + int froot; | |
| 195 | + const char *zName; | |
| 196 | + ForumThread *pThread; | |
| 197 | + ForumEntry *p; | |
| 198 | + | |
| 199 | + db_find_and_open_repository(0,0); | |
| 200 | + verify_all_options(); | |
| 201 | + if( g.argc!=3 ) usage("THREADID"); | |
| 202 | + zName = g.argv[2]; | |
| 203 | + fpid = symbolic_name_to_rid(zName, "f"); | |
| 204 | + if( fpid<=0 ){ | |
| 205 | + fossil_fatal("Unknown or ambiguous forum id: \"%s\"", zName); | |
| 206 | + } | |
| 207 | + froot = db_int(0, "SELECT froot FROM forumpost WHERE fpid=%d", fpid); | |
| 208 | + if( froot==0 ){ | |
| 209 | + fossil_fatal("Not a forum post: \"%s\"", zName); | |
| 210 | + } | |
| 211 | + fossil_print("fpid = %d\n", fpid); | |
| 212 | + fossil_print("froot = %d\n", froot); | |
| 213 | + pThread = forumthread_create(froot, 1); | |
| 214 | + fossil_print("Chronological:\n"); | |
| 215 | + /* 123456789 123456789 123456789 123456789 123456789 */ | |
| 216 | + fossil_print(" fpid firt fprev mfirt pLeaf\n"); | |
| 217 | + for(p=pThread->pFirst; p; p=p->pNext){ | |
| 218 | + fossil_print("%9d %9d %9d %9d %9d\n", | |
| 219 | + p->fpid, p->firt, p->fprev, p->mfirt, p->pLeaf ? p->pLeaf->fpid : 0); | |
| 220 | + } | |
| 221 | + fossil_print("\nDisplay\n"); | |
| 222 | + for(p=pThread->pDisplay; p; p=p->pDisplay){ | |
| 223 | + fossil_print("%*s", (p->nIndent-1)*3, ""); | |
| 224 | + if( p->pLeaf ){ | |
| 225 | + fossil_print("%d->%d\n", p->fpid, p->pLeaf->fpid); | |
| 226 | + }else{ | |
| 227 | + fossil_print("%d\n", p->fpid); | |
| 228 | + } | |
| 229 | + } | |
| 230 | + forumthread_delete(pThread); | |
| 231 | +} | |
| 232 | + | |
| 233 | +/* | |
| 234 | +** Render a forum post for display | |
| 235 | +*/ | |
| 236 | +void forum_render( | |
| 237 | + const char *zTitle, /* The title. Might be NULL for no title */ | |
| 238 | + const char *zMimetype, /* Mimetype of the message */ | |
| 239 | + const char *zContent, /* Content of the message */ | |
| 240 | + const char *zClass /* Put in a <div> if not NULL */ | |
| 241 | +){ | |
| 242 | + if( zClass ){ | |
| 243 | + @ <div class='%s(zClass)'> | |
| 244 | + } | |
| 245 | + if( zTitle ){ | |
| 246 | + if( zTitle[0] ){ | |
| 247 | + @ <h1>%h(zTitle)</h1> | |
| 248 | + }else{ | |
| 249 | + @ <h1><i>Deleted</i></h1> | |
| 250 | + } | |
| 251 | + } | |
| 252 | + if( zContent && zContent[0] ){ | |
| 253 | + Blob x; | |
| 254 | + blob_init(&x, 0, 0); | |
| 255 | + blob_append(&x, zContent, -1); | |
| 256 | + wiki_render_by_mimetype(&x, zMimetype); | |
| 257 | + blob_reset(&x); | |
| 258 | + }else{ | |
| 259 | + @ <i>Deleted</i> | |
| 260 | + } | |
| 261 | + if( zClass ){ | |
| 262 | + @ </div> | |
| 263 | + } | |
| 264 | +} | |
| 265 | + | |
| 266 | +/* | |
| 267 | +** Display all posts in a forum thread in chronological order | |
| 268 | +*/ | |
| 269 | +static void forum_display_chronological(int froot, int target){ | |
| 270 | + ForumThread *pThread = forumthread_create(froot, 0); | |
| 271 | + ForumEntry *p; | |
| 272 | + for(p=pThread->pFirst; p; p=p->pNext){ | |
| 273 | + char *zDate; | |
| 274 | + Manifest *pPost; | |
| 275 | + | |
| 276 | + pPost = manifest_get(p->fpid, CFTYPE_FORUM, 0); | |
| 277 | + if( pPost==0 ) continue; | |
| 278 | + if( p->fpid==target ){ | |
| 279 | + @ <div id="forum%d(p->fpid)" class="forumTime forumSel"> | |
| 280 | + }else if( p->pLeaf!=0 ){ | |
| 281 | + @ <div id="forum%d(p->fpid)" class="forumTime forumObs"> | |
| 282 | + }else{ | |
| 283 | + @ <div id="forum%d(p->fpid)" class="forumTime"> | |
| 284 | + } | |
| 285 | + if( pPost->zThreadTitle ){ | |
| 286 | + @ <h1>%h(pPost->zThreadTitle)</h1> | |
| 287 | + } | |
| 288 | + zDate = db_text(0, "SELECT datetime(%.17g)", pPost->rDate); | |
| 289 | + @ <p>By %h(pPost->zUser) on %h(zDate) (%d(p->fpid)) | |
| 290 | + fossil_free(zDate); | |
| 291 | + if( p->pEdit ){ | |
| 292 | + @ edit of %z(href("%R/forumpost/%S?t",p->pEdit->zUuid))%d(p->fprev)</a> | |
| 293 | + } | |
| 294 | + if( p->firt ){ | |
| 295 | + ForumEntry *pIrt = p->pPrev; | |
| 296 | + while( pIrt && pIrt->fpid!=p->firt ) pIrt = pIrt->pPrev; | |
| 297 | + if( pIrt ){ | |
| 298 | + @ reply to %z(href("%R/forumpost/%S?t",pIrt->zUuid))%d(p->firt)</a> | |
| 299 | + } | |
| 300 | + } | |
| 301 | + if( p->pLeaf ){ | |
| 302 | + @ updated by %z(href("%R/forumpost/%S?t",p->pLeaf->zUuid))\ | |
| 303 | + @ %d(p->pLeaf->fpid)</a> | |
| 304 | + } | |
| 305 | + if( g.perm.Debug ){ | |
| 306 | + @ <span class="debug">\ | |
| 307 | + @ <a href="%R/artifact/%h(p->zUuid)">artifact</a></span> | |
| 308 | + } | |
| 309 | + if( p->fpid!=target ){ | |
| 310 | + @ %z(href("%R/forumpost/%S?t",p->zUuid))[link]</a> | |
| 311 | + } | |
| 312 | + forum_render(0, pPost->zMimetype, pPost->zWiki, 0); | |
| 313 | + if( g.perm.WrForum && p->pLeaf==0 ){ | |
| 314 | + int sameUser = login_is_individual() | |
| 315 | + && fossil_strcmp(pPost->zUser, g.zLogin)==0; | |
| 316 | + int isPrivate = content_is_private(p->fpid); | |
| 317 | + @ <p><form action="%R/forumedit" method="POST"> | |
| 318 | + @ <input type="hidden" name="fpid" value="%s(p->zUuid)"> | |
| 319 | + if( !isPrivate ){ | |
| 320 | + /* Reply and Edit are only available if the post has already | |
| 321 | + ** been approved */ | |
| 322 | + @ <input type="submit" name="reply" value="Reply"> | |
| 323 | + if( g.perm.Admin || sameUser ){ | |
| 324 | + @ <input type="submit" name="edit" value="Edit"> | |
| 325 | + @ <input type="submit" name="nullout" value="Delete"> | |
| 326 | + } | |
| 327 | + }else if( g.perm.ModForum ){ | |
| 328 | + /* Provide moderators with moderation buttons for posts that | |
| 329 | + ** are pending moderation */ | |
| 330 | + @ <input type="submit" name="approve" value="Approve"> | |
| 331 | + @ <input type="submit" name="reject" value="Reject"> | |
| 332 | + }else if( sameUser ){ | |
| 333 | + /* A post that is pending moderation can be deleted by the | |
| 334 | + ** person who originally submitted the post */ | |
| 335 | + @ <input type="submit" name="reject" value="Delete"> | |
| 336 | + } | |
| 337 | + @ </form></p> | |
| 338 | + } | |
| 339 | + manifest_destroy(pPost); | |
| 340 | + @ </div> | |
| 341 | + } | |
| 342 | + forumthread_delete(pThread); | |
| 343 | +} | |
| 344 | + | |
| 345 | +/* | |
| 346 | +** Display all messages in a forumthread with indentation. | |
| 347 | +*/ | |
| 348 | +static int forum_display_hierarchical(int froot, int target){ | |
| 349 | + ForumThread *pThread; | |
| 350 | + ForumEntry *p; | |
| 351 | + Manifest *pPost, *pOPost; | |
| 352 | + int fpid; | |
| 353 | + const char *zUuid; | |
| 354 | + char *zDate; | |
| 355 | + const char *zSel; | |
| 356 | + | |
| 357 | + pThread = forumthread_create(froot, 1); | |
| 358 | + for(p=pThread->pFirst; p; p=p->pNext){ | |
| 359 | + if( p->fpid==target ){ | |
| 360 | + while( p->pEdit ) p = p->pEdit; | |
| 361 | + target = p->fpid; | |
| 362 | + break; | |
| 363 | + } | |
| 364 | + } | |
| 365 | + for(p=pThread->pDisplay; p; p=p->pDisplay){ | |
| 366 | + pOPost = manifest_get(p->fpid, CFTYPE_FORUM, 0); | |
| 367 | + if( p->pLeaf ){ | |
| 368 | + fpid = p->pLeaf->fpid; | |
| 369 | + zUuid = p->pLeaf->zUuid; | |
| 370 | + pPost = manifest_get(fpid, CFTYPE_FORUM, 0); | |
| 371 | + }else{ | |
| 372 | + fpid = p->fpid; | |
| 373 | + zUuid = p->zUuid; | |
| 374 | + pPost = pOPost; | |
| 375 | + } | |
| 376 | + zSel = p->fpid==target ? " forumSel" : ""; | |
| 377 | + if( p->nIndent==1 ){ | |
| 378 | + @ <div id='forum%d(fpid)' class='forumHierRoot%s(zSel)'> | |
| 379 | + }else{ | |
| 380 | + @ <div id='forum%d(fpid)' class='forumHier%s(zSel)' \ | |
| 381 | + @ style='margin-left: %d((p->nIndent-1)*3)ex;'> | |
| 382 | + } | |
| 383 | + pPost = manifest_get(fpid, CFTYPE_FORUM, 0); | |
| 384 | + if( pPost==0 ) continue; | |
| 385 | + if( pPost->zThreadTitle ){ | |
| 386 | + @ <h1>%h(pPost->zThreadTitle)</h1> | |
| 387 | + } | |
| 388 | + zDate = db_text(0, "SELECT datetime(%.17g)", pOPost->rDate); | |
| 389 | + @ <p>By %h(pOPost->zUser) on %h(zDate) | |
| 390 | + fossil_free(zDate); | |
| 391 | + if( g.perm.Debug ){ | |
| 392 | + @ <span class="debug">\ | |
| 393 | + @ <a href="%R/artifact/%h(p->zUuid)">(%d(p->fpid))</a></span> | |
| 394 | + } | |
| 395 | + if( p->pLeaf ){ | |
| 396 | + zDate = db_text(0, "SELECT datetime(%.17g)", pPost->rDate); | |
| 397 | + if( fossil_strcmp(pOPost->zUser,pPost->zUser)==0 ){ | |
| 398 | + @ and edited on %h(zDate) | |
| 399 | + }else{ | |
| 400 | + @ as edited by %h(pPost->zUser) on %h(zDate) | |
| 401 | + } | |
| 402 | + fossil_free(zDate); | |
| 403 | + if( g.perm.Debug ){ | |
| 404 | + @ <span class="debug">\ | |
| 405 | + @ <a href="%R/artifact/%h(p->pLeaf->zUuid)">(%d(fpid))</a></span> | |
| 406 | + } | |
| 407 | + manifest_destroy(pOPost); | |
| 408 | + } | |
| 409 | + if( fpid!=target ){ | |
| 410 | + @ %z(href("%R/forumpost/%S",zUuid))[link]</a> | |
| 411 | + } | |
| 412 | + forum_render(0, pPost->zMimetype, pPost->zWiki, 0); | |
| 413 | + if( g.perm.WrForum ){ | |
| 414 | + int sameUser = login_is_individual() | |
| 415 | + && fossil_strcmp(pPost->zUser, g.zLogin)==0; | |
| 416 | + int isPrivate = content_is_private(fpid); | |
| 417 | + @ <p><form action="%R/forumedit" method="POST"> | |
| 418 | + @ <input type="hidden" name="fpid" value="%s(zUuid)"> | |
| 419 | + if( !isPrivate ){ | |
| 420 | + /* Reply and Edit are only available if the post has already | |
| 421 | + ** been approved */ | |
| 422 | + @ <input type="submit" name="reply" value="Reply"> | |
| 423 | + if( g.perm.Admin || sameUser ){ | |
| 424 | + @ <input type="submit" name="edit" value="Edit"> | |
| 425 | + @ <input type="submit" name="nullout" value="Delete"> | |
| 426 | + } | |
| 427 | + }else if( g.perm.ModForum ){ | |
| 428 | + /* Provide moderators with moderation buttons for posts that | |
| 429 | + ** are pending moderation */ | |
| 430 | + @ <input type="submit" name="approve" value="Approve"> | |
| 431 | + @ <input type="submit" name="reject" value="Reject"> | |
| 432 | + }else if( sameUser ){ | |
| 433 | + /* A post that is pending moderation can be deleted by the | |
| 434 | + ** person who originally submitted the post */ | |
| 435 | + @ <input type="submit" name="reject" value="Delete"> | |
| 436 | + } | |
| 437 | + @ </form></p> | |
| 438 | + } | |
| 439 | + manifest_destroy(pPost); | |
| 440 | + @ </div> | |
| 441 | + } | |
| 442 | + forumthread_delete(pThread); | |
| 443 | + return target; | |
| 444 | +} | |
| 445 | + | |
| 446 | +/* | |
| 447 | +** WEBPAGE: forumpost | |
| 448 | +** | |
| 449 | +** Show a single forum posting. The posting is shown in context with | |
| 450 | +** it's entire thread. The selected posting is enclosed within | |
| 451 | +** <div class='forumSel'>...</div>. Javascript is used to move the | |
| 452 | +** selected posting into view after the page loads. | |
| 453 | +** | |
| 454 | +** Query parameters: | |
| 455 | +** | |
| 456 | +** name=X REQUIRED. The hash of the post to display | |
| 457 | +** t Show a chronologic listing instead of hierarchical | |
| 458 | +*/ | |
| 459 | +void forumpost_page(void){ | |
| 460 | + forumthread_page(); | |
| 461 | +} | |
| 462 | + | |
| 463 | +/* | |
| 464 | +** WEBPAGE: forumthread | |
| 465 | +** | |
| 466 | +** Show all forum messages associated with a particular message thread. | |
| 467 | +** The result is basically the same as /forumpost except that none of | |
| 468 | +** the postings in the thread are selected. | |
| 469 | +** | |
| 470 | +** Query parameters: | |
| 471 | +** | |
| 472 | +** name=X REQUIRED. The hash of any post of the thread. | |
| 473 | +** t Show a chronologic listing instead of hierarchical | |
| 474 | +*/ | |
| 475 | +void forumthread_page(void){ | |
| 476 | + int fpid; | |
| 477 | + int froot; | |
| 478 | + const char *zName = P("name"); | |
| 479 | + login_check_credentials(); | |
| 480 | + if( !g.perm.RdForum ){ | |
| 481 | + login_needed(g.anon.RdForum); | |
| 482 | + return; | |
| 483 | + } | |
| 484 | + if( zName==0 ){ | |
| 485 | + webpage_error("Missing \"name=\" query parameter"); | |
| 486 | + } | |
| 487 | + fpid = symbolic_name_to_rid(zName, "f"); | |
| 488 | + if( fpid<=0 ){ | |
| 489 | + webpage_error("Unknown or ambiguous forum id: \"%s\"", zName); | |
| 490 | + } | |
| 491 | + style_header("Forum"); | |
| 492 | + froot = db_int(0, "SELECT froot FROM forumpost WHERE fpid=%d", fpid); | |
| 493 | + if( froot==0 ){ | |
| 494 | + webpage_error("Not a forum post: \"%s\"", zName); | |
| 495 | + } | |
| 496 | + if( fossil_strcmp(g.zPath,"forumthread")==0 ) fpid = 0; | |
| 497 | + if( P("t") ){ | |
| 498 | + if( g.perm.Debug ){ | |
| 499 | + style_submenu_element("Hierarchical", "%R/%s/%s", g.zPath, zName); | |
| 500 | + } | |
| 501 | + forum_display_chronological(froot, fpid); | |
| 502 | + }else{ | |
| 503 | + if( g.perm.Debug ){ | |
| 504 | + style_submenu_element("Chronological", "%R/%s/%s?t", g.zPath, zName); | |
| 505 | + } | |
| 506 | + forum_display_hierarchical(froot, fpid); | |
| 507 | + } | |
| 508 | + style_load_js("forum.js"); | |
| 509 | + style_footer(); | |
| 510 | +} | |
| 511 | + | |
| 512 | +/* | |
| 513 | +** Return true if a forum post should be moderated. | |
| 514 | +*/ | |
| 515 | +static int forum_need_moderation(void){ | |
| 516 | + if( P("domod") ) return 1; | |
| 517 | + if( g.perm.WrTForum ) return 0; | |
| 518 | + if( g.perm.ModForum ) return 0; | |
| 519 | + return 1; | |
| 520 | +} | |
| 521 | + | |
| 522 | +/* | |
| 523 | +** Add a new Forum Post artifact to the repository. | |
| 524 | +** | |
| 525 | +** Return true if a redirect occurs. | |
| 526 | +*/ | |
| 527 | +static int forum_post( | |
| 528 | + const char *zTitle, /* Title. NULL for replies */ | |
| 529 | + int iInReplyTo, /* Post replying to. 0 for new threads */ | |
| 530 | + int iEdit, /* Post being edited, or zero for a new post */ | |
| 531 | + const char *zUser, /* Username. NULL means use login name */ | |
| 532 | + const char *zMimetype, /* Mimetype of content. */ | |
| 533 | + const char *zContent /* Content */ | |
| 534 | +){ | |
| 535 | + char *zDate; | |
| 536 | + char *zI; | |
| 537 | + char *zG; | |
| 538 | + int iBasis; | |
| 539 | + Blob x, cksum, formatCheck, errMsg; | |
| 540 | + Manifest *pPost; | |
| 541 | + | |
| 542 | + schema_forum(); | |
| 543 | + if( iInReplyTo==0 && iEdit>0 ){ | |
| 544 | + iBasis = iEdit; | |
| 545 | + iInReplyTo = db_int(0, "SELECT firt FROM forumpost WHERE fpid=%d", iEdit); | |
| 546 | + }else{ | |
| 547 | + iBasis = iInReplyTo; | |
| 548 | + } | |
| 549 | + webpage_assert( (zTitle==0)+(iInReplyTo==0)==1 ); | |
| 550 | + blob_init(&x, 0, 0); | |
| 551 | + zDate = date_in_standard_format("now"); | |
| 552 | + blob_appendf(&x, "D %s\n", zDate); | |
| 553 | + fossil_free(zDate); | |
| 554 | + zG = db_text(0, | |
| 555 | + "SELECT uuid FROM blob, forumpost" | |
| 556 | + " WHERE blob.rid==forumpost.froot" | |
| 557 | + " AND forumpost.fpid=%d", iBasis); | |
| 558 | + if( zG ){ | |
| 559 | + blob_appendf(&x, "G %s\n", zG); | |
| 560 | + fossil_free(zG); | |
| 561 | + } | |
| 562 | + if( zTitle ){ | |
| 563 | + blob_appendf(&x, "H %F\n", zTitle); | |
| 564 | + } | |
| 565 | + zI = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", iInReplyTo); | |
| 566 | + if( zI ){ | |
| 567 | + blob_appendf(&x, "I %s\n", zI); | |
| 568 | + fossil_free(zI); | |
| 569 | + } | |
| 570 | + if( fossil_strcmp(zMimetype,"text/x-fossil-wiki")!=0 ){ | |
| 571 | + blob_appendf(&x, "N %s\n", zMimetype); | |
| 572 | + } | |
| 573 | + if( iEdit>0 ){ | |
| 574 | + char *zP = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", iEdit); | |
| 575 | + if( zP==0 ) webpage_error("missing edit artifact %d", iEdit); | |
| 576 | + blob_appendf(&x, "P %s\n", zP); | |
| 577 | + fossil_free(zP); | |
| 578 | + } | |
| 579 | + if( zUser==0 ){ | |
| 580 | + if( login_is_nobody() ){ | |
| 581 | + zUser = "anonymous"; | |
| 582 | + }else{ | |
| 583 | + zUser = login_name(); | |
| 584 | + } | |
| 585 | + } | |
| 586 | + blob_appendf(&x, "U %F\n", zUser); | |
| 587 | + blob_appendf(&x, "W %d\n%s\n", strlen(zContent), zContent); | |
| 588 | + md5sum_blob(&x, &cksum); | |
| 589 | + blob_appendf(&x, "Z %b\n", &cksum); | |
| 590 | + blob_reset(&cksum); | |
| 591 | + | |
| 592 | + /* Verify that the artifact we are creating is well-formed */ | |
| 593 | + blob_init(&formatCheck, 0, 0); | |
| 594 | + blob_init(&errMsg, 0, 0); | |
| 595 | + blob_copy(&formatCheck, &x); | |
| 596 | + pPost = manifest_parse(&formatCheck, 0, &errMsg); | |
| 597 | + if( pPost==0 ){ | |
| 598 | + webpage_error("malformed forum post artifact - %s", blob_str(&errMsg)); | |
| 599 | + } | |
| 600 | + webpage_assert( pPost->type==CFTYPE_FORUM ); | |
| 601 | + manifest_destroy(pPost); | |
| 602 | + | |
| 603 | + if( P("dryrun") ){ | |
| 604 | + @ <div class='debug'> | |
| 605 | + @ This is the artifact that would have been generated: | |
| 606 | + @ <pre>%h(blob_str(&x))</pre> | |
| 607 | + @ </div> | |
| 608 | + blob_reset(&x); | |
| 609 | + return 0; | |
| 610 | + }else{ | |
| 611 | + int nrid = wiki_put(&x, 0, forum_need_moderation()); | |
| 612 | + cgi_redirectf("%R/forumpost/%S", rid_to_uuid(nrid)); | |
| 613 | + return 1; | |
| 614 | + } | |
| 615 | +} | |
| 616 | + | |
| 617 | +/* | |
| 618 | +** Paint the form elements for entering a Forum post | |
| 619 | +*/ | |
| 620 | +static void forum_entry_widget( | |
| 621 | + const char *zTitle, | |
| 622 | + const char *zMimetype, | |
| 623 | + const char *zContent | |
| 624 | +){ | |
| 625 | + if( zTitle ){ | |
| 626 | + @ Title: <input type="input" name="title" value="%h(zTitle)" size="50"><br> | |
| 627 | + } | |
| 628 | + @ Markup style: | |
| 629 | + mimetype_option_menu(zMimetype); | |
| 630 | + @ <br><textarea name="content" class="wikiedit" cols="80" \ | |
| 631 | + @ rows="25" wrap="virtual">%h(zContent)</textarea><br> | |
| 632 | +} | |
| 633 | + | |
| 634 | +/* | |
| 635 | +** WEBPAGE: forumnew | |
| 636 | +** WEBPAGE: forumedit | |
| 637 | +** | |
| 638 | +** Start a new thread on the forum or reply to an existing thread. | |
| 639 | +** But first prompt to see if the user would like to log in. | |
| 640 | +*/ | |
| 641 | +void forum_page_init(void){ | |
| 642 | + int isEdit; | |
| 643 | + char *zGoto; | |
| 644 | + login_check_credentials(); | |
| 645 | + if( !g.perm.WrForum ){ | |
| 646 | + login_needed(g.anon.WrForum); | |
| 647 | + return; | |
| 648 | + } | |
| 649 | + if( sqlite3_strglob("*edit*", g.zPath)==0 ){ | |
| 650 | + zGoto = mprintf("%R/forume2?fpid=%S",PD("fpid","")); | |
| 651 | + isEdit = 1; | |
| 652 | + }else{ | |
| 653 | + zGoto = mprintf("%R/forume1"); | |
| 654 | + isEdit = 0; | |
| 655 | + } | |
| 656 | + if( login_is_individual() ){ | |
| 657 | + if( isEdit ){ | |
| 658 | + forumedit_page(); | |
| 659 | + }else{ | |
| 660 | + forumnew_page(); | |
| 661 | + } | |
| 662 | + return; | |
| 663 | + } | |
| 664 | + style_header("%h As Anonymous?", isEdit ? "Reply" : "Post"); | |
| 665 | + @ <p>You are not logged in. | |
| 666 | + @ <p><table border="0" cellpadding="10"> | |
| 667 | + @ <tr><td> | |
| 668 | + @ <form action="%s(zGoto)" method="POST"> | |
| 669 | + @ <input type="submit" value="Remain Anonymous"> | |
| 670 | + @ </form> | |
| 671 | + @ <td>Post to the forum anonymously | |
| 672 | + if( login_self_register_available(0) ){ | |
| 673 | + @ <tr><td> | |
| 674 | + @ <form action="%R/register" method="POST"> | |
| 675 | + @ <input type="hidden" name="g" value="%s(zGoto)"> | |
| 676 | + @ <input type="submit" value="Create An Account"> | |
| 677 | + @ </form> | |
| 678 | + @ <td>Create a new account and post using that new account | |
| 679 | + } | |
| 680 | + @ <tr><td> | |
| 681 | + @ <form action="%R/login" method="POST"> | |
| 682 | + @ <input type="hidden" name="g" value="%s(zGoto)"> | |
| 683 | + @ <input type="hidden" name="noanon" value="1"> | |
| 684 | + @ <input type="submit" value="Login"> | |
| 685 | + @ </form> | |
| 686 | + @ <td>Log into an existing account | |
| 687 | + @ </table> | |
| 688 | + style_footer(); | |
| 689 | + fossil_free(zGoto); | |
| 690 | +} | |
| 691 | + | |
| 692 | +/* | |
| 693 | +** Write the "From: USER" line on the webpage. | |
| 694 | +*/ | |
| 695 | +static void forum_from_line(void){ | |
| 696 | + if( login_is_nobody() ){ | |
| 697 | + @ From: anonymous<br> | |
| 698 | + }else{ | |
| 699 | + @ From: %h(login_name())<br> | |
| 700 | + } | |
| 701 | +} | |
| 702 | + | |
| 703 | +/* | |
| 704 | +** WEBPAGE: forume1 | |
| 705 | +** | |
| 706 | +** Start a new forum thread. | |
| 707 | +*/ | |
| 708 | +void forumnew_page(void){ | |
| 709 | + const char *zTitle = PDT("title",""); | |
| 710 | + const char *zMimetype = PD("mimetype","text/x-fossil-wiki"); | |
| 711 | + const char *zContent = PDT("content",""); | |
| 712 | + login_check_credentials(); | |
| 713 | + if( !g.perm.WrForum ){ | |
| 714 | + login_needed(g.anon.WrForum); | |
| 715 | + return; | |
| 716 | + } | |
| 717 | + if( P("submit") ){ | |
| 718 | + if( forum_post(zTitle, 0, 0, 0, zMimetype, zContent) ) return; | |
| 719 | + } | |
| 720 | + if( P("preview") ){ | |
| 721 | + @ <h1>Preview:</h1> | |
| 722 | + forum_render(zTitle, zMimetype, zContent, "forumEdit"); | |
| 723 | + } | |
| 724 | + style_header("New Forum Thread"); | |
| 725 | + @ <form action="%R/forume1" method="POST"> | |
| 726 | + @ <h1>New Message:</h1> | |
| 727 | + forum_from_line(); | |
| 728 | + forum_entry_widget(zTitle, zMimetype, zContent); | |
| 729 | + @ <input type="submit" name="preview" value="Preview"> | |
| 730 | + if( P("preview") ){ | |
| 731 | + @ <input type="submit" name="submit" value="Submit"> | |
| 732 | + }else{ | |
| 733 | + @ <input type="submit" name="submit" value="Submit" disabled> | |
| 734 | + } | |
| 735 | + if( g.perm.Debug ){ | |
| 736 | + /* For the test-forumnew page add these extra debugging controls */ | |
| 737 | + @ <div class="debug"> | |
| 738 | + @ <label><input type="checkbox" name="dryrun" %s(PCK("dryrun"))> \ | |
| 739 | + @ Dry run</label> | |
| 740 | + @ <br><label><input type="checkbox" name="domod" %s(PCK("domod"))> \ | |
| 741 | + @ Require moderator approval</label> | |
| 742 | + @ <br><label><input type="checkbox" name="showqp" %s(PCK("showqp"))> \ | |
| 743 | + @ Show query parameters</label> | |
| 744 | + @ </div> | |
| 745 | + } | |
| 746 | + @ </form> | |
| 747 | + style_footer(); | |
| 748 | +} | |
| 749 | + | |
| 750 | +/* | |
| 751 | +** WEBPAGE: forume2 | |
| 752 | +** | |
| 753 | +** Edit an existing forum message. | |
| 754 | +** Query parameters: | |
| 755 | +** | |
| 756 | +** fpid=X Hash of the post to be editted. REQUIRED | |
| 757 | +*/ | |
| 758 | +void forumedit_page(void){ | |
| 759 | + int fpid; | |
| 760 | + Manifest *pPost; | |
| 761 | + const char *zMimetype = 0; | |
| 762 | + const char *zContent = 0; | |
| 763 | + const char *zTitle = 0; | |
| 764 | + int isCsrfSafe; | |
| 765 | + int isDelete = 0; | |
| 766 | + | |
| 767 | + login_check_credentials(); | |
| 768 | + if( !g.perm.WrForum ){ | |
| 769 | + login_needed(g.anon.WrForum); | |
| 770 | + return; | |
| 771 | + } | |
| 772 | + fpid = symbolic_name_to_rid(PD("fpid",""), "f"); | |
| 773 | + if( fpid<=0 || (pPost = manifest_get(fpid, CFTYPE_FORUM, 0))==0 ){ | |
| 774 | + webpage_error("Missing or invalid fpid query parameter"); | |
| 775 | + } | |
| 776 | + if( P("cancel") ){ | |
| 777 | + cgi_redirectf("%R/forumpost/%S",P("fpid")); | |
| 778 | + return; | |
| 779 | + } | |
| 780 | + isCsrfSafe = cgi_csrf_safe(1); | |
| 781 | + if( g.perm.ModForum && isCsrfSafe ){ | |
| 782 | + if( P("approve") ){ | |
| 783 | + moderation_approve(fpid); | |
| 784 | + cgi_redirectf("%R/forumpost/%S",P("fpid")); | |
| 785 | + return; | |
| 786 | + } | |
| 787 | + if( P("reject") ){ | |
| 788 | + char *zParent = | |
| 789 | + db_text(0, | |
| 790 | + "SELECT uuid FROM forumpost, blob" | |
| 791 | + " WHERE forumpost.fpid=%d AND blob.rid=forumpost.firt", | |
| 792 | + fpid | |
| 793 | + ); | |
| 794 | + moderation_disapprove(fpid); | |
| 795 | + if( zParent ){ | |
| 796 | + cgi_redirectf("%R/forumpost/%S",zParent); | |
| 797 | + }else{ | |
| 798 | + cgi_redirectf("%R/forum"); | |
| 799 | + } | |
| 800 | + return; | |
| 801 | + } | |
| 802 | + } | |
| 803 | + isDelete = P("nullout")!=0; | |
| 804 | + if( P("submit") && isCsrfSafe ){ | |
| 805 | + int done = 1; | |
| 806 | + const char *zMimetype = PD("mimetype","text/x-fossil-wiki"); | |
| 807 | + const char *zContent = PDT("content",""); | |
| 808 | + if( P("reply") ){ | |
| 809 | + done = forum_post(0, fpid, 0, 0, zMimetype, zContent); | |
| 810 | + }else if( P("edit") || isDelete ){ | |
| 811 | + done = forum_post(P("title"), 0, fpid, 0, zMimetype, zContent); | |
| 812 | + }else{ | |
| 813 | + webpage_error("Missing 'reply' query parameter"); | |
| 814 | + } | |
| 815 | + if( done ) return; | |
| 816 | + } | |
| 817 | + if( isDelete ){ | |
| 818 | + zMimetype = "text/x-fossil-wiki"; | |
| 819 | + zContent = ""; | |
| 820 | + if( pPost->zThreadTitle ) zTitle = ""; | |
| 821 | + style_header("Delete %s", zTitle ? "Post" : "Reply"); | |
| 822 | + @ <h1>Original Post:</h1> | |
| 823 | + forum_render(pPost->zThreadTitle, pPost->zMimetype, pPost->zWiki, | |
| 824 | + "forumEdit"); | |
| 825 | + @ <h1>Change Into:</h1> | |
| 826 | + forum_render(zTitle, zMimetype, zContent,"forumEdit"); | |
| 827 | + @ <form action="%R/forume2" method="POST"> | |
| 828 | + @ <input type="hidden" name="fpid" value="%h(P("fpid"))"> | |
| 829 | + @ <input type="hidden" name="nullout" value="1"> | |
| 830 | + @ <input type="hidden" name="mimetype" value="%h(zMimetype)"> | |
| 831 | + @ <input type="hidden" name="content" value="%h(zContent)"> | |
| 832 | + if( zTitle ){ | |
| 833 | + @ <input type="hidden" name="title" value="%h(zTitle)"> | |
| 834 | + } | |
| 835 | + }else if( P("edit") ){ | |
| 836 | + /* Provide an edit to the fpid post */ | |
| 837 | + zMimetype = P("mimetype"); | |
| 838 | + zContent = PT("content"); | |
| 839 | + zTitle = P("title"); | |
| 840 | + if( zContent==0 ) zContent = fossil_strdup(pPost->zWiki); | |
| 841 | + if( zMimetype==0 ) zMimetype = fossil_strdup(pPost->zMimetype); | |
| 842 | + if( zTitle==0 && pPost->zThreadTitle!=0 ){ | |
| 843 | + zTitle = fossil_strdup(pPost->zThreadTitle); | |
| 844 | + } | |
| 845 | + style_header("Edit %s", zTitle ? "Post" : "Reply"); | |
| 846 | + @ <h1>Original Post:</h1> | |
| 847 | + forum_render(pPost->zThreadTitle, pPost->zMimetype, pPost->zWiki, | |
| 848 | + "forumEdit"); | |
| 849 | + if( P("preview") ){ | |
| 850 | + @ <h1>Preview Of Editted Post:</h1> | |
| 851 | + forum_render(zTitle, zMimetype, zContent,"forumEdit"); | |
| 852 | + } | |
| 853 | + @ <h1>Revised Message:</h1> | |
| 854 | + @ <form action="%R/forume2" method="POST"> | |
| 855 | + @ <input type="hidden" name="fpid" value="%h(P("fpid"))"> | |
| 856 | + @ <input type="hidden" name="edit" value="1"> | |
| 857 | + forum_from_line(); | |
| 858 | + forum_entry_widget(zTitle, zMimetype, zContent); | |
| 859 | + }else{ | |
| 860 | + /* Reply */ | |
| 861 | + zMimetype = PD("mimetype","text/x-fossil-wiki"); | |
| 862 | + zContent = PDT("content",""); | |
| 863 | + style_header("Reply"); | |
| 864 | + @ <h1>Replying To:</h1> | |
| 865 | + forum_render(0, pPost->zMimetype, pPost->zWiki, "forumEdit"); | |
| 866 | + if( P("preview") ){ | |
| 867 | + @ <h1>Preview:</h1> | |
| 868 | + forum_render(0, zMimetype,zContent, "forumEdit"); | |
| 869 | + } | |
| 870 | + @ <h1>Enter Reply:</h1> | |
| 871 | + @ <form action="%R/forume2" method="POST"> | |
| 872 | + @ <input type="hidden" name="fpid" value="%h(P("fpid"))"> | |
| 873 | + @ <input type="hidden" name="reply" value="1"> | |
| 874 | + forum_from_line(); | |
| 875 | + forum_entry_widget(0, zMimetype, zContent); | |
| 876 | + } | |
| 877 | + if( !isDelete ){ | |
| 878 | + @ <input type="submit" name="preview" value="Preview"> | |
| 879 | + } | |
| 880 | + @ <input type="submit" name="cancel" value="Cancel"> | |
| 881 | + if( P("preview") || isDelete ){ | |
| 882 | + @ <input type="submit" name="submit" value="Submit"> | |
| 883 | + } | |
| 884 | + if( g.perm.Debug ){ | |
| 885 | + /* For the test-forumnew page add these extra debugging controls */ | |
| 886 | + @ <div class="debug"> | |
| 887 | + @ <label><input type="checkbox" name="dryrun" %s(PCK("dryrun"))> \ | |
| 888 | + @ Dry run</label> | |
| 889 | + @ <br><label><input type="checkbox" name="domod" %s(PCK("domod"))> \ | |
| 890 | + @ Require moderator approval</label> | |
| 891 | + @ <br><label><input type="checkbox" name="showqp" %s(PCK("showqp"))> \ | |
| 892 | + @ Show query parameters</label> | |
| 893 | + @ </div> | |
| 894 | + } | |
| 895 | + @ </form> | |
| 166 | 896 | style_footer(); |
| 167 | 897 | } |
| 168 | 898 | |
| 169 | 899 | /* |
| 170 | -** Use content in CGI parameters "s" (subject), "b" (body), and | |
| 171 | -** "mimetype" (mimetype) to create a new forum entry. | |
| 172 | -** Return the id of the new forum entry. | |
| 173 | -** | |
| 174 | -** If any problems occur, return 0 and set *pzErr to a description of | |
| 175 | -** the problem. | |
| 176 | -** | |
| 177 | -** Cases: | |
| 178 | -** | |
| 179 | -** itemId==0 && parentId==0 Starting a new thread. | |
| 180 | -** itemId==0 && parentId>0 New reply to parentId | |
| 181 | -** itemId>0 && parentId==0 Edit existing post itemId | |
| 182 | -*/ | |
| 183 | -static int forum_post(int itemId, int parentId, char **pzErr){ | |
| 184 | - const char *zSubject = 0; | |
| 185 | - int threadId; | |
| 186 | - double rNow = db_double(0.0, "SELECT julianday('now')"); | |
| 187 | - const char *zMime = wiki_filter_mimetypes(P("mimetype")); | |
| 188 | - if( itemId==0 && parentId==0 ){ | |
| 189 | - /* Start a new thread. Subject required. */ | |
| 190 | - sqlite3_uint64 r1, r2; | |
| 191 | - zSubject = PT("s"); | |
| 192 | - if( zSubject==0 || zSubject[0]==0 ){ | |
| 193 | - *pzErr = "\"Subject\" required to start a new thread"; | |
| 194 | - return 0; | |
| 195 | - } | |
| 196 | - sqlite3_randomness(sizeof(r1), &r1); | |
| 197 | - sqlite3_randomness(sizeof(r2), &r2); | |
| 198 | - db_multi_exec( | |
| 199 | - "INSERT INTO forumthread(mthreadhash, mtitle, mtime, npost)" | |
| 200 | - "VALUES(lower(hex(randomblob(28))),%Q,%!.17g,1)", | |
| 201 | - zSubject, rNow | |
| 202 | - ); | |
| 203 | - threadId = db_last_insert_rowid(); | |
| 204 | - }else{ | |
| 205 | - threadId = db_int(0, "SELECT mthreadid FROM forumpost" | |
| 206 | - " WHERE mpostid=%d", itemId ? itemId : parentId); | |
| 207 | - } | |
| 208 | - if( itemId ){ | |
| 209 | - if( db_int(0, "SELECT inreplyto IS NULL FROM forumpost" | |
| 210 | - " WHERE mpostid=%d", itemId) ){ | |
| 211 | - db_multi_exec( | |
| 212 | - "UPDATE forumthread SET mtitle=%Q WHERE mthreadid=%d", | |
| 213 | - PT("s"), threadId | |
| 214 | - ); | |
| 215 | - } | |
| 216 | - db_multi_exec( | |
| 217 | - "UPDATE forumpost SET" | |
| 218 | - " mtime=%!.17g," | |
| 219 | - " mimetype=%Q," | |
| 220 | - " ipaddr=%Q," | |
| 221 | - " mbody=%Q" | |
| 222 | - " WHERE mpostid=%d", | |
| 223 | - rNow, PT("mimetype"), P("REMOTE_ADDR"), PT("b"), itemId | |
| 224 | - ); | |
| 225 | - }else{ | |
| 226 | - db_multi_exec( | |
| 227 | - "INSERT INTO forumpost(mposthash,mthreadid,uname,mtime," | |
| 228 | - " mstatus,mimetype,ipaddr,inreplyto,mbody) VALUES" | |
| 229 | - " (lower(hex(randomblob(28))),%d,%Q,%!.17g,%Q,%Q,%Q,nullif(%d,0),%Q)", | |
| 230 | - threadId,g.zLogin,rNow,NULL,zMime,P("REMOTE_ADDR"),parentId,P("b")); | |
| 231 | - itemId = db_last_insert_rowid(); | |
| 232 | - } | |
| 233 | - if( zSubject==0 ){ | |
| 234 | - db_multi_exec( | |
| 235 | - "UPDATE forumthread SET mtime=%!.17g, npost=npost+1" | |
| 236 | - " WHERE mthreadid=(SELECT mthreadid FROM forumpost WHERE mpostid=%d)", | |
| 237 | - rNow, itemId | |
| 238 | - ); | |
| 239 | - } | |
| 240 | - return itemId; | |
| 241 | -} | |
| 242 | - | |
| 243 | -/* | |
| 244 | -** WEBPAGE: forumedit | |
| 245 | -** | |
| 246 | -** Query parameters: | |
| 247 | -** | |
| 248 | -** replyto=N Enter a reply to forum item N | |
| 249 | -** item=N Edit item N | |
| 250 | -** s=SUBJECT Subject. New thread only. Omitted for replies | |
| 251 | -** b=BODY Body of the post | |
| 252 | -** m=MIMETYPE Mimetype for the body of the post | |
| 253 | -** x Submit changes | |
| 254 | -** p Preview changes | |
| 255 | -*/ | |
| 256 | -void forum_edit_page(void){ | |
| 257 | - int itemId; | |
| 258 | - int parentId; | |
| 259 | - char *zErr = 0; | |
| 260 | - const char *zMime; | |
| 261 | - const char *zSub; | |
| 262 | - | |
| 263 | - login_check_credentials(); | |
| 264 | - if( !g.perm.WrForum ){ login_needed(g.anon.WrForum); return; } | |
| 265 | - forum_verify_schema(); | |
| 266 | - itemId = atoi(PD("item","0")); | |
| 267 | - parentId = atoi(PD("replyto","0")); | |
| 268 | - if( P("cancel")!=0 ){ | |
| 269 | - cgi_redirectf("%R/forum?item=%d", itemId ? itemId : parentId); | |
| 270 | - return; | |
| 271 | - } | |
| 272 | - if( P("x")!=0 && cgi_csrf_safe(1) ){ | |
| 273 | - itemId = forum_post(itemId,parentId,&zErr); | |
| 274 | - if( itemId ){ | |
| 275 | - cgi_redirectf("%R/forum?item=%d",itemId); | |
| 276 | - return; | |
| 277 | - } | |
| 278 | - } | |
| 279 | - if( itemId && (P("mimetype")==0 || P("b")==0) ){ | |
| 280 | - Stmt q; | |
| 281 | - db_prepare(&q, "SELECT mimetype, mbody FROM forumpost" | |
| 282 | - " WHERE mpostid=%d", itemId); | |
| 283 | - if( db_step(&q)==SQLITE_ROW ){ | |
| 284 | - if( P("mimetype")==0 ){ | |
| 285 | - cgi_set_query_parameter("mimetype", db_column_text(&q, 0)); | |
| 286 | - } | |
| 287 | - if( P("b")==0 ){ | |
| 288 | - cgi_set_query_parameter("b", db_column_text(&q, 1)); | |
| 289 | - } | |
| 290 | - } | |
| 291 | - db_finalize(&q); | |
| 292 | - } | |
| 293 | - zMime = wiki_filter_mimetypes(P("mimetype")); | |
| 294 | - if( itemId>0 ){ | |
| 295 | - style_header("Edit Forum Post"); | |
| 296 | - }else if( parentId>0 ){ | |
| 297 | - style_header("Comment On Forum Post"); | |
| 298 | - }else{ | |
| 299 | - style_header("New Forum Thread"); | |
| 300 | - } | |
| 301 | - @ <form action="%R/forumedit" method="POST"> | |
| 302 | - if( itemId ){ | |
| 303 | - @ <input type="hidden" name="item" value="%d(itemId)"> | |
| 304 | - } | |
| 305 | - if( parentId ){ | |
| 306 | - @ <input type="hidden" name="replyto" value="%d(parentId)"> | |
| 307 | - } | |
| 308 | - if( P("p") ){ | |
| 309 | - Blob x; | |
| 310 | - @ <div class="forumpreview"> | |
| 311 | - if( P("s") ){ | |
| 312 | - @ <h1>%h(PT("s"))</h1> | |
| 313 | - } | |
| 314 | - @ <div class="forumpreviewbody"> | |
| 315 | - blob_init(&x, PT("b"), -1); | |
| 316 | - wiki_render_by_mimetype(&x, PT("mimetype")); | |
| 317 | - blob_reset(&x); | |
| 318 | - @ </div> | |
| 319 | - @ </div> | |
| 320 | - @ <hr> | |
| 321 | - } | |
| 322 | - @ <table border="0" class="forumeditform"> | |
| 323 | - if( zErr ){ | |
| 324 | - @ <tr><td colspan="2"> | |
| 325 | - @ <span class='forumFormErr'>%h(zErr)</span> | |
| 326 | - } | |
| 327 | - if( (itemId==0 && parentId==0) | |
| 328 | - || (itemId && db_int(0, "SELECT inreplyto IS NULL FROM forumpost" | |
| 329 | - " WHERE mpostid=%d", itemId)) | |
| 330 | - ){ | |
| 331 | - zSub = PT("s"); | |
| 332 | - if( zSub==0 && itemId ){ | |
| 333 | - zSub = db_text("", | |
| 334 | - "SELECT mtitle FROM forumthread" | |
| 335 | - " WHERE mthreadid=(SELECT mthreadid FROM forumpost" | |
| 336 | - " WHERE mpostid=%d)", itemId); | |
| 337 | - } | |
| 338 | - @ <tr><td>Subject:</td> | |
| 339 | - @ <td><input type='text' class='forumFormSubject' name='s' value='%h(zSub)'> | |
| 340 | - } | |
| 341 | - @ <tr><td>Markup:</td><td> | |
| 342 | - mimetype_option_menu(zMime); | |
| 343 | - @ <tr><td>Comment:</td><td> | |
| 344 | - @ <textarea name="b" class="wikiedit" cols="80"\ | |
| 345 | - @ rows="20" wrap="virtual">%h(PD("b",""))</textarea></td> | |
| 346 | - @ <tr><td></td><td> | |
| 347 | - @ <input type="submit" name="p" value="Preview"> | |
| 348 | - if( P("p")!=0 ){ | |
| 349 | - @ <input type="submit" name="x" value="Submit"> | |
| 350 | - } | |
| 351 | - @ <input type="submit" name="cancel" value="Cancel"> | |
| 352 | - @ </table> | |
| 353 | - @ </form> | |
| 900 | +** WEBPAGE: forum | |
| 901 | +** | |
| 902 | +** The main page for the forum feature. Show a list of recent forum | |
| 903 | +** threads. Also show a search box at the top if search is enabled, | |
| 904 | +** and a button for creating a new thread, if enabled. | |
| 905 | +*/ | |
| 906 | +void forum_main_page(void){ | |
| 907 | + Stmt q; | |
| 908 | + int iLimit, iOfst; | |
| 909 | + login_check_credentials(); | |
| 910 | + if( !g.perm.RdForum ){ | |
| 911 | + login_needed(g.anon.RdForum); | |
| 912 | + return; | |
| 913 | + } | |
| 914 | + style_header("Forum"); | |
| 915 | + if( g.perm.WrForum ){ | |
| 916 | + style_submenu_element("New Message","%R/forumnew"); | |
| 917 | + } | |
| 918 | + if( g.perm.ModForum && moderation_needed() ){ | |
| 919 | + style_submenu_element("Moderation Requests", "%R/modreq"); | |
| 920 | + } | |
| 921 | + if( search_screen(SRCH_FORUM, 0) ){ | |
| 922 | + style_submenu_element("Recent Threads","%R/forum"); | |
| 923 | + style_footer(); | |
| 924 | + return; | |
| 925 | + } | |
| 926 | + iLimit = 50; | |
| 927 | + iOfst = 0; | |
| 928 | + @ <h1>Recent Threads</h1> | |
| 929 | + @ <div class='fileage'><table width="100%%"> | |
| 930 | + db_prepare(&q, | |
| 931 | + "SELECT julianday('now') - max(fmtime)," | |
| 932 | + " (SELECT uuid FROM blob WHERE rid=fpid)," | |
| 933 | + " (SELECT substr(comment,instr(comment,':')+2)" | |
| 934 | + " FROM event WHERE objid=fpid)" | |
| 935 | + " FROM forumpost" | |
| 936 | + " GROUP BY froot ORDER BY 1 LIMIT %d OFFSET %d", | |
| 937 | + iLimit, iOfst | |
| 938 | + ); | |
| 939 | + while( db_step(&q)==SQLITE_ROW ){ | |
| 940 | + char *zAge = human_readable_age(db_column_double(&q,0)); | |
| 941 | + const char *zUuid = db_column_text(&q, 1); | |
| 942 | + const char *zTitle = db_column_text(&q, 2); | |
| 943 | + @ <tr><td>%h(zAge) ago</td> | |
| 944 | + @ <td>%z(href("%R/forumpost/%S",zUuid))%h(zTitle)</a> | |
| 945 | + @ </tr> | |
| 946 | + fossil_free(zAge); | |
| 947 | + } | |
| 948 | + @ </table></div> | |
| 354 | 949 | style_footer(); |
| 355 | 950 | } |
| 356 | 951 | |
| 357 | 952 | ADDED src/forum.js |
| --- src/forum.c | |
| +++ src/forum.c | |
| @@ -19,337 +19,932 @@ | |
| 19 | */ |
| 20 | #include "config.h" |
| 21 | #include <assert.h> |
| 22 | #include "forum.h" |
| 23 | |
| 24 | /* |
| 25 | ** The schema for the tables that manage the forum, if forum is |
| 26 | ** enabled. |
| 27 | */ |
| 28 | static const char zForumInit[] = |
| 29 | @ CREATE TABLE repository.forumpost( |
| 30 | @ mpostid INTEGER PRIMARY KEY, -- unique id for each post (local) |
| 31 | @ mposthash TEXT, -- uuid for this post |
| 32 | @ mthreadid INTEGER, -- thread to which this post belongs |
| 33 | @ uname TEXT, -- name of user |
| 34 | @ mtime REAL, -- julian day number |
| 35 | @ mstatus TEXT, -- status. NULL=ok. 'mod'=pending moderation |
| 36 | @ mimetype TEXT, -- Mimetype for mbody |
| 37 | @ ipaddr TEXT, -- IP address of post origin |
| 38 | @ inreplyto INT, -- Parent posting |
| 39 | @ mbody TEXT -- Content of the post |
| 40 | @ ); |
| 41 | @ CREATE INDEX repository.forumpost_x1 ON |
| 42 | @ forumpost(inreplyto,mtime); |
| 43 | @ CREATE TABLE repository.forumthread( |
| 44 | @ mthreadid INTEGER PRIMARY KEY, |
| 45 | @ mthreadhash TEXT, -- uuid for this thread |
| 46 | @ mtitle TEXT, -- Title or subject line |
| 47 | @ mtime REAL, -- Most recent update |
| 48 | @ npost INT -- Number of posts on this thread |
| 49 | @ ); |
| 50 | ; |
| 51 | |
| 52 | /* |
| 53 | ** Create the forum tables in the schema if they do not already |
| 54 | ** exist. |
| 55 | */ |
| 56 | static void forum_verify_schema(void){ |
| 57 | if( !db_table_exists("repository","forumpost") ){ |
| 58 | db_multi_exec(zForumInit /*works-like:""*/); |
| 59 | } |
| 60 | } |
| 61 | |
| 62 | /* |
| 63 | ** WEBPAGE: forum |
| 64 | ** URL: /forum |
| 65 | ** Query parameters: |
| 66 | ** |
| 67 | ** item=N Show post N and its replies |
| 68 | ** |
| 69 | */ |
| 70 | void forum_page(void){ |
| 71 | int itemId; |
| 72 | Stmt q; |
| 73 | int i; |
| 74 | |
| 75 | login_check_credentials(); |
| 76 | if( !g.perm.RdForum ){ login_needed(g.anon.RdForum); return; } |
| 77 | forum_verify_schema(); |
| 78 | style_header("Forum"); |
| 79 | itemId = atoi(PD("item","0")); |
| 80 | if( itemId>0 ){ |
| 81 | int iUp; |
| 82 | double rNow; |
| 83 | style_submenu_element("Topics", "%R/forum"); |
| 84 | iUp = db_int(0, "SELECT inreplyto FROM forumpost WHERE mpostid=%d", itemId); |
| 85 | if( iUp ){ |
| 86 | style_submenu_element("Parent", "%R/forum?item=%d", iUp); |
| 87 | } |
| 88 | rNow = db_double(0.0, "SELECT julianday('now')"); |
| 89 | /* Show the post given by itemId and all its descendents */ |
| 90 | db_prepare(&q, |
| 91 | "WITH RECURSIVE" |
| 92 | " post(id,uname,mstat,mime,ipaddr,parent,mbody,depth,mtime) AS (" |
| 93 | " SELECT mpostid, uname, mstatus, mimetype, ipaddr, inreplyto, mbody," |
| 94 | " 0, mtime FROM forumpost WHERE mpostid=%d" |
| 95 | " UNION" |
| 96 | " SELECT f.mpostid, f.uname, f.mstatus, f.mimetype, f.ipaddr," |
| 97 | " f.inreplyto, f.mbody, p.depth+1 AS xdepth, f.mtime AS xtime" |
| 98 | " FROM forumpost AS f, post AS p" |
| 99 | " WHERE f.inreplyto=p.id" |
| 100 | " ORDER BY xdepth DESC, xtime ASC" |
| 101 | ") SELECT * FROM post;", |
| 102 | itemId |
| 103 | ); |
| 104 | while( db_step(&q)==SQLITE_ROW ){ |
| 105 | int id = db_column_int(&q, 0); |
| 106 | const char *zUser = db_column_text(&q, 1); |
| 107 | const char *zMime = db_column_text(&q, 3); |
| 108 | int iDepth = db_column_int(&q, 7); |
| 109 | double rMTime = db_column_double(&q, 8); |
| 110 | char *zAge = db_timespan_name(rNow - rMTime); |
| 111 | Blob body; |
| 112 | @ <!-- Forum post %d(id) --> |
| 113 | @ <table class="forum_post"> |
| 114 | @ <tr> |
| 115 | @ <td class="forum_margin" width="%d(iDepth*40)" rowspan="2"> |
| 116 | @ <td><span class="forum_author">%h(zUser)</span> |
| 117 | @ <span class="forum_age">%s(zAge) ago</span> |
| 118 | sqlite3_free(zAge); |
| 119 | if( g.perm.WrForum ){ |
| 120 | @ <span class="forum_buttons"> |
| 121 | if( g.perm.AdminForum || fossil_strcmp(g.zLogin, zUser)==0 ){ |
| 122 | @ <a href='%R/forumedit?item=%d(id)'>Edit</a> |
| 123 | } |
| 124 | @ <a href='%R/forumedit?replyto=%d(id)'>Reply</a> |
| 125 | @ </span> |
| 126 | } |
| 127 | @ </tr> |
| 128 | @ <tr><td><div class="forum_body"> |
| 129 | blob_init(&body, db_column_text(&q,6), db_column_bytes(&q,6)); |
| 130 | wiki_render_by_mimetype(&body, zMime); |
| 131 | blob_reset(&body); |
| 132 | @ </div></td></tr> |
| 133 | @ </table> |
| 134 | } |
| 135 | }else{ |
| 136 | /* If we reach this point, that means the users wants a list of |
| 137 | ** recent threads. |
| 138 | */ |
| 139 | i = 0; |
| 140 | db_prepare(&q, |
| 141 | "SELECT a.mtitle, a.npost, b.mpostid" |
| 142 | " FROM forumthread AS a, forumpost AS b " |
| 143 | " WHERE a.mthreadid=b.mthreadid" |
| 144 | " AND b.inreplyto IS NULL" |
| 145 | " ORDER BY a.mtime DESC LIMIT 40" |
| 146 | ); |
| 147 | if( g.perm.WrForum ){ |
| 148 | style_submenu_element("New", "%R/forumedit"); |
| 149 | } |
| 150 | @ <h1>Recent Forum Threads</h1> |
| 151 | while( db_step(&q)==SQLITE_ROW ){ |
| 152 | int n = db_column_int(&q,1); |
| 153 | int itemid = db_column_int(&q,2); |
| 154 | const char *zTitle = db_column_text(&q,0); |
| 155 | if( (i++)==0 ){ |
| 156 | @ <ol> |
| 157 | } |
| 158 | @ <li><span class="forum_title"> |
| 159 | @ %z(href("%R/forum?item=%d",itemid))%h(zTitle)</a></span> |
| 160 | @ <span class="forum_npost">%d(n) post%s(n==1?"":"s")</span></li> |
| 161 | } |
| 162 | if( i ){ |
| 163 | @ </ol> |
| 164 | } |
| 165 | } |
| 166 | style_footer(); |
| 167 | } |
| 168 | |
| 169 | /* |
| 170 | ** Use content in CGI parameters "s" (subject), "b" (body), and |
| 171 | ** "mimetype" (mimetype) to create a new forum entry. |
| 172 | ** Return the id of the new forum entry. |
| 173 | ** |
| 174 | ** If any problems occur, return 0 and set *pzErr to a description of |
| 175 | ** the problem. |
| 176 | ** |
| 177 | ** Cases: |
| 178 | ** |
| 179 | ** itemId==0 && parentId==0 Starting a new thread. |
| 180 | ** itemId==0 && parentId>0 New reply to parentId |
| 181 | ** itemId>0 && parentId==0 Edit existing post itemId |
| 182 | */ |
| 183 | static int forum_post(int itemId, int parentId, char **pzErr){ |
| 184 | const char *zSubject = 0; |
| 185 | int threadId; |
| 186 | double rNow = db_double(0.0, "SELECT julianday('now')"); |
| 187 | const char *zMime = wiki_filter_mimetypes(P("mimetype")); |
| 188 | if( itemId==0 && parentId==0 ){ |
| 189 | /* Start a new thread. Subject required. */ |
| 190 | sqlite3_uint64 r1, r2; |
| 191 | zSubject = PT("s"); |
| 192 | if( zSubject==0 || zSubject[0]==0 ){ |
| 193 | *pzErr = "\"Subject\" required to start a new thread"; |
| 194 | return 0; |
| 195 | } |
| 196 | sqlite3_randomness(sizeof(r1), &r1); |
| 197 | sqlite3_randomness(sizeof(r2), &r2); |
| 198 | db_multi_exec( |
| 199 | "INSERT INTO forumthread(mthreadhash, mtitle, mtime, npost)" |
| 200 | "VALUES(lower(hex(randomblob(28))),%Q,%!.17g,1)", |
| 201 | zSubject, rNow |
| 202 | ); |
| 203 | threadId = db_last_insert_rowid(); |
| 204 | }else{ |
| 205 | threadId = db_int(0, "SELECT mthreadid FROM forumpost" |
| 206 | " WHERE mpostid=%d", itemId ? itemId : parentId); |
| 207 | } |
| 208 | if( itemId ){ |
| 209 | if( db_int(0, "SELECT inreplyto IS NULL FROM forumpost" |
| 210 | " WHERE mpostid=%d", itemId) ){ |
| 211 | db_multi_exec( |
| 212 | "UPDATE forumthread SET mtitle=%Q WHERE mthreadid=%d", |
| 213 | PT("s"), threadId |
| 214 | ); |
| 215 | } |
| 216 | db_multi_exec( |
| 217 | "UPDATE forumpost SET" |
| 218 | " mtime=%!.17g," |
| 219 | " mimetype=%Q," |
| 220 | " ipaddr=%Q," |
| 221 | " mbody=%Q" |
| 222 | " WHERE mpostid=%d", |
| 223 | rNow, PT("mimetype"), P("REMOTE_ADDR"), PT("b"), itemId |
| 224 | ); |
| 225 | }else{ |
| 226 | db_multi_exec( |
| 227 | "INSERT INTO forumpost(mposthash,mthreadid,uname,mtime," |
| 228 | " mstatus,mimetype,ipaddr,inreplyto,mbody) VALUES" |
| 229 | " (lower(hex(randomblob(28))),%d,%Q,%!.17g,%Q,%Q,%Q,nullif(%d,0),%Q)", |
| 230 | threadId,g.zLogin,rNow,NULL,zMime,P("REMOTE_ADDR"),parentId,P("b")); |
| 231 | itemId = db_last_insert_rowid(); |
| 232 | } |
| 233 | if( zSubject==0 ){ |
| 234 | db_multi_exec( |
| 235 | "UPDATE forumthread SET mtime=%!.17g, npost=npost+1" |
| 236 | " WHERE mthreadid=(SELECT mthreadid FROM forumpost WHERE mpostid=%d)", |
| 237 | rNow, itemId |
| 238 | ); |
| 239 | } |
| 240 | return itemId; |
| 241 | } |
| 242 | |
| 243 | /* |
| 244 | ** WEBPAGE: forumedit |
| 245 | ** |
| 246 | ** Query parameters: |
| 247 | ** |
| 248 | ** replyto=N Enter a reply to forum item N |
| 249 | ** item=N Edit item N |
| 250 | ** s=SUBJECT Subject. New thread only. Omitted for replies |
| 251 | ** b=BODY Body of the post |
| 252 | ** m=MIMETYPE Mimetype for the body of the post |
| 253 | ** x Submit changes |
| 254 | ** p Preview changes |
| 255 | */ |
| 256 | void forum_edit_page(void){ |
| 257 | int itemId; |
| 258 | int parentId; |
| 259 | char *zErr = 0; |
| 260 | const char *zMime; |
| 261 | const char *zSub; |
| 262 | |
| 263 | login_check_credentials(); |
| 264 | if( !g.perm.WrForum ){ login_needed(g.anon.WrForum); return; } |
| 265 | forum_verify_schema(); |
| 266 | itemId = atoi(PD("item","0")); |
| 267 | parentId = atoi(PD("replyto","0")); |
| 268 | if( P("cancel")!=0 ){ |
| 269 | cgi_redirectf("%R/forum?item=%d", itemId ? itemId : parentId); |
| 270 | return; |
| 271 | } |
| 272 | if( P("x")!=0 && cgi_csrf_safe(1) ){ |
| 273 | itemId = forum_post(itemId,parentId,&zErr); |
| 274 | if( itemId ){ |
| 275 | cgi_redirectf("%R/forum?item=%d",itemId); |
| 276 | return; |
| 277 | } |
| 278 | } |
| 279 | if( itemId && (P("mimetype")==0 || P("b")==0) ){ |
| 280 | Stmt q; |
| 281 | db_prepare(&q, "SELECT mimetype, mbody FROM forumpost" |
| 282 | " WHERE mpostid=%d", itemId); |
| 283 | if( db_step(&q)==SQLITE_ROW ){ |
| 284 | if( P("mimetype")==0 ){ |
| 285 | cgi_set_query_parameter("mimetype", db_column_text(&q, 0)); |
| 286 | } |
| 287 | if( P("b")==0 ){ |
| 288 | cgi_set_query_parameter("b", db_column_text(&q, 1)); |
| 289 | } |
| 290 | } |
| 291 | db_finalize(&q); |
| 292 | } |
| 293 | zMime = wiki_filter_mimetypes(P("mimetype")); |
| 294 | if( itemId>0 ){ |
| 295 | style_header("Edit Forum Post"); |
| 296 | }else if( parentId>0 ){ |
| 297 | style_header("Comment On Forum Post"); |
| 298 | }else{ |
| 299 | style_header("New Forum Thread"); |
| 300 | } |
| 301 | @ <form action="%R/forumedit" method="POST"> |
| 302 | if( itemId ){ |
| 303 | @ <input type="hidden" name="item" value="%d(itemId)"> |
| 304 | } |
| 305 | if( parentId ){ |
| 306 | @ <input type="hidden" name="replyto" value="%d(parentId)"> |
| 307 | } |
| 308 | if( P("p") ){ |
| 309 | Blob x; |
| 310 | @ <div class="forumpreview"> |
| 311 | if( P("s") ){ |
| 312 | @ <h1>%h(PT("s"))</h1> |
| 313 | } |
| 314 | @ <div class="forumpreviewbody"> |
| 315 | blob_init(&x, PT("b"), -1); |
| 316 | wiki_render_by_mimetype(&x, PT("mimetype")); |
| 317 | blob_reset(&x); |
| 318 | @ </div> |
| 319 | @ </div> |
| 320 | @ <hr> |
| 321 | } |
| 322 | @ <table border="0" class="forumeditform"> |
| 323 | if( zErr ){ |
| 324 | @ <tr><td colspan="2"> |
| 325 | @ <span class='forumFormErr'>%h(zErr)</span> |
| 326 | } |
| 327 | if( (itemId==0 && parentId==0) |
| 328 | || (itemId && db_int(0, "SELECT inreplyto IS NULL FROM forumpost" |
| 329 | " WHERE mpostid=%d", itemId)) |
| 330 | ){ |
| 331 | zSub = PT("s"); |
| 332 | if( zSub==0 && itemId ){ |
| 333 | zSub = db_text("", |
| 334 | "SELECT mtitle FROM forumthread" |
| 335 | " WHERE mthreadid=(SELECT mthreadid FROM forumpost" |
| 336 | " WHERE mpostid=%d)", itemId); |
| 337 | } |
| 338 | @ <tr><td>Subject:</td> |
| 339 | @ <td><input type='text' class='forumFormSubject' name='s' value='%h(zSub)'> |
| 340 | } |
| 341 | @ <tr><td>Markup:</td><td> |
| 342 | mimetype_option_menu(zMime); |
| 343 | @ <tr><td>Comment:</td><td> |
| 344 | @ <textarea name="b" class="wikiedit" cols="80"\ |
| 345 | @ rows="20" wrap="virtual">%h(PD("b",""))</textarea></td> |
| 346 | @ <tr><td></td><td> |
| 347 | @ <input type="submit" name="p" value="Preview"> |
| 348 | if( P("p")!=0 ){ |
| 349 | @ <input type="submit" name="x" value="Submit"> |
| 350 | } |
| 351 | @ <input type="submit" name="cancel" value="Cancel"> |
| 352 | @ </table> |
| 353 | @ </form> |
| 354 | style_footer(); |
| 355 | } |
| 356 | |
| 357 | DDED src/forum.js |
| --- src/forum.c | |
| +++ src/forum.c | |
| @@ -19,337 +19,932 @@ | |
| 19 | */ |
| 20 | #include "config.h" |
| 21 | #include <assert.h> |
| 22 | #include "forum.h" |
| 23 | |
| 24 | #if INTERFACE |
| 25 | /* |
| 26 | ** Each instance of the following object represents a single message - |
| 27 | ** either the initial post, an edit to a post, a reply, or an edit to |
| 28 | ** a reply. |
| 29 | */ |
| 30 | struct ForumEntry { |
| 31 | int fpid; /* rid for this entry */ |
| 32 | int fprev; /* zero if initial entry. non-zero if an edit */ |
| 33 | int firt; /* This entry replies to firt */ |
| 34 | int mfirt; /* Root in-reply-to */ |
| 35 | char *zUuid; /* Artifact hash */ |
| 36 | ForumEntry *pLeaf; /* Most recent edit for this entry */ |
| 37 | ForumEntry *pEdit; /* This entry is an edit of pEditee */ |
| 38 | ForumEntry *pNext; /* Next in chronological order */ |
| 39 | ForumEntry *pPrev; /* Previous in chronological order */ |
| 40 | ForumEntry *pDisplay; /* Next in display order */ |
| 41 | int nIndent; /* Number of levels of indentation for this entry */ |
| 42 | }; |
| 43 | |
| 44 | /* |
| 45 | ** A single instance of the following tracks all entries for a thread. |
| 46 | */ |
| 47 | struct ForumThread { |
| 48 | ForumEntry *pFirst; /* First entry in chronological order */ |
| 49 | ForumEntry *pLast; /* Last entry in chronological order */ |
| 50 | ForumEntry *pDisplay; /* Entries in display order */ |
| 51 | ForumEntry *pTail; /* Last on the display list */ |
| 52 | }; |
| 53 | #endif /* INTERFACE */ |
| 54 | |
| 55 | /* |
| 56 | ** Delete a complete ForumThread and all its entries. |
| 57 | */ |
| 58 | static void forumthread_delete(ForumThread *pThread){ |
| 59 | ForumEntry *pEntry, *pNext; |
| 60 | for(pEntry=pThread->pFirst; pEntry; pEntry = pNext){ |
| 61 | pNext = pEntry->pNext; |
| 62 | fossil_free(pEntry->zUuid); |
| 63 | fossil_free(pEntry); |
| 64 | } |
| 65 | fossil_free(pThread); |
| 66 | } |
| 67 | |
| 68 | #if 0 /* not used */ |
| 69 | /* |
| 70 | ** Search a ForumEntry list forwards looking for the entry with fpid |
| 71 | */ |
| 72 | static ForumEntry *forumentry_forward(ForumEntry *p, int fpid){ |
| 73 | while( p && p->fpid!=fpid ) p = p->pNext; |
| 74 | return p; |
| 75 | } |
| 76 | #endif |
| 77 | |
| 78 | /* |
| 79 | ** Search backwards for a ForumEntry |
| 80 | */ |
| 81 | static ForumEntry *forumentry_backward(ForumEntry *p, int fpid){ |
| 82 | while( p && p->fpid!=fpid ) p = p->pPrev; |
| 83 | return p; |
| 84 | } |
| 85 | |
| 86 | /* |
| 87 | ** Add an entry to the display list |
| 88 | */ |
| 89 | static void forumentry_add_to_display(ForumThread *pThread, ForumEntry *p){ |
| 90 | if( pThread->pDisplay==0 ){ |
| 91 | pThread->pDisplay = p; |
| 92 | }else{ |
| 93 | pThread->pTail->pDisplay = p; |
| 94 | } |
| 95 | pThread->pTail = p; |
| 96 | } |
| 97 | |
| 98 | /* |
| 99 | ** Extend the display list for pThread by adding all entries that |
| 100 | ** reference fpid. The first such entry will be no earlier then |
| 101 | ** entry "p". |
| 102 | */ |
| 103 | static void forumthread_display_order( |
| 104 | ForumThread *pThread, |
| 105 | ForumEntry *p, |
| 106 | int fpid, |
| 107 | int nIndent |
| 108 | ){ |
| 109 | while( p ){ |
| 110 | if( p->fprev==0 && p->mfirt==fpid ){ |
| 111 | p->nIndent = nIndent; |
| 112 | forumentry_add_to_display(pThread, p); |
| 113 | forumthread_display_order(pThread, p->pNext, p->fpid, nIndent+1); |
| 114 | } |
| 115 | p = p->pNext; |
| 116 | } |
| 117 | } |
| 118 | |
| 119 | /* |
| 120 | ** Construct a ForumThread object given the root record id. |
| 121 | */ |
| 122 | static ForumThread *forumthread_create(int froot, int computeHierarchy){ |
| 123 | ForumThread *pThread; |
| 124 | ForumEntry *pEntry; |
| 125 | Stmt q; |
| 126 | pThread = fossil_malloc( sizeof(*pThread) ); |
| 127 | memset(pThread, 0, sizeof(*pThread)); |
| 128 | db_prepare(&q, |
| 129 | "SELECT fpid, firt, fprev, (SELECT uuid FROM blob WHERE rid=fpid)" |
| 130 | " FROM forumpost" |
| 131 | " WHERE froot=%d ORDER BY fmtime", |
| 132 | froot |
| 133 | ); |
| 134 | while( db_step(&q)==SQLITE_ROW ){ |
| 135 | pEntry = fossil_malloc( sizeof(*pEntry) ); |
| 136 | memset(pEntry, 0, sizeof(*pEntry)); |
| 137 | pEntry->fpid = db_column_int(&q, 0); |
| 138 | pEntry->firt = db_column_int(&q, 1); |
| 139 | pEntry->fprev = db_column_int(&q, 2); |
| 140 | pEntry->zUuid = fossil_strdup(db_column_text(&q,3)); |
| 141 | pEntry->mfirt = pEntry->firt; |
| 142 | pEntry->pPrev = pThread->pLast; |
| 143 | pEntry->pNext = 0; |
| 144 | if( pThread->pLast==0 ){ |
| 145 | pThread->pFirst = pEntry; |
| 146 | }else{ |
| 147 | pThread->pLast->pNext = pEntry; |
| 148 | } |
| 149 | pThread->pLast = pEntry; |
| 150 | } |
| 151 | db_finalize(&q); |
| 152 | |
| 153 | /* Establish which entries are the latest edit. After this loop |
| 154 | ** completes, entries that have non-NULL pLeaf should not be |
| 155 | ** displayed. |
| 156 | */ |
| 157 | for(pEntry=pThread->pFirst; pEntry; pEntry=pEntry->pNext){ |
| 158 | if( pEntry->fprev ){ |
| 159 | ForumEntry *pBase, *p; |
| 160 | p = forumentry_backward(pEntry->pPrev, pEntry->fprev); |
| 161 | pEntry->pEdit = p; |
| 162 | while( p ){ |
| 163 | pBase = p; |
| 164 | p->pLeaf = pEntry; |
| 165 | p = pBase->pEdit; |
| 166 | } |
| 167 | for(p=pEntry->pNext; p; p=p->pNext){ |
| 168 | if( p->mfirt==pEntry->fpid ) p->mfirt = pBase->fpid; |
| 169 | } |
| 170 | } |
| 171 | } |
| 172 | |
| 173 | if( computeHierarchy ){ |
| 174 | /* Compute the hierarchical display order */ |
| 175 | pEntry = pThread->pFirst; |
| 176 | pEntry->nIndent = 1; |
| 177 | forumentry_add_to_display(pThread, pEntry); |
| 178 | forumthread_display_order(pThread, pEntry, pEntry->fpid, 2); |
| 179 | } |
| 180 | |
| 181 | /* Return the result */ |
| 182 | return pThread; |
| 183 | } |
| 184 | |
| 185 | /* |
| 186 | ** COMMAND: test-forumthread |
| 187 | ** |
| 188 | ** Usage: %fossil test-forumthread THREADID |
| 189 | ** |
| 190 | ** Display a summary of all messages on a thread. |
| 191 | */ |
| 192 | void forumthread_cmd(void){ |
| 193 | int fpid; |
| 194 | int froot; |
| 195 | const char *zName; |
| 196 | ForumThread *pThread; |
| 197 | ForumEntry *p; |
| 198 | |
| 199 | db_find_and_open_repository(0,0); |
| 200 | verify_all_options(); |
| 201 | if( g.argc!=3 ) usage("THREADID"); |
| 202 | zName = g.argv[2]; |
| 203 | fpid = symbolic_name_to_rid(zName, "f"); |
| 204 | if( fpid<=0 ){ |
| 205 | fossil_fatal("Unknown or ambiguous forum id: \"%s\"", zName); |
| 206 | } |
| 207 | froot = db_int(0, "SELECT froot FROM forumpost WHERE fpid=%d", fpid); |
| 208 | if( froot==0 ){ |
| 209 | fossil_fatal("Not a forum post: \"%s\"", zName); |
| 210 | } |
| 211 | fossil_print("fpid = %d\n", fpid); |
| 212 | fossil_print("froot = %d\n", froot); |
| 213 | pThread = forumthread_create(froot, 1); |
| 214 | fossil_print("Chronological:\n"); |
| 215 | /* 123456789 123456789 123456789 123456789 123456789 */ |
| 216 | fossil_print(" fpid firt fprev mfirt pLeaf\n"); |
| 217 | for(p=pThread->pFirst; p; p=p->pNext){ |
| 218 | fossil_print("%9d %9d %9d %9d %9d\n", |
| 219 | p->fpid, p->firt, p->fprev, p->mfirt, p->pLeaf ? p->pLeaf->fpid : 0); |
| 220 | } |
| 221 | fossil_print("\nDisplay\n"); |
| 222 | for(p=pThread->pDisplay; p; p=p->pDisplay){ |
| 223 | fossil_print("%*s", (p->nIndent-1)*3, ""); |
| 224 | if( p->pLeaf ){ |
| 225 | fossil_print("%d->%d\n", p->fpid, p->pLeaf->fpid); |
| 226 | }else{ |
| 227 | fossil_print("%d\n", p->fpid); |
| 228 | } |
| 229 | } |
| 230 | forumthread_delete(pThread); |
| 231 | } |
| 232 | |
| 233 | /* |
| 234 | ** Render a forum post for display |
| 235 | */ |
| 236 | void forum_render( |
| 237 | const char *zTitle, /* The title. Might be NULL for no title */ |
| 238 | const char *zMimetype, /* Mimetype of the message */ |
| 239 | const char *zContent, /* Content of the message */ |
| 240 | const char *zClass /* Put in a <div> if not NULL */ |
| 241 | ){ |
| 242 | if( zClass ){ |
| 243 | @ <div class='%s(zClass)'> |
| 244 | } |
| 245 | if( zTitle ){ |
| 246 | if( zTitle[0] ){ |
| 247 | @ <h1>%h(zTitle)</h1> |
| 248 | }else{ |
| 249 | @ <h1><i>Deleted</i></h1> |
| 250 | } |
| 251 | } |
| 252 | if( zContent && zContent[0] ){ |
| 253 | Blob x; |
| 254 | blob_init(&x, 0, 0); |
| 255 | blob_append(&x, zContent, -1); |
| 256 | wiki_render_by_mimetype(&x, zMimetype); |
| 257 | blob_reset(&x); |
| 258 | }else{ |
| 259 | @ <i>Deleted</i> |
| 260 | } |
| 261 | if( zClass ){ |
| 262 | @ </div> |
| 263 | } |
| 264 | } |
| 265 | |
| 266 | /* |
| 267 | ** Display all posts in a forum thread in chronological order |
| 268 | */ |
| 269 | static void forum_display_chronological(int froot, int target){ |
| 270 | ForumThread *pThread = forumthread_create(froot, 0); |
| 271 | ForumEntry *p; |
| 272 | for(p=pThread->pFirst; p; p=p->pNext){ |
| 273 | char *zDate; |
| 274 | Manifest *pPost; |
| 275 | |
| 276 | pPost = manifest_get(p->fpid, CFTYPE_FORUM, 0); |
| 277 | if( pPost==0 ) continue; |
| 278 | if( p->fpid==target ){ |
| 279 | @ <div id="forum%d(p->fpid)" class="forumTime forumSel"> |
| 280 | }else if( p->pLeaf!=0 ){ |
| 281 | @ <div id="forum%d(p->fpid)" class="forumTime forumObs"> |
| 282 | }else{ |
| 283 | @ <div id="forum%d(p->fpid)" class="forumTime"> |
| 284 | } |
| 285 | if( pPost->zThreadTitle ){ |
| 286 | @ <h1>%h(pPost->zThreadTitle)</h1> |
| 287 | } |
| 288 | zDate = db_text(0, "SELECT datetime(%.17g)", pPost->rDate); |
| 289 | @ <p>By %h(pPost->zUser) on %h(zDate) (%d(p->fpid)) |
| 290 | fossil_free(zDate); |
| 291 | if( p->pEdit ){ |
| 292 | @ edit of %z(href("%R/forumpost/%S?t",p->pEdit->zUuid))%d(p->fprev)</a> |
| 293 | } |
| 294 | if( p->firt ){ |
| 295 | ForumEntry *pIrt = p->pPrev; |
| 296 | while( pIrt && pIrt->fpid!=p->firt ) pIrt = pIrt->pPrev; |
| 297 | if( pIrt ){ |
| 298 | @ reply to %z(href("%R/forumpost/%S?t",pIrt->zUuid))%d(p->firt)</a> |
| 299 | } |
| 300 | } |
| 301 | if( p->pLeaf ){ |
| 302 | @ updated by %z(href("%R/forumpost/%S?t",p->pLeaf->zUuid))\ |
| 303 | @ %d(p->pLeaf->fpid)</a> |
| 304 | } |
| 305 | if( g.perm.Debug ){ |
| 306 | @ <span class="debug">\ |
| 307 | @ <a href="%R/artifact/%h(p->zUuid)">artifact</a></span> |
| 308 | } |
| 309 | if( p->fpid!=target ){ |
| 310 | @ %z(href("%R/forumpost/%S?t",p->zUuid))[link]</a> |
| 311 | } |
| 312 | forum_render(0, pPost->zMimetype, pPost->zWiki, 0); |
| 313 | if( g.perm.WrForum && p->pLeaf==0 ){ |
| 314 | int sameUser = login_is_individual() |
| 315 | && fossil_strcmp(pPost->zUser, g.zLogin)==0; |
| 316 | int isPrivate = content_is_private(p->fpid); |
| 317 | @ <p><form action="%R/forumedit" method="POST"> |
| 318 | @ <input type="hidden" name="fpid" value="%s(p->zUuid)"> |
| 319 | if( !isPrivate ){ |
| 320 | /* Reply and Edit are only available if the post has already |
| 321 | ** been approved */ |
| 322 | @ <input type="submit" name="reply" value="Reply"> |
| 323 | if( g.perm.Admin || sameUser ){ |
| 324 | @ <input type="submit" name="edit" value="Edit"> |
| 325 | @ <input type="submit" name="nullout" value="Delete"> |
| 326 | } |
| 327 | }else if( g.perm.ModForum ){ |
| 328 | /* Provide moderators with moderation buttons for posts that |
| 329 | ** are pending moderation */ |
| 330 | @ <input type="submit" name="approve" value="Approve"> |
| 331 | @ <input type="submit" name="reject" value="Reject"> |
| 332 | }else if( sameUser ){ |
| 333 | /* A post that is pending moderation can be deleted by the |
| 334 | ** person who originally submitted the post */ |
| 335 | @ <input type="submit" name="reject" value="Delete"> |
| 336 | } |
| 337 | @ </form></p> |
| 338 | } |
| 339 | manifest_destroy(pPost); |
| 340 | @ </div> |
| 341 | } |
| 342 | forumthread_delete(pThread); |
| 343 | } |
| 344 | |
| 345 | /* |
| 346 | ** Display all messages in a forumthread with indentation. |
| 347 | */ |
| 348 | static int forum_display_hierarchical(int froot, int target){ |
| 349 | ForumThread *pThread; |
| 350 | ForumEntry *p; |
| 351 | Manifest *pPost, *pOPost; |
| 352 | int fpid; |
| 353 | const char *zUuid; |
| 354 | char *zDate; |
| 355 | const char *zSel; |
| 356 | |
| 357 | pThread = forumthread_create(froot, 1); |
| 358 | for(p=pThread->pFirst; p; p=p->pNext){ |
| 359 | if( p->fpid==target ){ |
| 360 | while( p->pEdit ) p = p->pEdit; |
| 361 | target = p->fpid; |
| 362 | break; |
| 363 | } |
| 364 | } |
| 365 | for(p=pThread->pDisplay; p; p=p->pDisplay){ |
| 366 | pOPost = manifest_get(p->fpid, CFTYPE_FORUM, 0); |
| 367 | if( p->pLeaf ){ |
| 368 | fpid = p->pLeaf->fpid; |
| 369 | zUuid = p->pLeaf->zUuid; |
| 370 | pPost = manifest_get(fpid, CFTYPE_FORUM, 0); |
| 371 | }else{ |
| 372 | fpid = p->fpid; |
| 373 | zUuid = p->zUuid; |
| 374 | pPost = pOPost; |
| 375 | } |
| 376 | zSel = p->fpid==target ? " forumSel" : ""; |
| 377 | if( p->nIndent==1 ){ |
| 378 | @ <div id='forum%d(fpid)' class='forumHierRoot%s(zSel)'> |
| 379 | }else{ |
| 380 | @ <div id='forum%d(fpid)' class='forumHier%s(zSel)' \ |
| 381 | @ style='margin-left: %d((p->nIndent-1)*3)ex;'> |
| 382 | } |
| 383 | pPost = manifest_get(fpid, CFTYPE_FORUM, 0); |
| 384 | if( pPost==0 ) continue; |
| 385 | if( pPost->zThreadTitle ){ |
| 386 | @ <h1>%h(pPost->zThreadTitle)</h1> |
| 387 | } |
| 388 | zDate = db_text(0, "SELECT datetime(%.17g)", pOPost->rDate); |
| 389 | @ <p>By %h(pOPost->zUser) on %h(zDate) |
| 390 | fossil_free(zDate); |
| 391 | if( g.perm.Debug ){ |
| 392 | @ <span class="debug">\ |
| 393 | @ <a href="%R/artifact/%h(p->zUuid)">(%d(p->fpid))</a></span> |
| 394 | } |
| 395 | if( p->pLeaf ){ |
| 396 | zDate = db_text(0, "SELECT datetime(%.17g)", pPost->rDate); |
| 397 | if( fossil_strcmp(pOPost->zUser,pPost->zUser)==0 ){ |
| 398 | @ and edited on %h(zDate) |
| 399 | }else{ |
| 400 | @ as edited by %h(pPost->zUser) on %h(zDate) |
| 401 | } |
| 402 | fossil_free(zDate); |
| 403 | if( g.perm.Debug ){ |
| 404 | @ <span class="debug">\ |
| 405 | @ <a href="%R/artifact/%h(p->pLeaf->zUuid)">(%d(fpid))</a></span> |
| 406 | } |
| 407 | manifest_destroy(pOPost); |
| 408 | } |
| 409 | if( fpid!=target ){ |
| 410 | @ %z(href("%R/forumpost/%S",zUuid))[link]</a> |
| 411 | } |
| 412 | forum_render(0, pPost->zMimetype, pPost->zWiki, 0); |
| 413 | if( g.perm.WrForum ){ |
| 414 | int sameUser = login_is_individual() |
| 415 | && fossil_strcmp(pPost->zUser, g.zLogin)==0; |
| 416 | int isPrivate = content_is_private(fpid); |
| 417 | @ <p><form action="%R/forumedit" method="POST"> |
| 418 | @ <input type="hidden" name="fpid" value="%s(zUuid)"> |
| 419 | if( !isPrivate ){ |
| 420 | /* Reply and Edit are only available if the post has already |
| 421 | ** been approved */ |
| 422 | @ <input type="submit" name="reply" value="Reply"> |
| 423 | if( g.perm.Admin || sameUser ){ |
| 424 | @ <input type="submit" name="edit" value="Edit"> |
| 425 | @ <input type="submit" name="nullout" value="Delete"> |
| 426 | } |
| 427 | }else if( g.perm.ModForum ){ |
| 428 | /* Provide moderators with moderation buttons for posts that |
| 429 | ** are pending moderation */ |
| 430 | @ <input type="submit" name="approve" value="Approve"> |
| 431 | @ <input type="submit" name="reject" value="Reject"> |
| 432 | }else if( sameUser ){ |
| 433 | /* A post that is pending moderation can be deleted by the |
| 434 | ** person who originally submitted the post */ |
| 435 | @ <input type="submit" name="reject" value="Delete"> |
| 436 | } |
| 437 | @ </form></p> |
| 438 | } |
| 439 | manifest_destroy(pPost); |
| 440 | @ </div> |
| 441 | } |
| 442 | forumthread_delete(pThread); |
| 443 | return target; |
| 444 | } |
| 445 | |
| 446 | /* |
| 447 | ** WEBPAGE: forumpost |
| 448 | ** |
| 449 | ** Show a single forum posting. The posting is shown in context with |
| 450 | ** it's entire thread. The selected posting is enclosed within |
| 451 | ** <div class='forumSel'>...</div>. Javascript is used to move the |
| 452 | ** selected posting into view after the page loads. |
| 453 | ** |
| 454 | ** Query parameters: |
| 455 | ** |
| 456 | ** name=X REQUIRED. The hash of the post to display |
| 457 | ** t Show a chronologic listing instead of hierarchical |
| 458 | */ |
| 459 | void forumpost_page(void){ |
| 460 | forumthread_page(); |
| 461 | } |
| 462 | |
| 463 | /* |
| 464 | ** WEBPAGE: forumthread |
| 465 | ** |
| 466 | ** Show all forum messages associated with a particular message thread. |
| 467 | ** The result is basically the same as /forumpost except that none of |
| 468 | ** the postings in the thread are selected. |
| 469 | ** |
| 470 | ** Query parameters: |
| 471 | ** |
| 472 | ** name=X REQUIRED. The hash of any post of the thread. |
| 473 | ** t Show a chronologic listing instead of hierarchical |
| 474 | */ |
| 475 | void forumthread_page(void){ |
| 476 | int fpid; |
| 477 | int froot; |
| 478 | const char *zName = P("name"); |
| 479 | login_check_credentials(); |
| 480 | if( !g.perm.RdForum ){ |
| 481 | login_needed(g.anon.RdForum); |
| 482 | return; |
| 483 | } |
| 484 | if( zName==0 ){ |
| 485 | webpage_error("Missing \"name=\" query parameter"); |
| 486 | } |
| 487 | fpid = symbolic_name_to_rid(zName, "f"); |
| 488 | if( fpid<=0 ){ |
| 489 | webpage_error("Unknown or ambiguous forum id: \"%s\"", zName); |
| 490 | } |
| 491 | style_header("Forum"); |
| 492 | froot = db_int(0, "SELECT froot FROM forumpost WHERE fpid=%d", fpid); |
| 493 | if( froot==0 ){ |
| 494 | webpage_error("Not a forum post: \"%s\"", zName); |
| 495 | } |
| 496 | if( fossil_strcmp(g.zPath,"forumthread")==0 ) fpid = 0; |
| 497 | if( P("t") ){ |
| 498 | if( g.perm.Debug ){ |
| 499 | style_submenu_element("Hierarchical", "%R/%s/%s", g.zPath, zName); |
| 500 | } |
| 501 | forum_display_chronological(froot, fpid); |
| 502 | }else{ |
| 503 | if( g.perm.Debug ){ |
| 504 | style_submenu_element("Chronological", "%R/%s/%s?t", g.zPath, zName); |
| 505 | } |
| 506 | forum_display_hierarchical(froot, fpid); |
| 507 | } |
| 508 | style_load_js("forum.js"); |
| 509 | style_footer(); |
| 510 | } |
| 511 | |
| 512 | /* |
| 513 | ** Return true if a forum post should be moderated. |
| 514 | */ |
| 515 | static int forum_need_moderation(void){ |
| 516 | if( P("domod") ) return 1; |
| 517 | if( g.perm.WrTForum ) return 0; |
| 518 | if( g.perm.ModForum ) return 0; |
| 519 | return 1; |
| 520 | } |
| 521 | |
| 522 | /* |
| 523 | ** Add a new Forum Post artifact to the repository. |
| 524 | ** |
| 525 | ** Return true if a redirect occurs. |
| 526 | */ |
| 527 | static int forum_post( |
| 528 | const char *zTitle, /* Title. NULL for replies */ |
| 529 | int iInReplyTo, /* Post replying to. 0 for new threads */ |
| 530 | int iEdit, /* Post being edited, or zero for a new post */ |
| 531 | const char *zUser, /* Username. NULL means use login name */ |
| 532 | const char *zMimetype, /* Mimetype of content. */ |
| 533 | const char *zContent /* Content */ |
| 534 | ){ |
| 535 | char *zDate; |
| 536 | char *zI; |
| 537 | char *zG; |
| 538 | int iBasis; |
| 539 | Blob x, cksum, formatCheck, errMsg; |
| 540 | Manifest *pPost; |
| 541 | |
| 542 | schema_forum(); |
| 543 | if( iInReplyTo==0 && iEdit>0 ){ |
| 544 | iBasis = iEdit; |
| 545 | iInReplyTo = db_int(0, "SELECT firt FROM forumpost WHERE fpid=%d", iEdit); |
| 546 | }else{ |
| 547 | iBasis = iInReplyTo; |
| 548 | } |
| 549 | webpage_assert( (zTitle==0)+(iInReplyTo==0)==1 ); |
| 550 | blob_init(&x, 0, 0); |
| 551 | zDate = date_in_standard_format("now"); |
| 552 | blob_appendf(&x, "D %s\n", zDate); |
| 553 | fossil_free(zDate); |
| 554 | zG = db_text(0, |
| 555 | "SELECT uuid FROM blob, forumpost" |
| 556 | " WHERE blob.rid==forumpost.froot" |
| 557 | " AND forumpost.fpid=%d", iBasis); |
| 558 | if( zG ){ |
| 559 | blob_appendf(&x, "G %s\n", zG); |
| 560 | fossil_free(zG); |
| 561 | } |
| 562 | if( zTitle ){ |
| 563 | blob_appendf(&x, "H %F\n", zTitle); |
| 564 | } |
| 565 | zI = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", iInReplyTo); |
| 566 | if( zI ){ |
| 567 | blob_appendf(&x, "I %s\n", zI); |
| 568 | fossil_free(zI); |
| 569 | } |
| 570 | if( fossil_strcmp(zMimetype,"text/x-fossil-wiki")!=0 ){ |
| 571 | blob_appendf(&x, "N %s\n", zMimetype); |
| 572 | } |
| 573 | if( iEdit>0 ){ |
| 574 | char *zP = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", iEdit); |
| 575 | if( zP==0 ) webpage_error("missing edit artifact %d", iEdit); |
| 576 | blob_appendf(&x, "P %s\n", zP); |
| 577 | fossil_free(zP); |
| 578 | } |
| 579 | if( zUser==0 ){ |
| 580 | if( login_is_nobody() ){ |
| 581 | zUser = "anonymous"; |
| 582 | }else{ |
| 583 | zUser = login_name(); |
| 584 | } |
| 585 | } |
| 586 | blob_appendf(&x, "U %F\n", zUser); |
| 587 | blob_appendf(&x, "W %d\n%s\n", strlen(zContent), zContent); |
| 588 | md5sum_blob(&x, &cksum); |
| 589 | blob_appendf(&x, "Z %b\n", &cksum); |
| 590 | blob_reset(&cksum); |
| 591 | |
| 592 | /* Verify that the artifact we are creating is well-formed */ |
| 593 | blob_init(&formatCheck, 0, 0); |
| 594 | blob_init(&errMsg, 0, 0); |
| 595 | blob_copy(&formatCheck, &x); |
| 596 | pPost = manifest_parse(&formatCheck, 0, &errMsg); |
| 597 | if( pPost==0 ){ |
| 598 | webpage_error("malformed forum post artifact - %s", blob_str(&errMsg)); |
| 599 | } |
| 600 | webpage_assert( pPost->type==CFTYPE_FORUM ); |
| 601 | manifest_destroy(pPost); |
| 602 | |
| 603 | if( P("dryrun") ){ |
| 604 | @ <div class='debug'> |
| 605 | @ This is the artifact that would have been generated: |
| 606 | @ <pre>%h(blob_str(&x))</pre> |
| 607 | @ </div> |
| 608 | blob_reset(&x); |
| 609 | return 0; |
| 610 | }else{ |
| 611 | int nrid = wiki_put(&x, 0, forum_need_moderation()); |
| 612 | cgi_redirectf("%R/forumpost/%S", rid_to_uuid(nrid)); |
| 613 | return 1; |
| 614 | } |
| 615 | } |
| 616 | |
| 617 | /* |
| 618 | ** Paint the form elements for entering a Forum post |
| 619 | */ |
| 620 | static void forum_entry_widget( |
| 621 | const char *zTitle, |
| 622 | const char *zMimetype, |
| 623 | const char *zContent |
| 624 | ){ |
| 625 | if( zTitle ){ |
| 626 | @ Title: <input type="input" name="title" value="%h(zTitle)" size="50"><br> |
| 627 | } |
| 628 | @ Markup style: |
| 629 | mimetype_option_menu(zMimetype); |
| 630 | @ <br><textarea name="content" class="wikiedit" cols="80" \ |
| 631 | @ rows="25" wrap="virtual">%h(zContent)</textarea><br> |
| 632 | } |
| 633 | |
| 634 | /* |
| 635 | ** WEBPAGE: forumnew |
| 636 | ** WEBPAGE: forumedit |
| 637 | ** |
| 638 | ** Start a new thread on the forum or reply to an existing thread. |
| 639 | ** But first prompt to see if the user would like to log in. |
| 640 | */ |
| 641 | void forum_page_init(void){ |
| 642 | int isEdit; |
| 643 | char *zGoto; |
| 644 | login_check_credentials(); |
| 645 | if( !g.perm.WrForum ){ |
| 646 | login_needed(g.anon.WrForum); |
| 647 | return; |
| 648 | } |
| 649 | if( sqlite3_strglob("*edit*", g.zPath)==0 ){ |
| 650 | zGoto = mprintf("%R/forume2?fpid=%S",PD("fpid","")); |
| 651 | isEdit = 1; |
| 652 | }else{ |
| 653 | zGoto = mprintf("%R/forume1"); |
| 654 | isEdit = 0; |
| 655 | } |
| 656 | if( login_is_individual() ){ |
| 657 | if( isEdit ){ |
| 658 | forumedit_page(); |
| 659 | }else{ |
| 660 | forumnew_page(); |
| 661 | } |
| 662 | return; |
| 663 | } |
| 664 | style_header("%h As Anonymous?", isEdit ? "Reply" : "Post"); |
| 665 | @ <p>You are not logged in. |
| 666 | @ <p><table border="0" cellpadding="10"> |
| 667 | @ <tr><td> |
| 668 | @ <form action="%s(zGoto)" method="POST"> |
| 669 | @ <input type="submit" value="Remain Anonymous"> |
| 670 | @ </form> |
| 671 | @ <td>Post to the forum anonymously |
| 672 | if( login_self_register_available(0) ){ |
| 673 | @ <tr><td> |
| 674 | @ <form action="%R/register" method="POST"> |
| 675 | @ <input type="hidden" name="g" value="%s(zGoto)"> |
| 676 | @ <input type="submit" value="Create An Account"> |
| 677 | @ </form> |
| 678 | @ <td>Create a new account and post using that new account |
| 679 | } |
| 680 | @ <tr><td> |
| 681 | @ <form action="%R/login" method="POST"> |
| 682 | @ <input type="hidden" name="g" value="%s(zGoto)"> |
| 683 | @ <input type="hidden" name="noanon" value="1"> |
| 684 | @ <input type="submit" value="Login"> |
| 685 | @ </form> |
| 686 | @ <td>Log into an existing account |
| 687 | @ </table> |
| 688 | style_footer(); |
| 689 | fossil_free(zGoto); |
| 690 | } |
| 691 | |
| 692 | /* |
| 693 | ** Write the "From: USER" line on the webpage. |
| 694 | */ |
| 695 | static void forum_from_line(void){ |
| 696 | if( login_is_nobody() ){ |
| 697 | @ From: anonymous<br> |
| 698 | }else{ |
| 699 | @ From: %h(login_name())<br> |
| 700 | } |
| 701 | } |
| 702 | |
| 703 | /* |
| 704 | ** WEBPAGE: forume1 |
| 705 | ** |
| 706 | ** Start a new forum thread. |
| 707 | */ |
| 708 | void forumnew_page(void){ |
| 709 | const char *zTitle = PDT("title",""); |
| 710 | const char *zMimetype = PD("mimetype","text/x-fossil-wiki"); |
| 711 | const char *zContent = PDT("content",""); |
| 712 | login_check_credentials(); |
| 713 | if( !g.perm.WrForum ){ |
| 714 | login_needed(g.anon.WrForum); |
| 715 | return; |
| 716 | } |
| 717 | if( P("submit") ){ |
| 718 | if( forum_post(zTitle, 0, 0, 0, zMimetype, zContent) ) return; |
| 719 | } |
| 720 | if( P("preview") ){ |
| 721 | @ <h1>Preview:</h1> |
| 722 | forum_render(zTitle, zMimetype, zContent, "forumEdit"); |
| 723 | } |
| 724 | style_header("New Forum Thread"); |
| 725 | @ <form action="%R/forume1" method="POST"> |
| 726 | @ <h1>New Message:</h1> |
| 727 | forum_from_line(); |
| 728 | forum_entry_widget(zTitle, zMimetype, zContent); |
| 729 | @ <input type="submit" name="preview" value="Preview"> |
| 730 | if( P("preview") ){ |
| 731 | @ <input type="submit" name="submit" value="Submit"> |
| 732 | }else{ |
| 733 | @ <input type="submit" name="submit" value="Submit" disabled> |
| 734 | } |
| 735 | if( g.perm.Debug ){ |
| 736 | /* For the test-forumnew page add these extra debugging controls */ |
| 737 | @ <div class="debug"> |
| 738 | @ <label><input type="checkbox" name="dryrun" %s(PCK("dryrun"))> \ |
| 739 | @ Dry run</label> |
| 740 | @ <br><label><input type="checkbox" name="domod" %s(PCK("domod"))> \ |
| 741 | @ Require moderator approval</label> |
| 742 | @ <br><label><input type="checkbox" name="showqp" %s(PCK("showqp"))> \ |
| 743 | @ Show query parameters</label> |
| 744 | @ </div> |
| 745 | } |
| 746 | @ </form> |
| 747 | style_footer(); |
| 748 | } |
| 749 | |
| 750 | /* |
| 751 | ** WEBPAGE: forume2 |
| 752 | ** |
| 753 | ** Edit an existing forum message. |
| 754 | ** Query parameters: |
| 755 | ** |
| 756 | ** fpid=X Hash of the post to be editted. REQUIRED |
| 757 | */ |
| 758 | void forumedit_page(void){ |
| 759 | int fpid; |
| 760 | Manifest *pPost; |
| 761 | const char *zMimetype = 0; |
| 762 | const char *zContent = 0; |
| 763 | const char *zTitle = 0; |
| 764 | int isCsrfSafe; |
| 765 | int isDelete = 0; |
| 766 | |
| 767 | login_check_credentials(); |
| 768 | if( !g.perm.WrForum ){ |
| 769 | login_needed(g.anon.WrForum); |
| 770 | return; |
| 771 | } |
| 772 | fpid = symbolic_name_to_rid(PD("fpid",""), "f"); |
| 773 | if( fpid<=0 || (pPost = manifest_get(fpid, CFTYPE_FORUM, 0))==0 ){ |
| 774 | webpage_error("Missing or invalid fpid query parameter"); |
| 775 | } |
| 776 | if( P("cancel") ){ |
| 777 | cgi_redirectf("%R/forumpost/%S",P("fpid")); |
| 778 | return; |
| 779 | } |
| 780 | isCsrfSafe = cgi_csrf_safe(1); |
| 781 | if( g.perm.ModForum && isCsrfSafe ){ |
| 782 | if( P("approve") ){ |
| 783 | moderation_approve(fpid); |
| 784 | cgi_redirectf("%R/forumpost/%S",P("fpid")); |
| 785 | return; |
| 786 | } |
| 787 | if( P("reject") ){ |
| 788 | char *zParent = |
| 789 | db_text(0, |
| 790 | "SELECT uuid FROM forumpost, blob" |
| 791 | " WHERE forumpost.fpid=%d AND blob.rid=forumpost.firt", |
| 792 | fpid |
| 793 | ); |
| 794 | moderation_disapprove(fpid); |
| 795 | if( zParent ){ |
| 796 | cgi_redirectf("%R/forumpost/%S",zParent); |
| 797 | }else{ |
| 798 | cgi_redirectf("%R/forum"); |
| 799 | } |
| 800 | return; |
| 801 | } |
| 802 | } |
| 803 | isDelete = P("nullout")!=0; |
| 804 | if( P("submit") && isCsrfSafe ){ |
| 805 | int done = 1; |
| 806 | const char *zMimetype = PD("mimetype","text/x-fossil-wiki"); |
| 807 | const char *zContent = PDT("content",""); |
| 808 | if( P("reply") ){ |
| 809 | done = forum_post(0, fpid, 0, 0, zMimetype, zContent); |
| 810 | }else if( P("edit") || isDelete ){ |
| 811 | done = forum_post(P("title"), 0, fpid, 0, zMimetype, zContent); |
| 812 | }else{ |
| 813 | webpage_error("Missing 'reply' query parameter"); |
| 814 | } |
| 815 | if( done ) return; |
| 816 | } |
| 817 | if( isDelete ){ |
| 818 | zMimetype = "text/x-fossil-wiki"; |
| 819 | zContent = ""; |
| 820 | if( pPost->zThreadTitle ) zTitle = ""; |
| 821 | style_header("Delete %s", zTitle ? "Post" : "Reply"); |
| 822 | @ <h1>Original Post:</h1> |
| 823 | forum_render(pPost->zThreadTitle, pPost->zMimetype, pPost->zWiki, |
| 824 | "forumEdit"); |
| 825 | @ <h1>Change Into:</h1> |
| 826 | forum_render(zTitle, zMimetype, zContent,"forumEdit"); |
| 827 | @ <form action="%R/forume2" method="POST"> |
| 828 | @ <input type="hidden" name="fpid" value="%h(P("fpid"))"> |
| 829 | @ <input type="hidden" name="nullout" value="1"> |
| 830 | @ <input type="hidden" name="mimetype" value="%h(zMimetype)"> |
| 831 | @ <input type="hidden" name="content" value="%h(zContent)"> |
| 832 | if( zTitle ){ |
| 833 | @ <input type="hidden" name="title" value="%h(zTitle)"> |
| 834 | } |
| 835 | }else if( P("edit") ){ |
| 836 | /* Provide an edit to the fpid post */ |
| 837 | zMimetype = P("mimetype"); |
| 838 | zContent = PT("content"); |
| 839 | zTitle = P("title"); |
| 840 | if( zContent==0 ) zContent = fossil_strdup(pPost->zWiki); |
| 841 | if( zMimetype==0 ) zMimetype = fossil_strdup(pPost->zMimetype); |
| 842 | if( zTitle==0 && pPost->zThreadTitle!=0 ){ |
| 843 | zTitle = fossil_strdup(pPost->zThreadTitle); |
| 844 | } |
| 845 | style_header("Edit %s", zTitle ? "Post" : "Reply"); |
| 846 | @ <h1>Original Post:</h1> |
| 847 | forum_render(pPost->zThreadTitle, pPost->zMimetype, pPost->zWiki, |
| 848 | "forumEdit"); |
| 849 | if( P("preview") ){ |
| 850 | @ <h1>Preview Of Editted Post:</h1> |
| 851 | forum_render(zTitle, zMimetype, zContent,"forumEdit"); |
| 852 | } |
| 853 | @ <h1>Revised Message:</h1> |
| 854 | @ <form action="%R/forume2" method="POST"> |
| 855 | @ <input type="hidden" name="fpid" value="%h(P("fpid"))"> |
| 856 | @ <input type="hidden" name="edit" value="1"> |
| 857 | forum_from_line(); |
| 858 | forum_entry_widget(zTitle, zMimetype, zContent); |
| 859 | }else{ |
| 860 | /* Reply */ |
| 861 | zMimetype = PD("mimetype","text/x-fossil-wiki"); |
| 862 | zContent = PDT("content",""); |
| 863 | style_header("Reply"); |
| 864 | @ <h1>Replying To:</h1> |
| 865 | forum_render(0, pPost->zMimetype, pPost->zWiki, "forumEdit"); |
| 866 | if( P("preview") ){ |
| 867 | @ <h1>Preview:</h1> |
| 868 | forum_render(0, zMimetype,zContent, "forumEdit"); |
| 869 | } |
| 870 | @ <h1>Enter Reply:</h1> |
| 871 | @ <form action="%R/forume2" method="POST"> |
| 872 | @ <input type="hidden" name="fpid" value="%h(P("fpid"))"> |
| 873 | @ <input type="hidden" name="reply" value="1"> |
| 874 | forum_from_line(); |
| 875 | forum_entry_widget(0, zMimetype, zContent); |
| 876 | } |
| 877 | if( !isDelete ){ |
| 878 | @ <input type="submit" name="preview" value="Preview"> |
| 879 | } |
| 880 | @ <input type="submit" name="cancel" value="Cancel"> |
| 881 | if( P("preview") || isDelete ){ |
| 882 | @ <input type="submit" name="submit" value="Submit"> |
| 883 | } |
| 884 | if( g.perm.Debug ){ |
| 885 | /* For the test-forumnew page add these extra debugging controls */ |
| 886 | @ <div class="debug"> |
| 887 | @ <label><input type="checkbox" name="dryrun" %s(PCK("dryrun"))> \ |
| 888 | @ Dry run</label> |
| 889 | @ <br><label><input type="checkbox" name="domod" %s(PCK("domod"))> \ |
| 890 | @ Require moderator approval</label> |
| 891 | @ <br><label><input type="checkbox" name="showqp" %s(PCK("showqp"))> \ |
| 892 | @ Show query parameters</label> |
| 893 | @ </div> |
| 894 | } |
| 895 | @ </form> |
| 896 | style_footer(); |
| 897 | } |
| 898 | |
| 899 | /* |
| 900 | ** WEBPAGE: forum |
| 901 | ** |
| 902 | ** The main page for the forum feature. Show a list of recent forum |
| 903 | ** threads. Also show a search box at the top if search is enabled, |
| 904 | ** and a button for creating a new thread, if enabled. |
| 905 | */ |
| 906 | void forum_main_page(void){ |
| 907 | Stmt q; |
| 908 | int iLimit, iOfst; |
| 909 | login_check_credentials(); |
| 910 | if( !g.perm.RdForum ){ |
| 911 | login_needed(g.anon.RdForum); |
| 912 | return; |
| 913 | } |
| 914 | style_header("Forum"); |
| 915 | if( g.perm.WrForum ){ |
| 916 | style_submenu_element("New Message","%R/forumnew"); |
| 917 | } |
| 918 | if( g.perm.ModForum && moderation_needed() ){ |
| 919 | style_submenu_element("Moderation Requests", "%R/modreq"); |
| 920 | } |
| 921 | if( search_screen(SRCH_FORUM, 0) ){ |
| 922 | style_submenu_element("Recent Threads","%R/forum"); |
| 923 | style_footer(); |
| 924 | return; |
| 925 | } |
| 926 | iLimit = 50; |
| 927 | iOfst = 0; |
| 928 | @ <h1>Recent Threads</h1> |
| 929 | @ <div class='fileage'><table width="100%%"> |
| 930 | db_prepare(&q, |
| 931 | "SELECT julianday('now') - max(fmtime)," |
| 932 | " (SELECT uuid FROM blob WHERE rid=fpid)," |
| 933 | " (SELECT substr(comment,instr(comment,':')+2)" |
| 934 | " FROM event WHERE objid=fpid)" |
| 935 | " FROM forumpost" |
| 936 | " GROUP BY froot ORDER BY 1 LIMIT %d OFFSET %d", |
| 937 | iLimit, iOfst |
| 938 | ); |
| 939 | while( db_step(&q)==SQLITE_ROW ){ |
| 940 | char *zAge = human_readable_age(db_column_double(&q,0)); |
| 941 | const char *zUuid = db_column_text(&q, 1); |
| 942 | const char *zTitle = db_column_text(&q, 2); |
| 943 | @ <tr><td>%h(zAge) ago</td> |
| 944 | @ <td>%z(href("%R/forumpost/%S",zUuid))%h(zTitle)</a> |
| 945 | @ </tr> |
| 946 | fossil_free(zAge); |
| 947 | } |
| 948 | @ </table></div> |
| 949 | style_footer(); |
| 950 | } |
| 951 | |
| 952 | DDED src/forum.js |
+17
| --- a/src/forum.js | ||
| +++ b/src/forum.js | ||
| @@ -0,0 +1,17 @@ | ||
| 1 | +(function(){ | |
| 2 | + function absoluteY(obj){ | |
| 3 | + var top = 0; | |
| 4 | + if( obj.offsetParent ){ | |
| 5 | + do{ | |
| 6 | + top += obj.offsetTop; | |
| 7 | + }while( obj = obj.offsetParent ); | |
| 8 | + } | |
| 9 | + return top; | |
| 10 | + } | |
| 11 | + var x = document.getElementsByClassName('forumSel'); | |
| 12 | + if(x[0]){ | |
| 13 | + var w = window.innerHeight; | |
| 14 | + var h = x[0].scrollHeight; | |
| 15 | + var y = absoluteY(x[0]); | |
| 16 | + if( w>h ) y = y + (h-w)/2; | |
| 17 | + if( y>0 ) w()) |
| --- a/src/forum.js | |
| +++ b/src/forum.js | |
| @@ -0,0 +1,17 @@ | |
| --- a/src/forum.js | |
| +++ b/src/forum.js | |
| @@ -0,0 +1,17 @@ | |
| 1 | (function(){ |
| 2 | function absoluteY(obj){ |
| 3 | var top = 0; |
| 4 | if( obj.offsetParent ){ |
| 5 | do{ |
| 6 | top += obj.offsetTop; |
| 7 | }while( obj = obj.offsetParent ); |
| 8 | } |
| 9 | return top; |
| 10 | } |
| 11 | var x = document.getElementsByClassName('forumSel'); |
| 12 | if(x[0]){ |
| 13 | var w = window.innerHeight; |
| 14 | var h = x[0].scrollHeight; |
| 15 | var y = absoluteY(x[0]); |
| 16 | if( w>h ) y = y + (h-w)/2; |
| 17 | if( y>0 ) w()) |
+11
-23
| --- src/info.c | ||
| +++ src/info.c | ||
| @@ -935,14 +935,11 @@ | ||
| 935 | 935 | @ <tr><th>Artifact ID:</th> |
| 936 | 936 | @ <td>%z(href("%R/artifact/%!S",zUuid))%s(zUuid)</a> |
| 937 | 937 | if( g.perm.Setup ){ |
| 938 | 938 | @ (%d(rid)) |
| 939 | 939 | } |
| 940 | - modPending = moderation_pending(rid); | |
| 941 | - if( modPending ){ | |
| 942 | - @ <span class="modpending">*** Awaiting Moderator Approval ***</span> | |
| 943 | - } | |
| 940 | + modPending = moderation_pending_www(rid); | |
| 944 | 941 | @ </td></tr> |
| 945 | 942 | @ <tr><th>Page Name:</th><td>%h(pWiki->zWikiTitle)</td></tr> |
| 946 | 943 | @ <tr><th>Date:</th><td> |
| 947 | 944 | hyperlink_to_date(zDate, "</td></tr>"); |
| 948 | 945 | @ <tr><th>Original User:</th><td> |
| @@ -978,25 +975,10 @@ | ||
| 978 | 975 | @ <div class="section">Content</div> |
| 979 | 976 | blob_init(&wiki, pWiki->zWiki, -1); |
| 980 | 977 | wiki_render_by_mimetype(&wiki, pWiki->zMimetype); |
| 981 | 978 | blob_reset(&wiki); |
| 982 | 979 | manifest_destroy(pWiki); |
| 983 | - style_footer(); | |
| 984 | -} | |
| 985 | - | |
| 986 | -/* | |
| 987 | -** Show a webpage error message | |
| 988 | -*/ | |
| 989 | -void webpage_error(const char *zFormat, ...){ | |
| 990 | - va_list ap; | |
| 991 | - const char *z; | |
| 992 | - va_start(ap, zFormat); | |
| 993 | - z = vmprintf(zFormat, ap); | |
| 994 | - va_end(ap); | |
| 995 | - style_header("URL Error"); | |
| 996 | - @ <h1>Error</h1> | |
| 997 | - @ <p>%h(z)</p> | |
| 998 | 980 | style_footer(); |
| 999 | 981 | } |
| 1000 | 982 | |
| 1001 | 983 | /* |
| 1002 | 984 | ** Find an check-in based on query parameter zParam and parse its |
| @@ -1246,10 +1228,11 @@ | ||
| 1246 | 1228 | #define OBJTYPE_ATTACHMENT 0x0010 |
| 1247 | 1229 | #define OBJTYPE_EVENT 0x0020 |
| 1248 | 1230 | #define OBJTYPE_TAG 0x0040 |
| 1249 | 1231 | #define OBJTYPE_SYMLINK 0x0080 |
| 1250 | 1232 | #define OBJTYPE_EXE 0x0100 |
| 1233 | +#define OBJTYPE_FORUM 0x0200 | |
| 1251 | 1234 | |
| 1252 | 1235 | /* |
| 1253 | 1236 | ** Possible flags for the second parameter to |
| 1254 | 1237 | ** object_description() |
| 1255 | 1238 | */ |
| @@ -1438,10 +1421,13 @@ | ||
| 1438 | 1421 | objType |= OBJTYPE_EVENT; |
| 1439 | 1422 | hyperlink_to_event_tagid(db_column_int(&q, 5)); |
| 1440 | 1423 | }else{ |
| 1441 | 1424 | @ Attachment to technote |
| 1442 | 1425 | } |
| 1426 | + }else if( zType[0]=='f' ){ | |
| 1427 | + objType |= OBJTYPE_FORUM; | |
| 1428 | + @ Forum post | |
| 1443 | 1429 | }else{ |
| 1444 | 1430 | @ Tag referencing |
| 1445 | 1431 | } |
| 1446 | 1432 | if( zType[0]!='e' || eventTagId == 0){ |
| 1447 | 1433 | hyperlink_to_uuid(zUuid); |
| @@ -2248,14 +2234,11 @@ | ||
| 2248 | 2234 | @ <tr><th>Artifact ID:</th> |
| 2249 | 2235 | @ <td>%z(href("%R/artifact/%!S",zUuid))%s(zUuid)</a> |
| 2250 | 2236 | if( g.perm.Setup ){ |
| 2251 | 2237 | @ (%d(rid)) |
| 2252 | 2238 | } |
| 2253 | - modPending = moderation_pending(rid); | |
| 2254 | - if( modPending ){ | |
| 2255 | - @ <span class="modpending">*** Awaiting Moderator Approval ***</span> | |
| 2256 | - } | |
| 2239 | + modPending = moderation_pending_www(rid); | |
| 2257 | 2240 | @ <tr><th>Ticket:</th> |
| 2258 | 2241 | @ <td>%z(href("%R/tktview/%s",zTktName))%s(zTktName)</a> |
| 2259 | 2242 | if( zTktTitle ){ |
| 2260 | 2243 | @<br />%h(zTktTitle) |
| 2261 | 2244 | } |
| @@ -2364,10 +2347,15 @@ | ||
| 2364 | 2347 | if( db_exists("SELECT 1 FROM plink WHERE pid=%d", rid) ){ |
| 2365 | 2348 | ci_page(); |
| 2366 | 2349 | }else |
| 2367 | 2350 | if( db_exists("SELECT 1 FROM attachment WHERE attachid=%d", rid) ){ |
| 2368 | 2351 | ainfo_page(); |
| 2352 | + }else | |
| 2353 | + if( db_table_exists("repository","forumpost") | |
| 2354 | + && db_exists("SELECT 1 FROM forumpost WHERE fpid=%d", rid) | |
| 2355 | + ){ | |
| 2356 | + forumthread_page(); | |
| 2369 | 2357 | }else |
| 2370 | 2358 | { |
| 2371 | 2359 | artifact_page(); |
| 2372 | 2360 | } |
| 2373 | 2361 | } |
| 2374 | 2362 |
| --- src/info.c | |
| +++ src/info.c | |
| @@ -935,14 +935,11 @@ | |
| 935 | @ <tr><th>Artifact ID:</th> |
| 936 | @ <td>%z(href("%R/artifact/%!S",zUuid))%s(zUuid)</a> |
| 937 | if( g.perm.Setup ){ |
| 938 | @ (%d(rid)) |
| 939 | } |
| 940 | modPending = moderation_pending(rid); |
| 941 | if( modPending ){ |
| 942 | @ <span class="modpending">*** Awaiting Moderator Approval ***</span> |
| 943 | } |
| 944 | @ </td></tr> |
| 945 | @ <tr><th>Page Name:</th><td>%h(pWiki->zWikiTitle)</td></tr> |
| 946 | @ <tr><th>Date:</th><td> |
| 947 | hyperlink_to_date(zDate, "</td></tr>"); |
| 948 | @ <tr><th>Original User:</th><td> |
| @@ -978,25 +975,10 @@ | |
| 978 | @ <div class="section">Content</div> |
| 979 | blob_init(&wiki, pWiki->zWiki, -1); |
| 980 | wiki_render_by_mimetype(&wiki, pWiki->zMimetype); |
| 981 | blob_reset(&wiki); |
| 982 | manifest_destroy(pWiki); |
| 983 | style_footer(); |
| 984 | } |
| 985 | |
| 986 | /* |
| 987 | ** Show a webpage error message |
| 988 | */ |
| 989 | void webpage_error(const char *zFormat, ...){ |
| 990 | va_list ap; |
| 991 | const char *z; |
| 992 | va_start(ap, zFormat); |
| 993 | z = vmprintf(zFormat, ap); |
| 994 | va_end(ap); |
| 995 | style_header("URL Error"); |
| 996 | @ <h1>Error</h1> |
| 997 | @ <p>%h(z)</p> |
| 998 | style_footer(); |
| 999 | } |
| 1000 | |
| 1001 | /* |
| 1002 | ** Find an check-in based on query parameter zParam and parse its |
| @@ -1246,10 +1228,11 @@ | |
| 1246 | #define OBJTYPE_ATTACHMENT 0x0010 |
| 1247 | #define OBJTYPE_EVENT 0x0020 |
| 1248 | #define OBJTYPE_TAG 0x0040 |
| 1249 | #define OBJTYPE_SYMLINK 0x0080 |
| 1250 | #define OBJTYPE_EXE 0x0100 |
| 1251 | |
| 1252 | /* |
| 1253 | ** Possible flags for the second parameter to |
| 1254 | ** object_description() |
| 1255 | */ |
| @@ -1438,10 +1421,13 @@ | |
| 1438 | objType |= OBJTYPE_EVENT; |
| 1439 | hyperlink_to_event_tagid(db_column_int(&q, 5)); |
| 1440 | }else{ |
| 1441 | @ Attachment to technote |
| 1442 | } |
| 1443 | }else{ |
| 1444 | @ Tag referencing |
| 1445 | } |
| 1446 | if( zType[0]!='e' || eventTagId == 0){ |
| 1447 | hyperlink_to_uuid(zUuid); |
| @@ -2248,14 +2234,11 @@ | |
| 2248 | @ <tr><th>Artifact ID:</th> |
| 2249 | @ <td>%z(href("%R/artifact/%!S",zUuid))%s(zUuid)</a> |
| 2250 | if( g.perm.Setup ){ |
| 2251 | @ (%d(rid)) |
| 2252 | } |
| 2253 | modPending = moderation_pending(rid); |
| 2254 | if( modPending ){ |
| 2255 | @ <span class="modpending">*** Awaiting Moderator Approval ***</span> |
| 2256 | } |
| 2257 | @ <tr><th>Ticket:</th> |
| 2258 | @ <td>%z(href("%R/tktview/%s",zTktName))%s(zTktName)</a> |
| 2259 | if( zTktTitle ){ |
| 2260 | @<br />%h(zTktTitle) |
| 2261 | } |
| @@ -2364,10 +2347,15 @@ | |
| 2364 | if( db_exists("SELECT 1 FROM plink WHERE pid=%d", rid) ){ |
| 2365 | ci_page(); |
| 2366 | }else |
| 2367 | if( db_exists("SELECT 1 FROM attachment WHERE attachid=%d", rid) ){ |
| 2368 | ainfo_page(); |
| 2369 | }else |
| 2370 | { |
| 2371 | artifact_page(); |
| 2372 | } |
| 2373 | } |
| 2374 |
| --- src/info.c | |
| +++ src/info.c | |
| @@ -935,14 +935,11 @@ | |
| 935 | @ <tr><th>Artifact ID:</th> |
| 936 | @ <td>%z(href("%R/artifact/%!S",zUuid))%s(zUuid)</a> |
| 937 | if( g.perm.Setup ){ |
| 938 | @ (%d(rid)) |
| 939 | } |
| 940 | modPending = moderation_pending_www(rid); |
| 941 | @ </td></tr> |
| 942 | @ <tr><th>Page Name:</th><td>%h(pWiki->zWikiTitle)</td></tr> |
| 943 | @ <tr><th>Date:</th><td> |
| 944 | hyperlink_to_date(zDate, "</td></tr>"); |
| 945 | @ <tr><th>Original User:</th><td> |
| @@ -978,25 +975,10 @@ | |
| 975 | @ <div class="section">Content</div> |
| 976 | blob_init(&wiki, pWiki->zWiki, -1); |
| 977 | wiki_render_by_mimetype(&wiki, pWiki->zMimetype); |
| 978 | blob_reset(&wiki); |
| 979 | manifest_destroy(pWiki); |
| 980 | style_footer(); |
| 981 | } |
| 982 | |
| 983 | /* |
| 984 | ** Find an check-in based on query parameter zParam and parse its |
| @@ -1246,10 +1228,11 @@ | |
| 1228 | #define OBJTYPE_ATTACHMENT 0x0010 |
| 1229 | #define OBJTYPE_EVENT 0x0020 |
| 1230 | #define OBJTYPE_TAG 0x0040 |
| 1231 | #define OBJTYPE_SYMLINK 0x0080 |
| 1232 | #define OBJTYPE_EXE 0x0100 |
| 1233 | #define OBJTYPE_FORUM 0x0200 |
| 1234 | |
| 1235 | /* |
| 1236 | ** Possible flags for the second parameter to |
| 1237 | ** object_description() |
| 1238 | */ |
| @@ -1438,10 +1421,13 @@ | |
| 1421 | objType |= OBJTYPE_EVENT; |
| 1422 | hyperlink_to_event_tagid(db_column_int(&q, 5)); |
| 1423 | }else{ |
| 1424 | @ Attachment to technote |
| 1425 | } |
| 1426 | }else if( zType[0]=='f' ){ |
| 1427 | objType |= OBJTYPE_FORUM; |
| 1428 | @ Forum post |
| 1429 | }else{ |
| 1430 | @ Tag referencing |
| 1431 | } |
| 1432 | if( zType[0]!='e' || eventTagId == 0){ |
| 1433 | hyperlink_to_uuid(zUuid); |
| @@ -2248,14 +2234,11 @@ | |
| 2234 | @ <tr><th>Artifact ID:</th> |
| 2235 | @ <td>%z(href("%R/artifact/%!S",zUuid))%s(zUuid)</a> |
| 2236 | if( g.perm.Setup ){ |
| 2237 | @ (%d(rid)) |
| 2238 | } |
| 2239 | modPending = moderation_pending_www(rid); |
| 2240 | @ <tr><th>Ticket:</th> |
| 2241 | @ <td>%z(href("%R/tktview/%s",zTktName))%s(zTktName)</a> |
| 2242 | if( zTktTitle ){ |
| 2243 | @<br />%h(zTktTitle) |
| 2244 | } |
| @@ -2364,10 +2347,15 @@ | |
| 2347 | if( db_exists("SELECT 1 FROM plink WHERE pid=%d", rid) ){ |
| 2348 | ci_page(); |
| 2349 | }else |
| 2350 | if( db_exists("SELECT 1 FROM attachment WHERE attachid=%d", rid) ){ |
| 2351 | ainfo_page(); |
| 2352 | }else |
| 2353 | if( db_table_exists("repository","forumpost") |
| 2354 | && db_exists("SELECT 1 FROM forumpost WHERE fpid=%d", rid) |
| 2355 | ){ |
| 2356 | forumthread_page(); |
| 2357 | }else |
| 2358 | { |
| 2359 | artifact_page(); |
| 2360 | } |
| 2361 | } |
| 2362 |
+128
-77
| --- src/login.c | ||
| +++ src/login.c | ||
| @@ -470,10 +470,28 @@ | ||
| 470 | 470 | zPattern = mprintf("%s/login*", g.zBaseURL); |
| 471 | 471 | rc = sqlite3_strglob(zPattern, zReferer)==0; |
| 472 | 472 | fossil_free(zPattern); |
| 473 | 473 | return rc; |
| 474 | 474 | } |
| 475 | + | |
| 476 | +/* | |
| 477 | +** Return TRUE if self-registration is available. If the zNeeded | |
| 478 | +** argument is not NULL, then only return true if self-registration is | |
| 479 | +** available and any of the capabilities named in zNeeded are available | |
| 480 | +** to self-registered users. | |
| 481 | +*/ | |
| 482 | +int login_self_register_available(const char *zNeeded){ | |
| 483 | + CapabilityString *pCap; | |
| 484 | + int rc; | |
| 485 | + if( !db_get_boolean("self-register",0) ) return 0; | |
| 486 | + if( zNeeded==0 ) return 1; | |
| 487 | + pCap = capability_add(0, db_get("default-perms","")); | |
| 488 | + capability_expand(pCap); | |
| 489 | + rc = capability_has_any(pCap, zNeeded); | |
| 490 | + capability_free(pCap); | |
| 491 | + return rc; | |
| 492 | +} | |
| 475 | 493 | |
| 476 | 494 | /* |
| 477 | 495 | ** There used to be a page named "my" that was designed to show information |
| 478 | 496 | ** about a specific user. The "my" page was linked from the "Logged in as USER" |
| 479 | 497 | ** line on the title bar. The "my" page was never completed so it is now |
| @@ -498,10 +516,11 @@ | ||
| 498 | 516 | char *zErrMsg = ""; |
| 499 | 517 | int uid; /* User id logged in user */ |
| 500 | 518 | char *zSha1Pw; |
| 501 | 519 | const char *zIpAddr; /* IP address of requestor */ |
| 502 | 520 | const char *zReferer; |
| 521 | + int noAnon = P("noanon")!=0; | |
| 503 | 522 | |
| 504 | 523 | login_check_credentials(); |
| 505 | 524 | if( login_wants_https_redirect() ){ |
| 506 | 525 | const char *zQS = P("QUERY_STRING"); |
| 507 | 526 | if( P("redir")!=0 ){ |
| @@ -536,10 +555,16 @@ | ||
| 536 | 555 | if( P("out") ){ |
| 537 | 556 | login_clear_login_data(); |
| 538 | 557 | redirect_to_g(); |
| 539 | 558 | return; |
| 540 | 559 | } |
| 560 | + | |
| 561 | + /* Redirect for create-new-account requests */ | |
| 562 | + if( P("self") ){ | |
| 563 | + cgi_redirectf("%R/register"); | |
| 564 | + return; | |
| 565 | + } | |
| 541 | 566 | |
| 542 | 567 | /* Deal with password-change requests */ |
| 543 | 568 | if( g.perm.Password && zPasswd |
| 544 | 569 | && (zNew1 = P("n1"))!=0 && (zNew2 = P("n2"))!=0 |
| 545 | 570 | ){ |
| @@ -631,11 +656,11 @@ | ||
| 631 | 656 | } |
| 632 | 657 | } |
| 633 | 658 | style_header("Login/Logout"); |
| 634 | 659 | style_adunit_config(ADUNIT_OFF); |
| 635 | 660 | @ %s(zErrMsg) |
| 636 | - if( zGoto ){ | |
| 661 | + if( zGoto && !noAnon ){ | |
| 637 | 662 | char *zAbbrev = fossil_strdup(zGoto); |
| 638 | 663 | int i; |
| 639 | 664 | for(i=0; zAbbrev[i] && zAbbrev[i]!='?'; i++){} |
| 640 | 665 | zAbbrev[i] = 0; |
| 641 | 666 | if( g.zLogin ){ |
| @@ -675,11 +700,11 @@ | ||
| 675 | 700 | @ <td><input type="text" id="u" name="u" value="anonymous" size="30"></td> |
| 676 | 701 | }else{ |
| 677 | 702 | @ <td><input type="text" id="u" name="u" value="" size="30" /></td> |
| 678 | 703 | } |
| 679 | 704 | if( P("HTTPS")==0 ){ |
| 680 | - @ <td width="15"><td rowspan="3"> | |
| 705 | + @ <td width="15"><td rowspan="2"> | |
| 681 | 706 | @ <p class='securityWarning'> |
| 682 | 707 | @ Warning: Your password will be sent in the clear over an |
| 683 | 708 | @ unencrypted connection. |
| 684 | 709 | if( g.sslNotAvailable ){ |
| 685 | 710 | @ No encrypted connection is available on this server. |
| @@ -690,28 +715,33 @@ | ||
| 690 | 715 | @ </p> |
| 691 | 716 | } |
| 692 | 717 | @ </tr> |
| 693 | 718 | @ <tr> |
| 694 | 719 | @ <td class="form_label">Password:</td> |
| 695 | - @ <td><input type="password" id="p" name="p" value="" size="30" /></td> | |
| 720 | + @ <td><input type="password" id="p" name="p" value="" size="30" /></td> | |
| 696 | 721 | @ </tr> |
| 697 | 722 | if( g.zLogin==0 && (anonFlag || zGoto==0) ){ |
| 698 | 723 | zAnonPw = db_text(0, "SELECT pw FROM user" |
| 699 | 724 | " WHERE login='anonymous'" |
| 700 | 725 | " AND cap!=''"); |
| 701 | 726 | } |
| 702 | 727 | @ <tr> |
| 703 | 728 | @ <td></td> |
| 704 | - @ <td><input type="submit" name="in" value="Login"> | |
| 729 | + @ <td><input type="submit" name="in" value="Login"></td> | |
| 730 | + @ <td colspan="2">← Pressing this button grants\ | |
| 731 | + @ permission to store a cookie | |
| 705 | 732 | @ </tr> |
| 733 | + if( !noAnon && login_self_register_available(0) ){ | |
| 734 | + @ <tr> | |
| 735 | + @ <td></td> | |
| 736 | + @ <td><input type="submit" name="self" value="Create A New Account"> | |
| 737 | + @ <td colspan="2"> \ | |
| 738 | + @ ← Don't have a login? Click this button to create one. | |
| 739 | + @ </tr> | |
| 740 | + } | |
| 706 | 741 | @ </table> |
| 707 | - @ <p>Pressing the Login button grants permission to store a cookie.</p> | |
| 708 | - if( db_get_boolean("self-register", 0) ){ | |
| 709 | - @ <p>If you do not have an account, you can | |
| 710 | - @ <a href="%R/register?g=%T(P("G"))">create one</a>. | |
| 711 | - } | |
| 712 | - if( zAnonPw ){ | |
| 742 | + if( zAnonPw && !noAnon ){ | |
| 713 | 743 | unsigned int uSeed = captcha_seed(); |
| 714 | 744 | const char *zDecoded = captcha_decode(uSeed); |
| 715 | 745 | int bAutoCaptcha = db_get_boolean("auto-captcha", 0); |
| 716 | 746 | char *zCaptcha = captcha_render(zDecoded); |
| 717 | 747 | |
| @@ -1459,82 +1489,81 @@ | ||
| 1459 | 1489 | ** |
| 1460 | 1490 | ** Page to allow users to self-register. The "self-register" setting |
| 1461 | 1491 | ** must be enabled for this page to operate. |
| 1462 | 1492 | */ |
| 1463 | 1493 | void register_page(void){ |
| 1464 | - const char *zUsername, *zPasswd, *zConfirm, *zContact, *zCS, *zPw, *zCap; | |
| 1494 | + const char *zUserID, *zPasswd, *zConfirm, *zEAddr; | |
| 1495 | + const char *zDName; | |
| 1465 | 1496 | unsigned int uSeed; |
| 1466 | 1497 | const char *zDecoded; |
| 1467 | 1498 | char *zCaptcha; |
| 1499 | + int iErrLine = -1; | |
| 1500 | + const char *zErr; | |
| 1468 | 1501 | if( !db_get_boolean("self-register", 0) ){ |
| 1469 | 1502 | style_header("Registration not possible"); |
| 1470 | 1503 | @ <p>This project does not allow user self-registration. Please contact the |
| 1471 | 1504 | @ project administrator to obtain an account.</p> |
| 1472 | 1505 | style_footer(); |
| 1473 | 1506 | return; |
| 1474 | 1507 | } |
| 1475 | 1508 | |
| 1476 | 1509 | style_header("Register"); |
| 1477 | - zUsername = P("u"); | |
| 1478 | - zPasswd = P("p"); | |
| 1479 | - zConfirm = P("cp"); | |
| 1480 | - zContact = P("c"); | |
| 1481 | - zCap = P("cap"); | |
| 1482 | - zCS = P("cs"); /* Captcha Secret */ | |
| 1510 | + zUserID = PDT("u",""); | |
| 1511 | + zPasswd = PDT("p",""); | |
| 1512 | + zConfirm = PDT("cp",""); | |
| 1513 | + zEAddr = PDT("ea",""); | |
| 1514 | + zDName = PDT("dn",""); | |
| 1483 | 1515 | |
| 1484 | 1516 | /* Try to make any sense from user input. */ |
| 1485 | - if( P("new") ){ | |
| 1486 | - if( zCS==0 ) fossil_redirect_home(); /* Forged request */ | |
| 1487 | - zPw = captcha_decode((unsigned int)atoi(zCS)); | |
| 1488 | - if( !(zUsername && zPasswd && zConfirm && zContact) ){ | |
| 1489 | - @ <p><span class="loginError"> | |
| 1490 | - @ All fields are obligatory. | |
| 1491 | - @ </span></p> | |
| 1492 | - }else if( strlen(zPasswd) < 6){ | |
| 1493 | - @ <p><span class="loginError"> | |
| 1494 | - @ Password too weak. | |
| 1495 | - @ </span></p> | |
| 1496 | - }else if( fossil_strcmp(zPasswd,zConfirm)!=0 ){ | |
| 1497 | - @ <p><span class="loginError"> | |
| 1498 | - @ The two copies of your new passwords do not match. | |
| 1499 | - @ </span></p> | |
| 1500 | - }else if( fossil_stricmp(zPw, zCap)!=0 ){ | |
| 1501 | - @ <p><span class="loginError"> | |
| 1502 | - @ Captcha text invalid. | |
| 1503 | - @ </span></p> | |
| 1504 | - }else{ | |
| 1505 | - /* This almost is stupid copy-paste of code from user.c:user_cmd(). */ | |
| 1506 | - Blob passwd, login, caps, contact; | |
| 1507 | - | |
| 1508 | - blob_init(&login, zUsername, -1); | |
| 1509 | - blob_init(&contact, zContact, -1); | |
| 1510 | - blob_init(&caps, db_get("default-perms", "u"), -1); | |
| 1511 | - blob_init(&passwd, zPasswd, -1); | |
| 1512 | - | |
| 1513 | - if( db_exists("SELECT 1 FROM user WHERE login=%B", &login) ){ | |
| 1514 | - /* Here lies the reason I don't use zErrMsg - it would not substitute | |
| 1515 | - * this %s(zUsername), or at least I don't know how to force it to.*/ | |
| 1516 | - @ <p><span class="loginError"> | |
| 1517 | - @ %h(zUsername) already exists. | |
| 1518 | - @ </span></p> | |
| 1519 | - }else{ | |
| 1520 | - char *zPw = sha1_shared_secret(blob_str(&passwd), blob_str(&login), 0); | |
| 1521 | - int uid; | |
| 1522 | - db_multi_exec( | |
| 1523 | - "INSERT INTO user(login,pw,cap,info,mtime)" | |
| 1524 | - "VALUES(%B,%Q,%B,%B,strftime('%%s','now'))", | |
| 1525 | - &login, zPw, &caps, &contact | |
| 1526 | - ); | |
| 1527 | - free(zPw); | |
| 1528 | - | |
| 1529 | - /* The user is registered, now just log him in. */ | |
| 1530 | - uid = db_int(0, "SELECT uid FROM user WHERE login=%Q", zUsername); | |
| 1531 | - login_set_user_cookie( zUsername, uid, NULL ); | |
| 1532 | - redirect_to_g(); | |
| 1533 | - | |
| 1534 | - } | |
| 1535 | - } | |
| 1517 | + if( P("new")==0 || !cgi_csrf_safe(1) ){ | |
| 1518 | + /* This is not a valid form submission. Fall through into | |
| 1519 | + ** the form display */ | |
| 1520 | + }else if( !captcha_is_correct(1) ){ | |
| 1521 | + iErrLine = 6; | |
| 1522 | + zErr = "Incorrect CAPTCHA"; | |
| 1523 | + }else if( strlen(zUserID)<3 ){ | |
| 1524 | + iErrLine = 1; | |
| 1525 | + zErr = "User ID too short. Must be at least 3 characters."; | |
| 1526 | + }else if( sqlite3_strglob("*[^-a-zA-Z0-9_.]*",zUserID)==0 ){ | |
| 1527 | + iErrLine = 1; | |
| 1528 | + zErr = "User ID may not contain spaces or special characters."; | |
| 1529 | + }else if( zDName[0]==0 ){ | |
| 1530 | + iErrLine = 2; | |
| 1531 | + zErr = "Required"; | |
| 1532 | + }else if( zEAddr[0]==0 ){ | |
| 1533 | + iErrLine = 3; | |
| 1534 | + zErr = "Required"; | |
| 1535 | + }else if( email_copy_addr(zEAddr,0)==0 ){ | |
| 1536 | + iErrLine = 3; | |
| 1537 | + zErr = "Not a valid email address"; | |
| 1538 | + }else if( strlen(zPasswd)<6 ){ | |
| 1539 | + iErrLine = 4; | |
| 1540 | + zErr = "Password must be at least 6 characters long"; | |
| 1541 | + }else if( fossil_strcmp(zPasswd,zConfirm)!=0 ){ | |
| 1542 | + iErrLine = 5; | |
| 1543 | + zErr = "Passwords do not match"; | |
| 1544 | + }else if( db_exists("SELECT 1 FROM user WHERE login=%Q", zUserID) ){ | |
| 1545 | + iErrLine = 1; | |
| 1546 | + zErr = "This User ID is already taken. Choose something different."; | |
| 1547 | + }else if( db_exists("SELECT 1 FROM user WHERE info LIKE '%%%q%%'", zEAddr) ){ | |
| 1548 | + iErrLine = 3; | |
| 1549 | + zErr = "This address is already used."; | |
| 1550 | + }else{ | |
| 1551 | + Blob sql; | |
| 1552 | + int uid; | |
| 1553 | + char *zPass = sha1_shared_secret(zPasswd, zUserID, 0); | |
| 1554 | + blob_init(&sql, 0, 0); | |
| 1555 | + blob_append_sql(&sql, | |
| 1556 | + "INSERT INTO user(login,pw,cap,info,mtime)\n" | |
| 1557 | + "VALUES(%Q,%Q,%Q," | |
| 1558 | + "'%q <%q>\nself-register from ip %q on '||datetime('now'),now())", | |
| 1559 | + zUserID, zPass, db_get("default-perms","u"), zDName, zEAddr, g.zIpAddr); | |
| 1560 | + fossil_free(zPass); | |
| 1561 | + db_multi_exec("%s", blob_sql_text(&sql)); | |
| 1562 | + uid = db_int(0, "SELECT uid FROM user WHERE login=%Q", zUserID); | |
| 1563 | + login_set_user_cookie(zUserID, uid, NULL); | |
| 1564 | + redirect_to_g(); | |
| 1536 | 1565 | } |
| 1537 | 1566 | |
| 1538 | 1567 | /* Prepare the captcha. */ |
| 1539 | 1568 | uSeed = captcha_seed(); |
| 1540 | 1569 | zDecoded = captcha_decode(uSeed); |
| @@ -1543,31 +1572,53 @@ | ||
| 1543 | 1572 | /* Print out the registration form. */ |
| 1544 | 1573 | form_begin(0, "%R/register"); |
| 1545 | 1574 | if( P("g") ){ |
| 1546 | 1575 | @ <input type="hidden" name="g" value="%h(P("g"))" /> |
| 1547 | 1576 | } |
| 1548 | - @ <p><input type="hidden" name="cs" value="%u(uSeed)" /> | |
| 1577 | + @ <p><input type="hidden" name="captchaseed" value="%u(uSeed)" /> | |
| 1549 | 1578 | @ <table class="login_out"> |
| 1550 | 1579 | @ <tr> |
| 1551 | 1580 | @ <td class="form_label" align="right">User ID:</td> |
| 1552 | - @ <td><input type="text" id="u" name="u" value="" size="30" /></td> | |
| 1581 | + @ <td><input type="text" name="u" value="%h(zUserID)" size="30"></td> | |
| 1582 | + if( iErrLine==1 ){ | |
| 1583 | + @ <td><span class='loginError'>← %h(zErr)</span></td> | |
| 1584 | + } | |
| 1585 | + @ </tr> | |
| 1586 | + @ <tr> | |
| 1587 | + @ <td class="form_label" align="right">Display Name:</td> | |
| 1588 | + @ <td><input type="text" name="dn" value="%h(zDName)" size="30"></td> | |
| 1589 | + if( iErrLine==2 ){ | |
| 1590 | + @ <td><span class='loginError'>← %h(zErr)</span></td> | |
| 1591 | + } | |
| 1592 | + @ </tr> | |
| 1593 | + @ <tr> | |
| 1594 | + @ <td class="form_label" align="right">Email Address:</td> | |
| 1595 | + @ <td><input type="text" name="ea" value="%h(zEAddr)" size="30"></td> | |
| 1596 | + if( iErrLine==3 ){ | |
| 1597 | + @ <td><span class='loginError'>← %h(zErr)</span></td> | |
| 1598 | + } | |
| 1553 | 1599 | @ </tr> |
| 1554 | 1600 | @ <tr> |
| 1555 | 1601 | @ <td class="form_label" align="right">Password:</td> |
| 1556 | - @ <td><input type="password" id="p" name="p" value="" size="30" /></td> | |
| 1602 | + @ <td><input type="password" name="p" value="%h(zPasswd)" size="30"></td> | |
| 1603 | + if( iErrLine==4 ){ | |
| 1604 | + @ <td><span class='loginError'>← %h(zErr)</span></td> | |
| 1605 | + } | |
| 1557 | 1606 | @ </tr> |
| 1558 | 1607 | @ <tr> |
| 1559 | 1608 | @ <td class="form_label" align="right">Confirm password:</td> |
| 1560 | - @ <td><input type="password" id="cp" name="cp" value="" size="30" /></td> | |
| 1561 | - @ </tr> | |
| 1562 | - @ <tr> | |
| 1563 | - @ <td class="form_label" align="right">Contact info:</td> | |
| 1564 | - @ <td><input type="text" id="c" name="c" value="" size="30" /></td> | |
| 1609 | + @ <td><input type="password" name="cp" value="%h(zConfirm)" size="30"></td> | |
| 1610 | + if( iErrLine==5 ){ | |
| 1611 | + @ <td><span class='loginError'>← %h(zErr)</span></td> | |
| 1612 | + } | |
| 1565 | 1613 | @ </tr> |
| 1566 | 1614 | @ <tr> |
| 1567 | 1615 | @ <td class="form_label" align="right">Captcha text (below):</td> |
| 1568 | - @ <td><input type="text" id="cap" name="cap" value="" size="30" /></td> | |
| 1616 | + @ <td><input type="text" name="captcha" value="" size="30"></td> | |
| 1617 | + if( iErrLine==6 ){ | |
| 1618 | + @ <td><span class='loginError'>← %h(zErr)</span></td> | |
| 1619 | + } | |
| 1569 | 1620 | @ </tr> |
| 1570 | 1621 | @ <tr><td></td> |
| 1571 | 1622 | @ <td><input type="submit" name="new" value="Register" /></td></tr> |
| 1572 | 1623 | @ </table> |
| 1573 | 1624 | @ <div class="captcha"><table class="captcha"><tr><td><pre> |
| 1574 | 1625 |
| --- src/login.c | |
| +++ src/login.c | |
| @@ -470,10 +470,28 @@ | |
| 470 | zPattern = mprintf("%s/login*", g.zBaseURL); |
| 471 | rc = sqlite3_strglob(zPattern, zReferer)==0; |
| 472 | fossil_free(zPattern); |
| 473 | return rc; |
| 474 | } |
| 475 | |
| 476 | /* |
| 477 | ** There used to be a page named "my" that was designed to show information |
| 478 | ** about a specific user. The "my" page was linked from the "Logged in as USER" |
| 479 | ** line on the title bar. The "my" page was never completed so it is now |
| @@ -498,10 +516,11 @@ | |
| 498 | char *zErrMsg = ""; |
| 499 | int uid; /* User id logged in user */ |
| 500 | char *zSha1Pw; |
| 501 | const char *zIpAddr; /* IP address of requestor */ |
| 502 | const char *zReferer; |
| 503 | |
| 504 | login_check_credentials(); |
| 505 | if( login_wants_https_redirect() ){ |
| 506 | const char *zQS = P("QUERY_STRING"); |
| 507 | if( P("redir")!=0 ){ |
| @@ -536,10 +555,16 @@ | |
| 536 | if( P("out") ){ |
| 537 | login_clear_login_data(); |
| 538 | redirect_to_g(); |
| 539 | return; |
| 540 | } |
| 541 | |
| 542 | /* Deal with password-change requests */ |
| 543 | if( g.perm.Password && zPasswd |
| 544 | && (zNew1 = P("n1"))!=0 && (zNew2 = P("n2"))!=0 |
| 545 | ){ |
| @@ -631,11 +656,11 @@ | |
| 631 | } |
| 632 | } |
| 633 | style_header("Login/Logout"); |
| 634 | style_adunit_config(ADUNIT_OFF); |
| 635 | @ %s(zErrMsg) |
| 636 | if( zGoto ){ |
| 637 | char *zAbbrev = fossil_strdup(zGoto); |
| 638 | int i; |
| 639 | for(i=0; zAbbrev[i] && zAbbrev[i]!='?'; i++){} |
| 640 | zAbbrev[i] = 0; |
| 641 | if( g.zLogin ){ |
| @@ -675,11 +700,11 @@ | |
| 675 | @ <td><input type="text" id="u" name="u" value="anonymous" size="30"></td> |
| 676 | }else{ |
| 677 | @ <td><input type="text" id="u" name="u" value="" size="30" /></td> |
| 678 | } |
| 679 | if( P("HTTPS")==0 ){ |
| 680 | @ <td width="15"><td rowspan="3"> |
| 681 | @ <p class='securityWarning'> |
| 682 | @ Warning: Your password will be sent in the clear over an |
| 683 | @ unencrypted connection. |
| 684 | if( g.sslNotAvailable ){ |
| 685 | @ No encrypted connection is available on this server. |
| @@ -690,28 +715,33 @@ | |
| 690 | @ </p> |
| 691 | } |
| 692 | @ </tr> |
| 693 | @ <tr> |
| 694 | @ <td class="form_label">Password:</td> |
| 695 | @ <td><input type="password" id="p" name="p" value="" size="30" /></td> |
| 696 | @ </tr> |
| 697 | if( g.zLogin==0 && (anonFlag || zGoto==0) ){ |
| 698 | zAnonPw = db_text(0, "SELECT pw FROM user" |
| 699 | " WHERE login='anonymous'" |
| 700 | " AND cap!=''"); |
| 701 | } |
| 702 | @ <tr> |
| 703 | @ <td></td> |
| 704 | @ <td><input type="submit" name="in" value="Login"> |
| 705 | @ </tr> |
| 706 | @ </table> |
| 707 | @ <p>Pressing the Login button grants permission to store a cookie.</p> |
| 708 | if( db_get_boolean("self-register", 0) ){ |
| 709 | @ <p>If you do not have an account, you can |
| 710 | @ <a href="%R/register?g=%T(P("G"))">create one</a>. |
| 711 | } |
| 712 | if( zAnonPw ){ |
| 713 | unsigned int uSeed = captcha_seed(); |
| 714 | const char *zDecoded = captcha_decode(uSeed); |
| 715 | int bAutoCaptcha = db_get_boolean("auto-captcha", 0); |
| 716 | char *zCaptcha = captcha_render(zDecoded); |
| 717 | |
| @@ -1459,82 +1489,81 @@ | |
| 1459 | ** |
| 1460 | ** Page to allow users to self-register. The "self-register" setting |
| 1461 | ** must be enabled for this page to operate. |
| 1462 | */ |
| 1463 | void register_page(void){ |
| 1464 | const char *zUsername, *zPasswd, *zConfirm, *zContact, *zCS, *zPw, *zCap; |
| 1465 | unsigned int uSeed; |
| 1466 | const char *zDecoded; |
| 1467 | char *zCaptcha; |
| 1468 | if( !db_get_boolean("self-register", 0) ){ |
| 1469 | style_header("Registration not possible"); |
| 1470 | @ <p>This project does not allow user self-registration. Please contact the |
| 1471 | @ project administrator to obtain an account.</p> |
| 1472 | style_footer(); |
| 1473 | return; |
| 1474 | } |
| 1475 | |
| 1476 | style_header("Register"); |
| 1477 | zUsername = P("u"); |
| 1478 | zPasswd = P("p"); |
| 1479 | zConfirm = P("cp"); |
| 1480 | zContact = P("c"); |
| 1481 | zCap = P("cap"); |
| 1482 | zCS = P("cs"); /* Captcha Secret */ |
| 1483 | |
| 1484 | /* Try to make any sense from user input. */ |
| 1485 | if( P("new") ){ |
| 1486 | if( zCS==0 ) fossil_redirect_home(); /* Forged request */ |
| 1487 | zPw = captcha_decode((unsigned int)atoi(zCS)); |
| 1488 | if( !(zUsername && zPasswd && zConfirm && zContact) ){ |
| 1489 | @ <p><span class="loginError"> |
| 1490 | @ All fields are obligatory. |
| 1491 | @ </span></p> |
| 1492 | }else if( strlen(zPasswd) < 6){ |
| 1493 | @ <p><span class="loginError"> |
| 1494 | @ Password too weak. |
| 1495 | @ </span></p> |
| 1496 | }else if( fossil_strcmp(zPasswd,zConfirm)!=0 ){ |
| 1497 | @ <p><span class="loginError"> |
| 1498 | @ The two copies of your new passwords do not match. |
| 1499 | @ </span></p> |
| 1500 | }else if( fossil_stricmp(zPw, zCap)!=0 ){ |
| 1501 | @ <p><span class="loginError"> |
| 1502 | @ Captcha text invalid. |
| 1503 | @ </span></p> |
| 1504 | }else{ |
| 1505 | /* This almost is stupid copy-paste of code from user.c:user_cmd(). */ |
| 1506 | Blob passwd, login, caps, contact; |
| 1507 | |
| 1508 | blob_init(&login, zUsername, -1); |
| 1509 | blob_init(&contact, zContact, -1); |
| 1510 | blob_init(&caps, db_get("default-perms", "u"), -1); |
| 1511 | blob_init(&passwd, zPasswd, -1); |
| 1512 | |
| 1513 | if( db_exists("SELECT 1 FROM user WHERE login=%B", &login) ){ |
| 1514 | /* Here lies the reason I don't use zErrMsg - it would not substitute |
| 1515 | * this %s(zUsername), or at least I don't know how to force it to.*/ |
| 1516 | @ <p><span class="loginError"> |
| 1517 | @ %h(zUsername) already exists. |
| 1518 | @ </span></p> |
| 1519 | }else{ |
| 1520 | char *zPw = sha1_shared_secret(blob_str(&passwd), blob_str(&login), 0); |
| 1521 | int uid; |
| 1522 | db_multi_exec( |
| 1523 | "INSERT INTO user(login,pw,cap,info,mtime)" |
| 1524 | "VALUES(%B,%Q,%B,%B,strftime('%%s','now'))", |
| 1525 | &login, zPw, &caps, &contact |
| 1526 | ); |
| 1527 | free(zPw); |
| 1528 | |
| 1529 | /* The user is registered, now just log him in. */ |
| 1530 | uid = db_int(0, "SELECT uid FROM user WHERE login=%Q", zUsername); |
| 1531 | login_set_user_cookie( zUsername, uid, NULL ); |
| 1532 | redirect_to_g(); |
| 1533 | |
| 1534 | } |
| 1535 | } |
| 1536 | } |
| 1537 | |
| 1538 | /* Prepare the captcha. */ |
| 1539 | uSeed = captcha_seed(); |
| 1540 | zDecoded = captcha_decode(uSeed); |
| @@ -1543,31 +1572,53 @@ | |
| 1543 | /* Print out the registration form. */ |
| 1544 | form_begin(0, "%R/register"); |
| 1545 | if( P("g") ){ |
| 1546 | @ <input type="hidden" name="g" value="%h(P("g"))" /> |
| 1547 | } |
| 1548 | @ <p><input type="hidden" name="cs" value="%u(uSeed)" /> |
| 1549 | @ <table class="login_out"> |
| 1550 | @ <tr> |
| 1551 | @ <td class="form_label" align="right">User ID:</td> |
| 1552 | @ <td><input type="text" id="u" name="u" value="" size="30" /></td> |
| 1553 | @ </tr> |
| 1554 | @ <tr> |
| 1555 | @ <td class="form_label" align="right">Password:</td> |
| 1556 | @ <td><input type="password" id="p" name="p" value="" size="30" /></td> |
| 1557 | @ </tr> |
| 1558 | @ <tr> |
| 1559 | @ <td class="form_label" align="right">Confirm password:</td> |
| 1560 | @ <td><input type="password" id="cp" name="cp" value="" size="30" /></td> |
| 1561 | @ </tr> |
| 1562 | @ <tr> |
| 1563 | @ <td class="form_label" align="right">Contact info:</td> |
| 1564 | @ <td><input type="text" id="c" name="c" value="" size="30" /></td> |
| 1565 | @ </tr> |
| 1566 | @ <tr> |
| 1567 | @ <td class="form_label" align="right">Captcha text (below):</td> |
| 1568 | @ <td><input type="text" id="cap" name="cap" value="" size="30" /></td> |
| 1569 | @ </tr> |
| 1570 | @ <tr><td></td> |
| 1571 | @ <td><input type="submit" name="new" value="Register" /></td></tr> |
| 1572 | @ </table> |
| 1573 | @ <div class="captcha"><table class="captcha"><tr><td><pre> |
| 1574 |
| --- src/login.c | |
| +++ src/login.c | |
| @@ -470,10 +470,28 @@ | |
| 470 | zPattern = mprintf("%s/login*", g.zBaseURL); |
| 471 | rc = sqlite3_strglob(zPattern, zReferer)==0; |
| 472 | fossil_free(zPattern); |
| 473 | return rc; |
| 474 | } |
| 475 | |
| 476 | /* |
| 477 | ** Return TRUE if self-registration is available. If the zNeeded |
| 478 | ** argument is not NULL, then only return true if self-registration is |
| 479 | ** available and any of the capabilities named in zNeeded are available |
| 480 | ** to self-registered users. |
| 481 | */ |
| 482 | int login_self_register_available(const char *zNeeded){ |
| 483 | CapabilityString *pCap; |
| 484 | int rc; |
| 485 | if( !db_get_boolean("self-register",0) ) return 0; |
| 486 | if( zNeeded==0 ) return 1; |
| 487 | pCap = capability_add(0, db_get("default-perms","")); |
| 488 | capability_expand(pCap); |
| 489 | rc = capability_has_any(pCap, zNeeded); |
| 490 | capability_free(pCap); |
| 491 | return rc; |
| 492 | } |
| 493 | |
| 494 | /* |
| 495 | ** There used to be a page named "my" that was designed to show information |
| 496 | ** about a specific user. The "my" page was linked from the "Logged in as USER" |
| 497 | ** line on the title bar. The "my" page was never completed so it is now |
| @@ -498,10 +516,11 @@ | |
| 516 | char *zErrMsg = ""; |
| 517 | int uid; /* User id logged in user */ |
| 518 | char *zSha1Pw; |
| 519 | const char *zIpAddr; /* IP address of requestor */ |
| 520 | const char *zReferer; |
| 521 | int noAnon = P("noanon")!=0; |
| 522 | |
| 523 | login_check_credentials(); |
| 524 | if( login_wants_https_redirect() ){ |
| 525 | const char *zQS = P("QUERY_STRING"); |
| 526 | if( P("redir")!=0 ){ |
| @@ -536,10 +555,16 @@ | |
| 555 | if( P("out") ){ |
| 556 | login_clear_login_data(); |
| 557 | redirect_to_g(); |
| 558 | return; |
| 559 | } |
| 560 | |
| 561 | /* Redirect for create-new-account requests */ |
| 562 | if( P("self") ){ |
| 563 | cgi_redirectf("%R/register"); |
| 564 | return; |
| 565 | } |
| 566 | |
| 567 | /* Deal with password-change requests */ |
| 568 | if( g.perm.Password && zPasswd |
| 569 | && (zNew1 = P("n1"))!=0 && (zNew2 = P("n2"))!=0 |
| 570 | ){ |
| @@ -631,11 +656,11 @@ | |
| 656 | } |
| 657 | } |
| 658 | style_header("Login/Logout"); |
| 659 | style_adunit_config(ADUNIT_OFF); |
| 660 | @ %s(zErrMsg) |
| 661 | if( zGoto && !noAnon ){ |
| 662 | char *zAbbrev = fossil_strdup(zGoto); |
| 663 | int i; |
| 664 | for(i=0; zAbbrev[i] && zAbbrev[i]!='?'; i++){} |
| 665 | zAbbrev[i] = 0; |
| 666 | if( g.zLogin ){ |
| @@ -675,11 +700,11 @@ | |
| 700 | @ <td><input type="text" id="u" name="u" value="anonymous" size="30"></td> |
| 701 | }else{ |
| 702 | @ <td><input type="text" id="u" name="u" value="" size="30" /></td> |
| 703 | } |
| 704 | if( P("HTTPS")==0 ){ |
| 705 | @ <td width="15"><td rowspan="2"> |
| 706 | @ <p class='securityWarning'> |
| 707 | @ Warning: Your password will be sent in the clear over an |
| 708 | @ unencrypted connection. |
| 709 | if( g.sslNotAvailable ){ |
| 710 | @ No encrypted connection is available on this server. |
| @@ -690,28 +715,33 @@ | |
| 715 | @ </p> |
| 716 | } |
| 717 | @ </tr> |
| 718 | @ <tr> |
| 719 | @ <td class="form_label">Password:</td> |
| 720 | @ <td><input type="password" id="p" name="p" value="" size="30" /></td> |
| 721 | @ </tr> |
| 722 | if( g.zLogin==0 && (anonFlag || zGoto==0) ){ |
| 723 | zAnonPw = db_text(0, "SELECT pw FROM user" |
| 724 | " WHERE login='anonymous'" |
| 725 | " AND cap!=''"); |
| 726 | } |
| 727 | @ <tr> |
| 728 | @ <td></td> |
| 729 | @ <td><input type="submit" name="in" value="Login"></td> |
| 730 | @ <td colspan="2">← Pressing this button grants\ |
| 731 | @ permission to store a cookie |
| 732 | @ </tr> |
| 733 | if( !noAnon && login_self_register_available(0) ){ |
| 734 | @ <tr> |
| 735 | @ <td></td> |
| 736 | @ <td><input type="submit" name="self" value="Create A New Account"> |
| 737 | @ <td colspan="2"> \ |
| 738 | @ ← Don't have a login? Click this button to create one. |
| 739 | @ </tr> |
| 740 | } |
| 741 | @ </table> |
| 742 | if( zAnonPw && !noAnon ){ |
| 743 | unsigned int uSeed = captcha_seed(); |
| 744 | const char *zDecoded = captcha_decode(uSeed); |
| 745 | int bAutoCaptcha = db_get_boolean("auto-captcha", 0); |
| 746 | char *zCaptcha = captcha_render(zDecoded); |
| 747 | |
| @@ -1459,82 +1489,81 @@ | |
| 1489 | ** |
| 1490 | ** Page to allow users to self-register. The "self-register" setting |
| 1491 | ** must be enabled for this page to operate. |
| 1492 | */ |
| 1493 | void register_page(void){ |
| 1494 | const char *zUserID, *zPasswd, *zConfirm, *zEAddr; |
| 1495 | const char *zDName; |
| 1496 | unsigned int uSeed; |
| 1497 | const char *zDecoded; |
| 1498 | char *zCaptcha; |
| 1499 | int iErrLine = -1; |
| 1500 | const char *zErr; |
| 1501 | if( !db_get_boolean("self-register", 0) ){ |
| 1502 | style_header("Registration not possible"); |
| 1503 | @ <p>This project does not allow user self-registration. Please contact the |
| 1504 | @ project administrator to obtain an account.</p> |
| 1505 | style_footer(); |
| 1506 | return; |
| 1507 | } |
| 1508 | |
| 1509 | style_header("Register"); |
| 1510 | zUserID = PDT("u",""); |
| 1511 | zPasswd = PDT("p",""); |
| 1512 | zConfirm = PDT("cp",""); |
| 1513 | zEAddr = PDT("ea",""); |
| 1514 | zDName = PDT("dn",""); |
| 1515 | |
| 1516 | /* Try to make any sense from user input. */ |
| 1517 | if( P("new")==0 || !cgi_csrf_safe(1) ){ |
| 1518 | /* This is not a valid form submission. Fall through into |
| 1519 | ** the form display */ |
| 1520 | }else if( !captcha_is_correct(1) ){ |
| 1521 | iErrLine = 6; |
| 1522 | zErr = "Incorrect CAPTCHA"; |
| 1523 | }else if( strlen(zUserID)<3 ){ |
| 1524 | iErrLine = 1; |
| 1525 | zErr = "User ID too short. Must be at least 3 characters."; |
| 1526 | }else if( sqlite3_strglob("*[^-a-zA-Z0-9_.]*",zUserID)==0 ){ |
| 1527 | iErrLine = 1; |
| 1528 | zErr = "User ID may not contain spaces or special characters."; |
| 1529 | }else if( zDName[0]==0 ){ |
| 1530 | iErrLine = 2; |
| 1531 | zErr = "Required"; |
| 1532 | }else if( zEAddr[0]==0 ){ |
| 1533 | iErrLine = 3; |
| 1534 | zErr = "Required"; |
| 1535 | }else if( email_copy_addr(zEAddr,0)==0 ){ |
| 1536 | iErrLine = 3; |
| 1537 | zErr = "Not a valid email address"; |
| 1538 | }else if( strlen(zPasswd)<6 ){ |
| 1539 | iErrLine = 4; |
| 1540 | zErr = "Password must be at least 6 characters long"; |
| 1541 | }else if( fossil_strcmp(zPasswd,zConfirm)!=0 ){ |
| 1542 | iErrLine = 5; |
| 1543 | zErr = "Passwords do not match"; |
| 1544 | }else if( db_exists("SELECT 1 FROM user WHERE login=%Q", zUserID) ){ |
| 1545 | iErrLine = 1; |
| 1546 | zErr = "This User ID is already taken. Choose something different."; |
| 1547 | }else if( db_exists("SELECT 1 FROM user WHERE info LIKE '%%%q%%'", zEAddr) ){ |
| 1548 | iErrLine = 3; |
| 1549 | zErr = "This address is already used."; |
| 1550 | }else{ |
| 1551 | Blob sql; |
| 1552 | int uid; |
| 1553 | char *zPass = sha1_shared_secret(zPasswd, zUserID, 0); |
| 1554 | blob_init(&sql, 0, 0); |
| 1555 | blob_append_sql(&sql, |
| 1556 | "INSERT INTO user(login,pw,cap,info,mtime)\n" |
| 1557 | "VALUES(%Q,%Q,%Q," |
| 1558 | "'%q <%q>\nself-register from ip %q on '||datetime('now'),now())", |
| 1559 | zUserID, zPass, db_get("default-perms","u"), zDName, zEAddr, g.zIpAddr); |
| 1560 | fossil_free(zPass); |
| 1561 | db_multi_exec("%s", blob_sql_text(&sql)); |
| 1562 | uid = db_int(0, "SELECT uid FROM user WHERE login=%Q", zUserID); |
| 1563 | login_set_user_cookie(zUserID, uid, NULL); |
| 1564 | redirect_to_g(); |
| 1565 | } |
| 1566 | |
| 1567 | /* Prepare the captcha. */ |
| 1568 | uSeed = captcha_seed(); |
| 1569 | zDecoded = captcha_decode(uSeed); |
| @@ -1543,31 +1572,53 @@ | |
| 1572 | /* Print out the registration form. */ |
| 1573 | form_begin(0, "%R/register"); |
| 1574 | if( P("g") ){ |
| 1575 | @ <input type="hidden" name="g" value="%h(P("g"))" /> |
| 1576 | } |
| 1577 | @ <p><input type="hidden" name="captchaseed" value="%u(uSeed)" /> |
| 1578 | @ <table class="login_out"> |
| 1579 | @ <tr> |
| 1580 | @ <td class="form_label" align="right">User ID:</td> |
| 1581 | @ <td><input type="text" name="u" value="%h(zUserID)" size="30"></td> |
| 1582 | if( iErrLine==1 ){ |
| 1583 | @ <td><span class='loginError'>← %h(zErr)</span></td> |
| 1584 | } |
| 1585 | @ </tr> |
| 1586 | @ <tr> |
| 1587 | @ <td class="form_label" align="right">Display Name:</td> |
| 1588 | @ <td><input type="text" name="dn" value="%h(zDName)" size="30"></td> |
| 1589 | if( iErrLine==2 ){ |
| 1590 | @ <td><span class='loginError'>← %h(zErr)</span></td> |
| 1591 | } |
| 1592 | @ </tr> |
| 1593 | @ <tr> |
| 1594 | @ <td class="form_label" align="right">Email Address:</td> |
| 1595 | @ <td><input type="text" name="ea" value="%h(zEAddr)" size="30"></td> |
| 1596 | if( iErrLine==3 ){ |
| 1597 | @ <td><span class='loginError'>← %h(zErr)</span></td> |
| 1598 | } |
| 1599 | @ </tr> |
| 1600 | @ <tr> |
| 1601 | @ <td class="form_label" align="right">Password:</td> |
| 1602 | @ <td><input type="password" name="p" value="%h(zPasswd)" size="30"></td> |
| 1603 | if( iErrLine==4 ){ |
| 1604 | @ <td><span class='loginError'>← %h(zErr)</span></td> |
| 1605 | } |
| 1606 | @ </tr> |
| 1607 | @ <tr> |
| 1608 | @ <td class="form_label" align="right">Confirm password:</td> |
| 1609 | @ <td><input type="password" name="cp" value="%h(zConfirm)" size="30"></td> |
| 1610 | if( iErrLine==5 ){ |
| 1611 | @ <td><span class='loginError'>← %h(zErr)</span></td> |
| 1612 | } |
| 1613 | @ </tr> |
| 1614 | @ <tr> |
| 1615 | @ <td class="form_label" align="right">Captcha text (below):</td> |
| 1616 | @ <td><input type="text" name="captcha" value="" size="30"></td> |
| 1617 | if( iErrLine==6 ){ |
| 1618 | @ <td><span class='loginError'>← %h(zErr)</span></td> |
| 1619 | } |
| 1620 | @ </tr> |
| 1621 | @ <tr><td></td> |
| 1622 | @ <td><input type="submit" name="new" value="Register" /></td></tr> |
| 1623 | @ </table> |
| 1624 | @ <div class="captcha"><table class="captcha"><tr><td><pre> |
| 1625 |
+24
-13
| --- src/main.c | ||
| +++ src/main.c | ||
| @@ -90,11 +90,11 @@ | ||
| 90 | 90 | char WrUnver; /* y: can push unversioned content */ |
| 91 | 91 | char RdForum; /* 2: Read forum posts */ |
| 92 | 92 | char WrForum; /* 3: Create new forum posts */ |
| 93 | 93 | char WrTForum; /* 4: Post to forums not subject to moderation */ |
| 94 | 94 | char ModForum; /* 5: Moderate (approve or reject) forum posts */ |
| 95 | - char AdminForum; /* 6: Edit forum posts by other users */ | |
| 95 | + char AdminForum; /* 6: Set or remove capability 4 on other users */ | |
| 96 | 96 | char EmailAlert; /* 7: Sign up for email notifications */ |
| 97 | 97 | char Announce; /* A: Send announcements */ |
| 98 | 98 | char Debug; /* D: show extra Fossil debugging features */ |
| 99 | 99 | }; |
| 100 | 100 | |
| @@ -640,11 +640,11 @@ | ||
| 640 | 640 | if( g.zVfsName ){ |
| 641 | 641 | sqlite3_vfs *pVfs = sqlite3_vfs_find(g.zVfsName); |
| 642 | 642 | if( pVfs ){ |
| 643 | 643 | sqlite3_vfs_register(pVfs, 1); |
| 644 | 644 | }else{ |
| 645 | - fossil_panic("no such VFS: \"%s\"", g.zVfsName); | |
| 645 | + fossil_fatal("no such VFS: \"%s\"", g.zVfsName); | |
| 646 | 646 | } |
| 647 | 647 | } |
| 648 | 648 | if( fossil_getenv("GATEWAY_INTERFACE")!=0 && !find_option("nocgi", 0, 0)){ |
| 649 | 649 | zCmdName = "cgi"; |
| 650 | 650 | g.isHTTP = 1; |
| @@ -691,11 +691,11 @@ | ||
| 691 | 691 | g.zErrlog = find_option("errorlog", 0, 1); |
| 692 | 692 | fossil_init_flags_from_options(); |
| 693 | 693 | if( find_option("utc",0,0) ) g.fTimeFormat = 1; |
| 694 | 694 | if( find_option("localtime",0,0) ) g.fTimeFormat = 2; |
| 695 | 695 | if( zChdir && file_chdir(zChdir, 0) ){ |
| 696 | - fossil_panic("unable to change directories to %s", zChdir); | |
| 696 | + fossil_fatal("unable to change directories to %s", zChdir); | |
| 697 | 697 | } |
| 698 | 698 | if( find_option("help",0,0)!=0 ){ |
| 699 | 699 | /* If --help is found anywhere on the command line, translate the command |
| 700 | 700 | * to "fossil help cmdname" where "cmdname" is the first argument that |
| 701 | 701 | * does not begin with a "-" character. If all arguments start with "-", |
| @@ -756,11 +756,11 @@ | ||
| 756 | 756 | rc = TH_OK; |
| 757 | 757 | } |
| 758 | 758 | if( rc==TH_OK || rc==TH_RETURN || rc==TH_CONTINUE ){ |
| 759 | 759 | if( rc==TH_OK || rc==TH_RETURN ){ |
| 760 | 760 | #endif |
| 761 | - fossil_panic("%s: unknown command: %s\n" | |
| 761 | + fossil_fatal("%s: unknown command: %s\n" | |
| 762 | 762 | "%s: use \"help\" for more information", |
| 763 | 763 | g.argv[0], zCmdName, g.argv[0]); |
| 764 | 764 | #ifdef FOSSIL_ENABLE_TH1_HOOKS |
| 765 | 765 | } |
| 766 | 766 | if( !g.isHTTP && !g.fNoThHook && (rc==TH_OK || rc==TH_CONTINUE) ){ |
| @@ -821,11 +821,11 @@ | ||
| 821 | 821 | |
| 822 | 822 | /* |
| 823 | 823 | ** Print a usage comment and quit |
| 824 | 824 | */ |
| 825 | 825 | void usage(const char *zFormat){ |
| 826 | - fossil_panic("Usage: %s %s %s", g.argv[0], g.argv[1], zFormat); | |
| 826 | + fossil_fatal("Usage: %s %s %s", g.argv[0], g.argv[1], zFormat); | |
| 827 | 827 | } |
| 828 | 828 | |
| 829 | 829 | /* |
| 830 | 830 | ** Remove n elements from g.argv beginning with the i-th element. |
| 831 | 831 | */ |
| @@ -939,11 +939,11 @@ | ||
| 939 | 939 | */ |
| 940 | 940 | void verify_all_options(void){ |
| 941 | 941 | int i; |
| 942 | 942 | for(i=1; i<g.argc; i++){ |
| 943 | 943 | if( g.argv[i][0]=='-' && g.argv[i][1]!=0 ){ |
| 944 | - fossil_panic( | |
| 944 | + fossil_fatal( | |
| 945 | 945 | "unrecognized command-line option, or missing argument: %s", |
| 946 | 946 | g.argv[i]); |
| 947 | 947 | } |
| 948 | 948 | } |
| 949 | 949 | } |
| @@ -1158,11 +1158,11 @@ | ||
| 1158 | 1158 | g.zHttpsURL = mprintf("https://%s", &g.zTop[7]); |
| 1159 | 1159 | }else if( strncmp(g.zTop, "https://", 8)==0 ){ |
| 1160 | 1160 | /* it is already HTTPS, use it. */ |
| 1161 | 1161 | g.zHttpsURL = mprintf("%s", g.zTop); |
| 1162 | 1162 | }else{ |
| 1163 | - fossil_panic("argument to --baseurl should be 'http://host/path'" | |
| 1163 | + fossil_fatal("argument to --baseurl should be 'http://host/path'" | |
| 1164 | 1164 | " or 'https://host/path'"); |
| 1165 | 1165 | } |
| 1166 | 1166 | for(i=n=0; (c = g.zTop[i])!=0; i++){ |
| 1167 | 1167 | if( c=='/' ){ |
| 1168 | 1168 | n++; |
| @@ -1171,11 +1171,11 @@ | ||
| 1171 | 1171 | break; |
| 1172 | 1172 | } |
| 1173 | 1173 | } |
| 1174 | 1174 | } |
| 1175 | 1175 | if( g.zTop==g.zBaseURL ){ |
| 1176 | - fossil_panic("argument to --baseurl should be 'http://host/path'" | |
| 1176 | + fossil_fatal("argument to --baseurl should be 'http://host/path'" | |
| 1177 | 1177 | " or 'https://host/path'"); |
| 1178 | 1178 | } |
| 1179 | 1179 | if( g.zTop[1]==0 ) g.zTop++; |
| 1180 | 1180 | }else{ |
| 1181 | 1181 | zHost = PD("HTTP_HOST",""); |
| @@ -1260,16 +1260,16 @@ | ||
| 1260 | 1260 | } |
| 1261 | 1261 | zRepo = &zDir[i]; |
| 1262 | 1262 | } |
| 1263 | 1263 | } |
| 1264 | 1264 | if( stat(zRepo, &sStat)!=0 ){ |
| 1265 | - fossil_panic("cannot stat() repository: %s", zRepo); | |
| 1265 | + fossil_fatal("cannot stat() repository: %s", zRepo); | |
| 1266 | 1266 | } |
| 1267 | 1267 | i = setgid(sStat.st_gid); |
| 1268 | 1268 | i = i || setuid(sStat.st_uid); |
| 1269 | 1269 | if(i){ |
| 1270 | - fossil_panic("setgid/uid() failed with errno %d", errno); | |
| 1270 | + fossil_fatal("setgid/uid() failed with errno %d", errno); | |
| 1271 | 1271 | } |
| 1272 | 1272 | if( g.db==0 && file_isfile(zRepo, ExtFILE) ){ |
| 1273 | 1273 | db_open_repository(zRepo); |
| 1274 | 1274 | } |
| 1275 | 1275 | } |
| @@ -2251,11 +2251,11 @@ | ||
| 2251 | 2251 | ){ |
| 2252 | 2252 | unsigned int nSize = 0; |
| 2253 | 2253 | if( sscanf(zPidKey, "%lu:%p:%u", pProcessId, ppAddress, &nSize)==3 ){ |
| 2254 | 2254 | *pnSize = (SIZE_T)nSize; |
| 2255 | 2255 | }else{ |
| 2256 | - fossil_panic("failed to parse pid key"); | |
| 2256 | + fossil_fatal("failed to parse pid key"); | |
| 2257 | 2257 | } |
| 2258 | 2258 | } |
| 2259 | 2259 | #endif |
| 2260 | 2260 | |
| 2261 | 2261 | /* |
| @@ -2702,11 +2702,11 @@ | ||
| 2702 | 2702 | } |
| 2703 | 2703 | if( g.repositoryOpen ) flags |= HTTP_SERVER_HAD_REPOSITORY; |
| 2704 | 2704 | if( g.localOpen ) flags |= HTTP_SERVER_HAD_CHECKOUT; |
| 2705 | 2705 | db_close(1); |
| 2706 | 2706 | if( cgi_http_server(iPort, mxPort, zBrowserCmd, zIpAddr, flags) ){ |
| 2707 | - fossil_panic("unable to listen on TCP socket %d", iPort); | |
| 2707 | + fossil_fatal("unable to listen on TCP socket %d", iPort); | |
| 2708 | 2708 | } |
| 2709 | 2709 | if( zMaxLatency ){ |
| 2710 | 2710 | signal(SIGALRM, sigalrm_handler); |
| 2711 | 2711 | alarm(atoi(zMaxLatency)); |
| 2712 | 2712 | } |
| @@ -2806,10 +2806,12 @@ | ||
| 2806 | 2806 | ** case=1 Issue a fossil_warning() while generating the page. |
| 2807 | 2807 | ** case=2 Extra db_begin_transaction() |
| 2808 | 2808 | ** case=3 Extra db_end_transaction() |
| 2809 | 2809 | ** case=4 Error during SQL processing |
| 2810 | 2810 | ** case=5 Call the segfault handler |
| 2811 | +** case=6 Call webpage_assert() | |
| 2812 | +** case=7 Call webpage_error() | |
| 2811 | 2813 | */ |
| 2812 | 2814 | void test_warning_page(void){ |
| 2813 | 2815 | int iCase = atoi(PD("case","0")); |
| 2814 | 2816 | int i; |
| 2815 | 2817 | login_check_credentials(); |
| @@ -2823,11 +2825,11 @@ | ||
| 2823 | 2825 | @ <p>Generate a message to the <a href="%R/errorlog">error log</a> |
| 2824 | 2826 | @ by clicking on one of the following cases: |
| 2825 | 2827 | }else{ |
| 2826 | 2828 | @ <p>This is the test page for case=%d(iCase). All possible cases: |
| 2827 | 2829 | } |
| 2828 | - for(i=1; i<=5; i++){ | |
| 2830 | + for(i=1; i<=7; i++){ | |
| 2829 | 2831 | @ <a href='./test-warning?case=%d(i)'>[%d(i)]</a> |
| 2830 | 2832 | } |
| 2831 | 2833 | @ </p> |
| 2832 | 2834 | @ <p><ol> |
| 2833 | 2835 | @ <li value='1'> Call fossil_warning() |
| @@ -2852,9 +2854,18 @@ | ||
| 2852 | 2854 | } |
| 2853 | 2855 | @ <li value='5'> simulate segfault handling |
| 2854 | 2856 | if( iCase==5 ){ |
| 2855 | 2857 | sigsegv_handler(0); |
| 2856 | 2858 | } |
| 2859 | + @ <li value='6'> call webpage_assert(0) | |
| 2860 | + if( iCase==6 ){ | |
| 2861 | + webpage_assert( 5==7 ); | |
| 2862 | + } | |
| 2863 | + @ <li value='7'> call webpage_error()" | |
| 2864 | + if( iCase==7 ){ | |
| 2865 | + cgi_reset_content(); | |
| 2866 | + webpage_error("Case 7 from /test-warning"); | |
| 2867 | + } | |
| 2857 | 2868 | @ </ol> |
| 2858 | 2869 | @ <p>End of test</p> |
| 2859 | 2870 | style_footer(); |
| 2860 | 2871 | } |
| 2861 | 2872 |
| --- src/main.c | |
| +++ src/main.c | |
| @@ -90,11 +90,11 @@ | |
| 90 | char WrUnver; /* y: can push unversioned content */ |
| 91 | char RdForum; /* 2: Read forum posts */ |
| 92 | char WrForum; /* 3: Create new forum posts */ |
| 93 | char WrTForum; /* 4: Post to forums not subject to moderation */ |
| 94 | char ModForum; /* 5: Moderate (approve or reject) forum posts */ |
| 95 | char AdminForum; /* 6: Edit forum posts by other users */ |
| 96 | char EmailAlert; /* 7: Sign up for email notifications */ |
| 97 | char Announce; /* A: Send announcements */ |
| 98 | char Debug; /* D: show extra Fossil debugging features */ |
| 99 | }; |
| 100 | |
| @@ -640,11 +640,11 @@ | |
| 640 | if( g.zVfsName ){ |
| 641 | sqlite3_vfs *pVfs = sqlite3_vfs_find(g.zVfsName); |
| 642 | if( pVfs ){ |
| 643 | sqlite3_vfs_register(pVfs, 1); |
| 644 | }else{ |
| 645 | fossil_panic("no such VFS: \"%s\"", g.zVfsName); |
| 646 | } |
| 647 | } |
| 648 | if( fossil_getenv("GATEWAY_INTERFACE")!=0 && !find_option("nocgi", 0, 0)){ |
| 649 | zCmdName = "cgi"; |
| 650 | g.isHTTP = 1; |
| @@ -691,11 +691,11 @@ | |
| 691 | g.zErrlog = find_option("errorlog", 0, 1); |
| 692 | fossil_init_flags_from_options(); |
| 693 | if( find_option("utc",0,0) ) g.fTimeFormat = 1; |
| 694 | if( find_option("localtime",0,0) ) g.fTimeFormat = 2; |
| 695 | if( zChdir && file_chdir(zChdir, 0) ){ |
| 696 | fossil_panic("unable to change directories to %s", zChdir); |
| 697 | } |
| 698 | if( find_option("help",0,0)!=0 ){ |
| 699 | /* If --help is found anywhere on the command line, translate the command |
| 700 | * to "fossil help cmdname" where "cmdname" is the first argument that |
| 701 | * does not begin with a "-" character. If all arguments start with "-", |
| @@ -756,11 +756,11 @@ | |
| 756 | rc = TH_OK; |
| 757 | } |
| 758 | if( rc==TH_OK || rc==TH_RETURN || rc==TH_CONTINUE ){ |
| 759 | if( rc==TH_OK || rc==TH_RETURN ){ |
| 760 | #endif |
| 761 | fossil_panic("%s: unknown command: %s\n" |
| 762 | "%s: use \"help\" for more information", |
| 763 | g.argv[0], zCmdName, g.argv[0]); |
| 764 | #ifdef FOSSIL_ENABLE_TH1_HOOKS |
| 765 | } |
| 766 | if( !g.isHTTP && !g.fNoThHook && (rc==TH_OK || rc==TH_CONTINUE) ){ |
| @@ -821,11 +821,11 @@ | |
| 821 | |
| 822 | /* |
| 823 | ** Print a usage comment and quit |
| 824 | */ |
| 825 | void usage(const char *zFormat){ |
| 826 | fossil_panic("Usage: %s %s %s", g.argv[0], g.argv[1], zFormat); |
| 827 | } |
| 828 | |
| 829 | /* |
| 830 | ** Remove n elements from g.argv beginning with the i-th element. |
| 831 | */ |
| @@ -939,11 +939,11 @@ | |
| 939 | */ |
| 940 | void verify_all_options(void){ |
| 941 | int i; |
| 942 | for(i=1; i<g.argc; i++){ |
| 943 | if( g.argv[i][0]=='-' && g.argv[i][1]!=0 ){ |
| 944 | fossil_panic( |
| 945 | "unrecognized command-line option, or missing argument: %s", |
| 946 | g.argv[i]); |
| 947 | } |
| 948 | } |
| 949 | } |
| @@ -1158,11 +1158,11 @@ | |
| 1158 | g.zHttpsURL = mprintf("https://%s", &g.zTop[7]); |
| 1159 | }else if( strncmp(g.zTop, "https://", 8)==0 ){ |
| 1160 | /* it is already HTTPS, use it. */ |
| 1161 | g.zHttpsURL = mprintf("%s", g.zTop); |
| 1162 | }else{ |
| 1163 | fossil_panic("argument to --baseurl should be 'http://host/path'" |
| 1164 | " or 'https://host/path'"); |
| 1165 | } |
| 1166 | for(i=n=0; (c = g.zTop[i])!=0; i++){ |
| 1167 | if( c=='/' ){ |
| 1168 | n++; |
| @@ -1171,11 +1171,11 @@ | |
| 1171 | break; |
| 1172 | } |
| 1173 | } |
| 1174 | } |
| 1175 | if( g.zTop==g.zBaseURL ){ |
| 1176 | fossil_panic("argument to --baseurl should be 'http://host/path'" |
| 1177 | " or 'https://host/path'"); |
| 1178 | } |
| 1179 | if( g.zTop[1]==0 ) g.zTop++; |
| 1180 | }else{ |
| 1181 | zHost = PD("HTTP_HOST",""); |
| @@ -1260,16 +1260,16 @@ | |
| 1260 | } |
| 1261 | zRepo = &zDir[i]; |
| 1262 | } |
| 1263 | } |
| 1264 | if( stat(zRepo, &sStat)!=0 ){ |
| 1265 | fossil_panic("cannot stat() repository: %s", zRepo); |
| 1266 | } |
| 1267 | i = setgid(sStat.st_gid); |
| 1268 | i = i || setuid(sStat.st_uid); |
| 1269 | if(i){ |
| 1270 | fossil_panic("setgid/uid() failed with errno %d", errno); |
| 1271 | } |
| 1272 | if( g.db==0 && file_isfile(zRepo, ExtFILE) ){ |
| 1273 | db_open_repository(zRepo); |
| 1274 | } |
| 1275 | } |
| @@ -2251,11 +2251,11 @@ | |
| 2251 | ){ |
| 2252 | unsigned int nSize = 0; |
| 2253 | if( sscanf(zPidKey, "%lu:%p:%u", pProcessId, ppAddress, &nSize)==3 ){ |
| 2254 | *pnSize = (SIZE_T)nSize; |
| 2255 | }else{ |
| 2256 | fossil_panic("failed to parse pid key"); |
| 2257 | } |
| 2258 | } |
| 2259 | #endif |
| 2260 | |
| 2261 | /* |
| @@ -2702,11 +2702,11 @@ | |
| 2702 | } |
| 2703 | if( g.repositoryOpen ) flags |= HTTP_SERVER_HAD_REPOSITORY; |
| 2704 | if( g.localOpen ) flags |= HTTP_SERVER_HAD_CHECKOUT; |
| 2705 | db_close(1); |
| 2706 | if( cgi_http_server(iPort, mxPort, zBrowserCmd, zIpAddr, flags) ){ |
| 2707 | fossil_panic("unable to listen on TCP socket %d", iPort); |
| 2708 | } |
| 2709 | if( zMaxLatency ){ |
| 2710 | signal(SIGALRM, sigalrm_handler); |
| 2711 | alarm(atoi(zMaxLatency)); |
| 2712 | } |
| @@ -2806,10 +2806,12 @@ | |
| 2806 | ** case=1 Issue a fossil_warning() while generating the page. |
| 2807 | ** case=2 Extra db_begin_transaction() |
| 2808 | ** case=3 Extra db_end_transaction() |
| 2809 | ** case=4 Error during SQL processing |
| 2810 | ** case=5 Call the segfault handler |
| 2811 | */ |
| 2812 | void test_warning_page(void){ |
| 2813 | int iCase = atoi(PD("case","0")); |
| 2814 | int i; |
| 2815 | login_check_credentials(); |
| @@ -2823,11 +2825,11 @@ | |
| 2823 | @ <p>Generate a message to the <a href="%R/errorlog">error log</a> |
| 2824 | @ by clicking on one of the following cases: |
| 2825 | }else{ |
| 2826 | @ <p>This is the test page for case=%d(iCase). All possible cases: |
| 2827 | } |
| 2828 | for(i=1; i<=5; i++){ |
| 2829 | @ <a href='./test-warning?case=%d(i)'>[%d(i)]</a> |
| 2830 | } |
| 2831 | @ </p> |
| 2832 | @ <p><ol> |
| 2833 | @ <li value='1'> Call fossil_warning() |
| @@ -2852,9 +2854,18 @@ | |
| 2852 | } |
| 2853 | @ <li value='5'> simulate segfault handling |
| 2854 | if( iCase==5 ){ |
| 2855 | sigsegv_handler(0); |
| 2856 | } |
| 2857 | @ </ol> |
| 2858 | @ <p>End of test</p> |
| 2859 | style_footer(); |
| 2860 | } |
| 2861 |
| --- src/main.c | |
| +++ src/main.c | |
| @@ -90,11 +90,11 @@ | |
| 90 | char WrUnver; /* y: can push unversioned content */ |
| 91 | char RdForum; /* 2: Read forum posts */ |
| 92 | char WrForum; /* 3: Create new forum posts */ |
| 93 | char WrTForum; /* 4: Post to forums not subject to moderation */ |
| 94 | char ModForum; /* 5: Moderate (approve or reject) forum posts */ |
| 95 | char AdminForum; /* 6: Set or remove capability 4 on other users */ |
| 96 | char EmailAlert; /* 7: Sign up for email notifications */ |
| 97 | char Announce; /* A: Send announcements */ |
| 98 | char Debug; /* D: show extra Fossil debugging features */ |
| 99 | }; |
| 100 | |
| @@ -640,11 +640,11 @@ | |
| 640 | if( g.zVfsName ){ |
| 641 | sqlite3_vfs *pVfs = sqlite3_vfs_find(g.zVfsName); |
| 642 | if( pVfs ){ |
| 643 | sqlite3_vfs_register(pVfs, 1); |
| 644 | }else{ |
| 645 | fossil_fatal("no such VFS: \"%s\"", g.zVfsName); |
| 646 | } |
| 647 | } |
| 648 | if( fossil_getenv("GATEWAY_INTERFACE")!=0 && !find_option("nocgi", 0, 0)){ |
| 649 | zCmdName = "cgi"; |
| 650 | g.isHTTP = 1; |
| @@ -691,11 +691,11 @@ | |
| 691 | g.zErrlog = find_option("errorlog", 0, 1); |
| 692 | fossil_init_flags_from_options(); |
| 693 | if( find_option("utc",0,0) ) g.fTimeFormat = 1; |
| 694 | if( find_option("localtime",0,0) ) g.fTimeFormat = 2; |
| 695 | if( zChdir && file_chdir(zChdir, 0) ){ |
| 696 | fossil_fatal("unable to change directories to %s", zChdir); |
| 697 | } |
| 698 | if( find_option("help",0,0)!=0 ){ |
| 699 | /* If --help is found anywhere on the command line, translate the command |
| 700 | * to "fossil help cmdname" where "cmdname" is the first argument that |
| 701 | * does not begin with a "-" character. If all arguments start with "-", |
| @@ -756,11 +756,11 @@ | |
| 756 | rc = TH_OK; |
| 757 | } |
| 758 | if( rc==TH_OK || rc==TH_RETURN || rc==TH_CONTINUE ){ |
| 759 | if( rc==TH_OK || rc==TH_RETURN ){ |
| 760 | #endif |
| 761 | fossil_fatal("%s: unknown command: %s\n" |
| 762 | "%s: use \"help\" for more information", |
| 763 | g.argv[0], zCmdName, g.argv[0]); |
| 764 | #ifdef FOSSIL_ENABLE_TH1_HOOKS |
| 765 | } |
| 766 | if( !g.isHTTP && !g.fNoThHook && (rc==TH_OK || rc==TH_CONTINUE) ){ |
| @@ -821,11 +821,11 @@ | |
| 821 | |
| 822 | /* |
| 823 | ** Print a usage comment and quit |
| 824 | */ |
| 825 | void usage(const char *zFormat){ |
| 826 | fossil_fatal("Usage: %s %s %s", g.argv[0], g.argv[1], zFormat); |
| 827 | } |
| 828 | |
| 829 | /* |
| 830 | ** Remove n elements from g.argv beginning with the i-th element. |
| 831 | */ |
| @@ -939,11 +939,11 @@ | |
| 939 | */ |
| 940 | void verify_all_options(void){ |
| 941 | int i; |
| 942 | for(i=1; i<g.argc; i++){ |
| 943 | if( g.argv[i][0]=='-' && g.argv[i][1]!=0 ){ |
| 944 | fossil_fatal( |
| 945 | "unrecognized command-line option, or missing argument: %s", |
| 946 | g.argv[i]); |
| 947 | } |
| 948 | } |
| 949 | } |
| @@ -1158,11 +1158,11 @@ | |
| 1158 | g.zHttpsURL = mprintf("https://%s", &g.zTop[7]); |
| 1159 | }else if( strncmp(g.zTop, "https://", 8)==0 ){ |
| 1160 | /* it is already HTTPS, use it. */ |
| 1161 | g.zHttpsURL = mprintf("%s", g.zTop); |
| 1162 | }else{ |
| 1163 | fossil_fatal("argument to --baseurl should be 'http://host/path'" |
| 1164 | " or 'https://host/path'"); |
| 1165 | } |
| 1166 | for(i=n=0; (c = g.zTop[i])!=0; i++){ |
| 1167 | if( c=='/' ){ |
| 1168 | n++; |
| @@ -1171,11 +1171,11 @@ | |
| 1171 | break; |
| 1172 | } |
| 1173 | } |
| 1174 | } |
| 1175 | if( g.zTop==g.zBaseURL ){ |
| 1176 | fossil_fatal("argument to --baseurl should be 'http://host/path'" |
| 1177 | " or 'https://host/path'"); |
| 1178 | } |
| 1179 | if( g.zTop[1]==0 ) g.zTop++; |
| 1180 | }else{ |
| 1181 | zHost = PD("HTTP_HOST",""); |
| @@ -1260,16 +1260,16 @@ | |
| 1260 | } |
| 1261 | zRepo = &zDir[i]; |
| 1262 | } |
| 1263 | } |
| 1264 | if( stat(zRepo, &sStat)!=0 ){ |
| 1265 | fossil_fatal("cannot stat() repository: %s", zRepo); |
| 1266 | } |
| 1267 | i = setgid(sStat.st_gid); |
| 1268 | i = i || setuid(sStat.st_uid); |
| 1269 | if(i){ |
| 1270 | fossil_fatal("setgid/uid() failed with errno %d", errno); |
| 1271 | } |
| 1272 | if( g.db==0 && file_isfile(zRepo, ExtFILE) ){ |
| 1273 | db_open_repository(zRepo); |
| 1274 | } |
| 1275 | } |
| @@ -2251,11 +2251,11 @@ | |
| 2251 | ){ |
| 2252 | unsigned int nSize = 0; |
| 2253 | if( sscanf(zPidKey, "%lu:%p:%u", pProcessId, ppAddress, &nSize)==3 ){ |
| 2254 | *pnSize = (SIZE_T)nSize; |
| 2255 | }else{ |
| 2256 | fossil_fatal("failed to parse pid key"); |
| 2257 | } |
| 2258 | } |
| 2259 | #endif |
| 2260 | |
| 2261 | /* |
| @@ -2702,11 +2702,11 @@ | |
| 2702 | } |
| 2703 | if( g.repositoryOpen ) flags |= HTTP_SERVER_HAD_REPOSITORY; |
| 2704 | if( g.localOpen ) flags |= HTTP_SERVER_HAD_CHECKOUT; |
| 2705 | db_close(1); |
| 2706 | if( cgi_http_server(iPort, mxPort, zBrowserCmd, zIpAddr, flags) ){ |
| 2707 | fossil_fatal("unable to listen on TCP socket %d", iPort); |
| 2708 | } |
| 2709 | if( zMaxLatency ){ |
| 2710 | signal(SIGALRM, sigalrm_handler); |
| 2711 | alarm(atoi(zMaxLatency)); |
| 2712 | } |
| @@ -2806,10 +2806,12 @@ | |
| 2806 | ** case=1 Issue a fossil_warning() while generating the page. |
| 2807 | ** case=2 Extra db_begin_transaction() |
| 2808 | ** case=3 Extra db_end_transaction() |
| 2809 | ** case=4 Error during SQL processing |
| 2810 | ** case=5 Call the segfault handler |
| 2811 | ** case=6 Call webpage_assert() |
| 2812 | ** case=7 Call webpage_error() |
| 2813 | */ |
| 2814 | void test_warning_page(void){ |
| 2815 | int iCase = atoi(PD("case","0")); |
| 2816 | int i; |
| 2817 | login_check_credentials(); |
| @@ -2823,11 +2825,11 @@ | |
| 2825 | @ <p>Generate a message to the <a href="%R/errorlog">error log</a> |
| 2826 | @ by clicking on one of the following cases: |
| 2827 | }else{ |
| 2828 | @ <p>This is the test page for case=%d(iCase). All possible cases: |
| 2829 | } |
| 2830 | for(i=1; i<=7; i++){ |
| 2831 | @ <a href='./test-warning?case=%d(i)'>[%d(i)]</a> |
| 2832 | } |
| 2833 | @ </p> |
| 2834 | @ <p><ol> |
| 2835 | @ <li value='1'> Call fossil_warning() |
| @@ -2852,9 +2854,18 @@ | |
| 2854 | } |
| 2855 | @ <li value='5'> simulate segfault handling |
| 2856 | if( iCase==5 ){ |
| 2857 | sigsegv_handler(0); |
| 2858 | } |
| 2859 | @ <li value='6'> call webpage_assert(0) |
| 2860 | if( iCase==6 ){ |
| 2861 | webpage_assert( 5==7 ); |
| 2862 | } |
| 2863 | @ <li value='7'> call webpage_error()" |
| 2864 | if( iCase==7 ){ |
| 2865 | cgi_reset_content(); |
| 2866 | webpage_error("Case 7 from /test-warning"); |
| 2867 | } |
| 2868 | @ </ol> |
| 2869 | @ <p>End of test</p> |
| 2870 | style_footer(); |
| 2871 | } |
| 2872 |
+13
| --- src/main.mk | ||
| +++ src/main.mk | ||
| @@ -25,10 +25,11 @@ | ||
| 25 | 25 | $(SRCDIR)/branch.c \ |
| 26 | 26 | $(SRCDIR)/browse.c \ |
| 27 | 27 | $(SRCDIR)/builtin.c \ |
| 28 | 28 | $(SRCDIR)/bundle.c \ |
| 29 | 29 | $(SRCDIR)/cache.c \ |
| 30 | + $(SRCDIR)/capabilities.c \ | |
| 30 | 31 | $(SRCDIR)/captcha.c \ |
| 31 | 32 | $(SRCDIR)/cgi.c \ |
| 32 | 33 | $(SRCDIR)/checkin.c \ |
| 33 | 34 | $(SRCDIR)/checkout.c \ |
| 34 | 35 | $(SRCDIR)/clearsign.c \ |
| @@ -205,10 +206,11 @@ | ||
| 205 | 206 | $(SRCDIR)/../skins/xekri/details.txt \ |
| 206 | 207 | $(SRCDIR)/../skins/xekri/footer.txt \ |
| 207 | 208 | $(SRCDIR)/../skins/xekri/header.txt \ |
| 208 | 209 | $(SRCDIR)/ci_edit.js \ |
| 209 | 210 | $(SRCDIR)/diff.tcl \ |
| 211 | + $(SRCDIR)/forum.js \ | |
| 210 | 212 | $(SRCDIR)/graph.js \ |
| 211 | 213 | $(SRCDIR)/href.js \ |
| 212 | 214 | $(SRCDIR)/login.js \ |
| 213 | 215 | $(SRCDIR)/markdown.md \ |
| 214 | 216 | $(SRCDIR)/menu.js \ |
| @@ -231,10 +233,11 @@ | ||
| 231 | 233 | $(OBJDIR)/branch_.c \ |
| 232 | 234 | $(OBJDIR)/browse_.c \ |
| 233 | 235 | $(OBJDIR)/builtin_.c \ |
| 234 | 236 | $(OBJDIR)/bundle_.c \ |
| 235 | 237 | $(OBJDIR)/cache_.c \ |
| 238 | + $(OBJDIR)/capabilities_.c \ | |
| 236 | 239 | $(OBJDIR)/captcha_.c \ |
| 237 | 240 | $(OBJDIR)/cgi_.c \ |
| 238 | 241 | $(OBJDIR)/checkin_.c \ |
| 239 | 242 | $(OBJDIR)/checkout_.c \ |
| 240 | 243 | $(OBJDIR)/clearsign_.c \ |
| @@ -366,10 +369,11 @@ | ||
| 366 | 369 | $(OBJDIR)/branch.o \ |
| 367 | 370 | $(OBJDIR)/browse.o \ |
| 368 | 371 | $(OBJDIR)/builtin.o \ |
| 369 | 372 | $(OBJDIR)/bundle.o \ |
| 370 | 373 | $(OBJDIR)/cache.o \ |
| 374 | + $(OBJDIR)/capabilities.o \ | |
| 371 | 375 | $(OBJDIR)/captcha.o \ |
| 372 | 376 | $(OBJDIR)/cgi.o \ |
| 373 | 377 | $(OBJDIR)/checkin.o \ |
| 374 | 378 | $(OBJDIR)/checkout.o \ |
| 375 | 379 | $(OBJDIR)/clearsign.o \ |
| @@ -699,10 +703,11 @@ | ||
| 699 | 703 | $(OBJDIR)/branch_.c:$(OBJDIR)/branch.h \ |
| 700 | 704 | $(OBJDIR)/browse_.c:$(OBJDIR)/browse.h \ |
| 701 | 705 | $(OBJDIR)/builtin_.c:$(OBJDIR)/builtin.h \ |
| 702 | 706 | $(OBJDIR)/bundle_.c:$(OBJDIR)/bundle.h \ |
| 703 | 707 | $(OBJDIR)/cache_.c:$(OBJDIR)/cache.h \ |
| 708 | + $(OBJDIR)/capabilities_.c:$(OBJDIR)/capabilities.h \ | |
| 704 | 709 | $(OBJDIR)/captcha_.c:$(OBJDIR)/captcha.h \ |
| 705 | 710 | $(OBJDIR)/cgi_.c:$(OBJDIR)/cgi.h \ |
| 706 | 711 | $(OBJDIR)/checkin_.c:$(OBJDIR)/checkin.h \ |
| 707 | 712 | $(OBJDIR)/checkout_.c:$(OBJDIR)/checkout.h \ |
| 708 | 713 | $(OBJDIR)/clearsign_.c:$(OBJDIR)/clearsign.h \ |
| @@ -922,10 +927,18 @@ | ||
| 922 | 927 | |
| 923 | 928 | $(OBJDIR)/cache.o: $(OBJDIR)/cache_.c $(OBJDIR)/cache.h $(SRCDIR)/config.h |
| 924 | 929 | $(XTCC) -o $(OBJDIR)/cache.o -c $(OBJDIR)/cache_.c |
| 925 | 930 | |
| 926 | 931 | $(OBJDIR)/cache.h: $(OBJDIR)/headers |
| 932 | + | |
| 933 | +$(OBJDIR)/capabilities_.c: $(SRCDIR)/capabilities.c $(OBJDIR)/translate | |
| 934 | + $(OBJDIR)/translate $(SRCDIR)/capabilities.c >$@ | |
| 935 | + | |
| 936 | +$(OBJDIR)/capabilities.o: $(OBJDIR)/capabilities_.c $(OBJDIR)/capabilities.h $(SRCDIR)/config.h | |
| 937 | + $(XTCC) -o $(OBJDIR)/capabilities.o -c $(OBJDIR)/capabilities_.c | |
| 938 | + | |
| 939 | +$(OBJDIR)/capabilities.h: $(OBJDIR)/headers | |
| 927 | 940 | |
| 928 | 941 | $(OBJDIR)/captcha_.c: $(SRCDIR)/captcha.c $(OBJDIR)/translate |
| 929 | 942 | $(OBJDIR)/translate $(SRCDIR)/captcha.c >$@ |
| 930 | 943 | |
| 931 | 944 | $(OBJDIR)/captcha.o: $(OBJDIR)/captcha_.c $(OBJDIR)/captcha.h $(SRCDIR)/config.h |
| 932 | 945 |
| --- src/main.mk | |
| +++ src/main.mk | |
| @@ -25,10 +25,11 @@ | |
| 25 | $(SRCDIR)/branch.c \ |
| 26 | $(SRCDIR)/browse.c \ |
| 27 | $(SRCDIR)/builtin.c \ |
| 28 | $(SRCDIR)/bundle.c \ |
| 29 | $(SRCDIR)/cache.c \ |
| 30 | $(SRCDIR)/captcha.c \ |
| 31 | $(SRCDIR)/cgi.c \ |
| 32 | $(SRCDIR)/checkin.c \ |
| 33 | $(SRCDIR)/checkout.c \ |
| 34 | $(SRCDIR)/clearsign.c \ |
| @@ -205,10 +206,11 @@ | |
| 205 | $(SRCDIR)/../skins/xekri/details.txt \ |
| 206 | $(SRCDIR)/../skins/xekri/footer.txt \ |
| 207 | $(SRCDIR)/../skins/xekri/header.txt \ |
| 208 | $(SRCDIR)/ci_edit.js \ |
| 209 | $(SRCDIR)/diff.tcl \ |
| 210 | $(SRCDIR)/graph.js \ |
| 211 | $(SRCDIR)/href.js \ |
| 212 | $(SRCDIR)/login.js \ |
| 213 | $(SRCDIR)/markdown.md \ |
| 214 | $(SRCDIR)/menu.js \ |
| @@ -231,10 +233,11 @@ | |
| 231 | $(OBJDIR)/branch_.c \ |
| 232 | $(OBJDIR)/browse_.c \ |
| 233 | $(OBJDIR)/builtin_.c \ |
| 234 | $(OBJDIR)/bundle_.c \ |
| 235 | $(OBJDIR)/cache_.c \ |
| 236 | $(OBJDIR)/captcha_.c \ |
| 237 | $(OBJDIR)/cgi_.c \ |
| 238 | $(OBJDIR)/checkin_.c \ |
| 239 | $(OBJDIR)/checkout_.c \ |
| 240 | $(OBJDIR)/clearsign_.c \ |
| @@ -366,10 +369,11 @@ | |
| 366 | $(OBJDIR)/branch.o \ |
| 367 | $(OBJDIR)/browse.o \ |
| 368 | $(OBJDIR)/builtin.o \ |
| 369 | $(OBJDIR)/bundle.o \ |
| 370 | $(OBJDIR)/cache.o \ |
| 371 | $(OBJDIR)/captcha.o \ |
| 372 | $(OBJDIR)/cgi.o \ |
| 373 | $(OBJDIR)/checkin.o \ |
| 374 | $(OBJDIR)/checkout.o \ |
| 375 | $(OBJDIR)/clearsign.o \ |
| @@ -699,10 +703,11 @@ | |
| 699 | $(OBJDIR)/branch_.c:$(OBJDIR)/branch.h \ |
| 700 | $(OBJDIR)/browse_.c:$(OBJDIR)/browse.h \ |
| 701 | $(OBJDIR)/builtin_.c:$(OBJDIR)/builtin.h \ |
| 702 | $(OBJDIR)/bundle_.c:$(OBJDIR)/bundle.h \ |
| 703 | $(OBJDIR)/cache_.c:$(OBJDIR)/cache.h \ |
| 704 | $(OBJDIR)/captcha_.c:$(OBJDIR)/captcha.h \ |
| 705 | $(OBJDIR)/cgi_.c:$(OBJDIR)/cgi.h \ |
| 706 | $(OBJDIR)/checkin_.c:$(OBJDIR)/checkin.h \ |
| 707 | $(OBJDIR)/checkout_.c:$(OBJDIR)/checkout.h \ |
| 708 | $(OBJDIR)/clearsign_.c:$(OBJDIR)/clearsign.h \ |
| @@ -922,10 +927,18 @@ | |
| 922 | |
| 923 | $(OBJDIR)/cache.o: $(OBJDIR)/cache_.c $(OBJDIR)/cache.h $(SRCDIR)/config.h |
| 924 | $(XTCC) -o $(OBJDIR)/cache.o -c $(OBJDIR)/cache_.c |
| 925 | |
| 926 | $(OBJDIR)/cache.h: $(OBJDIR)/headers |
| 927 | |
| 928 | $(OBJDIR)/captcha_.c: $(SRCDIR)/captcha.c $(OBJDIR)/translate |
| 929 | $(OBJDIR)/translate $(SRCDIR)/captcha.c >$@ |
| 930 | |
| 931 | $(OBJDIR)/captcha.o: $(OBJDIR)/captcha_.c $(OBJDIR)/captcha.h $(SRCDIR)/config.h |
| 932 |
| --- src/main.mk | |
| +++ src/main.mk | |
| @@ -25,10 +25,11 @@ | |
| 25 | $(SRCDIR)/branch.c \ |
| 26 | $(SRCDIR)/browse.c \ |
| 27 | $(SRCDIR)/builtin.c \ |
| 28 | $(SRCDIR)/bundle.c \ |
| 29 | $(SRCDIR)/cache.c \ |
| 30 | $(SRCDIR)/capabilities.c \ |
| 31 | $(SRCDIR)/captcha.c \ |
| 32 | $(SRCDIR)/cgi.c \ |
| 33 | $(SRCDIR)/checkin.c \ |
| 34 | $(SRCDIR)/checkout.c \ |
| 35 | $(SRCDIR)/clearsign.c \ |
| @@ -205,10 +206,11 @@ | |
| 206 | $(SRCDIR)/../skins/xekri/details.txt \ |
| 207 | $(SRCDIR)/../skins/xekri/footer.txt \ |
| 208 | $(SRCDIR)/../skins/xekri/header.txt \ |
| 209 | $(SRCDIR)/ci_edit.js \ |
| 210 | $(SRCDIR)/diff.tcl \ |
| 211 | $(SRCDIR)/forum.js \ |
| 212 | $(SRCDIR)/graph.js \ |
| 213 | $(SRCDIR)/href.js \ |
| 214 | $(SRCDIR)/login.js \ |
| 215 | $(SRCDIR)/markdown.md \ |
| 216 | $(SRCDIR)/menu.js \ |
| @@ -231,10 +233,11 @@ | |
| 233 | $(OBJDIR)/branch_.c \ |
| 234 | $(OBJDIR)/browse_.c \ |
| 235 | $(OBJDIR)/builtin_.c \ |
| 236 | $(OBJDIR)/bundle_.c \ |
| 237 | $(OBJDIR)/cache_.c \ |
| 238 | $(OBJDIR)/capabilities_.c \ |
| 239 | $(OBJDIR)/captcha_.c \ |
| 240 | $(OBJDIR)/cgi_.c \ |
| 241 | $(OBJDIR)/checkin_.c \ |
| 242 | $(OBJDIR)/checkout_.c \ |
| 243 | $(OBJDIR)/clearsign_.c \ |
| @@ -366,10 +369,11 @@ | |
| 369 | $(OBJDIR)/branch.o \ |
| 370 | $(OBJDIR)/browse.o \ |
| 371 | $(OBJDIR)/builtin.o \ |
| 372 | $(OBJDIR)/bundle.o \ |
| 373 | $(OBJDIR)/cache.o \ |
| 374 | $(OBJDIR)/capabilities.o \ |
| 375 | $(OBJDIR)/captcha.o \ |
| 376 | $(OBJDIR)/cgi.o \ |
| 377 | $(OBJDIR)/checkin.o \ |
| 378 | $(OBJDIR)/checkout.o \ |
| 379 | $(OBJDIR)/clearsign.o \ |
| @@ -699,10 +703,11 @@ | |
| 703 | $(OBJDIR)/branch_.c:$(OBJDIR)/branch.h \ |
| 704 | $(OBJDIR)/browse_.c:$(OBJDIR)/browse.h \ |
| 705 | $(OBJDIR)/builtin_.c:$(OBJDIR)/builtin.h \ |
| 706 | $(OBJDIR)/bundle_.c:$(OBJDIR)/bundle.h \ |
| 707 | $(OBJDIR)/cache_.c:$(OBJDIR)/cache.h \ |
| 708 | $(OBJDIR)/capabilities_.c:$(OBJDIR)/capabilities.h \ |
| 709 | $(OBJDIR)/captcha_.c:$(OBJDIR)/captcha.h \ |
| 710 | $(OBJDIR)/cgi_.c:$(OBJDIR)/cgi.h \ |
| 711 | $(OBJDIR)/checkin_.c:$(OBJDIR)/checkin.h \ |
| 712 | $(OBJDIR)/checkout_.c:$(OBJDIR)/checkout.h \ |
| 713 | $(OBJDIR)/clearsign_.c:$(OBJDIR)/clearsign.h \ |
| @@ -922,10 +927,18 @@ | |
| 927 | |
| 928 | $(OBJDIR)/cache.o: $(OBJDIR)/cache_.c $(OBJDIR)/cache.h $(SRCDIR)/config.h |
| 929 | $(XTCC) -o $(OBJDIR)/cache.o -c $(OBJDIR)/cache_.c |
| 930 | |
| 931 | $(OBJDIR)/cache.h: $(OBJDIR)/headers |
| 932 | |
| 933 | $(OBJDIR)/capabilities_.c: $(SRCDIR)/capabilities.c $(OBJDIR)/translate |
| 934 | $(OBJDIR)/translate $(SRCDIR)/capabilities.c >$@ |
| 935 | |
| 936 | $(OBJDIR)/capabilities.o: $(OBJDIR)/capabilities_.c $(OBJDIR)/capabilities.h $(SRCDIR)/config.h |
| 937 | $(XTCC) -o $(OBJDIR)/capabilities.o -c $(OBJDIR)/capabilities_.c |
| 938 | |
| 939 | $(OBJDIR)/capabilities.h: $(OBJDIR)/headers |
| 940 | |
| 941 | $(OBJDIR)/captcha_.c: $(SRCDIR)/captcha.c $(OBJDIR)/translate |
| 942 | $(OBJDIR)/translate $(SRCDIR)/captcha.c >$@ |
| 943 | |
| 944 | $(OBJDIR)/captcha.o: $(OBJDIR)/captcha_.c $(OBJDIR)/captcha.h $(SRCDIR)/config.h |
| 945 |
+1
| --- src/makemake.tcl | ||
| +++ src/makemake.tcl | ||
| @@ -37,10 +37,11 @@ | ||
| 37 | 37 | branch |
| 38 | 38 | browse |
| 39 | 39 | builtin |
| 40 | 40 | bundle |
| 41 | 41 | cache |
| 42 | + capabilities | |
| 42 | 43 | captcha |
| 43 | 44 | cgi |
| 44 | 45 | checkin |
| 45 | 46 | checkout |
| 46 | 47 | clearsign |
| 47 | 48 |
| --- src/makemake.tcl | |
| +++ src/makemake.tcl | |
| @@ -37,10 +37,11 @@ | |
| 37 | branch |
| 38 | browse |
| 39 | builtin |
| 40 | bundle |
| 41 | cache |
| 42 | captcha |
| 43 | cgi |
| 44 | checkin |
| 45 | checkout |
| 46 | clearsign |
| 47 |
| --- src/makemake.tcl | |
| +++ src/makemake.tcl | |
| @@ -37,10 +37,11 @@ | |
| 37 | branch |
| 38 | browse |
| 39 | builtin |
| 40 | bundle |
| 41 | cache |
| 42 | capabilities |
| 43 | captcha |
| 44 | cgi |
| 45 | checkin |
| 46 | checkout |
| 47 | clearsign |
| 48 |
+281
-86
| --- src/manifest.c | ||
| +++ src/manifest.c | ||
| @@ -34,10 +34,11 @@ | ||
| 34 | 34 | #define CFTYPE_CONTROL 3 |
| 35 | 35 | #define CFTYPE_WIKI 4 |
| 36 | 36 | #define CFTYPE_TICKET 5 |
| 37 | 37 | #define CFTYPE_ATTACHMENT 6 |
| 38 | 38 | #define CFTYPE_EVENT 7 |
| 39 | +#define CFTYPE_FORUM 8 | |
| 39 | 40 | |
| 40 | 41 | /* |
| 41 | 42 | ** File permissions used by Fossil internally. |
| 42 | 43 | */ |
| 43 | 44 | #define PERM_REG 0 /* regular file */ |
| @@ -76,16 +77,19 @@ | ||
| 76 | 77 | char *zUser; /* Name of the user from the U card. */ |
| 77 | 78 | char *zRepoCksum; /* MD5 checksum of the baseline content. R card. */ |
| 78 | 79 | char *zWiki; /* Text of the wiki page. W card. */ |
| 79 | 80 | char *zWikiTitle; /* Name of the wiki page. L card. */ |
| 80 | 81 | char *zMimetype; /* Mime type of wiki or comment text. N card. */ |
| 82 | + char *zThreadTitle; /* The forum thread title. H card */ | |
| 81 | 83 | double rEventDate; /* Date of an event. E card. */ |
| 82 | 84 | char *zEventId; /* Artifact hash for an event. E card. */ |
| 83 | 85 | char *zTicketUuid; /* UUID for a ticket. K card. */ |
| 84 | 86 | char *zAttachName; /* Filename of an attachment. A card. */ |
| 85 | 87 | char *zAttachSrc; /* Artifact hash for document being attached. A card. */ |
| 86 | 88 | char *zAttachTarget; /* Ticket or wiki that attachment applies to. A card */ |
| 89 | + char *zThreadRoot; /* Thread root artifact. G card */ | |
| 90 | + char *zInReplyTo; /* Forum in-reply-to artifact. I card */ | |
| 87 | 91 | int nFile; /* Number of F cards */ |
| 88 | 92 | int nFileAlloc; /* Slots allocated in aFile[] */ |
| 89 | 93 | int iFile; /* Index of current file in iterator */ |
| 90 | 94 | ManifestFile *aFile; /* One entry for each F-card */ |
| 91 | 95 | int nParent; /* Number of parents. */ |
| @@ -112,10 +116,42 @@ | ||
| 112 | 116 | char *zName; /* Key or field name */ |
| 113 | 117 | char *zValue; /* Value of the field */ |
| 114 | 118 | } *aField; /* One for each J card */ |
| 115 | 119 | }; |
| 116 | 120 | #endif |
| 121 | + | |
| 122 | +/* | |
| 123 | +** Allowed and required card types in each style of artifact | |
| 124 | +*/ | |
| 125 | +static struct { | |
| 126 | + const char *zAllowed; /* Allowed cards. Human-readable */ | |
| 127 | + const char *zRequired; /* Required cards. Human-readable */ | |
| 128 | +} manifestCardTypes[] = { | |
| 129 | + /* Allowed Required */ | |
| 130 | + /* CFTYPE_MANIFEST 1 */ { "BCDFNPQRTUZ", "CDUZ" }, | |
| 131 | + /* CFTYPE_CLUSTER 2 */ { "MZ", "MZ" }, | |
| 132 | + /* CFTYPE_CONTROL 3 */ { "DTUZ", "DTUZ" }, | |
| 133 | + /* CFTYPE_WIKI 4 */ { "DLNPUWZ", "DLUWZ" }, | |
| 134 | + /* CFTYPE_TICKET 5 */ { "DJKUZ", "DJKUZ" }, | |
| 135 | + /* CFTYPE_ATTACHMENT 6 */ { "ACDNUZ", "ADZ" }, | |
| 136 | + /* CFTYPE_EVENT 7 */ { "CDENPTUWZ", "DEWZ" }, | |
| 137 | + /* CFTYPE_FORUM 8 */ { "DGHINPUWZ", "DUWZ" }, | |
| 138 | +}; | |
| 139 | + | |
| 140 | +/* | |
| 141 | +** Names of manifest types | |
| 142 | +*/ | |
| 143 | +static const char *azNameOfMType[] = { | |
| 144 | + "manifest", | |
| 145 | + "cluster", | |
| 146 | + "tag", | |
| 147 | + "wiki", | |
| 148 | + "ticket", | |
| 149 | + "attachment", | |
| 150 | + "technote", | |
| 151 | + "forum post" | |
| 152 | +}; | |
| 117 | 153 | |
| 118 | 154 | /* |
| 119 | 155 | ** A cache of parsed manifests. This reduces the number of |
| 120 | 156 | ** calls to manifest_parse() when doing a rebuild. |
| 121 | 157 | */ |
| @@ -147,10 +183,35 @@ | ||
| 147 | 183 | if( p->pBaseline ) manifest_destroy(p->pBaseline); |
| 148 | 184 | memset(p, 0, sizeof(*p)); |
| 149 | 185 | fossil_free(p); |
| 150 | 186 | } |
| 151 | 187 | } |
| 188 | + | |
| 189 | +/* | |
| 190 | +** Given a string of upper-case letters, compute a mask of the letters | |
| 191 | +** present. For example, "ABC" computes 0x0007. "DE" gives 0x0018". | |
| 192 | +*/ | |
| 193 | +static unsigned int manifest_card_mask(const char *z){ | |
| 194 | + unsigned int m = 0; | |
| 195 | + char c; | |
| 196 | + while( (c = *(z++))>='A' && c<='Z' ){ | |
| 197 | + m |= 1 << (c - 'A'); | |
| 198 | + } | |
| 199 | + return m; | |
| 200 | +} | |
| 201 | + | |
| 202 | +/* | |
| 203 | +** Given an integer mask representing letters A-Z, return the | |
| 204 | +** letter which is the first bit set in the mask. Example: | |
| 205 | +** 0x03520 gives 'F' since the F-bit is the lowest. | |
| 206 | +*/ | |
| 207 | +static char maskToType(unsigned int x){ | |
| 208 | + char c = 'A'; | |
| 209 | + if( x==0 ) return '?'; | |
| 210 | + while( (x&1)==0 ){ x >>= 1; c++; } | |
| 211 | + return c; | |
| 212 | +} | |
| 152 | 213 | |
| 153 | 214 | /* |
| 154 | 215 | ** Add an element to the manifest cache using LRU replacement. |
| 155 | 216 | */ |
| 156 | 217 | void manifest_cache_insert(Manifest *p){ |
| @@ -352,22 +413,26 @@ | ||
| 352 | 413 | ** The card type determines the other parameters to the card. |
| 353 | 414 | ** Cards must occur in lexicographical order. |
| 354 | 415 | */ |
| 355 | 416 | Manifest *manifest_parse(Blob *pContent, int rid, Blob *pErr){ |
| 356 | 417 | Manifest *p; |
| 357 | - int seenZ = 0; | |
| 358 | 418 | int i, lineNo=0; |
| 359 | 419 | ManifestText x; |
| 360 | 420 | char cPrevType = 0; |
| 361 | 421 | char cType; |
| 362 | 422 | char *z; |
| 363 | 423 | int n; |
| 364 | 424 | char *zUuid; |
| 365 | 425 | int sz = 0; |
| 366 | - int isRepeat, hasSelfRefTag = 0; | |
| 426 | + int isRepeat; | |
| 427 | + int nSelfTag = 0; /* Number of T cards referring to this manifest */ | |
| 428 | + int nSimpleTag = 0; /* Number of T cards with "+" prefix */ | |
| 367 | 429 | static Bag seen; |
| 368 | 430 | const char *zErr = 0; |
| 431 | + unsigned int m; | |
| 432 | + unsigned int seenCard = 0; /* Which card types have been seen */ | |
| 433 | + char zErrBuf[100]; /* Write error messages here */ | |
| 369 | 434 | |
| 370 | 435 | if( rid==0 ){ |
| 371 | 436 | isRepeat = 1; |
| 372 | 437 | }else if( bag_find(&seen, rid) ){ |
| 373 | 438 | isRepeat = 1; |
| @@ -422,10 +487,12 @@ | ||
| 422 | 487 | x.z = z; |
| 423 | 488 | x.zEnd = &z[n]; |
| 424 | 489 | x.atEol = 1; |
| 425 | 490 | while( (cType = next_card(&x))!=0 && cType>=cPrevType ){ |
| 426 | 491 | lineNo++; |
| 492 | + if( cType<'A' || cType>'Z' ) SYNTAX("bad card type"); | |
| 493 | + seenCard |= 1 << (cType-'A'); | |
| 427 | 494 | switch( cType ){ |
| 428 | 495 | /* |
| 429 | 496 | ** A <filename> <target> ?<source>? |
| 430 | 497 | ** |
| 431 | 498 | ** Identifies an attachment to either a wiki page or a ticket. |
| @@ -454,10 +521,11 @@ | ||
| 454 | 521 | SYNTAX("invalid source on A-card"); |
| 455 | 522 | } |
| 456 | 523 | p->zAttachName = (char*)file_tail(zName); |
| 457 | 524 | p->zAttachSrc = zSrc; |
| 458 | 525 | p->zAttachTarget = zTarget; |
| 526 | + p->type = CFTYPE_ATTACHMENT; | |
| 459 | 527 | break; |
| 460 | 528 | } |
| 461 | 529 | |
| 462 | 530 | /* |
| 463 | 531 | ** B <uuid> |
| @@ -469,10 +537,11 @@ | ||
| 469 | 537 | p->zBaseline = next_token(&x, &sz); |
| 470 | 538 | if( p->zBaseline==0 ) SYNTAX("missing hash on B-card"); |
| 471 | 539 | if( !hname_validate(p->zBaseline,sz) ){ |
| 472 | 540 | SYNTAX("invalid hash on B-card"); |
| 473 | 541 | } |
| 542 | + p->type = CFTYPE_MANIFEST; | |
| 474 | 543 | break; |
| 475 | 544 | } |
| 476 | 545 | |
| 477 | 546 | |
| 478 | 547 | /* |
| @@ -520,10 +589,11 @@ | ||
| 520 | 589 | if( p->rEventDate<=0.0 ) SYNTAX("malformed date on E-card"); |
| 521 | 590 | p->zEventId = next_token(&x, &sz); |
| 522 | 591 | if( !hname_validate(p->zEventId, sz) ){ |
| 523 | 592 | SYNTAX("malformed hash on E-card"); |
| 524 | 593 | } |
| 594 | + p->type = CFTYPE_EVENT; | |
| 525 | 595 | break; |
| 526 | 596 | } |
| 527 | 597 | |
| 528 | 598 | /* |
| 529 | 599 | ** F <filename> ?<uuid>? ?<permissions>? ?<old-name>? |
| @@ -565,10 +635,59 @@ | ||
| 565 | 635 | p->aFile[i].zPerm = zPerm; |
| 566 | 636 | p->aFile[i].zPrior = zPriorName; |
| 567 | 637 | if( i>0 && fossil_strcmp(p->aFile[i-1].zName, zName)>=0 ){ |
| 568 | 638 | SYNTAX("incorrect F-card sort order"); |
| 569 | 639 | } |
| 640 | + p->type = CFTYPE_MANIFEST; | |
| 641 | + break; | |
| 642 | + } | |
| 643 | + | |
| 644 | + /* | |
| 645 | + ** G <hash> | |
| 646 | + ** | |
| 647 | + ** A G-card identifies the initial root forum post for the thread | |
| 648 | + ** of which this post is a part. Forum posts only. | |
| 649 | + */ | |
| 650 | + case 'G': { | |
| 651 | + if( p->zThreadRoot!=0 ) SYNTAX("more than one G-card"); | |
| 652 | + p->zThreadRoot = next_token(&x, &sz); | |
| 653 | + if( p->zThreadRoot==0 ) SYNTAX("missing hash on G-card"); | |
| 654 | + if( !hname_validate(p->zThreadRoot,sz) ){ | |
| 655 | + SYNTAX("Invalid hash on G-card"); | |
| 656 | + } | |
| 657 | + p->type = CFTYPE_FORUM; | |
| 658 | + break; | |
| 659 | + } | |
| 660 | + | |
| 661 | + /* | |
| 662 | + ** H <threadtitle> | |
| 663 | + ** | |
| 664 | + ** The title for a forum thread. | |
| 665 | + */ | |
| 666 | + case 'H': { | |
| 667 | + if( p->zThreadTitle!=0 ) SYNTAX("more than one H-card"); | |
| 668 | + p->zThreadTitle = next_token(&x,0); | |
| 669 | + if( p->zThreadTitle==0 ) SYNTAX("missing title on H-card"); | |
| 670 | + defossilize(p->zThreadTitle); | |
| 671 | + p->type = CFTYPE_FORUM; | |
| 672 | + break; | |
| 673 | + } | |
| 674 | + | |
| 675 | + /* | |
| 676 | + ** I <hash> | |
| 677 | + ** | |
| 678 | + ** A I-card identifies another forum post that the current forum post | |
| 679 | + ** is in reply to. | |
| 680 | + */ | |
| 681 | + case 'I': { | |
| 682 | + if( p->zInReplyTo!=0 ) SYNTAX("more than one I-card"); | |
| 683 | + p->zInReplyTo = next_token(&x, &sz); | |
| 684 | + if( p->zInReplyTo==0 ) SYNTAX("missing hash on I-card"); | |
| 685 | + if( !hname_validate(p->zInReplyTo,sz) ){ | |
| 686 | + SYNTAX("Invalid hash on I-card"); | |
| 687 | + } | |
| 688 | + p->type = CFTYPE_FORUM; | |
| 570 | 689 | break; |
| 571 | 690 | } |
| 572 | 691 | |
| 573 | 692 | /* |
| 574 | 693 | ** J <name> ?<value>? |
| @@ -594,10 +713,11 @@ | ||
| 594 | 713 | p->aField[i].zName = zName; |
| 595 | 714 | p->aField[i].zValue = zValue; |
| 596 | 715 | if( i>0 && fossil_strcmp(p->aField[i-1].zName, zName)>=0 ){ |
| 597 | 716 | SYNTAX("incorrect J-card sort order"); |
| 598 | 717 | } |
| 718 | + p->type = CFTYPE_TICKET; | |
| 599 | 719 | break; |
| 600 | 720 | } |
| 601 | 721 | |
| 602 | 722 | |
| 603 | 723 | /* |
| @@ -611,10 +731,11 @@ | ||
| 611 | 731 | p->zTicketUuid = next_token(&x, &sz); |
| 612 | 732 | if( sz!=HNAME_LEN_SHA1 ) SYNTAX("K-card UUID is the wrong size"); |
| 613 | 733 | if( !validate16(p->zTicketUuid, sz) ){ |
| 614 | 734 | SYNTAX("invalid K-card UUID"); |
| 615 | 735 | } |
| 736 | + p->type = CFTYPE_TICKET; | |
| 616 | 737 | break; |
| 617 | 738 | } |
| 618 | 739 | |
| 619 | 740 | /* |
| 620 | 741 | ** L <wikititle> |
| @@ -628,10 +749,11 @@ | ||
| 628 | 749 | if( p->zWikiTitle==0 ) SYNTAX("missing title on L-card"); |
| 629 | 750 | defossilize(p->zWikiTitle); |
| 630 | 751 | if( !wiki_name_is_wellformed((const unsigned char *)p->zWikiTitle) ){ |
| 631 | 752 | SYNTAX("L-card has malformed wiki name"); |
| 632 | 753 | } |
| 754 | + p->type = CFTYPE_WIKI; | |
| 633 | 755 | break; |
| 634 | 756 | } |
| 635 | 757 | |
| 636 | 758 | /* |
| 637 | 759 | ** M <hash> |
| @@ -653,10 +775,11 @@ | ||
| 653 | 775 | i = p->nCChild++; |
| 654 | 776 | p->azCChild[i] = zUuid; |
| 655 | 777 | if( i>0 && fossil_strcmp(p->azCChild[i-1], zUuid)>=0 ){ |
| 656 | 778 | SYNTAX("M-card in the wrong order"); |
| 657 | 779 | } |
| 780 | + p->type = CFTYPE_CLUSTER; | |
| 658 | 781 | break; |
| 659 | 782 | } |
| 660 | 783 | |
| 661 | 784 | /* |
| 662 | 785 | ** N <uuid> |
| @@ -717,10 +840,11 @@ | ||
| 717 | 840 | p->aCherrypick[n].zCPTarget = zUuid; |
| 718 | 841 | p->aCherrypick[n].zCPBase = zUuid = next_token(&x, &sz); |
| 719 | 842 | if( zUuid && !hname_validate(zUuid,sz) ){ |
| 720 | 843 | SYNTAX("invalid second hash on Q-card"); |
| 721 | 844 | } |
| 845 | + p->type = CFTYPE_MANIFEST; | |
| 722 | 846 | break; |
| 723 | 847 | } |
| 724 | 848 | |
| 725 | 849 | /* |
| 726 | 850 | ** R <md5sum> |
| @@ -731,10 +855,11 @@ | ||
| 731 | 855 | case 'R': { |
| 732 | 856 | if( p->zRepoCksum!=0 ) SYNTAX("more than one R-card"); |
| 733 | 857 | p->zRepoCksum = next_token(&x, &sz); |
| 734 | 858 | if( sz!=32 ) SYNTAX("wrong size cksum on R-card"); |
| 735 | 859 | if( !validate16(p->zRepoCksum, 32) ) SYNTAX("malformed R-card cksum"); |
| 860 | + p->type = CFTYPE_MANIFEST; | |
| 736 | 861 | break; |
| 737 | 862 | } |
| 738 | 863 | |
| 739 | 864 | /* |
| 740 | 865 | ** T (+|*|-)<tagname> <uuid> ?<value>? |
| @@ -759,24 +884,21 @@ | ||
| 759 | 884 | if( zUuid==0 ) SYNTAX("missing artifact hash on T-card"); |
| 760 | 885 | zValue = next_token(&x, 0); |
| 761 | 886 | if( zValue ) defossilize(zValue); |
| 762 | 887 | if( hname_validate(zUuid, sz) ){ |
| 763 | 888 | /* A valid artifact hash */ |
| 764 | - if( p->zEventId ) SYNTAX("non-self-referential T-card in event"); | |
| 765 | 889 | }else if( sz==1 && zUuid[0]=='*' ){ |
| 766 | 890 | zUuid = 0; |
| 767 | - hasSelfRefTag = 1; | |
| 768 | - if( p->zEventId && zName[0]!='+' ){ | |
| 769 | - SYNTAX("propagating T-card in event"); | |
| 770 | - } | |
| 891 | + nSelfTag++; | |
| 771 | 892 | }else{ |
| 772 | 893 | SYNTAX("malformed artifact hash on T-card"); |
| 773 | 894 | } |
| 774 | 895 | defossilize(zName); |
| 775 | 896 | if( zName[0]!='-' && zName[0]!='+' && zName[0]!='*' ){ |
| 776 | 897 | SYNTAX("T-card name does not begin with '-', '+', or '*'"); |
| 777 | 898 | } |
| 899 | + if( zName[0]=='+' ) nSimpleTag++; | |
| 778 | 900 | if( validate16(&zName[1], strlen(&zName[1])) ){ |
| 779 | 901 | /* Do not allow tags whose names look like a hash */ |
| 780 | 902 | SYNTAX("T-card name looks like a hexadecimal hash"); |
| 781 | 903 | } |
| 782 | 904 | if( p->nTag>=p->nTagAlloc ){ |
| @@ -858,104 +980,78 @@ | ||
| 858 | 980 | */ |
| 859 | 981 | case 'Z': { |
| 860 | 982 | zUuid = next_token(&x, &sz); |
| 861 | 983 | if( sz!=32 ) SYNTAX("wrong size for Z-card cksum"); |
| 862 | 984 | if( !validate16(zUuid, 32) ) SYNTAX("malformed Z-card cksum"); |
| 863 | - seenZ = 1; | |
| 864 | 985 | break; |
| 865 | 986 | } |
| 866 | 987 | default: { |
| 867 | 988 | SYNTAX("unrecognized card"); |
| 868 | 989 | } |
| 869 | 990 | } |
| 870 | 991 | } |
| 871 | 992 | if( x.z<x.zEnd ) SYNTAX("extra characters at end of card"); |
| 872 | 993 | |
| 873 | - if( p->nCChild>0 ){ | |
| 874 | - if( p->zAttachName | |
| 875 | - || p->zBaseline | |
| 876 | - || p->zComment | |
| 877 | - || p->rDate>0.0 | |
| 878 | - || p->zEventId | |
| 879 | - || p->nFile>0 | |
| 880 | - || p->nField>0 | |
| 881 | - || p->zTicketUuid | |
| 882 | - || p->zWikiTitle | |
| 883 | - || p->zMimetype | |
| 884 | - || p->nParent>0 | |
| 885 | - || p->nCherrypick>0 | |
| 886 | - || p->zRepoCksum | |
| 887 | - || p->nTag>0 | |
| 888 | - || p->zUser | |
| 889 | - || p->zWiki | |
| 890 | - ){ | |
| 891 | - SYNTAX("cluster contains a card other than M- or Z-"); | |
| 892 | - } | |
| 893 | - if( !seenZ ) SYNTAX("missing Z-card on cluster"); | |
| 894 | - p->type = CFTYPE_CLUSTER; | |
| 895 | - }else if( p->zEventId ){ | |
| 896 | - if( p->zAttachName ) SYNTAX("A-card in event"); | |
| 897 | - if( p->zBaseline ) SYNTAX("B-card in event"); | |
| 898 | - if( p->rDate<=0.0 ) SYNTAX("missing date on event"); | |
| 899 | - if( p->nFile>0 ) SYNTAX("F-card in event"); | |
| 900 | - if( p->nField>0 ) SYNTAX("J-card in event"); | |
| 901 | - if( p->zTicketUuid ) SYNTAX("K-card in event"); | |
| 902 | - if( p->zWikiTitle!=0 ) SYNTAX("L-card in event"); | |
| 903 | - if( p->zRepoCksum ) SYNTAX("R-card in event"); | |
| 904 | - if( p->zWiki==0 ) SYNTAX("missing W-card on event"); | |
| 905 | - if( !seenZ ) SYNTAX("missing Z-card on event"); | |
| 906 | - p->type = CFTYPE_EVENT; | |
| 907 | - }else if( p->zWiki!=0 || p->zWikiTitle!=0 ){ | |
| 908 | - if( p->zAttachName ) SYNTAX("A-card in wiki"); | |
| 909 | - if( p->zBaseline ) SYNTAX("B-card in wiki"); | |
| 910 | - if( p->rDate<=0.0 ) SYNTAX("missing date on wiki"); | |
| 911 | - if( p->nFile>0 ) SYNTAX("F-card in wiki"); | |
| 912 | - if( p->nField>0 ) SYNTAX("J-card in wiki"); | |
| 913 | - if( p->zTicketUuid ) SYNTAX("K-card in wiki"); | |
| 914 | - if( p->zWikiTitle==0 ) SYNTAX("missing L-card on wiki"); | |
| 915 | - if( p->zRepoCksum ) SYNTAX("R-card in wiki"); | |
| 916 | - if( p->nTag>0 ) SYNTAX("T-card in wiki"); | |
| 917 | - if( p->zWiki==0 ) SYNTAX("missing W-card on wiki"); | |
| 918 | - if( !seenZ ) SYNTAX("missing Z-card on wiki"); | |
| 919 | - p->type = CFTYPE_WIKI; | |
| 920 | - }else if( hasSelfRefTag || p->nFile>0 || p->zRepoCksum!=0 || p->zBaseline | |
| 921 | - || p->nParent>0 ){ | |
| 922 | - if( p->zAttachName ) SYNTAX("A-card in manifest"); | |
| 923 | - if( p->rDate<=0.0 ) SYNTAX("missing date on manifest"); | |
| 924 | - if( p->nField>0 ) SYNTAX("J-card in manifest"); | |
| 925 | - if( p->zTicketUuid ) SYNTAX("K-card in manifest"); | |
| 926 | - p->type = CFTYPE_MANIFEST; | |
| 927 | - }else if( p->nField>0 || p->zTicketUuid!=0 ){ | |
| 928 | - if( p->zAttachName ) SYNTAX("A-card in ticket"); | |
| 929 | - if( p->rDate<=0.0 ) SYNTAX("missing date on ticket"); | |
| 930 | - if( p->nField==0 ) SYNTAX("missing J-card on ticket"); | |
| 931 | - if( p->zTicketUuid==0 ) SYNTAX("missing K-card on ticket"); | |
| 932 | - if( p->zMimetype) SYNTAX("N-card in ticket"); | |
| 933 | - if( p->nTag>0 ) SYNTAX("T-card in ticket"); | |
| 934 | - if( p->zUser==0 ) SYNTAX("missing U-card on ticket"); | |
| 935 | - if( !seenZ ) SYNTAX("missing Z-card on ticket"); | |
| 936 | - p->type = CFTYPE_TICKET; | |
| 937 | - }else if( p->zAttachName ){ | |
| 938 | - if( p->rDate<=0.0 ) SYNTAX("missing date on attachment"); | |
| 939 | - if( p->nTag>0 ) SYNTAX("T-card in attachment"); | |
| 940 | - if( !seenZ ) SYNTAX("missing Z-card on attachment"); | |
| 941 | - p->type = CFTYPE_ATTACHMENT; | |
| 942 | - }else{ | |
| 943 | - if( p->rDate<=0.0 ) SYNTAX("missing date on control"); | |
| 944 | - if( p->zMimetype ) SYNTAX("N-card in control"); | |
| 945 | - if( !seenZ ) SYNTAX("missing Z-card on control"); | |
| 946 | - p->type = CFTYPE_CONTROL; | |
| 947 | - } | |
| 994 | + /* If the artifact type has not yet been determined, then compute | |
| 995 | + ** it now. */ | |
| 996 | + if( p->type==0 ){ | |
| 997 | + p->type = p->zComment!=0 ? CFTYPE_MANIFEST : CFTYPE_CONTROL; | |
| 998 | + } | |
| 999 | + | |
| 1000 | + /* Verify that no disallowed cards are present for this artifact type */ | |
| 1001 | + m = manifest_card_mask(manifestCardTypes[p->type-1].zAllowed); | |
| 1002 | + if( seenCard & ~m ){ | |
| 1003 | + sqlite3_snprintf(sizeof(zErrBuf), zErrBuf, "%c-card in %s", | |
| 1004 | + maskToType(seenCard & ~m), | |
| 1005 | + azNameOfMType[p->type-1]); | |
| 1006 | + zErr = zErrBuf; | |
| 1007 | + goto manifest_syntax_error; | |
| 1008 | + } | |
| 1009 | + | |
| 1010 | + /* Verify that all required cards are present for this artifact type */ | |
| 1011 | + m = manifest_card_mask(manifestCardTypes[p->type-1].zRequired); | |
| 1012 | + if( ~seenCard & m ){ | |
| 1013 | + sqlite3_snprintf(sizeof(zErrBuf), zErrBuf, "%c-card missing in %s", | |
| 1014 | + maskToType(~seenCard & m), | |
| 1015 | + azNameOfMType[p->type-1]); | |
| 1016 | + zErr = zErrBuf; | |
| 1017 | + goto manifest_syntax_error; | |
| 1018 | + } | |
| 1019 | + | |
| 1020 | + /* Additional checks based on artifact type */ | |
| 1021 | + switch( p->type ){ | |
| 1022 | + case CFTYPE_CONTROL: { | |
| 1023 | + if( nSelfTag ) SYNTAX("self-referential T-card in control artifact"); | |
| 1024 | + break; | |
| 1025 | + } | |
| 1026 | + case CFTYPE_EVENT: { | |
| 1027 | + if( p->nTag!=nSelfTag ){ | |
| 1028 | + SYNTAX("non-self-referential T-card in technote"); | |
| 1029 | + } | |
| 1030 | + if( p->nTag!=nSimpleTag ){ | |
| 1031 | + SYNTAX("T-card with '*' or '-' in technote"); | |
| 1032 | + } | |
| 1033 | + break; | |
| 1034 | + } | |
| 1035 | + case CFTYPE_FORUM: { | |
| 1036 | + if( p->zThreadTitle && p->zInReplyTo ){ | |
| 1037 | + SYNTAX("cannot have I-card and H-card in a forum post"); | |
| 1038 | + } | |
| 1039 | + if( p->nParent>1 ) SYNTAX("too many arguments to P-card"); | |
| 1040 | + break; | |
| 1041 | + } | |
| 1042 | + } | |
| 1043 | + | |
| 948 | 1044 | md5sum_init(); |
| 949 | 1045 | if( !isRepeat ) g.parseCnt[p->type]++; |
| 950 | 1046 | return p; |
| 951 | 1047 | |
| 952 | 1048 | manifest_syntax_error: |
| 953 | 1049 | { |
| 954 | 1050 | char *zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid); |
| 955 | 1051 | if( zUuid ){ |
| 956 | - blob_appendf(pErr, "manifest [%s] ", zUuid); | |
| 1052 | + blob_appendf(pErr, "artifact [%s] ", zUuid); | |
| 957 | 1053 | fossil_free(zUuid); |
| 958 | 1054 | } |
| 959 | 1055 | } |
| 960 | 1056 | if( zErr ){ |
| 961 | 1057 | blob_appendf(pErr, "line %d: %s", lineNo, zErr); |
| @@ -1015,11 +1111,12 @@ | ||
| 1015 | 1111 | /* |
| 1016 | 1112 | ** COMMAND: test-parse-manifest |
| 1017 | 1113 | ** |
| 1018 | 1114 | ** Usage: %fossil test-parse-manifest FILENAME ?N? |
| 1019 | 1115 | ** |
| 1020 | -** Parse the manifest and discarded. Use for testing only. | |
| 1116 | +** Parse the manifest(s) given on the command-line and report any | |
| 1117 | +** errors. If the N argument is given, run the parsing N times. | |
| 1021 | 1118 | */ |
| 1022 | 1119 | void manifest_test_parse_cmd(void){ |
| 1023 | 1120 | Manifest *p; |
| 1024 | 1121 | Blob b; |
| 1025 | 1122 | int i; |
| @@ -1039,10 +1136,47 @@ | ||
| 1039 | 1136 | if( p==0 ) fossil_print("ERROR: %s\n", blob_str(&err)); |
| 1040 | 1137 | blob_reset(&err); |
| 1041 | 1138 | manifest_destroy(p); |
| 1042 | 1139 | } |
| 1043 | 1140 | } |
| 1141 | + | |
| 1142 | +/* | |
| 1143 | +** COMMAND: test-parse-all-blobs | |
| 1144 | +** | |
| 1145 | +** Usage: %fossil test-parse-all-blobs | |
| 1146 | +** | |
| 1147 | +** Parse all entries in the BLOB table that are believed to be non-data | |
| 1148 | +** artifacts and report any errors. Run this test command on historical | |
| 1149 | +** repositories after making any changes to the manifest_parse() | |
| 1150 | +** implementation to confirm that the changes did not break anything. | |
| 1151 | +*/ | |
| 1152 | +void manifest_test_parse_all_blobs_cmd(void){ | |
| 1153 | + Manifest *p; | |
| 1154 | + Blob err; | |
| 1155 | + Stmt q; | |
| 1156 | + int nTest = 0; | |
| 1157 | + int nErr = 0; | |
| 1158 | + db_find_and_open_repository(0, 0); | |
| 1159 | + verify_all_options(); | |
| 1160 | + db_prepare(&q, "SELECT DISTINCT objid FROM EVENT"); | |
| 1161 | + while( db_step(&q)==SQLITE_ROW ){ | |
| 1162 | + int id = db_column_int(&q,0); | |
| 1163 | + fossil_print("Checking %d \r", id); | |
| 1164 | + nTest++; | |
| 1165 | + fflush(stdout); | |
| 1166 | + blob_init(&err, 0, 0); | |
| 1167 | + p = manifest_get(id, CFTYPE_ANY, &err); | |
| 1168 | + if( p==0 ){ | |
| 1169 | + fossil_print("%d ERROR: %s\n", id, blob_str(&err)); | |
| 1170 | + nErr++; | |
| 1171 | + } | |
| 1172 | + blob_reset(&err); | |
| 1173 | + manifest_destroy(p); | |
| 1174 | + } | |
| 1175 | + db_finalize(&q); | |
| 1176 | + fossil_print("%d tests with %d errors\n", nTest, nErr); | |
| 1177 | +} | |
| 1044 | 1178 | |
| 1045 | 1179 | /* |
| 1046 | 1180 | ** Fetch the baseline associated with the delta-manifest p. |
| 1047 | 1181 | ** Return 0 on success. If unable to parse the baseline, |
| 1048 | 1182 | ** throw an error. If the baseline is a manifest, throw an |
| @@ -1059,11 +1193,11 @@ | ||
| 1059 | 1193 | "INSERT OR IGNORE INTO orphan(rid, baseline) VALUES(%d,%d)", |
| 1060 | 1194 | p->rid, rid |
| 1061 | 1195 | ); |
| 1062 | 1196 | return 1; |
| 1063 | 1197 | } |
| 1064 | - fossil_panic("cannot access baseline manifest %S", p->zBaseline); | |
| 1198 | + fossil_fatal("cannot access baseline manifest %S", p->zBaseline); | |
| 1065 | 1199 | } |
| 1066 | 1200 | } |
| 1067 | 1201 | return 0; |
| 1068 | 1202 | } |
| 1069 | 1203 | |
| @@ -2378,10 +2512,71 @@ | ||
| 2378 | 2512 | "REPLACE INTO event(type,mtime,objid,user,comment)" |
| 2379 | 2513 | "VALUES('g',%.17g,%d,%Q,%Q)", |
| 2380 | 2514 | p->rDate, rid, p->zUser, blob_str(&comment)+1 |
| 2381 | 2515 | ); |
| 2382 | 2516 | blob_reset(&comment); |
| 2517 | + } | |
| 2518 | + if( p->type==CFTYPE_FORUM ){ | |
| 2519 | + int froot, fprev, firt; | |
| 2520 | + char *zFType; | |
| 2521 | + char *zTitle; | |
| 2522 | + schema_forum(); | |
| 2523 | + froot = p->zThreadRoot ? uuid_to_rid(p->zThreadRoot, 1) : rid; | |
| 2524 | + fprev = p->nParent ? uuid_to_rid(p->azParent[0],1) : 0; | |
| 2525 | + firt = p->zInReplyTo ? uuid_to_rid(p->zInReplyTo,1) : 0; | |
| 2526 | + db_multi_exec( | |
| 2527 | + "INSERT INTO forumpost(fpid,froot,fprev,firt,fmtime)" | |
| 2528 | + "VALUES(%d,%d,nullif(%d,0),nullif(%d,0),%.17g)", | |
| 2529 | + p->rid, froot, fprev, firt, p->rDate | |
| 2530 | + ); | |
| 2531 | + if( firt==0 ){ | |
| 2532 | + /* This is the start of a new thread, either the initial entry | |
| 2533 | + ** or an edit of the initial entry. */ | |
| 2534 | + zTitle = p->zThreadTitle; | |
| 2535 | + if( zTitle==0 || zTitle[0]==0 ){ | |
| 2536 | + zTitle = "(Deleted)"; | |
| 2537 | + } | |
| 2538 | + zFType = fprev ? "Edit" : "Post"; | |
| 2539 | + db_multi_exec( | |
| 2540 | + "REPLACE INTO event(type,mtime,objid,user,comment)" | |
| 2541 | + "VALUES('f',%.17g,%d,%Q,'%q: %q')", | |
| 2542 | + p->rDate, rid, p->zUser, zFType, zTitle | |
| 2543 | + ); | |
| 2544 | + /* | |
| 2545 | + ** If this edit is the most recent, then make it the title for | |
| 2546 | + ** all other entries for the same thread | |
| 2547 | + */ | |
| 2548 | + if( !db_exists("SELECT 1 FROM forumpost WHERE froot=%d AND firt=0" | |
| 2549 | + " AND fpid!=%d AND fmtime>%.17g", froot, rid, p->rDate) | |
| 2550 | + ){ | |
| 2551 | + /* This entry establishes a new title for all entries on the thread */ | |
| 2552 | + db_multi_exec( | |
| 2553 | + "UPDATE event" | |
| 2554 | + " SET comment=substr(comment,1,instr(comment,':')) || ' %q'" | |
| 2555 | + " WHERE objid IN (SELECT fpid FROM forumpost WHERE froot=%d)", | |
| 2556 | + zTitle, froot | |
| 2557 | + ); | |
| 2558 | + } | |
| 2559 | + }else{ | |
| 2560 | + /* This is a reply to a prior post. Take the title from the root. */ | |
| 2561 | + zTitle = db_text(0, "SELECT substr(comment,instr(comment,':')+2)" | |
| 2562 | + " FROM event WHERE objid=%d", froot); | |
| 2563 | + if( zTitle==0 ) zTitle = fossil_strdup("<i>Unknown</i>"); | |
| 2564 | + if( p->zWiki[0]==0 ){ | |
| 2565 | + zFType = "Delete reply"; | |
| 2566 | + }else if( fprev ){ | |
| 2567 | + zFType = "Edit reply"; | |
| 2568 | + }else{ | |
| 2569 | + zFType = "Reply"; | |
| 2570 | + } | |
| 2571 | + db_multi_exec( | |
| 2572 | + "REPLACE INTO event(type,mtime,objid,user,comment)" | |
| 2573 | + "VALUES('f',%.17g,%d,%Q,'%q: %q')", | |
| 2574 | + p->rDate, rid, p->zUser, zFType, zTitle | |
| 2575 | + ); | |
| 2576 | + fossil_free(zTitle); | |
| 2577 | + } | |
| 2383 | 2578 | } |
| 2384 | 2579 | db_end_transaction(0); |
| 2385 | 2580 | if( permitHooks ){ |
| 2386 | 2581 | rc = xfer_run_common_script(); |
| 2387 | 2582 | if( rc==TH_OK ){ |
| 2388 | 2583 |
| --- src/manifest.c | |
| +++ src/manifest.c | |
| @@ -34,10 +34,11 @@ | |
| 34 | #define CFTYPE_CONTROL 3 |
| 35 | #define CFTYPE_WIKI 4 |
| 36 | #define CFTYPE_TICKET 5 |
| 37 | #define CFTYPE_ATTACHMENT 6 |
| 38 | #define CFTYPE_EVENT 7 |
| 39 | |
| 40 | /* |
| 41 | ** File permissions used by Fossil internally. |
| 42 | */ |
| 43 | #define PERM_REG 0 /* regular file */ |
| @@ -76,16 +77,19 @@ | |
| 76 | char *zUser; /* Name of the user from the U card. */ |
| 77 | char *zRepoCksum; /* MD5 checksum of the baseline content. R card. */ |
| 78 | char *zWiki; /* Text of the wiki page. W card. */ |
| 79 | char *zWikiTitle; /* Name of the wiki page. L card. */ |
| 80 | char *zMimetype; /* Mime type of wiki or comment text. N card. */ |
| 81 | double rEventDate; /* Date of an event. E card. */ |
| 82 | char *zEventId; /* Artifact hash for an event. E card. */ |
| 83 | char *zTicketUuid; /* UUID for a ticket. K card. */ |
| 84 | char *zAttachName; /* Filename of an attachment. A card. */ |
| 85 | char *zAttachSrc; /* Artifact hash for document being attached. A card. */ |
| 86 | char *zAttachTarget; /* Ticket or wiki that attachment applies to. A card */ |
| 87 | int nFile; /* Number of F cards */ |
| 88 | int nFileAlloc; /* Slots allocated in aFile[] */ |
| 89 | int iFile; /* Index of current file in iterator */ |
| 90 | ManifestFile *aFile; /* One entry for each F-card */ |
| 91 | int nParent; /* Number of parents. */ |
| @@ -112,10 +116,42 @@ | |
| 112 | char *zName; /* Key or field name */ |
| 113 | char *zValue; /* Value of the field */ |
| 114 | } *aField; /* One for each J card */ |
| 115 | }; |
| 116 | #endif |
| 117 | |
| 118 | /* |
| 119 | ** A cache of parsed manifests. This reduces the number of |
| 120 | ** calls to manifest_parse() when doing a rebuild. |
| 121 | */ |
| @@ -147,10 +183,35 @@ | |
| 147 | if( p->pBaseline ) manifest_destroy(p->pBaseline); |
| 148 | memset(p, 0, sizeof(*p)); |
| 149 | fossil_free(p); |
| 150 | } |
| 151 | } |
| 152 | |
| 153 | /* |
| 154 | ** Add an element to the manifest cache using LRU replacement. |
| 155 | */ |
| 156 | void manifest_cache_insert(Manifest *p){ |
| @@ -352,22 +413,26 @@ | |
| 352 | ** The card type determines the other parameters to the card. |
| 353 | ** Cards must occur in lexicographical order. |
| 354 | */ |
| 355 | Manifest *manifest_parse(Blob *pContent, int rid, Blob *pErr){ |
| 356 | Manifest *p; |
| 357 | int seenZ = 0; |
| 358 | int i, lineNo=0; |
| 359 | ManifestText x; |
| 360 | char cPrevType = 0; |
| 361 | char cType; |
| 362 | char *z; |
| 363 | int n; |
| 364 | char *zUuid; |
| 365 | int sz = 0; |
| 366 | int isRepeat, hasSelfRefTag = 0; |
| 367 | static Bag seen; |
| 368 | const char *zErr = 0; |
| 369 | |
| 370 | if( rid==0 ){ |
| 371 | isRepeat = 1; |
| 372 | }else if( bag_find(&seen, rid) ){ |
| 373 | isRepeat = 1; |
| @@ -422,10 +487,12 @@ | |
| 422 | x.z = z; |
| 423 | x.zEnd = &z[n]; |
| 424 | x.atEol = 1; |
| 425 | while( (cType = next_card(&x))!=0 && cType>=cPrevType ){ |
| 426 | lineNo++; |
| 427 | switch( cType ){ |
| 428 | /* |
| 429 | ** A <filename> <target> ?<source>? |
| 430 | ** |
| 431 | ** Identifies an attachment to either a wiki page or a ticket. |
| @@ -454,10 +521,11 @@ | |
| 454 | SYNTAX("invalid source on A-card"); |
| 455 | } |
| 456 | p->zAttachName = (char*)file_tail(zName); |
| 457 | p->zAttachSrc = zSrc; |
| 458 | p->zAttachTarget = zTarget; |
| 459 | break; |
| 460 | } |
| 461 | |
| 462 | /* |
| 463 | ** B <uuid> |
| @@ -469,10 +537,11 @@ | |
| 469 | p->zBaseline = next_token(&x, &sz); |
| 470 | if( p->zBaseline==0 ) SYNTAX("missing hash on B-card"); |
| 471 | if( !hname_validate(p->zBaseline,sz) ){ |
| 472 | SYNTAX("invalid hash on B-card"); |
| 473 | } |
| 474 | break; |
| 475 | } |
| 476 | |
| 477 | |
| 478 | /* |
| @@ -520,10 +589,11 @@ | |
| 520 | if( p->rEventDate<=0.0 ) SYNTAX("malformed date on E-card"); |
| 521 | p->zEventId = next_token(&x, &sz); |
| 522 | if( !hname_validate(p->zEventId, sz) ){ |
| 523 | SYNTAX("malformed hash on E-card"); |
| 524 | } |
| 525 | break; |
| 526 | } |
| 527 | |
| 528 | /* |
| 529 | ** F <filename> ?<uuid>? ?<permissions>? ?<old-name>? |
| @@ -565,10 +635,59 @@ | |
| 565 | p->aFile[i].zPerm = zPerm; |
| 566 | p->aFile[i].zPrior = zPriorName; |
| 567 | if( i>0 && fossil_strcmp(p->aFile[i-1].zName, zName)>=0 ){ |
| 568 | SYNTAX("incorrect F-card sort order"); |
| 569 | } |
| 570 | break; |
| 571 | } |
| 572 | |
| 573 | /* |
| 574 | ** J <name> ?<value>? |
| @@ -594,10 +713,11 @@ | |
| 594 | p->aField[i].zName = zName; |
| 595 | p->aField[i].zValue = zValue; |
| 596 | if( i>0 && fossil_strcmp(p->aField[i-1].zName, zName)>=0 ){ |
| 597 | SYNTAX("incorrect J-card sort order"); |
| 598 | } |
| 599 | break; |
| 600 | } |
| 601 | |
| 602 | |
| 603 | /* |
| @@ -611,10 +731,11 @@ | |
| 611 | p->zTicketUuid = next_token(&x, &sz); |
| 612 | if( sz!=HNAME_LEN_SHA1 ) SYNTAX("K-card UUID is the wrong size"); |
| 613 | if( !validate16(p->zTicketUuid, sz) ){ |
| 614 | SYNTAX("invalid K-card UUID"); |
| 615 | } |
| 616 | break; |
| 617 | } |
| 618 | |
| 619 | /* |
| 620 | ** L <wikititle> |
| @@ -628,10 +749,11 @@ | |
| 628 | if( p->zWikiTitle==0 ) SYNTAX("missing title on L-card"); |
| 629 | defossilize(p->zWikiTitle); |
| 630 | if( !wiki_name_is_wellformed((const unsigned char *)p->zWikiTitle) ){ |
| 631 | SYNTAX("L-card has malformed wiki name"); |
| 632 | } |
| 633 | break; |
| 634 | } |
| 635 | |
| 636 | /* |
| 637 | ** M <hash> |
| @@ -653,10 +775,11 @@ | |
| 653 | i = p->nCChild++; |
| 654 | p->azCChild[i] = zUuid; |
| 655 | if( i>0 && fossil_strcmp(p->azCChild[i-1], zUuid)>=0 ){ |
| 656 | SYNTAX("M-card in the wrong order"); |
| 657 | } |
| 658 | break; |
| 659 | } |
| 660 | |
| 661 | /* |
| 662 | ** N <uuid> |
| @@ -717,10 +840,11 @@ | |
| 717 | p->aCherrypick[n].zCPTarget = zUuid; |
| 718 | p->aCherrypick[n].zCPBase = zUuid = next_token(&x, &sz); |
| 719 | if( zUuid && !hname_validate(zUuid,sz) ){ |
| 720 | SYNTAX("invalid second hash on Q-card"); |
| 721 | } |
| 722 | break; |
| 723 | } |
| 724 | |
| 725 | /* |
| 726 | ** R <md5sum> |
| @@ -731,10 +855,11 @@ | |
| 731 | case 'R': { |
| 732 | if( p->zRepoCksum!=0 ) SYNTAX("more than one R-card"); |
| 733 | p->zRepoCksum = next_token(&x, &sz); |
| 734 | if( sz!=32 ) SYNTAX("wrong size cksum on R-card"); |
| 735 | if( !validate16(p->zRepoCksum, 32) ) SYNTAX("malformed R-card cksum"); |
| 736 | break; |
| 737 | } |
| 738 | |
| 739 | /* |
| 740 | ** T (+|*|-)<tagname> <uuid> ?<value>? |
| @@ -759,24 +884,21 @@ | |
| 759 | if( zUuid==0 ) SYNTAX("missing artifact hash on T-card"); |
| 760 | zValue = next_token(&x, 0); |
| 761 | if( zValue ) defossilize(zValue); |
| 762 | if( hname_validate(zUuid, sz) ){ |
| 763 | /* A valid artifact hash */ |
| 764 | if( p->zEventId ) SYNTAX("non-self-referential T-card in event"); |
| 765 | }else if( sz==1 && zUuid[0]=='*' ){ |
| 766 | zUuid = 0; |
| 767 | hasSelfRefTag = 1; |
| 768 | if( p->zEventId && zName[0]!='+' ){ |
| 769 | SYNTAX("propagating T-card in event"); |
| 770 | } |
| 771 | }else{ |
| 772 | SYNTAX("malformed artifact hash on T-card"); |
| 773 | } |
| 774 | defossilize(zName); |
| 775 | if( zName[0]!='-' && zName[0]!='+' && zName[0]!='*' ){ |
| 776 | SYNTAX("T-card name does not begin with '-', '+', or '*'"); |
| 777 | } |
| 778 | if( validate16(&zName[1], strlen(&zName[1])) ){ |
| 779 | /* Do not allow tags whose names look like a hash */ |
| 780 | SYNTAX("T-card name looks like a hexadecimal hash"); |
| 781 | } |
| 782 | if( p->nTag>=p->nTagAlloc ){ |
| @@ -858,104 +980,78 @@ | |
| 858 | */ |
| 859 | case 'Z': { |
| 860 | zUuid = next_token(&x, &sz); |
| 861 | if( sz!=32 ) SYNTAX("wrong size for Z-card cksum"); |
| 862 | if( !validate16(zUuid, 32) ) SYNTAX("malformed Z-card cksum"); |
| 863 | seenZ = 1; |
| 864 | break; |
| 865 | } |
| 866 | default: { |
| 867 | SYNTAX("unrecognized card"); |
| 868 | } |
| 869 | } |
| 870 | } |
| 871 | if( x.z<x.zEnd ) SYNTAX("extra characters at end of card"); |
| 872 | |
| 873 | if( p->nCChild>0 ){ |
| 874 | if( p->zAttachName |
| 875 | || p->zBaseline |
| 876 | || p->zComment |
| 877 | || p->rDate>0.0 |
| 878 | || p->zEventId |
| 879 | || p->nFile>0 |
| 880 | || p->nField>0 |
| 881 | || p->zTicketUuid |
| 882 | || p->zWikiTitle |
| 883 | || p->zMimetype |
| 884 | || p->nParent>0 |
| 885 | || p->nCherrypick>0 |
| 886 | || p->zRepoCksum |
| 887 | || p->nTag>0 |
| 888 | || p->zUser |
| 889 | || p->zWiki |
| 890 | ){ |
| 891 | SYNTAX("cluster contains a card other than M- or Z-"); |
| 892 | } |
| 893 | if( !seenZ ) SYNTAX("missing Z-card on cluster"); |
| 894 | p->type = CFTYPE_CLUSTER; |
| 895 | }else if( p->zEventId ){ |
| 896 | if( p->zAttachName ) SYNTAX("A-card in event"); |
| 897 | if( p->zBaseline ) SYNTAX("B-card in event"); |
| 898 | if( p->rDate<=0.0 ) SYNTAX("missing date on event"); |
| 899 | if( p->nFile>0 ) SYNTAX("F-card in event"); |
| 900 | if( p->nField>0 ) SYNTAX("J-card in event"); |
| 901 | if( p->zTicketUuid ) SYNTAX("K-card in event"); |
| 902 | if( p->zWikiTitle!=0 ) SYNTAX("L-card in event"); |
| 903 | if( p->zRepoCksum ) SYNTAX("R-card in event"); |
| 904 | if( p->zWiki==0 ) SYNTAX("missing W-card on event"); |
| 905 | if( !seenZ ) SYNTAX("missing Z-card on event"); |
| 906 | p->type = CFTYPE_EVENT; |
| 907 | }else if( p->zWiki!=0 || p->zWikiTitle!=0 ){ |
| 908 | if( p->zAttachName ) SYNTAX("A-card in wiki"); |
| 909 | if( p->zBaseline ) SYNTAX("B-card in wiki"); |
| 910 | if( p->rDate<=0.0 ) SYNTAX("missing date on wiki"); |
| 911 | if( p->nFile>0 ) SYNTAX("F-card in wiki"); |
| 912 | if( p->nField>0 ) SYNTAX("J-card in wiki"); |
| 913 | if( p->zTicketUuid ) SYNTAX("K-card in wiki"); |
| 914 | if( p->zWikiTitle==0 ) SYNTAX("missing L-card on wiki"); |
| 915 | if( p->zRepoCksum ) SYNTAX("R-card in wiki"); |
| 916 | if( p->nTag>0 ) SYNTAX("T-card in wiki"); |
| 917 | if( p->zWiki==0 ) SYNTAX("missing W-card on wiki"); |
| 918 | if( !seenZ ) SYNTAX("missing Z-card on wiki"); |
| 919 | p->type = CFTYPE_WIKI; |
| 920 | }else if( hasSelfRefTag || p->nFile>0 || p->zRepoCksum!=0 || p->zBaseline |
| 921 | || p->nParent>0 ){ |
| 922 | if( p->zAttachName ) SYNTAX("A-card in manifest"); |
| 923 | if( p->rDate<=0.0 ) SYNTAX("missing date on manifest"); |
| 924 | if( p->nField>0 ) SYNTAX("J-card in manifest"); |
| 925 | if( p->zTicketUuid ) SYNTAX("K-card in manifest"); |
| 926 | p->type = CFTYPE_MANIFEST; |
| 927 | }else if( p->nField>0 || p->zTicketUuid!=0 ){ |
| 928 | if( p->zAttachName ) SYNTAX("A-card in ticket"); |
| 929 | if( p->rDate<=0.0 ) SYNTAX("missing date on ticket"); |
| 930 | if( p->nField==0 ) SYNTAX("missing J-card on ticket"); |
| 931 | if( p->zTicketUuid==0 ) SYNTAX("missing K-card on ticket"); |
| 932 | if( p->zMimetype) SYNTAX("N-card in ticket"); |
| 933 | if( p->nTag>0 ) SYNTAX("T-card in ticket"); |
| 934 | if( p->zUser==0 ) SYNTAX("missing U-card on ticket"); |
| 935 | if( !seenZ ) SYNTAX("missing Z-card on ticket"); |
| 936 | p->type = CFTYPE_TICKET; |
| 937 | }else if( p->zAttachName ){ |
| 938 | if( p->rDate<=0.0 ) SYNTAX("missing date on attachment"); |
| 939 | if( p->nTag>0 ) SYNTAX("T-card in attachment"); |
| 940 | if( !seenZ ) SYNTAX("missing Z-card on attachment"); |
| 941 | p->type = CFTYPE_ATTACHMENT; |
| 942 | }else{ |
| 943 | if( p->rDate<=0.0 ) SYNTAX("missing date on control"); |
| 944 | if( p->zMimetype ) SYNTAX("N-card in control"); |
| 945 | if( !seenZ ) SYNTAX("missing Z-card on control"); |
| 946 | p->type = CFTYPE_CONTROL; |
| 947 | } |
| 948 | md5sum_init(); |
| 949 | if( !isRepeat ) g.parseCnt[p->type]++; |
| 950 | return p; |
| 951 | |
| 952 | manifest_syntax_error: |
| 953 | { |
| 954 | char *zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid); |
| 955 | if( zUuid ){ |
| 956 | blob_appendf(pErr, "manifest [%s] ", zUuid); |
| 957 | fossil_free(zUuid); |
| 958 | } |
| 959 | } |
| 960 | if( zErr ){ |
| 961 | blob_appendf(pErr, "line %d: %s", lineNo, zErr); |
| @@ -1015,11 +1111,12 @@ | |
| 1015 | /* |
| 1016 | ** COMMAND: test-parse-manifest |
| 1017 | ** |
| 1018 | ** Usage: %fossil test-parse-manifest FILENAME ?N? |
| 1019 | ** |
| 1020 | ** Parse the manifest and discarded. Use for testing only. |
| 1021 | */ |
| 1022 | void manifest_test_parse_cmd(void){ |
| 1023 | Manifest *p; |
| 1024 | Blob b; |
| 1025 | int i; |
| @@ -1039,10 +1136,47 @@ | |
| 1039 | if( p==0 ) fossil_print("ERROR: %s\n", blob_str(&err)); |
| 1040 | blob_reset(&err); |
| 1041 | manifest_destroy(p); |
| 1042 | } |
| 1043 | } |
| 1044 | |
| 1045 | /* |
| 1046 | ** Fetch the baseline associated with the delta-manifest p. |
| 1047 | ** Return 0 on success. If unable to parse the baseline, |
| 1048 | ** throw an error. If the baseline is a manifest, throw an |
| @@ -1059,11 +1193,11 @@ | |
| 1059 | "INSERT OR IGNORE INTO orphan(rid, baseline) VALUES(%d,%d)", |
| 1060 | p->rid, rid |
| 1061 | ); |
| 1062 | return 1; |
| 1063 | } |
| 1064 | fossil_panic("cannot access baseline manifest %S", p->zBaseline); |
| 1065 | } |
| 1066 | } |
| 1067 | return 0; |
| 1068 | } |
| 1069 | |
| @@ -2378,10 +2512,71 @@ | |
| 2378 | "REPLACE INTO event(type,mtime,objid,user,comment)" |
| 2379 | "VALUES('g',%.17g,%d,%Q,%Q)", |
| 2380 | p->rDate, rid, p->zUser, blob_str(&comment)+1 |
| 2381 | ); |
| 2382 | blob_reset(&comment); |
| 2383 | } |
| 2384 | db_end_transaction(0); |
| 2385 | if( permitHooks ){ |
| 2386 | rc = xfer_run_common_script(); |
| 2387 | if( rc==TH_OK ){ |
| 2388 |
| --- src/manifest.c | |
| +++ src/manifest.c | |
| @@ -34,10 +34,11 @@ | |
| 34 | #define CFTYPE_CONTROL 3 |
| 35 | #define CFTYPE_WIKI 4 |
| 36 | #define CFTYPE_TICKET 5 |
| 37 | #define CFTYPE_ATTACHMENT 6 |
| 38 | #define CFTYPE_EVENT 7 |
| 39 | #define CFTYPE_FORUM 8 |
| 40 | |
| 41 | /* |
| 42 | ** File permissions used by Fossil internally. |
| 43 | */ |
| 44 | #define PERM_REG 0 /* regular file */ |
| @@ -76,16 +77,19 @@ | |
| 77 | char *zUser; /* Name of the user from the U card. */ |
| 78 | char *zRepoCksum; /* MD5 checksum of the baseline content. R card. */ |
| 79 | char *zWiki; /* Text of the wiki page. W card. */ |
| 80 | char *zWikiTitle; /* Name of the wiki page. L card. */ |
| 81 | char *zMimetype; /* Mime type of wiki or comment text. N card. */ |
| 82 | char *zThreadTitle; /* The forum thread title. H card */ |
| 83 | double rEventDate; /* Date of an event. E card. */ |
| 84 | char *zEventId; /* Artifact hash for an event. E card. */ |
| 85 | char *zTicketUuid; /* UUID for a ticket. K card. */ |
| 86 | char *zAttachName; /* Filename of an attachment. A card. */ |
| 87 | char *zAttachSrc; /* Artifact hash for document being attached. A card. */ |
| 88 | char *zAttachTarget; /* Ticket or wiki that attachment applies to. A card */ |
| 89 | char *zThreadRoot; /* Thread root artifact. G card */ |
| 90 | char *zInReplyTo; /* Forum in-reply-to artifact. I card */ |
| 91 | int nFile; /* Number of F cards */ |
| 92 | int nFileAlloc; /* Slots allocated in aFile[] */ |
| 93 | int iFile; /* Index of current file in iterator */ |
| 94 | ManifestFile *aFile; /* One entry for each F-card */ |
| 95 | int nParent; /* Number of parents. */ |
| @@ -112,10 +116,42 @@ | |
| 116 | char *zName; /* Key or field name */ |
| 117 | char *zValue; /* Value of the field */ |
| 118 | } *aField; /* One for each J card */ |
| 119 | }; |
| 120 | #endif |
| 121 | |
| 122 | /* |
| 123 | ** Allowed and required card types in each style of artifact |
| 124 | */ |
| 125 | static struct { |
| 126 | const char *zAllowed; /* Allowed cards. Human-readable */ |
| 127 | const char *zRequired; /* Required cards. Human-readable */ |
| 128 | } manifestCardTypes[] = { |
| 129 | /* Allowed Required */ |
| 130 | /* CFTYPE_MANIFEST 1 */ { "BCDFNPQRTUZ", "CDUZ" }, |
| 131 | /* CFTYPE_CLUSTER 2 */ { "MZ", "MZ" }, |
| 132 | /* CFTYPE_CONTROL 3 */ { "DTUZ", "DTUZ" }, |
| 133 | /* CFTYPE_WIKI 4 */ { "DLNPUWZ", "DLUWZ" }, |
| 134 | /* CFTYPE_TICKET 5 */ { "DJKUZ", "DJKUZ" }, |
| 135 | /* CFTYPE_ATTACHMENT 6 */ { "ACDNUZ", "ADZ" }, |
| 136 | /* CFTYPE_EVENT 7 */ { "CDENPTUWZ", "DEWZ" }, |
| 137 | /* CFTYPE_FORUM 8 */ { "DGHINPUWZ", "DUWZ" }, |
| 138 | }; |
| 139 | |
| 140 | /* |
| 141 | ** Names of manifest types |
| 142 | */ |
| 143 | static const char *azNameOfMType[] = { |
| 144 | "manifest", |
| 145 | "cluster", |
| 146 | "tag", |
| 147 | "wiki", |
| 148 | "ticket", |
| 149 | "attachment", |
| 150 | "technote", |
| 151 | "forum post" |
| 152 | }; |
| 153 | |
| 154 | /* |
| 155 | ** A cache of parsed manifests. This reduces the number of |
| 156 | ** calls to manifest_parse() when doing a rebuild. |
| 157 | */ |
| @@ -147,10 +183,35 @@ | |
| 183 | if( p->pBaseline ) manifest_destroy(p->pBaseline); |
| 184 | memset(p, 0, sizeof(*p)); |
| 185 | fossil_free(p); |
| 186 | } |
| 187 | } |
| 188 | |
| 189 | /* |
| 190 | ** Given a string of upper-case letters, compute a mask of the letters |
| 191 | ** present. For example, "ABC" computes 0x0007. "DE" gives 0x0018". |
| 192 | */ |
| 193 | static unsigned int manifest_card_mask(const char *z){ |
| 194 | unsigned int m = 0; |
| 195 | char c; |
| 196 | while( (c = *(z++))>='A' && c<='Z' ){ |
| 197 | m |= 1 << (c - 'A'); |
| 198 | } |
| 199 | return m; |
| 200 | } |
| 201 | |
| 202 | /* |
| 203 | ** Given an integer mask representing letters A-Z, return the |
| 204 | ** letter which is the first bit set in the mask. Example: |
| 205 | ** 0x03520 gives 'F' since the F-bit is the lowest. |
| 206 | */ |
| 207 | static char maskToType(unsigned int x){ |
| 208 | char c = 'A'; |
| 209 | if( x==0 ) return '?'; |
| 210 | while( (x&1)==0 ){ x >>= 1; c++; } |
| 211 | return c; |
| 212 | } |
| 213 | |
| 214 | /* |
| 215 | ** Add an element to the manifest cache using LRU replacement. |
| 216 | */ |
| 217 | void manifest_cache_insert(Manifest *p){ |
| @@ -352,22 +413,26 @@ | |
| 413 | ** The card type determines the other parameters to the card. |
| 414 | ** Cards must occur in lexicographical order. |
| 415 | */ |
| 416 | Manifest *manifest_parse(Blob *pContent, int rid, Blob *pErr){ |
| 417 | Manifest *p; |
| 418 | int i, lineNo=0; |
| 419 | ManifestText x; |
| 420 | char cPrevType = 0; |
| 421 | char cType; |
| 422 | char *z; |
| 423 | int n; |
| 424 | char *zUuid; |
| 425 | int sz = 0; |
| 426 | int isRepeat; |
| 427 | int nSelfTag = 0; /* Number of T cards referring to this manifest */ |
| 428 | int nSimpleTag = 0; /* Number of T cards with "+" prefix */ |
| 429 | static Bag seen; |
| 430 | const char *zErr = 0; |
| 431 | unsigned int m; |
| 432 | unsigned int seenCard = 0; /* Which card types have been seen */ |
| 433 | char zErrBuf[100]; /* Write error messages here */ |
| 434 | |
| 435 | if( rid==0 ){ |
| 436 | isRepeat = 1; |
| 437 | }else if( bag_find(&seen, rid) ){ |
| 438 | isRepeat = 1; |
| @@ -422,10 +487,12 @@ | |
| 487 | x.z = z; |
| 488 | x.zEnd = &z[n]; |
| 489 | x.atEol = 1; |
| 490 | while( (cType = next_card(&x))!=0 && cType>=cPrevType ){ |
| 491 | lineNo++; |
| 492 | if( cType<'A' || cType>'Z' ) SYNTAX("bad card type"); |
| 493 | seenCard |= 1 << (cType-'A'); |
| 494 | switch( cType ){ |
| 495 | /* |
| 496 | ** A <filename> <target> ?<source>? |
| 497 | ** |
| 498 | ** Identifies an attachment to either a wiki page or a ticket. |
| @@ -454,10 +521,11 @@ | |
| 521 | SYNTAX("invalid source on A-card"); |
| 522 | } |
| 523 | p->zAttachName = (char*)file_tail(zName); |
| 524 | p->zAttachSrc = zSrc; |
| 525 | p->zAttachTarget = zTarget; |
| 526 | p->type = CFTYPE_ATTACHMENT; |
| 527 | break; |
| 528 | } |
| 529 | |
| 530 | /* |
| 531 | ** B <uuid> |
| @@ -469,10 +537,11 @@ | |
| 537 | p->zBaseline = next_token(&x, &sz); |
| 538 | if( p->zBaseline==0 ) SYNTAX("missing hash on B-card"); |
| 539 | if( !hname_validate(p->zBaseline,sz) ){ |
| 540 | SYNTAX("invalid hash on B-card"); |
| 541 | } |
| 542 | p->type = CFTYPE_MANIFEST; |
| 543 | break; |
| 544 | } |
| 545 | |
| 546 | |
| 547 | /* |
| @@ -520,10 +589,11 @@ | |
| 589 | if( p->rEventDate<=0.0 ) SYNTAX("malformed date on E-card"); |
| 590 | p->zEventId = next_token(&x, &sz); |
| 591 | if( !hname_validate(p->zEventId, sz) ){ |
| 592 | SYNTAX("malformed hash on E-card"); |
| 593 | } |
| 594 | p->type = CFTYPE_EVENT; |
| 595 | break; |
| 596 | } |
| 597 | |
| 598 | /* |
| 599 | ** F <filename> ?<uuid>? ?<permissions>? ?<old-name>? |
| @@ -565,10 +635,59 @@ | |
| 635 | p->aFile[i].zPerm = zPerm; |
| 636 | p->aFile[i].zPrior = zPriorName; |
| 637 | if( i>0 && fossil_strcmp(p->aFile[i-1].zName, zName)>=0 ){ |
| 638 | SYNTAX("incorrect F-card sort order"); |
| 639 | } |
| 640 | p->type = CFTYPE_MANIFEST; |
| 641 | break; |
| 642 | } |
| 643 | |
| 644 | /* |
| 645 | ** G <hash> |
| 646 | ** |
| 647 | ** A G-card identifies the initial root forum post for the thread |
| 648 | ** of which this post is a part. Forum posts only. |
| 649 | */ |
| 650 | case 'G': { |
| 651 | if( p->zThreadRoot!=0 ) SYNTAX("more than one G-card"); |
| 652 | p->zThreadRoot = next_token(&x, &sz); |
| 653 | if( p->zThreadRoot==0 ) SYNTAX("missing hash on G-card"); |
| 654 | if( !hname_validate(p->zThreadRoot,sz) ){ |
| 655 | SYNTAX("Invalid hash on G-card"); |
| 656 | } |
| 657 | p->type = CFTYPE_FORUM; |
| 658 | break; |
| 659 | } |
| 660 | |
| 661 | /* |
| 662 | ** H <threadtitle> |
| 663 | ** |
| 664 | ** The title for a forum thread. |
| 665 | */ |
| 666 | case 'H': { |
| 667 | if( p->zThreadTitle!=0 ) SYNTAX("more than one H-card"); |
| 668 | p->zThreadTitle = next_token(&x,0); |
| 669 | if( p->zThreadTitle==0 ) SYNTAX("missing title on H-card"); |
| 670 | defossilize(p->zThreadTitle); |
| 671 | p->type = CFTYPE_FORUM; |
| 672 | break; |
| 673 | } |
| 674 | |
| 675 | /* |
| 676 | ** I <hash> |
| 677 | ** |
| 678 | ** A I-card identifies another forum post that the current forum post |
| 679 | ** is in reply to. |
| 680 | */ |
| 681 | case 'I': { |
| 682 | if( p->zInReplyTo!=0 ) SYNTAX("more than one I-card"); |
| 683 | p->zInReplyTo = next_token(&x, &sz); |
| 684 | if( p->zInReplyTo==0 ) SYNTAX("missing hash on I-card"); |
| 685 | if( !hname_validate(p->zInReplyTo,sz) ){ |
| 686 | SYNTAX("Invalid hash on I-card"); |
| 687 | } |
| 688 | p->type = CFTYPE_FORUM; |
| 689 | break; |
| 690 | } |
| 691 | |
| 692 | /* |
| 693 | ** J <name> ?<value>? |
| @@ -594,10 +713,11 @@ | |
| 713 | p->aField[i].zName = zName; |
| 714 | p->aField[i].zValue = zValue; |
| 715 | if( i>0 && fossil_strcmp(p->aField[i-1].zName, zName)>=0 ){ |
| 716 | SYNTAX("incorrect J-card sort order"); |
| 717 | } |
| 718 | p->type = CFTYPE_TICKET; |
| 719 | break; |
| 720 | } |
| 721 | |
| 722 | |
| 723 | /* |
| @@ -611,10 +731,11 @@ | |
| 731 | p->zTicketUuid = next_token(&x, &sz); |
| 732 | if( sz!=HNAME_LEN_SHA1 ) SYNTAX("K-card UUID is the wrong size"); |
| 733 | if( !validate16(p->zTicketUuid, sz) ){ |
| 734 | SYNTAX("invalid K-card UUID"); |
| 735 | } |
| 736 | p->type = CFTYPE_TICKET; |
| 737 | break; |
| 738 | } |
| 739 | |
| 740 | /* |
| 741 | ** L <wikititle> |
| @@ -628,10 +749,11 @@ | |
| 749 | if( p->zWikiTitle==0 ) SYNTAX("missing title on L-card"); |
| 750 | defossilize(p->zWikiTitle); |
| 751 | if( !wiki_name_is_wellformed((const unsigned char *)p->zWikiTitle) ){ |
| 752 | SYNTAX("L-card has malformed wiki name"); |
| 753 | } |
| 754 | p->type = CFTYPE_WIKI; |
| 755 | break; |
| 756 | } |
| 757 | |
| 758 | /* |
| 759 | ** M <hash> |
| @@ -653,10 +775,11 @@ | |
| 775 | i = p->nCChild++; |
| 776 | p->azCChild[i] = zUuid; |
| 777 | if( i>0 && fossil_strcmp(p->azCChild[i-1], zUuid)>=0 ){ |
| 778 | SYNTAX("M-card in the wrong order"); |
| 779 | } |
| 780 | p->type = CFTYPE_CLUSTER; |
| 781 | break; |
| 782 | } |
| 783 | |
| 784 | /* |
| 785 | ** N <uuid> |
| @@ -717,10 +840,11 @@ | |
| 840 | p->aCherrypick[n].zCPTarget = zUuid; |
| 841 | p->aCherrypick[n].zCPBase = zUuid = next_token(&x, &sz); |
| 842 | if( zUuid && !hname_validate(zUuid,sz) ){ |
| 843 | SYNTAX("invalid second hash on Q-card"); |
| 844 | } |
| 845 | p->type = CFTYPE_MANIFEST; |
| 846 | break; |
| 847 | } |
| 848 | |
| 849 | /* |
| 850 | ** R <md5sum> |
| @@ -731,10 +855,11 @@ | |
| 855 | case 'R': { |
| 856 | if( p->zRepoCksum!=0 ) SYNTAX("more than one R-card"); |
| 857 | p->zRepoCksum = next_token(&x, &sz); |
| 858 | if( sz!=32 ) SYNTAX("wrong size cksum on R-card"); |
| 859 | if( !validate16(p->zRepoCksum, 32) ) SYNTAX("malformed R-card cksum"); |
| 860 | p->type = CFTYPE_MANIFEST; |
| 861 | break; |
| 862 | } |
| 863 | |
| 864 | /* |
| 865 | ** T (+|*|-)<tagname> <uuid> ?<value>? |
| @@ -759,24 +884,21 @@ | |
| 884 | if( zUuid==0 ) SYNTAX("missing artifact hash on T-card"); |
| 885 | zValue = next_token(&x, 0); |
| 886 | if( zValue ) defossilize(zValue); |
| 887 | if( hname_validate(zUuid, sz) ){ |
| 888 | /* A valid artifact hash */ |
| 889 | }else if( sz==1 && zUuid[0]=='*' ){ |
| 890 | zUuid = 0; |
| 891 | nSelfTag++; |
| 892 | }else{ |
| 893 | SYNTAX("malformed artifact hash on T-card"); |
| 894 | } |
| 895 | defossilize(zName); |
| 896 | if( zName[0]!='-' && zName[0]!='+' && zName[0]!='*' ){ |
| 897 | SYNTAX("T-card name does not begin with '-', '+', or '*'"); |
| 898 | } |
| 899 | if( zName[0]=='+' ) nSimpleTag++; |
| 900 | if( validate16(&zName[1], strlen(&zName[1])) ){ |
| 901 | /* Do not allow tags whose names look like a hash */ |
| 902 | SYNTAX("T-card name looks like a hexadecimal hash"); |
| 903 | } |
| 904 | if( p->nTag>=p->nTagAlloc ){ |
| @@ -858,104 +980,78 @@ | |
| 980 | */ |
| 981 | case 'Z': { |
| 982 | zUuid = next_token(&x, &sz); |
| 983 | if( sz!=32 ) SYNTAX("wrong size for Z-card cksum"); |
| 984 | if( !validate16(zUuid, 32) ) SYNTAX("malformed Z-card cksum"); |
| 985 | break; |
| 986 | } |
| 987 | default: { |
| 988 | SYNTAX("unrecognized card"); |
| 989 | } |
| 990 | } |
| 991 | } |
| 992 | if( x.z<x.zEnd ) SYNTAX("extra characters at end of card"); |
| 993 | |
| 994 | /* If the artifact type has not yet been determined, then compute |
| 995 | ** it now. */ |
| 996 | if( p->type==0 ){ |
| 997 | p->type = p->zComment!=0 ? CFTYPE_MANIFEST : CFTYPE_CONTROL; |
| 998 | } |
| 999 | |
| 1000 | /* Verify that no disallowed cards are present for this artifact type */ |
| 1001 | m = manifest_card_mask(manifestCardTypes[p->type-1].zAllowed); |
| 1002 | if( seenCard & ~m ){ |
| 1003 | sqlite3_snprintf(sizeof(zErrBuf), zErrBuf, "%c-card in %s", |
| 1004 | maskToType(seenCard & ~m), |
| 1005 | azNameOfMType[p->type-1]); |
| 1006 | zErr = zErrBuf; |
| 1007 | goto manifest_syntax_error; |
| 1008 | } |
| 1009 | |
| 1010 | /* Verify that all required cards are present for this artifact type */ |
| 1011 | m = manifest_card_mask(manifestCardTypes[p->type-1].zRequired); |
| 1012 | if( ~seenCard & m ){ |
| 1013 | sqlite3_snprintf(sizeof(zErrBuf), zErrBuf, "%c-card missing in %s", |
| 1014 | maskToType(~seenCard & m), |
| 1015 | azNameOfMType[p->type-1]); |
| 1016 | zErr = zErrBuf; |
| 1017 | goto manifest_syntax_error; |
| 1018 | } |
| 1019 | |
| 1020 | /* Additional checks based on artifact type */ |
| 1021 | switch( p->type ){ |
| 1022 | case CFTYPE_CONTROL: { |
| 1023 | if( nSelfTag ) SYNTAX("self-referential T-card in control artifact"); |
| 1024 | break; |
| 1025 | } |
| 1026 | case CFTYPE_EVENT: { |
| 1027 | if( p->nTag!=nSelfTag ){ |
| 1028 | SYNTAX("non-self-referential T-card in technote"); |
| 1029 | } |
| 1030 | if( p->nTag!=nSimpleTag ){ |
| 1031 | SYNTAX("T-card with '*' or '-' in technote"); |
| 1032 | } |
| 1033 | break; |
| 1034 | } |
| 1035 | case CFTYPE_FORUM: { |
| 1036 | if( p->zThreadTitle && p->zInReplyTo ){ |
| 1037 | SYNTAX("cannot have I-card and H-card in a forum post"); |
| 1038 | } |
| 1039 | if( p->nParent>1 ) SYNTAX("too many arguments to P-card"); |
| 1040 | break; |
| 1041 | } |
| 1042 | } |
| 1043 | |
| 1044 | md5sum_init(); |
| 1045 | if( !isRepeat ) g.parseCnt[p->type]++; |
| 1046 | return p; |
| 1047 | |
| 1048 | manifest_syntax_error: |
| 1049 | { |
| 1050 | char *zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid); |
| 1051 | if( zUuid ){ |
| 1052 | blob_appendf(pErr, "artifact [%s] ", zUuid); |
| 1053 | fossil_free(zUuid); |
| 1054 | } |
| 1055 | } |
| 1056 | if( zErr ){ |
| 1057 | blob_appendf(pErr, "line %d: %s", lineNo, zErr); |
| @@ -1015,11 +1111,12 @@ | |
| 1111 | /* |
| 1112 | ** COMMAND: test-parse-manifest |
| 1113 | ** |
| 1114 | ** Usage: %fossil test-parse-manifest FILENAME ?N? |
| 1115 | ** |
| 1116 | ** Parse the manifest(s) given on the command-line and report any |
| 1117 | ** errors. If the N argument is given, run the parsing N times. |
| 1118 | */ |
| 1119 | void manifest_test_parse_cmd(void){ |
| 1120 | Manifest *p; |
| 1121 | Blob b; |
| 1122 | int i; |
| @@ -1039,10 +1136,47 @@ | |
| 1136 | if( p==0 ) fossil_print("ERROR: %s\n", blob_str(&err)); |
| 1137 | blob_reset(&err); |
| 1138 | manifest_destroy(p); |
| 1139 | } |
| 1140 | } |
| 1141 | |
| 1142 | /* |
| 1143 | ** COMMAND: test-parse-all-blobs |
| 1144 | ** |
| 1145 | ** Usage: %fossil test-parse-all-blobs |
| 1146 | ** |
| 1147 | ** Parse all entries in the BLOB table that are believed to be non-data |
| 1148 | ** artifacts and report any errors. Run this test command on historical |
| 1149 | ** repositories after making any changes to the manifest_parse() |
| 1150 | ** implementation to confirm that the changes did not break anything. |
| 1151 | */ |
| 1152 | void manifest_test_parse_all_blobs_cmd(void){ |
| 1153 | Manifest *p; |
| 1154 | Blob err; |
| 1155 | Stmt q; |
| 1156 | int nTest = 0; |
| 1157 | int nErr = 0; |
| 1158 | db_find_and_open_repository(0, 0); |
| 1159 | verify_all_options(); |
| 1160 | db_prepare(&q, "SELECT DISTINCT objid FROM EVENT"); |
| 1161 | while( db_step(&q)==SQLITE_ROW ){ |
| 1162 | int id = db_column_int(&q,0); |
| 1163 | fossil_print("Checking %d \r", id); |
| 1164 | nTest++; |
| 1165 | fflush(stdout); |
| 1166 | blob_init(&err, 0, 0); |
| 1167 | p = manifest_get(id, CFTYPE_ANY, &err); |
| 1168 | if( p==0 ){ |
| 1169 | fossil_print("%d ERROR: %s\n", id, blob_str(&err)); |
| 1170 | nErr++; |
| 1171 | } |
| 1172 | blob_reset(&err); |
| 1173 | manifest_destroy(p); |
| 1174 | } |
| 1175 | db_finalize(&q); |
| 1176 | fossil_print("%d tests with %d errors\n", nTest, nErr); |
| 1177 | } |
| 1178 | |
| 1179 | /* |
| 1180 | ** Fetch the baseline associated with the delta-manifest p. |
| 1181 | ** Return 0 on success. If unable to parse the baseline, |
| 1182 | ** throw an error. If the baseline is a manifest, throw an |
| @@ -1059,11 +1193,11 @@ | |
| 1193 | "INSERT OR IGNORE INTO orphan(rid, baseline) VALUES(%d,%d)", |
| 1194 | p->rid, rid |
| 1195 | ); |
| 1196 | return 1; |
| 1197 | } |
| 1198 | fossil_fatal("cannot access baseline manifest %S", p->zBaseline); |
| 1199 | } |
| 1200 | } |
| 1201 | return 0; |
| 1202 | } |
| 1203 | |
| @@ -2378,10 +2512,71 @@ | |
| 2512 | "REPLACE INTO event(type,mtime,objid,user,comment)" |
| 2513 | "VALUES('g',%.17g,%d,%Q,%Q)", |
| 2514 | p->rDate, rid, p->zUser, blob_str(&comment)+1 |
| 2515 | ); |
| 2516 | blob_reset(&comment); |
| 2517 | } |
| 2518 | if( p->type==CFTYPE_FORUM ){ |
| 2519 | int froot, fprev, firt; |
| 2520 | char *zFType; |
| 2521 | char *zTitle; |
| 2522 | schema_forum(); |
| 2523 | froot = p->zThreadRoot ? uuid_to_rid(p->zThreadRoot, 1) : rid; |
| 2524 | fprev = p->nParent ? uuid_to_rid(p->azParent[0],1) : 0; |
| 2525 | firt = p->zInReplyTo ? uuid_to_rid(p->zInReplyTo,1) : 0; |
| 2526 | db_multi_exec( |
| 2527 | "INSERT INTO forumpost(fpid,froot,fprev,firt,fmtime)" |
| 2528 | "VALUES(%d,%d,nullif(%d,0),nullif(%d,0),%.17g)", |
| 2529 | p->rid, froot, fprev, firt, p->rDate |
| 2530 | ); |
| 2531 | if( firt==0 ){ |
| 2532 | /* This is the start of a new thread, either the initial entry |
| 2533 | ** or an edit of the initial entry. */ |
| 2534 | zTitle = p->zThreadTitle; |
| 2535 | if( zTitle==0 || zTitle[0]==0 ){ |
| 2536 | zTitle = "(Deleted)"; |
| 2537 | } |
| 2538 | zFType = fprev ? "Edit" : "Post"; |
| 2539 | db_multi_exec( |
| 2540 | "REPLACE INTO event(type,mtime,objid,user,comment)" |
| 2541 | "VALUES('f',%.17g,%d,%Q,'%q: %q')", |
| 2542 | p->rDate, rid, p->zUser, zFType, zTitle |
| 2543 | ); |
| 2544 | /* |
| 2545 | ** If this edit is the most recent, then make it the title for |
| 2546 | ** all other entries for the same thread |
| 2547 | */ |
| 2548 | if( !db_exists("SELECT 1 FROM forumpost WHERE froot=%d AND firt=0" |
| 2549 | " AND fpid!=%d AND fmtime>%.17g", froot, rid, p->rDate) |
| 2550 | ){ |
| 2551 | /* This entry establishes a new title for all entries on the thread */ |
| 2552 | db_multi_exec( |
| 2553 | "UPDATE event" |
| 2554 | " SET comment=substr(comment,1,instr(comment,':')) || ' %q'" |
| 2555 | " WHERE objid IN (SELECT fpid FROM forumpost WHERE froot=%d)", |
| 2556 | zTitle, froot |
| 2557 | ); |
| 2558 | } |
| 2559 | }else{ |
| 2560 | /* This is a reply to a prior post. Take the title from the root. */ |
| 2561 | zTitle = db_text(0, "SELECT substr(comment,instr(comment,':')+2)" |
| 2562 | " FROM event WHERE objid=%d", froot); |
| 2563 | if( zTitle==0 ) zTitle = fossil_strdup("<i>Unknown</i>"); |
| 2564 | if( p->zWiki[0]==0 ){ |
| 2565 | zFType = "Delete reply"; |
| 2566 | }else if( fprev ){ |
| 2567 | zFType = "Edit reply"; |
| 2568 | }else{ |
| 2569 | zFType = "Reply"; |
| 2570 | } |
| 2571 | db_multi_exec( |
| 2572 | "REPLACE INTO event(type,mtime,objid,user,comment)" |
| 2573 | "VALUES('f',%.17g,%d,%Q,'%q: %q')", |
| 2574 | p->rDate, rid, p->zUser, zFType, zTitle |
| 2575 | ); |
| 2576 | fossil_free(zTitle); |
| 2577 | } |
| 2578 | } |
| 2579 | db_end_transaction(0); |
| 2580 | if( permitHooks ){ |
| 2581 | rc = xfer_run_common_script(); |
| 2582 | if( rc==TH_OK ){ |
| 2583 |
+29
-2
| --- src/moderate.c | ||
| +++ src/moderate.c | ||
| @@ -54,10 +54,34 @@ | ||
| 54 | 54 | db_bind_int(&q, ":objid", rid); |
| 55 | 55 | rc = db_step(&q)==SQLITE_ROW; |
| 56 | 56 | db_reset(&q); |
| 57 | 57 | return rc; |
| 58 | 58 | } |
| 59 | + | |
| 60 | +/* | |
| 61 | +** If the rid object is being held for moderation, write out | |
| 62 | +** an "awaiting moderation" message and return true. | |
| 63 | +** | |
| 64 | +** If the object is not being held for moderation, simply return | |
| 65 | +** false without generating any output. | |
| 66 | +*/ | |
| 67 | +int moderation_pending_www(int rid){ | |
| 68 | + int pending = moderation_pending(rid); | |
| 69 | + if( pending ){ | |
| 70 | + @ <span class="modpending">(Awaiting Moderator Approval)</span> | |
| 71 | + } | |
| 72 | + return pending; | |
| 73 | +} | |
| 74 | + | |
| 75 | + | |
| 76 | +/* | |
| 77 | +** Return TRUE if there any pending moderation requests. | |
| 78 | +*/ | |
| 79 | +int moderation_needed(void){ | |
| 80 | + if( !moderation_table_exists() ) return 0; | |
| 81 | + return db_exists("SELECT 1 FROM modreq"); | |
| 82 | +} | |
| 59 | 83 | |
| 60 | 84 | /* |
| 61 | 85 | ** Check to see if the object identified by RID is used for anything. |
| 62 | 86 | */ |
| 63 | 87 | static int object_used(int rid){ |
| @@ -101,10 +125,13 @@ | ||
| 101 | 125 | "DELETE FROM tagxref WHERE rid=%d;" |
| 102 | 126 | "DELETE FROM private WHERE rid=%d;" |
| 103 | 127 | "DELETE FROM attachment WHERE attachid=%d;", |
| 104 | 128 | rid, rid, rid, rid, rid, rid |
| 105 | 129 | ); |
| 130 | + if( db_table_exists("repository","forumpost") ){ | |
| 131 | + db_multi_exec("DELETE FROM forumpost WHERE fpid=%d", rid); | |
| 132 | + } | |
| 106 | 133 | zTktid = db_text(0, "SELECT tktid FROM modreq WHERE objid=%d", rid); |
| 107 | 134 | if( zTktid && zTktid[0] ){ |
| 108 | 135 | ticket_rebuild_entry(zTktid); |
| 109 | 136 | fossil_free(zTktid); |
| 110 | 137 | } |
| @@ -144,12 +171,12 @@ | ||
| 144 | 171 | void modreq_page(void){ |
| 145 | 172 | Blob sql; |
| 146 | 173 | Stmt q; |
| 147 | 174 | |
| 148 | 175 | login_check_credentials(); |
| 149 | - if( !g.perm.ModWiki && !g.perm.ModTkt ){ | |
| 150 | - login_needed(g.anon.ModWiki && g.anon.ModTkt); | |
| 176 | + if( !g.perm.ModWiki && !g.perm.ModTkt && !g.perm.ModForum ){ | |
| 177 | + login_needed(g.anon.ModWiki && g.anon.ModTkt && g.anon.ModForum); | |
| 151 | 178 | return; |
| 152 | 179 | } |
| 153 | 180 | style_header("Pending Moderation Requests"); |
| 154 | 181 | @ <h2>All Pending Moderation Requests</h2> |
| 155 | 182 | if( moderation_table_exists() ){ |
| 156 | 183 |
| --- src/moderate.c | |
| +++ src/moderate.c | |
| @@ -54,10 +54,34 @@ | |
| 54 | db_bind_int(&q, ":objid", rid); |
| 55 | rc = db_step(&q)==SQLITE_ROW; |
| 56 | db_reset(&q); |
| 57 | return rc; |
| 58 | } |
| 59 | |
| 60 | /* |
| 61 | ** Check to see if the object identified by RID is used for anything. |
| 62 | */ |
| 63 | static int object_used(int rid){ |
| @@ -101,10 +125,13 @@ | |
| 101 | "DELETE FROM tagxref WHERE rid=%d;" |
| 102 | "DELETE FROM private WHERE rid=%d;" |
| 103 | "DELETE FROM attachment WHERE attachid=%d;", |
| 104 | rid, rid, rid, rid, rid, rid |
| 105 | ); |
| 106 | zTktid = db_text(0, "SELECT tktid FROM modreq WHERE objid=%d", rid); |
| 107 | if( zTktid && zTktid[0] ){ |
| 108 | ticket_rebuild_entry(zTktid); |
| 109 | fossil_free(zTktid); |
| 110 | } |
| @@ -144,12 +171,12 @@ | |
| 144 | void modreq_page(void){ |
| 145 | Blob sql; |
| 146 | Stmt q; |
| 147 | |
| 148 | login_check_credentials(); |
| 149 | if( !g.perm.ModWiki && !g.perm.ModTkt ){ |
| 150 | login_needed(g.anon.ModWiki && g.anon.ModTkt); |
| 151 | return; |
| 152 | } |
| 153 | style_header("Pending Moderation Requests"); |
| 154 | @ <h2>All Pending Moderation Requests</h2> |
| 155 | if( moderation_table_exists() ){ |
| 156 |
| --- src/moderate.c | |
| +++ src/moderate.c | |
| @@ -54,10 +54,34 @@ | |
| 54 | db_bind_int(&q, ":objid", rid); |
| 55 | rc = db_step(&q)==SQLITE_ROW; |
| 56 | db_reset(&q); |
| 57 | return rc; |
| 58 | } |
| 59 | |
| 60 | /* |
| 61 | ** If the rid object is being held for moderation, write out |
| 62 | ** an "awaiting moderation" message and return true. |
| 63 | ** |
| 64 | ** If the object is not being held for moderation, simply return |
| 65 | ** false without generating any output. |
| 66 | */ |
| 67 | int moderation_pending_www(int rid){ |
| 68 | int pending = moderation_pending(rid); |
| 69 | if( pending ){ |
| 70 | @ <span class="modpending">(Awaiting Moderator Approval)</span> |
| 71 | } |
| 72 | return pending; |
| 73 | } |
| 74 | |
| 75 | |
| 76 | /* |
| 77 | ** Return TRUE if there any pending moderation requests. |
| 78 | */ |
| 79 | int moderation_needed(void){ |
| 80 | if( !moderation_table_exists() ) return 0; |
| 81 | return db_exists("SELECT 1 FROM modreq"); |
| 82 | } |
| 83 | |
| 84 | /* |
| 85 | ** Check to see if the object identified by RID is used for anything. |
| 86 | */ |
| 87 | static int object_used(int rid){ |
| @@ -101,10 +125,13 @@ | |
| 125 | "DELETE FROM tagxref WHERE rid=%d;" |
| 126 | "DELETE FROM private WHERE rid=%d;" |
| 127 | "DELETE FROM attachment WHERE attachid=%d;", |
| 128 | rid, rid, rid, rid, rid, rid |
| 129 | ); |
| 130 | if( db_table_exists("repository","forumpost") ){ |
| 131 | db_multi_exec("DELETE FROM forumpost WHERE fpid=%d", rid); |
| 132 | } |
| 133 | zTktid = db_text(0, "SELECT tktid FROM modreq WHERE objid=%d", rid); |
| 134 | if( zTktid && zTktid[0] ){ |
| 135 | ticket_rebuild_entry(zTktid); |
| 136 | fossil_free(zTktid); |
| 137 | } |
| @@ -144,12 +171,12 @@ | |
| 171 | void modreq_page(void){ |
| 172 | Blob sql; |
| 173 | Stmt q; |
| 174 | |
| 175 | login_check_credentials(); |
| 176 | if( !g.perm.ModWiki && !g.perm.ModTkt && !g.perm.ModForum ){ |
| 177 | login_needed(g.anon.ModWiki && g.anon.ModTkt && g.anon.ModForum); |
| 178 | return; |
| 179 | } |
| 180 | style_header("Pending Moderation Requests"); |
| 181 | @ <h2>All Pending Moderation Requests</h2> |
| 182 | if( moderation_table_exists() ){ |
| 183 |
+19
-3
| --- src/name.c | ||
| +++ src/name.c | ||
| @@ -94,19 +94,19 @@ | ||
| 94 | 94 | ** * "next" |
| 95 | 95 | ** |
| 96 | 96 | ** Return the RID of the matching artifact. Or return 0 if the name does not |
| 97 | 97 | ** match any known object. Or return -1 if the name is ambiguous. |
| 98 | 98 | ** |
| 99 | -** The zType parameter specifies the type of artifact: ci, t, w, e, g. | |
| 99 | +** The zType parameter specifies the type of artifact: ci, t, w, e, g, f. | |
| 100 | 100 | ** If zType is NULL or "" or "*" then any type of artifact will serve. |
| 101 | 101 | ** If zType is "br" then find the first check-in of the named branch |
| 102 | 102 | ** rather than the last. |
| 103 | 103 | ** zType is "ci" in most use cases since we are usually searching for |
| 104 | 104 | ** a check-in. |
| 105 | 105 | ** |
| 106 | 106 | ** Note that the input zTag for types "t" and "e" is the artifact hash of |
| 107 | -** the ticket-change or event-change artifact, not the randomly generated | |
| 107 | +** the ticket-change or technote-change artifact, not the randomly generated | |
| 108 | 108 | ** hexadecimal identifier assigned to tickets and events. Those identifiers |
| 109 | 109 | ** live in a separate namespace. |
| 110 | 110 | */ |
| 111 | 111 | int symbolic_name_to_rid(const char *zTag, const char *zType){ |
| 112 | 112 | int vid; |
| @@ -603,11 +603,12 @@ | ||
| 603 | 603 | if( db_step(&q)==SQLITE_ROW ){ |
| 604 | 604 | const char *zType; |
| 605 | 605 | switch( db_column_text(&q,0)[0] ){ |
| 606 | 606 | case 'c': zType = "Check-in"; break; |
| 607 | 607 | case 'w': zType = "Wiki-edit"; break; |
| 608 | - case 'e': zType = "Event"; break; | |
| 608 | + case 'e': zType = "Technote"; break; | |
| 609 | + case 'f': zType = "Forum-post"; break; | |
| 609 | 610 | case 't': zType = "Ticket-change"; break; |
| 610 | 611 | case 'g': zType = "Tag-change"; break; |
| 611 | 612 | default: zType = "Unknown"; break; |
| 612 | 613 | } |
| 613 | 614 | fossil_print("type: %s by %s on %s\n", zType, db_column_text(&q,2), |
| @@ -926,10 +927,25 @@ | ||
| 926 | 927 | " WHERE (blob.rid %s)\n" |
| 927 | 928 | " AND blob.rid NOT IN (SELECT rid FROM description)\n" |
| 928 | 929 | " AND blob.uuid=attachment.src", |
| 929 | 930 | zWhere /*safe-for-%s*/ |
| 930 | 931 | ); |
| 932 | + | |
| 933 | + /* Forum posts */ | |
| 934 | + if( db_table_exists("repository","forumpost") ){ | |
| 935 | + db_multi_exec( | |
| 936 | + "INSERT OR IGNORE INTO description(rid,uuid,ctime,type,summary)\n" | |
| 937 | + "SELECT postblob.rid, postblob.uuid, forumpost.fmtime, 'forumpost',\n" | |
| 938 | + " CASE WHEN fpid=froot THEN 'forum-post '\n" | |
| 939 | + " ELSE 'forum-reply-to ' END || substr(rootblob.uuid,1,14)\n" | |
| 940 | + " FROM forumpost, blob AS postblob, blob AS rootblob\n" | |
| 941 | + " WHERE (forumpost.fpid %s)\n" | |
| 942 | + " AND postblob.rid=forumpost.fpid" | |
| 943 | + " AND rootblob.rid=forumpost.froot", | |
| 944 | + zWhere /*safe-for-%s*/ | |
| 945 | + ); | |
| 946 | + } | |
| 931 | 947 | |
| 932 | 948 | /* Everything else */ |
| 933 | 949 | db_multi_exec( |
| 934 | 950 | "INSERT OR IGNORE INTO description(rid,uuid,type,summary)\n" |
| 935 | 951 | "SELECT blob.rid, blob.uuid," |
| 936 | 952 |
| --- src/name.c | |
| +++ src/name.c | |
| @@ -94,19 +94,19 @@ | |
| 94 | ** * "next" |
| 95 | ** |
| 96 | ** Return the RID of the matching artifact. Or return 0 if the name does not |
| 97 | ** match any known object. Or return -1 if the name is ambiguous. |
| 98 | ** |
| 99 | ** The zType parameter specifies the type of artifact: ci, t, w, e, g. |
| 100 | ** If zType is NULL or "" or "*" then any type of artifact will serve. |
| 101 | ** If zType is "br" then find the first check-in of the named branch |
| 102 | ** rather than the last. |
| 103 | ** zType is "ci" in most use cases since we are usually searching for |
| 104 | ** a check-in. |
| 105 | ** |
| 106 | ** Note that the input zTag for types "t" and "e" is the artifact hash of |
| 107 | ** the ticket-change or event-change artifact, not the randomly generated |
| 108 | ** hexadecimal identifier assigned to tickets and events. Those identifiers |
| 109 | ** live in a separate namespace. |
| 110 | */ |
| 111 | int symbolic_name_to_rid(const char *zTag, const char *zType){ |
| 112 | int vid; |
| @@ -603,11 +603,12 @@ | |
| 603 | if( db_step(&q)==SQLITE_ROW ){ |
| 604 | const char *zType; |
| 605 | switch( db_column_text(&q,0)[0] ){ |
| 606 | case 'c': zType = "Check-in"; break; |
| 607 | case 'w': zType = "Wiki-edit"; break; |
| 608 | case 'e': zType = "Event"; break; |
| 609 | case 't': zType = "Ticket-change"; break; |
| 610 | case 'g': zType = "Tag-change"; break; |
| 611 | default: zType = "Unknown"; break; |
| 612 | } |
| 613 | fossil_print("type: %s by %s on %s\n", zType, db_column_text(&q,2), |
| @@ -926,10 +927,25 @@ | |
| 926 | " WHERE (blob.rid %s)\n" |
| 927 | " AND blob.rid NOT IN (SELECT rid FROM description)\n" |
| 928 | " AND blob.uuid=attachment.src", |
| 929 | zWhere /*safe-for-%s*/ |
| 930 | ); |
| 931 | |
| 932 | /* Everything else */ |
| 933 | db_multi_exec( |
| 934 | "INSERT OR IGNORE INTO description(rid,uuid,type,summary)\n" |
| 935 | "SELECT blob.rid, blob.uuid," |
| 936 |
| --- src/name.c | |
| +++ src/name.c | |
| @@ -94,19 +94,19 @@ | |
| 94 | ** * "next" |
| 95 | ** |
| 96 | ** Return the RID of the matching artifact. Or return 0 if the name does not |
| 97 | ** match any known object. Or return -1 if the name is ambiguous. |
| 98 | ** |
| 99 | ** The zType parameter specifies the type of artifact: ci, t, w, e, g, f. |
| 100 | ** If zType is NULL or "" or "*" then any type of artifact will serve. |
| 101 | ** If zType is "br" then find the first check-in of the named branch |
| 102 | ** rather than the last. |
| 103 | ** zType is "ci" in most use cases since we are usually searching for |
| 104 | ** a check-in. |
| 105 | ** |
| 106 | ** Note that the input zTag for types "t" and "e" is the artifact hash of |
| 107 | ** the ticket-change or technote-change artifact, not the randomly generated |
| 108 | ** hexadecimal identifier assigned to tickets and events. Those identifiers |
| 109 | ** live in a separate namespace. |
| 110 | */ |
| 111 | int symbolic_name_to_rid(const char *zTag, const char *zType){ |
| 112 | int vid; |
| @@ -603,11 +603,12 @@ | |
| 603 | if( db_step(&q)==SQLITE_ROW ){ |
| 604 | const char *zType; |
| 605 | switch( db_column_text(&q,0)[0] ){ |
| 606 | case 'c': zType = "Check-in"; break; |
| 607 | case 'w': zType = "Wiki-edit"; break; |
| 608 | case 'e': zType = "Technote"; break; |
| 609 | case 'f': zType = "Forum-post"; break; |
| 610 | case 't': zType = "Ticket-change"; break; |
| 611 | case 'g': zType = "Tag-change"; break; |
| 612 | default: zType = "Unknown"; break; |
| 613 | } |
| 614 | fossil_print("type: %s by %s on %s\n", zType, db_column_text(&q,2), |
| @@ -926,10 +927,25 @@ | |
| 927 | " WHERE (blob.rid %s)\n" |
| 928 | " AND blob.rid NOT IN (SELECT rid FROM description)\n" |
| 929 | " AND blob.uuid=attachment.src", |
| 930 | zWhere /*safe-for-%s*/ |
| 931 | ); |
| 932 | |
| 933 | /* Forum posts */ |
| 934 | if( db_table_exists("repository","forumpost") ){ |
| 935 | db_multi_exec( |
| 936 | "INSERT OR IGNORE INTO description(rid,uuid,ctime,type,summary)\n" |
| 937 | "SELECT postblob.rid, postblob.uuid, forumpost.fmtime, 'forumpost',\n" |
| 938 | " CASE WHEN fpid=froot THEN 'forum-post '\n" |
| 939 | " ELSE 'forum-reply-to ' END || substr(rootblob.uuid,1,14)\n" |
| 940 | " FROM forumpost, blob AS postblob, blob AS rootblob\n" |
| 941 | " WHERE (forumpost.fpid %s)\n" |
| 942 | " AND postblob.rid=forumpost.fpid" |
| 943 | " AND rootblob.rid=forumpost.froot", |
| 944 | zWhere /*safe-for-%s*/ |
| 945 | ); |
| 946 | } |
| 947 | |
| 948 | /* Everything else */ |
| 949 | db_multi_exec( |
| 950 | "INSERT OR IGNORE INTO description(rid,uuid,type,summary)\n" |
| 951 | "SELECT blob.rid, blob.uuid," |
| 952 |
+1
-1
| --- src/popen.c | ||
| +++ src/popen.c | ||
| @@ -25,11 +25,11 @@ | ||
| 25 | 25 | #include <fcntl.h> |
| 26 | 26 | /* |
| 27 | 27 | ** Print a fatal error and quit. |
| 28 | 28 | */ |
| 29 | 29 | static void win32_fatal_error(const char *zMsg){ |
| 30 | - fossil_panic("%s", zMsg); | |
| 30 | + fossil_fatal("%s", zMsg); | |
| 31 | 31 | } |
| 32 | 32 | #else |
| 33 | 33 | #include <signal.h> |
| 34 | 34 | #include <sys/wait.h> |
| 35 | 35 | #endif |
| 36 | 36 |
| --- src/popen.c | |
| +++ src/popen.c | |
| @@ -25,11 +25,11 @@ | |
| 25 | #include <fcntl.h> |
| 26 | /* |
| 27 | ** Print a fatal error and quit. |
| 28 | */ |
| 29 | static void win32_fatal_error(const char *zMsg){ |
| 30 | fossil_panic("%s", zMsg); |
| 31 | } |
| 32 | #else |
| 33 | #include <signal.h> |
| 34 | #include <sys/wait.h> |
| 35 | #endif |
| 36 |
| --- src/popen.c | |
| +++ src/popen.c | |
| @@ -25,11 +25,11 @@ | |
| 25 | #include <fcntl.h> |
| 26 | /* |
| 27 | ** Print a fatal error and quit. |
| 28 | */ |
| 29 | static void win32_fatal_error(const char *zMsg){ |
| 30 | fossil_fatal("%s", zMsg); |
| 31 | } |
| 32 | #else |
| 33 | #include <signal.h> |
| 34 | #include <sys/wait.h> |
| 35 | #endif |
| 36 |
+4
| --- src/printf.c | ||
| +++ src/printf.c | ||
| @@ -1085,12 +1085,16 @@ | ||
| 1085 | 1085 | mainInFatalError = 1; |
| 1086 | 1086 | db_force_rollback(); |
| 1087 | 1087 | va_start(ap, zFormat); |
| 1088 | 1088 | sqlite3_vsnprintf(sizeof(z),z,zFormat, ap); |
| 1089 | 1089 | va_end(ap); |
| 1090 | + if( g.fAnyTrace ){ | |
| 1091 | + fprintf(stderr, "/***** panic on %d *****/\n", getpid()); | |
| 1092 | + } | |
| 1090 | 1093 | fossil_errorlog("panic: %s", z); |
| 1091 | 1094 | rc = fossil_print_error(rc, z); |
| 1095 | + abort(); | |
| 1092 | 1096 | exit(rc); |
| 1093 | 1097 | } |
| 1094 | 1098 | NORETURN void fossil_fatal(const char *zFormat, ...){ |
| 1095 | 1099 | char *z; |
| 1096 | 1100 | int rc = 1; |
| 1097 | 1101 |
| --- src/printf.c | |
| +++ src/printf.c | |
| @@ -1085,12 +1085,16 @@ | |
| 1085 | mainInFatalError = 1; |
| 1086 | db_force_rollback(); |
| 1087 | va_start(ap, zFormat); |
| 1088 | sqlite3_vsnprintf(sizeof(z),z,zFormat, ap); |
| 1089 | va_end(ap); |
| 1090 | fossil_errorlog("panic: %s", z); |
| 1091 | rc = fossil_print_error(rc, z); |
| 1092 | exit(rc); |
| 1093 | } |
| 1094 | NORETURN void fossil_fatal(const char *zFormat, ...){ |
| 1095 | char *z; |
| 1096 | int rc = 1; |
| 1097 |
| --- src/printf.c | |
| +++ src/printf.c | |
| @@ -1085,12 +1085,16 @@ | |
| 1085 | mainInFatalError = 1; |
| 1086 | db_force_rollback(); |
| 1087 | va_start(ap, zFormat); |
| 1088 | sqlite3_vsnprintf(sizeof(z),z,zFormat, ap); |
| 1089 | va_end(ap); |
| 1090 | if( g.fAnyTrace ){ |
| 1091 | fprintf(stderr, "/***** panic on %d *****/\n", getpid()); |
| 1092 | } |
| 1093 | fossil_errorlog("panic: %s", z); |
| 1094 | rc = fossil_print_error(rc, z); |
| 1095 | abort(); |
| 1096 | exit(rc); |
| 1097 | } |
| 1098 | NORETURN void fossil_fatal(const char *zFormat, ...){ |
| 1099 | char *z; |
| 1100 | int rc = 1; |
| 1101 |
+31
-2
| --- src/schema.c | ||
| +++ src/schema.c | ||
| @@ -290,14 +290,20 @@ | ||
| 290 | 290 | @ -- and so it makes sense to precompute the set of leaves. There is |
| 291 | 291 | @ -- one entry in the following table for each leaf. |
| 292 | 292 | @ -- |
| 293 | 293 | @ CREATE TABLE leaf(rid INTEGER PRIMARY KEY); |
| 294 | 294 | @ |
| 295 | -@ -- Events used to generate a timeline | |
| 295 | +@ -- Events used to generate a timeline. Type meanings: | |
| 296 | +@ -- ci Check-ins | |
| 297 | +@ -- e Technotes | |
| 298 | +@ -- f Forum posts | |
| 299 | +@ -- g Tags | |
| 300 | +@ -- t Ticket changes | |
| 301 | +@ -- w Wiki page edit | |
| 296 | 302 | @ -- |
| 297 | 303 | @ CREATE TABLE event( |
| 298 | -@ type TEXT, -- Type of event: 'ci', 'w', 'e', 't', 'g' | |
| 304 | +@ type TEXT, -- Type of event: ci, e, f, g, t, w | |
| 299 | 305 | @ mtime DATETIME, -- Time of occurrence. Julian day. |
| 300 | 306 | @ objid INTEGER PRIMARY KEY, -- Associated record ID |
| 301 | 307 | @ tagid INTEGER, -- Associated ticket or wiki name tag |
| 302 | 308 | @ uid INTEGER REFERENCES user, -- User who caused the event |
| 303 | 309 | @ bgcolor TEXT, -- Color set by 'bgcolor' property |
| @@ -542,5 +548,28 @@ | ||
| 542 | 548 | @ |
| 543 | 549 | @ -- Identifier for this file type. |
| 544 | 550 | @ -- The integer is the same as 'FSLC'. |
| 545 | 551 | @ PRAGMA application_id=252006674; |
| 546 | 552 | ; |
| 553 | + | |
| 554 | +/* | |
| 555 | +** The following table holds information about forum posts. It | |
| 556 | +** is created on-demand whenever the manifest parser encounters | |
| 557 | +** a forum-post artifact. | |
| 558 | +*/ | |
| 559 | +static const char zForumSchema[] = | |
| 560 | +@ CREATE TABLE repository.forumpost( | |
| 561 | +@ fpid INTEGER PRIMARY KEY, -- BLOB.rid for the artifact | |
| 562 | +@ froot INT, -- fpid of the thread root | |
| 563 | +@ fprev INT, -- Previous version of this same post | |
| 564 | +@ firt INT, -- This post is in-reply-to | |
| 565 | +@ fmtime REAL -- When posted. Julian day | |
| 566 | +@ ); | |
| 567 | +@ CREATE INDEX repository.forumthread ON forumpost(froot); | |
| 568 | +; | |
| 569 | + | |
| 570 | +/* Create the forum-post schema if it does not already exist */ | |
| 571 | +void schema_forum(void){ | |
| 572 | + if( !db_table_exists("repository","forumpost") ){ | |
| 573 | + db_multi_exec("%s",zForumSchema/*safe-for-%s*/); | |
| 574 | + } | |
| 575 | +} | |
| 547 | 576 |
| --- src/schema.c | |
| +++ src/schema.c | |
| @@ -290,14 +290,20 @@ | |
| 290 | @ -- and so it makes sense to precompute the set of leaves. There is |
| 291 | @ -- one entry in the following table for each leaf. |
| 292 | @ -- |
| 293 | @ CREATE TABLE leaf(rid INTEGER PRIMARY KEY); |
| 294 | @ |
| 295 | @ -- Events used to generate a timeline |
| 296 | @ -- |
| 297 | @ CREATE TABLE event( |
| 298 | @ type TEXT, -- Type of event: 'ci', 'w', 'e', 't', 'g' |
| 299 | @ mtime DATETIME, -- Time of occurrence. Julian day. |
| 300 | @ objid INTEGER PRIMARY KEY, -- Associated record ID |
| 301 | @ tagid INTEGER, -- Associated ticket or wiki name tag |
| 302 | @ uid INTEGER REFERENCES user, -- User who caused the event |
| 303 | @ bgcolor TEXT, -- Color set by 'bgcolor' property |
| @@ -542,5 +548,28 @@ | |
| 542 | @ |
| 543 | @ -- Identifier for this file type. |
| 544 | @ -- The integer is the same as 'FSLC'. |
| 545 | @ PRAGMA application_id=252006674; |
| 546 | ; |
| 547 |
| --- src/schema.c | |
| +++ src/schema.c | |
| @@ -290,14 +290,20 @@ | |
| 290 | @ -- and so it makes sense to precompute the set of leaves. There is |
| 291 | @ -- one entry in the following table for each leaf. |
| 292 | @ -- |
| 293 | @ CREATE TABLE leaf(rid INTEGER PRIMARY KEY); |
| 294 | @ |
| 295 | @ -- Events used to generate a timeline. Type meanings: |
| 296 | @ -- ci Check-ins |
| 297 | @ -- e Technotes |
| 298 | @ -- f Forum posts |
| 299 | @ -- g Tags |
| 300 | @ -- t Ticket changes |
| 301 | @ -- w Wiki page edit |
| 302 | @ -- |
| 303 | @ CREATE TABLE event( |
| 304 | @ type TEXT, -- Type of event: ci, e, f, g, t, w |
| 305 | @ mtime DATETIME, -- Time of occurrence. Julian day. |
| 306 | @ objid INTEGER PRIMARY KEY, -- Associated record ID |
| 307 | @ tagid INTEGER, -- Associated ticket or wiki name tag |
| 308 | @ uid INTEGER REFERENCES user, -- User who caused the event |
| 309 | @ bgcolor TEXT, -- Color set by 'bgcolor' property |
| @@ -542,5 +548,28 @@ | |
| 548 | @ |
| 549 | @ -- Identifier for this file type. |
| 550 | @ -- The integer is the same as 'FSLC'. |
| 551 | @ PRAGMA application_id=252006674; |
| 552 | ; |
| 553 | |
| 554 | /* |
| 555 | ** The following table holds information about forum posts. It |
| 556 | ** is created on-demand whenever the manifest parser encounters |
| 557 | ** a forum-post artifact. |
| 558 | */ |
| 559 | static const char zForumSchema[] = |
| 560 | @ CREATE TABLE repository.forumpost( |
| 561 | @ fpid INTEGER PRIMARY KEY, -- BLOB.rid for the artifact |
| 562 | @ froot INT, -- fpid of the thread root |
| 563 | @ fprev INT, -- Previous version of this same post |
| 564 | @ firt INT, -- This post is in-reply-to |
| 565 | @ fmtime REAL -- When posted. Julian day |
| 566 | @ ); |
| 567 | @ CREATE INDEX repository.forumthread ON forumpost(froot); |
| 568 | ; |
| 569 | |
| 570 | /* Create the forum-post schema if it does not already exist */ |
| 571 | void schema_forum(void){ |
| 572 | if( !db_table_exists("repository","forumpost") ){ |
| 573 | db_multi_exec("%s",zForumSchema/*safe-for-%s*/); |
| 574 | } |
| 575 | } |
| 576 |
+77
-8
| --- src/search.c | ||
| +++ src/search.c | ||
| @@ -638,11 +638,12 @@ | ||
| 638 | 638 | #define SRCH_CKIN 0x0001 /* Search over check-in comments */ |
| 639 | 639 | #define SRCH_DOC 0x0002 /* Search over embedded documents */ |
| 640 | 640 | #define SRCH_TKT 0x0004 /* Search over tickets */ |
| 641 | 641 | #define SRCH_WIKI 0x0008 /* Search over wiki */ |
| 642 | 642 | #define SRCH_TECHNOTE 0x0010 /* Search over tech notes */ |
| 643 | -#define SRCH_ALL 0x001f /* Search over everything */ | |
| 643 | +#define SRCH_FORUM 0x0020 /* Search over forum messages */ | |
| 644 | +#define SRCH_ALL 0x003f /* Search over everything */ | |
| 644 | 645 | #endif |
| 645 | 646 | |
| 646 | 647 | /* |
| 647 | 648 | ** Remove bits from srchFlags which are disallowed by either the |
| 648 | 649 | ** current server configuration or by user permissions. |
| @@ -654,15 +655,17 @@ | ||
| 654 | 655 | { SRCH_CKIN, "search-ci" }, |
| 655 | 656 | { SRCH_DOC, "search-doc" }, |
| 656 | 657 | { SRCH_TKT, "search-tkt" }, |
| 657 | 658 | { SRCH_WIKI, "search-wiki" }, |
| 658 | 659 | { SRCH_TECHNOTE, "search-technote" }, |
| 660 | + { SRCH_FORUM, "search-forum" }, | |
| 659 | 661 | }; |
| 660 | 662 | int i; |
| 661 | 663 | if( g.perm.Read==0 ) srchFlags &= ~(SRCH_CKIN|SRCH_DOC|SRCH_TECHNOTE); |
| 662 | 664 | if( g.perm.RdTkt==0 ) srchFlags &= ~(SRCH_TKT); |
| 663 | 665 | if( g.perm.RdWiki==0 ) srchFlags &= ~(SRCH_WIKI); |
| 666 | + if( g.perm.RdForum==0) srchFlags &= ~(SRCH_FORUM); | |
| 664 | 667 | for(i=0; i<count(aSetng); i++){ |
| 665 | 668 | unsigned int m = aSetng[i].m; |
| 666 | 669 | if( (srchFlags & m)==0 ) continue; |
| 667 | 670 | if( ((knownGood|knownBad) & m)!=0 ) continue; |
| 668 | 671 | if( db_get_boolean(aSetng[i].zKey,0) ){ |
| @@ -793,10 +796,23 @@ | ||
| 793 | 796 | " search_snippet()" |
| 794 | 797 | " FROM technote" |
| 795 | 798 | " WHERE search_match('',body('e',rid,NULL));" |
| 796 | 799 | ); |
| 797 | 800 | } |
| 801 | + if( (srchFlags & SRCH_FORUM)!=0 ){ | |
| 802 | + db_multi_exec( | |
| 803 | + "INSERT INTO x(label,url,score,id,date,snip)" | |
| 804 | + " SELECT 'Forum '||comment," | |
| 805 | + " '/forumpost/'||uuid," | |
| 806 | + " search_score()," | |
| 807 | + " 'f'||rid," | |
| 808 | + " datetime(event.mtime)," | |
| 809 | + " search_snippet()" | |
| 810 | + " FROM event JOIN blob on event.objid=blob.rid" | |
| 811 | + " WHERE search_match('',body('f',rid,NULL));" | |
| 812 | + ); | |
| 813 | + } | |
| 798 | 814 | } |
| 799 | 815 | |
| 800 | 816 | /* |
| 801 | 817 | ** Number of significant bits in a u32 |
| 802 | 818 | */ |
| @@ -913,10 +929,11 @@ | ||
| 913 | 929 | { SRCH_CKIN, 'c' }, |
| 914 | 930 | { SRCH_DOC, 'd' }, |
| 915 | 931 | { SRCH_TKT, 't' }, |
| 916 | 932 | { SRCH_WIKI, 'w' }, |
| 917 | 933 | { SRCH_TECHNOTE, 'e' }, |
| 934 | + { SRCH_FORUM, 'f' }, | |
| 918 | 935 | }; |
| 919 | 936 | int i; |
| 920 | 937 | for(i=0; i<count(aMask); i++){ |
| 921 | 938 | if( srchFlags & aMask[i].m ){ |
| 922 | 939 | blob_appendf(&sql, "%sftsdocs.type='%c'", zSep, aMask[i].c); |
| @@ -1049,29 +1066,38 @@ | ||
| 1049 | 1066 | ** |
| 1050 | 1067 | ** This routine automatically restricts srchFlag according to user |
| 1051 | 1068 | ** permissions and the server configuration. The entry box is shown |
| 1052 | 1069 | ** disabled if srchFlags is 0 after these restrictions are applied. |
| 1053 | 1070 | ** |
| 1054 | -** If useYparam is true, then this routine also looks at the y= query | |
| 1055 | -** parameter for further search restrictions. | |
| 1071 | +** The mFlags value controls options: | |
| 1072 | +** | |
| 1073 | +** 0x01 If the y= query parameter is present, use it as an addition | |
| 1074 | +** restriction what to search. | |
| 1075 | +** | |
| 1076 | +** 0x02 Show nothing if search is disabled. | |
| 1077 | +** | |
| 1078 | +** Return true if there are search results. | |
| 1056 | 1079 | */ |
| 1057 | -void search_screen(unsigned srchFlags, int useYparam){ | |
| 1080 | +int search_screen(unsigned srchFlags, int mFlags){ | |
| 1058 | 1081 | const char *zType = 0; |
| 1059 | 1082 | const char *zClass = 0; |
| 1060 | 1083 | const char *zDisable1; |
| 1061 | 1084 | const char *zDisable2; |
| 1062 | 1085 | const char *zPattern; |
| 1063 | 1086 | int fDebug = PB("debug"); |
| 1087 | + int haveResult = 0; | |
| 1064 | 1088 | srchFlags = search_restrict(srchFlags); |
| 1065 | 1089 | switch( srchFlags ){ |
| 1066 | 1090 | case SRCH_CKIN: zType = " Check-ins"; zClass = "Ckin"; break; |
| 1067 | 1091 | case SRCH_DOC: zType = " Docs"; zClass = "Doc"; break; |
| 1068 | 1092 | case SRCH_TKT: zType = " Tickets"; zClass = "Tkt"; break; |
| 1069 | 1093 | case SRCH_WIKI: zType = " Wiki"; zClass = "Wiki"; break; |
| 1070 | 1094 | case SRCH_TECHNOTE: zType = " Tech Notes"; zClass = "Note"; break; |
| 1095 | + case SRCH_FORUM: zType = " Forum"; zClass = "Frm"; break; | |
| 1071 | 1096 | } |
| 1072 | 1097 | if( srchFlags==0 ){ |
| 1098 | + if( mFlags & 0x02 ) return 0; | |
| 1073 | 1099 | zDisable1 = " disabled"; |
| 1074 | 1100 | zDisable2 = " disabled"; |
| 1075 | 1101 | zPattern = ""; |
| 1076 | 1102 | }else{ |
| 1077 | 1103 | zDisable1 = ""; /* Was: " autofocus" */ |
| @@ -1083,18 +1109,19 @@ | ||
| 1083 | 1109 | @ <div class='searchForm searchForm%s(zClass)'> |
| 1084 | 1110 | }else{ |
| 1085 | 1111 | @ <div class='searchForm'> |
| 1086 | 1112 | } |
| 1087 | 1113 | @ <input type="text" name="s" size="40" value="%h(zPattern)"%s(zDisable1)> |
| 1088 | - if( useYparam && (srchFlags & (srchFlags-1))!=0 && useYparam ){ | |
| 1114 | + if( (mFlags & 0x01)!=0 && (srchFlags & (srchFlags-1))!=0 ){ | |
| 1089 | 1115 | static const struct { char *z; char *zNm; unsigned m; } aY[] = { |
| 1090 | 1116 | { "all", "All", SRCH_ALL }, |
| 1091 | 1117 | { "c", "Check-ins", SRCH_CKIN }, |
| 1092 | 1118 | { "d", "Docs", SRCH_DOC }, |
| 1093 | 1119 | { "t", "Tickets", SRCH_TKT }, |
| 1094 | 1120 | { "w", "Wiki", SRCH_WIKI }, |
| 1095 | 1121 | { "e", "Tech Notes", SRCH_TECHNOTE }, |
| 1122 | + { "f", "Forum", SRCH_FORUM }, | |
| 1096 | 1123 | }; |
| 1097 | 1124 | const char *zY = PD("y","all"); |
| 1098 | 1125 | unsigned newFlags = srchFlags; |
| 1099 | 1126 | int i; |
| 1100 | 1127 | @ <select size='1' name='y'> |
| @@ -1127,11 +1154,13 @@ | ||
| 1127 | 1154 | } |
| 1128 | 1155 | if( search_run_and_output(zPattern, srchFlags, fDebug)==0 ){ |
| 1129 | 1156 | @ <p class='searchEmpty'>No matches for: <span>%h(zPattern)</span></p> |
| 1130 | 1157 | } |
| 1131 | 1158 | @ </div> |
| 1159 | + haveResult = 1; | |
| 1132 | 1160 | } |
| 1161 | + return haveResult; | |
| 1133 | 1162 | } |
| 1134 | 1163 | |
| 1135 | 1164 | /* |
| 1136 | 1165 | ** WEBPAGE: search |
| 1137 | 1166 | ** |
| @@ -1143,10 +1172,11 @@ | ||
| 1143 | 1172 | ** c -> check-ins |
| 1144 | 1173 | ** d -> documentation |
| 1145 | 1174 | ** t -> tickets |
| 1146 | 1175 | ** w -> wiki |
| 1147 | 1176 | ** e -> tech notes |
| 1177 | +** f -> forum | |
| 1148 | 1178 | ** all -> everything |
| 1149 | 1179 | */ |
| 1150 | 1180 | void search_page(void){ |
| 1151 | 1181 | login_check_credentials(); |
| 1152 | 1182 | style_header("Search"); |
| @@ -1250,10 +1280,11 @@ | ||
| 1250 | 1280 | ** cType: d Embedded documentation |
| 1251 | 1281 | ** w Wiki page |
| 1252 | 1282 | ** c Check-in comment |
| 1253 | 1283 | ** t Ticket text |
| 1254 | 1284 | ** e Tech note |
| 1285 | +** f Forum | |
| 1255 | 1286 | ** |
| 1256 | 1287 | ** rid The RID of an artifact that defines the object |
| 1257 | 1288 | ** being searched. |
| 1258 | 1289 | ** |
| 1259 | 1290 | ** zName Name of the object being searched. This is used |
| @@ -1275,17 +1306,27 @@ | ||
| 1275 | 1306 | blob_to_utf8_no_bom(&doc, 0); |
| 1276 | 1307 | get_stext_by_mimetype(&doc, mimetype_from_name(zName), pOut); |
| 1277 | 1308 | blob_reset(&doc); |
| 1278 | 1309 | break; |
| 1279 | 1310 | } |
| 1311 | + case 'f': /* Forum messages */ | |
| 1280 | 1312 | case 'e': /* Tech Notes */ |
| 1281 | 1313 | case 'w': { /* Wiki */ |
| 1282 | 1314 | Manifest *pWiki = manifest_get(rid, |
| 1283 | - cType == 'e' ? CFTYPE_EVENT : CFTYPE_WIKI, 0); | |
| 1315 | + cType == 'e' ? CFTYPE_EVENT : | |
| 1316 | + cType == 'f' ? CFTYPE_FORUM : CFTYPE_WIKI, 0); | |
| 1284 | 1317 | Blob wiki; |
| 1285 | 1318 | if( pWiki==0 ) break; |
| 1286 | - blob_init(&wiki, pWiki->zWiki, -1); | |
| 1319 | + if( cType=='f' ){ | |
| 1320 | + blob_init(&wiki, 0, 0); | |
| 1321 | + if( pWiki->zThreadTitle ){ | |
| 1322 | + blob_appendf(&wiki, "<h1>%h</h1>\n", pWiki->zThreadTitle); | |
| 1323 | + } | |
| 1324 | + blob_appendf(&wiki, "From %s:\n\n%s", pWiki->zUser, pWiki->zWiki); | |
| 1325 | + }else{ | |
| 1326 | + blob_init(&wiki, pWiki->zWiki, -1); | |
| 1327 | + } | |
| 1287 | 1328 | get_stext_by_mimetype(&wiki, wiki_filter_mimetypes(pWiki->zMimetype), |
| 1288 | 1329 | pOut); |
| 1289 | 1330 | blob_reset(&wiki); |
| 1290 | 1331 | manifest_destroy(pWiki); |
| 1291 | 1332 | break; |
| @@ -1516,11 +1557,11 @@ | ||
| 1516 | 1557 | "INSERT OR IGNORE INTO ftsdocs(type,rid,idxed)" |
| 1517 | 1558 | " SELECT 't', tkt_id, 0 FROM ticket;" |
| 1518 | 1559 | ); |
| 1519 | 1560 | db_multi_exec( |
| 1520 | 1561 | "INSERT OR IGNORE INTO ftsdocs(type,rid,name,idxed)" |
| 1521 | - " SELECT 'e', objid, comment, 0 FROM event WHERE type='e';" | |
| 1562 | + " SELECT type, objid, comment, 0 FROM event WHERE type IN ('e','f');" | |
| 1522 | 1563 | ); |
| 1523 | 1564 | } |
| 1524 | 1565 | |
| 1525 | 1566 | /* |
| 1526 | 1567 | ** The document described by cType,rid,zName is about to be added or |
| @@ -1553,10 +1594,11 @@ | ||
| 1553 | 1594 | db_multi_exec( |
| 1554 | 1595 | "DELETE FROM ftsdocs WHERE type='%c' AND name=%Q AND rid!=%d", |
| 1555 | 1596 | cType, zName, rid |
| 1556 | 1597 | ); |
| 1557 | 1598 | } |
| 1599 | + /* All forum posts are always indexed */ | |
| 1558 | 1600 | } |
| 1559 | 1601 | } |
| 1560 | 1602 | |
| 1561 | 1603 | /* |
| 1562 | 1604 | ** If the doc-glob and doc-br settings are valid for document search |
| @@ -1681,10 +1723,33 @@ | ||
| 1681 | 1723 | " tagxref.mtime" |
| 1682 | 1724 | " FROM tagxref WHERE tagxref.rid=ftsdocs.rid)" |
| 1683 | 1725 | " WHERE ftsdocs.type='w' AND NOT ftsdocs.idxed" |
| 1684 | 1726 | ); |
| 1685 | 1727 | } |
| 1728 | + | |
| 1729 | +/* | |
| 1730 | +** Deal with all of the unindexed 'f' terms in FTSDOCS | |
| 1731 | +*/ | |
| 1732 | +static void search_update_forum_index(void){ | |
| 1733 | + db_multi_exec( | |
| 1734 | + "INSERT INTO ftsidx(docid,title,body)" | |
| 1735 | + " SELECT rowid, title('f',rid,NULL),body('f',rid,NULL) FROM ftsdocs" | |
| 1736 | + " WHERE type='f' AND NOT idxed;" | |
| 1737 | + ); | |
| 1738 | + if( db_changes()==0 ) return; | |
| 1739 | + db_multi_exec( | |
| 1740 | + "UPDATE ftsdocs SET idxed=1, name=NULL," | |
| 1741 | + " (label,url,mtime) = " | |
| 1742 | + " (SELECT 'Forum '||event.comment," | |
| 1743 | + " '/forumpost/'||blob.uuid," | |
| 1744 | + " event.mtime" | |
| 1745 | + " FROM event, blob" | |
| 1746 | + " WHERE event.objid=ftsdocs.rid" | |
| 1747 | + " AND blob.rid=ftsdocs.rid)" | |
| 1748 | + "WHERE ftsdocs.type='f' AND NOT ftsdocs.idxed" | |
| 1749 | + ); | |
| 1750 | +} | |
| 1686 | 1751 | |
| 1687 | 1752 | /* |
| 1688 | 1753 | ** Deal with all of the unindexed 'e' terms in FTSDOCS |
| 1689 | 1754 | */ |
| 1690 | 1755 | static void search_update_technote_index(void){ |
| @@ -1728,10 +1793,13 @@ | ||
| 1728 | 1793 | search_update_wiki_index(); |
| 1729 | 1794 | } |
| 1730 | 1795 | if( srchFlags & SRCH_TECHNOTE ){ |
| 1731 | 1796 | search_update_technote_index(); |
| 1732 | 1797 | } |
| 1798 | + if( srchFlags & SRCH_FORUM ){ | |
| 1799 | + search_update_forum_index(); | |
| 1800 | + } | |
| 1733 | 1801 | } |
| 1734 | 1802 | |
| 1735 | 1803 | /* |
| 1736 | 1804 | ** Construct, prepopulate, and then update the full-text index. |
| 1737 | 1805 | */ |
| @@ -1780,10 +1848,11 @@ | ||
| 1780 | 1848 | { "search-ci", "check-in search:", "c" }, |
| 1781 | 1849 | { "search-doc", "document search:", "d" }, |
| 1782 | 1850 | { "search-tkt", "ticket search:", "t" }, |
| 1783 | 1851 | { "search-wiki", "wiki search:", "w" }, |
| 1784 | 1852 | { "search-technote", "tech note search:", "e" }, |
| 1853 | + { "search-forum", "forum search:", "f" }, | |
| 1785 | 1854 | }; |
| 1786 | 1855 | char *zSubCmd = 0; |
| 1787 | 1856 | int i, j, n; |
| 1788 | 1857 | int iCmd = 0; |
| 1789 | 1858 | int iAction = 0; |
| 1790 | 1859 |
| --- src/search.c | |
| +++ src/search.c | |
| @@ -638,11 +638,12 @@ | |
| 638 | #define SRCH_CKIN 0x0001 /* Search over check-in comments */ |
| 639 | #define SRCH_DOC 0x0002 /* Search over embedded documents */ |
| 640 | #define SRCH_TKT 0x0004 /* Search over tickets */ |
| 641 | #define SRCH_WIKI 0x0008 /* Search over wiki */ |
| 642 | #define SRCH_TECHNOTE 0x0010 /* Search over tech notes */ |
| 643 | #define SRCH_ALL 0x001f /* Search over everything */ |
| 644 | #endif |
| 645 | |
| 646 | /* |
| 647 | ** Remove bits from srchFlags which are disallowed by either the |
| 648 | ** current server configuration or by user permissions. |
| @@ -654,15 +655,17 @@ | |
| 654 | { SRCH_CKIN, "search-ci" }, |
| 655 | { SRCH_DOC, "search-doc" }, |
| 656 | { SRCH_TKT, "search-tkt" }, |
| 657 | { SRCH_WIKI, "search-wiki" }, |
| 658 | { SRCH_TECHNOTE, "search-technote" }, |
| 659 | }; |
| 660 | int i; |
| 661 | if( g.perm.Read==0 ) srchFlags &= ~(SRCH_CKIN|SRCH_DOC|SRCH_TECHNOTE); |
| 662 | if( g.perm.RdTkt==0 ) srchFlags &= ~(SRCH_TKT); |
| 663 | if( g.perm.RdWiki==0 ) srchFlags &= ~(SRCH_WIKI); |
| 664 | for(i=0; i<count(aSetng); i++){ |
| 665 | unsigned int m = aSetng[i].m; |
| 666 | if( (srchFlags & m)==0 ) continue; |
| 667 | if( ((knownGood|knownBad) & m)!=0 ) continue; |
| 668 | if( db_get_boolean(aSetng[i].zKey,0) ){ |
| @@ -793,10 +796,23 @@ | |
| 793 | " search_snippet()" |
| 794 | " FROM technote" |
| 795 | " WHERE search_match('',body('e',rid,NULL));" |
| 796 | ); |
| 797 | } |
| 798 | } |
| 799 | |
| 800 | /* |
| 801 | ** Number of significant bits in a u32 |
| 802 | */ |
| @@ -913,10 +929,11 @@ | |
| 913 | { SRCH_CKIN, 'c' }, |
| 914 | { SRCH_DOC, 'd' }, |
| 915 | { SRCH_TKT, 't' }, |
| 916 | { SRCH_WIKI, 'w' }, |
| 917 | { SRCH_TECHNOTE, 'e' }, |
| 918 | }; |
| 919 | int i; |
| 920 | for(i=0; i<count(aMask); i++){ |
| 921 | if( srchFlags & aMask[i].m ){ |
| 922 | blob_appendf(&sql, "%sftsdocs.type='%c'", zSep, aMask[i].c); |
| @@ -1049,29 +1066,38 @@ | |
| 1049 | ** |
| 1050 | ** This routine automatically restricts srchFlag according to user |
| 1051 | ** permissions and the server configuration. The entry box is shown |
| 1052 | ** disabled if srchFlags is 0 after these restrictions are applied. |
| 1053 | ** |
| 1054 | ** If useYparam is true, then this routine also looks at the y= query |
| 1055 | ** parameter for further search restrictions. |
| 1056 | */ |
| 1057 | void search_screen(unsigned srchFlags, int useYparam){ |
| 1058 | const char *zType = 0; |
| 1059 | const char *zClass = 0; |
| 1060 | const char *zDisable1; |
| 1061 | const char *zDisable2; |
| 1062 | const char *zPattern; |
| 1063 | int fDebug = PB("debug"); |
| 1064 | srchFlags = search_restrict(srchFlags); |
| 1065 | switch( srchFlags ){ |
| 1066 | case SRCH_CKIN: zType = " Check-ins"; zClass = "Ckin"; break; |
| 1067 | case SRCH_DOC: zType = " Docs"; zClass = "Doc"; break; |
| 1068 | case SRCH_TKT: zType = " Tickets"; zClass = "Tkt"; break; |
| 1069 | case SRCH_WIKI: zType = " Wiki"; zClass = "Wiki"; break; |
| 1070 | case SRCH_TECHNOTE: zType = " Tech Notes"; zClass = "Note"; break; |
| 1071 | } |
| 1072 | if( srchFlags==0 ){ |
| 1073 | zDisable1 = " disabled"; |
| 1074 | zDisable2 = " disabled"; |
| 1075 | zPattern = ""; |
| 1076 | }else{ |
| 1077 | zDisable1 = ""; /* Was: " autofocus" */ |
| @@ -1083,18 +1109,19 @@ | |
| 1083 | @ <div class='searchForm searchForm%s(zClass)'> |
| 1084 | }else{ |
| 1085 | @ <div class='searchForm'> |
| 1086 | } |
| 1087 | @ <input type="text" name="s" size="40" value="%h(zPattern)"%s(zDisable1)> |
| 1088 | if( useYparam && (srchFlags & (srchFlags-1))!=0 && useYparam ){ |
| 1089 | static const struct { char *z; char *zNm; unsigned m; } aY[] = { |
| 1090 | { "all", "All", SRCH_ALL }, |
| 1091 | { "c", "Check-ins", SRCH_CKIN }, |
| 1092 | { "d", "Docs", SRCH_DOC }, |
| 1093 | { "t", "Tickets", SRCH_TKT }, |
| 1094 | { "w", "Wiki", SRCH_WIKI }, |
| 1095 | { "e", "Tech Notes", SRCH_TECHNOTE }, |
| 1096 | }; |
| 1097 | const char *zY = PD("y","all"); |
| 1098 | unsigned newFlags = srchFlags; |
| 1099 | int i; |
| 1100 | @ <select size='1' name='y'> |
| @@ -1127,11 +1154,13 @@ | |
| 1127 | } |
| 1128 | if( search_run_and_output(zPattern, srchFlags, fDebug)==0 ){ |
| 1129 | @ <p class='searchEmpty'>No matches for: <span>%h(zPattern)</span></p> |
| 1130 | } |
| 1131 | @ </div> |
| 1132 | } |
| 1133 | } |
| 1134 | |
| 1135 | /* |
| 1136 | ** WEBPAGE: search |
| 1137 | ** |
| @@ -1143,10 +1172,11 @@ | |
| 1143 | ** c -> check-ins |
| 1144 | ** d -> documentation |
| 1145 | ** t -> tickets |
| 1146 | ** w -> wiki |
| 1147 | ** e -> tech notes |
| 1148 | ** all -> everything |
| 1149 | */ |
| 1150 | void search_page(void){ |
| 1151 | login_check_credentials(); |
| 1152 | style_header("Search"); |
| @@ -1250,10 +1280,11 @@ | |
| 1250 | ** cType: d Embedded documentation |
| 1251 | ** w Wiki page |
| 1252 | ** c Check-in comment |
| 1253 | ** t Ticket text |
| 1254 | ** e Tech note |
| 1255 | ** |
| 1256 | ** rid The RID of an artifact that defines the object |
| 1257 | ** being searched. |
| 1258 | ** |
| 1259 | ** zName Name of the object being searched. This is used |
| @@ -1275,17 +1306,27 @@ | |
| 1275 | blob_to_utf8_no_bom(&doc, 0); |
| 1276 | get_stext_by_mimetype(&doc, mimetype_from_name(zName), pOut); |
| 1277 | blob_reset(&doc); |
| 1278 | break; |
| 1279 | } |
| 1280 | case 'e': /* Tech Notes */ |
| 1281 | case 'w': { /* Wiki */ |
| 1282 | Manifest *pWiki = manifest_get(rid, |
| 1283 | cType == 'e' ? CFTYPE_EVENT : CFTYPE_WIKI, 0); |
| 1284 | Blob wiki; |
| 1285 | if( pWiki==0 ) break; |
| 1286 | blob_init(&wiki, pWiki->zWiki, -1); |
| 1287 | get_stext_by_mimetype(&wiki, wiki_filter_mimetypes(pWiki->zMimetype), |
| 1288 | pOut); |
| 1289 | blob_reset(&wiki); |
| 1290 | manifest_destroy(pWiki); |
| 1291 | break; |
| @@ -1516,11 +1557,11 @@ | |
| 1516 | "INSERT OR IGNORE INTO ftsdocs(type,rid,idxed)" |
| 1517 | " SELECT 't', tkt_id, 0 FROM ticket;" |
| 1518 | ); |
| 1519 | db_multi_exec( |
| 1520 | "INSERT OR IGNORE INTO ftsdocs(type,rid,name,idxed)" |
| 1521 | " SELECT 'e', objid, comment, 0 FROM event WHERE type='e';" |
| 1522 | ); |
| 1523 | } |
| 1524 | |
| 1525 | /* |
| 1526 | ** The document described by cType,rid,zName is about to be added or |
| @@ -1553,10 +1594,11 @@ | |
| 1553 | db_multi_exec( |
| 1554 | "DELETE FROM ftsdocs WHERE type='%c' AND name=%Q AND rid!=%d", |
| 1555 | cType, zName, rid |
| 1556 | ); |
| 1557 | } |
| 1558 | } |
| 1559 | } |
| 1560 | |
| 1561 | /* |
| 1562 | ** If the doc-glob and doc-br settings are valid for document search |
| @@ -1681,10 +1723,33 @@ | |
| 1681 | " tagxref.mtime" |
| 1682 | " FROM tagxref WHERE tagxref.rid=ftsdocs.rid)" |
| 1683 | " WHERE ftsdocs.type='w' AND NOT ftsdocs.idxed" |
| 1684 | ); |
| 1685 | } |
| 1686 | |
| 1687 | /* |
| 1688 | ** Deal with all of the unindexed 'e' terms in FTSDOCS |
| 1689 | */ |
| 1690 | static void search_update_technote_index(void){ |
| @@ -1728,10 +1793,13 @@ | |
| 1728 | search_update_wiki_index(); |
| 1729 | } |
| 1730 | if( srchFlags & SRCH_TECHNOTE ){ |
| 1731 | search_update_technote_index(); |
| 1732 | } |
| 1733 | } |
| 1734 | |
| 1735 | /* |
| 1736 | ** Construct, prepopulate, and then update the full-text index. |
| 1737 | */ |
| @@ -1780,10 +1848,11 @@ | |
| 1780 | { "search-ci", "check-in search:", "c" }, |
| 1781 | { "search-doc", "document search:", "d" }, |
| 1782 | { "search-tkt", "ticket search:", "t" }, |
| 1783 | { "search-wiki", "wiki search:", "w" }, |
| 1784 | { "search-technote", "tech note search:", "e" }, |
| 1785 | }; |
| 1786 | char *zSubCmd = 0; |
| 1787 | int i, j, n; |
| 1788 | int iCmd = 0; |
| 1789 | int iAction = 0; |
| 1790 |
| --- src/search.c | |
| +++ src/search.c | |
| @@ -638,11 +638,12 @@ | |
| 638 | #define SRCH_CKIN 0x0001 /* Search over check-in comments */ |
| 639 | #define SRCH_DOC 0x0002 /* Search over embedded documents */ |
| 640 | #define SRCH_TKT 0x0004 /* Search over tickets */ |
| 641 | #define SRCH_WIKI 0x0008 /* Search over wiki */ |
| 642 | #define SRCH_TECHNOTE 0x0010 /* Search over tech notes */ |
| 643 | #define SRCH_FORUM 0x0020 /* Search over forum messages */ |
| 644 | #define SRCH_ALL 0x003f /* Search over everything */ |
| 645 | #endif |
| 646 | |
| 647 | /* |
| 648 | ** Remove bits from srchFlags which are disallowed by either the |
| 649 | ** current server configuration or by user permissions. |
| @@ -654,15 +655,17 @@ | |
| 655 | { SRCH_CKIN, "search-ci" }, |
| 656 | { SRCH_DOC, "search-doc" }, |
| 657 | { SRCH_TKT, "search-tkt" }, |
| 658 | { SRCH_WIKI, "search-wiki" }, |
| 659 | { SRCH_TECHNOTE, "search-technote" }, |
| 660 | { SRCH_FORUM, "search-forum" }, |
| 661 | }; |
| 662 | int i; |
| 663 | if( g.perm.Read==0 ) srchFlags &= ~(SRCH_CKIN|SRCH_DOC|SRCH_TECHNOTE); |
| 664 | if( g.perm.RdTkt==0 ) srchFlags &= ~(SRCH_TKT); |
| 665 | if( g.perm.RdWiki==0 ) srchFlags &= ~(SRCH_WIKI); |
| 666 | if( g.perm.RdForum==0) srchFlags &= ~(SRCH_FORUM); |
| 667 | for(i=0; i<count(aSetng); i++){ |
| 668 | unsigned int m = aSetng[i].m; |
| 669 | if( (srchFlags & m)==0 ) continue; |
| 670 | if( ((knownGood|knownBad) & m)!=0 ) continue; |
| 671 | if( db_get_boolean(aSetng[i].zKey,0) ){ |
| @@ -793,10 +796,23 @@ | |
| 796 | " search_snippet()" |
| 797 | " FROM technote" |
| 798 | " WHERE search_match('',body('e',rid,NULL));" |
| 799 | ); |
| 800 | } |
| 801 | if( (srchFlags & SRCH_FORUM)!=0 ){ |
| 802 | db_multi_exec( |
| 803 | "INSERT INTO x(label,url,score,id,date,snip)" |
| 804 | " SELECT 'Forum '||comment," |
| 805 | " '/forumpost/'||uuid," |
| 806 | " search_score()," |
| 807 | " 'f'||rid," |
| 808 | " datetime(event.mtime)," |
| 809 | " search_snippet()" |
| 810 | " FROM event JOIN blob on event.objid=blob.rid" |
| 811 | " WHERE search_match('',body('f',rid,NULL));" |
| 812 | ); |
| 813 | } |
| 814 | } |
| 815 | |
| 816 | /* |
| 817 | ** Number of significant bits in a u32 |
| 818 | */ |
| @@ -913,10 +929,11 @@ | |
| 929 | { SRCH_CKIN, 'c' }, |
| 930 | { SRCH_DOC, 'd' }, |
| 931 | { SRCH_TKT, 't' }, |
| 932 | { SRCH_WIKI, 'w' }, |
| 933 | { SRCH_TECHNOTE, 'e' }, |
| 934 | { SRCH_FORUM, 'f' }, |
| 935 | }; |
| 936 | int i; |
| 937 | for(i=0; i<count(aMask); i++){ |
| 938 | if( srchFlags & aMask[i].m ){ |
| 939 | blob_appendf(&sql, "%sftsdocs.type='%c'", zSep, aMask[i].c); |
| @@ -1049,29 +1066,38 @@ | |
| 1066 | ** |
| 1067 | ** This routine automatically restricts srchFlag according to user |
| 1068 | ** permissions and the server configuration. The entry box is shown |
| 1069 | ** disabled if srchFlags is 0 after these restrictions are applied. |
| 1070 | ** |
| 1071 | ** The mFlags value controls options: |
| 1072 | ** |
| 1073 | ** 0x01 If the y= query parameter is present, use it as an addition |
| 1074 | ** restriction what to search. |
| 1075 | ** |
| 1076 | ** 0x02 Show nothing if search is disabled. |
| 1077 | ** |
| 1078 | ** Return true if there are search results. |
| 1079 | */ |
| 1080 | int search_screen(unsigned srchFlags, int mFlags){ |
| 1081 | const char *zType = 0; |
| 1082 | const char *zClass = 0; |
| 1083 | const char *zDisable1; |
| 1084 | const char *zDisable2; |
| 1085 | const char *zPattern; |
| 1086 | int fDebug = PB("debug"); |
| 1087 | int haveResult = 0; |
| 1088 | srchFlags = search_restrict(srchFlags); |
| 1089 | switch( srchFlags ){ |
| 1090 | case SRCH_CKIN: zType = " Check-ins"; zClass = "Ckin"; break; |
| 1091 | case SRCH_DOC: zType = " Docs"; zClass = "Doc"; break; |
| 1092 | case SRCH_TKT: zType = " Tickets"; zClass = "Tkt"; break; |
| 1093 | case SRCH_WIKI: zType = " Wiki"; zClass = "Wiki"; break; |
| 1094 | case SRCH_TECHNOTE: zType = " Tech Notes"; zClass = "Note"; break; |
| 1095 | case SRCH_FORUM: zType = " Forum"; zClass = "Frm"; break; |
| 1096 | } |
| 1097 | if( srchFlags==0 ){ |
| 1098 | if( mFlags & 0x02 ) return 0; |
| 1099 | zDisable1 = " disabled"; |
| 1100 | zDisable2 = " disabled"; |
| 1101 | zPattern = ""; |
| 1102 | }else{ |
| 1103 | zDisable1 = ""; /* Was: " autofocus" */ |
| @@ -1083,18 +1109,19 @@ | |
| 1109 | @ <div class='searchForm searchForm%s(zClass)'> |
| 1110 | }else{ |
| 1111 | @ <div class='searchForm'> |
| 1112 | } |
| 1113 | @ <input type="text" name="s" size="40" value="%h(zPattern)"%s(zDisable1)> |
| 1114 | if( (mFlags & 0x01)!=0 && (srchFlags & (srchFlags-1))!=0 ){ |
| 1115 | static const struct { char *z; char *zNm; unsigned m; } aY[] = { |
| 1116 | { "all", "All", SRCH_ALL }, |
| 1117 | { "c", "Check-ins", SRCH_CKIN }, |
| 1118 | { "d", "Docs", SRCH_DOC }, |
| 1119 | { "t", "Tickets", SRCH_TKT }, |
| 1120 | { "w", "Wiki", SRCH_WIKI }, |
| 1121 | { "e", "Tech Notes", SRCH_TECHNOTE }, |
| 1122 | { "f", "Forum", SRCH_FORUM }, |
| 1123 | }; |
| 1124 | const char *zY = PD("y","all"); |
| 1125 | unsigned newFlags = srchFlags; |
| 1126 | int i; |
| 1127 | @ <select size='1' name='y'> |
| @@ -1127,11 +1154,13 @@ | |
| 1154 | } |
| 1155 | if( search_run_and_output(zPattern, srchFlags, fDebug)==0 ){ |
| 1156 | @ <p class='searchEmpty'>No matches for: <span>%h(zPattern)</span></p> |
| 1157 | } |
| 1158 | @ </div> |
| 1159 | haveResult = 1; |
| 1160 | } |
| 1161 | return haveResult; |
| 1162 | } |
| 1163 | |
| 1164 | /* |
| 1165 | ** WEBPAGE: search |
| 1166 | ** |
| @@ -1143,10 +1172,11 @@ | |
| 1172 | ** c -> check-ins |
| 1173 | ** d -> documentation |
| 1174 | ** t -> tickets |
| 1175 | ** w -> wiki |
| 1176 | ** e -> tech notes |
| 1177 | ** f -> forum |
| 1178 | ** all -> everything |
| 1179 | */ |
| 1180 | void search_page(void){ |
| 1181 | login_check_credentials(); |
| 1182 | style_header("Search"); |
| @@ -1250,10 +1280,11 @@ | |
| 1280 | ** cType: d Embedded documentation |
| 1281 | ** w Wiki page |
| 1282 | ** c Check-in comment |
| 1283 | ** t Ticket text |
| 1284 | ** e Tech note |
| 1285 | ** f Forum |
| 1286 | ** |
| 1287 | ** rid The RID of an artifact that defines the object |
| 1288 | ** being searched. |
| 1289 | ** |
| 1290 | ** zName Name of the object being searched. This is used |
| @@ -1275,17 +1306,27 @@ | |
| 1306 | blob_to_utf8_no_bom(&doc, 0); |
| 1307 | get_stext_by_mimetype(&doc, mimetype_from_name(zName), pOut); |
| 1308 | blob_reset(&doc); |
| 1309 | break; |
| 1310 | } |
| 1311 | case 'f': /* Forum messages */ |
| 1312 | case 'e': /* Tech Notes */ |
| 1313 | case 'w': { /* Wiki */ |
| 1314 | Manifest *pWiki = manifest_get(rid, |
| 1315 | cType == 'e' ? CFTYPE_EVENT : |
| 1316 | cType == 'f' ? CFTYPE_FORUM : CFTYPE_WIKI, 0); |
| 1317 | Blob wiki; |
| 1318 | if( pWiki==0 ) break; |
| 1319 | if( cType=='f' ){ |
| 1320 | blob_init(&wiki, 0, 0); |
| 1321 | if( pWiki->zThreadTitle ){ |
| 1322 | blob_appendf(&wiki, "<h1>%h</h1>\n", pWiki->zThreadTitle); |
| 1323 | } |
| 1324 | blob_appendf(&wiki, "From %s:\n\n%s", pWiki->zUser, pWiki->zWiki); |
| 1325 | }else{ |
| 1326 | blob_init(&wiki, pWiki->zWiki, -1); |
| 1327 | } |
| 1328 | get_stext_by_mimetype(&wiki, wiki_filter_mimetypes(pWiki->zMimetype), |
| 1329 | pOut); |
| 1330 | blob_reset(&wiki); |
| 1331 | manifest_destroy(pWiki); |
| 1332 | break; |
| @@ -1516,11 +1557,11 @@ | |
| 1557 | "INSERT OR IGNORE INTO ftsdocs(type,rid,idxed)" |
| 1558 | " SELECT 't', tkt_id, 0 FROM ticket;" |
| 1559 | ); |
| 1560 | db_multi_exec( |
| 1561 | "INSERT OR IGNORE INTO ftsdocs(type,rid,name,idxed)" |
| 1562 | " SELECT type, objid, comment, 0 FROM event WHERE type IN ('e','f');" |
| 1563 | ); |
| 1564 | } |
| 1565 | |
| 1566 | /* |
| 1567 | ** The document described by cType,rid,zName is about to be added or |
| @@ -1553,10 +1594,11 @@ | |
| 1594 | db_multi_exec( |
| 1595 | "DELETE FROM ftsdocs WHERE type='%c' AND name=%Q AND rid!=%d", |
| 1596 | cType, zName, rid |
| 1597 | ); |
| 1598 | } |
| 1599 | /* All forum posts are always indexed */ |
| 1600 | } |
| 1601 | } |
| 1602 | |
| 1603 | /* |
| 1604 | ** If the doc-glob and doc-br settings are valid for document search |
| @@ -1681,10 +1723,33 @@ | |
| 1723 | " tagxref.mtime" |
| 1724 | " FROM tagxref WHERE tagxref.rid=ftsdocs.rid)" |
| 1725 | " WHERE ftsdocs.type='w' AND NOT ftsdocs.idxed" |
| 1726 | ); |
| 1727 | } |
| 1728 | |
| 1729 | /* |
| 1730 | ** Deal with all of the unindexed 'f' terms in FTSDOCS |
| 1731 | */ |
| 1732 | static void search_update_forum_index(void){ |
| 1733 | db_multi_exec( |
| 1734 | "INSERT INTO ftsidx(docid,title,body)" |
| 1735 | " SELECT rowid, title('f',rid,NULL),body('f',rid,NULL) FROM ftsdocs" |
| 1736 | " WHERE type='f' AND NOT idxed;" |
| 1737 | ); |
| 1738 | if( db_changes()==0 ) return; |
| 1739 | db_multi_exec( |
| 1740 | "UPDATE ftsdocs SET idxed=1, name=NULL," |
| 1741 | " (label,url,mtime) = " |
| 1742 | " (SELECT 'Forum '||event.comment," |
| 1743 | " '/forumpost/'||blob.uuid," |
| 1744 | " event.mtime" |
| 1745 | " FROM event, blob" |
| 1746 | " WHERE event.objid=ftsdocs.rid" |
| 1747 | " AND blob.rid=ftsdocs.rid)" |
| 1748 | "WHERE ftsdocs.type='f' AND NOT ftsdocs.idxed" |
| 1749 | ); |
| 1750 | } |
| 1751 | |
| 1752 | /* |
| 1753 | ** Deal with all of the unindexed 'e' terms in FTSDOCS |
| 1754 | */ |
| 1755 | static void search_update_technote_index(void){ |
| @@ -1728,10 +1793,13 @@ | |
| 1793 | search_update_wiki_index(); |
| 1794 | } |
| 1795 | if( srchFlags & SRCH_TECHNOTE ){ |
| 1796 | search_update_technote_index(); |
| 1797 | } |
| 1798 | if( srchFlags & SRCH_FORUM ){ |
| 1799 | search_update_forum_index(); |
| 1800 | } |
| 1801 | } |
| 1802 | |
| 1803 | /* |
| 1804 | ** Construct, prepopulate, and then update the full-text index. |
| 1805 | */ |
| @@ -1780,10 +1848,11 @@ | |
| 1848 | { "search-ci", "check-in search:", "c" }, |
| 1849 | { "search-doc", "document search:", "d" }, |
| 1850 | { "search-tkt", "ticket search:", "t" }, |
| 1851 | { "search-wiki", "wiki search:", "w" }, |
| 1852 | { "search-technote", "tech note search:", "e" }, |
| 1853 | { "search-forum", "forum search:", "f" }, |
| 1854 | }; |
| 1855 | char *zSubCmd = 0; |
| 1856 | int i, j, n; |
| 1857 | int iCmd = 0; |
| 1858 | int iAction = 0; |
| 1859 |
+56
-11
| --- src/security_audit.c | ||
| +++ src/security_audit.c | ||
| @@ -58,11 +58,11 @@ | ||
| 58 | 58 | /* Step 1: Determine if the repository is public or private. "Public" |
| 59 | 59 | ** means that any anonymous user on the internet can access all content. |
| 60 | 60 | ** "Private" repos require (non-anonymous) login to access all content, |
| 61 | 61 | ** though some content may be accessible anonymously. |
| 62 | 62 | */ |
| 63 | - zAnonCap = db_text("", "SELECT group_concat(coalesce(cap,'')) FROM user" | |
| 63 | + zAnonCap = db_text("", "SELECT capunion(cap) FROM user" | |
| 64 | 64 | " WHERE login IN ('anonymous','nobody')"); |
| 65 | 65 | zPubPages = db_get("public-pages",0); |
| 66 | 66 | if( hasAnyCap(zAnonCap,"as") ){ |
| 67 | 67 | @ <li><p>This repository is <big><b>Wildly INSECURE</b></big> because |
| 68 | 68 | @ it grants administrator privileges to anonymous users. You |
| @@ -78,11 +78,12 @@ | ||
| 78 | 78 | @ "nobody" on the <a href="setup_ulist">User Configuration</a> page. |
| 79 | 79 | }else if( hasAnyCap(zAnonCap,"goz") ){ |
| 80 | 80 | @ <li><p>This repository is <big><b>PUBLIC</b></big>. All |
| 81 | 81 | @ checked-in content can be accessed by anonymous users. |
| 82 | 82 | @ <a href="takeitprivate">Take it private</a>.<p> |
| 83 | - }else if( !hasAnyCap(zAnonCap, "jry") && (zPubPages==0 || zPubPages[0]==0) ){ | |
| 83 | + }else if( !hasAnyCap(zAnonCap, "jrwy234567") | |
| 84 | + && (zPubPages==0 || zPubPages[0]==0) ){ | |
| 84 | 85 | @ <li><p>This repository is <big><b>Completely PRIVATE</b></big>. |
| 85 | 86 | @ A valid login and password is required to access any content. |
| 86 | 87 | }else{ |
| 87 | 88 | @ <li><p>This repository is <big><b>Mostly PRIVATE</b></big>. |
| 88 | 89 | @ A valid login and password is usually required, however some |
| @@ -91,10 +92,13 @@ | ||
| 91 | 92 | if( hasAnyCap(zAnonCap,"j") ){ |
| 92 | 93 | @ <li> Wiki pages |
| 93 | 94 | } |
| 94 | 95 | if( hasAnyCap(zAnonCap,"r") ){ |
| 95 | 96 | @ <li> Tickets |
| 97 | + } | |
| 98 | + if( hasAnyCap(zAnonCap,"234567") ){ | |
| 99 | + @ <li> Forum posts | |
| 96 | 100 | } |
| 97 | 101 | if( zPubPages && zPubPages[0] ){ |
| 98 | 102 | Glob *pGlob = glob_create(zPubPages); |
| 99 | 103 | int i; |
| 100 | 104 | @ <li> URLs that match any of these GLOB patterns: |
| @@ -126,11 +130,12 @@ | ||
| 126 | 130 | */ |
| 127 | 131 | if( hasAnyCap(zAnonCap, "e") ){ |
| 128 | 132 | @ <li><p><b>WARNING:</b> |
| 129 | 133 | @ Anonymous users can view email addresses and other personally |
| 130 | 134 | @ identifiable information on tickets. |
| 131 | - @ <p>Fix this by removing the "Email" privilege from users | |
| 135 | + @ <p>Fix this by removing the "Email" privilege | |
| 136 | + @ (<a href="setup_ucap_list">capability "e") from users | |
| 132 | 137 | @ "anonymous" and "nobody" on the |
| 133 | 138 | @ <a href="setup_ulist">User Configuration</a> page. |
| 134 | 139 | } |
| 135 | 140 | |
| 136 | 141 | /* Anonymous users probably should not be allowed to push content |
| @@ -137,25 +142,27 @@ | ||
| 137 | 142 | ** to the repository. |
| 138 | 143 | */ |
| 139 | 144 | if( hasAnyCap(zAnonCap, "i") ){ |
| 140 | 145 | @ <li><p><b>WARNING:</b> |
| 141 | 146 | @ Anonymous users can push new check-ins into the repository. |
| 142 | - @ <p>Fix this by removing the "Check-in" privilege from users | |
| 147 | + @ <p>Fix this by removing the "Check-in" privilege | |
| 148 | + @ (<a href="setup_ucap_list">capability</a> "i") from users | |
| 143 | 149 | @ "anonymous" and "nobody" on the |
| 144 | 150 | @ <a href="setup_ulist">User Configuration</a> page. |
| 145 | 151 | } |
| 146 | 152 | |
| 147 | 153 | /* Anonymous users probably should not be allowed act as moderators |
| 148 | 154 | ** for wiki or tickets. |
| 149 | 155 | */ |
| 150 | - if( hasAnyCap(zAnonCap, "lq") ){ | |
| 156 | + if( hasAnyCap(zAnonCap, "lq5") ){ | |
| 151 | 157 | @ <li><p><b>WARNING:</b> |
| 152 | - @ Anonymous users can act as moderators for wiki and/or tickets. | |
| 153 | - @ This defeats the whole purpose of moderation. | |
| 154 | - @ <p>Fix this by removing the "Mod-Wiki" and "Mod-Tkt" | |
| 155 | - @ privilege from users "anonymous" and "nobody" on the | |
| 156 | - @ <a href="setup_ulist">User Configuration</a> page. | |
| 158 | + @ Anonymous users can act as moderators for wiki, tickets, or | |
| 159 | + @ forum posts. This defeats the whole purpose of moderation. | |
| 160 | + @ <p>Fix this by removing the "Mod-Wiki", "Mod-Tkt", and "Mod-Forum" | |
| 161 | + @ privileges (<a href="%R/setup_ucap_list">capabilities</a> "fq5") | |
| 162 | + @ from users "anonymous" and "nobody" | |
| 163 | + @ on the <a href="setup_ulist">User Configuration</a> page. | |
| 157 | 164 | } |
| 158 | 165 | |
| 159 | 166 | /* Anonymous users probably should not be allowed to delete |
| 160 | 167 | ** wiki or tickets. |
| 161 | 168 | */ |
| @@ -174,11 +181,11 @@ | ||
| 174 | 181 | if( db_get_boolean("modreq-wiki",0)==0 ){ |
| 175 | 182 | @ <li><p><b>WARNING:</b> |
| 176 | 183 | @ Anonymous users can create or edit wiki without moderation. |
| 177 | 184 | @ This can result in robots inserting lots of wiki spam into |
| 178 | 185 | @ repository. |
| 179 | - @ <p>Fix this by removing the "New-Wiki" and "Write-Wiki" | |
| 186 | + @ Fix this by removing the "New-Wiki" and "Write-Wiki" | |
| 180 | 187 | @ privileges from users "anonymous" and "nobody" on the |
| 181 | 188 | @ <a href="setup_ulist">User Configuration</a> page or |
| 182 | 189 | @ by enabling wiki moderation on the |
| 183 | 190 | @ <a href="setup_modreq">Moderation Setup</a> page. |
| 184 | 191 | }else{ |
| @@ -185,10 +192,36 @@ | ||
| 185 | 192 | @ <li><p> |
| 186 | 193 | @ Anonymous users can create or edit wiki, but moderator |
| 187 | 194 | @ approval is required before the edits become permanent. |
| 188 | 195 | } |
| 189 | 196 | } |
| 197 | + | |
| 198 | + /* Anonymous users should not be able to create trusted forum | |
| 199 | + ** posts. | |
| 200 | + */ | |
| 201 | + if( hasAnyCap(zAnonCap, "456") ){ | |
| 202 | + @ <li><p><b>WARNING:</b> | |
| 203 | + @ Anonymous users can create forum posts that are | |
| 204 | + @ accepted into the permanent record without moderation. | |
| 205 | + @ This can result in robots generating spam on forum posts. | |
| 206 | + @ Fix this by removing the "WriteTrusted-Forum" privilege | |
| 207 | + @ (<a href="setup_ucap_list">capabilities</a> "456") from | |
| 208 | + @ users "anonymous" and "nobody" on the | |
| 209 | + @ <a href="setup_ulist">User Configuration</a> page or | |
| 210 | + } | |
| 211 | + | |
| 212 | + /* Anonymous users should not be able to send announcements. | |
| 213 | + */ | |
| 214 | + if( hasAnyCap(zAnonCap, "A") ){ | |
| 215 | + @ <li><p><b>WARNING:</b> | |
| 216 | + @ Anonymous users can send announcements to anybody who is signed | |
| 217 | + @ up to receive announcements. This can result in spam. | |
| 218 | + @ Fix this by removing the "Announce" privilege | |
| 219 | + @ (<a href="setup_ucap_list">capability</a> "A") from | |
| 220 | + @ users "anonymous" and "nobody" on the | |
| 221 | + @ <a href="setup_ulist">User Configuration</a> page or | |
| 222 | + } | |
| 190 | 223 | |
| 191 | 224 | /* Administrative privilege should only be provided to |
| 192 | 225 | ** specific individuals, not to entire classes of people. |
| 193 | 226 | ** And not too many people should have administrator privilege. |
| 194 | 227 | */ |
| @@ -349,10 +382,22 @@ | ||
| 349 | 382 | @ <li><p> |
| 350 | 383 | @ The error log at "<a href='%R/errorlog'>%h(g.zErrlog)</a>" that is |
| 351 | 384 | @ %,lld(file_size(g.zErrlog, ExtFILE)) bytes in size. |
| 352 | 385 | } |
| 353 | 386 | } |
| 387 | + | |
| 388 | + @ <li><p> User capability summary: | |
| 389 | + capability_summary(); | |
| 390 | + | |
| 391 | + if( email_enabled() ){ | |
| 392 | + @ <li><p> Email alert configuration summary: | |
| 393 | + @ <table class="label-value"> | |
| 394 | + stats_for_email(); | |
| 395 | + @ </table> | |
| 396 | + }else{ | |
| 397 | + @ <li><p> Email alerts are disabled | |
| 398 | + } | |
| 354 | 399 | |
| 355 | 400 | @ </ol> |
| 356 | 401 | style_footer(); |
| 357 | 402 | } |
| 358 | 403 | |
| 359 | 404 |
| --- src/security_audit.c | |
| +++ src/security_audit.c | |
| @@ -58,11 +58,11 @@ | |
| 58 | /* Step 1: Determine if the repository is public or private. "Public" |
| 59 | ** means that any anonymous user on the internet can access all content. |
| 60 | ** "Private" repos require (non-anonymous) login to access all content, |
| 61 | ** though some content may be accessible anonymously. |
| 62 | */ |
| 63 | zAnonCap = db_text("", "SELECT group_concat(coalesce(cap,'')) FROM user" |
| 64 | " WHERE login IN ('anonymous','nobody')"); |
| 65 | zPubPages = db_get("public-pages",0); |
| 66 | if( hasAnyCap(zAnonCap,"as") ){ |
| 67 | @ <li><p>This repository is <big><b>Wildly INSECURE</b></big> because |
| 68 | @ it grants administrator privileges to anonymous users. You |
| @@ -78,11 +78,12 @@ | |
| 78 | @ "nobody" on the <a href="setup_ulist">User Configuration</a> page. |
| 79 | }else if( hasAnyCap(zAnonCap,"goz") ){ |
| 80 | @ <li><p>This repository is <big><b>PUBLIC</b></big>. All |
| 81 | @ checked-in content can be accessed by anonymous users. |
| 82 | @ <a href="takeitprivate">Take it private</a>.<p> |
| 83 | }else if( !hasAnyCap(zAnonCap, "jry") && (zPubPages==0 || zPubPages[0]==0) ){ |
| 84 | @ <li><p>This repository is <big><b>Completely PRIVATE</b></big>. |
| 85 | @ A valid login and password is required to access any content. |
| 86 | }else{ |
| 87 | @ <li><p>This repository is <big><b>Mostly PRIVATE</b></big>. |
| 88 | @ A valid login and password is usually required, however some |
| @@ -91,10 +92,13 @@ | |
| 91 | if( hasAnyCap(zAnonCap,"j") ){ |
| 92 | @ <li> Wiki pages |
| 93 | } |
| 94 | if( hasAnyCap(zAnonCap,"r") ){ |
| 95 | @ <li> Tickets |
| 96 | } |
| 97 | if( zPubPages && zPubPages[0] ){ |
| 98 | Glob *pGlob = glob_create(zPubPages); |
| 99 | int i; |
| 100 | @ <li> URLs that match any of these GLOB patterns: |
| @@ -126,11 +130,12 @@ | |
| 126 | */ |
| 127 | if( hasAnyCap(zAnonCap, "e") ){ |
| 128 | @ <li><p><b>WARNING:</b> |
| 129 | @ Anonymous users can view email addresses and other personally |
| 130 | @ identifiable information on tickets. |
| 131 | @ <p>Fix this by removing the "Email" privilege from users |
| 132 | @ "anonymous" and "nobody" on the |
| 133 | @ <a href="setup_ulist">User Configuration</a> page. |
| 134 | } |
| 135 | |
| 136 | /* Anonymous users probably should not be allowed to push content |
| @@ -137,25 +142,27 @@ | |
| 137 | ** to the repository. |
| 138 | */ |
| 139 | if( hasAnyCap(zAnonCap, "i") ){ |
| 140 | @ <li><p><b>WARNING:</b> |
| 141 | @ Anonymous users can push new check-ins into the repository. |
| 142 | @ <p>Fix this by removing the "Check-in" privilege from users |
| 143 | @ "anonymous" and "nobody" on the |
| 144 | @ <a href="setup_ulist">User Configuration</a> page. |
| 145 | } |
| 146 | |
| 147 | /* Anonymous users probably should not be allowed act as moderators |
| 148 | ** for wiki or tickets. |
| 149 | */ |
| 150 | if( hasAnyCap(zAnonCap, "lq") ){ |
| 151 | @ <li><p><b>WARNING:</b> |
| 152 | @ Anonymous users can act as moderators for wiki and/or tickets. |
| 153 | @ This defeats the whole purpose of moderation. |
| 154 | @ <p>Fix this by removing the "Mod-Wiki" and "Mod-Tkt" |
| 155 | @ privilege from users "anonymous" and "nobody" on the |
| 156 | @ <a href="setup_ulist">User Configuration</a> page. |
| 157 | } |
| 158 | |
| 159 | /* Anonymous users probably should not be allowed to delete |
| 160 | ** wiki or tickets. |
| 161 | */ |
| @@ -174,11 +181,11 @@ | |
| 174 | if( db_get_boolean("modreq-wiki",0)==0 ){ |
| 175 | @ <li><p><b>WARNING:</b> |
| 176 | @ Anonymous users can create or edit wiki without moderation. |
| 177 | @ This can result in robots inserting lots of wiki spam into |
| 178 | @ repository. |
| 179 | @ <p>Fix this by removing the "New-Wiki" and "Write-Wiki" |
| 180 | @ privileges from users "anonymous" and "nobody" on the |
| 181 | @ <a href="setup_ulist">User Configuration</a> page or |
| 182 | @ by enabling wiki moderation on the |
| 183 | @ <a href="setup_modreq">Moderation Setup</a> page. |
| 184 | }else{ |
| @@ -185,10 +192,36 @@ | |
| 185 | @ <li><p> |
| 186 | @ Anonymous users can create or edit wiki, but moderator |
| 187 | @ approval is required before the edits become permanent. |
| 188 | } |
| 189 | } |
| 190 | |
| 191 | /* Administrative privilege should only be provided to |
| 192 | ** specific individuals, not to entire classes of people. |
| 193 | ** And not too many people should have administrator privilege. |
| 194 | */ |
| @@ -349,10 +382,22 @@ | |
| 349 | @ <li><p> |
| 350 | @ The error log at "<a href='%R/errorlog'>%h(g.zErrlog)</a>" that is |
| 351 | @ %,lld(file_size(g.zErrlog, ExtFILE)) bytes in size. |
| 352 | } |
| 353 | } |
| 354 | |
| 355 | @ </ol> |
| 356 | style_footer(); |
| 357 | } |
| 358 | |
| 359 |
| --- src/security_audit.c | |
| +++ src/security_audit.c | |
| @@ -58,11 +58,11 @@ | |
| 58 | /* Step 1: Determine if the repository is public or private. "Public" |
| 59 | ** means that any anonymous user on the internet can access all content. |
| 60 | ** "Private" repos require (non-anonymous) login to access all content, |
| 61 | ** though some content may be accessible anonymously. |
| 62 | */ |
| 63 | zAnonCap = db_text("", "SELECT capunion(cap) FROM user" |
| 64 | " WHERE login IN ('anonymous','nobody')"); |
| 65 | zPubPages = db_get("public-pages",0); |
| 66 | if( hasAnyCap(zAnonCap,"as") ){ |
| 67 | @ <li><p>This repository is <big><b>Wildly INSECURE</b></big> because |
| 68 | @ it grants administrator privileges to anonymous users. You |
| @@ -78,11 +78,12 @@ | |
| 78 | @ "nobody" on the <a href="setup_ulist">User Configuration</a> page. |
| 79 | }else if( hasAnyCap(zAnonCap,"goz") ){ |
| 80 | @ <li><p>This repository is <big><b>PUBLIC</b></big>. All |
| 81 | @ checked-in content can be accessed by anonymous users. |
| 82 | @ <a href="takeitprivate">Take it private</a>.<p> |
| 83 | }else if( !hasAnyCap(zAnonCap, "jrwy234567") |
| 84 | && (zPubPages==0 || zPubPages[0]==0) ){ |
| 85 | @ <li><p>This repository is <big><b>Completely PRIVATE</b></big>. |
| 86 | @ A valid login and password is required to access any content. |
| 87 | }else{ |
| 88 | @ <li><p>This repository is <big><b>Mostly PRIVATE</b></big>. |
| 89 | @ A valid login and password is usually required, however some |
| @@ -91,10 +92,13 @@ | |
| 92 | if( hasAnyCap(zAnonCap,"j") ){ |
| 93 | @ <li> Wiki pages |
| 94 | } |
| 95 | if( hasAnyCap(zAnonCap,"r") ){ |
| 96 | @ <li> Tickets |
| 97 | } |
| 98 | if( hasAnyCap(zAnonCap,"234567") ){ |
| 99 | @ <li> Forum posts |
| 100 | } |
| 101 | if( zPubPages && zPubPages[0] ){ |
| 102 | Glob *pGlob = glob_create(zPubPages); |
| 103 | int i; |
| 104 | @ <li> URLs that match any of these GLOB patterns: |
| @@ -126,11 +130,12 @@ | |
| 130 | */ |
| 131 | if( hasAnyCap(zAnonCap, "e") ){ |
| 132 | @ <li><p><b>WARNING:</b> |
| 133 | @ Anonymous users can view email addresses and other personally |
| 134 | @ identifiable information on tickets. |
| 135 | @ <p>Fix this by removing the "Email" privilege |
| 136 | @ (<a href="setup_ucap_list">capability "e") from users |
| 137 | @ "anonymous" and "nobody" on the |
| 138 | @ <a href="setup_ulist">User Configuration</a> page. |
| 139 | } |
| 140 | |
| 141 | /* Anonymous users probably should not be allowed to push content |
| @@ -137,25 +142,27 @@ | |
| 142 | ** to the repository. |
| 143 | */ |
| 144 | if( hasAnyCap(zAnonCap, "i") ){ |
| 145 | @ <li><p><b>WARNING:</b> |
| 146 | @ Anonymous users can push new check-ins into the repository. |
| 147 | @ <p>Fix this by removing the "Check-in" privilege |
| 148 | @ (<a href="setup_ucap_list">capability</a> "i") from users |
| 149 | @ "anonymous" and "nobody" on the |
| 150 | @ <a href="setup_ulist">User Configuration</a> page. |
| 151 | } |
| 152 | |
| 153 | /* Anonymous users probably should not be allowed act as moderators |
| 154 | ** for wiki or tickets. |
| 155 | */ |
| 156 | if( hasAnyCap(zAnonCap, "lq5") ){ |
| 157 | @ <li><p><b>WARNING:</b> |
| 158 | @ Anonymous users can act as moderators for wiki, tickets, or |
| 159 | @ forum posts. This defeats the whole purpose of moderation. |
| 160 | @ <p>Fix this by removing the "Mod-Wiki", "Mod-Tkt", and "Mod-Forum" |
| 161 | @ privileges (<a href="%R/setup_ucap_list">capabilities</a> "fq5") |
| 162 | @ from users "anonymous" and "nobody" |
| 163 | @ on the <a href="setup_ulist">User Configuration</a> page. |
| 164 | } |
| 165 | |
| 166 | /* Anonymous users probably should not be allowed to delete |
| 167 | ** wiki or tickets. |
| 168 | */ |
| @@ -174,11 +181,11 @@ | |
| 181 | if( db_get_boolean("modreq-wiki",0)==0 ){ |
| 182 | @ <li><p><b>WARNING:</b> |
| 183 | @ Anonymous users can create or edit wiki without moderation. |
| 184 | @ This can result in robots inserting lots of wiki spam into |
| 185 | @ repository. |
| 186 | @ Fix this by removing the "New-Wiki" and "Write-Wiki" |
| 187 | @ privileges from users "anonymous" and "nobody" on the |
| 188 | @ <a href="setup_ulist">User Configuration</a> page or |
| 189 | @ by enabling wiki moderation on the |
| 190 | @ <a href="setup_modreq">Moderation Setup</a> page. |
| 191 | }else{ |
| @@ -185,10 +192,36 @@ | |
| 192 | @ <li><p> |
| 193 | @ Anonymous users can create or edit wiki, but moderator |
| 194 | @ approval is required before the edits become permanent. |
| 195 | } |
| 196 | } |
| 197 | |
| 198 | /* Anonymous users should not be able to create trusted forum |
| 199 | ** posts. |
| 200 | */ |
| 201 | if( hasAnyCap(zAnonCap, "456") ){ |
| 202 | @ <li><p><b>WARNING:</b> |
| 203 | @ Anonymous users can create forum posts that are |
| 204 | @ accepted into the permanent record without moderation. |
| 205 | @ This can result in robots generating spam on forum posts. |
| 206 | @ Fix this by removing the "WriteTrusted-Forum" privilege |
| 207 | @ (<a href="setup_ucap_list">capabilities</a> "456") from |
| 208 | @ users "anonymous" and "nobody" on the |
| 209 | @ <a href="setup_ulist">User Configuration</a> page or |
| 210 | } |
| 211 | |
| 212 | /* Anonymous users should not be able to send announcements. |
| 213 | */ |
| 214 | if( hasAnyCap(zAnonCap, "A") ){ |
| 215 | @ <li><p><b>WARNING:</b> |
| 216 | @ Anonymous users can send announcements to anybody who is signed |
| 217 | @ up to receive announcements. This can result in spam. |
| 218 | @ Fix this by removing the "Announce" privilege |
| 219 | @ (<a href="setup_ucap_list">capability</a> "A") from |
| 220 | @ users "anonymous" and "nobody" on the |
| 221 | @ <a href="setup_ulist">User Configuration</a> page or |
| 222 | } |
| 223 | |
| 224 | /* Administrative privilege should only be provided to |
| 225 | ** specific individuals, not to entire classes of people. |
| 226 | ** And not too many people should have administrator privilege. |
| 227 | */ |
| @@ -349,10 +382,22 @@ | |
| 382 | @ <li><p> |
| 383 | @ The error log at "<a href='%R/errorlog'>%h(g.zErrlog)</a>" that is |
| 384 | @ %,lld(file_size(g.zErrlog, ExtFILE)) bytes in size. |
| 385 | } |
| 386 | } |
| 387 | |
| 388 | @ <li><p> User capability summary: |
| 389 | capability_summary(); |
| 390 | |
| 391 | if( email_enabled() ){ |
| 392 | @ <li><p> Email alert configuration summary: |
| 393 | @ <table class="label-value"> |
| 394 | stats_for_email(); |
| 395 | @ </table> |
| 396 | }else{ |
| 397 | @ <li><p> Email alerts are disabled |
| 398 | } |
| 399 | |
| 400 | @ </ol> |
| 401 | style_footer(); |
| 402 | } |
| 403 | |
| 404 |
+6
-83
| --- src/setup.c | ||
| +++ src/setup.c | ||
| @@ -296,91 +296,10 @@ | ||
| 296 | 296 | db_finalize(&s); |
| 297 | 297 | style_table_sorter(); |
| 298 | 298 | style_footer(); |
| 299 | 299 | } |
| 300 | 300 | |
| 301 | -/* | |
| 302 | -** Render the user-capability table | |
| 303 | -*/ | |
| 304 | -static void setup_usercap_table(void){ | |
| 305 | - @ <table> | |
| 306 | - @ <tr><th valign="top">a</th> | |
| 307 | - @ <td><i>Admin:</i> Create and delete users</td></tr> | |
| 308 | - @ <tr><th valign="top">b</th> | |
| 309 | - @ <td><i>Attach:</i> Add attachments to wiki or tickets</td></tr> | |
| 310 | - @ <tr><th valign="top">c</th> | |
| 311 | - @ <td><i>Append-Tkt:</i> Append to tickets</td></tr> | |
| 312 | - @ <tr><th valign="top">d</th> | |
| 313 | - @ <td><i>Delete:</i> Delete wiki and tickets</td></tr> | |
| 314 | - @ <tr><th valign="top">e</th> | |
| 315 | - @ <td><i>View-PII:</i> \ | |
| 316 | - @ View sensitive data such as email addresses</td></tr> | |
| 317 | - @ <tr><th valign="top">f</th> | |
| 318 | - @ <td><i>New-Wiki:</i> Create new wiki pages</td></tr> | |
| 319 | - @ <tr><th valign="top">g</th> | |
| 320 | - @ <td><i>Clone:</i> Clone the repository</td></tr> | |
| 321 | - @ <tr><th valign="top">h</th> | |
| 322 | - @ <td><i>Hyperlinks:</i> Show hyperlinks to detailed | |
| 323 | - @ repository history</td></tr> | |
| 324 | - @ <tr><th valign="top">i</th> | |
| 325 | - @ <td><i>Check-In:</i> Commit new versions in the repository</td></tr> | |
| 326 | - @ <tr><th valign="top">j</th> | |
| 327 | - @ <td><i>Read-Wiki:</i> View wiki pages</td></tr> | |
| 328 | - @ <tr><th valign="top">k</th> | |
| 329 | - @ <td><i>Write-Wiki:</i> Edit wiki pages</td></tr> | |
| 330 | - @ <tr><th valign="top">l</th> | |
| 331 | - @ <td><i>Mod-Wiki:</i> Moderator for wiki pages</td></tr> | |
| 332 | - @ <tr><th valign="top">m</th> | |
| 333 | - @ <td><i>Append-Wiki:</i> Append to wiki pages</td></tr> | |
| 334 | - @ <tr><th valign="top">n</th> | |
| 335 | - @ <td><i>New-Tkt:</i> Create new tickets</td></tr> | |
| 336 | - @ <tr><th valign="top">o</th> | |
| 337 | - @ <td><i>Check-Out:</i> Check out versions</td></tr> | |
| 338 | - @ <tr><th valign="top">p</th> | |
| 339 | - @ <td><i>Password:</i> Change your own password</td></tr> | |
| 340 | - @ <tr><th valign="top">q</th> | |
| 341 | - @ <td><i>Mod-Tkt:</i> Moderator for tickets</td></tr> | |
| 342 | - @ <tr><th valign="top">r</th> | |
| 343 | - @ <td><i>Read-Tkt:</i> View tickets</td></tr> | |
| 344 | - @ <tr><th valign="top">s</th> | |
| 345 | - @ <td><i>Setup/Super-user:</i> Setup and configure this website</td></tr> | |
| 346 | - @ <tr><th valign="top">t</th> | |
| 347 | - @ <td><i>Tkt-Report:</i> Create new bug summary reports</td></tr> | |
| 348 | - @ <tr><th valign="top">u</th> | |
| 349 | - @ <td><i>Reader:</i> Inherit privileges of | |
| 350 | - @ user <tt>reader</tt></td></tr> | |
| 351 | - @ <tr><th valign="top">v</th> | |
| 352 | - @ <td><i>Developer:</i> Inherit privileges of | |
| 353 | - @ user <tt>developer</tt></td></tr> | |
| 354 | - @ <tr><th valign="top">w</th> | |
| 355 | - @ <td><i>Write-Tkt:</i> Edit tickets</td></tr> | |
| 356 | - @ <tr><th valign="top">x</th> | |
| 357 | - @ <td><i>Private:</i> Push and/or pull private branches</td></tr> | |
| 358 | - @ <tr><th valign="top">y</th> | |
| 359 | - @ <td><i>Write-Unver:</i> Push unversioned files</td></tr> | |
| 360 | - @ <tr><th valign="top">z</th> | |
| 361 | - @ <td><i>Zip download:</i> Download a ZIP archive or tarball</td></tr> | |
| 362 | - @ <tr><th valign="top">2</th> | |
| 363 | - @ <td><i>Forum-Read:</i> Read forum posts by others </td></tr> | |
| 364 | - @ <tr><th valign="top">3</th> | |
| 365 | - @ <td><i>Forum-Append:</i> Add new forum posts</td></tr> | |
| 366 | - @ <tr><th valign="top">4</th> | |
| 367 | - @ <td><i>Forum-Trusted:</i> Add pre-approved forum posts </td></tr> | |
| 368 | - @ <tr><th valign="top">5</th> | |
| 369 | - @ <td><i>Forum-Moderator:</i> Approve or disapprove forum posts</td></tr> | |
| 370 | - @ <tr><th valign="top">6</th> | |
| 371 | - @ <td><i>Forum-Supervisor:</i> \ | |
| 372 | - @ Forum administrator | |
| 373 | - @ <tr><th valign="top">7</th> | |
| 374 | - @ <td><i>Email-Alerts:</i> Sign up for email nofications</td></tr> | |
| 375 | - @ <tr><th valign="top">A</th> | |
| 376 | - @ <td><i>Announce:</i> Send announcements</td></tr> | |
| 377 | - @ <tr><th valign="top">D</th> | |
| 378 | - @ <td><i>Debug:</i> Enable debugging features</td></tr> | |
| 379 | - @ </table> | |
| 380 | -} | |
| 381 | - | |
| 382 | 301 | /* |
| 383 | 302 | ** WEBPAGE: setup_ulist_notes |
| 384 | 303 | ** |
| 385 | 304 | ** A documentation page showing notes about user configuration. This |
| 386 | 305 | ** information used to be a side-bar on the user list page, but has been |
| @@ -417,11 +336,11 @@ | ||
| 417 | 336 | @ <span class="usertype">anonymous</span>, and |
| 418 | 337 | @ <span class="usertype">nobody</span>. |
| 419 | 338 | @ </p></li> |
| 420 | 339 | @ |
| 421 | 340 | @ <li><p>The permission flags are as follows:</p> |
| 422 | - setup_usercap_table(); | |
| 341 | + capabilities_table(); | |
| 423 | 342 | @ </li> |
| 424 | 343 | @ </ol> |
| 425 | 344 | style_footer(); |
| 426 | 345 | } |
| 427 | 346 | |
| @@ -431,11 +350,11 @@ | ||
| 431 | 350 | ** A documentation page showing the meaning of the various user capabilities |
| 432 | 351 | ** code letters. |
| 433 | 352 | */ |
| 434 | 353 | void setup_ucap_list(void){ |
| 435 | 354 | style_header("User Capability Codes"); |
| 436 | - setup_usercap_table(); | |
| 355 | + capabilities_table(); | |
| 437 | 356 | style_footer(); |
| 438 | 357 | } |
| 439 | 358 | |
| 440 | 359 | /* |
| 441 | 360 | ** Return true if zPw is a valid password string. A valid |
| @@ -1312,10 +1231,12 @@ | ||
| 1312 | 1231 | "defaultperms", "u", 0); |
| 1313 | 1232 | @ <p>Permissions given to users that... <ul><li>register themselves using |
| 1314 | 1233 | @ the self-registration procedure (if enabled), or <li>access "public" |
| 1315 | 1234 | @ pages identified by the public-pages glob pattern above, or <li> |
| 1316 | 1235 | @ are users newly created by the administrator.</ul> |
| 1236 | + @ <p>Recommended value: "u" for Reader. | |
| 1237 | + @ <a href="%R/setup_ucap_list">Capability Key</a>. | |
| 1317 | 1238 | @ (Property: "default-perms") |
| 1318 | 1239 | @ </p> |
| 1319 | 1240 | |
| 1320 | 1241 | @ <hr /> |
| 1321 | 1242 | onoff_attribute("Show javascript button to fill in CAPTCHA", |
| @@ -2312,10 +2233,12 @@ | ||
| 2312 | 2233 | onoff_attribute("Search Tickets", "search-tkt", "st", 0, 0); |
| 2313 | 2234 | @ <br /> |
| 2314 | 2235 | onoff_attribute("Search Wiki", "search-wiki", "sw", 0, 0); |
| 2315 | 2236 | @ <br /> |
| 2316 | 2237 | onoff_attribute("Search Tech Notes", "search-technote", "se", 0, 0); |
| 2238 | + @ <br /> | |
| 2239 | + onoff_attribute("Search Forum", "search-forum", "sf", 0, 0); | |
| 2317 | 2240 | @ <hr /> |
| 2318 | 2241 | @ <p><input type="submit" name="submit" value="Apply Changes" /></p> |
| 2319 | 2242 | @ <hr /> |
| 2320 | 2243 | if( P("fts0") ){ |
| 2321 | 2244 | search_drop_index(); |
| 2322 | 2245 |
| --- src/setup.c | |
| +++ src/setup.c | |
| @@ -296,91 +296,10 @@ | |
| 296 | db_finalize(&s); |
| 297 | style_table_sorter(); |
| 298 | style_footer(); |
| 299 | } |
| 300 | |
| 301 | /* |
| 302 | ** Render the user-capability table |
| 303 | */ |
| 304 | static void setup_usercap_table(void){ |
| 305 | @ <table> |
| 306 | @ <tr><th valign="top">a</th> |
| 307 | @ <td><i>Admin:</i> Create and delete users</td></tr> |
| 308 | @ <tr><th valign="top">b</th> |
| 309 | @ <td><i>Attach:</i> Add attachments to wiki or tickets</td></tr> |
| 310 | @ <tr><th valign="top">c</th> |
| 311 | @ <td><i>Append-Tkt:</i> Append to tickets</td></tr> |
| 312 | @ <tr><th valign="top">d</th> |
| 313 | @ <td><i>Delete:</i> Delete wiki and tickets</td></tr> |
| 314 | @ <tr><th valign="top">e</th> |
| 315 | @ <td><i>View-PII:</i> \ |
| 316 | @ View sensitive data such as email addresses</td></tr> |
| 317 | @ <tr><th valign="top">f</th> |
| 318 | @ <td><i>New-Wiki:</i> Create new wiki pages</td></tr> |
| 319 | @ <tr><th valign="top">g</th> |
| 320 | @ <td><i>Clone:</i> Clone the repository</td></tr> |
| 321 | @ <tr><th valign="top">h</th> |
| 322 | @ <td><i>Hyperlinks:</i> Show hyperlinks to detailed |
| 323 | @ repository history</td></tr> |
| 324 | @ <tr><th valign="top">i</th> |
| 325 | @ <td><i>Check-In:</i> Commit new versions in the repository</td></tr> |
| 326 | @ <tr><th valign="top">j</th> |
| 327 | @ <td><i>Read-Wiki:</i> View wiki pages</td></tr> |
| 328 | @ <tr><th valign="top">k</th> |
| 329 | @ <td><i>Write-Wiki:</i> Edit wiki pages</td></tr> |
| 330 | @ <tr><th valign="top">l</th> |
| 331 | @ <td><i>Mod-Wiki:</i> Moderator for wiki pages</td></tr> |
| 332 | @ <tr><th valign="top">m</th> |
| 333 | @ <td><i>Append-Wiki:</i> Append to wiki pages</td></tr> |
| 334 | @ <tr><th valign="top">n</th> |
| 335 | @ <td><i>New-Tkt:</i> Create new tickets</td></tr> |
| 336 | @ <tr><th valign="top">o</th> |
| 337 | @ <td><i>Check-Out:</i> Check out versions</td></tr> |
| 338 | @ <tr><th valign="top">p</th> |
| 339 | @ <td><i>Password:</i> Change your own password</td></tr> |
| 340 | @ <tr><th valign="top">q</th> |
| 341 | @ <td><i>Mod-Tkt:</i> Moderator for tickets</td></tr> |
| 342 | @ <tr><th valign="top">r</th> |
| 343 | @ <td><i>Read-Tkt:</i> View tickets</td></tr> |
| 344 | @ <tr><th valign="top">s</th> |
| 345 | @ <td><i>Setup/Super-user:</i> Setup and configure this website</td></tr> |
| 346 | @ <tr><th valign="top">t</th> |
| 347 | @ <td><i>Tkt-Report:</i> Create new bug summary reports</td></tr> |
| 348 | @ <tr><th valign="top">u</th> |
| 349 | @ <td><i>Reader:</i> Inherit privileges of |
| 350 | @ user <tt>reader</tt></td></tr> |
| 351 | @ <tr><th valign="top">v</th> |
| 352 | @ <td><i>Developer:</i> Inherit privileges of |
| 353 | @ user <tt>developer</tt></td></tr> |
| 354 | @ <tr><th valign="top">w</th> |
| 355 | @ <td><i>Write-Tkt:</i> Edit tickets</td></tr> |
| 356 | @ <tr><th valign="top">x</th> |
| 357 | @ <td><i>Private:</i> Push and/or pull private branches</td></tr> |
| 358 | @ <tr><th valign="top">y</th> |
| 359 | @ <td><i>Write-Unver:</i> Push unversioned files</td></tr> |
| 360 | @ <tr><th valign="top">z</th> |
| 361 | @ <td><i>Zip download:</i> Download a ZIP archive or tarball</td></tr> |
| 362 | @ <tr><th valign="top">2</th> |
| 363 | @ <td><i>Forum-Read:</i> Read forum posts by others </td></tr> |
| 364 | @ <tr><th valign="top">3</th> |
| 365 | @ <td><i>Forum-Append:</i> Add new forum posts</td></tr> |
| 366 | @ <tr><th valign="top">4</th> |
| 367 | @ <td><i>Forum-Trusted:</i> Add pre-approved forum posts </td></tr> |
| 368 | @ <tr><th valign="top">5</th> |
| 369 | @ <td><i>Forum-Moderator:</i> Approve or disapprove forum posts</td></tr> |
| 370 | @ <tr><th valign="top">6</th> |
| 371 | @ <td><i>Forum-Supervisor:</i> \ |
| 372 | @ Forum administrator |
| 373 | @ <tr><th valign="top">7</th> |
| 374 | @ <td><i>Email-Alerts:</i> Sign up for email nofications</td></tr> |
| 375 | @ <tr><th valign="top">A</th> |
| 376 | @ <td><i>Announce:</i> Send announcements</td></tr> |
| 377 | @ <tr><th valign="top">D</th> |
| 378 | @ <td><i>Debug:</i> Enable debugging features</td></tr> |
| 379 | @ </table> |
| 380 | } |
| 381 | |
| 382 | /* |
| 383 | ** WEBPAGE: setup_ulist_notes |
| 384 | ** |
| 385 | ** A documentation page showing notes about user configuration. This |
| 386 | ** information used to be a side-bar on the user list page, but has been |
| @@ -417,11 +336,11 @@ | |
| 417 | @ <span class="usertype">anonymous</span>, and |
| 418 | @ <span class="usertype">nobody</span>. |
| 419 | @ </p></li> |
| 420 | @ |
| 421 | @ <li><p>The permission flags are as follows:</p> |
| 422 | setup_usercap_table(); |
| 423 | @ </li> |
| 424 | @ </ol> |
| 425 | style_footer(); |
| 426 | } |
| 427 | |
| @@ -431,11 +350,11 @@ | |
| 431 | ** A documentation page showing the meaning of the various user capabilities |
| 432 | ** code letters. |
| 433 | */ |
| 434 | void setup_ucap_list(void){ |
| 435 | style_header("User Capability Codes"); |
| 436 | setup_usercap_table(); |
| 437 | style_footer(); |
| 438 | } |
| 439 | |
| 440 | /* |
| 441 | ** Return true if zPw is a valid password string. A valid |
| @@ -1312,10 +1231,12 @@ | |
| 1312 | "defaultperms", "u", 0); |
| 1313 | @ <p>Permissions given to users that... <ul><li>register themselves using |
| 1314 | @ the self-registration procedure (if enabled), or <li>access "public" |
| 1315 | @ pages identified by the public-pages glob pattern above, or <li> |
| 1316 | @ are users newly created by the administrator.</ul> |
| 1317 | @ (Property: "default-perms") |
| 1318 | @ </p> |
| 1319 | |
| 1320 | @ <hr /> |
| 1321 | onoff_attribute("Show javascript button to fill in CAPTCHA", |
| @@ -2312,10 +2233,12 @@ | |
| 2312 | onoff_attribute("Search Tickets", "search-tkt", "st", 0, 0); |
| 2313 | @ <br /> |
| 2314 | onoff_attribute("Search Wiki", "search-wiki", "sw", 0, 0); |
| 2315 | @ <br /> |
| 2316 | onoff_attribute("Search Tech Notes", "search-technote", "se", 0, 0); |
| 2317 | @ <hr /> |
| 2318 | @ <p><input type="submit" name="submit" value="Apply Changes" /></p> |
| 2319 | @ <hr /> |
| 2320 | if( P("fts0") ){ |
| 2321 | search_drop_index(); |
| 2322 |
| --- src/setup.c | |
| +++ src/setup.c | |
| @@ -296,91 +296,10 @@ | |
| 296 | db_finalize(&s); |
| 297 | style_table_sorter(); |
| 298 | style_footer(); |
| 299 | } |
| 300 | |
| 301 | /* |
| 302 | ** WEBPAGE: setup_ulist_notes |
| 303 | ** |
| 304 | ** A documentation page showing notes about user configuration. This |
| 305 | ** information used to be a side-bar on the user list page, but has been |
| @@ -417,11 +336,11 @@ | |
| 336 | @ <span class="usertype">anonymous</span>, and |
| 337 | @ <span class="usertype">nobody</span>. |
| 338 | @ </p></li> |
| 339 | @ |
| 340 | @ <li><p>The permission flags are as follows:</p> |
| 341 | capabilities_table(); |
| 342 | @ </li> |
| 343 | @ </ol> |
| 344 | style_footer(); |
| 345 | } |
| 346 | |
| @@ -431,11 +350,11 @@ | |
| 350 | ** A documentation page showing the meaning of the various user capabilities |
| 351 | ** code letters. |
| 352 | */ |
| 353 | void setup_ucap_list(void){ |
| 354 | style_header("User Capability Codes"); |
| 355 | capabilities_table(); |
| 356 | style_footer(); |
| 357 | } |
| 358 | |
| 359 | /* |
| 360 | ** Return true if zPw is a valid password string. A valid |
| @@ -1312,10 +1231,12 @@ | |
| 1231 | "defaultperms", "u", 0); |
| 1232 | @ <p>Permissions given to users that... <ul><li>register themselves using |
| 1233 | @ the self-registration procedure (if enabled), or <li>access "public" |
| 1234 | @ pages identified by the public-pages glob pattern above, or <li> |
| 1235 | @ are users newly created by the administrator.</ul> |
| 1236 | @ <p>Recommended value: "u" for Reader. |
| 1237 | @ <a href="%R/setup_ucap_list">Capability Key</a>. |
| 1238 | @ (Property: "default-perms") |
| 1239 | @ </p> |
| 1240 | |
| 1241 | @ <hr /> |
| 1242 | onoff_attribute("Show javascript button to fill in CAPTCHA", |
| @@ -2312,10 +2233,12 @@ | |
| 2233 | onoff_attribute("Search Tickets", "search-tkt", "st", 0, 0); |
| 2234 | @ <br /> |
| 2235 | onoff_attribute("Search Wiki", "search-wiki", "sw", 0, 0); |
| 2236 | @ <br /> |
| 2237 | onoff_attribute("Search Tech Notes", "search-technote", "se", 0, 0); |
| 2238 | @ <br /> |
| 2239 | onoff_attribute("Search Forum", "search-forum", "sf", 0, 0); |
| 2240 | @ <hr /> |
| 2241 | @ <p><input type="submit" name="submit" value="Apply Changes" /></p> |
| 2242 | @ <hr /> |
| 2243 | if( P("fts0") ){ |
| 2244 | search_drop_index(); |
| 2245 |
+20
| --- src/shell.c | ||
| +++ src/shell.c | ||
| @@ -95,16 +95,19 @@ | ||
| 95 | 95 | # endif |
| 96 | 96 | #endif |
| 97 | 97 | #if (!defined(_WIN32) && !defined(WIN32)) || defined(__MINGW32__) |
| 98 | 98 | # include <unistd.h> |
| 99 | 99 | # include <dirent.h> |
| 100 | +# define GETPID getpid | |
| 100 | 101 | # if defined(__MINGW32__) |
| 101 | 102 | # define DIRENT dirent |
| 102 | 103 | # ifndef S_ISLNK |
| 103 | 104 | # define S_ISLNK(mode) (0) |
| 104 | 105 | # endif |
| 105 | 106 | # endif |
| 107 | +#else | |
| 108 | +# define GETPID (int)GetCurrentProcessId | |
| 106 | 109 | #endif |
| 107 | 110 | #include <sys/types.h> |
| 108 | 111 | #include <sys/stat.h> |
| 109 | 112 | |
| 110 | 113 | #if HAVE_READLINE |
| @@ -15847,10 +15850,27 @@ | ||
| 15847 | 15850 | |
| 15848 | 15851 | setBinaryMode(stdin, 0); |
| 15849 | 15852 | setvbuf(stderr, 0, _IONBF, 0); /* Make sure stderr is unbuffered */ |
| 15850 | 15853 | stdin_is_interactive = isatty(0); |
| 15851 | 15854 | stdout_is_console = isatty(1); |
| 15855 | + | |
| 15856 | +#if !defined(_WIN32_WCE) | |
| 15857 | + if( getenv("SQLITE_DEBUG_BREAK") ){ | |
| 15858 | + if( isatty(0) && isatty(2) ){ | |
| 15859 | + fprintf(stderr, | |
| 15860 | + "attach debugger to process %d and press any key to continue.\n", | |
| 15861 | + GETPID()); | |
| 15862 | + fgetc(stdin); | |
| 15863 | + }else{ | |
| 15864 | +#if defined(_WIN32) || defined(WIN32) | |
| 15865 | + DebugBreak(); | |
| 15866 | +#elif defined(SIGTRAP) | |
| 15867 | + raise(SIGTRAP); | |
| 15868 | +#endif | |
| 15869 | + } | |
| 15870 | + } | |
| 15871 | +#endif | |
| 15852 | 15872 | |
| 15853 | 15873 | #if USE_SYSTEM_SQLITE+0!=1 |
| 15854 | 15874 | if( strncmp(sqlite3_sourceid(),SQLITE_SOURCE_ID,60)!=0 ){ |
| 15855 | 15875 | utf8_printf(stderr, "SQLite header and source version mismatch\n%s\n%s\n", |
| 15856 | 15876 | sqlite3_sourceid(), SQLITE_SOURCE_ID); |
| 15857 | 15877 |
| --- src/shell.c | |
| +++ src/shell.c | |
| @@ -95,16 +95,19 @@ | |
| 95 | # endif |
| 96 | #endif |
| 97 | #if (!defined(_WIN32) && !defined(WIN32)) || defined(__MINGW32__) |
| 98 | # include <unistd.h> |
| 99 | # include <dirent.h> |
| 100 | # if defined(__MINGW32__) |
| 101 | # define DIRENT dirent |
| 102 | # ifndef S_ISLNK |
| 103 | # define S_ISLNK(mode) (0) |
| 104 | # endif |
| 105 | # endif |
| 106 | #endif |
| 107 | #include <sys/types.h> |
| 108 | #include <sys/stat.h> |
| 109 | |
| 110 | #if HAVE_READLINE |
| @@ -15847,10 +15850,27 @@ | |
| 15847 | |
| 15848 | setBinaryMode(stdin, 0); |
| 15849 | setvbuf(stderr, 0, _IONBF, 0); /* Make sure stderr is unbuffered */ |
| 15850 | stdin_is_interactive = isatty(0); |
| 15851 | stdout_is_console = isatty(1); |
| 15852 | |
| 15853 | #if USE_SYSTEM_SQLITE+0!=1 |
| 15854 | if( strncmp(sqlite3_sourceid(),SQLITE_SOURCE_ID,60)!=0 ){ |
| 15855 | utf8_printf(stderr, "SQLite header and source version mismatch\n%s\n%s\n", |
| 15856 | sqlite3_sourceid(), SQLITE_SOURCE_ID); |
| 15857 |
| --- src/shell.c | |
| +++ src/shell.c | |
| @@ -95,16 +95,19 @@ | |
| 95 | # endif |
| 96 | #endif |
| 97 | #if (!defined(_WIN32) && !defined(WIN32)) || defined(__MINGW32__) |
| 98 | # include <unistd.h> |
| 99 | # include <dirent.h> |
| 100 | # define GETPID getpid |
| 101 | # if defined(__MINGW32__) |
| 102 | # define DIRENT dirent |
| 103 | # ifndef S_ISLNK |
| 104 | # define S_ISLNK(mode) (0) |
| 105 | # endif |
| 106 | # endif |
| 107 | #else |
| 108 | # define GETPID (int)GetCurrentProcessId |
| 109 | #endif |
| 110 | #include <sys/types.h> |
| 111 | #include <sys/stat.h> |
| 112 | |
| 113 | #if HAVE_READLINE |
| @@ -15847,10 +15850,27 @@ | |
| 15850 | |
| 15851 | setBinaryMode(stdin, 0); |
| 15852 | setvbuf(stderr, 0, _IONBF, 0); /* Make sure stderr is unbuffered */ |
| 15853 | stdin_is_interactive = isatty(0); |
| 15854 | stdout_is_console = isatty(1); |
| 15855 | |
| 15856 | #if !defined(_WIN32_WCE) |
| 15857 | if( getenv("SQLITE_DEBUG_BREAK") ){ |
| 15858 | if( isatty(0) && isatty(2) ){ |
| 15859 | fprintf(stderr, |
| 15860 | "attach debugger to process %d and press any key to continue.\n", |
| 15861 | GETPID()); |
| 15862 | fgetc(stdin); |
| 15863 | }else{ |
| 15864 | #if defined(_WIN32) || defined(WIN32) |
| 15865 | DebugBreak(); |
| 15866 | #elif defined(SIGTRAP) |
| 15867 | raise(SIGTRAP); |
| 15868 | #endif |
| 15869 | } |
| 15870 | } |
| 15871 | #endif |
| 15872 | |
| 15873 | #if USE_SYSTEM_SQLITE+0!=1 |
| 15874 | if( strncmp(sqlite3_sourceid(),SQLITE_SOURCE_ID,60)!=0 ){ |
| 15875 | utf8_printf(stderr, "SQLite header and source version mismatch\n%s\n%s\n", |
| 15876 | sqlite3_sourceid(), SQLITE_SOURCE_ID); |
| 15877 |
+1
-1
| --- src/skins.c | ||
| +++ src/skins.c | ||
| @@ -254,11 +254,11 @@ | ||
| 254 | 254 | */ |
| 255 | 255 | const char *skin_detail(const char *zName){ |
| 256 | 256 | struct SkinDetail *pDetail; |
| 257 | 257 | skin_detail_initialize(); |
| 258 | 258 | pDetail = skin_detail_find(zName); |
| 259 | - if( pDetail==0 ) fossil_panic("no such skin detail: %s", zName); | |
| 259 | + if( pDetail==0 ) fossil_fatal("no such skin detail: %s", zName); | |
| 260 | 260 | return pDetail->zValue; |
| 261 | 261 | } |
| 262 | 262 | int skin_detail_boolean(const char *zName){ |
| 263 | 263 | return !is_false(skin_detail(zName)); |
| 264 | 264 | } |
| 265 | 265 |
| --- src/skins.c | |
| +++ src/skins.c | |
| @@ -254,11 +254,11 @@ | |
| 254 | */ |
| 255 | const char *skin_detail(const char *zName){ |
| 256 | struct SkinDetail *pDetail; |
| 257 | skin_detail_initialize(); |
| 258 | pDetail = skin_detail_find(zName); |
| 259 | if( pDetail==0 ) fossil_panic("no such skin detail: %s", zName); |
| 260 | return pDetail->zValue; |
| 261 | } |
| 262 | int skin_detail_boolean(const char *zName){ |
| 263 | return !is_false(skin_detail(zName)); |
| 264 | } |
| 265 |
| --- src/skins.c | |
| +++ src/skins.c | |
| @@ -254,11 +254,11 @@ | |
| 254 | */ |
| 255 | const char *skin_detail(const char *zName){ |
| 256 | struct SkinDetail *pDetail; |
| 257 | skin_detail_initialize(); |
| 258 | pDetail = skin_detail_find(zName); |
| 259 | if( pDetail==0 ) fossil_fatal("no such skin detail: %s", zName); |
| 260 | return pDetail->zValue; |
| 261 | } |
| 262 | int skin_detail_boolean(const char *zName){ |
| 263 | return !is_false(skin_detail(zName)); |
| 264 | } |
| 265 |
+2
-2
| --- src/smtp.c | ||
| +++ src/smtp.c | ||
| @@ -1271,11 +1271,11 @@ | ||
| 1271 | 1271 | smtp_server_send(&x, "250 ok\r\n"); |
| 1272 | 1272 | }else |
| 1273 | 1273 | if( strncmp(z, "MAIL FROM:<", 11)==0 ){ |
| 1274 | 1274 | smtp_server_route_incoming(&x, 0); |
| 1275 | 1275 | smtp_server_clear(&x, SMTPSRV_CLEAR_MSG); |
| 1276 | - x.zFrom = email_copy_addr(z+11); | |
| 1276 | + x.zFrom = email_copy_addr(z+11,'>'); | |
| 1277 | 1277 | if( x.zFrom==0 ){ |
| 1278 | 1278 | smtp_server_send(&x, "500 unacceptable email address\r\n"); |
| 1279 | 1279 | }else{ |
| 1280 | 1280 | smtp_server_send(&x, "250 ok\r\n"); |
| 1281 | 1281 | } |
| @@ -1284,11 +1284,11 @@ | ||
| 1284 | 1284 | char *zAddr; |
| 1285 | 1285 | if( x.zFrom==0 ){ |
| 1286 | 1286 | smtp_server_send(&x, "500 missing MAIL FROM\r\n"); |
| 1287 | 1287 | continue; |
| 1288 | 1288 | } |
| 1289 | - zAddr = email_copy_addr(z+9); | |
| 1289 | + zAddr = email_copy_addr(z+9, '>'); | |
| 1290 | 1290 | if( zAddr==0 ){ |
| 1291 | 1291 | smtp_server_send(&x, "505 no such user\r\n"); |
| 1292 | 1292 | continue; |
| 1293 | 1293 | } |
| 1294 | 1294 | smtp_append_to(&x, zAddr, 0); |
| 1295 | 1295 |
| --- src/smtp.c | |
| +++ src/smtp.c | |
| @@ -1271,11 +1271,11 @@ | |
| 1271 | smtp_server_send(&x, "250 ok\r\n"); |
| 1272 | }else |
| 1273 | if( strncmp(z, "MAIL FROM:<", 11)==0 ){ |
| 1274 | smtp_server_route_incoming(&x, 0); |
| 1275 | smtp_server_clear(&x, SMTPSRV_CLEAR_MSG); |
| 1276 | x.zFrom = email_copy_addr(z+11); |
| 1277 | if( x.zFrom==0 ){ |
| 1278 | smtp_server_send(&x, "500 unacceptable email address\r\n"); |
| 1279 | }else{ |
| 1280 | smtp_server_send(&x, "250 ok\r\n"); |
| 1281 | } |
| @@ -1284,11 +1284,11 @@ | |
| 1284 | char *zAddr; |
| 1285 | if( x.zFrom==0 ){ |
| 1286 | smtp_server_send(&x, "500 missing MAIL FROM\r\n"); |
| 1287 | continue; |
| 1288 | } |
| 1289 | zAddr = email_copy_addr(z+9); |
| 1290 | if( zAddr==0 ){ |
| 1291 | smtp_server_send(&x, "505 no such user\r\n"); |
| 1292 | continue; |
| 1293 | } |
| 1294 | smtp_append_to(&x, zAddr, 0); |
| 1295 |
| --- src/smtp.c | |
| +++ src/smtp.c | |
| @@ -1271,11 +1271,11 @@ | |
| 1271 | smtp_server_send(&x, "250 ok\r\n"); |
| 1272 | }else |
| 1273 | if( strncmp(z, "MAIL FROM:<", 11)==0 ){ |
| 1274 | smtp_server_route_incoming(&x, 0); |
| 1275 | smtp_server_clear(&x, SMTPSRV_CLEAR_MSG); |
| 1276 | x.zFrom = email_copy_addr(z+11,'>'); |
| 1277 | if( x.zFrom==0 ){ |
| 1278 | smtp_server_send(&x, "500 unacceptable email address\r\n"); |
| 1279 | }else{ |
| 1280 | smtp_server_send(&x, "250 ok\r\n"); |
| 1281 | } |
| @@ -1284,11 +1284,11 @@ | |
| 1284 | char *zAddr; |
| 1285 | if( x.zFrom==0 ){ |
| 1286 | smtp_server_send(&x, "500 missing MAIL FROM\r\n"); |
| 1287 | continue; |
| 1288 | } |
| 1289 | zAddr = email_copy_addr(z+9, '>'); |
| 1290 | if( zAddr==0 ){ |
| 1291 | smtp_server_send(&x, "505 no such user\r\n"); |
| 1292 | continue; |
| 1293 | } |
| 1294 | smtp_append_to(&x, zAddr, 0); |
| 1295 |
+142
-83
| --- src/sqlite3.c | ||
| +++ src/sqlite3.c | ||
| @@ -53,10 +53,16 @@ | ||
| 53 | 53 | /* These macros are provided to "stringify" the value of the define |
| 54 | 54 | ** for those options in which the value is meaningful. */ |
| 55 | 55 | #define CTIMEOPT_VAL_(opt) #opt |
| 56 | 56 | #define CTIMEOPT_VAL(opt) CTIMEOPT_VAL_(opt) |
| 57 | 57 | |
| 58 | +/* Like CTIMEOPT_VAL, but especially for SQLITE_DEFAULT_LOOKASIDE. This | |
| 59 | +** option requires a separate macro because legal values contain a single | |
| 60 | +** comma. e.g. (-DSQLITE_DEFAULT_LOOKASIDE="100,100") */ | |
| 61 | +#define CTIMEOPT_VAL2_(opt1,opt2) #opt1 "," #opt2 | |
| 62 | +#define CTIMEOPT_VAL2(opt) CTIMEOPT_VAL2_(opt) | |
| 63 | + | |
| 58 | 64 | /* |
| 59 | 65 | ** An array of names of all compile-time options. This array should |
| 60 | 66 | ** be sorted A-Z. |
| 61 | 67 | ** |
| 62 | 68 | ** This array looks large, but in a typical installation actually uses |
| @@ -136,11 +142,11 @@ | ||
| 136 | 142 | #endif |
| 137 | 143 | #ifdef SQLITE_DEFAULT_LOCKING_MODE |
| 138 | 144 | "DEFAULT_LOCKING_MODE=" CTIMEOPT_VAL(SQLITE_DEFAULT_LOCKING_MODE), |
| 139 | 145 | #endif |
| 140 | 146 | #ifdef SQLITE_DEFAULT_LOOKASIDE |
| 141 | - "DEFAULT_LOOKASIDE=" CTIMEOPT_VAL(SQLITE_DEFAULT_LOOKASIDE), | |
| 147 | + "DEFAULT_LOOKASIDE=" CTIMEOPT_VAL2(SQLITE_DEFAULT_LOOKASIDE), | |
| 142 | 148 | #endif |
| 143 | 149 | #if SQLITE_DEFAULT_MEMSTATUS |
| 144 | 150 | "DEFAULT_MEMSTATUS", |
| 145 | 151 | #endif |
| 146 | 152 | #ifdef SQLITE_DEFAULT_MMAP_SIZE |
| @@ -1150,11 +1156,11 @@ | ||
| 1150 | 1156 | ** [sqlite3_libversion_number()], [sqlite3_sourceid()], |
| 1151 | 1157 | ** [sqlite_version()] and [sqlite_source_id()]. |
| 1152 | 1158 | */ |
| 1153 | 1159 | #define SQLITE_VERSION "3.25.0" |
| 1154 | 1160 | #define SQLITE_VERSION_NUMBER 3025000 |
| 1155 | -#define SQLITE_SOURCE_ID "2018-07-18 19:09:07 a5087c5c87ad65f92e3bc96bbc84afb43faf10ab6b9ed3ba16304b5c60ad069f" | |
| 1161 | +#define SQLITE_SOURCE_ID "2018-07-24 22:02:12 2bd593332da0aade467e7a4ee89e966aa6302f37540a2c5e23671f98a6cb599c" | |
| 1156 | 1162 | |
| 1157 | 1163 | /* |
| 1158 | 1164 | ** CAPI3REF: Run-Time Library Version Numbers |
| 1159 | 1165 | ** KEYWORDS: sqlite3_version sqlite3_sourceid |
| 1160 | 1166 | ** |
| @@ -1912,11 +1918,12 @@ | ||
| 1912 | 1918 | ** interrogated. The zDbName parameter is ignored. |
| 1913 | 1919 | ** |
| 1914 | 1920 | ** <li>[[SQLITE_FCNTL_PERSIST_WAL]] |
| 1915 | 1921 | ** ^The [SQLITE_FCNTL_PERSIST_WAL] opcode is used to set or query the |
| 1916 | 1922 | ** persistent [WAL | Write Ahead Log] setting. By default, the auxiliary |
| 1917 | -** write ahead log and shared memory files used for transaction control | |
| 1923 | +** write ahead log ([WAL file]) and shared memory | |
| 1924 | +** files used for transaction control | |
| 1918 | 1925 | ** are automatically deleted when the latest connection to the database |
| 1919 | 1926 | ** closes. Setting persistent WAL mode causes those files to persist after |
| 1920 | 1927 | ** close. Persisting the files is useful when other processes that do not |
| 1921 | 1928 | ** have write permission on the directory containing the database file want |
| 1922 | 1929 | ** to read the database file, as the WAL and shared memory files must exist |
| @@ -9985,11 +9992,10 @@ | ||
| 9985 | 9992 | SQLITE_API int sqlite3_system_errno(sqlite3*); |
| 9986 | 9993 | |
| 9987 | 9994 | /* |
| 9988 | 9995 | ** CAPI3REF: Database Snapshot |
| 9989 | 9996 | ** KEYWORDS: {snapshot} {sqlite3_snapshot} |
| 9990 | -** EXPERIMENTAL | |
| 9991 | 9997 | ** |
| 9992 | 9998 | ** An instance of the snapshot object records the state of a [WAL mode] |
| 9993 | 9999 | ** database for some specific point in history. |
| 9994 | 10000 | ** |
| 9995 | 10001 | ** In [WAL mode], multiple [database connections] that are open on the |
| @@ -10002,23 +10008,18 @@ | ||
| 10002 | 10008 | ** |
| 10003 | 10009 | ** The sqlite3_snapshot object records state information about an historical |
| 10004 | 10010 | ** version of the database file so that it is possible to later open a new read |
| 10005 | 10011 | ** transaction that sees that historical version of the database rather than |
| 10006 | 10012 | ** the most recent version. |
| 10007 | -** | |
| 10008 | -** The constructor for this object is [sqlite3_snapshot_get()]. The | |
| 10009 | -** [sqlite3_snapshot_open()] method causes a fresh read transaction to refer | |
| 10010 | -** to an historical snapshot (if possible). The destructor for | |
| 10011 | -** sqlite3_snapshot objects is [sqlite3_snapshot_free()]. | |
| 10012 | 10013 | */ |
| 10013 | 10014 | typedef struct sqlite3_snapshot { |
| 10014 | 10015 | unsigned char hidden[48]; |
| 10015 | 10016 | } sqlite3_snapshot; |
| 10016 | 10017 | |
| 10017 | 10018 | /* |
| 10018 | 10019 | ** CAPI3REF: Record A Database Snapshot |
| 10019 | -** EXPERIMENTAL | |
| 10020 | +** CONSTRUCTOR: sqlite3_snapshot | |
| 10020 | 10021 | ** |
| 10021 | 10022 | ** ^The [sqlite3_snapshot_get(D,S,P)] interface attempts to make a |
| 10022 | 10023 | ** new [sqlite3_snapshot] object that records the current state of |
| 10023 | 10024 | ** schema S in database connection D. ^On success, the |
| 10024 | 10025 | ** [sqlite3_snapshot_get(D,S,P)] interface writes a pointer to the newly |
| @@ -10053,21 +10054,21 @@ | ||
| 10053 | 10054 | ** The [sqlite3_snapshot] object returned from a successful call to |
| 10054 | 10055 | ** [sqlite3_snapshot_get()] must be freed using [sqlite3_snapshot_free()] |
| 10055 | 10056 | ** to avoid a memory leak. |
| 10056 | 10057 | ** |
| 10057 | 10058 | ** The [sqlite3_snapshot_get()] interface is only available when the |
| 10058 | -** SQLITE_ENABLE_SNAPSHOT compile-time option is used. | |
| 10059 | +** [SQLITE_ENABLE_SNAPSHOT] compile-time option is used. | |
| 10059 | 10060 | */ |
| 10060 | 10061 | SQLITE_API SQLITE_EXPERIMENTAL int sqlite3_snapshot_get( |
| 10061 | 10062 | sqlite3 *db, |
| 10062 | 10063 | const char *zSchema, |
| 10063 | 10064 | sqlite3_snapshot **ppSnapshot |
| 10064 | 10065 | ); |
| 10065 | 10066 | |
| 10066 | 10067 | /* |
| 10067 | 10068 | ** CAPI3REF: Start a read transaction on an historical snapshot |
| 10068 | -** EXPERIMENTAL | |
| 10069 | +** METHOD: sqlite3_snapshot | |
| 10069 | 10070 | ** |
| 10070 | 10071 | ** ^The [sqlite3_snapshot_open(D,S,P)] interface starts a |
| 10071 | 10072 | ** read transaction for schema S of |
| 10072 | 10073 | ** [database connection] D such that the read transaction |
| 10073 | 10074 | ** refers to historical [snapshot] P, rather than the most |
| @@ -10091,34 +10092,34 @@ | ||
| 10091 | 10092 | ** after the most recent I/O on the database connection.)^ |
| 10092 | 10093 | ** (Hint: Run "[PRAGMA application_id]" against a newly opened |
| 10093 | 10094 | ** database connection in order to make it ready to use snapshots.) |
| 10094 | 10095 | ** |
| 10095 | 10096 | ** The [sqlite3_snapshot_open()] interface is only available when the |
| 10096 | -** SQLITE_ENABLE_SNAPSHOT compile-time option is used. | |
| 10097 | +** [SQLITE_ENABLE_SNAPSHOT] compile-time option is used. | |
| 10097 | 10098 | */ |
| 10098 | 10099 | SQLITE_API SQLITE_EXPERIMENTAL int sqlite3_snapshot_open( |
| 10099 | 10100 | sqlite3 *db, |
| 10100 | 10101 | const char *zSchema, |
| 10101 | 10102 | sqlite3_snapshot *pSnapshot |
| 10102 | 10103 | ); |
| 10103 | 10104 | |
| 10104 | 10105 | /* |
| 10105 | 10106 | ** CAPI3REF: Destroy a snapshot |
| 10106 | -** EXPERIMENTAL | |
| 10107 | +** DESTRUCTOR: sqlite3_snapshot | |
| 10107 | 10108 | ** |
| 10108 | 10109 | ** ^The [sqlite3_snapshot_free(P)] interface destroys [sqlite3_snapshot] P. |
| 10109 | 10110 | ** The application must eventually free every [sqlite3_snapshot] object |
| 10110 | 10111 | ** using this routine to avoid a memory leak. |
| 10111 | 10112 | ** |
| 10112 | 10113 | ** The [sqlite3_snapshot_free()] interface is only available when the |
| 10113 | -** SQLITE_ENABLE_SNAPSHOT compile-time option is used. | |
| 10114 | +** [SQLITE_ENABLE_SNAPSHOT] compile-time option is used. | |
| 10114 | 10115 | */ |
| 10115 | 10116 | SQLITE_API SQLITE_EXPERIMENTAL void sqlite3_snapshot_free(sqlite3_snapshot*); |
| 10116 | 10117 | |
| 10117 | 10118 | /* |
| 10118 | 10119 | ** CAPI3REF: Compare the ages of two snapshot handles. |
| 10119 | -** EXPERIMENTAL | |
| 10120 | +** METHOD: sqlite3_snapshot | |
| 10120 | 10121 | ** |
| 10121 | 10122 | ** The sqlite3_snapshot_cmp(P1, P2) interface is used to compare the ages |
| 10122 | 10123 | ** of two valid snapshot handles. |
| 10123 | 10124 | ** |
| 10124 | 10125 | ** If the two snapshot handles are not associated with the same database |
| @@ -10133,35 +10134,41 @@ | ||
| 10133 | 10134 | ** is undefined. |
| 10134 | 10135 | ** |
| 10135 | 10136 | ** Otherwise, this API returns a negative value if P1 refers to an older |
| 10136 | 10137 | ** snapshot than P2, zero if the two handles refer to the same database |
| 10137 | 10138 | ** snapshot, and a positive value if P1 is a newer snapshot than P2. |
| 10139 | +** | |
| 10140 | +** This interface is only available if SQLite is compiled with the | |
| 10141 | +** [SQLITE_ENABLE_SNAPSHOT] option. | |
| 10138 | 10142 | */ |
| 10139 | 10143 | SQLITE_API SQLITE_EXPERIMENTAL int sqlite3_snapshot_cmp( |
| 10140 | 10144 | sqlite3_snapshot *p1, |
| 10141 | 10145 | sqlite3_snapshot *p2 |
| 10142 | 10146 | ); |
| 10143 | 10147 | |
| 10144 | 10148 | /* |
| 10145 | 10149 | ** CAPI3REF: Recover snapshots from a wal file |
| 10146 | -** EXPERIMENTAL | |
| 10147 | -** | |
| 10148 | -** If all connections disconnect from a database file but do not perform | |
| 10149 | -** a checkpoint, the existing wal file is opened along with the database | |
| 10150 | -** file the next time the database is opened. At this point it is only | |
| 10151 | -** possible to successfully call sqlite3_snapshot_open() to open the most | |
| 10152 | -** recent snapshot of the database (the one at the head of the wal file), | |
| 10153 | -** even though the wal file may contain other valid snapshots for which | |
| 10154 | -** clients have sqlite3_snapshot handles. | |
| 10155 | -** | |
| 10156 | -** This function attempts to scan the wal file associated with database zDb | |
| 10150 | +** METHOD: sqlite3_snapshot | |
| 10151 | +** | |
| 10152 | +** If a [WAL file] remains on disk after all database connections close | |
| 10153 | +** (either through the use of the [SQLITE_FCNTL_PERSIST_WAL] [file control] | |
| 10154 | +** or because the last process to have the database opened exited without | |
| 10155 | +** calling [sqlite3_close()]) and a new connection is subsequently opened | |
| 10156 | +** on that database and [WAL file], the [sqlite3_snapshot_open()] interface | |
| 10157 | +** will only be able to open the last transaction added to the WAL file | |
| 10158 | +** even though the WAL file contains other valid transactions. | |
| 10159 | +** | |
| 10160 | +** This function attempts to scan the WAL file associated with database zDb | |
| 10157 | 10161 | ** of database handle db and make all valid snapshots available to |
| 10158 | 10162 | ** sqlite3_snapshot_open(). It is an error if there is already a read |
| 10159 | -** transaction open on the database, or if the database is not a wal mode | |
| 10163 | +** transaction open on the database, or if the database is not a WAL mode | |
| 10160 | 10164 | ** database. |
| 10161 | 10165 | ** |
| 10162 | 10166 | ** SQLITE_OK is returned if successful, or an SQLite error code otherwise. |
| 10167 | +** | |
| 10168 | +** This interface is only available if SQLite is compiled with the | |
| 10169 | +** [SQLITE_ENABLE_SNAPSHOT] option. | |
| 10163 | 10170 | */ |
| 10164 | 10171 | SQLITE_API SQLITE_EXPERIMENTAL int sqlite3_snapshot_recover(sqlite3 *db, const char *zDb); |
| 10165 | 10172 | |
| 10166 | 10173 | /* |
| 10167 | 10174 | ** CAPI3REF: Serialize a database |
| @@ -18937,11 +18944,11 @@ | ||
| 18937 | 18944 | SQLITE_PRIVATE void sqlite3Reindex(Parse*, Token*, Token*); |
| 18938 | 18945 | SQLITE_PRIVATE void sqlite3AlterFunctions(void); |
| 18939 | 18946 | SQLITE_PRIVATE void sqlite3AlterRenameTable(Parse*, SrcList*, Token*); |
| 18940 | 18947 | SQLITE_PRIVATE int sqlite3GetToken(const unsigned char *, int *); |
| 18941 | 18948 | SQLITE_PRIVATE void sqlite3NestedParse(Parse*, const char*, ...); |
| 18942 | -SQLITE_PRIVATE void sqlite3ExpirePreparedStatements(sqlite3*); | |
| 18949 | +SQLITE_PRIVATE void sqlite3ExpirePreparedStatements(sqlite3*, int); | |
| 18943 | 18950 | SQLITE_PRIVATE int sqlite3CodeSubselect(Parse*, Expr *, int, int); |
| 18944 | 18951 | SQLITE_PRIVATE void sqlite3SelectPrep(Parse*, Select*, NameContext*); |
| 18945 | 18952 | SQLITE_PRIVATE void sqlite3SelectWrongNumTermsError(Parse *pParse, Select *p); |
| 18946 | 18953 | SQLITE_PRIVATE int sqlite3MatchSpanName(const char*, const char*, const char*, const char*); |
| 18947 | 18954 | SQLITE_PRIVATE int sqlite3ResolveExprNames(NameContext*, Expr*); |
| @@ -20025,13 +20032,13 @@ | ||
| 20025 | 20032 | #endif |
| 20026 | 20033 | u16 nResColumn; /* Number of columns in one row of the result set */ |
| 20027 | 20034 | u8 errorAction; /* Recovery action to do in case of an error */ |
| 20028 | 20035 | u8 minWriteFileFormat; /* Minimum file format for writable database files */ |
| 20029 | 20036 | u8 prepFlags; /* SQLITE_PREPARE_* flags */ |
| 20030 | - bft expired:1; /* True if the VM needs to be recompiled */ | |
| 20031 | - bft doingRerun:1; /* True if rerunning after an auto-reprepare */ | |
| 20037 | + bft expired:2; /* 1: recompile VM immediately 2: when convenient */ | |
| 20032 | 20038 | bft explain:2; /* True if EXPLAIN present on SQL command */ |
| 20039 | + bft doingRerun:1; /* True if rerunning after an auto-reprepare */ | |
| 20033 | 20040 | bft changeCntOn:1; /* True to update the change-counter */ |
| 20034 | 20041 | bft runOnlyOnce:1; /* Automatically expire on reset */ |
| 20035 | 20042 | bft usesStmtJournal:1; /* True if uses a statement journal */ |
| 20036 | 20043 | bft readOnly:1; /* True for statements that do not write */ |
| 20037 | 20044 | bft bIsReader:1; /* True for statements that read */ |
| @@ -28640,11 +28647,11 @@ | ||
| 28640 | 28647 | sqlite3TreeViewLine(pView, "FUNCTION %Q", pExpr->u.zToken); |
| 28641 | 28648 | } |
| 28642 | 28649 | if( pFarg ){ |
| 28643 | 28650 | sqlite3TreeViewExprList(pView, pFarg, pWin!=0, 0); |
| 28644 | 28651 | } |
| 28645 | -#ifndef SQLITe_OMIT_WINDOWFUNC | |
| 28652 | +#ifndef SQLITE_OMIT_WINDOWFUNC | |
| 28646 | 28653 | if( pWin ){ |
| 28647 | 28654 | sqlite3TreeViewWindow(pView, pWin, 0); |
| 28648 | 28655 | } |
| 28649 | 28656 | #endif |
| 28650 | 28657 | break; |
| @@ -42948,10 +42955,13 @@ | ||
| 42948 | 42955 | */ |
| 42949 | 42956 | static int winTruncate(sqlite3_file *id, sqlite3_int64 nByte){ |
| 42950 | 42957 | winFile *pFile = (winFile*)id; /* File handle object */ |
| 42951 | 42958 | int rc = SQLITE_OK; /* Return code for this function */ |
| 42952 | 42959 | DWORD lastErrno; |
| 42960 | +#if SQLITE_MAX_MMAP_SIZE>0 | |
| 42961 | + sqlite3_int64 oldMmapSize; | |
| 42962 | +#endif | |
| 42953 | 42963 | |
| 42954 | 42964 | assert( pFile ); |
| 42955 | 42965 | SimulateIOError(return SQLITE_IOERR_TRUNCATE); |
| 42956 | 42966 | OSTRACE(("TRUNCATE pid=%lu, pFile=%p, file=%p, size=%lld, lock=%d\n", |
| 42957 | 42967 | osGetCurrentProcessId(), pFile, pFile->h, nByte, pFile->locktype)); |
| @@ -42962,10 +42972,19 @@ | ||
| 42962 | 42972 | ** size). |
| 42963 | 42973 | */ |
| 42964 | 42974 | if( pFile->szChunk>0 ){ |
| 42965 | 42975 | nByte = ((nByte + pFile->szChunk - 1)/pFile->szChunk) * pFile->szChunk; |
| 42966 | 42976 | } |
| 42977 | + | |
| 42978 | +#if SQLITE_MAX_MMAP_SIZE>0 | |
| 42979 | + if( pFile->pMapRegion ){ | |
| 42980 | + oldMmapSize = pFile->mmapSize; | |
| 42981 | + }else{ | |
| 42982 | + oldMmapSize = 0; | |
| 42983 | + } | |
| 42984 | + winUnmapfile(pFile); | |
| 42985 | +#endif | |
| 42967 | 42986 | |
| 42968 | 42987 | /* SetEndOfFile() returns non-zero when successful, or zero when it fails. */ |
| 42969 | 42988 | if( winSeekFile(pFile, nByte) ){ |
| 42970 | 42989 | rc = winLogError(SQLITE_IOERR_TRUNCATE, pFile->lastErrno, |
| 42971 | 42990 | "winTruncate1", pFile->zPath); |
| @@ -42975,16 +42994,16 @@ | ||
| 42975 | 42994 | rc = winLogError(SQLITE_IOERR_TRUNCATE, pFile->lastErrno, |
| 42976 | 42995 | "winTruncate2", pFile->zPath); |
| 42977 | 42996 | } |
| 42978 | 42997 | |
| 42979 | 42998 | #if SQLITE_MAX_MMAP_SIZE>0 |
| 42980 | - /* If the file was truncated to a size smaller than the currently | |
| 42981 | - ** mapped region, reduce the effective mapping size as well. SQLite will | |
| 42982 | - ** use read() and write() to access data beyond this point from now on. | |
| 42983 | - */ | |
| 42984 | - if( pFile->pMapRegion && nByte<pFile->mmapSize ){ | |
| 42985 | - pFile->mmapSize = nByte; | |
| 42999 | + if( rc==SQLITE_OK && oldMmapSize>0 ){ | |
| 43000 | + if( oldMmapSize>nByte ){ | |
| 43001 | + winMapfile(pFile, -1); | |
| 43002 | + }else{ | |
| 43003 | + winMapfile(pFile, oldMmapSize); | |
| 43004 | + } | |
| 42986 | 43005 | } |
| 42987 | 43006 | #endif |
| 42988 | 43007 | |
| 42989 | 43008 | OSTRACE(("TRUNCATE pid=%lu, pFile=%p, file=%p, rc=%s\n", |
| 42990 | 43009 | osGetCurrentProcessId(), pFile, pFile->h, sqlite3ErrName(rc))); |
| @@ -56461,22 +56480,22 @@ | ||
| 56461 | 56480 | |
| 56462 | 56481 | if( (rc&0xFF)==SQLITE_IOERR && rc!=SQLITE_IOERR_NOMEM ){ |
| 56463 | 56482 | rc = sqlite3JournalCreate(pPager->jfd); |
| 56464 | 56483 | if( rc!=SQLITE_OK ){ |
| 56465 | 56484 | sqlite3OsClose(pPager->jfd); |
| 56485 | + goto commit_phase_one_exit; | |
| 56466 | 56486 | } |
| 56467 | 56487 | bBatch = 0; |
| 56468 | 56488 | }else{ |
| 56469 | 56489 | sqlite3OsClose(pPager->jfd); |
| 56470 | 56490 | } |
| 56471 | 56491 | } |
| 56472 | 56492 | #endif /* SQLITE_ENABLE_BATCH_ATOMIC_WRITE */ |
| 56473 | 56493 | |
| 56474 | - if( bBatch==0 && rc==SQLITE_OK ){ | |
| 56494 | + if( bBatch==0 ){ | |
| 56475 | 56495 | rc = pager_write_pagelist(pPager, pList); |
| 56476 | 56496 | } |
| 56477 | - | |
| 56478 | 56497 | if( rc!=SQLITE_OK ){ |
| 56479 | 56498 | assert( rc!=SQLITE_IOERR_BLOCKED ); |
| 56480 | 56499 | goto commit_phase_one_exit; |
| 56481 | 56500 | } |
| 56482 | 56501 | sqlite3PcacheCleanAll(pPager->pPCache); |
| @@ -65751,10 +65770,16 @@ | ||
| 65751 | 65770 | */ |
| 65752 | 65771 | if( p->inTrans==TRANS_WRITE || (p->inTrans==TRANS_READ && !wrflag) ){ |
| 65753 | 65772 | goto trans_begun; |
| 65754 | 65773 | } |
| 65755 | 65774 | assert( pBt->inTransaction==TRANS_WRITE || IfNotOmitAV(pBt->bDoTruncate)==0 ); |
| 65775 | + | |
| 65776 | + if( (p->db->flags & SQLITE_ResetDatabase) | |
| 65777 | + && sqlite3PagerIsreadonly(pBt->pPager)==0 | |
| 65778 | + ){ | |
| 65779 | + pBt->btsFlags &= ~BTS_READ_ONLY; | |
| 65780 | + } | |
| 65756 | 65781 | |
| 65757 | 65782 | /* Write transactions are not possible on a read-only database */ |
| 65758 | 65783 | if( (pBt->btsFlags & BTS_READ_ONLY)!=0 && wrflag ){ |
| 65759 | 65784 | rc = SQLITE_READONLY; |
| 65760 | 65785 | goto trans_begun; |
| @@ -71767,12 +71792,11 @@ | ||
| 71767 | 71792 | ** if this is the first reference to the page. |
| 71768 | 71793 | ** |
| 71769 | 71794 | ** Also check that the page number is in bounds. |
| 71770 | 71795 | */ |
| 71771 | 71796 | static int checkRef(IntegrityCk *pCheck, Pgno iPage){ |
| 71772 | - if( iPage==0 ) return 1; | |
| 71773 | - if( iPage>pCheck->nPage ){ | |
| 71797 | + if( iPage>pCheck->nPage || iPage==0 ){ | |
| 71774 | 71798 | checkAppendMsg(pCheck, "invalid page number %d", iPage); |
| 71775 | 71799 | return 1; |
| 71776 | 71800 | } |
| 71777 | 71801 | if( getPageReferenced(pCheck, iPage) ){ |
| 71778 | 71802 | checkAppendMsg(pCheck, "2nd reference to page %d", iPage); |
| @@ -71823,21 +71847,16 @@ | ||
| 71823 | 71847 | int iPage, /* Page number for first page in the list */ |
| 71824 | 71848 | int N /* Expected number of pages in the list */ |
| 71825 | 71849 | ){ |
| 71826 | 71850 | int i; |
| 71827 | 71851 | int expected = N; |
| 71828 | - int iFirst = iPage; | |
| 71829 | - while( N-- > 0 && pCheck->mxErr ){ | |
| 71852 | + int nErrAtStart = pCheck->nErr; | |
| 71853 | + while( iPage!=0 && pCheck->mxErr ){ | |
| 71830 | 71854 | DbPage *pOvflPage; |
| 71831 | 71855 | unsigned char *pOvflData; |
| 71832 | - if( iPage<1 ){ | |
| 71833 | - checkAppendMsg(pCheck, | |
| 71834 | - "%d of %d pages missing from overflow list starting at %d", | |
| 71835 | - N+1, expected, iFirst); | |
| 71836 | - break; | |
| 71837 | - } | |
| 71838 | 71856 | if( checkRef(pCheck, iPage) ) break; |
| 71857 | + N--; | |
| 71839 | 71858 | if( sqlite3PagerGet(pCheck->pPager, (Pgno)iPage, &pOvflPage, 0) ){ |
| 71840 | 71859 | checkAppendMsg(pCheck, "failed to get page %d", iPage); |
| 71841 | 71860 | break; |
| 71842 | 71861 | } |
| 71843 | 71862 | pOvflData = (unsigned char *)sqlite3PagerGetData(pOvflPage); |
| @@ -71877,14 +71896,16 @@ | ||
| 71877 | 71896 | } |
| 71878 | 71897 | } |
| 71879 | 71898 | #endif |
| 71880 | 71899 | iPage = get4byte(pOvflData); |
| 71881 | 71900 | sqlite3PagerUnref(pOvflPage); |
| 71882 | - | |
| 71883 | - if( isFreeList && N<(iPage!=0) ){ | |
| 71884 | - checkAppendMsg(pCheck, "free-page count in header is too small"); | |
| 71885 | - } | |
| 71901 | + } | |
| 71902 | + if( N && nErrAtStart==pCheck->nErr ){ | |
| 71903 | + checkAppendMsg(pCheck, | |
| 71904 | + "%s is %d but should be %d", | |
| 71905 | + isFreeList ? "size" : "overflow list length", | |
| 71906 | + expected-N, expected); | |
| 71886 | 71907 | } |
| 71887 | 71908 | } |
| 71888 | 71909 | #endif /* SQLITE_OMIT_INTEGRITY_CHECK */ |
| 71889 | 71910 | |
| 71890 | 71911 | /* |
| @@ -72274,10 +72295,28 @@ | ||
| 72274 | 72295 | get4byte(&pBt->pPage1->aData[36])); |
| 72275 | 72296 | sCheck.zPfx = 0; |
| 72276 | 72297 | |
| 72277 | 72298 | /* Check all the tables. |
| 72278 | 72299 | */ |
| 72300 | +#ifndef SQLITE_OMIT_AUTOVACUUM | |
| 72301 | + if( pBt->autoVacuum ){ | |
| 72302 | + int mx = 0; | |
| 72303 | + int mxInHdr; | |
| 72304 | + for(i=0; (int)i<nRoot; i++) if( mx<aRoot[i] ) mx = aRoot[i]; | |
| 72305 | + mxInHdr = get4byte(&pBt->pPage1->aData[52]); | |
| 72306 | + if( mx!=mxInHdr ){ | |
| 72307 | + checkAppendMsg(&sCheck, | |
| 72308 | + "max rootpage (%d) disagrees with header (%d)", | |
| 72309 | + mx, mxInHdr | |
| 72310 | + ); | |
| 72311 | + } | |
| 72312 | + }else if( get4byte(&pBt->pPage1->aData[64])!=0 ){ | |
| 72313 | + checkAppendMsg(&sCheck, | |
| 72314 | + "incremental_vacuum enabled with a max rootpage of zero" | |
| 72315 | + ); | |
| 72316 | + } | |
| 72317 | +#endif | |
| 72279 | 72318 | testcase( pBt->db->flags & SQLITE_CellSizeCk ); |
| 72280 | 72319 | pBt->db->flags &= ~SQLITE_CellSizeCk; |
| 72281 | 72320 | for(i=0; (int)i<nRoot && sCheck.mxErr; i++){ |
| 72282 | 72321 | i64 notUsed; |
| 72283 | 72322 | if( aRoot[i]==0 ) continue; |
| @@ -79974,15 +80013,23 @@ | ||
| 79974 | 80013 | ** An expired statement means that recompilation of the statement is |
| 79975 | 80014 | ** recommend. Statements expire when things happen that make their |
| 79976 | 80015 | ** programs obsolete. Removing user-defined functions or collating |
| 79977 | 80016 | ** sequences, or changing an authorization function are the types of |
| 79978 | 80017 | ** things that make prepared statements obsolete. |
| 80018 | +** | |
| 80019 | +** If iCode is 1, then expiration is advisory. The statement should | |
| 80020 | +** be reprepared before being restarted, but if it is already running | |
| 80021 | +** it is allowed to run to completion. | |
| 80022 | +** | |
| 80023 | +** Internally, this function just sets the Vdbe.expired flag on all | |
| 80024 | +** prepared statements. The flag is set to 1 for an immediate expiration | |
| 80025 | +** and set to 2 for an advisory expiration. | |
| 79979 | 80026 | */ |
| 79980 | -SQLITE_PRIVATE void sqlite3ExpirePreparedStatements(sqlite3 *db){ | |
| 80027 | +SQLITE_PRIVATE void sqlite3ExpirePreparedStatements(sqlite3 *db, int iCode){ | |
| 79981 | 80028 | Vdbe *p; |
| 79982 | 80029 | for(p = db->pVdbe; p; p=p->pNext){ |
| 79983 | - p->expired = 1; | |
| 80030 | + p->expired = iCode+1; | |
| 79984 | 80031 | } |
| 79985 | 80032 | } |
| 79986 | 80033 | |
| 79987 | 80034 | /* |
| 79988 | 80035 | ** Return the database associated with the Vdbe. |
| @@ -85520,11 +85567,11 @@ | ||
| 85520 | 85567 | if( rc!=SQLITE_OK ){ |
| 85521 | 85568 | goto abort_due_to_error; |
| 85522 | 85569 | } |
| 85523 | 85570 | } |
| 85524 | 85571 | if( isSchemaChange ){ |
| 85525 | - sqlite3ExpirePreparedStatements(db); | |
| 85572 | + sqlite3ExpirePreparedStatements(db, 0); | |
| 85526 | 85573 | sqlite3ResetAllSchemasOfConnection(db); |
| 85527 | 85574 | db->mDbFlags |= DBFLAG_SchemaChange; |
| 85528 | 85575 | } |
| 85529 | 85576 | } |
| 85530 | 85577 | |
| @@ -85809,11 +85856,11 @@ | ||
| 85809 | 85856 | pDb->pSchema->file_format = pOp->p3; |
| 85810 | 85857 | } |
| 85811 | 85858 | if( pOp->p1==1 ){ |
| 85812 | 85859 | /* Invalidate all prepared statements whenever the TEMP database |
| 85813 | 85860 | ** schema is changed. Ticket #1644 */ |
| 85814 | - sqlite3ExpirePreparedStatements(db); | |
| 85861 | + sqlite3ExpirePreparedStatements(db, 0); | |
| 85815 | 85862 | p->expired = 0; |
| 85816 | 85863 | } |
| 85817 | 85864 | if( rc ) goto abort_due_to_error; |
| 85818 | 85865 | break; |
| 85819 | 85866 | } |
| @@ -85927,11 +85974,11 @@ | ||
| 85927 | 85974 | assert( pOp->opcode==OP_OpenWrite || pOp->p5==0 || pOp->p5==OPFLAG_SEEKEQ ); |
| 85928 | 85975 | assert( p->bIsReader ); |
| 85929 | 85976 | assert( pOp->opcode==OP_OpenRead || pOp->opcode==OP_ReopenIdx |
| 85930 | 85977 | || p->readOnly==0 ); |
| 85931 | 85978 | |
| 85932 | - if( p->expired ){ | |
| 85979 | + if( p->expired==1 ){ | |
| 85933 | 85980 | rc = SQLITE_ABORT_ROLLBACK; |
| 85934 | 85981 | goto abort_due_to_error; |
| 85935 | 85982 | } |
| 85936 | 85983 | |
| 85937 | 85984 | nField = 0; |
| @@ -89108,25 +89155,32 @@ | ||
| 89108 | 89155 | } |
| 89109 | 89156 | break; |
| 89110 | 89157 | } |
| 89111 | 89158 | #endif |
| 89112 | 89159 | |
| 89113 | -/* Opcode: Expire P1 * * * * | |
| 89160 | +/* Opcode: Expire P1 P2 * * * | |
| 89114 | 89161 | ** |
| 89115 | 89162 | ** Cause precompiled statements to expire. When an expired statement |
| 89116 | 89163 | ** is executed using sqlite3_step() it will either automatically |
| 89117 | 89164 | ** reprepare itself (if it was originally created using sqlite3_prepare_v2()) |
| 89118 | 89165 | ** or it will fail with SQLITE_SCHEMA. |
| 89119 | 89166 | ** |
| 89120 | 89167 | ** If P1 is 0, then all SQL statements become expired. If P1 is non-zero, |
| 89121 | 89168 | ** then only the currently executing statement is expired. |
| 89169 | +** | |
| 89170 | +** If P2 is 0, then SQL statements are expired immediately. If P2 is 1, | |
| 89171 | +** then running SQL statements are allowed to continue to run to completion. | |
| 89172 | +** The P2==1 case occurs when a CREATE INDEX or similar schema change happens | |
| 89173 | +** that might help the statement run faster but which does not affect the | |
| 89174 | +** correctness of operation. | |
| 89122 | 89175 | */ |
| 89123 | 89176 | case OP_Expire: { |
| 89177 | + assert( pOp->p2==0 || pOp->p2==1 ); | |
| 89124 | 89178 | if( !pOp->p1 ){ |
| 89125 | - sqlite3ExpirePreparedStatements(db); | |
| 89179 | + sqlite3ExpirePreparedStatements(db, pOp->p2); | |
| 89126 | 89180 | }else{ |
| 89127 | - p->expired = 1; | |
| 89181 | + p->expired = pOp->p2+1; | |
| 89128 | 89182 | } |
| 89129 | 89183 | break; |
| 89130 | 89184 | } |
| 89131 | 89185 | |
| 89132 | 89186 | #ifndef SQLITE_OMIT_SHARED_CACHE |
| @@ -103813,11 +103867,11 @@ | ||
| 103813 | 103867 | if( !pIdx->hasStat1 ) sqlite3DefaultRowEst(pIdx); |
| 103814 | 103868 | } |
| 103815 | 103869 | |
| 103816 | 103870 | /* Load the statistics from the sqlite_stat4 table. */ |
| 103817 | 103871 | #ifdef SQLITE_ENABLE_STAT3_OR_STAT4 |
| 103818 | - if( rc==SQLITE_OK && OptimizationEnabled(db, SQLITE_Stat34) ){ | |
| 103872 | + if( rc==SQLITE_OK ){ | |
| 103819 | 103873 | db->lookaside.bDisable++; |
| 103820 | 103874 | rc = loadStat4(db, sInfo.zDatabase); |
| 103821 | 103875 | db->lookaside.bDisable--; |
| 103822 | 103876 | } |
| 103823 | 103877 | for(i=sqliteHashFirst(&pSchema->idxHash); i; i=sqliteHashNext(i)){ |
| @@ -104545,11 +104599,11 @@ | ||
| 104545 | 104599 | if( !sqlite3SafetyCheckOk(db) ) return SQLITE_MISUSE_BKPT; |
| 104546 | 104600 | #endif |
| 104547 | 104601 | sqlite3_mutex_enter(db->mutex); |
| 104548 | 104602 | db->xAuth = (sqlite3_xauth)xAuth; |
| 104549 | 104603 | db->pAuthArg = pArg; |
| 104550 | - sqlite3ExpirePreparedStatements(db); | |
| 104604 | + sqlite3ExpirePreparedStatements(db, 0); | |
| 104551 | 104605 | sqlite3_mutex_leave(db->mutex); |
| 104552 | 104606 | return SQLITE_OK; |
| 104553 | 104607 | } |
| 104554 | 104608 | |
| 104555 | 104609 | /* |
| @@ -108209,11 +108263,11 @@ | ||
| 108209 | 108263 | if( pTblName ){ |
| 108210 | 108264 | sqlite3RefillIndex(pParse, pIndex, iMem); |
| 108211 | 108265 | sqlite3ChangeCookie(pParse, iDb); |
| 108212 | 108266 | sqlite3VdbeAddParseSchemaOp(v, iDb, |
| 108213 | 108267 | sqlite3MPrintf(db, "name='%q' AND type='index'", pIndex->zName)); |
| 108214 | - sqlite3VdbeAddOp0(v, OP_Expire); | |
| 108268 | + sqlite3VdbeAddOp2(v, OP_Expire, 0, 1); | |
| 108215 | 108269 | } |
| 108216 | 108270 | |
| 108217 | 108271 | sqlite3VdbeJumpHere(v, pIndex->tnum); |
| 108218 | 108272 | } |
| 108219 | 108273 | |
| @@ -131894,11 +131948,11 @@ | ||
| 131894 | 131948 | |
| 131895 | 131949 | assert( sqlite3BtreeHoldsAllMutexes(db) ); |
| 131896 | 131950 | assert( sqlite3_mutex_held(db->mutex) ); |
| 131897 | 131951 | |
| 131898 | 131952 | if( p ){ |
| 131899 | - sqlite3ExpirePreparedStatements(db); | |
| 131953 | + sqlite3ExpirePreparedStatements(db, 0); | |
| 131900 | 131954 | do { |
| 131901 | 131955 | VTable *pNext = p->pNext; |
| 131902 | 131956 | sqlite3VtabUnlock(p); |
| 131903 | 131957 | p = pNext; |
| 131904 | 131958 | }while( p ); |
| @@ -138680,11 +138734,13 @@ | ||
| 138680 | 138734 | |
| 138681 | 138735 | #ifdef SQLITE_ENABLE_STAT3_OR_STAT4 |
| 138682 | 138736 | Index *p = pLoop->u.btree.pIndex; |
| 138683 | 138737 | int nEq = pLoop->u.btree.nEq; |
| 138684 | 138738 | |
| 138685 | - if( p->nSample>0 && nEq<p->nSampleCol ){ | |
| 138739 | + if( p->nSample>0 && nEq<p->nSampleCol | |
| 138740 | + && OptimizationEnabled(pParse->db, SQLITE_Stat34) | |
| 138741 | + ){ | |
| 138686 | 138742 | if( nEq==pBuilder->nRecValid ){ |
| 138687 | 138743 | UnpackedRecord *pRec = pBuilder->pRec; |
| 138688 | 138744 | tRowcnt a[2]; |
| 138689 | 138745 | int nBtm = pLoop->u.btree.nBtm; |
| 138690 | 138746 | int nTop = pLoop->u.btree.nTop; |
| @@ -139828,10 +139884,11 @@ | ||
| 139828 | 139884 | tRowcnt nOut = 0; |
| 139829 | 139885 | if( nInMul==0 |
| 139830 | 139886 | && pProbe->nSample |
| 139831 | 139887 | && pNew->u.btree.nEq<=pProbe->nSampleCol |
| 139832 | 139888 | && ((eOp & WO_IN)==0 || !ExprHasProperty(pTerm->pExpr, EP_xIsSelect)) |
| 139889 | + && OptimizationEnabled(db, SQLITE_Stat34) | |
| 139833 | 139890 | ){ |
| 139834 | 139891 | Expr *pExpr = pTerm->pExpr; |
| 139835 | 139892 | if( (eOp & (WO_EQ|WO_ISNULL|WO_IS))!=0 ){ |
| 139836 | 139893 | testcase( eOp & WO_EQ ); |
| 139837 | 139894 | testcase( eOp & WO_IS ); |
| @@ -146916,11 +146973,11 @@ | ||
| 146916 | 146973 | |
| 146917 | 146974 | /* |
| 146918 | 146975 | ** Find the appropriate action for a parser given the non-terminal |
| 146919 | 146976 | ** look-ahead token iLookAhead. |
| 146920 | 146977 | */ |
| 146921 | -static int yy_find_reduce_action( | |
| 146978 | +static YYACTIONTYPE yy_find_reduce_action( | |
| 146922 | 146979 | YYACTIONTYPE stateno, /* Current state number */ |
| 146923 | 146980 | YYCODETYPE iLookAhead /* The look-ahead token */ |
| 146924 | 146981 | ){ |
| 146925 | 146982 | int i; |
| 146926 | 146983 | #ifdef YYERRORSYMBOL |
| @@ -147421,11 +147478,11 @@ | ||
| 147421 | 147478 | int yyLookahead, /* Lookahead token, or YYNOCODE if none */ |
| 147422 | 147479 | sqlite3ParserTOKENTYPE yyLookaheadToken /* Value of the lookahead token */ |
| 147423 | 147480 | sqlite3ParserCTX_PDECL /* %extra_context */ |
| 147424 | 147481 | ){ |
| 147425 | 147482 | int yygoto; /* The next state */ |
| 147426 | - int yyact; /* The next action */ | |
| 147483 | + YYACTIONTYPE yyact; /* The next action */ | |
| 147427 | 147484 | yyStackEntry *yymsp; /* The top of the parser's stack */ |
| 147428 | 147485 | int yysize; /* Amount to pop the stack */ |
| 147429 | 147486 | sqlite3ParserARG_FETCH |
| 147430 | 147487 | (void)yyLookahead; |
| 147431 | 147488 | (void)yyLookaheadToken; |
| @@ -148980,16 +149037,16 @@ | ||
| 148980 | 149037 | } |
| 148981 | 149038 | #endif |
| 148982 | 149039 | |
| 148983 | 149040 | do{ |
| 148984 | 149041 | assert( yyact==yypParser->yytos->stateno ); |
| 148985 | - yyact = yy_find_shift_action(yymajor,yyact); | |
| 149042 | + yyact = yy_find_shift_action((YYCODETYPE)yymajor,yyact); | |
| 148986 | 149043 | if( yyact >= YY_MIN_REDUCE ){ |
| 148987 | 149044 | yyact = yy_reduce(yypParser,yyact-YY_MIN_REDUCE,yymajor, |
| 148988 | 149045 | yyminor sqlite3ParserCTX_PARAM); |
| 148989 | 149046 | }else if( yyact <= YY_MAX_SHIFTREDUCE ){ |
| 148990 | - yy_shift(yypParser,yyact,yymajor,yyminor); | |
| 149047 | + yy_shift(yypParser,yyact,(YYCODETYPE)yymajor,yyminor); | |
| 148991 | 149048 | #ifndef YYNOERRORRECOVERY |
| 148992 | 149049 | yypParser->yyerrcnt--; |
| 148993 | 149050 | #endif |
| 148994 | 149051 | break; |
| 148995 | 149052 | }else if( yyact==YY_ACCEPT_ACTION ){ |
| @@ -151421,11 +151478,11 @@ | ||
| 151421 | 151478 | db->flags |= aFlagOp[i].mask; |
| 151422 | 151479 | }else if( onoff==0 ){ |
| 151423 | 151480 | db->flags &= ~aFlagOp[i].mask; |
| 151424 | 151481 | } |
| 151425 | 151482 | if( oldFlags!=db->flags ){ |
| 151426 | - sqlite3ExpirePreparedStatements(db); | |
| 151483 | + sqlite3ExpirePreparedStatements(db, 0); | |
| 151427 | 151484 | } |
| 151428 | 151485 | if( pRes ){ |
| 151429 | 151486 | *pRes = (db->flags & aFlagOp[i].mask)!=0; |
| 151430 | 151487 | } |
| 151431 | 151488 | rc = SQLITE_OK; |
| @@ -151864,11 +151921,11 @@ | ||
| 151864 | 151921 | } |
| 151865 | 151922 | sqlite3VtabRollback(db); |
| 151866 | 151923 | sqlite3EndBenignMalloc(); |
| 151867 | 151924 | |
| 151868 | 151925 | if( schemaChange ){ |
| 151869 | - sqlite3ExpirePreparedStatements(db); | |
| 151926 | + sqlite3ExpirePreparedStatements(db, 0); | |
| 151870 | 151927 | sqlite3ResetAllSchemasOfConnection(db); |
| 151871 | 151928 | } |
| 151872 | 151929 | sqlite3BtreeLeaveAll(db); |
| 151873 | 151930 | |
| 151874 | 151931 | /* Any deferred constraint violations have now been resolved. */ |
| @@ -152319,11 +152376,11 @@ | ||
| 152319 | 152376 | sqlite3ErrorWithMsg(db, SQLITE_BUSY, |
| 152320 | 152377 | "unable to delete/modify user-function due to active statements"); |
| 152321 | 152378 | assert( !db->mallocFailed ); |
| 152322 | 152379 | return SQLITE_BUSY; |
| 152323 | 152380 | }else{ |
| 152324 | - sqlite3ExpirePreparedStatements(db); | |
| 152381 | + sqlite3ExpirePreparedStatements(db, 0); | |
| 152325 | 152382 | } |
| 152326 | 152383 | } |
| 152327 | 152384 | |
| 152328 | 152385 | p = sqlite3FindFunction(db, zFunctionName, nArg, (u8)enc, 1); |
| 152329 | 152386 | assert(p || db->mallocFailed); |
| @@ -153094,11 +153151,11 @@ | ||
| 153094 | 153151 | if( db->nVdbeActive ){ |
| 153095 | 153152 | sqlite3ErrorWithMsg(db, SQLITE_BUSY, |
| 153096 | 153153 | "unable to delete/modify collation sequence due to active statements"); |
| 153097 | 153154 | return SQLITE_BUSY; |
| 153098 | 153155 | } |
| 153099 | - sqlite3ExpirePreparedStatements(db); | |
| 153156 | + sqlite3ExpirePreparedStatements(db, 0); | |
| 153100 | 153157 | |
| 153101 | 153158 | /* If collation sequence pColl was created directly by a call to |
| 153102 | 153159 | ** sqlite3_create_collation, and not generated by synthCollSeq(), |
| 153103 | 153160 | ** then any copies made by synthCollSeq() need to be invalidated. |
| 153104 | 153161 | ** Also, collation destructor - CollSeq.xDel() - function may need |
| @@ -194031,16 +194088,18 @@ | ||
| 194031 | 194088 | rc = sqlite3_create_function(db, aFunc[i].zName, aFunc[i].nArg, |
| 194032 | 194089 | SQLITE_UTF8 | SQLITE_DETERMINISTIC, |
| 194033 | 194090 | (void*)&aFunc[i].flag, |
| 194034 | 194091 | aFunc[i].xFunc, 0, 0); |
| 194035 | 194092 | } |
| 194093 | +#ifndef SQLITE_OMIT_WINDOWFUNC | |
| 194036 | 194094 | for(i=0; i<sizeof(aAgg)/sizeof(aAgg[0]) && rc==SQLITE_OK; i++){ |
| 194037 | 194095 | rc = sqlite3_create_window_function(db, aAgg[i].zName, aAgg[i].nArg, |
| 194038 | 194096 | SQLITE_UTF8 | SQLITE_DETERMINISTIC, 0, |
| 194039 | 194097 | aAgg[i].xStep, aAgg[i].xFinal, |
| 194040 | 194098 | aAgg[i].xValue, jsonGroupInverse, 0); |
| 194041 | 194099 | } |
| 194100 | +#endif | |
| 194042 | 194101 | #ifndef SQLITE_OMIT_VIRTUALTABLE |
| 194043 | 194102 | for(i=0; i<sizeof(aMod)/sizeof(aMod[0]) && rc==SQLITE_OK; i++){ |
| 194044 | 194103 | rc = sqlite3_create_module(db, aMod[i].zName, aMod[i].pModule, 0); |
| 194045 | 194104 | } |
| 194046 | 194105 | #endif |
| @@ -196237,11 +196296,11 @@ | ||
| 196237 | 196296 | |
| 196238 | 196297 | /* |
| 196239 | 196298 | ** Find the appropriate action for a parser given the non-terminal |
| 196240 | 196299 | ** look-ahead token iLookAhead. |
| 196241 | 196300 | */ |
| 196242 | -static int fts5yy_find_reduce_action( | |
| 196301 | +static fts5YYACTIONTYPE fts5yy_find_reduce_action( | |
| 196243 | 196302 | fts5YYACTIONTYPE stateno, /* Current state number */ |
| 196244 | 196303 | fts5YYCODETYPE iLookAhead /* The look-ahead token */ |
| 196245 | 196304 | ){ |
| 196246 | 196305 | int i; |
| 196247 | 196306 | #ifdef fts5YYERRORSYMBOL |
| @@ -196405,11 +196464,11 @@ | ||
| 196405 | 196464 | int fts5yyLookahead, /* Lookahead token, or fts5YYNOCODE if none */ |
| 196406 | 196465 | sqlite3Fts5ParserFTS5TOKENTYPE fts5yyLookaheadToken /* Value of the lookahead token */ |
| 196407 | 196466 | sqlite3Fts5ParserCTX_PDECL /* %extra_context */ |
| 196408 | 196467 | ){ |
| 196409 | 196468 | int fts5yygoto; /* The next state */ |
| 196410 | - int fts5yyact; /* The next action */ | |
| 196469 | + fts5YYACTIONTYPE fts5yyact; /* The next action */ | |
| 196411 | 196470 | fts5yyStackEntry *fts5yymsp; /* The top of the parser's stack */ |
| 196412 | 196471 | int fts5yysize; /* Amount to pop the stack */ |
| 196413 | 196472 | sqlite3Fts5ParserARG_FETCH |
| 196414 | 196473 | (void)fts5yyLookahead; |
| 196415 | 196474 | (void)fts5yyLookaheadToken; |
| @@ -196761,16 +196820,16 @@ | ||
| 196761 | 196820 | } |
| 196762 | 196821 | #endif |
| 196763 | 196822 | |
| 196764 | 196823 | do{ |
| 196765 | 196824 | assert( fts5yyact==fts5yypParser->fts5yytos->stateno ); |
| 196766 | - fts5yyact = fts5yy_find_shift_action(fts5yymajor,fts5yyact); | |
| 196825 | + fts5yyact = fts5yy_find_shift_action((fts5YYCODETYPE)fts5yymajor,fts5yyact); | |
| 196767 | 196826 | if( fts5yyact >= fts5YY_MIN_REDUCE ){ |
| 196768 | 196827 | fts5yyact = fts5yy_reduce(fts5yypParser,fts5yyact-fts5YY_MIN_REDUCE,fts5yymajor, |
| 196769 | 196828 | fts5yyminor sqlite3Fts5ParserCTX_PARAM); |
| 196770 | 196829 | }else if( fts5yyact <= fts5YY_MAX_SHIFTREDUCE ){ |
| 196771 | - fts5yy_shift(fts5yypParser,fts5yyact,fts5yymajor,fts5yyminor); | |
| 196830 | + fts5yy_shift(fts5yypParser,fts5yyact,(fts5YYCODETYPE)fts5yymajor,fts5yyminor); | |
| 196772 | 196831 | #ifndef fts5YYNOERRORRECOVERY |
| 196773 | 196832 | fts5yypParser->fts5yyerrcnt--; |
| 196774 | 196833 | #endif |
| 196775 | 196834 | break; |
| 196776 | 196835 | }else if( fts5yyact==fts5YY_ACCEPT_ACTION ){ |
| @@ -211513,11 +211572,11 @@ | ||
| 211513 | 211572 | int nArg, /* Number of args */ |
| 211514 | 211573 | sqlite3_value **apUnused /* Function arguments */ |
| 211515 | 211574 | ){ |
| 211516 | 211575 | assert( nArg==0 ); |
| 211517 | 211576 | UNUSED_PARAM2(nArg, apUnused); |
| 211518 | - sqlite3_result_text(pCtx, "fts5: 2018-07-18 19:09:07 a5087c5c87ad65f92e3bc96bbc84afb43faf10ab6b9ed3ba16304b5c60ad069f", -1, SQLITE_TRANSIENT); | |
| 211577 | + sqlite3_result_text(pCtx, "fts5: 2018-07-24 22:02:12 2bd593332da0aade467e7a4ee89e966aa6302f37540a2c5e23671f98a6cb599c", -1, SQLITE_TRANSIENT); | |
| 211519 | 211578 | } |
| 211520 | 211579 | |
| 211521 | 211580 | static int fts5Init(sqlite3 *db){ |
| 211522 | 211581 | static const sqlite3_module fts5Mod = { |
| 211523 | 211582 | /* iVersion */ 2, |
| @@ -214799,11 +214858,11 @@ | ||
| 214799 | 214858 | int iTbl = 0; |
| 214800 | 214859 | while( i<128 ){ |
| 214801 | 214860 | int bToken = aArray[ aFts5UnicodeData[iTbl] & 0x1F ]; |
| 214802 | 214861 | int n = (aFts5UnicodeData[iTbl] >> 5) + i; |
| 214803 | 214862 | for(; i<128 && i<n; i++){ |
| 214804 | - aAscii[i] = bToken; | |
| 214863 | + aAscii[i] = (u8)bToken; | |
| 214805 | 214864 | } |
| 214806 | 214865 | iTbl++; |
| 214807 | 214866 | } |
| 214808 | 214867 | } |
| 214809 | 214868 | |
| @@ -216223,12 +216282,12 @@ | ||
| 216223 | 216282 | } |
| 216224 | 216283 | #endif /* SQLITE_CORE */ |
| 216225 | 216284 | #endif /* !defined(SQLITE_CORE) || defined(SQLITE_ENABLE_STMTVTAB) */ |
| 216226 | 216285 | |
| 216227 | 216286 | /************** End of stmt.c ************************************************/ |
| 216228 | -#if __LINE__!=216228 | |
| 216287 | +#if __LINE__!=216287 | |
| 216229 | 216288 | #undef SQLITE_SOURCE_ID |
| 216230 | -#define SQLITE_SOURCE_ID "2018-07-18 19:09:07 a5087c5c87ad65f92e3bc96bbc84afb43faf10ab6b9ed3ba16304b5c60adalt2" | |
| 216289 | +#define SQLITE_SOURCE_ID "2018-07-24 22:02:12 2bd593332da0aade467e7a4ee89e966aa6302f37540a2c5e23671f98a6cbalt2" | |
| 216231 | 216290 | #endif |
| 216232 | 216291 | /* Return the source-id for this library */ |
| 216233 | 216292 | SQLITE_API const char *sqlite3_sourceid(void){ return SQLITE_SOURCE_ID; } |
| 216234 | 216293 | /************************** End of sqlite3.c ******************************/ |
| 216235 | 216294 |
| --- src/sqlite3.c | |
| +++ src/sqlite3.c | |
| @@ -53,10 +53,16 @@ | |
| 53 | /* These macros are provided to "stringify" the value of the define |
| 54 | ** for those options in which the value is meaningful. */ |
| 55 | #define CTIMEOPT_VAL_(opt) #opt |
| 56 | #define CTIMEOPT_VAL(opt) CTIMEOPT_VAL_(opt) |
| 57 | |
| 58 | /* |
| 59 | ** An array of names of all compile-time options. This array should |
| 60 | ** be sorted A-Z. |
| 61 | ** |
| 62 | ** This array looks large, but in a typical installation actually uses |
| @@ -136,11 +142,11 @@ | |
| 136 | #endif |
| 137 | #ifdef SQLITE_DEFAULT_LOCKING_MODE |
| 138 | "DEFAULT_LOCKING_MODE=" CTIMEOPT_VAL(SQLITE_DEFAULT_LOCKING_MODE), |
| 139 | #endif |
| 140 | #ifdef SQLITE_DEFAULT_LOOKASIDE |
| 141 | "DEFAULT_LOOKASIDE=" CTIMEOPT_VAL(SQLITE_DEFAULT_LOOKASIDE), |
| 142 | #endif |
| 143 | #if SQLITE_DEFAULT_MEMSTATUS |
| 144 | "DEFAULT_MEMSTATUS", |
| 145 | #endif |
| 146 | #ifdef SQLITE_DEFAULT_MMAP_SIZE |
| @@ -1150,11 +1156,11 @@ | |
| 1150 | ** [sqlite3_libversion_number()], [sqlite3_sourceid()], |
| 1151 | ** [sqlite_version()] and [sqlite_source_id()]. |
| 1152 | */ |
| 1153 | #define SQLITE_VERSION "3.25.0" |
| 1154 | #define SQLITE_VERSION_NUMBER 3025000 |
| 1155 | #define SQLITE_SOURCE_ID "2018-07-18 19:09:07 a5087c5c87ad65f92e3bc96bbc84afb43faf10ab6b9ed3ba16304b5c60ad069f" |
| 1156 | |
| 1157 | /* |
| 1158 | ** CAPI3REF: Run-Time Library Version Numbers |
| 1159 | ** KEYWORDS: sqlite3_version sqlite3_sourceid |
| 1160 | ** |
| @@ -1912,11 +1918,12 @@ | |
| 1912 | ** interrogated. The zDbName parameter is ignored. |
| 1913 | ** |
| 1914 | ** <li>[[SQLITE_FCNTL_PERSIST_WAL]] |
| 1915 | ** ^The [SQLITE_FCNTL_PERSIST_WAL] opcode is used to set or query the |
| 1916 | ** persistent [WAL | Write Ahead Log] setting. By default, the auxiliary |
| 1917 | ** write ahead log and shared memory files used for transaction control |
| 1918 | ** are automatically deleted when the latest connection to the database |
| 1919 | ** closes. Setting persistent WAL mode causes those files to persist after |
| 1920 | ** close. Persisting the files is useful when other processes that do not |
| 1921 | ** have write permission on the directory containing the database file want |
| 1922 | ** to read the database file, as the WAL and shared memory files must exist |
| @@ -9985,11 +9992,10 @@ | |
| 9985 | SQLITE_API int sqlite3_system_errno(sqlite3*); |
| 9986 | |
| 9987 | /* |
| 9988 | ** CAPI3REF: Database Snapshot |
| 9989 | ** KEYWORDS: {snapshot} {sqlite3_snapshot} |
| 9990 | ** EXPERIMENTAL |
| 9991 | ** |
| 9992 | ** An instance of the snapshot object records the state of a [WAL mode] |
| 9993 | ** database for some specific point in history. |
| 9994 | ** |
| 9995 | ** In [WAL mode], multiple [database connections] that are open on the |
| @@ -10002,23 +10008,18 @@ | |
| 10002 | ** |
| 10003 | ** The sqlite3_snapshot object records state information about an historical |
| 10004 | ** version of the database file so that it is possible to later open a new read |
| 10005 | ** transaction that sees that historical version of the database rather than |
| 10006 | ** the most recent version. |
| 10007 | ** |
| 10008 | ** The constructor for this object is [sqlite3_snapshot_get()]. The |
| 10009 | ** [sqlite3_snapshot_open()] method causes a fresh read transaction to refer |
| 10010 | ** to an historical snapshot (if possible). The destructor for |
| 10011 | ** sqlite3_snapshot objects is [sqlite3_snapshot_free()]. |
| 10012 | */ |
| 10013 | typedef struct sqlite3_snapshot { |
| 10014 | unsigned char hidden[48]; |
| 10015 | } sqlite3_snapshot; |
| 10016 | |
| 10017 | /* |
| 10018 | ** CAPI3REF: Record A Database Snapshot |
| 10019 | ** EXPERIMENTAL |
| 10020 | ** |
| 10021 | ** ^The [sqlite3_snapshot_get(D,S,P)] interface attempts to make a |
| 10022 | ** new [sqlite3_snapshot] object that records the current state of |
| 10023 | ** schema S in database connection D. ^On success, the |
| 10024 | ** [sqlite3_snapshot_get(D,S,P)] interface writes a pointer to the newly |
| @@ -10053,21 +10054,21 @@ | |
| 10053 | ** The [sqlite3_snapshot] object returned from a successful call to |
| 10054 | ** [sqlite3_snapshot_get()] must be freed using [sqlite3_snapshot_free()] |
| 10055 | ** to avoid a memory leak. |
| 10056 | ** |
| 10057 | ** The [sqlite3_snapshot_get()] interface is only available when the |
| 10058 | ** SQLITE_ENABLE_SNAPSHOT compile-time option is used. |
| 10059 | */ |
| 10060 | SQLITE_API SQLITE_EXPERIMENTAL int sqlite3_snapshot_get( |
| 10061 | sqlite3 *db, |
| 10062 | const char *zSchema, |
| 10063 | sqlite3_snapshot **ppSnapshot |
| 10064 | ); |
| 10065 | |
| 10066 | /* |
| 10067 | ** CAPI3REF: Start a read transaction on an historical snapshot |
| 10068 | ** EXPERIMENTAL |
| 10069 | ** |
| 10070 | ** ^The [sqlite3_snapshot_open(D,S,P)] interface starts a |
| 10071 | ** read transaction for schema S of |
| 10072 | ** [database connection] D such that the read transaction |
| 10073 | ** refers to historical [snapshot] P, rather than the most |
| @@ -10091,34 +10092,34 @@ | |
| 10091 | ** after the most recent I/O on the database connection.)^ |
| 10092 | ** (Hint: Run "[PRAGMA application_id]" against a newly opened |
| 10093 | ** database connection in order to make it ready to use snapshots.) |
| 10094 | ** |
| 10095 | ** The [sqlite3_snapshot_open()] interface is only available when the |
| 10096 | ** SQLITE_ENABLE_SNAPSHOT compile-time option is used. |
| 10097 | */ |
| 10098 | SQLITE_API SQLITE_EXPERIMENTAL int sqlite3_snapshot_open( |
| 10099 | sqlite3 *db, |
| 10100 | const char *zSchema, |
| 10101 | sqlite3_snapshot *pSnapshot |
| 10102 | ); |
| 10103 | |
| 10104 | /* |
| 10105 | ** CAPI3REF: Destroy a snapshot |
| 10106 | ** EXPERIMENTAL |
| 10107 | ** |
| 10108 | ** ^The [sqlite3_snapshot_free(P)] interface destroys [sqlite3_snapshot] P. |
| 10109 | ** The application must eventually free every [sqlite3_snapshot] object |
| 10110 | ** using this routine to avoid a memory leak. |
| 10111 | ** |
| 10112 | ** The [sqlite3_snapshot_free()] interface is only available when the |
| 10113 | ** SQLITE_ENABLE_SNAPSHOT compile-time option is used. |
| 10114 | */ |
| 10115 | SQLITE_API SQLITE_EXPERIMENTAL void sqlite3_snapshot_free(sqlite3_snapshot*); |
| 10116 | |
| 10117 | /* |
| 10118 | ** CAPI3REF: Compare the ages of two snapshot handles. |
| 10119 | ** EXPERIMENTAL |
| 10120 | ** |
| 10121 | ** The sqlite3_snapshot_cmp(P1, P2) interface is used to compare the ages |
| 10122 | ** of two valid snapshot handles. |
| 10123 | ** |
| 10124 | ** If the two snapshot handles are not associated with the same database |
| @@ -10133,35 +10134,41 @@ | |
| 10133 | ** is undefined. |
| 10134 | ** |
| 10135 | ** Otherwise, this API returns a negative value if P1 refers to an older |
| 10136 | ** snapshot than P2, zero if the two handles refer to the same database |
| 10137 | ** snapshot, and a positive value if P1 is a newer snapshot than P2. |
| 10138 | */ |
| 10139 | SQLITE_API SQLITE_EXPERIMENTAL int sqlite3_snapshot_cmp( |
| 10140 | sqlite3_snapshot *p1, |
| 10141 | sqlite3_snapshot *p2 |
| 10142 | ); |
| 10143 | |
| 10144 | /* |
| 10145 | ** CAPI3REF: Recover snapshots from a wal file |
| 10146 | ** EXPERIMENTAL |
| 10147 | ** |
| 10148 | ** If all connections disconnect from a database file but do not perform |
| 10149 | ** a checkpoint, the existing wal file is opened along with the database |
| 10150 | ** file the next time the database is opened. At this point it is only |
| 10151 | ** possible to successfully call sqlite3_snapshot_open() to open the most |
| 10152 | ** recent snapshot of the database (the one at the head of the wal file), |
| 10153 | ** even though the wal file may contain other valid snapshots for which |
| 10154 | ** clients have sqlite3_snapshot handles. |
| 10155 | ** |
| 10156 | ** This function attempts to scan the wal file associated with database zDb |
| 10157 | ** of database handle db and make all valid snapshots available to |
| 10158 | ** sqlite3_snapshot_open(). It is an error if there is already a read |
| 10159 | ** transaction open on the database, or if the database is not a wal mode |
| 10160 | ** database. |
| 10161 | ** |
| 10162 | ** SQLITE_OK is returned if successful, or an SQLite error code otherwise. |
| 10163 | */ |
| 10164 | SQLITE_API SQLITE_EXPERIMENTAL int sqlite3_snapshot_recover(sqlite3 *db, const char *zDb); |
| 10165 | |
| 10166 | /* |
| 10167 | ** CAPI3REF: Serialize a database |
| @@ -18937,11 +18944,11 @@ | |
| 18937 | SQLITE_PRIVATE void sqlite3Reindex(Parse*, Token*, Token*); |
| 18938 | SQLITE_PRIVATE void sqlite3AlterFunctions(void); |
| 18939 | SQLITE_PRIVATE void sqlite3AlterRenameTable(Parse*, SrcList*, Token*); |
| 18940 | SQLITE_PRIVATE int sqlite3GetToken(const unsigned char *, int *); |
| 18941 | SQLITE_PRIVATE void sqlite3NestedParse(Parse*, const char*, ...); |
| 18942 | SQLITE_PRIVATE void sqlite3ExpirePreparedStatements(sqlite3*); |
| 18943 | SQLITE_PRIVATE int sqlite3CodeSubselect(Parse*, Expr *, int, int); |
| 18944 | SQLITE_PRIVATE void sqlite3SelectPrep(Parse*, Select*, NameContext*); |
| 18945 | SQLITE_PRIVATE void sqlite3SelectWrongNumTermsError(Parse *pParse, Select *p); |
| 18946 | SQLITE_PRIVATE int sqlite3MatchSpanName(const char*, const char*, const char*, const char*); |
| 18947 | SQLITE_PRIVATE int sqlite3ResolveExprNames(NameContext*, Expr*); |
| @@ -20025,13 +20032,13 @@ | |
| 20025 | #endif |
| 20026 | u16 nResColumn; /* Number of columns in one row of the result set */ |
| 20027 | u8 errorAction; /* Recovery action to do in case of an error */ |
| 20028 | u8 minWriteFileFormat; /* Minimum file format for writable database files */ |
| 20029 | u8 prepFlags; /* SQLITE_PREPARE_* flags */ |
| 20030 | bft expired:1; /* True if the VM needs to be recompiled */ |
| 20031 | bft doingRerun:1; /* True if rerunning after an auto-reprepare */ |
| 20032 | bft explain:2; /* True if EXPLAIN present on SQL command */ |
| 20033 | bft changeCntOn:1; /* True to update the change-counter */ |
| 20034 | bft runOnlyOnce:1; /* Automatically expire on reset */ |
| 20035 | bft usesStmtJournal:1; /* True if uses a statement journal */ |
| 20036 | bft readOnly:1; /* True for statements that do not write */ |
| 20037 | bft bIsReader:1; /* True for statements that read */ |
| @@ -28640,11 +28647,11 @@ | |
| 28640 | sqlite3TreeViewLine(pView, "FUNCTION %Q", pExpr->u.zToken); |
| 28641 | } |
| 28642 | if( pFarg ){ |
| 28643 | sqlite3TreeViewExprList(pView, pFarg, pWin!=0, 0); |
| 28644 | } |
| 28645 | #ifndef SQLITe_OMIT_WINDOWFUNC |
| 28646 | if( pWin ){ |
| 28647 | sqlite3TreeViewWindow(pView, pWin, 0); |
| 28648 | } |
| 28649 | #endif |
| 28650 | break; |
| @@ -42948,10 +42955,13 @@ | |
| 42948 | */ |
| 42949 | static int winTruncate(sqlite3_file *id, sqlite3_int64 nByte){ |
| 42950 | winFile *pFile = (winFile*)id; /* File handle object */ |
| 42951 | int rc = SQLITE_OK; /* Return code for this function */ |
| 42952 | DWORD lastErrno; |
| 42953 | |
| 42954 | assert( pFile ); |
| 42955 | SimulateIOError(return SQLITE_IOERR_TRUNCATE); |
| 42956 | OSTRACE(("TRUNCATE pid=%lu, pFile=%p, file=%p, size=%lld, lock=%d\n", |
| 42957 | osGetCurrentProcessId(), pFile, pFile->h, nByte, pFile->locktype)); |
| @@ -42962,10 +42972,19 @@ | |
| 42962 | ** size). |
| 42963 | */ |
| 42964 | if( pFile->szChunk>0 ){ |
| 42965 | nByte = ((nByte + pFile->szChunk - 1)/pFile->szChunk) * pFile->szChunk; |
| 42966 | } |
| 42967 | |
| 42968 | /* SetEndOfFile() returns non-zero when successful, or zero when it fails. */ |
| 42969 | if( winSeekFile(pFile, nByte) ){ |
| 42970 | rc = winLogError(SQLITE_IOERR_TRUNCATE, pFile->lastErrno, |
| 42971 | "winTruncate1", pFile->zPath); |
| @@ -42975,16 +42994,16 @@ | |
| 42975 | rc = winLogError(SQLITE_IOERR_TRUNCATE, pFile->lastErrno, |
| 42976 | "winTruncate2", pFile->zPath); |
| 42977 | } |
| 42978 | |
| 42979 | #if SQLITE_MAX_MMAP_SIZE>0 |
| 42980 | /* If the file was truncated to a size smaller than the currently |
| 42981 | ** mapped region, reduce the effective mapping size as well. SQLite will |
| 42982 | ** use read() and write() to access data beyond this point from now on. |
| 42983 | */ |
| 42984 | if( pFile->pMapRegion && nByte<pFile->mmapSize ){ |
| 42985 | pFile->mmapSize = nByte; |
| 42986 | } |
| 42987 | #endif |
| 42988 | |
| 42989 | OSTRACE(("TRUNCATE pid=%lu, pFile=%p, file=%p, rc=%s\n", |
| 42990 | osGetCurrentProcessId(), pFile, pFile->h, sqlite3ErrName(rc))); |
| @@ -56461,22 +56480,22 @@ | |
| 56461 | |
| 56462 | if( (rc&0xFF)==SQLITE_IOERR && rc!=SQLITE_IOERR_NOMEM ){ |
| 56463 | rc = sqlite3JournalCreate(pPager->jfd); |
| 56464 | if( rc!=SQLITE_OK ){ |
| 56465 | sqlite3OsClose(pPager->jfd); |
| 56466 | } |
| 56467 | bBatch = 0; |
| 56468 | }else{ |
| 56469 | sqlite3OsClose(pPager->jfd); |
| 56470 | } |
| 56471 | } |
| 56472 | #endif /* SQLITE_ENABLE_BATCH_ATOMIC_WRITE */ |
| 56473 | |
| 56474 | if( bBatch==0 && rc==SQLITE_OK ){ |
| 56475 | rc = pager_write_pagelist(pPager, pList); |
| 56476 | } |
| 56477 | |
| 56478 | if( rc!=SQLITE_OK ){ |
| 56479 | assert( rc!=SQLITE_IOERR_BLOCKED ); |
| 56480 | goto commit_phase_one_exit; |
| 56481 | } |
| 56482 | sqlite3PcacheCleanAll(pPager->pPCache); |
| @@ -65751,10 +65770,16 @@ | |
| 65751 | */ |
| 65752 | if( p->inTrans==TRANS_WRITE || (p->inTrans==TRANS_READ && !wrflag) ){ |
| 65753 | goto trans_begun; |
| 65754 | } |
| 65755 | assert( pBt->inTransaction==TRANS_WRITE || IfNotOmitAV(pBt->bDoTruncate)==0 ); |
| 65756 | |
| 65757 | /* Write transactions are not possible on a read-only database */ |
| 65758 | if( (pBt->btsFlags & BTS_READ_ONLY)!=0 && wrflag ){ |
| 65759 | rc = SQLITE_READONLY; |
| 65760 | goto trans_begun; |
| @@ -71767,12 +71792,11 @@ | |
| 71767 | ** if this is the first reference to the page. |
| 71768 | ** |
| 71769 | ** Also check that the page number is in bounds. |
| 71770 | */ |
| 71771 | static int checkRef(IntegrityCk *pCheck, Pgno iPage){ |
| 71772 | if( iPage==0 ) return 1; |
| 71773 | if( iPage>pCheck->nPage ){ |
| 71774 | checkAppendMsg(pCheck, "invalid page number %d", iPage); |
| 71775 | return 1; |
| 71776 | } |
| 71777 | if( getPageReferenced(pCheck, iPage) ){ |
| 71778 | checkAppendMsg(pCheck, "2nd reference to page %d", iPage); |
| @@ -71823,21 +71847,16 @@ | |
| 71823 | int iPage, /* Page number for first page in the list */ |
| 71824 | int N /* Expected number of pages in the list */ |
| 71825 | ){ |
| 71826 | int i; |
| 71827 | int expected = N; |
| 71828 | int iFirst = iPage; |
| 71829 | while( N-- > 0 && pCheck->mxErr ){ |
| 71830 | DbPage *pOvflPage; |
| 71831 | unsigned char *pOvflData; |
| 71832 | if( iPage<1 ){ |
| 71833 | checkAppendMsg(pCheck, |
| 71834 | "%d of %d pages missing from overflow list starting at %d", |
| 71835 | N+1, expected, iFirst); |
| 71836 | break; |
| 71837 | } |
| 71838 | if( checkRef(pCheck, iPage) ) break; |
| 71839 | if( sqlite3PagerGet(pCheck->pPager, (Pgno)iPage, &pOvflPage, 0) ){ |
| 71840 | checkAppendMsg(pCheck, "failed to get page %d", iPage); |
| 71841 | break; |
| 71842 | } |
| 71843 | pOvflData = (unsigned char *)sqlite3PagerGetData(pOvflPage); |
| @@ -71877,14 +71896,16 @@ | |
| 71877 | } |
| 71878 | } |
| 71879 | #endif |
| 71880 | iPage = get4byte(pOvflData); |
| 71881 | sqlite3PagerUnref(pOvflPage); |
| 71882 | |
| 71883 | if( isFreeList && N<(iPage!=0) ){ |
| 71884 | checkAppendMsg(pCheck, "free-page count in header is too small"); |
| 71885 | } |
| 71886 | } |
| 71887 | } |
| 71888 | #endif /* SQLITE_OMIT_INTEGRITY_CHECK */ |
| 71889 | |
| 71890 | /* |
| @@ -72274,10 +72295,28 @@ | |
| 72274 | get4byte(&pBt->pPage1->aData[36])); |
| 72275 | sCheck.zPfx = 0; |
| 72276 | |
| 72277 | /* Check all the tables. |
| 72278 | */ |
| 72279 | testcase( pBt->db->flags & SQLITE_CellSizeCk ); |
| 72280 | pBt->db->flags &= ~SQLITE_CellSizeCk; |
| 72281 | for(i=0; (int)i<nRoot && sCheck.mxErr; i++){ |
| 72282 | i64 notUsed; |
| 72283 | if( aRoot[i]==0 ) continue; |
| @@ -79974,15 +80013,23 @@ | |
| 79974 | ** An expired statement means that recompilation of the statement is |
| 79975 | ** recommend. Statements expire when things happen that make their |
| 79976 | ** programs obsolete. Removing user-defined functions or collating |
| 79977 | ** sequences, or changing an authorization function are the types of |
| 79978 | ** things that make prepared statements obsolete. |
| 79979 | */ |
| 79980 | SQLITE_PRIVATE void sqlite3ExpirePreparedStatements(sqlite3 *db){ |
| 79981 | Vdbe *p; |
| 79982 | for(p = db->pVdbe; p; p=p->pNext){ |
| 79983 | p->expired = 1; |
| 79984 | } |
| 79985 | } |
| 79986 | |
| 79987 | /* |
| 79988 | ** Return the database associated with the Vdbe. |
| @@ -85520,11 +85567,11 @@ | |
| 85520 | if( rc!=SQLITE_OK ){ |
| 85521 | goto abort_due_to_error; |
| 85522 | } |
| 85523 | } |
| 85524 | if( isSchemaChange ){ |
| 85525 | sqlite3ExpirePreparedStatements(db); |
| 85526 | sqlite3ResetAllSchemasOfConnection(db); |
| 85527 | db->mDbFlags |= DBFLAG_SchemaChange; |
| 85528 | } |
| 85529 | } |
| 85530 | |
| @@ -85809,11 +85856,11 @@ | |
| 85809 | pDb->pSchema->file_format = pOp->p3; |
| 85810 | } |
| 85811 | if( pOp->p1==1 ){ |
| 85812 | /* Invalidate all prepared statements whenever the TEMP database |
| 85813 | ** schema is changed. Ticket #1644 */ |
| 85814 | sqlite3ExpirePreparedStatements(db); |
| 85815 | p->expired = 0; |
| 85816 | } |
| 85817 | if( rc ) goto abort_due_to_error; |
| 85818 | break; |
| 85819 | } |
| @@ -85927,11 +85974,11 @@ | |
| 85927 | assert( pOp->opcode==OP_OpenWrite || pOp->p5==0 || pOp->p5==OPFLAG_SEEKEQ ); |
| 85928 | assert( p->bIsReader ); |
| 85929 | assert( pOp->opcode==OP_OpenRead || pOp->opcode==OP_ReopenIdx |
| 85930 | || p->readOnly==0 ); |
| 85931 | |
| 85932 | if( p->expired ){ |
| 85933 | rc = SQLITE_ABORT_ROLLBACK; |
| 85934 | goto abort_due_to_error; |
| 85935 | } |
| 85936 | |
| 85937 | nField = 0; |
| @@ -89108,25 +89155,32 @@ | |
| 89108 | } |
| 89109 | break; |
| 89110 | } |
| 89111 | #endif |
| 89112 | |
| 89113 | /* Opcode: Expire P1 * * * * |
| 89114 | ** |
| 89115 | ** Cause precompiled statements to expire. When an expired statement |
| 89116 | ** is executed using sqlite3_step() it will either automatically |
| 89117 | ** reprepare itself (if it was originally created using sqlite3_prepare_v2()) |
| 89118 | ** or it will fail with SQLITE_SCHEMA. |
| 89119 | ** |
| 89120 | ** If P1 is 0, then all SQL statements become expired. If P1 is non-zero, |
| 89121 | ** then only the currently executing statement is expired. |
| 89122 | */ |
| 89123 | case OP_Expire: { |
| 89124 | if( !pOp->p1 ){ |
| 89125 | sqlite3ExpirePreparedStatements(db); |
| 89126 | }else{ |
| 89127 | p->expired = 1; |
| 89128 | } |
| 89129 | break; |
| 89130 | } |
| 89131 | |
| 89132 | #ifndef SQLITE_OMIT_SHARED_CACHE |
| @@ -103813,11 +103867,11 @@ | |
| 103813 | if( !pIdx->hasStat1 ) sqlite3DefaultRowEst(pIdx); |
| 103814 | } |
| 103815 | |
| 103816 | /* Load the statistics from the sqlite_stat4 table. */ |
| 103817 | #ifdef SQLITE_ENABLE_STAT3_OR_STAT4 |
| 103818 | if( rc==SQLITE_OK && OptimizationEnabled(db, SQLITE_Stat34) ){ |
| 103819 | db->lookaside.bDisable++; |
| 103820 | rc = loadStat4(db, sInfo.zDatabase); |
| 103821 | db->lookaside.bDisable--; |
| 103822 | } |
| 103823 | for(i=sqliteHashFirst(&pSchema->idxHash); i; i=sqliteHashNext(i)){ |
| @@ -104545,11 +104599,11 @@ | |
| 104545 | if( !sqlite3SafetyCheckOk(db) ) return SQLITE_MISUSE_BKPT; |
| 104546 | #endif |
| 104547 | sqlite3_mutex_enter(db->mutex); |
| 104548 | db->xAuth = (sqlite3_xauth)xAuth; |
| 104549 | db->pAuthArg = pArg; |
| 104550 | sqlite3ExpirePreparedStatements(db); |
| 104551 | sqlite3_mutex_leave(db->mutex); |
| 104552 | return SQLITE_OK; |
| 104553 | } |
| 104554 | |
| 104555 | /* |
| @@ -108209,11 +108263,11 @@ | |
| 108209 | if( pTblName ){ |
| 108210 | sqlite3RefillIndex(pParse, pIndex, iMem); |
| 108211 | sqlite3ChangeCookie(pParse, iDb); |
| 108212 | sqlite3VdbeAddParseSchemaOp(v, iDb, |
| 108213 | sqlite3MPrintf(db, "name='%q' AND type='index'", pIndex->zName)); |
| 108214 | sqlite3VdbeAddOp0(v, OP_Expire); |
| 108215 | } |
| 108216 | |
| 108217 | sqlite3VdbeJumpHere(v, pIndex->tnum); |
| 108218 | } |
| 108219 | |
| @@ -131894,11 +131948,11 @@ | |
| 131894 | |
| 131895 | assert( sqlite3BtreeHoldsAllMutexes(db) ); |
| 131896 | assert( sqlite3_mutex_held(db->mutex) ); |
| 131897 | |
| 131898 | if( p ){ |
| 131899 | sqlite3ExpirePreparedStatements(db); |
| 131900 | do { |
| 131901 | VTable *pNext = p->pNext; |
| 131902 | sqlite3VtabUnlock(p); |
| 131903 | p = pNext; |
| 131904 | }while( p ); |
| @@ -138680,11 +138734,13 @@ | |
| 138680 | |
| 138681 | #ifdef SQLITE_ENABLE_STAT3_OR_STAT4 |
| 138682 | Index *p = pLoop->u.btree.pIndex; |
| 138683 | int nEq = pLoop->u.btree.nEq; |
| 138684 | |
| 138685 | if( p->nSample>0 && nEq<p->nSampleCol ){ |
| 138686 | if( nEq==pBuilder->nRecValid ){ |
| 138687 | UnpackedRecord *pRec = pBuilder->pRec; |
| 138688 | tRowcnt a[2]; |
| 138689 | int nBtm = pLoop->u.btree.nBtm; |
| 138690 | int nTop = pLoop->u.btree.nTop; |
| @@ -139828,10 +139884,11 @@ | |
| 139828 | tRowcnt nOut = 0; |
| 139829 | if( nInMul==0 |
| 139830 | && pProbe->nSample |
| 139831 | && pNew->u.btree.nEq<=pProbe->nSampleCol |
| 139832 | && ((eOp & WO_IN)==0 || !ExprHasProperty(pTerm->pExpr, EP_xIsSelect)) |
| 139833 | ){ |
| 139834 | Expr *pExpr = pTerm->pExpr; |
| 139835 | if( (eOp & (WO_EQ|WO_ISNULL|WO_IS))!=0 ){ |
| 139836 | testcase( eOp & WO_EQ ); |
| 139837 | testcase( eOp & WO_IS ); |
| @@ -146916,11 +146973,11 @@ | |
| 146916 | |
| 146917 | /* |
| 146918 | ** Find the appropriate action for a parser given the non-terminal |
| 146919 | ** look-ahead token iLookAhead. |
| 146920 | */ |
| 146921 | static int yy_find_reduce_action( |
| 146922 | YYACTIONTYPE stateno, /* Current state number */ |
| 146923 | YYCODETYPE iLookAhead /* The look-ahead token */ |
| 146924 | ){ |
| 146925 | int i; |
| 146926 | #ifdef YYERRORSYMBOL |
| @@ -147421,11 +147478,11 @@ | |
| 147421 | int yyLookahead, /* Lookahead token, or YYNOCODE if none */ |
| 147422 | sqlite3ParserTOKENTYPE yyLookaheadToken /* Value of the lookahead token */ |
| 147423 | sqlite3ParserCTX_PDECL /* %extra_context */ |
| 147424 | ){ |
| 147425 | int yygoto; /* The next state */ |
| 147426 | int yyact; /* The next action */ |
| 147427 | yyStackEntry *yymsp; /* The top of the parser's stack */ |
| 147428 | int yysize; /* Amount to pop the stack */ |
| 147429 | sqlite3ParserARG_FETCH |
| 147430 | (void)yyLookahead; |
| 147431 | (void)yyLookaheadToken; |
| @@ -148980,16 +149037,16 @@ | |
| 148980 | } |
| 148981 | #endif |
| 148982 | |
| 148983 | do{ |
| 148984 | assert( yyact==yypParser->yytos->stateno ); |
| 148985 | yyact = yy_find_shift_action(yymajor,yyact); |
| 148986 | if( yyact >= YY_MIN_REDUCE ){ |
| 148987 | yyact = yy_reduce(yypParser,yyact-YY_MIN_REDUCE,yymajor, |
| 148988 | yyminor sqlite3ParserCTX_PARAM); |
| 148989 | }else if( yyact <= YY_MAX_SHIFTREDUCE ){ |
| 148990 | yy_shift(yypParser,yyact,yymajor,yyminor); |
| 148991 | #ifndef YYNOERRORRECOVERY |
| 148992 | yypParser->yyerrcnt--; |
| 148993 | #endif |
| 148994 | break; |
| 148995 | }else if( yyact==YY_ACCEPT_ACTION ){ |
| @@ -151421,11 +151478,11 @@ | |
| 151421 | db->flags |= aFlagOp[i].mask; |
| 151422 | }else if( onoff==0 ){ |
| 151423 | db->flags &= ~aFlagOp[i].mask; |
| 151424 | } |
| 151425 | if( oldFlags!=db->flags ){ |
| 151426 | sqlite3ExpirePreparedStatements(db); |
| 151427 | } |
| 151428 | if( pRes ){ |
| 151429 | *pRes = (db->flags & aFlagOp[i].mask)!=0; |
| 151430 | } |
| 151431 | rc = SQLITE_OK; |
| @@ -151864,11 +151921,11 @@ | |
| 151864 | } |
| 151865 | sqlite3VtabRollback(db); |
| 151866 | sqlite3EndBenignMalloc(); |
| 151867 | |
| 151868 | if( schemaChange ){ |
| 151869 | sqlite3ExpirePreparedStatements(db); |
| 151870 | sqlite3ResetAllSchemasOfConnection(db); |
| 151871 | } |
| 151872 | sqlite3BtreeLeaveAll(db); |
| 151873 | |
| 151874 | /* Any deferred constraint violations have now been resolved. */ |
| @@ -152319,11 +152376,11 @@ | |
| 152319 | sqlite3ErrorWithMsg(db, SQLITE_BUSY, |
| 152320 | "unable to delete/modify user-function due to active statements"); |
| 152321 | assert( !db->mallocFailed ); |
| 152322 | return SQLITE_BUSY; |
| 152323 | }else{ |
| 152324 | sqlite3ExpirePreparedStatements(db); |
| 152325 | } |
| 152326 | } |
| 152327 | |
| 152328 | p = sqlite3FindFunction(db, zFunctionName, nArg, (u8)enc, 1); |
| 152329 | assert(p || db->mallocFailed); |
| @@ -153094,11 +153151,11 @@ | |
| 153094 | if( db->nVdbeActive ){ |
| 153095 | sqlite3ErrorWithMsg(db, SQLITE_BUSY, |
| 153096 | "unable to delete/modify collation sequence due to active statements"); |
| 153097 | return SQLITE_BUSY; |
| 153098 | } |
| 153099 | sqlite3ExpirePreparedStatements(db); |
| 153100 | |
| 153101 | /* If collation sequence pColl was created directly by a call to |
| 153102 | ** sqlite3_create_collation, and not generated by synthCollSeq(), |
| 153103 | ** then any copies made by synthCollSeq() need to be invalidated. |
| 153104 | ** Also, collation destructor - CollSeq.xDel() - function may need |
| @@ -194031,16 +194088,18 @@ | |
| 194031 | rc = sqlite3_create_function(db, aFunc[i].zName, aFunc[i].nArg, |
| 194032 | SQLITE_UTF8 | SQLITE_DETERMINISTIC, |
| 194033 | (void*)&aFunc[i].flag, |
| 194034 | aFunc[i].xFunc, 0, 0); |
| 194035 | } |
| 194036 | for(i=0; i<sizeof(aAgg)/sizeof(aAgg[0]) && rc==SQLITE_OK; i++){ |
| 194037 | rc = sqlite3_create_window_function(db, aAgg[i].zName, aAgg[i].nArg, |
| 194038 | SQLITE_UTF8 | SQLITE_DETERMINISTIC, 0, |
| 194039 | aAgg[i].xStep, aAgg[i].xFinal, |
| 194040 | aAgg[i].xValue, jsonGroupInverse, 0); |
| 194041 | } |
| 194042 | #ifndef SQLITE_OMIT_VIRTUALTABLE |
| 194043 | for(i=0; i<sizeof(aMod)/sizeof(aMod[0]) && rc==SQLITE_OK; i++){ |
| 194044 | rc = sqlite3_create_module(db, aMod[i].zName, aMod[i].pModule, 0); |
| 194045 | } |
| 194046 | #endif |
| @@ -196237,11 +196296,11 @@ | |
| 196237 | |
| 196238 | /* |
| 196239 | ** Find the appropriate action for a parser given the non-terminal |
| 196240 | ** look-ahead token iLookAhead. |
| 196241 | */ |
| 196242 | static int fts5yy_find_reduce_action( |
| 196243 | fts5YYACTIONTYPE stateno, /* Current state number */ |
| 196244 | fts5YYCODETYPE iLookAhead /* The look-ahead token */ |
| 196245 | ){ |
| 196246 | int i; |
| 196247 | #ifdef fts5YYERRORSYMBOL |
| @@ -196405,11 +196464,11 @@ | |
| 196405 | int fts5yyLookahead, /* Lookahead token, or fts5YYNOCODE if none */ |
| 196406 | sqlite3Fts5ParserFTS5TOKENTYPE fts5yyLookaheadToken /* Value of the lookahead token */ |
| 196407 | sqlite3Fts5ParserCTX_PDECL /* %extra_context */ |
| 196408 | ){ |
| 196409 | int fts5yygoto; /* The next state */ |
| 196410 | int fts5yyact; /* The next action */ |
| 196411 | fts5yyStackEntry *fts5yymsp; /* The top of the parser's stack */ |
| 196412 | int fts5yysize; /* Amount to pop the stack */ |
| 196413 | sqlite3Fts5ParserARG_FETCH |
| 196414 | (void)fts5yyLookahead; |
| 196415 | (void)fts5yyLookaheadToken; |
| @@ -196761,16 +196820,16 @@ | |
| 196761 | } |
| 196762 | #endif |
| 196763 | |
| 196764 | do{ |
| 196765 | assert( fts5yyact==fts5yypParser->fts5yytos->stateno ); |
| 196766 | fts5yyact = fts5yy_find_shift_action(fts5yymajor,fts5yyact); |
| 196767 | if( fts5yyact >= fts5YY_MIN_REDUCE ){ |
| 196768 | fts5yyact = fts5yy_reduce(fts5yypParser,fts5yyact-fts5YY_MIN_REDUCE,fts5yymajor, |
| 196769 | fts5yyminor sqlite3Fts5ParserCTX_PARAM); |
| 196770 | }else if( fts5yyact <= fts5YY_MAX_SHIFTREDUCE ){ |
| 196771 | fts5yy_shift(fts5yypParser,fts5yyact,fts5yymajor,fts5yyminor); |
| 196772 | #ifndef fts5YYNOERRORRECOVERY |
| 196773 | fts5yypParser->fts5yyerrcnt--; |
| 196774 | #endif |
| 196775 | break; |
| 196776 | }else if( fts5yyact==fts5YY_ACCEPT_ACTION ){ |
| @@ -211513,11 +211572,11 @@ | |
| 211513 | int nArg, /* Number of args */ |
| 211514 | sqlite3_value **apUnused /* Function arguments */ |
| 211515 | ){ |
| 211516 | assert( nArg==0 ); |
| 211517 | UNUSED_PARAM2(nArg, apUnused); |
| 211518 | sqlite3_result_text(pCtx, "fts5: 2018-07-18 19:09:07 a5087c5c87ad65f92e3bc96bbc84afb43faf10ab6b9ed3ba16304b5c60ad069f", -1, SQLITE_TRANSIENT); |
| 211519 | } |
| 211520 | |
| 211521 | static int fts5Init(sqlite3 *db){ |
| 211522 | static const sqlite3_module fts5Mod = { |
| 211523 | /* iVersion */ 2, |
| @@ -214799,11 +214858,11 @@ | |
| 214799 | int iTbl = 0; |
| 214800 | while( i<128 ){ |
| 214801 | int bToken = aArray[ aFts5UnicodeData[iTbl] & 0x1F ]; |
| 214802 | int n = (aFts5UnicodeData[iTbl] >> 5) + i; |
| 214803 | for(; i<128 && i<n; i++){ |
| 214804 | aAscii[i] = bToken; |
| 214805 | } |
| 214806 | iTbl++; |
| 214807 | } |
| 214808 | } |
| 214809 | |
| @@ -216223,12 +216282,12 @@ | |
| 216223 | } |
| 216224 | #endif /* SQLITE_CORE */ |
| 216225 | #endif /* !defined(SQLITE_CORE) || defined(SQLITE_ENABLE_STMTVTAB) */ |
| 216226 | |
| 216227 | /************** End of stmt.c ************************************************/ |
| 216228 | #if __LINE__!=216228 |
| 216229 | #undef SQLITE_SOURCE_ID |
| 216230 | #define SQLITE_SOURCE_ID "2018-07-18 19:09:07 a5087c5c87ad65f92e3bc96bbc84afb43faf10ab6b9ed3ba16304b5c60adalt2" |
| 216231 | #endif |
| 216232 | /* Return the source-id for this library */ |
| 216233 | SQLITE_API const char *sqlite3_sourceid(void){ return SQLITE_SOURCE_ID; } |
| 216234 | /************************** End of sqlite3.c ******************************/ |
| 216235 |
| --- src/sqlite3.c | |
| +++ src/sqlite3.c | |
| @@ -53,10 +53,16 @@ | |
| 53 | /* These macros are provided to "stringify" the value of the define |
| 54 | ** for those options in which the value is meaningful. */ |
| 55 | #define CTIMEOPT_VAL_(opt) #opt |
| 56 | #define CTIMEOPT_VAL(opt) CTIMEOPT_VAL_(opt) |
| 57 | |
| 58 | /* Like CTIMEOPT_VAL, but especially for SQLITE_DEFAULT_LOOKASIDE. This |
| 59 | ** option requires a separate macro because legal values contain a single |
| 60 | ** comma. e.g. (-DSQLITE_DEFAULT_LOOKASIDE="100,100") */ |
| 61 | #define CTIMEOPT_VAL2_(opt1,opt2) #opt1 "," #opt2 |
| 62 | #define CTIMEOPT_VAL2(opt) CTIMEOPT_VAL2_(opt) |
| 63 | |
| 64 | /* |
| 65 | ** An array of names of all compile-time options. This array should |
| 66 | ** be sorted A-Z. |
| 67 | ** |
| 68 | ** This array looks large, but in a typical installation actually uses |
| @@ -136,11 +142,11 @@ | |
| 142 | #endif |
| 143 | #ifdef SQLITE_DEFAULT_LOCKING_MODE |
| 144 | "DEFAULT_LOCKING_MODE=" CTIMEOPT_VAL(SQLITE_DEFAULT_LOCKING_MODE), |
| 145 | #endif |
| 146 | #ifdef SQLITE_DEFAULT_LOOKASIDE |
| 147 | "DEFAULT_LOOKASIDE=" CTIMEOPT_VAL2(SQLITE_DEFAULT_LOOKASIDE), |
| 148 | #endif |
| 149 | #if SQLITE_DEFAULT_MEMSTATUS |
| 150 | "DEFAULT_MEMSTATUS", |
| 151 | #endif |
| 152 | #ifdef SQLITE_DEFAULT_MMAP_SIZE |
| @@ -1150,11 +1156,11 @@ | |
| 1156 | ** [sqlite3_libversion_number()], [sqlite3_sourceid()], |
| 1157 | ** [sqlite_version()] and [sqlite_source_id()]. |
| 1158 | */ |
| 1159 | #define SQLITE_VERSION "3.25.0" |
| 1160 | #define SQLITE_VERSION_NUMBER 3025000 |
| 1161 | #define SQLITE_SOURCE_ID "2018-07-24 22:02:12 2bd593332da0aade467e7a4ee89e966aa6302f37540a2c5e23671f98a6cb599c" |
| 1162 | |
| 1163 | /* |
| 1164 | ** CAPI3REF: Run-Time Library Version Numbers |
| 1165 | ** KEYWORDS: sqlite3_version sqlite3_sourceid |
| 1166 | ** |
| @@ -1912,11 +1918,12 @@ | |
| 1918 | ** interrogated. The zDbName parameter is ignored. |
| 1919 | ** |
| 1920 | ** <li>[[SQLITE_FCNTL_PERSIST_WAL]] |
| 1921 | ** ^The [SQLITE_FCNTL_PERSIST_WAL] opcode is used to set or query the |
| 1922 | ** persistent [WAL | Write Ahead Log] setting. By default, the auxiliary |
| 1923 | ** write ahead log ([WAL file]) and shared memory |
| 1924 | ** files used for transaction control |
| 1925 | ** are automatically deleted when the latest connection to the database |
| 1926 | ** closes. Setting persistent WAL mode causes those files to persist after |
| 1927 | ** close. Persisting the files is useful when other processes that do not |
| 1928 | ** have write permission on the directory containing the database file want |
| 1929 | ** to read the database file, as the WAL and shared memory files must exist |
| @@ -9985,11 +9992,10 @@ | |
| 9992 | SQLITE_API int sqlite3_system_errno(sqlite3*); |
| 9993 | |
| 9994 | /* |
| 9995 | ** CAPI3REF: Database Snapshot |
| 9996 | ** KEYWORDS: {snapshot} {sqlite3_snapshot} |
| 9997 | ** |
| 9998 | ** An instance of the snapshot object records the state of a [WAL mode] |
| 9999 | ** database for some specific point in history. |
| 10000 | ** |
| 10001 | ** In [WAL mode], multiple [database connections] that are open on the |
| @@ -10002,23 +10008,18 @@ | |
| 10008 | ** |
| 10009 | ** The sqlite3_snapshot object records state information about an historical |
| 10010 | ** version of the database file so that it is possible to later open a new read |
| 10011 | ** transaction that sees that historical version of the database rather than |
| 10012 | ** the most recent version. |
| 10013 | */ |
| 10014 | typedef struct sqlite3_snapshot { |
| 10015 | unsigned char hidden[48]; |
| 10016 | } sqlite3_snapshot; |
| 10017 | |
| 10018 | /* |
| 10019 | ** CAPI3REF: Record A Database Snapshot |
| 10020 | ** CONSTRUCTOR: sqlite3_snapshot |
| 10021 | ** |
| 10022 | ** ^The [sqlite3_snapshot_get(D,S,P)] interface attempts to make a |
| 10023 | ** new [sqlite3_snapshot] object that records the current state of |
| 10024 | ** schema S in database connection D. ^On success, the |
| 10025 | ** [sqlite3_snapshot_get(D,S,P)] interface writes a pointer to the newly |
| @@ -10053,21 +10054,21 @@ | |
| 10054 | ** The [sqlite3_snapshot] object returned from a successful call to |
| 10055 | ** [sqlite3_snapshot_get()] must be freed using [sqlite3_snapshot_free()] |
| 10056 | ** to avoid a memory leak. |
| 10057 | ** |
| 10058 | ** The [sqlite3_snapshot_get()] interface is only available when the |
| 10059 | ** [SQLITE_ENABLE_SNAPSHOT] compile-time option is used. |
| 10060 | */ |
| 10061 | SQLITE_API SQLITE_EXPERIMENTAL int sqlite3_snapshot_get( |
| 10062 | sqlite3 *db, |
| 10063 | const char *zSchema, |
| 10064 | sqlite3_snapshot **ppSnapshot |
| 10065 | ); |
| 10066 | |
| 10067 | /* |
| 10068 | ** CAPI3REF: Start a read transaction on an historical snapshot |
| 10069 | ** METHOD: sqlite3_snapshot |
| 10070 | ** |
| 10071 | ** ^The [sqlite3_snapshot_open(D,S,P)] interface starts a |
| 10072 | ** read transaction for schema S of |
| 10073 | ** [database connection] D such that the read transaction |
| 10074 | ** refers to historical [snapshot] P, rather than the most |
| @@ -10091,34 +10092,34 @@ | |
| 10092 | ** after the most recent I/O on the database connection.)^ |
| 10093 | ** (Hint: Run "[PRAGMA application_id]" against a newly opened |
| 10094 | ** database connection in order to make it ready to use snapshots.) |
| 10095 | ** |
| 10096 | ** The [sqlite3_snapshot_open()] interface is only available when the |
| 10097 | ** [SQLITE_ENABLE_SNAPSHOT] compile-time option is used. |
| 10098 | */ |
| 10099 | SQLITE_API SQLITE_EXPERIMENTAL int sqlite3_snapshot_open( |
| 10100 | sqlite3 *db, |
| 10101 | const char *zSchema, |
| 10102 | sqlite3_snapshot *pSnapshot |
| 10103 | ); |
| 10104 | |
| 10105 | /* |
| 10106 | ** CAPI3REF: Destroy a snapshot |
| 10107 | ** DESTRUCTOR: sqlite3_snapshot |
| 10108 | ** |
| 10109 | ** ^The [sqlite3_snapshot_free(P)] interface destroys [sqlite3_snapshot] P. |
| 10110 | ** The application must eventually free every [sqlite3_snapshot] object |
| 10111 | ** using this routine to avoid a memory leak. |
| 10112 | ** |
| 10113 | ** The [sqlite3_snapshot_free()] interface is only available when the |
| 10114 | ** [SQLITE_ENABLE_SNAPSHOT] compile-time option is used. |
| 10115 | */ |
| 10116 | SQLITE_API SQLITE_EXPERIMENTAL void sqlite3_snapshot_free(sqlite3_snapshot*); |
| 10117 | |
| 10118 | /* |
| 10119 | ** CAPI3REF: Compare the ages of two snapshot handles. |
| 10120 | ** METHOD: sqlite3_snapshot |
| 10121 | ** |
| 10122 | ** The sqlite3_snapshot_cmp(P1, P2) interface is used to compare the ages |
| 10123 | ** of two valid snapshot handles. |
| 10124 | ** |
| 10125 | ** If the two snapshot handles are not associated with the same database |
| @@ -10133,35 +10134,41 @@ | |
| 10134 | ** is undefined. |
| 10135 | ** |
| 10136 | ** Otherwise, this API returns a negative value if P1 refers to an older |
| 10137 | ** snapshot than P2, zero if the two handles refer to the same database |
| 10138 | ** snapshot, and a positive value if P1 is a newer snapshot than P2. |
| 10139 | ** |
| 10140 | ** This interface is only available if SQLite is compiled with the |
| 10141 | ** [SQLITE_ENABLE_SNAPSHOT] option. |
| 10142 | */ |
| 10143 | SQLITE_API SQLITE_EXPERIMENTAL int sqlite3_snapshot_cmp( |
| 10144 | sqlite3_snapshot *p1, |
| 10145 | sqlite3_snapshot *p2 |
| 10146 | ); |
| 10147 | |
| 10148 | /* |
| 10149 | ** CAPI3REF: Recover snapshots from a wal file |
| 10150 | ** METHOD: sqlite3_snapshot |
| 10151 | ** |
| 10152 | ** If a [WAL file] remains on disk after all database connections close |
| 10153 | ** (either through the use of the [SQLITE_FCNTL_PERSIST_WAL] [file control] |
| 10154 | ** or because the last process to have the database opened exited without |
| 10155 | ** calling [sqlite3_close()]) and a new connection is subsequently opened |
| 10156 | ** on that database and [WAL file], the [sqlite3_snapshot_open()] interface |
| 10157 | ** will only be able to open the last transaction added to the WAL file |
| 10158 | ** even though the WAL file contains other valid transactions. |
| 10159 | ** |
| 10160 | ** This function attempts to scan the WAL file associated with database zDb |
| 10161 | ** of database handle db and make all valid snapshots available to |
| 10162 | ** sqlite3_snapshot_open(). It is an error if there is already a read |
| 10163 | ** transaction open on the database, or if the database is not a WAL mode |
| 10164 | ** database. |
| 10165 | ** |
| 10166 | ** SQLITE_OK is returned if successful, or an SQLite error code otherwise. |
| 10167 | ** |
| 10168 | ** This interface is only available if SQLite is compiled with the |
| 10169 | ** [SQLITE_ENABLE_SNAPSHOT] option. |
| 10170 | */ |
| 10171 | SQLITE_API SQLITE_EXPERIMENTAL int sqlite3_snapshot_recover(sqlite3 *db, const char *zDb); |
| 10172 | |
| 10173 | /* |
| 10174 | ** CAPI3REF: Serialize a database |
| @@ -18937,11 +18944,11 @@ | |
| 18944 | SQLITE_PRIVATE void sqlite3Reindex(Parse*, Token*, Token*); |
| 18945 | SQLITE_PRIVATE void sqlite3AlterFunctions(void); |
| 18946 | SQLITE_PRIVATE void sqlite3AlterRenameTable(Parse*, SrcList*, Token*); |
| 18947 | SQLITE_PRIVATE int sqlite3GetToken(const unsigned char *, int *); |
| 18948 | SQLITE_PRIVATE void sqlite3NestedParse(Parse*, const char*, ...); |
| 18949 | SQLITE_PRIVATE void sqlite3ExpirePreparedStatements(sqlite3*, int); |
| 18950 | SQLITE_PRIVATE int sqlite3CodeSubselect(Parse*, Expr *, int, int); |
| 18951 | SQLITE_PRIVATE void sqlite3SelectPrep(Parse*, Select*, NameContext*); |
| 18952 | SQLITE_PRIVATE void sqlite3SelectWrongNumTermsError(Parse *pParse, Select *p); |
| 18953 | SQLITE_PRIVATE int sqlite3MatchSpanName(const char*, const char*, const char*, const char*); |
| 18954 | SQLITE_PRIVATE int sqlite3ResolveExprNames(NameContext*, Expr*); |
| @@ -20025,13 +20032,13 @@ | |
| 20032 | #endif |
| 20033 | u16 nResColumn; /* Number of columns in one row of the result set */ |
| 20034 | u8 errorAction; /* Recovery action to do in case of an error */ |
| 20035 | u8 minWriteFileFormat; /* Minimum file format for writable database files */ |
| 20036 | u8 prepFlags; /* SQLITE_PREPARE_* flags */ |
| 20037 | bft expired:2; /* 1: recompile VM immediately 2: when convenient */ |
| 20038 | bft explain:2; /* True if EXPLAIN present on SQL command */ |
| 20039 | bft doingRerun:1; /* True if rerunning after an auto-reprepare */ |
| 20040 | bft changeCntOn:1; /* True to update the change-counter */ |
| 20041 | bft runOnlyOnce:1; /* Automatically expire on reset */ |
| 20042 | bft usesStmtJournal:1; /* True if uses a statement journal */ |
| 20043 | bft readOnly:1; /* True for statements that do not write */ |
| 20044 | bft bIsReader:1; /* True for statements that read */ |
| @@ -28640,11 +28647,11 @@ | |
| 28647 | sqlite3TreeViewLine(pView, "FUNCTION %Q", pExpr->u.zToken); |
| 28648 | } |
| 28649 | if( pFarg ){ |
| 28650 | sqlite3TreeViewExprList(pView, pFarg, pWin!=0, 0); |
| 28651 | } |
| 28652 | #ifndef SQLITE_OMIT_WINDOWFUNC |
| 28653 | if( pWin ){ |
| 28654 | sqlite3TreeViewWindow(pView, pWin, 0); |
| 28655 | } |
| 28656 | #endif |
| 28657 | break; |
| @@ -42948,10 +42955,13 @@ | |
| 42955 | */ |
| 42956 | static int winTruncate(sqlite3_file *id, sqlite3_int64 nByte){ |
| 42957 | winFile *pFile = (winFile*)id; /* File handle object */ |
| 42958 | int rc = SQLITE_OK; /* Return code for this function */ |
| 42959 | DWORD lastErrno; |
| 42960 | #if SQLITE_MAX_MMAP_SIZE>0 |
| 42961 | sqlite3_int64 oldMmapSize; |
| 42962 | #endif |
| 42963 | |
| 42964 | assert( pFile ); |
| 42965 | SimulateIOError(return SQLITE_IOERR_TRUNCATE); |
| 42966 | OSTRACE(("TRUNCATE pid=%lu, pFile=%p, file=%p, size=%lld, lock=%d\n", |
| 42967 | osGetCurrentProcessId(), pFile, pFile->h, nByte, pFile->locktype)); |
| @@ -42962,10 +42972,19 @@ | |
| 42972 | ** size). |
| 42973 | */ |
| 42974 | if( pFile->szChunk>0 ){ |
| 42975 | nByte = ((nByte + pFile->szChunk - 1)/pFile->szChunk) * pFile->szChunk; |
| 42976 | } |
| 42977 | |
| 42978 | #if SQLITE_MAX_MMAP_SIZE>0 |
| 42979 | if( pFile->pMapRegion ){ |
| 42980 | oldMmapSize = pFile->mmapSize; |
| 42981 | }else{ |
| 42982 | oldMmapSize = 0; |
| 42983 | } |
| 42984 | winUnmapfile(pFile); |
| 42985 | #endif |
| 42986 | |
| 42987 | /* SetEndOfFile() returns non-zero when successful, or zero when it fails. */ |
| 42988 | if( winSeekFile(pFile, nByte) ){ |
| 42989 | rc = winLogError(SQLITE_IOERR_TRUNCATE, pFile->lastErrno, |
| 42990 | "winTruncate1", pFile->zPath); |
| @@ -42975,16 +42994,16 @@ | |
| 42994 | rc = winLogError(SQLITE_IOERR_TRUNCATE, pFile->lastErrno, |
| 42995 | "winTruncate2", pFile->zPath); |
| 42996 | } |
| 42997 | |
| 42998 | #if SQLITE_MAX_MMAP_SIZE>0 |
| 42999 | if( rc==SQLITE_OK && oldMmapSize>0 ){ |
| 43000 | if( oldMmapSize>nByte ){ |
| 43001 | winMapfile(pFile, -1); |
| 43002 | }else{ |
| 43003 | winMapfile(pFile, oldMmapSize); |
| 43004 | } |
| 43005 | } |
| 43006 | #endif |
| 43007 | |
| 43008 | OSTRACE(("TRUNCATE pid=%lu, pFile=%p, file=%p, rc=%s\n", |
| 43009 | osGetCurrentProcessId(), pFile, pFile->h, sqlite3ErrName(rc))); |
| @@ -56461,22 +56480,22 @@ | |
| 56480 | |
| 56481 | if( (rc&0xFF)==SQLITE_IOERR && rc!=SQLITE_IOERR_NOMEM ){ |
| 56482 | rc = sqlite3JournalCreate(pPager->jfd); |
| 56483 | if( rc!=SQLITE_OK ){ |
| 56484 | sqlite3OsClose(pPager->jfd); |
| 56485 | goto commit_phase_one_exit; |
| 56486 | } |
| 56487 | bBatch = 0; |
| 56488 | }else{ |
| 56489 | sqlite3OsClose(pPager->jfd); |
| 56490 | } |
| 56491 | } |
| 56492 | #endif /* SQLITE_ENABLE_BATCH_ATOMIC_WRITE */ |
| 56493 | |
| 56494 | if( bBatch==0 ){ |
| 56495 | rc = pager_write_pagelist(pPager, pList); |
| 56496 | } |
| 56497 | if( rc!=SQLITE_OK ){ |
| 56498 | assert( rc!=SQLITE_IOERR_BLOCKED ); |
| 56499 | goto commit_phase_one_exit; |
| 56500 | } |
| 56501 | sqlite3PcacheCleanAll(pPager->pPCache); |
| @@ -65751,10 +65770,16 @@ | |
| 65770 | */ |
| 65771 | if( p->inTrans==TRANS_WRITE || (p->inTrans==TRANS_READ && !wrflag) ){ |
| 65772 | goto trans_begun; |
| 65773 | } |
| 65774 | assert( pBt->inTransaction==TRANS_WRITE || IfNotOmitAV(pBt->bDoTruncate)==0 ); |
| 65775 | |
| 65776 | if( (p->db->flags & SQLITE_ResetDatabase) |
| 65777 | && sqlite3PagerIsreadonly(pBt->pPager)==0 |
| 65778 | ){ |
| 65779 | pBt->btsFlags &= ~BTS_READ_ONLY; |
| 65780 | } |
| 65781 | |
| 65782 | /* Write transactions are not possible on a read-only database */ |
| 65783 | if( (pBt->btsFlags & BTS_READ_ONLY)!=0 && wrflag ){ |
| 65784 | rc = SQLITE_READONLY; |
| 65785 | goto trans_begun; |
| @@ -71767,12 +71792,11 @@ | |
| 71792 | ** if this is the first reference to the page. |
| 71793 | ** |
| 71794 | ** Also check that the page number is in bounds. |
| 71795 | */ |
| 71796 | static int checkRef(IntegrityCk *pCheck, Pgno iPage){ |
| 71797 | if( iPage>pCheck->nPage || iPage==0 ){ |
| 71798 | checkAppendMsg(pCheck, "invalid page number %d", iPage); |
| 71799 | return 1; |
| 71800 | } |
| 71801 | if( getPageReferenced(pCheck, iPage) ){ |
| 71802 | checkAppendMsg(pCheck, "2nd reference to page %d", iPage); |
| @@ -71823,21 +71847,16 @@ | |
| 71847 | int iPage, /* Page number for first page in the list */ |
| 71848 | int N /* Expected number of pages in the list */ |
| 71849 | ){ |
| 71850 | int i; |
| 71851 | int expected = N; |
| 71852 | int nErrAtStart = pCheck->nErr; |
| 71853 | while( iPage!=0 && pCheck->mxErr ){ |
| 71854 | DbPage *pOvflPage; |
| 71855 | unsigned char *pOvflData; |
| 71856 | if( checkRef(pCheck, iPage) ) break; |
| 71857 | N--; |
| 71858 | if( sqlite3PagerGet(pCheck->pPager, (Pgno)iPage, &pOvflPage, 0) ){ |
| 71859 | checkAppendMsg(pCheck, "failed to get page %d", iPage); |
| 71860 | break; |
| 71861 | } |
| 71862 | pOvflData = (unsigned char *)sqlite3PagerGetData(pOvflPage); |
| @@ -71877,14 +71896,16 @@ | |
| 71896 | } |
| 71897 | } |
| 71898 | #endif |
| 71899 | iPage = get4byte(pOvflData); |
| 71900 | sqlite3PagerUnref(pOvflPage); |
| 71901 | } |
| 71902 | if( N && nErrAtStart==pCheck->nErr ){ |
| 71903 | checkAppendMsg(pCheck, |
| 71904 | "%s is %d but should be %d", |
| 71905 | isFreeList ? "size" : "overflow list length", |
| 71906 | expected-N, expected); |
| 71907 | } |
| 71908 | } |
| 71909 | #endif /* SQLITE_OMIT_INTEGRITY_CHECK */ |
| 71910 | |
| 71911 | /* |
| @@ -72274,10 +72295,28 @@ | |
| 72295 | get4byte(&pBt->pPage1->aData[36])); |
| 72296 | sCheck.zPfx = 0; |
| 72297 | |
| 72298 | /* Check all the tables. |
| 72299 | */ |
| 72300 | #ifndef SQLITE_OMIT_AUTOVACUUM |
| 72301 | if( pBt->autoVacuum ){ |
| 72302 | int mx = 0; |
| 72303 | int mxInHdr; |
| 72304 | for(i=0; (int)i<nRoot; i++) if( mx<aRoot[i] ) mx = aRoot[i]; |
| 72305 | mxInHdr = get4byte(&pBt->pPage1->aData[52]); |
| 72306 | if( mx!=mxInHdr ){ |
| 72307 | checkAppendMsg(&sCheck, |
| 72308 | "max rootpage (%d) disagrees with header (%d)", |
| 72309 | mx, mxInHdr |
| 72310 | ); |
| 72311 | } |
| 72312 | }else if( get4byte(&pBt->pPage1->aData[64])!=0 ){ |
| 72313 | checkAppendMsg(&sCheck, |
| 72314 | "incremental_vacuum enabled with a max rootpage of zero" |
| 72315 | ); |
| 72316 | } |
| 72317 | #endif |
| 72318 | testcase( pBt->db->flags & SQLITE_CellSizeCk ); |
| 72319 | pBt->db->flags &= ~SQLITE_CellSizeCk; |
| 72320 | for(i=0; (int)i<nRoot && sCheck.mxErr; i++){ |
| 72321 | i64 notUsed; |
| 72322 | if( aRoot[i]==0 ) continue; |
| @@ -79974,15 +80013,23 @@ | |
| 80013 | ** An expired statement means that recompilation of the statement is |
| 80014 | ** recommend. Statements expire when things happen that make their |
| 80015 | ** programs obsolete. Removing user-defined functions or collating |
| 80016 | ** sequences, or changing an authorization function are the types of |
| 80017 | ** things that make prepared statements obsolete. |
| 80018 | ** |
| 80019 | ** If iCode is 1, then expiration is advisory. The statement should |
| 80020 | ** be reprepared before being restarted, but if it is already running |
| 80021 | ** it is allowed to run to completion. |
| 80022 | ** |
| 80023 | ** Internally, this function just sets the Vdbe.expired flag on all |
| 80024 | ** prepared statements. The flag is set to 1 for an immediate expiration |
| 80025 | ** and set to 2 for an advisory expiration. |
| 80026 | */ |
| 80027 | SQLITE_PRIVATE void sqlite3ExpirePreparedStatements(sqlite3 *db, int iCode){ |
| 80028 | Vdbe *p; |
| 80029 | for(p = db->pVdbe; p; p=p->pNext){ |
| 80030 | p->expired = iCode+1; |
| 80031 | } |
| 80032 | } |
| 80033 | |
| 80034 | /* |
| 80035 | ** Return the database associated with the Vdbe. |
| @@ -85520,11 +85567,11 @@ | |
| 85567 | if( rc!=SQLITE_OK ){ |
| 85568 | goto abort_due_to_error; |
| 85569 | } |
| 85570 | } |
| 85571 | if( isSchemaChange ){ |
| 85572 | sqlite3ExpirePreparedStatements(db, 0); |
| 85573 | sqlite3ResetAllSchemasOfConnection(db); |
| 85574 | db->mDbFlags |= DBFLAG_SchemaChange; |
| 85575 | } |
| 85576 | } |
| 85577 | |
| @@ -85809,11 +85856,11 @@ | |
| 85856 | pDb->pSchema->file_format = pOp->p3; |
| 85857 | } |
| 85858 | if( pOp->p1==1 ){ |
| 85859 | /* Invalidate all prepared statements whenever the TEMP database |
| 85860 | ** schema is changed. Ticket #1644 */ |
| 85861 | sqlite3ExpirePreparedStatements(db, 0); |
| 85862 | p->expired = 0; |
| 85863 | } |
| 85864 | if( rc ) goto abort_due_to_error; |
| 85865 | break; |
| 85866 | } |
| @@ -85927,11 +85974,11 @@ | |
| 85974 | assert( pOp->opcode==OP_OpenWrite || pOp->p5==0 || pOp->p5==OPFLAG_SEEKEQ ); |
| 85975 | assert( p->bIsReader ); |
| 85976 | assert( pOp->opcode==OP_OpenRead || pOp->opcode==OP_ReopenIdx |
| 85977 | || p->readOnly==0 ); |
| 85978 | |
| 85979 | if( p->expired==1 ){ |
| 85980 | rc = SQLITE_ABORT_ROLLBACK; |
| 85981 | goto abort_due_to_error; |
| 85982 | } |
| 85983 | |
| 85984 | nField = 0; |
| @@ -89108,25 +89155,32 @@ | |
| 89155 | } |
| 89156 | break; |
| 89157 | } |
| 89158 | #endif |
| 89159 | |
| 89160 | /* Opcode: Expire P1 P2 * * * |
| 89161 | ** |
| 89162 | ** Cause precompiled statements to expire. When an expired statement |
| 89163 | ** is executed using sqlite3_step() it will either automatically |
| 89164 | ** reprepare itself (if it was originally created using sqlite3_prepare_v2()) |
| 89165 | ** or it will fail with SQLITE_SCHEMA. |
| 89166 | ** |
| 89167 | ** If P1 is 0, then all SQL statements become expired. If P1 is non-zero, |
| 89168 | ** then only the currently executing statement is expired. |
| 89169 | ** |
| 89170 | ** If P2 is 0, then SQL statements are expired immediately. If P2 is 1, |
| 89171 | ** then running SQL statements are allowed to continue to run to completion. |
| 89172 | ** The P2==1 case occurs when a CREATE INDEX or similar schema change happens |
| 89173 | ** that might help the statement run faster but which does not affect the |
| 89174 | ** correctness of operation. |
| 89175 | */ |
| 89176 | case OP_Expire: { |
| 89177 | assert( pOp->p2==0 || pOp->p2==1 ); |
| 89178 | if( !pOp->p1 ){ |
| 89179 | sqlite3ExpirePreparedStatements(db, pOp->p2); |
| 89180 | }else{ |
| 89181 | p->expired = pOp->p2+1; |
| 89182 | } |
| 89183 | break; |
| 89184 | } |
| 89185 | |
| 89186 | #ifndef SQLITE_OMIT_SHARED_CACHE |
| @@ -103813,11 +103867,11 @@ | |
| 103867 | if( !pIdx->hasStat1 ) sqlite3DefaultRowEst(pIdx); |
| 103868 | } |
| 103869 | |
| 103870 | /* Load the statistics from the sqlite_stat4 table. */ |
| 103871 | #ifdef SQLITE_ENABLE_STAT3_OR_STAT4 |
| 103872 | if( rc==SQLITE_OK ){ |
| 103873 | db->lookaside.bDisable++; |
| 103874 | rc = loadStat4(db, sInfo.zDatabase); |
| 103875 | db->lookaside.bDisable--; |
| 103876 | } |
| 103877 | for(i=sqliteHashFirst(&pSchema->idxHash); i; i=sqliteHashNext(i)){ |
| @@ -104545,11 +104599,11 @@ | |
| 104599 | if( !sqlite3SafetyCheckOk(db) ) return SQLITE_MISUSE_BKPT; |
| 104600 | #endif |
| 104601 | sqlite3_mutex_enter(db->mutex); |
| 104602 | db->xAuth = (sqlite3_xauth)xAuth; |
| 104603 | db->pAuthArg = pArg; |
| 104604 | sqlite3ExpirePreparedStatements(db, 0); |
| 104605 | sqlite3_mutex_leave(db->mutex); |
| 104606 | return SQLITE_OK; |
| 104607 | } |
| 104608 | |
| 104609 | /* |
| @@ -108209,11 +108263,11 @@ | |
| 108263 | if( pTblName ){ |
| 108264 | sqlite3RefillIndex(pParse, pIndex, iMem); |
| 108265 | sqlite3ChangeCookie(pParse, iDb); |
| 108266 | sqlite3VdbeAddParseSchemaOp(v, iDb, |
| 108267 | sqlite3MPrintf(db, "name='%q' AND type='index'", pIndex->zName)); |
| 108268 | sqlite3VdbeAddOp2(v, OP_Expire, 0, 1); |
| 108269 | } |
| 108270 | |
| 108271 | sqlite3VdbeJumpHere(v, pIndex->tnum); |
| 108272 | } |
| 108273 | |
| @@ -131894,11 +131948,11 @@ | |
| 131948 | |
| 131949 | assert( sqlite3BtreeHoldsAllMutexes(db) ); |
| 131950 | assert( sqlite3_mutex_held(db->mutex) ); |
| 131951 | |
| 131952 | if( p ){ |
| 131953 | sqlite3ExpirePreparedStatements(db, 0); |
| 131954 | do { |
| 131955 | VTable *pNext = p->pNext; |
| 131956 | sqlite3VtabUnlock(p); |
| 131957 | p = pNext; |
| 131958 | }while( p ); |
| @@ -138680,11 +138734,13 @@ | |
| 138734 | |
| 138735 | #ifdef SQLITE_ENABLE_STAT3_OR_STAT4 |
| 138736 | Index *p = pLoop->u.btree.pIndex; |
| 138737 | int nEq = pLoop->u.btree.nEq; |
| 138738 | |
| 138739 | if( p->nSample>0 && nEq<p->nSampleCol |
| 138740 | && OptimizationEnabled(pParse->db, SQLITE_Stat34) |
| 138741 | ){ |
| 138742 | if( nEq==pBuilder->nRecValid ){ |
| 138743 | UnpackedRecord *pRec = pBuilder->pRec; |
| 138744 | tRowcnt a[2]; |
| 138745 | int nBtm = pLoop->u.btree.nBtm; |
| 138746 | int nTop = pLoop->u.btree.nTop; |
| @@ -139828,10 +139884,11 @@ | |
| 139884 | tRowcnt nOut = 0; |
| 139885 | if( nInMul==0 |
| 139886 | && pProbe->nSample |
| 139887 | && pNew->u.btree.nEq<=pProbe->nSampleCol |
| 139888 | && ((eOp & WO_IN)==0 || !ExprHasProperty(pTerm->pExpr, EP_xIsSelect)) |
| 139889 | && OptimizationEnabled(db, SQLITE_Stat34) |
| 139890 | ){ |
| 139891 | Expr *pExpr = pTerm->pExpr; |
| 139892 | if( (eOp & (WO_EQ|WO_ISNULL|WO_IS))!=0 ){ |
| 139893 | testcase( eOp & WO_EQ ); |
| 139894 | testcase( eOp & WO_IS ); |
| @@ -146916,11 +146973,11 @@ | |
| 146973 | |
| 146974 | /* |
| 146975 | ** Find the appropriate action for a parser given the non-terminal |
| 146976 | ** look-ahead token iLookAhead. |
| 146977 | */ |
| 146978 | static YYACTIONTYPE yy_find_reduce_action( |
| 146979 | YYACTIONTYPE stateno, /* Current state number */ |
| 146980 | YYCODETYPE iLookAhead /* The look-ahead token */ |
| 146981 | ){ |
| 146982 | int i; |
| 146983 | #ifdef YYERRORSYMBOL |
| @@ -147421,11 +147478,11 @@ | |
| 147478 | int yyLookahead, /* Lookahead token, or YYNOCODE if none */ |
| 147479 | sqlite3ParserTOKENTYPE yyLookaheadToken /* Value of the lookahead token */ |
| 147480 | sqlite3ParserCTX_PDECL /* %extra_context */ |
| 147481 | ){ |
| 147482 | int yygoto; /* The next state */ |
| 147483 | YYACTIONTYPE yyact; /* The next action */ |
| 147484 | yyStackEntry *yymsp; /* The top of the parser's stack */ |
| 147485 | int yysize; /* Amount to pop the stack */ |
| 147486 | sqlite3ParserARG_FETCH |
| 147487 | (void)yyLookahead; |
| 147488 | (void)yyLookaheadToken; |
| @@ -148980,16 +149037,16 @@ | |
| 149037 | } |
| 149038 | #endif |
| 149039 | |
| 149040 | do{ |
| 149041 | assert( yyact==yypParser->yytos->stateno ); |
| 149042 | yyact = yy_find_shift_action((YYCODETYPE)yymajor,yyact); |
| 149043 | if( yyact >= YY_MIN_REDUCE ){ |
| 149044 | yyact = yy_reduce(yypParser,yyact-YY_MIN_REDUCE,yymajor, |
| 149045 | yyminor sqlite3ParserCTX_PARAM); |
| 149046 | }else if( yyact <= YY_MAX_SHIFTREDUCE ){ |
| 149047 | yy_shift(yypParser,yyact,(YYCODETYPE)yymajor,yyminor); |
| 149048 | #ifndef YYNOERRORRECOVERY |
| 149049 | yypParser->yyerrcnt--; |
| 149050 | #endif |
| 149051 | break; |
| 149052 | }else if( yyact==YY_ACCEPT_ACTION ){ |
| @@ -151421,11 +151478,11 @@ | |
| 151478 | db->flags |= aFlagOp[i].mask; |
| 151479 | }else if( onoff==0 ){ |
| 151480 | db->flags &= ~aFlagOp[i].mask; |
| 151481 | } |
| 151482 | if( oldFlags!=db->flags ){ |
| 151483 | sqlite3ExpirePreparedStatements(db, 0); |
| 151484 | } |
| 151485 | if( pRes ){ |
| 151486 | *pRes = (db->flags & aFlagOp[i].mask)!=0; |
| 151487 | } |
| 151488 | rc = SQLITE_OK; |
| @@ -151864,11 +151921,11 @@ | |
| 151921 | } |
| 151922 | sqlite3VtabRollback(db); |
| 151923 | sqlite3EndBenignMalloc(); |
| 151924 | |
| 151925 | if( schemaChange ){ |
| 151926 | sqlite3ExpirePreparedStatements(db, 0); |
| 151927 | sqlite3ResetAllSchemasOfConnection(db); |
| 151928 | } |
| 151929 | sqlite3BtreeLeaveAll(db); |
| 151930 | |
| 151931 | /* Any deferred constraint violations have now been resolved. */ |
| @@ -152319,11 +152376,11 @@ | |
| 152376 | sqlite3ErrorWithMsg(db, SQLITE_BUSY, |
| 152377 | "unable to delete/modify user-function due to active statements"); |
| 152378 | assert( !db->mallocFailed ); |
| 152379 | return SQLITE_BUSY; |
| 152380 | }else{ |
| 152381 | sqlite3ExpirePreparedStatements(db, 0); |
| 152382 | } |
| 152383 | } |
| 152384 | |
| 152385 | p = sqlite3FindFunction(db, zFunctionName, nArg, (u8)enc, 1); |
| 152386 | assert(p || db->mallocFailed); |
| @@ -153094,11 +153151,11 @@ | |
| 153151 | if( db->nVdbeActive ){ |
| 153152 | sqlite3ErrorWithMsg(db, SQLITE_BUSY, |
| 153153 | "unable to delete/modify collation sequence due to active statements"); |
| 153154 | return SQLITE_BUSY; |
| 153155 | } |
| 153156 | sqlite3ExpirePreparedStatements(db, 0); |
| 153157 | |
| 153158 | /* If collation sequence pColl was created directly by a call to |
| 153159 | ** sqlite3_create_collation, and not generated by synthCollSeq(), |
| 153160 | ** then any copies made by synthCollSeq() need to be invalidated. |
| 153161 | ** Also, collation destructor - CollSeq.xDel() - function may need |
| @@ -194031,16 +194088,18 @@ | |
| 194088 | rc = sqlite3_create_function(db, aFunc[i].zName, aFunc[i].nArg, |
| 194089 | SQLITE_UTF8 | SQLITE_DETERMINISTIC, |
| 194090 | (void*)&aFunc[i].flag, |
| 194091 | aFunc[i].xFunc, 0, 0); |
| 194092 | } |
| 194093 | #ifndef SQLITE_OMIT_WINDOWFUNC |
| 194094 | for(i=0; i<sizeof(aAgg)/sizeof(aAgg[0]) && rc==SQLITE_OK; i++){ |
| 194095 | rc = sqlite3_create_window_function(db, aAgg[i].zName, aAgg[i].nArg, |
| 194096 | SQLITE_UTF8 | SQLITE_DETERMINISTIC, 0, |
| 194097 | aAgg[i].xStep, aAgg[i].xFinal, |
| 194098 | aAgg[i].xValue, jsonGroupInverse, 0); |
| 194099 | } |
| 194100 | #endif |
| 194101 | #ifndef SQLITE_OMIT_VIRTUALTABLE |
| 194102 | for(i=0; i<sizeof(aMod)/sizeof(aMod[0]) && rc==SQLITE_OK; i++){ |
| 194103 | rc = sqlite3_create_module(db, aMod[i].zName, aMod[i].pModule, 0); |
| 194104 | } |
| 194105 | #endif |
| @@ -196237,11 +196296,11 @@ | |
| 196296 | |
| 196297 | /* |
| 196298 | ** Find the appropriate action for a parser given the non-terminal |
| 196299 | ** look-ahead token iLookAhead. |
| 196300 | */ |
| 196301 | static fts5YYACTIONTYPE fts5yy_find_reduce_action( |
| 196302 | fts5YYACTIONTYPE stateno, /* Current state number */ |
| 196303 | fts5YYCODETYPE iLookAhead /* The look-ahead token */ |
| 196304 | ){ |
| 196305 | int i; |
| 196306 | #ifdef fts5YYERRORSYMBOL |
| @@ -196405,11 +196464,11 @@ | |
| 196464 | int fts5yyLookahead, /* Lookahead token, or fts5YYNOCODE if none */ |
| 196465 | sqlite3Fts5ParserFTS5TOKENTYPE fts5yyLookaheadToken /* Value of the lookahead token */ |
| 196466 | sqlite3Fts5ParserCTX_PDECL /* %extra_context */ |
| 196467 | ){ |
| 196468 | int fts5yygoto; /* The next state */ |
| 196469 | fts5YYACTIONTYPE fts5yyact; /* The next action */ |
| 196470 | fts5yyStackEntry *fts5yymsp; /* The top of the parser's stack */ |
| 196471 | int fts5yysize; /* Amount to pop the stack */ |
| 196472 | sqlite3Fts5ParserARG_FETCH |
| 196473 | (void)fts5yyLookahead; |
| 196474 | (void)fts5yyLookaheadToken; |
| @@ -196761,16 +196820,16 @@ | |
| 196820 | } |
| 196821 | #endif |
| 196822 | |
| 196823 | do{ |
| 196824 | assert( fts5yyact==fts5yypParser->fts5yytos->stateno ); |
| 196825 | fts5yyact = fts5yy_find_shift_action((fts5YYCODETYPE)fts5yymajor,fts5yyact); |
| 196826 | if( fts5yyact >= fts5YY_MIN_REDUCE ){ |
| 196827 | fts5yyact = fts5yy_reduce(fts5yypParser,fts5yyact-fts5YY_MIN_REDUCE,fts5yymajor, |
| 196828 | fts5yyminor sqlite3Fts5ParserCTX_PARAM); |
| 196829 | }else if( fts5yyact <= fts5YY_MAX_SHIFTREDUCE ){ |
| 196830 | fts5yy_shift(fts5yypParser,fts5yyact,(fts5YYCODETYPE)fts5yymajor,fts5yyminor); |
| 196831 | #ifndef fts5YYNOERRORRECOVERY |
| 196832 | fts5yypParser->fts5yyerrcnt--; |
| 196833 | #endif |
| 196834 | break; |
| 196835 | }else if( fts5yyact==fts5YY_ACCEPT_ACTION ){ |
| @@ -211513,11 +211572,11 @@ | |
| 211572 | int nArg, /* Number of args */ |
| 211573 | sqlite3_value **apUnused /* Function arguments */ |
| 211574 | ){ |
| 211575 | assert( nArg==0 ); |
| 211576 | UNUSED_PARAM2(nArg, apUnused); |
| 211577 | sqlite3_result_text(pCtx, "fts5: 2018-07-24 22:02:12 2bd593332da0aade467e7a4ee89e966aa6302f37540a2c5e23671f98a6cb599c", -1, SQLITE_TRANSIENT); |
| 211578 | } |
| 211579 | |
| 211580 | static int fts5Init(sqlite3 *db){ |
| 211581 | static const sqlite3_module fts5Mod = { |
| 211582 | /* iVersion */ 2, |
| @@ -214799,11 +214858,11 @@ | |
| 214858 | int iTbl = 0; |
| 214859 | while( i<128 ){ |
| 214860 | int bToken = aArray[ aFts5UnicodeData[iTbl] & 0x1F ]; |
| 214861 | int n = (aFts5UnicodeData[iTbl] >> 5) + i; |
| 214862 | for(; i<128 && i<n; i++){ |
| 214863 | aAscii[i] = (u8)bToken; |
| 214864 | } |
| 214865 | iTbl++; |
| 214866 | } |
| 214867 | } |
| 214868 | |
| @@ -216223,12 +216282,12 @@ | |
| 216282 | } |
| 216283 | #endif /* SQLITE_CORE */ |
| 216284 | #endif /* !defined(SQLITE_CORE) || defined(SQLITE_ENABLE_STMTVTAB) */ |
| 216285 | |
| 216286 | /************** End of stmt.c ************************************************/ |
| 216287 | #if __LINE__!=216287 |
| 216288 | #undef SQLITE_SOURCE_ID |
| 216289 | #define SQLITE_SOURCE_ID "2018-07-24 22:02:12 2bd593332da0aade467e7a4ee89e966aa6302f37540a2c5e23671f98a6cbalt2" |
| 216290 | #endif |
| 216291 | /* Return the source-id for this library */ |
| 216292 | SQLITE_API const char *sqlite3_sourceid(void){ return SQLITE_SOURCE_ID; } |
| 216293 | /************************** End of sqlite3.c ******************************/ |
| 216294 |
+28
-27
| --- src/sqlite3.h | ||
| +++ src/sqlite3.h | ||
| @@ -123,11 +123,11 @@ | ||
| 123 | 123 | ** [sqlite3_libversion_number()], [sqlite3_sourceid()], |
| 124 | 124 | ** [sqlite_version()] and [sqlite_source_id()]. |
| 125 | 125 | */ |
| 126 | 126 | #define SQLITE_VERSION "3.25.0" |
| 127 | 127 | #define SQLITE_VERSION_NUMBER 3025000 |
| 128 | -#define SQLITE_SOURCE_ID "2018-07-18 19:09:07 a5087c5c87ad65f92e3bc96bbc84afb43faf10ab6b9ed3ba16304b5c60ad069f" | |
| 128 | +#define SQLITE_SOURCE_ID "2018-07-24 22:02:12 2bd593332da0aade467e7a4ee89e966aa6302f37540a2c5e23671f98a6cb599c" | |
| 129 | 129 | |
| 130 | 130 | /* |
| 131 | 131 | ** CAPI3REF: Run-Time Library Version Numbers |
| 132 | 132 | ** KEYWORDS: sqlite3_version sqlite3_sourceid |
| 133 | 133 | ** |
| @@ -885,11 +885,12 @@ | ||
| 885 | 885 | ** interrogated. The zDbName parameter is ignored. |
| 886 | 886 | ** |
| 887 | 887 | ** <li>[[SQLITE_FCNTL_PERSIST_WAL]] |
| 888 | 888 | ** ^The [SQLITE_FCNTL_PERSIST_WAL] opcode is used to set or query the |
| 889 | 889 | ** persistent [WAL | Write Ahead Log] setting. By default, the auxiliary |
| 890 | -** write ahead log and shared memory files used for transaction control | |
| 890 | +** write ahead log ([WAL file]) and shared memory | |
| 891 | +** files used for transaction control | |
| 891 | 892 | ** are automatically deleted when the latest connection to the database |
| 892 | 893 | ** closes. Setting persistent WAL mode causes those files to persist after |
| 893 | 894 | ** close. Persisting the files is useful when other processes that do not |
| 894 | 895 | ** have write permission on the directory containing the database file want |
| 895 | 896 | ** to read the database file, as the WAL and shared memory files must exist |
| @@ -8958,11 +8959,10 @@ | ||
| 8958 | 8959 | SQLITE_API int sqlite3_system_errno(sqlite3*); |
| 8959 | 8960 | |
| 8960 | 8961 | /* |
| 8961 | 8962 | ** CAPI3REF: Database Snapshot |
| 8962 | 8963 | ** KEYWORDS: {snapshot} {sqlite3_snapshot} |
| 8963 | -** EXPERIMENTAL | |
| 8964 | 8964 | ** |
| 8965 | 8965 | ** An instance of the snapshot object records the state of a [WAL mode] |
| 8966 | 8966 | ** database for some specific point in history. |
| 8967 | 8967 | ** |
| 8968 | 8968 | ** In [WAL mode], multiple [database connections] that are open on the |
| @@ -8975,23 +8975,18 @@ | ||
| 8975 | 8975 | ** |
| 8976 | 8976 | ** The sqlite3_snapshot object records state information about an historical |
| 8977 | 8977 | ** version of the database file so that it is possible to later open a new read |
| 8978 | 8978 | ** transaction that sees that historical version of the database rather than |
| 8979 | 8979 | ** the most recent version. |
| 8980 | -** | |
| 8981 | -** The constructor for this object is [sqlite3_snapshot_get()]. The | |
| 8982 | -** [sqlite3_snapshot_open()] method causes a fresh read transaction to refer | |
| 8983 | -** to an historical snapshot (if possible). The destructor for | |
| 8984 | -** sqlite3_snapshot objects is [sqlite3_snapshot_free()]. | |
| 8985 | 8980 | */ |
| 8986 | 8981 | typedef struct sqlite3_snapshot { |
| 8987 | 8982 | unsigned char hidden[48]; |
| 8988 | 8983 | } sqlite3_snapshot; |
| 8989 | 8984 | |
| 8990 | 8985 | /* |
| 8991 | 8986 | ** CAPI3REF: Record A Database Snapshot |
| 8992 | -** EXPERIMENTAL | |
| 8987 | +** CONSTRUCTOR: sqlite3_snapshot | |
| 8993 | 8988 | ** |
| 8994 | 8989 | ** ^The [sqlite3_snapshot_get(D,S,P)] interface attempts to make a |
| 8995 | 8990 | ** new [sqlite3_snapshot] object that records the current state of |
| 8996 | 8991 | ** schema S in database connection D. ^On success, the |
| 8997 | 8992 | ** [sqlite3_snapshot_get(D,S,P)] interface writes a pointer to the newly |
| @@ -9026,21 +9021,21 @@ | ||
| 9026 | 9021 | ** The [sqlite3_snapshot] object returned from a successful call to |
| 9027 | 9022 | ** [sqlite3_snapshot_get()] must be freed using [sqlite3_snapshot_free()] |
| 9028 | 9023 | ** to avoid a memory leak. |
| 9029 | 9024 | ** |
| 9030 | 9025 | ** The [sqlite3_snapshot_get()] interface is only available when the |
| 9031 | -** SQLITE_ENABLE_SNAPSHOT compile-time option is used. | |
| 9026 | +** [SQLITE_ENABLE_SNAPSHOT] compile-time option is used. | |
| 9032 | 9027 | */ |
| 9033 | 9028 | SQLITE_API SQLITE_EXPERIMENTAL int sqlite3_snapshot_get( |
| 9034 | 9029 | sqlite3 *db, |
| 9035 | 9030 | const char *zSchema, |
| 9036 | 9031 | sqlite3_snapshot **ppSnapshot |
| 9037 | 9032 | ); |
| 9038 | 9033 | |
| 9039 | 9034 | /* |
| 9040 | 9035 | ** CAPI3REF: Start a read transaction on an historical snapshot |
| 9041 | -** EXPERIMENTAL | |
| 9036 | +** METHOD: sqlite3_snapshot | |
| 9042 | 9037 | ** |
| 9043 | 9038 | ** ^The [sqlite3_snapshot_open(D,S,P)] interface starts a |
| 9044 | 9039 | ** read transaction for schema S of |
| 9045 | 9040 | ** [database connection] D such that the read transaction |
| 9046 | 9041 | ** refers to historical [snapshot] P, rather than the most |
| @@ -9064,34 +9059,34 @@ | ||
| 9064 | 9059 | ** after the most recent I/O on the database connection.)^ |
| 9065 | 9060 | ** (Hint: Run "[PRAGMA application_id]" against a newly opened |
| 9066 | 9061 | ** database connection in order to make it ready to use snapshots.) |
| 9067 | 9062 | ** |
| 9068 | 9063 | ** The [sqlite3_snapshot_open()] interface is only available when the |
| 9069 | -** SQLITE_ENABLE_SNAPSHOT compile-time option is used. | |
| 9064 | +** [SQLITE_ENABLE_SNAPSHOT] compile-time option is used. | |
| 9070 | 9065 | */ |
| 9071 | 9066 | SQLITE_API SQLITE_EXPERIMENTAL int sqlite3_snapshot_open( |
| 9072 | 9067 | sqlite3 *db, |
| 9073 | 9068 | const char *zSchema, |
| 9074 | 9069 | sqlite3_snapshot *pSnapshot |
| 9075 | 9070 | ); |
| 9076 | 9071 | |
| 9077 | 9072 | /* |
| 9078 | 9073 | ** CAPI3REF: Destroy a snapshot |
| 9079 | -** EXPERIMENTAL | |
| 9074 | +** DESTRUCTOR: sqlite3_snapshot | |
| 9080 | 9075 | ** |
| 9081 | 9076 | ** ^The [sqlite3_snapshot_free(P)] interface destroys [sqlite3_snapshot] P. |
| 9082 | 9077 | ** The application must eventually free every [sqlite3_snapshot] object |
| 9083 | 9078 | ** using this routine to avoid a memory leak. |
| 9084 | 9079 | ** |
| 9085 | 9080 | ** The [sqlite3_snapshot_free()] interface is only available when the |
| 9086 | -** SQLITE_ENABLE_SNAPSHOT compile-time option is used. | |
| 9081 | +** [SQLITE_ENABLE_SNAPSHOT] compile-time option is used. | |
| 9087 | 9082 | */ |
| 9088 | 9083 | SQLITE_API SQLITE_EXPERIMENTAL void sqlite3_snapshot_free(sqlite3_snapshot*); |
| 9089 | 9084 | |
| 9090 | 9085 | /* |
| 9091 | 9086 | ** CAPI3REF: Compare the ages of two snapshot handles. |
| 9092 | -** EXPERIMENTAL | |
| 9087 | +** METHOD: sqlite3_snapshot | |
| 9093 | 9088 | ** |
| 9094 | 9089 | ** The sqlite3_snapshot_cmp(P1, P2) interface is used to compare the ages |
| 9095 | 9090 | ** of two valid snapshot handles. |
| 9096 | 9091 | ** |
| 9097 | 9092 | ** If the two snapshot handles are not associated with the same database |
| @@ -9106,35 +9101,41 @@ | ||
| 9106 | 9101 | ** is undefined. |
| 9107 | 9102 | ** |
| 9108 | 9103 | ** Otherwise, this API returns a negative value if P1 refers to an older |
| 9109 | 9104 | ** snapshot than P2, zero if the two handles refer to the same database |
| 9110 | 9105 | ** snapshot, and a positive value if P1 is a newer snapshot than P2. |
| 9106 | +** | |
| 9107 | +** This interface is only available if SQLite is compiled with the | |
| 9108 | +** [SQLITE_ENABLE_SNAPSHOT] option. | |
| 9111 | 9109 | */ |
| 9112 | 9110 | SQLITE_API SQLITE_EXPERIMENTAL int sqlite3_snapshot_cmp( |
| 9113 | 9111 | sqlite3_snapshot *p1, |
| 9114 | 9112 | sqlite3_snapshot *p2 |
| 9115 | 9113 | ); |
| 9116 | 9114 | |
| 9117 | 9115 | /* |
| 9118 | 9116 | ** CAPI3REF: Recover snapshots from a wal file |
| 9119 | -** EXPERIMENTAL | |
| 9120 | -** | |
| 9121 | -** If all connections disconnect from a database file but do not perform | |
| 9122 | -** a checkpoint, the existing wal file is opened along with the database | |
| 9123 | -** file the next time the database is opened. At this point it is only | |
| 9124 | -** possible to successfully call sqlite3_snapshot_open() to open the most | |
| 9125 | -** recent snapshot of the database (the one at the head of the wal file), | |
| 9126 | -** even though the wal file may contain other valid snapshots for which | |
| 9127 | -** clients have sqlite3_snapshot handles. | |
| 9128 | -** | |
| 9129 | -** This function attempts to scan the wal file associated with database zDb | |
| 9117 | +** METHOD: sqlite3_snapshot | |
| 9118 | +** | |
| 9119 | +** If a [WAL file] remains on disk after all database connections close | |
| 9120 | +** (either through the use of the [SQLITE_FCNTL_PERSIST_WAL] [file control] | |
| 9121 | +** or because the last process to have the database opened exited without | |
| 9122 | +** calling [sqlite3_close()]) and a new connection is subsequently opened | |
| 9123 | +** on that database and [WAL file], the [sqlite3_snapshot_open()] interface | |
| 9124 | +** will only be able to open the last transaction added to the WAL file | |
| 9125 | +** even though the WAL file contains other valid transactions. | |
| 9126 | +** | |
| 9127 | +** This function attempts to scan the WAL file associated with database zDb | |
| 9130 | 9128 | ** of database handle db and make all valid snapshots available to |
| 9131 | 9129 | ** sqlite3_snapshot_open(). It is an error if there is already a read |
| 9132 | -** transaction open on the database, or if the database is not a wal mode | |
| 9130 | +** transaction open on the database, or if the database is not a WAL mode | |
| 9133 | 9131 | ** database. |
| 9134 | 9132 | ** |
| 9135 | 9133 | ** SQLITE_OK is returned if successful, or an SQLite error code otherwise. |
| 9134 | +** | |
| 9135 | +** This interface is only available if SQLite is compiled with the | |
| 9136 | +** [SQLITE_ENABLE_SNAPSHOT] option. | |
| 9136 | 9137 | */ |
| 9137 | 9138 | SQLITE_API SQLITE_EXPERIMENTAL int sqlite3_snapshot_recover(sqlite3 *db, const char *zDb); |
| 9138 | 9139 | |
| 9139 | 9140 | /* |
| 9140 | 9141 | ** CAPI3REF: Serialize a database |
| 9141 | 9142 |
| --- src/sqlite3.h | |
| +++ src/sqlite3.h | |
| @@ -123,11 +123,11 @@ | |
| 123 | ** [sqlite3_libversion_number()], [sqlite3_sourceid()], |
| 124 | ** [sqlite_version()] and [sqlite_source_id()]. |
| 125 | */ |
| 126 | #define SQLITE_VERSION "3.25.0" |
| 127 | #define SQLITE_VERSION_NUMBER 3025000 |
| 128 | #define SQLITE_SOURCE_ID "2018-07-18 19:09:07 a5087c5c87ad65f92e3bc96bbc84afb43faf10ab6b9ed3ba16304b5c60ad069f" |
| 129 | |
| 130 | /* |
| 131 | ** CAPI3REF: Run-Time Library Version Numbers |
| 132 | ** KEYWORDS: sqlite3_version sqlite3_sourceid |
| 133 | ** |
| @@ -885,11 +885,12 @@ | |
| 885 | ** interrogated. The zDbName parameter is ignored. |
| 886 | ** |
| 887 | ** <li>[[SQLITE_FCNTL_PERSIST_WAL]] |
| 888 | ** ^The [SQLITE_FCNTL_PERSIST_WAL] opcode is used to set or query the |
| 889 | ** persistent [WAL | Write Ahead Log] setting. By default, the auxiliary |
| 890 | ** write ahead log and shared memory files used for transaction control |
| 891 | ** are automatically deleted when the latest connection to the database |
| 892 | ** closes. Setting persistent WAL mode causes those files to persist after |
| 893 | ** close. Persisting the files is useful when other processes that do not |
| 894 | ** have write permission on the directory containing the database file want |
| 895 | ** to read the database file, as the WAL and shared memory files must exist |
| @@ -8958,11 +8959,10 @@ | |
| 8958 | SQLITE_API int sqlite3_system_errno(sqlite3*); |
| 8959 | |
| 8960 | /* |
| 8961 | ** CAPI3REF: Database Snapshot |
| 8962 | ** KEYWORDS: {snapshot} {sqlite3_snapshot} |
| 8963 | ** EXPERIMENTAL |
| 8964 | ** |
| 8965 | ** An instance of the snapshot object records the state of a [WAL mode] |
| 8966 | ** database for some specific point in history. |
| 8967 | ** |
| 8968 | ** In [WAL mode], multiple [database connections] that are open on the |
| @@ -8975,23 +8975,18 @@ | |
| 8975 | ** |
| 8976 | ** The sqlite3_snapshot object records state information about an historical |
| 8977 | ** version of the database file so that it is possible to later open a new read |
| 8978 | ** transaction that sees that historical version of the database rather than |
| 8979 | ** the most recent version. |
| 8980 | ** |
| 8981 | ** The constructor for this object is [sqlite3_snapshot_get()]. The |
| 8982 | ** [sqlite3_snapshot_open()] method causes a fresh read transaction to refer |
| 8983 | ** to an historical snapshot (if possible). The destructor for |
| 8984 | ** sqlite3_snapshot objects is [sqlite3_snapshot_free()]. |
| 8985 | */ |
| 8986 | typedef struct sqlite3_snapshot { |
| 8987 | unsigned char hidden[48]; |
| 8988 | } sqlite3_snapshot; |
| 8989 | |
| 8990 | /* |
| 8991 | ** CAPI3REF: Record A Database Snapshot |
| 8992 | ** EXPERIMENTAL |
| 8993 | ** |
| 8994 | ** ^The [sqlite3_snapshot_get(D,S,P)] interface attempts to make a |
| 8995 | ** new [sqlite3_snapshot] object that records the current state of |
| 8996 | ** schema S in database connection D. ^On success, the |
| 8997 | ** [sqlite3_snapshot_get(D,S,P)] interface writes a pointer to the newly |
| @@ -9026,21 +9021,21 @@ | |
| 9026 | ** The [sqlite3_snapshot] object returned from a successful call to |
| 9027 | ** [sqlite3_snapshot_get()] must be freed using [sqlite3_snapshot_free()] |
| 9028 | ** to avoid a memory leak. |
| 9029 | ** |
| 9030 | ** The [sqlite3_snapshot_get()] interface is only available when the |
| 9031 | ** SQLITE_ENABLE_SNAPSHOT compile-time option is used. |
| 9032 | */ |
| 9033 | SQLITE_API SQLITE_EXPERIMENTAL int sqlite3_snapshot_get( |
| 9034 | sqlite3 *db, |
| 9035 | const char *zSchema, |
| 9036 | sqlite3_snapshot **ppSnapshot |
| 9037 | ); |
| 9038 | |
| 9039 | /* |
| 9040 | ** CAPI3REF: Start a read transaction on an historical snapshot |
| 9041 | ** EXPERIMENTAL |
| 9042 | ** |
| 9043 | ** ^The [sqlite3_snapshot_open(D,S,P)] interface starts a |
| 9044 | ** read transaction for schema S of |
| 9045 | ** [database connection] D such that the read transaction |
| 9046 | ** refers to historical [snapshot] P, rather than the most |
| @@ -9064,34 +9059,34 @@ | |
| 9064 | ** after the most recent I/O on the database connection.)^ |
| 9065 | ** (Hint: Run "[PRAGMA application_id]" against a newly opened |
| 9066 | ** database connection in order to make it ready to use snapshots.) |
| 9067 | ** |
| 9068 | ** The [sqlite3_snapshot_open()] interface is only available when the |
| 9069 | ** SQLITE_ENABLE_SNAPSHOT compile-time option is used. |
| 9070 | */ |
| 9071 | SQLITE_API SQLITE_EXPERIMENTAL int sqlite3_snapshot_open( |
| 9072 | sqlite3 *db, |
| 9073 | const char *zSchema, |
| 9074 | sqlite3_snapshot *pSnapshot |
| 9075 | ); |
| 9076 | |
| 9077 | /* |
| 9078 | ** CAPI3REF: Destroy a snapshot |
| 9079 | ** EXPERIMENTAL |
| 9080 | ** |
| 9081 | ** ^The [sqlite3_snapshot_free(P)] interface destroys [sqlite3_snapshot] P. |
| 9082 | ** The application must eventually free every [sqlite3_snapshot] object |
| 9083 | ** using this routine to avoid a memory leak. |
| 9084 | ** |
| 9085 | ** The [sqlite3_snapshot_free()] interface is only available when the |
| 9086 | ** SQLITE_ENABLE_SNAPSHOT compile-time option is used. |
| 9087 | */ |
| 9088 | SQLITE_API SQLITE_EXPERIMENTAL void sqlite3_snapshot_free(sqlite3_snapshot*); |
| 9089 | |
| 9090 | /* |
| 9091 | ** CAPI3REF: Compare the ages of two snapshot handles. |
| 9092 | ** EXPERIMENTAL |
| 9093 | ** |
| 9094 | ** The sqlite3_snapshot_cmp(P1, P2) interface is used to compare the ages |
| 9095 | ** of two valid snapshot handles. |
| 9096 | ** |
| 9097 | ** If the two snapshot handles are not associated with the same database |
| @@ -9106,35 +9101,41 @@ | |
| 9106 | ** is undefined. |
| 9107 | ** |
| 9108 | ** Otherwise, this API returns a negative value if P1 refers to an older |
| 9109 | ** snapshot than P2, zero if the two handles refer to the same database |
| 9110 | ** snapshot, and a positive value if P1 is a newer snapshot than P2. |
| 9111 | */ |
| 9112 | SQLITE_API SQLITE_EXPERIMENTAL int sqlite3_snapshot_cmp( |
| 9113 | sqlite3_snapshot *p1, |
| 9114 | sqlite3_snapshot *p2 |
| 9115 | ); |
| 9116 | |
| 9117 | /* |
| 9118 | ** CAPI3REF: Recover snapshots from a wal file |
| 9119 | ** EXPERIMENTAL |
| 9120 | ** |
| 9121 | ** If all connections disconnect from a database file but do not perform |
| 9122 | ** a checkpoint, the existing wal file is opened along with the database |
| 9123 | ** file the next time the database is opened. At this point it is only |
| 9124 | ** possible to successfully call sqlite3_snapshot_open() to open the most |
| 9125 | ** recent snapshot of the database (the one at the head of the wal file), |
| 9126 | ** even though the wal file may contain other valid snapshots for which |
| 9127 | ** clients have sqlite3_snapshot handles. |
| 9128 | ** |
| 9129 | ** This function attempts to scan the wal file associated with database zDb |
| 9130 | ** of database handle db and make all valid snapshots available to |
| 9131 | ** sqlite3_snapshot_open(). It is an error if there is already a read |
| 9132 | ** transaction open on the database, or if the database is not a wal mode |
| 9133 | ** database. |
| 9134 | ** |
| 9135 | ** SQLITE_OK is returned if successful, or an SQLite error code otherwise. |
| 9136 | */ |
| 9137 | SQLITE_API SQLITE_EXPERIMENTAL int sqlite3_snapshot_recover(sqlite3 *db, const char *zDb); |
| 9138 | |
| 9139 | /* |
| 9140 | ** CAPI3REF: Serialize a database |
| 9141 |
| --- src/sqlite3.h | |
| +++ src/sqlite3.h | |
| @@ -123,11 +123,11 @@ | |
| 123 | ** [sqlite3_libversion_number()], [sqlite3_sourceid()], |
| 124 | ** [sqlite_version()] and [sqlite_source_id()]. |
| 125 | */ |
| 126 | #define SQLITE_VERSION "3.25.0" |
| 127 | #define SQLITE_VERSION_NUMBER 3025000 |
| 128 | #define SQLITE_SOURCE_ID "2018-07-24 22:02:12 2bd593332da0aade467e7a4ee89e966aa6302f37540a2c5e23671f98a6cb599c" |
| 129 | |
| 130 | /* |
| 131 | ** CAPI3REF: Run-Time Library Version Numbers |
| 132 | ** KEYWORDS: sqlite3_version sqlite3_sourceid |
| 133 | ** |
| @@ -885,11 +885,12 @@ | |
| 885 | ** interrogated. The zDbName parameter is ignored. |
| 886 | ** |
| 887 | ** <li>[[SQLITE_FCNTL_PERSIST_WAL]] |
| 888 | ** ^The [SQLITE_FCNTL_PERSIST_WAL] opcode is used to set or query the |
| 889 | ** persistent [WAL | Write Ahead Log] setting. By default, the auxiliary |
| 890 | ** write ahead log ([WAL file]) and shared memory |
| 891 | ** files used for transaction control |
| 892 | ** are automatically deleted when the latest connection to the database |
| 893 | ** closes. Setting persistent WAL mode causes those files to persist after |
| 894 | ** close. Persisting the files is useful when other processes that do not |
| 895 | ** have write permission on the directory containing the database file want |
| 896 | ** to read the database file, as the WAL and shared memory files must exist |
| @@ -8958,11 +8959,10 @@ | |
| 8959 | SQLITE_API int sqlite3_system_errno(sqlite3*); |
| 8960 | |
| 8961 | /* |
| 8962 | ** CAPI3REF: Database Snapshot |
| 8963 | ** KEYWORDS: {snapshot} {sqlite3_snapshot} |
| 8964 | ** |
| 8965 | ** An instance of the snapshot object records the state of a [WAL mode] |
| 8966 | ** database for some specific point in history. |
| 8967 | ** |
| 8968 | ** In [WAL mode], multiple [database connections] that are open on the |
| @@ -8975,23 +8975,18 @@ | |
| 8975 | ** |
| 8976 | ** The sqlite3_snapshot object records state information about an historical |
| 8977 | ** version of the database file so that it is possible to later open a new read |
| 8978 | ** transaction that sees that historical version of the database rather than |
| 8979 | ** the most recent version. |
| 8980 | */ |
| 8981 | typedef struct sqlite3_snapshot { |
| 8982 | unsigned char hidden[48]; |
| 8983 | } sqlite3_snapshot; |
| 8984 | |
| 8985 | /* |
| 8986 | ** CAPI3REF: Record A Database Snapshot |
| 8987 | ** CONSTRUCTOR: sqlite3_snapshot |
| 8988 | ** |
| 8989 | ** ^The [sqlite3_snapshot_get(D,S,P)] interface attempts to make a |
| 8990 | ** new [sqlite3_snapshot] object that records the current state of |
| 8991 | ** schema S in database connection D. ^On success, the |
| 8992 | ** [sqlite3_snapshot_get(D,S,P)] interface writes a pointer to the newly |
| @@ -9026,21 +9021,21 @@ | |
| 9021 | ** The [sqlite3_snapshot] object returned from a successful call to |
| 9022 | ** [sqlite3_snapshot_get()] must be freed using [sqlite3_snapshot_free()] |
| 9023 | ** to avoid a memory leak. |
| 9024 | ** |
| 9025 | ** The [sqlite3_snapshot_get()] interface is only available when the |
| 9026 | ** [SQLITE_ENABLE_SNAPSHOT] compile-time option is used. |
| 9027 | */ |
| 9028 | SQLITE_API SQLITE_EXPERIMENTAL int sqlite3_snapshot_get( |
| 9029 | sqlite3 *db, |
| 9030 | const char *zSchema, |
| 9031 | sqlite3_snapshot **ppSnapshot |
| 9032 | ); |
| 9033 | |
| 9034 | /* |
| 9035 | ** CAPI3REF: Start a read transaction on an historical snapshot |
| 9036 | ** METHOD: sqlite3_snapshot |
| 9037 | ** |
| 9038 | ** ^The [sqlite3_snapshot_open(D,S,P)] interface starts a |
| 9039 | ** read transaction for schema S of |
| 9040 | ** [database connection] D such that the read transaction |
| 9041 | ** refers to historical [snapshot] P, rather than the most |
| @@ -9064,34 +9059,34 @@ | |
| 9059 | ** after the most recent I/O on the database connection.)^ |
| 9060 | ** (Hint: Run "[PRAGMA application_id]" against a newly opened |
| 9061 | ** database connection in order to make it ready to use snapshots.) |
| 9062 | ** |
| 9063 | ** The [sqlite3_snapshot_open()] interface is only available when the |
| 9064 | ** [SQLITE_ENABLE_SNAPSHOT] compile-time option is used. |
| 9065 | */ |
| 9066 | SQLITE_API SQLITE_EXPERIMENTAL int sqlite3_snapshot_open( |
| 9067 | sqlite3 *db, |
| 9068 | const char *zSchema, |
| 9069 | sqlite3_snapshot *pSnapshot |
| 9070 | ); |
| 9071 | |
| 9072 | /* |
| 9073 | ** CAPI3REF: Destroy a snapshot |
| 9074 | ** DESTRUCTOR: sqlite3_snapshot |
| 9075 | ** |
| 9076 | ** ^The [sqlite3_snapshot_free(P)] interface destroys [sqlite3_snapshot] P. |
| 9077 | ** The application must eventually free every [sqlite3_snapshot] object |
| 9078 | ** using this routine to avoid a memory leak. |
| 9079 | ** |
| 9080 | ** The [sqlite3_snapshot_free()] interface is only available when the |
| 9081 | ** [SQLITE_ENABLE_SNAPSHOT] compile-time option is used. |
| 9082 | */ |
| 9083 | SQLITE_API SQLITE_EXPERIMENTAL void sqlite3_snapshot_free(sqlite3_snapshot*); |
| 9084 | |
| 9085 | /* |
| 9086 | ** CAPI3REF: Compare the ages of two snapshot handles. |
| 9087 | ** METHOD: sqlite3_snapshot |
| 9088 | ** |
| 9089 | ** The sqlite3_snapshot_cmp(P1, P2) interface is used to compare the ages |
| 9090 | ** of two valid snapshot handles. |
| 9091 | ** |
| 9092 | ** If the two snapshot handles are not associated with the same database |
| @@ -9106,35 +9101,41 @@ | |
| 9101 | ** is undefined. |
| 9102 | ** |
| 9103 | ** Otherwise, this API returns a negative value if P1 refers to an older |
| 9104 | ** snapshot than P2, zero if the two handles refer to the same database |
| 9105 | ** snapshot, and a positive value if P1 is a newer snapshot than P2. |
| 9106 | ** |
| 9107 | ** This interface is only available if SQLite is compiled with the |
| 9108 | ** [SQLITE_ENABLE_SNAPSHOT] option. |
| 9109 | */ |
| 9110 | SQLITE_API SQLITE_EXPERIMENTAL int sqlite3_snapshot_cmp( |
| 9111 | sqlite3_snapshot *p1, |
| 9112 | sqlite3_snapshot *p2 |
| 9113 | ); |
| 9114 | |
| 9115 | /* |
| 9116 | ** CAPI3REF: Recover snapshots from a wal file |
| 9117 | ** METHOD: sqlite3_snapshot |
| 9118 | ** |
| 9119 | ** If a [WAL file] remains on disk after all database connections close |
| 9120 | ** (either through the use of the [SQLITE_FCNTL_PERSIST_WAL] [file control] |
| 9121 | ** or because the last process to have the database opened exited without |
| 9122 | ** calling [sqlite3_close()]) and a new connection is subsequently opened |
| 9123 | ** on that database and [WAL file], the [sqlite3_snapshot_open()] interface |
| 9124 | ** will only be able to open the last transaction added to the WAL file |
| 9125 | ** even though the WAL file contains other valid transactions. |
| 9126 | ** |
| 9127 | ** This function attempts to scan the WAL file associated with database zDb |
| 9128 | ** of database handle db and make all valid snapshots available to |
| 9129 | ** sqlite3_snapshot_open(). It is an error if there is already a read |
| 9130 | ** transaction open on the database, or if the database is not a WAL mode |
| 9131 | ** database. |
| 9132 | ** |
| 9133 | ** SQLITE_OK is returned if successful, or an SQLite error code otherwise. |
| 9134 | ** |
| 9135 | ** This interface is only available if SQLite is compiled with the |
| 9136 | ** [SQLITE_ENABLE_SNAPSHOT] option. |
| 9137 | */ |
| 9138 | SQLITE_API SQLITE_EXPERIMENTAL int sqlite3_snapshot_recover(sqlite3 *db, const char *zDb); |
| 9139 | |
| 9140 | /* |
| 9141 | ** CAPI3REF: Serialize a database |
| 9142 |
+163
-60
| --- src/style.c | ||
| +++ src/style.c | ||
| @@ -433,10 +433,15 @@ | ||
| 433 | 433 | Th_Unstore("title"); /* Avoid collisions with ticket field names */ |
| 434 | 434 | cgi_destination(CGI_BODY); |
| 435 | 435 | g.cgiOutput = 1; |
| 436 | 436 | headerHasBeenGenerated = 1; |
| 437 | 437 | sideboxUsed = 0; |
| 438 | + if( g.perm.Debug && P("showqp") ){ | |
| 439 | + @ <div class="debug"> | |
| 440 | + cgi_print_all(0, 0); | |
| 441 | + @ </div> | |
| 442 | + } | |
| 438 | 443 | } |
| 439 | 444 | |
| 440 | 445 | #if INTERFACE |
| 441 | 446 | /* Allowed parameters for style_adunit() */ |
| 442 | 447 | #define ADUNIT_OFF 0x0001 /* Do not allow ads on this page */ |
| @@ -502,15 +507,36 @@ | ||
| 502 | 507 | ** Generate code to load a single javascript file |
| 503 | 508 | */ |
| 504 | 509 | void style_load_one_js_file(const char *zFile){ |
| 505 | 510 | @ <script src='%R/builtin/%s(zFile)?id=%S(MANIFEST_UUID)'></script> |
| 506 | 511 | } |
| 512 | + | |
| 513 | +/* | |
| 514 | +** All extra JS files to load. | |
| 515 | +*/ | |
| 516 | +static const char *azJsToLoad[4]; | |
| 517 | +static int nJsToLoad = 0; | |
| 518 | + | |
| 519 | +/* | |
| 520 | +** Register a new JS file to load at the end of the document. | |
| 521 | +*/ | |
| 522 | +void style_load_js(const char *zName){ | |
| 523 | + int i; | |
| 524 | + for(i=0; i<nJsToLoad; i++){ | |
| 525 | + if( fossil_strcmp(zName, azJsToLoad[i])==0 ) return; | |
| 526 | + } | |
| 527 | + if( nJsToLoad>=sizeof(azJsToLoad)/sizeof(azJsToLoad[0]) ){ | |
| 528 | + fossil_panic("too man JS files"); | |
| 529 | + } | |
| 530 | + azJsToLoad[nJsToLoad++] = zName; | |
| 531 | +} | |
| 507 | 532 | |
| 508 | 533 | /* |
| 509 | 534 | ** Generate code to load all required javascript files. |
| 510 | 535 | */ |
| 511 | 536 | static void style_load_all_js_files(void){ |
| 537 | + int i; | |
| 512 | 538 | if( needHrefJs ){ |
| 513 | 539 | int nDelay = db_get_int("auto-hyperlink-delay",0); |
| 514 | 540 | int bMouseover; |
| 515 | 541 | /* Load up the page data */ |
| 516 | 542 | bMouseover = (!g.isHuman || db_get_boolean("auto-hyperlink-ishuman",0)) |
| @@ -523,10 +549,13 @@ | ||
| 523 | 549 | style_load_one_js_file("sorttable.js"); |
| 524 | 550 | } |
| 525 | 551 | if( needGraphJs ){ |
| 526 | 552 | style_load_one_js_file("graph.js"); |
| 527 | 553 | } |
| 554 | + for(i=0; i<nJsToLoad; i++){ | |
| 555 | + style_load_one_js_file(azJsToLoad[i]); | |
| 556 | + } | |
| 528 | 557 | } |
| 529 | 558 | |
| 530 | 559 | /* |
| 531 | 560 | ** Draw the footer at the bottom of the page. |
| 532 | 561 | */ |
| @@ -864,22 +893,80 @@ | ||
| 864 | 893 | } |
| 865 | 894 | blob_init(&out, zTxt, -1); |
| 866 | 895 | cgi_set_content(&out); |
| 867 | 896 | } |
| 868 | 897 | |
| 898 | +/* | |
| 899 | +** All possible capabilities | |
| 900 | +*/ | |
| 901 | +static const char allCap[] = | |
| 902 | + "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKL"; | |
| 903 | + | |
| 904 | +/* | |
| 905 | +** Compute the current login capabilities | |
| 906 | +*/ | |
| 907 | +static char *find_capabilities(char *zCap){ | |
| 908 | + int i, j; | |
| 909 | + char c; | |
| 910 | + for(i=j=0; (c = allCap[j])!=0; j++){ | |
| 911 | + if( login_has_capability(&c, 1, 0) ) zCap[i++] = c; | |
| 912 | + } | |
| 913 | + zCap[i] = 0; | |
| 914 | + return zCap; | |
| 915 | +} | |
| 916 | + | |
| 917 | +/* | |
| 918 | +** Compute the current login capabilities that were | |
| 919 | +** contributed by Anonymous | |
| 920 | +*/ | |
| 921 | +static char *find_anon_capabilities(char *zCap){ | |
| 922 | + int i, j; | |
| 923 | + char c; | |
| 924 | + for(i=j=0; (c = allCap[j])!=0; j++){ | |
| 925 | + if( login_has_capability(&c, 1, LOGIN_ANON) | |
| 926 | + && !login_has_capability(&c, 1, 0) ) zCap[i++] = c; | |
| 927 | + } | |
| 928 | + zCap[i] = 0; | |
| 929 | + return zCap; | |
| 930 | +} | |
| 869 | 931 | |
| 870 | 932 | /* |
| 871 | 933 | ** WEBPAGE: test_env |
| 872 | 934 | ** |
| 873 | 935 | ** Display CGI-variables and other aspects of the run-time |
| 874 | 936 | ** environment, for debugging and trouble-shooting purposes. |
| 875 | 937 | */ |
| 876 | 938 | void page_test_env(void){ |
| 877 | - char c; | |
| 939 | + webpage_error(""); | |
| 940 | +} | |
| 941 | + | |
| 942 | +/* | |
| 943 | +** WEBPAGE: honeypot | |
| 944 | +** This page is a honeypot for spiders and bots. | |
| 945 | +*/ | |
| 946 | +void honeypot_page(void){ | |
| 947 | + cgi_set_status(403, "Forbidden"); | |
| 948 | + @ <p>Please enable javascript or log in to see this content</p> | |
| 949 | +} | |
| 950 | + | |
| 951 | +/* | |
| 952 | +** Webpages that encounter an error due to missing or incorrect | |
| 953 | +** query parameters can jump to this routine to render an error | |
| 954 | +** message screen. | |
| 955 | +** | |
| 956 | +** For administators, or if the test_env_enable setting is true, then | |
| 957 | +** details of the request environment are displayed. Otherwise, just | |
| 958 | +** the error message is shown. | |
| 959 | +** | |
| 960 | +** If zFormat is an empty string, then this is the /test_env page. | |
| 961 | +*/ | |
| 962 | +void webpage_error(const char *zFormat, ...){ | |
| 878 | 963 | int i; |
| 879 | 964 | int showAll; |
| 880 | - char zCap[30]; | |
| 965 | + char *zErr = 0; | |
| 966 | + int isAuth = 0; | |
| 967 | + char zCap[100]; | |
| 881 | 968 | static const char *const azCgiVars[] = { |
| 882 | 969 | "COMSPEC", "DOCUMENT_ROOT", "GATEWAY_INTERFACE", |
| 883 | 970 | "HTTP_ACCEPT", "HTTP_ACCEPT_CHARSET", "HTTP_ACCEPT_ENCODING", |
| 884 | 971 | "HTTP_ACCEPT_LANGUAGE", "HTTP_AUTHENICATION", |
| 885 | 972 | "HTTP_CONNECTION", "HTTP_HOST", |
| @@ -895,72 +982,88 @@ | ||
| 895 | 982 | "FOSSIL_TCL_PATH", "TH1_DELETE_INTERP", "TH1_ENABLE_DOCS", |
| 896 | 983 | "TH1_ENABLE_HOOKS", "TH1_ENABLE_TCL", "REMOTE_HOST" |
| 897 | 984 | }; |
| 898 | 985 | |
| 899 | 986 | login_check_credentials(); |
| 900 | - if( !g.perm.Admin && !g.perm.Setup && !db_get_boolean("test_env_enable",0) ){ | |
| 901 | - login_needed(0); | |
| 902 | - return; | |
| 987 | + if( g.perm.Admin || g.perm.Setup || db_get_boolean("test_env_enable",0) ){ | |
| 988 | + isAuth = 1; | |
| 903 | 989 | } |
| 904 | 990 | for(i=0; i<count(azCgiVars); i++) (void)P(azCgiVars[i]); |
| 905 | - style_header("Environment Test"); | |
| 906 | - showAll = PB("showall"); | |
| 907 | - style_submenu_checkbox("showall", "Cookies", 0, 0); | |
| 908 | - style_submenu_element("Stats", "%R/stat"); | |
| 909 | - | |
| 910 | -#if !defined(_WIN32) | |
| 911 | - @ uid=%d(getuid()), gid=%d(getgid())<br /> | |
| 912 | -#endif | |
| 913 | - @ g.zBaseURL = %h(g.zBaseURL)<br /> | |
| 914 | - @ g.zHttpsURL = %h(g.zHttpsURL)<br /> | |
| 915 | - @ g.zTop = %h(g.zTop)<br /> | |
| 916 | - @ g.zPath = %h(g.zPath)<br /> | |
| 917 | - for(i=0, c='a'; c<='z'; c++){ | |
| 918 | - if( login_has_capability(&c, 1, 0) ) zCap[i++] = c; | |
| 919 | - } | |
| 920 | - zCap[i] = 0; | |
| 921 | - @ g.userUid = %d(g.userUid)<br /> | |
| 922 | - @ g.zLogin = %h(g.zLogin)<br /> | |
| 923 | - @ g.isHuman = %d(g.isHuman)<br /> | |
| 924 | - if( g.nRequest ){ | |
| 925 | - @ g.nRequest = %d(g.nRequest)<br /> | |
| 926 | - } | |
| 927 | - if( g.nPendingRequest>1 ){ | |
| 928 | - @ g.nPendingRequest = %d(g.nPendingRequest)<br /> | |
| 929 | - } | |
| 930 | - @ capabilities = %s(zCap)<br /> | |
| 931 | - for(i=0, c='a'; c<='z'; c++){ | |
| 932 | - if( login_has_capability(&c, 1, LOGIN_ANON) | |
| 933 | - && !login_has_capability(&c, 1, 0) ) zCap[i++] = c; | |
| 934 | - } | |
| 935 | - zCap[i] = 0; | |
| 936 | - if( i>0 ){ | |
| 937 | - @ anonymous-adds = %s(zCap)<br /> | |
| 938 | - } | |
| 939 | - @ g.zRepositoryName = %h(g.zRepositoryName)<br /> | |
| 940 | - @ load_average() = %f(load_average())<br /> | |
| 941 | - @ cgi_csrf_safe(0) = %d(cgi_csrf_safe(0))<br /> | |
| 942 | - @ <hr /> | |
| 943 | - P("HTTP_USER_AGENT"); | |
| 944 | - cgi_print_all(showAll, 0); | |
| 945 | - if( showAll && blob_size(&g.httpHeader)>0 ){ | |
| 946 | - @ <hr /> | |
| 947 | - @ <pre> | |
| 948 | - @ %h(blob_str(&g.httpHeader)) | |
| 949 | - @ </pre> | |
| 950 | - } | |
| 951 | - if( g.perm.Setup ){ | |
| 952 | - const char *zRedir = P("redirect"); | |
| 953 | - if( zRedir ) cgi_redirect(zRedir); | |
| 991 | + if( zFormat[0] ){ | |
| 992 | + va_list ap; | |
| 993 | + va_start(ap, zFormat); | |
| 994 | + zErr = vmprintf(zFormat, ap); | |
| 995 | + va_end(ap); | |
| 996 | + style_header("Bad Request"); | |
| 997 | + @ <h1>/%h(g.zPath): %h(zErr)</h1> | |
| 998 | + showAll = 0; | |
| 999 | + cgi_set_status(500, "Bad Request"); | |
| 1000 | + }else if( !isAuth ){ | |
| 1001 | + login_needed(0); | |
| 1002 | + return; | |
| 1003 | + }else{ | |
| 1004 | + style_header("Environment Test"); | |
| 1005 | + showAll = PB("showall"); | |
| 1006 | + style_submenu_checkbox("showall", "Cookies", 0, 0); | |
| 1007 | + style_submenu_element("Stats", "%R/stat"); | |
| 1008 | + } | |
| 1009 | + | |
| 1010 | + if( isAuth ){ | |
| 1011 | + #if !defined(_WIN32) | |
| 1012 | + @ uid=%d(getuid()), gid=%d(getgid())<br /> | |
| 1013 | + #endif | |
| 1014 | + @ g.zBaseURL = %h(g.zBaseURL)<br /> | |
| 1015 | + @ g.zHttpsURL = %h(g.zHttpsURL)<br /> | |
| 1016 | + @ g.zTop = %h(g.zTop)<br /> | |
| 1017 | + @ g.zPath = %h(g.zPath)<br /> | |
| 1018 | + @ g.userUid = %d(g.userUid)<br /> | |
| 1019 | + @ g.zLogin = %h(g.zLogin)<br /> | |
| 1020 | + @ g.isHuman = %d(g.isHuman)<br /> | |
| 1021 | + if( g.nRequest ){ | |
| 1022 | + @ g.nRequest = %d(g.nRequest)<br /> | |
| 1023 | + } | |
| 1024 | + if( g.nPendingRequest>1 ){ | |
| 1025 | + @ g.nPendingRequest = %d(g.nPendingRequest)<br /> | |
| 1026 | + } | |
| 1027 | + @ capabilities = %s(find_capabilities(zCap))<br /> | |
| 1028 | + if( zCap[0] ){ | |
| 1029 | + @ anonymous-adds = %s(find_anon_capabilities(zCap))<br /> | |
| 1030 | + } | |
| 1031 | + @ g.zRepositoryName = %h(g.zRepositoryName)<br /> | |
| 1032 | + @ load_average() = %f(load_average())<br /> | |
| 1033 | + @ cgi_csrf_safe(0) = %d(cgi_csrf_safe(0))<br /> | |
| 1034 | + @ <hr /> | |
| 1035 | + P("HTTP_USER_AGENT"); | |
| 1036 | + cgi_print_all(showAll, 0); | |
| 1037 | + if( showAll && blob_size(&g.httpHeader)>0 ){ | |
| 1038 | + @ <hr /> | |
| 1039 | + @ <pre> | |
| 1040 | + @ %h(blob_str(&g.httpHeader)) | |
| 1041 | + @ </pre> | |
| 1042 | + } | |
| 954 | 1043 | } |
| 955 | 1044 | style_footer(); |
| 956 | - if( g.perm.Admin && P("err") ) fossil_fatal("%s", P("err")); | |
| 1045 | + if( zErr ){ | |
| 1046 | + cgi_reply(); | |
| 1047 | + fossil_exit(1); | |
| 1048 | + } | |
| 1049 | +} | |
| 1050 | + | |
| 1051 | +/* | |
| 1052 | +** Generate a Not Yet Implemented error page. | |
| 1053 | +*/ | |
| 1054 | +void webpage_not_yet_implemented(void){ | |
| 1055 | + webpage_error("Not yet implemented"); | |
| 957 | 1056 | } |
| 958 | 1057 | |
| 959 | 1058 | /* |
| 960 | -** WEBPAGE: honeypot | |
| 961 | -** This page is a honeypot for spiders and bots. | |
| 1059 | +** Generate a webpage for a webpage_assert(). | |
| 962 | 1060 | */ |
| 963 | -void honeypot_page(void){ | |
| 964 | - cgi_set_status(403, "Forbidden"); | |
| 965 | - @ <p>Please enable javascript or log in to see this content</p> | |
| 1061 | +void webpage_assert_page(const char *zFile, int iLine, const char *zExpr){ | |
| 1062 | + fossil_warning("assertion fault at %s:%d - %s", zFile, iLine, zExpr); | |
| 1063 | + cgi_reset_content(); | |
| 1064 | + webpage_error("assertion fault at %s:%d - %s", zFile, iLine, zExpr); | |
| 966 | 1065 | } |
| 1066 | + | |
| 1067 | +#if INTERFACE | |
| 1068 | +# define webpage_assert(T) if(!(T)){webpage_assert_page(__FILE__,__LINE__,#T);} | |
| 1069 | +#endif | |
| 967 | 1070 |
| --- src/style.c | |
| +++ src/style.c | |
| @@ -433,10 +433,15 @@ | |
| 433 | Th_Unstore("title"); /* Avoid collisions with ticket field names */ |
| 434 | cgi_destination(CGI_BODY); |
| 435 | g.cgiOutput = 1; |
| 436 | headerHasBeenGenerated = 1; |
| 437 | sideboxUsed = 0; |
| 438 | } |
| 439 | |
| 440 | #if INTERFACE |
| 441 | /* Allowed parameters for style_adunit() */ |
| 442 | #define ADUNIT_OFF 0x0001 /* Do not allow ads on this page */ |
| @@ -502,15 +507,36 @@ | |
| 502 | ** Generate code to load a single javascript file |
| 503 | */ |
| 504 | void style_load_one_js_file(const char *zFile){ |
| 505 | @ <script src='%R/builtin/%s(zFile)?id=%S(MANIFEST_UUID)'></script> |
| 506 | } |
| 507 | |
| 508 | /* |
| 509 | ** Generate code to load all required javascript files. |
| 510 | */ |
| 511 | static void style_load_all_js_files(void){ |
| 512 | if( needHrefJs ){ |
| 513 | int nDelay = db_get_int("auto-hyperlink-delay",0); |
| 514 | int bMouseover; |
| 515 | /* Load up the page data */ |
| 516 | bMouseover = (!g.isHuman || db_get_boolean("auto-hyperlink-ishuman",0)) |
| @@ -523,10 +549,13 @@ | |
| 523 | style_load_one_js_file("sorttable.js"); |
| 524 | } |
| 525 | if( needGraphJs ){ |
| 526 | style_load_one_js_file("graph.js"); |
| 527 | } |
| 528 | } |
| 529 | |
| 530 | /* |
| 531 | ** Draw the footer at the bottom of the page. |
| 532 | */ |
| @@ -864,22 +893,80 @@ | |
| 864 | } |
| 865 | blob_init(&out, zTxt, -1); |
| 866 | cgi_set_content(&out); |
| 867 | } |
| 868 | |
| 869 | |
| 870 | /* |
| 871 | ** WEBPAGE: test_env |
| 872 | ** |
| 873 | ** Display CGI-variables and other aspects of the run-time |
| 874 | ** environment, for debugging and trouble-shooting purposes. |
| 875 | */ |
| 876 | void page_test_env(void){ |
| 877 | char c; |
| 878 | int i; |
| 879 | int showAll; |
| 880 | char zCap[30]; |
| 881 | static const char *const azCgiVars[] = { |
| 882 | "COMSPEC", "DOCUMENT_ROOT", "GATEWAY_INTERFACE", |
| 883 | "HTTP_ACCEPT", "HTTP_ACCEPT_CHARSET", "HTTP_ACCEPT_ENCODING", |
| 884 | "HTTP_ACCEPT_LANGUAGE", "HTTP_AUTHENICATION", |
| 885 | "HTTP_CONNECTION", "HTTP_HOST", |
| @@ -895,72 +982,88 @@ | |
| 895 | "FOSSIL_TCL_PATH", "TH1_DELETE_INTERP", "TH1_ENABLE_DOCS", |
| 896 | "TH1_ENABLE_HOOKS", "TH1_ENABLE_TCL", "REMOTE_HOST" |
| 897 | }; |
| 898 | |
| 899 | login_check_credentials(); |
| 900 | if( !g.perm.Admin && !g.perm.Setup && !db_get_boolean("test_env_enable",0) ){ |
| 901 | login_needed(0); |
| 902 | return; |
| 903 | } |
| 904 | for(i=0; i<count(azCgiVars); i++) (void)P(azCgiVars[i]); |
| 905 | style_header("Environment Test"); |
| 906 | showAll = PB("showall"); |
| 907 | style_submenu_checkbox("showall", "Cookies", 0, 0); |
| 908 | style_submenu_element("Stats", "%R/stat"); |
| 909 | |
| 910 | #if !defined(_WIN32) |
| 911 | @ uid=%d(getuid()), gid=%d(getgid())<br /> |
| 912 | #endif |
| 913 | @ g.zBaseURL = %h(g.zBaseURL)<br /> |
| 914 | @ g.zHttpsURL = %h(g.zHttpsURL)<br /> |
| 915 | @ g.zTop = %h(g.zTop)<br /> |
| 916 | @ g.zPath = %h(g.zPath)<br /> |
| 917 | for(i=0, c='a'; c<='z'; c++){ |
| 918 | if( login_has_capability(&c, 1, 0) ) zCap[i++] = c; |
| 919 | } |
| 920 | zCap[i] = 0; |
| 921 | @ g.userUid = %d(g.userUid)<br /> |
| 922 | @ g.zLogin = %h(g.zLogin)<br /> |
| 923 | @ g.isHuman = %d(g.isHuman)<br /> |
| 924 | if( g.nRequest ){ |
| 925 | @ g.nRequest = %d(g.nRequest)<br /> |
| 926 | } |
| 927 | if( g.nPendingRequest>1 ){ |
| 928 | @ g.nPendingRequest = %d(g.nPendingRequest)<br /> |
| 929 | } |
| 930 | @ capabilities = %s(zCap)<br /> |
| 931 | for(i=0, c='a'; c<='z'; c++){ |
| 932 | if( login_has_capability(&c, 1, LOGIN_ANON) |
| 933 | && !login_has_capability(&c, 1, 0) ) zCap[i++] = c; |
| 934 | } |
| 935 | zCap[i] = 0; |
| 936 | if( i>0 ){ |
| 937 | @ anonymous-adds = %s(zCap)<br /> |
| 938 | } |
| 939 | @ g.zRepositoryName = %h(g.zRepositoryName)<br /> |
| 940 | @ load_average() = %f(load_average())<br /> |
| 941 | @ cgi_csrf_safe(0) = %d(cgi_csrf_safe(0))<br /> |
| 942 | @ <hr /> |
| 943 | P("HTTP_USER_AGENT"); |
| 944 | cgi_print_all(showAll, 0); |
| 945 | if( showAll && blob_size(&g.httpHeader)>0 ){ |
| 946 | @ <hr /> |
| 947 | @ <pre> |
| 948 | @ %h(blob_str(&g.httpHeader)) |
| 949 | @ </pre> |
| 950 | } |
| 951 | if( g.perm.Setup ){ |
| 952 | const char *zRedir = P("redirect"); |
| 953 | if( zRedir ) cgi_redirect(zRedir); |
| 954 | } |
| 955 | style_footer(); |
| 956 | if( g.perm.Admin && P("err") ) fossil_fatal("%s", P("err")); |
| 957 | } |
| 958 | |
| 959 | /* |
| 960 | ** WEBPAGE: honeypot |
| 961 | ** This page is a honeypot for spiders and bots. |
| 962 | */ |
| 963 | void honeypot_page(void){ |
| 964 | cgi_set_status(403, "Forbidden"); |
| 965 | @ <p>Please enable javascript or log in to see this content</p> |
| 966 | } |
| 967 |
| --- src/style.c | |
| +++ src/style.c | |
| @@ -433,10 +433,15 @@ | |
| 433 | Th_Unstore("title"); /* Avoid collisions with ticket field names */ |
| 434 | cgi_destination(CGI_BODY); |
| 435 | g.cgiOutput = 1; |
| 436 | headerHasBeenGenerated = 1; |
| 437 | sideboxUsed = 0; |
| 438 | if( g.perm.Debug && P("showqp") ){ |
| 439 | @ <div class="debug"> |
| 440 | cgi_print_all(0, 0); |
| 441 | @ </div> |
| 442 | } |
| 443 | } |
| 444 | |
| 445 | #if INTERFACE |
| 446 | /* Allowed parameters for style_adunit() */ |
| 447 | #define ADUNIT_OFF 0x0001 /* Do not allow ads on this page */ |
| @@ -502,15 +507,36 @@ | |
| 507 | ** Generate code to load a single javascript file |
| 508 | */ |
| 509 | void style_load_one_js_file(const char *zFile){ |
| 510 | @ <script src='%R/builtin/%s(zFile)?id=%S(MANIFEST_UUID)'></script> |
| 511 | } |
| 512 | |
| 513 | /* |
| 514 | ** All extra JS files to load. |
| 515 | */ |
| 516 | static const char *azJsToLoad[4]; |
| 517 | static int nJsToLoad = 0; |
| 518 | |
| 519 | /* |
| 520 | ** Register a new JS file to load at the end of the document. |
| 521 | */ |
| 522 | void style_load_js(const char *zName){ |
| 523 | int i; |
| 524 | for(i=0; i<nJsToLoad; i++){ |
| 525 | if( fossil_strcmp(zName, azJsToLoad[i])==0 ) return; |
| 526 | } |
| 527 | if( nJsToLoad>=sizeof(azJsToLoad)/sizeof(azJsToLoad[0]) ){ |
| 528 | fossil_panic("too man JS files"); |
| 529 | } |
| 530 | azJsToLoad[nJsToLoad++] = zName; |
| 531 | } |
| 532 | |
| 533 | /* |
| 534 | ** Generate code to load all required javascript files. |
| 535 | */ |
| 536 | static void style_load_all_js_files(void){ |
| 537 | int i; |
| 538 | if( needHrefJs ){ |
| 539 | int nDelay = db_get_int("auto-hyperlink-delay",0); |
| 540 | int bMouseover; |
| 541 | /* Load up the page data */ |
| 542 | bMouseover = (!g.isHuman || db_get_boolean("auto-hyperlink-ishuman",0)) |
| @@ -523,10 +549,13 @@ | |
| 549 | style_load_one_js_file("sorttable.js"); |
| 550 | } |
| 551 | if( needGraphJs ){ |
| 552 | style_load_one_js_file("graph.js"); |
| 553 | } |
| 554 | for(i=0; i<nJsToLoad; i++){ |
| 555 | style_load_one_js_file(azJsToLoad[i]); |
| 556 | } |
| 557 | } |
| 558 | |
| 559 | /* |
| 560 | ** Draw the footer at the bottom of the page. |
| 561 | */ |
| @@ -864,22 +893,80 @@ | |
| 893 | } |
| 894 | blob_init(&out, zTxt, -1); |
| 895 | cgi_set_content(&out); |
| 896 | } |
| 897 | |
| 898 | /* |
| 899 | ** All possible capabilities |
| 900 | */ |
| 901 | static const char allCap[] = |
| 902 | "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKL"; |
| 903 | |
| 904 | /* |
| 905 | ** Compute the current login capabilities |
| 906 | */ |
| 907 | static char *find_capabilities(char *zCap){ |
| 908 | int i, j; |
| 909 | char c; |
| 910 | for(i=j=0; (c = allCap[j])!=0; j++){ |
| 911 | if( login_has_capability(&c, 1, 0) ) zCap[i++] = c; |
| 912 | } |
| 913 | zCap[i] = 0; |
| 914 | return zCap; |
| 915 | } |
| 916 | |
| 917 | /* |
| 918 | ** Compute the current login capabilities that were |
| 919 | ** contributed by Anonymous |
| 920 | */ |
| 921 | static char *find_anon_capabilities(char *zCap){ |
| 922 | int i, j; |
| 923 | char c; |
| 924 | for(i=j=0; (c = allCap[j])!=0; j++){ |
| 925 | if( login_has_capability(&c, 1, LOGIN_ANON) |
| 926 | && !login_has_capability(&c, 1, 0) ) zCap[i++] = c; |
| 927 | } |
| 928 | zCap[i] = 0; |
| 929 | return zCap; |
| 930 | } |
| 931 | |
| 932 | /* |
| 933 | ** WEBPAGE: test_env |
| 934 | ** |
| 935 | ** Display CGI-variables and other aspects of the run-time |
| 936 | ** environment, for debugging and trouble-shooting purposes. |
| 937 | */ |
| 938 | void page_test_env(void){ |
| 939 | webpage_error(""); |
| 940 | } |
| 941 | |
| 942 | /* |
| 943 | ** WEBPAGE: honeypot |
| 944 | ** This page is a honeypot for spiders and bots. |
| 945 | */ |
| 946 | void honeypot_page(void){ |
| 947 | cgi_set_status(403, "Forbidden"); |
| 948 | @ <p>Please enable javascript or log in to see this content</p> |
| 949 | } |
| 950 | |
| 951 | /* |
| 952 | ** Webpages that encounter an error due to missing or incorrect |
| 953 | ** query parameters can jump to this routine to render an error |
| 954 | ** message screen. |
| 955 | ** |
| 956 | ** For administators, or if the test_env_enable setting is true, then |
| 957 | ** details of the request environment are displayed. Otherwise, just |
| 958 | ** the error message is shown. |
| 959 | ** |
| 960 | ** If zFormat is an empty string, then this is the /test_env page. |
| 961 | */ |
| 962 | void webpage_error(const char *zFormat, ...){ |
| 963 | int i; |
| 964 | int showAll; |
| 965 | char *zErr = 0; |
| 966 | int isAuth = 0; |
| 967 | char zCap[100]; |
| 968 | static const char *const azCgiVars[] = { |
| 969 | "COMSPEC", "DOCUMENT_ROOT", "GATEWAY_INTERFACE", |
| 970 | "HTTP_ACCEPT", "HTTP_ACCEPT_CHARSET", "HTTP_ACCEPT_ENCODING", |
| 971 | "HTTP_ACCEPT_LANGUAGE", "HTTP_AUTHENICATION", |
| 972 | "HTTP_CONNECTION", "HTTP_HOST", |
| @@ -895,72 +982,88 @@ | |
| 982 | "FOSSIL_TCL_PATH", "TH1_DELETE_INTERP", "TH1_ENABLE_DOCS", |
| 983 | "TH1_ENABLE_HOOKS", "TH1_ENABLE_TCL", "REMOTE_HOST" |
| 984 | }; |
| 985 | |
| 986 | login_check_credentials(); |
| 987 | if( g.perm.Admin || g.perm.Setup || db_get_boolean("test_env_enable",0) ){ |
| 988 | isAuth = 1; |
| 989 | } |
| 990 | for(i=0; i<count(azCgiVars); i++) (void)P(azCgiVars[i]); |
| 991 | if( zFormat[0] ){ |
| 992 | va_list ap; |
| 993 | va_start(ap, zFormat); |
| 994 | zErr = vmprintf(zFormat, ap); |
| 995 | va_end(ap); |
| 996 | style_header("Bad Request"); |
| 997 | @ <h1>/%h(g.zPath): %h(zErr)</h1> |
| 998 | showAll = 0; |
| 999 | cgi_set_status(500, "Bad Request"); |
| 1000 | }else if( !isAuth ){ |
| 1001 | login_needed(0); |
| 1002 | return; |
| 1003 | }else{ |
| 1004 | style_header("Environment Test"); |
| 1005 | showAll = PB("showall"); |
| 1006 | style_submenu_checkbox("showall", "Cookies", 0, 0); |
| 1007 | style_submenu_element("Stats", "%R/stat"); |
| 1008 | } |
| 1009 | |
| 1010 | if( isAuth ){ |
| 1011 | #if !defined(_WIN32) |
| 1012 | @ uid=%d(getuid()), gid=%d(getgid())<br /> |
| 1013 | #endif |
| 1014 | @ g.zBaseURL = %h(g.zBaseURL)<br /> |
| 1015 | @ g.zHttpsURL = %h(g.zHttpsURL)<br /> |
| 1016 | @ g.zTop = %h(g.zTop)<br /> |
| 1017 | @ g.zPath = %h(g.zPath)<br /> |
| 1018 | @ g.userUid = %d(g.userUid)<br /> |
| 1019 | @ g.zLogin = %h(g.zLogin)<br /> |
| 1020 | @ g.isHuman = %d(g.isHuman)<br /> |
| 1021 | if( g.nRequest ){ |
| 1022 | @ g.nRequest = %d(g.nRequest)<br /> |
| 1023 | } |
| 1024 | if( g.nPendingRequest>1 ){ |
| 1025 | @ g.nPendingRequest = %d(g.nPendingRequest)<br /> |
| 1026 | } |
| 1027 | @ capabilities = %s(find_capabilities(zCap))<br /> |
| 1028 | if( zCap[0] ){ |
| 1029 | @ anonymous-adds = %s(find_anon_capabilities(zCap))<br /> |
| 1030 | } |
| 1031 | @ g.zRepositoryName = %h(g.zRepositoryName)<br /> |
| 1032 | @ load_average() = %f(load_average())<br /> |
| 1033 | @ cgi_csrf_safe(0) = %d(cgi_csrf_safe(0))<br /> |
| 1034 | @ <hr /> |
| 1035 | P("HTTP_USER_AGENT"); |
| 1036 | cgi_print_all(showAll, 0); |
| 1037 | if( showAll && blob_size(&g.httpHeader)>0 ){ |
| 1038 | @ <hr /> |
| 1039 | @ <pre> |
| 1040 | @ %h(blob_str(&g.httpHeader)) |
| 1041 | @ </pre> |
| 1042 | } |
| 1043 | } |
| 1044 | style_footer(); |
| 1045 | if( zErr ){ |
| 1046 | cgi_reply(); |
| 1047 | fossil_exit(1); |
| 1048 | } |
| 1049 | } |
| 1050 | |
| 1051 | /* |
| 1052 | ** Generate a Not Yet Implemented error page. |
| 1053 | */ |
| 1054 | void webpage_not_yet_implemented(void){ |
| 1055 | webpage_error("Not yet implemented"); |
| 1056 | } |
| 1057 | |
| 1058 | /* |
| 1059 | ** Generate a webpage for a webpage_assert(). |
| 1060 | */ |
| 1061 | void webpage_assert_page(const char *zFile, int iLine, const char *zExpr){ |
| 1062 | fossil_warning("assertion fault at %s:%d - %s", zFile, iLine, zExpr); |
| 1063 | cgi_reset_content(); |
| 1064 | webpage_error("assertion fault at %s:%d - %s", zFile, iLine, zExpr); |
| 1065 | } |
| 1066 | |
| 1067 | #if INTERFACE |
| 1068 | # define webpage_assert(T) if(!(T)){webpage_assert_page(__FILE__,__LINE__,#T);} |
| 1069 | #endif |
| 1070 |
+15
-4
| --- src/timeline.c | ||
| +++ src/timeline.c | ||
| @@ -574,11 +574,11 @@ | ||
| 574 | 574 | cgi_printf("technote: "); |
| 575 | 575 | hyperlink_to_event_tagid(tagid<0?-tagid:tagid); |
| 576 | 576 | }else{ |
| 577 | 577 | cgi_printf("artifact: %z%S</a> ",href("%R/info/%!S",zUuid),zUuid); |
| 578 | 578 | } |
| 579 | - }else if( zType[0]=='g' || zType[0]=='w' || zType[0]=='t' ){ | |
| 579 | + }else if( zType[0]=='g' || zType[0]=='w' || zType[0]=='t' || zType[0]=='f'){ | |
| 580 | 580 | cgi_printf("artifact: %z%S</a> ",href("%R/info/%!S",zUuid),zUuid); |
| 581 | 581 | } |
| 582 | 582 | |
| 583 | 583 | if( g.perm.Hyperlink && fossil_strcmp(zDispUser, zThisUser)!=0 ){ |
| 584 | 584 | char *zLink = mprintf("%R/timeline?u=%h&c=%t&nd&n=200", zDispUser, zDate); |
| @@ -1031,11 +1031,11 @@ | ||
| 1031 | 1031 | ** set the y= parameter that determines which elements to display |
| 1032 | 1032 | ** on the timeline. |
| 1033 | 1033 | */ |
| 1034 | 1034 | static void timeline_y_submenu(int isDisabled){ |
| 1035 | 1035 | static int i = 0; |
| 1036 | - static const char *az[12]; | |
| 1036 | + static const char *az[14]; | |
| 1037 | 1037 | if( i==0 ){ |
| 1038 | 1038 | az[0] = "all"; |
| 1039 | 1039 | az[1] = "Any Type"; |
| 1040 | 1040 | i = 2; |
| 1041 | 1041 | if( g.perm.Read ){ |
| @@ -1053,10 +1053,14 @@ | ||
| 1053 | 1053 | az[i++] = "Tickets"; |
| 1054 | 1054 | } |
| 1055 | 1055 | if( g.perm.RdWiki ){ |
| 1056 | 1056 | az[i++] = "w"; |
| 1057 | 1057 | az[i++] = "Wiki"; |
| 1058 | + } | |
| 1059 | + if( g.perm.RdForum ){ | |
| 1060 | + az[i++] = "f"; | |
| 1061 | + az[i++] = "Forum"; | |
| 1058 | 1062 | } |
| 1059 | 1063 | assert( i<=count(az) ); |
| 1060 | 1064 | } |
| 1061 | 1065 | if( i>2 ){ |
| 1062 | 1066 | style_submenu_multichoice("y", i/2, az, isDisabled); |
| @@ -1357,11 +1361,11 @@ | ||
| 1357 | 1361 | ** r=TAG Show check-ins related to TAG, equivalent to t=TAG&rel |
| 1358 | 1362 | ** rel Show related check-ins as well as those matching t=TAG |
| 1359 | 1363 | ** mionly Limit rel to show ancestors but not descendants |
| 1360 | 1364 | ** ms=MATCHSTYLE Set tag match style to EXACT, GLOB, LIKE, REGEXP |
| 1361 | 1365 | ** u=USER Only show items associated with USER |
| 1362 | -** y=TYPE 'ci', 'w', 't', 'e', or 'all'. | |
| 1366 | +** y=TYPE 'ci', 'w', 't', 'e', 'f', or 'all'. | |
| 1363 | 1367 | ** ss=VIEWSTYLE c: "Compact" v: "Verbose" m: "Modern" j: "Columnar" |
| 1364 | 1368 | ** advm Use the "Advanced" or "Busy" menu design. |
| 1365 | 1369 | ** ng No Graph. |
| 1366 | 1370 | ** nd Do not highlight the focus check-in |
| 1367 | 1371 | ** v Show details of files changed |
| @@ -1477,11 +1481,11 @@ | ||
| 1477 | 1481 | pd_rid = name_to_typed_rid(P("dp"),"ci"); |
| 1478 | 1482 | if( pd_rid ){ |
| 1479 | 1483 | p_rid = d_rid = pd_rid; |
| 1480 | 1484 | } |
| 1481 | 1485 | login_check_credentials(); |
| 1482 | - if( (!g.perm.Read && !g.perm.RdTkt && !g.perm.RdWiki) | |
| 1486 | + if( (!g.perm.Read && !g.perm.RdTkt && !g.perm.RdWiki && !g.perm.RdForum) | |
| 1483 | 1487 | || (bisectOnly && !g.perm.Setup) |
| 1484 | 1488 | ){ |
| 1485 | 1489 | login_needed(g.anon.Read && g.anon.RdTkt && g.anon.RdWiki); |
| 1486 | 1490 | return; |
| 1487 | 1491 | } |
| @@ -1854,10 +1858,11 @@ | ||
| 1854 | 1858 | if( (zType[0]=='w' && !g.perm.RdWiki) |
| 1855 | 1859 | || (zType[0]=='t' && !g.perm.RdTkt) |
| 1856 | 1860 | || (zType[0]=='e' && !g.perm.RdWiki) |
| 1857 | 1861 | || (zType[0]=='c' && !g.perm.Read) |
| 1858 | 1862 | || (zType[0]=='g' && !g.perm.Read) |
| 1863 | + || (zType[0]=='f' && !g.perm.RdForum) | |
| 1859 | 1864 | ){ |
| 1860 | 1865 | zType = "all"; |
| 1861 | 1866 | } |
| 1862 | 1867 | if( zType[0]=='a' ){ |
| 1863 | 1868 | if( !g.perm.Read || !g.perm.RdWiki || !g.perm.RdTkt ){ |
| @@ -1872,10 +1877,14 @@ | ||
| 1872 | 1877 | cSep = ','; |
| 1873 | 1878 | } |
| 1874 | 1879 | if( g.perm.RdTkt ){ |
| 1875 | 1880 | blob_append_sql(&cond, "%c't'", cSep); |
| 1876 | 1881 | cSep = ','; |
| 1882 | + } | |
| 1883 | + if( g.perm.RdForum ){ | |
| 1884 | + blob_append_sql(&cond, "%c'f'", cSep); | |
| 1885 | + cSep = ','; | |
| 1877 | 1886 | } |
| 1878 | 1887 | blob_append_sql(&cond, ")"); |
| 1879 | 1888 | } |
| 1880 | 1889 | }else{ /* zType!="all" */ |
| 1881 | 1890 | blob_append_sql(&cond, " AND event.type=%Q", zType); |
| @@ -1887,10 +1896,12 @@ | ||
| 1887 | 1896 | zEType = "ticket change"; |
| 1888 | 1897 | }else if( zType[0]=='e' ){ |
| 1889 | 1898 | zEType = "technical note"; |
| 1890 | 1899 | }else if( zType[0]=='g' ){ |
| 1891 | 1900 | zEType = "tag"; |
| 1901 | + }else if( zType[0]=='f' ){ | |
| 1902 | + zEType = "forum post"; | |
| 1892 | 1903 | } |
| 1893 | 1904 | } |
| 1894 | 1905 | if( zUser ){ |
| 1895 | 1906 | int n = db_int(0,"SELECT count(*) FROM event" |
| 1896 | 1907 | " WHERE user=%Q OR euser=%Q", zUser, zUser); |
| 1897 | 1908 |
| --- src/timeline.c | |
| +++ src/timeline.c | |
| @@ -574,11 +574,11 @@ | |
| 574 | cgi_printf("technote: "); |
| 575 | hyperlink_to_event_tagid(tagid<0?-tagid:tagid); |
| 576 | }else{ |
| 577 | cgi_printf("artifact: %z%S</a> ",href("%R/info/%!S",zUuid),zUuid); |
| 578 | } |
| 579 | }else if( zType[0]=='g' || zType[0]=='w' || zType[0]=='t' ){ |
| 580 | cgi_printf("artifact: %z%S</a> ",href("%R/info/%!S",zUuid),zUuid); |
| 581 | } |
| 582 | |
| 583 | if( g.perm.Hyperlink && fossil_strcmp(zDispUser, zThisUser)!=0 ){ |
| 584 | char *zLink = mprintf("%R/timeline?u=%h&c=%t&nd&n=200", zDispUser, zDate); |
| @@ -1031,11 +1031,11 @@ | |
| 1031 | ** set the y= parameter that determines which elements to display |
| 1032 | ** on the timeline. |
| 1033 | */ |
| 1034 | static void timeline_y_submenu(int isDisabled){ |
| 1035 | static int i = 0; |
| 1036 | static const char *az[12]; |
| 1037 | if( i==0 ){ |
| 1038 | az[0] = "all"; |
| 1039 | az[1] = "Any Type"; |
| 1040 | i = 2; |
| 1041 | if( g.perm.Read ){ |
| @@ -1053,10 +1053,14 @@ | |
| 1053 | az[i++] = "Tickets"; |
| 1054 | } |
| 1055 | if( g.perm.RdWiki ){ |
| 1056 | az[i++] = "w"; |
| 1057 | az[i++] = "Wiki"; |
| 1058 | } |
| 1059 | assert( i<=count(az) ); |
| 1060 | } |
| 1061 | if( i>2 ){ |
| 1062 | style_submenu_multichoice("y", i/2, az, isDisabled); |
| @@ -1357,11 +1361,11 @@ | |
| 1357 | ** r=TAG Show check-ins related to TAG, equivalent to t=TAG&rel |
| 1358 | ** rel Show related check-ins as well as those matching t=TAG |
| 1359 | ** mionly Limit rel to show ancestors but not descendants |
| 1360 | ** ms=MATCHSTYLE Set tag match style to EXACT, GLOB, LIKE, REGEXP |
| 1361 | ** u=USER Only show items associated with USER |
| 1362 | ** y=TYPE 'ci', 'w', 't', 'e', or 'all'. |
| 1363 | ** ss=VIEWSTYLE c: "Compact" v: "Verbose" m: "Modern" j: "Columnar" |
| 1364 | ** advm Use the "Advanced" or "Busy" menu design. |
| 1365 | ** ng No Graph. |
| 1366 | ** nd Do not highlight the focus check-in |
| 1367 | ** v Show details of files changed |
| @@ -1477,11 +1481,11 @@ | |
| 1477 | pd_rid = name_to_typed_rid(P("dp"),"ci"); |
| 1478 | if( pd_rid ){ |
| 1479 | p_rid = d_rid = pd_rid; |
| 1480 | } |
| 1481 | login_check_credentials(); |
| 1482 | if( (!g.perm.Read && !g.perm.RdTkt && !g.perm.RdWiki) |
| 1483 | || (bisectOnly && !g.perm.Setup) |
| 1484 | ){ |
| 1485 | login_needed(g.anon.Read && g.anon.RdTkt && g.anon.RdWiki); |
| 1486 | return; |
| 1487 | } |
| @@ -1854,10 +1858,11 @@ | |
| 1854 | if( (zType[0]=='w' && !g.perm.RdWiki) |
| 1855 | || (zType[0]=='t' && !g.perm.RdTkt) |
| 1856 | || (zType[0]=='e' && !g.perm.RdWiki) |
| 1857 | || (zType[0]=='c' && !g.perm.Read) |
| 1858 | || (zType[0]=='g' && !g.perm.Read) |
| 1859 | ){ |
| 1860 | zType = "all"; |
| 1861 | } |
| 1862 | if( zType[0]=='a' ){ |
| 1863 | if( !g.perm.Read || !g.perm.RdWiki || !g.perm.RdTkt ){ |
| @@ -1872,10 +1877,14 @@ | |
| 1872 | cSep = ','; |
| 1873 | } |
| 1874 | if( g.perm.RdTkt ){ |
| 1875 | blob_append_sql(&cond, "%c't'", cSep); |
| 1876 | cSep = ','; |
| 1877 | } |
| 1878 | blob_append_sql(&cond, ")"); |
| 1879 | } |
| 1880 | }else{ /* zType!="all" */ |
| 1881 | blob_append_sql(&cond, " AND event.type=%Q", zType); |
| @@ -1887,10 +1896,12 @@ | |
| 1887 | zEType = "ticket change"; |
| 1888 | }else if( zType[0]=='e' ){ |
| 1889 | zEType = "technical note"; |
| 1890 | }else if( zType[0]=='g' ){ |
| 1891 | zEType = "tag"; |
| 1892 | } |
| 1893 | } |
| 1894 | if( zUser ){ |
| 1895 | int n = db_int(0,"SELECT count(*) FROM event" |
| 1896 | " WHERE user=%Q OR euser=%Q", zUser, zUser); |
| 1897 |
| --- src/timeline.c | |
| +++ src/timeline.c | |
| @@ -574,11 +574,11 @@ | |
| 574 | cgi_printf("technote: "); |
| 575 | hyperlink_to_event_tagid(tagid<0?-tagid:tagid); |
| 576 | }else{ |
| 577 | cgi_printf("artifact: %z%S</a> ",href("%R/info/%!S",zUuid),zUuid); |
| 578 | } |
| 579 | }else if( zType[0]=='g' || zType[0]=='w' || zType[0]=='t' || zType[0]=='f'){ |
| 580 | cgi_printf("artifact: %z%S</a> ",href("%R/info/%!S",zUuid),zUuid); |
| 581 | } |
| 582 | |
| 583 | if( g.perm.Hyperlink && fossil_strcmp(zDispUser, zThisUser)!=0 ){ |
| 584 | char *zLink = mprintf("%R/timeline?u=%h&c=%t&nd&n=200", zDispUser, zDate); |
| @@ -1031,11 +1031,11 @@ | |
| 1031 | ** set the y= parameter that determines which elements to display |
| 1032 | ** on the timeline. |
| 1033 | */ |
| 1034 | static void timeline_y_submenu(int isDisabled){ |
| 1035 | static int i = 0; |
| 1036 | static const char *az[14]; |
| 1037 | if( i==0 ){ |
| 1038 | az[0] = "all"; |
| 1039 | az[1] = "Any Type"; |
| 1040 | i = 2; |
| 1041 | if( g.perm.Read ){ |
| @@ -1053,10 +1053,14 @@ | |
| 1053 | az[i++] = "Tickets"; |
| 1054 | } |
| 1055 | if( g.perm.RdWiki ){ |
| 1056 | az[i++] = "w"; |
| 1057 | az[i++] = "Wiki"; |
| 1058 | } |
| 1059 | if( g.perm.RdForum ){ |
| 1060 | az[i++] = "f"; |
| 1061 | az[i++] = "Forum"; |
| 1062 | } |
| 1063 | assert( i<=count(az) ); |
| 1064 | } |
| 1065 | if( i>2 ){ |
| 1066 | style_submenu_multichoice("y", i/2, az, isDisabled); |
| @@ -1357,11 +1361,11 @@ | |
| 1361 | ** r=TAG Show check-ins related to TAG, equivalent to t=TAG&rel |
| 1362 | ** rel Show related check-ins as well as those matching t=TAG |
| 1363 | ** mionly Limit rel to show ancestors but not descendants |
| 1364 | ** ms=MATCHSTYLE Set tag match style to EXACT, GLOB, LIKE, REGEXP |
| 1365 | ** u=USER Only show items associated with USER |
| 1366 | ** y=TYPE 'ci', 'w', 't', 'e', 'f', or 'all'. |
| 1367 | ** ss=VIEWSTYLE c: "Compact" v: "Verbose" m: "Modern" j: "Columnar" |
| 1368 | ** advm Use the "Advanced" or "Busy" menu design. |
| 1369 | ** ng No Graph. |
| 1370 | ** nd Do not highlight the focus check-in |
| 1371 | ** v Show details of files changed |
| @@ -1477,11 +1481,11 @@ | |
| 1481 | pd_rid = name_to_typed_rid(P("dp"),"ci"); |
| 1482 | if( pd_rid ){ |
| 1483 | p_rid = d_rid = pd_rid; |
| 1484 | } |
| 1485 | login_check_credentials(); |
| 1486 | if( (!g.perm.Read && !g.perm.RdTkt && !g.perm.RdWiki && !g.perm.RdForum) |
| 1487 | || (bisectOnly && !g.perm.Setup) |
| 1488 | ){ |
| 1489 | login_needed(g.anon.Read && g.anon.RdTkt && g.anon.RdWiki); |
| 1490 | return; |
| 1491 | } |
| @@ -1854,10 +1858,11 @@ | |
| 1858 | if( (zType[0]=='w' && !g.perm.RdWiki) |
| 1859 | || (zType[0]=='t' && !g.perm.RdTkt) |
| 1860 | || (zType[0]=='e' && !g.perm.RdWiki) |
| 1861 | || (zType[0]=='c' && !g.perm.Read) |
| 1862 | || (zType[0]=='g' && !g.perm.Read) |
| 1863 | || (zType[0]=='f' && !g.perm.RdForum) |
| 1864 | ){ |
| 1865 | zType = "all"; |
| 1866 | } |
| 1867 | if( zType[0]=='a' ){ |
| 1868 | if( !g.perm.Read || !g.perm.RdWiki || !g.perm.RdTkt ){ |
| @@ -1872,10 +1877,14 @@ | |
| 1877 | cSep = ','; |
| 1878 | } |
| 1879 | if( g.perm.RdTkt ){ |
| 1880 | blob_append_sql(&cond, "%c't'", cSep); |
| 1881 | cSep = ','; |
| 1882 | } |
| 1883 | if( g.perm.RdForum ){ |
| 1884 | blob_append_sql(&cond, "%c'f'", cSep); |
| 1885 | cSep = ','; |
| 1886 | } |
| 1887 | blob_append_sql(&cond, ")"); |
| 1888 | } |
| 1889 | }else{ /* zType!="all" */ |
| 1890 | blob_append_sql(&cond, " AND event.type=%Q", zType); |
| @@ -1887,10 +1896,12 @@ | |
| 1896 | zEType = "ticket change"; |
| 1897 | }else if( zType[0]=='e' ){ |
| 1898 | zEType = "technical note"; |
| 1899 | }else if( zType[0]=='g' ){ |
| 1900 | zEType = "tag"; |
| 1901 | }else if( zType[0]=='f' ){ |
| 1902 | zEType = "forum post"; |
| 1903 | } |
| 1904 | } |
| 1905 | if( zUser ){ |
| 1906 | int n = db_int(0,"SELECT count(*) FROM event" |
| 1907 | " WHERE user=%Q OR euser=%Q", zUser, zUser); |
| 1908 |
+2
-1
| --- src/webmail.c | ||
| +++ src/webmail.c | ||
| @@ -708,11 +708,11 @@ | ||
| 708 | 708 | @ <tr><td align="left"> |
| 709 | 709 | if( d==2 ){ |
| 710 | 710 | @ <input type="submit" name="read" value="Undelete"> |
| 711 | 711 | @ <input type="submit" name="purge" value="Delete Permanently"> |
| 712 | 712 | }else{ |
| 713 | - @ <input type="submit" name="trash", value="Delete"> | |
| 713 | + @ <input type="submit" name="trash" value="Delete"> | |
| 714 | 714 | if( d!=1 ){ |
| 715 | 715 | @ <input type="submit" name="unread" value="Mark as unread"> |
| 716 | 716 | } |
| 717 | 717 | @ <input type="submit" name="read" value="Mark as read"> |
| 718 | 718 | } |
| @@ -732,10 +732,11 @@ | ||
| 732 | 732 | while( db_step(&q)==SQLITE_ROW ){ |
| 733 | 733 | const char *zId = db_column_text(&q,0); |
| 734 | 734 | const char *zFrom = db_column_text(&q, 1); |
| 735 | 735 | const char *zDate = db_column_text(&q, 2); |
| 736 | 736 | const char *zSubject = db_column_text(&q, 4); |
| 737 | + if( zSubject==0 || zSubject[0]==0 ) zSubject = "(no subject)"; | |
| 737 | 738 | @ <tr> |
| 738 | 739 | @ <td><input type="checkbox" class="webmailckbox" name="e%s(zId)"></td> |
| 739 | 740 | @ <td>%h(zFrom)</td> |
| 740 | 741 | @ <td><a href="%s(url_render(&url,"id",zId,0,0))">%h(zSubject)</a> \ |
| 741 | 742 | @ %s(zDate)</td> |
| 742 | 743 |
| --- src/webmail.c | |
| +++ src/webmail.c | |
| @@ -708,11 +708,11 @@ | |
| 708 | @ <tr><td align="left"> |
| 709 | if( d==2 ){ |
| 710 | @ <input type="submit" name="read" value="Undelete"> |
| 711 | @ <input type="submit" name="purge" value="Delete Permanently"> |
| 712 | }else{ |
| 713 | @ <input type="submit" name="trash", value="Delete"> |
| 714 | if( d!=1 ){ |
| 715 | @ <input type="submit" name="unread" value="Mark as unread"> |
| 716 | } |
| 717 | @ <input type="submit" name="read" value="Mark as read"> |
| 718 | } |
| @@ -732,10 +732,11 @@ | |
| 732 | while( db_step(&q)==SQLITE_ROW ){ |
| 733 | const char *zId = db_column_text(&q,0); |
| 734 | const char *zFrom = db_column_text(&q, 1); |
| 735 | const char *zDate = db_column_text(&q, 2); |
| 736 | const char *zSubject = db_column_text(&q, 4); |
| 737 | @ <tr> |
| 738 | @ <td><input type="checkbox" class="webmailckbox" name="e%s(zId)"></td> |
| 739 | @ <td>%h(zFrom)</td> |
| 740 | @ <td><a href="%s(url_render(&url,"id",zId,0,0))">%h(zSubject)</a> \ |
| 741 | @ %s(zDate)</td> |
| 742 |
| --- src/webmail.c | |
| +++ src/webmail.c | |
| @@ -708,11 +708,11 @@ | |
| 708 | @ <tr><td align="left"> |
| 709 | if( d==2 ){ |
| 710 | @ <input type="submit" name="read" value="Undelete"> |
| 711 | @ <input type="submit" name="purge" value="Delete Permanently"> |
| 712 | }else{ |
| 713 | @ <input type="submit" name="trash" value="Delete"> |
| 714 | if( d!=1 ){ |
| 715 | @ <input type="submit" name="unread" value="Mark as unread"> |
| 716 | } |
| 717 | @ <input type="submit" name="read" value="Mark as read"> |
| 718 | } |
| @@ -732,10 +732,11 @@ | |
| 732 | while( db_step(&q)==SQLITE_ROW ){ |
| 733 | const char *zId = db_column_text(&q,0); |
| 734 | const char *zFrom = db_column_text(&q, 1); |
| 735 | const char *zDate = db_column_text(&q, 2); |
| 736 | const char *zSubject = db_column_text(&q, 4); |
| 737 | if( zSubject==0 || zSubject[0]==0 ) zSubject = "(no subject)"; |
| 738 | @ <tr> |
| 739 | @ <td><input type="checkbox" class="webmailckbox" name="e%s(zId)"></td> |
| 740 | @ <td>%h(zFrom)</td> |
| 741 | @ <td><a href="%s(url_render(&url,"id",zId,0,0))">%h(zSubject)</a> \ |
| 742 | @ %s(zDate)</td> |
| 743 |
+4
-3
| --- src/wiki.c | ||
| +++ src/wiki.c | ||
| @@ -3,11 +3,11 @@ | ||
| 3 | 3 | ** Copyright (c) 2008 Stephan Beal |
| 4 | 4 | ** |
| 5 | 5 | ** This program is free software; you can redistribute it and/or |
| 6 | 6 | ** modify it under the terms of the Simplified BSD License (also |
| 7 | 7 | ** known as the "2-Clause License" or "FreeBSD License".) |
| 8 | - | |
| 8 | +** | |
| 9 | 9 | ** This program is distributed in the hope that it will be useful, |
| 10 | 10 | ** but without any warranty; without even the implied warranty of |
| 11 | 11 | ** merchantability or fitness for a particular purpose. |
| 12 | 12 | ** |
| 13 | 13 | ** Author contact information: |
| @@ -167,11 +167,11 @@ | ||
| 167 | 167 | Blob tail = BLOB_INITIALIZER; |
| 168 | 168 | markdown_to_html(pWiki, 0, &tail); |
| 169 | 169 | @ %s(blob_str(&tail)) |
| 170 | 170 | blob_reset(&tail); |
| 171 | 171 | }else{ |
| 172 | - @ <pre> | |
| 172 | + @ <pre class='textPlain'> | |
| 173 | 173 | @ %h(blob_str(pWiki)) |
| 174 | 174 | @ </pre> |
| 175 | 175 | } |
| 176 | 176 | } |
| 177 | 177 | |
| @@ -426,11 +426,11 @@ | ||
| 426 | 426 | } |
| 427 | 427 | |
| 428 | 428 | /* |
| 429 | 429 | ** Write a wiki artifact into the repository |
| 430 | 430 | */ |
| 431 | -static void wiki_put(Blob *pWiki, int parent, int needMod){ | |
| 431 | +int wiki_put(Blob *pWiki, int parent, int needMod){ | |
| 432 | 432 | int nrid; |
| 433 | 433 | if( !needMod ){ |
| 434 | 434 | nrid = content_put_ex(pWiki, 0, 0, 0, 0); |
| 435 | 435 | if( parent) content_deltify(parent, &nrid, 1, 0); |
| 436 | 436 | }else{ |
| @@ -439,10 +439,11 @@ | ||
| 439 | 439 | db_multi_exec("INSERT INTO modreq(objid) VALUES(%d)", nrid); |
| 440 | 440 | } |
| 441 | 441 | db_multi_exec("INSERT OR IGNORE INTO unsent VALUES(%d)", nrid); |
| 442 | 442 | db_multi_exec("INSERT OR IGNORE INTO unclustered VALUES(%d);", nrid); |
| 443 | 443 | manifest_crosslink(nrid, pWiki, MC_NONE); |
| 444 | + return nrid; | |
| 444 | 445 | } |
| 445 | 446 | |
| 446 | 447 | /* |
| 447 | 448 | ** Output a selection box from which the user can select the |
| 448 | 449 | ** wiki mimetype. |
| 449 | 450 |
| --- src/wiki.c | |
| +++ src/wiki.c | |
| @@ -3,11 +3,11 @@ | |
| 3 | ** Copyright (c) 2008 Stephan Beal |
| 4 | ** |
| 5 | ** This program is free software; you can redistribute it and/or |
| 6 | ** modify it under the terms of the Simplified BSD License (also |
| 7 | ** known as the "2-Clause License" or "FreeBSD License".) |
| 8 | |
| 9 | ** This program is distributed in the hope that it will be useful, |
| 10 | ** but without any warranty; without even the implied warranty of |
| 11 | ** merchantability or fitness for a particular purpose. |
| 12 | ** |
| 13 | ** Author contact information: |
| @@ -167,11 +167,11 @@ | |
| 167 | Blob tail = BLOB_INITIALIZER; |
| 168 | markdown_to_html(pWiki, 0, &tail); |
| 169 | @ %s(blob_str(&tail)) |
| 170 | blob_reset(&tail); |
| 171 | }else{ |
| 172 | @ <pre> |
| 173 | @ %h(blob_str(pWiki)) |
| 174 | @ </pre> |
| 175 | } |
| 176 | } |
| 177 | |
| @@ -426,11 +426,11 @@ | |
| 426 | } |
| 427 | |
| 428 | /* |
| 429 | ** Write a wiki artifact into the repository |
| 430 | */ |
| 431 | static void wiki_put(Blob *pWiki, int parent, int needMod){ |
| 432 | int nrid; |
| 433 | if( !needMod ){ |
| 434 | nrid = content_put_ex(pWiki, 0, 0, 0, 0); |
| 435 | if( parent) content_deltify(parent, &nrid, 1, 0); |
| 436 | }else{ |
| @@ -439,10 +439,11 @@ | |
| 439 | db_multi_exec("INSERT INTO modreq(objid) VALUES(%d)", nrid); |
| 440 | } |
| 441 | db_multi_exec("INSERT OR IGNORE INTO unsent VALUES(%d)", nrid); |
| 442 | db_multi_exec("INSERT OR IGNORE INTO unclustered VALUES(%d);", nrid); |
| 443 | manifest_crosslink(nrid, pWiki, MC_NONE); |
| 444 | } |
| 445 | |
| 446 | /* |
| 447 | ** Output a selection box from which the user can select the |
| 448 | ** wiki mimetype. |
| 449 |
| --- src/wiki.c | |
| +++ src/wiki.c | |
| @@ -3,11 +3,11 @@ | |
| 3 | ** Copyright (c) 2008 Stephan Beal |
| 4 | ** |
| 5 | ** This program is free software; you can redistribute it and/or |
| 6 | ** modify it under the terms of the Simplified BSD License (also |
| 7 | ** known as the "2-Clause License" or "FreeBSD License".) |
| 8 | ** |
| 9 | ** This program is distributed in the hope that it will be useful, |
| 10 | ** but without any warranty; without even the implied warranty of |
| 11 | ** merchantability or fitness for a particular purpose. |
| 12 | ** |
| 13 | ** Author contact information: |
| @@ -167,11 +167,11 @@ | |
| 167 | Blob tail = BLOB_INITIALIZER; |
| 168 | markdown_to_html(pWiki, 0, &tail); |
| 169 | @ %s(blob_str(&tail)) |
| 170 | blob_reset(&tail); |
| 171 | }else{ |
| 172 | @ <pre class='textPlain'> |
| 173 | @ %h(blob_str(pWiki)) |
| 174 | @ </pre> |
| 175 | } |
| 176 | } |
| 177 | |
| @@ -426,11 +426,11 @@ | |
| 426 | } |
| 427 | |
| 428 | /* |
| 429 | ** Write a wiki artifact into the repository |
| 430 | */ |
| 431 | int wiki_put(Blob *pWiki, int parent, int needMod){ |
| 432 | int nrid; |
| 433 | if( !needMod ){ |
| 434 | nrid = content_put_ex(pWiki, 0, 0, 0, 0); |
| 435 | if( parent) content_deltify(parent, &nrid, 1, 0); |
| 436 | }else{ |
| @@ -439,10 +439,11 @@ | |
| 439 | db_multi_exec("INSERT INTO modreq(objid) VALUES(%d)", nrid); |
| 440 | } |
| 441 | db_multi_exec("INSERT OR IGNORE INTO unsent VALUES(%d)", nrid); |
| 442 | db_multi_exec("INSERT OR IGNORE INTO unclustered VALUES(%d);", nrid); |
| 443 | manifest_crosslink(nrid, pWiki, MC_NONE); |
| 444 | return nrid; |
| 445 | } |
| 446 | |
| 447 | /* |
| 448 | ** Output a selection box from which the user can select the |
| 449 | ** wiki mimetype. |
| 450 |
+1
-1
| --- src/xfer.c | ||
| +++ src/xfer.c | ||
| @@ -1306,11 +1306,11 @@ | ||
| 1306 | 1306 | && blob_is_hname(&xfer.aToken[2]) |
| 1307 | 1307 | ){ |
| 1308 | 1308 | const char *zPCode; |
| 1309 | 1309 | zPCode = db_get("project-code", 0); |
| 1310 | 1310 | if( zPCode==0 ){ |
| 1311 | - fossil_panic("missing project code"); | |
| 1311 | + fossil_fatal("missing project code"); | |
| 1312 | 1312 | } |
| 1313 | 1313 | if( !blob_eq_str(&xfer.aToken[2], zPCode, -1) ){ |
| 1314 | 1314 | cgi_reset_content(); |
| 1315 | 1315 | @ error wrong\sproject |
| 1316 | 1316 | nErr++; |
| 1317 | 1317 |
| --- src/xfer.c | |
| +++ src/xfer.c | |
| @@ -1306,11 +1306,11 @@ | |
| 1306 | && blob_is_hname(&xfer.aToken[2]) |
| 1307 | ){ |
| 1308 | const char *zPCode; |
| 1309 | zPCode = db_get("project-code", 0); |
| 1310 | if( zPCode==0 ){ |
| 1311 | fossil_panic("missing project code"); |
| 1312 | } |
| 1313 | if( !blob_eq_str(&xfer.aToken[2], zPCode, -1) ){ |
| 1314 | cgi_reset_content(); |
| 1315 | @ error wrong\sproject |
| 1316 | nErr++; |
| 1317 |
| --- src/xfer.c | |
| +++ src/xfer.c | |
| @@ -1306,11 +1306,11 @@ | |
| 1306 | && blob_is_hname(&xfer.aToken[2]) |
| 1307 | ){ |
| 1308 | const char *zPCode; |
| 1309 | zPCode = db_get("project-code", 0); |
| 1310 | if( zPCode==0 ){ |
| 1311 | fossil_fatal("missing project code"); |
| 1312 | } |
| 1313 | if( !blob_eq_str(&xfer.aToken[2], zPCode, -1) ){ |
| 1314 | cgi_reset_content(); |
| 1315 | @ error wrong\sproject |
| 1316 | nErr++; |
| 1317 |
+10
-4
| --- win/Makefile.dmc | ||
| +++ win/Makefile.dmc | ||
| @@ -28,13 +28,13 @@ | ||
| 28 | 28 | |
| 29 | 29 | SQLITE_OPTIONS = -DNDEBUG=1 -DSQLITE_THREADSAFE=0 -DSQLITE_DEFAULT_MEMSTATUS=0 -DSQLITE_DEFAULT_WAL_SYNCHRONOUS=1 -DSQLITE_LIKE_DOESNT_MATCH_BLOBS -DSQLITE_OMIT_DECLTYPE -DSQLITE_OMIT_DEPRECATED -DSQLITE_OMIT_GET_TABLE -DSQLITE_OMIT_PROGRESS_CALLBACK -DSQLITE_OMIT_SHARED_CACHE -DSQLITE_OMIT_LOAD_EXTENSION -DSQLITE_MAX_EXPR_DEPTH=0 -DSQLITE_USE_ALLOCA -DSQLITE_ENABLE_LOCKING_STYLE=0 -DSQLITE_DEFAULT_FILE_FORMAT=4 -DSQLITE_ENABLE_EXPLAIN_COMMENTS -DSQLITE_ENABLE_FTS4 -DSQLITE_ENABLE_FTS3_PARENTHESIS -DSQLITE_ENABLE_DBSTAT_VTAB -DSQLITE_ENABLE_JSON1 -DSQLITE_ENABLE_FTS5 -DSQLITE_ENABLE_STMTVTAB -DSQLITE_HAVE_ZLIB -DSQLITE_INTROSPECTION_PRAGMAS -DSQLITE_ENABLE_DBPAGE_VTAB |
| 30 | 30 | |
| 31 | 31 | SHELL_OPTIONS = -DNDEBUG=1 -DSQLITE_THREADSAFE=0 -DSQLITE_DEFAULT_MEMSTATUS=0 -DSQLITE_DEFAULT_WAL_SYNCHRONOUS=1 -DSQLITE_LIKE_DOESNT_MATCH_BLOBS -DSQLITE_OMIT_DECLTYPE -DSQLITE_OMIT_DEPRECATED -DSQLITE_OMIT_GET_TABLE -DSQLITE_OMIT_PROGRESS_CALLBACK -DSQLITE_OMIT_SHARED_CACHE -DSQLITE_OMIT_LOAD_EXTENSION -DSQLITE_MAX_EXPR_DEPTH=0 -DSQLITE_USE_ALLOCA -DSQLITE_ENABLE_LOCKING_STYLE=0 -DSQLITE_DEFAULT_FILE_FORMAT=4 -DSQLITE_ENABLE_EXPLAIN_COMMENTS -DSQLITE_ENABLE_FTS4 -DSQLITE_ENABLE_FTS3_PARENTHESIS -DSQLITE_ENABLE_DBSTAT_VTAB -DSQLITE_ENABLE_JSON1 -DSQLITE_ENABLE_FTS5 -DSQLITE_ENABLE_STMTVTAB -DSQLITE_HAVE_ZLIB -DSQLITE_INTROSPECTION_PRAGMAS -DSQLITE_ENABLE_DBPAGE_VTAB -Dmain=sqlite3_shell -DSQLITE_SHELL_IS_UTF8=1 -DSQLITE_OMIT_LOAD_EXTENSION=1 -DUSE_SYSTEM_SQLITE=$(USE_SYSTEM_SQLITE) -DSQLITE_SHELL_DBNAME_PROC=sqlcmd_get_dbname -DSQLITE_SHELL_INIT_PROC=sqlcmd_init_proc -Daccess=file_access -Dsystem=fossil_system -Dgetenv=fossil_getenv -Dfopen=fossil_fopen |
| 32 | 32 | |
| 33 | -SRC = add_.c allrepo_.c attach_.c backoffice_.c bag_.c bisect_.c blob_.c branch_.c browse_.c builtin_.c bundle_.c cache_.c captcha_.c cgi_.c checkin_.c checkout_.c clearsign_.c clone_.c comformat_.c configure_.c content_.c cookies_.c db_.c delta_.c deltacmd_.c descendants_.c diff_.c diffcmd_.c dispatch_.c doc_.c email_.c encode_.c etag_.c event_.c export_.c file_.c finfo_.c foci_.c forum_.c fshell_.c fusefs_.c glob_.c graph_.c gzip_.c hname_.c http_.c http_socket_.c http_ssl_.c http_transport_.c import_.c info_.c json_.c json_artifact_.c json_branch_.c json_config_.c json_diff_.c json_dir_.c json_finfo_.c json_login_.c json_query_.c json_report_.c json_status_.c json_tag_.c json_timeline_.c json_user_.c json_wiki_.c leaf_.c loadctrl_.c login_.c lookslike_.c main_.c manifest_.c markdown_.c markdown_html_.c md5_.c merge_.c merge3_.c moderate_.c name_.c path_.c piechart_.c pivot_.c popen_.c pqueue_.c printf_.c publish_.c purge_.c rebuild_.c regexp_.c report_.c rss_.c schema_.c search_.c security_audit_.c setup_.c sha1_.c sha1hard_.c sha3_.c shun_.c sitemap_.c skins_.c smtp_.c sqlcmd_.c stash_.c stat_.c statrep_.c style_.c sync_.c tag_.c tar_.c th_main_.c timeline_.c tkt_.c tktsetup_.c undo_.c unicode_.c unversioned_.c update_.c url_.c user_.c utf8_.c util_.c verify_.c vfile_.c webmail_.c wiki_.c wikiformat_.c winfile_.c winhttp_.c wysiwyg_.c xfer_.c xfersetup_.c zip_.c | |
| 33 | +SRC = add_.c allrepo_.c attach_.c backoffice_.c bag_.c bisect_.c blob_.c branch_.c browse_.c builtin_.c bundle_.c cache_.c capabilities_.c captcha_.c cgi_.c checkin_.c checkout_.c clearsign_.c clone_.c comformat_.c configure_.c content_.c cookies_.c db_.c delta_.c deltacmd_.c descendants_.c diff_.c diffcmd_.c dispatch_.c doc_.c email_.c encode_.c etag_.c event_.c export_.c file_.c finfo_.c foci_.c forum_.c fshell_.c fusefs_.c glob_.c graph_.c gzip_.c hname_.c http_.c http_socket_.c http_ssl_.c http_transport_.c import_.c info_.c json_.c json_artifact_.c json_branch_.c json_config_.c json_diff_.c json_dir_.c json_finfo_.c json_login_.c json_query_.c json_report_.c json_status_.c json_tag_.c json_timeline_.c json_user_.c json_wiki_.c leaf_.c loadctrl_.c login_.c lookslike_.c main_.c manifest_.c markdown_.c markdown_html_.c md5_.c merge_.c merge3_.c moderate_.c name_.c path_.c piechart_.c pivot_.c popen_.c pqueue_.c printf_.c publish_.c purge_.c rebuild_.c regexp_.c report_.c rss_.c schema_.c search_.c security_audit_.c setup_.c sha1_.c sha1hard_.c sha3_.c shun_.c sitemap_.c skins_.c smtp_.c sqlcmd_.c stash_.c stat_.c statrep_.c style_.c sync_.c tag_.c tar_.c th_main_.c timeline_.c tkt_.c tktsetup_.c undo_.c unicode_.c unversioned_.c update_.c url_.c user_.c utf8_.c util_.c verify_.c vfile_.c webmail_.c wiki_.c wikiformat_.c winfile_.c winhttp_.c wysiwyg_.c xfer_.c xfersetup_.c zip_.c | |
| 34 | 34 | |
| 35 | -OBJ = $(OBJDIR)\add$O $(OBJDIR)\allrepo$O $(OBJDIR)\attach$O $(OBJDIR)\backoffice$O $(OBJDIR)\bag$O $(OBJDIR)\bisect$O $(OBJDIR)\blob$O $(OBJDIR)\branch$O $(OBJDIR)\browse$O $(OBJDIR)\builtin$O $(OBJDIR)\bundle$O $(OBJDIR)\cache$O $(OBJDIR)\captcha$O $(OBJDIR)\cgi$O $(OBJDIR)\checkin$O $(OBJDIR)\checkout$O $(OBJDIR)\clearsign$O $(OBJDIR)\clone$O $(OBJDIR)\comformat$O $(OBJDIR)\configure$O $(OBJDIR)\content$O $(OBJDIR)\cookies$O $(OBJDIR)\db$O $(OBJDIR)\delta$O $(OBJDIR)\deltacmd$O $(OBJDIR)\descendants$O $(OBJDIR)\diff$O $(OBJDIR)\diffcmd$O $(OBJDIR)\dispatch$O $(OBJDIR)\doc$O $(OBJDIR)\email$O $(OBJDIR)\encode$O $(OBJDIR)\etag$O $(OBJDIR)\event$O $(OBJDIR)\export$O $(OBJDIR)\file$O $(OBJDIR)\finfo$O $(OBJDIR)\foci$O $(OBJDIR)\forum$O $(OBJDIR)\fshell$O $(OBJDIR)\fusefs$O $(OBJDIR)\glob$O $(OBJDIR)\graph$O $(OBJDIR)\gzip$O $(OBJDIR)\hname$O $(OBJDIR)\http$O $(OBJDIR)\http_socket$O $(OBJDIR)\http_ssl$O $(OBJDIR)\http_transport$O $(OBJDIR)\import$O $(OBJDIR)\info$O $(OBJDIR)\json$O $(OBJDIR)\json_artifact$O $(OBJDIR)\json_branch$O $(OBJDIR)\json_config$O $(OBJDIR)\json_diff$O $(OBJDIR)\json_dir$O $(OBJDIR)\json_finfo$O $(OBJDIR)\json_login$O $(OBJDIR)\json_query$O $(OBJDIR)\json_report$O $(OBJDIR)\json_status$O $(OBJDIR)\json_tag$O $(OBJDIR)\json_timeline$O $(OBJDIR)\json_user$O $(OBJDIR)\json_wiki$O $(OBJDIR)\leaf$O $(OBJDIR)\loadctrl$O $(OBJDIR)\login$O $(OBJDIR)\lookslike$O $(OBJDIR)\main$O $(OBJDIR)\manifest$O $(OBJDIR)\markdown$O $(OBJDIR)\markdown_html$O $(OBJDIR)\md5$O $(OBJDIR)\merge$O $(OBJDIR)\merge3$O $(OBJDIR)\moderate$O $(OBJDIR)\name$O $(OBJDIR)\path$O $(OBJDIR)\piechart$O $(OBJDIR)\pivot$O $(OBJDIR)\popen$O $(OBJDIR)\pqueue$O $(OBJDIR)\printf$O $(OBJDIR)\publish$O $(OBJDIR)\purge$O $(OBJDIR)\rebuild$O $(OBJDIR)\regexp$O $(OBJDIR)\report$O $(OBJDIR)\rss$O $(OBJDIR)\schema$O $(OBJDIR)\search$O $(OBJDIR)\security_audit$O $(OBJDIR)\setup$O $(OBJDIR)\sha1$O $(OBJDIR)\sha1hard$O $(OBJDIR)\sha3$O $(OBJDIR)\shun$O $(OBJDIR)\sitemap$O $(OBJDIR)\skins$O $(OBJDIR)\smtp$O $(OBJDIR)\sqlcmd$O $(OBJDIR)\stash$O $(OBJDIR)\stat$O $(OBJDIR)\statrep$O $(OBJDIR)\style$O $(OBJDIR)\sync$O $(OBJDIR)\tag$O $(OBJDIR)\tar$O $(OBJDIR)\th_main$O $(OBJDIR)\timeline$O $(OBJDIR)\tkt$O $(OBJDIR)\tktsetup$O $(OBJDIR)\undo$O $(OBJDIR)\unicode$O $(OBJDIR)\unversioned$O $(OBJDIR)\update$O $(OBJDIR)\url$O $(OBJDIR)\user$O $(OBJDIR)\utf8$O $(OBJDIR)\util$O $(OBJDIR)\verify$O $(OBJDIR)\vfile$O $(OBJDIR)\webmail$O $(OBJDIR)\wiki$O $(OBJDIR)\wikiformat$O $(OBJDIR)\winfile$O $(OBJDIR)\winhttp$O $(OBJDIR)\wysiwyg$O $(OBJDIR)\xfer$O $(OBJDIR)\xfersetup$O $(OBJDIR)\zip$O $(OBJDIR)\shell$O $(OBJDIR)\sqlite3$O $(OBJDIR)\th$O $(OBJDIR)\th_lang$O | |
| 35 | +OBJ = $(OBJDIR)\add$O $(OBJDIR)\allrepo$O $(OBJDIR)\attach$O $(OBJDIR)\backoffice$O $(OBJDIR)\bag$O $(OBJDIR)\bisect$O $(OBJDIR)\blob$O $(OBJDIR)\branch$O $(OBJDIR)\browse$O $(OBJDIR)\builtin$O $(OBJDIR)\bundle$O $(OBJDIR)\cache$O $(OBJDIR)\capabilities$O $(OBJDIR)\captcha$O $(OBJDIR)\cgi$O $(OBJDIR)\checkin$O $(OBJDIR)\checkout$O $(OBJDIR)\clearsign$O $(OBJDIR)\clone$O $(OBJDIR)\comformat$O $(OBJDIR)\configure$O $(OBJDIR)\content$O $(OBJDIR)\cookies$O $(OBJDIR)\db$O $(OBJDIR)\delta$O $(OBJDIR)\deltacmd$O $(OBJDIR)\descendants$O $(OBJDIR)\diff$O $(OBJDIR)\diffcmd$O $(OBJDIR)\dispatch$O $(OBJDIR)\doc$O $(OBJDIR)\email$O $(OBJDIR)\encode$O $(OBJDIR)\etag$O $(OBJDIR)\event$O $(OBJDIR)\export$O $(OBJDIR)\file$O $(OBJDIR)\finfo$O $(OBJDIR)\foci$O $(OBJDIR)\forum$O $(OBJDIR)\fshell$O $(OBJDIR)\fusefs$O $(OBJDIR)\glob$O $(OBJDIR)\graph$O $(OBJDIR)\gzip$O $(OBJDIR)\hname$O $(OBJDIR)\http$O $(OBJDIR)\http_socket$O $(OBJDIR)\http_ssl$O $(OBJDIR)\http_transport$O $(OBJDIR)\import$O $(OBJDIR)\info$O $(OBJDIR)\json$O $(OBJDIR)\json_artifact$O $(OBJDIR)\json_branch$O $(OBJDIR)\json_config$O $(OBJDIR)\json_diff$O $(OBJDIR)\json_dir$O $(OBJDIR)\json_finfo$O $(OBJDIR)\json_login$O $(OBJDIR)\json_query$O $(OBJDIR)\json_report$O $(OBJDIR)\json_status$O $(OBJDIR)\json_tag$O $(OBJDIR)\json_timeline$O $(OBJDIR)\json_user$O $(OBJDIR)\json_wiki$O $(OBJDIR)\leaf$O $(OBJDIR)\loadctrl$O $(OBJDIR)\login$O $(OBJDIR)\lookslike$O $(OBJDIR)\main$O $(OBJDIR)\manifest$O $(OBJDIR)\markdown$O $(OBJDIR)\markdown_html$O $(OBJDIR)\md5$O $(OBJDIR)\merge$O $(OBJDIR)\merge3$O $(OBJDIR)\moderate$O $(OBJDIR)\name$O $(OBJDIR)\path$O $(OBJDIR)\piechart$O $(OBJDIR)\pivot$O $(OBJDIR)\popen$O $(OBJDIR)\pqueue$O $(OBJDIR)\printf$O $(OBJDIR)\publish$O $(OBJDIR)\purge$O $(OBJDIR)\rebuild$O $(OBJDIR)\regexp$O $(OBJDIR)\report$O $(OBJDIR)\rss$O $(OBJDIR)\schema$O $(OBJDIR)\search$O $(OBJDIR)\security_audit$O $(OBJDIR)\setup$O $(OBJDIR)\sha1$O $(OBJDIR)\sha1hard$O $(OBJDIR)\sha3$O $(OBJDIR)\shun$O $(OBJDIR)\sitemap$O $(OBJDIR)\skins$O $(OBJDIR)\smtp$O $(OBJDIR)\sqlcmd$O $(OBJDIR)\stash$O $(OBJDIR)\stat$O $(OBJDIR)\statrep$O $(OBJDIR)\style$O $(OBJDIR)\sync$O $(OBJDIR)\tag$O $(OBJDIR)\tar$O $(OBJDIR)\th_main$O $(OBJDIR)\timeline$O $(OBJDIR)\tkt$O $(OBJDIR)\tktsetup$O $(OBJDIR)\undo$O $(OBJDIR)\unicode$O $(OBJDIR)\unversioned$O $(OBJDIR)\update$O $(OBJDIR)\url$O $(OBJDIR)\user$O $(OBJDIR)\utf8$O $(OBJDIR)\util$O $(OBJDIR)\verify$O $(OBJDIR)\vfile$O $(OBJDIR)\webmail$O $(OBJDIR)\wiki$O $(OBJDIR)\wikiformat$O $(OBJDIR)\winfile$O $(OBJDIR)\winhttp$O $(OBJDIR)\wysiwyg$O $(OBJDIR)\xfer$O $(OBJDIR)\xfersetup$O $(OBJDIR)\zip$O $(OBJDIR)\shell$O $(OBJDIR)\sqlite3$O $(OBJDIR)\th$O $(OBJDIR)\th_lang$O | |
| 36 | 36 | |
| 37 | 37 | |
| 38 | 38 | RC=$(DMDIR)\bin\rcc |
| 39 | 39 | RCFLAGS=-32 -w1 -I$(SRCDIR) /D__DMC__ |
| 40 | 40 | |
| @@ -49,11 +49,11 @@ | ||
| 49 | 49 | |
| 50 | 50 | $(OBJDIR)\fossil.res: $B\win\fossil.rc |
| 51 | 51 | $(RC) $(RCFLAGS) -o$@ $** |
| 52 | 52 | |
| 53 | 53 | $(OBJDIR)\link: $B\win\Makefile.dmc $(OBJDIR)\fossil.res |
| 54 | - +echo add allrepo attach backoffice bag bisect blob branch browse builtin bundle cache captcha cgi checkin checkout clearsign clone comformat configure content cookies db delta deltacmd descendants diff diffcmd dispatch doc email encode etag event export file finfo foci forum fshell fusefs glob graph gzip hname http http_socket http_ssl http_transport import info json json_artifact json_branch json_config json_diff json_dir json_finfo json_login json_query json_report json_status json_tag json_timeline json_user json_wiki leaf loadctrl login lookslike main manifest markdown markdown_html md5 merge merge3 moderate name path piechart pivot popen pqueue printf publish purge rebuild regexp report rss schema search security_audit setup sha1 sha1hard sha3 shun sitemap skins smtp sqlcmd stash stat statrep style sync tag tar th_main timeline tkt tktsetup undo unicode unversioned update url user utf8 util verify vfile webmail wiki wikiformat winfile winhttp wysiwyg xfer xfersetup zip shell sqlite3 th th_lang > $@ | |
| 54 | + +echo add allrepo attach backoffice bag bisect blob branch browse builtin bundle cache capabilities captcha cgi checkin checkout clearsign clone comformat configure content cookies db delta deltacmd descendants diff diffcmd dispatch doc email encode etag event export file finfo foci forum fshell fusefs glob graph gzip hname http http_socket http_ssl http_transport import info json json_artifact json_branch json_config json_diff json_dir json_finfo json_login json_query json_report json_status json_tag json_timeline json_user json_wiki leaf loadctrl login lookslike main manifest markdown markdown_html md5 merge merge3 moderate name path piechart pivot popen pqueue printf publish purge rebuild regexp report rss schema search security_audit setup sha1 sha1hard sha3 shun sitemap skins smtp sqlcmd stash stat statrep style sync tag tar th_main timeline tkt tktsetup undo unicode unversioned update url user utf8 util verify vfile webmail wiki wikiformat winfile winhttp wysiwyg xfer xfersetup zip shell sqlite3 th th_lang > $@ | |
| 55 | 55 | +echo fossil >> $@ |
| 56 | 56 | +echo fossil >> $@ |
| 57 | 57 | +echo $(LIBS) >> $@ |
| 58 | 58 | +echo. >> $@ |
| 59 | 59 | +echo fossil >> $@ |
| @@ -200,10 +200,16 @@ | ||
| 200 | 200 | $(OBJDIR)\cache$O : cache_.c cache.h |
| 201 | 201 | $(TCC) -o$@ -c cache_.c |
| 202 | 202 | |
| 203 | 203 | cache_.c : $(SRCDIR)\cache.c |
| 204 | 204 | +translate$E $** > $@ |
| 205 | + | |
| 206 | +$(OBJDIR)\capabilities$O : capabilities_.c capabilities.h | |
| 207 | + $(TCC) -o$@ -c capabilities_.c | |
| 208 | + | |
| 209 | +capabilities_.c : $(SRCDIR)\capabilities.c | |
| 210 | + +translate$E $** > $@ | |
| 205 | 211 | |
| 206 | 212 | $(OBJDIR)\captcha$O : captcha_.c captcha.h |
| 207 | 213 | $(TCC) -o$@ -c captcha_.c |
| 208 | 214 | |
| 209 | 215 | captcha_.c : $(SRCDIR)\captcha.c |
| @@ -928,7 +934,7 @@ | ||
| 928 | 934 | |
| 929 | 935 | zip_.c : $(SRCDIR)\zip.c |
| 930 | 936 | +translate$E $** > $@ |
| 931 | 937 | |
| 932 | 938 | headers: makeheaders$E page_index.h builtin_data.h default_css.h VERSION.h |
| 933 | - +makeheaders$E add_.c:add.h allrepo_.c:allrepo.h attach_.c:attach.h backoffice_.c:backoffice.h bag_.c:bag.h bisect_.c:bisect.h blob_.c:blob.h branch_.c:branch.h browse_.c:browse.h builtin_.c:builtin.h bundle_.c:bundle.h cache_.c:cache.h captcha_.c:captcha.h cgi_.c:cgi.h checkin_.c:checkin.h checkout_.c:checkout.h clearsign_.c:clearsign.h clone_.c:clone.h comformat_.c:comformat.h configure_.c:configure.h content_.c:content.h cookies_.c:cookies.h db_.c:db.h delta_.c:delta.h deltacmd_.c:deltacmd.h descendants_.c:descendants.h diff_.c:diff.h diffcmd_.c:diffcmd.h dispatch_.c:dispatch.h doc_.c:doc.h email_.c:email.h encode_.c:encode.h etag_.c:etag.h event_.c:event.h export_.c:export.h file_.c:file.h finfo_.c:finfo.h foci_.c:foci.h forum_.c:forum.h fshell_.c:fshell.h fusefs_.c:fusefs.h glob_.c:glob.h graph_.c:graph.h gzip_.c:gzip.h hname_.c:hname.h http_.c:http.h http_socket_.c:http_socket.h http_ssl_.c:http_ssl.h http_transport_.c:http_transport.h import_.c:import.h info_.c:info.h json_.c:json.h json_artifact_.c:json_artifact.h json_branch_.c:json_branch.h json_config_.c:json_config.h json_diff_.c:json_diff.h json_dir_.c:json_dir.h json_finfo_.c:json_finfo.h json_login_.c:json_login.h json_query_.c:json_query.h json_report_.c:json_report.h json_status_.c:json_status.h json_tag_.c:json_tag.h json_timeline_.c:json_timeline.h json_user_.c:json_user.h json_wiki_.c:json_wiki.h leaf_.c:leaf.h loadctrl_.c:loadctrl.h login_.c:login.h lookslike_.c:lookslike.h main_.c:main.h manifest_.c:manifest.h markdown_.c:markdown.h markdown_html_.c:markdown_html.h md5_.c:md5.h merge_.c:merge.h merge3_.c:merge3.h moderate_.c:moderate.h name_.c:name.h path_.c:path.h piechart_.c:piechart.h pivot_.c:pivot.h popen_.c:popen.h pqueue_.c:pqueue.h printf_.c:printf.h publish_.c:publish.h purge_.c:purge.h rebuild_.c:rebuild.h regexp_.c:regexp.h report_.c:report.h rss_.c:rss.h schema_.c:schema.h search_.c:search.h security_audit_.c:security_audit.h setup_.c:setup.h sha1_.c:sha1.h sha1hard_.c:sha1hard.h sha3_.c:sha3.h shun_.c:shun.h sitemap_.c:sitemap.h skins_.c:skins.h smtp_.c:smtp.h sqlcmd_.c:sqlcmd.h stash_.c:stash.h stat_.c:stat.h statrep_.c:statrep.h style_.c:style.h sync_.c:sync.h tag_.c:tag.h tar_.c:tar.h th_main_.c:th_main.h timeline_.c:timeline.h tkt_.c:tkt.h tktsetup_.c:tktsetup.h undo_.c:undo.h unicode_.c:unicode.h unversioned_.c:unversioned.h update_.c:update.h url_.c:url.h user_.c:user.h utf8_.c:utf8.h util_.c:util.h verify_.c:verify.h vfile_.c:vfile.h webmail_.c:webmail.h wiki_.c:wiki.h wikiformat_.c:wikiformat.h winfile_.c:winfile.h winhttp_.c:winhttp.h wysiwyg_.c:wysiwyg.h xfer_.c:xfer.h xfersetup_.c:xfersetup.h zip_.c:zip.h $(SRCDIR)\sqlite3.h $(SRCDIR)\th.h VERSION.h $(SRCDIR)\cson_amalgamation.h | |
| 939 | + +makeheaders$E add_.c:add.h allrepo_.c:allrepo.h attach_.c:attach.h backoffice_.c:backoffice.h bag_.c:bag.h bisect_.c:bisect.h blob_.c:blob.h branch_.c:branch.h browse_.c:browse.h builtin_.c:builtin.h bundle_.c:bundle.h cache_.c:cache.h capabilities_.c:capabilities.h captcha_.c:captcha.h cgi_.c:cgi.h checkin_.c:checkin.h checkout_.c:checkout.h clearsign_.c:clearsign.h clone_.c:clone.h comformat_.c:comformat.h configure_.c:configure.h content_.c:content.h cookies_.c:cookies.h db_.c:db.h delta_.c:delta.h deltacmd_.c:deltacmd.h descendants_.c:descendants.h diff_.c:diff.h diffcmd_.c:diffcmd.h dispatch_.c:dispatch.h doc_.c:doc.h email_.c:email.h encode_.c:encode.h etag_.c:etag.h event_.c:event.h export_.c:export.h file_.c:file.h finfo_.c:finfo.h foci_.c:foci.h forum_.c:forum.h fshell_.c:fshell.h fusefs_.c:fusefs.h glob_.c:glob.h graph_.c:graph.h gzip_.c:gzip.h hname_.c:hname.h http_.c:http.h http_socket_.c:http_socket.h http_ssl_.c:http_ssl.h http_transport_.c:http_transport.h import_.c:import.h info_.c:info.h json_.c:json.h json_artifact_.c:json_artifact.h json_branch_.c:json_branch.h json_config_.c:json_config.h json_diff_.c:json_diff.h json_dir_.c:json_dir.h json_finfo_.c:json_finfo.h json_login_.c:json_login.h json_query_.c:json_query.h json_report_.c:json_report.h json_status_.c:json_status.h json_tag_.c:json_tag.h json_timeline_.c:json_timeline.h json_user_.c:json_user.h json_wiki_.c:json_wiki.h leaf_.c:leaf.h loadctrl_.c:loadctrl.h login_.c:login.h lookslike_.c:lookslike.h main_.c:main.h manifest_.c:manifest.h markdown_.c:markdown.h markdown_html_.c:markdown_html.h md5_.c:md5.h merge_.c:merge.h merge3_.c:merge3.h moderate_.c:moderate.h name_.c:name.h path_.c:path.h piechart_.c:piechart.h pivot_.c:pivot.h popen_.c:popen.h pqueue_.c:pqueue.h printf_.c:printf.h publish_.c:publish.h purge_.c:purge.h rebuild_.c:rebuild.h regexp_.c:regexp.h report_.c:report.h rss_.c:rss.h schema_.c:schema.h search_.c:search.h security_audit_.c:security_audit.h setup_.c:setup.h sha1_.c:sha1.h sha1hard_.c:sha1hard.h sha3_.c:sha3.h shun_.c:shun.h sitemap_.c:sitemap.h skins_.c:skins.h smtp_.c:smtp.h sqlcmd_.c:sqlcmd.h stash_.c:stash.h stat_.c:stat.h statrep_.c:statrep.h style_.c:style.h sync_.c:sync.h tag_.c:tag.h tar_.c:tar.h th_main_.c:th_main.h timeline_.c:timeline.h tkt_.c:tkt.h tktsetup_.c:tktsetup.h undo_.c:undo.h unicode_.c:unicode.h unversioned_.c:unversioned.h update_.c:update.h url_.c:url.h user_.c:user.h utf8_.c:utf8.h util_.c:util.h verify_.c:verify.h vfile_.c:vfile.h webmail_.c:webmail.h wiki_.c:wiki.h wikiformat_.c:wikiformat.h winfile_.c:winfile.h winhttp_.c:winhttp.h wysiwyg_.c:wysiwyg.h xfer_.c:xfer.h xfersetup_.c:xfersetup.h zip_.c:zip.h $(SRCDIR)\sqlite3.h $(SRCDIR)\th.h VERSION.h $(SRCDIR)\cson_amalgamation.h | |
| 934 | 940 | @copy /Y nul: headers |
| 935 | 941 |
| --- win/Makefile.dmc | |
| +++ win/Makefile.dmc | |
| @@ -28,13 +28,13 @@ | |
| 28 | |
| 29 | SQLITE_OPTIONS = -DNDEBUG=1 -DSQLITE_THREADSAFE=0 -DSQLITE_DEFAULT_MEMSTATUS=0 -DSQLITE_DEFAULT_WAL_SYNCHRONOUS=1 -DSQLITE_LIKE_DOESNT_MATCH_BLOBS -DSQLITE_OMIT_DECLTYPE -DSQLITE_OMIT_DEPRECATED -DSQLITE_OMIT_GET_TABLE -DSQLITE_OMIT_PROGRESS_CALLBACK -DSQLITE_OMIT_SHARED_CACHE -DSQLITE_OMIT_LOAD_EXTENSION -DSQLITE_MAX_EXPR_DEPTH=0 -DSQLITE_USE_ALLOCA -DSQLITE_ENABLE_LOCKING_STYLE=0 -DSQLITE_DEFAULT_FILE_FORMAT=4 -DSQLITE_ENABLE_EXPLAIN_COMMENTS -DSQLITE_ENABLE_FTS4 -DSQLITE_ENABLE_FTS3_PARENTHESIS -DSQLITE_ENABLE_DBSTAT_VTAB -DSQLITE_ENABLE_JSON1 -DSQLITE_ENABLE_FTS5 -DSQLITE_ENABLE_STMTVTAB -DSQLITE_HAVE_ZLIB -DSQLITE_INTROSPECTION_PRAGMAS -DSQLITE_ENABLE_DBPAGE_VTAB |
| 30 | |
| 31 | SHELL_OPTIONS = -DNDEBUG=1 -DSQLITE_THREADSAFE=0 -DSQLITE_DEFAULT_MEMSTATUS=0 -DSQLITE_DEFAULT_WAL_SYNCHRONOUS=1 -DSQLITE_LIKE_DOESNT_MATCH_BLOBS -DSQLITE_OMIT_DECLTYPE -DSQLITE_OMIT_DEPRECATED -DSQLITE_OMIT_GET_TABLE -DSQLITE_OMIT_PROGRESS_CALLBACK -DSQLITE_OMIT_SHARED_CACHE -DSQLITE_OMIT_LOAD_EXTENSION -DSQLITE_MAX_EXPR_DEPTH=0 -DSQLITE_USE_ALLOCA -DSQLITE_ENABLE_LOCKING_STYLE=0 -DSQLITE_DEFAULT_FILE_FORMAT=4 -DSQLITE_ENABLE_EXPLAIN_COMMENTS -DSQLITE_ENABLE_FTS4 -DSQLITE_ENABLE_FTS3_PARENTHESIS -DSQLITE_ENABLE_DBSTAT_VTAB -DSQLITE_ENABLE_JSON1 -DSQLITE_ENABLE_FTS5 -DSQLITE_ENABLE_STMTVTAB -DSQLITE_HAVE_ZLIB -DSQLITE_INTROSPECTION_PRAGMAS -DSQLITE_ENABLE_DBPAGE_VTAB -Dmain=sqlite3_shell -DSQLITE_SHELL_IS_UTF8=1 -DSQLITE_OMIT_LOAD_EXTENSION=1 -DUSE_SYSTEM_SQLITE=$(USE_SYSTEM_SQLITE) -DSQLITE_SHELL_DBNAME_PROC=sqlcmd_get_dbname -DSQLITE_SHELL_INIT_PROC=sqlcmd_init_proc -Daccess=file_access -Dsystem=fossil_system -Dgetenv=fossil_getenv -Dfopen=fossil_fopen |
| 32 | |
| 33 | SRC = add_.c allrepo_.c attach_.c backoffice_.c bag_.c bisect_.c blob_.c branch_.c browse_.c builtin_.c bundle_.c cache_.c captcha_.c cgi_.c checkin_.c checkout_.c clearsign_.c clone_.c comformat_.c configure_.c content_.c cookies_.c db_.c delta_.c deltacmd_.c descendants_.c diff_.c diffcmd_.c dispatch_.c doc_.c email_.c encode_.c etag_.c event_.c export_.c file_.c finfo_.c foci_.c forum_.c fshell_.c fusefs_.c glob_.c graph_.c gzip_.c hname_.c http_.c http_socket_.c http_ssl_.c http_transport_.c import_.c info_.c json_.c json_artifact_.c json_branch_.c json_config_.c json_diff_.c json_dir_.c json_finfo_.c json_login_.c json_query_.c json_report_.c json_status_.c json_tag_.c json_timeline_.c json_user_.c json_wiki_.c leaf_.c loadctrl_.c login_.c lookslike_.c main_.c manifest_.c markdown_.c markdown_html_.c md5_.c merge_.c merge3_.c moderate_.c name_.c path_.c piechart_.c pivot_.c popen_.c pqueue_.c printf_.c publish_.c purge_.c rebuild_.c regexp_.c report_.c rss_.c schema_.c search_.c security_audit_.c setup_.c sha1_.c sha1hard_.c sha3_.c shun_.c sitemap_.c skins_.c smtp_.c sqlcmd_.c stash_.c stat_.c statrep_.c style_.c sync_.c tag_.c tar_.c th_main_.c timeline_.c tkt_.c tktsetup_.c undo_.c unicode_.c unversioned_.c update_.c url_.c user_.c utf8_.c util_.c verify_.c vfile_.c webmail_.c wiki_.c wikiformat_.c winfile_.c winhttp_.c wysiwyg_.c xfer_.c xfersetup_.c zip_.c |
| 34 | |
| 35 | OBJ = $(OBJDIR)\add$O $(OBJDIR)\allrepo$O $(OBJDIR)\attach$O $(OBJDIR)\backoffice$O $(OBJDIR)\bag$O $(OBJDIR)\bisect$O $(OBJDIR)\blob$O $(OBJDIR)\branch$O $(OBJDIR)\browse$O $(OBJDIR)\builtin$O $(OBJDIR)\bundle$O $(OBJDIR)\cache$O $(OBJDIR)\captcha$O $(OBJDIR)\cgi$O $(OBJDIR)\checkin$O $(OBJDIR)\checkout$O $(OBJDIR)\clearsign$O $(OBJDIR)\clone$O $(OBJDIR)\comformat$O $(OBJDIR)\configure$O $(OBJDIR)\content$O $(OBJDIR)\cookies$O $(OBJDIR)\db$O $(OBJDIR)\delta$O $(OBJDIR)\deltacmd$O $(OBJDIR)\descendants$O $(OBJDIR)\diff$O $(OBJDIR)\diffcmd$O $(OBJDIR)\dispatch$O $(OBJDIR)\doc$O $(OBJDIR)\email$O $(OBJDIR)\encode$O $(OBJDIR)\etag$O $(OBJDIR)\event$O $(OBJDIR)\export$O $(OBJDIR)\file$O $(OBJDIR)\finfo$O $(OBJDIR)\foci$O $(OBJDIR)\forum$O $(OBJDIR)\fshell$O $(OBJDIR)\fusefs$O $(OBJDIR)\glob$O $(OBJDIR)\graph$O $(OBJDIR)\gzip$O $(OBJDIR)\hname$O $(OBJDIR)\http$O $(OBJDIR)\http_socket$O $(OBJDIR)\http_ssl$O $(OBJDIR)\http_transport$O $(OBJDIR)\import$O $(OBJDIR)\info$O $(OBJDIR)\json$O $(OBJDIR)\json_artifact$O $(OBJDIR)\json_branch$O $(OBJDIR)\json_config$O $(OBJDIR)\json_diff$O $(OBJDIR)\json_dir$O $(OBJDIR)\json_finfo$O $(OBJDIR)\json_login$O $(OBJDIR)\json_query$O $(OBJDIR)\json_report$O $(OBJDIR)\json_status$O $(OBJDIR)\json_tag$O $(OBJDIR)\json_timeline$O $(OBJDIR)\json_user$O $(OBJDIR)\json_wiki$O $(OBJDIR)\leaf$O $(OBJDIR)\loadctrl$O $(OBJDIR)\login$O $(OBJDIR)\lookslike$O $(OBJDIR)\main$O $(OBJDIR)\manifest$O $(OBJDIR)\markdown$O $(OBJDIR)\markdown_html$O $(OBJDIR)\md5$O $(OBJDIR)\merge$O $(OBJDIR)\merge3$O $(OBJDIR)\moderate$O $(OBJDIR)\name$O $(OBJDIR)\path$O $(OBJDIR)\piechart$O $(OBJDIR)\pivot$O $(OBJDIR)\popen$O $(OBJDIR)\pqueue$O $(OBJDIR)\printf$O $(OBJDIR)\publish$O $(OBJDIR)\purge$O $(OBJDIR)\rebuild$O $(OBJDIR)\regexp$O $(OBJDIR)\report$O $(OBJDIR)\rss$O $(OBJDIR)\schema$O $(OBJDIR)\search$O $(OBJDIR)\security_audit$O $(OBJDIR)\setup$O $(OBJDIR)\sha1$O $(OBJDIR)\sha1hard$O $(OBJDIR)\sha3$O $(OBJDIR)\shun$O $(OBJDIR)\sitemap$O $(OBJDIR)\skins$O $(OBJDIR)\smtp$O $(OBJDIR)\sqlcmd$O $(OBJDIR)\stash$O $(OBJDIR)\stat$O $(OBJDIR)\statrep$O $(OBJDIR)\style$O $(OBJDIR)\sync$O $(OBJDIR)\tag$O $(OBJDIR)\tar$O $(OBJDIR)\th_main$O $(OBJDIR)\timeline$O $(OBJDIR)\tkt$O $(OBJDIR)\tktsetup$O $(OBJDIR)\undo$O $(OBJDIR)\unicode$O $(OBJDIR)\unversioned$O $(OBJDIR)\update$O $(OBJDIR)\url$O $(OBJDIR)\user$O $(OBJDIR)\utf8$O $(OBJDIR)\util$O $(OBJDIR)\verify$O $(OBJDIR)\vfile$O $(OBJDIR)\webmail$O $(OBJDIR)\wiki$O $(OBJDIR)\wikiformat$O $(OBJDIR)\winfile$O $(OBJDIR)\winhttp$O $(OBJDIR)\wysiwyg$O $(OBJDIR)\xfer$O $(OBJDIR)\xfersetup$O $(OBJDIR)\zip$O $(OBJDIR)\shell$O $(OBJDIR)\sqlite3$O $(OBJDIR)\th$O $(OBJDIR)\th_lang$O |
| 36 | |
| 37 | |
| 38 | RC=$(DMDIR)\bin\rcc |
| 39 | RCFLAGS=-32 -w1 -I$(SRCDIR) /D__DMC__ |
| 40 | |
| @@ -49,11 +49,11 @@ | |
| 49 | |
| 50 | $(OBJDIR)\fossil.res: $B\win\fossil.rc |
| 51 | $(RC) $(RCFLAGS) -o$@ $** |
| 52 | |
| 53 | $(OBJDIR)\link: $B\win\Makefile.dmc $(OBJDIR)\fossil.res |
| 54 | +echo add allrepo attach backoffice bag bisect blob branch browse builtin bundle cache captcha cgi checkin checkout clearsign clone comformat configure content cookies db delta deltacmd descendants diff diffcmd dispatch doc email encode etag event export file finfo foci forum fshell fusefs glob graph gzip hname http http_socket http_ssl http_transport import info json json_artifact json_branch json_config json_diff json_dir json_finfo json_login json_query json_report json_status json_tag json_timeline json_user json_wiki leaf loadctrl login lookslike main manifest markdown markdown_html md5 merge merge3 moderate name path piechart pivot popen pqueue printf publish purge rebuild regexp report rss schema search security_audit setup sha1 sha1hard sha3 shun sitemap skins smtp sqlcmd stash stat statrep style sync tag tar th_main timeline tkt tktsetup undo unicode unversioned update url user utf8 util verify vfile webmail wiki wikiformat winfile winhttp wysiwyg xfer xfersetup zip shell sqlite3 th th_lang > $@ |
| 55 | +echo fossil >> $@ |
| 56 | +echo fossil >> $@ |
| 57 | +echo $(LIBS) >> $@ |
| 58 | +echo. >> $@ |
| 59 | +echo fossil >> $@ |
| @@ -200,10 +200,16 @@ | |
| 200 | $(OBJDIR)\cache$O : cache_.c cache.h |
| 201 | $(TCC) -o$@ -c cache_.c |
| 202 | |
| 203 | cache_.c : $(SRCDIR)\cache.c |
| 204 | +translate$E $** > $@ |
| 205 | |
| 206 | $(OBJDIR)\captcha$O : captcha_.c captcha.h |
| 207 | $(TCC) -o$@ -c captcha_.c |
| 208 | |
| 209 | captcha_.c : $(SRCDIR)\captcha.c |
| @@ -928,7 +934,7 @@ | |
| 928 | |
| 929 | zip_.c : $(SRCDIR)\zip.c |
| 930 | +translate$E $** > $@ |
| 931 | |
| 932 | headers: makeheaders$E page_index.h builtin_data.h default_css.h VERSION.h |
| 933 | +makeheaders$E add_.c:add.h allrepo_.c:allrepo.h attach_.c:attach.h backoffice_.c:backoffice.h bag_.c:bag.h bisect_.c:bisect.h blob_.c:blob.h branch_.c:branch.h browse_.c:browse.h builtin_.c:builtin.h bundle_.c:bundle.h cache_.c:cache.h captcha_.c:captcha.h cgi_.c:cgi.h checkin_.c:checkin.h checkout_.c:checkout.h clearsign_.c:clearsign.h clone_.c:clone.h comformat_.c:comformat.h configure_.c:configure.h content_.c:content.h cookies_.c:cookies.h db_.c:db.h delta_.c:delta.h deltacmd_.c:deltacmd.h descendants_.c:descendants.h diff_.c:diff.h diffcmd_.c:diffcmd.h dispatch_.c:dispatch.h doc_.c:doc.h email_.c:email.h encode_.c:encode.h etag_.c:etag.h event_.c:event.h export_.c:export.h file_.c:file.h finfo_.c:finfo.h foci_.c:foci.h forum_.c:forum.h fshell_.c:fshell.h fusefs_.c:fusefs.h glob_.c:glob.h graph_.c:graph.h gzip_.c:gzip.h hname_.c:hname.h http_.c:http.h http_socket_.c:http_socket.h http_ssl_.c:http_ssl.h http_transport_.c:http_transport.h import_.c:import.h info_.c:info.h json_.c:json.h json_artifact_.c:json_artifact.h json_branch_.c:json_branch.h json_config_.c:json_config.h json_diff_.c:json_diff.h json_dir_.c:json_dir.h json_finfo_.c:json_finfo.h json_login_.c:json_login.h json_query_.c:json_query.h json_report_.c:json_report.h json_status_.c:json_status.h json_tag_.c:json_tag.h json_timeline_.c:json_timeline.h json_user_.c:json_user.h json_wiki_.c:json_wiki.h leaf_.c:leaf.h loadctrl_.c:loadctrl.h login_.c:login.h lookslike_.c:lookslike.h main_.c:main.h manifest_.c:manifest.h markdown_.c:markdown.h markdown_html_.c:markdown_html.h md5_.c:md5.h merge_.c:merge.h merge3_.c:merge3.h moderate_.c:moderate.h name_.c:name.h path_.c:path.h piechart_.c:piechart.h pivot_.c:pivot.h popen_.c:popen.h pqueue_.c:pqueue.h printf_.c:printf.h publish_.c:publish.h purge_.c:purge.h rebuild_.c:rebuild.h regexp_.c:regexp.h report_.c:report.h rss_.c:rss.h schema_.c:schema.h search_.c:search.h security_audit_.c:security_audit.h setup_.c:setup.h sha1_.c:sha1.h sha1hard_.c:sha1hard.h sha3_.c:sha3.h shun_.c:shun.h sitemap_.c:sitemap.h skins_.c:skins.h smtp_.c:smtp.h sqlcmd_.c:sqlcmd.h stash_.c:stash.h stat_.c:stat.h statrep_.c:statrep.h style_.c:style.h sync_.c:sync.h tag_.c:tag.h tar_.c:tar.h th_main_.c:th_main.h timeline_.c:timeline.h tkt_.c:tkt.h tktsetup_.c:tktsetup.h undo_.c:undo.h unicode_.c:unicode.h unversioned_.c:unversioned.h update_.c:update.h url_.c:url.h user_.c:user.h utf8_.c:utf8.h util_.c:util.h verify_.c:verify.h vfile_.c:vfile.h webmail_.c:webmail.h wiki_.c:wiki.h wikiformat_.c:wikiformat.h winfile_.c:winfile.h winhttp_.c:winhttp.h wysiwyg_.c:wysiwyg.h xfer_.c:xfer.h xfersetup_.c:xfersetup.h zip_.c:zip.h $(SRCDIR)\sqlite3.h $(SRCDIR)\th.h VERSION.h $(SRCDIR)\cson_amalgamation.h |
| 934 | @copy /Y nul: headers |
| 935 |
| --- win/Makefile.dmc | |
| +++ win/Makefile.dmc | |
| @@ -28,13 +28,13 @@ | |
| 28 | |
| 29 | SQLITE_OPTIONS = -DNDEBUG=1 -DSQLITE_THREADSAFE=0 -DSQLITE_DEFAULT_MEMSTATUS=0 -DSQLITE_DEFAULT_WAL_SYNCHRONOUS=1 -DSQLITE_LIKE_DOESNT_MATCH_BLOBS -DSQLITE_OMIT_DECLTYPE -DSQLITE_OMIT_DEPRECATED -DSQLITE_OMIT_GET_TABLE -DSQLITE_OMIT_PROGRESS_CALLBACK -DSQLITE_OMIT_SHARED_CACHE -DSQLITE_OMIT_LOAD_EXTENSION -DSQLITE_MAX_EXPR_DEPTH=0 -DSQLITE_USE_ALLOCA -DSQLITE_ENABLE_LOCKING_STYLE=0 -DSQLITE_DEFAULT_FILE_FORMAT=4 -DSQLITE_ENABLE_EXPLAIN_COMMENTS -DSQLITE_ENABLE_FTS4 -DSQLITE_ENABLE_FTS3_PARENTHESIS -DSQLITE_ENABLE_DBSTAT_VTAB -DSQLITE_ENABLE_JSON1 -DSQLITE_ENABLE_FTS5 -DSQLITE_ENABLE_STMTVTAB -DSQLITE_HAVE_ZLIB -DSQLITE_INTROSPECTION_PRAGMAS -DSQLITE_ENABLE_DBPAGE_VTAB |
| 30 | |
| 31 | SHELL_OPTIONS = -DNDEBUG=1 -DSQLITE_THREADSAFE=0 -DSQLITE_DEFAULT_MEMSTATUS=0 -DSQLITE_DEFAULT_WAL_SYNCHRONOUS=1 -DSQLITE_LIKE_DOESNT_MATCH_BLOBS -DSQLITE_OMIT_DECLTYPE -DSQLITE_OMIT_DEPRECATED -DSQLITE_OMIT_GET_TABLE -DSQLITE_OMIT_PROGRESS_CALLBACK -DSQLITE_OMIT_SHARED_CACHE -DSQLITE_OMIT_LOAD_EXTENSION -DSQLITE_MAX_EXPR_DEPTH=0 -DSQLITE_USE_ALLOCA -DSQLITE_ENABLE_LOCKING_STYLE=0 -DSQLITE_DEFAULT_FILE_FORMAT=4 -DSQLITE_ENABLE_EXPLAIN_COMMENTS -DSQLITE_ENABLE_FTS4 -DSQLITE_ENABLE_FTS3_PARENTHESIS -DSQLITE_ENABLE_DBSTAT_VTAB -DSQLITE_ENABLE_JSON1 -DSQLITE_ENABLE_FTS5 -DSQLITE_ENABLE_STMTVTAB -DSQLITE_HAVE_ZLIB -DSQLITE_INTROSPECTION_PRAGMAS -DSQLITE_ENABLE_DBPAGE_VTAB -Dmain=sqlite3_shell -DSQLITE_SHELL_IS_UTF8=1 -DSQLITE_OMIT_LOAD_EXTENSION=1 -DUSE_SYSTEM_SQLITE=$(USE_SYSTEM_SQLITE) -DSQLITE_SHELL_DBNAME_PROC=sqlcmd_get_dbname -DSQLITE_SHELL_INIT_PROC=sqlcmd_init_proc -Daccess=file_access -Dsystem=fossil_system -Dgetenv=fossil_getenv -Dfopen=fossil_fopen |
| 32 | |
| 33 | SRC = add_.c allrepo_.c attach_.c backoffice_.c bag_.c bisect_.c blob_.c branch_.c browse_.c builtin_.c bundle_.c cache_.c capabilities_.c captcha_.c cgi_.c checkin_.c checkout_.c clearsign_.c clone_.c comformat_.c configure_.c content_.c cookies_.c db_.c delta_.c deltacmd_.c descendants_.c diff_.c diffcmd_.c dispatch_.c doc_.c email_.c encode_.c etag_.c event_.c export_.c file_.c finfo_.c foci_.c forum_.c fshell_.c fusefs_.c glob_.c graph_.c gzip_.c hname_.c http_.c http_socket_.c http_ssl_.c http_transport_.c import_.c info_.c json_.c json_artifact_.c json_branch_.c json_config_.c json_diff_.c json_dir_.c json_finfo_.c json_login_.c json_query_.c json_report_.c json_status_.c json_tag_.c json_timeline_.c json_user_.c json_wiki_.c leaf_.c loadctrl_.c login_.c lookslike_.c main_.c manifest_.c markdown_.c markdown_html_.c md5_.c merge_.c merge3_.c moderate_.c name_.c path_.c piechart_.c pivot_.c popen_.c pqueue_.c printf_.c publish_.c purge_.c rebuild_.c regexp_.c report_.c rss_.c schema_.c search_.c security_audit_.c setup_.c sha1_.c sha1hard_.c sha3_.c shun_.c sitemap_.c skins_.c smtp_.c sqlcmd_.c stash_.c stat_.c statrep_.c style_.c sync_.c tag_.c tar_.c th_main_.c timeline_.c tkt_.c tktsetup_.c undo_.c unicode_.c unversioned_.c update_.c url_.c user_.c utf8_.c util_.c verify_.c vfile_.c webmail_.c wiki_.c wikiformat_.c winfile_.c winhttp_.c wysiwyg_.c xfer_.c xfersetup_.c zip_.c |
| 34 | |
| 35 | OBJ = $(OBJDIR)\add$O $(OBJDIR)\allrepo$O $(OBJDIR)\attach$O $(OBJDIR)\backoffice$O $(OBJDIR)\bag$O $(OBJDIR)\bisect$O $(OBJDIR)\blob$O $(OBJDIR)\branch$O $(OBJDIR)\browse$O $(OBJDIR)\builtin$O $(OBJDIR)\bundle$O $(OBJDIR)\cache$O $(OBJDIR)\capabilities$O $(OBJDIR)\captcha$O $(OBJDIR)\cgi$O $(OBJDIR)\checkin$O $(OBJDIR)\checkout$O $(OBJDIR)\clearsign$O $(OBJDIR)\clone$O $(OBJDIR)\comformat$O $(OBJDIR)\configure$O $(OBJDIR)\content$O $(OBJDIR)\cookies$O $(OBJDIR)\db$O $(OBJDIR)\delta$O $(OBJDIR)\deltacmd$O $(OBJDIR)\descendants$O $(OBJDIR)\diff$O $(OBJDIR)\diffcmd$O $(OBJDIR)\dispatch$O $(OBJDIR)\doc$O $(OBJDIR)\email$O $(OBJDIR)\encode$O $(OBJDIR)\etag$O $(OBJDIR)\event$O $(OBJDIR)\export$O $(OBJDIR)\file$O $(OBJDIR)\finfo$O $(OBJDIR)\foci$O $(OBJDIR)\forum$O $(OBJDIR)\fshell$O $(OBJDIR)\fusefs$O $(OBJDIR)\glob$O $(OBJDIR)\graph$O $(OBJDIR)\gzip$O $(OBJDIR)\hname$O $(OBJDIR)\http$O $(OBJDIR)\http_socket$O $(OBJDIR)\http_ssl$O $(OBJDIR)\http_transport$O $(OBJDIR)\import$O $(OBJDIR)\info$O $(OBJDIR)\json$O $(OBJDIR)\json_artifact$O $(OBJDIR)\json_branch$O $(OBJDIR)\json_config$O $(OBJDIR)\json_diff$O $(OBJDIR)\json_dir$O $(OBJDIR)\json_finfo$O $(OBJDIR)\json_login$O $(OBJDIR)\json_query$O $(OBJDIR)\json_report$O $(OBJDIR)\json_status$O $(OBJDIR)\json_tag$O $(OBJDIR)\json_timeline$O $(OBJDIR)\json_user$O $(OBJDIR)\json_wiki$O $(OBJDIR)\leaf$O $(OBJDIR)\loadctrl$O $(OBJDIR)\login$O $(OBJDIR)\lookslike$O $(OBJDIR)\main$O $(OBJDIR)\manifest$O $(OBJDIR)\markdown$O $(OBJDIR)\markdown_html$O $(OBJDIR)\md5$O $(OBJDIR)\merge$O $(OBJDIR)\merge3$O $(OBJDIR)\moderate$O $(OBJDIR)\name$O $(OBJDIR)\path$O $(OBJDIR)\piechart$O $(OBJDIR)\pivot$O $(OBJDIR)\popen$O $(OBJDIR)\pqueue$O $(OBJDIR)\printf$O $(OBJDIR)\publish$O $(OBJDIR)\purge$O $(OBJDIR)\rebuild$O $(OBJDIR)\regexp$O $(OBJDIR)\report$O $(OBJDIR)\rss$O $(OBJDIR)\schema$O $(OBJDIR)\search$O $(OBJDIR)\security_audit$O $(OBJDIR)\setup$O $(OBJDIR)\sha1$O $(OBJDIR)\sha1hard$O $(OBJDIR)\sha3$O $(OBJDIR)\shun$O $(OBJDIR)\sitemap$O $(OBJDIR)\skins$O $(OBJDIR)\smtp$O $(OBJDIR)\sqlcmd$O $(OBJDIR)\stash$O $(OBJDIR)\stat$O $(OBJDIR)\statrep$O $(OBJDIR)\style$O $(OBJDIR)\sync$O $(OBJDIR)\tag$O $(OBJDIR)\tar$O $(OBJDIR)\th_main$O $(OBJDIR)\timeline$O $(OBJDIR)\tkt$O $(OBJDIR)\tktsetup$O $(OBJDIR)\undo$O $(OBJDIR)\unicode$O $(OBJDIR)\unversioned$O $(OBJDIR)\update$O $(OBJDIR)\url$O $(OBJDIR)\user$O $(OBJDIR)\utf8$O $(OBJDIR)\util$O $(OBJDIR)\verify$O $(OBJDIR)\vfile$O $(OBJDIR)\webmail$O $(OBJDIR)\wiki$O $(OBJDIR)\wikiformat$O $(OBJDIR)\winfile$O $(OBJDIR)\winhttp$O $(OBJDIR)\wysiwyg$O $(OBJDIR)\xfer$O $(OBJDIR)\xfersetup$O $(OBJDIR)\zip$O $(OBJDIR)\shell$O $(OBJDIR)\sqlite3$O $(OBJDIR)\th$O $(OBJDIR)\th_lang$O |
| 36 | |
| 37 | |
| 38 | RC=$(DMDIR)\bin\rcc |
| 39 | RCFLAGS=-32 -w1 -I$(SRCDIR) /D__DMC__ |
| 40 | |
| @@ -49,11 +49,11 @@ | |
| 49 | |
| 50 | $(OBJDIR)\fossil.res: $B\win\fossil.rc |
| 51 | $(RC) $(RCFLAGS) -o$@ $** |
| 52 | |
| 53 | $(OBJDIR)\link: $B\win\Makefile.dmc $(OBJDIR)\fossil.res |
| 54 | +echo add allrepo attach backoffice bag bisect blob branch browse builtin bundle cache capabilities captcha cgi checkin checkout clearsign clone comformat configure content cookies db delta deltacmd descendants diff diffcmd dispatch doc email encode etag event export file finfo foci forum fshell fusefs glob graph gzip hname http http_socket http_ssl http_transport import info json json_artifact json_branch json_config json_diff json_dir json_finfo json_login json_query json_report json_status json_tag json_timeline json_user json_wiki leaf loadctrl login lookslike main manifest markdown markdown_html md5 merge merge3 moderate name path piechart pivot popen pqueue printf publish purge rebuild regexp report rss schema search security_audit setup sha1 sha1hard sha3 shun sitemap skins smtp sqlcmd stash stat statrep style sync tag tar th_main timeline tkt tktsetup undo unicode unversioned update url user utf8 util verify vfile webmail wiki wikiformat winfile winhttp wysiwyg xfer xfersetup zip shell sqlite3 th th_lang > $@ |
| 55 | +echo fossil >> $@ |
| 56 | +echo fossil >> $@ |
| 57 | +echo $(LIBS) >> $@ |
| 58 | +echo. >> $@ |
| 59 | +echo fossil >> $@ |
| @@ -200,10 +200,16 @@ | |
| 200 | $(OBJDIR)\cache$O : cache_.c cache.h |
| 201 | $(TCC) -o$@ -c cache_.c |
| 202 | |
| 203 | cache_.c : $(SRCDIR)\cache.c |
| 204 | +translate$E $** > $@ |
| 205 | |
| 206 | $(OBJDIR)\capabilities$O : capabilities_.c capabilities.h |
| 207 | $(TCC) -o$@ -c capabilities_.c |
| 208 | |
| 209 | capabilities_.c : $(SRCDIR)\capabilities.c |
| 210 | +translate$E $** > $@ |
| 211 | |
| 212 | $(OBJDIR)\captcha$O : captcha_.c captcha.h |
| 213 | $(TCC) -o$@ -c captcha_.c |
| 214 | |
| 215 | captcha_.c : $(SRCDIR)\captcha.c |
| @@ -928,7 +934,7 @@ | |
| 934 | |
| 935 | zip_.c : $(SRCDIR)\zip.c |
| 936 | +translate$E $** > $@ |
| 937 | |
| 938 | headers: makeheaders$E page_index.h builtin_data.h default_css.h VERSION.h |
| 939 | +makeheaders$E add_.c:add.h allrepo_.c:allrepo.h attach_.c:attach.h backoffice_.c:backoffice.h bag_.c:bag.h bisect_.c:bisect.h blob_.c:blob.h branch_.c:branch.h browse_.c:browse.h builtin_.c:builtin.h bundle_.c:bundle.h cache_.c:cache.h capabilities_.c:capabilities.h captcha_.c:captcha.h cgi_.c:cgi.h checkin_.c:checkin.h checkout_.c:checkout.h clearsign_.c:clearsign.h clone_.c:clone.h comformat_.c:comformat.h configure_.c:configure.h content_.c:content.h cookies_.c:cookies.h db_.c:db.h delta_.c:delta.h deltacmd_.c:deltacmd.h descendants_.c:descendants.h diff_.c:diff.h diffcmd_.c:diffcmd.h dispatch_.c:dispatch.h doc_.c:doc.h email_.c:email.h encode_.c:encode.h etag_.c:etag.h event_.c:event.h export_.c:export.h file_.c:file.h finfo_.c:finfo.h foci_.c:foci.h forum_.c:forum.h fshell_.c:fshell.h fusefs_.c:fusefs.h glob_.c:glob.h graph_.c:graph.h gzip_.c:gzip.h hname_.c:hname.h http_.c:http.h http_socket_.c:http_socket.h http_ssl_.c:http_ssl.h http_transport_.c:http_transport.h import_.c:import.h info_.c:info.h json_.c:json.h json_artifact_.c:json_artifact.h json_branch_.c:json_branch.h json_config_.c:json_config.h json_diff_.c:json_diff.h json_dir_.c:json_dir.h json_finfo_.c:json_finfo.h json_login_.c:json_login.h json_query_.c:json_query.h json_report_.c:json_report.h json_status_.c:json_status.h json_tag_.c:json_tag.h json_timeline_.c:json_timeline.h json_user_.c:json_user.h json_wiki_.c:json_wiki.h leaf_.c:leaf.h loadctrl_.c:loadctrl.h login_.c:login.h lookslike_.c:lookslike.h main_.c:main.h manifest_.c:manifest.h markdown_.c:markdown.h markdown_html_.c:markdown_html.h md5_.c:md5.h merge_.c:merge.h merge3_.c:merge3.h moderate_.c:moderate.h name_.c:name.h path_.c:path.h piechart_.c:piechart.h pivot_.c:pivot.h popen_.c:popen.h pqueue_.c:pqueue.h printf_.c:printf.h publish_.c:publish.h purge_.c:purge.h rebuild_.c:rebuild.h regexp_.c:regexp.h report_.c:report.h rss_.c:rss.h schema_.c:schema.h search_.c:search.h security_audit_.c:security_audit.h setup_.c:setup.h sha1_.c:sha1.h sha1hard_.c:sha1hard.h sha3_.c:sha3.h shun_.c:shun.h sitemap_.c:sitemap.h skins_.c:skins.h smtp_.c:smtp.h sqlcmd_.c:sqlcmd.h stash_.c:stash.h stat_.c:stat.h statrep_.c:statrep.h style_.c:style.h sync_.c:sync.h tag_.c:tag.h tar_.c:tar.h th_main_.c:th_main.h timeline_.c:timeline.h tkt_.c:tkt.h tktsetup_.c:tktsetup.h undo_.c:undo.h unicode_.c:unicode.h unversioned_.c:unversioned.h update_.c:update.h url_.c:url.h user_.c:user.h utf8_.c:utf8.h util_.c:util.h verify_.c:verify.h vfile_.c:vfile.h webmail_.c:webmail.h wiki_.c:wiki.h wikiformat_.c:wikiformat.h winfile_.c:winfile.h winhttp_.c:winhttp.h wysiwyg_.c:wysiwyg.h xfer_.c:xfer.h xfersetup_.c:xfersetup.h zip_.c:zip.h $(SRCDIR)\sqlite3.h $(SRCDIR)\th.h VERSION.h $(SRCDIR)\cson_amalgamation.h |
| 940 | @copy /Y nul: headers |
| 941 |
+13
| --- win/Makefile.mingw | ||
| +++ win/Makefile.mingw | ||
| @@ -448,10 +448,11 @@ | ||
| 448 | 448 | $(SRCDIR)/branch.c \ |
| 449 | 449 | $(SRCDIR)/browse.c \ |
| 450 | 450 | $(SRCDIR)/builtin.c \ |
| 451 | 451 | $(SRCDIR)/bundle.c \ |
| 452 | 452 | $(SRCDIR)/cache.c \ |
| 453 | + $(SRCDIR)/capabilities.c \ | |
| 453 | 454 | $(SRCDIR)/captcha.c \ |
| 454 | 455 | $(SRCDIR)/cgi.c \ |
| 455 | 456 | $(SRCDIR)/checkin.c \ |
| 456 | 457 | $(SRCDIR)/checkout.c \ |
| 457 | 458 | $(SRCDIR)/clearsign.c \ |
| @@ -628,10 +629,11 @@ | ||
| 628 | 629 | $(SRCDIR)/../skins/xekri/details.txt \ |
| 629 | 630 | $(SRCDIR)/../skins/xekri/footer.txt \ |
| 630 | 631 | $(SRCDIR)/../skins/xekri/header.txt \ |
| 631 | 632 | $(SRCDIR)/ci_edit.js \ |
| 632 | 633 | $(SRCDIR)/diff.tcl \ |
| 634 | + $(SRCDIR)/forum.js \ | |
| 633 | 635 | $(SRCDIR)/graph.js \ |
| 634 | 636 | $(SRCDIR)/href.js \ |
| 635 | 637 | $(SRCDIR)/login.js \ |
| 636 | 638 | $(SRCDIR)/markdown.md \ |
| 637 | 639 | $(SRCDIR)/menu.js \ |
| @@ -654,10 +656,11 @@ | ||
| 654 | 656 | $(OBJDIR)/branch_.c \ |
| 655 | 657 | $(OBJDIR)/browse_.c \ |
| 656 | 658 | $(OBJDIR)/builtin_.c \ |
| 657 | 659 | $(OBJDIR)/bundle_.c \ |
| 658 | 660 | $(OBJDIR)/cache_.c \ |
| 661 | + $(OBJDIR)/capabilities_.c \ | |
| 659 | 662 | $(OBJDIR)/captcha_.c \ |
| 660 | 663 | $(OBJDIR)/cgi_.c \ |
| 661 | 664 | $(OBJDIR)/checkin_.c \ |
| 662 | 665 | $(OBJDIR)/checkout_.c \ |
| 663 | 666 | $(OBJDIR)/clearsign_.c \ |
| @@ -789,10 +792,11 @@ | ||
| 789 | 792 | $(OBJDIR)/branch.o \ |
| 790 | 793 | $(OBJDIR)/browse.o \ |
| 791 | 794 | $(OBJDIR)/builtin.o \ |
| 792 | 795 | $(OBJDIR)/bundle.o \ |
| 793 | 796 | $(OBJDIR)/cache.o \ |
| 797 | + $(OBJDIR)/capabilities.o \ | |
| 794 | 798 | $(OBJDIR)/captcha.o \ |
| 795 | 799 | $(OBJDIR)/cgi.o \ |
| 796 | 800 | $(OBJDIR)/checkin.o \ |
| 797 | 801 | $(OBJDIR)/checkout.o \ |
| 798 | 802 | $(OBJDIR)/clearsign.o \ |
| @@ -1143,10 +1147,11 @@ | ||
| 1143 | 1147 | $(OBJDIR)/branch_.c:$(OBJDIR)/branch.h \ |
| 1144 | 1148 | $(OBJDIR)/browse_.c:$(OBJDIR)/browse.h \ |
| 1145 | 1149 | $(OBJDIR)/builtin_.c:$(OBJDIR)/builtin.h \ |
| 1146 | 1150 | $(OBJDIR)/bundle_.c:$(OBJDIR)/bundle.h \ |
| 1147 | 1151 | $(OBJDIR)/cache_.c:$(OBJDIR)/cache.h \ |
| 1152 | + $(OBJDIR)/capabilities_.c:$(OBJDIR)/capabilities.h \ | |
| 1148 | 1153 | $(OBJDIR)/captcha_.c:$(OBJDIR)/captcha.h \ |
| 1149 | 1154 | $(OBJDIR)/cgi_.c:$(OBJDIR)/cgi.h \ |
| 1150 | 1155 | $(OBJDIR)/checkin_.c:$(OBJDIR)/checkin.h \ |
| 1151 | 1156 | $(OBJDIR)/checkout_.c:$(OBJDIR)/checkout.h \ |
| 1152 | 1157 | $(OBJDIR)/clearsign_.c:$(OBJDIR)/clearsign.h \ |
| @@ -1368,10 +1373,18 @@ | ||
| 1368 | 1373 | |
| 1369 | 1374 | $(OBJDIR)/cache.o: $(OBJDIR)/cache_.c $(OBJDIR)/cache.h $(SRCDIR)/config.h |
| 1370 | 1375 | $(XTCC) -o $(OBJDIR)/cache.o -c $(OBJDIR)/cache_.c |
| 1371 | 1376 | |
| 1372 | 1377 | $(OBJDIR)/cache.h: $(OBJDIR)/headers |
| 1378 | + | |
| 1379 | +$(OBJDIR)/capabilities_.c: $(SRCDIR)/capabilities.c $(TRANSLATE) | |
| 1380 | + $(TRANSLATE) $(SRCDIR)/capabilities.c >$@ | |
| 1381 | + | |
| 1382 | +$(OBJDIR)/capabilities.o: $(OBJDIR)/capabilities_.c $(OBJDIR)/capabilities.h $(SRCDIR)/config.h | |
| 1383 | + $(XTCC) -o $(OBJDIR)/capabilities.o -c $(OBJDIR)/capabilities_.c | |
| 1384 | + | |
| 1385 | +$(OBJDIR)/capabilities.h: $(OBJDIR)/headers | |
| 1373 | 1386 | |
| 1374 | 1387 | $(OBJDIR)/captcha_.c: $(SRCDIR)/captcha.c $(TRANSLATE) |
| 1375 | 1388 | $(TRANSLATE) $(SRCDIR)/captcha.c >$@ |
| 1376 | 1389 | |
| 1377 | 1390 | $(OBJDIR)/captcha.o: $(OBJDIR)/captcha_.c $(OBJDIR)/captcha.h $(SRCDIR)/config.h |
| 1378 | 1391 |
| --- win/Makefile.mingw | |
| +++ win/Makefile.mingw | |
| @@ -448,10 +448,11 @@ | |
| 448 | $(SRCDIR)/branch.c \ |
| 449 | $(SRCDIR)/browse.c \ |
| 450 | $(SRCDIR)/builtin.c \ |
| 451 | $(SRCDIR)/bundle.c \ |
| 452 | $(SRCDIR)/cache.c \ |
| 453 | $(SRCDIR)/captcha.c \ |
| 454 | $(SRCDIR)/cgi.c \ |
| 455 | $(SRCDIR)/checkin.c \ |
| 456 | $(SRCDIR)/checkout.c \ |
| 457 | $(SRCDIR)/clearsign.c \ |
| @@ -628,10 +629,11 @@ | |
| 628 | $(SRCDIR)/../skins/xekri/details.txt \ |
| 629 | $(SRCDIR)/../skins/xekri/footer.txt \ |
| 630 | $(SRCDIR)/../skins/xekri/header.txt \ |
| 631 | $(SRCDIR)/ci_edit.js \ |
| 632 | $(SRCDIR)/diff.tcl \ |
| 633 | $(SRCDIR)/graph.js \ |
| 634 | $(SRCDIR)/href.js \ |
| 635 | $(SRCDIR)/login.js \ |
| 636 | $(SRCDIR)/markdown.md \ |
| 637 | $(SRCDIR)/menu.js \ |
| @@ -654,10 +656,11 @@ | |
| 654 | $(OBJDIR)/branch_.c \ |
| 655 | $(OBJDIR)/browse_.c \ |
| 656 | $(OBJDIR)/builtin_.c \ |
| 657 | $(OBJDIR)/bundle_.c \ |
| 658 | $(OBJDIR)/cache_.c \ |
| 659 | $(OBJDIR)/captcha_.c \ |
| 660 | $(OBJDIR)/cgi_.c \ |
| 661 | $(OBJDIR)/checkin_.c \ |
| 662 | $(OBJDIR)/checkout_.c \ |
| 663 | $(OBJDIR)/clearsign_.c \ |
| @@ -789,10 +792,11 @@ | |
| 789 | $(OBJDIR)/branch.o \ |
| 790 | $(OBJDIR)/browse.o \ |
| 791 | $(OBJDIR)/builtin.o \ |
| 792 | $(OBJDIR)/bundle.o \ |
| 793 | $(OBJDIR)/cache.o \ |
| 794 | $(OBJDIR)/captcha.o \ |
| 795 | $(OBJDIR)/cgi.o \ |
| 796 | $(OBJDIR)/checkin.o \ |
| 797 | $(OBJDIR)/checkout.o \ |
| 798 | $(OBJDIR)/clearsign.o \ |
| @@ -1143,10 +1147,11 @@ | |
| 1143 | $(OBJDIR)/branch_.c:$(OBJDIR)/branch.h \ |
| 1144 | $(OBJDIR)/browse_.c:$(OBJDIR)/browse.h \ |
| 1145 | $(OBJDIR)/builtin_.c:$(OBJDIR)/builtin.h \ |
| 1146 | $(OBJDIR)/bundle_.c:$(OBJDIR)/bundle.h \ |
| 1147 | $(OBJDIR)/cache_.c:$(OBJDIR)/cache.h \ |
| 1148 | $(OBJDIR)/captcha_.c:$(OBJDIR)/captcha.h \ |
| 1149 | $(OBJDIR)/cgi_.c:$(OBJDIR)/cgi.h \ |
| 1150 | $(OBJDIR)/checkin_.c:$(OBJDIR)/checkin.h \ |
| 1151 | $(OBJDIR)/checkout_.c:$(OBJDIR)/checkout.h \ |
| 1152 | $(OBJDIR)/clearsign_.c:$(OBJDIR)/clearsign.h \ |
| @@ -1368,10 +1373,18 @@ | |
| 1368 | |
| 1369 | $(OBJDIR)/cache.o: $(OBJDIR)/cache_.c $(OBJDIR)/cache.h $(SRCDIR)/config.h |
| 1370 | $(XTCC) -o $(OBJDIR)/cache.o -c $(OBJDIR)/cache_.c |
| 1371 | |
| 1372 | $(OBJDIR)/cache.h: $(OBJDIR)/headers |
| 1373 | |
| 1374 | $(OBJDIR)/captcha_.c: $(SRCDIR)/captcha.c $(TRANSLATE) |
| 1375 | $(TRANSLATE) $(SRCDIR)/captcha.c >$@ |
| 1376 | |
| 1377 | $(OBJDIR)/captcha.o: $(OBJDIR)/captcha_.c $(OBJDIR)/captcha.h $(SRCDIR)/config.h |
| 1378 |
| --- win/Makefile.mingw | |
| +++ win/Makefile.mingw | |
| @@ -448,10 +448,11 @@ | |
| 448 | $(SRCDIR)/branch.c \ |
| 449 | $(SRCDIR)/browse.c \ |
| 450 | $(SRCDIR)/builtin.c \ |
| 451 | $(SRCDIR)/bundle.c \ |
| 452 | $(SRCDIR)/cache.c \ |
| 453 | $(SRCDIR)/capabilities.c \ |
| 454 | $(SRCDIR)/captcha.c \ |
| 455 | $(SRCDIR)/cgi.c \ |
| 456 | $(SRCDIR)/checkin.c \ |
| 457 | $(SRCDIR)/checkout.c \ |
| 458 | $(SRCDIR)/clearsign.c \ |
| @@ -628,10 +629,11 @@ | |
| 629 | $(SRCDIR)/../skins/xekri/details.txt \ |
| 630 | $(SRCDIR)/../skins/xekri/footer.txt \ |
| 631 | $(SRCDIR)/../skins/xekri/header.txt \ |
| 632 | $(SRCDIR)/ci_edit.js \ |
| 633 | $(SRCDIR)/diff.tcl \ |
| 634 | $(SRCDIR)/forum.js \ |
| 635 | $(SRCDIR)/graph.js \ |
| 636 | $(SRCDIR)/href.js \ |
| 637 | $(SRCDIR)/login.js \ |
| 638 | $(SRCDIR)/markdown.md \ |
| 639 | $(SRCDIR)/menu.js \ |
| @@ -654,10 +656,11 @@ | |
| 656 | $(OBJDIR)/branch_.c \ |
| 657 | $(OBJDIR)/browse_.c \ |
| 658 | $(OBJDIR)/builtin_.c \ |
| 659 | $(OBJDIR)/bundle_.c \ |
| 660 | $(OBJDIR)/cache_.c \ |
| 661 | $(OBJDIR)/capabilities_.c \ |
| 662 | $(OBJDIR)/captcha_.c \ |
| 663 | $(OBJDIR)/cgi_.c \ |
| 664 | $(OBJDIR)/checkin_.c \ |
| 665 | $(OBJDIR)/checkout_.c \ |
| 666 | $(OBJDIR)/clearsign_.c \ |
| @@ -789,10 +792,11 @@ | |
| 792 | $(OBJDIR)/branch.o \ |
| 793 | $(OBJDIR)/browse.o \ |
| 794 | $(OBJDIR)/builtin.o \ |
| 795 | $(OBJDIR)/bundle.o \ |
| 796 | $(OBJDIR)/cache.o \ |
| 797 | $(OBJDIR)/capabilities.o \ |
| 798 | $(OBJDIR)/captcha.o \ |
| 799 | $(OBJDIR)/cgi.o \ |
| 800 | $(OBJDIR)/checkin.o \ |
| 801 | $(OBJDIR)/checkout.o \ |
| 802 | $(OBJDIR)/clearsign.o \ |
| @@ -1143,10 +1147,11 @@ | |
| 1147 | $(OBJDIR)/branch_.c:$(OBJDIR)/branch.h \ |
| 1148 | $(OBJDIR)/browse_.c:$(OBJDIR)/browse.h \ |
| 1149 | $(OBJDIR)/builtin_.c:$(OBJDIR)/builtin.h \ |
| 1150 | $(OBJDIR)/bundle_.c:$(OBJDIR)/bundle.h \ |
| 1151 | $(OBJDIR)/cache_.c:$(OBJDIR)/cache.h \ |
| 1152 | $(OBJDIR)/capabilities_.c:$(OBJDIR)/capabilities.h \ |
| 1153 | $(OBJDIR)/captcha_.c:$(OBJDIR)/captcha.h \ |
| 1154 | $(OBJDIR)/cgi_.c:$(OBJDIR)/cgi.h \ |
| 1155 | $(OBJDIR)/checkin_.c:$(OBJDIR)/checkin.h \ |
| 1156 | $(OBJDIR)/checkout_.c:$(OBJDIR)/checkout.h \ |
| 1157 | $(OBJDIR)/clearsign_.c:$(OBJDIR)/clearsign.h \ |
| @@ -1368,10 +1373,18 @@ | |
| 1373 | |
| 1374 | $(OBJDIR)/cache.o: $(OBJDIR)/cache_.c $(OBJDIR)/cache.h $(SRCDIR)/config.h |
| 1375 | $(XTCC) -o $(OBJDIR)/cache.o -c $(OBJDIR)/cache_.c |
| 1376 | |
| 1377 | $(OBJDIR)/cache.h: $(OBJDIR)/headers |
| 1378 | |
| 1379 | $(OBJDIR)/capabilities_.c: $(SRCDIR)/capabilities.c $(TRANSLATE) |
| 1380 | $(TRANSLATE) $(SRCDIR)/capabilities.c >$@ |
| 1381 | |
| 1382 | $(OBJDIR)/capabilities.o: $(OBJDIR)/capabilities_.c $(OBJDIR)/capabilities.h $(SRCDIR)/config.h |
| 1383 | $(XTCC) -o $(OBJDIR)/capabilities.o -c $(OBJDIR)/capabilities_.c |
| 1384 | |
| 1385 | $(OBJDIR)/capabilities.h: $(OBJDIR)/headers |
| 1386 | |
| 1387 | $(OBJDIR)/captcha_.c: $(SRCDIR)/captcha.c $(TRANSLATE) |
| 1388 | $(TRANSLATE) $(SRCDIR)/captcha.c >$@ |
| 1389 | |
| 1390 | $(OBJDIR)/captcha.o: $(OBJDIR)/captcha_.c $(OBJDIR)/captcha.h $(SRCDIR)/config.h |
| 1391 |
+11
| --- win/Makefile.msc | ||
| +++ win/Makefile.msc | ||
| @@ -390,10 +390,11 @@ | ||
| 390 | 390 | branch_.c \ |
| 391 | 391 | browse_.c \ |
| 392 | 392 | builtin_.c \ |
| 393 | 393 | bundle_.c \ |
| 394 | 394 | cache_.c \ |
| 395 | + capabilities_.c \ | |
| 395 | 396 | captcha_.c \ |
| 396 | 397 | cgi_.c \ |
| 397 | 398 | checkin_.c \ |
| 398 | 399 | checkout_.c \ |
| 399 | 400 | clearsign_.c \ |
| @@ -569,10 +570,11 @@ | ||
| 569 | 570 | $(SRCDIR)\..\skins\xekri\details.txt \ |
| 570 | 571 | $(SRCDIR)\..\skins\xekri\footer.txt \ |
| 571 | 572 | $(SRCDIR)\..\skins\xekri\header.txt \ |
| 572 | 573 | $(SRCDIR)\ci_edit.js \ |
| 573 | 574 | $(SRCDIR)\diff.tcl \ |
| 575 | + $(SRCDIR)\forum.js \ | |
| 574 | 576 | $(SRCDIR)\graph.js \ |
| 575 | 577 | $(SRCDIR)\href.js \ |
| 576 | 578 | $(SRCDIR)\login.js \ |
| 577 | 579 | $(SRCDIR)\markdown.md \ |
| 578 | 580 | $(SRCDIR)\menu.js \ |
| @@ -594,10 +596,11 @@ | ||
| 594 | 596 | $(OX)\branch$O \ |
| 595 | 597 | $(OX)\browse$O \ |
| 596 | 598 | $(OX)\builtin$O \ |
| 597 | 599 | $(OX)\bundle$O \ |
| 598 | 600 | $(OX)\cache$O \ |
| 601 | + $(OX)\capabilities$O \ | |
| 599 | 602 | $(OX)\captcha$O \ |
| 600 | 603 | $(OX)\cgi$O \ |
| 601 | 604 | $(OX)\checkin$O \ |
| 602 | 605 | $(OX)\checkout$O \ |
| 603 | 606 | $(OX)\clearsign$O \ |
| @@ -788,10 +791,11 @@ | ||
| 788 | 791 | echo $(OX)\branch.obj >> $@ |
| 789 | 792 | echo $(OX)\browse.obj >> $@ |
| 790 | 793 | echo $(OX)\builtin.obj >> $@ |
| 791 | 794 | echo $(OX)\bundle.obj >> $@ |
| 792 | 795 | echo $(OX)\cache.obj >> $@ |
| 796 | + echo $(OX)\capabilities.obj >> $@ | |
| 793 | 797 | echo $(OX)\captcha.obj >> $@ |
| 794 | 798 | echo $(OX)\cgi.obj >> $@ |
| 795 | 799 | echo $(OX)\checkin.obj >> $@ |
| 796 | 800 | echo $(OX)\checkout.obj >> $@ |
| 797 | 801 | echo $(OX)\clearsign.obj >> $@ |
| @@ -1104,10 +1108,16 @@ | ||
| 1104 | 1108 | $(OX)\cache$O : cache_.c cache.h |
| 1105 | 1109 | $(TCC) /Fo$@ -c cache_.c |
| 1106 | 1110 | |
| 1107 | 1111 | cache_.c : $(SRCDIR)\cache.c |
| 1108 | 1112 | translate$E $** > $@ |
| 1113 | + | |
| 1114 | +$(OX)\capabilities$O : capabilities_.c capabilities.h | |
| 1115 | + $(TCC) /Fo$@ -c capabilities_.c | |
| 1116 | + | |
| 1117 | +capabilities_.c : $(SRCDIR)\capabilities.c | |
| 1118 | + translate$E $** > $@ | |
| 1109 | 1119 | |
| 1110 | 1120 | $(OX)\captcha$O : captcha_.c captcha.h |
| 1111 | 1121 | $(TCC) /Fo$@ -c captcha_.c |
| 1112 | 1122 | |
| 1113 | 1123 | captcha_.c : $(SRCDIR)\captcha.c |
| @@ -1847,10 +1857,11 @@ | ||
| 1847 | 1857 | branch_.c:branch.h \ |
| 1848 | 1858 | browse_.c:browse.h \ |
| 1849 | 1859 | builtin_.c:builtin.h \ |
| 1850 | 1860 | bundle_.c:bundle.h \ |
| 1851 | 1861 | cache_.c:cache.h \ |
| 1862 | + capabilities_.c:capabilities.h \ | |
| 1852 | 1863 | captcha_.c:captcha.h \ |
| 1853 | 1864 | cgi_.c:cgi.h \ |
| 1854 | 1865 | checkin_.c:checkin.h \ |
| 1855 | 1866 | checkout_.c:checkout.h \ |
| 1856 | 1867 | clearsign_.c:clearsign.h \ |
| 1857 | 1868 |
| --- win/Makefile.msc | |
| +++ win/Makefile.msc | |
| @@ -390,10 +390,11 @@ | |
| 390 | branch_.c \ |
| 391 | browse_.c \ |
| 392 | builtin_.c \ |
| 393 | bundle_.c \ |
| 394 | cache_.c \ |
| 395 | captcha_.c \ |
| 396 | cgi_.c \ |
| 397 | checkin_.c \ |
| 398 | checkout_.c \ |
| 399 | clearsign_.c \ |
| @@ -569,10 +570,11 @@ | |
| 569 | $(SRCDIR)\..\skins\xekri\details.txt \ |
| 570 | $(SRCDIR)\..\skins\xekri\footer.txt \ |
| 571 | $(SRCDIR)\..\skins\xekri\header.txt \ |
| 572 | $(SRCDIR)\ci_edit.js \ |
| 573 | $(SRCDIR)\diff.tcl \ |
| 574 | $(SRCDIR)\graph.js \ |
| 575 | $(SRCDIR)\href.js \ |
| 576 | $(SRCDIR)\login.js \ |
| 577 | $(SRCDIR)\markdown.md \ |
| 578 | $(SRCDIR)\menu.js \ |
| @@ -594,10 +596,11 @@ | |
| 594 | $(OX)\branch$O \ |
| 595 | $(OX)\browse$O \ |
| 596 | $(OX)\builtin$O \ |
| 597 | $(OX)\bundle$O \ |
| 598 | $(OX)\cache$O \ |
| 599 | $(OX)\captcha$O \ |
| 600 | $(OX)\cgi$O \ |
| 601 | $(OX)\checkin$O \ |
| 602 | $(OX)\checkout$O \ |
| 603 | $(OX)\clearsign$O \ |
| @@ -788,10 +791,11 @@ | |
| 788 | echo $(OX)\branch.obj >> $@ |
| 789 | echo $(OX)\browse.obj >> $@ |
| 790 | echo $(OX)\builtin.obj >> $@ |
| 791 | echo $(OX)\bundle.obj >> $@ |
| 792 | echo $(OX)\cache.obj >> $@ |
| 793 | echo $(OX)\captcha.obj >> $@ |
| 794 | echo $(OX)\cgi.obj >> $@ |
| 795 | echo $(OX)\checkin.obj >> $@ |
| 796 | echo $(OX)\checkout.obj >> $@ |
| 797 | echo $(OX)\clearsign.obj >> $@ |
| @@ -1104,10 +1108,16 @@ | |
| 1104 | $(OX)\cache$O : cache_.c cache.h |
| 1105 | $(TCC) /Fo$@ -c cache_.c |
| 1106 | |
| 1107 | cache_.c : $(SRCDIR)\cache.c |
| 1108 | translate$E $** > $@ |
| 1109 | |
| 1110 | $(OX)\captcha$O : captcha_.c captcha.h |
| 1111 | $(TCC) /Fo$@ -c captcha_.c |
| 1112 | |
| 1113 | captcha_.c : $(SRCDIR)\captcha.c |
| @@ -1847,10 +1857,11 @@ | |
| 1847 | branch_.c:branch.h \ |
| 1848 | browse_.c:browse.h \ |
| 1849 | builtin_.c:builtin.h \ |
| 1850 | bundle_.c:bundle.h \ |
| 1851 | cache_.c:cache.h \ |
| 1852 | captcha_.c:captcha.h \ |
| 1853 | cgi_.c:cgi.h \ |
| 1854 | checkin_.c:checkin.h \ |
| 1855 | checkout_.c:checkout.h \ |
| 1856 | clearsign_.c:clearsign.h \ |
| 1857 |
| --- win/Makefile.msc | |
| +++ win/Makefile.msc | |
| @@ -390,10 +390,11 @@ | |
| 390 | branch_.c \ |
| 391 | browse_.c \ |
| 392 | builtin_.c \ |
| 393 | bundle_.c \ |
| 394 | cache_.c \ |
| 395 | capabilities_.c \ |
| 396 | captcha_.c \ |
| 397 | cgi_.c \ |
| 398 | checkin_.c \ |
| 399 | checkout_.c \ |
| 400 | clearsign_.c \ |
| @@ -569,10 +570,11 @@ | |
| 570 | $(SRCDIR)\..\skins\xekri\details.txt \ |
| 571 | $(SRCDIR)\..\skins\xekri\footer.txt \ |
| 572 | $(SRCDIR)\..\skins\xekri\header.txt \ |
| 573 | $(SRCDIR)\ci_edit.js \ |
| 574 | $(SRCDIR)\diff.tcl \ |
| 575 | $(SRCDIR)\forum.js \ |
| 576 | $(SRCDIR)\graph.js \ |
| 577 | $(SRCDIR)\href.js \ |
| 578 | $(SRCDIR)\login.js \ |
| 579 | $(SRCDIR)\markdown.md \ |
| 580 | $(SRCDIR)\menu.js \ |
| @@ -594,10 +596,11 @@ | |
| 596 | $(OX)\branch$O \ |
| 597 | $(OX)\browse$O \ |
| 598 | $(OX)\builtin$O \ |
| 599 | $(OX)\bundle$O \ |
| 600 | $(OX)\cache$O \ |
| 601 | $(OX)\capabilities$O \ |
| 602 | $(OX)\captcha$O \ |
| 603 | $(OX)\cgi$O \ |
| 604 | $(OX)\checkin$O \ |
| 605 | $(OX)\checkout$O \ |
| 606 | $(OX)\clearsign$O \ |
| @@ -788,10 +791,11 @@ | |
| 791 | echo $(OX)\branch.obj >> $@ |
| 792 | echo $(OX)\browse.obj >> $@ |
| 793 | echo $(OX)\builtin.obj >> $@ |
| 794 | echo $(OX)\bundle.obj >> $@ |
| 795 | echo $(OX)\cache.obj >> $@ |
| 796 | echo $(OX)\capabilities.obj >> $@ |
| 797 | echo $(OX)\captcha.obj >> $@ |
| 798 | echo $(OX)\cgi.obj >> $@ |
| 799 | echo $(OX)\checkin.obj >> $@ |
| 800 | echo $(OX)\checkout.obj >> $@ |
| 801 | echo $(OX)\clearsign.obj >> $@ |
| @@ -1104,10 +1108,16 @@ | |
| 1108 | $(OX)\cache$O : cache_.c cache.h |
| 1109 | $(TCC) /Fo$@ -c cache_.c |
| 1110 | |
| 1111 | cache_.c : $(SRCDIR)\cache.c |
| 1112 | translate$E $** > $@ |
| 1113 | |
| 1114 | $(OX)\capabilities$O : capabilities_.c capabilities.h |
| 1115 | $(TCC) /Fo$@ -c capabilities_.c |
| 1116 | |
| 1117 | capabilities_.c : $(SRCDIR)\capabilities.c |
| 1118 | translate$E $** > $@ |
| 1119 | |
| 1120 | $(OX)\captcha$O : captcha_.c captcha.h |
| 1121 | $(TCC) /Fo$@ -c captcha_.c |
| 1122 | |
| 1123 | captcha_.c : $(SRCDIR)\captcha.c |
| @@ -1847,10 +1857,11 @@ | |
| 1857 | branch_.c:branch.h \ |
| 1858 | browse_.c:browse.h \ |
| 1859 | builtin_.c:builtin.h \ |
| 1860 | bundle_.c:bundle.h \ |
| 1861 | cache_.c:cache.h \ |
| 1862 | capabilities_.c:capabilities.h \ |
| 1863 | captcha_.c:captcha.h \ |
| 1864 | cgi_.c:cgi.h \ |
| 1865 | checkin_.c:checkin.h \ |
| 1866 | checkout_.c:checkout.h \ |
| 1867 | clearsign_.c:clearsign.h \ |
| 1868 |
+130
-4
| --- www/fileformat.wiki | ||
| +++ www/fileformat.wiki | ||
| @@ -71,13 +71,14 @@ | ||
| 71 | 71 | <li> [#ctrl | Control Artifacts] </li> |
| 72 | 72 | <li> [#wikichng | Wiki Pages] </li> |
| 73 | 73 | <li> [#tktchng | Ticket Changes] </li> |
| 74 | 74 | <li> [#attachment | Attachments] </li> |
| 75 | 75 | <li> [#event | TechNotes] </li> |
| 76 | +<li> [#forum | Forum Posts] </li> | |
| 76 | 77 | </ul> |
| 77 | 78 | |
| 78 | -These seven structural artifact types are described in subsections below. | |
| 79 | +These eight structural artifact types are described in subsections below. | |
| 79 | 80 | |
| 80 | 81 | Structural artifacts are ASCII text. The artifact may be PGP clearsigned. |
| 81 | 82 | After removal of the PGP clearsign header and suffix (if any) a structural |
| 82 | 83 | artifact consists of one or more "cards" separated by a single newline |
| 83 | 84 | (ASCII: 0x0a) character. Each card begins with a single |
| @@ -525,10 +526,84 @@ | ||
| 525 | 526 | technote. The format of the W card is exactly the same as for a |
| 526 | 527 | [#wikichng | wiki artifact]. |
| 527 | 528 | |
| 528 | 529 | The Z card is the required checksum over the rest of the artifact. |
| 529 | 530 | |
| 531 | +<a name="forum"></a> | |
| 532 | +<h3>2.8 Forum Posts</h3> | |
| 533 | + | |
| 534 | +Forum posts are intended as a mechanism for users and developers to | |
| 535 | +discuss a project. Forum mosts are like messages on a mailing list. | |
| 536 | + | |
| 537 | +The following cards are allowed on an forum post artifact: | |
| 538 | + | |
| 539 | +<blockquote> | |
| 540 | +<b>D</b> <i>time-and-date-stamp</i><br /> | |
| 541 | +<b>G</b> <i>thread-root</i><br /> | |
| 542 | +<b>H</b> <i>thread-title</i><br /> | |
| 543 | +<b>I</b> <i>in-reply-to</i><br /> | |
| 544 | +<b>N</b> <i>mimetype</i><br /> | |
| 545 | +<b>P</b> <i>parent-artifact-id</i><br /> | |
| 546 | +<b>U</b> <i>user-name</i><br /> | |
| 547 | +<b>W</b> <i>size</i> <b>\n</b> <i>text</i> <b>\n</b><br /> | |
| 548 | +<b>Z</b> <i>checksum</i> | |
| 549 | +</blockquote> | |
| 550 | + | |
| 551 | +Every forum post must have either one I card and one G card | |
| 552 | +or one H card. | |
| 553 | +Forum posts are organized into topic threads. The initial | |
| 554 | +post for a thread (the root post) has an H card giving the title or | |
| 555 | +subject for that thread. The argument to the H card is a string | |
| 556 | +in the same format as a comment string in a C card. | |
| 557 | +All follow-up posts have an I card that | |
| 558 | +indicates which prior post in the same thread the current forum | |
| 559 | +post is replying to, and a G card specifying the root post for | |
| 560 | +the entire thread. The argument to G and I cards is the | |
| 561 | +artifact hash for the prior forum post to which the card refers. | |
| 562 | + | |
| 563 | +In theory, it is sufficient for follow-up posts to have only an | |
| 564 | +I card, since the G card value could be computed by following a | |
| 565 | +chain of I cards. However, the G card is required in order to | |
| 566 | +associate the artifact with a forum thread in the case where an | |
| 567 | +intermediate artifact in the I card chain is shunned or otherwise | |
| 568 | +becomes unreadable. | |
| 569 | + | |
| 570 | +A single D card is required to give the date and time when the | |
| 571 | +forum post was created. | |
| 572 | + | |
| 573 | +The optional N card specifies the mimetype of the text of the technote | |
| 574 | +that is contained in the W card. If the N card is omitted, then the | |
| 575 | +W card text mimetype is assumed to be text/x-fossil, which is the | |
| 576 | +Fossil wiki format. | |
| 577 | + | |
| 578 | +The optional P card specifies a prior forum post for which this | |
| 579 | +forum post is an edit. For display purposes, only the child post | |
| 580 | +is shown, though the historical post is retained as a record. | |
| 581 | +If P cards are used and there exist multiple versions of the same | |
| 582 | +forum post, then I cards for other artifacts refer to whichever | |
| 583 | +version of the post was current at the time the reply was made, | |
| 584 | +but G cards refer to the initial, unedited root post for the thread. | |
| 585 | +Thus, following the chain of I cards back to the root of the thread | |
| 586 | +may land on a different post than the one given in the G card. | |
| 587 | +However, following the chain of I cards back to the thread root, | |
| 588 | +then following P cards back to the initial version of the thread | |
| 589 | +root must give the same artifact as is provided by the G card, | |
| 590 | +otherwise the artifact containing the G card is considered invalid | |
| 591 | +and should be ignored. | |
| 592 | + | |
| 593 | +In general, P cards may contain multiple arguments, indicating a | |
| 594 | +merge. But since forum posts cannot be merged, the | |
| 595 | +P card of a forum post may only contain a single argument. | |
| 596 | + | |
| 597 | +The U card gives name of the user who entered the forum post. | |
| 598 | + | |
| 599 | +A single W card provides wiki text for the forum post. | |
| 600 | +The format of the W card is exactly the same as for a | |
| 601 | +[#wikichng | wiki artifact]. | |
| 602 | + | |
| 603 | +The Z card is the required checksum over the rest of the artifact. | |
| 604 | + | |
| 530 | 605 | |
| 531 | 606 | <a name="summary"></a> |
| 532 | 607 | <h2>3.0 Card Summary</h2> |
| 533 | 608 | |
| 534 | 609 | The following table summarizes the various kinds of cards that appear |
| @@ -539,20 +614,21 @@ | ||
| 539 | 614 | or more such cards are required. |
| 540 | 615 | |
| 541 | 616 | <table border=1 width="100%"> |
| 542 | 617 | <tr> |
| 543 | 618 | <th rowspan=2 valign=bottom>Card Format</th> |
| 544 | -<th colspan=7>Used By</th> | |
| 619 | +<th colspan=8>Used By</th> | |
| 545 | 620 | </tr> |
| 546 | 621 | <tr> |
| 547 | 622 | <th>Manifest</th> |
| 548 | 623 | <th>Cluster</th> |
| 549 | 624 | <th>Control</th> |
| 550 | 625 | <th>Wiki</th> |
| 551 | 626 | <th>Ticket</th> |
| 552 | 627 | <th>Attachment</th> |
| 553 | 628 | <th>Technote</th> |
| 629 | +<th>Forum</th> | |
| 554 | 630 | </tr> |
| 555 | 631 | <tr> |
| 556 | 632 | <td><b>A</b> <i>filename</i> <i>target</i> ?<i>source</i>?</td> |
| 557 | 633 | <td> </td> |
| 558 | 634 | <td> </td> |
| @@ -559,36 +635,39 @@ | ||
| 559 | 635 | <td> </td> |
| 560 | 636 | <td> </td> |
| 561 | 637 | <td> </td> |
| 562 | 638 | <td align=center><b>1</b></td> |
| 563 | 639 | <td> </td> |
| 640 | +<td> </td> | |
| 564 | 641 | </tr> |
| 565 | 642 | <tr> |
| 566 | 643 | <td><b>B</b> <i>baseline</i></td> |
| 567 | -<td align=center><b>0-1*</b></td> | |
| 644 | +<td align=center><b>0-1</b></td> | |
| 645 | +<td> </td> | |
| 568 | 646 | <td> </td> |
| 569 | 647 | <td> </td> |
| 570 | 648 | <td> </td> |
| 571 | 649 | <td> </td> |
| 572 | 650 | <td> </td> |
| 573 | 651 | <td> </td> |
| 574 | 652 | </tr> |
| 575 | -<tr><td> </td><td colspan='7'>* = Required for delta manifests</td></tr> | |
| 576 | 653 | <tr> |
| 577 | 654 | <td><b>C</b> <i>comment-text</i></td> |
| 578 | 655 | <td align=center><b>1</b></td> |
| 579 | 656 | <td> </td> |
| 580 | 657 | <td> </td> |
| 581 | 658 | <td> </td> |
| 582 | 659 | <td> </td> |
| 583 | 660 | <td align=center><b>0-1</b></td> |
| 584 | 661 | <td align=center><b>0-1</b></td> |
| 662 | +<td> </td> | |
| 585 | 663 | </tr> |
| 586 | 664 | <tr> |
| 587 | 665 | <td><b>D</b> <i>date-time-stamp</i></td> |
| 588 | 666 | <td align=center><b>1</b></td> |
| 589 | 667 | <td> </td> |
| 668 | +<td align=center><b>1</b></td> | |
| 590 | 669 | <td align=center><b>1</b></td> |
| 591 | 670 | <td align=center><b>1</b></td> |
| 592 | 671 | <td align=center><b>1</b></td> |
| 593 | 672 | <td align=center><b>1</b></td> |
| 594 | 673 | <td align=center><b>1</b></td> |
| @@ -600,10 +679,11 @@ | ||
| 600 | 679 | <td> </td> |
| 601 | 680 | <td> </td> |
| 602 | 681 | <td> </td> |
| 603 | 682 | <td> </td> |
| 604 | 683 | <td align=center><b>1</b></td> |
| 684 | +<td> </td> | |
| 605 | 685 | </tr> |
| 606 | 686 | <tr> |
| 607 | 687 | <td><b>F</b> <i>filename</i> ?<i>uuid</i>? ?<i>permissions</i>? ?<i>oldname</i>?</td> |
| 608 | 688 | <td align=center><b>0+</b></td> |
| 609 | 689 | <td> </td> |
| @@ -610,18 +690,53 @@ | ||
| 610 | 690 | <td> </td> |
| 611 | 691 | <td> </td> |
| 612 | 692 | <td> </td> |
| 613 | 693 | <td> </td> |
| 614 | 694 | <td> </td> |
| 695 | +<td> </td> | |
| 696 | +</tr> | |
| 697 | +<tr> | |
| 698 | +<td><b>G</b> <i>thread-root</i></td> | |
| 699 | +<td> </td> | |
| 700 | +<td> </td> | |
| 701 | +<td> </td> | |
| 702 | +<td> </td> | |
| 703 | +<td> </td> | |
| 704 | +<td> </td> | |
| 705 | +<td> </td> | |
| 706 | +<td align=center><b>0-1</b></td> | |
| 707 | +</tr> | |
| 708 | +<tr> | |
| 709 | +<td><b>H</b> <i>thread-title</i></td> | |
| 710 | +<td> </td> | |
| 711 | +<td> </td> | |
| 712 | +<td> </td> | |
| 713 | +<td> </td> | |
| 714 | +<td> </td> | |
| 715 | +<td> </td> | |
| 716 | +<td> </td> | |
| 717 | +<td align=center><b>0-1</b></td> | |
| 718 | +</tr> | |
| 719 | +<tr> | |
| 720 | +<td><b>I</b> <i>in-reply-to</i></td> | |
| 721 | +<td> </td> | |
| 722 | +<td> </td> | |
| 723 | +<td> </td> | |
| 724 | +<td> </td> | |
| 725 | +<td> </td> | |
| 726 | +<td> </td> | |
| 727 | +<td> </td> | |
| 728 | +<td align=center><b>0-1</b></td> | |
| 615 | 729 | </tr> |
| 616 | 730 | <tr> |
| 617 | 731 | <td><b>J</b> <i>name</i> ?<i>value</i>?</td> |
| 618 | 732 | <td> </td> |
| 619 | 733 | <td> </td> |
| 620 | 734 | <td> </td> |
| 621 | 735 | <td> </td> |
| 622 | 736 | <td align=center><b>1+</b></td> |
| 737 | +<td> </td> | |
| 623 | 738 | <td> </td> |
| 624 | 739 | <td> </td> |
| 625 | 740 | </tr> |
| 626 | 741 | <tr> |
| 627 | 742 | <td><b>K</b> <i>ticket-uuid</i></td> |
| @@ -630,10 +745,11 @@ | ||
| 630 | 745 | <td> </td> |
| 631 | 746 | <td> </td> |
| 632 | 747 | <td align=center><b>1</b></td> |
| 633 | 748 | <td> </td> |
| 634 | 749 | <td> </td> |
| 750 | +<td> </td> | |
| 635 | 751 | </tr> |
| 636 | 752 | <tr> |
| 637 | 753 | <td><b>L</b> <i>wiki-title</i></td> |
| 638 | 754 | <td> </td> |
| 639 | 755 | <td> </td> |
| @@ -640,15 +756,17 @@ | ||
| 640 | 756 | <td> </td> |
| 641 | 757 | <td align=center><b>1</b></td> |
| 642 | 758 | <td> </td> |
| 643 | 759 | <td> </td> |
| 644 | 760 | <td> </td> |
| 761 | +<td> </td> | |
| 645 | 762 | </tr> |
| 646 | 763 | <tr> |
| 647 | 764 | <td><b>M</b> <i>uuid</i></td> |
| 648 | 765 | <td> </td> |
| 649 | 766 | <td align=center><b>1+</b></td> |
| 767 | +<td> </td> | |
| 650 | 768 | <td> </td> |
| 651 | 769 | <td> </td> |
| 652 | 770 | <td> </td> |
| 653 | 771 | <td> </td> |
| 654 | 772 | <td> </td> |
| @@ -660,10 +778,11 @@ | ||
| 660 | 778 | <td> </td> |
| 661 | 779 | <td align=center><b>0-1</b></td> |
| 662 | 780 | <td> </td> |
| 663 | 781 | <td align=center><b>0-1</b></td> |
| 664 | 782 | <td align=center><b>0-1</b></td> |
| 783 | +<td align=center><b>0-1</b></td> | |
| 665 | 784 | </tr> |
| 666 | 785 | <tr> |
| 667 | 786 | <td><b>P</b> <i>uuid ...</i></td> |
| 668 | 787 | <td align=center><b>0-1</b></td> |
| 669 | 788 | <td> </td> |
| @@ -670,14 +789,16 @@ | ||
| 670 | 789 | <td> </td> |
| 671 | 790 | <td align=center><b>0-1</b></td> |
| 672 | 791 | <td> </td> |
| 673 | 792 | <td> </td> |
| 674 | 793 | <td align=center><b>0-1</b></td> |
| 794 | +<td align=center><b>0-1</b></td> | |
| 675 | 795 | </tr> |
| 676 | 796 | <tr> |
| 677 | 797 | <td><b>Q</b> (<b>+</b>|<b>-</b>)<i>uuid</i> ?<i>uuid</i>?</td> |
| 678 | 798 | <td align=center><b>0+</b></td> |
| 799 | +<td> </td> | |
| 679 | 800 | <td> </td> |
| 680 | 801 | <td> </td> |
| 681 | 802 | <td> </td> |
| 682 | 803 | <td> </td> |
| 683 | 804 | <td> </td> |
| @@ -690,19 +811,21 @@ | ||
| 690 | 811 | <td> </td> |
| 691 | 812 | <td> </td> |
| 692 | 813 | <td> </td> |
| 693 | 814 | <td> </td> |
| 694 | 815 | <td> </td> |
| 816 | +<td> </td> | |
| 695 | 817 | <tr> |
| 696 | 818 | <td><b>T</b> (<b>+</b>|<b>*</b>|<b>-</b>)<i>tagname</i> <i>uuid</i> ?<i>value</i>?</td> |
| 697 | 819 | <td align=center><b>0+</b></td> |
| 698 | 820 | <td> </td> |
| 699 | 821 | <td align=center><b>1+</b></td> |
| 700 | 822 | <td> </td> |
| 701 | 823 | <td> </td> |
| 702 | 824 | <td> </td> |
| 703 | 825 | <td align=center><b>0+</b></td> |
| 826 | +<td> </td> | |
| 704 | 827 | </tr> |
| 705 | 828 | <tr> |
| 706 | 829 | <td><b>U</b> <i>username</i></td> |
| 707 | 830 | <td align=center><b>1</b></td> |
| 708 | 831 | <td> </td> |
| @@ -709,10 +832,11 @@ | ||
| 709 | 832 | <td align=center><b>1</b></td> |
| 710 | 833 | <td align=center><b>1</b></td> |
| 711 | 834 | <td align=center><b>1</b></td> |
| 712 | 835 | <td align=center><b>0-1</b></td> |
| 713 | 836 | <td align=center><b>0-1</b></td> |
| 837 | +<td align=center><b>1</b></td> | |
| 714 | 838 | </tr> |
| 715 | 839 | <tr> |
| 716 | 840 | <td><b>W</b> <i>size</i> <b>\n</b> <i>text</i> <b>\n</b></td> |
| 717 | 841 | <td> </td> |
| 718 | 842 | <td> </td> |
| @@ -719,13 +843,15 @@ | ||
| 719 | 843 | <td> </td> |
| 720 | 844 | <td align=center><b>1</b></td> |
| 721 | 845 | <td> </td> |
| 722 | 846 | <td> </td> |
| 723 | 847 | <td align=center><b>1</b></td> |
| 848 | +<td align=center><b>1</b></td> | |
| 724 | 849 | </tr> |
| 725 | 850 | <tr> |
| 726 | 851 | <td><b>Z</b> <i>md5sum</i></td> |
| 852 | +<td align=center><b>1</b></td> | |
| 727 | 853 | <td align=center><b>1</b></td> |
| 728 | 854 | <td align=center><b>1</b></td> |
| 729 | 855 | <td align=center><b>1</b></td> |
| 730 | 856 | <td align=center><b>1</b></td> |
| 731 | 857 | <td align=center><b>1</b></td> |
| 732 | 858 |
| --- www/fileformat.wiki | |
| +++ www/fileformat.wiki | |
| @@ -71,13 +71,14 @@ | |
| 71 | <li> [#ctrl | Control Artifacts] </li> |
| 72 | <li> [#wikichng | Wiki Pages] </li> |
| 73 | <li> [#tktchng | Ticket Changes] </li> |
| 74 | <li> [#attachment | Attachments] </li> |
| 75 | <li> [#event | TechNotes] </li> |
| 76 | </ul> |
| 77 | |
| 78 | These seven structural artifact types are described in subsections below. |
| 79 | |
| 80 | Structural artifacts are ASCII text. The artifact may be PGP clearsigned. |
| 81 | After removal of the PGP clearsign header and suffix (if any) a structural |
| 82 | artifact consists of one or more "cards" separated by a single newline |
| 83 | (ASCII: 0x0a) character. Each card begins with a single |
| @@ -525,10 +526,84 @@ | |
| 525 | technote. The format of the W card is exactly the same as for a |
| 526 | [#wikichng | wiki artifact]. |
| 527 | |
| 528 | The Z card is the required checksum over the rest of the artifact. |
| 529 | |
| 530 | |
| 531 | <a name="summary"></a> |
| 532 | <h2>3.0 Card Summary</h2> |
| 533 | |
| 534 | The following table summarizes the various kinds of cards that appear |
| @@ -539,20 +614,21 @@ | |
| 539 | or more such cards are required. |
| 540 | |
| 541 | <table border=1 width="100%"> |
| 542 | <tr> |
| 543 | <th rowspan=2 valign=bottom>Card Format</th> |
| 544 | <th colspan=7>Used By</th> |
| 545 | </tr> |
| 546 | <tr> |
| 547 | <th>Manifest</th> |
| 548 | <th>Cluster</th> |
| 549 | <th>Control</th> |
| 550 | <th>Wiki</th> |
| 551 | <th>Ticket</th> |
| 552 | <th>Attachment</th> |
| 553 | <th>Technote</th> |
| 554 | </tr> |
| 555 | <tr> |
| 556 | <td><b>A</b> <i>filename</i> <i>target</i> ?<i>source</i>?</td> |
| 557 | <td> </td> |
| 558 | <td> </td> |
| @@ -559,36 +635,39 @@ | |
| 559 | <td> </td> |
| 560 | <td> </td> |
| 561 | <td> </td> |
| 562 | <td align=center><b>1</b></td> |
| 563 | <td> </td> |
| 564 | </tr> |
| 565 | <tr> |
| 566 | <td><b>B</b> <i>baseline</i></td> |
| 567 | <td align=center><b>0-1*</b></td> |
| 568 | <td> </td> |
| 569 | <td> </td> |
| 570 | <td> </td> |
| 571 | <td> </td> |
| 572 | <td> </td> |
| 573 | <td> </td> |
| 574 | </tr> |
| 575 | <tr><td> </td><td colspan='7'>* = Required for delta manifests</td></tr> |
| 576 | <tr> |
| 577 | <td><b>C</b> <i>comment-text</i></td> |
| 578 | <td align=center><b>1</b></td> |
| 579 | <td> </td> |
| 580 | <td> </td> |
| 581 | <td> </td> |
| 582 | <td> </td> |
| 583 | <td align=center><b>0-1</b></td> |
| 584 | <td align=center><b>0-1</b></td> |
| 585 | </tr> |
| 586 | <tr> |
| 587 | <td><b>D</b> <i>date-time-stamp</i></td> |
| 588 | <td align=center><b>1</b></td> |
| 589 | <td> </td> |
| 590 | <td align=center><b>1</b></td> |
| 591 | <td align=center><b>1</b></td> |
| 592 | <td align=center><b>1</b></td> |
| 593 | <td align=center><b>1</b></td> |
| 594 | <td align=center><b>1</b></td> |
| @@ -600,10 +679,11 @@ | |
| 600 | <td> </td> |
| 601 | <td> </td> |
| 602 | <td> </td> |
| 603 | <td> </td> |
| 604 | <td align=center><b>1</b></td> |
| 605 | </tr> |
| 606 | <tr> |
| 607 | <td><b>F</b> <i>filename</i> ?<i>uuid</i>? ?<i>permissions</i>? ?<i>oldname</i>?</td> |
| 608 | <td align=center><b>0+</b></td> |
| 609 | <td> </td> |
| @@ -610,18 +690,53 @@ | |
| 610 | <td> </td> |
| 611 | <td> </td> |
| 612 | <td> </td> |
| 613 | <td> </td> |
| 614 | <td> </td> |
| 615 | </tr> |
| 616 | <tr> |
| 617 | <td><b>J</b> <i>name</i> ?<i>value</i>?</td> |
| 618 | <td> </td> |
| 619 | <td> </td> |
| 620 | <td> </td> |
| 621 | <td> </td> |
| 622 | <td align=center><b>1+</b></td> |
| 623 | <td> </td> |
| 624 | <td> </td> |
| 625 | </tr> |
| 626 | <tr> |
| 627 | <td><b>K</b> <i>ticket-uuid</i></td> |
| @@ -630,10 +745,11 @@ | |
| 630 | <td> </td> |
| 631 | <td> </td> |
| 632 | <td align=center><b>1</b></td> |
| 633 | <td> </td> |
| 634 | <td> </td> |
| 635 | </tr> |
| 636 | <tr> |
| 637 | <td><b>L</b> <i>wiki-title</i></td> |
| 638 | <td> </td> |
| 639 | <td> </td> |
| @@ -640,15 +756,17 @@ | |
| 640 | <td> </td> |
| 641 | <td align=center><b>1</b></td> |
| 642 | <td> </td> |
| 643 | <td> </td> |
| 644 | <td> </td> |
| 645 | </tr> |
| 646 | <tr> |
| 647 | <td><b>M</b> <i>uuid</i></td> |
| 648 | <td> </td> |
| 649 | <td align=center><b>1+</b></td> |
| 650 | <td> </td> |
| 651 | <td> </td> |
| 652 | <td> </td> |
| 653 | <td> </td> |
| 654 | <td> </td> |
| @@ -660,10 +778,11 @@ | |
| 660 | <td> </td> |
| 661 | <td align=center><b>0-1</b></td> |
| 662 | <td> </td> |
| 663 | <td align=center><b>0-1</b></td> |
| 664 | <td align=center><b>0-1</b></td> |
| 665 | </tr> |
| 666 | <tr> |
| 667 | <td><b>P</b> <i>uuid ...</i></td> |
| 668 | <td align=center><b>0-1</b></td> |
| 669 | <td> </td> |
| @@ -670,14 +789,16 @@ | |
| 670 | <td> </td> |
| 671 | <td align=center><b>0-1</b></td> |
| 672 | <td> </td> |
| 673 | <td> </td> |
| 674 | <td align=center><b>0-1</b></td> |
| 675 | </tr> |
| 676 | <tr> |
| 677 | <td><b>Q</b> (<b>+</b>|<b>-</b>)<i>uuid</i> ?<i>uuid</i>?</td> |
| 678 | <td align=center><b>0+</b></td> |
| 679 | <td> </td> |
| 680 | <td> </td> |
| 681 | <td> </td> |
| 682 | <td> </td> |
| 683 | <td> </td> |
| @@ -690,19 +811,21 @@ | |
| 690 | <td> </td> |
| 691 | <td> </td> |
| 692 | <td> </td> |
| 693 | <td> </td> |
| 694 | <td> </td> |
| 695 | <tr> |
| 696 | <td><b>T</b> (<b>+</b>|<b>*</b>|<b>-</b>)<i>tagname</i> <i>uuid</i> ?<i>value</i>?</td> |
| 697 | <td align=center><b>0+</b></td> |
| 698 | <td> </td> |
| 699 | <td align=center><b>1+</b></td> |
| 700 | <td> </td> |
| 701 | <td> </td> |
| 702 | <td> </td> |
| 703 | <td align=center><b>0+</b></td> |
| 704 | </tr> |
| 705 | <tr> |
| 706 | <td><b>U</b> <i>username</i></td> |
| 707 | <td align=center><b>1</b></td> |
| 708 | <td> </td> |
| @@ -709,10 +832,11 @@ | |
| 709 | <td align=center><b>1</b></td> |
| 710 | <td align=center><b>1</b></td> |
| 711 | <td align=center><b>1</b></td> |
| 712 | <td align=center><b>0-1</b></td> |
| 713 | <td align=center><b>0-1</b></td> |
| 714 | </tr> |
| 715 | <tr> |
| 716 | <td><b>W</b> <i>size</i> <b>\n</b> <i>text</i> <b>\n</b></td> |
| 717 | <td> </td> |
| 718 | <td> </td> |
| @@ -719,13 +843,15 @@ | |
| 719 | <td> </td> |
| 720 | <td align=center><b>1</b></td> |
| 721 | <td> </td> |
| 722 | <td> </td> |
| 723 | <td align=center><b>1</b></td> |
| 724 | </tr> |
| 725 | <tr> |
| 726 | <td><b>Z</b> <i>md5sum</i></td> |
| 727 | <td align=center><b>1</b></td> |
| 728 | <td align=center><b>1</b></td> |
| 729 | <td align=center><b>1</b></td> |
| 730 | <td align=center><b>1</b></td> |
| 731 | <td align=center><b>1</b></td> |
| 732 |
| --- www/fileformat.wiki | |
| +++ www/fileformat.wiki | |
| @@ -71,13 +71,14 @@ | |
| 71 | <li> [#ctrl | Control Artifacts] </li> |
| 72 | <li> [#wikichng | Wiki Pages] </li> |
| 73 | <li> [#tktchng | Ticket Changes] </li> |
| 74 | <li> [#attachment | Attachments] </li> |
| 75 | <li> [#event | TechNotes] </li> |
| 76 | <li> [#forum | Forum Posts] </li> |
| 77 | </ul> |
| 78 | |
| 79 | These eight structural artifact types are described in subsections below. |
| 80 | |
| 81 | Structural artifacts are ASCII text. The artifact may be PGP clearsigned. |
| 82 | After removal of the PGP clearsign header and suffix (if any) a structural |
| 83 | artifact consists of one or more "cards" separated by a single newline |
| 84 | (ASCII: 0x0a) character. Each card begins with a single |
| @@ -525,10 +526,84 @@ | |
| 526 | technote. The format of the W card is exactly the same as for a |
| 527 | [#wikichng | wiki artifact]. |
| 528 | |
| 529 | The Z card is the required checksum over the rest of the artifact. |
| 530 | |
| 531 | <a name="forum"></a> |
| 532 | <h3>2.8 Forum Posts</h3> |
| 533 | |
| 534 | Forum posts are intended as a mechanism for users and developers to |
| 535 | discuss a project. Forum mosts are like messages on a mailing list. |
| 536 | |
| 537 | The following cards are allowed on an forum post artifact: |
| 538 | |
| 539 | <blockquote> |
| 540 | <b>D</b> <i>time-and-date-stamp</i><br /> |
| 541 | <b>G</b> <i>thread-root</i><br /> |
| 542 | <b>H</b> <i>thread-title</i><br /> |
| 543 | <b>I</b> <i>in-reply-to</i><br /> |
| 544 | <b>N</b> <i>mimetype</i><br /> |
| 545 | <b>P</b> <i>parent-artifact-id</i><br /> |
| 546 | <b>U</b> <i>user-name</i><br /> |
| 547 | <b>W</b> <i>size</i> <b>\n</b> <i>text</i> <b>\n</b><br /> |
| 548 | <b>Z</b> <i>checksum</i> |
| 549 | </blockquote> |
| 550 | |
| 551 | Every forum post must have either one I card and one G card |
| 552 | or one H card. |
| 553 | Forum posts are organized into topic threads. The initial |
| 554 | post for a thread (the root post) has an H card giving the title or |
| 555 | subject for that thread. The argument to the H card is a string |
| 556 | in the same format as a comment string in a C card. |
| 557 | All follow-up posts have an I card that |
| 558 | indicates which prior post in the same thread the current forum |
| 559 | post is replying to, and a G card specifying the root post for |
| 560 | the entire thread. The argument to G and I cards is the |
| 561 | artifact hash for the prior forum post to which the card refers. |
| 562 | |
| 563 | In theory, it is sufficient for follow-up posts to have only an |
| 564 | I card, since the G card value could be computed by following a |
| 565 | chain of I cards. However, the G card is required in order to |
| 566 | associate the artifact with a forum thread in the case where an |
| 567 | intermediate artifact in the I card chain is shunned or otherwise |
| 568 | becomes unreadable. |
| 569 | |
| 570 | A single D card is required to give the date and time when the |
| 571 | forum post was created. |
| 572 | |
| 573 | The optional N card specifies the mimetype of the text of the technote |
| 574 | that is contained in the W card. If the N card is omitted, then the |
| 575 | W card text mimetype is assumed to be text/x-fossil, which is the |
| 576 | Fossil wiki format. |
| 577 | |
| 578 | The optional P card specifies a prior forum post for which this |
| 579 | forum post is an edit. For display purposes, only the child post |
| 580 | is shown, though the historical post is retained as a record. |
| 581 | If P cards are used and there exist multiple versions of the same |
| 582 | forum post, then I cards for other artifacts refer to whichever |
| 583 | version of the post was current at the time the reply was made, |
| 584 | but G cards refer to the initial, unedited root post for the thread. |
| 585 | Thus, following the chain of I cards back to the root of the thread |
| 586 | may land on a different post than the one given in the G card. |
| 587 | However, following the chain of I cards back to the thread root, |
| 588 | then following P cards back to the initial version of the thread |
| 589 | root must give the same artifact as is provided by the G card, |
| 590 | otherwise the artifact containing the G card is considered invalid |
| 591 | and should be ignored. |
| 592 | |
| 593 | In general, P cards may contain multiple arguments, indicating a |
| 594 | merge. But since forum posts cannot be merged, the |
| 595 | P card of a forum post may only contain a single argument. |
| 596 | |
| 597 | The U card gives name of the user who entered the forum post. |
| 598 | |
| 599 | A single W card provides wiki text for the forum post. |
| 600 | The format of the W card is exactly the same as for a |
| 601 | [#wikichng | wiki artifact]. |
| 602 | |
| 603 | The Z card is the required checksum over the rest of the artifact. |
| 604 | |
| 605 | |
| 606 | <a name="summary"></a> |
| 607 | <h2>3.0 Card Summary</h2> |
| 608 | |
| 609 | The following table summarizes the various kinds of cards that appear |
| @@ -539,20 +614,21 @@ | |
| 614 | or more such cards are required. |
| 615 | |
| 616 | <table border=1 width="100%"> |
| 617 | <tr> |
| 618 | <th rowspan=2 valign=bottom>Card Format</th> |
| 619 | <th colspan=8>Used By</th> |
| 620 | </tr> |
| 621 | <tr> |
| 622 | <th>Manifest</th> |
| 623 | <th>Cluster</th> |
| 624 | <th>Control</th> |
| 625 | <th>Wiki</th> |
| 626 | <th>Ticket</th> |
| 627 | <th>Attachment</th> |
| 628 | <th>Technote</th> |
| 629 | <th>Forum</th> |
| 630 | </tr> |
| 631 | <tr> |
| 632 | <td><b>A</b> <i>filename</i> <i>target</i> ?<i>source</i>?</td> |
| 633 | <td> </td> |
| 634 | <td> </td> |
| @@ -559,36 +635,39 @@ | |
| 635 | <td> </td> |
| 636 | <td> </td> |
| 637 | <td> </td> |
| 638 | <td align=center><b>1</b></td> |
| 639 | <td> </td> |
| 640 | <td> </td> |
| 641 | </tr> |
| 642 | <tr> |
| 643 | <td><b>B</b> <i>baseline</i></td> |
| 644 | <td align=center><b>0-1</b></td> |
| 645 | <td> </td> |
| 646 | <td> </td> |
| 647 | <td> </td> |
| 648 | <td> </td> |
| 649 | <td> </td> |
| 650 | <td> </td> |
| 651 | <td> </td> |
| 652 | </tr> |
| 653 | <tr> |
| 654 | <td><b>C</b> <i>comment-text</i></td> |
| 655 | <td align=center><b>1</b></td> |
| 656 | <td> </td> |
| 657 | <td> </td> |
| 658 | <td> </td> |
| 659 | <td> </td> |
| 660 | <td align=center><b>0-1</b></td> |
| 661 | <td align=center><b>0-1</b></td> |
| 662 | <td> </td> |
| 663 | </tr> |
| 664 | <tr> |
| 665 | <td><b>D</b> <i>date-time-stamp</i></td> |
| 666 | <td align=center><b>1</b></td> |
| 667 | <td> </td> |
| 668 | <td align=center><b>1</b></td> |
| 669 | <td align=center><b>1</b></td> |
| 670 | <td align=center><b>1</b></td> |
| 671 | <td align=center><b>1</b></td> |
| 672 | <td align=center><b>1</b></td> |
| 673 | <td align=center><b>1</b></td> |
| @@ -600,10 +679,11 @@ | |
| 679 | <td> </td> |
| 680 | <td> </td> |
| 681 | <td> </td> |
| 682 | <td> </td> |
| 683 | <td align=center><b>1</b></td> |
| 684 | <td> </td> |
| 685 | </tr> |
| 686 | <tr> |
| 687 | <td><b>F</b> <i>filename</i> ?<i>uuid</i>? ?<i>permissions</i>? ?<i>oldname</i>?</td> |
| 688 | <td align=center><b>0+</b></td> |
| 689 | <td> </td> |
| @@ -610,18 +690,53 @@ | |
| 690 | <td> </td> |
| 691 | <td> </td> |
| 692 | <td> </td> |
| 693 | <td> </td> |
| 694 | <td> </td> |
| 695 | <td> </td> |
| 696 | </tr> |
| 697 | <tr> |
| 698 | <td><b>G</b> <i>thread-root</i></td> |
| 699 | <td> </td> |
| 700 | <td> </td> |
| 701 | <td> </td> |
| 702 | <td> </td> |
| 703 | <td> </td> |
| 704 | <td> </td> |
| 705 | <td> </td> |
| 706 | <td align=center><b>0-1</b></td> |
| 707 | </tr> |
| 708 | <tr> |
| 709 | <td><b>H</b> <i>thread-title</i></td> |
| 710 | <td> </td> |
| 711 | <td> </td> |
| 712 | <td> </td> |
| 713 | <td> </td> |
| 714 | <td> </td> |
| 715 | <td> </td> |
| 716 | <td> </td> |
| 717 | <td align=center><b>0-1</b></td> |
| 718 | </tr> |
| 719 | <tr> |
| 720 | <td><b>I</b> <i>in-reply-to</i></td> |
| 721 | <td> </td> |
| 722 | <td> </td> |
| 723 | <td> </td> |
| 724 | <td> </td> |
| 725 | <td> </td> |
| 726 | <td> </td> |
| 727 | <td> </td> |
| 728 | <td align=center><b>0-1</b></td> |
| 729 | </tr> |
| 730 | <tr> |
| 731 | <td><b>J</b> <i>name</i> ?<i>value</i>?</td> |
| 732 | <td> </td> |
| 733 | <td> </td> |
| 734 | <td> </td> |
| 735 | <td> </td> |
| 736 | <td align=center><b>1+</b></td> |
| 737 | <td> </td> |
| 738 | <td> </td> |
| 739 | <td> </td> |
| 740 | </tr> |
| 741 | <tr> |
| 742 | <td><b>K</b> <i>ticket-uuid</i></td> |
| @@ -630,10 +745,11 @@ | |
| 745 | <td> </td> |
| 746 | <td> </td> |
| 747 | <td align=center><b>1</b></td> |
| 748 | <td> </td> |
| 749 | <td> </td> |
| 750 | <td> </td> |
| 751 | </tr> |
| 752 | <tr> |
| 753 | <td><b>L</b> <i>wiki-title</i></td> |
| 754 | <td> </td> |
| 755 | <td> </td> |
| @@ -640,15 +756,17 @@ | |
| 756 | <td> </td> |
| 757 | <td align=center><b>1</b></td> |
| 758 | <td> </td> |
| 759 | <td> </td> |
| 760 | <td> </td> |
| 761 | <td> </td> |
| 762 | </tr> |
| 763 | <tr> |
| 764 | <td><b>M</b> <i>uuid</i></td> |
| 765 | <td> </td> |
| 766 | <td align=center><b>1+</b></td> |
| 767 | <td> </td> |
| 768 | <td> </td> |
| 769 | <td> </td> |
| 770 | <td> </td> |
| 771 | <td> </td> |
| 772 | <td> </td> |
| @@ -660,10 +778,11 @@ | |
| 778 | <td> </td> |
| 779 | <td align=center><b>0-1</b></td> |
| 780 | <td> </td> |
| 781 | <td align=center><b>0-1</b></td> |
| 782 | <td align=center><b>0-1</b></td> |
| 783 | <td align=center><b>0-1</b></td> |
| 784 | </tr> |
| 785 | <tr> |
| 786 | <td><b>P</b> <i>uuid ...</i></td> |
| 787 | <td align=center><b>0-1</b></td> |
| 788 | <td> </td> |
| @@ -670,14 +789,16 @@ | |
| 789 | <td> </td> |
| 790 | <td align=center><b>0-1</b></td> |
| 791 | <td> </td> |
| 792 | <td> </td> |
| 793 | <td align=center><b>0-1</b></td> |
| 794 | <td align=center><b>0-1</b></td> |
| 795 | </tr> |
| 796 | <tr> |
| 797 | <td><b>Q</b> (<b>+</b>|<b>-</b>)<i>uuid</i> ?<i>uuid</i>?</td> |
| 798 | <td align=center><b>0+</b></td> |
| 799 | <td> </td> |
| 800 | <td> </td> |
| 801 | <td> </td> |
| 802 | <td> </td> |
| 803 | <td> </td> |
| 804 | <td> </td> |
| @@ -690,19 +811,21 @@ | |
| 811 | <td> </td> |
| 812 | <td> </td> |
| 813 | <td> </td> |
| 814 | <td> </td> |
| 815 | <td> </td> |
| 816 | <td> </td> |
| 817 | <tr> |
| 818 | <td><b>T</b> (<b>+</b>|<b>*</b>|<b>-</b>)<i>tagname</i> <i>uuid</i> ?<i>value</i>?</td> |
| 819 | <td align=center><b>0+</b></td> |
| 820 | <td> </td> |
| 821 | <td align=center><b>1+</b></td> |
| 822 | <td> </td> |
| 823 | <td> </td> |
| 824 | <td> </td> |
| 825 | <td align=center><b>0+</b></td> |
| 826 | <td> </td> |
| 827 | </tr> |
| 828 | <tr> |
| 829 | <td><b>U</b> <i>username</i></td> |
| 830 | <td align=center><b>1</b></td> |
| 831 | <td> </td> |
| @@ -709,10 +832,11 @@ | |
| 832 | <td align=center><b>1</b></td> |
| 833 | <td align=center><b>1</b></td> |
| 834 | <td align=center><b>1</b></td> |
| 835 | <td align=center><b>0-1</b></td> |
| 836 | <td align=center><b>0-1</b></td> |
| 837 | <td align=center><b>1</b></td> |
| 838 | </tr> |
| 839 | <tr> |
| 840 | <td><b>W</b> <i>size</i> <b>\n</b> <i>text</i> <b>\n</b></td> |
| 841 | <td> </td> |
| 842 | <td> </td> |
| @@ -719,13 +843,15 @@ | |
| 843 | <td> </td> |
| 844 | <td align=center><b>1</b></td> |
| 845 | <td> </td> |
| 846 | <td> </td> |
| 847 | <td align=center><b>1</b></td> |
| 848 | <td align=center><b>1</b></td> |
| 849 | </tr> |
| 850 | <tr> |
| 851 | <td><b>Z</b> <i>md5sum</i></td> |
| 852 | <td align=center><b>1</b></td> |
| 853 | <td align=center><b>1</b></td> |
| 854 | <td align=center><b>1</b></td> |
| 855 | <td align=center><b>1</b></td> |
| 856 | <td align=center><b>1</b></td> |
| 857 | <td align=center><b>1</b></td> |
| 858 |