| | @@ -26,44 +26,45 @@ |
| 26 | 26 | */ |
| 27 | 27 | #define DEFAULT_FORUM_MIMETYPE "text/x-markdown" |
| 28 | 28 | |
| 29 | 29 | #if INTERFACE |
| 30 | 30 | /* |
| 31 | | -** Each instance of the following object represents a single message - |
| 31 | +** Each instance of the following object represents a single message - |
| 32 | 32 | ** either the initial post, an edit to a post, a reply, or an edit to |
| 33 | 33 | ** a reply. |
| 34 | 34 | */ |
| 35 | | -struct ForumEntry { |
| 36 | | - int fpid; /* rid for this entry */ |
| 37 | | - int fprev; /* zero if initial entry. non-zero if an edit */ |
| 38 | | - int firt; /* This entry replies to firt */ |
| 39 | | - int mfirt; /* Root in-reply-to */ |
| 40 | | - int nReply; /* Number of replies to this entry */ |
| 35 | +struct ForumPost { |
| 36 | + int fpid; /* rid for this post */ |
| 41 | 37 | int sid; /* Serial ID number */ |
| 38 | + int rev; /* Revision number */ |
| 42 | 39 | char *zUuid; /* Artifact hash */ |
| 43 | | - ForumEntry *pLeaf; /* Most recent edit for this entry */ |
| 44 | | - ForumEntry *pEdit; /* This entry is an edit of pEdit */ |
| 45 | | - ForumEntry *pNext; /* Next in chronological order */ |
| 46 | | - ForumEntry *pPrev; /* Previous in chronological order */ |
| 47 | | - ForumEntry *pDisplay; /* Next in display order */ |
| 48 | | - int nIndent; /* Number of levels of indentation for this entry */ |
| 40 | + ForumPost *pIrt; /* This post replies to pIrt */ |
| 41 | + ForumPost *pEditHead; /* Original, unedited post */ |
| 42 | + ForumPost *pEditTail; /* Most recent edit for this post */ |
| 43 | + ForumPost *pEditNext; /* This post is edited by pEditNext */ |
| 44 | + ForumPost *pEditPrev; /* This post is an edit of pEditPrev */ |
| 45 | + ForumPost *pNext; /* Next in chronological order */ |
| 46 | + ForumPost *pPrev; /* Previous in chronological order */ |
| 47 | + ForumPost *pDisplay; /* Next in display order */ |
| 48 | + int nEdit; /* Number of edits to this post */ |
| 49 | + int nIndent; /* Number of levels of indentation for this post */ |
| 49 | 50 | }; |
| 50 | 51 | |
| 51 | 52 | /* |
| 52 | 53 | ** A single instance of the following tracks all entries for a thread. |
| 53 | 54 | */ |
| 54 | 55 | struct ForumThread { |
| 55 | | - ForumEntry *pFirst; /* First entry in chronological order */ |
| 56 | | - ForumEntry *pLast; /* Last entry in chronological order */ |
| 57 | | - ForumEntry *pDisplay; /* Entries in display order */ |
| 58 | | - ForumEntry *pTail; /* Last on the display list */ |
| 56 | + ForumPost *pFirst; /* First post in chronological order */ |
| 57 | + ForumPost *pLast; /* Last post in chronological order */ |
| 58 | + ForumPost *pDisplay; /* Entries in display order */ |
| 59 | + ForumPost *pTail; /* Last on the display list */ |
| 59 | 60 | int mxIndent; /* Maximum indentation level */ |
| 60 | 61 | }; |
| 61 | 62 | #endif /* INTERFACE */ |
| 62 | 63 | |
| 63 | 64 | /* |
| 64 | | -** Return true if the forum entry with the given rid has been |
| 65 | +** Return true if the forum post with the given rid has been |
| 65 | 66 | ** subsequently edited. |
| 66 | 67 | */ |
| 67 | 68 | int forum_rid_has_been_edited(int rid){ |
| 68 | 69 | static Stmt q; |
| 69 | 70 | int res; |
| | @@ -79,41 +80,39 @@ |
| 79 | 80 | |
| 80 | 81 | /* |
| 81 | 82 | ** Delete a complete ForumThread and all its entries. |
| 82 | 83 | */ |
| 83 | 84 | static void forumthread_delete(ForumThread *pThread){ |
| 84 | | - ForumEntry *pEntry, *pNext; |
| 85 | | - for(pEntry=pThread->pFirst; pEntry; pEntry = pNext){ |
| 86 | | - pNext = pEntry->pNext; |
| 87 | | - fossil_free(pEntry->zUuid); |
| 88 | | - fossil_free(pEntry); |
| 85 | + ForumPost *pPost, *pNext; |
| 86 | + for(pPost=pThread->pFirst; pPost; pPost = pNext){ |
| 87 | + pNext = pPost->pNext; |
| 88 | + fossil_free(pPost->zUuid); |
| 89 | + fossil_free(pPost); |
| 89 | 90 | } |
| 90 | 91 | fossil_free(pThread); |
| 91 | 92 | } |
| 92 | 93 | |
| 93 | | -#if 0 /* not used */ |
| 94 | 94 | /* |
| 95 | | -** Search a ForumEntry list forwards looking for the entry with fpid |
| 95 | +** Search a ForumPost list forwards looking for the post with fpid |
| 96 | 96 | */ |
| 97 | | -static ForumEntry *forumentry_forward(ForumEntry *p, int fpid){ |
| 97 | +static ForumPost *forumpost_forward(ForumPost *p, int fpid){ |
| 98 | 98 | while( p && p->fpid!=fpid ) p = p->pNext; |
| 99 | 99 | return p; |
| 100 | 100 | } |
| 101 | | -#endif |
| 102 | 101 | |
| 103 | 102 | /* |
| 104 | | -** Search backwards for a ForumEntry |
| 103 | +** Search backwards for a ForumPost |
| 105 | 104 | */ |
| 106 | | -static ForumEntry *forumentry_backward(ForumEntry *p, int fpid){ |
| 105 | +static ForumPost *forumpost_backward(ForumPost *p, int fpid){ |
| 107 | 106 | while( p && p->fpid!=fpid ) p = p->pPrev; |
| 108 | 107 | return p; |
| 109 | 108 | } |
| 110 | 109 | |
| 111 | 110 | /* |
| 112 | | -** Add an entry to the display list |
| 111 | +** Add a post to the display list |
| 113 | 112 | */ |
| 114 | | -static void forumentry_add_to_display(ForumThread *pThread, ForumEntry *p){ |
| 113 | +static void forumpost_add_to_display(ForumThread *pThread, ForumPost *p){ |
| 115 | 114 | if( pThread->pDisplay==0 ){ |
| 116 | 115 | pThread->pDisplay = p; |
| 117 | 116 | }else{ |
| 118 | 117 | pThread->pTail->pDisplay = p; |
| 119 | 118 | } |
| | @@ -120,108 +119,112 @@ |
| 120 | 119 | pThread->pTail = p; |
| 121 | 120 | } |
| 122 | 121 | |
| 123 | 122 | /* |
| 124 | 123 | ** Extend the display list for pThread by adding all entries that |
| 125 | | -** reference fpid. The first such entry will be no earlier then |
| 126 | | -** entry "p". |
| 124 | +** reference fpid. The first such post will be no earlier then |
| 125 | +** post "p". |
| 127 | 126 | */ |
| 128 | 127 | static void forumthread_display_order( |
| 129 | 128 | ForumThread *pThread, /* The complete thread */ |
| 130 | | - ForumEntry *pBase /* Add replies to this entry */ |
| 129 | + ForumPost *pBase /* Add replies to this post */ |
| 131 | 130 | ){ |
| 132 | | - ForumEntry *p; |
| 133 | | - ForumEntry *pPrev = 0; |
| 131 | + ForumPost *p; |
| 132 | + ForumPost *pPrev = 0; |
| 133 | + ForumPost *pBaseIrt; |
| 134 | 134 | for(p=pBase->pNext; p; p=p->pNext){ |
| 135 | | - if( p->fprev==0 && p->mfirt==pBase->fpid ){ |
| 136 | | - if( pPrev ){ |
| 137 | | - pPrev->nIndent = pBase->nIndent + 1; |
| 138 | | - forumentry_add_to_display(pThread, pPrev); |
| 139 | | - forumthread_display_order(pThread, pPrev); |
| 140 | | - } |
| 141 | | - pBase->nReply++; |
| 142 | | - pPrev = p; |
| 135 | + if( !p->pEditPrev && p->pIrt ){ |
| 136 | + pBaseIrt = p->pIrt->pEditHead ? p->pIrt->pEditHead : p->pIrt; |
| 137 | + if( pBaseIrt==pBase ){ |
| 138 | + if( pPrev ){ |
| 139 | + pPrev->nIndent = pBase->nIndent + 1; |
| 140 | + forumpost_add_to_display(pThread, pPrev); |
| 141 | + forumthread_display_order(pThread, pPrev); |
| 142 | + } |
| 143 | + pPrev = p; |
| 144 | + } |
| 143 | 145 | } |
| 144 | 146 | } |
| 145 | 147 | if( pPrev ){ |
| 146 | 148 | pPrev->nIndent = pBase->nIndent + 1; |
| 147 | 149 | if( pPrev->nIndent>pThread->mxIndent ) pThread->mxIndent = pPrev->nIndent; |
| 148 | | - forumentry_add_to_display(pThread, pPrev); |
| 150 | + forumpost_add_to_display(pThread, pPrev); |
| 149 | 151 | forumthread_display_order(pThread, pPrev); |
| 150 | 152 | } |
| 151 | 153 | } |
| 152 | 154 | |
| 153 | 155 | /* |
| 154 | 156 | ** Construct a ForumThread object given the root record id. |
| 155 | 157 | */ |
| 156 | 158 | static ForumThread *forumthread_create(int froot, int computeHierarchy){ |
| 157 | 159 | ForumThread *pThread; |
| 158 | | - ForumEntry *pEntry; |
| 160 | + ForumPost *pPost; |
| 161 | + ForumPost *p; |
| 159 | 162 | Stmt q; |
| 160 | 163 | int sid = 1; |
| 161 | | - Bag seen = Bag_INIT; |
| 164 | + int firt, fprev; |
| 162 | 165 | pThread = fossil_malloc( sizeof(*pThread) ); |
| 163 | 166 | memset(pThread, 0, sizeof(*pThread)); |
| 164 | 167 | db_prepare(&q, |
| 165 | 168 | "SELECT fpid, firt, fprev, (SELECT uuid FROM blob WHERE rid=fpid)" |
| 166 | 169 | " FROM forumpost" |
| 167 | 170 | " WHERE froot=%d ORDER BY fmtime", |
| 168 | 171 | froot |
| 169 | 172 | ); |
| 170 | 173 | while( db_step(&q)==SQLITE_ROW ){ |
| 171 | | - pEntry = fossil_malloc( sizeof(*pEntry) ); |
| 172 | | - memset(pEntry, 0, sizeof(*pEntry)); |
| 173 | | - pEntry->fpid = db_column_int(&q, 0); |
| 174 | | - pEntry->firt = db_column_int(&q, 1); |
| 175 | | - pEntry->fprev = db_column_int(&q, 2); |
| 176 | | - pEntry->zUuid = fossil_strdup(db_column_text(&q,3)); |
| 177 | | - pEntry->mfirt = pEntry->firt; |
| 178 | | - pEntry->sid = sid++; |
| 179 | | - pEntry->pPrev = pThread->pLast; |
| 180 | | - pEntry->pNext = 0; |
| 181 | | - bag_insert(&seen, pEntry->fpid); |
| 174 | + pPost = fossil_malloc( sizeof(*pPost) ); |
| 175 | + memset(pPost, 0, sizeof(*pPost)); |
| 176 | + pPost->fpid = db_column_int(&q, 0); |
| 177 | + firt = db_column_int(&q, 1); |
| 178 | + fprev = db_column_int(&q, 2); |
| 179 | + pPost->zUuid = fossil_strdup(db_column_text(&q,3)); |
| 180 | + if( !fprev ) pPost->sid = sid++; |
| 181 | + pPost->pPrev = pThread->pLast; |
| 182 | + pPost->pNext = 0; |
| 182 | 183 | if( pThread->pLast==0 ){ |
| 183 | | - pThread->pFirst = pEntry; |
| 184 | | - }else{ |
| 185 | | - pThread->pLast->pNext = pEntry; |
| 186 | | - } |
| 187 | | - if( pEntry->firt && !bag_find(&seen,pEntry->firt) ){ |
| 188 | | - pEntry->firt = froot; |
| 189 | | - pEntry->mfirt = froot; |
| 190 | | - } |
| 191 | | - pThread->pLast = pEntry; |
| 192 | | - } |
| 193 | | - db_finalize(&q); |
| 194 | | - bag_clear(&seen); |
| 195 | | - |
| 196 | | - /* Establish which entries are the latest edit. After this loop |
| 197 | | - ** completes, entries that have non-NULL pLeaf should not be |
| 198 | | - ** displayed. |
| 199 | | - */ |
| 200 | | - for(pEntry=pThread->pFirst; pEntry; pEntry=pEntry->pNext){ |
| 201 | | - if( pEntry->fprev ){ |
| 202 | | - ForumEntry *pBase = 0, *p; |
| 203 | | - p = forumentry_backward(pEntry->pPrev, pEntry->fprev); |
| 204 | | - pEntry->pEdit = p; |
| 205 | | - while( p ){ |
| 206 | | - pBase = p; |
| 207 | | - p->pLeaf = pEntry; |
| 208 | | - p = pBase->pEdit; |
| 209 | | - } |
| 210 | | - for(p=pEntry->pNext; p; p=p->pNext){ |
| 211 | | - if( p->mfirt==pEntry->fpid ) p->mfirt = pBase->fpid; |
| 212 | | - } |
| 213 | | - } |
| 214 | | - } |
| 184 | + pThread->pFirst = pPost; |
| 185 | + }else{ |
| 186 | + pThread->pLast->pNext = pPost; |
| 187 | + } |
| 188 | + pThread->pLast = pPost; |
| 189 | + |
| 190 | + /* Find the in-reply-to post. Default to the topic post if the replied-to |
| 191 | + ** post cannot be found. */ |
| 192 | + if( firt ){ |
| 193 | + pPost->pIrt = pThread->pFirst; |
| 194 | + for(p=pThread->pFirst; p; p=p->pNext){ |
| 195 | + if( p->fpid==firt ){ |
| 196 | + pPost->pIrt = p; |
| 197 | + break; |
| 198 | + } |
| 199 | + } |
| 200 | + } |
| 201 | + |
| 202 | + /* Maintain the linked list of post edits. */ |
| 203 | + if( fprev ){ |
| 204 | + p = forumpost_backward(pPost->pPrev, fprev); |
| 205 | + p->pEditNext = pPost; |
| 206 | + pPost->sid = p->sid; |
| 207 | + pPost->rev = p->rev+1; |
| 208 | + pPost->nEdit = p->nEdit+1; |
| 209 | + pPost->pEditPrev = p; |
| 210 | + pPost->pEditHead = p->pEditHead ? p->pEditHead : p; |
| 211 | + for(; p; p=p->pEditPrev ){ |
| 212 | + p->nEdit = pPost->nEdit; |
| 213 | + p->pEditTail = pPost; |
| 214 | + } |
| 215 | + } |
| 216 | + } |
| 217 | + db_finalize(&q); |
| 215 | 218 | |
| 216 | 219 | if( computeHierarchy ){ |
| 217 | 220 | /* Compute the hierarchical display order */ |
| 218 | | - pEntry = pThread->pFirst; |
| 219 | | - pEntry->nIndent = 1; |
| 221 | + pPost = pThread->pFirst; |
| 222 | + pPost->nIndent = 1; |
| 220 | 223 | pThread->mxIndent = 1; |
| 221 | | - forumentry_add_to_display(pThread, pEntry); |
| 222 | | - forumthread_display_order(pThread, pEntry); |
| 224 | + forumpost_add_to_display(pThread, pPost); |
| 225 | + forumthread_display_order(pThread, pPost); |
| 223 | 226 | } |
| 224 | 227 | |
| 225 | 228 | /* Return the result */ |
| 226 | 229 | return pThread; |
| 227 | 230 | } |
| | @@ -265,11 +268,11 @@ |
| 265 | 268 | void forumthread_cmd(void){ |
| 266 | 269 | int fpid; |
| 267 | 270 | int froot; |
| 268 | 271 | const char *zName; |
| 269 | 272 | ForumThread *pThread; |
| 270 | | - ForumEntry *p; |
| 273 | + ForumPost *p; |
| 271 | 274 | |
| 272 | 275 | db_find_and_open_repository(0,0); |
| 273 | 276 | verify_all_options(); |
| 274 | 277 | if( g.argc==2 ){ |
| 275 | 278 | forum_thread_list(); |
| | @@ -293,21 +296,22 @@ |
| 293 | 296 | pThread = forumthread_create(froot, 1); |
| 294 | 297 | fossil_print("Chronological:\n"); |
| 295 | 298 | fossil_print( |
| 296 | 299 | /* 0 1 2 3 4 5 6 7 */ |
| 297 | 300 | /* 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123 */ |
| 298 | | - " sid fpid firt fprev mfirt pLeaf nReply hash\n"); |
| 301 | + " sid rev fpid pIrt pEditPrev pEditTail hash\n"); |
| 299 | 302 | for(p=pThread->pFirst; p; p=p->pNext){ |
| 300 | | - fossil_print("%4d %9d %9d %9d %9d %9d %6d %8.8s\n", p->sid, |
| 301 | | - p->fpid, p->firt, p->fprev, p->mfirt, p->pLeaf ? p->pLeaf->fpid : 0, |
| 302 | | - p->nReply, p->zUuid); |
| 303 | + fossil_print("%4d %4d %9d %9d %9d %9d %8.8s\n", p->sid, p->rev, |
| 304 | + p->fpid, p->pIrt ? p->pIrt->fpid : 0, |
| 305 | + p->pEditPrev ? p->pEditPrev->fpid : 0, |
| 306 | + p->pEditTail ? p->pEditTail->fpid : 0, p->zUuid); |
| 303 | 307 | } |
| 304 | 308 | fossil_print("\nDisplay\n"); |
| 305 | 309 | for(p=pThread->pDisplay; p; p=p->pDisplay){ |
| 306 | 310 | fossil_print("%*s", (p->nIndent-1)*3, ""); |
| 307 | | - if( p->pLeaf ){ |
| 308 | | - fossil_print("%d->%d\n", p->fpid, p->pLeaf->fpid); |
| 311 | + if( p->pEditTail ){ |
| 312 | + fossil_print("%d->%d\n", p->fpid, p->pEditTail->fpid); |
| 309 | 313 | }else{ |
| 310 | 314 | fossil_print("%d\n", p->fpid); |
| 311 | 315 | } |
| 312 | 316 | } |
| 313 | 317 | forumthread_delete(pThread); |
| | @@ -352,28 +356,10 @@ |
| 352 | 356 | if( zClass ){ |
| 353 | 357 | @ </div> |
| 354 | 358 | } |
| 355 | 359 | } |
| 356 | 360 | |
| 357 | | -/* |
| 358 | | -** Generate the buttons in the display that allow a forum supervisor to |
| 359 | | -** mark a user as trusted. Only do this if: |
| 360 | | -** |
| 361 | | -** (1) The poster is an individual, not a special user like "anonymous" |
| 362 | | -** (2) The current user has Forum Supervisor privilege |
| 363 | | -*/ |
| 364 | | -static void generateTrustControls(Manifest *pPost){ |
| 365 | | - if( !g.perm.AdminForum ) return; |
| 366 | | - if( login_is_special(pPost->zUser) ) return; |
| 367 | | - @ <br> |
| 368 | | - @ <label><input type="checkbox" name="trust"> |
| 369 | | - @ Trust user "%h(pPost->zUser)" |
| 370 | | - @ so that future posts by "%h(pPost->zUser)" do not require moderation. |
| 371 | | - @ </label> |
| 372 | | - @ <input type="hidden" name="trustuser" value="%h(pPost->zUser)"> |
| 373 | | -} |
| 374 | | - |
| 375 | 361 | /* |
| 376 | 362 | ** Compute a display name from a login name. |
| 377 | 363 | ** |
| 378 | 364 | ** If the input login is found in the USER table, then check the USER.INFO |
| 379 | 365 | ** field to see if it has display-name followed by an email address. |
| | @@ -403,413 +389,326 @@ |
| 403 | 389 | db_reset(&q); |
| 404 | 390 | return zResult; |
| 405 | 391 | } |
| 406 | 392 | |
| 407 | 393 | /* |
| 408 | | -** Display all posts in a forum thread in chronological order |
| 394 | +** Display a single post in a forum thread. |
| 409 | 395 | */ |
| 410 | | -static void forum_display_chronological(int froot, int target, int bRawMode){ |
| 411 | | - ForumThread *pThread = forumthread_create(froot, 0); |
| 412 | | - ForumEntry *p; |
| 413 | | - int notAnon = login_is_individual(); |
| 414 | | - char cMode = bRawMode ? 'r' : 'c'; |
| 415 | | - for(p=pThread->pFirst; p; p=p->pNext){ |
| 416 | | - char *zDate; |
| 417 | | - Manifest *pPost; |
| 418 | | - int isPrivate; /* True for posts awaiting moderation */ |
| 419 | | - int sameUser; /* True if author is also the reader */ |
| 420 | | - const char *zUuid; |
| 421 | | - char *zDisplayName; /* The display name */ |
| 422 | | - int sid; |
| 423 | | - |
| 424 | | - pPost = manifest_get(p->fpid, CFTYPE_FORUM, 0); |
| 425 | | - if( pPost==0 ) continue; |
| 426 | | - if( p->fpid==target ){ |
| 427 | | - @ <div id="forum%d(p->fpid)" class="forumTime forumSel"> |
| 428 | | - }else if( p->pLeaf!=0 ){ |
| 429 | | - @ <div id="forum%d(p->fpid)" class="forumTime forumObs"> |
| 430 | | - }else{ |
| 431 | | - @ <div id="forum%d(p->fpid)" class="forumTime"> |
| 432 | | - } |
| 433 | | - if( pPost->zThreadTitle ){ |
| 434 | | - @ <h1>%h(pPost->zThreadTitle)</h1> |
| 435 | | - } |
| 436 | | - zDate = db_text(0, "SELECT datetime(%.17g)", pPost->rDate); |
| 437 | | - zDisplayName = display_name_from_login(pPost->zUser); |
| 438 | | - sid = p->pEdit ? p->pEdit->sid : p->sid; |
| 439 | | - @ <h3 class='forumPostHdr'>(%d(sid)) By %h(zDisplayName) on %h(zDate) |
| 396 | +static void forum_display_post( |
| 397 | + ForumPost *p, /* Forum post to display */ |
| 398 | + int iIndentScale, /* Indent scale factor */ |
| 399 | + int bRaw, /* True to omit the border */ |
| 400 | + int bUnf, /* True to leave the post unformatted */ |
| 401 | + int bHist, /* True if showing edit history */ |
| 402 | + int bSelect, /* True if this is the selected post */ |
| 403 | + char *zQuery /* Common query string */ |
| 404 | +){ |
| 405 | + char *zDisplayName; /* The display name */ |
| 406 | + char *zDate; /* The time/date string */ |
| 407 | + char *zHist; /* History query string */ |
| 408 | + Manifest *pManifest; /* Manifest comprising the current post */ |
| 409 | + int bPrivate; /* True for posts awaiting moderation */ |
| 410 | + int bSameUser; /* True if author is also the reader */ |
| 411 | + int iIndent; /* Indent level */ |
| 412 | + const char *zMimetype;/* Formatting MIME type */ |
| 413 | + |
| 414 | + /* Get the manifest for the post. Abort if not found (e.g. shunned). */ |
| 415 | + pManifest = manifest_get(p->fpid, CFTYPE_FORUM, 0); |
| 416 | + if( !pManifest ) return; |
| 417 | + |
| 418 | + /* When not in raw mode, create the border around the post. */ |
| 419 | + if( !bRaw ){ |
| 420 | + /* Open the <div> enclosing the post. Set the class string to mark the post |
| 421 | + ** as selected and/or obsolete. */ |
| 422 | + iIndent = (p->pEditHead ? p->pEditHead->nIndent : p->nIndent)-1; |
| 423 | + @ <div id='forum%d(p->fpid)' class='forumTime\ |
| 424 | + @ %s(bSelect ? " forumSel" : "")\ |
| 425 | + @ %s(p->pEditTail ? " forumObs" : "")'\ |
| 426 | + if( iIndent && iIndentScale ){ |
| 427 | + @ style='margin-left: %d(iIndent*iIndentScale)ex' |
| 428 | + } |
| 429 | + @ > |
| 430 | + |
| 431 | + /* If this is the first post (or an edit thereof), emit the thread title. */ |
| 432 | + if( pManifest->zThreadTitle ){ |
| 433 | + @ <h1>%h(pManifest->zThreadTitle)</h1> |
| 434 | + } |
| 435 | + |
| 436 | + /* Emit the serial number, revision number, author, and date. */ |
| 437 | + zDisplayName = display_name_from_login(pManifest->zUser); |
| 438 | + zDate = db_text(0, "SELECT datetime(%.17g)", pManifest->rDate); |
| 439 | + @ <h3 class='forumPostHdr'>(%d(p->sid)\ |
| 440 | + if( p->nEdit ){ |
| 441 | + @ .%.*d(fossil_num_digits(p->nEdit))(p->rev)\ |
| 442 | + } |
| 443 | + @ ) By %h(zDisplayName) on %h(zDate) |
| 440 | 444 | fossil_free(zDisplayName); |
| 441 | 445 | fossil_free(zDate); |
| 442 | | - if( p->pEdit ){ |
| 443 | | - @ edit of %z(href("%R/forumpost/%S?t=%c",p->pEdit->zUuid,cMode))\ |
| 444 | | - @ %d(p->pEdit->sid)</a> |
| 446 | + |
| 447 | + /* If this is an edit, refer back to the old version. Be sure "hist" is in |
| 448 | + ** the query string so the old version will actually be shown. */ |
| 449 | + if( p->pEditPrev ){ |
| 450 | + zHist = bHist ? "" : "&hist"; |
| 451 | + @ edit of \ |
| 452 | + @ %z(href("%R/forumpost/%S?%s%s",p->pEditPrev->zUuid,zQuery,zHist))\ |
| 453 | + @ %d(p->sid).%.*d(fossil_num_digits(p->nEdit))(p->pEditPrev->rev)</a> |
| 445 | 454 | } |
| 455 | + |
| 456 | + /* If debugging is enabled, link to the artifact page. */ |
| 446 | 457 | if( g.perm.Debug ){ |
| 447 | 458 | @ <span class="debug">\ |
| 448 | 459 | @ <a href="%R/artifact/%h(p->zUuid)">(artifact-%d(p->fpid))</a></span> |
| 449 | 460 | } |
| 450 | | - if( p->firt ){ |
| 451 | | - ForumEntry *pIrt = p->pPrev; |
| 452 | | - while( pIrt && pIrt->fpid!=p->firt ) pIrt = pIrt->pPrev; |
| 453 | | - if( pIrt ){ |
| 454 | | - @ in reply to %z(href("%R/forumpost/%S?t=%c",pIrt->zUuid,cMode))\ |
| 455 | | - @ %d(pIrt->sid)</a> |
| 456 | | - } |
| 457 | | - } |
| 458 | | - zUuid = p->zUuid; |
| 459 | | - if( p->pLeaf ){ |
| 460 | | - @ updated by %z(href("%R/forumpost/%S?t=%c",p->pLeaf->zUuid,cMode))\ |
| 461 | | - @ %d(p->pLeaf->sid)</a> |
| 462 | | - zUuid = p->pLeaf->zUuid; |
| 463 | | - } |
| 464 | | - if( p->fpid!=target ){ |
| 465 | | - @ %z(href("%R/forumpost/%S?t=%c",zUuid,cMode))[link]</a> |
| 466 | | - } |
| 467 | | - if( !bRawMode ){ |
| 468 | | - @ %z(href("%R/forumpost/%S?raw",zUuid))[source]</a> |
| 469 | | - } |
| 470 | | - isPrivate = content_is_private(p->fpid); |
| 471 | | - sameUser = notAnon && fossil_strcmp(pPost->zUser, g.zLogin)==0; |
| 472 | | - @ </h3> |
| 473 | | - if( isPrivate && !g.perm.ModForum && !sameUser ){ |
| 474 | | - @ <p><span class="modpending">Awaiting Moderator Approval</span></p> |
| 475 | | - }else{ |
| 476 | | - const char *zMimetype; |
| 477 | | - if( bRawMode ){ |
| 478 | | - zMimetype = "text/plain"; |
| 479 | | - }else if( p->pLeaf!=0 ){ |
| 480 | | - zMimetype = "text/plain"; |
| 481 | | - }else{ |
| 482 | | - zMimetype = pPost->zMimetype; |
| 483 | | - } |
| 484 | | - forum_render(0, zMimetype, pPost->zWiki, 0, 1); |
| 485 | | - } |
| 486 | | - if( g.perm.WrForum && p->pLeaf==0 ){ |
| 487 | | - int sameUser = login_is_individual() |
| 488 | | - && fossil_strcmp(pPost->zUser, g.zLogin)==0; |
| 461 | + |
| 462 | + /* If this is a reply, refer back to the parent post. */ |
| 463 | + if( p->pIrt ){ |
| 464 | + @ in reply to %z(href("%R/forumpost/%S?%s",p->pIrt->zUuid,zQuery))\ |
| 465 | + @ %d(p->pIrt->sid)\ |
| 466 | + if( p->pIrt->nEdit ){ |
| 467 | + @ .%.*d(fossil_num_digits(p->pIrt->nEdit))(p->pIrt->rev)\ |
| 468 | + } |
| 469 | + @ </a> |
| 470 | + } |
| 471 | + |
| 472 | + /* If this post was later edited, refer forward to the next edit. */ |
| 473 | + if( p->pEditNext ){ |
| 474 | + @ updated by %z(href("%R/forumpost/%S?%s",p->pEditNext->zUuid,zQuery))\ |
| 475 | + @ %d(p->pEditNext->sid)\ |
| 476 | + @ .%.*d(fossil_num_digits(p->nEdit))(p->pEditNext->rev)</a> |
| 477 | + } |
| 478 | + |
| 479 | + /* Provide a link to select the individual post. */ |
| 480 | + if( !bSelect ){ |
| 481 | + @ %z(href("%R/forumpost/%S?%s",p->zUuid,zQuery))[link]</a> |
| 482 | + } |
| 483 | + |
| 484 | + /* Provide a link to the raw source code. */ |
| 485 | + if( !bUnf ){ |
| 486 | + @ %z(href("%R/forumpost/%S?raw",p->zUuid))[source]</a> |
| 487 | + } |
| 488 | + @ </h3> |
| 489 | + } |
| 490 | + |
| 491 | + /* Check if this post is approved, also if it's by the current user. */ |
| 492 | + bPrivate = content_is_private(p->fpid); |
| 493 | + bSameUser = login_is_individual() |
| 494 | + && fossil_strcmp(pManifest->zUser, g.zLogin)==0; |
| 495 | + |
| 496 | + /* Render the post if the user is able to see it. */ |
| 497 | + if( bPrivate && !g.perm.ModForum && !bSameUser ){ |
| 498 | + @ <p><span class="modpending">Awaiting Moderator Approval</span></p> |
| 499 | + }else{ |
| 500 | + if( bRaw || bUnf || p->pEditTail ){ |
| 501 | + zMimetype = "text/plain"; |
| 502 | + }else{ |
| 503 | + zMimetype = pManifest->zMimetype; |
| 504 | + } |
| 505 | + forum_render(0, zMimetype, pManifest->zWiki, 0, !bRaw); |
| 506 | + } |
| 507 | + |
| 508 | + /* When not in raw mode, finish creating the border around the post. */ |
| 509 | + if( !bRaw ){ |
| 510 | + /* If the user is able to write to the forum and if this post has not been |
| 511 | + ** edited, create a form with various interaction buttons. */ |
| 512 | + if( g.perm.WrForum && !p->pEditTail ){ |
| 489 | 513 | @ <div><form action="%R/forumedit" method="POST"> |
| 490 | 514 | @ <input type="hidden" name="fpid" value="%s(p->zUuid)"> |
| 491 | | - if( !isPrivate ){ |
| 492 | | - /* Reply and Edit are only available if the post has already |
| 493 | | - ** been approved */ |
| 515 | + if( !bPrivate ){ |
| 516 | + /* Reply and Edit are only available if the post has been approved. */ |
| 494 | 517 | @ <input type="submit" name="reply" value="Reply"> |
| 495 | | - if( g.perm.Admin || sameUser ){ |
| 518 | + if( g.perm.Admin || bSameUser ){ |
| 496 | 519 | @ <input type="submit" name="edit" value="Edit"> |
| 497 | 520 | @ <input type="submit" name="nullout" value="Delete"> |
| 498 | 521 | } |
| 499 | 522 | }else if( g.perm.ModForum ){ |
| 500 | | - /* Provide moderators with moderation buttons for posts that |
| 501 | | - ** are pending moderation */ |
| 523 | + /* Allow moderators to approve or reject pending posts. Also allow |
| 524 | + ** forum supervisors to mark non-special users as trusted and therefore |
| 525 | + ** able to post unmoderated. */ |
| 502 | 526 | @ <input type="submit" name="approve" value="Approve"> |
| 503 | 527 | @ <input type="submit" name="reject" value="Reject"> |
| 504 | | - generateTrustControls(pPost); |
| 505 | | - }else if( sameUser ){ |
| 506 | | - /* A post that is pending moderation can be deleted by the |
| 507 | | - ** person who originally submitted the post */ |
| 528 | + if( g.perm.AdminForum && !login_is_special(pManifest->zUser) ){ |
| 529 | + @ <br><label><input type="checkbox" name="trust"> |
| 530 | + @ Trust user "%h(pManifest->zUser)" so that future posts by \ |
| 531 | + @ "%h(pManifest->zUser)" do not require moderation. |
| 532 | + @ </label> |
| 533 | + @ <input type="hidden" name="trustuser" value="%h(pManifest->zUser)"> |
| 534 | + } |
| 535 | + }else if( bSameUser ){ |
| 536 | + /* Allow users to delete (reject) their own pending posts. */ |
| 508 | 537 | @ <input type="submit" name="reject" value="Delete"> |
| 509 | 538 | } |
| 510 | 539 | @ </form></div> |
| 511 | 540 | } |
| 512 | | - manifest_destroy(pPost); |
| 513 | 541 | @ </div> |
| 514 | 542 | } |
| 515 | 543 | |
| 516 | | - /* Undocumented "threadtable" query parameter causes thread table |
| 517 | | - ** to be displayed for debugging purposes. |
| 518 | | - */ |
| 544 | + /* Clean up. */ |
| 545 | + manifest_destroy(pManifest); |
| 546 | +} |
| 547 | + |
| 548 | +/* |
| 549 | +** Possible display modes for forum_display_thread(). |
| 550 | +*/ |
| 551 | +enum { |
| 552 | + FD_RAW, /* Like FD_SINGLE, but additionally omit the border, force |
| 553 | + ** unformatted mode, and inhibit history mode */ |
| 554 | + FD_SINGLE, /* Render a single post and (optionally) its edit history */ |
| 555 | + FD_CHRONO, /* Render all posts in chronological order */ |
| 556 | + FD_HIER, /* Render all posts in an indented hierarchy */ |
| 557 | +}; |
| 558 | + |
| 559 | +/* |
| 560 | +** Display a forum thread. If mode is FD_RAW or FD_SINGLE, display only a |
| 561 | +** single post from the thread and (optionally) its edit history. |
| 562 | +*/ |
| 563 | +static void forum_display_thread( |
| 564 | + int froot, /* Forum thread root post ID */ |
| 565 | + int fpid, /* Selected forum post ID, or 0 if none selected */ |
| 566 | + int mode, /* Forum display mode, one of the FD_* enumerations */ |
| 567 | + int bUnf, /* True if rendering unformatted */ |
| 568 | + int bHist /* True if showing edit history, ignored for FD_RAW */ |
| 569 | +){ |
| 570 | + ForumThread *pThread; /* Thread structure */ |
| 571 | + ForumPost *pSelect; /* Currently selected post, or NULL if none */ |
| 572 | + ForumPost *p; /* Post iterator pointer */ |
| 573 | + char *zQuery; /* Common query string */ |
| 574 | + int iIndentScale = 4; /* Indent scale factor, measured in "ex" units */ |
| 575 | + int sid; /* Comparison serial ID */ |
| 576 | + |
| 577 | + /* In raw mode, force unformatted display and disable history. */ |
| 578 | + if( mode == FD_RAW ){ |
| 579 | + bUnf = 1; |
| 580 | + bHist = 0; |
| 581 | + } |
| 582 | + |
| 583 | + /* Thread together the posts and (optionally) compute the hierarchy. */ |
| 584 | + pThread = forumthread_create(froot, mode==FD_HIER); |
| 585 | + |
| 586 | + /* Compute the appropriate indent scaling. */ |
| 587 | + if( mode==FD_HIER ){ |
| 588 | + iIndentScale = 4; |
| 589 | + while( iIndentScale>1 && iIndentScale*pThread->mxIndent>25 ){ |
| 590 | + iIndentScale--; |
| 591 | + } |
| 592 | + }else{ |
| 593 | + iIndentScale = 0; |
| 594 | + } |
| 595 | + |
| 596 | + /* Find the selected post, or (depending on parameters) its latest edit. */ |
| 597 | + pSelect = fpid ? forumpost_forward(pThread->pFirst, fpid) : 0; |
| 598 | + if( !bHist && mode!=FD_RAW && pSelect && pSelect->pEditTail ){ |
| 599 | + pSelect = pSelect->pEditTail; |
| 600 | + } |
| 601 | + |
| 602 | + /* When displaying only a single post, abort if no post was selected or the |
| 603 | + ** selected forum post does not exist in the thread. Otherwise proceed to |
| 604 | + ** display the entire thread without marking any posts as selected. */ |
| 605 | + if( !pSelect && (mode==FD_RAW || mode==FD_SINGLE) ){ |
| 606 | + return; |
| 607 | + } |
| 608 | + |
| 609 | + /* Create the common query string to append to nearly all post links. */ |
| 610 | + zQuery = mode==FD_RAW ? 0 : mprintf("t=%c%s%s", |
| 611 | + mode==FD_SINGLE ? 's' : mode==FD_CHRONO ? 'c' : 'h', |
| 612 | + bUnf ? "&unf" : "", bHist ? "&hist" : ""); |
| 613 | + |
| 614 | + /* Identify which post to display first. If history is shown, start with the |
| 615 | + ** original, unedited post. Otherwise advance to the post's latest edit. */ |
| 616 | + if( mode==FD_RAW || mode==FD_SINGLE ){ |
| 617 | + p = pSelect; |
| 618 | + if( bHist && p->pEditHead ) p = p->pEditHead; |
| 619 | + }else{ |
| 620 | + p = mode==FD_CHRONO ? pThread->pFirst : pThread->pDisplay; |
| 621 | + if( !bHist && p->pEditTail ) p = p->pEditTail; |
| 622 | + } |
| 623 | + |
| 624 | + /* Display the appropriate subset of posts in sequence. */ |
| 625 | + while( p ){ |
| 626 | + /* Display the post. */ |
| 627 | + forum_display_post(p, iIndentScale, mode==FD_RAW, |
| 628 | + bUnf, bHist, p==pSelect, zQuery); |
| 629 | + |
| 630 | + /* Advance to the next post in the thread. */ |
| 631 | + if( mode==FD_CHRONO ){ |
| 632 | + /* Chronological mode: display posts (optionally including edits) in their |
| 633 | + ** original commit order. */ |
| 634 | + if( bHist ){ |
| 635 | + p = p->pNext; |
| 636 | + }else{ |
| 637 | + sid = p->sid; |
| 638 | + if( p->pEditHead ) p = p->pEditHead; |
| 639 | + do p = p->pNext; while( p && p->sid<=sid ); |
| 640 | + if( p && p->pEditTail ) p = p->pEditTail; |
| 641 | + } |
| 642 | + }else if( bHist && p->pEditNext ){ |
| 643 | + /* Hierarchical and single mode: display each post's edits in sequence. */ |
| 644 | + p = p->pEditNext; |
| 645 | + }else if( mode==FD_HIER ){ |
| 646 | + /* Hierarchical mode: after displaying with each post (optionally |
| 647 | + ** including edits), go to the next post in computed display order. */ |
| 648 | + p = p->pEditHead ? p->pEditHead->pDisplay : p->pDisplay; |
| 649 | + if( !bHist && p && p->pEditTail ) p = p->pEditTail; |
| 650 | + }else{ |
| 651 | + /* Single and raw mode: terminate after displaying the selected post and |
| 652 | + ** (optionally) its edits. */ |
| 653 | + break; |
| 654 | + } |
| 655 | + } |
| 656 | + |
| 657 | + /* Undocumented "threadtable" query parameter causes thread table to be |
| 658 | + ** displayed for debugging purposes. */ |
| 519 | 659 | if( PB("threadtable") ){ |
| 520 | 660 | @ <hr> |
| 521 | 661 | @ <table border="1" cellpadding="3" cellspacing="0"> |
| 522 | | - @ <tr><th>sid<th>fpid<th>firt<th>fprev<th>mfirt<th>pLeaf<th>nReply<th>hash |
| 662 | + @ <tr><th>sid<th>rev<th>fpid<th>pIrt<th>pEditHead<th>pEditTail\ |
| 663 | + @ <th>pEditNext<th>pEditPrev<th>pDisplay<th>hash |
| 523 | 664 | for(p=pThread->pFirst; p; p=p->pNext){ |
| 524 | | - @ <tr><td>%d(p->sid)<td>%d(p->fpid)<td>%d(p->firt)\ |
| 525 | | - @ <td>%d(p->fprev)<td>%d(p->mfirt)\ |
| 526 | | - @ <td>%d(p->pLeaf?p->pLeaf->fpid:0)<td>%d(p->nReply)\ |
| 665 | + @ <tr><td>%d(p->sid)<td>%d(p->rev)<td>%d(p->fpid)\ |
| 666 | + @ <td>%d(p->pIrt ? p->pIrt->fpid : 0)\ |
| 667 | + @ <td>%d(p->pEditHead ? p->pEditHead->fpid : 0)\ |
| 668 | + @ <td>%d(p->pEditTail ? p->pEditTail->fpid : 0)\ |
| 669 | + @ <td>%d(p->pEditNext ? p->pEditNext->fpid : 0)\ |
| 670 | + @ <td>%d(p->pEditPrev ? p->pEditPrev->fpid : 0)\ |
| 671 | + @ <td>%d(p->pDisplay ? p->pDisplay->fpid : 0)\ |
| 527 | 672 | @ <td>%S(p->zUuid)</tr> |
| 528 | 673 | } |
| 529 | 674 | @ </table> |
| 530 | 675 | } |
| 531 | 676 | |
| 532 | | - forumthread_delete(pThread); |
| 533 | | -} |
| 534 | | -/* |
| 535 | | -** Display all the edit history of post "target". |
| 536 | | -*/ |
| 537 | | -static void forum_display_history(int froot, int target, int bRawMode){ |
| 538 | | - ForumThread *pThread = forumthread_create(froot, 0); |
| 539 | | - ForumEntry *p; |
| 540 | | - int notAnon = login_is_individual(); |
| 541 | | - char cMode = bRawMode ? 'r' : 'c'; |
| 542 | | - ForumEntry *pLeaf = 0; |
| 543 | | - int cnt = 0; |
| 544 | | - for(p=pThread->pFirst; p; p=p->pNext){ |
| 545 | | - if( p->fpid==target ){ |
| 546 | | - pLeaf = p->pLeaf ? p->pLeaf : p; |
| 547 | | - break; |
| 548 | | - } |
| 549 | | - } |
| 550 | | - for(p=pThread->pFirst; p; p=p->pNext){ |
| 551 | | - char *zDate; |
| 552 | | - Manifest *pPost; |
| 553 | | - int isPrivate; /* True for posts awaiting moderation */ |
| 554 | | - int sameUser; /* True if author is also the reader */ |
| 555 | | - const char *zUuid; |
| 556 | | - char *zDisplayName; /* The display name */ |
| 557 | | - |
| 558 | | - if( p->fpid!=pLeaf->fpid && p->pLeaf!=pLeaf ) continue; |
| 559 | | - cnt++; |
| 560 | | - pPost = manifest_get(p->fpid, CFTYPE_FORUM, 0); |
| 561 | | - if( pPost==0 ) continue; |
| 562 | | - @ <div id="forum%d(p->fpid)" class="forumTime"> |
| 563 | | - zDate = db_text(0, "SELECT datetime(%.17g)", pPost->rDate); |
| 564 | | - zDisplayName = display_name_from_login(pPost->zUser); |
| 565 | | - @ <h3 class='forumPostHdr'>(%d(p->sid)) By %h(zDisplayName) on %h(zDate) |
| 566 | | - fossil_free(zDisplayName); |
| 567 | | - fossil_free(zDate); |
| 568 | | - if( g.perm.Debug ){ |
| 569 | | - @ <span class="debug">\ |
| 570 | | - @ <a href="%R/artifact/%h(p->zUuid)">(artifact-%d(p->fpid))</a></span> |
| 571 | | - } |
| 572 | | - if( p->firt && cnt==1 ){ |
| 573 | | - ForumEntry *pIrt = p->pPrev; |
| 574 | | - while( pIrt && pIrt->fpid!=p->firt ) pIrt = pIrt->pPrev; |
| 575 | | - if( pIrt ){ |
| 576 | | - @ in reply to %z(href("%R/forumpost/%S?t=%c",pIrt->zUuid,cMode))\ |
| 577 | | - @ %d(pIrt->sid)</a> |
| 578 | | - } |
| 579 | | - } |
| 580 | | - zUuid = p->zUuid; |
| 581 | | - @ %z(href("%R/forumpost/%S?t=c",zUuid))[link]</a> |
| 582 | | - if( !bRawMode ){ |
| 583 | | - @ %z(href("%R/forumpost/%S?raw",zUuid))[source]</a> |
| 584 | | - } |
| 585 | | - isPrivate = content_is_private(p->fpid); |
| 586 | | - sameUser = notAnon && fossil_strcmp(pPost->zUser, g.zLogin)==0; |
| 587 | | - @ </h3> |
| 588 | | - if( isPrivate && !g.perm.ModForum && !sameUser ){ |
| 589 | | - @ <p><span class="modpending">Awaiting Moderator Approval</span></p> |
| 590 | | - }else{ |
| 591 | | - forum_render(0, bRawMode?"text/plain":pPost->zMimetype, pPost->zWiki, |
| 592 | | - 0, 1); |
| 593 | | - } |
| 594 | | - if( g.perm.WrForum && p->pLeaf==0 ){ |
| 595 | | - int sameUser = login_is_individual() |
| 596 | | - && fossil_strcmp(pPost->zUser, g.zLogin)==0; |
| 597 | | - @ <div><form action="%R/forumedit" method="POST"> |
| 598 | | - @ <input type="hidden" name="fpid" value="%s(p->zUuid)"> |
| 599 | | - if( !isPrivate ){ |
| 600 | | - /* Reply and Edit are only available if the post has already |
| 601 | | - ** been approved */ |
| 602 | | - @ <input type="submit" name="reply" value="Reply"> |
| 603 | | - if( g.perm.Admin || sameUser ){ |
| 604 | | - @ <input type="submit" name="edit" value="Edit"> |
| 605 | | - @ <input type="submit" name="nullout" value="Delete"> |
| 606 | | - } |
| 607 | | - }else if( g.perm.ModForum ){ |
| 608 | | - /* Provide moderators with moderation buttons for posts that |
| 609 | | - ** are pending moderation */ |
| 610 | | - @ <input type="submit" name="approve" value="Approve"> |
| 611 | | - @ <input type="submit" name="reject" value="Reject"> |
| 612 | | - generateTrustControls(pPost); |
| 613 | | - }else if( sameUser ){ |
| 614 | | - /* A post that is pending moderation can be deleted by the |
| 615 | | - ** person who originally submitted the post */ |
| 616 | | - @ <input type="submit" name="reject" value="Delete"> |
| 617 | | - } |
| 618 | | - @ </form></div> |
| 619 | | - } |
| 620 | | - manifest_destroy(pPost); |
| 621 | | - @ </div> |
| 622 | | - } |
| 623 | | - forumthread_delete(pThread); |
| 624 | | -} |
| 625 | | - |
| 626 | | -/* |
| 627 | | -** Display all messages in a forumthread with indentation. |
| 628 | | -*/ |
| 629 | | -static int forum_display_hierarchical(int froot, int target){ |
| 630 | | - ForumThread *pThread; |
| 631 | | - ForumEntry *p; |
| 632 | | - Manifest *pPost, *pOPost; |
| 633 | | - int fpid; |
| 634 | | - const char *zUuid; |
| 635 | | - char *zDate; |
| 636 | | - const char *zSel; |
| 637 | | - int notAnon = login_is_individual(); |
| 638 | | - int iIndentScale = 4; |
| 639 | | - |
| 640 | | - pThread = forumthread_create(froot, 1); |
| 641 | | - for(p=pThread->pFirst; p; p=p->pNext){ |
| 642 | | - if( p->fpid==target ){ |
| 643 | | - while( p->pEdit ) p = p->pEdit; |
| 644 | | - target = p->fpid; |
| 645 | | - break; |
| 646 | | - } |
| 647 | | - } |
| 648 | | - while( iIndentScale>1 && iIndentScale*pThread->mxIndent>25 ){ |
| 649 | | - iIndentScale--; |
| 650 | | - } |
| 651 | | - for(p=pThread->pDisplay; p; p=p->pDisplay){ |
| 652 | | - int isPrivate; /* True for posts awaiting moderation */ |
| 653 | | - int sameUser; /* True if reader is also the poster */ |
| 654 | | - char *zDisplayName; /* User name to be displayed */ |
| 655 | | - pOPost = manifest_get(p->fpid, CFTYPE_FORUM, 0); |
| 656 | | - if( p->pLeaf ){ |
| 657 | | - fpid = p->pLeaf->fpid; |
| 658 | | - zUuid = p->pLeaf->zUuid; |
| 659 | | - pPost = manifest_get(fpid, CFTYPE_FORUM, 0); |
| 660 | | - }else{ |
| 661 | | - fpid = p->fpid; |
| 662 | | - zUuid = p->zUuid; |
| 663 | | - pPost = pOPost; |
| 664 | | - } |
| 665 | | - zSel = p->fpid==target ? " forumSel" : ""; |
| 666 | | - if( p->nIndent==1 ){ |
| 667 | | - @ <div id='forum%d(fpid)' class='forumHierRoot%s(zSel)'> |
| 668 | | - }else{ |
| 669 | | - @ <div id='forum%d(fpid)' class='forumHier%s(zSel)' \ |
| 670 | | - @ style='margin-left: %d((p->nIndent-1)*iIndentScale)ex;'> |
| 671 | | - } |
| 672 | | - if( pPost==0 ) continue; |
| 673 | | - if( pPost->zThreadTitle ){ |
| 674 | | - @ <h1>%h(pPost->zThreadTitle)</h1> |
| 675 | | - } |
| 676 | | - zDate = db_text(0, "SELECT datetime(%.17g)", pOPost->rDate); |
| 677 | | - zDisplayName = display_name_from_login(pOPost->zUser); |
| 678 | | - @ <h3 class='forumPostHdr'>\ |
| 679 | | - @ (%d(p->sid)) By %h(zDisplayName) on %h(zDate) |
| 680 | | - fossil_free(zDisplayName); |
| 681 | | - fossil_free(zDate); |
| 682 | | - if( g.perm.Debug ){ |
| 683 | | - @ <span class="debug">\ |
| 684 | | - @ <a href="%R/artifact/%h(p->zUuid)">(artifact-%d(p->fpid))</a></span> |
| 685 | | - } |
| 686 | | - if( p->pLeaf ){ |
| 687 | | - zDate = db_text(0, "SELECT datetime(%.17g)", pPost->rDate); |
| 688 | | - if( fossil_strcmp(pOPost->zUser,pPost->zUser)==0 ){ |
| 689 | | - @ and edited on %h(zDate) |
| 690 | | - }else{ |
| 691 | | - @ as edited by %h(pPost->zUser) on %h(zDate) |
| 692 | | - } |
| 693 | | - fossil_free(zDate); |
| 694 | | - if( g.perm.Debug ){ |
| 695 | | - @ <span class="debug">\ |
| 696 | | - @ <a href="%R/artifact/%h(p->pLeaf->zUuid)">\ |
| 697 | | - @ (artifact-%d(p->pLeaf->fpid))</a></span> |
| 698 | | - } |
| 699 | | - @ %z(href("%R/forumpost/%S?t=y",p->zUuid))[history]</a> |
| 700 | | - manifest_destroy(pOPost); |
| 701 | | - } |
| 702 | | - if( fpid!=target ){ |
| 703 | | - @ %z(href("%R/forumpost/%S",zUuid))[link]</a> |
| 704 | | - } |
| 705 | | - @ %z(href("%R/forumpost/%S?raw",zUuid))[source]</a> |
| 706 | | - if( p->firt ){ |
| 707 | | - ForumEntry *pIrt = p->pPrev; |
| 708 | | - while( pIrt && pIrt->fpid!=p->mfirt ) pIrt = pIrt->pPrev; |
| 709 | | - if( pIrt ){ |
| 710 | | - @ in reply to %z(href("%R/forumpost/%S?t=h",pIrt->zUuid))\ |
| 711 | | - @ %d(pIrt->sid)</a> |
| 712 | | - } |
| 713 | | - } |
| 714 | | - @ </h3> |
| 715 | | - isPrivate = content_is_private(fpid); |
| 716 | | - sameUser = notAnon && fossil_strcmp(pPost->zUser, g.zLogin)==0; |
| 717 | | - if( isPrivate && !g.perm.ModForum && !sameUser ){ |
| 718 | | - @ <p><span class="modpending">Awaiting Moderator Approval</span></p> |
| 719 | | - }else{ |
| 720 | | - forum_render(0, pPost->zMimetype, pPost->zWiki, 0, 1); |
| 721 | | - } |
| 722 | | - if( g.perm.WrForum ){ |
| 723 | | - @ <div><form action="%R/forumedit" method="POST"> |
| 724 | | - @ <input type="hidden" name="fpid" value="%s(zUuid)"> |
| 725 | | - if( !isPrivate ){ |
| 726 | | - /* Reply and Edit are only available if the post has already |
| 727 | | - ** been approved */ |
| 728 | | - @ <input type="submit" name="reply" value="Reply"> |
| 729 | | - if( g.perm.Admin || sameUser ){ |
| 730 | | - @ <input type="submit" name="edit" value="Edit"> |
| 731 | | - @ <input type="submit" name="nullout" value="Delete"> |
| 732 | | - } |
| 733 | | - }else if( g.perm.ModForum ){ |
| 734 | | - /* Provide moderators with moderation buttons for posts that |
| 735 | | - ** are pending moderation */ |
| 736 | | - @ <input type="submit" name="approve" value="Approve"> |
| 737 | | - @ <input type="submit" name="reject" value="Reject"> |
| 738 | | - generateTrustControls(pPost); |
| 739 | | - }else if( sameUser ){ |
| 740 | | - /* A post that is pending moderation can be deleted by the |
| 741 | | - ** person who originally submitted the post */ |
| 742 | | - @ <input type="submit" name="reject" value="Delete"> |
| 743 | | - } |
| 744 | | - @ </form></div> |
| 745 | | - } |
| 746 | | - manifest_destroy(pPost); |
| 747 | | - @ </div> |
| 748 | | - } |
| 749 | | - forumthread_delete(pThread); |
| 750 | | - return target; |
| 751 | | -} |
| 752 | | - |
| 753 | | -/* |
| 754 | | -** Emits all JS code required by /forumpost. |
| 755 | | -*/ |
| 756 | | -static void forumpost_emit_page_js(){ |
| 757 | | - static int once = 0; |
| 758 | | - if(0==once){ |
| 759 | | - once = 1; |
| 760 | | - style_emit_script_fossil_bootstrap(1); |
| 761 | | - builtin_request_js("forum.js"); |
| 762 | | - builtin_request_js("fossil.dom.js"); |
| 763 | | - builtin_request_js("fossil.page.forumpost.js"); |
| 764 | | - } |
| 677 | + /* Clean up. */ |
| 678 | + forumthread_delete(pThread); |
| 679 | + fossil_free(zQuery); |
| 765 | 680 | } |
| 766 | 681 | |
| 767 | 682 | /* |
| 768 | 683 | ** WEBPAGE: forumpost |
| 769 | 684 | ** |
| 770 | 685 | ** Show a single forum posting. The posting is shown in context with |
| 771 | | -** it's entire thread. The selected posting is enclosed within |
| 686 | +** its entire thread. The selected posting is enclosed within |
| 772 | 687 | ** <div class='forumSel'>...</div>. Javascript is used to move the |
| 773 | 688 | ** selected posting into view after the page loads. |
| 774 | 689 | ** |
| 775 | 690 | ** Query parameters: |
| 776 | 691 | ** |
| 777 | | -** name=X REQUIRED. The hash of the post to display |
| 778 | | -** t=MODE Display mode. |
| 779 | | -** 'c' for chronological |
| 780 | | -** 'h' for hierarchical |
| 781 | | -** 'a' for automatic |
| 782 | | -** 'r' for raw |
| 783 | | -** 'y' for history of post X only |
| 784 | | -** raw If present, show only the post specified and |
| 785 | | -** show its original unformatted source text. |
| 692 | +** name=X REQUIRED. The hash of the post to display. |
| 693 | +** t=a Automatic display mode, i.e. hierarchical for |
| 694 | +** desktop and chronological for mobile. This is the |
| 695 | +** default if the "t" query parameter is omitted. |
| 696 | +** t=c Show posts in the order they were written. |
| 697 | +** t=h Show posts usin hierarchical indenting. |
| 698 | +** t=s Show only the post specified by "name=X". |
| 699 | +** t=r Alias for "t=c&unf&hist". |
| 700 | +** t=y Alias for "t=s&unf&hist". |
| 701 | +** raw Alias for "t=s&unf". Additionally, omit the border |
| 702 | +** around the post, and ignore "t" and "hist". |
| 703 | +** unf Show the original, unformatted source text. |
| 704 | +** hist Show edit history in addition to current posts. |
| 786 | 705 | */ |
| 787 | 706 | void forumpost_page(void){ |
| 788 | 707 | forumthread_page(); |
| 789 | 708 | } |
| 790 | 709 | |
| 791 | | -/* |
| 792 | | -** Add an appropriate style_header() to include title of the |
| 793 | | -** given forum post. |
| 794 | | -*/ |
| 795 | | -static int forumthread_page_header(int froot, int fpid){ |
| 796 | | - char *zThreadTitle = 0; |
| 797 | | - |
| 798 | | - zThreadTitle = db_text("", |
| 799 | | - "SELECT" |
| 800 | | - " substr(event.comment,instr(event.comment,':')+2)" |
| 801 | | - " FROM forumpost, event" |
| 802 | | - " WHERE event.objid=forumpost.fpid" |
| 803 | | - " AND forumpost.fpid=%d;", |
| 804 | | - fpid |
| 805 | | - ); |
| 806 | | - style_header("%s%s", zThreadTitle, zThreadTitle[0] ? "" : "Forum"); |
| 807 | | - fossil_free(zThreadTitle); |
| 808 | | - return 0; |
| 809 | | -} |
| 810 | | - |
| 811 | 710 | /* |
| 812 | 711 | ** WEBPAGE: forumthread |
| 813 | 712 | ** |
| 814 | 713 | ** Show all forum messages associated with a particular message thread. |
| 815 | 714 | ** The result is basically the same as /forumpost except that none of |
| | @@ -816,24 +715,28 @@ |
| 816 | 715 | ** the postings in the thread are selected. |
| 817 | 716 | ** |
| 818 | 717 | ** Query parameters: |
| 819 | 718 | ** |
| 820 | 719 | ** name=X REQUIRED. The hash of any post of the thread. |
| 821 | | -** t=MODE Display mode. MODE is... |
| 822 | | -** 'c' for chronological, or |
| 823 | | -** 'h' for hierarchical, or |
| 824 | | -** 'a' for automatic, or |
| 825 | | -** 'r' for raw. |
| 826 | | -** raw Show only the post given by name= and show it unformatted |
| 827 | | -** hist Show only the edit history for the name= post |
| 720 | +** t=a Automatic display mode, i.e. hierarchical for |
| 721 | +** desktop and chronological for mobile. This is the |
| 722 | +** default if the "t" query parameter is omitted. |
| 723 | +** t=c Show posts in the order they were written. |
| 724 | +** t=h Show posts using hierarchical indenting. |
| 725 | +** unf Show the original, unformatted source text. |
| 726 | +** hist Show edit history in addition to current posts. |
| 828 | 727 | */ |
| 829 | 728 | void forumthread_page(void){ |
| 830 | 729 | int fpid; |
| 831 | 730 | int froot; |
| 731 | + char *zThreadTitle; |
| 832 | 732 | const char *zName = P("name"); |
| 833 | 733 | const char *zMode = PD("t","a"); |
| 834 | 734 | int bRaw = PB("raw"); |
| 735 | + int bUnf = PB("unf"); |
| 736 | + int bHist = PB("hist"); |
| 737 | + int mode; |
| 835 | 738 | login_check_credentials(); |
| 836 | 739 | if( !g.perm.RdForum ){ |
| 837 | 740 | login_needed(g.anon.RdForum); |
| 838 | 741 | return; |
| 839 | 742 | } |
| | @@ -847,54 +750,70 @@ |
| 847 | 750 | froot = db_int(0, "SELECT froot FROM forumpost WHERE fpid=%d", fpid); |
| 848 | 751 | if( froot==0 ){ |
| 849 | 752 | webpage_error("Not a forum post: \"%s\"", zName); |
| 850 | 753 | } |
| 851 | 754 | if( fossil_strcmp(g.zPath,"forumthread")==0 ) fpid = 0; |
| 852 | | - if( zMode[0]=='a' ){ |
| 853 | | - if( cgi_from_mobile() ){ |
| 854 | | - zMode = "c"; /* Default to chronological on mobile */ |
| 855 | | - }else{ |
| 856 | | - zMode = "h"; |
| 857 | | - } |
| 858 | | - } |
| 859 | | - if( zMode[0]!='y' ){ |
| 860 | | - forumthread_page_header(froot, fpid); |
| 861 | | - } |
| 862 | | - if( bRaw && fpid ){ |
| 863 | | - Manifest *pPost; |
| 864 | | - pPost = manifest_get(fpid, CFTYPE_FORUM, 0); |
| 865 | | - if( pPost==0 ){ |
| 866 | | - @ <p>No such forum post: %h(zName) |
| 867 | | - }else{ |
| 868 | | - int isPrivate = content_is_private(fpid); |
| 869 | | - int notAnon = login_is_individual(); |
| 870 | | - int sameUser = notAnon && fossil_strcmp(pPost->zUser, g.zLogin)==0; |
| 871 | | - if( isPrivate && !g.perm.ModForum && !sameUser ){ |
| 872 | | - @ <p><span class="modpending">Awaiting Moderator Approval</span></p> |
| 873 | | - }else{ |
| 874 | | - forum_render(0, "text/plain", pPost->zWiki, 0, 0); |
| 875 | | - } |
| 876 | | - manifest_destroy(pPost); |
| 877 | | - } |
| 878 | | - }else if( zMode[0]=='c' ){ |
| 879 | | - style_submenu_element("Hierarchical", "%R/%s/%s?t=h", g.zPath, zName); |
| 880 | | - style_submenu_element("Unformatted", "%R/%s/%s?t=r", g.zPath, zName); |
| 881 | | - forum_display_chronological(froot, fpid, 0); |
| 882 | | - }else if( zMode[0]=='r' ){ |
| 883 | | - style_submenu_element("Chronological", "%R/%s/%s?t=c", g.zPath, zName); |
| 884 | | - style_submenu_element("Hierarchical", "%R/%s/%s?t=h", g.zPath, zName); |
| 885 | | - forum_display_chronological(froot, fpid, 1); |
| 886 | | - }else if( zMode[0]=='y' ){ |
| 887 | | - style_header("Edit History Of A Forum Post"); |
| 888 | | - style_submenu_element("Complete Thread", "%R/%s/%s?t=a", g.zPath, zName); |
| 889 | | - forum_display_history(froot, fpid, 1); |
| 890 | | - }else{ |
| 891 | | - style_submenu_element("Chronological", "%R/%s/%s?t=c", g.zPath, zName); |
| 892 | | - style_submenu_element("Unformatted", "%R/%s/%s?t=r", g.zPath, zName); |
| 893 | | - forum_display_hierarchical(froot, fpid); |
| 894 | | - } |
| 895 | | - forumpost_emit_page_js(); |
| 755 | + |
| 756 | + /* Decode the mode parameters. */ |
| 757 | + if( bRaw ){ |
| 758 | + mode = FD_RAW; |
| 759 | + bUnf = 1; |
| 760 | + bHist = 0; |
| 761 | + cgi_replace_query_parameter("unf", "on"); |
| 762 | + cgi_delete_query_parameter("hist"); |
| 763 | + cgi_delete_query_parameter("raw"); |
| 764 | + }else{ |
| 765 | + switch( *zMode ){ |
| 766 | + case 'a': mode = cgi_from_mobile() ? FD_CHRONO : FD_HIER; break; |
| 767 | + case 'c': mode = FD_CHRONO; break; |
| 768 | + case 'h': mode = FD_HIER; break; |
| 769 | + case 's': mode = FD_SINGLE; break; |
| 770 | + case 'r': mode = FD_CHRONO; break; |
| 771 | + case 'y': mode = FD_SINGLE; break; |
| 772 | + default: webpage_error("Invalid thread mode: \"%s\"", zMode); |
| 773 | + } |
| 774 | + if( *zMode=='r' || *zMode=='y') { |
| 775 | + bUnf = 1; |
| 776 | + bHist = 1; |
| 777 | + cgi_replace_query_parameter("t", mode==FD_CHRONO ? "c" : "s"); |
| 778 | + cgi_replace_query_parameter("unf", "on"); |
| 779 | + cgi_replace_query_parameter("hist", "on"); |
| 780 | + } |
| 781 | + } |
| 782 | + |
| 783 | + /* Define the page header. */ |
| 784 | + zThreadTitle = db_text("", |
| 785 | + "SELECT" |
| 786 | + " substr(event.comment,instr(event.comment,':')+2)" |
| 787 | + " FROM forumpost, event" |
| 788 | + " WHERE event.objid=forumpost.fpid" |
| 789 | + " AND forumpost.fpid=%d;", |
| 790 | + fpid |
| 791 | + ); |
| 792 | + style_header("%s%s", zThreadTitle, *zThreadTitle ? "" : "Forum"); |
| 793 | + fossil_free(zThreadTitle); |
| 794 | + if( mode!=FD_CHRONO ){ |
| 795 | + style_submenu_element("Chronological", "%R/%s/%s?t=c%s%s", g.zPath, zName, |
| 796 | + bUnf ? "&unf" : "", bHist ? "&hist" : ""); |
| 797 | + } |
| 798 | + if( mode!=FD_HIER ){ |
| 799 | + style_submenu_element("Hierarchical", "%R/%s/%s?t=h%s%s", g.zPath, zName, |
| 800 | + bUnf ? "&unf" : "", bHist ? "&hist" : ""); |
| 801 | + } |
| 802 | + style_submenu_checkbox("unf", "Unformatted", 0, 0); |
| 803 | + style_submenu_checkbox("hist", "History", 0, 0); |
| 804 | + |
| 805 | + /* Display the thread. */ |
| 806 | + forum_display_thread(froot, fpid, mode, bUnf, bHist); |
| 807 | + |
| 808 | + /* Emit Forum Javascript. */ |
| 809 | + style_emit_script_fossil_bootstrap(1); |
| 810 | + builtin_request_js("forum.js"); |
| 811 | + builtin_request_js("fossil.dom.js"); |
| 812 | + builtin_request_js("fossil.page.forumpost.js"); |
| 813 | + |
| 814 | + /* Emit the page style. */ |
| 896 | 815 | style_footer(); |
| 897 | 816 | } |
| 898 | 817 | |
| 899 | 818 | /* |
| 900 | 819 | ** Return true if a forum post should be moderated. |
| | @@ -949,11 +868,11 @@ |
| 949 | 868 | webpage_assert( (zTitle==0)+(iInReplyTo==0)==1 ); |
| 950 | 869 | blob_init(&x, 0, 0); |
| 951 | 870 | zDate = date_in_standard_format("now"); |
| 952 | 871 | blob_appendf(&x, "D %s\n", zDate); |
| 953 | 872 | fossil_free(zDate); |
| 954 | | - zG = db_text(0, |
| 873 | + zG = db_text(0, |
| 955 | 874 | "SELECT uuid FROM blob, forumpost" |
| 956 | 875 | " WHERE blob.rid==forumpost.froot" |
| 957 | 876 | " AND forumpost.fpid=%d", iBasis); |
| 958 | 877 | if( zG ){ |
| 959 | 878 | blob_appendf(&x, "G %s\n", zG); |
| | @@ -1017,11 +936,11 @@ |
| 1017 | 936 | } |
| 1018 | 937 | |
| 1019 | 938 | /* |
| 1020 | 939 | ** Paint the form elements for entering a Forum post |
| 1021 | 940 | */ |
| 1022 | | -static void forum_entry_widget( |
| 941 | +static void forum_post_widget( |
| 1023 | 942 | const char *zTitle, |
| 1024 | 943 | const char *zMimetype, |
| 1025 | 944 | const char *zContent |
| 1026 | 945 | ){ |
| 1027 | 946 | if( zTitle ){ |
| | @@ -1126,11 +1045,11 @@ |
| 1126 | 1045 | } |
| 1127 | 1046 | style_header("New Forum Thread"); |
| 1128 | 1047 | @ <form action="%R/forume1" method="POST"> |
| 1129 | 1048 | @ <h1>New Thread:</h1> |
| 1130 | 1049 | forum_from_line(); |
| 1131 | | - forum_entry_widget(zTitle, zMimetype, zContent); |
| 1050 | + forum_post_widget(zTitle, zMimetype, zContent); |
| 1132 | 1051 | @ <input type="submit" name="preview" value="Preview"> |
| 1133 | 1052 | if( P("preview") && !whitespace_only(zContent) ){ |
| 1134 | 1053 | @ <input type="submit" name="submit" value="Submit"> |
| 1135 | 1054 | }else{ |
| 1136 | 1055 | @ <input type="submit" name="submit" value="Submit" disabled> |
| | @@ -1205,11 +1124,11 @@ |
| 1205 | 1124 | } |
| 1206 | 1125 | cgi_redirectf("%R/forumpost/%S",P("fpid")); |
| 1207 | 1126 | return; |
| 1208 | 1127 | } |
| 1209 | 1128 | if( P("reject") ){ |
| 1210 | | - char *zParent = |
| 1129 | + char *zParent = |
| 1211 | 1130 | db_text(0, |
| 1212 | 1131 | "SELECT uuid FROM forumpost, blob" |
| 1213 | 1132 | " WHERE forumpost.fpid=%d AND blob.rid=forumpost.firt", |
| 1214 | 1133 | fpid |
| 1215 | 1134 | ); |
| | @@ -1278,11 +1197,11 @@ |
| 1278 | 1197 | @ <h2>Revised Message:</h2> |
| 1279 | 1198 | @ <form action="%R/forume2" method="POST"> |
| 1280 | 1199 | @ <input type="hidden" name="fpid" value="%h(P("fpid"))"> |
| 1281 | 1200 | @ <input type="hidden" name="edit" value="1"> |
| 1282 | 1201 | forum_from_line(); |
| 1283 | | - forum_entry_widget(zTitle, zMimetype, zContent); |
| 1202 | + forum_post_widget(zTitle, zMimetype, zContent); |
| 1284 | 1203 | }else{ |
| 1285 | 1204 | /* Reply */ |
| 1286 | 1205 | char *zDisplayName; |
| 1287 | 1206 | zMimetype = PD("mimetype",DEFAULT_FORUM_MIMETYPE); |
| 1288 | 1207 | zContent = PDT("content",""); |
| | @@ -1304,11 +1223,11 @@ |
| 1304 | 1223 | @ <h2>Enter Reply:</h2> |
| 1305 | 1224 | @ <form action="%R/forume2" method="POST"> |
| 1306 | 1225 | @ <input type="hidden" name="fpid" value="%h(P("fpid"))"> |
| 1307 | 1226 | @ <input type="hidden" name="reply" value="1"> |
| 1308 | 1227 | forum_from_line(); |
| 1309 | | - forum_entry_widget(0, zMimetype, zContent); |
| 1228 | + forum_post_widget(0, zMimetype, zContent); |
| 1310 | 1229 | } |
| 1311 | 1230 | if( !isDelete ){ |
| 1312 | 1231 | @ <input type="submit" name="preview" value="Preview"> |
| 1313 | 1232 | } |
| 1314 | 1233 | @ <input type="submit" name="cancel" value="Cancel"> |
| 1315 | 1234 | |