Fossil SCM
Closed forum threads can no longer be edited by non-admins. Fix broken ability of non-builtin users to delete their own pending-moderation post. UI controls for closing/reing-open threads are still TODO.
Commit
8f02c1d4a8f984b70cbc473da7305b5ca162082b58c560408a1020d18d4e2a14
Parent
464f4d175f50380…
2 files changed
+90
-22
+6
-8
+90
-22
| --- src/forum.c | ||
| +++ src/forum.c | ||
| @@ -47,11 +47,11 @@ | ||
| 47 | 47 | ForumPost *pNext; /* Next in chronological order */ |
| 48 | 48 | ForumPost *pPrev; /* Previous in chronological order */ |
| 49 | 49 | ForumPost *pDisplay; /* Next in display order */ |
| 50 | 50 | int nEdit; /* Number of edits to this post */ |
| 51 | 51 | int nIndent; /* Number of levels of indentation for this post */ |
| 52 | - int fClosed; /* tagxref.tagtype if this (sub)thread has a closed tag. */ | |
| 52 | + int fClosed; /* See forum_rid_is_closed() */ | |
| 53 | 53 | }; |
| 54 | 54 | |
| 55 | 55 | /* |
| 56 | 56 | ** A single instance of the following tracks all entries for a thread. |
| 57 | 57 | */ |
| @@ -83,47 +83,96 @@ | ||
| 83 | 83 | |
| 84 | 84 | /* |
| 85 | 85 | ** Returns true if p, or any parent of p, has an active "closed" tag. |
| 86 | 86 | ** Returns 0 if !p. For an edited chain of post, the tag is checked on |
| 87 | 87 | ** the final edit in the chain, as that permits that a post can be |
| 88 | -** locked and later unlocked. | |
| 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. | |
| 89 | 94 | */ |
| 90 | -int forum_post_is_closed(ForumPost *p){ | |
| 95 | +int forum_post_is_closed(ForumPost *p, int bCheckParents){ | |
| 91 | 96 | if( !p ) return 0; |
| 92 | 97 | if( p->pEditTail ) p = p->pEditTail; |
| 93 | - if( p->fClosed ) return p->fClosed; | |
| 98 | + if( p->fClosed || !bCheckParents ) return p->fClosed; | |
| 94 | 99 | else if( p->pIrt ){ |
| 95 | 100 | return forum_post_is_closed(p->pIrt->pEditTail |
| 96 | - ? p->pIrt->pEditTail : p->pIrt); | |
| 101 | + ? p->pIrt->pEditTail : p->pIrt, | |
| 102 | + bCheckParents); | |
| 97 | 103 | } |
| 98 | 104 | return 0; |
| 99 | 105 | } |
| 100 | 106 | |
| 101 | 107 | /* |
| 102 | -** Given a forum post RID, this function returns true if that post or | |
| 103 | -** the latest version of any parent post in its hierarchy have an | |
| 104 | -** active "closed" tag. | |
| 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. | |
| 105 | 123 | */ |
| 106 | -int forum_rid_is_closed(int rid){ | |
| 124 | +static int forum_rid_is_closed(int rid, int bCheckParents){ | |
| 107 | 125 | static Stmt qIrt = empty_Stmt_m; |
| 108 | - int rc; | |
| 126 | + int rc = 0; | |
| 109 | 127 | |
| 110 | 128 | /* TODO: this can probably be turned into a CTE, rather than a |
| 111 | 129 | ** recursive call into this function, by someone with superior |
| 112 | 130 | ** SQL-fu. */ |
| 113 | 131 | rc = rid_has_active_tag_name(rid, "closed"); |
| 114 | - if( rc ) return rc; | |
| 132 | + if( rc || !bCheckParents ) return rc; | |
| 115 | 133 | else if( !qIrt.pStmt ) { |
| 116 | 134 | db_static_prepare(&qIrt, |
| 117 | 135 | "SELECT firt FROM forumpost " |
| 118 | 136 | "WHERE fpid=$fpid ORDER BY fmtime DESC" |
| 119 | 137 | ); |
| 120 | 138 | } |
| 121 | 139 | db_bind_int(&qIrt, "$fpid", rid); |
| 122 | - rc = SQLITE_ROW==db_step(&qIrt) ? db_column_int(&qIrt, 0) : 0; | |
| 140 | + rid = SQLITE_ROW==db_step(&qIrt) ? db_column_int(&qIrt, 0) : 0; | |
| 123 | 141 | db_reset(&qIrt); |
| 124 | - return rc>0 ? forum_rid_is_closed(rc) : 0; | |
| 142 | + if( rid ){ | |
| 143 | + rc = forum_rid_is_closed(rid, 1); | |
| 144 | + } | |
| 145 | + return rc>0 ? -rc : rc; | |
| 146 | +} | |
| 147 | + | |
| 148 | +/* | |
| 149 | +** If fClosed is true and the current user has admin privileges, this | |
| 150 | +** renders either a checkbox to unlock forum post fpid (if fClosed>0) | |
| 151 | +** or a SPAN.warning element that the given post inherits the CLOSED | |
| 152 | +** status from a parent post (if fClosed<0). If neither of the initial | |
| 153 | +** conditions is true, this is a no-op. | |
| 154 | +*/ | |
| 155 | +static void forumpost_emit_unlock_checkbox(int fClosed, int fpid){ | |
| 156 | + if( fClosed && g.perm.Admin ){ | |
| 157 | + if( fClosed>0 ){ | |
| 158 | + /* Only show the "unlock" checkbox on a post which is actually | |
| 159 | + ** closed, not on a post which inherits that state. */ | |
| 160 | + @ <label class='warning'><input type="checkbox" name="reopen" value="1"> | |
| 161 | + @ Re-open this CLOSED post? (NOT YET IMPLEMENTED)</label> | |
| 162 | + }else{ | |
| 163 | + @ <span class='warning'>This post is CLOSED via a parent post</span> | |
| 164 | + } | |
| 165 | + } | |
| 166 | +} | |
| 167 | + | |
| 168 | +/* | |
| 169 | +** Emits a warning that the current forum post is CLOSED and can only | |
| 170 | +** be edited or responded to by an administrator. */ | |
| 171 | +static void forumpost_error_closed(void){ | |
| 172 | + @ <div class='error'>This (sub)thread is CLOSED and can only be | |
| 173 | + @ edited or replied to by an admin user.</div> | |
| 125 | 174 | } |
| 126 | 175 | |
| 127 | 176 | /* |
| 128 | 177 | ** Delete a complete ForumThread and all its entries. |
| 129 | 178 | */ |
| @@ -259,11 +308,11 @@ | ||
| 259 | 308 | for(; p; p=p->pEditPrev ){ |
| 260 | 309 | p->nEdit = pPost->nEdit; |
| 261 | 310 | p->pEditTail = pPost; |
| 262 | 311 | } |
| 263 | 312 | } |
| 264 | - pPost->fClosed = rid_has_active_tag_name(pPost->fpid, "closed"); | |
| 313 | + pPost->fClosed = forum_rid_is_closed(pPost->fpid, 1); | |
| 265 | 314 | } |
| 266 | 315 | db_finalize(&q); |
| 267 | 316 | |
| 268 | 317 | if( computeHierarchy ){ |
| 269 | 318 | /* Compute the hierarchical display order */ |
| @@ -511,11 +560,11 @@ | ||
| 511 | 560 | const char *zMimetype;/* Formatting MIME type */ |
| 512 | 561 | |
| 513 | 562 | /* Get the manifest for the post. Abort if not found (e.g. shunned). */ |
| 514 | 563 | pManifest = manifest_get(p->fpid, CFTYPE_FORUM, 0); |
| 515 | 564 | if( !pManifest ) return; |
| 516 | - fClosed = forum_post_is_closed(p); | |
| 565 | + fClosed = forum_post_is_closed(p, 1); | |
| 517 | 566 | /* When not in raw mode, create the border around the post. */ |
| 518 | 567 | if( !bRaw ){ |
| 519 | 568 | /* Open the <div> enclosing the post. Set the class string to mark the post |
| 520 | 569 | ** as selected and/or obsolete. */ |
| 521 | 570 | iIndent = (p->pEditHead ? p->pEditHead->nIndent : p->nIndent)-1; |
| @@ -1024,10 +1073,15 @@ | ||
| 1024 | 1073 | Blob x, cksum, formatCheck, errMsg; |
| 1025 | 1074 | Manifest *pPost; |
| 1026 | 1075 | int nContent = zContent ? (int)strlen(zContent) : 0; |
| 1027 | 1076 | |
| 1028 | 1077 | schema_forum(); |
| 1078 | + if( !g.perm.Admin && (iEdit || iInReplyTo) | |
| 1079 | + && forum_rid_is_closed(iEdit ? iEdit : iInReplyTo, 1) ){ | |
| 1080 | + forumpost_error_closed(); | |
| 1081 | + return 0; | |
| 1082 | + } | |
| 1029 | 1083 | if( iEdit==0 && whitespace_only(zContent) ){ |
| 1030 | 1084 | return 0; |
| 1031 | 1085 | } |
| 1032 | 1086 | if( iInReplyTo==0 && iEdit>0 ){ |
| 1033 | 1087 | iBasis = iEdit; |
| @@ -1264,10 +1318,13 @@ | ||
| 1264 | 1318 | char *zDate = 0; |
| 1265 | 1319 | const char *zFpid = PD("fpid",""); |
| 1266 | 1320 | int isCsrfSafe; |
| 1267 | 1321 | int isDelete = 0; |
| 1268 | 1322 | int fClosed = 0; |
| 1323 | + int bSameUser; /* True if author is also the reader */ | |
| 1324 | + int bPreview; /* True in preview mode. */ | |
| 1325 | + int bPrivate; /* True if post is private (not yet moderated) */ | |
| 1269 | 1326 | |
| 1270 | 1327 | login_check_credentials(); |
| 1271 | 1328 | if( !g.perm.WrForum ){ |
| 1272 | 1329 | login_needed(g.anon.WrForum); |
| 1273 | 1330 | return; |
| @@ -1282,14 +1339,18 @@ | ||
| 1282 | 1339 | } |
| 1283 | 1340 | if( P("cancel") ){ |
| 1284 | 1341 | cgi_redirectf("%R/forumpost/%S",P("fpid")); |
| 1285 | 1342 | return; |
| 1286 | 1343 | } |
| 1287 | - fClosed = forum_rid_is_closed(fpid); | |
| 1344 | + bPreview = P("preview")!=0; | |
| 1345 | + fClosed = forum_rid_is_closed(fpid, froot!=fpid); | |
| 1288 | 1346 | isCsrfSafe = cgi_csrf_safe(1); |
| 1289 | - if( g.perm.ModForum && isCsrfSafe ){ | |
| 1290 | - if( P("approve") ){ | |
| 1347 | + bPrivate = content_is_private(fpid); | |
| 1348 | + bSameUser = login_is_individual() | |
| 1349 | + && fossil_strcmp(pPost->zUser, g.zLogin)==0; | |
| 1350 | + if( isCsrfSafe && (g.perm.ModForum || (bPrivate && bSameUser)) ){ | |
| 1351 | + if( g.perm.ModForum && P("approve") ){ | |
| 1291 | 1352 | const char *zUserToTrust; |
| 1292 | 1353 | moderation_approve('f', fpid); |
| 1293 | 1354 | if( g.perm.AdminForum |
| 1294 | 1355 | && PB("trust") |
| 1295 | 1356 | && (zUserToTrust = P("trustuser"))!=0 |
| @@ -1367,18 +1428,19 @@ | ||
| 1367 | 1428 | } |
| 1368 | 1429 | style_header("Edit %s", zTitle ? "Post" : "Reply"); |
| 1369 | 1430 | @ <h2>Original Post:</h2> |
| 1370 | 1431 | forum_render(pPost->zThreadTitle, pPost->zMimetype, pPost->zWiki, |
| 1371 | 1432 | "forumEdit", 1); |
| 1372 | - if( P("preview") ){ | |
| 1433 | + if( bPreview ){ | |
| 1373 | 1434 | @ <h2>Preview of Edited Post:</h2> |
| 1374 | 1435 | forum_render(zTitle, zMimetype, zContent,"forumEdit", 1); |
| 1375 | 1436 | } |
| 1376 | 1437 | @ <h2>Revised Message:</h2> |
| 1377 | 1438 | @ <form action="%R/forume2" method="POST"> |
| 1378 | 1439 | @ <input type="hidden" name="fpid" value="%h(P("fpid"))"> |
| 1379 | 1440 | @ <input type="hidden" name="edit" value="1"> |
| 1441 | + if( fClosed ) forumpost_error_closed(); | |
| 1380 | 1442 | forum_from_line(); |
| 1381 | 1443 | forum_post_widget(zTitle, zMimetype, zContent); |
| 1382 | 1444 | }else{ |
| 1383 | 1445 | /* Reply */ |
| 1384 | 1446 | char *zDisplayName; |
| @@ -1396,27 +1458,33 @@ | ||
| 1396 | 1458 | zDisplayName = display_name_from_login(pPost->zUser); |
| 1397 | 1459 | @ <h3 class='forumPostHdr'>By %s(zDisplayName) on %h(zDate)</h3> |
| 1398 | 1460 | fossil_free(zDisplayName); |
| 1399 | 1461 | fossil_free(zDate); |
| 1400 | 1462 | forum_render(0, pPost->zMimetype, pPost->zWiki, "forumEdit", 1); |
| 1401 | - if( P("preview") && !whitespace_only(zContent) ){ | |
| 1463 | + if( bPreview && !whitespace_only(zContent) ){ | |
| 1402 | 1464 | @ <h2>Preview:</h2> |
| 1403 | 1465 | forum_render(0, zMimetype,zContent, "forumEdit", 1); |
| 1404 | 1466 | } |
| 1405 | 1467 | @ <h2>Enter Reply:</h2> |
| 1406 | 1468 | @ <form action="%R/forume2" method="POST"> |
| 1407 | 1469 | @ <input type="hidden" name="fpid" value="%h(P("fpid"))"> |
| 1408 | 1470 | @ <input type="hidden" name="reply" value="1"> |
| 1471 | + if( fClosed ) forumpost_error_closed(); | |
| 1409 | 1472 | forum_from_line(); |
| 1410 | 1473 | forum_post_widget(0, zMimetype, zContent); |
| 1411 | 1474 | } |
| 1412 | 1475 | if( !isDelete ){ |
| 1413 | 1476 | @ <input type="submit" name="preview" value="Preview"> |
| 1414 | 1477 | } |
| 1415 | 1478 | @ <input type="submit" name="cancel" value="Cancel"> |
| 1416 | - if( (P("preview") && !whitespace_only(zContent)) || isDelete ){ | |
| 1417 | - @ <input type="submit" name="submit" value="Submit"> | |
| 1479 | + if( (bPreview && !whitespace_only(zContent)) || isDelete ){ | |
| 1480 | + if( !fClosed || g.perm.Admin ) { | |
| 1481 | + @ <input type="submit" name="submit" value="Submit"> | |
| 1482 | + } | |
| 1483 | + forumpost_emit_unlock_checkbox(fClosed, fpid); | |
| 1484 | + }else if( !bPreview && fClosed ){ | |
| 1485 | + @ <span class='warning'>This post is CLOSED</span> | |
| 1418 | 1486 | } |
| 1419 | 1487 | if( g.perm.Debug ){ |
| 1420 | 1488 | /* For the test-forumnew page add these extra debugging controls */ |
| 1421 | 1489 | @ <div class="debug"> |
| 1422 | 1490 | @ <label><input type="checkbox" name="dryrun" %s(PCK("dryrun"))> \ |
| 1423 | 1491 |
| --- src/forum.c | |
| +++ src/forum.c | |
| @@ -47,11 +47,11 @@ | |
| 47 | ForumPost *pNext; /* Next in chronological order */ |
| 48 | ForumPost *pPrev; /* Previous in chronological order */ |
| 49 | ForumPost *pDisplay; /* Next in display order */ |
| 50 | int nEdit; /* Number of edits to this post */ |
| 51 | int nIndent; /* Number of levels of indentation for this post */ |
| 52 | int fClosed; /* tagxref.tagtype if this (sub)thread has a closed tag. */ |
| 53 | }; |
| 54 | |
| 55 | /* |
| 56 | ** A single instance of the following tracks all entries for a thread. |
| 57 | */ |
| @@ -83,47 +83,96 @@ | |
| 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. |
| 89 | */ |
| 90 | int forum_post_is_closed(ForumPost *p){ |
| 91 | if( !p ) return 0; |
| 92 | if( p->pEditTail ) p = p->pEditTail; |
| 93 | if( p->fClosed ) return p->fClosed; |
| 94 | else if( p->pIrt ){ |
| 95 | return forum_post_is_closed(p->pIrt->pEditTail |
| 96 | ? p->pIrt->pEditTail : p->pIrt); |
| 97 | } |
| 98 | return 0; |
| 99 | } |
| 100 | |
| 101 | /* |
| 102 | ** Given a forum post RID, this function returns true if that post or |
| 103 | ** the latest version of any parent post in its hierarchy have an |
| 104 | ** active "closed" tag. |
| 105 | */ |
| 106 | int forum_rid_is_closed(int rid){ |
| 107 | static Stmt qIrt = empty_Stmt_m; |
| 108 | int rc; |
| 109 | |
| 110 | /* TODO: this can probably be turned into a CTE, rather than a |
| 111 | ** recursive call into this function, by someone with superior |
| 112 | ** SQL-fu. */ |
| 113 | rc = rid_has_active_tag_name(rid, "closed"); |
| 114 | if( rc ) return rc; |
| 115 | else if( !qIrt.pStmt ) { |
| 116 | db_static_prepare(&qIrt, |
| 117 | "SELECT firt FROM forumpost " |
| 118 | "WHERE fpid=$fpid ORDER BY fmtime DESC" |
| 119 | ); |
| 120 | } |
| 121 | db_bind_int(&qIrt, "$fpid", rid); |
| 122 | rc = SQLITE_ROW==db_step(&qIrt) ? db_column_int(&qIrt, 0) : 0; |
| 123 | db_reset(&qIrt); |
| 124 | return rc>0 ? forum_rid_is_closed(rc) : 0; |
| 125 | } |
| 126 | |
| 127 | /* |
| 128 | ** Delete a complete ForumThread and all its entries. |
| 129 | */ |
| @@ -259,11 +308,11 @@ | |
| 259 | for(; p; p=p->pEditPrev ){ |
| 260 | p->nEdit = pPost->nEdit; |
| 261 | p->pEditTail = pPost; |
| 262 | } |
| 263 | } |
| 264 | pPost->fClosed = rid_has_active_tag_name(pPost->fpid, "closed"); |
| 265 | } |
| 266 | db_finalize(&q); |
| 267 | |
| 268 | if( computeHierarchy ){ |
| 269 | /* Compute the hierarchical display order */ |
| @@ -511,11 +560,11 @@ | |
| 511 | const char *zMimetype;/* Formatting MIME type */ |
| 512 | |
| 513 | /* Get the manifest for the post. Abort if not found (e.g. shunned). */ |
| 514 | pManifest = manifest_get(p->fpid, CFTYPE_FORUM, 0); |
| 515 | if( !pManifest ) return; |
| 516 | fClosed = forum_post_is_closed(p); |
| 517 | /* When not in raw mode, create the border around the post. */ |
| 518 | if( !bRaw ){ |
| 519 | /* Open the <div> enclosing the post. Set the class string to mark the post |
| 520 | ** as selected and/or obsolete. */ |
| 521 | iIndent = (p->pEditHead ? p->pEditHead->nIndent : p->nIndent)-1; |
| @@ -1024,10 +1073,15 @@ | |
| 1024 | Blob x, cksum, formatCheck, errMsg; |
| 1025 | Manifest *pPost; |
| 1026 | int nContent = zContent ? (int)strlen(zContent) : 0; |
| 1027 | |
| 1028 | schema_forum(); |
| 1029 | if( iEdit==0 && whitespace_only(zContent) ){ |
| 1030 | return 0; |
| 1031 | } |
| 1032 | if( iInReplyTo==0 && iEdit>0 ){ |
| 1033 | iBasis = iEdit; |
| @@ -1264,10 +1318,13 @@ | |
| 1264 | char *zDate = 0; |
| 1265 | const char *zFpid = PD("fpid",""); |
| 1266 | int isCsrfSafe; |
| 1267 | int isDelete = 0; |
| 1268 | int fClosed = 0; |
| 1269 | |
| 1270 | login_check_credentials(); |
| 1271 | if( !g.perm.WrForum ){ |
| 1272 | login_needed(g.anon.WrForum); |
| 1273 | return; |
| @@ -1282,14 +1339,18 @@ | |
| 1282 | } |
| 1283 | if( P("cancel") ){ |
| 1284 | cgi_redirectf("%R/forumpost/%S",P("fpid")); |
| 1285 | return; |
| 1286 | } |
| 1287 | fClosed = forum_rid_is_closed(fpid); |
| 1288 | isCsrfSafe = cgi_csrf_safe(1); |
| 1289 | if( g.perm.ModForum && isCsrfSafe ){ |
| 1290 | if( P("approve") ){ |
| 1291 | const char *zUserToTrust; |
| 1292 | moderation_approve('f', fpid); |
| 1293 | if( g.perm.AdminForum |
| 1294 | && PB("trust") |
| 1295 | && (zUserToTrust = P("trustuser"))!=0 |
| @@ -1367,18 +1428,19 @@ | |
| 1367 | } |
| 1368 | style_header("Edit %s", zTitle ? "Post" : "Reply"); |
| 1369 | @ <h2>Original Post:</h2> |
| 1370 | forum_render(pPost->zThreadTitle, pPost->zMimetype, pPost->zWiki, |
| 1371 | "forumEdit", 1); |
| 1372 | if( P("preview") ){ |
| 1373 | @ <h2>Preview of Edited Post:</h2> |
| 1374 | forum_render(zTitle, zMimetype, zContent,"forumEdit", 1); |
| 1375 | } |
| 1376 | @ <h2>Revised Message:</h2> |
| 1377 | @ <form action="%R/forume2" method="POST"> |
| 1378 | @ <input type="hidden" name="fpid" value="%h(P("fpid"))"> |
| 1379 | @ <input type="hidden" name="edit" value="1"> |
| 1380 | forum_from_line(); |
| 1381 | forum_post_widget(zTitle, zMimetype, zContent); |
| 1382 | }else{ |
| 1383 | /* Reply */ |
| 1384 | char *zDisplayName; |
| @@ -1396,27 +1458,33 @@ | |
| 1396 | zDisplayName = display_name_from_login(pPost->zUser); |
| 1397 | @ <h3 class='forumPostHdr'>By %s(zDisplayName) on %h(zDate)</h3> |
| 1398 | fossil_free(zDisplayName); |
| 1399 | fossil_free(zDate); |
| 1400 | forum_render(0, pPost->zMimetype, pPost->zWiki, "forumEdit", 1); |
| 1401 | if( P("preview") && !whitespace_only(zContent) ){ |
| 1402 | @ <h2>Preview:</h2> |
| 1403 | forum_render(0, zMimetype,zContent, "forumEdit", 1); |
| 1404 | } |
| 1405 | @ <h2>Enter Reply:</h2> |
| 1406 | @ <form action="%R/forume2" method="POST"> |
| 1407 | @ <input type="hidden" name="fpid" value="%h(P("fpid"))"> |
| 1408 | @ <input type="hidden" name="reply" value="1"> |
| 1409 | forum_from_line(); |
| 1410 | forum_post_widget(0, zMimetype, zContent); |
| 1411 | } |
| 1412 | if( !isDelete ){ |
| 1413 | @ <input type="submit" name="preview" value="Preview"> |
| 1414 | } |
| 1415 | @ <input type="submit" name="cancel" value="Cancel"> |
| 1416 | if( (P("preview") && !whitespace_only(zContent)) || isDelete ){ |
| 1417 | @ <input type="submit" name="submit" value="Submit"> |
| 1418 | } |
| 1419 | if( g.perm.Debug ){ |
| 1420 | /* For the test-forumnew page add these extra debugging controls */ |
| 1421 | @ <div class="debug"> |
| 1422 | @ <label><input type="checkbox" name="dryrun" %s(PCK("dryrun"))> \ |
| 1423 |
| --- src/forum.c | |
| +++ src/forum.c | |
| @@ -47,11 +47,11 @@ | |
| 47 | ForumPost *pNext; /* Next in chronological order */ |
| 48 | ForumPost *pPrev; /* Previous in chronological order */ |
| 49 | ForumPost *pDisplay; /* Next in display order */ |
| 50 | int nEdit; /* Number of edits to this post */ |
| 51 | int nIndent; /* Number of levels of indentation for this post */ |
| 52 | int fClosed; /* See forum_rid_is_closed() */ |
| 53 | }; |
| 54 | |
| 55 | /* |
| 56 | ** A single instance of the following tracks all entries for a thread. |
| 57 | */ |
| @@ -83,47 +83,96 @@ | |
| 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 | int forum_post_is_closed(ForumPost *p, int bCheckParents){ |
| 96 | if( !p ) return 0; |
| 97 | if( p->pEditTail ) p = p->pEditTail; |
| 98 | if( p->fClosed || !bCheckParents ) return p->fClosed; |
| 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 | ** If fClosed is true and the current user has admin privileges, this |
| 150 | ** renders either a checkbox to unlock forum post fpid (if fClosed>0) |
| 151 | ** or a SPAN.warning element that the given post inherits the CLOSED |
| 152 | ** status from a parent post (if fClosed<0). If neither of the initial |
| 153 | ** conditions is true, this is a no-op. |
| 154 | */ |
| 155 | static void forumpost_emit_unlock_checkbox(int fClosed, int fpid){ |
| 156 | if( fClosed && g.perm.Admin ){ |
| 157 | if( fClosed>0 ){ |
| 158 | /* Only show the "unlock" checkbox on a post which is actually |
| 159 | ** closed, not on a post which inherits that state. */ |
| 160 | @ <label class='warning'><input type="checkbox" name="reopen" value="1"> |
| 161 | @ Re-open this CLOSED post? (NOT YET IMPLEMENTED)</label> |
| 162 | }else{ |
| 163 | @ <span class='warning'>This post is CLOSED via a parent post</span> |
| 164 | } |
| 165 | } |
| 166 | } |
| 167 | |
| 168 | /* |
| 169 | ** Emits a warning that the current forum post is CLOSED and can only |
| 170 | ** be edited or responded to by an administrator. */ |
| 171 | static void forumpost_error_closed(void){ |
| 172 | @ <div class='error'>This (sub)thread is CLOSED and can only be |
| 173 | @ edited or replied to by an admin user.</div> |
| 174 | } |
| 175 | |
| 176 | /* |
| 177 | ** Delete a complete ForumThread and all its entries. |
| 178 | */ |
| @@ -259,11 +308,11 @@ | |
| 308 | for(; p; p=p->pEditPrev ){ |
| 309 | p->nEdit = pPost->nEdit; |
| 310 | p->pEditTail = pPost; |
| 311 | } |
| 312 | } |
| 313 | pPost->fClosed = forum_rid_is_closed(pPost->fpid, 1); |
| 314 | } |
| 315 | db_finalize(&q); |
| 316 | |
| 317 | if( computeHierarchy ){ |
| 318 | /* Compute the hierarchical display order */ |
| @@ -511,11 +560,11 @@ | |
| 560 | const char *zMimetype;/* Formatting MIME type */ |
| 561 | |
| 562 | /* Get the manifest for the post. Abort if not found (e.g. shunned). */ |
| 563 | pManifest = manifest_get(p->fpid, CFTYPE_FORUM, 0); |
| 564 | if( !pManifest ) return; |
| 565 | fClosed = forum_post_is_closed(p, 1); |
| 566 | /* When not in raw mode, create the border around the post. */ |
| 567 | if( !bRaw ){ |
| 568 | /* Open the <div> enclosing the post. Set the class string to mark the post |
| 569 | ** as selected and/or obsolete. */ |
| 570 | iIndent = (p->pEditHead ? p->pEditHead->nIndent : p->nIndent)-1; |
| @@ -1024,10 +1073,15 @@ | |
| 1073 | Blob x, cksum, formatCheck, errMsg; |
| 1074 | Manifest *pPost; |
| 1075 | int nContent = zContent ? (int)strlen(zContent) : 0; |
| 1076 | |
| 1077 | schema_forum(); |
| 1078 | if( !g.perm.Admin && (iEdit || iInReplyTo) |
| 1079 | && forum_rid_is_closed(iEdit ? iEdit : iInReplyTo, 1) ){ |
| 1080 | forumpost_error_closed(); |
| 1081 | return 0; |
| 1082 | } |
| 1083 | if( iEdit==0 && whitespace_only(zContent) ){ |
| 1084 | return 0; |
| 1085 | } |
| 1086 | if( iInReplyTo==0 && iEdit>0 ){ |
| 1087 | iBasis = iEdit; |
| @@ -1264,10 +1318,13 @@ | |
| 1318 | char *zDate = 0; |
| 1319 | const char *zFpid = PD("fpid",""); |
| 1320 | int isCsrfSafe; |
| 1321 | int isDelete = 0; |
| 1322 | int fClosed = 0; |
| 1323 | int bSameUser; /* True if author is also the reader */ |
| 1324 | int bPreview; /* True in preview mode. */ |
| 1325 | int bPrivate; /* True if post is private (not yet moderated) */ |
| 1326 | |
| 1327 | login_check_credentials(); |
| 1328 | if( !g.perm.WrForum ){ |
| 1329 | login_needed(g.anon.WrForum); |
| 1330 | return; |
| @@ -1282,14 +1339,18 @@ | |
| 1339 | } |
| 1340 | if( P("cancel") ){ |
| 1341 | cgi_redirectf("%R/forumpost/%S",P("fpid")); |
| 1342 | return; |
| 1343 | } |
| 1344 | bPreview = P("preview")!=0; |
| 1345 | fClosed = forum_rid_is_closed(fpid, froot!=fpid); |
| 1346 | isCsrfSafe = cgi_csrf_safe(1); |
| 1347 | bPrivate = content_is_private(fpid); |
| 1348 | bSameUser = login_is_individual() |
| 1349 | && fossil_strcmp(pPost->zUser, g.zLogin)==0; |
| 1350 | if( isCsrfSafe && (g.perm.ModForum || (bPrivate && bSameUser)) ){ |
| 1351 | if( g.perm.ModForum && P("approve") ){ |
| 1352 | const char *zUserToTrust; |
| 1353 | moderation_approve('f', fpid); |
| 1354 | if( g.perm.AdminForum |
| 1355 | && PB("trust") |
| 1356 | && (zUserToTrust = P("trustuser"))!=0 |
| @@ -1367,18 +1428,19 @@ | |
| 1428 | } |
| 1429 | style_header("Edit %s", zTitle ? "Post" : "Reply"); |
| 1430 | @ <h2>Original Post:</h2> |
| 1431 | forum_render(pPost->zThreadTitle, pPost->zMimetype, pPost->zWiki, |
| 1432 | "forumEdit", 1); |
| 1433 | if( bPreview ){ |
| 1434 | @ <h2>Preview of Edited Post:</h2> |
| 1435 | forum_render(zTitle, zMimetype, zContent,"forumEdit", 1); |
| 1436 | } |
| 1437 | @ <h2>Revised Message:</h2> |
| 1438 | @ <form action="%R/forume2" method="POST"> |
| 1439 | @ <input type="hidden" name="fpid" value="%h(P("fpid"))"> |
| 1440 | @ <input type="hidden" name="edit" value="1"> |
| 1441 | if( fClosed ) forumpost_error_closed(); |
| 1442 | forum_from_line(); |
| 1443 | forum_post_widget(zTitle, zMimetype, zContent); |
| 1444 | }else{ |
| 1445 | /* Reply */ |
| 1446 | char *zDisplayName; |
| @@ -1396,27 +1458,33 @@ | |
| 1458 | zDisplayName = display_name_from_login(pPost->zUser); |
| 1459 | @ <h3 class='forumPostHdr'>By %s(zDisplayName) on %h(zDate)</h3> |
| 1460 | fossil_free(zDisplayName); |
| 1461 | fossil_free(zDate); |
| 1462 | forum_render(0, pPost->zMimetype, pPost->zWiki, "forumEdit", 1); |
| 1463 | if( bPreview && !whitespace_only(zContent) ){ |
| 1464 | @ <h2>Preview:</h2> |
| 1465 | forum_render(0, zMimetype,zContent, "forumEdit", 1); |
| 1466 | } |
| 1467 | @ <h2>Enter Reply:</h2> |
| 1468 | @ <form action="%R/forume2" method="POST"> |
| 1469 | @ <input type="hidden" name="fpid" value="%h(P("fpid"))"> |
| 1470 | @ <input type="hidden" name="reply" value="1"> |
| 1471 | if( fClosed ) forumpost_error_closed(); |
| 1472 | forum_from_line(); |
| 1473 | forum_post_widget(0, zMimetype, zContent); |
| 1474 | } |
| 1475 | if( !isDelete ){ |
| 1476 | @ <input type="submit" name="preview" value="Preview"> |
| 1477 | } |
| 1478 | @ <input type="submit" name="cancel" value="Cancel"> |
| 1479 | if( (bPreview && !whitespace_only(zContent)) || isDelete ){ |
| 1480 | if( !fClosed || g.perm.Admin ) { |
| 1481 | @ <input type="submit" name="submit" value="Submit"> |
| 1482 | } |
| 1483 | forumpost_emit_unlock_checkbox(fClosed, fpid); |
| 1484 | }else if( !bPreview && fClosed ){ |
| 1485 | @ <span class='warning'>This post is CLOSED</span> |
| 1486 | } |
| 1487 | if( g.perm.Debug ){ |
| 1488 | /* For the test-forumnew page add these extra debugging controls */ |
| 1489 | @ <div class="debug"> |
| 1490 | @ <label><input type="checkbox" name="dryrun" %s(PCK("dryrun"))> \ |
| 1491 |
+6
-8
| --- src/tag.c | ||
| +++ src/tag.c | ||
| @@ -913,24 +913,22 @@ | ||
| 913 | 913 | ** string, else returns 0. Note that this function does not |
| 914 | 914 | ** distinguish between a non-existent tag and a cancelled tag. |
| 915 | 915 | */ |
| 916 | 916 | int rid_has_active_tag_name(int rid, const char *zTagName){ |
| 917 | 917 | static Stmt q = empty_Stmt_m; |
| 918 | - int rc = 0; | |
| 918 | + int rc; | |
| 919 | 919 | |
| 920 | 920 | assert( 0 != zTagName ); |
| 921 | 921 | if( !q.pStmt ){ |
| 922 | 922 | db_static_prepare(&q, |
| 923 | - "SELECT tagxref.rowid FROM tagxref, tag" | |
| 924 | - " WHERE tagxref.rid=$rid AND tagtype>0 " | |
| 925 | - " AND tag.tagname=$tagname" | |
| 926 | - " AND tagxref.tagid=tag.tagid" | |
| 923 | + "SELECT x.rowid FROM tagxref x, tag t" | |
| 924 | + " WHERE x.rid=$rid AND x.tagtype>0 " | |
| 925 | + " AND x.tagid=t.tagid" | |
| 926 | + " AND t.tagname=$tagname" | |
| 927 | 927 | ); |
| 928 | 928 | } |
| 929 | 929 | db_bind_int(&q, "$rid", rid); |
| 930 | 930 | db_bind_text(&q, "$tagname", zTagName); |
| 931 | - if( SQLITE_ROW==db_step(&q) ){ | |
| 932 | - rc = db_column_int(&q, 0); | |
| 933 | - } | |
| 931 | + rc = (SQLITE_ROW==db_step(&q)) ? db_column_int(&q, 0) : 0; | |
| 934 | 932 | db_reset(&q); |
| 935 | 933 | return rc; |
| 936 | 934 | } |
| 937 | 935 |
| --- src/tag.c | |
| +++ src/tag.c | |
| @@ -913,24 +913,22 @@ | |
| 913 | ** string, else returns 0. Note that this function does not |
| 914 | ** distinguish between a non-existent tag and a cancelled tag. |
| 915 | */ |
| 916 | int rid_has_active_tag_name(int rid, const char *zTagName){ |
| 917 | static Stmt q = empty_Stmt_m; |
| 918 | int rc = 0; |
| 919 | |
| 920 | assert( 0 != zTagName ); |
| 921 | if( !q.pStmt ){ |
| 922 | db_static_prepare(&q, |
| 923 | "SELECT tagxref.rowid FROM tagxref, tag" |
| 924 | " WHERE tagxref.rid=$rid AND tagtype>0 " |
| 925 | " AND tag.tagname=$tagname" |
| 926 | " AND tagxref.tagid=tag.tagid" |
| 927 | ); |
| 928 | } |
| 929 | db_bind_int(&q, "$rid", rid); |
| 930 | db_bind_text(&q, "$tagname", zTagName); |
| 931 | if( SQLITE_ROW==db_step(&q) ){ |
| 932 | rc = db_column_int(&q, 0); |
| 933 | } |
| 934 | db_reset(&q); |
| 935 | return rc; |
| 936 | } |
| 937 |
| --- src/tag.c | |
| +++ src/tag.c | |
| @@ -913,24 +913,22 @@ | |
| 913 | ** string, else returns 0. Note that this function does not |
| 914 | ** distinguish between a non-existent tag and a cancelled tag. |
| 915 | */ |
| 916 | int rid_has_active_tag_name(int rid, const char *zTagName){ |
| 917 | static Stmt q = empty_Stmt_m; |
| 918 | int rc; |
| 919 | |
| 920 | assert( 0 != zTagName ); |
| 921 | if( !q.pStmt ){ |
| 922 | db_static_prepare(&q, |
| 923 | "SELECT x.rowid FROM tagxref x, tag t" |
| 924 | " WHERE x.rid=$rid AND x.tagtype>0 " |
| 925 | " AND x.tagid=t.tagid" |
| 926 | " AND t.tagname=$tagname" |
| 927 | ); |
| 928 | } |
| 929 | db_bind_int(&q, "$rid", rid); |
| 930 | db_bind_text(&q, "$tagname", zTagName); |
| 931 | rc = (SQLITE_ROW==db_step(&q)) ? db_column_int(&q, 0) : 0; |
| 932 | db_reset(&q); |
| 933 | return rc; |
| 934 | } |
| 935 |