| | @@ -255,14 +255,24 @@ |
| 255 | 255 | ** matches the event.(euser,user) field for a formpost entry with the |
| 256 | 256 | ** matching RID. Returns false if no match is found. If zUserName is |
| 257 | 257 | ** 0 then login_name() is used. |
| 258 | 258 | */ |
| 259 | 259 | int forumpost_is_owner(int rid, const char *zUserName){ |
| 260 | | - return db_int(0, "SELECT 1 FROM event " |
| 261 | | - "WHERE type='f' AND objid=%d " |
| 262 | | - "AND coalesce(euser,user)=%Q", |
| 263 | | - rid, zUserName ? zUserName : login_name()); |
| 260 | + static Stmt q; |
| 261 | + int rc; |
| 262 | + if( !q.pStmt ){ |
| 263 | + db_static_prepare( |
| 264 | + &q, "SELECT 1 FROM event" |
| 265 | + " WHERE type='f' AND objid=$rid" |
| 266 | + " AND coalesce(euser,user)=$user" |
| 267 | + ); |
| 268 | + } |
| 269 | + db_bind_int(&q, "$rid", rid); |
| 270 | + db_bind_text(&q, "$user", zUserName ? zUserName : login_name()); |
| 271 | + rc = SQLITE_ROW==db_step(&q); |
| 272 | + db_reset(&q); |
| 273 | + return rc; |
| 264 | 274 | } |
| 265 | 275 | |
| 266 | 276 | /* |
| 267 | 277 | ** Returns true if p, or any parent of p, has a non-zero iClosed |
| 268 | 278 | ** value. Returns 0 if !p. For an edited chain of post, the tag is |
| | @@ -397,25 +407,27 @@ |
| 397 | 407 | ** provide consistent behavior, it always acts on the first version of |
| 398 | 408 | ** the given forum post, walking the forumpost.fprev values to find |
| 399 | 409 | ** the head of the chain. |
| 400 | 410 | ** |
| 401 | 411 | ** If addTag is true then a propagating tag is added, except as noted |
| 402 | | -** below, with the given optional zReason string as the tag's |
| 412 | +** below, with the given optional zValue string as the tag's |
| 403 | 413 | ** value. If addTag is false then any matching active tag on frid is |
| 404 | | -** cancelled, except as noted below. zReason is ignored if it is NULL |
| 414 | +** cancelled, except as noted below. zValue is ignored if it is NULL |
| 405 | 415 | ** or starts with a NUL byte, or if addTag is false. |
| 406 | 416 | ** |
| 407 | 417 | ** This function only adds a tag if forum_rid_is_tagged() indicates |
| 408 | 418 | ** that frid's head is not tagged. If a parent post is already tagged, |
| 409 | 419 | ** no tag is added. Similarly, it will only remove a tag from a post |
| 410 | 420 | ** which has its own tag, and will not remove an inherited one from a |
| 411 | 421 | ** parent post. |
| 412 | 422 | ** |
| 413 | | -** If addTag is true and frid is already tagged (directly or |
| 414 | | -** inherited), this is a no-op. Likewise, if addTag is false and frid |
| 415 | | -** itself is not tagged (not accounting for an inherited closed tag), |
| 416 | | -** this is a no-op. |
| 423 | +** If addTag is true and frid is already tagged, this is a |
| 424 | +** no-op. Likewise, if addTag is false and frid is not tagged |
| 425 | +** (not accounting for an inherited closed tag), this is a no-op. |
| 426 | +** |
| 427 | +** If bCheckIrt is true then the forum post IRT hierarchy is searched |
| 428 | +** for the tag, otherwise only the given RID is checked. |
| 417 | 429 | ** |
| 418 | 430 | ** Returns true if it actually creates a new tag, else false. Fails |
| 419 | 431 | ** fatally on error. |
| 420 | 432 | ** |
| 421 | 433 | ** If it returns true then state from previously-loaded posts may be |
| | @@ -438,57 +450,62 @@ |
| 438 | 450 | ** - The applied tag is propagating so so that "closed" tags can |
| 439 | 451 | ** account for how edits of posts are handled. This differs from |
| 440 | 452 | ** closure of a branch, where a non-propagating tag is used. |
| 441 | 453 | */ |
| 442 | 454 | static int forumpost_tag(int frid, const char *zTagName, int addTag, |
| 443 | | - const char *zReason){ |
| 455 | + const char *zValue){ |
| 444 | 456 | Blob artifact = BLOB_INITIALIZER; /* Output artifact */ |
| 445 | 457 | Blob cksum = BLOB_INITIALIZER; /* Z-card */ |
| 446 | 458 | int iTagged; /* true if frid is already tagged */ |
| 447 | 459 | int trid; /* RID of new control artifact */ |
| 448 | 460 | char *zUuid; /* UUID of head version of post */ |
| 449 | 461 | |
| 450 | 462 | db_begin_transaction(); |
| 451 | 463 | frid = forumpost_head_rid(frid); |
| 452 | | - iTagged = forum_rid_is_tagged(frid, zTagName, 1); |
| 453 | | - if( (iTagged && addTag |
| 454 | | - /* Already tagged, noting that in the case of (addTag<0) it may |
| 455 | | - ** actually be a parent which is tagged. */) |
| 456 | | - || (iTagged<=0 && !addTag |
| 457 | | - /* This entry is not tagged, but a parent post may be. */) ){ |
| 464 | + iTagged = forum_rid_is_tagged(frid, zTagName, 0); |
| 465 | + if( !addTag && !iTagged ){ |
| 466 | + /* Nothing to do. We never tag an IRT-inherited post via this |
| 467 | + ** function. */ |
| 458 | 468 | db_end_transaction(0); |
| 459 | 469 | return 0; |
| 460 | 470 | } |
| 461 | | - if( addTag==0 || (zReason && !zReason[0]) ){ |
| 462 | | - zReason = 0; |
| 471 | + if( !addTag || (zValue && !zValue[0]) ){ |
| 472 | + zValue = 0; |
| 473 | + } |
| 474 | + if( addTag && iTagged ){ |
| 475 | + char *zOld = 0; |
| 476 | + int cmp; |
| 477 | + rid_has_tag2(iTagged, zTagName, &zOld); |
| 478 | + cmp = fossil_strcmp(zOld, zValue); |
| 479 | + fossil_free(zOld); |
| 480 | + if( 0==cmp ){ |
| 481 | + /* Same value - leave it as is. */ |
| 482 | + db_end_transaction(0); |
| 483 | + return 0; |
| 484 | + } |
| 463 | 485 | } |
| 464 | 486 | zUuid = rid_to_uuid(frid); |
| 465 | 487 | blob_appendf(&artifact, "D %z\n", date_in_standard_format( "now" )); |
| 466 | 488 | blob_appendf(&artifact, "T %c%s %s%s%F\n", |
| 467 | 489 | addTag ? '*' : '-', zTagName, |
| 468 | | - zUuid, zReason ? " " : "", zReason ? zReason : ""); |
| 490 | + zUuid, zValue ? " " : "", zValue ? zValue : ""); |
| 469 | 491 | blob_appendf(&artifact, "U %F\n", login_name()); |
| 470 | 492 | md5sum_blob(&artifact, &cksum); |
| 471 | 493 | blob_appendf(&artifact, "Z %b\n", &cksum); |
| 472 | 494 | blob_reset(&cksum); |
| 473 | 495 | trid = content_put_ex(&artifact, 0, 0, 0, 0); |
| 474 | 496 | if( trid==0 ){ |
| 475 | 497 | fossil_fatal("Error saving tag artifact: %s", g.zErrMsg); |
| 476 | 498 | } |
| 477 | | - if( manifest_crosslink(trid, &artifact, |
| 478 | | - MC_NONE /*MC_PERMIT_HOOKS?*/)==0 ){ |
| 499 | + if( manifest_crosslink(trid, &artifact, MC_NONE)==0 ){ |
| 479 | 500 | fossil_fatal("%s", g.zErrMsg); |
| 480 | 501 | } |
| 481 | 502 | assert( blob_is_reset(&artifact) ); |
| 482 | 503 | db_add_unsent(trid); |
| 483 | 504 | admin_log("Tag forum post %S with %c%s", |
| 484 | 505 | zUuid, addTag ? '*' : '-', zTagName); |
| 485 | 506 | fossil_free(zUuid); |
| 486 | | - /* Potential TODO: if (iClosed>0) then we could find the initial tag |
| 487 | | - ** artifact and content_deltify(thatRid,&trid,1,0). Given the tiny |
| 488 | | - ** size of these artifacts, however, that would save little space, |
| 489 | | - ** if any. */ |
| 490 | 507 | db_end_transaction(0); |
| 491 | 508 | return 1; |
| 492 | 509 | } |
| 493 | 510 | |
| 494 | 511 | /* |
| | @@ -853,10 +870,72 @@ |
| 853 | 870 | } |
| 854 | 871 | @ </table> |
| 855 | 872 | db_finalize(&q); |
| 856 | 873 | style_finish_page(); |
| 857 | 874 | } |
| 875 | + |
| 876 | +/* |
| 877 | +** Returns true if the current user is authorized to set forum post |
| 878 | +** fpid's status. |
| 879 | +*/ |
| 880 | +static int forum_may_set_status(int fpid){ |
| 881 | + return g.perm.Admin |
| 882 | + || g.perm.ModForum |
| 883 | + || (login_is_individual() |
| 884 | + && forumpost_is_owner(fpid, 0)); |
| 885 | +} |
| 886 | + |
| 887 | +/* |
| 888 | +** If the current user is authorized to set fp's status then this |
| 889 | +** renders a mini-form for setting the status then redirecting back to |
| 890 | +** the post. Else it may emit a status label or no output. |
| 891 | +*/ |
| 892 | +static void forum_render_status_selection( const ForumPost *fp ){ |
| 893 | + const ForumStatusList * const fss = forum_statuses(); |
| 894 | + if( fss->n>1 ){ |
| 895 | + const ForumPost * pHead = fp->pEditHead ? fp->pEditHead : fp; |
| 896 | + int i; |
| 897 | + char * zCurrent = 0; |
| 898 | + const ForumStatus * sCurrent = 0; |
| 899 | + rid_has_tag2(pHead->fpid, "status", &zCurrent); |
| 900 | + for( i = 0; i < fss->n; ++i ){ |
| 901 | + const ForumStatus * const fs = &fss->aStatus[i]; |
| 902 | + if( 0==fossil_strcmp(zCurrent, fs->zValue) ){ |
| 903 | + sCurrent = fs; |
| 904 | + break; |
| 905 | + } |
| 906 | + } |
| 907 | + if( !sCurrent ) sCurrent = &fss->aStatus[0]; |
| 908 | + assert( sCurrent ); |
| 909 | + @ <span class='forum-status-selection'> |
| 910 | + if( forum_may_set_status(fp->fpid) |
| 911 | + /* FIXME: only do this if fp is the currently-selected post */ ){ |
| 912 | + @ <form method="post" action='%R/forumpost_status'> |
| 913 | + login_insert_csrf_secret(); |
| 914 | + @ <input type='hidden' name='fpid' value='%s(fp->zUuid)' /> |
| 915 | + @ <select name='status' data-fpid='%s(fp->zUuid)>'\ |
| 916 | + @ data-initial-value='%h(zCurrent ? zCurrent : "")'> |
| 917 | + for( i = 0; i < fss->n; ++i ){ |
| 918 | + const ForumStatus * const fs = &fss->aStatus[i]; |
| 919 | + @ <option value='%h(fs->zValue)'\ |
| 920 | + @ %s(sCurrent==fs ? " selected" : "")>\ |
| 921 | + @ %h(fs->zLabel)</option> |
| 922 | + } |
| 923 | + @ </select> |
| 924 | + @ <input type='button' class='submit action-status' disabled |
| 925 | + @ value='Change' /> |
| 926 | + /* ^^^ This must be <input>, not <button>, or else tapping it |
| 927 | + ** will unconditionally submit. */ |
| 928 | + @ </form> |
| 929 | + /* Form is activated in fossil.page.forumpost.js */ |
| 930 | + }else{ |
| 931 | + @ Status: %h(sCurrent->zLabel); |
| 932 | + } |
| 933 | + @ </span> |
| 934 | + fossil_free(zCurrent); |
| 935 | + } |
| 936 | +} |
| 858 | 937 | |
| 859 | 938 | /* |
| 860 | 939 | ** Render a forum post for display |
| 861 | 940 | */ |
| 862 | 941 | void forum_render( |
| | @@ -1129,11 +1208,11 @@ |
| 1129 | 1208 | /* Provide a link to the raw source code. */ |
| 1130 | 1209 | if( !bUnf ){ |
| 1131 | 1210 | @ %z(href("%R/forumpost/%!S?raw",p->zUuid))[source]</a> |
| 1132 | 1211 | } |
| 1133 | 1212 | @ </h3> |
| 1134 | | - } |
| 1213 | + }/*!bRaw*/ |
| 1135 | 1214 | |
| 1136 | 1215 | /* Check if this post is approved, also if it's by the current user. */ |
| 1137 | 1216 | bPrivate = content_is_private(p->fpid); |
| 1138 | 1217 | bSameUser = login_is_individual() |
| 1139 | 1218 | && fossil_strcmp(pManifest->zUser, g.zLogin)==0; |
| | @@ -1193,25 +1272,25 @@ |
| 1193 | 1272 | login_insert_csrf_secret(); |
| 1194 | 1273 | @ </form> |
| 1195 | 1274 | |
| 1196 | 1275 | if( bSelect ){ |
| 1197 | 1276 | const ForumPost *pHead = p->pEditHead ? p->pEditHead : p; |
| 1277 | + const int bIsOwner = forumpost_is_owner(p/*not pHead*/->fpid, 0); |
| 1198 | 1278 | if( forumpost_may_close() && iClosed>=0 ){ |
| 1199 | 1279 | @ <form method="post" \ |
| 1200 | 1280 | @ action='%R/forumpost_%s(iClosed > 0 ? "reopen" : "close")'> |
| 1201 | 1281 | login_insert_csrf_secret(); |
| 1202 | 1282 | @ <input type="hidden" name="fpid" value="%s(p->zUuid)" /> |
| 1203 | 1283 | if( moderation_pending(p->fpid)==0 ){ |
| 1204 | 1284 | @ <input type="button" value='%s(iClosed ? "Re-open" : "Close")' \ |
| 1205 | | - @ class='hidden %s(iClosed ? "action-reopen" : "action-close")'/> |
| 1285 | + @ class='submit hidden \ |
| 1286 | + @ %s(iClosed ? "action-reopen" : "action-close")'/> |
| 1206 | 1287 | /* ^^^ activated by fossil.page.forumpost.js */ |
| 1207 | 1288 | } |
| 1208 | 1289 | @ </form> |
| 1209 | 1290 | } |
| 1210 | | - if( g.perm.Admin || |
| 1211 | | - (login_is_individual() |
| 1212 | | - && forumpost_is_owner(p/*not pHead*/->fpid, 0)) ){ |
| 1291 | + if( g.perm.Admin || (login_is_individual() && bIsOwner) ){ |
| 1213 | 1292 | /* When an admin edits someone else's post, the admin |
| 1214 | 1293 | ** effectively takes over ownership of it (and we currently |
| 1215 | 1294 | ** have no way of passing it back). Because of this, we |
| 1216 | 1295 | ** check the ownership of `p` instead of `pHead`. */ |
| 1217 | 1296 | @ <form method="post" action="%R/attachadd">\ |
| | @@ -1219,26 +1298,33 @@ |
| 1219 | 1298 | @ <input type="submit" value="Attach..."> |
| 1220 | 1299 | login_insert_csrf_secret(); |
| 1221 | 1300 | moderation_pending_www(p->fpid); |
| 1222 | 1301 | @ </form> |
| 1223 | 1302 | } |
| 1224 | | - if( !p->pIrt && g.perm.Setup ){ |
| 1225 | | - const int isPinned = forum_rid_is_tagged(pHead->fpid, "pinned", 0); |
| 1226 | | - @ <form method="post" \ |
| 1227 | | - @ action='%R/forumpost_%s(isPinned ? "unpin" : "pin")'> |
| 1228 | | - login_insert_csrf_secret(); |
| 1229 | | - @ <input type="hidden" name="fpid" value="%s(p->zUuid)" /> |
| 1230 | | - @ <input type="button" value='%s(isPinned ? "Unpin" : "Pin")' \ |
| 1231 | | - @ class='hidden %s(isPinned ? "action-unpin" : "action-pin")'/> |
| 1232 | | - /* ^^^ activated by fossil.page.forumpost.js */ |
| 1233 | | - @ </form> |
| 1303 | + if( !p->pIrt ){ |
| 1304 | + /* Root node only... */ |
| 1305 | + if( g.perm.Setup ){ |
| 1306 | + const int isPinned = forum_rid_is_tagged(pHead->fpid, "pinned", 0); |
| 1307 | + @ <form method="post" \ |
| 1308 | + @ action='%R/forumpost_%s(isPinned ? "unpin" : "pin")'> |
| 1309 | + login_insert_csrf_secret(); |
| 1310 | + @ <input type="hidden" name="fpid" value="%s(p->zUuid)" /> |
| 1311 | + @ <input type="button" value='%s(isPinned ? "Unpin" : "Pin")' \ |
| 1312 | + @ class='submit hidden \ |
| 1313 | + @ %s(isPinned ? "action-unpin" : "action-pin")'/> |
| 1314 | + /* ^^^ activated by fossil.page.forumpost.js */ |
| 1315 | + @ </form> |
| 1316 | + } |
| 1234 | 1317 | } |
| 1235 | 1318 | } |
| 1236 | 1319 | @ </div> |
| 1237 | 1320 | } |
| 1321 | + if( !p->pIrt && (flags & FDISPLAY_SELECTED)){ |
| 1322 | + forum_render_status_selection(p); |
| 1323 | + } |
| 1238 | 1324 | @ </div> |
| 1239 | | - } |
| 1325 | + }/*!bRaw*/ |
| 1240 | 1326 | |
| 1241 | 1327 | /* Clean up. */ |
| 1242 | 1328 | manifest_destroy(pManifest); |
| 1243 | 1329 | } |
| 1244 | 1330 | |
| | @@ -1730,28 +1816,41 @@ |
| 1730 | 1816 | mimetype_option_menu(zMimetype, "mimetype"); |
| 1731 | 1817 | @ <div class="forum-editor-widget"> |
| 1732 | 1818 | @ <textarea aria-label="Content:" name="content" class="wikiedit" \ |
| 1733 | 1819 | @ cols="80" rows="25" wrap="virtual">%h(zContent)</textarea></div> |
| 1734 | 1820 | } |
| 1821 | + |
| 1822 | +/* |
| 1823 | +** If PD("fpid") refers to a forum post, its rid is returned, else |
| 1824 | +** this function emits an error does not does return. |
| 1825 | +*/ |
| 1826 | +static int forum_validate_fpid_param(void){ |
| 1827 | + const char *zFpid = PD("fpid",""); |
| 1828 | + int fpid = symbolic_name_to_rid(zFpid, "f"); |
| 1829 | + if( fpid<=0 ){ |
| 1830 | + webpage_error("Missing or invalid fpid parameter."); |
| 1831 | + } |
| 1832 | + return fpid; |
| 1833 | +} |
| 1735 | 1834 | |
| 1736 | 1835 | /* |
| 1737 | 1836 | ** Internal helper for /forumpost_XYZ internal pages which tag/untag |
| 1738 | 1837 | ** posts. |
| 1739 | 1838 | */ |
| 1740 | 1839 | static void forumpost_action_helper(const char *zTag, const char *zVal, |
| 1741 | | - int addTag){ |
| 1742 | | - const char *zFpid = PD("fpid",""); |
| 1743 | | - int fpid; |
| 1744 | | - |
| 1745 | | - cgi_csrf_verify(); |
| 1746 | | - fpid = symbolic_name_to_rid(zFpid, "f"); |
| 1747 | | - if( fpid<=0 ){ |
| 1748 | | - webpage_error("Missing or invalid fpid query parameter"); |
| 1749 | | - } |
| 1750 | | - forumpost_tag(fpid, zTag, addTag, zVal); |
| 1751 | | - cgi_redirectf("%R/forumpost/%S",zFpid); |
| 1752 | | - return; |
| 1840 | + int addTag, int validFpid){ |
| 1841 | + if( !cgi_csrf_safe(2) ){ |
| 1842 | + webpage_error("CSRF validation failed"); |
| 1843 | + }else{ |
| 1844 | + const int fpid = validFpid>0 ? validFpid : forum_validate_fpid_param(); |
| 1845 | + forumpost_tag(fpid, zTag, addTag, zVal); |
| 1846 | +#if 0 |
| 1847 | + @ DEBUG frid=%d(fpid) addTag=%d(addTag) %h(zTag)=%h(zVal ? zVal : "NULL") |
| 1848 | +#else |
| 1849 | + cgi_redirectf("%R/forumpost/%S",P("fpid")); |
| 1850 | +#endif |
| 1851 | + } |
| 1753 | 1852 | } |
| 1754 | 1853 | |
| 1755 | 1854 | /* |
| 1756 | 1855 | ** WEBPAGE: forumpost_close hidden |
| 1757 | 1856 | ** WEBPAGE: forumpost_reopen hidden |
| | @@ -1769,11 +1868,11 @@ |
| 1769 | 1868 | if( forumpost_may_close()==0 ){ |
| 1770 | 1869 | login_needed(g.anon.Admin); |
| 1771 | 1870 | }else{ |
| 1772 | 1871 | const int bIsAdd = sqlite3_strglob("*_close*", g.zPath)==0; |
| 1773 | 1872 | char const *zReason = bIsAdd ? 0 : PD("reason", 0); |
| 1774 | | - forumpost_action_helper("closed", zReason, bIsAdd); |
| 1873 | + forumpost_action_helper("closed", zReason, bIsAdd, 0); |
| 1775 | 1874 | } |
| 1776 | 1875 | } |
| 1777 | 1876 | |
| 1778 | 1877 | /* |
| 1779 | 1878 | ** WEBPAGE: forumpost_pin hidden |
| | @@ -1790,11 +1889,35 @@ |
| 1790 | 1889 | login_check_credentials(); |
| 1791 | 1890 | if( !g.perm.Setup ){ |
| 1792 | 1891 | login_needed(g.anon.Setup); |
| 1793 | 1892 | }else{ |
| 1794 | 1893 | const int bIsAdd = sqlite3_strglob("*_pin*", g.zPath)==0; |
| 1795 | | - forumpost_action_helper("pinned", 0, bIsAdd); |
| 1894 | + forumpost_action_helper("pinned", 0, bIsAdd, 0); |
| 1895 | + } |
| 1896 | +} |
| 1897 | + |
| 1898 | +/* |
| 1899 | +** WEBPAGE: forumpost_status hidden |
| 1900 | +** |
| 1901 | +** fpid=X Hash of the post to be edited. REQUIRED |
| 1902 | +** status=Y New status value. REQUIRED |
| 1903 | +** |
| 1904 | +** Updates the current status=Y tag on the first version of |
| 1905 | +** the forum post X. Requires forum_may_set_status() permissions. |
| 1906 | +*/ |
| 1907 | +void forum_page_status(void){ |
| 1908 | + int fpid; |
| 1909 | + login_check_credentials(); |
| 1910 | + fpid = forum_validate_fpid_param(); |
| 1911 | + if(forum_may_set_status(fpid)){ |
| 1912 | + const char *zStatus = PD("status",0); |
| 1913 | + if( !zStatus || !zStatus[0] ){ |
| 1914 | + webpage_error("Missing required status."); |
| 1915 | + } |
| 1916 | + forumpost_action_helper("status", zStatus, 1, fpid); |
| 1917 | + }else{ |
| 1918 | + webpage_error("You lack permissions to change this post's status."); |
| 1796 | 1919 | } |
| 1797 | 1920 | } |
| 1798 | 1921 | |
| 1799 | 1922 | /* |
| 1800 | 1923 | ** WEBPAGE: forumnew |
| | @@ -2466,11 +2589,11 @@ |
| 2466 | 2589 | char *zDuration = human_readable_age(db_column_double(&q,1)); |
| 2467 | 2590 | @ %d(nMsg) posts spanning %h(zDuration)\ |
| 2468 | 2591 | fossil_free(zDuration); |
| 2469 | 2592 | } |
| 2470 | 2593 | @ </td>\ |
| 2471 | | - if( bHasStatus ){ |
| 2594 | + if( zStatus ){ |
| 2472 | 2595 | @ <td>%h(zStatus)</td>\ |
| 2473 | 2596 | } |
| 2474 | 2597 | @</tr> |
| 2475 | 2598 | fossil_free(zAge); |
| 2476 | 2599 | } |
| 2477 | 2600 | |