Fossil SCM

Re-add CSRF check when posting attachments. Touchups in the post-attach redirection support. Fix a mem leak in attach_commit(). Make whether to show the description field an option to the attacher widget constructor and hide the description fields for now because we apparently do nothing with them.

stephan 2026-06-03 13:09 UTC attach-v2
Commit 059760519fb0a26b60b2ebe52b7645a0be264391ed31708cc1b55263a6020d8f
2 files changed +18 -32 +35 -19
+18 -32
--- src/attach.c
+++ src/attach.c
@@ -388,11 +388,11 @@
388388
if( n>0 ){
389389
blob_appendf(&manifest, "C %#F\n", n, zComment);
390390
}
391391
}
392392
zDate = date_in_standard_format("now");
393
- blob_appendf(&manifest, "D %s\n", zDate);
393
+ blob_appendf(&manifest, "D %z\n", zDate);
394394
blob_appendf(&manifest, "U %F\n", login_name());
395395
md5sum_blob(&manifest, &cksum);
396396
blob_appendf(&manifest, "Z %b\n", &cksum);
397397
attach_put(&manifest, rid, needModerator);
398398
assert( blob_is_reset(&manifest) );
@@ -424,11 +424,11 @@
424424
const char *zFrom;
425425
const char *aContent;
426426
const char *zName;
427427
const char *zComment;
428428
const char *zTarget;
429
- char * zTo = 0;
429
+ char * zTo = 0; /* Optionally redirect here after saving */
430430
char *zTargetType = 0;
431431
char *zExtraFree = 0;
432432
int szContent;
433433
int goodCaptcha = 1;
434434
int szLimit = 0;
@@ -564,11 +564,11 @@
564564
}
565565
566566
/*
567567
** WEBPAGE: attachaddV2_ajax_post hidden
568568
**
569
-** Requires a POST request with:
569
+** Used by /attachaddV2 to handle attachments via POST requests with:
570570
**
571571
** target=ATTACHMENT_TARGET
572572
** file1..fileN=FILE_OBJECTS
573573
** dryrun=0|1
574574
**
@@ -596,10 +596,13 @@
596596
597597
if( ! ajax_route_bootstrap(0, 1) ){
598598
return;
599599
}else if( !(goodCaptcha = captcha_is_correct(0)) ){
600600
goto ajax_post_403;
601
+ }else if( !cgi_csrf_safe(2) ){
602
+ ajax_route_error(403, "Invalid CSRF signature.");
603
+ return;
601604
}
602605
db_begin_transaction();
603606
zTarget = P("target");
604607
iTgtType = attachment_target_type(zTarget);
605608
switch( iTgtType ){
@@ -716,18 +719,25 @@
716719
return;
717720
}
718721
719722
/*
720723
** WEBPAGE: attachaddV2 hidden
721
-** Add a new attachment.
724
+**
725
+** Lists attachments for, and can add them to, a target artifact.
722726
**
723727
** target=TKT_HASH|WIKIPAGE_NAME|TECHNOTE_HASH|FORUMPOST_HASH
724
-** from=URL
728
+** from=ORIGINATING_URL
729
+** to=URL_ON_COMPLETION
725730
**
726731
** Works like /attachadd but uses a JS-based interactive attachment
727732
** selector.
728733
**
734
+** from=X and to=X tell it how to redirect when it's done. to=X
735
+** overrides from=X. If neither is set, it will redirect back to this
736
+** page to render the updated attachment list.
737
+**
738
+** This page requires a post-2020 JS-capable browser.
729739
*/
730740
void attachaddV2_page(void){
731741
const char *zFrom = P("from");
732742
const char *zTarget = P("target");
733743
char *zTo = 0;
@@ -734,12 +744,10 @@
734744
char *zTargetType = 0;
735745
char *zExtraFree = 0;
736746
int iTgtType = 0;
737747
int szContent = 0;
738748
int goodCaptcha = 1;
739
- int szLimit = 0;
740
- int bNeedsModeration = 0;
741749
742750
if( zFrom==0 ) zFrom = mprintf("%R/home");
743751
if( P("cancel") ) cgi_redirect(zFrom);
744752
if( 0==zTarget ){
745753
webpage_error("Requires target=X");
@@ -766,11 +774,10 @@
766774
}
767775
zTarget = zExtraFree = rid_to_uuid(fpid);
768776
zTargetType = mprintf("Forum post <a href=\"%R/forumpost/%S\">%.16h</a>",
769777
zTarget, zTarget);
770778
zTo = mprintf("%R/forumpost/%S", zTarget);
771
- bNeedsModeration = forum_need_moderation();
772779
break;
773780
}
774781
case CFTYPE_EVENT:{
775782
if( g.perm.Write==0 || g.perm.ApndWiki==0 || g.perm.Attach==0 ){
776783
login_needed(g.anon.Write && g.anon.ApndWiki && g.anon.Attach);
@@ -781,11 +788,10 @@
781788
" WHERE tagname GLOB 'event-%q*'", zTarget);
782789
if( zTarget==0) fossil_redirect_home();
783790
}
784791
zTargetType = mprintf("Tech Note <a href=\"%R/technote/%s\">%S</a>",
785792
zTarget, zTarget);
786
- bNeedsModeration = 0;
787793
break;
788794
}
789795
case CFTYPE_TICKET:{
790796
if( g.perm.ApndTkt==0 || g.perm.Attach==0 ){
791797
login_needed(g.anon.ApndTkt && g.anon.Attach);
@@ -796,11 +802,10 @@
796802
" WHERE tagname GLOB 'tkt-%q*'", zTarget);
797803
if( zTarget==0 ) fossil_redirect_home();
798804
}
799805
zTargetType = mprintf("Ticket <a href=\"%R/tktview/%s\">%S</a>",
800806
zTarget, zTarget);
801
- bNeedsModeration = ticket_need_moderation(0);
802807
break;
803808
}
804809
case CFTYPE_WIKI:{
805810
if( g.perm.ApndWiki==0 || g.perm.Attach==0 ){
806811
login_needed(g.anon.ApndWiki && g.anon.Attach);
@@ -809,37 +814,15 @@
809814
if( !db_exists("SELECT 1 FROM tag WHERE tagname='wiki-%q'", zTarget) ){
810815
fossil_redirect_home();
811816
}
812817
zTargetType = mprintf("Wiki Page <a href=\"%R/wiki?name=%h\">%h</a>",
813818
zTarget, zTarget);
814
- bNeedsModeration = wiki_need_moderation(0);
815819
break;
816820
}
817821
}
818822
819823
db_begin_transaction();
820
-#if 0
821
- szLimit = db_get_int("attachment-size-limit", 0);
822
- if( szContent<0 || (szLimit && szContent>szLimit) ){
823
- /* This check must be done late so that zTargetType is set up. */
824
- @ <p class="generalError">Attachment %h(zName) is too large.
825
- @ <a href="%R/help/attachment-size-limit">Limit</a> is
826
- @ %d(szLimit ? szLimit : 0x7fffffff) bytes</p>
827
- /* Fall through and render form. */
828
- }else if( P("ok") && szContent>0 && (goodCaptcha = captcha_is_correct(0)) ){
829
-#if 0
830
- attach_commit(zName, zTarget, aContent, szContent,
831
- bNeedsModeration, zComment);
832
-#endif
833
- cgi_redirect(zTo ? zTo : zFrom);
834
- }
835
-#else
836
- (void)bNeedsModeration;
837
- (void)szLimit;
838
- (void)szContent;
839
- (void)zTo;
840
-#endif
841824
842825
style_set_current_feature("attach");
843826
style_header("Add Attachment");
844827
if( !goodCaptcha ){
845828
@ <p class="generalError">Error: Incorrect security code.</p>
@@ -849,10 +832,13 @@
849832
ATTACHLIST_SIZE | ATTACHLIST_HIDE_UNAPPROVED);
850833
/* Form gets fleshed out and activate from fossil.attach.js. */
851834
@ <div id='attachadd-form-wrapper'>
852835
@ <input type="hidden" name="target" value="%h(zTarget)">
853836
@ <input type="hidden" name="from" value="%h(zFrom)">
837
+ if( zTo ){
838
+ @ <input type="hidden" name="to" value="%h(zTo)">
839
+ }
854840
captcha_generate(0);
855841
login_insert_csrf_secret();
856842
@ </div>
857843
builtin_fossil_js_bundle_or("attach", NULL);
858844
db_end_transaction(0);
859845
--- src/attach.c
+++ src/attach.c
@@ -388,11 +388,11 @@
388 if( n>0 ){
389 blob_appendf(&manifest, "C %#F\n", n, zComment);
390 }
391 }
392 zDate = date_in_standard_format("now");
393 blob_appendf(&manifest, "D %s\n", zDate);
394 blob_appendf(&manifest, "U %F\n", login_name());
395 md5sum_blob(&manifest, &cksum);
396 blob_appendf(&manifest, "Z %b\n", &cksum);
397 attach_put(&manifest, rid, needModerator);
398 assert( blob_is_reset(&manifest) );
@@ -424,11 +424,11 @@
424 const char *zFrom;
425 const char *aContent;
426 const char *zName;
427 const char *zComment;
428 const char *zTarget;
429 char * zTo = 0;
430 char *zTargetType = 0;
431 char *zExtraFree = 0;
432 int szContent;
433 int goodCaptcha = 1;
434 int szLimit = 0;
@@ -564,11 +564,11 @@
564 }
565
566 /*
567 ** WEBPAGE: attachaddV2_ajax_post hidden
568 **
569 ** Requires a POST request with:
570 **
571 ** target=ATTACHMENT_TARGET
572 ** file1..fileN=FILE_OBJECTS
573 ** dryrun=0|1
574 **
@@ -596,10 +596,13 @@
596
597 if( ! ajax_route_bootstrap(0, 1) ){
598 return;
599 }else if( !(goodCaptcha = captcha_is_correct(0)) ){
600 goto ajax_post_403;
 
 
 
601 }
602 db_begin_transaction();
603 zTarget = P("target");
604 iTgtType = attachment_target_type(zTarget);
605 switch( iTgtType ){
@@ -716,18 +719,25 @@
716 return;
717 }
718
719 /*
720 ** WEBPAGE: attachaddV2 hidden
721 ** Add a new attachment.
 
722 **
723 ** target=TKT_HASH|WIKIPAGE_NAME|TECHNOTE_HASH|FORUMPOST_HASH
724 ** from=URL
 
725 **
726 ** Works like /attachadd but uses a JS-based interactive attachment
727 ** selector.
728 **
 
 
 
 
 
729 */
730 void attachaddV2_page(void){
731 const char *zFrom = P("from");
732 const char *zTarget = P("target");
733 char *zTo = 0;
@@ -734,12 +744,10 @@
734 char *zTargetType = 0;
735 char *zExtraFree = 0;
736 int iTgtType = 0;
737 int szContent = 0;
738 int goodCaptcha = 1;
739 int szLimit = 0;
740 int bNeedsModeration = 0;
741
742 if( zFrom==0 ) zFrom = mprintf("%R/home");
743 if( P("cancel") ) cgi_redirect(zFrom);
744 if( 0==zTarget ){
745 webpage_error("Requires target=X");
@@ -766,11 +774,10 @@
766 }
767 zTarget = zExtraFree = rid_to_uuid(fpid);
768 zTargetType = mprintf("Forum post <a href=\"%R/forumpost/%S\">%.16h</a>",
769 zTarget, zTarget);
770 zTo = mprintf("%R/forumpost/%S", zTarget);
771 bNeedsModeration = forum_need_moderation();
772 break;
773 }
774 case CFTYPE_EVENT:{
775 if( g.perm.Write==0 || g.perm.ApndWiki==0 || g.perm.Attach==0 ){
776 login_needed(g.anon.Write && g.anon.ApndWiki && g.anon.Attach);
@@ -781,11 +788,10 @@
781 " WHERE tagname GLOB 'event-%q*'", zTarget);
782 if( zTarget==0) fossil_redirect_home();
783 }
784 zTargetType = mprintf("Tech Note <a href=\"%R/technote/%s\">%S</a>",
785 zTarget, zTarget);
786 bNeedsModeration = 0;
787 break;
788 }
789 case CFTYPE_TICKET:{
790 if( g.perm.ApndTkt==0 || g.perm.Attach==0 ){
791 login_needed(g.anon.ApndTkt && g.anon.Attach);
@@ -796,11 +802,10 @@
796 " WHERE tagname GLOB 'tkt-%q*'", zTarget);
797 if( zTarget==0 ) fossil_redirect_home();
798 }
799 zTargetType = mprintf("Ticket <a href=\"%R/tktview/%s\">%S</a>",
800 zTarget, zTarget);
801 bNeedsModeration = ticket_need_moderation(0);
802 break;
803 }
804 case CFTYPE_WIKI:{
805 if( g.perm.ApndWiki==0 || g.perm.Attach==0 ){
806 login_needed(g.anon.ApndWiki && g.anon.Attach);
@@ -809,37 +814,15 @@
809 if( !db_exists("SELECT 1 FROM tag WHERE tagname='wiki-%q'", zTarget) ){
810 fossil_redirect_home();
811 }
812 zTargetType = mprintf("Wiki Page <a href=\"%R/wiki?name=%h\">%h</a>",
813 zTarget, zTarget);
814 bNeedsModeration = wiki_need_moderation(0);
815 break;
816 }
817 }
818
819 db_begin_transaction();
820 #if 0
821 szLimit = db_get_int("attachment-size-limit", 0);
822 if( szContent<0 || (szLimit && szContent>szLimit) ){
823 /* This check must be done late so that zTargetType is set up. */
824 @ <p class="generalError">Attachment %h(zName) is too large.
825 @ <a href="%R/help/attachment-size-limit">Limit</a> is
826 @ %d(szLimit ? szLimit : 0x7fffffff) bytes</p>
827 /* Fall through and render form. */
828 }else if( P("ok") && szContent>0 && (goodCaptcha = captcha_is_correct(0)) ){
829 #if 0
830 attach_commit(zName, zTarget, aContent, szContent,
831 bNeedsModeration, zComment);
832 #endif
833 cgi_redirect(zTo ? zTo : zFrom);
834 }
835 #else
836 (void)bNeedsModeration;
837 (void)szLimit;
838 (void)szContent;
839 (void)zTo;
840 #endif
841
842 style_set_current_feature("attach");
843 style_header("Add Attachment");
844 if( !goodCaptcha ){
845 @ <p class="generalError">Error: Incorrect security code.</p>
@@ -849,10 +832,13 @@
849 ATTACHLIST_SIZE | ATTACHLIST_HIDE_UNAPPROVED);
850 /* Form gets fleshed out and activate from fossil.attach.js. */
851 @ <div id='attachadd-form-wrapper'>
852 @ <input type="hidden" name="target" value="%h(zTarget)">
853 @ <input type="hidden" name="from" value="%h(zFrom)">
 
 
 
