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.

drh 2018-08-07 15:12 trunk merge
Commit 99fcc43f5d57ee530dd94aabf799ce0d3dc74ce8edd97182b54bb7feb85a3e7b
+3 -8
--- src/attach.c
+++ src/attach.c
@@ -106,15 +106,13 @@
106106
}else{
107107
zUrlTail = mprintf("page=%t&file=%t", zTarget, zFilename);
108108
}
109109
@ <li><p>
110110
@ 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);
114112
@ <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>
116114
if( zComment ) while( fossil_isspace(zComment[0]) ) zComment++;
117115
if( zComment && zComment[0] ){
118116
@ %!W(zComment)<br />
119117
}
120118
if( zPage==0 && zTkt==0 && zTechNote==0 ){
@@ -564,14 +562,11 @@
564562
@ <tr><th>Artifact&nbsp;ID:</th>
565563
@ <td>%z(href("%R/artifact/%!S",zUuid))%s(zUuid)</a>
566564
if( g.perm.Setup ){
567565
@ (%d(rid))
568566
}
569
- modPending = moderation_pending(rid);
570
- if( modPending ){
571
- @ <span class="modpending">*** Awaiting Moderator Approval ***</span>
572
- }
567
+ modPending = moderation_pending_www(rid);
573568
if( zTktUuid ){
574569
@ <tr><th>Ticket:</th>
575570
@ <td>%z(href("%R/tktview/%s",zTktUuid))%s(zTktUuid)</a></td></tr>
576571
}
577572
if( zTNUuid ){
578573
--- 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&nbsp;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&nbsp;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
--- src/backoffice.c
+++ src/backoffice.c
@@ -78,18 +78,16 @@
7878
** otherwise taking a long time to complete. Set this when a user-visible
7979
** process might need to wait for backoffice to complete.
8080
*/
8181
static int backofficeNoDelay = 0;
8282
83
-
8483
/*
8584
** Disable the backoffice
8685
*/
8786
void backoffice_no_delay(void){
8887
backofficeNoDelay = 1;
8988
}
90
-
9189
9290
/*
9391
** Parse a unsigned 64-bit integer from a string. Return a pointer
9492
** to the character of z[] that occurs after the integer.
9593
*/
@@ -264,11 +262,11 @@
264262
getpid());
265263
}
266264
backoffice_work();
267265
break;
268266
}
269
- if( backofficeNoDelay ){
267
+ if( backofficeNoDelay || db_get_boolean("backoffice-nodelay",1) ){
270268
/* If the no-delay flag is set, exit immediately rather than queuing
271269
** up. Assume that some future request will come along and handle any
272270
** necessary backoffice work. */
273271
db_end_transaction(0);
274272
break;
275273
276274
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
--- 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 @@
344344
345345
/* After the webpage has been sent, do any useful background
346346
** processing.
347347
*/
348348
g.cgiOutput = 2;
349
- if( g.db!=0 && iReplyStatus==200 ){
349
+ if( g.db!=0 && iReplyStatus==200 && !g.fSshClient ){
350350
fclose(g.httpOut);
351351
#ifdef _WIN32
352352
g.httpOut = fossil_fopen("NUL", "wb");
353353
#else
354354
g.httpOut = fossil_fopen("/dev/null", "wb");
@@ -1120,23 +1120,27 @@
11201120
return zDefault;
11211121
}
11221122
11231123
/*
11241124
** 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
11261126
*/
11271127
char *cgi_parameter_trimmed(const char *zName, const char *zDefault){
11281128
const char *zIn;
1129
- char *zOut;
1130
- int i;
1129
+ char *zOut, c;
1130
+ int i, j;
11311131
zIn = cgi_parameter(zName, 0);
11321132
if( zIn==0 ) zIn = zDefault;
11331133
if( zIn==0 ) return 0;
11341134
while( fossil_isspace(zIn[0]) ) zIn++;
11351135
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;
11381142
return zOut;
11391143
}
11401144
11411145
/*
11421146
** Return true if the CGI parameter zName exists and is not equal to 0,
11431147
--- 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
+24 -25
--- src/db.c
+++ src/db.c
@@ -73,11 +73,10 @@
7373
*/
7474
static void db_err(const char *zFormat, ...){
7575
static int rcLooping = 0;
7676
va_list ap;
7777
char *z;
78
- int rc = 1;
7978
if( rcLooping ) exit(rcLooping);
8079
va_start(ap, zFormat);
8180
z = vmprintf(zFormat, ap);
8281
va_end(ap);
8382
#ifdef FOSSIL_ENABLE_JSON
@@ -87,26 +86,16 @@
8786
rc = 0 /* avoid HTTP 500 */;
8887
}
8988
}
9089
else
9190
#endif /* FOSSIL_ENABLE_JSON */
92
- if( g.xferPanic ){
91
+ if( g.xferPanic && g.cgiOutput==1 ){
9392
cgi_reset_content();
9493
@ 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);
10897
}
10998
11099
/*
111100
** All static variable that a used by only this file are gathered into
112101
** the following structure.
@@ -492,15 +481,10 @@
492481
db_check_result(rc);
493482
return rc;
494483
}
495484
int db_finalize(Stmt *pStmt){
496485
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;
502486
if( pStmt->pNext ){
503487
pStmt->pNext->pPrev = pStmt->pPrev;
504488
}
505489
if( pStmt->pPrev ){
506490
pStmt->pPrev->pNext = pStmt->pNext;
@@ -507,10 +491,15 @@
507491
}else if( db.pAllStmt==pStmt ){
508492
db.pAllStmt = pStmt->pNext;
509493
}
510494
pStmt->pNext = 0;
511495
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;
512501
return rc;
513502
}
514503
515504
/*
516505
** Return the rowid of the most recent insert
@@ -1012,10 +1001,14 @@
10121001
db_tolocal_function, 0, 0);
10131002
sqlite3_create_function(db, "fromLocal", 0, SQLITE_UTF8, 0,
10141003
db_fromlocal_function, 0, 0);
10151004
sqlite3_create_function(db, "hextoblob", 1, SQLITE_UTF8, 0,
10161005
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);
10171010
}
10181011
10191012
#if USE_SEE
10201013
/*
10211014
** This is a pointer to the saved database encryption key string.
@@ -1632,22 +1625,22 @@
16321625
if( file_access(zDbName, R_OK) || file_size(zDbName, ExtFILE)<1024 ){
16331626
if( file_access(zDbName, F_OK) ){
16341627
#ifdef FOSSIL_ENABLE_JSON
16351628
g.json.resultCode = FSL_JSON_E_DB_NOT_FOUND;
16361629
#endif
1637
- fossil_panic("repository does not exist or"
1630
+ fossil_fatal("repository does not exist or"
16381631
" is in an unreadable directory: %s", zDbName);
16391632
}else if( file_access(zDbName, R_OK) ){
16401633
#ifdef FOSSIL_ENABLE_JSON
16411634
g.json.resultCode = FSL_JSON_E_DENIED;
16421635
#endif
1643
- fossil_panic("read permission denied for repository %s", zDbName);
1636
+ fossil_fatal("read permission denied for repository %s", zDbName);
16441637
}else{
16451638
#ifdef FOSSIL_ENABLE_JSON
16461639
g.json.resultCode = FSL_JSON_E_DB_NOT_VALID;
16471640
#endif
1648
- fossil_panic("not a valid repository: %s", zDbName);
1641
+ fossil_fatal("not a valid repository: %s", zDbName);
16491642
}
16501643
}
16511644
g.zRepositoryName = mprintf("%s", zDbName);
16521645
db_open_or_attach(g.zRepositoryName, "repository");
16531646
g.repositoryOpen = 1;
@@ -1725,13 +1718,13 @@
17251718
if( (bFlags & OPEN_OK_NOT_FOUND)==0 ){
17261719
#ifdef FOSSIL_ENABLE_JSON
17271720
g.json.resultCode = FSL_JSON_E_DB_NOT_FOUND;
17281721
#endif
17291722
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");
17311724
}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");
17331726
}
17341727
}
17351728
}
17361729
17371730
/*
@@ -3033,10 +3026,16 @@
30333026
** SETTING: autosync-tries width=16 default=1
30343027
** If autosync is enabled setting this to a value greater
30353028
** than zero will cause autosync to try no more than this
30363029
** number of attempts if there is a sync failure.
30373030
*/
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
+*/
30383037
/*
30393038
** SETTING: binary-glob width=40 versionable block-text
30403039
** The VALUE of this setting is a comma or newline-separated list of
30413040
** GLOB patterns that should be treated as binary files
30423041
** for committing and merging purposes. Example: *.jpg
30433042
--- 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
--- src/default_css.txt
+++ src/default_css.txt
@@ -534,10 +534,14 @@
534534
pre.th1error {
535535
white-space: pre-wrap;
536536
word-wrap: break-word;
537537
color: red;
538538
}
539
+pre.textPlain {
540
+ white-space: pre-wrap;
541
+ word-wrap: break-word;
542
+}
539543
.statistics-report-graph-line {
540544
background-color: #446979;
541545
}
542546
.statistics-report-table-events th {
543547
padding: 0 1em 0 1em;
@@ -672,5 +676,46 @@
672676
}
673677
td.form_label {
674678
vertical-align: top;
675679
text-align: right;
676680
}
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
+}
677722
--- 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 @@
167167
zQ = &z[i+1];
168168
}else{
169169
zQ = &z[i];
170170
}
171171
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",
173173
zName, z, z);
174174
}
175175
z = zQ;
176176
while( *z ){
177177
char *zName = z;
178178
--- 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 @@
11
/*
2
-** Copyright (c) 2007 D. Richard Hipp
2
+** Copyright (c) 2018 D. Richard Hipp
33
**
44
** This program is free software; you can redistribute it and/or
55
** modify it under the terms of the Simplified BSD License (also
66
** known as the "2-Clause License" or "FreeBSD License".)
77
**
@@ -14,11 +14,16 @@
1414
** http://www.hwaci.com/drh/
1515
**
1616
*******************************************************************************
1717
**
1818
** 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
+*/
2025
#include "config.h"
2126
#include "email.h"
2227
#include <assert.h>
2328
#include <time.h>
2429
@@ -72,12 +77,13 @@
7277
@ -- Remaining characters determine the specific event. For example,
7378
@ -- 'c4413' means check-in with rid=4413.
7479
@ --
7580
@ CREATE TABLE repository.pending_alert(
7681
@ 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
7985
@ ) WITHOUT ROWID;
8086
@
8187
@ DROP TABLE IF EXISTS repository.email_bounce;
8288
@ -- Record bounced emails. If too many bounces are received within
8389
@ -- some defined time range, then cancel the subscription. Older
@@ -110,10 +116,15 @@
110116
){
111117
return; /* Don't create table for disabled email */
112118
}
113119
db_multi_exec(zEmailInit/*works-like:""*/);
114120
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
+ );
115126
}
116127
}
117128
118129
/*
119130
** Enable triggers that automatically populate the pending_alert
@@ -289,18 +300,10 @@
289300
@ <p>This is the email for the human administrator for the system.
290301
@ Abuse and trouble reports are send here.
291302
@ (Property: "email-admin")</p>
292303
@ <hr>
293304
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>
302305
@ <p><input type="submit" name="submit" value="Apply Changes" /></p>
303306
@ </div></form>
304307
db_end_transaction(0);
305308
style_footer();
306309
}
@@ -568,27 +571,27 @@
568571
return 0;
569572
}
570573
571574
/*
572575
** Make a copy of the input string up to but not including the
573
-** first ">" character.
576
+** first cTerm character.
574577
**
575578
** Verify that the string really that is to be copied really is a
576579
** valid email address. If it is not, then return NULL.
577580
**
578581
** This routine is more restrictive than necessary. It does not
579582
** allow comments, IP address, quoted strings, or certain uncommon
580583
** characters. The only non-alphanumerics allowed in the local
581584
** part are "_", "+", "-" and "+".
582585
*/
583
-char *email_copy_addr(const char *z){
586
+char *email_copy_addr(const char *z, char cTerm ){
584587
int i;
585588
int nAt = 0;
586589
int nDot = 0;
587590
char c;
588591
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++){
590593
if( fossil_isalnum(c) ){
591594
/* Alphanumerics are always ok */
592595
}else if( c=='@' ){
593596
if( nAt ) return 0; /* Only a single "@" allowed */
594597
if( i>64 ) return 0; /* Local part too big */
@@ -598,22 +601,22 @@
598601
if( z[i-1]=='.' ) return 0; /* Last char of local cannot be "." */
599602
if( z[i+1]=='.' || z[i+1]=='-' ){
600603
return 0; /* Domain cannot begin with "." or "-" */
601604
}
602605
}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 "-" */
604607
}else if( c=='.' ){
605608
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 . */
607610
nDot++;
608611
}else if( (c=='_' || c=='+') && nAt==0 ){
609612
/* _ and + are ok in the local part */
610613
}else{
611614
return 0; /* Anything else is an error */
612615
}
613616
}
614
- if( c!='>' ) return 0; /* Missing final ">" */
617
+ if( c!=cTerm ) return 0; /* Missing terminator */
615618
if( nAt==0 ) return 0; /* No "@" found anywhere */
616619
if( nDot==0 ) return 0; /* No "." in the domain */
617620
618621
/* If we reach this point, the email address is valid */
619622
return mprintf("%.*s", i, z);
@@ -631,11 +634,11 @@
631634
int i;
632635
633636
email_header_value(pMsg, "to", &v);
634637
z = blob_str(&v);
635638
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 ){
637640
azTo = fossil_realloc(azTo, sizeof(azTo[0])*(nTo+1) );
638641
azTo[nTo++] = zAddr;
639642
}
640643
}
641644
*pnTo = nTo;
@@ -691,16 +694,18 @@
691694
pOut = &all;
692695
}
693696
blob_append(pOut, blob_buffer(pHdr), blob_size(pHdr));
694697
blob_appendf(pOut, "From: <%s>\r\n", p->zFrom);
695698
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
+ }
702707
blob_add_final_newline(pBody);
703708
blob_appendf(pOut,"Content-Type: text/plain\r\n");
704709
#if 0
705710
blob_appendf(pOut, "Content-Transfer-Encoding: base64\r\n\r\n");
706711
append_base64(pOut, pBody);
@@ -752,24 +757,10 @@
752757
fossil_print("%s", blob_str(&all));
753758
}
754759
blob_reset(&all);
755760
}
756761
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
-
771762
/*
772763
** SETTING: email-send-method width=5 default=off
773764
** Determine the method used to send email. Allowed values are
774765
** "off", "relay", "pipe", "dir", "db", and "stdout". The "off" value
775766
** means no email is ever sent. The "relay" value means emails are sent
@@ -801,16 +792,10 @@
801792
/*
802793
** SETTING: email-self width=40
803794
** This is the email address for the repository. Outbound emails add
804795
** this email address as the "From:" field.
805796
*/
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
-*/
812797
/*
813798
** SETTING: email-send-relayhost width=40
814799
** This is the hostname and TCP port to which output email messages
815800
** are sent when email-send-method is "relay". There should be an
816801
** SMTP server configured as a Mail Submission Agent listening on the
@@ -817,80 +802,72 @@
817802
** designated host and port and all times.
818803
*/
819804
820805
821806
/*
822
-** COMMAND: email
807
+** COMMAND: alerts
823808
**
824
-** Usage: %fossil email SUBCOMMAND ARGS...
809
+** Usage: %fossil alerts SUBCOMMAND ARGS...
825810
**
826811
** Subcommands:
827812
**
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.
829820
** Some installations may want to do this via
830821
** a cron-job to make sure alerts are sent
831822
** in a timely manner.
832823
** Options:
833824
**
834825
** --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
846837
** 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:
849840
**
850841
** --body FILENAME
851842
** --smtp-trace
852843
** --stdout
853844
** --subject|-S SUBJECT
854845
**
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
-**
860846
** unsubscribe EMAIL Remove a single subscriber with the given EMAIL.
861847
*/
862848
void email_cmd(void){
863849
const char *zCmd;
864850
int nCmd;
865851
db_find_and_open_repository(0, 0);
866852
email_schema(0);
867853
zCmd = g.argc>=3 ? g.argv[2] : "x";
868854
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);
892869
}else
893870
if( strncmp(zCmd, "reset", nCmd)==0 ){
894871
int c;
895872
int bForce = find_option("force","f",0)!=0;
896873
verify_all_options();
@@ -918,43 +895,17 @@
918895
);
919896
email_schema(0);
920897
}
921898
}else
922899
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
+ }
931905
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);
956907
}else
957908
if( strncmp(zCmd, "settings", nCmd)==0 ){
958909
int isGlobal = find_option("global",0,0)!=0;
959910
int nSetting;
960911
const Setting *pSetting = setting_info(&nSetting);
@@ -974,10 +925,32 @@
974925
for(; nSetting>0; nSetting--, pSetting++ ){
975926
if( strncmp(pSetting->name,"email-",6)!=0 ) continue;
976927
print_setting(pSetting);
977928
}
978929
}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
979952
if( strncmp(zCmd, "subscribers", nCmd)==0 ){
980953
Stmt q;
981954
verify_all_options();
982955
if( g.argc!=3 && g.argc!=4 ) usage("subscribers [PATTERN]");
983956
if( g.argc==4 ){
@@ -995,19 +968,54 @@
995968
}
996969
while( db_step(&q)==SQLITE_ROW ){
997970
fossil_print("%s\n", db_column_text(&q, 0));
998971
}
999972
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);
10001007
}else
10011008
if( strncmp(zCmd, "unsubscribe", nCmd)==0 ){
10021009
verify_all_options();
10031010
if( g.argc!=4 ) usage("unsubscribe EMAIL");
10041011
db_multi_exec(
10051012
"DELETE FROM subscriber WHERE semail=%Q", g.argv[3]);
10061013
}else
10071014
{
1008
- usage("exec|inbound|reset|send|setting|subscribers|unsubscribe");
1015
+ usage("pending|reset|send|setting|status|"
1016
+ "subscribers|test-message|unsubscribe");
10091017
}
10101018
}
10111019
10121020
/*
10131021
** Do error checking on a submitted subscription form. Return TRUE
@@ -1784,11 +1792,13 @@
17841792
/*
17851793
** A single event that might appear in an alert is recorded as an
17861794
** instance of the following object.
17871795
*/
17881796
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 */
17901800
Blob txt; /* Text description to appear in an alert */
17911801
EmailEvent *pNext; /* Next in chronological order */
17921802
};
17931803
#endif
17941804
@@ -1797,10 +1807,11 @@
17971807
*/
17981808
void email_free_eventlist(EmailEvent *p){
17991809
while( p ){
18001810
EmailEvent *pNext = p->pNext;
18011811
blob_reset(&p->txt);
1812
+ blob_reset(&p->hdr);
18021813
fossil_free(p);
18031814
p = pNext;
18041815
}
18051816
}
18061817
@@ -1807,68 +1818,152 @@
18071818
/*
18081819
** Compute and return a linked list of EmailEvent objects
18091820
** corresponding to the current content of the temp.wantalert
18101821
** table which should be defined as follows:
18111822
**
1812
-** CREATE TEMP TABLE wantalert(eventId TEXT);
1823
+** CREATE TEMP TABLE wantalert(eventId TEXT, needMod BOOLEAN);
18131824
*/
1814
-EmailEvent *email_compute_event_text(int *pnEvent){
1825
+EmailEvent *email_compute_event_text(int *pnEvent, int doDigest){
18151826
Stmt q;
18161827
EmailEvent *p;
18171828
EmailEvent anchor;
18181829
EmailEvent *pLast;
18191830
const char *zUrl = db_get("email-url","http://localhost:8080");
1831
+ const char *zFrom;
1832
+ const char *zSub;
1833
+
18201834
1835
+ /* First do non-forum post events */
18211836
db_prepare(&q,
18221837
"SELECT"
1823
- " blob.uuid," /* 0 */
1824
- " datetime(event.mtime)," /* 1 */
1838
+ " blob.uuid," /* 0 */
1839
+ " datetime(event.mtime)," /* 1 */
18251840
" coalesce(ecomment,comment)"
18261841
" || ' (user: ' || coalesce(euser,user,'?')"
18271842
" || (SELECT case when length(x)>0 then ' tags: ' || x else '' end"
18281843
" FROM (SELECT group_concat(substr(tagname,5), ', ') AS x"
18291844
" FROM tag, tagxref"
18301845
" WHERE tagname GLOB 'sym-*' AND tag.tagid=tagxref.tagid"
18311846
" 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"
18391851
" WHERE blob.rid=event.objid"
1840
- " AND tag.tagname='branch'"
18411852
" 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
18431856
);
18441857
memset(&anchor, 0, sizeof(anchor));
18451858
pLast = &anchor;
18461859
*pnEvent = 0;
18471860
while( db_step(&q)==SQLITE_ROW ){
18481861
const char *zType = "";
18491862
p = fossil_malloc( sizeof(EmailEvent) );
18501863
pLast->pNext = p;
18511864
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);
18531867
p->pNext = 0;
18541868
switch( p->type ){
18551869
case 'c': zType = "Check-In"; break;
1870
+ case 'f': zType = "Forum post"; break;
18561871
case 't': zType = "Wiki Edit"; break;
18571872
case 'w': zType = "Ticket Change"; break;
18581873
}
1874
+ blob_init(&p->hdr, 0, 0);
18591875
blob_init(&p->txt, 0, 0);
18601876
blob_appendf(&p->txt,"== %s %s ==\n%s\n%s/info/%.20s\n",
18611877
db_column_text(&q,1),
18621878
zType,
18631879
db_column_text(&q,2),
18641880
zUrl,
18651881
db_column_text(&q,0)
18661882
);
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);
18671961
(*pnEvent)++;
18681962
}
18691963
db_finalize(&q);
1964
+
18701965
return anchor.pNext;
18711966
}
18721967
18731968
/*
18741969
** Put a header on an alert email
@@ -1900,34 +1995,50 @@
19001995
** command line, generate text for all events named in the
19011996
** pending_alert table.
19021997
**
19031998
** This command is intended for testing and debugging the logic
19041999
** that generates email alert text.
2000
+**
2001
+** Options:
2002
+**
2003
+** --digest Generate digest alert text
2004
+** --needmod Assume all events are pending moderator approval
19052005
*/
19062006
void test_alert_cmd(void){
19072007
Blob out;
19082008
int nEvent;
2009
+ int needMod;
2010
+ int doDigest;
19092011
EmailEvent *pEvent, *p;
19102012
2013
+ doDigest = find_option("digest",0,0)!=0;
2014
+ needMod = find_option("needmod",0,0)!=0;
19112015
db_find_and_open_repository(0, 0);
19122016
verify_all_options();
19132017
db_begin_transaction();
19142018
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)");
19162020
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);
19182024
}else{
19192025
int i;
19202026
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);
19222029
}
19232030
}
19242031
blob_init(&out, 0, 0);
19252032
email_header(&out);
1926
- pEvent = email_compute_event_text(&nEvent);
2033
+ pEvent = email_compute_event_text(&nEvent, doDigest);
19272034
for(p=pEvent; p; p=p->pNext){
19282035
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
+ }
19292040
blob_append(&out, blob_buffer(&p->txt), blob_size(&p->txt));
19302041
}
19312042
email_free_eventlist(pEvent);
19322043
email_footer(&out);
19332044
fossil_print("%s", blob_str(&out));
@@ -1936,38 +2047,54 @@
19362047
}
19372048
19382049
/*
19392050
** COMMAND: test-add-alerts
19402051
**
1941
-** Usage: %fossil test-add-alerts [--backoffice] EVENTID ...
2052
+** Usage: %fossil test-add-alerts [OPTIONS] EVENTID ...
19422053
**
19432054
** Add one or more events to the pending_alert queue. Use this
19442055
** command during testing to force email notifications for specific
19452056
** events.
19462057
**
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
19492060
** integer that references the EVENT.OBJID value for the event.
19502061
** Run /timeline?showid to see these OBJID values.
19512062
**
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
19552074
*/
19562075
void test_add_alert_cmd(void){
19572076
int i;
19582077
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
+ }
19592086
db_find_and_open_repository(0, 0);
19602087
verify_all_options();
19612088
db_begin_write();
19622089
email_schema(0);
19632090
for(i=2; i<g.argc; i++){
19642091
db_multi_exec("REPLACE INTO pending_alert(eventId) VALUES(%Q)", g.argv[i]);
19652092
}
19662093
db_end_transaction(0);
19672094
if( doAuto ){
1968
- email_backoffice(SENDALERT_TRACE);
2095
+ email_backoffice(SENDALERT_TRACE|mFlags);
19692096
}
19702097
}
19712098
19722099
#if INTERFACE
19732100
/*
@@ -1979,11 +2106,35 @@
19792106
#define SENDALERT_TRACE 0x0008 /* Trace operation for debugging */
19802107
19812108
#endif /* INTERFACE */
19822109
19832110
/*
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.
19852136
*/
19862137
void email_send_alerts(u32 flags){
19872138
EmailEvent *pEvents, *p;
19882139
int nEvent = 0;
19892140
Stmt q;
@@ -1996,10 +2147,11 @@
19962147
EmailSender *pSender = 0;
19972148
u32 senderFlags = 0;
19982149
19992150
if( g.fSqlTrace ) fossil_trace("-- BEGIN email_send_alerts(%u)\n", flags);
20002151
db_begin_transaction();
2152
+ email_schema(0);
20012153
if( !email_enabled() ) goto send_alerts_done;
20022154
zUrl = db_get("email-url",0);
20032155
if( zUrl==0 ) goto send_alerts_done;
20042156
zRepoName = db_get("email-subname",0);
20052157
if( zRepoName==0 ) goto send_alerts_done;
@@ -2009,57 +2161,111 @@
20092161
senderFlags |= EMAIL_TRACE;
20102162
}
20112163
pSender = email_sender_new(zDest, senderFlags);
20122164
db_multi_exec(
20132165
"DROP TABLE IF EXISTS temp.wantalert;"
2014
- "CREATE TEMP TABLE wantalert(eventId TEXT);"
2166
+ "CREATE TEMP TABLE wantalert(eventId TEXT, needMod BOOLEAN, sentMod);"
20152167
);
20162168
if( flags & SENDALERT_DIGEST ){
2169
+ /* Unmoderated changes are never sent as part of a digest */
20172170
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"
20192174
" WHERE sentDigest IS FALSE"
2175
+ " AND NOT EXISTS(SELECT 1 FROM private WHERE rid=substr(eventid,2));"
20202176
);
20212177
zDigest = "true";
20222178
}else{
2179
+ /* Immediate alerts might include events that are subject to
2180
+ ** moderator approval */
20232181
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;"
20262189
);
20272190
}
2028
- pEvents = email_compute_event_text(&nEvent);
2191
+ pEvents = email_compute_event_text(&nEvent, (flags & SENDALERT_DIGEST)!=0);
20292192
if( nEvent==0 ) goto send_alerts_done;
20302193
blob_init(&hdr, 0, 0);
20312194
blob_init(&body, 0, 0);
20322195
db_prepare(&q,
20332196
"SELECT"
20342197
" hex(subscriberCode)," /* 0 */
20352198
" semail," /* 1 */
2036
- " ssub" /* 2 */
2199
+ " ssub," /* 2 */
2200
+ " fullcap((SELECT cap FROM user WHERE login=suname))" /* 3 */
20372201
" FROM subscriber"
20382202
" WHERE sverified AND NOT sdonotcall"
20392203
" AND sdigest IS %s",
20402204
zDigest/*safe-for-%s*/
20412205
);
20422206
while( db_step(&q)==SQLITE_ROW ){
20432207
const char *zCode = db_column_text(&q, 0);
20442208
const char *zSub = db_column_text(&q, 2);
20452209
const char *zEmail = db_column_text(&q, 1);
2210
+ const char *zCap = db_column_text(&q, 3);
20462211
int nHit = 0;
20472212
for(p=pEvents; p; p=p->pNext){
20482213
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
+ }
20612267
}
20622268
if( nHit==0 ) continue;
20632269
blob_appendf(&body,"\n-- \nSubscription info: %s/alerts/%s\n",
20642270
zUrl, zCode);
20652271
email_send(pSender,&hdr,&body);
@@ -2070,15 +2276,24 @@
20702276
blob_reset(&body);
20712277
db_finalize(&q);
20722278
email_free_eventlist(pEvents);
20732279
if( (flags & SENDALERT_PRESERVE)==0 ){
20742280
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
+ );
20762286
}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
+ );
20782294
}
2079
- db_multi_exec("DELETE FROM pending_alert WHERE sentDigest AND sentSep");
20802295
}
20812296
send_alerts_done:
20822297
email_sender_free(pSender);
20832298
if( g.fSqlTrace ) fossil_trace("-- END email_send_alerts(%u)\n", flags);
20842299
db_end_transaction(0);
20852300
--- 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 @@
1919
*/
2020
#include "config.h"
2121
#include <assert.h>
2222
#include "forum.h"
2323
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>
166896
style_footer();
167897
}
168898
169899
/*
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>
354949
style_footer();
355950
}
356951
357952
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 @@
935935
@ <tr><th>Artifact&nbsp;ID:</th>
936936
@ <td>%z(href("%R/artifact/%!S",zUuid))%s(zUuid)</a>
937937
if( g.perm.Setup ){
938938
@ (%d(rid))
939939
}
940
- modPending = moderation_pending(rid);
941
- if( modPending ){
942
- @ <span class="modpending">*** Awaiting Moderator Approval ***</span>
943
- }
940
+ modPending = moderation_pending_www(rid);
944941
@ </td></tr>
945942
@ <tr><th>Page&nbsp;Name:</th><td>%h(pWiki->zWikiTitle)</td></tr>
946943
@ <tr><th>Date:</th><td>
947944
hyperlink_to_date(zDate, "</td></tr>");
948945
@ <tr><th>Original&nbsp;User:</th><td>
@@ -978,25 +975,10 @@
978975
@ <div class="section">Content</div>
979976
blob_init(&wiki, pWiki->zWiki, -1);
980977
wiki_render_by_mimetype(&wiki, pWiki->zMimetype);
981978
blob_reset(&wiki);
982979
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>
998980
style_footer();
999981
}
1000982
1001983
/*
1002984
** Find an check-in based on query parameter zParam and parse its
@@ -1246,10 +1228,11 @@
12461228
#define OBJTYPE_ATTACHMENT 0x0010
12471229
#define OBJTYPE_EVENT 0x0020
12481230
#define OBJTYPE_TAG 0x0040
12491231
#define OBJTYPE_SYMLINK 0x0080
12501232
#define OBJTYPE_EXE 0x0100
1233
+#define OBJTYPE_FORUM 0x0200
12511234
12521235
/*
12531236
** Possible flags for the second parameter to
12541237
** object_description()
12551238
*/
@@ -1438,10 +1421,13 @@
14381421
objType |= OBJTYPE_EVENT;
14391422
hyperlink_to_event_tagid(db_column_int(&q, 5));
14401423
}else{
14411424
@ Attachment to technote
14421425
}
1426
+ }else if( zType[0]=='f' ){
1427
+ objType |= OBJTYPE_FORUM;
1428
+ @ Forum post
14431429
}else{
14441430
@ Tag referencing
14451431
}
14461432
if( zType[0]!='e' || eventTagId == 0){
14471433
hyperlink_to_uuid(zUuid);
@@ -2248,14 +2234,11 @@
22482234
@ <tr><th>Artifact&nbsp;ID:</th>
22492235
@ <td>%z(href("%R/artifact/%!S",zUuid))%s(zUuid)</a>
22502236
if( g.perm.Setup ){
22512237
@ (%d(rid))
22522238
}
2253
- modPending = moderation_pending(rid);
2254
- if( modPending ){
2255
- @ <span class="modpending">*** Awaiting Moderator Approval ***</span>
2256
- }
2239
+ modPending = moderation_pending_www(rid);
22572240
@ <tr><th>Ticket:</th>
22582241
@ <td>%z(href("%R/tktview/%s",zTktName))%s(zTktName)</a>
22592242
if( zTktTitle ){
22602243
@<br />%h(zTktTitle)
22612244
}
@@ -2364,10 +2347,15 @@
23642347
if( db_exists("SELECT 1 FROM plink WHERE pid=%d", rid) ){
23652348
ci_page();
23662349
}else
23672350
if( db_exists("SELECT 1 FROM attachment WHERE attachid=%d", rid) ){
23682351
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();
23692357
}else
23702358
{
23712359
artifact_page();
23722360
}
23732361
}
23742362
--- src/info.c
+++ src/info.c
@@ -935,14 +935,11 @@
935 @ <tr><th>Artifact&nbsp;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&nbsp;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&nbsp;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&nbsp;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&nbsp;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&nbsp;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&nbsp;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&nbsp;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 @@
470470
zPattern = mprintf("%s/login*", g.zBaseURL);
471471
rc = sqlite3_strglob(zPattern, zReferer)==0;
472472
fossil_free(zPattern);
473473
return rc;
474474
}
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
+}
475493
476494
/*
477495
** There used to be a page named "my" that was designed to show information
478496
** about a specific user. The "my" page was linked from the "Logged in as USER"
479497
** line on the title bar. The "my" page was never completed so it is now
@@ -498,10 +516,11 @@
498516
char *zErrMsg = "";
499517
int uid; /* User id logged in user */
500518
char *zSha1Pw;
501519
const char *zIpAddr; /* IP address of requestor */
502520
const char *zReferer;
521
+ int noAnon = P("noanon")!=0;
503522
504523
login_check_credentials();
505524
if( login_wants_https_redirect() ){
506525
const char *zQS = P("QUERY_STRING");
507526
if( P("redir")!=0 ){
@@ -536,10 +555,16 @@
536555
if( P("out") ){
537556
login_clear_login_data();
538557
redirect_to_g();
539558
return;
540559
}
560
+
561
+ /* Redirect for create-new-account requests */
562
+ if( P("self") ){
563
+ cgi_redirectf("%R/register");
564
+ return;
565
+ }
541566
542567
/* Deal with password-change requests */
543568
if( g.perm.Password && zPasswd
544569
&& (zNew1 = P("n1"))!=0 && (zNew2 = P("n2"))!=0
545570
){
@@ -631,11 +656,11 @@
631656
}
632657
}
633658
style_header("Login/Logout");
634659
style_adunit_config(ADUNIT_OFF);
635660
@ %s(zErrMsg)
636
- if( zGoto ){
661
+ if( zGoto && !noAnon ){
637662
char *zAbbrev = fossil_strdup(zGoto);
638663
int i;
639664
for(i=0; zAbbrev[i] && zAbbrev[i]!='?'; i++){}
640665
zAbbrev[i] = 0;
641666
if( g.zLogin ){
@@ -675,11 +700,11 @@
675700
@ <td><input type="text" id="u" name="u" value="anonymous" size="30"></td>
676701
}else{
677702
@ <td><input type="text" id="u" name="u" value="" size="30" /></td>
678703
}
679704
if( P("HTTPS")==0 ){
680
- @ <td width="15"><td rowspan="3">
705
+ @ <td width="15"><td rowspan="2">
681706
@ <p class='securityWarning'>
682707
@ Warning: Your password will be sent in the clear over an
683708
@ unencrypted connection.
684709
if( g.sslNotAvailable ){
685710
@ No encrypted connection is available on this server.
@@ -690,28 +715,33 @@
690715
@ </p>
691716
}
692717
@ </tr>
693718
@ <tr>
694719
@ <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>
696721
@ </tr>
697722
if( g.zLogin==0 && (anonFlag || zGoto==0) ){
698723
zAnonPw = db_text(0, "SELECT pw FROM user"
699724
" WHERE login='anonymous'"
700725
" AND cap!=''");
701726
}
702727
@ <tr>
703728
@ <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">&larr; Pressing this button grants\
731
+ @ permission to store a cookie
705732
@ </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
+ @ &larr; Don't have a login? Click this button to create one.
739
+ @ </tr>
740
+ }
706741
@ </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 ){
713743
unsigned int uSeed = captcha_seed();
714744
const char *zDecoded = captcha_decode(uSeed);
715745
int bAutoCaptcha = db_get_boolean("auto-captcha", 0);
716746
char *zCaptcha = captcha_render(zDecoded);
717747
@@ -1459,82 +1489,81 @@
14591489
**
14601490
** Page to allow users to self-register. The "self-register" setting
14611491
** must be enabled for this page to operate.
14621492
*/
14631493
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;
14651496
unsigned int uSeed;
14661497
const char *zDecoded;
14671498
char *zCaptcha;
1499
+ int iErrLine = -1;
1500
+ const char *zErr;
14681501
if( !db_get_boolean("self-register", 0) ){
14691502
style_header("Registration not possible");
14701503
@ <p>This project does not allow user self-registration. Please contact the
14711504
@ project administrator to obtain an account.</p>
14721505
style_footer();
14731506
return;
14741507
}
14751508
14761509
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","");
14831515
14841516
/* 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();
15361565
}
15371566
15381567
/* Prepare the captcha. */
15391568
uSeed = captcha_seed();
15401569
zDecoded = captcha_decode(uSeed);
@@ -1543,31 +1572,53 @@
15431572
/* Print out the registration form. */
15441573
form_begin(0, "%R/register");
15451574
if( P("g") ){
15461575
@ <input type="hidden" name="g" value="%h(P("g"))" />
15471576
}
1548
- @ <p><input type="hidden" name="cs" value="%u(uSeed)" />
1577
+ @ <p><input type="hidden" name="captchaseed" value="%u(uSeed)" />
15491578
@ <table class="login_out">
15501579
@ <tr>
15511580
@ <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'>&larr; %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'>&larr; %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'>&larr; %h(zErr)</span></td>
1598
+ }
15531599
@ </tr>
15541600
@ <tr>
15551601
@ <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'>&larr; %h(zErr)</span></td>
1605
+ }
15571606
@ </tr>
15581607
@ <tr>
15591608
@ <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'>&larr; %h(zErr)</span></td>
1612
+ }
15651613
@ </tr>
15661614
@ <tr>
15671615
@ <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'>&larr; %h(zErr)</span></td>
1619
+ }
15691620
@ </tr>
15701621
@ <tr><td></td>
15711622
@ <td><input type="submit" name="new" value="Register" /></td></tr>
15721623
@ </table>
15731624
@ <div class="captcha"><table class="captcha"><tr><td><pre>
15741625
--- 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">&larr; 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 @ &larr; 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'>&larr; %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'>&larr; %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'>&larr; %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'>&larr; %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'>&larr; %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'>&larr; %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 @@
9090
char WrUnver; /* y: can push unversioned content */
9191
char RdForum; /* 2: Read forum posts */
9292
char WrForum; /* 3: Create new forum posts */
9393
char WrTForum; /* 4: Post to forums not subject to moderation */
9494
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 */
9696
char EmailAlert; /* 7: Sign up for email notifications */
9797
char Announce; /* A: Send announcements */
9898
char Debug; /* D: show extra Fossil debugging features */
9999
};
100100
@@ -640,11 +640,11 @@
640640
if( g.zVfsName ){
641641
sqlite3_vfs *pVfs = sqlite3_vfs_find(g.zVfsName);
642642
if( pVfs ){
643643
sqlite3_vfs_register(pVfs, 1);
644644
}else{
645
- fossil_panic("no such VFS: \"%s\"", g.zVfsName);
645
+ fossil_fatal("no such VFS: \"%s\"", g.zVfsName);
646646
}
647647
}
648648
if( fossil_getenv("GATEWAY_INTERFACE")!=0 && !find_option("nocgi", 0, 0)){
649649
zCmdName = "cgi";
650650
g.isHTTP = 1;
@@ -691,11 +691,11 @@
691691
g.zErrlog = find_option("errorlog", 0, 1);
692692
fossil_init_flags_from_options();
693693
if( find_option("utc",0,0) ) g.fTimeFormat = 1;
694694
if( find_option("localtime",0,0) ) g.fTimeFormat = 2;
695695
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);
697697
}
698698
if( find_option("help",0,0)!=0 ){
699699
/* If --help is found anywhere on the command line, translate the command
700700
* to "fossil help cmdname" where "cmdname" is the first argument that
701701
* does not begin with a "-" character. If all arguments start with "-",
@@ -756,11 +756,11 @@
756756
rc = TH_OK;
757757
}
758758
if( rc==TH_OK || rc==TH_RETURN || rc==TH_CONTINUE ){
759759
if( rc==TH_OK || rc==TH_RETURN ){
760760
#endif
761
- fossil_panic("%s: unknown command: %s\n"
761
+ fossil_fatal("%s: unknown command: %s\n"
762762
"%s: use \"help\" for more information",
763763
g.argv[0], zCmdName, g.argv[0]);
764764
#ifdef FOSSIL_ENABLE_TH1_HOOKS
765765
}
766766
if( !g.isHTTP && !g.fNoThHook && (rc==TH_OK || rc==TH_CONTINUE) ){
@@ -821,11 +821,11 @@
821821
822822
/*
823823
** Print a usage comment and quit
824824
*/
825825
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);
827827
}
828828
829829
/*
830830
** Remove n elements from g.argv beginning with the i-th element.
831831
*/
@@ -939,11 +939,11 @@
939939
*/
940940
void verify_all_options(void){
941941
int i;
942942
for(i=1; i<g.argc; i++){
943943
if( g.argv[i][0]=='-' && g.argv[i][1]!=0 ){
944
- fossil_panic(
944
+ fossil_fatal(
945945
"unrecognized command-line option, or missing argument: %s",
946946
g.argv[i]);
947947
}
948948
}
949949
}
@@ -1158,11 +1158,11 @@
11581158
g.zHttpsURL = mprintf("https://%s", &g.zTop[7]);
11591159
}else if( strncmp(g.zTop, "https://", 8)==0 ){
11601160
/* it is already HTTPS, use it. */
11611161
g.zHttpsURL = mprintf("%s", g.zTop);
11621162
}else{
1163
- fossil_panic("argument to --baseurl should be 'http://host/path'"
1163
+ fossil_fatal("argument to --baseurl should be 'http://host/path'"
11641164
" or 'https://host/path'");
11651165
}
11661166
for(i=n=0; (c = g.zTop[i])!=0; i++){
11671167
if( c=='/' ){
11681168
n++;
@@ -1171,11 +1171,11 @@
11711171
break;
11721172
}
11731173
}
11741174
}
11751175
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'"
11771177
" or 'https://host/path'");
11781178
}
11791179
if( g.zTop[1]==0 ) g.zTop++;
11801180
}else{
11811181
zHost = PD("HTTP_HOST","");
@@ -1260,16 +1260,16 @@
12601260
}
12611261
zRepo = &zDir[i];
12621262
}
12631263
}
12641264
if( stat(zRepo, &sStat)!=0 ){
1265
- fossil_panic("cannot stat() repository: %s", zRepo);
1265
+ fossil_fatal("cannot stat() repository: %s", zRepo);
12661266
}
12671267
i = setgid(sStat.st_gid);
12681268
i = i || setuid(sStat.st_uid);
12691269
if(i){
1270
- fossil_panic("setgid/uid() failed with errno %d", errno);
1270
+ fossil_fatal("setgid/uid() failed with errno %d", errno);
12711271
}
12721272
if( g.db==0 && file_isfile(zRepo, ExtFILE) ){
12731273
db_open_repository(zRepo);
12741274
}
12751275
}
@@ -2251,11 +2251,11 @@
22512251
){
22522252
unsigned int nSize = 0;
22532253
if( sscanf(zPidKey, "%lu:%p:%u", pProcessId, ppAddress, &nSize)==3 ){
22542254
*pnSize = (SIZE_T)nSize;
22552255
}else{
2256
- fossil_panic("failed to parse pid key");
2256
+ fossil_fatal("failed to parse pid key");
22572257
}
22582258
}
22592259
#endif
22602260
22612261
/*
@@ -2702,11 +2702,11 @@
27022702
}
27032703
if( g.repositoryOpen ) flags |= HTTP_SERVER_HAD_REPOSITORY;
27042704
if( g.localOpen ) flags |= HTTP_SERVER_HAD_CHECKOUT;
27052705
db_close(1);
27062706
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);
27082708
}
27092709
if( zMaxLatency ){
27102710
signal(SIGALRM, sigalrm_handler);
27112711
alarm(atoi(zMaxLatency));
27122712
}
@@ -2806,10 +2806,12 @@
28062806
** case=1 Issue a fossil_warning() while generating the page.
28072807
** case=2 Extra db_begin_transaction()
28082808
** case=3 Extra db_end_transaction()
28092809
** case=4 Error during SQL processing
28102810
** case=5 Call the segfault handler
2811
+** case=6 Call webpage_assert()
2812
+** case=7 Call webpage_error()
28112813
*/
28122814
void test_warning_page(void){
28132815
int iCase = atoi(PD("case","0"));
28142816
int i;
28152817
login_check_credentials();
@@ -2823,11 +2825,11 @@
28232825
@ <p>Generate a message to the <a href="%R/errorlog">error log</a>
28242826
@ by clicking on one of the following cases:
28252827
}else{
28262828
@ <p>This is the test page for case=%d(iCase). All possible cases:
28272829
}
2828
- for(i=1; i<=5; i++){
2830
+ for(i=1; i<=7; i++){
28292831
@ <a href='./test-warning?case=%d(i)'>[%d(i)]</a>
28302832
}
28312833
@ </p>
28322834
@ <p><ol>
28332835
@ <li value='1'> Call fossil_warning()
@@ -2852,9 +2854,18 @@
28522854
}
28532855
@ <li value='5'> simulate segfault handling
28542856
if( iCase==5 ){
28552857
sigsegv_handler(0);
28562858
}
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
+ }
28572868
@ </ol>
28582869
@ <p>End of test</p>
28592870
style_footer();
28602871
}
28612872
--- 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 @@
2525
$(SRCDIR)/branch.c \
2626
$(SRCDIR)/browse.c \
2727
$(SRCDIR)/builtin.c \
2828
$(SRCDIR)/bundle.c \
2929
$(SRCDIR)/cache.c \
30
+ $(SRCDIR)/capabilities.c \
3031
$(SRCDIR)/captcha.c \
3132
$(SRCDIR)/cgi.c \
3233
$(SRCDIR)/checkin.c \
3334
$(SRCDIR)/checkout.c \
3435
$(SRCDIR)/clearsign.c \
@@ -205,10 +206,11 @@
205206
$(SRCDIR)/../skins/xekri/details.txt \
206207
$(SRCDIR)/../skins/xekri/footer.txt \
207208
$(SRCDIR)/../skins/xekri/header.txt \
208209
$(SRCDIR)/ci_edit.js \
209210
$(SRCDIR)/diff.tcl \
211
+ $(SRCDIR)/forum.js \
210212
$(SRCDIR)/graph.js \
211213
$(SRCDIR)/href.js \
212214
$(SRCDIR)/login.js \
213215
$(SRCDIR)/markdown.md \
214216
$(SRCDIR)/menu.js \
@@ -231,10 +233,11 @@
231233
$(OBJDIR)/branch_.c \
232234
$(OBJDIR)/browse_.c \
233235
$(OBJDIR)/builtin_.c \
234236
$(OBJDIR)/bundle_.c \
235237
$(OBJDIR)/cache_.c \
238
+ $(OBJDIR)/capabilities_.c \
236239
$(OBJDIR)/captcha_.c \
237240
$(OBJDIR)/cgi_.c \
238241
$(OBJDIR)/checkin_.c \
239242
$(OBJDIR)/checkout_.c \
240243
$(OBJDIR)/clearsign_.c \
@@ -366,10 +369,11 @@
366369
$(OBJDIR)/branch.o \
367370
$(OBJDIR)/browse.o \
368371
$(OBJDIR)/builtin.o \
369372
$(OBJDIR)/bundle.o \
370373
$(OBJDIR)/cache.o \
374
+ $(OBJDIR)/capabilities.o \
371375
$(OBJDIR)/captcha.o \
372376
$(OBJDIR)/cgi.o \
373377
$(OBJDIR)/checkin.o \
374378
$(OBJDIR)/checkout.o \
375379
$(OBJDIR)/clearsign.o \
@@ -699,10 +703,11 @@
699703
$(OBJDIR)/branch_.c:$(OBJDIR)/branch.h \
700704
$(OBJDIR)/browse_.c:$(OBJDIR)/browse.h \
701705
$(OBJDIR)/builtin_.c:$(OBJDIR)/builtin.h \
702706
$(OBJDIR)/bundle_.c:$(OBJDIR)/bundle.h \
703707
$(OBJDIR)/cache_.c:$(OBJDIR)/cache.h \
708
+ $(OBJDIR)/capabilities_.c:$(OBJDIR)/capabilities.h \
704709
$(OBJDIR)/captcha_.c:$(OBJDIR)/captcha.h \
705710
$(OBJDIR)/cgi_.c:$(OBJDIR)/cgi.h \
706711
$(OBJDIR)/checkin_.c:$(OBJDIR)/checkin.h \
707712
$(OBJDIR)/checkout_.c:$(OBJDIR)/checkout.h \
708713
$(OBJDIR)/clearsign_.c:$(OBJDIR)/clearsign.h \
@@ -922,10 +927,18 @@
922927
923928
$(OBJDIR)/cache.o: $(OBJDIR)/cache_.c $(OBJDIR)/cache.h $(SRCDIR)/config.h
924929
$(XTCC) -o $(OBJDIR)/cache.o -c $(OBJDIR)/cache_.c
925930
926931
$(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
927940
928941
$(OBJDIR)/captcha_.c: $(SRCDIR)/captcha.c $(OBJDIR)/translate
929942
$(OBJDIR)/translate $(SRCDIR)/captcha.c >$@
930943
931944
$(OBJDIR)/captcha.o: $(OBJDIR)/captcha_.c $(OBJDIR)/captcha.h $(SRCDIR)/config.h
932945
--- 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
--- src/makemake.tcl
+++ src/makemake.tcl
@@ -37,10 +37,11 @@
3737
branch
3838
browse
3939
builtin
4040
bundle
4141
cache
42
+ capabilities
4243
captcha
4344
cgi
4445
checkin
4546
checkout
4647
clearsign
4748
--- 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 @@
3434
#define CFTYPE_CONTROL 3
3535
#define CFTYPE_WIKI 4
3636
#define CFTYPE_TICKET 5
3737
#define CFTYPE_ATTACHMENT 6
3838
#define CFTYPE_EVENT 7
39
+#define CFTYPE_FORUM 8
3940
4041
/*
4142
** File permissions used by Fossil internally.
4243
*/
4344
#define PERM_REG 0 /* regular file */
@@ -76,16 +77,19 @@
7677
char *zUser; /* Name of the user from the U card. */
7778
char *zRepoCksum; /* MD5 checksum of the baseline content. R card. */
7879
char *zWiki; /* Text of the wiki page. W card. */
7980
char *zWikiTitle; /* Name of the wiki page. L card. */
8081
char *zMimetype; /* Mime type of wiki or comment text. N card. */
82
+ char *zThreadTitle; /* The forum thread title. H card */
8183
double rEventDate; /* Date of an event. E card. */
8284
char *zEventId; /* Artifact hash for an event. E card. */
8385
char *zTicketUuid; /* UUID for a ticket. K card. */
8486
char *zAttachName; /* Filename of an attachment. A card. */
8587
char *zAttachSrc; /* Artifact hash for document being attached. A card. */
8688
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 */
8791
int nFile; /* Number of F cards */
8892
int nFileAlloc; /* Slots allocated in aFile[] */
8993
int iFile; /* Index of current file in iterator */
9094
ManifestFile *aFile; /* One entry for each F-card */
9195
int nParent; /* Number of parents. */
@@ -112,10 +116,42 @@
112116
char *zName; /* Key or field name */
113117
char *zValue; /* Value of the field */
114118
} *aField; /* One for each J card */
115119
};
116120
#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
+};
117153
118154
/*
119155
** A cache of parsed manifests. This reduces the number of
120156
** calls to manifest_parse() when doing a rebuild.
121157
*/
@@ -147,10 +183,35 @@
147183
if( p->pBaseline ) manifest_destroy(p->pBaseline);
148184
memset(p, 0, sizeof(*p));
149185
fossil_free(p);
150186
}
151187
}
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
+}
152213
153214
/*
154215
** Add an element to the manifest cache using LRU replacement.
155216
*/
156217
void manifest_cache_insert(Manifest *p){
@@ -352,22 +413,26 @@
352413
** The card type determines the other parameters to the card.
353414
** Cards must occur in lexicographical order.
354415
*/
355416
Manifest *manifest_parse(Blob *pContent, int rid, Blob *pErr){
356417
Manifest *p;
357
- int seenZ = 0;
358418
int i, lineNo=0;
359419
ManifestText x;
360420
char cPrevType = 0;
361421
char cType;
362422
char *z;
363423
int n;
364424
char *zUuid;
365425
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 */
367429
static Bag seen;
368430
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 */
369434
370435
if( rid==0 ){
371436
isRepeat = 1;
372437
}else if( bag_find(&seen, rid) ){
373438
isRepeat = 1;
@@ -422,10 +487,12 @@
422487
x.z = z;
423488
x.zEnd = &z[n];
424489
x.atEol = 1;
425490
while( (cType = next_card(&x))!=0 && cType>=cPrevType ){
426491
lineNo++;
492
+ if( cType<'A' || cType>'Z' ) SYNTAX("bad card type");
493
+ seenCard |= 1 << (cType-'A');
427494
switch( cType ){
428495
/*
429496
** A <filename> <target> ?<source>?
430497
**
431498
** Identifies an attachment to either a wiki page or a ticket.
@@ -454,10 +521,11 @@
454521
SYNTAX("invalid source on A-card");
455522
}
456523
p->zAttachName = (char*)file_tail(zName);
457524
p->zAttachSrc = zSrc;
458525
p->zAttachTarget = zTarget;
526
+ p->type = CFTYPE_ATTACHMENT;
459527
break;
460528
}
461529
462530
/*
463531
** B <uuid>
@@ -469,10 +537,11 @@
469537
p->zBaseline = next_token(&x, &sz);
470538
if( p->zBaseline==0 ) SYNTAX("missing hash on B-card");
471539
if( !hname_validate(p->zBaseline,sz) ){
472540
SYNTAX("invalid hash on B-card");
473541
}
542
+ p->type = CFTYPE_MANIFEST;
474543
break;
475544
}
476545
477546
478547
/*
@@ -520,10 +589,11 @@
520589
if( p->rEventDate<=0.0 ) SYNTAX("malformed date on E-card");
521590
p->zEventId = next_token(&x, &sz);
522591
if( !hname_validate(p->zEventId, sz) ){
523592
SYNTAX("malformed hash on E-card");
524593
}
594
+ p->type = CFTYPE_EVENT;
525595
break;
526596
}
527597
528598
/*
529599
** F <filename> ?<uuid>? ?<permissions>? ?<old-name>?
@@ -565,10 +635,59 @@
565635
p->aFile[i].zPerm = zPerm;
566636
p->aFile[i].zPrior = zPriorName;
567637
if( i>0 && fossil_strcmp(p->aFile[i-1].zName, zName)>=0 ){
568638
SYNTAX("incorrect F-card sort order");
569639
}
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;
570689
break;
571690
}
572691
573692
/*
574693
** J <name> ?<value>?
@@ -594,10 +713,11 @@
594713
p->aField[i].zName = zName;
595714
p->aField[i].zValue = zValue;
596715
if( i>0 && fossil_strcmp(p->aField[i-1].zName, zName)>=0 ){
597716
SYNTAX("incorrect J-card sort order");
598717
}
718
+ p->type = CFTYPE_TICKET;
599719
break;
600720
}
601721
602722
603723
/*
@@ -611,10 +731,11 @@
611731
p->zTicketUuid = next_token(&x, &sz);
612732
if( sz!=HNAME_LEN_SHA1 ) SYNTAX("K-card UUID is the wrong size");
613733
if( !validate16(p->zTicketUuid, sz) ){
614734
SYNTAX("invalid K-card UUID");
615735
}
736
+ p->type = CFTYPE_TICKET;
616737
break;
617738
}
618739
619740
/*
620741
** L <wikititle>
@@ -628,10 +749,11 @@
628749
if( p->zWikiTitle==0 ) SYNTAX("missing title on L-card");
629750
defossilize(p->zWikiTitle);
630751
if( !wiki_name_is_wellformed((const unsigned char *)p->zWikiTitle) ){
631752
SYNTAX("L-card has malformed wiki name");
632753
}
754
+ p->type = CFTYPE_WIKI;
633755
break;
634756
}
635757
636758
/*
637759
** M <hash>
@@ -653,10 +775,11 @@
653775
i = p->nCChild++;
654776
p->azCChild[i] = zUuid;
655777
if( i>0 && fossil_strcmp(p->azCChild[i-1], zUuid)>=0 ){
656778
SYNTAX("M-card in the wrong order");
657779
}
780
+ p->type = CFTYPE_CLUSTER;
658781
break;
659782
}
660783
661784
/*
662785
** N <uuid>
@@ -717,10 +840,11 @@
717840
p->aCherrypick[n].zCPTarget = zUuid;
718841
p->aCherrypick[n].zCPBase = zUuid = next_token(&x, &sz);
719842
if( zUuid && !hname_validate(zUuid,sz) ){
720843
SYNTAX("invalid second hash on Q-card");
721844
}
845
+ p->type = CFTYPE_MANIFEST;
722846
break;
723847
}
724848
725849
/*
726850
** R <md5sum>
@@ -731,10 +855,11 @@
731855
case 'R': {
732856
if( p->zRepoCksum!=0 ) SYNTAX("more than one R-card");
733857
p->zRepoCksum = next_token(&x, &sz);
734858
if( sz!=32 ) SYNTAX("wrong size cksum on R-card");
735859
if( !validate16(p->zRepoCksum, 32) ) SYNTAX("malformed R-card cksum");
860
+ p->type = CFTYPE_MANIFEST;
736861
break;
737862
}
738863
739864
/*
740865
** T (+|*|-)<tagname> <uuid> ?<value>?
@@ -759,24 +884,21 @@
759884
if( zUuid==0 ) SYNTAX("missing artifact hash on T-card");
760885
zValue = next_token(&x, 0);
761886
if( zValue ) defossilize(zValue);
762887
if( hname_validate(zUuid, sz) ){
763888
/* A valid artifact hash */
764
- if( p->zEventId ) SYNTAX("non-self-referential T-card in event");
765889
}else if( sz==1 && zUuid[0]=='*' ){
766890
zUuid = 0;
767
- hasSelfRefTag = 1;
768
- if( p->zEventId && zName[0]!='+' ){
769
- SYNTAX("propagating T-card in event");
770
- }
891
+ nSelfTag++;
771892
}else{
772893
SYNTAX("malformed artifact hash on T-card");
773894
}
774895
defossilize(zName);
775896
if( zName[0]!='-' && zName[0]!='+' && zName[0]!='*' ){
776897
SYNTAX("T-card name does not begin with '-', '+', or '*'");
777898
}
899
+ if( zName[0]=='+' ) nSimpleTag++;
778900
if( validate16(&zName[1], strlen(&zName[1])) ){
779901
/* Do not allow tags whose names look like a hash */
780902
SYNTAX("T-card name looks like a hexadecimal hash");
781903
}
782904
if( p->nTag>=p->nTagAlloc ){
@@ -858,104 +980,78 @@
858980
*/
859981
case 'Z': {
860982
zUuid = next_token(&x, &sz);
861983
if( sz!=32 ) SYNTAX("wrong size for Z-card cksum");
862984
if( !validate16(zUuid, 32) ) SYNTAX("malformed Z-card cksum");
863
- seenZ = 1;
864985
break;
865986
}
866987
default: {
867988
SYNTAX("unrecognized card");
868989
}
869990
}
870991
}
871992
if( x.z<x.zEnd ) SYNTAX("extra characters at end of card");
872993
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
+
9481044
md5sum_init();
9491045
if( !isRepeat ) g.parseCnt[p->type]++;
9501046
return p;
9511047
9521048
manifest_syntax_error:
9531049
{
9541050
char *zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid);
9551051
if( zUuid ){
956
- blob_appendf(pErr, "manifest [%s] ", zUuid);
1052
+ blob_appendf(pErr, "artifact [%s] ", zUuid);
9571053
fossil_free(zUuid);
9581054
}
9591055
}
9601056
if( zErr ){
9611057
blob_appendf(pErr, "line %d: %s", lineNo, zErr);
@@ -1015,11 +1111,12 @@
10151111
/*
10161112
** COMMAND: test-parse-manifest
10171113
**
10181114
** Usage: %fossil test-parse-manifest FILENAME ?N?
10191115
**
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.
10211118
*/
10221119
void manifest_test_parse_cmd(void){
10231120
Manifest *p;
10241121
Blob b;
10251122
int i;
@@ -1039,10 +1136,47 @@
10391136
if( p==0 ) fossil_print("ERROR: %s\n", blob_str(&err));
10401137
blob_reset(&err);
10411138
manifest_destroy(p);
10421139
}
10431140
}
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
+}
10441178
10451179
/*
10461180
** Fetch the baseline associated with the delta-manifest p.
10471181
** Return 0 on success. If unable to parse the baseline,
10481182
** throw an error. If the baseline is a manifest, throw an
@@ -1059,11 +1193,11 @@
10591193
"INSERT OR IGNORE INTO orphan(rid, baseline) VALUES(%d,%d)",
10601194
p->rid, rid
10611195
);
10621196
return 1;
10631197
}
1064
- fossil_panic("cannot access baseline manifest %S", p->zBaseline);
1198
+ fossil_fatal("cannot access baseline manifest %S", p->zBaseline);
10651199
}
10661200
}
10671201
return 0;
10681202
}
10691203
@@ -2378,10 +2512,71 @@
23782512
"REPLACE INTO event(type,mtime,objid,user,comment)"
23792513
"VALUES('g',%.17g,%d,%Q,%Q)",
23802514
p->rDate, rid, p->zUser, blob_str(&comment)+1
23812515
);
23822516
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
+ }
23832578
}
23842579
db_end_transaction(0);
23852580
if( permitHooks ){
23862581
rc = xfer_run_common_script();
23872582
if( rc==TH_OK ){
23882583
--- 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 @@
5454
db_bind_int(&q, ":objid", rid);
5555
rc = db_step(&q)==SQLITE_ROW;
5656
db_reset(&q);
5757
return rc;
5858
}
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
+}
5983
6084
/*
6185
** Check to see if the object identified by RID is used for anything.
6286
*/
6387
static int object_used(int rid){
@@ -101,10 +125,13 @@
101125
"DELETE FROM tagxref WHERE rid=%d;"
102126
"DELETE FROM private WHERE rid=%d;"
103127
"DELETE FROM attachment WHERE attachid=%d;",
104128
rid, rid, rid, rid, rid, rid
105129
);
130
+ if( db_table_exists("repository","forumpost") ){
131
+ db_multi_exec("DELETE FROM forumpost WHERE fpid=%d", rid);
132
+ }
106133
zTktid = db_text(0, "SELECT tktid FROM modreq WHERE objid=%d", rid);
107134
if( zTktid && zTktid[0] ){
108135
ticket_rebuild_entry(zTktid);
109136
fossil_free(zTktid);
110137
}
@@ -144,12 +171,12 @@
144171
void modreq_page(void){
145172
Blob sql;
146173
Stmt q;
147174
148175
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);
151178
return;
152179
}
153180
style_header("Pending Moderation Requests");
154181
@ <h2>All Pending Moderation Requests</h2>
155182
if( moderation_table_exists() ){
156183
--- 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 @@
9494
** * "next"
9595
**
9696
** Return the RID of the matching artifact. Or return 0 if the name does not
9797
** match any known object. Or return -1 if the name is ambiguous.
9898
**
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.
100100
** If zType is NULL or "" or "*" then any type of artifact will serve.
101101
** If zType is "br" then find the first check-in of the named branch
102102
** rather than the last.
103103
** zType is "ci" in most use cases since we are usually searching for
104104
** a check-in.
105105
**
106106
** 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
108108
** hexadecimal identifier assigned to tickets and events. Those identifiers
109109
** live in a separate namespace.
110110
*/
111111
int symbolic_name_to_rid(const char *zTag, const char *zType){
112112
int vid;
@@ -603,11 +603,12 @@
603603
if( db_step(&q)==SQLITE_ROW ){
604604
const char *zType;
605605
switch( db_column_text(&q,0)[0] ){
606606
case 'c': zType = "Check-in"; break;
607607
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;
609610
case 't': zType = "Ticket-change"; break;
610611
case 'g': zType = "Tag-change"; break;
611612
default: zType = "Unknown"; break;
612613
}
613614
fossil_print("type: %s by %s on %s\n", zType, db_column_text(&q,2),
@@ -926,10 +927,25 @@
926927
" WHERE (blob.rid %s)\n"
927928
" AND blob.rid NOT IN (SELECT rid FROM description)\n"
928929
" AND blob.uuid=attachment.src",
929930
zWhere /*safe-for-%s*/
930931
);
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
+ }
931947
932948
/* Everything else */
933949
db_multi_exec(
934950
"INSERT OR IGNORE INTO description(rid,uuid,type,summary)\n"
935951
"SELECT blob.rid, blob.uuid,"
936952
--- 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 @@
2525
#include <fcntl.h>
2626
/*
2727
** Print a fatal error and quit.
2828
*/
2929
static void win32_fatal_error(const char *zMsg){
30
- fossil_panic("%s", zMsg);
30
+ fossil_fatal("%s", zMsg);
3131
}
3232
#else
3333
#include <signal.h>
3434
#include <sys/wait.h>
3535
#endif
3636
--- 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
--- src/printf.c
+++ src/printf.c
@@ -1085,12 +1085,16 @@
10851085
mainInFatalError = 1;
10861086
db_force_rollback();
10871087
va_start(ap, zFormat);
10881088
sqlite3_vsnprintf(sizeof(z),z,zFormat, ap);
10891089
va_end(ap);
1090
+ if( g.fAnyTrace ){
1091
+ fprintf(stderr, "/***** panic on %d *****/\n", getpid());
1092
+ }
10901093
fossil_errorlog("panic: %s", z);
10911094
rc = fossil_print_error(rc, z);
1095
+ abort();
10921096
exit(rc);
10931097
}
10941098
NORETURN void fossil_fatal(const char *zFormat, ...){
10951099
char *z;
10961100
int rc = 1;
10971101
--- 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 @@
290290
@ -- and so it makes sense to precompute the set of leaves. There is
291291
@ -- one entry in the following table for each leaf.
292292
@ --
293293
@ CREATE TABLE leaf(rid INTEGER PRIMARY KEY);
294294
@
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
296302
@ --
297303
@ 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
299305
@ mtime DATETIME, -- Time of occurrence. Julian day.
300306
@ objid INTEGER PRIMARY KEY, -- Associated record ID
301307
@ tagid INTEGER, -- Associated ticket or wiki name tag
302308
@ uid INTEGER REFERENCES user, -- User who caused the event
303309
@ bgcolor TEXT, -- Color set by 'bgcolor' property
@@ -542,5 +548,28 @@
542548
@
543549
@ -- Identifier for this file type.
544550
@ -- The integer is the same as 'FSLC'.
545551
@ PRAGMA application_id=252006674;
546552
;
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
+}
547576
--- 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 @@
638638
#define SRCH_CKIN 0x0001 /* Search over check-in comments */
639639
#define SRCH_DOC 0x0002 /* Search over embedded documents */
640640
#define SRCH_TKT 0x0004 /* Search over tickets */
641641
#define SRCH_WIKI 0x0008 /* Search over wiki */
642642
#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 */
644645
#endif
645646
646647
/*
647648
** Remove bits from srchFlags which are disallowed by either the
648649
** current server configuration or by user permissions.
@@ -654,15 +655,17 @@
654655
{ SRCH_CKIN, "search-ci" },
655656
{ SRCH_DOC, "search-doc" },
656657
{ SRCH_TKT, "search-tkt" },
657658
{ SRCH_WIKI, "search-wiki" },
658659
{ SRCH_TECHNOTE, "search-technote" },
660
+ { SRCH_FORUM, "search-forum" },
659661
};
660662
int i;
661663
if( g.perm.Read==0 ) srchFlags &= ~(SRCH_CKIN|SRCH_DOC|SRCH_TECHNOTE);
662664
if( g.perm.RdTkt==0 ) srchFlags &= ~(SRCH_TKT);
663665
if( g.perm.RdWiki==0 ) srchFlags &= ~(SRCH_WIKI);
666
+ if( g.perm.RdForum==0) srchFlags &= ~(SRCH_FORUM);
664667
for(i=0; i<count(aSetng); i++){
665668
unsigned int m = aSetng[i].m;
666669
if( (srchFlags & m)==0 ) continue;
667670
if( ((knownGood|knownBad) & m)!=0 ) continue;
668671
if( db_get_boolean(aSetng[i].zKey,0) ){
@@ -793,10 +796,23 @@
793796
" search_snippet()"
794797
" FROM technote"
795798
" WHERE search_match('',body('e',rid,NULL));"
796799
);
797800
}
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
+ }
798814
}
799815
800816
/*
801817
** Number of significant bits in a u32
802818
*/
@@ -913,10 +929,11 @@
913929
{ SRCH_CKIN, 'c' },
914930
{ SRCH_DOC, 'd' },
915931
{ SRCH_TKT, 't' },
916932
{ SRCH_WIKI, 'w' },
917933
{ SRCH_TECHNOTE, 'e' },
934
+ { SRCH_FORUM, 'f' },
918935
};
919936
int i;
920937
for(i=0; i<count(aMask); i++){
921938
if( srchFlags & aMask[i].m ){
922939
blob_appendf(&sql, "%sftsdocs.type='%c'", zSep, aMask[i].c);
@@ -1049,29 +1066,38 @@
10491066
**
10501067
** This routine automatically restricts srchFlag according to user
10511068
** permissions and the server configuration. The entry box is shown
10521069
** disabled if srchFlags is 0 after these restrictions are applied.
10531070
**
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.
10561079
*/
1057
-void search_screen(unsigned srchFlags, int useYparam){
1080
+int search_screen(unsigned srchFlags, int mFlags){
10581081
const char *zType = 0;
10591082
const char *zClass = 0;
10601083
const char *zDisable1;
10611084
const char *zDisable2;
10621085
const char *zPattern;
10631086
int fDebug = PB("debug");
1087
+ int haveResult = 0;
10641088
srchFlags = search_restrict(srchFlags);
10651089
switch( srchFlags ){
10661090
case SRCH_CKIN: zType = " Check-ins"; zClass = "Ckin"; break;
10671091
case SRCH_DOC: zType = " Docs"; zClass = "Doc"; break;
10681092
case SRCH_TKT: zType = " Tickets"; zClass = "Tkt"; break;
10691093
case SRCH_WIKI: zType = " Wiki"; zClass = "Wiki"; break;
10701094
case SRCH_TECHNOTE: zType = " Tech Notes"; zClass = "Note"; break;
1095
+ case SRCH_FORUM: zType = " Forum"; zClass = "Frm"; break;
10711096
}
10721097
if( srchFlags==0 ){
1098
+ if( mFlags & 0x02 ) return 0;
10731099
zDisable1 = " disabled";
10741100
zDisable2 = " disabled";
10751101
zPattern = "";
10761102
}else{
10771103
zDisable1 = ""; /* Was: " autofocus" */
@@ -1083,18 +1109,19 @@
10831109
@ <div class='searchForm searchForm%s(zClass)'>
10841110
}else{
10851111
@ <div class='searchForm'>
10861112
}
10871113
@ <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 ){
10891115
static const struct { char *z; char *zNm; unsigned m; } aY[] = {
10901116
{ "all", "All", SRCH_ALL },
10911117
{ "c", "Check-ins", SRCH_CKIN },
10921118
{ "d", "Docs", SRCH_DOC },
10931119
{ "t", "Tickets", SRCH_TKT },
10941120
{ "w", "Wiki", SRCH_WIKI },
10951121
{ "e", "Tech Notes", SRCH_TECHNOTE },
1122
+ { "f", "Forum", SRCH_FORUM },
10961123
};
10971124
const char *zY = PD("y","all");
10981125
unsigned newFlags = srchFlags;
10991126
int i;
11001127
@ <select size='1' name='y'>
@@ -1127,11 +1154,13 @@
11271154
}
11281155
if( search_run_and_output(zPattern, srchFlags, fDebug)==0 ){
11291156
@ <p class='searchEmpty'>No matches for: <span>%h(zPattern)</span></p>
11301157
}
11311158
@ </div>
1159
+ haveResult = 1;
11321160
}
1161
+ return haveResult;
11331162
}
11341163
11351164
/*
11361165
** WEBPAGE: search
11371166
**
@@ -1143,10 +1172,11 @@
11431172
** c -> check-ins
11441173
** d -> documentation
11451174
** t -> tickets
11461175
** w -> wiki
11471176
** e -> tech notes
1177
+** f -> forum
11481178
** all -> everything
11491179
*/
11501180
void search_page(void){
11511181
login_check_credentials();
11521182
style_header("Search");
@@ -1250,10 +1280,11 @@
12501280
** cType: d Embedded documentation
12511281
** w Wiki page
12521282
** c Check-in comment
12531283
** t Ticket text
12541284
** e Tech note
1285
+** f Forum
12551286
**
12561287
** rid The RID of an artifact that defines the object
12571288
** being searched.
12581289
**
12591290
** zName Name of the object being searched. This is used
@@ -1275,17 +1306,27 @@
12751306
blob_to_utf8_no_bom(&doc, 0);
12761307
get_stext_by_mimetype(&doc, mimetype_from_name(zName), pOut);
12771308
blob_reset(&doc);
12781309
break;
12791310
}
1311
+ case 'f': /* Forum messages */
12801312
case 'e': /* Tech Notes */
12811313
case 'w': { /* Wiki */
12821314
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);
12841317
Blob wiki;
12851318
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
+ }
12871328
get_stext_by_mimetype(&wiki, wiki_filter_mimetypes(pWiki->zMimetype),
12881329
pOut);
12891330
blob_reset(&wiki);
12901331
manifest_destroy(pWiki);
12911332
break;
@@ -1516,11 +1557,11 @@
15161557
"INSERT OR IGNORE INTO ftsdocs(type,rid,idxed)"
15171558
" SELECT 't', tkt_id, 0 FROM ticket;"
15181559
);
15191560
db_multi_exec(
15201561
"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');"
15221563
);
15231564
}
15241565
15251566
/*
15261567
** The document described by cType,rid,zName is about to be added or
@@ -1553,10 +1594,11 @@
15531594
db_multi_exec(
15541595
"DELETE FROM ftsdocs WHERE type='%c' AND name=%Q AND rid!=%d",
15551596
cType, zName, rid
15561597
);
15571598
}
1599
+ /* All forum posts are always indexed */
15581600
}
15591601
}
15601602
15611603
/*
15621604
** If the doc-glob and doc-br settings are valid for document search
@@ -1681,10 +1723,33 @@
16811723
" tagxref.mtime"
16821724
" FROM tagxref WHERE tagxref.rid=ftsdocs.rid)"
16831725
" WHERE ftsdocs.type='w' AND NOT ftsdocs.idxed"
16841726
);
16851727
}
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
+}
16861751
16871752
/*
16881753
** Deal with all of the unindexed 'e' terms in FTSDOCS
16891754
*/
16901755
static void search_update_technote_index(void){
@@ -1728,10 +1793,13 @@
17281793
search_update_wiki_index();
17291794
}
17301795
if( srchFlags & SRCH_TECHNOTE ){
17311796
search_update_technote_index();
17321797
}
1798
+ if( srchFlags & SRCH_FORUM ){
1799
+ search_update_forum_index();
1800
+ }
17331801
}
17341802
17351803
/*
17361804
** Construct, prepopulate, and then update the full-text index.
17371805
*/
@@ -1780,10 +1848,11 @@
17801848
{ "search-ci", "check-in search:", "c" },
17811849
{ "search-doc", "document search:", "d" },
17821850
{ "search-tkt", "ticket search:", "t" },
17831851
{ "search-wiki", "wiki search:", "w" },
17841852
{ "search-technote", "tech note search:", "e" },
1853
+ { "search-forum", "forum search:", "f" },
17851854
};
17861855
char *zSubCmd = 0;
17871856
int i, j, n;
17881857
int iCmd = 0;
17891858
int iAction = 0;
17901859
--- 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
--- src/security_audit.c
+++ src/security_audit.c
@@ -58,11 +58,11 @@
5858
/* Step 1: Determine if the repository is public or private. "Public"
5959
** means that any anonymous user on the internet can access all content.
6060
** "Private" repos require (non-anonymous) login to access all content,
6161
** though some content may be accessible anonymously.
6262
*/
63
- zAnonCap = db_text("", "SELECT group_concat(coalesce(cap,'')) FROM user"
63
+ zAnonCap = db_text("", "SELECT capunion(cap) FROM user"
6464
" WHERE login IN ('anonymous','nobody')");
6565
zPubPages = db_get("public-pages",0);
6666
if( hasAnyCap(zAnonCap,"as") ){
6767
@ <li><p>This repository is <big><b>Wildly INSECURE</b></big> because
6868
@ it grants administrator privileges to anonymous users. You
@@ -78,11 +78,12 @@
7878
@ "nobody" on the <a href="setup_ulist">User Configuration</a> page.
7979
}else if( hasAnyCap(zAnonCap,"goz") ){
8080
@ <li><p>This repository is <big><b>PUBLIC</b></big>. All
8181
@ checked-in content can be accessed by anonymous users.
8282
@ <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) ){
8485
@ <li><p>This repository is <big><b>Completely PRIVATE</b></big>.
8586
@ A valid login and password is required to access any content.
8687
}else{
8788
@ <li><p>This repository is <big><b>Mostly PRIVATE</b></big>.
8889
@ A valid login and password is usually required, however some
@@ -91,10 +92,13 @@
9192
if( hasAnyCap(zAnonCap,"j") ){
9293
@ <li> Wiki pages
9394
}
9495
if( hasAnyCap(zAnonCap,"r") ){
9596
@ <li> Tickets
97
+ }
98
+ if( hasAnyCap(zAnonCap,"234567") ){
99
+ @ <li> Forum posts
96100
}
97101
if( zPubPages && zPubPages[0] ){
98102
Glob *pGlob = glob_create(zPubPages);
99103
int i;
100104
@ <li> URLs that match any of these GLOB patterns:
@@ -126,11 +130,12 @@
126130
*/
127131
if( hasAnyCap(zAnonCap, "e") ){
128132
@ <li><p><b>WARNING:</b>
129133
@ Anonymous users can view email addresses and other personally
130134
@ 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
132137
@ "anonymous" and "nobody" on the
133138
@ <a href="setup_ulist">User Configuration</a> page.
134139
}
135140
136141
/* Anonymous users probably should not be allowed to push content
@@ -137,25 +142,27 @@
137142
** to the repository.
138143
*/
139144
if( hasAnyCap(zAnonCap, "i") ){
140145
@ <li><p><b>WARNING:</b>
141146
@ 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
143149
@ "anonymous" and "nobody" on the
144150
@ <a href="setup_ulist">User Configuration</a> page.
145151
}
146152
147153
/* Anonymous users probably should not be allowed act as moderators
148154
** for wiki or tickets.
149155
*/
150
- if( hasAnyCap(zAnonCap, "lq") ){
156
+ if( hasAnyCap(zAnonCap, "lq5") ){
151157
@ <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.
157164
}
158165
159166
/* Anonymous users probably should not be allowed to delete
160167
** wiki or tickets.
161168
*/
@@ -174,11 +181,11 @@
174181
if( db_get_boolean("modreq-wiki",0)==0 ){
175182
@ <li><p><b>WARNING:</b>
176183
@ Anonymous users can create or edit wiki without moderation.
177184
@ This can result in robots inserting lots of wiki spam into
178185
@ repository.
179
- @ <p>Fix this by removing the "New-Wiki" and "Write-Wiki"
186
+ @ Fix this by removing the "New-Wiki" and "Write-Wiki"
180187
@ privileges from users "anonymous" and "nobody" on the
181188
@ <a href="setup_ulist">User Configuration</a> page or
182189
@ by enabling wiki moderation on the
183190
@ <a href="setup_modreq">Moderation Setup</a> page.
184191
}else{
@@ -185,10 +192,36 @@
185192
@ <li><p>
186193
@ Anonymous users can create or edit wiki, but moderator
187194
@ approval is required before the edits become permanent.
188195
}
189196
}
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
+ }
190223
191224
/* Administrative privilege should only be provided to
192225
** specific individuals, not to entire classes of people.
193226
** And not too many people should have administrator privilege.
194227
*/
@@ -349,10 +382,22 @@
349382
@ <li><p>
350383
@ The error log at "<a href='%R/errorlog'>%h(g.zErrlog)</a>" that is
351384
@ %,lld(file_size(g.zErrlog, ExtFILE)) bytes in size.
352385
}
353386
}
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
+ }
354399
355400
@ </ol>
356401
style_footer();
357402
}
358403
359404
--- 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 @@
296296
db_finalize(&s);
297297
style_table_sorter();
298298
style_footer();
299299
}
300300
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
-
382301
/*
383302
** WEBPAGE: setup_ulist_notes
384303
**
385304
** A documentation page showing notes about user configuration. This
386305
** information used to be a side-bar on the user list page, but has been
@@ -417,11 +336,11 @@
417336
@ <span class="usertype">anonymous</span>, and
418337
@ <span class="usertype">nobody</span>.
419338
@ </p></li>
420339
@
421340
@ <li><p>The permission flags are as follows:</p>
422
- setup_usercap_table();
341
+ capabilities_table();
423342
@ </li>
424343
@ </ol>
425344
style_footer();
426345
}
427346
@@ -431,11 +350,11 @@
431350
** A documentation page showing the meaning of the various user capabilities
432351
** code letters.
433352
*/
434353
void setup_ucap_list(void){
435354
style_header("User Capability Codes");
436
- setup_usercap_table();
355
+ capabilities_table();
437356
style_footer();
438357
}
439358
440359
/*
441360
** Return true if zPw is a valid password string. A valid
@@ -1312,10 +1231,12 @@
13121231
"defaultperms", "u", 0);
13131232
@ <p>Permissions given to users that... <ul><li>register themselves using
13141233
@ the self-registration procedure (if enabled), or <li>access "public"
13151234
@ pages identified by the public-pages glob pattern above, or <li>
13161235
@ 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>.
13171238
@ (Property: "default-perms")
13181239
@ </p>
13191240
13201241
@ <hr />
13211242
onoff_attribute("Show javascript button to fill in CAPTCHA",
@@ -2312,10 +2233,12 @@
23122233
onoff_attribute("Search Tickets", "search-tkt", "st", 0, 0);
23132234
@ <br />
23142235
onoff_attribute("Search Wiki", "search-wiki", "sw", 0, 0);
23152236
@ <br />
23162237
onoff_attribute("Search Tech Notes", "search-technote", "se", 0, 0);
2238
+ @ <br />
2239
+ onoff_attribute("Search Forum", "search-forum", "sf", 0, 0);
23172240
@ <hr />
23182241
@ <p><input type="submit" name="submit" value="Apply Changes" /></p>
23192242
@ <hr />
23202243
if( P("fts0") ){
23212244
search_drop_index();
23222245
--- 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 @@
9595
# endif
9696
#endif
9797
#if (!defined(_WIN32) && !defined(WIN32)) || defined(__MINGW32__)
9898
# include <unistd.h>
9999
# include <dirent.h>
100
+# define GETPID getpid
100101
# if defined(__MINGW32__)
101102
# define DIRENT dirent
102103
# ifndef S_ISLNK
103104
# define S_ISLNK(mode) (0)
104105
# endif
105106
# endif
107
+#else
108
+# define GETPID (int)GetCurrentProcessId
106109
#endif
107110
#include <sys/types.h>
108111
#include <sys/stat.h>
109112
110113
#if HAVE_READLINE
@@ -15847,10 +15850,27 @@
1584715850
1584815851
setBinaryMode(stdin, 0);
1584915852
setvbuf(stderr, 0, _IONBF, 0); /* Make sure stderr is unbuffered */
1585015853
stdin_is_interactive = isatty(0);
1585115854
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
1585215872
1585315873
#if USE_SYSTEM_SQLITE+0!=1
1585415874
if( strncmp(sqlite3_sourceid(),SQLITE_SOURCE_ID,60)!=0 ){
1585515875
utf8_printf(stderr, "SQLite header and source version mismatch\n%s\n%s\n",
1585615876
sqlite3_sourceid(), SQLITE_SOURCE_ID);
1585715877
--- 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 @@
254254
*/
255255
const char *skin_detail(const char *zName){
256256
struct SkinDetail *pDetail;
257257
skin_detail_initialize();
258258
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);
260260
return pDetail->zValue;
261261
}
262262
int skin_detail_boolean(const char *zName){
263263
return !is_false(skin_detail(zName));
264264
}
265265
--- 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 @@
12711271
smtp_server_send(&x, "250 ok\r\n");
12721272
}else
12731273
if( strncmp(z, "MAIL FROM:<", 11)==0 ){
12741274
smtp_server_route_incoming(&x, 0);
12751275
smtp_server_clear(&x, SMTPSRV_CLEAR_MSG);
1276
- x.zFrom = email_copy_addr(z+11);
1276
+ x.zFrom = email_copy_addr(z+11,'>');
12771277
if( x.zFrom==0 ){
12781278
smtp_server_send(&x, "500 unacceptable email address\r\n");
12791279
}else{
12801280
smtp_server_send(&x, "250 ok\r\n");
12811281
}
@@ -1284,11 +1284,11 @@
12841284
char *zAddr;
12851285
if( x.zFrom==0 ){
12861286
smtp_server_send(&x, "500 missing MAIL FROM\r\n");
12871287
continue;
12881288
}
1289
- zAddr = email_copy_addr(z+9);
1289
+ zAddr = email_copy_addr(z+9, '>');
12901290
if( zAddr==0 ){
12911291
smtp_server_send(&x, "505 no such user\r\n");
12921292
continue;
12931293
}
12941294
smtp_append_to(&x, zAddr, 0);
12951295
--- 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 @@
5353
/* These macros are provided to "stringify" the value of the define
5454
** for those options in which the value is meaningful. */
5555
#define CTIMEOPT_VAL_(opt) #opt
5656
#define CTIMEOPT_VAL(opt) CTIMEOPT_VAL_(opt)
5757
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
+
5864
/*
5965
** An array of names of all compile-time options. This array should
6066
** be sorted A-Z.
6167
**
6268
** This array looks large, but in a typical installation actually uses
@@ -136,11 +142,11 @@
136142
#endif
137143
#ifdef SQLITE_DEFAULT_LOCKING_MODE
138144
"DEFAULT_LOCKING_MODE=" CTIMEOPT_VAL(SQLITE_DEFAULT_LOCKING_MODE),
139145
#endif
140146
#ifdef SQLITE_DEFAULT_LOOKASIDE
141
- "DEFAULT_LOOKASIDE=" CTIMEOPT_VAL(SQLITE_DEFAULT_LOOKASIDE),
147
+ "DEFAULT_LOOKASIDE=" CTIMEOPT_VAL2(SQLITE_DEFAULT_LOOKASIDE),
142148
#endif
143149
#if SQLITE_DEFAULT_MEMSTATUS
144150
"DEFAULT_MEMSTATUS",
145151
#endif
146152
#ifdef SQLITE_DEFAULT_MMAP_SIZE
@@ -1150,11 +1156,11 @@
11501156
** [sqlite3_libversion_number()], [sqlite3_sourceid()],
11511157
** [sqlite_version()] and [sqlite_source_id()].
11521158
*/
11531159
#define SQLITE_VERSION "3.25.0"
11541160
#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"
11561162
11571163
/*
11581164
** CAPI3REF: Run-Time Library Version Numbers
11591165
** KEYWORDS: sqlite3_version sqlite3_sourceid
11601166
**
@@ -1912,11 +1918,12 @@
19121918
** interrogated. The zDbName parameter is ignored.
19131919
**
19141920
** <li>[[SQLITE_FCNTL_PERSIST_WAL]]
19151921
** ^The [SQLITE_FCNTL_PERSIST_WAL] opcode is used to set or query the
19161922
** 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
19181925
** are automatically deleted when the latest connection to the database
19191926
** closes. Setting persistent WAL mode causes those files to persist after
19201927
** close. Persisting the files is useful when other processes that do not
19211928
** have write permission on the directory containing the database file want
19221929
** to read the database file, as the WAL and shared memory files must exist
@@ -9985,11 +9992,10 @@
99859992
SQLITE_API int sqlite3_system_errno(sqlite3*);
99869993
99879994
/*
99889995
** CAPI3REF: Database Snapshot
99899996
** KEYWORDS: {snapshot} {sqlite3_snapshot}
9990
-** EXPERIMENTAL
99919997
**
99929998
** An instance of the snapshot object records the state of a [WAL mode]
99939999
** database for some specific point in history.
999410000
**
999510001
** In [WAL mode], multiple [database connections] that are open on the
@@ -10002,23 +10008,18 @@
1000210008
**
1000310009
** The sqlite3_snapshot object records state information about an historical
1000410010
** version of the database file so that it is possible to later open a new read
1000510011
** transaction that sees that historical version of the database rather than
1000610012
** 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()].
1001210013
*/
1001310014
typedef struct sqlite3_snapshot {
1001410015
unsigned char hidden[48];
1001510016
} sqlite3_snapshot;
1001610017
1001710018
/*
1001810019
** CAPI3REF: Record A Database Snapshot
10019
-** EXPERIMENTAL
10020
+** CONSTRUCTOR: sqlite3_snapshot
1002010021
**
1002110022
** ^The [sqlite3_snapshot_get(D,S,P)] interface attempts to make a
1002210023
** new [sqlite3_snapshot] object that records the current state of
1002310024
** schema S in database connection D. ^On success, the
1002410025
** [sqlite3_snapshot_get(D,S,P)] interface writes a pointer to the newly
@@ -10053,21 +10054,21 @@
1005310054
** The [sqlite3_snapshot] object returned from a successful call to
1005410055
** [sqlite3_snapshot_get()] must be freed using [sqlite3_snapshot_free()]
1005510056
** to avoid a memory leak.
1005610057
**
1005710058
** 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.
1005910060
*/
1006010061
SQLITE_API SQLITE_EXPERIMENTAL int sqlite3_snapshot_get(
1006110062
sqlite3 *db,
1006210063
const char *zSchema,
1006310064
sqlite3_snapshot **ppSnapshot
1006410065
);
1006510066
1006610067
/*
1006710068
** CAPI3REF: Start a read transaction on an historical snapshot
10068
-** EXPERIMENTAL
10069
+** METHOD: sqlite3_snapshot
1006910070
**
1007010071
** ^The [sqlite3_snapshot_open(D,S,P)] interface starts a
1007110072
** read transaction for schema S of
1007210073
** [database connection] D such that the read transaction
1007310074
** refers to historical [snapshot] P, rather than the most
@@ -10091,34 +10092,34 @@
1009110092
** after the most recent I/O on the database connection.)^
1009210093
** (Hint: Run "[PRAGMA application_id]" against a newly opened
1009310094
** database connection in order to make it ready to use snapshots.)
1009410095
**
1009510096
** 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.
1009710098
*/
1009810099
SQLITE_API SQLITE_EXPERIMENTAL int sqlite3_snapshot_open(
1009910100
sqlite3 *db,
1010010101
const char *zSchema,
1010110102
sqlite3_snapshot *pSnapshot
1010210103
);
1010310104
1010410105
/*
1010510106
** CAPI3REF: Destroy a snapshot
10106
-** EXPERIMENTAL
10107
+** DESTRUCTOR: sqlite3_snapshot
1010710108
**
1010810109
** ^The [sqlite3_snapshot_free(P)] interface destroys [sqlite3_snapshot] P.
1010910110
** The application must eventually free every [sqlite3_snapshot] object
1011010111
** using this routine to avoid a memory leak.
1011110112
**
1011210113
** 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.
1011410115
*/
1011510116
SQLITE_API SQLITE_EXPERIMENTAL void sqlite3_snapshot_free(sqlite3_snapshot*);
1011610117
1011710118
/*
1011810119
** CAPI3REF: Compare the ages of two snapshot handles.
10119
-** EXPERIMENTAL
10120
+** METHOD: sqlite3_snapshot
1012010121
**
1012110122
** The sqlite3_snapshot_cmp(P1, P2) interface is used to compare the ages
1012210123
** of two valid snapshot handles.
1012310124
**
1012410125
** If the two snapshot handles are not associated with the same database
@@ -10133,35 +10134,41 @@
1013310134
** is undefined.
1013410135
**
1013510136
** Otherwise, this API returns a negative value if P1 refers to an older
1013610137
** snapshot than P2, zero if the two handles refer to the same database
1013710138
** 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.
1013810142
*/
1013910143
SQLITE_API SQLITE_EXPERIMENTAL int sqlite3_snapshot_cmp(
1014010144
sqlite3_snapshot *p1,
1014110145
sqlite3_snapshot *p2
1014210146
);
1014310147
1014410148
/*
1014510149
** 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
1015710161
** of database handle db and make all valid snapshots available to
1015810162
** 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
1016010164
** database.
1016110165
**
1016210166
** 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.
1016310170
*/
1016410171
SQLITE_API SQLITE_EXPERIMENTAL int sqlite3_snapshot_recover(sqlite3 *db, const char *zDb);
1016510172
1016610173
/*
1016710174
** CAPI3REF: Serialize a database
@@ -18937,11 +18944,11 @@
1893718944
SQLITE_PRIVATE void sqlite3Reindex(Parse*, Token*, Token*);
1893818945
SQLITE_PRIVATE void sqlite3AlterFunctions(void);
1893918946
SQLITE_PRIVATE void sqlite3AlterRenameTable(Parse*, SrcList*, Token*);
1894018947
SQLITE_PRIVATE int sqlite3GetToken(const unsigned char *, int *);
1894118948
SQLITE_PRIVATE void sqlite3NestedParse(Parse*, const char*, ...);
18942
-SQLITE_PRIVATE void sqlite3ExpirePreparedStatements(sqlite3*);
18949
+SQLITE_PRIVATE void sqlite3ExpirePreparedStatements(sqlite3*, int);
1894318950
SQLITE_PRIVATE int sqlite3CodeSubselect(Parse*, Expr *, int, int);
1894418951
SQLITE_PRIVATE void sqlite3SelectPrep(Parse*, Select*, NameContext*);
1894518952
SQLITE_PRIVATE void sqlite3SelectWrongNumTermsError(Parse *pParse, Select *p);
1894618953
SQLITE_PRIVATE int sqlite3MatchSpanName(const char*, const char*, const char*, const char*);
1894718954
SQLITE_PRIVATE int sqlite3ResolveExprNames(NameContext*, Expr*);
@@ -20025,13 +20032,13 @@
2002520032
#endif
2002620033
u16 nResColumn; /* Number of columns in one row of the result set */
2002720034
u8 errorAction; /* Recovery action to do in case of an error */
2002820035
u8 minWriteFileFormat; /* Minimum file format for writable database files */
2002920036
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 */
2003220038
bft explain:2; /* True if EXPLAIN present on SQL command */
20039
+ bft doingRerun:1; /* True if rerunning after an auto-reprepare */
2003320040
bft changeCntOn:1; /* True to update the change-counter */
2003420041
bft runOnlyOnce:1; /* Automatically expire on reset */
2003520042
bft usesStmtJournal:1; /* True if uses a statement journal */
2003620043
bft readOnly:1; /* True for statements that do not write */
2003720044
bft bIsReader:1; /* True for statements that read */
@@ -28640,11 +28647,11 @@
2864028647
sqlite3TreeViewLine(pView, "FUNCTION %Q", pExpr->u.zToken);
2864128648
}
2864228649
if( pFarg ){
2864328650
sqlite3TreeViewExprList(pView, pFarg, pWin!=0, 0);
2864428651
}
28645
-#ifndef SQLITe_OMIT_WINDOWFUNC
28652
+#ifndef SQLITE_OMIT_WINDOWFUNC
2864628653
if( pWin ){
2864728654
sqlite3TreeViewWindow(pView, pWin, 0);
2864828655
}
2864928656
#endif
2865028657
break;
@@ -42948,10 +42955,13 @@
4294842955
*/
4294942956
static int winTruncate(sqlite3_file *id, sqlite3_int64 nByte){
4295042957
winFile *pFile = (winFile*)id; /* File handle object */
4295142958
int rc = SQLITE_OK; /* Return code for this function */
4295242959
DWORD lastErrno;
42960
+#if SQLITE_MAX_MMAP_SIZE>0
42961
+ sqlite3_int64 oldMmapSize;
42962
+#endif
4295342963
4295442964
assert( pFile );
4295542965
SimulateIOError(return SQLITE_IOERR_TRUNCATE);
4295642966
OSTRACE(("TRUNCATE pid=%lu, pFile=%p, file=%p, size=%lld, lock=%d\n",
4295742967
osGetCurrentProcessId(), pFile, pFile->h, nByte, pFile->locktype));
@@ -42962,10 +42972,19 @@
4296242972
** size).
4296342973
*/
4296442974
if( pFile->szChunk>0 ){
4296542975
nByte = ((nByte + pFile->szChunk - 1)/pFile->szChunk) * pFile->szChunk;
4296642976
}
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
4296742986
4296842987
/* SetEndOfFile() returns non-zero when successful, or zero when it fails. */
4296942988
if( winSeekFile(pFile, nByte) ){
4297042989
rc = winLogError(SQLITE_IOERR_TRUNCATE, pFile->lastErrno,
4297142990
"winTruncate1", pFile->zPath);
@@ -42975,16 +42994,16 @@
4297542994
rc = winLogError(SQLITE_IOERR_TRUNCATE, pFile->lastErrno,
4297642995
"winTruncate2", pFile->zPath);
4297742996
}
4297842997
4297942998
#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
+ }
4298643005
}
4298743006
#endif
4298843007
4298943008
OSTRACE(("TRUNCATE pid=%lu, pFile=%p, file=%p, rc=%s\n",
4299043009
osGetCurrentProcessId(), pFile, pFile->h, sqlite3ErrName(rc)));
@@ -56461,22 +56480,22 @@
5646156480
5646256481
if( (rc&0xFF)==SQLITE_IOERR && rc!=SQLITE_IOERR_NOMEM ){
5646356482
rc = sqlite3JournalCreate(pPager->jfd);
5646456483
if( rc!=SQLITE_OK ){
5646556484
sqlite3OsClose(pPager->jfd);
56485
+ goto commit_phase_one_exit;
5646656486
}
5646756487
bBatch = 0;
5646856488
}else{
5646956489
sqlite3OsClose(pPager->jfd);
5647056490
}
5647156491
}
5647256492
#endif /* SQLITE_ENABLE_BATCH_ATOMIC_WRITE */
5647356493
56474
- if( bBatch==0 && rc==SQLITE_OK ){
56494
+ if( bBatch==0 ){
5647556495
rc = pager_write_pagelist(pPager, pList);
5647656496
}
56477
-
5647856497
if( rc!=SQLITE_OK ){
5647956498
assert( rc!=SQLITE_IOERR_BLOCKED );
5648056499
goto commit_phase_one_exit;
5648156500
}
5648256501
sqlite3PcacheCleanAll(pPager->pPCache);
@@ -65751,10 +65770,16 @@
6575165770
*/
6575265771
if( p->inTrans==TRANS_WRITE || (p->inTrans==TRANS_READ && !wrflag) ){
6575365772
goto trans_begun;
6575465773
}
6575565774
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
+ }
6575665781
6575765782
/* Write transactions are not possible on a read-only database */
6575865783
if( (pBt->btsFlags & BTS_READ_ONLY)!=0 && wrflag ){
6575965784
rc = SQLITE_READONLY;
6576065785
goto trans_begun;
@@ -71767,12 +71792,11 @@
7176771792
** if this is the first reference to the page.
7176871793
**
7176971794
** Also check that the page number is in bounds.
7177071795
*/
7177171796
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 ){
7177471798
checkAppendMsg(pCheck, "invalid page number %d", iPage);
7177571799
return 1;
7177671800
}
7177771801
if( getPageReferenced(pCheck, iPage) ){
7177871802
checkAppendMsg(pCheck, "2nd reference to page %d", iPage);
@@ -71823,21 +71847,16 @@
7182371847
int iPage, /* Page number for first page in the list */
7182471848
int N /* Expected number of pages in the list */
7182571849
){
7182671850
int i;
7182771851
int expected = N;
71828
- int iFirst = iPage;
71829
- while( N-- > 0 && pCheck->mxErr ){
71852
+ int nErrAtStart = pCheck->nErr;
71853
+ while( iPage!=0 && pCheck->mxErr ){
7183071854
DbPage *pOvflPage;
7183171855
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
- }
7183871856
if( checkRef(pCheck, iPage) ) break;
71857
+ N--;
7183971858
if( sqlite3PagerGet(pCheck->pPager, (Pgno)iPage, &pOvflPage, 0) ){
7184071859
checkAppendMsg(pCheck, "failed to get page %d", iPage);
7184171860
break;
7184271861
}
7184371862
pOvflData = (unsigned char *)sqlite3PagerGetData(pOvflPage);
@@ -71877,14 +71896,16 @@
7187771896
}
7187871897
}
7187971898
#endif
7188071899
iPage = get4byte(pOvflData);
7188171900
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);
7188671907
}
7188771908
}
7188871909
#endif /* SQLITE_OMIT_INTEGRITY_CHECK */
7188971910
7189071911
/*
@@ -72274,10 +72295,28 @@
7227472295
get4byte(&pBt->pPage1->aData[36]));
7227572296
sCheck.zPfx = 0;
7227672297
7227772298
/* Check all the tables.
7227872299
*/
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
7227972318
testcase( pBt->db->flags & SQLITE_CellSizeCk );
7228072319
pBt->db->flags &= ~SQLITE_CellSizeCk;
7228172320
for(i=0; (int)i<nRoot && sCheck.mxErr; i++){
7228272321
i64 notUsed;
7228372322
if( aRoot[i]==0 ) continue;
@@ -79974,15 +80013,23 @@
7997480013
** An expired statement means that recompilation of the statement is
7997580014
** recommend. Statements expire when things happen that make their
7997680015
** programs obsolete. Removing user-defined functions or collating
7997780016
** sequences, or changing an authorization function are the types of
7997880017
** 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.
7997980026
*/
79980
-SQLITE_PRIVATE void sqlite3ExpirePreparedStatements(sqlite3 *db){
80027
+SQLITE_PRIVATE void sqlite3ExpirePreparedStatements(sqlite3 *db, int iCode){
7998180028
Vdbe *p;
7998280029
for(p = db->pVdbe; p; p=p->pNext){
79983
- p->expired = 1;
80030
+ p->expired = iCode+1;
7998480031
}
7998580032
}
7998680033
7998780034
/*
7998880035
** Return the database associated with the Vdbe.
@@ -85520,11 +85567,11 @@
8552085567
if( rc!=SQLITE_OK ){
8552185568
goto abort_due_to_error;
8552285569
}
8552385570
}
8552485571
if( isSchemaChange ){
85525
- sqlite3ExpirePreparedStatements(db);
85572
+ sqlite3ExpirePreparedStatements(db, 0);
8552685573
sqlite3ResetAllSchemasOfConnection(db);
8552785574
db->mDbFlags |= DBFLAG_SchemaChange;
8552885575
}
8552985576
}
8553085577
@@ -85809,11 +85856,11 @@
8580985856
pDb->pSchema->file_format = pOp->p3;
8581085857
}
8581185858
if( pOp->p1==1 ){
8581285859
/* Invalidate all prepared statements whenever the TEMP database
8581385860
** schema is changed. Ticket #1644 */
85814
- sqlite3ExpirePreparedStatements(db);
85861
+ sqlite3ExpirePreparedStatements(db, 0);
8581585862
p->expired = 0;
8581685863
}
8581785864
if( rc ) goto abort_due_to_error;
8581885865
break;
8581985866
}
@@ -85927,11 +85974,11 @@
8592785974
assert( pOp->opcode==OP_OpenWrite || pOp->p5==0 || pOp->p5==OPFLAG_SEEKEQ );
8592885975
assert( p->bIsReader );
8592985976
assert( pOp->opcode==OP_OpenRead || pOp->opcode==OP_ReopenIdx
8593085977
|| p->readOnly==0 );
8593185978
85932
- if( p->expired ){
85979
+ if( p->expired==1 ){
8593385980
rc = SQLITE_ABORT_ROLLBACK;
8593485981
goto abort_due_to_error;
8593585982
}
8593685983
8593785984
nField = 0;
@@ -89108,25 +89155,32 @@
8910889155
}
8910989156
break;
8911089157
}
8911189158
#endif
8911289159
89113
-/* Opcode: Expire P1 * * * *
89160
+/* Opcode: Expire P1 P2 * * *
8911489161
**
8911589162
** Cause precompiled statements to expire. When an expired statement
8911689163
** is executed using sqlite3_step() it will either automatically
8911789164
** reprepare itself (if it was originally created using sqlite3_prepare_v2())
8911889165
** or it will fail with SQLITE_SCHEMA.
8911989166
**
8912089167
** If P1 is 0, then all SQL statements become expired. If P1 is non-zero,
8912189168
** 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.
8912289175
*/
8912389176
case OP_Expire: {
89177
+ assert( pOp->p2==0 || pOp->p2==1 );
8912489178
if( !pOp->p1 ){
89125
- sqlite3ExpirePreparedStatements(db);
89179
+ sqlite3ExpirePreparedStatements(db, pOp->p2);
8912689180
}else{
89127
- p->expired = 1;
89181
+ p->expired = pOp->p2+1;
8912889182
}
8912989183
break;
8913089184
}
8913189185
8913289186
#ifndef SQLITE_OMIT_SHARED_CACHE
@@ -103813,11 +103867,11 @@
103813103867
if( !pIdx->hasStat1 ) sqlite3DefaultRowEst(pIdx);
103814103868
}
103815103869
103816103870
/* Load the statistics from the sqlite_stat4 table. */
103817103871
#ifdef SQLITE_ENABLE_STAT3_OR_STAT4
103818
- if( rc==SQLITE_OK && OptimizationEnabled(db, SQLITE_Stat34) ){
103872
+ if( rc==SQLITE_OK ){
103819103873
db->lookaside.bDisable++;
103820103874
rc = loadStat4(db, sInfo.zDatabase);
103821103875
db->lookaside.bDisable--;
103822103876
}
103823103877
for(i=sqliteHashFirst(&pSchema->idxHash); i; i=sqliteHashNext(i)){
@@ -104545,11 +104599,11 @@
104545104599
if( !sqlite3SafetyCheckOk(db) ) return SQLITE_MISUSE_BKPT;
104546104600
#endif
104547104601
sqlite3_mutex_enter(db->mutex);
104548104602
db->xAuth = (sqlite3_xauth)xAuth;
104549104603
db->pAuthArg = pArg;
104550
- sqlite3ExpirePreparedStatements(db);
104604
+ sqlite3ExpirePreparedStatements(db, 0);
104551104605
sqlite3_mutex_leave(db->mutex);
104552104606
return SQLITE_OK;
104553104607
}
104554104608
104555104609
/*
@@ -108209,11 +108263,11 @@
108209108263
if( pTblName ){
108210108264
sqlite3RefillIndex(pParse, pIndex, iMem);
108211108265
sqlite3ChangeCookie(pParse, iDb);
108212108266
sqlite3VdbeAddParseSchemaOp(v, iDb,
108213108267
sqlite3MPrintf(db, "name='%q' AND type='index'", pIndex->zName));
108214
- sqlite3VdbeAddOp0(v, OP_Expire);
108268
+ sqlite3VdbeAddOp2(v, OP_Expire, 0, 1);
108215108269
}
108216108270
108217108271
sqlite3VdbeJumpHere(v, pIndex->tnum);
108218108272
}
108219108273
@@ -131894,11 +131948,11 @@
131894131948
131895131949
assert( sqlite3BtreeHoldsAllMutexes(db) );
131896131950
assert( sqlite3_mutex_held(db->mutex) );
131897131951
131898131952
if( p ){
131899
- sqlite3ExpirePreparedStatements(db);
131953
+ sqlite3ExpirePreparedStatements(db, 0);
131900131954
do {
131901131955
VTable *pNext = p->pNext;
131902131956
sqlite3VtabUnlock(p);
131903131957
p = pNext;
131904131958
}while( p );
@@ -138680,11 +138734,13 @@
138680138734
138681138735
#ifdef SQLITE_ENABLE_STAT3_OR_STAT4
138682138736
Index *p = pLoop->u.btree.pIndex;
138683138737
int nEq = pLoop->u.btree.nEq;
138684138738
138685
- if( p->nSample>0 && nEq<p->nSampleCol ){
138739
+ if( p->nSample>0 && nEq<p->nSampleCol
138740
+ && OptimizationEnabled(pParse->db, SQLITE_Stat34)
138741
+ ){
138686138742
if( nEq==pBuilder->nRecValid ){
138687138743
UnpackedRecord *pRec = pBuilder->pRec;
138688138744
tRowcnt a[2];
138689138745
int nBtm = pLoop->u.btree.nBtm;
138690138746
int nTop = pLoop->u.btree.nTop;
@@ -139828,10 +139884,11 @@
139828139884
tRowcnt nOut = 0;
139829139885
if( nInMul==0
139830139886
&& pProbe->nSample
139831139887
&& pNew->u.btree.nEq<=pProbe->nSampleCol
139832139888
&& ((eOp & WO_IN)==0 || !ExprHasProperty(pTerm->pExpr, EP_xIsSelect))
139889
+ && OptimizationEnabled(db, SQLITE_Stat34)
139833139890
){
139834139891
Expr *pExpr = pTerm->pExpr;
139835139892
if( (eOp & (WO_EQ|WO_ISNULL|WO_IS))!=0 ){
139836139893
testcase( eOp & WO_EQ );
139837139894
testcase( eOp & WO_IS );
@@ -146916,11 +146973,11 @@
146916146973
146917146974
/*
146918146975
** Find the appropriate action for a parser given the non-terminal
146919146976
** look-ahead token iLookAhead.
146920146977
*/
146921
-static int yy_find_reduce_action(
146978
+static YYACTIONTYPE yy_find_reduce_action(
146922146979
YYACTIONTYPE stateno, /* Current state number */
146923146980
YYCODETYPE iLookAhead /* The look-ahead token */
146924146981
){
146925146982
int i;
146926146983
#ifdef YYERRORSYMBOL
@@ -147421,11 +147478,11 @@
147421147478
int yyLookahead, /* Lookahead token, or YYNOCODE if none */
147422147479
sqlite3ParserTOKENTYPE yyLookaheadToken /* Value of the lookahead token */
147423147480
sqlite3ParserCTX_PDECL /* %extra_context */
147424147481
){
147425147482
int yygoto; /* The next state */
147426
- int yyact; /* The next action */
147483
+ YYACTIONTYPE yyact; /* The next action */
147427147484
yyStackEntry *yymsp; /* The top of the parser's stack */
147428147485
int yysize; /* Amount to pop the stack */
147429147486
sqlite3ParserARG_FETCH
147430147487
(void)yyLookahead;
147431147488
(void)yyLookaheadToken;
@@ -148980,16 +149037,16 @@
148980149037
}
148981149038
#endif
148982149039
148983149040
do{
148984149041
assert( yyact==yypParser->yytos->stateno );
148985
- yyact = yy_find_shift_action(yymajor,yyact);
149042
+ yyact = yy_find_shift_action((YYCODETYPE)yymajor,yyact);
148986149043
if( yyact >= YY_MIN_REDUCE ){
148987149044
yyact = yy_reduce(yypParser,yyact-YY_MIN_REDUCE,yymajor,
148988149045
yyminor sqlite3ParserCTX_PARAM);
148989149046
}else if( yyact <= YY_MAX_SHIFTREDUCE ){
148990
- yy_shift(yypParser,yyact,yymajor,yyminor);
149047
+ yy_shift(yypParser,yyact,(YYCODETYPE)yymajor,yyminor);
148991149048
#ifndef YYNOERRORRECOVERY
148992149049
yypParser->yyerrcnt--;
148993149050
#endif
148994149051
break;
148995149052
}else if( yyact==YY_ACCEPT_ACTION ){
@@ -151421,11 +151478,11 @@
151421151478
db->flags |= aFlagOp[i].mask;
151422151479
}else if( onoff==0 ){
151423151480
db->flags &= ~aFlagOp[i].mask;
151424151481
}
151425151482
if( oldFlags!=db->flags ){
151426
- sqlite3ExpirePreparedStatements(db);
151483
+ sqlite3ExpirePreparedStatements(db, 0);
151427151484
}
151428151485
if( pRes ){
151429151486
*pRes = (db->flags & aFlagOp[i].mask)!=0;
151430151487
}
151431151488
rc = SQLITE_OK;
@@ -151864,11 +151921,11 @@
151864151921
}
151865151922
sqlite3VtabRollback(db);
151866151923
sqlite3EndBenignMalloc();
151867151924
151868151925
if( schemaChange ){
151869
- sqlite3ExpirePreparedStatements(db);
151926
+ sqlite3ExpirePreparedStatements(db, 0);
151870151927
sqlite3ResetAllSchemasOfConnection(db);
151871151928
}
151872151929
sqlite3BtreeLeaveAll(db);
151873151930
151874151931
/* Any deferred constraint violations have now been resolved. */
@@ -152319,11 +152376,11 @@
152319152376
sqlite3ErrorWithMsg(db, SQLITE_BUSY,
152320152377
"unable to delete/modify user-function due to active statements");
152321152378
assert( !db->mallocFailed );
152322152379
return SQLITE_BUSY;
152323152380
}else{
152324
- sqlite3ExpirePreparedStatements(db);
152381
+ sqlite3ExpirePreparedStatements(db, 0);
152325152382
}
152326152383
}
152327152384
152328152385
p = sqlite3FindFunction(db, zFunctionName, nArg, (u8)enc, 1);
152329152386
assert(p || db->mallocFailed);
@@ -153094,11 +153151,11 @@
153094153151
if( db->nVdbeActive ){
153095153152
sqlite3ErrorWithMsg(db, SQLITE_BUSY,
153096153153
"unable to delete/modify collation sequence due to active statements");
153097153154
return SQLITE_BUSY;
153098153155
}
153099
- sqlite3ExpirePreparedStatements(db);
153156
+ sqlite3ExpirePreparedStatements(db, 0);
153100153157
153101153158
/* If collation sequence pColl was created directly by a call to
153102153159
** sqlite3_create_collation, and not generated by synthCollSeq(),
153103153160
** then any copies made by synthCollSeq() need to be invalidated.
153104153161
** Also, collation destructor - CollSeq.xDel() - function may need
@@ -194031,16 +194088,18 @@
194031194088
rc = sqlite3_create_function(db, aFunc[i].zName, aFunc[i].nArg,
194032194089
SQLITE_UTF8 | SQLITE_DETERMINISTIC,
194033194090
(void*)&aFunc[i].flag,
194034194091
aFunc[i].xFunc, 0, 0);
194035194092
}
194093
+#ifndef SQLITE_OMIT_WINDOWFUNC
194036194094
for(i=0; i<sizeof(aAgg)/sizeof(aAgg[0]) && rc==SQLITE_OK; i++){
194037194095
rc = sqlite3_create_window_function(db, aAgg[i].zName, aAgg[i].nArg,
194038194096
SQLITE_UTF8 | SQLITE_DETERMINISTIC, 0,
194039194097
aAgg[i].xStep, aAgg[i].xFinal,
194040194098
aAgg[i].xValue, jsonGroupInverse, 0);
194041194099
}
194100
+#endif
194042194101
#ifndef SQLITE_OMIT_VIRTUALTABLE
194043194102
for(i=0; i<sizeof(aMod)/sizeof(aMod[0]) && rc==SQLITE_OK; i++){
194044194103
rc = sqlite3_create_module(db, aMod[i].zName, aMod[i].pModule, 0);
194045194104
}
194046194105
#endif
@@ -196237,11 +196296,11 @@
196237196296
196238196297
/*
196239196298
** Find the appropriate action for a parser given the non-terminal
196240196299
** look-ahead token iLookAhead.
196241196300
*/
196242
-static int fts5yy_find_reduce_action(
196301
+static fts5YYACTIONTYPE fts5yy_find_reduce_action(
196243196302
fts5YYACTIONTYPE stateno, /* Current state number */
196244196303
fts5YYCODETYPE iLookAhead /* The look-ahead token */
196245196304
){
196246196305
int i;
196247196306
#ifdef fts5YYERRORSYMBOL
@@ -196405,11 +196464,11 @@
196405196464
int fts5yyLookahead, /* Lookahead token, or fts5YYNOCODE if none */
196406196465
sqlite3Fts5ParserFTS5TOKENTYPE fts5yyLookaheadToken /* Value of the lookahead token */
196407196466
sqlite3Fts5ParserCTX_PDECL /* %extra_context */
196408196467
){
196409196468
int fts5yygoto; /* The next state */
196410
- int fts5yyact; /* The next action */
196469
+ fts5YYACTIONTYPE fts5yyact; /* The next action */
196411196470
fts5yyStackEntry *fts5yymsp; /* The top of the parser's stack */
196412196471
int fts5yysize; /* Amount to pop the stack */
196413196472
sqlite3Fts5ParserARG_FETCH
196414196473
(void)fts5yyLookahead;
196415196474
(void)fts5yyLookaheadToken;
@@ -196761,16 +196820,16 @@
196761196820
}
196762196821
#endif
196763196822
196764196823
do{
196765196824
assert( fts5yyact==fts5yypParser->fts5yytos->stateno );
196766
- fts5yyact = fts5yy_find_shift_action(fts5yymajor,fts5yyact);
196825
+ fts5yyact = fts5yy_find_shift_action((fts5YYCODETYPE)fts5yymajor,fts5yyact);
196767196826
if( fts5yyact >= fts5YY_MIN_REDUCE ){
196768196827
fts5yyact = fts5yy_reduce(fts5yypParser,fts5yyact-fts5YY_MIN_REDUCE,fts5yymajor,
196769196828
fts5yyminor sqlite3Fts5ParserCTX_PARAM);
196770196829
}else if( fts5yyact <= fts5YY_MAX_SHIFTREDUCE ){
196771
- fts5yy_shift(fts5yypParser,fts5yyact,fts5yymajor,fts5yyminor);
196830
+ fts5yy_shift(fts5yypParser,fts5yyact,(fts5YYCODETYPE)fts5yymajor,fts5yyminor);
196772196831
#ifndef fts5YYNOERRORRECOVERY
196773196832
fts5yypParser->fts5yyerrcnt--;
196774196833
#endif
196775196834
break;
196776196835
}else if( fts5yyact==fts5YY_ACCEPT_ACTION ){
@@ -211513,11 +211572,11 @@
211513211572
int nArg, /* Number of args */
211514211573
sqlite3_value **apUnused /* Function arguments */
211515211574
){
211516211575
assert( nArg==0 );
211517211576
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);
211519211578
}
211520211579
211521211580
static int fts5Init(sqlite3 *db){
211522211581
static const sqlite3_module fts5Mod = {
211523211582
/* iVersion */ 2,
@@ -214799,11 +214858,11 @@
214799214858
int iTbl = 0;
214800214859
while( i<128 ){
214801214860
int bToken = aArray[ aFts5UnicodeData[iTbl] & 0x1F ];
214802214861
int n = (aFts5UnicodeData[iTbl] >> 5) + i;
214803214862
for(; i<128 && i<n; i++){
214804
- aAscii[i] = bToken;
214863
+ aAscii[i] = (u8)bToken;
214805214864
}
214806214865
iTbl++;
214807214866
}
214808214867
}
214809214868
@@ -216223,12 +216282,12 @@
216223216282
}
216224216283
#endif /* SQLITE_CORE */
216225216284
#endif /* !defined(SQLITE_CORE) || defined(SQLITE_ENABLE_STMTVTAB) */
216226216285
216227216286
/************** End of stmt.c ************************************************/
216228
-#if __LINE__!=216228
216287
+#if __LINE__!=216287
216229216288
#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"
216231216290
#endif
216232216291
/* Return the source-id for this library */
216233216292
SQLITE_API const char *sqlite3_sourceid(void){ return SQLITE_SOURCE_ID; }
216234216293
/************************** End of sqlite3.c ******************************/
216235216294
--- 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 @@
123123
** [sqlite3_libversion_number()], [sqlite3_sourceid()],
124124
** [sqlite_version()] and [sqlite_source_id()].
125125
*/
126126
#define SQLITE_VERSION "3.25.0"
127127
#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"
129129
130130
/*
131131
** CAPI3REF: Run-Time Library Version Numbers
132132
** KEYWORDS: sqlite3_version sqlite3_sourceid
133133
**
@@ -885,11 +885,12 @@
885885
** interrogated. The zDbName parameter is ignored.
886886
**
887887
** <li>[[SQLITE_FCNTL_PERSIST_WAL]]
888888
** ^The [SQLITE_FCNTL_PERSIST_WAL] opcode is used to set or query the
889889
** 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
891892
** are automatically deleted when the latest connection to the database
892893
** closes. Setting persistent WAL mode causes those files to persist after
893894
** close. Persisting the files is useful when other processes that do not
894895
** have write permission on the directory containing the database file want
895896
** to read the database file, as the WAL and shared memory files must exist
@@ -8958,11 +8959,10 @@
89588959
SQLITE_API int sqlite3_system_errno(sqlite3*);
89598960
89608961
/*
89618962
** CAPI3REF: Database Snapshot
89628963
** KEYWORDS: {snapshot} {sqlite3_snapshot}
8963
-** EXPERIMENTAL
89648964
**
89658965
** An instance of the snapshot object records the state of a [WAL mode]
89668966
** database for some specific point in history.
89678967
**
89688968
** In [WAL mode], multiple [database connections] that are open on the
@@ -8975,23 +8975,18 @@
89758975
**
89768976
** The sqlite3_snapshot object records state information about an historical
89778977
** version of the database file so that it is possible to later open a new read
89788978
** transaction that sees that historical version of the database rather than
89798979
** 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()].
89858980
*/
89868981
typedef struct sqlite3_snapshot {
89878982
unsigned char hidden[48];
89888983
} sqlite3_snapshot;
89898984
89908985
/*
89918986
** CAPI3REF: Record A Database Snapshot
8992
-** EXPERIMENTAL
8987
+** CONSTRUCTOR: sqlite3_snapshot
89938988
**
89948989
** ^The [sqlite3_snapshot_get(D,S,P)] interface attempts to make a
89958990
** new [sqlite3_snapshot] object that records the current state of
89968991
** schema S in database connection D. ^On success, the
89978992
** [sqlite3_snapshot_get(D,S,P)] interface writes a pointer to the newly
@@ -9026,21 +9021,21 @@
90269021
** The [sqlite3_snapshot] object returned from a successful call to
90279022
** [sqlite3_snapshot_get()] must be freed using [sqlite3_snapshot_free()]
90289023
** to avoid a memory leak.
90299024
**
90309025
** 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.
90329027
*/
90339028
SQLITE_API SQLITE_EXPERIMENTAL int sqlite3_snapshot_get(
90349029
sqlite3 *db,
90359030
const char *zSchema,
90369031
sqlite3_snapshot **ppSnapshot
90379032
);
90389033
90399034
/*
90409035
** CAPI3REF: Start a read transaction on an historical snapshot
9041
-** EXPERIMENTAL
9036
+** METHOD: sqlite3_snapshot
90429037
**
90439038
** ^The [sqlite3_snapshot_open(D,S,P)] interface starts a
90449039
** read transaction for schema S of
90459040
** [database connection] D such that the read transaction
90469041
** refers to historical [snapshot] P, rather than the most
@@ -9064,34 +9059,34 @@
90649059
** after the most recent I/O on the database connection.)^
90659060
** (Hint: Run "[PRAGMA application_id]" against a newly opened
90669061
** database connection in order to make it ready to use snapshots.)
90679062
**
90689063
** 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.
90709065
*/
90719066
SQLITE_API SQLITE_EXPERIMENTAL int sqlite3_snapshot_open(
90729067
sqlite3 *db,
90739068
const char *zSchema,
90749069
sqlite3_snapshot *pSnapshot
90759070
);
90769071
90779072
/*
90789073
** CAPI3REF: Destroy a snapshot
9079
-** EXPERIMENTAL
9074
+** DESTRUCTOR: sqlite3_snapshot
90809075
**
90819076
** ^The [sqlite3_snapshot_free(P)] interface destroys [sqlite3_snapshot] P.
90829077
** The application must eventually free every [sqlite3_snapshot] object
90839078
** using this routine to avoid a memory leak.
90849079
**
90859080
** 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.
90879082
*/
90889083
SQLITE_API SQLITE_EXPERIMENTAL void sqlite3_snapshot_free(sqlite3_snapshot*);
90899084
90909085
/*
90919086
** CAPI3REF: Compare the ages of two snapshot handles.
9092
-** EXPERIMENTAL
9087
+** METHOD: sqlite3_snapshot
90939088
**
90949089
** The sqlite3_snapshot_cmp(P1, P2) interface is used to compare the ages
90959090
** of two valid snapshot handles.
90969091
**
90979092
** If the two snapshot handles are not associated with the same database
@@ -9106,35 +9101,41 @@
91069101
** is undefined.
91079102
**
91089103
** Otherwise, this API returns a negative value if P1 refers to an older
91099104
** snapshot than P2, zero if the two handles refer to the same database
91109105
** 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.
91119109
*/
91129110
SQLITE_API SQLITE_EXPERIMENTAL int sqlite3_snapshot_cmp(
91139111
sqlite3_snapshot *p1,
91149112
sqlite3_snapshot *p2
91159113
);
91169114
91179115
/*
91189116
** 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
91309128
** of database handle db and make all valid snapshots available to
91319129
** 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
91339131
** database.
91349132
**
91359133
** 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.
91369137
*/
91379138
SQLITE_API SQLITE_EXPERIMENTAL int sqlite3_snapshot_recover(sqlite3 *db, const char *zDb);
91389139
91399140
/*
91409141
** CAPI3REF: Serialize a database
91419142
--- 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 @@
433433
Th_Unstore("title"); /* Avoid collisions with ticket field names */
434434
cgi_destination(CGI_BODY);
435435
g.cgiOutput = 1;
436436
headerHasBeenGenerated = 1;
437437
sideboxUsed = 0;
438
+ if( g.perm.Debug && P("showqp") ){
439
+ @ <div class="debug">
440
+ cgi_print_all(0, 0);
441
+ @ </div>
442
+ }
438443
}
439444
440445
#if INTERFACE
441446
/* Allowed parameters for style_adunit() */
442447
#define ADUNIT_OFF 0x0001 /* Do not allow ads on this page */
@@ -502,15 +507,36 @@
502507
** Generate code to load a single javascript file
503508
*/
504509
void style_load_one_js_file(const char *zFile){
505510
@ <script src='%R/builtin/%s(zFile)?id=%S(MANIFEST_UUID)'></script>
506511
}
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
+}
507532
508533
/*
509534
** Generate code to load all required javascript files.
510535
*/
511536
static void style_load_all_js_files(void){
537
+ int i;
512538
if( needHrefJs ){
513539
int nDelay = db_get_int("auto-hyperlink-delay",0);
514540
int bMouseover;
515541
/* Load up the page data */
516542
bMouseover = (!g.isHuman || db_get_boolean("auto-hyperlink-ishuman",0))
@@ -523,10 +549,13 @@
523549
style_load_one_js_file("sorttable.js");
524550
}
525551
if( needGraphJs ){
526552
style_load_one_js_file("graph.js");
527553
}
554
+ for(i=0; i<nJsToLoad; i++){
555
+ style_load_one_js_file(azJsToLoad[i]);
556
+ }
528557
}
529558
530559
/*
531560
** Draw the footer at the bottom of the page.
532561
*/
@@ -864,22 +893,80 @@
864893
}
865894
blob_init(&out, zTxt, -1);
866895
cgi_set_content(&out);
867896
}
868897
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
+}
869931
870932
/*
871933
** WEBPAGE: test_env
872934
**
873935
** Display CGI-variables and other aspects of the run-time
874936
** environment, for debugging and trouble-shooting purposes.
875937
*/
876938
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, ...){
878963
int i;
879964
int showAll;
880
- char zCap[30];
965
+ char *zErr = 0;
966
+ int isAuth = 0;
967
+ char zCap[100];
881968
static const char *const azCgiVars[] = {
882969
"COMSPEC", "DOCUMENT_ROOT", "GATEWAY_INTERFACE",
883970
"HTTP_ACCEPT", "HTTP_ACCEPT_CHARSET", "HTTP_ACCEPT_ENCODING",
884971
"HTTP_ACCEPT_LANGUAGE", "HTTP_AUTHENICATION",
885972
"HTTP_CONNECTION", "HTTP_HOST",
@@ -895,72 +982,88 @@
895982
"FOSSIL_TCL_PATH", "TH1_DELETE_INTERP", "TH1_ENABLE_DOCS",
896983
"TH1_ENABLE_HOOKS", "TH1_ENABLE_TCL", "REMOTE_HOST"
897984
};
898985
899986
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;
903989
}
904990
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
+ }
9541043
}
9551044
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");
9571056
}
9581057
9591058
/*
960
-** WEBPAGE: honeypot
961
-** This page is a honeypot for spiders and bots.
1059
+** Generate a webpage for a webpage_assert().
9621060
*/
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);
9661065
}
1066
+
1067
+#if INTERFACE
1068
+# define webpage_assert(T) if(!(T)){webpage_assert_page(__FILE__,__LINE__,#T);}
1069
+#endif
9671070
--- 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 @@
574574
cgi_printf("technote:&nbsp;");
575575
hyperlink_to_event_tagid(tagid<0?-tagid:tagid);
576576
}else{
577577
cgi_printf("artifact:&nbsp;%z%S</a> ",href("%R/info/%!S",zUuid),zUuid);
578578
}
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'){
580580
cgi_printf("artifact:&nbsp;%z%S</a> ",href("%R/info/%!S",zUuid),zUuid);
581581
}
582582
583583
if( g.perm.Hyperlink && fossil_strcmp(zDispUser, zThisUser)!=0 ){
584584
char *zLink = mprintf("%R/timeline?u=%h&c=%t&nd&n=200", zDispUser, zDate);
@@ -1031,11 +1031,11 @@
10311031
** set the y= parameter that determines which elements to display
10321032
** on the timeline.
10331033
*/
10341034
static void timeline_y_submenu(int isDisabled){
10351035
static int i = 0;
1036
- static const char *az[12];
1036
+ static const char *az[14];
10371037
if( i==0 ){
10381038
az[0] = "all";
10391039
az[1] = "Any Type";
10401040
i = 2;
10411041
if( g.perm.Read ){
@@ -1053,10 +1053,14 @@
10531053
az[i++] = "Tickets";
10541054
}
10551055
if( g.perm.RdWiki ){
10561056
az[i++] = "w";
10571057
az[i++] = "Wiki";
1058
+ }
1059
+ if( g.perm.RdForum ){
1060
+ az[i++] = "f";
1061
+ az[i++] = "Forum";
10581062
}
10591063
assert( i<=count(az) );
10601064
}
10611065
if( i>2 ){
10621066
style_submenu_multichoice("y", i/2, az, isDisabled);
@@ -1357,11 +1361,11 @@
13571361
** r=TAG Show check-ins related to TAG, equivalent to t=TAG&rel
13581362
** rel Show related check-ins as well as those matching t=TAG
13591363
** mionly Limit rel to show ancestors but not descendants
13601364
** ms=MATCHSTYLE Set tag match style to EXACT, GLOB, LIKE, REGEXP
13611365
** 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'.
13631367
** ss=VIEWSTYLE c: "Compact" v: "Verbose" m: "Modern" j: "Columnar"
13641368
** advm Use the "Advanced" or "Busy" menu design.
13651369
** ng No Graph.
13661370
** nd Do not highlight the focus check-in
13671371
** v Show details of files changed
@@ -1477,11 +1481,11 @@
14771481
pd_rid = name_to_typed_rid(P("dp"),"ci");
14781482
if( pd_rid ){
14791483
p_rid = d_rid = pd_rid;
14801484
}
14811485
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)
14831487
|| (bisectOnly && !g.perm.Setup)
14841488
){
14851489
login_needed(g.anon.Read && g.anon.RdTkt && g.anon.RdWiki);
14861490
return;
14871491
}
@@ -1854,10 +1858,11 @@
18541858
if( (zType[0]=='w' && !g.perm.RdWiki)
18551859
|| (zType[0]=='t' && !g.perm.RdTkt)
18561860
|| (zType[0]=='e' && !g.perm.RdWiki)
18571861
|| (zType[0]=='c' && !g.perm.Read)
18581862
|| (zType[0]=='g' && !g.perm.Read)
1863
+ || (zType[0]=='f' && !g.perm.RdForum)
18591864
){
18601865
zType = "all";
18611866
}
18621867
if( zType[0]=='a' ){
18631868
if( !g.perm.Read || !g.perm.RdWiki || !g.perm.RdTkt ){
@@ -1872,10 +1877,14 @@
18721877
cSep = ',';
18731878
}
18741879
if( g.perm.RdTkt ){
18751880
blob_append_sql(&cond, "%c't'", cSep);
18761881
cSep = ',';
1882
+ }
1883
+ if( g.perm.RdForum ){
1884
+ blob_append_sql(&cond, "%c'f'", cSep);
1885
+ cSep = ',';
18771886
}
18781887
blob_append_sql(&cond, ")");
18791888
}
18801889
}else{ /* zType!="all" */
18811890
blob_append_sql(&cond, " AND event.type=%Q", zType);
@@ -1887,10 +1896,12 @@
18871896
zEType = "ticket change";
18881897
}else if( zType[0]=='e' ){
18891898
zEType = "technical note";
18901899
}else if( zType[0]=='g' ){
18911900
zEType = "tag";
1901
+ }else if( zType[0]=='f' ){
1902
+ zEType = "forum post";
18921903
}
18931904
}
18941905
if( zUser ){
18951906
int n = db_int(0,"SELECT count(*) FROM event"
18961907
" WHERE user=%Q OR euser=%Q", zUser, zUser);
18971908
--- src/timeline.c
+++ src/timeline.c
@@ -574,11 +574,11 @@
574 cgi_printf("technote:&nbsp;");
575 hyperlink_to_event_tagid(tagid<0?-tagid:tagid);
576 }else{
577 cgi_printf("artifact:&nbsp;%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:&nbsp;%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:&nbsp;");
575 hyperlink_to_event_tagid(tagid<0?-tagid:tagid);
576 }else{
577 cgi_printf("artifact:&nbsp;%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:&nbsp;%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 @@
708708
@ <tr><td align="left">
709709
if( d==2 ){
710710
@ <input type="submit" name="read" value="Undelete">
711711
@ <input type="submit" name="purge" value="Delete Permanently">
712712
}else{
713
- @ <input type="submit" name="trash", value="Delete">
713
+ @ <input type="submit" name="trash" value="Delete">
714714
if( d!=1 ){
715715
@ <input type="submit" name="unread" value="Mark as unread">
716716
}
717717
@ <input type="submit" name="read" value="Mark as read">
718718
}
@@ -732,10 +732,11 @@
732732
while( db_step(&q)==SQLITE_ROW ){
733733
const char *zId = db_column_text(&q,0);
734734
const char *zFrom = db_column_text(&q, 1);
735735
const char *zDate = db_column_text(&q, 2);
736736
const char *zSubject = db_column_text(&q, 4);
737
+ if( zSubject==0 || zSubject[0]==0 ) zSubject = "(no subject)";
737738
@ <tr>
738739
@ <td><input type="checkbox" class="webmailckbox" name="e%s(zId)"></td>
739740
@ <td>%h(zFrom)</td>
740741
@ <td><a href="%s(url_render(&url,"id",zId,0,0))">%h(zSubject)</a> \
741742
@ %s(zDate)</td>
742743
--- 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 @@
33
** Copyright (c) 2008 Stephan Beal
44
**
55
** This program is free software; you can redistribute it and/or
66
** modify it under the terms of the Simplified BSD License (also
77
** known as the "2-Clause License" or "FreeBSD License".)
8
-
8
+**
99
** This program is distributed in the hope that it will be useful,
1010
** but without any warranty; without even the implied warranty of
1111
** merchantability or fitness for a particular purpose.
1212
**
1313
** Author contact information:
@@ -167,11 +167,11 @@
167167
Blob tail = BLOB_INITIALIZER;
168168
markdown_to_html(pWiki, 0, &tail);
169169
@ %s(blob_str(&tail))
170170
blob_reset(&tail);
171171
}else{
172
- @ <pre>
172
+ @ <pre class='textPlain'>
173173
@ %h(blob_str(pWiki))
174174
@ </pre>
175175
}
176176
}
177177
@@ -426,11 +426,11 @@
426426
}
427427
428428
/*
429429
** Write a wiki artifact into the repository
430430
*/
431
-static void wiki_put(Blob *pWiki, int parent, int needMod){
431
+int wiki_put(Blob *pWiki, int parent, int needMod){
432432
int nrid;
433433
if( !needMod ){
434434
nrid = content_put_ex(pWiki, 0, 0, 0, 0);
435435
if( parent) content_deltify(parent, &nrid, 1, 0);
436436
}else{
@@ -439,10 +439,11 @@
439439
db_multi_exec("INSERT INTO modreq(objid) VALUES(%d)", nrid);
440440
}
441441
db_multi_exec("INSERT OR IGNORE INTO unsent VALUES(%d)", nrid);
442442
db_multi_exec("INSERT OR IGNORE INTO unclustered VALUES(%d);", nrid);
443443
manifest_crosslink(nrid, pWiki, MC_NONE);
444
+ return nrid;
444445
}
445446
446447
/*
447448
** Output a selection box from which the user can select the
448449
** wiki mimetype.
449450
--- 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 @@
13061306
&& blob_is_hname(&xfer.aToken[2])
13071307
){
13081308
const char *zPCode;
13091309
zPCode = db_get("project-code", 0);
13101310
if( zPCode==0 ){
1311
- fossil_panic("missing project code");
1311
+ fossil_fatal("missing project code");
13121312
}
13131313
if( !blob_eq_str(&xfer.aToken[2], zPCode, -1) ){
13141314
cgi_reset_content();
13151315
@ error wrong\sproject
13161316
nErr++;
13171317
--- 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 @@
2828
2929
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
3030
3131
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
3232
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
3434
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
3636
3737
3838
RC=$(DMDIR)\bin\rcc
3939
RCFLAGS=-32 -w1 -I$(SRCDIR) /D__DMC__
4040
@@ -49,11 +49,11 @@
4949
5050
$(OBJDIR)\fossil.res: $B\win\fossil.rc
5151
$(RC) $(RCFLAGS) -o$@ $**
5252
5353
$(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 > $@
5555
+echo fossil >> $@
5656
+echo fossil >> $@
5757
+echo $(LIBS) >> $@
5858
+echo. >> $@
5959
+echo fossil >> $@
@@ -200,10 +200,16 @@
200200
$(OBJDIR)\cache$O : cache_.c cache.h
201201
$(TCC) -o$@ -c cache_.c
202202
203203
cache_.c : $(SRCDIR)\cache.c
204204
+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 $** > $@
205211
206212
$(OBJDIR)\captcha$O : captcha_.c captcha.h
207213
$(TCC) -o$@ -c captcha_.c
208214
209215
captcha_.c : $(SRCDIR)\captcha.c
@@ -928,7 +934,7 @@
928934
929935
zip_.c : $(SRCDIR)\zip.c
930936
+translate$E $** > $@
931937
932938
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
934940
@copy /Y nul: headers
935941
--- 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
--- win/Makefile.mingw
+++ win/Makefile.mingw
@@ -448,10 +448,11 @@
448448
$(SRCDIR)/branch.c \
449449
$(SRCDIR)/browse.c \
450450
$(SRCDIR)/builtin.c \
451451
$(SRCDIR)/bundle.c \
452452
$(SRCDIR)/cache.c \
453
+ $(SRCDIR)/capabilities.c \
453454
$(SRCDIR)/captcha.c \
454455
$(SRCDIR)/cgi.c \
455456
$(SRCDIR)/checkin.c \
456457
$(SRCDIR)/checkout.c \
457458
$(SRCDIR)/clearsign.c \
@@ -628,10 +629,11 @@
628629
$(SRCDIR)/../skins/xekri/details.txt \
629630
$(SRCDIR)/../skins/xekri/footer.txt \
630631
$(SRCDIR)/../skins/xekri/header.txt \
631632
$(SRCDIR)/ci_edit.js \
632633
$(SRCDIR)/diff.tcl \
634
+ $(SRCDIR)/forum.js \
633635
$(SRCDIR)/graph.js \
634636
$(SRCDIR)/href.js \
635637
$(SRCDIR)/login.js \
636638
$(SRCDIR)/markdown.md \
637639
$(SRCDIR)/menu.js \
@@ -654,10 +656,11 @@
654656
$(OBJDIR)/branch_.c \
655657
$(OBJDIR)/browse_.c \
656658
$(OBJDIR)/builtin_.c \
657659
$(OBJDIR)/bundle_.c \
658660
$(OBJDIR)/cache_.c \
661
+ $(OBJDIR)/capabilities_.c \
659662
$(OBJDIR)/captcha_.c \
660663
$(OBJDIR)/cgi_.c \
661664
$(OBJDIR)/checkin_.c \
662665
$(OBJDIR)/checkout_.c \
663666
$(OBJDIR)/clearsign_.c \
@@ -789,10 +792,11 @@
789792
$(OBJDIR)/branch.o \
790793
$(OBJDIR)/browse.o \
791794
$(OBJDIR)/builtin.o \
792795
$(OBJDIR)/bundle.o \
793796
$(OBJDIR)/cache.o \
797
+ $(OBJDIR)/capabilities.o \
794798
$(OBJDIR)/captcha.o \
795799
$(OBJDIR)/cgi.o \
796800
$(OBJDIR)/checkin.o \
797801
$(OBJDIR)/checkout.o \
798802
$(OBJDIR)/clearsign.o \
@@ -1143,10 +1147,11 @@
11431147
$(OBJDIR)/branch_.c:$(OBJDIR)/branch.h \
11441148
$(OBJDIR)/browse_.c:$(OBJDIR)/browse.h \
11451149
$(OBJDIR)/builtin_.c:$(OBJDIR)/builtin.h \
11461150
$(OBJDIR)/bundle_.c:$(OBJDIR)/bundle.h \
11471151
$(OBJDIR)/cache_.c:$(OBJDIR)/cache.h \
1152
+ $(OBJDIR)/capabilities_.c:$(OBJDIR)/capabilities.h \
11481153
$(OBJDIR)/captcha_.c:$(OBJDIR)/captcha.h \
11491154
$(OBJDIR)/cgi_.c:$(OBJDIR)/cgi.h \
11501155
$(OBJDIR)/checkin_.c:$(OBJDIR)/checkin.h \
11511156
$(OBJDIR)/checkout_.c:$(OBJDIR)/checkout.h \
11521157
$(OBJDIR)/clearsign_.c:$(OBJDIR)/clearsign.h \
@@ -1368,10 +1373,18 @@
13681373
13691374
$(OBJDIR)/cache.o: $(OBJDIR)/cache_.c $(OBJDIR)/cache.h $(SRCDIR)/config.h
13701375
$(XTCC) -o $(OBJDIR)/cache.o -c $(OBJDIR)/cache_.c
13711376
13721377
$(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
13731386
13741387
$(OBJDIR)/captcha_.c: $(SRCDIR)/captcha.c $(TRANSLATE)
13751388
$(TRANSLATE) $(SRCDIR)/captcha.c >$@
13761389
13771390
$(OBJDIR)/captcha.o: $(OBJDIR)/captcha_.c $(OBJDIR)/captcha.h $(SRCDIR)/config.h
13781391
--- 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
--- win/Makefile.msc
+++ win/Makefile.msc
@@ -390,10 +390,11 @@
390390
branch_.c \
391391
browse_.c \
392392
builtin_.c \
393393
bundle_.c \
394394
cache_.c \
395
+ capabilities_.c \
395396
captcha_.c \
396397
cgi_.c \
397398
checkin_.c \
398399
checkout_.c \
399400
clearsign_.c \
@@ -569,10 +570,11 @@
569570
$(SRCDIR)\..\skins\xekri\details.txt \
570571
$(SRCDIR)\..\skins\xekri\footer.txt \
571572
$(SRCDIR)\..\skins\xekri\header.txt \
572573
$(SRCDIR)\ci_edit.js \
573574
$(SRCDIR)\diff.tcl \
575
+ $(SRCDIR)\forum.js \
574576
$(SRCDIR)\graph.js \
575577
$(SRCDIR)\href.js \
576578
$(SRCDIR)\login.js \
577579
$(SRCDIR)\markdown.md \
578580
$(SRCDIR)\menu.js \
@@ -594,10 +596,11 @@
594596
$(OX)\branch$O \
595597
$(OX)\browse$O \
596598
$(OX)\builtin$O \
597599
$(OX)\bundle$O \
598600
$(OX)\cache$O \
601
+ $(OX)\capabilities$O \
599602
$(OX)\captcha$O \
600603
$(OX)\cgi$O \
601604
$(OX)\checkin$O \
602605
$(OX)\checkout$O \
603606
$(OX)\clearsign$O \
@@ -788,10 +791,11 @@
788791
echo $(OX)\branch.obj >> $@
789792
echo $(OX)\browse.obj >> $@
790793
echo $(OX)\builtin.obj >> $@
791794
echo $(OX)\bundle.obj >> $@
792795
echo $(OX)\cache.obj >> $@
796
+ echo $(OX)\capabilities.obj >> $@
793797
echo $(OX)\captcha.obj >> $@
794798
echo $(OX)\cgi.obj >> $@
795799
echo $(OX)\checkin.obj >> $@
796800
echo $(OX)\checkout.obj >> $@
797801
echo $(OX)\clearsign.obj >> $@
@@ -1104,10 +1108,16 @@
11041108
$(OX)\cache$O : cache_.c cache.h
11051109
$(TCC) /Fo$@ -c cache_.c
11061110
11071111
cache_.c : $(SRCDIR)\cache.c
11081112
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 $** > $@
11091119
11101120
$(OX)\captcha$O : captcha_.c captcha.h
11111121
$(TCC) /Fo$@ -c captcha_.c
11121122
11131123
captcha_.c : $(SRCDIR)\captcha.c
@@ -1847,10 +1857,11 @@
18471857
branch_.c:branch.h \
18481858
browse_.c:browse.h \
18491859
builtin_.c:builtin.h \
18501860
bundle_.c:bundle.h \
18511861
cache_.c:cache.h \
1862
+ capabilities_.c:capabilities.h \
18521863
captcha_.c:captcha.h \
18531864
cgi_.c:cgi.h \
18541865
checkin_.c:checkin.h \
18551866
checkout_.c:checkout.h \
18561867
clearsign_.c:clearsign.h \
18571868
--- 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
--- www/fileformat.wiki
+++ www/fileformat.wiki
@@ -71,13 +71,14 @@
7171
<li> [#ctrl | Control Artifacts] </li>
7272
<li> [#wikichng | Wiki Pages] </li>
7373
<li> [#tktchng | Ticket Changes] </li>
7474
<li> [#attachment | Attachments] </li>
7575
<li> [#event | TechNotes] </li>
76
+<li> [#forum | Forum Posts] </li>
7677
</ul>
7778
78
-These seven structural artifact types are described in subsections below.
79
+These eight structural artifact types are described in subsections below.
7980
8081
Structural artifacts are ASCII text. The artifact may be PGP clearsigned.
8182
After removal of the PGP clearsign header and suffix (if any) a structural
8283
artifact consists of one or more "cards" separated by a single newline
8384
(ASCII: 0x0a) character. Each card begins with a single
@@ -525,10 +526,84 @@
525526
technote. The format of the W card is exactly the same as for a
526527
[#wikichng | wiki artifact].
527528
528529
The Z card is the required checksum over the rest of the artifact.
529530
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
+
530605
531606
<a name="summary"></a>
532607
<h2>3.0 Card Summary</h2>
533608
534609
The following table summarizes the various kinds of cards that appear
@@ -539,20 +614,21 @@
539614
or more such cards are required.
540615
541616
<table border=1 width="100%">
542617
<tr>
543618
<th rowspan=2 valign=bottom>Card Format</th>
544
-<th colspan=7>Used By</th>
619
+<th colspan=8>Used By</th>
545620
</tr>
546621
<tr>
547622
<th>Manifest</th>
548623
<th>Cluster</th>
549624
<th>Control</th>
550625
<th>Wiki</th>
551626
<th>Ticket</th>
552627
<th>Attachment</th>
553628
<th>Technote</th>
629
+<th>Forum</th>
554630
</tr>
555631
<tr>
556632
<td><b>A</b> <i>filename</i> <i>target</i> ?<i>source</i>?</td>
557633
<td>&nbsp;</td>
558634
<td>&nbsp;</td>
@@ -559,36 +635,39 @@
559635
<td>&nbsp;</td>
560636
<td>&nbsp;</td>
561637
<td>&nbsp;</td>
562638
<td align=center><b>1</b></td>
563639
<td>&nbsp;</td>
640
+<td>&nbsp;</td>
564641
</tr>
565642
<tr>
566643
<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>&nbsp;</td>
568646
<td>&nbsp;</td>
569647
<td>&nbsp;</td>
570648
<td>&nbsp;</td>
571649
<td>&nbsp;</td>
572650
<td>&nbsp;</td>
573651
<td>&nbsp;</td>
574652
</tr>
575
-<tr><td>&nbsp;</td><td colspan='7'>* = Required for delta manifests</td></tr>
576653
<tr>
577654
<td><b>C</b> <i>comment-text</i></td>
578655
<td align=center><b>1</b></td>
579656
<td>&nbsp;</td>
580657
<td>&nbsp;</td>
581658
<td>&nbsp;</td>
582659
<td>&nbsp;</td>
583660
<td align=center><b>0-1</b></td>
584661
<td align=center><b>0-1</b></td>
662
+<td>&nbsp;</td>
585663
</tr>
586664
<tr>
587665
<td><b>D</b> <i>date-time-stamp</i></td>
588666
<td align=center><b>1</b></td>
589667
<td>&nbsp;</td>
668
+<td align=center><b>1</b></td>
590669
<td align=center><b>1</b></td>
591670
<td align=center><b>1</b></td>
592671
<td align=center><b>1</b></td>
593672
<td align=center><b>1</b></td>
594673
<td align=center><b>1</b></td>
@@ -600,10 +679,11 @@
600679
<td>&nbsp;</td>
601680
<td>&nbsp;</td>
602681
<td>&nbsp;</td>
603682
<td>&nbsp;</td>
604683
<td align=center><b>1</b></td>
684
+<td>&nbsp;</td>
605685
</tr>
606686
<tr>
607687
<td><b>F</b> <i>filename</i> ?<i>uuid</i>? ?<i>permissions</i>? ?<i>oldname</i>?</td>
608688
<td align=center><b>0+</b></td>
609689
<td>&nbsp;</td>
@@ -610,18 +690,53 @@
610690
<td>&nbsp;</td>
611691
<td>&nbsp;</td>
612692
<td>&nbsp;</td>
613693
<td>&nbsp;</td>
614694
<td>&nbsp;</td>
695
+<td>&nbsp;</td>
696
+</tr>
697
+<tr>
698
+<td><b>G</b> <i>thread-root</i></td>
699
+<td>&nbsp;</td>
700
+<td>&nbsp;</td>
701
+<td>&nbsp;</td>
702
+<td>&nbsp;</td>
703
+<td>&nbsp;</td>
704
+<td>&nbsp;</td>
705
+<td>&nbsp;</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>&nbsp;</td>
711
+<td>&nbsp;</td>
712
+<td>&nbsp;</td>
713
+<td>&nbsp;</td>
714
+<td>&nbsp;</td>
715
+<td>&nbsp;</td>
716
+<td>&nbsp;</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>&nbsp;</td>
722
+<td>&nbsp;</td>
723
+<td>&nbsp;</td>
724
+<td>&nbsp;</td>
725
+<td>&nbsp;</td>
726
+<td>&nbsp;</td>
727
+<td>&nbsp;</td>
728
+<td align=center><b>0-1</b></td>
615729
</tr>
616730
<tr>
617731
<td><b>J</b> <i>name</i> ?<i>value</i>?</td>
618732
<td>&nbsp;</td>
619733
<td>&nbsp;</td>
620734
<td>&nbsp;</td>
621735
<td>&nbsp;</td>
622736
<td align=center><b>1+</b></td>
737
+<td>&nbsp;</td>
623738
<td>&nbsp;</td>
624739
<td>&nbsp;</td>
625740
</tr>
626741
<tr>
627742
<td><b>K</b> <i>ticket-uuid</i></td>
@@ -630,10 +745,11 @@
630745
<td>&nbsp;</td>
631746
<td>&nbsp;</td>
632747
<td align=center><b>1</b></td>
633748
<td>&nbsp;</td>
634749
<td>&nbsp;</td>
750
+<td>&nbsp;</td>
635751
</tr>
636752
<tr>
637753
<td><b>L</b> <i>wiki-title</i></td>
638754
<td>&nbsp;</td>
639755
<td>&nbsp;</td>
@@ -640,15 +756,17 @@
640756
<td>&nbsp;</td>
641757
<td align=center><b>1</b></td>
642758
<td>&nbsp;</td>
643759
<td>&nbsp;</td>
644760
<td>&nbsp;</td>
761
+<td>&nbsp;</td>
645762
</tr>
646763
<tr>
647764
<td><b>M</b> <i>uuid</i></td>
648765
<td>&nbsp;</td>
649766
<td align=center><b>1+</b></td>
767
+<td>&nbsp;</td>
650768
<td>&nbsp;</td>
651769
<td>&nbsp;</td>
652770
<td>&nbsp;</td>
653771
<td>&nbsp;</td>
654772
<td>&nbsp;</td>
@@ -660,10 +778,11 @@
660778
<td>&nbsp;</td>
661779
<td align=center><b>0-1</b></td>
662780
<td>&nbsp;</td>
663781
<td align=center><b>0-1</b></td>
664782
<td align=center><b>0-1</b></td>
783
+<td align=center><b>0-1</b></td>
665784
</tr>
666785
<tr>
667786
<td><b>P</b> <i>uuid ...</i></td>
668787
<td align=center><b>0-1</b></td>
669788
<td>&nbsp;</td>
@@ -670,14 +789,16 @@
670789
<td>&nbsp;</td>
671790
<td align=center><b>0-1</b></td>
672791
<td>&nbsp;</td>
673792
<td>&nbsp;</td>
674793
<td align=center><b>0-1</b></td>
794
+<td align=center><b>0-1</b></td>
675795
</tr>
676796
<tr>
677797
<td><b>Q</b> (<b>+</b>|<b>-</b>)<i>uuid</i> ?<i>uuid</i>?</td>
678798
<td align=center><b>0+</b></td>
799
+<td>&nbsp;</td>
679800
<td>&nbsp;</td>
680801
<td>&nbsp;</td>
681802
<td>&nbsp;</td>
682803
<td>&nbsp;</td>
683804
<td>&nbsp;</td>
@@ -690,19 +811,21 @@
690811
<td>&nbsp;</td>
691812
<td>&nbsp;</td>
692813
<td>&nbsp;</td>
693814
<td>&nbsp;</td>
694815
<td>&nbsp;</td>
816
+<td>&nbsp;</td>
695817
<tr>
696818
<td><b>T</b> (<b>+</b>|<b>*</b>|<b>-</b>)<i>tagname</i> <i>uuid</i> ?<i>value</i>?</td>
697819
<td align=center><b>0+</b></td>
698820
<td>&nbsp;</td>
699821
<td align=center><b>1+</b></td>
700822
<td>&nbsp;</td>
701823
<td>&nbsp;</td>
702824
<td>&nbsp;</td>
703825
<td align=center><b>0+</b></td>
826
+<td>&nbsp;</td>
704827
</tr>
705828
<tr>
706829
<td><b>U</b> <i>username</i></td>
707830
<td align=center><b>1</b></td>
708831
<td>&nbsp;</td>
@@ -709,10 +832,11 @@
709832
<td align=center><b>1</b></td>
710833
<td align=center><b>1</b></td>
711834
<td align=center><b>1</b></td>
712835
<td align=center><b>0-1</b></td>
713836
<td align=center><b>0-1</b></td>
837
+<td align=center><b>1</b></td>
714838
</tr>
715839
<tr>
716840
<td><b>W</b> <i>size</i> <b>\n</b> <i>text</i> <b>\n</b></td>
717841
<td>&nbsp;</td>
718842
<td>&nbsp;</td>
@@ -719,13 +843,15 @@
719843
<td>&nbsp;</td>
720844
<td align=center><b>1</b></td>
721845
<td>&nbsp;</td>
722846
<td>&nbsp;</td>
723847
<td align=center><b>1</b></td>
848
+<td align=center><b>1</b></td>
724849
</tr>
725850
<tr>
726851
<td><b>Z</b> <i>md5sum</i></td>
852
+<td align=center><b>1</b></td>
727853
<td align=center><b>1</b></td>
728854
<td align=center><b>1</b></td>
729855
<td align=center><b>1</b></td>
730856
<td align=center><b>1</b></td>
731857
<td align=center><b>1</b></td>
732858
--- 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>&nbsp;</td>
558 <td>&nbsp;</td>
@@ -559,36 +635,39 @@
559 <td>&nbsp;</td>
560 <td>&nbsp;</td>
561 <td>&nbsp;</td>
562 <td align=center><b>1</b></td>
563 <td>&nbsp;</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>&nbsp;</td>
569 <td>&nbsp;</td>
570 <td>&nbsp;</td>
571 <td>&nbsp;</td>
572 <td>&nbsp;</td>
573 <td>&nbsp;</td>
574 </tr>
575 <tr><td>&nbsp;</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>&nbsp;</td>
580 <td>&nbsp;</td>
581 <td>&nbsp;</td>
582 <td>&nbsp;</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>&nbsp;</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>&nbsp;</td>
601 <td>&nbsp;</td>
602 <td>&nbsp;</td>
603 <td>&nbsp;</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>&nbsp;</td>
@@ -610,18 +690,53 @@
610 <td>&nbsp;</td>
611 <td>&nbsp;</td>
612 <td>&nbsp;</td>
613 <td>&nbsp;</td>
614 <td>&nbsp;</td>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
615 </tr>
616 <tr>
617 <td><b>J</b> <i>name</i> ?<i>value</i>?</td>
618 <td>&nbsp;</td>
619 <td>&nbsp;</td>
620 <td>&nbsp;</td>
621 <td>&nbsp;</td>
622 <td align=center><b>1+</b></td>
 
623 <td>&nbsp;</td>
624 <td>&nbsp;</td>
625 </tr>
626 <tr>
627 <td><b>K</b> <i>ticket-uuid</i></td>
@@ -630,10 +745,11 @@
630 <td>&nbsp;</td>
631 <td>&nbsp;</td>
632 <td align=center><b>1</b></td>
633 <td>&nbsp;</td>
634 <td>&nbsp;</td>
 
635 </tr>
636 <tr>
637 <td><b>L</b> <i>wiki-title</i></td>
638 <td>&nbsp;</td>
639 <td>&nbsp;</td>
@@ -640,15 +756,17 @@
640 <td>&nbsp;</td>
641 <td align=center><b>1</b></td>
642 <td>&nbsp;</td>
643 <td>&nbsp;</td>
644 <td>&nbsp;</td>
 
645 </tr>
646 <tr>
647 <td><b>M</b> <i>uuid</i></td>
648 <td>&nbsp;</td>
649 <td align=center><b>1+</b></td>
 
650 <td>&nbsp;</td>
651 <td>&nbsp;</td>
652 <td>&nbsp;</td>
653 <td>&nbsp;</td>
654 <td>&nbsp;</td>
@@ -660,10 +778,11 @@
660 <td>&nbsp;</td>
661 <td align=center><b>0-1</b></td>
662 <td>&nbsp;</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>&nbsp;</td>
@@ -670,14 +789,16 @@
670 <td>&nbsp;</td>
671 <td align=center><b>0-1</b></td>
672 <td>&nbsp;</td>
673 <td>&nbsp;</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>&nbsp;</td>
680 <td>&nbsp;</td>
681 <td>&nbsp;</td>
682 <td>&nbsp;</td>
683 <td>&nbsp;</td>
@@ -690,19 +811,21 @@
690 <td>&nbsp;</td>
691 <td>&nbsp;</td>
692 <td>&nbsp;</td>
693 <td>&nbsp;</td>
694 <td>&nbsp;</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>&nbsp;</td>
699 <td align=center><b>1+</b></td>
700 <td>&nbsp;</td>
701 <td>&nbsp;</td>
702 <td>&nbsp;</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>&nbsp;</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>&nbsp;</td>
718 <td>&nbsp;</td>
@@ -719,13 +843,15 @@
719 <td>&nbsp;</td>
720 <td align=center><b>1</b></td>
721 <td>&nbsp;</td>
722 <td>&nbsp;</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>&nbsp;</td>
634 <td>&nbsp;</td>
@@ -559,36 +635,39 @@
635 <td>&nbsp;</td>
636 <td>&nbsp;</td>
637 <td>&nbsp;</td>
638 <td align=center><b>1</b></td>
639 <td>&nbsp;</td>
640 <td>&nbsp;</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>&nbsp;</td>
646 <td>&nbsp;</td>
647 <td>&nbsp;</td>
648 <td>&nbsp;</td>
649 <td>&nbsp;</td>
650 <td>&nbsp;</td>
651 <td>&nbsp;</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>&nbsp;</td>
657 <td>&nbsp;</td>
658 <td>&nbsp;</td>
659 <td>&nbsp;</td>
660 <td align=center><b>0-1</b></td>
661 <td align=center><b>0-1</b></td>
662 <td>&nbsp;</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>&nbsp;</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>&nbsp;</td>
680 <td>&nbsp;</td>
681 <td>&nbsp;</td>
682 <td>&nbsp;</td>
683 <td align=center><b>1</b></td>
684 <td>&nbsp;</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>&nbsp;</td>
@@ -610,18 +690,53 @@
690 <td>&nbsp;</td>
691 <td>&nbsp;</td>
692 <td>&nbsp;</td>
693 <td>&nbsp;</td>
694 <td>&nbsp;</td>
695 <td>&nbsp;</td>
696 </tr>
697 <tr>
698 <td><b>G</b> <i>thread-root</i></td>
699 <td>&nbsp;</td>
700 <td>&nbsp;</td>
701 <td>&nbsp;</td>
702 <td>&nbsp;</td>
703 <td>&nbsp;</td>
704 <td>&nbsp;</td>
705 <td>&nbsp;</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>&nbsp;</td>
711 <td>&nbsp;</td>
712 <td>&nbsp;</td>
713 <td>&nbsp;</td>
714 <td>&nbsp;</td>
715 <td>&nbsp;</td>
716 <td>&nbsp;</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>&nbsp;</td>
722 <td>&nbsp;</td>
723 <td>&nbsp;</td>
724 <td>&nbsp;</td>
725 <td>&nbsp;</td>
726 <td>&nbsp;</td>
727 <td>&nbsp;</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>&nbsp;</td>
733 <td>&nbsp;</td>
734 <td>&nbsp;</td>
735 <td>&nbsp;</td>
736 <td align=center><b>1+</b></td>
737 <td>&nbsp;</td>
738 <td>&nbsp;</td>
739 <td>&nbsp;</td>
740 </tr>
741 <tr>
742 <td><b>K</b> <i>ticket-uuid</i></td>
@@ -630,10 +745,11 @@
745 <td>&nbsp;</td>
746 <td>&nbsp;</td>
747 <td align=center><b>1</b></td>
748 <td>&nbsp;</td>
749 <td>&nbsp;</td>
750 <td>&nbsp;</td>
751 </tr>
752 <tr>
753 <td><b>L</b> <i>wiki-title</i></td>
754 <td>&nbsp;</td>
755 <td>&nbsp;</td>
@@ -640,15 +756,17 @@
756 <td>&nbsp;</td>
757 <td align=center><b>1</b></td>
758 <td>&nbsp;</td>
759 <td>&nbsp;</td>
760 <td>&nbsp;</td>
761 <td>&nbsp;</td>
762 </tr>
763 <tr>
764 <td><b>M</b> <i>uuid</i></td>
765 <td>&nbsp;</td>
766 <td align=center><b>1+</b></td>
767 <td>&nbsp;</td>
768 <td>&nbsp;</td>
769 <td>&nbsp;</td>
770 <td>&nbsp;</td>
771 <td>&nbsp;</td>
772 <td>&nbsp;</td>
@@ -660,10 +778,11 @@
778 <td>&nbsp;</td>
779 <td align=center><b>0-1</b></td>
780 <td>&nbsp;</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>&nbsp;</td>
@@ -670,14 +789,16 @@
789 <td>&nbsp;</td>
790 <td align=center><b>0-1</b></td>
791 <td>&nbsp;</td>
792 <td>&nbsp;</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>&nbsp;</td>
800 <td>&nbsp;</td>
801 <td>&nbsp;</td>
802 <td>&nbsp;</td>
803 <td>&nbsp;</td>
804 <td>&nbsp;</td>
@@ -690,19 +811,21 @@
811 <td>&nbsp;</td>
812 <td>&nbsp;</td>
813 <td>&nbsp;</td>
814 <td>&nbsp;</td>
815 <td>&nbsp;</td>
816 <td>&nbsp;</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>&nbsp;</td>
821 <td align=center><b>1+</b></td>
822 <td>&nbsp;</td>
823 <td>&nbsp;</td>
824 <td>&nbsp;</td>
825 <td align=center><b>0+</b></td>
826 <td>&nbsp;</td>
827 </tr>
828 <tr>
829 <td><b>U</b> <i>username</i></td>
830 <td align=center><b>1</b></td>
831 <td>&nbsp;</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>&nbsp;</td>
842 <td>&nbsp;</td>
@@ -719,13 +843,15 @@
843 <td>&nbsp;</td>
844 <td align=center><b>1</b></td>
845 <td>&nbsp;</td>
846 <td>&nbsp;</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

Keyboard Shortcuts

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