Fossil SCM

Incremental work on the server side of the forum post ajax save.

stephan 2026-06-07 11:55 UTC forum-editor-2026
Commit e1571561608ec68e43b585f4fcf0de9ffc495b0502edb52e5fb96faabc58de0d
+7 -2
--- src/ajax.c
+++ src/ajax.c
@@ -191,23 +191,28 @@
191191
** {error: formatted message}
192192
**
193193
** If httpCode<=0 then it defaults to 500.
194194
**
195195
** After calling this, the caller should immediately return.
196
+**
197
+** Returns the resulting http code.
196198
*/
197
-void ajax_route_error(int httpCode, const char * zFmt, ...){
199
+int ajax_route_error(int httpCode, const char * zFmt, ...){
198200
Blob msg = empty_blob;
199201
Blob content = empty_blob;
200202
va_list vargs;
203
+
204
+ if( httpCode<=0 ) httpCode=500;
201205
va_start(vargs,zFmt);
202206
blob_vappendf(&msg, zFmt, vargs);
203207
va_end(vargs);
204208
blob_appendf(&content,"{\"error\":%!j}", blob_str(&msg));
205209
blob_reset(&msg);
206210
cgi_set_content(&content);
207
- cgi_set_status(httpCode>0 ? httpCode : 500, "Error");
211
+ cgi_set_status(httpCode, "Error");
208212
cgi_set_content_type("application/json");
213
+ return httpCode;
209214
}
210215
211216
void ajax_route_error_forbidden(){
212217
ajax_route_error(403, "Permission denied.");
213218
}
214219
--- src/ajax.c
+++ src/ajax.c
@@ -191,23 +191,28 @@
191 ** {error: formatted message}
192 **
193 ** If httpCode<=0 then it defaults to 500.
194 **
195 ** After calling this, the caller should immediately return.
 
 
196 */
197 void ajax_route_error(int httpCode, const char * zFmt, ...){
198 Blob msg = empty_blob;
199 Blob content = empty_blob;
200 va_list vargs;
 
 
201 va_start(vargs,zFmt);
202 blob_vappendf(&msg, zFmt, vargs);
203 va_end(vargs);
204 blob_appendf(&content,"{\"error\":%!j}", blob_str(&msg));
205 blob_reset(&msg);
206 cgi_set_content(&content);
207 cgi_set_status(httpCode>0 ? httpCode : 500, "Error");
208 cgi_set_content_type("application/json");
 
209 }
210
211 void ajax_route_error_forbidden(){
212 ajax_route_error(403, "Permission denied.");
213 }
214
--- src/ajax.c
+++ src/ajax.c
@@ -191,23 +191,28 @@
191 ** {error: formatted message}
192 **
193 ** If httpCode<=0 then it defaults to 500.
194 **
195 ** After calling this, the caller should immediately return.
196 **
197 ** Returns the resulting http code.
198 */
199 int ajax_route_error(int httpCode, const char * zFmt, ...){
200 Blob msg = empty_blob;
201 Blob content = empty_blob;
202 va_list vargs;
203
204 if( httpCode<=0 ) httpCode=500;
205 va_start(vargs,zFmt);
206 blob_vappendf(&msg, zFmt, vargs);
207 va_end(vargs);
208 blob_appendf(&content,"{\"error\":%!j}", blob_str(&msg));
209 blob_reset(&msg);
210 cgi_set_content(&content);
211 cgi_set_status(httpCode, "Error");
212 cgi_set_content_type("application/json");
213 return httpCode;
214 }
215
216 void ajax_route_error_forbidden(){
217 ajax_route_error(403, "Permission denied.");
218 }
219
+3 -3
--- src/attach.c
+++ src/attach.c
@@ -931,24 +931,24 @@
931931
}
932932
sqlite3_snprintf(sizeof(aKeySize), aKeySize, "%s:bytes",
933933
aKeyPrefix);
934934
szContent = atoi(PD(aKeySize,"-1"));
935935
if( szContent<=0 ){
936
- rc = -1;
936
+ rc = -400;
937937
ajax_route_error(400,"Invalid file size: %d", szContent);
938938
break;
939939
}else if( szLimit>0 && szContent>szLimit ){
940
- rc = -2;
940
+ rc = -413;
941941
ajax_route_error(413, "File size limit is %d bytes.", szLimit);
942942
break;
943943
}else{
944944
sqlite3_snprintf(sizeof(aKeyName), aKeyName, "%s:filename",
945945
aKeyPrefix);
946946
sqlite3_snprintf(sizeof(aKeyDesc), aKeyDesc, "%s_desc",
947947
aKeyPrefix);
948948
if( 0==(zFilename=P(aKeyName)) ){
949
- rc = -3;
949
+ rc = -400;
950950
ajax_route_error(400, "Missing filename.");
951951
break;
952952
}
953953
attach_commit(zFilename, zTarget, zContent, szContent,
954954
bNeedsModeration, P(aKeyDesc));
955955
--- src/attach.c
+++ src/attach.c
@@ -931,24 +931,24 @@
931 }
932 sqlite3_snprintf(sizeof(aKeySize), aKeySize, "%s:bytes",
933 aKeyPrefix);
934 szContent = atoi(PD(aKeySize,"-1"));
935 if( szContent<=0 ){
936 rc = -1;
937 ajax_route_error(400,"Invalid file size: %d", szContent);
938 break;
939 }else if( szLimit>0 && szContent>szLimit ){
940 rc = -2;
941 ajax_route_error(413, "File size limit is %d bytes.", szLimit);
942 break;
943 }else{
944 sqlite3_snprintf(sizeof(aKeyName), aKeyName, "%s:filename",
945 aKeyPrefix);
946 sqlite3_snprintf(sizeof(aKeyDesc), aKeyDesc, "%s_desc",
947 aKeyPrefix);
948 if( 0==(zFilename=P(aKeyName)) ){
949 rc = -3;
950 ajax_route_error(400, "Missing filename.");
951 break;
952 }
953 attach_commit(zFilename, zTarget, zContent, szContent,
954 bNeedsModeration, P(aKeyDesc));
955
--- src/attach.c
+++ src/attach.c
@@ -931,24 +931,24 @@
931 }
932 sqlite3_snprintf(sizeof(aKeySize), aKeySize, "%s:bytes",
933 aKeyPrefix);
934 szContent = atoi(PD(aKeySize,"-1"));
935 if( szContent<=0 ){
936 rc = -400;
937 ajax_route_error(400,"Invalid file size: %d", szContent);
938 break;
939 }else if( szLimit>0 && szContent>szLimit ){
940 rc = -413;
941 ajax_route_error(413, "File size limit is %d bytes.", szLimit);
942 break;
943 }else{
944 sqlite3_snprintf(sizeof(aKeyName), aKeyName, "%s:filename",
945 aKeyPrefix);
946 sqlite3_snprintf(sizeof(aKeyDesc), aKeyDesc, "%s_desc",
947 aKeyPrefix);
948 if( 0==(zFilename=P(aKeyName)) ){
949 rc = -400;
950 ajax_route_error(400, "Missing filename.");
951 break;
952 }
953 attach_commit(zFilename, zTarget, zContent, szContent,
954 bNeedsModeration, P(aKeyDesc));
955
+203 -9
--- src/forum.c
+++ src/forum.c
@@ -440,11 +440,12 @@
440440
**
441441
** - The applied tag is propagating so so that "closed" tags can
442442
** account for how edits of posts are handled. This differs from
443443
** closure of a branch, where a non-propagating tag is used.
444444
*/
445
-static int forumpost_tag(int frid, const char *zTagName, int addTag,
445
+static int forumpost_tag(int frid, int addTag,
446
+ const char *zTagName,
446447
const char *zValue){
447448
Blob artifact = BLOB_INITIALIZER; /* Output artifact */
448449
Blob cksum = BLOB_INITIALIZER; /* Z-card */
449450
int iTagged; /* true if frid is already tagged */
450451
int trid; /* RID of new control artifact */
@@ -1657,10 +1658,11 @@
16571658
return z[0]==0;
16581659
}
16591660
16601661
/* Flags for use with forum_post() */
16611662
#define FPOST_NO_ALERT 1 /* do not send any alerts */
1663
+#define FPOST_DRY_RUN 2 /* do not save the artifact */
16621664
16631665
/*
16641666
** Return a flags value for use with the final argument to
16651667
** forum_post(), extracted from the CGI environment.
16661668
*/
@@ -1667,10 +1669,13 @@
16671669
static int forum_post_flags(void){
16681670
int iPostFlags = 0;
16691671
if( g.perm.Debug && P("fpsilent")!=0 ){
16701672
iPostFlags |= FPOST_NO_ALERT;
16711673
}
1674
+ if( P("dryrun")!=0 ){
1675
+ iPostFlags |= FPOST_DRY_RUN;
1676
+ }
16721677
return iPostFlags;
16731678
}
16741679
16751680
/*
16761681
** Add a new Forum Post artifact to the repository.
@@ -1723,17 +1728,19 @@
17231728
fossil_free(zG);
17241729
}
17251730
if( zTitle ){
17261731
blob_appendf(&x, "H %F\n", zTitle);
17271732
}
1728
- zI = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", iInReplyTo);
1733
+ zI = rid_to_uuid(iInReplyTo);
17291734
if( zI ){
17301735
blob_appendf(&x, "I %s\n", zI);
17311736
fossil_free(zI);
17321737
}
1733
- if( fossil_strcmp(zMimetype,"text/x-fossil-wiki")!=0 ){
1734
- blob_appendf(&x, "N %s\n", zMimetype);
1738
+ if( zMimetype!=0
1739
+ && zMimetype[0]!=0
1740
+ && fossil_strcmp(zMimetype,"text/x-fossil-wiki")!=0 ){
1741
+ blob_appendf(&x, "N %F\n", zMimetype);
17351742
}
17361743
if( iEdit>0 ){
17371744
char *zP = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", iEdit);
17381745
if( zP==0 ) webpage_error("missing edit artifact %d", iEdit);
17391746
blob_appendf(&x, "P %s\n", zP);
@@ -1761,11 +1768,11 @@
17611768
webpage_error("malformed forum post artifact - %s", blob_str(&errMsg));
17621769
}
17631770
webpage_assert( pPost->type==CFTYPE_FORUM );
17641771
manifest_destroy(pPost);
17651772
1766
- if( P("dryrun") ){
1773
+ if( (iFlags & FPOST_DRY_RUN)!=0 ){
17671774
@ <div class='debug'>
17681775
@ This is the artifact that would have been generated:
17691776
@ <pre>%h(blob_str(&x))</pre>
17701777
@ </div>
17711778
blob_reset(&x);
@@ -1824,11 +1831,11 @@
18241831
int addTag, int validFpid){
18251832
if( !cgi_csrf_safe(2) ){
18261833
webpage_error("CSRF validation failed");
18271834
}else{
18281835
const int fpid = validFpid>0 ? validFpid : forum_validate_fpid_param();
1829
- forumpost_tag(fpid, zTag, addTag, zVal);
1836
+ forumpost_tag(fpid, addTag, zTag, zVal);
18301837
cgi_redirectf("%R/forumpost/%S",P("fpid"));
18311838
}
18321839
}
18331840
18341841
/*
@@ -2788,10 +2795,153 @@
27882795
forum_emit_js();
27892796
}
27902797
style_finish_page();
27912798
}
27922799
2800
+/*
2801
+** The AJAX counterpart of forum_post().
2802
+**
2803
+** Returns the new artifact's RID on success, 0 if no changes were
2804
+** necessary (e.g. an empty new post or dry-run mode), and a negative
2805
+** value on error. If it returns a negative value then it will have
2806
+** populated the ajax response state with an error object.
2807
+**
2808
+** The caller must have started a transaction and must roll it back if
2809
+** this call returns <=0, noting that only the negative-value case is
2810
+** an error.
2811
+*/
2812
+int forum_post_ajax(
2813
+ const char *zTitle, /* Title. NULL for replies */
2814
+ int iInReplyTo, /* Post replying to. 0 for new threads */
2815
+ int iEdit, /* Post being edited, or zero for a new post */
2816
+ const char *zUser, /* Username. NULL means use login name */
2817
+ const char *zMimetype, /* Mimetype of content. */
2818
+ const char *zContent, /* Content */
2819
+ int iFlags /* FPOST_xyz flag values */
2820
+){
2821
+ char *zI;
2822
+ char *zG;
2823
+ int iBasis;
2824
+ Blob x = BLOB_INITIALIZER,
2825
+ cksum = BLOB_INITIALIZER,
2826
+ formatCheck = BLOB_INITIALIZER,
2827
+ errMsg = BLOB_INITIALIZER;
2828
+ Manifest *pPost = 0;
2829
+ int nContent = zContent ? (int)strlen(zContent) : 0;
2830
+ int rc = 0;
2831
+
2832
+ assert( db_transaction_nesting_depth()>0 );
2833
+ schema_forum();
2834
+ if( iEdit==0 && whitespace_only(zContent) ){
2835
+ return 0;
2836
+ }
2837
+ if( !g.perm.Admin && (iEdit || iInReplyTo)
2838
+ && forum_rid_is_tagged(iEdit ? iEdit : iInReplyTo, "closed", 1) ){
2839
+ return -ajax_route_error(400, "Thread is closed.");
2840
+ }
2841
+ if( 0==iInReplyTo && whitespace_only(zTitle) ){
2842
+ return -ajax_route_error(400, "Empty title is not permitted.");
2843
+ }
2844
+
2845
+ if( zUser==0 ){
2846
+ if( login_is_nobody() ){
2847
+ zUser = "anonymous";
2848
+ }else{
2849
+ zUser = login_name();
2850
+ }
2851
+ }
2852
+ if( iEdit>0
2853
+ && !g.perm.Admin
2854
+ && !forumpost_is_owner(iEdit, zUser) ){
2855
+ return -ajax_route_error(
2856
+ 403, "Only admins may edit other peoples' posts."
2857
+ );
2858
+ }
2859
+ if( iInReplyTo==0 && iEdit>0 ){
2860
+ iBasis = iEdit;
2861
+ iInReplyTo = db_int(0, "SELECT firt FROM forumpost WHERE fpid=%d",
2862
+ iEdit);
2863
+ }else{
2864
+ iBasis = iInReplyTo;
2865
+ }
2866
+ webpage_assert( (zTitle==0)+(iInReplyTo==0)==1 );
2867
+ blob_init(&x, 0, 0);
2868
+ blob_appendf(&x, "D %z\n", date_in_standard_format("now"));
2869
+ zG = db_text(
2870
+ 0,
2871
+ "SELECT uuid FROM blob, forumpost"
2872
+ " WHERE blob.rid==forumpost.froot"
2873
+ " AND forumpost.fpid=%d",
2874
+ iBasis
2875
+ );
2876
+ if( zG ){
2877
+ blob_appendf(&x, "G %z\n", zG);
2878
+ }
2879
+ if( zTitle ){
2880
+ blob_appendf(&x, "H %F\n", zTitle);
2881
+ }
2882
+ if( iInReplyTo>0 ){
2883
+ zI = rid_to_uuid(iInReplyTo);
2884
+ if( 0==zI ){
2885
+ rc = -ajax_route_error(404, "Missing in-reply-to artifact %d",
2886
+ iInReplyTo);
2887
+ goto post_ajax_end;
2888
+ }
2889
+ blob_appendf(&x, "I %z\n", zI);
2890
+ }
2891
+ if( zMimetype!=0
2892
+ && zMimetype[0]!=0
2893
+ && fossil_strcmp(zMimetype,"text/x-fossil-wiki")!=0 ){
2894
+ blob_appendf(&x, "N %F\n", zMimetype);
2895
+ }
2896
+ if( iEdit>0 ){
2897
+ char *zP = rid_to_uuid(iEdit);
2898
+ if( zP==0 ){
2899
+ rc = -ajax_route_error(404, "Missing edit artifact %d", iEdit);
2900
+ goto post_ajax_end;
2901
+ }
2902
+ blob_appendf(&x, "P %z\n", zP);
2903
+ }
2904
+
2905
+ blob_appendf(&x, "U %F\n", zUser);
2906
+ blob_appendf(&x, "W %d\n%s\n", nContent, zContent);
2907
+ md5sum_blob(&x, &cksum);
2908
+ blob_appendf(&x, "Z %b\n", &cksum);
2909
+ blob_reset(&cksum);
2910
+
2911
+ /* Verify that the artifact we are creating is well-formed */
2912
+ blob_init(&formatCheck, 0, 0);
2913
+ blob_init(&errMsg, 0, 0);
2914
+ blob_copy(&formatCheck, &x);
2915
+ pPost = manifest_parse(&formatCheck, 0, &errMsg);
2916
+ if( pPost==0 ){
2917
+ ajax_route_error(500, "Malformed forum post artifact: %b", &errMsg);
2918
+ rc = -500;
2919
+ goto post_ajax_end;
2920
+ }
2921
+ webpage_assert( pPost->type==CFTYPE_FORUM );
2922
+
2923
+ if( (iFlags & FPOST_DRY_RUN)!=0 ){
2924
+ rc = 0;
2925
+ }else{
2926
+ int nrid;
2927
+ db_begin_transaction();
2928
+ nrid = wiki_put(&x, iEdit>0 ? iEdit : 0, forum_need_moderation());
2929
+ blob_reset(&x);
2930
+ if( (iFlags & FPOST_NO_ALERT)!=0 ){
2931
+ alert_unqueue('f', nrid);
2932
+ }
2933
+ rc = nrid;
2934
+ db_end_transaction(0);
2935
+ }
2936
+post_ajax_end:
2937
+ manifest_destroy(pPost);
2938
+ blob_reset(&x);
2939
+ blob_reset(&cksum);
2940
+ blob_reset(&formatCheck);
2941
+ return rc;
2942
+}
27932943
/*
27942944
** WEBPAGE: forumajax_save hidden
27952945
**
27962946
** WIP
27972947
**
@@ -2805,13 +2955,18 @@
28052955
const char *zIrt;
28062956
const char *zMimetype;
28072957
const char *zContent;
28082958
const char *zStatus;
28092959
const int bHasAttachment = P("file1")!=0;
2960
+ char *zNewUuid = 0;
28102961
int bNeedsModeration = 0;
28112962
int goodCaptcha = 1;
2812
- int bRollback = 0;
2963
+ int bRollback = PB("rollback"); /* True if we should roll back. */
2964
+ int iIrt = 0; /* In-reply-to rid or 0 */
2965
+ int iEditRid = 0; /* Post rid being edited or 0 */
2966
+ int rc = 0;
2967
+ int nrid = 0;
28132968
28142969
if( !ajax_route_bootstrap(0, 1) ){
28152970
return;
28162971
}else if( !g.perm.WrForum
28172972
|| (bHasAttachment && !g.perm.AttachForum) ){
@@ -2822,10 +2977,16 @@
28222977
return;
28232978
}else if( 0==(goodCaptcha = captcha_is_correct(0)) ){
28242979
ajax_route_error_captcha();
28252980
return;
28262981
}
2982
+
2983
+ zTitle = P("title");
2984
+ zIrt = P("firt");
2985
+ zMimetype = P("mimetype");
2986
+ zContent = P("content");
2987
+ zStatus = P("status");
28272988
db_begin_transaction();
28282989
/*
28292990
** TODOs include:
28302991
**
28312992
** - Permissions and sanity checks, of course.
@@ -2844,14 +3005,47 @@
28443005
** - Allow status change only if permissions allow.
28453006
*/
28463007
28473008
(void)bNeedsModeration;
28483009
(void)zUuid;
2849
- (void)zTitle;
28503010
(void)zIrt;
28513011
(void)zMimetype;
28523012
(void)zContent;
28533013
(void)zStatus;
28543014
2855
- ajax_route_error(400, "Save is TODO");
3015
+ if( 1 ){
3016
+ bRollback = 1;
3017
+ ajax_route_error(400, "Save is TODO");
3018
+ goto ajax_save_end;
3019
+ }
3020
+
3021
+ nrid = forum_post_ajax(zTitle, iIrt, iEditRid, 0, zMimetype,
3022
+ zContent, forum_post_flags());
3023
+ if( nrid==0 ){
3024
+ CX("{\"message\": \"No changes needed saving.\"}\n");
3025
+ goto ajax_save_end;
3026
+ }
3027
+ if( zStatus!=0 && zStatus[0]!=0 ){
3028
+ forumpost_tag(nrid, 1, "status", zStatus)
3029
+ /* FIXME: ^^^ fails fatally on error */;
3030
+ }
3031
+ zNewUuid = rid_to_uuid(nrid);
3032
+ if( 0!=P("file1") ){
3033
+ /* Attachments */
3034
+ const int atRc =
3035
+ attachments_ajax_from_POST(zNewUuid, bNeedsModeration);
3036
+ if( atRc<0 ){
3037
+ rc = atRc;
3038
+ goto ajax_save_end;
3039
+ }
3040
+ }
3041
+
3042
+
3043
+ if( 0==rc ){
3044
+ CX("{\"uuid\": %!j}\n", zNewUuid);
3045
+ }
3046
+
3047
+ajax_save_end:
3048
+ fossil_free(zNewUuid);
3049
+ if( 0!=rc ) bRollback = 1;
28563050
db_end_transaction(bRollback);
28573051
}
28583052
--- src/forum.c
+++ src/forum.c
@@ -440,11 +440,12 @@
440 **
441 ** - The applied tag is propagating so so that "closed" tags can
442 ** account for how edits of posts are handled. This differs from
443 ** closure of a branch, where a non-propagating tag is used.
444 */
445 static int forumpost_tag(int frid, const char *zTagName, int addTag,
 
446 const char *zValue){
447 Blob artifact = BLOB_INITIALIZER; /* Output artifact */
448 Blob cksum = BLOB_INITIALIZER; /* Z-card */
449 int iTagged; /* true if frid is already tagged */
450 int trid; /* RID of new control artifact */
@@ -1657,10 +1658,11 @@
1657 return z[0]==0;
1658 }
1659
1660 /* Flags for use with forum_post() */
1661 #define FPOST_NO_ALERT 1 /* do not send any alerts */
 
1662
1663 /*
1664 ** Return a flags value for use with the final argument to
1665 ** forum_post(), extracted from the CGI environment.
1666 */
@@ -1667,10 +1669,13 @@
1667 static int forum_post_flags(void){
1668 int iPostFlags = 0;
1669 if( g.perm.Debug && P("fpsilent")!=0 ){
1670 iPostFlags |= FPOST_NO_ALERT;
1671 }
 
 
 
1672 return iPostFlags;
1673 }
1674
1675 /*
1676 ** Add a new Forum Post artifact to the repository.
@@ -1723,17 +1728,19 @@
1723 fossil_free(zG);
1724 }
1725 if( zTitle ){
1726 blob_appendf(&x, "H %F\n", zTitle);
1727 }
1728 zI = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", iInReplyTo);
1729 if( zI ){
1730 blob_appendf(&x, "I %s\n", zI);
1731 fossil_free(zI);
1732 }
1733 if( fossil_strcmp(zMimetype,"text/x-fossil-wiki")!=0 ){
1734 blob_appendf(&x, "N %s\n", zMimetype);
 
 
1735 }
1736 if( iEdit>0 ){
1737 char *zP = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", iEdit);
1738 if( zP==0 ) webpage_error("missing edit artifact %d", iEdit);
1739 blob_appendf(&x, "P %s\n", zP);
@@ -1761,11 +1768,11 @@
1761 webpage_error("malformed forum post artifact - %s", blob_str(&errMsg));
1762 }
1763 webpage_assert( pPost->type==CFTYPE_FORUM );
1764 manifest_destroy(pPost);
1765
1766 if( P("dryrun") ){
1767 @ <div class='debug'>
1768 @ This is the artifact that would have been generated:
1769 @ <pre>%h(blob_str(&x))</pre>
1770 @ </div>
1771 blob_reset(&x);
@@ -1824,11 +1831,11 @@
1824 int addTag, int validFpid){
1825 if( !cgi_csrf_safe(2) ){
1826 webpage_error("CSRF validation failed");
1827 }else{
1828 const int fpid = validFpid>0 ? validFpid : forum_validate_fpid_param();
1829 forumpost_tag(fpid, zTag, addTag, zVal);
1830 cgi_redirectf("%R/forumpost/%S",P("fpid"));
1831 }
1832 }
1833
1834 /*
@@ -2788,10 +2795,153 @@
2788 forum_emit_js();
2789 }
2790 style_finish_page();
2791 }
2792
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2793 /*
2794 ** WEBPAGE: forumajax_save hidden
2795 **
2796 ** WIP
2797 **
@@ -2805,13 +2955,18 @@
2805 const char *zIrt;
2806 const char *zMimetype;
2807 const char *zContent;
2808 const char *zStatus;
2809 const int bHasAttachment = P("file1")!=0;
 
2810 int bNeedsModeration = 0;
2811 int goodCaptcha = 1;
2812 int bRollback = 0;
 
 
 
 
2813
2814 if( !ajax_route_bootstrap(0, 1) ){
2815 return;
2816 }else if( !g.perm.WrForum
2817 || (bHasAttachment && !g.perm.AttachForum) ){
@@ -2822,10 +2977,16 @@
2822 return;
2823 }else if( 0==(goodCaptcha = captcha_is_correct(0)) ){
2824 ajax_route_error_captcha();
2825 return;
2826 }
 
 
 
 
 
 
2827 db_begin_transaction();
2828 /*
2829 ** TODOs include:
2830 **
2831 ** - Permissions and sanity checks, of course.
@@ -2844,14 +3005,47 @@
2844 ** - Allow status change only if permissions allow.
2845 */
2846
2847 (void)bNeedsModeration;
2848 (void)zUuid;
2849 (void)zTitle;
2850 (void)zIrt;
2851 (void)zMimetype;
2852 (void)zContent;
2853 (void)zStatus;
2854
2855 ajax_route_error(400, "Save is TODO");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2856 db_end_transaction(bRollback);
2857 }
2858
--- src/forum.c
+++ src/forum.c
@@ -440,11 +440,12 @@
440 **
441 ** - The applied tag is propagating so so that "closed" tags can
442 ** account for how edits of posts are handled. This differs from
443 ** closure of a branch, where a non-propagating tag is used.
444 */
445 static int forumpost_tag(int frid, int addTag,
446 const char *zTagName,
447 const char *zValue){
448 Blob artifact = BLOB_INITIALIZER; /* Output artifact */
449 Blob cksum = BLOB_INITIALIZER; /* Z-card */
450 int iTagged; /* true if frid is already tagged */
451 int trid; /* RID of new control artifact */
@@ -1657,10 +1658,11 @@
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_DRY_RUN 2 /* do not save the artifact */
1664
1665 /*
1666 ** Return a flags value for use with the final argument to
1667 ** forum_post(), extracted from the CGI environment.
1668 */
@@ -1667,10 +1669,13 @@
1669 static int forum_post_flags(void){
1670 int iPostFlags = 0;
1671 if( g.perm.Debug && P("fpsilent")!=0 ){
1672 iPostFlags |= FPOST_NO_ALERT;
1673 }
1674 if( P("dryrun")!=0 ){
1675 iPostFlags |= FPOST_DRY_RUN;
1676 }
1677 return iPostFlags;
1678 }
1679
1680 /*
1681 ** Add a new Forum Post artifact to the repository.
@@ -1723,17 +1728,19 @@
1728 fossil_free(zG);
1729 }
1730 if( zTitle ){
1731 blob_appendf(&x, "H %F\n", zTitle);
1732 }
1733 zI = rid_to_uuid(iInReplyTo);
1734 if( zI ){
1735 blob_appendf(&x, "I %s\n", zI);
1736 fossil_free(zI);
1737 }
1738 if( zMimetype!=0
1739 && zMimetype[0]!=0
1740 && fossil_strcmp(zMimetype,"text/x-fossil-wiki")!=0 ){
1741 blob_appendf(&x, "N %F\n", zMimetype);
1742 }
1743 if( iEdit>0 ){
1744 char *zP = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", iEdit);
1745 if( zP==0 ) webpage_error("missing edit artifact %d", iEdit);
1746 blob_appendf(&x, "P %s\n", zP);
@@ -1761,11 +1768,11 @@
1768 webpage_error("malformed forum post artifact - %s", blob_str(&errMsg));
1769 }
1770 webpage_assert( pPost->type==CFTYPE_FORUM );
1771 manifest_destroy(pPost);
1772
1773 if( (iFlags & FPOST_DRY_RUN)!=0 ){
1774 @ <div class='debug'>
1775 @ This is the artifact that would have been generated:
1776 @ <pre>%h(blob_str(&x))</pre>
1777 @ </div>
1778 blob_reset(&x);
@@ -1824,11 +1831,11 @@
1831 int addTag, int validFpid){
1832 if( !cgi_csrf_safe(2) ){
1833 webpage_error("CSRF validation failed");
1834 }else{
1835 const int fpid = validFpid>0 ? validFpid : forum_validate_fpid_param();
1836 forumpost_tag(fpid, addTag, zTag, zVal);
1837 cgi_redirectf("%R/forumpost/%S",P("fpid"));
1838 }
1839 }
1840
1841 /*
@@ -2788,10 +2795,153 @@
2795 forum_emit_js();
2796 }
2797 style_finish_page();
2798 }
2799
2800 /*
2801 ** The AJAX counterpart of forum_post().
2802 **
2803 ** Returns the new artifact's RID on success, 0 if no changes were
2804 ** necessary (e.g. an empty new post or dry-run mode), and a negative
2805 ** value on error. If it returns a negative value then it will have
2806 ** populated the ajax response state with an error object.
2807 **
2808 ** The caller must have started a transaction and must roll it back if
2809 ** this call returns <=0, noting that only the negative-value case is
2810 ** an error.
2811 */
2812 int forum_post_ajax(
2813 const char *zTitle, /* Title. NULL for replies */
2814 int iInReplyTo, /* Post replying to. 0 for new threads */
2815 int iEdit, /* Post being edited, or zero for a new post */
2816 const char *zUser, /* Username. NULL means use login name */
2817 const char *zMimetype, /* Mimetype of content. */
2818 const char *zContent, /* Content */
2819 int iFlags /* FPOST_xyz flag values */
2820 ){
2821 char *zI;
2822 char *zG;
2823 int iBasis;
2824 Blob x = BLOB_INITIALIZER,
2825 cksum = BLOB_INITIALIZER,
2826 formatCheck = BLOB_INITIALIZER,
2827 errMsg = BLOB_INITIALIZER;
2828 Manifest *pPost = 0;
2829 int nContent = zContent ? (int)strlen(zContent) : 0;
2830 int rc = 0;
2831
2832 assert( db_transaction_nesting_depth()>0 );
2833 schema_forum();
2834 if( iEdit==0 && whitespace_only(zContent) ){
2835 return 0;
2836 }
2837 if( !g.perm.Admin && (iEdit || iInReplyTo)
2838 && forum_rid_is_tagged(iEdit ? iEdit : iInReplyTo, "closed", 1) ){
2839 return -ajax_route_error(400, "Thread is closed.");
2840 }
2841 if( 0==iInReplyTo && whitespace_only(zTitle) ){
2842 return -ajax_route_error(400, "Empty title is not permitted.");
2843 }
2844
2845 if( zUser==0 ){
2846 if( login_is_nobody() ){
2847 zUser = "anonymous";
2848 }else{
2849 zUser = login_name();
2850 }
2851 }
2852 if( iEdit>0
2853 && !g.perm.Admin
2854 && !forumpost_is_owner(iEdit, zUser) ){
2855 return -ajax_route_error(
2856 403, "Only admins may edit other peoples' posts."
2857 );
2858 }
2859 if( iInReplyTo==0 && iEdit>0 ){
2860 iBasis = iEdit;
2861 iInReplyTo = db_int(0, "SELECT firt FROM forumpost WHERE fpid=%d",
2862 iEdit);
2863 }else{
2864 iBasis = iInReplyTo;
2865 }
2866 webpage_assert( (zTitle==0)+(iInReplyTo==0)==1 );
2867 blob_init(&x, 0, 0);
2868 blob_appendf(&x, "D %z\n", date_in_standard_format("now"));
2869 zG = db_text(
2870 0,
2871 "SELECT uuid FROM blob, forumpost"
2872 " WHERE blob.rid==forumpost.froot"
2873 " AND forumpost.fpid=%d",
2874 iBasis
2875 );
2876 if( zG ){
2877 blob_appendf(&x, "G %z\n", zG);
2878 }
2879 if( zTitle ){
2880 blob_appendf(&x, "H %F\n", zTitle);
2881 }
2882 if( iInReplyTo>0 ){
2883 zI = rid_to_uuid(iInReplyTo);
2884 if( 0==zI ){
2885 rc = -ajax_route_error(404, "Missing in-reply-to artifact %d",
2886 iInReplyTo);
2887 goto post_ajax_end;
2888 }
2889 blob_appendf(&x, "I %z\n", zI);
2890 }
2891 if( zMimetype!=0
2892 && zMimetype[0]!=0
2893 && fossil_strcmp(zMimetype,"text/x-fossil-wiki")!=0 ){
2894 blob_appendf(&x, "N %F\n", zMimetype);
2895 }
2896 if( iEdit>0 ){
2897 char *zP = rid_to_uuid(iEdit);
2898 if( zP==0 ){
2899 rc = -ajax_route_error(404, "Missing edit artifact %d", iEdit);
2900 goto post_ajax_end;
2901 }
2902 blob_appendf(&x, "P %z\n", zP);
2903 }
2904
2905 blob_appendf(&x, "U %F\n", zUser);
2906 blob_appendf(&x, "W %d\n%s\n", nContent, zContent);
2907 md5sum_blob(&x, &cksum);
2908 blob_appendf(&x, "Z %b\n", &cksum);
2909 blob_reset(&cksum);
2910
2911 /* Verify that the artifact we are creating is well-formed */
2912 blob_init(&formatCheck, 0, 0);
2913 blob_init(&errMsg, 0, 0);
2914 blob_copy(&formatCheck, &x);
2915 pPost = manifest_parse(&formatCheck, 0, &errMsg);
2916 if( pPost==0 ){
2917 ajax_route_error(500, "Malformed forum post artifact: %b", &errMsg);
2918 rc = -500;
2919 goto post_ajax_end;
2920 }
2921 webpage_assert( pPost->type==CFTYPE_FORUM );
2922
2923 if( (iFlags & FPOST_DRY_RUN)!=0 ){
2924 rc = 0;
2925 }else{
2926 int nrid;
2927 db_begin_transaction();
2928 nrid = wiki_put(&x, iEdit>0 ? iEdit : 0, forum_need_moderation());
2929 blob_reset(&x);
2930 if( (iFlags & FPOST_NO_ALERT)!=0 ){
2931 alert_unqueue('f', nrid);
2932 }
2933 rc = nrid;
2934 db_end_transaction(0);
2935 }
2936 post_ajax_end:
2937 manifest_destroy(pPost);
2938 blob_reset(&x);
2939 blob_reset(&cksum);
2940 blob_reset(&formatCheck);
2941 return rc;
2942 }
2943 /*
2944 ** WEBPAGE: forumajax_save hidden
2945 **
2946 ** WIP
2947 **
@@ -2805,13 +2955,18 @@
2955 const char *zIrt;
2956 const char *zMimetype;
2957 const char *zContent;
2958 const char *zStatus;
2959 const int bHasAttachment = P("file1")!=0;
2960 char *zNewUuid = 0;
2961 int bNeedsModeration = 0;
2962 int goodCaptcha = 1;
2963 int bRollback = PB("rollback"); /* True if we should roll back. */
2964 int iIrt = 0; /* In-reply-to rid or 0 */
2965 int iEditRid = 0; /* Post rid being edited or 0 */
2966 int rc = 0;
2967 int nrid = 0;
2968
2969 if( !ajax_route_bootstrap(0, 1) ){
2970 return;
2971 }else if( !g.perm.WrForum
2972 || (bHasAttachment && !g.perm.AttachForum) ){
@@ -2822,10 +2977,16 @@
2977 return;
2978 }else if( 0==(goodCaptcha = captcha_is_correct(0)) ){
2979 ajax_route_error_captcha();
2980 return;
2981 }
2982
2983 zTitle = P("title");
2984 zIrt = P("firt");
2985 zMimetype = P("mimetype");
2986 zContent = P("content");
2987 zStatus = P("status");
2988 db_begin_transaction();
2989 /*
2990 ** TODOs include:
2991 **
2992 ** - Permissions and sanity checks, of course.
@@ -2844,14 +3005,47 @@
3005 ** - Allow status change only if permissions allow.
3006 */
3007
3008 (void)bNeedsModeration;
3009 (void)zUuid;
 
3010 (void)zIrt;
3011 (void)zMimetype;
3012 (void)zContent;
3013 (void)zStatus;
3014
3015 if( 1 ){
3016 bRollback = 1;
3017 ajax_route_error(400, "Save is TODO");
3018 goto ajax_save_end;
3019 }
3020
3021 nrid = forum_post_ajax(zTitle, iIrt, iEditRid, 0, zMimetype,
3022 zContent, forum_post_flags());
3023 if( nrid==0 ){
3024 CX("{\"message\": \"No changes needed saving.\"}\n");
3025 goto ajax_save_end;
3026 }
3027 if( zStatus!=0 && zStatus[0]!=0 ){
3028 forumpost_tag(nrid, 1, "status", zStatus)
3029 /* FIXME: ^^^ fails fatally on error */;
3030 }
3031 zNewUuid = rid_to_uuid(nrid);
3032 if( 0!=P("file1") ){
3033 /* Attachments */
3034 const int atRc =
3035 attachments_ajax_from_POST(zNewUuid, bNeedsModeration);
3036 if( atRc<0 ){
3037 rc = atRc;
3038 goto ajax_save_end;
3039 }
3040 }
3041
3042
3043 if( 0==rc ){
3044 CX("{\"uuid\": %!j}\n", zNewUuid);
3045 }
3046
3047 ajax_save_end:
3048 fossil_free(zNewUuid);
3049 if( 0!=rc ) bRollback = 1;
3050 db_end_transaction(bRollback);
3051 }
3052
--- src/fossil.page.forumpost.js
+++ src/fossil.page.forumpost.js
@@ -523,10 +523,13 @@
523523
const v = this.#e.status.value;
524524
if( this.#e.status.dataset.originalValue !== v ){
525525
fd.append( "status", v );
526526
}
527527
}
528
+ if( 1 ){
529
+ fd.append("dryrun", 1);
530
+ }
528531
console.warn("Ready to submit",fd);
529532
/*
530533
TODO: save it, set #isWaiting=false, then handle error or
531534
redirect to the post (if this is a new post) or, if replying
532535
inline, replace this object with a static rendering from the
@@ -535,12 +538,16 @@
535538
const resp = window.fetch(F.repoUrl('forumajax_save'), {
536539
method: 'POST',
537540
body: fd
538541
}).then(r=>r.json())
539542
.then(j=>{
543
+ console.debug("submit response:",j);
540544
if( j.error ){
541545
throw new Error(j.error);
546
+ }else if( j.message ){
547
+ this.reportError(j.message);
548
+ return;
542549
}
543550
this.#clearDraft();
544551
window.location = F.repoUrl('forumpost/'+j.uuid);
545552
})
546553
.catch((e)=>this.reportError(e.message))
547554
--- src/fossil.page.forumpost.js
+++ src/fossil.page.forumpost.js
@@ -523,10 +523,13 @@
523 const v = this.#e.status.value;
524 if( this.#e.status.dataset.originalValue !== v ){
525 fd.append( "status", v );
526 }
527 }
 
 
 
528 console.warn("Ready to submit",fd);
529 /*
530 TODO: save it, set #isWaiting=false, then handle error or
531 redirect to the post (if this is a new post) or, if replying
532 inline, replace this object with a static rendering from the
@@ -535,12 +538,16 @@
535 const resp = window.fetch(F.repoUrl('forumajax_save'), {
536 method: 'POST',
537 body: fd
538 }).then(r=>r.json())
539 .then(j=>{
 
540 if( j.error ){
541 throw new Error(j.error);
 
 
 
542 }
543 this.#clearDraft();
544 window.location = F.repoUrl('forumpost/'+j.uuid);
545 })
546 .catch((e)=>this.reportError(e.message))
547
--- src/fossil.page.forumpost.js
+++ src/fossil.page.forumpost.js
@@ -523,10 +523,13 @@
523 const v = this.#e.status.value;
524 if( this.#e.status.dataset.originalValue !== v ){
525 fd.append( "status", v );
526 }
527 }
528 if( 1 ){
529 fd.append("dryrun", 1);
530 }
531 console.warn("Ready to submit",fd);
532 /*
533 TODO: save it, set #isWaiting=false, then handle error or
534 redirect to the post (if this is a new post) or, if replying
535 inline, replace this object with a static rendering from the
@@ -535,12 +538,16 @@
538 const resp = window.fetch(F.repoUrl('forumajax_save'), {
539 method: 'POST',
540 body: fd
541 }).then(r=>r.json())
542 .then(j=>{
543 console.debug("submit response:",j);
544 if( j.error ){
545 throw new Error(j.error);
546 }else if( j.message ){
547 this.reportError(j.message);
548 return;
549 }
550 this.#clearDraft();
551 window.location = F.repoUrl('forumpost/'+j.uuid);
552 })
553 .catch((e)=>this.reportError(e.message))
554

Keyboard Shortcuts

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