854 captcha_generate(0);
855 login_insert_csrf_secret();
856 @ </div>
857 builtin_fossil_js_bundle_or("attach", NULL);
858 db_end_transaction(0);
859
--- src/attach.c
+++ src/attach.c
@@ -388,11 +388,11 @@
388 if( n>0 ){
389 blob_appendf(&manifest, "C %#F\n", n, zComment);
390 }
391 }
392 zDate = date_in_standard_format("now");
393 blob_appendf(&manifest, "D %z\n", zDate);
394 blob_appendf(&manifest, "U %F\n", login_name());
395 md5sum_blob(&manifest, &cksum);
396 blob_appendf(&manifest, "Z %b\n", &cksum);
397 attach_put(&manifest, rid, needModerator);
398 assert( blob_is_reset(&manifest) );
@@ -424,11 +424,11 @@
424 const char *zFrom;
425 const char *aContent;
426 const char *zName;
427 const char *zComment;
428 const char *zTarget;
429 char * zTo = 0; /* Optionally redirect here after saving */
430 char *zTargetType = 0;
431 char *zExtraFree = 0;
432 int szContent;
433 int goodCaptcha = 1;
434 int szLimit = 0;
@@ -564,11 +564,11 @@
564 }
565
566 /*
567 ** WEBPAGE: attachaddV2_ajax_post hidden
568 **
569 ** Used by /attachaddV2 to handle attachments via POST requests with:
570 **
571 ** target=ATTACHMENT_TARGET
572 ** file1..fileN=FILE_OBJECTS
573 ** dryrun=0|1
574 **
@@ -596,10 +596,13 @@
596
597 if( ! ajax_route_bootstrap(0, 1) ){
598 return;
599 }else if( !(goodCaptcha = captcha_is_correct(0)) ){
600 goto ajax_post_403;
601 }else if( !cgi_csrf_safe(2) ){
602 ajax_route_error(403, "Invalid CSRF signature.");
603 return;
604 }
605 db_begin_transaction();
606 zTarget = P("target");
607 iTgtType = attachment_target_type(zTarget);
608 switch( iTgtType ){
@@ -716,18 +719,25 @@
719 return;
720 }
721
722 /*
723 ** WEBPAGE: attachaddV2 hidden
724 **
725 ** Lists attachments for, and can add them to, a target artifact.
726 **
727 ** target=TKT_HASH|WIKIPAGE_NAME|TECHNOTE_HASH|FORUMPOST_HASH
728 ** from=ORIGINATING_URL
729 ** to=URL_ON_COMPLETION
730 **
731 ** Works like /attachadd but uses a JS-based interactive attachment
732 ** selector.
733 **
734 ** from=X and to=X tell it how to redirect when it's done. to=X
735 ** overrides from=X. If neither is set, it will redirect back to this
736 ** page to render the updated attachment list.
737 **
738 ** This page requires a post-2020 JS-capable browser.
739 */
740 void attachaddV2_page(void){
741 const char *zFrom = P("from");
742 const char *zTarget = P("target");
743 char *zTo = 0;
@@ -734,12 +744,10 @@
744 char *zTargetType = 0;
745 char *zExtraFree = 0;
746 int iTgtType = 0;
747 int szContent = 0;
748 int goodCaptcha = 1;
 
 
749
750 if( zFrom==0 ) zFrom = mprintf("%R/home");
751 if( P("cancel") ) cgi_redirect(zFrom);
752 if( 0==zTarget ){
753 webpage_error("Requires target=X");
@@ -766,11 +774,10 @@
774 }
775 zTarget = zExtraFree = rid_to_uuid(fpid);
776 zTargetType = mprintf("Forum post <a href=\"%R/forumpost/%S\">%.16h</a>",
777 zTarget, zTarget);
778 zTo = mprintf("%R/forumpost/%S", zTarget);
 
779 break;
780 }
781 case CFTYPE_EVENT:{
782 if( g.perm.Write==0 || g.perm.ApndWiki==0 || g.perm.Attach==0 ){
783 login_needed(g.anon.Write && g.anon.ApndWiki && g.anon.Attach);
@@ -781,11 +788,10 @@
788 " WHERE tagname GLOB 'event-%q*'", zTarget);
789 if( zTarget==0) fossil_redirect_home();
790 }
791 zTargetType = mprintf("Tech Note <a href=\"%R/technote/%s\">%S</a>",
792 zTarget, zTarget);
 
