| | @@ -61,12 +61,136 @@ |
| 61 | 61 | ForumPost *pDisplay; /* Entries in display order */ |
| 62 | 62 | ForumPost *pTail; /* Last on the display list */ |
| 63 | 63 | int mxIndent; /* Maximum indentation level */ |
| 64 | 64 | int nArtifact; /* Number of forum artifacts in this thread */ |
| 65 | 65 | }; |
| 66 | + |
| 67 | +/* |
| 68 | +** A single entry from the forum-statuses setting. |
| 69 | +*/ |
| 70 | +struct ForumStatus { |
| 71 | + char *zLabel; /* Label for the UI */ |
| 72 | + char *zValue; /* status=X tag value */ |
| 73 | + char *zDescr; /* Brief description */ |
| 74 | +}; |
| 75 | + |
| 76 | +/* |
| 77 | +** A list of ForumStatus objects. |
| 78 | +*/ |
| 79 | +struct ForumStatusList { |
| 80 | + struct ForumStatus *aStatus; /* List of statuses */ |
| 81 | + unsigned int n; /* Number of entries */ |
| 82 | +}; |
| 83 | + |
| 84 | +/* |
| 85 | +** Information passed into the status_match() SQL function |
| 86 | +** via the sqlite3_user_data() mechanism, and used by status_match() |
| 87 | +** to determine whether or not a particular forum thread should |
| 88 | +** be displayed. |
| 89 | +*/ |
| 90 | +struct ForumStatusMatch { |
| 91 | + const ForumStatusList *pFses; /* Parsed forum-statuses setting */ |
| 92 | + int eStatusTag; /* tagid for the "status" property */ |
| 93 | + unsigned int iMatch; /* Match this status value */ |
| 94 | +}; |
| 66 | 95 | #endif /* INTERFACE */ |
| 67 | 96 | |
| 97 | + |
| 98 | +/* |
| 99 | +** Returns a high-level representation of the forum-statuses setting. |
| 100 | +** This is a singleton, cached across calls. |
| 101 | + */ |
| 102 | +static const ForumStatusList * forum_statuses(void){ |
| 103 | + static ForumStatusList fses = {0,0}; |
| 104 | + static int once = 0; |
| 105 | + while( !once ){ |
| 106 | + ++once; |
| 107 | + /* Read `forum-statuses` setting and transform it into the |
| 108 | + ** fses object. |
| 109 | + ** |
| 110 | + ** Maybe: if it's empty, synthesize a length-1 list from |
| 111 | + ** {value:"default",label:"Default",...}. It's expected that |
| 112 | + ** usage may be slightly simplified if we always have a non-empty |
| 113 | + ** list. A length-1 list is, for purposes of the UI, identical to |
| 114 | + ** an empty one - status selection/filtering makes no sense if |
| 115 | + ** there's only one choice. */ |
| 116 | + db_multi_exec( |
| 117 | + "CREATE TEMP TABLE IF NOT EXISTS forumstatus(" |
| 118 | + " ord INTEGER PRIMARY KEY," |
| 119 | + " label, value, descr" |
| 120 | + ");" |
| 121 | + "DELETE FROM forumstatus;" |
| 122 | + "INSERT INTO forumstatus(label,value,descr)" |
| 123 | + " WITH setting(v) AS (" |
| 124 | + " SELECT value v FROM config WHERE name='forum-statuses'" |
| 125 | + " )," |
| 126 | + " room(r) AS (" |
| 127 | + " SELECT e.value FROM setting s, jsonb_each(s.v) e" |
| 128 | + " WHERE json_valid(s.v, 0x02)" |
| 129 | + " )" |
| 130 | + " SELECT r->>'label', r->>'value', r->>'description'" |
| 131 | + " FROM room;" |
| 132 | + ); |
| 133 | + fses.n = (unsigned)db_int(0, "SELECT count(*) FROM forumstatus"); |
| 134 | + if( fses.n ){ |
| 135 | + int i = 0; |
| 136 | + Stmt q; |
| 137 | + db_prepare(&q,"SELECT label, value, descr FROM forumstatus" |
| 138 | + " ORDER BY ord"); |
| 139 | + fses.aStatus = fossil_malloc(sizeof(fses.aStatus[0]) * fses.n); |
| 140 | + while( SQLITE_ROW==db_step(&q) ){ |
| 141 | + ForumStatus * fs = &fses.aStatus[i++]; |
| 142 | + fs->zLabel = fossil_strdup(db_column_text(&q, 0)); |
| 143 | + fs->zValue = fossil_strdup(db_column_text(&q, 1)); |
| 144 | + fs->zDescr = fossil_strdup(db_column_text(&q, 2)); |
| 145 | + } |
| 146 | + db_finalize(&q); |
| 147 | + } |
| 148 | + } |
| 149 | + return &fses; |
| 150 | +} |
| 151 | + |
| 152 | +/* |
| 153 | +** Search for a ForumStatus object by its tag value. If a match is |
| 154 | +** found, the corresponding object is returned. If no match is found |
| 155 | +** then (A) if bFirst is false then 0 is returned, else (B) the first |
| 156 | +** entry in the list is returned, noting that the list may be empty, |
| 157 | +** in which case 0 is returned. |
| 158 | +*/ |
| 159 | +const ForumStatus * forum_status_by_value(const char *z, int bFirst){ |
| 160 | + const ForumStatusList * const fses = forum_statuses(); |
| 161 | + const ForumStatus * fs0 = 0; |
| 162 | + unsigned int i; |
| 163 | + if( !fses->n ) return 0; |
| 164 | + for( i = 0; i < fses->n; ++i ){ |
| 165 | + const ForumStatus * fs = &fses->aStatus[i]; |
| 166 | + if( 0==fossil_strcmp(z, fs->zValue) ){ |
| 167 | + return fs; |
| 168 | + }else if( !fs0 ){ |
| 169 | + fs0 = fs; |
| 170 | + } |
| 171 | + } |
| 172 | + return bFirst ? fs0 : 0; |
| 173 | +} |
| 174 | + |
| 175 | +/* |
| 176 | +** COMMAND: test-forum-statuses |
| 177 | +*/ |
| 178 | +void test_forum_statuses_cmd(void){ |
| 179 | + const ForumStatusList * fses; |
| 180 | + unsigned i; |
| 181 | + db_find_and_open_repository(0,0); |
| 182 | + fses = forum_statuses(); |
| 183 | + for(i = 0; i < fses->n; ++i ){ |
| 184 | + const ForumStatus * fs = &fses->aStatus[i]; |
| 185 | + fossil_print("Status: %!j %!j %!j\n", |
| 186 | + fs->zValue, fs->zLabel, fs->zDescr); |
| 187 | + assert( fs==forum_status_by_value(fs->zValue, 0) ); |
| 188 | + } |
| 189 | + fossil_print("Total statuses: %u\n", i); |
| 190 | +} |
| 191 | + |
| 68 | 192 | /* |
| 69 | 193 | ** Return true if the forum post with the given rid has been |
| 70 | 194 | ** subsequently edited. |
| 71 | 195 | */ |
| 72 | 196 | int forum_rid_has_been_edited(int rid){ |
| | @@ -124,14 +248,24 @@ |
| 124 | 248 | ** matches the event.(euser,user) field for a formpost entry with the |
| 125 | 249 | ** matching RID. Returns false if no match is found. If zUserName is |
| 126 | 250 | ** 0 then login_name() is used. |
| 127 | 251 | */ |
| 128 | 252 | int forumpost_is_owner(int rid, const char *zUserName){ |
| 129 | | - return db_int(0, "SELECT 1 FROM event " |
| 130 | | - "WHERE type='f' AND objid=%d " |
| 131 | | - "AND coalesce(euser,user)=%Q", |
| 132 | | - rid, zUserName ? zUserName : login_name()); |
| 253 | + static Stmt q; |
| 254 | + int rc; |
| 255 | + if( !q.pStmt ){ |
| 256 | + db_static_prepare( |
| 257 | + &q, "SELECT 1 FROM event" |
| 258 | + " WHERE type='f' AND objid=$rid" |
| 259 | + " AND coalesce(euser,user)=$user" |
| 260 | + ); |
| 261 | + } |
| 262 | + db_bind_int(&q, "$rid", rid); |
| 263 | + db_bind_text(&q, "$user", zUserName ? zUserName : login_name()); |
| 264 | + rc = SQLITE_ROW==db_step(&q); |
| 265 | + db_reset(&q); |
| 266 | + return rc; |
| 133 | 267 | } |
| 134 | 268 | |
| 135 | 269 | /* |
| 136 | 270 | ** Returns true if p, or any parent of p, has a non-zero iClosed |
| 137 | 271 | ** value. Returns 0 if !p. For an edited chain of post, the tag is |
| | @@ -174,12 +308,12 @@ |
| 174 | 308 | ** - 0 if no matching tag is found. |
| 175 | 309 | ** |
| 176 | 310 | ** - The tagxref.rowid of the tagxref entry for the closure if rid is |
| 177 | 311 | ** the forum post to which the closure applies. |
| 178 | 312 | ** |
| 179 | | -** - (-tagxref.rowid) if the given rid inherits a "closed" tag from an |
| 180 | | -** IRT forum post. |
| 313 | +** - (-tagxref.rowid) if the given rid inherits the tag from an IRT |
| 314 | +** forum post. |
| 181 | 315 | */ |
| 182 | 316 | static int forum_rid_is_tagged(int rid, const char *zTagName, int bCheckIrt){ |
| 183 | 317 | static Stmt qIrt = empty_Stmt_m; |
| 184 | 318 | int rc = 0, i = 0; |
| 185 | 319 | /* TODO: this can probably be turned into a CTE by someone with |
| | @@ -266,25 +400,27 @@ |
| 266 | 400 | ** provide consistent behavior, it always acts on the first version of |
| 267 | 401 | ** the given forum post, walking the forumpost.fprev values to find |
| 268 | 402 | ** the head of the chain. |
| 269 | 403 | ** |
| 270 | 404 | ** If addTag is true then a propagating tag is added, except as noted |
| 271 | | -** below, with the given optional zReason string as the tag's |
| 405 | +** below, with the given optional zValue string as the tag's |
| 272 | 406 | ** value. If addTag is false then any matching active tag on frid is |
| 273 | | -** cancelled, except as noted below. zReason is ignored if it is NULL |
| 407 | +** cancelled, except as noted below. zValue is ignored if it is NULL |
| 274 | 408 | ** or starts with a NUL byte, or if addTag is false. |
| 275 | 409 | ** |
| 276 | 410 | ** This function only adds a tag if forum_rid_is_tagged() indicates |
| 277 | 411 | ** that frid's head is not tagged. If a parent post is already tagged, |
| 278 | 412 | ** no tag is added. Similarly, it will only remove a tag from a post |
| 279 | 413 | ** which has its own tag, and will not remove an inherited one from a |
| 280 | 414 | ** parent post. |
| 281 | 415 | ** |
| 282 | | -** If addTag is true and frid is already tagged (directly or |
| 283 | | -** inherited), this is a no-op. Likewise, if addTag is false and frid |
| 284 | | -** itself is not tagged (not accounting for an inherited closed tag), |
| 285 | | -** this is a no-op. |
| 416 | +** If addTag is true and frid is already tagged, this is a |
| 417 | +** no-op. Likewise, if addTag is false and frid is not tagged |
| 418 | +** (not accounting for an inherited closed tag), this is a no-op. |
| 419 | +** |
| 420 | +** If bCheckIrt is true then the forum post IRT hierarchy is searched |
| 421 | +** for the tag, otherwise only the given RID is checked. |
| 286 | 422 | ** |
| 287 | 423 | ** Returns true if it actually creates a new tag, else false. Fails |
| 288 | 424 | ** fatally on error. |
| 289 | 425 | ** |
| 290 | 426 | ** If it returns true then state from previously-loaded posts may be |
| | @@ -307,57 +443,62 @@ |
| 307 | 443 | ** - The applied tag is propagating so so that "closed" tags can |
| 308 | 444 | ** account for how edits of posts are handled. This differs from |
| 309 | 445 | ** closure of a branch, where a non-propagating tag is used. |
| 310 | 446 | */ |
| 311 | 447 | static int forumpost_tag(int frid, const char *zTagName, int addTag, |
| 312 | | - const char *zReason){ |
| 448 | + const char *zValue){ |
| 313 | 449 | Blob artifact = BLOB_INITIALIZER; /* Output artifact */ |
| 314 | 450 | Blob cksum = BLOB_INITIALIZER; /* Z-card */ |
| 315 | 451 | int iTagged; /* true if frid is already tagged */ |
| 316 | 452 | int trid; /* RID of new control artifact */ |
| 317 | 453 | char *zUuid; /* UUID of head version of post */ |
| 318 | 454 | |
| 319 | 455 | db_begin_transaction(); |
| 320 | 456 | frid = forumpost_head_rid(frid); |
| 321 | | - iTagged = forum_rid_is_tagged(frid, "closed", 1); |
| 322 | | - if( (iTagged && addTag |
| 323 | | - /* Already tagged, noting that in the case of (addTag<0) it may |
| 324 | | - ** actually be a parent which is tagged. */) |
| 325 | | - || (iTagged<=0 && !addTag |
| 326 | | - /* This entry is not tagged, but a parent post may be. */) ){ |
| 457 | + iTagged = forum_rid_is_tagged(frid, zTagName, 0); |
| 458 | + if( !addTag && !iTagged ){ |
| 459 | + /* Nothing to do. We never tag an IRT-inherited post via this |
| 460 | + ** function. */ |
| 327 | 461 | db_end_transaction(0); |
| 328 | 462 | return 0; |
| 329 | 463 | } |
| 330 | | - if( addTag==0 || (zReason && !zReason[0]) ){ |
| 331 | | - zReason = 0; |
| 464 | + if( !addTag || (zValue && !zValue[0]) ){ |
| 465 | + zValue = 0; |
| 466 | + } |
| 467 | + if( addTag && iTagged ){ |
| 468 | + char *zOld = 0; |
| 469 | + int cmp; |
| 470 | + rid_has_tag2(iTagged, zTagName, &zOld); |
| 471 | + cmp = fossil_strcmp(zOld, zValue); |
| 472 | + fossil_free(zOld); |
| 473 | + if( 0==cmp ){ |
| 474 | + /* Same value - leave it as is. */ |
| 475 | + db_end_transaction(0); |
| 476 | + return 0; |
| 477 | + } |
| 332 | 478 | } |
| 333 | 479 | zUuid = rid_to_uuid(frid); |
| 334 | 480 | blob_appendf(&artifact, "D %z\n", date_in_standard_format( "now" )); |
| 335 | 481 | blob_appendf(&artifact, "T %c%s %s%s%F\n", |
| 336 | 482 | addTag ? '*' : '-', zTagName, |
| 337 | | - zUuid, zReason ? " " : "", zReason ? zReason : ""); |
| 483 | + zUuid, zValue ? " " : "", zValue ? zValue : ""); |
| 338 | 484 | blob_appendf(&artifact, "U %F\n", login_name()); |
| 339 | 485 | md5sum_blob(&artifact, &cksum); |
| 340 | 486 | blob_appendf(&artifact, "Z %b\n", &cksum); |
| 341 | 487 | blob_reset(&cksum); |
| 342 | 488 | trid = content_put_ex(&artifact, 0, 0, 0, 0); |
| 343 | 489 | if( trid==0 ){ |
| 344 | 490 | fossil_fatal("Error saving tag artifact: %s", g.zErrMsg); |
| 345 | 491 | } |
| 346 | | - if( manifest_crosslink(trid, &artifact, |
| 347 | | - MC_NONE /*MC_PERMIT_HOOKS?*/)==0 ){ |
| 492 | + if( manifest_crosslink(trid, &artifact, MC_NONE)==0 ){ |
| 348 | 493 | fossil_fatal("%s", g.zErrMsg); |
| 349 | 494 | } |
| 350 | 495 | assert( blob_is_reset(&artifact) ); |
| 351 | 496 | db_add_unsent(trid); |
| 352 | 497 | admin_log("Tag forum post %S with %c%s", |
| 353 | 498 | zUuid, addTag ? '*' : '-', zTagName); |
| 354 | 499 | fossil_free(zUuid); |
| 355 | | - /* Potential TODO: if (iClosed>0) then we could find the initial tag |
| 356 | | - ** artifact and content_deltify(thatRid,&trid,1,0). Given the tiny |
| 357 | | - ** size of these artifacts, however, that would save little space, |
| 358 | | - ** if any. */ |
| 359 | 500 | db_end_transaction(0); |
| 360 | 501 | return 1; |
| 361 | 502 | } |
| 362 | 503 | |
| 363 | 504 | /* |
| | @@ -722,10 +863,71 @@ |
| 722 | 863 | } |
| 723 | 864 | @ </table> |
| 724 | 865 | db_finalize(&q); |
| 725 | 866 | style_finish_page(); |
| 726 | 867 | } |
| 868 | + |
| 869 | +/* |
| 870 | +** Returns true if the current user is authorized to set forum post |
| 871 | +** fpid's status. |
| 872 | +*/ |
| 873 | +static int forum_may_set_status(int fpid){ |
| 874 | + return g.perm.Admin |
| 875 | + || g.perm.ModForum |
| 876 | + || (login_is_individual() |
| 877 | + && forumpost_is_owner(fpid, 0)); |
| 878 | +} |
| 879 | + |
| 880 | +/* |
| 881 | +** If the current user is authorized to set fp's status then this |
| 882 | +** renders a mini-form for setting the status then redirecting back to |
| 883 | +** the post. Else it may emit a status label or no output. |
| 884 | +*/ |
| 885 | +static void forum_render_status_selection( const ForumPost *fp ){ |
| 886 | + const ForumStatusList * const fss = forum_statuses(); |
| 887 | + if( fss->n>1 ){ |
| 888 | + const ForumPost * pHead = fp->pEditHead ? fp->pEditHead : fp; |
| 889 | + int i; |
| 890 | + char * zCurrent = 0; |
| 891 | + const ForumStatus * sCurrent = 0; |
| 892 | + rid_has_tag2(pHead->fpid, "status", &zCurrent); |
| 893 | + for( i = 0; i < fss->n; ++i ){ |
| 894 | + const ForumStatus * const fs = &fss->aStatus[i]; |
| 895 | + if( 0==fossil_strcmp(zCurrent, fs->zValue) ){ |
| 896 | + sCurrent = fs; |
| 897 | + break; |
| 898 | + } |
| 899 | + } |
| 900 | + if( !sCurrent ) sCurrent = &fss->aStatus[0]; |
| 901 | + assert( sCurrent ); |
| 902 | + @ <span class='forum-status-selection'> |
| 903 | + if( forum_may_set_status(fp->fpid) ){ |
| 904 | + @ <form method="post" action='%R/forumpost_status'> |
| 905 | + login_insert_csrf_secret(); |
| 906 | + @ <input type='hidden' name='fpid' value='%s(fp->zUuid)' /> |
| 907 | + @ <select name='status' data-fpid='%s(fp->zUuid)'\ |
| 908 | + @ data-initial-value='%h(zCurrent ? zCurrent : "")'> |
| 909 | + for( i = 0; i < fss->n; ++i ){ |
| 910 | + const ForumStatus * const fs = &fss->aStatus[i]; |
| 911 | + @ <option value='%h(fs->zValue)'\ |
| 912 | + @ %s(sCurrent==fs ? " selected" : "")>\ |
| 913 | + @ %h(fs->zLabel)</option> |
| 914 | + } |
| 915 | + @ </select> |
| 916 | + @ <input type='button' class='submit action-status' disabled |
| 917 | + @ value='Change' /> |
| 918 | + /* ^^^ This must be <input>, not <button>, or else tapping it |
| 919 | + ** will unconditionally submit. */ |
| 920 | + @ </form> |
| 921 | + /* Form is activated in fossil.page.forumpost.js */ |
| 922 | + }else{ |
| 923 | + @ <button disabled>Status: %h(sCurrent->zLabel)</button> |
| 924 | + } |
| 925 | + @ </span> |
| 926 | + fossil_free(zCurrent); |
| 927 | + } |
| 928 | +} |
| 727 | 929 | |
| 728 | 930 | /* |
| 729 | 931 | ** Render a forum post for display |
| 730 | 932 | */ |
| 731 | 933 | void forum_render( |
| | @@ -863,21 +1065,24 @@ |
| 863 | 1065 | static void forum_render_attachment_list2(ForumPost *p){ |
| 864 | 1066 | if( p->pEditHead ) p = p->pEditHead; |
| 865 | 1067 | forum_render_attachment_list(p->zUuid); |
| 866 | 1068 | } |
| 867 | 1069 | |
| 1070 | +/* Flags for use with forum_display_post() */ |
| 1071 | +#define FDISPLAY_RAW 0x01 /* omit the border */ |
| 1072 | +#define FDISPLAY_UNFORMATTED 0x02 /* leave the post unformatted */ |
| 1073 | +#define FDISPLAY_HISTORY 0x04 /* Showing edit history */ |
| 1074 | +#define FDISPLAY_SELECTED 0x08 /* This is the selected post */ |
| 1075 | + |
| 868 | 1076 | /* |
| 869 | 1077 | ** Display a single post in a forum thread. |
| 870 | 1078 | */ |
| 871 | 1079 | static void forum_display_post( |
| 872 | 1080 | ForumThread *pThread, /* The thread that this post is a member of */ |
| 873 | 1081 | ForumPost *p, /* Forum post to display */ |
| 874 | 1082 | int iIndentScale, /* Indent scale factor */ |
| 875 | | - int bRaw, /* True to omit the border */ |
| 876 | | - int bUnf, /* True to leave the post unformatted */ |
| 877 | | - int bHist, /* True if showing edit history */ |
| 878 | | - int bSelect, /* True if this is the selected post */ |
| 1083 | + int flags, /* From the FDISPLAY_... enum */ |
| 879 | 1084 | char *zQuery /* Common query string */ |
| 880 | 1085 | ){ |
| 881 | 1086 | char *zPosterName; /* Name of user who originally made this post */ |
| 882 | 1087 | char *zEditorName; /* Name of user who provided the current edit */ |
| 883 | 1088 | char *zDate; /* The time/date string */ |
| | @@ -886,10 +1091,14 @@ |
| 886 | 1091 | Manifest *pManifest; /* Manifest comprising the current post */ |
| 887 | 1092 | int bPrivate; /* True for posts awaiting moderation */ |
| 888 | 1093 | int bSameUser; /* True if author is also the reader */ |
| 889 | 1094 | int iIndent; /* Indent level */ |
| 890 | 1095 | int iClosed; /* True if (sub)thread is closed */ |
| 1096 | + const int bRaw = flags & FDISPLAY_RAW; |
| 1097 | + const int bUnf = flags & FDISPLAY_UNFORMATTED; |
| 1098 | + const int bHist = flags & FDISPLAY_HISTORY; |
| 1099 | + const int bSelect = flags & FDISPLAY_SELECTED; |
| 891 | 1100 | const char *zMimetype;/* Formatting MIME type */ |
| 892 | 1101 | |
| 893 | 1102 | /* Get the manifest for the post. Abort if not found (e.g. shunned). */ |
| 894 | 1103 | pManifest = manifest_get(p->fpid, CFTYPE_FORUM, 0); |
| 895 | 1104 | if( !pManifest ) return; |
| | @@ -990,11 +1199,11 @@ |
| 990 | 1199 | /* Provide a link to the raw source code. */ |
| 991 | 1200 | if( !bUnf ){ |
| 992 | 1201 | @ %z(href("%R/forumpost/%!S?raw",p->zUuid))[source]</a> |
| 993 | 1202 | } |
| 994 | 1203 | @ </h3> |
| 995 | | - } |
| 1204 | + }/*!bRaw*/ |
| 996 | 1205 | |
| 997 | 1206 | /* Check if this post is approved, also if it's by the current user. */ |
| 998 | 1207 | bPrivate = content_is_private(p->fpid); |
| 999 | 1208 | bSameUser = login_is_individual() |
| 1000 | 1209 | && fossil_strcmp(pManifest->zUser, g.zLogin)==0; |
| | @@ -1058,14 +1267,16 @@ |
| 1058 | 1267 | const ForumPost *pHead = p->pEditHead ? p->pEditHead : p; |
| 1059 | 1268 | if( forumpost_may_close() && iClosed>=0 ){ |
| 1060 | 1269 | @ <form method="post" \ |
| 1061 | 1270 | @ action='%R/forumpost_%s(iClosed > 0 ? "reopen" : "close")'> |
| 1062 | 1271 | login_insert_csrf_secret(); |
| 1063 | | - @ <input type="hidden" name="fpid" value="%s(pHead->zUuid)" /> |
| 1272 | + @ <input type="hidden" name="fpid" value="%s(p->zUuid)" /> |
| 1064 | 1273 | if( moderation_pending(p->fpid)==0 ){ |
| 1065 | 1274 | @ <input type="button" value='%s(iClosed ? "Re-open" : "Close")' \ |
| 1066 | | - @ class='%s(iClosed ? "action-reopen" : "action-close")'/> |
| 1275 | + @ class='submit hidden \ |
| 1276 | + @ %s(iClosed ? "action-reopen" : "action-close")'/> |
| 1277 | + /* ^^^ activated by fossil.page.forumpost.js */ |
| 1067 | 1278 | } |
| 1068 | 1279 | @ </form> |
| 1069 | 1280 | } |
| 1070 | 1281 | if( g.perm.Admin || |
| 1071 | 1282 | (login_is_individual() |
| | @@ -1082,12 +1293,15 @@ |
| 1082 | 1293 | @ </form> |
| 1083 | 1294 | } |
| 1084 | 1295 | } |
| 1085 | 1296 | @ </div> |
| 1086 | 1297 | } |
| 1298 | + if( !p->pIrt && (flags & FDISPLAY_SELECTED)){ |
| 1299 | + forum_render_status_selection(p); |
| 1300 | + } |
| 1087 | 1301 | @ </div> |
| 1088 | | - } |
| 1302 | + }/*!bRaw*/ |
| 1089 | 1303 | |
| 1090 | 1304 | /* Clean up. */ |
| 1091 | 1305 | manifest_destroy(pManifest); |
| 1092 | 1306 | } |
| 1093 | 1307 | |
| | @@ -1197,12 +1411,18 @@ |
| 1197 | 1411 | } |
| 1198 | 1412 | |
| 1199 | 1413 | /* Display the appropriate subset of posts in sequence. */ |
| 1200 | 1414 | while( p ){ |
| 1201 | 1415 | /* Display the post. */ |
| 1202 | | - forum_display_post(pThread, p, iIndentScale, mode==FD_RAW, |
| 1203 | | - bUnf, bHist, p==pSelect, zQuery); |
| 1416 | + forum_display_post( |
| 1417 | + pThread, p, iIndentScale, |
| 1418 | + (mode==FD_RAW ? FDISPLAY_RAW : 0) | |
| 1419 | + (bUnf ? FDISPLAY_UNFORMATTED : 0) | |
| 1420 | + (bHist ? FDISPLAY_HISTORY : 0) | |
| 1421 | + (p==pSelect ? FDISPLAY_SELECTED : 0), |
| 1422 | + zQuery |
| 1423 | + ); |
| 1204 | 1424 | |
| 1205 | 1425 | /* Advance to the next post in the thread. */ |
| 1206 | 1426 | if( mode==FD_CHRONO ){ |
| 1207 | 1427 | /* Chronological mode: display posts (optionally including edits) in their |
| 1208 | 1428 | ** original commit order. */ |
| | @@ -1572,44 +1792,82 @@ |
| 1572 | 1792 | mimetype_option_menu(zMimetype, "mimetype"); |
| 1573 | 1793 | @ <div class="forum-editor-widget"> |
| 1574 | 1794 | @ <textarea aria-label="Content:" name="content" class="wikiedit" \ |
| 1575 | 1795 | @ cols="80" rows="25" wrap="virtual">%h(zContent)</textarea></div> |
| 1576 | 1796 | } |
| 1797 | + |
| 1798 | +/* |
| 1799 | +** If PD("fpid") refers to a forum post, its rid is returned, else |
| 1800 | +** this function emits an error does not does return. |
| 1801 | +*/ |
| 1802 | +static int forum_validate_fpid_param(void){ |
| 1803 | + const char *zFpid = PD("fpid",""); |
| 1804 | + int fpid = symbolic_name_to_rid(zFpid, "f"); |
| 1805 | + if( fpid<=0 ){ |
| 1806 | + webpage_error("Missing or invalid fpid parameter."); |
| 1807 | + } |
| 1808 | + return fpid; |
| 1809 | +} |
| 1810 | + |
| 1811 | +/* |
| 1812 | +** Internal helper for /forumpost_XYZ internal pages which tag/untag |
| 1813 | +** posts. |
| 1814 | +*/ |
| 1815 | +static void forumpost_action_helper(const char *zTag, const char *zVal, |
| 1816 | + int addTag, int validFpid){ |
| 1817 | + if( !cgi_csrf_safe(2) ){ |
| 1818 | + webpage_error("CSRF validation failed"); |
| 1819 | + }else{ |
| 1820 | + const int fpid = validFpid>0 ? validFpid : forum_validate_fpid_param(); |
| 1821 | + forumpost_tag(fpid, zTag, addTag, zVal); |
| 1822 | + cgi_redirectf("%R/forumpost/%S",P("fpid")); |
| 1823 | + } |
| 1824 | +} |
| 1577 | 1825 | |
| 1578 | 1826 | /* |
| 1579 | 1827 | ** WEBPAGE: forumpost_close hidden |
| 1580 | 1828 | ** WEBPAGE: forumpost_reopen hidden |
| 1581 | 1829 | ** |
| 1582 | 1830 | ** fpid=X Hash of the post to be edited. REQUIRED |
| 1583 | | -** reason=X Optional reason for closure. |
| 1584 | 1831 | ** |
| 1585 | 1832 | ** Closes or re-opens the given forum post, within the bounds of the |
| 1586 | 1833 | ** API for forumpost_tag(). After (perhaps) modifying the "closed" |
| 1587 | 1834 | ** status of the given thread, it redirects to that post's thread |
| 1588 | 1835 | ** view. Requires admin privileges. |
| 1589 | 1836 | */ |
| 1590 | 1837 | void forum_page_close(void){ |
| 1591 | | - const char *zFpid = PD("fpid",""); |
| 1592 | | - const char *zReason = 0; |
| 1593 | | - int fClose; |
| 1594 | | - int fpid; |
| 1595 | | - |
| 1596 | 1838 | login_check_credentials(); |
| 1597 | 1839 | if( forumpost_may_close()==0 ){ |
| 1598 | 1840 | login_needed(g.anon.Admin); |
| 1599 | | - return; |
| 1600 | | - } |
| 1601 | | - cgi_csrf_verify(); |
| 1602 | | - fpid = symbolic_name_to_rid(zFpid, "f"); |
| 1603 | | - if( fpid<=0 ){ |
| 1604 | | - webpage_error("Missing or invalid fpid query parameter"); |
| 1605 | | - } |
| 1606 | | - fClose = sqlite3_strglob("*_close*", g.zPath)==0; |
| 1607 | | - if( fClose ) zReason = PD("reason",0); |
| 1608 | | - forumpost_tag(fpid, "closed", fClose, zReason); |
| 1609 | | - cgi_redirectf("%R/forumpost/%S",zFpid); |
| 1610 | | - return; |
| 1841 | + }else{ |
| 1842 | + const int bIsAdd = sqlite3_strglob("*_close*", g.zPath)==0; |
| 1843 | + forumpost_action_helper("closed", 0, bIsAdd, 0); |
| 1844 | + } |
| 1845 | +} |
| 1846 | + |
| 1847 | +/* |
| 1848 | +** WEBPAGE: forumpost_status hidden |
| 1849 | +** |
| 1850 | +** fpid=X Hash of the post to be edited. REQUIRED |
| 1851 | +** status=Y New status value. REQUIRED |
| 1852 | +** |
| 1853 | +** Updates the current status=Y tag on the first version of |
| 1854 | +** the forum post X. Requires forum_may_set_status() permissions. |
| 1855 | +*/ |
| 1856 | +void forum_page_status(void){ |
| 1857 | + int fpid; |
| 1858 | + login_check_credentials(); |
| 1859 | + fpid = forum_validate_fpid_param(); |
| 1860 | + if(forum_may_set_status(fpid)){ |
| 1861 | + const char *zStatus = PD("status",0); |
| 1862 | + if( !zStatus || !zStatus[0] ){ |
| 1863 | + webpage_error("Missing required status."); |
| 1864 | + } |
| 1865 | + forumpost_action_helper("status", zStatus, 1, fpid); |
| 1866 | + }else{ |
| 1867 | + webpage_error("You lack permissions to change this post's status."); |
| 1868 | + } |
| 1611 | 1869 | } |
| 1612 | 1870 | |
| 1613 | 1871 | /* |
| 1614 | 1872 | ** WEBPAGE: forumnew |
| 1615 | 1873 | ** WEBPAGE: forumedit |
| | @@ -1788,11 +2046,13 @@ |
| 1788 | 2046 | fpid = symbolic_name_to_rid(zFpid, "f"); |
| 1789 | 2047 | if( fpid<=0 || (pPost = manifest_get(fpid, CFTYPE_FORUM, 0))==0 ){ |
| 1790 | 2048 | webpage_error("Missing or invalid fpid query parameter"); |
| 1791 | 2049 | } |
| 1792 | 2050 | froot = db_int(0, "SELECT froot FROM forumpost WHERE fpid=%d", fpid); |
| 1793 | | - if( froot==0 || (pRootPost = manifest_get(froot, CFTYPE_FORUM, 0))==0 ){ |
| 2051 | + if( (froot==0 || (pRootPost = manifest_get(froot, CFTYPE_FORUM, 0))==0) |
| 2052 | + && P("reject")==0 |
| 2053 | + ){ |
| 1794 | 2054 | webpage_error("fpid does not appear to be a forum post: \"%d\"", fpid); |
| 1795 | 2055 | } |
| 1796 | 2056 | if( P("cancel") ){ |
| 1797 | 2057 | cgi_redirectf("%R/forumpost/%S",zFpid); |
| 1798 | 2058 | return; |
| | @@ -1968,10 +2228,21 @@ |
| 1968 | 2228 | ** seems more appropriate for the particular usage. |
| 1969 | 2229 | ** |
| 1970 | 2230 | ** SETTING: attachment-size-limit width=16 |
| 1971 | 2231 | ** The maximum number of bytes for an attachment. The default (or 0) is |
| 1972 | 2232 | ** unlimited but a limit may be imposed by the web server or a proxy. |
| 2233 | +** |
| 2234 | +** SETTING: forum-statuses width=40 block-text |
| 2235 | +** This JSON5-formatted value defines an array of objects describing |
| 2236 | +** the available statuses of forum posts. Each entry of the array must |
| 2237 | +** be an object in the form {label:"X",value:"Y",description:"Z"}. |
| 2238 | +** The label is used in the UI and value becomes the value of the |
| 2239 | +** "status" tag on forum posts. Any forum post which has a status |
| 2240 | +** value which does not appear in this list is treated as if it had |
| 2241 | +** the first value from this list. If this setting is empty, is |
| 2242 | +** ill-formed JSON, or has only a single entry then the forum will |
| 2243 | +** lack the capability of setting and filtering by status. |
| 1973 | 2244 | */ |
| 1974 | 2245 | |
| 1975 | 2246 | /* |
| 1976 | 2247 | ** WEBPAGE: setup_forum |
| 1977 | 2248 | ** |
| | @@ -2098,10 +2369,104 @@ |
| 2098 | 2369 | @ </form> |
| 2099 | 2370 | } |
| 2100 | 2371 | |
| 2101 | 2372 | style_finish_page(); |
| 2102 | 2373 | } |
| 2374 | + |
| 2375 | +/* |
| 2376 | +** If the forum-statuses setting is active and has 2 or more entries, |
| 2377 | +** this adds a submenu for selecting the status filter, else it emits |
| 2378 | +** nothing. |
| 2379 | +*/ |
| 2380 | +static void forum_status_submenu(void){ |
| 2381 | + const ForumStatusList * const fss = forum_statuses(); |
| 2382 | + static int i = 0; |
| 2383 | + static const char **az; |
| 2384 | + if( i==0 && fss->n>1 ){ |
| 2385 | + unsigned j; |
| 2386 | + az = fossil_malloc(sizeof(az[0]) * ((1 + fss->n) * 2)); |
| 2387 | + az[i++] = "*"; |
| 2388 | + az[i++] = "Any status"; |
| 2389 | + for( j = 0; j < fss->n; ++j ){ |
| 2390 | + const ForumStatus * fs = &fss->aStatus[j]; |
| 2391 | + /* Potential TODO: skip any entries for which there are no |
| 2392 | + ** forum posts with a status=${fs->zValue} tag. */ |
| 2393 | + az[i++] = fs->zValue; |
| 2394 | + az[i++] = fs->zLabel; |
| 2395 | + } |
| 2396 | + //assert( i==(1+fss->n)*2 ); |
| 2397 | + } |
| 2398 | + if( i ){ |
| 2399 | + cookie_link_parameter("status","forumStatus","*"); |
| 2400 | + style_submenu_multichoice("status", i/2, az, 0); |
| 2401 | + } |
| 2402 | +} |
| 2403 | + |
| 2404 | +/* |
| 2405 | +** Transient SQL Function: status_match(FROOT) |
| 2406 | +** |
| 2407 | +** Return true if the forum thread identified by FROOT should be included |
| 2408 | +** in a list of threads. Used to implement the status=NAME query parameter |
| 2409 | +** on /forum. |
| 2410 | +** |
| 2411 | +** The result of this routine depends on the content of the |
| 2412 | +** ForumStatusMatch *pMData object that is available via sqlite3_user_data(). |
| 2413 | +** |
| 2414 | +** * If pMData==NULL, always return true. This means that no |
| 2415 | +** filtering of threads is being done. This is the common case. |
| 2416 | +** |
| 2417 | +** * If FROOT contains a status property value that matches |
| 2418 | +** pMData->iMatch, return true. |
| 2419 | +** |
| 2420 | +** * if pMData->iMatch==0 (meaning we want to match the default |
| 2421 | +** status value) and if the FROOT thread contains a status that |
| 2422 | +** is not on the list of statuses or if FROOT has no statue |
| 2423 | +** property at all, then return true. In other words, a forum |
| 2424 | +** thread with no status property or an unknown status property |
| 2425 | +** is treated as if it had the default status. |
| 2426 | +** |
| 2427 | +** * Otherwise, return false. |
| 2428 | +*/ |
| 2429 | +static void forum_status_match( |
| 2430 | + sqlite3_context *context, |
| 2431 | + int argc, |
| 2432 | + sqlite3_value **argv |
| 2433 | +){ |
| 2434 | + static Stmt q; |
| 2435 | + ForumStatusMatch *pMData = sqlite3_user_data(context); |
| 2436 | + int i; |
| 2437 | + |
| 2438 | + if( pMData==0 ){ |
| 2439 | + sqlite3_result_int(context, 1); |
| 2440 | + return; |
| 2441 | + } |
| 2442 | + db_static_prepare(&q, |
| 2443 | + "SELECT value FROM tagxref\n" |
| 2444 | + " WHERE tagid=%d\n" |
| 2445 | + " AND tagtype>=1\n" |
| 2446 | + " AND rid=:rid\n" |
| 2447 | + " ORDER BY mtime DESC LIMIT 1", |
| 2448 | + pMData->eStatusTag |
| 2449 | + ); |
| 2450 | + db_bind_int(&q, ":rid", sqlite3_value_int(argv[0])); |
| 2451 | + if( db_step(&q)==SQLITE_ROW ){ |
| 2452 | + const char *zValue = (const char*)db_column_text(&q,0); |
| 2453 | + const ForumStatusList *pFses = pMData->pFses; |
| 2454 | + if( zValue==0 ){ |
| 2455 | + i = 0; |
| 2456 | + }else{ |
| 2457 | + for(i=0; i<pFses->n; i++){ |
| 2458 | + if( fossil_strcmp(pFses->aStatus[i].zValue,zValue)==0 ) break; |
| 2459 | + } |
| 2460 | + } |
| 2461 | + if( i>=pMData->pFses->n ) i = 0; |
| 2462 | + }else{ |
| 2463 | + i = 0; |
| 2464 | + } |
| 2465 | + db_reset(&q); |
| 2466 | + sqlite3_result_int(context, i==pMData->iMatch); |
| 2467 | +} |
| 2103 | 2468 | |
| 2104 | 2469 | /* |
| 2105 | 2470 | ** WEBPAGE: forummain |
| 2106 | 2471 | ** WEBPAGE: forum |
| 2107 | 2472 | ** |
| | @@ -2118,30 +2483,35 @@ |
| 2118 | 2483 | void forum_main_page(void){ |
| 2119 | 2484 | Stmt q; |
| 2120 | 2485 | int iLimit = 0, iOfst, iCnt; |
| 2121 | 2486 | int srchFlags; |
| 2122 | 2487 | const int isSearch = P("s")!=0; |
| 2123 | | - char const *zLimit = 0; |
| 2488 | + const char *zStatusFilter; |
| 2489 | + char const *zLimit = 0; /* Value of the n= query parameter */ |
| 2490 | + int eStatusTag = 0; /* tagid for the "status" property */ |
| 2491 | + int bHasStatus = 0; /* True if forum-statuses setting exists */ |
| 2492 | + int bFilter = 0; /* True if status=NAME query parameter */ |
| 2493 | + ForumStatusMatch sFSM; /* Aux data to status_match() SQL function */ |
| 2124 | 2494 | |
| 2125 | 2495 | login_check_credentials(); |
| 2126 | 2496 | srchFlags = search_restrict(SRCH_FORUM); |
| 2127 | 2497 | if( !g.perm.RdForum ){ |
| 2128 | 2498 | login_needed(g.anon.RdForum); |
| 2129 | 2499 | return; |
| 2130 | 2500 | } |
| 2131 | 2501 | cgi_check_for_malice(); |
| 2502 | + eStatusTag = db_int(0, "SELECT tagid FROM tag WHERE tagname='status'"); |
| 2503 | + if( eStatusTag && forum_statuses()->n>1 ){ |
| 2504 | + bHasStatus = 1; |
| 2505 | + } |
| 2132 | 2506 | style_set_current_feature("forum"); |
| 2133 | 2507 | style_header("%s%s", db_get("forum-title","Forum"), |
| 2134 | 2508 | isSearch ? " Search Results" : ""); |
| 2135 | 2509 | style_submenu_element("Timeline", "%R/timeline?ss=v&y=f&vfx"); |
| 2136 | 2510 | if( g.perm.WrForum ){ |
| 2137 | 2511 | style_submenu_element("New Thread","%R/forumnew"); |
| 2138 | 2512 | }else{ |
| 2139 | | - /* Can't combine this with previous case using the ternary operator |
| 2140 | | - * because that causes an error yelling about "non-constant format" |
| 2141 | | - * with some compilers. I can't see it, since both expressions have |
| 2142 | | - * the same format, but I'm no C spec lawyer. */ |
| 2143 | 2513 | style_submenu_element("New Thread","%R/login"); |
| 2144 | 2514 | } |
| 2145 | 2515 | if( g.perm.ModForum && moderation_needed() ){ |
| 2146 | 2516 | style_submenu_element("Moderation Requests", "%R/modreq"); |
| 2147 | 2517 | } |
| | @@ -2164,95 +2534,195 @@ |
| 2164 | 2534 | cgi_replace_query_parameter("n", fossil_strdup("25")) |
| 2165 | 2535 | /*for the sake of Max, below*/; |
| 2166 | 2536 | iLimit = 25; |
| 2167 | 2537 | } |
| 2168 | 2538 | style_submenu_entry("n","Max:",4,0); |
| 2539 | + forum_status_submenu(); |
| 2540 | + zStatusFilter = P("status") /*must be after forum_status_submenu()!*/; |
| 2169 | 2541 | iOfst = atoi(PD("x","0")); |
| 2170 | 2542 | iCnt = 0; |
| 2543 | + if( zStatusFilter ){ |
| 2544 | + if( zStatusFilter[0]==0 || 0==fossil_strcmp("*",zStatusFilter) ){ |
| 2545 | + zStatusFilter = 0; |
| 2546 | + }else{ |
| 2547 | + bFilter = bHasStatus; |
| 2548 | + } |
| 2549 | + } |
| 2171 | 2550 | if( db_table_exists("repository","forumpost") ){ |
| 2551 | + const ForumStatusList *pFstat = forum_statuses(); |
| 2552 | + Stmt qStat = empty_Stmt; /* Query to get status information */ |
| 2553 | + if( bHasStatus ){ |
| 2554 | + /* The qStat query runs once for each output row generate by the |
| 2555 | + ** q query. It determines the value and label of the status for |
| 2556 | + ** the row with froot=:rowid |
| 2557 | + */ |
| 2558 | + db_prepare(&qStat, |
| 2559 | + "SELECT tagxref.value, forumstatus.label\n" |
| 2560 | + " FROM forumstatus, tagxref\n" |
| 2561 | + " WHERE tagid=%d AND tagtype>=1\n" |
| 2562 | + " AND forumstatus.value=tagxref.value\n" |
| 2563 | + " AND rid=:rid\n" |
| 2564 | + " ORDER BY mtime DESC", |
| 2565 | + eStatusTag |
| 2566 | + ); |
| 2567 | + } |
| 2568 | + |
| 2569 | + /* Create the status_match() SQL function that will determine |
| 2570 | + ** whether or not each thread in the "q" query below is eligible |
| 2571 | + ** for display |
| 2572 | + */ |
| 2573 | + if( bFilter ){ |
| 2574 | + sFSM.pFses = pFstat; |
| 2575 | + sFSM.eStatusTag = eStatusTag; |
| 2576 | + for(sFSM.iMatch=0; sFSM.iMatch<pFstat->n; sFSM.iMatch++){ |
| 2577 | + if( 0==fossil_strcmp(zStatusFilter, |
| 2578 | + pFstat->aStatus[sFSM.iMatch].zValue) ){ |
| 2579 | + break; |
| 2580 | + } |
| 2581 | + } |
| 2582 | + sqlite3_create_function(g.db,"status_match",1,SQLITE_UTF8,(void*)&sFSM, |
| 2583 | + forum_status_match, 0, 0); |
| 2584 | + }else{ |
| 2585 | + sqlite3_create_function(g.db,"status_match",1,SQLITE_UTF8,0, |
| 2586 | + forum_status_match, 0, 0); |
| 2587 | + } |
| 2588 | + |
| 2172 | 2589 | db_prepare(&q, |
| 2173 | | - "WITH thread(age,duration,cnt,root,last) AS (" |
| 2174 | | - " SELECT" |
| 2175 | | - " julianday('now') - max(fmtime)," |
| 2176 | | - " max(fmtime) - min(fmtime)," |
| 2177 | | - " sum(fprev IS NULL)," |
| 2178 | | - " froot," |
| 2179 | | - " (SELECT fpid FROM forumpost AS y" |
| 2180 | | - " WHERE y.froot=x.froot %s" |
| 2181 | | - " ORDER BY y.fmtime DESC LIMIT 1)" |
| 2182 | | - " FROM forumpost AS x" |
| 2183 | | - " WHERE %s" |
| 2184 | | - " GROUP BY froot" |
| 2185 | | - " ORDER BY 1 LIMIT %d OFFSET %d" |
| 2186 | | - ")" |
| 2187 | | - "SELECT" |
| 2188 | | - " thread.age," /* 0 */ |
| 2189 | | - " thread.duration," /* 1 */ |
| 2190 | | - " thread.cnt," /* 2 */ |
| 2191 | | - " blob.uuid," /* 3 */ |
| 2192 | | - " substr(event.comment,instr(event.comment,':')+1)," /* 4 */ |
| 2193 | | - " thread.last" /* 5 */ |
| 2194 | | - " FROM thread, blob, event" |
| 2195 | | - " WHERE blob.rid=thread.last" |
| 2196 | | - " AND event.objid=thread.last" |
| 2590 | + "WITH thread(root,endtime,lastrid) AS (\n" |
| 2591 | + " SELECT\n" |
| 2592 | + " froot,\n" |
| 2593 | + " max(fmtime),\n" |
| 2594 | + " fpid\n" |
| 2595 | + " FROM forumpost\n" |
| 2596 | + " WHERE %s/*ModForum*/\n" |
| 2597 | + " GROUP BY froot\n" |
| 2598 | + " HAVING status_match(froot)\n" |
| 2599 | + " ORDER BY 2 DESC\n" |
| 2600 | + " LIMIT %d OFFSET %d\n" |
| 2601 | + ")\n" |
| 2602 | + "SELECT\n" |
| 2603 | + " julianday('now') - thread.endtime,\n" /* 0 */ |
| 2604 | + " thread.endtime - " |
| 2605 | + "(SELECT fmtime FROM forumpost WHERE fpid=root),\n" /* 1 */ |
| 2606 | + " (SELECT sum(fprev IS NULL) FROM forumpost" |
| 2607 | + " WHERE froot=root),\n" /* 2 */ |
| 2608 | + " blob.uuid,\n" /* 3 */ |
| 2609 | + " substr(event.comment,instr(event.comment,':')+1),\n" /* 4 */ |
| 2610 | + " thread.lastrid,\n" /* 5 */ |
| 2611 | + " thread.root\n" /* 6 */ |
| 2612 | + " FROM thread, blob, event\n" |
| 2613 | + " WHERE blob.rid=thread.lastrid\n" |
| 2614 | + " AND event.objid=thread.lastrid\n" |
| 2197 | 2615 | " ORDER BY 1;", |
| 2198 | | - g.perm.ModForum ? "" : "AND y.fpid NOT IN private" /*safe-for-%s*/, |
| 2199 | 2616 | g.perm.ModForum ? "true" : "fpid NOT IN private" /*safe-for-%s*/, |
| 2200 | 2617 | iLimit+1, iOfst |
| 2201 | 2618 | ); |
| 2202 | 2619 | while( db_step(&q)==SQLITE_ROW ){ |
| 2203 | | - char *zAge = human_readable_age(db_column_double(&q,0)); |
| 2204 | | - int nMsg = db_column_int(&q, 2); |
| 2205 | | - const char *zUuid = db_column_text(&q, 3); |
| 2206 | | - const char *zTitle = db_column_text(&q, 4); |
| 2620 | + char *zAge; |
| 2621 | + int nMsg; |
| 2622 | + const char *zUuid; |
| 2623 | + const char *zTitle; |
| 2624 | + const char *zStatus; |
| 2625 | + const char *zStatusLbl; |
| 2626 | + const int bShowStatus = bHasStatus && !zStatusFilter; |
| 2627 | + const int nCols = bShowStatus ? 4 : 3; |
| 2628 | + |
| 2629 | + if( qStat.pStmt ){ |
| 2630 | + /* Determine the status value for this row */ |
| 2631 | + db_reset(&qStat); |
| 2632 | + db_bind_int(&qStat, ":rid", db_column_int(&q,6)); |
| 2633 | + if( db_step(&qStat)==SQLITE_ROW ){ |
| 2634 | + zStatus = db_column_text(&qStat, 0); |
| 2635 | + zStatusLbl = db_column_text(&qStat, 1); |
| 2636 | + }else{ |
| 2637 | + zStatus = pFstat->aStatus[0].zValue; |
| 2638 | + zStatusLbl = pFstat->aStatus[0].zLabel; |
| 2639 | + } |
| 2640 | + }else{ |
| 2641 | + zStatus = zStatusLbl = NULL; |
| 2642 | + } |
| 2643 | + zAge = human_readable_age(db_column_double(&q,0)); |
| 2644 | + nMsg = db_column_int(&q, 2); |
| 2645 | + zUuid = db_column_text(&q, 3); |
| 2646 | + zTitle = db_column_text(&q, 4); |
| 2207 | 2647 | if( iCnt==0 ){ |
| 2648 | + char *zTail = bFilter ? mprintf(" with status=%Q", zStatusFilter): 0; |
| 2208 | 2649 | if( iOfst>0 ){ |
| 2209 | | - @ <h1>Threads at least %s(zAge) old</h1> |
| 2650 | + @ <h1>Threads at least %s(zAge) old%h(zTail ? zTail : "")</h1> |
| 2210 | 2651 | }else{ |
| 2211 | | - @ <h1>Most recent threads</h1> |
| 2652 | + @ <h1>Most recent threads%h(zTail ? zTail : "")</h1> |
| 2212 | 2653 | } |
| 2654 | + fossil_free(zTail); |
| 2213 | 2655 | @ <div class='forumPosts fileage'><table width="100%%"> |
| 2214 | 2656 | if( iOfst>0 ){ |
| 2215 | 2657 | if( iOfst>iLimit ){ |
| 2216 | | - @ <tr><td colspan="3">\ |
| 2217 | | - @ %z(href("%R/forum?x=%d&n=%d",iOfst-iLimit,iLimit))\ |
| 2218 | | - @ ↑ Newer...</a></td></tr> |
| 2658 | + @ <tr><td colspan="%d(nCols)">\ |
| 2659 | + @ <a href='%R/forum?x=%d(iOfst-iLimit)&n=%d(iLimit) \ |
| 2660 | + if( bFilter ){ |
| 2661 | + @ &status=%T(zStatusFilter)\ |
| 2662 | + } |
| 2663 | + @ '>↑ Newer...</a></td></tr> |
| 2219 | 2664 | }else{ |
| 2220 | | - @ <tr><td colspan="3">%z(href("%R/forum?n=%d",iLimit))\ |
| 2221 | | - @ ↑ Newer...</a></td></tr> |
| 2665 | + @ <tr><td colspan="%d(nCols)">\ |
| 2666 | + @ <a href='%R/forum?n=%d(iLimit)\ |
| 2667 | + if( bFilter ){ |
| 2668 | + @ &status=%T(zStatusFilter) \ |
| 2669 | + } |
| 2670 | + @ '>↑ Newer...</a></td></tr> |
| 2222 | 2671 | } |
| 2223 | 2672 | } |
| 2224 | 2673 | } |
| 2225 | 2674 | iCnt++; |
| 2226 | 2675 | if( iCnt>iLimit ){ |
| 2227 | | - @ <tr><td colspan="3">\ |
| 2228 | | - @ %z(href("%R/forum?x=%d&n=%d",iOfst+iLimit,iLimit))\ |
| 2229 | | - @ ↓ Older...</a></td></tr> |
| 2676 | + @ <tr><td colspan="%d(nCols)">\ |
| 2677 | + @ <a href='%R/forum?x=%d(iOfst+iLimit)&n=%d(iLimit) \ |
| 2678 | + if( bFilter ){ |
| 2679 | + @ &status=%T(zStatusFilter)\ |
| 2680 | + } |
| 2681 | + @ '>↓ Older...</a></td></tr> |
| 2230 | 2682 | fossil_free(zAge); |
| 2231 | 2683 | break; |
| 2232 | 2684 | } |
| 2233 | | - @ <tr><td>%h(zAge) ago</td> |
| 2234 | | - @ <td>%z(href("%R/forumpost/%S",zUuid))%h(zTitle)</a></td> |
| 2235 | | - @ <td>\ |
| 2685 | + @ <tr \ |
| 2686 | + if( bHasStatus ){ |
| 2687 | + @ data-status="%h(zStatus)"\ |
| 2688 | + } |
| 2689 | + @ ><td>%h(zAge) ago</td> |
| 2690 | + @ <td class='subject'>%z(href("%R/forumpost/%S",zUuid))%h(zTitle)</a>\ |
| 2691 | + @ </td><td>\ |
| 2236 | 2692 | if( g.perm.ModForum && moderation_pending(db_column_int(&q,5)) ){ |
| 2237 | 2693 | @ <span class="modpending">\ |
| 2238 | 2694 | @ Awaiting Moderator Approval</span><br> |
| 2239 | 2695 | } |
| 2240 | 2696 | if( nMsg<2 ){ |
| 2241 | | - @ no replies</td> |
| 2697 | + @ no replies\ |
| 2242 | 2698 | }else{ |
| 2243 | 2699 | char *zDuration = human_readable_age(db_column_double(&q,1)); |
| 2244 | | - @ %d(nMsg) posts spanning %h(zDuration)</td> |
| 2700 | + @ %d(nMsg) posts spanning %h(zDuration)\ |
| 2245 | 2701 | fossil_free(zDuration); |
| 2246 | 2702 | } |
| 2247 | | - @ </tr> |
| 2703 | + @ </td>\ |
| 2704 | + if( bShowStatus ){ |
| 2705 | + @ <td class='status'>%h(zStatusLbl)</td>\ |
| 2706 | + } |
| 2707 | + if( qStat.pStmt ){ |
| 2708 | + db_reset(&qStat); |
| 2709 | + } |
| 2710 | + @</tr> |
| 2248 | 2711 | fossil_free(zAge); |
| 2249 | 2712 | } |
| 2250 | 2713 | db_finalize(&q); |
| 2714 | + if( qStat.pStmt ) db_finalize(&qStat); |
| 2715 | + sqlite3_create_function(g.db,"status_match",1,SQLITE_UTF8,0,0,0,0); |
| 2251 | 2716 | } |
| 2252 | 2717 | if( iCnt>0 ){ |
| 2253 | 2718 | @ </table></div> |
| 2254 | 2719 | }else{ |
| 2255 | 2720 | @ <h1>No forum posts found</h1> |
| 2256 | 2721 | } |
| 2722 | + if( bHasStatus ){ |
| 2723 | + /* We need a JS-side kludge to avoid passing on the x=N |
| 2724 | + ** URL arg when the status selection list is activated. */ |
| 2725 | + forum_emit_js(); |
| 2726 | + } |
| 2257 | 2727 | style_finish_page(); |
| 2258 | 2728 | } |
| 2259 | 2729 | |