Fossil SCM

Add ability to "close" forum posts.

stephan 2023-06-10 09:16 trunk merge
Commit 673dc38ffb4b444f5a874d4d2112f54026c6fd1ad9fa51fa27c82f8f5f2221d4
+1 -1
--- src/attach.c
+++ src/attach.c
@@ -244,11 +244,11 @@
244244
"INSERT INTO modreq(objid,attachRid) VALUES(%d,%d);",
245245
rid, attachRid
246246
);
247247
}else{
248248
rid = content_put(pAttach);
249
- db_multi_exec("INSERT OR IGNORE INTO unsent VALUES(%d);", rid);
249
+ db_add_unsent(rid);
250250
db_multi_exec("INSERT OR IGNORE INTO unclustered VALUES(%d);", rid);
251251
}
252252
manifest_crosslink(rid, pAttach, MC_NONE);
253253
}
254254
255255
--- src/attach.c
+++ src/attach.c
@@ -244,11 +244,11 @@
244 "INSERT INTO modreq(objid,attachRid) VALUES(%d,%d);",
245 rid, attachRid
246 );
247 }else{
248 rid = content_put(pAttach);
249 db_multi_exec("INSERT OR IGNORE INTO unsent VALUES(%d);", rid);
250 db_multi_exec("INSERT OR IGNORE INTO unclustered VALUES(%d);", rid);
251 }
252 manifest_crosslink(rid, pAttach, MC_NONE);
253 }
254
255
--- src/attach.c
+++ src/attach.c
@@ -244,11 +244,11 @@
244 "INSERT INTO modreq(objid,attachRid) VALUES(%d,%d);",
245 rid, attachRid
246 );
247 }else{
248 rid = content_put(pAttach);
249 db_add_unsent(rid);
250 db_multi_exec("INSERT OR IGNORE INTO unclustered VALUES(%d);", rid);
251 }
252 manifest_crosslink(rid, pAttach, MC_NONE);
253 }
254
255
+5 -5
--- src/branch.c
+++ src/branch.c
@@ -193,17 +193,17 @@
193193
194194
brid = content_put_ex(&branch, 0, 0, 0, isPrivate);
195195
if( brid==0 ){
196196
fossil_fatal("trouble committing manifest: %s", g.zErrMsg);
197197
}
198
- db_multi_exec("INSERT OR IGNORE INTO unsent VALUES(%d)", brid);
198
+ db_add_unsent(brid);
199199
if( manifest_crosslink(brid, &branch, MC_PERMIT_HOOKS)==0 ){
200200
fossil_fatal("%s", g.zErrMsg);
201201
}
202202
assert( blob_is_reset(&branch) );
203203
content_deltify(rootid, &brid, 1, 0);
204
- zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", brid);
204
+ zUuid = rid_to_uuid(brid);
205205
fossil_print("New branch: %s\n", zUuid);
206206
if( g.argc==3 ){
207207
fossil_print(
208208
"\n"
209209
"Note: the local check-out has not been updated to the new\n"
@@ -460,11 +460,11 @@
460460
}else if(manifest_crosslink(newRid, &manifest, 0)==0){
461461
fossil_fatal("Crosslinking error: %s", g.zErrMsg);
462462
}
463463
fossil_print("Saved new control artifact %z (RID %d).\n",
464464
rid_to_uuid(newRid), newRid);
465
- db_multi_exec("INSERT OR IGNORE INTO unsent VALUES(%d)", newRid);
465
+ db_add_unsent(newRid);
466466
if(fDryRun){
467467
fossil_print("Dry-run mode: rolling back new artifact.\n");
468468
assert(0!=doRollback);
469469
}
470470
}
@@ -719,12 +719,12 @@
719719
branch_prepare_list_query(&q, brFlags, zBrNameGlob, nLimit);
720720
while( db_step(&q)==SQLITE_ROW ){
721721
const char *zBr = db_column_text(&q, 0);
722722
int isPriv = zCurrent!=0 && db_column_int(&q, 1)==1;
723723
int isCur = zCurrent!=0 && fossil_strcmp(zCurrent,zBr)==0;
724
- fossil_print("%s%s%s\n",
725
- ( (brFlags & BRL_PRIVATE) ? " " : ( isPriv ? "#" : " ") ),
724
+ fossil_print("%s%s%s\n",
725
+ ( (brFlags & BRL_PRIVATE) ? " " : ( isPriv ? "#" : " ") ),
726726
(isCur ? "* " : " "), zBr);
727727
}
728728
db_finalize(&q);
729729
}else if( strncmp(zCmd,"new",n)==0 ){
730730
branch_new();
731731
--- src/branch.c
+++ src/branch.c
@@ -193,17 +193,17 @@
193
194 brid = content_put_ex(&branch, 0, 0, 0, isPrivate);
195 if( brid==0 ){
196 fossil_fatal("trouble committing manifest: %s", g.zErrMsg);
197 }
198 db_multi_exec("INSERT OR IGNORE INTO unsent VALUES(%d)", brid);
199 if( manifest_crosslink(brid, &branch, MC_PERMIT_HOOKS)==0 ){
200 fossil_fatal("%s", g.zErrMsg);
201 }
202 assert( blob_is_reset(&branch) );
203 content_deltify(rootid, &brid, 1, 0);
204 zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", brid);
205 fossil_print("New branch: %s\n", zUuid);
206 if( g.argc==3 ){
207 fossil_print(
208 "\n"
209 "Note: the local check-out has not been updated to the new\n"
@@ -460,11 +460,11 @@
460 }else if(manifest_crosslink(newRid, &manifest, 0)==0){
461 fossil_fatal("Crosslinking error: %s", g.zErrMsg);
462 }
463 fossil_print("Saved new control artifact %z (RID %d).\n",
464 rid_to_uuid(newRid), newRid);
465 db_multi_exec("INSERT OR IGNORE INTO unsent VALUES(%d)", newRid);
466 if(fDryRun){
467 fossil_print("Dry-run mode: rolling back new artifact.\n");
468 assert(0!=doRollback);
469 }
470 }
@@ -719,12 +719,12 @@
719 branch_prepare_list_query(&q, brFlags, zBrNameGlob, nLimit);
720 while( db_step(&q)==SQLITE_ROW ){
721 const char *zBr = db_column_text(&q, 0);
722 int isPriv = zCurrent!=0 && db_column_int(&q, 1)==1;
723 int isCur = zCurrent!=0 && fossil_strcmp(zCurrent,zBr)==0;
724 fossil_print("%s%s%s\n",
725 ( (brFlags & BRL_PRIVATE) ? " " : ( isPriv ? "#" : " ") ),
726 (isCur ? "* " : " "), zBr);
727 }
728 db_finalize(&q);
729 }else if( strncmp(zCmd,"new",n)==0 ){
730 branch_new();
731
--- src/branch.c
+++ src/branch.c
@@ -193,17 +193,17 @@
193
194 brid = content_put_ex(&branch, 0, 0, 0, isPrivate);
195 if( brid==0 ){
196 fossil_fatal("trouble committing manifest: %s", g.zErrMsg);
197 }
198 db_add_unsent(brid);
199 if( manifest_crosslink(brid, &branch, MC_PERMIT_HOOKS)==0 ){
200 fossil_fatal("%s", g.zErrMsg);
201 }
202 assert( blob_is_reset(&branch) );
203 content_deltify(rootid, &brid, 1, 0);
204 zUuid = rid_to_uuid(brid);
205 fossil_print("New branch: %s\n", zUuid);
206 if( g.argc==3 ){
207 fossil_print(
208 "\n"
209 "Note: the local check-out has not been updated to the new\n"
@@ -460,11 +460,11 @@
460 }else if(manifest_crosslink(newRid, &manifest, 0)==0){
461 fossil_fatal("Crosslinking error: %s", g.zErrMsg);
462 }
463 fossil_print("Saved new control artifact %z (RID %d).\n",
464 rid_to_uuid(newRid), newRid);
465 db_add_unsent(newRid);
466 if(fDryRun){
467 fossil_print("Dry-run mode: rolling back new artifact.\n");
468 assert(0!=doRollback);
469 }
470 }
@@ -719,12 +719,12 @@
719 branch_prepare_list_query(&q, brFlags, zBrNameGlob, nLimit);
720 while( db_step(&q)==SQLITE_ROW ){
721 const char *zBr = db_column_text(&q, 0);
722 int isPriv = zCurrent!=0 && db_column_int(&q, 1)==1;
723 int isCur = zCurrent!=0 && fossil_strcmp(zCurrent,zBr)==0;
724 fossil_print("%s%s%s\n",
725 ( (brFlags & BRL_PRIVATE) ? " " : ( isPriv ? "#" : " ") ),
726 (isCur ? "* " : " "), zBr);
727 }
728 db_finalize(&q);
729 }else if( strncmp(zCmd,"new",n)==0 ){
730 branch_new();
731
+2 -2
--- src/checkin.c
+++ src/checkin.c
@@ -2665,11 +2665,11 @@
26652665
if( rid>0 ){
26662666
content_deltify(rid, &nrid, 1, 0);
26672667
}
26682668
db_multi_exec("UPDATE vfile SET mrid=%d, rid=%d, mhash=NULL WHERE id=%d",
26692669
nrid,nrid,id);
2670
- db_multi_exec("INSERT OR IGNORE INTO unsent VALUES(%d)", nrid);
2670
+ db_add_unsent(nrid);
26712671
}
26722672
}
26732673
db_finalize(&q);
26742674
if( nConflict && !allowConflict ){
26752675
fossil_fatal("abort due to unresolved merge conflicts; "
@@ -2763,11 +2763,11 @@
27632763
27642764
nvid = content_put(&manifest);
27652765
if( nvid==0 ){
27662766
fossil_fatal("trouble committing manifest: %s", g.zErrMsg);
27672767
}
2768
- db_multi_exec("INSERT OR IGNORE INTO unsent VALUES(%d)", nvid);
2768
+ db_add_unsent(nvid);
27692769
if( manifest_crosslink(nvid, &manifest,
27702770
dryRunFlag ? MC_NONE : MC_PERMIT_HOOKS)==0 ){
27712771
fossil_fatal("%s", g.zErrMsg);
27722772
}
27732773
assert( blob_is_reset(&manifest) );
27742774
--- src/checkin.c
+++ src/checkin.c
@@ -2665,11 +2665,11 @@
2665 if( rid>0 ){
2666 content_deltify(rid, &nrid, 1, 0);
2667 }
2668 db_multi_exec("UPDATE vfile SET mrid=%d, rid=%d, mhash=NULL WHERE id=%d",
2669 nrid,nrid,id);
2670 db_multi_exec("INSERT OR IGNORE INTO unsent VALUES(%d)", nrid);
2671 }
2672 }
2673 db_finalize(&q);
2674 if( nConflict && !allowConflict ){
2675 fossil_fatal("abort due to unresolved merge conflicts; "
@@ -2763,11 +2763,11 @@
2763
2764 nvid = content_put(&manifest);
2765 if( nvid==0 ){
2766 fossil_fatal("trouble committing manifest: %s", g.zErrMsg);
2767 }
2768 db_multi_exec("INSERT OR IGNORE INTO unsent VALUES(%d)", nvid);
2769 if( manifest_crosslink(nvid, &manifest,
2770 dryRunFlag ? MC_NONE : MC_PERMIT_HOOKS)==0 ){
2771 fossil_fatal("%s", g.zErrMsg);
2772 }
2773 assert( blob_is_reset(&manifest) );
2774
--- src/checkin.c
+++ src/checkin.c
@@ -2665,11 +2665,11 @@
2665 if( rid>0 ){
2666 content_deltify(rid, &nrid, 1, 0);
2667 }
2668 db_multi_exec("UPDATE vfile SET mrid=%d, rid=%d, mhash=NULL WHERE id=%d",
2669 nrid,nrid,id);
2670 db_add_unsent(nrid);
2671 }
2672 }
2673 db_finalize(&q);
2674 if( nConflict && !allowConflict ){
2675 fossil_fatal("abort due to unresolved merge conflicts; "
@@ -2763,11 +2763,11 @@
2763
2764 nvid = content_put(&manifest);
2765 if( nvid==0 ){
2766 fossil_fatal("trouble committing manifest: %s", g.zErrMsg);
2767 }
2768 db_add_unsent(nvid);
2769 if( manifest_crosslink(nvid, &manifest,
2770 dryRunFlag ? MC_NONE : MC_PERMIT_HOOKS)==0 ){
2771 fossil_fatal("%s", g.zErrMsg);
2772 }
2773 assert( blob_is_reset(&manifest) );
2774
+14
--- src/db.c
+++ src/db.c
@@ -4650,10 +4650,17 @@
46504650
** to obtain a check-in lock during auto-sync, the server will
46514651
** send the "pragma avoid-delta-manifests" statement in its reply,
46524652
** which will cause the client to avoid generating a delta
46534653
** manifest.
46544654
*/
4655
+/*
4656
+** SETTING: forum-close-policy boolean default=off
4657
+** If true, forum moderators may close/re-open forum posts, and reply
4658
+** to closed posts. If false, only administrators may do so. Note that
4659
+** this only affects the forum web UI, not post-closing tags which
4660
+** arrive via the command-line or from synchronization with a remote.
4661
+*/
46554662
/*
46564663
** SETTING: gdiff-command width=40 default=gdiff sensitive
46574664
** The value is an external command to run when performing a graphical
46584665
** diff. If undefined, text diff will be used.
46594666
*/
@@ -5451,5 +5458,12 @@
54515458
fossil_free(zRepo);
54525459
}
54535460
fossil_free(zCkout);
54545461
return rc;
54555462
}
5463
+
5464
+/*
5465
+** Adds the given rid to the UNSENT table.
5466
+*/
5467
+void db_add_unsent(int rid){
5468
+ db_multi_exec("INSERT OR IGNORE INTO unsent VALUES(%d)", rid);
5469
+}
54565470
--- src/db.c
+++ src/db.c
@@ -4650,10 +4650,17 @@
4650 ** to obtain a check-in lock during auto-sync, the server will
4651 ** send the "pragma avoid-delta-manifests" statement in its reply,
4652 ** which will cause the client to avoid generating a delta
4653 ** manifest.
4654 */
 
 
 
 
 
 
 
4655 /*
4656 ** SETTING: gdiff-command width=40 default=gdiff sensitive
4657 ** The value is an external command to run when performing a graphical
4658 ** diff. If undefined, text diff will be used.
4659 */
@@ -5451,5 +5458,12 @@
5451 fossil_free(zRepo);
5452 }
5453 fossil_free(zCkout);
5454 return rc;
5455 }
 
 
 
 
 
 
 
