Fossil SCM

Get responses editing properly in the new editor. Fix editor-submitted attachments to target the oldest version of a post rather than the current, else the attachments "get lost" from other views. Remove some duplicted code.

stephan 2026-06-08 09:25 UTC forum-editor-2026
Commit 72d79e42847675fd02c341fb09dd1e8d347c18f9d841f8a9670cba26a9727c7c
--- src/attach.c
+++ src/attach.c
@@ -902,10 +902,15 @@
902902
** setting is >0 then each file's size must be <= that.
903903
**
904904
** If this returns a negative value, it will have populated an error
905905
** response using ajax_route_error(). On success it produces no
906906
** output.
907
+**
908
+** ACHTUNG: if zTarget is a forum post, it "really should" be the ID
909
+** of the first version of that post, as that's where attachments are
910
+** intended to be applied so that they can be found and removed
911
+** consistently.
907912
*/
908913
int attachments_ajax_from_POST(const char *zTarget, int bNeedsModeration){
909914
int i;
910915
int rc = 0;
911916
int n = 0;
@@ -912,10 +917,11 @@
912917
int szLimit; /* attachment-max-size setting */
913918
char aKeyPrefix[20]; /* Buffer for key "file%d" */
914919
char aKeySize[30]; /* Buffer for key "file%d:bytes" */
915920
char aKeyName[30]; /* Buffer for key "file%d:filename" */
916921
char aKeyDesc[30]; /* Buffer for key "file%d_desc" */
922
+
917923
db_begin_transaction();
918924
szLimit = db_get_int("attachment-size-limit", 0);
919925
for(i = 1; ; ++i, ++n){
920926
/* Look for P("fileN"), where N=1..n */
921927
const char *zContent;
922928
--- src/attach.c
+++ src/attach.c
@@ -902,10 +902,15 @@
902 ** setting is >0 then each file's size must be <= that.
903 **
904 ** If this returns a negative value, it will have populated an error
905 ** response using ajax_route_error(). On success it produces no
906 ** output.
 
 
 
 
 
907 */
908 int attachments_ajax_from_POST(const char *zTarget, int bNeedsModeration){
909 int i;
910 int rc = 0;
911 int n = 0;
@@ -912,10 +917,11 @@
912 int szLimit; /* attachment-max-size setting */
913 char aKeyPrefix[20]; /* Buffer for key "file%d" */
914 char aKeySize[30]; /* Buffer for key "file%d:bytes" */
915 char aKeyName[30]; /* Buffer for key "file%d:filename" */
916 char aKeyDesc[30]; /* Buffer for key "file%d_desc" */
 
917 db_begin_transaction();
918 szLimit = db_get_int("attachment-size-limit", 0);
919 for(i = 1; ; ++i, ++n){
920 /* Look for P("fileN"), where N=1..n */
921 const char *zContent;
922
--- src/attach.c
+++ src/attach.c
@@ -902,10 +902,15 @@
902 ** setting is >0 then each file's size must be <= that.
903 **
904 ** If this returns a negative value, it will have populated an error
905 ** response using ajax_route_error(). On success it produces no
906 ** output.
907 **
908 ** ACHTUNG: if zTarget is a forum post, it "really should" be the ID
909 ** of the first version of that post, as that's where attachments are
910 ** intended to be applied so that they can be found and removed
911 ** consistently.
912 */
913 int attachments_ajax_from_POST(const char *zTarget, int bNeedsModeration){
914 int i;
915 int rc = 0;
916 int n = 0;
@@ -912,10 +917,11 @@
917 int szLimit; /* attachment-max-size setting */
918 char aKeyPrefix[20]; /* Buffer for key "file%d" */
919 char aKeySize[30]; /* Buffer for key "file%d:bytes" */
920 char aKeyName[30]; /* Buffer for key "file%d:filename" */
921 char aKeyDesc[30]; /* Buffer for key "file%d_desc" */
922
923 db_begin_transaction();
924 szLimit = db_get_int("attachment-size-limit", 0);
925 for(i = 1; ; ++i, ++n){
926 /* Look for P("fileN"), where N=1..n */
927 const char *zContent;
928
+82 -44
--- src/forum.c
+++ src/forum.c
@@ -1124,11 +1124,18 @@
11241124
@ %s(iClosed ? " forumClosed" : "")\
11251125
@ %s(p->pEditTail ? " forumObs" : "")' \
11261126
if( iIndent && iIndentScale ){
11271127
@ style='margin-left:%d(iIndent*iIndentScale)ex;' \
11281128
}
1129
- @ data-fpid="%s(p->zUuid)">
1129
+ /* These data-X fields are used by the JS editor. */
1130
+ if( p->pIrt ){
1131
+ @ data-firt="%s(p->pIrt->zUuid)" \
1132
+ }
1133
+ if( p->pEditHead ){
1134
+ @ data-fedithead="%s(p->pEditHead->zUuid)" \
1135
+ }
1136
+ @ data-fpid="%s(p->zUuid)">\
11301137
11311138
/* If this is the first post (or an edit thereof), emit the thread title. */
11321139
if( pManifest->zThreadTitle ){
11331140
@ <h1>%h(pManifest->zThreadTitle)</h1>
11341141
}
@@ -1647,19 +1654,10 @@
16471654
if( g.perm.WrTForum ) return 0;
16481655
if( g.perm.ModForum ) return 0;
16491656
return 1;
16501657
}
16511658
1652
-/*
1653
-** Return true if the string is white-space only.
1654
-*/
1655
-static int whitespace_only(const char *z){
1656
- if( z==0 ) return 1;
1657
- while( z[0] && fossil_isspace(z[0]) ){ z++; }
1658
- return z[0]==0;
1659
-}
1660
-
16611659
/* Flags for use with forum_post() */
16621660
#define FPOST_NO_ALERT 1 /* do not send any alerts */
16631661
#define FPOST_DRYRUN 2 /* do not save the artifact */
16641662
16651663
/*
@@ -1703,11 +1701,11 @@
17031701
if( !g.perm.Admin && (iEdit || iInReplyTo)
17041702
&& forum_rid_is_tagged(iEdit ? iEdit : iInReplyTo, "closed", 1) ){
17051703
forumpost_error_closed();
17061704
return 0;
17071705
}
1708
- if( iEdit==0 && whitespace_only(zContent) ){
1706
+ if( iEdit==0 && fossil_all_whitespace(zContent) ){
17091707
return 0;
17101708
}
17111709
if( iInReplyTo==0 && iEdit>0 ){
17121710
iBasis = iEdit;
17131711
iInReplyTo = db_int(0, "SELECT firt FROM forumpost WHERE fpid=%d", iEdit);
@@ -1988,11 +1986,11 @@
19881986
@ it.</div>
19891987
}
19901988
}
19911989
19921990
/*
1993
-** WEBPAGE: forume1
1991
+** WEBPAGE: forume1 hidden
19941992
**
19951993
** Start a new forum thread.
19961994
*/
19971995
void forumnew_page(void){
19981996
const char *zTitle = PDT("title","");
@@ -2007,11 +2005,11 @@
20072005
}
20082006
if( P("submit") && cgi_csrf_safe(2) ){
20092007
if( forum_post(zTitle, 0, 0, 0, zMimetype, zContent,
20102008
forum_post_flags()) ) return;
20112009
}
2012
- if( P("preview") && !whitespace_only(zContent) ){
2010
+ if( P("preview") && !fossil_all_whitespace(zContent) ){
20132011
@ <h1>Preview:</h1>
20142012
forum_render(zTitle, zMimetype, zContent, "forumEdit", 1);
20152013
}
20162014
style_set_current_feature("forum");
20172015
style_header("New Forum Thread");
@@ -2023,11 +2021,11 @@
20232021
@ <h1>New Thread:</h1>
20242022
forum_from_line();
20252023
forum_post_widget(zTitle, zMimetype, zContent);
20262024
@ <input type="submit" name="preview" value="Preview">
20272025
@ <input type="hidden" name="legacy" value="1">
2028
- if( P("preview") && !whitespace_only(zContent) ){
2026
+ if( P("preview") && !fossil_all_whitespace(zContent) ){
20292027
@ <input type="submit" name="submit" value="Submit">
20302028
}else{
20312029
@ <input type="submit" name="submit" value="Submit" disabled>
20322030
}
20332031
forum_render_debug_options();
@@ -2048,11 +2046,11 @@
20482046
forum_emit_js();
20492047
style_finish_page();
20502048
}
20512049
20522050
/*
2053
-** WEBPAGE: forume2
2051
+** WEBPAGE: forume2 hidden
20542052
**
20552053
** Edit an existing forum message.
20562054
** Query parameters:
20572055
**
20582056
** fpid=X Hash of the post to be edited. REQUIRED
@@ -2143,11 +2141,11 @@
21432141
style_set_current_feature("forum");
21442142
isDelete = P("nullout")!=0;
21452143
if( P("submit")
21462144
&& isCsrfSafe
21472145
&& (zContent = PDT("content",""))!=0
2148
- && (!whitespace_only(zContent) || isDelete)
2146
+ && (isDelete || !fossil_all_whitespace(zContent))
21492147
){
21502148
int done = 1;
21512149
const char *zMimetype = PD("mimetype",DEFAULT_FORUM_MIMETYPE);
21522150
if( bReply ){
21532151
done = forum_post(0, fpid, 0, 0, zMimetype, zContent,
@@ -2221,11 +2219,11 @@
22212219
zDisplayName = display_name_from_login(pPost->zUser);
22222220
@ <h3 class='forumPostHdr'>By %s(zDisplayName) on %h(zDate)</h3>
22232221
fossil_free(zDisplayName);
22242222
fossil_free(zDate);
22252223
forum_render(0, pPost->zMimetype, pPost->zWiki, "forumEdit", 1);
2226
- if( bPreview && !whitespace_only(zContent) ){
2224
+ if( bPreview && !fossil_all_whitespace(zContent) ){
22272225
@ <h2>Preview:</h2>
22282226
forum_render(0, zMimetype,zContent, "forumEdit", 1);
22292227
}
22302228
@ <h2>Enter Reply:</h2>
22312229
@ <form action="%R/forume2" method="POST">
@@ -2237,11 +2235,11 @@
22372235
if( !isDelete ){
22382236
@ <input type="submit" name="preview" value="Preview">
22392237
}
22402238
@ <input type="hidden" name="legacy" value="1">
22412239
@ <input type="submit" name="cancel" value="Cancel">
2242
- if( (bPreview && !whitespace_only(zContent)) || isDelete ){
2240
+ if( isDelete || (bPreview && !fossil_all_whitespace(zContent)) ){
22432241
if( !iClosed || g.perm.Admin ) {
22442242
@ <input type="submit" name="submit" value="Submit">
22452243
}
22462244
}
22472245
forum_render_debug_options();
@@ -2807,16 +2805,26 @@
28072805
**
28082806
** Returns the new artifact's RID on success, 0 if no changes were
28092807
** necessary (e.g. an empty new post or dry-run mode), and a negative
28102808
** value on error. If it returns a negative value then it will have
28112809
** populated the ajax response state with an error object.
2810
+**
2811
+** zTitle must be NULL if iInReplyTo>0 and must be non-empty if
2812
+** iInReplyTo==0.
28122813
**
28132814
** The caller must have started a transaction and must roll it back if
28142815
** this call returns <=0, noting that only the negative-value case is
28152816
** an error.
2817
+**
2818
+** Maintenance reminders:
2819
+**
2820
+** - iInReplyTo==0 && iEdit==0: new thread
2821
+** - iInReplyTo==0 && iEdit>0 : edit top post or response
2822
+** - iInReplyTo>0 && iEdit==0: new response
2823
+** - iInReplyTo>0 && iEdit>0 : edit response
28162824
*/
2817
-int forum_post_ajax(
2825
+static int forum_post_ajax(
28182826
const char *zTitle, /* Title. NULL for replies */
28192827
int iInReplyTo, /* Post replying to. 0 for new threads */
28202828
int iEdit, /* Post being edited, or zero for a new post */
28212829
const char *zUser, /* Username. NULL means use login name */
28222830
const char *zMimetype, /* Mimetype of content. */
@@ -2834,18 +2842,18 @@
28342842
int nContent = zContent ? (int)strlen(zContent) : 0;
28352843
int rc = 0;
28362844
28372845
assert( db_transaction_nesting_depth()>0 );
28382846
schema_forum();
2839
- if( iEdit==0 && whitespace_only(zContent) ){
2847
+ if( iEdit==0 && fossil_all_whitespace(zContent) ){
28402848
return 0;
28412849
}
28422850
if( !g.perm.Admin && (iEdit || iInReplyTo)
28432851
&& forum_rid_is_tagged(iEdit ? iEdit : iInReplyTo, "closed", 1) ){
28442852
return -ajax_route_error(400, "Thread is closed.");
28452853
}
2846
- if( 0==iInReplyTo && whitespace_only(zTitle) ){
2854
+ if( 0==iInReplyTo && fossil_all_whitespace(zTitle) ){
28472855
return -ajax_route_error(400, "Empty title is not permitted.");
28482856
}
28492857
28502858
if( zUser==0 ){
28512859
if( login_is_nobody() ){
@@ -2865,11 +2873,16 @@
28652873
iBasis = iEdit;
28662874
iInReplyTo = db_int(0, "SELECT firt FROM forumpost WHERE fpid=%d",
28672875
iEdit);
28682876
}else{
28692877
iBasis = iInReplyTo;
2878
+ /* TODO (2026-06-008) If (iInReplyTo>0 && iEdit>0), validate that
2879
+ ** iInReplyTo is connected to iEdit properly, else we risk
2880
+ ** reparenting the new edit and having unrepredictable downstream
2881
+ ** side effects. */
28702882
}
2883
+ if( 0!=zTitle && 0==zTitle[0] ) zTitle = 0;
28712884
webpage_assert( (zTitle==0)+(iInReplyTo==0)==1 );
28722885
blob_init(&x, 0, 0);
28732886
blob_appendf(&x, "D %z\n", date_in_standard_format("now"));
28742887
zG = db_text(
28752888
0,
@@ -2960,18 +2973,19 @@
29602973
const char *zIrt;
29612974
const char *zMimetype;
29622975
const char *zContent;
29632976
const char *zStatus;
29642977
const int bHasAttachment = P("file1")!=0;
2978
+ Manifest *pPost = 0;
29652979
char *zNewUuid = 0;
29662980
int goodCaptcha = 1;
2967
- int iIrt = 0; /* In-reply-to rid or 0 */
2968
- int iEditRid = 0; /* Post rid being edited or 0 */
2969
- int rc = 0;
2970
- int nrid = 0;
2971
- int iPostFlags;
2972
- int bRollback = 1; /* True = roll back. */
2981
+ int firt = 0; /* In-reply-to rid or 0 */
2982
+ int fpid = 0; /* Post rid being edited or 0 */
2983
+ int rc = 0; /* Result code. */
2984
+ int nrid = 0; /* New artifact rid. */
2985
+ int iPostFlags; /* forum_post_flags() (after perms check) */
2986
+ int bRollback; /* True = roll back. */
29732987
29742988
if( !ajax_route_bootstrap(0, 1) ){
29752989
return;
29762990
}else if( !g.perm.WrForum
29772991
|| (bHasAttachment && !g.perm.AttachForum) ){
@@ -2983,35 +2997,48 @@
29832997
}else if( 0==(goodCaptcha = captcha_is_correct(0)) ){
29842998
ajax_route_error_captcha();
29852999
return;
29863000
}
29873001
2988
- iPostFlags = forum_post_flags();
3002
+ iPostFlags = forum_post_flags(/*must come after permissions init*/);
29893003
bRollback = (FPOST_DRYRUN & iPostFlags);
29903004
zFpid = P("fpid");
2991
- zTitle = P("title");
29923005
zIrt = P("firt");
29933006
zMimetype = P("mimetype");
29943007
zContent = P("content");
29953008
zStatus = P("status");
29963009
db_begin_transaction();
2997
- if( zFpid ){
2998
- iEditRid = symbolic_name_to_rid(zFpid, "f");
2999
- if( iEditRid<0 ){
3010
+ if( zFpid && zFpid[0] ){
3011
+ fpid = symbolic_name_to_rid(zFpid, "f");
3012
+ if( fpid<0 ){
30003013
rc = -ajax_route_error(400, "Ambiguous forum ID.");
30013014
goto ajax_save_end;
3002
- }else if( 0==iEditRid ){
3015
+ }else if( 0==fpid
3016
+ || 0==(pPost = manifest_get(fpid, CFTYPE_FORUM, 0)) ){
30033017
rc = -ajax_route_error(404, "Cannot resolve forum post ID.");
30043018
goto ajax_save_end;
30053019
}
30063020
}
3007
- if( zIrt ){
3008
- iIrt = symbolic_name_to_rid(zIrt, "f");
3009
- if( iIrt<0 ){
3021
+ /*
3022
+ ** Problem: if we derive firt from fpid/pPost then there's a race
3023
+ ** condition where the IRT post is edited between the time that this
3024
+ ** edit was initiated and when it is posted: the new edit's IRT will
3025
+ ** point to the edit which was made in the meantime, not the one the
3026
+ ** user intended to respond to. However, if we accept firt from the
3027
+ ** enviornment, we "really should" validate that it's actually in
3028
+ ** the current chain, to prohibit that malicious posts could move
3029
+ ** posts around.
3030
+ **
3031
+ ** forum_post_ajax() will, if fpid>0 && !firt, select fpid's current
3032
+ ** firt.
3033
+ */
3034
+ if( zIrt && zIrt[0] ){
3035
+ firt = symbolic_name_to_rid(zIrt, "f");
3036
+ if( firt<0 ){
30103037
rc = -ajax_route_error(400, "Ambiguous in-reply-do ID.");
30113038
goto ajax_save_end;
3012
- }else if( 0==iIrt ){
3039
+ }else if( 0==firt ){
30133040
rc = -ajax_route_error(404, "Cannot resolve in-reply-do ID.");
30143041
goto ajax_save_end;
30153042
}
30163043
}
30173044
@@ -3020,11 +3047,12 @@
30203047
"iPostFlags=%d debug=%d",
30213048
iPostFlags, g.perm.Debug);
30223049
goto ajax_save_end;
30233050
}
30243051
3025
- nrid = forum_post_ajax(zTitle, iIrt, iEditRid, 0, zMimetype,
3052
+ zTitle = firt ? 0 : P("title");
3053
+ nrid = forum_post_ajax(zTitle, firt, fpid, 0, zMimetype,
30263054
zContent, iPostFlags);
30273055
if( nrid<0 ){
30283056
rc = nrid;
30293057
goto ajax_save_end;
30303058
}else if( nrid==0 ){
@@ -3034,21 +3062,26 @@
30343062
}else{
30353063
CX("{\"message\": \"Rolled back for dry-run.\","
30363064
"\"iPostFlags\":%d}\n", iPostFlags);
30373065
}
30383066
goto ajax_save_end;
3039
- }
3040
- if( nrid>0 ){
3067
+ }else{
3068
+ const int bNeedsModeration = forum_need_moderation();
3069
+ const int fpRoot = forumpost_head_rid(nrid);
3070
+ assert( nrid>0 );
30413071
zNewUuid = rid_to_uuid(nrid);
30423072
if( 0!=P("file1") ){
30433073
/* Attachments */
30443074
if( !g.perm.Admin && !g.perm.AttachForum ){
30453075
rc = -ajax_route_error(403, "No permission no attach files.");
30463076
goto ajax_save_end;
30473077
}else{
3078
+ char *zRoot = (nrid==fpRoot) ? 0 : rid_to_uuid(fpRoot);
30483079
const int atRc =
3049
- attachments_ajax_from_POST(zNewUuid, forum_need_moderation());
3080
+ attachments_ajax_from_POST(zRoot ? zRoot : zNewUuid,
3081
+ bNeedsModeration);
3082
+ fossil_free(zRoot);
30503083
if( atRc<0 ){
30513084
rc = atRc;
30523085
goto ajax_save_end;
30533086
}
30543087
if( atRc>0
@@ -3055,12 +3088,12 @@
30553088
&& (iPostFlags & FPOST_NO_ALERT)!=0
30563089
&& db_table_exists("repository","pending_alert") ){
30573090
/* Unqueue any alerts for these attachments. Recall that
30583091
** they're attached to the first version of the post, which
30593092
** means we actually risk cancelling _other_ pending
3060
- ** notifications for attachments on this same post. */
3061
- const int fpRoot = forumpost_head_rid(nrid);
3093
+ ** notifications for attachments on this same post. C'est la
3094
+ ** vie.*/
30623095
db_multi_exec(
30633096
"WITH x(id) AS (\n"
30643097
" SELECT 'f%d'\n"
30653098
" UNION ALL\n"
30663099
" SELECT 'f'||a.attachid FROM blob b, attachment a\n"
@@ -3070,11 +3103,15 @@
30703103
fpRoot, fpRoot
30713104
);
30723105
}
30733106
}
30743107
}
3075
- if( zStatus!=0 && zStatus[0]!=0
3108
+ if( 0==bNeedsModeration
3109
+ /* ^^^ Do not allow a status tag on a pending-moderation post
3110
+ ** because it will introduce a reference to an artifact which
3111
+ ** will become a phantom if it is rejected by a moderator. */
3112
+ && zStatus!=0 && zStatus[0]!=0
30763113
&& forum_may_set_status(nrid)
30773114
&& forumpost_tag(nrid, 1, "status", zStatus)<0 ){
30783115
rc = -ajax_route_error(500, "Tagging failed: %s", g.zErrMsg);
30793116
goto ajax_save_end;
30803117
}
@@ -3084,8 +3121,9 @@
30843121
assert( zNewUuid );
30853122
CX("{\"uuid\": %!j, \"dryrun\": %s, \"iPostFlags\":%d}\n",
30863123
zNewUuid, bRollback ? "true" : "false", iPostFlags);
30873124
30883125
ajax_save_end:
3126
+ manifest_destroy(pPost);
30893127
fossil_free(zNewUuid);
30903128
db_end_transaction(rc || bRollback);
30913129
}
30923130
--- src/forum.c
+++ src/forum.c
@@ -1124,11 +1124,18 @@
1124 @ %s(iClosed ? " forumClosed" : "")\
1125 @ %s(p->pEditTail ? " forumObs" : "")' \
1126 if( iIndent && iIndentScale ){
1127 @ style='margin-left:%d(iIndent*iIndentScale)ex;' \
1128 }
1129 @ data-fpid="%s(p->zUuid)">
 
 
 
 
 
 
 
1130
1131 /* If this is the first post (or an edit thereof), emit the thread title. */
1132 if( pManifest->zThreadTitle ){
1133 @ <h1>%h(pManifest->zThreadTitle)</h1>
1134 }
@@ -1647,19 +1654,10 @@
1647 if( g.perm.WrTForum ) return 0;
1648 if( g.perm.ModForum ) return 0;
1649 return 1;
1650 }
1651
1652 /*
1653 ** Return true if the string is white-space only.
1654 */
1655 static int whitespace_only(const char *z){
1656 if( z==0 ) return 1;
1657 while( z[0] && fossil_isspace(z[0]) ){ z++; }
1658 return z[0]==0;
1659 }
1660
1661 /* Flags for use with forum_post() */
1662 #define FPOST_NO_ALERT 1 /* do not send any alerts */
1663 #define FPOST_DRYRUN 2 /* do not save the artifact */
1664
1665 /*
@@ -1703,11 +1701,11 @@
1703 if( !g.perm.Admin && (iEdit || iInReplyTo)
1704 && forum_rid_is_tagged(iEdit ? iEdit : iInReplyTo, "closed", 1) ){
1705 forumpost_error_closed();
1706 return 0;
1707 }
1708 if( iEdit==0 && whitespace_only(zContent) ){
1709 return 0;
1710 }
1711 if( iInReplyTo==0 && iEdit>0 ){
1712 iBasis = iEdit;
1713 iInReplyTo = db_int(0, "SELECT firt FROM forumpost WHERE fpid=%d", iEdit);
@@ -1988,11 +1986,11 @@
1988 @ it.</div>
1989 }
1990 }
1991
1992 /*
1993 ** WEBPAGE: forume1
1994 **
1995 ** Start a new forum thread.
1996 */
1997 void forumnew_page(void){
1998 const char *zTitle = PDT("title","");
@@ -2007,11 +2005,11 @@
2007 }
2008 if( P("submit") && cgi_csrf_safe(2) ){
2009 if( forum_post(zTitle, 0, 0, 0, zMimetype, zContent,
2010 forum_post_flags()) ) return;
2011 }
2012 if( P("preview") && !whitespace_only(zContent) ){
2013 @ <h1>Preview:</h1>
2014 forum_render(zTitle, zMimetype, zContent, "forumEdit", 1);
2015 }
2016 style_set_current_feature("forum");
2017 style_header("New Forum Thread");
@@ -2023,11 +2021,11 @@
2023 @ <h1>New Thread:</h1>
2024 forum_from_line();
2025 forum_post_widget(zTitle, zMimetype, zContent);
2026 @ <input type="submit" name="preview" value="Preview">
2027 @ <input type="hidden" name="legacy" value="1">
2028 if( P("preview") && !whitespace_only(zContent) ){
2029 @ <input type="submit" name="submit" value="Submit">
2030 }else{
2031 @ <input type="submit" name="submit" value="Submit" disabled>
2032 }
2033 forum_render_debug_options();
@@ -2048,11 +2046,11 @@
2048 forum_emit_js();
2049 style_finish_page();
2050 }
2051
2052 /*
2053 ** WEBPAGE: forume2
2054 **
2055 ** Edit an existing forum message.
2056 ** Query parameters:
2057 **
2058 ** fpid=X Hash of the post to be edited. REQUIRED
@@ -2143,11 +2141,11 @@
2143 style_set_current_feature("forum");
2144 isDelete = P("nullout")!=0;
2145 if( P("submit")
2146 && isCsrfSafe
2147 && (zContent = PDT("content",""))!=0
2148 && (!whitespace_only(zContent) || isDelete)
2149 ){
2150 int done = 1;
2151 const char *zMimetype = PD("mimetype",DEFAULT_FORUM_MIMETYPE);
2152 if( bReply ){
2153 done = forum_post(0, fpid, 0, 0, zMimetype, zContent,
@@ -2221,11 +2219,11 @@
2221 zDisplayName = display_name_from_login(pPost->zUser);
2222 @ <h3 class='forumPostHdr'>By %s(zDisplayName) on %h(zDate)</h3>
2223 fossil_free(zDisplayName);
2224 fossil_free(zDate);
2225 forum_render(0, pPost->zMimetype, pPost->zWiki, "forumEdit", 1);
2226 if( bPreview && !whitespace_only(zContent) ){
2227 @ <h2>Preview:</h2>
2228 forum_render(0, zMimetype,zContent, "forumEdit", 1);
2229 }
2230 @ <h2>Enter Reply:</h2>
2231 @ <form action="%R/forume2" method="POST">
@@ -2237,11 +2235,11 @@
2237 if( !isDelete ){
2238 @ <input type="submit" name="preview" value="Preview">
2239 }
2240 @ <input type="hidden" name="legacy" value="1">
2241 @ <input type="submit" name="cancel" value="Cancel">
2242 if( (bPreview && !whitespace_only(zContent)) || isDelete ){
2243 if( !iClosed || g.perm.Admin ) {
2244 @ <input type="submit" name="submit" value="Submit">
2245 }
2246 }
2247 forum_render_debug_options();
@@ -2807,16 +2805,26 @@
2807 **
2808 ** Returns the new artifact's RID on success, 0 if no changes were
2809 ** necessary (e.g. an empty new post or dry-run mode), and a negative
2810 ** value on error. If it returns a negative value then it will have
2811 ** populated the ajax response state with an error object.
 
 
 
2812 **
2813 ** The caller must have started a transaction and must roll it back if
2814 ** this call returns <=0, noting that only the negative-value case is
2815 ** an error.
 
 
 
 
 
 
 
2816 */
2817 int forum_post_ajax(
2818 const char *zTitle, /* Title. NULL for replies */
2819 int iInReplyTo, /* Post replying to. 0 for new threads */
2820 int iEdit, /* Post being edited, or zero for a new post */
2821 const char *zUser, /* Username. NULL means use login name */
2822 const char *zMimetype, /* Mimetype of content. */
@@ -2834,18 +2842,18 @@
2834 int nContent = zContent ? (int)strlen(zContent) : 0;
2835 int rc = 0;
2836
2837 assert( db_transaction_nesting_depth()>0 );
2838 schema_forum();
2839 if( iEdit==0 && whitespace_only(zContent) ){
2840 return 0;
2841 }
2842 if( !g.perm.Admin && (iEdit || iInReplyTo)
2843 && forum_rid_is_tagged(iEdit ? iEdit : iInReplyTo, "closed", 1) ){
2844 return -ajax_route_error(400, "Thread is closed.");
2845 }
2846 if( 0==iInReplyTo && whitespace_only(zTitle) ){
2847 return -ajax_route_error(400, "Empty title is not permitted.");
2848 }
2849
2850 if( zUser==0 ){
2851 if( login_is_nobody() ){
@@ -2865,11 +2873,16 @@
2865 iBasis = iEdit;
2866 iInReplyTo = db_int(0, "SELECT firt FROM forumpost WHERE fpid=%d",
2867 iEdit);
2868 }else{
2869 iBasis = iInReplyTo;
 
 
 
 
2870 }
 
2871 webpage_assert( (zTitle==0)+(iInReplyTo==0)==1 );
2872 blob_init(&x, 0, 0);
2873 blob_appendf(&x, "D %z\n", date_in_standard_format("now"));
2874 zG = db_text(
2875 0,
@@ -2960,18 +2973,19 @@
2960 const char *zIrt;
2961 const char *zMimetype;
2962 const char *zContent;
2963 const char *zStatus;
2964 const int bHasAttachment = P("file1")!=0;
 
2965 char *zNewUuid = 0;
2966 int goodCaptcha = 1;
2967 int iIrt = 0; /* In-reply-to rid or 0 */
2968 int iEditRid = 0; /* Post rid being edited or 0 */
2969 int rc = 0;
2970 int nrid = 0;
2971 int iPostFlags;
2972 int bRollback = 1; /* True = roll back. */
2973
2974 if( !ajax_route_bootstrap(0, 1) ){
2975 return;
2976 }else if( !g.perm.WrForum
2977 || (bHasAttachment && !g.perm.AttachForum) ){
@@ -2983,35 +2997,48 @@
2983 }else if( 0==(goodCaptcha = captcha_is_correct(0)) ){
2984 ajax_route_error_captcha();
2985 return;
2986 }
2987
2988 iPostFlags = forum_post_flags();
2989 bRollback = (FPOST_DRYRUN & iPostFlags);
2990 zFpid = P("fpid");
2991 zTitle = P("title");
2992 zIrt = P("firt");
2993 zMimetype = P("mimetype");
2994 zContent = P("content");
2995 zStatus = P("status");
2996 db_begin_transaction();
2997 if( zFpid ){
2998 iEditRid = symbolic_name_to_rid(zFpid, "f");
2999 if( iEditRid<0 ){
3000 rc = -ajax_route_error(400, "Ambiguous forum ID.");
3001 goto ajax_save_end;
3002 }else if( 0==iEditRid ){
 
3003 rc = -ajax_route_error(404, "Cannot resolve forum post ID.");
3004 goto ajax_save_end;
3005 }
3006 }
3007 if( zIrt ){
3008 iIrt = symbolic_name_to_rid(zIrt, "f");
3009 if( iIrt<0 ){
 
 
 
 
 
 
 
 
 
 
 
 
 
3010 rc = -ajax_route_error(400, "Ambiguous in-reply-do ID.");
3011 goto ajax_save_end;
3012 }else if( 0==iIrt ){
3013 rc = -ajax_route_error(404, "Cannot resolve in-reply-do ID.");
3014 goto ajax_save_end;
3015 }
3016 }
3017
@@ -3020,11 +3047,12 @@
3020 "iPostFlags=%d debug=%d",
3021 iPostFlags, g.perm.Debug);
3022 goto ajax_save_end;
3023 }
3024
3025 nrid = forum_post_ajax(zTitle, iIrt, iEditRid, 0, zMimetype,
 
3026 zContent, iPostFlags);
3027 if( nrid<0 ){
3028 rc = nrid;
3029 goto ajax_save_end;
3030 }else if( nrid==0 ){
@@ -3034,21 +3062,26 @@
3034 }else{
3035 CX("{\"message\": \"Rolled back for dry-run.\","
3036 "\"iPostFlags\":%d}\n", iPostFlags);
3037 }
3038 goto ajax_save_end;
3039 }
3040 if( nrid>0 ){
 
 
3041 zNewUuid = rid_to_uuid(nrid);
3042 if( 0!=P("file1") ){
3043 /* Attachments */
3044 if( !g.perm.Admin && !g.perm.AttachForum ){
3045 rc = -ajax_route_error(403, "No permission no attach files.");
3046 goto ajax_save_end;
3047 }else{
 
3048 const int atRc =
3049 attachments_ajax_from_POST(zNewUuid, forum_need_moderation());
 
 
3050 if( atRc<0 ){
3051 rc = atRc;
3052 goto ajax_save_end;
3053 }
3054 if( atRc>0
@@ -3055,12 +3088,12 @@
3055 && (iPostFlags & FPOST_NO_ALERT)!=0
3056 && db_table_exists("repository","pending_alert") ){
3057 /* Unqueue any alerts for these attachments. Recall that
3058 ** they're attached to the first version of the post, which
3059 ** means we actually risk cancelling _other_ pending
3060 ** notifications for attachments on this same post. */
3061 const int fpRoot = forumpost_head_rid(nrid);
3062 db_multi_exec(
3063 "WITH x(id) AS (\n"
3064 " SELECT 'f%d'\n"
3065 " UNION ALL\n"
3066 " SELECT 'f'||a.attachid FROM blob b, attachment a\n"
@@ -3070,11 +3103,15 @@
3070 fpRoot, fpRoot
3071 );
3072 }
3073 }
3074 }
3075 if( zStatus!=0 && zStatus[0]!=0
 
 
 
 
3076 && forum_may_set_status(nrid)
3077 && forumpost_tag(nrid, 1, "status", zStatus)<0 ){
3078 rc = -ajax_route_error(500, "Tagging failed: %s", g.zErrMsg);
3079 goto ajax_save_end;
3080 }
@@ -3084,8 +3121,9 @@
3084 assert( zNewUuid );
3085 CX("{\"uuid\": %!j, \"dryrun\": %s, \"iPostFlags\":%d}\n",
3086 zNewUuid, bRollback ? "true" : "false", iPostFlags);
3087
3088 ajax_save_end:
 
3089 fossil_free(zNewUuid);
3090 db_end_transaction(rc || bRollback);
3091 }
3092
--- src/forum.c
+++ src/forum.c
@@ -1124,11 +1124,18 @@
1124 @ %s(iClosed ? " forumClosed" : "")\
1125 @ %s(p->pEditTail ? " forumObs" : "")' \
1126 if( iIndent && iIndentScale ){
1127 @ style='margin-left:%d(iIndent*iIndentScale)ex;' \
1128 }
1129 /* These data-X fields are used by the JS editor. */
1130 if( p->pIrt ){
1131 @ data-firt="%s(p->pIrt->zUuid)" \
1132 }
1133 if( p->pEditHead ){
1134 @ data-fedithead="%s(p->pEditHead->zUuid)" \
1135 }
1136 @ data-fpid="%s(p->zUuid)">\
1137
1138 /* If this is the first post (or an edit thereof), emit the thread title. */
1139 if( pManifest->zThreadTitle ){
1140 @ <h1>%h(pManifest->zThreadTitle)</h1>
1141 }
@@ -1647,19 +1654,10 @@
1654 if( g.perm.WrTForum ) return 0;
1655 if( g.perm.ModForum ) return 0;
1656 return 1;
1657 }
1658
 
 
 
 
 
 
 
 
 
1659 /* Flags for use with forum_post() */
1660 #define FPOST_NO_ALERT 1 /* do not send any alerts */
1661 #define FPOST_DRYRUN 2 /* do not save the artifact */
1662
1663 /*
@@ -1703,11 +1701,11 @@
1701 if( !g.perm.Admin && (iEdit || iInReplyTo)
1702 && forum_rid_is_tagged(iEdit ? iEdit : iInReplyTo, "closed", 1) ){
1703 forumpost_error_closed();
1704 return 0;
1705 }
1706 if( iEdit==0 && fossil_all_whitespace(zContent) ){
1707 return 0;
1708 }
1709 if( iInReplyTo==0 && iEdit>0 ){
1710 iBasis = iEdit;
1711 iInReplyTo = db_int(0, "SELECT firt FROM forumpost WHERE fpid=%d", iEdit);
@@ -1988,11 +1986,11 @@
1986 @ it.</div>
1987 }
1988 }
1989
1990 /*
1991 ** WEBPAGE: forume1 hidden
1992 **
1993 ** Start a new forum thread.
1994 */
1995 void forumnew_page(void){
1996 const char *zTitle = PDT("title","");
@@ -2007,11 +2005,11 @@
2005 }
2006 if( P("submit") && cgi_csrf_safe(2) ){
2007 if( forum_post(zTitle, 0, 0, 0, zMimetype, zContent,
2008 forum_post_flags()) ) return;
2009 }
2010 if( P("preview") && !fossil_all_whitespace(zContent) ){
2011 @ <h1>Preview:</h1>
2012 forum_render(zTitle, zMimetype, zContent, "forumEdit", 1);
2013 }
2014 style_set_current_feature("forum");
2015 style_header("New Forum Thread");
@@ -2023,11 +2021,11 @@
2021 @ <h1>New Thread:</h1>
2022 forum_from_line();
2023 forum_post_widget(zTitle, zMimetype, zContent);
2024 @ <input type="submit" name="preview" value="Preview">
2025 @ <input type="hidden" name="legacy" value="1">
2026 if( P("preview") && !fossil_all_whitespace(zContent) ){
2027 @ <input type="submit" name="submit" value="Submit">
2028 }else{
2029 @ <input type="submit" name="submit" value="Submit" disabled>
2030 }
2031 forum_render_debug_options();
@@ -2048,11 +2046,11 @@
2046 forum_emit_js();
2047 style_finish_page();
2048 }
2049
2050 /*
2051 ** WEBPAGE: forume2 hidden
2052 **
2053 ** Edit an existing forum message.
2054 ** Query parameters:
2055 **
2056 ** fpid=X Hash of the post to be edited. REQUIRED
@@ -2143,11 +2141,11 @@
2141 style_set_current_feature("forum");
2142 isDelete = P("nullout")!=0;
2143 if( P("submit")
2144 && isCsrfSafe
2145 && (zContent = PDT("content",""))!=0
2146 && (isDelete || !fossil_all_whitespace(zContent))
2147 ){
2148 int done = 1;
2149 const char *zMimetype = PD("mimetype",DEFAULT_FORUM_MIMETYPE);
2150 if( bReply ){
2151 done = forum_post(0, fpid, 0, 0, zMimetype, zContent,
@@ -2221,11 +2219,11 @@
2219 zDisplayName = display_name_from_login(pPost->zUser);
2220 @ <h3 class='forumPostHdr'>By %s(zDisplayName) on %h(zDate)</h3>
2221 fossil_free(zDisplayName);
2222 fossil_free(zDate);
2223 forum_render(0, pPost->zMimetype, pPost->zWiki, "forumEdit", 1);
2224 if( bPreview && !fossil_all_whitespace(zContent) ){
2225 @ <h2>Preview:</h2>
2226 forum_render(0, zMimetype,zContent, "forumEdit", 1);
2227 }
2228 @ <h2>Enter Reply:</h2>
2229 @ <form action="%R/forume2" method="POST">
@@ -2237,11 +2235,11 @@
2235 if( !isDelete ){
2236 @ <input type="submit" name="preview" value="Preview">
2237 }
2238 @ <input type="hidden" name="legacy" value="1">
2239 @ <input type="submit" name="cancel" value="Cancel">
2240 if( isDelete || (bPreview && !fossil_all_whitespace(zContent)) ){
2241 if( !iClosed || g.perm.Admin ) {
2242 @ <input type="submit" name="submit" value="Submit">
2243 }
2244 }
2245 forum_render_debug_options();
@@ -2807,16 +2805,26 @@
2805 **
2806 ** Returns the new artifact's RID on success, 0 if no changes were
2807 ** necessary (e.g. an empty new post or dry-run mode), and a negative
2808 ** value on error. If it returns a negative value then it will have
2809 ** populated the ajax response state with an error object.
2810 **
2811 ** zTitle must be NULL if iInReplyTo>0 and must be non-empty if
2812 ** iInReplyTo==0.
2813 **
2814 ** The caller must have started a transaction and must roll it back if
2815 ** this call returns <=0, noting that only the negative-value case is
2816 ** an error.
2817 **
2818 ** Maintenance reminders:
2819 **
2820 ** - iInReplyTo==0 && iEdit==0: new thread
2821 ** - iInReplyTo==0 && iEdit>0 : edit top post or response
2822 ** - iInReplyTo>0 && iEdit==0: new response
2823 ** - iInReplyTo>0 && iEdit>0 : edit response
2824 */
2825 static int forum_post_ajax(
2826 const char *zTitle, /* Title. NULL for replies */
2827 int iInReplyTo, /* Post replying to. 0 for new threads */
2828 int iEdit, /* Post being edited, or zero for a new post */
2829 const char *zUser, /* Username. NULL means use login name */
2830 const char *zMimetype, /* Mimetype of content. */
@@ -2834,18 +2842,18 @@
2842 int nContent = zContent ? (int)strlen(zContent) : 0;
2843 int rc = 0;
2844
2845 assert( db_transaction_nesting_depth()>0 );
2846 schema_forum();
2847 if( iEdit==0 && fossil_all_whitespace(zContent) ){
2848 return 0;
2849 }
2850 if( !g.perm.Admin && (iEdit || iInReplyTo)
2851 && forum_rid_is_tagged(iEdit ? iEdit : iInReplyTo, "closed", 1) ){
2852 return -ajax_route_error(400, "Thread is closed.");
2853 }
2854 if( 0==iInReplyTo && fossil_all_whitespace(zTitle) ){
2855 return -ajax_route_error(400, "Empty title is not permitted.");
2856 }
2857
2858 if( zUser==0 ){
2859 if( login_is_nobody() ){
@@ -2865,11 +2873,16 @@
2873 iBasis = iEdit;
2874 iInReplyTo = db_int(0, "SELECT firt FROM forumpost WHERE fpid=%d",
2875 iEdit);
2876 }else{
2877 iBasis = iInReplyTo;
2878 /* TODO (2026-06-008) If (iInReplyTo>0 && iEdit>0), validate that
2879 ** iInReplyTo is connected to iEdit properly, else we risk
2880 ** reparenting the new edit and having unrepredictable downstream
2881 ** side effects. */
2882 }
2883 if( 0!=zTitle && 0==zTitle[0] ) zTitle = 0;
2884 webpage_assert( (zTitle==0)+(iInReplyTo==0)==1 );
2885 blob_init(&x, 0, 0);
2886 blob_appendf(&x, "D %z\n", date_in_standard_format("now"));
2887 zG = db_text(
2888 0,
@@ -2960,18 +2973,19 @@
2973 const char *zIrt;
2974 const char *zMimetype;
2975 const char *zContent;
2976 const char *zStatus;
2977 const int bHasAttachment = P("file1")!=0;
2978 Manifest *pPost = 0;
2979 char *zNewUuid = 0;
2980 int goodCaptcha = 1;
2981 int firt = 0; /* In-reply-to rid or 0 */
2982 int fpid = 0; /* Post rid being edited or 0 */
2983 int rc = 0; /* Result code. */
2984 int nrid = 0; /* New artifact rid. */
2985 int iPostFlags; /* forum_post_flags() (after perms check) */
2986 int bRollback; /* True = roll back. */
2987
2988 if( !ajax_route_bootstrap(0, 1) ){
2989 return;
2990 }else if( !g.perm.WrForum
2991 || (bHasAttachment && !g.perm.AttachForum) ){
@@ -2983,35 +2997,48 @@
2997 }else if( 0==(goodCaptcha = captcha_is_correct(0)) ){
2998 ajax_route_error_captcha();
2999 return;
3000 }
3001
3002 iPostFlags = forum_post_flags(/*must come after permissions init*/);
3003 bRollback = (FPOST_DRYRUN & iPostFlags);
3004 zFpid = P("fpid");
 
3005 zIrt = P("firt");
3006 zMimetype = P("mimetype");
3007 zContent = P("content");
3008 zStatus = P("status");
3009 db_begin_transaction();
3010 if( zFpid && zFpid[0] ){
3011 fpid = symbolic_name_to_rid(zFpid, "f");
3012 if( fpid<0 ){
3013 rc = -ajax_route_error(400, "Ambiguous forum ID.");
3014 goto ajax_save_end;
3015 }else if( 0==fpid
3016 || 0==(pPost = manifest_get(fpid, CFTYPE_FORUM, 0)) ){
3017 rc = -ajax_route_error(404, "Cannot resolve forum post ID.");
3018 goto ajax_save_end;
3019 }
3020 }
3021 /*
3022 ** Problem: if we derive firt from fpid/pPost then there's a race
3023 ** condition where the IRT post is edited between the time that this
3024 ** edit was initiated and when it is posted: the new edit's IRT will
3025 ** point to the edit which was made in the meantime, not the one the
3026 ** user intended to respond to. However, if we accept firt from the
3027 ** enviornment, we "really should" validate that it's actually in
3028 ** the current chain, to prohibit that malicious posts could move
3029 ** posts around.
3030 **
3031 ** forum_post_ajax() will, if fpid>0 && !firt, select fpid's current
3032 ** firt.
3033 */
3034 if( zIrt && zIrt[0] ){
3035 firt = symbolic_name_to_rid(zIrt, "f");
3036 if( firt<0 ){
3037 rc = -ajax_route_error(400, "Ambiguous in-reply-do ID.");
3038 goto ajax_save_end;
3039 }else if( 0==firt ){
3040 rc = -ajax_route_error(404, "Cannot resolve in-reply-do ID.");
3041 goto ajax_save_end;
3042 }
3043 }
3044
@@ -3020,11 +3047,12 @@
3047 "iPostFlags=%d debug=%d",
3048 iPostFlags, g.perm.Debug);
3049 goto ajax_save_end;
3050 }
3051
3052 zTitle = firt ? 0 : P("title");
3053 nrid = forum_post_ajax(zTitle, firt, fpid, 0, zMimetype,
3054 zContent, iPostFlags);
3055 if( nrid<0 ){
3056 rc = nrid;
3057 goto ajax_save_end;
3058 }else if( nrid==0 ){
@@ -3034,21 +3062,26 @@
3062 }else{
3063 CX("{\"message\": \"Rolled back for dry-run.\","
3064 "\"iPostFlags\":%d}\n", iPostFlags);
3065 }
3066 goto ajax_save_end;
3067 }else{
3068 const int bNeedsModeration = forum_need_moderation();
3069 const int fpRoot = forumpost_head_rid(nrid);
3070 assert( nrid>0 );
3071 zNewUuid = rid_to_uuid(nrid);
3072 if( 0!=P("file1") ){
3073 /* Attachments */
3074 if( !g.perm.Admin && !g.perm.AttachForum ){
3075 rc = -ajax_route_error(403, "No permission no attach files.");
3076 goto ajax_save_end;
3077 }else{
3078 char *zRoot = (nrid==fpRoot) ? 0 : rid_to_uuid(fpRoot);
3079 const int atRc =
3080 attachments_ajax_from_POST(zRoot ? zRoot : zNewUuid,
3081 bNeedsModeration);
3082 fossil_free(zRoot);
3083 if( atRc<0 ){
3084 rc = atRc;
3085 goto ajax_save_end;
3086 }
3087 if( atRc>0
@@ -3055,12 +3088,12 @@
3088 && (iPostFlags & FPOST_NO_ALERT)!=0
3089 && db_table_exists("repository","pending_alert") ){
3090 /* Unqueue any alerts for these attachments. Recall that
3091 ** they're attached to the first version of the post, which
3092 ** means we actually risk cancelling _other_ pending
3093 ** notifications for attachments on this same post. C'est la
3094 ** vie.*/
3095 db_multi_exec(
3096 "WITH x(id) AS (\n"
3097 " SELECT 'f%d'\n"
3098 " UNION ALL\n"
3099 " SELECT 'f'||a.attachid FROM blob b, attachment a\n"
@@ -3070,11 +3103,15 @@
3103 fpRoot, fpRoot
3104 );
3105 }
3106 }
3107 }
3108 if( 0==bNeedsModeration
3109 /* ^^^ Do not allow a status tag on a pending-moderation post
3110 ** because it will introduce a reference to an artifact which
3111 ** will become a phantom if it is rejected by a moderator. */
3112 && zStatus!=0 && zStatus[0]!=0
3113 && forum_may_set_status(nrid)
3114 && forumpost_tag(nrid, 1, "status", zStatus)<0 ){
3115 rc = -ajax_route_error(500, "Tagging failed: %s", g.zErrMsg);
3116 goto ajax_save_end;
3117 }
@@ -3084,8 +3121,9 @@
3121 assert( zNewUuid );
3122 CX("{\"uuid\": %!j, \"dryrun\": %s, \"iPostFlags\":%d}\n",
3123 zNewUuid, bRollback ? "true" : "false", iPostFlags);
3124
3125 ajax_save_end:
3126 manifest_destroy(pPost);
3127 fossil_free(zNewUuid);
3128 db_end_transaction(rc || bRollback);
3129 }
3130
--- src/fossil.page.forumpost.js
+++ src/fossil.page.forumpost.js
@@ -54,16 +54,16 @@
5454
incorporate into the form for requests which request the
5555
preview or save the post.
5656
5757
TODO:
5858
59
- opt.inReplyTo=uuid: if this is a new response to a post, this
59
+ opt.inReplyTo=uuid: if this is a response to a post, this
6060
is the full forum post uuid of the being-replied-to post.
6161
6262
opt.edit=artifactObject: if this is an edit of an existing
6363
post, this is the full JSON-format artifact of the forum post
64
- the being-edit post.
64
+ the being-edited post, as returned by /ajax/artifact.json.
6565
*/
6666
constructor(opt){
6767
opt = this.#opt = F.nu({
6868
// todo: defaults once we determine the options
6969
// inReplyTo: hash
@@ -437,11 +437,11 @@
437437
get mimetype(){
438438
return this.#e.mimetype.select.value;
439439
}
440440
441441
get title(){
442
- return this.#e.title?.value;
442
+ return this.#e.title?.value || this.#opt.edit?.H;
443443
}
444444
445445
#initHelpTab(){
446446
const eh = this.#e.help;
447447
const list = D.ul();
@@ -477,14 +477,15 @@
477477
#newFormData(addThisContent){
478478
const fd = new FormData;
479479
for(const f of this.#extraFields){
480480
fd.append(f.name, f.value);
481481
}
482
- if( this.#e.title ){
483
- fd.append('title', this.title.trim());
484
- }else if( this.#opt.edit?.H ){
485
- fd.append('title', this.#opt.edit.H);
482
+ let v;
483
+ if( this.#opt.inReplyTo ){
484
+ fd.append( 'firt', this.#opt.inReplyTo );
485
+ }else if( (v = this.#e.title?.trim?.() ?? this.#opt.edit?.H) ){
486
+ fd.append('title', v);
486487
}
487488
fd.append('mimetype', this.mimetype);
488489
fd.append('content', addThisContent || this.editorContent.trim());
489490
if( this.#e.captcha ){
490491
fd.append('captcha', this.#e.captcha.value);
@@ -614,14 +615,17 @@
614615
const resp = window.fetch(F.repoUrl('forumajax_save'), {
615616
method: 'POST',
616617
body: fd
617618
}).then(r=>r.json())
618619
.then(j=>{
619
- console.debug("forum post submit response:",j);
620
+ j = F.nu(j);
621
+ console.debug("forum post editor response:",j);
620622
if( j.error ){
621623
throw new Error(j.error);
622624
}else if( j.message ){
625
+ /* This is only for use in debugging during
626
+ * development. */
623627
this.reportError(j.message);
624628
return;
625629
}
626630
if( 1 ){
627631
this.#clearDraft();
@@ -894,10 +898,11 @@
894898
895899
const fetchPost = async (fpid)=>{
896900
return window.fetch(F.repoUrl('ajax/artifact.json?uuid='+fpid))
897901
.then(r=>r.json())
898902
.then(j=>{
903
+ j = F.nu(j);
899904
if( j.error ) throw new Error(j.error);
900905
return j;
901906
});
902907
};
903908
@@ -922,10 +927,12 @@
922927
D.enable(eToDisable);
923928
};
924929
925930
const replyClicked = (form, ePost, eBtnReply, eToDisable)=>{
926931
const fpid = setupEditReplyElement(ePost, eBtnReply, eToDisable);
932
+ const firt = ePost.dataset.firt;
933
+ const fEditHead = ePost.dataset.fedithead;
927934
eBtnReply.innerText = "Replying...";
928935
F.toast.error("Reply is TODO. fpid="+fpid);
929936
/*
930937
TODOs include:
931938
@@ -944,10 +951,12 @@
944951
restoreEditReplyElement(ePost, eBtnReply, eToDisable);
945952
}/*replyClicked()*/;
946953
947954
const editClicked = (form, ePost, eBtnEdit, eToDisable)=>{
948955
const fpid = setupEditReplyElement(ePost, eBtnEdit, eToDisable);
956
+ const firt = ePost.dataset.firt;
957
+ const fEditHead = ePost.dataset.fedithead;
949958
eBtnEdit.innerText = "Editing...";
950959
F.toast.error("Edit is TODO. fpid="+fpid);
951960
/*
952961
TODOs include:
953962
@@ -974,13 +983,14 @@
974983
};
975984
const fpe = new F.ForumPostEditor({
976985
hiddenFields: form.querySelectorAll('input[type=hidden]'),
977986
ondiscard: ondone,
978987
onsubmit: ondone,
979
- draftKey: 'draft-forumedit-'+fpid.substr(0,12),
988
+ draftKey: 'draft-forumedit-'+(fEditHead || fpid).substr(0,12),
980989
hideTitle: true/*fixme: only show if this is the root post*/,
981
- edit: artifact
990
+ edit: artifact,
991
+ inReplyTo: firt
982992
});
983993
const w = fpe.widget;
984994
w.style.borderTop = '2px dotted';
985995
ePost.append(w);
986996
});
987997
--- src/fossil.page.forumpost.js
+++ src/fossil.page.forumpost.js
@@ -54,16 +54,16 @@
54 incorporate into the form for requests which request the
55 preview or save the post.
56
57 TODO:
58
59 opt.inReplyTo=uuid: if this is a new response to a post, this
60 is the full forum post uuid of the being-replied-to post.
61
62 opt.edit=artifactObject: if this is an edit of an existing
63 post, this is the full JSON-format artifact of the forum post
64 the being-edit post.
65 */
66 constructor(opt){
67 opt = this.#opt = F.nu({
68 // todo: defaults once we determine the options
69 // inReplyTo: hash
@@ -437,11 +437,11 @@
437 get mimetype(){
438 return this.#e.mimetype.select.value;
439 }
440
441 get title(){
442 return this.#e.title?.value;
443 }
444
445 #initHelpTab(){
446 const eh = this.#e.help;
447 const list = D.ul();
@@ -477,14 +477,15 @@
477 #newFormData(addThisContent){
478 const fd = new FormData;
479 for(const f of this.#extraFields){
480 fd.append(f.name, f.value);
481 }
482 if( this.#e.title ){
483 fd.append('title', this.title.trim());
484 }else if( this.#opt.edit?.H ){
485 fd.append('title', this.#opt.edit.H);
 
486 }
487 fd.append('mimetype', this.mimetype);
488 fd.append('content', addThisContent || this.editorContent.trim());
489 if( this.#e.captcha ){
490 fd.append('captcha', this.#e.captcha.value);
@@ -614,14 +615,17 @@
614 const resp = window.fetch(F.repoUrl('forumajax_save'), {
615 method: 'POST',
616 body: fd
617 }).then(r=>r.json())
618 .then(j=>{
619 console.debug("forum post submit response:",j);
 
620 if( j.error ){
621 throw new Error(j.error);
622 }else if( j.message ){
 
 
623 this.reportError(j.message);
624 return;
625 }
626 if( 1 ){
627 this.#clearDraft();
@@ -894,10 +898,11 @@
894
895 const fetchPost = async (fpid)=>{
896 return window.fetch(F.repoUrl('ajax/artifact.json?uuid='+fpid))
897 .then(r=>r.json())
898 .then(j=>{
 
899 if( j.error ) throw new Error(j.error);
900 return j;
901 });
902 };
903
@@ -922,10 +927,12 @@
922 D.enable(eToDisable);
923 };
924
925 const replyClicked = (form, ePost, eBtnReply, eToDisable)=>{
926 const fpid = setupEditReplyElement(ePost, eBtnReply, eToDisable);
 
 
927 eBtnReply.innerText = "Replying...";
928 F.toast.error("Reply is TODO. fpid="+fpid);
929 /*
930 TODOs include:
931
@@ -944,10 +951,12 @@
944 restoreEditReplyElement(ePost, eBtnReply, eToDisable);
945 }/*replyClicked()*/;
946
947 const editClicked = (form, ePost, eBtnEdit, eToDisable)=>{
948 const fpid = setupEditReplyElement(ePost, eBtnEdit, eToDisable);
 
 
949 eBtnEdit.innerText = "Editing...";
950 F.toast.error("Edit is TODO. fpid="+fpid);
951 /*
952 TODOs include:
953
@@ -974,13 +983,14 @@
974 };
975 const fpe = new F.ForumPostEditor({
976 hiddenFields: form.querySelectorAll('input[type=hidden]'),
977 ondiscard: ondone,
978 onsubmit: ondone,
979 draftKey: 'draft-forumedit-'+fpid.substr(0,12),
980 hideTitle: true/*fixme: only show if this is the root post*/,
981 edit: artifact
 
982 });
983 const w = fpe.widget;
984 w.style.borderTop = '2px dotted';
985 ePost.append(w);
986 });
987
--- src/fossil.page.forumpost.js
+++ src/fossil.page.forumpost.js
@@ -54,16 +54,16 @@
54 incorporate into the form for requests which request the
55 preview or save the post.
56
57 TODO:
58
59 opt.inReplyTo=uuid: if this is a response to a post, this
60 is the full forum post uuid of the being-replied-to post.
61
62 opt.edit=artifactObject: if this is an edit of an existing
63 post, this is the full JSON-format artifact of the forum post
64 the being-edited post, as returned by /ajax/artifact.json.
65 */
66 constructor(opt){
67 opt = this.#opt = F.nu({
68 // todo: defaults once we determine the options
69 // inReplyTo: hash
@@ -437,11 +437,11 @@
437 get mimetype(){
438 return this.#e.mimetype.select.value;
439 }
440
441 get title(){
442 return this.#e.title?.value || this.#opt.edit?.H;
443 }
444
445 #initHelpTab(){
446 const eh = this.#e.help;
447 const list = D.ul();
@@ -477,14 +477,15 @@
477 #newFormData(addThisContent){
478 const fd = new FormData;
479 for(const f of this.#extraFields){
480 fd.append(f.name, f.value);
481 }
482 let v;
483 if( this.#opt.inReplyTo ){
484 fd.append( 'firt', this.#opt.inReplyTo );
485 }else if( (v = this.#e.title?.trim?.() ?? this.#opt.edit?.H) ){
486 fd.append('title', v);
487 }
488 fd.append('mimetype', this.mimetype);
489 fd.append('content', addThisContent || this.editorContent.trim());
490 if( this.#e.captcha ){
491 fd.append('captcha', this.#e.captcha.value);
@@ -614,14 +615,17 @@
615 const resp = window.fetch(F.repoUrl('forumajax_save'), {
616 method: 'POST',
617 body: fd
618 }).then(r=>r.json())
619 .then(j=>{
620 j = F.nu(j);
621 console.debug("forum post editor response:",j);
622 if( j.error ){
623 throw new Error(j.error);
624 }else if( j.message ){
625 /* This is only for use in debugging during
626 * development. */
627 this.reportError(j.message);
628 return;
629 }
630 if( 1 ){
631 this.#clearDraft();
@@ -894,10 +898,11 @@
898
899 const fetchPost = async (fpid)=>{
900 return window.fetch(F.repoUrl('ajax/artifact.json?uuid='+fpid))
901 .then(r=>r.json())
902 .then(j=>{
903 j = F.nu(j);
904 if( j.error ) throw new Error(j.error);
905 return j;
906 });
907 };
908
@@ -922,10 +927,12 @@
927 D.enable(eToDisable);
928 };
929
930 const replyClicked = (form, ePost, eBtnReply, eToDisable)=>{
931 const fpid = setupEditReplyElement(ePost, eBtnReply, eToDisable);
932 const firt = ePost.dataset.firt;
933 const fEditHead = ePost.dataset.fedithead;
934 eBtnReply.innerText = "Replying...";
935 F.toast.error("Reply is TODO. fpid="+fpid);
936 /*
937 TODOs include:
938
@@ -944,10 +951,12 @@
951 restoreEditReplyElement(ePost, eBtnReply, eToDisable);
952 }/*replyClicked()*/;
953
954 const editClicked = (form, ePost, eBtnEdit, eToDisable)=>{
955 const fpid = setupEditReplyElement(ePost, eBtnEdit, eToDisable);
956 const firt = ePost.dataset.firt;
957 const fEditHead = ePost.dataset.fedithead;
958 eBtnEdit.innerText = "Editing...";
959 F.toast.error("Edit is TODO. fpid="+fpid);
960 /*
961 TODOs include:
962
@@ -974,13 +983,14 @@
983 };
984 const fpe = new F.ForumPostEditor({
985 hiddenFields: form.querySelectorAll('input[type=hidden]'),
986 ondiscard: ondone,
987 onsubmit: ondone,
988 draftKey: 'draft-forumedit-'+(fEditHead || fpid).substr(0,12),
989 hideTitle: true/*fixme: only show if this is the root post*/,
990 edit: artifact,
991 inReplyTo: firt
992 });
993 const w = fpe.widget;
994 w.style.borderTop = '2px dotted';
995 ePost.append(w);
996 });
997
--- src/style.c
+++ src/style.c
@@ -1434,10 +1434,13 @@
14341434
** For administators, or if the test_env_enable setting is true, then
14351435
** details of the request environment are displayed. Otherwise, just
14361436
** the error message is shown.
14371437
**
14381438
** If zFormat is an empty string, then this is the /test-env page.
1439
+**
1440
+** If the resulting formatted error message is not empty then this
1441
+** function does not return.
14391442
*/
14401443
void webpage_error(const char *zFormat, ...){
14411444
int showAll = 0;
14421445
char *zErr = 0;
14431446
int isAuth = 0;
14441447
--- src/style.c
+++ src/style.c
@@ -1434,10 +1434,13 @@
1434 ** For administators, or if the test_env_enable setting is true, then
1435 ** details of the request environment are displayed. Otherwise, just
1436 ** the error message is shown.
1437 **
1438 ** If zFormat is an empty string, then this is the /test-env page.
 
 
 
1439 */
1440 void webpage_error(const char *zFormat, ...){
1441 int showAll = 0;
1442 char *zErr = 0;
1443 int isAuth = 0;
1444
--- src/style.c
+++ src/style.c
@@ -1434,10 +1434,13 @@
1434 ** For administators, or if the test_env_enable setting is true, then
1435 ** details of the request environment are displayed. Otherwise, just
1436 ** the error message is shown.
1437 **
1438 ** If zFormat is an empty string, then this is the /test-env page.
1439 **
1440 ** If the resulting formatted error message is not empty then this
1441 ** function does not return.
1442 */
1443 void webpage_error(const char *zFormat, ...){
1444 int showAll = 0;
1445 char *zErr = 0;
1446 int isAuth = 0;
1447

Keyboard Shortcuts

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