Fossil SCM

Improvements to the forum thread display. Additional details on the [https://fossil-scm.org/forum/forumpost/3d3ffe23ed?t=h|forum thread].

drh 2020-08-22 15:34 trunk merge
Commit 5182a1bfbf43e94d230544b738900497dfbf394cb64cdb65e0e951155e693e1e
3 files changed +448 -529 +29 -18 +11 -1
+448 -529
--- src/forum.c
+++ src/forum.c
@@ -26,44 +26,45 @@
2626
*/
2727
#define DEFAULT_FORUM_MIMETYPE "text/x-markdown"
2828
2929
#if INTERFACE
3030
/*
31
-** Each instance of the following object represents a single message -
31
+** Each instance of the following object represents a single message -
3232
** either the initial post, an edit to a post, a reply, or an edit to
3333
** a reply.
3434
*/
35
-struct ForumEntry {
36
- int fpid; /* rid for this entry */
37
- int fprev; /* zero if initial entry. non-zero if an edit */
38
- int firt; /* This entry replies to firt */
39
- int mfirt; /* Root in-reply-to */
40
- int nReply; /* Number of replies to this entry */
35
+struct ForumPost {
36
+ int fpid; /* rid for this post */
4137
int sid; /* Serial ID number */
38
+ int rev; /* Revision number */
4239
char *zUuid; /* Artifact hash */
43
- ForumEntry *pLeaf; /* Most recent edit for this entry */
44
- ForumEntry *pEdit; /* This entry is an edit of pEdit */
45
- ForumEntry *pNext; /* Next in chronological order */
46
- ForumEntry *pPrev; /* Previous in chronological order */
47
- ForumEntry *pDisplay; /* Next in display order */
48
- int nIndent; /* Number of levels of indentation for this entry */
40
+ ForumPost *pIrt; /* This post replies to pIrt */
41
+ ForumPost *pEditHead; /* Original, unedited post */
42
+ ForumPost *pEditTail; /* Most recent edit for this post */
43
+ ForumPost *pEditNext; /* This post is edited by pEditNext */
44
+ ForumPost *pEditPrev; /* This post is an edit of pEditPrev */
45
+ ForumPost *pNext; /* Next in chronological order */
46
+ ForumPost *pPrev; /* Previous in chronological order */
47
+ ForumPost *pDisplay; /* Next in display order */
48
+ int nEdit; /* Number of edits to this post */
49
+ int nIndent; /* Number of levels of indentation for this post */
4950
};
5051
5152
/*
5253
** A single instance of the following tracks all entries for a thread.
5354
*/
5455
struct ForumThread {
55
- ForumEntry *pFirst; /* First entry in chronological order */
56
- ForumEntry *pLast; /* Last entry in chronological order */
57
- ForumEntry *pDisplay; /* Entries in display order */
58
- ForumEntry *pTail; /* Last on the display list */
56
+ ForumPost *pFirst; /* First post in chronological order */
57
+ ForumPost *pLast; /* Last post in chronological order */
58
+ ForumPost *pDisplay; /* Entries in display order */
59
+ ForumPost *pTail; /* Last on the display list */
5960
int mxIndent; /* Maximum indentation level */
6061
};
6162
#endif /* INTERFACE */
6263
6364
/*
64
-** Return true if the forum entry with the given rid has been
65
+** Return true if the forum post with the given rid has been
6566
** subsequently edited.
6667
*/
6768
int forum_rid_has_been_edited(int rid){
6869
static Stmt q;
6970
int res;
@@ -79,41 +80,39 @@
7980
8081
/*
8182
** Delete a complete ForumThread and all its entries.
8283
*/
8384
static void forumthread_delete(ForumThread *pThread){
84
- ForumEntry *pEntry, *pNext;
85
- for(pEntry=pThread->pFirst; pEntry; pEntry = pNext){
86
- pNext = pEntry->pNext;
87
- fossil_free(pEntry->zUuid);
88
- fossil_free(pEntry);
85
+ ForumPost *pPost, *pNext;
86
+ for(pPost=pThread->pFirst; pPost; pPost = pNext){
87
+ pNext = pPost->pNext;
88
+ fossil_free(pPost->zUuid);
89
+ fossil_free(pPost);
8990
}
9091
fossil_free(pThread);
9192
}
9293
93
-#if 0 /* not used */
9494
/*
95
-** Search a ForumEntry list forwards looking for the entry with fpid
95
+** Search a ForumPost list forwards looking for the post with fpid
9696
*/
97
-static ForumEntry *forumentry_forward(ForumEntry *p, int fpid){
97
+static ForumPost *forumpost_forward(ForumPost *p, int fpid){
9898
while( p && p->fpid!=fpid ) p = p->pNext;
9999
return p;
100100
}
101
-#endif
102101
103102
/*
104
-** Search backwards for a ForumEntry
103
+** Search backwards for a ForumPost
105104
*/
106
-static ForumEntry *forumentry_backward(ForumEntry *p, int fpid){
105
+static ForumPost *forumpost_backward(ForumPost *p, int fpid){
107106
while( p && p->fpid!=fpid ) p = p->pPrev;
108107
return p;
109108
}
110109
111110
/*
112
-** Add an entry to the display list
111
+** Add a post to the display list
113112
*/
114
-static void forumentry_add_to_display(ForumThread *pThread, ForumEntry *p){
113
+static void forumpost_add_to_display(ForumThread *pThread, ForumPost *p){
115114
if( pThread->pDisplay==0 ){
116115
pThread->pDisplay = p;
117116
}else{
118117
pThread->pTail->pDisplay = p;
119118
}
@@ -120,108 +119,112 @@
120119
pThread->pTail = p;
121120
}
122121
123122
/*
124123
** Extend the display list for pThread by adding all entries that
125
-** reference fpid. The first such entry will be no earlier then
126
-** entry "p".
124
+** reference fpid. The first such post will be no earlier then
125
+** post "p".
127126
*/
128127
static void forumthread_display_order(
129128
ForumThread *pThread, /* The complete thread */
130
- ForumEntry *pBase /* Add replies to this entry */
129
+ ForumPost *pBase /* Add replies to this post */
131130
){
132
- ForumEntry *p;
133
- ForumEntry *pPrev = 0;
131
+ ForumPost *p;
132
+ ForumPost *pPrev = 0;
133
+ ForumPost *pBaseIrt;
134134
for(p=pBase->pNext; p; p=p->pNext){
135
- if( p->fprev==0 && p->mfirt==pBase->fpid ){
136
- if( pPrev ){
137
- pPrev->nIndent = pBase->nIndent + 1;
138
- forumentry_add_to_display(pThread, pPrev);
139
- forumthread_display_order(pThread, pPrev);
140
- }
141
- pBase->nReply++;
142
- pPrev = p;
135
+ if( !p->pEditPrev && p->pIrt ){
136
+ pBaseIrt = p->pIrt->pEditHead ? p->pIrt->pEditHead : p->pIrt;
137
+ if( pBaseIrt==pBase ){
138
+ if( pPrev ){
139
+ pPrev->nIndent = pBase->nIndent + 1;
140
+ forumpost_add_to_display(pThread, pPrev);
141
+ forumthread_display_order(pThread, pPrev);
142
+ }
143
+ pPrev = p;
144
+ }
143145
}
144146
}
145147
if( pPrev ){
146148
pPrev->nIndent = pBase->nIndent + 1;
147149
if( pPrev->nIndent>pThread->mxIndent ) pThread->mxIndent = pPrev->nIndent;
148
- forumentry_add_to_display(pThread, pPrev);
150
+ forumpost_add_to_display(pThread, pPrev);
149151
forumthread_display_order(pThread, pPrev);
150152
}
151153
}
152154
153155
/*
154156
** Construct a ForumThread object given the root record id.
155157
*/
156158
static ForumThread *forumthread_create(int froot, int computeHierarchy){
157159
ForumThread *pThread;
158
- ForumEntry *pEntry;
160
+ ForumPost *pPost;
161
+ ForumPost *p;
159162
Stmt q;
160163
int sid = 1;
161
- Bag seen = Bag_INIT;
164
+ int firt, fprev;
162165
pThread = fossil_malloc( sizeof(*pThread) );
163166
memset(pThread, 0, sizeof(*pThread));
164167
db_prepare(&q,
165168
"SELECT fpid, firt, fprev, (SELECT uuid FROM blob WHERE rid=fpid)"
166169
" FROM forumpost"
167170
" WHERE froot=%d ORDER BY fmtime",
168171
froot
169172
);
170173
while( db_step(&q)==SQLITE_ROW ){
171
- pEntry = fossil_malloc( sizeof(*pEntry) );
172
- memset(pEntry, 0, sizeof(*pEntry));
173
- pEntry->fpid = db_column_int(&q, 0);
174
- pEntry->firt = db_column_int(&q, 1);
175
- pEntry->fprev = db_column_int(&q, 2);
176
- pEntry->zUuid = fossil_strdup(db_column_text(&q,3));
177
- pEntry->mfirt = pEntry->firt;
178
- pEntry->sid = sid++;
179
- pEntry->pPrev = pThread->pLast;
180
- pEntry->pNext = 0;
181
- bag_insert(&seen, pEntry->fpid);
174
+ pPost = fossil_malloc( sizeof(*pPost) );
175
+ memset(pPost, 0, sizeof(*pPost));
176
+ pPost->fpid = db_column_int(&q, 0);
177
+ firt = db_column_int(&q, 1);
178
+ fprev = db_column_int(&q, 2);
179
+ pPost->zUuid = fossil_strdup(db_column_text(&q,3));
180
+ if( !fprev ) pPost->sid = sid++;
181
+ pPost->pPrev = pThread->pLast;
182
+ pPost->pNext = 0;
182183
if( pThread->pLast==0 ){
183
- pThread->pFirst = pEntry;
184
- }else{
185
- pThread->pLast->pNext = pEntry;
186
- }
187
- if( pEntry->firt && !bag_find(&seen,pEntry->firt) ){
188
- pEntry->firt = froot;
189
- pEntry->mfirt = froot;
190
- }
191
- pThread->pLast = pEntry;
192
- }
193
- db_finalize(&q);
194
- bag_clear(&seen);
195
-
196
- /* Establish which entries are the latest edit. After this loop
197
- ** completes, entries that have non-NULL pLeaf should not be
198
- ** displayed.
199
- */
200
- for(pEntry=pThread->pFirst; pEntry; pEntry=pEntry->pNext){
201
- if( pEntry->fprev ){
202
- ForumEntry *pBase = 0, *p;
203
- p = forumentry_backward(pEntry->pPrev, pEntry->fprev);
204
- pEntry->pEdit = p;
205
- while( p ){
206
- pBase = p;
207
- p->pLeaf = pEntry;
208
- p = pBase->pEdit;
209
- }
210
- for(p=pEntry->pNext; p; p=p->pNext){
211
- if( p->mfirt==pEntry->fpid ) p->mfirt = pBase->fpid;
212
- }
213
- }
214
- }
184
+ pThread->pFirst = pPost;
185
+ }else{
186
+ pThread->pLast->pNext = pPost;
187
+ }
188
+ pThread->pLast = pPost;
189
+
190
+ /* Find the in-reply-to post. Default to the topic post if the replied-to
191
+ ** post cannot be found. */
192
+ if( firt ){
193
+ pPost->pIrt = pThread->pFirst;
194
+ for(p=pThread->pFirst; p; p=p->pNext){
195
+ if( p->fpid==firt ){
196
+ pPost->pIrt = p;
197
+ break;
198
+ }
199
+ }
200
+ }
201
+
202
+ /* Maintain the linked list of post edits. */
203
+ if( fprev ){
204
+ p = forumpost_backward(pPost->pPrev, fprev);
205
+ p->pEditNext = pPost;
206
+ pPost->sid = p->sid;
207
+ pPost->rev = p->rev+1;
208
+ pPost->nEdit = p->nEdit+1;
209
+ pPost->pEditPrev = p;
210
+ pPost->pEditHead = p->pEditHead ? p->pEditHead : p;
211
+ for(; p; p=p->pEditPrev ){
212
+ p->nEdit = pPost->nEdit;
213
+ p->pEditTail = pPost;
214
+ }
215
+ }
216
+ }
217
+ db_finalize(&q);
215218
216219
if( computeHierarchy ){
217220
/* Compute the hierarchical display order */
218
- pEntry = pThread->pFirst;
219
- pEntry->nIndent = 1;
221
+ pPost = pThread->pFirst;
222
+ pPost->nIndent = 1;
220223
pThread->mxIndent = 1;
221
- forumentry_add_to_display(pThread, pEntry);
222
- forumthread_display_order(pThread, pEntry);
224
+ forumpost_add_to_display(pThread, pPost);
225
+ forumthread_display_order(pThread, pPost);
223226
}
224227
225228
/* Return the result */
226229
return pThread;
227230
}
@@ -265,11 +268,11 @@
265268
void forumthread_cmd(void){
266269
int fpid;
267270
int froot;
268271
const char *zName;
269272
ForumThread *pThread;
270
- ForumEntry *p;
273
+ ForumPost *p;
271274
272275
db_find_and_open_repository(0,0);
273276
verify_all_options();
274277
if( g.argc==2 ){
275278
forum_thread_list();
@@ -293,21 +296,22 @@
293296
pThread = forumthread_create(froot, 1);
294297
fossil_print("Chronological:\n");
295298
fossil_print(
296299
/* 0 1 2 3 4 5 6 7 */
297300
/* 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123 */
298
- " sid fpid firt fprev mfirt pLeaf nReply hash\n");
301
+ " sid rev fpid pIrt pEditPrev pEditTail hash\n");
299302
for(p=pThread->pFirst; p; p=p->pNext){
300
- fossil_print("%4d %9d %9d %9d %9d %9d %6d %8.8s\n", p->sid,
301
- p->fpid, p->firt, p->fprev, p->mfirt, p->pLeaf ? p->pLeaf->fpid : 0,
302
- p->nReply, p->zUuid);
303
+ fossil_print("%4d %4d %9d %9d %9d %9d %8.8s\n", p->sid, p->rev,
304
+ p->fpid, p->pIrt ? p->pIrt->fpid : 0,
305
+ p->pEditPrev ? p->pEditPrev->fpid : 0,
306
+ p->pEditTail ? p->pEditTail->fpid : 0, p->zUuid);
303307
}
304308
fossil_print("\nDisplay\n");
305309
for(p=pThread->pDisplay; p; p=p->pDisplay){
306310
fossil_print("%*s", (p->nIndent-1)*3, "");
307
- if( p->pLeaf ){
308
- fossil_print("%d->%d\n", p->fpid, p->pLeaf->fpid);
311
+ if( p->pEditTail ){
312
+ fossil_print("%d->%d\n", p->fpid, p->pEditTail->fpid);
309313
}else{
310314
fossil_print("%d\n", p->fpid);
311315
}
312316
}
313317
forumthread_delete(pThread);
@@ -352,28 +356,10 @@
352356
if( zClass ){
353357
@ </div>
354358
}
355359
}
356360
357
-/*
358
-** Generate the buttons in the display that allow a forum supervisor to
359
-** mark a user as trusted. Only do this if:
360
-**
361
-** (1) The poster is an individual, not a special user like "anonymous"
362
-** (2) The current user has Forum Supervisor privilege
363
-*/
364
-static void generateTrustControls(Manifest *pPost){
365
- if( !g.perm.AdminForum ) return;
366
- if( login_is_special(pPost->zUser) ) return;
367
- @ <br>
368
- @ <label><input type="checkbox" name="trust">
369
- @ Trust user "%h(pPost->zUser)"
370
- @ so that future posts by "%h(pPost->zUser)" do not require moderation.
371
- @ </label>
372
- @ <input type="hidden" name="trustuser" value="%h(pPost->zUser)">
373
-}
374
-
375361
/*
376362
** Compute a display name from a login name.
377363
**
378364
** If the input login is found in the USER table, then check the USER.INFO
379365
** field to see if it has display-name followed by an email address.
@@ -403,413 +389,326 @@
403389
db_reset(&q);
404390
return zResult;
405391
}
406392
407393
/*
408
-** Display all posts in a forum thread in chronological order
394
+** Display a single post in a forum thread.
409395
*/
410
-static void forum_display_chronological(int froot, int target, int bRawMode){
411
- ForumThread *pThread = forumthread_create(froot, 0);
412
- ForumEntry *p;
413
- int notAnon = login_is_individual();
414
- char cMode = bRawMode ? 'r' : 'c';
415
- for(p=pThread->pFirst; p; p=p->pNext){
416
- char *zDate;
417
- Manifest *pPost;
418
- int isPrivate; /* True for posts awaiting moderation */
419
- int sameUser; /* True if author is also the reader */
420
- const char *zUuid;
421
- char *zDisplayName; /* The display name */
422
- int sid;
423
-
424
- pPost = manifest_get(p->fpid, CFTYPE_FORUM, 0);
425
- if( pPost==0 ) continue;
426
- if( p->fpid==target ){
427
- @ <div id="forum%d(p->fpid)" class="forumTime forumSel">
428
- }else if( p->pLeaf!=0 ){
429
- @ <div id="forum%d(p->fpid)" class="forumTime forumObs">
430
- }else{
431
- @ <div id="forum%d(p->fpid)" class="forumTime">
432
- }
433
- if( pPost->zThreadTitle ){
434
- @ <h1>%h(pPost->zThreadTitle)</h1>
435
- }
436
- zDate = db_text(0, "SELECT datetime(%.17g)", pPost->rDate);
437
- zDisplayName = display_name_from_login(pPost->zUser);
438
- sid = p->pEdit ? p->pEdit->sid : p->sid;
439
- @ <h3 class='forumPostHdr'>(%d(sid)) By %h(zDisplayName) on %h(zDate)
396
+static void forum_display_post(
397
+ ForumPost *p, /* Forum post to display */
398
+ int iIndentScale, /* Indent scale factor */
399
+ int bRaw, /* True to omit the border */
400
+ int bUnf, /* True to leave the post unformatted */
401
+ int bHist, /* True if showing edit history */
402
+ int bSelect, /* True if this is the selected post */
403
+ char *zQuery /* Common query string */
404
+){
405
+ char *zDisplayName; /* The display name */
406
+ char *zDate; /* The time/date string */
407
+ char *zHist; /* History query string */
408
+ Manifest *pManifest; /* Manifest comprising the current post */
409
+ int bPrivate; /* True for posts awaiting moderation */
410
+ int bSameUser; /* True if author is also the reader */
411
+ int iIndent; /* Indent level */
412
+ const char *zMimetype;/* Formatting MIME type */
413
+
414
+ /* Get the manifest for the post. Abort if not found (e.g. shunned). */
415
+ pManifest = manifest_get(p->fpid, CFTYPE_FORUM, 0);
416
+ if( !pManifest ) return;
417
+
418
+ /* When not in raw mode, create the border around the post. */
419
+ if( !bRaw ){
420
+ /* Open the <div> enclosing the post. Set the class string to mark the post
421
+ ** as selected and/or obsolete. */
422
+ iIndent = (p->pEditHead ? p->pEditHead->nIndent : p->nIndent)-1;
423
+ @ <div id='forum%d(p->fpid)' class='forumTime\
424
+ @ %s(bSelect ? " forumSel" : "")\
425
+ @ %s(p->pEditTail ? " forumObs" : "")'\
426
+ if( iIndent && iIndentScale ){
427
+ @ style='margin-left: %d(iIndent*iIndentScale)ex'
428
+ }
429
+ @ >
430
+
431
+ /* If this is the first post (or an edit thereof), emit the thread title. */
432
+ if( pManifest->zThreadTitle ){
433
+ @ <h1>%h(pManifest->zThreadTitle)</h1>
434
+ }
435
+
436
+ /* Emit the serial number, revision number, author, and date. */
437
+ zDisplayName = display_name_from_login(pManifest->zUser);
438
+ zDate = db_text(0, "SELECT datetime(%.17g)", pManifest->rDate);
439
+ @ <h3 class='forumPostHdr'>(%d(p->sid)\
440
+ if( p->nEdit ){
441
+ @ .%.*d(fossil_num_digits(p->nEdit))(p->rev)\
442
+ }
443
+ @ ) By %h(zDisplayName) on %h(zDate)
440444
fossil_free(zDisplayName);
441445
fossil_free(zDate);
442
- if( p->pEdit ){
443
- @ edit of %z(href("%R/forumpost/%S?t=%c",p->pEdit->zUuid,cMode))\
444
- @ %d(p->pEdit->sid)</a>
446
+
447
+ /* If this is an edit, refer back to the old version. Be sure "hist" is in
448
+ ** the query string so the old version will actually be shown. */
449
+ if( p->pEditPrev ){
450
+ zHist = bHist ? "" : "&hist";
451
+ @ edit of \
452
+ @ %z(href("%R/forumpost/%S?%s%s",p->pEditPrev->zUuid,zQuery,zHist))\
453
+ @ %d(p->sid).%.*d(fossil_num_digits(p->nEdit))(p->pEditPrev->rev)</a>
445454
}
455
+
456
+ /* If debugging is enabled, link to the artifact page. */
446457
if( g.perm.Debug ){
447458
@ <span class="debug">\
448459
@ <a href="%R/artifact/%h(p->zUuid)">(artifact-%d(p->fpid))</a></span>
449460
}
450
- if( p->firt ){
451
- ForumEntry *pIrt = p->pPrev;
452
- while( pIrt && pIrt->fpid!=p->firt ) pIrt = pIrt->pPrev;
453
- if( pIrt ){
454
- @ in reply to %z(href("%R/forumpost/%S?t=%c",pIrt->zUuid,cMode))\
455
- @ %d(pIrt->sid)</a>
456
- }
457
- }
458
- zUuid = p->zUuid;
459
- if( p->pLeaf ){
460
- @ updated by %z(href("%R/forumpost/%S?t=%c",p->pLeaf->zUuid,cMode))\
461
- @ %d(p->pLeaf->sid)</a>
462
- zUuid = p->pLeaf->zUuid;
463
- }
464
- if( p->fpid!=target ){
465
- @ %z(href("%R/forumpost/%S?t=%c",zUuid,cMode))[link]</a>
466
- }
467
- if( !bRawMode ){
468
- @ %z(href("%R/forumpost/%S?raw",zUuid))[source]</a>
469
- }
470
- isPrivate = content_is_private(p->fpid);
471
- sameUser = notAnon && fossil_strcmp(pPost->zUser, g.zLogin)==0;
461
+
462
+ /* If this is a reply, refer back to the parent post. */
463
+ if( p->pIrt ){
464
+ @ in reply to %z(href("%R/forumpost/%S?%s",p->pIrt->zUuid,zQuery))\
465
+ @ %d(p->pIrt->sid)\
466
+ if( p->pIrt->nEdit ){
467
+ @ .%.*d(fossil_num_digits(p->pIrt->nEdit))(p->pIrt->rev)\
468
+ }
469
+ @ </a>
470
+ }
471
+
472
+ /* If this post was later edited, refer forward to the next edit. */
473
+ if( p->pEditNext ){
474
+ @ updated by %z(href("%R/forumpost/%S?%s",p->pEditNext->zUuid,zQuery))\
475
+ @ %d(p->pEditNext->sid)\
476
+ @ .%.*d(fossil_num_digits(p->nEdit))(p->pEditNext->rev)</a>
477
+ }
478
+
479
+ /* Provide a link to select the individual post. */
480
+ if( !bSelect ){
481
+ @ %z(href("%R/forumpost/%S?%s",p->zUuid,zQuery))[link]</a>
482
+ }
483
+
484
+ /* Provide a link to the raw source code. */
485
+ if( !bUnf ){
486
+ @ %z(href("%R/forumpost/%S?raw",p->zUuid))[source]</a>
487
+ }
472488
@ </h3>
473
- if( isPrivate && !g.perm.ModForum && !sameUser ){
474
- @ <p><span class="modpending">Awaiting Moderator Approval</span></p>
475
- }else{
476
- const char *zMimetype;
477
- if( bRawMode ){
478
- zMimetype = "text/plain";
479
- }else if( p->pLeaf!=0 ){
480
- zMimetype = "text/plain";
481
- }else{
482
- zMimetype = pPost->zMimetype;
483
- }
484
- forum_render(0, zMimetype, pPost->zWiki, 0, 1);
485
- }
486
- if( g.perm.WrForum && p->pLeaf==0 ){
487
- int sameUser = login_is_individual()
488
- && fossil_strcmp(pPost->zUser, g.zLogin)==0;
489
+ }
490
+
491
+ /* Check if this post is approved, also if it's by the current user. */
492
+ bPrivate = content_is_private(p->fpid);
493
+ bSameUser = login_is_individual()
494
+ && fossil_strcmp(pManifest->zUser, g.zLogin)==0;
495
+
496
+ /* Render the post if the user is able to see it. */
497
+ if( bPrivate && !g.perm.ModForum && !bSameUser ){
498
+ @ <p><span class="modpending">Awaiting Moderator Approval</span></p>
499
+ }else{
500
+ if( bRaw || bUnf || p->pEditTail ){
501
+ zMimetype = "text/plain";
502
+ }else{
503
+ zMimetype = pManifest->zMimetype;
504
+ }
505
+ forum_render(0, zMimetype, pManifest->zWiki, 0, !bRaw);
506
+ }
507
+
508
+ /* When not in raw mode, finish creating the border around the post. */
509
+ if( !bRaw ){
510
+ /* If the user is able to write to the forum and if this post has not been
511
+ ** edited, create a form with various interaction buttons. */
512
+ if( g.perm.WrForum && !p->pEditTail ){
489513
@ <div><form action="%R/forumedit" method="POST">
490514
@ <input type="hidden" name="fpid" value="%s(p->zUuid)">
491
- if( !isPrivate ){
492
- /* Reply and Edit are only available if the post has already
493
- ** been approved */
515
+ if( !bPrivate ){
516
+ /* Reply and Edit are only available if the post has been approved. */
494517
@ <input type="submit" name="reply" value="Reply">
495
- if( g.perm.Admin || sameUser ){
518
+ if( g.perm.Admin || bSameUser ){
496519
@ <input type="submit" name="edit" value="Edit">
497520
@ <input type="submit" name="nullout" value="Delete">
498521
}
499522
}else if( g.perm.ModForum ){
500
- /* Provide moderators with moderation buttons for posts that
501
- ** are pending moderation */
523
+ /* Allow moderators to approve or reject pending posts. Also allow
524
+ ** forum supervisors to mark non-special users as trusted and therefore
525
+ ** able to post unmoderated. */
502526
@ <input type="submit" name="approve" value="Approve">
503527
@ <input type="submit" name="reject" value="Reject">
504
- generateTrustControls(pPost);
505
- }else if( sameUser ){
506
- /* A post that is pending moderation can be deleted by the
507
- ** person who originally submitted the post */
528
+ if( g.perm.AdminForum && !login_is_special(pManifest->zUser) ){
529
+ @ <br><label><input type="checkbox" name="trust">
530
+ @ Trust user "%h(pManifest->zUser)" so that future posts by \
531
+ @ "%h(pManifest->zUser)" do not require moderation.
532
+ @ </label>
533
+ @ <input type="hidden" name="trustuser" value="%h(pManifest->zUser)">
534
+ }
535
+ }else if( bSameUser ){
536
+ /* Allow users to delete (reject) their own pending posts. */
508537
@ <input type="submit" name="reject" value="Delete">
509538
}
510539
@ </form></div>
511540
}
512
- manifest_destroy(pPost);
513541
@ </div>
514542
}
515543
516
- /* Undocumented "threadtable" query parameter causes thread table
517
- ** to be displayed for debugging purposes.
518
- */
544
+ /* Clean up. */
545
+ manifest_destroy(pManifest);
546
+}
547
+
548
+/*
549
+** Possible display modes for forum_display_thread().
550
+*/
551
+enum {
552
+ FD_RAW, /* Like FD_SINGLE, but additionally omit the border, force
553
+ ** unformatted mode, and inhibit history mode */
554
+ FD_SINGLE, /* Render a single post and (optionally) its edit history */
555
+ FD_CHRONO, /* Render all posts in chronological order */
556
+ FD_HIER, /* Render all posts in an indented hierarchy */
557
+};
558
+
559
+/*
560
+** Display a forum thread. If mode is FD_RAW or FD_SINGLE, display only a
561
+** single post from the thread and (optionally) its edit history.
562
+*/
563
+static void forum_display_thread(
564
+ int froot, /* Forum thread root post ID */
565
+ int fpid, /* Selected forum post ID, or 0 if none selected */
566
+ int mode, /* Forum display mode, one of the FD_* enumerations */
567
+ int bUnf, /* True if rendering unformatted */
568
+ int bHist /* True if showing edit history, ignored for FD_RAW */
569
+){
570
+ ForumThread *pThread; /* Thread structure */
571
+ ForumPost *pSelect; /* Currently selected post, or NULL if none */
572
+ ForumPost *p; /* Post iterator pointer */
573
+ char *zQuery; /* Common query string */
574
+ int iIndentScale = 4; /* Indent scale factor, measured in "ex" units */
575
+ int sid; /* Comparison serial ID */
576
+
577
+ /* In raw mode, force unformatted display and disable history. */
578
+ if( mode == FD_RAW ){
579
+ bUnf = 1;
580
+ bHist = 0;
581
+ }
582
+
583
+ /* Thread together the posts and (optionally) compute the hierarchy. */
584
+ pThread = forumthread_create(froot, mode==FD_HIER);
585
+
586
+ /* Compute the appropriate indent scaling. */
587
+ if( mode==FD_HIER ){
588
+ iIndentScale = 4;
589
+ while( iIndentScale>1 && iIndentScale*pThread->mxIndent>25 ){
590
+ iIndentScale--;
591
+ }
592
+ }else{
593
+ iIndentScale = 0;
594
+ }
595
+
596
+ /* Find the selected post, or (depending on parameters) its latest edit. */
597
+ pSelect = fpid ? forumpost_forward(pThread->pFirst, fpid) : 0;
598
+ if( !bHist && mode!=FD_RAW && pSelect && pSelect->pEditTail ){
599
+ pSelect = pSelect->pEditTail;
600
+ }
601
+
602
+ /* When displaying only a single post, abort if no post was selected or the
603
+ ** selected forum post does not exist in the thread. Otherwise proceed to
604
+ ** display the entire thread without marking any posts as selected. */
605
+ if( !pSelect && (mode==FD_RAW || mode==FD_SINGLE) ){
606
+ return;
607
+ }
608
+
609
+ /* Create the common query string to append to nearly all post links. */
610
+ zQuery = mode==FD_RAW ? 0 : mprintf("t=%c%s%s",
611
+ mode==FD_SINGLE ? 's' : mode==FD_CHRONO ? 'c' : 'h',
612
+ bUnf ? "&unf" : "", bHist ? "&hist" : "");
613
+
614
+ /* Identify which post to display first. If history is shown, start with the
615
+ ** original, unedited post. Otherwise advance to the post's latest edit. */
616
+ if( mode==FD_RAW || mode==FD_SINGLE ){
617
+ p = pSelect;
618
+ if( bHist && p->pEditHead ) p = p->pEditHead;
619
+ }else{
620
+ p = mode==FD_CHRONO ? pThread->pFirst : pThread->pDisplay;
621
+ if( !bHist && p->pEditTail ) p = p->pEditTail;
622
+ }
623
+
624
+ /* Display the appropriate subset of posts in sequence. */
625
+ while( p ){
626
+ /* Display the post. */
627
+ forum_display_post(p, iIndentScale, mode==FD_RAW,
628
+ bUnf, bHist, p==pSelect, zQuery);
629
+
630
+ /* Advance to the next post in the thread. */
631
+ if( mode==FD_CHRONO ){
632
+ /* Chronological mode: display posts (optionally including edits) in their
633
+ ** original commit order. */
634
+ if( bHist ){
635
+ p = p->pNext;
636
+ }else{
637
+ sid = p->sid;
638
+ if( p->pEditHead ) p = p->pEditHead;
639
+ do p = p->pNext; while( p && p->sid<=sid );
640
+ if( p && p->pEditTail ) p = p->pEditTail;
641
+ }
642
+ }else if( bHist && p->pEditNext ){
643
+ /* Hierarchical and single mode: display each post's edits in sequence. */
644
+ p = p->pEditNext;
645
+ }else if( mode==FD_HIER ){
646
+ /* Hierarchical mode: after displaying with each post (optionally
647
+ ** including edits), go to the next post in computed display order. */
648
+ p = p->pEditHead ? p->pEditHead->pDisplay : p->pDisplay;
649
+ if( !bHist && p && p->pEditTail ) p = p->pEditTail;
650
+ }else{
651
+ /* Single and raw mode: terminate after displaying the selected post and
652
+ ** (optionally) its edits. */
653
+ break;
654
+ }
655
+ }
656
+
657
+ /* Undocumented "threadtable" query parameter causes thread table to be
658
+ ** displayed for debugging purposes. */
519659
if( PB("threadtable") ){
520660
@ <hr>
521661
@ <table border="1" cellpadding="3" cellspacing="0">
522
- @ <tr><th>sid<th>fpid<th>firt<th>fprev<th>mfirt<th>pLeaf<th>nReply<th>hash
662
+ @ <tr><th>sid<th>rev<th>fpid<th>pIrt<th>pEditHead<th>pEditTail\
663
+ @ <th>pEditNext<th>pEditPrev<th>pDisplay<th>hash
523664
for(p=pThread->pFirst; p; p=p->pNext){
524
- @ <tr><td>%d(p->sid)<td>%d(p->fpid)<td>%d(p->firt)\
525
- @ <td>%d(p->fprev)<td>%d(p->mfirt)\
526
- @ <td>%d(p->pLeaf?p->pLeaf->fpid:0)<td>%d(p->nReply)\
665
+ @ <tr><td>%d(p->sid)<td>%d(p->rev)<td>%d(p->fpid)\
666
+ @ <td>%d(p->pIrt ? p->pIrt->fpid : 0)\
667
+ @ <td>%d(p->pEditHead ? p->pEditHead->fpid : 0)\
668
+ @ <td>%d(p->pEditTail ? p->pEditTail->fpid : 0)\
669
+ @ <td>%d(p->pEditNext ? p->pEditNext->fpid : 0)\
670
+ @ <td>%d(p->pEditPrev ? p->pEditPrev->fpid : 0)\
671
+ @ <td>%d(p->pDisplay ? p->pDisplay->fpid : 0)\
527672
@ <td>%S(p->zUuid)</tr>
528673
}
529674
@ </table>
530675
}
531676
532
- forumthread_delete(pThread);
533
-}
534
-/*
535
-** Display all the edit history of post "target".
536
-*/
537
-static void forum_display_history(int froot, int target, int bRawMode){
538
- ForumThread *pThread = forumthread_create(froot, 0);
539
- ForumEntry *p;
540
- int notAnon = login_is_individual();
541
- char cMode = bRawMode ? 'r' : 'c';
542
- ForumEntry *pLeaf = 0;
543
- int cnt = 0;
544
- for(p=pThread->pFirst; p; p=p->pNext){
545
- if( p->fpid==target ){
546
- pLeaf = p->pLeaf ? p->pLeaf : p;
547
- break;
548
- }
549
- }
550
- for(p=pThread->pFirst; p; p=p->pNext){
551
- char *zDate;
552
- Manifest *pPost;
553
- int isPrivate; /* True for posts awaiting moderation */
554
- int sameUser; /* True if author is also the reader */
555
- const char *zUuid;
556
- char *zDisplayName; /* The display name */
557
-
558
- if( p->fpid!=pLeaf->fpid && p->pLeaf!=pLeaf ) continue;
559
- cnt++;
560
- pPost = manifest_get(p->fpid, CFTYPE_FORUM, 0);
561
- if( pPost==0 ) continue;
562
- @ <div id="forum%d(p->fpid)" class="forumTime">
563
- zDate = db_text(0, "SELECT datetime(%.17g)", pPost->rDate);
564
- zDisplayName = display_name_from_login(pPost->zUser);
565
- @ <h3 class='forumPostHdr'>(%d(p->sid)) By %h(zDisplayName) on %h(zDate)
566
- fossil_free(zDisplayName);
567
- fossil_free(zDate);
568
- if( g.perm.Debug ){
569
- @ <span class="debug">\
570
- @ <a href="%R/artifact/%h(p->zUuid)">(artifact-%d(p->fpid))</a></span>
571
- }
572
- if( p->firt && cnt==1 ){
573
- ForumEntry *pIrt = p->pPrev;
574
- while( pIrt && pIrt->fpid!=p->firt ) pIrt = pIrt->pPrev;
575
- if( pIrt ){
576
- @ in reply to %z(href("%R/forumpost/%S?t=%c",pIrt->zUuid,cMode))\
577
- @ %d(pIrt->sid)</a>
578
- }
579
- }
580
- zUuid = p->zUuid;
581
- @ %z(href("%R/forumpost/%S?t=c",zUuid))[link]</a>
582
- if( !bRawMode ){
583
- @ %z(href("%R/forumpost/%S?raw",zUuid))[source]</a>
584
- }
585
- isPrivate = content_is_private(p->fpid);
586
- sameUser = notAnon && fossil_strcmp(pPost->zUser, g.zLogin)==0;
587
- @ </h3>
588
- if( isPrivate && !g.perm.ModForum && !sameUser ){
589
- @ <p><span class="modpending">Awaiting Moderator Approval</span></p>
590
- }else{
591
- forum_render(0, bRawMode?"text/plain":pPost->zMimetype, pPost->zWiki,
592
- 0, 1);
593
- }
594
- if( g.perm.WrForum && p->pLeaf==0 ){
595
- int sameUser = login_is_individual()
596
- && fossil_strcmp(pPost->zUser, g.zLogin)==0;
597
- @ <div><form action="%R/forumedit" method="POST">
598
- @ <input type="hidden" name="fpid" value="%s(p->zUuid)">
599
- if( !isPrivate ){
600
- /* Reply and Edit are only available if the post has already
601
- ** been approved */
602
- @ <input type="submit" name="reply" value="Reply">
603
- if( g.perm.Admin || sameUser ){
604
- @ <input type="submit" name="edit" value="Edit">
605
- @ <input type="submit" name="nullout" value="Delete">
606
- }
607
- }else if( g.perm.ModForum ){
608
- /* Provide moderators with moderation buttons for posts that
609
- ** are pending moderation */
610
- @ <input type="submit" name="approve" value="Approve">
611
- @ <input type="submit" name="reject" value="Reject">
612
- generateTrustControls(pPost);
613
- }else if( sameUser ){
614
- /* A post that is pending moderation can be deleted by the
615
- ** person who originally submitted the post */
616
- @ <input type="submit" name="reject" value="Delete">
617
- }
618
- @ </form></div>
619
- }
620
- manifest_destroy(pPost);
621
- @ </div>
622
- }
623
- forumthread_delete(pThread);
624
-}
625
-
626
-/*
627
-** Display all messages in a forumthread with indentation.
628
-*/
629
-static int forum_display_hierarchical(int froot, int target){
630
- ForumThread *pThread;
631
- ForumEntry *p;
632
- Manifest *pPost, *pOPost;
633
- int fpid;
634
- const char *zUuid;
635
- char *zDate;
636
- const char *zSel;
637
- int notAnon = login_is_individual();
638
- int iIndentScale = 4;
639
-
640
- pThread = forumthread_create(froot, 1);
641
- for(p=pThread->pFirst; p; p=p->pNext){
642
- if( p->fpid==target ){
643
- while( p->pEdit ) p = p->pEdit;
644
- target = p->fpid;
645
- break;
646
- }
647
- }
648
- while( iIndentScale>1 && iIndentScale*pThread->mxIndent>25 ){
649
- iIndentScale--;
650
- }
651
- for(p=pThread->pDisplay; p; p=p->pDisplay){
652
- int isPrivate; /* True for posts awaiting moderation */
653
- int sameUser; /* True if reader is also the poster */
654
- char *zDisplayName; /* User name to be displayed */
655
- pOPost = manifest_get(p->fpid, CFTYPE_FORUM, 0);
656
- if( p->pLeaf ){
657
- fpid = p->pLeaf->fpid;
658
- zUuid = p->pLeaf->zUuid;
659
- pPost = manifest_get(fpid, CFTYPE_FORUM, 0);
660
- }else{
661
- fpid = p->fpid;
662
- zUuid = p->zUuid;
663
- pPost = pOPost;
664
- }
665
- zSel = p->fpid==target ? " forumSel" : "";
666
- if( p->nIndent==1 ){
667
- @ <div id='forum%d(fpid)' class='forumHierRoot%s(zSel)'>
668
- }else{
669
- @ <div id='forum%d(fpid)' class='forumHier%s(zSel)' \
670
- @ style='margin-left: %d((p->nIndent-1)*iIndentScale)ex;'>
671
- }
672
- if( pPost==0 ) continue;
673
- if( pPost->zThreadTitle ){
674
- @ <h1>%h(pPost->zThreadTitle)</h1>
675
- }
676
- zDate = db_text(0, "SELECT datetime(%.17g)", pOPost->rDate);
677
- zDisplayName = display_name_from_login(pOPost->zUser);
678
- @ <h3 class='forumPostHdr'>\
679
- @ (%d(p->sid)) By %h(zDisplayName) on %h(zDate)
680
- fossil_free(zDisplayName);
681
- fossil_free(zDate);
682
- if( g.perm.Debug ){
683
- @ <span class="debug">\
684
- @ <a href="%R/artifact/%h(p->zUuid)">(artifact-%d(p->fpid))</a></span>
685
- }
686
- if( p->pLeaf ){
687
- zDate = db_text(0, "SELECT datetime(%.17g)", pPost->rDate);
688
- if( fossil_strcmp(pOPost->zUser,pPost->zUser)==0 ){
689
- @ and edited on %h(zDate)
690
- }else{
691
- @ as edited by %h(pPost->zUser) on %h(zDate)
692
- }
693
- fossil_free(zDate);
694
- if( g.perm.Debug ){
695
- @ <span class="debug">\
696
- @ <a href="%R/artifact/%h(p->pLeaf->zUuid)">\
697
- @ (artifact-%d(p->pLeaf->fpid))</a></span>
698
- }
699
- @ %z(href("%R/forumpost/%S?t=y",p->zUuid))[history]</a>
700
- manifest_destroy(pOPost);
701
- }
702
- if( fpid!=target ){
703
- @ %z(href("%R/forumpost/%S",zUuid))[link]</a>
704
- }
705
- @ %z(href("%R/forumpost/%S?raw",zUuid))[source]</a>
706
- if( p->firt ){
707
- ForumEntry *pIrt = p->pPrev;
708
- while( pIrt && pIrt->fpid!=p->mfirt ) pIrt = pIrt->pPrev;
709
- if( pIrt ){
710
- @ in reply to %z(href("%R/forumpost/%S?t=h",pIrt->zUuid))\
711
- @ %d(pIrt->sid)</a>
712
- }
713
- }
714
- @ </h3>
715
- isPrivate = content_is_private(fpid);
716
- sameUser = notAnon && fossil_strcmp(pPost->zUser, g.zLogin)==0;
717
- if( isPrivate && !g.perm.ModForum && !sameUser ){
718
- @ <p><span class="modpending">Awaiting Moderator Approval</span></p>
719
- }else{
720
- forum_render(0, pPost->zMimetype, pPost->zWiki, 0, 1);
721
- }
722
- if( g.perm.WrForum ){
723
- @ <div><form action="%R/forumedit" method="POST">
724
- @ <input type="hidden" name="fpid" value="%s(zUuid)">
725
- if( !isPrivate ){
726
- /* Reply and Edit are only available if the post has already
727
- ** been approved */
728
- @ <input type="submit" name="reply" value="Reply">
729
- if( g.perm.Admin || sameUser ){
730
- @ <input type="submit" name="edit" value="Edit">
731
- @ <input type="submit" name="nullout" value="Delete">
732
- }
733
- }else if( g.perm.ModForum ){
734
- /* Provide moderators with moderation buttons for posts that
735
- ** are pending moderation */
736
- @ <input type="submit" name="approve" value="Approve">
737
- @ <input type="submit" name="reject" value="Reject">
738
- generateTrustControls(pPost);
739
- }else if( sameUser ){
740
- /* A post that is pending moderation can be deleted by the
741
- ** person who originally submitted the post */
742
- @ <input type="submit" name="reject" value="Delete">
743
- }
744
- @ </form></div>
745
- }
746
- manifest_destroy(pPost);
747
- @ </div>
748
- }
749
- forumthread_delete(pThread);
750
- return target;
751
-}
752
-
753
-/*
754
-** Emits all JS code required by /forumpost.
755
-*/
756
-static void forumpost_emit_page_js(){
757
- static int once = 0;
758
- if(0==once){
759
- once = 1;
760
- style_emit_script_fossil_bootstrap(1);
761
- builtin_request_js("forum.js");
762
- builtin_request_js("fossil.dom.js");
763
- builtin_request_js("fossil.page.forumpost.js");
764
- }
677
+ /* Clean up. */
678
+ forumthread_delete(pThread);
679
+ fossil_free(zQuery);
765680
}
766681
767682
/*
768683
** WEBPAGE: forumpost
769684
**
770685
** Show a single forum posting. The posting is shown in context with
771
-** it's entire thread. The selected posting is enclosed within
686
+** its entire thread. The selected posting is enclosed within
772687
** <div class='forumSel'>...</div>. Javascript is used to move the
773688
** selected posting into view after the page loads.
774689
**
775690
** Query parameters:
776691
**
777
-** name=X REQUIRED. The hash of the post to display
778
-** t=MODE Display mode.
779
-** 'c' for chronological
780
-** 'h' for hierarchical
781
-** 'a' for automatic
782
-** 'r' for raw
783
-** 'y' for history of post X only
784
-** raw If present, show only the post specified and
785
-** show its original unformatted source text.
692
+** name=X REQUIRED. The hash of the post to display.
693
+** t=a Automatic display mode, i.e. hierarchical for
694
+** desktop and chronological for mobile. This is the
695
+** default if the "t" query parameter is omitted.
696
+** t=c Show posts in the order they were written.
697
+** t=h Show posts usin hierarchical indenting.
698
+** t=s Show only the post specified by "name=X".
699
+** t=r Alias for "t=c&unf&hist".
700
+** t=y Alias for "t=s&unf&hist".
701
+** raw Alias for "t=s&unf". Additionally, omit the border
702
+** around the post, and ignore "t" and "hist".
703
+** unf Show the original, unformatted source text.
704
+** hist Show edit history in addition to current posts.
786705
*/
787706
void forumpost_page(void){
788707
forumthread_page();
789708
}
790709
791
-/*
792
-** Add an appropriate style_header() to include title of the
793
-** given forum post.
794
-*/
795
-static int forumthread_page_header(int froot, int fpid){
796
- char *zThreadTitle = 0;
797
-
798
- zThreadTitle = db_text("",
799
- "SELECT"
800
- " substr(event.comment,instr(event.comment,':')+2)"
801
- " FROM forumpost, event"
802
- " WHERE event.objid=forumpost.fpid"
803
- " AND forumpost.fpid=%d;",
804
- fpid
805
- );
806
- style_header("%s%s", zThreadTitle, zThreadTitle[0] ? "" : "Forum");
807
- fossil_free(zThreadTitle);
808
- return 0;
809
-}
810
-
811710
/*
812711
** WEBPAGE: forumthread
813712
**
814713
** Show all forum messages associated with a particular message thread.
815714
** The result is basically the same as /forumpost except that none of
@@ -816,24 +715,28 @@
816715
** the postings in the thread are selected.
817716
**
818717
** Query parameters:
819718
**
820719
** name=X REQUIRED. The hash of any post of the thread.
821
-** t=MODE Display mode. MODE is...
822
-** 'c' for chronological, or
823
-** 'h' for hierarchical, or
824
-** 'a' for automatic, or
825
-** 'r' for raw.
826
-** raw Show only the post given by name= and show it unformatted
827
-** hist Show only the edit history for the name= post
720
+** t=a Automatic display mode, i.e. hierarchical for
721
+** desktop and chronological for mobile. This is the
722
+** default if the "t" query parameter is omitted.
723
+** t=c Show posts in the order they were written.
724
+** t=h Show posts using hierarchical indenting.
725
+** unf Show the original, unformatted source text.
726
+** hist Show edit history in addition to current posts.
828727
*/
829728
void forumthread_page(void){
830729
int fpid;
831730
int froot;
731
+ char *zThreadTitle;
832732
const char *zName = P("name");
833733
const char *zMode = PD("t","a");
834734
int bRaw = PB("raw");
735
+ int bUnf = PB("unf");
736
+ int bHist = PB("hist");
737
+ int mode;
835738
login_check_credentials();
836739
if( !g.perm.RdForum ){
837740
login_needed(g.anon.RdForum);
838741
return;
839742
}
@@ -847,54 +750,70 @@
847750
froot = db_int(0, "SELECT froot FROM forumpost WHERE fpid=%d", fpid);
848751
if( froot==0 ){
849752
webpage_error("Not a forum post: \"%s\"", zName);
850753
}
851754
if( fossil_strcmp(g.zPath,"forumthread")==0 ) fpid = 0;
852
- if( zMode[0]=='a' ){
853
- if( cgi_from_mobile() ){
854
- zMode = "c"; /* Default to chronological on mobile */
855
- }else{
856
- zMode = "h";
857
- }
858
- }
859
- if( zMode[0]!='y' ){
860
- forumthread_page_header(froot, fpid);
861
- }
862
- if( bRaw && fpid ){
863
- Manifest *pPost;
864
- pPost = manifest_get(fpid, CFTYPE_FORUM, 0);
865
- if( pPost==0 ){
866
- @ <p>No such forum post: %h(zName)
867
- }else{
868
- int isPrivate = content_is_private(fpid);
869
- int notAnon = login_is_individual();
870
- int sameUser = notAnon && fossil_strcmp(pPost->zUser, g.zLogin)==0;
871
- if( isPrivate && !g.perm.ModForum && !sameUser ){
872
- @ <p><span class="modpending">Awaiting Moderator Approval</span></p>
873
- }else{
874
- forum_render(0, "text/plain", pPost->zWiki, 0, 0);
875
- }
876
- manifest_destroy(pPost);
877
- }
878
- }else if( zMode[0]=='c' ){
879
- style_submenu_element("Hierarchical", "%R/%s/%s?t=h", g.zPath, zName);
880
- style_submenu_element("Unformatted", "%R/%s/%s?t=r", g.zPath, zName);
881
- forum_display_chronological(froot, fpid, 0);
882
- }else if( zMode[0]=='r' ){
883
- style_submenu_element("Chronological", "%R/%s/%s?t=c", g.zPath, zName);
884
- style_submenu_element("Hierarchical", "%R/%s/%s?t=h", g.zPath, zName);
885
- forum_display_chronological(froot, fpid, 1);
886
- }else if( zMode[0]=='y' ){
887
- style_header("Edit History Of A Forum Post");
888
- style_submenu_element("Complete Thread", "%R/%s/%s?t=a", g.zPath, zName);
889
- forum_display_history(froot, fpid, 1);
890
- }else{
891
- style_submenu_element("Chronological", "%R/%s/%s?t=c", g.zPath, zName);
892
- style_submenu_element("Unformatted", "%R/%s/%s?t=r", g.zPath, zName);
893
- forum_display_hierarchical(froot, fpid);
894
- }
895
- forumpost_emit_page_js();
755
+
756
+ /* Decode the mode parameters. */
757
+ if( bRaw ){
758
+ mode = FD_RAW;
759
+ bUnf = 1;
760
+ bHist = 0;
761
+ cgi_replace_query_parameter("unf", "on");
762
+ cgi_delete_query_parameter("hist");
763
+ cgi_delete_query_parameter("raw");
764
+ }else{
765
+ switch( *zMode ){
766
+ case 'a': mode = cgi_from_mobile() ? FD_CHRONO : FD_HIER; break;
767
+ case 'c': mode = FD_CHRONO; break;
768
+ case 'h': mode = FD_HIER; break;
769
+ case 's': mode = FD_SINGLE; break;
770
+ case 'r': mode = FD_CHRONO; break;
771
+ case 'y': mode = FD_SINGLE; break;
772
+ default: webpage_error("Invalid thread mode: \"%s\"", zMode);
773
+ }
774
+ if( *zMode=='r' || *zMode=='y') {
775
+ bUnf = 1;
776
+ bHist = 1;
777
+ cgi_replace_query_parameter("t", mode==FD_CHRONO ? "c" : "s");
778
+ cgi_replace_query_parameter("unf", "on");
779
+ cgi_replace_query_parameter("hist", "on");
780
+ }
781
+ }
782
+
783
+ /* Define the page header. */
784
+ zThreadTitle = db_text("",
785
+ "SELECT"
786
+ " substr(event.comment,instr(event.comment,':')+2)"
787
+ " FROM forumpost, event"
788
+ " WHERE event.objid=forumpost.fpid"
789
+ " AND forumpost.fpid=%d;",
790
+ fpid
791
+ );
792
+ style_header("%s%s", zThreadTitle, *zThreadTitle ? "" : "Forum");
793
+ fossil_free(zThreadTitle);
794
+ if( mode!=FD_CHRONO ){
795
+ style_submenu_element("Chronological", "%R/%s/%s?t=c%s%s", g.zPath, zName,
796
+ bUnf ? "&unf" : "", bHist ? "&hist" : "");
797
+ }
798
+ if( mode!=FD_HIER ){
799
+ style_submenu_element("Hierarchical", "%R/%s/%s?t=h%s%s", g.zPath, zName,
800
+ bUnf ? "&unf" : "", bHist ? "&hist" : "");
801
+ }
802
+ style_submenu_checkbox("unf", "Unformatted", 0, 0);
803
+ style_submenu_checkbox("hist", "History", 0, 0);
804
+
805
+ /* Display the thread. */
806
+ forum_display_thread(froot, fpid, mode, bUnf, bHist);
807
+
808
+ /* Emit Forum Javascript. */
809
+ style_emit_script_fossil_bootstrap(1);
810
+ builtin_request_js("forum.js");
811
+ builtin_request_js("fossil.dom.js");
812
+ builtin_request_js("fossil.page.forumpost.js");
813
+
814
+ /* Emit the page style. */
896815
style_footer();
897816
}
898817
899818
/*
900819
** Return true if a forum post should be moderated.
@@ -949,11 +868,11 @@
949868
webpage_assert( (zTitle==0)+(iInReplyTo==0)==1 );
950869
blob_init(&x, 0, 0);
951870
zDate = date_in_standard_format("now");
952871
blob_appendf(&x, "D %s\n", zDate);
953872
fossil_free(zDate);
954
- zG = db_text(0,
873
+ zG = db_text(0,
955874
"SELECT uuid FROM blob, forumpost"
956875
" WHERE blob.rid==forumpost.froot"
957876
" AND forumpost.fpid=%d", iBasis);
958877
if( zG ){
959878
blob_appendf(&x, "G %s\n", zG);
@@ -1017,11 +936,11 @@
1017936
}
1018937
1019938
/*
1020939
** Paint the form elements for entering a Forum post
1021940
*/
1022
-static void forum_entry_widget(
941
+static void forum_post_widget(
1023942
const char *zTitle,
1024943
const char *zMimetype,
1025944
const char *zContent
1026945
){
1027946
if( zTitle ){
@@ -1126,11 +1045,11 @@
11261045
}
11271046
style_header("New Forum Thread");
11281047
@ <form action="%R/forume1" method="POST">
11291048
@ <h1>New Thread:</h1>
11301049
forum_from_line();
1131
- forum_entry_widget(zTitle, zMimetype, zContent);
1050
+ forum_post_widget(zTitle, zMimetype, zContent);
11321051
@ <input type="submit" name="preview" value="Preview">
11331052
if( P("preview") && !whitespace_only(zContent) ){
11341053
@ <input type="submit" name="submit" value="Submit">
11351054
}else{
11361055
@ <input type="submit" name="submit" value="Submit" disabled>
@@ -1203,11 +1122,11 @@
12031122
}
12041123
cgi_redirectf("%R/forumpost/%S",P("fpid"));
12051124
return;
12061125
}
12071126
if( P("reject") ){
1208
- char *zParent =
1127
+ char *zParent =
12091128
db_text(0,
12101129
"SELECT uuid FROM forumpost, blob"
12111130
" WHERE forumpost.fpid=%d AND blob.rid=forumpost.firt",
12121131
fpid
12131132
);
@@ -1276,11 +1195,11 @@
12761195
@ <h2>Revised Message:</h2>
12771196
@ <form action="%R/forume2" method="POST">
12781197
@ <input type="hidden" name="fpid" value="%h(P("fpid"))">
12791198
@ <input type="hidden" name="edit" value="1">
12801199
forum_from_line();
1281
- forum_entry_widget(zTitle, zMimetype, zContent);
1200
+ forum_post_widget(zTitle, zMimetype, zContent);
12821201
}else{
12831202
/* Reply */
12841203
char *zDisplayName;
12851204
zMimetype = PD("mimetype",DEFAULT_FORUM_MIMETYPE);
12861205
zContent = PDT("content","");
@@ -1302,11 +1221,11 @@
13021221
@ <h2>Enter Reply:</h2>
13031222
@ <form action="%R/forume2" method="POST">
13041223
@ <input type="hidden" name="fpid" value="%h(P("fpid"))">
13051224
@ <input type="hidden" name="reply" value="1">
13061225
forum_from_line();
1307
- forum_entry_widget(0, zMimetype, zContent);
1226
+ forum_post_widget(0, zMimetype, zContent);
13081227
}
13091228
if( !isDelete ){
13101229
@ <input type="submit" name="preview" value="Preview">
13111230
}
13121231
@ <input type="submit" name="cancel" value="Cancel">
13131232
--- src/forum.c
+++ src/forum.c
@@ -26,44 +26,45 @@
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 ForumEntry {
36 int fpid; /* rid for this entry */
37 int fprev; /* zero if initial entry. non-zero if an edit */
38 int firt; /* This entry replies to firt */
39 int mfirt; /* Root in-reply-to */
40 int nReply; /* Number of replies to this entry */
41 int sid; /* Serial ID number */
 
42 char *zUuid; /* Artifact hash */
43 ForumEntry *pLeaf; /* Most recent edit for this entry */
44 ForumEntry *pEdit; /* This entry is an edit of pEdit */
45 ForumEntry *pNext; /* Next in chronological order */
46 ForumEntry *pPrev; /* Previous in chronological order */
47 ForumEntry *pDisplay; /* Next in display order */
48 int nIndent; /* Number of levels of indentation for this entry */
 
 
 
 
49 };
50
51 /*
52 ** A single instance of the following tracks all entries for a thread.
53 */
54 struct ForumThread {
55 ForumEntry *pFirst; /* First entry in chronological order */
56 ForumEntry *pLast; /* Last entry in chronological order */
57 ForumEntry *pDisplay; /* Entries in display order */
58 ForumEntry *pTail; /* Last on the display list */
59 int mxIndent; /* Maximum indentation level */
60 };
61 #endif /* INTERFACE */
62
63 /*
64 ** Return true if the forum entry with the given rid has been
65 ** subsequently edited.
66 */
67 int forum_rid_has_been_edited(int rid){
68 static Stmt q;
69 int res;
@@ -79,41 +80,39 @@
79
80 /*
81 ** Delete a complete ForumThread and all its entries.
82 */
83 static void forumthread_delete(ForumThread *pThread){
84 ForumEntry *pEntry, *pNext;
85 for(pEntry=pThread->pFirst; pEntry; pEntry = pNext){
86 pNext = pEntry->pNext;
87 fossil_free(pEntry->zUuid);
88 fossil_free(pEntry);
89 }
90 fossil_free(pThread);
91 }
92
93 #if 0 /* not used */
94 /*
95 ** Search a ForumEntry list forwards looking for the entry with fpid
96 */
97 static ForumEntry *forumentry_forward(ForumEntry *p, int fpid){
98 while( p && p->fpid!=fpid ) p = p->pNext;
99 return p;
100 }
101 #endif
102
103 /*
104 ** Search backwards for a ForumEntry
105 */
106 static ForumEntry *forumentry_backward(ForumEntry *p, int fpid){
107 while( p && p->fpid!=fpid ) p = p->pPrev;
108 return p;
109 }
110
111 /*
112 ** Add an entry to the display list
113 */
114 static void forumentry_add_to_display(ForumThread *pThread, ForumEntry *p){
115 if( pThread->pDisplay==0 ){
116 pThread->pDisplay = p;
117 }else{
118 pThread->pTail->pDisplay = p;
119 }
@@ -120,108 +119,112 @@
120 pThread->pTail = p;
121 }
122
123 /*
124 ** Extend the display list for pThread by adding all entries that
125 ** reference fpid. The first such entry will be no earlier then
126 ** entry "p".
127 */
128 static void forumthread_display_order(
129 ForumThread *pThread, /* The complete thread */
130 ForumEntry *pBase /* Add replies to this entry */
131 ){
132 ForumEntry *p;
133 ForumEntry *pPrev = 0;
 
134 for(p=pBase->pNext; p; p=p->pNext){
135 if( p->fprev==0 && p->mfirt==pBase->fpid ){
136 if( pPrev ){
137 pPrev->nIndent = pBase->nIndent + 1;
138 forumentry_add_to_display(pThread, pPrev);
139 forumthread_display_order(pThread, pPrev);
140 }
141 pBase->nReply++;
142 pPrev = p;
 
 
143 }
144 }
145 if( pPrev ){
146 pPrev->nIndent = pBase->nIndent + 1;
147 if( pPrev->nIndent>pThread->mxIndent ) pThread->mxIndent = pPrev->nIndent;
148 forumentry_add_to_display(pThread, pPrev);
149 forumthread_display_order(pThread, pPrev);
150 }
151 }
152
153 /*
154 ** Construct a ForumThread object given the root record id.
155 */
156 static ForumThread *forumthread_create(int froot, int computeHierarchy){
157 ForumThread *pThread;
158 ForumEntry *pEntry;
 
159 Stmt q;
160 int sid = 1;
161 Bag seen = Bag_INIT;
162 pThread = fossil_malloc( sizeof(*pThread) );
163 memset(pThread, 0, sizeof(*pThread));
164 db_prepare(&q,
165 "SELECT fpid, firt, fprev, (SELECT uuid FROM blob WHERE rid=fpid)"
166 " FROM forumpost"
167 " WHERE froot=%d ORDER BY fmtime",
168 froot
169 );
170 while( db_step(&q)==SQLITE_ROW ){
171 pEntry = fossil_malloc( sizeof(*pEntry) );
172 memset(pEntry, 0, sizeof(*pEntry));
173 pEntry->fpid = db_column_int(&q, 0);
174 pEntry->firt = db_column_int(&q, 1);
175 pEntry->fprev = db_column_int(&q, 2);
176 pEntry->zUuid = fossil_strdup(db_column_text(&q,3));
177 pEntry->mfirt = pEntry->firt;
178 pEntry->sid = sid++;
179 pEntry->pPrev = pThread->pLast;
180 pEntry->pNext = 0;
181 bag_insert(&seen, pEntry->fpid);
182 if( pThread->pLast==0 ){
183 pThread->pFirst = pEntry;
184 }else{
185 pThread->pLast->pNext = pEntry;
186 }
187 if( pEntry->firt && !bag_find(&seen,pEntry->firt) ){
188 pEntry->firt = froot;
189 pEntry->mfirt = froot;
190 }
191 pThread->pLast = pEntry;
192 }
193 db_finalize(&q);
194 bag_clear(&seen);
195
196 /* Establish which entries are the latest edit. After this loop
197 ** completes, entries that have non-NULL pLeaf should not be
198 ** displayed.
199 */
200 for(pEntry=pThread->pFirst; pEntry; pEntry=pEntry->pNext){
201 if( pEntry->fprev ){
202 ForumEntry *pBase = 0, *p;
203 p = forumentry_backward(pEntry->pPrev, pEntry->fprev);
204 pEntry->pEdit = p;
205 while( p ){
206 pBase = p;
207 p->pLeaf = pEntry;
208 p = pBase->pEdit;
209 }
210 for(p=pEntry->pNext; p; p=p->pNext){
211 if( p->mfirt==pEntry->fpid ) p->mfirt = pBase->fpid;
212 }
213 }
214 }
 
 
215
216 if( computeHierarchy ){
217 /* Compute the hierarchical display order */
218 pEntry = pThread->pFirst;
219 pEntry->nIndent = 1;
220 pThread->mxIndent = 1;
221 forumentry_add_to_display(pThread, pEntry);
222 forumthread_display_order(pThread, pEntry);
223 }
224
225 /* Return the result */
226 return pThread;
227 }
@@ -265,11 +268,11 @@
265 void forumthread_cmd(void){
266 int fpid;
267 int froot;
268 const char *zName;
269 ForumThread *pThread;
270 ForumEntry *p;
271
272 db_find_and_open_repository(0,0);
273 verify_all_options();
274 if( g.argc==2 ){
275 forum_thread_list();
@@ -293,21 +296,22 @@
293 pThread = forumthread_create(froot, 1);
294 fossil_print("Chronological:\n");
295 fossil_print(
296 /* 0 1 2 3 4 5 6 7 */
297 /* 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123 */
298 " sid fpid firt fprev mfirt pLeaf nReply hash\n");
299 for(p=pThread->pFirst; p; p=p->pNext){
300 fossil_print("%4d %9d %9d %9d %9d %9d %6d %8.8s\n", p->sid,
301 p->fpid, p->firt, p->fprev, p->mfirt, p->pLeaf ? p->pLeaf->fpid : 0,
302 p->nReply, p->zUuid);
 
303 }
304 fossil_print("\nDisplay\n");
305 for(p=pThread->pDisplay; p; p=p->pDisplay){
306 fossil_print("%*s", (p->nIndent-1)*3, "");
307 if( p->pLeaf ){
308 fossil_print("%d->%d\n", p->fpid, p->pLeaf->fpid);
309 }else{
310 fossil_print("%d\n", p->fpid);
311 }
312 }
313 forumthread_delete(pThread);
@@ -352,28 +356,10 @@
352 if( zClass ){
353 @ </div>
354 }
355 }
356
357 /*
358 ** Generate the buttons in the display that allow a forum supervisor to
359 ** mark a user as trusted. Only do this if:
360 **
361 ** (1) The poster is an individual, not a special user like "anonymous"
362 ** (2) The current user has Forum Supervisor privilege
363 */
364 static void generateTrustControls(Manifest *pPost){
365 if( !g.perm.AdminForum ) return;
366 if( login_is_special(pPost->zUser) ) return;
367 @ <br>
368 @ <label><input type="checkbox" name="trust">
369 @ Trust user "%h(pPost->zUser)"
370 @ so that future posts by "%h(pPost->zUser)" do not require moderation.
371 @ </label>
372 @ <input type="hidden" name="trustuser" value="%h(pPost->zUser)">
373 }
374
375 /*
376 ** Compute a display name from a login name.
377 **
378 ** If the input login is found in the USER table, then check the USER.INFO
379 ** field to see if it has display-name followed by an email address.
@@ -403,413 +389,326 @@
403 db_reset(&q);
404 return zResult;
405 }
406
407 /*
408 ** Display all posts in a forum thread in chronological order
409 */
410 static void forum_display_chronological(int froot, int target, int bRawMode){
411 ForumThread *pThread = forumthread_create(froot, 0);
412 ForumEntry *p;
413 int notAnon = login_is_individual();
414 char cMode = bRawMode ? 'r' : 'c';
415 for(p=pThread->pFirst; p; p=p->pNext){
416 char *zDate;
417 Manifest *pPost;
418 int isPrivate; /* True for posts awaiting moderation */
419 int sameUser; /* True if author is also the reader */
420 const char *zUuid;
421 char *zDisplayName; /* The display name */
422 int sid;
423
424 pPost = manifest_get(p->fpid, CFTYPE_FORUM, 0);
425 if( pPost==0 ) continue;
426 if( p->fpid==target ){
427 @ <div id="forum%d(p->fpid)" class="forumTime forumSel">
428 }else if( p->pLeaf!=0 ){
429 @ <div id="forum%d(p->fpid)" class="forumTime forumObs">
430 }else{
431 @ <div id="forum%d(p->fpid)" class="forumTime">
432 }
433 if( pPost->zThreadTitle ){
434 @ <h1>%h(pPost->zThreadTitle)</h1>
435 }
436 zDate = db_text(0, "SELECT datetime(%.17g)", pPost->rDate);
437 zDisplayName = display_name_from_login(pPost->zUser);
438 sid = p->pEdit ? p->pEdit->sid : p->sid;
439 @ <h3 class='forumPostHdr'>(%d(sid)) By %h(zDisplayName) on %h(zDate)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
440 fossil_free(zDisplayName);
441 fossil_free(zDate);
442 if( p->pEdit ){
443 @ edit of %z(href("%R/forumpost/%S?t=%c",p->pEdit->zUuid,cMode))\
444 @ %d(p->pEdit->sid)</a>
 
 
 
 
 
445 }
 
 
446 if( g.perm.Debug ){
447 @ <span class="debug">\
448 @ <a href="%R/artifact/%h(p->zUuid)">(artifact-%d(p->fpid))</a></span>
449 }
450 if( p->firt ){
451 ForumEntry *pIrt = p->pPrev;
452 while( pIrt && pIrt->fpid!=p->firt ) pIrt = pIrt->pPrev;
453 if( pIrt ){
454 @ in reply to %z(href("%R/forumpost/%S?t=%c",pIrt->zUuid,cMode))\
455 @ %d(pIrt->sid)</a>
456 }
457 }
458 zUuid = p->zUuid;
459 if( p->pLeaf ){
460 @ updated by %z(href("%R/forumpost/%S?t=%c",p->pLeaf->zUuid,cMode))\
461 @ %d(p->pLeaf->sid)</a>
462 zUuid = p->pLeaf->zUuid;
463 }
464 if( p->fpid!=target ){
465 @ %z(href("%R/forumpost/%S?t=%c",zUuid,cMode))[link]</a>
466 }
467 if( !bRawMode ){
468 @ %z(href("%R/forumpost/%S?raw",zUuid))[source]</a>
469 }
470 isPrivate = content_is_private(p->fpid);
471 sameUser = notAnon && fossil_strcmp(pPost->zUser, g.zLogin)==0;
 
 
 
 
 
472 @ </h3>
473 if( isPrivate && !g.perm.ModForum && !sameUser ){
474 @ <p><span class="modpending">Awaiting Moderator Approval</span></p>
475 }else{
476 const char *zMimetype;
477 if( bRawMode ){
478 zMimetype = "text/plain";
479 }else if( p->pLeaf!=0 ){
480 zMimetype = "text/plain";
481 }else{
482 zMimetype = pPost->zMimetype;
483 }
484 forum_render(0, zMimetype, pPost->zWiki, 0, 1);
485 }
486 if( g.perm.WrForum && p->pLeaf==0 ){
487 int sameUser = login_is_individual()
488 && fossil_strcmp(pPost->zUser, g.zLogin)==0;
 
 
 
 
 
 
 
 
489 @ <div><form action="%R/forumedit" method="POST">
490 @ <input type="hidden" name="fpid" value="%s(p->zUuid)">
491 if( !isPrivate ){
492 /* Reply and Edit are only available if the post has already
493 ** been approved */
494 @ <input type="submit" name="reply" value="Reply">
495 if( g.perm.Admin || sameUser ){
496 @ <input type="submit" name="edit" value="Edit">
497 @ <input type="submit" name="nullout" value="Delete">
498 }
499 }else if( g.perm.ModForum ){
500 /* Provide moderators with moderation buttons for posts that
501 ** are pending moderation */
 
502 @ <input type="submit" name="approve" value="Approve">
503 @ <input type="submit" name="reject" value="Reject">
504 generateTrustControls(pPost);
505 }else if( sameUser ){
506 /* A post that is pending moderation can be deleted by the
507 ** person who originally submitted the post */
 
 
 
 
 
508 @ <input type="submit" name="reject" value="Delete">
509 }
510 @ </form></div>
511 }
512 manifest_destroy(pPost);
513 @ </div>
514 }
515
516 /* Undocumented "threadtable" query parameter causes thread table
517 ** to be displayed for debugging purposes.
518 */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
519 if( PB("threadtable") ){
520 @ <hr>
521 @ <table border="1" cellpadding="3" cellspacing="0">
522 @ <tr><th>sid<th>fpid<th>firt<th>fprev<th>mfirt<th>pLeaf<th>nReply<th>hash
 
523 for(p=pThread->pFirst; p; p=p->pNext){
524 @ <tr><td>%d(p->sid)<td>%d(p->fpid)<td>%d(p->firt)\
525 @ <td>%d(p->fprev)<td>%d(p->mfirt)\
526 @ <td>%d(p->pLeaf?p->pLeaf->fpid:0)<td>%d(p->nReply)\
 
 
 
 
527 @ <td>%S(p->zUuid)</tr>
528 }
529 @ </table>
530 }
531
532 forumthread_delete(pThread);
533 }
534 /*
535 ** Display all the edit history of post "target".
536 */
537 static void forum_display_history(int froot, int target, int bRawMode){
538 ForumThread *pThread = forumthread_create(froot, 0);
539 ForumEntry *p;
540 int notAnon = login_is_individual();
541 char cMode = bRawMode ? 'r' : 'c';
542 ForumEntry *pLeaf = 0;
543 int cnt = 0;
544 for(p=pThread->pFirst; p; p=p->pNext){
545 if( p->fpid==target ){
546 pLeaf = p->pLeaf ? p->pLeaf : p;
547 break;
548 }
549 }
550 for(p=pThread->pFirst; p; p=p->pNext){
551 char *zDate;
552 Manifest *pPost;
553 int isPrivate; /* True for posts awaiting moderation */
554 int sameUser; /* True if author is also the reader */
555 const char *zUuid;
556 char *zDisplayName; /* The display name */
557
558 if( p->fpid!=pLeaf->fpid && p->pLeaf!=pLeaf ) continue;
559 cnt++;
560 pPost = manifest_get(p->fpid, CFTYPE_FORUM, 0);
561 if( pPost==0 ) continue;
562 @ <div id="forum%d(p->fpid)" class="forumTime">
563 zDate = db_text(0, "SELECT datetime(%.17g)", pPost->rDate);
564 zDisplayName = display_name_from_login(pPost->zUser);
565 @ <h3 class='forumPostHdr'>(%d(p->sid)) By %h(zDisplayName) on %h(zDate)
566 fossil_free(zDisplayName);
567 fossil_free(zDate);
568 if( g.perm.Debug ){
569 @ <span class="debug">\
570 @ <a href="%R/artifact/%h(p->zUuid)">(artifact-%d(p->fpid))</a></span>
571 }
572 if( p->firt && cnt==1 ){
573 ForumEntry *pIrt = p->pPrev;
574 while( pIrt && pIrt->fpid!=p->firt ) pIrt = pIrt->pPrev;
575 if( pIrt ){
576 @ in reply to %z(href("%R/forumpost/%S?t=%c",pIrt->zUuid,cMode))\
577 @ %d(pIrt->sid)</a>
578 }
579 }
580 zUuid = p->zUuid;
581 @ %z(href("%R/forumpost/%S?t=c",zUuid))[link]</a>
582 if( !bRawMode ){
583 @ %z(href("%R/forumpost/%S?raw",zUuid))[source]</a>
584 }
585 isPrivate = content_is_private(p->fpid);
586 sameUser = notAnon && fossil_strcmp(pPost->zUser, g.zLogin)==0;
587 @ </h3>
588 if( isPrivate && !g.perm.ModForum && !sameUser ){
589 @ <p><span class="modpending">Awaiting Moderator Approval</span></p>
590 }else{
591 forum_render(0, bRawMode?"text/plain":pPost->zMimetype, pPost->zWiki,
592 0, 1);
593 }
594 if( g.perm.WrForum && p->pLeaf==0 ){
595 int sameUser = login_is_individual()
596 && fossil_strcmp(pPost->zUser, g.zLogin)==0;
597 @ <div><form action="%R/forumedit" method="POST">
598 @ <input type="hidden" name="fpid" value="%s(p->zUuid)">
599 if( !isPrivate ){
600 /* Reply and Edit are only available if the post has already
601 ** been approved */
602 @ <input type="submit" name="reply" value="Reply">
603 if( g.perm.Admin || sameUser ){
604 @ <input type="submit" name="edit" value="Edit">
605 @ <input type="submit" name="nullout" value="Delete">
606 }
607 }else if( g.perm.ModForum ){
608 /* Provide moderators with moderation buttons for posts that
609 ** are pending moderation */
610 @ <input type="submit" name="approve" value="Approve">
611 @ <input type="submit" name="reject" value="Reject">
612 generateTrustControls(pPost);
613 }else if( sameUser ){
614 /* A post that is pending moderation can be deleted by the
615 ** person who originally submitted the post */
616 @ <input type="submit" name="reject" value="Delete">
617 }
618 @ </form></div>
619 }
620 manifest_destroy(pPost);
621 @ </div>
622 }
623 forumthread_delete(pThread);
624 }
625
626 /*
627 ** Display all messages in a forumthread with indentation.
628 */
629 static int forum_display_hierarchical(int froot, int target){
630 ForumThread *pThread;
631 ForumEntry *p;
632 Manifest *pPost, *pOPost;
633 int fpid;
634 const char *zUuid;
635 char *zDate;
636 const char *zSel;
637 int notAnon = login_is_individual();
638 int iIndentScale = 4;
639
640 pThread = forumthread_create(froot, 1);
641 for(p=pThread->pFirst; p; p=p->pNext){
642 if( p->fpid==target ){
643 while( p->pEdit ) p = p->pEdit;
644 target = p->fpid;
645 break;
646 }
647 }
648 while( iIndentScale>1 && iIndentScale*pThread->mxIndent>25 ){
649 iIndentScale--;
650 }
651 for(p=pThread->pDisplay; p; p=p->pDisplay){
652 int isPrivate; /* True for posts awaiting moderation */
653 int sameUser; /* True if reader is also the poster */
654 char *zDisplayName; /* User name to be displayed */
655 pOPost = manifest_get(p->fpid, CFTYPE_FORUM, 0);
656 if( p->pLeaf ){
657 fpid = p->pLeaf->fpid;
658 zUuid = p->pLeaf->zUuid;
659 pPost = manifest_get(fpid, CFTYPE_FORUM, 0);
660 }else{
661 fpid = p->fpid;
662 zUuid = p->zUuid;
663 pPost = pOPost;
664 }
665 zSel = p->fpid==target ? " forumSel" : "";
666 if( p->nIndent==1 ){
667 @ <div id='forum%d(fpid)' class='forumHierRoot%s(zSel)'>
668 }else{
669 @ <div id='forum%d(fpid)' class='forumHier%s(zSel)' \
670 @ style='margin-left: %d((p->nIndent-1)*iIndentScale)ex;'>
671 }
672 if( pPost==0 ) continue;
673 if( pPost->zThreadTitle ){
674 @ <h1>%h(pPost->zThreadTitle)</h1>
675 }
676 zDate = db_text(0, "SELECT datetime(%.17g)", pOPost->rDate);
677 zDisplayName = display_name_from_login(pOPost->zUser);
678 @ <h3 class='forumPostHdr'>\
679 @ (%d(p->sid)) By %h(zDisplayName) on %h(zDate)
680 fossil_free(zDisplayName);
681 fossil_free(zDate);
682 if( g.perm.Debug ){
683 @ <span class="debug">\
684 @ <a href="%R/artifact/%h(p->zUuid)">(artifact-%d(p->fpid))</a></span>
685 }
686 if( p->pLeaf ){
687 zDate = db_text(0, "SELECT datetime(%.17g)", pPost->rDate);
688 if( fossil_strcmp(pOPost->zUser,pPost->zUser)==0 ){
689 @ and edited on %h(zDate)
690 }else{
691 @ as edited by %h(pPost->zUser) on %h(zDate)
692 }
693 fossil_free(zDate);
694 if( g.perm.Debug ){
695 @ <span class="debug">\
696 @ <a href="%R/artifact/%h(p->pLeaf->zUuid)">\
697 @ (artifact-%d(p->pLeaf->fpid))</a></span>
698 }
699 @ %z(href("%R/forumpost/%S?t=y",p->zUuid))[history]</a>
700 manifest_destroy(pOPost);
701 }
702 if( fpid!=target ){
703 @ %z(href("%R/forumpost/%S",zUuid))[link]</a>
704 }
705 @ %z(href("%R/forumpost/%S?raw",zUuid))[source]</a>
706 if( p->firt ){
707 ForumEntry *pIrt = p->pPrev;
708 while( pIrt && pIrt->fpid!=p->mfirt ) pIrt = pIrt->pPrev;
709 if( pIrt ){
710 @ in reply to %z(href("%R/forumpost/%S?t=h",pIrt->zUuid))\
711 @ %d(pIrt->sid)</a>
712 }
713 }
714 @ </h3>
715 isPrivate = content_is_private(fpid);
716 sameUser = notAnon && fossil_strcmp(pPost->zUser, g.zLogin)==0;
717 if( isPrivate && !g.perm.ModForum && !sameUser ){
718 @ <p><span class="modpending">Awaiting Moderator Approval</span></p>
719 }else{
720 forum_render(0, pPost->zMimetype, pPost->zWiki, 0, 1);
721 }
722 if( g.perm.WrForum ){
723 @ <div><form action="%R/forumedit" method="POST">
724 @ <input type="hidden" name="fpid" value="%s(zUuid)">
725 if( !isPrivate ){
726 /* Reply and Edit are only available if the post has already
727 ** been approved */
728 @ <input type="submit" name="reply" value="Reply">
729 if( g.perm.Admin || sameUser ){
730 @ <input type="submit" name="edit" value="Edit">
731 @ <input type="submit" name="nullout" value="Delete">
732 }
733 }else if( g.perm.ModForum ){
734 /* Provide moderators with moderation buttons for posts that
735 ** are pending moderation */
736 @ <input type="submit" name="approve" value="Approve">
737 @ <input type="submit" name="reject" value="Reject">
738 generateTrustControls(pPost);
739 }else if( sameUser ){
740 /* A post that is pending moderation can be deleted by the
741 ** person who originally submitted the post */
742 @ <input type="submit" name="reject" value="Delete">
743 }
744 @ </form></div>
745 }
746 manifest_destroy(pPost);
747 @ </div>
748 }
749 forumthread_delete(pThread);
750 return target;
751 }
752
753 /*
754 ** Emits all JS code required by /forumpost.
755 */
756 static void forumpost_emit_page_js(){
757 static int once = 0;
758 if(0==once){
759 once = 1;
760 style_emit_script_fossil_bootstrap(1);
761 builtin_request_js("forum.js");
762 builtin_request_js("fossil.dom.js");
763 builtin_request_js("fossil.page.forumpost.js");
764 }
765 }
766
767 /*
768 ** WEBPAGE: forumpost
769 **
770 ** Show a single forum posting. The posting is shown in context with
771 ** it's entire thread. The selected posting is enclosed within
772 ** <div class='forumSel'>...</div>. Javascript is used to move the
773 ** selected posting into view after the page loads.
774 **
775 ** Query parameters:
776 **
777 ** name=X REQUIRED. The hash of the post to display
778 ** t=MODE Display mode.
779 ** 'c' for chronological
780 ** 'h' for hierarchical
781 ** 'a' for automatic
782 ** 'r' for raw
783 ** 'y' for history of post X only
784 ** raw If present, show only the post specified and
785 ** show its original unformatted source text.
 
 
 
 
786 */
787 void forumpost_page(void){
788 forumthread_page();
789 }
790
791 /*
792 ** Add an appropriate style_header() to include title of the
793 ** given forum post.
794 */
795 static int forumthread_page_header(int froot, int fpid){
796 char *zThreadTitle = 0;
797
798 zThreadTitle = db_text("",
799 "SELECT"
800 " substr(event.comment,instr(event.comment,':')+2)"
801 " FROM forumpost, event"
802 " WHERE event.objid=forumpost.fpid"
803 " AND forumpost.fpid=%d;",
804 fpid
805 );
806 style_header("%s%s", zThreadTitle, zThreadTitle[0] ? "" : "Forum");
807 fossil_free(zThreadTitle);
808 return 0;
809 }
810
811 /*
812 ** WEBPAGE: forumthread
813 **
814 ** Show all forum messages associated with a particular message thread.
815 ** The result is basically the same as /forumpost except that none of
@@ -816,24 +715,28 @@
816 ** the postings in the thread are selected.
817 **
818 ** Query parameters:
819 **
820 ** name=X REQUIRED. The hash of any post of the thread.
821 ** t=MODE Display mode. MODE is...
822 ** 'c' for chronological, or
823 ** 'h' for hierarchical, or
824 ** 'a' for automatic, or
825 ** 'r' for raw.
826 ** raw Show only the post given by name= and show it unformatted
827 ** hist Show only the edit history for the name= post
828 */
829 void forumthread_page(void){
830 int fpid;
831 int froot;
 
832 const char *zName = P("name");
833 const char *zMode = PD("t","a");
834 int bRaw = PB("raw");
 
 
 
835 login_check_credentials();
836 if( !g.perm.RdForum ){
837 login_needed(g.anon.RdForum);
838 return;
839 }
@@ -847,54 +750,70 @@
847 froot = db_int(0, "SELECT froot FROM forumpost WHERE fpid=%d", fpid);
848 if( froot==0 ){
849 webpage_error("Not a forum post: \"%s\"", zName);
850 }
851 if( fossil_strcmp(g.zPath,"forumthread")==0 ) fpid = 0;
852 if( zMode[0]=='a' ){
853 if( cgi_from_mobile() ){
854 zMode = "c"; /* Default to chronological on mobile */
855 }else{
856 zMode = "h";
857 }
858 }
859 if( zMode[0]!='y' ){
860 forumthread_page_header(froot, fpid);
861 }
862 if( bRaw && fpid ){
863 Manifest *pPost;
864 pPost = manifest_get(fpid, CFTYPE_FORUM, 0);
865 if( pPost==0 ){
866 @ <p>No such forum post: %h(zName)
867 }else{
868 int isPrivate = content_is_private(fpid);
869 int notAnon = login_is_individual();
870 int sameUser = notAnon && fossil_strcmp(pPost->zUser, g.zLogin)==0;
871 if( isPrivate && !g.perm.ModForum && !sameUser ){
872 @ <p><span class="modpending">Awaiting Moderator Approval</span></p>
873 }else{
874 forum_render(0, "text/plain", pPost->zWiki, 0, 0);
875 }
876 manifest_destroy(pPost);
877 }
878 }else if( zMode[0]=='c' ){
879 style_submenu_element("Hierarchical", "%R/%s/%s?t=h", g.zPath, zName);
880 style_submenu_element("Unformatted", "%R/%s/%s?t=r", g.zPath, zName);
881 forum_display_chronological(froot, fpid, 0);
882 }else if( zMode[0]=='r' ){
883 style_submenu_element("Chronological", "%R/%s/%s?t=c", g.zPath, zName);
884 style_submenu_element("Hierarchical", "%R/%s/%s?t=h", g.zPath, zName);
885 forum_display_chronological(froot, fpid, 1);
886 }else if( zMode[0]=='y' ){
887 style_header("Edit History Of A Forum Post");
888 style_submenu_element("Complete Thread", "%R/%s/%s?t=a", g.zPath, zName);
889 forum_display_history(froot, fpid, 1);
890 }else{
891 style_submenu_element("Chronological", "%R/%s/%s?t=c", g.zPath, zName);
892 style_submenu_element("Unformatted", "%R/%s/%s?t=r", g.zPath, zName);
893 forum_display_hierarchical(froot, fpid);
894 }
895 forumpost_emit_page_js();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
896 style_footer();
897 }
898
899 /*
900 ** Return true if a forum post should be moderated.
@@ -949,11 +868,11 @@
949 webpage_assert( (zTitle==0)+(iInReplyTo==0)==1 );
950 blob_init(&x, 0, 0);
951 zDate = date_in_standard_format("now");
952 blob_appendf(&x, "D %s\n", zDate);
953 fossil_free(zDate);
954 zG = db_text(0,
955 "SELECT uuid FROM blob, forumpost"
956 " WHERE blob.rid==forumpost.froot"
957 " AND forumpost.fpid=%d", iBasis);
958 if( zG ){
959 blob_appendf(&x, "G %s\n", zG);
@@ -1017,11 +936,11 @@
1017 }
1018
1019 /*
1020 ** Paint the form elements for entering a Forum post
1021 */
1022 static void forum_entry_widget(
1023 const char *zTitle,
1024 const char *zMimetype,
1025 const char *zContent
1026 ){
1027 if( zTitle ){
@@ -1126,11 +1045,11 @@
1126 }
1127 style_header("New Forum Thread");
1128 @ <form action="%R/forume1" method="POST">
1129 @ <h1>New Thread:</h1>
1130 forum_from_line();
1131 forum_entry_widget(zTitle, zMimetype, zContent);
1132 @ <input type="submit" name="preview" value="Preview">
1133 if( P("preview") && !whitespace_only(zContent) ){
1134 @ <input type="submit" name="submit" value="Submit">
1135 }else{
1136 @ <input type="submit" name="submit" value="Submit" disabled>
@@ -1203,11 +1122,11 @@
1203 }
1204 cgi_redirectf("%R/forumpost/%S",P("fpid"));
1205 return;
1206 }
1207 if( P("reject") ){
1208 char *zParent =
1209 db_text(0,
1210 "SELECT uuid FROM forumpost, blob"
1211 " WHERE forumpost.fpid=%d AND blob.rid=forumpost.firt",
1212 fpid
1213 );
@@ -1276,11 +1195,11 @@
1276 @ <h2>Revised Message:</h2>
1277 @ <form action="%R/forume2" method="POST">
1278 @ <input type="hidden" name="fpid" value="%h(P("fpid"))">
1279 @ <input type="hidden" name="edit" value="1">
1280 forum_from_line();
1281 forum_entry_widget(zTitle, zMimetype, zContent);
1282 }else{
1283 /* Reply */
1284 char *zDisplayName;
1285 zMimetype = PD("mimetype",DEFAULT_FORUM_MIMETYPE);
1286 zContent = PDT("content","");
@@ -1302,11 +1221,11 @@
1302 @ <h2>Enter Reply:</h2>
1303 @ <form action="%R/forume2" method="POST">
1304 @ <input type="hidden" name="fpid" value="%h(P("fpid"))">
1305 @ <input type="hidden" name="reply" value="1">
1306 forum_from_line();
1307 forum_entry_widget(0, zMimetype, zContent);
1308 }
1309 if( !isDelete ){
1310 @ <input type="submit" name="preview" value="Preview">
1311 }
1312 @ <input type="submit" name="cancel" value="Cancel">
1313
--- src/forum.c
+++ src/forum.c
@@ -26,44 +26,45 @@
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 ForumPost *pIrt; /* This post replies to pIrt */
41 ForumPost *pEditHead; /* Original, unedited post */
42 ForumPost *pEditTail; /* Most recent edit for this post */
43 ForumPost *pEditNext; /* This post is edited by pEditNext */
44 ForumPost *pEditPrev; /* This post is an edit of pEditPrev */
45 ForumPost *pNext; /* Next in chronological order */
46 ForumPost *pPrev; /* Previous in chronological order */
47 ForumPost *pDisplay; /* Next in display order */
48 int nEdit; /* Number of edits to this post */
49 int nIndent; /* Number of levels of indentation for this post */
50 };
51
52 /*
53 ** A single instance of the following tracks all entries for a thread.
54 */
55 struct ForumThread {
56 ForumPost *pFirst; /* First post in chronological order */
57 ForumPost *pLast; /* Last post in chronological order */
58 ForumPost *pDisplay; /* Entries in display order */
59 ForumPost *pTail; /* Last on the display list */
60 int mxIndent; /* Maximum indentation level */
61 };
62 #endif /* INTERFACE */
63
64 /*
65 ** Return true if the forum post with the given rid has been
66 ** subsequently edited.
67 */
68 int forum_rid_has_been_edited(int rid){
69 static Stmt q;
70 int res;
@@ -79,41 +80,39 @@
80
81 /*
82 ** Delete a complete ForumThread and all its entries.
83 */
84 static void forumthread_delete(ForumThread *pThread){
85 ForumPost *pPost, *pNext;
86 for(pPost=pThread->pFirst; pPost; pPost = pNext){
87 pNext = pPost->pNext;
88 fossil_free(pPost->zUuid);
89 fossil_free(pPost);
90 }
91 fossil_free(pThread);
92 }
93
 
94 /*
95 ** Search a ForumPost list forwards looking for the post with fpid
96 */
97 static ForumPost *forumpost_forward(ForumPost *p, int fpid){
98 while( p && p->fpid!=fpid ) p = p->pNext;
99 return p;
100 }
 
101
102 /*
103 ** Search backwards for a ForumPost
104 */
105 static ForumPost *forumpost_backward(ForumPost *p, int fpid){
106 while( p && p->fpid!=fpid ) p = p->pPrev;
107 return p;
108 }
109
110 /*
111 ** Add a post to the display list
112 */
113 static void forumpost_add_to_display(ForumThread *pThread, ForumPost *p){
114 if( pThread->pDisplay==0 ){
115 pThread->pDisplay = p;
116 }else{
117 pThread->pTail->pDisplay = p;
118 }
@@ -120,108 +119,112 @@
119 pThread->pTail = p;
120 }
121
122 /*
123 ** Extend the display list for pThread by adding all entries that
124 ** reference fpid. The first such post will be no earlier then
125 ** post "p".
126 */
127 static void forumthread_display_order(
128 ForumThread *pThread, /* The complete thread */
129 ForumPost *pBase /* Add replies to this post */
130 ){
131 ForumPost *p;
132 ForumPost *pPrev = 0;
133 ForumPost *pBaseIrt;
134 for(p=pBase->pNext; p; p=p->pNext){
135 if( !p->pEditPrev && p->pIrt ){
136 pBaseIrt = p->pIrt->pEditHead ? p->pIrt->pEditHead : p->pIrt;
137 if( pBaseIrt==pBase ){
138 if( pPrev ){
139 pPrev->nIndent = pBase->nIndent + 1;
140 forumpost_add_to_display(pThread, pPrev);
141 forumthread_display_order(pThread, pPrev);
142 }
143 pPrev = p;
144 }
145 }
146 }
147 if( pPrev ){
148 pPrev->nIndent = pBase->nIndent + 1;
149 if( pPrev->nIndent>pThread->mxIndent ) pThread->mxIndent = pPrev->nIndent;
150 forumpost_add_to_display(pThread, pPrev);
151 forumthread_display_order(pThread, pPrev);
152 }
153 }
154
155 /*
156 ** Construct a ForumThread object given the root record id.
157 */
158 static ForumThread *forumthread_create(int froot, int computeHierarchy){
159 ForumThread *pThread;
160 ForumPost *pPost;
161 ForumPost *p;
162 Stmt q;
163 int sid = 1;
164 int firt, fprev;
165 pThread = fossil_malloc( sizeof(*pThread) );
166 memset(pThread, 0, sizeof(*pThread));
167 db_prepare(&q,
168 "SELECT fpid, firt, fprev, (SELECT uuid FROM blob WHERE rid=fpid)"
169 " FROM forumpost"
170 " WHERE froot=%d ORDER BY fmtime",
171 froot
172 );
173 while( db_step(&q)==SQLITE_ROW ){
174 pPost = fossil_malloc( sizeof(*pPost) );
175 memset(pPost, 0, sizeof(*pPost));
176 pPost->fpid = db_column_int(&q, 0);
177 firt = db_column_int(&q, 1);
178 fprev = db_column_int(&q, 2);
179 pPost->zUuid = fossil_strdup(db_column_text(&q,3));
180 if( !fprev ) pPost->sid = sid++;
181 pPost->pPrev = pThread->pLast;
182 pPost->pNext = 0;
 
 
183 if( pThread->pLast==0 ){
184 pThread->pFirst = pPost;
185 }else{
186 pThread->pLast->pNext = pPost;
187 }
188 pThread->pLast = pPost;
189
190 /* Find the in-reply-to post. Default to the topic post if the replied-to
191 ** post cannot be found. */
192 if( firt ){
193 pPost->pIrt = pThread->pFirst;
194 for(p=pThread->pFirst; p; p=p->pNext){
195 if( p->fpid==firt ){
196 pPost->pIrt = p;
197 break;
198 }
199 }
200 }
201
202 /* Maintain the linked list of post edits. */
203 if( fprev ){
204 p = forumpost_backward(pPost->pPrev, fprev);
205 p->pEditNext = pPost;
206 pPost->sid = p->sid;
207 pPost->rev = p->rev+1;
208 pPost->nEdit = p->nEdit+1;
209 pPost->pEditPrev = p;
210 pPost->pEditHead = p->pEditHead ? p->pEditHead : p;
211 for(; p; p=p->pEditPrev ){
212 p->nEdit = pPost->nEdit;
213 p->pEditTail = pPost;
214 }
215 }
216 }
217 db_finalize(&q);
218
219 if( computeHierarchy ){
220 /* Compute the hierarchical display order */
221 pPost = pThread->pFirst;
222 pPost->nIndent = 1;
223 pThread->mxIndent = 1;
224 forumpost_add_to_display(pThread, pPost);
225 forumthread_display_order(pThread, pPost);
226 }
227
228 /* Return the result */
229 return pThread;
230 }
@@ -265,11 +268,11 @@
268 void forumthread_cmd(void){
269 int fpid;
270 int froot;
271 const char *zName;
272 ForumThread *pThread;
273 ForumPost *p;
274
275 db_find_and_open_repository(0,0);
276 verify_all_options();
277 if( g.argc==2 ){
278 forum_thread_list();
@@ -293,21 +296,22 @@
296 pThread = forumthread_create(froot, 1);
297 fossil_print("Chronological:\n");
298 fossil_print(
299 /* 0 1 2 3 4 5 6 7 */
300 /* 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123 */
301 " sid rev fpid pIrt pEditPrev pEditTail hash\n");
302 for(p=pThread->pFirst; p; p=p->pNext){
303 fossil_print("%4d %4d %9d %9d %9d %9d %8.8s\n", p->sid, p->rev,
304 p->fpid, p->pIrt ? p->pIrt->fpid : 0,
305 p->pEditPrev ? p->pEditPrev->fpid : 0,
306 p->pEditTail ? p->pEditTail->fpid : 0, p->zUuid);
307 }
308 fossil_print("\nDisplay\n");
309 for(p=pThread->pDisplay; p; p=p->pDisplay){
310 fossil_print("%*s", (p->nIndent-1)*3, "");
311 if( p->pEditTail ){
312 fossil_print("%d->%d\n", p->fpid, p->pEditTail->fpid);
313 }else{
314 fossil_print("%d\n", p->fpid);
315 }
316 }
317 forumthread_delete(pThread);
@@ -352,28 +356,10 @@
356 if( zClass ){
357 @ </div>
358 }
359 }
360
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
361 /*
362 ** Compute a display name from a login name.
363 **
364 ** If the input login is found in the USER table, then check the USER.INFO
365 ** field to see if it has display-name followed by an email address.
@@ -403,413 +389,326 @@
389 db_reset(&q);
390 return zResult;
391 }
392
393 /*
394 ** Display a single post in a forum thread.
395 */
396 static void forum_display_post(
397 ForumPost *p, /* Forum post to display */
398 int iIndentScale, /* Indent scale factor */
399 int bRaw, /* True to omit the border */
400 int bUnf, /* True to leave the post unformatted */
401 int bHist, /* True if showing edit history */
402 int bSelect, /* True if this is the selected post */
403 char *zQuery /* Common query string */
404 ){
405 char *zDisplayName; /* The display name */
406 char *zDate; /* The time/date string */
407 char *zHist; /* History query string */
408 Manifest *pManifest; /* Manifest comprising the current post */
409 int bPrivate; /* True for posts awaiting moderation */
410 int bSameUser; /* True if author is also the reader */
411 int iIndent; /* Indent level */
412 const char *zMimetype;/* Formatting MIME type */
413
414 /* Get the manifest for the post. Abort if not found (e.g. shunned). */
415 pManifest = manifest_get(p->fpid, CFTYPE_FORUM, 0);
416 if( !pManifest ) return;
417
418 /* When not in raw mode, create the border around the post. */
419 if( !bRaw ){
420 /* Open the <div> enclosing the post. Set the class string to mark the post
421 ** as selected and/or obsolete. */
422 iIndent = (p->pEditHead ? p->pEditHead->nIndent : p->nIndent)-1;
423 @ <div id='forum%d(p->fpid)' class='forumTime\
424 @ %s(bSelect ? " forumSel" : "")\
425 @ %s(p->pEditTail ? " forumObs" : "")'\
426 if( iIndent && iIndentScale ){
427 @ style='margin-left: %d(iIndent*iIndentScale)ex'
428 }
429 @ >
430
431 /* If this is the first post (or an edit thereof), emit the thread title. */
432 if( pManifest->zThreadTitle ){
433 @ <h1>%h(pManifest->zThreadTitle)</h1>
434 }
435
436 /* Emit the serial number, revision number, author, and date. */
437 zDisplayName = display_name_from_login(pManifest->zUser);
438 zDate = db_text(0, "SELECT datetime(%.17g)", pManifest->rDate);
439 @ <h3 class='forumPostHdr'>(%d(p->sid)\
440 if( p->nEdit ){
441 @ .%.*d(fossil_num_digits(p->nEdit))(p->rev)\
442 }
443 @ ) By %h(zDisplayName) on %h(zDate)
444 fossil_free(zDisplayName);
445 fossil_free(zDate);
446
447 /* If this is an edit, refer back to the old version. Be sure "hist" is in
448 ** the query string so the old version will actually be shown. */
449 if( p->pEditPrev ){
450 zHist = bHist ? "" : "&hist";
451 @ edit of \
452 @ %z(href("%R/forumpost/%S?%s%s",p->pEditPrev->zUuid,zQuery,zHist))\
453 @ %d(p->sid).%.*d(fossil_num_digits(p->nEdit))(p->pEditPrev->rev)</a>
454 }
455
456 /* If debugging is enabled, link to the artifact page. */
457 if( g.perm.Debug ){
458 @ <span class="debug">\
459 @ <a href="%R/artifact/%h(p->zUuid)">(artifact-%d(p->fpid))</a></span>
460 }
461
462 /* If this is a reply, refer back to the parent post. */
463 if( p->pIrt ){
464 @ in reply to %z(href("%R/forumpost/%S?%s",p->pIrt->zUuid,zQuery))\
465 @ %d(p->pIrt->sid)\
466 if( p->pIrt->nEdit ){
467 @ .%.*d(fossil_num_digits(p->pIrt->nEdit))(p->pIrt->rev)\
468 }
469 @ </a>
470 }
471
472 /* If this post was later edited, refer forward to the next edit. */
473 if( p->pEditNext ){
474 @ updated by %z(href("%R/forumpost/%S?%s",p->pEditNext->zUuid,zQuery))\
475 @ %d(p->pEditNext->sid)\
476 @ .%.*d(fossil_num_digits(p->nEdit))(p->pEditNext->rev)</a>
477 }
478
479 /* Provide a link to select the individual post. */
480 if( !bSelect ){
481 @ %z(href("%R/forumpost/%S?%s",p->zUuid,zQuery))[link]</a>
482 }
483
484 /* Provide a link to the raw source code. */
485 if( !bUnf ){
486 @ %z(href("%R/forumpost/%S?raw",p->zUuid))[source]</a>
487 }
488 @ </h3>
489 }
490
491 /* Check if this post is approved, also if it's by the current user. */
492 bPrivate = content_is_private(p->fpid);
493 bSameUser = login_is_individual()
494 && fossil_strcmp(pManifest->zUser, g.zLogin)==0;
495
496 /* Render the post if the user is able to see it. */
497 if( bPrivate && !g.perm.ModForum && !bSameUser ){
498 @ <p><span class="modpending">Awaiting Moderator Approval</span></p>
499 }else{
500 if( bRaw || bUnf || p->pEditTail ){
501 zMimetype = "text/plain";
502 }else{
503 zMimetype = pManifest->zMimetype;
504 }
505 forum_render(0, zMimetype, pManifest->zWiki, 0, !bRaw);
506 }
507
508 /* When not in raw mode, finish creating the border around the post. */
509 if( !bRaw ){
510 /* If the user is able to write to the forum and if this post has not been
511 ** edited, create a form with various interaction buttons. */
512 if( g.perm.WrForum && !p->pEditTail ){
513 @ <div><form action="%R/forumedit" method="POST">
514 @ <input type="hidden" name="fpid" value="%s(p->zUuid)">
515 if( !bPrivate ){
516 /* Reply and Edit are only available if the post has been approved. */
 
517 @ <input type="submit" name="reply" value="Reply">
518 if( g.perm.Admin || bSameUser ){
519 @ <input type="submit" name="edit" value="Edit">
520 @ <input type="submit" name="nullout" value="Delete">
521 }
522 }else if( g.perm.ModForum ){
523 /* Allow moderators to approve or reject pending posts. Also allow
524 ** forum supervisors to mark non-special users as trusted and therefore
525 ** able to post unmoderated. */
526 @ <input type="submit" name="approve" value="Approve">
527 @ <input type="submit" name="reject" value="Reject">
528 if( g.perm.AdminForum && !login_is_special(pManifest->zUser) ){
529 @ <br><label><input type="checkbox" name="trust">
530 @ Trust user "%h(pManifest->zUser)" so that future posts by \
531 @ "%h(pManifest->zUser)" do not require moderation.
532 @ </label>
533 @ <input type="hidden" name="trustuser" value="%h(pManifest->zUser)">
534 }
535 }else if( bSameUser ){
536 /* Allow users to delete (reject) their own pending posts. */
537 @ <input type="submit" name="reject" value="Delete">
538 }
539 @ </form></div>
540 }
 
541 @ </div>
542 }
543
544 /* Clean up. */
545 manifest_destroy(pManifest);
546 }
547
548 /*
549 ** Possible display modes for forum_display_thread().
550 */
551 enum {
552 FD_RAW, /* Like FD_SINGLE, but additionally omit the border, force
553 ** unformatted mode, and inhibit history mode */
554 FD_SINGLE, /* Render a single post and (optionally) its edit history */
555 FD_CHRONO, /* Render all posts in chronological order */
556 FD_HIER, /* Render all posts in an indented hierarchy */
557 };
558
559 /*
560 ** Display a forum thread. If mode is FD_RAW or FD_SINGLE, display only a
561 ** single post from the thread and (optionally) its edit history.
562 */
563 static void forum_display_thread(
564 int froot, /* Forum thread root post ID */
565 int fpid, /* Selected forum post ID, or 0 if none selected */
566 int mode, /* Forum display mode, one of the FD_* enumerations */
567 int bUnf, /* True if rendering unformatted */
568 int bHist /* True if showing edit history, ignored for FD_RAW */
569 ){
570 ForumThread *pThread; /* Thread structure */
571 ForumPost *pSelect; /* Currently selected post, or NULL if none */
572 ForumPost *p; /* Post iterator pointer */
573 char *zQuery; /* Common query string */
574 int iIndentScale = 4; /* Indent scale factor, measured in "ex" units */
575 int sid; /* Comparison serial ID */
576
577 /* In raw mode, force unformatted display and disable history. */
578 if( mode == FD_RAW ){
579 bUnf = 1;
580 bHist = 0;
581 }
582
583 /* Thread together the posts and (optionally) compute the hierarchy. */
584 pThread = forumthread_create(froot, mode==FD_HIER);
585
586 /* Compute the appropriate indent scaling. */
587 if( mode==FD_HIER ){
588 iIndentScale = 4;
589 while( iIndentScale>1 && iIndentScale*pThread->mxIndent>25 ){
590 iIndentScale--;
591 }
592 }else{
593 iIndentScale = 0;
594 }
595
596 /* Find the selected post, or (depending on parameters) its latest edit. */
597 pSelect = fpid ? forumpost_forward(pThread->pFirst, fpid) : 0;
598 if( !bHist && mode!=FD_RAW && pSelect && pSelect->pEditTail ){
599 pSelect = pSelect->pEditTail;
600 }
601
602 /* When displaying only a single post, abort if no post was selected or the
603 ** selected forum post does not exist in the thread. Otherwise proceed to
604 ** display the entire thread without marking any posts as selected. */
605 if( !pSelect && (mode==FD_RAW || mode==FD_SINGLE) ){
606 return;
607 }
608
609 /* Create the common query string to append to nearly all post links. */
610 zQuery = mode==FD_RAW ? 0 : mprintf("t=%c%s%s",
611 mode==FD_SINGLE ? 's' : mode==FD_CHRONO ? 'c' : 'h',
612 bUnf ? "&unf" : "", bHist ? "&hist" : "");
613
614 /* Identify which post to display first. If history is shown, start with the
615 ** original, unedited post. Otherwise advance to the post's latest edit. */
616 if( mode==FD_RAW || mode==FD_SINGLE ){
617 p = pSelect;
618 if( bHist && p->pEditHead ) p = p->pEditHead;
619 }else{
620 p = mode==FD_CHRONO ? pThread->pFirst : pThread->pDisplay;
621 if( !bHist && p->pEditTail ) p = p->pEditTail;
622 }
623
624 /* Display the appropriate subset of posts in sequence. */
625 while( p ){
626 /* Display the post. */
627 forum_display_post(p, iIndentScale, mode==FD_RAW,
628 bUnf, bHist, p==pSelect, zQuery);
629
630 /* Advance to the next post in the thread. */
631 if( mode==FD_CHRONO ){
632 /* Chronological mode: display posts (optionally including edits) in their
633 ** original commit order. */
634 if( bHist ){
635 p = p->pNext;
636 }else{
637 sid = p->sid;
638 if( p->pEditHead ) p = p->pEditHead;
639 do p = p->pNext; while( p && p->sid<=sid );
640 if( p && p->pEditTail ) p = p->pEditTail;
641 }
642 }else if( bHist && p->pEditNext ){
643 /* Hierarchical and single mode: display each post's edits in sequence. */
644 p = p->pEditNext;
645 }else if( mode==FD_HIER ){
646 /* Hierarchical mode: after displaying with each post (optionally
647 ** including edits), go to the next post in computed display order. */
648 p = p->pEditHead ? p->pEditHead->pDisplay : p->pDisplay;
649 if( !bHist && p && p->pEditTail ) p = p->pEditTail;
650 }else{
651 /* Single and raw mode: terminate after displaying the selected post and
652 ** (optionally) its edits. */
653 break;
654 }
655 }
656
657 /* Undocumented "threadtable" query parameter causes thread table to be
658 ** displayed for debugging purposes. */
659 if( PB("threadtable") ){
660 @ <hr>
661 @ <table border="1" cellpadding="3" cellspacing="0">
662 @ <tr><th>sid<th>rev<th>fpid<th>pIrt<th>pEditHead<th>pEditTail\
663 @ <th>pEditNext<th>pEditPrev<th>pDisplay<th>hash
664 for(p=pThread->pFirst; p; p=p->pNext){
665 @ <tr><td>%d(p->sid)<td>%d(p->rev)<td>%d(p->fpid)\
666 @ <td>%d(p->pIrt ? p->pIrt->fpid : 0)\
667 @ <td>%d(p->pEditHead ? p->pEditHead->fpid : 0)\
668 @ <td>%d(p->pEditTail ? p->pEditTail->fpid : 0)\
669 @ <td>%d(p->pEditNext ? p->pEditNext->fpid : 0)\
670 @ <td>%d(p->pEditPrev ? p->pEditPrev->fpid : 0)\
671 @ <td>%d(p->pDisplay ? p->pDisplay->fpid : 0)\
672 @ <td>%S(p->zUuid)</tr>
673 }
674 @ </table>
675 }
676
677 /* Clean up. */
678 forumthread_delete(pThread);
679 fossil_free(zQuery);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
680 }
681
682 /*
683 ** WEBPAGE: forumpost
684 **
685 ** Show a single forum posting. The posting is shown in context with
686 ** its entire thread. The selected posting is enclosed within
687 ** <div class='forumSel'>...</div>. Javascript is used to move the
688 ** selected posting into view after the page loads.
689 **
690 ** Query parameters:
691 **
692 ** name=X REQUIRED. The hash of the post to display.
693 ** t=a Automatic display mode, i.e. hierarchical for
694 ** desktop and chronological for mobile. This is the
695 ** default if the "t" query parameter is omitted.
696 ** t=c Show posts in the order they were written.
697 ** t=h Show posts usin hierarchical indenting.
698 ** t=s Show only the post specified by "name=X".
699 ** t=r Alias for "t=c&unf&hist".
700 ** t=y Alias for "t=s&unf&hist".
701 ** raw Alias for "t=s&unf". Additionally, omit the border
702 ** around the post, and ignore "t" and "hist".
703 ** unf Show the original, unformatted source text.
704 ** hist Show edit history in addition to current posts.
705 */
706 void forumpost_page(void){
707 forumthread_page();
708 }
709
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
710 /*
711 ** WEBPAGE: forumthread
712 **
713 ** Show all forum messages associated with a particular message thread.
714 ** The result is basically the same as /forumpost except that none of
@@ -816,24 +715,28 @@
715 ** the postings in the thread are selected.
716 **
717 ** Query parameters:
718 **
719 ** name=X REQUIRED. The hash of any post of the thread.
720 ** t=a Automatic display mode, i.e. hierarchical for
721 ** desktop and chronological for mobile. This is the
722 ** default if the "t" query parameter is omitted.
723 ** t=c Show posts in the order they were written.
724 ** t=h Show posts using hierarchical indenting.
725 ** unf Show the original, unformatted source text.
726 ** hist Show edit history in addition to current posts.
727 */
728 void forumthread_page(void){
729 int fpid;
730 int froot;
731 char *zThreadTitle;
732 const char *zName = P("name");
733 const char *zMode = PD("t","a");
734 int bRaw = PB("raw");
735 int bUnf = PB("unf");
736 int bHist = PB("hist");
737 int mode;
738 login_check_credentials();
739 if( !g.perm.RdForum ){
740 login_needed(g.anon.RdForum);
741 return;
742 }
@@ -847,54 +750,70 @@
750 froot = db_int(0, "SELECT froot FROM forumpost WHERE fpid=%d", fpid);
751 if( froot==0 ){
752 webpage_error("Not a forum post: \"%s\"", zName);
753 }
754 if( fossil_strcmp(g.zPath,"forumthread")==0 ) fpid = 0;
755
756 /* Decode the mode parameters. */
757 if( bRaw ){
758 mode = FD_RAW;
759 bUnf = 1;
760 bHist = 0;
761 cgi_replace_query_parameter("unf", "on");
762 cgi_delete_query_parameter("hist");
763 cgi_delete_query_parameter("raw");
764 }else{
765 switch( *zMode ){
766 case 'a': mode = cgi_from_mobile() ? FD_CHRONO : FD_HIER; break;
767 case 'c': mode = FD_CHRONO; break;
768 case 'h': mode = FD_HIER; break;
769 case 's': mode = FD_SINGLE; break;
770 case 'r': mode = FD_CHRONO; break;
771 case 'y': mode = FD_SINGLE; break;
772 default: webpage_error("Invalid thread mode: \"%s\"", zMode);
773 }
774 if( *zMode=='r' || *zMode=='y') {
775 bUnf = 1;
776 bHist = 1;
777 cgi_replace_query_parameter("t", mode==FD_CHRONO ? "c" : "s");
778 cgi_replace_query_parameter("unf", "on");
779 cgi_replace_query_parameter("hist", "on");
780 }
781 }
782
783 /* Define the page header. */
784 zThreadTitle = db_text("",
785 "SELECT"
786 " substr(event.comment,instr(event.comment,':')+2)"
787 " FROM forumpost, event"
788 " WHERE event.objid=forumpost.fpid"
789 " AND forumpost.fpid=%d;",
790 fpid
791 );
792 style_header("%s%s", zThreadTitle, *zThreadTitle ? "" : "Forum");
793 fossil_free(zThreadTitle);
794 if( mode!=FD_CHRONO ){
795 style_submenu_element("Chronological", "%R/%s/%s?t=c%s%s", g.zPath, zName,
796 bUnf ? "&unf" : "", bHist ? "&hist" : "");
797 }
798 if( mode!=FD_HIER ){
799 style_submenu_element("Hierarchical", "%R/%s/%s?t=h%s%s", g.zPath, zName,
800 bUnf ? "&unf" : "", bHist ? "&hist" : "");
801 }
802 style_submenu_checkbox("unf", "Unformatted", 0, 0);
803 style_submenu_checkbox("hist", "History", 0, 0);
804
805 /* Display the thread. */
806 forum_display_thread(froot, fpid, mode, bUnf, bHist);
807
808 /* Emit Forum Javascript. */
809 style_emit_script_fossil_bootstrap(1);
810 builtin_request_js("forum.js");
811 builtin_request_js("fossil.dom.js");
812 builtin_request_js("fossil.page.forumpost.js");
813
814 /* Emit the page style. */
815 style_footer();
816 }
817
818 /*
819 ** Return true if a forum post should be moderated.
@@ -949,11 +868,11 @@
868 webpage_assert( (zTitle==0)+(iInReplyTo==0)==1 );
869 blob_init(&x, 0, 0);
870 zDate = date_in_standard_format("now");
871 blob_appendf(&x, "D %s\n", zDate);
872 fossil_free(zDate);
873 zG = db_text(0,
874 "SELECT uuid FROM blob, forumpost"
875 " WHERE blob.rid==forumpost.froot"
876 " AND forumpost.fpid=%d", iBasis);
877 if( zG ){
878 blob_appendf(&x, "G %s\n", zG);
@@ -1017,11 +936,11 @@
936 }
937
938 /*
939 ** Paint the form elements for entering a Forum post
940 */
941 static void forum_post_widget(
942 const char *zTitle,
943 const char *zMimetype,
944 const char *zContent
945 ){
946 if( zTitle ){
@@ -1126,11 +1045,11 @@
1045 }
1046 style_header("New Forum Thread");
1047 @ <form action="%R/forume1" method="POST">
1048 @ <h1>New Thread:</h1>
1049 forum_from_line();
1050 forum_post_widget(zTitle, zMimetype, zContent);
1051 @ <input type="submit" name="preview" value="Preview">
1052 if( P("preview") && !whitespace_only(zContent) ){
1053 @ <input type="submit" name="submit" value="Submit">
1054 }else{
1055 @ <input type="submit" name="submit" value="Submit" disabled>
@@ -1203,11 +1122,11 @@
1122 }
1123 cgi_redirectf("%R/forumpost/%S",P("fpid"));
1124 return;
1125 }
1126 if( P("reject") ){
1127 char *zParent =
1128 db_text(0,
1129 "SELECT uuid FROM forumpost, blob"
1130 " WHERE forumpost.fpid=%d AND blob.rid=forumpost.firt",
1131 fpid
1132 );
@@ -1276,11 +1195,11 @@
1195 @ <h2>Revised Message:</h2>
1196 @ <form action="%R/forume2" method="POST">
1197 @ <input type="hidden" name="fpid" value="%h(P("fpid"))">
1198 @ <input type="hidden" name="edit" value="1">
1199 forum_from_line();
1200 forum_post_widget(zTitle, zMimetype, zContent);
1201 }else{
1202 /* Reply */
1203 char *zDisplayName;
1204 zMimetype = PD("mimetype",DEFAULT_FORUM_MIMETYPE);
1205 zContent = PDT("content","");
@@ -1302,11 +1221,11 @@
1221 @ <h2>Enter Reply:</h2>
1222 @ <form action="%R/forume2" method="POST">
1223 @ <input type="hidden" name="fpid" value="%h(P("fpid"))">
1224 @ <input type="hidden" name="reply" value="1">
1225 forum_from_line();
1226 forum_post_widget(0, zMimetype, zContent);
1227 }
1228 if( !isDelete ){
1229 @ <input type="submit" name="preview" value="Preview">
1230 }
1231 @ <input type="submit" name="cancel" value="Cancel">
1232
+29 -18
--- src/translate.c
+++ src/translate.c
@@ -155,19 +155,24 @@
155155
fprintf(out,"\n");
156156
}else{
157157
fprintf(out,"%*s\"%s%s\"\n",indent, "", zOut, zNewline);
158158
}
159159
}else{
160
- /* Otherwise (if the last non-whitespace was not '=') then generate
161
- ** a cgi_printf() statement whose format is the text following the '@'.
162
- ** Substrings of the form "%C(...)" (where C is any sequence of
163
- ** characters other than \000 and '(') will put "%C" in the
164
- ** format and add the "(...)" as an argument to the cgi_printf call.
160
+ /* Otherwise (if the last non-whitespace was not '=') then generate a
161
+ ** cgi_printf() statement whose format is the text following the '@'.
162
+ ** Substrings of the form "%C(...)" (where C is any sequence of characters
163
+ ** other than \000 and '(') will put "%C" in the format and add the
164
+ ** "(...)" as an argument to the cgi_printf call. Each '*' character
165
+ ** present in C (max two) causes one more "(...)" sequence to be consumed.
166
+ ** For example, "%*.*d(4)(2)(1)" converts to "%*.*d" with arguments "4",
167
+ ** "2", and "1", which will be used as the field width, precision, and
168
+ ** value, respectively, producing a final formatted result of " 01".
165169
*/
166170
const char *zNewline = "\\n";
167171
int indent;
168172
int nC;
173
+ int nParam;
169174
char c;
170175
i++;
171176
if( isspace(zLine[i]) ){ i++; }
172177
indent = i;
173178
for(j=0; zLine[i] && zLine[i]!='\r' && zLine[i]!='\n'; i++){
@@ -177,25 +182,31 @@
177182
break;
178183
}
179184
if( zLine[i]=='"' || zLine[i]=='\\' ){ zOut[j++] = '\\'; }
180185
zOut[j++] = zLine[i];
181186
if( zLine[i]!='%' || zLine[i+1]=='%' || zLine[i+1]==0 ) continue;
182
- for(nC=1; zLine[i+nC] && zLine[i+nC]!='('; nC++){}
187
+ nParam=1;
188
+ for(nC=1; zLine[i+nC] && zLine[i+nC]!='('; nC++){
189
+ if( zLine[i+nC]=='*' && nParam < 3 ) nParam++;
190
+ }
183191
if( zLine[i+nC]!='(' || !isalpha(zLine[i+nC-1]) ) continue;
184192
while( --nC ) zOut[j++] = zLine[++i];
185
- zArg[nArg++] = ',';
186
- k = 0; i++;
187
- while( (c = zLine[i])!=0 ){
188
- zArg[nArg++] = c;
189
- if( c==')' ){
190
- k--;
191
- if( k==0 ) break;
192
- }else if( c=='(' ){
193
- k++;
194
- }
195
- i++;
196
- }
193
+ do{
194
+ zArg[nArg++] = ',';
195
+ k = 0; i++;
196
+ if( zLine[i]!='(' ) break;
197
+ while( (c = zLine[i])!=0 ){
198
+ zArg[nArg++] = c;
199
+ if( c==')' ){
200
+ k--;
201
+ if( k==0 ) break;
202
+ }else if( c=='(' ){
203
+ k++;
204
+ }
205
+ i++;
206
+ }
207
+ }while( --nParam );
197208
}
198209
zOut[j] = 0;
199210
if( !inPrint ){
200211
fprintf(out,"%*scgi_printf(\"%s%s\"",indent-2,"", zOut, zNewline);
201212
inPrint = 1;
202213
--- src/translate.c
+++ src/translate.c
@@ -155,19 +155,24 @@
155 fprintf(out,"\n");
156 }else{
157 fprintf(out,"%*s\"%s%s\"\n",indent, "", zOut, zNewline);
158 }
159 }else{
160 /* Otherwise (if the last non-whitespace was not '=') then generate
161 ** a cgi_printf() statement whose format is the text following the '@'.
162 ** Substrings of the form "%C(...)" (where C is any sequence of
163 ** characters other than \000 and '(') will put "%C" in the
164 ** format and add the "(...)" as an argument to the cgi_printf call.
 
 
 
 
165 */
166 const char *zNewline = "\\n";
167 int indent;
168 int nC;
 
169 char c;
170 i++;
171 if( isspace(zLine[i]) ){ i++; }
172 indent = i;
173 for(j=0; zLine[i] && zLine[i]!='\r' && zLine[i]!='\n'; i++){
@@ -177,25 +182,31 @@
177 break;
178 }
179 if( zLine[i]=='"' || zLine[i]=='\\' ){ zOut[j++] = '\\'; }
180 zOut[j++] = zLine[i];
181 if( zLine[i]!='%' || zLine[i+1]=='%' || zLine[i+1]==0 ) continue;
182 for(nC=1; zLine[i+nC] && zLine[i+nC]!='('; nC++){}
 
 
 
183 if( zLine[i+nC]!='(' || !isalpha(zLine[i+nC-1]) ) continue;
184 while( --nC ) zOut[j++] = zLine[++i];
185 zArg[nArg++] = ',';
186 k = 0; i++;
187 while( (c = zLine[i])!=0 ){
188 zArg[nArg++] = c;
189 if( c==')' ){
190 k--;
191 if( k==0 ) break;
192 }else if( c=='(' ){
193 k++;
194 }
195 i++;
196 }
 
 
 
197 }
198 zOut[j] = 0;
199 if( !inPrint ){
200 fprintf(out,"%*scgi_printf(\"%s%s\"",indent-2,"", zOut, zNewline);
201 inPrint = 1;
202
--- src/translate.c
+++ src/translate.c
@@ -155,19 +155,24 @@
155 fprintf(out,"\n");
156 }else{
157 fprintf(out,"%*s\"%s%s\"\n",indent, "", zOut, zNewline);
158 }
159 }else{
160 /* Otherwise (if the last non-whitespace was not '=') then generate a
161 ** cgi_printf() statement whose format is the text following the '@'.
162 ** Substrings of the form "%C(...)" (where C is any sequence of characters
163 ** other than \000 and '(') will put "%C" in the format and add the
164 ** "(...)" as an argument to the cgi_printf call. Each '*' character
165 ** present in C (max two) causes one more "(...)" sequence to be consumed.
166 ** For example, "%*.*d(4)(2)(1)" converts to "%*.*d" with arguments "4",
167 ** "2", and "1", which will be used as the field width, precision, and
168 ** value, respectively, producing a final formatted result of " 01".
169 */
170 const char *zNewline = "\\n";
171 int indent;
172 int nC;
173 int nParam;
174 char c;
175 i++;
176 if( isspace(zLine[i]) ){ i++; }
177 indent = i;
178 for(j=0; zLine[i] && zLine[i]!='\r' && zLine[i]!='\n'; i++){
@@ -177,25 +182,31 @@
182 break;
183 }
184 if( zLine[i]=='"' || zLine[i]=='\\' ){ zOut[j++] = '\\'; }
185 zOut[j++] = zLine[i];
186 if( zLine[i]!='%' || zLine[i+1]=='%' || zLine[i+1]==0 ) continue;
187 nParam=1;
188 for(nC=1; zLine[i+nC] && zLine[i+nC]!='('; nC++){
189 if( zLine[i+nC]=='*' && nParam < 3 ) nParam++;
190 }
191 if( zLine[i+nC]!='(' || !isalpha(zLine[i+nC-1]) ) continue;
192 while( --nC ) zOut[j++] = zLine[++i];
193 do{
194 zArg[nArg++] = ',';
195 k = 0; i++;
196 if( zLine[i]!='(' ) break;
197 while( (c = zLine[i])!=0 ){
198 zArg[nArg++] = c;
199 if( c==')' ){
200 k--;
201 if( k==0 ) break;
202 }else if( c=='(' ){
203 k++;
204 }
205 i++;
206 }
207 }while( --nParam );
208 }
209 zOut[j] = 0;
210 if( !inPrint ){
211 fprintf(out,"%*scgi_printf(\"%s%s\"",indent-2,"", zOut, zNewline);
212 inPrint = 1;
213
+11 -1
--- src/util.c
+++ src/util.c
@@ -691,11 +691,11 @@
691691
int i;
692692
char z[60];
693693
694694
/* Source characters for the password. Omit characters like "0", "O",
695695
** "1" and "I" that might be easily confused */
696
- static const char zAlphabet[] =
696
+ static const char zAlphabet[] =
697697
/* 0 1 2 3 4 5 */
698698
/* 123456789 123456789 123456789 123456789 123456789 123456 */
699699
"23456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ";
700700
701701
if( N<8 ) N = 8;
@@ -728,5 +728,15 @@
728728
if( g.argc>=3 ){
729729
N = atoi(g.argv[2]);
730730
}
731731
fossil_print("%s\n", fossil_random_password(N));
732732
}
733
+
734
+/*
735
+** Return the number of decimal digits in a nonnegative integer. This is useful
736
+** when formatting text.
737
+*/
738
+int fossil_num_digits(int n){
739
+ return n< 10 ? 1 : n< 100 ? 2 : n< 1000 ? 3
740
+ : n< 10000 ? 4 : n< 100000 ? 5 : n< 1000000 ? 6
741
+ : n<10000000 ? 7 : n<100000000 ? 8 : n<1000000000 ? 9 : 10;
742
+}
733743
--- src/util.c
+++ src/util.c
@@ -691,11 +691,11 @@
691 int i;
692 char z[60];
693
694 /* Source characters for the password. Omit characters like "0", "O",
695 ** "1" and "I" that might be easily confused */
696 static const char zAlphabet[] =
697 /* 0 1 2 3 4 5 */
698 /* 123456789 123456789 123456789 123456789 123456789 123456 */
699 "23456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ";
700
701 if( N<8 ) N = 8;
@@ -728,5 +728,15 @@
728 if( g.argc>=3 ){
729 N = atoi(g.argv[2]);
730 }
731 fossil_print("%s\n", fossil_random_password(N));
732 }
 
 
 
 
 
 
 
 
 
 
733
--- src/util.c
+++ src/util.c
@@ -691,11 +691,11 @@
691 int i;
692 char z[60];
693
694 /* Source characters for the password. Omit characters like "0", "O",
695 ** "1" and "I" that might be easily confused */
696 static const char zAlphabet[] =
697 /* 0 1 2 3 4 5 */
698 /* 123456789 123456789 123456789 123456789 123456789 123456 */
699 "23456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ";
700
701 if( N<8 ) N = 8;
@@ -728,5 +728,15 @@
728 if( g.argc>=3 ){
729 N = atoi(g.argv[2]);
730 }
731 fossil_print("%s\n", fossil_random_password(N));
732 }
733
734 /*
735 ** Return the number of decimal digits in a nonnegative integer. This is useful
736 ** when formatting text.
737 */
738 int fossil_num_digits(int n){
739 return n< 10 ? 1 : n< 100 ? 2 : n< 1000 ? 3
740 : n< 10000 ? 4 : n< 100000 ? 5 : n< 1000000 ? 6
741 : n<10000000 ? 7 : n<100000000 ? 8 : n<1000000000 ? 9 : 10;
742 }
743

Keyboard Shortcuts

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