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).
Commit
cc6ca4e110a7cfcc66ad9034389162ef0790bb7c6c8f5a1c36fb7ba5d77bccf6
Parent
32fc62e68160783…
2 files changed
+13
-1
+160
-74
+13
-1
| --- src/default.css | ||
| +++ src/default.css | ||
| @@ -904,16 +904,28 @@ | ||
| 904 | 904 | div.forumClosed { |
| 905 | 905 | opacity: 0.7; |
| 906 | 906 | } |
| 907 | 907 | div.forumClosed > *:first-child::before { |
| 908 | 908 | content: "[CLOSED] "; |
| 909 | - color: red; | |
| 909 | + color: darkred; | |
| 910 | 910 | opacity: 0.7; |
| 911 | 911 | } |
| 912 | 912 | /*div.forumClosed > div.forumPostBody { |
| 913 | 913 | filter: blur(5px); |
| 914 | 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 | +} | |
| 915 | 927 | .forum div > form { |
| 916 | 928 | margin: 0.5em 0; |
| 917 | 929 | } |
| 918 | 930 | .forum-post-collapser { |
| 919 | 931 | /* Common style for the bottom-of-post and right-of-post |
| 920 | 932 |
| --- 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 @@ | ||
| 80 | 80 | db_reset(&q); |
| 81 | 81 | return res; |
| 82 | 82 | } |
| 83 | 83 | |
| 84 | 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. | |
| 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. | |
| 91 | 110 | ** |
| 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. | |
| 94 | 113 | */ |
| 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; | |
| 103 | 119 | } |
| 104 | 120 | return 0; |
| 105 | 121 | } |
| 106 | 122 | |
| 107 | 123 | /* |
| 108 | 124 | ** 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. | |
| 113 | 136 | ** |
| 114 | 137 | ** The return value is one of: |
| 115 | 138 | ** |
| 116 | 139 | ** - 0 if no "closed" tag is found. |
| 117 | 140 | ** |
| 118 | 141 | ** - 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. | |
| 120 | 143 | ** |
| 121 | 144 | ** - (-tagxref.rowid) if the given rid inherits a "closed" tag from an |
| 122 | -** ancestor forum post. | |
| 145 | +** IRT forum post. | |
| 123 | 146 | */ |
| 124 | -static int forum_rid_is_closed(int rid, int bCheckParents){ | |
| 147 | +static int forum_rid_is_closed(int rid, int bCheckIrt){ | |
| 125 | 148 | 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; | |
| 146 | 166 | } |
| 147 | 167 | |
| 148 | 168 | /* |
| 149 | -** UNTESTED! | |
| 150 | -** | |
| 151 | 169 | ** 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. | |
| 153 | 175 | ** |
| 154 | 176 | ** If doClose is true then a propagating "closed" tag is added, except |
| 155 | 177 | ** as noted below, with the given optional zReason string as the tag's |
| 156 | 178 | ** value. If doClose is false then any active "closed" tag on frid is |
| 157 | 179 | ** cancelled, except as noted below. zReason is ignored if doClose is |
| 158 | 180 | ** false or if zReason is NULL or starts with a NUL byte. |
| 159 | 181 | ** |
| 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. | |
| 165 | 187 | ** |
| 166 | 188 | ** If doClose is true and frid is closed (directly or inherited), this |
| 167 | 189 | ** is a no-op. Likewise, if doClose is false and frid itself is not |
| 168 | 190 | ** closed (not accounting for an inherited closed tag), this is a |
| 169 | 191 | ** no-op. |
| @@ -187,17 +209,19 @@ | ||
| 187 | 209 | ** |
| 188 | 210 | ** - Closure of a forum post requires a propagating "closed" tag to |
| 189 | 211 | ** account for how edits of posts are handled. This differs from |
| 190 | 212 | ** closure of a branch, where a non-propagating tag is used. |
| 191 | 213 | */ |
| 192 | -/*static*/ int forumpost_close(int frid, int doClose, const char *zReason){ | |
| 214 | +static int forumpost_close(int frid, int doClose, const char *zReason){ | |
| 193 | 215 | Blob artifact = BLOB_INITIALIZER; /* Output artifact */ |
| 194 | 216 | Blob cksum = BLOB_INITIALIZER; /* Z-card */ |
| 195 | 217 | int iClosed; /* true if frid is closed */ |
| 196 | 218 | int trid; /* RID of new control artifact */ |
| 219 | + char *zUuid; /* UUID of head version of post */ | |
| 197 | 220 | |
| 198 | 221 | db_begin_transaction(); |
| 222 | + frid = forumpost_head_rid(frid); | |
| 199 | 223 | iClosed = forum_rid_is_closed(frid, 1); |
| 200 | 224 | if( (iClosed && doClose |
| 201 | 225 | /* Already closed, noting that in the case of (iClosed<0), it's |
| 202 | 226 | ** actually a parent which is closed. */) |
| 203 | 227 | || (iClosed<=0 && !doClose |
| @@ -206,14 +230,15 @@ | ||
| 206 | 230 | return 0; |
| 207 | 231 | } |
| 208 | 232 | if( doClose==0 || (zReason && !zReason[0]) ){ |
| 209 | 233 | zReason = 0; |
| 210 | 234 | } |
| 235 | + zUuid = rid_to_uuid(frid); | |
| 211 | 236 | blob_appendf(&artifact, "D %z\n", date_in_standard_format( "now" )); |
| 212 | 237 | 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, | |
| 215 | 240 | zReason ? " " : "", zReason ? zReason : ""); |
| 216 | 241 | blob_appendf(&artifact, "U %F\n", login_name()); |
| 217 | 242 | md5sum_blob(&artifact, &cksum); |
| 218 | 243 | blob_appendf(&artifact, "Z %b\n", &cksum); |
| 219 | 244 | blob_reset(&cksum); |
| @@ -225,10 +250,12 @@ | ||
| 225 | 250 | MC_NONE /*MC_PERMIT_HOOKS?*/)==0 ){ |
| 226 | 251 | fossil_fatal("%s", g.zErrMsg); |
| 227 | 252 | } |
| 228 | 253 | assert( blob_is_reset(&artifact) ); |
| 229 | 254 | db_add_unsent(trid); |
| 255 | + admin_log("%s forum post %S", doClose ? "Close" : "Re-open", zUuid); | |
| 256 | + fossil_free(zUuid); | |
| 230 | 257 | /* Potential TODO: if (iClosed>0) then we could find the initial tag |
| 231 | 258 | ** artifact and content_deltify(thatRid,&trid,1,0). Given the tiny |
| 232 | 259 | ** size of these artifacts, however, that would save little space, |
| 233 | 260 | ** if any. */ |
| 234 | 261 | db_end_transaction(0); |
| @@ -240,21 +267,45 @@ | ||
| 240 | 267 | ** renders either a checkbox to unlock forum post fpid (if iClosed>0) |
| 241 | 268 | ** or a SPAN.warning element that the given post inherits the CLOSED |
| 242 | 269 | ** status from a parent post (if iClosed<0). If neither of the initial |
| 243 | 270 | ** conditions is true, this is a no-op. |
| 244 | 271 | */ |
| 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> | |
| 256 | 307 | } |
| 257 | 308 | |
| 258 | 309 | /* |
| 259 | 310 | ** Emits a warning that the current forum post is CLOSED and can only |
| 260 | 311 | ** be edited or responded to by an administrator. */ |
| @@ -398,11 +449,13 @@ | ||
| 398 | 449 | for(; p; p=p->pEditPrev ){ |
| 399 | 450 | p->nEdit = pPost->nEdit; |
| 400 | 451 | p->pEditTail = pPost; |
| 401 | 452 | } |
| 402 | 453 | } |
| 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); | |
| 404 | 457 | } |
| 405 | 458 | db_finalize(&q); |
| 406 | 459 | |
| 407 | 460 | if( computeHierarchy ){ |
| 408 | 461 | /* Compute the hierarchical display order */ |
| @@ -654,11 +707,11 @@ | ||
| 654 | 707 | const char *zMimetype;/* Formatting MIME type */ |
| 655 | 708 | |
| 656 | 709 | /* Get the manifest for the post. Abort if not found (e.g. shunned). */ |
| 657 | 710 | pManifest = manifest_get(p->fpid, CFTYPE_FORUM, 0); |
| 658 | 711 | if( !pManifest ) return; |
| 659 | - iClosed = forum_post_is_closed(p, 1); | |
| 712 | + iClosed = forumpost_is_closed(p, 1); | |
| 660 | 713 | /* When not in raw mode, create the border around the post. */ |
| 661 | 714 | if( !bRaw ){ |
| 662 | 715 | /* Open the <div> enclosing the post. Set the class string to mark the post |
| 663 | 716 | ** as selected and/or obsolete. */ |
| 664 | 717 | iIndent = (p->pEditHead ? p->pEditHead->nIndent : p->nIndent)-1; |
| @@ -1268,10 +1321,46 @@ | ||
| 1268 | 1321 | @ %z(href("%R/markup_help"))Markup style</a>: |
| 1269 | 1322 | mimetype_option_menu(zMimetype, "mimetype"); |
| 1270 | 1323 | @ <br><textarea aria-label="Content:" name="content" class="wikiedit" \ |
| 1271 | 1324 | @ cols="80" rows="25" wrap="virtual">%h(zContent)</textarea><br> |
| 1272 | 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 | +} | |
| 1273 | 1362 | |
| 1274 | 1363 | /* |
| 1275 | 1364 | ** WEBPAGE: forumnew |
| 1276 | 1365 | ** WEBPAGE: forumedit |
| 1277 | 1366 | ** |
| @@ -1430,15 +1519,15 @@ | ||
| 1430 | 1519 | froot = db_int(0, "SELECT froot FROM forumpost WHERE fpid=%d", fpid); |
| 1431 | 1520 | if( froot==0 || (pRootPost = manifest_get(froot, CFTYPE_FORUM, 0))==0 ){ |
| 1432 | 1521 | webpage_error("fpid does not appear to be a forum post: \"%d\"", fpid); |
| 1433 | 1522 | } |
| 1434 | 1523 | if( P("cancel") ){ |
| 1435 | - cgi_redirectf("%R/forumpost/%S",P("fpid")); | |
| 1524 | + cgi_redirectf("%R/forumpost/%S",zFpid); | |
| 1436 | 1525 | return; |
| 1437 | 1526 | } |
| 1438 | 1527 | bPreview = P("preview")!=0; |
| 1439 | - iClosed = forum_rid_is_closed(fpid, froot!=fpid); | |
| 1528 | + iClosed = forum_rid_is_closed(fpid, 1); | |
| 1440 | 1529 | isCsrfSafe = cgi_csrf_safe(1); |
| 1441 | 1530 | bPrivate = content_is_private(fpid); |
| 1442 | 1531 | bSameUser = login_is_individual() |
| 1443 | 1532 | && fossil_strcmp(pPost->zUser, g.zLogin)==0; |
| 1444 | 1533 | if( isCsrfSafe && (g.perm.ModForum || (bPrivate && bSameUser)) ){ |
| @@ -1498,10 +1587,11 @@ | ||
| 1498 | 1587 | if( pPost->zThreadTitle ) zTitle = ""; |
| 1499 | 1588 | style_header("Delete %s", zTitle ? "Post" : "Reply"); |
| 1500 | 1589 | @ <h1>Original Post:</h1> |
| 1501 | 1590 | forum_render(pPost->zThreadTitle, pPost->zMimetype, pPost->zWiki, |
| 1502 | 1591 | "forumEdit", 1); |
| 1592 | + forumpost_emit_closed_state(fpid, iClosed); | |
| 1503 | 1593 | @ <h1>Change Into:</h1> |
| 1504 | 1594 | forum_render(zTitle, zMimetype, zContent,"forumEdit", 1); |
| 1505 | 1595 | @ <form action="%R/forume2" method="POST"> |
| 1506 | 1596 | @ <input type="hidden" name="fpid" value="%h(P("fpid"))"> |
| 1507 | 1597 | @ <input type="hidden" name="nullout" value="1"> |
| @@ -1522,19 +1612,19 @@ | ||
| 1522 | 1612 | } |
| 1523 | 1613 | style_header("Edit %s", zTitle ? "Post" : "Reply"); |
| 1524 | 1614 | @ <h2>Original Post:</h2> |
| 1525 | 1615 | forum_render(pPost->zThreadTitle, pPost->zMimetype, pPost->zWiki, |
| 1526 | 1616 | "forumEdit", 1); |
| 1617 | + forumpost_emit_closed_state(fpid, iClosed); | |
| 1527 | 1618 | if( bPreview ){ |
| 1528 | 1619 | @ <h2>Preview of Edited Post:</h2> |
| 1529 | 1620 | forum_render(zTitle, zMimetype, zContent,"forumEdit", 1); |
| 1530 | 1621 | } |
| 1531 | 1622 | @ <h2>Revised Message:</h2> |
| 1532 | 1623 | @ <form action="%R/forume2" method="POST"> |
| 1533 | 1624 | @ <input type="hidden" name="fpid" value="%h(P("fpid"))"> |
| 1534 | 1625 | @ <input type="hidden" name="edit" value="1"> |
| 1535 | - if( iClosed ) forumpost_error_closed(); | |
| 1536 | 1626 | forum_from_line(); |
| 1537 | 1627 | forum_post_widget(zTitle, zMimetype, zContent); |
| 1538 | 1628 | }else{ |
| 1539 | 1629 | /* Reply */ |
| 1540 | 1630 | char *zDisplayName; |
| @@ -1560,11 +1650,10 @@ | ||
| 1560 | 1650 | } |
| 1561 | 1651 | @ <h2>Enter Reply:</h2> |
| 1562 | 1652 | @ <form action="%R/forume2" method="POST"> |
| 1563 | 1653 | @ <input type="hidden" name="fpid" value="%h(P("fpid"))"> |
| 1564 | 1654 | @ <input type="hidden" name="reply" value="1"> |
| 1565 | - if( iClosed ) forumpost_error_closed(); | |
| 1566 | 1655 | forum_from_line(); |
| 1567 | 1656 | forum_post_widget(0, zMimetype, zContent); |
| 1568 | 1657 | } |
| 1569 | 1658 | if( !isDelete ){ |
| 1570 | 1659 | @ <input type="submit" name="preview" value="Preview"> |
| @@ -1572,13 +1661,10 @@ | ||
| 1572 | 1661 | @ <input type="submit" name="cancel" value="Cancel"> |
| 1573 | 1662 | if( (bPreview && !whitespace_only(zContent)) || isDelete ){ |
| 1574 | 1663 | if( !iClosed || g.perm.Admin ) { |
| 1575 | 1664 | @ <input type="submit" name="submit" value="Submit"> |
| 1576 | 1665 | } |
| 1577 | - forumpost_emit_unlock_checkbox(iClosed, fpid); | |
| 1578 | - }else if( !bPreview && iClosed ){ | |
| 1579 | - @ <span class='warning'>This post is CLOSED</span> | |
| 1580 | 1666 | } |
| 1581 | 1667 | if( g.perm.Debug ){ |
| 1582 | 1668 | /* For the test-forumnew page add these extra debugging controls */ |
| 1583 | 1669 | @ <div class="debug"> |
| 1584 | 1670 | @ <label><input type="checkbox" name="dryrun" %s(PCK("dryrun"))> \ |
| 1585 | 1671 |
| --- 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 |