5456
--- src/db.c
+++ src/db.c
@@ -4650,10 +4650,17 @@
4650 ** to obtain a check-in lock during auto-sync, the server will
4651 ** send the "pragma avoid-delta-manifests" statement in its reply,
4652 ** which will cause the client to avoid generating a delta
4653 ** manifest.
4654 */
4655 /*
4656 ** SETTING: forum-close-policy boolean default=off
4657 ** If true, forum moderators may close/re-open forum posts, and reply
4658 ** to closed posts. If false, only administrators may do so. Note that
4659 ** this only affects the forum web UI, not post-closing tags which
4660 ** arrive via the command-line or from synchronization with a remote.
4661 */
4662 /*
4663 ** SETTING: gdiff-command width=40 default=gdiff sensitive
4664 ** The value is an external command to run when performing a graphical
4665 ** diff. If undefined, text diff will be used.
4666 */
@@ -5451,5 +5458,12 @@
5458 fossil_free(zRepo);
5459 }
5460 fossil_free(zCkout);
5461 return rc;
5462 }
5463
5464 /*
5465 ** Adds the given rid to the UNSENT table.
5466 */
5467 void db_add_unsent(int rid){
5468 db_multi_exec("INSERT OR IGNORE INTO unsent VALUES(%d)", rid);
5469 }
5470
--- src/default.css
+++ src/default.css
@@ -904,12 +904,39 @@
904904
padding-right: 1ex;
905905
margin-top: 1ex;
906906
display: flex;
907907
flex-direction: column;
908908
}
909
+div.forumClosed {
910
+}
911
+div.forumClosed > .forumPostBody {
912
+ opacity: 0.7;
913
+}
914
+div.forumClosed > .forumPostHdr::before {
915
+ content: "[CLOSED] ";
916
+}
917
+/*div.forumClosed > div.forumPostBody {
918
+ filter: blur(5px);
919
+}*/
920
+div.forumpost-closure-warning {
921
+ margin-top: 1em;
922
+ margin-bottom: 1em;
923
+ border-style: solid;
924
+ padding: 0.25em 0.5em;
925
+ background: #f4f400bb;
926
+ /*font-weight: bold;*/
927
+}
928
+div.forumpost-closure-warning input[type=submit] {
929
+ padding: 0.25em;
930
+}
931
+div.forumpost-single-controls {
932
+ /* UI controls along the bottom of a single post
933
+ ** in the thread view. */
934
+}
909935
.forum div > form {
910936
margin: 0.5em 0;
937
+ display: inline-block;
911938
}
912939
.forum-post-collapser {
913940
/* Common style for the bottom-of-post and right-of-post
914941
expand/collapse widgets. */
915942
font-size: 0.8em;
@@ -1001,10 +1028,30 @@
10011028
background-color: #cef;
10021029
}
10031030
div.forumObs {
10041031
color: #bbb;
10051032
}
1033
+
1034
+div.setup_forum-column {
1035
+ display: flex;
1036
+ flex-direction: column;
1037
+}
1038
+
1039
+body.cpage-setup_forum > .content table {
1040
+ margin-bottom: 1em;
1041
+}
1042
+body.cpage-setup_forum > .content table.bordered {
1043
+ border: 1px solid;
1044
+ border-radius: 0.25em;
1045
+}
1046
+body.cpage-setup_forum > .content table td,
1047
+body.cpage-setup_forum > .content table th {
1048
+ text-align: left;
1049
+}
1050
+body.cpage-setup_forum table.forum-settings-list > tbody > tr > td {
1051
+ min-width: 2em;
1052
+}
10061053
10071054
#capabilitySummary {
10081055
text-align: center;
10091056
}
10101057
#capabilitySummary td {
10111058
--- src/default.css
+++ src/default.css
@@ -904,12 +904,39 @@
904 padding-right: 1ex;
905 margin-top: 1ex;
906 display: flex;
907 flex-direction: column;
908 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
909 .forum div > form {
910 margin: 0.5em 0;
 
911 }
912 .forum-post-collapser {
913 /* Common style for the bottom-of-post and right-of-post
914 expand/collapse widgets. */
915 font-size: 0.8em;
@@ -1001,10 +1028,30 @@
1001 background-color: #cef;
1002 }
1003 div.forumObs {
1004 color: #bbb;
1005 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1006
1007 #capabilitySummary {
1008 text-align: center;
1009 }
1010 #capabilitySummary td {
1011
--- src/default.css
+++ src/default.css
@@ -904,12 +904,39 @@
904 padding-right: 1ex;
905 margin-top: 1ex;
906 display: flex;
907 flex-direction: column;
908 }
909 div.forumClosed {
910 }
911 div.forumClosed > .forumPostBody {
912 opacity: 0.7;
913 }
914 div.forumClosed > .forumPostHdr::before {
915 content: "[CLOSED] ";
916 }
917 /*div.forumClosed > div.forumPostBody {
918 filter: blur(5px);
919 }*/
920 div.forumpost-closure-warning {
921 margin-top: 1em;
922 margin-bottom: 1em;
923 border-style: solid;
924 padding: 0.25em 0.5em;
925 background: #f4f400bb;
926 /*font-weight: bold;*/
927 }
928 div.forumpost-closure-warning input[type=submit] {
929 padding: 0.25em;
930 }
931 div.forumpost-single-controls {
932 /* UI controls along the bottom of a single post
933 ** in the thread view. */
934 }
935 .forum div > form {
936 margin: 0.5em 0;
937 display: inline-block;
938 }
939 .forum-post-collapser {
940 /* Common style for the bottom-of-post and right-of-post
941 expand/collapse widgets. */
942 font-size: 0.8em;
@@ -1001,10 +1028,30 @@
1028 background-color: #cef;
1029 }
1030 div.forumObs {
1031 color: #bbb;
1032 }
1033
1034 div.setup_forum-column {
1035 display: flex;
1036 flex-direction: column;
1037 }
1038
1039 body.cpage-setup_forum > .content table {
1040 margin-bottom: 1em;
1041 }
1042 body.cpage-setup_forum > .content table.bordered {
1043 border: 1px solid;
1044 border-radius: 0.25em;
1045 }
1046 body.cpage-setup_forum > .content table td,
1047 body.cpage-setup_forum > .content table th {
1048 text-align: left;
1049 }
1050 body.cpage-setup_forum table.forum-settings-list > tbody > tr > td {
1051 min-width: 2em;
1052 }
1053
1054 #capabilitySummary {
1055 text-align: center;
1056 }
1057 #capabilitySummary td {
1058
+1 -1
--- src/event.c
+++ src/event.c
@@ -328,11 +328,11 @@
328328
blob_appendf(&event, "W %d\n%s\n", strlen(zBody), zBody);
329329
md5sum_blob(&event, &cksum);
330330
blob_appendf(&event, "Z %b\n", &cksum);
331331
blob_reset(&cksum);
332332
nrid = content_put(&event);
333
- db_multi_exec("INSERT OR IGNORE INTO unsent VALUES(%d)", nrid);
333
+ db_add_unsent(nrid);
334334
if( manifest_crosslink(nrid, &event, MC_NONE)==0 ){
335335
db_end_transaction(1);
336336
return 0;
337337
}
338338
assert( blob_is_reset(&event) );
339339
--- src/event.c
+++ src/event.c
@@ -328,11 +328,11 @@
328 blob_appendf(&event, "W %d\n%s\n", strlen(zBody), zBody);
329 md5sum_blob(&event, &cksum);
330 blob_appendf(&event, "Z %b\n", &cksum);
331 blob_reset(&cksum);
332 nrid = content_put(&event);
333 db_multi_exec("INSERT OR IGNORE INTO unsent VALUES(%d)", nrid);
334 if( manifest_crosslink(nrid, &event, MC_NONE)==0 ){
335 db_end_transaction(1);
336 return 0;
337 }
338 assert( blob_is_reset(&event) );
339
--- src/event.c
+++ src/event.c
@@ -328,11 +328,11 @@
328 blob_appendf(&event, "W %d\n%s\n", strlen(zBody), zBody);
329 md5sum_blob(&event, &cksum);
330 blob_appendf(&event, "Z %b\n", &cksum);
331 blob_reset(&cksum);
332 nrid = content_put(&event);
333 db_add_unsent(nrid);
334 if( manifest_crosslink(nrid, &event, MC_NONE)==0 ){
335 db_end_transaction(1);
336 return 0;
337 }
338 assert( blob_is_reset(&event) );
339
+493 -20
--- src/forum.c
+++ src/forum.c
@@ -47,10 +47,11 @@
4747
ForumPost *pNext; /* Next in chronological order */
4848
ForumPost *pPrev; /* Previous in chronological order */
4949
ForumPost *pDisplay; /* Next in display order */
5050
int nEdit; /* Number of edits to this post */
5151
int nIndent; /* Number of levels of indentation for this post */
52
+ int iClosed; /* See forum_rid_is_closed() */
5253
};
5354
5455
/*
5556
** A single instance of the following tracks all entries for a thread.
5657
*/
@@ -77,10 +78,285 @@
7778
db_bind_int(&q, "$rid", rid);
7879
res = db_step(&q)==SQLITE_ROW;
7980
db_reset(&q);
8081
return res;
8182
}
83
+
84
+/*
85
+** Given a valid forumpost.fpid value, this function returns the first
86
+** fpid in the chain of edits for that forum post, or rid if no prior
87
+** versions are found.
88
+*/
89
+static int forumpost_head_rid(int rid){
90
+ Stmt q;
91
+ int rcRid = rid;
92
+
93
+ db_prepare(&q, "SELECT fprev FROM forumpost"
94
+ " WHERE fpid=:rid AND fprev IS NOT NULL");
95
+ db_bind_int(&q, ":rid", rid);
96
+ while( SQLITE_ROW==db_step(&q) ){
97
+ rcRid = db_column_int(&q, 0);
98
+ db_reset(&q);
99
+ db_bind_int(&q, ":rid", rcRid);
100
+ }
101
+ db_finalize(&q);
102
+ return rcRid;
103
+}
104
+
105
+/*
106
+** Returns true if p, or any parent of p, has a non-zero iClosed
107
+** value. Returns 0 if !p. For an edited chain of post, the tag is
108
+** checked on the pEditHead entry, to simplify subsequent unlocking of
109
+** the post.
110
+**
111
+** If bCheckIrt is true then p's thread in-response-to parents are
112
+** checked (recursively) for closure, else only p is checked.
113
+*/
114
+static int forumpost_is_closed(ForumPost *p, int bCheckIrt){
115
+ while(p){
116
+ if( p->pEditHead ) p = p->pEditHead;
117
+ if( p->iClosed || !bCheckIrt ) return p->iClosed;
118
+ p = p->pIrt;
119
+ }
120
+ return 0;
121
+}
122
+
123
+/*
124
+** Given a forum post RID, this function returns true if that post has
125
+** (or inherits) an active "closed" tag. If bCheckIrt is true then
126
+** the post to which the given post responds is also checked
127
+** (recursively), else they are not. When checking in-response-to
128
+** posts, the first one which is closed ends the search.
129
+**
130
+** Note that this function checks _exactly_ the given rid, whereas
131
+** forum post closure/re-opening is always applied to the head of an
132
+** edit chain so that we get consistent implied locking beheavior for
133
+** later versions and responses to arbitrary versions in the
134
+** chain. Even so, the "closed" tag is applied as a propagating tag
135
+** so will apply to all edits in a given chain.
136
+**
137
+** The return value is one of:
138
+**
139
+** - 0 if no "closed" tag is found.
140
+**
141
+** - The tagxref.rowid of the tagxref entry for the closure if rid is
142
+** the forum post to which the closure applies.
143
+**
144
+** - (-tagxref.rowid) if the given rid inherits a "closed" tag from an
145
+** IRT forum post.
146
+*/
147
+static int forum_rid_is_closed(int rid, int bCheckIrt){
148
+ static Stmt qIrt = empty_Stmt_m;
149
+ int rc = 0, i = 0;
150
+ /* TODO: this can probably be turned into a CTE by someone with
151
+ ** superior SQL-fu. */
152
+ for( ; rid; i++ ){
153
+ rc = rid_has_active_tag_name(rid, "closed");
154
+ if( rc || !bCheckIrt ) break;
155
+ else if( !qIrt.pStmt ) {
156
+ db_static_prepare(&qIrt,
157
+ "SELECT firt FROM forumpost "
158
+ "WHERE fpid=$fpid ORDER BY fmtime DESC"
159
+ );
160
+ }
161
+ db_bind_int(&qIrt, "$fpid", rid);
162
+ rid = SQLITE_ROW==db_step(&qIrt) ? db_column_int(&qIrt, 0) : 0;
163
+ db_reset(&qIrt);
164
+ }
165
+ return i ? -rc : rc;
166
+}
167
+
168
+/*
169
+** Closes or re-opens the given forum RID via addition of a new
170
+** control artifact into the repository. In order to provide
171
+** consistent behavior for implied closing of responses and later
172
+** versions, it always acts on the first version of the given forum
173
+** post, walking the forumpost.fprev values to find the head of the
174
+** chain.
175
+**
176
+** If doClose is true then a propagating "closed" tag is added, except
177
+** as noted below, with the given optional zReason string as the tag's
178
+** value. If doClose is false then any active "closed" tag on frid is
179
+** cancelled, except as noted below. zReason is ignored if doClose is
180
+** false or if zReason is NULL or starts with a NUL byte.
181
+**
182
+** This function only adds a "closed" tag if forum_rid_is_closed()
183
+** indicates that frid's head is not closed. If a parent post is
184
+** already closed, no tag is added. Similarly, it will only remove a
185
+** "closed" tag from a post which has its own "closed" tag, and will
186
+** not remove an inherited one from a parent post.
187
+**
188
+** If doClose is true and frid is closed (directly or inherited), this
189
+** is a no-op. Likewise, if doClose is false and frid itself is not
190
+** closed (not accounting for an inherited closed tag), this is a
191
+** no-op.
192
+**
193
+** Returns true if it actually creates a new tag, else false. Fails
194
+** fatally on error. If it returns true then any ForumPost::iClosed
195
+** values from previously loaded posts are invalidated if they refer
196
+** to the amended post or a response to it.
197
+**
198
+** Sidebars:
199
+**
200
+** - Unless the caller has a transaction open, via
201
+** db_begin_transaction(), there is a very tiny race condition
202
+** window during which the caller's idea of whether or not the forum
203
+** post is closed may differ from the current repository state.
204
+**
205
+** - This routine assumes that frid really does refer to a forum post.
206
+**
207
+** - This routine assumes that frid is not private or pending
208
+** moderation.
209
+**
210
+** - Closure of a forum post requires a propagating "closed" tag to
211
+** account for how edits of posts are handled. This differs from
212
+** closure of a branch, where a non-propagating tag is used.
213
+*/
214
+static int forumpost_close(int frid, int doClose, const char *zReason){
215
+ Blob artifact = BLOB_INITIALIZER; /* Output artifact */
216
+ Blob cksum = BLOB_INITIALIZER; /* Z-card */
217
+ int iClosed; /* true if frid is closed */
218
+ int trid; /* RID of new control artifact */
219
+ char *zUuid; /* UUID of head version of post */
220
+
221
+ db_begin_transaction();
222
+ frid = forumpost_head_rid(frid);
223
+ iClosed = forum_rid_is_closed(frid, 1);
224
+ if( (iClosed && doClose
225
+ /* Already closed, noting that in the case of (iClosed<0), it's
226
+ ** actually a parent which is closed. */)
227
+ || (iClosed<=0 && !doClose
228
+ /* This entry is not closed, but a parent post may be. */) ){
229
+ db_end_transaction(0);
230
+ return 0;
231
+ }
232
+ if( doClose==0 || (zReason && !zReason[0]) ){
233
+ zReason = 0;
234
+ }
235
+ zUuid = rid_to_uuid(frid);
236
+ blob_appendf(&artifact, "D %z\n", date_in_standard_format( "now" ));
237
+ blob_appendf(&artifact,
238
+ "T %cclosed %s%s%F\n",
239
+ doClose ? '*' : '-', zUuid,
240
+ zReason ? " " : "", zReason ? zReason : "");
241
+ blob_appendf(&artifact, "U %F\n", login_name());
242
+ md5sum_blob(&artifact, &cksum);
243
+ blob_appendf(&artifact, "Z %b\n", &cksum);
244
+ blob_reset(&cksum);
245
+ trid = content_put_ex(&artifact, 0, 0, 0, 0);
246
+ if( trid==0 ){
247
+ fossil_fatal("Error saving tag artifact: %s", g.zErrMsg);
248
+ }
249
+ if( manifest_crosslink(trid, &artifact,
250
+ MC_NONE /*MC_PERMIT_HOOKS?*/)==0 ){
251
+ fossil_fatal("%s", g.zErrMsg);
252
+ }
253
+ assert( blob_is_reset(&artifact) );
254
+ db_add_unsent(trid);
255
+ admin_log("%s forum post %S", doClose ? "Close" : "Re-open", zUuid);
256
+ fossil_free(zUuid);
257
+ /* Potential TODO: if (iClosed>0) then we could find the initial tag
258
+ ** artifact and content_deltify(thatRid,&trid,1,0). Given the tiny
259
+ ** size of these artifacts, however, that would save little space,
260
+ ** if any. */
261
+ db_end_transaction(0);
262
+ return 1;
263
+}
264
+
265
+/*
266
+** Returns true if the forum-close-policy setting is true, else false,
267
+** caching the result for subsequent calls.
268
+*/
269
+static int forumpost_close_policy(void){
270
+ static int closePolicy = -99;
271
+
272
+ if( closePolicy==-99 ){
273
+ closePolicy = db_get_boolean("forum-close-policy",0)>0;
274
+ }
275
+ return closePolicy;
276
+}
277
+
278
+/*
279
+** Returns 1 if the current user is an admin, -1 if the current user
280
+** is a forum moderator and the forum-close-policy setting is true,
281
+** else returns 0. The value is cached for subsequent calls.
282
+*/
283
+static int forumpost_may_close(void){
284
+ static int permClose = -99;
285
+ if( permClose!=-99 ){
286
+ return permClose;
287
+ }else if( g.perm.Admin ){
288
+ return permClose = 1;
289
+ }else if( g.perm.ModForum ){
290
+ return permClose = forumpost_close_policy()>0 ? -1 : 0;
291
+ }else{
292
+ return permClose = 0;
293
+ }
294
+}
295
+
296
+/*
297
+** If iClosed is true and the current user forumpost-close privileges,
298
+** this renders either a checkbox to unlock forum post fpid (if
299
+** iClosed>0) or a SPAN.warning element that the given post inherits
300
+** the CLOSED status from a parent post (if iClosed<0). If neither of
301
+** the initial conditions is true, this is a no-op.
302
+*/
303
+static void forumpost_emit_closed_state(int fpid, int iClosed){
304
+ const char *zCommon;
305
+ int iHead = forumpost_head_rid(fpid);
306
+ const int permClose = forumpost_may_close();
307
+
308
+ zCommon = forumpost_close_policy()==0
309
+ ? "Admins may close or re-open posts, or respond to closed posts."
310
+ : "Admins or moderators "
311
+ "may close or re-open posts, or respond to closed posts.";
312
+ /*@ forumpost_emit_closed_state(%d(fpid), %d(iClosed))<br/>*/
313
+ if( iHead != fpid ){
314
+ iClosed = forum_rid_is_closed(iHead, 1);
315
+ /*@ forumpost_emit_closed_state() %d(iHead), %d(iClosed)*/
316
+ }
317
+ if( iClosed<0 ){
318
+ @ <div class="warning forumpost-closure-warning">\
319
+ @ This post is CLOSED via a parent post. %s(zCommon)\
320
+ @ </div>
321
+ return;
322
+ }
323
+ else if( iClosed==0 ){
324
+ if( permClose==0 ) return;
325
+ @ <div class="warning forumpost-closure-warning">
326
+ @ <form method="post" action="%R/forumpost_close">
327
+ @ <input type="hidden" name="fpid" value="%z(rid_to_uuid(iHead))" />
328
+ @ <input type="submit" value="CLOSE this post and its responses" />
329
+ @ <span>%s(zCommon)</span>
330
+ @ <span>This does NOT save any pending changes in
331
+ @ the editor!</span>
332
+ @ </form></div>
333
+ return;
334
+ }
335
+ assert( iClosed>0 );
336
+ /* Only show the "unlock" option on a post which is actually
337
+ ** closed, not on a post which inherits that state. */
338
+ @ <div class="warning forumpost-closure-warning">\
339
+ @ This post is CLOSED. %s(zCommon)
340
+ if( permClose ){
341
+ @ <form method="post" action="%R/forumpost_reopen">
342
+ @ <input type="hidden" name="fpid" value="%z(rid_to_uuid(iHead))" />
343
+ @ <input type="submit" value="Re-open this post and its responses" />
344
+ @ <span>This does NOT save any pending changes in
345
+ @ the editor!</span>
346
+ @ </form>
347
+ }
348
+ @ </div>
349
+}
350
+
351
+/*
352
+** Emits a warning that the current forum post is CLOSED and can only
353
+** be edited or responded to by an administrator. */
354
+static void forumpost_error_closed(void){
355
+ @ <div class='error'>This (sub)thread is CLOSED and can only be
356
+ @ edited or replied to by an admin user.</div>
357
+}
82358
83359
/*
84360
** Delete a complete ForumThread and all its entries.
85361
*/
86362
static void forumthread_delete(ForumThread *pThread){
@@ -215,10 +491,13 @@
215491
for(; p; p=p->pEditPrev ){
216492
p->nEdit = pPost->nEdit;
217493
p->pEditTail = pPost;
218494
}
219495
}
496
+ pPost->iClosed = forum_rid_is_closed(pPost->pEditHead
497
+ ? pPost->pEditHead->fpid
498
+ : pPost->fpid, 1);
220499
}
221500
db_finalize(&q);
222501
223502
if( computeHierarchy ){
224503
/* Compute the hierarchical display order */
@@ -300,25 +579,31 @@
300579
pThread = forumthread_create(froot, 1);
301580
fossil_print("Chronological:\n");
302581
fossil_print(
303582
/* 0 1 2 3 4 5 6 7 */
304583
/* 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123 */
305
- " sid rev fpid pIrt pEditPrev pEditTail hash\n");
584
+ " sid rev closed fpid pIrt pEditPrev pEditTail hash\n");
306585
for(p=pThread->pFirst; p; p=p->pNext){
307
- fossil_print("%4d %4d %9d %9d %9d %9d %8.8s\n", p->sid, p->rev,
586
+ fossil_print("%4d %4d %7d %9d %9d %9d %9d %8.8s\n",
587
+ p->sid, p->rev,
588
+ p->iClosed,
308589
p->fpid, p->pIrt ? p->pIrt->fpid : 0,
309590
p->pEditPrev ? p->pEditPrev->fpid : 0,
310591
p->pEditTail ? p->pEditTail->fpid : 0, p->zUuid);
311592
}
312593
fossil_print("\nDisplay\n");
313594
for(p=pThread->pDisplay; p; p=p->pDisplay){
314595
fossil_print("%*s", (p->nIndent-1)*3, "");
315596
if( p->pEditTail ){
316
- fossil_print("%d->%d\n", p->fpid, p->pEditTail->fpid);
597
+ fossil_print("%d->%d", p->fpid, p->pEditTail->fpid);
317598
}else{
318
- fossil_print("%d\n", p->fpid);
599
+ fossil_print("%d", p->fpid);
600
+ }
601
+ if( p->iClosed ){
602
+ fossil_print(" [closed%s]", p->iClosed<0 ? " via parent" : "");
319603
}
604
+ fossil_print("\n");
320605
}
321606
forumthread_delete(pThread);
322607
}
323608
324609
/*
@@ -511,23 +796,25 @@
511796
char *zHist; /* History query string */
512797
Manifest *pManifest; /* Manifest comprising the current post */
513798
int bPrivate; /* True for posts awaiting moderation */
514799
int bSameUser; /* True if author is also the reader */
515800
int iIndent; /* Indent level */
801
+ int iClosed; /* True if (sub)thread is closed */
516802
const char *zMimetype;/* Formatting MIME type */
517803
518804
/* Get the manifest for the post. Abort if not found (e.g. shunned). */
519805
pManifest = manifest_get(p->fpid, CFTYPE_FORUM, 0);
520806
if( !pManifest ) return;
521
-
807
+ iClosed = forumpost_is_closed(p, 1);
522808
/* When not in raw mode, create the border around the post. */
523809
if( !bRaw ){
524810
/* Open the <div> enclosing the post. Set the class string to mark the post
525811
** as selected and/or obsolete. */
526812
iIndent = (p->pEditHead ? p->pEditHead->nIndent : p->nIndent)-1;
527813
@ <div id='forum%d(p->fpid)' class='forumTime\
528814
@ %s(bSelect ? " forumSel" : "")\
815
+ @ %s(iClosed ? " forumClosed" : "")\
529816
@ %s(p->pEditTail ? " forumObs" : "")' \
530817
if( iIndent && iIndentScale ){
531818
@ style='margin-left:%d(iIndent*iIndentScale)ex;'>
532819
}else{
533820
@ >
@@ -543,11 +830,11 @@
543830
** * The post is unedited
544831
** * The post was last edited by the original author
545832
** * The post was last edited by a different person
546833
*/
547834
if( p->pEditHead ){
548
- zDate = db_text(0, "SELECT datetime(%.17g,toLocal())",
835
+ zDate = db_text(0, "SELECT datetime(%.17g,toLocal())",
549836
p->pEditHead->rDate);
550837
}else{
551838
zPosterName = forum_post_display_name(p, pManifest);
552839
zEditorName = zPosterName;
553840
}
@@ -555,11 +842,11 @@
555842
if( p->pEditPrev ){
556843
zPosterName = forum_post_display_name(p->pEditHead, 0);
557844
zEditorName = forum_post_display_name(p, pManifest);
558845
zHist = bHist ? "" : zQuery[0]==0 ? "?hist" : "&hist";
559846
@ <h3 class='forumPostHdr'>(%d(p->sid)\
560
- @ .%0*d(fossil_num_digits(p->nEdit))(p->rev)) \
847
+ @ .%0*d(fossil_num_digits(p->nEdit))(p->rev))
561848
if( fossil_strcmp(zPosterName, zEditorName)==0 ){
562849
@ By %s(zPosterName) on %h(zDate) edited from \
563850
@ %z(href("%R/forumpost/%S%s%s",p->pEditPrev->zUuid,zQuery,zHist))\
564851
@ %d(p->sid).%0*d(fossil_num_digits(p->nEdit))(p->pEditPrev->rev)</a>
565852
}else{
@@ -568,11 +855,11 @@
568855
@ %z(href("%R/forumpost/%S%s%s",p->pEditPrev->zUuid,zQuery,zHist))\
569856
@ %d(p->sid).%0*d(fossil_num_digits(p->nEdit))(p->pEditPrev->rev)</a>
570857
}
571858
}else{
572859
zPosterName = forum_post_display_name(p, pManifest);
573
- @ <h3 class='forumPostHdr'>(%d(p->sid)) \
860
+ @ <h3 class='forumPostHdr'>(%d(p->sid))
574861
@ By %s(zPosterName) on %h(zDate)
575862
}
576863
fossil_free(zDate);
577864
578865
@@ -631,18 +918,26 @@
631918
/* When not in raw mode, finish creating the border around the post. */
632919
if( !bRaw ){
633920
/* If the user is able to write to the forum and if this post has not been
634921
** edited, create a form with various interaction buttons. */
635922
if( g.perm.WrForum && !p->pEditTail ){
636
- @ <div><form action="%R/forumedit" method="POST">
923
+ @ <div class="forumpost-single-controls">\
924
+ @ <form action="%R/forumedit" method="POST">
637925
@ <input type="hidden" name="fpid" value="%s(p->zUuid)">
638926
if( !bPrivate ){
639
- /* Reply and Edit are only available if the post has been approved. */
640
- @ <input type="submit" name="reply" value="Reply">
641
- if( g.perm.Admin || bSameUser ){
642
- @ <input type="submit" name="edit" value="Edit">
643
- @ <input type="submit" name="nullout" value="Delete">
927
+ /* Reply and Edit are only available if the post has been
928
+ ** approved. Closed threads can only be edited or replied to
929
+ ** if forumpost_may_close() is true but a user may delete
930
+ ** their own posts even if they are closed. */
931
+ if( forumpost_may_close() || !iClosed ){
932
+ @ <input type="submit" name="reply" value="Reply">
933
+ if( g.perm.Admin || (bSameUser && !iClosed) ){
934
+ @ <input type="submit" name="edit" value="Edit">
935
+ }
936
+ if( g.perm.Admin || bSameUser ){
937
+ @ <input type="submit" name="nullout" value="Delete">
938
+ }
644939
}
645940
}else if( g.perm.ModForum ){
646941
/* Allow moderators to approve or reject pending posts. Also allow
647942
** forum supervisors to mark non-special users as trusted and therefore
648943
** able to post unmoderated. */
@@ -657,11 +952,20 @@
657952
}
658953
}else if( bSameUser ){
659954
/* Allow users to delete (reject) their own pending posts. */
660955
@ <input type="submit" name="reject" value="Delete">
661956
}
662
- @ </form></div>
957
+ @ </form>
958
+ if( bSelect && forumpost_may_close() && iClosed>=0 ){
959
+ int iHead = forumpost_head_rid(p->fpid);
960
+ @ <form method="post" \
961
+ @ action='%R/forumpost_%s(iClosed > 0 ? "reopen" : "close")'>
962
+ @ <input type="hidden" name="fpid" value="%z(rid_to_uuid(iHead))" />
963
+ @ <input type="submit" value='%s(iClosed ? "Re-open" : "Close")' />
964
+ @ </form>
965
+ }
966
+ @ </div>
663967
}
664968
@ </div>
665969
}
666970
667971
/* Clean up. */
@@ -1040,10 +1344,15 @@
10401344
Blob x, cksum, formatCheck, errMsg;
10411345
Manifest *pPost;
10421346
int nContent = zContent ? (int)strlen(zContent) : 0;
10431347
10441348
schema_forum();
1349
+ if( !g.perm.Admin && (iEdit || iInReplyTo)
1350
+ && forum_rid_is_closed(iEdit ? iEdit : iInReplyTo, 1) ){
1351
+ forumpost_error_closed();
1352
+ return 0;
1353
+ }
10451354
if( iEdit==0 && whitespace_only(zContent) ){
10461355
return 0;
10471356
}
10481357
if( iInReplyTo==0 && iEdit>0 ){
10491358
iBasis = iEdit;
@@ -1141,10 +1450,44 @@
11411450
@ %z(href("%R/markup_help"))Markup style</a>:
11421451
mimetype_option_menu(zMimetype, "mimetype");
11431452
@ <br><textarea aria-label="Content:" name="content" class="wikiedit" \
11441453
@ cols="80" rows="25" wrap="virtual">%h(zContent)</textarea><br>
11451454
}
1455
+
1456
+/*
1457
+** WEBPAGE: forumpost_close hidden
1458
+** WEBPAGE: forumpost_reopen hidden
1459
+**
1460
+** fpid=X Hash of the post to be edited. REQUIRED
1461
+** reason=X Optional reason for closure.
1462
+**
1463
+** Closes or re-opens the given forum post, within the bounds of the
1464
+** API for forumpost_close(). After (perhaps) modifying the "closed"
1465
+** status of the given thread, it redirects to that post's thread
1466
+** view. Requires admin privileges.
1467
+*/
1468
+void forum_page_close(void){
1469
+ const char *zFpid = PD("fpid","");
1470
+ const char *zReason = 0;
1471
+ int fClose;
1472
+ int fpid;
1473
+
1474
+ login_check_credentials();
1475
+ if( forumpost_may_close()==0 ){
1476
+ login_needed(g.anon.Admin);
1477
+ return;
1478
+ }
1479
+ fpid = symbolic_name_to_rid(zFpid, "f");
1480
+ if( fpid<=0 ){
1481
+ webpage_error("Missing or invalid fpid query parameter");
1482
+ }
1483
+ fClose = sqlite3_strglob("*_close*", g.zPath)==0;
1484
+ if( fClose ) zReason = PD("reason",0);
1485
+ forumpost_close(fpid, fClose, zReason);
1486
+ cgi_redirectf("%R/forumpost/%S",zFpid);
1487
+ return;
1488
+}
11461489
11471490
/*
11481491
** WEBPAGE: forumnew
11491492
** WEBPAGE: forumedit
11501493
**
@@ -1152,10 +1495,11 @@
11521495
** But first prompt to see if the user would like to log in.
11531496
*/
11541497
void forum_page_init(void){
11551498
int isEdit;
11561499
char *zGoto;
1500
+
11571501
login_check_credentials();
11581502
if( !g.perm.WrForum ){
11591503
login_needed(g.anon.WrForum);
11601504
return;
11611505
}
@@ -1291,11 +1635,13 @@
12911635
const char *zTitle = 0;
12921636
char *zDate = 0;
12931637
const char *zFpid = PD("fpid","");
12941638
int isCsrfSafe;
12951639
int isDelete = 0;
1640
+ int iClosed = 0;
12961641
int bSameUser; /* True if author is also the reader */
1642
+ int bPreview; /* True in preview mode. */
12971643
int bPrivate; /* True if post is private (not yet moderated) */
12981644
12991645
login_check_credentials();
13001646
if( !g.perm.WrForum ){
13011647
login_needed(g.anon.WrForum);
@@ -1308,13 +1654,15 @@
13081654
froot = db_int(0, "SELECT froot FROM forumpost WHERE fpid=%d", fpid);
13091655
if( froot==0 || (pRootPost = manifest_get(froot, CFTYPE_FORUM, 0))==0 ){
13101656
webpage_error("fpid does not appear to be a forum post: \"%d\"", fpid);
13111657
}
13121658
if( P("cancel") ){
1313
- cgi_redirectf("%R/forumpost/%S",P("fpid"));
1659
+ cgi_redirectf("%R/forumpost/%S",zFpid);
13141660
return;
13151661
}
1662
+ bPreview = P("preview")!=0;
1663
+ iClosed = forum_rid_is_closed(fpid, 1);
13161664
isCsrfSafe = cgi_csrf_safe(1);
13171665
bPrivate = content_is_private(fpid);
13181666
bSameUser = login_is_individual()
13191667
&& fossil_strcmp(pPost->zUser, g.zLogin)==0;
13201668
if( isCsrfSafe && (g.perm.ModForum || (bPrivate && bSameUser)) ){
@@ -1376,10 +1724,11 @@
13761724
if( pPost->zThreadTitle ) zTitle = "";
13771725
style_header("Delete %s", zTitle ? "Post" : "Reply");
13781726
@ <h1>Original Post:</h1>
13791727
forum_render(pPost->zThreadTitle, pPost->zMimetype, pPost->zWiki,
13801728
"forumEdit", 1);
1729
+ forumpost_emit_closed_state(fpid, iClosed);
13811730
@ <h1>Change Into:</h1>
13821731
forum_render(zTitle, zMimetype, zContent,"forumEdit", 1);
13831732
@ <form action="%R/forume2" method="POST">
13841733
@ <input type="hidden" name="fpid" value="%h(P("fpid"))">
13851734
@ <input type="hidden" name="nullout" value="1">
@@ -1400,11 +1749,11 @@
14001749
}
14011750
style_header("Edit %s", zTitle ? "Post" : "Reply");
14021751
@ <h2>Original Post:</h2>
14031752
forum_render(pPost->zThreadTitle, pPost->zMimetype, pPost->zWiki,
14041753
"forumEdit", 1);
1405
- if( P("preview") ){
1754
+ if( bPreview ){
14061755
@ <h2>Preview of Edited Post:</h2>
14071756
forum_render(zTitle, zMimetype, zContent,"forumEdit", 1);
14081757
}
14091758
@ <h2>Revised Message:</h2>
14101759
@ <form action="%R/forume2" method="POST">
@@ -1429,11 +1778,11 @@
14291778
zDisplayName = display_name_from_login(pPost->zUser);
14301779
@ <h3 class='forumPostHdr'>By %s(zDisplayName) on %h(zDate)</h3>
14311780
fossil_free(zDisplayName);
14321781
fossil_free(zDate);
14331782
forum_render(0, pPost->zMimetype, pPost->zWiki, "forumEdit", 1);
1434
- if( P("preview") && !whitespace_only(zContent) ){
1783
+ if( bPreview && !whitespace_only(zContent) ){
14351784
@ <h2>Preview:</h2>
14361785
forum_render(0, zMimetype,zContent, "forumEdit", 1);
14371786
}
14381787
@ <h2>Enter Reply:</h2>
14391788
@ <form action="%R/forume2" method="POST">
@@ -1444,16 +1793,140 @@
14441793
}
14451794
if( !isDelete ){
14461795
@ <input type="submit" name="preview" value="Preview">
14471796
}
14481797
@ <input type="submit" name="cancel" value="Cancel">
1449
- if( (P("preview") && !whitespace_only(zContent)) || isDelete ){
1450
- @ <input type="submit" name="submit" value="Submit">
1798
+ if( (bPreview && !whitespace_only(zContent)) || isDelete ){
1799
+ if( !iClosed || g.perm.Admin ) {
1800
+ @ <input type="submit" name="submit" value="Submit">
1801
+ }
14511802
}
14521803
forum_render_debug_options();
14531804
@ </form>
14541805
forum_emit_js();
1806
+ forumpost_emit_closed_state(fpid, iClosed);
1807
+ style_finish_page();
1808
+}
1809
+
1810
+/*
1811
+** WEBPAGE: setup_forum
1812
+**
1813
+** Forum configuration and metrics.
1814
+*/
1815
+void forum_setup(void){
1816
+ /* boolean config settings specific to the forum. */
1817
+ const char * zSettingsBool[] = {
1818
+ "forum-close-policy",
1819
+ NULL /* sentinel entry */
1820
+ };
1821
+
1822
+ login_check_credentials();
1823
+ if( !g.perm.Setup ){
1824
+ login_needed(g.anon.Setup);
1825
+ return;
1826
+ }
1827
+ style_set_current_feature("forum");
1828
+ style_header("Forum Setup");
1829
+
1830
+ @ <h2>Metrics</h2>
1831
+ {
1832
+ int nPosts = db_int(0, "SELECT COUNT(*) FROM event WHERE type='f'");
1833
+ @ <p><a href='%R/forum'>Forum posts</a>:
1834
+ @ <a href='%R/timeline?y=f'>%d(nPosts)</a></p>
1835
+ }
1836
+
1837
+ @ <h2>Supervisors</h2>
1838
+ @ <p>Users with capabilities 's', 'a', or '6'.</p>
1839
+ {
1840
+ Stmt q = empty_Stmt;
1841
+ int nRows = 0;
1842
+ db_prepare(&q, "SELECT uid, login, cap FROM user "
1843
+ "WHERE cap GLOB '*[as6]*' ORDER BY login");
1844
+ @ <table class='bordered'>
1845
+ @ <thead><tr><th>User</th><th>Capabilities</th></tr></thead>
1846
+ @ <tbody>
1847
+ while( SQLITE_ROW==db_step(&q) ){
1848
+ const int iUid = db_column_int(&q, 0);
1849
+ const char *zUser = db_column_text(&q, 1);
1850
+ const char *zCap = db_column_text(&q, 2);
1851
+ ++nRows;
1852
+ @ <tr>
1853
+ @ <td><a href='%R/setup_uedit?id=%d(iUid)'>%h(zUser)</a></td>
1854
+ @ <td>(%h(zCap))</td>
1855
+ @ </tr>
1856
+ }
1857
+ db_finalize(&q);
1858
+ @</tbody></table>
1859
+ if( 0==nRows ){
1860
+ @ No supervisors
1861
+ }else{
1862
+ @ %d(nRows) supervisor(s)
1863
+ }
1864
+ }
1865
+
1866
+ @ <h2>Moderators</h2>
1867
+ @ <p>Users with capability '5'.</p>
1868
+ {
1869
+ Stmt q = empty_Stmt;
1870
+ int nRows = 0;
1871
+ db_prepare(&q, "SELECT uid, login, cap FROM user "
1872
+ "WHERE cap GLOB '*5*' ORDER BY login");
1873
+ @ <table class='bordered'>
1874
+ @ <thead><tr><th>User</th><th>Capabilities</th></tr></thead>
1875
+ @ <tbody>
1876
+ while( SQLITE_ROW==db_step(&q) ){
1877
+ const int iUid = db_column_int(&q, 0);
1878
+ const char *zUser = db_column_text(&q, 1);
1879
+ const char *zCap = db_column_text(&q, 2);
1880
+ ++nRows;
1881
+ @ <tr>
1882
+ @ <td><a href='%R/setup_uedit?id=%d(iUid)'>%h(zUser)</a></td>
1883
+ @ <td>(%h(zCap))</td>
1884
+ @ </tr>
1885
+ }
1886
+ db_finalize(&q);
1887
+ @ </tbody></table>
1888
+ if( 0==nRows ){
1889
+ @ No non-supervisor moderators
1890
+ }else{
1891
+ @ %d(nRows) moderator(s)
1892
+ }
1893
+ }
1894
+
1895
+ @ <h2>Settings</h2>
1896
+ @ <p>Configuration settings specific to the forum.</p>
1897
+ if( P("submit") && cgi_csrf_safe(1) ){
1898
+ int i = 0;
1899
+ const char *zSetting;
1900
+ login_verify_csrf_secret();
1901
+ db_begin_transaction();
1902
+ while( (zSetting = zSettingsBool[i++]) ){
1903
+ const char *z = P(zSetting);
1904
+ if( !z || !z[0] ) z = "off";
1905
+ db_set(zSetting/*works-like:"x"*/, z, 0);
1906
+ }
1907
+ db_end_transaction(0);
1908
+ @ <p><em>Settings saved.</em></p>
1909
+ }
1910
+ {
1911
+ int i = 0;
1912
+ const char *zSetting;
1913
+ @ <form action="%R/setup_forum" method="post">
1914
+ login_insert_csrf_secret();
1915
+ @ <table class='forum-settings-list'><tbody>
1916
+ while( (zSetting = zSettingsBool[i++]) ){
1917
+ @ <tr><td>
1918
+ onoff_attribute("", zSetting, zSetting/*works-like:"x"*/, 0, 0);
1919
+ @ </td><td>
1920
+ @ <a href='%R/help?cmd=%h(zSetting)'>%h(zSetting)</a>
1921
+ @ </td></tr>
1922
+ }
1923
+ @ </tbody></table>
1924
+ @ <input type='submit' name='submit' value='Apply changes'>
1925
+ @ </form>
1926
+ }
1927
+
14551928
style_finish_page();
14561929
}
14571930
14581931
/*
14591932
** WEBPAGE: forummain
14601933
--- src/forum.c
+++ src/forum.c
@@ -47,10 +47,11 @@
47 ForumPost *pNext; /* Next in chronological order */
48 ForumPost *pPrev; /* Previous in chronological order */
49 ForumPost *pDisplay; /* Next in display order */
50 int nEdit; /* Number of edits to this post */
51 int nIndent; /* Number of levels of indentation for this post */
 
52 };
53
54 /*
55 ** A single instance of the following tracks all entries for a thread.
56 */
@@ -77,10 +78,285 @@
77 db_bind_int(&q, "$rid", rid);
78 res = db_step(&q)==SQLITE_ROW;
79 db_reset(&q);
80 return res;
81 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
83 /*
84 ** Delete a complete ForumThread and all its entries.
85 */
86 static void forumthread_delete(ForumThread *pThread){
@@ -215,10 +491,13 @@
215 for(; p; p=p->pEditPrev ){
216 p->nEdit = pPost->nEdit;
217 p->pEditTail = pPost;
218 }
219 }
 
 
 
220 }
221 db_finalize(&q);
222
223 if( computeHierarchy ){
224 /* Compute the hierarchical display order */
@@ -300,25 +579,31 @@
300 pThread = forumthread_create(froot, 1);
301 fossil_print("Chronological:\n");
302 fossil_print(
303 /* 0 1 2 3 4 5 6 7 */
304 /* 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123 */
305 " sid rev fpid pIrt pEditPrev pEditTail hash\n");
306 for(p=pThread->pFirst; p; p=p->pNext){
307 fossil_print("%4d %4d %9d %9d %9d %9d %8.8s\n", p->sid, p->rev,
 
 
308 p->fpid, p->pIrt ? p->pIrt->fpid : 0,
309 p->pEditPrev ? p->pEditPrev->fpid : 0,
310 p->pEditTail ? p->pEditTail->fpid : 0, p->zUuid);
311 }
312 fossil_print("\nDisplay\n");
313 for(p=pThread->pDisplay; p; p=p->pDisplay){
314 fossil_print("%*s", (p->nIndent-1)*3, "");
315 if( p->pEditTail ){
316 fossil_print("%d->%d\n", p->fpid, p->pEditTail->fpid);
317 }else{
318 fossil_print("%d\n", p->fpid);
 
 
 
319 }
 
320 }
321 forumthread_delete(pThread);
322 }
323
324 /*
@@ -511,23 +796,25 @@
511 char *zHist; /* History query string */
512 Manifest *pManifest; /* Manifest comprising the current post */
513 int bPrivate; /* True for posts awaiting moderation */
514 int bSameUser; /* True if author is also the reader */
515 int iIndent; /* Indent level */
 
516 const char *zMimetype;/* Formatting MIME type */
517
518 /* Get the manifest for the post. Abort if not found (e.g. shunned). */
519 pManifest = manifest_get(p->fpid, CFTYPE_FORUM, 0);
520 if( !pManifest ) return;
521
522 /* When not in raw mode, create the border around the post. */
523 if( !bRaw ){
524 /* Open the <div> enclosing the post. Set the class string to mark the post
525 ** as selected and/or obsolete. */
526 iIndent = (p->pEditHead ? p->pEditHead->nIndent : p->nIndent)-1;
527 @ <div id='forum%d(p->fpid)' class='forumTime\
528 @ %s(bSelect ? " forumSel" : "")\
 
529 @ %s(p->pEditTail ? " forumObs" : "")' \
530 if( iIndent && iIndentScale ){
531 @ style='margin-left:%d(iIndent*iIndentScale)ex;'>
532 }else{
533 @ >
@@ -543,11 +830,11 @@
543 ** * The post is unedited
544 ** * The post was last edited by the original author
545 ** * The post was last edited by a different person
546 */
547 if( p->pEditHead ){
548 zDate = db_text(0, "SELECT datetime(%.17g,toLocal())",
549 p->pEditHead->rDate);
550 }else{
551 zPosterName = forum_post_display_name(p, pManifest);
552 zEditorName = zPosterName;
553 }
@@ -555,11 +842,11 @@
555 if( p->pEditPrev ){
556 zPosterName = forum_post_display_name(p->pEditHead, 0);
557 zEditorName = forum_post_display_name(p, pManifest);
558 zHist = bHist ? "" : zQuery[0]==0 ? "?hist" : "&hist";
559 @ <h3 class='forumPostHdr'>(%d(p->sid)\
560 @ .%0*d(fossil_num_digits(p->nEdit))(p->rev)) \
561 if( fossil_strcmp(zPosterName, zEditorName)==0 ){
562 @ By %s(zPosterName) on %h(zDate) edited from \
563 @ %z(href("%R/forumpost/%S%s%s",p->pEditPrev->zUuid,zQuery,zHist))\
564 @ %d(p->sid).%0*d(fossil_num_digits(p->nEdit))(p->pEditPrev->rev)</a>
565 }else{
@@ -568,11 +855,11 @@
568 @ %z(href("%R/forumpost/%S%s%s",p->pEditPrev->zUuid,zQuery,zHist))\
569 @ %d(p->sid).%0*d(fossil_num_digits(p->nEdit))(p->pEditPrev->rev)</a>
570 }
571 }else{
572 zPosterName = forum_post_display_name(p, pManifest);
573 @ <h3 class='forumPostHdr'>(%d(p->sid)) \
574 @ By %s(zPosterName) on %h(zDate)
575 }
576 fossil_free(zDate);
577
578
@@ -631,18 +918,26 @@
631 /* When not in raw mode, finish creating the border around the post. */
632 if( !bRaw ){
633 /* If the user is able to write to the forum and if this post has not been
634 ** edited, create a form with various interaction buttons. */
635 if( g.perm.WrForum && !p->pEditTail ){
636 @ <div><form action="%R/forumedit" method="POST">
 
637 @ <input type="hidden" name="fpid" value="%s(p->zUuid)">
638 if( !bPrivate ){
639 /* Reply and Edit are only available if the post has been approved. */
640 @ <input type="submit" name="reply" value="Reply">
641 if( g.perm.Admin || bSameUser ){
642 @ <input type="submit" name="edit" value="Edit">
643 @ <input type="submit" name="nullout" value="Delete">
 
 
 
 
 
 
 
644 }
645 }else if( g.perm.ModForum ){
646 /* Allow moderators to approve or reject pending posts. Also allow
647 ** forum supervisors to mark non-special users as trusted and therefore
648 ** able to post unmoderated. */
@@ -657,11 +952,20 @@
657 }
658 }else if( bSameUser ){
659 /* Allow users to delete (reject) their own pending posts. */
660 @ <input type="submit" name="reject" value="Delete">
661 }
662 @ </form></div>
 
 
 
 
 
 
 
 
 
663 }
664 @ </div>
665 }
666
667 /* Clean up. */
@@ -1040,10 +1344,15 @@
1040 Blob x, cksum, formatCheck, errMsg;
1041 Manifest *pPost;
1042 int nContent = zContent ? (int)strlen(zContent) : 0;
1043
1044 schema_forum();
 
 
 
 
 
1045 if( iEdit==0 && whitespace_only(zContent) ){
1046 return 0;
1047 }
1048 if( iInReplyTo==0 && iEdit>0 ){
1049 iBasis = iEdit;
@@ -1141,10 +1450,44 @@
1141 @ %z(href("%R/markup_help"))Markup style</a>:
1142 mimetype_option_menu(zMimetype, "mimetype");
1143 @ <br><textarea aria-label="Content:" name="content" class="wikiedit" \
1144 @ cols="80" rows="25" wrap="virtual">%h(zContent)</textarea><br>
1145 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1146
1147 /*
1148 ** WEBPAGE: forumnew
1149 ** WEBPAGE: forumedit
1150 **
@@ -1152,10 +1495,11 @@
1152 ** But first prompt to see if the user would like to log in.
1153 */
1154 void forum_page_init(void){
1155 int isEdit;
1156 char *zGoto;
 
1157 login_check_credentials();
1158 if( !g.perm.WrForum ){
1159 login_needed(g.anon.WrForum);
1160 return;
1161 }
@@ -1291,11 +1635,13 @@
1291 const char *zTitle = 0;
1292 char *zDate = 0;
1293 const char *zFpid = PD("fpid","");
1294 int isCsrfSafe;
1295 int isDelete = 0;
 
1296 int bSameUser; /* True if author is also the reader */
 
1297 int bPrivate; /* True if post is private (not yet moderated) */
1298
1299 login_check_credentials();
1300 if( !g.perm.WrForum ){
1301 login_needed(g.anon.WrForum);
@@ -1308,13 +1654,15 @@
1308 froot = db_int(0, "SELECT froot FROM forumpost WHERE fpid=%d", fpid);
1309 if( froot==0 || (pRootPost = manifest_get(froot, CFTYPE_FORUM, 0))==0 ){
1310 webpage_error("fpid does not appear to be a forum post: \"%d\"", fpid);
1311 }
1312 if( P("cancel") ){
1313 cgi_redirectf("%R/forumpost/%S",P("fpid"));
1314 return;
1315 }
 
 
1316 isCsrfSafe = cgi_csrf_safe(1);
1317 bPrivate = content_is_private(fpid);
1318 bSameUser = login_is_individual()
1319 && fossil_strcmp(pPost->zUser, g.zLogin)==0;
1320 if( isCsrfSafe && (g.perm.ModForum || (bPrivate && bSameUser)) ){
@@ -1376,10 +1724,11 @@
1376 if( pPost->zThreadTitle ) zTitle = "";
1377 style_header("Delete %s", zTitle ? "Post" : "Reply");
1378 @ <h1>Original Post:</h1>
1379 forum_render(pPost->zThreadTitle, pPost->zMimetype, pPost->zWiki,
1380 "forumEdit", 1);
 
1381 @ <h1>Change Into:</h1>
1382 forum_render(zTitle, zMimetype, zContent,"forumEdit", 1);
1383 @ <form action="%R/forume2" method="POST">
1384 @ <input type="hidden" name="fpid" value="%h(P("fpid"))">
1385 @ <input type="hidden" name="nullout" value="1">
@@ -1400,11 +1749,11 @@
1400 }
1401 style_header("Edit %s", zTitle ? "Post" : "Reply");
1402 @ <h2>Original Post:</h2>
1403 forum_render(pPost->zThreadTitle, pPost->zMimetype, pPost->zWiki,
1404 "forumEdit", 1);
1405 if( P("preview") ){
1406 @ <h2>Preview of Edited Post:</h2>
1407 forum_render(zTitle, zMimetype, zContent,"forumEdit", 1);
1408 }
1409 @ <h2>Revised Message:</h2>
1410 @ <form action="%R/forume2" method="POST">
@@ -1429,11 +1778,11 @@
1429 zDisplayName = display_name_from_login(pPost->zUser);
1430 @ <h3 class='forumPostHdr'>By %s(zDisplayName) on %h(zDate)</h3>
1431 fossil_free(zDisplayName);
1432 fossil_free(zDate);
1433 forum_render(0, pPost->zMimetype, pPost->zWiki, "forumEdit", 1);
1434 if( P("preview") && !whitespace_only(zContent) ){
1435 @ <h2>Preview:</h2>
1436 forum_render(0, zMimetype,zContent, "forumEdit", 1);
1437 }
1438 @ <h2>Enter Reply:</h2>
1439 @ <form action="%R/forume2" method="POST">
@@ -1444,16 +1793,140 @@
1444 }
1445 if( !isDelete ){
1446 @ <input type="submit" name="preview" value="Preview">
1447 }
1448 @ <input type="submit" name="cancel" value="Cancel">
1449 if( (P("preview") && !whitespace_only(zContent)) || isDelete ){
1450 @ <input type="submit" name="submit" value="Submit">
 
 
1451 }
1452 forum_render_debug_options();
1453 @ </form>
1454 forum_emit_js();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1455 style_finish_page();
1456 }
1457
1458 /*
1459 ** WEBPAGE: forummain
1460
--- src/forum.c
+++ src/forum.c
@@ -47,10 +47,11 @@
47 ForumPost *pNext; /* Next in chronological order */
48 ForumPost *pPrev; /* Previous in chronological order */
49 ForumPost *pDisplay; /* Next in display order */
50 int nEdit; /* Number of edits to this post */
51 int nIndent; /* Number of levels of indentation for this post */
52 int iClosed; /* See forum_rid_is_closed() */
53 };
54
55 /*
56 ** A single instance of the following tracks all entries for a thread.
57 */
@@ -77,10 +78,285 @@
78 db_bind_int(&q, "$rid", rid);
79 res = db_step(&q)==SQLITE_ROW;
80 db_reset(&q);
81 return res;
82 }
83
84 /*
85 ** Given a valid forumpost.fpid value, this function returns the first
86 ** fpid in the chain of edits for that forum post, or rid if no prior
87 ** versions are found.
88 */
89 static int forumpost_head_rid(int rid){
90 Stmt q;
91 int rcRid = rid;
92
93 db_prepare(&q, "SELECT fprev FROM forumpost"
94 " WHERE fpid=:rid AND fprev IS NOT NULL");
95 db_bind_int(&q, ":rid", rid);
96 while( SQLITE_ROW==db_step(&q) ){
97 rcRid = db_column_int(&q, 0);
98 db_reset(&q);
99 db_bind_int(&q, ":rid", rcRid);
100 }
101 db_finalize(&q);
102 return rcRid;
103 }
104
105 /*
106 ** Returns true if p, or any parent of p, has a non-zero iClosed
107 ** value. Returns 0 if !p. For an edited chain of post, the tag is
108 ** checked on the pEditHead entry, to simplify subsequent unlocking of
109 ** the post.
110 **
111 ** If bCheckIrt is true then p's thread in-response-to parents are
112 ** checked (recursively) for closure, else only p is checked.
113 */
114 static int forumpost_is_closed(ForumPost *p, int bCheckIrt){
115 while(p){
116 if( p->pEditHead ) p = p->pEditHead;
117 if( p->iClosed || !bCheckIrt ) return p->iClosed;
118 p = p->pIrt;
119 }
120 return 0;
121 }
122
123 /*
124 ** Given a forum post RID, this function returns true if that post has
125 ** (or inherits) an active "closed" tag. If bCheckIrt is true then
126 ** the post to which the given post responds is also checked
127 ** (recursively), else they are not. When checking in-response-to
128 ** posts, the first one which is closed ends the search.
129 **
130 ** Note that this function checks _exactly_ the given rid, whereas
131 ** forum post closure/re-opening is always applied to the head of an
132 ** edit chain so that we get consistent implied locking beheavior for
133 ** later versions and responses to arbitrary versions in the
134 ** chain. Even so, the "closed" tag is applied as a propagating tag
135 ** so will apply to all edits in a given chain.
136 **
137 ** The return value is one of:
138 **
139 ** - 0 if no "closed" tag is found.
140 **
141 ** - The tagxref.rowid of the tagxref entry for the closure if rid is
142 ** the forum post to which the closure applies.
143 **
144 ** - (-tagxref.rowid) if the given rid inherits a "closed" tag from an
145 ** IRT forum post.
146 */
147 static int forum_rid_is_closed(int rid, int bCheckIrt){
148 static Stmt qIrt = empty_Stmt_m;
149 int rc = 0, i = 0;
150 /* TODO: this can probably be turned into a CTE by someone with
151 ** superior SQL-fu. */
152 for( ; rid; i++ ){
153 rc = rid_has_active_tag_name(rid, "closed");
154 if( rc || !bCheckIrt ) break;
155 else if( !qIrt.pStmt ) {
156 db_static_prepare(&qIrt,
157 "SELECT firt FROM forumpost "
158 "WHERE fpid=$fpid ORDER BY fmtime DESC"
159 );
160 }
161 db_bind_int(&qIrt, "$fpid", rid);
162 rid = SQLITE_ROW==db_step(&qIrt) ? db_column_int(&qIrt, 0) : 0;
163 db_reset(&qIrt);
164 }
165 return i ? -rc : rc;
166 }
167
168 /*
169 ** Closes or re-opens the given forum RID via addition of a new
170 ** control artifact into the repository. In order to provide
171 ** consistent behavior for implied closing of responses and later
172 ** versions, it always acts on the first version of the given forum
173 ** post, walking the forumpost.fprev values to find the head of the
174 ** chain.
175 **
176 ** If doClose is true then a propagating "closed" tag is added, except
177 ** as noted below, with the given optional zReason string as the tag's
178 ** value. If doClose is false then any active "closed" tag on frid is
179 ** cancelled, except as noted below. zReason is ignored if doClose is
180 ** false or if zReason is NULL or starts with a NUL byte.
181 **
182 ** This function only adds a "closed" tag if forum_rid_is_closed()
183 ** indicates that frid's head is not closed. If a parent post is
184 ** already closed, no tag is added. Similarly, it will only remove a
185 ** "closed" tag from a post which has its own "closed" tag, and will
186 ** not remove an inherited one from a parent post.
187 **
188 ** If doClose is true and frid is closed (directly or inherited), this
189 ** is a no-op. Likewise, if doClose is false and frid itself is not
190 ** closed (not accounting for an inherited closed tag), this is a
191 ** no-op.
192 **
193 ** Returns true if it actually creates a new tag, else false. Fails
194 ** fatally on error. If it returns true then any ForumPost::iClosed
195 ** values from previously loaded posts are invalidated if they refer
196 ** to the amended post or a response to it.
197 **
198 ** Sidebars:
199 **
200 ** - Unless the caller has a transaction open, via
201 ** db_begin_transaction(), there is a very tiny race condition
202 ** window during which the caller's idea of whether or not the forum
203 ** post is closed may differ from the current repository state.
204 **
205 ** - This routine assumes that frid really does refer to a forum post.
206 **
207 ** - This routine assumes that frid is not private or pending
208 ** moderation.
209 **
210 ** - Closure of a forum post requires a propagating "closed" tag to
211 ** account for how edits of posts are handled. This differs from
212 ** closure of a branch, where a non-propagating tag is used.
213 */
214 static int forumpost_close(int frid, int doClose, const char *zReason){
215 Blob artifact = BLOB_INITIALIZER; /* Output artifact */
216 Blob cksum = BLOB_INITIALIZER; /* Z-card */
217 int iClosed; /* true if frid is closed */
218 int trid; /* RID of new control artifact */
219 char *zUuid; /* UUID of head version of post */
220
221 db_begin_transaction();
222 frid = forumpost_head_rid(frid);
223 iClosed = forum_rid_is_closed(frid, 1);
224 if( (iClosed && doClose
225 /* Already closed, noting that in the case of (iClosed<0), it's
226 ** actually a parent which is closed. */)
227 || (iClosed<=0 && !doClose
228 /* This entry is not closed, but a parent post may be. */) ){
229 db_end_transaction(0);
230 return 0;
231 }
232 if( doClose==0 || (zReason && !zReason[0]) ){
233 zReason = 0;
234 }
235 zUuid = rid_to_uuid(frid);
236 blob_appendf(&artifact, "D %z\n", date_in_standard_format( "now" ));
237 blob_appendf(&artifact,
238 "T %cclosed %s%s%F\n",
239 doClose ? '*' : '-', zUuid,
240 zReason ? " " : "", zReason ? zReason : "");
241 blob_appendf(&artifact, "U %F\n", login_name());
242 md5sum_blob(&artifact, &cksum);
243 blob_appendf(&artifact, "Z %b\n", &cksum);
244 blob_reset(&cksum);
245 trid = content_put_ex(&artifact, 0, 0, 0, 0);
246 if( trid==0 ){
247 fossil_fatal("Error saving tag artifact: %s", g.zErrMsg);
248 }
249 if( manifest_crosslink(trid, &artifact,
250 MC_NONE /*MC_PERMIT_HOOKS?*/)==0 ){
251 fossil_fatal("%s", g.zErrMsg);
252 }
253 assert( blob_is_reset(&artifact) );
254 db_add_unsent(trid);
255 admin_log("%s forum post %S", doClose ? "Close" : "Re-open", zUuid);
256 fossil_free(zUuid);
257 /* Potential TODO: if (iClosed>0) then we could find the initial tag
258 ** artifact and content_deltify(thatRid,&trid,1,0). Given the tiny
259 ** size of these artifacts, however, that would save little space,
260 ** if any. */
261 db_end_transaction(0);
262 return 1;
263 }
264
265 /*
266 ** Returns true if the forum-close-policy setting is true, else false,
267 ** caching the result for subsequent calls.
268 */
269 static int forumpost_close_policy(void){
270 static int closePolicy = -99;
271
272 if( closePolicy==-99 ){
273 closePolicy = db_get_boolean("forum-close-policy",0)>0;
274 }
275 return closePolicy;
276 }
277
278 /*
279 ** Returns 1 if the current user is an admin, -1 if the current user
280 ** is a forum moderator and the forum-close-policy setting is true,
281 ** else returns 0. The value is cached for subsequent calls.
282 */
283 static int forumpost_may_close(void){
284 static int permClose = -99;
285 if( permClose!=-99 ){
286 return permClose;
287 }else if( g.perm.Admin ){
288 return permClose = 1;
289 }else if( g.perm.ModForum ){
290 return permClose = forumpost_close_policy()>0 ? -1 : 0;
291 }else{
292 return permClose = 0;
293 }
294 }
295
296 /*
297 ** If iClosed is true and the current user forumpost-close privileges,
298 ** this renders either a checkbox to unlock forum post fpid (if
299 ** iClosed>0) or a SPAN.warning element that the given post inherits
300 ** the CLOSED status from a parent post (if iClosed<0). If neither of
301 ** the initial conditions is true, this is a no-op.
302 */
303 static void forumpost_emit_closed_state(int fpid, int iClosed){
304 const char *zCommon;
305 int iHead = forumpost_head_rid(fpid);
306 const int permClose = forumpost_may_close();
307
308 zCommon = forumpost_close_policy()==0
309 ? "Admins may close or re-open posts, or respond to closed posts."
310 : "Admins or moderators "
311 "may close or re-open posts, or respond to closed posts.";
312 /*@ forumpost_emit_closed_state(%d(fpid), %d(iClosed))<br/>*/
313 if( iHead != fpid ){
314 iClosed = forum_rid_is_closed(iHead, 1);
315 /*@ forumpost_emit_closed_state() %d(iHead), %d(iClosed)*/
316 }
317 if( iClosed<0 ){
318 @ <div class="warning forumpost-closure-warning">\
319 @ This post is CLOSED via a parent post. %s(zCommon)\
320 @ </div>
321 return;
322 }
323 else if( iClosed==0 ){
324 if( permClose==0 ) return;
325 @ <div class="warning forumpost-closure-warning">
326 @ <form method="post" action="%R/forumpost_close">
327 @ <input type="hidden" name="fpid" value="%z(rid_to_uuid(iHead))" />
328 @ <input type="submit" value="CLOSE this post and its responses" />
329 @ <span>%s(zCommon)</span>
330 @ <span>This does NOT save any pending changes in
331 @ the editor!</span>
332 @ </form></div>
333 return;
334 }
335 assert( iClosed>0 );
336 /* Only show the "unlock" option on a post which is actually
337 ** closed, not on a post which inherits that state. */
338 @ <div class="warning forumpost-closure-warning">\
339 @ This post is CLOSED. %s(zCommon)
340 if( permClose ){
341 @ <form method="post" action="%R/forumpost_reopen">
342 @ <input type="hidden" name="fpid" value="%z(rid_to_uuid(iHead))" />
343 @ <input type="submit" value="Re-open this post and its responses" />
344 @ <span>This does NOT save any pending changes in
345 @ the editor!</span>
346 @ </form>
347 }
348 @ </div>
349 }
350
351 /*
352 ** Emits a warning that the current forum post is CLOSED and can only
353 ** be edited or responded to by an administrator. */
354 static void forumpost_error_closed(void){
355 @ <div class='error'>This (sub)thread is CLOSED and can only be
356 @ edited or replied to by an admin user.</div>
357 }
358
359 /*
360 ** Delete a complete ForumThread and all its entries.
361 */
362 static void forumthread_delete(ForumThread *pThread){
@@ -215,10 +491,13 @@
491 for(; p; p=p->pEditPrev ){
492 p->nEdit = pPost->nEdit;
493 p->pEditTail = pPost;
494 }
495 }
496 pPost->iClosed = forum_rid_is_closed(pPost->pEditHead
497 ? pPost->pEditHead->fpid
498 : pPost->fpid, 1);
499 }
500 db_finalize(&q);
501
502 if( computeHierarchy ){
503 /* Compute the hierarchical display order */
@@ -300,25 +579,31 @@
579 pThread = forumthread_create(froot, 1);
580 fossil_print("Chronological:\n");
581 fossil_print(
582 /* 0 1 2 3 4 5 6 7 */
583 /* 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123 */
584 " sid rev closed fpid pIrt pEditPrev pEditTail hash\n");
585 for(p=pThread->pFirst; p; p=p->pNext){
586 fossil_print("%4d %4d %7d %9d %9d %9d %9d %8.8s\n",
587 p->sid, p->rev,
588 p->iClosed,
589 p->fpid, p->pIrt ? p->pIrt->fpid : 0,
590 p->pEditPrev ? p->pEditPrev->fpid : 0,
591 p->pEditTail ? p->pEditTail->fpid : 0, p->zUuid);
592 }
593 fossil_print("\nDisplay\n");
594 for(p=pThread->pDisplay; p; p=p->pDisplay){
595 fossil_print("%*s", (p->nIndent-1)*3, "");
596 if( p->pEditTail ){
597 fossil_print("%d->%d", p->fpid, p->pEditTail->fpid);
598 }else{
599 fossil_print("%d", p->fpid);
600 }
601 if( p->iClosed ){
602 fossil_print(" [closed%s]", p->iClosed<0 ? " via parent" : "");
603 }
604 fossil_print("\n");
605 }
606 forumthread_delete(pThread);
607 }
608
609 /*
@@ -511,23 +796,25 @@
796 char *zHist; /* History query string */
797 Manifest *pManifest; /* Manifest comprising the current post */
798 int bPrivate; /* True for posts awaiting moderation */
799 int bSameUser; /* True if author is also the reader */
800 int iIndent; /* Indent level */
801 int iClosed; /* True if (sub)thread is closed */
802 const char *zMimetype;/* Formatting MIME type */
803
804 /* Get the manifest for the post. Abort if not found (e.g. shunned). */
805 pManifest = manifest_get(p->fpid, CFTYPE_FORUM, 0);
806 if( !pManifest ) return;
807 iClosed = forumpost_is_closed(p, 1);
808 /* When not in raw mode, create the border around the post. */
809 if( !bRaw ){
810 /* Open the <div> enclosing the post. Set the class string to mark the post
811 ** as selected and/or obsolete. */
812 iIndent = (p->pEditHead ? p->pEditHead->nIndent : p->nIndent)-1;
813 @ <div id='forum%d(p->fpid)' class='forumTime\
814 @ %s(bSelect ? " forumSel" : "")\
815 @ %s(iClosed ? " forumClosed" : "")\
816 @ %s(p->pEditTail ? " forumObs" : "")' \
817 if( iIndent && iIndentScale ){
818 @ style='margin-left:%d(iIndent*iIndentScale)ex;'>
819 }else{
820 @ >
@@ -543,11 +830,11 @@
830 ** * The post is unedited
831 ** * The post was last edited by the original author
832 ** * The post was last edited by a different person
833 */
834 if( p->pEditHead ){
835 zDate = db_text(0, "SELECT datetime(%.17g,toLocal())",
836 p->pEditHead->rDate);
837 }else{
838 zPosterName = forum_post_display_name(p, pManifest);
839 zEditorName = zPosterName;
840 }
@@ -555,11 +842,11 @@
842 if( p->pEditPrev ){
843 zPosterName = forum_post_display_name(p->pEditHead, 0);
844 zEditorName = forum_post_display_name(p, pManifest);
845 zHist = bHist ? "" : zQuery[0]==0 ? "?hist" : "&hist";
846 @ <h3 class='forumPostHdr'>(%d(p->sid)\
847 @ .%0*d(fossil_num_digits(p->nEdit))(p->rev))
848 if( fossil_strcmp(zPosterName, zEditorName)==0 ){
849 @ By %s(zPosterName) on %h(zDate) edited from \
850 @ %z(href("%R/forumpost/%S%s%s",p->pEditPrev->zUuid,zQuery,zHist))\
851 @ %d(p->sid).%0*d(fossil_num_digits(p->nEdit))(p->pEditPrev->rev)</a>
852 }else{
@@ -568,11 +855,11 @@
855 @ %z(href("%R/forumpost/%S%s%s",p->pEditPrev->zUuid,zQuery,zHist))\
856 @ %d(p->sid).%0*d(fossil_num_digits(p->nEdit))(p->pEditPrev->rev)</a>
857 }
858 }else{
859 zPosterName = forum_post_display_name(p, pManifest);
860 @ <h3 class='forumPostHdr'>(%d(p->sid))
861 @ By %s(zPosterName) on %h(zDate)
862 }
863 fossil_free(zDate);
864
865
@@ -631,18 +918,26 @@
918 /* When not in raw mode, finish creating the border around the post. */
919 if( !bRaw ){
920 /* If the user is able to write to the forum and if this post has not been
921 ** edited, create a form with various interaction buttons. */
922 if( g.perm.WrForum && !p->pEditTail ){
923 @ <div class="forumpost-single-controls">\
924 @ <form action="%R/forumedit" method="POST">
925 @ <input type="hidden" name="fpid" value="%s(p->zUuid)">
926 if( !bPrivate ){
927 /* Reply and Edit are only available if the post has been
928 ** approved. Closed threads can only be edited or replied to
929 ** if forumpost_may_close() is true but a user may delete
930 ** their own posts even if they are closed. */
931 if( forumpost_may_close() || !iClosed ){
932 @ <input type="submit" name="reply" value="Reply">
933 if( g.perm.Admin || (bSameUser && !iClosed) ){
934 @ <input type="submit" name="edit" value="Edit">
935 }
936 if( g.perm.Admin || bSameUser ){
937 @ <input type="submit" name="nullout" value="Delete">
938 }
939 }
940 }else if( g.perm.ModForum ){
941 /* Allow moderators to approve or reject pending posts. Also allow
942 ** forum supervisors to mark non-special users as trusted and therefore
943 ** able to post unmoderated. */
@@ -657,11 +952,20 @@
952 }
953 }else if( bSameUser ){
954 /* Allow users to delete (reject) their own pending posts. */
955 @ <input type="submit" name="reject" value="Delete">
956 }
957 @ </form>
958 if( bSelect && forumpost_may_close() && iClosed>=0 ){
959 int iHead = forumpost_head_rid(p->fpid);
960 @ <form method="post" \
961 @ action='%R/forumpost_%s(iClosed > 0 ? "reopen" : "close")'>
962 @ <input type="hidden" name="fpid" value="%z(rid_to_uuid(iHead))" />
963 @ <input type="submit" value='%s(iClosed ? "Re-open" : "Close")' />
964 @ </form>
965 }
966 @ </div>
967 }
968 @ </div>
969 }
970
971 /* Clean up. */
@@ -1040,10 +1344,15 @@
1344 Blob x, cksum, formatCheck, errMsg;
1345 Manifest *pPost;
1346 int nContent = zContent ? (int)strlen(zContent) : 0;
1347
1348 schema_forum();
1349 if( !g.perm.Admin && (iEdit || iInReplyTo)
1350 && forum_rid_is_closed(iEdit ? iEdit : iInReplyTo, 1) ){
1351 forumpost_error_closed();
1352 return 0;
1353 }
1354 if( iEdit==0 && whitespace_only(zContent) ){
1355 return 0;
1356 }
1357 if( iInReplyTo==0 && iEdit>0 ){
1358 iBasis = iEdit;
@@ -1141,10 +1450,44 @@
1450 @ %z(href("%R/markup_help"))Markup style</a>:
1451 mimetype_option_menu(zMimetype, "mimetype");
1452 @ <br><textarea aria-label="Content:" name="content" class="wikiedit" \
1453 @ cols="80" rows="25" wrap="virtual">%h(zContent)</textarea><br>
1454 }
1455
1456 /*
1457 ** WEBPAGE: forumpost_close hidden
1458 ** WEBPAGE: forumpost_reopen hidden
1459 **
1460 ** fpid=X Hash of the post to be edited. REQUIRED
1461 ** reason=X Optional reason for closure.
1462 **
1463 ** Closes or re-opens the given forum post, within the bounds of the
1464 ** API for forumpost_close(). After (perhaps) modifying the "closed"
1465 ** status of the given thread, it redirects to that post's thread
1466 ** view. Requires admin privileges.
1467 */
1468 void forum_page_close(void){
1469 const char *zFpid = PD("fpid","");
1470 const char *zReason = 0;
1471 int fClose;
1472 int fpid;
1473
1474 login_check_credentials();
1475 if( forumpost_may_close()==0 ){
1476 login_needed(g.anon.Admin);
1477 return;
1478 }
1479 fpid = symbolic_name_to_rid(zFpid, "f");
1480 if( fpid<=0 ){
1481 webpage_error("Missing or invalid fpid query parameter");
1482 }
1483 fClose = sqlite3_strglob("*_close*", g.zPath)==0;
1484 if( fClose ) zReason = PD("reason",0);
1485 forumpost_close(fpid, fClose, zReason);
1486 cgi_redirectf("%R/forumpost/%S",zFpid);
1487 return;
1488 }
1489
1490 /*
1491 ** WEBPAGE: forumnew
1492 ** WEBPAGE: forumedit
1493 **
@@ -1152,10 +1495,11 @@
1495 ** But first prompt to see if the user would like to log in.
1496 */
1497 void forum_page_init(void){
1498 int isEdit;
1499 char *zGoto;
1500
1501 login_check_credentials();
1502 if( !g.perm.WrForum ){
1503 login_needed(g.anon.WrForum);
1504 return;
1505 }
@@ -1291,11 +1635,13 @@
1635 const char *zTitle = 0;
1636 char *zDate = 0;
1637 const char *zFpid = PD("fpid","");
1638 int isCsrfSafe;
1639 int isDelete = 0;
1640 int iClosed = 0;
1641 int bSameUser; /* True if author is also the reader */
1642 int bPreview; /* True in preview mode. */
1643 int bPrivate; /* True if post is private (not yet moderated) */
1644
1645 login_check_credentials();
1646 if( !g.perm.WrForum ){
1647 login_needed(g.anon.WrForum);
@@ -1308,13 +1654,15 @@
1654 froot = db_int(0, "SELECT froot FROM forumpost WHERE fpid=%d", fpid);
1655 if( froot==0 || (pRootPost = manifest_get(froot, CFTYPE_FORUM, 0))==0 ){
1656 webpage_error("fpid does not appear to be a forum post: \"%d\"", fpid);
1657 }
1658 if( P("cancel") ){
1659 cgi_redirectf("%R/forumpost/%S",zFpid);
1660 return;
1661 }
1662 bPreview = P("preview")!=0;
1663 iClosed = forum_rid_is_closed(fpid, 1);
1664 isCsrfSafe = cgi_csrf_safe(1);
1665 bPrivate = content_is_private(fpid);
1666 bSameUser = login_is_individual()
1667 && fossil_strcmp(pPost->zUser, g.zLogin)==0;
1668 if( isCsrfSafe && (g.perm.ModForum || (bPrivate && bSameUser)) ){
@@ -1376,10 +1724,11 @@
1724 if( pPost->zThreadTitle ) zTitle = "";
1725 style_header("Delete %s", zTitle ? "Post" : "Reply");
1726 @ <h1>Original Post:</h1>
1727 forum_render(pPost->zThreadTitle, pPost->zMimetype, pPost->zWiki,
1728 "forumEdit", 1);
1729 forumpost_emit_closed_state(fpid, iClosed);
1730 @ <h1>Change Into:</h1>
1731 forum_render(zTitle, zMimetype, zContent,"forumEdit", 1);
1732 @ <form action="%R/forume2" method="POST">
1733 @ <input type="hidden" name="fpid" value="%h(P("fpid"))">
1734 @ <input type="hidden" name="nullout" value="1">
@@ -1400,11 +1749,11 @@
1749 }
1750 style_header("Edit %s", zTitle ? "Post" : "Reply");
1751 @ <h2>Original Post:</h2>
1752 forum_render(pPost->zThreadTitle, pPost->zMimetype, pPost->zWiki,
1753 "forumEdit", 1);
1754 if( bPreview ){
1755 @ <h2>Preview of Edited Post:</h2>
1756 forum_render(zTitle, zMimetype, zContent,"forumEdit", 1);
1757 }
1758 @ <h2>Revised Message:</h2>
1759 @ <form action="%R/forume2" method="POST">
@@ -1429,11 +1778,11 @@
1778 zDisplayName = display_name_from_login(pPost->zUser);
1779 @ <h3 class='forumPostHdr'>By %s(zDisplayName) on %h(zDate)</h3>
1780 fossil_free(zDisplayName);
1781 fossil_free(zDate);
1782 forum_render(0, pPost->zMimetype, pPost->zWiki, "forumEdit", 1);
1783 if( bPreview && !whitespace_only(zContent) ){
1784 @ <h2>Preview:</h2>
1785 forum_render(0, zMimetype,zContent, "forumEdit", 1);
1786 }
1787 @ <h2>Enter Reply:</h2>
1788 @ <form action="%R/forume2" method="POST">
@@ -1444,16 +1793,140 @@
1793 }
1794 if( !isDelete ){
1795 @ <input type="submit" name="preview" value="Preview">
1796 }
1797 @ <input type="submit" name="cancel" value="Cancel">
1798 if( (bPreview && !whitespace_only(zContent)) || isDelete ){
1799 if( !iClosed || g.perm.Admin ) {
1800 @ <input type="submit" name="submit" value="Submit">
1801 }
1802 }
1803 forum_render_debug_options();
1804 @ </form>
1805 forum_emit_js();
1806 forumpost_emit_closed_state(fpid, iClosed);
1807 style_finish_page();
1808 }
1809
1810 /*
1811 ** WEBPAGE: setup_forum
1812 **
1813 ** Forum configuration and metrics.
1814 */
1815 void forum_setup(void){
1816 /* boolean config settings specific to the forum. */
1817 const char * zSettingsBool[] = {
1818 "forum-close-policy",
1819 NULL /* sentinel entry */
1820 };
1821
1822 login_check_credentials();
1823 if( !g.perm.Setup ){
1824 login_needed(g.anon.Setup);
1825 return;
1826 }
1827 style_set_current_feature("forum");
1828 style_header("Forum Setup");
1829
1830 @ <h2>Metrics</h2>
1831 {
1832 int nPosts = db_int(0, "SELECT COUNT(*) FROM event WHERE type='f'");
1833 @ <p><a href='%R/forum'>Forum posts</a>:
1834 @ <a href='%R/timeline?y=f'>%d(nPosts)</a></p>
1835 }
1836
1837 @ <h2>Supervisors</h2>
1838 @ <p>Users with capabilities 's', 'a', or '6'.</p>
1839 {
1840 Stmt q = empty_Stmt;
1841 int nRows = 0;
1842 db_prepare(&q, "SELECT uid, login, cap FROM user "
1843 "WHERE cap GLOB '*[as6]*' ORDER BY login");
1844 @ <table class='bordered'>
1845 @ <thead><tr><th>User</th><th>Capabilities</th></tr></thead>
1846 @ <tbody>
1847 while( SQLITE_ROW==db_step(&q) ){
1848 const int iUid = db_column_int(&q, 0);
1849 const char *zUser = db_column_text(&q, 1);
1850 const char *zCap = db_column_text(&q, 2);
1851 ++nRows;
1852 @ <tr>
1853 @ <td><a href='%R/setup_uedit?id=%d(iUid)'>%h(zUser)</a></td>
1854 @ <td>(%h(zCap))</td>
1855 @ </tr>
1856 }
1857 db_finalize(&q);
1858 @</tbody></table>
1859 if( 0==nRows ){
1860 @ No supervisors
1861 }else{
1862 @ %d(nRows) supervisor(s)
1863 }
1864 }
1865
1866 @ <h2>Moderators</h2>
1867 @ <p>Users with capability '5'.</p>
1868 {
1869 Stmt q = empty_Stmt;
1870 int nRows = 0;
1871 db_prepare(&q, "SELECT uid, login, cap FROM user "
1872 "WHERE cap GLOB '*5*' ORDER BY login");
1873 @ <table class='bordered'>
1874 @ <thead><tr><th>User</th><th>Capabilities</th></tr></thead>
1875 @ <tbody>
1876 while( SQLITE_ROW==db_step(&q) ){
1877 const int iUid = db_column_int(&q, 0);
1878 const char *zUser = db_column_text(&q, 1);
1879 const char *zCap = db_column_text(&q, 2);
1880 ++nRows;
1881 @ <tr>
1882 @ <td><a href='%R/setup_uedit?id=%d(iUid)'>%h(zUser)</a></td>
1883 @ <td>(%h(zCap))</td>
1884 @ </tr>
1885 }
1886 db_finalize(&q);
1887 @ </tbody></table>
1888 if( 0==nRows ){
1889 @ No non-supervisor moderators
1890 }else{
1891 @ %d(nRows) moderator(s)
1892 }
1893 }
1894
1895 @ <h2>Settings</h2>
1896 @ <p>Configuration settings specific to the forum.</p>
1897 if( P("submit") && cgi_csrf_safe(1) ){
1898 int i = 0;
1899 const char *zSetting;
1900 login_verify_csrf_secret();
1901 db_begin_transaction();
1902 while( (zSetting = zSettingsBool[i++]) ){
1903 const char *z = P(zSetting);
1904 if( !z || !z[0] ) z = "off";
1905 db_set(zSetting/*works-like:"x"*/, z, 0);
1906 }
1907 db_end_transaction(0);
1908 @ <p><em>Settings saved.</em></p>
1909 }
1910 {
1911 int i = 0;
1912 const char *zSetting;
1913 @ <form action="%R/setup_forum" method="post">
1914 login_insert_csrf_secret();
1915 @ <table class='forum-settings-list'><tbody>
1916 while( (zSetting = zSettingsBool[i++]) ){
1917 @ <tr><td>
1918 onoff_attribute("", zSetting, zSetting/*works-like:"x"*/, 0, 0);
1919 @ </td><td>
1920 @ <a href='%R/help?cmd=%h(zSetting)'>%h(zSetting)</a>
1921 @ </td></tr>
1922 }
1923 @ </tbody></table>
1924 @ <input type='submit' name='submit' value='Apply changes'>
1925 @ </form>
1926 }
1927
1928 style_finish_page();
1929 }
1930
1931 /*
1932 ** WEBPAGE: forummain
1933
--- src/json_branch.c
+++ src/json_branch.c
@@ -293,11 +293,11 @@
293293
294294
brid = content_put_ex(&branch, 0, 0, 0, zOpt->isPrivate);
295295
if( brid==0 ){
296296
fossil_panic("Problem committing manifest: %s", g.zErrMsg);
297297
}
298
- db_multi_exec("INSERT OR IGNORE INTO unsent VALUES(%d)", brid);
298
+ db_add_unsent(brid);
299299
if( manifest_crosslink(brid, &branch, MC_PERMIT_HOOKS)==0 ){
300300
fossil_panic("%s", g.zErrMsg);
301301
}
302302
assert( blob_is_reset(&branch) );
303303
content_deltify(rootid, &brid, 1, 0);
304304
--- src/json_branch.c
+++ src/json_branch.c
@@ -293,11 +293,11 @@
293
294 brid = content_put_ex(&branch, 0, 0, 0, zOpt->isPrivate);
295 if( brid==0 ){
296 fossil_panic("Problem committing manifest: %s", g.zErrMsg);
297 }
298 db_multi_exec("INSERT OR IGNORE INTO unsent VALUES(%d)", brid);
299 if( manifest_crosslink(brid, &branch, MC_PERMIT_HOOKS)==0 ){
300 fossil_panic("%s", g.zErrMsg);
301 }
302 assert( blob_is_reset(&branch) );
303 content_deltify(rootid, &brid, 1, 0);
304
--- src/json_branch.c
+++ src/json_branch.c
@@ -293,11 +293,11 @@
293
294 brid = content_put_ex(&branch, 0, 0, 0, zOpt->isPrivate);
295 if( brid==0 ){
296 fossil_panic("Problem committing manifest: %s", g.zErrMsg);
297 }
298 db_add_unsent(brid);
299 if( manifest_crosslink(brid, &branch, MC_PERMIT_HOOKS)==0 ){
300 fossil_panic("%s", g.zErrMsg);
301 }
302 assert( blob_is_reset(&branch) );
303 content_deltify(rootid, &brid, 1, 0);
304
+3 -1
--- src/setup.c
+++ src/setup.c
@@ -130,10 +130,12 @@
130130
"Configure the wiki for this repository");
131131
setup_menu_entry("Interwiki Map", "intermap",
132132
"Mapping keywords for interwiki links");
133133
setup_menu_entry("Chat", "setup_chat",
134134
"Configure the chatroom");
135
+ setup_menu_entry("Forum", "setup_forum",
136
+ "Forum config and metrics");
135137
}
136138
setup_menu_entry("Search","srchsetup",
137139
"Configure the built-in search engine");
138140
setup_menu_entry("URL Aliases", "waliassetup",
139141
"Configure URL aliases");
@@ -1972,11 +1974,11 @@
19721974
@ <p><a href="admin_log?n=%d(limit)&x=%d(prevx)">[Newer]</a></p>
19731975
}
19741976
db_prepare(&stLog,
19751977
"SELECT datetime(time,'unixepoch'), who, page, what "
19761978
"FROM admin_log "
1977
- "ORDER BY time DESC");
1979
+ "ORDER BY time DESC, rowid DESC");
19781980
style_table_sorter();
19791981
@ <table class="sortable adminLogTable" width="100%%" \
19801982
@ data-column-types='Tttx' data-init-sort='1'>
19811983
@ <thead>
19821984
@ <th>Time</th>
19831985
--- src/setup.c
+++ src/setup.c
@@ -130,10 +130,12 @@
130 "Configure the wiki for this repository");
131 setup_menu_entry("Interwiki Map", "intermap",
132 "Mapping keywords for interwiki links");
133 setup_menu_entry("Chat", "setup_chat",
134 "Configure the chatroom");
 
 
135 }
136 setup_menu_entry("Search","srchsetup",
137 "Configure the built-in search engine");
138 setup_menu_entry("URL Aliases", "waliassetup",
139 "Configure URL aliases");
@@ -1972,11 +1974,11 @@
1972 @ <p><a href="admin_log?n=%d(limit)&x=%d(prevx)">[Newer]</a></p>
1973 }
1974 db_prepare(&stLog,
1975 "SELECT datetime(time,'unixepoch'), who, page, what "
1976 "FROM admin_log "
1977 "ORDER BY time DESC");
1978 style_table_sorter();
1979 @ <table class="sortable adminLogTable" width="100%%" \
1980 @ data-column-types='Tttx' data-init-sort='1'>
1981 @ <thead>
1982 @ <th>Time</th>
1983
--- src/setup.c
+++ src/setup.c
@@ -130,10 +130,12 @@
130 "Configure the wiki for this repository");
131 setup_menu_entry("Interwiki Map", "intermap",
132 "Mapping keywords for interwiki links");
133 setup_menu_entry("Chat", "setup_chat",
134 "Configure the chatroom");
135 setup_menu_entry("Forum", "setup_forum",
136 "Forum config and metrics");
137 }
138 setup_menu_entry("Search","srchsetup",
139 "Configure the built-in search engine");
140 setup_menu_entry("URL Aliases", "waliassetup",
141 "Configure URL aliases");
@@ -1972,11 +1974,11 @@
1974 @ <p><a href="admin_log?n=%d(limit)&x=%d(prevx)">[Newer]</a></p>
1975 }
1976 db_prepare(&stLog,
1977 "SELECT datetime(time,'unixepoch'), who, page, what "
1978 "FROM admin_log "
1979 "ORDER BY time DESC, rowid DESC");
1980 style_table_sorter();
1981 @ <table class="sortable adminLogTable" width="100%%" \
1982 @ data-column-types='Tttx' data-init-sort='1'>
1983 @ <thead>
1984 @ <th>Time</th>
1985
+31
--- src/tag.c
+++ src/tag.c
@@ -903,5 +903,36 @@
903903
" AND tag.tagid=%d"
904904
" AND tagxref.tagid=tag.tagid",
905905
rid, tagId
906906
);
907907
}
908
+
909
+
910
+/*
911
+** Returns tagxref.rowid if the given blob.rid has a tagxref.rid entry
912
+** of an active (non-cancelled) tag matching the given rid and tag
913
+** name string, else returns 0. Note that this function does not
914
+** distinguish between a non-existent tag and a cancelled tag.
915
+**
916
+** Design note: the return value is the tagxref.rowid because that
917
+** gives us an easy way to fetch the value of the tag later on, if
918
+** needed.
919
+*/
920
+int rid_has_active_tag_name(int rid, const char *zTagName){
921
+ static Stmt q = empty_Stmt_m;
922
+ int rc;
923
+
924
+ assert( 0 != zTagName );
925
+ if( !q.pStmt ){
926
+ db_static_prepare(&q,
927
+ "SELECT x.rowid FROM tagxref x, tag t"
928
+ " WHERE x.rid=$rid AND x.tagtype>0 "
929
+ " AND x.tagid=t.tagid"
930
+ " AND t.tagname=$tagname"
931
+ );
932
+ }
933
+ db_bind_int(&q, "$rid", rid);
934
+ db_bind_text(&q, "$tagname", zTagName);
935
+ rc = (SQLITE_ROW==db_step(&q)) ? db_column_int(&q, 0) : 0;
936
+ db_reset(&q);
937
+ return rc;
938
+}
908939
--- src/tag.c
+++ src/tag.c
@@ -903,5 +903,36 @@
903 " AND tag.tagid=%d"
904 " AND tagxref.tagid=tag.tagid",
905 rid, tagId
906 );
907 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
908
--- src/tag.c
+++ src/tag.c
@@ -903,5 +903,36 @@
903 " AND tag.tagid=%d"
904 " AND tagxref.tagid=tag.tagid",
905 rid, tagId
906 );
907 }
908
909
910 /*
911 ** Returns tagxref.rowid if the given blob.rid has a tagxref.rid entry
912 ** of an active (non-cancelled) tag matching the given rid and tag
913 ** name string, else returns 0. Note that this function does not
914 ** distinguish between a non-existent tag and a cancelled tag.
915 **
916 ** Design note: the return value is the tagxref.rowid because that
917 ** gives us an easy way to fetch the value of the tag later on, if
918 ** needed.
919 */
920 int rid_has_active_tag_name(int rid, const char *zTagName){
921 static Stmt q = empty_Stmt_m;
922 int rc;
923
924 assert( 0 != zTagName );
925 if( !q.pStmt ){
926 db_static_prepare(&q,
927 "SELECT x.rowid FROM tagxref x, tag t"
928 " WHERE x.rid=$rid AND x.tagtype>0 "
929 " AND x.tagid=t.tagid"
930 " AND t.tagname=$tagname"
931 );
932 }
933 db_bind_int(&q, "$rid", rid);
934 db_bind_text(&q, "$tagname", zTagName);
935 rc = (SQLITE_ROW==db_step(&q)) ? db_column_int(&q, 0) : 0;
936 db_reset(&q);
937 return rc;
938 }
939
+1 -1
--- src/tkt.c
+++ src/tkt.c
@@ -864,11 +864,11 @@
864864
db_multi_exec(
865865
"INSERT INTO modreq(objid, tktid) VALUES(%d,%Q)",
866866
rid, zTktId
867867
);
868868
}else{
869
- db_multi_exec("INSERT OR IGNORE INTO unsent VALUES(%d);", rid);
869
+ db_add_unsent(rid);
870870
db_multi_exec("INSERT OR IGNORE INTO unclustered VALUES(%d);", rid);
871871
}
872872
result = (manifest_crosslink(rid, pTicket, MC_NONE)==0);
873873
assert( blob_is_reset(pTicket) );
874874
if( !result ){
875875
--- src/tkt.c
+++ src/tkt.c
@@ -864,11 +864,11 @@
864 db_multi_exec(
865 "INSERT INTO modreq(objid, tktid) VALUES(%d,%Q)",
866 rid, zTktId
867 );
868 }else{
869 db_multi_exec("INSERT OR IGNORE INTO unsent VALUES(%d);", rid);
870 db_multi_exec("INSERT OR IGNORE INTO unclustered VALUES(%d);", rid);
871 }
872 result = (manifest_crosslink(rid, pTicket, MC_NONE)==0);
873 assert( blob_is_reset(pTicket) );
874 if( !result ){
875
--- src/tkt.c
+++ src/tkt.c
@@ -864,11 +864,11 @@
864 db_multi_exec(
865 "INSERT INTO modreq(objid, tktid) VALUES(%d,%Q)",
866 rid, zTktId
867 );
868 }else{
869 db_add_unsent(rid);
870 db_multi_exec("INSERT OR IGNORE INTO unclustered VALUES(%d);", rid);
871 }
872 result = (manifest_crosslink(rid, pTicket, MC_NONE)==0);
873 assert( blob_is_reset(pTicket) );
874 if( !result ){
875
+1 -1
--- src/wiki.c
+++ src/wiki.c
@@ -635,11 +635,11 @@
635635
}else{
636636
nrid = content_put_ex(pWiki, 0, 0, 0, 1);
637637
moderation_table_create();
638638
db_multi_exec("INSERT INTO modreq(objid) VALUES(%d)", nrid);
639639
}
640
- db_multi_exec("INSERT OR IGNORE INTO unsent VALUES(%d)", nrid);
640
+ db_add_unsent(nrid);
641641
db_multi_exec("INSERT OR IGNORE INTO unclustered VALUES(%d);", nrid);
642642
manifest_crosslink(nrid, pWiki, MC_NONE);
643643
if( login_is_individual() ){
644644
alert_user_contact(login_name());
645645
}
646646
--- src/wiki.c
+++ src/wiki.c
@@ -635,11 +635,11 @@
635 }else{
636 nrid = content_put_ex(pWiki, 0, 0, 0, 1);
637 moderation_table_create();
638 db_multi_exec("INSERT INTO modreq(objid) VALUES(%d)", nrid);
639 }
640 db_multi_exec("INSERT OR IGNORE INTO unsent VALUES(%d)", nrid);
641 db_multi_exec("INSERT OR IGNORE INTO unclustered VALUES(%d);", nrid);
642 manifest_crosslink(nrid, pWiki, MC_NONE);
643 if( login_is_individual() ){
644 alert_user_contact(login_name());
645 }
646
--- src/wiki.c
+++ src/wiki.c
@@ -635,11 +635,11 @@
635 }else{
636 nrid = content_put_ex(pWiki, 0, 0, 0, 1);
637 moderation_table_create();
638 db_multi_exec("INSERT INTO modreq(objid) VALUES(%d)", nrid);
639 }
640 db_add_unsent(nrid);
641 db_multi_exec("INSERT OR IGNORE INTO unclustered VALUES(%d);", nrid);
642 manifest_crosslink(nrid, pWiki, MC_NONE);
643 if( login_is_individual() ){
644 alert_user_contact(login_name());
645 }
646
--- www/changes.wiki
+++ www/changes.wiki
@@ -1,6 +1,14 @@
11
<title>Change Log</title>
2
+
3
+<h2 id='v2_23'>Changes for version 2.23 (pending)</h2>
4
+
5
+ * Add ability to "close" forum threads, such that unprivileged users
6
+ may no longer respond to them. Only administrators can close
7
+ threads or respond to them by default, and the
8
+ [/help?cmd=forum-close-policy|forum-close-policy setting] can be
9
+ used to add that capability to moderators.
210
311
<h2 id='v2_22'>Changes for version 2.22 (2023-05-31)</h2>
412
* Enhancements to the [/help?cmd=/timeline|/timeline webpage]: <ol type="a">
513
<li> Add the ft=TAG query parameter which in combination with d=Y
614
shows all descendants of Y up to TAG
715
--- www/changes.wiki
+++ www/changes.wiki
@@ -1,6 +1,14 @@
1 <title>Change Log</title>
 
 
 
 
 
 
 
 
2
3 <h2 id='v2_22'>Changes for version 2.22 (2023-05-31)</h2>
4 * Enhancements to the [/help?cmd=/timeline|/timeline webpage]: <ol type="a">
5 <li> Add the ft=TAG query parameter which in combination with d=Y
6 shows all descendants of Y up to TAG
7
--- www/changes.wiki
+++ www/changes.wiki
@@ -1,6 +1,14 @@
1 <title>Change Log</title>
2
3 <h2 id='v2_23'>Changes for version 2.23 (pending)</h2>
4
5 * Add ability to "close" forum threads, such that unprivileged users
6 may no longer respond to them. Only administrators can close
7 threads or respond to them by default, and the
8 [/help?cmd=forum-close-policy|forum-close-policy setting] can be
9 used to add that capability to moderators.
10
11 <h2 id='v2_22'>Changes for version 2.22 (2023-05-31)</h2>
12 * Enhancements to the [/help?cmd=/timeline|/timeline webpage]: <ol type="a">
13 <li> Add the ft=TAG query parameter which in combination with d=Y
14 shows all descendants of Y up to TAG
15
--- www/changes.wiki
+++ www/changes.wiki
@@ -1,6 +1,14 @@
11
<title>Change Log</title>
2
+
3
+<h2 id='v2_23'>Changes for version 2.23 (pending)</h2>
4
+
5
+ * Add ability to "close" forum threads, such that unprivileged users
6
+ may no longer respond to them. Only administrators can close
7
+ threads or respond to them by default, and the
8
+ [/help?cmd=forum-close-policy|forum-close-policy setting] can be
9
+ used to add that capability to moderators.
210
311
<h2 id='v2_22'>Changes for version 2.22 (2023-05-31)</h2>
412
* Enhancements to the [/help?cmd=/timeline|/timeline webpage]: <ol type="a">
513
<li> Add the ft=TAG query parameter which in combination with d=Y
614
shows all descendants of Y up to TAG
715
--- www/changes.wiki
+++ www/changes.wiki
@@ -1,6 +1,14 @@
1 <title>Change Log</title>
 
 
 
 
 
 
 
 
2
3 <h2 id='v2_22'>Changes for version 2.22 (2023-05-31)</h2>
4 * Enhancements to the [/help?cmd=/timeline|/timeline webpage]: <ol type="a">
5 <li> Add the ft=TAG query parameter which in combination with d=Y
6 shows all descendants of Y up to TAG
7
--- www/changes.wiki
+++ www/changes.wiki
@@ -1,6 +1,14 @@
1 <title>Change Log</title>
2
3 <h2 id='v2_23'>Changes for version 2.23 (pending)</h2>
4
5 * Add ability to "close" forum threads, such that unprivileged users
6 may no longer respond to them. Only administrators can close
7 threads or respond to them by default, and the
8 [/help?cmd=forum-close-policy|forum-close-policy setting] can be
9 used to add that capability to moderators.
10
11 <h2 id='v2_22'>Changes for version 2.22 (2023-05-31)</h2>
12 * Enhancements to the [/help?cmd=/timeline|/timeline webpage]: <ol type="a">
13 <li> Add the ft=TAG query parameter which in combination with d=Y
14 shows all descendants of Y up to TAG
15

Keyboard Shortcuts

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