793 break;
794 }
795 case CFTYPE_TICKET:{
796 if( g.perm.ApndTkt==0 || g.perm.Attach==0 ){
797 login_needed(g.anon.ApndTkt && g.anon.Attach);
@@ -796,11 +802,10 @@
802 " WHERE tagname GLOB 'tkt-%q*'", zTarget);
803 if( zTarget==0 ) fossil_redirect_home();
804 }
805 zTargetType = mprintf("Ticket <a href=\"%R/tktview/%s\">%S</a>",
806 zTarget, zTarget);
 
807 break;
808 }
809 case CFTYPE_WIKI:{
810 if( g.perm.ApndWiki==0 || g.perm.Attach==0 ){
811 login_needed(g.anon.ApndWiki && g.anon.Attach);
@@ -809,37 +814,15 @@
814 if( !db_exists("SELECT 1 FROM tag WHERE tagname='wiki-%q'", zTarget) ){
815 fossil_redirect_home();
816 }
817 zTargetType = mprintf("Wiki Page <a href=\"%R/wiki?name=%h\">%h</a>",
818 zTarget, zTarget);
 
819 break;
820 }
821 }
822
823 db_begin_transaction();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
824
825 style_set_current_feature("attach");
826 style_header("Add Attachment");
827 if( !goodCaptcha ){
828 @ <p class="generalError">Error: Incorrect security code.</p>
@@ -849,10 +832,13 @@
832 ATTACHLIST_SIZE | ATTACHLIST_HIDE_UNAPPROVED);
833 /* Form gets fleshed out and activate from fossil.attach.js. */
834 @ <div id='attachadd-form-wrapper'>
835 @ <input type="hidden" name="target" value="%h(zTarget)">
836 @ <input type="hidden" name="from" value="%h(zFrom)">
837 if( zTo ){
838 @ <input type="hidden" name="to" value="%h(zTo)">
839 }
840 captcha_generate(0);
841 login_insert_csrf_secret();
842 @ </div>
843 builtin_fossil_js_bundle_or("attach", NULL);
844 db_end_transaction(0);
845
--- src/fossil.attach.js
+++ src/fossil.attach.js
@@ -36,10 +36,13 @@
3636
3737
opt.startWith[=0]: if >0 then that many file selection widgets
3838
are automatically activated, as if the user had tapped the Add
3939
button that many times. As a special case, if this is >0
4040
and the user removes the last entry, a new one is added.
41
+
42
+ opt.description[=true]: if true then show the file description
43
+ field, otherwise elide it.
4144
4245
opt.controls = [array of DOM elements]. Optional DOM elements
4346
to inject into the UI element which wraps the "Add" button.
4447
See this.controlsElement.
4548
@@ -70,11 +73,12 @@
7073
constructor(opt){
7174
this.#opt = opt = F.nu({
7275
addButtonLabel: false,
7376
startWith: 0,
7477
limit: 0,
75
- dryRun: false
78
+ dryRun: false,
79
+ description: true
7680
}, opt);
7781
this.#e.body = D.addClass(D.div(), 'attach-widget');
7882
const eBtnAdd = this.#e.btnAdd = D.addClass(
7983
D.button(this.#opt.addButtonLabel || 'Add attachment',
8084
()=>this.#addRow()),
@@ -248,15 +252,17 @@
248252
"Select/drop file or click the outer border and tap your "+
249253
"platform's conventional Paste keyboard shortcut."
250254
);
251255
const eSize = D.addClass(D.span(), 'attach-size');
252256
eInfo.append(eFilename, eSize);
253
- const eDesc = D.addClass(
254
- D.attr(D.textarea(), 'placeholder',
255
- 'Optional description...'),
256
- 'hidden', 'attach-desc'
257
- );
257
+ const eDesc = this.#opt.description
258
+ ? D.addClass(
259
+ D.attr(D.textarea(), 'placeholder',
260
+ 'Optional description...'),
261
+ 'hidden', 'attach-desc'
262
+ )
263
+ : undefined;
258264
const eRemove = D.addClass(
259265
D.button('Remove', (ev)=>{
260266
ev.stopPropagation();
261267
this.#removeRow(rowObj);
262268
}),
@@ -311,11 +317,12 @@
311317
this.#injestBlob(rowObj, blob);
312318
break;
313319
}
314320
}
315321
});
316
- D.append(eRow, eDropzone, eDesc);
322
+ eRow.append(eDropzone);
323
+ if( eDesc ) eRow.append(eDesc);
317324
rowObj.e = F.nu({
318325
dropzone: eDropzone,
319326
info: eInfo,
320327
filename: eFilename,
321328
size: eSize,
@@ -392,16 +399,22 @@
392399
rowObj.file = file;
393400
rowObj.mimeType = file.type || 'application/octet-stream';
394401
D.clearElement(rowObj.e.filename).append(file.name || 'Pasted Content');
395402
D.clearElement(rowObj.e.size).append(szLbl, ' ', rowObj.mimeType || '');
396403
rowObj.e.dropzone.classList.add('populated');
397
- rowObj.e.desc.classList.remove('hidden');
404
+ if( rowObj.e.desc ){
405
+ rowObj.e.desc.classList.remove('hidden');
406
+ }
407
+ if( rowObj.e.thumbnail ){
408
+ rowObj.e.thumbnail.remove();
409
+ rowObj.e.thumbnail = undefined;
410
+ }
398411
if( file.type?.startsWith?.('image/') || file.type==='BITMAP' ){
399412
/* Add a thumbnail */
400
- const img = rowObj.e.dropzone.querySelector('img.thumbnail') || D.img();
401
- img.classList.add('thumbnail');
413
+ const img = rowObj.e.thumbnail = D.img();
402414
rowObj.e.dropzone.insertBefore(img, rowObj.e.remove);
415
+ img.classList.add('thumbnail');
403416
const reader = new FileReader();
404417
reader.onload = (e)=>img.setAttribute('src', e.target.result);
405418
reader.readAsDataURL(file);
406419
}
407420
if( file.size>F.config.attachmentSizeLimit ){
@@ -469,12 +482,14 @@
469482
}/*Attacher*/;
470483
F.Attacher = Attacher;
471484
472485
const eFormDiv = document.querySelector('#attachadd-form-wrapper');
473486
if( eFormDiv ){
487
+ /* Inject a file-attachment form. */
474488
const urlArgs = new URLSearchParams(window.location.search);
475489
let zTarget = urlArgs.get('target');
490
+ let zTo = urlArgs.get('to') || urlArgs.get('from');
476491
const eBtnSubmit = D.button("Submit");
477492
eBtnSubmit.type = 'button';
478493
const updateBtnSubmit = (attacher)=>{
479494
if( attacher.isPopulated ){
480495
eBtnSubmit.removeAttribute('disabled');
@@ -488,11 +503,12 @@
488503
};
489504
const att = new Attacher({
490505
container: eFormDiv,
491506
startWith: 1,
492507
listener: cbAttacherChange,
493
- controls: [eBtnSubmit]
508
+ controls: [eBtnSubmit],
509
+ description: false
494510
});
495511
eBtnSubmit.addEventListener('click', async (ev)=>{
496512
att.reportError();
497513
const li = att.collectState();
498514
if( !li.length ) return;
@@ -501,16 +517,18 @@
501517
D.disable(eBtnSubmit);
502518
const fd = new FormData();
503519
let i = 0;
504520
for(const row of li){
505521
++i;
506
- fd.append(`file${i}`, row.content);
507
- if( row.description ) fd.append(`file${i}_desc`, row.description);
522
+ fd.append('file'+i, row.content);
523
+ if( row.description ) fd.append('file'+i+'_desc', row.description);
508524
}
509525
for( const eIn of eFormDiv.querySelectorAll(':scope > input[type="hidden"]') ){
510526
if( eIn.name==='target' ){
511527
zTarget = eIn.value;
528
+ }else if( eIn.name==='to' || (eIn.name==='from' && !zTo) ){
529
+ zTo = eIn.value;
512530
}
513531
fd.append(eIn.name, eIn.value)
514532
}
515533
if( att.isDryRun ){
516534
fd.append('dryrun', '1');
@@ -528,23 +546,21 @@
528546
if( jr?.error || !resp.ok ){
529547
const msg = err ? err.message : (jr?.error || resp.statusText);
530548
att.reportError("Attaching failed: ", msg);
531549
}else{
532550
att.clear();
533
- const to = urlArgs.get('to') || urlArgs.get('from') || jr?.redirect;
551
+ let to = zTo || jr?.redirect;
534552
if( to ){
535
- if( '/'===to[0] ){
536
- to = F.repoUrl(to.substr(1));
553
+ if( '/'!==to[0] ){
554
+ to = F.repoUrl(to);
537555
}
538556
window.location = to;
539557
}else{
540
- const tgt = '?target='+zTarget+'&cacheBuster='+Date.now();
541
- //console.error("FIXME: location=",tgt);
542
- window.location = tgt;
558
+ window.location = '?target='+zTarget+'&'+Date.now();
543559
}
544560
}
545561
})/*submit handler*/;
546562
updateBtnSubmit(att);
547563
F.page.attacher = att /* only for testing via dev console */;
548564
}/* /attachaddV2 */
549565
550566
})(window.fossil);
551567
--- src/fossil.attach.js
+++ src/fossil.attach.js
@@ -36,10 +36,13 @@
36
37 opt.startWith[=0]: if >0 then that many file selection widgets
38 are automatically activated, as if the user had tapped the Add
39 button that many times. As a special case, if this is >0
40 and the user removes the last entry, a new one is added.
 
 
 
41
42 opt.controls = [array of DOM elements]. Optional DOM elements
43 to inject into the UI element which wraps the "Add" button.
44 See this.controlsElement.
45
@@ -70,11 +73,12 @@
70 constructor(opt){
71 this.#opt = opt = F.nu({
72 addButtonLabel: false,
73 startWith: 0,
74 limit: 0,
75 dryRun: false
 
76 }, opt);
77 this.#e.body = D.addClass(D.div(), 'attach-widget');
78 const eBtnAdd = this.#e.btnAdd = D.addClass(
79 D.button(this.#opt.addButtonLabel || 'Add attachment',
80 ()=>this.#addRow()),
@@ -248,15 +252,17 @@
248 "Select/drop file or click the outer border and tap your "+
249 "platform's conventional Paste keyboard shortcut."
250 );
251 const eSize = D.addClass(D.span(), 'attach-size');
252 eInfo.append(eFilename, eSize);
253 const eDesc = D.addClass(
254 D.attr(D.textarea(), 'placeholder',
255 'Optional description...'),
256 'hidden', 'attach-desc'
257 );
 
 
258 const eRemove = D.addClass(
259 D.button('Remove', (ev)=>{
260 ev.stopPropagation();
261 this.#removeRow(rowObj);
262 }),
@@ -311,11 +317,12 @@
311 this.#injestBlob(rowObj, blob);
312 break;
313 }
314 }
315 });
316 D.append(eRow, eDropzone, eDesc);
 
