/* ** Copyright (c) 2018 D. Richard Hipp ** ** This program is free software; you can redistribute it and/or ** modify it under the terms of the Simplified BSD License (also ** known as the "2-Clause License" or "FreeBSD License".) ** ** This program is distributed in the hope that it will be useful, ** but without any warranty; without even the implied warranty of ** merchantability or fitness for a particular purpose. ** ** Author contact information: ** drh@hwaci.com ** http://www.hwaci.com/drh/ ** ******************************************************************************* ** ** This file contains code used to generate the user forum. */ #include "config.h" #include #include "forum.h" /* ** Default to using Markdown markup */ #define DEFAULT_FORUM_MIMETYPE "text/x-markdown" #if INTERFACE /* ** Each instance of the following object represents a single message - ** either the initial post, an edit to a post, a reply, or an edit to ** a reply. */ struct ForumPost { int fpid; /* rid for this post */ int sid; /* Serial ID number */ int rev; /* Revision number */ char *zUuid; /* Artifact hash */ char *zDisplayName; /* Name of user who wrote this post */ double rDate; /* Date for this post */ ForumPost *pIrt; /* This post replies to pIrt */ ForumPost *pEditHead; /* Original, unedited post */ ForumPost *pEditTail; /* Most recent edit for this post */ ForumPost *pEditNext; /* This post is edited by pEditNext */ ForumPost *pEditPrev; /* This post is an edit of pEditPrev */ ForumPost *pNext; /* Next in chronological order */ ForumPost *pPrev; /* Previous in chronological order */ ForumPost *pDisplay; /* Next in display order */ int nEdit; /* Number of edits to this post */ int nIndent; /* Number of levels of indentation for this post */ }; /* ** A single instance of the following tracks all entries for a thread. */ struct ForumThread { ForumPost *pFirst; /* First post in chronological order */ ForumPost *pLast; /* Last post in chronological order */ ForumPost *pDisplay; /* Entries in display order */ ForumPost *pTail; /* Last on the display list */ int mxIndent; /* Maximum indentation level */ }; #endif /* INTERFACE */ /* ** Return true if the forum post with the given rid has been ** subsequently edited. */ int forum_rid_has_been_edited(int rid){ static Stmt q; int res; db_static_prepare(&q, "SELECT 1 FROM forumpost A, forumpost B" " WHERE A.fpid=$rid AND B.froot=A.froot AND B.fprev=$rid" ); db_bind_int(&q, "$rid", rid); res = db_step(&q)==SQLITE_ROW; db_reset(&q); return res; } /* ** Delete a complete ForumThread and all its entries. */ static void forumthread_delete(ForumThread *pThread){ ForumPost *pPost, *pNext; for(pPost=pThread->pFirst; pPost; pPost = pNext){ pNext = pPost->pNext; fossil_free(pPost->zUuid); fossil_free(pPost->zDisplayName); fossil_free(pPost); } fossil_free(pThread); } /* ** Search a ForumPost list forwards looking for the post with fpid */ static ForumPost *forumpost_forward(ForumPost *p, int fpid){ while( p && p->fpid!=fpid ) p = p->pNext; return p; } /* ** Search backwards for a ForumPost */ static ForumPost *forumpost_backward(ForumPost *p, int fpid){ while( p && p->fpid!=fpid ) p = p->pPrev; return p; } /* ** Add a post to the display list */ static void forumpost_add_to_display(ForumThread *pThread, ForumPost *p){ if( pThread->pDisplay==0 ){ pThread->pDisplay = p; }else{ pThread->pTail->pDisplay = p; } pThread->pTail = p; } /* ** Extend the display list for pThread by adding all entries that ** reference fpid. The first such post will be no earlier then ** post "p". */ static void forumthread_display_order( ForumThread *pThread, /* The complete thread */ ForumPost *pBase /* Add replies to this post */ ){ ForumPost *p; ForumPost *pPrev = 0; ForumPost *pBaseIrt; for(p=pBase->pNext; p; p=p->pNext){ if( !p->pEditPrev && p->pIrt ){ pBaseIrt = p->pIrt->pEditHead ? p->pIrt->pEditHead : p->pIrt; if( pBaseIrt==pBase ){ if( pPrev ){ pPrev->nIndent = pBase->nIndent + 1; forumpost_add_to_display(pThread, pPrev); forumthread_display_order(pThread, pPrev); } pPrev = p; } } } if( pPrev ){ pPrev->nIndent = pBase->nIndent + 1; if( pPrev->nIndent>pThread->mxIndent ) pThread->mxIndent = pPrev->nIndent; forumpost_add_to_display(pThread, pPrev); forumthread_display_order(pThread, pPrev); } } /* ** Construct a ForumThread object given the root record id. */ static ForumThread *forumthread_create(int froot, int computeHierarchy){ ForumThread *pThread; ForumPost *pPost; ForumPost *p; Stmt q; int sid = 1; int firt, fprev; pThread = fossil_malloc( sizeof(*pThread) ); memset(pThread, 0, sizeof(*pThread)); db_prepare(&q, "SELECT fpid, firt, fprev, (SELECT uuid FROM blob WHERE rid=fpid), fmtime" " FROM forumpost" " WHERE froot=%d ORDER BY fmtime", froot ); while( db_step(&q)==SQLITE_ROW ){ pPost = fossil_malloc( sizeof(*pPost) ); memset(pPost, 0, sizeof(*pPost)); pPost->fpid = db_column_int(&q, 0); firt = db_column_int(&q, 1); fprev = db_column_int(&q, 2); pPost->zUuid = fossil_strdup(db_column_text(&q,3)); pPost->rDate = db_column_double(&q,4); if( !fprev ) pPost->sid = sid++; pPost->pPrev = pThread->pLast; pPost->pNext = 0; if( pThread->pLast==0 ){ pThread->pFirst = pPost; }else{ pThread->pLast->pNext = pPost; } pThread->pLast = pPost; /* Find the in-reply-to post. Default to the topic post if the replied-to ** post cannot be found. */ if( firt ){ pPost->pIrt = pThread->pFirst; for(p=pThread->pFirst; p; p=p->pNext){ if( p->fpid==firt ){ pPost->pIrt = p; break; } } } /* Maintain the linked list of post edits. */ if( fprev ){ p = forumpost_backward(pPost->pPrev, fprev); p->pEditNext = pPost; pPost->sid = p->sid; pPost->rev = p->rev+1; pPost->nEdit = p->nEdit+1; pPost->pEditPrev = p; pPost->pEditHead = p->pEditHead ? p->pEditHead : p; for(; p; p=p->pEditPrev ){ p->nEdit = pPost->nEdit; p->pEditTail = pPost; } } } db_finalize(&q); if( computeHierarchy ){ /* Compute the hierarchical display order */ pPost = pThread->pFirst; pPost->nIndent = 1; pThread->mxIndent = 1; forumpost_add_to_display(pThread, pPost); forumthread_display_order(pThread, pPost); } /* Return the result */ return pThread; } /* ** List all forum threads to standard output. */ static void forum_thread_list(void){ Stmt q; db_prepare(&q, " SELECT" " datetime(max(fmtime))," " sum(fprev IS NULL)," " froot" " FROM forumpost" " GROUP BY froot" " ORDER BY 1;" ); fossil_print(" id cnt most recent post\n"); fossil_print("------ ---- -------------------\n"); while( db_step(&q)==SQLITE_ROW ){ fossil_print("%6d %4d %s\n", db_column_int(&q, 2), db_column_int(&q, 1), db_column_text(&q, 0) ); } db_finalize(&q); } /* ** COMMAND: test-forumthread ** ** Usage: %fossil test-forumthread [THREADID] ** ** Display a summary of all messages on a thread THREADID. If the ** THREADID argument is omitted, then show a list of all threads. ** ** This command is intended for testing an analysis only. */ void forumthread_cmd(void){ int fpid; int froot; const char *zName; ForumThread *pThread; ForumPost *p; db_find_and_open_repository(0,0); verify_all_options(); if( g.argc==2 ){ forum_thread_list(); return; } if( g.argc!=3 ) usage("THREADID"); zName = g.argv[2]; fpid = symbolic_name_to_rid(zName, "f"); if( fpid<=0 ){ fpid = db_int(0, "SELECT rid FROM blob WHERE rid=%d", atoi(zName)); } if( fpid<=0 ){ fossil_fatal("unknown or ambiguous forum id: \"%s\"", zName); } froot = db_int(0, "SELECT froot FROM forumpost WHERE fpid=%d", fpid); if( froot==0 ){ fossil_fatal("Not a forum post: \"%s\"", zName); } fossil_print("fpid = %d\n", fpid); fossil_print("froot = %d\n", froot); pThread = forumthread_create(froot, 1); fossil_print("Chronological:\n"); fossil_print( /* 0 1 2 3 4 5 6 7 */ /* 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123 */ " sid rev fpid pIrt pEditPrev pEditTail hash\n"); for(p=pThread->pFirst; p; p=p->pNext){ fossil_print("%4d %4d %9d %9d %9d %9d %8.8s\n", p->sid, p->rev, p->fpid, p->pIrt ? p->pIrt->fpid : 0, p->pEditPrev ? p->pEditPrev->fpid : 0, p->pEditTail ? p->pEditTail->fpid : 0, p->zUuid); } fossil_print("\nDisplay\n"); for(p=pThread->pDisplay; p; p=p->pDisplay){ fossil_print("%*s", (p->nIndent-1)*3, ""); if( p->pEditTail ){ fossil_print("%d->%d\n", p->fpid, p->pEditTail->fpid); }else{ fossil_print("%d\n", p->fpid); } } forumthread_delete(pThread); } /* ** WEBPAGE: forumthreadhashlist ** ** Usage: /forumthreadhashlist/HASH-OF-ROOT ** ** This page (accessibly only to admins) shows a list of all artifacts ** associated with a single forum thread. An admin might copy/paste this ** list into the /shun page in order to shun an entire thread. */ void forumthreadhashlist(void){ int fpid; int froot; const char *zName = P("name"); ForumThread *pThread; ForumPost *p; char *fuuid; login_check_credentials(); if( !g.perm.Admin ){ return; } if( zName==0 ){ webpage_error("Missing \"name=\" query parameter"); } fpid = symbolic_name_to_rid(zName, "f"); if( fpid<=0 ){ if( fpid==0 ){ webpage_notfound_error("Unknown forum id: \"%s\"", zName); }else{ ambiguous_page(); } return; } froot = db_int(0, "SELECT froot FROM forumpost WHERE fpid=%d", fpid); if( froot==0 ){ webpage_notfound_error("Not a forum post: \"%s\"", zName); } fuuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", froot); style_set_current_feature("forum"); style_header("Artifacts Of Forum Thread"); @

