Fossil SCM

fossil-scm / src / attach.c
Blame History Raw 1860 lines
1
/*
2
** Copyright (c) 2010 D. Richard Hipp
3
**
4
** This program is free software; you can redistribute it and/or
5
** modify it under the terms of the Simplified BSD License (also
6
** known as the "2-Clause License" or "FreeBSD License".)
7
**
8
** This program is distributed in the hope that it will be useful,
9
** but without any warranty; without even the implied warranty of
10
** merchantability or fitness for a particular purpose.
11
**
12
** Author contact information:
13
** [email protected]
14
** http://www.hwaci.com/drh/
15
**
16
*******************************************************************************
17
**
18
** This file contains code 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 distinguish
28
** the target type.
29
**
30
** zTarget is an attachment target name: wiki page name, tech-note ID,
31
** ticket ID, or forumpost hash.
32
**
33
** If bFull is true then it requires zTarget to be a full ID for
34
** tech-notes and tickets, otherwise such IDs may be prefixes. If
35
** bFull is false then tech-notes and tickets will perform a prefix
36
** match, but it is up to the caller to provide enough of a prefix to
37
** rule out ambiguity[^1]. When called repeatedly, this routine can
38
** run a bit faster and more efficiently if bFull is true, but some
39
** historical use cases call for prefix matches.
40
**
41
** Wiki page names always require an exact match.
42
**
43
** Forum posts are a special case:
44
**
45
** - They ignore the bFull flag. That is, they will do prefix matches
46
** but will not match an ambiguous prefix.
47
**
48
** - It is up to the caller to, if needed, resolve zTarget using
49
** forumpost_head_rid2() to resolve the RID of the earliest version
50
** of the post, as that is the only one which attachments should
51
** target.
52
**
53
** [^1]: Historically (from the perspective of 2026-06) attachment
54
** target lookups have used GLOB prefix matching but have taken no
55
** measures to ensure that the prefix is unambiguous. Ergo we do the
56
** same here. It is assumed that the caller passes enough of a prefix
57
** to be unambiguous and that's worked out fine so far.
58
*/
59
int attachment_target_type(const char *zTarget, int bFull){
60
if( !zTarget || !zTarget[0] || strlen(zTarget)>64/*vs. abuse*/ ){
61
return 0;
62
}
63
if( symbolic_name_to_rid(zTarget, "f")>0 ){
64
/* Check forum posts first because they are the most likely target
65
** as of 2026. We should arguably use something more
66
** specialized/efficient than symbolic_name_to_rid(). */
67
return CFTYPE_FORUM;
68
}
69
if( bFull ){
70
static Stmt q = empty_Stmt_m;
71
int rc = 0;
72
if( !q.pStmt ){
73
db_static_prepare(
74
&q,
75
"SELECT CASE "
76
/* Ordered by presumed likelihood of attachments. */
77
"WHEN (SELECT 1 FROM tag WHERE tagname='tkt-'||:tgt) THEN %d\n"
78
"WHEN (SELECT 1 FROM tag WHERE tagname='wiki-'||:tgt) THEN %d\n"
79
"WHEN (SELECT 1 FROM tag WHERE tagname='event-'||:tgt) THEN %d\n"
80
"ELSE 0 END",
81
CFTYPE_TICKET, CFTYPE_WIKI, CFTYPE_EVENT
82
);
83
}
84
db_bind_text(&q, ":tgt", zTarget);
85
if( SQLITE_ROW==db_step(&q) ){
86
rc = db_column_int(&q, 0);
87
}
88
db_reset(&q);
89
return rc;
90
}else{
91
return db_int(
92
0,
93
"SELECT CASE "
94
"WHEN (SELECT tagid FROM tag WHERE tagname GLOB 'tkt-%q*')"
95
" THEN %d\n"
96
"WHEN (SELECT tagid FROM tag WHERE tagname='wiki-%q')"
97
" THEN %d\n"
98
"WHEN (SELECT tagid FROM tag WHERE tagname GLOB 'event-%q*')"
99
" THEN %d\n"
100
"ELSE 0 END",
101
zTarget, CFTYPE_TICKET,
102
zTarget, CFTYPE_WIKI,
103
zTarget, CFTYPE_EVENT
104
);
105
}
106
}
107
108
/*
109
** Given an attachment target name, returns the target's blob.rid.
110
** zTarget and bFull work as described for attachment_target_type().
111
**
112
** For forum posts, this always returns the RID of the first version
113
** of the post, as attachments should always target that instance.
114
*/
115
int attachment_target_rid(const char *zTarget, int bFull){
116
int rid = 0;
117
const int eType = attachment_target_type(zTarget, bFull);
118
switch(eType){
119
case CFTYPE_TICKET:
120
case CFTYPE_EVENT:{
121
const char *zTagPrefix = (eType==CFTYPE_EVENT) ? "event" : "tkt";
122
rid = db_int(
123
0, "SELECT b.rid FROM blob b, tag t, tagxref x\n"
124
"WHERE tagname %s '%s-%q%s'\n"
125
"AND x.tagtype>0\n"
126
"AND x.tagid=t.tagid\n"
127
"AND x.rid=b.rid\n"
128
"ORDER BY x.mtime DESC",
129
bFull ? "=" : "GLOB"/*safe-for-%s*/,
130
zTagPrefix/*safe-for-%s*/,
131
zTarget,
132
bFull ? "" : "*"/*safe-for-%s*/
133
);
134
break;
135
}
136
case CFTYPE_FORUM:
137
rid = db_int(
138
0, "SELECT f.fpid FROM forumpost f, blob b\n"
139
"WHERE f.fpid=b.rid\n"
140
"AND b.uuid %s '%q%s'",
141
bFull ? "=" : "GLOB"/*safe-for-%s*/,
142
zTarget,
143
bFull ? "" : "*"/*safe-for-%s*/
144
);
145
if( rid>0 ){
146
rid = forumpost_head_rid(rid);
147
}
148
break;
149
case CFTYPE_WIKI:
150
rid = db_int(
151
0, "SELECT b.rid FROM blob b, tag t, tagxref x\n"
152
"WHERE tagname='wiki-%q'\n"
153
"AND x.tagtype>0\n"
154
"AND x.tagid=t.tagid\n"
155
"AND x.rid=b.rid\n"
156
"ORDER BY x.mtime DESC",
157
zTarget
158
);
159
break;
160
default:
161
break;
162
}
163
return rid;
164
}
165
166
/*
167
** For a given aritfact ID and type (from the CFTYPE_xyz enum),
168
** returns true if the current user could hypothetically apply and
169
** attachment to it, else returns 0.
170
**
171
** The rid is currently only relevant when eArtifactType is
172
** CFTYPE_FORUM. For forum posts, it checks precisely the rid given,
173
** not the head RID, to keep non-admins from attaching files to
174
** threads which have since been taken over by another user (this
175
** happens when an admin edits another user's post).
176
*/
177
int attach_user_may(int rid, int eArtifactType){
178
if( g.perm.Admin ) return 1;
179
if( !login_is_individual() ) return 0;
180
switch(eArtifactType){
181
case CFTYPE_FORUM:
182
return g.perm.AttachForum && forumpost_is_owner(rid, 0);
183
case CFTYPE_WIKI:
184
return g.perm.ApndWiki && g.perm.Attach;
185
case CFTYPE_TICKET:
186
return g.perm.ApndTkt && g.perm.Attach;
187
case CFTYPE_EVENT:
188
return g.perm.Write && g.perm.ApndWiki && g.perm.Attach;
189
default:
190
return 0;
191
}
192
}
193
194
/*
195
** Emits a single-button FORM which invokes
196
** /attachadd with target=$zTarget.
197
*/
198
void attach_render_attachadd_button(const char *zTarget){
199
/* This could be changed from POST to GET, and arguably should so
200
** that the target=X part becomes part of the resulting URL. */
201
@ <form method="post" action="%R/attachadd">\
202
@ <input type="hidden" name="target" value="%T(zTarget)">\
203
@ <input type="submit" value="Attach...">
204
@ </form>\
205
}
206
207
/*
208
** WEBPAGE: attachlist
209
** List attachments.
210
**
211
** tkt=HASH
212
** page=WIKIPAGE
213
** technote=HASH
214
** forumpost=HASH
215
**
216
** At most one of technote=, tkt=, forumpost=, or page= may be supplied.
217
**
218
** If none are given, all attachments are listed. If one is given, only
219
** attachments for the designated technote, ticket or wiki page are shown.
220
**
221
** HASH may be just a prefix of the relevant forum post, technical
222
** note, or ticket artifact hash, in which case all attachments of all
223
** technical notes or tickets with the prefix will be listed. Forum
224
** posts, on the other hand, require a unique hash or hash prefix.
225
*/
226
void attachlist_page(void){
227
const char *zPage = P("page");
228
const char *zTkt = P("tkt");
229
const char *zTechNote = P("technote");
230
const char *zForumPost = P("forumpost");
231
char *zLink = 0;
232
Blob sql;
233
Stmt q;
234
235
if( zPage && zTkt ) zTkt = 0;
236
login_check_credentials();
237
style_set_current_feature("attach");
238
blob_zero(&sql);
239
blob_append_sql(&sql,
240
"SELECT datetime(mtime,toLocal()), src, target, filename,"
241
" comment, user,"
242
" (SELECT uuid FROM blob WHERE rid=attachid), attachid"
243
" FROM attachment"
244
);
245
if( zForumPost ){
246
int fnid;
247
if( g.perm.RdForum==0 ){ login_needed(g.anon.RdForum); return; }
248
style_header("Attachments To Forum post %S", zForumPost);
249
fnid = forumpost_head_rid2(zForumPost);
250
if( fnid<=0 ){
251
webpage_error("Invalid forum post ID: %h", zForumPost);
252
}
253
blob_append_sql(&sql, " WHERE target="
254
"(SELECT uuid FROM blob WHERE rid=%d)", fnid);
255
zLink = mprintf("forum post <a href='%R/forumpost/%t'>%#h</a>",
256
zForumPost, hash_digits(0), zForumPost);
257
}else if( zPage ){
258
if( g.perm.RdWiki==0 ){ login_needed(g.anon.RdWiki); return; }
259
style_header("Attachments To Wiki page %h", zPage);
260
blob_append_sql(&sql, " WHERE target=%Q", zPage);
261
zLink = mprintf("wiki page <a href='%R/wiki?name=%t'>%h</a>",
262
zPage, zPage);
263
}else if( zTkt ){
264
if( g.perm.RdTkt==0 ){ login_needed(g.anon.RdTkt); return; }
265
style_header("Attachments To Ticket %S", zTkt);
266
blob_append_sql(&sql, " WHERE target GLOB '%q*'", zTkt);
267
zLink = mprintf("ticket <a href='%R/tktview?name=%t'>%#h</a>",
268
zTkt, hash_digits(0), zTkt);
269
}else if( zTechNote ){
270
if( g.perm.RdWiki==0 ){ login_needed(g.anon.RdWiki); return; }
271
style_header("Attachments To Tech Note %S", zTechNote);
272
blob_append_sql(&sql, " WHERE target GLOB '%q*'",
273
zTechNote);
274
zLink = mprintf("tech-note <a href='%R/technote?name=%t'>%#h</a>",
275
zTechNote, hash_digits(0), zTechNote);
276
}else{
277
if( g.perm.RdTkt==0 && g.perm.RdWiki==0 ){
278
login_needed(g.anon.RdTkt || g.anon.RdWiki);
279
return;
280
}
281
style_header("All Attachments");
282
}
283
blob_append_sql(&sql, " ORDER BY mtime DESC");
284
db_prepare(&q, "%s", blob_sql_text(&sql));
285
286
if( zLink ){
287
@ <h2>Attachments for %s(zLink)</h2>
288
fossil_free(zLink);
289
zLink = 0;
290
}
291
292
@ <ol>
293
while( db_step(&q)==SQLITE_ROW ){
294
const char *zDate;
295
const char *zSrc;
296
const char *zTarget;
297
const char *zFilename;
298
const char *zComment;
299
const char *zUser;
300
const char *zUuid;
301
const char *zDispUser;
302
const int attachid = db_column_int(&q, 7);
303
int type;
304
int i;
305
int bDeleted;
306
char *zUrlTail = 0;
307
308
if( moderation_pending(attachid)
309
&& !moderation_user_could(attachid, 1, 0) ){
310
/* Elide entries which are currently pending moderation unless
311
** the user would be able to moderate the entry themselves. */
312
continue;
313
}
314
315
zDate = db_column_text(&q, 0);
316
zSrc = db_column_text(&q, 1);
317
zTarget = db_column_text(&q, 2);
318
zFilename = db_column_text(&q, 3);
319
zComment = db_column_text(&q, 4);
320
zUser = db_column_text(&q, 5);
321
zUuid = db_column_text(&q, 6);
322
zDispUser = zUser && zUser[0] ? zUser : "anonymous";
323
for(i=0; zFilename[i]; i++){
324
if( zFilename[i]=='/' && zFilename[i+1]!=0 ){
325
zFilename = &zFilename[i+1];
326
i = -1;
327
}
328
}
329
bDeleted = 0==zSrc || 0==zSrc[0];
330
type = attachment_target_type(zTarget, 1);
331
switch( type ){
332
case CFTYPE_TICKET:
333
zUrlTail = mprintf("tkt=%s&file=%t", zTarget, zFilename);
334
break;
335
case CFTYPE_EVENT:
336
zUrlTail = mprintf("technote=%s&file=%t", zTarget, zFilename);
337
break;
338
case CFTYPE_FORUM:
339
zUrlTail = mprintf("forumpost=%t&file=%t", zTarget, zFilename);
340
break;
341
case CFTYPE_WIKI:
342
zUrlTail = mprintf("page=%t&file=%t", zTarget, zFilename);
343
break;
344
}
345
@ <li><p>
346
if( bDeleted ){
347
@ <s>\
348
}
349
@ Attachment %z(href("%R/ainfo/%!S",zUuid))%S(zUuid)</a>\
350
moderation_pending_www(attachid);
351
@ <br>\
352
@ <a href="%R/attachview?%s(zUrlTail)">%h(zFilename)</a>
353
@ [<a href="%R/attachdownload/%t(zFilename)?%s(zUrlTail)">download</a>]\
354
if( bDeleted ){
355
@ </s>
356
}
357
@ <br>
358
if( zComment ) while( fossil_isspace(zComment[0]) ) zComment++;
359
if( zComment && zComment[0] ){
360
/* FIXME (2026-06-05): Honor the N-card (comment mimetype). %W
361
** (historically used here) assumes fossil-wiki and the
362
** fileformat.wiki doc has always claimed that it defaults to
363
** text/plain. /ainfo assumes it is plain text. */
364
@ %h(zComment)<br>
365
}
366
if( zForumPost==0 && zPage==0 && zTkt==0 && zTechNote==0 ){
367
if( bDeleted ){
368
zSrc = "Deleted from";
369
}else {
370
zSrc = "Added to";
371
}
372
switch( type ){
373
case CFTYPE_TICKET:
374
@ %s(zSrc) ticket <a href="%R/tktview?name=%s(zTarget)">
375
@ %S(zTarget)</a>
376
break;
377
case CFTYPE_EVENT:
378
@ %s(zSrc) tech note <a href="%R/technote/%s(zTarget)">
379
@ %S(zTarget)</a>
380
break;
381
case CFTYPE_WIKI:
382
@ %s(zSrc) wiki page <a href="%R/wiki?name=%t(zTarget)">
383
@ %h(zTarget)</a>
384
break;
385
case CFTYPE_FORUM:
386
@ %s(zSrc) forum post <a href="%R/forumpost/%s(zTarget)">
387
@ %h(zTarget)</a>
388
break;
389
default:
390
@ <span class='error'>%s(zSrc) cannot determine target type
391
@ of %h(zTarget)</span>
392
break;
393
}
394
}else{
395
if( zSrc==0 || zSrc[0]==0 ){
396
@ Deleted
397
}else {
398
@ Added
399
}
400
}
401
@ by %h(zDispUser) on
402
hyperlink_to_date(zDate, ".");
403
free(zUrlTail);
404
}
405
db_finalize(&q);
406
@ </ol>
407
style_finish_page();
408
return;
409
}
410
411
/*
412
** WEBPAGE: attachdownload
413
** WEBPAGE: attachimage
414
** WEBPAGE: attachview
415
**
416
** Download or display an attachment.
417
**
418
** Query parameters:
419
**
420
** tkt=HASH
421
** page=WIKIPAGE
422
** technote=HASH
423
** forumpost=HASH
424
** file=FILENAME
425
** attachid=ID
426
**
427
*/
428
void attachview_page(void){
429
const char *zPage = P("page");
430
const char *zTkt = P("tkt");
431
const char *zTechNote = P("technote");
432
const char *zForumPost = P("forumpost");
433
const char *zFile = P("file");
434
const char *zTarget = 0;
435
int attachid = atoi(PD("attachid","0"));
436
char *zUUID = 0;
437
438
if( zFile==0 ) fossil_redirect_home();
439
login_check_credentials();
440
style_set_current_feature("attach");
441
if( zForumPost ){
442
int fnid;
443
if( g.perm.RdForum==0 ){ login_needed(g.anon.RdForum); return; }
444
/* Forum attachments are always tied to the post's initial version */
445
fnid = forumpost_head_rid2(zForumPost);
446
if( fnid>0 ) zTarget = rid_to_uuid(fnid);
447
}else if( zPage ){
448
if( g.perm.RdWiki==0 ){ login_needed(g.anon.RdWiki); return; }
449
zTarget = zPage;
450
}else if( zTkt ){
451
if( g.perm.RdTkt==0 ){ login_needed(g.anon.RdTkt); return; }
452
zTarget = zTkt;
453
}else if( zTechNote ){
454
if( g.perm.RdWiki==0 ){ login_needed(g.anon.RdWiki); return; }
455
zTarget = zTechNote;
456
}else{
457
fossil_redirect_home();
458
}
459
if( attachid>0 ){
460
zUUID = db_text(0,
461
"SELECT coalesce(src,'x') FROM attachment"
462
" WHERE target=%Q AND attachid=%d",
463
zTarget, attachid
464
);
465
}else{
466
zUUID = db_text(0,
467
"SELECT coalesce(src,'x') FROM attachment"
468
" WHERE target=%Q AND filename=%Q"
469
" ORDER BY mtime DESC LIMIT 1",
470
zTarget, zFile
471
);
472
}
473
if( zUUID==0 || zUUID[0]==0 ){
474
style_header("No Such Attachment");
475
@ No such attachment....
476
style_finish_page();
477
return;
478
}else if( zUUID[0]=='x' ){
479
style_header("Missing");
480
@ Attachment has been deleted
481
style_finish_page();
482
return;
483
}else{
484
g.perm.Read = 1;
485
cgi_replace_parameter("name",zUUID);
486
if( fossil_strcmp(g.zPath,"attachview")==0 ){
487
artifact_page();
488
}else{
489
cgi_replace_parameter("m", mimetype_from_name(zFile));
490
rawartifact_page();
491
}
492
}
493
}
494
495
/*
496
** Save an attachment control artifact into the repository
497
*/
498
static void attach_put(
499
Blob *pAttach, /* Text of the Attachment record */
500
int attachRid, /* RID for the file that is being attached */
501
int needMod /* True if the attachment is subject to moderation */
502
){
503
int rid;
504
if( needMod ){
505
rid = content_put_ex(pAttach, 0, 0, 0, 1);
506
moderation_table_create();
507
db_multi_exec(
508
"INSERT INTO modreq(objid,attachRid) VALUES(%d,%d);",
509
rid, attachRid
510
);
511
}else{
512
rid = content_put(pAttach);
513
db_add_unsent(rid);
514
db_multi_exec("INSERT OR IGNORE INTO unclustered VALUES(%d);", rid);
515
}
516
manifest_crosslink(rid, pAttach, MC_NONE);
517
}
518
519
520
/*
521
** Commit a new attachment into the repository
522
*/
523
void attach_commit(
524
const char *zName, /* The filename of the attachment */
525
const char *zTarget, /* The artifact hash to attach to */
526
const char *aContent, /* The content of the attachment */
527
int szContent, /* The length of the attachment */
528
int needModerator, /* Moderate the attachment? */
529
const char *zComment /* The comment for the attachment */
530
){
531
Blob content;
532
Blob manifest;
533
Blob cksum;
534
char *zUUID;
535
char *zDate;
536
int rid;
537
int i, n;
538
int addCompress = 0;
539
Manifest *pManifest;
540
541
db_begin_transaction();
542
blob_init(&content, aContent, szContent);
543
pManifest = manifest_parse(&content, 0, 0);
544
addCompress = pManifest!=0;
545
manifest_destroy(pManifest);
546
blob_init(&content, aContent, szContent);
547
if( addCompress ){
548
blob_compress(&content, &content);
549
}
550
rid = content_put_ex(&content, 0, 0, 0, needModerator);
551
zUUID = rid_to_uuid(rid);
552
blob_zero(&manifest);
553
for(i=n=0; zName[i]; i++){
554
if( zName[i]=='/' || zName[i]=='\\' ) n = i+1;
555
}
556
zName += n;
557
if( zName[0]==0 ) zName = "unknown";
558
blob_appendf(&manifest, "A %F%s %F %s\n",
559
zName, addCompress ? ".gz" : "", zTarget, zUUID);
560
if( zComment!=0 && zComment[0]!=0 ){
561
while( fossil_isspace(zComment[0]) ) zComment++;
562
n = strlen(zComment);
563
while( n>0 && fossil_isspace(zComment[n-1]) ){ n--; }
564
if( n>0 ){
565
blob_appendf(&manifest, "C %#F\n", n, zComment);
566
}
567
}
568
zDate = date_in_standard_format("now");
569
blob_appendf(&manifest, "D %z\n", zDate);
570
blob_appendf(&manifest, "U %F\n", login_name());
571
md5sum_blob(&manifest, &cksum);
572
blob_appendf(&manifest, "Z %b\n", &cksum);
573
attach_put(&manifest, rid, needModerator);
574
assert( blob_is_reset(&manifest) );
575
db_end_transaction(0);
576
}
577
578
/*
579
** Renders the "legacy" (static) /attachadd form. One of the first
580
** four arguments must be non-NULL and the other three must be NULL.
581
** zComment may be NULL, as may zFrom. See the call sites for more
582
** context.
583
*/
584
static void attach_render_legacy_form(const char *zForumPost,
585
const char *zTechNote,
586
const char *zTicket,
587
const char *zWikiPage,
588
const char *zComment,
589
const char *zFrom){
590
form_begin("enctype='multipart/form-data' id='attachadd-legacy-form'",
591
"%R/attachadd");
592
@ <div>\
593
@ File to Attach:
594
@ <input type="file" name="f" size="60"><br>
595
@ Description:<br>
596
@ <textarea name="comment" cols="80" rows="5" wrap="virtual"\
597
@ >%h(zComment)</textarea><br>
598
if( zForumPost ){
599
@ <input type="hidden" name="forumpost" value="%h(zForumPost)">\
600
}else if( zTicket ){
601
@ <input type="hidden" name="tkt" value="%h(zTicket)">\
602
}else if( zTechNote ){
603
@ <input type="hidden" name="technote" value="%h(zTechNote)">\
604
}else if( zWikiPage ){
605
@ <input type="hidden" name="page" value="%h(zWikiPage)">\
606
}
607
@ <input type="hidden" name="from" value="%h(zFrom)">\
608
@ <input type="submit" name="ok" value="Add Attachment">\
609
@ <input type="submit" name="cancel" value="Cancel">\
610
@ </div>
611
captcha_generate(0);
612
login_insert_csrf_secret();
613
@ </form>
614
}
615
616
/*
617
** WEBPAGE: attachadd
618
** Add a new attachment.
619
**
620
** tkt=HASH
621
** page=WIKIPAGE
622
** technote=HASH
623
** forumpost=HASH
624
** from=URL
625
**
626
** Adds a POSTed file attachment to the given target.
627
**
628
** Or the "version 2" interface:
629
**
630
** target=ATTACHMENT_TARGET
631
**
632
** Behaves as documented for attachaddV2_page().
633
*/
634
void attachadd_page(void){
635
const char *zPage;
636
const char *zForumPost;
637
const char *zTkt;
638
const char *zTechNote;
639
const char *aContent;
640
const char *zName;
641
const char *zComment;
642
const char *zTarget;
643
const char *zFrom; /* Origin page - redirect here after saving */
644
char *zTo = 0; /* Optionally redirect here after saving */
645
char *zTargetType = 0;
646
char *zExtraFree = 0;
647
int szContent;
648
int goodCaptcha = 1;
649
int szLimit = 0;
650
651
if( P("target")!=0 ){
652
attachaddV2_page();
653
return;
654
}
655
zPage = P("page");
656
zForumPost = P("forumpost");
657
zTkt = P("tkt");
658
zTechNote = P("technote");
659
zFrom = P("from");
660
aContent = P("f");
661
zName = PD("f:filename","unknown");
662
zComment = PD("comment", "");
663
szContent = atoi(PD("f:bytes","0"));
664
665
if( zFrom==0 ) zFrom = mprintf("%R/home");
666
if( P("cancel") ) cgi_redirect(zFrom);
667
if( (!!zPage + !!zTkt + !!zTechNote + !!zForumPost)!=1 ){
668
webpage_error("Requires exactly one one: page=X, tkt=X, forumpost=X,"
669
" technote=X, or target=X");
670
}
671
login_check_credentials();
672
if( zForumPost ){
673
int fpid;
674
if( g.perm.AttachForum==0 ){
675
login_needed(g.anon.AttachForum);
676
return;
677
}
678
fpid = forumpost_head_rid2(zForumPost);
679
if( fpid<=0 ){
680
webpage_error("Invalid forum post ID: %h", zForumPost);
681
}else if( !g.perm.Admin && !forumpost_is_owner(fpid, 0) ){
682
webpage_error("Only admins can attach files to other users' "
683
"forum posts.");
684
}
685
zTarget = zExtraFree = rid_to_uuid(fpid);
686
zTargetType = mprintf("Forum post <a href=\"%R/forumpost/%S\">%h</a>",
687
zTarget, zForumPost);
688
zTo = zFrom ? 0 : mprintf("%R/forumpost/%S", zTarget);
689
}else if( zPage ){
690
if( g.perm.ApndWiki==0 || g.perm.Attach==0 ){
691
login_needed(g.anon.ApndWiki && g.anon.Attach);
692
return;
693
}
694
if( !db_exists("SELECT 1 FROM tag WHERE tagname='wiki-%q'", zPage) ){
695
fossil_redirect_home();
696
}
697
zTarget = zPage;
698
zTargetType = mprintf("Wiki Page <a href=\"%R/wiki?name=%t\">%h</a>",
699
zPage, zPage);
700
zTo = zFrom ? 0 : mprintf("%R/wiki?name=%T", zTarget);
701
}else if ( zTechNote ){
702
if( g.perm.Write==0 || g.perm.ApndWiki==0 || g.perm.Attach==0 ){
703
login_needed(g.anon.Write && g.anon.ApndWiki && g.anon.Attach);
704
return;
705
}
706
if( !db_exists("SELECT 1 FROM tag WHERE tagname='event-%q'", zTechNote) ){
707
zTechNote = db_text(0, "SELECT substr(tagname,7) FROM tag"
708
" WHERE tagname GLOB 'event-%q*'", zTechNote);
709
if( zTechNote==0) fossil_redirect_home();
710
}
711
zTarget = zTechNote;
712
zTargetType = mprintf("Tech Note <a href=\"%R/technote/%s\">%S</a>",
713
zTechNote, zTechNote);
714
zTo = zFrom ? 0 : mprintf("%R/technote/%S", zTarget);
715
}else{
716
assert( zTkt );
717
if( g.perm.ApndTkt==0 || g.perm.Attach==0 ){
718
login_needed(g.anon.ApndTkt && g.anon.Attach);
719
return;
720
}
721
if( !db_exists("SELECT 1 FROM tag WHERE tagname='tkt-%q'", zTkt) ){
722
zTkt = db_text(0, "SELECT substr(tagname,5) FROM tag"
723
" WHERE tagname GLOB 'tkt-%q*'", zTkt);
724
if( zTkt==0 ) fossil_redirect_home();
725
}
726
zTarget = zTkt;
727
zTargetType = mprintf("Ticket <a href=\"%R/tktview/%S\">%S</a>",
728
zTkt, zTkt);
729
zTo = zFrom ? 0 : mprintf("%R/tktview/%S", zTarget);
730
}
731
szLimit = db_get_int("attachment-size-limit", 0);
732
if( szContent<0 || (szLimit && szContent>szLimit) ){
733
/* This check must be done late so that zTargetType is set up. */
734
@ <p class="generalError">Attachment %h(zName) is too large.
735
@ <a href="%R/help/attachment-size-limit">Limit</a> is
736
@ %d(szLimit ? szLimit : 0x7fffffff) bytes</p>
737
/* Fall through and render form. */
738
}else if( P("ok")
739
&& cgi_csrf_safe(2)
740
&& szContent>0
741
&& (goodCaptcha = captcha_is_correct(0)) ){
742
int needModerator = (zForumPost!=0 && forum_need_moderation()) ||
743
(zTkt!=0 && ticket_need_moderation(0)) ||
744
(zPage!=0 && wiki_need_moderation(0));
745
attach_commit(zName, zTarget, aContent, szContent, needModerator, zComment);
746
cgi_redirect(zTo ? zTo : zFrom);
747
}
748
749
style_set_current_feature("attach");
750
style_header("Add Attachment");
751
if( !goodCaptcha ){
752
@ <p class="generalError">Error: Incorrect security code.</p>
753
}
754
@ <h2>Add Attachment To %s(zTargetType)</h2>
755
attach_render_legacy_form(zForumPost, zTechNote, zTkt, zPage,
756
zComment, zFrom);
757
builtin_fossil_js_bundle_or("attach", NULL);
758
style_finish_page();
759
fossil_free(zTargetType);
760
fossil_free(zExtraFree);
761
}
762
763
/*
764
** WEBPAGE: attachadd_ajax_post hidden
765
**
766
** Used by attachadd V2 to handle attachments via POST requests with:
767
**
768
** target=ATTACHMENT_TARGET
769
** file1..fileN=FILE_OBJECTS
770
** dryrun=0|1
771
**
772
** Each posted file in the set file1..fileN gets attached to the given
773
** target, permissions permitting. If dryrun>0 then the change is
774
** rolled back instead of committed. target=X must refer to a full
775
** target ID, not a prefix.
776
**
777
** Responds with JSON: an empty object on success and
778
** {error:"message"} on error. The on-success response structure is
779
** subject to amendment.
780
*/
781
void attachadd_ajax_post(void){
782
const char *zTarget;
783
char *zExtraFree = 0;
784
int eTgtType = 0;
785
int bNeedsModeration = 0;
786
int goodCaptcha = 1;
787
int bRollback = 0; /* Roll back if true. */
788
789
if( ! ajax_route_bootstrap(0, 1) ){
790
return;
791
}else if( !(goodCaptcha = captcha_is_correct(0)) ){
792
goto ajax_err_403;
793
}else if( !ajax_check_csrf(2) ){
794
return;
795
}
796
db_begin_transaction();
797
zTarget = P("target");
798
eTgtType = attachment_target_type(zTarget, 1);
799
CX("{");
800
switch( eTgtType ){
801
default:
802
case 0:
803
ajax_route_error(400, "Invalid attachment target.");
804
db_rollback_transaction();
805
return;
806
case CFTYPE_FORUM:{
807
int fpid;
808
if( g.perm.AttachForum==0 ){
809
goto ajax_err_403;
810
}
811
fpid = forumpost_head_rid2(zTarget);
812
if( fpid<=0 ){
813
goto ajax_err_404;
814
}else if( !g.perm.Admin && !forumpost_is_owner(fpid, 0) ){
815
ajax_route_error(403, "Only admins can attach files to "
816
"other users' forum posts.");
817
db_rollback_transaction();
818
return;
819
}
820
zTarget = zExtraFree = rid_to_uuid(fpid);
821
bNeedsModeration = forum_need_moderation();
822
break;
823
}
824
case CFTYPE_EVENT:{
825
if( g.perm.Write==0 || g.perm.ApndWiki==0 || g.perm.Attach==0 ){
826
goto ajax_err_403;
827
}
828
if( !db_exists("SELECT 1 FROM tag WHERE tagname='event-%q'",
829
zTarget) ){
830
zTarget = zExtraFree =
831
db_text(0, "SELECT substr(tagname,7) FROM tag"
832
" WHERE tagname GLOB 'event-%q*'", zTarget);
833
if( zTarget==0){
834
goto ajax_err_404;
835
}
836
}
837
bNeedsModeration = 0;
838
break;
839
}
840
case CFTYPE_TICKET:{
841
if( g.perm.ApndTkt==0 || g.perm.Attach==0 ){
842
goto ajax_err_403;
843
}
844
if( !db_exists("SELECT 1 FROM tag WHERE tagname='tkt-%q'",
845
zTarget) ){
846
zTarget = db_text(0, "SELECT substr(tagname,5) FROM tag"
847
" WHERE tagname GLOB 'tkt-%q*'", zTarget);
848
if( zTarget==0 ){
849
goto ajax_err_404;
850
}
851
}
852
bNeedsModeration = ticket_need_moderation(0);
853
break;
854
}
855
case CFTYPE_WIKI:{
856
if( g.perm.ApndWiki==0 || g.perm.Attach==0 ){
857
goto ajax_err_403;
858
}
859
if( !db_exists("SELECT 1 FROM tag WHERE tagname='wiki-%q'",
860
zTarget) ){
861
goto ajax_err_404;
862
}
863
bNeedsModeration = wiki_need_moderation(0);
864
break;
865
}
866
}
867
868
if( attachments_ajax_from_POST(zTarget, bNeedsModeration)>=0 ){
869
CX("}");
870
if( atoi(PD("dryrun","0"))>0 ){
871
bRollback = 1;
872
}
873
}/*else error response was set up*/
874
fossil_free(zExtraFree);
875
db_end_transaction(bRollback);
876
return;
877
ajax_err_403:
878
if( db_transaction_nesting_depth()>0 ){
879
db_rollback_transaction();
880
}
881
ajax_route_error_forbidden();
882
return;
883
ajax_err_404:
884
assert( db_transaction_nesting_depth()>0 );
885
db_rollback_transaction();
886
ajax_route_error(404, "Target not found.");
887
return;
888
}
889
890
/*
891
** A helper for AJAX-style routines which accept file attachments via
892
** POST. zTarget must be a full attachment target. bNeedsModeration
893
** must be true if the attachment requires moderation.
894
**
895
** It is up to the caller to have validated all security measures
896
** before calling this.
897
**
898
** This looks for POSTed files names "file1".."fileN", stopping when
899
** it finds no entry. Returns the number of entries attached to the
900
** target or a negative value on error (in which case the current db
901
** transaction will be in a rollback state).
902
**
903
** The only errors are currently attachment size limit violations:
904
** attachments must have a non-0 size and if the attachment-size-limit
905
** setting is >0 then each file's size must be <= that.
906
**
907
** If this returns a negative value, it will have populated an error
908
** response using ajax_route_error(). On success it produces no
909
** output.
910
**
911
** ACHTUNG: if zTarget is a forum post, it "really should" be the ID
912
** of the first version of that post, as that's where attachments are
913
** intended to be applied so that they can be found and removed
914
** consistently. Potential TODO is have this function do that if
915
** attachment_target_type(zTarget,1)!=0 but it would (for current
916
** uses) require duplicating work already done in the callers.
917
*/
918
int attachments_ajax_from_POST(const char *zTarget, int bNeedsModeration){
919
int i;
920
int rc = 0;
921
int n = 0;
922
int szLimit; /* attachment-max-size setting */
923
char aKeyPrefix[20]; /* Buffer for key "file%d" */
924
char aKeySize[30]; /* Buffer for key "file%d:bytes" */
925
char aKeyName[30]; /* Buffer for key "file%d:filename" */
926
char aKeyDesc[30]; /* Buffer for key "file%d_desc" */
927
928
db_begin_transaction();
929
szLimit = db_get_int("attachment-size-limit", 0);
930
931
for(i = 1; ; ++i, ++n){
932
/* Look for P("fileN"), where N=1..n */
933
const char *zContent;
934
const char *zFilename;
935
int szContent;
936
sqlite3_snprintf(sizeof(aKeyPrefix), aKeyPrefix, "file%d", i);
937
zContent = P(aKeyPrefix);
938
if( !zContent ){
939
/* End of the list. */
940
break;
941
}
942
sqlite3_snprintf(sizeof(aKeySize), aKeySize, "%s:bytes",
943
aKeyPrefix);
944
szContent = atoi(PD(aKeySize,"-1"));
945
if( szContent<=0 ){
946
rc = -ajax_route_error(400,"Invalid file size: %d", szContent);
947
break;
948
}else if( szLimit>0 && szContent>szLimit ){
949
rc = -ajax_route_error(413, "File size limit is %d bytes.", szLimit);
950
break;
951
}else{
952
sqlite3_snprintf(sizeof(aKeyName), aKeyName, "%s:filename",
953
aKeyPrefix);
954
sqlite3_snprintf(sizeof(aKeyDesc), aKeyDesc, "%s_desc",
955
aKeyPrefix);
956
if( 0==(zFilename=P(aKeyName)) ){
957
rc = -ajax_route_error(400, "Missing filename.");
958
break;
959
}
960
attach_commit(zFilename, zTarget, zContent, szContent,
961
bNeedsModeration, P(aKeyDesc));
962
}
963
}
964
if( rc<0 ){
965
db_rollback_transaction();
966
return rc;
967
}else{
968
db_commit_transaction();
969
return n;
970
}
971
}
972
973
/*
974
** Proxy for /attachadd?target=X
975
**
976
** Lists attachments for, and can add them to, a target artifact.
977
**
978
** target=TKT_HASH|WIKIPAGE_NAME|TECHNOTE_HASH|FORUMPOST_HASH
979
** from=ORIGINATING_URL
980
**
981
** Works like /attachadd but uses a JS-based interactive attachment
982
** selector.
983
**
984
** from=X tells it where to redirect to when it's done.
985
**
986
** This page requires a post-2018-ish JS-capable browser.
987
*/
988
void attachaddV2_page(void){
989
const char *zFrom = P("from");
990
const char *zTarget = P("target");
991
char *zTo = 0;
992
char *zTargetType = 0;
993
char *zExtraFree = 0;
994
int eTgtType = 0;
995
int goodCaptcha = 1;
996
char const * noJsArgs[] = {0,0,0,0}; /* Args for noscript form */
997
998
if( P("cancel") ) cgi_redirect(zFrom);
999
if( 0==zTarget ){
1000
webpage_error("Requires target=X");
1001
}
1002
login_check_credentials();
1003
eTgtType = attachment_target_type(zTarget, 1);
1004
switch( eTgtType ){
1005
default:
1006
case 0:
1007
webpage_error("Cannot resolve target=%h.", zTarget);
1008
break;
1009
case CFTYPE_FORUM:{
1010
int fpid;
1011
if( g.perm.AttachForum==0 ){
1012
login_needed(g.anon.AttachForum);
1013
return;
1014
}
1015
fpid = forumpost_head_rid2(zTarget);
1016
if( fpid<=0 ){
1017
webpage_error("Invalid forum post ID: %h", zTarget);
1018
}else if( !g.perm.Admin && !forumpost_is_owner(fpid, 0) ){
1019
webpage_error("Only admins can attach files to other users' "
1020
"forum posts.");
1021
}
1022
zTarget = zExtraFree = rid_to_uuid(fpid);
1023
noJsArgs[0] = zTarget;
1024
zTargetType = mprintf(
1025
"Forum post <a href=\"%R/forumpost/%S\">%.16h</a>",
1026
zTarget, zTarget
1027
);
1028
zTo = mprintf("%R/forumpost/%S", zTarget);
1029
break;
1030
}
1031
case CFTYPE_EVENT:{
1032
if( g.perm.Write==0 || g.perm.ApndWiki==0 || g.perm.Attach==0 ){
1033
login_needed(g.anon.Write && g.anon.ApndWiki && g.anon.Attach);
1034
return;
1035
}
1036
if( !db_exists("SELECT 1 FROM tag WHERE tagname='event-%q'",
1037
zTarget) ){
1038
zTarget = db_text(0, "SELECT substr(tagname,7) FROM tag"
1039
" WHERE tagname GLOB 'event-%q*'",
1040
zTarget);
1041
if( zTarget==0) fossil_redirect_home();
1042
}
1043
zTo = zFrom ? 0 : mprintf("%R/technote?name=%T", zTarget);
1044
zTargetType = mprintf("Tech-note <a href=\"%R/technote/%s\">%S</a>",
1045
zTarget, zTarget);
1046
noJsArgs[1] = zTarget;
1047
break;
1048
}
1049
case CFTYPE_TICKET:{
1050
if( g.perm.ApndTkt==0 || g.perm.Attach==0 ){
1051
login_needed(g.anon.ApndTkt && g.anon.Attach);
1052
return;
1053
}
1054
if( !db_exists("SELECT 1 FROM tag WHERE tagname='tkt-%q'",
1055
zTarget) ){
1056
zTarget = db_text(0, "SELECT substr(tagname,5) FROM tag"
1057
" WHERE tagname GLOB 'tkt-%q*'", zTarget);
1058
if( zTarget==0 ) fossil_redirect_home();
1059
}
1060
zTo = zFrom ? 0 : mprintf("%R/tktview/%t", zTarget);
1061
zTargetType = mprintf("Ticket <a href=\"%R/tktview/%s\">%S</a>",
1062
zTarget, zTarget);
1063
noJsArgs[2] = zTarget;
1064
break;
1065
}
1066
case CFTYPE_WIKI:{
1067
if( g.perm.ApndWiki==0 || g.perm.Attach==0 ){
1068
login_needed(g.anon.ApndWiki && g.anon.Attach);
1069
return;
1070
}
1071
if( !db_exists("SELECT 1 FROM tag WHERE tagname='wiki-%q'",
1072
zTarget) ){
1073
fossil_redirect_home();
1074
}
1075
zTo = zFrom ? 0 : mprintf("%R/wiki?name=%T", zTarget);
1076
zTargetType = mprintf(
1077
"Wiki page <a href=\"%R/wiki?name=%h\">%h</a>",
1078
zTarget, zTarget
1079
);
1080
noJsArgs[3] = zTarget;
1081
break;
1082
}
1083
}
1084
1085
db_begin_transaction();
1086
1087
style_set_current_feature("attach");
1088
style_header("Add Attachment");
1089
if( !goodCaptcha ){
1090
@ <p class="generalError">Error: Incorrect security code.</p>
1091
}
1092
@ <h2>Attachments for %s(zTargetType)</h2>
1093
attachment_list(zTarget, NULL,
1094
ATTACHLIST_SIZE | ATTACHLIST_HIDE_UNAPPROVED);
1095
attach_render_legacy_form(
1096
noJsArgs[0], noJsArgs[1], noJsArgs[2],
1097
noJsArgs[3], 0,
1098
zFrom ? zFrom : (zTo ? zTo : (zTo=mprintf("%R/home")))
1099
);
1100
@ <div id='attachadd-form-wrapper' class='hidden'>
1101
/* fossil.attach.js populates this DIV with the attachment widget,
1102
** imports these hidden fields, and removes the legacy form. */
1103
@ <input type="hidden" name="target" value="%h(zTarget)">
1104
if( zFrom ){
1105
@ <input type="hidden" name="from" value="%h(zFrom)">
1106
}
1107
if( zTo ){
1108
@ <input type="hidden" name="to" value="%h(zTo)">
1109
}
1110
captcha_generate(0);
1111
login_insert_csrf_secret();
1112
@ </div>
1113
builtin_fossil_js_bundle_or("attach", NULL);
1114
db_end_transaction(0);
1115
style_finish_page();
1116
fossil_free(zTargetType);
1117
fossil_free(zExtraFree);
1118
fossil_free(zTo);
1119
}
1120
1121
/*
1122
** WEBPAGE: ainfo
1123
** URL: /ainfo?name=ARTIFACTID
1124
**
1125
** name=ATTACHMENT_ARTIFACT_UUID
1126
**
1127
** Show the details of an attachment artifact.
1128
*/
1129
void ainfo_page(void){
1130
int rid; /* RID for the control artifact */
1131
int ridSrc; /* RID for the attached file */
1132
char *zDate; /* Date attached */
1133
const char *zUuid; /* Hash of the control artifact */
1134
Manifest *pAttach; /* Parse of the control artifact */
1135
const char *zTarget; /* Wiki, ticket or tech note attached to */
1136
const char *zSrc; /* Hash of the attached file */
1137
const char *zName; /* Name of the attached file */
1138
const char *zDesc; /* Description of the attached file */
1139
const char *zWikiName = 0; /* Wiki page name when attached to Wiki */
1140
const char *zTNUuid = 0; /* Tech Note ID when attached to tech note */
1141
const char *zTktUuid = 0; /* Ticket ID when attached to a ticket */
1142
const char *zForumPost = 0; /* Forum UID when attached to forum post */
1143
int modPending; /* True if awaiting moderation */
1144
const char *zModAction; /* Moderation action or NULL */
1145
int isModerator; /* TRUE if user is the moderator */
1146
const char *zMime; /* MIME Type */
1147
Blob attach; /* Content of the attachment */
1148
int fShowContent = 0; /* True to emit the content */
1149
int bUserIsOwner = 0; /* True if pAttach->zUser is login_name() */
1150
int showDelMenu = 0; /* True to enable delete option */
1151
const char *zLn = P("ln");
1152
1153
login_check_credentials();
1154
if( !g.perm.RdTkt && !g.perm.RdWiki ){
1155
login_needed(g.anon.RdTkt || g.anon.RdWiki);
1156
return;
1157
}
1158
rid = name_to_rid_www("name");
1159
if( rid==0 ){ fossil_redirect_home(); }
1160
zUuid = rid_to_uuid(rid);
1161
pAttach = manifest_get(rid, CFTYPE_ATTACHMENT, 0);
1162
if( pAttach==0 ) fossil_redirect_home();
1163
bUserIsOwner =
1164
0==fossil_strcmp(pAttach->zUser, login_name())
1165
&& login_is_individual();
1166
zTarget = pAttach->zAttachTarget;
1167
zSrc = pAttach->zAttachSrc;
1168
ridSrc = db_int(0,"SELECT rid FROM blob WHERE uuid='%q'", zSrc);
1169
zName = pAttach->zAttachName;
1170
zDesc = pAttach->zComment;
1171
zMime = mimetype_from_name(zName);
1172
fShowContent = zMime ? strncmp(zMime,"text/", 5)==0 : 0;
1173
if( db_int(0,"SELECT 1 FROM event WHERE objid=%d and type='f'", rid) ){
1174
if( !g.perm.RdForum ){ login_needed(g.anon.RdForum); return; }
1175
showDelMenu = g.perm.Admin || bUserIsOwner;
1176
zForumPost = zTarget;
1177
}else if( validate16(zTarget, strlen(zTarget))
1178
&& db_exists("SELECT 1 FROM ticket WHERE tkt_uuid='%q'", zTarget)
1179
){
1180
if( !g.perm.RdTkt ){ login_needed(g.anon.RdTkt); return; }
1181
zTktUuid = zTarget;
1182
showDelMenu = g.perm.WrTkt;
1183
}else if( db_exists("SELECT 1 FROM tag WHERE tagname='wiki-%q'",
1184
zTarget) ){
1185
if( !g.perm.RdWiki ){ login_needed(g.anon.RdWiki); return; }
1186
zWikiName = zTarget;
1187
showDelMenu = g.perm.WrWiki;
1188
}else if( db_exists("SELECT 1 FROM tag WHERE tagname='event-%q'",
1189
zTarget) ){
1190
if( !g.perm.RdWiki ){ login_needed(g.anon.RdWiki); return; }
1191
zTNUuid = zTarget;
1192
showDelMenu = g.perm.Write && g.perm.WrWiki;
1193
}
1194
if( showDelMenu ){
1195
style_submenu_element("Delete", "%R/ainfo/%s?del", zUuid);
1196
}
1197
zDate = db_text(0, "SELECT datetime(%.12f)", pAttach->rDate);
1198
1199
if( P("confirm")
1200
&& cgi_csrf_safe(2)
1201
&& ((zForumPost
1202
&& ((bUserIsOwner && g.perm.AttachForum) ||
1203
forumpost_may_close())) ||
1204
(zTktUuid && g.perm.WrTkt) ||
1205
(zWikiName && g.perm.WrWiki) ||
1206
(zTNUuid && g.perm.Write && g.perm.WrWiki))
1207
){
1208
/* Delete attachment. */
1209
int i, n, rid;
1210
char *zNewDate;
1211
Blob manifest;
1212
Blob cksum;
1213
const char *zFile = zName;
1214
1215
if( !bUserIsOwner ){
1216
if( zForumPost ? !forumpost_may_close() : !g.perm.Admin ){
1217
webpage_error(
1218
"Only admins can delete other users' attachments."
1219
);
1220
}
1221
}
1222
db_begin_transaction();
1223
blob_zero(&manifest);
1224
for(i=n=0; zFile[i]; i++){
1225
if( zFile[i]=='/' || zFile[i]=='\\' ) n = i;
1226
}
1227
zFile += n;
1228
if( zFile[0]==0 ) zFile = "unknown";
1229
blob_appendf(&manifest, "A %F %F\n", zFile, zTarget);
1230
zNewDate = date_in_standard_format("now");
1231
blob_appendf(&manifest, "D %s\n", zNewDate);
1232
blob_appendf(&manifest, "U %F\n", login_name());
1233
md5sum_blob(&manifest, &cksum);
1234
blob_appendf(&manifest, "Z %b\n", &cksum);
1235
rid = content_put(&manifest);
1236
manifest_crosslink(rid, &manifest, MC_NONE);
1237
db_end_transaction(0);
1238
@ <p>The attachment below has been deleted.</p>
1239
fossil_free(zNewDate);
1240
}
1241
1242
if( P("del")
1243
&& ((zForumPost && (bUserIsOwner || forumpost_may_close()))
1244
|| (zTktUuid && g.perm.WrTkt)
1245
|| (zWikiName && g.perm.WrWiki)
1246
|| (zTNUuid && g.perm.Write && g.perm.WrWiki))
1247
){
1248
form_begin(0, "%R/ainfo/%!S", zUuid);
1249
@ <p>Confirm you want to delete the attachment shown below.
1250
@ <input type="submit" name="confirm" value="Confirm">
1251
login_insert_csrf_secret();
1252
@ </form>
1253
}
1254
1255
isModerator = g.perm.Admin ||
1256
(zForumPost && g.perm.ModForum) ||
1257
(zTktUuid && g.perm.ModTkt) ||
1258
(zWikiName && g.perm.ModWiki);
1259
zModAction = P("modaction");
1260
if( zModAction!=0 && cgi_csrf_safe(2) ){
1261
if( strcmp(zModAction,"delete")==0 ){
1262
if( isModerator || bUserIsOwner ){
1263
moderation_disapprove(rid);
1264
}
1265
if( zForumPost ){
1266
cgi_redirectf("%R/forumpost/%!S", zForumPost);
1267
}else if( zTktUuid ){
1268
cgi_redirectf("%R/tktview/%!S", zTktUuid);
1269
}else if( zWikiName ) {
1270
cgi_redirectf("%R/wiki?name=%t", zWikiName);
1271
}
1272
/* zTNUuid is intentionally unhandled. Tech note attachments
1273
** don't go through moderation. */
1274
return;
1275
}
1276
if( isModerator && strcmp(zModAction,"approve")==0 ){
1277
moderation_approve('a', rid);
1278
}
1279
}
1280
style_set_current_feature("attach");
1281
style_header("Attachment Details");
1282
style_submenu_element("Raw", "%R/artifact/%s", zUuid);
1283
if(fShowContent){
1284
style_submenu_element("Line Numbers", "%R/ainfo/%s%s", zUuid,
1285
((zLn&&*zLn) ? "" : "?ln=0"));
1286
}
1287
1288
@ <div class="section">Overview</div>
1289
@ <p><table class="label-value">
1290
@ <tr><th>Artifact&nbsp;ID:</th>
1291
@ <td>%z(href("%R/artifact/%!S",zUuid))%s(zUuid)</a>
1292
if( g.perm.Setup ){
1293
@ (%d(rid))
1294
}
1295
modPending = moderation_pending_www(rid);
1296
if( zForumPost ){
1297
@ <tr><th>Forum&nbsp;Post:</th>
1298
@ <td>%z(href("%R/forumpost/%s",zForumPost))%h(zForumPost)</a>\
1299
@ </td></tr>
1300
}else if( zTktUuid ){
1301
@ <tr><th>Ticket:</th>
1302
@ <td>%z(href("%R/tktview/%s",zTktUuid))%s(zTktUuid)</a></td></tr>
1303
}else if( zTNUuid ){
1304
@ <tr><th>Tech Note:</th>
1305
@ <td>%z(href("%R/technote/%s",zTNUuid))%s(zTNUuid)</a></td></tr>
1306
}else if( zWikiName ){
1307
@ <tr><th>Wiki&nbsp;Page:</th>
1308
@ <td>%z(href("%R/wiki?name=%t",zWikiName))%h(zWikiName)</a>\
1309
@ </td></tr>
1310
}
1311
@ <tr><th>Date:</th><td>
1312
hyperlink_to_date(zDate, "</td></tr>");
1313
@ <tr><th>User:</th><td>
1314
hyperlink_to_user(pAttach->zUser, zDate, "</td></tr>");
1315
@ <tr><th>Artifact&nbsp;Attached:</th>
1316
@ <td>%z(href("%R/artifact/%s",zSrc))%s(zSrc)</a>
1317
if( g.perm.Setup ){
1318
@ (%d(ridSrc))
1319
}
1320
@ <tr><th>Filename:</th><td>%h(zName)</td></tr>
1321
if( g.perm.Setup ){
1322
@ <tr><th>MIME-Type:</th><td>%h(zMime)</td></tr>
1323
}
1324
@ <tr><th valign="top">Description:</th>\
1325
/* FIXME (2026-06-05): Honor the N-card (comment mimetype). */
1326
@ <td valign="top">%h(zDesc)</td></tr>
1327
@ </table>
1328
1329
if( modPending && (isModerator || bUserIsOwner) ){
1330
@ <div class="section">Moderation</div>
1331
@ <blockquote>
1332
form_begin(0, "%R/ainfo/%s", zUuid);
1333
@ <label><input type="radio" name="modaction" value="delete">
1334
@ Delete this attachment</label><br>
1335
if( isModerator ){
1336
#if 0
1337
/* TODO/FIXME (2026-06-03): only allow approval of an attachment
1338
** if its target has been approved. Without this, we can end up
1339
** with stale attachments which refer to rejected targets. We
1340
** need a type-specific RID/UUID here, which requires
1341
** refactoring above to get it. */
1342
const int tgtid = 0;
1343
if( moderation_pending(tgtid) ){
1344
@ <label><input type="radio" name="modaction" \
1345
@ disabled value="approve">
1346
@ <span class='modpending'>Cannot approve:
1347
@ target is pending moderation</span>\
1348
@ </label><br>
1349
}else
1350
#else
1351
{
1352
@ <label><input type="radio" name="modaction" value="approve">
1353
@ Approve this attachment</label><br>
1354
}
1355
#endif
1356
}
1357
@ <input type="submit" value="Submit">
1358
login_insert_csrf_secret();
1359
@ </form>
1360
@ </blockquote>
1361
}
1362
1363
@ <div class="section">Content:</div>
1364
blob_zero(&attach);
1365
if( modPending && !moderation_user_could(rid, 1, 0) ){
1366
@ <p><span class="modpending">Content is awaiting moderator \
1367
@ approval.</span></p>
1368
}else{
1369
@ <blockquote>
1370
if( fShowContent ){
1371
const char *z;
1372
content_get(ridSrc, &attach);
1373
blob_to_utf8_no_bom(&attach, 0);
1374
z = blob_str(&attach);
1375
if( zLn ){
1376
output_text_with_line_numbers(z, blob_size(&attach),
1377
zName, zLn, 1);
1378
}else{
1379
@ <pre>
1380
@ %h(z)
1381
@ </pre>
1382
}
1383
}else if( strncmp(zMime, "image/", 6)==0 ){
1384
int sz = db_int(0, "SELECT size FROM blob WHERE rid=%d", ridSrc);
1385
@ <i>(file is %d(sz) bytes of image data)</i><br>
1386
@ <img src="%R/raw/%s(zSrc)?m=%s(zMime)"></img>
1387
style_submenu_element("Image", "%R/raw/%s?m=%s", zSrc, zMime);
1388
}else{
1389
int sz = db_int(0, "SELECT size FROM blob WHERE rid=%d", ridSrc);
1390
@ <i>(file is %d(sz) bytes of binary data)</i>
1391
}
1392
@ </blockquote>
1393
}
1394
manifest_destroy(pAttach);
1395
blob_reset(&attach);
1396
style_finish_page();
1397
}
1398
1399
#if INTERFACE
1400
/*
1401
** Flags for use with attachment_list(). ATTACHLIST_HRULE_ABOVE
1402
** must have a value of 1 for historical call compatibility.
1403
*/
1404
#define ATTACHLIST_HRULE_ABOVE 0x01 /* Insert <hr> above header */
1405
#define ATTACHLIST_TARGET_BLANK 0x02 /* use target=_blank for links */
1406
#define ATTACHLIST_SIZE 0x04 /* add size */
1407
#define ATTACHLIST_HIDE_UNAPPROVED 0x08 /* Hide pending-moderation files */
1408
#define ATTACHLIST_DETAILS_CLOSED 0x10 /* Wrap in a closed DETAILS element */
1409
#define ATTACHLIST_DETAILS_OPEN 0x20 /* Wrap in an open DETAILS element */
1410
#define ATTACHLIST_HIDE_EMPTY 0x40 /* Skip if size<1 */
1411
#endif
1412
1413
/*
1414
** Output HTML to show a list of attachments.
1415
*/
1416
void attachment_list(
1417
const char *zTarget, /* Object that things are attached to */
1418
const char *zHeader, /* Header to display with attachments */
1419
const int flags /* ATTACHLIST_... flags */
1420
){
1421
int cnt = 0;
1422
char szBuf[36] = {0}; /* scratchpad for attachment size value */
1423
const char *zLinkTgt = (ATTACHLIST_TARGET_BLANK & flags)
1424
? " target=\"_blank\"" : "";
1425
const int bUseDetail = flags &
1426
(ATTACHLIST_DETAILS_CLOSED | ATTACHLIST_DETAILS_OPEN);
1427
Stmt q;
1428
1429
db_prepare(&q,
1430
"SELECT datetime(mtime,toLocal()), a.filename, a.user,"
1431
" b1.uuid, a.src, a.target, a.attachid, b2.size\n"
1432
" FROM attachment a, blob b1, blob b2\n"
1433
" WHERE a.isLatest\n"
1434
" AND a.src IS NOT NULL\n"
1435
" AND a.target=%Q\n"
1436
" AND b1.rid=a.attachid\n"
1437
" AND b2.uuid=a.src\n"
1438
" ORDER BY mtime DESC",
1439
zTarget
1440
);
1441
while( db_step(&q)==SQLITE_ROW ){
1442
const char *zDate = db_column_text(&q, 0);
1443
const char *zFile = db_column_text(&q, 1);
1444
const char *zUser = db_column_text(&q, 2);
1445
const char *zUuid = db_column_text(&q, 3);
1446
const char *zSrc = db_column_text(&q, 4);
1447
const char *zTarget = db_column_text(&q, 5);
1448
const char *zDispUser = zUser && zUser[0] ? zUser : "anonymous";
1449
const char *zTypeArg = 0; /* URL arg name for /attachdownload */
1450
const int aid = db_column_int(&q, 6);
1451
const int sz = db_column_int(&q, 7);
1452
if( (flags & ATTACHLIST_HIDE_UNAPPROVED)
1453
&& moderation_pending(aid)
1454
&& !moderation_user_could(aid, 1, 0) ){
1455
continue;
1456
}
1457
if( sz<1 && (flags & ATTACHLIST_HIDE_EMPTY) ){
1458
/* Deleted or phantom items. */
1459
continue;
1460
}
1461
if( cnt==0 ){
1462
if( bUseDetail ){
1463
@ <details class='attachlist'
1464
if( ATTACHLIST_DETAILS_OPEN & flags ){
1465
@ open
1466
}
1467
@ >
1468
}else{
1469
@ <section class='attachlist'>
1470
}
1471
if( flags & ATTACHLIST_HRULE_ABOVE ){
1472
@ <hr>
1473
}
1474
if( bUseDetail ){
1475
@ <summary>%s(zHeader)</summary>
1476
}else{
1477
@ %s(zHeader)
1478
}
1479
@ <ul>
1480
}
1481
cnt++;
1482
switch( attachment_target_type(zTarget, 1) ){
1483
case CFTYPE_TICKET: zTypeArg = "tkt"; break;
1484
case CFTYPE_FORUM: zTypeArg = "forumpost"; break;
1485
case CFTYPE_EVENT: zTypeArg = "technote"; break;
1486
case CFTYPE_WIKI:
1487
default: zTypeArg = "page"; break;
1488
}
1489
@ <li>
1490
@ <a href="%R/artifact/%!S(zSrc)"%s(zLinkTgt)>%h(zFile)</a>
1491
if( flags & ATTACHLIST_SIZE ){
1492
sqlite3_snprintf(sizeof(szBuf), szBuf, " %d bytes", sz);
1493
}
1494
@ [<a href="%R/attachdownload/%t(zFile)?%s(zTypeArg)=%t(zTarget)\
1495
@&file=%t(zFile)%s(zLinkTgt)">download</a>%s(szBuf)]
1496
@ added by %h(zDispUser) on
1497
hyperlink_to_date(zDate, ".");
1498
@ [<a href="%R/ainfo/%!S(zUuid)"%s(zLinkTgt)>details</a>]
1499
moderation_pending_www(aid);
1500
@ </li>
1501
}
1502
if( cnt ){
1503
@ </ul>
1504
if( bUseDetail ){
1505
@ </details>
1506
}else{
1507
@ </section>
1508
}
1509
}
1510
db_finalize(&q);
1511
}
1512
1513
/*
1514
** COMMAND: attachment*
1515
**
1516
** Usage: %fossil attachment add ?PAGENAME? FILENAME ?OPTIONS?
1517
**
1518
** Add an attachment to an existing wiki page or tech note.
1519
**
1520
** Options:
1521
** -t|--technote DATETIME Specifies the timestamp of
1522
** the technote to which the attachment
1523
** is to be made. The attachment will be
1524
** to the most recently modified tech note
1525
** with the specified timestamp.
1526
** -t|--technote TECHNOTE-ID Specifies the technote to be
1527
** updated by its technote id
1528
**
1529
** One of PAGENAME, DATETIME or TECHNOTE-ID must be specified.
1530
**
1531
** DATETIME may be "now" or "YYYY-MM-DDTHH:MM:SS.SSS". If in
1532
** year-month-day form, it may be truncated, the "T" may be replaced by
1533
** a space, and it may also name a timezone offset from UTC as "-HH:MM"
1534
** (westward) or "+HH:MM" (eastward). Either no timezone suffix or "Z"
1535
** means UTC.
1536
*/
1537
void attachment_cmd(void){
1538
int n;
1539
db_find_and_open_repository(0, 0);
1540
if( g.argc<3 ){
1541
goto attachment_cmd_usage;
1542
}
1543
n = strlen(g.argv[2]);
1544
if( n==0 ){
1545
goto attachment_cmd_usage;
1546
}
1547
1548
if( strncmp(g.argv[2],"add",n)==0 ){
1549
const char *zPageName = 0; /* Name of the wiki page to attach to */
1550
const char *zFile; /* Name of the file to be attached */
1551
const char *zETime; /* The name of the technote to attach to */
1552
Manifest *pWiki = 0; /* Parsed wiki page content */
1553
char *zBody = 0; /* Wiki page content */
1554
int rid;
1555
const char *zTarget; /* Target of the attachment */
1556
Blob content; /* The content of the attachment */
1557
zETime = find_option("technote","t",1);
1558
if( !zETime ){
1559
if( g.argc!=5 ){
1560
usage("add PAGENAME FILENAME");
1561
}
1562
zPageName = g.argv[3];
1563
rid = db_int(0, "SELECT x.rid FROM tag t, tagxref x"
1564
" WHERE x.tagid=t.tagid AND t.tagname='wiki-%q'"
1565
" ORDER BY x.mtime DESC LIMIT 1",
1566
zPageName
1567
);
1568
if( (pWiki = manifest_get(rid, CFTYPE_WIKI, 0))!=0 ){
1569
zBody = pWiki->zWiki;
1570
}
1571
if( zBody==0 ){
1572
fossil_fatal("wiki page [%s] not found",zPageName);
1573
}
1574
zTarget = zPageName;
1575
zFile = g.argv[4];
1576
}else{
1577
if( g.argc!=4 ){
1578
usage("add FILENAME --technote DATETIME|TECHNOTE-ID");
1579
}
1580
rid = wiki_technote_to_rid(zETime);
1581
if( rid<0 ){
1582
fossil_fatal("ambiguous tech note id: %s", zETime);
1583
}
1584
if( (pWiki = manifest_get(rid, CFTYPE_EVENT, 0))!=0 ){
1585
zBody = pWiki->zWiki;
1586
}
1587
if( zBody==0 ){
1588
fossil_fatal("technote [%s] not found",zETime);
1589
}
1590
zTarget = db_text(0,
1591
"SELECT substr(tagname,7) FROM tag "
1592
" WHERE tagid=(SELECT tagid FROM event WHERE objid='%d')",
1593
rid
1594
);
1595
zFile = g.argv[3];
1596
}
1597
blob_read_from_file(&content, zFile, ExtFILE);
1598
user_select();
1599
attach_commit(
1600
zFile, /* The filename of the attachment */
1601
zTarget, /* The artifact hash to attach to */
1602
blob_buffer(&content), /* The content of the attachment */
1603
blob_size(&content), /* The length of the attachment */
1604
0, /* No need to moderate the attachment */
1605
"" /* Empty attachment comment */
1606
);
1607
if( !zETime ){
1608
fossil_print("Attached %s to wiki page %s.\n", zFile, zPageName);
1609
}else{
1610
fossil_print("Attached %s to tech note %s.\n", zFile, zETime);
1611
}
1612
}else{
1613
goto attachment_cmd_usage;
1614
}
1615
return;
1616
1617
attachment_cmd_usage:
1618
usage("add ?PAGENAME? FILENAME [-t|--technote DATETIME ]");
1619
}
1620
1621
1622
/*
1623
** COMMAND: test-list-attachments
1624
**
1625
** Usage: %fossil test-list-attachments ?-latest? ?TargetName(s)...?
1626
**
1627
** List attachments for one or more attachment targets. The target
1628
** name arguments are glob prefixes for the attachment.target
1629
** field. If no names are provided then a prefix of [a-zA-Z] is used,
1630
** which will match most wiki page names and some ticket hashes.
1631
**
1632
** Options:
1633
** -latest List only the latest version of a given attachment
1634
**
1635
*/
1636
void test_list_attachments(void){
1637
Stmt q;
1638
int i;
1639
const int fLatest = find_option("latest", 0, 0) != 0;
1640
1641
db_find_and_open_repository(0, 0);
1642
verify_all_options();
1643
db_prepare(&q,
1644
"SELECT datetime(mtime,toLocal()), src, target, filename,"
1645
" comment, user "
1646
" FROM attachment"
1647
" WHERE target GLOB :tgtname ||'*'"
1648
" AND (isLatest OR %d)"
1649
" ORDER BY target, isLatest DESC, mtime DESC",
1650
!fLatest
1651
);
1652
if(g.argc<3){
1653
static char * argv[3] = {0,0,"[a-zA-Z]"};
1654
g.argc = 3;
1655
g.argv = argv;
1656
}
1657
for(i = 2; i < g.argc; ++i){
1658
const char *zPage = g.argv[i];
1659
db_bind_text(&q, ":tgtname", zPage);
1660
while(SQLITE_ROW == db_step(&q)){
1661
const char *zTime = db_column_text(&q, 0);
1662
const char *zSrc = db_column_text(&q, 1);
1663
const char *zTarget = db_column_text(&q, 2);
1664
const char *zName = db_column_text(&q, 3);
1665
printf("%-20s %s %.12s %s\n", zTarget, zTime, zSrc, zName);
1666
}
1667
db_reset(&q);
1668
}
1669
db_finalize(&q);
1670
}
1671
1672
/*
1673
** Renders the list of attachments for artifact pManifest as JSON to
1674
** blob pOut. If pManifest->type is not one of (CFTYPE_TICKET,
1675
** CFTYPE_FORUM, CFTYPE_EVENT, CFTYPE_WIKI) then it behaves as if the
1676
** result set is empty.
1677
**
1678
** If there are no matching attachments then its behavior depends on
1679
** emptyPolicy:
1680
**
1681
** <0 = emit a JSON NULL
1682
** 0 = emit no output
1683
** >0 = emit an empty JSON array
1684
**
1685
** If bLatestOnly is true then only the most recent entry for a given
1686
** attachment is emitted, else all versions are emitted in descending
1687
** mtime order.
1688
**
1689
** Returns the number of attachments.
1690
**
1691
** Output format:
1692
**
1693
** [{
1694
** "uuid": attachment artifact hash,
1695
** "src": hash of the attachment blob,
1696
** "target": wiki page name or ticket/event ID,
1697
** "filename": filename of attachment,
1698
** "mtime": ISO-8601 timestamp UTC,
1699
** "isLatest": true if this is the latest version of this file
1700
** else false,
1701
** }, ...once per attachment]
1702
**
1703
*/
1704
int attachments_to_json(const Manifest *pManifest,
1705
Blob *pOut, int bLatestOnly,
1706
int emptyPolicy){
1707
int i = 0;
1708
Stmt q = empty_Stmt;
1709
char *zToFree = 0;
1710
const char *zTgt = 0;
1711
switch(pManifest->type){
1712
case CFTYPE_FORUM: zTgt = zToFree = rid_to_uuid(pManifest->rid);
1713
break;
1714
case CFTYPE_WIKI: zTgt = pManifest->zWikiTitle; break;
1715
case CFTYPE_EVENT: zTgt = pManifest->zEventId; break;
1716
case CFTYPE_TICKET: zTgt = pManifest->zTicketUuid; break;
1717
default:
1718
goto empty_result;
1719
}
1720
db_prepare(&q,
1721
"SELECT datetime(mtime), a.src, a.target, a.filename, a.isLatest,\n"
1722
" b2.size, b1.uuid, a.user, a.comment\n"
1723
" FROM attachment a, blob b1, blob b2\n"
1724
" WHERE a.target=%Q\n"
1725
" AND a.src IS NOT NULL\n"
1726
" AND b1.rid=a.attachid\n"
1727
" AND b2.uuid=a.src\n"
1728
" AND (a.isLatest OR %d)\n"
1729
" ORDER BY a.target, a.isLatest DESC, a.mtime DESC\n",
1730
zTgt, !bLatestOnly
1731
);
1732
while(SQLITE_ROW == db_step(&q)){
1733
const char *zTime = db_column_text(&q, 0);
1734
const char *zSrc = db_column_text(&q, 1);
1735
const char *zTarget = db_column_text(&q, 2);
1736
const char *zName = db_column_text(&q, 3);
1737
const int isLatest = db_column_int(&q, 4);
1738
const int sz = db_column_int(&q, 5);
1739
const char *zUuid = db_column_text(&q, 6);
1740
const char *zUser = db_column_text(&q, 7);
1741
const char *zComment = db_column_text(&q, 8);
1742
if(!i++){
1743
blob_append_char(pOut, '[');
1744
}else{
1745
blob_append_char(pOut, ',');
1746
}
1747
blob_appendf(
1748
pOut,
1749
"{\"uuid\": %!j, \"src\": %!j, \"target\": %!j, "
1750
"\"filename\": %!j, \"size\":%d, \"mtime\": %!j, "
1751
"\"isLatest\": %s, \"user\": %!j, \"comment\": ",
1752
zUuid, zSrc, zTarget,
1753
zName, sz, zTime, isLatest ? "true" : "false",
1754
zUser
1755
);
1756
if( zComment && zComment[0] ){
1757
blob_appendf(pOut, "%!j", zComment);
1758
}else{
1759
blob_append_literal(pOut, "null");
1760
}
1761
blob_append_char(pOut, '}');
1762
}
1763
fossil_free(zToFree);
1764
db_finalize(&q);
1765
if(!i){
1766
empty_result:
1767
if( emptyPolicy>0 ){
1768
blob_append_literal(pOut, "[]");
1769
}else if( emptyPolicy<0 ){
1770
blob_append_literal(pOut, "null");
1771
}
1772
}else{
1773
blob_append_char(pOut, ']');
1774
}
1775
return i;
1776
}
1777
1778
/*
1779
** COMMAND: test-attachment-target
1780
**
1781
** Usage: %fossil test-attachment-target TARGET_ID...
1782
*/
1783
void test_attachment_target_type_cmd(void){
1784
int i;
1785
verify_all_options();
1786
db_find_and_open_repository(0, 0);
1787
if( g.argc<3 ){
1788
usage("test-attachment-target TARGET_ID");
1789
return;
1790
}
1791
for( i = 2; i < g.argc; ++i ){
1792
const char *zTarget = g.argv[i];
1793
const int rid = attachment_target_rid(zTarget, 0);
1794
const int type = attachment_target_type(zTarget, 0);
1795
const char *zType = "<invalid>";
1796
switch(type){
1797
case CFTYPE_EVENT: zType = "technote"; break;
1798
case CFTYPE_FORUM: zType = "forumpost"; break;
1799
case CFTYPE_TICKET: zType = "ticket"; break;
1800
case CFTYPE_WIKI: zType = "wiki"; break;
1801
}
1802
fossil_print("%-20s = %-9s #%d %z\n",
1803
zTarget, zType, rid,
1804
rid>0 ? rid_to_uuid(rid) : 0);
1805
}
1806
}
1807
1808
1809
/*
1810
** COMMAND: test-attachments-to-json
1811
**
1812
** Usage: %fossil test-attachments-to-json TARGET_ID...
1813
**
1814
** Options:
1815
** --old List all versions of attachments. Default is to
1816
** list only the latest.
1817
** --full Require a full target ID, not a prefix.
1818
**
1819
** Emits a JSON array of attachments for the given attachment targets.
1820
** The given IDs must be wiki page names, ticket hashes, tech-note
1821
** hashes, or forum post hashes. By default it accepts hash prefixes
1822
** but does no detection of ambiguity or cross-type prefix collisions
1823
** so may emit curious results if given short, colliding IDs.
1824
*/
1825
void test_attachments_to_json_cmd(void){
1826
const int emptyPolicy = 1;
1827
const int bLatestOnly = find_option("old",0,0)==0;
1828
const int bFullId = find_option("full",0,0)!=0;
1829
int i;
1830
1831
verify_all_options();
1832
db_find_and_open_repository(0, 0);
1833
if( g.argc<3 ){
1834
usage("test-attachments-to-json TARGET_ID");
1835
return;
1836
}
1837
for( i = 2; i < g.argc; ++i ){
1838
const char *zTarget = g.argv[i];
1839
const int rid = attachment_target_rid(zTarget, bFullId);
1840
if( 0==rid ){
1841
fossil_print("** cannot resolve %s\n", zTarget);
1842
}else{
1843
Blob b = BLOB_INITIALIZER;
1844
Manifest *pManifest = manifest_get(rid, CFTYPE_ANY, NULL);
1845
assert( pManifest );
1846
attachments_to_json(pManifest, &b, bLatestOnly, emptyPolicy);
1847
fossil_print("Attachments for %s: ", zTarget);
1848
if( b.nUsed ){
1849
char *zPretty = db_text(0,"SELECT json_pretty(%B)", &b);
1850
fossil_print("%s\n", zPretty);
1851
fossil_free(zPretty);
1852
}else{
1853
fossil_print("none\n");
1854
}
1855
blob_reset(&b);
1856
manifest_destroy(pManifest);
1857
}
1858
}
1859
}
1860

Keyboard Shortcuts

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