317 rowObj.e = F.nu({
318 dropzone: eDropzone,
319 info: eInfo,
320 filename: eFilename,
321 size: eSize,
@@ -392,16 +399,22 @@
392 rowObj.file = file;
393 rowObj.mimeType = file.type || 'application/octet-stream';
394 D.clearElement(rowObj.e.filename).append(file.name || 'Pasted Content');
395 D.clearElement(rowObj.e.size).append(szLbl, ' ', rowObj.mimeType || '');
396 rowObj.e.dropzone.classList.add('populated');
397 rowObj.e.desc.classList.remove('hidden');
 
 
 
 
 
 
398 if( file.type?.startsWith?.('image/') || file.type==='BITMAP' ){
399 /* Add a thumbnail */
400 const img = rowObj.e.dropzone.querySelector('img.thumbnail') || D.img();
401 img.classList.add('thumbnail');
402 rowObj.e.dropzone.insertBefore(img, rowObj.e.remove);
 
403 const reader = new FileReader();
404 reader.onload = (e)=>img.setAttribute('src', e.target.result);
405 reader.readAsDataURL(file);
406 }
407 if( file.size>F.config.attachmentSizeLimit ){
@@ -469,12 +482,14 @@
469 }/*Attacher*/;
470 F.Attacher = Attacher;
471
472 const eFormDiv = document.querySelector('#attachadd-form-wrapper');
473 if( eFormDiv ){
 
474 const urlArgs = new URLSearchParams(window.location.search);
475 let zTarget = urlArgs.get('target');
 
476 const eBtnSubmit = D.button("Submit");
477 eBtnSubmit.type = 'button';
478 const updateBtnSubmit = (attacher)=>{
479 if( attacher.isPopulated ){
480 eBtnSubmit.removeAttribute('disabled');
@@ -488,11 +503,12 @@
488 };
489 const att = new Attacher({
490 container: eFormDiv,
491 startWith: 1,
492 listener: cbAttacherChange,
493 controls: [eBtnSubmit]
 
494 });
495 eBtnSubmit.addEventListener('click', async (ev)=>{
496 att.reportError();
497 const li = att.collectState();
498 if( !li.length ) return;
@@ -501,16 +517,18 @@
501 D.disable(eBtnSubmit);
502 const fd = new FormData();
503 let i = 0;
504 for(const row of li){
505 ++i;
506 fd.append(`file${i}`, row.content);
507 if( row.description ) fd.append(`file${i}_desc`, row.description);
508 }
509 for( const eIn of eFormDiv.querySelectorAll(':scope > input[type="hidden"]') ){
510 if( eIn.name==='target' ){
511 zTarget = eIn.value;
 
 
512 }
513 fd.append(eIn.name, eIn.value)
514 }
515 if( att.isDryRun ){
516 fd.append('dryrun', '1');
@@ -528,23 +546,21 @@
528 if( jr?.error || !resp.ok ){
529 const msg = err ? err.message : (jr?.error || resp.statusText);
530 att.reportError("Attaching failed: ", msg);
531 }else{
532 att.clear();
533 const to = urlArgs.get('to') || urlArgs.get('from') || jr?.redirect;
534 if( to ){
535 if( '/'===to[0] ){
536 to = F.repoUrl(to.substr(1));
537 }
538 window.location = to;
539 }else{
540 const tgt = '?target='+zTarget+'&cacheBuster='+Date.now();
541 //console.error("FIXME: location=",tgt);
542 window.location = tgt;
543 }
544 }
545 })/*submit handler*/;
546 updateBtnSubmit(att);
547 F.page.attacher = att /* only for testing via dev console */;
548 }/* /attachaddV2 */
549
550 })(window.fossil);
551
--- src/fossil.attach.js
+++ src/fossil.attach.js
@@ -36,10 +36,13 @@
36
37 opt.startWith[=0]: if >0 then that many file selection widgets
38 are automatically activated, as if the user had tapped the Add
39 button that many times. As a special case, if this is >0
40 and the user removes the last entry, a new one is added.
41
42 opt.description[=true]: if true then show the file description
43 field, otherwise elide it.
44
45 opt.controls = [array of DOM elements]. Optional DOM elements
46 to inject into the UI element which wraps the "Add" button.
47 See this.controlsElement.
48
@@ -70,11 +73,12 @@
73 constructor(opt){
74 this.#opt = opt = F.nu({
75 addButtonLabel: false,
76 startWith: 0,
77 limit: 0,
78 dryRun: false,
79 description: true
80 }, opt);
81 this.#e.body = D.addClass(D.div(), 'attach-widget');
82 const eBtnAdd = this.#e.btnAdd = D.addClass(
83 D.button(this.#opt.addButtonLabel || 'Add attachment',
84 ()=>this.#addRow()),
@@ -248,15 +252,17 @@
252 "Select/drop file or click the outer border and tap your "+
253 "platform's conventional Paste keyboard shortcut."
254 );
255 const eSize = D.addClass(D.span(), 'attach-size');
256 eInfo.append(eFilename, eSize);
257 const eDesc = this.#opt.description
258 ? D.addClass(
259 D.attr(D.textarea(), 'placeholder',
260 'Optional description...'),
261 'hidden', 'attach-desc'
262 )
263 : undefined;
264 const eRemove = D.addClass(
265 D.button('Remove', (ev)=>{
266 ev.stopPropagation();
267 this.#removeRow(rowObj);
268 }),
@@ -311,11 +317,12 @@
317 this.#injestBlob(rowObj, blob);
318 break;
319 }
320 }
321 });
322 eRow.append(eDropzone);
323 if( eDesc ) eRow.append(eDesc);
324 rowObj.e = F.nu({
325 dropzone: eDropzone,
326 info: eInfo,
327 filename: eFilename,
328 size: eSize,
@@ -392,16 +399,22 @@
399 rowObj.file = file;
400 rowObj.mimeType = file.type || 'application/octet-stream';
401 D.clearElement(rowObj.e.filename).append(file.name || 'Pasted Content');
402 D.clearElement(rowObj.e.size).append(szLbl, ' ', rowObj.mimeType || '');
403 rowObj.e.dropzone.classList.add('populated');
404 if( rowObj.e.desc ){
405 rowObj.e.desc.classList.remove('hidden');
406 }
407 if( rowObj.e.thumbnail ){
408 rowObj.e.thumbnail.remove();
409 rowObj.e.thumbnail = undefined;
410 }
411 if( file.type?.startsWith?.('image/') || file.type==='BITMAP' ){
412 /* Add a thumbnail */
413 const img = rowObj.e.thumbnail = D.img();
 
414 rowObj.e.dropzone.insertBefore(img, rowObj.e.remove);
415 img.classList.add('thumbnail');
416 const reader = new FileReader();
417 reader.onload = (e)=>img.setAttribute('src', e.target.result);
418 reader.readAsDataURL(file);
419 }
420 if( file.size>F.config.attachmentSizeLimit ){
@@ -469,12 +482,14 @@
482 }/*Attacher*/;
483 F.Attacher = Attacher;
484
485 const eFormDiv = document.querySelector('#attachadd-form-wrapper');
486 if( eFormDiv ){
487 /* Inject a file-attachment form. */
488 const urlArgs = new URLSearchParams(window.location.search);
489 let zTarget = urlArgs.get('target');
490 let zTo = urlArgs.get('to') || urlArgs.get('from');
491 const eBtnSubmit = D.button("Submit");
492 eBtnSubmit.type = 'button';
493 const updateBtnSubmit = (attacher)=>{
494 if( attacher.isPopulated ){
495 eBtnSubmit.removeAttribute('disabled');
@@ -488,11 +503,12 @@
503 };
504 const att = new Attacher({
505 container: eFormDiv,
506 startWith: 1,
507 listener: cbAttacherChange,
508 controls: [eBtnSubmit],
509 description: false
510 });
511 eBtnSubmit.addEventListener('click', async (ev)=>{
512 att.reportError();
513 const li = att.collectState();
514 if( !li.length ) return;
@@ -501,16 +517,18 @@
517 D.disable(eBtnSubmit);
518 const fd = new FormData();
519 let i = 0;
520 for(const row of li){
521 ++i;
522 fd.append('file'+i, row.content);
523 if( row.description ) fd.append('file'+i+'_desc', row.description);
524 }
525 for( const eIn of eFormDiv.querySelectorAll(':scope > input[type="hidden"]') ){
526 if( eIn.name==='target' ){
527 zTarget = eIn.value;
528 }else if( eIn.name==='to' || (eIn.name==='from' && !zTo) ){
529 zTo = eIn.value;
530 }
531 fd.append(eIn.name, eIn.value)
532 }
533 if( att.isDryRun ){
534 fd.append('dryrun', '1');
@@ -528,23 +546,21 @@
546 if( jr?.error || !resp.ok ){
547 const msg = err ? err.message : (jr?.error || resp.statusText);
548 att.reportError("Attaching failed: ", msg);
549 }else{
550 att.clear();
551 let to = zTo || jr?.redirect;
552 if( to ){
553 if( '/'!==to[0] ){
554 to = F.repoUrl(to);
555 }
556 window.location = to;
557 }else{
558 window.location = '?target='+zTarget+'&'+Date.now();
 
 
559 }
560 }
561 })/*submit handler*/;
562 updateBtnSubmit(att);
563 F.page.attacher = att /* only for testing via dev console */;
564 }/* /attachaddV2 */
565
566 })(window.fossil);
567

Keyboard Shortcuts

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