Fossil SCM

Rework forumpost closure to always apply to the first artifact in an edit chain to enable consistent behavior across the whole chain and responses to arbitrary versions within that chain. Add rudimentary UI elements for closing/re-opening posts, but their layout needs to be revisited (noting that they need to be in a separate form from the main editor so that closing/re-opening introduces only a smalll control artifact instead of a whole forumpost artifact).

stephan 2023-02-22 04:46 forumpost-locking
Commit cc6ca4e110a7cfcc66ad9034389162ef0790bb7c6c8f5a1c36fb7ba5d77bccf6
2 files changed +13 -1 +160 -74
+13 -1
--- src/default.css
+++ src/default.css
@@ -904,16 +904,28 @@
904904
div.forumClosed {
905905
opacity: 0.7;
906906
}
907907
div.forumClosed > *:first-child::before {
908908
content: "[CLOSED] ";
909
- color: red;
909
+ color: darkred;
910910
opacity: 0.7;
911911
}
912912
/*div.forumClosed > div.forumPostBody {
913913
filter: blur(5px);
914914
}*/
915
+div.forumpost-closed-warning {
916
+ margin-top: 1em;
917
+ margin-bottom: 1em;
918
+ border-style: solid;
919
+ padding: 0.25em 0.5em;
920
+ background: yellow;
921
+ color: darkred;
922
+ font-weight: bold;
923
+}
924
+div.forumpost-closed-warning input[type=submit] {
925
+ padding: 0.25em;
926
+}
915927
.forum div > form {
916928
margin: 0.5em 0;
917929
}
918930
.forum-post-collapser {
919931
/* Common style for the bottom-of-post and right-of-post
920932
--- src/default.css
+++ src/default.css
@@ -904,16 +904,28 @@
904 div.forumClosed {
905 opacity: 0.7;
906 }
907 div.forumClosed > *:first-child::before {
908 content: "[CLOSED] ";
909 color: red;
910 opacity: 0.7;
911 }
912 /*div.forumClosed > div.forumPostBody {
913 filter: blur(5px);
914 }*/
 
 
 
 
 
 
 
 
 
 
 
 
915 .forum div > form {
916 margin: 0.5em 0;
917 }
918 .forum-post-collapser {
919 /* Common style for the bottom-of-post and right-of-post
920
--- src/default.css
+++ src/default.css
@@ -904,16 +904,28 @@
904 div.forumClosed {
905 opacity: 0.7;
906 }
907 div.forumClosed > *:first-child::before {
908 content: "[CLOSED] ";
909 color: darkred;
910 opacity: 0.7;
911 }
912 /*div.forumClosed > div.forumPostBody {
913 filter: blur(5px);
914 }*/
915 div.forumpost-closed-warning {
916 margin-top: 1em;
917 margin-bottom: 1em;
918 border-style: solid;
919 padding: 0.25em 0.5em;
920 background: yellow;
921 color: darkred;
922 font-weight: bold;
923 }
924 div.forumpost-closed-warning input[type=submit] {
925 padding: 0.25em;
926 }
927 .forum div > form {
928 margin: 0.5em 0;
929 }
930 .forum-post-collapser {
931 /* Common style for the bottom-of-post and right-of-post
932
+160 -74
--- src/forum.c
+++ src/forum.c
@@ -80,90 +80,112 @@
8080
db_reset(&q);
8181
return res;
8282
}
8383
8484
/*
85
-** Returns true if p, or any parent of p, has an active "closed" tag.
86
-** Returns 0 if !p. For an edited chain of post, the tag is checked on
87
-** the final edit in the chain, as that permits that a post can be
88
-** locked and later unlocked. The return value is the tagxref.rowid
89
-** value of the tagxref entry which applies the "closed" tag, or 0 if
90
-** no active tag is found.
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.
91110
**
92
-** If bCheckParents is true then p's thread parents are checked
93
-** (recursively) for closure, else only p is checked.
111
+** If bCheckIrt is true then p's thread in-response-to parents are
112
+** checked (recursively) for closure, else only p is checked.
94113
*/
95
-static int forum_post_is_closed(ForumPost *p, int bCheckParents){
96
- if( !p ) return 0;
97
- if( p->pEditTail ) p = p->pEditTail;
98
- if( p->iClosed || !bCheckParents ) return p->iClosed;
99
- else if( p->pIrt ){
100
- return forum_post_is_closed(p->pIrt->pEditTail
101
- ? p->pIrt->pEditTail : p->pIrt,
102
- bCheckParents);
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;
103119
}
104120
return 0;
105121
}
106122
107123
/*
108124
** Given a forum post RID, this function returns true if that post has
109
-** an active "closed" tag. If bCheckParents is true, the latest
110
-** version of each parent post is also checked (recursively), else
111
-** they are not. When checking parents, the first parent which is
112
-** closed ends the search.
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.
113136
**
114137
** The return value is one of:
115138
**
116139
** - 0 if no "closed" tag is found.
117140
**
118141
** - The tagxref.rowid of the tagxref entry for the closure if rid is
119
-** the artifact to which the closure applies.
142
+** the forum post to which the closure applies.
120143
**
121144
** - (-tagxref.rowid) if the given rid inherits a "closed" tag from an
122
-** ancestor forum post.
145
+** IRT forum post.
123146
*/
124
-static int forum_rid_is_closed(int rid, int bCheckParents){
147
+static int forum_rid_is_closed(int rid, int bCheckIrt){
125148
static Stmt qIrt = empty_Stmt_m;
126
- int rc = 0;
127
-
128
- /* TODO: this can probably be turned into a CTE, rather than a
129
- ** recursive call into this function, by someone with superior
130
- ** SQL-fu. */
131
- rc = rid_has_active_tag_name(rid, "closed");
132
- if( rc || !bCheckParents ) return rc;
133
- else if( !qIrt.pStmt ) {
134
- db_static_prepare(&qIrt,
135
- "SELECT firt FROM forumpost "
136
- "WHERE fpid=$fpid ORDER BY fmtime DESC"
137
- );
138
- }
139
- db_bind_int(&qIrt, "$fpid", rid);
140
- rid = SQLITE_ROW==db_step(&qIrt) ? db_column_int(&qIrt, 0) : 0;
141
- db_reset(&qIrt);
142
- if( rid ){
143
- rc = forum_rid_is_closed(rid, 1);
144
- }
145
- return rc>0 ? -rc : rc;
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;
146166
}
147167
148168
/*
149
-** UNTESTED!
150
-**
151169
** Closes or re-opens the given forum RID via addition of a new
152
-** control artifact into the repository.
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.
153175
**
154176
** If doClose is true then a propagating "closed" tag is added, except
155177
** as noted below, with the given optional zReason string as the tag's
156178
** value. If doClose is false then any active "closed" tag on frid is
157179
** cancelled, except as noted below. zReason is ignored if doClose is
158180
** false or if zReason is NULL or starts with a NUL byte.
159181
**
160
-** This function only adds a "closed" tag to frid if
161
-** forum_rid_is_closed() indicates that frid is not closed. If a
162
-** parent post is already closed, no tag is added. Similarly, it will
163
-** only remove a "closed" tag from a post which has its own "closed"
164
-** tag, and will not remove an inherited one from a parent post.
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.
165187
**
166188
** If doClose is true and frid is closed (directly or inherited), this
167189
** is a no-op. Likewise, if doClose is false and frid itself is not
168190
** closed (not accounting for an inherited closed tag), this is a
169191
** no-op.
@@ -187,17 +209,19 @@
187209
**
188210
** - Closure of a forum post requires a propagating "closed" tag to
189211
** account for how edits of posts are handled. This differs from
190212
** closure of a branch, where a non-propagating tag is used.
191213
*/
192
-/*static*/ int forumpost_close(int frid, int doClose, const char *zReason){
214
+static int forumpost_close(int frid, int doClose, const char *zReason){
193215
Blob artifact = BLOB_INITIALIZER; /* Output artifact */
194216
Blob cksum = BLOB_INITIALIZER; /* Z-card */
195217
int iClosed; /* true if frid is closed */
196218
int trid; /* RID of new control artifact */
219
+ char *zUuid; /* UUID of head version of post */
197220
198221
db_begin_transaction();
222
+ frid = forumpost_head_rid(frid);
199223
iClosed = forum_rid_is_closed(frid, 1);
200224
if( (iClosed && doClose
201225
/* Already closed, noting that in the case of (iClosed<0), it's
202226
** actually a parent which is closed. */)
203227
|| (iClosed<=0 && !doClose
@@ -206,14 +230,15 @@
206230
return 0;
207231
}
208232
if( doClose==0 || (zReason && !zReason[0]) ){
209233
zReason = 0;
210234
}
235
+ zUuid = rid_to_uuid(frid);
211236
blob_appendf(&artifact, "D %z\n", date_in_standard_format( "now" ));
212237
blob_appendf(&artifact,
213
- "T %cclosed %z%s%F\n",
214
- doClose ? '*' : '-', rid_to_uuid(frid),
238
+ "T %cclosed %s%s%F\n",
239
+ doClose ? '*' : '-', zUuid,
215240
zReason ? " " : "", zReason ? zReason : "");
216241
blob_appendf(&artifact, "U %F\n", login_name());
217242
md5sum_blob(&artifact, &cksum);
218243
blob_appendf(&artifact, "Z %b\n", &cksum);
219244
blob_reset(&cksum);
@@ -225,10 +250,12 @@
225250
MC_NONE /*MC_PERMIT_HOOKS?*/)==0 ){
226251
fossil_fatal("%s", g.zErrMsg);
227252
}
228253
assert( blob_is_reset(&artifact) );
229254
db_add_unsent(trid);
255
+ admin_log("%s forum post %S", doClose ? "Close" : "Re-open", zUuid);
256
+ fossil_free(zUuid);
230257
/* Potential TODO: if (iClosed>0) then we could find the initial tag
231258
** artifact and content_deltify(thatRid,&trid,1,0). Given the tiny
232259
** size of these artifacts, however, that would save little space,
233260
** if any. */
234261
db_end_transaction(0);
@@ -240,21 +267,45 @@
240267
** renders either a checkbox to unlock forum post fpid (if iClosed>0)
241268
** or a SPAN.warning element that the given post inherits the CLOSED
242269
** status from a parent post (if iClosed<0). If neither of the initial
243270
** conditions is true, this is a no-op.
244271
*/
245
-static void forumpost_emit_unlock_checkbox(int iClosed, int fpid){
246
- if( iClosed && g.perm.Admin ){
247
- if( iClosed>0 ){
248
- /* Only show the "unlock" checkbox on a post which is actually
249
- ** closed, not on a post which inherits that state. */
250
- @ <label class='warning'><input type="checkbox" name="reopen" value="1">
251
- @ Re-open this CLOSED post? (NOT YET IMPLEMENTED)</label>
252
- }else{
253
- @ <span class='warning'>This post is CLOSED via a parent post</span>
254
- }
255
- }
272
+static void forumpost_emit_closed_state(int fpid, int iClosed){
273
+ const char *zCommon =
274
+ "Only admins may edit or respond to closed posts.";
275
+ int iHead = forumpost_head_rid(fpid);
276
+ /*@ forumpost_emit_closed_state(%d(fpid), %d(iClosed))<br/>*/
277
+ if( iHead != fpid ){
278
+ iClosed = forum_rid_is_closed(iHead, 1);
279
+ /*@ forumpost_emit_closed_state() %d(iHead), %d(iClosed)*/
280
+ }
281
+ if( iClosed<0 ){
282
+ @ <div class="warning forumpost-closed-warning">\
283
+ @ This post is CLOSED via a parent post. %s(zCommon)\
284
+ @ </div>
285
+ return;
286
+ }
287
+ else if( iClosed==0 ){
288
+ if( g.perm.Admin==0 ) return;
289
+ @ <div class="warning forumpost-closed-warning">
290
+ @ <form method="post" action="%R/forumpost_close">
291
+ @ <input type="hidden" name="fpid" value="%z(rid_to_uuid(iHead))" />
292
+ @ <input type="submit" value="CLOSE this post and its responses" />
293
+ @ %s(zCommon)
294
+ @ </form></div>
295
+ return;
296
+ }
297
+ assert( iClosed>0 );
298
+ /* Only show the "unlock" checkbox on a post which is actually
299
+ ** closed, not on a post which inherits that state. */
300
+ @ <div class="warning forumpost-closed-warning">\
301
+ @ This post is CLOSED. %s(zCommon)
302
+ @ <form method="post" action="%R/forumpost_reopen">
303
+ @ <input type="hidden" name="fpid" value="%z(rid_to_uuid(iHead))" />
304
+ @ <input type="submit" value="Re-open this post and its responses" />
305
+ @ </form>
306
+ @ </div>
256307
}
257308
258309
/*
259310
** Emits a warning that the current forum post is CLOSED and can only
260311
** be edited or responded to by an administrator. */
@@ -398,11 +449,13 @@
398449
for(; p; p=p->pEditPrev ){
399450
p->nEdit = pPost->nEdit;
400451
p->pEditTail = pPost;
401452
}
402453
}
403
- pPost->iClosed = forum_rid_is_closed(pPost->fpid, 1);
454
+ pPost->iClosed = forum_rid_is_closed(pPost->pEditHead
455
+ ? pPost->pEditHead->fpid
456
+ : pPost->fpid, 1);
404457
}
405458
db_finalize(&q);
406459
407460
if( computeHierarchy ){
408461
/* Compute the hierarchical display order */
@@ -654,11 +707,11 @@
654707
const char *zMimetype;/* Formatting MIME type */
655708
656709
/* Get the manifest for the post. Abort if not found (e.g. shunned). */
657710
pManifest = manifest_get(p->fpid, CFTYPE_FORUM, 0);
658711
if( !pManifest ) return;
659
- iClosed = forum_post_is_closed(p, 1);
712
+ iClosed = forumpost_is_closed(p, 1);
660713
/* When not in raw mode, create the border around the post. */
661714
if( !bRaw ){
662715
/* Open the <div> enclosing the post. Set the class string to mark the post
663716
** as selected and/or obsolete. */
664717
iIndent = (p->pEditHead ? p->pEditHead->nIndent : p->nIndent)-1;
@@ -1268,10 +1321,46 @@
12681321
@ %z(href("%R/markup_help"))Markup style</a>:
12691322
mimetype_option_menu(zMimetype, "mimetype");
12701323
@ <br><textarea aria-label="Content:" name="content" class="wikiedit" \
12711324
@ cols="80" rows="25" wrap="virtual">%h(zContent)</textarea><br>
12721325
}
1326
+
1327
+/*
1328
+** WEBPAGE: forumpost_close hidden
1329
+** WEBPAGE: forumpost_reopen hidden
1330
+**
1331
+** fpid=X Hash of the post to be edited. REQUIRED
1332
+** reason=X Optional reason for closure.
1333
+**
1334
+** Closes or re-opens the given forum post, within the bounds of the
1335
+** API for forumpost_close(). After (perhaps) modifying the "closed"
1336
+** status of the given thread, it redirects to that post's thread
1337
+** view. Requires admin privileges.
1338
+*/
1339
+void forum_page_close(void){
1340
+ const char *zFpid = PD("fpid","");
1341
+ const char *zReason = 0;
1342
+ int fClose;
1343
+ int fpid;
1344
+
1345
+ login_check_credentials();
1346
+ if( !g.perm.Admin ){
1347
+ login_needed(g.anon.Admin);
1348
+ return;
1349
+ }
1350
+ fpid = symbolic_name_to_rid(zFpid, "f");
1351
+ if( fpid<=0 ){
1352
+ webpage_error("Missing or invalid fpid query parameter");
1353
+ }
1354
+ fClose = sqlite3_strglob("*_close*", g.zPath)==0;
1355
+ if( fClose ) zReason = PD("reason",0);
1356
+ if( forumpost_close(fpid, fClose, zReason)!=0 ){
1357
+ admin_log("%s forum post %S", fClose ? "Close" : "Re-open", zFpid);
1358
+ }
1359
+ cgi_redirectf("%R/forumpost/%S",zFpid);
1360
+ return;
1361
+}
12731362
12741363
/*
12751364
** WEBPAGE: forumnew
12761365
** WEBPAGE: forumedit
12771366
**
@@ -1430,15 +1519,15 @@
14301519
froot = db_int(0, "SELECT froot FROM forumpost WHERE fpid=%d", fpid);
14311520
if( froot==0 || (pRootPost = manifest_get(froot, CFTYPE_FORUM, 0))==0 ){
14321521
webpage_error("fpid does not appear to be a forum post: \"%d\"", fpid);
14331522
}
14341523
if( P("cancel") ){
1435
- cgi_redirectf("%R/forumpost/%S",P("fpid"));
1524
+ cgi_redirectf("%R/forumpost/%S",zFpid);
14361525
return;
14371526
}
14381527
bPreview = P("preview")!=0;
1439
- iClosed = forum_rid_is_closed(fpid, froot!=fpid);
1528
+ iClosed = forum_rid_is_closed(fpid, 1);
14401529
isCsrfSafe = cgi_csrf_safe(1);
14411530
bPrivate = content_is_private(fpid);
14421531
bSameUser = login_is_individual()
14431532
&& fossil_strcmp(pPost->zUser, g.zLogin)==0;
14441533
if( isCsrfSafe && (g.perm.ModForum || (bPrivate && bSameUser)) ){
@@ -1498,10 +1587,11 @@
14981587
if( pPost->zThreadTitle ) zTitle = "";
14991588
style_header("Delete %s", zTitle ? "Post" : "Reply");
15001589
@ <h1>Original Post:</h1>
15011590
forum_render(pPost->zThreadTitle, pPost->zMimetype, pPost->zWiki,
15021591
"forumEdit", 1);
1592
+ forumpost_emit_closed_state(fpid, iClosed);
15031593
@ <h1>Change Into:</h1>
15041594
forum_render(zTitle, zMimetype, zContent,"forumEdit", 1);
15051595
@ <form action="%R/forume2" method="POST">
15061596
@ <input type="hidden" name="fpid" value="%h(P("fpid"))">
15071597
@ <input type="hidden" name="nullout" value="1">
@@ -1522,19 +1612,19 @@
15221612
}
15231613
style_header("Edit %s", zTitle ? "Post" : "Reply");
15241614
@ <h2>Original Post:</h2>
15251615
forum_render(pPost->zThreadTitle, pPost->zMimetype, pPost->zWiki,
15261616
"forumEdit", 1);
1617
+ forumpost_emit_closed_state(fpid, iClosed);
15271618
if( bPreview ){
15281619
@ <h2>Preview of Edited Post:</h2>
15291620
forum_render(zTitle, zMimetype, zContent,"forumEdit", 1);
15301621
}
15311622
@ <h2>Revised Message:</h2>
15321623
@ <form action="%R/forume2" method="POST">
15331624
@ <input type="hidden" name="fpid" value="%h(P("fpid"))">
15341625
@ <input type="hidden" name="edit" value="1">
1535
- if( iClosed ) forumpost_error_closed();
15361626
forum_from_line();
15371627
forum_post_widget(zTitle, zMimetype, zContent);
15381628
}else{
15391629
/* Reply */
15401630
char *zDisplayName;
@@ -1560,11 +1650,10 @@
15601650
}
15611651
@ <h2>Enter Reply:</h2>
15621652
@ <form action="%R/forume2" method="POST">
15631653
@ <input type="hidden" name="fpid" value="%h(P("fpid"))">
15641654
@ <input type="hidden" name="reply" value="1">
1565
- if( iClosed ) forumpost_error_closed();
15661655
forum_from_line();
15671656
forum_post_widget(0, zMimetype, zContent);
15681657
}
15691658
if( !isDelete ){
15701659
@ <input type="submit" name="preview" value="Preview">
@@ -1572,13 +1661,10 @@
15721661
@ <input type="submit" name="cancel" value="Cancel">
15731662
if( (bPreview && !whitespace_only(zContent)) || isDelete ){
15741663
if( !iClosed || g.perm.Admin ) {
15751664
@ <input type="submit" name="submit" value="Submit">
15761665
}
1577
- forumpost_emit_unlock_checkbox(iClosed, fpid);
1578
- }else if( !bPreview && iClosed ){
1579
- @ <span class='warning'>This post is CLOSED</span>
15801666
}
15811667
if( g.perm.Debug ){
15821668
/* For the test-forumnew page add these extra debugging controls */
15831669
@ <div class="debug">
15841670
@ <label><input type="checkbox" name="dryrun" %s(PCK("dryrun"))> \
15851671
--- src/forum.c
+++ src/forum.c
@@ -80,90 +80,112 @@
80 db_reset(&q);
81 return res;
82 }
83
84 /*
85 ** Returns true if p, or any parent of p, has an active "closed" tag.
86 ** Returns 0 if !p. For an edited chain of post, the tag is checked on
87 ** the final edit in the chain, as that permits that a post can be
88 ** locked and later unlocked. The return value is the tagxref.rowid
89 ** value of the tagxref entry which applies the "closed" tag, or 0 if
90 ** no active tag is found.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91 **
92 ** If bCheckParents is true then p's thread parents are checked
93 ** (recursively) for closure, else only p is checked.
94 */
95 static int forum_post_is_closed(ForumPost *p, int bCheckParents){
96 if( !p ) return 0;
97 if( p->pEditTail ) p = p->pEditTail;
98 if( p->iClosed || !bCheckParents ) return p->iClosed;
99 else if( p->pIrt ){
100 return forum_post_is_closed(p->pIrt->pEditTail
101 ? p->pIrt->pEditTail : p->pIrt,
102 bCheckParents);
103 }
104 return 0;
105 }
106
107 /*
108 ** Given a forum post RID, this function returns true if that post has
109 ** an active "closed" tag. If bCheckParents is true, the latest
110 ** version of each parent post is also checked (recursively), else
111 ** they are not. When checking parents, the first parent which is
112 ** closed ends the search.
 
 
 
 
 
 
 
113 **
114 ** The return value is one of:
115 **
116 ** - 0 if no "closed" tag is found.
117 **
118 ** - The tagxref.rowid of the tagxref entry for the closure if rid is
119 ** the artifact to which the closure applies.
120 **
121 ** - (-tagxref.rowid) if the given rid inherits a "closed" tag from an
122 ** ancestor forum post.
123 */
124 static int forum_rid_is_closed(int rid, int bCheckParents){
125 static Stmt qIrt = empty_Stmt_m;
126 int rc = 0;
127
128 /* TODO: this can probably be turned into a CTE, rather than a
129 ** recursive call into this function, by someone with superior
130 ** SQL-fu. */
131 rc = rid_has_active_tag_name(rid, "closed");
132 if( rc || !bCheckParents ) return rc;
133 else if( !qIrt.pStmt ) {
134 db_static_prepare(&qIrt,
135 "SELECT firt FROM forumpost "
136 "WHERE fpid=$fpid ORDER BY fmtime DESC"
137 );
138 }
139 db_bind_int(&qIrt, "$fpid", rid);
140 rid = SQLITE_ROW==db_step(&qIrt) ? db_column_int(&qIrt, 0) : 0;
141 db_reset(&qIrt);
142 if( rid ){
143 rc = forum_rid_is_closed(rid, 1);
144 }
145 return rc>0 ? -rc : rc;
146 }
147
148 /*
149 ** UNTESTED!
150 **
151 ** Closes or re-opens the given forum RID via addition of a new
152 ** control artifact into the repository.
 
 
 
 
153 **
154 ** If doClose is true then a propagating "closed" tag is added, except
155 ** as noted below, with the given optional zReason string as the tag's
156 ** value. If doClose is false then any active "closed" tag on frid is
157 ** cancelled, except as noted below. zReason is ignored if doClose is
158 ** false or if zReason is NULL or starts with a NUL byte.
159 **
160 ** This function only adds a "closed" tag to frid if
161 ** forum_rid_is_closed() indicates that frid is not closed. If a
162 ** parent post is already closed, no tag is added. Similarly, it will
163 ** only remove a "closed" tag from a post which has its own "closed"
164 ** tag, and will not remove an inherited one from a parent post.
165 **
166 ** If doClose is true and frid is closed (directly or inherited), this
167 ** is a no-op. Likewise, if doClose is false and frid itself is not
168 ** closed (not accounting for an inherited closed tag), this is a
169 ** no-op.
@@ -187,17 +209,19 @@
187 **
188 ** - Closure of a forum post requires a propagating "closed" tag to
189 ** account for how edits of posts are handled. This differs from
190 ** closure of a branch, where a non-propagating tag is used.
191 */
192 /*static*/ int forumpost_close(int frid, int doClose, const char *zReason){
193 Blob artifact = BLOB_INITIALIZER; /* Output artifact */
194 Blob cksum = BLOB_INITIALIZER; /* Z-card */
195 int iClosed; /* true if frid is closed */
196 int trid; /* RID of new control artifact */
 
197
198 db_begin_transaction();
 
199 iClosed = forum_rid_is_closed(frid, 1);
200 if( (iClosed && doClose
201 /* Already closed, noting that in the case of (iClosed<0), it's
202 ** actually a parent which is closed. */)
203 || (iClosed<=0 && !doClose
@@ -206,14 +230,15 @@
206 return 0;
207 }
208 if( doClose==0 || (zReason && !zReason[0]) ){
209 zReason = 0;
210 }
 
211 blob_appendf(&artifact, "D %z\n", date_in_standard_format( "now" ));
212 blob_appendf(&artifact,
213 "T %cclosed %z%s%F\n",
214 doClose ? '*' : '-', rid_to_uuid(frid),
215 zReason ? " " : "", zReason ? zReason : "");
216 blob_appendf(&artifact, "U %F\n", login_name());
217 md5sum_blob(&artifact, &cksum);
218 blob_appendf(&artifact, "Z %b\n", &cksum);
219 blob_reset(&cksum);
@@ -225,10 +250,12 @@
225 MC_NONE /*MC_PERMIT_HOOKS?*/)==0 ){
226 fossil_fatal("%s", g.zErrMsg);
227 }
228 assert( blob_is_reset(&artifact) );
229 db_add_unsent(trid);
 
 
230 /* Potential TODO: if (iClosed>0) then we could find the initial tag
231 ** artifact and content_deltify(thatRid,&trid,1,0). Given the tiny
232 ** size of these artifacts, however, that would save little space,
233 ** if any. */
234 db_end_transaction(0);
@@ -240,21 +267,45 @@
240 ** renders either a checkbox to unlock forum post fpid (if iClosed>0)
241 ** or a SPAN.warning element that the given post inherits the CLOSED
242 ** status from a parent post (if iClosed<0). If neither of the initial
243 ** conditions is true, this is a no-op.
244 */
245 static void forumpost_emit_unlock_checkbox(int iClosed, int fpid){
246 if( iClosed && g.perm.Admin ){
247 if( iClosed>0 ){
248 /* Only show the "unlock" checkbox on a post which is actually
249 ** closed, not on a post which inherits that state. */
250 @ <label class='warning'><input type="checkbox" name="reopen" value="1">
251 @ Re-open this CLOSED post? (NOT YET IMPLEMENTED)</label>
252 }else{
253 @ <span class='warning'>This post is CLOSED via a parent post</span>
254 }
255 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
256 }
257
258 /*
259 ** Emits a warning that the current forum post is CLOSED and can only
260 ** be edited or responded to by an administrator. */
@@ -398,11 +449,13 @@
398 for(; p; p=p->pEditPrev ){
399 p->nEdit = pPost->nEdit;
400 p->pEditTail = pPost;
401 }
402 }
403 pPost->iClosed = forum_rid_is_closed(pPost->fpid, 1);
 
 
404 }
405 db_finalize(&q);
406
407 if( computeHierarchy ){
408 /* Compute the hierarchical display order */
@@ -654,11 +707,11 @@
654 const char *zMimetype;/* Formatting MIME type */
655
656 /* Get the manifest for the post. Abort if not found (e.g. shunned). */
657 pManifest = manifest_get(p->fpid, CFTYPE_FORUM, 0);
658 if( !pManifest ) return;
659 iClosed = forum_post_is_closed(p, 1);
660 /* When not in raw mode, create the border around the post. */
661 if( !bRaw ){
662 /* Open the <div> enclosing the post. Set the class string to mark the post
663 ** as selected and/or obsolete. */
664 iIndent = (p->pEditHead ? p->pEditHead->nIndent : p->nIndent)-1;
@@ -1268,10 +1321,46 @@
1268 @ %z(href("%R/markup_help"))Markup style</a>:
1269 mimetype_option_menu(zMimetype, "mimetype");
1270 @ <br><textarea aria-label="Content:" name="content" class="wikiedit" \
1271 @ cols="80" rows="25" wrap="virtual">%h(zContent)</textarea><br>
1272 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1273
1274 /*
1275 ** WEBPAGE: forumnew
1276 ** WEBPAGE: forumedit
1277 **
@@ -1430,15 +1519,15 @@
1430 froot = db_int(0, "SELECT froot FROM forumpost WHERE fpid=%d", fpid);
1431 if( froot==0 || (pRootPost = manifest_get(froot, CFTYPE_FORUM, 0))==0 ){
1432 webpage_error("fpid does not appear to be a forum post: \"%d\"", fpid);
1433 }
1434 if( P("cancel") ){
1435 cgi_redirectf("%R/forumpost/%S",P("fpid"));
1436 return;
1437 }
1438 bPreview = P("preview")!=0;
1439 iClosed = forum_rid_is_closed(fpid, froot!=fpid);
1440 isCsrfSafe = cgi_csrf_safe(1);
1441 bPrivate = content_is_private(fpid);
1442 bSameUser = login_is_individual()
1443 && fossil_strcmp(pPost->zUser, g.zLogin)==0;
1444 if( isCsrfSafe && (g.perm.ModForum || (bPrivate && bSameUser)) ){
@@ -1498,10 +1587,11 @@
1498 if( pPost->zThreadTitle ) zTitle = "";
1499 style_header("Delete %s", zTitle ? "Post" : "Reply");
1500 @ <h1>Original Post:</h1>
1501 forum_render(pPost->zThreadTitle, pPost->zMimetype, pPost->zWiki,
1502 "forumEdit", 1);
 
1503 @ <h1>Change Into:</h1>
1504 forum_render(zTitle, zMimetype, zContent,"forumEdit", 1);
1505 @ <form action="%R/forume2" method="POST">
1506 @ <input type="hidden" name="fpid" value="%h(P("fpid"))">
1507 @ <input type="hidden" name="nullout" value="1">
@@ -1522,19 +1612,19 @@
1522 }
1523 style_header("Edit %s", zTitle ? "Post" : "Reply");
1524 @ <h2>Original Post:</h2>
1525 forum_render(pPost->zThreadTitle, pPost->zMimetype, pPost->zWiki,
1526 "forumEdit", 1);
 
1527 if( bPreview ){
1528 @ <h2>Preview of Edited Post:</h2>
1529 forum_render(zTitle, zMimetype, zContent,"forumEdit", 1);
1530 }
1531 @ <h2>Revised Message:</h2>
1532 @ <form action="%R/forume2" method="POST">
1533 @ <input type="hidden" name="fpid" value="%h(P("fpid"))">
1534 @ <input type="hidden" name="edit" value="1">
1535 if( iClosed ) forumpost_error_closed();
1536 forum_from_line();
1537 forum_post_widget(zTitle, zMimetype, zContent);
1538 }else{
1539 /* Reply */
1540 char *zDisplayName;
@@ -1560,11 +1650,10 @@
1560 }
1561 @ <h2>Enter Reply:</h2>
1562 @ <form action="%R/forume2" method="POST">
1563 @ <input type="hidden" name="fpid" value="%h(P("fpid"))">
1564 @ <input type="hidden" name="reply" value="1">
1565 if( iClosed ) forumpost_error_closed();
1566 forum_from_line();
1567 forum_post_widget(0, zMimetype, zContent);
1568 }
1569 if( !isDelete ){
1570 @ <input type="submit" name="preview" value="Preview">
@@ -1572,13 +1661,10 @@
1572 @ <input type="submit" name="cancel" value="Cancel">
1573 if( (bPreview && !whitespace_only(zContent)) || isDelete ){
1574 if( !iClosed || g.perm.Admin ) {
1575 @ <input type="submit" name="submit" value="Submit">
1576 }
1577 forumpost_emit_unlock_checkbox(iClosed, fpid);
1578 }else if( !bPreview && iClosed ){
1579 @ <span class='warning'>This post is CLOSED</span>
1580 }
1581 if( g.perm.Debug ){
1582 /* For the test-forumnew page add these extra debugging controls */
1583 @ <div class="debug">
1584 @ <label><input type="checkbox" name="dryrun" %s(PCK("dryrun"))> \
1585
--- src/forum.c
+++ src/forum.c
@@ -80,90 +80,112 @@
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.
@@ -187,17 +209,19 @@
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
@@ -206,14 +230,15 @@
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);
@@ -225,10 +250,12 @@
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);
@@ -240,21 +267,45 @@
267 ** renders either a checkbox to unlock forum post fpid (if iClosed>0)
268 ** or a SPAN.warning element that the given post inherits the CLOSED
269 ** status from a parent post (if iClosed<0). If neither of the initial
270 ** conditions is true, this is a no-op.
271 */
272 static void forumpost_emit_closed_state(int fpid, int iClosed){
273 const char *zCommon =
274 "Only admins may edit or respond to closed posts.";
275 int iHead = forumpost_head_rid(fpid);
276 /*@ forumpost_emit_closed_state(%d(fpid), %d(iClosed))<br/>*/
277 if( iHead != fpid ){
278 iClosed = forum_rid_is_closed(iHead, 1);
279 /*@ forumpost_emit_closed_state() %d(iHead), %d(iClosed)*/
280 }
281 if( iClosed<0 ){
282 @ <div class="warning forumpost-closed-warning">\
283 @ This post is CLOSED via a parent post. %s(zCommon)\
284 @ </div>
285 return;
286 }
287 else if( iClosed==0 ){
288 if( g.perm.Admin==0 ) return;
289 @ <div class="warning forumpost-closed-warning">
290 @ <form method="post" action="%R/forumpost_close">
291 @ <input type="hidden" name="fpid" value="%z(rid_to_uuid(iHead))" />
292 @ <input type="submit" value="CLOSE this post and its responses" />
293 @ %s(zCommon)
294 @ </form></div>
295 return;
296 }
297 assert( iClosed>0 );
298 /* Only show the "unlock" checkbox on a post which is actually
299 ** closed, not on a post which inherits that state. */
300 @ <div class="warning forumpost-closed-warning">\
301 @ This post is CLOSED. %s(zCommon)
302 @ <form method="post" action="%R/forumpost_reopen">
303 @ <input type="hidden" name="fpid" value="%z(rid_to_uuid(iHead))" />
304 @ <input type="submit" value="Re-open this post and its responses" />
305 @ </form>
306 @ </div>
307 }
308
309 /*
310 ** Emits a warning that the current forum post is CLOSED and can only
311 ** be edited or responded to by an administrator. */
@@ -398,11 +449,13 @@
449 for(; p; p=p->pEditPrev ){
450 p->nEdit = pPost->nEdit;
451 p->pEditTail = pPost;
452 }
453 }
454 pPost->iClosed = forum_rid_is_closed(pPost->pEditHead
455 ? pPost->pEditHead->fpid
456 : pPost->fpid, 1);
457 }
458 db_finalize(&q);
459
460 if( computeHierarchy ){
461 /* Compute the hierarchical display order */
@@ -654,11 +707,11 @@
707 const char *zMimetype;/* Formatting MIME type */
708
709 /* Get the manifest for the post. Abort if not found (e.g. shunned). */
710 pManifest = manifest_get(p->fpid, CFTYPE_FORUM, 0);
711 if( !pManifest ) return;
712 iClosed = forumpost_is_closed(p, 1);
713 /* When not in raw mode, create the border around the post. */
714 if( !bRaw ){
715 /* Open the <div> enclosing the post. Set the class string to mark the post
716 ** as selected and/or obsolete. */
717 iIndent = (p->pEditHead ? p->pEditHead->nIndent : p->nIndent)-1;
@@ -1268,10 +1321,46 @@
1321 @ %z(href("%R/markup_help"))Markup style</a>:
1322 mimetype_option_menu(zMimetype, "mimetype");
1323 @ <br><textarea aria-label="Content:" name="content" class="wikiedit" \
1324 @ cols="80" rows="25" wrap="virtual">%h(zContent)</textarea><br>
1325 }
1326
1327 /*
1328 ** WEBPAGE: forumpost_close hidden
1329 ** WEBPAGE: forumpost_reopen hidden
1330 **
1331 ** fpid=X Hash of the post to be edited. REQUIRED
1332 ** reason=X Optional reason for closure.
1333 **
1334 ** Closes or re-opens the given forum post, within the bounds of the
1335 ** API for forumpost_close(). After (perhaps) modifying the "closed"
1336 ** status of the given thread, it redirects to that post's thread
1337 ** view. Requires admin privileges.
1338 */
1339 void forum_page_close(void){
1340 const char *zFpid = PD("fpid","");
1341 const char *zReason = 0;
1342 int fClose;
1343 int fpid;
1344
1345 login_check_credentials();
1346 if( !g.perm.Admin ){
1347 login_needed(g.anon.Admin);
1348 return;
1349 }
1350 fpid = symbolic_name_to_rid(zFpid, "f");
1351 if( fpid<=0 ){
1352 webpage_error("Missing or invalid fpid query parameter");
1353 }
1354 fClose = sqlite3_strglob("*_close*", g.zPath)==0;
1355 if( fClose ) zReason = PD("reason",0);
1356 if( forumpost_close(fpid, fClose, zReason)!=0 ){
1357 admin_log("%s forum post %S", fClose ? "Close" : "Re-open", zFpid);
1358 }
1359 cgi_redirectf("%R/forumpost/%S",zFpid);
1360 return;
1361 }
1362
1363 /*
1364 ** WEBPAGE: forumnew
1365 ** WEBPAGE: forumedit
1366 **
@@ -1430,15 +1519,15 @@
1519 froot = db_int(0, "SELECT froot FROM forumpost WHERE fpid=%d", fpid);
1520 if( froot==0 || (pRootPost = manifest_get(froot, CFTYPE_FORUM, 0))==0 ){
1521 webpage_error("fpid does not appear to be a forum post: \"%d\"", fpid);
1522 }
1523 if( P("cancel") ){
1524 cgi_redirectf("%R/forumpost/%S",zFpid);
1525 return;
1526 }
1527 bPreview = P("preview")!=0;
1528 iClosed = forum_rid_is_closed(fpid, 1);
1529 isCsrfSafe = cgi_csrf_safe(1);
1530 bPrivate = content_is_private(fpid);
1531 bSameUser = login_is_individual()
1532 && fossil_strcmp(pPost->zUser, g.zLogin)==0;
1533 if( isCsrfSafe && (g.perm.ModForum || (bPrivate && bSameUser)) ){
@@ -1498,10 +1587,11 @@
1587 if( pPost->zThreadTitle ) zTitle = "";
1588 style_header("Delete %s", zTitle ? "Post" : "Reply");
1589 @ <h1>Original Post:</h1>
1590 forum_render(pPost->zThreadTitle, pPost->zMimetype, pPost->zWiki,
1591 "forumEdit", 1);
1592 forumpost_emit_closed_state(fpid, iClosed);
1593 @ <h1>Change Into:</h1>
1594 forum_render(zTitle, zMimetype, zContent,"forumEdit", 1);
1595 @ <form action="%R/forume2" method="POST">
1596 @ <input type="hidden" name="fpid" value="%h(P("fpid"))">
1597 @ <input type="hidden" name="nullout" value="1">
@@ -1522,19 +1612,19 @@
1612 }
1613 style_header("Edit %s", zTitle ? "Post" : "Reply");
1614 @ <h2>Original Post:</h2>
1615 forum_render(pPost->zThreadTitle, pPost->zMimetype, pPost->zWiki,
1616 "forumEdit", 1);
1617 forumpost_emit_closed_state(fpid, iClosed);
1618 if( bPreview ){
1619 @ <h2>Preview of Edited Post:</h2>
1620 forum_render(zTitle, zMimetype, zContent,"forumEdit", 1);
1621 }
1622 @ <h2>Revised Message:</h2>
1623 @ <form action="%R/forume2" method="POST">
1624 @ <input type="hidden" name="fpid" value="%h(P("fpid"))">
1625 @ <input type="hidden" name="edit" value="1">
 
1626 forum_from_line();
1627 forum_post_widget(zTitle, zMimetype, zContent);
1628 }else{
1629 /* Reply */
1630 char *zDisplayName;
@@ -1560,11 +1650,10 @@
1650 }
1651 @ <h2>Enter Reply:</h2>
1652 @ <form action="%R/forume2" method="POST">
1653 @ <input type="hidden" name="fpid" value="%h(P("fpid"))">
1654 @ <input type="hidden" name="reply" value="1">
 
1655 forum_from_line();
1656 forum_post_widget(0, zMimetype, zContent);
1657 }
1658 if( !isDelete ){
1659 @ <input type="submit" name="preview" value="Preview">
@@ -1572,13 +1661,10 @@
1661 @ <input type="submit" name="cancel" value="Cancel">
1662 if( (bPreview && !whitespace_only(zContent)) || isDelete ){
1663 if( !iClosed || g.perm.Admin ) {
1664 @ <input type="submit" name="submit" value="Submit">
1665 }
 
 
 
1666 }
1667 if( g.perm.Debug ){
1668 /* For the test-forumnew page add these extra debugging controls */
1669 @ <div class="debug">
1670 @ <label><input type="checkbox" name="dryrun" %s(PCK("dryrun"))> \
1671

Keyboard Shortcuts

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