Fossil SCM

Add file attachment capability to forum posts.

stephan 2026-05-25 11:39 UTC trunk merge
Commit 5079ffb0278d122d1218faed72743cfaf695eee0da7cb79d12ccc368829ea71a
+327 -131
--- src/attach.c
+++ src/attach.c
@@ -18,32 +18,71 @@
1818
** This file contains code for dealing with attachments.
1919
*/
2020
#include "config.h"
2121
#include "attach.h"
2222
#include <assert.h>
23
+
24
+/*
25
+** Given a presumedly legal attachment target name, this guesses the
26
+** target type and returns one of CFTYPE_FORUM, CFTYPE_WIKI,
27
+** CFTYPE_TICKET, or CFTYPE_EVENT. Returns 0 if it cannot
28
+** distinguish the target type.
29
+**
30
+** In the case of CFTYPE_FORUM, it is up to the caller to ensure that,
31
+** if needed, they resolve zTarget using forumpost_head_rid2() so that
32
+** they get the RID of the earliest version of the post, as that is
33
+** the only one which attachments should target.
34
+*/
35
+int attachment_target_type(const char *zTarget){
36
+ static Stmt q = empty_Stmt_m;
37
+ int rc = 0;
38
+ if( forumpost_head_rid2(zTarget)>0 ){
39
+ return CFTYPE_FORUM;
40
+ }
41
+ if( !q.pStmt ){
42
+ db_static_prepare(
43
+ &q,
44
+ "SELECT CASE "
45
+ "WHEN 'tkt-'||:tgt IN (SELECT tagname FROM tag) THEN %d "
46
+ "WHEN 'event-'||:tgt IN (SELECT tagname FROM tag) THEN %d "
47
+ "WHEN 'wiki-'||:tgt IN (SELECT tagname FROM tag) THEN %d "
48
+ "ELSE 0 END",
49
+ CFTYPE_TICKET, CFTYPE_EVENT, CFTYPE_WIKI
50
+ );
51
+ }
52
+ db_bind_text(&q, ":tgt", zTarget);
53
+ if( SQLITE_ROW==db_step(&q) ){
54
+ rc = db_column_int(&q, 0);
55
+ }
56
+ db_reset(&q);
57
+ return rc;
58
+}
2359
2460
/*
2561
** WEBPAGE: attachlist
2662
** List attachments.
2763
**
2864
** tkt=HASH
2965
** page=WIKIPAGE
3066
** technote=HASH
67
+** forumpost=HASH
3168
**
32
-** At most one of technote=, tkt= or page= may be supplied.
69
+** At most one of technote=, tkt=, forumpost=, or page= may be supplied.
3370
**
3471
** If none are given, all attachments are listed. If one is given, only
3572
** attachments for the designated technote, ticket or wiki page are shown.
3673
**
3774
** HASH may be just a prefix of the relevant technical note or ticket
3875
** artifact hash, in which case all attachments of all technical notes or
39
-** tickets with the prefix will be listed.
76
+** tickets with the prefix will be listed. Forum posts, on the other hand,
77
+** require a unique hash prefix.
4078
*/
4179
void attachlist_page(void){
4280
const char *zPage = P("page");
4381
const char *zTkt = P("tkt");
4482
const char *zTechNote = P("technote");
83
+ const char *zForumPost = P("forumpost");
4584
Blob sql;
4685
Stmt q;
4786
4887
if( zPage && zTkt ) zTkt = 0;
4988
login_check_credentials();
@@ -50,29 +89,34 @@
5089
style_set_current_feature("attach");
5190
blob_zero(&sql);
5291
blob_append_sql(&sql,
5392
"SELECT datetime(mtime,toLocal()), src, target, filename,"
5493
" comment, user,"
55
- " (SELECT uuid FROM blob WHERE rid=attachid), attachid,"
56
- " (CASE WHEN 'tkt-'||target IN (SELECT tagname FROM tag)"
57
- " THEN 1"
58
- " WHEN 'event-'||target IN (SELECT tagname FROM tag)"
59
- " THEN 2"
60
- " ELSE 0 END)"
94
+ " (SELECT uuid FROM blob WHERE rid=attachid), attachid"
6195
" FROM attachment"
6296
);
63
- if( zPage ){
97
+ if( zForumPost ){
98
+ int fnid;
99
+ if( g.perm.RdForum==0 ){ login_needed(g.anon.RdForum); return; }
100
+ style_header("Attachments To Forum post %S", zForumPost);
101
+ fnid = forumpost_head_rid2(zForumPost);
102
+ if( fnid<=0 ){
103
+ webpage_error("Invalid forum post ID: %h", zForumPost);
104
+ }
105
+ blob_append_sql(&sql, " WHERE target="
106
+ "(SELECT uuid FROM blob WHERE rid=%d)", fnid);
107
+ }else if( zPage ){
64108
if( g.perm.RdWiki==0 ){ login_needed(g.anon.RdWiki); return; }
65
- style_header("Attachments To %h", zPage);
109
+ style_header("Attachments To Wiki page %h", zPage);
66110
blob_append_sql(&sql, " WHERE target=%Q", zPage);
67111
}else if( zTkt ){
68112
if( g.perm.RdTkt==0 ){ login_needed(g.anon.RdTkt); return; }
69113
style_header("Attachments To Ticket %S", zTkt);
70114
blob_append_sql(&sql, " WHERE target GLOB '%q*'", zTkt);
71115
}else if( zTechNote ){
72116
if( g.perm.RdWiki==0 ){ login_needed(g.anon.RdWiki); return; }
73
- style_header("Attachments to Tech Note %S", zTechNote);
117
+ style_header("Attachments To Tech Note %S", zTechNote);
74118
blob_append_sql(&sql, " WHERE target GLOB '%q*'",
75119
zTechNote);
76120
}else{
77121
if( g.perm.RdTkt==0 && g.perm.RdWiki==0 ){
78122
login_needed(g.anon.RdTkt || g.anon.RdWiki);
@@ -82,35 +126,58 @@
82126
}
83127
blob_append_sql(&sql, " ORDER BY mtime DESC");
84128
db_prepare(&q, "%s", blob_sql_text(&sql));
85129
@ <ol>
86130
while( db_step(&q)==SQLITE_ROW ){
87
- const char *zDate = db_column_text(&q, 0);
88
- const char *zSrc = db_column_text(&q, 1);
89
- const char *zTarget = db_column_text(&q, 2);
90
- const char *zFilename = db_column_text(&q, 3);
91
- const char *zComment = db_column_text(&q, 4);
92
- const char *zUser = db_column_text(&q, 5);
93
- const char *zUuid = db_column_text(&q, 6);
94
- int attachid = db_column_int(&q, 7);
95
- /* type 0 is a wiki page, 1 is a ticket, 2 is a tech note */
96
- int type = db_column_int(&q, 8);
97
- const char *zDispUser = zUser && zUser[0] ? zUser : "anonymous";
131
+ const char *zDate;
132
+ const char *zSrc;
133
+ const char *zTarget;
134
+ const char *zFilename;
135
+ const char *zComment;
136
+ const char *zUser;
137
+ const char *zUuid;
138
+ const char *zDispUser;
139
+ const int attachid = db_column_int(&q, 7);
140
+ int type;
98141
int i;
99
- char *zUrlTail;
142
+ char *zUrlTail = 0;
143
+
144
+ if( moderation_pending(attachid)
145
+ && !moderation_user_could(attachid, 1, 0) ){
146
+ /* Elide entries which are currently pending moderation unless
147
+ ** the user would be able to moderate the entry themselves. */
148
+ continue;
149
+ }
150
+
151
+ zDate = db_column_text(&q, 0);
152
+ zSrc = db_column_text(&q, 1);
153
+ zTarget = db_column_text(&q, 2);
154
+ zFilename = db_column_text(&q, 3);
155
+ zComment = db_column_text(&q, 4);
156
+ zUser = db_column_text(&q, 5);
157
+ zUuid = db_column_text(&q, 6);
158
+ zDispUser = zUser && zUser[0] ? zUser : "anonymous";
100159
for(i=0; zFilename[i]; i++){
101160
if( zFilename[i]=='/' && zFilename[i+1]!=0 ){
102161
zFilename = &zFilename[i+1];
103162
i = -1;
104163
}
105164
}
106
- if( type==1 ){
107
- zUrlTail = mprintf("tkt=%s&file=%t", zTarget, zFilename);
108
- }else if( type==2 ){
109
- zUrlTail = mprintf("technote=%s&file=%t", zTarget, zFilename);
110
- }else{
111
- zUrlTail = mprintf("page=%t&file=%t", zTarget, zFilename);
165
+ type = attachment_target_type(zTarget);
166
+ switch( type ){
167
+ case CFTYPE_TICKET:
168
+ zUrlTail = mprintf("tkt=%s&file=%t", zTarget, zFilename);
169
+ break;
170
+ case CFTYPE_EVENT:
171
+ zUrlTail = mprintf("technote=%s&file=%t", zTarget, zFilename);
172
+ break;
173
+ case CFTYPE_FORUM:
174
+ zUrlTail = mprintf("forumpost=%t&file=%t", zTarget, zFilename);
175
+ break;
176
+ case CFTYPE_WIKI:
177
+ zUrlTail = mprintf("page=%t&file=%t", zTarget, zFilename);
178
+ break;
112179
}
113180
@ <li><p>
114181
@ Attachment %z(href("%R/ainfo/%!S",zUuid))%S(zUuid)</a>
115182
moderation_pending_www(attachid);
116183
@ <br><a href="%R/attachview?%s(zUrlTail)">%h(zFilename)</a>
@@ -117,25 +184,37 @@
117184
@ [<a href="%R/attachdownload/%t(zFilename)?%s(zUrlTail)">download</a>]<br>
118185
if( zComment ) while( fossil_isspace(zComment[0]) ) zComment++;
119186
if( zComment && zComment[0] ){
120187
@ %!W(zComment)<br>
121188
}
122
- if( zPage==0 && zTkt==0 && zTechNote==0 ){
189
+ if( zForumPost==0 && zPage==0 && zTkt==0 && zTechNote==0 ){
123190
if( zSrc==0 || zSrc[0]==0 ){
124191
zSrc = "Deleted from";
125192
}else {
126193
zSrc = "Added to";
127194
}
128
- if( type==1 ){
129
- @ %s(zSrc) ticket <a href="%R/tktview?name=%s(zTarget)">
130
- @ %S(zTarget)</a>
131
- }else if( type==2 ){
132
- @ %s(zSrc) tech note <a href="%R/technote/%s(zTarget)">
133
- @ %S(zTarget)</a>
134
- }else{
135
- @ %s(zSrc) wiki page <a href="%R/wiki?name=%t(zTarget)">
136
- @ %h(zTarget)</a>
195
+ switch( type ){
196
+ case CFTYPE_TICKET:
197
+ @ %s(zSrc) ticket <a href="%R/tktview?name=%s(zTarget)">
198
+ @ %S(zTarget)</a>
199
+ break;
200
+ case CFTYPE_EVENT:
201
+ @ %s(zSrc) tech note <a href="%R/technote/%s(zTarget)">
202
+ @ %S(zTarget)</a>
203
+ break;
204
+ case CFTYPE_WIKI:
205
+ @ %s(zSrc) wiki page <a href="%R/wiki?name=%t(zTarget)">
206
+ @ %h(zTarget)</a>
207
+ break;
208
+ case CFTYPE_FORUM:
209
+ @ %s(zSrc) forum post <a href="%R/forumpost/%s(zTarget)">
210
+ @ %h(zTarget)</a>
211
+ break;
212
+ default:
213
+ @ <span class='error'>%s(zSrc) cannot determine target type
214
+ @ of %h(zTarget)</span>
215
+ break;
137216
}
138217
}else{
139218
if( zSrc==0 || zSrc[0]==0 ){
140219
@ Deleted
141220
}else {
@@ -162,27 +241,35 @@
162241
** Query parameters:
163242
**
164243
** tkt=HASH
165244
** page=WIKIPAGE
166245
** technote=HASH
246
+** forumpost=HASH
167247
** file=FILENAME
168248
** attachid=ID
169249
**
170250
*/
171251
void attachview_page(void){
172252
const char *zPage = P("page");
173253
const char *zTkt = P("tkt");
174254
const char *zTechNote = P("technote");
255
+ const char *zForumPost = P("forumpost");
175256
const char *zFile = P("file");
176257
const char *zTarget = 0;
177258
int attachid = atoi(PD("attachid","0"));
178
- char *zUUID;
259
+ char *zUUID = 0;
179260
180261
if( zFile==0 ) fossil_redirect_home();
181262
login_check_credentials();
182263
style_set_current_feature("attach");
183
- if( zPage ){
264
+ if( zForumPost ){
265
+ int fnid;
266
+ if( g.perm.RdForum==0 ){ login_needed(g.anon.RdForum); return; }
267
+ /* Forum attachments are always tied to the post's initial version */
268
+ fnid = forumpost_head_rid2(zForumPost);
269
+ if( fnid>0 ) zTarget = rid_to_uuid(fnid);
270
+ }else if( zPage ){
184271
if( g.perm.RdWiki==0 ){ login_needed(g.anon.RdWiki); return; }
185272
zTarget = zPage;
186273
}else if( zTkt ){
187274
if( g.perm.RdTkt==0 ){ login_needed(g.anon.RdTkt); return; }
188275
zTarget = zTkt;
@@ -314,35 +401,60 @@
314401
** Add a new attachment.
315402
**
316403
** tkt=HASH
317404
** page=WIKIPAGE
318405
** technote=HASH
406
+** forumpost=HASH
319407
** from=URL
320408
**
321409
*/
322410
void attachadd_page(void){
323411
const char *zPage = P("page");
412
+ const char *zForumPost = P("forumpost");
324413
const char *zTkt = P("tkt");
325414
const char *zTechNote = P("technote");
326415
const char *zFrom = P("from");
327416
const char *aContent = P("f");
328417
const char *zName = PD("f:filename","unknown");
418
+ const char *zComment = PD("comment", "");
329419
const char *zTarget;
330
- char *zTargetType;
420
+ char * zTo = 0;
421
+ char *zTargetType = 0;
422
+ char *zExtraFree = 0;
331423
int szContent = atoi(PD("f:bytes","0"));
332424
int goodCaptcha = 1;
425
+ int szLimit = 0;
333426
427
+ if( zFrom==0 ) zFrom = mprintf("%R/home");
334428
if( P("cancel") ) cgi_redirect(zFrom);
335
- if( (zPage && zTkt)
336
- || (zPage && zTechNote)
337
- || (zTkt && zTechNote)
338
- ){
339
- fossil_redirect_home();
429
+ if( (!!zPage + !!zTkt + !!zTechNote + !!zForumPost)!=1 ){
430
+ webpage_error("Requires exactly one one: page=X, tkt=X, forumpost=X,"
431
+ " or technote=X");
340432
}
341
- if( zPage==0 && zTkt==0 && zTechNote==0) fossil_redirect_home();
342433
login_check_credentials();
343
- if( zPage ){
434
+ if( zForumPost ){
435
+ int fpid;
436
+ if( g.perm.AttachForum==0 ){
437
+ login_needed(g.anon.AttachForum);
438
+ return;
439
+ }
440
+ fpid = forumpost_head_rid2(zForumPost);
441
+ if( fpid<=0 ){
442
+ webpage_error("Invalid forum post ID: %h", zForumPost);
443
+ }else if( !g.perm.Admin && !forumpost_is_owner(fpid, 0) ){
444
+ webpage_error("Only admins can attach files to other users' "
445
+ "forum posts.");
446
+ }
447
+ zTarget = zExtraFree = rid_to_uuid(fpid);
448
+ zTargetType = mprintf("Forum post <a href=\"%R/forumpost/%S\">%h</a>",
449
+ zTarget, zForumPost);
450
+ zTo = 1
451
+ ? mprintf("%R/forumpost/%S", zTarget)
452
+ : mprintf("%R/attachview?forumpost=%T&file=%T",
453
+ zTarget, zName)
454
+ /* Or we could return directly to the forum post. */;
455
+ }else if( zPage ){
344456
if( g.perm.ApndWiki==0 || g.perm.Attach==0 ){
345457
login_needed(g.anon.ApndWiki && g.anon.Attach);
346458
return;
347459
}
348460
if( !db_exists("SELECT 1 FROM tag WHERE tagname='wiki-%q'", zPage) ){
@@ -364,10 +476,11 @@
364476
zTarget = zTechNote;
365477
zTargetType = mprintf("Tech Note <a href=\"%R/technote/%s\">%S</a>",
366478
zTechNote, zTechNote);
367479
368480
}else{
481
+ assert( zTkt );
369482
if( g.perm.ApndTkt==0 || g.perm.Attach==0 ){
370483
login_needed(g.anon.ApndTkt && g.anon.Attach);
371484
return;
372485
}
373486
if( !db_exists("SELECT 1 FROM tag WHERE tagname='tkt-%q'", zTkt) ){
@@ -377,21 +490,25 @@
377490
}
378491
zTarget = zTkt;
379492
zTargetType = mprintf("Ticket <a href=\"%R/tktview/%s\">%S</a>",
380493
zTkt, zTkt);
381494
}
382
- if( zFrom==0 ) zFrom = mprintf("%R/home");
383
- if( P("cancel") ){
384
- cgi_redirect(zFrom);
385
- }
386
- if( P("ok") && szContent>0 && (goodCaptcha = captcha_is_correct(0)) ){
387
- int needModerator = (zTkt!=0 && ticket_need_moderation(0)) ||
495
+ szLimit = db_get_int("attachment-size-limit", 0);
496
+ if( szContent<0 || (szLimit && szContent>szLimit) ){
497
+ /* This check must be done late so that zTargetType is set up. */
498
+ @ <p class="generalError">Attachment %h(zName) is too large.
499
+ @ <a href="%R/help/attachment-size-limit">Limit</a> is
500
+ @ %d(szLimit ? szLimit : 0x7fffffff) bytes</p>
501
+ /* Fall through and render form. */
502
+ }else if( P("ok") && szContent>0 && (goodCaptcha = captcha_is_correct(0)) ){
503
+ int needModerator = (zForumPost!=0 && forum_need_moderation()) ||
504
+ (zTkt!=0 && ticket_need_moderation(0)) ||
388505
(zPage!=0 && wiki_need_moderation(0));
389
- const char *zComment = PD("comment", "");
390506
attach_commit(zName, zTarget, aContent, szContent, needModerator, zComment);
391
- cgi_redirect(zFrom);
507
+ cgi_redirect(zTo ? zTo : zFrom);
392508
}
509
+
393510
style_set_current_feature("attach");
394511
style_header("Add Attachment");
395512
if( !goodCaptcha ){
396513
@ <p class="generalError">Error: Incorrect security code.</p>
397514
}
@@ -399,12 +516,15 @@
399516
form_begin("enctype='multipart/form-data'", "%R/attachadd");
400517
@ <div>
401518
@ File to Attach:
402519
@ <input type="file" name="f" size="60"><br>
403520
@ Description:<br>
404
- @ <textarea name="comment" cols="80" rows="5" wrap="virtual"></textarea><br>
405
- if( zTkt ){
521
+ @ <textarea name="comment" cols="80" rows="5" wrap="virtual"\
522
+ @ >%h(zComment)</textarea><br>
523
+ if( zForumPost ){
524
+ @ <input type="hidden" name="forumpost" value="%h(zTarget)">
525
+ }else if( zTkt ){
406526
@ <input type="hidden" name="tkt" value="%h(zTkt)">
407527
}else if( zTechNote ){
408528
@ <input type="hidden" name="technote" value="%h(zTechNote)">
409529
}else{
410530
@ <input type="hidden" name="page" value="%h(zPage)">
@@ -415,15 +535,18 @@
415535
@ </div>
416536
captcha_generate(0);
417537
@ </form>
418538
style_finish_page();
419539
fossil_free(zTargetType);
540
+ fossil_free(zExtraFree);
420541
}
421542
422543
/*
423544
** WEBPAGE: ainfo
424545
** URL: /ainfo?name=ARTIFACTID
546
+**
547
+** name=ATTACHMENT_ARTIFACT_UUID
425548
**
426549
** Show the details of an attachment artifact.
427550
*/
428551
void ainfo_page(void){
429552
int rid; /* RID for the control artifact */
@@ -436,113 +559,141 @@
436559
const char *zName; /* Name of the attached file */
437560
const char *zDesc; /* Description of the attached file */
438561
const char *zWikiName = 0; /* Wiki page name when attached to Wiki */
439562
const char *zTNUuid = 0; /* Tech Note ID when attached to tech note */
440563
const char *zTktUuid = 0; /* Ticket ID when attached to a ticket */
564
+ const char *zForumPost = 0; /* Forum UID when attached to forum post */
441565
int modPending; /* True if awaiting moderation */
442566
const char *zModAction; /* Moderation action or NULL */
443567
int isModerator; /* TRUE if user is the moderator */
444568
const char *zMime; /* MIME Type */
445569
Blob attach; /* Content of the attachment */
446
- int fShowContent = 0;
570
+ int fShowContent = 0; /* True to emit the content */
571
+ int bUserIsOwner = 0; /* True if pAttach->zUser is login_name() */
572
+ int showDelMenu = 0; /* True to enable delete option */
447573
const char *zLn = P("ln");
448574
449575
login_check_credentials();
450576
if( !g.perm.RdTkt && !g.perm.RdWiki ){
451577
login_needed(g.anon.RdTkt || g.anon.RdWiki);
452578
return;
453579
}
454580
rid = name_to_rid_www("name");
455581
if( rid==0 ){ fossil_redirect_home(); }
456
- zUuid = db_text("", "SELECT uuid FROM blob WHERE rid=%d", rid);
582
+ zUuid = rid_to_uuid(rid);
457583
pAttach = manifest_get(rid, CFTYPE_ATTACHMENT, 0);
458584
if( pAttach==0 ) fossil_redirect_home();
585
+ bUserIsOwner =
586
+ 0==fossil_strcmp(pAttach->zUser, login_name())
587
+ && login_is_individual();
459588
zTarget = pAttach->zAttachTarget;
460589
zSrc = pAttach->zAttachSrc;
461590
ridSrc = db_int(0,"SELECT rid FROM blob WHERE uuid='%q'", zSrc);
462591
zName = pAttach->zAttachName;
463592
zDesc = pAttach->zComment;
464593
zMime = mimetype_from_name(zName);
465594
fShowContent = zMime ? strncmp(zMime,"text/", 5)==0 : 0;
466
- if( validate16(zTarget, strlen(zTarget))
595
+ if( db_int(0,"SELECT 1 FROM event WHERE objid=%d and type='f'", rid) ){
596
+ if( !g.perm.RdForum ){ login_needed(g.anon.RdForum); return; }
597
+ showDelMenu = g.perm.Admin || bUserIsOwner;
598
+ zForumPost = zTarget;
599
+ }else if( validate16(zTarget, strlen(zTarget))
467600
&& db_exists("SELECT 1 FROM ticket WHERE tkt_uuid='%q'", zTarget)
468601
){
469
- zTktUuid = zTarget;
470
- if( !g.perm.RdTkt ){ login_needed(g.anon.RdTkt); return; }
471
- if( g.perm.WrTkt ){
472
- style_submenu_element("Delete", "%R/ainfo/%s?del", zUuid);
473
- }
474
- }else if( db_exists("SELECT 1 FROM tag WHERE tagname='wiki-%q'",zTarget) ){
475
- zWikiName = zTarget;
476
- if( !g.perm.RdWiki ){ login_needed(g.anon.RdWiki); return; }
477
- if( g.perm.WrWiki ){
478
- style_submenu_element("Delete", "%R/ainfo/%s?del", zUuid);
479
- }
480
- }else if( db_exists("SELECT 1 FROM tag WHERE tagname='event-%q'",zTarget) ){
481
- zTNUuid = zTarget;
482
- if( !g.perm.RdWiki ){ login_needed(g.anon.RdWiki); return; }
483
- if( g.perm.Write && g.perm.WrWiki ){
484
- style_submenu_element("Delete", "%R/ainfo/%s?del", zUuid);
485
- }
602
+ if( !g.perm.RdTkt ){ login_needed(g.anon.RdTkt); return; }
603
+ zTktUuid = zTarget;
604
+ showDelMenu = g.perm.WrTkt;
605
+ }else if( db_exists("SELECT 1 FROM tag WHERE tagname='wiki-%q'",zTarget) ){
606
+ if( !g.perm.RdWiki ){ login_needed(g.anon.RdWiki); return; }
607
+ zWikiName = zTarget;
608
+ showDelMenu = g.perm.WrWiki;
609
+ }else if( db_exists("SELECT 1 FROM tag WHERE tagname='event-%q'",zTarget) ){
610
+ if( !g.perm.RdWiki ){ login_needed(g.anon.RdWiki); return; }
611
+ zTNUuid = zTarget;
612
+ showDelMenu = g.perm.Write && g.perm.WrWiki;
613
+ }
614
+ if( showDelMenu ){
615
+ style_submenu_element("Delete", "%R/ainfo/%s?del", zUuid);
486616
}
487617
zDate = db_text(0, "SELECT datetime(%.12f)", pAttach->rDate);
488618
489619
if( P("confirm")
490
- && ((zTktUuid && g.perm.WrTkt) ||
620
+ && cgi_csrf_safe(2)
621
+ && ((zForumPost
622
+ && ((bUserIsOwner && g.perm.AttachForum) ||
623
+ forumpost_may_close())) ||
624
+ (zTktUuid && g.perm.WrTkt) ||
491625
(zWikiName && g.perm.WrWiki) ||
492626
(zTNUuid && g.perm.Write && g.perm.WrWiki))
493627
){
628
+ /* Delete attachment. */
494629
int i, n, rid;
495
- char *zDate;
630
+ char *zNewDate;
496631
Blob manifest;
497632
Blob cksum;
498633
const char *zFile = zName;
499634
635
+ if( !bUserIsOwner ){
636
+ if( zForumPost ? !forumpost_may_close() : !g.perm.Admin ){
637
+ webpage_error("Only admins can delete other users' attachments.");
638
+ }
639
+ }
500640
db_begin_transaction();
501641
blob_zero(&manifest);
502642
for(i=n=0; zFile[i]; i++){
503643
if( zFile[i]=='/' || zFile[i]=='\\' ) n = i;
504644
}
505645
zFile += n;
506646
if( zFile[0]==0 ) zFile = "unknown";
507647
blob_appendf(&manifest, "A %F %F\n", zFile, zTarget);
508
- zDate = date_in_standard_format("now");
509
- blob_appendf(&manifest, "D %s\n", zDate);
648
+ zNewDate = date_in_standard_format("now");
649
+ blob_appendf(&manifest, "D %s\n", zNewDate);
510650
blob_appendf(&manifest, "U %F\n", login_name());
511651
md5sum_blob(&manifest, &cksum);
512652
blob_appendf(&manifest, "Z %b\n", &cksum);
513653
rid = content_put(&manifest);
514654
manifest_crosslink(rid, &manifest, MC_NONE);
515655
db_end_transaction(0);
516656
@ <p>The attachment below has been deleted.</p>
657
+ fossil_free(zNewDate);
517658
}
518659
519660
if( P("del")
520
- && ((zTktUuid && g.perm.WrTkt) ||
661
+ && ((zForumPost && (bUserIsOwner || forumpost_may_close())) ||
662
+ (zTktUuid && g.perm.WrTkt) ||
521663
(zWikiName && g.perm.WrWiki) ||
522664
(zTNUuid && g.perm.Write && g.perm.WrWiki))
523665
){
524666
form_begin(0, "%R/ainfo/%!S", zUuid);
525667
@ <p>Confirm you want to delete the attachment shown below.
526668
@ <input type="submit" name="confirm" value="Confirm">
669
+ login_insert_csrf_secret();
527670
@ </form>
528671
}
529672
530673
isModerator = g.perm.Admin ||
531
- (zTktUuid && g.perm.ModTkt) ||
532
- (zWikiName && g.perm.ModWiki);
533
- if( isModerator && (zModAction = P("modaction"))!=0 ){
674
+ (zForumPost && g.perm.ModForum) ||
675
+ (zTktUuid && g.perm.ModTkt) ||
676
+ (zWikiName && g.perm.ModWiki);
677
+ zModAction = P("modaction");
678
+ if( zModAction!=0 && cgi_csrf_safe(2) ){
534679
if( strcmp(zModAction,"delete")==0 ){
535
- moderation_disapprove(rid);
536
- if( zTktUuid ){
680
+ if( isModerator || bUserIsOwner ){
681
+ moderation_disapprove(rid);
682
+ }
683
+ if( zForumPost ){
684
+ cgi_redirectf("%R/forumpost/%!S", zForumPost);
685
+ }else if( zTktUuid ){
537686
cgi_redirectf("%R/tktview/%!S", zTktUuid);
538
- }else{
687
+ }else if( zWikiName ) {
539688
cgi_redirectf("%R/wiki?name=%t", zWikiName);
540689
}
690
+ /* zTNUuid is intentionally unhandled. Tech note attachments
691
+ ** don't go through moderation. */
541692
return;
542693
}
543
- if( strcmp(zModAction,"approve")==0 ){
694
+ if( isModerator && strcmp(zModAction,"approve")==0 ){
544695
moderation_approve('a', rid);
545696
}
546697
}
547698
style_set_current_feature("attach");
548699
style_header("Attachment Details");
@@ -558,19 +709,20 @@
558709
@ <td>%z(href("%R/artifact/%!S",zUuid))%s(zUuid)</a>
559710
if( g.perm.Setup ){
560711
@ (%d(rid))
561712
}
562713
modPending = moderation_pending_www(rid);
563
- if( zTktUuid ){
714
+ if( zForumPost ){
715
+ @ <tr><th>Forum&nbsp;Post:</th>
716
+ @ <td>%z(href("%R/forumpost/%s",zForumPost))%h(zForumPost)</a></td></tr>
717
+ }else if( zTktUuid ){
564718
@ <tr><th>Ticket:</th>
565719
@ <td>%z(href("%R/tktview/%s",zTktUuid))%s(zTktUuid)</a></td></tr>
566
- }
567
- if( zTNUuid ){
720
+ }else if( zTNUuid ){
568721
@ <tr><th>Tech Note:</th>
569722
@ <td>%z(href("%R/technote/%s",zTNUuid))%s(zTNUuid)</a></td></tr>
570
- }
571
- if( zWikiName ){
723
+ }else if( zWikiName ){
572724
@ <tr><th>Wiki&nbsp;Page:</th>
573725
@ <td>%z(href("%R/wiki?name=%t",zWikiName))%h(zWikiName)</a></td></tr>
574726
}
575727
@ <tr><th>Date:</th><td>
576728
hyperlink_to_date(zDate, "</td></tr>");
@@ -586,66 +738,89 @@
586738
@ <tr><th>MIME-Type:</th><td>%h(zMime)</td></tr>
587739
}
588740
@ <tr><th valign="top">Description:</th><td valign="top">%h(zDesc)</td></tr>
589741
@ </table>
590742
591
- if( isModerator && modPending ){
743
+ if( modPending && (isModerator || bUserIsOwner) ){
592744
@ <div class="section">Moderation</div>
593745
@ <blockquote>
594746
form_begin(0, "%R/ainfo/%s", zUuid);
595747
@ <label><input type="radio" name="modaction" value="delete">
596
- @ Delete this change</label><br>
597
- @ <label><input type="radio" name="modaction" value="approve">
598
- @ Approve this change</label><br>
748
+ @ Delete this attachment</label><br>
749
+ if( isModerator ){
750
+ @ <label><input type="radio" name="modaction" value="approve">
751
+ @ Approve this attachment</label><br>
752
+ }
599753
@ <input type="submit" value="Submit">
754
+ login_insert_csrf_secret();
600755
@ </form>
601756
@ </blockquote>
602757
}
603758
604
- @ <div class="section">Content Appended</div>
605
- @ <blockquote>
759
+ @ <div class="section">Content:</div>
606760
blob_zero(&attach);
607
- if( fShowContent ){
608
- const char *z;
609
- content_get(ridSrc, &attach);
610
- blob_to_utf8_no_bom(&attach, 0);
611
- z = blob_str(&attach);
612
- if( zLn ){
613
- output_text_with_line_numbers(z, blob_size(&attach), zName, zLn, 1);
614
- }else{
615
- @ <pre>
616
- @ %h(z)
617
- @ </pre>
618
- }
619
- }else if( strncmp(zMime, "image/", 6)==0 ){
620
- int sz = db_int(0, "SELECT size FROM blob WHERE rid=%d", ridSrc);
621
- @ <i>(file is %d(sz) bytes of image data)</i><br>
622
- @ <img src="%R/raw/%s(zSrc)?m=%s(zMime)"></img>
623
- style_submenu_element("Image", "%R/raw/%s?m=%s", zSrc, zMime);
624
- }else{
625
- int sz = db_int(0, "SELECT size FROM blob WHERE rid=%d", ridSrc);
626
- @ <i>(file is %d(sz) bytes of binary data)</i>
627
- }
628
- @ </blockquote>
761
+ if( modPending && !moderation_user_could(rid, 1, 0) ){
762
+ @ <p><span class="modpending">Content is awaiting moderator \
763
+ @ approval.</span></p>
764
+ }else{
765
+ @ <blockquote>
766
+ if( fShowContent ){
767
+ const char *z;
768
+ content_get(ridSrc, &attach);
769
+ blob_to_utf8_no_bom(&attach, 0);
770
+ z = blob_str(&attach);
771
+ if( zLn ){
772
+ output_text_with_line_numbers(z, blob_size(&attach), zName, zLn, 1);
773
+ }else{
774
+ @ <pre>
775
+ @ %h(z)
776
+ @ </pre>
777
+ }
778
+ }else if( strncmp(zMime, "image/", 6)==0 ){
779
+ int sz = db_int(0, "SELECT size FROM blob WHERE rid=%d", ridSrc);
780
+ @ <i>(file is %d(sz) bytes of image data)</i><br>
781
+ @ <img src="%R/raw/%s(zSrc)?m=%s(zMime)"></img>
782
+ style_submenu_element("Image", "%R/raw/%s?m=%s", zSrc, zMime);
783
+ }else{
784
+ int sz = db_int(0, "SELECT size FROM blob WHERE rid=%d", ridSrc);
785
+ @ <i>(file is %d(sz) bytes of binary data)</i>
786
+ }
787
+ @ </blockquote>
788
+ }
629789
manifest_destroy(pAttach);
630790
blob_reset(&attach);
631791
style_finish_page();
632792
}
793
+
794
+#if INTERFACE
795
+/*
796
+** Flags for use with attachment_list(). ATTACHLIST_HRULE_ABOVE
797
+** must have a value of 1 for historical call compatibility.
798
+*/
799
+#define ATTACHLIST_HRULE_ABOVE 0x01 /* Insert <hr> above header */
800
+#define ATTACHLIST_TARGET_BLANK 0x02 /* use target=_blank for links */
801
+#define ATTACHLIST_SIZE 0x04 /* add size */
802
+#define ATTACHLIST_HIDE_UNAPPROVED 0x08 /* Hide pending-moderation files */
803
+#endif
633804
634805
/*
635806
** Output HTML to show a list of attachments.
636807
*/
637808
void attachment_list(
638809
const char *zTarget, /* Object that things are attached to */
639810
const char *zHeader, /* Header to display with attachments */
640
- int fHorizontalRule /* Insert <hr> separator above header */
811
+ const int flags /* ATTACHLIST_... flags */
641812
){
642813
int cnt = 0;
814
+ char szBuf[36] = {0}; /* scratchpad for attachment size value */
815
+ const char * zLinkTgt = (ATTACHLIST_TARGET_BLANK & flags)
816
+ ? " target=\"_blank\"" : "";
643817
Stmt q;
644818
db_prepare(&q,
645819
"SELECT datetime(mtime,toLocal()), filename, user,"
646
- " (SELECT uuid FROM blob WHERE rid=attachid), src"
820
+ " (SELECT uuid FROM blob WHERE rid=attachid), src, target, "
821
+ " attachid "
647822
" FROM attachment"
648823
" WHERE isLatest AND src!='' AND target=%Q"
649824
" ORDER BY mtime DESC",
650825
zTarget
651826
);
@@ -653,34 +828,55 @@
653828
const char *zDate = db_column_text(&q, 0);
654829
const char *zFile = db_column_text(&q, 1);
655830
const char *zUser = db_column_text(&q, 2);
656831
const char *zUuid = db_column_text(&q, 3);
657832
const char *zSrc = db_column_text(&q, 4);
833
+ const char *zTarget = db_column_text(&q, 5);
658834
const char *zDispUser = zUser && zUser[0] ? zUser : "anonymous";
835
+ const char *zTypeArg = 0; /* URL arg name for /attachdownload */
836
+ const int aid = db_column_int(&q, 6);
837
+ const int iAType = attachment_target_type(zTarget);
838
+ if( (flags & ATTACHLIST_HIDE_UNAPPROVED)
839
+ && moderation_pending(aid)
840
+ && !moderation_user_could(aid, 1, 0) ){
841
+ continue;
842
+ }
659843
if( cnt==0 ){
660844
@ <section class='attachlist'>
661
- if( fHorizontalRule ){
845
+ if( flags & ATTACHLIST_HRULE_ABOVE ){
662846
@ <hr>
663847
}
664848
@ %s(zHeader)
665849
@ <ul>
666850
}
667851
cnt++;
852
+ switch( iAType ){
853
+ case CFTYPE_TICKET: zTypeArg = "tkt"; break;
854
+ case CFTYPE_FORUM: zTypeArg = "forumpost"; break;
855
+ case CFTYPE_EVENT: zTypeArg = "technote"; break;
856
+ case CFTYPE_WIKI:
857
+ default: zTypeArg = "page"; break;
858
+ }
668859
@ <li>
669
- @ %z(href("%R/artifact/%!S",zSrc))%h(zFile)</a>
670
- @ [<a href="%R/attachdownload/%t(zFile)?page=%t(zTarget)&file=%t(zFile)">download</a>]
860
+ @ <a href="%R/artifact/%!S(zSrc)"%s(zLinkTgt)>%h(zFile)</a>
861
+ if( flags & ATTACHLIST_SIZE ){
862
+ const int sz = db_int(0,"SELECT size FROM blob WHERE uuid=%Q", zSrc);
863
+ sqlite3_snprintf(sizeof(szBuf), szBuf, " %d bytes", sz);
864
+ }
865
+ @ [<a href="%R/attachdownload/%t(zFile)?%s(zTypeArg)=%t(zTarget)\
866
+ @&file=%t(zFile)%s(zLinkTgt)">download</a>%s(szBuf)]
671867
@ added by %h(zDispUser) on
672868
hyperlink_to_date(zDate, ".");
673
- @ [%z(href("%R/ainfo/%!S",zUuid))details</a>]
869
+ @ [<a href="%R/ainfo/%!S(zUuid)"%s(zLinkTgt)>details</a>]
870
+ moderation_pending_www(aid);
674871
@ </li>
675872
}
676873
if( cnt ){
677874
@ </ul>
678875
@ </section>
679876
}
680877
db_finalize(&q);
681
-
682878
}
683879
684880
/*
685881
** COMMAND: attachment*
686882
**
687883
--- src/attach.c
+++ src/attach.c
@@ -18,32 +18,71 @@
18 ** This file contains code for dealing with attachments.
19 */
20 #include "config.h"
21 #include "attach.h"
22 #include <assert.h>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
24 /*
25 ** WEBPAGE: attachlist
26 ** List attachments.
27 **
28 ** tkt=HASH
29 ** page=WIKIPAGE
30 ** technote=HASH
 
31 **
32 ** At most one of technote=, tkt= or page= may be supplied.
33 **
34 ** If none are given, all attachments are listed. If one is given, only
35 ** attachments for the designated technote, ticket or wiki page are shown.
36 **
37 ** HASH may be just a prefix of the relevant technical note or ticket
38 ** artifact hash, in which case all attachments of all technical notes or
39 ** tickets with the prefix will be listed.
 
40 */
41 void attachlist_page(void){
42 const char *zPage = P("page");
43 const char *zTkt = P("tkt");
44 const char *zTechNote = P("technote");
 
45 Blob sql;
46 Stmt q;
47
48 if( zPage && zTkt ) zTkt = 0;
49 login_check_credentials();
@@ -50,29 +89,34 @@
50 style_set_current_feature("attach");
51 blob_zero(&sql);
52 blob_append_sql(&sql,
53 "SELECT datetime(mtime,toLocal()), src, target, filename,"
54 " comment, user,"
55 " (SELECT uuid FROM blob WHERE rid=attachid), attachid,"
56 " (CASE WHEN 'tkt-'||target IN (SELECT tagname FROM tag)"
57 " THEN 1"
58 " WHEN 'event-'||target IN (SELECT tagname FROM tag)"
59 " THEN 2"
60 " ELSE 0 END)"
61 " FROM attachment"
62 );
63 if( zPage ){
 
 
 
 
 
 
 
 
 
 
64 if( g.perm.RdWiki==0 ){ login_needed(g.anon.RdWiki); return; }
65 style_header("Attachments To %h", zPage);
66 blob_append_sql(&sql, " WHERE target=%Q", zPage);
67 }else if( zTkt ){
68 if( g.perm.RdTkt==0 ){ login_needed(g.anon.RdTkt); return; }
69 style_header("Attachments To Ticket %S", zTkt);
70 blob_append_sql(&sql, " WHERE target GLOB '%q*'", zTkt);
71 }else if( zTechNote ){
72 if( g.perm.RdWiki==0 ){ login_needed(g.anon.RdWiki); return; }
73 style_header("Attachments to Tech Note %S", zTechNote);
74 blob_append_sql(&sql, " WHERE target GLOB '%q*'",
75 zTechNote);
76 }else{
77 if( g.perm.RdTkt==0 && g.perm.RdWiki==0 ){
78 login_needed(g.anon.RdTkt || g.anon.RdWiki);
@@ -82,35 +126,58 @@
82 }
83 blob_append_sql(&sql, " ORDER BY mtime DESC");
84 db_prepare(&q, "%s", blob_sql_text(&sql));
85 @ <ol>
86 while( db_step(&q)==SQLITE_ROW ){
87 const char *zDate = db_column_text(&q, 0);
88 const char *zSrc = db_column_text(&q, 1);
89 const char *zTarget = db_column_text(&q, 2);
90 const char *zFilename = db_column_text(&q, 3);
91 const char *zComment = db_column_text(&q, 4);
92 const char *zUser = db_column_text(&q, 5);
93 const char *zUuid = db_column_text(&q, 6);
94 int attachid = db_column_int(&q, 7);
95 /* type 0 is a wiki page, 1 is a ticket, 2 is a tech note */
96 int type = db_column_int(&q, 8);
97 const char *zDispUser = zUser && zUser[0] ? zUser : "anonymous";
98 int i;
99 char *zUrlTail;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100 for(i=0; zFilename[i]; i++){
101 if( zFilename[i]=='/' && zFilename[i+1]!=0 ){
102 zFilename = &zFilename[i+1];
103 i = -1;
104 }
105 }
106 if( type==1 ){
107 zUrlTail = mprintf("tkt=%s&file=%t", zTarget, zFilename);
108 }else if( type==2 ){
109 zUrlTail = mprintf("technote=%s&file=%t", zTarget, zFilename);
110 }else{
111 zUrlTail = mprintf("page=%t&file=%t", zTarget, zFilename);
 
 
 
 
 
 
 
 
112 }
113 @ <li><p>
114 @ Attachment %z(href("%R/ainfo/%!S",zUuid))%S(zUuid)</a>
115 moderation_pending_www(attachid);
116 @ <br><a href="%R/attachview?%s(zUrlTail)">%h(zFilename)</a>
@@ -117,25 +184,37 @@
117 @ [<a href="%R/attachdownload/%t(zFilename)?%s(zUrlTail)">download</a>]<br>
118 if( zComment ) while( fossil_isspace(zComment[0]) ) zComment++;
119 if( zComment && zComment[0] ){
120 @ %!W(zComment)<br>
121 }
122 if( zPage==0 && zTkt==0 && zTechNote==0 ){
123 if( zSrc==0 || zSrc[0]==0 ){
124 zSrc = "Deleted from";
125 }else {
126 zSrc = "Added to";
127 }
128 if( type==1 ){
129 @ %s(zSrc) ticket <a href="%R/tktview?name=%s(zTarget)">
130 @ %S(zTarget)</a>
131 }else if( type==2 ){
132 @ %s(zSrc) tech note <a href="%R/technote/%s(zTarget)">
133 @ %S(zTarget)</a>
134 }else{
135 @ %s(zSrc) wiki page <a href="%R/wiki?name=%t(zTarget)">
136 @ %h(zTarget)</a>
 
 
 
 
 
 
 
 
 
 
 
 
137 }
138 }else{
139 if( zSrc==0 || zSrc[0]==0 ){
140 @ Deleted
141 }else {
@@ -162,27 +241,35 @@
162 ** Query parameters:
163 **
164 ** tkt=HASH
165 ** page=WIKIPAGE
166 ** technote=HASH
 
167 ** file=FILENAME
168 ** attachid=ID
169 **
170 */
171 void attachview_page(void){
172 const char *zPage = P("page");
173 const char *zTkt = P("tkt");
174 const char *zTechNote = P("technote");
 
175 const char *zFile = P("file");
176 const char *zTarget = 0;
177 int attachid = atoi(PD("attachid","0"));
178 char *zUUID;
179
180 if( zFile==0 ) fossil_redirect_home();
181 login_check_credentials();
182 style_set_current_feature("attach");
183 if( zPage ){
 
 
 
 
 
 
184 if( g.perm.RdWiki==0 ){ login_needed(g.anon.RdWiki); return; }
185 zTarget = zPage;
186 }else if( zTkt ){
187 if( g.perm.RdTkt==0 ){ login_needed(g.anon.RdTkt); return; }
188 zTarget = zTkt;
@@ -314,35 +401,60 @@
314 ** Add a new attachment.
315 **
316 ** tkt=HASH
317 ** page=WIKIPAGE
318 ** technote=HASH
 
319 ** from=URL
320 **
321 */
322 void attachadd_page(void){
323 const char *zPage = P("page");
 
324 const char *zTkt = P("tkt");
325 const char *zTechNote = P("technote");
326 const char *zFrom = P("from");
327 const char *aContent = P("f");
328 const char *zName = PD("f:filename","unknown");
 
329 const char *zTarget;
330 char *zTargetType;
 
 
331 int szContent = atoi(PD("f:bytes","0"));
332 int goodCaptcha = 1;
 
333
 
334 if( P("cancel") ) cgi_redirect(zFrom);
335 if( (zPage && zTkt)
336 || (zPage && zTechNote)
337 || (zTkt && zTechNote)
338 ){
339 fossil_redirect_home();
340 }
341 if( zPage==0 && zTkt==0 && zTechNote==0) fossil_redirect_home();
342 login_check_credentials();
343 if( zPage ){
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
344 if( g.perm.ApndWiki==0 || g.perm.Attach==0 ){
345 login_needed(g.anon.ApndWiki && g.anon.Attach);
346 return;
347 }
348 if( !db_exists("SELECT 1 FROM tag WHERE tagname='wiki-%q'", zPage) ){
@@ -364,10 +476,11 @@
364 zTarget = zTechNote;
365 zTargetType = mprintf("Tech Note <a href=\"%R/technote/%s\">%S</a>",
366 zTechNote, zTechNote);
367
368 }else{
 
369 if( g.perm.ApndTkt==0 || g.perm.Attach==0 ){
370 login_needed(g.anon.ApndTkt && g.anon.Attach);
371 return;
372 }
373 if( !db_exists("SELECT 1 FROM tag WHERE tagname='tkt-%q'", zTkt) ){
@@ -377,21 +490,25 @@
377 }
378 zTarget = zTkt;
379 zTargetType = mprintf("Ticket <a href=\"%R/tktview/%s\">%S</a>",
380 zTkt, zTkt);
381 }
382 if( zFrom==0 ) zFrom = mprintf("%R/home");
383 if( P("cancel") ){
384 cgi_redirect(zFrom);
385 }
386 if( P("ok") && szContent>0 && (goodCaptcha = captcha_is_correct(0)) ){
387 int needModerator = (zTkt!=0 && ticket_need_moderation(0)) ||
 
 
 
 
388 (zPage!=0 && wiki_need_moderation(0));
389 const char *zComment = PD("comment", "");
390 attach_commit(zName, zTarget, aContent, szContent, needModerator, zComment);
391 cgi_redirect(zFrom);
392 }
 
393 style_set_current_feature("attach");
394 style_header("Add Attachment");
395 if( !goodCaptcha ){
396 @ <p class="generalError">Error: Incorrect security code.</p>
397 }
@@ -399,12 +516,15 @@
399 form_begin("enctype='multipart/form-data'", "%R/attachadd");
400 @ <div>
401 @ File to Attach:
402 @ <input type="file" name="f" size="60"><br>
403 @ Description:<br>
404 @ <textarea name="comment" cols="80" rows="5" wrap="virtual"></textarea><br>
405 if( zTkt ){
 
 
 
406 @ <input type="hidden" name="tkt" value="%h(zTkt)">
407 }else if( zTechNote ){
408 @ <input type="hidden" name="technote" value="%h(zTechNote)">
409 }else{
410 @ <input type="hidden" name="page" value="%h(zPage)">
@@ -415,15 +535,18 @@
415 @ </div>
416 captcha_generate(0);
417 @ </form>
418 style_finish_page();
419 fossil_free(zTargetType);
 
420 }
421
422 /*
423 ** WEBPAGE: ainfo
424 ** URL: /ainfo?name=ARTIFACTID
 
 
425 **
426 ** Show the details of an attachment artifact.
427 */
428 void ainfo_page(void){
429 int rid; /* RID for the control artifact */
@@ -436,113 +559,141 @@
436 const char *zName; /* Name of the attached file */
437 const char *zDesc; /* Description of the attached file */
438 const char *zWikiName = 0; /* Wiki page name when attached to Wiki */
439 const char *zTNUuid = 0; /* Tech Note ID when attached to tech note */
440 const char *zTktUuid = 0; /* Ticket ID when attached to a ticket */
 
441 int modPending; /* True if awaiting moderation */
442 const char *zModAction; /* Moderation action or NULL */
443 int isModerator; /* TRUE if user is the moderator */
444 const char *zMime; /* MIME Type */
445 Blob attach; /* Content of the attachment */
446 int fShowContent = 0;
 
 
447 const char *zLn = P("ln");
448
449 login_check_credentials();
450 if( !g.perm.RdTkt && !g.perm.RdWiki ){
451 login_needed(g.anon.RdTkt || g.anon.RdWiki);
452 return;
453 }
454 rid = name_to_rid_www("name");
455 if( rid==0 ){ fossil_redirect_home(); }
456 zUuid = db_text("", "SELECT uuid FROM blob WHERE rid=%d", rid);
457 pAttach = manifest_get(rid, CFTYPE_ATTACHMENT, 0);
458 if( pAttach==0 ) fossil_redirect_home();
 
 
 
459 zTarget = pAttach->zAttachTarget;
460 zSrc = pAttach->zAttachSrc;
461 ridSrc = db_int(0,"SELECT rid FROM blob WHERE uuid='%q'", zSrc);
462 zName = pAttach->zAttachName;
463 zDesc = pAttach->zComment;
464 zMime = mimetype_from_name(zName);
465 fShowContent = zMime ? strncmp(zMime,"text/", 5)==0 : 0;
466 if( validate16(zTarget, strlen(zTarget))
 
 
 
 
467 && db_exists("SELECT 1 FROM ticket WHERE tkt_uuid='%q'", zTarget)
468 ){
469 zTktUuid = zTarget;
470 if( !g.perm.RdTkt ){ login_needed(g.anon.RdTkt); return; }
471 if( g.perm.WrTkt ){
472 style_submenu_element("Delete", "%R/ainfo/%s?del", zUuid);
473 }
474 }else if( db_exists("SELECT 1 FROM tag WHERE tagname='wiki-%q'",zTarget) ){
475 zWikiName = zTarget;
476 if( !g.perm.RdWiki ){ login_needed(g.anon.RdWiki); return; }
477 if( g.perm.WrWiki ){
478 style_submenu_element("Delete", "%R/ainfo/%s?del", zUuid);
479 }
480 }else if( db_exists("SELECT 1 FROM tag WHERE tagname='event-%q'",zTarget) ){
481 zTNUuid = zTarget;
482 if( !g.perm.RdWiki ){ login_needed(g.anon.RdWiki); return; }
483 if( g.perm.Write && g.perm.WrWiki ){
484 style_submenu_element("Delete", "%R/ainfo/%s?del", zUuid);
485 }
486 }
487 zDate = db_text(0, "SELECT datetime(%.12f)", pAttach->rDate);
488
489 if( P("confirm")
490 && ((zTktUuid && g.perm.WrTkt) ||
 
 
 
 
491 (zWikiName && g.perm.WrWiki) ||
492 (zTNUuid && g.perm.Write && g.perm.WrWiki))
493 ){
 
494 int i, n, rid;
495 char *zDate;
496 Blob manifest;
497 Blob cksum;
498 const char *zFile = zName;
499
 
 
 
 
 
500 db_begin_transaction();
501 blob_zero(&manifest);
502 for(i=n=0; zFile[i]; i++){
503 if( zFile[i]=='/' || zFile[i]=='\\' ) n = i;
504 }
505 zFile += n;
506 if( zFile[0]==0 ) zFile = "unknown";
507 blob_appendf(&manifest, "A %F %F\n", zFile, zTarget);
508 zDate = date_in_standard_format("now");
509 blob_appendf(&manifest, "D %s\n", zDate);
510 blob_appendf(&manifest, "U %F\n", login_name());
511 md5sum_blob(&manifest, &cksum);
512 blob_appendf(&manifest, "Z %b\n", &cksum);
513 rid = content_put(&manifest);
514 manifest_crosslink(rid, &manifest, MC_NONE);
515 db_end_transaction(0);
516 @ <p>The attachment below has been deleted.</p>
 
517 }
518
519 if( P("del")
520 && ((zTktUuid && g.perm.WrTkt) ||
 
521 (zWikiName && g.perm.WrWiki) ||
522 (zTNUuid && g.perm.Write && g.perm.WrWiki))
523 ){
524 form_begin(0, "%R/ainfo/%!S", zUuid);
525 @ <p>Confirm you want to delete the attachment shown below.
526 @ <input type="submit" name="confirm" value="Confirm">
 
527 @ </form>
528 }
529
530 isModerator = g.perm.Admin ||
531 (zTktUuid && g.perm.ModTkt) ||
532 (zWikiName && g.perm.ModWiki);
533 if( isModerator && (zModAction = P("modaction"))!=0 ){
 
 
534 if( strcmp(zModAction,"delete")==0 ){
535 moderation_disapprove(rid);
536 if( zTktUuid ){
 
 
 
 
537 cgi_redirectf("%R/tktview/%!S", zTktUuid);
538 }else{
539 cgi_redirectf("%R/wiki?name=%t", zWikiName);
540 }
 
 
541 return;
542 }
543 if( strcmp(zModAction,"approve")==0 ){
544 moderation_approve('a', rid);
545 }
546 }
547 style_set_current_feature("attach");
548 style_header("Attachment Details");
@@ -558,19 +709,20 @@
558 @ <td>%z(href("%R/artifact/%!S",zUuid))%s(zUuid)</a>
559 if( g.perm.Setup ){
560 @ (%d(rid))
561 }
562 modPending = moderation_pending_www(rid);
563 if( zTktUuid ){
 
 
 
564 @ <tr><th>Ticket:</th>
565 @ <td>%z(href("%R/tktview/%s",zTktUuid))%s(zTktUuid)</a></td></tr>
566 }
567 if( zTNUuid ){
568 @ <tr><th>Tech Note:</th>
569 @ <td>%z(href("%R/technote/%s",zTNUuid))%s(zTNUuid)</a></td></tr>
570 }
571 if( zWikiName ){
572 @ <tr><th>Wiki&nbsp;Page:</th>
573 @ <td>%z(href("%R/wiki?name=%t",zWikiName))%h(zWikiName)</a></td></tr>
574 }
575 @ <tr><th>Date:</th><td>
576 hyperlink_to_date(zDate, "</td></tr>");
@@ -586,66 +738,89 @@
586 @ <tr><th>MIME-Type:</th><td>%h(zMime)</td></tr>
587 }
588 @ <tr><th valign="top">Description:</th><td valign="top">%h(zDesc)</td></tr>
589 @ </table>
590
591 if( isModerator && modPending ){
592 @ <div class="section">Moderation</div>
593 @ <blockquote>
594 form_begin(0, "%R/ainfo/%s", zUuid);
595 @ <label><input type="radio" name="modaction" value="delete">
596 @ Delete this change</label><br>
597 @ <label><input type="radio" name="modaction" value="approve">
598 @ Approve this change</label><br>
 
 
599 @ <input type="submit" value="Submit">
 
600 @ </form>
601 @ </blockquote>
602 }
603
604 @ <div class="section">Content Appended</div>
605 @ <blockquote>
606 blob_zero(&attach);
607 if( fShowContent ){
608 const char *z;
609 content_get(ridSrc, &attach);
610 blob_to_utf8_no_bom(&attach, 0);
611 z = blob_str(&attach);
612 if( zLn ){
613 output_text_with_line_numbers(z, blob_size(&attach), zName, zLn, 1);
614 }else{
615 @ <pre>
616 @ %h(z)
617 @ </pre>
618 }
619 }else if( strncmp(zMime, "image/", 6)==0 ){
620 int sz = db_int(0, "SELECT size FROM blob WHERE rid=%d", ridSrc);
621 @ <i>(file is %d(sz) bytes of image data)</i><br>
622 @ <img src="%R/raw/%s(zSrc)?m=%s(zMime)"></img>
623 style_submenu_element("Image", "%R/raw/%s?m=%s", zSrc, zMime);
624 }else{
625 int sz = db_int(0, "SELECT size FROM blob WHERE rid=%d", ridSrc);
626 @ <i>(file is %d(sz) bytes of binary data)</i>
627 }
628 @ </blockquote>
 
 
 
 
 
 
629 manifest_destroy(pAttach);
630 blob_reset(&attach);
631 style_finish_page();
632 }
 
 
 
 
 
 
 
 
 
 
 
633
634 /*
635 ** Output HTML to show a list of attachments.
636 */
637 void attachment_list(
638 const char *zTarget, /* Object that things are attached to */
639 const char *zHeader, /* Header to display with attachments */
640 int fHorizontalRule /* Insert <hr> separator above header */
641 ){
642 int cnt = 0;
 
 
 
643 Stmt q;
644 db_prepare(&q,
645 "SELECT datetime(mtime,toLocal()), filename, user,"
646 " (SELECT uuid FROM blob WHERE rid=attachid), src"
 
647 " FROM attachment"
648 " WHERE isLatest AND src!='' AND target=%Q"
649 " ORDER BY mtime DESC",
650 zTarget
651 );
@@ -653,34 +828,55 @@
653 const char *zDate = db_column_text(&q, 0);
654 const char *zFile = db_column_text(&q, 1);
655 const char *zUser = db_column_text(&q, 2);
656 const char *zUuid = db_column_text(&q, 3);
657 const char *zSrc = db_column_text(&q, 4);
 
658 const char *zDispUser = zUser && zUser[0] ? zUser : "anonymous";
 
 
 
 
 
 
 
 
659 if( cnt==0 ){
660 @ <section class='attachlist'>
661 if( fHorizontalRule ){
662 @ <hr>
663 }
664 @ %s(zHeader)
665 @ <ul>
666 }
667 cnt++;
 
 
 
 
 
 
 
668 @ <li>
669 @ %z(href("%R/artifact/%!S",zSrc))%h(zFile)</a>
670 @ [<a href="%R/attachdownload/%t(zFile)?page=%t(zTarget)&file=%t(zFile)">download</a>]
 
 
 
 
 
671 @ added by %h(zDispUser) on
672 hyperlink_to_date(zDate, ".");
673 @ [%z(href("%R/ainfo/%!S",zUuid))details</a>]
 
674 @ </li>
675 }
676 if( cnt ){
677 @ </ul>
678 @ </section>
679 }
680 db_finalize(&q);
681
682 }
683
684 /*
685 ** COMMAND: attachment*
686 **
687
--- src/attach.c
+++ src/attach.c
@@ -18,32 +18,71 @@
18 ** This file contains code for dealing with attachments.
19 */
20 #include "config.h"
21 #include "attach.h"
22 #include <assert.h>
23
24 /*
25 ** Given a presumedly legal attachment target name, this guesses the
26 ** target type and returns one of CFTYPE_FORUM, CFTYPE_WIKI,
27 ** CFTYPE_TICKET, or CFTYPE_EVENT. Returns 0 if it cannot
28 ** distinguish the target type.
29 **
30 ** In the case of CFTYPE_FORUM, it is up to the caller to ensure that,
31 ** if needed, they resolve zTarget using forumpost_head_rid2() so that
32 ** they get the RID of the earliest version of the post, as that is
33 ** the only one which attachments should target.
34 */
35 int attachment_target_type(const char *zTarget){
36 static Stmt q = empty_Stmt_m;
37 int rc = 0;
38 if( forumpost_head_rid2(zTarget)>0 ){
39 return CFTYPE_FORUM;
40 }
41 if( !q.pStmt ){
42 db_static_prepare(
43 &q,
44 "SELECT CASE "
45 "WHEN 'tkt-'||:tgt IN (SELECT tagname FROM tag) THEN %d "
46 "WHEN 'event-'||:tgt IN (SELECT tagname FROM tag) THEN %d "
47 "WHEN 'wiki-'||:tgt IN (SELECT tagname FROM tag) THEN %d "
48 "ELSE 0 END",
49 CFTYPE_TICKET, CFTYPE_EVENT, CFTYPE_WIKI
50 );
51 }
52 db_bind_text(&q, ":tgt", zTarget);
53 if( SQLITE_ROW==db_step(&q) ){
54 rc = db_column_int(&q, 0);
55 }
56 db_reset(&q);
57 return rc;
58 }
59
60 /*
61 ** WEBPAGE: attachlist
62 ** List attachments.
63 **
64 ** tkt=HASH
65 ** page=WIKIPAGE
66 ** technote=HASH
67 ** forumpost=HASH
68 **
69 ** At most one of technote=, tkt=, forumpost=, or page= may be supplied.
70 **
71 ** If none are given, all attachments are listed. If one is given, only
72 ** attachments for the designated technote, ticket or wiki page are shown.
73 **
74 ** HASH may be just a prefix of the relevant technical note or ticket
75 ** artifact hash, in which case all attachments of all technical notes or
76 ** tickets with the prefix will be listed. Forum posts, on the other hand,
77 ** require a unique hash prefix.
78 */
79 void attachlist_page(void){
80 const char *zPage = P("page");
81 const char *zTkt = P("tkt");
82 const char *zTechNote = P("technote");
83 const char *zForumPost = P("forumpost");
84 Blob sql;
85 Stmt q;
86
87 if( zPage && zTkt ) zTkt = 0;
88 login_check_credentials();
@@ -50,29 +89,34 @@
89 style_set_current_feature("attach");
90 blob_zero(&sql);
91 blob_append_sql(&sql,
92 "SELECT datetime(mtime,toLocal()), src, target, filename,"
93 " comment, user,"
94 " (SELECT uuid FROM blob WHERE rid=attachid), attachid"
 
 
 
 
 
95 " FROM attachment"
96 );
97 if( zForumPost ){
98 int fnid;
99 if( g.perm.RdForum==0 ){ login_needed(g.anon.RdForum); return; }
100 style_header("Attachments To Forum post %S", zForumPost);
101 fnid = forumpost_head_rid2(zForumPost);
102 if( fnid<=0 ){
103 webpage_error("Invalid forum post ID: %h", zForumPost);
104 }
105 blob_append_sql(&sql, " WHERE target="
106 "(SELECT uuid FROM blob WHERE rid=%d)", fnid);
107 }else if( zPage ){
108 if( g.perm.RdWiki==0 ){ login_needed(g.anon.RdWiki); return; }
109 style_header("Attachments To Wiki page %h", zPage);
110 blob_append_sql(&sql, " WHERE target=%Q", zPage);
111 }else if( zTkt ){
112 if( g.perm.RdTkt==0 ){ login_needed(g.anon.RdTkt); return; }
113 style_header("Attachments To Ticket %S", zTkt);
114 blob_append_sql(&sql, " WHERE target GLOB '%q*'", zTkt);
115 }else if( zTechNote ){
116 if( g.perm.RdWiki==0 ){ login_needed(g.anon.RdWiki); return; }
117 style_header("Attachments To Tech Note %S", zTechNote);
118 blob_append_sql(&sql, " WHERE target GLOB '%q*'",
119 zTechNote);
120 }else{
121 if( g.perm.RdTkt==0 && g.perm.RdWiki==0 ){
122 login_needed(g.anon.RdTkt || g.anon.RdWiki);
@@ -82,35 +126,58 @@
126 }
127 blob_append_sql(&sql, " ORDER BY mtime DESC");
128 db_prepare(&q, "%s", blob_sql_text(&sql));
129 @ <ol>
130 while( db_step(&q)==SQLITE_ROW ){
131 const char *zDate;
132 const char *zSrc;
133 const char *zTarget;
134 const char *zFilename;
135 const char *zComment;
136 const char *zUser;
137 const char *zUuid;
138 const char *zDispUser;
139 const int attachid = db_column_int(&q, 7);
140 int type;
 
141 int i;
142 char *zUrlTail = 0;
143
144 if( moderation_pending(attachid)
145 && !moderation_user_could(attachid, 1, 0) ){
146 /* Elide entries which are currently pending moderation unless
147 ** the user would be able to moderate the entry themselves. */
148 continue;
149 }
150
151 zDate = db_column_text(&q, 0);
152 zSrc = db_column_text(&q, 1);
153 zTarget = db_column_text(&q, 2);
154 zFilename = db_column_text(&q, 3);
155 zComment = db_column_text(&q, 4);
156 zUser = db_column_text(&q, 5);
157 zUuid = db_column_text(&q, 6);
158 zDispUser = zUser && zUser[0] ? zUser : "anonymous";
159 for(i=0; zFilename[i]; i++){
160 if( zFilename[i]=='/' && zFilename[i+1]!=0 ){
161 zFilename = &zFilename[i+1];
162 i = -1;
163 }
164 }
165 type = attachment_target_type(zTarget);
166 switch( type ){
167 case CFTYPE_TICKET:
168 zUrlTail = mprintf("tkt=%s&file=%t", zTarget, zFilename);
169 break;
170 case CFTYPE_EVENT:
171 zUrlTail = mprintf("technote=%s&file=%t", zTarget, zFilename);
172 break;
173 case CFTYPE_FORUM:
174 zUrlTail = mprintf("forumpost=%t&file=%t", zTarget, zFilename);
175 break;
176 case CFTYPE_WIKI:
177 zUrlTail = mprintf("page=%t&file=%t", zTarget, zFilename);
178 break;
179 }
180 @ <li><p>
181 @ Attachment %z(href("%R/ainfo/%!S",zUuid))%S(zUuid)</a>
182 moderation_pending_www(attachid);
183 @ <br><a href="%R/attachview?%s(zUrlTail)">%h(zFilename)</a>
@@ -117,25 +184,37 @@
184 @ [<a href="%R/attachdownload/%t(zFilename)?%s(zUrlTail)">download</a>]<br>
185 if( zComment ) while( fossil_isspace(zComment[0]) ) zComment++;
186 if( zComment && zComment[0] ){
187 @ %!W(zComment)<br>
188 }
189 if( zForumPost==0 && zPage==0 && zTkt==0 && zTechNote==0 ){
190 if( zSrc==0 || zSrc[0]==0 ){
191 zSrc = "Deleted from";
192 }else {
193 zSrc = "Added to";
194 }
195 switch( type ){
196 case CFTYPE_TICKET:
197 @ %s(zSrc) ticket <a href="%R/tktview?name=%s(zTarget)">
198 @ %S(zTarget)</a>
199 break;
200 case CFTYPE_EVENT:
201 @ %s(zSrc) tech note <a href="%R/technote/%s(zTarget)">
202 @ %S(zTarget)</a>
203 break;
204 case CFTYPE_WIKI:
205 @ %s(zSrc) wiki page <a href="%R/wiki?name=%t(zTarget)">
206 @ %h(zTarget)</a>
207 break;
208 case CFTYPE_FORUM:
209 @ %s(zSrc) forum post <a href="%R/forumpost/%s(zTarget)">
210 @ %h(zTarget)</a>
211 break;
212 default:
213 @ <span class='error'>%s(zSrc) cannot determine target type
214 @ of %h(zTarget)</span>
215 break;
216 }
217 }else{
218 if( zSrc==0 || zSrc[0]==0 ){
219 @ Deleted
220 }else {
@@ -162,27 +241,35 @@
241 ** Query parameters:
242 **
243 ** tkt=HASH
244 ** page=WIKIPAGE
245 ** technote=HASH
246 ** forumpost=HASH
247 ** file=FILENAME
248 ** attachid=ID
249 **
250 */
251 void attachview_page(void){
252 const char *zPage = P("page");
253 const char *zTkt = P("tkt");
254 const char *zTechNote = P("technote");
255 const char *zForumPost = P("forumpost");
256 const char *zFile = P("file");
257 const char *zTarget = 0;
258 int attachid = atoi(PD("attachid","0"));
259 char *zUUID = 0;
260
261 if( zFile==0 ) fossil_redirect_home();
262 login_check_credentials();
263 style_set_current_feature("attach");
264 if( zForumPost ){
265 int fnid;
266 if( g.perm.RdForum==0 ){ login_needed(g.anon.RdForum); return; }
267 /* Forum attachments are always tied to the post's initial version */
268 fnid = forumpost_head_rid2(zForumPost);
269 if( fnid>0 ) zTarget = rid_to_uuid(fnid);
270 }else if( zPage ){
271 if( g.perm.RdWiki==0 ){ login_needed(g.anon.RdWiki); return; }
272 zTarget = zPage;
273 }else if( zTkt ){
274 if( g.perm.RdTkt==0 ){ login_needed(g.anon.RdTkt); return; }
275 zTarget = zTkt;
@@ -314,35 +401,60 @@
401 ** Add a new attachment.
402 **
403 ** tkt=HASH
404 ** page=WIKIPAGE
405 ** technote=HASH
406 ** forumpost=HASH
407 ** from=URL
408 **
409 */
410 void attachadd_page(void){
411 const char *zPage = P("page");
412 const char *zForumPost = P("forumpost");
413 const char *zTkt = P("tkt");
414 const char *zTechNote = P("technote");
415 const char *zFrom = P("from");
416 const char *aContent = P("f");
417 const char *zName = PD("f:filename","unknown");
418 const char *zComment = PD("comment", "");
419 const char *zTarget;
420 char * zTo = 0;
421 char *zTargetType = 0;
422 char *zExtraFree = 0;
423 int szContent = atoi(PD("f:bytes","0"));
424 int goodCaptcha = 1;
425 int szLimit = 0;
426
427 if( zFrom==0 ) zFrom = mprintf("%R/home");
428 if( P("cancel") ) cgi_redirect(zFrom);
429 if( (!!zPage + !!zTkt + !!zTechNote + !!zForumPost)!=1 ){
430 webpage_error("Requires exactly one one: page=X, tkt=X, forumpost=X,"
431 " or technote=X");
 
 
432 }
 
433 login_check_credentials();
434 if( zForumPost ){
435 int fpid;
436 if( g.perm.AttachForum==0 ){
437 login_needed(g.anon.AttachForum);
438 return;
439 }
440 fpid = forumpost_head_rid2(zForumPost);
441 if( fpid<=0 ){
442 webpage_error("Invalid forum post ID: %h", zForumPost);
443 }else if( !g.perm.Admin && !forumpost_is_owner(fpid, 0) ){
444 webpage_error("Only admins can attach files to other users' "
445 "forum posts.");
446 }
447 zTarget = zExtraFree = rid_to_uuid(fpid);
448 zTargetType = mprintf("Forum post <a href=\"%R/forumpost/%S\">%h</a>",
449 zTarget, zForumPost);
450 zTo = 1
451 ? mprintf("%R/forumpost/%S", zTarget)
452 : mprintf("%R/attachview?forumpost=%T&file=%T",
453 zTarget, zName)
454 /* Or we could return directly to the forum post. */;
455 }else if( zPage ){
456 if( g.perm.ApndWiki==0 || g.perm.Attach==0 ){
457 login_needed(g.anon.ApndWiki && g.anon.Attach);
458 return;
459 }
460 if( !db_exists("SELECT 1 FROM tag WHERE tagname='wiki-%q'", zPage) ){
@@ -364,10 +476,11 @@
476 zTarget = zTechNote;
477 zTargetType = mprintf("Tech Note <a href=\"%R/technote/%s\">%S</a>",
478 zTechNote, zTechNote);
479
480 }else{
481 assert( zTkt );
482 if( g.perm.ApndTkt==0 || g.perm.Attach==0 ){
483 login_needed(g.anon.ApndTkt && g.anon.Attach);
484 return;
485 }
486 if( !db_exists("SELECT 1 FROM tag WHERE tagname='tkt-%q'", zTkt) ){
@@ -377,21 +490,25 @@
490 }
491 zTarget = zTkt;
492 zTargetType = mprintf("Ticket <a href=\"%R/tktview/%s\">%S</a>",
493 zTkt, zTkt);
494 }
495 szLimit = db_get_int("attachment-size-limit", 0);
496 if( szContent<0 || (szLimit && szContent>szLimit) ){
497 /* This check must be done late so that zTargetType is set up. */
498 @ <p class="generalError">Attachment %h(zName) is too large.
499 @ <a href="%R/help/attachment-size-limit">Limit</a> is
500 @ %d(szLimit ? szLimit : 0x7fffffff) bytes</p>
501 /* Fall through and render form. */
502 }else if( P("ok") && szContent>0 && (goodCaptcha = captcha_is_correct(0)) ){
503 int needModerator = (zForumPost!=0 && forum_need_moderation()) ||
504 (zTkt!=0 && ticket_need_moderation(0)) ||
505 (zPage!=0 && wiki_need_moderation(0));
 
506 attach_commit(zName, zTarget, aContent, szContent, needModerator, zComment);
507 cgi_redirect(zTo ? zTo : zFrom);
508 }
509
510 style_set_current_feature("attach");
511 style_header("Add Attachment");
512 if( !goodCaptcha ){
513 @ <p class="generalError">Error: Incorrect security code.</p>
514 }
@@ -399,12 +516,15 @@
516 form_begin("enctype='multipart/form-data'", "%R/attachadd");
517 @ <div>
518 @ File to Attach:
519 @ <input type="file" name="f" size="60"><br>
520 @ Description:<br>
521 @ <textarea name="comment" cols="80" rows="5" wrap="virtual"\
522 @ >%h(zComment)</textarea><br>
523 if( zForumPost ){
524 @ <input type="hidden" name="forumpost" value="%h(zTarget)">
525 }else if( zTkt ){
526 @ <input type="hidden" name="tkt" value="%h(zTkt)">
527 }else if( zTechNote ){
528 @ <input type="hidden" name="technote" value="%h(zTechNote)">
529 }else{
530 @ <input type="hidden" name="page" value="%h(zPage)">
@@ -415,15 +535,18 @@
535 @ </div>
536 captcha_generate(0);
537 @ </form>
538 style_finish_page();
539 fossil_free(zTargetType);
540 fossil_free(zExtraFree);
541 }
542
543 /*
544 ** WEBPAGE: ainfo
545 ** URL: /ainfo?name=ARTIFACTID
546 **
547 ** name=ATTACHMENT_ARTIFACT_UUID
548 **
549 ** Show the details of an attachment artifact.
550 */
551 void ainfo_page(void){
552 int rid; /* RID for the control artifact */
@@ -436,113 +559,141 @@
559 const char *zName; /* Name of the attached file */
560 const char *zDesc; /* Description of the attached file */
561 const char *zWikiName = 0; /* Wiki page name when attached to Wiki */
562 const char *zTNUuid = 0; /* Tech Note ID when attached to tech note */
563 const char *zTktUuid = 0; /* Ticket ID when attached to a ticket */
564 const char *zForumPost = 0; /* Forum UID when attached to forum post */
565 int modPending; /* True if awaiting moderation */
566 const char *zModAction; /* Moderation action or NULL */
567 int isModerator; /* TRUE if user is the moderator */
568 const char *zMime; /* MIME Type */
569 Blob attach; /* Content of the attachment */
570 int fShowContent = 0; /* True to emit the content */
571 int bUserIsOwner = 0; /* True if pAttach->zUser is login_name() */
572 int showDelMenu = 0; /* True to enable delete option */
573 const char *zLn = P("ln");
574
575 login_check_credentials();
576 if( !g.perm.RdTkt && !g.perm.RdWiki ){
577 login_needed(g.anon.RdTkt || g.anon.RdWiki);
578 return;
579 }
580 rid = name_to_rid_www("name");
581 if( rid==0 ){ fossil_redirect_home(); }
582 zUuid = rid_to_uuid(rid);
583 pAttach = manifest_get(rid, CFTYPE_ATTACHMENT, 0);
584 if( pAttach==0 ) fossil_redirect_home();
585 bUserIsOwner =
586 0==fossil_strcmp(pAttach->zUser, login_name())
587 && login_is_individual();
588 zTarget = pAttach->zAttachTarget;
589 zSrc = pAttach->zAttachSrc;
590 ridSrc = db_int(0,"SELECT rid FROM blob WHERE uuid='%q'", zSrc);
591 zName = pAttach->zAttachName;
592 zDesc = pAttach->zComment;
593 zMime = mimetype_from_name(zName);
594 fShowContent = zMime ? strncmp(zMime,"text/", 5)==0 : 0;
595 if( db_int(0,"SELECT 1 FROM event WHERE objid=%d and type='f'", rid) ){
596 if( !g.perm.RdForum ){ login_needed(g.anon.RdForum); return; }
597 showDelMenu = g.perm.Admin || bUserIsOwner;
598 zForumPost = zTarget;
599 }else if( validate16(zTarget, strlen(zTarget))
600 && db_exists("SELECT 1 FROM ticket WHERE tkt_uuid='%q'", zTarget)
601 ){
602 if( !g.perm.RdTkt ){ login_needed(g.anon.RdTkt); return; }
603 zTktUuid = zTarget;
604 showDelMenu = g.perm.WrTkt;
605 }else if( db_exists("SELECT 1 FROM tag WHERE tagname='wiki-%q'",zTarget) ){
606 if( !g.perm.RdWiki ){ login_needed(g.anon.RdWiki); return; }
607 zWikiName = zTarget;
608 showDelMenu = g.perm.WrWiki;
609 }else if( db_exists("SELECT 1 FROM tag WHERE tagname='event-%q'",zTarget) ){
610 if( !g.perm.RdWiki ){ login_needed(g.anon.RdWiki); return; }
611 zTNUuid = zTarget;
612 showDelMenu = g.perm.Write && g.perm.WrWiki;
613 }
614 if( showDelMenu ){
615 style_submenu_element("Delete", "%R/ainfo/%s?del", zUuid);
 
 
 
616 }
617 zDate = db_text(0, "SELECT datetime(%.12f)", pAttach->rDate);
618
619 if( P("confirm")
620 && cgi_csrf_safe(2)
621 && ((zForumPost
622 && ((bUserIsOwner && g.perm.AttachForum) ||
623 forumpost_may_close())) ||
624 (zTktUuid && g.perm.WrTkt) ||
625 (zWikiName && g.perm.WrWiki) ||
626 (zTNUuid && g.perm.Write && g.perm.WrWiki))
627 ){
628 /* Delete attachment. */
629 int i, n, rid;
630 char *zNewDate;
631 Blob manifest;
632 Blob cksum;
633 const char *zFile = zName;
634
635 if( !bUserIsOwner ){
636 if( zForumPost ? !forumpost_may_close() : !g.perm.Admin ){
637 webpage_error("Only admins can delete other users' attachments.");
638 }
639 }
640 db_begin_transaction();
641 blob_zero(&manifest);
642 for(i=n=0; zFile[i]; i++){
643 if( zFile[i]=='/' || zFile[i]=='\\' ) n = i;
644 }
645 zFile += n;
646 if( zFile[0]==0 ) zFile = "unknown";
647 blob_appendf(&manifest, "A %F %F\n", zFile, zTarget);
648 zNewDate = date_in_standard_format("now");
649 blob_appendf(&manifest, "D %s\n", zNewDate);
650 blob_appendf(&manifest, "U %F\n", login_name());
651 md5sum_blob(&manifest, &cksum);
652 blob_appendf(&manifest, "Z %b\n", &cksum);
653 rid = content_put(&manifest);
654 manifest_crosslink(rid, &manifest, MC_NONE);
655 db_end_transaction(0);
656 @ <p>The attachment below has been deleted.</p>
657 fossil_free(zNewDate);
658 }
659
660 if( P("del")
661 && ((zForumPost && (bUserIsOwner || forumpost_may_close())) ||
662 (zTktUuid && g.perm.WrTkt) ||
663 (zWikiName && g.perm.WrWiki) ||
664 (zTNUuid && g.perm.Write && g.perm.WrWiki))
665 ){
666 form_begin(0, "%R/ainfo/%!S", zUuid);
667 @ <p>Confirm you want to delete the attachment shown below.
668 @ <input type="submit" name="confirm" value="Confirm">
669 login_insert_csrf_secret();
670 @ </form>
671 }
672
673 isModerator = g.perm.Admin ||
674 (zForumPost && g.perm.ModForum) ||
675 (zTktUuid && g.perm.ModTkt) ||
676 (zWikiName && g.perm.ModWiki);
677 zModAction = P("modaction");
678 if( zModAction!=0 && cgi_csrf_safe(2) ){
679 if( strcmp(zModAction,"delete")==0 ){
680 if( isModerator || bUserIsOwner ){
681 moderation_disapprove(rid);
682 }
683 if( zForumPost ){
684 cgi_redirectf("%R/forumpost/%!S", zForumPost);
685 }else if( zTktUuid ){
686 cgi_redirectf("%R/tktview/%!S", zTktUuid);
687 }else if( zWikiName ) {
688 cgi_redirectf("%R/wiki?name=%t", zWikiName);
689 }
690 /* zTNUuid is intentionally unhandled. Tech note attachments
691 ** don't go through moderation. */
692 return;
693 }
694 if( isModerator && strcmp(zModAction,"approve")==0 ){
695 moderation_approve('a', rid);
696 }
697 }
698 style_set_current_feature("attach");
699 style_header("Attachment Details");
@@ -558,19 +709,20 @@
709 @ <td>%z(href("%R/artifact/%!S",zUuid))%s(zUuid)</a>
710 if( g.perm.Setup ){
711 @ (%d(rid))
712 }
713 modPending = moderation_pending_www(rid);
714 if( zForumPost ){
715 @ <tr><th>Forum&nbsp;Post:</th>
716 @ <td>%z(href("%R/forumpost/%s",zForumPost))%h(zForumPost)</a></td></tr>
717 }else if( zTktUuid ){
718 @ <tr><th>Ticket:</th>
719 @ <td>%z(href("%R/tktview/%s",zTktUuid))%s(zTktUuid)</a></td></tr>
720 }else if( zTNUuid ){
 
721 @ <tr><th>Tech Note:</th>
722 @ <td>%z(href("%R/technote/%s",zTNUuid))%s(zTNUuid)</a></td></tr>
723 }else if( zWikiName ){
 
724 @ <tr><th>Wiki&nbsp;Page:</th>
725 @ <td>%z(href("%R/wiki?name=%t",zWikiName))%h(zWikiName)</a></td></tr>
726 }
727 @ <tr><th>Date:</th><td>
728 hyperlink_to_date(zDate, "</td></tr>");
@@ -586,66 +738,89 @@
738 @ <tr><th>MIME-Type:</th><td>%h(zMime)</td></tr>
739 }
740 @ <tr><th valign="top">Description:</th><td valign="top">%h(zDesc)</td></tr>
741 @ </table>
742
743 if( modPending && (isModerator || bUserIsOwner) ){
744 @ <div class="section">Moderation</div>
745 @ <blockquote>
746 form_begin(0, "%R/ainfo/%s", zUuid);
747 @ <label><input type="radio" name="modaction" value="delete">
748 @ Delete this attachment</label><br>
749 if( isModerator ){
750 @ <label><input type="radio" name="modaction" value="approve">
751 @ Approve this attachment</label><br>
752 }
753 @ <input type="submit" value="Submit">
754 login_insert_csrf_secret();
755 @ </form>
756 @ </blockquote>
757 }
758
759 @ <div class="section">Content:</div>
 
760 blob_zero(&attach);
761 if( modPending && !moderation_user_could(rid, 1, 0) ){
762 @ <p><span class="modpending">Content is awaiting moderator \
763 @ approval.</span></p>
764 }else{
765 @ <blockquote>
766 if( fShowContent ){
767 const char *z;
768 content_get(ridSrc, &attach);
769 blob_to_utf8_no_bom(&attach, 0);
770 z = blob_str(&attach);
771 if( zLn ){
772 output_text_with_line_numbers(z, blob_size(&attach), zName, zLn, 1);
773 }else{
774 @ <pre>
775 @ %h(z)
776 @ </pre>
777 }
778 }else if( strncmp(zMime, "image/", 6)==0 ){
779 int sz = db_int(0, "SELECT size FROM blob WHERE rid=%d", ridSrc);
780 @ <i>(file is %d(sz) bytes of image data)</i><br>
781 @ <img src="%R/raw/%s(zSrc)?m=%s(zMime)"></img>
782 style_submenu_element("Image", "%R/raw/%s?m=%s", zSrc, zMime);
783 }else{
784 int sz = db_int(0, "SELECT size FROM blob WHERE rid=%d", ridSrc);
785 @ <i>(file is %d(sz) bytes of binary data)</i>
786 }
787 @ </blockquote>
788 }
789 manifest_destroy(pAttach);
790 blob_reset(&attach);
791 style_finish_page();
792 }
793
794 #if INTERFACE
795 /*
796 ** Flags for use with attachment_list(). ATTACHLIST_HRULE_ABOVE
797 ** must have a value of 1 for historical call compatibility.
798 */
799 #define ATTACHLIST_HRULE_ABOVE 0x01 /* Insert <hr> above header */
800 #define ATTACHLIST_TARGET_BLANK 0x02 /* use target=_blank for links */
801 #define ATTACHLIST_SIZE 0x04 /* add size */
802 #define ATTACHLIST_HIDE_UNAPPROVED 0x08 /* Hide pending-moderation files */
803 #endif
804
805 /*
806 ** Output HTML to show a list of attachments.
807 */
808 void attachment_list(
809 const char *zTarget, /* Object that things are attached to */
810 const char *zHeader, /* Header to display with attachments */
811 const int flags /* ATTACHLIST_... flags */
812 ){
813 int cnt = 0;
814 char szBuf[36] = {0}; /* scratchpad for attachment size value */
815 const char * zLinkTgt = (ATTACHLIST_TARGET_BLANK & flags)
816 ? " target=\"_blank\"" : "";
817 Stmt q;
818 db_prepare(&q,
819 "SELECT datetime(mtime,toLocal()), filename, user,"
820 " (SELECT uuid FROM blob WHERE rid=attachid), src, target, "
821 " attachid "
822 " FROM attachment"
823 " WHERE isLatest AND src!='' AND target=%Q"
824 " ORDER BY mtime DESC",
825 zTarget
826 );
@@ -653,34 +828,55 @@
828 const char *zDate = db_column_text(&q, 0);
829 const char *zFile = db_column_text(&q, 1);
830 const char *zUser = db_column_text(&q, 2);
831 const char *zUuid = db_column_text(&q, 3);
832 const char *zSrc = db_column_text(&q, 4);
833 const char *zTarget = db_column_text(&q, 5);
834 const char *zDispUser = zUser && zUser[0] ? zUser : "anonymous";
835 const char *zTypeArg = 0; /* URL arg name for /attachdownload */
836 const int aid = db_column_int(&q, 6);
837 const int iAType = attachment_target_type(zTarget);
838 if( (flags & ATTACHLIST_HIDE_UNAPPROVED)
839 && moderation_pending(aid)
840 && !moderation_user_could(aid, 1, 0) ){
841 continue;
842 }
843 if( cnt==0 ){
844 @ <section class='attachlist'>
845 if( flags & ATTACHLIST_HRULE_ABOVE ){
846 @ <hr>
847 }
848 @ %s(zHeader)
849 @ <ul>
850 }
851 cnt++;
852 switch( iAType ){
853 case CFTYPE_TICKET: zTypeArg = "tkt"; break;
854 case CFTYPE_FORUM: zTypeArg = "forumpost"; break;
855 case CFTYPE_EVENT: zTypeArg = "technote"; break;
856 case CFTYPE_WIKI:
857 default: zTypeArg = "page"; break;
858 }
859 @ <li>
860 @ <a href="%R/artifact/%!S(zSrc)"%s(zLinkTgt)>%h(zFile)</a>
861 if( flags & ATTACHLIST_SIZE ){
862 const int sz = db_int(0,"SELECT size FROM blob WHERE uuid=%Q", zSrc);
863 sqlite3_snprintf(sizeof(szBuf), szBuf, " %d bytes", sz);
864 }
865 @ [<a href="%R/attachdownload/%t(zFile)?%s(zTypeArg)=%t(zTarget)\
866 @&file=%t(zFile)%s(zLinkTgt)">download</a>%s(szBuf)]
867 @ added by %h(zDispUser) on
868 hyperlink_to_date(zDate, ".");
869 @ [<a href="%R/ainfo/%!S(zUuid)"%s(zLinkTgt)>details</a>]
870 moderation_pending_www(aid);
871 @ </li>
872 }
873 if( cnt ){
874 @ </ul>
875 @ </section>
876 }
877 db_finalize(&q);
 
878 }
879
880 /*
881 ** COMMAND: attachment*
882 **
883
--- src/capabilities.c
+++ src/capabilities.c
@@ -304,10 +304,12 @@
304304
"Forum-Admin", "Grant capability '4' to other users" },
305305
{ '7', CAPCLASS_ALERT, 0,
306306
"Alerts", "Sign up for email alerts" },
307307
{ 'A', CAPCLASS_ALERT|CAPCLASS_SUPER, 0,
308308
"Announce", "Send announcements to all subscribers" },
309
+ { 'B', CAPCLASS_FORUM|CAPCLASS_SUPER, 0,
310
+ "Forum-Attach", "Add attachment to Forum posts" },
309311
{ 'C', CAPCLASS_FORUM, 0,
310312
"Chat", "Read and/or writes messages in the chatroom" },
311313
{ 'D', CAPCLASS_OTHER, 0,
312314
"Debug", "Enable debugging features" },
313315
};
314316
--- src/capabilities.c
+++ src/capabilities.c
@@ -304,10 +304,12 @@
304 "Forum-Admin", "Grant capability '4' to other users" },
305 { '7', CAPCLASS_ALERT, 0,
306 "Alerts", "Sign up for email alerts" },
307 { 'A', CAPCLASS_ALERT|CAPCLASS_SUPER, 0,
308 "Announce", "Send announcements to all subscribers" },
 
 
309 { 'C', CAPCLASS_FORUM, 0,
310 "Chat", "Read and/or writes messages in the chatroom" },
311 { 'D', CAPCLASS_OTHER, 0,
312 "Debug", "Enable debugging features" },
313 };
314
--- src/capabilities.c
+++ src/capabilities.c
@@ -304,10 +304,12 @@
304 "Forum-Admin", "Grant capability '4' to other users" },
305 { '7', CAPCLASS_ALERT, 0,
306 "Alerts", "Sign up for email alerts" },
307 { 'A', CAPCLASS_ALERT|CAPCLASS_SUPER, 0,
308 "Announce", "Send announcements to all subscribers" },
309 { 'B', CAPCLASS_FORUM|CAPCLASS_SUPER, 0,
310 "Forum-Attach", "Add attachment to Forum posts" },
311 { 'C', CAPCLASS_FORUM, 0,
312 "Chat", "Read and/or writes messages in the chatroom" },
313 { 'D', CAPCLASS_OTHER, 0,
314 "Debug", "Enable debugging features" },
315 };
316
+1 -1
--- src/db.c
+++ src/db.c
@@ -3576,11 +3576,11 @@
35763576
}
35773577
35783578
/*
35793579
** Attempt to look up the input in the CONCEALED table. If found,
35803580
** and if the okRdAddr permission is enabled then return the
3581
-** original value for which the input is a hash. If okRdAddr is
3581
+** original value for which the input is a hash. If g.perm.RdAddr is
35823582
** false or if the lookup fails, return the original string content.
35833583
**
35843584
** In either case, the string returned is stored in space obtained
35853585
** from malloc and should be freed by the calling function.
35863586
*/
35873587
--- src/db.c
+++ src/db.c
@@ -3576,11 +3576,11 @@
3576 }
3577
3578 /*
3579 ** Attempt to look up the input in the CONCEALED table. If found,
3580 ** and if the okRdAddr permission is enabled then return the
3581 ** original value for which the input is a hash. If okRdAddr is
3582 ** false or if the lookup fails, return the original string content.
3583 **
3584 ** In either case, the string returned is stored in space obtained
3585 ** from malloc and should be freed by the calling function.
3586 */
3587
--- src/db.c
+++ src/db.c
@@ -3576,11 +3576,11 @@
3576 }
3577
3578 /*
3579 ** Attempt to look up the input in the CONCEALED table. If found,
3580 ** and if the okRdAddr permission is enabled then return the
3581 ** original value for which the input is a hash. If g.perm.RdAddr is
3582 ** false or if the lookup fails, return the original string content.
3583 **
3584 ** In either case, the string returned is stored in space obtained
3585 ** from malloc and should be freed by the calling function.
3586 */
3587
+185 -31
--- src/forum.c
+++ src/forum.c
@@ -81,29 +81,58 @@
8181
db_reset(&q);
8282
return res;
8383
}
8484
8585
/*
86
-** Given a valid forumpost.fpid value, this function returns the first
87
-** fpid in the chain of edits for that forum post, or rid if no prior
88
-** versions are found.
86
+** Given a valid forumpost.fpid value, this function returns the
87
+** initial forumpost.fpid in the chain of edits for that forum post,
88
+** or rid if no prior versions are found.
8989
*/
90
-static int forumpost_head_rid(int rid){
91
- Stmt q;
90
+int forumpost_head_rid(int rid){
91
+ static Stmt q = empty_Stmt_m;
9292
int rcRid = rid;
93
-
94
- db_prepare(&q, "SELECT fprev FROM forumpost"
95
- " WHERE fpid=:rid AND fprev IS NOT NULL");
93
+ if( !q.pStmt ){
94
+ db_static_prepare(&q,
95
+ "SELECT fprev FROM forumpost"
96
+ " WHERE fpid=:rid AND fprev IS NOT NULL"
97
+ );
98
+ }
9699
db_bind_int(&q, ":rid", rid);
97100
while( SQLITE_ROW==db_step(&q) ){
98101
rcRid = db_column_int(&q, 0);
99102
db_reset(&q);
100103
db_bind_int(&q, ":rid", rcRid);
101104
}
102
- db_finalize(&q);
105
+ db_reset(&q);
103106
return rcRid;
104107
}
108
+
109
+/*
110
+** Works like forumpost_head_rid() but expects zUuid to be an
111
+** unambiguous forum post name. It may be a hash prefix, so long as
112
+** it's unambiguous. Returns 0 if the name cannot be unambiguously
113
+** resolved as a forum post.
114
+*/
115
+int forumpost_head_rid2(const char *zUuid){
116
+ const int fpid = symbolic_name_to_rid(zUuid, "f");
117
+ return fpid>0
118
+ ? forumpost_head_rid(fpid)
119
+ : 0;
120
+}
121
+
122
+/*
123
+** Given a forum post RID and user name, returns true if zUserName
124
+** matches the event.(euser,user) field for a formpost entry with the
125
+** matching RID. Returns false if no match is found. If zUserName is
126
+** 0 then login_name() is used.
127
+*/
128
+int forumpost_is_owner(int rid, const char *zUserName){
129
+ return db_int(0, "SELECT 1 FROM event "
130
+ "WHERE type='f' AND objid=%d "
131
+ "AND coalesce(euser,user)=%Q",
132
+ rid, zUserName ? zUserName : login_name());
133
+}
105134
106135
/*
107136
** Returns true if p, or any parent of p, has a non-zero iClosed
108137
** value. Returns 0 if !p. For an edited chain of post, the tag is
109138
** checked on the pEditHead entry, to simplify subsequent unlocking of
@@ -169,10 +198,70 @@
169198
db_reset(&qIrt);
170199
}
171200
return i ? -rc : rc;
172201
}
173202
203
+/* True if moderation of forum posts performs the same operation
204
+** on its attachments. */
205
+#define FORUMPOST_MOD_ATTACHMENTS 1
206
+#if FORUMPOST_MOD_ATTACHMENTS
207
+/*
208
+** Internal helper for moderation_forumpost_...().
209
+*/
210
+static void forumpost_prep_pending_attachids(Stmt *q, int fpid){
211
+ db_prepare(
212
+ q,
213
+ "SELECT attachid FROM attachment "
214
+ "WHERE target=("
215
+ " SELECT uuid FROM blob WHERE rid=%d"
216
+ ") and attachid in ("
217
+ " SELECT objid FROM modreq"
218
+ ")",
219
+ forumpost_head_rid(fpid)
220
+ );
221
+}
222
+#endif
223
+
224
+/*
225
+** Approve the given forum post RID and any pending-approval
226
+** attachments associated with its initial version.
227
+*/
228
+static void moderation_forumpost_approve(int fpid){
229
+#if !FORUMPOST_MOD_ATTACHMENTS
230
+ moderation_approve('f', fpid);
231
+#else
232
+ /* Also approve any pending attachments */
233
+ Stmt q;
234
+ moderation_approve('f', fpid);
235
+ forumpost_prep_pending_attachids(&q, fpid);
236
+ while( SQLITE_ROW==db_step(&q) ){
237
+ moderation_approve('a', db_column_int(&q, 0));
238
+ }
239
+ db_finalize(&q);
240
+#endif
241
+}
242
+
243
+/*
244
+** Disapprove the given forum post and any pending-moderation
245
+** attachments on its initial version.
246
+*/
247
+static void moderation_forumpost_disapprove(int fpid){
248
+#if !FORUMPOST_MOD_ATTACHMENTS
249
+ moderation_disapprove(fpid);
250
+#else
251
+ /* Also disapprove any pending attachments */
252
+ Stmt q;
253
+ moderation_disapprove(fpid);
254
+ forumpost_prep_pending_attachids(&q, fpid);
255
+ while( SQLITE_ROW==db_step(&q) ){
256
+ moderation_disapprove(db_column_int(&q, 0));
257
+ }
258
+ db_finalize(&q);
259
+#endif
260
+}
261
+#undef FORUMPOST_MOD_ATTACHMENTS
262
+
174263
/*
175264
** Applies or cancels a tag named zTagName on the given forum RID via
176265
** addition of a new control artifact into the repository. In order to
177266
** provide consistent behavior, it always acts on the first version of
178267
** the given forum post, walking the forumpost.fprev values to find
@@ -179,18 +268,18 @@
179268
** the head of the chain.
180269
**
181270
** If addTag is true then a propagating tag is added, except as noted
182271
** below, with the given optional zReason string as the tag's
183272
** value. If addTag is false then any matching active tag on frid is
184
-** cancelled, except as noted below. zReason is ignored if doClose is
185
-** false or if zReason is NULL or starts with a NUL byte.
273
+** cancelled, except as noted below. zReason is ignored if it is NULL
274
+** or starts with a NUL byte, or if addTag is false.
186275
**
187276
** This function only adds a tag if forum_rid_is_tagged() indicates
188277
** that frid's head is not tagged. If a parent post is already tagged,
189
-** no tag is added. Similarly, it will only remove a tagtag from a
190
-** post which has its own tag tag, and will not remove an inherited
191
-** one from a parent post.
278
+** no tag is added. Similarly, it will only remove a tag from a post
279
+** which has its own tag, and will not remove an inherited one from a
280
+** parent post.
192281
**
193282
** If addTag is true and frid is already tagged (directly or
194283
** inherited), this is a no-op. Likewise, if addTag is false and frid
195284
** itself is not tagged (not accounting for an inherited closed tag),
196285
** this is a no-op.
@@ -286,12 +375,15 @@
286375
287376
/*
288377
** Returns 1 if the current user is an admin, -1 if the current user
289378
** is a forum moderator and the forum-close-policy setting is true,
290379
** else returns 0. The value is cached for subsequent calls.
380
+**
381
+** This policy also determines whether non-admin forum moderators
382
+** may delete forum attachments.
291383
*/
292
-static int forumpost_may_close(void){
384
+int forumpost_may_close(void){
293385
static int permClose = -99;
294386
if( permClose!=-99 ){
295387
return permClose;
296388
}else if( g.perm.Admin ){
297389
return permClose = 1;
@@ -749,10 +841,31 @@
749841
if( pToFree ) manifest_destroy(pToFree);
750842
if( p->zDisplayName==0 ) return "(unknown)";
751843
return p->zDisplayName;
752844
}
753845
846
+/*
847
+** Renders the attachment list for the given forum post.
848
+** Emits no output if there are no attachments.
849
+*/
850
+static void forum_render_attachment_list(const char *zUuid){
851
+ char * zLbl = mprintf("<a href='%R/attachlist?forumpost=%s'>"
852
+ "Attachments:</a>", zUuid);
853
+ attachment_list(zUuid, zLbl,
854
+ ATTACHLIST_HRULE_ABOVE
855
+ | ATTACHLIST_SIZE
856
+ | ATTACHLIST_HIDE_UNAPPROVED);
857
+ fossil_free(zLbl);
858
+}
859
+
860
+/*
861
+** Renders the attachment list for p or (if not NULL) pEditHead.
862
+*/
863
+static void forum_render_attachment_list2(ForumPost *p){
864
+ if( p->pEditHead ) p = p->pEditHead;
865
+ forum_render_attachment_list(p->zUuid);
866
+}
754867
755868
/*
756869
** Display a single post in a forum thread.
757870
*/
758871
static void forum_display_post(
@@ -894,10 +1007,11 @@
8941007
zMimetype = "text/plain";
8951008
}else{
8961009
zMimetype = pManifest->zMimetype;
8971010
}
8981011
forum_render(0, zMimetype, pManifest->zWiki, 0, !bRaw);
1012
+ forum_render_attachment_list2(p);
8991013
}
9001014
9011015
/* When not in raw mode, finish creating the border around the post. */
9021016
if( !bRaw ){
9031017
/* If the user is able to write to the forum and if this post has not been
@@ -937,21 +1051,38 @@
9371051
/* Allow users to delete (reject) their own pending posts. */
9381052
@ <input type="submit" name="reject" value="Delete">
9391053
}
9401054
login_insert_csrf_secret();
9411055
@ </form>
942
- if( bSelect && forumpost_may_close() && iClosed>=0 ){
943
- int iHead = forumpost_head_rid(p->fpid);
944
- @ <form method="post" \
945
- @ action='%R/forumpost_%s(iClosed > 0 ? "reopen" : "close")'>
946
- login_insert_csrf_secret();
947
- @ <input type="hidden" name="fpid" value="%z(rid_to_uuid(iHead))" />
948
- if( moderation_pending(p->fpid)==0 ){
949
- @ <input type="button" value='%s(iClosed ? "Re-open" : "Close")' \
950
- @ class='%s(iClosed ? "action-reopen" : "action-close")'/>
951
- }
952
- @ </form>
1056
+
1057
+ if( bSelect ){
1058
+ const ForumPost *pHead = p->pEditHead ? p->pEditHead : p;
1059
+ if( forumpost_may_close() && iClosed>=0 ){
1060
+ @ <form method="post" \
1061
+ @ action='%R/forumpost_%s(iClosed > 0 ? "reopen" : "close")'>
1062
+ login_insert_csrf_secret();
1063
+ @ <input type="hidden" name="fpid" value="%s(pHead->zUuid)" />
1064
+ if( moderation_pending(p->fpid)==0 ){
1065
+ @ <input type="button" value='%s(iClosed ? "Re-open" : "Close")' \
1066
+ @ class='%s(iClosed ? "action-reopen" : "action-close")'/>
1067
+ }
1068
+ @ </form>
1069
+ }
1070
+ if( g.perm.Admin ||
1071
+ (login_is_individual()
1072
+ && forumpost_is_owner(p/*not pHead*/->fpid, 0)) ){
1073
+ /* When an admin edits someone else's post, the admin
1074
+ ** effectively takes over ownership of it (and we currently
1075
+ ** have no way of passing it back). Because of this, we
1076
+ ** check the ownership of `p` instead of `pHead`. */
1077
+ @ <form method="post" action="%R/attachadd">\
1078
+ @ <input type="hidden" name="forumpost" value="%T(pHead->zUuid)">
1079
+ @ <input type="submit" value="Attach...">
1080
+ login_insert_csrf_secret();
1081
+ moderation_pending_www(p->fpid);
1082
+ @ </form>
1083
+ }
9531084
}
9541085
@ </div>
9551086
}
9561087
@ </div>
9571088
}
@@ -1280,11 +1411,11 @@
12801411
}
12811412
12821413
/*
12831414
** Return true if a forum post should be moderated.
12841415
*/
1285
-static int forum_need_moderation(void){
1416
+int forum_need_moderation(void){
12861417
if( P("domod") ) return 1;
12871418
if( g.perm.WrTForum ) return 0;
12881419
if( g.perm.ModForum ) return 0;
12891420
return 1;
12901421
}
@@ -1565,10 +1696,22 @@
15651696
@ <br><label><input type="checkbox" name="fpsilent" %s(PCK("fpsilent"))> \
15661697
@ Do not send notification emails</label>
15671698
@ </div>
15681699
}
15691700
}
1701
+
1702
+/*
1703
+** If the user has AttachForum permissions, emit a notice that
1704
+** attachments may be added after saving. If p is not NULL,
1705
+** also emit its list of attachments.
1706
+*/
1707
+static void forum_render_attachment_notice(void){
1708
+ if( g.perm.AttachForum ){
1709
+ @ <div>You will be able to attach files to this post after saving
1710
+ @ it.</div>
1711
+ }
1712
+}
15701713
15711714
/*
15721715
** WEBPAGE: forume1
15731716
**
15741717
** Start a new forum thread.
@@ -1604,10 +1747,11 @@
16041747
@ <input type="submit" name="submit" value="Submit" disabled>
16051748
}
16061749
forum_render_debug_options();
16071750
login_insert_csrf_secret();
16081751
@ </form>
1752
+ forum_render_attachment_notice();
16091753
forum_emit_js();
16101754
style_finish_page();
16111755
}
16121756
16131757
/*
@@ -1661,11 +1805,11 @@
16611805
bSameUser = login_is_individual()
16621806
&& fossil_strcmp(pPost->zUser, g.zLogin)==0;
16631807
if( isCsrfSafe && (g.perm.ModForum || (bPrivate && bSameUser)) ){
16641808
if( g.perm.ModForum && P("approve") ){
16651809
const char *zUserToTrust;
1666
- moderation_approve('f', fpid);
1810
+ moderation_forumpost_approve(fpid);
16671811
if( g.perm.AdminForum
16681812
&& PB("trust")
16691813
&& (zUserToTrust = P("trustuser"))!=0
16701814
){
16711815
db_unprotect(PROTECT_USER);
@@ -1682,11 +1826,11 @@
16821826
db_text(0,
16831827
"SELECT uuid FROM forumpost, blob"
16841828
" WHERE forumpost.fpid=%d AND blob.rid=forumpost.firt",
16851829
fpid
16861830
);
1687
- moderation_disapprove(fpid);
1831
+ moderation_forumpost_disapprove(fpid);
16881832
if( zParent ){
16891833
cgi_redirectf("%R/forumpost/%S",zParent);
16901834
}else{
16911835
cgi_redirectf("%R/forum");
16921836
}
@@ -1797,10 +1941,14 @@
17971941
}
17981942
}
17991943
forum_render_debug_options();
18001944
login_insert_csrf_secret();
18011945
@ </form>
1946
+ if( !bReply ){
1947
+ forum_render_attachment_list(rid_to_uuid(fpid));
1948
+ }
1949
+ forum_render_attachment_notice();
18021950
forum_emit_js();
18031951
style_finish_page();
18041952
}
18051953
18061954
/*
@@ -1807,17 +1955,23 @@
18071955
** SETTING: forum-close-policy boolean default=off
18081956
** If true, forum moderators may close/re-open forum posts, and reply
18091957
** to closed posts. If false, only administrators may do so. Note that
18101958
** this only affects the forum web UI, not post-closing tags which
18111959
** arrive via the command-line or from synchronization with a remote.
1960
+** This policy also determines whether moderators may delete forum
1961
+** attachments.
18121962
*/
18131963
/*
18141964
** SETTING: forum-title width=20 default=Forum
18151965
** This is the name or "title" of the Forum for this repository. The
18161966
** default is just "Forum". But in some setups, admins might want to
18171967
** change it to "Developer Forum" or "User Forum" or whatever other name
18181968
** seems more appropriate for the particular usage.
1969
+**
1970
+** SETTING: attachment-size-limit width=16
1971
+** The maximum number of bytes for an attachment. The default (or 0) is
1972
+** unlimited but a limit may be imposed by the web server or a proxy.
18191973
*/
18201974
18211975
/*
18221976
** WEBPAGE: setup_forum
18231977
**
@@ -1935,11 +2089,11 @@
19352089
@ <a href='%R/help/%h(pSetting->name)'>%h(pSetting->name)</a>:
19362090
@ </td><td>
19372091
entry_attribute("", 25, pSetting->name, zQP/*works-like:""*/,
19382092
pSetting->def, 0);
19392093
@ </td></tr>
1940
- }
2094
+ }
19412095
}
19422096
@ </tbody></table>
19432097
@ <input type='submit' name='submit' value='Apply changes'>
19442098
@ </form>
19452099
}
@@ -1974,11 +2128,11 @@
19742128
login_needed(g.anon.RdForum);
19752129
return;
19762130
}
19772131
cgi_check_for_malice();
19782132
style_set_current_feature("forum");
1979
- style_header("%s%s", db_get("forum-title","Forum"),
2133
+ style_header("%s%s", db_get("forum-title","Forum"),
19802134
isSearch ? " Search Results" : "");
19812135
style_submenu_element("Timeline", "%R/timeline?ss=v&y=f&vfx");
19822136
if( g.perm.WrForum ){
19832137
style_submenu_element("New Thread","%R/forumnew");
19842138
}else{
19852139
--- src/forum.c
+++ src/forum.c
@@ -81,29 +81,58 @@
81 db_reset(&q);
82 return res;
83 }
84
85 /*
86 ** Given a valid forumpost.fpid value, this function returns the first
87 ** fpid in the chain of edits for that forum post, or rid if no prior
88 ** versions are found.
89 */
90 static int forumpost_head_rid(int rid){
91 Stmt q;
92 int rcRid = rid;
93
94 db_prepare(&q, "SELECT fprev FROM forumpost"
95 " WHERE fpid=:rid AND fprev IS NOT NULL");
 
 
 
96 db_bind_int(&q, ":rid", rid);
97 while( SQLITE_ROW==db_step(&q) ){
98 rcRid = db_column_int(&q, 0);
99 db_reset(&q);
100 db_bind_int(&q, ":rid", rcRid);
101 }
102 db_finalize(&q);
103 return rcRid;
104 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
106 /*
107 ** Returns true if p, or any parent of p, has a non-zero iClosed
108 ** value. Returns 0 if !p. For an edited chain of post, the tag is
109 ** checked on the pEditHead entry, to simplify subsequent unlocking of
@@ -169,10 +198,70 @@
169 db_reset(&qIrt);
170 }
171 return i ? -rc : rc;
172 }
173
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174 /*
175 ** Applies or cancels a tag named zTagName on the given forum RID via
176 ** addition of a new control artifact into the repository. In order to
177 ** provide consistent behavior, it always acts on the first version of
178 ** the given forum post, walking the forumpost.fprev values to find
@@ -179,18 +268,18 @@
179 ** the head of the chain.
180 **
181 ** If addTag is true then a propagating tag is added, except as noted
182 ** below, with the given optional zReason string as the tag's
183 ** value. If addTag is false then any matching active tag on frid is
184 ** cancelled, except as noted below. zReason is ignored if doClose is
185 ** false or if zReason is NULL or starts with a NUL byte.
186 **
187 ** This function only adds a tag if forum_rid_is_tagged() indicates
188 ** that frid's head is not tagged. If a parent post is already tagged,
189 ** no tag is added. Similarly, it will only remove a tagtag from a
190 ** post which has its own tag tag, and will not remove an inherited
191 ** one from a parent post.
192 **
193 ** If addTag is true and frid is already tagged (directly or
194 ** inherited), this is a no-op. Likewise, if addTag is false and frid
195 ** itself is not tagged (not accounting for an inherited closed tag),
196 ** this is a no-op.
@@ -286,12 +375,15 @@
286
287 /*
288 ** Returns 1 if the current user is an admin, -1 if the current user
289 ** is a forum moderator and the forum-close-policy setting is true,
290 ** else returns 0. The value is cached for subsequent calls.
 
 
 
291 */
292 static int forumpost_may_close(void){
293 static int permClose = -99;
294 if( permClose!=-99 ){
295 return permClose;
296 }else if( g.perm.Admin ){
297 return permClose = 1;
@@ -749,10 +841,31 @@
749 if( pToFree ) manifest_destroy(pToFree);
750 if( p->zDisplayName==0 ) return "(unknown)";
751 return p->zDisplayName;
752 }
753
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
754
755 /*
756 ** Display a single post in a forum thread.
757 */
758 static void forum_display_post(
@@ -894,10 +1007,11 @@
894 zMimetype = "text/plain";
895 }else{
896 zMimetype = pManifest->zMimetype;
897 }
898 forum_render(0, zMimetype, pManifest->zWiki, 0, !bRaw);
 
899 }
900
901 /* When not in raw mode, finish creating the border around the post. */
902 if( !bRaw ){
903 /* If the user is able to write to the forum and if this post has not been
@@ -937,21 +1051,38 @@
937 /* Allow users to delete (reject) their own pending posts. */
938 @ <input type="submit" name="reject" value="Delete">
939 }
940 login_insert_csrf_secret();
941 @ </form>
942 if( bSelect && forumpost_may_close() && iClosed>=0 ){
943 int iHead = forumpost_head_rid(p->fpid);
944 @ <form method="post" \
945 @ action='%R/forumpost_%s(iClosed > 0 ? "reopen" : "close")'>
946 login_insert_csrf_secret();
947 @ <input type="hidden" name="fpid" value="%z(rid_to_uuid(iHead))" />
948 if( moderation_pending(p->fpid)==0 ){
949 @ <input type="button" value='%s(iClosed ? "Re-open" : "Close")' \
950 @ class='%s(iClosed ? "action-reopen" : "action-close")'/>
951 }
952 @ </form>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
953 }
954 @ </div>
955 }
956 @ </div>
957 }
@@ -1280,11 +1411,11 @@
1280 }
1281
1282 /*
1283 ** Return true if a forum post should be moderated.
1284 */
1285 static int forum_need_moderation(void){
1286 if( P("domod") ) return 1;
1287 if( g.perm.WrTForum ) return 0;
1288 if( g.perm.ModForum ) return 0;
1289 return 1;
1290 }
@@ -1565,10 +1696,22 @@
1565 @ <br><label><input type="checkbox" name="fpsilent" %s(PCK("fpsilent"))> \
1566 @ Do not send notification emails</label>
1567 @ </div>
1568 }
1569 }
 
 
 
 
 
 
 
 
 
 
 
 
1570
1571 /*
1572 ** WEBPAGE: forume1
1573 **
1574 ** Start a new forum thread.
@@ -1604,10 +1747,11 @@
1604 @ <input type="submit" name="submit" value="Submit" disabled>
1605 }
1606 forum_render_debug_options();
1607 login_insert_csrf_secret();
1608 @ </form>
 
1609 forum_emit_js();
1610 style_finish_page();
1611 }
1612
1613 /*
@@ -1661,11 +1805,11 @@
1661 bSameUser = login_is_individual()
1662 && fossil_strcmp(pPost->zUser, g.zLogin)==0;
1663 if( isCsrfSafe && (g.perm.ModForum || (bPrivate && bSameUser)) ){
1664 if( g.perm.ModForum && P("approve") ){
1665 const char *zUserToTrust;
1666 moderation_approve('f', fpid);
1667 if( g.perm.AdminForum
1668 && PB("trust")
1669 && (zUserToTrust = P("trustuser"))!=0
1670 ){
1671 db_unprotect(PROTECT_USER);
@@ -1682,11 +1826,11 @@
1682 db_text(0,
1683 "SELECT uuid FROM forumpost, blob"
1684 " WHERE forumpost.fpid=%d AND blob.rid=forumpost.firt",
1685 fpid
1686 );
1687 moderation_disapprove(fpid);
1688 if( zParent ){
1689 cgi_redirectf("%R/forumpost/%S",zParent);
1690 }else{
1691 cgi_redirectf("%R/forum");
1692 }
@@ -1797,10 +1941,14 @@
1797 }
1798 }
1799 forum_render_debug_options();
1800 login_insert_csrf_secret();
1801 @ </form>
 
 
 
 
1802 forum_emit_js();
1803 style_finish_page();
1804 }
1805
1806 /*
@@ -1807,17 +1955,23 @@
1807 ** SETTING: forum-close-policy boolean default=off
1808 ** If true, forum moderators may close/re-open forum posts, and reply
1809 ** to closed posts. If false, only administrators may do so. Note that
1810 ** this only affects the forum web UI, not post-closing tags which
1811 ** arrive via the command-line or from synchronization with a remote.
 
 
1812 */
1813 /*
1814 ** SETTING: forum-title width=20 default=Forum
1815 ** This is the name or "title" of the Forum for this repository. The
1816 ** default is just "Forum". But in some setups, admins might want to
1817 ** change it to "Developer Forum" or "User Forum" or whatever other name
1818 ** seems more appropriate for the particular usage.
 
 
 
 
1819 */
1820
1821 /*
1822 ** WEBPAGE: setup_forum
1823 **
@@ -1935,11 +2089,11 @@
1935 @ <a href='%R/help/%h(pSetting->name)'>%h(pSetting->name)</a>:
1936 @ </td><td>
1937 entry_attribute("", 25, pSetting->name, zQP/*works-like:""*/,
1938 pSetting->def, 0);
1939 @ </td></tr>
1940 }
1941 }
1942 @ </tbody></table>
1943 @ <input type='submit' name='submit' value='Apply changes'>
1944 @ </form>
1945 }
@@ -1974,11 +2128,11 @@
1974 login_needed(g.anon.RdForum);
1975 return;
1976 }
1977 cgi_check_for_malice();
1978 style_set_current_feature("forum");
1979 style_header("%s%s", db_get("forum-title","Forum"),
1980 isSearch ? " Search Results" : "");
1981 style_submenu_element("Timeline", "%R/timeline?ss=v&y=f&vfx");
1982 if( g.perm.WrForum ){
1983 style_submenu_element("New Thread","%R/forumnew");
1984 }else{
1985
--- src/forum.c
+++ src/forum.c
@@ -81,29 +81,58 @@
81 db_reset(&q);
82 return res;
83 }
84
85 /*
86 ** Given a valid forumpost.fpid value, this function returns the
87 ** initial forumpost.fpid in the chain of edits for that forum post,
88 ** or rid if no prior versions are found.
89 */
90 int forumpost_head_rid(int rid){
91 static Stmt q = empty_Stmt_m;
92 int rcRid = rid;
93 if( !q.pStmt ){
94 db_static_prepare(&q,
95 "SELECT fprev FROM forumpost"
96 " WHERE fpid=:rid AND fprev IS NOT NULL"
97 );
98 }
99 db_bind_int(&q, ":rid", rid);
100 while( SQLITE_ROW==db_step(&q) ){
101 rcRid = db_column_int(&q, 0);
102 db_reset(&q);
103 db_bind_int(&q, ":rid", rcRid);
104 }
105 db_reset(&q);
106 return rcRid;
107 }
108
109 /*
110 ** Works like forumpost_head_rid() but expects zUuid to be an
111 ** unambiguous forum post name. It may be a hash prefix, so long as
112 ** it's unambiguous. Returns 0 if the name cannot be unambiguously
113 ** resolved as a forum post.
114 */
115 int forumpost_head_rid2(const char *zUuid){
116 const int fpid = symbolic_name_to_rid(zUuid, "f");
117 return fpid>0
118 ? forumpost_head_rid(fpid)
119 : 0;
120 }
121
122 /*
123 ** Given a forum post RID and user name, returns true if zUserName
124 ** matches the event.(euser,user) field for a formpost entry with the
125 ** matching RID. Returns false if no match is found. If zUserName is
126 ** 0 then login_name() is used.
127 */
128 int forumpost_is_owner(int rid, const char *zUserName){
129 return db_int(0, "SELECT 1 FROM event "
130 "WHERE type='f' AND objid=%d "
131 "AND coalesce(euser,user)=%Q",
132 rid, zUserName ? zUserName : login_name());
133 }
134
135 /*
136 ** Returns true if p, or any parent of p, has a non-zero iClosed
137 ** value. Returns 0 if !p. For an edited chain of post, the tag is
138 ** checked on the pEditHead entry, to simplify subsequent unlocking of
@@ -169,10 +198,70 @@
198 db_reset(&qIrt);
199 }
200 return i ? -rc : rc;
201 }
202
203 /* True if moderation of forum posts performs the same operation
204 ** on its attachments. */
205 #define FORUMPOST_MOD_ATTACHMENTS 1
206 #if FORUMPOST_MOD_ATTACHMENTS
207 /*
208 ** Internal helper for moderation_forumpost_...().
209 */
210 static void forumpost_prep_pending_attachids(Stmt *q, int fpid){
211 db_prepare(
212 q,
213 "SELECT attachid FROM attachment "
214 "WHERE target=("
215 " SELECT uuid FROM blob WHERE rid=%d"
216 ") and attachid in ("
217 " SELECT objid FROM modreq"
218 ")",
219 forumpost_head_rid(fpid)
220 );
221 }
222 #endif
223
224 /*
225 ** Approve the given forum post RID and any pending-approval
226 ** attachments associated with its initial version.
227 */
228 static void moderation_forumpost_approve(int fpid){
229 #if !FORUMPOST_MOD_ATTACHMENTS
230 moderation_approve('f', fpid);
231 #else
232 /* Also approve any pending attachments */
233 Stmt q;
234 moderation_approve('f', fpid);
235 forumpost_prep_pending_attachids(&q, fpid);
236 while( SQLITE_ROW==db_step(&q) ){
237 moderation_approve('a', db_column_int(&q, 0));
238 }
239 db_finalize(&q);
240 #endif
241 }
242
243 /*
244 ** Disapprove the given forum post and any pending-moderation
245 ** attachments on its initial version.
246 */
247 static void moderation_forumpost_disapprove(int fpid){
248 #if !FORUMPOST_MOD_ATTACHMENTS
249 moderation_disapprove(fpid);
250 #else
251 /* Also disapprove any pending attachments */
252 Stmt q;
253 moderation_disapprove(fpid);
254 forumpost_prep_pending_attachids(&q, fpid);
255 while( SQLITE_ROW==db_step(&q) ){
256 moderation_disapprove(db_column_int(&q, 0));
257 }
258 db_finalize(&q);
259 #endif
260 }
261 #undef FORUMPOST_MOD_ATTACHMENTS
262
263 /*
264 ** Applies or cancels a tag named zTagName on the given forum RID via
265 ** addition of a new control artifact into the repository. In order to
266 ** provide consistent behavior, it always acts on the first version of
267 ** the given forum post, walking the forumpost.fprev values to find
@@ -179,18 +268,18 @@
268 ** the head of the chain.
269 **
270 ** If addTag is true then a propagating tag is added, except as noted
271 ** below, with the given optional zReason string as the tag's
272 ** value. If addTag is false then any matching active tag on frid is
273 ** cancelled, except as noted below. zReason is ignored if it is NULL
274 ** or starts with a NUL byte, or if addTag is false.
275 **
276 ** This function only adds a tag if forum_rid_is_tagged() indicates
277 ** that frid's head is not tagged. If a parent post is already tagged,
278 ** no tag is added. Similarly, it will only remove a tag from a post
279 ** which has its own tag, and will not remove an inherited one from a
280 ** parent post.
281 **
282 ** If addTag is true and frid is already tagged (directly or
283 ** inherited), this is a no-op. Likewise, if addTag is false and frid
284 ** itself is not tagged (not accounting for an inherited closed tag),
285 ** this is a no-op.
@@ -286,12 +375,15 @@
375
376 /*
377 ** Returns 1 if the current user is an admin, -1 if the current user
378 ** is a forum moderator and the forum-close-policy setting is true,
379 ** else returns 0. The value is cached for subsequent calls.
380 **
381 ** This policy also determines whether non-admin forum moderators
382 ** may delete forum attachments.
383 */
384 int forumpost_may_close(void){
385 static int permClose = -99;
386 if( permClose!=-99 ){
387 return permClose;
388 }else if( g.perm.Admin ){
389 return permClose = 1;
@@ -749,10 +841,31 @@
841 if( pToFree ) manifest_destroy(pToFree);
842 if( p->zDisplayName==0 ) return "(unknown)";
843 return p->zDisplayName;
844 }
845
846 /*
847 ** Renders the attachment list for the given forum post.
848 ** Emits no output if there are no attachments.
849 */
850 static void forum_render_attachment_list(const char *zUuid){
851 char * zLbl = mprintf("<a href='%R/attachlist?forumpost=%s'>"
852 "Attachments:</a>", zUuid);
853 attachment_list(zUuid, zLbl,
854 ATTACHLIST_HRULE_ABOVE
855 | ATTACHLIST_SIZE
856 | ATTACHLIST_HIDE_UNAPPROVED);
857 fossil_free(zLbl);
858 }
859
860 /*
861 ** Renders the attachment list for p or (if not NULL) pEditHead.
862 */
863 static void forum_render_attachment_list2(ForumPost *p){
864 if( p->pEditHead ) p = p->pEditHead;
865 forum_render_attachment_list(p->zUuid);
866 }
867
868 /*
869 ** Display a single post in a forum thread.
870 */
871 static void forum_display_post(
@@ -894,10 +1007,11 @@
1007 zMimetype = "text/plain";
1008 }else{
1009 zMimetype = pManifest->zMimetype;
1010 }
1011 forum_render(0, zMimetype, pManifest->zWiki, 0, !bRaw);
1012 forum_render_attachment_list2(p);
1013 }
1014
1015 /* When not in raw mode, finish creating the border around the post. */
1016 if( !bRaw ){
1017 /* If the user is able to write to the forum and if this post has not been
@@ -937,21 +1051,38 @@
1051 /* Allow users to delete (reject) their own pending posts. */
1052 @ <input type="submit" name="reject" value="Delete">
1053 }
1054 login_insert_csrf_secret();
1055 @ </form>
1056
1057 if( bSelect ){
1058 const ForumPost *pHead = p->pEditHead ? p->pEditHead : p;
1059 if( forumpost_may_close() && iClosed>=0 ){
1060 @ <form method="post" \
1061 @ action='%R/forumpost_%s(iClosed > 0 ? "reopen" : "close")'>
1062 login_insert_csrf_secret();
1063 @ <input type="hidden" name="fpid" value="%s(pHead->zUuid)" />
1064 if( moderation_pending(p->fpid)==0 ){
1065 @ <input type="button" value='%s(iClosed ? "Re-open" : "Close")' \
1066 @ class='%s(iClosed ? "action-reopen" : "action-close")'/>
1067 }
1068 @ </form>
1069 }
1070 if( g.perm.Admin ||
1071 (login_is_individual()
1072 && forumpost_is_owner(p/*not pHead*/->fpid, 0)) ){
1073 /* When an admin edits someone else's post, the admin
1074 ** effectively takes over ownership of it (and we currently
1075 ** have no way of passing it back). Because of this, we
1076 ** check the ownership of `p` instead of `pHead`. */
1077 @ <form method="post" action="%R/attachadd">\
1078 @ <input type="hidden" name="forumpost" value="%T(pHead->zUuid)">
1079 @ <input type="submit" value="Attach...">
1080 login_insert_csrf_secret();
1081 moderation_pending_www(p->fpid);
1082 @ </form>
1083 }
1084 }
1085 @ </div>
1086 }
1087 @ </div>
1088 }
@@ -1280,11 +1411,11 @@
1411 }
1412
1413 /*
1414 ** Return true if a forum post should be moderated.
1415 */
1416 int forum_need_moderation(void){
1417 if( P("domod") ) return 1;
1418 if( g.perm.WrTForum ) return 0;
1419 if( g.perm.ModForum ) return 0;
1420 return 1;
1421 }
@@ -1565,10 +1696,22 @@
1696 @ <br><label><input type="checkbox" name="fpsilent" %s(PCK("fpsilent"))> \
1697 @ Do not send notification emails</label>
1698 @ </div>
1699 }
1700 }
1701
1702 /*
1703 ** If the user has AttachForum permissions, emit a notice that
1704 ** attachments may be added after saving. If p is not NULL,
1705 ** also emit its list of attachments.
1706 */
1707 static void forum_render_attachment_notice(void){
1708 if( g.perm.AttachForum ){
1709 @ <div>You will be able to attach files to this post after saving
1710 @ it.</div>
1711 }
1712 }
1713
1714 /*
1715 ** WEBPAGE: forume1
1716 **
1717 ** Start a new forum thread.
@@ -1604,10 +1747,11 @@
1747 @ <input type="submit" name="submit" value="Submit" disabled>
1748 }
1749 forum_render_debug_options();
1750 login_insert_csrf_secret();
1751 @ </form>
1752 forum_render_attachment_notice();
1753 forum_emit_js();
1754 style_finish_page();
1755 }
1756
1757 /*
@@ -1661,11 +1805,11 @@
1805 bSameUser = login_is_individual()
1806 && fossil_strcmp(pPost->zUser, g.zLogin)==0;
1807 if( isCsrfSafe && (g.perm.ModForum || (bPrivate && bSameUser)) ){
1808 if( g.perm.ModForum && P("approve") ){
1809 const char *zUserToTrust;
1810 moderation_forumpost_approve(fpid);
1811 if( g.perm.AdminForum
1812 && PB("trust")
1813 && (zUserToTrust = P("trustuser"))!=0
1814 ){
1815 db_unprotect(PROTECT_USER);
@@ -1682,11 +1826,11 @@
1826 db_text(0,
1827 "SELECT uuid FROM forumpost, blob"
1828 " WHERE forumpost.fpid=%d AND blob.rid=forumpost.firt",
1829 fpid
1830 );
1831 moderation_forumpost_disapprove(fpid);
1832 if( zParent ){
1833 cgi_redirectf("%R/forumpost/%S",zParent);
1834 }else{
1835 cgi_redirectf("%R/forum");
1836 }
@@ -1797,10 +1941,14 @@
1941 }
1942 }
1943 forum_render_debug_options();
1944 login_insert_csrf_secret();
1945 @ </form>
1946 if( !bReply ){
1947 forum_render_attachment_list(rid_to_uuid(fpid));
1948 }
1949 forum_render_attachment_notice();
1950 forum_emit_js();
1951 style_finish_page();
1952 }
1953
1954 /*
@@ -1807,17 +1955,23 @@
1955 ** SETTING: forum-close-policy boolean default=off
1956 ** If true, forum moderators may close/re-open forum posts, and reply
1957 ** to closed posts. If false, only administrators may do so. Note that
1958 ** this only affects the forum web UI, not post-closing tags which
1959 ** arrive via the command-line or from synchronization with a remote.
1960 ** This policy also determines whether moderators may delete forum
1961 ** attachments.
1962 */
1963 /*
1964 ** SETTING: forum-title width=20 default=Forum
1965 ** This is the name or "title" of the Forum for this repository. The
1966 ** default is just "Forum". But in some setups, admins might want to
1967 ** change it to "Developer Forum" or "User Forum" or whatever other name
1968 ** seems more appropriate for the particular usage.
1969 **
1970 ** SETTING: attachment-size-limit width=16
1971 ** The maximum number of bytes for an attachment. The default (or 0) is
1972 ** unlimited but a limit may be imposed by the web server or a proxy.
1973 */
1974
1975 /*
1976 ** WEBPAGE: setup_forum
1977 **
@@ -1935,11 +2089,11 @@
2089 @ <a href='%R/help/%h(pSetting->name)'>%h(pSetting->name)</a>:
2090 @ </td><td>
2091 entry_attribute("", 25, pSetting->name, zQP/*works-like:""*/,
2092 pSetting->def, 0);
2093 @ </td></tr>
2094 }
2095 }
2096 @ </tbody></table>
2097 @ <input type='submit' name='submit' value='Apply changes'>
2098 @ </form>
2099 }
@@ -1974,11 +2128,11 @@
2128 login_needed(g.anon.RdForum);
2129 return;
2130 }
2131 cgi_check_for_malice();
2132 style_set_current_feature("forum");
2133 style_header("%s%s", db_get("forum-title","Forum"),
2134 isSearch ? " Search Results" : "");
2135 style_submenu_element("Timeline", "%R/timeline?ss=v&y=f&vfx");
2136 if( g.perm.WrForum ){
2137 style_submenu_element("New Thread","%R/forumnew");
2138 }else{
2139
+14 -15
--- src/info.c
+++ src/info.c
@@ -1871,40 +1871,39 @@
18711871
@ Also attachment "%h(zFilename)" to
18721872
}else{
18731873
@ Attachment "%h(zFilename)" to
18741874
}
18751875
objType |= OBJTYPE_ATTACHMENT;
1876
- if( fossil_is_artifact_hash(zTarget) ){
1877
- if ( db_exists("SELECT 1 FROM tag WHERE tagname='tkt-%q'",
1878
- zTarget)
1879
- ){
1876
+ switch( attachment_target_type(zTarget) ){
1877
+ case CFTYPE_FORUM:
1878
+ if( g.perm.Hyperlink && g.anon.RdForum ){
1879
+ @ forum post [%z(href("%R/forumpost/%!S",zTarget))%S(zTarget)</a>]
1880
+ }else{
1881
+ @ forum post [%S(zTarget)]
1882
+ }
1883
+ break;
1884
+ case CFTYPE_TICKET:
18801885
if( g.perm.Hyperlink && g.anon.RdTkt ){
18811886
@ ticket [%z(href("%R/tktview?name=%!S",zTarget))%S(zTarget)</a>]
18821887
}else{
18831888
@ ticket [%S(zTarget)]
18841889
}
1885
- }else if( db_exists("SELECT 1 FROM tag WHERE tagname='event-%q'",
1886
- zTarget)
1887
- ){
1890
+ break;
1891
+ case CFTYPE_EVENT:
18881892
if( g.perm.Hyperlink && g.anon.RdWiki ){
18891893
@ tech note [%z(href("%R/technote/%h",zTarget))%S(zTarget)</a>]
18901894
}else{
18911895
@ tech note [%S(zTarget)]
18921896
}
1893
- }else{
1897
+ break;
1898
+ case CFTYPE_WIKI:
1899
+ default /* historical behavior - assume wiki */:
18941900
if( g.perm.Hyperlink && g.anon.RdWiki ){
18951901
@ wiki page [%z(href("%R/wiki?name=%t",zTarget))%h(zTarget)</a>]
18961902
}else{
18971903
@ wiki page [%h(zTarget)]
18981904
}
1899
- }
1900
- }else{
1901
- if( g.perm.Hyperlink && g.anon.RdWiki ){
1902
- @ wiki page [%z(href("%R/wiki?name=%t",zTarget))%h(zTarget)</a>]
1903
- }else{
1904
- @ wiki page [%h(zTarget)]
1905
- }
19061905
}
19071906
@ added by
19081907
hyperlink_to_user(zUser,zDate," on");
19091908
hyperlink_to_date(zDate,".");
19101909
cnt++;
19111910
--- src/info.c
+++ src/info.c
@@ -1871,40 +1871,39 @@
1871 @ Also attachment "%h(zFilename)" to
1872 }else{
1873 @ Attachment "%h(zFilename)" to
1874 }
1875 objType |= OBJTYPE_ATTACHMENT;
1876 if( fossil_is_artifact_hash(zTarget) ){
1877 if ( db_exists("SELECT 1 FROM tag WHERE tagname='tkt-%q'",
1878 zTarget)
1879 ){
 
 
 
 
 
1880 if( g.perm.Hyperlink && g.anon.RdTkt ){
1881 @ ticket [%z(href("%R/tktview?name=%!S",zTarget))%S(zTarget)</a>]
1882 }else{
1883 @ ticket [%S(zTarget)]
1884 }
1885 }else if( db_exists("SELECT 1 FROM tag WHERE tagname='event-%q'",
1886 zTarget)
1887 ){
1888 if( g.perm.Hyperlink && g.anon.RdWiki ){
1889 @ tech note [%z(href("%R/technote/%h",zTarget))%S(zTarget)</a>]
1890 }else{
1891 @ tech note [%S(zTarget)]
1892 }
1893 }else{
 
 
1894 if( g.perm.Hyperlink && g.anon.RdWiki ){
1895 @ wiki page [%z(href("%R/wiki?name=%t",zTarget))%h(zTarget)</a>]
1896 }else{
1897 @ wiki page [%h(zTarget)]
1898 }
1899 }
1900 }else{
1901 if( g.perm.Hyperlink && g.anon.RdWiki ){
1902 @ wiki page [%z(href("%R/wiki?name=%t",zTarget))%h(zTarget)</a>]
1903 }else{
1904 @ wiki page [%h(zTarget)]
1905 }
1906 }
1907 @ added by
1908 hyperlink_to_user(zUser,zDate," on");
1909 hyperlink_to_date(zDate,".");
1910 cnt++;
1911
--- src/info.c
+++ src/info.c
@@ -1871,40 +1871,39 @@
1871 @ Also attachment "%h(zFilename)" to
1872 }else{
1873 @ Attachment "%h(zFilename)" to
1874 }
1875 objType |= OBJTYPE_ATTACHMENT;
1876 switch( attachment_target_type(zTarget) ){
1877 case CFTYPE_FORUM:
1878 if( g.perm.Hyperlink && g.anon.RdForum ){
1879 @ forum post [%z(href("%R/forumpost/%!S",zTarget))%S(zTarget)</a>]
1880 }else{
1881 @ forum post [%S(zTarget)]
1882 }
1883 break;
1884 case CFTYPE_TICKET:
1885 if( g.perm.Hyperlink && g.anon.RdTkt ){
1886 @ ticket [%z(href("%R/tktview?name=%!S",zTarget))%S(zTarget)</a>]
1887 }else{
1888 @ ticket [%S(zTarget)]
1889 }
1890 break;
1891 case CFTYPE_EVENT:
 
1892 if( g.perm.Hyperlink && g.anon.RdWiki ){
1893 @ tech note [%z(href("%R/technote/%h",zTarget))%S(zTarget)</a>]
1894 }else{
1895 @ tech note [%S(zTarget)]
1896 }
1897 break;
1898 case CFTYPE_WIKI:
1899 default /* historical behavior - assume wiki */:
1900 if( g.perm.Hyperlink && g.anon.RdWiki ){
1901 @ wiki page [%z(href("%R/wiki?name=%t",zTarget))%h(zTarget)</a>]
1902 }else{
1903 @ wiki page [%h(zTarget)]
1904 }
 
 
 
 
 
 
 
1905 }
1906 @ added by
1907 hyperlink_to_user(zUser,zDate," on");
1908 hyperlink_to_date(zDate,".");
1909 cnt++;
1910
+3 -1
--- src/login.c
+++ src/login.c
@@ -1708,11 +1708,12 @@
17081708
p->NewTkt = p->Password = p->RdAddr =
17091709
p->TktFmt = p->Attach = p->ApndTkt =
17101710
p->ModWiki = p->ModTkt =
17111711
p->RdForum = p->WrForum = p->ModForum =
17121712
p->WrTForum = p->AdminForum = p->Chat =
1713
- p->EmailAlert = p->Announce = p->Debug = 1;
1713
+ p->EmailAlert = p->Announce = p->AttachForum =
1714
+ p->Debug = 1;
17141715
/* Fall thru into Read/Write */
17151716
case 'i': p->Read = p->Write = 1; break;
17161717
case 'o': p->Read = 1; break;
17171718
case 'z': p->Zip = 1; break;
17181719
@@ -1744,10 +1745,11 @@
17441745
case '3': p->WrForum = 1;
17451746
case '2': p->RdForum = 1; break;
17461747
17471748
case '7': p->EmailAlert = 1; break;
17481749
case 'A': p->Announce = 1; break;
1750
+ case 'B': p->AttachForum = 1; break;
17491751
case 'C': p->Chat = 1; break;
17501752
case 'D': p->Debug = 1; break;
17511753
17521754
/* The "u" privilege recursively
17531755
** inherits all privileges of the user named "reader" */
17541756
--- src/login.c
+++ src/login.c
@@ -1708,11 +1708,12 @@
1708 p->NewTkt = p->Password = p->RdAddr =
1709 p->TktFmt = p->Attach = p->ApndTkt =
1710 p->ModWiki = p->ModTkt =
1711 p->RdForum = p->WrForum = p->ModForum =
1712 p->WrTForum = p->AdminForum = p->Chat =
1713 p->EmailAlert = p->Announce = p->Debug = 1;
 
1714 /* Fall thru into Read/Write */
1715 case 'i': p->Read = p->Write = 1; break;
1716 case 'o': p->Read = 1; break;
1717 case 'z': p->Zip = 1; break;
1718
@@ -1744,10 +1745,11 @@
1744 case '3': p->WrForum = 1;
1745 case '2': p->RdForum = 1; break;
1746
1747 case '7': p->EmailAlert = 1; break;
1748 case 'A': p->Announce = 1; break;
 
1749 case 'C': p->Chat = 1; break;
1750 case 'D': p->Debug = 1; break;
1751
1752 /* The "u" privilege recursively
1753 ** inherits all privileges of the user named "reader" */
1754
--- src/login.c
+++ src/login.c
@@ -1708,11 +1708,12 @@
1708 p->NewTkt = p->Password = p->RdAddr =
1709 p->TktFmt = p->Attach = p->ApndTkt =
1710 p->ModWiki = p->ModTkt =
1711 p->RdForum = p->WrForum = p->ModForum =
1712 p->WrTForum = p->AdminForum = p->Chat =
1713 p->EmailAlert = p->Announce = p->AttachForum =
1714 p->Debug = 1;
1715 /* Fall thru into Read/Write */
1716 case 'i': p->Read = p->Write = 1; break;
1717 case 'o': p->Read = 1; break;
1718 case 'z': p->Zip = 1; break;
1719
@@ -1744,10 +1745,11 @@
1745 case '3': p->WrForum = 1;
1746 case '2': p->RdForum = 1; break;
1747
1748 case '7': p->EmailAlert = 1; break;
1749 case 'A': p->Announce = 1; break;
1750 case 'B': p->AttachForum = 1; break;
1751 case 'C': p->Chat = 1; break;
1752 case 'D': p->Debug = 1; break;
1753
1754 /* The "u" privilege recursively
1755 ** inherits all privileges of the user named "reader" */
1756
+2 -1
--- src/main.c
+++ src/main.c
@@ -97,11 +97,11 @@
9797
char RdTkt; /* r: view tickets via web */
9898
char NewTkt; /* n: create new tickets */
9999
char ApndTkt; /* c: append to tickets via the web */
100100
char WrTkt; /* w: make changes to tickets via web */
101101
char ModTkt; /* q: approve and publish ticket changes (Moderator) */
102
- char Attach; /* b: add attachments */
102
+ char Attach; /* b: add attachments to wiki or tickets */
103103
char TktFmt; /* t: create new ticket report formats */
104104
char RdAddr; /* e: read email addresses or other private data */
105105
char Zip; /* z: download zipped artifact via /zip URL */
106106
char Private; /* x: can send and receive private content */
107107
char WrUnver; /* y: can push unversioned content */
@@ -110,10 +110,11 @@
110110
char WrTForum; /* 4: Post to forums not subject to moderation */
111111
char ModForum; /* 5: Moderate (approve or reject) forum posts */
112112
char AdminForum; /* 6: Grant capability 4 to other users */
113113
char EmailAlert; /* 7: Sign up for email notifications */
114114
char Announce; /* A: Send announcements */
115
+ char AttachForum; /* B: add attachments to forum */
115116
char Chat; /* C: read or write the chatroom */
116117
char Debug; /* D: show extra Fossil debugging features */
117118
/* These last two are included to block infinite recursion */
118119
char XReader; /* u: Inherit all privileges of "reader" */
119120
char XDeveloper; /* v: Inherit all privileges of "developer" */
120121
--- src/main.c
+++ src/main.c
@@ -97,11 +97,11 @@
97 char RdTkt; /* r: view tickets via web */
98 char NewTkt; /* n: create new tickets */
99 char ApndTkt; /* c: append to tickets via the web */
100 char WrTkt; /* w: make changes to tickets via web */
101 char ModTkt; /* q: approve and publish ticket changes (Moderator) */
102 char Attach; /* b: add attachments */
103 char TktFmt; /* t: create new ticket report formats */
104 char RdAddr; /* e: read email addresses or other private data */
105 char Zip; /* z: download zipped artifact via /zip URL */
106 char Private; /* x: can send and receive private content */
107 char WrUnver; /* y: can push unversioned content */
@@ -110,10 +110,11 @@
110 char WrTForum; /* 4: Post to forums not subject to moderation */
111 char ModForum; /* 5: Moderate (approve or reject) forum posts */
112 char AdminForum; /* 6: Grant capability 4 to other users */
113 char EmailAlert; /* 7: Sign up for email notifications */
114 char Announce; /* A: Send announcements */
 
115 char Chat; /* C: read or write the chatroom */
116 char Debug; /* D: show extra Fossil debugging features */
117 /* These last two are included to block infinite recursion */
118 char XReader; /* u: Inherit all privileges of "reader" */
119 char XDeveloper; /* v: Inherit all privileges of "developer" */
120
--- src/main.c
+++ src/main.c
@@ -97,11 +97,11 @@
97 char RdTkt; /* r: view tickets via web */
98 char NewTkt; /* n: create new tickets */
99 char ApndTkt; /* c: append to tickets via the web */
100 char WrTkt; /* w: make changes to tickets via web */
101 char ModTkt; /* q: approve and publish ticket changes (Moderator) */
102 char Attach; /* b: add attachments to wiki or tickets */
103 char TktFmt; /* t: create new ticket report formats */
104 char RdAddr; /* e: read email addresses or other private data */
105 char Zip; /* z: download zipped artifact via /zip URL */
106 char Private; /* x: can send and receive private content */
107 char WrUnver; /* y: can push unversioned content */
@@ -110,10 +110,11 @@
110 char WrTForum; /* 4: Post to forums not subject to moderation */
111 char ModForum; /* 5: Moderate (approve or reject) forum posts */
112 char AdminForum; /* 6: Grant capability 4 to other users */
113 char EmailAlert; /* 7: Sign up for email notifications */
114 char Announce; /* A: Send announcements */
115 char AttachForum; /* B: add attachments to forum */
116 char Chat; /* C: read or write the chatroom */
117 char Debug; /* D: show extra Fossil debugging features */
118 /* These last two are included to block infinite recursion */
119 char XReader; /* u: Inherit all privileges of "reader" */
120 char XDeveloper; /* v: Inherit all privileges of "developer" */
121
+73 -46
--- src/manifest.c
+++ src/manifest.c
@@ -2630,26 +2630,11 @@
26302630
db_finalize(&qatt);
26312631
}
26322632
if( p->type==CFTYPE_ATTACHMENT ){
26332633
char *zComment = 0;
26342634
const char isAdd = (p->zAttachSrc && p->zAttachSrc[0]) ? 1 : 0;
2635
- /* We assume that we're attaching to a wiki page until we
2636
- ** prove otherwise (which could on a later artifact if we
2637
- ** process the attachment artifact before the artifact to
2638
- ** which it is attached!) */
2639
- char attachToType = 'w';
2640
- if( fossil_is_artifact_hash(p->zAttachTarget) ){
2641
- if( db_exists("SELECT 1 FROM tag WHERE tagname='tkt-%q'",
2642
- p->zAttachTarget)
2643
- ){
2644
- attachToType = 't'; /* Attaching to known ticket */
2645
- }else if( db_exists("SELECT 1 FROM tag WHERE tagname='event-%q'",
2646
- p->zAttachTarget)
2647
- ){
2648
- attachToType = 'e'; /* Attaching to known tech note */
2649
- }
2650
- }
2635
+ char attachToType = 0;
26512636
db_multi_exec(
26522637
"INSERT INTO attachment(attachid, mtime, src, target,"
26532638
"filename, comment, user)"
26542639
"VALUES(%d,%.17g,%Q,%Q,%Q,%Q,%Q);",
26552640
rid, p->rDate, p->zAttachSrc, p->zAttachTarget, p->zAttachName,
@@ -2661,40 +2646,82 @@
26612646
" WHERE target=%Q AND filename=%Q))"
26622647
" WHERE target=%Q AND filename=%Q",
26632648
p->zAttachTarget, p->zAttachName,
26642649
p->zAttachTarget, p->zAttachName
26652650
);
2666
- if( 'w' == attachToType ){
2667
- if( isAdd ){
2668
- zComment = mprintf(
2669
- "Add attachment [/artifact/%!S|%h] to wiki page [%h]",
2670
- p->zAttachSrc, p->zAttachName, p->zAttachTarget);
2671
- }else{
2672
- zComment = mprintf("Delete attachment \"%h\" from wiki page [%h]",
2673
- p->zAttachName, p->zAttachTarget);
2674
- }
2675
- }else if( 'e' == attachToType ){
2676
- if( isAdd ){
2677
- zComment = mprintf(
2678
- "Add attachment [/artifact/%!S|%h] to tech note [/technote/%!S|%S]",
2679
- p->zAttachSrc, p->zAttachName, p->zAttachTarget, p->zAttachTarget);
2680
- }else{
2681
- zComment = mprintf(
2682
- "Delete attachment \"/artifact/%!S|%h\" from"
2683
- " tech note [/technote/%!S|%S]",
2684
- p->zAttachName, p->zAttachName,
2685
- p->zAttachTarget,p->zAttachTarget);
2686
- }
2687
- }else{
2688
- if( isAdd ){
2689
- zComment = mprintf(
2690
- "Add attachment [/artifact/%!S|%h] to ticket [%!S|%S]",
2691
- p->zAttachSrc, p->zAttachName, p->zAttachTarget, p->zAttachTarget);
2692
- }else{
2693
- zComment = mprintf("Delete attachment \"%h\" from ticket [%!S|%S]",
2694
- p->zAttachName, p->zAttachTarget, p->zAttachTarget);
2695
- }
2651
+ switch( attachment_target_type(p->zAttachTarget) ){
2652
+ case 0:
2653
+ /* It is possible that p->zAttachTarget is not yet in this
2654
+ ** copy of the repository. If we cannot identify it yet,
2655
+ ** generate a generic /artifact link to it instead of a
2656
+ ** type-specific link or an error message. */
2657
+ attachToType = 'a';
2658
+ if( isAdd ){
2659
+ zComment = mprintf(
2660
+ "Add attachment [/artifact/%!S|%h] to [/artifact/%!S|%h]",
2661
+ p->zAttachSrc, p->zAttachName,
2662
+ p->zAttachTarget, p->zAttachTarget);
2663
+ }else{
2664
+ zComment = mprintf("Delete attachment \"%h\" from "
2665
+ "[/artifact/%!S|%h",
2666
+ p->zAttachName, p->zAttachTarget,
2667
+ p->zAttachTarget);
2668
+ }
2669
+ break;
2670
+ case CFTYPE_WIKI:
2671
+ attachToType = 'w';
2672
+ if( isAdd ){
2673
+ zComment = mprintf(
2674
+ "Add attachment [/artifact/%!S|%h] to wiki page [%h]",
2675
+ p->zAttachSrc, p->zAttachName, p->zAttachTarget);
2676
+ }else{
2677
+ zComment = mprintf("Delete attachment \"%h\" from "
2678
+ "wiki page [%h]",
2679
+ p->zAttachName, p->zAttachTarget);
2680
+ }
2681
+ break;
2682
+ case CFTYPE_EVENT:
2683
+ attachToType = 'e';
2684
+ if( isAdd ){
2685
+ zComment = mprintf(
2686
+ "Add attachment [/artifact/%!S|%h] to tech note "
2687
+ "[/technote/%!S|%S]",
2688
+ p->zAttachSrc, p->zAttachName, p->zAttachTarget, p->zAttachTarget);
2689
+ }else{
2690
+ zComment = mprintf(
2691
+ "Delete attachment \"/artifact/%!S|%h\" from"
2692
+ " tech note [/technote/%!S|%S]",
2693
+ p->zAttachName, p->zAttachName,
2694
+ p->zAttachTarget,p->zAttachTarget);
2695
+ }
2696
+ break;
2697
+ case CFTYPE_TICKET:
2698
+ attachToType = 't';
2699
+ if( isAdd ){
2700
+ zComment = mprintf(
2701
+ "Add attachment [/artifact/%!S|%h] to ticket [%!S|%S]",
2702
+ p->zAttachSrc, p->zAttachName, p->zAttachTarget, p->zAttachTarget);
2703
+ }else{
2704
+ zComment = mprintf(
2705
+ "Delete attachment \"%h\" from ticket [%!S|%S]",
2706
+ p->zAttachName, p->zAttachTarget, p->zAttachTarget);
2707
+ }
2708
+ break;
2709
+ case CFTYPE_FORUM:
2710
+ attachToType = 'f';
2711
+ if( isAdd ){
2712
+ zComment = mprintf(
2713
+ "Add attachment [/artifact/%!S|%h] to forum post "
2714
+ "[/forumpost/%!S|%S]",
2715
+ p->zAttachSrc, p->zAttachName, p->zAttachTarget, p->zAttachTarget);
2716
+ }else{
2717
+ zComment = mprintf(
2718
+ "Delete attachment \"%h\" from forum post "
2719
+ "[/forumpost/%!S|%S]",
2720
+ p->zAttachName, p->zAttachTarget, p->zAttachTarget);
2721
+ }
2722
+ break;
26962723
}
26972724
assert( manifest_event_triggers_are_enabled );
26982725
db_multi_exec(
26992726
"REPLACE INTO event(type,mtime,objid,user,comment)"
27002727
"VALUES('%c',%.17g,%d,%Q,%Q)",
27012728
--- src/manifest.c
+++ src/manifest.c
@@ -2630,26 +2630,11 @@
2630 db_finalize(&qatt);
2631 }
2632 if( p->type==CFTYPE_ATTACHMENT ){
2633 char *zComment = 0;
2634 const char isAdd = (p->zAttachSrc && p->zAttachSrc[0]) ? 1 : 0;
2635 /* We assume that we're attaching to a wiki page until we
2636 ** prove otherwise (which could on a later artifact if we
2637 ** process the attachment artifact before the artifact to
2638 ** which it is attached!) */
2639 char attachToType = 'w';
2640 if( fossil_is_artifact_hash(p->zAttachTarget) ){
2641 if( db_exists("SELECT 1 FROM tag WHERE tagname='tkt-%q'",
2642 p->zAttachTarget)
2643 ){
2644 attachToType = 't'; /* Attaching to known ticket */
2645 }else if( db_exists("SELECT 1 FROM tag WHERE tagname='event-%q'",
2646 p->zAttachTarget)
2647 ){
2648 attachToType = 'e'; /* Attaching to known tech note */
2649 }
2650 }
2651 db_multi_exec(
2652 "INSERT INTO attachment(attachid, mtime, src, target,"
2653 "filename, comment, user)"
2654 "VALUES(%d,%.17g,%Q,%Q,%Q,%Q,%Q);",
2655 rid, p->rDate, p->zAttachSrc, p->zAttachTarget, p->zAttachName,
@@ -2661,40 +2646,82 @@
2661 " WHERE target=%Q AND filename=%Q))"
2662 " WHERE target=%Q AND filename=%Q",
2663 p->zAttachTarget, p->zAttachName,
2664 p->zAttachTarget, p->zAttachName
2665 );
2666 if( 'w' == attachToType ){
2667 if( isAdd ){
2668 zComment = mprintf(
2669 "Add attachment [/artifact/%!S|%h] to wiki page [%h]",
2670 p->zAttachSrc, p->zAttachName, p->zAttachTarget);
2671 }else{
2672 zComment = mprintf("Delete attachment \"%h\" from wiki page [%h]",
2673 p->zAttachName, p->zAttachTarget);
2674 }
2675 }else if( 'e' == attachToType ){
2676 if( isAdd ){
2677 zComment = mprintf(
2678 "Add attachment [/artifact/%!S|%h] to tech note [/technote/%!S|%S]",
2679 p->zAttachSrc, p->zAttachName, p->zAttachTarget, p->zAttachTarget);
2680 }else{
2681 zComment = mprintf(
2682 "Delete attachment \"/artifact/%!S|%h\" from"
2683 " tech note [/technote/%!S|%S]",
2684 p->zAttachName, p->zAttachName,
2685 p->zAttachTarget,p->zAttachTarget);
2686 }
2687 }else{
2688 if( isAdd ){
2689 zComment = mprintf(
2690 "Add attachment [/artifact/%!S|%h] to ticket [%!S|%S]",
2691 p->zAttachSrc, p->zAttachName, p->zAttachTarget, p->zAttachTarget);
2692 }else{
2693 zComment = mprintf("Delete attachment \"%h\" from ticket [%!S|%S]",
2694 p->zAttachName, p->zAttachTarget, p->zAttachTarget);
2695 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2696 }
2697 assert( manifest_event_triggers_are_enabled );
2698 db_multi_exec(
2699 "REPLACE INTO event(type,mtime,objid,user,comment)"
2700 "VALUES('%c',%.17g,%d,%Q,%Q)",
2701
--- src/manifest.c
+++ src/manifest.c
@@ -2630,26 +2630,11 @@
2630 db_finalize(&qatt);
2631 }
2632 if( p->type==CFTYPE_ATTACHMENT ){
2633 char *zComment = 0;
2634 const char isAdd = (p->zAttachSrc && p->zAttachSrc[0]) ? 1 : 0;
2635 char attachToType = 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2636 db_multi_exec(
2637 "INSERT INTO attachment(attachid, mtime, src, target,"
2638 "filename, comment, user)"
2639 "VALUES(%d,%.17g,%Q,%Q,%Q,%Q,%Q);",
2640 rid, p->rDate, p->zAttachSrc, p->zAttachTarget, p->zAttachName,
@@ -2661,40 +2646,82 @@
2646 " WHERE target=%Q AND filename=%Q))"
2647 " WHERE target=%Q AND filename=%Q",
2648 p->zAttachTarget, p->zAttachName,
2649 p->zAttachTarget, p->zAttachName
2650 );
2651 switch( attachment_target_type(p->zAttachTarget) ){
2652 case 0:
2653 /* It is possible that p->zAttachTarget is not yet in this
2654 ** copy of the repository. If we cannot identify it yet,
2655 ** generate a generic /artifact link to it instead of a
2656 ** type-specific link or an error message. */
2657 attachToType = 'a';
2658 if( isAdd ){
2659 zComment = mprintf(
2660 "Add attachment [/artifact/%!S|%h] to [/artifact/%!S|%h]",
2661 p->zAttachSrc, p->zAttachName,
2662 p->zAttachTarget, p->zAttachTarget);
2663 }else{
2664 zComment = mprintf("Delete attachment \"%h\" from "
2665 "[/artifact/%!S|%h",
2666 p->zAttachName, p->zAttachTarget,
2667 p->zAttachTarget);
2668 }
2669 break;
2670 case CFTYPE_WIKI:
2671 attachToType = 'w';
2672 if( isAdd ){
2673 zComment = mprintf(
2674 "Add attachment [/artifact/%!S|%h] to wiki page [%h]",
2675 p->zAttachSrc, p->zAttachName, p->zAttachTarget);
2676 }else{
2677 zComment = mprintf("Delete attachment \"%h\" from "
2678 "wiki page [%h]",
2679 p->zAttachName, p->zAttachTarget);
2680 }
2681 break;
2682 case CFTYPE_EVENT:
2683 attachToType = 'e';
2684 if( isAdd ){
2685 zComment = mprintf(
2686 "Add attachment [/artifact/%!S|%h] to tech note "
2687 "[/technote/%!S|%S]",
2688 p->zAttachSrc, p->zAttachName, p->zAttachTarget, p->zAttachTarget);
2689 }else{
2690 zComment = mprintf(
2691 "Delete attachment \"/artifact/%!S|%h\" from"
2692 " tech note [/technote/%!S|%S]",
2693 p->zAttachName, p->zAttachName,
2694 p->zAttachTarget,p->zAttachTarget);
2695 }
2696 break;
2697 case CFTYPE_TICKET:
2698 attachToType = 't';
2699 if( isAdd ){
2700 zComment = mprintf(
2701 "Add attachment [/artifact/%!S|%h] to ticket [%!S|%S]",
2702 p->zAttachSrc, p->zAttachName, p->zAttachTarget, p->zAttachTarget);
2703 }else{
2704 zComment = mprintf(
2705 "Delete attachment \"%h\" from ticket [%!S|%S]",
2706 p->zAttachName, p->zAttachTarget, p->zAttachTarget);
2707 }
2708 break;
2709 case CFTYPE_FORUM:
2710 attachToType = 'f';
2711 if( isAdd ){
2712 zComment = mprintf(
2713 "Add attachment [/artifact/%!S|%h] to forum post "
2714 "[/forumpost/%!S|%S]",
2715 p->zAttachSrc, p->zAttachName, p->zAttachTarget, p->zAttachTarget);
2716 }else{
2717 zComment = mprintf(
2718 "Delete attachment \"%h\" from forum post "
2719 "[/forumpost/%!S|%S]",
2720 p->zAttachName, p->zAttachTarget, p->zAttachTarget);
2721 }
2722 break;
2723 }
2724 assert( manifest_event_triggers_are_enabled );
2725 db_multi_exec(
2726 "REPLACE INTO event(type,mtime,objid,user,comment)"
2727 "VALUES('%c',%.17g,%d,%Q,%Q)",
2728
+128 -1
--- src/moderate.c
+++ src/moderate.c
@@ -65,11 +65,25 @@
6565
** false without generating any output.
6666
*/
6767
int moderation_pending_www(int rid){
6868
int pending = moderation_pending(rid);
6969
if( pending ){
70
- @ <span class="modpending">(Awaiting Moderator Approval)</span>
70
+#if 0
71
+ if( moderation_user_could(rid, 1, 0) ){
72
+ /* It would be nice to emit a link to the appropriate page to
73
+ ** approve/reject the moderation, but for that we need
74
+ ** artifact-type-dependent info and links. That's complicated by
75
+ ** the fact that deriving whether rid refers to an attachment or
76
+ ** an attachment target is apparently tricky because of how
77
+ ** attachments are recorded in the event table. */
78
+ @ <span class="modpending">(<a href="%R/WHAT_GOES_HERE?">\
79
+ @Awaiting Moderator Approval</a>)</span>
80
+ }else
81
+#endif
82
+ {
83
+ @ <span class="modpending">(Awaiting Moderator Approval)</span>
84
+ }
7185
}
7286
return pending;
7387
}
7488
7589
@@ -225,5 +239,118 @@
225239
}
226240
db_finalize(&q);
227241
setup_incr_cfgcnt();
228242
db_end_transaction(0);
229243
}
244
+
245
+/*
246
+** Returns true if the current user could ostensibly moderate the blob
247
+** refered to by rid, irrespective of whether that object is currently
248
+** pending moderation. If rid is not an event.objid value then this
249
+** returns 0.
250
+**
251
+** If bMayDeny is true then a matching user is permitted to moderate a
252
+** decline by not an approval. Pass 1 here if true should be returned
253
+** if the current user matches the artifact. When passing false, it
254
+** will only return true for users who have explicit moderation
255
+** permissions. The purpose of this is to exclude pending-moderation
256
+** from the current user in some contexts but not others.
257
+**
258
+** zWho is an optional user name to consider for ownership of an
259
+** artifact, as compared to the artifact's matching event.(euser,user)
260
+** fields. If 0 then it defaults to login_name(). This is strictly a
261
+** name comparison - it does not inspect zWho's repo-level
262
+** permissions.
263
+**
264
+** Design issue: since this gets its info from the event table, it
265
+** cannot unambiguously distinguish between an attachment-capable
266
+** artifact type and attachments to one. Attachment events are encoded
267
+** with type=X, where X is the same as the artifact type to which the
268
+** attachment was applied.
269
+**
270
+** The moderation rules applied here are:
271
+**
272
+** - Admins may always moderate. This is a fast path which bypasses
273
+** artifact lookup. For non-admins, we look for a record in the
274
+** event table.
275
+**
276
+** - Forum, Wiki, and Ticket moderators may always moderate a matching
277
+** artifact. If bMayDeny is true then an artifact's owner, even if
278
+** not a moderator, may moderate it. i.e. a non-moderator owner can
279
+** reject their pending-moderation objects but they may not approve
280
+** them.
281
+**
282
+** - Returns 0 for all other artifact types except that it will always
283
+** return true for admins because that check skips looking at the
284
+** db.
285
+**
286
+ */
287
+int moderation_user_could(int rid, int bMayDeny, const char *zWho){
288
+ static Stmt q;
289
+ int rc = 0;
290
+ if( g.perm.Admin ) return g.perm.Admin;
291
+ if( !q.pStmt ){
292
+ db_static_prepare(
293
+ &q,
294
+ "SELECT coalesce(euser,user)=:user, type FROM event "
295
+ "WHERE objid=:rid"
296
+ );
297
+ }
298
+ db_bind_int(&q, ":rid", rid);
299
+ db_bind_text(&q, ":user", zWho ? zWho : login_name());
300
+ if( SQLITE_ROW==db_step(&q) ){
301
+ const int bIsOwner = db_column_int(&q, 0);
302
+ const char *zType = db_column_text(&q, 1);
303
+ switch( zType ? zType[0] : 0 ){
304
+ case 'f': rc = g.perm.ModForum || (bIsOwner && bMayDeny); break;
305
+ case 't': rc = g.perm.ModTkt || (bIsOwner && bMayDeny); break;
306
+ case 'w': rc = g.perm.ModWiki || (bIsOwner && bMayDeny); break;
307
+ /* case 'e': Technotes and their attachments are not subject
308
+ ** to moderation. */
309
+ default: break;
310
+ }
311
+ }
312
+ db_reset(&q);
313
+ return rc;
314
+}
315
+
316
+
317
+/*
318
+** COMMAND: test-user-could-moderate
319
+**
320
+** Usage: %fossil test-user-could-moderate ?-deny? user-name ...artifactNames
321
+**
322
+** Tests whether a given user would have the ability to moderate
323
+** the given artifacts. The -deny flag indicates that the check should
324
+** permit moderation if the artifact is owned by the same user.
325
+*/
326
+void test_moderation_user_could_cmd(void){
327
+ const char *zWho;
328
+ const int bMayDeny = find_option("deny",0,0) != 0;
329
+ char * zCap;
330
+ int i;
331
+ db_find_and_open_repository(0,0);
332
+ verify_all_options();
333
+ if( g.argc<4 ){
334
+ usage("user-name artifact-names...");
335
+ }
336
+ zWho = g.zLogin = g.argv[2];
337
+ zCap = db_text(
338
+ 0, "SELECT cap FROM user WHERE login=%Q", zWho
339
+ );
340
+ if( !zCap ){
341
+ fossil_fatal("Cannot determine capabilities of user %s", zWho);
342
+ }
343
+ login_set_capabilities(zCap, 0);
344
+ fossil_print("User: %s\nCaps: %s\n", zWho, zCap);
345
+ fossil_free(zCap);
346
+ for( i = 3; i < g.argc; ++i ){
347
+ const char * zArg = g.argv[i];
348
+ int rid = symbolic_name_to_rid(zArg, "*");
349
+ int may;
350
+ if( rid<=0 ){
351
+ fossil_fatal("Cannot resolve name: %s", zArg);
352
+ }
353
+ may = moderation_user_could(rid, bMayDeny, zWho);
354
+ fossil_print("%s\t\t=> %d\t=> %s\n", zArg, rid, may ? "yes" : "no");
355
+ }
356
+}
230357
--- src/moderate.c
+++ src/moderate.c
@@ -65,11 +65,25 @@
65 ** false without generating any output.
66 */
67 int moderation_pending_www(int rid){
68 int pending = moderation_pending(rid);
69 if( pending ){
70 @ <span class="modpending">(Awaiting Moderator Approval)</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71 }
72 return pending;
73 }
74
75
@@ -225,5 +239,118 @@
225 }
226 db_finalize(&q);
227 setup_incr_cfgcnt();
228 db_end_transaction(0);
229 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
230
--- src/moderate.c
+++ src/moderate.c
@@ -65,11 +65,25 @@
65 ** false without generating any output.
66 */
67 int moderation_pending_www(int rid){
68 int pending = moderation_pending(rid);
69 if( pending ){
70 #if 0
71 if( moderation_user_could(rid, 1, 0) ){
72 /* It would be nice to emit a link to the appropriate page to
73 ** approve/reject the moderation, but for that we need
74 ** artifact-type-dependent info and links. That's complicated by
75 ** the fact that deriving whether rid refers to an attachment or
76 ** an attachment target is apparently tricky because of how
77 ** attachments are recorded in the event table. */
78 @ <span class="modpending">(<a href="%R/WHAT_GOES_HERE?">\
79 @Awaiting Moderator Approval</a>)</span>
80 }else
81 #endif
82 {
83 @ <span class="modpending">(Awaiting Moderator Approval)</span>
84 }
85 }
86 return pending;
87 }
88
89
@@ -225,5 +239,118 @@
239 }
240 db_finalize(&q);
241 setup_incr_cfgcnt();
242 db_end_transaction(0);
243 }
244
245 /*
246 ** Returns true if the current user could ostensibly moderate the blob
247 ** refered to by rid, irrespective of whether that object is currently
248 ** pending moderation. If rid is not an event.objid value then this
249 ** returns 0.
250 **
251 ** If bMayDeny is true then a matching user is permitted to moderate a
252 ** decline by not an approval. Pass 1 here if true should be returned
253 ** if the current user matches the artifact. When passing false, it
254 ** will only return true for users who have explicit moderation
255 ** permissions. The purpose of this is to exclude pending-moderation
256 ** from the current user in some contexts but not others.
257 **
258 ** zWho is an optional user name to consider for ownership of an
259 ** artifact, as compared to the artifact's matching event.(euser,user)
260 ** fields. If 0 then it defaults to login_name(). This is strictly a
261 ** name comparison - it does not inspect zWho's repo-level
262 ** permissions.
263 **
264 ** Design issue: since this gets its info from the event table, it
265 ** cannot unambiguously distinguish between an attachment-capable
266 ** artifact type and attachments to one. Attachment events are encoded
267 ** with type=X, where X is the same as the artifact type to which the
268 ** attachment was applied.
269 **
270 ** The moderation rules applied here are:
271 **
272 ** - Admins may always moderate. This is a fast path which bypasses
273 ** artifact lookup. For non-admins, we look for a record in the
274 ** event table.
275 **
276 ** - Forum, Wiki, and Ticket moderators may always moderate a matching
277 ** artifact. If bMayDeny is true then an artifact's owner, even if
278 ** not a moderator, may moderate it. i.e. a non-moderator owner can
279 ** reject their pending-moderation objects but they may not approve
280 ** them.
281 **
282 ** - Returns 0 for all other artifact types except that it will always
283 ** return true for admins because that check skips looking at the
284 ** db.
285 **
286 */
287 int moderation_user_could(int rid, int bMayDeny, const char *zWho){
288 static Stmt q;
289 int rc = 0;
290 if( g.perm.Admin ) return g.perm.Admin;
291 if( !q.pStmt ){
292 db_static_prepare(
293 &q,
294 "SELECT coalesce(euser,user)=:user, type FROM event "
295 "WHERE objid=:rid"
296 );
297 }
298 db_bind_int(&q, ":rid", rid);
299 db_bind_text(&q, ":user", zWho ? zWho : login_name());
300 if( SQLITE_ROW==db_step(&q) ){
301 const int bIsOwner = db_column_int(&q, 0);
302 const char *zType = db_column_text(&q, 1);
303 switch( zType ? zType[0] : 0 ){
304 case 'f': rc = g.perm.ModForum || (bIsOwner && bMayDeny); break;
305 case 't': rc = g.perm.ModTkt || (bIsOwner && bMayDeny); break;
306 case 'w': rc = g.perm.ModWiki || (bIsOwner && bMayDeny); break;
307 /* case 'e': Technotes and their attachments are not subject
308 ** to moderation. */
309 default: break;
310 }
311 }
312 db_reset(&q);
313 return rc;
314 }
315
316
317 /*
318 ** COMMAND: test-user-could-moderate
319 **
320 ** Usage: %fossil test-user-could-moderate ?-deny? user-name ...artifactNames
321 **
322 ** Tests whether a given user would have the ability to moderate
323 ** the given artifacts. The -deny flag indicates that the check should
324 ** permit moderation if the artifact is owned by the same user.
325 */
326 void test_moderation_user_could_cmd(void){
327 const char *zWho;
328 const int bMayDeny = find_option("deny",0,0) != 0;
329 char * zCap;
330 int i;
331 db_find_and_open_repository(0,0);
332 verify_all_options();
333 if( g.argc<4 ){
334 usage("user-name artifact-names...");
335 }
336 zWho = g.zLogin = g.argv[2];
337 zCap = db_text(
338 0, "SELECT cap FROM user WHERE login=%Q", zWho
339 );
340 if( !zCap ){
341 fossil_fatal("Cannot determine capabilities of user %s", zWho);
342 }
343 login_set_capabilities(zCap, 0);
344 fossil_print("User: %s\nCaps: %s\n", zWho, zCap);
345 fossil_free(zCap);
346 for( i = 3; i < g.argc; ++i ){
347 const char * zArg = g.argv[i];
348 int rid = symbolic_name_to_rid(zArg, "*");
349 int may;
350 if( rid<=0 ){
351 fossil_fatal("Cannot resolve name: %s", zArg);
352 }
353 may = moderation_user_could(rid, bMayDeny, zWho);
354 fossil_print("%s\t\t=> %d\t=> %s\n", zArg, rid, may ? "yes" : "no");
355 }
356 }
357
+1 -1
--- src/schema.c
+++ src/schema.c
@@ -427,11 +427,11 @@
427427
@ CREATE TABLE attachment(
428428
@ attachid INTEGER PRIMARY KEY, -- Local id for this attachment
429429
@ isLatest BOOLEAN DEFAULT 0, -- True if this is the one to use
430430
@ mtime TIMESTAMP, -- Last changed. Julian day.
431431
@ src TEXT, -- Hash of the attachment. NULL to delete
432
-@ target TEXT, -- Object attached to. Wikiname or Tkt hash
432
+@ target TEXT, -- Object attached to. Wikiname or Tkt/Event/Forum post ID
433433
@ filename TEXT, -- Filename for the attachment
434434
@ comment TEXT, -- Comment associated with this attachment
435435
@ user TEXT -- Name of user adding attachment
436436
@ );
437437
@ CREATE INDEX attachment_idx1 ON attachment(target, filename, mtime);
438438
--- src/schema.c
+++ src/schema.c
@@ -427,11 +427,11 @@
427 @ CREATE TABLE attachment(
428 @ attachid INTEGER PRIMARY KEY, -- Local id for this attachment
429 @ isLatest BOOLEAN DEFAULT 0, -- True if this is the one to use
430 @ mtime TIMESTAMP, -- Last changed. Julian day.
431 @ src TEXT, -- Hash of the attachment. NULL to delete
432 @ target TEXT, -- Object attached to. Wikiname or Tkt hash
433 @ filename TEXT, -- Filename for the attachment
434 @ comment TEXT, -- Comment associated with this attachment
435 @ user TEXT -- Name of user adding attachment
436 @ );
437 @ CREATE INDEX attachment_idx1 ON attachment(target, filename, mtime);
438
--- src/schema.c
+++ src/schema.c
@@ -427,11 +427,11 @@
427 @ CREATE TABLE attachment(
428 @ attachid INTEGER PRIMARY KEY, -- Local id for this attachment
429 @ isLatest BOOLEAN DEFAULT 0, -- True if this is the one to use
430 @ mtime TIMESTAMP, -- Last changed. Julian day.
431 @ src TEXT, -- Hash of the attachment. NULL to delete
432 @ target TEXT, -- Object attached to. Wikiname or Tkt/Event/Forum post ID
433 @ filename TEXT, -- Filename for the attachment
434 @ comment TEXT, -- Comment associated with this attachment
435 @ user TEXT -- Name of user adding attachment
436 @ );
437 @ CREATE INDEX attachment_idx1 ON attachment(target, filename, mtime);
438
--- src/setupuser.c
+++ src/setupuser.c
@@ -821,10 +821,12 @@
821821
@ Check-Out%s(B('o'))</label>
822822
@ <li><label><input type="checkbox" name="ah"%s(oa['h'])>
823823
@ Hyperlinks%s(B('h'))</label>
824824
@ <li><label><input type="checkbox" name="ab"%s(oa['b'])>
825825
@ Attachments%s(B('b'))</label>
826
+ @ <li><label><input type="checkbox" name="aB"%s(oa['B'])>
827
+ @ Forum Attachments%s(B('B'))</label>
826828
@ <li><label><input type="checkbox" name="ag"%s(oa['g'])>
827829
@ Clone%s(B('g'))</label>
828830
@ <li><label><input type="checkbox" name="aj"%s(oa['j'])>
829831
@ Read Wiki%s(B('j'))</label>
830832
@ <li><label><input type="checkbox" name="af"%s(oa['f'])>
@@ -857,10 +859,12 @@
857859
@ Read Forum%s(B('2'))</label>
858860
@ <li><label><input type="checkbox" name="a3"%s(oa['3'])>
859861
@ Write Forum%s(B('3'))</label>
860862
@ <li><label><input type="checkbox" name="a4"%s(oa['4'])>
861863
@ WriteTrusted Forum%s(B('4'))</label>
864
+ @ <li><label><input type="checkbox" name="aB"%s(oa['B'])>
865
+ @ Attach to Forum%s(B('B'))</label>
862866
@ <li><label><input type="checkbox" name="a5"%s(oa['5'])>
863867
@ Moderate Forum%s(B('5'))</label>
864868
@ <li><label><input type="checkbox" name="a6"%s(oa['6'])>
865869
@ Supervise Forum%s(B('6'))</label>
866870
@ <li><label><input type="checkbox" name="a7"%s(oa['7'])>
867871
--- src/setupuser.c
+++ src/setupuser.c
@@ -821,10 +821,12 @@
821 @ Check-Out%s(B('o'))</label>
822 @ <li><label><input type="checkbox" name="ah"%s(oa['h'])>
823 @ Hyperlinks%s(B('h'))</label>
824 @ <li><label><input type="checkbox" name="ab"%s(oa['b'])>
825 @ Attachments%s(B('b'))</label>
 
 
826 @ <li><label><input type="checkbox" name="ag"%s(oa['g'])>
827 @ Clone%s(B('g'))</label>
828 @ <li><label><input type="checkbox" name="aj"%s(oa['j'])>
829 @ Read Wiki%s(B('j'))</label>
830 @ <li><label><input type="checkbox" name="af"%s(oa['f'])>
@@ -857,10 +859,12 @@
857 @ Read Forum%s(B('2'))</label>
858 @ <li><label><input type="checkbox" name="a3"%s(oa['3'])>
859 @ Write Forum%s(B('3'))</label>
860 @ <li><label><input type="checkbox" name="a4"%s(oa['4'])>
861 @ WriteTrusted Forum%s(B('4'))</label>
 
 
862 @ <li><label><input type="checkbox" name="a5"%s(oa['5'])>
863 @ Moderate Forum%s(B('5'))</label>
864 @ <li><label><input type="checkbox" name="a6"%s(oa['6'])>
865 @ Supervise Forum%s(B('6'))</label>
866 @ <li><label><input type="checkbox" name="a7"%s(oa['7'])>
867
--- src/setupuser.c
+++ src/setupuser.c
@@ -821,10 +821,12 @@
821 @ Check-Out%s(B('o'))</label>
822 @ <li><label><input type="checkbox" name="ah"%s(oa['h'])>
823 @ Hyperlinks%s(B('h'))</label>
824 @ <li><label><input type="checkbox" name="ab"%s(oa['b'])>
825 @ Attachments%s(B('b'))</label>
826 @ <li><label><input type="checkbox" name="aB"%s(oa['B'])>
827 @ Forum Attachments%s(B('B'))</label>
828 @ <li><label><input type="checkbox" name="ag"%s(oa['g'])>
829 @ Clone%s(B('g'))</label>
830 @ <li><label><input type="checkbox" name="aj"%s(oa['j'])>
831 @ Read Wiki%s(B('j'))</label>
832 @ <li><label><input type="checkbox" name="af"%s(oa['f'])>
@@ -857,10 +859,12 @@
859 @ Read Forum%s(B('2'))</label>
860 @ <li><label><input type="checkbox" name="a3"%s(oa['3'])>
861 @ Write Forum%s(B('3'))</label>
862 @ <li><label><input type="checkbox" name="a4"%s(oa['4'])>
863 @ WriteTrusted Forum%s(B('4'))</label>
864 @ <li><label><input type="checkbox" name="aB"%s(oa['B'])>
865 @ Attach to Forum%s(B('B'))</label>
866 @ <li><label><input type="checkbox" name="a5"%s(oa['5'])>
867 @ Moderate Forum%s(B('5'))</label>
868 @ <li><label><input type="checkbox" name="a6"%s(oa['6'])>
869 @ Supervise Forum%s(B('6'))</label>
870 @ <li><label><input type="checkbox" name="a7"%s(oa['7'])>
871
+45 -36
--- www/caps/ref.html
+++ www/caps/ref.html
@@ -56,28 +56,29 @@
5656
Admin users have <em>all</em> of the capabilities below except for
5757
<a href="#s">setup</a>, <a herf="#x">Private</a>, and <a href="#y">WrUnver</a>.
5858
See <a href="admin-v-setup.md">Admin vs. Setup</a> for a more
5959
nuanced discussion. Mnemonic: <b>a</b>dministrate.
6060
</td>
61
- </tr>
61
+ </tr>
6262
6363
<tr id="b">
6464
<th>b</th>
6565
<th>Attach</th>
6666
<td>
67
- Add attachments to wiki articles or tickets. Mnemonics: <b>b</b>ind,
68
- <b>b</b>utton, <b>b</b>ond, or <b>b</b>olt.
67
+ Add attachments to wiki articles, technotes, or tickets.
68
+ Mnemonics: <b>b</b>ind, <b>b</b>utton, <b>b</b>ond, or <b>b</b>olt.
69
+ See also: <a href='#B'>B</a>.
6970
</td>
70
- </tr>
71
+ </tr>
7172
7273
<tr id="c">
7374
<th>c</th>
7475
<th>ApndTkt</th>
7576
<td>
7677
Append comments to existing tickets. Mnemonic: <b>c</b>omment.
7778
</td>
78
- </tr>
79
+ </tr>
7980
8081
<tr id="d">
8182
<th>d</th>
8283
<th>n/a</th>
8384
<td>
@@ -100,21 +101,21 @@
100101
identifying information</a> (PII) about other users such as email
101102
addresses. Mnemonics: show <b>e</b>mail addresses; or
102103
<b>E</b>urope, home of <a
103104
href="https://en.wikipedia.org/wiki/General_Data_Protection_Regulation">GDPR</a>.
104105
</td>
105
- </tr>
106
+ </tr>
106107
107108
<tr id="f">
108109
<th>f</th>
109110
<th>NewWiki</th>
110111
<td>
111112
Create new wiki articles. Mnemonic: <b>f</b>ast, English
112113
translation of the Hawaiian word <a
113114
href="https://en.wikipedia.org/wiki/History_of_wikis#WikiWikiWeb,_the_first_wiki"><i>wiki</i></a>.
114115
</td>
115
- </tr>
116
+ </tr>
116117
117118
<tr id="g">
118119
<th>g</th>
119120
<th>Clone</th>
120121
<td>
@@ -121,11 +122,11 @@
121122
Clone the repository. Note that this is distinct from <a
122123
href="#o">check-out capability, <b>o</b></a>; and that upon cloning
123124
not just files, but also tickets, wikis, technotes and forum posts
124125
are tranferred. Mnemonic: <b>g</b>et.
125126
</td>
126
- </tr>
127
+ </tr>
127128
128129
<tr id="h">
129130
<th>h</th>
130131
<th>Hyperlink</th>
131132
<td>
@@ -134,11 +135,11 @@
134135
“nobody” category, to <a href="../antibot.wiki">prevent bots from
135136
wandering around aimlessly</a> in the site’s hyperlink web, <a
136137
href="../loadmgmt.md">chewing up server resources</a> to little
137138
good purpose. Mnemonic: <b>h</b>yperlink.
138139
</td>
139
- </tr>
140
+ </tr>
140141
141142
<tr id="i">
142143
<th>i</th>
143144
<th>Write</th>
144145
<td>
@@ -149,20 +150,20 @@
149150
Also note that not just files, but also tickets, wikis, technotes
150151
and forum posts will be accepted from clones upon syncronization.
151152
Granting this capability also grants <b>o (Read)</b> Mnemonics:
152153
<b>i</b>nput, check <b>i</b>n changes.
153154
</td>
154
- </tr>
155
+ </tr>
155156
156157
<tr id="j">
157158
<th>j</th>
158159
<th>RdWiki</th>
159160
<td>
160161
View wiki articles. Mnemonic: in<b>j</b>est page content. (All
161162
right, you critics, you do better, then.)
162163
</td>
163
- </tr>
164
+ </tr>
164165
165166
<tr id="k">
166167
<th>k</th>
167168
<th>WrWiki</th>
168169
<td>
@@ -169,11 +170,11 @@
169170
Edit wiki articles. Granting this capability also grants <a
170171
href="#j"><b>RdWiki</b></a> and <a href="#m"><b>ApndWiki</b></a>,
171172
but it does <em>not</em> grant <a href="#f"><b>NewWiki</b></a>!
172173
Mnemonic: <b>k</b>ontribute.
173174
</td>
174
- </tr>
175
+ </tr>
175176
176177
<tr id="l">
177178
<th>l</th>
178179
<th>ModWiki</th>
179180
<td>
@@ -180,63 +181,63 @@
180181
Moderate <a href="#m">wiki article appends</a>. Appends do not get
181182
saved permanently to the receiving repo’s block chain until <a
182183
href="#s">Setup</a> or someone with this cap approves it.
183184
Mnemonic: a<b>l</b>low.
184185
</td>
185
- </tr>
186
+ </tr>
186187
187188
<tr id="m">
188189
<th>m</th>
189190
<th>ApndWiki</th>
190191
<td>
191192
Append content to existing wiki articles. Mnemonic: a<b>m</b>end
192193
wiki
193194
</td>
194
- </tr>
195
+ </tr>
195196
196197
<tr id="n">
197198
<th>n</th>
198199
<th>NewTkt</th>
199200
<td>
200201
File new tickets. Mnemonic: <b>n</b>ew ticket.
201202
</td>
202
- </tr>
203
+ </tr>
203204
204205
<tr id="o">
205206
<th>o</th>
206207
<th>Read</th>
207208
<td>
208209
Read content and history of files from a remote Fossil instance over
209210
HTTP. See <a href="index.md#read-v-clone">Reading vs.
210211
Cloning</a>. Mnemonic: check <b>o</b>ut remote repo contents.
211212
</td>
212
- </tr>
213
+ </tr>
213214
214215
<tr id="p">
215216
<th>p</th>
216217
<th>Password</th>
217218
<td>
218219
Change one’s own password. Mnemonic: <b>p</b>assword.
219220
</td>
220
- </tr>
221
+ </tr>
221222
222223
<tr id="q">
223224
<th>q</th>
224225
<th>ModTkt</th>
225226
<td>
226227
Moderate tickets: delete comments appended to tickets. Mnemonic:
227228
<b>q</b>uash noise commentary.
228229
</td>
229
- </tr>
230
+ </tr>
230231
231232
<tr id="r">
232233
<th>r</th>
233234
<th>RdTkt</th>
234235
<td>
235236
View existing tickets. Mnemonic: <b>r</b>ead tickets.
236237
</td>
237
- </tr>
238
+ </tr>
238239
239240
<tr id="s">
240241
<th>s</th>
241242
<th>Setup</th>
242243
<td>
@@ -256,41 +257,41 @@
256257
because it is internally restricted to read-only queries on the
257258
tickets table only. (This restriction is done with an SQLite
258259
authorization hook, not by any method so weak as SQL text
259260
filtering.) Mnemonic: new <b>t</b>icket report.
260261
</td>
261
- </tr>
262
+ </tr>
262263
263264
<tr id="u">
264265
<th>u</th>
265266
<th>n/a</th>
266267
<td>
267268
Inherit all capabilities of the “reader” user category; does not
268269
have a dedicated flag internally within Fossil. Mnemonic:
269270
<a href="./index.md#ucat"><b>u</b>ser</a>
270271
</td>
271
- </tr>
272
+ </tr>
272273
273274
<tr id="v">
274275
<th>v</th>
275276
<th>n/a</th>
276277
<td>
277278
Inherit all capabilities of the “developer” user category; does
278279
not have a dedicated flag internally within Fossil. Mnemonic:
279280
de<b>v</b>eloper.
280281
</td>
281
- </tr>
282
+ </tr>
282283
283284
<tr id="w">
284285
<th>w</th>
285286
<th>WrTkt</th>
286287
<td>
287288
Edit existing tickets. Granting this capability also grants <a
288289
href="#r"><b>RdTkt</b></a>, <a href="#c"><b>ApndTkt</b></a>, and
289290
<a href="#n"><b>NewTkt</b></a>. Mnemonic: <b>w</b>rite to ticket.
290291
</td>
291
- </tr>
292
+ </tr>
292293
293294
<tr id="x">
294295
<th>x</th>
295296
<th>Private</th>
296297
<td>
@@ -298,21 +299,21 @@
298299
Mnemonic: e<b>x</b>clusivity; “x” connotes unknown material in
299300
many Western languages due to its <a
300301
href="https://en.wikipedia.org/wiki/La_Géométrie#The_text">traditional
301302
use in mathematics</a>.
302303
</td>
303
- </tr>
304
+ </tr>
304305
305306
<tr id="y">
306307
<th>y</th>
307308
<th>WrUnver</th>
308309
<td>
309310
Push <a href="../unvers.wiki">unversioned content</a>. Mnemonic:
310311
<b>y</b>ield, <a href="https://en.wiktionary.org/wiki/yield">sense
311312
4</a>: “hand over.”
312313
</td>
313
- </tr>
314
+ </tr>
314315
315316
<tr id="z">
316317
<th>z</th>
317318
<th>Zip</th>
318319
<td>
@@ -323,20 +324,20 @@
323324
expensive capability to grant, because creating such archives can
324325
put a large load on <a href="../server/">a Fossil server</a> which
325326
you may then need to <a href="../loadmgmt.md">manage</a>.
326327
Mnemonic: <b>z</b>ip file download.
327328
</td>
328
- </tr>
329
+ </tr>
329330
330331
<tr id="2">
331332
<th>2</th>
332333
<th>RdForum</th>
333334
<td>
334335
Read <a href="../forum.wiki">forum posts</a> by other users.
335336
Mnemonic: from thee <b>2</b> me.
336337
</td>
337
- </tr>
338
+ </tr>
338339
339340
<tr id="3">
340341
<th>3</th>
341342
<th>WrForum</th>
342343
<td>
@@ -346,20 +347,20 @@
346347
not appear in repo clones or syncs. Granting this capability also
347348
grants <a href="#2"><b>RdForum</b></a>. Mnemonic: post for
348349
<b>3</b> audiences: me, <a href="#5">the mods</a>, and <a
349350
href="https://en.wikipedia.org/wiki/The_Man">the Man</a>.
350351
</td>
351
- </tr>
352
+ </tr>
352353
353354
<tr id="4">
354355
<th>4</th>
355356
<th>WrTForum</th>
356357
<td>
357358
Extends <a href="#3"><b>WrForum</b></a>, bypassing the moderation
358359
and sync restrictions. Mnemonic: post <b>4</b> immediate release.
359360
</td>
360
- </tr>
361
+ </tr>
361362
362363
<tr id="5">
363364
<th>5</th>
364365
<th>ModForum</th>
365366
<td>
@@ -368,11 +369,11 @@
368369
href="#4"><b>WrTForum</b></a> and <a href="#2"><b>RdForum</b></a>,
369370
so a user with this cap never has to moderate their own posts.
370371
Mnemonic: “May I have <b>5</b> seconds of your time, honored
371372
Gatekeeper?”
372373
</td>
373
- </tr>
374
+ </tr>
374375
375376
<tr id="6">
376377
<th>6</th>
377378
<th>AdminForum</th>
378379
<td>
@@ -387,46 +388,54 @@
387388
currently revoke granted caps. Granting this capability also
388389
grants <a href="#5"><b>ModForum</b></a> and those it in turn
389390
grants. Mnemonic: “I’m <b>6</b> [sick] of hitting Approve on your
390391
posts!”
391392
</td>
392
- </tr>
393
+ </tr>
393394
394395
<tr id="7">
395396
<th>7</th>
396397
<th>EmailAlert</th>
397398
<td>
398399
User can sign up for <a href="../alerts.md">email alerts</a>.
399400
Mnemonic: <a href="https://en.wikipedia.org/wiki/Heaven_Can_Wait">Seven can
400401
wait</a>, I’ve got email to read now.
401402
</td>
402
- </tr>
403
+ </tr>
403404
404405
<tr id="A">
405406
<th>A</th>
406407
<th>Announce</th>
407408
<td>
408409
Send email announcements to users <a href="#7">signed up to
409410
receive them</a>. Mnemonic: <b>a</b>nnounce.
410411
</td>
411
- </tr>
412
+ </tr>
413
+
414
+ <tr id="B">
415
+ <th>B</th>
416
+ <th>Attach to Forum</th>
417
+ <td>
418
+ Add attachments to forum posts. See also: <a href='#b'>b</a>.
419
+ </td>
420
+ </tr>
412421
413422
<tr id="C">
414423
<th>C</th>
415424
<th>Chat</th>
416425
<td>
417426
Allow access to the <tt>/chat</tt> room.
418427
</td>
419
- </tr>
428
+ </tr>
420429
421430
<tr id="D">
422431
<th>D</th>
423432
<th>Debug</th>
424433
<td>
425434
Enable debugging features. Mnemonic: <b>d</b>ebug.
426435
</td>
427
- </tr>
436
+ </tr>
428437
429438
<tr id="L">
430439
<th>L</th>
431440
<th>Is-logged-in</th>
432441
<td>
@@ -433,13 +442,13 @@
433442
This is not a real capability, but is used in certain capability
434443
checks, e.g. via <a href="../th1.md#capexpr">capexpr</a>. It
435444
resolves to true if the current user is logged in.
436445
Mnemonic: <b>L</b>ogged in.
437446
</td>
438
- </tr>
447
+ </tr>
439448
440449
</table>
441450
442451
<hr/>
443452
444453
<p id="backlink"><a href="./"><em>Back to Administering User
445454
Capabilities</em></a></p>
446455
--- www/caps/ref.html
+++ www/caps/ref.html
@@ -56,28 +56,29 @@
56 Admin users have <em>all</em> of the capabilities below except for
57 <a href="#s">setup</a>, <a herf="#x">Private</a>, and <a href="#y">WrUnver</a>.
58 See <a href="admin-v-setup.md">Admin vs. Setup</a> for a more
59 nuanced discussion. Mnemonic: <b>a</b>dministrate.
60 </td>
61 </tr>
62
63 <tr id="b">
64 <th>b</th>
65 <th>Attach</th>
66 <td>
67 Add attachments to wiki articles or tickets. Mnemonics: <b>b</b>ind,
68 <b>b</b>utton, <b>b</b>ond, or <b>b</b>olt.
 
69 </td>
70 </tr>
71
72 <tr id="c">
73 <th>c</th>
74 <th>ApndTkt</th>
75 <td>
76 Append comments to existing tickets. Mnemonic: <b>c</b>omment.
77 </td>
78 </tr>
79
80 <tr id="d">
81 <th>d</th>
82 <th>n/a</th>
83 <td>
@@ -100,21 +101,21 @@
100 identifying information</a> (PII) about other users such as email
101 addresses. Mnemonics: show <b>e</b>mail addresses; or
102 <b>E</b>urope, home of <a
103 href="https://en.wikipedia.org/wiki/General_Data_Protection_Regulation">GDPR</a>.
104 </td>
105 </tr>
106
107 <tr id="f">
108 <th>f</th>
109 <th>NewWiki</th>
110 <td>
111 Create new wiki articles. Mnemonic: <b>f</b>ast, English
112 translation of the Hawaiian word <a
113 href="https://en.wikipedia.org/wiki/History_of_wikis#WikiWikiWeb,_the_first_wiki"><i>wiki</i></a>.
114 </td>
115 </tr>
116
117 <tr id="g">
118 <th>g</th>
119 <th>Clone</th>
120 <td>
@@ -121,11 +122,11 @@
121 Clone the repository. Note that this is distinct from <a
122 href="#o">check-out capability, <b>o</b></a>; and that upon cloning
123 not just files, but also tickets, wikis, technotes and forum posts
124 are tranferred. Mnemonic: <b>g</b>et.
125 </td>
126 </tr>
127
128 <tr id="h">
129 <th>h</th>
130 <th>Hyperlink</th>
131 <td>
@@ -134,11 +135,11 @@
134 “nobody” category, to <a href="../antibot.wiki">prevent bots from
135 wandering around aimlessly</a> in the site’s hyperlink web, <a
136 href="../loadmgmt.md">chewing up server resources</a> to little
137 good purpose. Mnemonic: <b>h</b>yperlink.
138 </td>
139 </tr>
140
141 <tr id="i">
142 <th>i</th>
143 <th>Write</th>
144 <td>
@@ -149,20 +150,20 @@
149 Also note that not just files, but also tickets, wikis, technotes
150 and forum posts will be accepted from clones upon syncronization.
151 Granting this capability also grants <b>o (Read)</b> Mnemonics:
152 <b>i</b>nput, check <b>i</b>n changes.
153 </td>
154 </tr>
155
156 <tr id="j">
157 <th>j</th>
158 <th>RdWiki</th>
159 <td>
160 View wiki articles. Mnemonic: in<b>j</b>est page content. (All
161 right, you critics, you do better, then.)
162 </td>
163 </tr>
164
165 <tr id="k">
166 <th>k</th>
167 <th>WrWiki</th>
168 <td>
@@ -169,11 +170,11 @@
169 Edit wiki articles. Granting this capability also grants <a
170 href="#j"><b>RdWiki</b></a> and <a href="#m"><b>ApndWiki</b></a>,
171 but it does <em>not</em> grant <a href="#f"><b>NewWiki</b></a>!
172 Mnemonic: <b>k</b>ontribute.
173 </td>
174 </tr>
175
176 <tr id="l">
177 <th>l</th>
178 <th>ModWiki</th>
179 <td>
@@ -180,63 +181,63 @@
180 Moderate <a href="#m">wiki article appends</a>. Appends do not get
181 saved permanently to the receiving repo’s block chain until <a
182 href="#s">Setup</a> or someone with this cap approves it.
183 Mnemonic: a<b>l</b>low.
184 </td>
185 </tr>
186
187 <tr id="m">
188 <th>m</th>
189 <th>ApndWiki</th>
190 <td>
191 Append content to existing wiki articles. Mnemonic: a<b>m</b>end
192 wiki
193 </td>
194 </tr>
195
196 <tr id="n">
197 <th>n</th>
198 <th>NewTkt</th>
199 <td>
200 File new tickets. Mnemonic: <b>n</b>ew ticket.
201 </td>
202 </tr>
203
204 <tr id="o">
205 <th>o</th>
206 <th>Read</th>
207 <td>
208 Read content and history of files from a remote Fossil instance over
209 HTTP. See <a href="index.md#read-v-clone">Reading vs.
210 Cloning</a>. Mnemonic: check <b>o</b>ut remote repo contents.
211 </td>
212 </tr>
213
214 <tr id="p">
215 <th>p</th>
216 <th>Password</th>
217 <td>
218 Change one’s own password. Mnemonic: <b>p</b>assword.
219 </td>
220 </tr>
221
222 <tr id="q">
223 <th>q</th>
224 <th>ModTkt</th>
225 <td>
226 Moderate tickets: delete comments appended to tickets. Mnemonic:
227 <b>q</b>uash noise commentary.
228 </td>
229 </tr>
230
231 <tr id="r">
232 <th>r</th>
233 <th>RdTkt</th>
234 <td>
235 View existing tickets. Mnemonic: <b>r</b>ead tickets.
236 </td>
237 </tr>
238
239 <tr id="s">
240 <th>s</th>
241 <th>Setup</th>
242 <td>
@@ -256,41 +257,41 @@
256 because it is internally restricted to read-only queries on the
257 tickets table only. (This restriction is done with an SQLite
258 authorization hook, not by any method so weak as SQL text
259 filtering.) Mnemonic: new <b>t</b>icket report.
260 </td>
261 </tr>
262
263 <tr id="u">
264 <th>u</th>
265 <th>n/a</th>
266 <td>
267 Inherit all capabilities of the “reader” user category; does not
268 have a dedicated flag internally within Fossil. Mnemonic:
269 <a href="./index.md#ucat"><b>u</b>ser</a>
270 </td>
271 </tr>
272
273 <tr id="v">
274 <th>v</th>
275 <th>n/a</th>
276 <td>
277 Inherit all capabilities of the “developer” user category; does
278 not have a dedicated flag internally within Fossil. Mnemonic:
279 de<b>v</b>eloper.
280 </td>
281 </tr>
282
283 <tr id="w">
284 <th>w</th>
285 <th>WrTkt</th>
286 <td>
287 Edit existing tickets. Granting this capability also grants <a
288 href="#r"><b>RdTkt</b></a>, <a href="#c"><b>ApndTkt</b></a>, and
289 <a href="#n"><b>NewTkt</b></a>. Mnemonic: <b>w</b>rite to ticket.
290 </td>
291 </tr>
292
293 <tr id="x">
294 <th>x</th>
295 <th>Private</th>
296 <td>
@@ -298,21 +299,21 @@
298 Mnemonic: e<b>x</b>clusivity; “x” connotes unknown material in
299 many Western languages due to its <a
300 href="https://en.wikipedia.org/wiki/La_Géométrie#The_text">traditional
301 use in mathematics</a>.
302 </td>
303 </tr>
304
305 <tr id="y">
306 <th>y</th>
307 <th>WrUnver</th>
308 <td>
309 Push <a href="../unvers.wiki">unversioned content</a>. Mnemonic:
310 <b>y</b>ield, <a href="https://en.wiktionary.org/wiki/yield">sense
311 4</a>: “hand over.”
312 </td>
313 </tr>
314
315 <tr id="z">
316 <th>z</th>
317 <th>Zip</th>
318 <td>
@@ -323,20 +324,20 @@
323 expensive capability to grant, because creating such archives can
324 put a large load on <a href="../server/">a Fossil server</a> which
325 you may then need to <a href="../loadmgmt.md">manage</a>.
326 Mnemonic: <b>z</b>ip file download.
327 </td>
328 </tr>
329
330 <tr id="2">
331 <th>2</th>
332 <th>RdForum</th>
333 <td>
334 Read <a href="../forum.wiki">forum posts</a> by other users.
335 Mnemonic: from thee <b>2</b> me.
336 </td>
337 </tr>
338
339 <tr id="3">
340 <th>3</th>
341 <th>WrForum</th>
342 <td>
@@ -346,20 +347,20 @@
346 not appear in repo clones or syncs. Granting this capability also
347 grants <a href="#2"><b>RdForum</b></a>. Mnemonic: post for
348 <b>3</b> audiences: me, <a href="#5">the mods</a>, and <a
349 href="https://en.wikipedia.org/wiki/The_Man">the Man</a>.
350 </td>
351 </tr>
352
353 <tr id="4">
354 <th>4</th>
355 <th>WrTForum</th>
356 <td>
357 Extends <a href="#3"><b>WrForum</b></a>, bypassing the moderation
358 and sync restrictions. Mnemonic: post <b>4</b> immediate release.
359 </td>
360 </tr>
361
362 <tr id="5">
363 <th>5</th>
364 <th>ModForum</th>
365 <td>
@@ -368,11 +369,11 @@
368 href="#4"><b>WrTForum</b></a> and <a href="#2"><b>RdForum</b></a>,
369 so a user with this cap never has to moderate their own posts.
370 Mnemonic: “May I have <b>5</b> seconds of your time, honored
371 Gatekeeper?”
372 </td>
373 </tr>
374
375 <tr id="6">
376 <th>6</th>
377 <th>AdminForum</th>
378 <td>
@@ -387,46 +388,54 @@
387 currently revoke granted caps. Granting this capability also
388 grants <a href="#5"><b>ModForum</b></a> and those it in turn
389 grants. Mnemonic: “I’m <b>6</b> [sick] of hitting Approve on your
390 posts!”
391 </td>
392 </tr>
393
394 <tr id="7">
395 <th>7</th>
396 <th>EmailAlert</th>
397 <td>
398 User can sign up for <a href="../alerts.md">email alerts</a>.
399 Mnemonic: <a href="https://en.wikipedia.org/wiki/Heaven_Can_Wait">Seven can
400 wait</a>, I’ve got email to read now.
401 </td>
402 </tr>
403
404 <tr id="A">
405 <th>A</th>
406 <th>Announce</th>
407 <td>
408 Send email announcements to users <a href="#7">signed up to
409 receive them</a>. Mnemonic: <b>a</b>nnounce.
410 </td>
411 </tr>
 
 
 
 
 
 
 
 
412
413 <tr id="C">
414 <th>C</th>
415 <th>Chat</th>
416 <td>
417 Allow access to the <tt>/chat</tt> room.
418 </td>
419 </tr>
420
421 <tr id="D">
422 <th>D</th>
423 <th>Debug</th>
424 <td>
425 Enable debugging features. Mnemonic: <b>d</b>ebug.
426 </td>
427 </tr>
428
429 <tr id="L">
430 <th>L</th>
431 <th>Is-logged-in</th>
432 <td>
@@ -433,13 +442,13 @@
433 This is not a real capability, but is used in certain capability
434 checks, e.g. via <a href="../th1.md#capexpr">capexpr</a>. It
435 resolves to true if the current user is logged in.
436 Mnemonic: <b>L</b>ogged in.
437 </td>
438 </tr>
439
440 </table>
441
442 <hr/>
443
444 <p id="backlink"><a href="./"><em>Back to Administering User
445 Capabilities</em></a></p>
446
--- www/caps/ref.html
+++ www/caps/ref.html
@@ -56,28 +56,29 @@
56 Admin users have <em>all</em> of the capabilities below except for
57 <a href="#s">setup</a>, <a herf="#x">Private</a>, and <a href="#y">WrUnver</a>.
58 See <a href="admin-v-setup.md">Admin vs. Setup</a> for a more
59 nuanced discussion. Mnemonic: <b>a</b>dministrate.
60 </td>
61 </tr>
62
63 <tr id="b">
64 <th>b</th>
65 <th>Attach</th>
66 <td>
67 Add attachments to wiki articles, technotes, or tickets.
68 Mnemonics: <b>b</b>ind, <b>b</b>utton, <b>b</b>ond, or <b>b</b>olt.
69 See also: <a href='#B'>B</a>.
70 </td>
71 </tr>
72
73 <tr id="c">
74 <th>c</th>
75 <th>ApndTkt</th>
76 <td>
77 Append comments to existing tickets. Mnemonic: <b>c</b>omment.
78 </td>
79 </tr>
80
81 <tr id="d">
82 <th>d</th>
83 <th>n/a</th>
84 <td>
@@ -100,21 +101,21 @@
101 identifying information</a> (PII) about other users such as email
102 addresses. Mnemonics: show <b>e</b>mail addresses; or
103 <b>E</b>urope, home of <a
104 href="https://en.wikipedia.org/wiki/General_Data_Protection_Regulation">GDPR</a>.
105 </td>
106 </tr>
107
108 <tr id="f">
109 <th>f</th>
110 <th>NewWiki</th>
111 <td>
112 Create new wiki articles. Mnemonic: <b>f</b>ast, English
113 translation of the Hawaiian word <a
114 href="https://en.wikipedia.org/wiki/History_of_wikis#WikiWikiWeb,_the_first_wiki"><i>wiki</i></a>.
115 </td>
116 </tr>
117
118 <tr id="g">
119 <th>g</th>
120 <th>Clone</th>
121 <td>
@@ -121,11 +122,11 @@
122 Clone the repository. Note that this is distinct from <a
123 href="#o">check-out capability, <b>o</b></a>; and that upon cloning
124 not just files, but also tickets, wikis, technotes and forum posts
125 are tranferred. Mnemonic: <b>g</b>et.
126 </td>
127 </tr>
128
129 <tr id="h">
130 <th>h</th>
131 <th>Hyperlink</th>
132 <td>
@@ -134,11 +135,11 @@
135 “nobody” category, to <a href="../antibot.wiki">prevent bots from
136 wandering around aimlessly</a> in the site’s hyperlink web, <a
137 href="../loadmgmt.md">chewing up server resources</a> to little
138 good purpose. Mnemonic: <b>h</b>yperlink.
139 </td>
140 </tr>
141
142 <tr id="i">
143 <th>i</th>
144 <th>Write</th>
145 <td>
@@ -149,20 +150,20 @@
150 Also note that not just files, but also tickets, wikis, technotes
151 and forum posts will be accepted from clones upon syncronization.
152 Granting this capability also grants <b>o (Read)</b> Mnemonics:
153 <b>i</b>nput, check <b>i</b>n changes.
154 </td>
155 </tr>
156
157 <tr id="j">
158 <th>j</th>
159 <th>RdWiki</th>
160 <td>
161 View wiki articles. Mnemonic: in<b>j</b>est page content. (All
162 right, you critics, you do better, then.)
163 </td>
164 </tr>
165
166 <tr id="k">
167 <th>k</th>
168 <th>WrWiki</th>
169 <td>
@@ -169,11 +170,11 @@
170 Edit wiki articles. Granting this capability also grants <a
171 href="#j"><b>RdWiki</b></a> and <a href="#m"><b>ApndWiki</b></a>,
172 but it does <em>not</em> grant <a href="#f"><b>NewWiki</b></a>!
173 Mnemonic: <b>k</b>ontribute.
174 </td>
175 </tr>
176
177 <tr id="l">
178 <th>l</th>
179 <th>ModWiki</th>
180 <td>
@@ -180,63 +181,63 @@
181 Moderate <a href="#m">wiki article appends</a>. Appends do not get
182 saved permanently to the receiving repo’s block chain until <a
183 href="#s">Setup</a> or someone with this cap approves it.
184 Mnemonic: a<b>l</b>low.
185 </td>
186 </tr>
187
188 <tr id="m">
189 <th>m</th>
190 <th>ApndWiki</th>
191 <td>
192 Append content to existing wiki articles. Mnemonic: a<b>m</b>end
193 wiki
194 </td>
195 </tr>
196
197 <tr id="n">
198 <th>n</th>
199 <th>NewTkt</th>
200 <td>
201 File new tickets. Mnemonic: <b>n</b>ew ticket.
202 </td>
203 </tr>
204
205 <tr id="o">
206 <th>o</th>
207 <th>Read</th>
208 <td>
209 Read content and history of files from a remote Fossil instance over
210 HTTP. See <a href="index.md#read-v-clone">Reading vs.
211 Cloning</a>. Mnemonic: check <b>o</b>ut remote repo contents.
212 </td>
213 </tr>
214
215 <tr id="p">
216 <th>p</th>
217 <th>Password</th>
218 <td>
219 Change one’s own password. Mnemonic: <b>p</b>assword.
220 </td>
221 </tr>
222
223 <tr id="q">
224 <th>q</th>
225 <th>ModTkt</th>
226 <td>
227 Moderate tickets: delete comments appended to tickets. Mnemonic:
228 <b>q</b>uash noise commentary.
229 </td>
230 </tr>
231
232 <tr id="r">
233 <th>r</th>
234 <th>RdTkt</th>
235 <td>
236 View existing tickets. Mnemonic: <b>r</b>ead tickets.
237 </td>
238 </tr>
239
240 <tr id="s">
241 <th>s</th>
242 <th>Setup</th>
243 <td>
@@ -256,41 +257,41 @@
257 because it is internally restricted to read-only queries on the
258 tickets table only. (This restriction is done with an SQLite
259 authorization hook, not by any method so weak as SQL text
260 filtering.) Mnemonic: new <b>t</b>icket report.
261 </td>
262 </tr>
263
264 <tr id="u">
265 <th>u</th>
266 <th>n/a</th>
267 <td>
268 Inherit all capabilities of the “reader” user category; does not
269 have a dedicated flag internally within Fossil. Mnemonic:
270 <a href="./index.md#ucat"><b>u</b>ser</a>
271 </td>
272 </tr>
273
274 <tr id="v">
275 <th>v</th>
276 <th>n/a</th>
277 <td>
278 Inherit all capabilities of the “developer” user category; does
279 not have a dedicated flag internally within Fossil. Mnemonic:
280 de<b>v</b>eloper.
281 </td>
282 </tr>
283
284 <tr id="w">
285 <th>w</th>
286 <th>WrTkt</th>
287 <td>
288 Edit existing tickets. Granting this capability also grants <a
289 href="#r"><b>RdTkt</b></a>, <a href="#c"><b>ApndTkt</b></a>, and
290 <a href="#n"><b>NewTkt</b></a>. Mnemonic: <b>w</b>rite to ticket.
291 </td>
292 </tr>
293
294 <tr id="x">
295 <th>x</th>
296 <th>Private</th>
297 <td>
@@ -298,21 +299,21 @@
299 Mnemonic: e<b>x</b>clusivity; “x” connotes unknown material in
300 many Western languages due to its <a
301 href="https://en.wikipedia.org/wiki/La_Géométrie#The_text">traditional
302 use in mathematics</a>.
303 </td>
304 </tr>
305
306 <tr id="y">
307 <th>y</th>
308 <th>WrUnver</th>
309 <td>
310 Push <a href="../unvers.wiki">unversioned content</a>. Mnemonic:
311 <b>y</b>ield, <a href="https://en.wiktionary.org/wiki/yield">sense
312 4</a>: “hand over.”
313 </td>
314 </tr>
315
316 <tr id="z">
317 <th>z</th>
318 <th>Zip</th>
319 <td>
@@ -323,20 +324,20 @@
324 expensive capability to grant, because creating such archives can
325 put a large load on <a href="../server/">a Fossil server</a> which
326 you may then need to <a href="../loadmgmt.md">manage</a>.
327 Mnemonic: <b>z</b>ip file download.
328 </td>
329 </tr>
330
331 <tr id="2">
332 <th>2</th>
333 <th>RdForum</th>
334 <td>
335 Read <a href="../forum.wiki">forum posts</a> by other users.
336 Mnemonic: from thee <b>2</b> me.
337 </td>
338 </tr>
339
340 <tr id="3">
341 <th>3</th>
342 <th>WrForum</th>
343 <td>
@@ -346,20 +347,20 @@
347 not appear in repo clones or syncs. Granting this capability also
348 grants <a href="#2"><b>RdForum</b></a>. Mnemonic: post for
349 <b>3</b> audiences: me, <a href="#5">the mods</a>, and <a
350 href="https://en.wikipedia.org/wiki/The_Man">the Man</a>.
351 </td>
352 </tr>
353
354 <tr id="4">
355 <th>4</th>
356 <th>WrTForum</th>
357 <td>
358 Extends <a href="#3"><b>WrForum</b></a>, bypassing the moderation
359 and sync restrictions. Mnemonic: post <b>4</b> immediate release.
360 </td>
361 </tr>
362
363 <tr id="5">
364 <th>5</th>
365 <th>ModForum</th>
366 <td>
@@ -368,11 +369,11 @@
369 href="#4"><b>WrTForum</b></a> and <a href="#2"><b>RdForum</b></a>,
370 so a user with this cap never has to moderate their own posts.
371 Mnemonic: “May I have <b>5</b> seconds of your time, honored
372 Gatekeeper?”
373 </td>
374 </tr>
375
376 <tr id="6">
377 <th>6</th>
378 <th>AdminForum</th>
379 <td>
@@ -387,46 +388,54 @@
388 currently revoke granted caps. Granting this capability also
389 grants <a href="#5"><b>ModForum</b></a> and those it in turn
390 grants. Mnemonic: “I’m <b>6</b> [sick] of hitting Approve on your
391 posts!”
392 </td>
393 </tr>
394
395 <tr id="7">
396 <th>7</th>
397 <th>EmailAlert</th>
398 <td>
399 User can sign up for <a href="../alerts.md">email alerts</a>.
400 Mnemonic: <a href="https://en.wikipedia.org/wiki/Heaven_Can_Wait">Seven can
401 wait</a>, I’ve got email to read now.
402 </td>
403 </tr>
404
405 <tr id="A">
406 <th>A</th>
407 <th>Announce</th>
408 <td>
409 Send email announcements to users <a href="#7">signed up to
410 receive them</a>. Mnemonic: <b>a</b>nnounce.
411 </td>
412 </tr>
413
414 <tr id="B">
415 <th>B</th>
416 <th>Attach to Forum</th>
417 <td>
418 Add attachments to forum posts. See also: <a href='#b'>b</a>.
419 </td>
420 </tr>
421
422 <tr id="C">
423 <th>C</th>
424 <th>Chat</th>
425 <td>
426 Allow access to the <tt>/chat</tt> room.
427 </td>
428 </tr>
429
430 <tr id="D">
431 <th>D</th>
432 <th>Debug</th>
433 <td>
434 Enable debugging features. Mnemonic: <b>d</b>ebug.
435 </td>
436 </tr>
437
438 <tr id="L">
439 <th>L</th>
440 <th>Is-logged-in</th>
441 <td>
@@ -433,13 +442,13 @@
442 This is not a real capability, but is used in certain capability
443 checks, e.g. via <a href="../th1.md#capexpr">capexpr</a>. It
444 resolves to true if the current user is logged in.
445 Mnemonic: <b>L</b>ogged in.
446 </td>
447 </tr>
448
449 </table>
450
451 <hr/>
452
453 <p id="backlink"><a href="./"><em>Back to Administering User
454 Capabilities</em></a></p>
455
--- www/changes.wiki
+++ www/changes.wiki
@@ -14,10 +14,15 @@
1414
the unmanaged file, even if that unmanaged file is read-only.
1515
<li> Improve the default prompts used by the
1616
"[/help/sqlite3|fossil sql]" command.
1717
<li> The captcha now uses light-gray boxes as the background, instead of
1818
spaces, to work around width inconsistencies in some fonts.
19
+ <li> Forum posts may now have attachments if their poster has the new "B"
20
+ capability.</li>
21
+ <li> Add the "[/help/attachment-size-limit|attachment-size-limit]" setting
22
+ to limit the size of file attachments to wiki pages, tech notes,
23
+ tickets, and forum posts.
1924
</ol>
2025
2126
<h2 id='v2_28'>Changes for version 2.28 (2026-03-11)</h2><ol>
2227
<li> Improvements to [./antibot.wiki|anti-robot defenses]:<ol type="a">
2328
<li> The default configuration now allows robots to download any tarball
2429
--- www/changes.wiki
+++ www/changes.wiki
@@ -14,10 +14,15 @@
14 the unmanaged file, even if that unmanaged file is read-only.
15 <li> Improve the default prompts used by the
16 "[/help/sqlite3|fossil sql]" command.
17 <li> The captcha now uses light-gray boxes as the background, instead of
18 spaces, to work around width inconsistencies in some fonts.
 
 
 
 
 
19 </ol>
20
21 <h2 id='v2_28'>Changes for version 2.28 (2026-03-11)</h2><ol>
22 <li> Improvements to [./antibot.wiki|anti-robot defenses]:<ol type="a">
23 <li> The default configuration now allows robots to download any tarball
24
--- www/changes.wiki
+++ www/changes.wiki
@@ -14,10 +14,15 @@
14 the unmanaged file, even if that unmanaged file is read-only.
15 <li> Improve the default prompts used by the
16 "[/help/sqlite3|fossil sql]" command.
17 <li> The captcha now uses light-gray boxes as the background, instead of
18 spaces, to work around width inconsistencies in some fonts.
19 <li> Forum posts may now have attachments if their poster has the new "B"
20 capability.</li>
21 <li> Add the "[/help/attachment-size-limit|attachment-size-limit]" setting
22 to limit the size of file attachments to wiki pages, tech notes,
23 tickets, and forum posts.
24 </ol>
25
26 <h2 id='v2_28'>Changes for version 2.28 (2026-03-11)</h2><ol>
27 <li> Improvements to [./antibot.wiki|anti-robot defenses]:<ol type="a">
28 <li> The default configuration now allows robots to download any tarball
29
--- www/fileformat.wiki
+++ www/fileformat.wiki
@@ -424,14 +424,13 @@
424424
[/artifact/91f1ec6af053 | here].
425425
426426
<h3 id="attachment">2.6 Attachments</h3>
427427
428428
An attachment artifact associates some other artifact that is the
429
-attachment (the source artifact) with a ticket or wiki page or
430
-technical note to which
431
-the attachment is connected (the target artifact).
432
-The following cards are allowed on an attachment artifact:
429
+attachment (the source artifact) with a ticket, wiki page, technical
430
+note, or forum post to which the attachment is connected (the target
431
+artifact). The following cards are allowed on an attachment artifact:
433432
434433
<div class="indent">
435434
<b>A</b> <i>filename target</i> ?<i>source</i>?<br />
436435
<b>C</b> <i>comment</i><br />
437436
<b>D</b> <i>time-and-date-stamp</i><br />
@@ -438,16 +437,19 @@
438437
<b>N</b> <i>mimetype</i><br />
439438
<b>U</b> <i>user-name</i><br />
440439
<b>Z</b> <i>checksum</i>
441440
</div>
442441
443
-The <b>A</b> card specifies a filename for the attachment in its first argument.
444
-The second argument to the <b>A</b> card is the name of the wiki page or
445
-ticket or technical note to which the attachment is connected. The
446
-third argument is either missing or else it is the lower-case artifact
447
-ID of the attachment itself. A missing third argument means that the
448
-attachment should be deleted.
442
+The <b>A</b> card specifies a filename for the attachment in its first
443
+argument. The second argument to the <b>A</b> card is the name of the
444
+wiki page, ticket, technical note, or full hash of a forum post to
445
+which the attachment is connected. The third argument is either
446
+missing or else it is the lower-case artifact ID of the attachment
447
+itself. A missing third argument means that the attachment should be
448
+deleted. If <i>target</i> is a forum post, the hash provided should
449
+generally be that of the first version of the post for reasons
450
+[#forumpost-tag|explained elsewhere].
449451
450452
The <b>C</b> card is an optional comment describing what the attachment is about.
451453
The <b>C</b> card is optional, but there can only be one.
452454
453455
A single <b>D</b> card is required to give the date and time when the attachment
@@ -602,10 +604,26 @@
602604
The format of the <b>W</b> card is exactly the same as for a
603605
[#wikichng | wiki artifact].
604606
605607
The <b>Z</b> card is the required checksum over the rest of the artifact.
606608
609
+<a name="forumpost-tag"></a>
610
+<h4>2.8.1 Tags and Attachments on Forum Posts</h4>
611
+
612
+When adding [#ctrl|tags] or [#attachment|attachments] to [#forum|forum
613
+posts] it is generally up to the application to tag, or attach to,
614
+only the first version of any given post. For example, if post X has
615
+two edits then a tag applied, or attachment added, by the user to post
616
+X+2 should generally be applied to version X instead. Though this
617
+complicates the app logic for applying tags, it simplifies the app's
618
+location of tags for purposes of applying tag-/attachment-dependent
619
+logic. As of this writing (May 2026), no current Fossil use cases
620
+would be improved by tagging specific subsequent versions of posts,
621
+e.g. the hypothetical X+1.
622
+
623
+Forum posts, because they have a P-card, support propagating tags.
624
+
607625
608626
<h2 id="summary">3.0 Card Summary</h2>
609627
610628
The following table summarizes the various kinds of cards that appear
611629
on Fossil artifacts. A blank entry means that combination of card and
612630
--- www/fileformat.wiki
+++ www/fileformat.wiki
@@ -424,14 +424,13 @@
424 [/artifact/91f1ec6af053 | here].
425
426 <h3 id="attachment">2.6 Attachments</h3>
427
428 An attachment artifact associates some other artifact that is the
429 attachment (the source artifact) with a ticket or wiki page or
430 technical note to which
431 the attachment is connected (the target artifact).
432 The following cards are allowed on an attachment artifact:
433
434 <div class="indent">
435 <b>A</b> <i>filename target</i> ?<i>source</i>?<br />
436 <b>C</b> <i>comment</i><br />
437 <b>D</b> <i>time-and-date-stamp</i><br />
@@ -438,16 +437,19 @@
438 <b>N</b> <i>mimetype</i><br />
439 <b>U</b> <i>user-name</i><br />
440 <b>Z</b> <i>checksum</i>
441 </div>
442
443 The <b>A</b> card specifies a filename for the attachment in its first argument.
444 The second argument to the <b>A</b> card is the name of the wiki page or
445 ticket or technical note to which the attachment is connected. The
446 third argument is either missing or else it is the lower-case artifact
447 ID of the attachment itself. A missing third argument means that the
448 attachment should be deleted.
 
 
 
449
450 The <b>C</b> card is an optional comment describing what the attachment is about.
451 The <b>C</b> card is optional, but there can only be one.
452
453 A single <b>D</b> card is required to give the date and time when the attachment
@@ -602,10 +604,26 @@
602 The format of the <b>W</b> card is exactly the same as for a
603 [#wikichng | wiki artifact].
604
605 The <b>Z</b> card is the required checksum over the rest of the artifact.
606
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
607
608 <h2 id="summary">3.0 Card Summary</h2>
609
610 The following table summarizes the various kinds of cards that appear
611 on Fossil artifacts. A blank entry means that combination of card and
612
--- www/fileformat.wiki
+++ www/fileformat.wiki
@@ -424,14 +424,13 @@
424 [/artifact/91f1ec6af053 | here].
425
426 <h3 id="attachment">2.6 Attachments</h3>
427
428 An attachment artifact associates some other artifact that is the
429 attachment (the source artifact) with a ticket, wiki page, technical
430 note, or forum post to which the attachment is connected (the target
431 artifact). The following cards are allowed on an attachment artifact:
 
432
433 <div class="indent">
434 <b>A</b> <i>filename target</i> ?<i>source</i>?<br />
435 <b>C</b> <i>comment</i><br />
436 <b>D</b> <i>time-and-date-stamp</i><br />
@@ -438,16 +437,19 @@
437 <b>N</b> <i>mimetype</i><br />
438 <b>U</b> <i>user-name</i><br />
439 <b>Z</b> <i>checksum</i>
440 </div>
441
442 The <b>A</b> card specifies a filename for the attachment in its first
443 argument. The second argument to the <b>A</b> card is the name of the
444 wiki page, ticket, technical note, or full hash of a forum post to
445 which the attachment is connected. The third argument is either
446 missing or else it is the lower-case artifact ID of the attachment
447 itself. A missing third argument means that the attachment should be
448 deleted. If <i>target</i> is a forum post, the hash provided should
449 generally be that of the first version of the post for reasons
450 [#forumpost-tag|explained elsewhere].
451
452 The <b>C</b> card is an optional comment describing what the attachment is about.
453 The <b>C</b> card is optional, but there can only be one.
454
455 A single <b>D</b> card is required to give the date and time when the attachment
@@ -602,10 +604,26 @@
604 The format of the <b>W</b> card is exactly the same as for a
605 [#wikichng | wiki artifact].
606
607 The <b>Z</b> card is the required checksum over the rest of the artifact.
608
609 <a name="forumpost-tag"></a>
610 <h4>2.8.1 Tags and Attachments on Forum Posts</h4>
611
612 When adding [#ctrl|tags] or [#attachment|attachments] to [#forum|forum
613 posts] it is generally up to the application to tag, or attach to,
614 only the first version of any given post. For example, if post X has
615 two edits then a tag applied, or attachment added, by the user to post
616 X+2 should generally be applied to version X instead. Though this
617 complicates the app logic for applying tags, it simplifies the app's
618 location of tags for purposes of applying tag-/attachment-dependent
619 logic. As of this writing (May 2026), no current Fossil use cases
620 would be improved by tagging specific subsequent versions of posts,
621 e.g. the hypothetical X+1.
622
623 Forum posts, because they have a P-card, support propagating tags.
624
625
626 <h2 id="summary">3.0 Card Summary</h2>
627
628 The following table summarizes the various kinds of cards that appear
629 on Fossil artifacts. A blank entry means that combination of card and
630

Keyboard Shortcuts

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