Fossil SCM

Add /json/settings/set. Replace several free() calls with fossil_free(). Work around json_send_response() being called twice in some CLI-based cases.

stephan 2023-01-20 04:25 json-settings-command
Commit 4d2aeb29eb0d9a2a3f1d39990f6f7b9925f7715505b9b1a9f8919c51eef1fb7c
+13 -6
--- src/json.c
+++ src/json.c
@@ -288,11 +288,11 @@
288288
va_list vargs;
289289
va_start(vargs,fmt);
290290
zStr = vmprintf(fmt,vargs);
291291
va_end(vargs);
292292
v = cson_value_new_string(zStr, strlen(zStr));
293
- free(zStr);
293
+ fossil_free(zStr);
294294
return v;
295295
}
296296
297297
cson_value * json_new_int( i64 v ){
298298
return cson_value_new_integer((cson_int_t)v);
@@ -630,13 +630,20 @@
630630
** is not called to flush the output.
631631
**
632632
** If g.json.jsonp is not NULL then the content type is set to
633633
** text/javascript and the output is wrapped in a jsonp
634634
** wrapper.
635
+**
636
+** This function works only the first time it is called. It "should
637
+** not" ever be called more than once but certain calling
638
+** constellations might trigger that, in which case the second and
639
+** subsequent calls are no-ops.
635640
*/
636641
void json_send_response( cson_value const * pResponse ){
642
+ static int once = 0;
637643
assert( NULL != pResponse );
644
+ if( once++ ) return;
638645
if( g.isHTTP ){
639646
cgi_reset_content();
640647
if( g.json.jsonp ){
641648
cgi_set_content_type("text/javascript");
642649
cgi_printf("%s(",g.json.jsonp);
@@ -844,11 +851,11 @@
844851
char * msg;
845852
va_start(vargs,fmt);
846853
msg = vmprintf(fmt,vargs);
847854
va_end(vargs);
848855
cson_object_set(obj,"text", cson_value_new_string(msg,strlen(msg)));
849
- free(msg);
856
+ fossil_free(msg);
850857
}
851858
}
852859
853860
/*
854861
** Splits zStr (which must not be NULL) into tokens separated by the
@@ -1623,11 +1630,11 @@
16231630
**
16241631
** code must be in the inclusive range 1000..9999.
16251632
*/
16261633
int json_set_err( int code, char const * fmt, ... ){
16271634
assert( (code>=1000) && (code<=9999) );
1628
- free(g.zErrMsg);
1635
+ fossil_free(g.zErrMsg);
16291636
g.json.resultCode = code;
16301637
if(!fmt || !*fmt){
16311638
g.zErrMsg = mprintf("%s", json_err_cstr(code));
16321639
}else{
16331640
va_list vargs;
@@ -1771,11 +1778,11 @@
17711778
char * tags = info_tags_of_checkin(rid, propagatingOnly);
17721779
if(tags){
17731780
if(*tags){
17741781
v = json_string_split2(tags,',',0);
17751782
}
1776
- free(tags);
1783
+ fossil_free(tags);
17771784
}
17781785
return v;
17791786
}
17801787
17811788
/*
@@ -2006,14 +2013,14 @@
20062013
jv = cson_value_new_object();
20072014
jo = cson_value_get_object(jv);
20082015
20092016
zTmp = db_get("project-name",NULL);
20102017
cson_object_set(jo, "projectName", json_new_string(zTmp));
2011
- free(zTmp);
2018
+ fossil_free(zTmp);
20122019
zTmp = db_get("project-description",NULL);
20132020
cson_object_set(jo, "projectDescription", json_new_string(zTmp));
2014
- free(zTmp);
2021
+ fossil_free(zTmp);
20152022
zTmp = NULL;
20162023
fsize = file_size(g.zRepositoryName, ExtFILE);
20172024
cson_object_set(jo, "repositorySize",
20182025
cson_value_new_integer((cson_int_t)fsize));
20192026
20202027
--- src/json.c
+++ src/json.c
@@ -288,11 +288,11 @@
288 va_list vargs;
289 va_start(vargs,fmt);
290 zStr = vmprintf(fmt,vargs);
291 va_end(vargs);
292 v = cson_value_new_string(zStr, strlen(zStr));
293 free(zStr);
294 return v;
295 }
296
297 cson_value * json_new_int( i64 v ){
298 return cson_value_new_integer((cson_int_t)v);
@@ -630,13 +630,20 @@
630 ** is not called to flush the output.
631 **
632 ** If g.json.jsonp is not NULL then the content type is set to
633 ** text/javascript and the output is wrapped in a jsonp
634 ** wrapper.
 
 
 
 
 
635 */
636 void json_send_response( cson_value const * pResponse ){
 
637 assert( NULL != pResponse );
 
638 if( g.isHTTP ){
639 cgi_reset_content();
640 if( g.json.jsonp ){
641 cgi_set_content_type("text/javascript");
642 cgi_printf("%s(",g.json.jsonp);
@@ -844,11 +851,11 @@
844 char * msg;
845 va_start(vargs,fmt);
846 msg = vmprintf(fmt,vargs);
847 va_end(vargs);
848 cson_object_set(obj,"text", cson_value_new_string(msg,strlen(msg)));
849 free(msg);
850 }
851 }
852
853 /*
854 ** Splits zStr (which must not be NULL) into tokens separated by the
@@ -1623,11 +1630,11 @@
1623 **
1624 ** code must be in the inclusive range 1000..9999.
1625 */
1626 int json_set_err( int code, char const * fmt, ... ){
1627 assert( (code>=1000) && (code<=9999) );
1628 free(g.zErrMsg);
1629 g.json.resultCode = code;
1630 if(!fmt || !*fmt){
1631 g.zErrMsg = mprintf("%s", json_err_cstr(code));
1632 }else{
1633 va_list vargs;
@@ -1771,11 +1778,11 @@
1771 char * tags = info_tags_of_checkin(rid, propagatingOnly);
1772 if(tags){
1773 if(*tags){
1774 v = json_string_split2(tags,',',0);
1775 }
1776 free(tags);
1777 }
1778 return v;
1779 }
1780
1781 /*
@@ -2006,14 +2013,14 @@
2006 jv = cson_value_new_object();
2007 jo = cson_value_get_object(jv);
2008
2009 zTmp = db_get("project-name",NULL);
2010 cson_object_set(jo, "projectName", json_new_string(zTmp));
2011 free(zTmp);
2012 zTmp = db_get("project-description",NULL);
2013 cson_object_set(jo, "projectDescription", json_new_string(zTmp));
2014 free(zTmp);
2015 zTmp = NULL;
2016 fsize = file_size(g.zRepositoryName, ExtFILE);
2017 cson_object_set(jo, "repositorySize",
2018 cson_value_new_integer((cson_int_t)fsize));
2019
2020
--- src/json.c
+++ src/json.c
@@ -288,11 +288,11 @@
288 va_list vargs;
289 va_start(vargs,fmt);
290 zStr = vmprintf(fmt,vargs);
291 va_end(vargs);
292 v = cson_value_new_string(zStr, strlen(zStr));
293 fossil_free(zStr);
294 return v;
295 }
296
297 cson_value * json_new_int( i64 v ){
298 return cson_value_new_integer((cson_int_t)v);
@@ -630,13 +630,20 @@
630 ** is not called to flush the output.
631 **
632 ** If g.json.jsonp is not NULL then the content type is set to
633 ** text/javascript and the output is wrapped in a jsonp
634 ** wrapper.
635 **
636 ** This function works only the first time it is called. It "should
637 ** not" ever be called more than once but certain calling
638 ** constellations might trigger that, in which case the second and
639 ** subsequent calls are no-ops.
640 */
641 void json_send_response( cson_value const * pResponse ){
642 static int once = 0;
643 assert( NULL != pResponse );
644 if( once++ ) return;
645 if( g.isHTTP ){
646 cgi_reset_content();
647 if( g.json.jsonp ){
648 cgi_set_content_type("text/javascript");
649 cgi_printf("%s(",g.json.jsonp);
@@ -844,11 +851,11 @@
851 char * msg;
852 va_start(vargs,fmt);
853 msg = vmprintf(fmt,vargs);
854 va_end(vargs);
855 cson_object_set(obj,"text", cson_value_new_string(msg,strlen(msg)));
856 fossil_free(msg);
857 }
858 }
859
860 /*
861 ** Splits zStr (which must not be NULL) into tokens separated by the
@@ -1623,11 +1630,11 @@
1630 **
1631 ** code must be in the inclusive range 1000..9999.
1632 */
1633 int json_set_err( int code, char const * fmt, ... ){
1634 assert( (code>=1000) && (code<=9999) );
1635 fossil_free(g.zErrMsg);
1636 g.json.resultCode = code;
1637 if(!fmt || !*fmt){
1638 g.zErrMsg = mprintf("%s", json_err_cstr(code));
1639 }else{
1640 va_list vargs;
@@ -1771,11 +1778,11 @@
1778 char * tags = info_tags_of_checkin(rid, propagatingOnly);
1779 if(tags){
1780 if(*tags){
1781 v = json_string_split2(tags,',',0);
1782 }
1783 fossil_free(tags);
1784 }
1785 return v;
1786 }
1787
1788 /*
@@ -2006,14 +2013,14 @@
2013 jv = cson_value_new_object();
2014 jo = cson_value_get_object(jv);
2015
2016 zTmp = db_get("project-name",NULL);
2017 cson_object_set(jo, "projectName", json_new_string(zTmp));
2018 fossil_free(zTmp);
2019 zTmp = db_get("project-description",NULL);
2020 cson_object_set(jo, "projectDescription", json_new_string(zTmp));
2021 fossil_free(zTmp);
2022 zTmp = NULL;
2023 fsize = file_size(g.zRepositoryName, ExtFILE);
2024 cson_object_set(jo, "repositorySize",
2025 cson_value_new_integer((cson_int_t)fsize));
2026
2027
--- src/json_config.c
+++ src/json_config.c
@@ -35,15 +35,17 @@
3535
/* Last entry MUST have a NULL name. */
3636
{NULL,NULL,0}
3737
};
3838
3939
static cson_value * json_settings_get(void);
40
+static cson_value * json_settings_set(void);
4041
/*
4142
** Mapping of /json/settings/XXX commands/paths to callbacks.
4243
*/
4344
static const JsonPageDef JsonPageDefs_Settings[] = {
4445
{"get", json_settings_get, 0},
46
+{"set", json_settings_set, 0},
4547
/* Last entry MUST have a NULL name. */
4648
{NULL,NULL,0}
4749
};
4850
4951
@@ -318,7 +320,85 @@
318320
db_finalize(&q);
319321
db_finalize(&qFoci);
320322
fossil_free(zUuid);
321323
return cson_object_value(pay);
322324
}
325
+
326
+/*
327
+** Impl of /json/settings/set.
328
+**
329
+** Input payload is an object mapping setting names to values. All
330
+** values are set in the repository.config table. It has no response
331
+** payload.
332
+*/
333
+static cson_value * json_settings_set(void){
334
+ Stmt q = empty_Stmt; /* Config-set query */
335
+ cson_object_iterator objIter = cson_object_iterator_empty;
336
+ cson_kvp * pKvp;
337
+ int nErr = 0, nProp = 0;
338
+
339
+ if( 0==g.perm.Setup ){
340
+ json_set_err( FSL_JSON_E_DENIED, "Setting settings requires 's' access." );
341
+ return NULL;
342
+ }
343
+ else if( 0==g.json.reqPayload.o ){
344
+ json_set_err(FSL_JSON_E_MISSING_ARGS,
345
+ "Missing payload of setting-to-value mappings.");
346
+ return NULL;
347
+ }
348
+
349
+ db_unprotect(PROTECT_CONFIG);
350
+ db_prepare(&q,
351
+ "INSERT OR REPLACE INTO config (name, value, mtime) "
352
+ "VALUES(:name, :value, CAST(strftime('%%s') AS INT))"
353
+ );
354
+ db_begin_transaction();
355
+ cson_object_iter_init( g.json.reqPayload.o, &objIter );
356
+ while( (pKvp = cson_object_iter_next(&objIter)) ){
357
+ char const * zKey = cson_string_cstr( cson_kvp_key(pKvp) );
358
+ cson_value * pVal;
359
+ const Setting *pSetting = db_find_setting( zKey, 0 );
360
+ if( 0==pSetting ){
361
+ nErr = json_set_err(FSL_JSON_E_INVALID_ARGS,
362
+ "Unknown setting: %s", zKey);
363
+ break;
364
+ }
365
+ pVal = cson_kvp_value(pKvp);
366
+ switch( cson_value_type_id(pVal) ){
367
+ case CSON_TYPE_NULL:
368
+ db_multi_exec("DELETE FROM config WHERE name=%Q", pSetting->name);
369
+ continue;
370
+ case CSON_TYPE_BOOL:
371
+ db_bind_int(&q, ":value", cson_value_get_bool(pVal) ? 1 : 0);
372
+ break;
373
+ case CSON_TYPE_INTEGER:
374
+ db_bind_int64(&q, ":value", cson_value_get_integer(pVal));
375
+ break;
376
+ case CSON_TYPE_DOUBLE:
377
+ db_bind_double(&q, ":value", cson_value_get_double(pVal));
378
+ break;
379
+ case CSON_TYPE_STRING:
380
+ db_bind_text(&q, ":value", cson_value_get_cstr(pVal));
381
+ break;
382
+ default:
383
+ nErr = json_set_err(FSL_JSON_E_USAGE,
384
+ "Invalid value type for setting '%s'.",
385
+ pSetting->name);
386
+ break;
387
+ }
388
+ if( 0!=nErr ) break;
389
+ db_bind_text(&q, ":name", zKey);
390
+ db_step(&q);
391
+ db_reset(&q);
392
+ ++nProp;
393
+ }
394
+ db_finalize(&q);
395
+ if( 0==nErr && 0==nProp ){
396
+ nErr = json_set_err(FSL_JSON_E_INVALID_ARGS,
397
+ "Payload contains no settings to set.");
398
+ }
399
+ db_end_transaction(nErr);
400
+ db_protect_pop();
401
+ return NULL;
402
+}
323403
324404
#endif /* FOSSIL_ENABLE_JSON */
325405
--- src/json_config.c
+++ src/json_config.c
@@ -35,15 +35,17 @@
35 /* Last entry MUST have a NULL name. */
36 {NULL,NULL,0}
37 };
38
39 static cson_value * json_settings_get(void);
 
40 /*
41 ** Mapping of /json/settings/XXX commands/paths to callbacks.
42 */
43 static const JsonPageDef JsonPageDefs_Settings[] = {
44 {"get", json_settings_get, 0},
 
45 /* Last entry MUST have a NULL name. */
46 {NULL,NULL,0}
47 };
48
49
@@ -318,7 +320,85 @@
318 db_finalize(&q);
319 db_finalize(&qFoci);
320 fossil_free(zUuid);
321 return cson_object_value(pay);
322 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
323
324 #endif /* FOSSIL_ENABLE_JSON */
325
--- src/json_config.c
+++ src/json_config.c
@@ -35,15 +35,17 @@
35 /* Last entry MUST have a NULL name. */
36 {NULL,NULL,0}
37 };
38
39 static cson_value * json_settings_get(void);
40 static cson_value * json_settings_set(void);
41 /*
42 ** Mapping of /json/settings/XXX commands/paths to callbacks.
43 */
44 static const JsonPageDef JsonPageDefs_Settings[] = {
45 {"get", json_settings_get, 0},
46 {"set", json_settings_set, 0},
47 /* Last entry MUST have a NULL name. */
48 {NULL,NULL,0}
49 };
50
51
@@ -318,7 +320,85 @@
320 db_finalize(&q);
321 db_finalize(&qFoci);
322 fossil_free(zUuid);
323 return cson_object_value(pay);
324 }
325
326 /*
327 ** Impl of /json/settings/set.
328 **
329 ** Input payload is an object mapping setting names to values. All
330 ** values are set in the repository.config table. It has no response
331 ** payload.
332 */
333 static cson_value * json_settings_set(void){
334 Stmt q = empty_Stmt; /* Config-set query */
335 cson_object_iterator objIter = cson_object_iterator_empty;
336 cson_kvp * pKvp;
337 int nErr = 0, nProp = 0;
338
339 if( 0==g.perm.Setup ){
340 json_set_err( FSL_JSON_E_DENIED, "Setting settings requires 's' access." );
341 return NULL;
342 }
343 else if( 0==g.json.reqPayload.o ){
344 json_set_err(FSL_JSON_E_MISSING_ARGS,
345 "Missing payload of setting-to-value mappings.");
346 return NULL;
347 }
348
349 db_unprotect(PROTECT_CONFIG);
350 db_prepare(&q,
351 "INSERT OR REPLACE INTO config (name, value, mtime) "
352 "VALUES(:name, :value, CAST(strftime('%%s') AS INT))"
353 );
354 db_begin_transaction();
355 cson_object_iter_init( g.json.reqPayload.o, &objIter );
356 while( (pKvp = cson_object_iter_next(&objIter)) ){
357 char const * zKey = cson_string_cstr( cson_kvp_key(pKvp) );
358 cson_value * pVal;
359 const Setting *pSetting = db_find_setting( zKey, 0 );
360 if( 0==pSetting ){
361 nErr = json_set_err(FSL_JSON_E_INVALID_ARGS,
362 "Unknown setting: %s", zKey);
363 break;
364 }
365 pVal = cson_kvp_value(pKvp);
366 switch( cson_value_type_id(pVal) ){
367 case CSON_TYPE_NULL:
368 db_multi_exec("DELETE FROM config WHERE name=%Q", pSetting->name);
369 continue;
370 case CSON_TYPE_BOOL:
371 db_bind_int(&q, ":value", cson_value_get_bool(pVal) ? 1 : 0);
372 break;
373 case CSON_TYPE_INTEGER:
374 db_bind_int64(&q, ":value", cson_value_get_integer(pVal));
375 break;
376 case CSON_TYPE_DOUBLE:
377 db_bind_double(&q, ":value", cson_value_get_double(pVal));
378 break;
379 case CSON_TYPE_STRING:
380 db_bind_text(&q, ":value", cson_value_get_cstr(pVal));
381 break;
382 default:
383 nErr = json_set_err(FSL_JSON_E_USAGE,
384 "Invalid value type for setting '%s'.",
385 pSetting->name);
386 break;
387 }
388 if( 0!=nErr ) break;
389 db_bind_text(&q, ":name", zKey);
390 db_step(&q);
391 db_reset(&q);
392 ++nProp;
393 }
394 db_finalize(&q);
395 if( 0==nErr && 0==nProp ){
396 nErr = json_set_err(FSL_JSON_E_INVALID_ARGS,
397 "Payload contains no settings to set.");
398 }
399 db_end_transaction(nErr);
400 db_protect_pop();
401 return NULL;
402 }
403
404 #endif /* FOSSIL_ENABLE_JSON */
405
--- www/json-api/api-settings.md
+++ www/json-api/api-settings.md
@@ -2,11 +2,11 @@
22
([&#x2b11;JSON API Index](index.md))
33
44
Jump to:
55
66
* [Fetch Settings](#get)
7
-* Set Settings is TODO
7
+* [Set Settings](#set)
88
99
---
1010
1111
<a id="get"></a>
1212
# Fetch Settings
@@ -80,5 +80,48 @@
8080
8181
Note that settings are internally stored as strings, even if they're
8282
semantically treated as numbers. The way settings are stored and
8383
handled does not give us enough information to recognize their exact
8484
data type here so they are passed on as-is.
85
+
86
+
87
+<a id="set"></a>
88
+# Set Settings
89
+
90
+**Status:** Implemented 20230120
91
+
92
+**Required permissions:** "s"
93
+
94
+**Request:** `/json/settings/set`
95
+
96
+This call requires that the input payload be an object containing a
97
+mapping of fossil-known configuration keys (case-sensitive) to
98
+values. For example:
99
+
100
+```json
101
+{
102
+ "editor": "emacs",
103
+ "admin-log": true,
104
+ "auto-captcha": false
105
+}
106
+```
107
+
108
+It iterates through each property, which must have a data type of
109
+`null`, boolean, number, or string. A value of `null` _unsets_
110
+(deletes) the setting. Boolean values are stored as integer 0
111
+or 1. All other types are stored as-is. It only updates the
112
+`repository.config` database and never updates a checkout or global
113
+config database, nor is it capable of updating versioned settings
114
+(^Updating versioned settings requires creating a full check-in.).
115
+
116
+It has no result payload but this may be changed in the future it
117
+practice shows that it should return something specific.
118
+
119
+Error responses include:
120
+
121
+- `FOSSIL-2002`: called without "setup" permissions.
122
+- `FOSSIL-3002`: called without a payload object.
123
+- `FOSSIL-3001`: passed an unknown config option.
124
+- `FOSSIL-3000`: a value has an unsupported data type.
125
+
126
+If an error is triggered, any settings made by this call up until that
127
+point are discarded.
85128
--- www/json-api/api-settings.md
+++ www/json-api/api-settings.md
@@ -2,11 +2,11 @@
2 ([&#x2b11;JSON API Index](index.md))
3
4 Jump to:
5
6 * [Fetch Settings](#get)
7 * Set Settings is TODO
8
9 ---
10
11 <a id="get"></a>
12 # Fetch Settings
@@ -80,5 +80,48 @@
80
81 Note that settings are internally stored as strings, even if they're
82 semantically treated as numbers. The way settings are stored and
83 handled does not give us enough information to recognize their exact
84 data type here so they are passed on as-is.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
--- www/json-api/api-settings.md
+++ www/json-api/api-settings.md
@@ -2,11 +2,11 @@
2 ([&#x2b11;JSON API Index](index.md))
3
4 Jump to:
5
6 * [Fetch Settings](#get)
7 * [Set Settings](#set)
8
9 ---
10
11 <a id="get"></a>
12 # Fetch Settings
@@ -80,5 +80,48 @@
80
81 Note that settings are internally stored as strings, even if they're
82 semantically treated as numbers. The way settings are stored and
83 handled does not give us enough information to recognize their exact
84 data type here so they are passed on as-is.
85
86
87 <a id="set"></a>
88 # Set Settings
89
90 **Status:** Implemented 20230120
91
92 **Required permissions:** "s"
93
94 **Request:** `/json/settings/set`
95
96 This call requires that the input payload be an object containing a
97 mapping of fossil-known configuration keys (case-sensitive) to
98 values. For example:
99
100 ```json
101 {
102 "editor": "emacs",
103 "admin-log": true,
104 "auto-captcha": false
105 }
106 ```
107
108 It iterates through each property, which must have a data type of
109 `null`, boolean, number, or string. A value of `null` _unsets_
110 (deletes) the setting. Boolean values are stored as integer 0
111 or 1. All other types are stored as-is. It only updates the
112 `repository.config` database and never updates a checkout or global
113 config database, nor is it capable of updating versioned settings
114 (^Updating versioned settings requires creating a full check-in.).
115
116 It has no result payload but this may be changed in the future it
117 practice shows that it should return something specific.
118
119 Error responses include:
120
121 - `FOSSIL-2002`: called without "setup" permissions.
122 - `FOSSIL-3002`: called without a payload object.
123 - `FOSSIL-3001`: passed an unknown config option.
124 - `FOSSIL-3000`: a value has an unsupported data type.
125
126 If an error is triggered, any settings made by this call up until that
127 point are discarded.
128

Keyboard Shortcuts

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