@ Artifacts associated with the forum thread @ %S(fuuid):

@
  pThread = forumthread_create(froot, 1);
  for(p=pThread->pFirst; p; p=p->pNext){
    @ %h(p->zUuid)
  }
  forumthread_delete(pThread);
  @ 
style_finish_page(); } /* ** Render a forum post for display */ void forum_render( const char *zTitle, /* The title. Might be NULL for no title */ const char *zMimetype, /* Mimetype of the message */ const char *zContent, /* Content of the message */ const char *zClass, /* Put in a
if not NULL */ int bScroll /* Large message content scrolls if true */ ){ if( zClass ){ @
} if( zTitle ){ if( zTitle[0] ){ @

%h(zTitle)

}else{ @

Deleted

} } if( zContent && zContent[0] ){ Blob x; const int isFossilWiki = zMimetype==0 || fossil_strcmp(zMimetype, "text/x-fossil-wiki")==0; if( bScroll ){ @
}else{ @
} blob_init(&x, 0, 0); blob_append(&x, zContent, -1); safe_html_context(DOCSRC_FORUM); if( isFossilWiki ){ /* Markdown and plain-text rendering add a wrapper DIV resp. PRE ** element around the post, and some CSS relies on its existence ** in order to handle expansion/collapse of the post. Fossil ** Wiki rendering does not do so, so we must wrap those manually ** here. */ @
} wiki_render_by_mimetype(&x, zMimetype); if( isFossilWiki ){ @
} blob_reset(&x); @
}else{ @ Deleted } if( zClass ){ @
} } /* ** Compute a display name from a login name. ** ** If the input login is found in the USER table, then check the USER.INFO ** field to see if it has display-name followed by an email address. ** If it does, that becomes the new display name. If not, let the display ** name just be the login. ** ** Space to hold the returned name is obtained from fossil_strdup() or ** mprintf() and should be freed by the caller. ** ** HTML markup within the reply has been property escaped. Hyperlinks ** may have been added. The result is safe for use with %s. */ static char *display_name_from_login(const char *zLogin){ static Stmt q; char *zResult; db_static_prepare(&q, "SELECT display_name(info) FROM user WHERE login=$login" ); db_bind_text(&q, "$login", zLogin); if( db_step(&q)==SQLITE_ROW && db_column_type(&q,0)==SQLITE_TEXT ){ const char *zDisplay = db_column_text(&q,0); if( fossil_strcmp(zDisplay,zLogin)==0 ){ zResult = mprintf("%z%h", href("%R/timeline?ss=v&y=f&vfx&u=%t",zLogin),zLogin); }else{ zResult = mprintf("%s (%z%h)", zDisplay, href("%R/timeline?ss=v&y=f&vfx&u=%t",zLogin),zLogin); } }else{ zResult = mprintf("%z%h", href("%R/timeline?ss=v&y=f&vfx&u=%t",zLogin),zLogin); } db_reset(&q); return zResult; } /* ** Compute and return the display name for a ForumPost. If ** pManifest is not NULL, then it is a Manifest object for the post. ** if pManifest is NULL, this routine has to fetch and parse the ** Manifest object for itself. ** ** Memory to hold the display name is attached to p->zDisplayName ** and will be freed together with the ForumPost object p when it ** is freed. ** ** The returned text has had all HTML markup escaped and is safe for ** use within %s. */ static char *forum_post_display_name(ForumPost *p, Manifest *pManifest){ Manifest *pToFree = 0; if( p->zDisplayName ) return p->zDisplayName; if( pManifest==0 ){ pManifest = pToFree = manifest_get(p->fpid, CFTYPE_FORUM, 0); if( pManifest==0 ) return "(unknown)"; } p->zDisplayName = display_name_from_login(pManifest->zUser); if( pToFree ) manifest_destroy(pToFree); if( p->zDisplayName==0 ) return "(unknown)"; return p->zDisplayName; } /* ** Display a single post in a forum thread. */ static void forum_display_post( ForumPost *p, /* Forum post to display */ int iIndentScale, /* Indent scale factor */ int bRaw, /* True to omit the border */ int bUnf, /* True to leave the post unformatted */ int bHist, /* True if showing edit history */ int bSelect, /* True if this is the selected post */ char *zQuery /* Common query string */ ){ char *zPosterName; /* Name of user who originally made this post */ char *zEditorName; /* Name of user who provided the current edit */ char *zDate; /* The time/date string */ char *zHist; /* History query string */ Manifest *pManifest; /* Manifest comprising the current post */ int bPrivate; /* True for posts awaiting moderation */ int bSameUser; /* True if author is also the reader */ int iIndent; /* Indent level */ const char *zMimetype;/* Formatting MIME type */ /* Get the manifest for the post. Abort if not found (e.g. shunned). */ pManifest = manifest_get(p->fpid, CFTYPE_FORUM, 0); if( !pManifest ) return; /* When not in raw mode, create the border around the post. */ if( !bRaw ){ /* Open the
enclosing the post. Set the class string to mark the post ** as selected and/or obsolete. */ iIndent = (p->pEditHead ? p->pEditHead->nIndent : p->nIndent)-1; @
}else{ @ > } /* If this is the first post (or an edit thereof), emit the thread title. */ if( pManifest->zThreadTitle ){ @

%h(pManifest->zThreadTitle)

} /* Begin emitting the header line. The forum of the title ** varies depending on whether: ** * The post is unedited ** * The post was last edited by the original author ** * The post was last edited by a different person */ if( p->pEditHead ){ zDate = db_text(0, "SELECT datetime(%.17g,toLocal())", p->pEditHead->rDate); }else{ zPosterName = forum_post_display_name(p, pManifest); zEditorName = zPosterName; } zDate = db_text(0, "SELECT datetime(%.17g,toLocal())", p->rDate); if( p->pEditPrev ){ zPosterName = forum_post_display_name(p->pEditHead, 0); zEditorName = forum_post_display_name(p, pManifest); zHist = bHist ? "" : zQuery[0]==0 ? "?hist" : "&hist"; @

(%d(p->sid)\ @ .%0*d(fossil_num_digits(p->nEdit))(p->rev)) \ if( fossil_strcmp(zPosterName, zEditorName)==0 ){ @ By %s(zPosterName) on %h(zDate) edited from \ @ %z(href("%R/forumpost/%S%s%s",p->pEditPrev->zUuid,zQuery,zHist))\ @ %d(p->sid).%0*d(fossil_num_digits(p->nEdit))(p->pEditPrev->rev) }else{ @ Originally by %s(zPosterName) \ @ with edits by %s(zEditorName) on %h(zDate) from \ @ %z(href("%R/forumpost/%S%s%s",p->pEditPrev->zUuid,zQuery,zHist))\ @ %d(p->sid).%0*d(fossil_num_digits(p->nEdit))(p->pEditPrev->rev) } }else{ zPosterName = forum_post_display_name(p, pManifest); @

(%d(p->sid)) \ @ By %s(zPosterName) on %h(zDate) } fossil_free(zDate); /* If debugging is enabled, link to the artifact page. */ if( g.perm.Debug ){ @ \ @ (artifact-%d(p->fpid)) } /* If this is a reply, refer back to the parent post. */ if( p->pIrt ){ @ in reply to %z(href("%R/forumpost/%S%s",p->pIrt->zUuid,zQuery))\ @ %d(p->pIrt->sid)\ if( p->pIrt->nEdit ){ @ .%0*d(fossil_num_digits(p->pIrt->nEdit))(p->pIrt->rev)\ } @ } /* If this post was later edited, refer forward to the next edit. */ if( p->pEditNext ){ @ updated by %z(href("%R/forumpost/%S%s",p->pEditNext->zUuid,zQuery))\ @ %d(p->pEditNext->sid)\ @ .%0*d(fossil_num_digits(p->nEdit))(p->pEditNext->rev) } /* Provide a link to select the individual post. */ if( !bSelect ){ @ %z(href("%R/forumpost/%!S%s",p->zUuid,zQuery))[link] } /* Provide a link to the raw source code. */ if( !bUnf ){ @ %z(href("%R/forumpost/%!S?raw",p->zUuid))[source] } @

} /* Check if this post is approved, also if it's by the current user. */ bPrivate = content_is_private(p->fpid); bSameUser = login_is_individual() && fossil_strcmp(pManifest->zUser, g.zLogin)==0; /* Render the post if the user is able to see it. */ if( bPrivate && !g.perm.ModForum && !bSameUser ){ @

Awaiting Moderator Approval

}else{ if( bRaw || bUnf || p->pEditTail ){ zMimetype = "text/plain"; }else{ zMimetype = pManifest->zMimetype; } forum_render(0, zMimetype, pManifest->zWiki, 0, !bRaw); } /* When not in raw mode, finish creating the border around the post. */ if( !bRaw ){ /* If the user is able to write to the forum and if this post has not been ** edited, create a form with various interaction buttons. */ if( g.perm.WrForum && !p->pEditTail ){ @
@ if( !bPrivate ){ /* Reply and Edit are only available if the post has been approved. */ @ if( g.perm.Admin || bSameUser ){ @ @ } }else if( g.perm.ModForum ){ /* Allow moderators to approve or reject pending posts. Also allow ** forum supervisors to mark non-special users as trusted and therefore ** able to post unmoderated. */ @ @ if( g.perm.AdminForum && !login_is_special(pManifest->zUser) ){ @
@ } }else if( bSameUser ){ /* Allow users to delete (reject) their own pending posts. */ @ } @
} @
} /* Clean up. */ manifest_destroy(pManifest); } /* ** Possible display modes for forum_display_thread(). */ enum { FD_RAW, /* Like FD_SINGLE, but additionally omit the border, force ** unformatted mode, and inhibit history mode */ FD_SINGLE, /* Render a single post and (optionally) its edit history */ FD_CHRONO, /* Render all posts in chronological order */ FD_HIER, /* Render all posts in an indented hierarchy */ }; /* ** Display a forum thread. If mode is FD_RAW or FD_SINGLE, display only a ** single post from the thread and (optionally) its edit history. */ static void forum_display_thread( int froot, /* Forum thread root post ID */ int fpid, /* Selected forum post ID, or 0 if none selected */ int mode, /* Forum display mode, one of the FD_* enumerations */ int autoMode, /* mode was selected automatically */ int bUnf, /* True if rendering unformatted */ int bHist /* True if showing edit history, ignored for FD_RAW */ ){ ForumThread *pThread; /* Thread structure */ ForumPost *pSelect; /* Currently selected post, or NULL if none */ ForumPost *p; /* Post iterator pointer */ char zQuery[30]; /* Common query string */ int iIndentScale = 4; /* Indent scale factor, measured in "ex" units */ int sid; /* Comparison serial ID */ int i; /* In raw mode, force unformatted display and disable history. */ if( mode == FD_RAW ){ bUnf = 1; bHist = 0; } /* Thread together the posts and (optionally) compute the hierarchy. */ pThread = forumthread_create(froot, mode==FD_HIER); /* Compute the appropriate indent scaling. */ if( mode==FD_HIER ){ iIndentScale = 4; while( iIndentScale>1 && iIndentScale*pThread->mxIndent>25 ){ iIndentScale--; } }else{ iIndentScale = 0; } /* Find the selected post, or (depending on parameters) its latest edit. */ pSelect = fpid ? forumpost_forward(pThread->pFirst, fpid) : 0; if( !bHist && mode!=FD_RAW && pSelect && pSelect->pEditTail ){ pSelect = pSelect->pEditTail; } /* When displaying only a single post, abort if no post was selected or the ** selected forum post does not exist in the thread. Otherwise proceed to ** display the entire thread without marking any posts as selected. */ if( !pSelect && (mode==FD_RAW || mode==FD_SINGLE) ){ return; } /* Create the common query string to append to nearly all post links. */ i = 0; if( !autoMode ){ char m = 'a'; switch( mode ){ case FD_RAW: m = 'r'; break; case FD_CHRONO: m = 'c'; break; case FD_HIER: m = 'h'; break; case FD_SINGLE: m = 's'; break; } zQuery[i++] = '?'; zQuery[i++] = 't'; zQuery[i++] = '='; zQuery[i++] = m; } if( bUnf ){ zQuery[i] = i==0 ? '?' : '&'; i++; zQuery[i++] = 'u'; zQuery[i++] = 'n'; zQuery[i++] = 'f'; } if( bHist ){ zQuery[i] = i==0 ? '?' : '&'; i++; zQuery[i++] = 'h'; zQuery[i++] = 'i'; zQuery[i++] = 's'; zQuery[i++] = 't'; } assert( i<(int)sizeof(zQuery) ); zQuery[i] = 0; assert( zQuery[0]==0 || zQuery[0]=='?' ); /* Identify which post to display first. If history is shown, start with the ** original, unedited post. Otherwise advance to the post's latest edit. */ if( mode==FD_RAW || mode==FD_SINGLE ){ p = pSelect; if( bHist && p->pEditHead ) p = p->pEditHead; }else{ p = mode==FD_CHRONO ? pThread->pFirst : pThread->pDisplay; if( !bHist && p->pEditTail ) p = p->pEditTail; } /* Display the appropriate subset of posts in sequence. */ while( p ){ /* Display the post. */ forum_display_post(p, iIndentScale, mode==FD_RAW, bUnf, bHist, p==pSelect, zQuery); /* Advance to the next post in the thread. */ if( mode==FD_CHRONO ){ /* Chronological mode: display posts (optionally including edits) in their ** original commit order. */ if( bHist ){ p = p->pNext; }else{ sid = p->sid; if( p->pEditHead ) p = p->pEditHead; do p = p->pNext; while( p && p->sid<=sid ); if( p && p->pEditTail ) p = p->pEditTail; } }else if( bHist && p->pEditNext ){ /* Hierarchical and single mode: display each post's edits in sequence. */ p = p->pEditNext; }else if( mode==FD_HIER ){ /* Hierarchical mode: after displaying with each post (optionally ** including edits), go to the next post in computed display order. */ p = p->pEditHead ? p->pEditHead->pDisplay : p->pDisplay; if( !bHist && p && p->pEditTail ) p = p->pEditTail; }else{ /* Single and raw mode: terminate after displaying the selected post and ** (optionally) its edits. */ break; } } /* Undocumented "threadtable" query parameter causes thread table to be ** displayed for debugging purposes. */ if( PB("threadtable") ){ @
@ @ } @
sidrevfpidpIrtpEditHeadpEditTail\ @ pEditNextpEditPrevpDisplayhash for(p=pThread->pFirst; p; p=p->pNext){ @
%d(p->sid)%d(p->rev)%d(p->fpid)\ @ %d(p->pIrt ? p->pIrt->fpid : 0)\ @ %d(p->pEditHead ? p->pEditHead->fpid : 0)\ @ %d(p->pEditTail ? p->pEditTail->fpid : 0)\ @ %d(p->pEditNext ? p->pEditNext->fpid : 0)\ @ %d(p->pEditPrev ? p->pEditPrev->fpid : 0)\ @ %d(p->pDisplay ? p->pDisplay->fpid : 0)\ @ %S(p->zUuid)
} /* Clean up. */ forumthread_delete(pThread); } /* ** Emit Forum Javascript which applies (or optionally can apply) ** to all forum-related pages. It does not include page-specific ** code (e.g. "forum.js"). */ static void forum_emit_js(void){ builtin_fossil_js_bundle_or("copybutton", "pikchr", NULL); builtin_request_js("fossil.page.forumpost.js"); } /* ** WEBPAGE: forumpost ** ** Show a single forum posting. The posting is shown in context with ** its entire thread. The selected posting is enclosed within **
...
. Javascript is used to move the ** selected posting into view after the page loads. ** ** Query parameters: ** ** name=X REQUIRED. The hash of the post to display. ** t=a Automatic display mode, i.e. hierarchical for ** desktop and chronological for mobile. This is the ** default if the "t" query parameter is omitted. ** t=c Show posts in the order they were written. ** t=h Show posts using hierarchical indenting. ** t=s Show only the post specified by "name=X". ** t=r Alias for "t=c&unf&hist". ** t=y Alias for "t=s&unf&hist". ** raw Alias for "t=s&unf". Additionally, omit the border ** around the post, and ignore "t" and "hist". ** unf Show the original, unformatted source text. ** hist Show edit history in addition to current posts. */ void forumpost_page(void){ forumthread_page(); } /* ** WEBPAGE: forumthread ** ** Show all forum messages associated with a particular message thread. ** The result is basically the same as /forumpost except that none of ** the postings in the thread are selected. ** ** Query parameters: ** ** name=X REQUIRED. The hash of any post of the thread. ** t=a Automatic display mode, i.e. hierarchical for ** desktop and chronological for mobile. This is the ** default if the "t" query parameter is omitted. ** t=c Show posts in the order they were written. ** t=h Show posts using hierarchical indenting. ** unf Show the original, unformatted source text. ** hist Show edit history in addition to current posts. */ void forumthread_page(void){ int fpid; int froot; char *zThreadTitle; const char *zName = P("name"); const char *zMode = PD("t","a"); int bRaw = PB("raw"); int bUnf = PB("unf"); int bHist = PB("hist"); int mode = 0; int autoMode = 0; login_check_credentials(); if( !g.perm.RdForum ){ login_needed(g.anon.RdForum); return; } if( zName==0 ){ webpage_error("Missing \"name=\" query parameter"); } fpid = symbolic_name_to_rid(zName, "f"); if( fpid<=0 ){ if( fpid==0 ){ webpage_notfound_error("Unknown forum id: \"%s\"", zName); }else{ ambiguous_page(); } return; } froot = db_int(0, "SELECT froot FROM forumpost WHERE fpid=%d", fpid); if( froot==0 ){ webpage_notfound_error("Not a forum post: \"%s\"", zName); } /* Decode the mode parameters. */ if( bRaw ){ mode = FD_RAW; bUnf = 1; bHist = 0; cgi_replace_query_parameter("unf", "on"); cgi_delete_query_parameter("hist"); cgi_delete_query_parameter("raw"); }else{ switch( *zMode ){ case 'a': mode = cgi_from_mobile() ? FD_CHRONO : FD_HIER; autoMode=1; break; case 'c': mode = FD_CHRONO; break; case 'h': mode = FD_HIER; break; case 's': mode = FD_SINGLE; break; case 'r': mode = FD_CHRONO; break; case 'y': mode = FD_SINGLE; break; default: webpage_error("Invalid thread mode: \"%s\"", zMode); } if( *zMode=='r' || *zMode=='y') { bUnf = 1; bHist = 1; cgi_replace_query_parameter("t", mode==FD_CHRONO ? "c" : "s"); cgi_replace_query_parameter("unf", "on"); cgi_replace_query_parameter("hist", "on"); } } /* Define the page header. */ zThreadTitle = db_text("", "SELECT" " substr(event.comment,instr(event.comment,':')+2)" " FROM forumpost, event" " WHERE event.objid=forumpost.fpid" " AND forumpost.fpid=%d;", fpid ); style_set_current_feature("forum"); style_header("%s%s", zThreadTitle, *zThreadTitle ? "" : "Forum"); fossil_free(zThreadTitle); if( mode!=FD_CHRONO ){ style_submenu_element("Chronological", "%R/%s/%s?t=c%s%s", g.zPath, zName, bUnf ? "&unf" : "", bHist ? "&hist" : ""); } if( mode!=FD_HIER ){ style_submenu_element("Hierarchical", "%R/%s/%s?t=h%s%s", g.zPath, zName, bUnf ? "&unf" : "", bHist ? "&hist" : ""); } style_submenu_checkbox("unf", "Unformatted", 0, 0); style_submenu_checkbox("hist", "History", 0, 0); if( g.perm.Admin ){ style_submenu_element("Artifacts", "%R/forumthreadhashlist/%t", zName); } /* Display the thread. */ if( fossil_strcmp(g.zPath,"forumthread")==0 ) fpid = 0; forum_display_thread(froot, fpid, mode, autoMode, bUnf, bHist); /* Emit Forum Javascript. */ builtin_request_js("forum.js"); forum_emit_js(); /* Emit the page style. */ style_finish_page(); } /* ** Return true if a forum post should be moderated. */ static int forum_need_moderation(void){ if( P("domod") ) return 1; if( g.perm.WrTForum ) return 0; if( g.perm.ModForum ) return 0; return 1; } /* ** Return true if the string is white-space only. */ static int whitespace_only(const char *z){ if( z==0 ) return 1; while( z[0] && fossil_isspace(z[0]) ){ z++; } return z[0]==0; } /* Flags for use with forum_post() */ #define FPOST_NO_ALERT 1 /* do not send any alerts */ /* ** Return a flags value for use with the final argument to ** forum_post(), extracted from the CGI environment. */ static int forum_post_flags(void){ int iPostFlags = 0; if( g.perm.Debug && P("fpsilent")!=0 ){ iPostFlags |= FPOST_NO_ALERT; } return iPostFlags; } /* ** Add a new Forum Post artifact to the repository. ** ** Return true if a redirect occurs. */ static int forum_post( const char *zTitle, /* Title. NULL for replies */ int iInReplyTo, /* Post replying to. 0 for new threads */ int iEdit, /* Post being edited, or zero for a new post */ const char *zUser, /* Username. NULL means use login name */ const char *zMimetype, /* Mimetype of content. */ const char *zContent, /* Content */ int iFlags /* FPOST_xyz flag values */ ){ char *zDate; char *zI; char *zG; int iBasis; Blob x, cksum, formatCheck, errMsg; Manifest *pPost; int nContent = zContent ? (int)strlen(zContent) : 0; schema_forum(); if( iEdit==0 && whitespace_only(zContent) ){ return 0; } if( iInReplyTo==0 && iEdit>0 ){ iBasis = iEdit; iInReplyTo = db_int(0, "SELECT firt FROM forumpost WHERE fpid=%d", iEdit); }else{ iBasis = iInReplyTo; } webpage_assert( (zTitle==0)+(iInReplyTo==0)==1 ); blob_init(&x, 0, 0); zDate = date_in_standard_format("now"); blob_appendf(&x, "D %s\n", zDate); fossil_free(zDate); zG = db_text(0, "SELECT uuid FROM blob, forumpost" " WHERE blob.rid==forumpost.froot" " AND forumpost.fpid=%d", iBasis); if( zG ){ blob_appendf(&x, "G %s\n", zG); fossil_free(zG); } if( zTitle ){ blob_appendf(&x, "H %F\n", zTitle); } zI = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", iInReplyTo); if( zI ){ blob_appendf(&x, "I %s\n", zI); fossil_free(zI); } if( fossil_strcmp(zMimetype,"text/x-fossil-wiki")!=0 ){ blob_appendf(&x, "N %s\n", zMimetype); } if( iEdit>0 ){ char *zP = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", iEdit); if( zP==0 ) webpage_error("missing edit artifact %d", iEdit); blob_appendf(&x, "P %s\n", zP); fossil_free(zP); } if( zUser==0 ){ if( login_is_nobody() ){ zUser = "anonymous"; }else{ zUser = login_name(); } } blob_appendf(&x, "U %F\n", zUser); blob_appendf(&x, "W %d\n%s\n", nContent, zContent); md5sum_blob(&x, &cksum); blob_appendf(&x, "Z %b\n", &cksum); blob_reset(&cksum); /* Verify that the artifact we are creating is well-formed */ blob_init(&formatCheck, 0, 0); blob_init(&errMsg, 0, 0); blob_copy(&formatCheck, &x); pPost = manifest_parse(&formatCheck, 0, &errMsg); if( pPost==0 ){ webpage_error("malformed forum post artifact - %s", blob_str(&errMsg)); } webpage_assert( pPost->type==CFTYPE_FORUM ); manifest_destroy(pPost); if( P("dryrun") ){ @
@ This is the artifact that would have been generated: @
%h(blob_str(&x))
@
blob_reset(&x); return 0; }else{ int nrid; db_begin_transaction(); nrid = wiki_put(&x, iEdit>0 ? iEdit : 0, forum_need_moderation()); blob_reset(&x); if( (iFlags & FPOST_NO_ALERT)!=0 ){ alert_unqueue('f', nrid); } db_commit_transaction(); cgi_redirectf("%R/forumpost/%S", rid_to_uuid(nrid)); return 1; } } /* ** Paint the form elements for entering a Forum post */ static void forum_post_widget( const char *zTitle, const char *zMimetype, const char *zContent ){ if( zTitle ){ @ Title:
} @ %z(href("%R/markup_help"))Markup style: mimetype_option_menu(zMimetype, "mimetype"); @

} /* ** WEBPAGE: forumnew ** WEBPAGE: forumedit ** ** Start a new thread on the forum or reply to an existing thread. ** But first prompt to see if the user would like to log in. */ void forum_page_init(void){ int isEdit; char *zGoto; login_check_credentials(); if( !g.perm.WrForum ){ login_needed(g.anon.WrForum); return; } if( sqlite3_strglob("*edit*", g.zPath)==0 ){ zGoto = mprintf("forume2?fpid=%S",PD("fpid","")); isEdit = 1; }else{ zGoto = mprintf("forume1"); isEdit = 0; } if( login_is_individual() ){ if( isEdit ){ forumedit_page(); }else{ forumnew_page(); } return; } style_set_current_feature("forum"); style_header("%h As Anonymous?", isEdit ? "Reply" : "Post"); @

You are not logged in. @

@
@
@ @
@
Post to the forum anonymously if( login_self_register_available(0) ){ @
@
@ @ @
@
Create a new account and post using that new account } @
@
@ @ @ @
@
Log into an existing account @
forum_emit_js(); style_finish_page(); fossil_free(zGoto); } /* ** Write the "From: USER" line on the webpage. */ static void forum_from_line(void){ if( login_is_nobody() ){ @ From: anonymous
}else{ @ From: %h(login_name())
} } static void forum_render_debug_options(void){ if( g.perm.Debug ){ /* Give extra control over the post to users with the special * Debug capability, which includes Admin and Setup users */ @

@ @
@
@
@
} } /* ** WEBPAGE: forume1 ** ** Start a new forum thread. */ void forumnew_page(void){ const char *zTitle = PDT("title",""); const char *zMimetype = PD("mimetype",DEFAULT_FORUM_MIMETYPE); const char *zContent = PDT("content",""); login_check_credentials(); if( !g.perm.WrForum ){ login_needed(g.anon.WrForum); return; } if( P("submit") && cgi_csrf_safe(1) ){ if( forum_post(zTitle, 0, 0, 0, zMimetype, zContent, forum_post_flags()) ) return; } if( P("preview") && !whitespace_only(zContent) ){ @

Preview:

forum_render(zTitle, zMimetype, zContent, "forumEdit", 1); } style_set_current_feature("forum"); style_header("New Forum Thread"); @
@

New Thread:

forum_from_line(); forum_post_widget(zTitle, zMimetype, zContent); @ if( P("preview") && !whitespace_only(zContent) ){ @ }else{ @ } forum_render_debug_options(); @
forum_emit_js(); style_finish_page(); } /* ** WEBPAGE: forume2 ** ** Edit an existing forum message. ** Query parameters: ** ** fpid=X Hash of the post to be edited. REQUIRED */ void forumedit_page(void){ int fpid; int froot; Manifest *pPost = 0; Manifest *pRootPost = 0; const char *zMimetype = 0; const char *zContent = 0; const char *zTitle = 0; char *zDate = 0; const char *zFpid = PD("fpid",""); int isCsrfSafe; int isDelete = 0; int bSameUser; /* True if author is also the reader */ int bPrivate; /* True if post is private (not yet moderated) */ login_check_credentials(); if( !g.perm.WrForum ){ login_needed(g.anon.WrForum); return; } fpid = symbolic_name_to_rid(zFpid, "f"); if( fpid<=0 || (pPost = manifest_get(fpid, CFTYPE_FORUM, 0))==0 ){ webpage_error("Missing or invalid fpid query parameter"); } froot = db_int(0, "SELECT froot FROM forumpost WHERE fpid=%d", fpid); if( froot==0 || (pRootPost = manifest_get(froot, CFTYPE_FORUM, 0))==0 ){ webpage_error("fpid does not appear to be a forum post: \"%d\"", fpid); } if( P("cancel") ){ cgi_redirectf("%R/forumpost/%S",P("fpid")); return; } isCsrfSafe = cgi_csrf_safe(1); bPrivate = content_is_private(fpid); bSameUser = login_is_individual() && fossil_strcmp(pPost->zUser, g.zLogin)==0; if( isCsrfSafe && (g.perm.ModForum || (bPrivate && bSameUser)) ){ if( g.perm.ModForum && P("approve") ){ const char *zUserToTrust; moderation_approve('f', fpid); if( g.perm.AdminForum && PB("trust") && (zUserToTrust = P("trustuser"))!=0 ){ db_unprotect(PROTECT_USER); db_multi_exec("UPDATE user SET cap=cap||'4' " "WHERE login=%Q AND cap NOT GLOB '*4*'", zUserToTrust); db_protect_pop(); } cgi_redirectf("%R/forumpost/%S",P("fpid")); return; } if( P("reject") ){ char *zParent = db_text(0, "SELECT uuid FROM forumpost, blob" " WHERE forumpost.fpid=%d AND blob.rid=forumpost.firt", fpid ); moderation_disapprove(fpid); if( zParent ){ cgi_redirectf("%R/forumpost/%S",zParent); }else{ cgi_redirectf("%R/forum"); } return; } } style_set_current_feature("forum"); isDelete = P("nullout")!=0; if( P("submit") && isCsrfSafe && (zContent = PDT("content",""))!=0 && (!whitespace_only(zContent) || isDelete) ){ int done = 1; const char *zMimetype = PD("mimetype",DEFAULT_FORUM_MIMETYPE); if( P("reply") ){ done = forum_post(0, fpid, 0, 0, zMimetype, zContent, forum_post_flags()); }else if( P("edit") || isDelete ){ done = forum_post(P("title"), 0, fpid, 0, zMimetype, zContent, forum_post_flags()); }else{ webpage_error("Missing 'reply' query parameter"); } if( done ) return; } if( isDelete ){ zMimetype = "text/x-fossil-wiki"; zContent = ""; if( pPost->zThreadTitle ) zTitle = ""; style_header("Delete %s", zTitle ? "Post" : "Reply"); @

Original Post:

forum_render(pPost->zThreadTitle, pPost->zMimetype, pPost->zWiki, "forumEdit", 1); @

Change Into:

forum_render(zTitle, zMimetype, zContent,"forumEdit", 1); @
@ @ @ @ if( zTitle ){ @ } }else if( P("edit") ){ /* Provide an edit to the fpid post */ zMimetype = P("mimetype"); zContent = PT("content"); zTitle = P("title"); if( zContent==0 ) zContent = fossil_strdup(pPost->zWiki); if( zMimetype==0 ) zMimetype = fossil_strdup(pPost->zMimetype); if( zTitle==0 && pPost->zThreadTitle!=0 ){ zTitle = fossil_strdup(pPost->zThreadTitle); } style_header("Edit %s", zTitle ? "Post" : "Reply"); @

Original Post:

forum_render(pPost->zThreadTitle, pPost->zMimetype, pPost->zWiki, "forumEdit", 1); if( P("preview") ){ @

Preview of Edited Post:

forum_render(zTitle, zMimetype, zContent,"forumEdit", 1); } @

Revised Message:

@ @ @ forum_from_line(); forum_post_widget(zTitle, zMimetype, zContent); }else{ /* Reply */ char *zDisplayName; zMimetype = PD("mimetype",DEFAULT_FORUM_MIMETYPE); zContent = PDT("content",""); style_header("Reply"); @

Replying to @ %S(zFpid) if( pRootPost->zThreadTitle ){ @ in thread @ %h(pRootPost->zThreadTitle) } @

zDate = db_text(0, "SELECT datetime(%.17g,toLocal())", pPost->rDate); zDisplayName = display_name_from_login(pPost->zUser); @

By %s(zDisplayName) on %h(zDate)

fossil_free(zDisplayName); fossil_free(zDate); forum_render(0, pPost->zMimetype, pPost->zWiki, "forumEdit", 1); if( P("preview") && !whitespace_only(zContent) ){ @

Preview:

forum_render(0, zMimetype,zContent, "forumEdit", 1); } @

Enter Reply:

@ @ @ forum_from_line(); forum_post_widget(0, zMimetype, zContent); } if( !isDelete ){ @ } @ if( (P("preview") && !whitespace_only(zContent)) || isDelete ){ @ } forum_render_debug_options(); @
forum_emit_js(); style_finish_page(); } /* ** WEBPAGE: forummain ** WEBPAGE: forum ** ** The main page for the forum feature. Show a list of recent forum ** threads. Also show a search box at the top if search is enabled, ** and a button for creating a new thread, if enabled. ** ** Query parameters: ** ** n=N The number of threads to show on each page ** x=X Skip the first X threads ** s=Y Search for term Y. */ void forum_main_page(void){ Stmt q; int iLimit = 0, iOfst, iCnt; int srchFlags; const int isSearch = P("s")!=0; char const *zLimit = 0; login_check_credentials(); srchFlags = search_restrict(SRCH_FORUM); if( !g.perm.RdForum ){ login_needed(g.anon.RdForum); return; } style_set_current_feature("forum"); style_header( "%s", isSearch ? "Forum Search Results" : "Forum" ); style_submenu_element("Timeline", "%R/timeline?ss=v&y=f&vfx"); if( g.perm.WrForum ){ style_submenu_element("New Thread","%R/forumnew"); }else{ /* Can't combine this with previous case using the ternary operator * because that causes an error yelling about "non-constant format" * with some compilers. I can't see it, since both expressions have * the same format, but I'm no C spec lawyer. */ style_submenu_element("New Thread","%R/login"); } if( g.perm.ModForum && moderation_needed() ){ style_submenu_element("Moderation Requests", "%R/modreq"); } if( (srchFlags & SRCH_FORUM)!=0 ){ if( search_screen(SRCH_FORUM, 0) ){ style_submenu_element("Recent Threads","%R/forum"); style_finish_page(); return; } } cookie_read_parameter("n","forum-n"); zLimit = P("n"); if( zLimit!=0 ){ iLimit = atoi(zLimit); if( iLimit>=0 && P("udc")!=0 ){ cookie_write_parameter("n","forum-n",0); } } if( iLimit<=0 ){ cgi_replace_query_parameter("n", fossil_strdup("25")) /*for the sake of Max, below*/; iLimit = 25; } style_submenu_entry("n","Max:",4,0); iOfst = atoi(PD("x","0")); iCnt = 0; if( db_table_exists("repository","forumpost") ){ db_prepare(&q, "WITH thread(age,duration,cnt,root,last) AS (" " SELECT" " julianday('now') - max(fmtime)," " max(fmtime) - min(fmtime)," " sum(fprev IS NULL)," " froot," " (SELECT fpid FROM forumpost AS y" " WHERE y.froot=x.froot %s" " ORDER BY y.fmtime DESC LIMIT 1)" " FROM forumpost AS x" " WHERE %s" " GROUP BY froot" " ORDER BY 1 LIMIT %d OFFSET %d" ")" "SELECT" " thread.age," /* 0 */ " thread.duration," /* 1 */ " thread.cnt," /* 2 */ " blob.uuid," /* 3 */ " substr(event.comment,instr(event.comment,':')+1)," /* 4 */ " thread.last" /* 5 */ " FROM thread, blob, event" " WHERE blob.rid=thread.last" " AND event.objid=thread.last" " ORDER BY 1;", g.perm.ModForum ? "" : "AND y.fpid NOT IN private" /*safe-for-%s*/, g.perm.ModForum ? "true" : "fpid NOT IN private" /*safe-for-%s*/, iLimit+1, iOfst ); while( db_step(&q)==SQLITE_ROW ){ char *zAge = human_readable_age(db_column_double(&q,0)); int nMsg = db_column_int(&q, 2); const char *zUuid = db_column_text(&q, 3); const char *zTitle = db_column_text(&q, 4); if( iCnt==0 ){ if( iOfst>0 ){ @

Threads at least %s(zAge) old

}else{ @

Most recent threads

} @
if( iOfst>0 ){ if( iOfst>iLimit ){ @ }else{ @ } } } iCnt++; if( iCnt>iLimit ){ @ fossil_free(zAge); break; } @ @ @ }else{ char *zDuration = human_readable_age(db_column_double(&q,1)); @ %d(nMsg) posts spanning %h(zDuration) fossil_free(zDuration); } @ fossil_free(zAge); } db_finalize(&q); } if( iCnt>0 ){ @
\ @ %z(href("%R/forum?x=%d&n=%d",iOfst-iLimit,iLimit))\ @ ↑ Newer...
%z(href("%R/forum?n=%d",iLimit))\ @ ↑ Newer...
\ @ %z(href("%R/forum?x=%d&n=%d",iOfst+iLimit,iLimit))\ @ ↓ Older...
%h(zAge) ago%z(href("%R/forumpost/%S",zUuid))%h(zTitle)\ if( g.perm.ModForum && moderation_pending(db_column_int(&q,5)) ){ @ \ @ Awaiting Moderator Approval
} if( nMsg<2 ){ @ no replies
}else{ @

No forum posts found

} style_finish_page(); }