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