| | @@ -20,336 +20,100 @@ |
| 20 | 20 | #include "config.h" |
| 21 | 21 | #include <assert.h> |
| 22 | 22 | #include "forum.h" |
| 23 | 23 | |
| 24 | 24 | /* |
| 25 | | -** The schema for the tables that manage the forum, if forum is |
| 26 | | -** enabled. |
| 27 | | -*/ |
| 28 | | -static const char zForumInit[] = |
| 29 | | -@ CREATE TABLE repository.forumpost( |
| 30 | | -@ mpostid INTEGER PRIMARY KEY, -- unique id for each post (local) |
| 31 | | -@ mposthash TEXT, -- uuid for this post |
| 32 | | -@ mthreadid INTEGER, -- thread to which this post belongs |
| 33 | | -@ uname TEXT, -- name of user |
| 34 | | -@ mtime REAL, -- julian day number |
| 35 | | -@ mstatus TEXT, -- status. NULL=ok. 'mod'=pending moderation |
| 36 | | -@ mimetype TEXT, -- Mimetype for mbody |
| 37 | | -@ ipaddr TEXT, -- IP address of post origin |
| 38 | | -@ inreplyto INT, -- Parent posting |
| 39 | | -@ mbody TEXT -- Content of the post |
| 40 | | -@ ); |
| 41 | | -@ CREATE INDEX repository.forumpost_x1 ON |
| 42 | | -@ forumpost(inreplyto,mtime); |
| 43 | | -@ CREATE TABLE repository.forumthread( |
| 44 | | -@ mthreadid INTEGER PRIMARY KEY, |
| 45 | | -@ mthreadhash TEXT, -- uuid for this thread |
| 46 | | -@ mtitle TEXT, -- Title or subject line |
| 47 | | -@ mtime REAL, -- Most recent update |
| 48 | | -@ npost INT -- Number of posts on this thread |
| 49 | | -@ ); |
| 50 | | -; |
| 51 | | - |
| 52 | | -/* |
| 53 | | -** Create the forum tables in the schema if they do not already |
| 54 | | -** exist. |
| 55 | | -*/ |
| 56 | | -static void forum_verify_schema(void){ |
| 57 | | - if( !db_table_exists("repository","forumpost") ){ |
| 58 | | - db_multi_exec(zForumInit /*works-like:""*/); |
| 59 | | - } |
| 60 | | -} |
| 61 | | - |
| 62 | | -/* |
| 63 | | -** WEBPAGE: forum |
| 64 | | -** URL: /forum |
| 65 | | -** Query parameters: |
| 66 | | -** |
| 67 | | -** item=N Show post N and its replies |
| 68 | | -** |
| 69 | | -*/ |
| 70 | | -void forum_page(void){ |
| 71 | | - int itemId; |
| 72 | | - Stmt q; |
| 73 | | - int i; |
| 74 | | - |
| 75 | | - login_check_credentials(); |
| 76 | | - if( !g.perm.RdForum ){ login_needed(g.anon.RdForum); return; } |
| 77 | | - forum_verify_schema(); |
| 78 | | - style_header("Forum"); |
| 79 | | - itemId = atoi(PD("item","0")); |
| 80 | | - if( itemId>0 ){ |
| 81 | | - int iUp; |
| 82 | | - double rNow; |
| 83 | | - style_submenu_element("Topics", "%R/forum"); |
| 84 | | - iUp = db_int(0, "SELECT inreplyto FROM forumpost WHERE mpostid=%d", itemId); |
| 85 | | - if( iUp ){ |
| 86 | | - style_submenu_element("Parent", "%R/forum?item=%d", iUp); |
| 87 | | - } |
| 88 | | - rNow = db_double(0.0, "SELECT julianday('now')"); |
| 89 | | - /* Show the post given by itemId and all its descendents */ |
| 90 | | - db_prepare(&q, |
| 91 | | - "WITH RECURSIVE" |
| 92 | | - " post(id,uname,mstat,mime,ipaddr,parent,mbody,depth,mtime) AS (" |
| 93 | | - " SELECT mpostid, uname, mstatus, mimetype, ipaddr, inreplyto, mbody," |
| 94 | | - " 0, mtime FROM forumpost WHERE mpostid=%d" |
| 95 | | - " UNION" |
| 96 | | - " SELECT f.mpostid, f.uname, f.mstatus, f.mimetype, f.ipaddr," |
| 97 | | - " f.inreplyto, f.mbody, p.depth+1 AS xdepth, f.mtime AS xtime" |
| 98 | | - " FROM forumpost AS f, post AS p" |
| 99 | | - " WHERE f.inreplyto=p.id" |
| 100 | | - " ORDER BY xdepth DESC, xtime ASC" |
| 101 | | - ") SELECT * FROM post;", |
| 102 | | - itemId |
| 103 | | - ); |
| 104 | | - while( db_step(&q)==SQLITE_ROW ){ |
| 105 | | - int id = db_column_int(&q, 0); |
| 106 | | - const char *zUser = db_column_text(&q, 1); |
| 107 | | - const char *zMime = db_column_text(&q, 3); |
| 108 | | - int iDepth = db_column_int(&q, 7); |
| 109 | | - double rMTime = db_column_double(&q, 8); |
| 110 | | - char *zAge = db_timespan_name(rNow - rMTime); |
| 111 | | - Blob body; |
| 112 | | - @ <!-- Forum post %d(id) --> |
| 113 | | - @ <table class="forum_post"> |
| 114 | | - @ <tr> |
| 115 | | - @ <td class="forum_margin" width="%d(iDepth*40)" rowspan="2"> |
| 116 | | - @ <td><span class="forum_author">%h(zUser)</span> |
| 117 | | - @ <span class="forum_age">%s(zAge) ago</span> |
| 118 | | - sqlite3_free(zAge); |
| 119 | | - if( g.perm.WrForum ){ |
| 120 | | - @ <span class="forum_buttons"> |
| 121 | | - if( g.perm.AdminForum || fossil_strcmp(g.zLogin, zUser)==0 ){ |
| 122 | | - @ <a href='%R/forumedit?item=%d(id)'>Edit</a> |
| 123 | | - } |
| 124 | | - @ <a href='%R/forumedit?replyto=%d(id)'>Reply</a> |
| 125 | | - @ </span> |
| 126 | | - } |
| 127 | | - @ </tr> |
| 128 | | - @ <tr><td><div class="forum_body"> |
| 129 | | - blob_init(&body, db_column_text(&q,6), db_column_bytes(&q,6)); |
| 130 | | - wiki_render_by_mimetype(&body, zMime); |
| 131 | | - blob_reset(&body); |
| 132 | | - @ </div></td></tr> |
| 133 | | - @ </table> |
| 134 | | - } |
| 135 | | - }else{ |
| 136 | | - /* If we reach this point, that means the users wants a list of |
| 137 | | - ** recent threads. |
| 138 | | - */ |
| 139 | | - i = 0; |
| 140 | | - db_prepare(&q, |
| 141 | | - "SELECT a.mtitle, a.npost, b.mpostid" |
| 142 | | - " FROM forumthread AS a, forumpost AS b " |
| 143 | | - " WHERE a.mthreadid=b.mthreadid" |
| 144 | | - " AND b.inreplyto IS NULL" |
| 145 | | - " ORDER BY a.mtime DESC LIMIT 40" |
| 146 | | - ); |
| 147 | | - if( g.perm.WrForum ){ |
| 148 | | - style_submenu_element("New", "%R/forumedit"); |
| 149 | | - } |
| 150 | | - @ <h1>Recent Forum Threads</h1> |
| 151 | | - while( db_step(&q)==SQLITE_ROW ){ |
| 152 | | - int n = db_column_int(&q,1); |
| 153 | | - int itemid = db_column_int(&q,2); |
| 154 | | - const char *zTitle = db_column_text(&q,0); |
| 155 | | - if( (i++)==0 ){ |
| 156 | | - @ <ol> |
| 157 | | - } |
| 158 | | - @ <li><span class="forum_title"> |
| 159 | | - @ %z(href("%R/forum?item=%d",itemid))%h(zTitle)</a></span> |
| 160 | | - @ <span class="forum_npost">%d(n) post%s(n==1?"":"s")</span></li> |
| 161 | | - } |
| 162 | | - if( i ){ |
| 163 | | - @ </ol> |
| 164 | | - } |
| 165 | | - } |
| 25 | +** Display all posts in a forum thread in chronological order |
| 26 | +*/ |
| 27 | +static void forum_thread_chronological(int froot){ |
| 28 | + Stmt q; |
| 29 | + db_prepare(&q, "SELECT fpid FROM forumpost WHERE froot=%d" |
| 30 | + " ORDER BY fmtime", froot); |
| 31 | + while( db_step(&q)==SQLITE_ROW ){ |
| 32 | + int fpid = db_column_int(&q, 0); |
| 33 | + Manifest *pPost = manifest_get(fpid, CFTYPE_FORUM, 0); |
| 34 | + if( pPost==0 ) continue; |
| 35 | + manifest_destroy(pPost); |
| 36 | + } |
| 37 | + db_finalize(&q); |
| 38 | +} |
| 39 | + |
| 40 | +/* |
| 41 | +** WEBPAGE: forumthread |
| 42 | +** |
| 43 | +** Show all forum messages associated with a particular message thread. |
| 44 | +** |
| 45 | +** Query parameters: |
| 46 | +** |
| 47 | +** name=X The hash of the first post of the thread. REQUIRED |
| 48 | +*/ |
| 49 | +void forumthread_page(void){ |
| 50 | + int fpid; |
| 51 | + int froot; |
| 52 | + const char *zName = P("name"); |
| 53 | + login_check_credentials(); |
| 54 | + if( !g.perm.RdForum ){ |
| 55 | + login_needed(g.anon.RdForum); |
| 56 | + return; |
| 57 | + } |
| 58 | + style_header("Forum"); |
| 59 | + if( zName==0 ){ |
| 60 | + @ <p class='generalError'>Missing name= query parameter</p> |
| 61 | + style_footer(); |
| 62 | + return; |
| 63 | + } |
| 64 | + fpid = symbolic_name_to_rid(zName, "f"); |
| 65 | + if( fpid<=0 ){ |
| 66 | + @ <p class='generalError'>Unknown or ambiguous forum id in the "name=" |
| 67 | + @ query parameter</p> |
| 68 | + style_footer(); |
| 69 | + return; |
| 70 | + } |
| 71 | + froot = db_int(0, "SELECT froot FROM forumpost WHERE fpid=%d", fpid); |
| 72 | + if( froot==0 ){ |
| 73 | + @ <p class='generalError'>Invalid forum id in the "name=" |
| 74 | + @ query parameter</p> |
| 75 | + style_footer(); |
| 76 | + return; |
| 77 | + } |
| 78 | + forum_thread_chronological(froot); |
| 79 | + style_footer(); |
| 80 | +} |
| 81 | + |
| 82 | +/* |
| 83 | +** WEBPAGE: forumnew |
| 84 | +** |
| 85 | +** Start a new forum thread. |
| 86 | +*/ |
| 87 | +void forumnew_page(void){ |
| 88 | + style_header("Pending"); |
| 89 | + @ TBD... |
| 166 | 90 | style_footer(); |
| 167 | 91 | } |
| 168 | 92 | |
| 169 | 93 | /* |
| 170 | | -** Use content in CGI parameters "s" (subject), "b" (body), and |
| 171 | | -** "mimetype" (mimetype) to create a new forum entry. |
| 172 | | -** Return the id of the new forum entry. |
| 173 | | -** |
| 174 | | -** If any problems occur, return 0 and set *pzErr to a description of |
| 175 | | -** the problem. |
| 176 | | -** |
| 177 | | -** Cases: |
| 178 | | -** |
| 179 | | -** itemId==0 && parentId==0 Starting a new thread. |
| 180 | | -** itemId==0 && parentId>0 New reply to parentId |
| 181 | | -** itemId>0 && parentId==0 Edit existing post itemId |
| 94 | +** WEBPAGE: forumreply |
| 95 | +** |
| 96 | +** Reply to a forum message. |
| 97 | +** Query parameters: |
| 98 | +** |
| 99 | +** name=X Hash of the post to reply to. REQUIRED |
| 182 | 100 | */ |
| 183 | | -static int forum_post(int itemId, int parentId, char **pzErr){ |
| 184 | | - const char *zSubject = 0; |
| 185 | | - int threadId; |
| 186 | | - double rNow = db_double(0.0, "SELECT julianday('now')"); |
| 187 | | - const char *zMime = wiki_filter_mimetypes(P("mimetype")); |
| 188 | | - if( itemId==0 && parentId==0 ){ |
| 189 | | - /* Start a new thread. Subject required. */ |
| 190 | | - sqlite3_uint64 r1, r2; |
| 191 | | - zSubject = PT("s"); |
| 192 | | - if( zSubject==0 || zSubject[0]==0 ){ |
| 193 | | - *pzErr = "\"Subject\" required to start a new thread"; |
| 194 | | - return 0; |
| 195 | | - } |
| 196 | | - sqlite3_randomness(sizeof(r1), &r1); |
| 197 | | - sqlite3_randomness(sizeof(r2), &r2); |
| 198 | | - db_multi_exec( |
| 199 | | - "INSERT INTO forumthread(mthreadhash, mtitle, mtime, npost)" |
| 200 | | - "VALUES(lower(hex(randomblob(28))),%Q,%!.17g,1)", |
| 201 | | - zSubject, rNow |
| 202 | | - ); |
| 203 | | - threadId = db_last_insert_rowid(); |
| 204 | | - }else{ |
| 205 | | - threadId = db_int(0, "SELECT mthreadid FROM forumpost" |
| 206 | | - " WHERE mpostid=%d", itemId ? itemId : parentId); |
| 207 | | - } |
| 208 | | - if( itemId ){ |
| 209 | | - if( db_int(0, "SELECT inreplyto IS NULL FROM forumpost" |
| 210 | | - " WHERE mpostid=%d", itemId) ){ |
| 211 | | - db_multi_exec( |
| 212 | | - "UPDATE forumthread SET mtitle=%Q WHERE mthreadid=%d", |
| 213 | | - PT("s"), threadId |
| 214 | | - ); |
| 215 | | - } |
| 216 | | - db_multi_exec( |
| 217 | | - "UPDATE forumpost SET" |
| 218 | | - " mtime=%!.17g," |
| 219 | | - " mimetype=%Q," |
| 220 | | - " ipaddr=%Q," |
| 221 | | - " mbody=%Q" |
| 222 | | - " WHERE mpostid=%d", |
| 223 | | - rNow, PT("mimetype"), P("REMOTE_ADDR"), PT("b"), itemId |
| 224 | | - ); |
| 225 | | - }else{ |
| 226 | | - db_multi_exec( |
| 227 | | - "INSERT INTO forumpost(mposthash,mthreadid,uname,mtime," |
| 228 | | - " mstatus,mimetype,ipaddr,inreplyto,mbody) VALUES" |
| 229 | | - " (lower(hex(randomblob(28))),%d,%Q,%!.17g,%Q,%Q,%Q,nullif(%d,0),%Q)", |
| 230 | | - threadId,g.zLogin,rNow,NULL,zMime,P("REMOTE_ADDR"),parentId,P("b")); |
| 231 | | - itemId = db_last_insert_rowid(); |
| 232 | | - } |
| 233 | | - if( zSubject==0 ){ |
| 234 | | - db_multi_exec( |
| 235 | | - "UPDATE forumthread SET mtime=%!.17g, npost=npost+1" |
| 236 | | - " WHERE mthreadid=(SELECT mthreadid FROM forumpost WHERE mpostid=%d)", |
| 237 | | - rNow, itemId |
| 238 | | - ); |
| 239 | | - } |
| 240 | | - return itemId; |
| 101 | +void forumreply_page(void){ |
| 102 | + style_header("Pending"); |
| 103 | + @ TBD... |
| 104 | + style_footer(); |
| 241 | 105 | } |
| 242 | 106 | |
| 243 | 107 | /* |
| 244 | 108 | ** WEBPAGE: forumedit |
| 245 | 109 | ** |
| 110 | +** Edit an existing forum message. |
| 246 | 111 | ** Query parameters: |
| 247 | 112 | ** |
| 248 | | -** replyto=N Enter a reply to forum item N |
| 249 | | -** item=N Edit item N |
| 250 | | -** s=SUBJECT Subject. New thread only. Omitted for replies |
| 251 | | -** b=BODY Body of the post |
| 252 | | -** m=MIMETYPE Mimetype for the body of the post |
| 253 | | -** x Submit changes |
| 254 | | -** p Preview changes |
| 113 | +** name=X Hash of the post to be editted. REQUIRED |
| 255 | 114 | */ |
| 256 | | -void forum_edit_page(void){ |
| 257 | | - int itemId; |
| 258 | | - int parentId; |
| 259 | | - char *zErr = 0; |
| 260 | | - const char *zMime; |
| 261 | | - const char *zSub; |
| 262 | | - |
| 263 | | - login_check_credentials(); |
| 264 | | - if( !g.perm.WrForum ){ login_needed(g.anon.WrForum); return; } |
| 265 | | - forum_verify_schema(); |
| 266 | | - itemId = atoi(PD("item","0")); |
| 267 | | - parentId = atoi(PD("replyto","0")); |
| 268 | | - if( P("cancel")!=0 ){ |
| 269 | | - cgi_redirectf("%R/forum?item=%d", itemId ? itemId : parentId); |
| 270 | | - return; |
| 271 | | - } |
| 272 | | - if( P("x")!=0 && cgi_csrf_safe(1) ){ |
| 273 | | - itemId = forum_post(itemId,parentId,&zErr); |
| 274 | | - if( itemId ){ |
| 275 | | - cgi_redirectf("%R/forum?item=%d",itemId); |
| 276 | | - return; |
| 277 | | - } |
| 278 | | - } |
| 279 | | - if( itemId && (P("mimetype")==0 || P("b")==0) ){ |
| 280 | | - Stmt q; |
| 281 | | - db_prepare(&q, "SELECT mimetype, mbody FROM forumpost" |
| 282 | | - " WHERE mpostid=%d", itemId); |
| 283 | | - if( db_step(&q)==SQLITE_ROW ){ |
| 284 | | - if( P("mimetype")==0 ){ |
| 285 | | - cgi_set_query_parameter("mimetype", db_column_text(&q, 0)); |
| 286 | | - } |
| 287 | | - if( P("b")==0 ){ |
| 288 | | - cgi_set_query_parameter("b", db_column_text(&q, 1)); |
| 289 | | - } |
| 290 | | - } |
| 291 | | - db_finalize(&q); |
| 292 | | - } |
| 293 | | - zMime = wiki_filter_mimetypes(P("mimetype")); |
| 294 | | - if( itemId>0 ){ |
| 295 | | - style_header("Edit Forum Post"); |
| 296 | | - }else if( parentId>0 ){ |
| 297 | | - style_header("Comment On Forum Post"); |
| 298 | | - }else{ |
| 299 | | - style_header("New Forum Thread"); |
| 300 | | - } |
| 301 | | - @ <form action="%R/forumedit" method="POST"> |
| 302 | | - if( itemId ){ |
| 303 | | - @ <input type="hidden" name="item" value="%d(itemId)"> |
| 304 | | - } |
| 305 | | - if( parentId ){ |
| 306 | | - @ <input type="hidden" name="replyto" value="%d(parentId)"> |
| 307 | | - } |
| 308 | | - if( P("p") ){ |
| 309 | | - Blob x; |
| 310 | | - @ <div class="forumpreview"> |
| 311 | | - if( P("s") ){ |
| 312 | | - @ <h1>%h(PT("s"))</h1> |
| 313 | | - } |
| 314 | | - @ <div class="forumpreviewbody"> |
| 315 | | - blob_init(&x, PT("b"), -1); |
| 316 | | - wiki_render_by_mimetype(&x, PT("mimetype")); |
| 317 | | - blob_reset(&x); |
| 318 | | - @ </div> |
| 319 | | - @ </div> |
| 320 | | - @ <hr> |
| 321 | | - } |
| 322 | | - @ <table border="0" class="forumeditform"> |
| 323 | | - if( zErr ){ |
| 324 | | - @ <tr><td colspan="2"> |
| 325 | | - @ <span class='forumFormErr'>%h(zErr)</span> |
| 326 | | - } |
| 327 | | - if( (itemId==0 && parentId==0) |
| 328 | | - || (itemId && db_int(0, "SELECT inreplyto IS NULL FROM forumpost" |
| 329 | | - " WHERE mpostid=%d", itemId)) |
| 330 | | - ){ |
| 331 | | - zSub = PT("s"); |
| 332 | | - if( zSub==0 && itemId ){ |
| 333 | | - zSub = db_text("", |
| 334 | | - "SELECT mtitle FROM forumthread" |
| 335 | | - " WHERE mthreadid=(SELECT mthreadid FROM forumpost" |
| 336 | | - " WHERE mpostid=%d)", itemId); |
| 337 | | - } |
| 338 | | - @ <tr><td>Subject:</td> |
| 339 | | - @ <td><input type='text' class='forumFormSubject' name='s' value='%h(zSub)'> |
| 340 | | - } |
| 341 | | - @ <tr><td>Markup:</td><td> |
| 342 | | - mimetype_option_menu(zMime); |
| 343 | | - @ <tr><td>Comment:</td><td> |
| 344 | | - @ <textarea name="b" class="wikiedit" cols="80"\ |
| 345 | | - @ rows="20" wrap="virtual">%h(PD("b",""))</textarea></td> |
| 346 | | - @ <tr><td></td><td> |
| 347 | | - @ <input type="submit" name="p" value="Preview"> |
| 348 | | - if( P("p")!=0 ){ |
| 349 | | - @ <input type="submit" name="x" value="Submit"> |
| 350 | | - } |
| 351 | | - @ <input type="submit" name="cancel" value="Cancel"> |
| 352 | | - @ </table> |
| 353 | | - @ </form> |
| 115 | +void forumedit_page(void){ |
| 116 | + style_header("Pending"); |
| 117 | + @ TBD... |
| 354 | 118 | style_footer(); |
| 355 | 119 | } |
| 356 | 120 | |