Fossil SCM

fossil-scm / src / forum.c
Blame History Raw 2107 lines
1
/*
2
** Copyright (c) 2018 D. Richard Hipp
3
**
4
** This program is free software; you can redistribute it and/or
5
** modify it under the terms of the Simplified BSD License (also
6
** known as the "2-Clause License" or "FreeBSD License".)
7
**
8
** This program is distributed in the hope that it will be useful,
9
** but without any warranty; without even the implied warranty of
10
** merchantability or fitness for a particular purpose.
11
**
12
** Author contact information:
13
** [email protected]
14
** http://www.hwaci.com/drh/
15
**
16
*******************************************************************************
17
**
18
** This file contains code used to generate the user forum.
19
*/
20
#include "config.h"
21
#include <assert.h>
22
#include "forum.h"
23
24
/*
25
** Default to using Markdown markup
26
*/
27
#define DEFAULT_FORUM_MIMETYPE "text/x-markdown"
28
29
#if INTERFACE
30
/*
31
** Each instance of the following object represents a single message -
32
** either the initial post, an edit to a post, a reply, or an edit to
33
** a reply.
34
*/
35
struct ForumPost {
36
int fpid; /* rid for this post */
37
int sid; /* Serial ID number */
38
int rev; /* Revision number */
39
char *zUuid; /* Artifact hash */
40
char *zDisplayName; /* Name of user who wrote this post */
41
double rDate; /* Date for this post */
42
ForumPost *pIrt; /* This post replies to pIrt */
43
ForumPost *pEditHead; /* Original, unedited post */
44
ForumPost *pEditTail; /* Most recent edit for this post */
45
ForumPost *pEditNext; /* This post is edited by pEditNext */
46
ForumPost *pEditPrev; /* This post is an edit of pEditPrev */
47
ForumPost *pNext; /* Next in chronological order */
48
ForumPost *pPrev; /* Previous in chronological order */
49
ForumPost *pDisplay; /* Next in display order */
50
int nEdit; /* Number of edits to this post */
51
int nIndent; /* Number of levels of indentation for this post */
52
int iClosed; /* See forum_rid_is_closed() */
53
};
54
55
/*
56
** A single instance of the following tracks all entries for a thread.
57
*/
58
struct ForumThread {
59
ForumPost *pFirst; /* First post in chronological order */
60
ForumPost *pLast; /* Last post in chronological order */
61
ForumPost *pDisplay; /* Entries in display order */
62
ForumPost *pTail; /* Last on the display list */
63
int mxIndent; /* Maximum indentation level */
64
int nArtifact; /* Number of forum artifacts in this thread */
65
};
66
#endif /* INTERFACE */
67
68
/*
69
** Return true if the forum post with the given rid has been
70
** subsequently edited.
71
*/
72
int forum_rid_has_been_edited(int rid){
73
static Stmt q;
74
int res;
75
db_static_prepare(&q,
76
"SELECT 1 FROM forumpost A, forumpost B"
77
" WHERE A.fpid=$rid AND B.froot=A.froot AND B.fprev=$rid"
78
);
79
db_bind_int(&q, "$rid", rid);
80
res = db_step(&q)==SQLITE_ROW;
81
db_reset(&q);
82
return res;
83
}
84
85
/*
86
** Given a valid forumpost.fpid value, this function returns the first
87
** fpid in the chain of edits for that forum post, or rid if no prior
88
** versions are found.
89
*/
90
static int forumpost_head_rid(int rid){
91
Stmt q;
92
int rcRid = rid;
93
94
db_prepare(&q, "SELECT fprev FROM forumpost"
95
" WHERE fpid=:rid AND fprev IS NOT NULL");
96
db_bind_int(&q, ":rid", rid);
97
while( SQLITE_ROW==db_step(&q) ){
98
rcRid = db_column_int(&q, 0);
99
db_reset(&q);
100
db_bind_int(&q, ":rid", rcRid);
101
}
102
db_finalize(&q);
103
return rcRid;
104
}
105
106
/*
107
** Returns true if p, or any parent of p, has a non-zero iClosed
108
** value. Returns 0 if !p. For an edited chain of post, the tag is
109
** checked on the pEditHead entry, to simplify subsequent unlocking of
110
** the post.
111
**
112
** If bCheckIrt is true then p's thread in-response-to parents are
113
** checked (recursively) for closure, else only p is checked.
114
*/
115
static int forumpost_is_closed(
116
ForumThread *pThread, /* Thread that the post is a member of */
117
ForumPost *p, /* the forum post */
118
int bCheckIrt /* True to check In-Reply-To posts */
119
){
120
int mx = pThread->nArtifact+1;
121
while( p && (mx--)>0 ){
122
if( p->pEditHead ) p = p->pEditHead;
123
if( p->iClosed || !bCheckIrt ) return p->iClosed;
124
p = p->pIrt;
125
}
126
return 0;
127
}
128
129
/*
130
** Given a forum post RID, this function returns true if that post has
131
** (or inherits) an active "closed" tag. If bCheckIrt is true then
132
** the post to which the given post responds is also checked
133
** (recursively), else they are not. When checking in-response-to
134
** posts, the first one which is closed ends the search.
135
**
136
** Note that this function checks _exactly_ the given rid, whereas
137
** forum post closure/re-opening is always applied to the head of an
138
** edit chain so that we get consistent implied locking behavior for
139
** later versions and responses to arbitrary versions in the
140
** chain. Even so, the "closed" tag is applied as a propagating tag
141
** so will apply to all edits in a given chain.
142
**
143
** The return value is one of:
144
**
145
** - 0 if no "closed" tag is found.
146
**
147
** - The tagxref.rowid of the tagxref entry for the closure if rid is
148
** the forum post to which the closure applies.
149
**
150
** - (-tagxref.rowid) if the given rid inherits a "closed" tag from an
151
** IRT forum post.
152
*/
153
static int forum_rid_is_closed(int rid, int bCheckIrt){
154
static Stmt qIrt = empty_Stmt_m;
155
int rc = 0, i = 0;
156
/* TODO: this can probably be turned into a CTE by someone with
157
** superior SQL-fu. */
158
for( ; rid; i++ ){
159
rc = rid_has_active_tag_name(rid, "closed");
160
if( rc || !bCheckIrt ) break;
161
else if( !qIrt.pStmt ) {
162
db_static_prepare(&qIrt,
163
"SELECT firt FROM forumpost "
164
"WHERE fpid=$fpid ORDER BY fmtime DESC"
165
);
166
}
167
db_bind_int(&qIrt, "$fpid", rid);
168
rid = SQLITE_ROW==db_step(&qIrt) ? db_column_int(&qIrt, 0) : 0;
169
db_reset(&qIrt);
170
}
171
return i ? -rc : rc;
172
}
173
174
/*
175
** Closes or re-opens the given forum RID via addition of a new
176
** control artifact into the repository. In order to provide
177
** consistent behavior for implied closing of responses and later
178
** versions, it always acts on the first version of the given forum
179
** post, walking the forumpost.fprev values to find the head of the
180
** chain.
181
**
182
** If doClose is true then a propagating "closed" tag is added, except
183
** as noted below, with the given optional zReason string as the tag's
184
** value. If doClose is false then any active "closed" tag on frid is
185
** cancelled, except as noted below. zReason is ignored if doClose is
186
** false or if zReason is NULL or starts with a NUL byte.
187
**
188
** This function only adds a "closed" tag if forum_rid_is_closed()
189
** indicates that frid's head is not closed. If a parent post is
190
** already closed, no tag is added. Similarly, it will only remove a
191
** "closed" tag from a post which has its own "closed" tag, and will
192
** not remove an inherited one from a parent post.
193
**
194
** If doClose is true and frid is closed (directly or inherited), this
195
** is a no-op. Likewise, if doClose is false and frid itself is not
196
** closed (not accounting for an inherited closed tag), this is a
197
** no-op.
198
**
199
** Returns true if it actually creates a new tag, else false. Fails
200
** fatally on error. If it returns true then any ForumPost::iClosed
201
** values from previously loaded posts are invalidated if they refer
202
** to the amended post or a response to it.
203
**
204
** Sidebars:
205
**
206
** - Unless the caller has a transaction open, via
207
** db_begin_transaction(), there is a very tiny race condition
208
** window during which the caller's idea of whether or not the forum
209
** post is closed may differ from the current repository state.
210
**
211
** - This routine assumes that frid really does refer to a forum post.
212
**
213
** - This routine assumes that frid is not private or pending
214
** moderation.
215
**
216
** - Closure of a forum post requires a propagating "closed" tag to
217
** account for how edits of posts are handled. This differs from
218
** closure of a branch, where a non-propagating tag is used.
219
*/
220
static int forumpost_close(int frid, int doClose, const char *zReason){
221
Blob artifact = BLOB_INITIALIZER; /* Output artifact */
222
Blob cksum = BLOB_INITIALIZER; /* Z-card */
223
int iClosed; /* true if frid is closed */
224
int trid; /* RID of new control artifact */
225
char *zUuid; /* UUID of head version of post */
226
227
db_begin_transaction();
228
frid = forumpost_head_rid(frid);
229
iClosed = forum_rid_is_closed(frid, 1);
230
if( (iClosed && doClose
231
/* Already closed, noting that in the case of (iClosed<0), it's
232
** actually a parent which is closed. */)
233
|| (iClosed<=0 && !doClose
234
/* This entry is not closed, but a parent post may be. */) ){
235
db_end_transaction(0);
236
return 0;
237
}
238
if( doClose==0 || (zReason && !zReason[0]) ){
239
zReason = 0;
240
}
241
zUuid = rid_to_uuid(frid);
242
blob_appendf(&artifact, "D %z\n", date_in_standard_format( "now" ));
243
blob_appendf(&artifact,
244
"T %cclosed %s%s%F\n",
245
doClose ? '*' : '-', zUuid,
246
zReason ? " " : "", zReason ? zReason : "");
247
blob_appendf(&artifact, "U %F\n", login_name());
248
md5sum_blob(&artifact, &cksum);
249
blob_appendf(&artifact, "Z %b\n", &cksum);
250
blob_reset(&cksum);
251
trid = content_put_ex(&artifact, 0, 0, 0, 0);
252
if( trid==0 ){
253
fossil_fatal("Error saving tag artifact: %s", g.zErrMsg);
254
}
255
if( manifest_crosslink(trid, &artifact,
256
MC_NONE /*MC_PERMIT_HOOKS?*/)==0 ){
257
fossil_fatal("%s", g.zErrMsg);
258
}
259
assert( blob_is_reset(&artifact) );
260
db_add_unsent(trid);
261
admin_log("%s forum post %S", doClose ? "Close" : "Re-open", zUuid);
262
fossil_free(zUuid);
263
/* Potential TODO: if (iClosed>0) then we could find the initial tag
264
** artifact and content_deltify(thatRid,&trid,1,0). Given the tiny
265
** size of these artifacts, however, that would save little space,
266
** if any. */
267
db_end_transaction(0);
268
return 1;
269
}
270
271
/*
272
** Returns true if the forum-close-policy setting is true, else false,
273
** caching the result for subsequent calls.
274
*/
275
static int forumpost_close_policy(void){
276
static int closePolicy = -99;
277
278
if( closePolicy==-99 ){
279
closePolicy = db_get_boolean("forum-close-policy",0)>0;
280
}
281
return closePolicy;
282
}
283
284
/*
285
** Returns 1 if the current user is an admin, -1 if the current user
286
** is a forum moderator and the forum-close-policy setting is true,
287
** else returns 0. The value is cached for subsequent calls.
288
*/
289
static int forumpost_may_close(void){
290
static int permClose = -99;
291
if( permClose!=-99 ){
292
return permClose;
293
}else if( g.perm.Admin ){
294
return permClose = 1;
295
}else if( g.perm.ModForum ){
296
return permClose = forumpost_close_policy()>0 ? -1 : 0;
297
}else{
298
return permClose = 0;
299
}
300
}
301
302
/*
303
** Emits a warning that the current forum post is CLOSED and can only
304
** be edited or responded to by an administrator. */
305
static void forumpost_error_closed(void){
306
@ <div class='error'>This (sub)thread is CLOSED and can only be
307
@ edited or replied to by an admin user.</div>
308
}
309
310
/*
311
** Delete a complete ForumThread and all its entries.
312
*/
313
static void forumthread_delete(ForumThread *pThread){
314
ForumPost *pPost, *pNext;
315
for(pPost=pThread->pFirst; pPost; pPost = pNext){
316
pNext = pPost->pNext;
317
fossil_free(pPost->zUuid);
318
fossil_free(pPost->zDisplayName);
319
fossil_free(pPost);
320
}
321
fossil_free(pThread);
322
}
323
324
/*
325
** Search a ForumPost list forwards looking for the post with fpid
326
*/
327
static ForumPost *forumpost_forward(ForumPost *p, int fpid){
328
while( p && p->fpid!=fpid ) p = p->pNext;
329
return p;
330
}
331
332
/*
333
** Search backwards for a ForumPost
334
*/
335
static ForumPost *forumpost_backward(ForumPost *p, int fpid){
336
while( p && p->fpid!=fpid ) p = p->pPrev;
337
return p;
338
}
339
340
/*
341
** Add a post to the display list
342
*/
343
static void forumpost_add_to_display(ForumThread *pThread, ForumPost *p){
344
if( pThread->pDisplay==0 ){
345
pThread->pDisplay = p;
346
}else{
347
pThread->pTail->pDisplay = p;
348
}
349
pThread->pTail = p;
350
}
351
352
/*
353
** Extend the display list for pThread by adding all entries that
354
** reference fpid. The first such post will be no earlier then
355
** post "p".
356
*/
357
static void forumthread_display_order(
358
ForumThread *pThread, /* The complete thread */
359
ForumPost *pBase /* Add replies to this post */
360
){
361
ForumPost *p;
362
ForumPost *pPrev = 0;
363
ForumPost *pBaseIrt;
364
for(p=pBase->pNext; p; p=p->pNext){
365
if( !p->pEditPrev && p->pIrt ){
366
pBaseIrt = p->pIrt->pEditHead ? p->pIrt->pEditHead : p->pIrt;
367
if( pBaseIrt==pBase ){
368
if( pPrev ){
369
pPrev->nIndent = pBase->nIndent + 1;
370
forumpost_add_to_display(pThread, pPrev);
371
forumthread_display_order(pThread, pPrev);
372
}
373
pPrev = p;
374
}
375
}
376
}
377
if( pPrev ){
378
pPrev->nIndent = pBase->nIndent + 1;
379
if( pPrev->nIndent>pThread->mxIndent ) pThread->mxIndent = pPrev->nIndent;
380
forumpost_add_to_display(pThread, pPrev);
381
forumthread_display_order(pThread, pPrev);
382
}
383
}
384
385
/*
386
** Construct a ForumThread object given the root record id.
387
*/
388
static ForumThread *forumthread_create(int froot, int computeHierarchy){
389
ForumThread *pThread;
390
ForumPost *pPost;
391
ForumPost *p;
392
Stmt q;
393
int sid = 1;
394
int firt, fprev;
395
pThread = fossil_malloc( sizeof(*pThread) );
396
memset(pThread, 0, sizeof(*pThread));
397
db_prepare(&q,
398
"SELECT fpid, firt, fprev, (SELECT uuid FROM blob WHERE rid=fpid), fmtime"
399
" FROM forumpost"
400
" WHERE froot=%d ORDER BY fmtime",
401
froot
402
);
403
while( db_step(&q)==SQLITE_ROW ){
404
pPost = fossil_malloc( sizeof(*pPost) );
405
memset(pPost, 0, sizeof(*pPost));
406
pPost->fpid = db_column_int(&q, 0);
407
firt = db_column_int(&q, 1);
408
fprev = db_column_int(&q, 2);
409
pPost->zUuid = fossil_strdup(db_column_text(&q,3));
410
pPost->rDate = db_column_double(&q,4);
411
if( !fprev ) pPost->sid = sid++;
412
pPost->pPrev = pThread->pLast;
413
pPost->pNext = 0;
414
if( pThread->pLast==0 ){
415
pThread->pFirst = pPost;
416
}else{
417
pThread->pLast->pNext = pPost;
418
}
419
pThread->pLast = pPost;
420
pThread->nArtifact++;
421
422
/* Find the in-reply-to post. Default to the topic post if the replied-to
423
** post cannot be found. */
424
if( firt ){
425
pPost->pIrt = pThread->pFirst;
426
for(p=pThread->pFirst; p; p=p->pNext){
427
if( p->fpid==firt ){
428
pPost->pIrt = p;
429
break;
430
}
431
}
432
}
433
434
/* Maintain the linked list of post edits. */
435
if( fprev ){
436
p = forumpost_backward(pPost->pPrev, fprev);
437
p->pEditNext = pPost;
438
pPost->sid = p->sid;
439
pPost->rev = p->rev+1;
440
pPost->nEdit = p->nEdit+1;
441
pPost->pEditPrev = p;
442
pPost->pEditHead = p->pEditHead ? p->pEditHead : p;
443
for(; p; p=p->pEditPrev ){
444
p->nEdit = pPost->nEdit;
445
p->pEditTail = pPost;
446
}
447
}
448
pPost->iClosed = forum_rid_is_closed(pPost->pEditHead
449
? pPost->pEditHead->fpid
450
: pPost->fpid, 1);
451
}
452
db_finalize(&q);
453
454
if( computeHierarchy ){
455
/* Compute the hierarchical display order */
456
pPost = pThread->pFirst;
457
pPost->nIndent = 1;
458
pThread->mxIndent = 1;
459
forumpost_add_to_display(pThread, pPost);
460
forumthread_display_order(pThread, pPost);
461
}
462
463
/* Return the result */
464
return pThread;
465
}
466
467
/*
468
** List all forum threads to standard output.
469
*/
470
static void forum_thread_list(void){
471
Stmt q;
472
db_prepare(&q,
473
" SELECT"
474
" datetime(max(fmtime)),"
475
" sum(fprev IS NULL),"
476
" froot"
477
" FROM forumpost"
478
" GROUP BY froot"
479
" ORDER BY 1;"
480
);
481
fossil_print(" id cnt most recent post\n");
482
fossil_print("------ ---- -------------------\n");
483
while( db_step(&q)==SQLITE_ROW ){
484
fossil_print("%6d %4d %s\n",
485
db_column_int(&q, 2),
486
db_column_int(&q, 1),
487
db_column_text(&q, 0)
488
);
489
}
490
db_finalize(&q);
491
}
492
493
/*
494
** COMMAND: test-forumthread
495
**
496
** Usage: %fossil test-forumthread [THREADID]
497
**
498
** Display a summary of all messages on a thread THREADID. If the
499
** THREADID argument is omitted, then show a list of all threads.
500
**
501
** This command is intended for testing an analysis only.
502
*/
503
void forumthread_cmd(void){
504
int fpid;
505
int froot;
506
const char *zName;
507
ForumThread *pThread;
508
ForumPost *p;
509
510
db_find_and_open_repository(0,0);
511
verify_all_options();
512
if( g.argc==2 ){
513
forum_thread_list();
514
return;
515
}
516
if( g.argc!=3 ) usage("THREADID");
517
zName = g.argv[2];
518
fpid = symbolic_name_to_rid(zName, "f");
519
if( fpid<=0 ){
520
fpid = db_int(0, "SELECT rid FROM blob WHERE rid=%d", atoi(zName));
521
}
522
if( fpid<=0 ){
523
fossil_fatal("unknown or ambiguous forum id: \"%s\"", zName);
524
}
525
froot = db_int(0, "SELECT froot FROM forumpost WHERE fpid=%d", fpid);
526
if( froot==0 ){
527
fossil_fatal("Not a forum post: \"%s\"", zName);
528
}
529
fossil_print("fpid = %d\n", fpid);
530
fossil_print("froot = %d\n", froot);
531
pThread = forumthread_create(froot, 1);
532
fossil_print("count = %d\n", pThread->nArtifact);
533
fossil_print("Chronological:\n");
534
fossil_print(
535
/* 0 1 2 3 4 5 6 7 */
536
/* 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123 */
537
" sid rev closed fpid pIrt pEditPrev pEditTail hash\n");
538
for(p=pThread->pFirst; p; p=p->pNext){
539
fossil_print("%4d %4d %7d %9d %9d %9d %9d %8.8s\n",
540
p->sid, p->rev,
541
p->iClosed,
542
p->fpid, p->pIrt ? p->pIrt->fpid : 0,
543
p->pEditPrev ? p->pEditPrev->fpid : 0,
544
p->pEditTail ? p->pEditTail->fpid : 0, p->zUuid);
545
}
546
fossil_print("\nDisplay\n");
547
for(p=pThread->pDisplay; p; p=p->pDisplay){
548
fossil_print("%*s", (p->nIndent-1)*3, "");
549
if( p->pEditTail ){
550
fossil_print("%d->%d", p->fpid, p->pEditTail->fpid);
551
}else{
552
fossil_print("%d", p->fpid);
553
}
554
if( p->iClosed ){
555
fossil_print(" [closed%s]", p->iClosed<0 ? " via parent" : "");
556
}
557
fossil_print("\n");
558
}
559
forumthread_delete(pThread);
560
}
561
562
/*
563
** WEBPAGE: forumthreadhashlist
564
**
565
** Usage: /forumthreadhashlist/HASH-OF-ROOT
566
**
567
** This page (accessibly only to admins) shows a list of all artifacts
568
** associated with a single forum thread. An admin might copy/paste this
569
** list into the /shun page in order to shun an entire thread.
570
*/
571
void forumthreadhashlist(void){
572
int fpid;
573
int froot;
574
const char *zName = P("name");
575
ForumThread *pThread;
576
ForumPost *p;
577
char *fuuid;
578
Stmt q;
579
580
login_check_credentials();
581
if( !g.perm.Admin ){
582
return;
583
}
584
if( zName==0 ){
585
webpage_error("Missing \"name=\" query parameter");
586
}
587
fpid = symbolic_name_to_rid(zName, "f");
588
if( fpid<=0 ){
589
if( fpid==0 ){
590
webpage_notfound_error("Unknown forum id: \"%s\"", zName);
591
}else{
592
ambiguous_page();
593
}
594
return;
595
}
596
froot = db_int(0, "SELECT froot FROM forumpost WHERE fpid=%d", fpid);
597
if( froot==0 ){
598
webpage_notfound_error("Not a forum post: \"%s\"", zName);
599
}
600
fuuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", froot);
601
style_set_current_feature("forum");
602
style_header("Artifacts Of Forum Thread");
603
@ <h2>
604
@ Artifacts associated with the forum thread
605
@ <a href="%R/forumthread/%S(fuuid)">%S(fuuid)</a>:</h2>
606
@ <pre>
607
pThread = forumthread_create(froot, 1);
608
for(p=pThread->pFirst; p; p=p->pNext){
609
@ %h(p->zUuid)
610
}
611
forumthread_delete(pThread);
612
@ </pre>
613
@ <hr>
614
@ <h2>Related FORUMPOST Table Content</h2>
615
@ <table border="1" cellpadding="4" cellspacing="0">
616
@ <tr><th>fpid<th>froot<th>fprev<th>firt<th>fmtime
617
db_prepare(&q, "SELECT fpid, froot, fprev, firt, datetime(fmtime)"
618
" FROM forumpost"
619
" WHERE froot=%d"
620
" ORDER BY fmtime", froot);
621
while( db_step(&q)==SQLITE_ROW ){
622
@ <tr><td>%d(db_column_int(&q,0))\
623
@ <td>%d(db_column_int(&q,1))\
624
@ <td>%d(db_column_int(&q,2))\
625
@ <td>%d(db_column_int(&q,3))\
626
@ <td>%h(db_column_text(&q,4))</tr>
627
}
628
@ </table>
629
db_finalize(&q);
630
style_finish_page();
631
}
632
633
/*
634
** Render a forum post for display
635
*/
636
void forum_render(
637
const char *zTitle, /* The title. Might be NULL for no title */
638
const char *zMimetype, /* Mimetype of the message */
639
const char *zContent, /* Content of the message */
640
const char *zClass, /* Put in a <div> if not NULL */
641
int bScroll /* Large message content scrolls if true */
642
){
643
if( zClass ){
644
@ <div class='%s(zClass)'>
645
}
646
if( zTitle ){
647
if( zTitle[0] ){
648
@ <h1>%h(zTitle)</h1>
649
}else{
650
@ <h1><i>Deleted</i></h1>
651
}
652
}
653
if( zContent && zContent[0] ){
654
Blob x;
655
const int isFossilWiki = zMimetype==0
656
|| fossil_strcmp(zMimetype, "text/x-fossil-wiki")==0;
657
if( bScroll ){
658
@ <div class='forumPostBody'>
659
}else{
660
@ <div class='forumPostFullBody'>
661
}
662
blob_init(&x, 0, 0);
663
blob_append(&x, zContent, -1);
664
safe_html_context(DOCSRC_FORUM);
665
if( isFossilWiki ){
666
/* Markdown and plain-text rendering add a wrapper DIV resp. PRE
667
** element around the post, and some CSS relies on its existence
668
** in order to handle expansion/collapse of the post. Fossil
669
** Wiki rendering does not do so, so we must wrap those manually
670
** here. */
671
@ <div class='fossilWiki'>
672
}
673
wiki_render_by_mimetype(&x, zMimetype);
674
if( isFossilWiki ){
675
@ </div>
676
}
677
blob_reset(&x);
678
@ </div>
679
}else{
680
@ <i>Deleted</i>
681
}
682
if( zClass ){
683
@ </div>
684
}
685
}
686
687
/*
688
** Compute a display name from a login name.
689
**
690
** If the input login is found in the USER table, then check the USER.INFO
691
** field to see if it has display-name followed by an email address.
692
** If it does, that becomes the new display name. If not, let the display
693
** name just be the login.
694
**
695
** Space to hold the returned name is obtained from fossil_strdup() or
696
** mprintf() and should be freed by the caller.
697
**
698
** HTML markup within the reply has been property escaped. Hyperlinks
699
** may have been added. The result is safe for use with %s.
700
*/
701
static char *display_name_from_login(const char *zLogin){
702
static Stmt q;
703
char *zResult;
704
db_static_prepare(&q,
705
"SELECT display_name(info) FROM user WHERE login=$login"
706
);
707
db_bind_text(&q, "$login", zLogin);
708
if( db_step(&q)==SQLITE_ROW && db_column_type(&q,0)==SQLITE_TEXT ){
709
const char *zDisplay = db_column_text(&q,0);
710
if( fossil_strcmp(zDisplay,zLogin)==0 ){
711
zResult = mprintf("%z%h</a>",
712
href("%R/timeline?ss=v&y=f&vfx&u=%t",zLogin),zLogin);
713
}else{
714
zResult = mprintf("%s (%z%h</a>)", zDisplay,
715
href("%R/timeline?ss=v&y=f&vfx&u=%t",zLogin),zLogin);
716
}
717
}else{
718
zResult = mprintf("%z%h</a>",
719
href("%R/timeline?ss=v&y=f&vfx&u=%t",zLogin),zLogin);
720
}
721
db_reset(&q);
722
return zResult;
723
}
724
725
/*
726
** Compute and return the display name for a ForumPost. If
727
** pManifest is not NULL, then it is a Manifest object for the post.
728
** if pManifest is NULL, this routine has to fetch and parse the
729
** Manifest object for itself.
730
**
731
** Memory to hold the display name is attached to p->zDisplayName
732
** and will be freed together with the ForumPost object p when it
733
** is freed.
734
**
735
** The returned text has had all HTML markup escaped and is safe for
736
** use within %s.
737
*/
738
static char *forum_post_display_name(ForumPost *p, Manifest *pManifest){
739
Manifest *pToFree = 0;
740
if( p->zDisplayName ) return p->zDisplayName;
741
if( pManifest==0 ){
742
pManifest = pToFree = manifest_get(p->fpid, CFTYPE_FORUM, 0);
743
if( pManifest==0 ) return "(unknown)";
744
}
745
p->zDisplayName = display_name_from_login(pManifest->zUser);
746
if( pToFree ) manifest_destroy(pToFree);
747
if( p->zDisplayName==0 ) return "(unknown)";
748
return p->zDisplayName;
749
}
750
751
752
/*
753
** Display a single post in a forum thread.
754
*/
755
static void forum_display_post(
756
ForumThread *pThread, /* The thread that this post is a member of */
757
ForumPost *p, /* Forum post to display */
758
int iIndentScale, /* Indent scale factor */
759
int bRaw, /* True to omit the border */
760
int bUnf, /* True to leave the post unformatted */
761
int bHist, /* True if showing edit history */
762
int bSelect, /* True if this is the selected post */
763
char *zQuery /* Common query string */
764
){
765
char *zPosterName; /* Name of user who originally made this post */
766
char *zEditorName; /* Name of user who provided the current edit */
767
char *zDate; /* The time/date string */
768
char *zHist; /* History query string */
769
Manifest *pManifest; /* Manifest comprising the current post */
770
int bPrivate; /* True for posts awaiting moderation */
771
int bSameUser; /* True if author is also the reader */
772
int iIndent; /* Indent level */
773
int iClosed; /* True if (sub)thread is closed */
774
const char *zMimetype;/* Formatting MIME type */
775
776
/* Get the manifest for the post. Abort if not found (e.g. shunned). */
777
pManifest = manifest_get(p->fpid, CFTYPE_FORUM, 0);
778
if( !pManifest ) return;
779
iClosed = forumpost_is_closed(pThread, p, 1);
780
/* When not in raw mode, create the border around the post. */
781
if( !bRaw ){
782
/* Open the <div> enclosing the post. Set the class string to mark the post
783
** as selected and/or obsolete. */
784
iIndent = (p->pEditHead ? p->pEditHead->nIndent : p->nIndent)-1;
785
@ <div id='forum%d(p->fpid)' class='forumTime\
786
@ %s(bSelect ? " forumSel" : "")\
787
@ %s(iClosed ? " forumClosed" : "")\
788
@ %s(p->pEditTail ? " forumObs" : "")' \
789
if( iIndent && iIndentScale ){
790
@ style='margin-left:%d(iIndent*iIndentScale)ex;'>
791
}else{
792
@ >
793
}
794
795
/* If this is the first post (or an edit thereof), emit the thread title. */
796
if( pManifest->zThreadTitle ){
797
@ <h1>%h(pManifest->zThreadTitle)</h1>
798
}
799
800
/* Begin emitting the header line. The forum of the title
801
** varies depending on whether:
802
** * The post is unedited
803
** * The post was last edited by the original author
804
** * The post was last edited by a different person
805
*/
806
if( p->pEditHead ){
807
zDate = db_text(0, "SELECT datetime(%.17g,toLocal())",
808
p->pEditHead->rDate);
809
}else{
810
zPosterName = forum_post_display_name(p, pManifest);
811
zEditorName = zPosterName;
812
}
813
zDate = db_text(0, "SELECT datetime(%.17g,toLocal())", p->rDate);
814
if( p->pEditPrev ){
815
zPosterName = forum_post_display_name(p->pEditHead, 0);
816
zEditorName = forum_post_display_name(p, pManifest);
817
zHist = bHist ? "" : zQuery[0]==0 ? "?hist" : "&hist";
818
@ <h3 class='forumPostHdr'>(%d(p->sid)\
819
@ .%0*d(fossil_num_digits(p->nEdit))(p->rev))
820
if( fossil_strcmp(zPosterName, zEditorName)==0 ){
821
@ By %s(zPosterName) on %h(zDate) edited from \
822
@ %z(href("%R/forumpost/%S%s%s",p->pEditPrev->zUuid,zQuery,zHist))\
823
@ %d(p->sid).%0*d(fossil_num_digits(p->nEdit))(p->pEditPrev->rev)</a>
824
}else{
825
@ Originally by %s(zPosterName) \
826
@ with edits by %s(zEditorName) on %h(zDate) from \
827
@ %z(href("%R/forumpost/%S%s%s",p->pEditPrev->zUuid,zQuery,zHist))\
828
@ %d(p->sid).%0*d(fossil_num_digits(p->nEdit))(p->pEditPrev->rev)</a>
829
}
830
}else{
831
zPosterName = forum_post_display_name(p, pManifest);
832
@ <h3 class='forumPostHdr'>(%d(p->sid))
833
@ By %s(zPosterName) on %h(zDate)
834
}
835
fossil_free(zDate);
836
837
838
/* If debugging is enabled, link to the artifact page. */
839
if( g.perm.Debug ){
840
@ <span class="debug">\
841
@ <a href="%R/artifact/%h(p->zUuid)">(artifact-%d(p->fpid))</a></span>
842
}
843
844
/* If this is a reply, refer back to the parent post. */
845
if( p->pIrt ){
846
@ in reply to %z(href("%R/forumpost/%S%s",p->pIrt->zUuid,zQuery))\
847
@ %d(p->pIrt->sid)\
848
if( p->pIrt->nEdit ){
849
@ .%0*d(fossil_num_digits(p->pIrt->nEdit))(p->pIrt->rev)\
850
}
851
@ </a>
852
}
853
854
/* If this post was later edited, refer forward to the next edit. */
855
if( p->pEditNext ){
856
@ updated by %z(href("%R/forumpost/%S%s",p->pEditNext->zUuid,zQuery))\
857
@ %d(p->pEditNext->sid)\
858
@ .%0*d(fossil_num_digits(p->nEdit))(p->pEditNext->rev)</a>
859
}
860
861
/* Provide a link to select the individual post. */
862
if( !bSelect ){
863
@ %z(href("%R/forumpost/%!S%s",p->zUuid,zQuery))[link]</a>
864
}
865
866
/* Provide a link to the raw source code. */
867
if( !bUnf ){
868
@ %z(href("%R/forumpost/%!S?raw",p->zUuid))[source]</a>
869
}
870
@ </h3>
871
}
872
873
/* Check if this post is approved, also if it's by the current user. */
874
bPrivate = content_is_private(p->fpid);
875
bSameUser = login_is_individual()
876
&& fossil_strcmp(pManifest->zUser, g.zLogin)==0;
877
878
/* Render the post if the user is able to see it. */
879
if( bPrivate && !g.perm.ModForum && !bSameUser ){
880
@ <p><span class="modpending">Awaiting Moderator Approval</span></p>
881
}else{
882
if( bRaw || bUnf || p->pEditTail ){
883
zMimetype = "text/plain";
884
}else{
885
zMimetype = pManifest->zMimetype;
886
}
887
forum_render(0, zMimetype, pManifest->zWiki, 0, !bRaw);
888
}
889
890
/* When not in raw mode, finish creating the border around the post. */
891
if( !bRaw ){
892
/* If the user is able to write to the forum and if this post has not been
893
** edited, create a form with various interaction buttons. */
894
if( g.perm.WrForum && !p->pEditTail ){
895
@ <div class="forumpost-single-controls">\
896
@ <form action="%R/forumedit" method="POST">
897
@ <input type="hidden" name="fpid" value="%s(p->zUuid)">
898
if( !bPrivate ){
899
/* Reply and Edit are only available if the post has been
900
** approved. Closed threads can only be edited or replied to
901
** if forumpost_may_close() is true but a user may delete
902
** their own posts even if they are closed. */
903
if( forumpost_may_close() || !iClosed ){
904
@ <input type="submit" name="reply" value="Reply">
905
if( g.perm.Admin || (bSameUser && !iClosed) ){
906
@ <input type="submit" name="edit" value="Edit">
907
}
908
if( g.perm.Admin || bSameUser ){
909
@ <input type="submit" name="nullout" value="Delete">
910
}
911
}
912
}else if( g.perm.ModForum ){
913
/* Allow moderators to approve or reject pending posts. Also allow
914
** forum supervisors to mark non-special users as trusted and therefore
915
** able to post unmoderated. */
916
@ <input type="submit" name="approve" value="Approve">
917
@ <input type="submit" name="reject" value="Reject">
918
if( g.perm.AdminForum && !login_is_special(pManifest->zUser) ){
919
@ <br><label><input type="checkbox" name="trust">
920
@ Trust user "%h(pManifest->zUser)" so that future posts by \
921
@ "%h(pManifest->zUser)" do not require moderation.
922
@ </label>
923
@ <input type="hidden" name="trustuser" value="%h(pManifest->zUser)">
924
}
925
}else if( bSameUser ){
926
/* Allow users to delete (reject) their own pending posts. */
927
@ <input type="submit" name="reject" value="Delete">
928
}
929
login_insert_csrf_secret();
930
@ </form>
931
if( bSelect && forumpost_may_close() && iClosed>=0 ){
932
int iHead = forumpost_head_rid(p->fpid);
933
@ <form method="post" \
934
@ action='%R/forumpost_%s(iClosed > 0 ? "reopen" : "close")'>
935
login_insert_csrf_secret();
936
@ <input type="hidden" name="fpid" value="%z(rid_to_uuid(iHead))" />
937
if( moderation_pending(p->fpid)==0 ){
938
@ <input type="button" value='%s(iClosed ? "Re-open" : "Close")' \
939
@ class='%s(iClosed ? "action-reopen" : "action-close")'/>
940
}
941
@ </form>
942
}
943
@ </div>
944
}
945
@ </div>
946
}
947
948
/* Clean up. */
949
manifest_destroy(pManifest);
950
}
951
952
/*
953
** Possible display modes for forum_display_thread().
954
*/
955
enum {
956
FD_RAW, /* Like FD_SINGLE, but additionally omit the border, force
957
** unformatted mode, and inhibit history mode */
958
FD_SINGLE, /* Render a single post and (optionally) its edit history */
959
FD_CHRONO, /* Render all posts in chronological order */
960
FD_HIER, /* Render all posts in an indented hierarchy */
961
};
962
963
/*
964
** Display a forum thread. If mode is FD_RAW or FD_SINGLE, display only a
965
** single post from the thread and (optionally) its edit history.
966
*/
967
static void forum_display_thread(
968
int froot, /* Forum thread root post ID */
969
int fpid, /* Selected forum post ID, or 0 if none selected */
970
int mode, /* Forum display mode, one of the FD_* enumerations */
971
int autoMode, /* mode was selected automatically */
972
int bUnf, /* True if rendering unformatted */
973
int bHist /* True if showing edit history, ignored for FD_RAW */
974
){
975
ForumThread *pThread; /* Thread structure */
976
ForumPost *pSelect; /* Currently selected post, or NULL if none */
977
ForumPost *p; /* Post iterator pointer */
978
char zQuery[30]; /* Common query string */
979
int iIndentScale = 4; /* Indent scale factor, measured in "ex" units */
980
int sid; /* Comparison serial ID */
981
int i;
982
983
/* In raw mode, force unformatted display and disable history. */
984
if( mode == FD_RAW ){
985
bUnf = 1;
986
bHist = 0;
987
}
988
989
/* Thread together the posts and (optionally) compute the hierarchy. */
990
pThread = forumthread_create(froot, mode==FD_HIER);
991
992
/* Compute the appropriate indent scaling. */
993
if( mode==FD_HIER ){
994
iIndentScale = 4;
995
while( iIndentScale>1 && iIndentScale*pThread->mxIndent>25 ){
996
iIndentScale--;
997
}
998
}else{
999
iIndentScale = 0;
1000
}
1001
1002
/* Find the selected post, or (depending on parameters) its latest edit. */
1003
pSelect = fpid ? forumpost_forward(pThread->pFirst, fpid) : 0;
1004
if( !bHist && mode!=FD_RAW && pSelect && pSelect->pEditTail ){
1005
pSelect = pSelect->pEditTail;
1006
}
1007
1008
/* When displaying only a single post, abort if no post was selected or the
1009
** selected forum post does not exist in the thread. Otherwise proceed to
1010
** display the entire thread without marking any posts as selected. */
1011
if( !pSelect && (mode==FD_RAW || mode==FD_SINGLE) ){
1012
return;
1013
}
1014
1015
/* Create the common query string to append to nearly all post links. */
1016
i = 0;
1017
if( !autoMode ){
1018
char m = 'a';
1019
switch( mode ){
1020
case FD_RAW: m = 'r'; break;
1021
case FD_CHRONO: m = 'c'; break;
1022
case FD_HIER: m = 'h'; break;
1023
case FD_SINGLE: m = 's'; break;
1024
}
1025
zQuery[i++] = '?';
1026
zQuery[i++] = 't';
1027
zQuery[i++] = '=';
1028
zQuery[i++] = m;
1029
}
1030
if( bUnf ){
1031
zQuery[i] = i==0 ? '?' : '&'; i++;
1032
zQuery[i++] = 'u';
1033
zQuery[i++] = 'n';
1034
zQuery[i++] = 'f';
1035
}
1036
if( bHist ){
1037
zQuery[i] = i==0 ? '?' : '&'; i++;
1038
zQuery[i++] = 'h';
1039
zQuery[i++] = 'i';
1040
zQuery[i++] = 's';
1041
zQuery[i++] = 't';
1042
}
1043
assert( i<(int)sizeof(zQuery) );
1044
zQuery[i] = 0;
1045
assert( zQuery[0]==0 || zQuery[0]=='?' );
1046
1047
/* Identify which post to display first. If history is shown, start with the
1048
** original, unedited post. Otherwise advance to the post's latest edit. */
1049
if( mode==FD_RAW || mode==FD_SINGLE ){
1050
p = pSelect;
1051
if( bHist && p->pEditHead ) p = p->pEditHead;
1052
}else{
1053
p = mode==FD_CHRONO ? pThread->pFirst : pThread->pDisplay;
1054
if( !bHist && p->pEditTail ) p = p->pEditTail;
1055
}
1056
1057
/* Display the appropriate subset of posts in sequence. */
1058
while( p ){
1059
/* Display the post. */
1060
forum_display_post(pThread, p, iIndentScale, mode==FD_RAW,
1061
bUnf, bHist, p==pSelect, zQuery);
1062
1063
/* Advance to the next post in the thread. */
1064
if( mode==FD_CHRONO ){
1065
/* Chronological mode: display posts (optionally including edits) in their
1066
** original commit order. */
1067
if( bHist ){
1068
p = p->pNext;
1069
}else{
1070
sid = p->sid;
1071
if( p->pEditHead ) p = p->pEditHead;
1072
do p = p->pNext; while( p && p->sid<=sid );
1073
if( p && p->pEditTail ) p = p->pEditTail;
1074
}
1075
}else if( bHist && p->pEditNext ){
1076
/* Hierarchical and single mode: display each post's edits in sequence. */
1077
p = p->pEditNext;
1078
}else if( mode==FD_HIER ){
1079
/* Hierarchical mode: after displaying with each post (optionally
1080
** including edits), go to the next post in computed display order. */
1081
p = p->pEditHead ? p->pEditHead->pDisplay : p->pDisplay;
1082
if( !bHist && p && p->pEditTail ) p = p->pEditTail;
1083
}else{
1084
/* Single and raw mode: terminate after displaying the selected post and
1085
** (optionally) its edits. */
1086
break;
1087
}
1088
}
1089
1090
/* Undocumented "threadtable" query parameter causes thread table to be
1091
** displayed for debugging purposes. */
1092
if( PB("threadtable") ){
1093
@ <hr>
1094
@ <table border="1" cellpadding="3" cellspacing="0">
1095
@ <tr><th>sid<th>rev<th>fpid<th>pIrt<th>pEditHead<th>pEditTail\
1096
@ <th>pEditNext<th>pEditPrev<th>pDisplay<th>hash
1097
for(p=pThread->pFirst; p; p=p->pNext){
1098
@ <tr><td>%d(p->sid)<td>%d(p->rev)<td>%d(p->fpid)\
1099
@ <td>%d(p->pIrt ? p->pIrt->fpid : 0)\
1100
@ <td>%d(p->pEditHead ? p->pEditHead->fpid : 0)\
1101
@ <td>%d(p->pEditTail ? p->pEditTail->fpid : 0)\
1102
@ <td>%d(p->pEditNext ? p->pEditNext->fpid : 0)\
1103
@ <td>%d(p->pEditPrev ? p->pEditPrev->fpid : 0)\
1104
@ <td>%d(p->pDisplay ? p->pDisplay->fpid : 0)\
1105
@ <td>%S(p->zUuid)</tr>
1106
}
1107
@ </table>
1108
}
1109
1110
/* Clean up. */
1111
forumthread_delete(pThread);
1112
}
1113
1114
/*
1115
** Emit Forum Javascript which applies (or optionally can apply)
1116
** to all forum-related pages. It does not include page-specific
1117
** code (e.g. "forum.js").
1118
*/
1119
static void forum_emit_js(void){
1120
builtin_fossil_js_bundle_or("copybutton", "pikchr", "confirmer",
1121
NULL);
1122
builtin_request_js("fossil.page.forumpost.js");
1123
}
1124
1125
/*
1126
** WEBPAGE: forumpost
1127
**
1128
** Show a single forum posting. The posting is shown in context with
1129
** its entire thread. The selected posting is enclosed within
1130
** <div class='forumSel'>...</div>. Javascript is used to move the
1131
** selected posting into view after the page loads.
1132
**
1133
** Query parameters:
1134
**
1135
** name=X REQUIRED. The hash of the post to display.
1136
** t=a Automatic display mode, i.e. hierarchical for
1137
** desktop and chronological for mobile. This is the
1138
** default if the "t" query parameter is omitted.
1139
** t=c Show posts in the order they were written.
1140
** t=h Show posts using hierarchical indenting.
1141
** t=s Show only the post specified by "name=X".
1142
** t=r Alias for "t=c&unf&hist".
1143
** t=y Alias for "t=s&unf&hist".
1144
** raw Alias for "t=s&unf". Additionally, omit the border
1145
** around the post, and ignore "t" and "hist".
1146
** unf Show the original, unformatted source text.
1147
** hist Show edit history in addition to current posts.
1148
*/
1149
void forumpost_page(void){
1150
forumthread_page();
1151
}
1152
1153
/*
1154
** WEBPAGE: forumthread
1155
**
1156
** Show all forum messages associated with a particular message thread.
1157
** The result is basically the same as /forumpost except that none of
1158
** the postings in the thread are selected.
1159
**
1160
** Query parameters:
1161
**
1162
** name=X REQUIRED. The hash of any post of the thread.
1163
** t=a Automatic display mode, i.e. hierarchical for
1164
** desktop and chronological for mobile. This is the
1165
** default if the "t" query parameter is omitted.
1166
** t=c Show posts in the order they were written.
1167
** t=h Show posts using hierarchical indenting.
1168
** unf Show the original, unformatted source text.
1169
** hist Show edit history in addition to current posts.
1170
*/
1171
void forumthread_page(void){
1172
int fpid;
1173
int froot;
1174
char *zThreadTitle;
1175
const char *zName = P("name");
1176
const char *zMode = PD("t","a");
1177
int bRaw = PB("raw");
1178
int bUnf = PB("unf");
1179
int bHist = PB("hist");
1180
int mode = 0;
1181
int autoMode = 0;
1182
login_check_credentials();
1183
if( !g.perm.RdForum ){
1184
login_needed(g.anon.RdForum);
1185
return;
1186
}
1187
if( zName==0 ){
1188
webpage_error("Missing \"name=\" query parameter");
1189
}
1190
cgi_check_for_malice();
1191
fpid = symbolic_name_to_rid(zName, "f");
1192
if( fpid<=0 ){
1193
if( fpid==0 ){
1194
webpage_notfound_error("Unknown forum id: \"%s\"", zName);
1195
}else{
1196
ambiguous_page();
1197
}
1198
return;
1199
}
1200
froot = db_int(0, "SELECT froot FROM forumpost WHERE fpid=%d", fpid);
1201
if( froot==0 ){
1202
webpage_notfound_error("Not a forum post: \"%s\"", zName);
1203
}
1204
1205
/* Decode the mode parameters. */
1206
if( bRaw ){
1207
mode = FD_RAW;
1208
bUnf = 1;
1209
bHist = 0;
1210
cgi_replace_query_parameter("unf", "on");
1211
cgi_delete_query_parameter("hist");
1212
cgi_delete_query_parameter("raw");
1213
}else{
1214
switch( *zMode ){
1215
case 'a': mode = cgi_from_mobile() ? FD_CHRONO : FD_HIER;
1216
autoMode=1; break;
1217
case 'c': mode = FD_CHRONO; break;
1218
case 'h': mode = FD_HIER; break;
1219
case 's': mode = FD_SINGLE; break;
1220
case 'r': mode = FD_CHRONO; break;
1221
case 'y': mode = FD_SINGLE; break;
1222
default: webpage_error("Invalid thread mode: \"%s\"", zMode);
1223
}
1224
if( *zMode=='r' || *zMode=='y') {
1225
bUnf = 1;
1226
bHist = 1;
1227
cgi_replace_query_parameter("t", mode==FD_CHRONO ? "c" : "s");
1228
cgi_replace_query_parameter("unf", "on");
1229
cgi_replace_query_parameter("hist", "on");
1230
}
1231
}
1232
1233
/* Define the page header. */
1234
zThreadTitle = db_text("",
1235
"SELECT"
1236
" substr(event.comment,instr(event.comment,':')+2)"
1237
" FROM forumpost, event"
1238
" WHERE event.objid=forumpost.fpid"
1239
" AND forumpost.fpid=%d;",
1240
fpid
1241
);
1242
style_set_current_feature("forum");
1243
style_header("%s%s", zThreadTitle, *zThreadTitle ? "" : "Forum");
1244
fossil_free(zThreadTitle);
1245
if( mode!=FD_CHRONO ){
1246
style_submenu_element("Chronological", "%R/%s/%s?t=c%s%s", g.zPath, zName,
1247
bUnf ? "&unf" : "", bHist ? "&hist" : "");
1248
}
1249
if( mode!=FD_HIER ){
1250
style_submenu_element("Hierarchical", "%R/%s/%s?t=h%s%s", g.zPath, zName,
1251
bUnf ? "&unf" : "", bHist ? "&hist" : "");
1252
}
1253
style_submenu_checkbox("unf", "Unformatted", 0, 0);
1254
style_submenu_checkbox("hist", "History", 0, 0);
1255
if( g.perm.Admin ){
1256
style_submenu_element("Artifacts", "%R/forumthreadhashlist/%t", zName);
1257
}
1258
1259
/* Display the thread. */
1260
if( fossil_strcmp(g.zPath,"forumthread")==0 ) fpid = 0;
1261
forum_display_thread(froot, fpid, mode, autoMode, bUnf, bHist);
1262
1263
/* Emit Forum Javascript. */
1264
builtin_request_js("forum.js");
1265
forum_emit_js();
1266
1267
/* Emit the page style. */
1268
style_finish_page();
1269
}
1270
1271
/*
1272
** Return true if a forum post should be moderated.
1273
*/
1274
static int forum_need_moderation(void){
1275
if( P("domod") ) return 1;
1276
if( g.perm.WrTForum ) return 0;
1277
if( g.perm.ModForum ) return 0;
1278
return 1;
1279
}
1280
1281
/*
1282
** Return true if the string is white-space only.
1283
*/
1284
static int whitespace_only(const char *z){
1285
if( z==0 ) return 1;
1286
while( z[0] && fossil_isspace(z[0]) ){ z++; }
1287
return z[0]==0;
1288
}
1289
1290
/* Flags for use with forum_post() */
1291
#define FPOST_NO_ALERT 1 /* do not send any alerts */
1292
1293
/*
1294
** Return a flags value for use with the final argument to
1295
** forum_post(), extracted from the CGI environment.
1296
*/
1297
static int forum_post_flags(void){
1298
int iPostFlags = 0;
1299
if( g.perm.Debug && P("fpsilent")!=0 ){
1300
iPostFlags |= FPOST_NO_ALERT;
1301
}
1302
return iPostFlags;
1303
}
1304
1305
/*
1306
** Add a new Forum Post artifact to the repository.
1307
**
1308
** Return true if a redirect occurs.
1309
*/
1310
static int forum_post(
1311
const char *zTitle, /* Title. NULL for replies */
1312
int iInReplyTo, /* Post replying to. 0 for new threads */
1313
int iEdit, /* Post being edited, or zero for a new post */
1314
const char *zUser, /* Username. NULL means use login name */
1315
const char *zMimetype, /* Mimetype of content. */
1316
const char *zContent, /* Content */
1317
int iFlags /* FPOST_xyz flag values */
1318
){
1319
char *zDate;
1320
char *zI;
1321
char *zG;
1322
int iBasis;
1323
Blob x, cksum, formatCheck, errMsg;
1324
Manifest *pPost;
1325
int nContent = zContent ? (int)strlen(zContent) : 0;
1326
1327
schema_forum();
1328
if( !g.perm.Admin && (iEdit || iInReplyTo)
1329
&& forum_rid_is_closed(iEdit ? iEdit : iInReplyTo, 1) ){
1330
forumpost_error_closed();
1331
return 0;
1332
}
1333
if( iEdit==0 && whitespace_only(zContent) ){
1334
return 0;
1335
}
1336
if( iInReplyTo==0 && iEdit>0 ){
1337
iBasis = iEdit;
1338
iInReplyTo = db_int(0, "SELECT firt FROM forumpost WHERE fpid=%d", iEdit);
1339
}else{
1340
iBasis = iInReplyTo;
1341
}
1342
webpage_assert( (zTitle==0)+(iInReplyTo==0)==1 );
1343
blob_init(&x, 0, 0);
1344
zDate = date_in_standard_format("now");
1345
blob_appendf(&x, "D %s\n", zDate);
1346
fossil_free(zDate);
1347
zG = db_text(0,
1348
"SELECT uuid FROM blob, forumpost"
1349
" WHERE blob.rid==forumpost.froot"
1350
" AND forumpost.fpid=%d", iBasis);
1351
if( zG ){
1352
blob_appendf(&x, "G %s\n", zG);
1353
fossil_free(zG);
1354
}
1355
if( zTitle ){
1356
blob_appendf(&x, "H %F\n", zTitle);
1357
}
1358
zI = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", iInReplyTo);
1359
if( zI ){
1360
blob_appendf(&x, "I %s\n", zI);
1361
fossil_free(zI);
1362
}
1363
if( fossil_strcmp(zMimetype,"text/x-fossil-wiki")!=0 ){
1364
blob_appendf(&x, "N %s\n", zMimetype);
1365
}
1366
if( iEdit>0 ){
1367
char *zP = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", iEdit);
1368
if( zP==0 ) webpage_error("missing edit artifact %d", iEdit);
1369
blob_appendf(&x, "P %s\n", zP);
1370
fossil_free(zP);
1371
}
1372
if( zUser==0 ){
1373
if( login_is_nobody() ){
1374
zUser = "anonymous";
1375
}else{
1376
zUser = login_name();
1377
}
1378
}
1379
blob_appendf(&x, "U %F\n", zUser);
1380
blob_appendf(&x, "W %d\n%s\n", nContent, zContent);
1381
md5sum_blob(&x, &cksum);
1382
blob_appendf(&x, "Z %b\n", &cksum);
1383
blob_reset(&cksum);
1384
1385
/* Verify that the artifact we are creating is well-formed */
1386
blob_init(&formatCheck, 0, 0);
1387
blob_init(&errMsg, 0, 0);
1388
blob_copy(&formatCheck, &x);
1389
pPost = manifest_parse(&formatCheck, 0, &errMsg);
1390
if( pPost==0 ){
1391
webpage_error("malformed forum post artifact - %s", blob_str(&errMsg));
1392
}
1393
webpage_assert( pPost->type==CFTYPE_FORUM );
1394
manifest_destroy(pPost);
1395
1396
if( P("dryrun") ){
1397
@ <div class='debug'>
1398
@ This is the artifact that would have been generated:
1399
@ <pre>%h(blob_str(&x))</pre>
1400
@ </div>
1401
blob_reset(&x);
1402
return 0;
1403
}else{
1404
int nrid;
1405
db_begin_transaction();
1406
nrid = wiki_put(&x, iEdit>0 ? iEdit : 0, forum_need_moderation());
1407
blob_reset(&x);
1408
if( (iFlags & FPOST_NO_ALERT)!=0 ){
1409
alert_unqueue('f', nrid);
1410
}
1411
db_commit_transaction();
1412
cgi_redirectf("%R/forumpost/%S", rid_to_uuid(nrid));
1413
return 1;
1414
}
1415
}
1416
1417
/*
1418
** Paint the form elements for entering a Forum post
1419
*/
1420
static void forum_post_widget(
1421
const char *zTitle,
1422
const char *zMimetype,
1423
const char *zContent
1424
){
1425
if( zTitle ){
1426
@ Title: <input type="input" name="title" value="%h(zTitle)" size="50"
1427
@ maxlength="125"><br>
1428
}
1429
@ %z(href("%R/markup_help"))Markup style</a>:
1430
mimetype_option_menu(zMimetype, "mimetype");
1431
@ <div class="forum-editor-widget">
1432
@ <textarea aria-label="Content:" name="content" class="wikiedit" \
1433
@ cols="80" rows="25" wrap="virtual">%h(zContent)</textarea></div>
1434
}
1435
1436
/*
1437
** WEBPAGE: forumpost_close hidden
1438
** WEBPAGE: forumpost_reopen hidden
1439
**
1440
** fpid=X Hash of the post to be edited. REQUIRED
1441
** reason=X Optional reason for closure.
1442
**
1443
** Closes or re-opens the given forum post, within the bounds of the
1444
** API for forumpost_close(). After (perhaps) modifying the "closed"
1445
** status of the given thread, it redirects to that post's thread
1446
** view. Requires admin privileges.
1447
*/
1448
void forum_page_close(void){
1449
const char *zFpid = PD("fpid","");
1450
const char *zReason = 0;
1451
int fClose;
1452
int fpid;
1453
1454
login_check_credentials();
1455
if( forumpost_may_close()==0 ){
1456
login_needed(g.anon.Admin);
1457
return;
1458
}
1459
cgi_csrf_verify();
1460
fpid = symbolic_name_to_rid(zFpid, "f");
1461
if( fpid<=0 ){
1462
webpage_error("Missing or invalid fpid query parameter");
1463
}
1464
fClose = sqlite3_strglob("*_close*", g.zPath)==0;
1465
if( fClose ) zReason = PD("reason",0);
1466
forumpost_close(fpid, fClose, zReason);
1467
cgi_redirectf("%R/forumpost/%S",zFpid);
1468
return;
1469
}
1470
1471
/*
1472
** WEBPAGE: forumnew
1473
** WEBPAGE: forumedit
1474
**
1475
** Start a new thread on the forum or reply to an existing thread.
1476
** But first prompt to see if the user would like to log in.
1477
*/
1478
void forum_page_init(void){
1479
int isEdit;
1480
char *zGoto;
1481
1482
login_check_credentials();
1483
if( !g.perm.WrForum ){
1484
login_needed(g.anon.WrForum);
1485
return;
1486
}
1487
if( sqlite3_strglob("*edit*", g.zPath)==0 ){
1488
zGoto = mprintf("forume2?fpid=%S",PD("fpid",""));
1489
isEdit = 1;
1490
}else{
1491
zGoto = mprintf("forume1");
1492
isEdit = 0;
1493
}
1494
if( login_is_individual() ){
1495
if( isEdit ){
1496
forumedit_page();
1497
}else{
1498
forumnew_page();
1499
}
1500
return;
1501
}
1502
style_set_current_feature("forum");
1503
style_header("%h As Anonymous?", isEdit ? "Reply" : "Post");
1504
@ <p>You are not logged in.
1505
@ <p><table border="0" cellpadding="10">
1506
@ <tr><td>
1507
@ <form action="%s(zGoto)" method="POST">
1508
@ <input type="submit" value="Remain Anonymous">
1509
@ </form>
1510
@ <td>Post to the forum anonymously
1511
if( login_self_register_available(0) ){
1512
@ <tr><td>
1513
@ <form action="%R/register" method="POST">
1514
@ <input type="hidden" name="g" value="%s(zGoto)">
1515
@ <input type="submit" value="Create An Account">
1516
@ </form>
1517
@ <td>Create a new account and post using that new account
1518
}
1519
@ <tr><td>
1520
@ <form action="%R/login" method="POST">
1521
@ <input type="hidden" name="g" value="%s(zGoto)">
1522
@ <input type="hidden" name="noanon" value="1">
1523
@ <input type="submit" value="Login">
1524
@ </form>
1525
@ <td>Log into an existing account
1526
@ </table>
1527
forum_emit_js();
1528
style_finish_page();
1529
fossil_free(zGoto);
1530
}
1531
1532
/*
1533
** Write the "From: USER" line on the webpage.
1534
*/
1535
static void forum_from_line(void){
1536
if( login_is_nobody() ){
1537
@ From: anonymous<br>
1538
}else{
1539
@ From: %h(login_name())<br>
1540
}
1541
}
1542
1543
static void forum_render_debug_options(void){
1544
if( g.perm.Debug ){
1545
/* Give extra control over the post to users with the special
1546
* Debug capability, which includes Admin and Setup users */
1547
@ <div class="debug">
1548
@ <label><input type="checkbox" name="dryrun" %s(PCK("dryrun"))> \
1549
@ Dry run</label>
1550
@ <br><label><input type="checkbox" name="domod" %s(PCK("domod"))> \
1551
@ Require moderator approval</label>
1552
@ <br><label><input type="checkbox" name="showqp" %s(PCK("showqp"))> \
1553
@ Show query parameters</label>
1554
@ <br><label><input type="checkbox" name="fpsilent" %s(PCK("fpsilent"))> \
1555
@ Do not send notification emails</label>
1556
@ </div>
1557
}
1558
}
1559
1560
static void forum_render_notification_reminder(void){
1561
if( alert_tables_exist() ){
1562
@ <div class='debug'>Creating and editing forum posts may send notifications
1563
@ to an arbitrary number of subscribers. To reduce subscriber annoyance and
1564
@ the risk of this forum's messages being flagged as spam by mail providers,
1565
@ <em>please refrain from making multiple new posts or edits in rapid
1566
@ succession</em>.
1567
@ </div>
1568
}
1569
}
1570
1571
/*
1572
** WEBPAGE: forume1
1573
**
1574
** Start a new forum thread.
1575
*/
1576
void forumnew_page(void){
1577
const char *zTitle = PDT("title","");
1578
const char *zMimetype = PD("mimetype",DEFAULT_FORUM_MIMETYPE);
1579
const char *zContent = PDT("content","");
1580
1581
login_check_credentials();
1582
if( !g.perm.WrForum ){
1583
login_needed(g.anon.WrForum);
1584
return;
1585
}
1586
if( P("submit") && cgi_csrf_safe(2) ){
1587
if( forum_post(zTitle, 0, 0, 0, zMimetype, zContent,
1588
forum_post_flags()) ) return;
1589
}
1590
if( P("preview") && !whitespace_only(zContent) ){
1591
@ <h1>Preview:</h1>
1592
forum_render(zTitle, zMimetype, zContent, "forumEdit", 1);
1593
}
1594
style_set_current_feature("forum");
1595
style_header("New Forum Thread");
1596
@ <form action="%R/forume1" method="POST">
1597
@ <h1>New Thread:</h1>
1598
forum_from_line();
1599
forum_post_widget(zTitle, zMimetype, zContent);
1600
forum_render_notification_reminder();
1601
@ <input type="submit" name="preview" value="Preview">
1602
if( P("preview") && !whitespace_only(zContent) ){
1603
@ <input type="submit" name="submit" value="Submit">
1604
}else{
1605
@ <input type="submit" name="submit" value="Submit" disabled>
1606
}
1607
forum_render_debug_options();
1608
login_insert_csrf_secret();
1609
@ </form>
1610
forum_emit_js();
1611
style_finish_page();
1612
}
1613
1614
/*
1615
** WEBPAGE: forume2
1616
**
1617
** Edit an existing forum message.
1618
** Query parameters:
1619
**
1620
** fpid=X Hash of the post to be edited. REQUIRED
1621
*/
1622
void forumedit_page(void){
1623
int fpid;
1624
int froot;
1625
Manifest *pPost = 0;
1626
Manifest *pRootPost = 0;
1627
const char *zMimetype = 0;
1628
const char *zContent = 0;
1629
const char *zTitle = 0;
1630
char *zDate = 0;
1631
const char *zFpid = PD("fpid","");
1632
int isCsrfSafe;
1633
int isDelete = 0;
1634
int iClosed = 0;
1635
int bSameUser; /* True if author is also the reader */
1636
int bPreview; /* True in preview mode. */
1637
int bPrivate; /* True if post is private (not yet moderated) */
1638
int bReply; /* True if replying to a post */
1639
1640
login_check_credentials();
1641
if( !g.perm.WrForum ){
1642
login_needed(g.anon.WrForum);
1643
return;
1644
}
1645
fpid = symbolic_name_to_rid(zFpid, "f");
1646
if( fpid<=0 || (pPost = manifest_get(fpid, CFTYPE_FORUM, 0))==0 ){
1647
webpage_error("Missing or invalid fpid query parameter");
1648
}
1649
froot = db_int(0, "SELECT froot FROM forumpost WHERE fpid=%d", fpid);
1650
if( froot==0 || (pRootPost = manifest_get(froot, CFTYPE_FORUM, 0))==0 ){
1651
webpage_error("fpid does not appear to be a forum post: \"%d\"", fpid);
1652
}
1653
if( P("cancel") ){
1654
cgi_redirectf("%R/forumpost/%S",zFpid);
1655
return;
1656
}
1657
bPreview = P("preview")!=0;
1658
bReply = P("reply")!=0;
1659
iClosed = forum_rid_is_closed(fpid, 1);
1660
isCsrfSafe = cgi_csrf_safe(2);
1661
bPrivate = content_is_private(fpid);
1662
bSameUser = login_is_individual()
1663
&& fossil_strcmp(pPost->zUser, g.zLogin)==0;
1664
if( isCsrfSafe && (g.perm.ModForum || (bPrivate && bSameUser)) ){
1665
if( g.perm.ModForum && P("approve") ){
1666
const char *zUserToTrust;
1667
moderation_approve('f', fpid);
1668
if( g.perm.AdminForum
1669
&& PB("trust")
1670
&& (zUserToTrust = P("trustuser"))!=0
1671
){
1672
db_unprotect(PROTECT_USER);
1673
db_multi_exec("UPDATE user SET cap=cap||'4' "
1674
"WHERE login=%Q AND cap NOT GLOB '*4*'",
1675
zUserToTrust);
1676
db_protect_pop();
1677
}
1678
cgi_redirectf("%R/forumpost/%S",P("fpid"));
1679
return;
1680
}
1681
if( P("reject") ){
1682
char *zParent =
1683
db_text(0,
1684
"SELECT uuid FROM forumpost, blob"
1685
" WHERE forumpost.fpid=%d AND blob.rid=forumpost.firt",
1686
fpid
1687
);
1688
moderation_disapprove(fpid);
1689
if( zParent ){
1690
cgi_redirectf("%R/forumpost/%S",zParent);
1691
}else{
1692
cgi_redirectf("%R/forum");
1693
}
1694
return;
1695
}
1696
}
1697
style_set_current_feature("forum");
1698
isDelete = P("nullout")!=0;
1699
if( P("submit")
1700
&& isCsrfSafe
1701
&& (zContent = PDT("content",""))!=0
1702
&& (!whitespace_only(zContent) || isDelete)
1703
){
1704
int done = 1;
1705
const char *zMimetype = PD("mimetype",DEFAULT_FORUM_MIMETYPE);
1706
if( bReply ){
1707
done = forum_post(0, fpid, 0, 0, zMimetype, zContent,
1708
forum_post_flags());
1709
}else if( P("edit") || isDelete ){
1710
done = forum_post(P("title"), 0, fpid, 0, zMimetype, zContent,
1711
forum_post_flags());
1712
}else{
1713
webpage_error("Missing 'reply' query parameter");
1714
}
1715
if( done ) return;
1716
}
1717
if( isDelete ){
1718
zMimetype = "text/x-fossil-wiki";
1719
zContent = "";
1720
if( pPost->zThreadTitle ) zTitle = "";
1721
style_header("Delete %s", zTitle ? "Post" : "Reply");
1722
@ <h1>Original Post:</h1>
1723
forum_render(pPost->zThreadTitle, pPost->zMimetype, pPost->zWiki,
1724
"forumEdit", 1);
1725
@ <h1>Change Into:</h1>
1726
forum_render(zTitle, zMimetype, zContent,"forumEdit", 1);
1727
@ <form action="%R/forume2" method="POST">
1728
login_insert_csrf_secret();
1729
@ <input type="hidden" name="fpid" value="%h(P("fpid"))">
1730
@ <input type="hidden" name="nullout" value="1">
1731
@ <input type="hidden" name="mimetype" value="%h(zMimetype)">
1732
@ <input type="hidden" name="content" value="%h(zContent)">
1733
if( zTitle ){
1734
@ <input aria-label="Title" type="hidden" name="title" value="%h(zTitle)">
1735
}
1736
}else if( P("edit") ){
1737
/* Provide an edit to the fpid post */
1738
zMimetype = P("mimetype");
1739
zContent = PT("content");
1740
zTitle = P("title");
1741
if( zContent==0 ) zContent = fossil_strdup(pPost->zWiki);
1742
if( zMimetype==0 ) zMimetype = fossil_strdup(pPost->zMimetype);
1743
if( zTitle==0 && pPost->zThreadTitle!=0 ){
1744
zTitle = fossil_strdup(pPost->zThreadTitle);
1745
}
1746
style_header("Edit %s", zTitle ? "Post" : "Reply");
1747
@ <h2>Original Post:</h2>
1748
forum_render(pPost->zThreadTitle, pPost->zMimetype, pPost->zWiki,
1749
"forumEdit", 1);
1750
if( bPreview ){
1751
@ <h2>Preview of Edited Post:</h2>
1752
forum_render(zTitle, zMimetype, zContent,"forumEdit", 1);
1753
}
1754
@ <h2>Revised Message:</h2>
1755
@ <form action="%R/forume2" method="POST">
1756
login_insert_csrf_secret();
1757
@ <input type="hidden" name="fpid" value="%h(P("fpid"))">
1758
@ <input type="hidden" name="edit" value="1">
1759
forum_from_line();
1760
forum_post_widget(zTitle, zMimetype, zContent);
1761
}else{
1762
/* Reply */
1763
char *zDisplayName;
1764
zMimetype = PD("mimetype",DEFAULT_FORUM_MIMETYPE);
1765
zContent = PDT("content","");
1766
style_header("Reply");
1767
@ <h2>Replying to
1768
@ <a href="%R/forumpost/%!S(zFpid)" target="_blank">%S(zFpid)</a>
1769
if( pRootPost->zThreadTitle ){
1770
@ in thread
1771
@ <span class="forumPostReplyTitle">%h(pRootPost->zThreadTitle)</span>
1772
}
1773
@ </h2>
1774
zDate = db_text(0, "SELECT datetime(%.17g,toLocal())", pPost->rDate);
1775
zDisplayName = display_name_from_login(pPost->zUser);
1776
@ <h3 class='forumPostHdr'>By %s(zDisplayName) on %h(zDate)</h3>
1777
fossil_free(zDisplayName);
1778
fossil_free(zDate);
1779
forum_render(0, pPost->zMimetype, pPost->zWiki, "forumEdit", 1);
1780
if( bPreview && !whitespace_only(zContent) ){
1781
@ <h2>Preview:</h2>
1782
forum_render(0, zMimetype,zContent, "forumEdit", 1);
1783
}
1784
@ <h2>Enter Reply:</h2>
1785
@ <form action="%R/forume2" method="POST">
1786
@ <input type="hidden" name="fpid" value="%h(P("fpid"))">
1787
@ <input type="hidden" name="reply" value="1">
1788
forum_from_line();
1789
forum_post_widget(0, zMimetype, zContent);
1790
}
1791
if( !isDelete ){
1792
forum_render_notification_reminder();
1793
@ <input type="submit" name="preview" value="Preview">
1794
}
1795
@ <input type="submit" name="cancel" value="Cancel">
1796
if( (bPreview && !whitespace_only(zContent)) || isDelete ){
1797
if( !iClosed || g.perm.Admin ) {
1798
@ <input type="submit" name="submit" value="Submit">
1799
}
1800
}
1801
forum_render_debug_options();
1802
login_insert_csrf_secret();
1803
@ </form>
1804
forum_emit_js();
1805
style_finish_page();
1806
}
1807
1808
/*
1809
** SETTING: forum-close-policy boolean default=off
1810
** If true, forum moderators may close/re-open forum posts, and reply
1811
** to closed posts. If false, only administrators may do so. Note that
1812
** this only affects the forum web UI, not post-closing tags which
1813
** arrive via the command-line or from synchronization with a remote.
1814
*/
1815
/*
1816
** SETTING: forum-title width=20 default=Forum
1817
** This is the name or "title" of the Forum for this repository. The
1818
** default is just "Forum". But in some setups, admins might want to
1819
** change it to "Developer Forum" or "User Forum" or whatever other name
1820
** seems more appropriate for the particular usage.
1821
*/
1822
1823
/*
1824
** WEBPAGE: setup_forum
1825
**
1826
** Forum configuration and metrics.
1827
*/
1828
void forum_setup(void){
1829
/* boolean config settings specific to the forum. */
1830
static const char *azForumSettings[] = {
1831
"forum-close-policy",
1832
"forum-title",
1833
};
1834
1835
login_check_credentials();
1836
if( !g.perm.Setup ){
1837
login_needed(g.anon.Setup);
1838
return;
1839
}
1840
style_set_current_feature("forum");
1841
style_header("Forum Setup");
1842
1843
@ <h2>Metrics</h2>
1844
{
1845
int nPosts = db_int(0, "SELECT COUNT(*) FROM event WHERE type='f'");
1846
@ <p><a href='%R/forum'>Forum posts</a>:
1847
@ <a href='%R/timeline?y=f'>%d(nPosts)</a></p>
1848
}
1849
1850
@ <h2>Supervisors</h2>
1851
{
1852
Stmt q = empty_Stmt;
1853
db_prepare(&q, "SELECT uid, login, cap FROM user "
1854
"WHERE cap GLOB '*[as6]*' ORDER BY login");
1855
@ <table class='bordered'>
1856
@ <thead><tr><th>User</th><th>Capabilities</th></tr></thead>
1857
@ <tbody>
1858
while( SQLITE_ROW==db_step(&q) ){
1859
const int iUid = db_column_int(&q, 0);
1860
const char *zUser = db_column_text(&q, 1);
1861
const char *zCap = db_column_text(&q, 2);
1862
@ <tr>
1863
@ <td><a href='%R/setup_uedit?id=%d(iUid)'>%h(zUser)</a></td>
1864
@ <td>(%h(zCap))</td>
1865
@ </tr>
1866
}
1867
db_finalize(&q);
1868
@</tbody></table>
1869
}
1870
1871
@ <h2>Moderators</h2>
1872
if( db_int(0, "SELECT count(*) FROM user "
1873
" WHERE cap GLOB '*5*' AND cap NOT GLOB '*[as6]*'")==0 ){
1874
@ <p>No non-supervisor moderators
1875
}else{
1876
Stmt q = empty_Stmt;
1877
db_prepare(&q, "SELECT uid, login, cap FROM user "
1878
"WHERE cap GLOB '*5*' AND cap NOT GLOB '*[as6]*'"
1879
" ORDER BY login");
1880
@ <table class='bordered'>
1881
@ <thead><tr><th>User</th><th>Capabilities</th></tr></thead>
1882
@ <tbody>
1883
while( SQLITE_ROW==db_step(&q) ){
1884
const int iUid = db_column_int(&q, 0);
1885
const char *zUser = db_column_text(&q, 1);
1886
const char *zCap = db_column_text(&q, 2);
1887
@ <tr>
1888
@ <td><a href='%R/setup_uedit?id=%d(iUid)'>%h(zUser)</a></td>
1889
@ <td>(%h(zCap))</td>
1890
@ </tr>
1891
}
1892
db_finalize(&q);
1893
@ </tbody></table>
1894
}
1895
1896
@ <h2>Settings</h2>
1897
if( P("submit") && cgi_csrf_safe(2) ){
1898
int i = 0;
1899
db_begin_transaction();
1900
for(i=0; i<ArraySize(azForumSettings); i++){
1901
char zQP[4];
1902
const char *z;
1903
const Setting *pSetting = setting_find(azForumSettings[i]);
1904
if( pSetting==0 ) continue;
1905
zQP[0] = 'a'+i;
1906
zQP[1] = zQP[0];
1907
zQP[2] = 0;
1908
z = P(zQP);
1909
if( z==0 || z[0]==0 ) continue;
1910
db_set(pSetting->name/*works-like:"x"*/, z, 0);
1911
}
1912
db_end_transaction(0);
1913
@ <p><em>Settings saved.</em></p>
1914
}
1915
{
1916
int i = 0;
1917
@ <form action="%R/setup_forum" method="post">
1918
login_insert_csrf_secret();
1919
@ <table class='forum-settings-list'><tbody>
1920
for(i=0; i<ArraySize(azForumSettings); i++){
1921
char zQP[4];
1922
const Setting *pSetting = setting_find(azForumSettings[i]);
1923
if( pSetting==0 ) continue;
1924
zQP[0] = 'a'+i;
1925
zQP[1] = zQP[0];
1926
zQP[2] = 0;
1927
if( pSetting->width==0 ){
1928
/* Boolean setting */
1929
@ <tr><td align="right">
1930
@ <a href='%R/help/%h(pSetting->name)'>%h(pSetting->name)</a>:
1931
@ </td><td>
1932
onoff_attribute("", zQP, pSetting->name/*works-like:"x"*/, 0, 0);
1933
@ </td></tr>
1934
}else{
1935
/* Text value setting */
1936
@ <tr><td align="right">
1937
@ <a href='%R/help/%h(pSetting->name)'>%h(pSetting->name)</a>:
1938
@ </td><td>
1939
entry_attribute("", 25, pSetting->name, zQP/*works-like:""*/,
1940
pSetting->def, 0);
1941
@ </td></tr>
1942
}
1943
}
1944
@ </tbody></table>
1945
@ <input type='submit' name='submit' value='Apply changes'>
1946
@ </form>
1947
}
1948
1949
style_finish_page();
1950
}
1951
1952
/*
1953
** WEBPAGE: forummain
1954
** WEBPAGE: forum
1955
**
1956
** The main page for the forum feature. Show a list of recent forum
1957
** threads. Also show a search box at the top if search is enabled,
1958
** and a button for creating a new thread, if enabled.
1959
**
1960
** Query parameters:
1961
**
1962
** n=N The number of threads to show on each page
1963
** x=X Skip the first X threads
1964
** s=Y Search for term Y.
1965
*/
1966
void forum_main_page(void){
1967
Stmt q;
1968
int iLimit = 0, iOfst, iCnt;
1969
int srchFlags;
1970
const int isSearch = P("s")!=0;
1971
char const *zLimit = 0;
1972
1973
login_check_credentials();
1974
srchFlags = search_restrict(SRCH_FORUM);
1975
if( !g.perm.RdForum ){
1976
login_needed(g.anon.RdForum);
1977
return;
1978
}
1979
cgi_check_for_malice();
1980
style_set_current_feature("forum");
1981
style_header("%s%s", db_get("forum-title","Forum"),
1982
isSearch ? " Search Results" : "");
1983
style_submenu_element("Timeline", "%R/timeline?ss=v&y=f&vfx");
1984
if( g.perm.WrForum ){
1985
style_submenu_element("New Thread","%R/forumnew");
1986
}else{
1987
/* Can't combine this with previous case using the ternary operator
1988
* because that causes an error yelling about "non-constant format"
1989
* with some compilers. I can't see it, since both expressions have
1990
* the same format, but I'm no C spec lawyer. */
1991
style_submenu_element("New Thread","%R/login");
1992
}
1993
if( g.perm.ModForum && moderation_needed() ){
1994
style_submenu_element("Moderation Requests", "%R/modreq");
1995
}
1996
if( (srchFlags & SRCH_FORUM)!=0 ){
1997
if( search_screen(SRCH_FORUM, 0) ){
1998
style_submenu_element("Recent Threads","%R/forum");
1999
style_finish_page();
2000
return;
2001
}
2002
}
2003
cookie_read_parameter("n","forum-n");
2004
zLimit = P("n");
2005
if( zLimit!=0 ){
2006
iLimit = atoi(zLimit);
2007
if( iLimit>=0 && P("udc")!=0 ){
2008
cookie_write_parameter("n","forum-n",0);
2009
}
2010
}
2011
if( iLimit<=0 ){
2012
cgi_replace_query_parameter("n", fossil_strdup("25"))
2013
/*for the sake of Max, below*/;
2014
iLimit = 25;
2015
}
2016
style_submenu_entry("n","Max:",4,0);
2017
iOfst = atoi(PD("x","0"));
2018
iCnt = 0;
2019
if( db_table_exists("repository","forumpost") ){
2020
db_prepare(&q,
2021
"WITH thread(age,duration,cnt,root,last) AS ("
2022
" SELECT"
2023
" julianday('now') - max(fmtime),"
2024
" max(fmtime) - min(fmtime),"
2025
" sum(fprev IS NULL),"
2026
" froot,"
2027
" (SELECT fpid FROM forumpost AS y"
2028
" WHERE y.froot=x.froot %s"
2029
" ORDER BY y.fmtime DESC LIMIT 1)"
2030
" FROM forumpost AS x"
2031
" WHERE %s"
2032
" GROUP BY froot"
2033
" ORDER BY 1 LIMIT %d OFFSET %d"
2034
")"
2035
"SELECT"
2036
" thread.age," /* 0 */
2037
" thread.duration," /* 1 */
2038
" thread.cnt," /* 2 */
2039
" blob.uuid," /* 3 */
2040
" substr(event.comment,instr(event.comment,':')+1)," /* 4 */
2041
" thread.last" /* 5 */
2042
" FROM thread, blob, event"
2043
" WHERE blob.rid=thread.last"
2044
" AND event.objid=thread.last"
2045
" ORDER BY 1;",
2046
g.perm.ModForum ? "" : "AND y.fpid NOT IN private" /*safe-for-%s*/,
2047
g.perm.ModForum ? "true" : "fpid NOT IN private" /*safe-for-%s*/,
2048
iLimit+1, iOfst
2049
);
2050
while( db_step(&q)==SQLITE_ROW ){
2051
char *zAge = human_readable_age(db_column_double(&q,0));
2052
int nMsg = db_column_int(&q, 2);
2053
const char *zUuid = db_column_text(&q, 3);
2054
const char *zTitle = db_column_text(&q, 4);
2055
if( iCnt==0 ){
2056
if( iOfst>0 ){
2057
@ <h1>Threads at least %s(zAge) old</h1>
2058
}else{
2059
@ <h1>Most recent threads</h1>
2060
}
2061
@ <div class='forumPosts fileage'><table width="100%%">
2062
if( iOfst>0 ){
2063
if( iOfst>iLimit ){
2064
@ <tr><td colspan="3">\
2065
@ %z(href("%R/forum?x=%d&n=%d",iOfst-iLimit,iLimit))\
2066
@ &uarr; Newer...</a></td></tr>
2067
}else{
2068
@ <tr><td colspan="3">%z(href("%R/forum?n=%d",iLimit))\
2069
@ &uarr; Newer...</a></td></tr>
2070
}
2071
}
2072
}
2073
iCnt++;
2074
if( iCnt>iLimit ){
2075
@ <tr><td colspan="3">\
2076
@ %z(href("%R/forum?x=%d&n=%d",iOfst+iLimit,iLimit))\
2077
@ &darr; Older...</a></td></tr>
2078
fossil_free(zAge);
2079
break;
2080
}
2081
@ <tr><td>%h(zAge) ago</td>
2082
@ <td>%z(href("%R/forumpost/%S",zUuid))%h(zTitle)</a></td>
2083
@ <td>\
2084
if( g.perm.ModForum && moderation_pending(db_column_int(&q,5)) ){
2085
@ <span class="modpending">\
2086
@ Awaiting Moderator Approval</span><br>
2087
}
2088
if( nMsg<2 ){
2089
@ no replies</td>
2090
}else{
2091
char *zDuration = human_readable_age(db_column_double(&q,1));
2092
@ %d(nMsg) posts spanning %h(zDuration)</td>
2093
fossil_free(zDuration);
2094
}
2095
@ </tr>
2096
fossil_free(zAge);
2097
}
2098
db_finalize(&q);
2099
}
2100
if( iCnt>0 ){
2101
@ </table></div>
2102
}else{
2103
@ <h1>No forum posts found</h1>
2104
}
2105
style_finish_page();
2106
}
2107

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button