Fossil SCM

Merge the latest enhancements from trunk.

drh 2020-08-22 15:35 sec2020 merge
Commit 11c1566a93f0ebc61f71e4347e8e2c31dfde1c02653314d4b43408eee1b76c8d
+449 -530
--- 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;
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;
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 ){
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>
@@ -1205,11 +1124,11 @@
12051124
}
12061125
cgi_redirectf("%R/forumpost/%S",P("fpid"));
12071126
return;
12081127
}
12091128
if( P("reject") ){
1210
- char *zParent =
1129
+ char *zParent =
12111130
db_text(0,
12121131
"SELECT uuid FROM forumpost, blob"
12131132
" WHERE forumpost.fpid=%d AND blob.rid=forumpost.firt",
12141133
fpid
12151134
);
@@ -1278,11 +1197,11 @@
12781197
@ <h2>Revised Message:</h2>
12791198
@ <form action="%R/forume2" method="POST">
12801199
@ <input type="hidden" name="fpid" value="%h(P("fpid"))">
12811200
@ <input type="hidden" name="edit" value="1">
12821201
forum_from_line();
1283
- forum_entry_widget(zTitle, zMimetype, zContent);
1202
+ forum_post_widget(zTitle, zMimetype, zContent);
12841203
}else{
12851204
/* Reply */
12861205
char *zDisplayName;
12871206
zMimetype = PD("mimetype",DEFAULT_FORUM_MIMETYPE);
12881207
zContent = PDT("content","");
@@ -1304,11 +1223,11 @@
13041223
@ <h2>Enter Reply:</h2>
13051224
@ <form action="%R/forume2" method="POST">
13061225
@ <input type="hidden" name="fpid" value="%h(P("fpid"))">
13071226
@ <input type="hidden" name="reply" value="1">
13081227
forum_from_line();
1309
- forum_entry_widget(0, zMimetype, zContent);
1228
+ forum_post_widget(0, zMimetype, zContent);
13101229
}
13111230
if( !isDelete ){
13121231
@ <input type="submit" name="preview" value="Preview">
13131232
}
13141233
@ <input type="submit" name="cancel" value="Cancel">
13151234
--- 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>
@@ -1205,11 +1124,11 @@
1205 }
1206 cgi_redirectf("%R/forumpost/%S",P("fpid"));
1207 return;
1208 }
1209 if( P("reject") ){
1210 char *zParent =
1211 db_text(0,
1212 "SELECT uuid FROM forumpost, blob"
1213 " WHERE forumpost.fpid=%d AND blob.rid=forumpost.firt",
1214 fpid
1215 );
@@ -1278,11 +1197,11 @@
1278 @ <h2>Revised Message:</h2>
1279 @ <form action="%R/forume2" method="POST">
1280 @ <input type="hidden" name="fpid" value="%h(P("fpid"))">
1281 @ <input type="hidden" name="edit" value="1">
1282 forum_from_line();
1283 forum_entry_widget(zTitle, zMimetype, zContent);
1284 }else{
1285 /* Reply */
1286 char *zDisplayName;
1287 zMimetype = PD("mimetype",DEFAULT_FORUM_MIMETYPE);
1288 zContent = PDT("content","");
@@ -1304,11 +1223,11 @@
1304 @ <h2>Enter Reply:</h2>
1305 @ <form action="%R/forume2" method="POST">
1306 @ <input type="hidden" name="fpid" value="%h(P("fpid"))">
1307 @ <input type="hidden" name="reply" value="1">
1308 forum_from_line();
1309 forum_entry_widget(0, zMimetype, zContent);
1310 }
1311 if( !isDelete ){
1312 @ <input type="submit" name="preview" value="Preview">
1313 }
1314 @ <input type="submit" name="cancel" value="Cancel">
1315
--- 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>
@@ -1205,11 +1124,11 @@
1124 }
1125 cgi_redirectf("%R/forumpost/%S",P("fpid"));
1126 return;
1127 }
1128 if( P("reject") ){
1129 char *zParent =
1130 db_text(0,
1131 "SELECT uuid FROM forumpost, blob"
1132 " WHERE forumpost.fpid=%d AND blob.rid=forumpost.firt",
1133 fpid
1134 );
@@ -1278,11 +1197,11 @@
1197 @ <h2>Revised Message:</h2>
1198 @ <form action="%R/forume2" method="POST">
1199 @ <input type="hidden" name="fpid" value="%h(P("fpid"))">
1200 @ <input type="hidden" name="edit" value="1">
1201 forum_from_line();
1202 forum_post_widget(zTitle, zMimetype, zContent);
1203 }else{
1204 /* Reply */
1205 char *zDisplayName;
1206 zMimetype = PD("mimetype",DEFAULT_FORUM_MIMETYPE);
1207 zContent = PDT("content","");
@@ -1304,11 +1223,11 @@
1223 @ <h2>Enter Reply:</h2>
1224 @ <form action="%R/forume2" method="POST">
1225 @ <input type="hidden" name="fpid" value="%h(P("fpid"))">
1226 @ <input type="hidden" name="reply" value="1">
1227 forum_from_line();
1228 forum_post_widget(0, zMimetype, zContent);
1229 }
1230 if( !isDelete ){
1231 @ <input type="submit" name="preview" value="Preview">
1232 }
1233 @ <input type="submit" name="cancel" value="Cancel">
1234
+449 -530
--- 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;
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;
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 ){
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>
@@ -1205,11 +1124,11 @@
12051124
}
12061125
cgi_redirectf("%R/forumpost/%S",P("fpid"));
12071126
return;
12081127
}
12091128
if( P("reject") ){
1210
- char *zParent =
1129
+ char *zParent =
12111130
db_text(0,
12121131
"SELECT uuid FROM forumpost, blob"
12131132
" WHERE forumpost.fpid=%d AND blob.rid=forumpost.firt",
12141133
fpid
12151134
);
@@ -1278,11 +1197,11 @@
12781197
@ <h2>Revised Message:</h2>
12791198
@ <form action="%R/forume2" method="POST">
12801199
@ <input type="hidden" name="fpid" value="%h(P("fpid"))">
12811200
@ <input type="hidden" name="edit" value="1">
12821201
forum_from_line();
1283
- forum_entry_widget(zTitle, zMimetype, zContent);
1202
+ forum_post_widget(zTitle, zMimetype, zContent);
12841203
}else{
12851204
/* Reply */
12861205
char *zDisplayName;
12871206
zMimetype = PD("mimetype",DEFAULT_FORUM_MIMETYPE);
12881207
zContent = PDT("content","");
@@ -1304,11 +1223,11 @@
13041223
@ <h2>Enter Reply:</h2>
13051224
@ <form action="%R/forume2" method="POST">
13061225
@ <input type="hidden" name="fpid" value="%h(P("fpid"))">
13071226
@ <input type="hidden" name="reply" value="1">
13081227
forum_from_line();
1309
- forum_entry_widget(0, zMimetype, zContent);
1228
+ forum_post_widget(0, zMimetype, zContent);
13101229
}
13111230
if( !isDelete ){
13121231
@ <input type="submit" name="preview" value="Preview">
13131232
}
13141233
@ <input type="submit" name="cancel" value="Cancel">
13151234
--- 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>
@@ -1205,11 +1124,11 @@
1205 }
1206 cgi_redirectf("%R/forumpost/%S",P("fpid"));
1207 return;
1208 }
1209 if( P("reject") ){
1210 char *zParent =
1211 db_text(0,
1212 "SELECT uuid FROM forumpost, blob"
1213 " WHERE forumpost.fpid=%d AND blob.rid=forumpost.firt",
1214 fpid
1215 );
@@ -1278,11 +1197,11 @@
1278 @ <h2>Revised Message:</h2>
1279 @ <form action="%R/forume2" method="POST">
1280 @ <input type="hidden" name="fpid" value="%h(P("fpid"))">
1281 @ <input type="hidden" name="edit" value="1">
1282 forum_from_line();
1283 forum_entry_widget(zTitle, zMimetype, zContent);
1284 }else{
1285 /* Reply */
1286 char *zDisplayName;
1287 zMimetype = PD("mimetype",DEFAULT_FORUM_MIMETYPE);
1288 zContent = PDT("content","");
@@ -1304,11 +1223,11 @@
1304 @ <h2>Enter Reply:</h2>
1305 @ <form action="%R/forume2" method="POST">
1306 @ <input type="hidden" name="fpid" value="%h(P("fpid"))">
1307 @ <input type="hidden" name="reply" value="1">
1308 forum_from_line();
1309 forum_entry_widget(0, zMimetype, zContent);
1310 }
1311 if( !isDelete ){
1312 @ <input type="submit" name="preview" value="Preview">
1313 }
1314 @ <input type="submit" name="cancel" value="Cancel">
1315
--- 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>
@@ -1205,11 +1124,11 @@
1124 }
1125 cgi_redirectf("%R/forumpost/%S",P("fpid"));
1126 return;
1127 }
1128 if( P("reject") ){
1129 char *zParent =
1130 db_text(0,
1131 "SELECT uuid FROM forumpost, blob"
1132 " WHERE forumpost.fpid=%d AND blob.rid=forumpost.firt",
1133 fpid
1134 );
@@ -1278,11 +1197,11 @@
1197 @ <h2>Revised Message:</h2>
1198 @ <form action="%R/forume2" method="POST">
1199 @ <input type="hidden" name="fpid" value="%h(P("fpid"))">
1200 @ <input type="hidden" name="edit" value="1">
1201 forum_from_line();
1202 forum_post_widget(zTitle, zMimetype, zContent);
1203 }else{
1204 /* Reply */
1205 char *zDisplayName;
1206 zMimetype = PD("mimetype",DEFAULT_FORUM_MIMETYPE);
1207 zContent = PDT("content","");
@@ -1304,11 +1223,11 @@
1223 @ <h2>Enter Reply:</h2>
1224 @ <form action="%R/forume2" method="POST">
1225 @ <input type="hidden" name="fpid" value="%h(P("fpid"))">
1226 @ <input type="hidden" name="reply" value="1">
1227 forum_from_line();
1228 forum_post_widget(0, zMimetype, zContent);
1229 }
1230 if( !isDelete ){
1231 @ <input type="submit" name="preview" value="Preview">
1232 }
1233 @ <input type="submit" name="cancel" value="Cancel">
1234
--- src/moderate.c
+++ src/moderate.c
@@ -159,10 +159,11 @@
159159
rid, rid, rid
160160
);
161161
db_multi_exec("DELETE FROM modreq WHERE objid=%d", rid);
162162
admin_log("Approved moderation of rid %c-%d.", class, rid);
163163
if( class!='a' ) search_doc_touch(class, rid, 0);
164
+ setup_incr_cfgcnt();
164165
db_end_transaction(0);
165166
}
166167
167168
/*
168169
** WEBPAGE: modreq
@@ -221,7 +222,8 @@
221222
while( db_step(&q)==SQLITE_ROW ){
222223
int const objid = db_column_int(&q, 0);
223224
moderation_disapprove(objid);
224225
}
225226
db_finalize(&q);
227
+ setup_incr_cfgcnt();
226228
db_end_transaction(0);
227229
}
228230
--- src/moderate.c
+++ src/moderate.c
@@ -159,10 +159,11 @@
159 rid, rid, rid
160 );
161 db_multi_exec("DELETE FROM modreq WHERE objid=%d", rid);
162 admin_log("Approved moderation of rid %c-%d.", class, rid);
163 if( class!='a' ) search_doc_touch(class, rid, 0);
 
164 db_end_transaction(0);
165 }
166
167 /*
168 ** WEBPAGE: modreq
@@ -221,7 +222,8 @@
221 while( db_step(&q)==SQLITE_ROW ){
222 int const objid = db_column_int(&q, 0);
223 moderation_disapprove(objid);
224 }
225 db_finalize(&q);
 
226 db_end_transaction(0);
227 }
228
--- src/moderate.c
+++ src/moderate.c
@@ -159,10 +159,11 @@
159 rid, rid, rid
160 );
161 db_multi_exec("DELETE FROM modreq WHERE objid=%d", rid);
162 admin_log("Approved moderation of rid %c-%d.", class, rid);
163 if( class!='a' ) search_doc_touch(class, rid, 0);
164 setup_incr_cfgcnt();
165 db_end_transaction(0);
166 }
167
168 /*
169 ** WEBPAGE: modreq
@@ -221,7 +222,8 @@
222 while( db_step(&q)==SQLITE_ROW ){
223 int const objid = db_column_int(&q, 0);
224 moderation_disapprove(objid);
225 }
226 db_finalize(&q);
227 setup_incr_cfgcnt();
228 db_end_transaction(0);
229 }
230
--- src/sounds/README.md
+++ src/sounds/README.md
@@ -3,11 +3,11 @@
33
hexadecimal digits (as is the case for captchas generated by the
44
[../captcha.c module](../captcha.c)) then these WAV files can be
55
concatenated together to generate an audio reading of the captcha, which
66
enables visually impaired users to complete the captcha.
77
8
-Each of the WAV files uses 8000 samples per second, 8 bytes per sample
8
+Each of the WAV files uses 8000 samples per second, 8 bits per sample
99
and are 6000 samples in length.
1010
1111
The recordings are made by Philip Bennefall and are of his own voice.
1212
Mr. Bennefall is himself blind and uses this system implemented with these
1313
recordings to complete captchas for Fossil.
1414
--- src/sounds/README.md
+++ src/sounds/README.md
@@ -3,11 +3,11 @@
3 hexadecimal digits (as is the case for captchas generated by the
4 [../captcha.c module](../captcha.c)) then these WAV files can be
5 concatenated together to generate an audio reading of the captcha, which
6 enables visually impaired users to complete the captcha.
7
8 Each of the WAV files uses 8000 samples per second, 8 bytes per sample
9 and are 6000 samples in length.
10
11 The recordings are made by Philip Bennefall and are of his own voice.
12 Mr. Bennefall is himself blind and uses this system implemented with these
13 recordings to complete captchas for Fossil.
14
--- src/sounds/README.md
+++ src/sounds/README.md
@@ -3,11 +3,11 @@
3 hexadecimal digits (as is the case for captchas generated by the
4 [../captcha.c module](../captcha.c)) then these WAV files can be
5 concatenated together to generate an audio reading of the captcha, which
6 enables visually impaired users to complete the captcha.
7
8 Each of the WAV files uses 8000 samples per second, 8 bits per sample
9 and are 6000 samples in length.
10
11 The recordings are made by Philip Bennefall and are of his own voice.
12 Mr. Bennefall is himself blind and uses this system implemented with these
13 recordings to complete captchas for Fossil.
14
+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
--- win/Makefile.mingw.mistachkin
+++ win/Makefile.mingw.mistachkin
@@ -478,10 +478,11 @@
478478
$(SRCDIR)/fuzz.c \
479479
$(SRCDIR)/glob.c \
480480
$(SRCDIR)/graph.c \
481481
$(SRCDIR)/gzip.c \
482482
$(SRCDIR)/hname.c \
483
+ $(SRCDIR)/hook.c \
483484
$(SRCDIR)/http.c \
484485
$(SRCDIR)/http_socket.c \
485486
$(SRCDIR)/http_ssl.c \
486487
$(SRCDIR)/http_transport.c \
487488
$(SRCDIR)/import.c \
@@ -565,11 +566,10 @@
565566
$(SRCDIR)/webmail.c \
566567
$(SRCDIR)/wiki.c \
567568
$(SRCDIR)/wikiformat.c \
568569
$(SRCDIR)/winfile.c \
569570
$(SRCDIR)/winhttp.c \
570
- $(SRCDIR)/wysiwyg.c \
571571
$(SRCDIR)/xfer.c \
572572
$(SRCDIR)/xfersetup.c \
573573
$(SRCDIR)/zip.c
574574
575575
EXTRA_FILES = \
@@ -635,13 +635,18 @@
635635
$(SRCDIR)/default.css \
636636
$(SRCDIR)/diff.tcl \
637637
$(SRCDIR)/forum.js \
638638
$(SRCDIR)/fossil.bootstrap.js \
639639
$(SRCDIR)/fossil.confirmer.js \
640
+ $(SRCDIR)/fossil.copybutton.js \
640641
$(SRCDIR)/fossil.dom.js \
641642
$(SRCDIR)/fossil.fetch.js \
643
+ $(SRCDIR)/fossil.numbered-lines.js \
642644
$(SRCDIR)/fossil.page.fileedit.js \
645
+ $(SRCDIR)/fossil.page.forumpost.js \
646
+ $(SRCDIR)/fossil.page.wikiedit.js \
647
+ $(SRCDIR)/fossil.popupwidget.js \
643648
$(SRCDIR)/fossil.storage.js \
644649
$(SRCDIR)/fossil.tabs.js \
645650
$(SRCDIR)/graph.js \
646651
$(SRCDIR)/href.js \
647652
$(SRCDIR)/login.js \
@@ -667,10 +672,11 @@
667672
$(SRCDIR)/sounds/d.wav \
668673
$(SRCDIR)/sounds/e.wav \
669674
$(SRCDIR)/sounds/f.wav \
670675
$(SRCDIR)/style.admin_log.css \
671676
$(SRCDIR)/style.fileedit.css \
677
+ $(SRCDIR)/style.wikiedit.css \
672678
$(SRCDIR)/tree.js \
673679
$(SRCDIR)/useredit.js \
674680
$(SRCDIR)/wiki.wiki
675681
676682
TRANS_SRC = \
@@ -724,10 +730,11 @@
724730
$(OBJDIR)/fuzz_.c \
725731
$(OBJDIR)/glob_.c \
726732
$(OBJDIR)/graph_.c \
727733
$(OBJDIR)/gzip_.c \
728734
$(OBJDIR)/hname_.c \
735
+ $(OBJDIR)/hook_.c \
729736
$(OBJDIR)/http_.c \
730737
$(OBJDIR)/http_socket_.c \
731738
$(OBJDIR)/http_ssl_.c \
732739
$(OBJDIR)/http_transport_.c \
733740
$(OBJDIR)/import_.c \
@@ -811,11 +818,10 @@
811818
$(OBJDIR)/webmail_.c \
812819
$(OBJDIR)/wiki_.c \
813820
$(OBJDIR)/wikiformat_.c \
814821
$(OBJDIR)/winfile_.c \
815822
$(OBJDIR)/winhttp_.c \
816
- $(OBJDIR)/wysiwyg_.c \
817823
$(OBJDIR)/xfer_.c \
818824
$(OBJDIR)/xfersetup_.c \
819825
$(OBJDIR)/zip_.c
820826
821827
OBJ = \
@@ -869,10 +875,11 @@
869875
$(OBJDIR)/fuzz.o \
870876
$(OBJDIR)/glob.o \
871877
$(OBJDIR)/graph.o \
872878
$(OBJDIR)/gzip.o \
873879
$(OBJDIR)/hname.o \
880
+ $(OBJDIR)/hook.o \
874881
$(OBJDIR)/http.o \
875882
$(OBJDIR)/http_socket.o \
876883
$(OBJDIR)/http_ssl.o \
877884
$(OBJDIR)/http_transport.o \
878885
$(OBJDIR)/import.o \
@@ -956,11 +963,10 @@
956963
$(OBJDIR)/webmail.o \
957964
$(OBJDIR)/wiki.o \
958965
$(OBJDIR)/wikiformat.o \
959966
$(OBJDIR)/winfile.o \
960967
$(OBJDIR)/winhttp.o \
961
- $(OBJDIR)/wysiwyg.o \
962968
$(OBJDIR)/xfer.o \
963969
$(OBJDIR)/xfersetup.o \
964970
$(OBJDIR)/zip.o
965971
966972
APPNAME = fossil.exe
@@ -1057,13 +1063,16 @@
10571063
# build is done from, i.e. the checkout belongs to. Do not sync/push
10581064
# the repository after running the tests.
10591065
test: $(OBJDIR) $(APPNAME)
10601066
$(TCLSH) $(SRCDIR)/../test/tester.tcl $(APPNAME)
10611067
1062
-$(OBJDIR)/VERSION.h: $(SRCDIR)/../manifest.uuid $(SRCDIR)/../manifest $(MKVERSION)
1068
+$(OBJDIR)/VERSION.h: $(SRCDIR)/../manifest.uuid $(SRCDIR)/../manifest $(MKVERSION) $(OBJDIR)/phony.h
10631069
$(MKVERSION) $(SRCDIR)/../manifest.uuid $(SRCDIR)/../manifest $(SRCDIR)/../VERSION >$@
10641070
1071
+$(OBJDIR)/phony.h:
1072
+ # Force rebuild of VERSION.h every time "make" is run
1073
+
10651074
# The USE_SYSTEM_SQLITE variable may be undefined, set to 0, or set
10661075
# to 1. If it is set to 1, then there is no need to build or link
10671076
# the sqlite3.o object. Instead, the system SQLite will be linked
10681077
# using -lsqlite3.
10691078
SQLITE3_OBJ.0 = $(OBJDIR)/sqlite3.o
@@ -1226,10 +1235,11 @@
12261235
$(OBJDIR)/fuzz_.c:$(OBJDIR)/fuzz.h \
12271236
$(OBJDIR)/glob_.c:$(OBJDIR)/glob.h \
12281237
$(OBJDIR)/graph_.c:$(OBJDIR)/graph.h \
12291238
$(OBJDIR)/gzip_.c:$(OBJDIR)/gzip.h \
12301239
$(OBJDIR)/hname_.c:$(OBJDIR)/hname.h \
1240
+ $(OBJDIR)/hook_.c:$(OBJDIR)/hook.h \
12311241
$(OBJDIR)/http_.c:$(OBJDIR)/http.h \
12321242
$(OBJDIR)/http_socket_.c:$(OBJDIR)/http_socket.h \
12331243
$(OBJDIR)/http_ssl_.c:$(OBJDIR)/http_ssl.h \
12341244
$(OBJDIR)/http_transport_.c:$(OBJDIR)/http_transport.h \
12351245
$(OBJDIR)/import_.c:$(OBJDIR)/import.h \
@@ -1313,11 +1323,10 @@
13131323
$(OBJDIR)/webmail_.c:$(OBJDIR)/webmail.h \
13141324
$(OBJDIR)/wiki_.c:$(OBJDIR)/wiki.h \
13151325
$(OBJDIR)/wikiformat_.c:$(OBJDIR)/wikiformat.h \
13161326
$(OBJDIR)/winfile_.c:$(OBJDIR)/winfile.h \
13171327
$(OBJDIR)/winhttp_.c:$(OBJDIR)/winhttp.h \
1318
- $(OBJDIR)/wysiwyg_.c:$(OBJDIR)/wysiwyg.h \
13191328
$(OBJDIR)/xfer_.c:$(OBJDIR)/xfer.h \
13201329
$(OBJDIR)/xfersetup_.c:$(OBJDIR)/xfersetup.h \
13211330
$(OBJDIR)/zip_.c:$(OBJDIR)/zip.h \
13221331
$(SRCDIR)/sqlite3.h \
13231332
$(SRCDIR)/th.h \
@@ -1741,10 +1750,18 @@
17411750
17421751
$(OBJDIR)/hname.o: $(OBJDIR)/hname_.c $(OBJDIR)/hname.h $(SRCDIR)/config.h
17431752
$(XTCC) -o $(OBJDIR)/hname.o -c $(OBJDIR)/hname_.c
17441753
17451754
$(OBJDIR)/hname.h: $(OBJDIR)/headers
1755
+
1756
+$(OBJDIR)/hook_.c: $(SRCDIR)/hook.c $(TRANSLATE)
1757
+ $(TRANSLATE) $(SRCDIR)/hook.c >$@
1758
+
1759
+$(OBJDIR)/hook.o: $(OBJDIR)/hook_.c $(OBJDIR)/hook.h $(SRCDIR)/config.h
1760
+ $(XTCC) -o $(OBJDIR)/hook.o -c $(OBJDIR)/hook_.c
1761
+
1762
+$(OBJDIR)/hook.h: $(OBJDIR)/headers
17461763
17471764
$(OBJDIR)/http_.c: $(SRCDIR)/http.c $(TRANSLATE)
17481765
$(TRANSLATE) $(SRCDIR)/http.c >$@
17491766
17501767
$(OBJDIR)/http.o: $(OBJDIR)/http_.c $(OBJDIR)/http.h $(SRCDIR)/config.h
@@ -2438,18 +2455,10 @@
24382455
$(OBJDIR)/winhttp.o: $(OBJDIR)/winhttp_.c $(OBJDIR)/winhttp.h $(SRCDIR)/config.h
24392456
$(XTCC) -o $(OBJDIR)/winhttp.o -c $(OBJDIR)/winhttp_.c
24402457
24412458
$(OBJDIR)/winhttp.h: $(OBJDIR)/headers
24422459
2443
-$(OBJDIR)/wysiwyg_.c: $(SRCDIR)/wysiwyg.c $(TRANSLATE)
2444
- $(TRANSLATE) $(SRCDIR)/wysiwyg.c >$@
2445
-
2446
-$(OBJDIR)/wysiwyg.o: $(OBJDIR)/wysiwyg_.c $(OBJDIR)/wysiwyg.h $(SRCDIR)/config.h
2447
- $(XTCC) -o $(OBJDIR)/wysiwyg.o -c $(OBJDIR)/wysiwyg_.c
2448
-
2449
-$(OBJDIR)/wysiwyg.h: $(OBJDIR)/headers
2450
-
24512460
$(OBJDIR)/xfer_.c: $(SRCDIR)/xfer.c $(TRANSLATE)
24522461
$(TRANSLATE) $(SRCDIR)/xfer.c >$@
24532462
24542463
$(OBJDIR)/xfer.o: $(OBJDIR)/xfer_.c $(OBJDIR)/xfer.h $(SRCDIR)/config.h
24552464
$(XTCC) -o $(OBJDIR)/xfer.o -c $(OBJDIR)/xfer_.c
24562465
--- win/Makefile.mingw.mistachkin
+++ win/Makefile.mingw.mistachkin
@@ -478,10 +478,11 @@
478 $(SRCDIR)/fuzz.c \
479 $(SRCDIR)/glob.c \
480 $(SRCDIR)/graph.c \
481 $(SRCDIR)/gzip.c \
482 $(SRCDIR)/hname.c \
 
483 $(SRCDIR)/http.c \
484 $(SRCDIR)/http_socket.c \
485 $(SRCDIR)/http_ssl.c \
486 $(SRCDIR)/http_transport.c \
487 $(SRCDIR)/import.c \
@@ -565,11 +566,10 @@
565 $(SRCDIR)/webmail.c \
566 $(SRCDIR)/wiki.c \
567 $(SRCDIR)/wikiformat.c \
568 $(SRCDIR)/winfile.c \
569 $(SRCDIR)/winhttp.c \
570 $(SRCDIR)/wysiwyg.c \
571 $(SRCDIR)/xfer.c \
572 $(SRCDIR)/xfersetup.c \
573 $(SRCDIR)/zip.c
574
575 EXTRA_FILES = \
@@ -635,13 +635,18 @@
635 $(SRCDIR)/default.css \
636 $(SRCDIR)/diff.tcl \
637 $(SRCDIR)/forum.js \
638 $(SRCDIR)/fossil.bootstrap.js \
639 $(SRCDIR)/fossil.confirmer.js \
 
640 $(SRCDIR)/fossil.dom.js \
641 $(SRCDIR)/fossil.fetch.js \
 
642 $(SRCDIR)/fossil.page.fileedit.js \
 
 
 
643 $(SRCDIR)/fossil.storage.js \
644 $(SRCDIR)/fossil.tabs.js \
645 $(SRCDIR)/graph.js \
646 $(SRCDIR)/href.js \
647 $(SRCDIR)/login.js \
@@ -667,10 +672,11 @@
667 $(SRCDIR)/sounds/d.wav \
668 $(SRCDIR)/sounds/e.wav \
669 $(SRCDIR)/sounds/f.wav \
670 $(SRCDIR)/style.admin_log.css \
671 $(SRCDIR)/style.fileedit.css \
 
672 $(SRCDIR)/tree.js \
673 $(SRCDIR)/useredit.js \
674 $(SRCDIR)/wiki.wiki
675
676 TRANS_SRC = \
@@ -724,10 +730,11 @@
724 $(OBJDIR)/fuzz_.c \
725 $(OBJDIR)/glob_.c \
726 $(OBJDIR)/graph_.c \
727 $(OBJDIR)/gzip_.c \
728 $(OBJDIR)/hname_.c \
 
729 $(OBJDIR)/http_.c \
730 $(OBJDIR)/http_socket_.c \
731 $(OBJDIR)/http_ssl_.c \
732 $(OBJDIR)/http_transport_.c \
733 $(OBJDIR)/import_.c \
@@ -811,11 +818,10 @@
811 $(OBJDIR)/webmail_.c \
812 $(OBJDIR)/wiki_.c \
813 $(OBJDIR)/wikiformat_.c \
814 $(OBJDIR)/winfile_.c \
815 $(OBJDIR)/winhttp_.c \
816 $(OBJDIR)/wysiwyg_.c \
817 $(OBJDIR)/xfer_.c \
818 $(OBJDIR)/xfersetup_.c \
819 $(OBJDIR)/zip_.c
820
821 OBJ = \
@@ -869,10 +875,11 @@
869 $(OBJDIR)/fuzz.o \
870 $(OBJDIR)/glob.o \
871 $(OBJDIR)/graph.o \
872 $(OBJDIR)/gzip.o \
873 $(OBJDIR)/hname.o \
 
874 $(OBJDIR)/http.o \
875 $(OBJDIR)/http_socket.o \
876 $(OBJDIR)/http_ssl.o \
877 $(OBJDIR)/http_transport.o \
878 $(OBJDIR)/import.o \
@@ -956,11 +963,10 @@
956 $(OBJDIR)/webmail.o \
957 $(OBJDIR)/wiki.o \
958 $(OBJDIR)/wikiformat.o \
959 $(OBJDIR)/winfile.o \
960 $(OBJDIR)/winhttp.o \
961 $(OBJDIR)/wysiwyg.o \
962 $(OBJDIR)/xfer.o \
963 $(OBJDIR)/xfersetup.o \
964 $(OBJDIR)/zip.o
965
966 APPNAME = fossil.exe
@@ -1057,13 +1063,16 @@
1057 # build is done from, i.e. the checkout belongs to. Do not sync/push
1058 # the repository after running the tests.
1059 test: $(OBJDIR) $(APPNAME)
1060 $(TCLSH) $(SRCDIR)/../test/tester.tcl $(APPNAME)
1061
1062 $(OBJDIR)/VERSION.h: $(SRCDIR)/../manifest.uuid $(SRCDIR)/../manifest $(MKVERSION)
1063 $(MKVERSION) $(SRCDIR)/../manifest.uuid $(SRCDIR)/../manifest $(SRCDIR)/../VERSION >$@
1064
 
 
 
1065 # The USE_SYSTEM_SQLITE variable may be undefined, set to 0, or set
1066 # to 1. If it is set to 1, then there is no need to build or link
1067 # the sqlite3.o object. Instead, the system SQLite will be linked
1068 # using -lsqlite3.
1069 SQLITE3_OBJ.0 = $(OBJDIR)/sqlite3.o
@@ -1226,10 +1235,11 @@
1226 $(OBJDIR)/fuzz_.c:$(OBJDIR)/fuzz.h \
1227 $(OBJDIR)/glob_.c:$(OBJDIR)/glob.h \
1228 $(OBJDIR)/graph_.c:$(OBJDIR)/graph.h \
1229 $(OBJDIR)/gzip_.c:$(OBJDIR)/gzip.h \
1230 $(OBJDIR)/hname_.c:$(OBJDIR)/hname.h \
 
1231 $(OBJDIR)/http_.c:$(OBJDIR)/http.h \
1232 $(OBJDIR)/http_socket_.c:$(OBJDIR)/http_socket.h \
1233 $(OBJDIR)/http_ssl_.c:$(OBJDIR)/http_ssl.h \
1234 $(OBJDIR)/http_transport_.c:$(OBJDIR)/http_transport.h \
1235 $(OBJDIR)/import_.c:$(OBJDIR)/import.h \
@@ -1313,11 +1323,10 @@
1313 $(OBJDIR)/webmail_.c:$(OBJDIR)/webmail.h \
1314 $(OBJDIR)/wiki_.c:$(OBJDIR)/wiki.h \
1315 $(OBJDIR)/wikiformat_.c:$(OBJDIR)/wikiformat.h \
1316 $(OBJDIR)/winfile_.c:$(OBJDIR)/winfile.h \
1317 $(OBJDIR)/winhttp_.c:$(OBJDIR)/winhttp.h \
1318 $(OBJDIR)/wysiwyg_.c:$(OBJDIR)/wysiwyg.h \
1319 $(OBJDIR)/xfer_.c:$(OBJDIR)/xfer.h \
1320 $(OBJDIR)/xfersetup_.c:$(OBJDIR)/xfersetup.h \
1321 $(OBJDIR)/zip_.c:$(OBJDIR)/zip.h \
1322 $(SRCDIR)/sqlite3.h \
1323 $(SRCDIR)/th.h \
@@ -1741,10 +1750,18 @@
1741
1742 $(OBJDIR)/hname.o: $(OBJDIR)/hname_.c $(OBJDIR)/hname.h $(SRCDIR)/config.h
1743 $(XTCC) -o $(OBJDIR)/hname.o -c $(OBJDIR)/hname_.c
1744
1745 $(OBJDIR)/hname.h: $(OBJDIR)/headers
 
 
 
 
 
 
 
 
1746
1747 $(OBJDIR)/http_.c: $(SRCDIR)/http.c $(TRANSLATE)
1748 $(TRANSLATE) $(SRCDIR)/http.c >$@
1749
1750 $(OBJDIR)/http.o: $(OBJDIR)/http_.c $(OBJDIR)/http.h $(SRCDIR)/config.h
@@ -2438,18 +2455,10 @@
2438 $(OBJDIR)/winhttp.o: $(OBJDIR)/winhttp_.c $(OBJDIR)/winhttp.h $(SRCDIR)/config.h
2439 $(XTCC) -o $(OBJDIR)/winhttp.o -c $(OBJDIR)/winhttp_.c
2440
2441 $(OBJDIR)/winhttp.h: $(OBJDIR)/headers
2442
2443 $(OBJDIR)/wysiwyg_.c: $(SRCDIR)/wysiwyg.c $(TRANSLATE)
2444 $(TRANSLATE) $(SRCDIR)/wysiwyg.c >$@
2445
2446 $(OBJDIR)/wysiwyg.o: $(OBJDIR)/wysiwyg_.c $(OBJDIR)/wysiwyg.h $(SRCDIR)/config.h
2447 $(XTCC) -o $(OBJDIR)/wysiwyg.o -c $(OBJDIR)/wysiwyg_.c
2448
2449 $(OBJDIR)/wysiwyg.h: $(OBJDIR)/headers
2450
2451 $(OBJDIR)/xfer_.c: $(SRCDIR)/xfer.c $(TRANSLATE)
2452 $(TRANSLATE) $(SRCDIR)/xfer.c >$@
2453
2454 $(OBJDIR)/xfer.o: $(OBJDIR)/xfer_.c $(OBJDIR)/xfer.h $(SRCDIR)/config.h
2455 $(XTCC) -o $(OBJDIR)/xfer.o -c $(OBJDIR)/xfer_.c
2456
--- win/Makefile.mingw.mistachkin
+++ win/Makefile.mingw.mistachkin
@@ -478,10 +478,11 @@
478 $(SRCDIR)/fuzz.c \
479 $(SRCDIR)/glob.c \
480 $(SRCDIR)/graph.c \
481 $(SRCDIR)/gzip.c \
482 $(SRCDIR)/hname.c \
483 $(SRCDIR)/hook.c \
484 $(SRCDIR)/http.c \
485 $(SRCDIR)/http_socket.c \
486 $(SRCDIR)/http_ssl.c \
487 $(SRCDIR)/http_transport.c \
488 $(SRCDIR)/import.c \
@@ -565,11 +566,10 @@
566 $(SRCDIR)/webmail.c \
567 $(SRCDIR)/wiki.c \
568 $(SRCDIR)/wikiformat.c \
569 $(SRCDIR)/winfile.c \
570 $(SRCDIR)/winhttp.c \
 
571 $(SRCDIR)/xfer.c \
572 $(SRCDIR)/xfersetup.c \
573 $(SRCDIR)/zip.c
574
575 EXTRA_FILES = \
@@ -635,13 +635,18 @@
635 $(SRCDIR)/default.css \
636 $(SRCDIR)/diff.tcl \
637 $(SRCDIR)/forum.js \
638 $(SRCDIR)/fossil.bootstrap.js \
639 $(SRCDIR)/fossil.confirmer.js \
640 $(SRCDIR)/fossil.copybutton.js \
641 $(SRCDIR)/fossil.dom.js \
642 $(SRCDIR)/fossil.fetch.js \
643 $(SRCDIR)/fossil.numbered-lines.js \
644 $(SRCDIR)/fossil.page.fileedit.js \
645 $(SRCDIR)/fossil.page.forumpost.js \
646 $(SRCDIR)/fossil.page.wikiedit.js \
647 $(SRCDIR)/fossil.popupwidget.js \
648 $(SRCDIR)/fossil.storage.js \
649 $(SRCDIR)/fossil.tabs.js \
650 $(SRCDIR)/graph.js \
651 $(SRCDIR)/href.js \
652 $(SRCDIR)/login.js \
@@ -667,10 +672,11 @@
672 $(SRCDIR)/sounds/d.wav \
673 $(SRCDIR)/sounds/e.wav \
674 $(SRCDIR)/sounds/f.wav \
675 $(SRCDIR)/style.admin_log.css \
676 $(SRCDIR)/style.fileedit.css \
677 $(SRCDIR)/style.wikiedit.css \
678 $(SRCDIR)/tree.js \
679 $(SRCDIR)/useredit.js \
680 $(SRCDIR)/wiki.wiki
681
682 TRANS_SRC = \
@@ -724,10 +730,11 @@
730 $(OBJDIR)/fuzz_.c \
731 $(OBJDIR)/glob_.c \
732 $(OBJDIR)/graph_.c \
733 $(OBJDIR)/gzip_.c \
734 $(OBJDIR)/hname_.c \
735 $(OBJDIR)/hook_.c \
736 $(OBJDIR)/http_.c \
737 $(OBJDIR)/http_socket_.c \
738 $(OBJDIR)/http_ssl_.c \
739 $(OBJDIR)/http_transport_.c \
740 $(OBJDIR)/import_.c \
@@ -811,11 +818,10 @@
818 $(OBJDIR)/webmail_.c \
819 $(OBJDIR)/wiki_.c \
820 $(OBJDIR)/wikiformat_.c \
821 $(OBJDIR)/winfile_.c \
822 $(OBJDIR)/winhttp_.c \
 
823 $(OBJDIR)/xfer_.c \
824 $(OBJDIR)/xfersetup_.c \
825 $(OBJDIR)/zip_.c
826
827 OBJ = \
@@ -869,10 +875,11 @@
875 $(OBJDIR)/fuzz.o \
876 $(OBJDIR)/glob.o \
877 $(OBJDIR)/graph.o \
878 $(OBJDIR)/gzip.o \
879 $(OBJDIR)/hname.o \
880 $(OBJDIR)/hook.o \
881 $(OBJDIR)/http.o \
882 $(OBJDIR)/http_socket.o \
883 $(OBJDIR)/http_ssl.o \
884 $(OBJDIR)/http_transport.o \
885 $(OBJDIR)/import.o \
@@ -956,11 +963,10 @@
963 $(OBJDIR)/webmail.o \
964 $(OBJDIR)/wiki.o \
965 $(OBJDIR)/wikiformat.o \
966 $(OBJDIR)/winfile.o \
967 $(OBJDIR)/winhttp.o \
 
968 $(OBJDIR)/xfer.o \
969 $(OBJDIR)/xfersetup.o \
970 $(OBJDIR)/zip.o
971
972 APPNAME = fossil.exe
@@ -1057,13 +1063,16 @@
1063 # build is done from, i.e. the checkout belongs to. Do not sync/push
1064 # the repository after running the tests.
1065 test: $(OBJDIR) $(APPNAME)
1066 $(TCLSH) $(SRCDIR)/../test/tester.tcl $(APPNAME)
1067
1068 $(OBJDIR)/VERSION.h: $(SRCDIR)/../manifest.uuid $(SRCDIR)/../manifest $(MKVERSION) $(OBJDIR)/phony.h
1069 $(MKVERSION) $(SRCDIR)/../manifest.uuid $(SRCDIR)/../manifest $(SRCDIR)/../VERSION >$@
1070
1071 $(OBJDIR)/phony.h:
1072 # Force rebuild of VERSION.h every time "make" is run
1073
1074 # The USE_SYSTEM_SQLITE variable may be undefined, set to 0, or set
1075 # to 1. If it is set to 1, then there is no need to build or link
1076 # the sqlite3.o object. Instead, the system SQLite will be linked
1077 # using -lsqlite3.
1078 SQLITE3_OBJ.0 = $(OBJDIR)/sqlite3.o
@@ -1226,10 +1235,11 @@
1235 $(OBJDIR)/fuzz_.c:$(OBJDIR)/fuzz.h \
1236 $(OBJDIR)/glob_.c:$(OBJDIR)/glob.h \
1237 $(OBJDIR)/graph_.c:$(OBJDIR)/graph.h \
1238 $(OBJDIR)/gzip_.c:$(OBJDIR)/gzip.h \
1239 $(OBJDIR)/hname_.c:$(OBJDIR)/hname.h \
1240 $(OBJDIR)/hook_.c:$(OBJDIR)/hook.h \
1241 $(OBJDIR)/http_.c:$(OBJDIR)/http.h \
1242 $(OBJDIR)/http_socket_.c:$(OBJDIR)/http_socket.h \
1243 $(OBJDIR)/http_ssl_.c:$(OBJDIR)/http_ssl.h \
1244 $(OBJDIR)/http_transport_.c:$(OBJDIR)/http_transport.h \
1245 $(OBJDIR)/import_.c:$(OBJDIR)/import.h \
@@ -1313,11 +1323,10 @@
1323 $(OBJDIR)/webmail_.c:$(OBJDIR)/webmail.h \
1324 $(OBJDIR)/wiki_.c:$(OBJDIR)/wiki.h \
1325 $(OBJDIR)/wikiformat_.c:$(OBJDIR)/wikiformat.h \
1326 $(OBJDIR)/winfile_.c:$(OBJDIR)/winfile.h \
1327 $(OBJDIR)/winhttp_.c:$(OBJDIR)/winhttp.h \
 
1328 $(OBJDIR)/xfer_.c:$(OBJDIR)/xfer.h \
1329 $(OBJDIR)/xfersetup_.c:$(OBJDIR)/xfersetup.h \
1330 $(OBJDIR)/zip_.c:$(OBJDIR)/zip.h \
1331 $(SRCDIR)/sqlite3.h \
1332 $(SRCDIR)/th.h \
@@ -1741,10 +1750,18 @@
1750
1751 $(OBJDIR)/hname.o: $(OBJDIR)/hname_.c $(OBJDIR)/hname.h $(SRCDIR)/config.h
1752 $(XTCC) -o $(OBJDIR)/hname.o -c $(OBJDIR)/hname_.c
1753
1754 $(OBJDIR)/hname.h: $(OBJDIR)/headers
1755
1756 $(OBJDIR)/hook_.c: $(SRCDIR)/hook.c $(TRANSLATE)
1757 $(TRANSLATE) $(SRCDIR)/hook.c >$@
1758
1759 $(OBJDIR)/hook.o: $(OBJDIR)/hook_.c $(OBJDIR)/hook.h $(SRCDIR)/config.h
1760 $(XTCC) -o $(OBJDIR)/hook.o -c $(OBJDIR)/hook_.c
1761
1762 $(OBJDIR)/hook.h: $(OBJDIR)/headers
1763
1764 $(OBJDIR)/http_.c: $(SRCDIR)/http.c $(TRANSLATE)
1765 $(TRANSLATE) $(SRCDIR)/http.c >$@
1766
1767 $(OBJDIR)/http.o: $(OBJDIR)/http_.c $(OBJDIR)/http.h $(SRCDIR)/config.h
@@ -2438,18 +2455,10 @@
2455 $(OBJDIR)/winhttp.o: $(OBJDIR)/winhttp_.c $(OBJDIR)/winhttp.h $(SRCDIR)/config.h
2456 $(XTCC) -o $(OBJDIR)/winhttp.o -c $(OBJDIR)/winhttp_.c
2457
2458 $(OBJDIR)/winhttp.h: $(OBJDIR)/headers
2459
 
 
 
 
 
 
 
 
2460 $(OBJDIR)/xfer_.c: $(SRCDIR)/xfer.c $(TRANSLATE)
2461 $(TRANSLATE) $(SRCDIR)/xfer.c >$@
2462
2463 $(OBJDIR)/xfer.o: $(OBJDIR)/xfer_.c $(OBJDIR)/xfer.h $(SRCDIR)/config.h
2464 $(XTCC) -o $(OBJDIR)/xfer.o -c $(OBJDIR)/xfer_.c
2465
+1 -1
--- www/chroot.md
+++ www/chroot.md
@@ -37,6 +37,6 @@
3737
[bld]: https://www.fossil-scm.org/fossil/doc/trunk/www/build.wiki
3838
[cj]: https://en.wikipedia.org/wiki/Chroot
3939
[fls]: ./loadmgmt.md
4040
[mnl]: https://fossil-scm.org/forum/forumpost/90caff30cb
4141
[srv]: ./server/
42
-[obsd]: ./server/openbsd/httpd.md#chroot
42
+[obsd]: ./server/openbsd/fastcgi.md#chroot
4343
--- www/chroot.md
+++ www/chroot.md
@@ -37,6 +37,6 @@
37 [bld]: https://www.fossil-scm.org/fossil/doc/trunk/www/build.wiki
38 [cj]: https://en.wikipedia.org/wiki/Chroot
39 [fls]: ./loadmgmt.md
40 [mnl]: https://fossil-scm.org/forum/forumpost/90caff30cb
41 [srv]: ./server/
42 [obsd]: ./server/openbsd/httpd.md#chroot
43
--- www/chroot.md
+++ www/chroot.md
@@ -37,6 +37,6 @@
37 [bld]: https://www.fossil-scm.org/fossil/doc/trunk/www/build.wiki
38 [cj]: https://en.wikipedia.org/wiki/Chroot
39 [fls]: ./loadmgmt.md
40 [mnl]: https://fossil-scm.org/forum/forumpost/90caff30cb
41 [srv]: ./server/
42 [obsd]: ./server/openbsd/fastcgi.md#chroot
43
--- www/fileformat.wiki
+++ www/fileformat.wiki
@@ -563,11 +563,11 @@
563563
subject for that thread. The argument to the <b>H</b> card is a string
564564
in the same format as a comment string in a <b>C</b> card.
565565
All follow-up posts have an <b>I</b> card that
566566
indicates which prior post in the same thread the current forum
567567
post is replying to, and a <b>G</b> card specifying the root post for
568
-the entire thread. The argument to G and <b>I</b> cards is the
568
+the entire thread. The argument to <b>G</b> and <b>I</b> cards is the
569569
artifact hash for the prior forum post to which the card refers.
570570
571571
In theory, it is sufficient for follow-up posts to have only an
572572
<b>I</b> card, since the <b>G</b> card value could be computed by following a
573573
chain of <b>I</b> cards. However, the <b>G</b> card is required in order to
574574
--- www/fileformat.wiki
+++ www/fileformat.wiki
@@ -563,11 +563,11 @@
563 subject for that thread. The argument to the <b>H</b> card is a string
564 in the same format as a comment string in a <b>C</b> card.
565 All follow-up posts have an <b>I</b> card that
566 indicates which prior post in the same thread the current forum
567 post is replying to, and a <b>G</b> card specifying the root post for
568 the entire thread. The argument to G and <b>I</b> cards is the
569 artifact hash for the prior forum post to which the card refers.
570
571 In theory, it is sufficient for follow-up posts to have only an
572 <b>I</b> card, since the <b>G</b> card value could be computed by following a
573 chain of <b>I</b> cards. However, the <b>G</b> card is required in order to
574
--- www/fileformat.wiki
+++ www/fileformat.wiki
@@ -563,11 +563,11 @@
563 subject for that thread. The argument to the <b>H</b> card is a string
564 in the same format as a comment string in a <b>C</b> card.
565 All follow-up posts have an <b>I</b> card that
566 indicates which prior post in the same thread the current forum
567 post is replying to, and a <b>G</b> card specifying the root post for
568 the entire thread. The argument to <b>G</b> and <b>I</b> cards is the
569 artifact hash for the prior forum post to which the card refers.
570
571 In theory, it is sufficient for follow-up posts to have only an
572 <b>I</b> card, since the <b>G</b> card value could be computed by following a
573 chain of <b>I</b> cards. However, the <b>G</b> card is required in order to
574
+6 -10
--- www/quotes.wiki
+++ www/quotes.wiki
@@ -19,11 +19,13 @@
1919
represented as n-dimensional membranes, mapping the spatial loci of
2020
successive commits onto the projected manifold of each cloned
2121
repository.</nowiki>
2222
2323
<blockquote>
24
-<i>At [http://tartley.com/?p=1267]</i>
24
+<i>Previously at
25
+[https://www.tartley.com/a-guide-to-git-using-spatial-analogies], since
26
+removed;<br>Quoted here: [https://lwn.net/Articles/420152/].</i>
2527
</blockquote>
2628
2729
<li>Git is not a Prius. Git is a Model T.
2830
Its plumbing and wiring sticks out all over the place.
2931
You have to be a mechanic to operate it successfully or you'll be
@@ -50,16 +52,10 @@
5052
<p>* dkf really wishes he could use fossil instead</p>
5153
<blockquote>
5254
<i>by Donal K. Fellow (dkf) on the Tcl/Tk chatroom, 2013-04-09.</i>
5355
</blockquote>
5456
55
-<li>Klingon Code Warriors embrace Git; we enjoy arbitrary conflicts. Git is not for the weak and feeble. TODAY IS A GOOD DAY TO CODE.
56
-
57
-<blockquote>
58
-<i>teastain at [http://www.reddit.com/r/programming/comments/xpitj/10_things_i_hate_about_git/c5oj4fk]
59
-</blockquote>
60
-
6157
<li>&#91;G&#93;it is <i>designed</i> to forget things.
6258
6359
<blockquote>
6460
<i>[http://www.cs.cmu.edu/~davide/howto/git_lose.html]
6561
</blockquote>
@@ -91,11 +87,11 @@
9187
<li value=11>
9288
Fossil mesmerizes me with simplicity especially after I struggled to
9389
get a bug-tracking system to work with mercurial.
9490
9591
<blockquote>
96
-<i>rawjeev at [http://stackoverflow.com/questions/156322/what-do-people-think-of-the-fossil-dvcs]</i>
92
+<i>rawjeev at [https://stackoverflow.com/a/2100469/142454]</i>
9793
</blockquote>
9894
9995
<li>Fossil is the best thing to happen
10096
to my development workflow this year, as I am pretty sure that using
10197
Git has resulted in the premature death of too many of my brain cells.
@@ -108,11 +104,11 @@
108104
109105
<li>Fossil is awesome!!! I have never seen an app like that before,
110106
such simplicity and flexibility!!!
111107
112108
<blockquote>
113
-<i>zengr at [http://stackoverflow.com/questions/138621/best-version-control-for-lone-developer]</i>
109
+<i>zengr at [https://stackoverflow.com/a/629967/142454]</i>
114110
</blockquote>
115111
116112
<li>This is my favourite VCS. I can carry it on a USB. And it's a complete system, with it's own
117113
server, ticketing system, Wiki pages, and a very, very helpful timeline visualization. And
118114
the entire program in a single file!
@@ -150,11 +146,11 @@
150146
sometimes very restrictive firewalls, OSX/Win/Linux). We are happy with it
151147
and teaching a Msc/Phd student (read complete novice) fossil has just
152148
been a smoother ride than Git was.
153149
154150
<blockquote>
155
-<i>viablepanic at [http://www.reddit.com/r/programming/comments/bxcto/why_not_fossil_scm/]</i>
151
+<i>viablepanic at [https://www.reddit.com/r/programming/comments/bxcto/why_not_fossil_scm/c0p30b4?utm_source=share&utm_medium=web2x&context=3]</i>
156152
</blockquote>
157153
158154
<li>In the fossil community - and hence in fossil itself - development history
159155
is pretty much sacrosanct. The very name "fossil" was to chosen to
160156
reflect the unchanging nature of things in that history.
161157
--- www/quotes.wiki
+++ www/quotes.wiki
@@ -19,11 +19,13 @@
19 represented as n-dimensional membranes, mapping the spatial loci of
20 successive commits onto the projected manifold of each cloned
21 repository.</nowiki>
22
23 <blockquote>
24 <i>At [http://tartley.com/?p=1267]</i>
 
 
25 </blockquote>
26
27 <li>Git is not a Prius. Git is a Model T.
28 Its plumbing and wiring sticks out all over the place.
29 You have to be a mechanic to operate it successfully or you'll be
@@ -50,16 +52,10 @@
50 <p>* dkf really wishes he could use fossil instead</p>
51 <blockquote>
52 <i>by Donal K. Fellow (dkf) on the Tcl/Tk chatroom, 2013-04-09.</i>
53 </blockquote>
54
55 <li>Klingon Code Warriors embrace Git; we enjoy arbitrary conflicts. Git is not for the weak and feeble. TODAY IS A GOOD DAY TO CODE.
56
57 <blockquote>
58 <i>teastain at [http://www.reddit.com/r/programming/comments/xpitj/10_things_i_hate_about_git/c5oj4fk]
59 </blockquote>
60
61 <li>&#91;G&#93;it is <i>designed</i> to forget things.
62
63 <blockquote>
64 <i>[http://www.cs.cmu.edu/~davide/howto/git_lose.html]
65 </blockquote>
@@ -91,11 +87,11 @@
91 <li value=11>
92 Fossil mesmerizes me with simplicity especially after I struggled to
93 get a bug-tracking system to work with mercurial.
94
95 <blockquote>
96 <i>rawjeev at [http://stackoverflow.com/questions/156322/what-do-people-think-of-the-fossil-dvcs]</i>
97 </blockquote>
98
99 <li>Fossil is the best thing to happen
100 to my development workflow this year, as I am pretty sure that using
101 Git has resulted in the premature death of too many of my brain cells.
@@ -108,11 +104,11 @@
108
109 <li>Fossil is awesome!!! I have never seen an app like that before,
110 such simplicity and flexibility!!!
111
112 <blockquote>
113 <i>zengr at [http://stackoverflow.com/questions/138621/best-version-control-for-lone-developer]</i>
114 </blockquote>
115
116 <li>This is my favourite VCS. I can carry it on a USB. And it's a complete system, with it's own
117 server, ticketing system, Wiki pages, and a very, very helpful timeline visualization. And
118 the entire program in a single file!
@@ -150,11 +146,11 @@
150 sometimes very restrictive firewalls, OSX/Win/Linux). We are happy with it
151 and teaching a Msc/Phd student (read complete novice) fossil has just
152 been a smoother ride than Git was.
153
154 <blockquote>
155 <i>viablepanic at [http://www.reddit.com/r/programming/comments/bxcto/why_not_fossil_scm/]</i>
156 </blockquote>
157
158 <li>In the fossil community - and hence in fossil itself - development history
159 is pretty much sacrosanct. The very name "fossil" was to chosen to
160 reflect the unchanging nature of things in that history.
161
--- www/quotes.wiki
+++ www/quotes.wiki
@@ -19,11 +19,13 @@
19 represented as n-dimensional membranes, mapping the spatial loci of
20 successive commits onto the projected manifold of each cloned
21 repository.</nowiki>
22
23 <blockquote>
24 <i>Previously at
25 [https://www.tartley.com/a-guide-to-git-using-spatial-analogies], since
26 removed;<br>Quoted here: [https://lwn.net/Articles/420152/].</i>
27 </blockquote>
28
29 <li>Git is not a Prius. Git is a Model T.
30 Its plumbing and wiring sticks out all over the place.
31 You have to be a mechanic to operate it successfully or you'll be
@@ -50,16 +52,10 @@
52 <p>* dkf really wishes he could use fossil instead</p>
53 <blockquote>
54 <i>by Donal K. Fellow (dkf) on the Tcl/Tk chatroom, 2013-04-09.</i>
55 </blockquote>
56
 
 
 
 
 
 
57 <li>&#91;G&#93;it is <i>designed</i> to forget things.
58
59 <blockquote>
60 <i>[http://www.cs.cmu.edu/~davide/howto/git_lose.html]
61 </blockquote>
@@ -91,11 +87,11 @@
87 <li value=11>
88 Fossil mesmerizes me with simplicity especially after I struggled to
89 get a bug-tracking system to work with mercurial.
90
91 <blockquote>
92 <i>rawjeev at [https://stackoverflow.com/a/2100469/142454]</i>
93 </blockquote>
94
95 <li>Fossil is the best thing to happen
96 to my development workflow this year, as I am pretty sure that using
97 Git has resulted in the premature death of too many of my brain cells.
@@ -108,11 +104,11 @@
104
105 <li>Fossil is awesome!!! I have never seen an app like that before,
106 such simplicity and flexibility!!!
107
108 <blockquote>
109 <i>zengr at [https://stackoverflow.com/a/629967/142454]</i>
110 </blockquote>
111
112 <li>This is my favourite VCS. I can carry it on a USB. And it's a complete system, with it's own
113 server, ticketing system, Wiki pages, and a very, very helpful timeline visualization. And
114 the entire program in a single file!
@@ -150,11 +146,11 @@
146 sometimes very restrictive firewalls, OSX/Win/Linux). We are happy with it
147 and teaching a Msc/Phd student (read complete novice) fossil has just
148 been a smoother ride than Git was.
149
150 <blockquote>
151 <i>viablepanic at [https://www.reddit.com/r/programming/comments/bxcto/why_not_fossil_scm/c0p30b4?utm_source=share&utm_medium=web2x&context=3]</i>
152 </blockquote>
153
154 <li>In the fossil community - and hence in fossil itself - development history
155 is pretty much sacrosanct. The very name "fossil" was to chosen to
156 reflect the unchanging nature of things in that history.
157
--- www/server/any/scgi.md
+++ www/server/any/scgi.md
@@ -63,11 +63,11 @@
6363
We go into more detail on nginx service setup with Fossil in our
6464
[Debian/Ubuntu specific guide](../debian/nginx.md). Then in [a later
6565
article](../../tls-nginx.md) that builds upon that, we show how to add
6666
TLS encryption to this basic SCGI + nginx setup on Debian type OSes.
6767
68
-Similarly, our [OpenBSD specific guide](../openbsd/httpd.md) details how
68
+Similarly, our [OpenBSD specific guide](../openbsd/fastcgi.md) details how
6969
to setup a Fossil server using httpd and FastCGI on OpenBSD.
7070
7171
*[Return to the top-level Fossil server article.](../)*
7272
7373
[404]: https://en.wikipedia.org/wiki/HTTP_404
7474
--- www/server/any/scgi.md
+++ www/server/any/scgi.md
@@ -63,11 +63,11 @@
63 We go into more detail on nginx service setup with Fossil in our
64 [Debian/Ubuntu specific guide](../debian/nginx.md). Then in [a later
65 article](../../tls-nginx.md) that builds upon that, we show how to add
66 TLS encryption to this basic SCGI + nginx setup on Debian type OSes.
67
68 Similarly, our [OpenBSD specific guide](../openbsd/httpd.md) details how
69 to setup a Fossil server using httpd and FastCGI on OpenBSD.
70
71 *[Return to the top-level Fossil server article.](../)*
72
73 [404]: https://en.wikipedia.org/wiki/HTTP_404
74
--- www/server/any/scgi.md
+++ www/server/any/scgi.md
@@ -63,11 +63,11 @@
63 We go into more detail on nginx service setup with Fossil in our
64 [Debian/Ubuntu specific guide](../debian/nginx.md). Then in [a later
65 article](../../tls-nginx.md) that builds upon that, we show how to add
66 TLS encryption to this basic SCGI + nginx setup on Debian type OSes.
67
68 Similarly, our [OpenBSD specific guide](../openbsd/fastcgi.md) details how
69 to setup a Fossil server using httpd and FastCGI on OpenBSD.
70
71 *[Return to the top-level Fossil server article.](../)*
72
73 [404]: https://en.wikipedia.org/wiki/HTTP_404
74

Keyboard Shortcuts

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