Fossil SCM
Corrected parsing of /json-mode POST data in TLS mode. Extended /json/wiki/preview to support a mimetype option.
Commit
7f5877e8439531f8c0f758db922bf8a0e2fa58d1d3051386812edb4c91292fa7
Parent
acffc8f7858254e…
4 files changed
+19
-72
+6
-15
+27
-6
+16
-11
+19
-72
| --- src/cgi.c | ||
| +++ src/cgi.c | ||
| @@ -1061,71 +1061,25 @@ | ||
| 1061 | 1061 | } |
| 1062 | 1062 | |
| 1063 | 1063 | |
| 1064 | 1064 | #ifdef FOSSIL_ENABLE_JSON |
| 1065 | 1065 | /* |
| 1066 | -** Internal helper for cson_data_source_FILE_n(). | |
| 1067 | -*/ | |
| 1068 | -typedef struct CgiPostReadState_ { | |
| 1069 | - FILE * fh; | |
| 1070 | - unsigned int len; | |
| 1071 | - unsigned int pos; | |
| 1072 | -} CgiPostReadState; | |
| 1073 | - | |
| 1074 | -/* | |
| 1075 | -** cson_data_source_f() impl which reads only up to | |
| 1076 | -** a specified amount of data from its input FILE. | |
| 1077 | -** state MUST be a full populated (CgiPostReadState*). | |
| 1078 | -*/ | |
| 1079 | -static int cson_data_source_FILE_n( void * state, | |
| 1080 | - void * dest, | |
| 1081 | - unsigned int * n ){ | |
| 1082 | - if( ! state || !dest || !n ) return cson_rc.ArgError; | |
| 1083 | - else { | |
| 1084 | - CgiPostReadState * st = (CgiPostReadState *)state; | |
| 1085 | - if( st->pos >= st->len ){ | |
| 1086 | - *n = 0; | |
| 1087 | - return 0; | |
| 1088 | - }else if( !*n || ((st->pos + *n) > st->len) ){ | |
| 1089 | - return cson_rc.RangeError; | |
| 1090 | - }else{ | |
| 1091 | - unsigned int rsz = (unsigned int)fread( dest, 1, *n, st->fh ); | |
| 1092 | - if( ! rsz ){ | |
| 1093 | - *n = rsz; | |
| 1094 | - return feof(st->fh) ? 0 : cson_rc.IOError; | |
| 1095 | - }else{ | |
| 1096 | - *n = rsz; | |
| 1097 | - st->pos += *n; | |
| 1098 | - return 0; | |
| 1099 | - } | |
| 1100 | - } | |
| 1101 | - } | |
| 1102 | -} | |
| 1103 | - | |
| 1104 | -/* | |
| 1105 | -** Reads a JSON object from the first contentLen bytes of zIn. On | |
| 1106 | -** g.json.post is updated to hold the content. On error a | |
| 1107 | -** FSL_JSON_E_INVALID_REQUEST response is output and fossil_exit() is | |
| 1108 | -** called (in HTTP mode exit code 0 is used). | |
| 1109 | -** | |
| 1110 | -** If contentLen is 0 then the whole file is read. | |
| 1111 | -*/ | |
| 1112 | -void cgi_parse_POST_JSON( FILE * zIn, unsigned int contentLen ){ | |
| 1113 | - cson_value * jv = NULL; | |
| 1114 | - int rc; | |
| 1115 | - CgiPostReadState state; | |
| 1066 | +** Reads a JSON object from the given blob, which is assumed to have | |
| 1067 | +** been populated by the caller from stdin, the SSL API, or a file, as | |
| 1068 | +** appropriate for the particular use case. On success g.json.post is | |
| 1069 | +** updated to hold the content. On error a FSL_JSON_E_INVALID_REQUEST | |
| 1070 | +** response is output and fossil_exit() is called (in HTTP mode exit | |
| 1071 | +** code 0 is used). | |
| 1072 | +*/ | |
| 1073 | +void cgi_parse_POST_JSON( Blob * pIn ){ | |
| 1074 | + cson_value * jv = NULL; | |
| 1116 | 1075 | cson_parse_opt popt = cson_parse_opt_empty; |
| 1117 | 1076 | cson_parse_info pinfo = cson_parse_info_empty; |
| 1118 | 1077 | assert(g.json.gc.a && "json_bootstrap_early() was not called!"); |
| 1119 | 1078 | popt.maxDepth = 15; |
| 1120 | - state.fh = zIn; | |
| 1121 | - state.len = contentLen; | |
| 1122 | - state.pos = 0; | |
| 1123 | - rc = cson_parse( &jv, | |
| 1124 | - contentLen ? cson_data_source_FILE_n : cson_data_source_FILE, | |
| 1125 | - contentLen ? (void *)&state : (void *)zIn, &popt, &pinfo ); | |
| 1126 | - if(rc){ | |
| 1079 | + jv = cson_parse_Blob(pIn, &pinfo); | |
| 1080 | + if( jv==NULL ){ | |
| 1127 | 1081 | goto invalidRequest; |
| 1128 | 1082 | }else{ |
| 1129 | 1083 | json_gc_add( "POST.JSON", jv ); |
| 1130 | 1084 | g.json.post.v = jv; |
| 1131 | 1085 | g.json.post.o = cson_value_get_object( jv ); |
| @@ -1140,11 +1094,11 @@ | ||
| 1140 | 1094 | char * msg = mprintf("JSON parse error at line %u, column %u, " |
| 1141 | 1095 | "byte offset %u: %s", |
| 1142 | 1096 | pinfo.line, pinfo.col, pinfo.length, |
| 1143 | 1097 | cson_rc_string(pinfo.errorCode)); |
| 1144 | 1098 | json_err( FSL_JSON_E_INVALID_REQUEST, msg, 1 ); |
| 1145 | - free(msg); | |
| 1099 | + fossil_free(msg); | |
| 1146 | 1100 | }else if(jv && !g.json.post.o){ |
| 1147 | 1101 | json_err( FSL_JSON_E_INVALID_REQUEST, |
| 1148 | 1102 | "Request envelope must be a JSON Object (not array).", 1 ); |
| 1149 | 1103 | }else{ /* generic error message */ |
| 1150 | 1104 | json_err( FSL_JSON_E_INVALID_REQUEST, NULL, 1 ); |
| @@ -1368,28 +1322,21 @@ | ||
| 1368 | 1322 | if( blob_read_from_cgi(&g.cgiIn, len)!=len ){ |
| 1369 | 1323 | malformed_request("CGI content-length mismatch"); |
| 1370 | 1324 | } |
| 1371 | 1325 | blob_uncompress(&g.cgiIn, &g.cgiIn); |
| 1372 | 1326 | } |
| 1373 | -#ifdef FOSSIL_ENABLE_JSON | |
| 1374 | - else if( noJson==0 && g.json.isJsonMode!=0 | |
| 1375 | - && json_can_consume_content_type(zType)!=0 ){ | |
| 1376 | - assert( !g.httpUseSSL ); | |
| 1377 | - cgi_parse_POST_JSON(g.httpIn, (unsigned int)len); | |
| 1378 | - /* | |
| 1379 | - Potential TODOs: | |
| 1380 | - | |
| 1381 | - 1) If parsing fails, immediately return an error response | |
| 1382 | - without dispatching the ostensibly-upcoming JSON API. | |
| 1383 | - */ | |
| 1384 | - cgi_set_content_type(json_guess_content_type()); | |
| 1385 | - } | |
| 1386 | -#endif /* FOSSIL_ENABLE_JSON */ | |
| 1387 | 1327 | else{ |
| 1388 | 1328 | if( blob_read_from_cgi(&g.cgiIn, len)!=len ){ |
| 1389 | 1329 | malformed_request("CGI content-length mismatch"); |
| 1390 | 1330 | } |
| 1331 | +#ifdef FOSSIL_ENABLE_JSON | |
| 1332 | + if( noJson==0 && g.json.isJsonMode!=0 | |
| 1333 | + && json_can_consume_content_type(zType)!=0 ){ | |
| 1334 | + cgi_parse_POST_JSON(&g.cgiIn); | |
| 1335 | + cgi_set_content_type(json_guess_content_type()); | |
| 1336 | + } | |
| 1337 | +#endif /* FOSSIL_ENABLE_JSON */ | |
| 1391 | 1338 | } |
| 1392 | 1339 | } |
| 1393 | 1340 | } |
| 1394 | 1341 | |
| 1395 | 1342 | /* |
| 1396 | 1343 |
| --- src/cgi.c | |
| +++ src/cgi.c | |
| @@ -1061,71 +1061,25 @@ | |
| 1061 | } |
| 1062 | |
| 1063 | |
| 1064 | #ifdef FOSSIL_ENABLE_JSON |
| 1065 | /* |
| 1066 | ** Internal helper for cson_data_source_FILE_n(). |
| 1067 | */ |
| 1068 | typedef struct CgiPostReadState_ { |
| 1069 | FILE * fh; |
| 1070 | unsigned int len; |
| 1071 | unsigned int pos; |
| 1072 | } CgiPostReadState; |
| 1073 | |
| 1074 | /* |
| 1075 | ** cson_data_source_f() impl which reads only up to |
| 1076 | ** a specified amount of data from its input FILE. |
| 1077 | ** state MUST be a full populated (CgiPostReadState*). |
| 1078 | */ |
| 1079 | static int cson_data_source_FILE_n( void * state, |
| 1080 | void * dest, |
| 1081 | unsigned int * n ){ |
| 1082 | if( ! state || !dest || !n ) return cson_rc.ArgError; |
| 1083 | else { |
| 1084 | CgiPostReadState * st = (CgiPostReadState *)state; |
| 1085 | if( st->pos >= st->len ){ |
| 1086 | *n = 0; |
| 1087 | return 0; |
| 1088 | }else if( !*n || ((st->pos + *n) > st->len) ){ |
| 1089 | return cson_rc.RangeError; |
| 1090 | }else{ |
| 1091 | unsigned int rsz = (unsigned int)fread( dest, 1, *n, st->fh ); |
| 1092 | if( ! rsz ){ |
| 1093 | *n = rsz; |
| 1094 | return feof(st->fh) ? 0 : cson_rc.IOError; |
| 1095 | }else{ |
| 1096 | *n = rsz; |
| 1097 | st->pos += *n; |
| 1098 | return 0; |
| 1099 | } |
| 1100 | } |
| 1101 | } |
| 1102 | } |
| 1103 | |
| 1104 | /* |
| 1105 | ** Reads a JSON object from the first contentLen bytes of zIn. On |
| 1106 | ** g.json.post is updated to hold the content. On error a |
| 1107 | ** FSL_JSON_E_INVALID_REQUEST response is output and fossil_exit() is |
| 1108 | ** called (in HTTP mode exit code 0 is used). |
| 1109 | ** |
| 1110 | ** If contentLen is 0 then the whole file is read. |
| 1111 | */ |
| 1112 | void cgi_parse_POST_JSON( FILE * zIn, unsigned int contentLen ){ |
| 1113 | cson_value * jv = NULL; |
| 1114 | int rc; |
| 1115 | CgiPostReadState state; |
| 1116 | cson_parse_opt popt = cson_parse_opt_empty; |
| 1117 | cson_parse_info pinfo = cson_parse_info_empty; |
| 1118 | assert(g.json.gc.a && "json_bootstrap_early() was not called!"); |
| 1119 | popt.maxDepth = 15; |
| 1120 | state.fh = zIn; |
| 1121 | state.len = contentLen; |
| 1122 | state.pos = 0; |
| 1123 | rc = cson_parse( &jv, |
| 1124 | contentLen ? cson_data_source_FILE_n : cson_data_source_FILE, |
| 1125 | contentLen ? (void *)&state : (void *)zIn, &popt, &pinfo ); |
| 1126 | if(rc){ |
| 1127 | goto invalidRequest; |
| 1128 | }else{ |
| 1129 | json_gc_add( "POST.JSON", jv ); |
| 1130 | g.json.post.v = jv; |
| 1131 | g.json.post.o = cson_value_get_object( jv ); |
| @@ -1140,11 +1094,11 @@ | |
| 1140 | char * msg = mprintf("JSON parse error at line %u, column %u, " |
| 1141 | "byte offset %u: %s", |
| 1142 | pinfo.line, pinfo.col, pinfo.length, |
| 1143 | cson_rc_string(pinfo.errorCode)); |
| 1144 | json_err( FSL_JSON_E_INVALID_REQUEST, msg, 1 ); |
| 1145 | free(msg); |
| 1146 | }else if(jv && !g.json.post.o){ |
| 1147 | json_err( FSL_JSON_E_INVALID_REQUEST, |
| 1148 | "Request envelope must be a JSON Object (not array).", 1 ); |
| 1149 | }else{ /* generic error message */ |
| 1150 | json_err( FSL_JSON_E_INVALID_REQUEST, NULL, 1 ); |
| @@ -1368,28 +1322,21 @@ | |
| 1368 | if( blob_read_from_cgi(&g.cgiIn, len)!=len ){ |
| 1369 | malformed_request("CGI content-length mismatch"); |
| 1370 | } |
| 1371 | blob_uncompress(&g.cgiIn, &g.cgiIn); |
| 1372 | } |
| 1373 | #ifdef FOSSIL_ENABLE_JSON |
| 1374 | else if( noJson==0 && g.json.isJsonMode!=0 |
| 1375 | && json_can_consume_content_type(zType)!=0 ){ |
| 1376 | assert( !g.httpUseSSL ); |
| 1377 | cgi_parse_POST_JSON(g.httpIn, (unsigned int)len); |
| 1378 | /* |
| 1379 | Potential TODOs: |
| 1380 | |
| 1381 | 1) If parsing fails, immediately return an error response |
| 1382 | without dispatching the ostensibly-upcoming JSON API. |
| 1383 | */ |
| 1384 | cgi_set_content_type(json_guess_content_type()); |
| 1385 | } |
| 1386 | #endif /* FOSSIL_ENABLE_JSON */ |
| 1387 | else{ |
| 1388 | if( blob_read_from_cgi(&g.cgiIn, len)!=len ){ |
| 1389 | malformed_request("CGI content-length mismatch"); |
| 1390 | } |
| 1391 | } |
| 1392 | } |
| 1393 | } |
| 1394 | |
| 1395 | /* |
| 1396 |
| --- src/cgi.c | |
| +++ src/cgi.c | |
| @@ -1061,71 +1061,25 @@ | |
| 1061 | } |
| 1062 | |
| 1063 | |
| 1064 | #ifdef FOSSIL_ENABLE_JSON |
| 1065 | /* |
| 1066 | ** Reads a JSON object from the given blob, which is assumed to have |
| 1067 | ** been populated by the caller from stdin, the SSL API, or a file, as |
| 1068 | ** appropriate for the particular use case. On success g.json.post is |
| 1069 | ** updated to hold the content. On error a FSL_JSON_E_INVALID_REQUEST |
| 1070 | ** response is output and fossil_exit() is called (in HTTP mode exit |
| 1071 | ** code 0 is used). |
| 1072 | */ |
| 1073 | void cgi_parse_POST_JSON( Blob * pIn ){ |
| 1074 | cson_value * jv = NULL; |
| 1075 | cson_parse_opt popt = cson_parse_opt_empty; |
| 1076 | cson_parse_info pinfo = cson_parse_info_empty; |
| 1077 | assert(g.json.gc.a && "json_bootstrap_early() was not called!"); |
| 1078 | popt.maxDepth = 15; |
| 1079 | jv = cson_parse_Blob(pIn, &pinfo); |
| 1080 | if( jv==NULL ){ |
| 1081 | goto invalidRequest; |
| 1082 | }else{ |
| 1083 | json_gc_add( "POST.JSON", jv ); |
| 1084 | g.json.post.v = jv; |
| 1085 | g.json.post.o = cson_value_get_object( jv ); |
| @@ -1140,11 +1094,11 @@ | |
| 1094 | char * msg = mprintf("JSON parse error at line %u, column %u, " |
| 1095 | "byte offset %u: %s", |
| 1096 | pinfo.line, pinfo.col, pinfo.length, |
| 1097 | cson_rc_string(pinfo.errorCode)); |
| 1098 | json_err( FSL_JSON_E_INVALID_REQUEST, msg, 1 ); |
| 1099 | fossil_free(msg); |
| 1100 | }else if(jv && !g.json.post.o){ |
| 1101 | json_err( FSL_JSON_E_INVALID_REQUEST, |
| 1102 | "Request envelope must be a JSON Object (not array).", 1 ); |
| 1103 | }else{ /* generic error message */ |
| 1104 | json_err( FSL_JSON_E_INVALID_REQUEST, NULL, 1 ); |
| @@ -1368,28 +1322,21 @@ | |
| 1322 | if( blob_read_from_cgi(&g.cgiIn, len)!=len ){ |
| 1323 | malformed_request("CGI content-length mismatch"); |
| 1324 | } |
| 1325 | blob_uncompress(&g.cgiIn, &g.cgiIn); |
| 1326 | } |
| 1327 | else{ |
| 1328 | if( blob_read_from_cgi(&g.cgiIn, len)!=len ){ |
| 1329 | malformed_request("CGI content-length mismatch"); |
| 1330 | } |
| 1331 | #ifdef FOSSIL_ENABLE_JSON |
| 1332 | if( noJson==0 && g.json.isJsonMode!=0 |
| 1333 | && json_can_consume_content_type(zType)!=0 ){ |
| 1334 | cgi_parse_POST_JSON(&g.cgiIn); |
| 1335 | cgi_set_content_type(json_guess_content_type()); |
| 1336 | } |
| 1337 | #endif /* FOSSIL_ENABLE_JSON */ |
| 1338 | } |
| 1339 | } |
| 1340 | } |
| 1341 | |
| 1342 | /* |
| 1343 |
+6
-15
| --- src/json.c | ||
| +++ src/json.c | ||
| @@ -1,8 +1,8 @@ | ||
| 1 | 1 | #ifdef FOSSIL_ENABLE_JSON |
| 2 | 2 | /* |
| 3 | -** Copyright (c) 2011 D. Richard Hipp | |
| 3 | +** Copyright (c) 2011-2022 D. Richard Hipp | |
| 4 | 4 | ** |
| 5 | 5 | ** This program is free software; you can redistribute it and/or |
| 6 | 6 | ** modify it under the terms of the Simplified BSD License (also |
| 7 | 7 | ** known as the "2-Clause License" or "FreeBSD License".) |
| 8 | 8 | |
| @@ -172,21 +172,10 @@ | ||
| 172 | 172 | Blob * b = (Blob*)pState; |
| 173 | 173 | blob_append( b, (char const *)src, (int)n ) /* will die on OOM */; |
| 174 | 174 | return 0; |
| 175 | 175 | } |
| 176 | 176 | |
| 177 | -/* | |
| 178 | -** Implements the cson_data_source_f() interface and reads input from | |
| 179 | -** a fossil Blob object. pState must be-a (Blob*) populated with JSON | |
| 180 | -** data. | |
| 181 | -*/ | |
| 182 | -int cson_data_src_Blob(void * pState, void * dest, unsigned int * n){ | |
| 183 | - Blob * b = (Blob*)pState; | |
| 184 | - *n = blob_read( b, dest, *n ); | |
| 185 | - return 0; | |
| 186 | -} | |
| 187 | - | |
| 188 | 177 | /* |
| 189 | 178 | ** Convenience wrapper around cson_output() which appends the output |
| 190 | 179 | ** to pDest. pOpt may be NULL, in which case g.json.outOpt will be used. |
| 191 | 180 | */ |
| 192 | 181 | int cson_output_Blob( cson_value const * pVal, Blob * pDest, cson_output_opt const * pOpt ){ |
| @@ -204,12 +193,11 @@ | ||
| 204 | 193 | ** On success a new JSON Object or Array is returned (owned by the |
| 205 | 194 | ** caller). On error NULL is returned. |
| 206 | 195 | */ |
| 207 | 196 | cson_value * cson_parse_Blob( Blob * pSrc, cson_parse_info * pInfo ){ |
| 208 | 197 | cson_value * root = NULL; |
| 209 | - blob_rewind( pSrc ); | |
| 210 | - cson_parse( &root, cson_data_src_Blob, pSrc, NULL, pInfo ); | |
| 198 | + cson_parse_string( &root, blob_str(pSrc), blob_size(pSrc), NULL, pInfo ); | |
| 211 | 199 | return root; |
| 212 | 200 | } |
| 213 | 201 | |
| 214 | 202 | /* |
| 215 | 203 | ** Implements the cson_data_dest_f() interface and outputs the data to |
| @@ -1065,10 +1053,11 @@ | ||
| 1065 | 1053 | because outOpt cannot (generically) be configured until after |
| 1066 | 1054 | POST-reading is finished. |
| 1067 | 1055 | */ |
| 1068 | 1056 | FILE * inFile = NULL; |
| 1069 | 1057 | char const * jfile = find_option("json-input",NULL,1); |
| 1058 | + Blob json = BLOB_INITIALIZER; | |
| 1070 | 1059 | if(!jfile || !*jfile){ |
| 1071 | 1060 | break; |
| 1072 | 1061 | } |
| 1073 | 1062 | inFile = (0==strcmp("-",jfile)) |
| 1074 | 1063 | ? stdin |
| @@ -1077,12 +1066,14 @@ | ||
| 1077 | 1066 | g.json.resultCode = FSL_JSON_E_FILE_OPEN_FAILED; |
| 1078 | 1067 | fossil_fatal("Could not open JSON file [%s].",jfile) |
| 1079 | 1068 | /* Does not return. */ |
| 1080 | 1069 | ; |
| 1081 | 1070 | } |
| 1082 | - cgi_parse_POST_JSON(inFile, 0); | |
| 1071 | + blob_read_from_channel(&json, inFile, -1); | |
| 1083 | 1072 | fossil_fclose(inFile); |
| 1073 | + cgi_parse_POST_JSON(&json); | |
| 1074 | + blob_reset(&json); | |
| 1084 | 1075 | break; |
| 1085 | 1076 | } |
| 1086 | 1077 | |
| 1087 | 1078 | /* g.json.reqPayload exists only to simplify some of our access to |
| 1088 | 1079 | the request payload. We currently only use this in the context of |
| 1089 | 1080 |
| --- src/json.c | |
| +++ src/json.c | |
| @@ -1,8 +1,8 @@ | |
| 1 | #ifdef FOSSIL_ENABLE_JSON |
| 2 | /* |
| 3 | ** Copyright (c) 2011 D. Richard Hipp |
| 4 | ** |
| 5 | ** This program is free software; you can redistribute it and/or |
| 6 | ** modify it under the terms of the Simplified BSD License (also |
| 7 | ** known as the "2-Clause License" or "FreeBSD License".) |
| 8 | |
| @@ -172,21 +172,10 @@ | |
| 172 | Blob * b = (Blob*)pState; |
| 173 | blob_append( b, (char const *)src, (int)n ) /* will die on OOM */; |
| 174 | return 0; |
| 175 | } |
| 176 | |
| 177 | /* |
| 178 | ** Implements the cson_data_source_f() interface and reads input from |
| 179 | ** a fossil Blob object. pState must be-a (Blob*) populated with JSON |
| 180 | ** data. |
| 181 | */ |
| 182 | int cson_data_src_Blob(void * pState, void * dest, unsigned int * n){ |
| 183 | Blob * b = (Blob*)pState; |
| 184 | *n = blob_read( b, dest, *n ); |
| 185 | return 0; |
| 186 | } |
| 187 | |
| 188 | /* |
| 189 | ** Convenience wrapper around cson_output() which appends the output |
| 190 | ** to pDest. pOpt may be NULL, in which case g.json.outOpt will be used. |
| 191 | */ |
| 192 | int cson_output_Blob( cson_value const * pVal, Blob * pDest, cson_output_opt const * pOpt ){ |
| @@ -204,12 +193,11 @@ | |
| 204 | ** On success a new JSON Object or Array is returned (owned by the |
| 205 | ** caller). On error NULL is returned. |
| 206 | */ |
| 207 | cson_value * cson_parse_Blob( Blob * pSrc, cson_parse_info * pInfo ){ |
| 208 | cson_value * root = NULL; |
| 209 | blob_rewind( pSrc ); |
| 210 | cson_parse( &root, cson_data_src_Blob, pSrc, NULL, pInfo ); |
| 211 | return root; |
| 212 | } |
| 213 | |
| 214 | /* |
| 215 | ** Implements the cson_data_dest_f() interface and outputs the data to |
| @@ -1065,10 +1053,11 @@ | |
| 1065 | because outOpt cannot (generically) be configured until after |
| 1066 | POST-reading is finished. |
| 1067 | */ |
| 1068 | FILE * inFile = NULL; |
| 1069 | char const * jfile = find_option("json-input",NULL,1); |
| 1070 | if(!jfile || !*jfile){ |
| 1071 | break; |
| 1072 | } |
| 1073 | inFile = (0==strcmp("-",jfile)) |
| 1074 | ? stdin |
| @@ -1077,12 +1066,14 @@ | |
| 1077 | g.json.resultCode = FSL_JSON_E_FILE_OPEN_FAILED; |
| 1078 | fossil_fatal("Could not open JSON file [%s].",jfile) |
| 1079 | /* Does not return. */ |
| 1080 | ; |
| 1081 | } |
| 1082 | cgi_parse_POST_JSON(inFile, 0); |
| 1083 | fossil_fclose(inFile); |
| 1084 | break; |
| 1085 | } |
| 1086 | |
| 1087 | /* g.json.reqPayload exists only to simplify some of our access to |
| 1088 | the request payload. We currently only use this in the context of |
| 1089 |
| --- src/json.c | |
| +++ src/json.c | |
| @@ -1,8 +1,8 @@ | |
| 1 | #ifdef FOSSIL_ENABLE_JSON |
| 2 | /* |
| 3 | ** Copyright (c) 2011-2022 D. Richard Hipp |
| 4 | ** |
| 5 | ** This program is free software; you can redistribute it and/or |
| 6 | ** modify it under the terms of the Simplified BSD License (also |
| 7 | ** known as the "2-Clause License" or "FreeBSD License".) |
| 8 | |
| @@ -172,21 +172,10 @@ | |
| 172 | Blob * b = (Blob*)pState; |
| 173 | blob_append( b, (char const *)src, (int)n ) /* will die on OOM */; |
| 174 | return 0; |
| 175 | } |
| 176 | |
| 177 | /* |
| 178 | ** Convenience wrapper around cson_output() which appends the output |
| 179 | ** to pDest. pOpt may be NULL, in which case g.json.outOpt will be used. |
| 180 | */ |
| 181 | int cson_output_Blob( cson_value const * pVal, Blob * pDest, cson_output_opt const * pOpt ){ |
| @@ -204,12 +193,11 @@ | |
| 193 | ** On success a new JSON Object or Array is returned (owned by the |
| 194 | ** caller). On error NULL is returned. |
| 195 | */ |
| 196 | cson_value * cson_parse_Blob( Blob * pSrc, cson_parse_info * pInfo ){ |
| 197 | cson_value * root = NULL; |
| 198 | cson_parse_string( &root, blob_str(pSrc), blob_size(pSrc), NULL, pInfo ); |
| 199 | return root; |
| 200 | } |
| 201 | |
| 202 | /* |
| 203 | ** Implements the cson_data_dest_f() interface and outputs the data to |
| @@ -1065,10 +1053,11 @@ | |
| 1053 | because outOpt cannot (generically) be configured until after |
| 1054 | POST-reading is finished. |
| 1055 | */ |
| 1056 | FILE * inFile = NULL; |
| 1057 | char const * jfile = find_option("json-input",NULL,1); |
| 1058 | Blob json = BLOB_INITIALIZER; |
| 1059 | if(!jfile || !*jfile){ |
| 1060 | break; |
| 1061 | } |
| 1062 | inFile = (0==strcmp("-",jfile)) |
| 1063 | ? stdin |
| @@ -1077,12 +1066,14 @@ | |
| 1066 | g.json.resultCode = FSL_JSON_E_FILE_OPEN_FAILED; |
| 1067 | fossil_fatal("Could not open JSON file [%s].",jfile) |
| 1068 | /* Does not return. */ |
| 1069 | ; |
| 1070 | } |
| 1071 | blob_read_from_channel(&json, inFile, -1); |
| 1072 | fossil_fclose(inFile); |
| 1073 | cgi_parse_POST_JSON(&json); |
| 1074 | blob_reset(&json); |
| 1075 | break; |
| 1076 | } |
| 1077 | |
| 1078 | /* g.json.reqPayload exists only to simplify some of our access to |
| 1079 | the request payload. We currently only use this in the context of |
| 1080 |
+27
-6
| --- src/json_wiki.c | ||
| +++ src/json_wiki.c | ||
| @@ -273,30 +273,51 @@ | ||
| 273 | 273 | return json_wiki_get_by_name_or_symname( zPageName, zSymName, contentFormat ); |
| 274 | 274 | } |
| 275 | 275 | |
| 276 | 276 | /* |
| 277 | 277 | ** Implementation of /json/wiki/preview. |
| 278 | -** | |
| 279 | 278 | */ |
| 280 | 279 | static cson_value * json_wiki_preview(){ |
| 281 | 280 | char const * zContent = NULL; |
| 281 | + char const * zMime = NULL; | |
| 282 | + cson_string * sContent = NULL; | |
| 282 | 283 | cson_value * pay = NULL; |
| 283 | 284 | Blob contentOrig = empty_blob; |
| 284 | 285 | Blob contentHtml = empty_blob; |
| 285 | 286 | if( !g.perm.WrWiki ){ |
| 286 | 287 | json_set_err(FSL_JSON_E_DENIED, |
| 287 | 288 | "Requires 'k' access."); |
| 288 | 289 | return NULL; |
| 289 | 290 | } |
| 290 | - zContent = cson_string_cstr(cson_value_get_string(g.json.reqPayload.v)); | |
| 291 | - if(!zContent) { | |
| 291 | + | |
| 292 | + if(g.json.reqPayload.o){ | |
| 293 | + sContent = cson_value_get_string( | |
| 294 | + cson_object_get(g.json.reqPayload.o, "body")); | |
| 295 | + zMime = cson_value_get_cstr(cson_object_get(g.json.reqPayload.o, | |
| 296 | + "mimetype")); | |
| 297 | + }else{ | |
| 298 | + sContent = cson_value_get_string(g.json.reqPayload.v); | |
| 299 | + } | |
| 300 | + if(!sContent) { | |
| 292 | 301 | json_set_err(FSL_JSON_E_MISSING_ARGS, |
| 293 | - "The 'payload' property must be a string containing the wiki code to preview."); | |
| 302 | + "The 'payload' property must be either a string containing the " | |
| 303 | + "Fossil wiki code to preview or an object with body + mimetype " | |
| 304 | + "properties."); | |
| 294 | 305 | return NULL; |
| 295 | 306 | } |
| 296 | - blob_append( &contentOrig, zContent, (int)cson_string_length_bytes(cson_value_get_string(g.json.reqPayload.v)) ); | |
| 297 | - wiki_convert( &contentOrig, &contentHtml, 0 ); | |
| 307 | + zContent = cson_string_cstr(sContent); | |
| 308 | + blob_append( &contentOrig, zContent, (int)cson_string_length_bytes(sContent) ); | |
| 309 | + zMime = wiki_filter_mimetypes(zMime); | |
| 310 | + if( 0==fossil_strcmp(zMime, "text/x-markdown") ){ | |
| 311 | + markdown_to_html(&contentOrig, 0, &contentHtml); | |
| 312 | + }else if( 0==fossil_strcmp(zMime, "text/plain") ){ | |
| 313 | + blob_append(&contentHtml, "<pre class='textPlain'>", -1); | |
| 314 | + blob_append(&contentHtml, blob_str(&contentOrig), blob_size(&contentOrig)); | |
| 315 | + blob_append(&contentHtml, "</pre>", -1); | |
| 316 | + }else{ | |
| 317 | + wiki_convert( &contentOrig, &contentHtml, 0 ); | |
| 318 | + } | |
| 298 | 319 | blob_reset( &contentOrig ); |
| 299 | 320 | pay = cson_value_new_string( blob_str(&contentHtml), (unsigned int)blob_size(&contentHtml)); |
| 300 | 321 | blob_reset( &contentHtml ); |
| 301 | 322 | return pay; |
| 302 | 323 | } |
| 303 | 324 |
| --- src/json_wiki.c | |
| +++ src/json_wiki.c | |
| @@ -273,30 +273,51 @@ | |
| 273 | return json_wiki_get_by_name_or_symname( zPageName, zSymName, contentFormat ); |
| 274 | } |
| 275 | |
| 276 | /* |
| 277 | ** Implementation of /json/wiki/preview. |
| 278 | ** |
| 279 | */ |
| 280 | static cson_value * json_wiki_preview(){ |
| 281 | char const * zContent = NULL; |
| 282 | cson_value * pay = NULL; |
| 283 | Blob contentOrig = empty_blob; |
| 284 | Blob contentHtml = empty_blob; |
| 285 | if( !g.perm.WrWiki ){ |
| 286 | json_set_err(FSL_JSON_E_DENIED, |
| 287 | "Requires 'k' access."); |
| 288 | return NULL; |
| 289 | } |
| 290 | zContent = cson_string_cstr(cson_value_get_string(g.json.reqPayload.v)); |
| 291 | if(!zContent) { |
| 292 | json_set_err(FSL_JSON_E_MISSING_ARGS, |
| 293 | "The 'payload' property must be a string containing the wiki code to preview."); |
| 294 | return NULL; |
| 295 | } |
| 296 | blob_append( &contentOrig, zContent, (int)cson_string_length_bytes(cson_value_get_string(g.json.reqPayload.v)) ); |
| 297 | wiki_convert( &contentOrig, &contentHtml, 0 ); |
| 298 | blob_reset( &contentOrig ); |
| 299 | pay = cson_value_new_string( blob_str(&contentHtml), (unsigned int)blob_size(&contentHtml)); |
| 300 | blob_reset( &contentHtml ); |
| 301 | return pay; |
| 302 | } |
| 303 |
| --- src/json_wiki.c | |
| +++ src/json_wiki.c | |
| @@ -273,30 +273,51 @@ | |
| 273 | return json_wiki_get_by_name_or_symname( zPageName, zSymName, contentFormat ); |
| 274 | } |
| 275 | |
| 276 | /* |
| 277 | ** Implementation of /json/wiki/preview. |
| 278 | */ |
| 279 | static cson_value * json_wiki_preview(){ |
| 280 | char const * zContent = NULL; |
| 281 | char const * zMime = NULL; |
| 282 | cson_string * sContent = NULL; |
| 283 | cson_value * pay = NULL; |
| 284 | Blob contentOrig = empty_blob; |
| 285 | Blob contentHtml = empty_blob; |
| 286 | if( !g.perm.WrWiki ){ |
| 287 | json_set_err(FSL_JSON_E_DENIED, |
| 288 | "Requires 'k' access."); |
| 289 | return NULL; |
| 290 | } |
| 291 | |
| 292 | if(g.json.reqPayload.o){ |
| 293 | sContent = cson_value_get_string( |
| 294 | cson_object_get(g.json.reqPayload.o, "body")); |
| 295 | zMime = cson_value_get_cstr(cson_object_get(g.json.reqPayload.o, |
| 296 | "mimetype")); |
| 297 | }else{ |
| 298 | sContent = cson_value_get_string(g.json.reqPayload.v); |
| 299 | } |
| 300 | if(!sContent) { |
| 301 | json_set_err(FSL_JSON_E_MISSING_ARGS, |
| 302 | "The 'payload' property must be either a string containing the " |
| 303 | "Fossil wiki code to preview or an object with body + mimetype " |
| 304 | "properties."); |
| 305 | return NULL; |
| 306 | } |
| 307 | zContent = cson_string_cstr(sContent); |
| 308 | blob_append( &contentOrig, zContent, (int)cson_string_length_bytes(sContent) ); |
| 309 | zMime = wiki_filter_mimetypes(zMime); |
| 310 | if( 0==fossil_strcmp(zMime, "text/x-markdown") ){ |
| 311 | markdown_to_html(&contentOrig, 0, &contentHtml); |
| 312 | }else if( 0==fossil_strcmp(zMime, "text/plain") ){ |
| 313 | blob_append(&contentHtml, "<pre class='textPlain'>", -1); |
| 314 | blob_append(&contentHtml, blob_str(&contentOrig), blob_size(&contentOrig)); |
| 315 | blob_append(&contentHtml, "</pre>", -1); |
| 316 | }else{ |
| 317 | wiki_convert( &contentOrig, &contentHtml, 0 ); |
| 318 | } |
| 319 | blob_reset( &contentOrig ); |
| 320 | pay = cson_value_new_string( blob_str(&contentHtml), (unsigned int)blob_size(&contentHtml)); |
| 321 | blob_reset( &contentHtml ); |
| 322 | return pay; |
| 323 | } |
| 324 |
+16
-11
| --- www/json-api/api-wiki.md | ||
| +++ www/json-api/api-wiki.md | ||
| @@ -166,10 +166,13 @@ | ||
| 166 | 166 | - `name=string` specifies the page name. |
| 167 | 167 | - `content=string` is the body text.\ |
| 168 | 168 | Content is required for save (unless `createIfNotExists` is true *and* |
| 169 | 169 | the page does not exist), optional for create. It *may* be an empty |
| 170 | 170 | string. |
| 171 | +- `mimetype=string` specifies the mimetype for the body, noting any any | |
| 172 | + unrecognized/unsupported mimetype is silently treated as | |
| 173 | + `text/x-fossil-wiki`. | |
| 171 | 174 | - Save (not create) supports a `createIfNotExists` boolean option which |
| 172 | 175 | makes it a functional superset of the create/save features. i.e. it |
| 173 | 176 | will create if needed, else it will update. If createIfNotExists is |
| 174 | 177 | false (the default) then save will fail if given a page name which |
| 175 | 178 | does not refer to an existing page. |
| @@ -179,10 +182,11 @@ | ||
| 179 | 182 | high-priority addition. See:\ |
| 180 | 183 | [](/doc/trunk/www/fileformat.wiki#wikichng) |
| 181 | 184 | - **Potential TODO:** we *could* optionally also support |
| 182 | 185 | multi-page saving using an array of pages in the request payload:\ |
| 183 | 186 | `[… page objects … ]` |
| 187 | + | |
| 184 | 188 | |
| 185 | 189 | |
| 186 | 190 | <a id="diffs"></a> |
| 187 | 191 | # Wiki Diffs |
| 188 | 192 | |
| @@ -238,28 +242,29 @@ | ||
| 238 | 242 | |
| 239 | 243 | This command wiki-processes arbitrary text sent from the client. To help |
| 240 | 244 | curb potential abuse, its use is restricted to those with "k" access |
| 241 | 245 | rights. |
| 242 | 246 | |
| 243 | -The `POST.payload` property must be a string containing Fossil wiki | |
| 244 | -markup. The response payload is also a string, but contains the | |
| 245 | -HTML-processed form of the string. Whether or not "all HTML" is allowed | |
| 246 | -depends on site-level configuration options, and that changes how the | |
| 247 | -input is processed. | |
| 247 | +The `POST.payload` property must be either: | |
| 248 | + | |
| 249 | +1) A string containing Fossil wiki markup. | |
| 250 | + | |
| 251 | +2) An Object with a `body` property holding the text to render and a | |
| 252 | + `mimetype` property describing the wiki format: | |
| 253 | + `text/x-fossil-wiki` (the default), `text/x-markdown`, or | |
| 254 | + `text/plain`. Any unknown type is treated as `text/x-fossil-wiki`. | |
| 255 | + | |
| 256 | +The response payload is a string containing the rendered page. Whether | |
| 257 | +or not "all HTML" is allowed depends on site-level configuration | |
| 258 | +options, and that changes how the input is processed. | |
| 248 | 259 | |
| 249 | 260 | Note that the links in the generated page are for the HTML interface, |
| 250 | 261 | and will not work as-is for arbitrary JSON clients. In order to |
| 251 | 262 | integrate the parsed content with JSON-based clients the HTML will |
| 252 | 263 | probably need to be post-processed, e.g. using jQuery to fish out the |
| 253 | 264 | links and re-map wiki page links to a JSON-capable page handler. |
| 254 | 265 | |
| 255 | -**TODO**: Update this to accept the other two wiki formats (which | |
| 256 | -didn't exist when this API was implemented): markdown and plain text | |
| 257 | -(which gets HTML-ized for preview purposes). That requires changing | |
| 258 | -the payload to an object, perhaps simply submitting the same thing as | |
| 259 | -`/json/save`. There's no reason we can't support both call forms. | |
| 260 | - | |
| 261 | 266 | |
| 262 | 267 | <a id="todo"></a> |
| 263 | 268 | # Notes and TODOs |
| 264 | 269 | |
| 265 | 270 | - When server-parsing the wiki content, the generated |
| 266 | 271 |
| --- www/json-api/api-wiki.md | |
| +++ www/json-api/api-wiki.md | |
| @@ -166,10 +166,13 @@ | |
| 166 | - `name=string` specifies the page name. |
| 167 | - `content=string` is the body text.\ |
| 168 | Content is required for save (unless `createIfNotExists` is true *and* |
| 169 | the page does not exist), optional for create. It *may* be an empty |
| 170 | string. |
| 171 | - Save (not create) supports a `createIfNotExists` boolean option which |
| 172 | makes it a functional superset of the create/save features. i.e. it |
| 173 | will create if needed, else it will update. If createIfNotExists is |
| 174 | false (the default) then save will fail if given a page name which |
| 175 | does not refer to an existing page. |
| @@ -179,10 +182,11 @@ | |
| 179 | high-priority addition. See:\ |
| 180 | [](/doc/trunk/www/fileformat.wiki#wikichng) |
| 181 | - **Potential TODO:** we *could* optionally also support |
| 182 | multi-page saving using an array of pages in the request payload:\ |
| 183 | `[… page objects … ]` |
| 184 | |
| 185 | |
| 186 | <a id="diffs"></a> |
| 187 | # Wiki Diffs |
| 188 | |
| @@ -238,28 +242,29 @@ | |
| 238 | |
| 239 | This command wiki-processes arbitrary text sent from the client. To help |
| 240 | curb potential abuse, its use is restricted to those with "k" access |
| 241 | rights. |
| 242 | |
| 243 | The `POST.payload` property must be a string containing Fossil wiki |
| 244 | markup. The response payload is also a string, but contains the |
| 245 | HTML-processed form of the string. Whether or not "all HTML" is allowed |
| 246 | depends on site-level configuration options, and that changes how the |
| 247 | input is processed. |
| 248 | |
| 249 | Note that the links in the generated page are for the HTML interface, |
| 250 | and will not work as-is for arbitrary JSON clients. In order to |
| 251 | integrate the parsed content with JSON-based clients the HTML will |
| 252 | probably need to be post-processed, e.g. using jQuery to fish out the |
| 253 | links and re-map wiki page links to a JSON-capable page handler. |
| 254 | |
| 255 | **TODO**: Update this to accept the other two wiki formats (which |
| 256 | didn't exist when this API was implemented): markdown and plain text |
| 257 | (which gets HTML-ized for preview purposes). That requires changing |
| 258 | the payload to an object, perhaps simply submitting the same thing as |
| 259 | `/json/save`. There's no reason we can't support both call forms. |
| 260 | |
| 261 | |
| 262 | <a id="todo"></a> |
| 263 | # Notes and TODOs |
| 264 | |
| 265 | - When server-parsing the wiki content, the generated |
| 266 |
| --- www/json-api/api-wiki.md | |
| +++ www/json-api/api-wiki.md | |
| @@ -166,10 +166,13 @@ | |
| 166 | - `name=string` specifies the page name. |
| 167 | - `content=string` is the body text.\ |
| 168 | Content is required for save (unless `createIfNotExists` is true *and* |
| 169 | the page does not exist), optional for create. It *may* be an empty |
| 170 | string. |
| 171 | - `mimetype=string` specifies the mimetype for the body, noting any any |
| 172 | unrecognized/unsupported mimetype is silently treated as |
| 173 | `text/x-fossil-wiki`. |
| 174 | - Save (not create) supports a `createIfNotExists` boolean option which |
| 175 | makes it a functional superset of the create/save features. i.e. it |
| 176 | will create if needed, else it will update. If createIfNotExists is |
| 177 | false (the default) then save will fail if given a page name which |
| 178 | does not refer to an existing page. |
| @@ -179,10 +182,11 @@ | |
| 182 | high-priority addition. See:\ |
| 183 | [](/doc/trunk/www/fileformat.wiki#wikichng) |
| 184 | - **Potential TODO:** we *could* optionally also support |
| 185 | multi-page saving using an array of pages in the request payload:\ |
| 186 | `[… page objects … ]` |
| 187 | |
| 188 | |
| 189 | |
| 190 | <a id="diffs"></a> |
| 191 | # Wiki Diffs |
| 192 | |
| @@ -238,28 +242,29 @@ | |
| 242 | |
| 243 | This command wiki-processes arbitrary text sent from the client. To help |
| 244 | curb potential abuse, its use is restricted to those with "k" access |
| 245 | rights. |
| 246 | |
| 247 | The `POST.payload` property must be either: |
| 248 | |
| 249 | 1) A string containing Fossil wiki markup. |
| 250 | |
| 251 | 2) An Object with a `body` property holding the text to render and a |
| 252 | `mimetype` property describing the wiki format: |
| 253 | `text/x-fossil-wiki` (the default), `text/x-markdown`, or |
| 254 | `text/plain`. Any unknown type is treated as `text/x-fossil-wiki`. |
| 255 | |
| 256 | The response payload is a string containing the rendered page. Whether |
| 257 | or not "all HTML" is allowed depends on site-level configuration |
| 258 | options, and that changes how the input is processed. |
| 259 | |
| 260 | Note that the links in the generated page are for the HTML interface, |
| 261 | and will not work as-is for arbitrary JSON clients. In order to |
| 262 | integrate the parsed content with JSON-based clients the HTML will |
| 263 | probably need to be post-processed, e.g. using jQuery to fish out the |
| 264 | links and re-map wiki page links to a JSON-capable page handler. |
| 265 | |
| 266 | |
| 267 | <a id="todo"></a> |
| 268 | # Notes and TODOs |
| 269 | |
| 270 | - When server-parsing the wiki content, the generated |
| 271 |