Fossil SCM

fossil-scm / src / forum.c
Blame History Raw 3195 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; /* True if forum_rid_is_tagged("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
67
/*
68
** A single entry from the forum-statuses setting.
69
*/
70
struct ForumStatus {
71
char *zLabel; /* Label for the UI */
72
char *zValue; /* status=X tag value */
73
};
74
75
/*
76
** A list of ForumStatus objects.
77
*/
78
struct ForumStatusList {
79
struct ForumStatus *aStatus; /* List of statuses */
80
unsigned int n; /* Number of entries */
81
};
82
83
/*
84
** Information passed into the status_match() SQL function
85
** via the sqlite3_user_data() mechanism, and used by status_match()
86
** to determine whether or not a particular forum thread should
87
** be displayed.
88
*/
89
struct ForumStatusMatch {
90
const ForumStatusList *pFses; /* Parsed forum-statuses setting */
91
int eStatusTag; /* tagid for the "status" property */
92
unsigned int iMatch; /* Match this status value */
93
};
94
#endif /* INTERFACE */
95
96
97
/*
98
** Returns a high-level representation of the forum-statuses setting.
99
** This is a singleton, cached across calls.
100
*/
101
const ForumStatusList * forum_statuses(void){
102
static ForumStatusList fses = {0,0};
103
static int once = 0;
104
while( !once ){
105
++once;
106
/* Read `forum-statuses` setting and transform it into the
107
** fses object.
108
**
109
** Maybe: if it's empty, synthesize a length-1 list from
110
** {value:"default",label:"Default",...}. It's expected that
111
** usage may be slightly simplified if we always have a non-empty
112
** list. A length-1 list is, for purposes of the UI, identical to
113
** an empty one - status selection/filtering makes no sense if
114
** there's only one choice. */
115
db_multi_exec(
116
"CREATE TEMP TABLE IF NOT EXISTS forumstatus("
117
" ord INTEGER PRIMARY KEY, label, value"
118
");"
119
"DELETE FROM forumstatus;"
120
"INSERT INTO forumstatus(label,value)"
121
" WITH setting(v) AS ("
122
" SELECT value v FROM config WHERE name='forum-statuses'"
123
" ),"
124
" room(r) AS ("
125
" SELECT e.value FROM setting s, jsonb_each(s.v) e"
126
" WHERE json_valid(s.v, 0x02)"
127
" )"
128
" SELECT r->>'label', r->>'value'"
129
" FROM room;"
130
);
131
fses.n = (unsigned)db_int(0, "SELECT count(*) FROM forumstatus");
132
if( fses.n ){
133
int i = 0;
134
Stmt q;
135
db_prepare(&q,"SELECT label, value FROM forumstatus"
136
" ORDER BY ord");
137
fses.aStatus = fossil_malloc(sizeof(fses.aStatus[0]) * fses.n);
138
while( SQLITE_ROW==db_step(&q) ){
139
ForumStatus * fs = &fses.aStatus[i++];
140
fs->zLabel = fossil_strdup(db_column_text(&q, 0));
141
fs->zValue = fossil_strdup(db_column_text(&q, 1));
142
}
143
db_finalize(&q);
144
}
145
}
146
return &fses;
147
}
148
149
/*
150
** Search for a ForumStatus object by its tag value. If a match is
151
** found, the corresponding object is returned. If no match is found
152
** then (A) if bFirst is false then 0 is returned, else (B) the first
153
** entry in the list is returned, noting that the list may be empty,
154
** in which case 0 is returned.
155
*/
156
static const ForumStatus * forum_status_by_value(
157
const char *z, int bFirst
158
){
159
const ForumStatusList * const fses = forum_statuses();
160
const ForumStatus * fs0 = 0;
161
unsigned int i;
162
if( !fses->n ) return 0;
163
for( i = 0; i < fses->n; ++i ){
164
const ForumStatus * fs = &fses->aStatus[i];
165
if( 0==fossil_strcmp(z, fs->zValue) ){
166
return fs;
167
}else if( !fs0 ){
168
fs0 = fs;
169
}
170
}
171
return bFirst ? fs0 : 0;
172
}
173
174
/*
175
** COMMAND: test-forum-statuses
176
*/
177
void test_forum_statuses_cmd(void){
178
const ForumStatusList * fses;
179
unsigned i;
180
db_find_and_open_repository(0,0);
181
fses = forum_statuses();
182
for(i = 0; i < fses->n; ++i ){
183
const ForumStatus * fs = &fses->aStatus[i];
184
fossil_print("Status: %!j %!j\n", fs->zValue, fs->zLabel);
185
assert( fs==forum_status_by_value(fs->zValue, 0) );
186
}
187
fossil_print("Total statuses: %u\n", i);
188
}
189
190
/*
191
** Return true if the forum post with the given rid has been
192
** subsequently edited.
193
*/
194
int forum_rid_has_been_edited(int rid){
195
static Stmt q;
196
int res;
197
db_static_prepare(&q,
198
"SELECT 1 FROM forumpost A, forumpost B"
199
" WHERE A.fpid=$rid AND B.froot=A.froot AND B.fprev=$rid"
200
);
201
db_bind_int(&q, "$rid", rid);
202
res = db_step(&q)==SQLITE_ROW;
203
db_reset(&q);
204
return res;
205
}
206
207
/*
208
** Given a valid forumpost.fpid value, this function returns the
209
** initial forumpost.fpid in the chain of edits for that forum post,
210
** or rid if no prior versions are found.
211
*/
212
int forumpost_head_rid(int rid){
213
static Stmt q = empty_Stmt_m;
214
int rcRid = rid;
215
if( !q.pStmt ){
216
db_static_prepare(&q,
217
"SELECT fprev FROM forumpost"
218
" WHERE fpid=:rid AND fprev IS NOT NULL"
219
);
220
}
221
db_bind_int(&q, ":rid", rid);
222
while( SQLITE_ROW==db_step(&q) ){
223
rcRid = db_column_int(&q, 0);
224
db_reset(&q);
225
db_bind_int(&q, ":rid", rcRid);
226
}
227
db_reset(&q);
228
return rcRid;
229
}
230
231
/*
232
** Works like forumpost_head_rid() but expects zUuid to be an
233
** unambiguous forum post name. It may be a hash prefix, so long as
234
** it's unambiguous. Returns the rid of the head post, -1 if the name
235
** is ambiguous, and 0 if the name cannot be resolved as a forum post.
236
*/
237
int forumpost_head_rid2(const char *zUuid){
238
const int fpid = symbolic_name_to_rid(zUuid, "f");
239
return fpid>0
240
? forumpost_head_rid(fpid)
241
: fpid;
242
}
243
244
/*
245
** Given a forum post RID and user name, returns true if zUserName
246
** matches the event.(euser,user) field for a formpost entry with the
247
** matching RID. Returns false if no match is found. If zUserName is
248
** 0 then login_name() is used.
249
*/
250
int forumpost_is_owner(int rid, const char *zUserName){
251
static Stmt q;
252
int rc;
253
if( !q.pStmt ){
254
db_static_prepare(
255
&q, "SELECT 1 FROM event"
256
" WHERE type='f' AND objid=$rid"
257
" AND coalesce(euser,user)=$user"
258
);
259
}
260
db_bind_int(&q, "$rid", rid);
261
db_bind_text(&q, "$user", zUserName ? zUserName : login_name());
262
rc = SQLITE_ROW==db_step(&q);
263
db_reset(&q);
264
return rc;
265
}
266
267
/*
268
** Returns true if p, or any parent of p, has a non-zero iClosed
269
** value. Returns 0 if !p. For an edited chain of post, the tag is
270
** checked on the pEditHead entry, to simplify subsequent unlocking of
271
** the post.
272
**
273
** If bCheckIrt is true then p's thread in-response-to parents are
274
** checked (recursively) for closure, else only p is checked.
275
*/
276
static int forumpost_is_closed(
277
ForumThread *pThread, /* Thread that the post is a member of */
278
ForumPost *p, /* the forum post */
279
int bCheckIrt /* True to check In-Reply-To posts */
280
){
281
int mx = pThread->nArtifact+1;
282
while( p && (mx--)>0 ){
283
if( p->pEditHead ) p = p->pEditHead;
284
if( p->iClosed || !bCheckIrt ) return p->iClosed;
285
p = p->pIrt;
286
}
287
return 0;
288
}
289
290
/*
291
** Given a forum post RID, this function returns true if that post has
292
** (or inherits) an active tag named zTagName. If bCheckIrt is true
293
** then the post to which the given post responds is also checked
294
** (recursively), else they are not. When checking in-response-to
295
** posts, the first one which is closed ends the search.
296
**
297
** This function checks _exactly_ the given rid, whereas forum post
298
** closure/re-opening is always applied to the head of an edit chain
299
** so that we get consistent implied locking behavior for later
300
** versions and responses to arbitrary versions in the chain. Even so,
301
** the "closed" tag is applied as a propagating tag so will apply to
302
** all edits in a given chain.
303
**
304
** The return value is one of:
305
**
306
** - 0 if no matching tag is found.
307
**
308
** - The tagxref.rowid of the tagxref entry for the closure if rid is
309
** the forum post to which the closure applies.
310
**
311
** - (-tagxref.rowid) if the given rid inherits the tag from an IRT
312
** forum post.
313
*/
314
static int forum_rid_is_tagged(int rid, const char *zTagName, int bCheckIrt){
315
static Stmt qIrt = empty_Stmt_m;
316
int rc = 0, i = 0;
317
/* TODO: this can probably be turned into a CTE by someone with
318
** superior SQL-fu. */
319
for( ; rid; i++ ){
320
rc = rid_has_active_tag_name(rid, zTagName);
321
if( rc || !bCheckIrt ) break;
322
else if( !qIrt.pStmt ) {
323
db_static_prepare(&qIrt,
324
"SELECT firt FROM forumpost "
325
"WHERE fpid=$fpid ORDER BY fmtime DESC"
326
);
327
}
328
db_bind_int(&qIrt, "$fpid", rid);
329
rid = SQLITE_ROW==db_step(&qIrt) ? db_column_int(&qIrt, 0) : 0;
330
db_reset(&qIrt);
331
}
332
return i ? -rc : rc;
333
}
334
335
/* True if moderation of forum posts performs the same operation
336
** on its attachments. */
337
#define FORUMPOST_MOD_ATTACHMENTS 1
338
#if FORUMPOST_MOD_ATTACHMENTS
339
/*
340
** Internal helper for moderation_forumpost_...().
341
*/
342
static void forumpost_prep_pending_attachids(Stmt *q, int fpid){
343
db_prepare(
344
q,
345
"SELECT attachid FROM attachment "
346
"WHERE target=("
347
" SELECT uuid FROM blob WHERE rid=%d"
348
") and attachid in ("
349
" SELECT objid FROM modreq"
350
")",
351
forumpost_head_rid(fpid)
352
);
353
}
354
#endif
355
356
/*
357
** Approve the given forum post RID and any pending-approval
358
** attachments associated with its initial version.
359
*/
360
static void moderation_forumpost_approve(int fpid){
361
#if !FORUMPOST_MOD_ATTACHMENTS
362
moderation_approve('f', fpid);
363
#else
364
/* Also approve any pending attachments */
365
Stmt q;
366
moderation_approve('f', fpid);
367
forumpost_prep_pending_attachids(&q, fpid);
368
while( SQLITE_ROW==db_step(&q) ){
369
moderation_approve('a', db_column_int(&q, 0));
370
}
371
db_finalize(&q);
372
#endif
373
}
374
375
/*
376
** Disapprove the given forum post and any pending-moderation
377
** attachments on its initial version.
378
*/
379
static void moderation_forumpost_disapprove(int fpid){
380
#if !FORUMPOST_MOD_ATTACHMENTS
381
moderation_disapprove(fpid);
382
#else
383
/* Also disapprove any pending attachments */
384
Stmt q;
385
moderation_disapprove(fpid);
386
forumpost_prep_pending_attachids(&q, fpid);
387
while( SQLITE_ROW==db_step(&q) ){
388
moderation_disapprove(db_column_int(&q, 0));
389
}
390
db_finalize(&q);
391
#endif
392
}
393
#undef FORUMPOST_MOD_ATTACHMENTS
394
395
/*
396
** Applies or cancels a tag named zTagName on the given forum RID via
397
** addition of a new control artifact into the repository. In order to
398
** provide consistent behavior, it always acts on the first version of
399
** the given forum post, walking the forumpost.fprev values to find
400
** the head of the chain.
401
**
402
** If addTag is true then a propagating tag is added, except as noted
403
** below, with the given optional zValue string as the tag's
404
** value. If addTag is false then any matching active tag on frid is
405
** cancelled, except as noted below. zValue is ignored if it is NULL
406
** or starts with a NUL byte, or if addTag is false.
407
**
408
** This function only adds a tag if forum_rid_is_tagged() indicates
409
** that frid's head is not tagged. If a parent post is already tagged,
410
** no tag is added. Similarly, it will only remove a tag from a post
411
** which has its own tag, and will not remove an inherited one from a
412
** parent post.
413
**
414
** If addTag is true and frid is already tagged, this is a
415
** no-op. Likewise, if addTag is false and frid is not tagged (not
416
** accounting for a tag inherited via an in-response-to post), this is
417
** a no-op.
418
**
419
** Returns a positive value (a new tag.tagid value) if it actually
420
** creates a new tag, else 0. On error it returns a negative alue
421
** and g.zErrMsg "should" contain details.
422
**
423
** If it returns true then state from previously-loaded posts may be
424
** invalidated if they refer to the amended post or a response to it.
425
** e.g. if zTagName is "closed" then ForumPost::iClosed values may be
426
** stale.
427
**
428
** Sidebars:
429
**
430
** - Unless the caller has a transaction open, via
431
** db_begin_transaction(), there is a very tiny race condition
432
** window during which the caller's idea of whether or not the forum
433
** post is tagged may differ from the current repository state.
434
**
435
** - This routine assumes that frid really does refer to a forum post.
436
**
437
** - This routine assumes that frid is not private or pending
438
** moderation.
439
**
440
** - The applied tag is propagating so so that "closed" tags can
441
** account for how edits of posts are handled. This differs from
442
** closure of a branch, where a non-propagating tag is used.
443
*/
444
static int forumpost_tag(int frid, int addTag,
445
const char *zTagName,
446
const char *zValue){
447
Blob artifact = BLOB_INITIALIZER; /* Output artifact */
448
Blob cksum = BLOB_INITIALIZER; /* Z-card */
449
int iTagged; /* true if frid is already tagged */
450
int trid; /* RID of new control artifact */
451
char *zUuid; /* UUID of head version of post */
452
453
db_begin_transaction();
454
frid = forumpost_head_rid(frid);
455
iTagged = forum_rid_is_tagged(frid, zTagName, 0);
456
if( !addTag && !iTagged ){
457
/* Nothing to do. We never tag an IRT-inherited post via this
458
** function. */
459
db_end_transaction(0);
460
return 0;
461
}
462
if( !addTag || (zValue && !zValue[0]) ){
463
zValue = 0;
464
}
465
if( addTag && iTagged ){
466
char *zOld = 0;
467
int cmp;
468
rid_has_tag2(frid, zTagName, &zOld);
469
cmp = fossil_strcmp(zOld, zValue);
470
fossil_free(zOld);
471
if( 0==cmp ){
472
/* Same value - leave it as is. */
473
db_end_transaction(0);
474
return 0;
475
}
476
}
477
zUuid = rid_to_uuid(frid);
478
blob_appendf(&artifact, "D %z\n", date_in_standard_format( "now" ));
479
blob_appendf(&artifact, "T %c%s %s%s%F\n",
480
addTag ? '*' : '-', zTagName,
481
zUuid, zValue ? " " : "", zValue ? zValue : "");
482
blob_appendf(&artifact, "U %F\n", login_name());
483
md5sum_blob(&artifact, &cksum);
484
blob_appendf(&artifact, "Z %b\n", &cksum);
485
blob_reset(&cksum);
486
trid = content_put_ex(&artifact, 0, 0, 0, 0);
487
if( trid==0 ){
488
return -1;
489
}
490
if( manifest_crosslink(trid, &artifact, MC_NONE)==0 ){
491
return -2;
492
}
493
assert( blob_is_reset(&artifact) );
494
db_add_unsent(trid);
495
admin_log("Tag forum post %S with %c%s",
496
zUuid, addTag ? '*' : '-', zTagName);
497
fossil_free(zUuid);
498
db_end_transaction(0);
499
return trid;
500
}
501
502
/*
503
** COMMAND: test-forumpost-tag
504
**
505
** Usage: %fossil test-forumpost-tag ?-cancel? THREADID TAGNAME TAGVAL
506
**
507
** A tester for forumpost_tag(). It always rolls back changes.
508
*/
509
void test_forumpost_tag_command(void){
510
int fpid;
511
int rc;
512
const char *zPost;
513
const char *zTag;
514
const char *zVal;
515
const int bAdd = find_option("cancel","",0)==0;
516
517
db_find_and_open_repository(0,0);
518
verify_all_options();
519
if( g.argc<5 ){
520
usage("forum-post-id tag-name value");
521
}
522
zPost = g.argv[2];
523
zTag = g.argv[3];
524
zVal = g.argv[4];
525
526
db_begin_transaction();
527
fpid = forumpost_head_rid2(zPost);
528
if( fpid<=0 ){
529
fossil_fatal("Cannot resolve post ID %s", zPost);
530
}
531
fossil_print("%s => %d => %z\n", zTag, fpid,
532
rid_to_uuid(fpid));
533
rc = forumpost_tag(fpid, bAdd, zTag, zVal);
534
fossil_print("tag fpid=%d taxgxref.tagid=%d\n", fpid, rc);
535
db_end_transaction(1);
536
}
537
538
/*
539
** Returns true if the forum-close-policy setting is true, else false,
540
** caching the result for subsequent calls.
541
*/
542
static int forumpost_close_policy(void){
543
static int closePolicy = -99;
544
545
if( closePolicy==-99 ){
546
closePolicy = db_get_boolean("forum-close-policy",0)>0;
547
}
548
return closePolicy;
549
}
550
551
/*
552
** Returns 1 if the current user is an admin, -1 if the current user
553
** is a forum moderator and the forum-close-policy setting is true,
554
** else returns 0. The value is cached for subsequent calls.
555
**
556
** This policy also determines whether non-admin forum moderators
557
** may delete forum attachments.
558
*/
559
int forumpost_may_close(void){
560
static int permClose = -99;
561
if( permClose!=-99 ){
562
return permClose;
563
}else if( g.perm.Admin ){
564
return permClose = 1;
565
}else if( g.perm.ModForum ){
566
return permClose = forumpost_close_policy()>0 ? -1 : 0;
567
}else{
568
return permClose = 0;
569
}
570
}
571
572
/*
573
** Emits a warning that the current forum post is CLOSED and can only
574
** be edited or responded to by an administrator. */
575
static void forumpost_error_closed(void){
576
@ <div class='error'>This (sub)thread is CLOSED and can only be
577
@ edited or replied to by an admin user.</div>
578
}
579
580
/*
581
** Delete a complete ForumThread and all its entries.
582
*/
583
static void forumthread_delete(ForumThread *pThread){
584
ForumPost *pPost, *pNext;
585
for(pPost=pThread->pFirst; pPost; pPost = pNext){
586
pNext = pPost->pNext;
587
fossil_free(pPost->zUuid);
588
fossil_free(pPost->zDisplayName);
589
fossil_free(pPost);
590
}
591
fossil_free(pThread);
592
}
593
594
/*
595
** Search a ForumPost list forwards looking for the post with fpid
596
*/
597
static ForumPost *forumpost_forward(ForumPost *p, int fpid){
598
while( p && p->fpid!=fpid ) p = p->pNext;
599
return p;
600
}
601
602
/*
603
** Search backwards for a ForumPost
604
*/
605
static ForumPost *forumpost_backward(ForumPost *p, int fpid){
606
while( p && p->fpid!=fpid ) p = p->pPrev;
607
return p;
608
}
609
610
/*
611
** Add a post to the display list
612
*/
613
static void forumpost_add_to_display(ForumThread *pThread, ForumPost *p){
614
if( pThread->pDisplay==0 ){
615
pThread->pDisplay = p;
616
}else{
617
pThread->pTail->pDisplay = p;
618
}
619
pThread->pTail = p;
620
}
621
622
/*
623
** Extend the display list for pThread by adding all entries that
624
** reference fpid. The first such post will be no earlier then
625
** post "p".
626
*/
627
static void forumthread_display_order(
628
ForumThread *pThread, /* The complete thread */
629
ForumPost *pBase /* Add replies to this post */
630
){
631
ForumPost *p;
632
ForumPost *pPrev = 0;
633
ForumPost *pBaseIrt;
634
for(p=pBase->pNext; p; p=p->pNext){
635
if( !p->pEditPrev && p->pIrt ){
636
pBaseIrt = p->pIrt->pEditHead ? p->pIrt->pEditHead : p->pIrt;
637
if( pBaseIrt==pBase ){
638
if( pPrev ){
639
pPrev->nIndent = pBase->nIndent + 1;
640
forumpost_add_to_display(pThread, pPrev);
641
forumthread_display_order(pThread, pPrev);
642
}
643
pPrev = p;
644
}
645
}
646
}
647
if( pPrev ){
648
pPrev->nIndent = pBase->nIndent + 1;
649
if( pPrev->nIndent>pThread->mxIndent ) pThread->mxIndent = pPrev->nIndent;
650
forumpost_add_to_display(pThread, pPrev);
651
forumthread_display_order(pThread, pPrev);
652
}
653
}
654
655
/*
656
** Construct a ForumThread object given the root record id.
657
*/
658
static ForumThread *forumthread_create(int froot, int computeHierarchy){
659
ForumThread *pThread;
660
ForumPost *pPost;
661
ForumPost *p;
662
Stmt q;
663
int sid = 1;
664
int firt, fprev;
665
pThread = fossil_malloc( sizeof(*pThread) );
666
memset(pThread, 0, sizeof(*pThread));
667
db_prepare(&q,
668
"SELECT fpid, firt, fprev, (SELECT uuid FROM blob WHERE rid=fpid), fmtime"
669
" FROM forumpost"
670
" WHERE froot=%d ORDER BY fmtime",
671
froot
672
);
673
while( db_step(&q)==SQLITE_ROW ){
674
pPost = fossil_malloc( sizeof(*pPost) );
675
memset(pPost, 0, sizeof(*pPost));
676
pPost->fpid = db_column_int(&q, 0);
677
firt = db_column_int(&q, 1);
678
fprev = db_column_int(&q, 2);
679
pPost->zUuid = fossil_strdup(db_column_text(&q,3));
680
pPost->rDate = db_column_double(&q,4);
681
if( !fprev ) pPost->sid = sid++;
682
pPost->pPrev = pThread->pLast;
683
pPost->pNext = 0;
684
if( pThread->pLast==0 ){
685
pThread->pFirst = pPost;
686
}else{
687
pThread->pLast->pNext = pPost;
688
}
689
pThread->pLast = pPost;
690
pThread->nArtifact++;
691
692
/* Find the in-reply-to post. Default to the topic post if the replied-to
693
** post cannot be found. */
694
if( firt ){
695
pPost->pIrt = pThread->pFirst;
696
for(p=pThread->pFirst; p; p=p->pNext){
697
if( p->fpid==firt ){
698
pPost->pIrt = p;
699
break;
700
}
701
}
702
}
703
704
/* Maintain the linked list of post edits. */
705
if( fprev ){
706
p = forumpost_backward(pPost->pPrev, fprev);
707
p->pEditNext = pPost;
708
pPost->sid = p->sid;
709
pPost->rev = p->rev+1;
710
pPost->nEdit = p->nEdit+1;
711
pPost->pEditPrev = p;
712
pPost->pEditHead = p->pEditHead ? p->pEditHead : p;
713
for(; p; p=p->pEditPrev ){
714
p->nEdit = pPost->nEdit;
715
p->pEditTail = pPost;
716
}
717
}
718
pPost->iClosed = forum_rid_is_tagged(pPost->pEditHead
719
? pPost->pEditHead->fpid
720
: pPost->fpid, "closed", 1);
721
}
722
db_finalize(&q);
723
724
if( computeHierarchy ){
725
/* Compute the hierarchical display order */
726
pPost = pThread->pFirst;
727
pPost->nIndent = 1;
728
pThread->mxIndent = 1;
729
forumpost_add_to_display(pThread, pPost);
730
forumthread_display_order(pThread, pPost);
731
}
732
733
/* Return the result */
734
return pThread;
735
}
736
737
/*
738
** List all forum threads to standard output.
739
*/
740
static void forum_thread_list(void){
741
Stmt q;
742
db_prepare(&q,
743
" SELECT"
744
" datetime(max(fmtime)),"
745
" sum(fprev IS NULL),"
746
" froot"
747
" FROM forumpost"
748
" GROUP BY froot"
749
" ORDER BY 1;"
750
);
751
fossil_print(" id cnt most recent post\n");
752
fossil_print("------ ---- -------------------\n");
753
while( db_step(&q)==SQLITE_ROW ){
754
fossil_print("%6d %4d %s\n",
755
db_column_int(&q, 2),
756
db_column_int(&q, 1),
757
db_column_text(&q, 0)
758
);
759
}
760
db_finalize(&q);
761
}
762
763
/*
764
** COMMAND: test-forumthread
765
**
766
** Usage: %fossil test-forumthread [THREADID]
767
**
768
** Display a summary of all messages on a thread THREADID. If the
769
** THREADID argument is omitted, then show a list of all threads.
770
**
771
** This command is intended for testing an analysis only.
772
*/
773
void forumthread_cmd(void){
774
int fpid;
775
int froot;
776
const char *zName;
777
ForumThread *pThread;
778
ForumPost *p;
779
780
db_find_and_open_repository(0,0);
781
verify_all_options();
782
if( g.argc==2 ){
783
forum_thread_list();
784
return;
785
}
786
if( g.argc!=3 ) usage("THREADID");
787
zName = g.argv[2];
788
fpid = symbolic_name_to_rid(zName, "f");
789
if( fpid<=0 ){
790
fpid = db_int(0, "SELECT rid FROM blob WHERE rid=%d", atoi(zName));
791
}
792
if( fpid<=0 ){
793
fossil_fatal("unknown or ambiguous forum id: \"%s\"", zName);
794
}
795
froot = db_int(0, "SELECT froot FROM forumpost WHERE fpid=%d", fpid);
796
if( froot==0 ){
797
fossil_fatal("Not a forum post: \"%s\"", zName);
798
}
799
fossil_print("fpid = %d\n", fpid);
800
fossil_print("froot = %d\n", froot);
801
pThread = forumthread_create(froot, 1);
802
fossil_print("count = %d\n", pThread->nArtifact);
803
fossil_print("Chronological:\n");
804
fossil_print(
805
/* 0 1 2 3 4 5 6 7 */
806
/* 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123 */
807
" sid rev closed fpid pIrt pEditPrev pEditTail hash\n");
808
for(p=pThread->pFirst; p; p=p->pNext){
809
fossil_print("%4d %4d %7d %9d %9d %9d %9d %8.8s\n",
810
p->sid, p->rev,
811
p->iClosed,
812
p->fpid, p->pIrt ? p->pIrt->fpid : 0,
813
p->pEditPrev ? p->pEditPrev->fpid : 0,
814
p->pEditTail ? p->pEditTail->fpid : 0, p->zUuid);
815
}
816
fossil_print("\nDisplay\n");
817
for(p=pThread->pDisplay; p; p=p->pDisplay){
818
fossil_print("%*s", (p->nIndent-1)*3, "");
819
if( p->pEditTail ){
820
fossil_print("%d->%d", p->fpid, p->pEditTail->fpid);
821
}else{
822
fossil_print("%d", p->fpid);
823
}
824
if( p->iClosed ){
825
fossil_print(" [closed%s]", p->iClosed<0 ? " via parent" : "");
826
}
827
fossil_print("\n");
828
}
829
forumthread_delete(pThread);
830
}
831
832
/*
833
** WEBPAGE: forumthreadhashlist
834
**
835
** Usage: /forumthreadhashlist/HASH-OF-ROOT
836
**
837
** This page (accessibly only to admins) shows a list of all artifacts
838
** associated with a single forum thread. An admin might copy/paste this
839
** list into the /shun page in order to shun an entire thread.
840
*/
841
void forumthreadhashlist(void){
842
int fpid;
843
int froot;
844
const char *zName = P("name");
845
ForumThread *pThread;
846
ForumPost *p;
847
char *fuuid;
848
Stmt q;
849
850
login_check_credentials();
851
if( !g.perm.Admin ){
852
return;
853
}
854
if( zName==0 ){
855
webpage_error("Missing \"name=\" query parameter");
856
}
857
fpid = symbolic_name_to_rid(zName, "f");
858
if( fpid<=0 ){
859
if( fpid==0 ){
860
webpage_notfound_error("Unknown forum id: \"%s\"", zName);
861
}else{
862
ambiguous_page();
863
}
864
return;
865
}
866
froot = db_int(0, "SELECT froot FROM forumpost WHERE fpid=%d", fpid);
867
if( froot==0 ){
868
webpage_notfound_error("Not a forum post: \"%s\"", zName);
869
}
870
fuuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", froot);
871
style_set_current_feature("forum");
872
style_header("Artifacts Of Forum Thread");
873
@ <h2>
874
@ Artifacts associated with the forum thread
875
@ <a href="%R/forumthread/%S(fuuid)">%S(fuuid)</a>:</h2>
876
@ <pre>
877
pThread = forumthread_create(froot, 1);
878
for(p=pThread->pFirst; p; p=p->pNext){
879
@ %h(p->zUuid)
880
}
881
forumthread_delete(pThread);
882
@ </pre>
883
@ <hr>
884
@ <h2>Related FORUMPOST Table Content</h2>
885
@ <table border="1" cellpadding="4" cellspacing="0">
886
@ <tr><th>fpid<th>froot<th>fprev<th>firt<th>fmtime
887
db_prepare(&q, "SELECT fpid, froot, fprev, firt, datetime(fmtime)"
888
" FROM forumpost"
889
" WHERE froot=%d"
890
" ORDER BY fmtime", froot);
891
while( db_step(&q)==SQLITE_ROW ){
892
@ <tr><td>%d(db_column_int(&q,0))\
893
@ <td>%d(db_column_int(&q,1))\
894
@ <td>%d(db_column_int(&q,2))\
895
@ <td>%d(db_column_int(&q,3))\
896
@ <td>%h(db_column_text(&q,4))</tr>
897
}
898
@ </table>
899
db_finalize(&q);
900
style_finish_page();
901
}
902
903
/*
904
** Returns true if the current user is authorized to set forum post
905
** fpid's status.
906
*/
907
static int forum_may_set_status(int fpid){
908
if( moderation_pending(fpid) ) return 0;
909
return g.perm.Admin
910
|| g.perm.ModForum
911
|| (login_is_individual()
912
&& forumpost_is_owner(fpid, 0));
913
}
914
915
/*
916
** If the current user is authorized to set fp's status then this
917
** renders a mini-form for setting the status then redirecting back to
918
** the post. Else it may emit a status label or no output.
919
*/
920
static void forum_render_status_selection( const ForumPost *fp ){
921
const ForumStatusList * const fss = forum_statuses();
922
if( fss->n>1 ){
923
const ForumPost * pHead = fp->pEditHead ? fp->pEditHead : fp;
924
int i;
925
char * zCurrent = 0;
926
const ForumStatus * sCurrent = 0;
927
rid_has_tag2(pHead->fpid, "status", &zCurrent);
928
for( i = 0; i < fss->n; ++i ){
929
const ForumStatus * const fs = &fss->aStatus[i];
930
if( 0==fossil_strcmp(zCurrent, fs->zValue) ){
931
sCurrent = fs;
932
break;
933
}
934
}
935
if( !sCurrent ) sCurrent = &fss->aStatus[0];
936
assert( sCurrent );
937
@ <fieldset class='forum-status-selection'>\
938
@ <legend>Status \
939
@ <span class='help-buttonlet initially-hidden'>\
940
@ Moderators and the post's owner may change \
941
@ the status of this thread unless it is still. \
942
@ pending moderation. See \
943
@ <a href='%R/help/forum-statuses' target='_new'>\
944
@ /help/forum-statuses</a></span>\
945
@ </legend>\
946
if( forum_may_set_status(fp->fpid) ){
947
@ <form method="post" action='%R/forumpost_status'>
948
login_insert_csrf_secret();
949
@ <input type='hidden' name='fpid' value='%s(fp->zUuid)' />
950
@ <select name='status' data-fpid='%s(fp->zUuid)'\
951
@ data-initial-value='%h(zCurrent ? zCurrent : "")'>
952
for( i = 0; i < fss->n; ++i ){
953
const ForumStatus * const fs = &fss->aStatus[i];
954
@ <option value='%h(fs->zValue)'\
955
@ %s(sCurrent==fs ? " selected" : "")>\
956
@ %h(fs->zLabel)</option>
957
}
958
@ </select>
959
@ <input type='button' class='submit action-status' disabled
960
@ value='Change' />
961
/* ^^^ This must be <input>, not <button>, or else tapping it
962
** will unconditionally submit. */
963
@ </form>
964
/* Form is activated in fossil.page.forumpost.js */
965
}else{
966
@ <button disabled>Status: %h(sCurrent->zLabel)</button>
967
}
968
@ </fieldset>
969
fossil_free(zCurrent);
970
}
971
}
972
973
/*
974
** Render a forum post for display
975
*/
976
void forum_render(
977
const char *zTitle, /* The title. Might be NULL for no title */
978
const char *zMimetype, /* Mimetype of the message */
979
const char *zContent, /* Content of the message */
980
const char *zClass, /* Put in a <div> if not NULL */
981
int bScroll /* Large message content scrolls if true */
982
){
983
if( zClass ){
984
@ <div class='%s(zClass)'>
985
}
986
if( zTitle ){
987
if( zTitle[0] ){
988
@ <h1>%h(zTitle)</h1>
989
}else{
990
@ <h1><i>Deleted</i></h1>
991
}
992
}
993
if( zContent && zContent[0] ){
994
Blob x;
995
const int isFossilWiki = zMimetype==0
996
|| fossil_strcmp(zMimetype, "text/x-fossil-wiki")==0;
997
if( bScroll ){
998
@ <div class='forumPostBody'>
999
}else{
1000
@ <div class='forumPostFullBody'>
1001
}
1002
blob_init(&x, 0, 0);
1003
blob_append(&x, zContent, -1);
1004
safe_html_context(DOCSRC_FORUM);
1005
if( isFossilWiki ){
1006
/* Markdown and plain-text rendering add a wrapper DIV resp. PRE
1007
** element around the post, and some CSS relies on its existence
1008
** in order to handle expansion/collapse of the post. Fossil
1009
** Wiki rendering does not do so, so we must wrap those manually
1010
** here. */
1011
@ <div class='fossilWiki'>
1012
}
1013
wiki_render_by_mimetype(&x, zMimetype);
1014
if( isFossilWiki ){
1015
@ </div>
1016
}
1017
blob_reset(&x);
1018
@ </div>
1019
}else{
1020
@ <i>Deleted</i>
1021
}
1022
if( zClass ){
1023
@ </div>
1024
}
1025
}
1026
1027
/*
1028
** Compute a display name from a login name.
1029
**
1030
** If the input login is found in the USER table, then check the USER.INFO
1031
** field to see if it has display-name followed by an email address.
1032
** If it does, that becomes the new display name. If not, let the display
1033
** name just be the login.
1034
**
1035
** Space to hold the returned name is obtained from fossil_strdup() or
1036
** mprintf() and should be freed by the caller.
1037
**
1038
** HTML markup within the reply has been property escaped. Hyperlinks
1039
** may have been added. The result is safe for use with %s.
1040
*/
1041
static char *display_name_from_login(const char *zLogin){
1042
static Stmt q;
1043
char *zResult;
1044
db_static_prepare(&q,
1045
"SELECT display_name(info) FROM user WHERE login=$login"
1046
);
1047
db_bind_text(&q, "$login", zLogin);
1048
if( db_step(&q)==SQLITE_ROW && db_column_type(&q,0)==SQLITE_TEXT ){
1049
const char *zDisplay = db_column_text(&q,0);
1050
if( fossil_strcmp(zDisplay,zLogin)==0 ){
1051
zResult = mprintf("%z%h</a>",
1052
href("%R/timeline?ss=v&y=f&vfx&u=%t",zLogin),zLogin);
1053
}else{
1054
zResult = mprintf("%s (%z%h</a>)", zDisplay,
1055
href("%R/timeline?ss=v&y=f&vfx&u=%t",zLogin),zLogin);
1056
}
1057
}else{
1058
zResult = mprintf("%z%h</a>",
1059
href("%R/timeline?ss=v&y=f&vfx&u=%t",zLogin),zLogin);
1060
}
1061
db_reset(&q);
1062
return zResult;
1063
}
1064
1065
/*
1066
** Compute and return the display name for a ForumPost. If
1067
** pManifest is not NULL, then it is a Manifest object for the post.
1068
** if pManifest is NULL, this routine has to fetch and parse the
1069
** Manifest object for itself.
1070
**
1071
** Memory to hold the display name is attached to p->zDisplayName
1072
** and will be freed together with the ForumPost object p when it
1073
** is freed.
1074
**
1075
** The returned text has had all HTML markup escaped and is safe for
1076
** use within %s.
1077
*/
1078
static char *forum_post_display_name(ForumPost *p, Manifest *pManifest){
1079
Manifest *pToFree = 0;
1080
if( p->zDisplayName ) return p->zDisplayName;
1081
if( pManifest==0 ){
1082
pManifest = pToFree = manifest_get(p->fpid, CFTYPE_FORUM, 0);
1083
if( pManifest==0 ) return "(unknown)";
1084
}
1085
p->zDisplayName = display_name_from_login(pManifest->zUser);
1086
if( pToFree ) manifest_destroy(pToFree);
1087
if( p->zDisplayName==0 ) return "(unknown)";
1088
return p->zDisplayName;
1089
}
1090
1091
/*
1092
** Renders the attachment list for the given forum post.
1093
** Emits no output if there are no attachments.
1094
*/
1095
static void forum_render_attachment_list(const char *zUuid){
1096
#if 1
1097
attachment_list(zUuid, "&#x1f4ce; Attachments", 0
1098
| ATTACHLIST_SIZE
1099
| ATTACHLIST_HIDE_UNAPPROVED
1100
| ATTACHLIST_DETAILS_CLOSED
1101
| ATTACHLIST_HIDE_EMPTY);
1102
#else
1103
char * zLbl = mprintf("<a href='%R/attachlist?forumpost=%!S'>"
1104
"Attachments</a>:", zUuid);
1105
attachment_list(zUuid, zLbl,
1106
ATTACHLIST_HRULE_ABOVE
1107
| ATTACHLIST_SIZE
1108
| ATTACHLIST_HIDE_UNAPPROVED
1109
| ATTACHLIST_HIDE_EMPTY);
1110
fossil_free(zLbl);
1111
#endif
1112
}
1113
1114
/*
1115
** Renders the attachment list for p or (if not NULL) pEditHead.
1116
*/
1117
static void forum_render_attachment_list2(ForumPost *p){
1118
if( p->pEditHead ) p = p->pEditHead;
1119
forum_render_attachment_list(p->zUuid);
1120
}
1121
1122
/* Flags for use with forum_display_post() */
1123
#define FDISPLAY_RAW 0x01 /* omit the border */
1124
#define FDISPLAY_UNFORMATTED 0x02 /* leave the post unformatted */
1125
#define FDISPLAY_HISTORY 0x04 /* Showing edit history */
1126
#define FDISPLAY_SELECTED 0x08 /* This is the selected post */
1127
1128
/*
1129
** Display a single post in a forum thread.
1130
*/
1131
static void forum_display_post(
1132
ForumThread *pThread, /* The thread that this post is a member of */
1133
ForumPost *p, /* Forum post to display */
1134
int iIndentScale, /* Indent scale factor */
1135
int flags, /* From the FDISPLAY_... enum */
1136
char *zQuery /* Common query string */
1137
){
1138
char *zPosterName; /* Name of user who originally made this post */
1139
char *zEditorName; /* Name of user who provided the current edit */
1140
char *zDate; /* The time/date string */
1141
char *zDateZulu; /* The date/time string in Zulul time */
1142
char *zHist; /* History query string */
1143
Manifest *pManifest; /* Manifest comprising the current post */
1144
int bPrivate; /* True for posts awaiting moderation */
1145
int bSameUser; /* True if author is also the reader */
1146
int iIndent; /* Indent level */
1147
int iClosed; /* True if (sub)thread is closed */
1148
const int bRaw = flags & FDISPLAY_RAW;
1149
const int bUnf = flags & FDISPLAY_UNFORMATTED;
1150
const int bHist = flags & FDISPLAY_HISTORY;
1151
const int bSelect = flags & FDISPLAY_SELECTED;
1152
const char *zMimetype;/* Formatting MIME type */
1153
1154
/* Get the manifest for the post. Abort if not found (e.g. shunned). */
1155
pManifest = manifest_get(p->fpid, CFTYPE_FORUM, 0);
1156
if( !pManifest ) return;
1157
iClosed = forumpost_is_closed(pThread, p, 1);
1158
bPrivate = content_is_private(p->fpid);
1159
bSameUser = login_is_individual()
1160
&& fossil_strcmp(pManifest->zUser, g.zLogin)==0;
1161
/* When not in raw mode, create the border around the post. */
1162
if( !bRaw ){
1163
/* Open the <div> enclosing the post. Set the class string to mark the post
1164
** as selected and/or obsolete. */
1165
iIndent = (p->pEditHead ? p->pEditHead->nIndent : p->nIndent)-1;
1166
@ <div id='forum%d(p->fpid)' class='forumpost forumTime\
1167
@ %s(bSelect ? " forumSel" : "")\
1168
@ %s(iClosed ? " forumClosed" : "")\
1169
@ %s(p->pEditTail ? " forumObs" : "")' \
1170
if( iIndent && iIndentScale ){
1171
@ style='margin-left:%d(iIndent*iIndentScale)ex;' \
1172
}
1173
/* These data-X fields are used by the JS editor. */
1174
if( p->pIrt ){
1175
@ data-firt="%s(p->pIrt->zUuid)" \
1176
}
1177
if( p->pEditHead ){
1178
@ data-fedithead="%s(p->pEditHead->zUuid)" \
1179
}
1180
@ data-fpid="%s(p->zUuid)">\
1181
1182
/* If this is the first post (or an edit thereof), emit the thread title. */
1183
if( pManifest->zThreadTitle ){
1184
@ <h1>%h(pManifest->zThreadTitle)</h1>
1185
}
1186
1187
/* Begin emitting the header line. The forum of the title
1188
** varies depending on whether:
1189
** * The post is unedited
1190
** * The post was last edited by the original author
1191
** * The post was last edited by a different person
1192
*/
1193
if( !p->pEditHead ){
1194
zPosterName = forum_post_display_name(p, pManifest);
1195
zEditorName = zPosterName;
1196
}
1197
zDate = db_text(0, "SELECT datetime(%.17g,toLocal())", p->rDate);
1198
zDateZulu = db_text(0,
1199
"SELECT strftime('%%Y-%%m-%%dT%%H:%%M:%%SZ',%.17g)",
1200
p->rDate);
1201
if( p->pEditPrev ){
1202
zPosterName = forum_post_display_name(p->pEditHead, 0);
1203
zEditorName = forum_post_display_name(p, pManifest);
1204
zHist = bHist ? "" : zQuery[0]==0 ? "?hist" : "&hist";
1205
@ <h3 class='forumPostHdr'>(%d(p->sid)\
1206
@ .%0*d(fossil_num_digits(p->nEdit))(p->rev))
1207
if( fossil_strcmp(zPosterName, zEditorName)==0 ){
1208
@ By %s(zPosterName) on \
1209
style_copy_button(1, 0, 0, 0, zDateZulu, "%h", zDate);
1210
@ &#32;edited from \
1211
@ %z(href("%R/forumpost/%S%s%s",p->pEditPrev->zUuid,zQuery,zHist))\
1212
@ %d(p->sid).%0*d(fossil_num_digits(p->nEdit))(p->pEditPrev->rev)</a>
1213
}else{
1214
@ Originally by %s(zPosterName) \
1215
@ with edits by %s(zEditorName) on \
1216
style_copy_button(1, 0, 0, 0, zDateZulu, "%h", zDate);
1217
@ &#32;from \
1218
@ %z(href("%R/forumpost/%S%s%s",p->pEditPrev->zUuid,zQuery,zHist))\
1219
@ %d(p->sid).%0*d(fossil_num_digits(p->nEdit))(p->pEditPrev->rev)</a>
1220
}
1221
}else{
1222
zPosterName = forum_post_display_name(p, pManifest);
1223
@ <h3 class='forumPostHdr'>(%d(p->sid))
1224
@ By %s(zPosterName) on \
1225
style_copy_button(1, 0, 0, 0, zDateZulu, "%h", zDate);
1226
cgi_append_content(" ", 1);
1227
}
1228
fossil_free(zDate);
1229
fossil_free(zDateZulu);
1230
1231
1232
/* If debugging is enabled, link to the artifact page. */
1233
if( g.perm.Debug ){
1234
@ <span class="debug">\
1235
@ <a href="%R/artifact/%h(p->zUuid)">(artifact-%d(p->fpid))</a></span>
1236
}
1237
1238
/* If this is a reply, refer back to the parent post. */
1239
if( p->pIrt ){
1240
@ in reply to %z(href("%R/forumpost/%S%s",p->pIrt->zUuid,zQuery))\
1241
@ %d(p->pIrt->sid)\
1242
if( p->pIrt->nEdit ){
1243
@ .%0*d(fossil_num_digits(p->pIrt->nEdit))(p->pIrt->rev)\
1244
}
1245
@ </a>
1246
}
1247
1248
/* If this post was later edited, refer forward to the next edit. */
1249
if( p->pEditNext ){
1250
@ updated by %z(href("%R/forumpost/%S%s",p->pEditNext->zUuid,zQuery))\
1251
@ %d(p->pEditNext->sid)\
1252
@ .%0*d(fossil_num_digits(p->nEdit))(p->pEditNext->rev)</a>
1253
}
1254
1255
/* Provide a link to select the individual post. */
1256
if( !bSelect ){
1257
@ %z(href("%R/forumpost/%!S%s",p->zUuid,zQuery))[link]</a>
1258
}
1259
1260
/* Provide a link to the raw source code. */
1261
if( !bUnf ){
1262
@ %z(href("%R/forumpost/%!S?raw",p->zUuid))[source]</a>
1263
}
1264
@ </h3>
1265
1266
if( bPrivate && (bSameUser || g.perm.Admin || g.perm.ModForum) ){
1267
moderation_pending_www(p->fpid);
1268
}
1269
}/*!bRaw*/
1270
1271
/* Check if this post is approved, also if it's by the current user.
1272
Render the post if the user is able to see it. */
1273
if( bPrivate && !g.perm.ModForum && !bSameUser ){
1274
@ <p><span class="modpending">Awaiting Moderator Approval</span></p>
1275
}else{
1276
if( bRaw || bUnf || p->pEditTail ){
1277
zMimetype = "text/plain";
1278
}else{
1279
zMimetype = pManifest->zMimetype;
1280
}
1281
forum_render(0, zMimetype, pManifest->zWiki, 0, !bRaw);
1282
}
1283
1284
/* When not in raw mode, finish creating the border around the post. */
1285
if( !bRaw ){
1286
int bBrBeforeAttach = 0; /* Layout kludge for Attach button */
1287
forum_render_attachment_list2(p);
1288
/* If the user is able to write to the forum and if this post has not been
1289
** edited, create a form with various interaction buttons. */
1290
if( g.perm.WrForum && !p->pEditTail ){
1291
@ <div class="forumpost-single-controls">\
1292
@ <form action="%R/forumedit" method="POST">
1293
@ <input type="hidden" name="fpid" value="%s(p->zUuid)">
1294
if( !bPrivate ){
1295
/* Reply and Edit are only available if the post has been
1296
** approved. Closed threads can only be edited or replied to
1297
** if forumpost_may_close() is true but a user may delete
1298
** their own posts even if they are closed. */
1299
if( forumpost_may_close() || !iClosed ){
1300
@ <input type="submit" name="reply" value="Reply">
1301
if( g.perm.Admin || (bSameUser && !iClosed) ){
1302
@ <input type="submit" name="edit" value="Edit">
1303
}
1304
if( g.perm.Admin || bSameUser ){
1305
@ <input type="submit" name="nullout" value="Delete">
1306
}
1307
}
1308
}else if( g.perm.ModForum ){
1309
/* Allow moderators to approve or reject pending posts. Also allow
1310
** forum supervisors to mark non-special users as trusted and therefore
1311
** able to post unmoderated. */
1312
@ <input type="submit" name="approve" value="Approve" \
1313
@ class="action-approve">\
1314
@ <input type="submit" name="reject" value="Reject" \
1315
@ class="action-reject">\
1316
if( g.perm.AdminForum && !login_is_special(pManifest->zUser) ){
1317
@ <br><label><input type="checkbox" name="trust">
1318
@ Trust user "%h(pManifest->zUser)" so that future posts by \
1319
@ "%h(pManifest->zUser)" do not require moderation.
1320
@ </label>
1321
@ <input type="hidden" name="trustuser" value="%h(pManifest->zUser)">
1322
bBrBeforeAttach = 1 /* slightly unmangle the layout */;
1323
}
1324
}else if( bSameUser ){
1325
/* Allow users to delete (reject) their own pending posts. */
1326
@ <input type="submit" name="reject" value="Delete">
1327
}
1328
login_insert_csrf_secret();
1329
@ </form>
1330
1331
if( bSelect ){
1332
const ForumPost *pHead = p->pEditHead ? p->pEditHead : p;
1333
if( !bPrivate && forumpost_may_close() && iClosed>=0 ){
1334
@ <form method="post" \
1335
@ action='%R/forumpost_%s(iClosed > 0 ? "reopen" : "close")'>
1336
login_insert_csrf_secret();
1337
@ <input type="hidden" name="fpid" value="%s(p->zUuid)" />
1338
if( moderation_pending(p->fpid)==0 ){
1339
@ <input type="button" value='%s(iClosed ? "Re-open" : "Close")' \
1340
@ class='submit hidden \
1341
@ %s(iClosed ? "action-reopen" : "action-close")'/>
1342
/* ^^^ activated by fossil.page.forumpost.js */
1343
}
1344
@ </form>
1345
}
1346
if( attach_user_may(p/*not pHead*/->fpid, CFTYPE_FORUM) ){
1347
/* When an admin edits someone else's post, the admin
1348
** effectively takes over ownership of it (and we currently
1349
** have no way of passing it back). Because of this, we
1350
** check the ownership of `p` instead of `pHead`. */
1351
if( bBrBeforeAttach ){
1352
@ <br>
1353
}
1354
attach_render_attachadd_button(pHead->zUuid);
1355
}
1356
}
1357
@ </div>
1358
}
1359
if( !p->pIrt && (flags & FDISPLAY_SELECTED)){
1360
forum_render_status_selection(p);
1361
}
1362
@ </div>
1363
}/*!bRaw*/
1364
1365
/* Clean up. */
1366
manifest_destroy(pManifest);
1367
}
1368
1369
/*
1370
** Possible display modes for forum_display_thread().
1371
*/
1372
enum {
1373
FD_RAW, /* Like FD_SINGLE, but additionally omit the border, force
1374
** unformatted mode, and inhibit history mode */
1375
FD_SINGLE, /* Render a single post and (optionally) its edit history */
1376
FD_CHRONO, /* Render all posts in chronological order */
1377
FD_HIER, /* Render all posts in an indented hierarchy */
1378
};
1379
1380
/*
1381
** Display a forum thread. If mode is FD_RAW or FD_SINGLE, display only a
1382
** single post from the thread and (optionally) its edit history.
1383
*/
1384
static void forum_display_thread(
1385
int froot, /* Forum thread root post ID */
1386
int fpid, /* Selected forum post ID, or 0 if none selected */
1387
int mode, /* Forum display mode, one of the FD_* enumerations */
1388
int autoMode, /* mode was selected automatically */
1389
int bUnf, /* True if rendering unformatted */
1390
int bHist /* True if showing edit history, ignored for FD_RAW */
1391
){
1392
ForumThread *pThread; /* Thread structure */
1393
ForumPost *pSelect; /* Currently selected post, or NULL if none */
1394
ForumPost *p; /* Post iterator pointer */
1395
char zQuery[30]; /* Common query string */
1396
int iIndentScale = 4; /* Indent scale factor, measured in "ex" units */
1397
int sid; /* Comparison serial ID */
1398
int i;
1399
1400
/* In raw mode, force unformatted display and disable history. */
1401
if( mode == FD_RAW ){
1402
bUnf = 1;
1403
bHist = 0;
1404
}
1405
1406
/* Thread together the posts and (optionally) compute the hierarchy. */
1407
pThread = forumthread_create(froot, mode==FD_HIER);
1408
1409
/* Compute the appropriate indent scaling. */
1410
if( mode==FD_HIER ){
1411
iIndentScale = 4;
1412
while( iIndentScale>1 && iIndentScale*pThread->mxIndent>25 ){
1413
iIndentScale--;
1414
}
1415
}else{
1416
iIndentScale = 0;
1417
}
1418
1419
/* Find the selected post, or (depending on parameters) its latest edit. */
1420
pSelect = fpid ? forumpost_forward(pThread->pFirst, fpid) : 0;
1421
if( !bHist && mode!=FD_RAW && pSelect && pSelect->pEditTail ){
1422
pSelect = pSelect->pEditTail;
1423
}
1424
1425
/* When displaying only a single post, abort if no post was selected or the
1426
** selected forum post does not exist in the thread. Otherwise proceed to
1427
** display the entire thread without marking any posts as selected. */
1428
if( !pSelect && (mode==FD_RAW || mode==FD_SINGLE) ){
1429
return;
1430
}
1431
1432
/* Create the common query string to append to nearly all post links. */
1433
i = 0;
1434
if( !autoMode ){
1435
char m = 'a';
1436
switch( mode ){
1437
case FD_RAW: m = 'r'; break;
1438
case FD_CHRONO: m = 'c'; break;
1439
case FD_HIER: m = 'h'; break;
1440
case FD_SINGLE: m = 's'; break;
1441
}
1442
zQuery[i++] = '?';
1443
zQuery[i++] = 't';
1444
zQuery[i++] = '=';
1445
zQuery[i++] = m;
1446
}
1447
if( bUnf ){
1448
zQuery[i] = i==0 ? '?' : '&'; i++;
1449
zQuery[i++] = 'u';
1450
zQuery[i++] = 'n';
1451
zQuery[i++] = 'f';
1452
}
1453
if( bHist ){
1454
zQuery[i] = i==0 ? '?' : '&'; i++;
1455
zQuery[i++] = 'h';
1456
zQuery[i++] = 'i';
1457
zQuery[i++] = 's';
1458
zQuery[i++] = 't';
1459
}
1460
assert( i<(int)sizeof(zQuery) );
1461
zQuery[i] = 0;
1462
assert( zQuery[0]==0 || zQuery[0]=='?' );
1463
1464
/* Identify which post to display first. If history is shown, start with the
1465
** original, unedited post. Otherwise advance to the post's latest edit. */
1466
if( mode==FD_RAW || mode==FD_SINGLE ){
1467
p = pSelect;
1468
if( bHist && p->pEditHead ) p = p->pEditHead;
1469
}else{
1470
p = mode==FD_CHRONO ? pThread->pFirst : pThread->pDisplay;
1471
if( !bHist && p->pEditTail ) p = p->pEditTail;
1472
}
1473
1474
/* Display the appropriate subset of posts in sequence. */
1475
while( p ){
1476
/* Display the post. */
1477
forum_display_post(
1478
pThread, p, iIndentScale,
1479
(mode==FD_RAW ? FDISPLAY_RAW : 0) |
1480
(bUnf ? FDISPLAY_UNFORMATTED : 0) |
1481
(bHist ? FDISPLAY_HISTORY : 0) |
1482
(p==pSelect ? FDISPLAY_SELECTED : 0),
1483
zQuery
1484
);
1485
1486
/* Advance to the next post in the thread. */
1487
if( mode==FD_CHRONO ){
1488
/* Chronological mode: display posts (optionally including edits) in their
1489
** original commit order. */
1490
if( bHist ){
1491
p = p->pNext;
1492
}else{
1493
sid = p->sid;
1494
if( p->pEditHead ) p = p->pEditHead;
1495
do p = p->pNext; while( p && p->sid<=sid );
1496
if( p && p->pEditTail ) p = p->pEditTail;
1497
}
1498
}else if( bHist && p->pEditNext ){
1499
/* Hierarchical and single mode: display each post's edits in sequence. */
1500
p = p->pEditNext;
1501
}else if( mode==FD_HIER ){
1502
/* Hierarchical mode: after displaying with each post (optionally
1503
** including edits), go to the next post in computed display order. */
1504
p = p->pEditHead ? p->pEditHead->pDisplay : p->pDisplay;
1505
if( !bHist && p && p->pEditTail ) p = p->pEditTail;
1506
}else{
1507
/* Single and raw mode: terminate after displaying the selected post and
1508
** (optionally) its edits. */
1509
break;
1510
}
1511
}
1512
1513
/* Undocumented "threadtable" query parameter causes thread table to be
1514
** displayed for debugging purposes. */
1515
if( PB("threadtable") ){
1516
@ <hr>
1517
@ <table border="1" cellpadding="3" cellspacing="0">
1518
@ <tr><th>sid<th>rev<th>fpid<th>pIrt<th>pEditHead<th>pEditTail\
1519
@ <th>pEditNext<th>pEditPrev<th>pDisplay<th>hash
1520
for(p=pThread->pFirst; p; p=p->pNext){
1521
@ <tr><td>%d(p->sid)<td>%d(p->rev)<td>%d(p->fpid)\
1522
@ <td>%d(p->pIrt ? p->pIrt->fpid : 0)\
1523
@ <td>%d(p->pEditHead ? p->pEditHead->fpid : 0)\
1524
@ <td>%d(p->pEditTail ? p->pEditTail->fpid : 0)\
1525
@ <td>%d(p->pEditNext ? p->pEditNext->fpid : 0)\
1526
@ <td>%d(p->pEditPrev ? p->pEditPrev->fpid : 0)\
1527
@ <td>%d(p->pDisplay ? p->pDisplay->fpid : 0)\
1528
@ <td>%S(p->zUuid)</tr>
1529
}
1530
@ </table>
1531
}
1532
1533
/* Clean up. */
1534
forumthread_delete(pThread);
1535
}
1536
1537
/*
1538
** Emit Forum Javascript which applies (or optionally can apply)
1539
** to all forum-related pages. It does not include page-specific
1540
** code (e.g. "forum.js").
1541
*/
1542
static void forum_emit_js(void){
1543
builtin_fossil_js_bundle_or("copybutton", "pikchr", "confirmer",
1544
"attach", "tabs", "storage",
1545
"popupwidget", NULL);
1546
builtin_request_js("fossil.page.forumpost.js");
1547
}
1548
1549
/*
1550
** WEBPAGE: forumpost
1551
**
1552
** Show a single forum posting. The posting is shown in context with
1553
** its entire thread. The selected posting is enclosed within
1554
** <div class='forumSel'>...</div>. Javascript is used to move the
1555
** selected posting into view after the page loads.
1556
**
1557
** Query parameters:
1558
**
1559
** name=NAME Required. The NAME value is a hash prefix or timestamp
1560
** for the post to be displayed.
1561
** t=a Automatic display mode, i.e. hierarchical for
1562
** desktop and chronological for mobile. This is the
1563
** default if the "t" query parameter is omitted.
1564
** t=c Show posts in the order they were written.
1565
** t=h Show posts using hierarchical indenting.
1566
** t=s Show only the post specified by "name=X".
1567
** t=r Alias for "t=c" with "unf" and "hist".
1568
** t=y Alias for "t=s" with "unf" and "hist".
1569
** raw Alias for "t=s" with "unf". Additionally, omit
1570
** the border around the post, and ignore "t" and "hist".
1571
** unf Show the original, unformatted source text.
1572
** hist Show edit history in addition to current posts.
1573
*/
1574
void forumpost_page(void){
1575
forumthread_page();
1576
}
1577
1578
/*
1579
** WEBPAGE: forumthread
1580
**
1581
** Show all forum messages associated with a particular message thread.
1582
** The result is basically the same as /forumpost except that none of
1583
** the postings in the thread are selected.
1584
**
1585
** Query parameters:
1586
**
1587
** name=NAME Required. The NAME value is a hash prefix or timestamp
1588
** for one of the posts in the thread.
1589
** t=a Automatic display mode, i.e. hierarchical for
1590
** desktop and chronological for mobile. This is the
1591
** default if the "t" query parameter is omitted.
1592
** t=c Show posts in the order they were written.
1593
** t=h Show posts using hierarchical indenting.
1594
** unf Show the original, unformatted source text.
1595
** hist Show edit history in addition to current posts.
1596
*/
1597
void forumthread_page(void){
1598
int fpid;
1599
int froot;
1600
char *zThreadTitle;
1601
const char *zName = P("name");
1602
const char *zMode = PD("t","a");
1603
int bRaw = PB("raw");
1604
int bUnf = PB("unf");
1605
int bHist = PB("hist");
1606
int mode = 0;
1607
int autoMode = 0;
1608
login_check_credentials();
1609
if( !g.perm.RdForum ){
1610
login_needed(g.anon.RdForum);
1611
return;
1612
}
1613
if( zName==0 ){
1614
webpage_error("Missing \"name=\" query parameter");
1615
return;
1616
}
1617
cgi_check_for_malice();
1618
fpid = symbolic_name_to_rid(zName, "f");
1619
if( fpid<=0 ){
1620
if( fpid==0 ){
1621
webpage_notfound_error("Unknown forum id: \"%s\"", zName);
1622
}else{
1623
ambiguous_page();
1624
}
1625
return;
1626
}
1627
froot = db_int(0, "SELECT froot FROM forumpost WHERE fpid=%d", fpid);
1628
if( froot==0 ){
1629
webpage_notfound_error("Not a forum post: \"%s\"", zName);
1630
return;
1631
}
1632
1633
/* Decode the mode parameters. */
1634
if( bRaw ){
1635
mode = FD_RAW;
1636
bUnf = 1;
1637
bHist = 0;
1638
cgi_replace_query_parameter("unf", "on");
1639
cgi_delete_query_parameter("hist");
1640
cgi_delete_query_parameter("raw");
1641
}else{
1642
switch( *zMode ){
1643
case 'a': mode = cgi_from_mobile() ? FD_CHRONO : FD_HIER;
1644
autoMode=1; break;
1645
case 'c': mode = FD_CHRONO; break;
1646
case 'h': mode = FD_HIER; break;
1647
case 's': mode = FD_SINGLE; break;
1648
case 'r': mode = FD_CHRONO; break;
1649
case 'y': mode = FD_SINGLE; break;
1650
default: webpage_error("Invalid thread mode: \"%s\"", zMode);
1651
}
1652
if( *zMode=='r' || *zMode=='y') {
1653
bUnf = 1;
1654
bHist = 1;
1655
cgi_replace_query_parameter("t", mode==FD_CHRONO ? "c" : "s");
1656
cgi_replace_query_parameter("unf", "on");
1657
cgi_replace_query_parameter("hist", "on");
1658
}
1659
}
1660
1661
/* Define the page header. */
1662
zThreadTitle = db_text("",
1663
"SELECT"
1664
" substr(event.comment,instr(event.comment,':')+2)"
1665
" FROM forumpost, event"
1666
" WHERE event.objid=forumpost.fpid"
1667
" AND forumpost.fpid=%d;",
1668
fpid
1669
);
1670
style_set_current_feature("forum");
1671
style_header("%s%s", zThreadTitle, *zThreadTitle ? "" : "Forum");
1672
fossil_free(zThreadTitle);
1673
if( mode!=FD_CHRONO ){
1674
style_submenu_element("Chronological", "%R/%s/%s?t=c%s%s", g.zPath, zName,
1675
bUnf ? "&unf" : "", bHist ? "&hist" : "");
1676
}
1677
if( mode!=FD_HIER ){
1678
style_submenu_element("Hierarchical", "%R/%s/%s?t=h%s%s", g.zPath, zName,
1679
bUnf ? "&unf" : "", bHist ? "&hist" : "");
1680
}
1681
style_submenu_checkbox("unf", "Unformatted", 0, 0);
1682
style_submenu_checkbox("hist", "History", 0, 0);
1683
if( g.perm.Admin ){
1684
style_submenu_element("Artifacts", "%R/forumthreadhashlist/%t", zName);
1685
}
1686
1687
/* Display the thread. */
1688
if( fossil_strcmp(g.zPath,"forumthread")==0 ) fpid = 0;
1689
forum_display_thread(froot, fpid, mode, autoMode, bUnf, bHist);
1690
1691
/* Emit Forum Javascript. */
1692
builtin_request_js("forum.js");
1693
forum_emit_js();
1694
1695
/* Emit the page style. */
1696
style_finish_page();
1697
}
1698
1699
/*
1700
** Return true if a forum post should be moderated.
1701
*/
1702
int forum_need_moderation(void){
1703
if( P("domod") ) return 1;
1704
if( g.perm.WrTForum ) return 0;
1705
if( g.perm.ModForum ) return 0;
1706
return 1;
1707
}
1708
1709
/* Flags for use with forum_post() */
1710
#define FPOST_NO_ALERT 1 /* do not send any alerts */
1711
#define FPOST_DRYRUN 2 /* do not save the artifact */
1712
1713
/*
1714
** Return a flags value for use with the final argument to
1715
** forum_post(), extracted from the CGI environment.
1716
*/
1717
static int forum_post_flags(void){
1718
int iPostFlags = 0;
1719
if( g.perm.Debug && P("fpsilent")!=0 ){
1720
iPostFlags |= FPOST_NO_ALERT;
1721
}
1722
if( P("dryrun")!=0 ){
1723
iPostFlags |= FPOST_DRYRUN;
1724
}
1725
return iPostFlags;
1726
}
1727
1728
/*
1729
** Add a new Forum Post artifact to the repository.
1730
**
1731
** Return true if a redirect occurs.
1732
*/
1733
static int forum_post(
1734
const char *zTitle, /* Title. NULL for replies */
1735
int iInReplyTo, /* Post replying to. 0 for new threads */
1736
int iEdit, /* Post being edited, or zero for a new post */
1737
const char *zUser, /* Username. NULL means use login name */
1738
const char *zMimetype, /* Mimetype of content. */
1739
const char *zContent, /* Content */
1740
int iFlags /* FPOST_xyz flag values */
1741
){
1742
char *zDate;
1743
char *zI;
1744
char *zG;
1745
int iBasis;
1746
Blob x, cksum, formatCheck, errMsg;
1747
Manifest *pPost;
1748
int nContent = zContent ? (int)strlen(zContent) : 0;
1749
1750
schema_forum();
1751
if( !g.perm.Admin && (iEdit || iInReplyTo)
1752
&& forum_rid_is_tagged(iEdit ? iEdit : iInReplyTo, "closed", 1) ){
1753
forumpost_error_closed();
1754
return 0;
1755
}
1756
if( iEdit==0 && fossil_all_whitespace(zContent) ){
1757
return 0;
1758
}
1759
if( iInReplyTo==0 && iEdit>0 ){
1760
iBasis = iEdit;
1761
iInReplyTo = db_int(0, "SELECT firt FROM forumpost WHERE fpid=%d", iEdit);
1762
}else{
1763
iBasis = iInReplyTo;
1764
}
1765
webpage_assert( (zTitle==0)+(iInReplyTo==0)==1 );
1766
blob_init(&x, 0, 0);
1767
zDate = date_in_standard_format("now");
1768
blob_appendf(&x, "D %s\n", zDate);
1769
fossil_free(zDate);
1770
zG = db_text(0,
1771
"SELECT uuid FROM blob, forumpost"
1772
" WHERE blob.rid==forumpost.froot"
1773
" AND forumpost.fpid=%d", iBasis);
1774
if( zG ){
1775
blob_appendf(&x, "G %s\n", zG);
1776
fossil_free(zG);
1777
}
1778
if( zTitle ){
1779
blob_appendf(&x, "H %F\n", zTitle);
1780
}
1781
zI = rid_to_uuid(iInReplyTo);
1782
if( zI ){
1783
blob_appendf(&x, "I %s\n", zI);
1784
fossil_free(zI);
1785
}
1786
if( zMimetype!=0
1787
&& zMimetype[0]!=0
1788
&& fossil_strcmp(zMimetype,"text/x-fossil-wiki")!=0 ){
1789
blob_appendf(&x, "N %F\n", zMimetype);
1790
}
1791
if( iEdit>0 ){
1792
char *zP = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", iEdit);
1793
if( zP==0 ) webpage_error("missing edit artifact %d", iEdit);
1794
blob_appendf(&x, "P %s\n", zP);
1795
fossil_free(zP);
1796
}
1797
if( zUser==0 ){
1798
if( login_is_nobody() ){
1799
zUser = "anonymous";
1800
}else{
1801
zUser = login_name();
1802
}
1803
}
1804
blob_appendf(&x, "U %F\n", zUser);
1805
blob_appendf(&x, "W %d\n%s\n", nContent, zContent);
1806
md5sum_blob(&x, &cksum);
1807
blob_appendf(&x, "Z %b\n", &cksum);
1808
blob_reset(&cksum);
1809
1810
/* Verify that the artifact we are creating is well-formed */
1811
blob_init(&formatCheck, 0, 0);
1812
blob_init(&errMsg, 0, 0);
1813
blob_copy(&formatCheck, &x);
1814
pPost = manifest_parse(&formatCheck, 0, &errMsg);
1815
if( pPost==0 ){
1816
webpage_error("malformed forum post artifact - %s", blob_str(&errMsg));
1817
}
1818
webpage_assert( pPost->type==CFTYPE_FORUM );
1819
manifest_destroy(pPost);
1820
1821
if( (iFlags & FPOST_DRYRUN)!=0 ){
1822
@ <div class='debug'>
1823
@ This is the artifact that would have been generated:
1824
@ <pre>%h(blob_str(&x))</pre>
1825
@ </div>
1826
blob_reset(&x);
1827
return 0;
1828
}else{
1829
int nrid;
1830
db_begin_transaction();
1831
nrid = wiki_put(&x, iEdit>0 ? iEdit : 0, forum_need_moderation());
1832
blob_reset(&x);
1833
if( (iFlags & FPOST_NO_ALERT)!=0 ){
1834
alert_unqueue('f', nrid);
1835
}
1836
db_commit_transaction();
1837
cgi_redirectf("%R/forumpost/%S", rid_to_uuid(nrid));
1838
return 1;
1839
}
1840
}
1841
1842
/*
1843
** Paint the form elements for entering a Forum post
1844
*/
1845
static void forum_post_widget(
1846
const char *zTitle,
1847
const char *zMimetype,
1848
const char *zContent
1849
){
1850
if( zTitle ){
1851
@ Title: <input type="input" name="title" value="%h(zTitle)" size="50"
1852
@ maxlength="125"><br>
1853
}
1854
@ %z(href("%R/markup_help"))Markup style</a>:
1855
mimetype_option_menu(zMimetype, "mimetype");
1856
@ <div class="forum-editor-widget">
1857
@ <textarea aria-label="Content:" name="content" class="wikiedit" \
1858
@ cols="80" rows="25" wrap="virtual">%h(zContent)</textarea></div>
1859
}
1860
1861
/*
1862
** If PD("fpid") refers to a forum post, its rid is returned, else
1863
** this function emits an error does not does return.
1864
*/
1865
static int forum_validate_fpid_param(void){
1866
const char *zFpid = PD("fpid","");
1867
int fpid = symbolic_name_to_rid(zFpid, "f");
1868
if( fpid<=0 ){
1869
webpage_error("Missing or invalid fpid parameter.");
1870
}
1871
return fpid;
1872
}
1873
1874
/*
1875
** Internal helper for /forumpost_XYZ internal pages which tag/untag
1876
** posts.
1877
*/
1878
static void forumpost_action_helper(const char *zTag, const char *zVal,
1879
int addTag, int validFpid){
1880
if( !cgi_csrf_safe(2) ){
1881
webpage_error("CSRF validation failed");
1882
}else{
1883
const int fpid = validFpid>0 ? validFpid : forum_validate_fpid_param();
1884
if( fpid>0 ){
1885
if( forumpost_tag(fpid, addTag, zTag, zVal) < 0 ){
1886
webpage_error("Tagging artifact failed: %s", g.zErrMsg);
1887
}else{
1888
cgi_redirectf("%R/forumpost/%S",P("fpid"));
1889
}
1890
}
1891
}
1892
}
1893
1894
/*
1895
** WEBPAGE: forumpost_close hidden
1896
** WEBPAGE: forumpost_reopen hidden
1897
**
1898
** fpid=X Hash of the post to be edited. REQUIRED
1899
**
1900
** Closes or re-opens the given forum post, within the bounds of the
1901
** API for forumpost_tag(). After (perhaps) modifying the "closed"
1902
** status of the given thread, it redirects to that post's thread
1903
** view. Requires admin privileges.
1904
*/
1905
void forum_page_close(void){
1906
login_check_credentials();
1907
if( forumpost_may_close()==0 ){
1908
login_needed(g.anon.Admin);
1909
}else{
1910
const int bIsAdd = sqlite3_strglob("*_close*", g.zPath)==0;
1911
forumpost_action_helper("closed", 0, bIsAdd, 0);
1912
}
1913
}
1914
1915
/*
1916
** WEBPAGE: forumpost_status hidden
1917
**
1918
** fpid=X Hash of the post to be edited. REQUIRED
1919
** status=Y New status value. REQUIRED
1920
**
1921
** Updates the current status=Y tag on the first version of
1922
** the forum post X. Requires forum_may_set_status() permissions.
1923
*/
1924
void forum_page_status(void){
1925
int fpid;
1926
login_check_credentials();
1927
fpid = forum_validate_fpid_param();
1928
if(forum_may_set_status(fpid)){
1929
const char *zStatus = PD("status",0);
1930
if( !zStatus || !zStatus[0] ){
1931
webpage_error("Missing required status.");
1932
}
1933
forumpost_action_helper("status", zStatus, 1, fpid);
1934
}else{
1935
webpage_error("You lack permissions to change this post's status.");
1936
}
1937
}
1938
1939
/*
1940
** WEBPAGE: forumnew
1941
** WEBPAGE: forumedit
1942
**
1943
** Start a new thread on the forum or reply to an existing thread.
1944
** But first prompt to see if the user would like to log in.
1945
*/
1946
void forum_page_init(void){
1947
int isEdit;
1948
char *zGoto;
1949
1950
login_check_credentials();
1951
if( !g.perm.WrForum ){
1952
login_needed(g.anon.WrForum);
1953
return;
1954
}
1955
if( sqlite3_strglob("*edit*", g.zPath)==0 ){
1956
zGoto = mprintf("forume2?fpid=%S",PD("fpid",""));
1957
isEdit = 1;
1958
}else{
1959
zGoto = mprintf("forume1");
1960
isEdit = 0;
1961
}
1962
if( login_is_individual() ){
1963
if( isEdit ){
1964
forumedit_page();
1965
}else{
1966
forumnew_page();
1967
}
1968
return;
1969
}
1970
style_set_current_feature("forum");
1971
style_header("%h As Anonymous?", isEdit ? "Reply" : "Post");
1972
@ <p>You are not logged in.
1973
@ <p><table border="0" cellpadding="10">
1974
@ <tr><td>
1975
@ <form action="%s(zGoto)" method="POST">
1976
@ <input type="submit" value="Remain Anonymous">
1977
@ </form>
1978
@ <td>Post to the forum anonymously
1979
if( login_self_register_available(0) ){
1980
@ <tr><td>
1981
@ <form action="%R/register" method="POST">
1982
@ <input type="hidden" name="g" value="%s(zGoto)">
1983
@ <input type="submit" value="Create An Account">
1984
@ </form>
1985
@ <td>Create a new account and post using that new account
1986
}
1987
@ <tr><td>
1988
@ <form action="%R/login" method="POST">
1989
@ <input type="hidden" name="g" value="%s(zGoto)">
1990
@ <input type="hidden" name="noanon" value="1">
1991
@ <input type="submit" value="Login">
1992
@ </form>
1993
@ <td>Log into an existing account
1994
@ </table>
1995
forum_emit_js();
1996
style_finish_page();
1997
fossil_free(zGoto);
1998
}
1999
2000
/*
2001
** Write the "From: USER" line on the webpage.
2002
*/
2003
static void forum_from_line(void){
2004
if( login_is_nobody() ){
2005
@ From: anonymous<br>
2006
}else{
2007
@ From: %h(login_name())<br>
2008
}
2009
}
2010
2011
static void forum_render_debug_options(void){
2012
if( g.perm.Debug ){
2013
/* Give extra control over the post to users with the special
2014
* Debug capability, which includes Admin and Setup users */
2015
@ <div class="debug">
2016
@ <label><input type="checkbox" name="dryrun" %s(PCK("dryrun"))> \
2017
@ Dry run</label>
2018
@ <br><label><input type="checkbox" name="domod" %s(PCK("domod"))> \
2019
@ Require moderator approval</label>
2020
@ <br><label><input type="checkbox" name="showqp" %s(PCK("showqp"))> \
2021
@ Show query parameters</label>
2022
@ <br><label><input type="checkbox" name="fpsilent" %s(PCK("fpsilent"))> \
2023
@ Do not send notification emails</label>
2024
@ </div>
2025
}
2026
}
2027
2028
/*
2029
** If the user has AttachForum permissions, emit a notice that
2030
** attachments may be added after saving. If p is not NULL, also emit
2031
** its list of attachments. We only emit this for No-JS environments,
2032
** as in JS environments the interactive forum editor includes
2033
** attachment support.
2034
*/
2035
static void forum_render_attachment_notice(void){
2036
if( g.perm.AttachForum ){
2037
@ <noscript><div>You will be able to attach files to this post
2038
@ after saving it.</div></noscript>
2039
}
2040
}
2041
2042
/*
2043
** WEBPAGE: forume1 hidden
2044
**
2045
** Start a new forum thread.
2046
*/
2047
void forumnew_page(void){
2048
const char *zTitle = PDT("title","");
2049
const char *zMimetype = PD("mimetype",DEFAULT_FORUM_MIMETYPE);
2050
const char *zContent = PDT("content","");
2051
2052
login_check_credentials();
2053
if( !g.perm.WrForum ){
2054
login_needed(g.anon.WrForum);
2055
return;
2056
}
2057
if( P("submit") && cgi_csrf_safe(2) ){
2058
if( forum_post(zTitle, 0, 0, 0, zMimetype, zContent,
2059
forum_post_flags()) ) return;
2060
}
2061
if( P("preview") && !fossil_all_whitespace(zContent) ){
2062
@ <h1>Preview:</h1>
2063
forum_render(zTitle, zMimetype, zContent, "forumEdit", 1);
2064
}
2065
style_set_current_feature("forum");
2066
style_header("New Forum Thread");
2067
2068
@ <form action="%R/forume1" method="POST" \
2069
@ class="remove-if-replaced">
2070
@ <h1>New Thread:</h1>
2071
forum_from_line();
2072
forum_post_widget(zTitle, zMimetype, zContent);
2073
@ <input type="submit" name="preview" value="Preview">
2074
if( P("preview") && !fossil_all_whitespace(zContent) ){
2075
@ <input type="submit" name="submit" value="Submit">
2076
}else{
2077
@ <input type="submit" name="submit" value="Submit" disabled>
2078
}
2079
forum_render_debug_options();
2080
login_insert_csrf_secret();
2081
@ </form>
2082
/* When JS is disabled the block above will work. When it's
2083
enabled, the above will be removed and JS will render the editor
2084
form in the next element. */
2085
@ <div hidden id='forumnew-placeholder'>
2086
@ <input type='hidden' name='title' value='%h(zTitle)'>
2087
login_insert_csrf_secret();
2088
@ </div>
2089
forum_render_attachment_notice();
2090
forum_emit_js();
2091
style_finish_page();
2092
}
2093
2094
/*
2095
** WEBPAGE: forume2 hidden
2096
**
2097
** Edit an existing forum message.
2098
** Query parameters:
2099
**
2100
** fpid=X Hash of the post to be edited. REQUIRED
2101
*/
2102
void forumedit_page(void){
2103
int fpid;
2104
int froot;
2105
Manifest *pPost = 0;
2106
Manifest *pRootPost = 0;
2107
const char *zMimetype = 0;
2108
const char *zContent = 0;
2109
const char *zTitle = 0;
2110
char *zDate = 0;
2111
const char *zFpid = PD("fpid","");
2112
int isCsrfSafe;
2113
int isDelete = 0;
2114
int iClosed = 0;
2115
int bSameUser; /* True if author is also the reader */
2116
int bPreview; /* True in preview mode. */
2117
int bPrivate; /* True if post is private (not yet moderated) */
2118
int bReply; /* True if replying to a post */
2119
2120
login_check_credentials();
2121
if( !g.perm.WrForum ){
2122
login_needed(g.anon.WrForum);
2123
return;
2124
}
2125
fpid = symbolic_name_to_rid(zFpid, "f");
2126
if( fpid<=0 || (pPost = manifest_get(fpid, CFTYPE_FORUM, 0))==0 ){
2127
webpage_error("Missing or invalid fpid query parameter");
2128
}
2129
froot = db_int(0, "SELECT froot FROM forumpost WHERE fpid=%d", fpid);
2130
if( (froot==0 || (pRootPost = manifest_get(froot, CFTYPE_FORUM, 0))==0)
2131
&& P("reject")==0
2132
){
2133
webpage_error("fpid does not appear to be a forum post: \"%d\"", fpid);
2134
}
2135
if( P("cancel") ){
2136
cgi_redirectf("%R/forumpost/%S",zFpid);
2137
return;
2138
}
2139
bPreview = P("preview")!=0;
2140
bReply = P("reply")!=0;
2141
iClosed = forum_rid_is_tagged(fpid, "closed", 1);
2142
isCsrfSafe = cgi_csrf_safe(2);
2143
bPrivate = content_is_private(fpid);
2144
bSameUser = login_is_individual()
2145
&& fossil_strcmp(pPost->zUser, g.zLogin)==0;
2146
if( isCsrfSafe && (g.perm.ModForum || (bPrivate && bSameUser)) ){
2147
if( g.perm.ModForum && P("approve") ){
2148
const char *zUserToTrust;
2149
moderation_forumpost_approve(fpid);
2150
if( g.perm.AdminForum
2151
&& PB("trust")
2152
&& (zUserToTrust = P("trustuser"))!=0
2153
){
2154
db_unprotect(PROTECT_USER);
2155
db_multi_exec("UPDATE user SET cap=cap||'4' "
2156
"WHERE login=%Q AND cap NOT GLOB '*4*'",
2157
zUserToTrust);
2158
db_protect_pop();
2159
}
2160
cgi_redirectf("%R/forumpost/%S",P("fpid"));
2161
return;
2162
}
2163
if( P("reject") ){
2164
char *zParent =
2165
db_text(0,
2166
"SELECT uuid FROM forumpost, blob"
2167
" WHERE forumpost.fpid=%d AND blob.rid=forumpost.firt",
2168
fpid
2169
);
2170
moderation_forumpost_disapprove(fpid);
2171
if( zParent ){
2172
cgi_redirectf("%R/forumpost/%S",zParent);
2173
}else{
2174
cgi_redirectf("%R/forum");
2175
}
2176
return;
2177
}
2178
}
2179
style_set_current_feature("forum");
2180
isDelete = P("nullout")!=0;
2181
if( P("submit")
2182
&& isCsrfSafe
2183
&& (zContent = PDT("content",""))!=0
2184
&& (isDelete || !fossil_all_whitespace(zContent))
2185
){
2186
int done = 1;
2187
const char *zMimetype = PD("mimetype",DEFAULT_FORUM_MIMETYPE);
2188
if( bReply ){
2189
done = forum_post(0, fpid, 0, 0, zMimetype, zContent,
2190
forum_post_flags());
2191
}else if( P("edit") || isDelete ){
2192
done = forum_post(P("title"), 0, fpid, 0, zMimetype, zContent,
2193
forum_post_flags());
2194
}else{
2195
webpage_error("Missing 'reply' query parameter");
2196
}
2197
if( done ) return;
2198
}
2199
if( isDelete ){
2200
zMimetype = "text/x-fossil-wiki";
2201
zContent = "";
2202
if( pPost->zThreadTitle ) zTitle = "";
2203
style_header("Delete %s", zTitle ? "Post" : "Reply");
2204
@ <h1>Original Post:</h1>
2205
forum_render(pPost->zThreadTitle, pPost->zMimetype, pPost->zWiki,
2206
"forumEdit", 1);
2207
@ <h1>Change Into:</h1>
2208
forum_render(zTitle, zMimetype, zContent,"forumEdit", 1);
2209
@ <form action="%R/forume2" method="POST">
2210
login_insert_csrf_secret();
2211
@ <input type="hidden" name="fpid" value="%h(P("fpid"))">
2212
@ <input type="hidden" name="nullout" value="1">
2213
@ <input type="hidden" name="mimetype" value="%h(zMimetype)">
2214
@ <input type="hidden" name="content" value="%h(zContent)">
2215
if( zTitle ){
2216
@ <input aria-label="Title" type="hidden" name="title" value="%h(zTitle)">
2217
}
2218
}else if( P("edit") ){
2219
/* Provide an edit to the fpid post */
2220
zMimetype = P("mimetype");
2221
zContent = PT("content");
2222
zTitle = P("title");
2223
if( zContent==0 ) zContent = fossil_strdup(pPost->zWiki);
2224
if( zMimetype==0 ) zMimetype = fossil_strdup(pPost->zMimetype);
2225
if( zTitle==0 && pPost->zThreadTitle!=0 ){
2226
zTitle = fossil_strdup(pPost->zThreadTitle);
2227
}
2228
style_header("Edit %s", zTitle ? "Post" : "Reply");
2229
@ <h2>Original Post:</h2>
2230
forum_render(pPost->zThreadTitle, pPost->zMimetype, pPost->zWiki,
2231
"forumEdit", 1);
2232
if( bPreview ){
2233
@ <h2>Preview of Edited Post:</h2>
2234
forum_render(zTitle, zMimetype, zContent,"forumEdit", 1);
2235
}
2236
@ <h2>Revised Message:</h2>
2237
@ <form action="%R/forume2" method="POST">
2238
login_insert_csrf_secret();
2239
@ <input type="hidden" name="fpid" value="%h(P("fpid"))">
2240
@ <input type="hidden" name="edit" value="1">
2241
forum_from_line();
2242
forum_post_widget(zTitle, zMimetype, zContent);
2243
}else{
2244
/* Reply */
2245
char *zDisplayName;
2246
zMimetype = PD("mimetype",DEFAULT_FORUM_MIMETYPE);
2247
zContent = PDT("content","");
2248
style_header("Reply");
2249
@ <h2>Replying to
2250
@ <a href="%R/forumpost/%!S(zFpid)" target="_blank">%S(zFpid)</a>
2251
if( pRootPost->zThreadTitle ){
2252
@ in thread
2253
@ <span class="forumPostReplyTitle">%h(pRootPost->zThreadTitle)</span>
2254
}
2255
@ </h2>
2256
zDate = db_text(0, "SELECT datetime(%.17g,toLocal())", pPost->rDate);
2257
zDisplayName = display_name_from_login(pPost->zUser);
2258
@ <h3 class='forumPostHdr'>By %s(zDisplayName) on %h(zDate)</h3>
2259
fossil_free(zDisplayName);
2260
fossil_free(zDate);
2261
forum_render(0, pPost->zMimetype, pPost->zWiki, "forumEdit", 1);
2262
if( bPreview && !fossil_all_whitespace(zContent) ){
2263
@ <h2>Preview:</h2>
2264
forum_render(0, zMimetype,zContent, "forumEdit", 1);
2265
}
2266
@ <h2>Enter Reply:</h2>
2267
@ <form action="%R/forume2" method="POST">
2268
@ <input type="hidden" name="fpid" value="%h(P("fpid"))">
2269
@ <input type="hidden" name="reply" value="1">
2270
forum_from_line();
2271
forum_post_widget(0, zMimetype, zContent);
2272
}
2273
if( !isDelete ){
2274
@ <input type="submit" name="preview" value="Preview">
2275
}
2276
@ <input type="submit" name="cancel" value="Cancel">
2277
if( isDelete || (bPreview && !fossil_all_whitespace(zContent)) ){
2278
if( !iClosed || g.perm.Admin ) {
2279
@ <input type="submit" name="submit" value="Submit">
2280
}
2281
}
2282
forum_render_debug_options();
2283
login_insert_csrf_secret();
2284
@ </form>
2285
if( !bReply ){
2286
forum_render_attachment_list(rid_to_uuid(fpid));
2287
}
2288
if( !isDelete ){
2289
forum_render_attachment_notice();
2290
}
2291
forum_emit_js();
2292
style_finish_page();
2293
}
2294
2295
/*
2296
** SETTING: forum-close-policy boolean default=off
2297
** If true, forum moderators may close/re-open forum posts, and reply
2298
** to closed posts. If false, only administrators may do so. Note that
2299
** this only affects the forum web UI, not post-closing tags which
2300
** arrive via the command-line or from synchronization with a remote.
2301
** This policy also determines whether moderators may delete forum
2302
** attachments.
2303
**
2304
** SETTING: forum-title width=20 default=Forum
2305
** This is the name or "title" of the Forum for this repository. The
2306
** default is just "Forum". But in some setups, admins might want to
2307
** change it to "Developer Forum" or "User Forum" or whatever other name
2308
** seems more appropriate for the particular usage.
2309
**
2310
** SETTING: attachment-size-limit width=16
2311
** The maximum number of bytes for an attachment to a wiki page,
2312
** ticket, tech note, or forum post. The default (or 0) is unlimited
2313
** but a limit may be imposed by the web server or a proxy.
2314
**
2315
** SETTING: forum-statuses width=40 block-text
2316
** This JSON5-formatted value defines an array of objects describing
2317
** the available statuses of forum posts. Each entry of the array must
2318
** be an object in the form {label:"X",value:"Y"}.
2319
** The label is used in the UI and value becomes the value of the
2320
** "status" tag on forum posts. Any forum post which has a status
2321
** value which does not appear in this list is treated as if it had
2322
** the first value from this list. If this setting is empty, is
2323
** ill-formed JSON, or has only a single entry then the forum will
2324
** lack the capability of setting and filtering by status.
2325
*/
2326
2327
/*
2328
** WEBPAGE: setup_forum
2329
**
2330
** Forum configuration and metrics.
2331
*/
2332
void forum_setup(void){
2333
/* boolean config settings specific to the forum. */
2334
static const char *azForumSettings[] = {
2335
"forum-close-policy",
2336
"forum-title",
2337
};
2338
2339
login_check_credentials();
2340
if( !g.perm.Setup ){
2341
login_needed(g.anon.Setup);
2342
return;
2343
}
2344
style_set_current_feature("forum");
2345
style_header("Forum Setup");
2346
2347
@ <h2>Metrics</h2>
2348
{
2349
int nPosts = db_int(0, "SELECT COUNT(*) FROM event WHERE type='f'");
2350
@ <p><a href='%R/forum'>Forum posts</a>:
2351
@ <a href='%R/timeline?y=f'>%d(nPosts)</a></p>
2352
}
2353
2354
@ <h2>Supervisors</h2>
2355
{
2356
Stmt q = empty_Stmt;
2357
db_prepare(&q, "SELECT uid, login, cap FROM user "
2358
"WHERE cap GLOB '*[as6]*' ORDER BY login");
2359
@ <table class='bordered'>
2360
@ <thead><tr><th>User</th><th>Capabilities</th></tr></thead>
2361
@ <tbody>
2362
while( SQLITE_ROW==db_step(&q) ){
2363
const int iUid = db_column_int(&q, 0);
2364
const char *zUser = db_column_text(&q, 1);
2365
const char *zCap = db_column_text(&q, 2);
2366
@ <tr>
2367
@ <td><a href='%R/setup_uedit?id=%d(iUid)'>%h(zUser)</a></td>
2368
@ <td>(%h(zCap))</td>
2369
@ </tr>
2370
}
2371
db_finalize(&q);
2372
@</tbody></table>
2373
}
2374
2375
@ <h2>Moderators</h2>
2376
if( db_int(0, "SELECT count(*) FROM user "
2377
" WHERE cap GLOB '*5*' AND cap NOT GLOB '*[as6]*'")==0 ){
2378
@ <p>No non-supervisor moderators
2379
}else{
2380
Stmt q = empty_Stmt;
2381
db_prepare(&q, "SELECT uid, login, cap FROM user "
2382
"WHERE cap GLOB '*5*' AND cap NOT GLOB '*[as6]*'"
2383
" ORDER BY login");
2384
@ <table class='bordered'>
2385
@ <thead><tr><th>User</th><th>Capabilities</th></tr></thead>
2386
@ <tbody>
2387
while( SQLITE_ROW==db_step(&q) ){
2388
const int iUid = db_column_int(&q, 0);
2389
const char *zUser = db_column_text(&q, 1);
2390
const char *zCap = db_column_text(&q, 2);
2391
@ <tr>
2392
@ <td><a href='%R/setup_uedit?id=%d(iUid)'>%h(zUser)</a></td>
2393
@ <td>(%h(zCap))</td>
2394
@ </tr>
2395
}
2396
db_finalize(&q);
2397
@ </tbody></table>
2398
}
2399
2400
@ <h2>Settings</h2>
2401
if( P("submit") && cgi_csrf_safe(2) ){
2402
int i = 0;
2403
db_begin_transaction();
2404
for(i=0; i<ArraySize(azForumSettings); i++){
2405
char zQP[4];
2406
const char *z;
2407
const Setting *pSetting = setting_find(azForumSettings[i]);
2408
if( pSetting==0 ) continue;
2409
zQP[0] = 'a'+i;
2410
zQP[1] = zQP[0];
2411
zQP[2] = 0;
2412
z = P(zQP);
2413
if( z==0 || z[0]==0 ) continue;
2414
db_set(pSetting->name/*works-like:"x"*/, z, 0);
2415
}
2416
db_end_transaction(0);
2417
@ <p><em>Settings saved.</em></p>
2418
}
2419
{
2420
int i = 0;
2421
@ <form action="%R/setup_forum" method="post">
2422
login_insert_csrf_secret();
2423
@ <table class='forum-settings-list'><tbody>
2424
for(i=0; i<ArraySize(azForumSettings); i++){
2425
char zQP[4];
2426
const Setting *pSetting = setting_find(azForumSettings[i]);
2427
if( pSetting==0 ) continue;
2428
zQP[0] = 'a'+i;
2429
zQP[1] = zQP[0];
2430
zQP[2] = 0;
2431
if( pSetting->width==0 ){
2432
/* Boolean setting */
2433
@ <tr><td align="right">
2434
@ <a href='%R/help/%h(pSetting->name)'>%h(pSetting->name)</a>:
2435
@ </td><td>
2436
onoff_attribute("", zQP, pSetting->name/*works-like:"x"*/, 0, 0);
2437
@ </td></tr>
2438
}else{
2439
/* Text value setting */
2440
@ <tr><td align="right">
2441
@ <a href='%R/help/%h(pSetting->name)'>%h(pSetting->name)</a>:
2442
@ </td><td>
2443
entry_attribute("", 25, pSetting->name, zQP/*works-like:""*/,
2444
pSetting->def, 0);
2445
@ </td></tr>
2446
}
2447
}
2448
@ </tbody></table>
2449
@ <input type='submit' name='submit' value='Apply changes'>
2450
@ </form>
2451
}
2452
2453
style_finish_page();
2454
}
2455
2456
/*
2457
** If the forum-statuses setting is active and has 2 or more entries,
2458
** this adds a submenu for selecting the status filter, else it emits
2459
** nothing.
2460
*/
2461
static void forum_status_submenu(void){
2462
const ForumStatusList * const fss = forum_statuses();
2463
static int i = 0;
2464
static const char **az;
2465
if( i==0 && fss->n>1 ){
2466
unsigned j;
2467
az = fossil_malloc(sizeof(az[0]) * ((1 + fss->n) * 2));
2468
az[i++] = "*";
2469
az[i++] = "Any status";
2470
for( j = 0; j < fss->n; ++j ){
2471
const ForumStatus * fs = &fss->aStatus[j];
2472
/* Potential TODO: skip any entries for which there are no
2473
** forum posts with a status=${fs->zValue} tag. */
2474
az[i++] = fs->zValue;
2475
az[i++] = fs->zLabel;
2476
}
2477
//assert( i==(1+fss->n)*2 );
2478
}
2479
if( i ){
2480
cookie_link_parameter("status","forumStatus","*");
2481
style_submenu_multichoice("status", i/2, az, 0);
2482
}
2483
}
2484
2485
/*
2486
** Transient SQL Function: status_match(FROOT)
2487
**
2488
** Return true if the forum thread identified by FROOT should be included
2489
** in a list of threads. Used to implement the status=NAME query parameter
2490
** on /forum.
2491
**
2492
** The result of this routine depends on the content of the
2493
** ForumStatusMatch *pMData object that is available via sqlite3_user_data().
2494
**
2495
** * If pMData==NULL, always return true. This means that no
2496
** filtering of threads is being done. This is the common case.
2497
**
2498
** * If FROOT contains a status property value that matches
2499
** pMData->iMatch, return true.
2500
**
2501
** * if pMData->iMatch==0 (meaning we want to match the default
2502
** status value) and if the FROOT thread contains a status that
2503
** is not on the list of statuses or if FROOT has no statue
2504
** property at all, then return true. In other words, a forum
2505
** thread with no status property or an unknown status property
2506
** is treated as if it had the default status.
2507
**
2508
** * Otherwise, return false.
2509
*/
2510
static void forum_status_match(
2511
sqlite3_context *context,
2512
int argc,
2513
sqlite3_value **argv
2514
){
2515
static Stmt q;
2516
ForumStatusMatch *pMData = sqlite3_user_data(context);
2517
int i;
2518
2519
if( pMData==0 ){
2520
sqlite3_result_int(context, 1);
2521
return;
2522
}
2523
db_static_prepare(&q,
2524
"SELECT value FROM tagxref\n"
2525
" WHERE tagid=%d\n"
2526
" AND tagtype>=1\n"
2527
" AND rid=:rid\n"
2528
" ORDER BY mtime DESC LIMIT 1",
2529
pMData->eStatusTag
2530
);
2531
db_bind_int(&q, ":rid", sqlite3_value_int(argv[0]));
2532
if( db_step(&q)==SQLITE_ROW ){
2533
const char *zValue = (const char*)db_column_text(&q,0);
2534
const ForumStatusList *pFses = pMData->pFses;
2535
if( zValue==0 ){
2536
i = 0;
2537
}else{
2538
for(i=0; i<pFses->n; i++){
2539
if( fossil_strcmp(pFses->aStatus[i].zValue,zValue)==0 ) break;
2540
}
2541
}
2542
if( i>=pMData->pFses->n ) i = 0;
2543
}else{
2544
i = 0;
2545
}
2546
db_reset(&q);
2547
sqlite3_result_int(context, i==pMData->iMatch);
2548
}
2549
2550
/*
2551
** WEBPAGE: forummain
2552
** WEBPAGE: forum
2553
**
2554
** The main page for the forum feature. Show a list of recent forum
2555
** threads. Also show a search box at the top if search is enabled,
2556
** and a button for creating a new thread, if enabled.
2557
**
2558
** Query parameters:
2559
**
2560
** n=N The number of threads to show on each page
2561
** x=X Skip the first X threads
2562
** s=Y Search for term Y.
2563
*/
2564
void forum_main_page(void){
2565
Stmt q;
2566
int iLimit = 0, iOfst, iCnt;
2567
int srchFlags;
2568
const int isSearch = P("s")!=0;
2569
const char *zStatusFilter;
2570
char const *zLimit = 0; /* Value of the n= query parameter */
2571
int eStatusTag = 0; /* tagid for the "status" property */
2572
int bHasStatus = 0; /* True if forum-statuses setting exists */
2573
int bFilter = 0; /* True if status=NAME query parameter */
2574
int bHasForum = 0; /* True if forumpost table exists */
2575
const ForumStatusList *pFstat = forum_statuses();
2576
ForumStatusMatch sFSM; /* Aux data to status_match() SQL function */
2577
2578
login_check_credentials();
2579
srchFlags = search_restrict(SRCH_FORUM);
2580
if( !g.perm.RdForum ){
2581
login_needed(g.anon.RdForum);
2582
return;
2583
}
2584
cgi_check_for_malice();
2585
bHasForum = db_table_exists("repository","forumpost");
2586
if( bHasForum ){
2587
eStatusTag = db_int(0, "SELECT tagid FROM tag WHERE tagname='status'");
2588
bHasStatus = pFstat->n>1;
2589
}
2590
style_set_current_feature("forum");
2591
style_header("%s%s", db_get("forum-title","Forum"),
2592
isSearch ? " Search Results" : "");
2593
style_submenu_element("Timeline", "%R/timeline?ss=v&y=f&vfx");
2594
if( g.perm.WrForum ){
2595
style_submenu_element("New Thread","%R/forumnew");
2596
}else{
2597
style_submenu_element("New Thread","%R/login");
2598
}
2599
if( g.perm.ModForum && moderation_needed() ){
2600
style_submenu_element("Moderation Requests", "%R/modreq");
2601
}
2602
if( (srchFlags & SRCH_FORUM)!=0 ){
2603
if( search_screen(SRCH_FORUM, 0) ){
2604
style_submenu_element("Recent Threads","%R/forum");
2605
style_finish_page();
2606
return;
2607
}
2608
}
2609
cookie_read_parameter("n","forum-n");
2610
zLimit = P("n");
2611
if( zLimit!=0 ){
2612
iLimit = atoi(zLimit);
2613
if( iLimit>=0 && P("udc")!=0 ){
2614
cookie_write_parameter("n","forum-n",0);
2615
}
2616
}
2617
if( iLimit<=0 ){
2618
cgi_replace_query_parameter("n", fossil_strdup("25"))
2619
/*for the sake of Max, below*/;
2620
iLimit = 25;
2621
}
2622
style_submenu_entry("n","Max:",4,0);
2623
forum_status_submenu();
2624
zStatusFilter = P("status") /*must be after forum_status_submenu()!*/;
2625
iOfst = atoi(PD("x","0"));
2626
iCnt = 0;
2627
if( zStatusFilter ){
2628
if( zStatusFilter[0]==0 || 0==fossil_strcmp("*",zStatusFilter) ){
2629
zStatusFilter = 0;
2630
}else{
2631
bFilter = bHasStatus;
2632
}
2633
}
2634
if( bHasForum ){
2635
Stmt qStat = empty_Stmt; /* Query to get status information */
2636
if( bHasStatus ){
2637
/* The qStat query runs once for each output row generate by the
2638
** q query. It determines the value and label of the status for
2639
** the row with froot=:rowid
2640
*/
2641
db_prepare(&qStat,
2642
"SELECT tagxref.value, forumstatus.label\n"
2643
" FROM forumstatus, tagxref\n"
2644
" WHERE tagid=%d AND tagtype>=1\n"
2645
" AND forumstatus.value=tagxref.value\n"
2646
" AND rid=:rid\n"
2647
" ORDER BY mtime DESC",
2648
eStatusTag
2649
);
2650
}
2651
2652
/* Create the status_match() SQL function that will determine
2653
** whether or not each thread in the "q" query below is eligible
2654
** for display
2655
*/
2656
if( bFilter ){
2657
sFSM.pFses = pFstat;
2658
sFSM.eStatusTag = eStatusTag;
2659
for(sFSM.iMatch=0; sFSM.iMatch<pFstat->n; sFSM.iMatch++){
2660
if( 0==fossil_strcmp(zStatusFilter,
2661
pFstat->aStatus[sFSM.iMatch].zValue) ){
2662
break;
2663
}
2664
}
2665
sqlite3_create_function(g.db,"status_match",1,SQLITE_UTF8,(void*)&sFSM,
2666
forum_status_match, 0, 0);
2667
}else{
2668
sqlite3_create_function(g.db,"status_match",1,SQLITE_UTF8,0,
2669
forum_status_match, 0, 0);
2670
}
2671
2672
db_prepare(&q,
2673
"WITH thread(root,endtime,lastrid) AS (\n"
2674
" SELECT\n"
2675
" froot,\n"
2676
" max(fmtime),\n"
2677
" fpid\n"
2678
" FROM forumpost\n"
2679
" WHERE %s/*ModForum*/\n"
2680
" GROUP BY froot\n"
2681
" HAVING status_match(froot)\n"
2682
" ORDER BY 2 DESC\n"
2683
" LIMIT %d OFFSET %d\n"
2684
")\n"
2685
"SELECT\n"
2686
" julianday('now') - thread.endtime,\n" /* 0 */
2687
" thread.endtime - "
2688
"(SELECT fmtime FROM forumpost WHERE fpid=root),\n" /* 1 */
2689
" (SELECT sum(fprev IS NULL) FROM forumpost"
2690
" WHERE froot=root),\n" /* 2 */
2691
" blob.uuid,\n" /* 3 */
2692
" substr(event.comment,instr(event.comment,':')+1),\n" /* 4 */
2693
" thread.lastrid,\n" /* 5 */
2694
" thread.root\n" /* 6 */
2695
" FROM thread, blob, event\n"
2696
" WHERE blob.rid=thread.lastrid\n"
2697
" AND event.objid=thread.lastrid\n"
2698
" ORDER BY 1;",
2699
g.perm.ModForum ? "true" : "fpid NOT IN private" /*safe-for-%s*/,
2700
iLimit+1, iOfst
2701
);
2702
while( db_step(&q)==SQLITE_ROW ){
2703
char *zAge;
2704
int nMsg;
2705
const char *zUuid;
2706
const char *zTitle;
2707
const char *zStatus;
2708
const char *zStatusLbl;
2709
const int bShowStatus = bHasStatus && !zStatusFilter;
2710
const int nCols = bShowStatus ? 4 : 3;
2711
2712
if( qStat.pStmt ){
2713
/* Determine the status value for this row */
2714
db_reset(&qStat);
2715
db_bind_int(&qStat, ":rid", db_column_int(&q,6));
2716
if( db_step(&qStat)==SQLITE_ROW ){
2717
zStatus = db_column_text(&qStat, 0);
2718
zStatusLbl = db_column_text(&qStat, 1);
2719
}else{
2720
zStatus = pFstat->aStatus[0].zValue;
2721
zStatusLbl = pFstat->aStatus[0].zLabel;
2722
}
2723
}else{
2724
zStatus = zStatusLbl = NULL;
2725
}
2726
zAge = human_readable_age(db_column_double(&q,0));
2727
nMsg = db_column_int(&q, 2);
2728
zUuid = db_column_text(&q, 3);
2729
zTitle = db_column_text(&q, 4);
2730
if( iCnt==0 ){
2731
char *zTail = bFilter ? mprintf(" with status=%Q", zStatusFilter): 0;
2732
if( iOfst>0 ){
2733
@ <h1>Threads at least %s(zAge) old%h(zTail ? zTail : "")</h1>
2734
}else{
2735
@ <h1>Most recent threads%h(zTail ? zTail : "")</h1>
2736
}
2737
fossil_free(zTail);
2738
@ <div class='forumPosts fileage'><table width="100%%">
2739
if( iOfst>0 ){
2740
if( iOfst>iLimit ){
2741
@ <tr><td colspan="%d(nCols)">\
2742
@ <a href='%R/forum?x=%d(iOfst-iLimit)&n=%d(iLimit) \
2743
if( bFilter ){
2744
@ &status=%T(zStatusFilter)\
2745
}
2746
@ '>&uarr; Newer...</a></td></tr>
2747
}else{
2748
@ <tr><td colspan="%d(nCols)">\
2749
@ <a href='%R/forum?n=%d(iLimit)\
2750
if( bFilter ){
2751
@ &status=%T(zStatusFilter) \
2752
}
2753
@ '>&uarr; Newer...</a></td></tr>
2754
}
2755
}
2756
}
2757
iCnt++;
2758
if( iCnt>iLimit ){
2759
@ <tr><td colspan="%d(nCols)">\
2760
@ <a href='%R/forum?x=%d(iOfst+iLimit)&n=%d(iLimit) \
2761
if( bFilter ){
2762
@ &status=%T(zStatusFilter)\
2763
}
2764
@ '>&darr; Older...</a></td></tr>
2765
fossil_free(zAge);
2766
break;
2767
}
2768
@ <tr \
2769
if( bHasStatus ){
2770
@ data-status="%h(zStatus)"\
2771
}
2772
@ ><td>%h(zAge) ago</td>
2773
@ <td class='subject'>%z(href("%R/forumpost/%S",zUuid))%h(zTitle)</a>\
2774
@ </td><td>\
2775
if( g.perm.ModForum && moderation_pending(db_column_int(&q,5)) ){
2776
@ <span class="modpending">\
2777
@ Awaiting Moderator Approval</span><br>
2778
}
2779
if( nMsg<2 ){
2780
@ no replies\
2781
}else{
2782
char *zDuration = human_readable_age(db_column_double(&q,1));
2783
@ %d(nMsg) posts spanning %h(zDuration)\
2784
fossil_free(zDuration);
2785
}
2786
@ </td>\
2787
if( bShowStatus ){
2788
@ <td class='status'>%h(zStatusLbl)</td>\
2789
}
2790
if( qStat.pStmt ){
2791
db_reset(&qStat);
2792
}
2793
@</tr>
2794
fossil_free(zAge);
2795
}
2796
db_finalize(&q);
2797
if( qStat.pStmt ) db_finalize(&qStat);
2798
sqlite3_create_function(g.db,"status_match",1,SQLITE_UTF8,0,0,0,0);
2799
}
2800
if( iCnt>0 ){
2801
@ </table></div>
2802
}else{
2803
@ <h1>No forum posts found</h1>
2804
}
2805
if( bHasStatus ){
2806
/* We need a JS-side kludge to avoid passing on the x=N
2807
** URL arg when the status selection list is activated. */
2808
forum_emit_js();
2809
}
2810
style_finish_page();
2811
}
2812
2813
/*
2814
** The AJAX counterpart of forum_post().
2815
**
2816
** Returns the new artifact's RID on success, 0 if no changes were
2817
** necessary (e.g. an empty new post or dry-run mode), and a negative
2818
** value on error. If it returns a negative value then it will have
2819
** populated the ajax response state with an error object.
2820
**
2821
** zTitle must be NULL if iInReplyTo>0 and must be non-empty if
2822
** iInReplyTo==0.
2823
**
2824
** The caller must have started a transaction and must roll it back if
2825
** this call returns <=0, noting that only the negative-value case is
2826
** an error.
2827
**
2828
** This function does some work to try to ensure that duplicate
2829
** entries are not save (this can happen as a side effect of the forum
2830
** post editor added in 2026-06). If the given post will not have been
2831
** materially edited by these changes, they are not applied and the
2832
** rid of the existing entry is used.
2833
**
2834
** Maintenance reminders:
2835
**
2836
** - iInReplyTo==0 && iEdit==0: new thread
2837
** - iInReplyTo==0 && iEdit>0 : edit top post or response
2838
** - iInReplyTo>0 && iEdit==0: new response
2839
** - iInReplyTo>0 && iEdit>0 : edit response
2840
*/
2841
static int forum_post_ajax(
2842
const char *zTitle, /* Title. NULL for replies */
2843
int iInReplyTo, /* Post replying to. 0 for new threads */
2844
int iEdit, /* Post being edited, or zero for a new post */
2845
const char *zUser, /* Username. NULL means use login name */
2846
const char *zMimetype, /* Mimetype of content. */
2847
const char *zContent, /* Content */
2848
int iFlags /* FPOST_xyz flag values */
2849
){
2850
char *zI;
2851
char *zG;
2852
char *zP = 0;
2853
int iBasis;
2854
Blob x = BLOB_INITIALIZER,
2855
cksum = BLOB_INITIALIZER,
2856
formatCheck = BLOB_INITIALIZER,
2857
errMsg = BLOB_INITIALIZER;
2858
Manifest *pPost = 0;
2859
int nContent = zContent ? (int)strlen(zContent) : 0;
2860
int rc = 0;
2861
2862
assert( db_transaction_nesting_depth()>0 );
2863
schema_forum();
2864
if( iEdit==0 && fossil_all_whitespace(zContent) ){
2865
return 0;
2866
}
2867
if( !g.perm.Admin && (iEdit || iInReplyTo)
2868
&& forum_rid_is_tagged(iEdit ? iEdit : iInReplyTo, "closed", 1) ){
2869
return -ajax_route_error(400, "Thread is closed.");
2870
}
2871
if( 0==iInReplyTo && fossil_all_whitespace(zTitle) ){
2872
return -ajax_route_error(400, "Empty title is not permitted.");
2873
}
2874
2875
if( zUser==0 ){
2876
if( login_is_nobody() ){
2877
zUser = "anonymous";
2878
}else{
2879
zUser = login_name();
2880
}
2881
}
2882
if( iEdit>0
2883
&& !g.perm.Admin
2884
&& !forumpost_is_owner(iEdit, zUser) ){
2885
return -ajax_route_error(
2886
403, "Only admins may edit other peoples' posts."
2887
);
2888
}
2889
if( iInReplyTo==0 && iEdit>0 ){
2890
iBasis = iEdit;
2891
iInReplyTo = db_int(0, "SELECT firt FROM forumpost WHERE fpid=%d",
2892
iEdit);
2893
}else{
2894
iBasis = iInReplyTo;
2895
/* TODO (2026-06-008) If (iInReplyTo>0 && iEdit>0), validate that
2896
** iInReplyTo is connected to iEdit properly, else we risk
2897
** reparenting the new edit and having unrepredictable downstream
2898
** side effects. */
2899
}
2900
2901
if( 0!=zMimetype && 0==zMimetype[0] ){
2902
zMimetype = 0;
2903
}
2904
2905
if( 0!=zTitle && 0==zTitle[0] ) zTitle = 0;
2906
webpage_assert( (zTitle==0)+(iInReplyTo==0)==1 );
2907
2908
if( iEdit>0 ){
2909
int cmp;
2910
pPost = manifest_get(iEdit, CFTYPE_FORUM, 0);
2911
if( pPost==0 ){
2912
rc = -ajax_route_error(404, "Missing edit artifact %d", iEdit);
2913
goto post_ajax_end;
2914
}
2915
/*
2916
** If the old content matches the new then do not save a new copy.
2917
** It's easy to get re-posts of unedited content via the forum
2918
** editor, especially since the one added in 2026-06, where a
2919
** post's status and attachments may be amended from the editor
2920
** without modifying any of the post's content. In the legacy
2921
** editor such "out-of-band" changes weren't possible and users
2922
** have never made a practice of re-posting unedited content.
2923
**
2924
** We compare the following fields to the original: user, mimetype,
2925
** content, and (for root posts only) the title.
2926
*/
2927
cmp = (0==pPost->zInReplyTo)
2928
? fossil_strcmp(pPost->zThreadTitle, zTitle)
2929
: 0;
2930
if( 0==cmp ){
2931
cmp=fossil_strcmp(pPost->zWiki, zContent);
2932
if( 0==cmp ){
2933
cmp = fossil_strcmp(pPost->zUser, zUser);
2934
}
2935
if( 0==cmp
2936
&& 0!=(cmp=fossil_strcmp(pPost->zMimetype, zMimetype)) ){
2937
/* Extra mimetype checks for a common condition seen elsewhere */
2938
if( (0==zMimetype
2939
&& 0==fossil_strcmp(pPost->zMimetype, "text/x-fossil-wiki"))
2940
|| (0==pPost->zMimetype
2941
&& 0==fossil_strcmp(zMimetype, "text/x-fossil-wiki")) ){
2942
cmp = 0;
2943
}
2944
}
2945
if( 0==cmp ){
2946
rc = iEdit;
2947
goto post_ajax_end;
2948
}
2949
}
2950
zP = rid_to_uuid(iEdit);
2951
}
2952
2953
/* Write the new artifact */
2954
blob_init(&x, 0, 0);
2955
blob_appendf(&x, "D %z\n", date_in_standard_format("now"));
2956
zG = db_text(
2957
0,
2958
"SELECT uuid FROM blob, forumpost"
2959
" WHERE blob.rid==forumpost.froot"
2960
" AND forumpost.fpid=%d",
2961
iBasis
2962
);
2963
if( zG ){
2964
blob_appendf(&x, "G %z\n", zG);
2965
}
2966
if( zTitle ){
2967
blob_appendf(&x, "H %F\n", zTitle);
2968
}
2969
if( iInReplyTo>0 ){
2970
zI = rid_to_uuid(iInReplyTo);
2971
if( 0==zI ){
2972
rc = -ajax_route_error(404, "Missing in-reply-to artifact %d",
2973
iInReplyTo);
2974
goto post_ajax_end;
2975
}
2976
blob_appendf(&x, "I %z\n", zI);
2977
}
2978
if( zMimetype!=0
2979
&& fossil_strcmp(zMimetype,"text/x-fossil-wiki")!=0 ){
2980
blob_appendf(&x, "N %F\n", zMimetype);
2981
}
2982
if( zP ){
2983
blob_appendf(&x, "P %s\n", zP);
2984
}
2985
2986
blob_appendf(&x, "U %F\n", zUser);
2987
blob_appendf(&x, "W %d\n%s\n", nContent, zContent);
2988
md5sum_blob(&x, &cksum);
2989
blob_appendf(&x, "Z %b\n", &cksum);
2990
blob_reset(&cksum);
2991
2992
/* Verify that the artifact we are creating is well-formed */
2993
blob_init(&formatCheck, 0, 0);
2994
blob_init(&errMsg, 0, 0);
2995
blob_copy(&formatCheck, &x);
2996
pPost = manifest_parse(&formatCheck, 0, &errMsg);
2997
if( pPost==0 ){
2998
ajax_route_error(500, "Malformed forum post artifact: %b", &errMsg);
2999
rc = -500;
3000
goto post_ajax_end;
3001
}
3002
webpage_assert( pPost->type==CFTYPE_FORUM );
3003
3004
if( (iFlags & FPOST_DRYRUN)!=0 ){
3005
rc = 0;
3006
}else{
3007
int nrid;
3008
db_begin_transaction();
3009
nrid = wiki_put(&x, iEdit>0 ? iEdit : 0, forum_need_moderation());
3010
blob_reset(&x);
3011
if( (iFlags & FPOST_NO_ALERT)!=0 ){
3012
alert_unqueue('f', nrid);
3013
}
3014
rc = nrid;
3015
db_end_transaction(0);
3016
}
3017
post_ajax_end:
3018
manifest_destroy(pPost);
3019
fossil_free(zP);
3020
blob_reset(&x);
3021
blob_reset(&cksum);
3022
blob_reset(&formatCheck);
3023
return rc;
3024
}
3025
/*
3026
** WEBPAGE: forumajax_save hidden
3027
**
3028
** WIP
3029
**
3030
** Response JSON:
3031
**
3032
** { uuid: hash, ...tbd }
3033
*/
3034
void forum_ajax_save_page(void){
3035
const char *zFpid;
3036
const char *zTitle;
3037
const char *zIrt;
3038
const char *zMimetype;
3039
const char *zContent;
3040
const char *zStatus;
3041
const int bHasAttachment = P("file1")!=0;
3042
Manifest *pPost = 0;
3043
char *zNewUuid = 0;
3044
int firt = 0; /* In-reply-to rid or 0 */
3045
int fpid = 0; /* Post rid being edited or 0 */
3046
int rc = 0; /* Result code. */
3047
int nrid = 0; /* New artifact rid. */
3048
int iPostFlags; /* forum_post_flags() (after perms check) */
3049
int bRollback; /* True = roll back. */
3050
int nAttach = 0; /* Number of attachments added */
3051
int bStatusSet = 0; /* True if status tag set. */
3052
3053
if( !ajax_route_bootstrap(0, 1) ){
3054
return;
3055
}else if( !g.perm.WrForum
3056
|| (bHasAttachment && !g.perm.AttachForum) ){
3057
ajax_route_error_forbidden();
3058
return;
3059
}else if( !ajax_check_csrf(2) ){
3060
ajax_route_error_csrf();
3061
return;
3062
}
3063
3064
iPostFlags = forum_post_flags(/*must come after permissions init*/);
3065
bRollback = (FPOST_DRYRUN & iPostFlags);
3066
zFpid = P("fpid");
3067
zIrt = P("firt");
3068
zMimetype = P("mimetype");
3069
zContent = P("content");
3070
zStatus = P("status");
3071
db_begin_transaction();
3072
if( zFpid && zFpid[0] ){
3073
fpid = symbolic_name_to_rid(zFpid, "f");
3074
if( fpid<0 ){
3075
rc = -ajax_route_error(400, "Ambiguous forum ID.");
3076
goto ajax_save_end;
3077
}else if( 0==fpid
3078
|| 0==(pPost = manifest_get(fpid, CFTYPE_FORUM, 0)) ){
3079
rc = -ajax_route_error(404, "Cannot resolve forum post ID.");
3080
goto ajax_save_end;
3081
}
3082
}
3083
/*
3084
** Problem: if we derive firt from fpid/pPost then there's a race
3085
** condition where the IRT post is edited between the time that this
3086
** edit was initiated and when it is posted: the new edit's IRT will
3087
** point to the edit which was made in the meantime, not the one the
3088
** user intended to respond to. However, if we accept firt from the
3089
** enviornment, we "really should" validate that it's actually in
3090
** the current chain, to prohibit that malicious posts could move
3091
** posts around.
3092
**
3093
** forum_post_ajax() will, if fpid>0 && !firt, select fpid's current
3094
** firt.
3095
*/
3096
if( zIrt && zIrt[0] ){
3097
firt = symbolic_name_to_rid(zIrt, "f");
3098
if( firt<0 ){
3099
rc = -ajax_route_error(400, "Ambiguous in-reply-do ID.");
3100
goto ajax_save_end;
3101
}else if( 0==firt ){
3102
rc = -ajax_route_error(404, "Cannot resolve in-reply-do ID.");
3103
goto ajax_save_end;
3104
}
3105
}
3106
3107
if( 0 ){
3108
rc = -ajax_route_error(400, "Save is TODO. "
3109
"iPostFlags=%d debug=%d",
3110
iPostFlags, g.perm.Debug);
3111
goto ajax_save_end;
3112
}
3113
3114
zTitle = firt ? 0 : P("title");
3115
nrid = forum_post_ajax(zTitle, firt, fpid, 0, zMimetype,
3116
zContent, iPostFlags);
3117
if( nrid<0 ){
3118
rc = nrid;
3119
goto ajax_save_end;
3120
}else if( nrid==0 ){
3121
if( 0==(FPOST_DRYRUN & iPostFlags) ){
3122
bRollback = 1;
3123
CX("{\"message\": \"No saving needed.\"}\n");
3124
}else{
3125
CX("{\"message\": \"Rolled back for dry-run.\","
3126
"\"iPostFlags\":%d}\n", iPostFlags);
3127
}
3128
goto ajax_save_end;
3129
}else{
3130
const int bNeedsModeration = forum_need_moderation();
3131
const int fpHead = forumpost_head_rid(nrid);
3132
assert( nrid>0 );
3133
assert( fpHead>0 );
3134
zNewUuid = rid_to_uuid(nrid);
3135
if( 0!=P("file1") ){
3136
/* Attachments */
3137
if( !g.perm.Admin && !g.perm.AttachForum ){
3138
rc = -ajax_route_error(403, "No permission no attach files.");
3139
goto ajax_save_end;
3140
}else{
3141
char *zRoot = (nrid==fpHead) ? 0 : rid_to_uuid(fpHead);
3142
nAttach = attachments_ajax_from_POST(zRoot ? zRoot : zNewUuid,
3143
bNeedsModeration);
3144
fossil_free(zRoot);
3145
if( nAttach<0 ){
3146
rc = nAttach;
3147
goto ajax_save_end;
3148
}
3149
if( nAttach>0
3150
&& (iPostFlags & FPOST_NO_ALERT)!=0
3151
&& db_table_exists("repository","pending_alert") ){
3152
/* Unqueue any alerts for these attachments. Recall that
3153
** they're attached to the first version of the post, which
3154
** means we actually risk cancelling _other_ pending
3155
** notifications for attachments on this same post. C'est la
3156
** vie.*/
3157
db_multi_exec(
3158
"WITH x(id) AS (\n"
3159
" SELECT 'f%d'\n"
3160
" UNION ALL\n"
3161
" SELECT 'f'||a.attachid FROM blob b, attachment a\n"
3162
" WHERE b.rid=%d\n"
3163
" AND b.uuid=a.target\n"
3164
") DELETE FROM pending_alert WHERE eventid IN x",
3165
fpHead, fpHead
3166
);
3167
}
3168
}
3169
}
3170
if( 0==bNeedsModeration
3171
/* ^^^ Do not allow a status tag on a pending-moderation post
3172
** because it will introduce a reference to an artifact which
3173
** will become a phantom if it is rejected by a moderator. */
3174
&& zStatus!=0 && zStatus[0]!=0
3175
&& forum_may_set_status(nrid)
3176
&& (bStatusSet=forumpost_tag(nrid, 1, "status", zStatus))<0 ){
3177
rc = -ajax_route_error(500, "Tagging failed: %s", g.zErrMsg);
3178
goto ajax_save_end;
3179
}
3180
}
3181
3182
assert( 0==rc );
3183
assert( zNewUuid );
3184
CX("{\"uuid\": %!j, \"attachedCount\": %d, "
3185
"\"statusModified\": %d, "
3186
"\"dryrun\": %s, \"iPostFlags\":%d}\n",
3187
zNewUuid, nAttach,
3188
bStatusSet, bRollback ? "true" : "false", iPostFlags);
3189
3190
ajax_save_end:
3191
manifest_destroy(pPost);
3192
fossil_free(zNewUuid);
3193
db_end_transaction(rc || bRollback);
3194
}
3195

Keyboard Shortcuts

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